From 861eb45025abd46367b17e0ec30600db7c898ae6 Mon Sep 17 00:00:00 2001 From: wu ming <569287825@qq.com> Date: Fri, 2 Dec 2016 19:43:43 +0800 Subject: [PATCH] my first commit --- .gitignore | 22 + .travis.yml | 19 + CONTRIBUTING.md | 11 + LICENSE | 202 + README.md | 73 + pom.xml | 916 + presto-base-jdbc/pom.xml | 201 + .../presto/plugin/jdbc/BaseJdbcClient.java | 540 + .../presto/plugin/jdbc/BaseJdbcConfig.java | 62 + .../presto/plugin/jdbc/JdbcClient.java | 62 + .../presto/plugin/jdbc/JdbcColumnHandle.java | 97 + .../presto/plugin/jdbc/JdbcConnector.java | 98 + .../plugin/jdbc/JdbcConnectorFactory.java | 76 + .../presto/plugin/jdbc/JdbcConnectorId.java | 53 + .../plugin/jdbc/JdbcHandleResolver.java | 84 + .../presto/plugin/jdbc/JdbcMetadata.java | 209 + .../plugin/jdbc/JdbcMetadataConfig.java | 35 + .../presto/plugin/jdbc/JdbcModule.java | 45 + .../plugin/jdbc/JdbcOutputTableHandle.java | 181 + .../presto/plugin/jdbc/JdbcPartition.java | 59 + .../presto/plugin/jdbc/JdbcPlugin.java | 63 + .../presto/plugin/jdbc/JdbcRecordCursor.java | 362 + .../presto/plugin/jdbc/JdbcRecordSet.java | 58 + .../plugin/jdbc/JdbcRecordSetProvider.java | 52 + .../presto/plugin/jdbc/JdbcRecordSink.java | 205 + .../plugin/jdbc/JdbcRecordSinkProvider.java | 48 + .../presto/plugin/jdbc/JdbcSplit.java | 211 + .../presto/plugin/jdbc/JdbcSplitManager.java | 67 + .../presto/plugin/jdbc/JdbcTableHandle.java | 109 + .../presto/plugin/jdbc/QueryBuilder.java | 199 + .../facebook/presto/plugin/jdbc/Types.java | 33 + .../plugin/jdbc/cache/JdbcCacheConfig.java | 89 + .../plugin/jdbc/cache/JdbcCacheSplit.java | 98 + .../plugin/jdbc/cache/JdbcJavaBean.java | 42 + .../plugin/jdbc/cache/JdbcResultCache.java | 227 + .../plugin/jdbc/subtable/JdbcLoadTread.java | 255 + .../jdbc/subtable/JdbcSubTableConfig.java | 90 + .../jdbc/subtable/JdbcSubTableManager.java | 297 + .../plugin/jdbc/subtable/PdboSplit.java | 284 + .../plugin/jdbc/subtable/PdboSplitSource.java | 129 + .../plugin/jdbc/subtable/PdboTableInfo.java | 141 + .../presto/plugin/jdbc/util/JdbcUtil.java | 176 + .../presto/plugin/jdbc/util/PdboMetadata.java | 79 + .../presto/plugin/jdbc/JdbcQueryRunner.java | 94 + .../presto/plugin/jdbc/MetadataUtil.java | 78 + .../plugin/jdbc/TestBaseJdbcConfig.java | 49 + .../presto/plugin/jdbc/TestJdbcClient.java | 75 + .../plugin/jdbc/TestJdbcColumnHandle.java | 53 + .../plugin/jdbc/TestJdbcConnectorFactory.java | 33 + .../jdbc/TestJdbcDistributedQueries.java | 29 + .../plugin/jdbc/TestJdbcHandleResolver.java | 60 + .../jdbc/TestJdbcIntegrationSmokeTest.java | 29 + .../presto/plugin/jdbc/TestJdbcMetadata.java | 190 + .../plugin/jdbc/TestJdbcMetadataConfig.java | 46 + .../jdbc/TestJdbcOutputTableHandle.java | 44 + .../presto/plugin/jdbc/TestJdbcRecordSet.java | 154 + .../jdbc/TestJdbcRecordSetProvider.java | 191 + .../presto/plugin/jdbc/TestJdbcSplit.java | 60 + .../plugin/jdbc/TestJdbcTableHandle.java | 52 + .../presto/plugin/jdbc/TestingDatabase.java | 113 + .../plugin/jdbc/TestingH2JdbcModule.java | 52 + presto-benchmark-driver/pom.xml | 127 + .../benchmark/driver/BenchmarkDriver.java | 89 + .../BenchmarkDriverExecutionException.java | 25 + .../driver/BenchmarkDriverOptions.java | 217 + .../benchmark/driver/BenchmarkQuery.java | 91 + .../driver/BenchmarkQueryResult.java | 124 + .../driver/BenchmarkQueryRunner.java | 279 + .../driver/BenchmarkResultsPrinter.java | 115 + .../driver/BenchmarkResultsStore.java | 19 + .../benchmark/driver/BenchmarkSchema.java | 55 + .../driver/PrestoBenchmarkDriver.java | 172 + .../benchmark/driver/RegexTemplate.java | 103 + .../presto/benchmark/driver/Stat.java | 58 + .../presto/benchmark/driver/Suite.java | 157 + .../benchmark/driver/RegexTemplateTest.java | 43 + .../src/test/resources/groups.json | 16 + .../test/resources/sql/live_sum_discount.sql | 5 + .../src/test/resources/sql/sum_discount.sql | 4 + .../src/test/resources/sql/sum_quantity.sql | 4 + presto-benchmark/pom.xml | 95 + .../presto/benchmark/AbstractBenchmark.java | 140 + .../benchmark/AbstractOperatorBenchmark.java | 145 + .../AbstractSimpleOperatorBenchmark.java | 60 + .../benchmark/AbstractSqlBenchmark.java | 46 + .../benchmark/ArrayComparisonBenchmark.java | 86 + .../benchmark/AverageBenchmarkResults.java | 60 + .../benchmark/BenchmarkQueryRunner.java | 66 + .../presto/benchmark/BenchmarkResultHook.java | 23 + .../presto/benchmark/BenchmarkSuite.java | 179 + .../benchmark/CountAggregationBenchmark.java | 48 + .../CountAggregationSqlBenchmark.java | 32 + .../CountWithFilterSqlBenchmark.java | 32 + .../DoubleSumAggregationBenchmark.java | 48 + .../presto/benchmark/FormatUtils.java | 147 + .../GroupByAggregationSqlBenchmark.java | 32 + .../GroupBySumWithArithmeticSqlBenchmark.java | 36 + .../presto/benchmark/HandTpchQuery1.java | 324 + .../presto/benchmark/HandTpchQuery6.java | 119 + .../benchmark/HashAggregationBenchmark.java | 61 + .../benchmark/HashBuildAndJoinBenchmark.java | 97 + .../presto/benchmark/HashBuildBenchmark.java | 53 + .../presto/benchmark/HashJoinBenchmark.java | 80 + .../JsonAvgBenchmarkResultWriter.java | 110 + .../benchmark/JsonBenchmarkResultWriter.java | 73 + .../LongMaxAggregationSqlBenchmark.java | 32 + .../benchmark/OdsBenchmarkResultWriter.java | 76 + .../presto/benchmark/OrderByBenchmark.java | 59 + .../benchmark/PredicateFilterBenchmark.java | 79 + .../PredicateFilterSqlBenchmark.java | 32 + .../benchmark/RawStreamingBenchmark.java | 44 + .../benchmark/RawStreamingSqlBenchmark.java | 32 + .../SimpleLineBenchmarkResultWriter.java | 57 + ...proximateCountDistinctDoubleBenchmark.java | 32 + ...ApproximateCountDistinctLongBenchmark.java | 32 + ...ximateCountDistinctVarBinaryBenchmark.java | 32 + .../SqlApproximatePercentileBenchmark.java | 32 + .../presto/benchmark/SqlBetweenBenchmark.java | 32 + .../benchmark/SqlDistinctMultipleFields.java | 32 + .../benchmark/SqlDistinctSingleField.java | 32 + .../SqlDoubleSumAggregationBenchmark.java | 32 + .../benchmark/SqlHashJoinBenchmark.java | 36 + .../presto/benchmark/SqlInBenchmark.java | 36 + .../SqlJoinWithPredicateBenchmark.java | 36 + .../presto/benchmark/SqlLikeBenchmark.java | 32 + .../benchmark/SqlRegexpLikeBenchmark.java | 32 + .../SqlSemiJoinInPredicateBenchmark.java | 36 + .../presto/benchmark/SqlTpchQuery1.java | 53 + .../presto/benchmark/SqlTpchQuery6.java | 39 + .../presto/benchmark/StatisticsBenchmark.java | 106 + .../benchmark/StructuredTypesBenchmark.java | 53 + .../presto/benchmark/Top100Benchmark.java | 52 + .../presto/benchmark/Top100SqlBenchmark.java | 32 + .../VarBinaryMaxAggregationSqlBenchmark.java | 32 + .../presto/benchmark/TestBenchmarks.java | 40 + presto-cassandra/pom.xml | 376 + .../presto/cassandra/BackoffRetryPolicy.java | 61 + .../CachingCassandraSchemaProvider.java | 352 + .../cassandra/CassandraClientConfig.java | 355 + .../cassandra/CassandraClientModule.java | 129 + .../cassandra/CassandraColumnHandle.java | 271 + .../presto/cassandra/CassandraConnector.java | 98 + .../cassandra/CassandraConnectorFactory.java | 84 + .../cassandra/CassandraConnectorId.java | 56 + .../CassandraConnectorRecordSinkProvider.java | 52 + .../cassandra/CassandraHandleResolver.java | 93 + .../presto/cassandra/CassandraMetadata.java | 342 + .../cassandra/CassandraOutputTableHandle.java | 108 + .../presto/cassandra/CassandraPartition.java | 86 + .../presto/cassandra/CassandraPlugin.java | 45 + .../cassandra/CassandraRecordCursor.java | 141 + .../presto/cassandra/CassandraRecordSet.java | 57 + .../cassandra/CassandraRecordSetProvider.java | 75 + .../presto/cassandra/CassandraRecordSink.java | 172 + .../presto/cassandra/CassandraSession.java | 464 + .../presto/cassandra/CassandraSplit.java | 148 + .../cassandra/CassandraSplitManager.java | 378 + .../presto/cassandra/CassandraTable.java | 93 + .../cassandra/CassandraTableHandle.java | 92 + .../cassandra/CassandraThriftClient.java | 100 + .../CassandraThriftConnectionFactory.java | 112 + .../cassandra/CassandraTokenSplitManager.java | 169 + .../presto/cassandra/CassandraType.java | 479 + .../CassandraTypeWithTypeArguments.java | 54 + .../presto/cassandra/ExtraColumnMetadata.java | 81 + .../presto/cassandra/ForCassandra.java | 31 + .../presto/cassandra/FullCassandraType.java | 23 + .../cassandra/RebindSafeMBeanServer.java | 333 + .../presto/cassandra/RetryDriver.java | 101 + .../presto/cassandra/RetryPolicyType.java | 41 + .../cassandra/util/CassandraCqlUtils.java | 189 + .../cassandra/util/HostAddressFactory.java | 61 + .../facebook/presto/cassandra/util/Types.java | 33 + .../com/datastax/driver/core/RowUtil.java | 35 + .../com/datastax/driver/core/TestHost.java | 25 + .../cassandra/CassandraQueryRunner.java | 99 + .../cassandra/CassandraTestingUtils.java | 191 + .../cassandra/MockCassandraSession.java | 158 + .../TestCachingCassandraSchemaProvider.java | 165 + .../cassandra/TestCassandraClientConfig.java | 112 + .../cassandra/TestCassandraColumnHandle.java | 67 + .../cassandra/TestCassandraConnector.java | 276 + .../cassandra/TestCassandraDistributed.java | 81 + .../TestCassandraIntegrationSmokeTest.java | 46 + .../presto/cassandra/TestCassandraSplit.java | 68 + .../cassandra/TestCassandraTableHandle.java | 36 + .../presto/cassandra/TestCassandraType.java | 58 + .../cassandra/TestJsonCassandraHandles.java | 157 + .../cassandra/util/TestCassandraCqlUtils.java | 85 + .../util/TestHostAddressFactory.java | 49 + .../src/test/resources/cu-cassandra.yaml | 564 + presto-cli/pom.xml | 152 + .../presto/cli/AlignedTablePrinter.java | 192 + .../facebook/presto/cli/ClientOptions.java | 219 + .../com/facebook/presto/cli/Completion.java | 58 + .../java/com/facebook/presto/cli/Console.java | 336 + .../facebook/presto/cli/ConsolePrinter.java | 121 + .../com/facebook/presto/cli/CsvPrinter.java | 83 + .../com/facebook/presto/cli/FormatUtils.java | 232 + .../java/com/facebook/presto/cli/Help.java | 38 + .../com/facebook/presto/cli/LineReader.java | 74 + .../com/facebook/presto/cli/NullPrinter.java | 30 + .../facebook/presto/cli/OutputHandler.java | 97 + .../facebook/presto/cli/OutputPrinter.java | 26 + .../java/com/facebook/presto/cli/Pager.java | 154 + .../com/facebook/presto/cli/PerfTest.java | 337 + .../java/com/facebook/presto/cli/Presto.java | 34 + .../java/com/facebook/presto/cli/Query.java | 321 + .../presto/cli/QueryAbortedException.java | 25 + .../com/facebook/presto/cli/QueryRunner.java | 94 + .../facebook/presto/cli/StatusPrinter.java | 398 + .../presto/cli/TableNameCompleter.java | 154 + .../com/facebook/presto/cli/TsvPrinter.java | 110 + .../facebook/presto/cli/VersionOption.java | 33 + .../presto/cli/VerticalRecordPrinter.java | 101 + .../presto/cli/TestAlignedTablePrinter.java | 141 + .../presto/cli/TestClientOptions.java | 157 + .../facebook/presto/cli/TestCsvPrinter.java | 92 + .../facebook/presto/cli/TestTsvPrinter.java | 89 + .../presto/cli/TestVerticalRecordPrinter.java | 171 + presto-client/pom.xml | 66 + .../facebook/presto/client/ClientSession.java | 168 + .../presto/client/ClientTypeSignature.java | 144 + .../com/facebook/presto/client/Column.java | 58 + .../facebook/presto/client/ErrorLocation.java | 62 + .../facebook/presto/client/FailureInfo.java | 174 + .../facebook/presto/client/PrestoHeaders.java | 35 + .../facebook/presto/client/QueryError.java | 116 + .../facebook/presto/client/QueryResults.java | 285 + .../facebook/presto/client/StageStats.java | 312 + .../presto/client/StatementClient.java | 320 + .../presto/client/StatementStats.java | 293 + presto-docs/Makefile | 178 + presto-docs/pom.xml | 101 + presto-docs/src/main/assembly/docs.xml | 19 + presto-docs/src/main/assembly/sources.xml | 15 + presto-docs/src/main/resources/design.graffle | 7371 ++++ .../print/dark/FB_Presto_Logo_PRINT_DarkBG.ai | 2578 ++ .../light/FB_Presto_Logo_PRINT_LightBG.ai | 2534 ++ .../logo/web/fb/Presto_FB_System_Lockups.ai | 2848 ++ .../black/Presto_FB_Lockups_BLACK_BG-25.svg | 71 + .../black/Presto_FB_Lockups_BLACK_BG-26.svg | 70 + .../black/Presto_FB_Lockups_BLACK_BG-27.svg | 70 + .../black/Presto_FB_Lockups_BLACK_BG-28.svg | 47 + .../black/Presto_FB_Lockups_BLACK_BG-29.svg | 46 + .../black/Presto_FB_Lockups_BLACK_BG-30.svg | 61 + .../black/Presto_FB_Lockups_BLACK_BG-31.svg | 67 + .../black/Presto_FB_Lockups_BLACK_BG-32.svg | 66 + .../blue/Presto_FB_Lockups_FB_BLUE_BG-17.svg | 71 + .../blue/Presto_FB_Lockups_FB_BLUE_BG-18.svg | 70 + .../blue/Presto_FB_Lockups_FB_BLUE_BG-19.svg | 66 + .../blue/Presto_FB_Lockups_FB_BLUE_BG-20.svg | 41 + .../blue/Presto_FB_Lockups_FB_BLUE_BG-21.svg | 54 + .../blue/Presto_FB_Lockups_FB_BLUE_BG-22.svg | 62 + .../blue/Presto_FB_Lockups_FB_BLUE_BG-23.svg | 67 + .../blue/Presto_FB_Lockups_FB_BLUE_BG-24.svg | 66 + .../Presto_FB_Lockups_DARKBLUE_BG-09.svg | 71 + .../Presto_FB_Lockups_DARKBLUE_BG-10.svg | 69 + .../Presto_FB_Lockups_DARKBLUE_BG-11.svg | 70 + .../Presto_FB_Lockups_DARKBLUE_BG-12.svg | 47 + .../Presto_FB_Lockups_DARKBLUE_BG-13.svg | 46 + .../Presto_FB_Lockups_DARKBLUE_BG-14.svg | 62 + .../Presto_FB_Lockups_DARKBLUE_BG-15.svg | 67 + .../Presto_FB_Lockups_DARKBLUE_BG-16.svg | 67 + .../Presto_FB_Lockups_DARKGREY_BG-33.svg | 71 + .../Presto_FB_Lockups_DARKGREY_BG-34.svg | 70 + .../Presto_FB_Lockups_DARKGREY_BG-35.svg | 64 + .../Presto_FB_Lockups_DARKGREY_BG-36.svg | 41 + .../Presto_FB_Lockups_DARKGREY_BG-37.svg | 50 + .../Presto_FB_Lockups_DARKGREY_BG-38.svg | 62 + .../Presto_FB_Lockups_DARKGREY_BG-39.svg | 67 + .../Presto_FB_Lockups_DARKGREY_BG-40.svg | 67 + .../Presto_FB_Lockups_LIGHTGREY_BG-41.svg | 72 + .../Presto_FB_Lockups_LIGHTGREY_BG-42.svg | 70 + .../Presto_FB_Lockups_LIGHTGREY_BG-43.svg | 67 + .../Presto_FB_Lockups_LIGHTGREY_BG-44.svg | 41 + .../Presto_FB_Lockups_LIGHTGREY_BG-45.svg | 44 + .../Presto_FB_Lockups_LIGHTGREY_BG-46.svg | 63 + .../Presto_FB_Lockups_LIGHTGREY_BG-47.svg | 66 + .../Presto_FB_Lockups_LIGHTGREY_BG-48.svg | 66 + .../web/fb/white/Presto_FB_Lockups-01.svg | 70 + .../web/fb/white/Presto_FB_Lockups-02.svg | 71 + .../web/fb/white/Presto_FB_Lockups-03.svg | 67 + .../web/fb/white/Presto_FB_Lockups-04.svg | 41 + .../web/fb/white/Presto_FB_Lockups-05.svg | 44 + .../web/fb/white/Presto_FB_Lockups-06.svg | 64 + .../web/fb/white/Presto_FB_Lockups-07.svg | 67 + .../web/fb/white/Presto_FB_Lockups-08.svg | 66 + .../main/black/FB_Presto_Logo_BlackBG-01.svg | 61 + .../web/main/black/FB_Presto_Logo_BlackBG.ai | 2444 ++ .../main/blue/FB_Presto_Logo_DarkBlueBG.ai | 2411 ++ .../main/blue/FB_Presto_Logo_DarkBlueBG.svg | 61 + .../main/white/FB_Presto_Logo_WhiteBG-01.svg | 60 + .../web/main/white/FB_Presto_Logo_WhiteBG.ai | 2523 ++ presto-docs/src/main/sphinx/admin.rst | 8 + presto-docs/src/main/sphinx/admin/queue.rst | 105 + presto-docs/src/main/sphinx/conf.py | 97 + presto-docs/src/main/sphinx/connector.rst | 19 + .../src/main/sphinx/connector/cassandra.rst | 172 + .../src/main/sphinx/connector/hive.rst | 134 + presto-docs/src/main/sphinx/connector/jmx.rst | 55 + .../main/sphinx/connector/kafka-tutorial.rst | 613 + .../src/main/sphinx/connector/kafka.rst | 387 + .../src/main/sphinx/connector/mysql.rst | 52 + .../src/main/sphinx/connector/postgresql.rst | 57 + .../src/main/sphinx/connector/system.rst | 57 + .../src/main/sphinx/connector/tpch.rst | 58 + presto-docs/src/main/sphinx/develop.rst | 13 + .../src/main/sphinx/develop/example-http.rst | 150 + .../src/main/sphinx/develop/functions.rst | 122 + .../src/main/sphinx/develop/spi-overview.rst | 90 + presto-docs/src/main/sphinx/develop/types.rst | 34 + presto-docs/src/main/sphinx/ext/download.py | 60 + presto-docs/src/main/sphinx/functions.rst | 23 + .../src/main/sphinx/functions/aggregate.rst | 186 + .../src/main/sphinx/functions/array.rst | 60 + .../src/main/sphinx/functions/binary.rst | 35 + .../src/main/sphinx/functions/color.rst | 68 + .../src/main/sphinx/functions/comparison.rst | 122 + .../src/main/sphinx/functions/conditional.rst | 88 + .../src/main/sphinx/functions/conversion.rst | 23 + .../src/main/sphinx/functions/datetime.rst | 328 + .../src/main/sphinx/functions/json.rst | 63 + .../src/main/sphinx/functions/logical.rst | 68 + presto-docs/src/main/sphinx/functions/map.rst | 34 + .../src/main/sphinx/functions/math.rst | 176 + .../src/main/sphinx/functions/regexp.rst | 91 + .../src/main/sphinx/functions/string.rst | 130 + presto-docs/src/main/sphinx/functions/url.rst | 44 + .../src/main/sphinx/functions/window.rst | 123 + .../sphinx/images/functions_color_bar.png | Bin 0 -> 49267 bytes presto-docs/src/main/sphinx/index.rst | 18 + presto-docs/src/main/sphinx/installation.rst | 12 + .../sphinx/installation/benchmark-driver.rst | 97 + .../src/main/sphinx/installation/cli.rst | 23 + .../main/sphinx/installation/deployment.rst | 257 + .../src/main/sphinx/installation/jdbc.rst | 21 + .../src/main/sphinx/installation/verifier.rst | 45 + presto-docs/src/main/sphinx/language.rst | 8 + .../src/main/sphinx/language/types.rst | 118 + presto-docs/src/main/sphinx/migration.rst | 9 + .../src/main/sphinx/migration/from-hive.rst | 134 + presto-docs/src/main/sphinx/overview.rst | 11 + .../src/main/sphinx/overview/concepts.rst | 278 + .../src/main/sphinx/overview/use-cases.rst | 46 + presto-docs/src/main/sphinx/release.rst | 61 + .../src/main/sphinx/release/release-0.100.rst | 29 + .../src/main/sphinx/release/release-0.101.rst | 80 + .../src/main/sphinx/release/release-0.102.rst | 53 + .../src/main/sphinx/release/release-0.103.rst | 55 + .../src/main/sphinx/release/release-0.104.rst | 30 + .../src/main/sphinx/release/release-0.105.rst | 21 + .../src/main/sphinx/release/release-0.106.rst | 15 + .../src/main/sphinx/release/release-0.107.rst | 15 + .../src/main/sphinx/release/release-0.54.rst | 36 + .../src/main/sphinx/release/release-0.55.rst | 109 + .../src/main/sphinx/release/release-0.56.rst | 36 + .../src/main/sphinx/release/release-0.57.rst | 56 + .../src/main/sphinx/release/release-0.58.rst | 16 + .../src/main/sphinx/release/release-0.59.rst | 6 + .../src/main/sphinx/release/release-0.60.rst | 152 + .../src/main/sphinx/release/release-0.61.rst | 111 + .../src/main/sphinx/release/release-0.62.rst | 13 + .../src/main/sphinx/release/release-0.63.rst | 9 + .../src/main/sphinx/release/release-0.64.rst | 14 + .../src/main/sphinx/release/release-0.65.rst | 7 + .../src/main/sphinx/release/release-0.66.rst | 189 + .../src/main/sphinx/release/release-0.67.rst | 19 + .../src/main/sphinx/release/release-0.68.rst | 10 + .../src/main/sphinx/release/release-0.69.rst | 102 + .../src/main/sphinx/release/release-0.70.rst | 103 + .../src/main/sphinx/release/release-0.71.rst | 8 + .../src/main/sphinx/release/release-0.72.rst | 6 + .../src/main/sphinx/release/release-0.73.rst | 18 + .../src/main/sphinx/release/release-0.74.rst | 32 + .../src/main/sphinx/release/release-0.75.rst | 111 + .../src/main/sphinx/release/release-0.76.rst | 74 + .../src/main/sphinx/release/release-0.77.rst | 42 + .../src/main/sphinx/release/release-0.78.rst | 59 + .../src/main/sphinx/release/release-0.79.rst | 20 + .../src/main/sphinx/release/release-0.80.rst | 98 + .../src/main/sphinx/release/release-0.81.rst | 15 + .../src/main/sphinx/release/release-0.82.rst | 11 + .../src/main/sphinx/release/release-0.83.rst | 20 + .../src/main/sphinx/release/release-0.84.rst | 10 + .../src/main/sphinx/release/release-0.85.rst | 6 + .../src/main/sphinx/release/release-0.86.rst | 27 + .../src/main/sphinx/release/release-0.87.rst | 9 + .../src/main/sphinx/release/release-0.88.rst | 18 + .../src/main/sphinx/release/release-0.89.rst | 21 + .../src/main/sphinx/release/release-0.90.rst | 61 + .../src/main/sphinx/release/release-0.91.rst | 10 + .../src/main/sphinx/release/release-0.92.rst | 8 + .../src/main/sphinx/release/release-0.93.rst | 40 + .../src/main/sphinx/release/release-0.94.rst | 25 + .../src/main/sphinx/release/release-0.95.rst | 8 + .../src/main/sphinx/release/release-0.96.rst | 22 + .../src/main/sphinx/release/release-0.97.rst | 20 + .../src/main/sphinx/release/release-0.98.rst | 35 + .../src/main/sphinx/release/release-0.99.rst | 10 + presto-docs/src/main/sphinx/rest.rst | 69 + presto-docs/src/main/sphinx/rest/execute.rst | 63 + presto-docs/src/main/sphinx/rest/node.rst | 110 + presto-docs/src/main/sphinx/rest/query.rst | 75 + presto-docs/src/main/sphinx/rest/stage.rst | 11 + .../src/main/sphinx/rest/statement.rst | 229 + presto-docs/src/main/sphinx/rest/task.rst | 317 + presto-docs/src/main/sphinx/sql.rst | 30 + .../src/main/sphinx/sql/alter-table.rst | 27 + .../src/main/sphinx/sql/create-table-as.rst | 26 + .../src/main/sphinx/sql/create-table.rst | 30 + .../src/main/sphinx/sql/create-view.rst | 43 + presto-docs/src/main/sphinx/sql/delete.rst | 32 + presto-docs/src/main/sphinx/sql/describe.rst | 34 + .../src/main/sphinx/sql/drop-table.rst | 30 + presto-docs/src/main/sphinx/sql/drop-view.rst | 29 + presto-docs/src/main/sphinx/sql/explain.rst | 68 + presto-docs/src/main/sphinx/sql/insert.rst | 37 + .../src/main/sphinx/sql/reset-session.rst | 24 + presto-docs/src/main/sphinx/sql/select.rst | 315 + .../src/main/sphinx/sql/set-session.rst | 24 + .../src/main/sphinx/sql/show-catalogs.rst | 15 + .../src/main/sphinx/sql/show-columns.rst | 15 + .../src/main/sphinx/sql/show-functions.rst | 15 + .../src/main/sphinx/sql/show-partitions.rst | 33 + .../src/main/sphinx/sql/show-schemas.rst | 28 + .../src/main/sphinx/sql/show-session.rst | 15 + .../src/main/sphinx/sql/show-tables.rst | 16 + presto-docs/src/main/sphinx/sql/use.rst | 27 + .../src/main/sphinx/themes/presto/layout.html | 38 + .../themes/presto/static/alert_info_32.png | Bin 0 -> 1530 bytes .../themes/presto/static/alert_warning_32.png | Bin 0 -> 974 bytes .../sphinx/themes/presto/static/bullet.png | Bin 0 -> 109 bytes .../sphinx/themes/presto/static/presto.css_t | 114 + .../sphinx/themes/presto/static/presto.png | Bin 0 -> 10985 bytes .../src/main/sphinx/themes/presto/theme.conf | 24 + presto-example-http/pom.xml | 134 + .../presto/example/ExampleClient.java | 121 + .../presto/example/ExampleColumn.java | 79 + .../presto/example/ExampleColumnHandle.java | 108 + .../presto/example/ExampleConfig.java | 38 + .../presto/example/ExampleConnector.java | 88 + .../example/ExampleConnectorFactory.java | 72 + .../presto/example/ExampleConnectorId.java | 54 + .../presto/example/ExampleHandleResolver.java | 71 + .../presto/example/ExampleMetadata.java | 178 + .../presto/example/ExampleModule.java | 84 + .../presto/example/ExamplePartition.java | 65 + .../presto/example/ExamplePlugin.java | 60 + .../presto/example/ExampleRecordCursor.java | 160 + .../presto/example/ExampleRecordSet.java | 66 + .../example/ExampleRecordSetProvider.java | 55 + .../facebook/presto/example/ExampleSplit.java | 96 + .../presto/example/ExampleSplitManager.java | 85 + .../facebook/presto/example/ExampleTable.java | 75 + .../presto/example/ExampleTableHandle.java | 94 + .../com/facebook/presto/example/Types.java | 33 + .../presto/example/ExampleHttpServer.java | 94 + .../facebook/presto/example/MetadataUtil.java | 79 + .../presto/example/TestExampleClient.java | 50 + .../example/TestExampleColumnHandle.java | 57 + .../presto/example/TestExampleConfig.java | 44 + .../presto/example/TestExampleMetadata.java | 162 + .../presto/example/TestExampleRecordSet.java | 133 + .../example/TestExampleRecordSetProvider.java | 82 + .../presto/example/TestExampleSplit.java | 68 + .../presto/example/TestExampleTable.java | 52 + .../example/TestExampleTableHandle.java | 46 + .../example-data/example-metadata.json | 128 + .../resources/example-data/lineitem-1.csv | 299 + .../resources/example-data/lineitem-2.csv | 486 + .../test/resources/example-data/numbers-1.csv | 3 + .../test/resources/example-data/numbers-2.csv | 3 + .../test/resources/example-data/orders-1.csv | 100 + .../test/resources/example-data/orders-2.csv | 100 + presto-hive-cdh4/pom.xml | 91 + .../facebook/presto/hive/HiveCdh4Plugin.java | 23 + .../facebook/presto/hive/TestHiveClient.java | 31 + .../presto/hive/TestHiveClientS3.java | 38 + .../hive/TestSplitIteratorBackpressure.java | 31 + presto-hive-cdh5/pom.xml | 91 + .../facebook/presto/hive/HiveCdh5Plugin.java | 23 + .../facebook/presto/hive/TestHiveClient.java | 31 + .../presto/hive/TestHiveClientS3.java | 38 + .../hive/TestSplitIteratorBackpressure.java | 31 + presto-hive-hadoop1/pom.xml | 91 + .../presto/hive/HiveHadoop1Plugin.java | 23 + .../facebook/presto/hive/TestHiveClient.java | 31 + .../presto/hive/TestHiveClientS3.java | 38 + .../hive/TestSplitIteratorBackpressure.java | 31 + presto-hive-hadoop2/pom.xml | 91 + .../presto/hive/HiveHadoop2Plugin.java | 23 + .../facebook/presto/hive/TestHiveClient.java | 31 + .../presto/hive/TestHiveClientS3.java | 38 + .../hive/TestSplitIteratorBackpressure.java | 31 + presto-hive/pom.xml | 275 + .../hive/BackgroundHiveSplitLoader.java | 460 + .../hive/ColumnarBinaryHiveRecordCursor.java | 626 + ...olumnarBinaryHiveRecordCursorProvider.java | 71 + .../hive/ColumnarTextHiveRecordCursor.java | 568 + .../ColumnarTextHiveRecordCursorProvider.java | 71 + .../presto/hive/ConcurrentLazyQueue.java | 42 + .../facebook/presto/hive/DirectoryLister.java | 27 + .../hive/DiscoveryLocatedHiveCluster.java | 77 + .../facebook/presto/hive/ForHiveClient.java | 31 + .../presto/hive/ForHiveMetastore.java | 31 + .../presto/hive/GenericHiveRecordCursor.java | 487 + .../hive/GenericHiveRecordCursorProvider.java | 64 + .../presto/hive/HadoopDirectoryLister.java | 34 + .../presto/hive/HdfsConfiguration.java | 23 + .../presto/hive/HdfsConfigurationUpdater.java | 164 + .../facebook/presto/hive/HdfsEnvironment.java | 58 + .../presto/hive/HiveBooleanParser.java | 61 + .../facebook/presto/hive/HiveBucketing.java | 226 + .../presto/hive/HiveClientConfig.java | 702 + .../presto/hive/HiveClientModule.java | 156 + .../com/facebook/presto/hive/HiveCluster.java | 31 + .../presto/hive/HiveColumnHandle.java | 151 + .../facebook/presto/hive/HiveConnector.java | 108 + .../presto/hive/HiveConnectorFactory.java | 127 + .../facebook/presto/hive/HiveConnectorId.java | 56 + .../facebook/presto/hive/HiveErrorCode.java | 54 + .../presto/hive/HiveHandleResolver.java | 100 + .../presto/hive/HiveHdfsConfiguration.java | 53 + .../presto/hive/HiveInsertTableHandle.java | 240 + .../facebook/presto/hive/HiveMetadata.java | 1191 + .../presto/hive/HiveMetastoreClient.java | 46 + .../hive/HiveMetastoreClientFactory.java | 219 + .../presto/hive/HiveOutputTableHandle.java | 204 + .../presto/hive/HivePageSourceFactory.java | 40 + .../presto/hive/HivePageSourceProvider.java | 148 + .../facebook/presto/hive/HivePartition.java | 124 + .../presto/hive/HivePartitionKey.java | 94 + .../presto/hive/HivePartitionMetadata.java | 40 + .../com/facebook/presto/hive/HivePlugin.java | 81 + .../presto/hive/HivePluginConfig.java | 38 + .../presto/hive/HiveRecordCursor.java | 50 + .../presto/hive/HiveRecordCursorProvider.java | 42 + .../facebook/presto/hive/HiveRecordSink.java | 558 + .../presto/hive/HiveRecordSinkProvider.java | 57 + .../presto/hive/HiveSessionProperties.java | 125 + .../com/facebook/presto/hive/HiveSplit.java | 203 + .../facebook/presto/hive/HiveSplitLoader.java | 23 + .../presto/hive/HiveSplitManager.java | 547 + .../facebook/presto/hive/HiveSplitSource.java | 158 + .../presto/hive/HiveStorageFormat.java | 83 + .../facebook/presto/hive/HiveTableHandle.java | 102 + .../com/facebook/presto/hive/HiveType.java | 312 + .../com/facebook/presto/hive/HiveUtil.java | 555 + .../hive/HiveViewNotSupportedException.java | 41 + .../facebook/presto/hive/NamenodeStats.java | 82 + .../facebook/presto/hive/NumberParser.java | 58 + .../presto/hive/ParquetHiveRecordCursor.java | 1135 + .../hive/ParquetRecordCursorProvider.java | 105 + .../hive/PartitionOfflineException.java | 54 + .../facebook/presto/hive/PartitionOption.java | 55 + .../presto/hive/PrestoS3FileSystem.java | 886 + .../PrestoS3FileSystemMetricCollector.java | 64 + .../presto/hive/PrestoS3FileSystemStats.java | 306 + .../presto/hive/RebindSafeMBeanServer.java | 333 + .../com/facebook/presto/hive/RetryDriver.java | 144 + .../presto/hive/StaticHiveCluster.java | 48 + .../hive/TableAlreadyExistsException.java | 41 + .../presto/hive/TableOfflineException.java | 42 + .../presto/hive/UnpartitionedPartition.java | 39 + .../hive/ViewAlreadyExistsException.java | 41 + .../hive/metastore/CachingHiveMetastore.java | 903 + .../metastore/CachingHiveMetastoreStats.java | 117 + .../presto/hive/metastore/HiveMetastore.java | 62 + .../hive/metastore/HiveMetastoreApiStats.java | 94 + .../hive/metastore/InMemoryHiveMetastore.java | 191 + .../presto/hive/orc/DwrfHiveRecordCursor.java | 532 + .../hive/orc/DwrfPageSourceFactory.java | 111 + .../hive/orc/DwrfRecordCursorProvider.java | 173 + .../presto/hive/orc/HdfsOrcDataSource.java | 46 + .../presto/hive/orc/OrcHiveRecordCursor.java | 507 + .../presto/hive/orc/OrcPageSource.java | 484 + .../presto/hive/orc/OrcPageSourceFactory.java | 210 + .../hive/orc/OrcRecordCursorProvider.java | 146 + .../hive/rcfile/RcBinaryBlockLoader.java | 727 + .../presto/hive/rcfile/RcFileBlockLoader.java | 28 + .../presto/hive/rcfile/RcFilePageSource.java | 388 + .../hive/rcfile/RcFilePageSourceFactory.java | 147 + .../presto/hive/rcfile/RcTextBlockLoader.java | 534 + .../facebook/presto/hive/util/AsyncQueue.java | 115 + .../presto/hive/util/HiveFileIterator.java | 167 + .../facebook/presto/hive/util/SerDeUtils.java | 246 + .../com/facebook/presto/hive/util/Types.java | 49 + .../presto/hive/AbstractTestHiveClient.java | 1854 + .../presto/hive/AbstractTestHiveClientS3.java | 445 + .../hive/AbstractTestHiveFileFormats.java | 515 + ...AbstractTestSplitIteratorBackpressure.java | 25 + .../presto/hive/BenchmarkHiveFileFormats.java | 1850 + .../presto/hive/HiveBenchmarkQueryRunner.java | 96 + .../facebook/presto/hive/HiveQueryRunner.java | 113 + .../facebook/presto/hive/HiveTestUtils.java | 93 + .../facebook/presto/hive/MockAmazonS3.java | 766 + .../presto/hive/TestHiveBooleanParser.java | 51 + .../presto/hive/TestHiveBucketing.java | 68 + .../presto/hive/TestHiveClientConfig.java | 186 + .../presto/hive/TestHiveColumnHandle.java | 42 + .../presto/hive/TestHiveConnectorFactory.java | 54 + .../hive/TestHiveDistributedQueries.java | 51 + .../presto/hive/TestHiveFileFormats.java | 478 + .../hive/TestHiveIntegrationSmokeTest.java | 181 + .../presto/hive/TestHivePluginConfig.java | 47 + .../facebook/presto/hive/TestHiveSplit.java | 77 + .../presto/hive/TestHiveSplitSource.java | 236 + .../presto/hive/TestHiveTableHandle.java | 40 + .../facebook/presto/hive/TestHiveUtil.java | 61 + .../presto/hive/TestJsonHiveHandles.java | 120 + .../presto/hive/TestNumberParser.java | 109 + .../presto/hive/TestPrestoS3FileSystem.java | 223 + .../presto/hive/TestingHiveCluster.java | 68 + .../metastore/MockHiveMetastoreClient.java | 182 + .../metastore/TestCachingHiveMetastore.java | 242 + .../presto/hive/util/TestLazyMap.java | 84 + .../presto/hive/util/TestSerDeUtils.java | 294 + .../src/test/resources/addressbook.parquet | Bin 0 -> 3956 bytes presto-hive/src/test/sql/create-test-cdh4.sql | 113 + .../src/test/sql/create-test-hive12.sql | 83 + .../src/test/sql/create-test-hive13.sql | 125 + presto-hive/src/test/sql/create-test-s3.sql | 13 + presto-hive/src/test/sql/create-test.sql | 234 + presto-hive/src/test/sql/drop-test-s3.sql | 1 + presto-hive/src/test/sql/drop-test.sql | 33 + presto-jdbc/pom.xml | 252 + .../com/facebook/presto/jdbc/ColumnInfo.java | 386 + .../facebook/presto/jdbc/JettyLogging.java | 32 + .../presto/jdbc/NotImplementedException.java | 35 + .../com/facebook/presto/jdbc/PrestoArray.java | 118 + .../presto/jdbc/PrestoConnection.java | 666 + .../presto/jdbc/PrestoDatabaseMetaData.java | 1528 + .../facebook/presto/jdbc/PrestoDriver.java | 154 + .../presto/jdbc/PrestoIntervalDayTime.java | 89 + .../presto/jdbc/PrestoIntervalYearMonth.java | 66 + .../presto/jdbc/PrestoPreparedStatement.java | 439 + .../facebook/presto/jdbc/PrestoResultSet.java | 1832 + .../presto/jdbc/PrestoResultSetMetaData.java | 259 + .../facebook/presto/jdbc/PrestoStatement.java | 446 + .../facebook/presto/jdbc/QueryExecutor.java | 99 + .../presto/jdbc/UserAgentRequestFilter.java | 40 + .../META-INF/license/LICENSE.jol.txt | 347 + .../META-INF/services/java.sql.Driver | 1 + .../com/facebook/presto/jdbc/TestDriver.java | 993 + .../presto/jdbc/TestJdbcResultSet.java | 126 + presto-kafka/pom.xml | 214 + .../presto/kafka/KafkaColumnHandle.java | 219 + .../facebook/presto/kafka/KafkaConnector.java | 91 + .../presto/kafka/KafkaConnectorConfig.java | 171 + .../presto/kafka/KafkaConnectorFactory.java | 106 + .../presto/kafka/KafkaConnectorModule.java | 93 + .../facebook/presto/kafka/KafkaErrorCode.java | 45 + .../presto/kafka/KafkaFieldValueProvider.java | 49 + .../presto/kafka/KafkaHandleResolver.java | 106 + .../kafka/KafkaInternalFieldDescription.java | 279 + .../facebook/presto/kafka/KafkaMetadata.java | 245 + .../facebook/presto/kafka/KafkaPartition.java | 91 + .../facebook/presto/kafka/KafkaPlugin.java | 77 + .../facebook/presto/kafka/KafkaRecordSet.java | 320 + .../presto/kafka/KafkaRecordSetProvider.java | 95 + .../kafka/KafkaSimpleConsumerManager.java | 101 + .../com/facebook/presto/kafka/KafkaSplit.java | 148 + .../presto/kafka/KafkaSplitManager.java | 193 + .../kafka/KafkaTableDescriptionSupplier.java | 117 + .../presto/kafka/KafkaTableHandle.java | 151 + .../presto/kafka/KafkaTopicDescription.java | 92 + .../kafka/KafkaTopicFieldDescription.java | 159 + .../presto/kafka/KafkaTopicFieldGroup.java | 62 + .../kafka/decoder/KafkaDecoderModule.java | 54 + .../kafka/decoder/KafkaDecoderRegistry.java | 116 + .../kafka/decoder/KafkaFieldDecoder.java | 54 + .../presto/kafka/decoder/KafkaRowDecoder.java | 47 + .../decoder/csv/CsvKafkaDecoderModule.java | 35 + .../decoder/csv/CsvKafkaFieldDecoder.java | 105 + .../kafka/decoder/csv/CsvKafkaRowDecoder.java | 88 + .../dummy/DummyKafkaDecoderModule.java | 35 + .../decoder/dummy/DummyKafkaFieldDecoder.java | 79 + .../decoder/dummy/DummyKafkaRowDecoder.java | 48 + .../CustomDateTimeJsonKafkaFieldDecoder.java | 86 + .../json/ISO8601JsonKafkaFieldDecoder.java | 90 + .../decoder/json/JsonKafkaDecoderModule.java | 53 + .../decoder/json/JsonKafkaFieldDecoder.java | 173 + .../decoder/json/JsonKafkaRowDecoder.java | 97 + ...econdsSinceEpochJsonKafkaFieldDecoder.java | 90 + .../json/RFC2822JsonKafkaFieldDecoder.java | 91 + ...econdsSinceEpochJsonKafkaFieldDecoder.java | 90 + .../decoder/raw/RawKafkaDecoderModule.java | 34 + .../decoder/raw/RawKafkaFieldDecoder.java | 229 + .../kafka/decoder/raw/RawKafkaRowDecoder.java | 57 + .../presto/kafka/KafkaQueryRunner.java | 134 + .../kafka/TestKafkaConnectorConfig.java | 62 + .../presto/kafka/TestKafkaDistributed.java | 155 + .../kafka/TestKafkaIntegrationSmokeTest.java | 53 + .../presto/kafka/TestKafkaPlugin.java | 139 + .../presto/kafka/TestManySegments.java | 122 + .../kafka/TestMinimalFunctionality.java | 141 + .../kafka/decoder/csv/TestCsvDecoder.java | 142 + .../TestISO8601JsonKafkaFieldDecoder.java | 135 + .../kafka/decoder/json/TestJsonDecoder.java | 133 + ...econdsSinceEpochJsonKafkaFieldDecoder.java | 130 + .../TestRFC2822JsonKafkaFieldDecoder.java | 131 + ...econdsSinceEpochJsonKafkaFieldDecoder.java | 131 + .../kafka/decoder/raw/TestRawDecoder.java | 232 + .../kafka/decoder/util/DecoderTestUtil.java | 74 + .../presto/kafka/util/CodecSupplier.java | 71 + .../presto/kafka/util/EmbeddedKafka.java | 175 + .../presto/kafka/util/EmbeddedZookeeper.java | 96 + .../presto/kafka/util/JsonEncoder.java | 44 + .../presto/kafka/util/KafkaLoader.java | 152 + .../presto/kafka/util/NumberEncoder.java | 37 + .../presto/kafka/util/NumberPartitioner.java | 37 + .../facebook/presto/kafka/util/TestUtils.java | 96 + .../test/resources/decoder/json/message.json | 80 + .../src/test/resources/tpch/customer.json | 61 + .../src/test/resources/tpch/lineitem.json | 104 + .../src/test/resources/tpch/nation.json | 41 + .../src/test/resources/tpch/orders.json | 67 + .../src/test/resources/tpch/part.json | 66 + .../src/test/resources/tpch/partsupp.json | 46 + .../src/test/resources/tpch/region.json | 36 + .../src/test/resources/tpch/supplier.json | 56 + presto-main/etc/catalog/default.properties | 6 + presto-main/etc/catalog/example.properties | 2 + presto-main/etc/catalog/hive.properties | 2 + presto-main/etc/catalog/jmx.properties | 1 + presto-main/etc/catalog/tpch.properties | 2 + presto-main/etc/config.properties | 31 + presto-main/etc/jvm.config | 0 presto-main/etc/log.properties | 4 + presto-main/pom.xml | 365 + .../presto/ExceededMemoryLimitException.java | 41 + .../presto/HashPagePartitionFunction.java | 162 + .../com/facebook/presto/OutputBuffers.java | 170 + .../presto/PagePartitionFunction.java | 33 + .../facebook/presto/PagesIndexPageSorter.java | 51 + .../com/facebook/presto/PrestoMediaTypes.java | 26 + .../com/facebook/presto/ScheduledSplit.java | 75 + .../java/com/facebook/presto/Session.java | 364 + .../presto/SystemSessionProperties.java | 126 + .../java/com/facebook/presto/TaskSource.java | 103 + .../UnpartitionedPagePartitionFunction.java | 60 + .../presto/block/BlockEncodingManager.java | 149 + .../com/facebook/presto/block/BlockUtils.java | 58 + .../com/facebook/presto/block/PagesSerde.java | 123 + .../com/facebook/presto/byteCode/Access.java | 92 + .../presto/byteCode/AnnotationDefinition.java | 232 + .../com/facebook/presto/byteCode/Block.java | 951 + .../presto/byteCode/ByteCodeNode.java | 27 + .../presto/byteCode/ByteCodeVisitor.java | 327 + .../presto/byteCode/ClassDefinition.java | 302 + .../facebook/presto/byteCode/ClassInfo.java | 186 + .../presto/byteCode/ClassInfoLoader.java | 154 + .../com/facebook/presto/byteCode/Comment.java | 63 + .../presto/byteCode/DumpByteCodeVisitor.java | 612 + .../presto/byteCode/DynamicClassLoader.java | 143 + .../presto/byteCode/FieldDefinition.java | 123 + .../presto/byteCode/MethodDefinition.java | 327 + .../byteCode/MethodGenerationContext.java | 135 + .../com/facebook/presto/byteCode/OpCode.java | 278 + .../facebook/presto/byteCode/Parameter.java | 41 + .../presto/byteCode/ParameterizedType.java | 329 + .../com/facebook/presto/byteCode/Scope.java | 105 + .../presto/byteCode/SmartClassWriter.java | 54 + .../facebook/presto/byteCode/Variable.java | 97 + .../byteCode/control/CaseStatement.java | 85 + .../presto/byteCode/control/DoWhileLoop.java | 121 + .../presto/byteCode/control/FlowControl.java | 22 + .../presto/byteCode/control/ForLoop.java | 157 + .../presto/byteCode/control/IfStatement.java | 139 + .../presto/byteCode/control/LookupSwitch.java | 126 + .../presto/byteCode/control/TryCatch.java | 104 + .../presto/byteCode/control/WhileLoop.java | 115 + .../presto/byteCode/debug/DebugNode.java | 21 + .../presto/byteCode/debug/LineNumberNode.java | 71 + .../byteCode/debug/LocalVariableNode.java | 74 + .../expression/AndByteCodeExpression.java | 71 + .../ArithmeticByteCodeExpression.java | 204 + .../expression/ByteCodeExpression.java | 209 + .../expression/ByteCodeExpressions.java | 574 + .../expression/CastByteCodeExpression.java | 283 + .../ComparisonByteCodeExpression.java | 315 + .../ConstantByteCodeExpression.java | 77 + .../GetElementByteCodeExpression.java | 95 + .../GetFieldByteCodeExpression.java | 120 + .../InlineIfByteCodeExpression.java | 71 + .../expression/InvokeByteCodeExpression.java | 118 + .../InvokeDynamicByteCodeExpression.java | 86 + .../expression/NegateByteCodeExpression.java | 68 + .../NewInstanceByteCodeExpression.java | 67 + .../expression/NotByteCodeExpression.java | 65 + .../expression/OrByteCodeExpression.java | 71 + .../expression/PopByteCodeExpression.java | 56 + .../expression/ReturnByteCodeExpression.java | 90 + .../SetFieldByteCodeExpression.java | 144 + .../presto/byteCode/instruction/Constant.java | 623 + .../instruction/FieldInstruction.java | 177 + .../byteCode/instruction/InstructionNode.java | 21 + .../instruction/InvokeInstruction.java | 442 + .../byteCode/instruction/JumpInstruction.java | 162 + .../byteCode/instruction/LabelNode.java | 86 + .../byteCode/instruction/TypeInstruction.java | 102 + .../instruction/VariableInstruction.java | 153 + .../presto/concurrent/FairBatchExecutor.java | 198 + .../presto/connector/ConnectorManager.java | 292 + .../InformationSchemaColumnHandle.java | 77 + .../InformationSchemaHandleResolver.java | 59 + .../InformationSchemaMetadata.java | 210 + .../InformationSchemaModule.java | 36 + .../InformationSchemaPageSourceProvider.java | 341 + .../InformationSchemaSplit.java | 91 + .../InformationSchemaSplitManager.java | 134 + .../InformationSchemaTableHandle.java | 103 + .../presto/connector/jmx/JmxColumnHandle.java | 106 + .../connector/jmx/JmxConnectorFactory.java | 80 + .../presto/connector/jmx/JmxConnectorId.java | 51 + .../connector/jmx/JmxHandleResolver.java | 59 + .../presto/connector/jmx/JmxMetadata.java | 212 + .../connector/jmx/JmxRecordSetProvider.java | 176 + .../presto/connector/jmx/JmxSplit.java | 65 + .../presto/connector/jmx/JmxSplitManager.java | 135 + .../presto/connector/jmx/JmxTableHandle.java | 100 + .../connector/system/CatalogSystemTable.java | 70 + .../connector/system/NodeSystemTable.java | 93 + .../connector/system/QuerySystemTable.java | 122 + .../connector/system/SystemColumnHandle.java | 77 + .../connector/system/SystemConnector.java | 68 + .../system/SystemHandleResolver.java | 59 + .../system/SystemRecordSetProvider.java | 82 + .../presto/connector/system/SystemSplit.java | 84 + .../connector/system/SystemSplitManager.java | 127 + .../connector/system/SystemTableHandle.java | 89 + .../system/SystemTablesMetadata.java | 137 + .../connector/system/SystemTablesModule.java | 49 + .../connector/system/TaskSystemTable.java | 161 + .../discovery/EmbeddedDiscoveryConfig.java | 33 + .../discovery/EmbeddedDiscoveryModule.java | 149 + .../event/query/QueryCompletionEvent.java | 396 + .../presto/event/query/QueryCreatedEvent.java | 141 + .../presto/event/query/QueryMonitor.java | 271 + .../event/query/SplitCompletionEvent.java | 212 + .../presto/execution/AbandonedException.java | 29 + .../facebook/presto/execution/BufferInfo.java | 116 + .../presto/execution/BufferResult.java | 107 + .../com/facebook/presto/execution/Column.java | 92 + .../presto/execution/CreateTableTask.java | 78 + .../presto/execution/CreateViewTask.java | 115 + .../execution/DataDefinitionExecution.java | 208 + .../presto/execution/DataDefinitionTask.java | 25 + .../presto/execution/DropTableTask.java | 52 + .../presto/execution/DropViewTask.java | 52 + .../execution/ExecutionFailureInfo.java | 170 + .../execution/FailedQueryException.java | 35 + .../execution/FailedQueryExecution.java | 127 + .../facebook/presto/execution/Failure.java | 54 + .../presto/execution/ForQueryExecution.java | 31 + .../com/facebook/presto/execution/Input.java | 112 + .../presto/execution/LocationFactory.java | 31 + .../execution/NoSuchBufferException.java | 23 + .../presto/execution/NodeScheduler.java | 519 + .../presto/execution/NodeSchedulerConfig.java | 144 + .../presto/execution/NodeTaskMap.java | 89 + .../presto/execution/PageBufferInfo.java | 113 + .../presto/execution/PageSplitterUtil.java | 48 + .../presto/execution/PartitionBuffer.java | 215 + .../presto/execution/QueryExecution.java | 58 + .../presto/execution/QueryExecutionMBean.java | 43 + .../facebook/presto/execution/QueryId.java | 107 + .../presto/execution/QueryIdGenerator.java | 114 + .../facebook/presto/execution/QueryInfo.java | 225 + .../presto/execution/QueryManager.java | 40 + .../presto/execution/QueryManagerConfig.java | 252 + .../facebook/presto/execution/QueryQueue.java | 111 + .../execution/QueryQueueDefinition.java | 67 + .../presto/execution/QueryQueueManager.java | 28 + .../presto/execution/QueryQueueRule.java | 93 + .../facebook/presto/execution/QueryState.java | 64 + .../presto/execution/QueryStateMachine.java | 421 + .../facebook/presto/execution/QueryStats.java | 367 + .../presto/execution/QueuedExecution.java | 78 + .../facebook/presto/execution/RemoteTask.java | 44 + .../presto/execution/RemoteTaskFactory.java | 32 + .../presto/execution/RenameColumnTask.java | 60 + .../presto/execution/RenameTableTask.java | 58 + .../presto/execution/ResetSessionTask.java | 41 + .../ResettableRandomizedIterator.java | 60 + .../presto/execution/SetSessionTask.java | 42 + .../presto/execution/SharedBuffer.java | 586 + .../presto/execution/SharedBufferInfo.java | 146 + .../execution/SharedBufferMemoryManager.java | 40 + .../presto/execution/SimpleDomain.java | 117 + .../presto/execution/SimpleMarker.java | 113 + .../presto/execution/SimpleRange.java | 93 + .../presto/execution/SplitRunner.java | 31 + .../presto/execution/SqlQueryExecution.java | 562 + .../presto/execution/SqlQueryManager.java | 455 + .../execution/SqlQueryManagerStats.java | 189 + .../execution/SqlQueryQueueManager.java | 353 + .../presto/execution/SqlStageExecution.java | 830 + .../facebook/presto/execution/SqlTask.java | 353 + .../presto/execution/SqlTaskExecution.java | 647 + .../execution/SqlTaskExecutionFactory.java | 106 + .../presto/execution/SqlTaskIoStats.java | 86 + .../presto/execution/SqlTaskManager.java | 361 + .../presto/execution/SqlTaskManagerStats.java | 167 + .../facebook/presto/execution/StageId.java | 85 + .../facebook/presto/execution/StageInfo.java | 154 + .../facebook/presto/execution/StageState.java | 87 + .../presto/execution/StageStateMachine.java | 296 + .../facebook/presto/execution/StageStats.java | 320 + .../presto/execution/StateMachine.java | 365 + .../presto/execution/TaskExecutor.java | 827 + .../com/facebook/presto/execution/TaskId.java | 92 + .../facebook/presto/execution/TaskInfo.java | 167 + .../presto/execution/TaskManager.java | 93 + .../presto/execution/TaskManagerConfig.java | 267 + .../facebook/presto/execution/TaskState.java | 67 + .../presto/execution/TaskStateMachine.java | 140 + .../failureDetector/FailureDetector.java | 23 + .../FailureDetectorConfig.java | 102 + .../FailureDetectorModule.java | 47 + .../failureDetector/ForFailureDetector.java | 31 + .../HeartbeatFailureDetector.java | 463 + .../index/IndexHandleJacksonModule.java | 56 + .../facebook/presto/index/IndexManager.java | 80 + .../presto/memory/ClusterMemoryManager.java | 285 + .../presto/memory/ClusterMemoryPool.java | 139 + .../presto/memory/ForMemoryManager.java | 31 + .../presto/memory/LocalMemoryManager.java | 71 + .../memory/LocalMemoryManagerExporter.java | 70 + .../facebook/presto/memory/MemoryInfo.java | 58 + .../presto/memory/MemoryManagerConfig.java | 66 + .../facebook/presto/memory/MemoryPool.java | 131 + .../presto/memory/MemoryPoolAssignment.java | 55 + .../memory/MemoryPoolAssignmentsRequest.java | 65 + .../facebook/presto/memory/MemoryPoolId.java | 66 + .../presto/memory/MemoryPoolInfo.java | 52 + .../presto/memory/MemoryResource.java | 51 + .../facebook/presto/memory/QueryContext.java | 127 + .../presto/memory/RemoteNodeMemory.java | 128 + .../memory/ReservedSystemMemoryConfig.java | 40 + .../presto/memory/VersionedMemoryPoolId.java | 49 + .../metadata/AbstractTypedJacksonModule.java | 188 + .../facebook/presto/metadata/AllNodes.java | 43 + .../presto/metadata/CatalogManager.java | 201 + .../presto/metadata/CatalogManagerConfig.java | 38 + .../metadata/ColumnHandleJacksonModule.java | 53 + .../presto/metadata/DiscoveryNodeManager.java | 211 + .../presto/metadata/FunctionFactory.java | 22 + .../presto/metadata/FunctionInfo.java | 252 + .../presto/metadata/FunctionListBuilder.java | 371 + .../presto/metadata/FunctionRegistry.java | 789 + .../presto/metadata/HandleJsonModule.java | 44 + .../presto/metadata/HandleResolver.java | 188 + .../presto/metadata/InMemoryNodeManager.java | 96 + .../facebook/presto/metadata/IndexHandle.java | 75 + .../presto/metadata/InsertTableHandle.java | 75 + .../InsertTableHandleJacksonModule.java | 53 + .../presto/metadata/InternalNodeManager.java | 24 + .../presto/metadata/InternalTable.java | 123 + .../presto/metadata/JsonTypeIdResolver.java | 21 + .../metadata/LegacyTableLayoutHandle.java | 85 + .../facebook/presto/metadata/Metadata.java | 235 + .../presto/metadata/MetadataManager.java | 725 + .../presto/metadata/MetadataUtil.java | 180 + .../facebook/presto/metadata/NodeVersion.java | 56 + .../metadata/OperatorNotFoundException.java | 63 + .../presto/metadata/OperatorType.java | 212 + .../presto/metadata/OutputTableHandle.java | 75 + .../OutputTableHandleJacksonModule.java | 53 + .../metadata/ParametricAggregation.java | 60 + .../presto/metadata/ParametricFunction.java | 43 + .../presto/metadata/ParametricOperator.java | 93 + .../presto/metadata/ParametricScalar.java | 48 + .../facebook/presto/metadata/PrestoNode.java | 95 + .../presto/metadata/QualifiedTableName.java | 109 + .../presto/metadata/QualifiedTablePrefix.java | 127 + .../metadata/RemoteSplitHandleResolver.java | 60 + .../presto/metadata/ResolvedIndex.java | 44 + .../facebook/presto/metadata/Signature.java | 373 + .../metadata/SpecializedFunctionKey.java | 73 + .../com/facebook/presto/metadata/Split.java | 75 + .../presto/metadata/SplitJacksonModule.java | 53 + .../facebook/presto/metadata/TableHandle.java | 75 + .../metadata/TableHandleJacksonModule.java | 53 + .../facebook/presto/metadata/TableLayout.java | 75 + .../presto/metadata/TableLayoutHandle.java | 72 + .../TableLayoutHandleJacksonModule.java | 53 + .../presto/metadata/TableLayoutResult.java | 39 + .../presto/metadata/TableMetadata.java | 56 + .../presto/metadata/TypeParameter.java | 123 + .../presto/metadata/ViewDefinition.java | 113 + .../presto/operator/AggregationOperator.java | 240 + .../presto/operator/ArrayUnnester.java | 77 + .../presto/operator/BigintGroupByHash.java | 309 + .../presto/operator/BlockedReason.java | 19 + .../facebook/presto/operator/ChannelSet.java | 101 + .../presto/operator/CursorProcessor.java | 26 + .../presto/operator/DeleteOperator.java | 190 + .../facebook/presto/operator/Description.java | 28 + .../operator/DistinctLimitOperator.java | 171 + .../com/facebook/presto/operator/Driver.java | 626 + .../presto/operator/DriverContext.java | 427 + .../presto/operator/DriverFactory.java | 114 + .../facebook/presto/operator/DriverStats.java | 278 + .../presto/operator/ExchangeClient.java | 405 + .../presto/operator/ExchangeClientConfig.java | 101 + .../operator/ExchangeClientFactory.java | 89 + .../presto/operator/ExchangeClientStatus.java | 90 + .../presto/operator/ExchangeOperator.java | 190 + .../operator/FilterAndProjectOperator.java | 142 + .../presto/operator/FilterFunction.java | 24 + .../presto/operator/FilterFunctions.java | 46 + .../presto/operator/FinishedOperator.java | 75 + .../facebook/presto/operator/ForExchange.java | 31 + .../presto/operator/ForScheduler.java | 31 + .../operator/GenericCursorProcessor.java | 63 + .../presto/operator/GenericPageProcessor.java | 51 + .../facebook/presto/operator/GroupByHash.java | 50 + .../presto/operator/GroupByIdBlock.java | 193 + .../operator/HashAggregationOperator.java | 417 + .../presto/operator/HashBuilderOperator.java | 175 + .../presto/operator/HashGenerator.java | 55 + .../operator/HashPartitionMaskOperator.java | 236 + .../presto/operator/HashSemiJoinOperator.java | 203 + .../presto/operator/HttpPageBufferClient.java | 563 + .../presto/operator/InMemoryExchange.java | 283 + .../InMemoryExchangeSinkOperator.java | 105 + .../InMemoryExchangeSourceOperator.java | 161 + .../presto/operator/InMemoryJoinHash.java | 234 + .../operator/InterpretedHashGenerator.java | 63 + .../facebook/presto/operator/JoinProbe.java | 27 + .../presto/operator/JoinProbeFactory.java | 21 + .../presto/operator/LimitOperator.java | 137 + .../presto/operator/LookupJoinOperator.java | 288 + .../operator/LookupJoinOperatorFactory.java | 77 + .../presto/operator/LookupJoinOperators.java | 56 + .../presto/operator/LookupSource.java | 41 + .../presto/operator/LookupSourceSupplier.java | 34 + .../facebook/presto/operator/MapUnnester.java | 81 + .../presto/operator/MarkDistinctHash.java | 64 + .../presto/operator/MarkDistinctOperator.java | 159 + .../facebook/presto/operator/Mergeable.java | 20 + .../operator/MultiChannelGroupByHash.java | 389 + .../facebook/presto/operator/Operator.java | 83 + .../presto/operator/OperatorContext.java | 425 + .../presto/operator/OperatorFactory.java | 30 + .../presto/operator/OperatorStats.java | 366 + .../presto/operator/OrderByOperator.java | 207 + .../presto/operator/OutputFactory.java | 23 + .../operator/PageBufferClientStatus.java | 114 + .../presto/operator/PageProcessor.java | 23 + .../presto/operator/PageSourceOperator.java | 109 + .../operator/PageTooLargeException.java | 27 + .../operator/PageTransportErrorException.java | 27 + .../PageTransportTimeoutException.java | 27 + .../presto/operator/PagesHashStrategy.java | 61 + .../facebook/presto/operator/PagesIndex.java | 402 + .../presto/operator/PagesIndexComparator.java | 19 + .../presto/operator/PagesIndexOrdering.java | 162 + .../presto/operator/ParallelHashBuilder.java | 430 + .../ParallelLookupSourceSupplier.java | 94 + .../operator/PartitionedLookupSource.java | 118 + .../operator/PartitionedOutputOperator.java | 251 + .../presto/operator/PipelineContext.java | 415 + .../presto/operator/PipelineStats.java | 329 + .../operator/PrecomputedHashGenerator.java | 43 + .../presto/operator/ProjectionFunction.java | 28 + .../presto/operator/ProjectionFunctions.java | 92 + .../operator/RecordProjectOperator.java | 170 + .../presto/operator/RowComparator.java | 61 + .../presto/operator/RowNumberOperator.java | 287 + .../presto/operator/SampleOperator.java | 179 + .../ScanFilterAndProjectOperator.java | 327 + .../presto/operator/SetBuilderOperator.java | 198 + .../SettableLookupSourceSupplier.java | 88 + .../presto/operator/SharedLookupSource.java | 94 + .../presto/operator/SimpleJoinProbe.java | 124 + .../operator/SimplePagesHashStrategy.java | 142 + .../operator/SimplePagesIndexComparator.java | 65 + .../presto/operator/SourceOperator.java | 31 + .../operator/SourceOperatorFactory.java | 25 + .../presto/operator/SyntheticAddress.java | 43 + .../presto/operator/TableCommitOperator.java | 174 + .../presto/operator/TableScanOperator.java | 261 + .../presto/operator/TableWriterOperator.java | 213 + .../facebook/presto/operator/TaskContext.java | 362 + .../presto/operator/TaskOutputOperator.java | 152 + .../facebook/presto/operator/TaskStats.java | 373 + .../presto/operator/TopNOperator.java | 358 + .../operator/TopNRowNumberOperator.java | 473 + .../presto/operator/TwoChannelJoinProbe.java | 124 + .../presto/operator/UnnestOperator.java | 259 + .../facebook/presto/operator/Unnester.java | 25 + .../presto/operator/ValuesOperator.java | 128 + .../operator/WindowFunctionDefinition.java | 65 + .../presto/operator/WindowOperator.java | 464 + .../AbstractMinMaxAggregation.java | 236 + .../operator/aggregation/Accumulator.java | 36 + .../aggregation/AccumulatorCompiler.java | 789 + .../aggregation/AccumulatorFactory.java | 29 + .../aggregation/AccumulatorFactoryBinder.java | 22 + .../aggregation/AggregationCompiler.java | 262 + .../aggregation/AggregationFunction.java | 32 + .../aggregation/AggregationMetadata.java | 327 + .../aggregation/AggregationUtils.java | 157 + .../ApproximateAverageAggregations.java | 118 + .../ApproximateCountAggregation.java | 65 + .../ApproximateCountColumnAggregations.java | 85 + .../ApproximateCountDistinctAggregations.java | 139 + ...proximateDoublePercentileAggregations.java | 90 + ...ApproximateLongPercentileAggregations.java | 108 + .../ApproximateSetAggregation.java | 93 + .../ApproximateSumAggregations.java | 120 + .../aggregation/ApproximateUtils.java | 137 + .../aggregation/ArbitraryAggregation.java | 129 + .../aggregation/ArrayAggregation.java | 163 + .../aggregation/AverageAggregations.java | 67 + .../operator/aggregation/BlockIndex.java | 25 + .../operator/aggregation/BlockPosition.java | 25 + .../aggregation/BooleanAndAggregation.java | 44 + .../aggregation/BooleanOrAggregation.java | 44 + .../operator/aggregation/CombineFunction.java | 25 + .../aggregation/CorrelationAggregation.java | 60 + .../aggregation/CountAggregation.java | 38 + .../operator/aggregation/CountColumn.java | 117 + .../aggregation/CountIfAggregation.java | 38 + .../aggregation/CovarianceAggregation.java | 67 + .../aggregation/DoubleSumAggregation.java | 34 + .../GenericAccumulatorFactory.java | 109 + .../GenericAccumulatorFactoryBinder.java | 78 + .../GenericAggregationFunctionFactory.java | 51 + .../aggregation/GroupedAccumulator.java | 37 + .../operator/aggregation/InputFunction.java | 25 + .../IntermediateInputFunction.java | 25 + .../InternalAggregationFunction.java | 84 + .../operator/aggregation/KeyValuePairs.java | 113 + .../LazyAccumulatorFactoryBinder.java | 38 + .../aggregation/LongSumAggregation.java | 34 + .../operator/aggregation/MapAggregation.java | 170 + .../operator/aggregation/MaxAggregation.java | 36 + .../presto/operator/aggregation/MaxBy.java | 141 + .../MergeHyperLogLogAggregation.java | 55 + .../operator/aggregation/MinAggregation.java | 36 + .../presto/operator/aggregation/MinBy.java | 141 + .../aggregation/NullablePosition.java | 25 + .../aggregation/NumericHistogram.java | 418 + .../NumericHistogramAggregation.java | 96 + .../NumericHistogramStateFactory.java | 115 + .../operator/aggregation/OutputFunction.java | 26 + .../aggregation/RegressionAggregation.java | 77 + .../operator/aggregation/SampleWeight.java | 25 + .../operator/aggregation/SimpleTypedSet.java | 176 + .../presto/operator/aggregation/TypedSet.java | 34 + .../aggregation/VarianceAggregation.java | 109 + .../AbstractGroupedAccumulatorState.java | 31 + .../aggregation/state/AccumulatorState.java | 19 + .../state/AccumulatorStateFactory.java | 28 + .../state/AccumulatorStateMetadata.java | 28 + .../state/AccumulatorStateSerializer.java | 27 + .../state/ArbitraryAggregationState.java | 28 + .../ArbitraryAggregationStateFactory.java | 137 + .../ArbitraryAggregationStateSerializer.java | 107 + .../state/ArrayAggregationState.java | 27 + .../state/ArrayAggregationStateFactory.java | 128 + .../ArrayAggregationStateSerializer.java | 66 + .../aggregation/state/CorrelationState.java | 26 + .../aggregation/state/CovarianceState.java | 34 + .../state/DigestAndPercentileState.java | 31 + .../DigestAndPercentileStateFactory.java | 148 + .../DigestAndPercentileStateSerializer.java | 68 + .../state/GroupedAccumulatorState.java | 22 + .../aggregation/state/HyperLogLogState.java | 32 + .../state/HyperLogLogStateFactory.java | 119 + .../state/HyperLogLogStateSerializer.java | 50 + .../state/InitialBooleanValue.java | 26 + .../aggregation/state/InitialDoubleValue.java | 26 + .../aggregation/state/InitialLongValue.java | 26 + .../state/KeyValuePairStateSerializer.java | 50 + .../aggregation/state/KeyValuePairsState.java | 32 + .../state/KeyValuePairsStateFactory.java | 175 + .../aggregation/state/LongAndDoubleState.java | 26 + .../operator/aggregation/state/LongState.java | 22 + .../aggregation/state/MaxOrMinByState.java | 29 + .../state/MaxOrMinByStateFactory.java | 134 + .../state/MaxOrMinByStateSerializer.java | 130 + .../state/NullableBooleanState.java | 28 + .../state/NullableBooleanStateSerializer.java | 62 + .../state/NullableDoubleState.java | 28 + .../state/NullableDoubleStateSerializer.java | 62 + .../aggregation/state/NullableLongState.java | 28 + .../state/NullableLongStateSerializer.java | 62 + .../NumericHistogramStateSerializer.java | 51 + .../aggregation/state/RegressionState.java | 22 + .../aggregation/state/SliceState.java | 25 + .../state/SliceStateSerializer.java | 52 + .../aggregation/state/StateCompiler.java | 727 + .../aggregation/state/StateCompilerUtils.java | 230 + .../state/TriStateBooleanState.java | 27 + .../state/TriStateBooleanStateSerializer.java | 55 + .../aggregation/state/VarianceState.java | 30 + .../index/DynamicTupleFilterFactory.java | 65 + .../index/FieldSetFilteringRecordSet.java | 190 + .../IndexBuildDriverFactoryProvider.java | 79 + .../operator/index/IndexJoinLookupStats.java | 90 + .../presto/operator/index/IndexLoader.java | 410 + .../operator/index/IndexLookupSource.java | 101 + .../index/IndexLookupSourceSupplier.java | 69 + .../presto/operator/index/IndexSnapshot.java | 68 + .../operator/index/IndexSnapshotBuilder.java | 162 + .../operator/index/IndexSourceOperator.java | 210 + .../presto/operator/index/IndexSplit.java | 56 + .../presto/operator/index/IndexedData.java | 43 + .../presto/operator/index/PageBuffer.java | 78 + .../operator/index/PageBufferOperator.java | 138 + .../presto/operator/index/PageRecordSet.java | 151 + .../index/PagesIndexBuilderOperator.java | 129 + .../operator/index/StreamingIndexedData.java | 142 + .../operator/index/TupleFilterProcessor.java | 86 + .../index/UnloadedIndexKeyRecordSet.java | 257 + .../presto/operator/index/UpdateRequest.java | 67 + .../scalar/ArrayCardinalityFunction.java | 79 + .../operator/scalar/ArrayConcatFunction.java | 74 + .../operator/scalar/ArrayConcatUtils.java | 147 + .../operator/scalar/ArrayConstructor.java | 179 + .../presto/operator/scalar/ArrayContains.java | 157 + .../scalar/ArrayDistinctFunction.java | 104 + .../operator/scalar/ArrayEqualOperator.java | 91 + .../operator/scalar/ArrayFunctions.java | 38 + .../scalar/ArrayGreaterThanOperator.java | 97 + .../ArrayGreaterThanOrEqualOperator.java | 98 + .../scalar/ArrayHashCodeOperator.java | 63 + .../scalar/ArrayIntersectFunction.java | 159 + .../presto/operator/scalar/ArrayJoin.java | 216 + .../scalar/ArrayLessThanOperator.java | 97 + .../scalar/ArrayLessThanOrEqualOperator.java | 98 + .../scalar/ArrayNotEqualOperator.java | 63 + .../scalar/ArrayPositionFunction.java | 193 + .../operator/scalar/ArrayRemoveFunction.java | 151 + .../operator/scalar/ArraySortFunction.java | 115 + .../scalar/ArraySubscriptOperator.java | 133 + .../operator/scalar/ArrayToArrayCast.java | 131 + .../scalar/ArrayToElementConcatFunction.java | 76 + .../operator/scalar/ArrayToJsonCast.java | 80 + .../operator/scalar/ColorFunctions.java | 316 + .../operator/scalar/CombineHashFunction.java | 29 + .../operator/scalar/DateTimeFunctions.java | 982 + .../scalar/ElementToArrayConcatFunction.java | 75 + .../presto/operator/scalar/Greatest.java | 242 + .../operator/scalar/HyperLogLogFunctions.java | 33 + .../presto/operator/scalar/IdentityCast.java | 50 + .../presto/operator/scalar/JsonExtract.java | 359 + .../presto/operator/scalar/JsonFunctions.java | 382 + .../presto/operator/scalar/JsonOperators.java | 50 + .../presto/operator/scalar/JsonPath.java | 45 + .../operator/scalar/JsonPathTokenizer.java | 175 + .../operator/scalar/JsonToArrayCast.java | 79 + .../presto/operator/scalar/JsonToMapCast.java | 80 + .../presto/operator/scalar/Least.java | 241 + .../scalar/MapCardinalityFunction.java | 87 + .../operator/scalar/MapConstructor.java | 112 + .../operator/scalar/MapEqualOperator.java | 171 + .../operator/scalar/MapHashCodeOperator.java | 76 + .../presto/operator/scalar/MapKeys.java | 92 + .../operator/scalar/MapNotEqualOperator.java | 75 + .../operator/scalar/MapSubscriptOperator.java | 222 + .../presto/operator/scalar/MapToJsonCast.java | 109 + .../presto/operator/scalar/MapValues.java | 92 + .../presto/operator/scalar/MathFunctions.java | 384 + .../scalar/QuarterOfYearDateTimeField.java | 74 + .../operator/scalar/RegexpFunctions.java | 315 + .../operator/scalar/RowEqualOperator.java | 65 + .../operator/scalar/RowFieldReference.java | 129 + .../operator/scalar/RowHashCodeOperator.java | 63 + .../operator/scalar/RowNotEqualOperator.java | 59 + .../presto/operator/scalar/RowToJsonCast.java | 77 + .../operator/scalar/ScalarFunction.java | 32 + .../operator/scalar/ScalarOperator.java | 28 + .../operator/scalar/StringFunctions.java | 453 + .../scalar/TestingRowConstructor.java | 168 + .../operator/scalar/TryCastFunction.java | 97 + .../presto/operator/scalar/UrlFunctions.java | 147 + .../operator/scalar/VarbinaryFunctions.java | 133 + .../AbstractWindowFunctionSupplier.java | 65 + .../window/AggregateWindowFunction.java | 129 + .../CumulativeDistributionFunction.java | 48 + .../operator/window/DenseRankFunction.java | 46 + .../operator/window/FirstValueFunction.java | 101 + .../presto/operator/window/FrameInfo.java | 82 + .../presto/operator/window/LagFunction.java | 120 + .../operator/window/LastValueFunction.java | 101 + .../presto/operator/window/LeadFunction.java | 120 + .../presto/operator/window/NTileFunction.java | 79 + .../operator/window/NthValueFunction.java | 116 + .../operator/window/PercentRankFunction.java | 60 + .../presto/operator/window/RankFunction.java | 52 + .../window/RankingWindowFunction.java | 74 + .../ReflectionWindowFunctionSupplier.java | 79 + .../operator/window/RowNumberFunction.java | 35 + .../operator/window/ValueWindowFunction.java | 59 + .../operator/window/WindowFunction.java | 45 + .../window/WindowFunctionSupplier.java | 27 + .../presto/operator/window/WindowIndex.java | 92 + .../operator/window/WindowPartition.java | 232 + .../com/facebook/presto/pdbo/PdboConfig.java | 115 + .../com/facebook/presto/pdbo/PdboManager.java | 53 + .../com/facebook/presto/pdbo/PdboTable.java | 132 + .../facebook/presto/pdbo/StepCalcManager.java | 411 + .../server/AsyncHttpExecutionMBean.java | 45 + .../presto/server/BasicQueryInfo.java | 239 + .../presto/server/CoordinatorModule.java | 172 + .../presto/server/ExchangeExecutionMBean.java | 46 + .../presto/server/ExecuteResource.java | 194 + .../presto/server/ForAsyncHttpResponse.java | 31 + .../facebook/presto/server/ForExecute.java | 30 + .../presto/server/HttpLocationFactory.java | 94 + .../presto/server/HttpRemoteTask.java | 945 + .../presto/server/HttpRemoteTaskFactory.java | 112 + .../facebook/presto/server/NodeResource.java | 50 + .../presto/server/PagesResponseWriter.java | 101 + .../presto/server/PluginClassLoader.java | 221 + .../facebook/presto/server/PluginManager.java | 291 + .../presto/server/PluginManagerConfig.java | 113 + .../presto/server/PrestoJvmRequirements.java | 83 + .../facebook/presto/server/PrestoServer.java | 216 + .../presto/server/QueryExecutionResource.java | 337 + .../facebook/presto/server/QueryResource.java | 122 + .../facebook/presto/server/ResourceUtil.java | 170 + .../facebook/presto/server/ServerConfig.java | 74 + .../presto/server/ServerMainModule.java | 363 + .../presto/server/SliceDeserializer.java | 34 + .../presto/server/SliceSerializer.java | 32 + .../facebook/presto/server/StageResource.java | 44 + .../presto/server/StatementResource.java | 744 + .../facebook/presto/server/TaskResource.java | 246 + .../presto/server/TaskUpdateRequest.java | 88 + .../presto/server/ThreadResource.java | 194 + .../presto/server/ThrowableMapper.java | 65 + .../presto/server/testing/FileUtils.java | 83 + .../server/testing/TestingPrestoServer.java | 291 + .../split/ConnectorAwareSplitSource.java | 79 + .../presto/split/MappedRecordSet.java | 147 + .../presto/split/PageSinkManager.java | 61 + .../presto/split/PageSinkProvider.java | 25 + .../presto/split/PageSourceManager.java | 62 + .../presto/split/PageSourceProvider.java | 25 + .../presto/split/RecordPageSinkProvider.java | 46 + .../split/RecordPageSourceProvider.java | 42 + .../facebook/presto/split/RemoteSplit.java | 70 + .../presto/split/SampledSplitSource.java | 66 + .../facebook/presto/split/SplitManager.java | 74 + .../facebook/presto/split/SplitSource.java | 43 + .../facebook/presto/sql/ExpressionUtils.java | 200 + .../sql/analyzer/AggregateExtractor.java | 53 + .../sql/analyzer/AggregationAnalyzer.java | 431 + .../presto/sql/analyzer/Analysis.java | 436 + .../presto/sql/analyzer/AnalysisContext.java | 81 + .../presto/sql/analyzer/Analyzer.java | 71 + .../sql/analyzer/ExpressionAnalysis.java | 60 + .../sql/analyzer/ExpressionAnalyzer.java | 1052 + .../presto/sql/analyzer/FeaturesConfig.java | 100 + .../facebook/presto/sql/analyzer/Field.java | 139 + .../sql/analyzer/FieldOrExpression.java | 106 + .../presto/sql/analyzer/QueryExplainer.java | 115 + .../sql/analyzer/SemanticErrorCode.java | 74 + .../sql/analyzer/SemanticException.java | 46 + .../sql/analyzer/StatementAnalyzer.java | 790 + .../presto/sql/analyzer/TupleAnalyzer.java | 1140 + .../presto/sql/analyzer/TupleDescriptor.java | 210 + .../sql/analyzer/WindowFunctionExtractor.java | 42 + .../sql/analyzer/WindowFunctionValidator.java | 37 + .../presto/sql/gen/AndCodeGenerator.java | 102 + .../presto/sql/gen/ArrayGeneratorUtils.java | 67 + .../sql/gen/ArrayMapByteCodeExpression.java | 109 + .../com/facebook/presto/sql/gen/Binding.java | 49 + .../facebook/presto/sql/gen/BodyCompiler.java | 24 + .../facebook/presto/sql/gen/Bootstrap.java | 64 + .../sql/gen/ByteCodeExpressionVisitor.java | 165 + .../presto/sql/gen/ByteCodeGenerator.java | 26 + .../sql/gen/ByteCodeGeneratorContext.java | 87 + .../presto/sql/gen/ByteCodeUtils.java | 314 + .../presto/sql/gen/CallSiteBinder.java | 58 + .../presto/sql/gen/CastCodeGenerator.java | 49 + .../presto/sql/gen/CoalesceCodeGenerator.java | 66 + .../presto/sql/gen/CompilerOperations.java | 74 + .../presto/sql/gen/CompilerUtils.java | 140 + .../sql/gen/CursorProcessorCompiler.java | 259 + .../presto/sql/gen/ExpressionCompiler.java | 196 + .../sql/gen/FunctionCallCodeGenerator.java | 51 + .../presto/sql/gen/IfCodeGenerator.java | 51 + .../presto/sql/gen/InCodeGenerator.java | 244 + .../gen/InvokeFunctionByteCodeExpression.java | 81 + .../sql/gen/IsDistinctFromCodeGenerator.java | 91 + .../presto/sql/gen/IsNullCodeGenerator.java | 58 + .../presto/sql/gen/IsolatedClass.java | 70 + .../facebook/presto/sql/gen/JoinCompiler.java | 612 + .../presto/sql/gen/JoinProbeCompiler.java | 517 + .../presto/sql/gen/NullIfCodeGenerator.java | 95 + .../presto/sql/gen/OrCodeGenerator.java | 101 + .../presto/sql/gen/OrderingCompiler.java | 274 + .../presto/sql/gen/PageProcessorCompiler.java | 346 + .../sql/gen/SqlTypeByteCodeExpression.java | 114 + .../presto/sql/gen/SwitchCodeGenerator.java | 140 + .../presto/sql/planner/CompilerConfig.java | 35 + .../sql/planner/DependencyExtractor.java | 61 + .../sql/planner/DeterminismEvaluator.java | 53 + .../planner/DistributedExecutionPlanner.java | 280 + .../presto/sql/planner/DomainTranslator.java | 610 + .../presto/sql/planner/DomainUtils.java | 52 + .../planner/EffectivePredicateExtractor.java | 322 + .../presto/sql/planner/EqualityInference.java | 409 + .../sql/planner/ExpressionInterpreter.java | 890 + .../sql/planner/ExpressionNodeInliner.java | 37 + .../sql/planner/ExpressionSymbolInliner.java | 38 + .../presto/sql/planner/InputExtractor.java | 177 + .../planner/InterpretedFilterFunction.java | 70 + .../InterpretedProjectionFunction.java | 108 + .../sql/planner/LiteralInterpreter.java | 246 + .../sql/planner/LocalExecutionPlanner.java | 1865 + .../presto/sql/planner/LogicalPlanner.java | 226 + .../sql/planner/LookupSymbolResolver.java | 60 + .../sql/planner/NoOpSymbolResolver.java | 28 + .../com/facebook/presto/sql/planner/Plan.java | 50 + .../presto/sql/planner/PlanBuilder.java | 79 + .../presto/sql/planner/PlanFragment.java | 210 + .../presto/sql/planner/PlanFragmenter.java | 323 + .../sql/planner/PlanNodeIdAllocator.java | 26 + .../sql/planner/PlanOptimizersFactory.java | 116 + .../presto/sql/planner/PlanPrinter.java | 647 + .../presto/sql/planner/PlanSanityChecker.java | 481 + .../sql/planner/ProjectionPushDown.java | 126 + .../presto/sql/planner/QueryPlanner.java | 749 + .../presto/sql/planner/RelationPlan.java | 75 + .../presto/sql/planner/RelationPlanner.java | 747 + .../sql/planner/StageExecutionPlan.java | 75 + .../sql/planner/SubExpressionExtractor.java | 72 + .../facebook/presto/sql/planner/SubPlan.java | 86 + .../facebook/presto/sql/planner/Symbol.java | 92 + .../presto/sql/planner/SymbolAllocator.java | 112 + .../presto/sql/planner/SymbolExtractor.java | 330 + .../presto/sql/planner/SymbolResolver.java | 19 + .../sql/planner/SymbolToInputRewriter.java | 47 + .../presto/sql/planner/TranslationMap.java | 216 + .../optimizations/ActualProperties.java | 336 + .../planner/optimizations/AddExchanges.java | 1009 + .../optimizations/BeginTableWrite.java | 126 + .../CanonicalizeExpressions.java | 195 + .../optimizations/CountConstantOptimizer.java | 116 + .../HashGenerationOptimizer.java | 330 + .../ImplementSampleAsFilter.java | 73 + .../optimizations/IndexJoinOptimizer.java | 488 + .../planner/optimizations/LimitPushDown.java | 211 + .../optimizations/LocalProperties.java | 141 + .../optimizations/MergeProjections.java | 73 + .../optimizations/MetadataQueryOptimizer.java | 219 + .../sql/planner/optimizations/PickLayout.java | 154 + .../planner/optimizations/PlanOptimizer.java | 32 + .../optimizations/PredicatePushDown.java | 877 + .../optimizations/PreferredProperties.java | 262 + .../optimizations/PropertyDerivations.java | 529 + .../PruneRedundantProjections.java | 80 + .../PruneUnreferencedOutputs.java | 586 + .../optimizations/SetFlatteningOptimizer.java | 117 + .../optimizations/SimplifyExpressions.java | 125 + .../SingleDistinctOptimizer.java | 133 + .../UnaliasSymbolReferences.java | 442 + .../optimizations/WindowFilterPushDown.java | 279 + .../sql/planner/plan/AggregationNode.java | 162 + .../sql/planner/plan/ChildReplacer.java | 231 + .../presto/sql/planner/plan/DeleteNode.java | 89 + .../sql/planner/plan/DistinctLimitNode.java | 96 + .../presto/sql/planner/plan/ExchangeNode.java | 150 + .../presto/sql/planner/plan/FilterNode.java | 73 + .../sql/planner/plan/IndexJoinNode.java | 157 + .../sql/planner/plan/IndexSourceNode.java | 115 + .../presto/sql/planner/plan/JoinNode.java | 188 + .../presto/sql/planner/plan/LimitNode.java | 74 + .../sql/planner/plan/MarkDistinctNode.java | 96 + .../presto/sql/planner/plan/OutputNode.java | 81 + .../sql/planner/plan/PlanFragmentId.java | 66 + .../presto/sql/planner/plan/PlanNode.java | 81 + .../presto/sql/planner/plan/PlanNodeId.java | 66 + .../presto/sql/planner/plan/PlanRewriter.java | 113 + .../presto/sql/planner/plan/PlanVisitor.java | 152 + .../presto/sql/planner/plan/ProjectNode.java | 83 + .../sql/planner/plan/RemoteSourceNode.java | 76 + .../sql/planner/plan/RowNumberNode.java | 110 + .../presto/sql/planner/plan/SampleNode.java | 136 + .../presto/sql/planner/plan/SemiJoinNode.java | 122 + .../presto/sql/planner/plan/SortNode.java | 87 + .../sql/planner/plan/TableCommitNode.java | 82 + .../sql/planner/plan/TableScanNode.java | 150 + .../sql/planner/plan/TableWriterNode.java | 268 + .../presto/sql/planner/plan/TopNNode.java | 109 + .../sql/planner/plan/TopNRowNumberNode.java | 147 + .../presto/sql/planner/plan/UnionNode.java | 135 + .../presto/sql/planner/plan/UnnestNode.java | 107 + .../presto/sql/planner/plan/ValuesNode.java | 74 + .../presto/sql/planner/plan/WindowNode.java | 228 + .../presto/sql/relational/CallExpression.java | 89 + .../sql/relational/ConstantExpression.java | 76 + .../presto/sql/relational/Expressions.java | 89 + .../relational/InputReferenceExpression.java | 79 + .../presto/sql/relational/RowExpression.java | 31 + .../sql/relational/RowExpressionVisitor.java | 21 + .../presto/sql/relational/Signatures.java | 161 + .../SqlToRowExpressionTranslator.java | 516 + .../optimizer/ExpressionOptimizer.java | 158 + .../presto/testing/LocalQueryRunner.java | 565 + .../presto/testing/MaterializedResult.java | 301 + .../presto/testing/MaterializedRow.java | 149 + .../presto/testing/MaterializingOperator.java | 138 + .../presto/testing/NullOutputOperator.java | 124 + .../facebook/presto/testing/QueryRunner.java | 46 + .../testing/RunLengthBlockEncoding.java | 110 + .../presto/testing/RunLengthEncodedBlock.java | 203 + .../presto/testing/TestingTaskContext.java | 61 + .../presto/type/ArrayParametricType.java | 45 + .../com/facebook/presto/type/ArrayType.java | 185 + .../facebook/presto/type/BigintOperators.java | 175 + .../presto/type/BooleanOperators.java | 125 + .../com/facebook/presto/type/ColorType.java | 96 + .../facebook/presto/type/DateOperators.java | 147 + .../presto/type/DateTimeOperators.java | 267 + .../facebook/presto/type/DoubleOperators.java | 179 + .../presto/type/HyperLogLogOperators.java | 41 + .../presto/type/IntervalDayTimeOperators.java | 165 + .../type/IntervalYearMonthOperators.java | 164 + .../facebook/presto/type/JsonPathType.java | 61 + .../com/facebook/presto/type/JsonType.java | 103 + .../facebook/presto/type/LikeFunctions.java | 152 + .../facebook/presto/type/LikePatternType.java | 61 + .../presto/type/MapParametricType.java | 45 + .../com/facebook/presto/type/MapType.java | 226 + .../facebook/presto/type/ParametricType.java | 25 + .../presto/type/RawSliceSerializer.java | 32 + .../com/facebook/presto/type/RegexpType.java | 61 + .../presto/type/RowParametricType.java | 72 + .../com/facebook/presto/type/RowType.java | 219 + .../com/facebook/presto/type/SqlType.java | 26 + .../facebook/presto/type/TimeOperators.java | 139 + .../type/TimeWithTimeZoneOperators.java | 125 + .../presto/type/TimestampOperators.java | 158 + .../type/TimestampWithTimeZoneOperators.java | 148 + .../presto/type/TypeDeserializer.java | 46 + .../facebook/presto/type/TypeJsonUtils.java | 190 + .../facebook/presto/type/TypeRegistry.java | 165 + .../com/facebook/presto/type/TypeUtils.java | 280 + .../com/facebook/presto/type/UnknownType.java | 86 + .../presto/type/VarbinaryOperators.java | 90 + .../presto/type/VarcharOperators.java | 192 + .../com/facebook/presto/util/CpuTimer.java | 165 + .../facebook/presto/util/DateTimeUtils.java | 445 + .../presto/util/DateTimeZoneIndex.java | 93 + .../com/facebook/presto/util/Failures.java | 118 + .../presto/util/GenerateTimeZoneIndex.java | 127 + .../facebook/presto/util/GraphvizPrinter.java | 594 + .../presto/util/ImmutableCollectors.java | 63 + .../facebook/presto/util/JsonPlanPrinter.java | 286 + .../com/facebook/presto/util/MoreFutures.java | 144 + .../util/NodeIdUserAgentRequestFilter.java | 64 + .../presto/util/QueryExplanation.java | 41 + .../com/facebook/presto/util/Reflection.java | 58 + .../presto/util/ThreadLocalCache.java | 74 + .../com/facebook/presto/util/Threads.java | 69 + .../java/com/facebook/presto/util/Types.java | 33 + .../facebook/presto/util/array/BigArrays.java | 65 + .../presto/util/array/BlockBigArray.java | 77 + .../presto/util/array/BooleanBigArray.java | 121 + .../presto/util/array/ByteBigArray.java | 121 + .../presto/util/array/DoubleBigArray.java | 135 + .../presto/util/array/IntBigArray.java | 145 + .../presto/util/array/LongBigArray.java | 145 + .../presto/util/array/ObjectBigArray.java | 121 + .../presto/util/array/SliceBigArray.java | 77 + .../presto/server/query-execution.html | 204 + .../com/facebook/presto/server/thread.html | 188 + .../src/main/resources/webapp/favicon.ico | Bin 0 -> 318 bytes .../src/main/resources/webapp/index.html | 293 + .../src/main/resources/webapp/presto.png | Bin 0 -> 19402 bytes .../src/main/resources/webapp/query.html | 688 + .../src/main/resources/webapp/timeline.html | 176 + .../src/main/resources/webapp/units.js | 77 + .../webapp/vendor/bootstrap/css/bootstrap.css | 6584 +++ .../vendor/bootstrap/css/bootstrap.css.map | 1 + .../vendor/bootstrap/css/bootstrap.min.css | 5 + .../img/glyphicons-halflings-white.png | Bin 0 -> 8777 bytes .../bootstrap/img/glyphicons-halflings.png | Bin 0 -> 12799 bytes .../webapp/vendor/bootstrap/js/bootstrap.js | 2317 + .../vendor/bootstrap/js/bootstrap.min.js | 7 + .../webapp/vendor/d3-tip/d3.tip.v0.6.3.js | 280 + .../resources/webapp/vendor/d3/d3-3.3.4.js | 9294 ++++ .../webapp/vendor/dagre-d3/dagre-d3-0.3.2.js | 11825 +++++ .../webapp/vendor/dagre-d3/dagre-d3.min.js | 2 + .../vendor/highlightjs/7.3/highlight.min.js | 1 + .../highlightjs/7.3/styles/idea.min.css | 1 + .../webapp/vendor/react/JSXTransformer.js | 15924 +++++++ .../resources/webapp/vendor/react/react.js | 19541 +++++++++ .../webapp/vendor/react/react.min.js | 15 + .../vis/dist/img/network/acceptDeleteIcon.png | Bin 0 -> 20675 bytes .../vis/dist/img/network/addNodeIcon.png | Bin 0 -> 20998 bytes .../vendor/vis/dist/img/network/backIcon.png | Bin 0 -> 20802 bytes .../vis/dist/img/network/connectIcon.png | Bin 0 -> 20764 bytes .../vendor/vis/dist/img/network/cross.png | Bin 0 -> 18303 bytes .../vendor/vis/dist/img/network/cross2.png | Bin 0 -> 17768 bytes .../vis/dist/img/network/deleteIcon.png | Bin 0 -> 20981 bytes .../vendor/vis/dist/img/network/downArrow.png | Bin 0 -> 4460 bytes .../vendor/vis/dist/img/network/editIcon.png | Bin 0 -> 21016 bytes .../vendor/vis/dist/img/network/leftArrow.png | Bin 0 -> 4531 bytes .../vendor/vis/dist/img/network/minus.png | Bin 0 -> 4147 bytes .../vendor/vis/dist/img/network/plus.png | Bin 0 -> 4341 bytes .../vis/dist/img/network/rightArrow.png | Bin 0 -> 4514 bytes .../vendor/vis/dist/img/network/upArrow.png | Bin 0 -> 4461 bytes .../vis/dist/img/network/zoomExtends.png | Bin 0 -> 4464 bytes .../vendor/vis/dist/img/timeline/delete.png | Bin 0 -> 665 bytes .../resources/webapp/vendor/vis/dist/vis.css | 810 + .../resources/webapp/vendor/vis/dist/vis.js | 35665 ++++++++++++++++ .../resources/webapp/vendor/vis/dist/vis.map | 1 + .../webapp/vendor/vis/dist/vis.min.css | 1 + .../webapp/vendor/vis/dist/vis.min.js | 44 + .../BenchmarkHashPagePartitionFunction.java | 130 + .../presto/BenchmarkPagesIndexPageSorter.java | 148 + .../com/facebook/presto/RowPageBuilder.java | 109 + .../com/facebook/presto/RowPagesBuilder.java | 147 + .../facebook/presto/SequencePageBuilder.java | 73 + .../com/facebook/presto/SessionTestUtils.java | 36 + .../facebook/presto/TestHiddenColumns.java | 68 + .../presto/TestPagesIndexPageSorter.java | 179 + .../presto/block/AbstractTestBlock.java | 186 + .../presto/block/BlockAssertions.java | 258 + .../presto/block/TestBlockBuilder.java | 42 + .../presto/block/TestFixedWidthBlock.java | 56 + .../presto/block/TestLazySliceArrayBlock.java | 84 + .../facebook/presto/block/TestPagesSerde.java | 128 + .../block/TestRunLengthEncodedBlock.java | 52 + .../presto/block/TestSliceArrayBlock.java | 45 + .../presto/block/TestVariableWidthBlock.java | 55 + .../ByteCodeExpressionAssertions.java | 90 + .../TestArithmeticByteCodeExpression.java | 149 + .../TestCastByteCodeExpression.java | 133 + .../TestComparisonByteCodeExpression.java | 186 + .../TestConstantByteCodeExpression.java | 68 + .../TestGetElementByteCodeExpression.java | 31 + .../TestGetFieldByteCodeExpression.java | 45 + .../TestInlineIfByteCodeExpression.java | 33 + .../TestInvokeByteCodeExpression.java | 58 + .../TestInvokeDynamicByteCodeExpression.java | 61 + .../TestLogicalByteCodeExpression.java | 54 + .../TestNewInstanceByteCodeExpression.java | 37 + .../expression/TestPopByteCodeExpression.java | 49 + .../TestSetFieldByteCodeExpression.java | 81 + .../TestSetVariableByteCodeExpression.java | 50 + .../concurrent/TestFairBatchExecutor.java | 160 + .../TestEmbeddedDiscoveryConfig.java | 46 + .../execution/MockRemoteTaskFactory.java | 256 + .../execution/TaskExecutorSimulator.java | 418 + .../presto/execution/TaskExecutorTest.java | 209 + .../presto/execution/TaskTestUtils.java | 106 + .../facebook/presto/execution/TestColumn.java | 36 + .../facebook/presto/execution/TestInput.java | 42 + .../presto/execution/TestNodeScheduler.java | 298 + .../execution/TestNodeSchedulerConfig.java | 58 + .../execution/TestPageSplitterUtil.java | 68 + .../execution/TestQueryIdGenerator.java | 76 + .../execution/TestQueryManagerConfig.java | 87 + .../execution/TestQueryQueueDefinition.java | 46 + .../presto/execution/TestQueryQueueRule.java | 47 + .../execution/TestQueryStateMachine.java | 319 + .../presto/execution/TestQueryStats.java | 119 + .../execution/TestResetSessionTask.java | 56 + .../TestResettableRandomizedIterator.java | 77 + .../presto/execution/TestSetSessionTask.java | 53 + .../presto/execution/TestSharedBuffer.java | 902 + .../presto/execution/TestSimpleDomain.java | 43 + .../presto/execution/TestSimpleMarker.java | 41 + .../presto/execution/TestSimpleRange.java | 41 + .../execution/TestSqlQueryQueueManager.java | 55 + .../execution/TestSqlStageExecution.java | 511 + .../presto/execution/TestSqlTask.java | 292 + .../presto/execution/TestSqlTaskManager.java | 302 + .../execution/TestStageStateMachine.java | 337 + .../presto/execution/TestStageStats.java | 118 + .../presto/execution/TestStateMachine.java | 324 + .../execution/TestTaskManagerConfig.java | 95 + .../execution/TestingPageSourceProvider.java | 43 + .../presto/execution/TestingSplit.java | 74 + .../TestHeartbeatFailureDetector.java | 98 + .../memory/TestMemoryManagerConfig.java | 54 + .../presto/memory/TestMemoryPools.java | 78 + .../TestReservedSystemMemoryConfig.java | 56 + .../metadata/TestDiscoveryNodeManager.java | 133 + .../presto/metadata/TestFunctionRegistry.java | 146 + .../presto/metadata/TestJsonTableHandle.java | 153 + .../metadata/TestQualifiedTablePrefix.java | 71 + .../presto/metadata/TestSignature.java | 168 + .../presto/metadata/TestingMetadata.java | 289 + .../presto/operator/BenchmarkGroupByHash.java | 358 + .../MockExchangeRequestProcessor.java | 215 + .../presto/operator/OperatorAssertion.java | 235 + .../presto/operator/PageAssertions.java | 38 + .../operator/TestAggregationOperator.java | 101 + .../operator/TestDistinctLimitOperator.java | 132 + .../facebook/presto/operator/TestDriver.java | 464 + .../presto/operator/TestDriverStats.java | 100 + .../presto/operator/TestExchangeClient.java | 323 + .../operator/TestExchangeClientConfig.java | 63 + .../presto/operator/TestExchangeOperator.java | 402 + .../TestFilterAndProjectOperator.java | 147 + .../presto/operator/TestGroupByHash.java | 203 + .../operator/TestHashAggregationOperator.java | 366 + .../presto/operator/TestHashJoinOperator.java | 503 + .../TestHashPartitionMaskOperator.java | 163 + .../operator/TestHashSemiJoinOperator.java | 303 + .../operator/TestHttpPageBufferClient.java | 548 + .../presto/operator/TestLimitOperator.java | 96 + .../operator/TestMarkDistinctOperator.java | 88 + .../presto/operator/TestOperatorStats.java | 237 + .../presto/operator/TestOrderByOperator.java | 189 + .../presto/operator/TestPipelineStats.java | 125 + .../operator/TestRecordProjectOperator.java | 153 + .../operator/TestRowNumberOperator.java | 352 + .../TestScanFilterAndProjectOperator.java | 148 + .../presto/operator/TestTaskStats.java | 112 + .../presto/operator/TestTopNOperator.java | 194 + .../operator/TestTopNRowNumberOperator.java | 163 + .../presto/operator/TestUnnestOperator.java | 154 + .../presto/operator/TestWindowOperator.java | 666 + .../AbstractTestAggregationFunction.java | 150 + ...actTestApproximateAggregationFunction.java | 214 + .../AbstractTestApproximateCountDistinct.java | 225 + .../aggregation/AggregationTestUtils.java | 448 + .../TestApproximateCountAggregation.java | 49 + ...tApproximateCountDistinctAggregations.java | 65 + .../TestApproximateCountDistinctDouble.java | 43 + .../TestApproximateCountDistinctLong.java | 43 + ...TestApproximateCountDistinctVarBinary.java | 48 + ...TestApproximateCountDoubleAggregation.java | 45 + .../TestApproximateCountLongAggregation.java | 45 + .../TestApproximateDoubleSumAggregation.java | 55 + .../TestApproximateLongSumAggregation.java | 55 + .../TestApproximatePercentileAggregation.java | 376 + .../aggregation/TestArbitraryAggregation.java | 170 + .../aggregation/TestArrayAggregation.java | 152 + .../aggregation/TestArrayMinAggregation.java | 60 + .../TestBooleanAndAggregation.java | 62 + .../TestBooleanMaxAggregation.java | 24 + .../TestBooleanMinAggregation.java | 24 + .../aggregation/TestBooleanOrAggregation.java | 62 + .../TestCorrelationAggregation.java | 81 + .../aggregation/TestCountAggregation.java | 61 + .../TestCountColumnAggregation.java | 56 + .../aggregation/TestCountIfAggregation.java | 62 + .../aggregation/TestCountNullAggregation.java | 99 + .../TestCovariancePopAggregation.java | 59 + .../TestCovarianceSampAggregation.java | 72 + .../aggregation/TestDateMaxAggregation.java | 59 + ...stDoubleApproximateAverageAggregation.java | 57 + .../TestDoubleAverageAggregation.java | 64 + .../aggregation/TestDoubleMaxAggregation.java | 59 + .../aggregation/TestDoubleMinAggregation.java | 59 + .../TestDoubleStdDevAggregation.java | 67 + .../TestDoubleStdDevPopAggregation.java | 67 + .../aggregation/TestDoubleSumAggregation.java | 64 + .../TestDoubleVarianceAggregation.java | 67 + .../TestDoubleVariancePopAggregation.java | 67 + ...TestLongApproximateAverageAggregation.java | 57 + .../TestLongAverageAggregation.java | 64 + .../aggregation/TestLongMaxAggregation.java | 59 + .../aggregation/TestLongMinAggregation.java | 59 + .../TestLongStdDevAggregation.java | 67 + .../TestLongStdDevPopAggregation.java | 67 + .../aggregation/TestLongSumAggregation.java | 64 + .../TestLongVarianceAggregation.java | 67 + .../TestLongVariancePopAggregation.java | 67 + .../aggregation/TestMapAggAggregation.java | 228 + .../aggregation/TestMaxByAggregation.java | 263 + .../aggregation/TestMinByAggregation.java | 222 + .../aggregation/TestNumericHistogram.java | 141 + .../TestNumericHistogramAggregation.java | 128 + .../TestRegrInterceptAggregation.java | 94 + .../aggregation/TestRegrSlopeAggregation.java | 79 + .../aggregation/TestSimpleTypedSet.java | 103 + .../aggregation/TestStateCompiler.java | 221 + .../TestVarBinaryMaxAggregation.java | 69 + .../TestVarBinaryMinAggregation.java | 69 + .../index/TestTupleFilterProcessor.java | 104 + .../scalar/AbstractTestFunctions.java | 89 + .../operator/scalar/CustomFunctions.java | 46 + .../operator/scalar/FunctionAssertions.java | 698 + .../scalar/RegexpFunctionsBenchmark.java | 124 + .../scalar/StringFunctionsBenchmark.java | 267 + .../operator/scalar/TestColorFunctions.java | 134 + .../operator/scalar/TestConditions.java | 280 + .../operator/scalar/TestCustomFunctions.java | 51 + .../scalar/TestDateTimeFunctions.java | 823 + .../operator/scalar/TestJsonExtract.java | 357 + .../operator/scalar/TestJsonFunctions.java | 308 + .../operator/scalar/TestMathFunctions.java | 440 + .../operator/scalar/TestRegexpFunctions.java | 149 + .../operator/scalar/TestStringFunctions.java | 389 + .../operator/scalar/TestUrlFunctions.java | 66 + .../scalar/TestVarbinaryFunctions.java | 156 + .../window/AbstractTestWindowFunction.java | 65 + .../window/TestAggregateWindowFunction.java | 406 + .../TestCumulativeDistributionFunction.java | 138 + .../window/TestDenseRankFunction.java | 56 + .../window/TestFirstValueFunction.java | 131 + .../operator/window/TestLagFunction.java | 193 + .../window/TestLastValueFunction.java | 130 + .../operator/window/TestLeadFunction.java | 193 + .../operator/window/TestNTileFunction.java | 99 + .../operator/window/TestNthValueFunction.java | 164 + .../window/TestPercentRankFunction.java | 138 + .../operator/window/TestRankFunction.java | 56 + .../window/TestRowNumberFunction.java | 61 + .../operator/window/WindowAssertions.java | 78 + .../presto/server/MockHttpServletRequest.java | 489 + .../presto/server/NoOpFailureDetector.java | 30 + .../presto/server/TestExecuteResource.java | 84 + .../server/TestFailureDetectorConfig.java | 58 + .../presto/server/TestNodeResource.java | 75 + .../server/TestPluginManagerConfig.java | 58 + .../server/TestPrestoJvmRequirements.java | 28 + .../presto/server/TestResourceUtil.java | 70 + .../facebook/presto/server/TestServer.java | 133 + .../presto/server/TestServerConfig.java | 55 + .../presto/split/TestInternalSplit.java | 41 + .../presto/split/TestJmxSplitManager.java | 160 + .../presto/sql/TestExpressionInterpreter.java | 892 + .../presto/sql/TestExpressionOptimizer.java | 47 + .../presto/sql/TestLikeFunctions.java | 96 + .../sql/TestSqlToRowExpressionTranslator.java | 49 + .../presto/sql/analyzer/TestAnalyzer.java | 868 + .../sql/analyzer/TestFeaturesConfig.java | 71 + .../sql/gen/BenchmarkPageProcessor.java | 218 + .../sql/gen/TestExpressionCompiler.java | 1337 + .../presto/sql/gen/TestJoinCompiler.java | 257 + .../presto/sql/gen/TestJoinProbeCompiler.java | 138 + .../sql/planner/TestDeterminismEvaluator.java | 47 + .../sql/planner/TestDomainTranslator.java | 1035 + .../TestEffectivePredicateExtractor.java | 680 + .../sql/planner/TestEqualityInference.java | 429 + .../TestInterpretedFilterFunction.java | 209 + .../TestInterpretedProjectionFunction.java | 200 + .../planner/TestLocalExecutionPlanner.java | 60 + .../sql/planner/TestSymbolAllocator.java | 40 + .../sql/planner/TestingColumnHandle.java | 68 + .../sql/planner/TestingTableHandle.java | 21 + .../optimizations/TestAddExchanges.java | 773 + .../optimizations/TestLocalProperties.java | 772 + .../presto/type/AbstractTestType.java | 487 + .../presto/type/BooleanOperatorsTest.java | 136 + .../presto/type/TestArrayOperators.java | 515 + .../presto/type/TestBigintArrayType.java | 62 + .../presto/type/TestBigintOperators.java | 226 + .../facebook/presto/type/TestBigintType.java | 52 + .../presto/type/TestBigintVarcharMapType.java | 50 + .../presto/type/TestBooleanOperators.java | 122 + .../facebook/presto/type/TestBooleanType.java | 52 + .../facebook/presto/type/TestColorType.java | 53 + .../com/facebook/presto/type/TestDate.java | 198 + .../presto/type/TestDateTimeOperators.java | 328 + .../facebook/presto/type/TestDateType.java | 53 + .../presto/type/TestDoubleOperators.java | 202 + .../facebook/presto/type/TestDoubleType.java | 52 + .../presto/type/TestIntervalDayTime.java | 267 + .../presto/type/TestIntervalDayTimeType.java | 53 + .../presto/type/TestIntervalYearMonth.java | 205 + .../type/TestIntervalYearMonthType.java | 53 + .../facebook/presto/type/TestJsonType.java | 45 + .../presto/type/TestMapOperators.java | 291 + .../presto/type/TestRowOperators.java | 135 + .../presto/type/TestSimpleRowType.java | 66 + .../com/facebook/presto/type/TestTime.java | 189 + .../facebook/presto/type/TestTimeType.java | 53 + .../presto/type/TestTimeWithTimeZone.java | 241 + .../presto/type/TestTimeWithTimeZoneType.java | 57 + .../facebook/presto/type/TestTimestamp.java | 242 + .../presto/type/TestTimestampType.java | 53 + .../type/TestTimestampWithTimeZone.java | 319 + .../type/TestTimestampWithTimeZoneType.java | 57 + .../facebook/presto/type/TestUnknownType.java | 39 + .../presto/type/TestVarbinaryType.java | 55 + .../presto/type/TestVarcharOperators.java | 120 + .../facebook/presto/type/TestVarcharType.java | 54 + .../presto/util/InfiniteRecordSet.java | 139 + .../presto/util/TestThreadLocalCache.java | 71 + .../presto/util/TestTimeZoneUtils.java | 83 + .../com/facebook/presto/util/TestTypes.java | 51 + .../src/test/resources/queue_config.json | 64 + .../resources/queue_config_bad_cycle.json | 47 + .../resources/queue_config_bad_selfcycle.json | 16 + presto-ml/pom.xml | 134 + .../ml/AbstractFeatureTransformation.java | 31 + .../facebook/presto/ml/AbstractSvmModel.java | 137 + .../com/facebook/presto/ml/Classifier.java | 20 + .../ml/ClassifierFeatureTransformer.java | 67 + .../java/com/facebook/presto/ml/Dataset.java | 56 + ...luateClassifierPredictionsAggregation.java | 115 + .../EvaluateClassifierPredictionsState.java | 32 + ...uateClassifierPredictionsStateFactory.java | 151 + ...eClassifierPredictionsStateSerializer.java | 80 + .../presto/ml/FeatureTransformation.java | 22 + .../presto/ml/FeatureUnitNormalizer.java | 123 + .../com/facebook/presto/ml/FeatureVector.java | 56 + .../ml/FeatureVectorUnitNormalizer.java | 70 + .../presto/ml/LearnClassifierAggregation.java | 62 + .../ml/LearnLibSvmClassifierAggregation.java | 72 + .../ml/LearnLibSvmRegressorAggregation.java | 72 + ...arnLibSvmVarcharClassifierAggregation.java | 60 + .../presto/ml/LearnRegressorAggregation.java | 63 + .../com/facebook/presto/ml/LearnState.java | 46 + .../facebook/presto/ml/LearnStateFactory.java | 207 + .../presto/ml/LearnStateSerializer.java | 42 + .../ml/LearnVarcharClassifierAggregation.java | 52 + .../com/facebook/presto/ml/LibSvmUtils.java | 100 + .../facebook/presto/ml/MLFunctionFactory.java | 47 + .../com/facebook/presto/ml/MLFunctions.java | 174 + .../java/com/facebook/presto/ml/MLPlugin.java | 63 + .../java/com/facebook/presto/ml/Model.java | 31 + .../com/facebook/presto/ml/ModelUtils.java | 217 + .../com/facebook/presto/ml/Regressor.java | 20 + .../ml/RegressorFeatureTransformer.java | 67 + .../presto/ml/StringClassifierAdapter.java | 110 + .../com/facebook/presto/ml/SvmClassifier.java | 79 + .../com/facebook/presto/ml/SvmRegressor.java | 79 + .../ml/type/ClassifierParametricType.java | 40 + .../presto/ml/type/ClassifierType.java | 49 + .../facebook/presto/ml/type/ModelType.java | 83 + .../presto/ml/type/RegressorType.java | 31 + presto-ml/src/main/provisio/assembly.xml | 4 + .../ml/TestEvaluateClassifierPredictions.java | 77 + .../presto/ml/TestFeatureTransformations.java | 78 + .../presto/ml/TestLearnAggregations.java | 115 + .../com/facebook/presto/ml/TestMLQueries.java | 87 + .../presto/ml/TestModelSerialization.java | 92 + .../com/facebook/presto/ml/TestUtils.java | 40 + presto-mysql/pom.xml | 173 + .../presto/plugin/mysql/MySqlClient.java | 112 + .../plugin/mysql/MySqlClientModule.java | 38 + .../presto/plugin/mysql/MySqlConfig.java | 65 + .../presto/plugin/mysql/MySqlPlugin.java | 25 + .../presto/plugin/mysql/MySqlQueryRunner.java | 86 + .../presto/plugin/mysql/TestMySqlConfig.java | 53 + .../mysql/TestMySqlDistributedQueries.java | 62 + .../mysql/TestMySqlIntegrationSmokeTest.java | 49 + .../presto/plugin/mysql/TestMySqlPlugin.java | 33 + presto-oracle/pom.xml | 33 + .../presto/plugin/oracle/OracleClient.java | 38 + .../plugin/oracle/OracleClientModule.java | 38 + .../presto/plugin/oracle/OraclePlugin.java | 36 + .../presto/plugin/oracle/AppTest.java | 38 + presto-orc/pom.xml | 161 + .../presto/orc/AbstractOrcDataSource.java | 237 + .../facebook/presto/orc/BooleanVector.java | 45 + .../com/facebook/presto/orc/DiskRange.java | 99 + .../com/facebook/presto/orc/DoubleVector.java | 45 + .../presto/orc/FileOrcDataSource.java | 49 + .../com/facebook/presto/orc/LongVector.java | 45 + .../com/facebook/presto/orc/ObjectVector.java | 37 + .../presto/orc/OrcCorruptionException.java | 37 + .../facebook/presto/orc/OrcDataSource.java | 43 + .../presto/orc/OrcDataSourceUtils.java | 90 + .../com/facebook/presto/orc/OrcPredicate.java | 40 + .../com/facebook/presto/orc/OrcReader.java | 240 + .../facebook/presto/orc/OrcRecordReader.java | 405 + .../com/facebook/presto/orc/RowGroup.java | 58 + .../com/facebook/presto/orc/SliceVector.java | 44 + .../facebook/presto/orc/StreamDescriptor.java | 83 + .../com/facebook/presto/orc/StreamId.java | 76 + .../java/com/facebook/presto/orc/Stripe.java | 70 + .../com/facebook/presto/orc/StripeReader.java | 422 + .../presto/orc/TupleDomainOrcPredicate.java | 183 + .../java/com/facebook/presto/orc/Vector.java | 24 + .../presto/orc/block/BlockReader.java | 42 + .../presto/orc/block/BlockReaders.java | 64 + .../presto/orc/block/BooleanBlockReader.java | 113 + .../presto/orc/block/ByteBlockReader.java | 114 + .../presto/orc/block/DateBlockReader.java | 114 + .../presto/orc/block/DoubleBlockReader.java | 114 + .../presto/orc/block/FloatBlockReader.java | 115 + .../presto/orc/block/ListBlockReader.java | 152 + .../presto/orc/block/LongBlockReader.java | 96 + .../orc/block/LongDictionaryBlockReader.java | 145 + .../orc/block/LongDirectBlockReader.java | 110 + .../presto/orc/block/MapBlockReader.java | 162 + .../presto/orc/block/SliceBlockReader.java | 94 + .../orc/block/SliceDictionaryBlockReader.java | 219 + .../orc/block/SliceDirectBlockReader.java | 157 + .../presto/orc/block/StructBlockReader.java | 140 + .../orc/block/TimestampBlockReader.java | 134 + .../checkpoint/BooleanStreamCheckpoint.java | 58 + .../checkpoint/ByteArrayStreamCheckpoint.java | 50 + .../orc/checkpoint/ByteStreamCheckpoint.java | 59 + .../presto/orc/checkpoint/Checkpoints.java | 428 + .../checkpoint/DoubleStreamCheckpoint.java | 50 + .../orc/checkpoint/FloatStreamCheckpoint.java | 50 + .../orc/checkpoint/InputStreamCheckpoint.java | 64 + .../InvalidCheckpointException.java | 25 + .../orc/checkpoint/LongStreamCheckpoint.java | 19 + .../checkpoint/LongStreamDwrfCheckpoint.java | 50 + .../checkpoint/LongStreamV1Checkpoint.java | 59 + .../checkpoint/LongStreamV2Checkpoint.java | 59 + ...GroupDictionaryLengthStreamCheckpoint.java | 53 + .../orc/checkpoint/StreamCheckpoint.java | 18 + .../orc/metadata/BooleanStatistics.java | 29 + .../presto/orc/metadata/ColumnEncoding.java | 57 + .../presto/orc/metadata/ColumnStatistics.java | 74 + .../presto/orc/metadata/CompressionKind.java | 19 + .../presto/orc/metadata/DateStatistics.java | 39 + .../presto/orc/metadata/DoubleStatistics.java | 39 + .../orc/metadata/DwrfMetadataReader.java | 331 + .../facebook/presto/orc/metadata/Footer.java | 76 + .../orc/metadata/IntegerStatistics.java | 37 + .../presto/orc/metadata/Metadata.java | 31 + .../presto/orc/metadata/MetadataReader.java | 36 + .../orc/metadata/OrcMetadataReader.java | 430 + .../facebook/presto/orc/metadata/OrcType.java | 105 + .../presto/orc/metadata/PostScript.java | 76 + .../presto/orc/metadata/RangeStatistics.java | 20 + .../presto/orc/metadata/RowGroupIndex.java | 42 + .../facebook/presto/orc/metadata/Stream.java | 78 + .../presto/orc/metadata/StringStatistics.java | 41 + .../presto/orc/metadata/StripeFooter.java | 42 + .../orc/metadata/StripeInformation.java | 71 + .../presto/orc/metadata/StripeStatistics.java | 35 + .../facebook/presto/orc/orc-file-format.md | 70 + .../presto/orc/reader/BlockStreamReader.java | 176 + .../orc/reader/BooleanStreamReader.java | 161 + .../presto/orc/reader/ByteStreamReader.java | 163 + .../presto/orc/reader/DoubleStreamReader.java | 163 + .../presto/orc/reader/FloatStreamReader.java | 164 + .../reader/LongDictionaryStreamReader.java | 223 + .../orc/reader/LongDirectStreamReader.java | 163 + .../presto/orc/reader/LongStreamReader.java | 91 + .../presto/orc/reader/OrcReaderUtils.java | 32 + .../reader/SliceDictionaryStreamReader.java | 298 + .../orc/reader/SliceDirectStreamReader.java | 218 + .../presto/orc/reader/SliceStreamReader.java | 92 + .../presto/orc/reader/StreamReader.java | 34 + .../presto/orc/reader/StreamReaders.java | 59 + .../orc/reader/TimestampStreamReader.java | 233 + .../java/com/facebook/presto/orc/results.txt | 47 + .../presto/orc/stream/BooleanStream.java | 212 + .../presto/orc/stream/ByteArrayStream.java | 67 + .../presto/orc/stream/ByteStream.java | 138 + .../orc/stream/CheckpointStreamSource.java | 70 + .../presto/orc/stream/DoubleStream.java | 104 + .../presto/orc/stream/FloatStream.java | 109 + .../presto/orc/stream/LongDecode.java | 188 + .../presto/orc/stream/LongStream.java | 40 + .../presto/orc/stream/LongStreamDwrf.java | 129 + .../presto/orc/stream/LongStreamV1.java | 188 + .../presto/orc/stream/LongStreamV2.java | 456 + .../orc/stream/MissingStreamSource.java | 47 + .../presto/orc/stream/OrcInputStream.java | 299 + .../presto/orc/stream/OrcStreamUtils.java | 66 + .../RowGroupDictionaryLengthStream.java | 52 + .../presto/orc/stream/StreamSource.java | 27 + .../presto/orc/stream/StreamSources.java | 57 + .../presto/orc/stream/ValueStream.java | 29 + .../presto/orc/stream/ValueStreamSource.java | 54 + .../presto/orc/stream/ValueStreams.java | 158 + .../presto/orc/AbstractTestOrcReader.java | 481 + .../com/facebook/presto/orc/OrcTester.java | 811 + .../presto/orc/TestFullOrcReader.java | 26 + .../presto/orc/TestOrcDataSourceUtils.java | 85 + .../facebook/presto/orc/TestOrcReader.java | 26 + .../presto/orc/TestOrcReaderPositions.java | 175 + .../orc/TestTupleDomainOrcPredicate.java | 201 + .../presto/orc/TestingOrcPredicate.java | 337 + .../orc/metadata/TestOrcMetadataReader.java | 92 + presto-parser/pom.xml | 75 + .../com/facebook/presto/sql/parser/SqlBase.g4 | 566 + .../presto/sql/ExpressionFormatter.java | 583 + .../com/facebook/presto/sql/QueryUtil.java | 214 + .../facebook/presto/sql/Serialization.java | 82 + .../com/facebook/presto/sql/SqlFormatter.java | 726 + .../com/facebook/presto/sql/TreePrinter.java | 357 + .../presto/sql/parser/AstBuilder.java | 1317 + .../sql/parser/CaseInsensitiveStream.java | 93 + .../presto/sql/parser/DelimiterLexer.java | 126 + .../presto/sql/parser/IdentifierSymbol.java | 32 + .../presto/sql/parser/ParsingException.java | 59 + .../facebook/presto/sql/parser/SqlParser.java | 178 + .../presto/sql/parser/SqlParserOptions.java | 44 + .../presto/sql/parser/StatementSplitter.java | 161 + .../presto/sql/testing/TreeAssertions.java | 114 + .../presto/sql/tree/AliasedRelation.java | 104 + .../facebook/presto/sql/tree/AllColumns.java | 81 + .../facebook/presto/sql/tree/Approximate.java | 68 + .../sql/tree/ArithmeticBinaryExpression.java | 104 + .../sql/tree/ArithmeticUnaryExpression.java | 94 + .../presto/sql/tree/ArrayConstructor.java | 68 + .../facebook/presto/sql/tree/AstVisitor.java | 474 + .../presto/sql/tree/BetweenPredicate.java | 90 + .../presto/sql/tree/BooleanLiteral.java | 67 + .../com/facebook/presto/sql/tree/Cast.java | 84 + .../presto/sql/tree/CoalesceExpression.java | 74 + .../presto/sql/tree/ComparisonExpression.java | 113 + .../facebook/presto/sql/tree/CreateTable.java | 89 + .../presto/sql/tree/CreateTableAsSelect.java | 94 + .../facebook/presto/sql/tree/CreateView.java | 86 + .../facebook/presto/sql/tree/CurrentTime.java | 102 + .../DefaultExpressionTraversalVisitor.java | 28 + .../sql/tree/DefaultTraversalVisitor.java | 434 + .../com/facebook/presto/sql/tree/Delete.java | 78 + .../presto/sql/tree/DoubleLiteral.java | 65 + .../facebook/presto/sql/tree/DropTable.java | 76 + .../facebook/presto/sql/tree/DropView.java | 76 + .../com/facebook/presto/sql/tree/Except.java | 90 + .../presto/sql/tree/ExistsPredicate.java | 64 + .../com/facebook/presto/sql/tree/Explain.java | 85 + .../presto/sql/tree/ExplainFormat.java | 69 + .../presto/sql/tree/ExplainOption.java | 24 + .../facebook/presto/sql/tree/ExplainType.java | 68 + .../facebook/presto/sql/tree/Expression.java | 32 + .../presto/sql/tree/ExpressionRewriter.java | 157 + .../sql/tree/ExpressionTreeRewriter.java | 719 + .../com/facebook/presto/sql/tree/Extract.java | 102 + .../facebook/presto/sql/tree/FrameBound.java | 92 + .../presto/sql/tree/FunctionCall.java | 94 + .../presto/sql/tree/GenericLiteral.java | 70 + .../presto/sql/tree/IfExpression.java | 79 + .../presto/sql/tree/InListExpression.java | 63 + .../facebook/presto/sql/tree/InPredicate.java | 73 + .../presto/sql/tree/InputReference.java | 63 + .../com/facebook/presto/sql/tree/Insert.java | 107 + .../facebook/presto/sql/tree/Intersect.java | 82 + .../presto/sql/tree/IntervalLiteral.java | 124 + .../presto/sql/tree/IsNotNullPredicate.java | 64 + .../presto/sql/tree/IsNullPredicate.java | 64 + .../com/facebook/presto/sql/tree/Join.java | 127 + .../presto/sql/tree/JoinCriteria.java | 27 + .../com/facebook/presto/sql/tree/JoinOn.java | 62 + .../facebook/presto/sql/tree/JoinUsing.java | 68 + .../presto/sql/tree/LikePredicate.java | 89 + .../com/facebook/presto/sql/tree/Literal.java | 24 + .../sql/tree/LogicalBinaryExpression.java | 105 + .../facebook/presto/sql/tree/LongLiteral.java | 70 + .../facebook/presto/sql/tree/NaturalJoin.java | 41 + .../com/facebook/presto/sql/tree/Node.java | 32 + .../presto/sql/tree/NotExpression.java | 64 + .../presto/sql/tree/NullIfExpression.java | 76 + .../facebook/presto/sql/tree/NullLiteral.java | 43 + .../presto/sql/tree/PartitionElement.java | 78 + .../presto/sql/tree/QualifiedName.java | 146 + .../sql/tree/QualifiedNameReference.java | 66 + .../com/facebook/presto/sql/tree/Query.java | 118 + .../facebook/presto/sql/tree/QueryBody.java | 33 + .../presto/sql/tree/QuerySpecification.java | 139 + .../facebook/presto/sql/tree/Relation.java | 33 + .../presto/sql/tree/RenameColumn.java | 86 + .../facebook/presto/sql/tree/RenameTable.java | 77 + .../presto/sql/tree/ResetSession.java | 67 + .../com/facebook/presto/sql/tree/Row.java | 63 + .../presto/sql/tree/SampledRelation.java | 119 + .../sql/tree/SearchedCaseExpression.java | 82 + .../com/facebook/presto/sql/tree/Select.java | 90 + .../facebook/presto/sql/tree/SelectItem.java | 19 + .../presto/sql/tree/SetOperation.java | 24 + .../facebook/presto/sql/tree/SetSession.java | 76 + .../presto/sql/tree/ShowCatalogs.java | 47 + .../facebook/presto/sql/tree/ShowColumns.java | 68 + .../presto/sql/tree/ShowFunctions.java | 47 + .../presto/sql/tree/ShowPartitions.java | 99 + .../facebook/presto/sql/tree/ShowSchemas.java | 62 + .../facebook/presto/sql/tree/ShowSession.java | 47 + .../facebook/presto/sql/tree/ShowTables.java | 81 + .../presto/sql/tree/SimpleCaseExpression.java | 93 + .../presto/sql/tree/SingleColumn.java | 90 + .../facebook/presto/sql/tree/SortItem.java | 106 + .../facebook/presto/sql/tree/Statement.java | 24 + .../presto/sql/tree/StringLiteral.java | 75 + .../presto/sql/tree/SubqueryExpression.java | 61 + .../presto/sql/tree/SubscriptExpression.java | 68 + .../com/facebook/presto/sql/tree/Table.java | 71 + .../presto/sql/tree/TableElement.java | 96 + .../presto/sql/tree/TableSubquery.java | 71 + .../facebook/presto/sql/tree/TimeLiteral.java | 64 + .../presto/sql/tree/TimestampLiteral.java | 65 + .../com/facebook/presto/sql/tree/Union.java | 82 + .../com/facebook/presto/sql/tree/Unnest.java | 81 + .../com/facebook/presto/sql/tree/Use.java | 85 + .../com/facebook/presto/sql/tree/Values.java | 70 + .../facebook/presto/sql/tree/WhenClause.java | 73 + .../com/facebook/presto/sql/tree/Window.java | 88 + .../facebook/presto/sql/tree/WindowFrame.java | 92 + .../com/facebook/presto/sql/tree/With.java | 84 + .../facebook/presto/sql/tree/WithQuery.java | 88 + .../presto/sql/parser/TestSqlParser.java | 891 + .../sql/parser/TestStatementBuilder.java | 265 + .../sql/parser/TestStatementSplitter.java | 251 + .../src/test/resources/tpch/queries/1.sql | 28 + .../src/test/resources/tpch/queries/10.sql | 38 + .../src/test/resources/tpch/queries/11.sql | 34 + .../src/test/resources/tpch/queries/12.sql | 35 + .../src/test/resources/tpch/queries/13.sql | 27 + .../src/test/resources/tpch/queries/14.sql | 20 + .../src/test/resources/tpch/queries/15.sql | 40 + .../src/test/resources/tpch/queries/16.sql | 37 + .../src/test/resources/tpch/queries/17.sql | 24 + .../src/test/resources/tpch/queries/18.sql | 39 + .../src/test/resources/tpch/queries/19.sql | 42 + .../src/test/resources/tpch/queries/2.sql | 50 + .../src/test/resources/tpch/queries/20.sql | 44 + .../src/test/resources/tpch/queries/21.sql | 46 + .../src/test/resources/tpch/queries/22.sql | 44 + .../src/test/resources/tpch/queries/3.sql | 29 + .../src/test/resources/tpch/queries/4.sql | 28 + .../src/test/resources/tpch/queries/5.sql | 31 + .../src/test/resources/tpch/queries/6.sql | 16 + .../src/test/resources/tpch/queries/7.sql | 46 + .../src/test/resources/tpch/queries/8.sql | 44 + .../src/test/resources/tpch/queries/9.sql | 39 + presto-postgresql/pom.xml | 163 + .../plugin/postgresql/PostgreSqlClient.java | 60 + .../postgresql/PostgreSqlClientModule.java | 33 + .../plugin/postgresql/PostgreSqlPlugin.java | 25 + .../postgresql/PostgreSqlQueryRunner.java | 101 + .../TestPostgreSqlDistributedQueries.java | 65 + .../TestPostgreSqlIntegrationSmokeTest.java | 51 + .../postgresql/TestPostgreSqlPlugin.java | 33 + presto-raptor/pom.xml | 221 + .../facebook/presto/raptor/PluginInfo.java | 38 + .../presto/raptor/RaptorColumnHandle.java | 117 + .../presto/raptor/RaptorConnector.java | 98 + .../presto/raptor/RaptorConnectorFactory.java | 118 + .../presto/raptor/RaptorConnectorId.java | 54 + .../presto/raptor/RaptorErrorCode.java | 40 + .../presto/raptor/RaptorHandleResolver.java | 100 + .../raptor/RaptorInsertTableHandle.java | 112 + .../presto/raptor/RaptorMetadata.java | 468 + .../facebook/presto/raptor/RaptorModule.java | 72 + .../raptor/RaptorOutputTableHandle.java | 110 + .../presto/raptor/RaptorPageSink.java | 183 + .../presto/raptor/RaptorPageSinkProvider.java | 88 + .../raptor/RaptorPageSourceProvider.java | 61 + .../presto/raptor/RaptorPartition.java | 59 + .../facebook/presto/raptor/RaptorPlugin.java | 127 + .../facebook/presto/raptor/RaptorSplit.java | 89 + .../presto/raptor/RaptorSplitManager.java | 201 + .../presto/raptor/RaptorTableHandle.java | 114 + .../presto/raptor/backup/BackupConfig.java | 61 + .../presto/raptor/backup/BackupModule.java | 83 + .../presto/raptor/backup/BackupStore.java | 46 + .../raptor/backup/FileBackupConfig.java | 40 + .../raptor/backup/FileBackupModule.java | 31 + .../presto/raptor/backup/FileBackupStore.java | 110 + .../raptor/backup/TimeoutBackupStore.java | 94 + .../presto/raptor/metadata/ColumnInfo.java | 52 + .../presto/raptor/metadata/ColumnStats.java | 70 + .../metadata/DatabaseMetadataModule.java | 53 + .../raptor/metadata/DatabaseShardManager.java | 411 + .../presto/raptor/metadata/ForMetadata.java | 31 + .../presto/raptor/metadata/IndexInserter.java | 129 + .../presto/raptor/metadata/MetadataDao.java | 228 + .../raptor/metadata/MetadataDaoUtils.java | 67 + .../facebook/presto/raptor/metadata/Node.java | 57 + .../metadata/SchemaTableNameMapper.java | 34 + .../presto/raptor/metadata/ShardDelta.java | 58 + .../presto/raptor/metadata/ShardInfo.java | 107 + .../presto/raptor/metadata/ShardIterator.java | 174 + .../presto/raptor/metadata/ShardManager.java | 72 + .../raptor/metadata/ShardManagerDao.java | 149 + .../raptor/metadata/ShardManagerDaoUtils.java | 62 + .../presto/raptor/metadata/ShardMetadata.java | 129 + .../presto/raptor/metadata/ShardNode.java | 90 + .../presto/raptor/metadata/ShardNodes.java | 74 + .../raptor/metadata/ShardPredicate.java | 192 + .../presto/raptor/metadata/SqlUtils.java | 75 + .../presto/raptor/metadata/Table.java | 76 + .../presto/raptor/metadata/TableColumn.java | 140 + .../presto/raptor/metadata/TableMetadata.java | 82 + .../presto/raptor/metadata/ViewResult.java | 59 + .../raptor/storage/CompactionSetCreator.java | 71 + .../raptor/storage/FileStorageService.java | 126 + .../raptor/storage/ForStorageManager.java | 31 + .../raptor/storage/OrcFileRewriter.java | 163 + .../presto/raptor/storage/OrcFileWriter.java | 251 + .../presto/raptor/storage/OrcPageSource.java | 419 + .../raptor/storage/OrcStorageManager.java | 510 + .../facebook/presto/raptor/storage/Row.java | 110 + .../storage/ShardCompactionManager.java | 310 + .../presto/raptor/storage/ShardCompactor.java | 232 + .../raptor/storage/ShardRecoveryManager.java | 398 + .../raptor/storage/ShardRecoveryStats.java | 125 + .../presto/raptor/storage/ShardRewriter.java | 24 + .../presto/raptor/storage/ShardStats.java | 258 + .../presto/raptor/storage/StorageManager.java | 34 + .../raptor/storage/StorageManagerConfig.java | 213 + .../raptor/storage/StorageManagerStats.java | 61 + .../presto/raptor/storage/StorageModule.java | 51 + .../raptor/storage/StoragePageSink.java | 36 + .../presto/raptor/storage/StorageService.java | 30 + .../presto/raptor/storage/StorageType.java | 42 + .../presto/raptor/util/ArrayUtil.java | 50 + .../presto/raptor/util/CloseableIterator.java | 24 + .../facebook/presto/raptor/util/Closer.java | 52 + .../presto/raptor/util/ConditionalModule.java | 72 + .../presto/raptor/util/CurrentNodeId.java | 32 + .../facebook/presto/raptor/util/FileUtil.java | 45 + .../presto/raptor/util/MetadataUtil.java | 40 + .../presto/raptor/util/PageBuffer.java | 76 + .../raptor/util/PrioritizedFifoExecutor.java | 147 + .../raptor/util/RebindSafeMBeanServer.java | 334 + .../presto/raptor/util/SyncingFileSystem.java | 94 + .../facebook/presto/raptor/util/Types.java | 33 + .../facebook/presto/raptor/util/UuidUtil.java | 129 + .../raptor/RaptorBenchmarkQueryRunner.java | 117 + .../presto/raptor/RaptorQueryRunner.java | 94 + .../raptor/TestRaptorDistributedQueries.java | 30 + .../TestRaptorIntegrationSmokeTest.java | 30 + .../presto/raptor/TestRaptorPlugin.java | 80 + .../raptor/backup/TestBackupConfig.java | 52 + .../raptor/backup/TestFileBackupConfig.java | 47 + .../raptor/backup/TestFileBackupStore.java | 91 + .../metadata/TestDatabaseShardManager.java | 571 + .../raptor/metadata/TestMetadataDao.java | 69 + .../raptor/metadata/TestRaptorMetadata.java | 259 + .../metadata/TestRaptorSplitManager.java | 222 + .../raptor/metadata/TestShardManagerDao.java | 261 + .../presto/raptor/storage/OrcTestingUtil.java | 98 + .../storage/TestMissingShardComparator.java | 57 + .../raptor/storage/TestOrcFileRewriter.java | 221 + .../raptor/storage/TestOrcStorageManager.java | 561 + .../raptor/storage/TestShardCompactor.java | 260 + .../raptor/storage/TestShardRecovery.java | 148 + .../raptor/storage/TestShardWriter.java | 171 + .../storage/TestStorageManagerConfig.java | 97 + .../util/TestPrioritizedFifoExecutor.java | 153 + presto-server/NOTICE | 2836 ++ presto-server/README.txt | 5 + presto-server/pom.xml | 26 + presto-server/src/main/provisio/presto.xml | 106 + presto-spi/pom.xml | 73 + .../presto/spi/BeginDeleteResult.java | 38 + .../com/facebook/presto/spi/ColumnHandle.java | 18 + .../facebook/presto/spi/ColumnMetadata.java | 115 + .../presto/spi/ColumnNotFoundException.java | 67 + .../com/facebook/presto/spi/Connector.java | 84 + .../facebook/presto/spi/ConnectorFactory.java | 23 + .../presto/spi/ConnectorHandleResolver.java | 69 + .../facebook/presto/spi/ConnectorIndex.java | 20 + .../presto/spi/ConnectorIndexHandle.java | 18 + .../presto/spi/ConnectorIndexResolver.java | 25 + .../spi/ConnectorInsertTableHandle.java | 19 + .../presto/spi/ConnectorMetadata.java | 207 + .../spi/ConnectorOutputTableHandle.java | 19 + .../presto/spi/ConnectorPageSink.java | 28 + .../presto/spi/ConnectorPageSinkProvider.java | 21 + .../presto/spi/ConnectorPageSource.java | 57 + .../spi/ConnectorPageSourceProvider.java | 21 + .../presto/spi/ConnectorPartition.java | 28 + .../presto/spi/ConnectorPartitionResult.java | 49 + .../spi/ConnectorRecordSetProvider.java | 21 + .../spi/ConnectorRecordSinkProvider.java | 21 + .../presto/spi/ConnectorResolvedIndex.java | 38 + .../facebook/presto/spi/ConnectorSession.java | 96 + .../facebook/presto/spi/ConnectorSplit.java | 25 + .../presto/spi/ConnectorSplitManager.java | 46 + .../presto/spi/ConnectorSplitSource.java | 41 + .../presto/spi/ConnectorTableHandle.java | 18 + .../presto/spi/ConnectorTableLayout.java | 132 + .../spi/ConnectorTableLayoutHandle.java | 18 + .../spi/ConnectorTableLayoutResult.java | 38 + .../presto/spi/ConnectorTableMetadata.java | 86 + .../facebook/presto/spi/ConstantProperty.java | 86 + .../com/facebook/presto/spi/Constraint.java | 49 + .../presto/spi/CreateTableOption.java | 48 + .../java/com/facebook/presto/spi/Domain.java | 262 + .../com/facebook/presto/spi/ErrorCode.java | 53 + .../presto/spi/ErrorCodeSupplier.java | 19 + .../facebook/presto/spi/FixedPageSource.java | 82 + .../facebook/presto/spi/FixedSplitSource.java | 70 + .../facebook/presto/spi/GroupingProperty.java | 111 + .../com/facebook/presto/spi/HostAddress.java | 330 + .../presto/spi/InMemoryRecordSet.java | 285 + .../com/facebook/presto/spi/InsertOption.java | 61 + .../facebook/presto/spi/LocalProperty.java | 57 + .../java/com/facebook/presto/spi/Marker.java | 296 + .../java/com/facebook/presto/spi/Node.java | 25 + .../com/facebook/presto/spi/NodeManager.java | 27 + .../presto/spi/NotFoundException.java | 45 + .../java/com/facebook/presto/spi/Page.java | 135 + .../com/facebook/presto/spi/PageBuilder.java | 117 + .../com/facebook/presto/spi/PageSorter.java | 38 + .../java/com/facebook/presto/spi/Plugin.java | 24 + .../facebook/presto/spi/PrestoException.java | 60 + .../java/com/facebook/presto/spi/Range.java | 223 + .../presto/spi/ReadOnlyConnectorMetadata.java | 106 + .../com/facebook/presto/spi/RecordCursor.java | 46 + .../facebook/presto/spi/RecordPageSink.java | 89 + .../facebook/presto/spi/RecordPageSource.java | 138 + .../com/facebook/presto/spi/RecordSet.java | 25 + .../com/facebook/presto/spi/RecordSink.java | 47 + .../presto/spi/SchemaNotFoundException.java | 53 + .../facebook/presto/spi/SchemaTableName.java | 86 + .../presto/spi/SchemaTablePrefix.java | 96 + .../com/facebook/presto/spi/SchemaUtil.java | 32 + .../presto/spi/SerializableNativeValue.java | 188 + .../facebook/presto/spi/SortedRangeSet.java | 388 + .../facebook/presto/spi/SortingProperty.java | 123 + .../presto/spi/StandardErrorCode.java | 90 + .../com/facebook/presto/spi/SystemTable.java | 39 + .../presto/spi/TableNotFoundException.java | 55 + .../com/facebook/presto/spi/TupleDomain.java | 466 + .../presto/spi/UpdatablePageSource.java | 29 + .../presto/spi/ViewNotFoundException.java | 50 + .../spi/block/AbstractFixedWidthBlock.java | 189 + .../spi/block/AbstractVariableWidthBlock.java | 166 + .../com/facebook/presto/spi/block/Block.java | 166 + .../presto/spi/block/BlockBuilder.java | 70 + .../presto/spi/block/BlockBuilderStatus.java | 64 + .../presto/spi/block/BlockEncoding.java | 46 + .../spi/block/BlockEncodingFactory.java | 36 + .../presto/spi/block/BlockEncodingSerde.java | 30 + .../presto/spi/block/EncoderUtil.java | 88 + .../presto/spi/block/FixedWidthBlock.java | 117 + .../spi/block/FixedWidthBlockBuilder.java | 239 + .../spi/block/FixedWidthBlockEncoding.java | 123 + .../presto/spi/block/LazyBlockLoader.java | 19 + .../presto/spi/block/LazyFixedWidthBlock.java | 158 + .../presto/spi/block/LazySliceArrayBlock.java | 160 + .../block/LazySliceArrayBlockEncoding.java | 134 + .../presto/spi/block/PageBuilderStatus.java | 89 + .../presto/spi/block/SliceArrayBlock.java | 178 + .../spi/block/SliceArrayBlockEncoding.java | 134 + .../facebook/presto/spi/block/SortOrder.java | 65 + .../presto/spi/block/VariableWidthBlock.java | 130 + .../spi/block/VariableWidthBlockBuilder.java | 247 + .../spi/block/VariableWidthBlockEncoding.java | 126 + ...lassLoaderSafeConnectorHandleResolver.java | 158 + ...ClassLoaderSafeConnectorIndexResolver.java | 56 + .../ClassLoaderSafeConnectorMetadata.java | 237 + ...ssLoaderSafeConnectorPageSinkProvider.java | 50 + ...LoaderSafeConnectorPageSourceProvider.java | 53 + ...sLoaderSafeConnectorRecordSetProvider.java | 52 + ...LoaderSafeConnectorRecordSinkProvider.java | 50 + .../ClassLoaderSafeConnectorSplitManager.java | 72 + .../classloader/ClassLoaderSafeRecordSet.java | 59 + .../classloader/ThreadContextClassLoader.java | 34 + .../spi/type/AbstractFixedWidthType.java | 63 + .../presto/spi/type/AbstractType.java | 168 + .../spi/type/AbstractVariableWidthType.java | 42 + .../facebook/presto/spi/type/BigintType.java | 100 + .../facebook/presto/spi/type/BooleanType.java | 100 + .../presto/spi/type/DateTimeEncoding.java | 64 + .../facebook/presto/spi/type/DateType.java | 109 + .../facebook/presto/spi/type/DoubleType.java | 99 + .../presto/spi/type/FixedWidthType.java | 35 + .../presto/spi/type/HyperLogLogType.java | 77 + .../presto/spi/type/IntervalDayTimeType.java | 99 + .../spi/type/IntervalYearMonthType.java | 99 + .../com/facebook/presto/spi/type/SqlDate.java | 67 + .../presto/spi/type/SqlIntervalDayTime.java | 87 + .../presto/spi/type/SqlIntervalYearMonth.java | 64 + .../com/facebook/presto/spi/type/SqlTime.java | 73 + .../presto/spi/type/SqlTimeWithTimeZone.java | 88 + .../presto/spi/type/SqlTimestamp.java | 73 + .../spi/type/SqlTimestampWithTimeZone.java | 88 + .../presto/spi/type/SqlVarbinary.java | 55 + .../presto/spi/type/StandardTypes.java | 37 + .../facebook/presto/spi/type/TimeType.java | 104 + .../presto/spi/type/TimeWithTimeZoneType.java | 101 + .../presto/spi/type/TimeZoneIndex.java | 50 + .../facebook/presto/spi/type/TimeZoneKey.java | 299 + .../type/TimeZoneNotSupportedException.java | 31 + .../presto/spi/type/TimestampType.java | 104 + .../spi/type/TimestampWithTimeZoneType.java | 101 + .../com/facebook/presto/spi/type/Type.java | 148 + .../facebook/presto/spi/type/TypeManager.java | 34 + .../facebook/presto/spi/type/TypeSerde.java | 68 + .../presto/spi/type/TypeSignature.java | 235 + .../presto/spi/type/VarbinaryType.java | 111 + .../facebook/presto/spi/type/VarcharType.java | 117 + .../presto/spi/type/VariableWidthType.java | 22 + .../presto/spi/type/zone-index.properties | 2276 + .../presto/spi/BenchmarkSortedRangeSet.java | 87 + .../com/facebook/presto/spi/TestDomain.java | 367 + .../com/facebook/presto/spi/TestMarker.java | 176 + .../com/facebook/presto/spi/TestPage.java | 50 + .../presto/spi/TestPrestoException.java | 43 + .../com/facebook/presto/spi/TestRange.java | 294 + .../presto/spi/TestSortedRangeSet.java | 449 + .../presto/spi/TestStandardErrorCode.java | 87 + .../facebook/presto/spi/TestTupleDomain.java | 667 + .../presto/spi/TestingColumnHandle.java | 67 + .../spi/block/TestFixedWidthBlockBuilder.java | 45 + .../block/TestVariableWidthBlockBuilder.java | 48 + .../block/TestVariableWidthBlockEncoding.java | 53 + .../presto/spi/type/TestTimeZoneKey.java | 193 + .../presto/spi/type/TestTypeSerde.java | 64 + .../presto/spi/type/TestTypeSignature.java | 102 + presto-sqlserver/pom.xml | 186 + .../plugin/sqlserver/SqlServerClient.java | 40 + .../plugin/sqlserver/SqlServerModule.java | 37 + .../plugin/sqlserver/SqlServerPlugin.java | 25 + presto-tests/pom.xml | 176 + .../tests/AbstractTestApproximateQueries.java | 90 + .../tests/AbstractTestDistributedQueries.java | 411 + .../tests/AbstractTestIndexedQueries.java | 498 + .../AbstractTestIntegrationSmokeTest.java | 212 + .../presto/tests/AbstractTestQueries.java | 4220 ++ .../tests/AbstractTestQueryFramework.java | 175 + .../tests/AbstractTestingPrestoClient.java | 149 + .../com/facebook/presto/tests/CreateHll.java | 34 + .../com/facebook/presto/tests/CustomAdd.java | 30 + .../com/facebook/presto/tests/CustomRank.java | 54 + .../com/facebook/presto/tests/CustomSum.java | 35 + .../presto/tests/DistributedQueryRunner.java | 256 + .../facebook/presto/tests/H2QueryRunner.java | 278 + .../presto/tests/QueryAssertions.java | 177 + .../facebook/presto/tests/ResultsSession.java | 26 + .../presto/tests/StandaloneQueryRunner.java | 175 + .../presto/tests/TestingDiscoveryServer.java | 96 + .../presto/tests/TestingPrestoClient.java | 171 + .../presto/tests/tpch/AppendingRecordSet.java | 185 + .../presto/tests/tpch/ConcatRecordSet.java | 178 + .../presto/tests/tpch/ExampleSystemTable.java | 56 + .../tpch/IndexedTpchConnectorFactory.java | 110 + .../presto/tests/tpch/IndexedTpchPlugin.java | 64 + .../presto/tests/tpch/MaterializedTuple.java | 60 + .../tpch/MaterializedTupleRecordSet.java | 135 + .../presto/tests/tpch/TpchConnectorIndex.java | 51 + .../presto/tests/tpch/TpchIndexHandle.java | 80 + .../tests/tpch/TpchIndexHandleResolver.java | 43 + .../presto/tests/tpch/TpchIndexResolver.java | 170 + .../presto/tests/tpch/TpchIndexSpec.java | 59 + .../presto/tests/tpch/TpchIndexedData.java | 289 + .../presto/tests/tpch/TpchScaledTable.java | 59 + .../presto/memory/TestMemoryManager.java | 230 + .../tests/TestDistributedQueriesIndexed.java | 59 + .../presto/tests/TestLocalQueries.java | 73 + .../presto/tests/TestLocalQueriesIndexed.java | 53 + .../tests/TestTpchDistributedQueries.java | 60 + presto-tpch/pom.xml | 61 + .../presto/tpch/TpchColumnHandle.java | 76 + .../presto/tpch/TpchConnectorFactory.java | 93 + .../presto/tpch/TpchHandleResolver.java | 81 + .../facebook/presto/tpch/TpchMetadata.java | 273 + .../com/facebook/presto/tpch/TpchPlugin.java | 53 + .../facebook/presto/tpch/TpchRecordSet.java | 179 + .../presto/tpch/TpchRecordSetProvider.java | 107 + .../com/facebook/presto/tpch/TpchSplit.java | 121 + .../presto/tpch/TpchSplitManager.java | 67 + .../facebook/presto/tpch/TpchTableHandle.java | 85 + .../presto/tpch/TpchTableLayoutHandle.java | 41 + .../java/com/facebook/presto/tpch/Types.java | 33 + .../testing/SampledTpchConnectorFactory.java | 93 + .../tpch/testing/SampledTpchMetadata.java | 38 + .../tpch/testing/SampledTpchPlugin.java | 53 + .../testing/SampledTpchRecordSetProvider.java | 303 + .../services/com.facebook.presto.spi.Plugin | 1 + presto-verifier/pom.xml | 166 + .../presto/verifier/ForwardingDriver.java | 81 + .../verifier/HumanReadableEventClient.java | 65 + .../presto/verifier/JsonEventClient.java | 62 + .../presto/verifier/PrestoVerifier.java | 194 + .../presto/verifier/PrestoVerifierModule.java | 56 + .../com/facebook/presto/verifier/Query.java | 68 + .../facebook/presto/verifier/QueryPair.java | 50 + .../presto/verifier/QueryPairMapper.java | 37 + .../facebook/presto/verifier/QueryResult.java | 62 + .../verifier/TypesDoNotMatchException.java | 23 + .../facebook/presto/verifier/Validator.java | 539 + .../facebook/presto/verifier/Verifier.java | 197 + .../presto/verifier/VerifierConfig.java | 576 + .../facebook/presto/verifier/VerifierDao.java | 44 + .../presto/verifier/VerifierException.java | 23 + .../presto/verifier/VerifierQueryEvent.java | 179 + .../presto/verifier/TestVerifierConfig.java | 148 + src/checkstyle/checks.xml | 117 + src/license/LICENSE-HEADER.txt | 11 + 2574 files changed, 473035 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 pom.xml create mode 100644 presto-base-jdbc/pom.xml create mode 100644 presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/BaseJdbcClient.java create mode 100644 presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/BaseJdbcConfig.java create mode 100644 presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcClient.java create mode 100644 presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcColumnHandle.java create mode 100644 presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcConnector.java create mode 100644 presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcConnectorFactory.java create mode 100644 presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcConnectorId.java create mode 100644 presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcHandleResolver.java create mode 100644 presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcMetadata.java create mode 100644 presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcMetadataConfig.java create mode 100644 presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcModule.java create mode 100644 presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcOutputTableHandle.java create mode 100644 presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcPartition.java create mode 100644 presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcPlugin.java create mode 100644 presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcRecordCursor.java create mode 100644 presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcRecordSet.java create mode 100644 presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcRecordSetProvider.java create mode 100644 presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcRecordSink.java create mode 100644 presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcRecordSinkProvider.java create mode 100644 presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcSplit.java create mode 100644 presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcSplitManager.java create mode 100644 presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcTableHandle.java create mode 100644 presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/QueryBuilder.java create mode 100644 presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/Types.java create mode 100644 presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/cache/JdbcCacheConfig.java create mode 100644 presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/cache/JdbcCacheSplit.java create mode 100644 presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/cache/JdbcJavaBean.java create mode 100644 presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/cache/JdbcResultCache.java create mode 100644 presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/subtable/JdbcLoadTread.java create mode 100644 presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/subtable/JdbcSubTableConfig.java create mode 100644 presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/subtable/JdbcSubTableManager.java create mode 100644 presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/subtable/PdboSplit.java create mode 100644 presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/subtable/PdboSplitSource.java create mode 100644 presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/subtable/PdboTableInfo.java create mode 100644 presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/util/JdbcUtil.java create mode 100644 presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/util/PdboMetadata.java create mode 100644 presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/JdbcQueryRunner.java create mode 100644 presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/MetadataUtil.java create mode 100644 presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestBaseJdbcConfig.java create mode 100644 presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcClient.java create mode 100644 presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcColumnHandle.java create mode 100644 presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcConnectorFactory.java create mode 100644 presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcDistributedQueries.java create mode 100644 presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcHandleResolver.java create mode 100644 presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcIntegrationSmokeTest.java create mode 100644 presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcMetadata.java create mode 100644 presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcMetadataConfig.java create mode 100644 presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcOutputTableHandle.java create mode 100644 presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcRecordSet.java create mode 100644 presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcRecordSetProvider.java create mode 100644 presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcSplit.java create mode 100644 presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcTableHandle.java create mode 100644 presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestingDatabase.java create mode 100644 presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestingH2JdbcModule.java create mode 100644 presto-benchmark-driver/pom.xml create mode 100644 presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/BenchmarkDriver.java create mode 100644 presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/BenchmarkDriverExecutionException.java create mode 100644 presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/BenchmarkDriverOptions.java create mode 100644 presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/BenchmarkQuery.java create mode 100644 presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/BenchmarkQueryResult.java create mode 100644 presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/BenchmarkQueryRunner.java create mode 100644 presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/BenchmarkResultsPrinter.java create mode 100644 presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/BenchmarkResultsStore.java create mode 100644 presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/BenchmarkSchema.java create mode 100644 presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/PrestoBenchmarkDriver.java create mode 100644 presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/RegexTemplate.java create mode 100644 presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/Stat.java create mode 100644 presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/Suite.java create mode 100644 presto-benchmark-driver/src/test/java/com/facebook/presto/benchmark/driver/RegexTemplateTest.java create mode 100644 presto-benchmark-driver/src/test/resources/groups.json create mode 100644 presto-benchmark-driver/src/test/resources/sql/live_sum_discount.sql create mode 100644 presto-benchmark-driver/src/test/resources/sql/sum_discount.sql create mode 100644 presto-benchmark-driver/src/test/resources/sql/sum_quantity.sql create mode 100644 presto-benchmark/pom.xml create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/AbstractBenchmark.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/AbstractOperatorBenchmark.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/AbstractSimpleOperatorBenchmark.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/AbstractSqlBenchmark.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/ArrayComparisonBenchmark.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/AverageBenchmarkResults.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/BenchmarkQueryRunner.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/BenchmarkResultHook.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/BenchmarkSuite.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/CountAggregationBenchmark.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/CountAggregationSqlBenchmark.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/CountWithFilterSqlBenchmark.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/DoubleSumAggregationBenchmark.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/FormatUtils.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/GroupByAggregationSqlBenchmark.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/GroupBySumWithArithmeticSqlBenchmark.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/HandTpchQuery1.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/HandTpchQuery6.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/HashAggregationBenchmark.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/HashBuildAndJoinBenchmark.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/HashBuildBenchmark.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/HashJoinBenchmark.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/JsonAvgBenchmarkResultWriter.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/JsonBenchmarkResultWriter.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/LongMaxAggregationSqlBenchmark.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/OdsBenchmarkResultWriter.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/OrderByBenchmark.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/PredicateFilterBenchmark.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/PredicateFilterSqlBenchmark.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/RawStreamingBenchmark.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/RawStreamingSqlBenchmark.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/SimpleLineBenchmarkResultWriter.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlApproximateCountDistinctDoubleBenchmark.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlApproximateCountDistinctLongBenchmark.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlApproximateCountDistinctVarBinaryBenchmark.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlApproximatePercentileBenchmark.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlBetweenBenchmark.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlDistinctMultipleFields.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlDistinctSingleField.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlDoubleSumAggregationBenchmark.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlHashJoinBenchmark.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlInBenchmark.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlJoinWithPredicateBenchmark.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlLikeBenchmark.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlRegexpLikeBenchmark.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlSemiJoinInPredicateBenchmark.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlTpchQuery1.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlTpchQuery6.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/StatisticsBenchmark.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/StructuredTypesBenchmark.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/Top100Benchmark.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/Top100SqlBenchmark.java create mode 100644 presto-benchmark/src/main/java/com/facebook/presto/benchmark/VarBinaryMaxAggregationSqlBenchmark.java create mode 100644 presto-benchmark/src/test/java/com/facebook/presto/benchmark/TestBenchmarks.java create mode 100644 presto-cassandra/pom.xml create mode 100644 presto-cassandra/src/main/java/com/facebook/presto/cassandra/BackoffRetryPolicy.java create mode 100644 presto-cassandra/src/main/java/com/facebook/presto/cassandra/CachingCassandraSchemaProvider.java create mode 100644 presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraClientConfig.java create mode 100644 presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraClientModule.java create mode 100644 presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraColumnHandle.java create mode 100644 presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraConnector.java create mode 100644 presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraConnectorFactory.java create mode 100644 presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraConnectorId.java create mode 100644 presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraConnectorRecordSinkProvider.java create mode 100644 presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraHandleResolver.java create mode 100644 presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraMetadata.java create mode 100644 presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraOutputTableHandle.java create mode 100644 presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraPartition.java create mode 100644 presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraPlugin.java create mode 100644 presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraRecordCursor.java create mode 100644 presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraRecordSet.java create mode 100644 presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraRecordSetProvider.java create mode 100644 presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraRecordSink.java create mode 100644 presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraSession.java create mode 100644 presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraSplit.java create mode 100644 presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraSplitManager.java create mode 100644 presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraTable.java create mode 100644 presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraTableHandle.java create mode 100644 presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraThriftClient.java create mode 100644 presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraThriftConnectionFactory.java create mode 100644 presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraTokenSplitManager.java create mode 100644 presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraType.java create mode 100644 presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraTypeWithTypeArguments.java create mode 100644 presto-cassandra/src/main/java/com/facebook/presto/cassandra/ExtraColumnMetadata.java create mode 100644 presto-cassandra/src/main/java/com/facebook/presto/cassandra/ForCassandra.java create mode 100644 presto-cassandra/src/main/java/com/facebook/presto/cassandra/FullCassandraType.java create mode 100644 presto-cassandra/src/main/java/com/facebook/presto/cassandra/RebindSafeMBeanServer.java create mode 100644 presto-cassandra/src/main/java/com/facebook/presto/cassandra/RetryDriver.java create mode 100644 presto-cassandra/src/main/java/com/facebook/presto/cassandra/RetryPolicyType.java create mode 100644 presto-cassandra/src/main/java/com/facebook/presto/cassandra/util/CassandraCqlUtils.java create mode 100644 presto-cassandra/src/main/java/com/facebook/presto/cassandra/util/HostAddressFactory.java create mode 100644 presto-cassandra/src/main/java/com/facebook/presto/cassandra/util/Types.java create mode 100644 presto-cassandra/src/test/java/com/datastax/driver/core/RowUtil.java create mode 100644 presto-cassandra/src/test/java/com/datastax/driver/core/TestHost.java create mode 100644 presto-cassandra/src/test/java/com/facebook/presto/cassandra/CassandraQueryRunner.java create mode 100644 presto-cassandra/src/test/java/com/facebook/presto/cassandra/CassandraTestingUtils.java create mode 100644 presto-cassandra/src/test/java/com/facebook/presto/cassandra/MockCassandraSession.java create mode 100644 presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestCachingCassandraSchemaProvider.java create mode 100644 presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestCassandraClientConfig.java create mode 100644 presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestCassandraColumnHandle.java create mode 100644 presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestCassandraConnector.java create mode 100644 presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestCassandraDistributed.java create mode 100644 presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestCassandraIntegrationSmokeTest.java create mode 100644 presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestCassandraSplit.java create mode 100644 presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestCassandraTableHandle.java create mode 100644 presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestCassandraType.java create mode 100644 presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestJsonCassandraHandles.java create mode 100644 presto-cassandra/src/test/java/com/facebook/presto/cassandra/util/TestCassandraCqlUtils.java create mode 100644 presto-cassandra/src/test/java/com/facebook/presto/cassandra/util/TestHostAddressFactory.java create mode 100644 presto-cassandra/src/test/resources/cu-cassandra.yaml create mode 100644 presto-cli/pom.xml create mode 100644 presto-cli/src/main/java/com/facebook/presto/cli/AlignedTablePrinter.java create mode 100644 presto-cli/src/main/java/com/facebook/presto/cli/ClientOptions.java create mode 100644 presto-cli/src/main/java/com/facebook/presto/cli/Completion.java create mode 100644 presto-cli/src/main/java/com/facebook/presto/cli/Console.java create mode 100644 presto-cli/src/main/java/com/facebook/presto/cli/ConsolePrinter.java create mode 100644 presto-cli/src/main/java/com/facebook/presto/cli/CsvPrinter.java create mode 100644 presto-cli/src/main/java/com/facebook/presto/cli/FormatUtils.java create mode 100644 presto-cli/src/main/java/com/facebook/presto/cli/Help.java create mode 100644 presto-cli/src/main/java/com/facebook/presto/cli/LineReader.java create mode 100644 presto-cli/src/main/java/com/facebook/presto/cli/NullPrinter.java create mode 100644 presto-cli/src/main/java/com/facebook/presto/cli/OutputHandler.java create mode 100644 presto-cli/src/main/java/com/facebook/presto/cli/OutputPrinter.java create mode 100644 presto-cli/src/main/java/com/facebook/presto/cli/Pager.java create mode 100644 presto-cli/src/main/java/com/facebook/presto/cli/PerfTest.java create mode 100644 presto-cli/src/main/java/com/facebook/presto/cli/Presto.java create mode 100644 presto-cli/src/main/java/com/facebook/presto/cli/Query.java create mode 100644 presto-cli/src/main/java/com/facebook/presto/cli/QueryAbortedException.java create mode 100644 presto-cli/src/main/java/com/facebook/presto/cli/QueryRunner.java create mode 100644 presto-cli/src/main/java/com/facebook/presto/cli/StatusPrinter.java create mode 100644 presto-cli/src/main/java/com/facebook/presto/cli/TableNameCompleter.java create mode 100644 presto-cli/src/main/java/com/facebook/presto/cli/TsvPrinter.java create mode 100644 presto-cli/src/main/java/com/facebook/presto/cli/VersionOption.java create mode 100644 presto-cli/src/main/java/com/facebook/presto/cli/VerticalRecordPrinter.java create mode 100644 presto-cli/src/test/java/com/facebook/presto/cli/TestAlignedTablePrinter.java create mode 100644 presto-cli/src/test/java/com/facebook/presto/cli/TestClientOptions.java create mode 100644 presto-cli/src/test/java/com/facebook/presto/cli/TestCsvPrinter.java create mode 100644 presto-cli/src/test/java/com/facebook/presto/cli/TestTsvPrinter.java create mode 100644 presto-cli/src/test/java/com/facebook/presto/cli/TestVerticalRecordPrinter.java create mode 100644 presto-client/pom.xml create mode 100644 presto-client/src/main/java/com/facebook/presto/client/ClientSession.java create mode 100644 presto-client/src/main/java/com/facebook/presto/client/ClientTypeSignature.java create mode 100644 presto-client/src/main/java/com/facebook/presto/client/Column.java create mode 100644 presto-client/src/main/java/com/facebook/presto/client/ErrorLocation.java create mode 100644 presto-client/src/main/java/com/facebook/presto/client/FailureInfo.java create mode 100644 presto-client/src/main/java/com/facebook/presto/client/PrestoHeaders.java create mode 100644 presto-client/src/main/java/com/facebook/presto/client/QueryError.java create mode 100644 presto-client/src/main/java/com/facebook/presto/client/QueryResults.java create mode 100644 presto-client/src/main/java/com/facebook/presto/client/StageStats.java create mode 100644 presto-client/src/main/java/com/facebook/presto/client/StatementClient.java create mode 100644 presto-client/src/main/java/com/facebook/presto/client/StatementStats.java create mode 100644 presto-docs/Makefile create mode 100644 presto-docs/pom.xml create mode 100644 presto-docs/src/main/assembly/docs.xml create mode 100644 presto-docs/src/main/assembly/sources.xml create mode 100644 presto-docs/src/main/resources/design.graffle create mode 100644 presto-docs/src/main/resources/logo/print/dark/FB_Presto_Logo_PRINT_DarkBG.ai create mode 100644 presto-docs/src/main/resources/logo/print/light/FB_Presto_Logo_PRINT_LightBG.ai create mode 100644 presto-docs/src/main/resources/logo/web/fb/Presto_FB_System_Lockups.ai create mode 100644 presto-docs/src/main/resources/logo/web/fb/black/Presto_FB_Lockups_BLACK_BG-25.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/black/Presto_FB_Lockups_BLACK_BG-26.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/black/Presto_FB_Lockups_BLACK_BG-27.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/black/Presto_FB_Lockups_BLACK_BG-28.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/black/Presto_FB_Lockups_BLACK_BG-29.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/black/Presto_FB_Lockups_BLACK_BG-30.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/black/Presto_FB_Lockups_BLACK_BG-31.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/black/Presto_FB_Lockups_BLACK_BG-32.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/blue/Presto_FB_Lockups_FB_BLUE_BG-17.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/blue/Presto_FB_Lockups_FB_BLUE_BG-18.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/blue/Presto_FB_Lockups_FB_BLUE_BG-19.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/blue/Presto_FB_Lockups_FB_BLUE_BG-20.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/blue/Presto_FB_Lockups_FB_BLUE_BG-21.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/blue/Presto_FB_Lockups_FB_BLUE_BG-22.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/blue/Presto_FB_Lockups_FB_BLUE_BG-23.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/blue/Presto_FB_Lockups_FB_BLUE_BG-24.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/dark-blue/Presto_FB_Lockups_DARKBLUE_BG-09.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/dark-blue/Presto_FB_Lockups_DARKBLUE_BG-10.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/dark-blue/Presto_FB_Lockups_DARKBLUE_BG-11.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/dark-blue/Presto_FB_Lockups_DARKBLUE_BG-12.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/dark-blue/Presto_FB_Lockups_DARKBLUE_BG-13.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/dark-blue/Presto_FB_Lockups_DARKBLUE_BG-14.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/dark-blue/Presto_FB_Lockups_DARKBLUE_BG-15.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/dark-blue/Presto_FB_Lockups_DARKBLUE_BG-16.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/dark-grey/Presto_FB_Lockups_DARKGREY_BG-33.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/dark-grey/Presto_FB_Lockups_DARKGREY_BG-34.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/dark-grey/Presto_FB_Lockups_DARKGREY_BG-35.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/dark-grey/Presto_FB_Lockups_DARKGREY_BG-36.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/dark-grey/Presto_FB_Lockups_DARKGREY_BG-37.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/dark-grey/Presto_FB_Lockups_DARKGREY_BG-38.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/dark-grey/Presto_FB_Lockups_DARKGREY_BG-39.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/dark-grey/Presto_FB_Lockups_DARKGREY_BG-40.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/light-grey/Presto_FB_Lockups_LIGHTGREY_BG-41.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/light-grey/Presto_FB_Lockups_LIGHTGREY_BG-42.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/light-grey/Presto_FB_Lockups_LIGHTGREY_BG-43.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/light-grey/Presto_FB_Lockups_LIGHTGREY_BG-44.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/light-grey/Presto_FB_Lockups_LIGHTGREY_BG-45.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/light-grey/Presto_FB_Lockups_LIGHTGREY_BG-46.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/light-grey/Presto_FB_Lockups_LIGHTGREY_BG-47.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/light-grey/Presto_FB_Lockups_LIGHTGREY_BG-48.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/white/Presto_FB_Lockups-01.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/white/Presto_FB_Lockups-02.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/white/Presto_FB_Lockups-03.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/white/Presto_FB_Lockups-04.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/white/Presto_FB_Lockups-05.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/white/Presto_FB_Lockups-06.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/white/Presto_FB_Lockups-07.svg create mode 100644 presto-docs/src/main/resources/logo/web/fb/white/Presto_FB_Lockups-08.svg create mode 100644 presto-docs/src/main/resources/logo/web/main/black/FB_Presto_Logo_BlackBG-01.svg create mode 100644 presto-docs/src/main/resources/logo/web/main/black/FB_Presto_Logo_BlackBG.ai create mode 100644 presto-docs/src/main/resources/logo/web/main/blue/FB_Presto_Logo_DarkBlueBG.ai create mode 100644 presto-docs/src/main/resources/logo/web/main/blue/FB_Presto_Logo_DarkBlueBG.svg create mode 100644 presto-docs/src/main/resources/logo/web/main/white/FB_Presto_Logo_WhiteBG-01.svg create mode 100644 presto-docs/src/main/resources/logo/web/main/white/FB_Presto_Logo_WhiteBG.ai create mode 100644 presto-docs/src/main/sphinx/admin.rst create mode 100644 presto-docs/src/main/sphinx/admin/queue.rst create mode 100644 presto-docs/src/main/sphinx/conf.py create mode 100644 presto-docs/src/main/sphinx/connector.rst create mode 100644 presto-docs/src/main/sphinx/connector/cassandra.rst create mode 100644 presto-docs/src/main/sphinx/connector/hive.rst create mode 100644 presto-docs/src/main/sphinx/connector/jmx.rst create mode 100644 presto-docs/src/main/sphinx/connector/kafka-tutorial.rst create mode 100644 presto-docs/src/main/sphinx/connector/kafka.rst create mode 100644 presto-docs/src/main/sphinx/connector/mysql.rst create mode 100644 presto-docs/src/main/sphinx/connector/postgresql.rst create mode 100644 presto-docs/src/main/sphinx/connector/system.rst create mode 100644 presto-docs/src/main/sphinx/connector/tpch.rst create mode 100644 presto-docs/src/main/sphinx/develop.rst create mode 100644 presto-docs/src/main/sphinx/develop/example-http.rst create mode 100644 presto-docs/src/main/sphinx/develop/functions.rst create mode 100644 presto-docs/src/main/sphinx/develop/spi-overview.rst create mode 100644 presto-docs/src/main/sphinx/develop/types.rst create mode 100644 presto-docs/src/main/sphinx/ext/download.py create mode 100644 presto-docs/src/main/sphinx/functions.rst create mode 100644 presto-docs/src/main/sphinx/functions/aggregate.rst create mode 100644 presto-docs/src/main/sphinx/functions/array.rst create mode 100644 presto-docs/src/main/sphinx/functions/binary.rst create mode 100644 presto-docs/src/main/sphinx/functions/color.rst create mode 100644 presto-docs/src/main/sphinx/functions/comparison.rst create mode 100644 presto-docs/src/main/sphinx/functions/conditional.rst create mode 100644 presto-docs/src/main/sphinx/functions/conversion.rst create mode 100644 presto-docs/src/main/sphinx/functions/datetime.rst create mode 100644 presto-docs/src/main/sphinx/functions/json.rst create mode 100644 presto-docs/src/main/sphinx/functions/logical.rst create mode 100644 presto-docs/src/main/sphinx/functions/map.rst create mode 100644 presto-docs/src/main/sphinx/functions/math.rst create mode 100644 presto-docs/src/main/sphinx/functions/regexp.rst create mode 100644 presto-docs/src/main/sphinx/functions/string.rst create mode 100644 presto-docs/src/main/sphinx/functions/url.rst create mode 100644 presto-docs/src/main/sphinx/functions/window.rst create mode 100644 presto-docs/src/main/sphinx/images/functions_color_bar.png create mode 100644 presto-docs/src/main/sphinx/index.rst create mode 100644 presto-docs/src/main/sphinx/installation.rst create mode 100644 presto-docs/src/main/sphinx/installation/benchmark-driver.rst create mode 100644 presto-docs/src/main/sphinx/installation/cli.rst create mode 100644 presto-docs/src/main/sphinx/installation/deployment.rst create mode 100644 presto-docs/src/main/sphinx/installation/jdbc.rst create mode 100644 presto-docs/src/main/sphinx/installation/verifier.rst create mode 100644 presto-docs/src/main/sphinx/language.rst create mode 100644 presto-docs/src/main/sphinx/language/types.rst create mode 100644 presto-docs/src/main/sphinx/migration.rst create mode 100644 presto-docs/src/main/sphinx/migration/from-hive.rst create mode 100644 presto-docs/src/main/sphinx/overview.rst create mode 100644 presto-docs/src/main/sphinx/overview/concepts.rst create mode 100644 presto-docs/src/main/sphinx/overview/use-cases.rst create mode 100644 presto-docs/src/main/sphinx/release.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.100.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.101.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.102.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.103.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.104.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.105.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.106.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.107.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.54.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.55.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.56.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.57.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.58.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.59.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.60.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.61.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.62.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.63.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.64.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.65.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.66.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.67.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.68.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.69.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.70.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.71.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.72.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.73.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.74.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.75.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.76.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.77.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.78.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.79.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.80.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.81.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.82.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.83.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.84.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.85.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.86.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.87.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.88.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.89.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.90.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.91.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.92.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.93.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.94.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.95.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.96.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.97.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.98.rst create mode 100644 presto-docs/src/main/sphinx/release/release-0.99.rst create mode 100644 presto-docs/src/main/sphinx/rest.rst create mode 100644 presto-docs/src/main/sphinx/rest/execute.rst create mode 100644 presto-docs/src/main/sphinx/rest/node.rst create mode 100644 presto-docs/src/main/sphinx/rest/query.rst create mode 100644 presto-docs/src/main/sphinx/rest/stage.rst create mode 100644 presto-docs/src/main/sphinx/rest/statement.rst create mode 100644 presto-docs/src/main/sphinx/rest/task.rst create mode 100644 presto-docs/src/main/sphinx/sql.rst create mode 100644 presto-docs/src/main/sphinx/sql/alter-table.rst create mode 100644 presto-docs/src/main/sphinx/sql/create-table-as.rst create mode 100644 presto-docs/src/main/sphinx/sql/create-table.rst create mode 100644 presto-docs/src/main/sphinx/sql/create-view.rst create mode 100644 presto-docs/src/main/sphinx/sql/delete.rst create mode 100644 presto-docs/src/main/sphinx/sql/describe.rst create mode 100644 presto-docs/src/main/sphinx/sql/drop-table.rst create mode 100644 presto-docs/src/main/sphinx/sql/drop-view.rst create mode 100644 presto-docs/src/main/sphinx/sql/explain.rst create mode 100644 presto-docs/src/main/sphinx/sql/insert.rst create mode 100644 presto-docs/src/main/sphinx/sql/reset-session.rst create mode 100644 presto-docs/src/main/sphinx/sql/select.rst create mode 100644 presto-docs/src/main/sphinx/sql/set-session.rst create mode 100644 presto-docs/src/main/sphinx/sql/show-catalogs.rst create mode 100644 presto-docs/src/main/sphinx/sql/show-columns.rst create mode 100644 presto-docs/src/main/sphinx/sql/show-functions.rst create mode 100644 presto-docs/src/main/sphinx/sql/show-partitions.rst create mode 100644 presto-docs/src/main/sphinx/sql/show-schemas.rst create mode 100644 presto-docs/src/main/sphinx/sql/show-session.rst create mode 100644 presto-docs/src/main/sphinx/sql/show-tables.rst create mode 100644 presto-docs/src/main/sphinx/sql/use.rst create mode 100644 presto-docs/src/main/sphinx/themes/presto/layout.html create mode 100644 presto-docs/src/main/sphinx/themes/presto/static/alert_info_32.png create mode 100644 presto-docs/src/main/sphinx/themes/presto/static/alert_warning_32.png create mode 100644 presto-docs/src/main/sphinx/themes/presto/static/bullet.png create mode 100644 presto-docs/src/main/sphinx/themes/presto/static/presto.css_t create mode 100644 presto-docs/src/main/sphinx/themes/presto/static/presto.png create mode 100644 presto-docs/src/main/sphinx/themes/presto/theme.conf create mode 100644 presto-example-http/pom.xml create mode 100644 presto-example-http/src/main/java/com/facebook/presto/example/ExampleClient.java create mode 100644 presto-example-http/src/main/java/com/facebook/presto/example/ExampleColumn.java create mode 100644 presto-example-http/src/main/java/com/facebook/presto/example/ExampleColumnHandle.java create mode 100644 presto-example-http/src/main/java/com/facebook/presto/example/ExampleConfig.java create mode 100644 presto-example-http/src/main/java/com/facebook/presto/example/ExampleConnector.java create mode 100644 presto-example-http/src/main/java/com/facebook/presto/example/ExampleConnectorFactory.java create mode 100644 presto-example-http/src/main/java/com/facebook/presto/example/ExampleConnectorId.java create mode 100644 presto-example-http/src/main/java/com/facebook/presto/example/ExampleHandleResolver.java create mode 100644 presto-example-http/src/main/java/com/facebook/presto/example/ExampleMetadata.java create mode 100644 presto-example-http/src/main/java/com/facebook/presto/example/ExampleModule.java create mode 100644 presto-example-http/src/main/java/com/facebook/presto/example/ExamplePartition.java create mode 100644 presto-example-http/src/main/java/com/facebook/presto/example/ExamplePlugin.java create mode 100644 presto-example-http/src/main/java/com/facebook/presto/example/ExampleRecordCursor.java create mode 100644 presto-example-http/src/main/java/com/facebook/presto/example/ExampleRecordSet.java create mode 100644 presto-example-http/src/main/java/com/facebook/presto/example/ExampleRecordSetProvider.java create mode 100644 presto-example-http/src/main/java/com/facebook/presto/example/ExampleSplit.java create mode 100644 presto-example-http/src/main/java/com/facebook/presto/example/ExampleSplitManager.java create mode 100644 presto-example-http/src/main/java/com/facebook/presto/example/ExampleTable.java create mode 100644 presto-example-http/src/main/java/com/facebook/presto/example/ExampleTableHandle.java create mode 100644 presto-example-http/src/main/java/com/facebook/presto/example/Types.java create mode 100644 presto-example-http/src/test/java/com/facebook/presto/example/ExampleHttpServer.java create mode 100644 presto-example-http/src/test/java/com/facebook/presto/example/MetadataUtil.java create mode 100644 presto-example-http/src/test/java/com/facebook/presto/example/TestExampleClient.java create mode 100644 presto-example-http/src/test/java/com/facebook/presto/example/TestExampleColumnHandle.java create mode 100644 presto-example-http/src/test/java/com/facebook/presto/example/TestExampleConfig.java create mode 100644 presto-example-http/src/test/java/com/facebook/presto/example/TestExampleMetadata.java create mode 100644 presto-example-http/src/test/java/com/facebook/presto/example/TestExampleRecordSet.java create mode 100644 presto-example-http/src/test/java/com/facebook/presto/example/TestExampleRecordSetProvider.java create mode 100644 presto-example-http/src/test/java/com/facebook/presto/example/TestExampleSplit.java create mode 100644 presto-example-http/src/test/java/com/facebook/presto/example/TestExampleTable.java create mode 100644 presto-example-http/src/test/java/com/facebook/presto/example/TestExampleTableHandle.java create mode 100644 presto-example-http/src/test/resources/example-data/example-metadata.json create mode 100644 presto-example-http/src/test/resources/example-data/lineitem-1.csv create mode 100644 presto-example-http/src/test/resources/example-data/lineitem-2.csv create mode 100644 presto-example-http/src/test/resources/example-data/numbers-1.csv create mode 100644 presto-example-http/src/test/resources/example-data/numbers-2.csv create mode 100644 presto-example-http/src/test/resources/example-data/orders-1.csv create mode 100644 presto-example-http/src/test/resources/example-data/orders-2.csv create mode 100644 presto-hive-cdh4/pom.xml create mode 100644 presto-hive-cdh4/src/main/java/com/facebook/presto/hive/HiveCdh4Plugin.java create mode 100644 presto-hive-cdh4/src/test/java/com/facebook/presto/hive/TestHiveClient.java create mode 100644 presto-hive-cdh4/src/test/java/com/facebook/presto/hive/TestHiveClientS3.java create mode 100644 presto-hive-cdh4/src/test/java/com/facebook/presto/hive/TestSplitIteratorBackpressure.java create mode 100644 presto-hive-cdh5/pom.xml create mode 100644 presto-hive-cdh5/src/main/java/com/facebook/presto/hive/HiveCdh5Plugin.java create mode 100644 presto-hive-cdh5/src/test/java/com/facebook/presto/hive/TestHiveClient.java create mode 100644 presto-hive-cdh5/src/test/java/com/facebook/presto/hive/TestHiveClientS3.java create mode 100644 presto-hive-cdh5/src/test/java/com/facebook/presto/hive/TestSplitIteratorBackpressure.java create mode 100644 presto-hive-hadoop1/pom.xml create mode 100644 presto-hive-hadoop1/src/main/java/com/facebook/presto/hive/HiveHadoop1Plugin.java create mode 100644 presto-hive-hadoop1/src/test/java/com/facebook/presto/hive/TestHiveClient.java create mode 100644 presto-hive-hadoop1/src/test/java/com/facebook/presto/hive/TestHiveClientS3.java create mode 100644 presto-hive-hadoop1/src/test/java/com/facebook/presto/hive/TestSplitIteratorBackpressure.java create mode 100644 presto-hive-hadoop2/pom.xml create mode 100644 presto-hive-hadoop2/src/main/java/com/facebook/presto/hive/HiveHadoop2Plugin.java create mode 100644 presto-hive-hadoop2/src/test/java/com/facebook/presto/hive/TestHiveClient.java create mode 100644 presto-hive-hadoop2/src/test/java/com/facebook/presto/hive/TestHiveClientS3.java create mode 100644 presto-hive-hadoop2/src/test/java/com/facebook/presto/hive/TestSplitIteratorBackpressure.java create mode 100644 presto-hive/pom.xml create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/BackgroundHiveSplitLoader.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/ColumnarBinaryHiveRecordCursor.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/ColumnarBinaryHiveRecordCursorProvider.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/ColumnarTextHiveRecordCursor.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/ColumnarTextHiveRecordCursorProvider.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/ConcurrentLazyQueue.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/DirectoryLister.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/DiscoveryLocatedHiveCluster.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/ForHiveClient.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/ForHiveMetastore.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/GenericHiveRecordCursor.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/GenericHiveRecordCursorProvider.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/HadoopDirectoryLister.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/HdfsConfiguration.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/HdfsConfigurationUpdater.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/HdfsEnvironment.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/HiveBooleanParser.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/HiveBucketing.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/HiveClientConfig.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/HiveClientModule.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/HiveCluster.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/HiveColumnHandle.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/HiveConnector.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/HiveConnectorFactory.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/HiveConnectorId.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/HiveErrorCode.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/HiveHandleResolver.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/HiveHdfsConfiguration.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/HiveInsertTableHandle.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/HiveMetadata.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/HiveMetastoreClient.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/HiveMetastoreClientFactory.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/HiveOutputTableHandle.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/HivePageSourceFactory.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/HivePageSourceProvider.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/HivePartition.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/HivePartitionKey.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/HivePartitionMetadata.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/HivePlugin.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/HivePluginConfig.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/HiveRecordCursor.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/HiveRecordCursorProvider.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/HiveRecordSink.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/HiveRecordSinkProvider.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/HiveSessionProperties.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/HiveSplit.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/HiveSplitLoader.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/HiveSplitManager.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/HiveSplitSource.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/HiveStorageFormat.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/HiveTableHandle.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/HiveType.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/HiveUtil.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/HiveViewNotSupportedException.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/NamenodeStats.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/NumberParser.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/ParquetHiveRecordCursor.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/ParquetRecordCursorProvider.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/PartitionOfflineException.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/PartitionOption.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/PrestoS3FileSystem.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/PrestoS3FileSystemMetricCollector.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/PrestoS3FileSystemStats.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/RebindSafeMBeanServer.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/RetryDriver.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/StaticHiveCluster.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/TableAlreadyExistsException.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/TableOfflineException.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/UnpartitionedPartition.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/ViewAlreadyExistsException.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/metastore/CachingHiveMetastore.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/metastore/CachingHiveMetastoreStats.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/metastore/HiveMetastore.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/metastore/HiveMetastoreApiStats.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/metastore/InMemoryHiveMetastore.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/orc/DwrfHiveRecordCursor.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/orc/DwrfPageSourceFactory.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/orc/DwrfRecordCursorProvider.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/orc/HdfsOrcDataSource.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/orc/OrcHiveRecordCursor.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/orc/OrcPageSource.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/orc/OrcPageSourceFactory.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/orc/OrcRecordCursorProvider.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/rcfile/RcBinaryBlockLoader.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/rcfile/RcFileBlockLoader.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/rcfile/RcFilePageSource.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/rcfile/RcFilePageSourceFactory.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/rcfile/RcTextBlockLoader.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/util/AsyncQueue.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/util/HiveFileIterator.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/util/SerDeUtils.java create mode 100644 presto-hive/src/main/java/com/facebook/presto/hive/util/Types.java create mode 100644 presto-hive/src/test/java/com/facebook/presto/hive/AbstractTestHiveClient.java create mode 100644 presto-hive/src/test/java/com/facebook/presto/hive/AbstractTestHiveClientS3.java create mode 100644 presto-hive/src/test/java/com/facebook/presto/hive/AbstractTestHiveFileFormats.java create mode 100644 presto-hive/src/test/java/com/facebook/presto/hive/AbstractTestSplitIteratorBackpressure.java create mode 100644 presto-hive/src/test/java/com/facebook/presto/hive/BenchmarkHiveFileFormats.java create mode 100644 presto-hive/src/test/java/com/facebook/presto/hive/HiveBenchmarkQueryRunner.java create mode 100644 presto-hive/src/test/java/com/facebook/presto/hive/HiveQueryRunner.java create mode 100644 presto-hive/src/test/java/com/facebook/presto/hive/HiveTestUtils.java create mode 100644 presto-hive/src/test/java/com/facebook/presto/hive/MockAmazonS3.java create mode 100644 presto-hive/src/test/java/com/facebook/presto/hive/TestHiveBooleanParser.java create mode 100644 presto-hive/src/test/java/com/facebook/presto/hive/TestHiveBucketing.java create mode 100644 presto-hive/src/test/java/com/facebook/presto/hive/TestHiveClientConfig.java create mode 100644 presto-hive/src/test/java/com/facebook/presto/hive/TestHiveColumnHandle.java create mode 100644 presto-hive/src/test/java/com/facebook/presto/hive/TestHiveConnectorFactory.java create mode 100644 presto-hive/src/test/java/com/facebook/presto/hive/TestHiveDistributedQueries.java create mode 100644 presto-hive/src/test/java/com/facebook/presto/hive/TestHiveFileFormats.java create mode 100644 presto-hive/src/test/java/com/facebook/presto/hive/TestHiveIntegrationSmokeTest.java create mode 100644 presto-hive/src/test/java/com/facebook/presto/hive/TestHivePluginConfig.java create mode 100644 presto-hive/src/test/java/com/facebook/presto/hive/TestHiveSplit.java create mode 100644 presto-hive/src/test/java/com/facebook/presto/hive/TestHiveSplitSource.java create mode 100644 presto-hive/src/test/java/com/facebook/presto/hive/TestHiveTableHandle.java create mode 100644 presto-hive/src/test/java/com/facebook/presto/hive/TestHiveUtil.java create mode 100644 presto-hive/src/test/java/com/facebook/presto/hive/TestJsonHiveHandles.java create mode 100644 presto-hive/src/test/java/com/facebook/presto/hive/TestNumberParser.java create mode 100644 presto-hive/src/test/java/com/facebook/presto/hive/TestPrestoS3FileSystem.java create mode 100644 presto-hive/src/test/java/com/facebook/presto/hive/TestingHiveCluster.java create mode 100644 presto-hive/src/test/java/com/facebook/presto/hive/metastore/MockHiveMetastoreClient.java create mode 100644 presto-hive/src/test/java/com/facebook/presto/hive/metastore/TestCachingHiveMetastore.java create mode 100644 presto-hive/src/test/java/com/facebook/presto/hive/util/TestLazyMap.java create mode 100644 presto-hive/src/test/java/com/facebook/presto/hive/util/TestSerDeUtils.java create mode 100644 presto-hive/src/test/resources/addressbook.parquet create mode 100644 presto-hive/src/test/sql/create-test-cdh4.sql create mode 100644 presto-hive/src/test/sql/create-test-hive12.sql create mode 100644 presto-hive/src/test/sql/create-test-hive13.sql create mode 100644 presto-hive/src/test/sql/create-test-s3.sql create mode 100644 presto-hive/src/test/sql/create-test.sql create mode 100644 presto-hive/src/test/sql/drop-test-s3.sql create mode 100644 presto-hive/src/test/sql/drop-test.sql create mode 100644 presto-jdbc/pom.xml create mode 100644 presto-jdbc/src/main/java/com/facebook/presto/jdbc/ColumnInfo.java create mode 100644 presto-jdbc/src/main/java/com/facebook/presto/jdbc/JettyLogging.java create mode 100644 presto-jdbc/src/main/java/com/facebook/presto/jdbc/NotImplementedException.java create mode 100644 presto-jdbc/src/main/java/com/facebook/presto/jdbc/PrestoArray.java create mode 100644 presto-jdbc/src/main/java/com/facebook/presto/jdbc/PrestoConnection.java create mode 100644 presto-jdbc/src/main/java/com/facebook/presto/jdbc/PrestoDatabaseMetaData.java create mode 100644 presto-jdbc/src/main/java/com/facebook/presto/jdbc/PrestoDriver.java create mode 100644 presto-jdbc/src/main/java/com/facebook/presto/jdbc/PrestoIntervalDayTime.java create mode 100644 presto-jdbc/src/main/java/com/facebook/presto/jdbc/PrestoIntervalYearMonth.java create mode 100644 presto-jdbc/src/main/java/com/facebook/presto/jdbc/PrestoPreparedStatement.java create mode 100644 presto-jdbc/src/main/java/com/facebook/presto/jdbc/PrestoResultSet.java create mode 100644 presto-jdbc/src/main/java/com/facebook/presto/jdbc/PrestoResultSetMetaData.java create mode 100644 presto-jdbc/src/main/java/com/facebook/presto/jdbc/PrestoStatement.java create mode 100644 presto-jdbc/src/main/java/com/facebook/presto/jdbc/QueryExecutor.java create mode 100644 presto-jdbc/src/main/java/com/facebook/presto/jdbc/UserAgentRequestFilter.java create mode 100644 presto-jdbc/src/main/resources/META-INF/license/LICENSE.jol.txt create mode 100644 presto-jdbc/src/main/resources/META-INF/services/java.sql.Driver create mode 100644 presto-jdbc/src/test/java/com/facebook/presto/jdbc/TestDriver.java create mode 100644 presto-jdbc/src/test/java/com/facebook/presto/jdbc/TestJdbcResultSet.java create mode 100644 presto-kafka/pom.xml create mode 100644 presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaColumnHandle.java create mode 100644 presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaConnector.java create mode 100644 presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaConnectorConfig.java create mode 100644 presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaConnectorFactory.java create mode 100644 presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaConnectorModule.java create mode 100644 presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaErrorCode.java create mode 100644 presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaFieldValueProvider.java create mode 100644 presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaHandleResolver.java create mode 100644 presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaInternalFieldDescription.java create mode 100644 presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaMetadata.java create mode 100644 presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaPartition.java create mode 100644 presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaPlugin.java create mode 100644 presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaRecordSet.java create mode 100644 presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaRecordSetProvider.java create mode 100644 presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaSimpleConsumerManager.java create mode 100644 presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaSplit.java create mode 100644 presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaSplitManager.java create mode 100644 presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaTableDescriptionSupplier.java create mode 100644 presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaTableHandle.java create mode 100644 presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaTopicDescription.java create mode 100644 presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaTopicFieldDescription.java create mode 100644 presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaTopicFieldGroup.java create mode 100644 presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/KafkaDecoderModule.java create mode 100644 presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/KafkaDecoderRegistry.java create mode 100644 presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/KafkaFieldDecoder.java create mode 100644 presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/KafkaRowDecoder.java create mode 100644 presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/csv/CsvKafkaDecoderModule.java create mode 100644 presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/csv/CsvKafkaFieldDecoder.java create mode 100644 presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/csv/CsvKafkaRowDecoder.java create mode 100644 presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/dummy/DummyKafkaDecoderModule.java create mode 100644 presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/dummy/DummyKafkaFieldDecoder.java create mode 100644 presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/dummy/DummyKafkaRowDecoder.java create mode 100644 presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/json/CustomDateTimeJsonKafkaFieldDecoder.java create mode 100644 presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/json/ISO8601JsonKafkaFieldDecoder.java create mode 100644 presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/json/JsonKafkaDecoderModule.java create mode 100644 presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/json/JsonKafkaFieldDecoder.java create mode 100644 presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/json/JsonKafkaRowDecoder.java create mode 100644 presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/json/MillisecondsSinceEpochJsonKafkaFieldDecoder.java create mode 100644 presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/json/RFC2822JsonKafkaFieldDecoder.java create mode 100644 presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/json/SecondsSinceEpochJsonKafkaFieldDecoder.java create mode 100644 presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/raw/RawKafkaDecoderModule.java create mode 100644 presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/raw/RawKafkaFieldDecoder.java create mode 100644 presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/raw/RawKafkaRowDecoder.java create mode 100644 presto-kafka/src/test/java/com/facebook/presto/kafka/KafkaQueryRunner.java create mode 100644 presto-kafka/src/test/java/com/facebook/presto/kafka/TestKafkaConnectorConfig.java create mode 100644 presto-kafka/src/test/java/com/facebook/presto/kafka/TestKafkaDistributed.java create mode 100644 presto-kafka/src/test/java/com/facebook/presto/kafka/TestKafkaIntegrationSmokeTest.java create mode 100644 presto-kafka/src/test/java/com/facebook/presto/kafka/TestKafkaPlugin.java create mode 100644 presto-kafka/src/test/java/com/facebook/presto/kafka/TestManySegments.java create mode 100644 presto-kafka/src/test/java/com/facebook/presto/kafka/TestMinimalFunctionality.java create mode 100644 presto-kafka/src/test/java/com/facebook/presto/kafka/decoder/csv/TestCsvDecoder.java create mode 100644 presto-kafka/src/test/java/com/facebook/presto/kafka/decoder/json/TestISO8601JsonKafkaFieldDecoder.java create mode 100644 presto-kafka/src/test/java/com/facebook/presto/kafka/decoder/json/TestJsonDecoder.java create mode 100644 presto-kafka/src/test/java/com/facebook/presto/kafka/decoder/json/TestMillisecondsSinceEpochJsonKafkaFieldDecoder.java create mode 100644 presto-kafka/src/test/java/com/facebook/presto/kafka/decoder/json/TestRFC2822JsonKafkaFieldDecoder.java create mode 100644 presto-kafka/src/test/java/com/facebook/presto/kafka/decoder/json/TestSecondsSinceEpochJsonKafkaFieldDecoder.java create mode 100644 presto-kafka/src/test/java/com/facebook/presto/kafka/decoder/raw/TestRawDecoder.java create mode 100644 presto-kafka/src/test/java/com/facebook/presto/kafka/decoder/util/DecoderTestUtil.java create mode 100644 presto-kafka/src/test/java/com/facebook/presto/kafka/util/CodecSupplier.java create mode 100644 presto-kafka/src/test/java/com/facebook/presto/kafka/util/EmbeddedKafka.java create mode 100644 presto-kafka/src/test/java/com/facebook/presto/kafka/util/EmbeddedZookeeper.java create mode 100644 presto-kafka/src/test/java/com/facebook/presto/kafka/util/JsonEncoder.java create mode 100644 presto-kafka/src/test/java/com/facebook/presto/kafka/util/KafkaLoader.java create mode 100644 presto-kafka/src/test/java/com/facebook/presto/kafka/util/NumberEncoder.java create mode 100644 presto-kafka/src/test/java/com/facebook/presto/kafka/util/NumberPartitioner.java create mode 100644 presto-kafka/src/test/java/com/facebook/presto/kafka/util/TestUtils.java create mode 100644 presto-kafka/src/test/resources/decoder/json/message.json create mode 100644 presto-kafka/src/test/resources/tpch/customer.json create mode 100644 presto-kafka/src/test/resources/tpch/lineitem.json create mode 100644 presto-kafka/src/test/resources/tpch/nation.json create mode 100644 presto-kafka/src/test/resources/tpch/orders.json create mode 100644 presto-kafka/src/test/resources/tpch/part.json create mode 100644 presto-kafka/src/test/resources/tpch/partsupp.json create mode 100644 presto-kafka/src/test/resources/tpch/region.json create mode 100644 presto-kafka/src/test/resources/tpch/supplier.json create mode 100644 presto-main/etc/catalog/default.properties create mode 100644 presto-main/etc/catalog/example.properties create mode 100644 presto-main/etc/catalog/hive.properties create mode 100644 presto-main/etc/catalog/jmx.properties create mode 100644 presto-main/etc/catalog/tpch.properties create mode 100644 presto-main/etc/config.properties create mode 100644 presto-main/etc/jvm.config create mode 100644 presto-main/etc/log.properties create mode 100644 presto-main/pom.xml create mode 100644 presto-main/src/main/java/com/facebook/presto/ExceededMemoryLimitException.java create mode 100644 presto-main/src/main/java/com/facebook/presto/HashPagePartitionFunction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/OutputBuffers.java create mode 100644 presto-main/src/main/java/com/facebook/presto/PagePartitionFunction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/PagesIndexPageSorter.java create mode 100644 presto-main/src/main/java/com/facebook/presto/PrestoMediaTypes.java create mode 100644 presto-main/src/main/java/com/facebook/presto/ScheduledSplit.java create mode 100644 presto-main/src/main/java/com/facebook/presto/Session.java create mode 100644 presto-main/src/main/java/com/facebook/presto/SystemSessionProperties.java create mode 100644 presto-main/src/main/java/com/facebook/presto/TaskSource.java create mode 100644 presto-main/src/main/java/com/facebook/presto/UnpartitionedPagePartitionFunction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/block/BlockEncodingManager.java create mode 100644 presto-main/src/main/java/com/facebook/presto/block/BlockUtils.java create mode 100644 presto-main/src/main/java/com/facebook/presto/block/PagesSerde.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/Access.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/AnnotationDefinition.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/Block.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/ByteCodeNode.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/ByteCodeVisitor.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/ClassDefinition.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/ClassInfo.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/ClassInfoLoader.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/Comment.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/DumpByteCodeVisitor.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/DynamicClassLoader.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/FieldDefinition.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/MethodDefinition.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/MethodGenerationContext.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/OpCode.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/Parameter.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/ParameterizedType.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/Scope.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/SmartClassWriter.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/Variable.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/control/CaseStatement.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/control/DoWhileLoop.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/control/FlowControl.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/control/ForLoop.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/control/IfStatement.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/control/LookupSwitch.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/control/TryCatch.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/control/WhileLoop.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/debug/DebugNode.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/debug/LineNumberNode.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/debug/LocalVariableNode.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/expression/AndByteCodeExpression.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/expression/ArithmeticByteCodeExpression.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/expression/ByteCodeExpression.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/expression/ByteCodeExpressions.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/expression/CastByteCodeExpression.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/expression/ComparisonByteCodeExpression.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/expression/ConstantByteCodeExpression.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/expression/GetElementByteCodeExpression.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/expression/GetFieldByteCodeExpression.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/expression/InlineIfByteCodeExpression.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/expression/InvokeByteCodeExpression.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/expression/InvokeDynamicByteCodeExpression.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/expression/NegateByteCodeExpression.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/expression/NewInstanceByteCodeExpression.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/expression/NotByteCodeExpression.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/expression/OrByteCodeExpression.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/expression/PopByteCodeExpression.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/expression/ReturnByteCodeExpression.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/expression/SetFieldByteCodeExpression.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/instruction/Constant.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/instruction/FieldInstruction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/instruction/InstructionNode.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/instruction/InvokeInstruction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/instruction/JumpInstruction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/instruction/LabelNode.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/instruction/TypeInstruction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/byteCode/instruction/VariableInstruction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/concurrent/FairBatchExecutor.java create mode 100644 presto-main/src/main/java/com/facebook/presto/connector/ConnectorManager.java create mode 100644 presto-main/src/main/java/com/facebook/presto/connector/informationSchema/InformationSchemaColumnHandle.java create mode 100644 presto-main/src/main/java/com/facebook/presto/connector/informationSchema/InformationSchemaHandleResolver.java create mode 100644 presto-main/src/main/java/com/facebook/presto/connector/informationSchema/InformationSchemaMetadata.java create mode 100644 presto-main/src/main/java/com/facebook/presto/connector/informationSchema/InformationSchemaModule.java create mode 100644 presto-main/src/main/java/com/facebook/presto/connector/informationSchema/InformationSchemaPageSourceProvider.java create mode 100644 presto-main/src/main/java/com/facebook/presto/connector/informationSchema/InformationSchemaSplit.java create mode 100644 presto-main/src/main/java/com/facebook/presto/connector/informationSchema/InformationSchemaSplitManager.java create mode 100644 presto-main/src/main/java/com/facebook/presto/connector/informationSchema/InformationSchemaTableHandle.java create mode 100644 presto-main/src/main/java/com/facebook/presto/connector/jmx/JmxColumnHandle.java create mode 100644 presto-main/src/main/java/com/facebook/presto/connector/jmx/JmxConnectorFactory.java create mode 100644 presto-main/src/main/java/com/facebook/presto/connector/jmx/JmxConnectorId.java create mode 100644 presto-main/src/main/java/com/facebook/presto/connector/jmx/JmxHandleResolver.java create mode 100644 presto-main/src/main/java/com/facebook/presto/connector/jmx/JmxMetadata.java create mode 100644 presto-main/src/main/java/com/facebook/presto/connector/jmx/JmxRecordSetProvider.java create mode 100644 presto-main/src/main/java/com/facebook/presto/connector/jmx/JmxSplit.java create mode 100644 presto-main/src/main/java/com/facebook/presto/connector/jmx/JmxSplitManager.java create mode 100644 presto-main/src/main/java/com/facebook/presto/connector/jmx/JmxTableHandle.java create mode 100644 presto-main/src/main/java/com/facebook/presto/connector/system/CatalogSystemTable.java create mode 100644 presto-main/src/main/java/com/facebook/presto/connector/system/NodeSystemTable.java create mode 100644 presto-main/src/main/java/com/facebook/presto/connector/system/QuerySystemTable.java create mode 100644 presto-main/src/main/java/com/facebook/presto/connector/system/SystemColumnHandle.java create mode 100644 presto-main/src/main/java/com/facebook/presto/connector/system/SystemConnector.java create mode 100644 presto-main/src/main/java/com/facebook/presto/connector/system/SystemHandleResolver.java create mode 100644 presto-main/src/main/java/com/facebook/presto/connector/system/SystemRecordSetProvider.java create mode 100644 presto-main/src/main/java/com/facebook/presto/connector/system/SystemSplit.java create mode 100644 presto-main/src/main/java/com/facebook/presto/connector/system/SystemSplitManager.java create mode 100644 presto-main/src/main/java/com/facebook/presto/connector/system/SystemTableHandle.java create mode 100644 presto-main/src/main/java/com/facebook/presto/connector/system/SystemTablesMetadata.java create mode 100644 presto-main/src/main/java/com/facebook/presto/connector/system/SystemTablesModule.java create mode 100644 presto-main/src/main/java/com/facebook/presto/connector/system/TaskSystemTable.java create mode 100644 presto-main/src/main/java/com/facebook/presto/discovery/EmbeddedDiscoveryConfig.java create mode 100644 presto-main/src/main/java/com/facebook/presto/discovery/EmbeddedDiscoveryModule.java create mode 100644 presto-main/src/main/java/com/facebook/presto/event/query/QueryCompletionEvent.java create mode 100644 presto-main/src/main/java/com/facebook/presto/event/query/QueryCreatedEvent.java create mode 100644 presto-main/src/main/java/com/facebook/presto/event/query/QueryMonitor.java create mode 100644 presto-main/src/main/java/com/facebook/presto/event/query/SplitCompletionEvent.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/AbandonedException.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/BufferInfo.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/BufferResult.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/Column.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/CreateTableTask.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/CreateViewTask.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/DataDefinitionExecution.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/DataDefinitionTask.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/DropTableTask.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/DropViewTask.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/ExecutionFailureInfo.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/FailedQueryException.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/FailedQueryExecution.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/Failure.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/ForQueryExecution.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/Input.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/LocationFactory.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/NoSuchBufferException.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/NodeScheduler.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/NodeSchedulerConfig.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/NodeTaskMap.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/PageBufferInfo.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/PageSplitterUtil.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/PartitionBuffer.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/QueryExecution.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/QueryExecutionMBean.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/QueryId.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/QueryIdGenerator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/QueryInfo.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/QueryManager.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/QueryManagerConfig.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/QueryQueue.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/QueryQueueDefinition.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/QueryQueueManager.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/QueryQueueRule.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/QueryState.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/QueryStateMachine.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/QueryStats.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/QueuedExecution.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/RemoteTask.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/RemoteTaskFactory.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/RenameColumnTask.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/RenameTableTask.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/ResetSessionTask.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/ResettableRandomizedIterator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/SetSessionTask.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/SharedBuffer.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/SharedBufferInfo.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/SharedBufferMemoryManager.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/SimpleDomain.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/SimpleMarker.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/SimpleRange.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/SplitRunner.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/SqlQueryExecution.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/SqlQueryManager.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/SqlQueryManagerStats.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/SqlQueryQueueManager.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/SqlStageExecution.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/SqlTask.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/SqlTaskExecution.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/SqlTaskExecutionFactory.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/SqlTaskIoStats.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/SqlTaskManager.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/SqlTaskManagerStats.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/StageId.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/StageInfo.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/StageState.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/StageStateMachine.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/StageStats.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/StateMachine.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/TaskExecutor.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/TaskId.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/TaskInfo.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/TaskManager.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/TaskManagerConfig.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/TaskState.java create mode 100644 presto-main/src/main/java/com/facebook/presto/execution/TaskStateMachine.java create mode 100644 presto-main/src/main/java/com/facebook/presto/failureDetector/FailureDetector.java create mode 100644 presto-main/src/main/java/com/facebook/presto/failureDetector/FailureDetectorConfig.java create mode 100644 presto-main/src/main/java/com/facebook/presto/failureDetector/FailureDetectorModule.java create mode 100644 presto-main/src/main/java/com/facebook/presto/failureDetector/ForFailureDetector.java create mode 100644 presto-main/src/main/java/com/facebook/presto/failureDetector/HeartbeatFailureDetector.java create mode 100644 presto-main/src/main/java/com/facebook/presto/index/IndexHandleJacksonModule.java create mode 100644 presto-main/src/main/java/com/facebook/presto/index/IndexManager.java create mode 100644 presto-main/src/main/java/com/facebook/presto/memory/ClusterMemoryManager.java create mode 100644 presto-main/src/main/java/com/facebook/presto/memory/ClusterMemoryPool.java create mode 100644 presto-main/src/main/java/com/facebook/presto/memory/ForMemoryManager.java create mode 100644 presto-main/src/main/java/com/facebook/presto/memory/LocalMemoryManager.java create mode 100644 presto-main/src/main/java/com/facebook/presto/memory/LocalMemoryManagerExporter.java create mode 100644 presto-main/src/main/java/com/facebook/presto/memory/MemoryInfo.java create mode 100644 presto-main/src/main/java/com/facebook/presto/memory/MemoryManagerConfig.java create mode 100644 presto-main/src/main/java/com/facebook/presto/memory/MemoryPool.java create mode 100644 presto-main/src/main/java/com/facebook/presto/memory/MemoryPoolAssignment.java create mode 100644 presto-main/src/main/java/com/facebook/presto/memory/MemoryPoolAssignmentsRequest.java create mode 100644 presto-main/src/main/java/com/facebook/presto/memory/MemoryPoolId.java create mode 100644 presto-main/src/main/java/com/facebook/presto/memory/MemoryPoolInfo.java create mode 100644 presto-main/src/main/java/com/facebook/presto/memory/MemoryResource.java create mode 100644 presto-main/src/main/java/com/facebook/presto/memory/QueryContext.java create mode 100644 presto-main/src/main/java/com/facebook/presto/memory/RemoteNodeMemory.java create mode 100644 presto-main/src/main/java/com/facebook/presto/memory/ReservedSystemMemoryConfig.java create mode 100644 presto-main/src/main/java/com/facebook/presto/memory/VersionedMemoryPoolId.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/AbstractTypedJacksonModule.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/AllNodes.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/CatalogManager.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/CatalogManagerConfig.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/ColumnHandleJacksonModule.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/DiscoveryNodeManager.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/FunctionFactory.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/FunctionInfo.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/FunctionListBuilder.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/FunctionRegistry.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/HandleJsonModule.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/HandleResolver.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/InMemoryNodeManager.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/IndexHandle.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/InsertTableHandle.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/InsertTableHandleJacksonModule.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/InternalNodeManager.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/InternalTable.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/JsonTypeIdResolver.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/LegacyTableLayoutHandle.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/Metadata.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/MetadataManager.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/MetadataUtil.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/NodeVersion.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/OperatorNotFoundException.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/OperatorType.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/OutputTableHandle.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/OutputTableHandleJacksonModule.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/ParametricAggregation.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/ParametricFunction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/ParametricOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/ParametricScalar.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/PrestoNode.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/QualifiedTableName.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/QualifiedTablePrefix.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/RemoteSplitHandleResolver.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/ResolvedIndex.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/Signature.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/SpecializedFunctionKey.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/Split.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/SplitJacksonModule.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/TableHandle.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/TableHandleJacksonModule.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/TableLayout.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/TableLayoutHandle.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/TableLayoutHandleJacksonModule.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/TableLayoutResult.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/TableMetadata.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/TypeParameter.java create mode 100644 presto-main/src/main/java/com/facebook/presto/metadata/ViewDefinition.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/AggregationOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/ArrayUnnester.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/BigintGroupByHash.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/BlockedReason.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/ChannelSet.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/CursorProcessor.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/DeleteOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/Description.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/DistinctLimitOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/Driver.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/DriverContext.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/DriverFactory.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/DriverStats.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/ExchangeClient.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/ExchangeClientConfig.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/ExchangeClientFactory.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/ExchangeClientStatus.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/ExchangeOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/FilterAndProjectOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/FilterFunction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/FilterFunctions.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/FinishedOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/ForExchange.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/ForScheduler.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/GenericCursorProcessor.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/GenericPageProcessor.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/GroupByHash.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/GroupByIdBlock.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/HashAggregationOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/HashBuilderOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/HashGenerator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/HashPartitionMaskOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/HashSemiJoinOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/HttpPageBufferClient.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/InMemoryExchange.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/InMemoryExchangeSinkOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/InMemoryExchangeSourceOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/InMemoryJoinHash.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/InterpretedHashGenerator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/JoinProbe.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/JoinProbeFactory.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/LimitOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/LookupJoinOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/LookupJoinOperatorFactory.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/LookupJoinOperators.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/LookupSource.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/LookupSourceSupplier.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/MapUnnester.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/MarkDistinctHash.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/MarkDistinctOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/Mergeable.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/MultiChannelGroupByHash.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/Operator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/OperatorContext.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/OperatorFactory.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/OperatorStats.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/OrderByOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/OutputFactory.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/PageBufferClientStatus.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/PageProcessor.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/PageSourceOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/PageTooLargeException.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/PageTransportErrorException.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/PageTransportTimeoutException.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/PagesHashStrategy.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/PagesIndex.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/PagesIndexComparator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/PagesIndexOrdering.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/ParallelHashBuilder.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/ParallelLookupSourceSupplier.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/PartitionedLookupSource.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/PartitionedOutputOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/PipelineContext.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/PipelineStats.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/PrecomputedHashGenerator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/ProjectionFunction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/ProjectionFunctions.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/RecordProjectOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/RowComparator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/RowNumberOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/SampleOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/ScanFilterAndProjectOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/SetBuilderOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/SettableLookupSourceSupplier.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/SharedLookupSource.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/SimpleJoinProbe.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/SimplePagesHashStrategy.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/SimplePagesIndexComparator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/SourceOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/SourceOperatorFactory.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/SyntheticAddress.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/TableCommitOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/TableScanOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/TableWriterOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/TaskContext.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/TaskOutputOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/TaskStats.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/TopNOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/TopNRowNumberOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/TwoChannelJoinProbe.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/UnnestOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/Unnester.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/ValuesOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/WindowFunctionDefinition.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/WindowOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/AbstractMinMaxAggregation.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/Accumulator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/AccumulatorCompiler.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/AccumulatorFactory.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/AccumulatorFactoryBinder.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/AggregationCompiler.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/AggregationFunction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/AggregationMetadata.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/AggregationUtils.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/ApproximateAverageAggregations.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/ApproximateCountAggregation.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/ApproximateCountColumnAggregations.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/ApproximateCountDistinctAggregations.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/ApproximateDoublePercentileAggregations.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/ApproximateLongPercentileAggregations.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/ApproximateSetAggregation.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/ApproximateSumAggregations.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/ApproximateUtils.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/ArbitraryAggregation.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/ArrayAggregation.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/AverageAggregations.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/BlockIndex.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/BlockPosition.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/BooleanAndAggregation.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/BooleanOrAggregation.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/CombineFunction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/CorrelationAggregation.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/CountAggregation.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/CountColumn.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/CountIfAggregation.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/CovarianceAggregation.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/DoubleSumAggregation.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/GenericAccumulatorFactory.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/GenericAccumulatorFactoryBinder.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/GenericAggregationFunctionFactory.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/GroupedAccumulator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/InputFunction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/IntermediateInputFunction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/InternalAggregationFunction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/KeyValuePairs.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/LazyAccumulatorFactoryBinder.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/LongSumAggregation.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/MapAggregation.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/MaxAggregation.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/MaxBy.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/MergeHyperLogLogAggregation.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/MinAggregation.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/MinBy.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/NullablePosition.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/NumericHistogram.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/NumericHistogramAggregation.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/NumericHistogramStateFactory.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/OutputFunction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/RegressionAggregation.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/SampleWeight.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/SimpleTypedSet.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/TypedSet.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/VarianceAggregation.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/AbstractGroupedAccumulatorState.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/AccumulatorState.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/AccumulatorStateFactory.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/AccumulatorStateMetadata.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/AccumulatorStateSerializer.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/ArbitraryAggregationState.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/ArbitraryAggregationStateFactory.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/ArbitraryAggregationStateSerializer.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/ArrayAggregationState.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/ArrayAggregationStateFactory.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/ArrayAggregationStateSerializer.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/CorrelationState.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/CovarianceState.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/DigestAndPercentileState.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/DigestAndPercentileStateFactory.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/DigestAndPercentileStateSerializer.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/GroupedAccumulatorState.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/HyperLogLogState.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/HyperLogLogStateFactory.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/HyperLogLogStateSerializer.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/InitialBooleanValue.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/InitialDoubleValue.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/InitialLongValue.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/KeyValuePairStateSerializer.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/KeyValuePairsState.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/KeyValuePairsStateFactory.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/LongAndDoubleState.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/LongState.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/MaxOrMinByState.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/MaxOrMinByStateFactory.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/MaxOrMinByStateSerializer.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/NullableBooleanState.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/NullableBooleanStateSerializer.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/NullableDoubleState.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/NullableDoubleStateSerializer.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/NullableLongState.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/NullableLongStateSerializer.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/NumericHistogramStateSerializer.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/RegressionState.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/SliceState.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/SliceStateSerializer.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/StateCompiler.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/StateCompilerUtils.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/TriStateBooleanState.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/TriStateBooleanStateSerializer.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/VarianceState.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/index/DynamicTupleFilterFactory.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/index/FieldSetFilteringRecordSet.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/index/IndexBuildDriverFactoryProvider.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/index/IndexJoinLookupStats.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/index/IndexLoader.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/index/IndexLookupSource.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/index/IndexLookupSourceSupplier.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/index/IndexSnapshot.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/index/IndexSnapshotBuilder.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/index/IndexSourceOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/index/IndexSplit.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/index/IndexedData.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/index/PageBuffer.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/index/PageBufferOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/index/PageRecordSet.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/index/PagesIndexBuilderOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/index/StreamingIndexedData.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/index/TupleFilterProcessor.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/index/UnloadedIndexKeyRecordSet.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/index/UpdateRequest.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayCardinalityFunction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayConcatFunction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayConcatUtils.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayConstructor.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayContains.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayDistinctFunction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayEqualOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayFunctions.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayGreaterThanOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayGreaterThanOrEqualOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayHashCodeOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayIntersectFunction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayJoin.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayLessThanOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayLessThanOrEqualOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayNotEqualOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayPositionFunction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayRemoveFunction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/ArraySortFunction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/ArraySubscriptOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayToArrayCast.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayToElementConcatFunction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayToJsonCast.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/ColorFunctions.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/CombineHashFunction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/DateTimeFunctions.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/ElementToArrayConcatFunction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/Greatest.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/HyperLogLogFunctions.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/IdentityCast.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/JsonExtract.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/JsonFunctions.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/JsonOperators.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/JsonPath.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/JsonPathTokenizer.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/JsonToArrayCast.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/JsonToMapCast.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/Least.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/MapCardinalityFunction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/MapConstructor.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/MapEqualOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/MapHashCodeOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/MapKeys.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/MapNotEqualOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/MapSubscriptOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/MapToJsonCast.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/MapValues.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/MathFunctions.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/QuarterOfYearDateTimeField.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/RegexpFunctions.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/RowEqualOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/RowFieldReference.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/RowHashCodeOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/RowNotEqualOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/RowToJsonCast.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/ScalarFunction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/ScalarOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/StringFunctions.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/TestingRowConstructor.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/TryCastFunction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/UrlFunctions.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/scalar/VarbinaryFunctions.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/window/AbstractWindowFunctionSupplier.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/window/AggregateWindowFunction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/window/CumulativeDistributionFunction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/window/DenseRankFunction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/window/FirstValueFunction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/window/FrameInfo.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/window/LagFunction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/window/LastValueFunction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/window/LeadFunction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/window/NTileFunction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/window/NthValueFunction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/window/PercentRankFunction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/window/RankFunction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/window/RankingWindowFunction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/window/ReflectionWindowFunctionSupplier.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/window/RowNumberFunction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/window/ValueWindowFunction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/window/WindowFunction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/window/WindowFunctionSupplier.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/window/WindowIndex.java create mode 100644 presto-main/src/main/java/com/facebook/presto/operator/window/WindowPartition.java create mode 100644 presto-main/src/main/java/com/facebook/presto/pdbo/PdboConfig.java create mode 100644 presto-main/src/main/java/com/facebook/presto/pdbo/PdboManager.java create mode 100644 presto-main/src/main/java/com/facebook/presto/pdbo/PdboTable.java create mode 100644 presto-main/src/main/java/com/facebook/presto/pdbo/StepCalcManager.java create mode 100644 presto-main/src/main/java/com/facebook/presto/server/AsyncHttpExecutionMBean.java create mode 100644 presto-main/src/main/java/com/facebook/presto/server/BasicQueryInfo.java create mode 100644 presto-main/src/main/java/com/facebook/presto/server/CoordinatorModule.java create mode 100644 presto-main/src/main/java/com/facebook/presto/server/ExchangeExecutionMBean.java create mode 100644 presto-main/src/main/java/com/facebook/presto/server/ExecuteResource.java create mode 100644 presto-main/src/main/java/com/facebook/presto/server/ForAsyncHttpResponse.java create mode 100644 presto-main/src/main/java/com/facebook/presto/server/ForExecute.java create mode 100644 presto-main/src/main/java/com/facebook/presto/server/HttpLocationFactory.java create mode 100644 presto-main/src/main/java/com/facebook/presto/server/HttpRemoteTask.java create mode 100644 presto-main/src/main/java/com/facebook/presto/server/HttpRemoteTaskFactory.java create mode 100644 presto-main/src/main/java/com/facebook/presto/server/NodeResource.java create mode 100644 presto-main/src/main/java/com/facebook/presto/server/PagesResponseWriter.java create mode 100644 presto-main/src/main/java/com/facebook/presto/server/PluginClassLoader.java create mode 100644 presto-main/src/main/java/com/facebook/presto/server/PluginManager.java create mode 100644 presto-main/src/main/java/com/facebook/presto/server/PluginManagerConfig.java create mode 100644 presto-main/src/main/java/com/facebook/presto/server/PrestoJvmRequirements.java create mode 100644 presto-main/src/main/java/com/facebook/presto/server/PrestoServer.java create mode 100644 presto-main/src/main/java/com/facebook/presto/server/QueryExecutionResource.java create mode 100644 presto-main/src/main/java/com/facebook/presto/server/QueryResource.java create mode 100644 presto-main/src/main/java/com/facebook/presto/server/ResourceUtil.java create mode 100644 presto-main/src/main/java/com/facebook/presto/server/ServerConfig.java create mode 100644 presto-main/src/main/java/com/facebook/presto/server/ServerMainModule.java create mode 100644 presto-main/src/main/java/com/facebook/presto/server/SliceDeserializer.java create mode 100644 presto-main/src/main/java/com/facebook/presto/server/SliceSerializer.java create mode 100644 presto-main/src/main/java/com/facebook/presto/server/StageResource.java create mode 100644 presto-main/src/main/java/com/facebook/presto/server/StatementResource.java create mode 100644 presto-main/src/main/java/com/facebook/presto/server/TaskResource.java create mode 100644 presto-main/src/main/java/com/facebook/presto/server/TaskUpdateRequest.java create mode 100644 presto-main/src/main/java/com/facebook/presto/server/ThreadResource.java create mode 100644 presto-main/src/main/java/com/facebook/presto/server/ThrowableMapper.java create mode 100644 presto-main/src/main/java/com/facebook/presto/server/testing/FileUtils.java create mode 100644 presto-main/src/main/java/com/facebook/presto/server/testing/TestingPrestoServer.java create mode 100644 presto-main/src/main/java/com/facebook/presto/split/ConnectorAwareSplitSource.java create mode 100644 presto-main/src/main/java/com/facebook/presto/split/MappedRecordSet.java create mode 100644 presto-main/src/main/java/com/facebook/presto/split/PageSinkManager.java create mode 100644 presto-main/src/main/java/com/facebook/presto/split/PageSinkProvider.java create mode 100644 presto-main/src/main/java/com/facebook/presto/split/PageSourceManager.java create mode 100644 presto-main/src/main/java/com/facebook/presto/split/PageSourceProvider.java create mode 100644 presto-main/src/main/java/com/facebook/presto/split/RecordPageSinkProvider.java create mode 100644 presto-main/src/main/java/com/facebook/presto/split/RecordPageSourceProvider.java create mode 100644 presto-main/src/main/java/com/facebook/presto/split/RemoteSplit.java create mode 100644 presto-main/src/main/java/com/facebook/presto/split/SampledSplitSource.java create mode 100644 presto-main/src/main/java/com/facebook/presto/split/SplitManager.java create mode 100644 presto-main/src/main/java/com/facebook/presto/split/SplitSource.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/ExpressionUtils.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/analyzer/AggregateExtractor.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/analyzer/AggregationAnalyzer.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/analyzer/Analysis.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/analyzer/AnalysisContext.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/analyzer/Analyzer.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/analyzer/ExpressionAnalysis.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/analyzer/ExpressionAnalyzer.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/analyzer/FeaturesConfig.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/analyzer/Field.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/analyzer/FieldOrExpression.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/analyzer/QueryExplainer.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/analyzer/SemanticErrorCode.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/analyzer/SemanticException.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/analyzer/StatementAnalyzer.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/analyzer/TupleAnalyzer.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/analyzer/TupleDescriptor.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/analyzer/WindowFunctionExtractor.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/analyzer/WindowFunctionValidator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/gen/AndCodeGenerator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/gen/ArrayGeneratorUtils.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/gen/ArrayMapByteCodeExpression.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/gen/Binding.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/gen/BodyCompiler.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/gen/Bootstrap.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/gen/ByteCodeExpressionVisitor.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/gen/ByteCodeGenerator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/gen/ByteCodeGeneratorContext.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/gen/ByteCodeUtils.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/gen/CallSiteBinder.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/gen/CastCodeGenerator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/gen/CoalesceCodeGenerator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/gen/CompilerOperations.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/gen/CompilerUtils.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/gen/CursorProcessorCompiler.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/gen/ExpressionCompiler.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/gen/FunctionCallCodeGenerator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/gen/IfCodeGenerator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/gen/InCodeGenerator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/gen/InvokeFunctionByteCodeExpression.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/gen/IsDistinctFromCodeGenerator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/gen/IsNullCodeGenerator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/gen/IsolatedClass.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/gen/JoinCompiler.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/gen/JoinProbeCompiler.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/gen/NullIfCodeGenerator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/gen/OrCodeGenerator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/gen/OrderingCompiler.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/gen/PageProcessorCompiler.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/gen/SqlTypeByteCodeExpression.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/gen/SwitchCodeGenerator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/CompilerConfig.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/DependencyExtractor.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/DeterminismEvaluator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/DistributedExecutionPlanner.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/DomainTranslator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/DomainUtils.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/EffectivePredicateExtractor.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/EqualityInference.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/ExpressionInterpreter.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/ExpressionNodeInliner.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/ExpressionSymbolInliner.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/InputExtractor.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/InterpretedFilterFunction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/InterpretedProjectionFunction.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/LiteralInterpreter.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/LocalExecutionPlanner.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/LogicalPlanner.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/LookupSymbolResolver.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/NoOpSymbolResolver.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/Plan.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/PlanBuilder.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/PlanFragment.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/PlanFragmenter.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/PlanNodeIdAllocator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/PlanOptimizersFactory.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/PlanPrinter.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/PlanSanityChecker.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/ProjectionPushDown.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/QueryPlanner.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/RelationPlan.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/RelationPlanner.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/StageExecutionPlan.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/SubExpressionExtractor.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/SubPlan.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/Symbol.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/SymbolAllocator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/SymbolExtractor.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/SymbolResolver.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/SymbolToInputRewriter.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/TranslationMap.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/ActualProperties.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/AddExchanges.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/BeginTableWrite.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/CanonicalizeExpressions.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/CountConstantOptimizer.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/HashGenerationOptimizer.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/ImplementSampleAsFilter.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/IndexJoinOptimizer.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/LimitPushDown.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/LocalProperties.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/MergeProjections.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/MetadataQueryOptimizer.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/PickLayout.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/PlanOptimizer.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/PredicatePushDown.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/PreferredProperties.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/PropertyDerivations.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/PruneRedundantProjections.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/PruneUnreferencedOutputs.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/SetFlatteningOptimizer.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/SimplifyExpressions.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/SingleDistinctOptimizer.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/UnaliasSymbolReferences.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/WindowFilterPushDown.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/plan/AggregationNode.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/plan/ChildReplacer.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/plan/DeleteNode.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/plan/DistinctLimitNode.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/plan/ExchangeNode.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/plan/FilterNode.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/plan/IndexJoinNode.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/plan/IndexSourceNode.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/plan/JoinNode.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/plan/LimitNode.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/plan/MarkDistinctNode.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/plan/OutputNode.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/plan/PlanFragmentId.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/plan/PlanNode.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/plan/PlanNodeId.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/plan/PlanRewriter.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/plan/PlanVisitor.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/plan/ProjectNode.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/plan/RemoteSourceNode.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/plan/RowNumberNode.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/plan/SampleNode.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/plan/SemiJoinNode.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/plan/SortNode.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/plan/TableCommitNode.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/plan/TableScanNode.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/plan/TableWriterNode.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/plan/TopNNode.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/plan/TopNRowNumberNode.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/plan/UnionNode.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/plan/UnnestNode.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/plan/ValuesNode.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/planner/plan/WindowNode.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/relational/CallExpression.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/relational/ConstantExpression.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/relational/Expressions.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/relational/InputReferenceExpression.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/relational/RowExpression.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/relational/RowExpressionVisitor.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/relational/Signatures.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/relational/SqlToRowExpressionTranslator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/sql/relational/optimizer/ExpressionOptimizer.java create mode 100644 presto-main/src/main/java/com/facebook/presto/testing/LocalQueryRunner.java create mode 100644 presto-main/src/main/java/com/facebook/presto/testing/MaterializedResult.java create mode 100644 presto-main/src/main/java/com/facebook/presto/testing/MaterializedRow.java create mode 100644 presto-main/src/main/java/com/facebook/presto/testing/MaterializingOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/testing/NullOutputOperator.java create mode 100644 presto-main/src/main/java/com/facebook/presto/testing/QueryRunner.java create mode 100644 presto-main/src/main/java/com/facebook/presto/testing/RunLengthBlockEncoding.java create mode 100644 presto-main/src/main/java/com/facebook/presto/testing/RunLengthEncodedBlock.java create mode 100644 presto-main/src/main/java/com/facebook/presto/testing/TestingTaskContext.java create mode 100644 presto-main/src/main/java/com/facebook/presto/type/ArrayParametricType.java create mode 100644 presto-main/src/main/java/com/facebook/presto/type/ArrayType.java create mode 100644 presto-main/src/main/java/com/facebook/presto/type/BigintOperators.java create mode 100644 presto-main/src/main/java/com/facebook/presto/type/BooleanOperators.java create mode 100644 presto-main/src/main/java/com/facebook/presto/type/ColorType.java create mode 100644 presto-main/src/main/java/com/facebook/presto/type/DateOperators.java create mode 100644 presto-main/src/main/java/com/facebook/presto/type/DateTimeOperators.java create mode 100644 presto-main/src/main/java/com/facebook/presto/type/DoubleOperators.java create mode 100644 presto-main/src/main/java/com/facebook/presto/type/HyperLogLogOperators.java create mode 100644 presto-main/src/main/java/com/facebook/presto/type/IntervalDayTimeOperators.java create mode 100644 presto-main/src/main/java/com/facebook/presto/type/IntervalYearMonthOperators.java create mode 100644 presto-main/src/main/java/com/facebook/presto/type/JsonPathType.java create mode 100644 presto-main/src/main/java/com/facebook/presto/type/JsonType.java create mode 100644 presto-main/src/main/java/com/facebook/presto/type/LikeFunctions.java create mode 100644 presto-main/src/main/java/com/facebook/presto/type/LikePatternType.java create mode 100644 presto-main/src/main/java/com/facebook/presto/type/MapParametricType.java create mode 100644 presto-main/src/main/java/com/facebook/presto/type/MapType.java create mode 100644 presto-main/src/main/java/com/facebook/presto/type/ParametricType.java create mode 100644 presto-main/src/main/java/com/facebook/presto/type/RawSliceSerializer.java create mode 100644 presto-main/src/main/java/com/facebook/presto/type/RegexpType.java create mode 100644 presto-main/src/main/java/com/facebook/presto/type/RowParametricType.java create mode 100644 presto-main/src/main/java/com/facebook/presto/type/RowType.java create mode 100644 presto-main/src/main/java/com/facebook/presto/type/SqlType.java create mode 100644 presto-main/src/main/java/com/facebook/presto/type/TimeOperators.java create mode 100644 presto-main/src/main/java/com/facebook/presto/type/TimeWithTimeZoneOperators.java create mode 100644 presto-main/src/main/java/com/facebook/presto/type/TimestampOperators.java create mode 100644 presto-main/src/main/java/com/facebook/presto/type/TimestampWithTimeZoneOperators.java create mode 100644 presto-main/src/main/java/com/facebook/presto/type/TypeDeserializer.java create mode 100644 presto-main/src/main/java/com/facebook/presto/type/TypeJsonUtils.java create mode 100644 presto-main/src/main/java/com/facebook/presto/type/TypeRegistry.java create mode 100644 presto-main/src/main/java/com/facebook/presto/type/TypeUtils.java create mode 100644 presto-main/src/main/java/com/facebook/presto/type/UnknownType.java create mode 100644 presto-main/src/main/java/com/facebook/presto/type/VarbinaryOperators.java create mode 100644 presto-main/src/main/java/com/facebook/presto/type/VarcharOperators.java create mode 100644 presto-main/src/main/java/com/facebook/presto/util/CpuTimer.java create mode 100644 presto-main/src/main/java/com/facebook/presto/util/DateTimeUtils.java create mode 100644 presto-main/src/main/java/com/facebook/presto/util/DateTimeZoneIndex.java create mode 100644 presto-main/src/main/java/com/facebook/presto/util/Failures.java create mode 100644 presto-main/src/main/java/com/facebook/presto/util/GenerateTimeZoneIndex.java create mode 100644 presto-main/src/main/java/com/facebook/presto/util/GraphvizPrinter.java create mode 100644 presto-main/src/main/java/com/facebook/presto/util/ImmutableCollectors.java create mode 100644 presto-main/src/main/java/com/facebook/presto/util/JsonPlanPrinter.java create mode 100644 presto-main/src/main/java/com/facebook/presto/util/MoreFutures.java create mode 100644 presto-main/src/main/java/com/facebook/presto/util/NodeIdUserAgentRequestFilter.java create mode 100644 presto-main/src/main/java/com/facebook/presto/util/QueryExplanation.java create mode 100644 presto-main/src/main/java/com/facebook/presto/util/Reflection.java create mode 100644 presto-main/src/main/java/com/facebook/presto/util/ThreadLocalCache.java create mode 100644 presto-main/src/main/java/com/facebook/presto/util/Threads.java create mode 100644 presto-main/src/main/java/com/facebook/presto/util/Types.java create mode 100644 presto-main/src/main/java/com/facebook/presto/util/array/BigArrays.java create mode 100644 presto-main/src/main/java/com/facebook/presto/util/array/BlockBigArray.java create mode 100644 presto-main/src/main/java/com/facebook/presto/util/array/BooleanBigArray.java create mode 100644 presto-main/src/main/java/com/facebook/presto/util/array/ByteBigArray.java create mode 100644 presto-main/src/main/java/com/facebook/presto/util/array/DoubleBigArray.java create mode 100644 presto-main/src/main/java/com/facebook/presto/util/array/IntBigArray.java create mode 100644 presto-main/src/main/java/com/facebook/presto/util/array/LongBigArray.java create mode 100644 presto-main/src/main/java/com/facebook/presto/util/array/ObjectBigArray.java create mode 100644 presto-main/src/main/java/com/facebook/presto/util/array/SliceBigArray.java create mode 100644 presto-main/src/main/resources/com/facebook/presto/server/query-execution.html create mode 100644 presto-main/src/main/resources/com/facebook/presto/server/thread.html create mode 100644 presto-main/src/main/resources/webapp/favicon.ico create mode 100644 presto-main/src/main/resources/webapp/index.html create mode 100644 presto-main/src/main/resources/webapp/presto.png create mode 100644 presto-main/src/main/resources/webapp/query.html create mode 100644 presto-main/src/main/resources/webapp/timeline.html create mode 100644 presto-main/src/main/resources/webapp/units.js create mode 100644 presto-main/src/main/resources/webapp/vendor/bootstrap/css/bootstrap.css create mode 100644 presto-main/src/main/resources/webapp/vendor/bootstrap/css/bootstrap.css.map create mode 100644 presto-main/src/main/resources/webapp/vendor/bootstrap/css/bootstrap.min.css create mode 100644 presto-main/src/main/resources/webapp/vendor/bootstrap/img/glyphicons-halflings-white.png create mode 100644 presto-main/src/main/resources/webapp/vendor/bootstrap/img/glyphicons-halflings.png create mode 100644 presto-main/src/main/resources/webapp/vendor/bootstrap/js/bootstrap.js create mode 100644 presto-main/src/main/resources/webapp/vendor/bootstrap/js/bootstrap.min.js create mode 100644 presto-main/src/main/resources/webapp/vendor/d3-tip/d3.tip.v0.6.3.js create mode 100644 presto-main/src/main/resources/webapp/vendor/d3/d3-3.3.4.js create mode 100644 presto-main/src/main/resources/webapp/vendor/dagre-d3/dagre-d3-0.3.2.js create mode 100644 presto-main/src/main/resources/webapp/vendor/dagre-d3/dagre-d3.min.js create mode 100644 presto-main/src/main/resources/webapp/vendor/highlightjs/7.3/highlight.min.js create mode 100644 presto-main/src/main/resources/webapp/vendor/highlightjs/7.3/styles/idea.min.css create mode 100644 presto-main/src/main/resources/webapp/vendor/react/JSXTransformer.js create mode 100644 presto-main/src/main/resources/webapp/vendor/react/react.js create mode 100644 presto-main/src/main/resources/webapp/vendor/react/react.min.js create mode 100644 presto-main/src/main/resources/webapp/vendor/vis/dist/img/network/acceptDeleteIcon.png create mode 100644 presto-main/src/main/resources/webapp/vendor/vis/dist/img/network/addNodeIcon.png create mode 100644 presto-main/src/main/resources/webapp/vendor/vis/dist/img/network/backIcon.png create mode 100644 presto-main/src/main/resources/webapp/vendor/vis/dist/img/network/connectIcon.png create mode 100644 presto-main/src/main/resources/webapp/vendor/vis/dist/img/network/cross.png create mode 100644 presto-main/src/main/resources/webapp/vendor/vis/dist/img/network/cross2.png create mode 100644 presto-main/src/main/resources/webapp/vendor/vis/dist/img/network/deleteIcon.png create mode 100644 presto-main/src/main/resources/webapp/vendor/vis/dist/img/network/downArrow.png create mode 100644 presto-main/src/main/resources/webapp/vendor/vis/dist/img/network/editIcon.png create mode 100644 presto-main/src/main/resources/webapp/vendor/vis/dist/img/network/leftArrow.png create mode 100644 presto-main/src/main/resources/webapp/vendor/vis/dist/img/network/minus.png create mode 100644 presto-main/src/main/resources/webapp/vendor/vis/dist/img/network/plus.png create mode 100644 presto-main/src/main/resources/webapp/vendor/vis/dist/img/network/rightArrow.png create mode 100644 presto-main/src/main/resources/webapp/vendor/vis/dist/img/network/upArrow.png create mode 100644 presto-main/src/main/resources/webapp/vendor/vis/dist/img/network/zoomExtends.png create mode 100644 presto-main/src/main/resources/webapp/vendor/vis/dist/img/timeline/delete.png create mode 100644 presto-main/src/main/resources/webapp/vendor/vis/dist/vis.css create mode 100644 presto-main/src/main/resources/webapp/vendor/vis/dist/vis.js create mode 100644 presto-main/src/main/resources/webapp/vendor/vis/dist/vis.map create mode 100644 presto-main/src/main/resources/webapp/vendor/vis/dist/vis.min.css create mode 100644 presto-main/src/main/resources/webapp/vendor/vis/dist/vis.min.js create mode 100644 presto-main/src/test/java/com/facebook/presto/BenchmarkHashPagePartitionFunction.java create mode 100644 presto-main/src/test/java/com/facebook/presto/BenchmarkPagesIndexPageSorter.java create mode 100644 presto-main/src/test/java/com/facebook/presto/RowPageBuilder.java create mode 100644 presto-main/src/test/java/com/facebook/presto/RowPagesBuilder.java create mode 100644 presto-main/src/test/java/com/facebook/presto/SequencePageBuilder.java create mode 100644 presto-main/src/test/java/com/facebook/presto/SessionTestUtils.java create mode 100644 presto-main/src/test/java/com/facebook/presto/TestHiddenColumns.java create mode 100644 presto-main/src/test/java/com/facebook/presto/TestPagesIndexPageSorter.java create mode 100644 presto-main/src/test/java/com/facebook/presto/block/AbstractTestBlock.java create mode 100644 presto-main/src/test/java/com/facebook/presto/block/BlockAssertions.java create mode 100644 presto-main/src/test/java/com/facebook/presto/block/TestBlockBuilder.java create mode 100644 presto-main/src/test/java/com/facebook/presto/block/TestFixedWidthBlock.java create mode 100644 presto-main/src/test/java/com/facebook/presto/block/TestLazySliceArrayBlock.java create mode 100644 presto-main/src/test/java/com/facebook/presto/block/TestPagesSerde.java create mode 100644 presto-main/src/test/java/com/facebook/presto/block/TestRunLengthEncodedBlock.java create mode 100644 presto-main/src/test/java/com/facebook/presto/block/TestSliceArrayBlock.java create mode 100644 presto-main/src/test/java/com/facebook/presto/block/TestVariableWidthBlock.java create mode 100644 presto-main/src/test/java/com/facebook/presto/byteCode/expression/ByteCodeExpressionAssertions.java create mode 100644 presto-main/src/test/java/com/facebook/presto/byteCode/expression/TestArithmeticByteCodeExpression.java create mode 100644 presto-main/src/test/java/com/facebook/presto/byteCode/expression/TestCastByteCodeExpression.java create mode 100644 presto-main/src/test/java/com/facebook/presto/byteCode/expression/TestComparisonByteCodeExpression.java create mode 100644 presto-main/src/test/java/com/facebook/presto/byteCode/expression/TestConstantByteCodeExpression.java create mode 100644 presto-main/src/test/java/com/facebook/presto/byteCode/expression/TestGetElementByteCodeExpression.java create mode 100644 presto-main/src/test/java/com/facebook/presto/byteCode/expression/TestGetFieldByteCodeExpression.java create mode 100644 presto-main/src/test/java/com/facebook/presto/byteCode/expression/TestInlineIfByteCodeExpression.java create mode 100644 presto-main/src/test/java/com/facebook/presto/byteCode/expression/TestInvokeByteCodeExpression.java create mode 100644 presto-main/src/test/java/com/facebook/presto/byteCode/expression/TestInvokeDynamicByteCodeExpression.java create mode 100644 presto-main/src/test/java/com/facebook/presto/byteCode/expression/TestLogicalByteCodeExpression.java create mode 100644 presto-main/src/test/java/com/facebook/presto/byteCode/expression/TestNewInstanceByteCodeExpression.java create mode 100644 presto-main/src/test/java/com/facebook/presto/byteCode/expression/TestPopByteCodeExpression.java create mode 100644 presto-main/src/test/java/com/facebook/presto/byteCode/expression/TestSetFieldByteCodeExpression.java create mode 100644 presto-main/src/test/java/com/facebook/presto/byteCode/expression/TestSetVariableByteCodeExpression.java create mode 100644 presto-main/src/test/java/com/facebook/presto/concurrent/TestFairBatchExecutor.java create mode 100644 presto-main/src/test/java/com/facebook/presto/discovery/TestEmbeddedDiscoveryConfig.java create mode 100644 presto-main/src/test/java/com/facebook/presto/execution/MockRemoteTaskFactory.java create mode 100644 presto-main/src/test/java/com/facebook/presto/execution/TaskExecutorSimulator.java create mode 100644 presto-main/src/test/java/com/facebook/presto/execution/TaskExecutorTest.java create mode 100644 presto-main/src/test/java/com/facebook/presto/execution/TaskTestUtils.java create mode 100644 presto-main/src/test/java/com/facebook/presto/execution/TestColumn.java create mode 100644 presto-main/src/test/java/com/facebook/presto/execution/TestInput.java create mode 100644 presto-main/src/test/java/com/facebook/presto/execution/TestNodeScheduler.java create mode 100644 presto-main/src/test/java/com/facebook/presto/execution/TestNodeSchedulerConfig.java create mode 100644 presto-main/src/test/java/com/facebook/presto/execution/TestPageSplitterUtil.java create mode 100644 presto-main/src/test/java/com/facebook/presto/execution/TestQueryIdGenerator.java create mode 100644 presto-main/src/test/java/com/facebook/presto/execution/TestQueryManagerConfig.java create mode 100644 presto-main/src/test/java/com/facebook/presto/execution/TestQueryQueueDefinition.java create mode 100644 presto-main/src/test/java/com/facebook/presto/execution/TestQueryQueueRule.java create mode 100644 presto-main/src/test/java/com/facebook/presto/execution/TestQueryStateMachine.java create mode 100644 presto-main/src/test/java/com/facebook/presto/execution/TestQueryStats.java create mode 100644 presto-main/src/test/java/com/facebook/presto/execution/TestResetSessionTask.java create mode 100644 presto-main/src/test/java/com/facebook/presto/execution/TestResettableRandomizedIterator.java create mode 100644 presto-main/src/test/java/com/facebook/presto/execution/TestSetSessionTask.java create mode 100644 presto-main/src/test/java/com/facebook/presto/execution/TestSharedBuffer.java create mode 100644 presto-main/src/test/java/com/facebook/presto/execution/TestSimpleDomain.java create mode 100644 presto-main/src/test/java/com/facebook/presto/execution/TestSimpleMarker.java create mode 100644 presto-main/src/test/java/com/facebook/presto/execution/TestSimpleRange.java create mode 100644 presto-main/src/test/java/com/facebook/presto/execution/TestSqlQueryQueueManager.java create mode 100644 presto-main/src/test/java/com/facebook/presto/execution/TestSqlStageExecution.java create mode 100644 presto-main/src/test/java/com/facebook/presto/execution/TestSqlTask.java create mode 100644 presto-main/src/test/java/com/facebook/presto/execution/TestSqlTaskManager.java create mode 100644 presto-main/src/test/java/com/facebook/presto/execution/TestStageStateMachine.java create mode 100644 presto-main/src/test/java/com/facebook/presto/execution/TestStageStats.java create mode 100644 presto-main/src/test/java/com/facebook/presto/execution/TestStateMachine.java create mode 100644 presto-main/src/test/java/com/facebook/presto/execution/TestTaskManagerConfig.java create mode 100644 presto-main/src/test/java/com/facebook/presto/execution/TestingPageSourceProvider.java create mode 100644 presto-main/src/test/java/com/facebook/presto/execution/TestingSplit.java create mode 100644 presto-main/src/test/java/com/facebook/presto/failureDetector/TestHeartbeatFailureDetector.java create mode 100644 presto-main/src/test/java/com/facebook/presto/memory/TestMemoryManagerConfig.java create mode 100644 presto-main/src/test/java/com/facebook/presto/memory/TestMemoryPools.java create mode 100644 presto-main/src/test/java/com/facebook/presto/memory/TestReservedSystemMemoryConfig.java create mode 100644 presto-main/src/test/java/com/facebook/presto/metadata/TestDiscoveryNodeManager.java create mode 100644 presto-main/src/test/java/com/facebook/presto/metadata/TestFunctionRegistry.java create mode 100644 presto-main/src/test/java/com/facebook/presto/metadata/TestJsonTableHandle.java create mode 100644 presto-main/src/test/java/com/facebook/presto/metadata/TestQualifiedTablePrefix.java create mode 100644 presto-main/src/test/java/com/facebook/presto/metadata/TestSignature.java create mode 100644 presto-main/src/test/java/com/facebook/presto/metadata/TestingMetadata.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/BenchmarkGroupByHash.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/MockExchangeRequestProcessor.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/OperatorAssertion.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/PageAssertions.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/TestAggregationOperator.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/TestDistinctLimitOperator.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/TestDriver.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/TestDriverStats.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/TestExchangeClient.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/TestExchangeClientConfig.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/TestExchangeOperator.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/TestFilterAndProjectOperator.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/TestGroupByHash.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/TestHashAggregationOperator.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/TestHashJoinOperator.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/TestHashPartitionMaskOperator.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/TestHashSemiJoinOperator.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/TestHttpPageBufferClient.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/TestLimitOperator.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/TestMarkDistinctOperator.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/TestOperatorStats.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/TestOrderByOperator.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/TestPipelineStats.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/TestRecordProjectOperator.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/TestRowNumberOperator.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/TestScanFilterAndProjectOperator.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/TestTaskStats.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/TestTopNOperator.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/TestTopNRowNumberOperator.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/TestUnnestOperator.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/TestWindowOperator.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/AbstractTestAggregationFunction.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/AbstractTestApproximateAggregationFunction.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/AbstractTestApproximateCountDistinct.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/AggregationTestUtils.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestApproximateCountAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestApproximateCountDistinctAggregations.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestApproximateCountDistinctDouble.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestApproximateCountDistinctLong.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestApproximateCountDistinctVarBinary.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestApproximateCountDoubleAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestApproximateCountLongAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestApproximateDoubleSumAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestApproximateLongSumAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestApproximatePercentileAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestArbitraryAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestArrayAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestArrayMinAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestBooleanAndAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestBooleanMaxAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestBooleanMinAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestBooleanOrAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestCorrelationAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestCountAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestCountColumnAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestCountIfAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestCountNullAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestCovariancePopAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestCovarianceSampAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestDateMaxAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestDoubleApproximateAverageAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestDoubleAverageAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestDoubleMaxAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestDoubleMinAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestDoubleStdDevAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestDoubleStdDevPopAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestDoubleSumAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestDoubleVarianceAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestDoubleVariancePopAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestLongApproximateAverageAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestLongAverageAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestLongMaxAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestLongMinAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestLongStdDevAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestLongStdDevPopAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestLongSumAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestLongVarianceAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestLongVariancePopAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestMapAggAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestMaxByAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestMinByAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestNumericHistogram.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestNumericHistogramAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestRegrInterceptAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestRegrSlopeAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestSimpleTypedSet.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestStateCompiler.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestVarBinaryMaxAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/aggregation/TestVarBinaryMinAggregation.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/index/TestTupleFilterProcessor.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/scalar/AbstractTestFunctions.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/scalar/CustomFunctions.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/scalar/FunctionAssertions.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/scalar/RegexpFunctionsBenchmark.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/scalar/StringFunctionsBenchmark.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/scalar/TestColorFunctions.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/scalar/TestConditions.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/scalar/TestCustomFunctions.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/scalar/TestDateTimeFunctions.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/scalar/TestJsonExtract.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/scalar/TestJsonFunctions.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/scalar/TestMathFunctions.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/scalar/TestRegexpFunctions.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/scalar/TestStringFunctions.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/scalar/TestUrlFunctions.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/scalar/TestVarbinaryFunctions.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/window/AbstractTestWindowFunction.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/window/TestAggregateWindowFunction.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/window/TestCumulativeDistributionFunction.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/window/TestDenseRankFunction.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/window/TestFirstValueFunction.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/window/TestLagFunction.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/window/TestLastValueFunction.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/window/TestLeadFunction.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/window/TestNTileFunction.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/window/TestNthValueFunction.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/window/TestPercentRankFunction.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/window/TestRankFunction.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/window/TestRowNumberFunction.java create mode 100644 presto-main/src/test/java/com/facebook/presto/operator/window/WindowAssertions.java create mode 100644 presto-main/src/test/java/com/facebook/presto/server/MockHttpServletRequest.java create mode 100644 presto-main/src/test/java/com/facebook/presto/server/NoOpFailureDetector.java create mode 100644 presto-main/src/test/java/com/facebook/presto/server/TestExecuteResource.java create mode 100644 presto-main/src/test/java/com/facebook/presto/server/TestFailureDetectorConfig.java create mode 100644 presto-main/src/test/java/com/facebook/presto/server/TestNodeResource.java create mode 100644 presto-main/src/test/java/com/facebook/presto/server/TestPluginManagerConfig.java create mode 100644 presto-main/src/test/java/com/facebook/presto/server/TestPrestoJvmRequirements.java create mode 100644 presto-main/src/test/java/com/facebook/presto/server/TestResourceUtil.java create mode 100644 presto-main/src/test/java/com/facebook/presto/server/TestServer.java create mode 100644 presto-main/src/test/java/com/facebook/presto/server/TestServerConfig.java create mode 100644 presto-main/src/test/java/com/facebook/presto/split/TestInternalSplit.java create mode 100644 presto-main/src/test/java/com/facebook/presto/split/TestJmxSplitManager.java create mode 100644 presto-main/src/test/java/com/facebook/presto/sql/TestExpressionInterpreter.java create mode 100644 presto-main/src/test/java/com/facebook/presto/sql/TestExpressionOptimizer.java create mode 100644 presto-main/src/test/java/com/facebook/presto/sql/TestLikeFunctions.java create mode 100644 presto-main/src/test/java/com/facebook/presto/sql/TestSqlToRowExpressionTranslator.java create mode 100644 presto-main/src/test/java/com/facebook/presto/sql/analyzer/TestAnalyzer.java create mode 100644 presto-main/src/test/java/com/facebook/presto/sql/analyzer/TestFeaturesConfig.java create mode 100644 presto-main/src/test/java/com/facebook/presto/sql/gen/BenchmarkPageProcessor.java create mode 100644 presto-main/src/test/java/com/facebook/presto/sql/gen/TestExpressionCompiler.java create mode 100644 presto-main/src/test/java/com/facebook/presto/sql/gen/TestJoinCompiler.java create mode 100644 presto-main/src/test/java/com/facebook/presto/sql/gen/TestJoinProbeCompiler.java create mode 100644 presto-main/src/test/java/com/facebook/presto/sql/planner/TestDeterminismEvaluator.java create mode 100644 presto-main/src/test/java/com/facebook/presto/sql/planner/TestDomainTranslator.java create mode 100644 presto-main/src/test/java/com/facebook/presto/sql/planner/TestEffectivePredicateExtractor.java create mode 100644 presto-main/src/test/java/com/facebook/presto/sql/planner/TestEqualityInference.java create mode 100644 presto-main/src/test/java/com/facebook/presto/sql/planner/TestInterpretedFilterFunction.java create mode 100644 presto-main/src/test/java/com/facebook/presto/sql/planner/TestInterpretedProjectionFunction.java create mode 100644 presto-main/src/test/java/com/facebook/presto/sql/planner/TestLocalExecutionPlanner.java create mode 100644 presto-main/src/test/java/com/facebook/presto/sql/planner/TestSymbolAllocator.java create mode 100644 presto-main/src/test/java/com/facebook/presto/sql/planner/TestingColumnHandle.java create mode 100644 presto-main/src/test/java/com/facebook/presto/sql/planner/TestingTableHandle.java create mode 100644 presto-main/src/test/java/com/facebook/presto/sql/planner/optimizations/TestAddExchanges.java create mode 100644 presto-main/src/test/java/com/facebook/presto/sql/planner/optimizations/TestLocalProperties.java create mode 100644 presto-main/src/test/java/com/facebook/presto/type/AbstractTestType.java create mode 100644 presto-main/src/test/java/com/facebook/presto/type/BooleanOperatorsTest.java create mode 100644 presto-main/src/test/java/com/facebook/presto/type/TestArrayOperators.java create mode 100644 presto-main/src/test/java/com/facebook/presto/type/TestBigintArrayType.java create mode 100644 presto-main/src/test/java/com/facebook/presto/type/TestBigintOperators.java create mode 100644 presto-main/src/test/java/com/facebook/presto/type/TestBigintType.java create mode 100644 presto-main/src/test/java/com/facebook/presto/type/TestBigintVarcharMapType.java create mode 100644 presto-main/src/test/java/com/facebook/presto/type/TestBooleanOperators.java create mode 100644 presto-main/src/test/java/com/facebook/presto/type/TestBooleanType.java create mode 100644 presto-main/src/test/java/com/facebook/presto/type/TestColorType.java create mode 100644 presto-main/src/test/java/com/facebook/presto/type/TestDate.java create mode 100644 presto-main/src/test/java/com/facebook/presto/type/TestDateTimeOperators.java create mode 100644 presto-main/src/test/java/com/facebook/presto/type/TestDateType.java create mode 100644 presto-main/src/test/java/com/facebook/presto/type/TestDoubleOperators.java create mode 100644 presto-main/src/test/java/com/facebook/presto/type/TestDoubleType.java create mode 100644 presto-main/src/test/java/com/facebook/presto/type/TestIntervalDayTime.java create mode 100644 presto-main/src/test/java/com/facebook/presto/type/TestIntervalDayTimeType.java create mode 100644 presto-main/src/test/java/com/facebook/presto/type/TestIntervalYearMonth.java create mode 100644 presto-main/src/test/java/com/facebook/presto/type/TestIntervalYearMonthType.java create mode 100644 presto-main/src/test/java/com/facebook/presto/type/TestJsonType.java create mode 100644 presto-main/src/test/java/com/facebook/presto/type/TestMapOperators.java create mode 100644 presto-main/src/test/java/com/facebook/presto/type/TestRowOperators.java create mode 100644 presto-main/src/test/java/com/facebook/presto/type/TestSimpleRowType.java create mode 100644 presto-main/src/test/java/com/facebook/presto/type/TestTime.java create mode 100644 presto-main/src/test/java/com/facebook/presto/type/TestTimeType.java create mode 100644 presto-main/src/test/java/com/facebook/presto/type/TestTimeWithTimeZone.java create mode 100644 presto-main/src/test/java/com/facebook/presto/type/TestTimeWithTimeZoneType.java create mode 100644 presto-main/src/test/java/com/facebook/presto/type/TestTimestamp.java create mode 100644 presto-main/src/test/java/com/facebook/presto/type/TestTimestampType.java create mode 100644 presto-main/src/test/java/com/facebook/presto/type/TestTimestampWithTimeZone.java create mode 100644 presto-main/src/test/java/com/facebook/presto/type/TestTimestampWithTimeZoneType.java create mode 100644 presto-main/src/test/java/com/facebook/presto/type/TestUnknownType.java create mode 100644 presto-main/src/test/java/com/facebook/presto/type/TestVarbinaryType.java create mode 100644 presto-main/src/test/java/com/facebook/presto/type/TestVarcharOperators.java create mode 100644 presto-main/src/test/java/com/facebook/presto/type/TestVarcharType.java create mode 100644 presto-main/src/test/java/com/facebook/presto/util/InfiniteRecordSet.java create mode 100644 presto-main/src/test/java/com/facebook/presto/util/TestThreadLocalCache.java create mode 100644 presto-main/src/test/java/com/facebook/presto/util/TestTimeZoneUtils.java create mode 100644 presto-main/src/test/java/com/facebook/presto/util/TestTypes.java create mode 100644 presto-main/src/test/resources/queue_config.json create mode 100644 presto-main/src/test/resources/queue_config_bad_cycle.json create mode 100644 presto-main/src/test/resources/queue_config_bad_selfcycle.json create mode 100644 presto-ml/pom.xml create mode 100644 presto-ml/src/main/java/com/facebook/presto/ml/AbstractFeatureTransformation.java create mode 100644 presto-ml/src/main/java/com/facebook/presto/ml/AbstractSvmModel.java create mode 100644 presto-ml/src/main/java/com/facebook/presto/ml/Classifier.java create mode 100644 presto-ml/src/main/java/com/facebook/presto/ml/ClassifierFeatureTransformer.java create mode 100644 presto-ml/src/main/java/com/facebook/presto/ml/Dataset.java create mode 100644 presto-ml/src/main/java/com/facebook/presto/ml/EvaluateClassifierPredictionsAggregation.java create mode 100644 presto-ml/src/main/java/com/facebook/presto/ml/EvaluateClassifierPredictionsState.java create mode 100644 presto-ml/src/main/java/com/facebook/presto/ml/EvaluateClassifierPredictionsStateFactory.java create mode 100644 presto-ml/src/main/java/com/facebook/presto/ml/EvaluateClassifierPredictionsStateSerializer.java create mode 100644 presto-ml/src/main/java/com/facebook/presto/ml/FeatureTransformation.java create mode 100644 presto-ml/src/main/java/com/facebook/presto/ml/FeatureUnitNormalizer.java create mode 100644 presto-ml/src/main/java/com/facebook/presto/ml/FeatureVector.java create mode 100644 presto-ml/src/main/java/com/facebook/presto/ml/FeatureVectorUnitNormalizer.java create mode 100644 presto-ml/src/main/java/com/facebook/presto/ml/LearnClassifierAggregation.java create mode 100644 presto-ml/src/main/java/com/facebook/presto/ml/LearnLibSvmClassifierAggregation.java create mode 100644 presto-ml/src/main/java/com/facebook/presto/ml/LearnLibSvmRegressorAggregation.java create mode 100644 presto-ml/src/main/java/com/facebook/presto/ml/LearnLibSvmVarcharClassifierAggregation.java create mode 100644 presto-ml/src/main/java/com/facebook/presto/ml/LearnRegressorAggregation.java create mode 100644 presto-ml/src/main/java/com/facebook/presto/ml/LearnState.java create mode 100644 presto-ml/src/main/java/com/facebook/presto/ml/LearnStateFactory.java create mode 100644 presto-ml/src/main/java/com/facebook/presto/ml/LearnStateSerializer.java create mode 100644 presto-ml/src/main/java/com/facebook/presto/ml/LearnVarcharClassifierAggregation.java create mode 100644 presto-ml/src/main/java/com/facebook/presto/ml/LibSvmUtils.java create mode 100644 presto-ml/src/main/java/com/facebook/presto/ml/MLFunctionFactory.java create mode 100644 presto-ml/src/main/java/com/facebook/presto/ml/MLFunctions.java create mode 100644 presto-ml/src/main/java/com/facebook/presto/ml/MLPlugin.java create mode 100644 presto-ml/src/main/java/com/facebook/presto/ml/Model.java create mode 100644 presto-ml/src/main/java/com/facebook/presto/ml/ModelUtils.java create mode 100644 presto-ml/src/main/java/com/facebook/presto/ml/Regressor.java create mode 100644 presto-ml/src/main/java/com/facebook/presto/ml/RegressorFeatureTransformer.java create mode 100644 presto-ml/src/main/java/com/facebook/presto/ml/StringClassifierAdapter.java create mode 100644 presto-ml/src/main/java/com/facebook/presto/ml/SvmClassifier.java create mode 100644 presto-ml/src/main/java/com/facebook/presto/ml/SvmRegressor.java create mode 100644 presto-ml/src/main/java/com/facebook/presto/ml/type/ClassifierParametricType.java create mode 100644 presto-ml/src/main/java/com/facebook/presto/ml/type/ClassifierType.java create mode 100644 presto-ml/src/main/java/com/facebook/presto/ml/type/ModelType.java create mode 100644 presto-ml/src/main/java/com/facebook/presto/ml/type/RegressorType.java create mode 100644 presto-ml/src/main/provisio/assembly.xml create mode 100644 presto-ml/src/test/java/com/facebook/presto/ml/TestEvaluateClassifierPredictions.java create mode 100644 presto-ml/src/test/java/com/facebook/presto/ml/TestFeatureTransformations.java create mode 100644 presto-ml/src/test/java/com/facebook/presto/ml/TestLearnAggregations.java create mode 100644 presto-ml/src/test/java/com/facebook/presto/ml/TestMLQueries.java create mode 100644 presto-ml/src/test/java/com/facebook/presto/ml/TestModelSerialization.java create mode 100644 presto-ml/src/test/java/com/facebook/presto/ml/TestUtils.java create mode 100644 presto-mysql/pom.xml create mode 100644 presto-mysql/src/main/java/com/facebook/presto/plugin/mysql/MySqlClient.java create mode 100644 presto-mysql/src/main/java/com/facebook/presto/plugin/mysql/MySqlClientModule.java create mode 100644 presto-mysql/src/main/java/com/facebook/presto/plugin/mysql/MySqlConfig.java create mode 100644 presto-mysql/src/main/java/com/facebook/presto/plugin/mysql/MySqlPlugin.java create mode 100644 presto-mysql/src/test/java/com/facebook/presto/plugin/mysql/MySqlQueryRunner.java create mode 100644 presto-mysql/src/test/java/com/facebook/presto/plugin/mysql/TestMySqlConfig.java create mode 100644 presto-mysql/src/test/java/com/facebook/presto/plugin/mysql/TestMySqlDistributedQueries.java create mode 100644 presto-mysql/src/test/java/com/facebook/presto/plugin/mysql/TestMySqlIntegrationSmokeTest.java create mode 100644 presto-mysql/src/test/java/com/facebook/presto/plugin/mysql/TestMySqlPlugin.java create mode 100644 presto-oracle/pom.xml create mode 100644 presto-oracle/src/main/java/com/facebook/presto/plugin/oracle/OracleClient.java create mode 100644 presto-oracle/src/main/java/com/facebook/presto/plugin/oracle/OracleClientModule.java create mode 100644 presto-oracle/src/main/java/com/facebook/presto/plugin/oracle/OraclePlugin.java create mode 100644 presto-oracle/src/test/java/com/facebook/presto/plugin/oracle/AppTest.java create mode 100644 presto-orc/pom.xml create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/AbstractOrcDataSource.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/BooleanVector.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/DiskRange.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/DoubleVector.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/FileOrcDataSource.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/LongVector.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/ObjectVector.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/OrcCorruptionException.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/OrcDataSource.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/OrcDataSourceUtils.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/OrcPredicate.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/OrcReader.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/OrcRecordReader.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/RowGroup.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/SliceVector.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/StreamDescriptor.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/StreamId.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/Stripe.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/StripeReader.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/TupleDomainOrcPredicate.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/Vector.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/block/BlockReader.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/block/BlockReaders.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/block/BooleanBlockReader.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/block/ByteBlockReader.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/block/DateBlockReader.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/block/DoubleBlockReader.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/block/FloatBlockReader.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/block/ListBlockReader.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/block/LongBlockReader.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/block/LongDictionaryBlockReader.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/block/LongDirectBlockReader.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/block/MapBlockReader.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/block/SliceBlockReader.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/block/SliceDictionaryBlockReader.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/block/SliceDirectBlockReader.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/block/StructBlockReader.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/block/TimestampBlockReader.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/checkpoint/BooleanStreamCheckpoint.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/checkpoint/ByteArrayStreamCheckpoint.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/checkpoint/ByteStreamCheckpoint.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/checkpoint/Checkpoints.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/checkpoint/DoubleStreamCheckpoint.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/checkpoint/FloatStreamCheckpoint.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/checkpoint/InputStreamCheckpoint.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/checkpoint/InvalidCheckpointException.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/checkpoint/LongStreamCheckpoint.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/checkpoint/LongStreamDwrfCheckpoint.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/checkpoint/LongStreamV1Checkpoint.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/checkpoint/LongStreamV2Checkpoint.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/checkpoint/RowGroupDictionaryLengthStreamCheckpoint.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/checkpoint/StreamCheckpoint.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/metadata/BooleanStatistics.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/metadata/ColumnEncoding.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/metadata/ColumnStatistics.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/metadata/CompressionKind.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/metadata/DateStatistics.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/metadata/DoubleStatistics.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/metadata/DwrfMetadataReader.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/metadata/Footer.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/metadata/IntegerStatistics.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/metadata/Metadata.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/metadata/MetadataReader.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/metadata/OrcMetadataReader.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/metadata/OrcType.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/metadata/PostScript.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/metadata/RangeStatistics.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/metadata/RowGroupIndex.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/metadata/Stream.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/metadata/StringStatistics.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/metadata/StripeFooter.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/metadata/StripeInformation.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/metadata/StripeStatistics.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/orc-file-format.md create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/reader/BlockStreamReader.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/reader/BooleanStreamReader.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/reader/ByteStreamReader.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/reader/DoubleStreamReader.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/reader/FloatStreamReader.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/reader/LongDictionaryStreamReader.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/reader/LongDirectStreamReader.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/reader/LongStreamReader.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/reader/OrcReaderUtils.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/reader/SliceDictionaryStreamReader.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/reader/SliceDirectStreamReader.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/reader/SliceStreamReader.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/reader/StreamReader.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/reader/StreamReaders.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/reader/TimestampStreamReader.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/results.txt create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/stream/BooleanStream.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/stream/ByteArrayStream.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/stream/ByteStream.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/stream/CheckpointStreamSource.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/stream/DoubleStream.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/stream/FloatStream.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/stream/LongDecode.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/stream/LongStream.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/stream/LongStreamDwrf.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/stream/LongStreamV1.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/stream/LongStreamV2.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/stream/MissingStreamSource.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/stream/OrcInputStream.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/stream/OrcStreamUtils.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/stream/RowGroupDictionaryLengthStream.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/stream/StreamSource.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/stream/StreamSources.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/stream/ValueStream.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/stream/ValueStreamSource.java create mode 100644 presto-orc/src/main/java/com/facebook/presto/orc/stream/ValueStreams.java create mode 100644 presto-orc/src/test/java/com/facebook/presto/orc/AbstractTestOrcReader.java create mode 100644 presto-orc/src/test/java/com/facebook/presto/orc/OrcTester.java create mode 100644 presto-orc/src/test/java/com/facebook/presto/orc/TestFullOrcReader.java create mode 100644 presto-orc/src/test/java/com/facebook/presto/orc/TestOrcDataSourceUtils.java create mode 100644 presto-orc/src/test/java/com/facebook/presto/orc/TestOrcReader.java create mode 100644 presto-orc/src/test/java/com/facebook/presto/orc/TestOrcReaderPositions.java create mode 100644 presto-orc/src/test/java/com/facebook/presto/orc/TestTupleDomainOrcPredicate.java create mode 100644 presto-orc/src/test/java/com/facebook/presto/orc/TestingOrcPredicate.java create mode 100644 presto-orc/src/test/java/com/facebook/presto/orc/metadata/TestOrcMetadataReader.java create mode 100644 presto-parser/pom.xml create mode 100644 presto-parser/src/main/antlr4/com/facebook/presto/sql/parser/SqlBase.g4 create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/ExpressionFormatter.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/QueryUtil.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/Serialization.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/SqlFormatter.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/TreePrinter.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/parser/AstBuilder.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/parser/CaseInsensitiveStream.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/parser/DelimiterLexer.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/parser/IdentifierSymbol.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/parser/ParsingException.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/parser/SqlParser.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/parser/SqlParserOptions.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/parser/StatementSplitter.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/testing/TreeAssertions.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/AliasedRelation.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/AllColumns.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/Approximate.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/ArithmeticBinaryExpression.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/ArithmeticUnaryExpression.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/ArrayConstructor.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/AstVisitor.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/BetweenPredicate.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/BooleanLiteral.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/Cast.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/CoalesceExpression.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/ComparisonExpression.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/CreateTable.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/CreateTableAsSelect.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/CreateView.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/CurrentTime.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/DefaultExpressionTraversalVisitor.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/DefaultTraversalVisitor.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/Delete.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/DoubleLiteral.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/DropTable.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/DropView.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/Except.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/ExistsPredicate.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/Explain.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/ExplainFormat.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/ExplainOption.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/ExplainType.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/Expression.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/ExpressionRewriter.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/ExpressionTreeRewriter.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/Extract.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/FrameBound.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/FunctionCall.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/GenericLiteral.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/IfExpression.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/InListExpression.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/InPredicate.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/InputReference.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/Insert.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/Intersect.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/IntervalLiteral.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/IsNotNullPredicate.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/IsNullPredicate.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/Join.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/JoinCriteria.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/JoinOn.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/JoinUsing.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/LikePredicate.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/Literal.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/LogicalBinaryExpression.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/LongLiteral.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/NaturalJoin.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/Node.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/NotExpression.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/NullIfExpression.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/NullLiteral.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/PartitionElement.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/QualifiedName.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/QualifiedNameReference.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/Query.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/QueryBody.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/QuerySpecification.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/Relation.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/RenameColumn.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/RenameTable.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/ResetSession.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/Row.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/SampledRelation.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/SearchedCaseExpression.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/Select.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/SelectItem.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/SetOperation.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/SetSession.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/ShowCatalogs.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/ShowColumns.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/ShowFunctions.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/ShowPartitions.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/ShowSchemas.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/ShowSession.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/ShowTables.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/SimpleCaseExpression.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/SingleColumn.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/SortItem.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/Statement.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/StringLiteral.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/SubqueryExpression.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/SubscriptExpression.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/Table.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/TableElement.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/TableSubquery.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/TimeLiteral.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/TimestampLiteral.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/Union.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/Unnest.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/Use.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/Values.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/WhenClause.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/Window.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/WindowFrame.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/With.java create mode 100644 presto-parser/src/main/java/com/facebook/presto/sql/tree/WithQuery.java create mode 100644 presto-parser/src/test/java/com/facebook/presto/sql/parser/TestSqlParser.java create mode 100644 presto-parser/src/test/java/com/facebook/presto/sql/parser/TestStatementBuilder.java create mode 100644 presto-parser/src/test/java/com/facebook/presto/sql/parser/TestStatementSplitter.java create mode 100644 presto-parser/src/test/resources/tpch/queries/1.sql create mode 100644 presto-parser/src/test/resources/tpch/queries/10.sql create mode 100644 presto-parser/src/test/resources/tpch/queries/11.sql create mode 100644 presto-parser/src/test/resources/tpch/queries/12.sql create mode 100644 presto-parser/src/test/resources/tpch/queries/13.sql create mode 100644 presto-parser/src/test/resources/tpch/queries/14.sql create mode 100644 presto-parser/src/test/resources/tpch/queries/15.sql create mode 100644 presto-parser/src/test/resources/tpch/queries/16.sql create mode 100644 presto-parser/src/test/resources/tpch/queries/17.sql create mode 100644 presto-parser/src/test/resources/tpch/queries/18.sql create mode 100644 presto-parser/src/test/resources/tpch/queries/19.sql create mode 100644 presto-parser/src/test/resources/tpch/queries/2.sql create mode 100644 presto-parser/src/test/resources/tpch/queries/20.sql create mode 100644 presto-parser/src/test/resources/tpch/queries/21.sql create mode 100644 presto-parser/src/test/resources/tpch/queries/22.sql create mode 100644 presto-parser/src/test/resources/tpch/queries/3.sql create mode 100644 presto-parser/src/test/resources/tpch/queries/4.sql create mode 100644 presto-parser/src/test/resources/tpch/queries/5.sql create mode 100644 presto-parser/src/test/resources/tpch/queries/6.sql create mode 100644 presto-parser/src/test/resources/tpch/queries/7.sql create mode 100644 presto-parser/src/test/resources/tpch/queries/8.sql create mode 100644 presto-parser/src/test/resources/tpch/queries/9.sql create mode 100644 presto-postgresql/pom.xml create mode 100644 presto-postgresql/src/main/java/com/facebook/presto/plugin/postgresql/PostgreSqlClient.java create mode 100644 presto-postgresql/src/main/java/com/facebook/presto/plugin/postgresql/PostgreSqlClientModule.java create mode 100644 presto-postgresql/src/main/java/com/facebook/presto/plugin/postgresql/PostgreSqlPlugin.java create mode 100644 presto-postgresql/src/test/java/com/facebook/presto/plugin/postgresql/PostgreSqlQueryRunner.java create mode 100644 presto-postgresql/src/test/java/com/facebook/presto/plugin/postgresql/TestPostgreSqlDistributedQueries.java create mode 100644 presto-postgresql/src/test/java/com/facebook/presto/plugin/postgresql/TestPostgreSqlIntegrationSmokeTest.java create mode 100644 presto-postgresql/src/test/java/com/facebook/presto/plugin/postgresql/TestPostgreSqlPlugin.java create mode 100644 presto-raptor/pom.xml create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/PluginInfo.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/RaptorColumnHandle.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/RaptorConnector.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/RaptorConnectorFactory.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/RaptorConnectorId.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/RaptorErrorCode.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/RaptorHandleResolver.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/RaptorInsertTableHandle.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/RaptorMetadata.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/RaptorModule.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/RaptorOutputTableHandle.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/RaptorPageSink.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/RaptorPageSinkProvider.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/RaptorPageSourceProvider.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/RaptorPartition.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/RaptorPlugin.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/RaptorSplit.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/RaptorSplitManager.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/RaptorTableHandle.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/backup/BackupConfig.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/backup/BackupModule.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/backup/BackupStore.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/backup/FileBackupConfig.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/backup/FileBackupModule.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/backup/FileBackupStore.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/backup/TimeoutBackupStore.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/metadata/ColumnInfo.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/metadata/ColumnStats.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/metadata/DatabaseMetadataModule.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/metadata/DatabaseShardManager.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/metadata/ForMetadata.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/metadata/IndexInserter.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/metadata/MetadataDao.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/metadata/MetadataDaoUtils.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/metadata/Node.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/metadata/SchemaTableNameMapper.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/metadata/ShardDelta.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/metadata/ShardInfo.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/metadata/ShardIterator.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/metadata/ShardManager.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/metadata/ShardManagerDao.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/metadata/ShardManagerDaoUtils.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/metadata/ShardMetadata.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/metadata/ShardNode.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/metadata/ShardNodes.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/metadata/ShardPredicate.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/metadata/SqlUtils.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/metadata/Table.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/metadata/TableColumn.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/metadata/TableMetadata.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/metadata/ViewResult.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/storage/CompactionSetCreator.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/storage/FileStorageService.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/storage/ForStorageManager.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/storage/OrcFileRewriter.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/storage/OrcFileWriter.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/storage/OrcPageSource.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/storage/OrcStorageManager.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/storage/Row.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/storage/ShardCompactionManager.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/storage/ShardCompactor.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/storage/ShardRecoveryManager.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/storage/ShardRecoveryStats.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/storage/ShardRewriter.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/storage/ShardStats.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/storage/StorageManager.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/storage/StorageManagerConfig.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/storage/StorageManagerStats.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/storage/StorageModule.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/storage/StoragePageSink.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/storage/StorageService.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/storage/StorageType.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/util/ArrayUtil.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/util/CloseableIterator.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/util/Closer.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/util/ConditionalModule.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/util/CurrentNodeId.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/util/FileUtil.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/util/MetadataUtil.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/util/PageBuffer.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/util/PrioritizedFifoExecutor.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/util/RebindSafeMBeanServer.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/util/SyncingFileSystem.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/util/Types.java create mode 100644 presto-raptor/src/main/java/com/facebook/presto/raptor/util/UuidUtil.java create mode 100644 presto-raptor/src/test/java/com/facebook/presto/raptor/RaptorBenchmarkQueryRunner.java create mode 100644 presto-raptor/src/test/java/com/facebook/presto/raptor/RaptorQueryRunner.java create mode 100644 presto-raptor/src/test/java/com/facebook/presto/raptor/TestRaptorDistributedQueries.java create mode 100644 presto-raptor/src/test/java/com/facebook/presto/raptor/TestRaptorIntegrationSmokeTest.java create mode 100644 presto-raptor/src/test/java/com/facebook/presto/raptor/TestRaptorPlugin.java create mode 100644 presto-raptor/src/test/java/com/facebook/presto/raptor/backup/TestBackupConfig.java create mode 100644 presto-raptor/src/test/java/com/facebook/presto/raptor/backup/TestFileBackupConfig.java create mode 100644 presto-raptor/src/test/java/com/facebook/presto/raptor/backup/TestFileBackupStore.java create mode 100644 presto-raptor/src/test/java/com/facebook/presto/raptor/metadata/TestDatabaseShardManager.java create mode 100644 presto-raptor/src/test/java/com/facebook/presto/raptor/metadata/TestMetadataDao.java create mode 100644 presto-raptor/src/test/java/com/facebook/presto/raptor/metadata/TestRaptorMetadata.java create mode 100644 presto-raptor/src/test/java/com/facebook/presto/raptor/metadata/TestRaptorSplitManager.java create mode 100644 presto-raptor/src/test/java/com/facebook/presto/raptor/metadata/TestShardManagerDao.java create mode 100644 presto-raptor/src/test/java/com/facebook/presto/raptor/storage/OrcTestingUtil.java create mode 100644 presto-raptor/src/test/java/com/facebook/presto/raptor/storage/TestMissingShardComparator.java create mode 100644 presto-raptor/src/test/java/com/facebook/presto/raptor/storage/TestOrcFileRewriter.java create mode 100644 presto-raptor/src/test/java/com/facebook/presto/raptor/storage/TestOrcStorageManager.java create mode 100644 presto-raptor/src/test/java/com/facebook/presto/raptor/storage/TestShardCompactor.java create mode 100644 presto-raptor/src/test/java/com/facebook/presto/raptor/storage/TestShardRecovery.java create mode 100644 presto-raptor/src/test/java/com/facebook/presto/raptor/storage/TestShardWriter.java create mode 100644 presto-raptor/src/test/java/com/facebook/presto/raptor/storage/TestStorageManagerConfig.java create mode 100644 presto-raptor/src/test/java/com/facebook/presto/raptor/util/TestPrioritizedFifoExecutor.java create mode 100644 presto-server/NOTICE create mode 100644 presto-server/README.txt create mode 100644 presto-server/pom.xml create mode 100644 presto-server/src/main/provisio/presto.xml create mode 100644 presto-spi/pom.xml create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/BeginDeleteResult.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/ColumnHandle.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/ColumnMetadata.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/ColumnNotFoundException.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/Connector.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/ConnectorFactory.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/ConnectorHandleResolver.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/ConnectorIndex.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/ConnectorIndexHandle.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/ConnectorIndexResolver.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/ConnectorInsertTableHandle.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/ConnectorMetadata.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/ConnectorOutputTableHandle.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/ConnectorPageSink.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/ConnectorPageSinkProvider.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/ConnectorPageSource.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/ConnectorPageSourceProvider.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/ConnectorPartition.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/ConnectorPartitionResult.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/ConnectorRecordSetProvider.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/ConnectorRecordSinkProvider.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/ConnectorResolvedIndex.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/ConnectorSession.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/ConnectorSplit.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/ConnectorSplitManager.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/ConnectorSplitSource.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/ConnectorTableHandle.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/ConnectorTableLayout.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/ConnectorTableLayoutHandle.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/ConnectorTableLayoutResult.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/ConnectorTableMetadata.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/ConstantProperty.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/Constraint.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/CreateTableOption.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/Domain.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/ErrorCode.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/ErrorCodeSupplier.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/FixedPageSource.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/FixedSplitSource.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/GroupingProperty.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/HostAddress.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/InMemoryRecordSet.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/InsertOption.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/LocalProperty.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/Marker.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/Node.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/NodeManager.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/NotFoundException.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/Page.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/PageBuilder.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/PageSorter.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/Plugin.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/PrestoException.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/Range.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/ReadOnlyConnectorMetadata.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/RecordCursor.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/RecordPageSink.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/RecordPageSource.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/RecordSet.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/RecordSink.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/SchemaNotFoundException.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/SchemaTableName.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/SchemaTablePrefix.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/SchemaUtil.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/SerializableNativeValue.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/SortedRangeSet.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/SortingProperty.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/StandardErrorCode.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/SystemTable.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/TableNotFoundException.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/TupleDomain.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/UpdatablePageSource.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/ViewNotFoundException.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/block/AbstractFixedWidthBlock.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/block/AbstractVariableWidthBlock.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/block/Block.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/block/BlockBuilder.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/block/BlockBuilderStatus.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/block/BlockEncoding.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/block/BlockEncodingFactory.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/block/BlockEncodingSerde.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/block/EncoderUtil.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/block/FixedWidthBlock.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/block/FixedWidthBlockBuilder.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/block/FixedWidthBlockEncoding.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/block/LazyBlockLoader.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/block/LazyFixedWidthBlock.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/block/LazySliceArrayBlock.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/block/LazySliceArrayBlockEncoding.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/block/PageBuilderStatus.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/block/SliceArrayBlock.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/block/SliceArrayBlockEncoding.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/block/SortOrder.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/block/VariableWidthBlock.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/block/VariableWidthBlockBuilder.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/block/VariableWidthBlockEncoding.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/classloader/ClassLoaderSafeConnectorHandleResolver.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/classloader/ClassLoaderSafeConnectorIndexResolver.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/classloader/ClassLoaderSafeConnectorMetadata.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/classloader/ClassLoaderSafeConnectorPageSinkProvider.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/classloader/ClassLoaderSafeConnectorPageSourceProvider.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/classloader/ClassLoaderSafeConnectorRecordSetProvider.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/classloader/ClassLoaderSafeConnectorRecordSinkProvider.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/classloader/ClassLoaderSafeConnectorSplitManager.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/classloader/ClassLoaderSafeRecordSet.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/classloader/ThreadContextClassLoader.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/type/AbstractFixedWidthType.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/type/AbstractType.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/type/AbstractVariableWidthType.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/type/BigintType.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/type/BooleanType.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/type/DateTimeEncoding.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/type/DateType.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/type/DoubleType.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/type/FixedWidthType.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/type/HyperLogLogType.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/type/IntervalDayTimeType.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/type/IntervalYearMonthType.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/type/SqlDate.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/type/SqlIntervalDayTime.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/type/SqlIntervalYearMonth.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/type/SqlTime.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/type/SqlTimeWithTimeZone.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/type/SqlTimestamp.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/type/SqlTimestampWithTimeZone.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/type/SqlVarbinary.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/type/StandardTypes.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/type/TimeType.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/type/TimeWithTimeZoneType.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/type/TimeZoneIndex.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/type/TimeZoneKey.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/type/TimeZoneNotSupportedException.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/type/TimestampType.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/type/TimestampWithTimeZoneType.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/type/Type.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/type/TypeManager.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/type/TypeSerde.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/type/TypeSignature.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/type/VarbinaryType.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/type/VarcharType.java create mode 100644 presto-spi/src/main/java/com/facebook/presto/spi/type/VariableWidthType.java create mode 100644 presto-spi/src/main/resources/com/facebook/presto/spi/type/zone-index.properties create mode 100644 presto-spi/src/test/java/com/facebook/presto/spi/BenchmarkSortedRangeSet.java create mode 100644 presto-spi/src/test/java/com/facebook/presto/spi/TestDomain.java create mode 100644 presto-spi/src/test/java/com/facebook/presto/spi/TestMarker.java create mode 100644 presto-spi/src/test/java/com/facebook/presto/spi/TestPage.java create mode 100644 presto-spi/src/test/java/com/facebook/presto/spi/TestPrestoException.java create mode 100644 presto-spi/src/test/java/com/facebook/presto/spi/TestRange.java create mode 100644 presto-spi/src/test/java/com/facebook/presto/spi/TestSortedRangeSet.java create mode 100644 presto-spi/src/test/java/com/facebook/presto/spi/TestStandardErrorCode.java create mode 100644 presto-spi/src/test/java/com/facebook/presto/spi/TestTupleDomain.java create mode 100644 presto-spi/src/test/java/com/facebook/presto/spi/TestingColumnHandle.java create mode 100644 presto-spi/src/test/java/com/facebook/presto/spi/block/TestFixedWidthBlockBuilder.java create mode 100644 presto-spi/src/test/java/com/facebook/presto/spi/block/TestVariableWidthBlockBuilder.java create mode 100644 presto-spi/src/test/java/com/facebook/presto/spi/block/TestVariableWidthBlockEncoding.java create mode 100644 presto-spi/src/test/java/com/facebook/presto/spi/type/TestTimeZoneKey.java create mode 100644 presto-spi/src/test/java/com/facebook/presto/spi/type/TestTypeSerde.java create mode 100644 presto-spi/src/test/java/com/facebook/presto/spi/type/TestTypeSignature.java create mode 100644 presto-sqlserver/pom.xml create mode 100644 presto-sqlserver/src/main/java/com/facebook/presto/plugin/sqlserver/SqlServerClient.java create mode 100644 presto-sqlserver/src/main/java/com/facebook/presto/plugin/sqlserver/SqlServerModule.java create mode 100644 presto-sqlserver/src/main/java/com/facebook/presto/plugin/sqlserver/SqlServerPlugin.java create mode 100644 presto-tests/pom.xml create mode 100644 presto-tests/src/main/java/com/facebook/presto/tests/AbstractTestApproximateQueries.java create mode 100644 presto-tests/src/main/java/com/facebook/presto/tests/AbstractTestDistributedQueries.java create mode 100644 presto-tests/src/main/java/com/facebook/presto/tests/AbstractTestIndexedQueries.java create mode 100644 presto-tests/src/main/java/com/facebook/presto/tests/AbstractTestIntegrationSmokeTest.java create mode 100644 presto-tests/src/main/java/com/facebook/presto/tests/AbstractTestQueries.java create mode 100644 presto-tests/src/main/java/com/facebook/presto/tests/AbstractTestQueryFramework.java create mode 100644 presto-tests/src/main/java/com/facebook/presto/tests/AbstractTestingPrestoClient.java create mode 100644 presto-tests/src/main/java/com/facebook/presto/tests/CreateHll.java create mode 100644 presto-tests/src/main/java/com/facebook/presto/tests/CustomAdd.java create mode 100644 presto-tests/src/main/java/com/facebook/presto/tests/CustomRank.java create mode 100644 presto-tests/src/main/java/com/facebook/presto/tests/CustomSum.java create mode 100644 presto-tests/src/main/java/com/facebook/presto/tests/DistributedQueryRunner.java create mode 100644 presto-tests/src/main/java/com/facebook/presto/tests/H2QueryRunner.java create mode 100644 presto-tests/src/main/java/com/facebook/presto/tests/QueryAssertions.java create mode 100644 presto-tests/src/main/java/com/facebook/presto/tests/ResultsSession.java create mode 100644 presto-tests/src/main/java/com/facebook/presto/tests/StandaloneQueryRunner.java create mode 100644 presto-tests/src/main/java/com/facebook/presto/tests/TestingDiscoveryServer.java create mode 100644 presto-tests/src/main/java/com/facebook/presto/tests/TestingPrestoClient.java create mode 100644 presto-tests/src/main/java/com/facebook/presto/tests/tpch/AppendingRecordSet.java create mode 100644 presto-tests/src/main/java/com/facebook/presto/tests/tpch/ConcatRecordSet.java create mode 100644 presto-tests/src/main/java/com/facebook/presto/tests/tpch/ExampleSystemTable.java create mode 100644 presto-tests/src/main/java/com/facebook/presto/tests/tpch/IndexedTpchConnectorFactory.java create mode 100644 presto-tests/src/main/java/com/facebook/presto/tests/tpch/IndexedTpchPlugin.java create mode 100644 presto-tests/src/main/java/com/facebook/presto/tests/tpch/MaterializedTuple.java create mode 100644 presto-tests/src/main/java/com/facebook/presto/tests/tpch/MaterializedTupleRecordSet.java create mode 100644 presto-tests/src/main/java/com/facebook/presto/tests/tpch/TpchConnectorIndex.java create mode 100644 presto-tests/src/main/java/com/facebook/presto/tests/tpch/TpchIndexHandle.java create mode 100644 presto-tests/src/main/java/com/facebook/presto/tests/tpch/TpchIndexHandleResolver.java create mode 100644 presto-tests/src/main/java/com/facebook/presto/tests/tpch/TpchIndexResolver.java create mode 100644 presto-tests/src/main/java/com/facebook/presto/tests/tpch/TpchIndexSpec.java create mode 100644 presto-tests/src/main/java/com/facebook/presto/tests/tpch/TpchIndexedData.java create mode 100644 presto-tests/src/main/java/com/facebook/presto/tests/tpch/TpchScaledTable.java create mode 100644 presto-tests/src/test/java/com/facebook/presto/memory/TestMemoryManager.java create mode 100644 presto-tests/src/test/java/com/facebook/presto/tests/TestDistributedQueriesIndexed.java create mode 100644 presto-tests/src/test/java/com/facebook/presto/tests/TestLocalQueries.java create mode 100644 presto-tests/src/test/java/com/facebook/presto/tests/TestLocalQueriesIndexed.java create mode 100644 presto-tests/src/test/java/com/facebook/presto/tests/TestTpchDistributedQueries.java create mode 100644 presto-tpch/pom.xml create mode 100644 presto-tpch/src/main/java/com/facebook/presto/tpch/TpchColumnHandle.java create mode 100644 presto-tpch/src/main/java/com/facebook/presto/tpch/TpchConnectorFactory.java create mode 100644 presto-tpch/src/main/java/com/facebook/presto/tpch/TpchHandleResolver.java create mode 100644 presto-tpch/src/main/java/com/facebook/presto/tpch/TpchMetadata.java create mode 100644 presto-tpch/src/main/java/com/facebook/presto/tpch/TpchPlugin.java create mode 100644 presto-tpch/src/main/java/com/facebook/presto/tpch/TpchRecordSet.java create mode 100644 presto-tpch/src/main/java/com/facebook/presto/tpch/TpchRecordSetProvider.java create mode 100644 presto-tpch/src/main/java/com/facebook/presto/tpch/TpchSplit.java create mode 100644 presto-tpch/src/main/java/com/facebook/presto/tpch/TpchSplitManager.java create mode 100644 presto-tpch/src/main/java/com/facebook/presto/tpch/TpchTableHandle.java create mode 100644 presto-tpch/src/main/java/com/facebook/presto/tpch/TpchTableLayoutHandle.java create mode 100644 presto-tpch/src/main/java/com/facebook/presto/tpch/Types.java create mode 100644 presto-tpch/src/main/java/com/facebook/presto/tpch/testing/SampledTpchConnectorFactory.java create mode 100644 presto-tpch/src/main/java/com/facebook/presto/tpch/testing/SampledTpchMetadata.java create mode 100644 presto-tpch/src/main/java/com/facebook/presto/tpch/testing/SampledTpchPlugin.java create mode 100644 presto-tpch/src/main/java/com/facebook/presto/tpch/testing/SampledTpchRecordSetProvider.java create mode 100644 presto-tpch/src/main/resources/META-INF/services/com.facebook.presto.spi.Plugin create mode 100644 presto-verifier/pom.xml create mode 100644 presto-verifier/src/main/java/com/facebook/presto/verifier/ForwardingDriver.java create mode 100644 presto-verifier/src/main/java/com/facebook/presto/verifier/HumanReadableEventClient.java create mode 100644 presto-verifier/src/main/java/com/facebook/presto/verifier/JsonEventClient.java create mode 100644 presto-verifier/src/main/java/com/facebook/presto/verifier/PrestoVerifier.java create mode 100644 presto-verifier/src/main/java/com/facebook/presto/verifier/PrestoVerifierModule.java create mode 100644 presto-verifier/src/main/java/com/facebook/presto/verifier/Query.java create mode 100644 presto-verifier/src/main/java/com/facebook/presto/verifier/QueryPair.java create mode 100644 presto-verifier/src/main/java/com/facebook/presto/verifier/QueryPairMapper.java create mode 100644 presto-verifier/src/main/java/com/facebook/presto/verifier/QueryResult.java create mode 100644 presto-verifier/src/main/java/com/facebook/presto/verifier/TypesDoNotMatchException.java create mode 100644 presto-verifier/src/main/java/com/facebook/presto/verifier/Validator.java create mode 100644 presto-verifier/src/main/java/com/facebook/presto/verifier/Verifier.java create mode 100644 presto-verifier/src/main/java/com/facebook/presto/verifier/VerifierConfig.java create mode 100644 presto-verifier/src/main/java/com/facebook/presto/verifier/VerifierDao.java create mode 100644 presto-verifier/src/main/java/com/facebook/presto/verifier/VerifierException.java create mode 100644 presto-verifier/src/main/java/com/facebook/presto/verifier/VerifierQueryEvent.java create mode 100644 presto-verifier/src/test/java/com/facebook/presto/verifier/TestVerifierConfig.java create mode 100644 src/checkstyle/checks.xml create mode 100644 src/license/LICENSE-HEADER.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..5d69247e --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +*.iml +*.ipr +*.iws +target/ +/var +/*/var/ +pom.xml.versionsBackup +test-output/ +/atlassian-ide-plugin.xml +.idea +.DS_Store +.classpath +.settings +.project +temp-testng-customsuite.xml +test-output +.externalToolBuilders +*~ +benchmark_outputs +*.pyc +*.class +.checkstyle diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..36ee884d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,19 @@ +language: java + +jdk: + - oraclejdk8 + +env: + global: + - MAVEN_OPTS="-Xmx256M" + +sudo: false + +cache: + directories: + - $HOME/.m2/io + - $HOME/.m2/org + +install: mvn install -DskipTests=true -Dmaven.javadoc.skip=true -B -V -q -T 2 + +script: mvn test -Dair.check.skip-dependency=true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..003eab32 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,11 @@ +# Contributing to Presto + +## Contributor License Agreement ("CLA") + +In order to accept your pull request, we need you to submit a CLA. You only need to do this once, so if you've done this for another Facebook open source project, you're good to go. If you are submitting a pull request for the first time, just let us know that you have completed the CLA and we can cross-check with your GitHub username. + +Complete your CLA here: + +## License + +By contributing to Presto, you agree that your contributions will be licensed under the [Apache License Version 2.0 (APLv2)](LICENSE). diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 00000000..3c056eae --- /dev/null +++ b/README.md @@ -0,0 +1,73 @@ +# Presto + +Presto is a distributed SQL query engine for big data. + +See the [User Manual](https://prestodb.io/docs/current/) for deployment instructions and end user documentation. + +## Requirements + +* Mac OS X or Linux +* Java 8, 64-bit +* Maven 3.2.3+ (for building) +* Python 2.4+ (for running with the launcher script) + +## Building Presto + +Presto is a standard Maven project. Simply run the following command from the project root directory: + + mvn clean install + +On the first build, Maven will download all the dependencies from the internet and cache them in the local repository (`~/.m2/repository`), which can take a considerable amount of time. Subsequent builds will be faster. + +Presto has a comprehensive set of unit tests that can take several minutes to run. You can disable the tests when building: + + mvn clean install -DskipTests + +## Running Presto in your IDE + +### Overview + +After building Presto for the first time, you can load the project into your IDE and run the server. We recommend using [IntelliJ IDEA](http://www.jetbrains.com/idea/). Because Presto is a standard Maven project, you can import it into your IDE using the root `pom.xml` file. In IntelliJ, choose Open Project from the Quick Start box or choose Open from the File menu and select the root `pom.xml` file. + +After opening the project in IntelliJ, double check that the Java SDK is properly configured properly for the project: + +* Open the File menu and select Project Structure +* In the SDKs section, ensure that a 1.8 JDK is selected (create one if none exist) +* In the Project section, ensure the Project language level is set to 8.0 as Presto makes use of several Java 8 language features + +Presto comes with sample configuration that should work out-of-the-box for development. Use the following options to create a run configuration: + +* Main Class: `com.facebook.presto.server.PrestoServer` +* VM Options: `-ea -Xmx2G -Dconfig=etc/config.properties -Dlog.levels-file=etc/log.properties` +* Working directory: `$MODULE_DIR$` +* Use classpath of module: `presto-main` + +The working directory should be the `presto-main` subdirectory. In IntelliJ, using `$MODULE_DIR$` accomplishes this automatically. + +Additionally, the Hive plugin must be configured with location of your Hive metastore Thrift service. Add the following to the list of VM options, replacing `localhost:9083` with the correct host and port (or use the below value if you do not have a Hive metastore): + + -Dhive.metastore.uri=thrift://localhost:9083 + +### Using SOCKS for Hive or HDFS + +If your Hive metastore or HDFS cluster is not directly accessible to your local machine, you can use SSH port forwarding to access it. Setup a dynamic SOCKS proxy with SSH listening on local port 1080: + + ssh -v -N -D 1080 server + +Then add the following to the list of VM options: + + -Dhive.metastore.thrift.client.socks-proxy=localhost:1080 + +### Running the CLI + +Start the CLI to connect to the server and run SQL queries: + + presto-cli/target/presto-cli-*-executable.jar + +Run a query to see the nodes in the cluster: + + SELECT * FROM system.runtime.nodes; + +In the sample configuration, the Hive connector is mounted in the `hive` catalog, so you can run the following queries to show the tables in the Hive database `default`: + + SHOW TABLES FROM hive.default; diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..3a0951ec --- /dev/null +++ b/pom.xml @@ -0,0 +1,916 @@ + + + 4.0.0 + + + io.airlift + airbase + 39 + + + com.facebook.presto + presto-root + 0.107 + pom + + presto-root + Presto + https://github.com/facebook/presto + + 2012 + + + + Apache License 2.0 + http://www.apache.org/licenses/LICENSE-2.0 + repo + + + + + scm:git:git://github.com/facebook/presto.git + https://github.com/facebook/presto + 0.107 + + + + ${project.basedir} + + true + false + + true + false + + 4.3 + 0.110 + ${dep.airlift.version} + 0.14 + + true + None + + + Asia/Katmandu + methods + 4 + 1792m + + -missing + + + + presto-spi + presto-kafka + presto-cassandra + presto-orc + presto-hive + presto-hive-hadoop1 + presto-hive-hadoop2 + presto-hive-cdh4 + presto-hive-cdh5 + presto-example-http + presto-tpch + presto-raptor + presto-base-jdbc + presto-mysql + presto-sqlserver + presto-postgresql + presto-client + presto-parser + presto-main + presto-ml + presto-benchmark + presto-tests + presto-jdbc + presto-cli + presto-benchmark-driver + presto-server + presto-docs + presto-verifier + presto-oracle + + + + + + com.facebook.presto + presto-spi + ${project.version} + + + + com.facebook.presto + presto-orc + ${project.version} + + + + com.facebook.presto + presto-hive + ${project.version} + + + + com.facebook.presto + presto-hive-cdh4 + ${project.version} + zip + + + + com.facebook.presto + presto-example-http + ${project.version} + zip + + + + com.facebook.presto + presto-hive + ${project.version} + test-jar + + + + com.facebook.presto + presto-tpch + ${project.version} + + + + com.facebook.presto + presto-base-jdbc + ${project.version} + + + + com.facebook.presto + presto-mysql + ${project.version} + + + + com.facebook.presto + presto-sqlserver + ${project.version} + + + + com.facebook.presto + presto-raptor + ${project.version} + + + + com.facebook.presto + presto-cli + ${project.version} + + + + com.facebook.presto + presto-client + ${project.version} + + + + com.facebook.presto + presto-parser + ${project.version} + + + + com.facebook.presto + presto-parser + ${project.version} + test-jar + + + + com.facebook.presto + presto-main + ${project.version} + + + + com.facebook.presto + presto-main + ${project.version} + test-jar + + + + com.facebook.presto + presto-jdbc + ${project.version} + + + + com.facebook.presto + presto-server + ${project.version} + + + + com.facebook.presto + presto-tests + ${project.version} + + + + com.facebook.presto + presto-benchmark + ${project.version} + + + + com.facebook.presto.hadoop + hadoop-apache1 + 0.2 + + + + com.facebook.presto.hadoop + hadoop-apache2 + 0.1 + + + + com.facebook.presto.hadoop + hadoop-cdh4 + 0.8 + + + + com.facebook.presto.hive + hive-apache + 0.14 + + + + com.facebook.hive + hive-dwrf + 0.8 + + + commons-logging + commons-logging + + + org.iq80.snappy + snappy + + + com.facebook.presto.hadoop + hadoop-cdh4 + + + it.unimi.dsi + fastutil + + + + + + com.facebook.hive + hive-dwrf-shims + 0.8 + + + commons-logging + commons-logging + + + + + + com.google.protobuf + protobuf-java + 2.4.1 + + + + io.airlift + log + ${dep.airlift.version} + + + + io.airlift + log-manager + ${dep.airlift.version} + + + + io.airlift + json + ${dep.airlift.version} + + + + io.airlift + units + ${dep.airlift.version} + + + + io.airlift + concurrent + ${dep.airlift.version} + + + + io.airlift + configuration + ${dep.airlift.version} + + + + io.airlift + discovery + ${dep.airlift.version} + + + + io.airlift + testing + ${dep.airlift.version} + + + + io.airlift + node + ${dep.airlift.version} + + + + io.airlift + bootstrap + ${dep.airlift.version} + + + + io.airlift + event + ${dep.airlift.version} + + + + io.airlift + http-server + ${dep.airlift.version} + + + + io.airlift + jaxrs + ${dep.airlift.version} + + + + io.airlift + jmx + ${dep.airlift.version} + + + + io.airlift + trace-token + ${dep.airlift.version} + + + + io.airlift + dbpool + ${dep.airlift.version} + + + + io.airlift + jmx-http + ${dep.airlift.version} + + + + io.airlift + http-client + ${dep.airlift.version} + + + + io.airlift + stats + ${dep.airlift.version} + + + + io.airlift + joni + 2.1.5.1 + + + + io.airlift.tpch + tpch + 0.4 + + + + org.ow2.asm + asm-all + 4.1 + + + + com.h2database + h2 + 1.3.170 + + + + org.sonatype.aether + aether-api + 1.13.1 + + + + io.airlift.resolver + resolver + 1.1 + + + + io.airlift + airline + 0.6 + + + + org.iq80.snappy + snappy + 0.3 + + + + org.openjdk.jol + jol-core + 0.2 + + + + org.jetbrains + annotations + 13.0 + + + + it.unimi.dsi + fastutil + 6.5.9 + + + + com.facebook.thirdparty + libsvm + 3.18.1 + + + + mysql + mysql-connector-java + 5.1.35 + + + + org.postgresql + postgresql + 9.3-1102-jdbc41 + + + + org.antlr + antlr4-runtime + ${dep.antlr.version} + + + + org.antlr + antlr4-annotations + ${dep.antlr.version} + + + + jline + jline + 2.12 + + + + org.jdbi + jdbi + 2.55 + + + + org.apache.thrift + libthrift + 0.9.1 + + + org.apache.commons + commons-lang3 + + + org.apache.httpcomponents + httpcore + + + org.apache.httpcomponents + httpclient + + + + + + net.sf.opencsv + opencsv + 2.3 + + + + org.apache.commons + commons-math3 + 3.2 + + + + commons-codec + commons-codec + 1.9 + + + + io.netty + netty + 3.7.0.Final + + + + io.airlift.discovery + discovery-server + 1.24 + + + + com.amazonaws + aws-java-sdk + 1.8.9.1 + + + commons-logging + commons-logging + + + + + + io.airlift + testing-mysql-server + 0.1 + + + + io.airlift + testing-postgresql-server + 0.3 + + + + org.apache.kafka + kafka_2.10 + 0.8.1.1 + + + log4j + log4j + + + org.slf4j + slf4j-jdk14 + + + + + + org.xerial.snappy + snappy-java + 1.1.1.3 + + + + org.apache.zookeeper + zookeeper + 3.3.6 + + + junit + junit + + + log4j + log4j + + + + + + com.101tec + zkclient + 0.4 + + + log4j + log4j + + + + + + org.jgrapht + jgrapht-core + 0.9.0 + + + + + + + + + org.antlr + antlr4-maven-plugin + ${dep.antlr.version} + + + + antlr4 + + + + + true + + + + + org.apache.maven.plugins + maven-shade-plugin + 2.3 + + + + org.skife.maven + really-executable-jar-maven-plugin + 1.0.5 + + + + org.codehaus.mojo + exec-maven-plugin + 1.2.1 + + + + org.tomdz.maven + sphinx-maven-plugin + 1.0.3 + + + + + + org.eclipse.m2e + + lifecycle-mapping + + 1.0.0 + + + + + + org.apache.maven.plugins + maven-dependency-plugin + [2.5.1,) + + copy + analyze-dep-mgt + analyze-duplicate + analyze-only + + + + + + + + + org.jacoco + jacoco-maven-plugin + [0.6.2.201302030002,) + + prepare-agent + + + + + + + + + com.mycila + license-maven-plugin + [2.3,) + + check + + + + + + + + + com.ning.maven.plugins + maven-duplicate-finder-plugin + [1.0.4,) + + check + + + + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + [0,) + + check + + + + + + + + + io.takari.maven.plugins + presto-maven-plugin + [0,) + + generate-service-descriptor + + + + + + + + + io.takari.maven.plugins + takari-lifecycle-plugin + [0,) + + compile + process-resources + process-test-resources + testCompile + + + + + + + + + org.gaul + modernizer-maven-plugin + [0,) + + modernizer + + + + + + + + + + + + + + + + org.gaul + modernizer-maven-plugin + 1.2.2 + + 1.8 + false + + + + modernizer + + modernizer + + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 2.15 + + + validate + + check + + + ${air.check.skip-checkstyle} + ${air.check.fail-checkstyle} + true + true + ${air.main.basedir}/src/checkstyle/checks.xml + **/com/facebook/presto/operator/PagesIndexOrdering.java + + + + + + + com.puppycrawl.tools + checkstyle + 6.6 + + + + + + io.takari.maven.plugins + presto-maven-plugin + 0.1.5 + true + + + + io.takari.maven.plugins + provisio-maven-plugin + 0.1.11 + true + + + + org.apache.maven.plugins + maven-compiler-plugin + + false + + + + + + + + + cli + + + + org.codehaus.mojo + exec-maven-plugin + + ${cli.skip-execute} + ${java.home}/bin/java + ${cli.main-class} + + --debug + + + + + + + + \ No newline at end of file diff --git a/presto-base-jdbc/pom.xml b/presto-base-jdbc/pom.xml new file mode 100644 index 00000000..31baee02 --- /dev/null +++ b/presto-base-jdbc/pom.xml @@ -0,0 +1,201 @@ + + + 4.0.0 + + + com.facebook.presto + presto-root + 0.107 + + + presto-base-jdbc + presto-base-jdbc + Presto - Base JDBC Connector + + + ${project.parent.basedir} + + + + + io.airlift + bootstrap + + + + io.airlift + log + + + + io.airlift + configuration + + + + io.airlift + concurrent + + + + com.google.guava + guava + + + + com.google.inject + guice + + + + javax.validation + validation-api + + + + com.google.code.findbugs + annotations + + + + joda-time + joda-time + + + + + com.facebook.presto + presto-spi + provided + + + + io.airlift + slice + provided + + + + javax.inject + javax.inject + provided + + + + com.fasterxml.jackson.core + jackson-annotations + provided + + + + com.fasterxml.jackson.core + jackson-core + provided + + + + com.fasterxml.jackson.core + jackson-databind + provided + + + + mysql + mysql-connector-java + provided + + + + commons-dbutils + commons-dbutils + 1.6 + + + + com.facebook.presto + presto-main + provided + + + + io.airlift + units + provided + + + + + org.testng + testng + test + + + + io.airlift + testing + test + + + + com.h2database + h2 + test + + + + io.airlift + json + test + + + + com.facebook.presto + presto-tpch + test + + + + io.airlift.tpch + tpch + test + + + + com.facebook.presto + presto-tests + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + **/TestJdbcDistributedQueries.java + + + + + + + + + ci + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + + + + + + diff --git a/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/BaseJdbcClient.java b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/BaseJdbcClient.java new file mode 100644 index 00000000..eccce0fc --- /dev/null +++ b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/BaseJdbcClient.java @@ -0,0 +1,540 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc; + +import com.facebook.presto.plugin.jdbc.cache.JdbcCacheConfig; +import com.facebook.presto.plugin.jdbc.cache.JdbcCacheSplit; +import com.facebook.presto.plugin.jdbc.cache.JdbcJavaBean; +import com.facebook.presto.plugin.jdbc.cache.JdbcResultCache; +import com.facebook.presto.plugin.jdbc.subtable.JdbcSubTableConfig; +import com.facebook.presto.plugin.jdbc.subtable.JdbcSubTableManager; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ColumnMetadata; +import com.facebook.presto.spi.ConnectorPartition; +import com.facebook.presto.spi.ConnectorPartitionResult; +import com.facebook.presto.spi.ConnectorSplitSource; +import com.facebook.presto.spi.ConnectorTableMetadata; +import com.facebook.presto.spi.FixedSplitSource; +import com.facebook.presto.spi.HostAddress; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.spi.TableNotFoundException; +import com.facebook.presto.spi.TupleDomain; +import com.facebook.presto.spi.type.Type; +import com.google.common.base.Joiner; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import io.airlift.log.Logger; +import io.airlift.slice.Slice; + +import javax.annotation.Nullable; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.Driver; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Types; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.UUID; + +import static com.facebook.presto.spi.StandardErrorCode.NOT_FOUND; +import static com.facebook.presto.spi.StandardErrorCode.NOT_SUPPORTED; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.BooleanType.BOOLEAN; +import static com.facebook.presto.spi.type.DateType.DATE; +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; +import static com.facebook.presto.spi.type.TimeType.TIME; +import static com.facebook.presto.spi.type.TimeWithTimeZoneType.TIME_WITH_TIME_ZONE; +import static com.facebook.presto.spi.type.TimestampType.TIMESTAMP; +import static com.facebook.presto.spi.type.TimestampWithTimeZoneType.TIMESTAMP_WITH_TIME_ZONE; +import static com.facebook.presto.spi.type.VarbinaryType.VARBINARY; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Strings.isNullOrEmpty; +import static com.google.common.collect.Iterables.getOnlyElement; +import static com.google.common.collect.Maps.fromProperties; +import static java.util.Collections.nCopies; +import static java.util.Locale.ENGLISH; + +public class BaseJdbcClient + implements JdbcClient +{ + private static final Logger log = Logger.get(BaseJdbcClient.class); + public static final int TYPE_MYSQL = 1; + public static final int TYPE_ORACLE = 2; + public static final int TYPE_SQLSERVER = 3; + + private static final Map SQL_TYPES = ImmutableMap.builder() + .put(BOOLEAN, "boolean") + .put(BIGINT, "bigint") + .put(DOUBLE, "double precision") + .put(VARCHAR, "varchar") + .put(VARBINARY, "varbinary") + .put(DATE, "date") + .put(TIME, "time") + .put(TIME_WITH_TIME_ZONE, "time with timezone") + .put(TIMESTAMP, "timestamp") + .put(TIMESTAMP_WITH_TIME_ZONE, "timestamp with timezone") + .build(); + + protected final String connectorId; + protected final Driver driver; + protected final String connectionUrl; + protected final Properties connectionProperties; + protected final String identifierQuote; + protected int dbType; + + protected final boolean jdbcSubTableEnable; + private JdbcSubTableManager subTableManager; + + protected final boolean cacheEnable; + private JdbcResultCache jdbcResultCache; + + public BaseJdbcClient(JdbcConnectorId connectorId, + BaseJdbcConfig config, + String identifierQuote, + Driver driver, + JdbcSubTableConfig subTableConfig, + JdbcCacheConfig cacheConfig) + { + this.connectorId = checkNotNull(connectorId, "connectorId is null").toString(); + this.identifierQuote = checkNotNull(identifierQuote, "identifierQuote is null"); + this.driver = checkNotNull(driver, "driver is null"); + + checkNotNull(config, "config is null"); + connectionUrl = config.getConnectionUrl(); + + connectionProperties = new Properties(); + if (config.getConnectionUser() != null) { + connectionProperties.setProperty("user", config.getConnectionUser()); + } + if (config.getConnectionPassword() != null) { + connectionProperties.setProperty("password", config.getConnectionPassword()); + } + + // sub table + jdbcSubTableEnable = subTableConfig.getJdbcSubTableEnable(); + if (jdbcSubTableEnable) { + this.subTableManager = new JdbcSubTableManager(this.connectorId, identifierQuote, + driver, connectionUrl, connectionProperties, subTableConfig); + } + // jdbc cache + cacheEnable = cacheConfig.getJdbcCacheEnable(); + if (cacheEnable) { + this.jdbcResultCache = new JdbcResultCache(identifierQuote, driver, connectionProperties, cacheConfig); + } + } + + @Override + public Set getSchemaNames() + { + try (Connection connection = driver.connect(connectionUrl, connectionProperties); + ResultSet resultSet = connection.getMetaData().getSchemas()) { + ImmutableSet.Builder schemaNames = ImmutableSet.builder(); + while (resultSet.next()) { + String schemaName = resultSet.getString("TABLE_SCHEM").toLowerCase(ENGLISH); + // skip internal schemas + if (!schemaName.equals("information_schema")) { + schemaNames.add(schemaName); + } + } + return schemaNames.build(); + } + catch (SQLException e) { + throw Throwables.propagate(e); + } + } + + @Override + public List getTableNames(@Nullable String schema) + { + try (Connection connection = driver.connect(connectionUrl, connectionProperties)) { + DatabaseMetaData metadata = connection.getMetaData(); + if (metadata.storesUpperCaseIdentifiers() && (schema != null)) { + schema = schema.toUpperCase(ENGLISH); + } + try (ResultSet resultSet = getTables(connection, schema, null)) { + ImmutableList.Builder list = ImmutableList.builder(); + while (resultSet.next()) { + list.add(getSchemaTableName(resultSet)); + } + return list.build(); + } + } + catch (SQLException e) { + throw Throwables.propagate(e); + } + } + + @Nullable + @Override + public JdbcTableHandle getTableHandle(SchemaTableName schemaTableName) + { + try (Connection connection = driver.connect(connectionUrl, connectionProperties)) { + DatabaseMetaData metadata = connection.getMetaData(); + String jdbcSchemaName = schemaTableName.getSchemaName(); + String jdbcTableName = schemaTableName.getTableName(); + if (metadata.storesUpperCaseIdentifiers()) { + jdbcSchemaName = jdbcSchemaName.toUpperCase(ENGLISH); + jdbcTableName = jdbcTableName.toUpperCase(ENGLISH); + } + try (ResultSet resultSet = getTables(connection, jdbcSchemaName, jdbcTableName)) { + List tableHandles = new ArrayList<>(); + while (resultSet.next()) { + tableHandles.add(new JdbcTableHandle( + connectorId, + schemaTableName, + resultSet.getString("TABLE_CAT"), + resultSet.getString("TABLE_SCHEM"), + resultSet.getString("TABLE_NAME"))); + } + if (tableHandles.isEmpty()) { + return null; + } + if (tableHandles.size() > 1) { + throw new PrestoException(NOT_SUPPORTED, "Multiple tables matched: " + schemaTableName); + } + return getOnlyElement(tableHandles); + } + } + catch (SQLException e) { + throw Throwables.propagate(e); + } + } + + @Override + public List getColumns(JdbcTableHandle tableHandle) + { + try (Connection connection = driver.connect(connectionUrl, connectionProperties)) { + DatabaseMetaData metadata = connection.getMetaData(); + try (ResultSet resultSet = metadata.getColumns(tableHandle.getCatalogName(), tableHandle.getSchemaName(), tableHandle.getTableName(), null)) { + List columns = new ArrayList<>(); + boolean found = false; + while (resultSet.next()) { + found = true; + Type columnType = toPrestoType(resultSet.getInt("DATA_TYPE")); + // skip unsupported column types + if (columnType != null) { + String columnName = resultSet.getString("COLUMN_NAME"); + columns.add(new JdbcColumnHandle(connectorId, columnName, columnType)); + } + } + if (!found) { + throw new TableNotFoundException(tableHandle.getSchemaTableName()); + } + if (columns.isEmpty()) { + throw new PrestoException(NOT_SUPPORTED, "Table has no supported column types: " + tableHandle.getSchemaTableName()); + } + return ImmutableList.copyOf(columns); + } + } + catch (SQLException e) { + throw Throwables.propagate(e); + } + } + + @Override + public ConnectorPartitionResult getPartitions(JdbcTableHandle jdbcTableHandle, TupleDomain tupleDomain) + { + // currently we don't support partitions + return new ConnectorPartitionResult( + ImmutableList.of(new JdbcPartition(jdbcTableHandle, tupleDomain)), + tupleDomain); + } + + @Override + public ConnectorSplitSource getPartitionSplits(JdbcPartition jdbcPartition) + { + if (jdbcSubTableEnable) { + return subTableManager.getTableSplits(jdbcPartition); + } + JdbcTableHandle jdbcTableHandle = jdbcPartition.getJdbcTableHandle(); + List of = ImmutableList.of(); + JdbcSplit jdbcSplit = new JdbcSplit( + connectorId, + jdbcTableHandle.getCatalogName(), + jdbcTableHandle.getSchemaName(), + jdbcTableHandle.getTableName(), + connectionUrl, + fromProperties(connectionProperties), + jdbcPartition.getTupleDomain(), + "", of, true, "", "", "", "", System.nanoTime(), 1, false, ""); + return new FixedSplitSource(connectorId, ImmutableList.of(jdbcSplit)); + } + + @Override + public Connection getConnection(JdbcSplit split) + throws SQLException + { + Connection connection = driver.connect(split.getConnectionUrl(), toProperties(split.getConnectionProperties())); + try { + connection.setReadOnly(true); + } + catch (SQLException e) { + connection.close(); + throw e; + } + return connection; + } + + @Override + public String buildSql(JdbcSplit split, List columnHandles) + { + return new QueryBuilder(identifierQuote).buildSql( + dbType, + split.getCatalogName(), + split.getSchemaName(), + split.getTableName(), + columnHandles, + split.getTupleDomain()); + } + + @Override + public JdbcOutputTableHandle beginCreateTable(ConnectorTableMetadata tableMetadata) + { + SchemaTableName schemaTableName = tableMetadata.getTable(); + String schema = schemaTableName.getSchemaName(); + String table = schemaTableName.getTableName(); + + if (!getSchemaNames().contains(schema)) { + throw new PrestoException(NOT_FOUND, "Schema not found: " + schema); + } + + try (Connection connection = driver.connect(connectionUrl, connectionProperties)) { + boolean uppercase = connection.getMetaData().storesUpperCaseIdentifiers(); + if (uppercase) { + schema = schema.toUpperCase(ENGLISH); + table = table.toUpperCase(ENGLISH); + } + String catalog = connection.getCatalog(); + + String temporaryName = "tmp_presto_" + UUID.randomUUID().toString().replace("-", ""); + StringBuilder sql = new StringBuilder() + .append("CREATE TABLE ") + .append(quoted(catalog, schema, temporaryName)) + .append(" ("); + ImmutableList.Builder columnNames = ImmutableList.builder(); + ImmutableList.Builder columnTypes = ImmutableList.builder(); + ImmutableList.Builder columnList = ImmutableList.builder(); + for (ColumnMetadata column : tableMetadata.getColumns()) { + String columnName = column.getName(); + if (uppercase) { + columnName = columnName.toUpperCase(ENGLISH); + } + columnNames.add(columnName); + columnTypes.add(column.getType()); + columnList.add(new StringBuilder() + .append(quoted(columnName)) + .append(" ") + .append(toSqlType(column.getType())) + .toString()); + } + Joiner.on(", ").appendTo(sql, columnList.build()); + sql.append(")"); + + execute(connection, sql.toString()); + + return new JdbcOutputTableHandle( + connectorId, + catalog, + schema, + table, + columnNames.build(), + columnTypes.build(), + tableMetadata.getOwner(), + temporaryName, + connectionUrl, + fromProperties(connectionProperties)); + } + catch (SQLException e) { + throw Throwables.propagate(e); + } + } + + @Override + public void commitCreateTable(JdbcOutputTableHandle handle, Collection fragments) + { + StringBuilder sql = new StringBuilder() + .append("ALTER TABLE ") + .append(quoted(handle.getCatalogName(), handle.getSchemaName(), handle.getTemporaryTableName())) + .append(" RENAME TO ") + .append(quoted(handle.getCatalogName(), handle.getSchemaName(), handle.getTableName())); + + try (Connection connection = getConnection(handle)) { + execute(connection, sql.toString()); + } + catch (SQLException e) { + throw Throwables.propagate(e); + } + } + + @Override + public void dropTable(JdbcTableHandle handle) + { + StringBuilder sql = new StringBuilder() + .append("DROP TABLE ") + .append(quoted(handle.getCatalogName(), handle.getSchemaName(), handle.getTableName())); + + try (Connection connection = driver.connect(connectionUrl, connectionProperties)) { + execute(connection, sql.toString()); + } + catch (SQLException e) { + throw Throwables.propagate(e); + } + } + + @Override + public String buildInsertSql(JdbcOutputTableHandle handle) + { + String vars = Joiner.on(',').join(nCopies(handle.getColumnNames().size(), "?")); + return new StringBuilder() + .append("INSERT INTO ") + .append(quoted(handle.getCatalogName(), handle.getSchemaName(), handle.getTemporaryTableName())) + .append(" VALUES (").append(vars).append(")") + .toString(); + } + + @Override + public Connection getConnection(JdbcOutputTableHandle handle) + throws SQLException + { + return driver.connect(handle.getConnectionUrl(), toProperties(handle.getConnectionProperties())); + } + + protected ResultSet getTables(Connection connection, String schemaName, String tableName) + throws SQLException + { + return connection.getMetaData().getTables(connection.getCatalog(), schemaName, tableName, new String[] {"TABLE", "VIEW"}); + } + + protected SchemaTableName getSchemaTableName(ResultSet resultSet) + throws SQLException + { + return new SchemaTableName( + resultSet.getString("TABLE_SCHEM").toLowerCase(ENGLISH), + resultSet.getString("TABLE_NAME").toLowerCase(ENGLISH)); + } + + protected void execute(Connection connection, String query) + throws SQLException + { + try (Statement statement = connection.createStatement()) { + log.debug("Execute: %s", query); + statement.execute(query); + } + } + + protected Type toPrestoType(int jdbcType) + { + switch (jdbcType) { + case Types.BIT: + case Types.BOOLEAN: + return BOOLEAN; + case Types.TINYINT: + case Types.SMALLINT: + case Types.INTEGER: + case Types.BIGINT: + return BIGINT; + case Types.FLOAT: + case Types.REAL: + case Types.DOUBLE: + case Types.NUMERIC: + case Types.DECIMAL: + return DOUBLE; + case Types.CHAR: + case Types.NCHAR: + case Types.VARCHAR: + case Types.NVARCHAR: + case Types.LONGVARCHAR: + case Types.LONGNVARCHAR: + return VARCHAR; + case Types.BINARY: + case Types.VARBINARY: + case Types.LONGVARBINARY: + return VARBINARY; + case Types.DATE: + return DATE; + case Types.TIME: + return TIME; + case Types.TIMESTAMP: + return TIMESTAMP; + } + return null; + } + + protected String toSqlType(Type type) + { + String sqlType = SQL_TYPES.get(type); + if (sqlType != null) { + return sqlType; + } + throw new PrestoException(NOT_SUPPORTED, "Unsuported column type: " + type.getTypeSignature()); + } + + protected String quoted(String name) + { + name = name.replace(identifierQuote, identifierQuote + identifierQuote); + return identifierQuote + name + identifierQuote; + } + + protected String quoted(String catalog, String schema, String table) + { + StringBuilder sb = new StringBuilder(); + if (!isNullOrEmpty(catalog)) { + sb.append(quoted(catalog)).append("."); + } + if (!isNullOrEmpty(schema)) { + sb.append(quoted(schema)).append("."); + } + sb.append(quoted(table)); + return sb.toString(); + } + + private static Properties toProperties(Map map) + { + Properties properties = new Properties(); + for (Map.Entry entry : map.entrySet()) { + properties.setProperty(entry.getKey(), entry.getValue()); + } + return properties; + } + + public synchronized List getTableDataSet(JdbcCacheSplit key) + { + return jdbcResultCache.getResult(key); + } + + public boolean isCacheTable(String tableName) + { + return cacheEnable && jdbcResultCache != null && jdbcResultCache.isCacheTable(tableName); + } + + public void commitPdboLogs(JdbcSplit split, long rowCount) + { + this.subTableManager.commitPdboLogs(split, rowCount); + } + + public int getDb_type() + { + return dbType; + } +} diff --git a/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/BaseJdbcConfig.java b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/BaseJdbcConfig.java new file mode 100644 index 00000000..49b08005 --- /dev/null +++ b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/BaseJdbcConfig.java @@ -0,0 +1,62 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc; + +import io.airlift.configuration.Config; + +import javax.validation.constraints.NotNull; + +public class BaseJdbcConfig +{ + private String connectionUrl; + private String connectionUser; + private String connectionPassword; + + @NotNull + public String getConnectionUrl() + { + return connectionUrl; + } + + @Config("connection-url") + public BaseJdbcConfig setConnectionUrl(String connectionUrl) + { + this.connectionUrl = connectionUrl; + return this; + } + + public String getConnectionUser() + { + return connectionUser; + } + + @Config("connection-user") + public BaseJdbcConfig setConnectionUser(String connectionUser) + { + this.connectionUser = connectionUser; + return this; + } + + public String getConnectionPassword() + { + return connectionPassword; + } + + @Config("connection-password") + public BaseJdbcConfig setConnectionPassword(String connectionPassword) + { + this.connectionPassword = connectionPassword; + return this; + } +} diff --git a/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcClient.java b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcClient.java new file mode 100644 index 00000000..d409a140 --- /dev/null +++ b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcClient.java @@ -0,0 +1,62 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc; + +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorPartitionResult; +import com.facebook.presto.spi.ConnectorSplitSource; +import com.facebook.presto.spi.ConnectorTableMetadata; +import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.spi.TupleDomain; +import io.airlift.slice.Slice; + +import javax.annotation.Nullable; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +public interface JdbcClient +{ + Set getSchemaNames(); + + List getTableNames(@Nullable String schema); + + @Nullable + JdbcTableHandle getTableHandle(SchemaTableName schemaTableName); + + List getColumns(JdbcTableHandle tableHandle); + + ConnectorPartitionResult getPartitions(JdbcTableHandle jdbcTableHandle, TupleDomain tupleDomain); + + ConnectorSplitSource getPartitionSplits(JdbcPartition jdbcPartition); + + Connection getConnection(JdbcSplit split) + throws SQLException; + + String buildSql(JdbcSplit split, List columnHandles); + + JdbcOutputTableHandle beginCreateTable(ConnectorTableMetadata tableMetadata); + + void commitCreateTable(JdbcOutputTableHandle handle, Collection fragments); + + void dropTable(JdbcTableHandle jdbcTableHandle); + + String buildInsertSql(JdbcOutputTableHandle handle); + + Connection getConnection(JdbcOutputTableHandle handle) + throws SQLException; +} diff --git a/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcColumnHandle.java b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcColumnHandle.java new file mode 100644 index 00000000..88c8c5c8 --- /dev/null +++ b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcColumnHandle.java @@ -0,0 +1,97 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc; + +import com.facebook.presto.spi.ColumnMetadata; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.type.Type; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkNotNull; + +public final class JdbcColumnHandle + implements ColumnHandle +{ + private final String connectorId; + private final String columnName; + private final Type columnType; + + @JsonCreator + public JdbcColumnHandle( + @JsonProperty("connectorId") String connectorId, + @JsonProperty("columnName") String columnName, + @JsonProperty("columnType") Type columnType) + { + this.connectorId = checkNotNull(connectorId, "connectorId is null"); + this.columnName = checkNotNull(columnName, "columnName is null"); + this.columnType = checkNotNull(columnType, "columnType is null"); + } + + @JsonProperty + public String getConnectorId() + { + return connectorId; + } + + @JsonProperty + public String getColumnName() + { + return columnName; + } + + @JsonProperty + public Type getColumnType() + { + return columnType; + } + + public ColumnMetadata getColumnMetadata() + { + return new ColumnMetadata(columnName, columnType, false); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if ((obj == null) || (getClass() != obj.getClass())) { + return false; + } + JdbcColumnHandle o = (JdbcColumnHandle) obj; + return Objects.equals(this.connectorId, o.connectorId) && + Objects.equals(this.columnName, o.columnName); + } + + @Override + public int hashCode() + { + return Objects.hash(connectorId, columnName); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("connectorId", connectorId) + .add("columnName", columnName) + .add("columnType", columnType) + .toString(); + } +} diff --git a/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcConnector.java b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcConnector.java new file mode 100644 index 00000000..baee1f34 --- /dev/null +++ b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcConnector.java @@ -0,0 +1,98 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc; + +import com.facebook.presto.spi.Connector; +import com.facebook.presto.spi.ConnectorHandleResolver; +import com.facebook.presto.spi.ConnectorMetadata; +import com.facebook.presto.spi.ConnectorRecordSetProvider; +import com.facebook.presto.spi.ConnectorRecordSinkProvider; +import com.facebook.presto.spi.ConnectorSplitManager; +import io.airlift.bootstrap.LifeCycleManager; +import io.airlift.log.Logger; + +import javax.inject.Inject; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class JdbcConnector + implements Connector +{ + private static final Logger log = Logger.get(JdbcConnector.class); + + private final LifeCycleManager lifeCycleManager; + private final JdbcMetadata jdbcMetadata; + private final JdbcSplitManager jdbcSplitManager; + private final JdbcRecordSetProvider jdbcRecordSetProvider; + private final JdbcHandleResolver jdbcHandleResolver; + private final JdbcRecordSinkProvider jdbcRecordSinkProvider; + + @Inject + public JdbcConnector( + LifeCycleManager lifeCycleManager, + JdbcMetadata jdbcMetadata, + JdbcSplitManager jdbcSplitManager, + JdbcRecordSetProvider jdbcRecordSetProvider, + JdbcHandleResolver jdbcHandleResolver, + JdbcRecordSinkProvider jdbcRecordSinkProvider) + { + this.lifeCycleManager = checkNotNull(lifeCycleManager, "lifeCycleManager is null"); + this.jdbcMetadata = checkNotNull(jdbcMetadata, "jdbcMetadata is null"); + this.jdbcSplitManager = checkNotNull(jdbcSplitManager, "jdbcSplitManager is null"); + this.jdbcRecordSetProvider = checkNotNull(jdbcRecordSetProvider, "jdbcRecordSetProvider is null"); + this.jdbcHandleResolver = checkNotNull(jdbcHandleResolver, "jdbcHandleResolver is null"); + this.jdbcRecordSinkProvider = checkNotNull(jdbcRecordSinkProvider, "jdbcRecordSinkProvider is null"); + } + + @Override + public ConnectorMetadata getMetadata() + { + return jdbcMetadata; + } + + @Override + public ConnectorSplitManager getSplitManager() + { + return jdbcSplitManager; + } + + @Override + public ConnectorRecordSetProvider getRecordSetProvider() + { + return jdbcRecordSetProvider; + } + + @Override + public ConnectorHandleResolver getHandleResolver() + { + return jdbcHandleResolver; + } + + @Override + public ConnectorRecordSinkProvider getRecordSinkProvider() + { + return jdbcRecordSinkProvider; + } + + @Override + public final void shutdown() + { + try { + lifeCycleManager.stop(); + } + catch (Exception e) { + log.error(e, "Error shutting down connector"); + } + } +} diff --git a/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcConnectorFactory.java b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcConnectorFactory.java new file mode 100644 index 00000000..fdfe4e24 --- /dev/null +++ b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcConnectorFactory.java @@ -0,0 +1,76 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc; + +import com.facebook.presto.spi.Connector; +import com.facebook.presto.spi.ConnectorFactory; +import com.facebook.presto.spi.classloader.ThreadContextClassLoader; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableMap; +import com.google.inject.Injector; +import com.google.inject.Module; +import io.airlift.bootstrap.Bootstrap; + +import java.util.Map; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Strings.isNullOrEmpty; + +public class JdbcConnectorFactory + implements ConnectorFactory +{ + private final String name; + private final Module module; + private final Map optionalConfig; + private final ClassLoader classLoader; + + public JdbcConnectorFactory(String name, Module module, Map optionalConfig, ClassLoader classLoader) + { + checkArgument(!isNullOrEmpty(name), "name is null or empty"); + this.name = name; + this.module = checkNotNull(module, "module is null"); + this.optionalConfig = ImmutableMap.copyOf(checkNotNull(optionalConfig, "optionalConfig is null")); + this.classLoader = checkNotNull(classLoader, "classLoader is null"); + } + + @Override + public String getName() + { + return name; + } + + @Override + public Connector create(String connectorId, Map requiredConfig) + { + checkNotNull(requiredConfig, "requiredConfig is null"); + checkNotNull(optionalConfig, "optionalConfig is null"); + + try (ThreadContextClassLoader ignored = new ThreadContextClassLoader(classLoader)) { + Bootstrap app = new Bootstrap(new JdbcModule(connectorId), module); + + Injector injector = app + .strictConfig() + .doNotInitializeLogging() + .setRequiredConfigurationProperties(requiredConfig) + .setOptionalConfigurationProperties(optionalConfig) + .initialize(); + + return injector.getInstance(JdbcConnector.class); + } + catch (Exception e) { + throw Throwables.propagate(e); + } + } +} diff --git a/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcConnectorId.java b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcConnectorId.java new file mode 100644 index 00000000..93b4a4f3 --- /dev/null +++ b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcConnectorId.java @@ -0,0 +1,53 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc; + +import java.util.Objects; + +import static com.google.common.base.Preconditions.checkNotNull; + +public final class JdbcConnectorId +{ + private final String id; + + public JdbcConnectorId(String id) + { + this.id = checkNotNull(id, "id is null"); + } + + @Override + public String toString() + { + return id; + } + + @Override + public int hashCode() + { + return Objects.hash(id); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if ((obj == null) || (getClass() != obj.getClass())) { + return false; + } + JdbcConnectorId other = (JdbcConnectorId) obj; + return Objects.equals(this.id, other.id); + } +} diff --git a/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcHandleResolver.java b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcHandleResolver.java new file mode 100644 index 00000000..05ded2cb --- /dev/null +++ b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcHandleResolver.java @@ -0,0 +1,84 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc; + +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorHandleResolver; +import com.facebook.presto.spi.ConnectorOutputTableHandle; +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.ConnectorTableHandle; + +import javax.inject.Inject; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class JdbcHandleResolver + implements ConnectorHandleResolver +{ + private final String connectorId; + + @Inject + public JdbcHandleResolver(JdbcConnectorId clientId) + { + this.connectorId = checkNotNull(clientId, "clientId is null").toString(); + } + + @Override + public boolean canHandle(ConnectorTableHandle tableHandle) + { + return tableHandle instanceof JdbcTableHandle && ((JdbcTableHandle) tableHandle).getConnectorId().equals(connectorId); + } + + @Override + public boolean canHandle(ColumnHandle columnHandle) + { + return columnHandle instanceof JdbcColumnHandle && ((JdbcColumnHandle) columnHandle).getConnectorId().equals(connectorId); + } + + @Override + public boolean canHandle(ConnectorSplit split) + { + return split instanceof JdbcSplit && ((JdbcSplit) split).getConnectorId().equals(connectorId); + } + + @Override + public boolean canHandle(ConnectorOutputTableHandle tableHandle) + { + return (tableHandle instanceof JdbcOutputTableHandle) && ((JdbcOutputTableHandle) tableHandle).getConnectorId().equals(connectorId); + } + + @Override + public Class getTableHandleClass() + { + return JdbcTableHandle.class; + } + + @Override + public Class getColumnHandleClass() + { + return JdbcColumnHandle.class; + } + + @Override + public Class getSplitClass() + { + return JdbcSplit.class; + } + + @Override + public Class getOutputTableHandleClass() + { + return JdbcOutputTableHandle.class; + } +} diff --git a/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcMetadata.java b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcMetadata.java new file mode 100644 index 00000000..be31dd93 --- /dev/null +++ b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcMetadata.java @@ -0,0 +1,209 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc; + +import com.facebook.presto.spi.ColumnMetadata; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorInsertTableHandle; +import com.facebook.presto.spi.ConnectorMetadata; +import com.facebook.presto.spi.ConnectorOutputTableHandle; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.ConnectorTableHandle; +import com.facebook.presto.spi.ConnectorTableMetadata; +import com.facebook.presto.spi.InsertOption; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.spi.SchemaTablePrefix; +import com.facebook.presto.spi.TableNotFoundException; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.airlift.slice.Slice; + +import javax.inject.Inject; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static com.facebook.presto.plugin.jdbc.Types.checkType; +import static com.facebook.presto.spi.StandardErrorCode.NOT_SUPPORTED; +import static com.facebook.presto.spi.StandardErrorCode.PERMISSION_DENIED; +import static com.google.common.base.Preconditions.checkNotNull; + +public class JdbcMetadata + implements ConnectorMetadata +{ + private final JdbcClient jdbcClient; + private final boolean allowDropTable; + + @Inject + public JdbcMetadata(JdbcConnectorId connectorId, JdbcClient jdbcClient, JdbcMetadataConfig config) + { + this.jdbcClient = checkNotNull(jdbcClient, "client is null"); + + checkNotNull(config, "config is null"); + allowDropTable = config.isAllowDropTable(); + } + + @Override + public List listSchemaNames(ConnectorSession session) + { + return ImmutableList.copyOf(jdbcClient.getSchemaNames()); + } + + @Override + public JdbcTableHandle getTableHandle(ConnectorSession session, SchemaTableName tableName) + { + return jdbcClient.getTableHandle(tableName); + } + + @Override + public ConnectorTableMetadata getTableMetadata(ConnectorTableHandle table) + { + JdbcTableHandle handle = checkType(table, JdbcTableHandle.class, "tableHandle"); + + ImmutableList.Builder columnMetadata = ImmutableList.builder(); + for (JdbcColumnHandle column : jdbcClient.getColumns(handle)) { + columnMetadata.add(column.getColumnMetadata()); + } + return new ConnectorTableMetadata(handle.getSchemaTableName(), columnMetadata.build()); + } + + @Override + public List listTables(ConnectorSession session, String schemaNameOrNull) + { + return jdbcClient.getTableNames(schemaNameOrNull); + } + + @Override + public ColumnHandle getSampleWeightColumnHandle(ConnectorTableHandle tableHandle) + { + return null; + } + + @Override + public Map getColumnHandles(ConnectorTableHandle tableHandle) + { + JdbcTableHandle jdbcTableHandle = checkType(tableHandle, JdbcTableHandle.class, "tableHandle"); + + ImmutableMap.Builder columnHandles = ImmutableMap.builder(); + for (JdbcColumnHandle column : jdbcClient.getColumns(jdbcTableHandle)) { + columnHandles.put(column.getColumnMetadata().getName(), column); + } + return columnHandles.build(); + } + + @Override + public Map> listTableColumns(ConnectorSession session, SchemaTablePrefix prefix) + { + ImmutableMap.Builder> columns = ImmutableMap.builder(); + for (SchemaTableName tableName : listTables(session, prefix.getSchemaName())) { + try { + JdbcTableHandle tableHandle = jdbcClient.getTableHandle(tableName); + if (tableHandle == null) { + continue; + } + columns.put(tableName, getTableMetadata(tableHandle).getColumns()); + } + catch (TableNotFoundException e) { + // table disappeared during listing operation + } + } + return columns.build(); + } + + @Override + public ColumnMetadata getColumnMetadata(ConnectorTableHandle tableHandle, ColumnHandle columnHandle) + { + checkType(tableHandle, JdbcTableHandle.class, "tableHandle"); + return checkType(columnHandle, JdbcColumnHandle.class, "columnHandle").getColumnMetadata(); + } + + @Override + public boolean canCreateSampledTables(ConnectorSession session) + { + return false; + } + + @Override + public void createTable(ConnectorSession session, ConnectorTableMetadata tableMetadata) + { + throw new PrestoException(NOT_SUPPORTED, "This connector does not support creating tables"); + } + + @Override + public void dropTable(ConnectorTableHandle tableHandle) + { + if (!allowDropTable) { + throw new PrestoException(PERMISSION_DENIED, "DROP TABLE is disabled in this catalog"); + } + JdbcTableHandle handle = checkType(tableHandle, JdbcTableHandle.class, "tableHandle"); + jdbcClient.dropTable(handle); + } + + @Override + public ConnectorOutputTableHandle beginCreateTable(ConnectorSession session, ConnectorTableMetadata tableMetadata) + { + return jdbcClient.beginCreateTable(tableMetadata); + } + + @Override + public void commitCreateTable(ConnectorOutputTableHandle tableHandle, Collection fragments) + { + JdbcOutputTableHandle handle = checkType(tableHandle, JdbcOutputTableHandle.class, "tableHandle"); + jdbcClient.commitCreateTable(handle, fragments); + } + + @Override + public void renameTable(ConnectorTableHandle tableHandle, SchemaTableName newTableName) + { + throw new PrestoException(NOT_SUPPORTED, "This connector does not support renaming tables"); + } + + @Override + public ConnectorInsertTableHandle beginInsert(ConnectorSession session, ConnectorTableHandle tableHandle, InsertOption insertOption) + { + throw new PrestoException(NOT_SUPPORTED, "This connector does not support inserts"); + } + + @Override + public void commitInsert(ConnectorInsertTableHandle insertHandle, Collection fragments) + { + throw new UnsupportedOperationException(); + } + + @Override + public void createView(ConnectorSession session, SchemaTableName viewName, String viewData, boolean replace) + { + throw new PrestoException(NOT_SUPPORTED, "This connector does not support creating views"); + } + + @Override + public void dropView(ConnectorSession session, SchemaTableName viewName) + { + throw new PrestoException(NOT_SUPPORTED, "This connector does not support dropping views"); + } + + @Override + public List listViews(ConnectorSession session, String schemaNameOrNull) + { + return ImmutableList.of(); + } + + @Override + public Map getViews(ConnectorSession session, SchemaTablePrefix prefix) + { + return ImmutableMap.of(); + } +} diff --git a/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcMetadataConfig.java b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcMetadataConfig.java new file mode 100644 index 00000000..6b3e5fb4 --- /dev/null +++ b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcMetadataConfig.java @@ -0,0 +1,35 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc; + +import io.airlift.configuration.Config; +import io.airlift.configuration.ConfigDescription; + +public class JdbcMetadataConfig +{ + private boolean allowDropTable; + + public boolean isAllowDropTable() + { + return allowDropTable; + } + + @Config("allow-drop-table") + @ConfigDescription("Allow connector to drop tables") + public JdbcMetadataConfig setAllowDropTable(boolean allowDropTable) + { + this.allowDropTable = allowDropTable; + return this; + } +} diff --git a/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcModule.java b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcModule.java new file mode 100644 index 00000000..18625396 --- /dev/null +++ b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcModule.java @@ -0,0 +1,45 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc; + +import com.google.inject.Binder; +import com.google.inject.Module; +import com.google.inject.Scopes; + +import static com.google.common.base.Preconditions.checkNotNull; +import static io.airlift.configuration.ConfigBinder.configBinder; + +public class JdbcModule + implements Module +{ + private final String connectorId; + + public JdbcModule(String connectorId) + { + this.connectorId = checkNotNull(connectorId, "connector id is null"); + } + + @Override + public void configure(Binder binder) + { + binder.bind(JdbcConnectorId.class).toInstance(new JdbcConnectorId(connectorId)); + binder.bind(JdbcMetadata.class).in(Scopes.SINGLETON); + binder.bind(JdbcSplitManager.class).in(Scopes.SINGLETON); + binder.bind(JdbcRecordSetProvider.class).in(Scopes.SINGLETON); + binder.bind(JdbcHandleResolver.class).in(Scopes.SINGLETON); + binder.bind(JdbcRecordSinkProvider.class).in(Scopes.SINGLETON); + binder.bind(JdbcConnector.class).in(Scopes.SINGLETON); + configBinder(binder).bindConfig(JdbcMetadataConfig.class); + } +} diff --git a/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcOutputTableHandle.java b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcOutputTableHandle.java new file mode 100644 index 00000000..0cc123dc --- /dev/null +++ b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcOutputTableHandle.java @@ -0,0 +1,181 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc; + +import com.facebook.presto.spi.ConnectorOutputTableHandle; +import com.facebook.presto.spi.type.Type; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +import javax.annotation.Nullable; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.lang.String.format; + +public class JdbcOutputTableHandle + implements ConnectorOutputTableHandle +{ + private final String connectorId; + private final String catalogName; + private final String schemaName; + private final String tableName; + private final List columnNames; + private final List columnTypes; + private final String tableOwner; + private final String temporaryTableName; + private final String connectionUrl; + private final Map connectionProperties; + + @JsonCreator + public JdbcOutputTableHandle( + @JsonProperty("connectorId") String connectorId, + @JsonProperty("catalogName") @Nullable String catalogName, + @JsonProperty("schemaName") @Nullable String schemaName, + @JsonProperty("tableName") String tableName, + @JsonProperty("columnNames") List columnNames, + @JsonProperty("columnTypes") List columnTypes, + @JsonProperty("tableOwner") String tableOwner, + @JsonProperty("temporaryTableName") String temporaryTableName, + @JsonProperty("connectionUrl") String connectionUrl, + @JsonProperty("connectionProperties") Map connectionProperties) + { + this.connectorId = checkNotNull(connectorId, "connectorId is null"); + this.catalogName = catalogName; + this.schemaName = schemaName; + this.tableName = checkNotNull(tableName, "tableName is null"); + this.tableOwner = checkNotNull(tableOwner, "tableOwner is null"); + this.temporaryTableName = checkNotNull(temporaryTableName, "temporaryTableName is null"); + this.connectionUrl = checkNotNull(connectionUrl, "connectionUrl is null"); + this.connectionProperties = ImmutableMap.copyOf(checkNotNull(connectionProperties, "connectionProperties is null")); + + checkNotNull(columnNames, "columnNames is null"); + checkNotNull(columnTypes, "columnTypes is null"); + checkArgument(columnNames.size() == columnTypes.size(), "columnNames and columnTypes sizes don't match"); + this.columnNames = ImmutableList.copyOf(columnNames); + this.columnTypes = ImmutableList.copyOf(columnTypes); + } + + @JsonProperty + public String getConnectorId() + { + return connectorId; + } + + @JsonProperty + @Nullable + public String getCatalogName() + { + return catalogName; + } + + @JsonProperty + @Nullable + public String getSchemaName() + { + return schemaName; + } + + @JsonProperty + public String getTableName() + { + return tableName; + } + + @JsonProperty + public List getColumnNames() + { + return columnNames; + } + + @JsonProperty + public List getColumnTypes() + { + return columnTypes; + } + + @JsonProperty + public String getTableOwner() + { + return tableOwner; + } + + @JsonProperty + public String getTemporaryTableName() + { + return temporaryTableName; + } + + @JsonProperty + public String getConnectionUrl() + { + return connectionUrl; + } + + @JsonProperty + public Map getConnectionProperties() + { + return connectionProperties; + } + + @Override + public String toString() + { + return format("jdbc:%s.%s.%s", catalogName, schemaName, tableName); + } + + @Override + public int hashCode() + { + return Objects.hash( + connectorId, + catalogName, + schemaName, + tableName, + columnNames, + columnTypes, + tableOwner, + temporaryTableName, + connectionUrl, + connectionProperties); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + JdbcOutputTableHandle other = (JdbcOutputTableHandle) obj; + return Objects.equals(this.connectorId, other.connectorId) && + Objects.equals(this.catalogName, other.catalogName) && + Objects.equals(this.schemaName, other.schemaName) && + Objects.equals(this.tableName, other.tableName) && + Objects.equals(this.columnNames, other.columnNames) && + Objects.equals(this.columnTypes, other.columnTypes) && + Objects.equals(this.tableOwner, other.tableOwner) && + Objects.equals(this.temporaryTableName, other.temporaryTableName) && + Objects.equals(this.connectionUrl, other.connectionUrl) && + Objects.equals(this.connectionProperties, other.connectionProperties); + } +} diff --git a/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcPartition.java b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcPartition.java new file mode 100644 index 00000000..4736f07e --- /dev/null +++ b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcPartition.java @@ -0,0 +1,59 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc; + +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorPartition; +import com.facebook.presto.spi.TupleDomain; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkNotNull; + +public class JdbcPartition + implements ConnectorPartition +{ + private final JdbcTableHandle jdbcTableHandle; + private final TupleDomain domain; + + public JdbcPartition(JdbcTableHandle jdbcTableHandle, TupleDomain domain) + { + this.jdbcTableHandle = checkNotNull(jdbcTableHandle, "jdbcTableHandle is null"); + this.domain = checkNotNull(domain, "domain is null"); + } + + @Override + public String getPartitionId() + { + return jdbcTableHandle.toString(); + } + + public JdbcTableHandle getJdbcTableHandle() + { + return jdbcTableHandle; + } + + @Override + public TupleDomain getTupleDomain() + { + return domain; + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("jdbcTableHandle", jdbcTableHandle) + .toString(); + } +} diff --git a/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcPlugin.java b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcPlugin.java new file mode 100644 index 00000000..c0a08e1e --- /dev/null +++ b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcPlugin.java @@ -0,0 +1,63 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc; + +import com.facebook.presto.spi.ConnectorFactory; +import com.facebook.presto.spi.Plugin; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.inject.Module; + +import java.util.List; +import java.util.Map; + +import static com.google.common.base.MoreObjects.firstNonNull; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Strings.isNullOrEmpty; + +public class JdbcPlugin + implements Plugin +{ + private final String name; + private final Module module; + private Map optionalConfig = ImmutableMap.of(); + + public JdbcPlugin(String name, Module module) + { + checkArgument(!isNullOrEmpty(name), "name is null or empty"); + this.name = name; + this.module = checkNotNull(module, "module is null"); + } + + @Override + public void setOptionalConfig(Map optionalConfig) + { + this.optionalConfig = ImmutableMap.copyOf(checkNotNull(optionalConfig, "optionalConfig is null")); + } + + @Override + public List getServices(Class type) + { + if (type == ConnectorFactory.class) { + return ImmutableList.of(type.cast(new JdbcConnectorFactory(name, module, optionalConfig, getClassLoader()))); + } + return ImmutableList.of(); + } + + private static ClassLoader getClassLoader() + { + return firstNonNull(Thread.currentThread().getContextClassLoader(), JdbcPlugin.class.getClassLoader()); + } +} diff --git a/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcRecordCursor.java b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcRecordCursor.java new file mode 100644 index 00000000..393e048b --- /dev/null +++ b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcRecordCursor.java @@ -0,0 +1,362 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc; + +import com.facebook.presto.plugin.jdbc.cache.JdbcCacheSplit; +import com.facebook.presto.plugin.jdbc.cache.JdbcJavaBean; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.RecordCursor; +import com.facebook.presto.spi.type.BigintType; +import com.facebook.presto.spi.type.DateType; +import com.facebook.presto.spi.type.TimeType; +import com.facebook.presto.spi.type.TimestampType; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.VarbinaryType; +import com.facebook.presto.spi.type.VarcharType; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import io.airlift.log.Logger; +import io.airlift.slice.Slice; +import org.joda.time.chrono.ISOChronology; + +import java.sql.Connection; +import java.sql.Date; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +import static com.facebook.presto.spi.StandardErrorCode.INTERNAL_ERROR; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Strings.isNullOrEmpty; +import static io.airlift.slice.Slices.utf8Slice; +import static io.airlift.slice.Slices.wrappedBuffer; +import static org.joda.time.DateTimeZone.UTC; + +public class JdbcRecordCursor + implements RecordCursor +{ + private static final Logger log = Logger.get(JdbcRecordCursor.class); + + private static final ISOChronology UTC_CHRONOLOGY = ISOChronology.getInstance(UTC); + + private final List columnHandles; + + private Connection connection; + private Statement statement; + private ResultSet resultSet; + private boolean closed; + + private List tableDataSet; + private boolean isCacheTable = false; + private AtomicLong rowRecord = new AtomicLong(0); + private BaseJdbcClient client; + private JdbcSplit split; + + public JdbcRecordCursor(JdbcClient jdbcClient, JdbcSplit split, List columnHandles) + { + this.client = (BaseJdbcClient) jdbcClient; + this.split = split; + isCacheTable = client.isCacheTable(split.getBaseTableName()); + this.columnHandles = ImmutableList.copyOf(checkNotNull(columnHandles, "columnHandles is null")); + if (isCacheTable) { + JdbcCacheSplit key = new JdbcCacheSplit(split.getConnectorId(), split.getCatalogName(), + split.getSchemaName(), split.getTableName(), split.getConnectionUrl(), split.getBaseTableName()); + tableDataSet = client.getTableDataSet(key); + } + else { + String sql = jdbcClient.buildSql(split, columnHandles); + try { + connection = jdbcClient.getConnection(split); + statement = connection.createStatement(); + statement.setFetchSize(1000); + + String whereCondition = split.getSplitPart(); + if (!isNullOrEmpty(whereCondition)) { + if (whereCondition.indexOf("LIMIT") != -1) { + sql += split.getSplitPart(); + } + else { + if (sql.indexOf("WHERE") != -1) { + sql += " AND " + split.getSplitPart(); + } + else { + sql += " WHERE " + split.getSplitPart(); + } + } + } + long startTime = System.currentTimeMillis(); + log.info("JdbcRecordCursor Executing: %s ", sql); + resultSet = statement.executeQuery(sql); + log.debug("The connection url: %s ,JdbcRecordCursor Executing: %s ,spend time : %s", split.getConnectionUrl(), sql, (System.currentTimeMillis() - startTime)); + } + catch (SQLException e) { + log.error("Execute sql [%s] error, connection url : %s", sql, split.getConnectionUrl()); + throw handleSqlException(e); + } + } + } + + @Override + public long getReadTimeNanos() + { + return 0; + } + + @Override + public long getTotalBytes() + { + return 0; + } + + @Override + public long getCompletedBytes() + { + return 0; + } + + @Override + public Type getType(int field) + { + return columnHandles.get(field).getColumnType(); + } + + @Override + public boolean advanceNextPosition() + { + if (closed) { + return false; + } + boolean result; + if (isCacheTable) { + long andIncrement = rowRecord.incrementAndGet(); + result = andIncrement <= tableDataSet.size(); + } + else { + try { + result = resultSet.next(); + if (result) { + rowRecord.getAndIncrement(); + } + } + catch (SQLException e) { + throw handleSqlException(e); + } + } + if (!result) { + close(); + } + return result; + } + + @Override + public boolean getBoolean(int field) + { + checkState(!closed, "cursor is closed"); + if (isCacheTable) { + return (boolean) getFieldValue(field); + } + else { + try { + return resultSet.getBoolean(field + 1); + } + catch (SQLException e) { + throw handleSqlException(e); + } + } + } + + @Override + public long getLong(int field) + { + checkState(!closed, "cursor is closed"); + try { + Type type = getType(field); + if (type.equals(BigintType.BIGINT)) { + if (isCacheTable) { + return (long) getFieldValue(field); + } + else { + return resultSet.getLong(field + 1); + } + } + if (type.equals(DateType.DATE)) { + Date date = null; + if (isCacheTable) { + date = (Date) getFieldValue(field); + } + else { + date = resultSet.getDate(field + 1); + } + // JDBC returns a date using a timestamp at midnight in the JVM timezone + long localMillis = date.getTime(); + // Convert it to a midnight in UTC + long utcMillis = ISOChronology.getInstance().getZone().getMillisKeepLocal(UTC, localMillis); + // convert to days + return TimeUnit.MILLISECONDS.toDays(utcMillis); + } + if (type.equals(TimeType.TIME)) { + Time time = null; + if (isCacheTable) { + time = (Time) getFieldValue(field); + } + else { + time = resultSet.getTime(field + 1); + } + return UTC_CHRONOLOGY.millisOfDay().get(time.getTime()); + } + if (type.equals(TimestampType.TIMESTAMP)) { + Timestamp timestamp = null; + if (isCacheTable) { + timestamp = (Timestamp) getFieldValue(field); + } + else { + timestamp = resultSet.getTimestamp(field + 1); + } + return timestamp.getTime(); + } + throw new PrestoException(INTERNAL_ERROR, "Unhandled type for long: " + type.getTypeSignature()); + } + catch (SQLException e) { + throw handleSqlException(e); + } + } + + @Override + public double getDouble(int field) + { + checkState(!closed, "cursor is closed"); + if (isCacheTable) { + return (double) getFieldValue(field); + } + else { + try { + return resultSet.getDouble(field + 1); + } + catch (SQLException e) { + throw handleSqlException(e); + } + } + } + + @Override + public Slice getSlice(int field) + { + checkState(!closed, "cursor is closed"); + try { + Type type = getType(field); + if (type.equals(VarcharType.VARCHAR)) { + String str = null; + if (isCacheTable) { + str = (String) getFieldValue(field); + } + else { + str = resultSet.getString(field + 1); + } + return utf8Slice(str); + } + if (type.equals(VarbinaryType.VARBINARY)) { + byte[] bytes = null; + if (isCacheTable) { + bytes = (byte[]) getFieldValue(field); + } + else { + bytes = resultSet.getBytes(field + 1); + } + return wrappedBuffer(bytes); + } + throw new PrestoException(INTERNAL_ERROR, "Unhandled type for slice: " + type.getTypeSignature()); + } + catch (SQLException e) { + throw handleSqlException(e); + } + } + + @Override + public boolean isNull(int field) + { + checkState(!closed, "cursor is closed"); + checkArgument(field < columnHandles.size(), "Invalid field index"); + + try { + if (isCacheTable) { + Object feildValue = getFieldValue(field); + return feildValue == null; + } + // JDBC is kind of dumb: we need to read the field and then ask + // if it was null, which means we are wasting effort here. + // We could save the result of the field access if it matters. + resultSet.getObject(field + 1); + + return resultSet.wasNull(); + } + catch (SQLException e) { + throw handleSqlException(e); + } + } + + @SuppressWarnings({"UnusedDeclaration", "EmptyTryBlock"}) + @Override + public void close() + { + if (closed) { + return; + } + if (!isNullOrEmpty(split.getSplitField()) && split.isCalcStepEnable()) { + client.commitPdboLogs(split, rowRecord.get()); + } + closed = true; + try { + if (statement != null) { + statement.cancel(); + } + } + catch (SQLException e) { + throw Throwables.propagate(e); + } + // use try with resources to close everything properly + try (ResultSet resultSet = this.resultSet; + Statement statement = this.statement; + Connection connection = this.connection) { + // do nothing + } + catch (SQLException e) { + throw Throwables.propagate(e); + } + } + + private Object getFieldValue(int field) + { + String lowerCase = columnHandles.get(field).getColumnName().toLowerCase(); + JdbcJavaBean jdbcJavaBean = tableDataSet.get(rowRecord.intValue() - 1); + return jdbcJavaBean.getFieldObjectValue(jdbcJavaBean.getColumns().indexOf(lowerCase)); + } + + private RuntimeException handleSqlException(SQLException e) + { + try { + close(); + } + catch (Exception closeException) { + e.addSuppressed(closeException); + } + return Throwables.propagate(e); + } +} diff --git a/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcRecordSet.java b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcRecordSet.java new file mode 100644 index 00000000..378452b6 --- /dev/null +++ b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcRecordSet.java @@ -0,0 +1,58 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc; + +import com.facebook.presto.spi.RecordCursor; +import com.facebook.presto.spi.RecordSet; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class JdbcRecordSet + implements RecordSet +{ + private final JdbcClient jdbcClient; + private final List columnHandles; + private final List columnTypes; + private final JdbcSplit split; + + public JdbcRecordSet(JdbcClient jdbcClient, JdbcSplit split, List columnHandles) + { + this.jdbcClient = checkNotNull(jdbcClient, "jdbcClient is null"); + this.split = checkNotNull(split, "split is null"); + + checkNotNull(split, "split is null"); + this.columnHandles = checkNotNull(columnHandles, "column handles is null"); + ImmutableList.Builder types = ImmutableList.builder(); + for (JdbcColumnHandle column : columnHandles) { + types.add(column.getColumnType()); + } + this.columnTypes = types.build(); + } + + @Override + public List getColumnTypes() + { + return columnTypes; + } + + @Override + public RecordCursor cursor() + { + return new JdbcRecordCursor(jdbcClient, split, columnHandles); + } +} diff --git a/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcRecordSetProvider.java b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcRecordSetProvider.java new file mode 100644 index 00000000..c14f37bd --- /dev/null +++ b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcRecordSetProvider.java @@ -0,0 +1,52 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc; + +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorRecordSetProvider; +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.RecordSet; +import com.google.common.collect.ImmutableList; + +import javax.inject.Inject; + +import java.util.List; + +import static com.facebook.presto.plugin.jdbc.Types.checkType; +import static com.google.common.base.Preconditions.checkNotNull; + +public class JdbcRecordSetProvider + implements ConnectorRecordSetProvider +{ + private final JdbcClient jdbcClient; + + @Inject + public JdbcRecordSetProvider(JdbcClient jdbcClient) + { + this.jdbcClient = checkNotNull(jdbcClient, "jdbcClient is null"); + } + + @Override + public RecordSet getRecordSet(ConnectorSplit split, List columns) + { + JdbcSplit jdbcSplit = checkType(split, JdbcSplit.class, "split"); + + ImmutableList.Builder handles = ImmutableList.builder(); + for (ColumnHandle handle : columns) { + handles.add(checkType(handle, JdbcColumnHandle.class, "columnHandle")); + } + + return new JdbcRecordSet(jdbcClient, jdbcSplit, handles.build()); + } +} diff --git a/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcRecordSink.java b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcRecordSink.java new file mode 100644 index 00000000..2ab44bad --- /dev/null +++ b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcRecordSink.java @@ -0,0 +1,205 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc; + +import com.facebook.presto.spi.RecordSink; +import com.facebook.presto.spi.type.Type; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; +import org.joda.time.DateTimeZone; +import org.joda.time.chrono.ISOChronology; + +import java.sql.Connection; +import java.sql.Date; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static com.facebook.presto.spi.type.DateType.DATE; +import static com.google.common.base.Preconditions.checkState; +import static java.nio.charset.StandardCharsets.UTF_8; + +public class JdbcRecordSink + implements RecordSink +{ + private final Connection connection; + private final PreparedStatement statement; + + private final int fieldCount; + private final List columnTypes; + private int field = -1; + private int batchSize; + + public JdbcRecordSink(JdbcOutputTableHandle handle, JdbcClient jdbcClient) + { + try { + connection = jdbcClient.getConnection(handle); + connection.setAutoCommit(false); + } + catch (SQLException e) { + throw Throwables.propagate(e); + } + + try { + statement = connection.prepareStatement(jdbcClient.buildInsertSql(handle)); + } + catch (SQLException e) { + throw Throwables.propagate(e); + } + + fieldCount = handle.getColumnNames().size(); + columnTypes = handle.getColumnTypes(); + } + + @Override + public void beginRecord(long sampleWeight) + { + checkState(field == -1, "already in record"); + field = 0; + } + + @Override + public void finishRecord() + { + checkState(field != -1, "not in record"); + checkState(field == fieldCount, "not all fields set"); + field = -1; + + try { + statement.addBatch(); + batchSize++; + + if (batchSize >= 1000) { + statement.executeBatch(); + connection.commit(); + connection.setAutoCommit(false); + batchSize = 0; + } + } + catch (SQLException e) { + throw Throwables.propagate(e); + } + } + + @Override + public void appendNull() + { + try { + statement.setObject(next(), null); + } + catch (SQLException e) { + throw Throwables.propagate(e); + } + } + + @Override + public void appendBoolean(boolean value) + { + try { + statement.setBoolean(next(), value); + } + catch (SQLException e) { + throw Throwables.propagate(e); + } + } + + @Override + public void appendLong(long value) + { + try { + if (DATE.equals(columnTypes.get(field))) { + // convert to midnight in default time zone + long utcMillis = TimeUnit.DAYS.toMillis(value); + long localMillis = ISOChronology.getInstanceUTC().getZone().getMillisKeepLocal(DateTimeZone.getDefault(), utcMillis); + statement.setDate(next(), new Date(localMillis)); + } + else { + statement.setLong(next(), value); + } + } + catch (SQLException e) { + throw Throwables.propagate(e); + } + } + + @Override + public void appendDouble(double value) + { + try { + statement.setDouble(next(), value); + } + catch (SQLException e) { + throw Throwables.propagate(e); + } + } + + @Override + public void appendString(byte[] value) + { + try { + statement.setString(next(), new String(value, UTF_8)); + } + catch (SQLException e) { + throw Throwables.propagate(e); + } + } + + @Override + public Collection commit() + { + // commit and close + try (Connection connection = this.connection) { + if (batchSize > 0) { + statement.executeBatch(); + connection.commit(); + } + } + catch (SQLException e) { + throw Throwables.propagate(e); + } + // the committer does not need any additional info + return ImmutableList.of(); + } + + @SuppressWarnings("UnusedDeclaration") + @Override + public void rollback() + { + // rollback and close + try (Connection connection = this.connection; + PreparedStatement statement = this.statement) { + connection.rollback(); + } + catch (SQLException e) { + throw Throwables.propagate(e); + } + } + + @Override + public List getColumnTypes() + { + return columnTypes; + } + + private int next() + { + checkState(field != -1, "not in record"); + checkState(field < fieldCount, "all fields already set"); + field++; + return field; + } +} diff --git a/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcRecordSinkProvider.java b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcRecordSinkProvider.java new file mode 100644 index 00000000..bd92bb1a --- /dev/null +++ b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcRecordSinkProvider.java @@ -0,0 +1,48 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc; + +import com.facebook.presto.spi.ConnectorInsertTableHandle; +import com.facebook.presto.spi.ConnectorOutputTableHandle; +import com.facebook.presto.spi.ConnectorRecordSinkProvider; +import com.facebook.presto.spi.RecordSink; + +import javax.inject.Inject; + +import static com.facebook.presto.plugin.jdbc.Types.checkType; +import static com.google.common.base.Preconditions.checkNotNull; + +public class JdbcRecordSinkProvider + implements ConnectorRecordSinkProvider +{ + private final JdbcClient jdbcClient; + + @Inject + public JdbcRecordSinkProvider(JdbcClient jdbcClient) + { + this.jdbcClient = checkNotNull(jdbcClient, "jdbcClient is null"); + } + + @Override + public RecordSink getRecordSink(ConnectorOutputTableHandle tableHandle) + { + return new JdbcRecordSink(checkType(tableHandle, JdbcOutputTableHandle.class, "tableHandle"), jdbcClient); + } + + @Override + public RecordSink getRecordSink(ConnectorInsertTableHandle tableHandle) + { + throw new UnsupportedOperationException(); + } +} diff --git a/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcSplit.java b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcSplit.java new file mode 100644 index 00000000..f207a310 --- /dev/null +++ b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcSplit.java @@ -0,0 +1,211 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc; + +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.HostAddress; +import com.facebook.presto.spi.TupleDomain; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableMap; + +import javax.annotation.Nullable; + +import java.util.List; +import java.util.Map; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class JdbcSplit + implements ConnectorSplit +{ + private final String connectorId; + private final String catalogName; + private final String schemaName; + private final String tableName; + private final String connectionUrl; + private final Map connectionProperties; + private final TupleDomain tupleDomain; + private final String splitPart; + private final List addresses; + private final boolean remotelyAccessible; + private final String baseTableName; + private final String splitField; + private final String beginIndex; + private final String endIndex; + private final long timeStamp; + private final int scanNodes; + private final boolean isCalcStepEnable; + private final String dbHost; + + @JsonCreator + public JdbcSplit( + @JsonProperty("connectorId") String connectorId, + @JsonProperty("catalogName") @Nullable String catalogName, + @JsonProperty("schemaName") @Nullable String schemaName, + @JsonProperty("tableName") String tableName, + @JsonProperty("connectionUrl") String connectionUrl, + @JsonProperty("connectionProperties") Map connectionProperties, + @JsonProperty("tupleDomain") TupleDomain tupleDomain, + @JsonProperty("splitPart") String splitPart, + @JsonProperty("addresses") List addresses, + @JsonProperty("remotelyAccessible") boolean remotelyAccessible, + @JsonProperty("baseTableName") String baseTableName, + @JsonProperty("splitField") String splitField, + @JsonProperty("beginIndex") String beginIndex, + @JsonProperty("endIndex") String endIndex, + @JsonProperty("timeStamp") long timeStamp, + @JsonProperty("scanNodes") int scanNodes, + @JsonProperty("isCalcStepEnable") boolean isCalcStepEnable, + @JsonProperty("dbHost") String dbHost) + { + this.connectorId = checkNotNull(connectorId, "connector id is null"); + this.catalogName = catalogName; + this.schemaName = schemaName; + this.tableName = checkNotNull(tableName, "table name is null"); + this.connectionUrl = checkNotNull(connectionUrl, "connectionUrl is null"); + this.connectionProperties = ImmutableMap.copyOf(checkNotNull(connectionProperties, "connectionProperties is null")); + this.tupleDomain = checkNotNull(tupleDomain, "tupleDomain is null"); + this.splitPart = splitPart; + this.remotelyAccessible = remotelyAccessible; + this.addresses = checkNotNull(addresses, "host addresses is null"); + this.baseTableName = baseTableName; + this.splitField = splitField; + this.beginIndex = beginIndex; + this.endIndex = endIndex; + this.timeStamp = timeStamp; + this.scanNodes = scanNodes; + this.isCalcStepEnable = isCalcStepEnable; + this.dbHost = dbHost; + } + + @JsonProperty + public String getConnectorId() + { + return connectorId; + } + + @JsonProperty + @Nullable + public String getCatalogName() + { + return catalogName; + } + + @JsonProperty + @Nullable + public String getSchemaName() + { + return schemaName; + } + + @JsonProperty + public String getTableName() + { + return tableName; + } + + @JsonProperty + public String getConnectionUrl() + { + return connectionUrl; + } + + @JsonProperty + public Map getConnectionProperties() + { + return connectionProperties; + } + + @JsonProperty + public TupleDomain getTupleDomain() + { + return tupleDomain; + } + + @JsonProperty + @Override + public boolean isRemotelyAccessible() + { + return remotelyAccessible; + } + + @JsonProperty + @Override + public List getAddresses() + { + return addresses; + } + + @Override + public Object getInfo() + { + return this; + } + + @JsonProperty + public String getSplitPart() + { + return splitPart; + } + + @JsonProperty + public String getBaseTableName() + { + return baseTableName; + } + + @JsonProperty + public String getSplitField() + { + return splitField; + } + + @JsonProperty + public String getBeginIndex() + { + return beginIndex; + } + + @JsonProperty + public String getEndIndex() + { + return endIndex; + } + + @JsonProperty + public long getTimeStamp() + { + return timeStamp; + } + + @JsonProperty + public int getScanNodes() + { + return scanNodes; + } + + @JsonProperty + public boolean isCalcStepEnable() + { + return isCalcStepEnable; + } + + @JsonProperty + public String getDbHost() + { + return dbHost; + } +} diff --git a/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcSplitManager.java b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcSplitManager.java new file mode 100644 index 00000000..23a89ceb --- /dev/null +++ b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcSplitManager.java @@ -0,0 +1,67 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc; + +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorPartition; +import com.facebook.presto.spi.ConnectorPartitionResult; +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.ConnectorSplitManager; +import com.facebook.presto.spi.ConnectorSplitSource; +import com.facebook.presto.spi.ConnectorTableHandle; +import com.facebook.presto.spi.FixedSplitSource; +import com.facebook.presto.spi.TupleDomain; +import com.google.common.collect.ImmutableList; + +import javax.inject.Inject; + +import java.util.List; + +import static com.facebook.presto.plugin.jdbc.Types.checkType; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class JdbcSplitManager + implements ConnectorSplitManager +{ + private final String connectorId; + private final JdbcClient jdbcClient; + + @Inject + public JdbcSplitManager(JdbcConnectorId connectorId, JdbcClient jdbcClient) + { + this.connectorId = checkNotNull(connectorId, "connectorId is null").toString(); + this.jdbcClient = checkNotNull(jdbcClient, "client is null"); + } + + @Override + public ConnectorPartitionResult getPartitions(ConnectorTableHandle tableHandle, TupleDomain tupleDomain) + { + JdbcTableHandle handle = checkType(tableHandle, JdbcTableHandle.class, "tableHandle"); + return jdbcClient.getPartitions(handle, tupleDomain); + } + + @Override + public ConnectorSplitSource getPartitionSplits(ConnectorTableHandle tableHandle, List partitions) + { + if (partitions.isEmpty()) { + return new FixedSplitSource(connectorId, ImmutableList.of()); + } + + checkArgument(partitions.size() == 1, "Expected one partition but got %s", partitions.size()); + JdbcPartition partition = checkType(partitions.get(0), JdbcPartition.class, "partition"); + + return jdbcClient.getPartitionSplits(partition); + } +} diff --git a/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcTableHandle.java b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcTableHandle.java new file mode 100644 index 00000000..44f90077 --- /dev/null +++ b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/JdbcTableHandle.java @@ -0,0 +1,109 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc; + +import com.facebook.presto.spi.ConnectorTableHandle; +import com.facebook.presto.spi.SchemaTableName; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Joiner; + +import javax.annotation.Nullable; + +import java.util.Objects; + +import static com.google.common.base.Preconditions.checkNotNull; + +public final class JdbcTableHandle + implements ConnectorTableHandle +{ + private final String connectorId; + private final SchemaTableName schemaTableName; + private final String catalogName; + private final String schemaName; + private final String tableName; + + @JsonCreator + public JdbcTableHandle( + @JsonProperty("connectorId") String connectorId, + @JsonProperty("schemaTableName") SchemaTableName schemaTableName, + @JsonProperty("catalogName") @Nullable String catalogName, + @JsonProperty("schemaName") @Nullable String schemaName, + @JsonProperty("tableName") String tableName) + { + this.connectorId = checkNotNull(connectorId, "connectorId is null"); + this.schemaTableName = checkNotNull(schemaTableName, "schemaTableName is null"); + this.catalogName = catalogName; + this.schemaName = schemaName; + this.tableName = checkNotNull(tableName, "tableName is null"); + } + + @JsonProperty + public String getConnectorId() + { + return connectorId; + } + + @JsonProperty + public SchemaTableName getSchemaTableName() + { + return schemaTableName; + } + + @JsonProperty + @Nullable + public String getCatalogName() + { + return catalogName; + } + + @JsonProperty + @Nullable + public String getSchemaName() + { + return schemaName; + } + + @JsonProperty + public String getTableName() + { + return tableName; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if ((obj == null) || (getClass() != obj.getClass())) { + return false; + } + JdbcTableHandle o = (JdbcTableHandle) obj; + return Objects.equals(this.connectorId, o.connectorId) && + Objects.equals(this.schemaTableName, o.schemaTableName); + } + + @Override + public int hashCode() + { + return Objects.hash(connectorId, schemaTableName); + } + + @Override + public String toString() + { + return Joiner.on(":").useForNull("null").join(connectorId, schemaTableName, catalogName, schemaName, tableName); + } +} diff --git a/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/QueryBuilder.java b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/QueryBuilder.java new file mode 100644 index 00000000..70e7c1aa --- /dev/null +++ b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/QueryBuilder.java @@ -0,0 +1,199 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc; + +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.Domain; +import com.facebook.presto.spi.Range; +import com.facebook.presto.spi.TupleDomain; +import com.facebook.presto.spi.type.BigintType; +import com.facebook.presto.spi.type.BooleanType; +import com.facebook.presto.spi.type.DateType; +import com.facebook.presto.spi.type.DoubleType; +import com.facebook.presto.spi.type.TimestampType; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.VarcharType; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; + +import io.airlift.slice.Slice; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Strings.isNullOrEmpty; +import static com.google.common.collect.Iterables.getOnlyElement; +import static com.google.common.collect.Iterables.transform; + +public class QueryBuilder +{ + private final String quote; + + public QueryBuilder(String quote) + { + this.quote = checkNotNull(quote, "quote is null"); + } + + public String buildSql(int dbtype, String catalog, String schema, String table, List columns, TupleDomain tupleDomain) + { + StringBuilder sql = new StringBuilder(); + + sql.append("SELECT "); + Joiner.on(", ").appendTo(sql, transform(columns, column -> quote(column.getColumnName()))); + if (columns.isEmpty()) { + sql.append("null"); + } + + sql.append(" FROM "); + if (!isNullOrEmpty(catalog)) { + sql.append(quote(catalog)).append('.'); + } + if (!isNullOrEmpty(schema)) { + sql.append(quote(schema)).append('.'); + } + sql.append(quote(table)); + if (dbtype == BaseJdbcClient.TYPE_SQLSERVER) { + sql.append(" WITH(NOLOCK) "); + } + + List clauses = toConjuncts(columns, tupleDomain); + if (!clauses.isEmpty()) { + sql.append(" WHERE ") + .append(Joiner.on(" AND ").join(clauses)); + } + + return sql.toString(); + } + + private List toConjuncts(List columns, TupleDomain tupleDomain) + { + ImmutableList.Builder builder = ImmutableList.builder(); + for (JdbcColumnHandle column : columns) { + Type type = column.getColumnType(); + if (type.equals(BigintType.BIGINT) || type.equals(DoubleType.DOUBLE) || type.equals(BooleanType.BOOLEAN) + || type.equals(VarcharType.VARCHAR) || type.equals(DateType.DATE) || type.equals(TimestampType.TIMESTAMP)) { + Domain domain = tupleDomain.getDomains().get(column); + if (domain != null) { + builder.add(toPredicate(column.getColumnName(), domain, type)); + } + } + } + return builder.build(); + } + + private String toPredicate(String columnName, Domain domain, Type columnType) + { + if (domain.getRanges().isNone() && domain.isNullAllowed()) { + return quote(columnName) + " IS NULL"; + } + + if (domain.getRanges().isAll() && !domain.isNullAllowed()) { + return quote(columnName) + " IS NOT NULL"; + } + + // Add disjuncts for ranges + List disjuncts = new ArrayList<>(); + List singleValues = new ArrayList<>(); + for (Range range : domain.getRanges()) { + checkState(!range.isAll()); // Already checked + if (range.isSingleValue()) { + singleValues.add(range.getLow().getValue()); + } + else { + List rangeConjuncts = new ArrayList<>(); + if (!range.getLow().isLowerUnbounded()) { + switch (range.getLow().getBound()) { + case ABOVE: + rangeConjuncts.add(toPredicate(columnName, ">", range.getLow().getValue(), columnType)); + break; + case EXACTLY: + rangeConjuncts.add(toPredicate(columnName, ">=", range.getLow().getValue(), columnType)); + break; + case BELOW: + throw new IllegalArgumentException("Low Marker should never use BELOW bound: " + range); + default: + throw new AssertionError("Unhandled bound: " + range.getLow().getBound()); + } + } + if (!range.getHigh().isUpperUnbounded()) { + switch (range.getHigh().getBound()) { + case ABOVE: + throw new IllegalArgumentException("High Marker should never use ABOVE bound: " + range); + case EXACTLY: + rangeConjuncts.add(toPredicate(columnName, "<=", range.getHigh().getValue(), columnType)); + break; + case BELOW: + rangeConjuncts.add(toPredicate(columnName, "<", range.getHigh().getValue(), columnType)); + break; + default: + throw new AssertionError("Unhandled bound: " + range.getHigh().getBound()); + } + } + // If rangeConjuncts is null, then the range was ALL, which should already have been checked for + checkState(!rangeConjuncts.isEmpty()); + disjuncts.add("(" + Joiner.on(" AND ").join(rangeConjuncts) + ")"); + } + } + + // Add back all of the possible single values either as an equality or an IN predicate + if (singleValues.size() == 1) { + disjuncts.add(toPredicate(columnName, "=", getOnlyElement(singleValues), columnType)); + } + else if (singleValues.size() > 1) { + ImmutableList.Builder inListBuilder = ImmutableList.builder(); + singleValues.stream().forEach(value -> inListBuilder.add(encode(value, columnType))); + disjuncts.add(quote(columnName) + " IN (" + Joiner.on(",").join(inListBuilder.build()) + ")"); + } + + // Add nullability disjuncts + checkState(!disjuncts.isEmpty()); + if (domain.isNullAllowed()) { + disjuncts.add(quote(columnName) + " IS NULL"); + } + + return "(" + Joiner.on(" OR ").join(disjuncts) + ")"; + } + + private String toPredicate(String columnName, String operator, Object value, Type columnType) + { + return quote(columnName) + " " + operator + " " + encode(value, columnType); + } + + private String quote(String name) + { + name = name.replace(quote, quote + quote); + return quote + name + quote; + } + + private static String encode(Object value, Type columnType) + { + if (value instanceof Number || value instanceof Boolean) { + if (columnType.equals(DateType.DATE)) { + return "'" + new SimpleDateFormat("yyyy-MM-dd").format(new Date(86400000 * Long.parseLong(value.toString(), 10))) + "'"; + } + else if (columnType.equals(TimestampType.TIMESTAMP)) { + return "'" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date(Long.parseLong(value.toString(), 10))) + "'"; + } + return value.toString(); + } + else if (value instanceof Slice) { + return "'" + ((Slice) value).toStringUtf8() + "'"; + } + throw new UnsupportedOperationException("Can't handle type: " + value.getClass().getName()); + } +} diff --git a/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/Types.java b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/Types.java new file mode 100644 index 00000000..e33d3e28 --- /dev/null +++ b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/Types.java @@ -0,0 +1,33 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public final class Types +{ + private Types() {} + + public static B checkType(A value, Class target, String name) + { + checkNotNull(value, "%s is null", name); + checkArgument(target.isInstance(value), + "%s must be of type %s, not %s", + name, + target.getName(), + value.getClass().getName()); + return target.cast(value); + } +} diff --git a/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/cache/JdbcCacheConfig.java b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/cache/JdbcCacheConfig.java new file mode 100644 index 00000000..9a51871b --- /dev/null +++ b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/cache/JdbcCacheConfig.java @@ -0,0 +1,89 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc.cache; + +import io.airlift.configuration.Config; +import io.airlift.units.Duration; + +import java.util.concurrent.TimeUnit; + +public class JdbcCacheConfig +{ + public static final String DEFAULT_VALUE = "NA"; + private String cacheTableConfig = DEFAULT_VALUE; + private String cacheTableClause; + private Duration cacheRefreshInterval = new Duration(5, TimeUnit.MINUTES); + private Duration cacheExpireInterval = new Duration(5, TimeUnit.MINUTES); + private boolean jdbcCacheEnable = false; + + public String getCacheTableConfig() + { + return cacheTableConfig; + } + + @Config("jdbc-cache-table-config") + public JdbcCacheConfig setCacheTableConfig(String cacheTableConfig) + { + this.cacheTableConfig = cacheTableConfig; + return this; + } + + public String getCacheTableClause() + { + return cacheTableClause; + } + + @Config("jdbc-cache-table-clause") + public JdbcCacheConfig setCacheTableClause(String cacheTableClause) + { + this.cacheTableClause = cacheTableClause; + return this; + } + + public Duration getCacheRefreshInterval() + { + return cacheRefreshInterval; + } + + @Config("jdbc-cache-refresh-interval") + public JdbcCacheConfig setCacheRefreshInterval(Duration cacheRefreshInterval) + { + this.cacheRefreshInterval = cacheRefreshInterval; + return this; + } + + public Duration getCacheExpireInterval() + { + return cacheExpireInterval; + } + + @Config("jdbc-cache-expire-interval") + public JdbcCacheConfig setCacheExpireInterval(Duration cacheExpireInterval) + { + this.cacheExpireInterval = cacheExpireInterval; + return this; + } + + public boolean getJdbcCacheEnable() + { + return jdbcCacheEnable; + } + + @Config("jdbc-cache-enable") + public JdbcCacheConfig setJdbcCacheEnable(boolean jdbcCacheEnable) + { + this.jdbcCacheEnable = jdbcCacheEnable; + return this; + } +} diff --git a/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/cache/JdbcCacheSplit.java b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/cache/JdbcCacheSplit.java new file mode 100644 index 00000000..5203f3c9 --- /dev/null +++ b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/cache/JdbcCacheSplit.java @@ -0,0 +1,98 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc.cache; + +import java.util.Objects; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class JdbcCacheSplit +{ + private final String connectorId; + private final String catalogName; + private final String schemaName; + private final String tableName; + private final String baseTableName; + private final String connectionUrl; + + public JdbcCacheSplit(String connectorId, String catalogName, + String schemaName, String tableName, String connectionUrl, String baseTableName) + { + this.connectorId = checkNotNull(connectorId, "connector id is null"); + this.catalogName = catalogName; + this.schemaName = schemaName; + this.tableName = checkNotNull(tableName, "table name is null"); + this.connectionUrl = checkNotNull(connectionUrl, "connectionUrl is null"); + this.baseTableName = checkNotNull(baseTableName, "table name is null"); + } + + public String getConnectorId() + { + return connectorId; + } + + public String getCatalogName() + { + return catalogName == null ? "null" : catalogName; + } + + public String getSchemaName() + { + return schemaName == null ? "null" : schemaName; + } + + public String getTableName() + { + return tableName; + } + + public String getBaseTableName() + { + return baseTableName; + } + + public String getConnectionUrl() + { + return connectionUrl; + } + @Override + public int hashCode() + { + return Objects.hash(getConnectorId(), getConnectionUrl(), getCatalogName(), getSchemaName(), getTableName()); + } + @Override + public boolean equals(Object obj) + { + if (obj instanceof JdbcCacheSplit) { + JdbcCacheSplit other = (JdbcCacheSplit) obj; + return this.getConnectorId().equals(other.getConnectorId()) + && this.getConnectionUrl().equals(other.getConnectionUrl()) + && this.getCatalogName().equals(other.getCatalogName()) + && this.getSchemaName().equals(other.getSchemaName()) + && this.getTableName().equals(other.getTableName()); + } + else { + return this.hashCode() == obj.hashCode(); + } + } + @Override + public String toString() + { + return getConnectorId() + "," + + getConnectionUrl() + "," + + getCatalogName() + "," + + getSchemaName() + "," + + getTableName(); + } +} diff --git a/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/cache/JdbcJavaBean.java b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/cache/JdbcJavaBean.java new file mode 100644 index 00000000..9633875c --- /dev/null +++ b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/cache/JdbcJavaBean.java @@ -0,0 +1,42 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc.cache; + +import java.util.List; + +public class JdbcJavaBean +{ + private List columns; + private Object[] values; + public JdbcJavaBean(List columns) + { + this.columns = columns; + this.values = new Object[columns.size()]; + } + + public Object getFieldObjectValue(int index) + { + return values[index]; + } + + public void setFieldObjectValue(int index, Object value) + { + values[index] = value; + } + + public List getColumns() + { + return columns; + } +} diff --git a/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/cache/JdbcResultCache.java b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/cache/JdbcResultCache.java new file mode 100644 index 00000000..2c1caf93 --- /dev/null +++ b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/cache/JdbcResultCache.java @@ -0,0 +1,227 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc.cache; + +import io.airlift.log.Logger; + +import java.io.IOException; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.Driver; +import java.sql.Statement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import com.facebook.presto.plugin.jdbc.util.JdbcUtil; +import com.facebook.presto.spi.type.BigintType; +import com.facebook.presto.spi.type.BooleanType; +import com.facebook.presto.spi.type.DateType; +import com.facebook.presto.spi.type.DoubleType; +import com.facebook.presto.spi.type.TimeType; +import com.facebook.presto.spi.type.TimestampType; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.VarbinaryType; +import com.facebook.presto.spi.type.VarcharType; +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Joiner; +import com.google.common.base.Throwables; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Strings.isNullOrEmpty; +import static java.util.Locale.ENGLISH; + +public class JdbcResultCache +{ + private final LoadingCache> jdbcResultCache; + private final String identifierQuote; + private final Driver driver; + private final Properties connectionProperties; + private static final Logger log = Logger.get(JdbcResultCache.class); + private List tableList = new ArrayList(); + private HashMap> fieldList = new HashMap>(); + private LinkedHashMap cacheTableClauseMap; + + public JdbcResultCache(String identifierQuote, + Driver driver, + Properties connectionProperties, + JdbcCacheConfig cacheConfig) + { + this.identifierQuote = identifierQuote; + this.driver = driver; + this.connectionProperties = connectionProperties; + long expiresAfterWrite = checkNotNull(cacheConfig.getCacheExpireInterval(), "cacheExpireInterval is null").toMillis(); + long refreshAfterWrite = checkNotNull(cacheConfig.getCacheRefreshInterval(), "cacheRefreshInterval is null").toMillis(); + analyseCacheTableAndField(cacheConfig.getCacheTableConfig(), cacheConfig.getCacheTableClause()); + jdbcResultCache = CacheBuilder + .newBuilder() + .expireAfterWrite(expiresAfterWrite, TimeUnit.MILLISECONDS) + .refreshAfterWrite(refreshAfterWrite, TimeUnit.MILLISECONDS) + .build(new CacheLoader>(){ + @Override + public List load(JdbcCacheSplit key) throws Exception + { + return loadTableDataSet(key); + } + }); + } + private List loadTableDataSet(JdbcCacheSplit key) + { + log.debug("loadTableDataSet key : " + key); + List list = new ArrayList(); + try { + Connection connection = getConnection(key.getConnectionUrl()); + HashMap types = getColumnTypes(key); + String tableName = key.getBaseTableName(); + List columns = fieldList.get(tableName); + String columnPart = Joiner.on(",").join(columns); + String sql = "SELECT " + columnPart + " FROM " + + JdbcUtil.getTableName(identifierQuote, key.getCatalogName(), key.getSchemaName(), key.getTableName()); + if (cacheTableClauseMap != null && !isNullOrEmpty(cacheTableClauseMap.get(tableName))) { + sql += " WHERE " + cacheTableClauseMap.get(tableName); + } + Statement statement = connection.createStatement(); + statement.setFetchSize(10_000); + long startTime = System.currentTimeMillis(); + ResultSet resultSet = statement.executeQuery(sql); + log.debug("The connection url: %s ,ExecuteQuery: %s ,spend time : %s , thread id : %s", key.getConnectionUrl(), sql, (System.currentTimeMillis() - startTime), Thread.currentThread().getId()); + while (resultSet.next()) { + JdbcJavaBean tableDataSet = new JdbcJavaBean(columns); + for (int i = 1; i <= columns.size(); i++) { + Type type = types.get(columns.get(i - 1)); + if (type.equals(BooleanType.BOOLEAN)) { + tableDataSet.setFieldObjectValue((i - 1), resultSet.getBoolean(i)); + } + else if (type.equals(BigintType.BIGINT)) { + tableDataSet.setFieldObjectValue((i - 1), resultSet.getLong(i)); + } + else if (type.equals(DateType.DATE)) { + tableDataSet.setFieldObjectValue((i - 1), resultSet.getDate(i)); + } + else if (type.equals(TimeType.TIME)) { + tableDataSet.setFieldObjectValue((i - 1), resultSet.getTime(i)); + } + else if (type.equals(TimestampType.TIMESTAMP)) { + tableDataSet.setFieldObjectValue((i - 1), resultSet.getTimestamp(i)); + } + else if (type.equals(DoubleType.DOUBLE)) { + tableDataSet.setFieldObjectValue((i - 1), resultSet.getDouble(i)); + } + else if (type.equals(VarcharType.VARCHAR)) { + tableDataSet.setFieldObjectValue((i - 1), resultSet.getString(i)); + } + else if (type.equals(VarbinaryType.VARBINARY)) { + tableDataSet.setFieldObjectValue((i - 1), resultSet.getBytes(i)); + } + } + list.add(tableDataSet); + } + log.debug("The connection url: %s ,parse result: %s ,spend time : %s , thread id : %s", key.getConnectionUrl(), sql, (System.currentTimeMillis() - startTime), Thread.currentThread().getId()); + } + catch (SQLException e) { + throw Throwables.propagate(e); + } + return list; + } + public List getResult(JdbcCacheSplit key) + { + try { + return jdbcResultCache.get(key); + } + catch (ExecutionException e) { + throw Throwables.propagate(e); + } + } + + public HashMap getColumnTypes(JdbcCacheSplit key) + { + HashMap types = new HashMap(); + try (Connection connection = getConnection(key.getConnectionUrl())) { + DatabaseMetaData metadata = connection.getMetaData(); + try (ResultSet resultSet = metadata.getColumns(key.getSchemaName(), key.getCatalogName(), key.getTableName(), null)) { + while (resultSet.next()) { + Type columnType = JdbcUtil.toPrestoType(resultSet.getInt("DATA_TYPE")); + if (columnType != null) { + String columnName = resultSet.getString("COLUMN_NAME").toLowerCase(ENGLISH); + types.put(columnName, columnType); + } + } + } + } + catch (SQLException e) { + throw Throwables.propagate(e); + } + return types; + } + + public Connection getConnection(String connectionURL) + throws SQLException + { + Connection connection = driver.connect(connectionURL, connectionProperties); + try { + connection.setReadOnly(true); + } + catch (SQLException e) { + connection.close(); + throw e; + } + return connection; + } + + private void analyseCacheTableAndField(String cacheTableConfig, String cacheTableClause) + { + ObjectMapper objectMapper = new ObjectMapper(); + try { + // table name and column field + List> readValue = objectMapper.readValue(cacheTableConfig.toLowerCase(ENGLISH), ArrayList.class); + for (LinkedHashMap map : readValue) { + for (String t : map.keySet()) { + tableList.add(t); + ArrayList object = (ArrayList) map.get(t); + fieldList.put(t, object); + } + } + if (!isNullOrEmpty(cacheTableClause)) { + // table where condition + cacheTableClauseMap = (LinkedHashMap) objectMapper.readValue(cacheTableClause, Map.class); + } + } + catch (JsonParseException e) { + throw Throwables.propagate(e); + } + catch (JsonMappingException e) { + throw Throwables.propagate(e); + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } + + public boolean isCacheTable(String tableName) + { + return tableList.contains(tableName); + } +} diff --git a/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/subtable/JdbcLoadTread.java b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/subtable/JdbcLoadTread.java new file mode 100644 index 00000000..6005e34c --- /dev/null +++ b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/subtable/JdbcLoadTread.java @@ -0,0 +1,255 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc.subtable; + +import static java.util.Locale.ENGLISH; +import io.airlift.log.Logger; +import io.airlift.units.Duration; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.apache.commons.dbutils.DbUtils; +import org.apache.commons.dbutils.QueryRunner; +import org.apache.commons.dbutils.ResultSetHandler; + +import com.facebook.presto.plugin.jdbc.JdbcSplit; +import com.facebook.presto.plugin.jdbc.subtable.PdboTableInfo.DBType; +import com.facebook.presto.plugin.jdbc.util.JdbcUtil; +import com.facebook.presto.plugin.jdbc.util.PdboMetadata; +import com.facebook.presto.server.PrestoServer; +import com.mysql.jdbc.Driver; + +public class JdbcLoadTread implements Runnable +{ + private static final Logger log = Logger.get(JdbcLoadTread.class); + + protected final String connectionUrl; + protected final Properties connectionProperties; + protected final String connectorId; + protected final Duration jdbcReloadSubtableInterval; + private long lastLoadSubTableTimeStamp = 0L; + protected final Driver driver; + private final boolean jdbcSubTableAllocator; + + private final ConcurrentMap> pdboTables = new ConcurrentHashMap<>(); + + public JdbcLoadTread(String connectionUrl, + Properties connectionProperties, + String connectorId, + Duration jdbcReloadSubtableInterval) throws SQLException + { + this.connectionUrl = connectionUrl; + this.connectionProperties = connectionProperties; + this.connectorId = connectorId; + this.jdbcReloadSubtableInterval = jdbcReloadSubtableInterval; + this.driver = new Driver(); + this.jdbcSubTableAllocator = PrestoServer.isCoordinator(); + } + + public void run() + { + while (jdbcSubTableAllocator) { + try { + if (lastLoadSubTableTimeStamp == 0) { + loadPdboTableInfo(); + } + else { + Thread.sleep(jdbcReloadSubtableInterval.toMillis()); + long curTime = System.currentTimeMillis(); + loadPdboTableInfo(); + log.debug(connectorId + " load sub-table info spend time : " + (System.currentTimeMillis() - curTime) + " ms "); + } + lastLoadSubTableTimeStamp = System.currentTimeMillis(); + } + catch (Exception e) { + lastLoadSubTableTimeStamp = System.currentTimeMillis(); + log.error(e, connectorId + " Error reloading sub-table infomation : %s" , e.getMessage()); + } + } + } + + public synchronized void loadPdboTableInfo() + { + String sql = PdboMetadata.getPdboTableInfoSQL(); + Connection conn = getConnection(); + QueryRunner runner = new QueryRunner(); + try { + runner.query(conn, sql, new PdboTableResultHandle(), connectorId); + } + catch (SQLException e) { + log.error(e, "loadPdboTableInfo Execute %s error : %s" , sql, e.getMessage()); + } + finally { + DbUtils.closeQuietly(conn); + } + } + + public List getPDBOLogs(String connectorId, String schemaName, String tableName) + { + String sql = PdboMetadata.getPdboLogsSQL(); + Connection conn = getConnection(); + QueryRunner runner = new QueryRunner(); + List pdboSplits = null; + try { + pdboSplits = runner.query(conn, sql, new PdboLogsResultHandle(), connectorId, schemaName, tableName); + } + catch (SQLException e) { + log.error(e, "getPDBOLogs Execute %s error : %s" , sql, e.getMessage()); + } + finally { + DbUtils.closeQuietly(conn); + } + return pdboSplits; + } + + private class PdboLogsResultHandle implements ResultSetHandler> + { + @Override + public List handle(ResultSet rs) throws SQLException + { + List tables = new ArrayList<>(); + int scannodenumber = 0; + //A.CONNECTORID,A.SCHEMANAME,A.TABLENAME,A.ROWS,A.BEGININDEX,A.ENDINDEX,B.DBTYPE, + //C.DBHOST,C.DBPORT,C.CONNECTION_PROPERTIES,C.PRESTO_WORK_HOST,C.REMOTELYACCESSIBLE,C.SPLITFIELD, + //C.SCANNODENUMBER,D.USERNAME,D.PASSWORD,A.CONTROL_SCAN_CONCURRENCY_ENABLED,A.SCAN_CONCURRENCY_COUNT + while (rs.next()) { + scannodenumber = rs.getInt(14); + tables.add(new PdboSplit().setConnectorId(rs.getString(1)). + setSchemaName(rs.getString(2)). + setTableName(rs.getString(3)). + setRows(rs.getLong(4)). + setBeginIndex(rs.getLong(5)). + setEndIndex(rs.getLong(6)). + setDbHost(rs.getString(8)). + setConnectionUrl(getConnectionURL(rs.getString(7), rs.getString(8), rs.getString(9), rs.getString(10))). + setPrestoWorkHost(rs.getString(11)). + setRemotelyAccessible(rs.getString(12)). + setSplitField(rs.getString(13)). + setScanNodes(rs.getInt(14)). + setUsername(JdbcUtil.Base64Decode(rs.getString(15))). + setPassword(JdbcUtil.Base64Decode(rs.getString(16))). + setCalcStepEnable("Y"). + setControlScanConcurrencyEnabled(rs.getString(17)). + setScanConcurrencyCount(rs.getInt(18))); + } + if (scannodenumber != tables.size()) { + tables.clear(); + loadPdboTableInfo(); + } + return tables; + } + } + + private class PdboTableResultHandle implements ResultSetHandler + { + @Override + public String handle(ResultSet rs) throws SQLException + { + pdboTables.clear(); + //CONNECTORID,PRESTO_SCHEMA,PRESTO_TABLE,DBTYPE,PDBOENABLE,CONTROL_SCAN_CONCURRENCY_ENABLED,SCAN_CONCURRENCY_COUNT + //DBHOST,DBPORT,CONNECTION_PROPERTIES,SOURCE_SCHEMA,SOURCE_TABLE,SPLITFIELD,REMOTELYACCESSIBLE,PRESTO_WORK_HOST,SCANNODENUMBER, + //FIELDMAXVALUE,FIELDMINVALUE,USERNAME,PASSWORD," + while (rs.next()) { + PdboTableInfo table = new PdboTableInfo(rs.getString(1).toLowerCase(ENGLISH), + rs.getString(2).toLowerCase(ENGLISH), rs.getString(3).toLowerCase(ENGLISH)); + table.setDbType(rs.getString(4)); + table.setCalcStepEnable(rs.getString(5)); + table.setControlScanConcurrencyEnabled(rs.getString(6)); + table.setScanConcurrencyCount(rs.getInt(7)); + String connectionUrl = getConnectionURL(table.getDbType(), rs.getString(8), rs.getString(9), rs.getString(10)); + PdboSplit pdboSplit = new PdboSplit().setSchemaName(rs.getString(11).toLowerCase(ENGLISH)). + setTableName(rs.getString(12).toLowerCase(ENGLISH)). + setDbHost(rs.getString(8)). + setConnectionUrl(connectionUrl). + setSplitField(rs.getString(13)). + setRemotelyAccessible(rs.getString(14)). + setPrestoWorkHost(rs.getString(15)). + setScanNodes(rs.getInt(16)). + setFieldMaxValue(rs.getLong(17)). + setFieldMinValue(rs.getLong(18)). + setUsername(JdbcUtil.Base64Decode(rs.getString(19))). + setPassword(JdbcUtil.Base64Decode(rs.getString(20))). + setCalcStepEnable(rs.getString(5)). + setControlScanConcurrencyEnabled(rs.getString(6)). + setScanConcurrencyCount(rs.getInt(7)); + ArrayList routeList = pdboTables.get(table); + if (routeList == null) { + routeList = new ArrayList<>(); + } + routeList.add(pdboSplit); + pdboTables.put(table, routeList); + } + return null; + } + } + + private String getConnectionURL(String dbType, String dbHost, String dbPort, String connectionProperties) + throws SQLException + { + String connectionUrl = ""; + if (dbType.equals(DBType.MYSQL.toString())) { + connectionUrl = "jdbc:mysql://"; + } + else if (dbType.equals(DBType.SQLSERVER.toString())) { + connectionUrl = "jdbc:jtds:sqlserver://"; + } + else if (dbType.equals(DBType.ORACLE.toString())) { + connectionUrl = "jdbc:oracle:thin:@"; + } + connectionUrl += dbHost + ":" + dbPort + connectionProperties; + return connectionUrl; + } + + public ConcurrentMap> getPdboTableInfo() + { + return pdboTables; + } + + public void commitPdboLogs(JdbcSplit split, long rowCount) + { + Connection conn = getConnection(); + QueryRunner runner = new QueryRunner(); + String insertSql = PdboMetadata.getInsertPdboLogSQL(split, rowCount, connectorId); + String updateSql = PdboMetadata.getUpdatePdboHistoryLogSQL(split, connectorId); + try { + runner.update(conn, updateSql); + runner.update(conn, insertSql); + } + catch (SQLException e) { + log.error(e, "insert sql : %s,update sql : %s commitPdboLogs error : %s", insertSql, updateSql, e.getMessage()); + } + finally { + DbUtils.closeQuietly(conn); + } + } + + public Connection getConnection() + { + Connection conn = null; + try { + conn = driver.connect(connectionUrl, connectionProperties); + } + catch (SQLException e) { + log.error("Connect pdbo db error : " + e.getMessage()); + } + return conn; + } +} diff --git a/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/subtable/JdbcSubTableConfig.java b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/subtable/JdbcSubTableConfig.java new file mode 100644 index 00000000..b650e7d4 --- /dev/null +++ b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/subtable/JdbcSubTableConfig.java @@ -0,0 +1,90 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc.subtable; + +import io.airlift.configuration.Config; +import io.airlift.units.Duration; + +import java.util.concurrent.TimeUnit; + +public class JdbcSubTableConfig +{ + public static final String DEFAULT_VALUE = "NA"; + + private String jdbcSubTableConnectionURL; + private String jdbcSubTableConnectionUser; + private String jdbcSubTableConnectionPassword; + private Duration jdbcReloadSubtableInterval = new Duration(5, TimeUnit.MINUTES); + private boolean jdbcSubTableEnable = false; + + public String getJdbcSubTableConnectionURL() + { + return jdbcSubTableConnectionURL; + } + + @Config("jdbc-sub-table-connection-url") + public JdbcSubTableConfig setJdbcSubTableConnectionURL(String connectionUrl) + { + this.jdbcSubTableConnectionURL = connectionUrl; + return this; + } + + public String getJdbcSubTableConnectionUser() + { + return jdbcSubTableConnectionUser; + } + + @Config("jdbc-sub-table-connection-user") + public JdbcSubTableConfig setJdbcSubTableConnectionUser(String connectionUser) + { + this.jdbcSubTableConnectionUser = connectionUser; + return this; + } + + public String getJdbcSubTableConnectionPassword() + { + return jdbcSubTableConnectionPassword; + } + + @Config("jdbc-sub-table-connection-password") + public JdbcSubTableConfig setJdbcSubTableConnectionPassword(String connectionPassword) + { + this.jdbcSubTableConnectionPassword = connectionPassword; + return this; + } + + public Duration getJdbcReloadSubtableInterval() + { + return jdbcReloadSubtableInterval; + } + + @Config("jdbc-reload-subtable-interval") + public JdbcSubTableConfig setJdbcReloadSubtableInterval(Duration jdbcReloadSubtableInterval) + { + this.jdbcReloadSubtableInterval = jdbcReloadSubtableInterval; + return this; + } + + public boolean getJdbcSubTableEnable() + { + return jdbcSubTableEnable; + } + + @Config("jdbc-sub-table-enable") + public JdbcSubTableConfig setJdbcSubTableEnable(boolean jdbcSubTableEnable) + { + this.jdbcSubTableEnable = jdbcSubTableEnable; + return this; + } +} diff --git a/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/subtable/JdbcSubTableManager.java b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/subtable/JdbcSubTableManager.java new file mode 100644 index 00000000..314032e5 --- /dev/null +++ b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/subtable/JdbcSubTableManager.java @@ -0,0 +1,297 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc.subtable; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Strings.isNullOrEmpty; +import static com.google.common.collect.Maps.fromProperties; +import static java.util.Locale.ENGLISH; +import io.airlift.log.Logger; + +import java.sql.Connection; +import java.sql.Driver; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +import com.facebook.presto.plugin.jdbc.JdbcPartition; +import com.facebook.presto.plugin.jdbc.JdbcSplit; +import com.facebook.presto.plugin.jdbc.JdbcTableHandle; +import com.facebook.presto.plugin.jdbc.util.JdbcUtil; +import com.facebook.presto.spi.ConnectorSplitSource; +import com.facebook.presto.spi.HostAddress; +import com.google.common.collect.ImmutableList; + +public class JdbcSubTableManager +{ + private static final Logger log = Logger.get(JdbcSubTableManager.class); + + protected final String connectorId; + protected final String identifierQuote; + protected final Driver driver; + protected final String defaultConnectionUrl; + protected final Properties defaultConnectionProperties; + protected final String jdbcSubTableConnectionUrl; + protected final Properties jdbcSubTableConnectionProperties; + + private JdbcLoadTread loadTread; + + public JdbcSubTableManager(String connectorId, + String identifierQuote, + Driver driver, + String defaultConnectionUrl, + Properties defaultConnectionProperties, + JdbcSubTableConfig config) + { + this.connectorId = checkNotNull(connectorId, "connectorId is null").toString(); + this.identifierQuote = checkNotNull(identifierQuote, "identifierQuote is null"); + this.driver = checkNotNull(driver, "driver is null"); + this.defaultConnectionUrl = defaultConnectionUrl; + this.defaultConnectionProperties = defaultConnectionProperties; + + checkNotNull(config, "config is null"); + jdbcSubTableConnectionUrl = config.getJdbcSubTableConnectionURL(); + jdbcSubTableConnectionProperties = new Properties(); + jdbcSubTableConnectionProperties.setProperty("user", config.getJdbcSubTableConnectionUser()); + jdbcSubTableConnectionProperties.setProperty("password", config.getJdbcSubTableConnectionPassword()); + + if (config.getJdbcSubTableEnable()) { + try { + loadTread = new JdbcLoadTread(config.getJdbcSubTableConnectionURL(), jdbcSubTableConnectionProperties, + connectorId, config.getJdbcReloadSubtableInterval()); + } + catch (SQLException e) { + log.error("Init JdbcLoadTread error", e); + } + Thread loadTableThread = new Thread(loadTread); + loadTableThread.setName("LoadTableThread"); + loadTableThread.setDaemon(true); + loadTableThread.start(); + } + } + + /** + * Get table splits + * @param jdbcPartition + * @return + */ + public ConnectorSplitSource getTableSplits(JdbcPartition jdbcPartition) + { + JdbcTableHandle jdbcTableHandle = jdbcPartition.getJdbcTableHandle(); + List jdbcSplitsList = new ArrayList(); + String schemaName = getSchemaName(jdbcTableHandle); + PdboTableInfo key = new PdboTableInfo(connectorId, schemaName, jdbcTableHandle.getTableName().toLowerCase(ENGLISH)); + ArrayList pdboSplits = loadTread.getPdboTableInfo().get(key); + if (JdbcUtil.checkListNullOrEmpty(pdboSplits)) { + if (pdboSplits == null) { + pdboSplits = new ArrayList(); + } + PdboSplit config = new PdboSplit(); + config.setConnectionUrl(defaultConnectionUrl); + config.setConnectorId(jdbcTableHandle.getCatalogName()); + config.setSchemaName(jdbcTableHandle.getSchemaName()); + config.setTableName(jdbcTableHandle.getTableName()); + config.setRemotelyAccessible("Y"); + config.setPrestoTableName(jdbcTableHandle.getTableName()); + pdboSplits.add(config); + } + long timeStamp = System.nanoTime(); + if (pdboSplits.get(0).isCalcStepEnable()) { + jdbcSplitsList = getTableSplitsFromPdboLog(connectorId, schemaName, jdbcTableHandle.getTableName().toLowerCase(ENGLISH), jdbcPartition, timeStamp); + } + if (JdbcUtil.checkListNullOrEmpty(jdbcSplitsList)) { + for (PdboSplit config : pdboSplits) { + constructJdbcSplits(jdbcPartition, jdbcSplitsList, config, timeStamp); + } + } + return new PdboSplitSource(connectorId, jdbcSplitsList, pdboSplits.get(0).getControlScanConcurrencyEnabled(), pdboSplits.get(0).getScanConcurrencyCount()); + } + + private String getSchemaName(JdbcTableHandle jdbcTableHandle) + { + String schemaName = ""; + if (defaultConnectionUrl.indexOf("mysql") != -1) { + schemaName = jdbcTableHandle.getCatalogName().toLowerCase(ENGLISH); + } + else { + schemaName = jdbcTableHandle.getSchemaName().toLowerCase(ENGLISH); + } + return schemaName; + } + + private void constructJdbcSplits(JdbcPartition jdbcPartition, + List splits, PdboSplit config, long timeStamp) + { + List addresses = getSplitHost(config.getPrestoWorkHost()); + Properties connectionProperties = resetConnectionProperties(config.getUsername(), config.getPassword()); + int scanNodes = config.getScanNodes() <= 0 ? 1 : config.getScanNodes(); + if (scanNodes == 1 || (scanNodes > 1 && isNullOrEmpty(config.getSplitField()))) { + addJdbcSplit(jdbcPartition, splits, addresses, new String[]{"", "", ""}, connectionProperties, timeStamp, scanNodes, config); + } + else { + splitTable(jdbcPartition, splits, config, addresses, connectionProperties, scanNodes, timeStamp); + } + } + + /** + * Splitting table by field or limit + */ + private void splitTable(JdbcPartition jdbcPartition, + List splits, PdboSplit config, + List addresses, Properties connectionProperties, + int scanNodes, long timeStamp) + { + long tableTotalRecords = 0L; + Long[] autoIncrementFieldMinAndMaxValue = new Long[2]; + autoIncrementFieldMinAndMaxValue = getSplitFieldMinAndMaxValue(config, connectionProperties); + tableTotalRecords = autoIncrementFieldMinAndMaxValue[0] - autoIncrementFieldMinAndMaxValue[1]; + long targetChunkSize = (long) Math.ceil(tableTotalRecords * 1.0 / scanNodes); + long chunkOffset = 0L; + long autoIncrementOffset = 0L; + while (chunkOffset < tableTotalRecords) { + long chunkLength = Math.min(targetChunkSize, tableTotalRecords - chunkOffset); + if (chunkOffset == 0) { + autoIncrementOffset = autoIncrementFieldMinAndMaxValue[1] - 1; + } + String[] splitInfo = getSplitInfo(chunkOffset, autoIncrementOffset, + autoIncrementFieldMinAndMaxValue, chunkLength, tableTotalRecords, config.getSplitField()); + addJdbcSplit(jdbcPartition, splits, addresses, splitInfo, connectionProperties, timeStamp, scanNodes, config); + chunkOffset += chunkLength; + autoIncrementOffset += chunkLength; + } + fillLastRecord(jdbcPartition, connectionProperties, splits, scanNodes, timeStamp, config.getFieldMaxValue(), config); + } + + /** + * If the table split by field,the filter conditions will follow like this : + * field > offset and field <= offset + chunkLength. + * @return splitInfo[0] : splitPart; splitInfo[1] : beginIndex; splitInfo[2] : endIndex; + */ + private String[] getSplitInfo(long chunkOffset, long autoIncrementOffset, + Long[] autoIncrementFieldMinAndMaxValue, long chunkLength, long tableTotalRecords, String splitField) + { + String[] splitInfo = new String[3]; + String splitPart = ""; + splitInfo[1] = String.valueOf(autoIncrementOffset); + splitPart = splitField + " > " + autoIncrementOffset + " and " + splitField + " <= "; + if ((chunkOffset + chunkLength) == tableTotalRecords) { + splitPart += autoIncrementFieldMinAndMaxValue[0]; + splitInfo[2] = String.valueOf(autoIncrementFieldMinAndMaxValue[0]); + } + else { + splitPart += (autoIncrementOffset + chunkLength); + splitInfo[2] = String.valueOf(autoIncrementOffset + chunkLength); + } + splitInfo[0] = splitPart; + return splitInfo; + } + + private void addJdbcSplit(JdbcPartition jdbcPartition, + List builder, List addresses, String[] splitInfo, + Properties connectionProperties, long timeStamp, int scanNodes, PdboSplit config) + { + builder.add(new JdbcSplit(connectorId, config.getConnectorId(), config.getSchemaName(), config.getTableName(), + config.getConnectionUrl(), fromProperties(connectionProperties), jdbcPartition.getTupleDomain(), + splitInfo[0], addresses, config.getRemotelyAccessible(), config.getPrestoTableName(), + config.getSplitField(), splitInfo[1], splitInfo[2], timeStamp, scanNodes, config.isCalcStepEnable(), config.getDbHost())); + } + + protected Long[] getSplitFieldMinAndMaxValue(PdboSplit conf, Properties connectionProperties) + { + Long[] value = new Long[2]; + if (conf.getFieldMaxValue() > 0) { + value[0] = conf.getFieldMaxValue(); + value[1] = conf.getFieldMinValue(); + return value; + } + String sql = "SELECT MAX(" + conf.getSplitField() + "),MIN(" + conf.getSplitField() + ") FROM " + + JdbcUtil.getTableName(identifierQuote, conf.getConnectorId(), conf.getSchemaName(), conf.getTableName()); + Connection connection = null; + Statement stat = null; + ResultSet rs = null; + try { + connection = driver.connect(conf.getConnectionUrl(), connectionProperties); + stat = connection.createStatement(); + rs = stat.executeQuery(sql.toString()); + while (rs.next()) { + value[0] = rs.getLong(1); + value[1] = rs.getLong(2); + } + } + catch (SQLException e) { + log.error("SQL : " + sql + ",getSplitFieldMinAndMaxValue error : " + e.getMessage()); + return null; + } + finally { + JdbcUtil.closeJdbcConnection(connection, stat, rs); + } + return value; + } + + public List getTableSplitsFromPdboLog(String catalogName, String schemaName, String tableName, + JdbcPartition jdbcPartition, long timeStamp) + { + List splits = new ArrayList<>(); + List pdboLogs = loadTread.getPDBOLogs(catalogName, schemaName, tableName); + if (JdbcUtil.checkListNullOrEmpty(pdboLogs)) { + return splits; + } + int scanNodes = pdboLogs.size(); + for (PdboSplit table : pdboLogs) { + table.setConnectorId(null); + List addresses = getSplitHost(table.getPrestoWorkHost()); + Properties connectionProperties = resetConnectionProperties(table.getUsername(), table.getPassword()); + String splitPart = table.getSplitField() + " > " + table.getBeginIndex() + " and " + + table.getSplitField() + " <= " + table.getEndIndex(); + addJdbcSplit(jdbcPartition, splits, addresses, + new String[]{splitPart, String.valueOf(table.getBeginIndex()), String.valueOf(table.getEndIndex())}, + connectionProperties, timeStamp, scanNodes, table); + } + PdboSplit lastRecord = pdboLogs.get(scanNodes - 1); + fillLastRecord(jdbcPartition, resetConnectionProperties(lastRecord.getUsername(), lastRecord.getPassword()), + splits, scanNodes, timeStamp, lastRecord.getEndIndex(), lastRecord); + return splits; + } + + private void fillLastRecord(JdbcPartition jdbcPartition, Properties connectionProperties, + List splits, int scanNodes, long timeStamp, long endIndex, PdboSplit config) + { + String splitPart = config.getSplitField() + " > " + endIndex; + addJdbcSplit(jdbcPartition, splits, getSplitHost(config.getPrestoWorkHost()), + new String[]{splitPart, "", ""}, connectionProperties, timeStamp, scanNodes, config); + } + + private Properties resetConnectionProperties(String username, String password) + { + Properties connectionProperties = (Properties) defaultConnectionProperties.clone(); + if (!isNullOrEmpty(username) && !isNullOrEmpty(password)) { + connectionProperties.setProperty("user", username); + connectionProperties.setProperty("password", password); + } + return connectionProperties; + } + + private List getSplitHost(String host) + { + return isNullOrEmpty(host) ? ImmutableList.of() : ImmutableList.of(HostAddress.fromString(host)); + } + + public void commitPdboLogs(JdbcSplit split, long rowCount) + { + loadTread.commitPdboLogs(split, rowCount); + } +} diff --git a/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/subtable/PdboSplit.java b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/subtable/PdboSplit.java new file mode 100644 index 00000000..4c815559 --- /dev/null +++ b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/subtable/PdboSplit.java @@ -0,0 +1,284 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc.subtable; + +import com.facebook.presto.plugin.jdbc.util.JdbcUtil; + +public class PdboSplit +{ + private String connectorId; + private String schemaName; + private String tableName; + private Long rows; + private Long beginIndex; + private Long endIndex; + private String recordFlag; + private String dbHost; + private String connectionUrl; + private String prestoWorkHost; + private String remotelyAccessible; + private String splitField; + private String username; + private String password; + private Long timeStamp; + private int scanNodes; + private long fieldMaxValue; + private long fieldMinValue; + private String prestoTableName; + private String calcStepEnable; + private String controlScanConcurrencyEnabled; + private int scanConcurrencyCount; + + public String getConnectorId() + { + return connectorId; + } + + public PdboSplit setConnectorId(String connectorId) + { + this.connectorId = connectorId; + return this; + } + + public String getSchemaName() + { + return schemaName; + } + + public PdboSplit setSchemaName(String schemaName) + { + this.schemaName = schemaName; + return this; + } + + public String getTableName() + { + return tableName; + } + + public PdboSplit setTableName(String tableName) + { + this.tableName = tableName; + return this; + } + + public Long getRows() + { + return rows; + } + + public PdboSplit setRows(Long rows) + { + this.rows = rows; + return this; + } + + public Long getBeginIndex() + { + return beginIndex; + } + + public PdboSplit setBeginIndex(Long beginIndex) + { + this.beginIndex = beginIndex; + return this; + } + + public Long getEndIndex() + { + return endIndex; + } + + public PdboSplit setEndIndex(Long endIndex) + { + this.endIndex = endIndex; + return this; + } + + public String getRecordFlag() + { + return recordFlag; + } + + public PdboSplit setRecordFlag(String recordFlag) + { + this.recordFlag = recordFlag; + return this; + } + + public String getDbHost() + { + return (dbHost == null || dbHost.length() == 0) ? "default" : dbHost; + } + + public PdboSplit setDbHost(String dbHost) + { + this.dbHost = dbHost; + return this; + } + + public String getConnectionUrl() + { + return connectionUrl; + } + + public PdboSplit setConnectionUrl(String connectionUrl) + { + this.connectionUrl = connectionUrl; + return this; + } + + public String getPrestoWorkHost() + { + return prestoWorkHost; + } + + public PdboSplit setPrestoWorkHost(String prestoWorkHost) + { + this.prestoWorkHost = prestoWorkHost; + return this; + } + + public boolean getRemotelyAccessible() + { + return JdbcUtil.converStringToBoolean(remotelyAccessible, false); + } + + public PdboSplit setRemotelyAccessible(String remotelyAccessible) + { + this.remotelyAccessible = remotelyAccessible; + return this; + } + + public String getUsername() + { + return username; + } + + public PdboSplit setUsername(String username) + { + this.username = username; + return this; + } + + public String getPassword() + { + return password; + } + + public PdboSplit setPassword(String password) + { + this.password = password; + return this; + } + + public String getSplitField() + { + return splitField; + } + + public PdboSplit setSplitField(String splitField) + { + this.splitField = splitField; + return this; + } + + public Long getTimeStamp() + { + return timeStamp; + } + + public PdboSplit setTimeStamp(Long timeStamp) + { + this.timeStamp = timeStamp; + return this; + } + + public int getScanNodes() + { + return scanNodes; + } + + public PdboSplit setScanNodes(int scanNodes) + { + this.scanNodes = scanNodes; + return this; + } + + public long getFieldMaxValue() + { + return fieldMaxValue; + } + + public PdboSplit setFieldMaxValue(long fieldMaxValue) + { + this.fieldMaxValue = fieldMaxValue; + return this; + } + + public long getFieldMinValue() + { + return fieldMinValue; + } + + public PdboSplit setFieldMinValue(long fieldMinValue) + { + this.fieldMinValue = fieldMinValue; + return this; + } + + public String getPrestoTableName() + { + return prestoTableName; + } + + public void setPrestoTableName(String prestoTableName) + { + this.prestoTableName = prestoTableName; + } + + public boolean isCalcStepEnable() + { + return JdbcUtil.converStringToBoolean(calcStepEnable, false); + } + + public PdboSplit setCalcStepEnable(String calcStepEnable) + { + this.calcStepEnable = calcStepEnable; + return this; + } + + public boolean getControlScanConcurrencyEnabled() + { + return JdbcUtil.converStringToBoolean(controlScanConcurrencyEnabled, false); + } + + public PdboSplit setControlScanConcurrencyEnabled( + String controlScanConcurrencyEnabled) + { + this.controlScanConcurrencyEnabled = controlScanConcurrencyEnabled; + return this; + } + + public int getScanConcurrencyCount() + { + return scanConcurrencyCount; + } + + public PdboSplit setScanConcurrencyCount(int scanConcurrencyCount) + { + this.scanConcurrencyCount = scanConcurrencyCount; + return this; + } +} diff --git a/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/subtable/PdboSplitSource.java b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/subtable/PdboSplitSource.java new file mode 100644 index 00000000..16af15ec --- /dev/null +++ b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/subtable/PdboSplitSource.java @@ -0,0 +1,129 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc.subtable; + +import com.facebook.presto.plugin.jdbc.JdbcSplit; +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.ConnectorSplitSource; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import static java.util.concurrent.CompletableFuture.completedFuture; + +public class PdboSplitSource + implements ConnectorSplitSource +{ + private final String dataSourceName; + private final List splits; + private int offset; + private final boolean controlScanConcurrencyEnabled; + private final int scanConcurrencyCount; + + public PdboSplitSource(String dataSourceName, Iterable splits, boolean controlScanConcurrencyEnabled, int scanConcurrencyCount) + { + this.dataSourceName = dataSourceName; + if (splits == null) { + throw new NullPointerException("splits is null"); + } + List splitsList = new ArrayList<>(); + sortSplitByDbHost(splits, controlScanConcurrencyEnabled, splitsList); + this.splits = Collections.unmodifiableList(splitsList); + this.controlScanConcurrencyEnabled = controlScanConcurrencyEnabled; + this.scanConcurrencyCount = scanConcurrencyCount; + } + + private void sortSplitByDbHost(Iterable splits, + boolean controlScanConcurrencyEnabled, + List splitsList) + { + Map> map = new HashMap>(); + if (controlScanConcurrencyEnabled) { + int splitSize = 0; + for (ConnectorSplit split : splits) { + if (split instanceof JdbcSplit) { + JdbcSplit jdbcSplit = (JdbcSplit) split; + List list = map.get(jdbcSplit.getDbHost()); + if (list == null) { + list = new ArrayList<>(); + } + list.add(split); + map.put(jdbcSplit.getDbHost(), list); + splitSize++; + } + } + int loopCount = 0; + while (loopCount < splitSize) { + for (String dbHost : map.keySet()) { + List list = map.get(dbHost); + if (list == null || list.isEmpty()) { + continue; + } + splitsList.add(list.get(0)); + loopCount++; + list.remove(0); + map.put(dbHost, list); + } + } + } + else { + for (ConnectorSplit split : splits) { + splitsList.add(split); + } + } + } + + @Override + public String getDataSourceName() + { + return dataSourceName; + } + + @Override + public CompletableFuture> getNextBatch(int maxSize) + { + int remainingSplits = splits.size() - offset; + int size = Math.min(remainingSplits, maxSize); + List results = splits.subList(offset, offset + size); + offset += size; + return completedFuture(results); + } + + @Override + public boolean isFinished() + { + return offset >= splits.size(); + } + + @Override + public void close() + { + } + + @Override + public boolean isControlScanConcurrencyEnabled() + { + return controlScanConcurrencyEnabled; + } + + @Override + public int getScanConcurrencyCount() + { + return scanConcurrencyCount; + } +} diff --git a/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/subtable/PdboTableInfo.java b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/subtable/PdboTableInfo.java new file mode 100644 index 00000000..3779d1e0 --- /dev/null +++ b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/subtable/PdboTableInfo.java @@ -0,0 +1,141 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc.subtable; + +import java.util.Objects; + +import com.facebook.presto.plugin.jdbc.util.JdbcUtil; + +public class PdboTableInfo +{ + enum DBType + { + MYSQL, + SQLSERVER, + ORACLE + } + private String tableId; + private String dbType; + private String connectorId; + private String prestoSchema; + private String prestoTable; + private String calcStepEnable; + private String controlScanConcurrencyEnabled; + private int scanConcurrencyCount; + + public PdboTableInfo(String connectorId, String prestoSchema, String prestoTable) + { + this.connectorId = connectorId; + this.prestoSchema = prestoSchema; + this.prestoTable = prestoTable; + } + public String getTableId() + { + return tableId; + } + + public void setTableId(String tableId) + { + this.tableId = tableId; + } + + public String getDbType() + { + return dbType; + } + + public void setDbType(String dbType) + { + this.dbType = dbType; + } + + public String getConnectorId() + { + return connectorId; + } + + public void setConnectorId(String connectorId) + { + this.connectorId = connectorId; + } + + public String getPrestoSchema() + { + return prestoSchema; + } + + public void setPrestoSchema(String prestoSchema) + { + this.prestoSchema = prestoSchema; + } + + public String getPrestoTable() + { + return prestoTable; + } + + public void setPrestoTable(String prestoTable) + { + this.prestoTable = prestoTable; + } + + public boolean isCalcStepEnable() + { + return JdbcUtil.converStringToBoolean(calcStepEnable, false); + } + + public void setCalcStepEnable(String calcStepEnable) + { + this.calcStepEnable = calcStepEnable; + } + + public boolean getControlScanConcurrencyEnabled() + { + return JdbcUtil.converStringToBoolean(controlScanConcurrencyEnabled, false); + } + + public void setControlScanConcurrencyEnabled( + String controlScanConcurrencyEnabled) + { + this.controlScanConcurrencyEnabled = controlScanConcurrencyEnabled; + } + + public int getScanConcurrencyCount() + { + return scanConcurrencyCount; + } + + public void setScanConcurrencyCount(int scanConcurrencyCount) + { + this.scanConcurrencyCount = scanConcurrencyCount; + } + + @Override + public int hashCode() + { + return Objects.hash(getConnectorId(), getPrestoSchema(), getPrestoTable()); + } + + @Override + public String toString() + { + return getConnectorId() + "-" + getPrestoSchema() + "-" + getPrestoTable(); + } + + @Override + public boolean equals(Object obj) + { + return this.hashCode() == obj.hashCode(); + } +} diff --git a/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/util/JdbcUtil.java b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/util/JdbcUtil.java new file mode 100644 index 00000000..2f85a843 --- /dev/null +++ b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/util/JdbcUtil.java @@ -0,0 +1,176 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc.util; + +import com.facebook.presto.spi.type.Type; +import io.airlift.log.Logger; + +import java.nio.charset.StandardCharsets; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Types; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.BooleanType.BOOLEAN; +import static com.facebook.presto.spi.type.DateType.DATE; +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; +import static com.facebook.presto.spi.type.TimeType.TIME; +import static com.facebook.presto.spi.type.TimestampType.TIMESTAMP; +import static com.facebook.presto.spi.type.VarbinaryType.VARBINARY; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static com.google.common.base.Strings.isNullOrEmpty; + +public final class JdbcUtil +{ + private static final Logger log = Logger.get(JdbcUtil.class); + + private JdbcUtil() + { + } + /** + * Get table name : catalog.schema.table + * @param catalog + * @param schema + * @param table + * @return + */ + public static String getTableName(String identifierQuote, String catalog, String schema, String table) + { + StringBuilder sql = new StringBuilder(); + if (!isNullOrEmpty(schema)) { + sql.append(quoted(identifierQuote, schema)).append('.'); + } + else { + if (!isNullOrEmpty(catalog)) { + sql.append(quoted(identifierQuote, catalog)).append('.'); + } + } + sql.append(quoted(identifierQuote, table)); + return sql.toString(); + } + + private static String quoted(String identifierQuote, String name) + { + name = name.replace(identifierQuote, identifierQuote + identifierQuote); + return identifierQuote + name + identifierQuote; + } + + /** + * close JDBC connection + * @param connection + * @param stat + * @param rs + */ + public static void closeJdbcConnection(Connection connection, Statement stat, ResultSet rs) + { + try { + if (rs != null) { + rs.close(); + } + if (stat != null) { + stat.close(); + } + if (connection != null) { + connection.close(); + } + } + catch (SQLException e) { + log.error("close connection error : " + e.getMessage()); + } + } + + public static Type toPrestoType(int jdbcType) + { + switch (jdbcType) { + case Types.BIT: + case Types.BOOLEAN: + return BOOLEAN; + case Types.TINYINT: + case Types.SMALLINT: + case Types.INTEGER: + case Types.BIGINT: + return BIGINT; + case Types.FLOAT: + case Types.REAL: + case Types.DOUBLE: + case Types.NUMERIC: + case Types.DECIMAL: + return DOUBLE; + case Types.CHAR: + case Types.NCHAR: + case Types.VARCHAR: + case Types.NVARCHAR: + case Types.LONGVARCHAR: + case Types.LONGNVARCHAR: + return VARCHAR; + case Types.BINARY: + case Types.VARBINARY: + case Types.LONGVARBINARY: + return VARBINARY; + case Types.DATE: + return DATE; + case Types.TIME: + return TIME; + case Types.TIMESTAMP: + return TIMESTAMP; + } + return null; + } + + public static Properties toProperties(Map map) + { + Properties properties = new Properties(); + for (Map.Entry entry : map.entrySet()) { + properties.setProperty(entry.getKey(), entry.getValue()); + } + return properties; + } + + public static boolean checkListNullOrEmpty(List list) + { + return list == null || list.isEmpty(); + } + + public static boolean converStringToBoolean(String fieldValue, boolean defaultValue) + { + if (fieldValue == null || "".equals(fieldValue)) { + return defaultValue; + } + else if ("Y".equals(fieldValue) + || "y".equals(fieldValue)) { + return true; + } + else { + return false; + } + } + + public static String Base64Encode(String str) + { + str = str == null ? "" : str; + return new String(Base64.getEncoder().encode(str.getBytes()), StandardCharsets.UTF_8); + } + + public static String Base64Decode(String str) + { + str = str == null ? "" : str; + return new String(Base64.getDecoder().decode(str.getBytes()), StandardCharsets.UTF_8); + } +} diff --git a/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/util/PdboMetadata.java b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/util/PdboMetadata.java new file mode 100644 index 00000000..3379cc4b --- /dev/null +++ b/presto-base-jdbc/src/main/java/com/facebook/presto/plugin/jdbc/util/PdboMetadata.java @@ -0,0 +1,79 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc.util; + +import com.facebook.presto.plugin.jdbc.JdbcSplit; + +public final class PdboMetadata +{ + public static final String PDBO_DATABASE = "route_schema"; + public static final String PDBO_TABLE = PDBO_DATABASE + ".pdbo_table"; + public static final String PDBO_ROUTE = PDBO_DATABASE + ".pdbo_route"; + public static final String PDBO_LOG = PDBO_DATABASE + ".pdbo_log"; + public static final String DB_INFO = PDBO_DATABASE + ".db_info"; + + private PdboMetadata() + { + } + + public static String getPdboLogsSQL() + { + return "SELECT A.CONNECTORID,A.SCHEMANAME,A.TABLENAME,A.ROWS,A.BEGININDEX,A.ENDINDEX,B.DBTYPE," + + " C.DBHOST,C.DBPORT,C.CONNECTION_PROPERTIES,C.PRESTO_WORK_HOST,C.REMOTELYACCESSIBLE,C.SPLITFIELD, " + + " C.SCANNODENUMBER,D.USERNAME,D.PASSWORD,A.CONTROL_SCAN_CONCURRENCY_ENABLED,A.SCAN_CONCURRENCY_COUNT " + + " FROM " + PDBO_LOG + " A INNER JOIN " + PDBO_TABLE + " B " + + " ON A.CONNECTORID = B.CONNECTORID AND A.SCHEMANAME = B.PRESTO_SCHEMA AND A.TABLENAME = B.PRESTO_TABLE " + + " INNER JOIN " + PDBO_ROUTE + " C ON B.TABLEID = C.TABLEID " + + " LEFT JOIN " + DB_INFO + " D ON C.UID = D.UID " + + " WHERE A.CONNECTORID = ? " + + " AND A.SCHEMANAME = ? " + + " AND A.TABLENAME = ? " + + " AND A.RECORDFLAG = 'finish' " + + " AND B.CALC_STEP_ENABLE = 'Y' " + + " ORDER BY A.BEGININDEX"; + } + + public static String getPdboTableInfoSQL() + { + return "SELECT CONNECTORID,PRESTO_SCHEMA,PRESTO_TABLE,DBTYPE,CALC_STEP_ENABLE,CONTROL_SCAN_CONCURRENCY_ENABLED,SCAN_CONCURRENCY_COUNT," + + " B.DBHOST,B.DBPORT,CONNECTION_PROPERTIES,SOURCE_SCHEMA,SOURCE_TABLE,SPLITFIELD,REMOTELYACCESSIBLE,PRESTO_WORK_HOST,SCANNODENUMBER," + + " FIELDMAXVALUE,FIELDMINVALUE,USERNAME,PASSWORD" + + " FROM " + PDBO_TABLE + " A INNER JOIN " + PDBO_ROUTE + " B ON A.TABLEID = B.TABLEID" + + " LEFT JOIN " + DB_INFO + " C ON B.UID = C.UID" + + " WHERE CONNECTORID = ? "; + } + + public static String getInsertPdboLogSQL(JdbcSplit split, long rowCount, String connectorId) + { + return "INSERT INTO " + PDBO_LOG + + " (CONNECTORID,SCHEMANAME,TABLENAME,ROWS,BEGININDEX,ENDINDEX,RECORDFLAG,SCANNODES,TIMESTAMP) VALUES " + + "('" + connectorId + "'," + + "'" + split.getSchemaName() + "'," + + "'" + split.getTableName() + "'," + + rowCount + "," + + split.getBeginIndex() + "," + + split.getEndIndex() + "," + + "'new'," + + split.getScanNodes() + "," + + split.getTimeStamp() + ")"; + } + + public static String getUpdatePdboHistoryLogSQL(JdbcSplit split, String connectorId) + { + return "UPDATE " + PDBO_LOG + " SET RECORDFLAG='runhistory' " + + "WHERE RECORDFLAG='new' AND CONNECTORID='" + connectorId + + "' AND SCHEMANAME='" + split.getSchemaName() + "' AND TABLENAME='" + + split.getTableName() + "'" + " AND timestamp < " + split.getTimeStamp(); + } +} diff --git a/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/JdbcQueryRunner.java b/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/JdbcQueryRunner.java new file mode 100644 index 00000000..37c14bdd --- /dev/null +++ b/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/JdbcQueryRunner.java @@ -0,0 +1,94 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc; + +import com.facebook.presto.Session; +import com.facebook.presto.tests.DistributedQueryRunner; +import com.facebook.presto.tpch.TpchPlugin; +import com.google.common.collect.ImmutableList; +import io.airlift.tpch.TpchTable; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Map; + +import static com.facebook.presto.spi.type.TimeZoneKey.UTC_KEY; +import static com.facebook.presto.tests.QueryAssertions.copyTpchTables; +import static com.facebook.presto.tpch.TpchMetadata.TINY_SCHEMA_NAME; +import static io.airlift.testing.Closeables.closeAllSuppress; +import static java.util.Locale.ENGLISH; + +public final class JdbcQueryRunner +{ + private JdbcQueryRunner() + { + } + + private static final String TPCH_SCHEMA = "tpch"; + + public static DistributedQueryRunner createJdbcQueryRunner(TpchTable... tables) + throws Exception + { + return createJdbcQueryRunner(ImmutableList.copyOf(tables)); + } + + public static DistributedQueryRunner createJdbcQueryRunner(Iterable> tables) + throws Exception + { + DistributedQueryRunner queryRunner = null; + try { + queryRunner = new DistributedQueryRunner(createSession(), 3); + + queryRunner.installPlugin(new TpchPlugin()); + queryRunner.createCatalog("tpch", "tpch"); + + Map properties = TestingH2JdbcModule.createProperties(); + createSchema(properties, "tpch"); + + queryRunner.installPlugin(new JdbcPlugin("base-jdbc", new TestingH2JdbcModule())); + queryRunner.createCatalog("jdbc", "base-jdbc", properties); + + copyTpchTables(queryRunner, "tpch", TINY_SCHEMA_NAME, createSession(), tables); + + return queryRunner; + } + catch (Throwable e) { + closeAllSuppress(e, queryRunner); + throw e; + } + } + + private static void createSchema(Map properties, String schema) + throws SQLException + { + try (Connection connection = DriverManager.getConnection(properties.get("connection-url")); + Statement statement = connection.createStatement()) { + statement.execute("CREATE SCHEMA " + schema); + } + } + + public static Session createSession() + { + return Session.builder() + .setUser("user") + .setSource("test") + .setCatalog("jdbc") + .setSchema(TPCH_SCHEMA) + .setTimeZoneKey(UTC_KEY) + .setLocale(ENGLISH) + .build(); + } +} diff --git a/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/MetadataUtil.java b/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/MetadataUtil.java new file mode 100644 index 00000000..bce7ffb3 --- /dev/null +++ b/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/MetadataUtil.java @@ -0,0 +1,78 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc; + +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.deser.std.FromStringDeserializer; +import com.google.common.collect.ImmutableMap; +import io.airlift.json.JsonCodec; +import io.airlift.json.JsonCodecFactory; +import io.airlift.json.ObjectMapperProvider; + +import java.util.Map; + +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static com.google.common.base.Preconditions.checkArgument; +import static java.util.Locale.ENGLISH; +import static org.testng.Assert.assertEquals; + +final class MetadataUtil +{ + private MetadataUtil() {} + + public static final JsonCodec COLUMN_CODEC; + public static final JsonCodec TABLE_CODEC; + public static final JsonCodec OUTPUT_TABLE_CODEC; + + static { + ObjectMapperProvider provider = new ObjectMapperProvider(); + provider.setJsonDeserializers(ImmutableMap., JsonDeserializer>of(Type.class, new TestingTypeDeserializer())); + JsonCodecFactory codecFactory = new JsonCodecFactory(provider); + COLUMN_CODEC = codecFactory.jsonCodec(JdbcColumnHandle.class); + TABLE_CODEC = codecFactory.jsonCodec(JdbcTableHandle.class); + OUTPUT_TABLE_CODEC = codecFactory.jsonCodec(JdbcOutputTableHandle.class); + } + + public static final class TestingTypeDeserializer + extends FromStringDeserializer + { + private final Map types = ImmutableMap.of( + StandardTypes.BIGINT, BIGINT, + StandardTypes.VARCHAR, VARCHAR); + + public TestingTypeDeserializer() + { + super(Type.class); + } + + @Override + protected Type _deserialize(String value, DeserializationContext context) + { + Type type = types.get(value.toLowerCase(ENGLISH)); + checkArgument(type != null, "Unknown type %s", value); + return type; + } + } + + public static void assertJsonRoundTrip(JsonCodec codec, T object) + { + String json = codec.toJson(object); + T copy = codec.fromJson(json); + assertEquals(copy, object); + } +} diff --git a/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestBaseJdbcConfig.java b/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestBaseJdbcConfig.java new file mode 100644 index 00000000..bf8508a5 --- /dev/null +++ b/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestBaseJdbcConfig.java @@ -0,0 +1,49 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc; + +import com.google.common.collect.ImmutableMap; +import io.airlift.configuration.testing.ConfigAssertions; +import org.testng.annotations.Test; + +import java.util.Map; + +public class TestBaseJdbcConfig +{ + @Test + public void testDefaults() + { + ConfigAssertions.assertRecordedDefaults(ConfigAssertions.recordDefaults(BaseJdbcConfig.class) + .setConnectionUrl(null) + .setConnectionUser(null) + .setConnectionPassword(null)); + } + + @Test + public void testExplicitPropertyMappings() + { + Map properties = new ImmutableMap.Builder() + .put("connection-url", "jdbc:h2:mem:config") + .put("connection-user", "user") + .put("connection-password", "password") + .build(); + + BaseJdbcConfig expected = new BaseJdbcConfig() + .setConnectionUrl("jdbc:h2:mem:config") + .setConnectionUser("user") + .setConnectionPassword("password"); + + ConfigAssertions.assertFullMapping(properties, expected); + } +} diff --git a/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcClient.java b/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcClient.java new file mode 100644 index 00000000..57850870 --- /dev/null +++ b/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcClient.java @@ -0,0 +1,75 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc; + +import com.facebook.presto.spi.SchemaTableName; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import static com.facebook.presto.plugin.jdbc.TestingDatabase.CONNECTOR_ID; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static java.util.Locale.ENGLISH; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +@Test +public class TestJdbcClient +{ + private TestingDatabase database; + private String catalogName; + private JdbcClient jdbcClient; + + @BeforeClass + public void setUp() + throws Exception + { + database = new TestingDatabase(); + catalogName = database.getConnection().getCatalog(); + jdbcClient = database.getJdbcClient(); + } + + @AfterClass + public void tearDown() + throws Exception + { + database.close(); + } + + @Test + public void testMetadata() + throws Exception + { + assertTrue(jdbcClient.getSchemaNames().containsAll(ImmutableSet.of("example", "tpch"))); + assertEquals(jdbcClient.getTableNames("example"), ImmutableList.of(new SchemaTableName("example", "numbers"))); + assertEquals(jdbcClient.getTableNames("tpch"), ImmutableList.of( + new SchemaTableName("tpch", "lineitem"), + new SchemaTableName("tpch", "orders"))); + + SchemaTableName schemaTableName = new SchemaTableName("example", "numbers"); + JdbcTableHandle table = jdbcClient.getTableHandle(schemaTableName); + assertNotNull(table, "table is null"); + assertEquals(table.getCatalogName(), catalogName.toUpperCase(ENGLISH)); + assertEquals(table.getSchemaName(), "EXAMPLE"); + assertEquals(table.getTableName(), "NUMBERS"); + assertEquals(table.getSchemaTableName(), schemaTableName); + assertEquals(jdbcClient.getColumns(table), ImmutableList.of( + new JdbcColumnHandle(CONNECTOR_ID, "TEXT", VARCHAR), + new JdbcColumnHandle(CONNECTOR_ID, "VALUE", BIGINT))); + } +} diff --git a/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcColumnHandle.java b/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcColumnHandle.java new file mode 100644 index 00000000..9f2a66eb --- /dev/null +++ b/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcColumnHandle.java @@ -0,0 +1,53 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc; + +import io.airlift.testing.EquivalenceTester; +import org.testng.annotations.Test; + +import static com.facebook.presto.plugin.jdbc.MetadataUtil.COLUMN_CODEC; +import static com.facebook.presto.plugin.jdbc.MetadataUtil.assertJsonRoundTrip; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; + +public class TestJdbcColumnHandle +{ + @Test + public void testJsonRoundTrip() + { + assertJsonRoundTrip(COLUMN_CODEC, new JdbcColumnHandle("connectorId", "columnName", VARCHAR)); + } + + @Test + public void testEquivalence() + { + EquivalenceTester.equivalenceTester() + .addEquivalentGroup( + new JdbcColumnHandle("connectorId", "columnName", VARCHAR), + new JdbcColumnHandle("connectorId", "columnName", VARCHAR), + new JdbcColumnHandle("connectorId", "columnName", BIGINT), + new JdbcColumnHandle("connectorId", "columnName", VARCHAR)) + .addEquivalentGroup( + new JdbcColumnHandle("connectorIdX", "columnName", VARCHAR), + new JdbcColumnHandle("connectorIdX", "columnName", VARCHAR), + new JdbcColumnHandle("connectorIdX", "columnName", BIGINT), + new JdbcColumnHandle("connectorIdX", "columnName", VARCHAR)) + .addEquivalentGroup( + new JdbcColumnHandle("connectorId", "columnNameX", VARCHAR), + new JdbcColumnHandle("connectorId", "columnNameX", VARCHAR), + new JdbcColumnHandle("connectorId", "columnNameX", BIGINT), + new JdbcColumnHandle("connectorId", "columnNameX", VARCHAR)) + .check(); + } +} diff --git a/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcConnectorFactory.java b/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcConnectorFactory.java new file mode 100644 index 00000000..6fe0456e --- /dev/null +++ b/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcConnectorFactory.java @@ -0,0 +1,33 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc; + +import com.google.common.collect.ImmutableMap; +import org.testng.annotations.Test; + +public class TestJdbcConnectorFactory +{ + @Test + public void test() + throws Exception + { + JdbcConnectorFactory connectorFactory = new JdbcConnectorFactory( + "test", + new TestingH2JdbcModule(), + ImmutableMap.of(), + getClass().getClassLoader()); + + connectorFactory.create("test", TestingH2JdbcModule.createProperties()); + } +} diff --git a/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcDistributedQueries.java b/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcDistributedQueries.java new file mode 100644 index 00000000..05aeb873 --- /dev/null +++ b/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcDistributedQueries.java @@ -0,0 +1,29 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc; + +import com.facebook.presto.tests.AbstractTestQueries; +import io.airlift.tpch.TpchTable; + +import static com.facebook.presto.plugin.jdbc.JdbcQueryRunner.createJdbcQueryRunner; + +public class TestJdbcDistributedQueries + extends AbstractTestQueries +{ + public TestJdbcDistributedQueries() + throws Exception + { + super(createJdbcQueryRunner(TpchTable.getTables())); + } +} diff --git a/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcHandleResolver.java b/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcHandleResolver.java new file mode 100644 index 00000000..de2c900e --- /dev/null +++ b/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcHandleResolver.java @@ -0,0 +1,60 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc; + +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.HostAddress; +import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.spi.TupleDomain; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import org.testng.annotations.Test; + +import java.util.List; + +import static com.facebook.presto.plugin.jdbc.TestingDatabase.CONNECTOR_ID; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +public class TestJdbcHandleResolver +{ + private static final JdbcHandleResolver RESOLVER = new JdbcHandleResolver(new JdbcConnectorId(CONNECTOR_ID)); + + @Test + public void testCanHandle() + throws Exception + { + assertTrue(RESOLVER.canHandle(createTableHandle(CONNECTOR_ID))); + assertFalse(RESOLVER.canHandle(createTableHandle("unknown"))); + } + + @Test + public void testCanHandleRecordSet() + { + assertTrue(RESOLVER.canHandle(createSplit(CONNECTOR_ID))); + assertFalse(RESOLVER.canHandle(createSplit("unknown"))); + } + + private static JdbcTableHandle createTableHandle(String connectorId) + { + return new JdbcTableHandle(connectorId, new SchemaTableName("schema", "table"), "catalog", "schema", "table"); + } + + private static JdbcSplit createSplit(String connectorId) + { + List address = ImmutableList.of(); + return new JdbcSplit(connectorId, "catalog", "schema", "table", "connectionUrl", ImmutableMap.of(), TupleDomain.all(), + "", address, true, "", "", "", "", System.nanoTime(), 1, false, ""); + } +} diff --git a/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcIntegrationSmokeTest.java b/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcIntegrationSmokeTest.java new file mode 100644 index 00000000..deba7b3c --- /dev/null +++ b/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcIntegrationSmokeTest.java @@ -0,0 +1,29 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc; + +import com.facebook.presto.tests.AbstractTestIntegrationSmokeTest; + +import static com.facebook.presto.plugin.jdbc.JdbcQueryRunner.createJdbcQueryRunner; +import static io.airlift.tpch.TpchTable.ORDERS; + +public class TestJdbcIntegrationSmokeTest + extends AbstractTestIntegrationSmokeTest +{ + public TestJdbcIntegrationSmokeTest() + throws Exception + { + super(createJdbcQueryRunner(ORDERS)); + } +} diff --git a/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcMetadata.java b/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcMetadata.java new file mode 100644 index 00000000..2fb0d0e6 --- /dev/null +++ b/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcMetadata.java @@ -0,0 +1,190 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc; + +import com.facebook.presto.spi.ColumnMetadata; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.ConnectorTableMetadata; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.spi.TableNotFoundException; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import static com.facebook.presto.plugin.jdbc.TestingDatabase.CONNECTOR_ID; +import static com.facebook.presto.spi.StandardErrorCode.NOT_FOUND; +import static com.facebook.presto.spi.StandardErrorCode.PERMISSION_DENIED; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.TimeZoneKey.UTC_KEY; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static java.util.Locale.ENGLISH; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.fail; + +@Test(singleThreaded = true) +public class TestJdbcMetadata +{ + private static final ConnectorSession SESSION = new ConnectorSession("user", UTC_KEY, ENGLISH, System.currentTimeMillis(), null); + + private TestingDatabase database; + private JdbcMetadata metadata; + private JdbcTableHandle tableHandle; + + @BeforeMethod + public void setUp() + throws Exception + { + database = new TestingDatabase(); + metadata = new JdbcMetadata(new JdbcConnectorId(CONNECTOR_ID), database.getJdbcClient(), new JdbcMetadataConfig()); + tableHandle = metadata.getTableHandle(SESSION, new SchemaTableName("example", "numbers")); + } + + @AfterMethod(alwaysRun = true) + public void tearDown() + throws Exception + { + database.close(); + } + + @Test + public void testListSchemaNames() + { + assertTrue(metadata.listSchemaNames(SESSION).containsAll(ImmutableSet.of("example", "tpch"))); + } + + @Test + public void testGetTableHandle() + { + JdbcTableHandle tableHandle = metadata.getTableHandle(SESSION, new SchemaTableName("example", "numbers")); + assertEquals(metadata.getTableHandle(SESSION, new SchemaTableName("example", "numbers")), tableHandle); + assertNull(metadata.getTableHandle(SESSION, new SchemaTableName("example", "unknown"))); + assertNull(metadata.getTableHandle(SESSION, new SchemaTableName("unknown", "numbers"))); + assertNull(metadata.getTableHandle(SESSION, new SchemaTableName("unknown", "unknown"))); + } + + @Test + public void testGetColumnHandles() + { + // known table + assertEquals(metadata.getColumnHandles(tableHandle), ImmutableMap.of( + "text", new JdbcColumnHandle(CONNECTOR_ID, "TEXT", VARCHAR), + "value", new JdbcColumnHandle(CONNECTOR_ID, "VALUE", BIGINT))); + + // unknown table + unknownTableColumnHandle(new JdbcTableHandle(CONNECTOR_ID, new SchemaTableName("unknown", "unknown"), "unknown", "unknown", "unknown")); + unknownTableColumnHandle(new JdbcTableHandle(CONNECTOR_ID, new SchemaTableName("example", "numbers"), null, "example", "unknown")); + } + + private void unknownTableColumnHandle(JdbcTableHandle tableHandle) + { + try { + metadata.getColumnHandles(tableHandle); + fail("Expected getColumnHandle of unknown table to throw a TableNotFoundException"); + } + catch (TableNotFoundException ignored) { + } + } + + @Test + public void getTableMetadata() + { + // known table + ConnectorTableMetadata tableMetadata = metadata.getTableMetadata(tableHandle); + assertEquals(tableMetadata.getTable(), new SchemaTableName("example", "numbers")); + assertEquals(tableMetadata.getColumns(), ImmutableList.of( + new ColumnMetadata("text", VARCHAR, false), + new ColumnMetadata("value", BIGINT, false))); + + // unknown tables should produce null + unknownTableMetadata(new JdbcTableHandle(CONNECTOR_ID, new SchemaTableName("u", "numbers"), null, "unknown", "unknown")); + unknownTableMetadata(new JdbcTableHandle(CONNECTOR_ID, new SchemaTableName("example", "numbers"), null, "example", "unknown")); + unknownTableMetadata(new JdbcTableHandle(CONNECTOR_ID, new SchemaTableName("example", "numbers"), null, "unknown", "numbers")); + } + + private void unknownTableMetadata(JdbcTableHandle tableHandle) + { + try { + metadata.getTableMetadata(tableHandle); + fail("Expected getTableMetadata of unknown table to throw a TableNotFoundException"); + } + catch (TableNotFoundException ignored) { + } + } + + @Test + public void testListTables() + { + // all schemas + assertEquals(ImmutableSet.copyOf(metadata.listTables(SESSION, null)), ImmutableSet.of( + new SchemaTableName("example", "numbers"), + new SchemaTableName("tpch", "orders"), + new SchemaTableName("tpch", "lineitem"))); + + // specific schema + assertEquals(ImmutableSet.copyOf(metadata.listTables(SESSION, "example")), ImmutableSet.of( + new SchemaTableName("example", "numbers"))); + assertEquals(ImmutableSet.copyOf(metadata.listTables(SESSION, "tpch")), ImmutableSet.of( + new SchemaTableName("tpch", "orders"), + new SchemaTableName("tpch", "lineitem"))); + + // unknown schema + assertEquals(ImmutableSet.copyOf(metadata.listTables(SESSION, "unknown")), ImmutableSet.of()); + } + + @Test + public void getColumnMetadata() + { + assertEquals( + metadata.getColumnMetadata(tableHandle, new JdbcColumnHandle(CONNECTOR_ID, "text", VARCHAR)), + new ColumnMetadata("text", VARCHAR, false)); + } + + @Test(expectedExceptions = PrestoException.class) + public void testCreateTable() + { + metadata.createTable(SESSION, new ConnectorTableMetadata( + new SchemaTableName("example", "foo"), + ImmutableList.of(new ColumnMetadata("text", VARCHAR, false)))); + } + + @Test + public void testDropTableTable() + { + try { + metadata.dropTable(tableHandle); + fail("expected exception"); + } + catch (PrestoException e) { + assertEquals(e.getErrorCode(), PERMISSION_DENIED.toErrorCode()); + } + + JdbcMetadataConfig config = new JdbcMetadataConfig().setAllowDropTable(true); + metadata = new JdbcMetadata(new JdbcConnectorId(CONNECTOR_ID), database.getJdbcClient(), config); + metadata.dropTable(tableHandle); + + try { + metadata.getTableMetadata(tableHandle); + fail("expected exception"); + } + catch (PrestoException e) { + assertEquals(e.getErrorCode(), NOT_FOUND.toErrorCode()); + } + } +} diff --git a/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcMetadataConfig.java b/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcMetadataConfig.java new file mode 100644 index 00000000..da079f32 --- /dev/null +++ b/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcMetadataConfig.java @@ -0,0 +1,46 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc; + +import com.google.common.collect.ImmutableMap; +import org.testng.annotations.Test; + +import java.util.Map; + +import static io.airlift.configuration.testing.ConfigAssertions.assertFullMapping; +import static io.airlift.configuration.testing.ConfigAssertions.assertRecordedDefaults; +import static io.airlift.configuration.testing.ConfigAssertions.recordDefaults; + +public class TestJdbcMetadataConfig +{ + @Test + public void testDefaults() + { + assertRecordedDefaults(recordDefaults(JdbcMetadataConfig.class) + .setAllowDropTable(false)); + } + + @Test + public void testExplicitPropertyMappings() + { + Map properties = new ImmutableMap.Builder() + .put("allow-drop-table", "true") + .build(); + + JdbcMetadataConfig expected = new JdbcMetadataConfig() + .setAllowDropTable(true); + + assertFullMapping(properties, expected); + } +} diff --git a/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcOutputTableHandle.java b/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcOutputTableHandle.java new file mode 100644 index 00000000..8d25bd28 --- /dev/null +++ b/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcOutputTableHandle.java @@ -0,0 +1,44 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc; + +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import org.testng.annotations.Test; + +import static com.facebook.presto.plugin.jdbc.MetadataUtil.OUTPUT_TABLE_CODEC; +import static com.facebook.presto.plugin.jdbc.MetadataUtil.assertJsonRoundTrip; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; + +public class TestJdbcOutputTableHandle +{ + @Test + public void testJsonRoundTrip() + { + JdbcOutputTableHandle handle = new JdbcOutputTableHandle( + "connectorId", + "catalog", + "schema", + "table", + ImmutableList.of("abc", "xyz"), + ImmutableList.of(VARCHAR, VARCHAR), + "test", + "tmp_table", + "jdbc:junk", + ImmutableMap.of("user", "test")); + + assertJsonRoundTrip(OUTPUT_TABLE_CODEC, handle); + } +} diff --git a/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcRecordSet.java b/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcRecordSet.java new file mode 100644 index 00000000..c0e06572 --- /dev/null +++ b/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcRecordSet.java @@ -0,0 +1,154 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc; + +import com.facebook.presto.spi.RecordCursor; +import com.facebook.presto.spi.RecordSet; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import java.util.LinkedHashMap; +import java.util.Map; + +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; + +@Test +public class TestJdbcRecordSet +{ + private TestingDatabase database; + private JdbcClient jdbcClient; + private JdbcSplit split; + private Map columnHandles; + + @BeforeClass + public void setUp() + throws Exception + { + database = new TestingDatabase(); + jdbcClient = database.getJdbcClient(); + split = database.getSplit("example", "numbers"); + columnHandles = database.getColumnHandles("example", "numbers"); + } + + @AfterClass + public void tearDown() + throws Exception + { + database.close(); + } + + @Test + public void testGetColumnTypes() + throws Exception + { + RecordSet recordSet = new JdbcRecordSet(jdbcClient, split, ImmutableList.of( + new JdbcColumnHandle("test", "text", VARCHAR), + new JdbcColumnHandle("test", "value", BIGINT))); + assertEquals(recordSet.getColumnTypes(), ImmutableList.of(VARCHAR, BIGINT)); + + recordSet = new JdbcRecordSet(jdbcClient, split, ImmutableList.of( + new JdbcColumnHandle("test", "value", BIGINT), + new JdbcColumnHandle("test", "text", VARCHAR))); + assertEquals(recordSet.getColumnTypes(), ImmutableList.of(BIGINT, VARCHAR)); + + recordSet = new JdbcRecordSet(jdbcClient, split, ImmutableList.of( + new JdbcColumnHandle("test", "value", BIGINT), + new JdbcColumnHandle("test", "value", BIGINT), + new JdbcColumnHandle("test", "text", VARCHAR))); + assertEquals(recordSet.getColumnTypes(), ImmutableList.of(BIGINT, BIGINT, VARCHAR)); + + recordSet = new JdbcRecordSet(jdbcClient, split, ImmutableList.of()); + assertEquals(recordSet.getColumnTypes(), ImmutableList.of()); + } + + @Test + public void testCursorSimple() + throws Exception + { + RecordSet recordSet = new JdbcRecordSet(jdbcClient, split, ImmutableList.of( + columnHandles.get("text"), + columnHandles.get("value"))); + + try (RecordCursor cursor = recordSet.cursor()) { + assertEquals(cursor.getType(0), VARCHAR); + assertEquals(cursor.getType(1), BIGINT); + + Map data = new LinkedHashMap<>(); + while (cursor.advanceNextPosition()) { + data.put(cursor.getSlice(0).toStringUtf8(), cursor.getLong(1)); + assertFalse(cursor.isNull(0)); + assertFalse(cursor.isNull(1)); + } + + assertEquals(data, ImmutableMap.builder() + .put("one", 1L) + .put("two", 2L) + .put("three", 3L) + .put("ten", 10L) + .put("eleven", 11L) + .put("twelve", 12L) + .build()); + } + } + + @Test + public void testCursorMixedOrder() + throws Exception + { + RecordSet recordSet = new JdbcRecordSet(jdbcClient, split, ImmutableList.of( + columnHandles.get("value"), + columnHandles.get("value"), + columnHandles.get("text"))); + + try (RecordCursor cursor = recordSet.cursor()) { + assertEquals(cursor.getType(0), BIGINT); + assertEquals(cursor.getType(1), BIGINT); + assertEquals(cursor.getType(2), VARCHAR); + + Map data = new LinkedHashMap<>(); + while (cursor.advanceNextPosition()) { + assertEquals(cursor.getLong(0), cursor.getLong(1)); + data.put(cursor.getSlice(2).toStringUtf8(), cursor.getLong(0)); + } + + assertEquals(data, ImmutableMap.builder() + .put("one", 1L) + .put("two", 2L) + .put("three", 3L) + .put("ten", 10L) + .put("eleven", 11L) + .put("twelve", 12L) + .build()); + } + } + + @Test + public void testIdempotentClose() + { + RecordSet recordSet = new JdbcRecordSet(jdbcClient, split, ImmutableList.of( + columnHandles.get("value"), + columnHandles.get("value"), + columnHandles.get("text"))); + + RecordCursor cursor = recordSet.cursor(); + cursor.close(); + cursor.close(); + } +} diff --git a/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcRecordSetProvider.java b/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcRecordSetProvider.java new file mode 100644 index 00000000..7e78b795 --- /dev/null +++ b/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcRecordSetProvider.java @@ -0,0 +1,191 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc; + +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorPartitionResult; +import com.facebook.presto.spi.ConnectorSplitSource; +import com.facebook.presto.spi.Domain; +import com.facebook.presto.spi.Range; +import com.facebook.presto.spi.RecordCursor; +import com.facebook.presto.spi.RecordSet; +import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.spi.SortedRangeSet; +import com.facebook.presto.spi.TupleDomain; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static com.google.common.collect.Iterables.getOnlyElement; +import static io.airlift.concurrent.MoreFutures.getFutureValue; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +@Test +public class TestJdbcRecordSetProvider +{ + private TestingDatabase database; + private JdbcClient jdbcClient; + private JdbcSplit split; + + private JdbcTableHandle table; + private JdbcColumnHandle textColumn; + private JdbcColumnHandle valueColumn; + + @BeforeClass + public void setUp() + throws Exception + { + database = new TestingDatabase(); + jdbcClient = database.getJdbcClient(); + split = database.getSplit("example", "numbers"); + + table = jdbcClient.getTableHandle(new SchemaTableName("example", "numbers")); + + Map columns = database.getColumnHandles("example", "numbers"); + textColumn = columns.get("text"); + valueColumn = columns.get("value"); + } + + @AfterClass + public void tearDown() + throws Exception + { + database.close(); + } + + @Test + public void testGetRecordSet() + throws Exception + { + JdbcRecordSetProvider recordSetProvider = new JdbcRecordSetProvider(jdbcClient); + RecordSet recordSet = recordSetProvider.getRecordSet(split, ImmutableList.of(textColumn, valueColumn)); + assertNotNull(recordSet, "recordSet is null"); + + RecordCursor cursor = recordSet.cursor(); + assertNotNull(cursor, "cursor is null"); + + Map data = new LinkedHashMap<>(); + while (cursor.advanceNextPosition()) { + data.put(cursor.getSlice(0).toStringUtf8(), cursor.getLong(1)); + } + assertEquals(data, ImmutableMap.builder() + .put("one", 1L) + .put("two", 2L) + .put("three", 3L) + .put("ten", 10L) + .put("eleven", 11L) + .put("twelve", 12L) + .build()); + } + + @Test + public void testTupleDomain() + throws Exception + { + // single value + getCursor(table, ImmutableList.of(textColumn, valueColumn), TupleDomain.withColumnDomains( + ImmutableMap.of(textColumn, Domain.singleValue("foo")) + )); + + // multiple values (string) + getCursor(table, ImmutableList.of(textColumn, valueColumn), TupleDomain.withColumnDomains( + ImmutableMap.of(textColumn, Domain.union(ImmutableList.of(Domain.singleValue("foo"), Domain.singleValue("bar")))) + )); + + // inequality (string) + getCursor(table, ImmutableList.of(textColumn, valueColumn), TupleDomain.withColumnDomains( + ImmutableMap.of(textColumn, Domain.create(SortedRangeSet.of(Range.greaterThan("foo")), false)) + )); + + getCursor(table, ImmutableList.of(textColumn, valueColumn), TupleDomain.withColumnDomains( + ImmutableMap.of(textColumn, Domain.create(SortedRangeSet.of(Range.greaterThan("foo")), false)) + )); + + getCursor(table, ImmutableList.of(textColumn, valueColumn), TupleDomain.withColumnDomains( + ImmutableMap.of(textColumn, Domain.create(SortedRangeSet.of(Range.lessThanOrEqual("foo")), false)) + )); + + getCursor(table, ImmutableList.of(textColumn, valueColumn), TupleDomain.withColumnDomains( + ImmutableMap.of(textColumn, Domain.create(SortedRangeSet.of(Range.lessThan("foo")), false)) + )); + + // is null + getCursor(table, ImmutableList.of(textColumn, valueColumn), TupleDomain.withColumnDomains( + ImmutableMap.of(textColumn, Domain.onlyNull(String.class)) + )); + + // not null + getCursor(table, ImmutableList.of(textColumn, valueColumn), TupleDomain.withColumnDomains( + ImmutableMap.of(textColumn, Domain.notNull(String.class)) + )); + + // specific value or null + getCursor(table, ImmutableList.of(textColumn, valueColumn), TupleDomain.withColumnDomains( + ImmutableMap.of(textColumn, Domain.union(ImmutableList.of(Domain.singleValue("foo"), Domain.onlyNull(String.class)))) + )); + + getCursor(table, ImmutableList.of(textColumn, valueColumn), TupleDomain.withColumnDomains( + ImmutableMap.of(textColumn, Domain.create(SortedRangeSet.of(Range.range("bar", true, "foo", true)), false)) + )); + + getCursor(table, ImmutableList.of(textColumn, valueColumn), TupleDomain.withColumnDomains( + ImmutableMap.of(textColumn, Domain.create(SortedRangeSet.of( + Range.range("bar", true, "foo", true), + Range.range("hello", false, "world", false)), + false + )) + )); + + getCursor(table, ImmutableList.of(textColumn, valueColumn), TupleDomain.withColumnDomains( + ImmutableMap.of( + textColumn, + Domain.create(SortedRangeSet.of( + Range.range("bar", true, "foo", true), + Range.range("hello", false, "world", false), + Range.equal("apple"), + Range.equal("banana"), + Range.equal("zoo")), + false + ), + + valueColumn, + Domain.create(SortedRangeSet.of( + Range.range(1, true, 5, true), + Range.range(10, false, 20, false)), + true + ) + ) + )); + } + + private RecordCursor getCursor(JdbcTableHandle jdbcTableHandle, List columns, TupleDomain domain) + throws InterruptedException + { + ConnectorPartitionResult partitions = jdbcClient.getPartitions(jdbcTableHandle, domain); + ConnectorSplitSource splits = jdbcClient.getPartitionSplits((JdbcPartition) getOnlyElement(partitions.getPartitions())); + JdbcSplit split = (JdbcSplit) getOnlyElement(getFutureValue(splits.getNextBatch(1000))); + + JdbcRecordSetProvider recordSetProvider = new JdbcRecordSetProvider(jdbcClient); + RecordSet recordSet = recordSetProvider.getRecordSet(split, columns); + + return recordSet.cursor(); + } +} diff --git a/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcSplit.java b/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcSplit.java new file mode 100644 index 00000000..7492c4b1 --- /dev/null +++ b/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcSplit.java @@ -0,0 +1,60 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc; + +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.HostAddress; +import com.facebook.presto.spi.TupleDomain; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.airlift.json.JsonCodec; +import org.testng.annotations.Test; + +import java.util.List; + +import static io.airlift.json.JsonCodec.jsonCodec; +import static org.testng.Assert.assertEquals; + +public class TestJdbcSplit +{ + List address = ImmutableList.of(); + private final JdbcSplit split = new JdbcSplit("connectorId", "catalog", "schemaName", "tableName", "connectionUrl", ImmutableMap.of(), TupleDomain.all(), + "", address, true, "", "", "", "", System.nanoTime(), 1, false, ""); + + @Test + public void testAddresses() + { + // split uses "example" scheme so no addresses are available and is not remotely accessible + assertEquals(split.getAddresses(), ImmutableList.of()); + assertEquals(split.isRemotelyAccessible(), true); + + JdbcSplit jdbcSplit = new JdbcSplit("connectorId", "catalog", "schemaName", "tableName", "connectionUrl", ImmutableMap.of(), TupleDomain.all(), + "", address, true, "", "", "", "", System.nanoTime(), 1, false, ""); + assertEquals(jdbcSplit.getAddresses(), ImmutableList.of()); + } + + @Test + public void testJsonRoundTrip() + { + JsonCodec codec = jsonCodec(JdbcSplit.class); + String json = codec.toJson(split); + JdbcSplit copy = codec.fromJson(json); + assertEquals(copy.getConnectorId(), split.getConnectorId()); + assertEquals(copy.getSchemaName(), split.getSchemaName()); + assertEquals(copy.getTableName(), split.getTableName()); + + assertEquals(copy.getAddresses(), ImmutableList.of()); + assertEquals(copy.isRemotelyAccessible(), true); + } +} diff --git a/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcTableHandle.java b/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcTableHandle.java new file mode 100644 index 00000000..ab854c5c --- /dev/null +++ b/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestJdbcTableHandle.java @@ -0,0 +1,52 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc; + +import com.facebook.presto.spi.SchemaTableName; +import io.airlift.testing.EquivalenceTester; +import org.testng.annotations.Test; + +import static com.facebook.presto.plugin.jdbc.MetadataUtil.TABLE_CODEC; +import static com.facebook.presto.plugin.jdbc.MetadataUtil.assertJsonRoundTrip; + +public class TestJdbcTableHandle +{ + @Test + public void testJsonRoundTrip() + { + assertJsonRoundTrip(TABLE_CODEC, new JdbcTableHandle("connectorId", new SchemaTableName("schema", "table"), "jdbcCatalog", "jdbcSchema", "jdbcTable")); + } + + @Test + public void testEquivalence() + { + EquivalenceTester.equivalenceTester() + .addEquivalentGroup( + new JdbcTableHandle("connectorId", new SchemaTableName("schema", "table"), "jdbcCatalog", "jdbcSchema", "jdbcTable"), + new JdbcTableHandle("connectorId", new SchemaTableName("schema", "table"), "jdbcCatalogX", "jdbcSchema", "jdbcTable"), + new JdbcTableHandle("connectorId", new SchemaTableName("schema", "table"), "jdbcCatalog", "jdbcSchemaX", "jdbcTable"), + new JdbcTableHandle("connectorId", new SchemaTableName("schema", "table"), "jdbcCatalog", "jdbcSchema", "jdbcTableX")) + .addEquivalentGroup( + new JdbcTableHandle("connectorIdX", new SchemaTableName("schema", "table"), "jdbcCatalog", "jdbcSchema", "jdbcTable"), + new JdbcTableHandle("connectorIdX", new SchemaTableName("schema", "table"), "jdbcCatalogX", "jdbcSchema", "jdbcTable"), + new JdbcTableHandle("connectorIdX", new SchemaTableName("schema", "table"), "jdbcCatalog", "jdbcSchemaX", "jdbcTable"), + new JdbcTableHandle("connectorIdX", new SchemaTableName("schema", "table"), "jdbcCatalog", "jdbcSchema", "jdbcTableX")) + .addEquivalentGroup( + new JdbcTableHandle("connectorId", new SchemaTableName("schemaX", "table"), "jdbcCatalog", "jdbcSchema", "jdbcTable"), + new JdbcTableHandle("connectorId", new SchemaTableName("schemaX", "table"), "jdbcCatalogX", "jdbcSchema", "jdbcTable"), + new JdbcTableHandle("connectorId", new SchemaTableName("schemaX", "table"), "jdbcCatalog", "jdbcSchemaX", "jdbcTable"), + new JdbcTableHandle("connectorId", new SchemaTableName("schemaX", "table"), "jdbcCatalog", "jdbcSchema", "jdbcTableX")) + .check(); + } +} diff --git a/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestingDatabase.java b/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestingDatabase.java new file mode 100644 index 00000000..a07442cf --- /dev/null +++ b/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestingDatabase.java @@ -0,0 +1,113 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc; + +import com.facebook.presto.plugin.jdbc.cache.JdbcCacheConfig; +import com.facebook.presto.plugin.jdbc.subtable.JdbcSubTableConfig; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorPartitionResult; +import com.facebook.presto.spi.ConnectorSplitSource; +import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.spi.TupleDomain; +import com.google.common.collect.ImmutableMap; +import org.h2.Driver; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.List; +import java.util.Map; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.collect.Iterables.getOnlyElement; +import static io.airlift.concurrent.MoreFutures.getFutureValue; + +final class TestingDatabase + implements AutoCloseable +{ + public static final String CONNECTOR_ID = "test"; + + private final Connection connection; + private final JdbcClient jdbcClient; + + public TestingDatabase() + throws SQLException + { + String connectionUrl = "jdbc:h2:mem:test" + System.nanoTime(); + jdbcClient = new BaseJdbcClient( + new JdbcConnectorId(CONNECTOR_ID), + new BaseJdbcConfig().setConnectionUrl(connectionUrl), + "\"", + new Driver(), + new JdbcSubTableConfig(), + new JdbcCacheConfig()); + + connection = DriverManager.getConnection(connectionUrl); + connection.createStatement().execute("CREATE SCHEMA example"); + + connection.createStatement().execute("CREATE TABLE example.numbers(text varchar primary key, value bigint)"); + connection.createStatement().execute("INSERT INTO example.numbers(text, value) VALUES " + + "('one', 1)," + + "('two', 2)," + + "('three', 3)," + + "('ten', 10)," + + "('eleven', 11)," + + "('twelve', 12)" + + ""); + connection.createStatement().execute("CREATE SCHEMA tpch"); + connection.createStatement().execute("CREATE TABLE tpch.orders(orderkey bigint primary key, custkey bigint)"); + connection.createStatement().execute("CREATE TABLE tpch.lineitem(orderkey bigint primary key, partkey bigint)"); + + connection.commit(); + } + + @Override + public void close() + throws SQLException + { + connection.close(); + } + + public Connection getConnection() + { + return connection; + } + + public JdbcClient getJdbcClient() + { + return jdbcClient; + } + + public JdbcSplit getSplit(String schemaName, String tableName) + throws InterruptedException + { + JdbcTableHandle jdbcTableHandle = jdbcClient.getTableHandle(new SchemaTableName(schemaName, tableName)); + ConnectorPartitionResult partitions = jdbcClient.getPartitions(jdbcTableHandle, TupleDomain.all()); + ConnectorSplitSource splits = jdbcClient.getPartitionSplits((JdbcPartition) getOnlyElement(partitions.getPartitions())); + return (JdbcSplit) getOnlyElement(getFutureValue(splits.getNextBatch(1000))); + } + + public Map getColumnHandles(String schemaName, String tableName) + { + JdbcTableHandle tableHandle = jdbcClient.getTableHandle(new SchemaTableName(schemaName, tableName)); + List columns = jdbcClient.getColumns(tableHandle); + checkArgument(columns != null, "table not found: %s.%s", schemaName, tableName); + + ImmutableMap.Builder columnHandles = ImmutableMap.builder(); + for (JdbcColumnHandle column : columns) { + columnHandles.put(column.getColumnMetadata().getName(), column); + } + return columnHandles.build(); + } +} diff --git a/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestingH2JdbcModule.java b/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestingH2JdbcModule.java new file mode 100644 index 00000000..36d85978 --- /dev/null +++ b/presto-base-jdbc/src/test/java/com/facebook/presto/plugin/jdbc/TestingH2JdbcModule.java @@ -0,0 +1,52 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.plugin.jdbc; + +import com.facebook.presto.plugin.jdbc.cache.JdbcCacheConfig; +import com.facebook.presto.plugin.jdbc.subtable.JdbcSubTableConfig; +import com.google.common.collect.ImmutableMap; +import com.google.inject.Binder; +import com.google.inject.Module; +import com.google.inject.Provides; +import org.h2.Driver; + +import java.util.Map; + +import static io.airlift.configuration.ConfigBinder.configBinder; +import static java.lang.String.format; + +class TestingH2JdbcModule + implements Module +{ + @Override + public void configure(Binder binder) + { + configBinder(binder).bindConfig(BaseJdbcConfig.class); + } + + @Provides + public JdbcClient provideJdbcClient(JdbcConnectorId id, BaseJdbcConfig config) + { + return new BaseJdbcClient(id, config, "\"", new Driver(), + new JdbcSubTableConfig(), + new JdbcCacheConfig()); + } + + public static Map createProperties() + { + return ImmutableMap.builder() + .put("connection-url", format("jdbc:h2:mem:test%s;DB_CLOSE_DELAY=-1", System.nanoTime())) + .build(); + } +} diff --git a/presto-benchmark-driver/pom.xml b/presto-benchmark-driver/pom.xml new file mode 100644 index 00000000..2dc982ca --- /dev/null +++ b/presto-benchmark-driver/pom.xml @@ -0,0 +1,127 @@ + + + 4.0.0 + + + com.facebook.presto + presto-root + 0.107 + + + presto-benchmark-driver + presto-benchmark-driver + + + ${project.parent.basedir} + com.facebook.presto.benchmark.driver.PrestoBenchmarkDriver + + + + + com.facebook.presto + presto-client + + + + io.airlift + airline + + + + io.airlift + discovery + + + + io.airlift + http-client + + + + io.airlift + json + + + + io.airlift + units + + + + io.airlift + log-manager + + + + com.fasterxml.jackson.core + jackson-annotations + + + + javax.inject + javax.inject + + + + com.google.guava + guava + + + + org.apache.commons + commons-math3 + + + + + org.testng + testng + test + + + + + + + org.apache.maven.plugins + maven-shade-plugin + + + package + + shade + + + true + executable + + + + ${main-class} + + + + + + + + + + org.skife.maven + really-executable-jar-maven-plugin + + -Xmx1G + executable + + + + package + + really-executable-jar + + + + + + + diff --git a/presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/BenchmarkDriver.java b/presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/BenchmarkDriver.java new file mode 100644 index 00000000..088f7d54 --- /dev/null +++ b/presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/BenchmarkDriver.java @@ -0,0 +1,89 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark.driver; + +import com.facebook.presto.client.ClientSession; +import com.google.common.collect.ImmutableList; +import com.google.common.net.HostAndPort; + +import java.io.Closeable; +import java.util.List; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class BenchmarkDriver + implements Closeable +{ + private final ClientSession clientSession; + private final List queries; + private final BenchmarkResultsStore resultsStore; + private final BenchmarkQueryRunner queryRunner; + + public BenchmarkDriver(BenchmarkResultsStore resultsStore, + ClientSession clientSession, + Iterable queries, + int warm, + int runs, + boolean debug, + int maxFailures, + Optional socksProxy) + { + this.resultsStore = checkNotNull(resultsStore, "resultsStore is null"); + this.clientSession = checkNotNull(clientSession, "clientSession is null"); + this.queries = ImmutableList.copyOf(checkNotNull(queries, "queries is null")); + + queryRunner = new BenchmarkQueryRunner(warm, runs, debug, maxFailures, clientSession.getServer(), socksProxy); + } + + public void run(Suite suite) + throws Exception + { + // select queries to run + List queries = suite.selectQueries(this.queries); + if (queries.isEmpty()) { + return; + } + + ClientSession session = ClientSession.withSessionProperties(clientSession, suite.getSessionProperties()); + + // select schemas to use + List benchmarkSchemas; + if (!suite.getSchemaNameTemplates().isEmpty()) { + List schemas = queryRunner.getSchemas(session); + benchmarkSchemas = suite.selectSchemas(schemas); + } + else { + benchmarkSchemas = ImmutableList.of(new BenchmarkSchema(session.getSchema())); + } + if (benchmarkSchemas.isEmpty()) { + return; + } + + for (BenchmarkSchema benchmarkSchema : benchmarkSchemas) { + for (BenchmarkQuery benchmarkQuery : queries) { + session = ClientSession.withCatalogAndSchema(session, session.getCatalog(), benchmarkSchema.getName()); + BenchmarkQueryResult result = queryRunner.execute(suite, session, benchmarkQuery); + + resultsStore.store(benchmarkSchema, result); + } + } + } + + @Override + public void close() + { + queryRunner.close(); + } +} diff --git a/presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/BenchmarkDriverExecutionException.java b/presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/BenchmarkDriverExecutionException.java new file mode 100644 index 00000000..1621e4c7 --- /dev/null +++ b/presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/BenchmarkDriverExecutionException.java @@ -0,0 +1,25 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark.driver; + +import static java.util.Objects.requireNonNull; + +public class BenchmarkDriverExecutionException + extends RuntimeException +{ + public BenchmarkDriverExecutionException(String message, RuntimeException cause) + { + super(requireNonNull(message, "message is null"), requireNonNull(cause, "cause is null")); + } +} diff --git a/presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/BenchmarkDriverOptions.java b/presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/BenchmarkDriverOptions.java new file mode 100644 index 00000000..de6a90c6 --- /dev/null +++ b/presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/BenchmarkDriverOptions.java @@ -0,0 +1,217 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark.driver; + +import com.facebook.presto.client.ClientSession; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableMap; +import com.google.common.net.HostAndPort; +import io.airlift.command.Option; + +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.CharsetEncoder; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.TimeZone; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.nio.charset.StandardCharsets.US_ASCII; +import static java.util.Locale.ENGLISH; + +public class BenchmarkDriverOptions +{ + @Option(name = "--server", title = "server", description = "Presto server location (default: localhost:8080)") + public String server = "localhost:8080"; + + @Option(name = "--user", title = "user", description = "Username") + public String user = System.getProperty("user.name"); + + @Option(name = "--catalog", title = "catalog", description = "Default catalog (default: default)") + public String catalog = "default"; + + @Option(name = "--schema", title = "schema", description = "Default schema (default: default)") + public String schema = "default"; + + @Option(name = "--suite", title = "suite", description = "Suite to execute") + public List suites = new ArrayList<>(); + + @Option(name = "--suite-config", title = "suite-config", description = "Suites configuration file (default: suite.json)") + public String suiteConfigFile = "suite.json"; + + @Option(name = "--sql", title = "sql", description = "Directory containing sql files (default: sql)") + public String sqlTemplateDir = "sql"; + + @Option(name = "--query", title = "query", description = "Queries to execute") + public List queries = new ArrayList<>(); + + @Option(name = "--debug", title = "debug", description = "Enable debug information (default: false)") + public boolean debug; + + @Option(name = "--session", title = "session", description = "Session property (property can be used multiple times; format is key=value)") + public final List sessionProperties = new ArrayList<>(); + + @Option(name = "--runs", title = "runs", description = "Number of times to run each query (default: 3)") + public int runs = 3; + + @Option(name = "--warm", title = "warm", description = "Number of times to run each query for a warm-up (default: 1)") + public int warm = 1; + + @Option(name = "--max-failures", title = "max failures", description = "Max number of consecutive failures before benchmark fails") + public int maxFailures = 10; + + @Option(name = "--socks", title = "socks", description = "Socks proxy to use") + public HostAndPort socksProxy; + + public ClientSession getClientSession() + { + return new ClientSession( + parseServer(server), + user, + "presto-benchmark", + catalog, + schema, + TimeZone.getDefault().getID(), + Locale.getDefault(), + toProperties(this.sessionProperties), + debug); + } + + private static URI parseServer(String server) + { + server = server.toLowerCase(ENGLISH); + if (server.startsWith("http://") || server.startsWith("https://")) { + return URI.create(server); + } + + HostAndPort host = HostAndPort.fromString(server); + try { + return new URI("http", null, host.getHostText(), host.getPortOrDefault(80), null, null, null); + } + catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } + } + + private static Map toProperties(List sessionProperties) + { + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (ClientSessionProperty sessionProperty : sessionProperties) { + String name = sessionProperty.getName(); + if (sessionProperty.getCatalog().isPresent()) { + name = sessionProperty.getCatalog().get() + "." + name; + } + builder.put(name, sessionProperty.getValue()); + } + return builder.build(); + } + + public static final class ClientSessionProperty + { + private static final Splitter NAME_VALUE_SPLITTER = Splitter.on('=').limit(2); + private static final Splitter NAME_SPLITTER = Splitter.on('.'); + private final Optional catalog; + private final String name; + private final String value; + + public ClientSessionProperty(String property) + { + List nameValue = NAME_VALUE_SPLITTER.splitToList(property); + checkArgument(nameValue.size() == 2, "Session property: %s", property); + + List nameParts = NAME_SPLITTER.splitToList(nameValue.get(0)); + checkArgument(nameParts.size() == 1 || nameParts.size() == 2, "Invalid session property: %s", property); + if (nameParts.size() == 1) { + catalog = Optional.empty(); + name = nameParts.get(0); + } + else { + catalog = Optional.of(nameParts.get(0)); + name = nameParts.get(1); + } + value = nameValue.get(1); + + verifyProperty(catalog, name, value); + } + + public ClientSessionProperty(Optional catalog, String name, String value) + { + this.catalog = checkNotNull(catalog, "catalog is null"); + this.name = checkNotNull(name, "name is null"); + this.value = checkNotNull(value, "value is null"); + + verifyProperty(catalog, name, value); + } + + private static void verifyProperty(Optional catalog, String name, String value) + { + checkArgument(!catalog.isPresent() || !catalog.get().isEmpty(), "Invalid session property: %s.%s:%s", catalog, name, value); + checkArgument(!name.isEmpty(), "Session property name is empty"); + + CharsetEncoder charsetEncoder = US_ASCII.newEncoder(); + checkArgument(catalog.orElse("").indexOf('=') < 0, "Session property catalog must not contain '=': %s", name); + checkArgument(charsetEncoder.canEncode(catalog.orElse("")), "Session property catalog is not US_ASCII: %s", name); + checkArgument(name.indexOf('=') < 0, "Session property name must not contain '=': %s", name); + checkArgument(charsetEncoder.canEncode(name), "Session property name is not US_ASCII: %s", name); + checkArgument(charsetEncoder.canEncode(value), "Session property value is not US_ASCII: %s", value); + } + + public Optional getCatalog() + { + return catalog; + } + + public String getName() + { + return name; + } + + public String getValue() + { + return value; + } + + @Override + public String toString() + { + return (catalog.isPresent() ? catalog.get() + '.' : "") + name + '=' + value; + } + + @Override + public int hashCode() + { + return Objects.hash(catalog, name, value); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + ClientSessionProperty other = (ClientSessionProperty) obj; + return Objects.equals(this.catalog, other.catalog) && + Objects.equals(this.name, other.name) && + Objects.equals(this.value, other.value); + } + } +} diff --git a/presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/BenchmarkQuery.java b/presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/BenchmarkQuery.java new file mode 100644 index 00000000..b43ebefb --- /dev/null +++ b/presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/BenchmarkQuery.java @@ -0,0 +1,91 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark.driver; + +import com.google.common.base.Function; +import com.google.common.base.MoreObjects; +import com.google.common.base.Splitter; +import com.google.common.base.Splitter.MapSplitter; +import com.google.common.collect.ImmutableMap; +import com.google.common.io.Files; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +import static com.google.common.base.Preconditions.checkNotNull; + +public final class BenchmarkQuery +{ + private static final Splitter SECTION_SPLITTER = Splitter.on(Pattern.compile("\n\\s*=+\\s*\n", Pattern.MULTILINE)).limit(2).trimResults(); + private static final MapSplitter TAGS_SPLITTER = Splitter.on("\n").trimResults().withKeyValueSeparator('='); + + private final String name; + private final Map tags; + private final String sql; + + public BenchmarkQuery(File file) + throws IOException + { + checkNotNull(file, "file is null"); + + name = Files.getNameWithoutExtension(file.getName()); + + // file can have 2 sections separated by a line of equals signs + String text = Files.toString(file, StandardCharsets.UTF_8); + List sections = SECTION_SPLITTER.splitToList(text); + if (sections.size() == 2) { + this.tags = ImmutableMap.copyOf(TAGS_SPLITTER.split(sections.get(0))); + this.sql = sections.get(1); + } + else { + // no tags + this.tags = ImmutableMap.of(); + this.sql = sections.get(0); + } + } + + public static Function queryNameGetter() + { + return query -> query.getName(); + } + + public String getName() + { + return name; + } + + public Map getTags() + { + return tags; + } + + public String getSql() + { + return sql; + } + + @Override + public String toString() + { + return MoreObjects.toStringHelper(this) + .add("name", name) + .add("tags", tags) + .add("sql", sql) + .toString(); + } +} diff --git a/presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/BenchmarkQueryResult.java b/presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/BenchmarkQueryResult.java new file mode 100644 index 00000000..292da417 --- /dev/null +++ b/presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/BenchmarkQueryResult.java @@ -0,0 +1,124 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark.driver; + +import com.google.common.base.MoreObjects; +import io.airlift.units.Duration; + +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.Objects.requireNonNull; +import static java.util.concurrent.TimeUnit.NANOSECONDS; + +public class BenchmarkQueryResult +{ + public enum Status + { + PASS, FAIL + } + + private static final Stat FAIL_STAT = new Stat(new double[0]); + + public static BenchmarkQueryResult passResult(Suite suite, BenchmarkQuery benchmarkQuery, Stat wallTimeNanos, Stat processCpuTimeNanos, Stat queryCpuTimeNanos) + { + return new BenchmarkQueryResult(suite, benchmarkQuery, Status.PASS, Optional.empty(), wallTimeNanos, processCpuTimeNanos, queryCpuTimeNanos); + } + + public static BenchmarkQueryResult failResult(Suite suite, BenchmarkQuery benchmarkQuery, String errorMessage) + { + return new BenchmarkQueryResult(suite, benchmarkQuery, Status.FAIL, Optional.of(errorMessage), FAIL_STAT, FAIL_STAT, FAIL_STAT); + } + + private final Suite suite; + private final BenchmarkQuery benchmarkQuery; + private final Status status; + private final Optional errorMessage; + private final Stat wallTimeNanos; + private final Stat processCpuTimeNanos; + private final Stat queryCpuTimeNanos; + + private BenchmarkQueryResult( + Suite suite, + BenchmarkQuery benchmarkQuery, + Status status, + Optional errorMessage, + Stat wallTimeNanos, + Stat processCpuTimeNanos, + Stat queryCpuTimeNanos) + { + this.suite = checkNotNull(suite, "suite is null"); + this.benchmarkQuery = checkNotNull(benchmarkQuery, "benchmarkQuery is null"); + this.status = checkNotNull(status, "status is null"); + this.errorMessage = requireNonNull(errorMessage, "errorMessage is null"); + this.wallTimeNanos = checkNotNull(wallTimeNanos, "wallTimeNanos is null"); + this.processCpuTimeNanos = checkNotNull(processCpuTimeNanos, "processCpuTimeNanos is null"); + this.queryCpuTimeNanos = checkNotNull(queryCpuTimeNanos, "queryCpuTimeNanos is null"); + } + + public Suite getSuite() + { + return suite; + } + + public BenchmarkQuery getBenchmarkQuery() + { + return benchmarkQuery; + } + + public Status getStatus() + { + return status; + } + + public Optional getErrorMessage() + { + return errorMessage; + } + + public Stat getWallTimeNanos() + { + return wallTimeNanos; + } + + public Stat getProcessCpuTimeNanos() + { + return processCpuTimeNanos; + } + + public Stat getQueryCpuTimeNanos() + { + return queryCpuTimeNanos; + } + + @Override + public String toString() + { + return MoreObjects.toStringHelper(this) + .add("suite", suite.getName()) + .add("benchmarkQuery", benchmarkQuery.getName()) + .add("status", status) + .add("wallTimeMedian", new Duration(wallTimeNanos.getMedian(), NANOSECONDS).convertToMostSuccinctTimeUnit()) + .add("wallTimeMean", new Duration(wallTimeNanos.getMean(), NANOSECONDS).convertToMostSuccinctTimeUnit()) + .add("wallTimeStd", new Duration(wallTimeNanos.getStandardDeviation(), NANOSECONDS).convertToMostSuccinctTimeUnit()) + .add("processCpuTimeMedian", new Duration(processCpuTimeNanos.getMedian(), NANOSECONDS).convertToMostSuccinctTimeUnit()) + .add("processCpuTimeMean", new Duration(processCpuTimeNanos.getMean(), NANOSECONDS).convertToMostSuccinctTimeUnit()) + .add("processCpuTimeStd", new Duration(processCpuTimeNanos.getStandardDeviation(), NANOSECONDS).convertToMostSuccinctTimeUnit()) + .add("queryCpuTimeMedian", new Duration(queryCpuTimeNanos.getMedian(), NANOSECONDS).convertToMostSuccinctTimeUnit()) + .add("queryCpuTimeMean", new Duration(queryCpuTimeNanos.getMean(), NANOSECONDS).convertToMostSuccinctTimeUnit()) + .add("queryCpuTimeStd", new Duration(queryCpuTimeNanos.getStandardDeviation(), NANOSECONDS).convertToMostSuccinctTimeUnit()) + .add("error", errorMessage) + .toString(); + } +} diff --git a/presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/BenchmarkQueryRunner.java b/presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/BenchmarkQueryRunner.java new file mode 100644 index 00000000..58edf4b8 --- /dev/null +++ b/presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/BenchmarkQueryRunner.java @@ -0,0 +1,279 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark.driver; + +import com.facebook.presto.client.ClientSession; +import com.facebook.presto.client.QueryError; +import com.facebook.presto.client.QueryResults; +import com.facebook.presto.client.StatementClient; +import com.facebook.presto.client.StatementStats; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.common.net.HostAndPort; +import io.airlift.discovery.client.ServiceDescriptor; +import io.airlift.discovery.client.ServiceDescriptorsRepresentation; +import io.airlift.http.client.HttpClient; +import io.airlift.http.client.HttpClientConfig; +import io.airlift.http.client.JsonResponseHandler; +import io.airlift.http.client.Request; +import io.airlift.http.client.jetty.JettyHttpClient; +import io.airlift.json.JsonCodec; +import io.airlift.units.Duration; + +import java.io.Closeable; +import java.net.URI; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import static com.facebook.presto.benchmark.driver.BenchmarkQueryResult.failResult; +import static com.facebook.presto.benchmark.driver.BenchmarkQueryResult.passResult; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.airlift.http.client.HttpUriBuilder.uriBuilderFrom; +import static io.airlift.http.client.JsonResponseHandler.createJsonResponseHandler; +import static io.airlift.http.client.Request.Builder.prepareGet; +import static io.airlift.http.client.StringResponseHandler.createStringResponseHandler; +import static io.airlift.json.JsonCodec.jsonCodec; +import static java.lang.Long.parseLong; +import static java.lang.String.format; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +public class BenchmarkQueryRunner + implements Closeable +{ + private final int warm; + private final int runs; + private final boolean debug; + private final int maxFailures; + + private final HttpClient httpClient; + private final List nodes; + private final JsonCodec queryResultsCodec; + + private int failures; + + public BenchmarkQueryRunner(int warm, int runs, boolean debug, int maxFailures, URI serverUri, Optional socksProxy) + { + checkArgument(warm >= 0, "warm is negative"); + this.warm = warm; + + checkArgument(runs >= 1, "runs must be at least 1"); + this.runs = runs; + + checkArgument(maxFailures >= 0, "maxFailures must be at least 0"); + this.maxFailures = maxFailures; + + this.debug = debug; + + this.queryResultsCodec = jsonCodec(QueryResults.class); + + checkNotNull(socksProxy, "socksProxy is null"); + HttpClientConfig httpClientConfig = new HttpClientConfig(); + if (socksProxy.isPresent()) { + httpClientConfig.setSocksProxy(socksProxy.get()); + } + + this.httpClient = new JettyHttpClient(httpClientConfig.setConnectTimeout(new Duration(10, TimeUnit.SECONDS))); + + nodes = getAllNodes(checkNotNull(serverUri, "serverUri is null")); + } + + @SuppressWarnings("AssignmentToForLoopParameter") + public BenchmarkQueryResult execute(Suite suite, ClientSession session, BenchmarkQuery query) + { + failures = 0; + for (int i = 0; i < warm; ) { + try { + execute(session, query.getName(), query.getSql()); + i++; + failures = 0; + } + catch (BenchmarkDriverExecutionException e) { + return failResult(suite, query, e.getCause().getMessage()); + } + catch (Exception e) { + handleFailure(e); + } + } + + double[] wallTimeNanos = new double[runs]; + double[] processCpuTimeNanos = new double[runs]; + double[] queryCpuTimeNanos = new double[runs]; + for (int i = 0; i < runs; ) { + try { + long startCpuTime = getTotalCpuTime(); + long startWallTime = System.nanoTime(); + + StatementStats statementStats = execute(session, query.getName(), query.getSql()); + + long endWallTime = System.nanoTime(); + long endCpuTime = getTotalCpuTime(); + + wallTimeNanos[i] = endWallTime - startWallTime; + processCpuTimeNanos[i] = endCpuTime - startCpuTime; + queryCpuTimeNanos[i] = MILLISECONDS.toNanos(statementStats.getCpuTimeMillis()); + + i++; + failures = 0; + } + catch (BenchmarkDriverExecutionException e) { + return failResult(suite, query, e.getCause().getMessage()); + } + catch (Exception e) { + handleFailure(e); + } + } + + return passResult( + suite, + query, + new Stat(wallTimeNanos), + new Stat(processCpuTimeNanos), + new Stat(queryCpuTimeNanos)); + } + + public List getSchemas(ClientSession session) + { + failures = 0; + while (true) { + // start query + StatementClient client = new StatementClient(httpClient, queryResultsCodec, session, "show schemas"); + + // read query output + ImmutableList.Builder schemas = ImmutableList.builder(); + while (client.isValid() && client.advance()) { + // we do not process the output + Iterable> data = client.current().getData(); + if (data != null) { + for (List objects : data) { + schemas.add(objects.get(0).toString()); + } + } + } + + // verify final state + if (client.isClosed()) { + throw new IllegalStateException("Query aborted by user"); + } + + if (client.isGone()) { + throw new IllegalStateException("Query is gone (server restarted?)"); + } + + QueryError resultsError = client.finalResults().getError(); + if (resultsError != null) { + RuntimeException cause = null; + if (resultsError.getFailureInfo() != null) { + cause = resultsError.getFailureInfo().toException(); + } + handleFailure(cause); + + continue; + } + + return schemas.build(); + } + } + + private StatementStats execute(ClientSession session, String name, String query) + { + // start query + StatementClient client = new StatementClient(httpClient, queryResultsCodec, session, query); + + // read query output + while (client.isValid() && client.advance()) { + // we do not process the output + } + + // verify final state + if (client.isClosed()) { + throw new IllegalStateException("Query aborted by user"); + } + + if (client.isGone()) { + throw new IllegalStateException("Query is gone (server restarted?)"); + } + + QueryError resultsError = client.finalResults().getError(); + if (resultsError != null) { + RuntimeException cause = null; + if (resultsError.getFailureInfo() != null) { + cause = resultsError.getFailureInfo().toException(); + } + + throw new BenchmarkDriverExecutionException(format("Query %s failed: %s", name, resultsError.getMessage()), cause); + } + + return client.finalResults().getStats(); + } + + @Override + public void close() + { + httpClient.close(); + } + + @SuppressWarnings("CallToPrintStackTrace") + public void handleFailure(Exception e) + { + if (debug) { + if (e == null) { + e = new RuntimeException("Unknown error"); + } + e.printStackTrace(); + } + + failures++; + + if (failures > maxFailures) { + throw new RuntimeException("To many consecutive failures"); + } + + try { + TimeUnit.SECONDS.sleep(5); + } + catch (InterruptedException interruptedException) { + Thread.currentThread().interrupt(); + throw Throwables.propagate(interruptedException); + } + } + + private long getTotalCpuTime() + { + long totalCpuTime = 0; + for (URI server : nodes) { + URI addressUri = uriBuilderFrom(server).replacePath("/v1/jmx/mbean/java.lang:type=OperatingSystem/ProcessCpuTime").build(); + String data = httpClient.execute(prepareGet().setUri(addressUri).build(), createStringResponseHandler()).getBody(); + totalCpuTime += parseLong(data.trim()); + } + return TimeUnit.NANOSECONDS.toNanos(totalCpuTime); + } + + private List getAllNodes(URI server) + { + Request request = prepareGet().setUri(uriBuilderFrom(server).replacePath("/v1/service/presto").build()).build(); + JsonResponseHandler responseHandler = createJsonResponseHandler(jsonCodec(ServiceDescriptorsRepresentation.class)); + ServiceDescriptorsRepresentation serviceDescriptors = httpClient.execute(request, responseHandler); + + ImmutableList.Builder addresses = ImmutableList.builder(); + for (ServiceDescriptor serviceDescriptor : serviceDescriptors.getServiceDescriptors()) { + String httpUri = serviceDescriptor.getProperties().get("http"); + if (httpUri != null) { + addresses.add(URI.create(httpUri)); + } + } + return addresses.build(); + } +} diff --git a/presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/BenchmarkResultsPrinter.java b/presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/BenchmarkResultsPrinter.java new file mode 100644 index 00000000..dc4149d2 --- /dev/null +++ b/presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/BenchmarkResultsPrinter.java @@ -0,0 +1,115 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark.driver; + +import com.google.common.base.Joiner; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.SortedSet; +import java.util.TreeSet; + +import static com.google.common.base.CharMatcher.anyOf; +import static com.google.common.base.Functions.forMap; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.Iterables.getFirst; +import static com.google.common.collect.Iterables.transform; +import static java.util.concurrent.TimeUnit.NANOSECONDS; + +public class BenchmarkResultsPrinter + implements BenchmarkResultsStore +{ + private final List tagNames; + + public BenchmarkResultsPrinter(Iterable suites, Iterable queries) + { + this(getSelectedQueryTagNames(suites, queries)); + } + + private static List getSelectedQueryTagNames(Iterable suites, Iterable queries) + { + SortedSet tags = new TreeSet<>(); + for (Suite suite : suites) { + for (BenchmarkQuery query : suite.selectQueries(queries)) { + tags.addAll(query.getTags().keySet()); + } + + for (RegexTemplate regexTemplate : suite.getSchemaNameTemplates()) { + tags.addAll(regexTemplate.getFieldNames()); + } + } + return ImmutableList.copyOf(tags); + } + + public BenchmarkResultsPrinter(List tagNames) + { + this.tagNames = checkNotNull(tagNames, "tagNames is null"); + + // print header row + printRow(ImmutableList.builder() + .add("suite") + .add("query") + .addAll(tagNames) + .add("wallTimeP50") + .add("wallTimeMean") + .add("wallTimeStd") + .add("processCpuTimeP50") + .add("processCpuTimeMean") + .add("processCpuTimeStd") + .add("queryCpuTimeP50") + .add("queryCpuTimeMean") + .add("queryCpuTimeStd") + .add("status") + .add("error") + .build()); + } + + @Override + public void store(BenchmarkSchema benchmarkSchema, BenchmarkQueryResult result) + { + Map tags = new LinkedHashMap<>(); + tags.putAll(result.getBenchmarkQuery().getTags()); + tags.putAll(benchmarkSchema.getTags()); + + // only print first line of error message + Optional errorMessage = result.getErrorMessage().map(error -> getFirst(Splitter.on(anyOf("\r\n")).trimResults().split(error), "")); + + printRow(ImmutableList.builder() + .add(result.getSuite().getName()) + .add(result.getBenchmarkQuery().getName()) + .addAll(transform(tagNames, forMap(tags, ""))) + .add(NANOSECONDS.toMillis((long) result.getWallTimeNanos().getMedian())) + .add(NANOSECONDS.toMillis((long) result.getWallTimeNanos().getMean())) + .add(NANOSECONDS.toMillis((long) result.getWallTimeNanos().getStandardDeviation())) + .add(NANOSECONDS.toMillis((long) result.getProcessCpuTimeNanos().getMedian())) + .add(NANOSECONDS.toMillis((long) result.getProcessCpuTimeNanos().getMean())) + .add(NANOSECONDS.toMillis((long) result.getProcessCpuTimeNanos().getStandardDeviation())) + .add(NANOSECONDS.toMillis((long) result.getQueryCpuTimeNanos().getMedian())) + .add(NANOSECONDS.toMillis((long) result.getQueryCpuTimeNanos().getMean())) + .add(NANOSECONDS.toMillis((long) result.getQueryCpuTimeNanos().getStandardDeviation())) + .add(result.getStatus().toString().toLowerCase()) + .add(errorMessage.orElse("")) + .build()); + } + + @SuppressWarnings("UseOfSystemOutOrSystemErr") + private static void printRow(Iterable values) + { + System.out.println(Joiner.on('\t').join(values)); + } +} diff --git a/presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/BenchmarkResultsStore.java b/presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/BenchmarkResultsStore.java new file mode 100644 index 00000000..045d7418 --- /dev/null +++ b/presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/BenchmarkResultsStore.java @@ -0,0 +1,19 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark.driver; + +public interface BenchmarkResultsStore +{ + void store(BenchmarkSchema benchmarkSchema, BenchmarkQueryResult result); +} diff --git a/presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/BenchmarkSchema.java b/presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/BenchmarkSchema.java new file mode 100644 index 00000000..d1b52fd7 --- /dev/null +++ b/presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/BenchmarkSchema.java @@ -0,0 +1,55 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark.driver; + +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableMap; + +import java.util.Map; + +public class BenchmarkSchema +{ + private final String name; + private final Map tags; + + public BenchmarkSchema(String name) + { + this(name, ImmutableMap.of()); + } + + public BenchmarkSchema(String name, Map tags) + { + this.name = name; + this.tags = tags; + } + + public String getName() + { + return name; + } + + public Map getTags() + { + return tags; + } + + @Override + public String toString() + { + return MoreObjects.toStringHelper(this) + .add("name", name) + .add("tags", tags) + .toString(); + } +} diff --git a/presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/PrestoBenchmarkDriver.java b/presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/PrestoBenchmarkDriver.java new file mode 100644 index 00000000..209d82bb --- /dev/null +++ b/presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/PrestoBenchmarkDriver.java @@ -0,0 +1,172 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark.driver; + +import com.facebook.presto.client.ClientSession; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import io.airlift.command.Command; +import io.airlift.command.HelpOption; +import io.airlift.log.Level; +import io.airlift.log.Logging; +import io.airlift.log.LoggingConfiguration; + +import javax.inject.Inject; + +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import static com.google.common.io.ByteStreams.nullOutputStream; +import static io.airlift.command.SingleCommand.singleCommand; +import static java.util.function.Function.identity; + +@Command(name = "presto-benchmark", description = "Presto benchmark driver") +public class PrestoBenchmarkDriver +{ + @Inject + public HelpOption helpOption; + + @Inject + public BenchmarkDriverOptions benchmarkDriverOptions = new BenchmarkDriverOptions(); + + public static void main(String[] args) + throws Exception + { + new PrestoBenchmarkDriver().run(args); + } + + protected void run(String[] args) + throws Exception + { + PrestoBenchmarkDriver prestoBenchmarkDriver = singleCommand(PrestoBenchmarkDriver.class).parse(args); + + if (prestoBenchmarkDriver.helpOption.showHelpIfRequested()) { + return; + } + + BenchmarkDriverOptions driverOptions = prestoBenchmarkDriver.benchmarkDriverOptions; + + initializeLogging(driverOptions.debug); + + // select suites + List suites = Suite.readSuites(new File(driverOptions.suiteConfigFile)); + if (!driverOptions.suites.isEmpty()) { + suites = suites.stream() + .filter(suite -> driverOptions.suites.contains(suite.getName())) + .collect(Collectors.toList()); + } + suites = ImmutableList.copyOf(suites); + + // load queries + File queriesDir = new File(driverOptions.sqlTemplateDir); + List allQueries = readQueries(queriesDir); + + // select queries to run + Set queries; + if (driverOptions.queries.isEmpty()) { + queries = suites.stream() + .map(suite -> suite.selectQueries(allQueries)) + .flatMap(List::stream) + .collect(Collectors.toSet()); + } + else { + queries = driverOptions.queries.stream() + .map(Pattern::compile) + .map(pattern -> allQueries.stream().filter(query -> pattern.matcher(query.getName()).matches())) + .flatMap(identity()) + .collect(Collectors.toSet()); + } + + // create results store + BenchmarkResultsStore resultsStore = getResultsStore(suites, queries); + + // create session + ClientSession session = driverOptions.getClientSession(); + + try (BenchmarkDriver benchmarkDriver = new BenchmarkDriver( + resultsStore, + session, + queries, + driverOptions.warm, + driverOptions.runs, + driverOptions.debug, + driverOptions.maxFailures, + Optional.ofNullable(driverOptions.socksProxy))) { + for (Suite suite : suites) { + benchmarkDriver.run(suite); + } + } + } + + protected BenchmarkResultsStore getResultsStore(List suites, Set queries) + { + return new BenchmarkResultsPrinter(suites, queries); + } + + private static List readQueries(File queriesDir) + throws IOException + { + File[] files = queriesDir.listFiles(); + if (files == null) { + return ImmutableList.of(); + } + Arrays.sort(files); + + ImmutableList.Builder queries = ImmutableList.builder(); + for (File file : files) { + String fileName = file.getName(); + if (fileName.endsWith(".sql")) { + queries.add(new BenchmarkQuery(file)); + } + } + return queries.build(); + } + + @SuppressWarnings("UseOfSystemOutOrSystemErr") + public static void initializeLogging(boolean debug) + { + // unhook out and err while initializing logging or logger will print to them + PrintStream out = System.out; + PrintStream err = System.err; + try { + if (debug) { + Logging logging = Logging.initialize(); + logging.configure(new LoggingConfiguration()); + logging.setLevel("com.facebook.presto", Level.DEBUG); + } + else { + System.setOut(new PrintStream(nullOutputStream())); + System.setErr(new PrintStream(nullOutputStream())); + + Logging logging = Logging.initialize(); + logging.configure(new LoggingConfiguration()); + logging.disableConsole(); + } + } + catch (IOException e) { + throw Throwables.propagate(e); + } + finally { + System.setOut(out); + System.setErr(err); + } + } +} diff --git a/presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/RegexTemplate.java b/presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/RegexTemplate.java new file mode 100644 index 00000000..32867732 --- /dev/null +++ b/presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/RegexTemplate.java @@ -0,0 +1,103 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark.driver; + +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableBiMap; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSortedMap; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class RegexTemplate +{ + private static final Method NAMED_GROUPS_METHOD; + + static { + try { + NAMED_GROUPS_METHOD = Pattern.class.getDeclaredMethod("namedGroups"); + NAMED_GROUPS_METHOD.setAccessible(true); + } + catch (NoSuchMethodException e) { + throw Throwables.propagate(e); + } + } + + private final String template; + private final Pattern pattern; + private final List fieldNames; + + public RegexTemplate(String template) + { + this.template = checkNotNull(template, "template is null"); + + try { + this.pattern = Pattern.compile(template); + } + catch (Exception e) { + throw new IllegalArgumentException("Invalid template: " + template, e); + } + + Map namedGroups; + try { + namedGroups = (Map) NAMED_GROUPS_METHOD.invoke(pattern); + } + catch (Exception e) { + throw Throwables.propagate(e); + } + ImmutableSortedMap sortedGroups = ImmutableSortedMap.copyOf(ImmutableBiMap.copyOf(namedGroups).inverse()); + this.fieldNames = ImmutableList.copyOf(sortedGroups.values()); + } + + public List getFieldNames() + { + return fieldNames; + } + + public Optional> parse(String value) + { + checkNotNull(value, "value is null"); + + Matcher matcher = pattern.matcher(value); + if (!matcher.matches()) { + return Optional.empty(); + } + + ImmutableMap.Builder fieldsBuilder = ImmutableMap.builder(); + for (int index = 0; index < fieldNames.size(); index++) { + String fieldName = fieldNames.get(index); + String fieldValue = matcher.group(index + 1); + if (fieldValue != null) { + fieldsBuilder.put(fieldName, fieldValue); + } + } + + Map fields = fieldsBuilder.build(); + return Optional.of(fields); + } + + @Override + public String toString() + { + return template; + } +} diff --git a/presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/Stat.java b/presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/Stat.java new file mode 100644 index 00000000..b28e1c61 --- /dev/null +++ b/presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/Stat.java @@ -0,0 +1,58 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark.driver; + +import com.google.common.base.MoreObjects; +import org.apache.commons.math3.stat.descriptive.moment.Mean; +import org.apache.commons.math3.stat.descriptive.moment.StandardDeviation; +import org.apache.commons.math3.stat.descriptive.rank.Median; + +public class Stat +{ + private final double mean; + private final double standardDeviation; + private final double median; + + public Stat(double[] values) + { + mean = new Mean().evaluate(values); + standardDeviation = new StandardDeviation().evaluate(values); + median = new Median().evaluate(values); + } + + public double getMean() + { + return mean; + } + + public double getStandardDeviation() + { + return standardDeviation; + } + + public double getMedian() + { + return median; + } + + @Override + public String toString() + { + return MoreObjects.toStringHelper(this) + .add("mean", mean) + .add("standardDeviation", standardDeviation) + .add("median", median) + .toString(); + } +} diff --git a/presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/Suite.java b/presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/Suite.java new file mode 100644 index 00000000..237e553d --- /dev/null +++ b/presto-benchmark-driver/src/main/java/com/facebook/presto/benchmark/driver/Suite.java @@ -0,0 +1,157 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark.driver; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.airlift.json.JsonCodec.mapJsonCodec; + +public class Suite +{ + private final String name; + private final Map sessionProperties; + private final List schemaNameTemplates; + private final List queryNamePatterns; + + public Suite(String name, Map sessionProperties, Iterable schemaNameTemplates, Iterable queryNamePatterns) + { + this.name = checkNotNull(name, "name is null"); + this.sessionProperties = sessionProperties == null ? ImmutableMap.of() : ImmutableMap.copyOf(sessionProperties); + this.schemaNameTemplates = ImmutableList.copyOf(checkNotNull(schemaNameTemplates, "schemaNameTemplates is null")); + this.queryNamePatterns = ImmutableList.copyOf(checkNotNull(queryNamePatterns, "queryNamePatterns is null")); + } + + public String getName() + { + return name; + } + + public Map getSessionProperties() + { + return sessionProperties; + } + + public List getSchemaNameTemplates() + { + return schemaNameTemplates; + } + + public List getQueryNamePatterns() + { + return queryNamePatterns; + } + + public List selectSchemas(Iterable schemas) + { + if (schemaNameTemplates.isEmpty()) { + return ImmutableList.of(); + } + + ImmutableList.Builder benchmarkSchemas = ImmutableList.builder(); + for (RegexTemplate schemaNameTemplate : schemaNameTemplates) { + for (String schema : schemas) { + Optional> tags = schemaNameTemplate.parse(schema); + if (tags.isPresent()) { + benchmarkSchemas.add(new BenchmarkSchema(schema, tags.get())); + } + } + } + return benchmarkSchemas.build(); + } + + public List selectQueries(Iterable queries) + { + if (getQueryNamePatterns().isEmpty()) { + return ImmutableList.copyOf(queries); + } + + List filteredQueries = StreamSupport.stream(queries.spliterator(), false) + .filter(query -> getQueryNamePatterns().stream().anyMatch(pattern -> pattern.matcher(query.getName()).matches())) + .collect(Collectors.toList()); + + return ImmutableList.copyOf(filteredQueries); + } + + @Override + public String toString() + { + return MoreObjects.toStringHelper(this) + .add("name", name) + .add("sessionProperties", sessionProperties) + .add("queryNamePatterns", queryNamePatterns) + .toString(); + } + + public static List readSuites(File file) + throws IOException + { + checkNotNull(file, "file is null"); + checkArgument(file.canRead(), "Can not read file: %s" + file); + byte[] json = Files.readAllBytes(file.toPath()); + Map options = mapJsonCodec(String.class, OptionsJson.class).fromJson(json); + ImmutableList.Builder runOptions = ImmutableList.builder(); + for (Entry entry : options.entrySet()) { + runOptions.add(entry.getValue().toSuite(entry.getKey())); + } + return runOptions.build(); + } + + public static class OptionsJson + { + private final List schema; + private final Map session; + private final List query; + + @JsonCreator + public OptionsJson( + @JsonProperty("schema") List schema, + @JsonProperty("session") Map session, + @JsonProperty("query") List query) + { + this.schema = checkNotNull(ImmutableList.copyOf(schema), "schema is null"); + this.session = checkNotNull(ImmutableMap.copyOf(session), "session is null"); + this.query = checkNotNull(query, "query is null"); + } + + public Suite toSuite(String name) + { + ImmutableList.Builder queryNameTemplates = ImmutableList.builder(); + for (String q : query) { + queryNameTemplates.add(Pattern.compile(q)); + } + ImmutableList.Builder schemaNameTemplates = ImmutableList.builder(); + for (String s : schema) { + schemaNameTemplates.add(new RegexTemplate(s)); + } + return new Suite(name, session, schemaNameTemplates.build(), queryNameTemplates.build()); + } + } +} diff --git a/presto-benchmark-driver/src/test/java/com/facebook/presto/benchmark/driver/RegexTemplateTest.java b/presto-benchmark-driver/src/test/java/com/facebook/presto/benchmark/driver/RegexTemplateTest.java new file mode 100644 index 00000000..51ffe84d --- /dev/null +++ b/presto-benchmark-driver/src/test/java/com/facebook/presto/benchmark/driver/RegexTemplateTest.java @@ -0,0 +1,43 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark.driver; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import org.testng.annotations.Test; + +import java.util.Optional; + +import static org.testng.Assert.assertEquals; + +public class RegexTemplateTest +{ + @Test + public void test() + throws Exception + { + RegexTemplate regexTemplate = new RegexTemplate("tpch_sf(?.*?)_(?.*?)_(?.*?)"); + + assertEquals(regexTemplate.getFieldNames(), ImmutableList.of("scale", "format", "compression")); + assertEquals(regexTemplate.parse("tpch_sf100_orc_zlib"), Optional.of(ImmutableMap.of("scale", "100", "format", "orc", "compression", "zlib"))); + assertEquals(regexTemplate.parse("foo_tpch_sf100_orc_zlib"), Optional.empty()); + assertEquals(regexTemplate.parse("tpch_sf100_orc"), Optional.empty()); + assertEquals(regexTemplate.parse(""), Optional.empty()); + + regexTemplate = new RegexTemplate("tpch_sf(?.*?)_(?.*?)_(?.*?)\\.sql"); + assertEquals(regexTemplate.parse("tpch_sf100_orc_zlib.sql"), Optional.of(ImmutableMap.of("scale", "100", "format", "orc", "compression", "zlib"))); + assertEquals(regexTemplate.parse("tpch_sf100_orc_zlibXsql"), Optional.empty()); + assertEquals(regexTemplate.parse("tpch_sf100_orc_zlib.sqlFoo"), Optional.empty()); + } +} diff --git a/presto-benchmark-driver/src/test/resources/groups.json b/presto-benchmark-driver/src/test/resources/groups.json new file mode 100644 index 00000000..74562f6b --- /dev/null +++ b/presto-benchmark-driver/src/test/resources/groups.json @@ -0,0 +1,16 @@ +{ + "legacy_readers": { + "schema": [ "(?tiny)", "sf(?.)" ], + "session": { + "enable_optimized_readers": "false" + }, + "query": ["sum_quantity.*", "sum_discount.*"] + }, + "optimized_readers": { + "schema": [ "(?tiny)", "sf(?.)" ], + "session": { + "enable_optimized_readers": "true" + }, + "query": ["sum_quantity.*", "sum_discount.*"] + } +} diff --git a/presto-benchmark-driver/src/test/resources/sql/live_sum_discount.sql b/presto-benchmark-driver/src/test/resources/sql/live_sum_discount.sql new file mode 100644 index 00000000..a4cf6e36 --- /dev/null +++ b/presto-benchmark-driver/src/test/resources/sql/live_sum_discount.sql @@ -0,0 +1,5 @@ +format=generate +scale=1 +===== +select count(discount) +from tpch_sf1_rcbinary_lineitem diff --git a/presto-benchmark-driver/src/test/resources/sql/sum_discount.sql b/presto-benchmark-driver/src/test/resources/sql/sum_discount.sql new file mode 100644 index 00000000..67e82fd7 --- /dev/null +++ b/presto-benchmark-driver/src/test/resources/sql/sum_discount.sql @@ -0,0 +1,4 @@ +format=generate +===== +select count(discount) +from lineitem diff --git a/presto-benchmark-driver/src/test/resources/sql/sum_quantity.sql b/presto-benchmark-driver/src/test/resources/sql/sum_quantity.sql new file mode 100644 index 00000000..3b528f3f --- /dev/null +++ b/presto-benchmark-driver/src/test/resources/sql/sum_quantity.sql @@ -0,0 +1,4 @@ +format=generate +===== +select count(quantity) +from lineitem diff --git a/presto-benchmark/pom.xml b/presto-benchmark/pom.xml new file mode 100644 index 00000000..ee15feb2 --- /dev/null +++ b/presto-benchmark/pom.xml @@ -0,0 +1,95 @@ + + + 4.0.0 + + + presto-root + com.facebook.presto + 0.107 + + + presto-benchmark + presto-benchmark + + + ${project.parent.basedir} + + + + + com.facebook.presto + presto-spi + + + + com.facebook.presto + presto-main + + + + com.facebook.presto + presto-tpch + + + + com.google.code.findbugs + annotations + + + + io.airlift + json + + + + io.airlift + log + + + + io.airlift + units + + + + com.fasterxml.jackson.core + jackson-annotations + + + + com.fasterxml.jackson.core + jackson-core + + + + com.google.guava + guava + + + + org.jetbrains + annotations + provided + + + + + io.airlift + bootstrap + runtime + + + + javax.ws.rs + javax.ws.rs-api + runtime + + + + + org.testng + testng + test + + + diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/AbstractBenchmark.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/AbstractBenchmark.java new file mode 100644 index 00000000..adcf2bcf --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/AbstractBenchmark.java @@ -0,0 +1,140 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import io.airlift.units.DataSize; +import io.airlift.units.Duration; + +import javax.annotation.Nullable; + +import java.util.Map; + +import static com.facebook.presto.benchmark.FormatUtils.formatCount; +import static com.facebook.presto.benchmark.FormatUtils.formatCountRate; +import static com.facebook.presto.benchmark.FormatUtils.formatDataRate; +import static com.facebook.presto.benchmark.FormatUtils.formatDataSize; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.airlift.units.DataSize.Unit.BYTE; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.NANOSECONDS; + +public abstract class AbstractBenchmark +{ + private final String benchmarkName; + private final int warmupIterations; + private final int measuredIterations; + + protected AbstractBenchmark(String benchmarkName, int warmupIterations, int measuredIterations) + { + checkNotNull(benchmarkName, "benchmarkName is null"); + checkArgument(warmupIterations >= 0, "warmupIterations must not be negative"); + checkArgument(measuredIterations >= 0, "measuredIterations must not be negative"); + + this.benchmarkName = benchmarkName; + this.warmupIterations = warmupIterations; + this.measuredIterations = measuredIterations; + } + + public String getBenchmarkName() + { + return benchmarkName; + } + + protected int getWarmupIterations() + { + return warmupIterations; + } + + protected int getMeasuredIterations() + { + return measuredIterations; + } + + /** + * Initialize any state necessary to run benchmark. This is run once at start up. + */ + protected void setUp() + { + // Default: no-op + } + + /** + * Runs the benchmark and returns the result metrics + */ + protected abstract Map runOnce(); + + /** + * Clean up any state from the benchmark. This is run once after all the iterations are complete. + */ + protected void tearDown() + { + // Default: no-op + } + + public void runBenchmark() + { + runBenchmark(null); + } + + public void runBenchmark(@Nullable BenchmarkResultHook benchmarkResultHook) + { + AverageBenchmarkResults averageBenchmarkResults = new AverageBenchmarkResults(); + setUp(); + try { + for (int i = 0; i < warmupIterations; i++) { + runOnce(); + } + for (int i = 0; i < measuredIterations; i++) { + Map results = runOnce(); + if (benchmarkResultHook != null) { + benchmarkResultHook.addResults(results); + } + averageBenchmarkResults.addResults(results); + } + } + catch (Throwable t) { + throw new RuntimeException("Exception in " + getBenchmarkName(), t); + } + finally { + tearDown(); + } + if (benchmarkResultHook != null) { + benchmarkResultHook.finished(); + } + + Map resultsAvg = averageBenchmarkResults.getAverageResultsValues(); + Duration cpuNanos = new Duration(resultsAvg.get("cpu_nanos"), NANOSECONDS); + + long inputRows = resultsAvg.get("input_rows").longValue(); + DataSize inputBytes = new DataSize(resultsAvg.get("input_bytes"), BYTE); + + long outputRows = resultsAvg.get("output_rows").longValue(); + DataSize outputBytes = new DataSize(resultsAvg.get("output_bytes"), BYTE); + + System.out.printf("%35s :: %8.3f cpu ms :: in %5s, %6s, %8s, %8s :: out %5s, %6s, %8s, %8s%n", + getBenchmarkName(), + cpuNanos.getValue(MILLISECONDS), + + formatCount(inputRows), + formatDataSize(inputBytes, true), + formatCountRate(inputRows, cpuNanos, true), + formatDataRate(inputBytes, cpuNanos, true), + + formatCount(outputRows), + formatDataSize(outputBytes, true), + formatCountRate(outputRows, cpuNanos, true), + formatDataRate(outputBytes, cpuNanos, true)); + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/AbstractOperatorBenchmark.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/AbstractOperatorBenchmark.java new file mode 100644 index 00000000..589d1029 --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/AbstractOperatorBenchmark.java @@ -0,0 +1,145 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.facebook.presto.Session; +import com.facebook.presto.execution.TaskId; +import com.facebook.presto.execution.TaskStateMachine; +import com.facebook.presto.memory.MemoryPool; +import com.facebook.presto.memory.MemoryPoolId; +import com.facebook.presto.memory.QueryContext; +import com.facebook.presto.operator.Driver; +import com.facebook.presto.operator.OperatorFactory; +import com.facebook.presto.operator.TaskContext; +import com.facebook.presto.operator.TaskStats; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.testing.LocalQueryRunner; +import com.facebook.presto.util.CpuTimer; +import com.facebook.presto.util.CpuTimer.CpuDuration; +import com.google.common.collect.ImmutableMap; +import io.airlift.units.DataSize; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; + +import static com.facebook.presto.spi.type.TimeZoneKey.UTC_KEY; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.airlift.units.DataSize.Unit.BYTE; +import static io.airlift.units.DataSize.Unit.GIGABYTE; +import static io.airlift.units.DataSize.Unit.MEGABYTE; +import static java.util.Locale.ENGLISH; +import static java.util.concurrent.TimeUnit.NANOSECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; + +/** + * Abstract template for benchmarks that want to test the performance of an Operator. + */ +public abstract class AbstractOperatorBenchmark + extends AbstractBenchmark +{ + protected final LocalQueryRunner localQueryRunner; + + protected AbstractOperatorBenchmark( + LocalQueryRunner localQueryRunner, + String benchmarkName, + int warmupIterations, + int measuredIterations) + { + super(benchmarkName, warmupIterations, measuredIterations); + this.localQueryRunner = checkNotNull(localQueryRunner, "localQueryRunner is null"); + } + + protected OperatorFactory createTableScanOperator(int operatorId, String tableName, String... columnNames) + { + return localQueryRunner.createTableScanOperator(operatorId, tableName, columnNames); + } + + protected OperatorFactory createHashProjectOperator(int operatorId, List types) + { + return localQueryRunner.createHashProjectOperator(operatorId, types); + } + + protected abstract List createDrivers(TaskContext taskContext); + + protected void execute(TaskContext taskContext) + { + List drivers = createDrivers(taskContext); + + boolean done = false; + while (!done) { + boolean processed = false; + for (Driver driver : drivers) { + if (!driver.isFinished()) { + driver.process(); + processed = true; + } + } + done = !processed; + } + } + + @Override + protected Map runOnce() + { + Session session = Session.builder() + .setUser("user") + .setSource("source") + .setCatalog("catalog") + .setSchema("schema") + .setTimeZoneKey(UTC_KEY) + .setLocale(ENGLISH) + .setSystemProperties(ImmutableMap.of("optimizer.optimize-hash-generation", "true")) + .build(); + ExecutorService executor = localQueryRunner.getExecutor(); + MemoryPool memoryPool = new MemoryPool(new MemoryPoolId("test"), new DataSize(1, GIGABYTE), false); + TaskContext taskContext = new QueryContext(false, new DataSize(256, MEGABYTE), memoryPool, executor) + .addTaskContext(new TaskStateMachine(new TaskId("query", "stage", "task"), executor), + session, + new DataSize(256, MEGABYTE), + new DataSize(1, MEGABYTE), + false, + false); + + CpuTimer cpuTimer = new CpuTimer(); + execute(taskContext); + CpuDuration executionTime = cpuTimer.elapsedTime(); + + TaskStats taskStats = taskContext.getTaskStats(); + long inputRows = taskStats.getRawInputPositions(); + long inputBytes = taskStats.getRawInputDataSize().toBytes(); + long outputRows = taskStats.getOutputPositions(); + long outputBytes = taskStats.getOutputDataSize().toBytes(); + + double inputMegaBytes = new DataSize(inputBytes, BYTE).getValue(MEGABYTE); + + return ImmutableMap.builder() + // legacy computed values + .put("elapsed_millis", executionTime.getWall().toMillis()) + .put("input_rows_per_second", (long) (inputRows / executionTime.getWall().getValue(SECONDS))) + .put("output_rows_per_second", (long) (outputRows / executionTime.getWall().getValue(SECONDS))) + .put("input_megabytes", (long) inputMegaBytes) + .put("input_megabytes_per_second", (long) (inputMegaBytes / executionTime.getWall().getValue(SECONDS))) + + .put("wall_nanos", executionTime.getWall().roundTo(NANOSECONDS)) + .put("cpu_nanos", executionTime.getCpu().roundTo(NANOSECONDS)) + .put("user_nanos", executionTime.getUser().roundTo(NANOSECONDS)) + .put("input_rows", inputRows) + .put("input_bytes", inputBytes) + .put("output_rows", outputRows) + .put("output_bytes", outputBytes) + + .build(); + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/AbstractSimpleOperatorBenchmark.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/AbstractSimpleOperatorBenchmark.java new file mode 100644 index 00000000..252c1ec3 --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/AbstractSimpleOperatorBenchmark.java @@ -0,0 +1,60 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.facebook.presto.operator.Driver; +import com.facebook.presto.operator.DriverContext; +import com.facebook.presto.operator.DriverFactory; +import com.facebook.presto.operator.OperatorFactory; +import com.facebook.presto.operator.TaskContext; +import com.facebook.presto.testing.LocalQueryRunner; +import com.facebook.presto.testing.NullOutputOperator.NullOutputOperatorFactory; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; + +import java.util.ArrayList; +import java.util.List; + +public abstract class AbstractSimpleOperatorBenchmark + extends AbstractOperatorBenchmark +{ + protected AbstractSimpleOperatorBenchmark( + LocalQueryRunner localQueryRunner, + String benchmarkName, + int warmupIterations, + int measuredIterations) + { + super(localQueryRunner, benchmarkName, warmupIterations, measuredIterations); + } + + protected abstract List createOperatorFactories(); + + protected DriverFactory createDriverFactory() + { + List operatorFactories = new ArrayList<>(createOperatorFactories()); + + operatorFactories.add(new NullOutputOperatorFactory(999, Iterables.getLast(operatorFactories).getTypes())); + + return new DriverFactory(true, true, operatorFactories); + } + + @Override + protected List createDrivers(TaskContext taskContext) + { + DriverFactory driverFactory = createDriverFactory(); + DriverContext driverContext = taskContext.addPipelineContext(true, true).addDriverContext(); + Driver driver = driverFactory.createDriver(driverContext); + return ImmutableList.of(driver); + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/AbstractSqlBenchmark.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/AbstractSqlBenchmark.java new file mode 100644 index 00000000..e788814d --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/AbstractSqlBenchmark.java @@ -0,0 +1,46 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.facebook.presto.operator.Driver; +import com.facebook.presto.operator.TaskContext; +import com.facebook.presto.testing.LocalQueryRunner; +import com.facebook.presto.testing.NullOutputOperator.NullOutputFactory; +import org.intellij.lang.annotations.Language; + +import java.util.List; + +public abstract class AbstractSqlBenchmark + extends AbstractOperatorBenchmark +{ + @Language("SQL") + private final String query; + + protected AbstractSqlBenchmark( + LocalQueryRunner localQueryRunner, + String benchmarkName, + int warmupIterations, + int measuredIterations, + @Language("SQL") String query) + { + super(localQueryRunner, benchmarkName, warmupIterations, measuredIterations); + this.query = query; + } + + @Override + protected List createDrivers(TaskContext taskContext) + { + return localQueryRunner.createDrivers(query, new NullOutputFactory(), taskContext); + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/ArrayComparisonBenchmark.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/ArrayComparisonBenchmark.java new file mode 100644 index 00000000..1df0e0bd --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/ArrayComparisonBenchmark.java @@ -0,0 +1,86 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.facebook.presto.testing.LocalQueryRunner; + +import static com.facebook.presto.benchmark.BenchmarkQueryRunner.createLocalQueryRunner; + +public abstract class ArrayComparisonBenchmark +{ + public static void main(String... args) + { + LocalQueryRunner localQueryRunner = createLocalQueryRunner(); + new ArrayEqualsBenchmark(localQueryRunner).runBenchmark(new SimpleLineBenchmarkResultWriter(System.out)); + new ArrayLessThanBenchmark(localQueryRunner).runBenchmark(new SimpleLineBenchmarkResultWriter(System.out)); + new ArrayGreaterThanBenchmark(localQueryRunner).runBenchmark(new SimpleLineBenchmarkResultWriter(System.out)); + new ArrayNotEqualBenchmark(localQueryRunner).runBenchmark(new SimpleLineBenchmarkResultWriter(System.out)); + new ArrayLessThanOrEqualBenchmark(localQueryRunner).runBenchmark(new SimpleLineBenchmarkResultWriter(System.out)); + new ArrayGreaterThanOrEqualBenchmark(localQueryRunner).runBenchmark(new SimpleLineBenchmarkResultWriter(System.out)); + } + + public static class ArrayEqualsBenchmark + extends AbstractSqlBenchmark + { + public ArrayEqualsBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "array_equals", 5, 50, "SELECT COUNT_IF(ARRAY [orderkey, orderkey + 1, orderkey + 2] = ARRAY[orderkey + 1, orderkey + 2, orderkey + 3]) FROM orders"); + } + } + + public static class ArrayLessThanBenchmark + extends AbstractSqlBenchmark + { + public ArrayLessThanBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "array_less_than", 5, 50, "SELECT COUNT_IF(ARRAY [quantity, quantity + 5] < ARRAY[quantity, quantity + 5, quantity + 3]) FROM lineitem"); + } + } + + public static class ArrayGreaterThanBenchmark + extends AbstractSqlBenchmark + { + public ArrayGreaterThanBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "array_greater_than", 5, 50, "SELECT COUNT_IF(ARRAY [quantity, quantity + 6] > ARRAY[quantity, quantity + 5, quantity + 3]) FROM lineitem"); + } + } + + public static class ArrayNotEqualBenchmark + extends AbstractSqlBenchmark + { + public ArrayNotEqualBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "array_not_equal", 5, 50, "SELECT COUNT_IF(ARRAY [orderkey, orderkey + 1, orderkey + 2] != ARRAY[orderkey + 1, orderkey + 2, orderkey + 3]) FROM orders"); + } + } + + public static class ArrayLessThanOrEqualBenchmark + extends AbstractSqlBenchmark + { + public ArrayLessThanOrEqualBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "array_less_than_or_equal", 5, 50, "SELECT COUNT_IF(ARRAY [quantity, quantity + 5, quantity + 2] <= ARRAY[quantity, quantity + 5, quantity + 3]) FROM lineitem"); + } + } + + public static class ArrayGreaterThanOrEqualBenchmark + extends AbstractSqlBenchmark + { + public ArrayGreaterThanOrEqualBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "array_greater_than_or_equal", 5, 50, "SELECT COUNT_IF(ARRAY [quantity, quantity + 6] >= ARRAY[quantity, quantity + 5, quantity + 3]) FROM lineitem"); + } + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/AverageBenchmarkResults.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/AverageBenchmarkResults.java new file mode 100644 index 00000000..1cb345a9 --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/AverageBenchmarkResults.java @@ -0,0 +1,60 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.google.common.collect.Maps; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class AverageBenchmarkResults + implements BenchmarkResultHook +{ + private final Map resultsSum = new LinkedHashMap<>(); + private int resultsCount; + + @Override + public BenchmarkResultHook addResults(Map results) + { + checkNotNull(results, "results is null"); + for (Entry entry : results.entrySet()) { + Long currentSum = resultsSum.get(entry.getKey()); + if (currentSum == null) { + currentSum = 0L; + } + resultsSum.put(entry.getKey(), currentSum + entry.getValue()); + } + resultsCount++; + + return this; + } + + public Map getAverageResultsValues() + { + return Maps.transformValues(resultsSum, input -> 1.0 * input / resultsCount); + } + + public Map getAverageResultsStrings() + { + return Maps.transformValues(resultsSum, input -> String.format("%,3.2f", 1.0 * input / resultsCount)); + } + + @Override + public void finished() + { + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/BenchmarkQueryRunner.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/BenchmarkQueryRunner.java new file mode 100644 index 00000000..ab0ad140 --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/BenchmarkQueryRunner.java @@ -0,0 +1,66 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.facebook.presto.Session; +import com.facebook.presto.metadata.InMemoryNodeManager; +import com.facebook.presto.testing.LocalQueryRunner; +import com.facebook.presto.tpch.TpchConnectorFactory; +import com.google.common.collect.ImmutableMap; + +import static com.facebook.presto.Session.SessionBuilder; +import static com.facebook.presto.spi.type.TimeZoneKey.UTC_KEY; +import static com.facebook.presto.tpch.TpchMetadata.TINY_SCHEMA_NAME; +import static java.util.Locale.ENGLISH; + +public final class BenchmarkQueryRunner +{ + private BenchmarkQueryRunner() + { + } + + public static LocalQueryRunner createLocalQueryRunnerHashEnabled() + { + return createLocalQueryRunner(true); + } + + public static LocalQueryRunner createLocalQueryRunner() + { + return createLocalQueryRunner(false); + } + public static LocalQueryRunner createLocalQueryRunner(boolean hashingEnabled) + { + SessionBuilder sessionBuilder = Session + .builder() + .setUser("user") + .setSource("test") + .setCatalog("tpch") + .setSchema(TINY_SCHEMA_NAME) + .setTimeZoneKey(UTC_KEY) + .setLocale(ENGLISH); + + if (hashingEnabled) { + sessionBuilder.setSystemProperties(ImmutableMap.of("optimizer.optimize_hash_generation", "true")); + } + + Session session = sessionBuilder.build(); + LocalQueryRunner localQueryRunner = new LocalQueryRunner(session); + + // add tpch + InMemoryNodeManager nodeManager = localQueryRunner.getNodeManager(); + localQueryRunner.createCatalog("tpch", new TpchConnectorFactory(nodeManager, 1), ImmutableMap.of()); + + return localQueryRunner; + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/BenchmarkResultHook.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/BenchmarkResultHook.java new file mode 100644 index 00000000..c81dc92a --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/BenchmarkResultHook.java @@ -0,0 +1,23 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import java.util.Map; + +public interface BenchmarkResultHook +{ + BenchmarkResultHook addResults(Map results); + + void finished(); +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/BenchmarkSuite.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/BenchmarkSuite.java new file mode 100644 index 00000000..f73cf6a6 --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/BenchmarkSuite.java @@ -0,0 +1,179 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.facebook.presto.testing.LocalQueryRunner; +import com.google.common.collect.ImmutableList; +import com.google.common.io.Files; +import io.airlift.log.Logger; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.List; +import java.util.Map; + +import static com.facebook.presto.testing.LocalQueryRunner.createHashEnabledQueryRunner; +import static com.google.common.base.Preconditions.checkNotNull; + +public class BenchmarkSuite +{ + private static final Logger LOGGER = Logger.get(BenchmarkSuite.class); + + public static List createBenchmarks(LocalQueryRunner localQueryRunner, LocalQueryRunner hashEnabledLocalQueryRunner) + { + return ImmutableList.of( + // hand built benchmarks + new CountAggregationBenchmark(localQueryRunner), + new DoubleSumAggregationBenchmark(localQueryRunner), + new HashAggregationBenchmark(localQueryRunner), + new PredicateFilterBenchmark(localQueryRunner), + new RawStreamingBenchmark(localQueryRunner), + new Top100Benchmark(localQueryRunner), + new OrderByBenchmark(localQueryRunner), + new HashBuildBenchmark(localQueryRunner), + new HashJoinBenchmark(localQueryRunner), + new HashBuildAndJoinBenchmark(localQueryRunner), + new HashBuildAndJoinBenchmark(hashEnabledLocalQueryRunner), + new HandTpchQuery1(localQueryRunner), + new HandTpchQuery6(localQueryRunner), + + // sql benchmarks + new GroupBySumWithArithmeticSqlBenchmark(localQueryRunner), + new CountAggregationSqlBenchmark(localQueryRunner), + new SqlDoubleSumAggregationBenchmark(localQueryRunner), + new CountWithFilterSqlBenchmark(localQueryRunner), + new GroupByAggregationSqlBenchmark(localQueryRunner), + new PredicateFilterSqlBenchmark(localQueryRunner), + new RawStreamingSqlBenchmark(localQueryRunner), + new Top100SqlBenchmark(localQueryRunner), + new SqlHashJoinBenchmark(localQueryRunner), + new SqlJoinWithPredicateBenchmark(localQueryRunner), + new LongMaxAggregationSqlBenchmark(localQueryRunner), + new VarBinaryMaxAggregationSqlBenchmark(localQueryRunner), + new SqlDistinctMultipleFields(localQueryRunner), + new SqlDistinctSingleField(localQueryRunner), + new SqlTpchQuery1(localQueryRunner), + new SqlTpchQuery6(localQueryRunner), + new SqlLikeBenchmark(localQueryRunner), + new SqlInBenchmark(localQueryRunner), + new SqlSemiJoinInPredicateBenchmark(localQueryRunner), + new SqlRegexpLikeBenchmark(localQueryRunner), + new SqlApproximatePercentileBenchmark(localQueryRunner), + new SqlBetweenBenchmark(localQueryRunner), + + // statistics benchmarks + new StatisticsBenchmark.LongVarianceBenchmark(localQueryRunner), + new StatisticsBenchmark.LongVariancePopBenchmark(localQueryRunner), + new StatisticsBenchmark.DoubleVarianceBenchmark(localQueryRunner), + new StatisticsBenchmark.DoubleVariancePopBenchmark(localQueryRunner), + new StatisticsBenchmark.LongStdDevBenchmark(localQueryRunner), + new StatisticsBenchmark.LongStdDevPopBenchmark(localQueryRunner), + new StatisticsBenchmark.DoubleStdDevBenchmark(localQueryRunner), + new StatisticsBenchmark.DoubleStdDevPopBenchmark(localQueryRunner), + + // array comparison benchmarks + new ArrayComparisonBenchmark.ArrayEqualsBenchmark(localQueryRunner), + new ArrayComparisonBenchmark.ArrayLessThanBenchmark(localQueryRunner), + new ArrayComparisonBenchmark.ArrayGreaterThanBenchmark(localQueryRunner), + new ArrayComparisonBenchmark.ArrayNotEqualBenchmark(localQueryRunner), + new ArrayComparisonBenchmark.ArrayLessThanOrEqualBenchmark(localQueryRunner), + new ArrayComparisonBenchmark.ArrayGreaterThanOrEqualBenchmark(localQueryRunner), + + new SqlApproximateCountDistinctLongBenchmark(localQueryRunner), + new SqlApproximateCountDistinctDoubleBenchmark(localQueryRunner), + new SqlApproximateCountDistinctVarBinaryBenchmark(localQueryRunner) + ); + } + + private final LocalQueryRunner localQueryRunner; + private final LocalQueryRunner hashEnabledLocalQueryRunner; + private final String outputDirectory; + + public BenchmarkSuite(LocalQueryRunner localQueryRunner, String outputDirectory) + { + this.localQueryRunner = localQueryRunner; + this.hashEnabledLocalQueryRunner = createHashEnabledQueryRunner(localQueryRunner); + this.outputDirectory = checkNotNull(outputDirectory, "outputDirectory is null"); + } + + private static File createOutputFile(String fileName) + throws IOException + { + File outputFile = new File(fileName); + Files.createParentDirs(outputFile); + return outputFile; + } + + public void runAllBenchmarks() + throws IOException + { + List benchmarks = createBenchmarks(localQueryRunner, hashEnabledLocalQueryRunner); + + LOGGER.info("=== Pre-running all benchmarks for JVM warmup ==="); + for (AbstractBenchmark benchmark : benchmarks) { + benchmark.runBenchmark(); + } + + LOGGER.info("=== Actually running benchmarks for metrics ==="); + for (AbstractBenchmark benchmark : benchmarks) { + try (OutputStream jsonOut = new FileOutputStream(createOutputFile(String.format("%s/json/%s.json", outputDirectory, benchmark.getBenchmarkName()))); + OutputStream jsonAvgOut = new FileOutputStream(createOutputFile(String.format("%s/json-avg/%s.json", outputDirectory, benchmark.getBenchmarkName()))); + OutputStream csvOut = new FileOutputStream(createOutputFile(String.format("%s/csv/%s.csv", outputDirectory, benchmark.getBenchmarkName()))); + OutputStream odsOut = new FileOutputStream(createOutputFile(String.format("%s/ods/%s.json", outputDirectory, benchmark.getBenchmarkName())))) { + benchmark.runBenchmark( + new ForwardingBenchmarkResultWriter( + ImmutableList.of( + new JsonBenchmarkResultWriter(jsonOut), + new JsonAvgBenchmarkResultWriter(jsonAvgOut), + new SimpleLineBenchmarkResultWriter(csvOut), + new OdsBenchmarkResultWriter("presto.benchmark." + benchmark.getBenchmarkName(), odsOut) + ) + ) + ); + } + } + } + + private static class ForwardingBenchmarkResultWriter + implements BenchmarkResultHook + { + private final List benchmarkResultHooks; + + private ForwardingBenchmarkResultWriter(List benchmarkResultHooks) + { + checkNotNull(benchmarkResultHooks, "benchmarkResultWriters is null"); + this.benchmarkResultHooks = ImmutableList.copyOf(benchmarkResultHooks); + } + + @Override + public BenchmarkResultHook addResults(Map results) + { + checkNotNull(results, "results is null"); + for (BenchmarkResultHook benchmarkResultHook : benchmarkResultHooks) { + benchmarkResultHook.addResults(results); + } + return this; + } + + @Override + public void finished() + { + for (BenchmarkResultHook benchmarkResultHook : benchmarkResultHooks) { + benchmarkResultHook.finished(); + } + } + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/CountAggregationBenchmark.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/CountAggregationBenchmark.java new file mode 100644 index 00000000..12cdf792 --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/CountAggregationBenchmark.java @@ -0,0 +1,48 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.facebook.presto.operator.AggregationOperator.AggregationOperatorFactory; +import com.facebook.presto.operator.OperatorFactory; +import com.facebook.presto.sql.planner.plan.AggregationNode.Step; +import com.facebook.presto.testing.LocalQueryRunner; +import com.google.common.collect.ImmutableList; + +import java.util.List; +import java.util.Optional; + +import static com.facebook.presto.benchmark.BenchmarkQueryRunner.createLocalQueryRunner; +import static com.facebook.presto.operator.aggregation.CountAggregation.COUNT; + +public class CountAggregationBenchmark + extends AbstractSimpleOperatorBenchmark +{ + public CountAggregationBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "count_agg", 10, 100); + } + + @Override + protected List createOperatorFactories() + { + OperatorFactory tableScanOperator = createTableScanOperator(0, "orders", "orderkey"); + AggregationOperatorFactory aggregationOperator = new AggregationOperatorFactory(1, Step.SINGLE, ImmutableList.of(COUNT.bind(ImmutableList.of(0), Optional.empty(), Optional.empty(), 1.0))); + return ImmutableList.of(tableScanOperator, aggregationOperator); + } + + public static void main(String[] args) + { + new CountAggregationBenchmark(createLocalQueryRunner()).runBenchmark(new SimpleLineBenchmarkResultWriter(System.out)); + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/CountAggregationSqlBenchmark.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/CountAggregationSqlBenchmark.java new file mode 100644 index 00000000..0866af44 --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/CountAggregationSqlBenchmark.java @@ -0,0 +1,32 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.facebook.presto.testing.LocalQueryRunner; + +import static com.facebook.presto.benchmark.BenchmarkQueryRunner.createLocalQueryRunner; + +public class CountAggregationSqlBenchmark + extends AbstractSqlBenchmark +{ + public CountAggregationSqlBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "sql_count_agg", 10, 100, "select count(*) from orders"); + } + + public static void main(String[] args) + { + new CountAggregationSqlBenchmark(createLocalQueryRunner()).runBenchmark(new SimpleLineBenchmarkResultWriter(System.out)); + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/CountWithFilterSqlBenchmark.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/CountWithFilterSqlBenchmark.java new file mode 100644 index 00000000..12a9db6b --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/CountWithFilterSqlBenchmark.java @@ -0,0 +1,32 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.facebook.presto.testing.LocalQueryRunner; + +import static com.facebook.presto.benchmark.BenchmarkQueryRunner.createLocalQueryRunner; + +public class CountWithFilterSqlBenchmark + extends AbstractSqlBenchmark +{ + public CountWithFilterSqlBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "sql_count_with_filter", 10, 100, "SELECT count(*) from orders where orderstatus = 'F'"); + } + + public static void main(String[] args) + { + new CountWithFilterSqlBenchmark(createLocalQueryRunner()).runBenchmark(new SimpleLineBenchmarkResultWriter(System.out)); + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/DoubleSumAggregationBenchmark.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/DoubleSumAggregationBenchmark.java new file mode 100644 index 00000000..ced463b6 --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/DoubleSumAggregationBenchmark.java @@ -0,0 +1,48 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.facebook.presto.operator.AggregationOperator.AggregationOperatorFactory; +import com.facebook.presto.operator.OperatorFactory; +import com.facebook.presto.sql.planner.plan.AggregationNode.Step; +import com.facebook.presto.testing.LocalQueryRunner; +import com.google.common.collect.ImmutableList; + +import java.util.List; +import java.util.Optional; + +import static com.facebook.presto.benchmark.BenchmarkQueryRunner.createLocalQueryRunner; +import static com.facebook.presto.operator.aggregation.DoubleSumAggregation.DOUBLE_SUM; + +public class DoubleSumAggregationBenchmark + extends AbstractSimpleOperatorBenchmark +{ + public DoubleSumAggregationBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "double_sum_agg", 10, 100); + } + + @Override + protected List createOperatorFactories() + { + OperatorFactory tableScanOperator = createTableScanOperator(0, "orders", "totalprice"); + AggregationOperatorFactory aggregationOperator = new AggregationOperatorFactory(1, Step.SINGLE, ImmutableList.of(DOUBLE_SUM.bind(ImmutableList.of(0), Optional.empty(), Optional.empty(), 1.0))); + return ImmutableList.of(tableScanOperator, aggregationOperator); + } + + public static void main(String[] args) + { + new DoubleSumAggregationBenchmark(createLocalQueryRunner()).runBenchmark(new SimpleLineBenchmarkResultWriter(System.out)); + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/FormatUtils.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/FormatUtils.java new file mode 100644 index 00000000..752bec0a --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/FormatUtils.java @@ -0,0 +1,147 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import io.airlift.units.DataSize; +import io.airlift.units.Duration; + +import java.math.RoundingMode; +import java.text.DecimalFormat; + +import static io.airlift.units.DataSize.Unit.BYTE; +import static java.lang.String.format; +import static java.util.concurrent.TimeUnit.SECONDS; + +// TODO: these should be in airlift +final class FormatUtils +{ + private FormatUtils() {} + + public static String formatCount(long count) + { + double fractional = count; + String unit = ""; + if (fractional > 1000) { + fractional /= 1000; + unit = "K"; + } + if (fractional > 1000) { + fractional /= 1000; + unit = "M"; + } + if (fractional > 1000) { + fractional /= 1000; + unit = "B"; + } + if (fractional > 1000) { + fractional /= 1000; + unit = "T"; + } + if (fractional > 1000) { + fractional /= 1000; + unit = "Q"; + } + + return format("%s%s", getFormat(fractional).format(fractional), unit); + } + + public static String formatCountRate(double count, Duration duration, boolean longForm) + { + double rate = count / duration.getValue(SECONDS); + if (Double.isNaN(rate) || Double.isInfinite(rate)) { + rate = 0; + } + + String rateString = formatCount((long) rate); + if (longForm) { + if (rateString.endsWith(" ")) { + rateString = rateString.substring(0, rateString.length() - 1); + } + rateString += "/s"; + } + return rateString; + } + + public static String formatDataSize(DataSize size, boolean longForm) + { + double fractional = size.toBytes(); + String unit = null; + if (fractional >= 1024) { + fractional /= 1024; + unit = "K"; + } + if (fractional >= 1024) { + fractional /= 1024; + unit = "M"; + } + if (fractional >= 1024) { + fractional /= 1024; + unit = "G"; + } + if (fractional >= 1024) { + fractional /= 1024; + unit = "T"; + } + if (fractional >= 1024) { + fractional /= 1024; + unit = "P"; + } + + if (unit == null) { + unit = "B"; + } + else if (longForm) { + unit += "B"; + } + + return format("%s%s", getFormat(fractional).format(fractional), unit); + } + + public static String formatDataRate(DataSize dataSize, Duration duration, boolean longForm) + { + double rate = dataSize.toBytes() / duration.getValue(SECONDS); + if (Double.isNaN(rate) || Double.isInfinite(rate)) { + rate = 0; + } + + String rateString = formatDataSize(new DataSize(rate, BYTE), false); + if (longForm) { + if (!rateString.endsWith("B")) { + rateString += "B"; + } + rateString += "/s"; + } + return rateString; + } + + public static DecimalFormat getFormat(double value) + { + DecimalFormat format; + if (value < 10) { + // show up to two decimals to get 3 significant digits + format = new DecimalFormat("#.##"); + } + else if (value < 100) { + // show up to one decimal to get 3 significant digits + format = new DecimalFormat("#.#"); + } + else { + // show no decimals -- we have enough digits in the integer part + format = new DecimalFormat("#"); + } + + format.setRoundingMode(RoundingMode.HALF_UP); + return format; + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/GroupByAggregationSqlBenchmark.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/GroupByAggregationSqlBenchmark.java new file mode 100644 index 00000000..db4048fe --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/GroupByAggregationSqlBenchmark.java @@ -0,0 +1,32 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.facebook.presto.testing.LocalQueryRunner; + +import static com.facebook.presto.benchmark.BenchmarkQueryRunner.createLocalQueryRunner; + +public class GroupByAggregationSqlBenchmark + extends AbstractSqlBenchmark +{ + public GroupByAggregationSqlBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "sql_groupby_agg", 15, 100, "select orderstatus, sum(totalprice) from orders group by orderstatus"); + } + + public static void main(String[] args) + { + new GroupByAggregationSqlBenchmark(createLocalQueryRunner()).runBenchmark(new SimpleLineBenchmarkResultWriter(System.out)); + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/GroupBySumWithArithmeticSqlBenchmark.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/GroupBySumWithArithmeticSqlBenchmark.java new file mode 100644 index 00000000..38454135 --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/GroupBySumWithArithmeticSqlBenchmark.java @@ -0,0 +1,36 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.facebook.presto.testing.LocalQueryRunner; + +import static com.facebook.presto.benchmark.BenchmarkQueryRunner.createLocalQueryRunner; + +public class GroupBySumWithArithmeticSqlBenchmark + extends AbstractSqlBenchmark +{ + public GroupBySumWithArithmeticSqlBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, + "sql_groupby_agg_with_arithmetic", + 1, + 4, + "select linestatus, sum(orderkey - partkey) from lineitem group by linestatus"); + } + + public static void main(String[] args) + { + new GroupBySumWithArithmeticSqlBenchmark(createLocalQueryRunner()).runBenchmark(new SimpleLineBenchmarkResultWriter(System.out)); + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/HandTpchQuery1.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/HandTpchQuery1.java new file mode 100644 index 00000000..6a314028 --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/HandTpchQuery1.java @@ -0,0 +1,324 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.facebook.presto.benchmark.HandTpchQuery1.TpchQuery1Operator.TpchQuery1OperatorFactory; +import com.facebook.presto.operator.DriverContext; +import com.facebook.presto.operator.HashAggregationOperator.HashAggregationOperatorFactory; +import com.facebook.presto.operator.Operator; +import com.facebook.presto.operator.OperatorContext; +import com.facebook.presto.operator.OperatorFactory; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.planner.plan.AggregationNode.Step; +import com.facebook.presto.testing.LocalQueryRunner; +import com.facebook.presto.util.DateTimeUtils; +import com.google.common.collect.ImmutableList; +import com.google.common.primitives.Ints; +import io.airlift.units.DataSize; + +import java.util.List; +import java.util.Optional; + +import static com.facebook.presto.benchmark.BenchmarkQueryRunner.createLocalQueryRunner; +import static com.facebook.presto.operator.aggregation.AverageAggregations.DOUBLE_AVERAGE; +import static com.facebook.presto.operator.aggregation.AverageAggregations.LONG_AVERAGE; +import static com.facebook.presto.operator.aggregation.CountAggregation.COUNT; +import static com.facebook.presto.operator.aggregation.DoubleSumAggregation.DOUBLE_SUM; +import static com.facebook.presto.operator.aggregation.LongSumAggregation.LONG_SUM; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.DateType.DATE; +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static io.airlift.units.DataSize.Unit.MEGABYTE; + +public class HandTpchQuery1 + extends AbstractSimpleOperatorBenchmark +{ + public HandTpchQuery1(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "hand_tpch_query_1", 1, 5); + } + + @Override + protected List createOperatorFactories() + { + // select + // returnflag, + // linestatus, + // sum(quantity) as sum_qty, + // sum(extendedprice) as sum_base_price, + // sum(extendedprice * (1 - discount)) as sum_disc_price, + // sum(extendedprice * (1 - discount) * (1 + tax)) as sum_charge, + // avg(quantity) as avg_qty, + // avg(extendedprice) as avg_price, + // avg(discount) as avg_disc, + // count(*) as count_order + // from + // lineitem + // where + // shipdate <= '1998-09-02' + // group by + // returnflag, + // linestatus + // order by + // returnflag, + // linestatus + + OperatorFactory tableScanOperator = createTableScanOperator( + 0, + "lineitem", + "returnflag", + "linestatus", + "quantity", + "extendedprice", + "discount", + "tax", + "shipdate"); + + TpchQuery1OperatorFactory tpchQuery1Operator = new TpchQuery1OperatorFactory(1); + HashAggregationOperatorFactory aggregationOperator = new HashAggregationOperatorFactory( + 2, + ImmutableList.of(tpchQuery1Operator.getTypes().get(0), tpchQuery1Operator.getTypes().get(1)), + Ints.asList(0, 1), + Step.SINGLE, + ImmutableList.of( + LONG_SUM.bind(ImmutableList.of(2), Optional.empty(), Optional.empty(), 1.0), + DOUBLE_SUM.bind(ImmutableList.of(3), Optional.empty(), Optional.empty(), 1.0), + DOUBLE_SUM.bind(ImmutableList.of(4), Optional.empty(), Optional.empty(), 1.0), + LONG_AVERAGE.bind(ImmutableList.of(2), Optional.empty(), Optional.empty(), 1.0), + DOUBLE_AVERAGE.bind(ImmutableList.of(5), Optional.empty(), Optional.empty(), 1.0), + DOUBLE_AVERAGE.bind(ImmutableList.of(6), Optional.empty(), Optional.empty(), 1.0), + COUNT.bind(ImmutableList.of(2), Optional.empty(), Optional.empty(), 1.0) + ), + Optional.empty(), + Optional.empty(), + 10_000, + new DataSize(16, MEGABYTE)); + + return ImmutableList.of(tableScanOperator, tpchQuery1Operator, aggregationOperator); + } + + public static class TpchQuery1Operator + implements com.facebook.presto.operator.Operator // TODO: use import when Java 7 compiler bug is fixed + { + private static final ImmutableList TYPES = ImmutableList.of( + VARCHAR, + VARCHAR, + BIGINT, + DOUBLE, + DOUBLE, + DOUBLE, + DOUBLE); + + public static class TpchQuery1OperatorFactory + implements OperatorFactory + { + private final int operatorId; + + public TpchQuery1OperatorFactory(int operatorId) + { + this.operatorId = operatorId; + } + + @Override + public List getTypes() + { + return TYPES; + } + + @Override + public Operator createOperator(DriverContext driverContext) + { + OperatorContext operatorContext = driverContext.addOperatorContext(operatorId, TpchQuery1Operator.class.getSimpleName()); + return new TpchQuery1Operator(operatorContext); + } + + @Override + public void close() + { + } + } + + private final OperatorContext operatorContext; + private final PageBuilder pageBuilder; + private boolean finishing; + + public TpchQuery1Operator(OperatorContext operatorContext) + { + this.operatorContext = checkNotNull(operatorContext, "operatorContext is null"); + this.pageBuilder = new PageBuilder(TYPES); + } + + @Override + public OperatorContext getOperatorContext() + { + return operatorContext; + } + + @Override + public List getTypes() + { + return TYPES; + } + + @Override + public void finish() + { + finishing = true; + } + + @Override + public boolean isFinished() + { + return finishing && pageBuilder.isEmpty(); + } + + @Override + public boolean needsInput() + { + return !pageBuilder.isFull(); + } + + @Override + public void addInput(Page page) + { + checkNotNull(page, "page is null"); + checkState(!pageBuilder.isFull(), "Output buffer is full"); + checkState(!finishing, "Operator is finished"); + + filterAndProjectRowOriented(pageBuilder, + page.getBlock(0), + page.getBlock(1), + page.getBlock(2), + page.getBlock(3), + page.getBlock(4), + page.getBlock(5), + page.getBlock(6)); + } + + @Override + public Page getOutput() + { + // only return a page if the page buffer isFull or we are finishing and the page buffer has data + if (pageBuilder.isFull() || (finishing && !pageBuilder.isEmpty())) { + Page page = pageBuilder.build(); + pageBuilder.reset(); + return page; + } + return null; + } + + private static final int MAX_SHIP_DATE = DateTimeUtils.parseDate("1998-09-02"); + + private static void filterAndProjectRowOriented(PageBuilder pageBuilder, + Block returnFlagBlock, + Block lineStatusBlock, + Block quantityBlock, + Block extendedPriceBlock, + Block discountBlock, + Block taxBlock, + Block shipDateBlock) + { + int rows = returnFlagBlock.getPositionCount(); + for (int position = 0; position < rows; position++) { + if (shipDateBlock.isNull(position)) { + continue; + } + + int shipDate = (int) DATE.getLong(shipDateBlock, position); + + // where + // shipdate <= '1998-09-02' + if (shipDate <= MAX_SHIP_DATE) { + // returnflag, + // linestatus + // quantity + // extendedprice + // extendedprice * (1 - discount) + // extendedprice * (1 - discount) * (1 + tax) + // discount + + pageBuilder.declarePosition(); + if (returnFlagBlock.isNull(position)) { + pageBuilder.getBlockBuilder(0).appendNull(); + } + else { + VARCHAR.appendTo(returnFlagBlock, position, pageBuilder.getBlockBuilder(0)); + } + if (lineStatusBlock.isNull(position)) { + pageBuilder.getBlockBuilder(1).appendNull(); + } + else { + VARCHAR.appendTo(lineStatusBlock, position, pageBuilder.getBlockBuilder(1)); + } + + long quantity = BIGINT.getLong(quantityBlock, position); + double extendedPrice = DOUBLE.getDouble(extendedPriceBlock, position); + double discount = DOUBLE.getDouble(discountBlock, position); + double tax = DOUBLE.getDouble(taxBlock, position); + + boolean quantityIsNull = quantityBlock.isNull(position); + boolean extendedPriceIsNull = extendedPriceBlock.isNull(position); + boolean discountIsNull = discountBlock.isNull(position); + boolean taxIsNull = taxBlock.isNull(position); + + if (quantityIsNull) { + pageBuilder.getBlockBuilder(2).appendNull(); + } + else { + BIGINT.writeLong(pageBuilder.getBlockBuilder(2), quantity); + } + + if (extendedPriceIsNull) { + pageBuilder.getBlockBuilder(3).appendNull(); + } + else { + DOUBLE.writeDouble(pageBuilder.getBlockBuilder(3), extendedPrice); + } + + if (extendedPriceIsNull || discountIsNull) { + pageBuilder.getBlockBuilder(4).appendNull(); + } + else { + DOUBLE.writeDouble(pageBuilder.getBlockBuilder(4), extendedPrice * (1 - discount)); + } + + if (extendedPriceIsNull || discountIsNull || taxIsNull) { + pageBuilder.getBlockBuilder(5).appendNull(); + } + else { + DOUBLE.writeDouble(pageBuilder.getBlockBuilder(5), extendedPrice * (1 - discount) * (1 + tax)); + } + + if (discountIsNull) { + pageBuilder.getBlockBuilder(6).appendNull(); + } + else { + DOUBLE.writeDouble(pageBuilder.getBlockBuilder(6), discount); + } + } + } + } + } + + public static void main(String[] args) + { + new HandTpchQuery1(createLocalQueryRunner()).runBenchmark(new SimpleLineBenchmarkResultWriter(System.out)); + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/HandTpchQuery6.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/HandTpchQuery6.java new file mode 100644 index 00000000..8058c98a --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/HandTpchQuery6.java @@ -0,0 +1,119 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.facebook.presto.operator.AggregationOperator.AggregationOperatorFactory; +import com.facebook.presto.operator.FilterAndProjectOperator; +import com.facebook.presto.operator.OperatorFactory; +import com.facebook.presto.operator.PageProcessor; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.planner.plan.AggregationNode.Step; +import com.facebook.presto.testing.LocalQueryRunner; +import com.facebook.presto.util.DateTimeUtils; +import com.google.common.collect.ImmutableList; + +import java.util.List; +import java.util.Optional; + +import static com.facebook.presto.benchmark.BenchmarkQueryRunner.createLocalQueryRunner; +import static com.facebook.presto.operator.aggregation.DoubleSumAggregation.DOUBLE_SUM; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.DateType.DATE; +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; + +public class HandTpchQuery6 + extends AbstractSimpleOperatorBenchmark +{ + public HandTpchQuery6(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "hand_tpch_query_6", 10, 100); + } + + @Override + protected List createOperatorFactories() + { + // select sum(extendedprice * discount) as revenue + // from lineitem + // where shipdate >= '1994-01-01' + // and shipdate < '1995-01-01' + // and discount >= 0.05 + // and discount <= 0.07 + // and quantity < 24; + OperatorFactory tableScanOperator = createTableScanOperator(0, "lineitem", "extendedprice", "discount", "shipdate", "quantity"); + + FilterAndProjectOperator.FilterAndProjectOperatorFactory tpchQuery6Operator = new FilterAndProjectOperator.FilterAndProjectOperatorFactory(1, new TpchQuery6Processor(), ImmutableList.of(DOUBLE)); + + AggregationOperatorFactory aggregationOperator = new AggregationOperatorFactory( + 2, + Step.SINGLE, + ImmutableList.of( + DOUBLE_SUM.bind(ImmutableList.of(0), Optional.empty(), Optional.empty(), 1.0) + )); + + return ImmutableList.of(tableScanOperator, tpchQuery6Operator, aggregationOperator); + } + + public static class TpchQuery6Processor + implements PageProcessor + { + private static final int MIN_SHIP_DATE = DateTimeUtils.parseDate("1994-01-01"); + private static final int MAX_SHIP_DATE = DateTimeUtils.parseDate("1995-01-01"); + + @Override + public int process(ConnectorSession session, Page page, int start, int end, PageBuilder pageBuilder) + { + Block discountBlock = page.getBlock(1); + int position = start; + for (; position < end; position++) { + // where shipdate >= '1994-01-01' + // and shipdate < '1995-01-01' + // and discount >= 0.05 + // and discount <= 0.07 + // and quantity < 24; + if (filter(position, discountBlock, page.getBlock(2), page.getBlock(3))) { + project(position, pageBuilder, page.getBlock(0), discountBlock); + } + } + return position; + } + + private static void project(int position, PageBuilder pageBuilder, Block extendedPriceBlock, Block discountBlock) + { + if (discountBlock.isNull(position) || extendedPriceBlock.isNull(position)) { + pageBuilder.getBlockBuilder(0).appendNull(); + } + else { + DOUBLE.writeDouble(pageBuilder.getBlockBuilder(0), DOUBLE.getDouble(extendedPriceBlock, position) * DOUBLE.getDouble(discountBlock, position)); + } + } + + private static boolean filter(int position, Block discountBlock, Block shipDateBlock, Block quantityBlock) + { + return !shipDateBlock.isNull(position) && DATE.getLong(shipDateBlock, position) >= MIN_SHIP_DATE && + !shipDateBlock.isNull(position) && DATE.getLong(shipDateBlock, position) < MAX_SHIP_DATE && + !discountBlock.isNull(position) && DOUBLE.getDouble(discountBlock, position) >= 0.05 && + !discountBlock.isNull(position) && DOUBLE.getDouble(discountBlock, position) <= 0.07 && + !quantityBlock.isNull(position) && BIGINT.getLong(quantityBlock, position) < 24; + } + } + + public static void main(String[] args) + { + new HandTpchQuery6(createLocalQueryRunner()).runBenchmark(new SimpleLineBenchmarkResultWriter(System.out)); + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/HashAggregationBenchmark.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/HashAggregationBenchmark.java new file mode 100644 index 00000000..7148f9aa --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/HashAggregationBenchmark.java @@ -0,0 +1,61 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.facebook.presto.operator.HashAggregationOperator.HashAggregationOperatorFactory; +import com.facebook.presto.operator.OperatorFactory; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.planner.plan.AggregationNode.Step; +import com.facebook.presto.testing.LocalQueryRunner; +import com.google.common.collect.ImmutableList; +import com.google.common.primitives.Ints; +import io.airlift.units.DataSize; + +import java.util.List; +import java.util.Optional; + +import static com.facebook.presto.benchmark.BenchmarkQueryRunner.createLocalQueryRunner; +import static com.facebook.presto.operator.aggregation.DoubleSumAggregation.DOUBLE_SUM; +import static io.airlift.units.DataSize.Unit.MEGABYTE; + +public class HashAggregationBenchmark + extends AbstractSimpleOperatorBenchmark +{ + public HashAggregationBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "hash_agg", 5, 25); + } + + @Override + protected List createOperatorFactories() + { + OperatorFactory tableScanOperator = createTableScanOperator(0, "orders", "orderstatus", "totalprice"); + List types = ImmutableList.of(tableScanOperator.getTypes().get(0)); + HashAggregationOperatorFactory aggregationOperator = new HashAggregationOperatorFactory(1, + types, + Ints.asList(0), + Step.SINGLE, + ImmutableList.of(DOUBLE_SUM.bind(ImmutableList.of(1), Optional.empty(), Optional.empty(), 1.0)), + Optional.empty(), + Optional.empty(), + 100_000, + new DataSize(16, MEGABYTE)); + return ImmutableList.of(tableScanOperator, aggregationOperator); + } + + public static void main(String[] args) + { + new HashAggregationBenchmark(createLocalQueryRunner()).runBenchmark(new SimpleLineBenchmarkResultWriter(System.out)); + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/HashBuildAndJoinBenchmark.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/HashBuildAndJoinBenchmark.java new file mode 100644 index 00000000..d63cbf10 --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/HashBuildAndJoinBenchmark.java @@ -0,0 +1,97 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.facebook.presto.operator.Driver; +import com.facebook.presto.operator.DriverFactory; +import com.facebook.presto.operator.HashBuilderOperator.HashBuilderOperatorFactory; +import com.facebook.presto.operator.LookupJoinOperators; +import com.facebook.presto.operator.OperatorFactory; +import com.facebook.presto.operator.TaskContext; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.testing.LocalQueryRunner; +import com.facebook.presto.testing.NullOutputOperator.NullOutputOperatorFactory; +import com.google.common.collect.ImmutableList; +import com.google.common.primitives.Ints; + +import java.util.List; +import java.util.Optional; + +import static com.facebook.presto.benchmark.BenchmarkQueryRunner.createLocalQueryRunner; +import static com.facebook.presto.benchmark.BenchmarkQueryRunner.createLocalQueryRunnerHashEnabled; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; + +public class HashBuildAndJoinBenchmark + extends AbstractOperatorBenchmark +{ + private final boolean hashEnabled; + private final OperatorFactory ordersTableScan = createTableScanOperator(0, "orders", "orderkey", "totalprice"); + private final OperatorFactory lineItemTableScan = createTableScanOperator(0, "lineitem", "orderkey", "quantity"); + + public HashBuildAndJoinBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "hash_build_and_join_hash_enabled_" + localQueryRunner.isHashEnabled(), 4, 5); + this.hashEnabled = localQueryRunner.isHashEnabled(); + } + + /* + select orderkey, quantity, totalprice + from lineitem join orders using (orderkey) + */ + @Override + protected List createDrivers(TaskContext taskContext) + { + ImmutableList.Builder driversBuilder = ImmutableList.builder(); + driversBuilder.add(ordersTableScan); + OperatorFactory source = ordersTableScan; + Optional hashChannel = Optional.empty(); + if (hashEnabled) { + source = createHashProjectOperator(1, ImmutableList.of(BIGINT, DOUBLE)); + driversBuilder.add(source); + hashChannel = Optional.of(2); + } + + // hash build + HashBuilderOperatorFactory hashBuilder = new HashBuilderOperatorFactory(2, source.getTypes(), Ints.asList(0), hashChannel, 1_500_000); + driversBuilder.add(hashBuilder); + DriverFactory hashBuildDriverFactory = new DriverFactory(true, false, driversBuilder.build()); + Driver hashBuildDriver = hashBuildDriverFactory.createDriver(taskContext.addPipelineContext(true, false).addDriverContext()); + + // join + ImmutableList.Builder joinDriversBuilder = ImmutableList.builder(); + joinDriversBuilder.add(lineItemTableScan); + source = lineItemTableScan; + hashChannel = Optional.empty(); + if (hashEnabled) { + source = createHashProjectOperator(1, ImmutableList.of(BIGINT, BIGINT)); + joinDriversBuilder.add(source); + hashChannel = Optional.of(2); + } + + OperatorFactory joinOperator = LookupJoinOperators.innerJoin(1, hashBuilder.getLookupSourceSupplier(), source.getTypes(), Ints.asList(0), hashChannel); + joinDriversBuilder.add(joinOperator); + joinDriversBuilder.add(new NullOutputOperatorFactory(2, joinOperator.getTypes())); + DriverFactory joinDriverFactory = new DriverFactory(true, true, joinDriversBuilder.build()); + Driver joinDriver = joinDriverFactory.createDriver(taskContext.addPipelineContext(true, true).addDriverContext()); + + return ImmutableList.of(hashBuildDriver, joinDriver); + } + + public static void main(String[] args) + { + new HashBuildAndJoinBenchmark(createLocalQueryRunner()).runBenchmark(new SimpleLineBenchmarkResultWriter(System.out)); + new HashBuildAndJoinBenchmark(createLocalQueryRunnerHashEnabled()).runBenchmark(new SimpleLineBenchmarkResultWriter(System.out)); + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/HashBuildBenchmark.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/HashBuildBenchmark.java new file mode 100644 index 00000000..db80b9d1 --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/HashBuildBenchmark.java @@ -0,0 +1,53 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.facebook.presto.operator.Driver; +import com.facebook.presto.operator.DriverFactory; +import com.facebook.presto.operator.HashBuilderOperator.HashBuilderOperatorFactory; +import com.facebook.presto.operator.OperatorFactory; +import com.facebook.presto.operator.TaskContext; +import com.facebook.presto.testing.LocalQueryRunner; +import com.google.common.collect.ImmutableList; +import com.google.common.primitives.Ints; + +import java.util.List; +import java.util.Optional; + +import static com.facebook.presto.benchmark.BenchmarkQueryRunner.createLocalQueryRunner; + +public class HashBuildBenchmark + extends AbstractOperatorBenchmark +{ + public HashBuildBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "hash_build" + localQueryRunner.isHashEnabled(), 4, 5); + } + + @Override + protected List createDrivers(TaskContext taskContext) + { + OperatorFactory ordersTableScan = createTableScanOperator(0, "orders", "orderkey", "totalprice"); + HashBuilderOperatorFactory hashBuilder = new HashBuilderOperatorFactory(1, ordersTableScan.getTypes(), Ints.asList(0), Optional.empty(), 1_500_000); + + DriverFactory driverFactory = new DriverFactory(true, true, ordersTableScan, hashBuilder); + Driver driver = driverFactory.createDriver(taskContext.addPipelineContext(true, true).addDriverContext()); + return ImmutableList.of(driver); + } + + public static void main(String[] args) + { + new HashBuildBenchmark(createLocalQueryRunner()).runBenchmark(new SimpleLineBenchmarkResultWriter(System.out)); + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/HashJoinBenchmark.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/HashJoinBenchmark.java new file mode 100644 index 00000000..374f184a --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/HashJoinBenchmark.java @@ -0,0 +1,80 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.facebook.presto.operator.Driver; +import com.facebook.presto.operator.DriverContext; +import com.facebook.presto.operator.DriverFactory; +import com.facebook.presto.operator.HashBuilderOperator.HashBuilderOperatorFactory; +import com.facebook.presto.operator.LookupJoinOperators; +import com.facebook.presto.operator.LookupSourceSupplier; +import com.facebook.presto.operator.OperatorFactory; +import com.facebook.presto.operator.TaskContext; +import com.facebook.presto.testing.LocalQueryRunner; +import com.facebook.presto.testing.NullOutputOperator.NullOutputOperatorFactory; +import com.google.common.collect.ImmutableList; +import com.google.common.primitives.Ints; + +import java.util.List; +import java.util.Optional; + +import static com.facebook.presto.benchmark.BenchmarkQueryRunner.createLocalQueryRunner; + +public class HashJoinBenchmark + extends AbstractOperatorBenchmark +{ + private LookupSourceSupplier lookupSourceSupplier; + + public HashJoinBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "hash_join", 4, 50); + } + + /* + select orderkey, quantity, totalprice + from lineitem join orders using (orderkey) + */ + + @Override + protected List createDrivers(TaskContext taskContext) + { + if (lookupSourceSupplier == null) { + OperatorFactory ordersTableScan = createTableScanOperator(0, "orders", "orderkey", "totalprice"); + HashBuilderOperatorFactory hashBuilder = new HashBuilderOperatorFactory(1, ordersTableScan.getTypes(), Ints.asList(0), Optional.empty(), 1_500_000); + + DriverContext driverContext = taskContext.addPipelineContext(false, false).addDriverContext(); + Driver driver = new DriverFactory(false, false, ordersTableScan, hashBuilder).createDriver(driverContext); + while (!driver.isFinished()) { + driver.process(); + } + lookupSourceSupplier = hashBuilder.getLookupSourceSupplier(); + } + + OperatorFactory lineItemTableScan = createTableScanOperator(0, "lineitem", "orderkey", "quantity"); + + OperatorFactory joinOperator = LookupJoinOperators.innerJoin(1, lookupSourceSupplier, lineItemTableScan.getTypes(), Ints.asList(0), Optional.empty()); + + NullOutputOperatorFactory output = new NullOutputOperatorFactory(2, joinOperator.getTypes()); + + DriverFactory driverFactory = new DriverFactory(true, true, lineItemTableScan, joinOperator, output); + DriverContext driverContext = taskContext.addPipelineContext(true, true).addDriverContext(); + Driver driver = driverFactory.createDriver(driverContext); + return ImmutableList.of(driver); + } + + public static void main(String[] args) + { + new HashJoinBenchmark(createLocalQueryRunner()).runBenchmark(new SimpleLineBenchmarkResultWriter(System.out)); + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/JsonAvgBenchmarkResultWriter.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/JsonAvgBenchmarkResultWriter.java new file mode 100644 index 00000000..59e8ddc7 --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/JsonAvgBenchmarkResultWriter.java @@ -0,0 +1,110 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Preconditions; +import com.google.common.base.Throwables; +import io.airlift.json.JsonCodec; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Map; + +import static java.nio.charset.StandardCharsets.UTF_8; + +public class JsonAvgBenchmarkResultWriter + implements BenchmarkResultHook +{ + private static final JsonCodec JSON_CODEC = JsonCodec.jsonCodec(BuildResult.class); + private final OutputStream outputStream; + + private int sampleCount; + public long totalElapsedMillis; + public long totalInputRows; + public long totalInputRowsPerSecond; + public long totalOutputRows; + public long totalOutputRowsPerSecond; + public long totalInputMegabytes; + public long totalInputMegabytesPerSecond; + + public JsonAvgBenchmarkResultWriter(OutputStream outputStream) + { + Preconditions.checkNotNull(outputStream, "outputStream is null"); + this.outputStream = outputStream; + } + + @Override + public BenchmarkResultHook addResults(Map results) + { + Preconditions.checkNotNull(results, "results is null"); + sampleCount++; + totalElapsedMillis += getValue(results, "elapsed_millis"); + totalInputRows += getValue(results, "input_rows;"); + totalInputRowsPerSecond += getValue(results, "input_rows_per_second"); + totalOutputRows += getValue(results, "output_rows"); + totalOutputRowsPerSecond += getValue(results, "output_rows_per_second"); + totalInputMegabytes += getValue(results, "input_megabytes"); + totalInputMegabytesPerSecond += getValue(results, "input_megabytes_per_second"); + return this; + } + + private long getValue(Map results, String name) + { + Long value = results.get(name); + if (value == null) { + return 0; + } + return value; + } + + @Override + public void finished() + { + BuildResult average = new BuildResult(); + average.elapsedMillis += totalElapsedMillis / sampleCount; + average.inputRows += totalInputRows / sampleCount; + average.inputRowsPerSecond += totalInputRowsPerSecond / sampleCount; + average.outputRows += totalOutputRows / sampleCount; + average.outputRowsPerSecond += totalOutputRowsPerSecond / sampleCount; + average.inputMegabytes += totalInputRows / sampleCount; + average.inputMegabytesPerSecond += totalInputMegabytesPerSecond / sampleCount; + + String json = JSON_CODEC.toJson(average); + try { + outputStream.write(json.getBytes(UTF_8)); + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } + + public static class BuildResult + { + @JsonProperty + public long elapsedMillis; + @JsonProperty + public long inputRows; + @JsonProperty + public long inputRowsPerSecond; + @JsonProperty + public long outputRows; + @JsonProperty + public long outputRowsPerSecond; + @JsonProperty + public long inputMegabytes; + @JsonProperty + public long inputMegabytesPerSecond; + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/JsonBenchmarkResultWriter.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/JsonBenchmarkResultWriter.java new file mode 100644 index 00000000..5331530f --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/JsonBenchmarkResultWriter.java @@ -0,0 +1,73 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.fasterxml.jackson.core.JsonEncoding; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import com.google.common.base.Preconditions; +import com.google.common.base.Throwables; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Map; + +public class JsonBenchmarkResultWriter + implements BenchmarkResultHook +{ + private final JsonGenerator jsonGenerator; + + public JsonBenchmarkResultWriter(OutputStream outputStream) + { + Preconditions.checkNotNull(outputStream, "outputStream is null"); + try { + jsonGenerator = new JsonFactory().createJsonGenerator(outputStream, JsonEncoding.UTF8); + jsonGenerator.writeStartObject(); + jsonGenerator.writeArrayFieldStart("samples"); + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } + + @Override + public BenchmarkResultHook addResults(Map results) + { + Preconditions.checkNotNull(results, "results is null"); + try { + jsonGenerator.writeStartObject(); + for (Map.Entry entry : results.entrySet()) { + jsonGenerator.writeNumberField(entry.getKey(), entry.getValue()); + } + jsonGenerator.writeEndObject(); + } + catch (IOException e) { + throw Throwables.propagate(e); + } + return this; + } + + @Override + public void finished() + { + try { + jsonGenerator.writeEndArray(); + jsonGenerator.writeEndObject(); + jsonGenerator.close(); + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/LongMaxAggregationSqlBenchmark.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/LongMaxAggregationSqlBenchmark.java new file mode 100644 index 00000000..fa5488bd --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/LongMaxAggregationSqlBenchmark.java @@ -0,0 +1,32 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.facebook.presto.testing.LocalQueryRunner; + +import static com.facebook.presto.benchmark.BenchmarkQueryRunner.createLocalQueryRunner; + +public class LongMaxAggregationSqlBenchmark + extends AbstractSqlBenchmark +{ + public LongMaxAggregationSqlBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "sql_long_max", 40, 200, "select max(partkey) from lineitem"); + } + + public static void main(String[] args) + { + new LongMaxAggregationSqlBenchmark(createLocalQueryRunner()).runBenchmark(new SimpleLineBenchmarkResultWriter(System.out)); + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/OdsBenchmarkResultWriter.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/OdsBenchmarkResultWriter.java new file mode 100644 index 00000000..9feeefa0 --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/OdsBenchmarkResultWriter.java @@ -0,0 +1,76 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.fasterxml.jackson.core.JsonEncoding; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import com.google.common.base.Preconditions; +import com.google.common.base.Throwables; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Map; + +public class OdsBenchmarkResultWriter + implements BenchmarkResultHook +{ + private final String entity; + private final JsonGenerator jsonGenerator; + + public OdsBenchmarkResultWriter(String entity, OutputStream outputStream) + { + Preconditions.checkNotNull(entity, "entity is null"); + Preconditions.checkNotNull(outputStream, "outputStream is null"); + this.entity = entity; + try { + jsonGenerator = new JsonFactory().createJsonGenerator(outputStream, JsonEncoding.UTF8); + jsonGenerator.writeStartArray(); + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } + + @Override + public BenchmarkResultHook addResults(Map results) + { + Preconditions.checkNotNull(results, "results is null"); + try { + for (Map.Entry entry : results.entrySet()) { + jsonGenerator.writeStartObject(); + jsonGenerator.writeStringField("entity", entity); + jsonGenerator.writeStringField("key", entry.getKey()); + jsonGenerator.writeNumberField("value", entry.getValue()); + jsonGenerator.writeEndObject(); + } + } + catch (IOException e) { + throw Throwables.propagate(e); + } + return this; + } + + @Override + public void finished() + { + try { + jsonGenerator.writeEndArray(); + jsonGenerator.close(); + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/OrderByBenchmark.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/OrderByBenchmark.java new file mode 100644 index 00000000..96b7aa35 --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/OrderByBenchmark.java @@ -0,0 +1,59 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.facebook.presto.operator.LimitOperator.LimitOperatorFactory; +import com.facebook.presto.operator.OperatorFactory; +import com.facebook.presto.operator.OrderByOperator.OrderByOperatorFactory; +import com.facebook.presto.testing.LocalQueryRunner; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.facebook.presto.benchmark.BenchmarkQueryRunner.createLocalQueryRunner; +import static com.facebook.presto.spi.block.SortOrder.ASC_NULLS_LAST; + +public class OrderByBenchmark + extends AbstractSimpleOperatorBenchmark +{ + private static final int ROWS = 1_500_000; + + public OrderByBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "in_memory_orderby_1.5M", 5, 10); + } + + @Override + protected List createOperatorFactories() + { + OperatorFactory tableScanOperator = createTableScanOperator(0, "orders", "totalprice", "clerk"); + + LimitOperatorFactory limitOperator = new LimitOperatorFactory(1, tableScanOperator.getTypes(), ROWS); + + OrderByOperatorFactory orderByOperator = new OrderByOperatorFactory( + 2, + limitOperator.getTypes(), + ImmutableList.of(1), + ROWS, + ImmutableList.of(0), + ImmutableList.of(ASC_NULLS_LAST)); + + return ImmutableList.of(tableScanOperator, limitOperator, orderByOperator); + } + + public static void main(String[] args) + { + new OrderByBenchmark(createLocalQueryRunner()).runBenchmark(new SimpleLineBenchmarkResultWriter(System.out)); + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/PredicateFilterBenchmark.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/PredicateFilterBenchmark.java new file mode 100644 index 00000000..f6a18681 --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/PredicateFilterBenchmark.java @@ -0,0 +1,79 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.facebook.presto.operator.FilterAndProjectOperator; +import com.facebook.presto.operator.FilterFunction; +import com.facebook.presto.operator.GenericPageProcessor; +import com.facebook.presto.operator.OperatorFactory; +import com.facebook.presto.spi.RecordCursor; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.testing.LocalQueryRunner; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.facebook.presto.benchmark.BenchmarkQueryRunner.createLocalQueryRunner; +import static com.facebook.presto.operator.ProjectionFunctions.singleColumn; +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; + +public class PredicateFilterBenchmark + extends AbstractSimpleOperatorBenchmark +{ + public PredicateFilterBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "predicate_filter", 5, 50); + } + + @Override + protected List createOperatorFactories() + { + OperatorFactory tableScanOperator = createTableScanOperator(0, "orders", "totalprice"); + FilterAndProjectOperator.FilterAndProjectOperatorFactory filterAndProjectOperator = new FilterAndProjectOperator.FilterAndProjectOperatorFactory( + 1, + new GenericPageProcessor(new DoubleFilter(50000.00), ImmutableList.of(singleColumn(DOUBLE, 0))), + ImmutableList.of(DOUBLE)); + + return ImmutableList.of(tableScanOperator, filterAndProjectOperator); + } + + public static class DoubleFilter + implements FilterFunction + { + private final double minValue; + + public DoubleFilter(double minValue) + { + this.minValue = minValue; + } + + @Override + public boolean filter(int position, Block... blocks) + { + return DOUBLE.getDouble(blocks[0], position) >= minValue; + } + + @Override + public boolean filter(RecordCursor cursor) + { + return cursor.getDouble(0) >= minValue; + } + } + + public static void main(String[] args) + { + new PredicateFilterBenchmark(createLocalQueryRunner()).runBenchmark(new SimpleLineBenchmarkResultWriter(System.out)); + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/PredicateFilterSqlBenchmark.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/PredicateFilterSqlBenchmark.java new file mode 100644 index 00000000..98a95f32 --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/PredicateFilterSqlBenchmark.java @@ -0,0 +1,32 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.facebook.presto.testing.LocalQueryRunner; + +import static com.facebook.presto.benchmark.BenchmarkQueryRunner.createLocalQueryRunner; + +public class PredicateFilterSqlBenchmark + extends AbstractSqlBenchmark +{ + public PredicateFilterSqlBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "sql_predicate_filter", 5, 50, "select totalprice from orders where totalprice > 50000"); + } + + public static void main(String[] args) + { + new PredicateFilterSqlBenchmark(createLocalQueryRunner()).runBenchmark(new SimpleLineBenchmarkResultWriter(System.out)); + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/RawStreamingBenchmark.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/RawStreamingBenchmark.java new file mode 100644 index 00000000..fb285443 --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/RawStreamingBenchmark.java @@ -0,0 +1,44 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.facebook.presto.operator.OperatorFactory; +import com.facebook.presto.testing.LocalQueryRunner; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.facebook.presto.benchmark.BenchmarkQueryRunner.createLocalQueryRunner; + +public class RawStreamingBenchmark + extends AbstractSimpleOperatorBenchmark +{ + public RawStreamingBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "raw_stream", 10, 100); + } + + @Override + protected List createOperatorFactories() + { + OperatorFactory tableScanOperator = createTableScanOperator(0, "orders", "totalprice"); + + return ImmutableList.of(tableScanOperator); + } + + public static void main(String[] args) + { + new RawStreamingBenchmark(createLocalQueryRunner()).runBenchmark(new SimpleLineBenchmarkResultWriter(System.out)); + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/RawStreamingSqlBenchmark.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/RawStreamingSqlBenchmark.java new file mode 100644 index 00000000..c9a352d2 --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/RawStreamingSqlBenchmark.java @@ -0,0 +1,32 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.facebook.presto.testing.LocalQueryRunner; + +import static com.facebook.presto.benchmark.BenchmarkQueryRunner.createLocalQueryRunner; + +public class RawStreamingSqlBenchmark + extends AbstractSqlBenchmark +{ + public RawStreamingSqlBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "sql_raw_stream", 10, 100, "select totalprice from orders"); + } + + public static void main(String[] args) + { + new RawStreamingSqlBenchmark(createLocalQueryRunner()).runBenchmark(new SimpleLineBenchmarkResultWriter(System.out)); + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SimpleLineBenchmarkResultWriter.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SimpleLineBenchmarkResultWriter.java new file mode 100644 index 00000000..c94937d1 --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SimpleLineBenchmarkResultWriter.java @@ -0,0 +1,57 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.google.common.base.Joiner; +import com.google.common.base.Throwables; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.util.Map; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class SimpleLineBenchmarkResultWriter + implements BenchmarkResultHook +{ + private final Writer writer; + + public SimpleLineBenchmarkResultWriter(OutputStream outputStream) + { + writer = new OutputStreamWriter(checkNotNull(outputStream, "outputStream is null")); + } + + @Override + public BenchmarkResultHook addResults(Map results) + { + checkNotNull(results, "results is null"); + try { + Joiner.on(",").withKeyValueSeparator(":").appendTo(writer, results); + writer.write('\n'); + writer.flush(); + } + catch (IOException e) { + Throwables.propagate(e); + } + return this; + } + + @Override + public void finished() + { + // No-op + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlApproximateCountDistinctDoubleBenchmark.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlApproximateCountDistinctDoubleBenchmark.java new file mode 100644 index 00000000..c0e7e950 --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlApproximateCountDistinctDoubleBenchmark.java @@ -0,0 +1,32 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.facebook.presto.testing.LocalQueryRunner; + +import static com.facebook.presto.benchmark.BenchmarkQueryRunner.createLocalQueryRunner; + +public class SqlApproximateCountDistinctDoubleBenchmark + extends AbstractSqlBenchmark +{ + public SqlApproximateCountDistinctDoubleBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "sql_approx_count_distinct_double", 10, 50, "select approx_distinct(totalprice) from orders"); + } + + public static void main(String[] args) + { + new SqlApproximateCountDistinctDoubleBenchmark(createLocalQueryRunner()).runBenchmark(new SimpleLineBenchmarkResultWriter(System.out)); + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlApproximateCountDistinctLongBenchmark.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlApproximateCountDistinctLongBenchmark.java new file mode 100644 index 00000000..dfafb651 --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlApproximateCountDistinctLongBenchmark.java @@ -0,0 +1,32 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.facebook.presto.testing.LocalQueryRunner; + +import static com.facebook.presto.benchmark.BenchmarkQueryRunner.createLocalQueryRunner; + +public class SqlApproximateCountDistinctLongBenchmark + extends AbstractSqlBenchmark +{ + public SqlApproximateCountDistinctLongBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "sql_approx_count_distinct_long", 10, 50, "select approx_distinct(custkey) from orders"); + } + + public static void main(String[] args) + { + new SqlApproximateCountDistinctLongBenchmark(createLocalQueryRunner()).runBenchmark(new SimpleLineBenchmarkResultWriter(System.out)); + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlApproximateCountDistinctVarBinaryBenchmark.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlApproximateCountDistinctVarBinaryBenchmark.java new file mode 100644 index 00000000..2b4d4c08 --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlApproximateCountDistinctVarBinaryBenchmark.java @@ -0,0 +1,32 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.facebook.presto.testing.LocalQueryRunner; + +import static com.facebook.presto.benchmark.BenchmarkQueryRunner.createLocalQueryRunner; + +public class SqlApproximateCountDistinctVarBinaryBenchmark + extends AbstractSqlBenchmark +{ + public SqlApproximateCountDistinctVarBinaryBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "sql_approx_count_distinct_varbinary", 10, 50, "select approx_distinct(clerk) from orders"); + } + + public static void main(String[] args) + { + new SqlApproximateCountDistinctVarBinaryBenchmark(createLocalQueryRunner()).runBenchmark(new SimpleLineBenchmarkResultWriter(System.out)); + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlApproximatePercentileBenchmark.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlApproximatePercentileBenchmark.java new file mode 100644 index 00000000..29a30759 --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlApproximatePercentileBenchmark.java @@ -0,0 +1,32 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.facebook.presto.testing.LocalQueryRunner; + +import static com.facebook.presto.benchmark.BenchmarkQueryRunner.createLocalQueryRunner; + +public class SqlApproximatePercentileBenchmark + extends AbstractSqlBenchmark +{ + public SqlApproximatePercentileBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "sql_approx_percentile_long", 10, 30, "select approx_percentile(custkey, 0.9) from orders"); + } + + public static void main(String[] args) + { + new SqlApproximatePercentileBenchmark(createLocalQueryRunner()).runBenchmark(new SimpleLineBenchmarkResultWriter(System.out)); + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlBetweenBenchmark.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlBetweenBenchmark.java new file mode 100644 index 00000000..4f5c2314 --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlBetweenBenchmark.java @@ -0,0 +1,32 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.facebook.presto.testing.LocalQueryRunner; + +import static com.facebook.presto.benchmark.BenchmarkQueryRunner.createLocalQueryRunner; + +public class SqlBetweenBenchmark + extends AbstractSqlBenchmark +{ + public SqlBetweenBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "sql_between_long", 10, 30, "SELECT COUNT(*) FROM orders WHERE custkey BETWEEN 10000 AND 20000 OR custkey BETWEEN 30000 AND 35000 OR custkey BETWEEN 50000 AND 51000"); + } + + public static void main(String[] args) + { + new SqlBetweenBenchmark(createLocalQueryRunner()).runBenchmark(new SimpleLineBenchmarkResultWriter(System.out)); + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlDistinctMultipleFields.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlDistinctMultipleFields.java new file mode 100644 index 00000000..3976cb4f --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlDistinctMultipleFields.java @@ -0,0 +1,32 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.facebook.presto.testing.LocalQueryRunner; + +import static com.facebook.presto.benchmark.BenchmarkQueryRunner.createLocalQueryRunner; + +public class SqlDistinctMultipleFields + extends AbstractSqlBenchmark +{ + public SqlDistinctMultipleFields(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "sql_distinct_multi", 4, 10, "SELECT DISTINCT orderpriority, shippriority FROM orders"); + } + + public static void main(String[] args) + { + new SqlDistinctMultipleFields(createLocalQueryRunner()).runBenchmark(new SimpleLineBenchmarkResultWriter(System.out)); + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlDistinctSingleField.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlDistinctSingleField.java new file mode 100644 index 00000000..1ce7d927 --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlDistinctSingleField.java @@ -0,0 +1,32 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.facebook.presto.testing.LocalQueryRunner; + +import static com.facebook.presto.benchmark.BenchmarkQueryRunner.createLocalQueryRunner; + +public class SqlDistinctSingleField + extends AbstractSqlBenchmark +{ + public SqlDistinctSingleField(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "sql_distinct_single", 10, 20, "SELECT DISTINCT shippriority FROM orders"); + } + + public static void main(String[] args) + { + new SqlDistinctSingleField(createLocalQueryRunner()).runBenchmark(new SimpleLineBenchmarkResultWriter(System.out)); + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlDoubleSumAggregationBenchmark.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlDoubleSumAggregationBenchmark.java new file mode 100644 index 00000000..c9f504d1 --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlDoubleSumAggregationBenchmark.java @@ -0,0 +1,32 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.facebook.presto.testing.LocalQueryRunner; + +import static com.facebook.presto.benchmark.BenchmarkQueryRunner.createLocalQueryRunner; + +public class SqlDoubleSumAggregationBenchmark + extends AbstractSqlBenchmark +{ + public SqlDoubleSumAggregationBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "sql_double_sum_agg", 10, 100, "select sum(totalprice) from orders"); + } + + public static void main(String[] args) + { + new SqlDoubleSumAggregationBenchmark(createLocalQueryRunner()).runBenchmark(new SimpleLineBenchmarkResultWriter(System.out)); + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlHashJoinBenchmark.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlHashJoinBenchmark.java new file mode 100644 index 00000000..f91ad1d3 --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlHashJoinBenchmark.java @@ -0,0 +1,36 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.facebook.presto.testing.LocalQueryRunner; + +import static com.facebook.presto.benchmark.BenchmarkQueryRunner.createLocalQueryRunner; + +public class SqlHashJoinBenchmark + extends AbstractSqlBenchmark +{ + public SqlHashJoinBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, + "sql_hash_join", + 4, + 5, + "select lineitem.orderkey, lineitem.quantity, orders.totalprice, orders.orderkey from lineitem join orders using (orderkey)"); + } + + public static void main(String[] args) + { + new SqlHashJoinBenchmark(createLocalQueryRunner()).runBenchmark(new SimpleLineBenchmarkResultWriter(System.out)); + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlInBenchmark.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlInBenchmark.java new file mode 100644 index 00000000..9927c7e3 --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlInBenchmark.java @@ -0,0 +1,36 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.facebook.presto.testing.LocalQueryRunner; + +import static com.facebook.presto.benchmark.BenchmarkQueryRunner.createLocalQueryRunner; + +public class SqlInBenchmark + extends AbstractSqlBenchmark +{ + public SqlInBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, + "sql_in", + 10, + 50, + "SELECT orderkey FROM lineitem WHERE orderkey IN (1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30)"); + } + + public static void main(String[] args) + { + new SqlInBenchmark(createLocalQueryRunner()).runBenchmark(new SimpleLineBenchmarkResultWriter(System.out)); + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlJoinWithPredicateBenchmark.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlJoinWithPredicateBenchmark.java new file mode 100644 index 00000000..0002537b --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlJoinWithPredicateBenchmark.java @@ -0,0 +1,36 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.facebook.presto.testing.LocalQueryRunner; + +import static com.facebook.presto.benchmark.BenchmarkQueryRunner.createLocalQueryRunner; + +public class SqlJoinWithPredicateBenchmark + extends AbstractSqlBenchmark +{ + public SqlJoinWithPredicateBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, + "sql_join_with_predicate", + 1, + 5, + "select count(*) from lineitem l join orders o on l.orderkey = o.orderkey and l.partkey % 2 = 0 and o.orderkey % 2 = 0\n"); + } + + public static void main(String[] args) + { + new SqlJoinWithPredicateBenchmark(createLocalQueryRunner()).runBenchmark(new SimpleLineBenchmarkResultWriter(System.out)); + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlLikeBenchmark.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlLikeBenchmark.java new file mode 100644 index 00000000..892d2259 --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlLikeBenchmark.java @@ -0,0 +1,32 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.facebook.presto.testing.LocalQueryRunner; + +import static com.facebook.presto.benchmark.BenchmarkQueryRunner.createLocalQueryRunner; + +public class SqlLikeBenchmark + extends AbstractSqlBenchmark +{ + public SqlLikeBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "sql_like", 4, 5, "SELECT orderkey FROM lineitem WHERE comment LIKE '%ly%ly%'"); + } + + public static void main(String[] args) + { + new SqlLikeBenchmark(createLocalQueryRunner()).runBenchmark(new SimpleLineBenchmarkResultWriter(System.out)); + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlRegexpLikeBenchmark.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlRegexpLikeBenchmark.java new file mode 100644 index 00000000..27607b50 --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlRegexpLikeBenchmark.java @@ -0,0 +1,32 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.facebook.presto.testing.LocalQueryRunner; + +import static com.facebook.presto.benchmark.BenchmarkQueryRunner.createLocalQueryRunner; + +public class SqlRegexpLikeBenchmark + extends AbstractSqlBenchmark +{ + public SqlRegexpLikeBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "sql_regexp_like", 4, 5, "SELECT count(*) FROM orders WHERE regexp_like(comment, '\\b[a-z]{5}ly\\b')"); + } + + public static void main(String[] args) + { + new SqlRegexpLikeBenchmark(createLocalQueryRunner()).runBenchmark(new SimpleLineBenchmarkResultWriter(System.out)); + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlSemiJoinInPredicateBenchmark.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlSemiJoinInPredicateBenchmark.java new file mode 100644 index 00000000..d28ecbe0 --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlSemiJoinInPredicateBenchmark.java @@ -0,0 +1,36 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.facebook.presto.testing.LocalQueryRunner; + +import static com.facebook.presto.benchmark.BenchmarkQueryRunner.createLocalQueryRunner; + +public class SqlSemiJoinInPredicateBenchmark + extends AbstractSqlBenchmark +{ + public SqlSemiJoinInPredicateBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, + "sql_semijoin_in", + 2, + 4, + "SELECT orderkey FROM lineitem WHERE orderkey IN (SELECT orderkey FROM orders WHERE orderkey % 2 = 0)"); + } + + public static void main(String[] args) + { + new SqlSemiJoinInPredicateBenchmark(createLocalQueryRunner()).runBenchmark(new SimpleLineBenchmarkResultWriter(System.out)); + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlTpchQuery1.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlTpchQuery1.java new file mode 100644 index 00000000..76a10dad --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlTpchQuery1.java @@ -0,0 +1,53 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.facebook.presto.testing.LocalQueryRunner; + +import static com.facebook.presto.benchmark.BenchmarkQueryRunner.createLocalQueryRunner; + +public class SqlTpchQuery1 + extends AbstractSqlBenchmark +{ + public SqlTpchQuery1(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "sql_tpch_query_1", 1, 5, "" + + "select\n" + + " returnflag,\n" + + " linestatus,\n" + + " sum(quantity) as sum_qty,\n" + + " sum(extendedprice) as sum_base_price,\n" + + " sum(extendedprice * (1 - discount)) as sum_disc_price,\n" + + " sum(extendedprice * (1 - discount) * (1 + tax)) as sum_charge,\n" + + " avg(quantity) as avg_qty,\n" + + " avg(extendedprice) as avg_price,\n" + + " avg(discount) as avg_disc,\n" + + " count(*) as count_order\n" + + "from\n" + + " lineitem\n" + + "where\n" + + " shipdate <= DATE '1998-09-02'\n" + + "group by\n" + + " returnflag,\n" + + " linestatus\n" + + "order by\n" + + " returnflag,\n" + + " linestatus"); + } + + public static void main(String[] args) + { + new SqlTpchQuery1(createLocalQueryRunner()).runBenchmark(new SimpleLineBenchmarkResultWriter(System.out)); + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlTpchQuery6.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlTpchQuery6.java new file mode 100644 index 00000000..28590d98 --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/SqlTpchQuery6.java @@ -0,0 +1,39 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.facebook.presto.testing.LocalQueryRunner; + +import static com.facebook.presto.benchmark.BenchmarkQueryRunner.createLocalQueryRunner; + +public class SqlTpchQuery6 + extends AbstractSqlBenchmark +{ + public SqlTpchQuery6(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "sql_tpch_query_6", 4, 20, "" + + "select sum(extendedprice * discount) as revenue \n" + + "from lineitem \n" + + "where shipdate >= DATE '1994-01-01' \n" + + " and shipdate < DATE '1995-01-01' \n" + + " and discount >= 0.05 \n" + + " and discount <= 0.07 \n" + + " and quantity < 24"); + } + + public static void main(String[] args) + { + new SqlTpchQuery6(createLocalQueryRunner()).runBenchmark(new SimpleLineBenchmarkResultWriter(System.out)); + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/StatisticsBenchmark.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/StatisticsBenchmark.java new file mode 100644 index 00000000..c4806a0c --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/StatisticsBenchmark.java @@ -0,0 +1,106 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.facebook.presto.testing.LocalQueryRunner; + +import static com.facebook.presto.benchmark.BenchmarkQueryRunner.createLocalQueryRunner; + +public abstract class StatisticsBenchmark +{ + public static void main(String... args) + { + LocalQueryRunner localQueryRunner = createLocalQueryRunner(); + new LongVarianceBenchmark(localQueryRunner).runBenchmark(new AverageBenchmarkResults()); + new LongVariancePopBenchmark(localQueryRunner).runBenchmark(new AverageBenchmarkResults()); + new DoubleVarianceBenchmark(localQueryRunner).runBenchmark(new AverageBenchmarkResults()); + new DoubleVariancePopBenchmark(localQueryRunner).runBenchmark(new AverageBenchmarkResults()); + new LongStdDevBenchmark(localQueryRunner).runBenchmark(new AverageBenchmarkResults()); + new LongStdDevPopBenchmark(localQueryRunner).runBenchmark(new AverageBenchmarkResults()); + new DoubleStdDevBenchmark(localQueryRunner).runBenchmark(new AverageBenchmarkResults()); + new DoubleStdDevPopBenchmark(localQueryRunner).runBenchmark(new AverageBenchmarkResults()); + } + + public static class LongVarianceBenchmark + extends AbstractSqlBenchmark + { + public LongVarianceBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "stat_long_variance", 25, 150, "select var_samp(orderkey) from orders"); + } + } + + public static class LongVariancePopBenchmark + extends AbstractSqlBenchmark + { + public LongVariancePopBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "stat_long_variance_pop", 25, 150, "select var_pop(orderkey) from orders"); + } + } + + public static class DoubleVarianceBenchmark + extends AbstractSqlBenchmark + { + public DoubleVarianceBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "stat_double_variance", 25, 150, "select var_samp(totalprice) from orders"); + } + } + + public static class DoubleVariancePopBenchmark + extends AbstractSqlBenchmark + { + public DoubleVariancePopBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "stat_double_variance_pop", 25, 150, "select var_pop(totalprice) from orders"); + } + } + + public static class LongStdDevBenchmark + extends AbstractSqlBenchmark + { + public LongStdDevBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "stat_long_stddev", 25, 150, "select stddev_samp(orderkey) from orders"); + } + } + + public static class LongStdDevPopBenchmark + extends AbstractSqlBenchmark + { + public LongStdDevPopBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "stat_long_stddev_pop", 25, 150, "select stddev_pop(orderkey) from orders"); + } + } + + public static class DoubleStdDevBenchmark + extends AbstractSqlBenchmark + { + public DoubleStdDevBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "stat_double_stddev", 25, 150, "select stddev_samp(totalprice) from orders"); + } + } + + public static class DoubleStdDevPopBenchmark + extends AbstractSqlBenchmark + { + public DoubleStdDevPopBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "stat_double_stddev_pop", 25, 150, "select stddev_pop(totalprice) from orders"); + } + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/StructuredTypesBenchmark.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/StructuredTypesBenchmark.java new file mode 100644 index 00000000..c85b96f3 --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/StructuredTypesBenchmark.java @@ -0,0 +1,53 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.facebook.presto.testing.LocalQueryRunner; + +import static com.facebook.presto.benchmark.BenchmarkQueryRunner.createLocalQueryRunner; + +public class StructuredTypesBenchmark + extends AbstractSqlBenchmark +{ + // Benchmark is modeled after TPCH query 1, with array/map creation added in + public StructuredTypesBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "structured_types", 4, 5, "" + + "select\n" + + " returnflag,\n" + + " linestatus,\n" + + " sum(array[quantity][1]) as sum_qty,\n" + + " sum(array[extendedprice][1]) as sum_base_price,\n" + + " sum(array[extendedprice][1] * (1 - map(array['key'], array[discount])['key'])) as sum_disc_price,\n" + + " sum(array[extendedprice][1] * (1 - map(array['key'], array[discount])['key']) * (1 + tax)) as sum_charge,\n" + + " avg(map(array['key'], array[quantity])['key']) as avg_qty,\n" + + " avg(map(array['key'], array[extendedprice])['key']) as avg_price,\n" + + " avg(map(array['key'], array[discount])['key']) as avg_disc\n" + + "from\n" + + " lineitem\n" + + "where\n" + + " shipdate <= DATE '1998-09-02'\n" + + "group by\n" + + " returnflag,\n" + + " linestatus\n" + + "order by\n" + + " returnflag,\n" + + " linestatus"); + } + + public static void main(String[] args) + { + new StructuredTypesBenchmark(createLocalQueryRunner()).runBenchmark(new SimpleLineBenchmarkResultWriter(System.out)); + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/Top100Benchmark.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/Top100Benchmark.java new file mode 100644 index 00000000..4ce842db --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/Top100Benchmark.java @@ -0,0 +1,52 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.facebook.presto.operator.OperatorFactory; +import com.facebook.presto.operator.TopNOperator.TopNOperatorFactory; +import com.facebook.presto.testing.LocalQueryRunner; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.facebook.presto.benchmark.BenchmarkQueryRunner.createLocalQueryRunner; +import static com.facebook.presto.spi.block.SortOrder.ASC_NULLS_LAST; + +public class Top100Benchmark + extends AbstractSimpleOperatorBenchmark +{ + public Top100Benchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "top100", 5, 50); + } + + @Override + protected List createOperatorFactories() + { + OperatorFactory tableScanOperator = createTableScanOperator(0, "orders", "totalprice"); + TopNOperatorFactory topNOperator = new TopNOperatorFactory( + 1, + tableScanOperator.getTypes(), + 100, + ImmutableList.of(0), + ImmutableList.of(ASC_NULLS_LAST), + false); + return ImmutableList.of(tableScanOperator, topNOperator); + } + + public static void main(String[] args) + { + new Top100Benchmark(createLocalQueryRunner()).runBenchmark(new SimpleLineBenchmarkResultWriter(System.out)); + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/Top100SqlBenchmark.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/Top100SqlBenchmark.java new file mode 100644 index 00000000..bf0a6739 --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/Top100SqlBenchmark.java @@ -0,0 +1,32 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.facebook.presto.testing.LocalQueryRunner; + +import static com.facebook.presto.benchmark.BenchmarkQueryRunner.createLocalQueryRunner; + +public class Top100SqlBenchmark + extends AbstractSqlBenchmark +{ + public Top100SqlBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "sql_top_100", 5, 50, "select totalprice from orders order by totalprice desc limit 100"); + } + + public static void main(String[] args) + { + new Top100SqlBenchmark(createLocalQueryRunner()).runBenchmark(new SimpleLineBenchmarkResultWriter(System.out)); + } +} diff --git a/presto-benchmark/src/main/java/com/facebook/presto/benchmark/VarBinaryMaxAggregationSqlBenchmark.java b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/VarBinaryMaxAggregationSqlBenchmark.java new file mode 100644 index 00000000..450f33a6 --- /dev/null +++ b/presto-benchmark/src/main/java/com/facebook/presto/benchmark/VarBinaryMaxAggregationSqlBenchmark.java @@ -0,0 +1,32 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.facebook.presto.testing.LocalQueryRunner; + +import static com.facebook.presto.benchmark.BenchmarkQueryRunner.createLocalQueryRunner; + +public class VarBinaryMaxAggregationSqlBenchmark + extends AbstractSqlBenchmark +{ + public VarBinaryMaxAggregationSqlBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "sql_varbinary_max", 4, 20, "select max(shipinstruct) from lineitem"); + } + + public static void main(String[] args) + { + new VarBinaryMaxAggregationSqlBenchmark(createLocalQueryRunner()).runBenchmark(new SimpleLineBenchmarkResultWriter(System.out)); + } +} diff --git a/presto-benchmark/src/test/java/com/facebook/presto/benchmark/TestBenchmarks.java b/presto-benchmark/src/test/java/com/facebook/presto/benchmark/TestBenchmarks.java new file mode 100644 index 00000000..92da86f7 --- /dev/null +++ b/presto-benchmark/src/test/java/com/facebook/presto/benchmark/TestBenchmarks.java @@ -0,0 +1,40 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.benchmark; + +import com.facebook.presto.testing.LocalQueryRunner; +import org.testng.annotations.Test; + +import static com.facebook.presto.benchmark.BenchmarkQueryRunner.createLocalQueryRunner; +import static com.facebook.presto.benchmark.BenchmarkSuite.createBenchmarks; + +public class TestBenchmarks +{ + @Test + public void smokeTest() + throws Exception + { + try (LocalQueryRunner localQueryRunner = createLocalQueryRunner(); + LocalQueryRunner hashEnabledLocalQueryRunner = createLocalQueryRunner()) { + for (AbstractBenchmark benchmark : createBenchmarks(localQueryRunner, hashEnabledLocalQueryRunner)) { + try { + benchmark.runOnce(); + } + catch (RuntimeException e) { + throw new AssertionError("Error running " + benchmark.getBenchmarkName(), e); + } + } + } + } +} diff --git a/presto-cassandra/pom.xml b/presto-cassandra/pom.xml new file mode 100644 index 00000000..24a1a9cd --- /dev/null +++ b/presto-cassandra/pom.xml @@ -0,0 +1,376 @@ + + + 4.0.0 + + com.facebook.presto + presto-root + 0.107 + + + presto-cassandra + Presto - Cassandra Connector + presto-plugin + + + ${project.parent.basedir} + 2.1.0 + 2.1.5 + + + + + com.datastax.cassandra + cassandra-driver-core + ${datastax.version} + + + io.netty + netty + + + + + + org.apache.cassandra + cassandra-thrift + ${cassandra.version} + + + junit + junit + + + org.apache.thrift + libthrift + + + commons-cli + commons-cli + + + org.yaml + snakeyaml + + + log4j + log4j + + + org.slf4j + log4j-over-slf4j + + + org.slf4j + slf4j-jdk14 + + + org.slf4j + slf4j-log4j12 + + + + + + org.apache.cassandra + cassandra-all + ${cassandra.version} + + + junit + junit + + + org.apache.thrift + libthrift + + + commons-cli + commons-cli + + + org.yaml + snakeyaml + + + ch.qos.logback + logback-classic + + + ch.qos.logback + logback-core + + + io.netty + netty + + + + + + org.weakref + jmxutils + + + + com.google.code.findbugs + annotations + + + + io.airlift + bootstrap + + + + io.airlift + json + + + + io.airlift + concurrent + + + + io.airlift + log + + + + io.airlift + units + + + + io.airlift + configuration + + + + com.google.guava + guava + + + + com.google.inject + guice + + + + javax.validation + validation-api + + + + joda-time + joda-time + + + + + com.facebook.presto + presto-spi + provided + + + + io.airlift + slice + provided + + + + javax.inject + javax.inject + provided + + + + com.fasterxml.jackson.core + jackson-annotations + provided + + + + com.fasterxml.jackson.core + jackson-core + provided + + + + com.fasterxml.jackson.core + jackson-databind + provided + + + + + com.facebook.presto + presto-main + test + + + + com.facebook.presto + presto-tests + test + + + + com.facebook.presto + presto-tpch + test + + + + io.airlift.tpch + tpch + test + + + + org.cassandraunit + cassandra-unit + 2.0.2.0 + test + + + junit + junit + + + org.slf4j + slf4j-log4j12 + + + org.apache.thrift + libthrift + + + org.jboss.netty + netty + + + org.yaml + snakeyaml + + + com.datastax.cassandra + cassandra-driver-core + + + + + + org.apache.thrift + libthrift + + + + com.datastax.cassandra + cassandra-driver-mapping + ${datastax.version} + test + + + io.netty + netty + + + + + + org.testng + testng + test + + + + io.airlift + testing + test + + + + javax.servlet + javax.servlet-api + test + + + + org.jetbrains + annotations + provided + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + + + + 1 + + + + **/TestCassandraDistributed.java + + + + + + com.ning.maven.plugins + maven-duplicate-finder-plugin + + + + + + org.apache.cassandra + cassandra-all + + + org.apache.cassandra + cassandra-thrift + + + + org.apache.cassandra.thrift.ITransportFactory + org.apache.cassandra.thrift.TFramedTransportFactory + + + + + + org.cassandraunit + cassandra-unit + + + + + + + + + + ci-cassandra + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + + + + + + diff --git a/presto-cassandra/src/main/java/com/facebook/presto/cassandra/BackoffRetryPolicy.java b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/BackoffRetryPolicy.java new file mode 100644 index 00000000..26b0532d --- /dev/null +++ b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/BackoffRetryPolicy.java @@ -0,0 +1,61 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import com.datastax.driver.core.ConsistencyLevel; +import com.datastax.driver.core.Statement; +import com.datastax.driver.core.WriteType; +import com.datastax.driver.core.policies.DefaultRetryPolicy; +import com.datastax.driver.core.policies.RetryPolicy; + +import java.util.concurrent.ThreadLocalRandom; + +public class BackoffRetryPolicy + implements RetryPolicy +{ + public static final BackoffRetryPolicy INSTANCE = new BackoffRetryPolicy(); + + private BackoffRetryPolicy() {} + + @Override + public RetryDecision onUnavailable(Statement statement, ConsistencyLevel consistencyLevel, int requiredReplica, int aliveReplica, int retries) + { + if (retries >= 10) { + return RetryDecision.rethrow(); + } + + try { + int jitter = ThreadLocalRandom.current().nextInt(100); + int delay = (100 * (retries + 1)) + jitter; + Thread.sleep(delay); + return RetryDecision.retry(consistencyLevel); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return RetryDecision.rethrow(); + } + } + + @Override + public RetryDecision onReadTimeout(Statement statement, ConsistencyLevel cl, int requiredResponses, int receivedResponses, boolean dataRetrieved, int nbRetry) + { + return DefaultRetryPolicy.INSTANCE.onReadTimeout(statement, cl, requiredResponses, receivedResponses, dataRetrieved, nbRetry); + } + + @Override + public RetryDecision onWriteTimeout(Statement statement, ConsistencyLevel cl, WriteType writeType, int requiredAcks, int receivedAcks, int nbRetry) + { + return DefaultRetryPolicy.INSTANCE.onWriteTimeout(statement, cl, writeType, requiredAcks, receivedAcks, nbRetry); + } +} diff --git a/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CachingCassandraSchemaProvider.java b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CachingCassandraSchemaProvider.java new file mode 100644 index 00000000..ea96ffbc --- /dev/null +++ b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CachingCassandraSchemaProvider.java @@ -0,0 +1,352 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import com.facebook.presto.spi.NotFoundException; +import com.facebook.presto.spi.SchemaNotFoundException; +import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.spi.TableNotFoundException; +import com.google.common.base.Throwables; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Maps; +import com.google.common.util.concurrent.UncheckedExecutionException; +import io.airlift.units.Duration; +import org.weakref.jmx.Managed; + +import javax.annotation.concurrent.ThreadSafe; +import javax.inject.Inject; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; + +import static com.facebook.presto.cassandra.RetryDriver.retry; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.cache.CacheLoader.asyncReloading; +import static java.util.Locale.ENGLISH; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +/** + * Cassandra Schema Cache + */ +@ThreadSafe +public class CachingCassandraSchemaProvider +{ + private final String connectorId; + private final CassandraSession session; + + /** + * Mapping from an empty string to all schema names. Each schema name is a + * mapping from the lower case schema name to the case sensitive schema name. + * This mapping is necessary because Presto currently does not properly handle + * case sensitive names. + */ + private final LoadingCache> schemaNamesCache; + + /** + * Mapping from lower case schema name to all tables in that schema. Each + * table name is a mapping from the lower case table name to the case + * sensitive table name. This mapping is necessary because Presto currently + * does not properly handle case sensitive names. + */ + private final LoadingCache> tableNamesCache; + + private final LoadingCache> partitionsCache; + private final LoadingCache> partitionsCacheFull; + private final LoadingCache tableCache; + + @Inject + public CachingCassandraSchemaProvider( + CassandraConnectorId connectorId, + CassandraSession session, + @ForCassandra ExecutorService executor, + CassandraClientConfig cassandraClientConfig) + { + this(checkNotNull(connectorId, "connectorId is null").toString(), + session, + executor, + checkNotNull(cassandraClientConfig, "cassandraClientConfig is null").getSchemaCacheTtl(), + cassandraClientConfig.getSchemaRefreshInterval()); + } + + public CachingCassandraSchemaProvider(String connectorId, CassandraSession session, ExecutorService executor, Duration cacheTtl, Duration refreshInterval) + { + this.connectorId = checkNotNull(connectorId, "connectorId is null"); + this.session = checkNotNull(session, "cassandraSession is null"); + + checkNotNull(executor, "executor is null"); + + long expiresAfterWriteMillis = checkNotNull(cacheTtl, "cacheTtl is null").toMillis(); + long refreshMills = checkNotNull(refreshInterval, "refreshInterval is null").toMillis(); + + schemaNamesCache = CacheBuilder.newBuilder() + .expireAfterWrite(expiresAfterWriteMillis, MILLISECONDS) + .refreshAfterWrite(refreshMills, MILLISECONDS) + .build(asyncReloading(new CacheLoader>() + { + @Override + public Map load(String key) + throws Exception + { + return loadAllSchemas(); + } + }, executor)); + + tableNamesCache = CacheBuilder.newBuilder() + .expireAfterWrite(expiresAfterWriteMillis, MILLISECONDS) + .refreshAfterWrite(refreshMills, MILLISECONDS) + .build(asyncReloading(new CacheLoader>() + { + @Override + public Map load(String databaseName) + throws Exception + { + return loadAllTables(databaseName); + } + }, executor)); + + tableCache = CacheBuilder.newBuilder() + .expireAfterWrite(expiresAfterWriteMillis, MILLISECONDS) + .refreshAfterWrite(refreshMills, MILLISECONDS) + .build(asyncReloading(new CacheLoader() + { + @Override + public CassandraTable load(SchemaTableName tableName) + throws Exception + { + return loadTable(tableName); + } + }, executor)); + + partitionsCache = CacheBuilder.newBuilder() + .expireAfterWrite(expiresAfterWriteMillis, MILLISECONDS) + .refreshAfterWrite(refreshMills, MILLISECONDS) + .build(asyncReloading(new CacheLoader>() + { + @Override + public List load(PartitionListKey key) + throws Exception + { + return loadPartitions(key); + } + }, executor)); + + partitionsCacheFull = CacheBuilder.newBuilder() + .expireAfterWrite(expiresAfterWriteMillis, MILLISECONDS) + .build(asyncReloading(new CacheLoader>() + { + @Override + public List load(PartitionListKey key) + throws Exception + { + return loadPartitions(key); + } + }, executor)); + } + + @Managed + public void flushCache() + { + schemaNamesCache.invalidateAll(); + tableNamesCache.invalidateAll(); + partitionsCache.invalidateAll(); + tableCache.invalidateAll(); + } + + public List getAllSchemas() + { + return ImmutableList.copyOf(getCacheValue(schemaNamesCache, "", RuntimeException.class).keySet()); + } + + private Map loadAllSchemas() + throws Exception + { + return retry() + .stopOnIllegalExceptions() + .run("getAllSchemas", () -> Maps.uniqueIndex(session.getAllSchemas(), CachingCassandraSchemaProvider::toLowerCase)); + } + + public List getAllTables(String databaseName) + throws SchemaNotFoundException + { + return ImmutableList.copyOf(getCacheValue(tableNamesCache, databaseName, SchemaNotFoundException.class).keySet()); + } + + private Map loadAllTables(final String databaseName) + throws Exception + { + return retry().stopOn(NotFoundException.class).stopOnIllegalExceptions() + .run("getAllTables", () -> { + String caseSensitiveDatabaseName = getCaseSensitiveSchemaName(databaseName); + if (caseSensitiveDatabaseName == null) { + caseSensitiveDatabaseName = databaseName; + } + List tables = session.getAllTables(caseSensitiveDatabaseName); + Map nameMap = Maps.uniqueIndex(tables, CachingCassandraSchemaProvider::toLowerCase); + + if (tables.isEmpty()) { + // Check to see if the database exists + session.getSchema(databaseName); + } + return nameMap; + }); + } + + public CassandraTableHandle getTableHandle(SchemaTableName schemaTableName) + { + checkNotNull(schemaTableName, "schemaTableName is null"); + String schemaName = getCaseSensitiveSchemaName(schemaTableName.getSchemaName()); + String tableName = getCaseSensitiveTableName(schemaTableName); + CassandraTableHandle tableHandle = new CassandraTableHandle(connectorId, schemaName, tableName); + return tableHandle; + } + + public String getCaseSensitiveSchemaName(String caseInsensitiveName) + { + String caseSensitiveSchemaName = getCacheValue(schemaNamesCache, "", RuntimeException.class).get(caseInsensitiveName.toLowerCase(ENGLISH)); + return caseSensitiveSchemaName == null ? caseInsensitiveName : caseSensitiveSchemaName; + } + + public String getCaseSensitiveTableName(SchemaTableName schemaTableName) + { + String caseSensitiveTableName = getCacheValue(tableNamesCache, schemaTableName.getSchemaName(), SchemaNotFoundException.class).get(schemaTableName.getTableName().toLowerCase(ENGLISH)); + return caseSensitiveTableName == null ? schemaTableName.getTableName() : caseSensitiveTableName; + } + + public CassandraTable getTable(CassandraTableHandle tableHandle) + throws TableNotFoundException + { + return getCacheValue(tableCache, tableHandle.getSchemaTableName(), TableNotFoundException.class); + } + + public void flushTable(SchemaTableName tableName) + { + tableCache.asMap().remove(tableName); + + tableNamesCache.asMap().remove(tableName.getSchemaName()); + + for (Iterator iterator = partitionsCache.asMap().keySet().iterator(); iterator.hasNext(); ) { + PartitionListKey partitionListKey = iterator.next(); + if (partitionListKey.getTable().getTableHandle().getSchemaTableName().equals(tableName)) { + iterator.remove(); + } + } + for (Iterator iterator = partitionsCacheFull.asMap().keySet().iterator(); iterator.hasNext(); ) { + PartitionListKey partitionListKey = iterator.next(); + if (partitionListKey.getTable().getTableHandle().getSchemaTableName().equals(tableName)) { + iterator.remove(); + } + } + } + + private CassandraTable loadTable(final SchemaTableName tableName) + throws Exception + { + return retry() + .stopOn(NotFoundException.class) + .stopOnIllegalExceptions() + .run("getTable", () -> session.getTable(tableName)); + } + + public List getAllPartitions(CassandraTable table) + { + PartitionListKey key = new PartitionListKey(table, ImmutableList.>of()); + return getCacheValue(partitionsCache, key, RuntimeException.class); + } + + public List getPartitions(CassandraTable table, List> partitionKeys) + { + checkNotNull(table, "table is null"); + checkNotNull(partitionKeys, "partitionKeys is null"); + checkArgument(partitionKeys.size() == table.getPartitionKeyColumns().size()); + + PartitionListKey key = new PartitionListKey(table, partitionKeys); + return getCacheValue(partitionsCacheFull, key, RuntimeException.class); + } + + private List loadPartitions(final PartitionListKey key) + throws Exception + { + return retry() + .stopOnIllegalExceptions() + .run("getPartitions", () -> session.getPartitions(key.getTable(), key.getFilterPrefix())); + } + + private static V getCacheValue(LoadingCache cache, K key, Class exceptionClass) + throws E + { + try { + return cache.get(key); + } + catch (ExecutionException | UncheckedExecutionException e) { + Throwable t = e.getCause(); + Throwables.propagateIfInstanceOf(t, exceptionClass); + throw Throwables.propagate(t); + } + } + + private static String toLowerCase(String value) + { + return value.toLowerCase(ENGLISH); + } + + private static final class PartitionListKey + { + private final CassandraTable table; + private final List> filterPrefix; + + PartitionListKey(CassandraTable table, List> filterPrefix) + { + this.table = table; + this.filterPrefix = ImmutableList.copyOf(filterPrefix); + } + + public List> getFilterPrefix() + { + return filterPrefix; + } + + public CassandraTable getTable() + { + return table; + } + + @Override + public int hashCode() + { + return Objects.hash(table, filterPrefix); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + PartitionListKey other = (PartitionListKey) obj; + return Objects.equals(this.table, other.table) && + Objects.equals(this.filterPrefix, other.filterPrefix); + } + } +} diff --git a/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraClientConfig.java b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraClientConfig.java new file mode 100644 index 00000000..4b79d974 --- /dev/null +++ b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraClientConfig.java @@ -0,0 +1,355 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import com.datastax.driver.core.ConsistencyLevel; +import com.datastax.driver.core.SocketOptions; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; +import io.airlift.configuration.Config; +import io.airlift.configuration.ConfigDescription; +import io.airlift.units.Duration; +import io.airlift.units.MaxDuration; +import io.airlift.units.MinDuration; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +public class CassandraClientConfig +{ + private static final Splitter SPLITTER = Splitter.on(',').trimResults().omitEmptyStrings(); + + private Duration schemaCacheTtl = new Duration(1, TimeUnit.HOURS); + private Duration schemaRefreshInterval = new Duration(2, TimeUnit.MINUTES); + private int maxSchemaRefreshThreads = 10; + private int limitForPartitionKeySelect = 200; + private int fetchSizeForPartitionKeySelect = 20_000; + private ConsistencyLevel consistencyLevel = ConsistencyLevel.ONE; + private int fetchSize = 5_000; + private List contactPoints = ImmutableList.of(); + private int nativeProtocolPort = 9042; + private int partitionSizeForBatchSelect = 100; + private int splitSize = 1_024; + private String partitioner = "Murmur3Partitioner"; + private int thriftPort = 9160; + private String thriftConnectionFactoryClassName = "org.apache.cassandra.thrift.TFramedTransportFactory"; + private Map transportFactoryOptions = new HashMap<>(); + private boolean allowDropTable; + private String username; + private String password; + private Duration clientReadTimeout = new Duration(SocketOptions.DEFAULT_READ_TIMEOUT_MILLIS, MILLISECONDS); + private Duration clientConnectTimeout = new Duration(SocketOptions.DEFAULT_CONNECT_TIMEOUT_MILLIS, MILLISECONDS); + private Integer clientSoLinger; + private RetryPolicyType retryPolicy = RetryPolicyType.DEFAULT; + + @Min(0) + public int getLimitForPartitionKeySelect() + { + return limitForPartitionKeySelect; + } + + @Config("cassandra.limit-for-partition-key-select") + public CassandraClientConfig setLimitForPartitionKeySelect(int limitForPartitionKeySelect) + { + this.limitForPartitionKeySelect = limitForPartitionKeySelect; + return this; + } + + @Min(1) + public int getMaxSchemaRefreshThreads() + { + return maxSchemaRefreshThreads; + } + + @Config("cassandra.max-schema-refresh-threads") + public CassandraClientConfig setMaxSchemaRefreshThreads(int maxSchemaRefreshThreads) + { + this.maxSchemaRefreshThreads = maxSchemaRefreshThreads; + return this; + } + + @NotNull + public Duration getSchemaCacheTtl() + { + return schemaCacheTtl; + } + + @Config("cassandra.schema-cache-ttl") + public CassandraClientConfig setSchemaCacheTtl(Duration schemaCacheTtl) + { + this.schemaCacheTtl = schemaCacheTtl; + return this; + } + + @NotNull + public Duration getSchemaRefreshInterval() + { + return schemaRefreshInterval; + } + + @Config("cassandra.schema-refresh-interval") + public CassandraClientConfig setSchemaRefreshInterval(Duration schemaRefreshInterval) + { + this.schemaRefreshInterval = schemaRefreshInterval; + return this; + } + + @NotNull + @Size(min = 1) + public List getContactPoints() + { + return contactPoints; + } + + @Config("cassandra.contact-points") + public CassandraClientConfig setContactPoints(String commaSeparatedList) + { + this.contactPoints = SPLITTER.splitToList(commaSeparatedList); + return this; + } + + public CassandraClientConfig setContactPoints(String... contactPoints) + { + this.contactPoints = Arrays.asList(contactPoints); + return this; + } + + @Min(1) + public int getNativeProtocolPort() + { + return nativeProtocolPort; + } + + @Config(("cassandra.native-protocol-port")) + public CassandraClientConfig setNativeProtocolPort(int nativeProtocolPort) + { + this.nativeProtocolPort = nativeProtocolPort; + return this; + } + + @NotNull + public ConsistencyLevel getConsistencyLevel() + { + return consistencyLevel; + } + + @Config("cassandra.consistency-level") + public CassandraClientConfig setConsistencyLevel(ConsistencyLevel level) + { + this.consistencyLevel = level; + return this; + } + + @Min(1) + public int getFetchSize() + { + return fetchSize; + } + + @Config("cassandra.fetch-size") + public CassandraClientConfig setFetchSize(int fetchSize) + { + this.fetchSize = fetchSize; + return this; + } + + @Min(1) + public int getFetchSizeForPartitionKeySelect() + { + return fetchSizeForPartitionKeySelect; + } + + @Config("cassandra.fetch-size-for-partition-key-select") + public CassandraClientConfig setFetchSizeForPartitionKeySelect(int fetchSizeForPartitionKeySelect) + { + this.fetchSizeForPartitionKeySelect = fetchSizeForPartitionKeySelect; + return this; + } + + @Min(1) + public int getPartitionSizeForBatchSelect() + { + return partitionSizeForBatchSelect; + } + + @Config("cassandra.partition-size-for-batch-select") + public CassandraClientConfig setPartitionSizeForBatchSelect(int partitionSizeForBatchSelect) + { + this.partitionSizeForBatchSelect = partitionSizeForBatchSelect; + return this; + } + + public int getThriftPort() + { + return thriftPort; + } + + @Config(("cassandra.thrift-port")) + public CassandraClientConfig setThriftPort(int thriftPort) + { + this.thriftPort = thriftPort; + return this; + } + + @Min(1) + public int getSplitSize() + { + return splitSize; + } + + @Config("cassandra.split-size") + public CassandraClientConfig setSplitSize(int splitSize) + { + this.splitSize = splitSize; + return this; + } + + public String getPartitioner() + { + return partitioner; + } + + @Config("cassandra.partitioner") + public CassandraClientConfig setPartitioner(String partitioner) + { + this.partitioner = partitioner; + return this; + } + + public String getThriftConnectionFactoryClassName() + { + return thriftConnectionFactoryClassName; + } + + @Config("cassandra.thrift-connection-factory-class") + public CassandraClientConfig setThriftConnectionFactoryClassName(String thriftConnectionFactoryClassName) + { + this.thriftConnectionFactoryClassName = thriftConnectionFactoryClassName; + return this; + } + + public Map getTransportFactoryOptions() + { + return transportFactoryOptions; + } + + @Config("cassandra.transport-factory-options") + public CassandraClientConfig setTransportFactoryOptions(String transportFactoryOptions) + { + checkNotNull(transportFactoryOptions, "transportFactoryOptions is null"); + this.transportFactoryOptions = Splitter.on(',').omitEmptyStrings().trimResults().withKeyValueSeparator("=").split(transportFactoryOptions); + return this; + } + + public boolean getAllowDropTable() + { + return this.allowDropTable; + } + + @Config("cassandra.allow-drop-table") + @ConfigDescription("Allow hive connector to drop table") + public CassandraClientConfig setAllowDropTable(boolean allowDropTable) + { + this.allowDropTable = allowDropTable; + return this; + } + + public String getUsername() + { + return username; + } + + @Config("cassandra.username") + public CassandraClientConfig setUsername(String username) + { + this.username = username; + return this; + } + + public String getPassword() + { + return password; + } + + @Config("cassandra.password") + public CassandraClientConfig setPassword(String password) + { + this.password = password; + return this; + } + + @MinDuration("1ms") + @MaxDuration("1h") + public Duration getClientReadTimeout() + { + return clientReadTimeout; + } + + @Config("cassandra.client.read-timeout") + public CassandraClientConfig setClientReadTimeout(Duration clientReadTimeout) + { + this.clientReadTimeout = clientReadTimeout; + return this; + } + + @MinDuration("1ms") + @MaxDuration("1h") + public Duration getClientConnectTimeout() + { + return clientConnectTimeout; + } + + @Config("cassandra.client.connect-timeout") + public CassandraClientConfig setClientConnectTimeout(Duration clientConnectTimeout) + { + this.clientConnectTimeout = clientConnectTimeout; + return this; + } + + @Min(0) + public Integer getClientSoLinger() + { + return clientSoLinger; + } + + @Config("cassandra.client.so-linger") + public CassandraClientConfig setClientSoLinger(Integer clientSoLinger) + { + this.clientSoLinger = clientSoLinger; + return this; + } + + @NotNull + public RetryPolicyType getRetryPolicy() + { + return retryPolicy; + } + + @Config("cassandra.retry-policy") + public CassandraClientConfig setRetryPolicy(RetryPolicyType retryPolicy) + { + this.retryPolicy = retryPolicy; + return this; + } +} diff --git a/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraClientModule.java b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraClientModule.java new file mode 100644 index 00000000..9e0a403c --- /dev/null +++ b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraClientModule.java @@ -0,0 +1,129 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import com.datastax.driver.core.Cluster; +import com.datastax.driver.core.QueryOptions; +import com.datastax.driver.core.SocketOptions; +import com.datastax.driver.core.policies.ExponentialReconnectionPolicy; +import com.google.common.primitives.Ints; +import com.google.inject.Binder; +import com.google.inject.Module; +import com.google.inject.Provides; +import com.google.inject.Scopes; +import io.airlift.json.JsonCodec; + +import javax.inject.Singleton; + +import java.util.List; +import java.util.concurrent.ExecutorService; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.airlift.concurrent.Threads.daemonThreadsNamed; +import static io.airlift.configuration.ConfigBinder.configBinder; +import static io.airlift.json.JsonCodecBinder.jsonCodecBinder; +import static java.util.concurrent.Executors.newFixedThreadPool; +import static org.weakref.jmx.ObjectNames.generatedNameOf; +import static org.weakref.jmx.guice.ExportBinder.newExporter; + +public class CassandraClientModule + implements Module +{ + private final String connectorId; + + public CassandraClientModule(String connectorId) + { + this.connectorId = connectorId; + } + + @Override + public void configure(Binder binder) + { + binder.bind(CassandraConnectorId.class).toInstance(new CassandraConnectorId(connectorId)); + binder.bind(CassandraConnector.class).in(Scopes.SINGLETON); + binder.bind(CassandraMetadata.class).in(Scopes.SINGLETON); + binder.bind(CassandraSplitManager.class).in(Scopes.SINGLETON); + binder.bind(CassandraTokenSplitManager.class).in(Scopes.SINGLETON); + binder.bind(CassandraRecordSetProvider.class).in(Scopes.SINGLETON); + binder.bind(CassandraHandleResolver.class).in(Scopes.SINGLETON); + binder.bind(CassandraConnectorRecordSinkProvider.class).in(Scopes.SINGLETON); + + binder.bind(CassandraThriftConnectionFactory.class).in(Scopes.SINGLETON); + + configBinder(binder).bindConfig(CassandraClientConfig.class); + + binder.bind(CassandraThriftConnectionFactory.class).in(Scopes.SINGLETON); + + binder.bind(CachingCassandraSchemaProvider.class).in(Scopes.SINGLETON); + newExporter(binder).export(CachingCassandraSchemaProvider.class).as(generatedNameOf(CachingCassandraSchemaProvider.class, connectorId)); + + jsonCodecBinder(binder).bindListJsonCodec(ExtraColumnMetadata.class); + } + + @ForCassandra + @Singleton + @Provides + public static ExecutorService createCachingCassandraSchemaExecutor(CassandraConnectorId clientId, CassandraClientConfig cassandraClientConfig) + { + return newFixedThreadPool( + cassandraClientConfig.getMaxSchemaRefreshThreads(), + daemonThreadsNamed("cassandra-" + clientId + "-%s")); + } + + @Singleton + @Provides + public static CassandraSession createCassandraSession( + CassandraConnectorId connectorId, + CassandraClientConfig config, + JsonCodec> extraColumnMetadataCodec) + { + checkNotNull(config, "config is null"); + checkNotNull(extraColumnMetadataCodec, "extraColumnMetadataCodec is null"); + + Cluster.Builder clusterBuilder = Cluster.builder(); + + List contactPoints = checkNotNull(config.getContactPoints(), "contactPoints is null"); + checkArgument(!contactPoints.isEmpty(), "empty contactPoints"); + clusterBuilder.addContactPoints(contactPoints.toArray(new String[contactPoints.size()])); + + clusterBuilder.withPort(config.getNativeProtocolPort()); + clusterBuilder.withReconnectionPolicy(new ExponentialReconnectionPolicy(500, 10000)); + clusterBuilder.withRetryPolicy(config.getRetryPolicy().getPolicy()); + + SocketOptions socketOptions = new SocketOptions(); + socketOptions.setReadTimeoutMillis(Ints.checkedCast(config.getClientReadTimeout().toMillis())); + socketOptions.setConnectTimeoutMillis(Ints.checkedCast(config.getClientConnectTimeout().toMillis())); + if (config.getClientSoLinger() != null) { + socketOptions.setSoLinger(config.getClientSoLinger()); + } + clusterBuilder.withSocketOptions(socketOptions); + + if (config.getUsername() != null && config.getPassword() != null) { + clusterBuilder.withCredentials(config.getUsername(), config.getPassword()); + } + + QueryOptions options = new QueryOptions(); + options.setFetchSize(config.getFetchSize()); + options.setConsistencyLevel(config.getConsistencyLevel()); + clusterBuilder.withQueryOptions(options); + + return new CassandraSession( + connectorId.toString(), + clusterBuilder, + config.getFetchSizeForPartitionKeySelect(), + config.getLimitForPartitionKeySelect(), + extraColumnMetadataCodec); + } +} diff --git a/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraColumnHandle.java b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraColumnHandle.java new file mode 100644 index 00000000..ee923751 --- /dev/null +++ b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraColumnHandle.java @@ -0,0 +1,271 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import com.facebook.presto.cassandra.util.CassandraCqlUtils; +import com.facebook.presto.spi.ColumnMetadata; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.type.Type; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Function; +import com.google.common.base.MoreObjects.ToStringHelper; +import com.google.common.base.Predicate; + +import javax.annotation.Nullable; + +import java.util.List; +import java.util.Objects; + +import static com.facebook.presto.cassandra.util.Types.checkType; +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class CassandraColumnHandle + implements ColumnHandle +{ + public static final String SAMPLE_WEIGHT_COLUMN_NAME = "presto_sample_weight"; + + private final String connectorId; + private final String name; + private final int ordinalPosition; + private final CassandraType cassandraType; + private final List typeArguments; + private final boolean partitionKey; + private final boolean clusteringKey; + private final boolean indexed; + private final boolean hidden; + + @JsonCreator + public CassandraColumnHandle( + @JsonProperty("connectorId") String connectorId, + @JsonProperty("name") String name, + @JsonProperty("ordinalPosition") int ordinalPosition, + @JsonProperty("cassandraType") CassandraType cassandraType, + @Nullable @JsonProperty("typeArguments") List typeArguments, + @JsonProperty("partitionKey") boolean partitionKey, + @JsonProperty("clusteringKey") boolean clusteringKey, + @JsonProperty("indexed") boolean indexed, + @JsonProperty("hidden") boolean hidden) + { + this.connectorId = checkNotNull(connectorId, "connectorId is null"); + this.name = checkNotNull(name, "name is null"); + checkArgument(ordinalPosition >= 0, "ordinalPosition is negative"); + this.ordinalPosition = ordinalPosition; + this.cassandraType = checkNotNull(cassandraType, "cassandraType is null"); + int typeArgsSize = cassandraType.getTypeArgumentSize(); + if (typeArgsSize > 0) { + this.typeArguments = checkNotNull(typeArguments, "typeArguments is null"); + checkArgument(typeArguments.size() == typeArgsSize, cassandraType + + " must provide " + typeArgsSize + " type arguments"); + } + else { + this.typeArguments = null; + } + this.partitionKey = partitionKey; + this.clusteringKey = clusteringKey; + this.indexed = indexed; + this.hidden = hidden; + } + + @JsonProperty + public String getConnectorId() + { + return connectorId; + } + + @JsonProperty + public String getName() + { + return name; + } + + @JsonProperty + public int getOrdinalPosition() + { + return ordinalPosition; + } + + @JsonProperty + public CassandraType getCassandraType() + { + return cassandraType; + } + + @JsonProperty + public List getTypeArguments() + { + return typeArguments; + } + + @JsonProperty + public boolean isPartitionKey() + { + return partitionKey; + } + + @JsonProperty + public boolean isClusteringKey() + { + return clusteringKey; + } + + @JsonProperty + public boolean isIndexed() + { + return indexed; + } + + @JsonProperty + public boolean isHidden() + { + return hidden; + } + + public ColumnMetadata getColumnMetadata() + { + return new ColumnMetadata(CassandraCqlUtils.cqlNameToSqlName(name), cassandraType.getNativeType(), partitionKey, null, hidden); + } + + public Type getType() + { + return cassandraType.getNativeType(); + } + + @Override + public int hashCode() + { + return Objects.hash( + connectorId, + name, + ordinalPosition, + cassandraType, + typeArguments, + partitionKey, + clusteringKey, + indexed, + hidden); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + CassandraColumnHandle other = (CassandraColumnHandle) obj; + return Objects.equals(this.connectorId, other.connectorId) && + Objects.equals(this.name, other.name) && + Objects.equals(this.ordinalPosition, other.ordinalPosition) && + Objects.equals(this.cassandraType, other.cassandraType) && + Objects.equals(this.typeArguments, other.typeArguments) && + Objects.equals(this.partitionKey, other.partitionKey) && + Objects.equals(this.clusteringKey, other.clusteringKey) && + Objects.equals(this.indexed, other.indexed) && + Objects.equals(this.hidden, other.hidden); + } + + @Override + public String toString() + { + ToStringHelper helper = toStringHelper(this) + .add("connectorId", connectorId) + .add("name", name) + .add("ordinalPosition", ordinalPosition) + .add("cassandraType", cassandraType); + + if (typeArguments != null && !typeArguments.isEmpty()) { + helper.add("typeArguments", typeArguments); + } + + helper.add("partitionKey", partitionKey) + .add("clusteringKey", clusteringKey) + .add("indexed", indexed) + .add("hidden", hidden); + + return helper.toString(); + } + + public static Function cassandraColumnHandle() + { + return new Function() + { + @Override + public CassandraColumnHandle apply(ColumnHandle columnHandle) + { + return checkType(columnHandle, CassandraColumnHandle.class, "columnHandle"); + } + }; + } + + public static Function columnMetadataGetter() + { + return new Function() + { + @Override + public ColumnMetadata apply(ColumnHandle columnHandle) + { + checkNotNull(columnHandle, "columnHandle is null"); + checkArgument(columnHandle instanceof CassandraColumnHandle, + "columnHandle is not an instance of CassandraColumnHandle"); + return ((CassandraColumnHandle) columnHandle).getColumnMetadata(); + } + }; + } + + public static Function nativeTypeGetter() + { + return new Function() + { + @Override + public Type apply(CassandraColumnHandle input) + { + return input.getType(); + } + }; + } + + public static Function cassandraFullTypeGetter() + { + return new Function() + { + @Override + public FullCassandraType apply(CassandraColumnHandle input) + { + if (input.getCassandraType().getTypeArgumentSize() == 0) { + return input.getCassandraType(); + } + else { + return new CassandraTypeWithTypeArguments(input.getCassandraType(), input.getTypeArguments()); + } + } + }; + } + + public static Predicate partitionKeyPredicate() + { + return new Predicate() + { + @Override + public boolean apply(CassandraColumnHandle columnHandle) + { + return columnHandle.isPartitionKey(); + } + }; + } +} diff --git a/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraConnector.java b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraConnector.java new file mode 100644 index 00000000..fcc03e44 --- /dev/null +++ b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraConnector.java @@ -0,0 +1,98 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import com.facebook.presto.spi.Connector; +import com.facebook.presto.spi.ConnectorHandleResolver; +import com.facebook.presto.spi.ConnectorMetadata; +import com.facebook.presto.spi.ConnectorRecordSetProvider; +import com.facebook.presto.spi.ConnectorRecordSinkProvider; +import com.facebook.presto.spi.ConnectorSplitManager; +import io.airlift.bootstrap.LifeCycleManager; +import io.airlift.log.Logger; + +import javax.inject.Inject; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class CassandraConnector + implements Connector +{ + private static final Logger log = Logger.get(CassandraConnector.class); + + private final LifeCycleManager lifeCycleManager; + private final CassandraMetadata metadata; + private final CassandraSplitManager splitManager; + private final ConnectorRecordSetProvider recordSetProvider; + private final CassandraHandleResolver handleResolver; + private final CassandraConnectorRecordSinkProvider recordSinkProvider; + + @Inject + public CassandraConnector( + LifeCycleManager lifeCycleManager, + CassandraMetadata metadata, + CassandraSplitManager splitManager, + CassandraRecordSetProvider recordSetProvider, + CassandraHandleResolver handleResolver, + CassandraConnectorRecordSinkProvider recordSinkProvider) + { + this.lifeCycleManager = checkNotNull(lifeCycleManager, "lifeCycleManager is null"); + this.metadata = checkNotNull(metadata, "metadata is null"); + this.splitManager = checkNotNull(splitManager, "splitManager is null"); + this.recordSetProvider = checkNotNull(recordSetProvider, "recordSetProvider is null"); + this.handleResolver = checkNotNull(handleResolver, "handleResolver is null"); + this.recordSinkProvider = checkNotNull(recordSinkProvider, "recordSinkProvider is null"); + } + + @Override + public ConnectorMetadata getMetadata() + { + return metadata; + } + + @Override + public ConnectorSplitManager getSplitManager() + { + return splitManager; + } + + @Override + public ConnectorRecordSetProvider getRecordSetProvider() + { + return recordSetProvider; + } + + @Override + public ConnectorHandleResolver getHandleResolver() + { + return handleResolver; + } + + @Override + public ConnectorRecordSinkProvider getRecordSinkProvider() + { + return recordSinkProvider; + } + + @Override + public final void shutdown() + { + try { + lifeCycleManager.stop(); + } + catch (Exception e) { + log.error(e, "Error shutting down connector"); + } + } +} diff --git a/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraConnectorFactory.java b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraConnectorFactory.java new file mode 100644 index 00000000..77354469 --- /dev/null +++ b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraConnectorFactory.java @@ -0,0 +1,84 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import com.facebook.presto.spi.Connector; +import com.facebook.presto.spi.ConnectorFactory; +import com.google.common.base.Throwables; +import com.google.inject.Binder; +import com.google.inject.Injector; +import com.google.inject.Module; +import io.airlift.bootstrap.Bootstrap; +import io.airlift.json.JsonModule; +import org.weakref.jmx.guice.MBeanModule; + +import javax.management.MBeanServer; + +import java.lang.management.ManagementFactory; +import java.util.Map; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Strings.isNullOrEmpty; + +public class CassandraConnectorFactory + implements ConnectorFactory +{ + private final String name; + private final Map optionalConfig; + + public CassandraConnectorFactory(String name, Map optionalConfig) + { + checkArgument(!isNullOrEmpty(name), "name is null or empty"); + this.name = name; + this.optionalConfig = checkNotNull(optionalConfig, "optionalConfig is null"); + } + + @Override + public String getName() + { + return name; + } + + @Override + public Connector create(String connectorId, Map config) + { + checkNotNull(config, "config is null"); + + try { + Bootstrap app = new Bootstrap( + new MBeanModule(), + new JsonModule(), + new CassandraClientModule(connectorId), + new Module() + { + @Override + public void configure(Binder binder) + { + MBeanServer platformMBeanServer = ManagementFactory.getPlatformMBeanServer(); + binder.bind(MBeanServer.class).toInstance(new RebindSafeMBeanServer(platformMBeanServer)); + } + }); + + Injector injector = app.strictConfig().doNotInitializeLogging() + .setRequiredConfigurationProperties(config) + .setOptionalConfigurationProperties(optionalConfig).initialize(); + + return injector.getInstance(CassandraConnector.class); + } + catch (Exception e) { + throw Throwables.propagate(e); + } + } +} diff --git a/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraConnectorId.java b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraConnectorId.java new file mode 100644 index 00000000..2832a7a9 --- /dev/null +++ b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraConnectorId.java @@ -0,0 +1,56 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import java.util.Objects; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class CassandraConnectorId +{ + private final String connectorId; + + public CassandraConnectorId(String connectorId) + { + checkNotNull(connectorId, "connectorId is null"); + checkArgument(!connectorId.isEmpty(), "connectorId is empty"); + this.connectorId = connectorId; + } + + @Override + public int hashCode() + { + return Objects.hash(connectorId); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + CassandraConnectorId other = (CassandraConnectorId) obj; + return Objects.equals(this.connectorId, other.connectorId); + } + + @Override + public String toString() + { + return connectorId; + } +} diff --git a/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraConnectorRecordSinkProvider.java b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraConnectorRecordSinkProvider.java new file mode 100644 index 00000000..7674e27f --- /dev/null +++ b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraConnectorRecordSinkProvider.java @@ -0,0 +1,52 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import com.facebook.presto.spi.ConnectorInsertTableHandle; +import com.facebook.presto.spi.ConnectorOutputTableHandle; +import com.facebook.presto.spi.ConnectorRecordSinkProvider; +import com.facebook.presto.spi.RecordSink; + +import javax.inject.Inject; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class CassandraConnectorRecordSinkProvider + implements ConnectorRecordSinkProvider +{ + private final CassandraSession cassandraSession; + + @Inject + public CassandraConnectorRecordSinkProvider(CassandraSession cassandraSession) + { + this.cassandraSession = checkNotNull(cassandraSession, "cassandraSession is null"); + } + + @Override + public RecordSink getRecordSink(ConnectorOutputTableHandle tableHandle) + { + checkNotNull(tableHandle, "tableHandle is null"); + checkArgument(tableHandle instanceof CassandraOutputTableHandle, "tableHandle is not an instance of CassandraOutputTableHandle"); + CassandraOutputTableHandle handle = (CassandraOutputTableHandle) tableHandle; + + return new CassandraRecordSink(handle, cassandraSession); + } + + @Override + public RecordSink getRecordSink(ConnectorInsertTableHandle tableHandle) + { + throw new UnsupportedOperationException(); + } +} diff --git a/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraHandleResolver.java b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraHandleResolver.java new file mode 100644 index 00000000..3b019ffe --- /dev/null +++ b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraHandleResolver.java @@ -0,0 +1,93 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorHandleResolver; +import com.facebook.presto.spi.ConnectorOutputTableHandle; +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.ConnectorTableHandle; + +import javax.inject.Inject; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkNotNull; + +public class CassandraHandleResolver + implements ConnectorHandleResolver +{ + private final String connectorId; + + @Inject + public CassandraHandleResolver(CassandraConnectorId connectorId) + { + this.connectorId = checkNotNull(connectorId, "connectorId is null").toString(); + } + + @Override + public boolean canHandle(ConnectorTableHandle tableHandle) + { + return tableHandle instanceof CassandraTableHandle && ((CassandraTableHandle) tableHandle).getConnectorId().equals(connectorId); + } + + @Override + public boolean canHandle(ColumnHandle columnHandle) + { + return columnHandle instanceof CassandraColumnHandle && ((CassandraColumnHandle) columnHandle).getConnectorId().equals(connectorId); + } + + @Override + public boolean canHandle(ConnectorSplit split) + { + return split instanceof CassandraSplit && ((CassandraSplit) split).getConnectorId().equals(connectorId); + } + + @Override + public boolean canHandle(ConnectorOutputTableHandle tableHandle) + { + return (tableHandle instanceof CassandraOutputTableHandle) && ((CassandraOutputTableHandle) tableHandle).getConnectorId().equals(connectorId); + } + + @Override + public Class getTableHandleClass() + { + return CassandraTableHandle.class; + } + + @Override + public Class getColumnHandleClass() + { + return CassandraColumnHandle.class; + } + + @Override + public Class getSplitClass() + { + return CassandraSplit.class; + } + + @Override + public Class getOutputTableHandleClass() + { + return CassandraOutputTableHandle.class; + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("connectorId", connectorId) + .toString(); + } +} diff --git a/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraMetadata.java b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraMetadata.java new file mode 100644 index 00000000..21dabe36 --- /dev/null +++ b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraMetadata.java @@ -0,0 +1,342 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import com.facebook.presto.cassandra.util.CassandraCqlUtils; +import com.facebook.presto.spi.ColumnMetadata; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorInsertTableHandle; +import com.facebook.presto.spi.ConnectorMetadata; +import com.facebook.presto.spi.ConnectorOutputTableHandle; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.ConnectorTableHandle; +import com.facebook.presto.spi.ConnectorTableMetadata; +import com.facebook.presto.spi.InsertOption; +import com.facebook.presto.spi.NotFoundException; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.SchemaNotFoundException; +import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.spi.SchemaTablePrefix; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.airlift.json.JsonCodec; +import io.airlift.slice.Slice; + +import javax.inject.Inject; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static com.facebook.presto.cassandra.CassandraColumnHandle.SAMPLE_WEIGHT_COLUMN_NAME; +import static com.facebook.presto.cassandra.CassandraColumnHandle.columnMetadataGetter; +import static com.facebook.presto.cassandra.CassandraType.BIGINT; +import static com.facebook.presto.cassandra.CassandraType.toCassandraType; +import static com.facebook.presto.cassandra.util.Types.checkType; +import static com.facebook.presto.spi.StandardErrorCode.NOT_SUPPORTED; +import static com.facebook.presto.spi.StandardErrorCode.PERMISSION_DENIED; +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Strings.isNullOrEmpty; +import static com.google.common.collect.Iterables.transform; +import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; +import static java.util.Locale.ENGLISH; + +public class CassandraMetadata + implements ConnectorMetadata +{ + private final String connectorId; + private final CachingCassandraSchemaProvider schemaProvider; + private final CassandraSession cassandraSession; + private final boolean allowDropTable; + + private final JsonCodec> extraColumnMetadataCodec; + + @Inject + public CassandraMetadata(CassandraConnectorId connectorId, + CachingCassandraSchemaProvider schemaProvider, + CassandraSession cassandraSession, + JsonCodec> extraColumnMetadataCodec, + CassandraClientConfig config) + { + this.connectorId = checkNotNull(connectorId, "connectorId is null").toString(); + this.schemaProvider = checkNotNull(schemaProvider, "schemaProvider is null"); + this.cassandraSession = checkNotNull(cassandraSession, "cassandraSession is null"); + this.allowDropTable = checkNotNull(config, "config is null").getAllowDropTable(); + this.extraColumnMetadataCodec = checkNotNull(extraColumnMetadataCodec, "extraColumnMetadataCodec is null"); + } + + @Override + public List listSchemaNames(ConnectorSession session) + { + return schemaProvider.getAllSchemas(); + } + + @Override + public CassandraTableHandle getTableHandle(ConnectorSession session, SchemaTableName tableName) + { + checkNotNull(tableName, "tableName is null"); + try { + CassandraTableHandle tableHandle = schemaProvider.getTableHandle(tableName); + schemaProvider.getTable(tableHandle); + return tableHandle; + } + catch (NotFoundException e) { + // table was not found + return null; + } + } + + private static SchemaTableName getTableName(ConnectorTableHandle tableHandle) + { + return checkType(tableHandle, CassandraTableHandle.class, "tableHandle").getSchemaTableName(); + } + + @Override + public ConnectorTableMetadata getTableMetadata(ConnectorTableHandle tableHandle) + { + checkNotNull(tableHandle, "tableHandle is null"); + SchemaTableName tableName = getTableName(tableHandle); + return getTableMetadata(tableName); + } + + private ConnectorTableMetadata getTableMetadata(SchemaTableName tableName) + { + CassandraTableHandle tableHandle = schemaProvider.getTableHandle(tableName); + List columns = ImmutableList.copyOf(transform(getColumnHandles(tableHandle).values(), columnMetadataGetter())); + return new ConnectorTableMetadata(tableName, columns); + } + + @Override + public List listTables(ConnectorSession session, String schemaNameOrNull) + { + ImmutableList.Builder tableNames = ImmutableList.builder(); + for (String schemaName : listSchemas(session, schemaNameOrNull)) { + try { + for (String tableName : schemaProvider.getAllTables(schemaName)) { + tableNames.add(new SchemaTableName(schemaName, tableName.toLowerCase(ENGLISH))); + } + } + catch (SchemaNotFoundException e) { + // schema disappeared during listing operation + } + } + return tableNames.build(); + } + + private List listSchemas(ConnectorSession session, String schemaNameOrNull) + { + if (schemaNameOrNull == null) { + return listSchemaNames(session); + } + return ImmutableList.of(schemaNameOrNull); + } + + @Override + public ColumnHandle getSampleWeightColumnHandle(ConnectorTableHandle tableHandle) + { + return getColumnHandles(tableHandle, true).get(SAMPLE_WEIGHT_COLUMN_NAME); + } + + @Override + public Map getColumnHandles(ConnectorTableHandle tableHandle) + { + return getColumnHandles(tableHandle, false); + } + + private Map getColumnHandles(ConnectorTableHandle tableHandle, boolean includeSampleWeight) + { + CassandraTable table = schemaProvider.getTable((CassandraTableHandle) tableHandle); + ImmutableMap.Builder columnHandles = ImmutableMap.builder(); + for (CassandraColumnHandle columnHandle : table.getColumns()) { + if (includeSampleWeight || !columnHandle.getName().equals(SAMPLE_WEIGHT_COLUMN_NAME)) { + columnHandles.put(CassandraCqlUtils.cqlNameToSqlName(columnHandle.getName()).toLowerCase(ENGLISH), columnHandle); + } + } + return columnHandles.build(); + } + + @Override + public Map> listTableColumns(ConnectorSession session, SchemaTablePrefix prefix) + { + checkNotNull(prefix, "prefix is null"); + ImmutableMap.Builder> columns = ImmutableMap.builder(); + for (SchemaTableName tableName : listTables(session, prefix)) { + try { + columns.put(tableName, getTableMetadata(tableName).getColumns()); + } + catch (NotFoundException e) { + // table disappeared during listing operation + } + } + return columns.build(); + } + + private List listTables(ConnectorSession session, SchemaTablePrefix prefix) + { + if (prefix.getSchemaName() == null) { + return listTables(session, prefix.getSchemaName()); + } + return ImmutableList.of(new SchemaTableName(prefix.getSchemaName(), prefix.getTableName())); + } + + @Override + public ColumnMetadata getColumnMetadata(ConnectorTableHandle tableHandle, ColumnHandle columnHandle) + { + checkType(tableHandle, CassandraTableHandle.class, "tableHandle"); + return checkType(columnHandle, CassandraColumnHandle.class, "columnHandle").getColumnMetadata(); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("connectorId", connectorId) + .toString(); + } + + @Override + public boolean canCreateSampledTables(ConnectorSession session) + { + return true; + } + + @Override + public void createTable(ConnectorSession session, ConnectorTableMetadata tableMetadata) + { + throw new PrestoException(NOT_SUPPORTED, "CREATE TABLE not yet supported for Cassandra"); + } + + @Override + public void dropTable(ConnectorTableHandle tableHandle) + { + checkArgument(tableHandle instanceof CassandraTableHandle, "tableHandle is not an instance of CassandraTableHandle"); + + if (!allowDropTable) { + throw new PrestoException(PERMISSION_DENIED, "DROP TABLE is disabled in this Cassandra catalog"); + } + + CassandraTableHandle cassandraTableHandle = (CassandraTableHandle) tableHandle; + String schemaName = cassandraTableHandle.getSchemaName(); + String tableName = cassandraTableHandle.getTableName(); + + StringBuilder queryBuilder = new StringBuilder(String.format("DROP TABLE \"%s\".\"%s\"", schemaName, tableName)); + cassandraSession.executeQuery(schemaName, queryBuilder.toString()); + schemaProvider.flushTable(cassandraTableHandle.getSchemaTableName()); + } + + @Override + public void renameTable(ConnectorTableHandle tableHandle, SchemaTableName newTableName) + { + throw new PrestoException(NOT_SUPPORTED, "Renaming tables not yet supported for Cassandra"); + } + + @Override + public ConnectorOutputTableHandle beginCreateTable(ConnectorSession session, ConnectorTableMetadata tableMetadata) + { + checkArgument(!isNullOrEmpty(tableMetadata.getOwner()), "Table owner is null or empty"); + + ImmutableList.Builder columnNames = ImmutableList.builder(); + ImmutableList.Builder columnTypes = ImmutableList.builder(); + ImmutableList.Builder columnExtra = ImmutableList.builder(); + columnExtra.add(new ExtraColumnMetadata("id", true)); + for (ColumnMetadata column : tableMetadata.getColumns()) { + columnNames.add(column.getName()); + columnTypes.add(column.getType()); + columnExtra.add(new ExtraColumnMetadata(column.getName(), column.isHidden())); + } + + // get the root directory for the database + SchemaTableName table = tableMetadata.getTable(); + String schemaName = schemaProvider.getCaseSensitiveSchemaName(table.getSchemaName()); + String tableName = table.getTableName(); + List columns = columnNames.build(); + List types = columnTypes.build(); + StringBuilder queryBuilder = new StringBuilder(String.format("CREATE TABLE \"%s\".\"%s\"(id uuid primary key", schemaName, tableName)); + if (tableMetadata.isSampled()) { + queryBuilder.append(", ").append(SAMPLE_WEIGHT_COLUMN_NAME).append(" ").append(BIGINT.name().toLowerCase(ENGLISH)); + columnExtra.add(new ExtraColumnMetadata(SAMPLE_WEIGHT_COLUMN_NAME, true)); + } + for (int i = 0; i < columns.size(); i++) { + String name = columns.get(i); + Type type = types.get(i); + queryBuilder.append(", ") + .append(name) + .append(" ") + .append(toCassandraType(type).name().toLowerCase(ENGLISH)); + } + queryBuilder.append(") "); + + // encode column ordering in the cassandra table comment field since there is no better place to store this + String columnMetadata = extraColumnMetadataCodec.toJson(columnExtra.build()); + queryBuilder.append("WITH comment='").append(CassandraSession.PRESTO_COMMENT_METADATA).append(" ").append(columnMetadata).append("'"); + + // We need create Cassandra table before commit because record need to be written to the table . + cassandraSession.executeQuery(schemaName, queryBuilder.toString()); + return new CassandraOutputTableHandle( + connectorId, + schemaName, + tableName, + columnNames.build(), + columnTypes.build(), + tableMetadata.isSampled(), + tableMetadata.getOwner()); + } + + @Override + public void commitCreateTable(ConnectorOutputTableHandle tableHandle, Collection fragments) + { + CassandraOutputTableHandle outputTableHandle = checkType(tableHandle, CassandraOutputTableHandle.class, "tableHandle"); + schemaProvider.flushTable(new SchemaTableName(outputTableHandle.getSchemaName(), outputTableHandle.getTableName())); + } + + @Override + public ConnectorInsertTableHandle beginInsert(ConnectorSession session, ConnectorTableHandle tableHandle, InsertOption insertOption) + { + throw new PrestoException(NOT_SUPPORTED, "INSERT not yet supported for Cassandra"); + } + + @Override + public void commitInsert(ConnectorInsertTableHandle insertHandle, Collection fragments) + { + throw new UnsupportedOperationException(); + } + + @Override + public void createView(ConnectorSession session, SchemaTableName viewName, String viewData, boolean replace) + { + throw new PrestoException(NOT_SUPPORTED, "CREATE VIEW not yet supported for Cassandra"); + } + + @Override + public void dropView(ConnectorSession session, SchemaTableName viewName) + { + throw new PrestoException(NOT_SUPPORTED, "DROP VIEW not yet supported for Cassandra"); + } + + @Override + public List listViews(ConnectorSession session, String schemaNameOrNull) + { + return emptyList(); + } + + @Override + public Map getViews(ConnectorSession session, SchemaTablePrefix prefix) + { + return emptyMap(); + } +} diff --git a/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraOutputTableHandle.java b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraOutputTableHandle.java new file mode 100644 index 00000000..ce43f1c3 --- /dev/null +++ b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraOutputTableHandle.java @@ -0,0 +1,108 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import com.facebook.presto.spi.ConnectorOutputTableHandle; +import com.facebook.presto.spi.type.Type; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class CassandraOutputTableHandle + implements ConnectorOutputTableHandle +{ + private final String connectorId; + private final String schemaName; + private final String tableName; + private final List columnNames; + private final List columnTypes; + private final boolean sampled; + private final String tableOwner; + + @JsonCreator + public CassandraOutputTableHandle( + @JsonProperty("connectorId") String connectorId, + @JsonProperty("schemaName") String schemaName, + @JsonProperty("tableName") String tableName, + @JsonProperty("columnNames") List columnNames, + @JsonProperty("columnTypes") List columnTypes, + @JsonProperty("sampled") boolean sampled, + @JsonProperty("tableOwner") String tableOwner) + { + this.sampled = sampled; + this.connectorId = checkNotNull(connectorId, "clientId is null"); + this.schemaName = checkNotNull(schemaName, "schemaName is null"); + this.tableName = checkNotNull(tableName, "tableName is null"); + this.tableOwner = checkNotNull(tableOwner, "tableOwner is null"); + + checkNotNull(columnNames, "columnNames is null"); + checkNotNull(columnTypes, "columnTypes is null"); + checkArgument(columnNames.size() == columnTypes.size(), "columnNames and columnTypes sizes don't match"); + this.columnNames = ImmutableList.copyOf(columnNames); + this.columnTypes = ImmutableList.copyOf(columnTypes); + } + + @JsonProperty + public String getConnectorId() + { + return connectorId; + } + + @JsonProperty + public String getSchemaName() + { + return schemaName; + } + + @JsonProperty + public String getTableName() + { + return tableName; + } + + @JsonProperty + public List getColumnNames() + { + return columnNames; + } + + @JsonProperty + public List getColumnTypes() + { + return columnTypes; + } + + @JsonProperty + public boolean isSampled() + { + return sampled; + } + + @JsonProperty + public String getTableOwner() + { + return tableOwner; + } + + @Override + public String toString() + { + return "cassandra:" + schemaName + "." + tableName; + } +} diff --git a/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraPartition.java b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraPartition.java new file mode 100644 index 00000000..9292e1f1 --- /dev/null +++ b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraPartition.java @@ -0,0 +1,86 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorPartition; +import com.facebook.presto.spi.TupleDomain; + +import java.nio.ByteBuffer; + +public class CassandraPartition + implements ConnectorPartition +{ + static final String UNPARTITIONED_ID = ""; + public static final CassandraPartition UNPARTITIONED = new CassandraPartition(); + + private final String partitionId; + private final byte[] key; + private final TupleDomain tupleDomain; + private final boolean indexedColumnPredicatePushdown; + + private CassandraPartition() + { + partitionId = UNPARTITIONED_ID; + tupleDomain = TupleDomain.all(); + key = null; + indexedColumnPredicatePushdown = false; + } + + public CassandraPartition(byte[] key, String partitionId, TupleDomain tupleDomain, boolean indexedColumnPredicatePushdown) + { + this.key = key; + this.partitionId = partitionId; + this.tupleDomain = tupleDomain; + this.indexedColumnPredicatePushdown = indexedColumnPredicatePushdown; + } + + public boolean isUnpartitioned() + { + return partitionId.equals(UNPARTITIONED_ID); + } + + public boolean isIndexedColumnPredicatePushdown() + { + return indexedColumnPredicatePushdown; + } + + @Override + public TupleDomain getTupleDomain() + { + return tupleDomain; + } + + @Override + public String getPartitionId() + { + return partitionId; + } + + @Override + public String toString() + { + return partitionId; + } + + public ByteBuffer getKeyAsByteBuffer() + { + return ByteBuffer.wrap(key); + } + + public byte[] getKey() + { + return key; + } +} diff --git a/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraPlugin.java b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraPlugin.java new file mode 100644 index 00000000..c803bca4 --- /dev/null +++ b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraPlugin.java @@ -0,0 +1,45 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import com.facebook.presto.spi.ConnectorFactory; +import com.facebook.presto.spi.Plugin; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +import java.util.List; +import java.util.Map; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class CassandraPlugin + implements Plugin +{ + private Map optionalConfig = ImmutableMap.of(); + + @Override + public void setOptionalConfig(Map optionalConfig) + { + this.optionalConfig = ImmutableMap.copyOf(checkNotNull(optionalConfig, "optionalConfig is null")); + } + + @Override + public List getServices(Class type) + { + if (type == ConnectorFactory.class) { + return ImmutableList.of(type.cast(new CassandraConnectorFactory("cassandra", optionalConfig))); + } + return ImmutableList.of(); + } +} diff --git a/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraRecordCursor.java b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraRecordCursor.java new file mode 100644 index 00000000..f0f1afa5 --- /dev/null +++ b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraRecordCursor.java @@ -0,0 +1,141 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import com.datastax.driver.core.ResultSet; +import com.datastax.driver.core.Row; +import com.facebook.presto.spi.RecordCursor; +import com.facebook.presto.spi.type.Type; +import io.airlift.slice.Slice; + +import java.util.List; + +import static io.airlift.slice.Slices.utf8Slice; + +public class CassandraRecordCursor + implements RecordCursor +{ + private final List fullCassandraTypes; + private final ResultSet rs; + private Row currentRow; + private long atLeastCount; + private long count; + + public CassandraRecordCursor( + CassandraSession cassandraSession, + String schema, + List fullCassandraTypes, + String cql) + { + this.fullCassandraTypes = fullCassandraTypes; + rs = cassandraSession.executeQuery(schema, cql); + currentRow = null; + atLeastCount = rs.getAvailableWithoutFetching(); + } + + @Override + public boolean advanceNextPosition() + { + if (!rs.isExhausted()) { + currentRow = rs.one(); + count++; + atLeastCount = count + rs.getAvailableWithoutFetching(); + return true; + } + return false; + } + + @Override + public void close() + { + } + + @Override + public boolean getBoolean(int i) + { + return currentRow.getBool(i); + } + + @Override + public long getCompletedBytes() + { + return count; + } + + @Override + public long getReadTimeNanos() + { + return 0; + } + + @Override + public double getDouble(int i) + { + switch (getCassandraType(i)) { + case DOUBLE: + return currentRow.getDouble(i); + case FLOAT: + return currentRow.getFloat(i); + case DECIMAL: + return currentRow.getDecimal(i).doubleValue(); + default: + throw new IllegalStateException("Cannot retrieve double for " + getCassandraType(i)); + } + } + + @Override + public long getLong(int i) + { + switch (getCassandraType(i)) { + case INT: + return currentRow.getInt(i); + case BIGINT: + case COUNTER: + return currentRow.getLong(i); + case TIMESTAMP: + return currentRow.getDate(i).getTime(); + default: + throw new IllegalStateException("Cannot retrieve long for " + getCassandraType(i)); + } + } + + private CassandraType getCassandraType(int i) + { + return fullCassandraTypes.get(i).getCassandraType(); + } + + @Override + public Slice getSlice(int i) + { + return utf8Slice(CassandraType.getColumnValue(currentRow, i, fullCassandraTypes.get(i)).toString()); + } + + @Override + public long getTotalBytes() + { + return atLeastCount; + } + + @Override + public Type getType(int i) + { + return getCassandraType(i).getNativeType(); + } + + @Override + public boolean isNull(int i) + { + return currentRow.isNull(i); + } +} diff --git a/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraRecordSet.java b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraRecordSet.java new file mode 100644 index 00000000..5daf0052 --- /dev/null +++ b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraRecordSet.java @@ -0,0 +1,57 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import com.facebook.presto.spi.RecordCursor; +import com.facebook.presto.spi.RecordSet; +import com.facebook.presto.spi.type.Type; + +import java.util.List; + +import static com.facebook.presto.cassandra.CassandraColumnHandle.cassandraFullTypeGetter; +import static com.facebook.presto.cassandra.CassandraColumnHandle.nativeTypeGetter; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.Lists.transform; + +public class CassandraRecordSet + implements RecordSet +{ + private final CassandraSession cassandraSession; + private final String schema; + private final String cql; + private final List cassandraTypes; + private final List columnTypes; + + public CassandraRecordSet(CassandraSession cassandraSession, String schema, String cql, List cassandraColumns) + { + this.cassandraSession = checkNotNull(cassandraSession, "cassandraSession is null"); + this.schema = checkNotNull(schema, "schema is null"); + this.cql = checkNotNull(cql, "cql is null"); + checkNotNull(cassandraColumns, "cassandraColumns is null"); + this.cassandraTypes = transform(cassandraColumns, cassandraFullTypeGetter()); + this.columnTypes = transform(cassandraColumns, nativeTypeGetter()); + } + + @Override + public List getColumnTypes() + { + return columnTypes; + } + + @Override + public RecordCursor cursor() + { + return new CassandraRecordCursor(cassandraSession, schema, cassandraTypes, cql); + } +} diff --git a/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraRecordSetProvider.java b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraRecordSetProvider.java new file mode 100644 index 00000000..2d097199 --- /dev/null +++ b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraRecordSetProvider.java @@ -0,0 +1,75 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import com.facebook.presto.cassandra.util.CassandraCqlUtils; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorRecordSetProvider; +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.RecordSet; +import com.google.common.collect.ImmutableList; +import io.airlift.log.Logger; + +import javax.inject.Inject; + +import java.util.List; + +import static com.facebook.presto.cassandra.util.Types.checkType; +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.Iterables.transform; + +public class CassandraRecordSetProvider + implements ConnectorRecordSetProvider +{ + private static final Logger log = Logger.get(ConnectorRecordSetProvider.class); + + private final String connectorId; + private final CassandraSession cassandraSession; + + @Inject + public CassandraRecordSetProvider(CassandraConnectorId connectorId, CassandraSession cassandraSession) + { + this.connectorId = checkNotNull(connectorId, "connectorId is null").toString(); + this.cassandraSession = checkNotNull(cassandraSession, "cassandraSession is null"); + } + + @Override + public RecordSet getRecordSet(ConnectorSplit split, List columns) + { + CassandraSplit cassandraSplit = checkType(split, CassandraSplit.class, "split"); + + checkNotNull(columns, "columns is null"); + List cassandraColumns = ImmutableList.copyOf(transform(columns, CassandraColumnHandle.cassandraColumnHandle())); + + String selectCql = CassandraCqlUtils.selectFrom(cassandraSplit.getCassandraTableHandle(), cassandraColumns).getQueryString(); + StringBuilder sb = new StringBuilder(selectCql); + if (sb.charAt(sb.length() - 1) == ';') { + sb.setLength(sb.length() - 1); + } + sb.append(cassandraSplit.getWhereClause()); + String cql = sb.toString(); + log.debug("Creating record set: %s", cql); + + return new CassandraRecordSet(cassandraSession, cassandraSplit.getSchema(), cql, cassandraColumns); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("connectorId", connectorId) + .toString(); + } +} diff --git a/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraRecordSink.java b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraRecordSink.java new file mode 100644 index 00000000..57bfc38a --- /dev/null +++ b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraRecordSink.java @@ -0,0 +1,172 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import com.facebook.presto.spi.RecordSink; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import io.airlift.slice.Slice; +import org.joda.time.format.DateTimeFormatter; +import org.joda.time.format.ISODateTimeFormat; + +import javax.inject.Inject; + +import java.util.Collection; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import static com.facebook.presto.cassandra.CassandraColumnHandle.SAMPLE_WEIGHT_COLUMN_NAME; +import static com.facebook.presto.spi.type.DateType.DATE; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static java.nio.charset.StandardCharsets.UTF_8; + +public class CassandraRecordSink + implements RecordSink +{ + private static final DateTimeFormatter DATE_FORMATTER = ISODateTimeFormat.date().withZoneUTC(); + + private final int fieldCount; + private final CassandraSession cassandraSession; + private final boolean sampled; + private final String insertQuery; + private final List values; + private final String schemaName; + private final List columnTypes; + private int field = -1; + + @Inject + public CassandraRecordSink(CassandraOutputTableHandle handle, CassandraSession cassandraSession) + { + this.fieldCount = checkNotNull(handle, "handle is null").getColumnNames().size(); + this.cassandraSession = checkNotNull(cassandraSession, "cassandraSession is null"); + this.sampled = handle.isSampled(); + + schemaName = handle.getSchemaName(); + StringBuilder queryBuilder = new StringBuilder(String.format("INSERT INTO \"%s\".\"%s\"(", schemaName, handle.getTableName())); + queryBuilder.append("id"); + + if (sampled) { + queryBuilder.append("," + SAMPLE_WEIGHT_COLUMN_NAME); + } + + for (String columnName : handle.getColumnNames()) { + queryBuilder.append(",").append(columnName); + } + queryBuilder.append(") VALUES (?"); + + if (sampled) { + queryBuilder.append(",?"); + } + + for (int i = 0; i < handle.getColumnNames().size(); i++) { + queryBuilder.append(",?"); + } + queryBuilder.append(")"); + + insertQuery = queryBuilder.toString(); + values = Lists.newArrayList(); + + columnTypes = handle.getColumnTypes(); + } + + @Override + public void beginRecord(long sampleWeight) + { + checkState(field == -1, "already in record"); + + field = 0; + values.clear(); + values.add(UUID.randomUUID()); + + if (sampled) { + values.add(sampleWeight); + } + else { + checkArgument(sampleWeight == 1, "Sample weight must be 1 when sampling is disabled"); + } + } + + @Override + public void finishRecord() + { + checkState(field != -1, "not in record"); + checkState(field == fieldCount, "not all fields set"); + field = -1; + cassandraSession.execute(schemaName, insertQuery, values.toArray()); + } + + @Override + public void appendNull() + { + append(null); + } + + @Override + public void appendBoolean(boolean value) + { + append(value); + } + + @Override + public void appendLong(long value) + { + if (DATE.equals(columnTypes.get(field))) { + append(DATE_FORMATTER.print(TimeUnit.DAYS.toMillis(value))); + } + else { + append(value); + } + } + + @Override + public void appendDouble(double value) + { + append(value); + } + + @Override + public void appendString(byte[] value) + { + append(new String(value, UTF_8)); + } + + @Override + public Collection commit() + { + checkState(field == -1, "record not finished"); + // the committer does not need any additional info + return ImmutableList.of(); + } + + @Override + public void rollback() {} + + @Override + public List getColumnTypes() + { + return columnTypes; + } + + private void append(Object value) + { + checkState(field != -1, "not in record"); + checkState(field < fieldCount, "all fields already set"); + values.add(value); + field++; + } +} diff --git a/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraSession.java b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraSession.java new file mode 100644 index 00000000..6e2be368 --- /dev/null +++ b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraSession.java @@ -0,0 +1,464 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import com.datastax.driver.core.Cluster.Builder; +import com.datastax.driver.core.ColumnMetadata; +import com.datastax.driver.core.DataType; +import com.datastax.driver.core.Host; +import com.datastax.driver.core.KeyspaceMetadata; +import com.datastax.driver.core.ResultSet; +import com.datastax.driver.core.ResultSetFuture; +import com.datastax.driver.core.Row; +import com.datastax.driver.core.Session; +import com.datastax.driver.core.TableMetadata; +import com.datastax.driver.core.exceptions.NoHostAvailableException; +import com.datastax.driver.core.querybuilder.Clause; +import com.datastax.driver.core.querybuilder.QueryBuilder; +import com.datastax.driver.core.querybuilder.Select; +import com.facebook.presto.cassandra.util.CassandraCqlUtils; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.SchemaNotFoundException; +import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.spi.TableNotFoundException; +import com.facebook.presto.spi.TupleDomain; +import com.google.common.base.Function; +import com.google.common.base.Throwables; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.collect.Ordering; +import com.google.common.util.concurrent.UncheckedExecutionException; +import io.airlift.json.JsonCodec; + +import javax.annotation.Nullable; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ExecutionException; + +import static com.datastax.driver.core.querybuilder.Select.Where; +import static com.facebook.presto.cassandra.ExtraColumnMetadata.hiddenPredicate; +import static com.facebook.presto.cassandra.ExtraColumnMetadata.nameGetter; +import static com.google.common.base.Predicates.in; +import static com.google.common.base.Predicates.not; +import static com.google.common.collect.Iterables.filter; +import static com.google.common.collect.Iterables.transform; + +public class CassandraSession +{ + static final String PRESTO_COMMENT_METADATA = "Presto Metadata:"; + protected final String connectorId; + private final int fetchSizeForPartitionKeySelect; + private final int limitForPartitionKeySelect; + private final JsonCodec> extraColumnMetadataCodec; + + private LoadingCache sessionBySchema; + + public CassandraSession(String connectorId, + final Builder clusterBuilder, + int fetchSizeForPartitionKeySelect, + int limitForPartitionKeySelect, + JsonCodec> extraColumnMetadataCodec) + { + this.connectorId = connectorId; + this.fetchSizeForPartitionKeySelect = fetchSizeForPartitionKeySelect; + this.limitForPartitionKeySelect = limitForPartitionKeySelect; + this.extraColumnMetadataCodec = extraColumnMetadataCodec; + + sessionBySchema = CacheBuilder.newBuilder() + .build(new CacheLoader() + { + @Override + public Session load(String key) + throws Exception + { + return clusterBuilder.build().connect(); + } + }); + } + + public Set getReplicas(final String schemaName, final ByteBuffer partitionKey) + { + return executeWithSession(schemaName, new SessionCallable>() + { + @Override + public Set executeWithSession(Session session) + { + return session.getCluster().getMetadata().getReplicas(schemaName, partitionKey); + } + }); + } + + private Session getSession(String schemaName) + { + try { + return sessionBySchema.get(schemaName); + } + catch (ExecutionException | UncheckedExecutionException e) { + throw Throwables.propagate(e.getCause()); + } + } + + public ResultSet executeQuery(String schemaName, final String cql) + { + return executeWithSession(schemaName, new SessionCallable() { + @Override + public ResultSet executeWithSession(Session session) + { + return session.execute(cql); + } + }); + } + + public ResultSet execute(String schemaName, final String cql, final Object... values) + { + return executeWithSession(schemaName, new SessionCallable() + { + @Override + public ResultSet executeWithSession(Session session) + { + return session.execute(cql, values); + } + }); + } + + public Collection getAllHosts() + { + return executeWithSession("", new SessionCallable>() { + @Override + public Collection executeWithSession(Session session) + { + return session.getCluster().getMetadata().getAllHosts(); + } + }); + } + + public List getAllSchemas() + { + ImmutableList.Builder builder = ImmutableList.builder(); + List keyspaces = executeWithSession("", new SessionCallable>() { + @Override + public List executeWithSession(Session session) + { + return session.getCluster().getMetadata().getKeyspaces(); + } + }); + for (KeyspaceMetadata meta : keyspaces) { + builder.add(meta.getName()); + } + return builder.build(); + } + + public List getAllTables(String schema) + throws SchemaNotFoundException + { + KeyspaceMetadata meta = getCheckedKeyspaceMetadata(schema); + ImmutableList.Builder builder = ImmutableList.builder(); + for (TableMetadata tableMeta : meta.getTables()) { + builder.add(tableMeta.getName()); + } + return builder.build(); + } + + private KeyspaceMetadata getCheckedKeyspaceMetadata(final String schema) + throws SchemaNotFoundException + { + KeyspaceMetadata keyspaceMetadata = executeWithSession(schema, new SessionCallable() { + @Override + public KeyspaceMetadata executeWithSession(Session session) + { + return session.getCluster().getMetadata().getKeyspace(schema); + } + }); + if (keyspaceMetadata == null) { + throw new SchemaNotFoundException(schema); + } + return keyspaceMetadata; + } + + public void getSchema(String schema) + throws SchemaNotFoundException + { + getCheckedKeyspaceMetadata(schema); + } + + public CassandraTable getTable(SchemaTableName tableName) + throws TableNotFoundException + { + TableMetadata tableMeta = getTableMetadata(tableName); + + List columnNames = new ArrayList<>(); + for (ColumnMetadata columnMetadata : tableMeta.getColumns()) { + columnNames.add(columnMetadata.getName()); + } + + // check if there is a comment to establish column ordering + String comment = tableMeta.getOptions().getComment(); + Set hiddenColumns = ImmutableSet.of(); + if (comment != null && comment.startsWith(PRESTO_COMMENT_METADATA)) { + String columnOrderingString = comment.substring(PRESTO_COMMENT_METADATA.length()); + + // column ordering + List extras = extraColumnMetadataCodec.fromJson(columnOrderingString); + List explicitColumnOrder = new ArrayList<>(ImmutableList.copyOf(transform(extras, nameGetter()))); + hiddenColumns = ImmutableSet.copyOf(transform(filter(extras, hiddenPredicate()), nameGetter())); + + // add columns not in the comment to the ordering + Iterables.addAll(explicitColumnOrder, filter(columnNames, not(in(explicitColumnOrder)))); + + // sort the actual columns names using the explicit column order (this allows for missing columns) + columnNames = Ordering.explicit(explicitColumnOrder).sortedCopy(columnNames); + } + + ImmutableList.Builder columnHandles = ImmutableList.builder(); + + // add primary keys first + Set primaryKeySet = new HashSet<>(); + for (ColumnMetadata columnMeta : tableMeta.getPartitionKey()) { + primaryKeySet.add(columnMeta.getName()); + boolean hidden = hiddenColumns.contains(columnMeta.getName()); + CassandraColumnHandle columnHandle = buildColumnHandle(columnMeta, true, false, columnNames.indexOf(columnMeta.getName()), hidden); + columnHandles.add(columnHandle); + } + + // add clustering columns + for (ColumnMetadata columnMeta : tableMeta.getClusteringColumns()) { + primaryKeySet.add(columnMeta.getName()); + boolean hidden = hiddenColumns.contains(columnMeta.getName()); + CassandraColumnHandle columnHandle = buildColumnHandle(columnMeta, false, true, columnNames.indexOf(columnMeta.getName()), hidden); + columnHandles.add(columnHandle); + } + + // add other columns + for (ColumnMetadata columnMeta : tableMeta.getColumns()) { + if (!primaryKeySet.contains(columnMeta.getName())) { + boolean hidden = hiddenColumns.contains(columnMeta.getName()); + CassandraColumnHandle columnHandle = buildColumnHandle(columnMeta, false, false, columnNames.indexOf(columnMeta.getName()), hidden); + columnHandles.add(columnHandle); + } + } + + List sortedColumnHandles = Ordering.natural().onResultOf(new Function() + { + @Nullable + @Override + public Integer apply(CassandraColumnHandle columnHandle) + { + return columnHandle.getOrdinalPosition(); + } + }).sortedCopy(columnHandles.build()); + + CassandraTableHandle tableHandle = new CassandraTableHandle(connectorId, tableMeta.getKeyspace().getName(), tableMeta.getName()); + return new CassandraTable(tableHandle, sortedColumnHandles); + } + + private TableMetadata getTableMetadata(SchemaTableName schemaTableName) + { + String schemaName = schemaTableName.getSchemaName(); + String tableName = schemaTableName.getTableName(); + + KeyspaceMetadata keyspaceMetadata = getCheckedKeyspaceMetadata(schemaName); + TableMetadata tableMetadata = keyspaceMetadata.getTable(tableName); + if (tableMetadata != null) { + return tableMetadata; + } + + for (TableMetadata table : keyspaceMetadata.getTables()) { + if (table.getName().equalsIgnoreCase(tableName)) { + return table; + } + } + throw new TableNotFoundException(schemaTableName); + } + + private CassandraColumnHandle buildColumnHandle(ColumnMetadata columnMeta, boolean partitionKey, boolean clusteringKey, int ordinalPosition, boolean hidden) + { + CassandraType cassandraType = CassandraType.getCassandraType(columnMeta.getType().getName()); + List typeArguments = null; + if (cassandraType != null && cassandraType.getTypeArgumentSize() > 0) { + List typeArgs = columnMeta.getType().getTypeArguments(); + switch (cassandraType.getTypeArgumentSize()) { + case 1: + typeArguments = ImmutableList.of(CassandraType.getCassandraType(typeArgs.get(0).getName())); + break; + case 2: + typeArguments = ImmutableList.of(CassandraType.getCassandraType(typeArgs.get(0).getName()), CassandraType.getCassandraType(typeArgs.get(1).getName())); + break; + default: + throw new IllegalArgumentException("Invalid type arguments: " + typeArgs); + } + } + boolean indexed = columnMeta.getIndex() != null; + return new CassandraColumnHandle(connectorId, columnMeta.getName(), ordinalPosition, cassandraType, typeArguments, partitionKey, clusteringKey, indexed, hidden); + } + + public List getPartitions(CassandraTable table, List> filterPrefix) + { + Iterable rows = queryPartitionKeys(table, filterPrefix); + if (rows == null) { + // just split the whole partition range + return ImmutableList.of(CassandraPartition.UNPARTITIONED); + } + + List partitionKeyColumns = table.getPartitionKeyColumns(); + + ByteBuffer buffer = ByteBuffer.allocate(1000); + HashMap> map = new HashMap<>(); + Set uniquePartitionIds = new HashSet<>(); + StringBuilder stringBuilder = new StringBuilder(); + + boolean isComposite = partitionKeyColumns.size() > 1; + + ImmutableList.Builder partitions = ImmutableList.builder(); + for (Row row : rows) { + buffer.clear(); + map.clear(); + stringBuilder.setLength(0); + for (int i = 0; i < partitionKeyColumns.size(); i++) { + ByteBuffer component = row.getBytesUnsafe(i); + if (isComposite) { + // build composite key + short len = (short) component.limit(); + buffer.putShort(len); + buffer.put(component); + buffer.put((byte) 0); + } + else { + buffer.put(component); + } + CassandraColumnHandle columnHandle = partitionKeyColumns.get(i); + Comparable keyPart = CassandraType.getColumnValueForPartitionKey(row, i, columnHandle.getCassandraType(), columnHandle.getTypeArguments()); + map.put(columnHandle, keyPart); + if (i > 0) { + stringBuilder.append(" AND "); + } + stringBuilder.append(CassandraCqlUtils.validColumnName(columnHandle.getName())); + stringBuilder.append(" = "); + stringBuilder.append(CassandraType.getColumnValueForCql(row, i, columnHandle.getCassandraType())); + } + buffer.flip(); + byte[] key = new byte[buffer.limit()]; + buffer.get(key); + TupleDomain tupleDomain = TupleDomain.withFixedValues(map); + String partitionId = stringBuilder.toString(); + if (uniquePartitionIds.add(partitionId)) { + partitions.add(new CassandraPartition(key, partitionId, tupleDomain, false)); + } + } + return partitions.build(); + } + + protected Iterable queryPartitionKeys(CassandraTable table, List> filterPrefix) + { + CassandraTableHandle tableHandle = table.getTableHandle(); + String schemaName = tableHandle.getSchemaName(); + List partitionKeyColumns = table.getPartitionKeyColumns(); + + boolean fullPartitionKey = filterPrefix.size() == partitionKeyColumns.size(); + ResultSetFuture countFuture; + if (!fullPartitionKey) { + final Select countAll = CassandraCqlUtils.selectCountAllFrom(tableHandle).limit(limitForPartitionKeySelect); + countFuture = executeWithSession(schemaName, new SessionCallable() { + @Override + public ResultSetFuture executeWithSession(Session session) + { + return session.executeAsync(countAll); + } + }); + } + else { + // no need to count if partition key is completely known + countFuture = null; + } + + int limit = fullPartitionKey ? 1 : limitForPartitionKeySelect; + final Select partitionKeys = CassandraCqlUtils.selectDistinctFrom(tableHandle, partitionKeyColumns); + partitionKeys.limit(limit); + partitionKeys.setFetchSize(fetchSizeForPartitionKeySelect); + + if (!fullPartitionKey) { + addWhereClause(partitionKeys.where(), partitionKeyColumns, new ArrayList>()); + ResultSetFuture partitionKeyFuture = executeWithSession(schemaName, new SessionCallable() { + @Override + public ResultSetFuture executeWithSession(Session session) + { + return session.executeAsync(partitionKeys); + } + }); + + long count = countFuture.getUninterruptibly().one().getLong(0); + if (count == limitForPartitionKeySelect) { + partitionKeyFuture.cancel(true); + return null; // too much effort to query all partition keys + } + else { + return partitionKeyFuture.getUninterruptibly(); + } + } + else { + addWhereClause(partitionKeys.where(), partitionKeyColumns, filterPrefix); + ResultSetFuture partitionKeyFuture = executeWithSession(schemaName, new SessionCallable() { + @Override + public ResultSetFuture executeWithSession(Session session) + { + return session.executeAsync(partitionKeys); + } + }); + return partitionKeyFuture.getUninterruptibly(); + } + } + + public T executeWithSession(String schemaName, SessionCallable sessionCallable) + { + NoHostAvailableException lastException = null; + for (int i = 0; i < 2; i++) { + Session session = getSession(schemaName); + try { + return sessionCallable.executeWithSession(session); + } + catch (NoHostAvailableException e) { + lastException = e; + + // Something happened with our client connection. We need to + // re-establish the connection using our contact points. + sessionBySchema.asMap().remove(schemaName, session); + } + } + throw lastException; + } + + private interface SessionCallable + { + T executeWithSession(Session session); + } + + private static void addWhereClause(Where where, List partitionKeyColumns, List> filterPrefix) + { + for (int i = 0; i < filterPrefix.size(); i++) { + CassandraColumnHandle column = partitionKeyColumns.get(i); + Object value = column.getCassandraType().getJavaValue(filterPrefix.get(i)); + Clause clause = QueryBuilder.eq(CassandraCqlUtils.validColumnName(column.getName()), value); + where.and(clause); + } + } +} diff --git a/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraSplit.java b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraSplit.java new file mode 100644 index 00000000..50b107e3 --- /dev/null +++ b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraSplit.java @@ -0,0 +1,148 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.HostAddress; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +import java.util.List; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkNotNull; + +public class CassandraSplit + implements ConnectorSplit +{ + private final String connectorId; + private final String partitionId; + private final List addresses; + private final String schema; + private final String table; + private final String splitCondition; + + @JsonCreator + public CassandraSplit( + @JsonProperty("connectorId") String connectorId, + @JsonProperty("schema") String schema, + @JsonProperty("table") String table, + @JsonProperty("partitionId") String partitionId, + @JsonProperty("splitCondition") String splitCondition, + @JsonProperty("addresses") List addresses) + { + checkNotNull(connectorId, "connectorId is null"); + checkNotNull(schema, "schema is null"); + checkNotNull(table, "table is null"); + checkNotNull(partitionId, "partitionName is null"); + checkNotNull(addresses, "addresses is null"); + + this.connectorId = connectorId; + this.schema = schema; + this.table = table; + this.partitionId = partitionId; + this.addresses = ImmutableList.copyOf(addresses); + this.splitCondition = splitCondition; + } + + @JsonProperty + public String getConnectorId() + { + return connectorId; + } + + @JsonProperty + public String getSchema() + { + return schema; + } + + @JsonProperty + public String getSplitCondition() + { + return splitCondition; + } + + @JsonProperty + public String getTable() + { + return table; + } + + @JsonProperty + public String getPartitionId() + { + return partitionId; + } + + @JsonProperty + @Override + public List getAddresses() + { + return addresses; + } + + @Override + public boolean isRemotelyAccessible() + { + return true; + } + + @Override + public Object getInfo() + { + return ImmutableMap.builder() + .put("hosts", addresses) + .put("schema", schema) + .put("table", table) + .put("partitionId", partitionId) + .build(); + } + + @Override + public String toString() + { + return toStringHelper(this) + .addValue(table) + .addValue(partitionId) + .toString(); + } + + public String getWhereClause() + { + if (partitionId.equals(CassandraPartition.UNPARTITIONED_ID)) { + if (splitCondition != null) { + return " WHERE " + splitCondition; + } + else { + return ""; + } + } + else { + if (splitCondition != null) { + return " WHERE " + partitionId + " AND " + splitCondition; + } + else { + return " WHERE " + partitionId; + } + } + } + + public CassandraTableHandle getCassandraTableHandle() + { + return new CassandraTableHandle(connectorId, schema, table); + } +} diff --git a/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraSplitManager.java b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraSplitManager.java new file mode 100644 index 00000000..9bcdbf0d --- /dev/null +++ b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraSplitManager.java @@ -0,0 +1,378 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import com.datastax.driver.core.Host; +import com.facebook.presto.cassandra.util.CassandraCqlUtils; +import com.facebook.presto.cassandra.util.HostAddressFactory; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorPartition; +import com.facebook.presto.spi.ConnectorPartitionResult; +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.ConnectorSplitManager; +import com.facebook.presto.spi.ConnectorSplitSource; +import com.facebook.presto.spi.ConnectorTableHandle; +import com.facebook.presto.spi.Domain; +import com.facebook.presto.spi.FixedSplitSource; +import com.facebook.presto.spi.HostAddress; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.Range; +import com.facebook.presto.spi.TupleDomain; +import com.google.common.base.Predicate; +import com.google.common.base.Throwables; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import io.airlift.log.Logger; + +import javax.inject.Inject; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; + +import static com.facebook.presto.cassandra.util.CassandraCqlUtils.toCQLCompatibleString; +import static com.facebook.presto.cassandra.util.Types.checkType; +import static com.facebook.presto.spi.StandardErrorCode.EXTERNAL; +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Predicates.in; +import static com.google.common.base.Predicates.not; +import static com.google.common.util.concurrent.MoreExecutors.listeningDecorator; + +public class CassandraSplitManager + implements ConnectorSplitManager +{ + private static final Logger log = Logger.get(CassandraSplitManager.class); + + private final String connectorId; + private final CassandraSession cassandraSession; + private final CachingCassandraSchemaProvider schemaProvider; + private final int partitionSizeForBatchSelect; + private final CassandraTokenSplitManager tokenSplitMgr; + private final ListeningExecutorService executor; + + @Inject + public CassandraSplitManager(CassandraConnectorId connectorId, + CassandraClientConfig cassandraClientConfig, + CassandraSession cassandraSession, + CachingCassandraSchemaProvider schemaProvider, + CassandraTokenSplitManager tokenSplitMgr, + @ForCassandra ExecutorService executor) + { + this.connectorId = checkNotNull(connectorId, "connectorId is null").toString(); + this.schemaProvider = checkNotNull(schemaProvider, "schemaProvider is null"); + this.cassandraSession = checkNotNull(cassandraSession, "cassandraSession is null"); + this.partitionSizeForBatchSelect = cassandraClientConfig.getPartitionSizeForBatchSelect(); + this.tokenSplitMgr = tokenSplitMgr; + this.executor = listeningDecorator(executor); + } + + @Override + public ConnectorPartitionResult getPartitions(ConnectorTableHandle tableHandle, TupleDomain tupleDomain) + { + CassandraTableHandle cassandraTableHandle = checkType(tableHandle, CassandraTableHandle.class, "tableHandle"); + checkNotNull(tupleDomain, "tupleDomain is null"); + CassandraTable table = schemaProvider.getTable(cassandraTableHandle); + List partitionKeys = table.getPartitionKeyColumns(); + + // fetch the partitions + List allPartitions = getCassandraPartitions(table, tupleDomain); + log.debug("%s.%s #partitions: %d", cassandraTableHandle.getSchemaName(), cassandraTableHandle.getTableName(), allPartitions.size()); + + // do a final pass to filter based on fields that could not be used to build the prefix + List partitions = FluentIterable.from(allPartitions) + .filter(partitionMatches(tupleDomain)) + .filter(ConnectorPartition.class) + .toList(); + + // All partition key domains will be fully evaluated, so we don't need to include those + TupleDomain remainingTupleDomain = TupleDomain.none(); + if (!tupleDomain.isNone()) { + if (partitions.size() == 1 && ((CassandraPartition) partitions.get(0)).isUnpartitioned()) { + remainingTupleDomain = tupleDomain; + } + else { + @SuppressWarnings({"rawtypes", "unchecked"}) + List partitionColumns = (List) partitionKeys; + remainingTupleDomain = TupleDomain.withColumnDomains(Maps.filterKeys(tupleDomain.getDomains(), not(in(partitionColumns)))); + } + } + + // push down indexed column fixed value predicates only for unpartitioned partition which uses token range query + if (partitions.size() == 1 && ((CassandraPartition) partitions.get(0)).isUnpartitioned()) { + Map domains = tupleDomain.getDomains(); + List indexedColumns = Lists.newArrayList(); + // compose partitionId by using indexed column + StringBuilder sb = new StringBuilder(); + for (Map.Entry entry : domains.entrySet()) { + CassandraColumnHandle column = (CassandraColumnHandle) entry.getKey(); + Domain domain = entry.getValue(); + if (column.isIndexed() && domain.isSingleValue()) { + sb.append(CassandraCqlUtils.validColumnName(column.getName())) + .append(" = ") + .append(CassandraCqlUtils.cqlValue(toCQLCompatibleString(entry.getValue().getSingleValue()), column.getCassandraType())); + indexedColumns.add(column); + // Only one indexed column predicate can be pushed down. + break; + } + } + if (sb.length() > 0) { + CassandraPartition partition = (CassandraPartition) partitions.get(0); + TupleDomain filterIndexedColumn = TupleDomain.withColumnDomains(Maps.filterKeys(remainingTupleDomain.getDomains(), not(in(indexedColumns)))); + partitions = Lists.newArrayList(); + partitions.add(new CassandraPartition(partition.getKey(), sb.toString(), filterIndexedColumn, true)); + return new ConnectorPartitionResult(partitions, filterIndexedColumn); + } + } + return new ConnectorPartitionResult(partitions, remainingTupleDomain); + } + + private List getCassandraPartitions(final CassandraTable table, TupleDomain tupleDomain) + { + if (tupleDomain.isNone()) { + return ImmutableList.of(); + } + + Set>> partitionKeysSet = getPartitionKeysSet(table, tupleDomain); + + // empty filter means, all partitions + if (partitionKeysSet.isEmpty()) { + return schemaProvider.getAllPartitions(table); + } + + ImmutableList.Builder>> getPartitionResults = ImmutableList.builder(); + for (final List> partitionKeys : partitionKeysSet) { + getPartitionResults.add(executor.submit(new Callable>() + { + @Override + public List call() + { + return schemaProvider.getPartitions(table, partitionKeys); + } + })); + } + + ImmutableList.Builder partitions = ImmutableList.builder(); + for (ListenableFuture> result : getPartitionResults.build()) { + try { + partitions.addAll(result.get()); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw Throwables.propagate(e); + } + catch (ExecutionException e) { + throw new PrestoException(EXTERNAL, "Error fetching cassandra partitions", e); + } + } + + return partitions.build(); + } + + private static Set>> getPartitionKeysSet(CassandraTable table, TupleDomain tupleDomain) + { + ImmutableList.Builder>> partitionColumnValues = ImmutableList.builder(); + for (CassandraColumnHandle columnHandle : table.getPartitionKeyColumns()) { + Domain domain = tupleDomain.getDomains().get(columnHandle); + + // if there is no constraint on a partition key, return an empty set + if (domain == null) { + return ImmutableSet.of(); + } + + // todo does cassandra allow null partition keys? + if (domain.isNullAllowed()) { + return ImmutableSet.of(); + } + + ImmutableSet.Builder> columnValues = ImmutableSet.builder(); + for (Range range : domain.getRanges()) { + // if the range is not a single value, we can not perform partition pruning + if (!range.isSingleValue()) { + return ImmutableSet.of(); + } + Comparable value = range.getSingleValue(); + + CassandraType valueType = columnHandle.getCassandraType(); + columnValues.add(valueType.getValueForPartitionKey(value)); + + } + partitionColumnValues.add(columnValues.build()); + } + return Sets.cartesianProduct(partitionColumnValues.build()); + } + + @Override + public ConnectorSplitSource getPartitionSplits(ConnectorTableHandle tableHandle, List partitions) + { + checkNotNull(tableHandle, "tableHandle is null"); + CassandraTableHandle cassandraTableHandle = checkType(tableHandle, CassandraTableHandle.class, "tableHandle"); + + checkNotNull(partitions, "partitions is null"); + if (partitions.isEmpty()) { + return new FixedSplitSource(connectorId, ImmutableList.of()); + } + + // if this is an unpartitioned table, split into equal ranges + if (partitions.size() == 1) { + ConnectorPartition partition = partitions.get(0); + CassandraPartition cassandraPartition = checkType(partition, CassandraPartition.class, "partition"); + + if (cassandraPartition.isUnpartitioned() || cassandraPartition.isIndexedColumnPredicatePushdown()) { + CassandraTable table = schemaProvider.getTable(cassandraTableHandle); + List splits = getSplitsByTokenRange(table, cassandraPartition.getPartitionId()); + return new FixedSplitSource(connectorId, splits); + } + } + + return new FixedSplitSource(connectorId, getSplitsForPartitions(cassandraTableHandle, partitions)); + } + + private List getSplitsByTokenRange(CassandraTable table, String partitionId) + { + String schema = table.getTableHandle().getSchemaName(); + String tableName = table.getTableHandle().getTableName(); + String tokenExpression = table.getTokenExpression(); + + ImmutableList.Builder builder = ImmutableList.builder(); + List tokenSplits; + try { + tokenSplits = tokenSplitMgr.getSplits(schema, tableName); + } + catch (IOException e) { + throw new RuntimeException(e); + } + for (CassandraTokenSplitManager.TokenSplit tokenSplit : tokenSplits) { + String condition = buildTokenCondition(tokenExpression, tokenSplit.getStartToken(), tokenSplit.getEndToken()); + List addresses = new HostAddressFactory().AddressNamesToHostAddressList(tokenSplit.getHosts()); + CassandraSplit split = new CassandraSplit(connectorId, schema, tableName, partitionId, condition, addresses); + builder.add(split); + } + + return builder.build(); + } + + private static String buildTokenCondition(String tokenExpression, String startToken, String endToken) + { + return tokenExpression + " > " + startToken + " AND " + tokenExpression + " <= " + endToken; + } + + private List getSplitsForPartitions(CassandraTableHandle cassTableHandle, List partitions) + { + String schema = cassTableHandle.getSchemaName(); + String table = cassTableHandle.getTableName(); + HostAddressFactory hostAddressFactory = new HostAddressFactory(); + ImmutableList.Builder builder = ImmutableList.builder(); + + // For single partition key column table, we can merge multiple partitions into a single split + // by using IN CLAUSE in a single select query if the partitions have the same host list. + // For multiple partition key columns table, we can't merge them into a single select query, so + // keep them in a separate split. + boolean singlePartitionKeyColumn = true; + String partitionKeyColumnName = null; + if (!partitions.isEmpty()) { + singlePartitionKeyColumn = partitions.get(0).getTupleDomain().getNullableColumnDomains().size() == 1; + if (singlePartitionKeyColumn) { + String partitionId = partitions.get(0).getPartitionId(); + partitionKeyColumnName = partitionId.substring(0, partitionId.lastIndexOf("=") - 1); + } + } + Map, Set> hostsToPartitionKeys = Maps.newHashMap(); + Map, List> hostMap = Maps.newHashMap(); + + for (ConnectorPartition partition : partitions) { + CassandraPartition cassandraPartition = checkType(partition, CassandraPartition.class, "partition"); + Set hosts = cassandraSession.getReplicas(schema, cassandraPartition.getKeyAsByteBuffer()); + List addresses = hostAddressFactory.toHostAddressList(hosts); + if (singlePartitionKeyColumn) { + // host ip addresses + ImmutableSet.Builder sb = ImmutableSet.builder(); + for (HostAddress address : addresses) { + sb.add(address.getHostText()); + } + Set hostAddresses = sb.build(); + // partition key values + Set values = hostsToPartitionKeys.get(hostAddresses); + if (values == null) { + values = Sets.newHashSet(); + } + String partitionId = cassandraPartition.getPartitionId(); + values.add(partitionId.substring(partitionId.lastIndexOf("=") + 2)); + hostsToPartitionKeys.put(hostAddresses, values); + hostMap.put(hostAddresses, addresses); + } + else { + CassandraSplit split = new CassandraSplit(connectorId, schema, table, cassandraPartition.getPartitionId(), null, addresses); + builder.add(split); + } + } + if (singlePartitionKeyColumn) { + for (Map.Entry, Set> entry : hostsToPartitionKeys.entrySet()) { + StringBuilder sb = new StringBuilder(partitionSizeForBatchSelect); + int size = 0; + for (String value : entry.getValue()) { + if (size > 0) { + sb.append(","); + } + sb.append(value); + size++; + if (size > partitionSizeForBatchSelect) { + String partitionId = String.format("%s in (%s)", partitionKeyColumnName, sb.toString()); + CassandraSplit split = new CassandraSplit(connectorId, schema, table, partitionId, null, hostMap.get(entry.getKey())); + builder.add(split); + size = 0; + sb.setLength(0); + sb.trimToSize(); + } + } + if (size > 0) { + String partitionId = String.format("%s in (%s)", partitionKeyColumnName, sb.toString()); + CassandraSplit split = new CassandraSplit(connectorId, schema, table, partitionId, null, hostMap.get(entry.getKey())); + builder.add(split); + } + } + } + return builder.build(); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("clientId", connectorId) + .toString(); + } + + public static Predicate partitionMatches(final TupleDomain tupleDomain) + { + return new Predicate() + { + @Override + public boolean apply(CassandraPartition partition) + { + return tupleDomain.overlaps(partition.getTupleDomain()); + } + }; + } +} diff --git a/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraTable.java b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraTable.java new file mode 100644 index 00000000..fac9ed41 --- /dev/null +++ b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraTable.java @@ -0,0 +1,93 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import com.facebook.presto.cassandra.util.CassandraCqlUtils; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; + +import java.util.List; + +import static com.facebook.presto.cassandra.CassandraColumnHandle.partitionKeyPredicate; +import static com.google.common.base.MoreObjects.toStringHelper; + +public class CassandraTable +{ + private final CassandraTableHandle tableHandle; + private final List columns; + + public CassandraTable(CassandraTableHandle tableHandle, List columns) + { + this.tableHandle = tableHandle; + this.columns = ImmutableList.copyOf(columns); + } + + public List getColumns() + { + return columns; + } + + public CassandraTableHandle getTableHandle() + { + return tableHandle; + } + + public List getPartitionKeyColumns() + { + return ImmutableList.copyOf(Iterables.filter(columns, partitionKeyPredicate())); + } + + public String getTokenExpression() + { + StringBuilder sb = new StringBuilder(); + for (CassandraColumnHandle column : getPartitionKeyColumns()) { + if (sb.length() == 0) { + sb.append("token("); + } + else { + sb.append(","); + } + sb.append(CassandraCqlUtils.validColumnName(column.getName())); + } + sb.append(")"); + return sb.toString(); + } + + @Override + public int hashCode() + { + return tableHandle.hashCode(); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (!(obj instanceof CassandraTable)) { + return false; + } + CassandraTable that = (CassandraTable) obj; + return this.tableHandle.equals(that.tableHandle); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("tableHandle", tableHandle) + .toString(); + } +} diff --git a/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraTableHandle.java b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraTableHandle.java new file mode 100644 index 00000000..067e03c9 --- /dev/null +++ b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraTableHandle.java @@ -0,0 +1,92 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import com.facebook.presto.spi.ConnectorTableHandle; +import com.facebook.presto.spi.SchemaTableName; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class CassandraTableHandle + implements ConnectorTableHandle +{ + private final String connectorId; + private final String schemaName; + private final String tableName; + + @JsonCreator + public CassandraTableHandle( + @JsonProperty("connectorId") String connectorId, + @JsonProperty("schemaName") String schemaName, + @JsonProperty("tableName") String tableName) + { + this.connectorId = checkNotNull(connectorId, "connectorId is null"); + this.schemaName = checkNotNull(schemaName, "schemaName is null"); + this.tableName = checkNotNull(tableName, "tableName is null"); + } + + @JsonProperty + public String getConnectorId() + { + return connectorId; + } + + @JsonProperty + public String getSchemaName() + { + return schemaName; + } + + @JsonProperty + public String getTableName() + { + return tableName; + } + + public SchemaTableName getSchemaTableName() + { + return new SchemaTableName(schemaName, tableName); + } + + @Override + public int hashCode() + { + return Objects.hash(connectorId, schemaName, tableName); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + CassandraTableHandle other = (CassandraTableHandle) obj; + return Objects.equals(this.connectorId, other.connectorId) && + Objects.equals(this.schemaName, other.schemaName) && + Objects.equals(this.tableName, other.tableName); + } + + @Override + public String toString() + { + return connectorId + ":" + schemaName + ":" + tableName; + } +} diff --git a/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraThriftClient.java b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraThriftClient.java new file mode 100644 index 00000000..27a47a24 --- /dev/null +++ b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraThriftClient.java @@ -0,0 +1,100 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import com.google.common.collect.Lists; +import org.apache.cassandra.thrift.Cassandra.Client; +import org.apache.cassandra.thrift.CfSplit; +import org.apache.cassandra.thrift.TokenRange; +import org.apache.thrift.TApplicationException; +import org.apache.thrift.TException; +import org.apache.thrift.protocol.TProtocol; +import org.apache.thrift.transport.TTransport; + +import java.util.List; + +public class CassandraThriftClient +{ + private final CassandraThriftConnectionFactory connectionFactory; + + public CassandraThriftClient(CassandraThriftConnectionFactory connectionFactory) + { + this.connectionFactory = connectionFactory; + } + + public List getRangeMap(String keyspace) + { + Client client = connectionFactory.create(); + try { + return client.describe_ring(keyspace); + } + catch (TException e) { + throw new RuntimeException(e); + } + finally { + closeQuietly(client); + } + } + + public List getSubSplits(String keyspace, String columnFamily, TokenRange range, int splitSize) + { + Client client = connectionFactory.create(); + try { + client.set_keyspace(keyspace); + try { + return client.describe_splits_ex(columnFamily, range.start_token, range.end_token, splitSize); + } + catch (TApplicationException e) { + // fallback to guessing split size if talking to a server without describe_splits_ex method + if (e.getType() == TApplicationException.UNKNOWN_METHOD) { + List splitPoints = client.describe_splits(columnFamily, range.start_token, range.end_token, splitSize); + return tokenListToSplits(splitPoints, splitSize); + } + throw e; + } + } + catch (TException e) { + throw new RuntimeException(e); + } + finally { + closeQuietly(client); + } + } + + private static List tokenListToSplits(List splitTokens, int splitSize) + { + List splits = Lists.newArrayListWithExpectedSize(splitTokens.size() - 1); + for (int index = 0; index < splitTokens.size() - 1; index++) { + splits.add(new CfSplit(splitTokens.get(index), splitTokens.get(index + 1), splitSize)); + } + return splits; + } + + public static void closeQuietly(Client client) + { + try { + TProtocol inputProtocol = client.getInputProtocol(); + if (inputProtocol == null) { + return; + } + TTransport transport = inputProtocol.getTransport(); + if (transport == null) { + return; + } + transport.close(); + } + catch (Exception ignored) { + } + } +} diff --git a/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraThriftConnectionFactory.java b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraThriftConnectionFactory.java new file mode 100644 index 00000000..dac7bec8 --- /dev/null +++ b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraThriftConnectionFactory.java @@ -0,0 +1,112 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import com.google.common.collect.ImmutableMap; +import io.airlift.log.Logger; +import org.apache.cassandra.thrift.Cassandra; +import org.apache.cassandra.thrift.ITransportFactory; +import org.apache.thrift.protocol.TBinaryProtocol; +import org.apache.thrift.transport.TTransport; + +import javax.inject.Inject; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class CassandraThriftConnectionFactory +{ + private static final Logger log = Logger.get(CassandraThriftConnectionFactory.class); + private final int port; + private final List addresses; + private final String factoryClassName; + private final Map transportFactoryOptions; + + @Inject + public CassandraThriftConnectionFactory(CassandraClientConfig config) + { + this.addresses = config.getContactPoints(); + this.port = config.getThriftPort(); + this.factoryClassName = config.getThriftConnectionFactoryClassName(); + this.transportFactoryOptions = ImmutableMap.copyOf(config.getTransportFactoryOptions()); + } + + public Cassandra.Client create() + { + try { + return getClientFromAddressList(); + } + catch (IOException e) { + throw new RuntimeException(e); + } + } + + private Cassandra.Client getClientFromAddressList() throws IOException + { + List exceptions = new ArrayList(); + for (String address : addresses) { + try { + return createConnection(address, port, factoryClassName); + } + catch (IOException ioe) { + exceptions.add(ioe); + } + } + log.error("failed to connect to any initial addresses"); + for (IOException exception : exceptions) { + log.error(exception); + } + throw exceptions.get(exceptions.size() - 1); + } + + public Cassandra.Client createConnection(String host, Integer port, String factoryClassName) throws IOException + { + try { + TTransport transport = getClientTransportFactory(factoryClassName).openTransport(host, port); + return new Cassandra.Client(new TBinaryProtocol(transport, true, true)); + } + catch (Exception e) { + throw new IOException("Unable to connect to server " + host + ":" + port, e); + } + } + + private ITransportFactory getClientTransportFactory(String factoryClassName) + { + try { + ITransportFactory factory = (ITransportFactory) Class.forName(factoryClassName).newInstance(); + Map options = getOptions(factory.supportedOptions()); + factory.setOptions(options); + return factory; + } + catch (Exception e) { + throw new RuntimeException("Failed to instantiate transport factory:" + factoryClassName, e); + } + } + + private Map getOptions(Set supportedOptions) + { + Map options = new HashMap<>(); + for (String optionKey : supportedOptions) { + String optionValue = transportFactoryOptions.get(optionKey); + if (optionValue != null) { + options.put(optionKey, optionValue); + } + } + return options; + } +} diff --git a/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraTokenSplitManager.java b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraTokenSplitManager.java new file mode 100644 index 00000000..ba4fa56c --- /dev/null +++ b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraTokenSplitManager.java @@ -0,0 +1,169 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import com.google.common.collect.ImmutableList; +import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.exceptions.ConfigurationException; +import org.apache.cassandra.thrift.CfSplit; +import org.apache.cassandra.thrift.TokenRange; +import org.apache.cassandra.utils.FBUtilities; + +import javax.inject.Inject; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.ThreadLocalRandom; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static org.apache.cassandra.dht.Token.TokenFactory; + +public class CassandraTokenSplitManager +{ + private final CassandraThriftClient cassandraThriftClient; + private final ExecutorService executor; + private final int splitSize; + private final IPartitioner partitioner; + + @Inject + public CassandraTokenSplitManager(CassandraThriftConnectionFactory connectionFactory, @ForCassandra ExecutorService executor, CassandraClientConfig config) + { + this.cassandraThriftClient = new CassandraThriftClient(checkNotNull(connectionFactory, "connectionFactory is null")); + this.executor = checkNotNull(executor, "executor is null"); + this.splitSize = config.getSplitSize(); + try { + this.partitioner = FBUtilities.newPartitioner(config.getPartitioner()); + } + catch (ConfigurationException e) { + throw new RuntimeException(e); + } + } + + public List getSplits(String keyspace, String columnFamily) + throws IOException + { + List masterRangeNodes = cassandraThriftClient.getRangeMap(keyspace); + + // canonical ranges, split into pieces, fetching the splits in parallel + List splits = new ArrayList<>(); + List>> splitFutures = new ArrayList<>(); + for (TokenRange range : masterRangeNodes) { + // for each range, pick a live owner and ask it to compute bite-sized splits + splitFutures.add(executor.submit(new SplitCallable<>(range, keyspace, columnFamily, splitSize, cassandraThriftClient, partitioner))); + } + + // wait until we have all the results back + for (Future> futureInputSplits : splitFutures) { + try { + splits.addAll(futureInputSplits.get()); + } + catch (Exception e) { + throw new IOException("Could not get input splits", e); + } + } + + checkState(!splits.isEmpty(), "No splits created"); + //noinspection SharedThreadLocalRandom + Collections.shuffle(splits, ThreadLocalRandom.current()); + return splits; + } + + /** + * Gets a token range and splits it up according to the suggested + * size into input splits that Hadoop can use. + */ + private class SplitCallable> + implements Callable> + { + private final TokenRange range; + private final String keyspace; + private final String columnFamily; + private final int splitSize; + private final CassandraThriftClient client; + private final IPartitioner partitioner; + + public SplitCallable(TokenRange range, String keyspace, String columnFamily, int splitSize, CassandraThriftClient client, IPartitioner partitioner) + { + checkArgument(range.rpc_endpoints.size() == range.endpoints.size(), "rpc_endpoints size must match endpoints size"); + this.range = range; + this.keyspace = keyspace; + this.columnFamily = columnFamily; + this.splitSize = splitSize; + this.client = client; + this.partitioner = partitioner; + } + + @Override + public List call() + throws Exception + { + ArrayList splits = new ArrayList<>(); + List subSplits = client.getSubSplits(keyspace, columnFamily, range, splitSize); + + // turn the sub-ranges into InputSplits + List endpoints = range.endpoints; + TokenFactory factory = partitioner.getTokenFactory(); + for (CfSplit subSplit : subSplits) { + Token left = factory.fromString(subSplit.getStart_token()); + Token right = factory.fromString(subSplit.getEnd_token()); + Range> range = new Range<>(left, right, partitioner); + List>> ranges = range.isWrapAround() ? range.unwrap() : ImmutableList.of(range); + for (Range> subRange : ranges) { + TokenSplit split = new TokenSplit(factory.toString(subRange.left), factory.toString(subRange.right), endpoints); + + splits.add(split); + } + } + return splits; + } + } + + public static class TokenSplit + { + private String startToken; + private String endToken; + private List hosts; + + public TokenSplit(String startToken, String endToken, List hosts) + { + this.startToken = checkNotNull(startToken, "startToken is null"); + this.endToken = checkNotNull(endToken, "endToken is null"); + this.hosts = ImmutableList.copyOf(checkNotNull(hosts, "hosts is null")); + } + + public String getStartToken() + { + return startToken; + } + + public String getEndToken() + { + return endToken; + } + + public List getHosts() + { + return hosts; + } + } +} diff --git a/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraType.java b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraType.java new file mode 100644 index 00000000..c35014a9 --- /dev/null +++ b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraType.java @@ -0,0 +1,479 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import com.datastax.driver.core.DataType; +import com.datastax.driver.core.Row; +import com.datastax.driver.core.utils.Bytes; +import com.facebook.presto.cassandra.util.CassandraCqlUtils; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.type.BigintType; +import com.facebook.presto.spi.type.BooleanType; +import com.facebook.presto.spi.type.DateType; +import com.facebook.presto.spi.type.DoubleType; +import com.facebook.presto.spi.type.TimestampType; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.VarcharType; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.net.InetAddresses; +import io.airlift.slice.Slice; +import io.airlift.slice.Slices; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import static com.facebook.presto.spi.StandardErrorCode.NOT_SUPPORTED; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.net.InetAddresses.toAddrString; + +public enum CassandraType + implements FullCassandraType +{ + ASCII(VarcharType.VARCHAR, String.class), + BIGINT(BigintType.BIGINT, Long.class), + BLOB(VarcharType.VARCHAR, ByteBuffer.class), + CUSTOM(VarcharType.VARCHAR, ByteBuffer.class), + BOOLEAN(BooleanType.BOOLEAN, Boolean.class), + COUNTER(BigintType.BIGINT, Long.class), + DECIMAL(DoubleType.DOUBLE, BigDecimal.class), + DOUBLE(DoubleType.DOUBLE, Double.class), + FLOAT(DoubleType.DOUBLE, Float.class), + INET(VarcharType.VARCHAR, InetAddress.class), + INT(BigintType.BIGINT, Integer.class), + TEXT(VarcharType.VARCHAR, String.class), + TIMESTAMP(TimestampType.TIMESTAMP, Date.class), + UUID(VarcharType.VARCHAR, java.util.UUID.class), + TIMEUUID(VarcharType.VARCHAR, java.util.UUID.class), + VARCHAR(VarcharType.VARCHAR, String.class), + VARINT(VarcharType.VARCHAR, BigInteger.class), + LIST(VarcharType.VARCHAR, null), + MAP(VarcharType.VARCHAR, null), + SET(VarcharType.VARCHAR, null); + + private final Type nativeType; + private final Class javaType; + + CassandraType(Type nativeType, Class javaType) + { + this.nativeType = checkNotNull(nativeType, "nativeType is null"); + this.javaType = javaType; + } + + public Type getNativeType() + { + return nativeType; + } + + public int getTypeArgumentSize() + { + switch (this) { + case LIST: + case SET: + return 1; + case MAP: + return 2; + default: + return 0; + } + } + + public static CassandraType getSupportedCassandraType(DataType.Name name) + { + CassandraType cassandraType = getCassandraType(name); + checkArgument(cassandraType != null, "Unknown Cassandra type: " + name); + return cassandraType; + } + + public static CassandraType getCassandraType(DataType.Name name) + { + switch (name) { + case ASCII: + return ASCII; + case BIGINT: + return BIGINT; + case BLOB: + return BLOB; + case BOOLEAN: + return BOOLEAN; + case COUNTER: + return COUNTER; + case CUSTOM: + return CUSTOM; + case DECIMAL: + return DECIMAL; + case DOUBLE: + return DOUBLE; + case FLOAT: + return FLOAT; + case INET: + return INET; + case INT: + return INT; + case LIST: + return LIST; + case MAP: + return MAP; + case SET: + return SET; + case TEXT: + return TEXT; + case TIMESTAMP: + return TIMESTAMP; + case TIMEUUID: + return TIMEUUID; + case UUID: + return UUID; + case VARCHAR: + return VARCHAR; + case VARINT: + return VARINT; + default: + return null; + } + } + + public static CassandraType getSupportedCassandraType(String cassandraTypeName) + { + CassandraType cassandraType = getCassandraType(cassandraTypeName); + checkArgument(cassandraType != null, "Unknown Cassandra type: " + cassandraTypeName); + return cassandraType; + } + + public static CassandraType getCassandraType(String cassandraTypeName) + { + DataType.Name name = DataType.Name.valueOf(cassandraTypeName); + if (name != null) { + return getCassandraType(name); + } + return null; + } + + public static Comparable getColumnValue(Row row, int i, FullCassandraType fullCassandraType) + { + return getColumnValue(row, i, fullCassandraType.getCassandraType(), fullCassandraType.getTypeArguments()); + } + + public static Comparable getColumnValue(Row row, int i, CassandraType cassandraType, + List typeArguments) + { + if (row.isNull(i)) { + return null; + } + else { + switch (cassandraType) { + case ASCII: + case TEXT: + case VARCHAR: + return row.getString(i); + case INT: + return (long) row.getInt(i); + case BIGINT: + case COUNTER: + return row.getLong(i); + case BOOLEAN: + return row.getBool(i); + case DOUBLE: + return row.getDouble(i); + case FLOAT: + return (double) row.getFloat(i); + case DECIMAL: + return row.getDecimal(i).doubleValue(); + case UUID: + case TIMEUUID: + return row.getUUID(i).toString(); + case TIMESTAMP: + return row.getDate(i).getTime(); + case INET: + return toAddrString(row.getInet(i)); + case VARINT: + return row.getVarint(i).toString(); + case BLOB: + case CUSTOM: + return Bytes.toHexString(row.getBytesUnsafe(i)); + case SET: + checkTypeArguments(cassandraType, 1, typeArguments); + return buildSetValue(row, i, typeArguments.get(0)); + case LIST: + checkTypeArguments(cassandraType, 1, typeArguments); + return buildListValue(row, i, typeArguments.get(0)); + case MAP: + checkTypeArguments(cassandraType, 2, typeArguments); + return buildMapValue(row, i, typeArguments.get(0), typeArguments.get(1)); + default: + throw new IllegalStateException("Handling of type " + cassandraType + + " is not implemented"); + } + } + } + + public static Comparable getColumnValueForPartitionKey(Row row, int i, CassandraType cassandraType, List typeArguments) + { + if (row.isNull(i)) { + return null; + } + if (cassandraType == ASCII || cassandraType == TEXT || cassandraType == VARCHAR) { + return Slices.utf8Slice(row.getString(i)); + } + return getColumnValue(row, i, cassandraType, typeArguments); + } + + private static String buildSetValue(Row row, int i, CassandraType elemType) + { + return buildArrayValue(row.getSet(i, elemType.javaType), elemType); + } + + private static String buildListValue(Row row, int i, CassandraType elemType) + { + return buildArrayValue(row.getList(i, elemType.javaType), elemType); + } + + private static String buildMapValue(Row row, int i, CassandraType keyType, CassandraType valueType) + { + StringBuilder sb = new StringBuilder(); + sb.append("{"); + for (Map.Entry entry : row.getMap(i, keyType.javaType, valueType.javaType).entrySet()) { + if (sb.length() > 1) { + sb.append(","); + } + sb.append(objectToString(entry.getKey(), keyType)); + sb.append(":"); + sb.append(objectToString(entry.getValue(), valueType)); + } + sb.append("}"); + return sb.toString(); + } + + @VisibleForTesting + static String buildArrayValue(Collection collection, CassandraType elemType) + { + StringBuilder sb = new StringBuilder(); + sb.append("["); + for (Object value : collection) { + if (sb.length() > 1) { + sb.append(","); + } + sb.append(objectToString(value, elemType)); + } + sb.append("]"); + return sb.toString(); + } + + private static void checkTypeArguments(CassandraType type, int expectedSize, + List typeArguments) + { + if (typeArguments == null || typeArguments.size() != expectedSize) { + throw new IllegalArgumentException("Wrong number of type arguments " + typeArguments + + " for " + type); + } + } + + public static String getColumnValueForCql(Row row, int i, CassandraType cassandraType) + { + if (row.isNull(i)) { + return null; + } + else { + switch (cassandraType) { + case ASCII: + case TEXT: + case VARCHAR: + return CassandraCqlUtils.quoteStringLiteral(row.getString(i)); + case INT: + return Integer.toString(row.getInt(i)); + case BIGINT: + case COUNTER: + return Long.toString(row.getLong(i)); + case BOOLEAN: + return Boolean.toString(row.getBool(i)); + case DOUBLE: + return Double.toString(row.getDouble(i)); + case FLOAT: + return Float.toString(row.getFloat(i)); + case DECIMAL: + return row.getDecimal(i).toString(); + case UUID: + case TIMEUUID: + return row.getUUID(i).toString(); + case TIMESTAMP: + return Long.toString(row.getDate(i).getTime()); + case INET: + return CassandraCqlUtils.quoteStringLiteral(toAddrString(row.getInet(i))); + case VARINT: + return row.getVarint(i).toString(); + case BLOB: + case CUSTOM: + return Bytes.toHexString(row.getBytesUnsafe(i)); + default: + throw new IllegalStateException("Handling of type " + cassandraType + + " is not implemented"); + } + } + } + + private static String objectToString(Object object, CassandraType elemType) + { + switch (elemType) { + case ASCII: + case TEXT: + case VARCHAR: + case UUID: + case TIMEUUID: + case TIMESTAMP: + case INET: + case VARINT: + return CassandraCqlUtils.quoteStringLiteralForJson(object.toString()); + + case BLOB: + case CUSTOM: + return CassandraCqlUtils.quoteStringLiteralForJson(Bytes.toHexString((ByteBuffer) object)); + + case INT: + case BIGINT: + case COUNTER: + case BOOLEAN: + case DOUBLE: + case FLOAT: + case DECIMAL: + return object.toString(); + default: + throw new IllegalStateException("Handling of type " + elemType + " is not implemented"); + } + } + + @Override + public CassandraType getCassandraType() + { + if (getTypeArgumentSize() == 0) { + return this; + } + else { + // must not be called for types with type arguments + throw new IllegalStateException(); + } + } + + @Override + public List getTypeArguments() + { + if (getTypeArgumentSize() == 0) { + return null; + } + else { + // must not be called for types with type arguments + throw new IllegalStateException(); + } + } + + public Object getJavaValue(Comparable comparable) + { + switch (this) { + case ASCII: + case TEXT: + case VARCHAR: + case BIGINT: + case BOOLEAN: + case DOUBLE: + case COUNTER: + return comparable; + case INET: + return InetAddresses.forString((String) comparable); + case INT: + return ((Long) comparable).intValue(); + case FLOAT: + // conversion can result in precision lost + return ((Double) comparable).floatValue(); + case DECIMAL: + // conversion can result in precision lost + // Presto uses double for decimal, so to keep the floating point precision, convert it to string. + // Otherwise partition id doesn't match + return new BigDecimal(comparable.toString()); + case TIMESTAMP: + return new Date((Long) comparable); + case UUID: + case TIMEUUID: + return java.util.UUID.fromString((String) comparable); + case BLOB: + case CUSTOM: + return Bytes.fromHexString((String) comparable); + case VARINT: + return new BigInteger((String) comparable); + case SET: + case LIST: + case MAP: + default: + throw new IllegalStateException("Back conversion not implemented for " + this); + } + } + + public Comparable getValueForPartitionKey(Comparable comparable) + { + switch (this) { + case ASCII: + case TEXT: + case VARCHAR: + if (comparable instanceof Slice) { + return ((Slice) comparable).toStringUtf8(); + } + return comparable; + case BIGINT: + case BOOLEAN: + case DOUBLE: + case INET: + case INT: + case FLOAT: + case DECIMAL: + case TIMESTAMP: + case UUID: + case TIMEUUID: + return comparable; + case COUNTER: + case BLOB: + case CUSTOM: + case VARINT: + case SET: + case LIST: + case MAP: + default: + // todo should we just skip partition pruning instead of throwing an exception? + throw new PrestoException(NOT_SUPPORTED, "Unsupport partition key type: " + this); + } + } + + public static CassandraType toCassandraType(Type type) + { + if (type.equals(BooleanType.BOOLEAN)) { + return BOOLEAN; + } + else if (type.equals(BigintType.BIGINT)) { + return BIGINT; + } + else if (type.equals(DoubleType.DOUBLE)) { + return DOUBLE; + } + else if (type.equals(VarcharType.VARCHAR)) { + return TEXT; + } + else if (type.equals(DateType.DATE)) { + return TEXT; + } + throw new IllegalArgumentException("unsupported type: " + type); + } +} diff --git a/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraTypeWithTypeArguments.java b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraTypeWithTypeArguments.java new file mode 100644 index 00000000..2896ed8c --- /dev/null +++ b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraTypeWithTypeArguments.java @@ -0,0 +1,54 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class CassandraTypeWithTypeArguments + implements FullCassandraType +{ + private final CassandraType cassandraType; + private final List typeArguments; + + public CassandraTypeWithTypeArguments(CassandraType cassandraType, List typeArguments) + { + this.cassandraType = checkNotNull(cassandraType, "cassandraType is null"); + this.typeArguments = checkNotNull(typeArguments, "typeArguments is null"); + } + + @Override + public CassandraType getCassandraType() + { + return cassandraType; + } + + @Override + public List getTypeArguments() + { + return typeArguments; + } + + @Override + public String toString() + { + if (typeArguments != null) { + return cassandraType.toString() + typeArguments.toString(); + } + else { + return cassandraType.toString(); + } + } +} diff --git a/presto-cassandra/src/main/java/com/facebook/presto/cassandra/ExtraColumnMetadata.java b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/ExtraColumnMetadata.java new file mode 100644 index 00000000..46c68f4e --- /dev/null +++ b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/ExtraColumnMetadata.java @@ -0,0 +1,81 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Function; +import com.google.common.base.Predicate; + +import static com.google.common.base.MoreObjects.toStringHelper; + +public class ExtraColumnMetadata +{ + private final String name; + private final boolean hidden; + + @JsonCreator + public ExtraColumnMetadata( + @JsonProperty("name") String name, + @JsonProperty("hidden") boolean hidden) + { + this.name = name; + this.hidden = hidden; + } + + @JsonProperty + public String getName() + { + return name; + } + + @JsonProperty + public boolean isHidden() + { + return hidden; + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("name", name) + .add("hidden", hidden) + .toString(); + } + + public static Function nameGetter() + { + return new Function() + { + @Override + public String apply(ExtraColumnMetadata extraColumnMetadata) + { + return extraColumnMetadata.getName(); + } + }; + } + + public static Predicate hiddenPredicate() + { + return new Predicate() + { + @Override + public boolean apply(ExtraColumnMetadata extraColumnMetadata) + { + return extraColumnMetadata.isHidden(); + } + }; + } +} diff --git a/presto-cassandra/src/main/java/com/facebook/presto/cassandra/ForCassandra.java b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/ForCassandra.java new file mode 100644 index 00000000..704b2a74 --- /dev/null +++ b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/ForCassandra.java @@ -0,0 +1,31 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import javax.inject.Qualifier; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention(RUNTIME) +@Target({FIELD, PARAMETER, METHOD}) +@Qualifier +public @interface ForCassandra +{ +} diff --git a/presto-cassandra/src/main/java/com/facebook/presto/cassandra/FullCassandraType.java b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/FullCassandraType.java new file mode 100644 index 00000000..d8917214 --- /dev/null +++ b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/FullCassandraType.java @@ -0,0 +1,23 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import java.util.List; + +public interface FullCassandraType +{ + CassandraType getCassandraType(); + + List getTypeArguments(); +} diff --git a/presto-cassandra/src/main/java/com/facebook/presto/cassandra/RebindSafeMBeanServer.java b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/RebindSafeMBeanServer.java new file mode 100644 index 00000000..ecb68fee --- /dev/null +++ b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/RebindSafeMBeanServer.java @@ -0,0 +1,333 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import io.airlift.log.Logger; + +import javax.annotation.concurrent.ThreadSafe; +import javax.management.Attribute; +import javax.management.AttributeList; +import javax.management.AttributeNotFoundException; +import javax.management.InstanceAlreadyExistsException; +import javax.management.InstanceNotFoundException; +import javax.management.IntrospectionException; +import javax.management.InvalidAttributeValueException; +import javax.management.ListenerNotFoundException; +import javax.management.MBeanException; +import javax.management.MBeanInfo; +import javax.management.MBeanRegistrationException; +import javax.management.MBeanServer; +import javax.management.NotCompliantMBeanException; +import javax.management.NotificationFilter; +import javax.management.NotificationListener; +import javax.management.ObjectInstance; +import javax.management.ObjectName; +import javax.management.OperationsException; +import javax.management.QueryExp; +import javax.management.ReflectionException; +import javax.management.loading.ClassLoaderRepository; + +import java.io.ObjectInputStream; +import java.util.Set; + +/** + * MBeanServer wrapper that a ignores calls to registerMBean when there is already + * a MBean registered with the specified object name. + */ +@ThreadSafe +public class RebindSafeMBeanServer + implements MBeanServer +{ + private static final Logger log = Logger.get(RebindSafeMBeanServer.class); + + private final MBeanServer mbeanServer; + + public RebindSafeMBeanServer(MBeanServer mbeanServer) + { + this.mbeanServer = mbeanServer; + } + + /** + * Delegates to the wrapped mbean server, but if a mbean is already registered + * with the specified name, the existing instance is returned. + */ + @Override + public ObjectInstance registerMBean(Object object, ObjectName name) + throws MBeanRegistrationException, NotCompliantMBeanException + { + while (true) { + try { + // try to register the mbean + return mbeanServer.registerMBean(object, name); + } + catch (InstanceAlreadyExistsException ignored) { + } + + try { + // a mbean is already installed, try to return the already registered instance + ObjectInstance objectInstance = mbeanServer.getObjectInstance(name); + log.debug("%s already bound to %s", name, objectInstance); + return objectInstance; + } + catch (InstanceNotFoundException ignored) { + // the mbean was removed before we could get the reference + // start the whole process over again + } + } + } + + @Override + public void unregisterMBean(ObjectName name) + throws InstanceNotFoundException, MBeanRegistrationException + { + mbeanServer.unregisterMBean(name); + } + + @Override + public ObjectInstance getObjectInstance(ObjectName name) + throws InstanceNotFoundException + { + return mbeanServer.getObjectInstance(name); + } + + @Override + public Set queryMBeans(ObjectName name, QueryExp query) + { + return mbeanServer.queryMBeans(name, query); + } + + @Override + public Set queryNames(ObjectName name, QueryExp query) + { + return mbeanServer.queryNames(name, query); + } + + @Override + public boolean isRegistered(ObjectName name) + { + return mbeanServer.isRegistered(name); + } + + @Override + public Integer getMBeanCount() + { + return mbeanServer.getMBeanCount(); + } + + @Override + public Object getAttribute(ObjectName name, String attribute) + throws MBeanException, AttributeNotFoundException, InstanceNotFoundException, ReflectionException + { + return mbeanServer.getAttribute(name, attribute); + } + + @Override + public AttributeList getAttributes(ObjectName name, String[] attributes) + throws InstanceNotFoundException, ReflectionException + { + return mbeanServer.getAttributes(name, attributes); + } + + @Override + public void setAttribute(ObjectName name, Attribute attribute) + throws InstanceNotFoundException, AttributeNotFoundException, InvalidAttributeValueException, MBeanException, ReflectionException + { + mbeanServer.setAttribute(name, attribute); + } + + @Override + public AttributeList setAttributes(ObjectName name, AttributeList attributes) + throws InstanceNotFoundException, ReflectionException + { + return mbeanServer.setAttributes(name, attributes); + } + + @Override + public Object invoke(ObjectName name, String operationName, Object[] params, String[] signature) + throws InstanceNotFoundException, MBeanException, ReflectionException + { + return mbeanServer.invoke(name, operationName, params, signature); + } + + @Override + public String getDefaultDomain() + { + return mbeanServer.getDefaultDomain(); + } + + @Override + public String[] getDomains() + { + return mbeanServer.getDomains(); + } + + @Override + public void addNotificationListener(ObjectName name, NotificationListener listener, NotificationFilter filter, Object context) + throws InstanceNotFoundException + { + mbeanServer.addNotificationListener(name, listener, filter, context); + } + + @Override + public void addNotificationListener(ObjectName name, ObjectName listener, NotificationFilter filter, Object context) + throws InstanceNotFoundException + { + mbeanServer.addNotificationListener(name, listener, filter, context); + } + + @Override + public void removeNotificationListener(ObjectName name, ObjectName listener) + throws InstanceNotFoundException, ListenerNotFoundException + { + mbeanServer.removeNotificationListener(name, listener); + } + + @Override + public void removeNotificationListener(ObjectName name, ObjectName listener, NotificationFilter filter, Object context) + throws InstanceNotFoundException, ListenerNotFoundException + { + mbeanServer.removeNotificationListener(name, listener, filter, context); + } + + @Override + public void removeNotificationListener(ObjectName name, NotificationListener listener) + throws InstanceNotFoundException, ListenerNotFoundException + { + mbeanServer.removeNotificationListener(name, listener); + } + + @Override + public void removeNotificationListener(ObjectName name, NotificationListener listener, NotificationFilter filter, Object context) + throws InstanceNotFoundException, ListenerNotFoundException + { + mbeanServer.removeNotificationListener(name, listener, filter, context); + } + + @Override + public MBeanInfo getMBeanInfo(ObjectName name) + throws InstanceNotFoundException, IntrospectionException, ReflectionException + { + return mbeanServer.getMBeanInfo(name); + } + + @Override + public boolean isInstanceOf(ObjectName name, String className) + throws InstanceNotFoundException + { + return mbeanServer.isInstanceOf(name, className); + } + + @Override + public Object instantiate(String className) + throws ReflectionException, MBeanException + { + return mbeanServer.instantiate(className); + } + + @Override + public Object instantiate(String className, ObjectName loaderName) + throws ReflectionException, MBeanException, InstanceNotFoundException + { + return mbeanServer.instantiate(className, loaderName); + } + + @Override + public Object instantiate(String className, Object[] params, String[] signature) + throws ReflectionException, MBeanException + { + return mbeanServer.instantiate(className, params, signature); + } + + @Override + public Object instantiate(String className, ObjectName loaderName, Object[] params, String[] signature) + throws ReflectionException, MBeanException, InstanceNotFoundException + { + return mbeanServer.instantiate(className, loaderName, params, signature); + } + + @Override + @Deprecated + @SuppressWarnings("deprecation") + public ObjectInputStream deserialize(ObjectName name, byte[] data) + throws OperationsException + { + return mbeanServer.deserialize(name, data); + } + + @Override + @Deprecated + @SuppressWarnings("deprecation") + public ObjectInputStream deserialize(String className, byte[] data) + throws OperationsException, ReflectionException + { + return mbeanServer.deserialize(className, data); + } + + @Override + @Deprecated + @SuppressWarnings("deprecation") + public ObjectInputStream deserialize(String className, ObjectName loaderName, byte[] data) + throws OperationsException, ReflectionException + { + return mbeanServer.deserialize(className, loaderName, data); + } + + @Override + public ClassLoader getClassLoaderFor(ObjectName mbeanName) + throws InstanceNotFoundException + { + return mbeanServer.getClassLoaderFor(mbeanName); + } + + @Override + public ClassLoader getClassLoader(ObjectName loaderName) + throws InstanceNotFoundException + { + return mbeanServer.getClassLoader(loaderName); + } + + @Override + public ClassLoaderRepository getClassLoaderRepository() + { + return mbeanServer.getClassLoaderRepository(); + } + + @Override + public ObjectInstance createMBean(String className, ObjectName name) + throws ReflectionException, InstanceAlreadyExistsException, MBeanException, NotCompliantMBeanException + { + return mbeanServer.createMBean(className, name); + } + + @Override + public ObjectInstance createMBean(String className, ObjectName name, ObjectName loaderName) + throws ReflectionException, InstanceAlreadyExistsException, MBeanException, NotCompliantMBeanException, InstanceNotFoundException + { + return mbeanServer.createMBean(className, name, loaderName); + } + + @Override + public ObjectInstance createMBean(String className, ObjectName name, Object[] params, String[] signature) + throws ReflectionException, InstanceAlreadyExistsException, MBeanException, NotCompliantMBeanException + { + return mbeanServer.createMBean(className, name, params, signature); + } + + @Override + public ObjectInstance createMBean(String className, ObjectName name, ObjectName loaderName, Object[] params, String[] signature) + throws ReflectionException, InstanceAlreadyExistsException, MBeanException, NotCompliantMBeanException, InstanceNotFoundException + { + return mbeanServer.createMBean(className, name, loaderName, params, signature); + } +} diff --git a/presto-cassandra/src/main/java/com/facebook/presto/cassandra/RetryDriver.java b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/RetryDriver.java new file mode 100644 index 00000000..1fa45791 --- /dev/null +++ b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/RetryDriver.java @@ -0,0 +1,101 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import com.google.common.collect.ImmutableList; +import io.airlift.log.Logger; +import io.airlift.units.Duration; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class RetryDriver +{ + private static final Logger log = Logger.get(RetryDriver.class); + private static final int DEFAULT_RETRY_ATTEMPTS = 10; + private static final Duration DEFAULT_SLEEP_TIME = Duration.valueOf("1s"); + private static final Duration DEFAULT_MAX_RETRY_TIME = Duration.valueOf("30s"); + + private final int maxRetryAttempts; + private final Duration sleepTime; + private final Duration maxRetryTime; + private final List> exceptionWhiteList; + + private RetryDriver(int maxRetryAttempts, Duration sleepTime, Duration maxRetryTime, List> exceptionWhiteList) + { + this.maxRetryAttempts = maxRetryAttempts; + this.sleepTime = sleepTime; + this.maxRetryTime = maxRetryTime; + this.exceptionWhiteList = exceptionWhiteList; + } + + private RetryDriver() + { + this(DEFAULT_RETRY_ATTEMPTS, DEFAULT_SLEEP_TIME, DEFAULT_MAX_RETRY_TIME, ImmutableList.>of()); + } + + public static RetryDriver retry() + { + return new RetryDriver(); + } + + @SafeVarargs + public final RetryDriver stopOn(Class... classes) + { + checkNotNull(classes, "classes is null"); + List> exceptions = ImmutableList.>builder() + .addAll(exceptionWhiteList) + .addAll(Arrays.asList(classes)) + .build(); + + return new RetryDriver(maxRetryAttempts, sleepTime, maxRetryTime, exceptions); + } + + public RetryDriver stopOnIllegalExceptions() + { + return stopOn(NullPointerException.class, IllegalStateException.class, IllegalArgumentException.class); + } + + public V run(String callableName, Callable callable) + throws Exception + { + checkNotNull(callableName, "callableName is null"); + checkNotNull(callable, "callable is null"); + + long startTime = System.nanoTime(); + int attempt = 0; + while (true) { + attempt++; + try { + return callable.call(); + } + catch (Exception e) { + for (Class clazz : exceptionWhiteList) { + if (clazz.isInstance(e)) { + throw e; + } + } + if (attempt >= maxRetryAttempts || Duration.nanosSince(startTime).compareTo(maxRetryTime) >= 0) { + throw e; + } + log.debug("Failed on executing %s with attempt %d, will retry. Exception: %s", callableName, attempt, e.getMessage()); + TimeUnit.MILLISECONDS.sleep(sleepTime.toMillis()); + } + } + } +} diff --git a/presto-cassandra/src/main/java/com/facebook/presto/cassandra/RetryPolicyType.java b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/RetryPolicyType.java new file mode 100644 index 00000000..412e372f --- /dev/null +++ b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/RetryPolicyType.java @@ -0,0 +1,41 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import com.datastax.driver.core.policies.DefaultRetryPolicy; +import com.datastax.driver.core.policies.DowngradingConsistencyRetryPolicy; +import com.datastax.driver.core.policies.FallthroughRetryPolicy; +import com.datastax.driver.core.policies.RetryPolicy; + +import static com.google.common.base.Preconditions.checkNotNull; + +public enum RetryPolicyType +{ + DEFAULT(DefaultRetryPolicy.INSTANCE), + BACKOFF(BackoffRetryPolicy.INSTANCE), + DOWNGRADING_CONSISTENCY(DowngradingConsistencyRetryPolicy.INSTANCE), + FALLTHROUGH(FallthroughRetryPolicy.INSTANCE); + + private final RetryPolicy policy; + + RetryPolicyType(RetryPolicy policy) + { + this.policy = checkNotNull(policy, "policy is null"); + } + + public RetryPolicy getPolicy() + { + return policy; + } +} diff --git a/presto-cassandra/src/main/java/com/facebook/presto/cassandra/util/CassandraCqlUtils.java b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/util/CassandraCqlUtils.java new file mode 100644 index 00000000..30065a01 --- /dev/null +++ b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/util/CassandraCqlUtils.java @@ -0,0 +1,189 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra.util; + +import com.datastax.driver.core.querybuilder.QueryBuilder; +import com.datastax.driver.core.querybuilder.Select; +import com.datastax.driver.core.querybuilder.Select.Selection; +import com.facebook.presto.cassandra.CassandraColumnHandle; +import com.facebook.presto.cassandra.CassandraTableHandle; +import com.facebook.presto.cassandra.CassandraType; +import com.facebook.presto.spi.ColumnHandle; +import com.fasterxml.jackson.core.io.JsonStringEncoder; +import io.airlift.slice.Slice; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static java.util.Locale.ENGLISH; + +public final class CassandraCqlUtils +{ + private CassandraCqlUtils() + { + } + + private static final String[] KEYWORDS = {"ADD", "ALL", "ALLOW", "ALTER", "AND", "APPLY", + "ASC", "ASCII", "AUTHORIZE", "BATCH", "BEGIN", "BIGINT", "BLOB", "BOOLEAN", "BY", + "CLUSTERING", "COLUMNFAMILY", "COMPACT", "COUNT", "COUNTER", "CREATE", "DECIMAL", + "DELETE", "DESC", "DOUBLE", "DROP", "FILTERING", "FLOAT", "FROM", "GRANT", "IN", + "INDEX", "INET", "INSERT", "INT", "INTO", "KEY", "KEYSPACE", "KEYSPACES", "LIMIT", + "LIST", "MAP", "MODIFY", "NORECURSIVE", "NOSUPERUSER", "OF", "ON", "ORDER", "PASSWORD", + "PERMISSION", "PERMISSIONS", "PRIMARY", "RENAME", "REVOKE", "SCHEMA", "SELECT", "SET", + "STORAGE", "SUPERUSER", "TABLE", "TEXT", "TIMESTAMP", "TIMEUUID", "TO", "TOKEN", + "TRUNCATE", "TTL", "TYPE", "UNLOGGED", "UPDATE", "USE", "USER", "USERS", "USING", + "UUID", "VALUES", "VARCHAR", "VARINT", "WHERE", "WITH", "WRITETIME"}; + + private static final Set keywords = new HashSet<>(Arrays.asList(KEYWORDS)); + + public static final String EMPTY_COLUMN_NAME = "__empty__"; + + public static String validSchemaName(String identifier) + { + return validIdentifier(identifier); + } + + public static String validTableName(String identifier) + { + return validIdentifier(identifier); + } + + public static String validColumnName(String identifier) + { + if (identifier.isEmpty() || identifier.equals(EMPTY_COLUMN_NAME)) { + return "\"\""; + } + + return validIdentifier(identifier); + } + + private static String validIdentifier(String identifier) + { + if (!identifier.equals(identifier.toLowerCase(ENGLISH))) { + return quoteIdentifier(identifier); + } + + if (keywords.contains(identifier.toUpperCase(ENGLISH))) { + return quoteIdentifier(identifier); + } + return identifier; + } + + private static String quoteIdentifier(String identifier) + { + return '"' + identifier + '"'; + } + + public static String quoteStringLiteral(String string) + { + return "'" + string.replace("'", "''") + "'"; + } + + public static String quoteStringLiteralForJson(String string) + { + return '"' + new String(JsonStringEncoder.getInstance().quoteAsUTF8(string)) + '"'; + } + + public static void appendSelectColumns(StringBuilder stringBuilder, List columns) + { + appendSelectColumns(stringBuilder, columns, true); + } + + private static void appendSelectColumns(StringBuilder stringBuilder, List columns, boolean first) + { + for (ColumnHandle column : columns) { + if (first) { + first = false; + } + else { + stringBuilder.append(","); + } + stringBuilder.append(validColumnName(((CassandraColumnHandle) column).getName())); + } + } + + public static String cqlNameToSqlName(String name) + { + if (name.isEmpty()) { + return EMPTY_COLUMN_NAME; + } + return name; + } + + public static String sqlNameToCqlName(String name) + { + if (name.equals(EMPTY_COLUMN_NAME)) { + return ""; + } + return name; + } + + public static Selection select(List columns) + { + Selection selection = QueryBuilder.select(); + for (CassandraColumnHandle column : columns) { + selection.column(validColumnName(column.getName())); + } + return selection; + } + + public static Select selectFrom(CassandraTableHandle tableHandle, List columns) + { + return from(select(columns), tableHandle); + } + + public static Select from(Selection selection, CassandraTableHandle tableHandle) + { + String schema = validSchemaName(tableHandle.getSchemaName()); + String table = validTableName(tableHandle.getTableName()); + return selection.from(schema, table); + } + + public static Select selectDistinctFrom(CassandraTableHandle tableHandle, List columns) + { + return from(select(columns).distinct(), tableHandle); + } + + public static Select selectCountAllFrom(CassandraTableHandle tableHandle) + { + String schema = validSchemaName(tableHandle.getSchemaName()); + String table = validTableName(tableHandle.getTableName()); + return QueryBuilder.select().countAll().from(schema, table); + } + + public static String cqlValue(String value, CassandraType cassandraType) + { + switch (cassandraType) { + case ASCII: + case TEXT: + case VARCHAR: + return quoteStringLiteral(value); + case INET: + // remove '/' in the string. e.g. /127.0.0.1 + return quoteStringLiteral(value.substring(1)); + default: + return value; + } + } + + public static String toCQLCompatibleString(Comparable comparable) + { + if (comparable instanceof Slice) { + return ((Slice) comparable).toStringUtf8(); + } + return comparable.toString(); + } +} diff --git a/presto-cassandra/src/main/java/com/facebook/presto/cassandra/util/HostAddressFactory.java b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/util/HostAddressFactory.java new file mode 100644 index 00000000..d689eecb --- /dev/null +++ b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/util/HostAddressFactory.java @@ -0,0 +1,61 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra.util; + +import com.datastax.driver.core.Host; +import com.facebook.presto.spi.HostAddress; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class HostAddressFactory +{ + private final Map hostMap = new HashMap<>(); + + public HostAddress toHostAddress(Host host) + { + return toHostAddress(host.getAddress().getHostAddress()); + } + + public List toHostAddressList(Collection hosts) + { + ArrayList list = new ArrayList<>(hosts.size()); + for (Host host : hosts) { + list.add(toHostAddress(host)); + } + return list; + } + + public HostAddress toHostAddress(String hostAddressName) + { + HostAddress address = hostMap.get(hostAddressName); + if (address == null) { + address = HostAddress.fromString(hostAddressName); + hostMap.put(hostAddressName, address); + } + return address; + } + + public List AddressNamesToHostAddressList(Collection hosts) + { + ArrayList list = new ArrayList<>(hosts.size()); + for (String host : hosts) { + list.add(toHostAddress(host)); + } + return list; + } +} diff --git a/presto-cassandra/src/main/java/com/facebook/presto/cassandra/util/Types.java b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/util/Types.java new file mode 100644 index 00000000..08145e13 --- /dev/null +++ b/presto-cassandra/src/main/java/com/facebook/presto/cassandra/util/Types.java @@ -0,0 +1,33 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra.util; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public final class Types +{ + private Types() {} + + public static B checkType(A value, Class target, String name) + { + checkNotNull(value, "%s is null", name); + checkArgument(target.isInstance(value), + "%s must be of type %s, not %s", + name, + target.getName(), + value.getClass().getName()); + return target.cast(value); + } +} diff --git a/presto-cassandra/src/test/java/com/datastax/driver/core/RowUtil.java b/presto-cassandra/src/test/java/com/datastax/driver/core/RowUtil.java new file mode 100644 index 00000000..8b2fc080 --- /dev/null +++ b/presto-cassandra/src/test/java/com/datastax/driver/core/RowUtil.java @@ -0,0 +1,35 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.driver.core; + +import com.google.common.collect.ImmutableList; + +import java.nio.ByteBuffer; + +import static com.datastax.driver.core.ColumnDefinitions.Definition; +import static java.nio.charset.StandardCharsets.UTF_8; + +public final class RowUtil +{ + private RowUtil() + { + } + + public static Row createSingleStringRow(String value, int protocolVersion) + { + ColumnDefinitions definitions = new ColumnDefinitions(new Definition[] {new Definition("keyspace", "table", "column", DataType.ascii())}); + ByteBuffer data = ByteBuffer.wrap(value.getBytes(UTF_8)); + return ArrayBackedRow.fromData(definitions, null, ProtocolVersion.fromInt(protocolVersion), ImmutableList.of(data)); + } +} diff --git a/presto-cassandra/src/test/java/com/datastax/driver/core/TestHost.java b/presto-cassandra/src/test/java/com/datastax/driver/core/TestHost.java new file mode 100644 index 00000000..349e68fa --- /dev/null +++ b/presto-cassandra/src/test/java/com/datastax/driver/core/TestHost.java @@ -0,0 +1,25 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.driver.core; + +import java.net.InetSocketAddress; + +public class TestHost + extends Host +{ + public TestHost(InetSocketAddress address) + { + super(address, new ConvictionPolicy.Simple.Factory(), null); + } +} diff --git a/presto-cassandra/src/test/java/com/facebook/presto/cassandra/CassandraQueryRunner.java b/presto-cassandra/src/test/java/com/facebook/presto/cassandra/CassandraQueryRunner.java new file mode 100644 index 00000000..81b01552 --- /dev/null +++ b/presto-cassandra/src/test/java/com/facebook/presto/cassandra/CassandraQueryRunner.java @@ -0,0 +1,99 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import com.datastax.driver.core.Cluster; +import com.facebook.presto.Session; +import com.facebook.presto.tests.DistributedQueryRunner; +import com.facebook.presto.tpch.TpchPlugin; +import com.facebook.presto.tpch.testing.SampledTpchPlugin; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.airlift.tpch.TpchTable; +import org.cassandraunit.utils.EmbeddedCassandraServerHelper; + +import static com.facebook.presto.cassandra.CassandraTestingUtils.createOrReplaceKeyspace; +import static com.facebook.presto.spi.type.TimeZoneKey.UTC_KEY; +import static com.facebook.presto.tests.QueryAssertions.copyTpchTables; +import static com.facebook.presto.tpch.TpchMetadata.TINY_SCHEMA_NAME; +import static java.util.Locale.ENGLISH; + +public final class CassandraQueryRunner +{ + private CassandraQueryRunner() + { + } + + private static final String TPCH_SCHEMA = "tpch"; + private static final String TPCH_SAMPLED_SCHEMA = "tpch_sampled"; + + public static DistributedQueryRunner createCassandraQueryRunner(TpchTable... tables) + throws Exception + { + return createCassandraQueryRunner(ImmutableList.copyOf(tables)); + } + + public static DistributedQueryRunner createCassandraQueryRunner(Iterable> tables) + throws Exception + { + EmbeddedCassandraServerHelper.startEmbeddedCassandra(); + + try (Cluster cluster = CassandraTestingUtils.getCluster(); + com.datastax.driver.core.Session session = cluster.connect()) { + createOrReplaceKeyspace(session, "tpch"); + createOrReplaceKeyspace(session, "tpch_sampled"); + } + + DistributedQueryRunner queryRunner = new DistributedQueryRunner(createSession(), 4); + + queryRunner.installPlugin(new TpchPlugin()); + queryRunner.createCatalog("tpch", "tpch"); + + queryRunner.installPlugin(new SampledTpchPlugin()); + queryRunner.createCatalog("tpch_sampled", "tpch_sampled"); + + queryRunner.installPlugin(new CassandraPlugin()); + queryRunner.createCatalog("cassandra", "cassandra", ImmutableMap.of( + "cassandra.contact-points", "localhost", + "cassandra.native-protocol-port", "9142", + "cassandra.allow-drop-table", "true")); + + copyTpchTables(queryRunner, "tpch", TINY_SCHEMA_NAME, createSession(), tables); + copyTpchTables(queryRunner, "tpch_sampled", TINY_SCHEMA_NAME, createSampledSession(), tables); + + return queryRunner; + } + + public static Session createSession() + { + return createCassandraSession(TPCH_SCHEMA); + } + + public static Session createSampledSession() + { + return createCassandraSession(TPCH_SAMPLED_SCHEMA); + } + + public static Session createCassandraSession(String schema) + { + return Session.builder() + .setUser("user") + .setSource("test") + .setCatalog("cassandra") + .setSchema(schema) + .setTimeZoneKey(UTC_KEY) + .setLocale(ENGLISH) + .build(); + } +} diff --git a/presto-cassandra/src/test/java/com/facebook/presto/cassandra/CassandraTestingUtils.java b/presto-cassandra/src/test/java/com/facebook/presto/cassandra/CassandraTestingUtils.java new file mode 100644 index 00000000..4feee433 --- /dev/null +++ b/presto-cassandra/src/test/java/com/facebook/presto/cassandra/CassandraTestingUtils.java @@ -0,0 +1,191 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import com.datastax.driver.core.Cluster; +import com.datastax.driver.core.Session; +import com.datastax.driver.mapping.Mapper; +import com.datastax.driver.mapping.MappingManager; +import com.datastax.driver.mapping.annotations.PartitionKey; +import com.datastax.driver.mapping.annotations.Table; +import com.google.common.primitives.Ints; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.Date; +import java.util.UUID; + +import static org.testng.Assert.assertEquals; + +public class CassandraTestingUtils +{ + public static final String HOSTNAME = "localhost"; + public static final int PORT = 9142; + public static final String KEYSPACE_NAME = "Presto_Database"; + public static final String TABLE_NAME = "Presto_Test"; + private static final String CLUSTER_NAME = "TestCluster"; + + private CassandraTestingUtils() {} + + public static Cluster getCluster() + { + return Cluster.builder() + .withClusterName(CLUSTER_NAME) + .addContactPointsWithPorts(Arrays.asList(new InetSocketAddress(HOSTNAME, PORT))) + .build(); + } + + public static void createOrReplaceKeyspace(Session session, String keyspaceName) + { + session.execute("DROP KEYSPACE IF EXISTS " + keyspaceName); + session.execute("CREATE KEYSPACE " + keyspaceName + " WITH REPLICATION = {'class':'SimpleStrategy', 'replication_factor':1}"); + } + + public static void initializeTestData(Date date) + { + try (Cluster cluster = getCluster()) { + try (Session session = cluster.connect()) { + createOrReplaceKeyspace(session, KEYSPACE_NAME); + } + + try (Session session = cluster.connect(KEYSPACE_NAME)) { + session.execute("DROP TABLE IF EXISTS " + TABLE_NAME); + session.execute("CREATE TABLE " + TABLE_NAME + " (" + + " key text PRIMARY KEY, " + + " typeuuid uuid, " + + " typeinteger int, " + + " typelong bigint, " + + " typebytes blob, " + + " typetimestamp timestamp " + + ")"); + + Mapper mapper = new MappingManager(session).mapper(TableRow.class); + + for (Integer rowNumber = 1; rowNumber < 10; rowNumber++) { + TableRow tableRow = new TableRow( + "key " + rowNumber.toString(), + UUID.fromString(String.format("00000000-0000-0000-0000-%012d", rowNumber)), + rowNumber, + rowNumber.longValue() + 1000, + ByteBuffer.wrap(Ints.toByteArray(rowNumber)).asReadOnlyBuffer(), + date + ); + mapper.save(tableRow); + assertEquals(mapper.get(tableRow.getKey()).toString(), tableRow.toString()); + } + + assertEquals(session.execute("SELECT COUNT(*) FROM presto_test").all().get(0).getLong(0), 9); + } + } + } + + @Table(keyspace = "presto_database", name = "presto_test") + public static class TableRow + { + @PartitionKey + private String key; + private UUID typeuuid; + private Integer typeinteger; + private Long typelong; + private ByteBuffer typebytes; + private Date typetimestamp; + + public TableRow() {} + + public TableRow(String key, UUID typeuuid, Integer typeinteger, Long typelong, ByteBuffer typebytes, Date typetimestamp) + { + this.key = key; + this.typeuuid = typeuuid; + this.typeinteger = typeinteger; + this.typelong = typelong; + this.typebytes = typebytes; + this.typetimestamp = typetimestamp; + } + + public String getKey() + { + return key; + } + + public void setKey(String key) + { + this.key = key; + } + + public UUID getTypeuuid() + { + return typeuuid; + } + + public void setTypeuuid(UUID typeuuid) + { + this.typeuuid = typeuuid; + } + + public Integer getTypeinteger() + { + return typeinteger; + } + + public void setTypeinteger(Integer typeinteger) + { + this.typeinteger = typeinteger; + } + + public Long getTypelong() + { + return typelong; + } + + public void setTypelong(Long typelong) + { + this.typelong = typelong; + } + + public ByteBuffer getTypebytes() + { + return typebytes; + } + + public void setTypebytes(ByteBuffer typebytes) + { + this.typebytes = typebytes; + } + + public Date getTypetimestamp() + { + return typetimestamp; + } + + public void setTypetimestamp(Date typetimestamp) + { + this.typetimestamp = typetimestamp; + } + + @Override + public String toString() + { + return "TableRow{" + + "key='" + key + '\'' + + ", typeuuid=" + typeuuid + + ", typeinteger=" + typeinteger + + ", typelong=" + typelong + + ", typebytes=" + Charset.forName("UTF-8").decode(typebytes) + + ", typetimestamp=" + typetimestamp + + '}'; + } + } +} diff --git a/presto-cassandra/src/test/java/com/facebook/presto/cassandra/MockCassandraSession.java b/presto-cassandra/src/test/java/com/facebook/presto/cassandra/MockCassandraSession.java new file mode 100644 index 00000000..89f44bbb --- /dev/null +++ b/presto-cassandra/src/test/java/com/facebook/presto/cassandra/MockCassandraSession.java @@ -0,0 +1,158 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import com.datastax.driver.core.Host; +import com.datastax.driver.core.ResultSet; +import com.datastax.driver.core.Row; +import com.facebook.presto.spi.SchemaNotFoundException; +import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.spi.TableNotFoundException; +import com.google.common.collect.ImmutableList; + +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +import static com.datastax.driver.core.RowUtil.createSingleStringRow; +import static io.airlift.json.JsonCodec.listJsonCodec; + +public class MockCassandraSession + extends CassandraSession +{ + static final String TEST_SCHEMA = "testkeyspace"; + static final String BAD_SCHEMA = "badkeyspace"; + static final String TEST_TABLE = "testtbl"; + static final String TEST_COLUMN1 = "column1"; + static final String TEST_COLUMN2 = "column2"; + static final String TEST_PARTITION_KEY1 = "testpartition1"; + static final String TEST_PARTITION_KEY2 = "testpartition2"; + + private final AtomicInteger accessCount = new AtomicInteger(); + private boolean throwException; + + public MockCassandraSession(String connectorId, CassandraClientConfig config) + { + super(connectorId, + null, + config.getFetchSizeForPartitionKeySelect(), + config.getLimitForPartitionKeySelect(), + listJsonCodec(ExtraColumnMetadata.class)); + } + + public void setThrowException(boolean throwException) + { + this.throwException = throwException; + } + + public int getAccessCount() + { + return accessCount.get(); + } + + @Override + public List getAllSchemas() + { + accessCount.incrementAndGet(); + + if (throwException) { + throw new IllegalStateException(); + } + return ImmutableList.of(TEST_SCHEMA); + } + + @Override + public List getAllTables(String schema) + throws SchemaNotFoundException + { + accessCount.incrementAndGet(); + if (throwException) { + throw new IllegalStateException(); + } + + if (schema.equals(TEST_SCHEMA)) { + return ImmutableList.of(TEST_TABLE); + } + throw new SchemaNotFoundException(schema); + } + + @Override + public void getSchema(String schema) + throws SchemaNotFoundException + { + accessCount.incrementAndGet(); + if (throwException) { + throw new IllegalStateException(); + } + + if (!schema.equals(TEST_SCHEMA)) { + throw new SchemaNotFoundException(schema); + } + } + + @Override + public CassandraTable getTable(SchemaTableName tableName) + throws TableNotFoundException + { + accessCount.incrementAndGet(); + if (throwException) { + throw new IllegalStateException(); + } + + if (tableName.getSchemaName().equals(TEST_SCHEMA) && tableName.getTableName().equals(TEST_TABLE)) { + return new CassandraTable( + new CassandraTableHandle(connectorId, TEST_SCHEMA, TEST_TABLE), + ImmutableList.of( + new CassandraColumnHandle(connectorId, TEST_COLUMN1, 0, CassandraType.VARCHAR, null, true, false, false, false), + new CassandraColumnHandle(connectorId, TEST_COLUMN2, 0, CassandraType.INT, null, false, false, false, false))); + } + throw new TableNotFoundException(tableName); + } + + @Override + public List getPartitions(CassandraTable table, List> filterPrefix) + { + accessCount.incrementAndGet(); + if (throwException) { + throw new IllegalStateException(); + } + + return super.getPartitions(table, filterPrefix); + } + + @Override + protected List queryPartitionKeys(CassandraTable table, List> filterPrefix) + { + CassandraTableHandle tableHandle = table.getTableHandle(); + if (tableHandle.getSchemaName().equals(TEST_SCHEMA) && tableHandle.getTableName().equals(TEST_TABLE)) { + return ImmutableList.of( + createSingleStringRow(TEST_PARTITION_KEY1, 2), + createSingleStringRow(TEST_PARTITION_KEY2, 2)); + } + throw new IllegalStateException(); + } + + @Override + public Set getReplicas(String schemaName, ByteBuffer partitionKey) + { + throw new UnsupportedOperationException(); + } + + @Override + public ResultSet executeQuery(String schemaName, String cql) + { + throw new IllegalStateException("unexpected CQL query: " + cql); + } +} diff --git a/presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestCachingCassandraSchemaProvider.java b/presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestCachingCassandraSchemaProvider.java new file mode 100644 index 00000000..5a560490 --- /dev/null +++ b/presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestCachingCassandraSchemaProvider.java @@ -0,0 +1,165 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import com.facebook.presto.spi.SchemaNotFoundException; +import com.facebook.presto.spi.TableNotFoundException; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.ListeningExecutorService; +import io.airlift.units.Duration; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.util.concurrent.TimeUnit; + +import static com.facebook.presto.cassandra.MockCassandraSession.BAD_SCHEMA; +import static com.facebook.presto.cassandra.MockCassandraSession.TEST_SCHEMA; +import static com.facebook.presto.cassandra.MockCassandraSession.TEST_TABLE; +import static com.google.common.util.concurrent.MoreExecutors.listeningDecorator; +import static io.airlift.concurrent.Threads.daemonThreadsNamed; +import static java.util.concurrent.Executors.newCachedThreadPool; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +@Test(singleThreaded = true) +public class TestCachingCassandraSchemaProvider +{ + private static final String CONNECTOR_ID = "test-cassandra"; + private MockCassandraSession mockSession; + private CachingCassandraSchemaProvider schemaProvider; + + @BeforeMethod + public void setUp() + throws Exception + { + mockSession = new MockCassandraSession(CONNECTOR_ID, new CassandraClientConfig()); + ListeningExecutorService executor = listeningDecorator(newCachedThreadPool(daemonThreadsNamed("test-%s"))); + schemaProvider = new CachingCassandraSchemaProvider( + CONNECTOR_ID, + mockSession, + executor, + new Duration(5, TimeUnit.MINUTES), + new Duration(1, TimeUnit.MINUTES)); + } + + @Test + public void testGetAllDatabases() + throws Exception + { + assertEquals(mockSession.getAccessCount(), 0); + assertEquals(schemaProvider.getAllSchemas(), ImmutableList.of(TEST_SCHEMA)); + assertEquals(mockSession.getAccessCount(), 1); + assertEquals(schemaProvider.getAllSchemas(), ImmutableList.of(TEST_SCHEMA)); + assertEquals(mockSession.getAccessCount(), 1); + + schemaProvider.flushCache(); + + assertEquals(schemaProvider.getAllSchemas(), ImmutableList.of(TEST_SCHEMA)); + assertEquals(mockSession.getAccessCount(), 2); + } + + @Test + public void testGetAllTable() + throws Exception + { + assertEquals(mockSession.getAccessCount(), 0); + assertEquals(schemaProvider.getAllTables(TEST_SCHEMA), ImmutableList.of(TEST_TABLE)); + assertEquals(mockSession.getAccessCount(), 2); + assertEquals(schemaProvider.getAllTables(TEST_SCHEMA), ImmutableList.of(TEST_TABLE)); + assertEquals(mockSession.getAccessCount(), 2); + + schemaProvider.flushCache(); + + assertEquals(schemaProvider.getAllTables(TEST_SCHEMA), ImmutableList.of(TEST_TABLE)); + assertEquals(mockSession.getAccessCount(), 4); + } + + @Test(expectedExceptions = SchemaNotFoundException.class) + public void testInvalidDbGetAllTAbles() + throws Exception + { + schemaProvider.getAllTables(BAD_SCHEMA); + } + + @Test + public void testGetTable() + throws Exception + { + CassandraTableHandle tableHandle = new CassandraTableHandle(CONNECTOR_ID, TEST_SCHEMA, TEST_TABLE); + assertEquals(mockSession.getAccessCount(), 0); + assertNotNull(schemaProvider.getTable(tableHandle)); + assertEquals(mockSession.getAccessCount(), 1); + assertNotNull(schemaProvider.getTable(tableHandle)); + assertEquals(mockSession.getAccessCount(), 1); + + schemaProvider.flushCache(); + + assertNotNull(schemaProvider.getTable(tableHandle)); + assertEquals(mockSession.getAccessCount(), 2); + } + + @Test(expectedExceptions = TableNotFoundException.class) + public void testInvalidDbGetTable() + throws Exception + { + CassandraTableHandle tableHandle = new CassandraTableHandle(CONNECTOR_ID, BAD_SCHEMA, TEST_TABLE); + schemaProvider.getTable(tableHandle); + } + + @Test + public void testGetPartitions() + throws Exception + { + CassandraTableHandle tableHandle = new CassandraTableHandle(CONNECTOR_ID, TEST_SCHEMA, TEST_TABLE); + assertEquals(mockSession.getAccessCount(), 0); + + CassandraTable table = schemaProvider.getTable(tableHandle); + assertNotNull(table); + + String expectedList = "[column1 = 'testpartition1', column1 = 'testpartition2']"; + + assertEquals(mockSession.getAccessCount(), 1); + assertEquals(expectedList, schemaProvider.getAllPartitions(table).toString()); + assertEquals(mockSession.getAccessCount(), 2); + assertEquals(expectedList, schemaProvider.getAllPartitions(table).toString()); + assertEquals(mockSession.getAccessCount(), 2); + + schemaProvider.flushCache(); + + assertEquals(expectedList, schemaProvider.getAllPartitions(table).toString()); + assertEquals(mockSession.getAccessCount(), 3); + } + + @Test + public void testNoCacheExceptions() + throws Exception + { + // Throw exceptions on usage + mockSession.setThrowException(true); + try { + schemaProvider.getAllSchemas(); + } + catch (RuntimeException ignored) { + } + assertEquals(mockSession.getAccessCount(), 1); + + // Second try should hit the client again + try { + schemaProvider.getAllSchemas(); + } + catch (RuntimeException ignored) { + } + assertEquals(mockSession.getAccessCount(), 2); + } +} diff --git a/presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestCassandraClientConfig.java b/presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestCassandraClientConfig.java new file mode 100644 index 00000000..23de1bce --- /dev/null +++ b/presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestCassandraClientConfig.java @@ -0,0 +1,112 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import com.datastax.driver.core.ConsistencyLevel; +import com.datastax.driver.core.SocketOptions; +import com.google.common.collect.ImmutableMap; +import io.airlift.configuration.testing.ConfigAssertions; +import io.airlift.units.Duration; +import org.testng.annotations.Test; + +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +public class TestCassandraClientConfig +{ + @Test + public void testDefaults() + { + ConfigAssertions.assertRecordedDefaults(ConfigAssertions.recordDefaults(CassandraClientConfig.class) + .setLimitForPartitionKeySelect(200) + .setFetchSizeForPartitionKeySelect(20_000) + .setMaxSchemaRefreshThreads(10) + .setSchemaCacheTtl(new Duration(1, TimeUnit.HOURS)) + .setSchemaRefreshInterval(new Duration(2, TimeUnit.MINUTES)) + .setFetchSize(5_000) + .setConsistencyLevel(ConsistencyLevel.ONE) + .setContactPoints("") + .setNativeProtocolPort(9042) + .setPartitionSizeForBatchSelect(100) + .setSplitSize(1_024) + .setPartitioner("Murmur3Partitioner") + .setThriftPort(9160) + .setTransportFactoryOptions("") + .setThriftConnectionFactoryClassName("org.apache.cassandra.thrift.TFramedTransportFactory") + .setAllowDropTable(false) + .setUsername(null) + .setPassword(null) + .setClientReadTimeout(new Duration(SocketOptions.DEFAULT_READ_TIMEOUT_MILLIS, MILLISECONDS)) + .setClientConnectTimeout(new Duration(SocketOptions.DEFAULT_CONNECT_TIMEOUT_MILLIS, MILLISECONDS)) + .setClientSoLinger(null) + .setRetryPolicy(RetryPolicyType.DEFAULT)); + } + + @Test + public void testExplicitPropertyMappings() + { + Map properties = new ImmutableMap.Builder() + .put("cassandra.limit-for-partition-key-select", "100") + .put("cassandra.fetch-size-for-partition-key-select", "500") + .put("cassandra.max-schema-refresh-threads", "2") + .put("cassandra.schema-cache-ttl", "2h") + .put("cassandra.schema-refresh-interval", "30m") + .put("cassandra.contact-points", "host1,host2") + .put("cassandra.native-protocol-port", "9999") + .put("cassandra.fetch-size", "10000") + .put("cassandra.consistency-level", "TWO") + .put("cassandra.partition-size-for-batch-select", "77") + .put("cassandra.split-size", "1025") + .put("cassandra.thrift-port", "9161") + .put("cassandra.partitioner", "RandomPartitioner") + .put("cassandra.transport-factory-options", "a=b") + .put("cassandra.thrift-connection-factory-class", "org.apache.cassandra.thrift.TFramedTransportFactory1") + .put("cassandra.allow-drop-table", "true") + .put("cassandra.username", "my_username") + .put("cassandra.password", "my_password") + .put("cassandra.client.read-timeout", "11ms") + .put("cassandra.client.connect-timeout", "22ms") + .put("cassandra.client.so-linger", "33") + .put("cassandra.retry-policy", "BACKOFF") + .build(); + + CassandraClientConfig expected = new CassandraClientConfig() + .setLimitForPartitionKeySelect(100) + .setFetchSizeForPartitionKeySelect(500) + .setMaxSchemaRefreshThreads(2) + .setSchemaCacheTtl(new Duration(2, TimeUnit.HOURS)) + .setSchemaRefreshInterval(new Duration(30, TimeUnit.MINUTES)) + .setContactPoints("host1", "host2") + .setNativeProtocolPort(9999) + .setFetchSize(10_000) + .setConsistencyLevel(ConsistencyLevel.TWO) + .setPartitionSizeForBatchSelect(77) + .setSplitSize(1_025) + .setThriftPort(9161) + .setPartitioner("RandomPartitioner") + .setTransportFactoryOptions("a=b") + .setThriftConnectionFactoryClassName("org.apache.cassandra.thrift.TFramedTransportFactory1") + .setAllowDropTable(true) + .setUsername("my_username") + .setPassword("my_password") + .setClientReadTimeout(new Duration(11, MILLISECONDS)) + .setClientConnectTimeout(new Duration(22, MILLISECONDS)) + .setClientSoLinger(33) + .setRetryPolicy(RetryPolicyType.BACKOFF); + + ConfigAssertions.assertFullMapping(properties, expected); + } +} diff --git a/presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestCassandraColumnHandle.java b/presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestCassandraColumnHandle.java new file mode 100644 index 00000000..a084d901 --- /dev/null +++ b/presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestCassandraColumnHandle.java @@ -0,0 +1,67 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import com.google.common.collect.ImmutableList; +import io.airlift.json.JsonCodec; +import org.testng.annotations.Test; + +import static io.airlift.json.JsonCodec.jsonCodec; +import static org.testng.Assert.assertEquals; + +public class TestCassandraColumnHandle +{ + private final JsonCodec codec = jsonCodec(CassandraColumnHandle.class); + + @Test + public void testRoundTrip() + { + CassandraColumnHandle expected = new CassandraColumnHandle("connector", "name", 42, CassandraType.FLOAT, null, true, false, false, false); + + String json = codec.toJson(expected); + CassandraColumnHandle actual = codec.fromJson(json); + + assertEquals(actual.getConnectorId(), expected.getConnectorId()); + assertEquals(actual.getName(), expected.getName()); + assertEquals(actual.getOrdinalPosition(), expected.getOrdinalPosition()); + assertEquals(actual.getCassandraType(), expected.getCassandraType()); + assertEquals(actual.isPartitionKey(), expected.isPartitionKey()); + assertEquals(actual.isClusteringKey(), expected.isClusteringKey()); + } + + @Test + public void testRoundTrip2() + { + CassandraColumnHandle expected = new CassandraColumnHandle( + "connector", + "name2", + 1, + CassandraType.MAP, + ImmutableList.of(CassandraType.VARCHAR, CassandraType.UUID), + false, + true, + false, + false); + + String json = codec.toJson(expected); + CassandraColumnHandle actual = codec.fromJson(json); + + assertEquals(actual.getConnectorId(), expected.getConnectorId()); + assertEquals(actual.getName(), expected.getName()); + assertEquals(actual.getOrdinalPosition(), expected.getOrdinalPosition()); + assertEquals(actual.getCassandraType(), expected.getCassandraType()); + assertEquals(actual.isPartitionKey(), expected.isPartitionKey()); + assertEquals(actual.isClusteringKey(), expected.isClusteringKey()); + } +} diff --git a/presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestCassandraConnector.java b/presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestCassandraConnector.java new file mode 100644 index 00000000..b8e7ea4d --- /dev/null +++ b/presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestCassandraConnector.java @@ -0,0 +1,276 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ColumnMetadata; +import com.facebook.presto.spi.Connector; +import com.facebook.presto.spi.ConnectorHandleResolver; +import com.facebook.presto.spi.ConnectorMetadata; +import com.facebook.presto.spi.ConnectorPartitionResult; +import com.facebook.presto.spi.ConnectorRecordSetProvider; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.ConnectorSplitManager; +import com.facebook.presto.spi.ConnectorSplitSource; +import com.facebook.presto.spi.ConnectorTableHandle; +import com.facebook.presto.spi.ConnectorTableMetadata; +import com.facebook.presto.spi.RecordCursor; +import com.facebook.presto.spi.SchemaNotFoundException; +import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.spi.SchemaTablePrefix; +import com.facebook.presto.spi.TupleDomain; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import org.cassandraunit.utils.EmbeddedCassandraServerHelper; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +import static com.facebook.presto.cassandra.CassandraTestingUtils.HOSTNAME; +import static com.facebook.presto.cassandra.CassandraTestingUtils.KEYSPACE_NAME; +import static com.facebook.presto.cassandra.CassandraTestingUtils.PORT; +import static com.facebook.presto.cassandra.CassandraTestingUtils.TABLE_NAME; +import static com.facebook.presto.cassandra.CassandraTestingUtils.initializeTestData; +import static com.facebook.presto.cassandra.util.Types.checkType; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.BooleanType.BOOLEAN; +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; +import static com.facebook.presto.spi.type.TimeZoneKey.UTC_KEY; +import static com.facebook.presto.spi.type.TimestampType.TIMESTAMP; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static com.google.common.base.Preconditions.checkArgument; +import static io.airlift.concurrent.MoreFutures.getFutureValue; +import static io.airlift.testing.Assertions.assertInstanceOf; +import static java.util.Locale.ENGLISH; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.fail; + +@Test(singleThreaded = true) +public class TestCassandraConnector +{ + private static final ConnectorSession SESSION = new ConnectorSession("user", UTC_KEY, ENGLISH, System.currentTimeMillis(), null); + protected static final String INVALID_DATABASE = "totally_invalid_database"; + private static final Date DATE = new Date(); + protected String database; + protected SchemaTableName table; + protected SchemaTableName tableUnpartitioned; + protected SchemaTableName invalidTable; + private ConnectorMetadata metadata; + private ConnectorSplitManager splitManager; + private ConnectorRecordSetProvider recordSetProvider; + + @BeforeClass + public void setup() + throws Exception + { + EmbeddedCassandraServerHelper.startEmbeddedCassandra(); + + initializeTestData(DATE); + + String connectorId = "cassandra-test"; + CassandraConnectorFactory connectorFactory = new CassandraConnectorFactory( + connectorId, + ImmutableMap.of()); + + Connector connector = connectorFactory.create(connectorId, ImmutableMap.of( + "cassandra.contact-points", HOSTNAME, + "cassandra.native-protocol-port", Integer.toString(PORT))); + + metadata = connector.getMetadata(); + assertInstanceOf(metadata, CassandraMetadata.class); + + splitManager = connector.getSplitManager(); + assertInstanceOf(splitManager, CassandraSplitManager.class); + + recordSetProvider = connector.getRecordSetProvider(); + assertInstanceOf(recordSetProvider, CassandraRecordSetProvider.class); + + ConnectorHandleResolver handleResolver = connector.getHandleResolver(); + assertInstanceOf(handleResolver, CassandraHandleResolver.class); + + database = KEYSPACE_NAME.toLowerCase(); + table = new SchemaTableName(database, TABLE_NAME.toLowerCase()); + tableUnpartitioned = new SchemaTableName(database, "presto_test_unpartitioned"); + invalidTable = new SchemaTableName(database, "totally_invalid_table_name"); + } + + @AfterMethod + public void tearDown() + throws Exception + { + } + + @Test + public void testGetClient() + { + } + + @Test + public void testGetDatabaseNames() + throws Exception + { + List databases = metadata.listSchemaNames(SESSION); + assertTrue(databases.contains(database.toLowerCase(ENGLISH))); + } + + @Test + public void testGetTableNames() + throws Exception + { + List tables = metadata.listTables(SESSION, database); + assertTrue(tables.contains(table)); + } + + // disabled until metadata manager is updated to handle invalid catalogs and schemas + @Test(enabled = false, expectedExceptions = SchemaNotFoundException.class) + public void testGetTableNamesException() + throws Exception + { + metadata.listTables(SESSION, INVALID_DATABASE); + } + + @Test + public void testListUnknownSchema() + { + assertNull(metadata.getTableHandle(SESSION, new SchemaTableName("totally_invalid_database_name", "dual"))); + assertEquals(metadata.listTables(SESSION, "totally_invalid_database_name"), ImmutableList.of()); + assertEquals(metadata.listTableColumns(SESSION, new SchemaTablePrefix("totally_invalid_database_name", "dual")), ImmutableMap.of()); + } + + @Test + public void testGetRecords() + throws Exception + { + ConnectorTableHandle tableHandle = getTableHandle(table); + ConnectorTableMetadata tableMetadata = metadata.getTableMetadata(tableHandle); + List columnHandles = ImmutableList.copyOf(metadata.getColumnHandles(tableHandle).values()); + Map columnIndex = indexColumns(columnHandles); + + ConnectorPartitionResult partitionResult = splitManager.getPartitions(tableHandle, TupleDomain.all()); + List splits = getAllSplits(splitManager.getPartitionSplits(tableHandle, partitionResult.getPartitions())); + + long rowNumber = 0; + for (ConnectorSplit split : splits) { + CassandraSplit cassandraSplit = (CassandraSplit) split; + + long completedBytes = 0; + try (RecordCursor cursor = recordSetProvider.getRecordSet(cassandraSplit, columnHandles).cursor()) { + while (cursor.advanceNextPosition()) { + try { + assertReadFields(cursor, tableMetadata.getColumns()); + } + catch (RuntimeException e) { + throw new RuntimeException("row " + rowNumber, e); + } + + rowNumber++; + + String keyValue = cursor.getSlice(columnIndex.get("key")).toStringUtf8(); + assertTrue(keyValue.startsWith("key ")); + int rowId = Integer.parseInt(keyValue.substring(4)); + + assertEquals(keyValue, String.format("key %d", rowId)); + + // bytes are encoded as a hex string for some reason + // this check keeps failing for some reason; disabling it for now + assertEquals(cursor.getSlice(columnIndex.get("typebytes")).toStringUtf8(), String.format("0x%08X", rowId)); + + // VARINT is returned as a string + assertEquals(cursor.getSlice(columnIndex.get("typeinteger")).toStringUtf8(), String.valueOf(rowId)); + + assertEquals(cursor.getLong(columnIndex.get("typelong")), 1000 + rowId); + + assertEquals(cursor.getSlice(columnIndex.get("typeuuid")).toStringUtf8(), String.format("00000000-0000-0000-0000-%012d", rowId)); + + assertEquals(cursor.getSlice(columnIndex.get("typetimestamp")).toStringUtf8(), Long.valueOf(DATE.getTime()).toString()); + + long newCompletedBytes = cursor.getCompletedBytes(); + assertTrue(newCompletedBytes >= completedBytes); + completedBytes = newCompletedBytes; + } + } + } + assertEquals(rowNumber, 9); + } + + private static void assertReadFields(RecordCursor cursor, List schema) + { + for (int columnIndex = 0; columnIndex < schema.size(); columnIndex++) { + ColumnMetadata column = schema.get(columnIndex); + if (!cursor.isNull(columnIndex)) { + Type type = column.getType(); + if (BOOLEAN.equals(type)) { + cursor.getBoolean(columnIndex); + } + else if (BIGINT.equals(type)) { + cursor.getLong(columnIndex); + } + else if (TIMESTAMP.equals(type)) { + cursor.getLong(columnIndex); + } + else if (DOUBLE.equals(type)) { + cursor.getDouble(columnIndex); + } + else if (VARCHAR.equals(type)) { + try { + cursor.getSlice(columnIndex); + } + catch (RuntimeException e) { + throw new RuntimeException("column " + column, e); + } + } + else { + fail("Unknown primitive type " + columnIndex); + } + } + } + } + + private ConnectorTableHandle getTableHandle(SchemaTableName tableName) + { + ConnectorTableHandle handle = metadata.getTableHandle(SESSION, tableName); + checkArgument(handle != null, "table not found: %s", tableName); + return handle; + } + + private static List getAllSplits(ConnectorSplitSource splitSource) + throws InterruptedException + { + ImmutableList.Builder splits = ImmutableList.builder(); + while (!splitSource.isFinished()) { + splits.addAll(getFutureValue(splitSource.getNextBatch(1000))); + } + return splits.build(); + } + + private static ImmutableMap indexColumns(List columnHandles) + { + ImmutableMap.Builder index = ImmutableMap.builder(); + int i = 0; + for (ColumnHandle columnHandle : columnHandles) { + String name = checkType(columnHandle, CassandraColumnHandle.class, "columnHandle").getName(); + index.put(name, i); + i++; + } + return index.build(); + } +} diff --git a/presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestCassandraDistributed.java b/presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestCassandraDistributed.java new file mode 100644 index 00000000..8501029e --- /dev/null +++ b/presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestCassandraDistributed.java @@ -0,0 +1,81 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import com.facebook.presto.tests.AbstractTestDistributedQueries; +import io.airlift.tpch.TpchTable; +import org.testng.annotations.Test; + +import static com.facebook.presto.cassandra.CassandraQueryRunner.createCassandraQueryRunner; +import static com.facebook.presto.cassandra.CassandraQueryRunner.createSampledSession; + +@Test(singleThreaded = true) +public class TestCassandraDistributed + extends AbstractTestDistributedQueries +{ + public TestCassandraDistributed() + throws Exception + { + super(createCassandraQueryRunner(TpchTable.getTables()), createSampledSession()); + } + + @Override + public void testRenameTable() + throws Exception + { + // Cassandra does not support renaming tables + } + + @Override + public void testRenameColumn() + throws Exception + { + // Cassandra does not support renaming columns + } + + @Override + public void testView() + throws Exception + { + // Cassandra connector currently does not support views + } + + @Override + public void testViewMetadata() + throws Exception + { + // Cassandra connector currently does not support views + } + + @Override + public void testInsert() + throws Exception + { + // Cassandra connector currently does not support insert + } + + @Override + public void testCreateTable() + throws Exception + { + // Cassandra connector currently does not support create table + } + + @Override + public void testDelete() + throws Exception + { + // Cassandra connector currently does not support delete + } +} diff --git a/presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestCassandraIntegrationSmokeTest.java b/presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestCassandraIntegrationSmokeTest.java new file mode 100644 index 00000000..13d92f27 --- /dev/null +++ b/presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestCassandraIntegrationSmokeTest.java @@ -0,0 +1,46 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import com.facebook.presto.tests.AbstractTestIntegrationSmokeTest; +import org.testng.annotations.Test; + +import java.util.Date; + +import static com.facebook.presto.cassandra.CassandraQueryRunner.createCassandraQueryRunner; +import static com.facebook.presto.cassandra.CassandraQueryRunner.createCassandraSession; +import static com.facebook.presto.cassandra.CassandraQueryRunner.createSampledSession; +import static com.facebook.presto.cassandra.CassandraTestingUtils.initializeTestData; +import static io.airlift.tpch.TpchTable.ORDERS; + +@Test(singleThreaded = true) +public class TestCassandraIntegrationSmokeTest + extends AbstractTestIntegrationSmokeTest +{ + public TestCassandraIntegrationSmokeTest() + throws Exception + { + super(createCassandraQueryRunner(ORDERS), createSampledSession()); + } + + @Test + public void testStringPartitionKey() + { + initializeTestData(new Date()); + + queryRunner.execute(createCassandraSession("presto_database"), "select * from presto_database.presto_test where key='key 1'"); + queryRunner.execute(createCassandraSession("presto_database"), "select * from presto_database.presto_test where key='key 2'"); + queryRunner.execute(createCassandraSession("presto_database"), "select * from presto_database.presto_test where key='key 3'"); + } +} diff --git a/presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestCassandraSplit.java b/presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestCassandraSplit.java new file mode 100644 index 00000000..8d4d4c10 --- /dev/null +++ b/presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestCassandraSplit.java @@ -0,0 +1,68 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import com.facebook.presto.spi.HostAddress; +import com.google.common.collect.ImmutableList; +import io.airlift.json.JsonCodec; +import org.testng.annotations.Test; + +import static org.testng.Assert.assertEquals; + +public class TestCassandraSplit +{ + private final JsonCodec codec = JsonCodec.jsonCodec(CassandraSplit.class); + + private final ImmutableList addresses = ImmutableList.of( + HostAddress.fromParts("127.0.0.1", 44), + HostAddress.fromParts("127.0.0.1", 45)); + + @Test + public void testJsonRoundTrip() + { + CassandraSplit expected = new CassandraSplit("connectorId", "schema1", "table1", "partitionId", "condition", addresses); + + String json = codec.toJson(expected); + CassandraSplit actual = codec.fromJson(json); + + assertEquals(actual.getConnectorId(), expected.getConnectorId()); + assertEquals(actual.getSchema(), expected.getSchema()); + assertEquals(actual.getTable(), expected.getTable()); + assertEquals(actual.getSplitCondition(), expected.getSplitCondition()); + assertEquals(actual.getAddresses(), expected.getAddresses()); + } + + @Test + public void testWhereClause() + { + CassandraSplit split; + split = new CassandraSplit( + "connectorId", + "schema1", + "table1", + CassandraPartition.UNPARTITIONED_ID, + "token(k) >= 0 AND token(k) <= 2", + addresses); + assertEquals(split.getWhereClause(), " WHERE token(k) >= 0 AND token(k) <= 2"); + + split = new CassandraSplit( + "connectorId", + "schema1", + "table1", + "key = 123", + null, + addresses); + assertEquals(split.getWhereClause(), " WHERE key = 123"); + } +} diff --git a/presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestCassandraTableHandle.java b/presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestCassandraTableHandle.java new file mode 100644 index 00000000..2da21d68 --- /dev/null +++ b/presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestCassandraTableHandle.java @@ -0,0 +1,36 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import io.airlift.json.JsonCodec; +import org.testng.annotations.Test; + +import static org.testng.Assert.assertEquals; + +public class TestCassandraTableHandle +{ + private final JsonCodec codec = JsonCodec.jsonCodec(CassandraTableHandle.class); + + @Test + public void testRoundTrip() + { + CassandraTableHandle expected = new CassandraTableHandle("client", "schema", "table"); + + String json = codec.toJson(expected); + CassandraTableHandle actual = codec.fromJson(json); + + assertEquals(actual.getConnectorId(), expected.getConnectorId()); + assertEquals(actual.getSchemaTableName(), expected.getSchemaTableName()); + } +} diff --git a/presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestCassandraType.java b/presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestCassandraType.java new file mode 100644 index 00000000..4d48573b --- /dev/null +++ b/presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestCassandraType.java @@ -0,0 +1,58 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.Lists; +import org.testng.annotations.Test; + +import java.io.IOException; + +import static org.testng.Assert.assertTrue; + +public class TestCassandraType +{ + @Test + public void testJsonMapEncoding() + { + assertTrue(isValidJson(CassandraType.buildArrayValue(Lists.newArrayList("one", "two", "three\""), CassandraType.VARCHAR))); + assertTrue(isValidJson(CassandraType.buildArrayValue(Lists.newArrayList(1, 2, 3), CassandraType.INT))); + assertTrue(isValidJson(CassandraType.buildArrayValue(Lists.newArrayList(100000L, 200000000L, 3000000000L), CassandraType.BIGINT))); + assertTrue(isValidJson(CassandraType.buildArrayValue(Lists.newArrayList(1.0, 2.0, 3.0), CassandraType.DOUBLE))); + } + + private static void continueWhileNotNull(JsonParser parser, JsonToken token) + throws IOException + { + if (token != null) { + continueWhileNotNull(parser, parser.nextToken()); + } + } + + private static boolean isValidJson(String json) + { + boolean valid = false; + try { + JsonParser parser = new ObjectMapper().getFactory() + .createJsonParser(json); + continueWhileNotNull(parser, parser.nextToken()); + valid = true; + } + catch (IOException ignored) { + } + return valid; + } +} diff --git a/presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestJsonCassandraHandles.java b/presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestJsonCassandraHandles.java new file mode 100644 index 00000000..134c4dca --- /dev/null +++ b/presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestJsonCassandraHandles.java @@ -0,0 +1,157 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra; + +import com.facebook.presto.spi.SchemaTableName; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.airlift.json.ObjectMapperProvider; +import org.testng.annotations.Test; + +import java.util.Map; + +import static io.airlift.testing.Assertions.assertEqualsIgnoreOrder; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +@Test +public class TestJsonCassandraHandles +{ + private static final Map TABLE_HANDLE_AS_MAP = ImmutableMap.of( + "connectorId", "cassandra", + "schemaName", "cassandra_schema", + "tableName", "cassandra_table"); + + private static final Map COLUMN_HANDLE_AS_MAP = ImmutableMap.builder() + .put("connectorId", "cassandra") + .put("name", "column") + .put("ordinalPosition", 42) + .put("cassandraType", "BIGINT") + .put("partitionKey", false) + .put("clusteringKey", true) + .put("indexed", false) + .put("hidden", false) + .build(); + + private static final Map COLUMN2_HANDLE_AS_MAP = ImmutableMap.builder() + .put("connectorId", "cassandra") + .put("name", "column2") + .put("ordinalPosition", 0) + .put("cassandraType", "SET") + .put("typeArguments", ImmutableList.of("INT")) + .put("partitionKey", false) + .put("clusteringKey", false) + .put("indexed", false) + .put("hidden", false) + .build(); + + private final ObjectMapper objectMapper = new ObjectMapperProvider().get(); + + @Test + public void testTableHandleSerialize() + throws Exception + { + CassandraTableHandle tableHandle = new CassandraTableHandle("cassandra", "cassandra_schema", "cassandra_table"); + + assertTrue(objectMapper.canSerialize(CassandraTableHandle.class)); + String json = objectMapper.writeValueAsString(tableHandle); + testJsonEquals(json, TABLE_HANDLE_AS_MAP); + } + + @Test + public void testTableHandleDeserialize() + throws Exception + { + String json = objectMapper.writeValueAsString(TABLE_HANDLE_AS_MAP); + + CassandraTableHandle tableHandle = objectMapper.readValue(json, CassandraTableHandle.class); + + assertEquals(tableHandle.getConnectorId(), "cassandra"); + assertEquals(tableHandle.getSchemaName(), "cassandra_schema"); + assertEquals(tableHandle.getTableName(), "cassandra_table"); + assertEquals(tableHandle.getSchemaTableName(), new SchemaTableName("cassandra_schema", "cassandra_table")); + } + + @Test + public void testColumnHandleSerialize() + throws Exception + { + CassandraColumnHandle columnHandle = new CassandraColumnHandle("cassandra", "column", 42, CassandraType.BIGINT, null, false, true, false, false); + + assertTrue(objectMapper.canSerialize(CassandraColumnHandle.class)); + String json = objectMapper.writeValueAsString(columnHandle); + testJsonEquals(json, COLUMN_HANDLE_AS_MAP); + } + + @Test + public void testColumn2HandleSerialize() + throws Exception + { + CassandraColumnHandle columnHandle = new CassandraColumnHandle( + "cassandra", + "column2", + 0, + CassandraType.SET, + ImmutableList.of(CassandraType.INT), + false, + false, + false, + false); + + assertTrue(objectMapper.canSerialize(CassandraColumnHandle.class)); + String json = objectMapper.writeValueAsString(columnHandle); + testJsonEquals(json, COLUMN2_HANDLE_AS_MAP); + } + + @Test + public void testColumnHandleDeserialize() + throws Exception + { + String json = objectMapper.writeValueAsString(COLUMN_HANDLE_AS_MAP); + + CassandraColumnHandle columnHandle = objectMapper.readValue(json, CassandraColumnHandle.class); + + assertEquals(columnHandle.getName(), "column"); + assertEquals(columnHandle.getOrdinalPosition(), 42); + assertEquals(columnHandle.getCassandraType(), CassandraType.BIGINT); + assertEquals(columnHandle.getTypeArguments(), null); + assertEquals(columnHandle.isPartitionKey(), false); + assertEquals(columnHandle.isClusteringKey(), true); + } + + @Test + public void testColumn2HandleDeserialize() + throws Exception + { + String json = objectMapper.writeValueAsString(COLUMN2_HANDLE_AS_MAP); + + CassandraColumnHandle columnHandle = objectMapper.readValue(json, CassandraColumnHandle.class); + + assertEquals(columnHandle.getName(), "column2"); + assertEquals(columnHandle.getOrdinalPosition(), 0); + assertEquals(columnHandle.getCassandraType(), CassandraType.SET); + assertEquals(columnHandle.getTypeArguments(), ImmutableList.of(CassandraType.INT)); + assertEquals(columnHandle.isPartitionKey(), false); + assertEquals(columnHandle.isClusteringKey(), false); + } + + private void testJsonEquals(String json, Map expectedMap) + throws Exception + { + Map jsonMap = objectMapper.readValue(json, new TypeReference>() {}); + assertEqualsIgnoreOrder(jsonMap.entrySet(), expectedMap.entrySet()); + } +} diff --git a/presto-cassandra/src/test/java/com/facebook/presto/cassandra/util/TestCassandraCqlUtils.java b/presto-cassandra/src/test/java/com/facebook/presto/cassandra/util/TestCassandraCqlUtils.java new file mode 100644 index 00000000..ea3b0a2d --- /dev/null +++ b/presto-cassandra/src/test/java/com/facebook/presto/cassandra/util/TestCassandraCqlUtils.java @@ -0,0 +1,85 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra.util; + +import com.facebook.presto.cassandra.CassandraColumnHandle; +import com.facebook.presto.cassandra.CassandraType; +import com.google.common.collect.ImmutableList; +import org.testng.annotations.Test; + +import java.util.List; + +import static com.facebook.presto.cassandra.util.CassandraCqlUtils.quoteStringLiteral; +import static com.facebook.presto.cassandra.util.CassandraCqlUtils.quoteStringLiteralForJson; +import static com.facebook.presto.cassandra.util.CassandraCqlUtils.validColumnName; +import static com.facebook.presto.cassandra.util.CassandraCqlUtils.validSchemaName; +import static com.facebook.presto.cassandra.util.CassandraCqlUtils.validTableName; +import static org.testng.Assert.assertEquals; + +public class TestCassandraCqlUtils +{ + @Test + public void testValidSchemaName() + { + assertEquals("foo", validSchemaName("foo")); + assertEquals("\"select\"", validSchemaName("select")); + } + + @Test + public void testValidTableName() + { + assertEquals("foo", validTableName("foo")); + assertEquals("\"Foo\"", validTableName("Foo")); + assertEquals("\"select\"", validTableName("select")); + } + + @Test + public void testValidColumnName() + { + assertEquals("foo", validColumnName("foo")); + assertEquals("\"\"", validColumnName(CassandraCqlUtils.EMPTY_COLUMN_NAME)); + assertEquals("\"\"", validColumnName("")); + assertEquals("\"select\"", validColumnName("select")); + } + + @Test + public void testQuote() + { + assertEquals("'foo'", quoteStringLiteral("foo")); + assertEquals("'Presto''s'", quoteStringLiteral("Presto's")); + } + + @Test + public void testQuoteJson() + { + assertEquals("\"foo\"", quoteStringLiteralForJson("foo")); + assertEquals("\"Presto's\"", quoteStringLiteralForJson("Presto's")); + assertEquals("\"xx\\\"xx\"", quoteStringLiteralForJson("xx\"xx")); + } + + @Test + public void testAppendSelectColumns() + { + List columns = ImmutableList.of( + new CassandraColumnHandle("", "foo", 0, CassandraType.VARCHAR, null, false, false, false, false), + new CassandraColumnHandle("", "bar", 0, CassandraType.VARCHAR, null, false, false, false, false), + new CassandraColumnHandle("", "table", 0, CassandraType.VARCHAR, null, false, false, false, false)); + + StringBuilder sb = new StringBuilder(); + CassandraCqlUtils.appendSelectColumns(sb, columns); + String str = sb.toString(); + + assertEquals("foo,bar,\"table\"", str); + } +} diff --git a/presto-cassandra/src/test/java/com/facebook/presto/cassandra/util/TestHostAddressFactory.java b/presto-cassandra/src/test/java/com/facebook/presto/cassandra/util/TestHostAddressFactory.java new file mode 100644 index 00000000..fb341506 --- /dev/null +++ b/presto-cassandra/src/test/java/com/facebook/presto/cassandra/util/TestHostAddressFactory.java @@ -0,0 +1,49 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cassandra.util; + +import com.datastax.driver.core.Host; +import com.datastax.driver.core.TestHost; +import com.facebook.presto.spi.HostAddress; +import com.google.common.collect.ImmutableSet; +import org.testng.annotations.Test; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.util.List; +import java.util.Set; + +import static org.testng.Assert.assertEquals; + +public class TestHostAddressFactory +{ + @Test + public void testToHostAddressList() + throws Exception + { + Set hosts = ImmutableSet.of( + new TestHost( + new InetSocketAddress( + InetAddress.getByAddress(new byte[] { + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 + }), + 3000)), + new TestHost(new InetSocketAddress(InetAddress.getByAddress(new byte[] {1, 2, 3, 4}), 3000))); + + HostAddressFactory hostAddressFactory = new HostAddressFactory(); + List list = hostAddressFactory.toHostAddressList(hosts); + + assertEquals(list.toString(), "[[102:304:506:708:90a:b0c:d0e:f10], 1.2.3.4]"); + } +} diff --git a/presto-cassandra/src/test/resources/cu-cassandra.yaml b/presto-cassandra/src/test/resources/cu-cassandra.yaml new file mode 100644 index 00000000..496227d0 --- /dev/null +++ b/presto-cassandra/src/test/resources/cu-cassandra.yaml @@ -0,0 +1,564 @@ +# Cassandra storage config YAML + +# NOTE: +# See http://wiki.apache.org/cassandra/StorageConfiguration for +# full explanations of configuration directives +# /NOTE + +# The name of the cluster. This is mainly used to prevent machines in +# one logical cluster from joining another. +cluster_name: 'Test Cluster' + +# You should always specify InitialToken when setting up a production +# cluster for the first time, and often when adding capacity later. +# The principle is that each node should be given an equal slice of +# the token ring; see http://wiki.apache.org/cassandra/Operations +# for more details. +# +# If blank, Cassandra will request a token bisecting the range of +# the heaviest-loaded existing node. If there is no load information +# available, such as is the case with a new cluster, it will pick +# a random token, which will lead to hot spots. +#initial_token: + +# See http://wiki.apache.org/cassandra/HintedHandoff +hinted_handoff_enabled: true +# this defines the maximum amount of time a dead host will have hints +# generated. After it has been dead this long, new hints for it will not be +# created until it has been seen alive and gone down again. +max_hint_window_in_ms: 10800000 # 3 hours +# Maximum throttle in KBs per second, per delivery thread. This will be +# reduced proportionally to the number of nodes in the cluster. (If there +# are two nodes in the cluster, each delivery thread will use the maximum +# rate; if there are three, each will throttle to half of the maximum, +# since we expect two nodes to be delivering hints simultaneously.) +hinted_handoff_throttle_in_kb: 1024 +# Number of threads with which to deliver hints; +# Consider increasing this number when you have multi-dc deployments, since +# cross-dc handoff tends to be slower +max_hints_delivery_threads: 2 + +# The following setting populates the page cache on memtable flush and compaction +# WARNING: Enable this setting only when the whole node's data fits in memory. +# Defaults to: false +# populate_io_cache_on_flush: false + +# Authentication backend, implementing IAuthenticator; used to identify users +# Out of the box, Cassandra provides org.apache.cassandra.auth.{AllowAllAuthenticator, +# PasswordAuthenticator}. +# +# - AllowAllAuthenticator performs no checks - set it to disable authentication. +# - PasswordAuthenticator relies on username/password pairs to authenticate +# users. It keeps usernames and hashed passwords in system_auth.credentials table. +# Please increase system_auth keyspace replication factor if you use this authenticator. +authenticator: AllowAllAuthenticator + +# Authorization backend, implementing IAuthorizer; used to limit access/provide permissions +# Out of the box, Cassandra provides org.apache.cassandra.auth.{AllowAllAuthorizer, +# CassandraAuthorizer}. +# +# - AllowAllAuthorizer allows any action to any user - set it to disable authorization. +# - CassandraAuthorizer stores permissions in system_auth.permissions table. Please +# increase system_auth keyspace replication factor if you use this authorizer. +authorizer: AllowAllAuthorizer + +# Validity period for permissions cache (fetching permissions can be an +# expensive operation depending on the authorizer, CassandraAuthorizer is +# one example). Defaults to 2000, set to 0 to disable. +# Will be disabled automatically for AllowAllAuthorizer. +permissions_validity_in_ms: 2000 + + +# The partitioner is responsible for distributing rows (by key) across +# nodes in the cluster. Any IPartitioner may be used, including your +# own as long as it is on the classpath. Out of the box, Cassandra +# provides org.apache.cassandra.dht.{Murmur3Partitioner, RandomPartitioner +# ByteOrderedPartitioner, OrderPreservingPartitioner (deprecated)}. +# +# - RandomPartitioner distributes rows across the cluster evenly by md5. +# This is the default prior to 1.2 and is retained for compatibility. +# - Murmur3Partitioner is similar to RandomPartioner but uses Murmur3_128 +# Hash Function instead of md5. When in doubt, this is the best option. +# - ByteOrderedPartitioner orders rows lexically by key bytes. BOP allows +# scanning rows in key order, but the ordering can generate hot spots +# for sequential insertion workloads. +# - OrderPreservingPartitioner is an obsolete form of BOP, that stores +# - keys in a less-efficient format and only works with keys that are +# UTF8-encoded Strings. +# - CollatingOPP collates according to EN,US rules rather than lexical byte +# ordering. Use this as an example if you need custom collation. +# +# See http://wiki.apache.org/cassandra/Operations for more on +# partitioners and token selection. +partitioner: org.apache.cassandra.dht.Murmur3Partitioner + +# directories where Cassandra should store data on disk. +data_file_directories: + - target/embeddedCassandra/data + +# commit log +commitlog_directory: target/embeddedCassandra/commitlog + +# policy for data disk failures: +# stop: shut down gossip and Thrift, leaving the node effectively dead, but +# can still be inspected via JMX. +# best_effort: stop using the failed disk and respond to requests based on +# remaining available sstables. This means you WILL see obsolete +# data at CL.ONE! +# ignore: ignore fatal errors and let requests fail, as in pre-1.2 Cassandra +disk_failure_policy: stop + + +# Maximum size of the key cache in memory. +# +# Each key cache hit saves 1 seek and each row cache hit saves 2 seeks at the +# minimum, sometimes more. The key cache is fairly tiny for the amount of +# time it saves, so it's worthwhile to use it at large numbers. +# The row cache saves even more time, but must store the whole values of +# its rows, so it is extremely space-intensive. It's best to only use the +# row cache if you have hot rows or static rows. +# +# NOTE: if you reduce the size, you may not get you hottest keys loaded on startup. +# +# Default value is empty to make it "auto" (min(5% of Heap (in MB), 100MB)). Set to 0 to disable key cache. +key_cache_size_in_mb: + +# Duration in seconds after which Cassandra should +# safe the keys cache. Caches are saved to saved_caches_directory as +# specified in this configuration file. +# +# Saved caches greatly improve cold-start speeds, and is relatively cheap in +# terms of I/O for the key cache. Row cache saving is much more expensive and +# has limited use. +# +# Default is 14400 or 4 hours. +key_cache_save_period: 14400 + +# Number of keys from the key cache to save +# Disabled by default, meaning all keys are going to be saved +# key_cache_keys_to_save: 100 + +# Maximum size of the row cache in memory. +# NOTE: if you reduce the size, you may not get you hottest keys loaded on startup. +# +# Default value is 0, to disable row caching. +row_cache_size_in_mb: 0 + +# Duration in seconds after which Cassandra should +# safe the row cache. Caches are saved to saved_caches_directory as specified +# in this configuration file. +# +# Saved caches greatly improve cold-start speeds, and is relatively cheap in +# terms of I/O for the key cache. Row cache saving is much more expensive and +# has limited use. +# +# Default is 0 to disable saving the row cache. +row_cache_save_period: 0 + +# Number of keys from the row cache to save +# Disabled by default, meaning all keys are going to be saved +# row_cache_keys_to_save: 100 + +# saved caches +saved_caches_directory: target/embeddedCassandra/saved_caches + +# commitlog_sync may be either "periodic" or "batch." +# When in batch mode, Cassandra won't ack writes until the commit log +# has been fsynced to disk. It will wait up to +# commitlog_sync_batch_window_in_ms milliseconds for other writes, before +# performing the sync. +# +# commitlog_sync: batch +# commitlog_sync_batch_window_in_ms: 50 +# +# the other option is "periodic" where writes may be acked immediately +# and the CommitLog is simply synced every commitlog_sync_period_in_ms +# milliseconds. +commitlog_sync: periodic +commitlog_sync_period_in_ms: 10000 + +# The size of the individual commitlog file segments. A commitlog +# segment may be archived, deleted, or recycled once all the data +# in it (potentially from each columnfamily in the system) has been +# flushed to sstables. +# +# The default size is 32, which is almost always fine, but if you are +# archiving commitlog segments (see commitlog_archiving.properties), +# then you probably want a finer granularity of archiving; 8 or 16 MB +# is reasonable. +commitlog_segment_size_in_mb: 32 + +# any class that implements the SeedProvider interface and has a +# constructor that takes a Map of parameters will do. +seed_provider: + # Addresses of hosts that are deemed contact points. + # Cassandra nodes use this list of hosts to find each other and learn + # the topology of the ring. You must change this if you are running + # multiple nodes! + - class_name: org.apache.cassandra.locator.SimpleSeedProvider + parameters: + # seeds is actually a comma-delimited list of addresses. + # Ex: ",," + - seeds: "127.0.0.1" + + +# For workloads with more data than can fit in memory, Cassandra's +# bottleneck will be reads that need to fetch data from +# disk. "concurrent_reads" should be set to (16 * number_of_drives) in +# order to allow the operations to enqueue low enough in the stack +# that the OS and drives can reorder them. +# +# On the other hand, since writes are almost never IO bound, the ideal +# number of "concurrent_writes" is dependent on the number of cores in +# your system; (8 * number_of_cores) is a good rule of thumb. +concurrent_reads: 32 +concurrent_writes: 32 + +# Total memory to use for memtables. Cassandra will flush the largest +# memtable when this much memory is used. +# If omitted, Cassandra will set it to 1/3 of the heap. +# memtable_total_space_in_mb: 2048 + +# Total space to use for commitlogs. +# If space gets above this value (it will round up to the next nearest +# segment multiple), Cassandra will flush every dirty CF in the oldest +# segment and remove it. +# commitlog_total_space_in_mb: 4096 + +# This sets the amount of memtable flush writer threads. These will +# be blocked by disk io, and each one will hold a memtable in memory +# while blocked. If you have a large heap and many data directories, +# you can increase this value for better flush performance. +# By default this will be set to the amount of data directories defined. +#memtable_flush_writers: 1 + +# Whether to, when doing sequential writing, fsync() at intervals in +# order to force the operating system to flush the dirty +# buffers. Enable this to avoid sudden dirty buffer flushing from +# impacting read latencies. Almost always a good idea on SSD:s; not +# necessarily on platters. +trickle_fsync: false +trickle_fsync_interval_in_kb: 10240 + +# TCP port, for commands and data +storage_port: 7010 + +# SSL port, for encrypted communication. Unused unless enabled in +# encryption_options +ssl_storage_port: 7011 + +# Address to bind to and tell other Cassandra nodes to connect to. You +# _must_ change this if you want multiple nodes to be able to +# communicate! +# +# Leaving it blank leaves it up to InetAddress.getLocalHost(). This +# will always do the Right Thing *if* the node is properly configured +# (hostname, name resolution, etc), and the Right Thing is to use the +# address associated with the hostname (it might not be). +# +# Setting this to 0.0.0.0 is always wrong. +listen_address: 127.0.0.1 + +start_native_transport: true +# port for the CQL native transport to listen for clients on +native_transport_port: 9142 + +# Whether to start the thrift rpc server. +start_rpc: true + +# Address to broadcast to other Cassandra nodes +# Leaving this blank will set it to the same value as listen_address +# broadcast_address: 1.2.3.4 + +# The address to bind the Thrift RPC service to -- clients connect +# here. Unlike ListenAddress above, you *can* specify 0.0.0.0 here if +# you want Thrift to listen on all interfaces. +# +# Leaving this blank has the same effect it does for ListenAddress, +# (i.e. it will be based on the configured hostname of the node). +rpc_address: localhost +# port for Thrift to listen for clients on +rpc_port: 9160 + +# enable or disable keepalive on rpc connections +rpc_keepalive: true + +# Cassandra provides three options for the RPC Server: +# +# sync -> One connection per thread in the rpc pool (see below). +# For a very large number of clients, memory will be your limiting +# factor; on a 64 bit JVM, 128KB is the minimum stack size per thread. +# Connection pooling is very, very strongly recommended. +# +# async -> Nonblocking server implementation with one thread to serve +# rpc connections. This is not recommended for high throughput use +# cases. Async has been tested to be about 50% slower than sync +# or hsha and is deprecated: it will be removed in the next major release. +# +# hsha -> Stands for "half synchronous, half asynchronous." The rpc thread pool +# (see below) is used to manage requests, but the threads are multiplexed +# across the different clients. +# +# The default is sync because on Windows hsha is about 30% slower. On Linux, +# sync/hsha performance is about the same, with hsha of course using less memory. +rpc_server_type: sync + +# Uncomment rpc_min|max|thread to set request pool size. +# You would primarily set max for the sync server to safeguard against +# misbehaved clients; if you do hit the max, Cassandra will block until one +# disconnects before accepting more. The defaults for sync are min of 16 and max +# unlimited. +# +# For the Hsha server, the min and max both default to quadruple the number of +# CPU cores. +# +# This configuration is ignored by the async server. +# +# rpc_min_threads: 16 +# rpc_max_threads: 2048 + +# uncomment to set socket buffer sizes on rpc connections +# rpc_send_buff_size_in_bytes: +# rpc_recv_buff_size_in_bytes: + +# Frame size for thrift (maximum field length). +# 0 disables TFramedTransport in favor of TSocket. This option +# is deprecated; we strongly recommend using Framed mode. +thrift_framed_transport_size_in_mb: 15 + +# The max length of a thrift message, including all fields and +# internal thrift overhead. +thrift_max_message_length_in_mb: 16 + +# Set to true to have Cassandra create a hard link to each sstable +# flushed or streamed locally in a backups/ subdirectory of the +# Keyspace data. Removing these links is the operator's +# responsibility. +incremental_backups: false + +# Whether or not to take a snapshot before each compaction. Be +# careful using this option, since Cassandra won't clean up the +# snapshots for you. Mostly useful if you're paranoid when there +# is a data format change. +snapshot_before_compaction: false + +# Whether or not a snapshot is taken of the data before keyspace truncation +# or dropping of column families. The STRONGLY advised default of true +# should be used to provide data safety. If you set this flag to false, you will +# lose data on truncation or drop. +auto_snapshot: false + +# Add column indexes to a row after its contents reach this size. +# Increase if your column values are large, or if you have a very large +# number of columns. The competing causes are, Cassandra has to +# deserialize this much of the row to read a single column, so you want +# it to be small - at least if you do many partial-row reads - but all +# the index data is read for each access, so you don't want to generate +# that wastefully either. +column_index_size_in_kb: 64 + +# Number of simultaneous compactions to allow, NOT including +# validation "compactions" for anti-entropy repair. Simultaneous +# compactions can help preserve read performance in a mixed read/write +# workload, by mitigating the tendency of small sstables to accumulate +# during a single long running compactions. The default is usually +# fine and if you experience problems with compaction running too +# slowly or too fast, you should look at +# compaction_throughput_mb_per_sec first. +# +# This setting has no effect on LeveledCompactionStrategy. +# +# concurrent_compactors defaults to the number of cores. +# Uncomment to make compaction mono-threaded, the pre-0.8 default. +#concurrent_compactors: 1 + +# Throttles compaction to the given total throughput across the entire +# system. The faster you insert data, the faster you need to compact in +# order to keep the sstable count down, but in general, setting this to +# 16 to 32 times the rate you are inserting data is more than sufficient. +# Setting this to 0 disables throttling. Note that this account for all types +# of compaction, including validation compaction. +compaction_throughput_mb_per_sec: 16 + +# Throttles all outbound streaming file transfers on this node to the +# given total throughput in Mbps. This is necessary because Cassandra does +# mostly sequential IO when streaming data during bootstrap or repair, which +# can lead to saturating the network connection and degrading rpc performance. +# When unset, the default is 200 Mbps or 25 MB/s. +# stream_throughput_outbound_megabits_per_sec: 200 + +# How long the coordinator should wait for read operations to complete +read_request_timeout_in_ms: 30000 +# How long the coordinator should wait for seq or index scans to complete +range_request_timeout_in_ms: 30000 +# How long the coordinator should wait for writes to complete +write_request_timeout_in_ms: 30000 +# How long a coordinator should continue to retry a CAS operation +# that contends with other proposals for the same row +cas_contention_timeout_in_ms: 30000 +# How long the coordinator should wait for truncates to complete +# (This can be much longer, because unless auto_snapshot is disabled +# we need to flush first so we can snapshot before removing the data.) +truncate_request_timeout_in_ms: 60000 +# The default timeout for other, miscellaneous operations +request_timeout_in_ms: 30000 + +# Enable operation timeout information exchange between nodes to accurately +# measure request timeouts. If disabled, replicas will assume that requests +# were forwarded to them instantly by the coordinator, which means that +# under overload conditions we will waste that much extra time processing +# already-timed-out requests. +# +# Warning: before enabling this property make sure to ntp is installed +# and the times are synchronized between the nodes. +cross_node_timeout: false + +# Enable socket timeout for streaming operation. +# When a timeout occurs during streaming, streaming is retried from the start +# of the current file. This _can_ involve re-streaming an important amount of +# data, so you should avoid setting the value too low. +# Default value is 0, which never timeout streams. +# streaming_socket_timeout_in_ms: 0 + +# phi value that must be reached for a host to be marked down. +# most users should never need to adjust this. +# phi_convict_threshold: 8 + +# endpoint_snitch -- Set this to a class that implements +# IEndpointSnitch. The snitch has two functions: +# - it teaches Cassandra enough about your network topology to route +# requests efficiently +# - it allows Cassandra to spread replicas around your cluster to avoid +# correlated failures. It does this by grouping machines into +# "datacenters" and "racks." Cassandra will do its best not to have +# more than one replica on the same "rack" (which may not actually +# be a physical location) +# +# IF YOU CHANGE THE SNITCH AFTER DATA IS INSERTED INTO THE CLUSTER, +# YOU MUST RUN A FULL REPAIR, SINCE THE SNITCH AFFECTS WHERE REPLICAS +# ARE PLACED. +# +# Out of the box, Cassandra provides +# - SimpleSnitch: +# Treats Strategy order as proximity. This improves cache locality +# when disabling read repair, which can further improve throughput. +# Only appropriate for single-datacenter deployments. +# - PropertyFileSnitch: +# Proximity is determined by rack and data center, which are +# explicitly configured in cassandra-topology.properties. +# - RackInferringSnitch: +# Proximity is determined by rack and data center, which are +# assumed to correspond to the 3rd and 2nd octet of each node's +# IP address, respectively. Unless this happens to match your +# deployment conventions (as it did Facebook's), this is best used +# as an example of writing a custom Snitch class. +# - Ec2Snitch: +# Appropriate for EC2 deployments in a single Region. Loads Region +# and Availability Zone information from the EC2 API. The Region is +# treated as the Datacenter, and the Availability Zone as the rack. +# Only private IPs are used, so this will not work across multiple +# Regions. +# - Ec2MultiRegionSnitch: +# Uses public IPs as broadcast_address to allow cross-region +# connectivity. (Thus, you should set seed addresses to the public +# IP as well.) You will need to open the storage_port or +# ssl_storage_port on the public IP firewall. (For intra-Region +# traffic, Cassandra will switch to the private IP after +# establishing a connection.) +# +# You can use a custom Snitch by setting this to the full class name +# of the snitch, which will be assumed to be on your classpath. +endpoint_snitch: SimpleSnitch + +# controls how often to perform the more expensive part of host score +# calculation +dynamic_snitch_update_interval_in_ms: 100 +# controls how often to reset all host scores, allowing a bad host to +# possibly recover +dynamic_snitch_reset_interval_in_ms: 600000 +# if set greater than zero and read_repair_chance is < 1.0, this will allow +# 'pinning' of replicas to hosts in order to increase cache capacity. +# The badness threshold will control how much worse the pinned host has to be +# before the dynamic snitch will prefer other replicas over it. This is +# expressed as a double which represents a percentage. Thus, a value of +# 0.2 means Cassandra would continue to prefer the static snitch values +# until the pinned host was 20% worse than the fastest. +dynamic_snitch_badness_threshold: 0.1 + +# request_scheduler -- Set this to a class that implements +# RequestScheduler, which will schedule incoming client requests +# according to the specific policy. This is useful for multi-tenancy +# with a single Cassandra cluster. +# NOTE: This is specifically for requests from the client and does +# not affect inter node communication. +# org.apache.cassandra.scheduler.NoScheduler - No scheduling takes place +# org.apache.cassandra.scheduler.RoundRobinScheduler - Round robin of +# client requests to a node with a separate queue for each +# request_scheduler_id. The scheduler is further customized by +# request_scheduler_options as described below. +request_scheduler: org.apache.cassandra.scheduler.NoScheduler + +# Scheduler Options vary based on the type of scheduler +# NoScheduler - Has no options +# RoundRobin +# - throttle_limit -- The throttle_limit is the number of in-flight +# requests per client. Requests beyond +# that limit are queued up until +# running requests can complete. +# The value of 80 here is twice the number of +# concurrent_reads + concurrent_writes. +# - default_weight -- default_weight is optional and allows for +# overriding the default which is 1. +# - weights -- Weights are optional and will default to 1 or the +# overridden default_weight. The weight translates into how +# many requests are handled during each turn of the +# RoundRobin, based on the scheduler id. +# +# request_scheduler_options: +# throttle_limit: 80 +# default_weight: 5 +# weights: +# Keyspace1: 1 +# Keyspace2: 5 + +# request_scheduler_id -- An identifer based on which to perform +# the request scheduling. Currently the only valid option is keyspace. +# request_scheduler_id: keyspace + +# index_interval controls the sampling of entries from the primrary +# row index in terms of space versus time. The larger the interval, +# the smaller and less effective the sampling will be. In technicial +# terms, the interval coresponds to the number of index entries that +# are skipped between taking each sample. All the sampled entries +# must fit in memory. Generally, a value between 128 and 512 here +# coupled with a large key cache size on CFs results in the best trade +# offs. This value is not often changed, however if you have many +# very small rows (many to an OS page), then increasing this will +# often lower memory usage without a impact on performance. +index_interval: 128 + +# Enable or disable inter-node encryption +# Default settings are TLS v1, RSA 1024-bit keys (it is imperative that +# users generate their own keys) TLS_RSA_WITH_AES_128_CBC_SHA as the cipher +# suite for authentication, key exchange and encryption of the actual data transfers. +# NOTE: No custom encryption options are enabled at the moment +# The available internode options are : all, none, dc, rack +# +# If set to dc cassandra will encrypt the traffic between the DCs +# If set to rack cassandra will encrypt the traffic between the racks +# +# The passwords used in these options must match the passwords used when generating +# the keystore and truststore. For instructions on generating these files, see: +# http://download.oracle.com/javase/6/docs/technotes/guides/security/jsse/JSSERefGuide.html#CreateKeystore +# +encryption_options: + internode_encryption: none + keystore: conf/.keystore + keystore_password: cassandra + truststore: conf/.truststore + truststore_password: cassandra + # More advanced defaults below: + # protocol: TLS + # algorithm: SunX509 + # store_type: JKS + # cipher_suites: [TLS_RSA_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_AES_256_CBC_SHA] diff --git a/presto-cli/pom.xml b/presto-cli/pom.xml new file mode 100644 index 00000000..af471f68 --- /dev/null +++ b/presto-cli/pom.xml @@ -0,0 +1,152 @@ + + + 4.0.0 + + + com.facebook.presto + presto-root + 0.107 + + + presto-cli + presto-cli + + + ${project.parent.basedir} + com.facebook.presto.cli.Presto + false + ${main-class} + + + + + com.facebook.presto + presto-client + + + + com.facebook.presto + presto-parser + + + + io.airlift + airline + + + + io.airlift + concurrent + + + + io.airlift + http-client + + + + io.airlift + json + + + + io.airlift + log + + + + io.airlift + log-manager + + + + io.airlift + units + + + + com.google.code.findbugs + annotations + + + + javax.inject + javax.inject + + + + com.google.guava + guava + + + + jline + jline + + + + + + net.sf.opencsv + opencsv + + + + + org.testng + testng + test + + + + + + + org.apache.maven.plugins + maven-shade-plugin + + + package + + shade + + + true + executable + + + + ${main-class} + + + + + + + + + + org.skife.maven + really-executable-jar-maven-plugin + + -Xmx1G + executable + + + + package + + really-executable-jar + + + + + + + diff --git a/presto-cli/src/main/java/com/facebook/presto/cli/AlignedTablePrinter.java b/presto-cli/src/main/java/com/facebook/presto/cli/AlignedTablePrinter.java new file mode 100644 index 00000000..0178e635 --- /dev/null +++ b/presto-cli/src/main/java/com/facebook/presto/cli/AlignedTablePrinter.java @@ -0,0 +1,192 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cli; + +import com.google.common.base.Joiner; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; +import org.fusesource.jansi.AnsiString; + +import java.io.IOException; +import java.io.Writer; +import java.util.ArrayList; +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Strings.repeat; +import static com.google.common.collect.Iterables.partition; +import static com.google.common.collect.Iterables.transform; +import static com.google.common.io.BaseEncoding.base16; +import static java.lang.Math.max; +import static java.lang.String.format; + +public class AlignedTablePrinter + implements OutputPrinter +{ + private static final Splitter LINE_SPLITTER = Splitter.on('\n'); + private static final Splitter HEX_SPLITTER = Splitter.fixedLength(2); + private static final Joiner HEX_BYTE_JOINER = Joiner.on(' '); + private static final Joiner HEX_LINE_JOINER = Joiner.on('\n'); + + private final List fieldNames; + private final Writer writer; + + private boolean headerOutput; + private long rowCount; + + public AlignedTablePrinter(List fieldNames, Writer writer) + { + this.fieldNames = ImmutableList.copyOf(checkNotNull(fieldNames, "fieldNames is null")); + this.writer = checkNotNull(writer, "writer is null"); + } + + @Override + public void finish() + throws IOException + { + printRows(ImmutableList.>of(), true); + writer.append(format("(%s row%s)%n", rowCount, (rowCount != 1) ? "s" : "")); + writer.flush(); + } + + @Override + public void printRows(List> rows, boolean complete) + throws IOException + { + rowCount += rows.size(); + int columns = fieldNames.size(); + + int[] maxWidth = new int[columns]; + for (int i = 0; i < columns; i++) { + maxWidth[i] = max(1, fieldNames.get(i).length()); + } + for (List row : rows) { + for (int i = 0; i < row.size(); i++) { + String s = formatValue(row.get(i)); + maxWidth[i] = max(maxWidth[i], maxLineLength(s)); + } + } + + if (!headerOutput) { + headerOutput = true; + + for (int i = 0; i < columns; i++) { + if (i > 0) { + writer.append('|'); + } + String name = fieldNames.get(i); + writer.append(center(name, maxWidth[i], 1)); + } + writer.append('\n'); + + for (int i = 0; i < columns; i++) { + if (i > 0) { + writer.append('+'); + } + writer.append(repeat("-", maxWidth[i] + 2)); + } + writer.append('\n'); + } + + for (List row : rows) { + List> columnLines = new ArrayList<>(columns); + int maxLines = 1; + for (int i = 0; i < columns; i++) { + String s = formatValue(row.get(i)); + ImmutableList lines = ImmutableList.copyOf(LINE_SPLITTER.split(s)); + columnLines.add(lines); + maxLines = max(maxLines, lines.size()); + } + + for (int line = 0; line < maxLines; line++) { + for (int column = 0; column < columns; column++) { + if (column > 0) { + writer.append('|'); + } + List lines = columnLines.get(column); + String s = (line < lines.size()) ? lines.get(line) : ""; + boolean numeric = row.get(column) instanceof Number; + String out = align(s, maxWidth[column], 1, numeric); + if ((!complete || (rowCount > 1)) && ((line + 1) < lines.size())) { + out = out.substring(0, out.length() - 1) + "+"; + } + writer.append(out); + } + writer.append('\n'); + } + } + + writer.flush(); + } + + static String formatValue(Object o) + { + if (o == null) { + return "NULL"; + } + + if (o instanceof byte[]) { + return formatHexDump((byte[]) o, 16); + } + + return o.toString(); + } + + private static String formatHexDump(byte[] bytes, int bytesPerLine) + { + // hex dump: "616263" + String hexDump = base16().lowerCase().encode(bytes); + + // hex pairs: ["61", "62", "63"] + Iterable hexPairs = HEX_SPLITTER.split(hexDump); + + // hex lines: [["61", "62", "63], [...]] + Iterable> hexLines = partition(hexPairs, bytesPerLine); + + // lines: ["61 62 63", ...] + Iterable lines = transform(hexLines, HEX_BYTE_JOINER::join); + + // joined: "61 62 63\n..." + return HEX_LINE_JOINER.join(lines); + } + + private static String center(String s, int maxWidth, int padding) + { + AnsiString ansiString = new AnsiString(s); + + checkState(ansiString.length() <= maxWidth, "string length is greater than max width"); + int left = (maxWidth - ansiString.length()) / 2; + int right = maxWidth - (left + ansiString.length()); + return repeat(" ", left + padding) + s + repeat(" ", right + padding); + } + + private static String align(String s, int maxWidth, int padding, boolean right) + { + AnsiString ansiString = new AnsiString(s); + checkState(ansiString.length() <= maxWidth, "string length is greater than max width"); + String large = repeat(" ", (maxWidth - ansiString.length()) + padding); + String small = repeat(" ", padding); + return right ? (large + s + small) : (small + s + large); + } + + static int maxLineLength(String s) + { + int n = 0; + for (String line : LINE_SPLITTER.split(s)) { + n = max(n, new AnsiString(line).length()); + } + return n; + } +} diff --git a/presto-cli/src/main/java/com/facebook/presto/cli/ClientOptions.java b/presto-cli/src/main/java/com/facebook/presto/cli/ClientOptions.java new file mode 100644 index 00000000..cb1df2c9 --- /dev/null +++ b/presto-cli/src/main/java/com/facebook/presto/cli/ClientOptions.java @@ -0,0 +1,219 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cli; + +import com.facebook.presto.client.ClientSession; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableMap; +import com.google.common.net.HostAndPort; +import io.airlift.command.Option; + +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.CharsetEncoder; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.TimeZone; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.nio.charset.StandardCharsets.US_ASCII; +import static java.util.Locale.ENGLISH; + +public class ClientOptions +{ + @Option(name = "--server", title = "server", description = "Presto server location (default: localhost:8080)") + public String server = "localhost:8080"; + + @Option(name = "--user", title = "user", description = "Username") + public String user = System.getProperty("user.name"); + + @Option(name = "--source", title = "source", description = "Name of source making query") + public String source = "presto-cli"; + + @Option(name = "--catalog", title = "catalog", description = "Default catalog") + public String catalog = "default"; + + @Option(name = "--schema", title = "schema", description = "Default schema") + public String schema = "default"; + + @Option(name = {"-f", "--file"}, title = "file", description = "Execute statements from file and exit") + public String file; + + @Option(name = "--debug", title = "debug", description = "Enable debug information") + public boolean debug; + + @Option(name = "--execute", title = "execute", description = "Execute specified statements and exit") + public String execute; + + @Option(name = "--output-format", title = "output-format", description = "Output format for batch mode (default: CSV)") + public OutputFormat outputFormat = OutputFormat.CSV; + + @Option(name = "--session", title = "session", description = "Session property (property can be used multiple times; format is key=value)") + public final List sessionProperties = new ArrayList<>(); + + @Option(name = "--socks-proxy", title = "socks-proxy", description = "SOCKS proxy to use for server connections") + public HostAndPort socksProxy; + + public enum OutputFormat + { + ALIGNED, + VERTICAL, + CSV, + TSV, + CSV_HEADER, + TSV_HEADER, + NULL + } + + public ClientSession toClientSession() + { + return new ClientSession( + parseServer(server), + user, + source, + catalog, + schema, + TimeZone.getDefault().getID(), + Locale.getDefault(), + toProperties(sessionProperties), + debug); + } + + public static URI parseServer(String server) + { + server = server.toLowerCase(ENGLISH); + if (server.startsWith("http://") || server.startsWith("https://")) { + return URI.create(server); + } + + HostAndPort host = HostAndPort.fromString(server); + try { + return new URI("http", null, host.getHostText(), host.getPortOrDefault(80), null, null, null); + } + catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } + } + + public static Map toProperties(List sessionProperties) + { + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (ClientSessionProperty sessionProperty : sessionProperties) { + String name = sessionProperty.getName(); + if (sessionProperty.getCatalog().isPresent()) { + name = sessionProperty.getCatalog().get() + "." + name; + } + builder.put(name, sessionProperty.getValue()); + } + return builder.build(); + } + + public static final class ClientSessionProperty + { + private static final Splitter NAME_VALUE_SPLITTER = Splitter.on('=').limit(2); + private static final Splitter NAME_SPLITTER = Splitter.on('.'); + private final Optional catalog; + private final String name; + private final String value; + + public ClientSessionProperty(String property) + { + List nameValue = NAME_VALUE_SPLITTER.splitToList(property); + checkArgument(nameValue.size() == 2, "Session property: %s", property); + + List nameParts = NAME_SPLITTER.splitToList(nameValue.get(0)); + checkArgument(nameParts.size() == 1 || nameParts.size() == 2, "Invalid session property: %s", property); + if (nameParts.size() == 1) { + catalog = Optional.empty(); + name = nameParts.get(0); + } + else { + catalog = Optional.of(nameParts.get(0)); + name = nameParts.get(1); + } + value = nameValue.get(1); + + verifyProperty(catalog, name, value); + } + + public ClientSessionProperty(Optional catalog, String name, String value) + { + this.catalog = checkNotNull(catalog, "catalog is null"); + this.name = checkNotNull(name, "name is null"); + this.value = checkNotNull(value, "value is null"); + + verifyProperty(catalog, name, value); + } + + private static void verifyProperty(Optional catalog, String name, String value) + { + checkArgument(!catalog.isPresent() || !catalog.get().isEmpty(), "Invalid session property: %s.%s:%s", catalog, name, value); + checkArgument(!name.isEmpty(), "Session property name is empty"); + + CharsetEncoder charsetEncoder = US_ASCII.newEncoder(); + checkArgument(catalog.orElse("").indexOf('=') < 0, "Session property catalog must not contain '=': %s", name); + checkArgument(charsetEncoder.canEncode(catalog.orElse("")), "Session property catalog is not US_ASCII: %s", name); + checkArgument(name.indexOf('=') < 0, "Session property name must not contain '=': %s", name); + checkArgument(charsetEncoder.canEncode(name), "Session property name is not US_ASCII: %s", name); + checkArgument(charsetEncoder.canEncode(value), "Session property value is not US_ASCII: %s", value); + } + + public Optional getCatalog() + { + return catalog; + } + + public String getName() + { + return name; + } + + public String getValue() + { + return value; + } + + @Override + public String toString() + { + return (catalog.isPresent() ? catalog.get() + '.' : "") + name + '=' + value; + } + + @Override + public int hashCode() + { + return Objects.hash(catalog, name, value); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + ClientSessionProperty other = (ClientSessionProperty) obj; + return Objects.equals(this.catalog, other.catalog) && + Objects.equals(this.name, other.name) && + Objects.equals(this.value, other.value); + } + } +} diff --git a/presto-cli/src/main/java/com/facebook/presto/cli/Completion.java b/presto-cli/src/main/java/com/facebook/presto/cli/Completion.java new file mode 100644 index 00000000..100c33d5 --- /dev/null +++ b/presto-cli/src/main/java/com/facebook/presto/cli/Completion.java @@ -0,0 +1,58 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cli; + +import com.google.common.collect.ImmutableSet; +import jline.console.completer.Completer; +import jline.console.completer.StringsCompleter; + +import java.util.Locale; +import java.util.Set; + +import static java.util.stream.Collectors.toSet; + +public final class Completion +{ + private static final Set COMMANDS = ImmutableSet.of( + "SELECT", + "SHOW CATALOGS", + "SHOW COLUMNS", + "SHOW FUNCTIONS", + "SHOW PARTITIONS", + "SHOW SCHEMAS", + "SHOW SESSION", + "SHOW TABLES", + "CREATE TABLE", + "DROP TABLE", + "EXPLAIN", + "DESCRIBE", + "USE", + "HELP", + "QUIT"); + + private Completion() {} + + public static Completer commandCompleter() + { + return new StringsCompleter(COMMANDS); + } + + // TODO: create a case-insensitive completer + public static Completer lowerCaseCommandCompleter() + { + return new StringsCompleter(COMMANDS.stream() + .map(s -> s.toLowerCase(Locale.ENGLISH)) + .collect(toSet())); + } +} diff --git a/presto-cli/src/main/java/com/facebook/presto/cli/Console.java b/presto-cli/src/main/java/com/facebook/presto/cli/Console.java new file mode 100644 index 00000000..4dcb9987 --- /dev/null +++ b/presto-cli/src/main/java/com/facebook/presto/cli/Console.java @@ -0,0 +1,336 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cli; + +import com.facebook.presto.cli.ClientOptions.OutputFormat; +import com.facebook.presto.client.ClientSession; +import com.facebook.presto.sql.parser.IdentifierSymbol; +import com.facebook.presto.sql.parser.ParsingException; +import com.facebook.presto.sql.parser.SqlParser; +import com.facebook.presto.sql.parser.SqlParserOptions; +import com.facebook.presto.sql.parser.StatementSplitter; +import com.facebook.presto.sql.tree.Use; +import com.google.common.base.Strings; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableSet; +import com.google.common.io.Files; +import io.airlift.command.Command; +import io.airlift.command.HelpOption; +import io.airlift.log.Level; +import io.airlift.log.Logging; +import io.airlift.log.LoggingConfiguration; +import jline.console.history.FileHistory; +import jline.console.history.History; +import jline.console.history.MemoryHistory; +import org.fusesource.jansi.AnsiConsole; + +import javax.inject.Inject; + +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Pattern; + +import static com.facebook.presto.cli.Completion.commandCompleter; +import static com.facebook.presto.cli.Completion.lowerCaseCommandCompleter; +import static com.facebook.presto.cli.Help.getHelpText; +import static com.facebook.presto.client.ClientSession.withProperties; +import static com.facebook.presto.sql.parser.StatementSplitter.Statement; +import static com.facebook.presto.sql.parser.StatementSplitter.isEmptyStatement; +import static com.facebook.presto.sql.parser.StatementSplitter.squeezeStatement; +import static com.google.common.io.ByteStreams.nullOutputStream; +import static java.lang.Integer.parseInt; +import static java.lang.String.format; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Locale.ENGLISH; +import static jline.internal.Configuration.getUserHome; + +@Command(name = "presto", description = "Presto interactive console") +public class Console + implements Runnable +{ + private static final String PROMPT_NAME = "presto"; + + // create a parser with all identifier options enabled, since this is only used for USE statements + private static final SqlParser SQL_PARSER = new SqlParser(new SqlParserOptions().allowIdentifierSymbol(EnumSet.allOf(IdentifierSymbol.class))); + + private static final Pattern HISTORY_INDEX_PATTERN = Pattern.compile("!\\d+"); + + @Inject + public HelpOption helpOption; + + @Inject + public VersionOption versionOption = new VersionOption(); + + @Inject + public ClientOptions clientOptions = new ClientOptions(); + + @Override + public void run() + { + ClientSession session = clientOptions.toClientSession(); + boolean hasQuery = !Strings.isNullOrEmpty(clientOptions.execute); + boolean isFromFile = !Strings.isNullOrEmpty(clientOptions.file); + + if (!hasQuery || !isFromFile) { + AnsiConsole.systemInstall(); + } + + initializeLogging(session.isDebug()); + + String query = clientOptions.execute; + if (hasQuery) { + query += ";"; + } + + if (isFromFile) { + if (hasQuery) { + throw new RuntimeException("both --execute and --file specified"); + } + try { + query = Files.toString(new File(clientOptions.file), UTF_8); + hasQuery = true; + } + catch (IOException e) { + throw new RuntimeException(format("Error reading from file %s: %s", clientOptions.file, e.getMessage())); + } + } + + try (QueryRunner queryRunner = QueryRunner.create(session, Optional.ofNullable(clientOptions.socksProxy))) { + if (hasQuery) { + executeCommand(queryRunner, query, clientOptions.outputFormat); + } + else { + runConsole(queryRunner, session); + } + } + } + + private static void runConsole(QueryRunner queryRunner, ClientSession session) + { + try (TableNameCompleter tableNameCompleter = new TableNameCompleter(queryRunner); + LineReader reader = new LineReader(getHistory(), commandCompleter(), lowerCaseCommandCompleter(), tableNameCompleter)) { + tableNameCompleter.populateCache(); + StringBuilder buffer = new StringBuilder(); + while (true) { + // read a line of input from user + String prompt = PROMPT_NAME + ":" + session.getSchema(); + if (buffer.length() > 0) { + prompt = Strings.repeat(" ", prompt.length() - 1) + "-"; + } + String commandPrompt = prompt + "> "; + String line = reader.readLine(commandPrompt); + + // add buffer to history and clear on user interrupt + if (reader.interrupted()) { + String partial = squeezeStatement(buffer.toString()); + if (!partial.isEmpty()) { + reader.getHistory().add(partial); + } + buffer = new StringBuilder(); + continue; + } + + // exit on EOF + if (line == null) { + System.out.println(); + return; + } + + // check for special commands if this is the first line + if (buffer.length() == 0) { + String command = line.trim(); + + if (HISTORY_INDEX_PATTERN.matcher(command).matches()) { + int historyIndex = parseInt(command.substring(1)); + History history = reader.getHistory(); + if ((historyIndex <= 0) || (historyIndex > history.index())) { + System.err.println("Command does not exist"); + continue; + } + line = history.get(historyIndex - 1).toString(); + System.out.println(commandPrompt + line); + } + + if (command.endsWith(";")) { + command = command.substring(0, command.length() - 1).trim(); + } + + switch (command.toLowerCase(ENGLISH)) { + case "exit": + case "quit": + return; + case "history": + for (History.Entry entry : reader.getHistory()) { + System.out.printf("%5d %s%n", entry.index() + 1, entry.value()); + } + continue; + case "help": + System.out.println(); + System.out.println(getHelpText()); + continue; + } + } + + // not a command, add line to buffer + buffer.append(line).append("\n"); + + // execute any complete statements + String sql = buffer.toString(); + StatementSplitter splitter = new StatementSplitter(sql, ImmutableSet.of(";", "\\G")); + for (Statement split : splitter.getCompleteStatements()) { + Optional statement = getParsedStatement(split.statement()); + if (statement.isPresent() && isSessionParameterChange(statement.get())) { + session = processSessionParameterChange(statement.get(), session); + queryRunner.setSession(session); + tableNameCompleter.populateCache(); + } + else { + OutputFormat outputFormat = OutputFormat.ALIGNED; + if (split.terminator().equals("\\G")) { + outputFormat = OutputFormat.VERTICAL; + } + + process(queryRunner, split.statement(), outputFormat, true); + } + reader.getHistory().add(squeezeStatement(split.statement()) + split.terminator()); + } + + // replace buffer with trailing partial statement + buffer = new StringBuilder(); + String partial = splitter.getPartialStatement(); + if (!partial.isEmpty()) { + buffer.append(partial).append('\n'); + } + } + } + catch (IOException e) { + System.err.println("Readline error: " + e.getMessage()); + } + } + + private static Optional getParsedStatement(String statement) + { + try { + return Optional.of((Object) SQL_PARSER.createStatement(statement)); + } + catch (ParsingException e) { + return Optional.empty(); + } + } + + static ClientSession processSessionParameterChange(Object parsedStatement, ClientSession session) + { + if (parsedStatement instanceof Use) { + Use use = (Use) parsedStatement; + return ClientSession.withCatalogAndSchema(session, use.getCatalog().orElse(session.getCatalog()), use.getSchema()); + } + return session; + } + + private static boolean isSessionParameterChange(Object statement) + { + return statement instanceof Use; + } + + private static void executeCommand(QueryRunner queryRunner, String query, OutputFormat outputFormat) + { + StatementSplitter splitter = new StatementSplitter(query); + for (Statement split : splitter.getCompleteStatements()) { + if (!isEmptyStatement(split.statement())) { + process(queryRunner, split.statement(), outputFormat, false); + } + } + if (!isEmptyStatement(splitter.getPartialStatement())) { + System.err.println("Non-terminated statement: " + splitter.getPartialStatement()); + } + } + + private static void process(QueryRunner queryRunner, String sql, OutputFormat outputFormat, boolean interactive) + { + try (Query query = queryRunner.startQuery(sql)) { + query.renderOutput(System.out, outputFormat, interactive); + + // update session properties if present + if (!query.getSetSessionProperties().isEmpty() || !query.getResetSessionProperties().isEmpty()) { + Map sessionProperties = new HashMap<>(queryRunner.getSession().getProperties()); + sessionProperties.putAll(query.getSetSessionProperties()); + sessionProperties.keySet().removeAll(query.getResetSessionProperties()); + queryRunner.setSession(withProperties(queryRunner.getSession(), sessionProperties)); + } + } + catch (RuntimeException e) { + System.out.println("Error running command: " + e.getMessage()); + if (queryRunner.getSession().isDebug()) { + e.printStackTrace(); + } + } + } + + private static MemoryHistory getHistory() + { + MemoryHistory history; + File historyFile = new File(getUserHome(), ".presto_history"); + try { + history = new FileHistory(historyFile); + history.setMaxSize(10000); + } + catch (IOException e) { + System.err.printf("WARNING: Failed to load history file (%s): %s. " + + "History will not be available during this session.%n", + historyFile, e.getMessage()); + history = new MemoryHistory(); + } + history.setAutoTrim(true); + return history; + } + + public static void initializeLogging(boolean debug) + { + // unhook out and err while initializing logging or logger will print to them + PrintStream out = System.out; + PrintStream err = System.err; + try { + if (debug) { + Logging logging = Logging.initialize(); + logging.configure(new LoggingConfiguration()); + logging.setLevel("com.facebook.presto", Level.DEBUG); + } + else { + System.setOut(nullPrintStream()); + System.setErr(nullPrintStream()); + + Logging logging = Logging.initialize(); + logging.configure(new LoggingConfiguration()); + logging.disableConsole(); + } + } + catch (IOException e) { + throw Throwables.propagate(e); + } + finally { + System.setOut(out); + System.setErr(err); + } + } + + public static PrintStream nullPrintStream() + { + return new PrintStream(nullOutputStream()); + } +} diff --git a/presto-cli/src/main/java/com/facebook/presto/cli/ConsolePrinter.java b/presto-cli/src/main/java/com/facebook/presto/cli/ConsolePrinter.java new file mode 100644 index 00000000..035581b8 --- /dev/null +++ b/presto-cli/src/main/java/com/facebook/presto/cli/ConsolePrinter.java @@ -0,0 +1,121 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cli; + +import jline.TerminalFactory; + +import java.io.PrintStream; + +import static com.google.common.base.Preconditions.checkNotNull; +import static org.fusesource.jansi.Ansi.Erase; +import static org.fusesource.jansi.Ansi.ansi; +import static org.fusesource.jansi.internal.CLibrary.STDOUT_FILENO; +import static org.fusesource.jansi.internal.CLibrary.isatty; + +public class ConsolePrinter +{ + public static final boolean REAL_TERMINAL = detectRealTerminal(); + + private final PrintStream out; + private int lines; + + public ConsolePrinter(PrintStream out) + { + this.out = checkNotNull(out, "out is null"); + } + + public void reprintLine(String line) + { + if (isRealTerminal()) { + out.print(ansi().eraseLine(Erase.ALL).a(line).a('\n').toString()); + } + else { + out.print('\r' + line); + } + out.flush(); + lines++; + } + + public void repositionCursor() + { + if (lines > 0) { + if (isRealTerminal()) { + out.print(ansi().cursorUp(lines).toString()); + } + else { + out.print('\r'); + } + out.flush(); + lines = 0; + } + } + + public void resetScreen() + { + if (lines > 0) { + if (isRealTerminal()) { + out.print(ansi().cursorUp(lines).eraseScreen(Erase.FORWARD).toString()); + } + else { + out.print('\r'); + } + out.flush(); + lines = 0; + } + } + + public int getWidth() + { + return TerminalFactory.get().getWidth(); + } + + @SuppressWarnings("MethodMayBeStatic") + public boolean isRealTerminal() + { + return REAL_TERMINAL; + } + + private static boolean detectRealTerminal() + { + // If the jansi.passthrough property is set, then don't interpret + // any of the ansi sequences. + if (Boolean.parseBoolean(System.getProperty("jansi.passthrough"))) { + return true; + } + + // If the jansi.strip property is set, then we just strip the + // the ansi escapes. + if (Boolean.parseBoolean(System.getProperty("jansi.strip"))) { + return false; + } + + String os = System.getProperty("os.name"); + if (os.startsWith("Windows")) { + // We could support this, but we'd need a windows box + return true; + } + + // We must be on some unix variant.. + try { + // check if standard out is a terminal + if (isatty(STDOUT_FILENO) == 0) { + return false; + } + } + catch (NoClassDefFoundError | UnsatisfiedLinkError ignore) { + // These errors happen if the JNI lib is not available for your platform. + } + return true; + } +} diff --git a/presto-cli/src/main/java/com/facebook/presto/cli/CsvPrinter.java b/presto-cli/src/main/java/com/facebook/presto/cli/CsvPrinter.java new file mode 100644 index 00000000..5bb04011 --- /dev/null +++ b/presto-cli/src/main/java/com/facebook/presto/cli/CsvPrinter.java @@ -0,0 +1,83 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cli; + +import au.com.bytecode.opencsv.CSVWriter; +import com.google.common.collect.ImmutableList; + +import java.io.IOException; +import java.io.Writer; +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class CsvPrinter + implements OutputPrinter +{ + private final List fieldNames; + private final CSVWriter writer; + + private boolean needHeader; + + public CsvPrinter(List fieldNames, Writer writer, boolean header) + { + checkNotNull(fieldNames, "fieldNames is null"); + checkNotNull(writer, "writer is null"); + this.fieldNames = ImmutableList.copyOf(fieldNames); + this.writer = new CSVWriter(writer); + this.needHeader = header; + } + + @Override + public void printRows(List> rows, boolean complete) + throws IOException + { + if (needHeader) { + needHeader = false; + writer.writeNext(toStrings(fieldNames)); + } + + for (List row : rows) { + writer.writeNext(toStrings(row)); + checkError(); + } + } + + @Override + public void finish() + throws IOException + { + printRows(ImmutableList.>of(), true); + writer.flush(); + checkError(); + } + + private void checkError() + throws IOException + { + if (writer.checkError()) { + throw new IOException("error writing to output"); + } + } + + private static String[] toStrings(List values) + { + String[] array = new String[values.size()]; + for (int i = 0; i < values.size(); i++) { + Object value = values.get(i); + array[i] = (value == null) ? "" : value.toString(); + } + return array; + } +} diff --git a/presto-cli/src/main/java/com/facebook/presto/cli/FormatUtils.java b/presto-cli/src/main/java/com/facebook/presto/cli/FormatUtils.java new file mode 100644 index 00000000..bb3c7d75 --- /dev/null +++ b/presto-cli/src/main/java/com/facebook/presto/cli/FormatUtils.java @@ -0,0 +1,232 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cli; + +import com.google.common.primitives.Ints; +import io.airlift.units.DataSize; +import io.airlift.units.Duration; + +import java.math.RoundingMode; +import java.text.DecimalFormat; + +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Strings.repeat; +import static io.airlift.units.DataSize.Unit.BYTE; +import static java.lang.Math.max; +import static java.lang.Math.min; +import static java.lang.String.format; +import static java.util.concurrent.TimeUnit.SECONDS; + +public final class FormatUtils +{ + private FormatUtils() {} + + public static String formatCount(long count) + { + double fractional = count; + String unit = ""; + if (fractional > 1000) { + fractional /= 1000; + unit = "K"; + } + if (fractional > 1000) { + fractional /= 1000; + unit = "M"; + } + if (fractional > 1000) { + fractional /= 1000; + unit = "B"; + } + if (fractional > 1000) { + fractional /= 1000; + unit = "T"; + } + if (fractional > 1000) { + fractional /= 1000; + unit = "Q"; + } + + return format("%s%s", getFormat(fractional).format(fractional), unit); + } + + public static String formatCountRate(double count, Duration duration, boolean longForm) + { + double rate = count / duration.getValue(SECONDS); + if (Double.isNaN(rate) || Double.isInfinite(rate)) { + rate = 0; + } + + String rateString = formatCount((long) rate); + if (longForm) { + if (rateString.endsWith(" ")) { + rateString = rateString.substring(0, rateString.length() - 1); + } + rateString += "/s"; + } + return rateString; + } + + public static String formatDataSize(DataSize size, boolean longForm) + { + double fractional = size.toBytes(); + String unit = null; + if (fractional >= 1024) { + fractional /= 1024; + unit = "K"; + } + if (fractional >= 1024) { + fractional /= 1024; + unit = "M"; + } + if (fractional >= 1024) { + fractional /= 1024; + unit = "G"; + } + if (fractional >= 1024) { + fractional /= 1024; + unit = "T"; + } + if (fractional >= 1024) { + fractional /= 1024; + unit = "P"; + } + + if (unit == null) { + unit = "B"; + } + else if (longForm) { + unit += "B"; + } + + return format("%s%s", getFormat(fractional).format(fractional), unit); + } + + public static String formatDataRate(DataSize dataSize, Duration duration, boolean longForm) + { + double rate = dataSize.toBytes() / duration.getValue(SECONDS); + if (Double.isNaN(rate) || Double.isInfinite(rate)) { + rate = 0; + } + + String rateString = formatDataSize(new DataSize(rate, BYTE), false); + if (longForm) { + if (!rateString.endsWith("B")) { + rateString += "B"; + } + rateString += "/s"; + } + return rateString; + } + + private static DecimalFormat getFormat(double value) + { + DecimalFormat format; + if (value < 10) { + // show up to two decimals to get 3 significant digits + format = new DecimalFormat("#.##"); + } + else if (value < 100) { + // show up to one decimal to get 3 significant digits + format = new DecimalFormat("#.#"); + } + else { + // show no decimals -- we have enough digits in the integer part + format = new DecimalFormat("#"); + } + + format.setRoundingMode(RoundingMode.HALF_UP); + return format; + } + + public static String pluralize(String word, int count) + { + if (count != 1) { + return word + "s"; + } + return word; + } + + public static String formatTime(Duration duration) + { + int totalSeconds = Ints.saturatedCast(duration.roundTo(SECONDS)); + int minutes = totalSeconds / 60; + int seconds = totalSeconds % 60; + + return format("%s:%02d", minutes, seconds); + } + + /** + * Format an indeterminate progress bar: [ <=> ] + */ + public static String formatProgressBar(int width, int tick) + { + int markerWidth = 3; // must be odd >= 3 (1 for each < and > marker, the rest for "=" + + int range = width - markerWidth; // "lower" must fall within this range for the marker to fit within the bar + int lower = tick % range; + + if (((tick / range) % 2) == 1) { // are we going or coming back? + lower = range - lower; + } + + return repeat(" ", lower) + + "<" + repeat("=", markerWidth - 2) + ">" + + repeat(" ", width - (lower + markerWidth)); + } + + public static String formatProgressBar(int width, int complete, int running, int total) + { + if (total == 0) { + return repeat(" ", width); + } + + int pending = max(0, total - complete - running); + + // compute nominal lengths + int completeLength = min(width, ceil(complete * width, total)); + int pendingLength = min(width, ceil(pending * width, total)); + + // leave space for at least one ">" as long as running is > 0 + int minRunningLength = (running > 0) ? 1 : 0; + int runningLength = max(min(width, ceil(running * width, total)), minRunningLength); + + // adjust to fix rounding errors + if (((completeLength + runningLength + pendingLength) != width) && (pending > 0)) { + // sacrifice "pending" if we're over the max width + pendingLength = max(0, width - completeLength - runningLength); + } + if ((completeLength + runningLength + pendingLength) != width) { + // then, sacrifice "running" + runningLength = max(minRunningLength, width - completeLength - pendingLength); + } + if (((completeLength + runningLength + pendingLength) > width) && (complete > 0)) { + // finally, sacrifice "complete" if we're still over the limit + completeLength = max(0, width - runningLength - pendingLength); + } + + checkState((completeLength + runningLength + pendingLength) == width, + "Expected completeLength (%s) + runningLength (%s) + pendingLength (%s) == width (%s), was %s for complete = %s, running = %s, total = %s", + completeLength, runningLength, pendingLength, width, completeLength + runningLength + pendingLength, complete, running, total); + + return repeat("=", completeLength) + repeat(">", runningLength) + repeat(" ", pendingLength); + } + + /** + * Ceiling of integer division + */ + private static int ceil(int dividend, int divisor) + { + return ((dividend + divisor) - 1) / divisor; + } +} diff --git a/presto-cli/src/main/java/com/facebook/presto/cli/Help.java b/presto-cli/src/main/java/com/facebook/presto/cli/Help.java new file mode 100644 index 00000000..055d5c13 --- /dev/null +++ b/presto-cli/src/main/java/com/facebook/presto/cli/Help.java @@ -0,0 +1,38 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cli; + +public final class Help +{ + private Help() {} + + public static String getHelpText() + { + return "" + + "Supported commands:\n" + + "QUIT\n" + + "EXPLAIN [ ( option [, ...] ) ] \n" + + " options: FORMAT { TEXT | GRAPHVIZ }\n" + + " TYPE { LOGICAL | DISTRIBUTED }\n" + + "DESCRIBE \n" + + "SHOW COLUMNS FROM
\n" + + "SHOW FUNCTIONS\n" + + "SHOW CATALOGS\n" + + "SHOW SCHEMAS\n" + + "SHOW TABLES [LIKE ]\n" + + "SHOW PARTITIONS FROM
[WHERE ...] [ORDER BY ...] [LIMIT n]\n" + + "USE [.]\n" + + ""; + } +} diff --git a/presto-cli/src/main/java/com/facebook/presto/cli/LineReader.java b/presto-cli/src/main/java/com/facebook/presto/cli/LineReader.java new file mode 100644 index 00000000..8619952b --- /dev/null +++ b/presto-cli/src/main/java/com/facebook/presto/cli/LineReader.java @@ -0,0 +1,74 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cli; + +import jline.console.ConsoleReader; +import jline.console.UserInterruptException; +import jline.console.completer.Completer; +import jline.console.history.History; + +import java.io.Closeable; +import java.io.Flushable; +import java.io.IOException; + +public class LineReader + extends ConsoleReader + implements Closeable +{ + private boolean interrupted; + + LineReader(History history, Completer... completers) + throws IOException + { + setExpandEvents(false); + setBellEnabled(true); + setHandleUserInterrupt(true); + setHistory(history); + setHistoryEnabled(false); + for (Completer completer : completers) { + addCompleter(completer); + } + } + + @Override + public String readLine(String prompt, Character mask) + throws IOException + { + String line; + interrupted = false; + try { + line = super.readLine(prompt, mask); + } + catch (UserInterruptException e) { + interrupted = true; + return null; + } + + if (getHistory() instanceof Flushable) { + ((Flushable) getHistory()).flush(); + } + return line; + } + + @Override + public void close() + { + shutdown(); + } + + public boolean interrupted() + { + return interrupted; + } +} diff --git a/presto-cli/src/main/java/com/facebook/presto/cli/NullPrinter.java b/presto-cli/src/main/java/com/facebook/presto/cli/NullPrinter.java new file mode 100644 index 00000000..f0a22d9a --- /dev/null +++ b/presto-cli/src/main/java/com/facebook/presto/cli/NullPrinter.java @@ -0,0 +1,30 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cli; + +import java.util.List; + +public class NullPrinter + implements OutputPrinter +{ + @Override + public void printRows(List> rows, boolean complete) + { + } + + @Override + public void finish() + { + } +} diff --git a/presto-cli/src/main/java/com/facebook/presto/cli/OutputHandler.java b/presto-cli/src/main/java/com/facebook/presto/cli/OutputHandler.java new file mode 100644 index 00000000..a03f243f --- /dev/null +++ b/presto-cli/src/main/java/com/facebook/presto/cli/OutputHandler.java @@ -0,0 +1,97 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cli; + +import com.facebook.presto.client.StatementClient; +import io.airlift.units.Duration; + +import java.io.Closeable; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import static com.google.common.base.Preconditions.checkNotNull; +import static io.airlift.units.Duration.nanosSince; +import static java.util.Collections.unmodifiableList; +import static java.util.concurrent.TimeUnit.SECONDS; + +public final class OutputHandler + implements Closeable +{ + private static final Duration MAX_BUFFER_TIME = new Duration(3, SECONDS); + private static final int MAX_BUFFERED_ROWS = 10_000; + + private final AtomicBoolean closed = new AtomicBoolean(); + private final List> rowBuffer = new ArrayList<>(MAX_BUFFERED_ROWS); + private final OutputPrinter printer; + + private long bufferStart; + + public OutputHandler(OutputPrinter printer) + { + this.printer = checkNotNull(printer, "printer is null"); + } + + public void processRow(List row) + throws IOException + { + if (rowBuffer.isEmpty()) { + bufferStart = System.nanoTime(); + } + + rowBuffer.add(row); + if (rowBuffer.size() >= MAX_BUFFERED_ROWS) { + flush(false); + } + } + + @Override + public void close() + throws IOException + { + if (!closed.getAndSet(true)) { + flush(true); + printer.finish(); + } + } + + public void processRows(StatementClient client) + throws IOException + { + while (client.isValid()) { + Iterable> data = client.current().getData(); + if (data != null) { + for (List row : data) { + processRow(unmodifiableList(row)); + } + } + + if (nanosSince(bufferStart).compareTo(MAX_BUFFER_TIME) >= 0) { + flush(false); + } + + client.advance(); + } + } + + private void flush(boolean complete) + throws IOException + { + if (!rowBuffer.isEmpty()) { + printer.printRows(unmodifiableList(rowBuffer), complete); + rowBuffer.clear(); + } + } +} diff --git a/presto-cli/src/main/java/com/facebook/presto/cli/OutputPrinter.java b/presto-cli/src/main/java/com/facebook/presto/cli/OutputPrinter.java new file mode 100644 index 00000000..e32f2f4d --- /dev/null +++ b/presto-cli/src/main/java/com/facebook/presto/cli/OutputPrinter.java @@ -0,0 +1,26 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cli; + +import java.io.IOException; +import java.util.List; + +public interface OutputPrinter +{ + void printRows(List> rows, boolean complete) + throws IOException; + + void finish() + throws IOException; +} diff --git a/presto-cli/src/main/java/com/facebook/presto/cli/Pager.java b/presto-cli/src/main/java/com/facebook/presto/cli/Pager.java new file mode 100644 index 00000000..f7ff8ac2 --- /dev/null +++ b/presto-cli/src/main/java/com/facebook/presto/cli/Pager.java @@ -0,0 +1,154 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cli; + +import com.google.common.collect.ImmutableList; + +import javax.annotation.Nullable; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.List; + +public class Pager + extends FilterOutputStream +{ + public static final String ENV_PAGER = "PRESTO_PAGER"; + public static final List LESS = ImmutableList.of("less", "-FXRSn"); + + private final Process process; + + private Pager(OutputStream out, @Nullable Process process) + { + super(out); + this.process = process; + } + + @Override + public void close() + throws IOException + { + try { + super.close(); + } + catch (IOException e) { + throw propagateIOException(e); + } + finally { + if (process != null) { + try { + process.waitFor(); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + process.destroy(); + } + } + } + } + + @Override + public void write(int b) + throws IOException + { + try { + super.write(b); + } + catch (IOException e) { + throw propagateIOException(e); + } + } + + @Override + public void write(byte[] b, int off, int len) + throws IOException + { + try { + super.write(b, off, len); + } + catch (IOException e) { + throw propagateIOException(e); + } + } + + @Override + public void flush() + throws IOException + { + try { + super.flush(); + } + catch (IOException e) { + throw propagateIOException(e); + } + } + + private static IOException propagateIOException(IOException e) + throws IOException + { + // TODO: check if the pager exited and verify the exit status? + if ("Broken pipe".equals(e.getMessage()) || "Stream closed".equals(e.getMessage())) { + throw new QueryAbortedException(e); + } + throw e; + } + + public static Pager create() + { + String pager = System.getenv(ENV_PAGER); + if (pager == null) { + return create(LESS); + } + pager = pager.trim(); + if (pager.isEmpty()) { + return createNullPager(); + } + return create(ImmutableList.of("/bin/sh", "-c", pager)); + } + + public static Pager create(List command) + { + try { + Process process = new ProcessBuilder() + .command(command) + .redirectOutput(ProcessBuilder.Redirect.INHERIT) + .redirectError(ProcessBuilder.Redirect.INHERIT) + .start(); + return new Pager(process.getOutputStream(), process); + } + catch (IOException e) { + System.err.println("ERROR: failed to open pager: " + e.getMessage()); + return createNullPager(); + } + } + + private static Pager createNullPager() + { + return new Pager(uncloseableOutputStream(System.out), null); + } + + private static OutputStream uncloseableOutputStream(OutputStream out) + { + return new FilterOutputStream(out) + { + @Override + public void close() + throws IOException + { + flush(); + } + }; + } +} diff --git a/presto-cli/src/main/java/com/facebook/presto/cli/PerfTest.java b/presto-cli/src/main/java/com/facebook/presto/cli/PerfTest.java new file mode 100644 index 00000000..038f07ad --- /dev/null +++ b/presto-cli/src/main/java/com/facebook/presto/cli/PerfTest.java @@ -0,0 +1,337 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cli; + +import com.facebook.presto.client.ClientSession; +import com.facebook.presto.client.PrestoHeaders; +import com.facebook.presto.sql.parser.StatementSplitter; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.io.Files; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import io.airlift.command.Command; +import io.airlift.command.HelpOption; +import io.airlift.command.Option; +import io.airlift.http.client.HttpClient; +import io.airlift.http.client.HttpClientConfig; +import io.airlift.http.client.Request; +import io.airlift.http.client.StatusResponseHandler.StatusResponse; +import io.airlift.http.client.jetty.JettyHttpClient; +import io.airlift.log.Level; +import io.airlift.log.Logging; +import io.airlift.log.LoggingConfiguration; +import io.airlift.units.Duration; + +import javax.annotation.Nullable; +import javax.inject.Inject; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.TimeZone; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static com.facebook.presto.cli.ClientOptions.parseServer; +import static com.facebook.presto.sql.parser.StatementSplitter.Statement; +import static com.google.common.base.MoreObjects.firstNonNull; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.Iterables.transform; +import static com.google.common.io.ByteStreams.nullOutputStream; +import static com.google.common.net.HttpHeaders.USER_AGENT; +import static com.google.common.util.concurrent.MoreExecutors.listeningDecorator; +import static io.airlift.command.SingleCommand.singleCommand; +import static io.airlift.concurrent.Threads.daemonThreadsNamed; +import static io.airlift.http.client.HttpUriBuilder.uriBuilderFrom; +import static io.airlift.http.client.Request.Builder.preparePost; +import static io.airlift.http.client.StaticBodyGenerator.createStaticBodyGenerator; +import static io.airlift.http.client.StatusResponseHandler.createStatusResponseHandler; +import static java.lang.String.format; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.concurrent.Executors.newCachedThreadPool; + +@Command(name = "presto", description = "Presto interactive console") +public class PerfTest +{ + private static final String USER_AGENT_VALUE = PerfTest.class.getSimpleName() + + "/" + + firstNonNull(PerfTest.class.getPackage().getImplementationVersion(), "unknown"); + + @Inject + public HelpOption helpOption; + + @Option(name = "--server", title = "server", description = "Presto server location (default: localhost:8080)") + public String server = "localhost:8080"; + + @Option(name = "--catalog", title = "catalog", description = "Default catalog") + public String catalog = "default"; + + @Option(name = "--schema", title = "schema", description = "Default schema") + public String schema = "default"; + + @Option(name = {"-f", "--file"}, title = "file", description = "Execute statements from file and exit") + public String file; + + @Option(name = "--debug", title = "debug", description = "Enable debug information") + public boolean debug; + + @Option(name = {"-r", "--runs"}, title = "number", description = "Number of runs until exit (default: 10)") + public int runs = 10; + + @Option(name = "--timeout", title = "timeout", description = "Timeout for HTTP-Client to wait for query results (default: 600)") + public int timeout = 600; + + public void run() + throws Exception + { + initializeLogging(debug); + List queries = loadQueries(); + + try (ParallelQueryRunner parallelQueryRunner = new ParallelQueryRunner(16, parseServer(server), catalog, schema, debug, timeout)) { + for (int loop = 0; loop < runs; loop++) { + executeQueries(queries, parallelQueryRunner, 1); + executeQueries(queries, parallelQueryRunner, 2); + executeQueries(queries, parallelQueryRunner, 4); + executeQueries(queries, parallelQueryRunner, 8); + executeQueries(queries, parallelQueryRunner, 16); + } + } + } + + private void executeQueries(List queries, ParallelQueryRunner parallelQueryRunner, int parallelism) + throws Exception + { + Duration duration = parallelQueryRunner.executeCommands(parallelism, queries); + System.out.printf("%2d: %s\n", parallelism, duration.convertTo(TimeUnit.SECONDS)); + } + + private List loadQueries() + { + try { + String query = Files.toString(new File(file), UTF_8); + StatementSplitter splitter = new StatementSplitter(query + ";"); + return ImmutableList.copyOf(transform(splitter.getCompleteStatements(), Statement::statement)); + } + catch (IOException e) { + throw new RuntimeException(format("Error reading from file %s: %s", file, e.getMessage())); + } + } + + public static class ParallelQueryRunner + implements Closeable + { + private final ListeningExecutorService executor; + private final List runners; + + public ParallelQueryRunner(int maxParallelism, URI server, String catalog, String schema, boolean debug, int timeout) + { + executor = listeningDecorator(newCachedThreadPool(daemonThreadsNamed("query-runner-%s"))); + + ImmutableList.Builder runners = ImmutableList.builder(); + for (int i = 0; i < maxParallelism; i++) { + ClientSession session = new ClientSession( + server, + "test-" + i, + "presto-perf", + catalog, + schema, + TimeZone.getDefault().getID(), + Locale.getDefault(), + ImmutableMap.of(), + debug); + runners.add(new QueryRunner(session, executor, timeout)); + } + this.runners = runners.build(); + } + + public Duration executeCommands(int parallelism, List queries) + throws Exception + { + checkArgument(parallelism >= 0, "parallelism is negative"); + checkArgument(parallelism <= runners.size(), "parallelism is greater than maxParallelism"); + checkNotNull(queries, "queries is null"); + + CountDownLatch remainingQueries = new CountDownLatch(queries.size()); + BlockingQueue queue = new ArrayBlockingQueue<>(queries.size(), false, queries); + + List> futures = new ArrayList<>(parallelism); + long start = System.nanoTime(); + for (int i = 0; i < parallelism; i++) { + QueryRunner runner = runners.get(i); + futures.add(runner.execute(queue, remainingQueries)); + } + + // kill test if anything fails + ListenableFuture> allFutures = Futures.allAsList(futures); + Futures.addCallback(allFutures, new FutureCallback>() + { + @Override + public void onSuccess(@Nullable List result) + { + } + + @Override + public void onFailure(Throwable t) + { + System.err.println("Run failed"); + t.printStackTrace(System.err); + System.exit(1); + } + }, executor); + + remainingQueries.await(); + Duration executionTime = Duration.nanosSince(start); + + // wait for runners to spin-down + allFutures.get(); + + return executionTime; + } + + @Override + public void close() + throws IOException + { + for (QueryRunner runner : runners) { + try { + runner.close(); + } + catch (Exception ignored) { + } + } + } + } + + public static class QueryRunner + implements Closeable + { + private final ClientSession session; + private final ListeningExecutorService executor; + private final HttpClient httpClient; + + public QueryRunner(ClientSession session, ListeningExecutorService executor, int timeout) + { + this.session = session; + this.executor = executor; + + HttpClientConfig clientConfig = new HttpClientConfig(); + clientConfig.setConnectTimeout(new Duration(10, TimeUnit.SECONDS)); + clientConfig.setIdleTimeout(new Duration(timeout, TimeUnit.SECONDS)); + clientConfig.setKeepAliveInterval(new Duration(timeout, TimeUnit.SECONDS)); + httpClient = new JettyHttpClient(clientConfig); + } + + public ListenableFuture execute(final BlockingQueue queue, final CountDownLatch remainingQueries) + { + return executor.submit(() -> { + for (String query = queue.poll(); query != null; query = queue.poll()) { + execute(query); + remainingQueries.countDown(); + } + }); + } + + public void execute(String query) + { + Request request = buildQueryRequest(session, query); + StatusResponse response = httpClient.execute(request, createStatusResponseHandler()); + if (response.getStatusCode() != 200) { + throw new RuntimeException("Query failed: [" + response.getStatusCode() + "] " + response.getStatusMessage()); + } + } + + private static Request buildQueryRequest(ClientSession session, String query) + { + Request.Builder builder = preparePost() + .setUri(uriBuilderFrom(session.getServer()).replacePath("/v1/execute").build()) + .setBodyGenerator(createStaticBodyGenerator(query, UTF_8)); + + if (session.getUser() != null) { + builder.setHeader(PrestoHeaders.PRESTO_USER, session.getUser()); + } + if (session.getSource() != null) { + builder.setHeader(PrestoHeaders.PRESTO_SOURCE, session.getSource()); + } + if (session.getCatalog() != null) { + builder.setHeader(PrestoHeaders.PRESTO_CATALOG, session.getCatalog()); + } + if (session.getSchema() != null) { + builder.setHeader(PrestoHeaders.PRESTO_SCHEMA, session.getSchema()); + } + builder.setHeader(PrestoHeaders.PRESTO_TIME_ZONE, session.getTimeZoneId()); + builder.setHeader(USER_AGENT, USER_AGENT_VALUE); + + return builder.build(); + } + + @Override + public void close() + { + httpClient.close(); + } + } + + private static void initializeLogging(boolean debug) + { + // unhook out and err while initializing logging or logger will print to them + PrintStream out = System.out; + PrintStream err = System.err; + try { + if (debug) { + Logging logging = Logging.initialize(); + logging.configure(new LoggingConfiguration()); + logging.setLevel("com.facebook.presto", Level.DEBUG); + } + else { + System.setOut(new PrintStream(nullOutputStream())); + System.setErr(new PrintStream(nullOutputStream())); + + Logging logging = Logging.initialize(); + logging.configure(new LoggingConfiguration()); + logging.disableConsole(); + } + } + catch (IOException e) { + throw Throwables.propagate(e); + } + finally { + System.setOut(out); + System.setErr(err); + } + } + + public static void main(String[] args) + throws Exception + { + PerfTest perfTest = singleCommand(PerfTest.class).parse(args); + + if (perfTest.helpOption.showHelpIfRequested()) { + return; + } + + perfTest.run(); + } +} diff --git a/presto-cli/src/main/java/com/facebook/presto/cli/Presto.java b/presto-cli/src/main/java/com/facebook/presto/cli/Presto.java new file mode 100644 index 00000000..b9e9d7f9 --- /dev/null +++ b/presto-cli/src/main/java/com/facebook/presto/cli/Presto.java @@ -0,0 +1,34 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cli; + +import static io.airlift.command.SingleCommand.singleCommand; + +public final class Presto +{ + private Presto() {} + + public static void main(String[] args) + throws Exception + { + Console console = singleCommand(Console.class).parse(args); + + if (console.helpOption.showHelpIfRequested() || + console.versionOption.showVersionIfRequested()) { + return; + } + + console.run(); + } +} diff --git a/presto-cli/src/main/java/com/facebook/presto/cli/Query.java b/presto-cli/src/main/java/com/facebook/presto/cli/Query.java new file mode 100644 index 00000000..e9fd6df1 --- /dev/null +++ b/presto-cli/src/main/java/com/facebook/presto/cli/Query.java @@ -0,0 +1,321 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cli; + +import com.facebook.presto.cli.ClientOptions.OutputFormat; +import com.facebook.presto.client.Column; +import com.facebook.presto.client.ErrorLocation; +import com.facebook.presto.client.QueryResults; +import com.facebook.presto.client.StatementClient; +import com.google.common.base.Splitter; +import com.google.common.base.Strings; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import io.airlift.log.Logger; +import io.airlift.units.Duration; +import org.fusesource.jansi.Ansi; +import sun.misc.Signal; +import sun.misc.SignalHandler; + +import java.io.Closeable; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintStream; +import java.io.Writer; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +import static com.facebook.presto.cli.ConsolePrinter.REAL_TERMINAL; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.lang.String.format; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.concurrent.TimeUnit.SECONDS; + +public class Query + implements Closeable +{ + private static final Logger log = Logger.get(Query.class); + + private static final Signal SIGINT = new Signal("INT"); + + private final AtomicBoolean ignoreUserInterrupt = new AtomicBoolean(); + private final StatementClient client; + + public Query(StatementClient client) + { + this.client = checkNotNull(client, "client is null"); + } + + public Map getSetSessionProperties() + { + return client.getSetSessionProperties(); + } + + public Set getResetSessionProperties() + { + return client.getResetSessionProperties(); + } + + public void renderOutput(PrintStream out, OutputFormat outputFormat, boolean interactive) + { + Thread clientThread = Thread.currentThread(); + SignalHandler oldHandler = Signal.handle(SIGINT, new SignalHandler() + { + @Override + public void handle(Signal signal) + { + if (ignoreUserInterrupt.get() || client.isClosed()) { + return; + } + try { + if (client.cancelLeafStage(new Duration(1, SECONDS))) { + return; + } + } + catch (RuntimeException e) { + log.debug(e, "error canceling leaf stage"); + } + client.close(); + clientThread.interrupt(); + } + }); + try { + renderQueryOutput(out, outputFormat, interactive); + } + finally { + Signal.handle(SIGINT, oldHandler); + Thread.interrupted(); // clear interrupt status + } + } + + private void renderQueryOutput(PrintStream out, OutputFormat outputFormat, boolean interactive) + { + StatusPrinter statusPrinter = null; + @SuppressWarnings("resource") + PrintStream errorChannel = interactive ? out : System.err; + + if (interactive) { + statusPrinter = new StatusPrinter(client, out); + statusPrinter.printInitialStatusUpdates(); + } + else { + waitForData(); + } + + if ((!client.isFailed()) && (!client.isGone()) && (!client.isClosed())) { + QueryResults results = client.isValid() ? client.current() : client.finalResults(); + if (results.getUpdateType() != null) { + renderUpdate(out, results); + } + else if (results.getColumns() == null) { + errorChannel.printf("Query %s has no columns\n", results.getId()); + return; + } + else { + renderResults(out, outputFormat, interactive, results.getColumns()); + } + } + + if (statusPrinter != null) { + statusPrinter.printFinalInfo(); + } + + if (client.isClosed()) { + errorChannel.println("Query aborted by user"); + } + else if (client.isGone()) { + errorChannel.println("Query is gone (server restarted?)"); + } + else if (client.isFailed()) { + renderFailure(client.finalResults(), errorChannel); + } + } + + private void waitForData() + { + while (client.isValid() && (client.current().getData() == null)) { + client.advance(); + } + } + + private void renderUpdate(PrintStream out, QueryResults results) + { + String status = results.getUpdateType(); + if (results.getUpdateCount() != null) { + long count = results.getUpdateCount(); + status += format(": %s row%s", count, (count != 1) ? "s" : ""); + } + out.println(status); + discardResults(); + } + + private void discardResults() + { + try (OutputHandler handler = new OutputHandler(new NullPrinter())) { + handler.processRows(client); + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } + + private void renderResults(PrintStream out, OutputFormat outputFormat, boolean interactive, List columns) + { + try { + doRenderResults(out, outputFormat, interactive, columns); + } + catch (QueryAbortedException e) { + System.out.println("(query aborted by user)"); + client.close(); + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } + + private void doRenderResults(PrintStream out, OutputFormat format, boolean interactive, List columns) + throws IOException + { + List fieldNames = Lists.transform(columns, Column::getName); + if (interactive) { + pageOutput(format, fieldNames); + } + else { + sendOutput(out, format, fieldNames); + } + } + + private void pageOutput(OutputFormat format, List fieldNames) + throws IOException + { + // ignore the user pressing ctrl-C while in the pager + ignoreUserInterrupt.set(true); + + try (Writer writer = createWriter(Pager.create()); + OutputHandler handler = createOutputHandler(format, writer, fieldNames)) { + handler.processRows(client); + } + } + + private void sendOutput(PrintStream out, OutputFormat format, List fieldNames) + throws IOException + { + try (OutputHandler handler = createOutputHandler(format, createWriter(out), fieldNames)) { + handler.processRows(client); + } + } + + private static OutputHandler createOutputHandler(OutputFormat format, Writer writer, List fieldNames) + { + return new OutputHandler(createOutputPrinter(format, writer, fieldNames)); + } + + private static OutputPrinter createOutputPrinter(OutputFormat format, Writer writer, List fieldNames) + { + switch (format) { + case ALIGNED: + return new AlignedTablePrinter(fieldNames, writer); + case VERTICAL: + return new VerticalRecordPrinter(fieldNames, writer); + case CSV: + return new CsvPrinter(fieldNames, writer, false); + case CSV_HEADER: + return new CsvPrinter(fieldNames, writer, true); + case TSV: + return new TsvPrinter(fieldNames, writer, false); + case TSV_HEADER: + return new TsvPrinter(fieldNames, writer, true); + case NULL: + return new NullPrinter(); + } + throw new RuntimeException(format + " not supported"); + } + + private static Writer createWriter(OutputStream out) + { + return new OutputStreamWriter(out, UTF_8); + } + + @Override + public void close() + { + client.close(); + } + + public void renderFailure(QueryResults results, PrintStream out) + { + out.printf("Query %s failed: %s%n", results.getId(), results.getError().getMessage()); + if (client.isDebug()) { + renderStack(results, out); + } + renderErrorLocation(client.getQuery(), results, out); + out.println(); + } + + private static void renderErrorLocation(String query, QueryResults results, PrintStream out) + { + if (results.getError().getErrorLocation() != null) { + renderErrorLocation(query, results.getError().getErrorLocation(), out); + } + } + + private static void renderErrorLocation(String query, ErrorLocation location, PrintStream out) + { + List lines = ImmutableList.copyOf(Splitter.on('\n').split(query).iterator()); + + String errorLine = lines.get(location.getLineNumber() - 1); + String good = errorLine.substring(0, location.getColumnNumber() - 1); + String bad = errorLine.substring(location.getColumnNumber() - 1); + + if ((location.getLineNumber() == lines.size()) && bad.trim().isEmpty()) { + bad = " "; + } + + if (REAL_TERMINAL) { + Ansi ansi = Ansi.ansi(); + + ansi.fg(Ansi.Color.CYAN); + for (int i = 1; i < location.getLineNumber(); i++) { + ansi.a(lines.get(i - 1)).newline(); + } + ansi.a(good); + + ansi.fg(Ansi.Color.RED); + ansi.a(bad).newline(); + for (int i = location.getLineNumber(); i < lines.size(); i++) { + ansi.a(lines.get(i)).newline(); + } + + ansi.reset(); + out.print(ansi); + } + else { + String prefix = format("LINE %s: ", location.getLineNumber()); + String padding = Strings.repeat(" ", prefix.length() + (location.getColumnNumber() - 1)); + out.println(prefix + errorLine); + out.println(padding + "^"); + } + } + + private static void renderStack(QueryResults results, PrintStream out) + { + if (results.getError().getFailureInfo() != null) { + results.getError().getFailureInfo().toException().printStackTrace(out); + } + } +} diff --git a/presto-cli/src/main/java/com/facebook/presto/cli/QueryAbortedException.java b/presto-cli/src/main/java/com/facebook/presto/cli/QueryAbortedException.java new file mode 100644 index 00000000..3dcb3240 --- /dev/null +++ b/presto-cli/src/main/java/com/facebook/presto/cli/QueryAbortedException.java @@ -0,0 +1,25 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cli; + +import java.io.IOException; + +public class QueryAbortedException + extends IOException +{ + public QueryAbortedException(Throwable cause) + { + super(cause); + } +} diff --git a/presto-cli/src/main/java/com/facebook/presto/cli/QueryRunner.java b/presto-cli/src/main/java/com/facebook/presto/cli/QueryRunner.java new file mode 100644 index 00000000..0becc1af --- /dev/null +++ b/presto-cli/src/main/java/com/facebook/presto/cli/QueryRunner.java @@ -0,0 +1,94 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cli; + +import com.facebook.presto.client.ClientSession; +import com.facebook.presto.client.QueryResults; +import com.facebook.presto.client.StatementClient; +import com.google.common.net.HostAndPort; +import io.airlift.http.client.HttpClient; +import io.airlift.http.client.HttpClientConfig; +import io.airlift.http.client.jetty.JettyHttpClient; +import io.airlift.json.JsonCodec; +import io.airlift.units.Duration; + +import java.io.Closeable; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import static com.google.common.base.Preconditions.checkNotNull; +import static io.airlift.json.JsonCodec.jsonCodec; + +public class QueryRunner + implements Closeable +{ + private final JsonCodec queryResultsCodec; + private final AtomicReference session; + private final HttpClient httpClient; + + public QueryRunner(ClientSession session, JsonCodec queryResultsCodec, Optional socksProxy) + { + this.session = new AtomicReference<>(checkNotNull(session, "session is null")); + this.queryResultsCodec = checkNotNull(queryResultsCodec, "queryResultsCodec is null"); + this.httpClient = new JettyHttpClient(getHttpClientConfig(socksProxy)); + } + + public ClientSession getSession() + { + return session.get(); + } + + public HttpClient getHttpClient() + { + return httpClient; + } + + public void setSession(ClientSession session) + { + this.session.set(checkNotNull(session, "session is null")); + } + + public Query startQuery(String query) + { + return new Query(startInternalQuery(query)); + } + + public StatementClient startInternalQuery(String query) + { + return new StatementClient(httpClient, queryResultsCodec, session.get(), query); + } + + @Override + public void close() + { + httpClient.close(); + } + + public static QueryRunner create(ClientSession session, Optional socksProxy) + { + return new QueryRunner(session, jsonCodec(QueryResults.class), socksProxy); + } + + private static HttpClientConfig getHttpClientConfig(Optional socksProxy) + { + HttpClientConfig httpClientConfig = new HttpClientConfig().setConnectTimeout(new Duration(10, TimeUnit.SECONDS)); + + if (socksProxy.isPresent()) { + httpClientConfig.setSocksProxy(socksProxy.get()); + } + + return httpClientConfig; + } +} diff --git a/presto-cli/src/main/java/com/facebook/presto/cli/StatusPrinter.java b/presto-cli/src/main/java/com/facebook/presto/cli/StatusPrinter.java new file mode 100644 index 00000000..0d0b92be --- /dev/null +++ b/presto-cli/src/main/java/com/facebook/presto/cli/StatusPrinter.java @@ -0,0 +1,398 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cli; + +import com.facebook.presto.client.QueryResults; +import com.facebook.presto.client.StageStats; +import com.facebook.presto.client.StatementClient; +import com.facebook.presto.client.StatementStats; +import com.google.common.base.Strings; +import com.google.common.primitives.Ints; +import io.airlift.log.Logger; +import io.airlift.units.DataSize; +import io.airlift.units.Duration; + +import java.io.PrintStream; +import java.util.concurrent.atomic.AtomicInteger; + +import static com.facebook.presto.cli.FormatUtils.formatCount; +import static com.facebook.presto.cli.FormatUtils.formatCountRate; +import static com.facebook.presto.cli.FormatUtils.formatDataRate; +import static com.facebook.presto.cli.FormatUtils.formatDataSize; +import static com.facebook.presto.cli.FormatUtils.formatProgressBar; +import static com.facebook.presto.cli.FormatUtils.formatTime; +import static com.facebook.presto.cli.FormatUtils.pluralize; +import static io.airlift.units.DataSize.Unit.BYTE; +import static java.lang.Math.max; +import static java.lang.Math.min; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; + +public class StatusPrinter +{ + private static final Logger log = Logger.get(StatusPrinter.class); + + private final long start = System.nanoTime(); + private final StatementClient client; + private final PrintStream out; + private final ConsolePrinter console; + + public StatusPrinter(StatementClient client, PrintStream out) + { + this.client = client; + this.out = out; + this.console = new ConsolePrinter(out); + } + +/* + +Query 16, RUNNING, 1 node, 855 splits +http://my.server:8080/v1/query/16?pretty +Splits: 646 queued, 34 running, 175 done +CPU Time: 33.7s total, 191K rows/s, 16.6MB/s, 22% active +Per Node: 2.5 parallelism, 473K rows/s, 41.1MB/s +Parallelism: 2.5 +0:13 [6.45M rows, 560MB] [ 473K rows/s, 41.1MB/s] [=========>> ] 20% + + STAGES ROWS ROWS/s BYTES BYTES/s PEND RUN DONE +0.........R 13.8M 336K 1.99G 49.5M 0 1 706 + 1.......R 666K 41.5K 82.1M 5.12M 563 65 79 + 2.....R 4.58M 234K 620M 31.6M 406 65 236 + + */ + + public void printInitialStatusUpdates() + { + long lastPrint = System.nanoTime(); + try { + while (client.isValid()) { + try { + // exit status loop if there is there is pending output + if (client.current().getData() != null) { + return; + } + + // update screen if enough time has passed + if (Duration.nanosSince(lastPrint).getValue(SECONDS) >= 0.5) { + console.repositionCursor(); + printQueryInfo(client.current()); + lastPrint = System.nanoTime(); + } + + // fetch next results (server will wait for a while if no data) + client.advance(); + } + catch (RuntimeException e) { + log.debug(e, "error printing status"); + } + } + } + finally { + console.resetScreen(); + } + } + + public void printFinalInfo() + { + Duration wallTime = Duration.nanosSince(start); + + QueryResults results = client.finalResults(); + StatementStats stats = results.getStats(); + + int nodes = stats.getNodes(); + if ((nodes == 0) || (stats.getTotalSplits() == 0)) { + return; + } + + // blank line + out.println(); + + // Query 12, FINISHED, 1 node + String querySummary = String.format("Query %s, %s, %,d %s", + results.getId(), + stats.getState(), + nodes, + pluralize("node", nodes)); + out.println(querySummary); + + if (client.isDebug()) { + out.println(results.getInfoUri() + "?pretty"); + } + + // Splits: 1000 total, 842 done (84.20%) + String splitsSummary = String.format("Splits: %,d total, %,d done (%.2f%%)", + stats.getTotalSplits(), + stats.getCompletedSplits(), + percentage(stats.getCompletedSplits(), stats.getTotalSplits())); + out.println(splitsSummary); + + if (client.isDebug()) { + // CPU Time: 565.2s total, 26K rows/s, 3.85MB/s + Duration cpuTime = millis(stats.getCpuTimeMillis()); + String cpuTimeSummary = String.format("CPU Time: %.1fs total, %5s rows/s, %8s, %d%% active", + cpuTime.getValue(SECONDS), + formatCountRate(stats.getProcessedRows(), cpuTime, false), + formatDataRate(bytes(stats.getProcessedBytes()), cpuTime, true), + (int) percentage(stats.getCpuTimeMillis(), stats.getWallTimeMillis())); + out.println(cpuTimeSummary); + + double parallelism = cpuTime.getValue(MILLISECONDS) / wallTime.getValue(MILLISECONDS); + + // Per Node: 3.5 parallelism, 83.3K rows/s, 0.7 MB/s + String perNodeSummary = String.format("Per Node: %.1f parallelism, %5s rows/s, %8s", + parallelism / nodes, + formatCountRate((double) stats.getProcessedRows() / nodes, wallTime, false), + formatDataRate(bytes(stats.getProcessedBytes() / nodes), wallTime, true)); + reprintLine(perNodeSummary); + + out.println(String.format("Parallelism: %.1f", parallelism)); + } + + // 0:32 [2.12GB, 15M rows] [67MB/s, 463K rows/s] + String statsLine = String.format("%s [%s rows, %s] [%s rows/s, %s]", + formatTime(wallTime), + formatCount(stats.getProcessedRows()), + formatDataSize(bytes(stats.getProcessedBytes()), true), + formatCountRate(stats.getProcessedRows(), wallTime, false), + formatDataRate(bytes(stats.getProcessedBytes()), wallTime, true)); + + out.println(statsLine); + + // blank line + out.println(); + } + + private void printQueryInfo(QueryResults results) + { + StatementStats stats = results.getStats(); + Duration wallTime = Duration.nanosSince(start); + + // cap progress at 99%, otherwise it looks weird when the query is still running and it says 100% + int progressPercentage = (int) min(99, percentage(stats.getCompletedSplits(), stats.getTotalSplits())); + + if (console.isRealTerminal()) { + // blank line + reprintLine(""); + + int terminalWidth = console.getWidth(); + + if (terminalWidth < 75) { + reprintLine("WARNING: Terminal"); + reprintLine("must be at least"); + reprintLine("80 characters wide"); + reprintLine(""); + reprintLine(stats.getState()); + reprintLine(String.format("%s %d%%", formatTime(wallTime), progressPercentage)); + return; + } + + int nodes = stats.getNodes(); + + // Query 10, RUNNING, 1 node, 778 splits + String querySummary = String.format("Query %s, %s, %,d %s, %,d splits", + results.getId(), + stats.getState(), + nodes, + pluralize("node", nodes), + stats.getTotalSplits()); + reprintLine(querySummary); + + if (client.isDebug()) { + reprintLine(results.getInfoUri() + "?pretty"); + } + + if ((nodes == 0) || (stats.getTotalSplits() == 0)) { + return; + } + + if (client.isDebug()) { + // Splits: 620 queued, 34 running, 124 done + String splitsSummary = String.format("Splits: %,d queued, %,d running, %,d done", + stats.getQueuedSplits(), + stats.getRunningSplits(), + stats.getCompletedSplits()); + reprintLine(splitsSummary); + + // CPU Time: 56.5s total, 36.4K rows/s, 4.44MB/s, 60% active + Duration cpuTime = millis(stats.getCpuTimeMillis()); + String cpuTimeSummary = String.format("CPU Time: %.1fs total, %5s rows/s, %8s, %d%% active", + cpuTime.getValue(SECONDS), + formatCountRate(stats.getProcessedRows(), cpuTime, false), + formatDataRate(bytes(stats.getProcessedBytes()), cpuTime, true), + (int) percentage(stats.getCpuTimeMillis(), stats.getWallTimeMillis())); + reprintLine(cpuTimeSummary); + + double parallelism = cpuTime.getValue(MILLISECONDS) / wallTime.getValue(MILLISECONDS); + + // Per Node: 3.5 parallelism, 83.3K rows/s, 0.7 MB/s + String perNodeSummary = String.format("Per Node: %.1f parallelism, %5s rows/s, %8s", + parallelism / nodes, + formatCountRate((double) stats.getProcessedRows() / nodes, wallTime, false), + formatDataRate(bytes(stats.getProcessedBytes() / nodes), wallTime, true)); + reprintLine(perNodeSummary); + + reprintLine(String.format("Parallelism: %.1f", parallelism)); + } + + assert terminalWidth >= 75; + int progressWidth = (min(terminalWidth, 100) - 75) + 17; // progress bar is 17-42 characters wide + + if (stats.isScheduled()) { + String progressBar = formatProgressBar(progressWidth, + stats.getCompletedSplits(), + max(0, stats.getRunningSplits()), + stats.getTotalSplits()); + + // 0:17 [ 103MB, 802K rows] [5.74MB/s, 44.9K rows/s] [=====>> ] 10% + String progressLine = String.format("%s [%5s rows, %6s] [%5s rows/s, %8s] [%s] %d%%", + formatTime(wallTime), + formatCount(stats.getProcessedRows()), + formatDataSize(bytes(stats.getProcessedBytes()), true), + formatCountRate(stats.getProcessedRows(), wallTime, false), + formatDataRate(bytes(stats.getProcessedBytes()), wallTime, true), + progressBar, + progressPercentage); + + reprintLine(progressLine); + } + else { + String progressBar = formatProgressBar(progressWidth, Ints.saturatedCast(Duration.nanosSince(start).roundTo(SECONDS))); + + // 0:17 [ 103MB, 802K rows] [5.74MB/s, 44.9K rows/s] [ <=> ] + String progressLine = String.format("%s [%5s rows, %6s] [%5s rows/s, %8s] [%s]", + formatTime(wallTime), + formatCount(stats.getProcessedRows()), + formatDataSize(bytes(stats.getProcessedBytes()), true), + formatCountRate(stats.getProcessedRows(), wallTime, false), + formatDataRate(bytes(stats.getProcessedBytes()), wallTime, true), + progressBar); + + reprintLine(progressLine); + } + + // todo Mem: 1949M shared, 7594M private + + // blank line + reprintLine(""); + + // STAGE S ROWS RPS BYTES BPS QUEUED RUN DONE + String stagesHeader = String.format("%10s%1s %5s %6s %5s %7s %6s %5s %5s", + "STAGE", + "S", + "ROWS", + "ROWS/s", + "BYTES", + "BYTES/s", + "QUEUED", + "RUN", + "DONE"); + reprintLine(stagesHeader); + + printStageTree(stats.getRootStage(), "", new AtomicInteger()); + } + else { + // Query 31 [S] i[2.7M 67.3MB 62.7MBps] o[35 6.1KB 1KBps] splits[252/16/380] + String querySummary = String.format("Query %s [%s] i[%s %s %s] o[%s %s %s] splits[%,d/%,d/%,d]", + results.getId(), + stats.getState(), + + formatCount(stats.getProcessedRows()), + formatDataSize(bytes(stats.getProcessedBytes()), false), + formatDataRate(bytes(stats.getProcessedBytes()), wallTime, false), + + formatCount(stats.getProcessedRows()), + formatDataSize(bytes(stats.getProcessedBytes()), false), + formatDataRate(bytes(stats.getProcessedBytes()), wallTime, false), + + stats.getQueuedSplits(), + stats.getRunningSplits(), + stats.getCompletedSplits()); + reprintLine(querySummary); + } + } + + private void printStageTree(StageStats stage, String indent, AtomicInteger stageNumberCounter) + { + Duration elapsedTime = Duration.nanosSince(start); + + // STAGE S ROWS ROWS/s BYTES BYTES/s QUEUED RUN DONE + // 0......Q 26M 9077M 9993G 9077M 9077M 9077M 9077M + // 2....R 17K 627M 673M 627M 627M 627M 627M + // 3..C 999 627M 673M 627M 627M 627M 627M + // 4....R 26M 627M 673T 627M 627M 627M 627M + // 5..F 29T 627M 673M 627M 627M 627M 627M + + String id = String.valueOf(stageNumberCounter.getAndIncrement()); + String name = indent + id; + name += Strings.repeat(".", max(0, 10 - name.length())); + + String bytesPerSecond; + String rowsPerSecond; + if (stage.isDone()) { + bytesPerSecond = formatDataRate(new DataSize(0, BYTE), new Duration(0, SECONDS), false); + rowsPerSecond = formatCountRate(0, new Duration(0, SECONDS), false); + } + else { + bytesPerSecond = formatDataRate(bytes(stage.getProcessedBytes()), elapsedTime, false); + rowsPerSecond = formatCountRate(stage.getProcessedRows(), elapsedTime, false); + } + + String stageSummary = String.format("%10s%1s %5s %6s %5s %7s %6s %5s %5s", + name, + stageStateCharacter(stage.getState()), + + formatCount(stage.getProcessedRows()), + rowsPerSecond, + + formatDataSize(bytes(stage.getProcessedBytes()), false), + bytesPerSecond, + + stage.getQueuedSplits(), + stage.getRunningSplits(), + stage.getCompletedSplits()); + reprintLine(stageSummary); + + for (StageStats subStage : stage.getSubStages()) { + printStageTree(subStage, indent + " ", stageNumberCounter); + } + } + + private void reprintLine(String line) + { + console.reprintLine(line); + } + + private static char stageStateCharacter(String state) + { + return "FAILED".equals(state) ? 'X' : state.charAt(0); + } + + private static Duration millis(long millis) + { + return new Duration(millis, MILLISECONDS); + } + + private static DataSize bytes(long bytes) + { + return new DataSize(bytes, BYTE); + } + + private static double percentage(double count, double total) + { + if (total == 0) { + return 0; + } + return min(100, (count * 100.0) / total); + } +} diff --git a/presto-cli/src/main/java/com/facebook/presto/cli/TableNameCompleter.java b/presto-cli/src/main/java/com/facebook/presto/cli/TableNameCompleter.java new file mode 100644 index 00000000..b7cf7348 --- /dev/null +++ b/presto-cli/src/main/java/com/facebook/presto/cli/TableNameCompleter.java @@ -0,0 +1,154 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cli; + +import com.facebook.presto.client.QueryResults; +import com.facebook.presto.client.StatementClient; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.ImmutableList; +import jline.console.completer.Completer; + +import java.io.Closeable; +import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.cache.CacheLoader.asyncReloading; +import static io.airlift.concurrent.Threads.daemonThreadsNamed; +import static java.lang.String.format; +import static java.util.concurrent.Executors.newCachedThreadPool; + +public class TableNameCompleter + implements Completer, Closeable +{ + private static final long RELOAD_TIME_MINUTES = 2; + + private final ExecutorService executor = newCachedThreadPool(daemonThreadsNamed("completer-%s")); + private final QueryRunner queryRunner; + private final LoadingCache> tableCache; + private final LoadingCache> functionCache; + + public TableNameCompleter(QueryRunner queryRunner) + { + this.queryRunner = checkNotNull(queryRunner, "queryRunner session was null!"); + + tableCache = CacheBuilder.newBuilder() + .refreshAfterWrite(RELOAD_TIME_MINUTES, TimeUnit.MINUTES) + .build(asyncReloading(new CacheLoader>() + { + @Override + public List load(String schemaName) + { + return queryMetadata(format("SELECT table_name FROM information_schema.tables WHERE table_schema = '%s'", schemaName)); + } + }, executor)); + + functionCache = CacheBuilder.newBuilder() + .build(asyncReloading(new CacheLoader>() + { + @Override + public List load(String schemaName) + { + return queryMetadata("SHOW FUNCTIONS"); + } + }, executor)); + } + + private List queryMetadata(String query) + { + ImmutableList.Builder cache = ImmutableList.builder(); + try (StatementClient client = queryRunner.startInternalQuery(query)) { + while (client.isValid() && !Thread.currentThread().isInterrupted()) { + QueryResults results = client.current(); + if (results.getData() != null) { + for (List row : results.getData()) { + cache.add((String) row.get(0)); + } + } + client.advance(); + } + } + return cache.build(); + } + + public void populateCache() + { + final String schemaName = queryRunner.getSession().getSchema(); + executor.execute(new Runnable() + { + @Override + public void run() + { + functionCache.refresh(schemaName); + tableCache.refresh(schemaName); + } + }); + } + + @Override + public int complete(String buffer, int cursor, List candidates) + { + if (cursor <= 0) { + return cursor; + } + int blankPos = findLastBlank(buffer.substring(0, cursor)); + String prefix = buffer.substring(blankPos + 1, cursor); + String schemaName = queryRunner.getSession().getSchema(); + List functionNames = functionCache.getIfPresent(schemaName); + List tableNames = tableCache.getIfPresent(schemaName); + + SortedSet sortedCandidates = new TreeSet<>(); + if (functionNames != null) { + sortedCandidates.addAll(filterResults(functionNames, prefix)); + } + if (tableNames != null) { + sortedCandidates.addAll(filterResults(tableNames, prefix)); + } + + candidates.addAll(sortedCandidates); + return blankPos + 1; + } + + private static int findLastBlank(String buffer) + { + for (int i = buffer.length() - 1; i >= 0; i--) { + if (Character.isWhitespace(buffer.charAt(i))) { + return i; + } + } + return -1; + } + + private static List filterResults(List values, String prefix) + { + ImmutableList.Builder builder = ImmutableList.builder(); + for (String value : values) { + if (value.startsWith(prefix)) { + builder.add(value); + } + } + return builder.build(); + } + + @Override + public void close() + { + executor.shutdownNow(); + } +} diff --git a/presto-cli/src/main/java/com/facebook/presto/cli/TsvPrinter.java b/presto-cli/src/main/java/com/facebook/presto/cli/TsvPrinter.java new file mode 100644 index 00000000..badc8d17 --- /dev/null +++ b/presto-cli/src/main/java/com/facebook/presto/cli/TsvPrinter.java @@ -0,0 +1,110 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cli; + +import com.google.common.collect.ImmutableList; + +import java.io.IOException; +import java.io.Writer; +import java.util.Iterator; +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class TsvPrinter + implements OutputPrinter +{ + private final List fieldNames; + private final Writer writer; + + private boolean needHeader; + + public TsvPrinter(List fieldNames, Writer writer, boolean header) + { + this.fieldNames = ImmutableList.copyOf(checkNotNull(fieldNames, "fieldNames is null")); + this.writer = checkNotNull(writer, "writer is null"); + this.needHeader = header; + } + + @Override + public void printRows(List> rows, boolean complete) + throws IOException + { + if (needHeader) { + needHeader = false; + printRows(ImmutableList.>of(fieldNames), false); + } + + for (List row : rows) { + writer.write(formatRow(row)); + } + } + + @Override + public void finish() + throws IOException + { + printRows(ImmutableList.>of(), true); + writer.flush(); + } + + private static String formatRow(List row) + { + StringBuilder sb = new StringBuilder(); + Iterator iter = row.iterator(); + while (iter.hasNext()) { + Object value = iter.next(); + String s = (value == null) ? "" : value.toString(); + + for (int i = 0; i < s.length(); i++) { + escapeCharacter(sb, s.charAt(i)); + } + + if (iter.hasNext()) { + sb.append('\t'); + } + } + sb.append('\n'); + return sb.toString(); + } + + private static void escapeCharacter(StringBuilder sb, char c) + { + switch (c) { + case '\0': + sb.append('\\').append('0'); + break; + case '\b': + sb.append('\\').append('b'); + break; + case '\f': + sb.append('\\').append('f'); + break; + case '\n': + sb.append('\\').append('n'); + break; + case '\r': + sb.append('\\').append('r'); + break; + case '\t': + sb.append('\\').append('t'); + break; + case '\\': + sb.append('\\').append('\\'); + break; + default: + sb.append(c); + } + } +} diff --git a/presto-cli/src/main/java/com/facebook/presto/cli/VersionOption.java b/presto-cli/src/main/java/com/facebook/presto/cli/VersionOption.java new file mode 100644 index 00000000..b013e09d --- /dev/null +++ b/presto-cli/src/main/java/com/facebook/presto/cli/VersionOption.java @@ -0,0 +1,33 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cli; + +import io.airlift.command.Option; + +import static com.google.common.base.MoreObjects.firstNonNull; + +public class VersionOption +{ + @Option(name = "--version", description = "Display version information and exit") + public Boolean version = false; + + public boolean showVersionIfRequested() + { + if (version) { + String clientVersion = Presto.class.getPackage().getImplementationVersion(); + System.out.println("Presto CLI " + firstNonNull(clientVersion, "(version unknown)")); + } + return version; + } +} diff --git a/presto-cli/src/main/java/com/facebook/presto/cli/VerticalRecordPrinter.java b/presto-cli/src/main/java/com/facebook/presto/cli/VerticalRecordPrinter.java new file mode 100644 index 00000000..38a127fe --- /dev/null +++ b/presto-cli/src/main/java/com/facebook/presto/cli/VerticalRecordPrinter.java @@ -0,0 +1,101 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cli; + +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; + +import java.io.IOException; +import java.io.Writer; +import java.util.List; + +import static com.facebook.presto.cli.AlignedTablePrinter.formatValue; +import static com.facebook.presto.cli.AlignedTablePrinter.maxLineLength; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Strings.repeat; +import static java.lang.Math.max; + +public class VerticalRecordPrinter + implements OutputPrinter +{ + private static final Splitter LINE_SPLITTER = Splitter.on('\n'); + + private final List fieldNames; + private final int namesWidth; + private final Writer writer; + + private long rowCount; + + public VerticalRecordPrinter(List fieldNames, Writer writer) + { + this.fieldNames = ImmutableList.copyOf(checkNotNull(fieldNames, "fieldNames is null")); + this.namesWidth = maxWidth(fieldNames); + this.writer = checkNotNull(writer, "writer is null"); + } + + @Override + public void finish() + throws IOException + { + if (rowCount == 0) { + writer.append("(no rows)\n"); + } + writer.flush(); + } + + @Override + public void printRows(List> rows, boolean complete) + throws IOException + { + int valuesWidth = 0; + for (List row : rows) { + for (Object o : row) { + valuesWidth = max(valuesWidth, maxLineLength(formatValue(o))); + } + } + + for (List row : rows) { + rowCount++; + + String header = "-[ RECORD " + rowCount + " ]"; + if ((namesWidth + 1) >= header.length()) { + header += repeat("-", (namesWidth + 1) - header.length()) + "+"; + } + header += repeat("-", max(0, (namesWidth + valuesWidth + 3) - header.length())); + writer.append(header).append('\n'); + + for (int i = 0; i < row.size(); i++) { + String name = fieldNames.get(i); + String column = formatValue(row.get(i)); + for (String line : LINE_SPLITTER.split(column)) { + writer.append(name) + .append(repeat(" ", namesWidth - name.length())) + .append(" | ") + .append(formatValue(line)) + .append("\n"); + name = repeat(" ", name.length()); + } + } + } + } + + private static int maxWidth(Iterable strings) + { + int n = 0; + for (String s : strings) { + n = max(n, s.length()); + } + return n; + } +} diff --git a/presto-cli/src/test/java/com/facebook/presto/cli/TestAlignedTablePrinter.java b/presto-cli/src/test/java/com/facebook/presto/cli/TestAlignedTablePrinter.java new file mode 100644 index 00000000..e26965a3 --- /dev/null +++ b/presto-cli/src/test/java/com/facebook/presto/cli/TestAlignedTablePrinter.java @@ -0,0 +1,141 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cli; + +import com.google.common.collect.ImmutableList; +import org.testng.annotations.Test; + +import java.io.StringWriter; +import java.util.List; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Arrays.asList; +import static org.testng.Assert.assertEquals; + +public class TestAlignedTablePrinter +{ + @Test + public void testAlignedPrinting() + throws Exception + { + StringWriter writer = new StringWriter(); + List fieldNames = ImmutableList.of("first", "last", "quantity"); + OutputPrinter printer = new AlignedTablePrinter(fieldNames, writer); + + printer.printRows(rows( + row("hello", "world", 123), + row("a", null, 4.5), + row("some long\ntext that\ndoes not\nfit on\none line", "more\ntext", 4567), + row("bye", "done", -15)), + true); + printer.finish(); + + String expected = "" + + " first | last | quantity \n" + + "-----------+-------+----------\n" + + " hello | world | 123 \n" + + " a | NULL | 4.5 \n" + + " some long+| more +| 4567 \n" + + " text that+| text | \n" + + " does not +| | \n" + + " fit on +| | \n" + + " one line | | \n" + + " bye | done | -15 \n" + + "(4 rows)\n"; + + assertEquals(writer.getBuffer().toString(), expected); + } + + @Test + public void testAlignedPrintingOneRow() + throws Exception + { + StringWriter writer = new StringWriter(); + List fieldNames = ImmutableList.of("first", "last"); + OutputPrinter printer = new AlignedTablePrinter(fieldNames, writer); + + printer.printRows(rows(row("a long line\nwithout wrapping", "text")), true); + printer.finish(); + + String expected = "" + + " first | last \n" + + "------------------+------\n" + + " a long line | text \n" + + " without wrapping | \n" + + "(1 row)\n"; + + assertEquals(writer.getBuffer().toString(), expected); + } + + @Test + public void testAlignedPrintingNoRows() + throws Exception + { + StringWriter writer = new StringWriter(); + List fieldNames = ImmutableList.of("first", "last"); + OutputPrinter printer = new AlignedTablePrinter(fieldNames, writer); + + printer.finish(); + + String expected = "" + + " first | last \n" + + "-------+------\n" + + "(0 rows)\n"; + + assertEquals(writer.getBuffer().toString(), expected); + } + + @Test + public void testAlignedPrintingHex() + throws Exception + { + StringWriter writer = new StringWriter(); + List fieldNames = ImmutableList.of("first", "binary", "last"); + OutputPrinter printer = new AlignedTablePrinter(fieldNames, writer); + + printer.printRows(rows( + row("hello", bytes("hello"), "world"), + row("a", bytes("some long text that is more than 16 bytes"), "b"), + row("cat", bytes(""), "dog")), + true); + printer.finish(); + + String expected = "" + + " first | binary | last \n" + + "-------+-------------------------------------------------+-------\n" + + " hello | 68 65 6c 6c 6f | world \n" + + " a | 73 6f 6d 65 20 6c 6f 6e 67 20 74 65 78 74 20 74+| b \n" + + " | 68 61 74 20 69 73 20 6d 6f 72 65 20 74 68 61 6e+| \n" + + " | 20 31 36 20 62 79 74 65 73 | \n" + + " cat | | dog \n" + + "(3 rows)\n"; + + assertEquals(writer.getBuffer().toString(), expected); + } + + static List row(Object... values) + { + return asList(values); + } + + static List> rows(List... rows) + { + return asList(rows); + } + + static byte[] bytes(String s) + { + return s.getBytes(UTF_8); + } +} diff --git a/presto-cli/src/test/java/com/facebook/presto/cli/TestClientOptions.java b/presto-cli/src/test/java/com/facebook/presto/cli/TestClientOptions.java new file mode 100644 index 00000000..e390162b --- /dev/null +++ b/presto-cli/src/test/java/com/facebook/presto/cli/TestClientOptions.java @@ -0,0 +1,157 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cli; + +import com.facebook.presto.cli.ClientOptions.ClientSessionProperty; +import com.facebook.presto.client.ClientSession; +import com.facebook.presto.sql.parser.SqlParser; +import com.google.common.collect.ImmutableList; +import org.testng.annotations.Test; + +import java.util.Optional; + +import static io.airlift.command.SingleCommand.singleCommand; +import static org.testng.Assert.assertEquals; + +public class TestClientOptions +{ + @Test + public void testDefault() + { + ClientSession session = new ClientOptions().toClientSession(); + assertEquals(session.getServer().toString(), "http://localhost:8080"); + assertEquals(session.getSource(), "presto-cli"); + } + + @Test + public void testSource() + { + ClientOptions options = new ClientOptions(); + options.source = "test"; + ClientSession session = options.toClientSession(); + assertEquals(session.getSource(), "test"); + } + + @Test + public void testServerHostOnly() + { + ClientOptions options = new ClientOptions(); + options.server = "localhost"; + ClientSession session = options.toClientSession(); + assertEquals(session.getServer().toString(), "http://localhost:80"); + } + + @Test + public void testServerHostPort() + { + ClientOptions options = new ClientOptions(); + options.server = "localhost:8888"; + ClientSession session = options.toClientSession(); + assertEquals(session.getServer().toString(), "http://localhost:8888"); + } + + @Test + public void testServerHttpUri() + { + ClientOptions options = new ClientOptions(); + options.server = "http://localhost/foo"; + ClientSession session = options.toClientSession(); + assertEquals(session.getServer().toString(), "http://localhost/foo"); + } + + @Test + public void testServerHttpsUri() + { + ClientOptions options = new ClientOptions(); + options.server = "https://localhost/foo"; + ClientSession session = options.toClientSession(); + assertEquals(session.getServer().toString(), "https://localhost/foo"); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testInvalidServer() + { + ClientOptions options = new ClientOptions(); + options.server = "x:y"; + options.toClientSession(); + } + + @Test + public void testSessionProperties() + { + Console console = singleCommand(Console.class).parse("--session", "system=system-value", "--session", "catalog.name=catalog-property"); + + ClientOptions options = console.clientOptions; + assertEquals(options.sessionProperties, ImmutableList.of( + new ClientSessionProperty(Optional.empty(), "system", "system-value"), + new ClientSessionProperty(Optional.of("catalog"), "name", "catalog-property"))); + + // special characters are allowed in the value + assertEquals(new ClientSessionProperty("foo=bar:=baz"), new ClientSessionProperty(Optional.empty(), "foo", "bar:=baz")); + + // empty values are allowed + assertEquals(new ClientSessionProperty("foo="), new ClientSessionProperty(Optional.empty(), "foo", "")); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testThreePartPropertyName() + { + new ClientSessionProperty("foo.bar.baz=value"); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testEmptyPropertyName() + { + new ClientSessionProperty("=value"); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testInvalidCharsetPropertyName() + { + new ClientSessionProperty("\u2603=value"); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testInvalidCharsetPropertyValue() + { + new ClientSessionProperty("name=\u2603"); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testEqualSignNoAllowedInPropertyCatalog() + { + new ClientSessionProperty(Optional.of("cat=alog"), "name", "value"); + } + + @Test + public void testUpdateSessionParameters() + throws Exception + { + ClientOptions options = new ClientOptions(); + ClientSession session = options.toClientSession(); + SqlParser sqlParser = new SqlParser(); + + session = Console.processSessionParameterChange(sqlParser.createStatement("USE test_catalog.test_schema"), session); + assertEquals(session.getCatalog(), "test_catalog"); + assertEquals(session.getSchema(), "test_schema"); + + session = Console.processSessionParameterChange(sqlParser.createStatement("USE test_schema_b"), session); + assertEquals(session.getCatalog(), "test_catalog"); + assertEquals(session.getSchema(), "test_schema_b"); + + session = Console.processSessionParameterChange(sqlParser.createStatement("USE test_catalog_2.test_schema"), session); + assertEquals(session.getCatalog(), "test_catalog_2"); + assertEquals(session.getSchema(), "test_schema"); + } +} diff --git a/presto-cli/src/test/java/com/facebook/presto/cli/TestCsvPrinter.java b/presto-cli/src/test/java/com/facebook/presto/cli/TestCsvPrinter.java new file mode 100644 index 00000000..ab41ab30 --- /dev/null +++ b/presto-cli/src/test/java/com/facebook/presto/cli/TestCsvPrinter.java @@ -0,0 +1,92 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cli; + +import com.google.common.collect.ImmutableList; +import org.testng.annotations.Test; + +import java.io.StringWriter; +import java.util.List; + +import static com.facebook.presto.cli.TestAlignedTablePrinter.row; +import static com.facebook.presto.cli.TestAlignedTablePrinter.rows; +import static org.testng.Assert.assertEquals; + +public class TestCsvPrinter +{ + @Test + public void testCsvPrinting() + throws Exception + { + StringWriter writer = new StringWriter(); + List fieldNames = ImmutableList.of("first", "last", "quantity"); + OutputPrinter printer = new CsvPrinter(fieldNames, writer, true); + + printer.printRows(rows( + row("hello", "world", 123), + row("a", null, 4.5), + row("some long\ntext that\ndoes not\nfit on\none line", "more\ntext", 4567), + row("bye", "done", -15)), + true); + printer.finish(); + + String expected = "" + + "\"first\",\"last\",\"quantity\"\n" + + "\"hello\",\"world\",\"123\"\n" + + "\"a\",\"\",\"4.5\"\n" + + "\"some long\n" + + "text that\n" + + "does not\n" + + "fit on\n" + + "one line\",\"more\n" + + "text\",\"4567\"\n" + + "\"bye\",\"done\",\"-15\"\n"; + + assertEquals(writer.getBuffer().toString(), expected); + } + + @Test + public void testCsvPrintingNoRows() + throws Exception + { + StringWriter writer = new StringWriter(); + List fieldNames = ImmutableList.of("first", "last"); + OutputPrinter printer = new CsvPrinter(fieldNames, writer, true); + + printer.finish(); + + assertEquals(writer.getBuffer().toString(), "\"first\",\"last\"\n"); + } + + @Test + public void testCsvPrintingNoHeader() + throws Exception + { + StringWriter writer = new StringWriter(); + List fieldNames = ImmutableList.of("first", "last", "quantity"); + OutputPrinter printer = new CsvPrinter(fieldNames, writer, false); + + printer.printRows(rows( + row("hello", "world", 123), + row("a", null, 4.5)), + true); + printer.finish(); + + String expected = "" + + "\"hello\",\"world\",\"123\"\n" + + "\"a\",\"\",\"4.5\"\n"; + + assertEquals(writer.getBuffer().toString(), expected); + } +} diff --git a/presto-cli/src/test/java/com/facebook/presto/cli/TestTsvPrinter.java b/presto-cli/src/test/java/com/facebook/presto/cli/TestTsvPrinter.java new file mode 100644 index 00000000..c09d6032 --- /dev/null +++ b/presto-cli/src/test/java/com/facebook/presto/cli/TestTsvPrinter.java @@ -0,0 +1,89 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cli; + +import com.google.common.collect.ImmutableList; +import org.testng.annotations.Test; + +import java.io.StringWriter; +import java.util.List; + +import static com.facebook.presto.cli.TestAlignedTablePrinter.row; +import static com.facebook.presto.cli.TestAlignedTablePrinter.rows; +import static org.testng.Assert.assertEquals; + +public class TestTsvPrinter +{ + @Test + public void testTsvPrinting() + throws Exception + { + StringWriter writer = new StringWriter(); + List fieldNames = ImmutableList.of("first", "last", "quantity"); + OutputPrinter printer = new TsvPrinter(fieldNames, writer, true); + + printer.printRows(rows( + row("hello", "world", 123), + row("a", null, 4.5), + row("some long\ntext\tdone", "more\ntext", 4567), + row("bye", "done", -15), + row("oops\0a\nb\rc\bd\fe\tf\\g\1done", "escape", 9)), + true); + printer.finish(); + + String expected = "" + + "first\tlast\tquantity\n" + + "hello\tworld\t123\n" + + "a\t\t4.5\n" + + "some long\\ntext\\tdone\tmore\\ntext\t4567\n" + + "bye\tdone\t-15\n" + + "oops\\0a\\nb\\rc\\bd\\fe\\tf\\\\g\1done\tescape\t9\n"; + + assertEquals(writer.getBuffer().toString(), expected); + } + + @Test + public void testTsvPrintingNoRows() + throws Exception + { + StringWriter writer = new StringWriter(); + List fieldNames = ImmutableList.of("first", "last"); + OutputPrinter printer = new TsvPrinter(fieldNames, writer, true); + + printer.finish(); + + assertEquals(writer.getBuffer().toString(), "first\tlast\n"); + } + + @Test + public void testTsvPrintingNoHeader() + throws Exception + { + StringWriter writer = new StringWriter(); + List fieldNames = ImmutableList.of("first", "last", "quantity"); + OutputPrinter printer = new TsvPrinter(fieldNames, writer, false); + + printer.printRows(rows( + row("hello", "world", 123), + row("a", null, 4.5)), + true); + printer.finish(); + + String expected = "" + + "hello\tworld\t123\n" + + "a\t\t4.5\n"; + + assertEquals(writer.getBuffer().toString(), expected); + } +} diff --git a/presto-cli/src/test/java/com/facebook/presto/cli/TestVerticalRecordPrinter.java b/presto-cli/src/test/java/com/facebook/presto/cli/TestVerticalRecordPrinter.java new file mode 100644 index 00000000..787e9260 --- /dev/null +++ b/presto-cli/src/test/java/com/facebook/presto/cli/TestVerticalRecordPrinter.java @@ -0,0 +1,171 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.cli; + +import com.google.common.collect.ImmutableList; +import org.testng.annotations.Test; + +import java.io.StringWriter; +import java.util.List; + +import static com.facebook.presto.cli.TestAlignedTablePrinter.bytes; +import static com.facebook.presto.cli.TestAlignedTablePrinter.row; +import static com.facebook.presto.cli.TestAlignedTablePrinter.rows; +import static org.testng.Assert.assertEquals; + +public class TestVerticalRecordPrinter +{ + @Test + public void testVerticalPrinting() + throws Exception + { + StringWriter writer = new StringWriter(); + List fieldNames = ImmutableList.of("first", "last", "quantity"); + OutputPrinter printer = new VerticalRecordPrinter(fieldNames, writer); + + printer.printRows(rows( + row("hello", "world", 123), + row("a", null, 4.5), + row("some long\ntext that\ndoes not\nfit on\none line", "more\ntext", 4567), + row("bye", "done", -15)), + true); + printer.finish(); + + String expected = "" + + "-[ RECORD 1 ]-------\n" + + "first | hello\n" + + "last | world\n" + + "quantity | 123\n" + + "-[ RECORD 2 ]-------\n" + + "first | a\n" + + "last | NULL\n" + + "quantity | 4.5\n" + + "-[ RECORD 3 ]-------\n" + + "first | some long\n" + + " | text that\n" + + " | does not\n" + + " | fit on\n" + + " | one line\n" + + "last | more\n" + + " | text\n" + + "quantity | 4567\n" + + "-[ RECORD 4 ]-------\n" + + "first | bye\n" + + "last | done\n" + + "quantity | -15\n"; + + assertEquals(writer.getBuffer().toString(), expected); + } + + @Test + public void testVerticalShortName() + throws Exception + { + StringWriter writer = new StringWriter(); + List fieldNames = ImmutableList.of("a"); + OutputPrinter printer = new VerticalRecordPrinter(fieldNames, writer); + + printer.printRows(rows(row("x")), true); + printer.finish(); + + String expected = "" + + "-[ RECORD 1 ]\n" + + "a | x\n"; + + assertEquals(writer.getBuffer().toString(), expected); + } + + @Test + public void testVerticalLongName() + throws Exception + { + StringWriter writer = new StringWriter(); + List fieldNames = ImmutableList.of("shippriority"); + OutputPrinter printer = new VerticalRecordPrinter(fieldNames, writer); + + printer.printRows(rows(row("hello")), true); + printer.finish(); + + String expected = "" + + "-[ RECORD 1 ]+------\n" + + "shippriority | hello\n"; + + assertEquals(writer.getBuffer().toString(), expected); + } + + @Test + public void testVerticalLongerName() + throws Exception + { + StringWriter writer = new StringWriter(); + List fieldNames = ImmutableList.of("order_priority"); + OutputPrinter printer = new VerticalRecordPrinter(fieldNames, writer); + + printer.printRows(rows(row("hello")), true); + printer.finish(); + + String expected = "" + + "-[ RECORD 1 ]--+------\n" + + "order_priority | hello\n"; + + assertEquals(writer.getBuffer().toString(), expected); + } + + @Test + public void testVerticalPrintingNoRows() + throws Exception + { + StringWriter writer = new StringWriter(); + List fieldNames = ImmutableList.of("none"); + OutputPrinter printer = new VerticalRecordPrinter(fieldNames, writer); + + printer.finish(); + + assertEquals(writer.getBuffer().toString(), "(no rows)\n"); + } + + @Test + public void testVerticalPrintingHex() + throws Exception + { + StringWriter writer = new StringWriter(); + List fieldNames = ImmutableList.of("first", "binary", "last"); + OutputPrinter printer = new VerticalRecordPrinter(fieldNames, writer); + + printer.printRows(rows( + row("hello", bytes("hello"), "world"), + row("a", bytes("some long text that is more than 16 bytes"), "b"), + row("cat", bytes(""), "dog")), + true); + printer.finish(); + + String expected = "" + + "-[ RECORD 1 ]-------------------------------------------\n" + + "first | hello\n" + + "binary | 68 65 6c 6c 6f\n" + + "last | world\n" + + "-[ RECORD 2 ]-------------------------------------------\n" + + "first | a\n" + + "binary | 73 6f 6d 65 20 6c 6f 6e 67 20 74 65 78 74 20 74\n" + + " | 68 61 74 20 69 73 20 6d 6f 72 65 20 74 68 61 6e\n" + + " | 20 31 36 20 62 79 74 65 73\n" + + "last | b\n" + + "-[ RECORD 3 ]-------------------------------------------\n" + + "first | cat\n" + + "binary | \n" + + "last | dog\n"; + + assertEquals(writer.getBuffer().toString(), expected); + } +} diff --git a/presto-client/pom.xml b/presto-client/pom.xml new file mode 100644 index 00000000..5e37f61e --- /dev/null +++ b/presto-client/pom.xml @@ -0,0 +1,66 @@ + + + 4.0.0 + + + com.facebook.presto + presto-root + 0.107 + + + presto-client + presto-client + + + ${project.parent.basedir} + + + + + com.facebook.presto + presto-spi + + + + javax.validation + validation-api + + + + com.google.code.findbugs + annotations + + + + com.fasterxml.jackson.core + jackson-annotations + + + + io.airlift + http-client + + + + io.airlift + json + + + + io.airlift + units + + + + com.google.guava + guava + + + + + org.testng + testng + test + + + diff --git a/presto-client/src/main/java/com/facebook/presto/client/ClientSession.java b/presto-client/src/main/java/com/facebook/presto/client/ClientSession.java new file mode 100644 index 00000000..44d11ed5 --- /dev/null +++ b/presto-client/src/main/java/com/facebook/presto/client/ClientSession.java @@ -0,0 +1,168 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.client; + +import com.google.common.collect.ImmutableMap; + +import java.net.URI; +import java.nio.charset.CharsetEncoder; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.nio.charset.StandardCharsets.US_ASCII; + +public class ClientSession +{ + private final URI server; + private final String user; + private final String source; + private final String catalog; + private final String schema; + private final String timeZoneId; + private final Locale locale; + private final Map properties; + private final boolean debug; + + public static ClientSession withCatalogAndSchema(ClientSession session, String catalog, String schema) + { + return new ClientSession( + session.getServer(), + session.getUser(), + session.getSource(), + catalog, + schema, + session.getTimeZoneId(), + session.getLocale(), + session.getProperties(), + session.isDebug()); + } + + public static ClientSession withSessionProperties(ClientSession session, Map sessionProperties) + { + Map properties = new HashMap<>(session.getProperties()); + properties.putAll(sessionProperties); + + return new ClientSession( + session.getServer(), + session.getUser(), + session.getSource(), + session.getCatalog(), + session.getSchema(), + session.getTimeZoneId(), + session.getLocale(), + properties, + session.isDebug()); + } + + public static ClientSession withProperties(ClientSession session, Map properties) + { + return new ClientSession( + session.getServer(), + session.getUser(), + session.getSource(), + session.getCatalog(), + session.getSchema(), + session.getTimeZoneId(), + session.getLocale(), + properties, + session.isDebug()); + } + + public ClientSession(URI server, String user, String source, String catalog, String schema, String timeZoneId, Locale locale, Map properties, boolean debug) + { + this.server = checkNotNull(server, "server is null"); + this.user = user; + this.source = source; + this.catalog = catalog; + this.schema = schema; + this.locale = locale; + this.timeZoneId = checkNotNull(timeZoneId, "timeZoneId is null"); + this.debug = debug; + this.properties = ImmutableMap.copyOf(checkNotNull(properties, "properties is null")); + + // verify the properties are valid + CharsetEncoder charsetEncoder = US_ASCII.newEncoder(); + for (Entry entry : properties.entrySet()) { + checkArgument(!entry.getKey().isEmpty(), "Session property name is empty"); + checkArgument(entry.getKey().indexOf('=') < 0, "Session property name must not contain '=': %s", entry.getKey()); + checkArgument(charsetEncoder.canEncode(entry.getKey()), "Session property name is not US_ASCII: %s", entry.getKey()); + checkArgument(charsetEncoder.canEncode(entry.getValue()), "Session property value is not US_ASCII: %s", entry.getValue()); + } + } + + public URI getServer() + { + return server; + } + + public String getUser() + { + return user; + } + + public String getSource() + { + return source; + } + + public String getCatalog() + { + return catalog; + } + + public String getSchema() + { + return schema; + } + + public String getTimeZoneId() + { + return timeZoneId; + } + + public Locale getLocale() + { + return locale; + } + + public Map getProperties() + { + return properties; + } + + public boolean isDebug() + { + return debug; + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("server", server) + .add("user", user) + .add("catalog", catalog) + .add("schema", schema) + .add("timeZone", timeZoneId) + .add("locale", locale) + .add("properties", properties) + .add("debug", debug) + .toString(); + } +} diff --git a/presto-client/src/main/java/com/facebook/presto/client/ClientTypeSignature.java b/presto-client/src/main/java/com/facebook/presto/client/ClientTypeSignature.java new file mode 100644 index 00000000..99b39113 --- /dev/null +++ b/presto-client/src/main/java/com/facebook/presto/client/ClientTypeSignature.java @@ -0,0 +1,144 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.client; + +import com.facebook.presto.spi.type.TypeSignature; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.Lists; + +import javax.annotation.concurrent.Immutable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.regex.Pattern; + +import static com.google.common.base.Preconditions.checkArgument; +import static java.util.Collections.unmodifiableList; + +@Immutable +public class ClientTypeSignature +{ + private static final Pattern PATTERN = Pattern.compile(".*[<>,].*"); + private final String rawType; + private final List typeArguments; + private final List literalArguments; + + public ClientTypeSignature(TypeSignature typeSignature) + { + this( + typeSignature.getBase(), + Lists.transform(typeSignature.getParameters(), ClientTypeSignature::new), + typeSignature.getLiteralParameters()); + } + + @JsonCreator + public ClientTypeSignature( + @JsonProperty("rawType") String rawType, + @JsonProperty("typeArguments") List typeArguments, + @JsonProperty("literalArguments") List literalArguments) + { + checkArgument(rawType != null, "rawType is null"); + this.rawType = rawType; + checkArgument(!rawType.isEmpty(), "rawType is empty"); + checkArgument(!PATTERN.matcher(rawType).matches(), "Bad characters in rawType type: %s", rawType); + checkArgument(typeArguments != null, "typeArguments is null"); + checkArgument(literalArguments != null, "literalArguments is null"); + for (Object literal : literalArguments) { + checkArgument(literal instanceof String || literal instanceof Long, "Unsupported literal type: %s", literal.getClass()); + } + this.typeArguments = unmodifiableList(new ArrayList<>(typeArguments)); + this.literalArguments = unmodifiableList(new ArrayList<>(literalArguments)); + } + + @JsonProperty + public String getRawType() + { + return rawType; + } + + @JsonProperty + public List getTypeArguments() + { + return typeArguments; + } + + @JsonProperty + public List getLiteralArguments() + { + return literalArguments; + } + + @Override + public String toString() + { + StringBuilder typeName = new StringBuilder(rawType); + if (!typeArguments.isEmpty()) { + typeName.append("<"); + boolean first = true; + for (ClientTypeSignature argument : typeArguments) { + if (!first) { + typeName.append(","); + } + first = false; + typeName.append(argument.toString()); + } + typeName.append(">"); + } + if (!literalArguments.isEmpty()) { + typeName.append("("); + boolean first = true; + for (Object parameter : literalArguments) { + if (!first) { + typeName.append(","); + } + first = false; + if (parameter instanceof String) { + typeName.append("'").append(parameter).append("'"); + } + else { + typeName.append(parameter.toString()); + } + } + typeName.append(")"); + } + + return typeName.toString(); + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + ClientTypeSignature other = (ClientTypeSignature) o; + + return Objects.equals(this.rawType.toLowerCase(Locale.ENGLISH), other.rawType.toLowerCase(Locale.ENGLISH)) && + Objects.equals(this.typeArguments, other.typeArguments) && + Objects.equals(this.literalArguments, other.literalArguments); + } + + @Override + public int hashCode() + { + return Objects.hash(rawType.toLowerCase(Locale.ENGLISH), typeArguments, literalArguments); + } +} diff --git a/presto-client/src/main/java/com/facebook/presto/client/Column.java b/presto-client/src/main/java/com/facebook/presto/client/Column.java new file mode 100644 index 00000000..ce92208f --- /dev/null +++ b/presto-client/src/main/java/com/facebook/presto/client/Column.java @@ -0,0 +1,58 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.client; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.concurrent.Immutable; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Immutable +public class Column +{ + private final String name; + private final String type; + private final ClientTypeSignature typeSignature; + + @JsonCreator + public Column( + @JsonProperty("name") String name, + @JsonProperty("type") String type, + @JsonProperty("typeSignature") ClientTypeSignature typeSignature) + { + this.name = checkNotNull(name, "name is null"); + this.type = checkNotNull(type, "type is null"); + this.typeSignature = typeSignature; + } + + @JsonProperty + public String getName() + { + return name; + } + + @JsonProperty + public String getType() + { + return type; + } + + @JsonProperty + public ClientTypeSignature getTypeSignature() + { + return typeSignature; + } +} diff --git a/presto-client/src/main/java/com/facebook/presto/client/ErrorLocation.java b/presto-client/src/main/java/com/facebook/presto/client/ErrorLocation.java new file mode 100644 index 00000000..1f24e399 --- /dev/null +++ b/presto-client/src/main/java/com/facebook/presto/client/ErrorLocation.java @@ -0,0 +1,62 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.client; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.concurrent.Immutable; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkArgument; + +@Immutable +public class ErrorLocation +{ + private final int lineNumber; + private final int columnNumber; + + @JsonCreator + public ErrorLocation( + @JsonProperty("lineNumber") int lineNumber, + @JsonProperty("columnNumber") int columnNumber) + { + checkArgument(lineNumber >= 1, "lineNumber must be at least one"); + checkArgument(columnNumber >= 1, "columnNumber must be at least one"); + + this.lineNumber = lineNumber; + this.columnNumber = columnNumber; + } + + @JsonProperty + public int getLineNumber() + { + return lineNumber; + } + + @JsonProperty + public int getColumnNumber() + { + return columnNumber; + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("lineNumber", lineNumber) + .add("columnNumber", columnNumber) + .toString(); + } +} diff --git a/presto-client/src/main/java/com/facebook/presto/client/FailureInfo.java b/presto-client/src/main/java/com/facebook/presto/client/FailureInfo.java new file mode 100644 index 00000000..effb2f5e --- /dev/null +++ b/presto-client/src/main/java/com/facebook/presto/client/FailureInfo.java @@ -0,0 +1,174 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.client; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; +import javax.validation.constraints.NotNull; + +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Immutable +public class FailureInfo +{ + private static final Pattern STACK_TRACE_PATTERN = Pattern.compile("(.*)\\.(.*)\\(([^:]*)(?::(.*))?\\)"); + + private final String type; + private final String message; + private final FailureInfo cause; + private final List suppressed; + private final List stack; + private final ErrorLocation errorLocation; + + @JsonCreator + public FailureInfo( + @JsonProperty("type") String type, + @JsonProperty("message") String message, + @JsonProperty("cause") FailureInfo cause, + @JsonProperty("suppressed") List suppressed, + @JsonProperty("stack") List stack, + @JsonProperty("errorLocation") @Nullable ErrorLocation errorLocation) + { + checkNotNull(type, "type is null"); + checkNotNull(suppressed, "suppressed is null"); + checkNotNull(stack, "stack is null"); + + this.type = type; + this.message = message; + this.cause = cause; + this.suppressed = ImmutableList.copyOf(suppressed); + this.stack = ImmutableList.copyOf(stack); + this.errorLocation = errorLocation; + } + + @NotNull + @JsonProperty + public String getType() + { + return type; + } + + @Nullable + @JsonProperty + public String getMessage() + { + return message; + } + + @Nullable + @JsonProperty + public FailureInfo getCause() + { + return cause; + } + + @NotNull + @JsonProperty + public List getSuppressed() + { + return suppressed; + } + + @NotNull + @JsonProperty + public List getStack() + { + return stack; + } + + @Nullable + @JsonProperty + public ErrorLocation getErrorLocation() + { + return errorLocation; + } + + public RuntimeException toException() + { + return toException(this); + } + + private static FailureException toException(FailureInfo failureInfo) + { + if (failureInfo == null) { + return null; + } + FailureException failure = new FailureException(failureInfo.getType(), failureInfo.getMessage(), toException(failureInfo.getCause())); + for (FailureInfo suppressed : failureInfo.getSuppressed()) { + failure.addSuppressed(toException(suppressed)); + } + ImmutableList.Builder stackTraceBuilder = ImmutableList.builder(); + for (String stack : failureInfo.getStack()) { + stackTraceBuilder.add(toStackTraceElement(stack)); + } + ImmutableList stackTrace = stackTraceBuilder.build(); + failure.setStackTrace(stackTrace.toArray(new StackTraceElement[stackTrace.size()])); + return failure; + } + + public static StackTraceElement toStackTraceElement(String stack) + { + Matcher matcher = STACK_TRACE_PATTERN.matcher(stack); + if (matcher.matches()) { + String declaringClass = matcher.group(1); + String methodName = matcher.group(2); + String fileName = matcher.group(3); + int number = -1; + if (fileName.equals("Native Method")) { + fileName = null; + number = -2; + } + else if (matcher.group(4) != null) { + number = Integer.parseInt(matcher.group(4)); + } + return new StackTraceElement(declaringClass, methodName, fileName, number); + } + return new StackTraceElement("Unknown", stack, null, -1); + } + + private static class FailureException + extends RuntimeException + { + private final String type; + + FailureException(String type, String message, FailureException cause) + { + super(message, cause, true, true); + this.type = checkNotNull(type, "type is null"); + } + + public String getType() + { + return type; + } + + @Override + public String toString() + { + String message = getMessage(); + if (message != null) { + return type + ": " + message; + } + return type; + } + } +} diff --git a/presto-client/src/main/java/com/facebook/presto/client/PrestoHeaders.java b/presto-client/src/main/java/com/facebook/presto/client/PrestoHeaders.java new file mode 100644 index 00000000..0dd4c4c3 --- /dev/null +++ b/presto-client/src/main/java/com/facebook/presto/client/PrestoHeaders.java @@ -0,0 +1,35 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.client; + +public final class PrestoHeaders +{ + public static final String PRESTO_USER = "X-Presto-User"; + public static final String PRESTO_SOURCE = "X-Presto-Source"; + public static final String PRESTO_CATALOG = "X-Presto-Catalog"; + public static final String PRESTO_SCHEMA = "X-Presto-Schema"; + public static final String PRESTO_TIME_ZONE = "X-Presto-Time-Zone"; + public static final String PRESTO_LANGUAGE = "X-Presto-Language"; + public static final String PRESTO_SESSION = "X-Presto-Session"; + public static final String PRESTO_SET_SESSION = "X-Presto-Set-Session"; + public static final String PRESTO_CLEAR_SESSION = "X-Presto-Clear-Session"; + + public static final String PRESTO_CURRENT_STATE = "X-Presto-Current-State"; + public static final String PRESTO_MAX_WAIT = "X-Presto-Max-Wait"; + public static final String PRESTO_MAX_SIZE = "X-Presto-Max-Size"; + public static final String PRESTO_PAGE_TOKEN = "X-Presto-Page-Sequence-Id"; + public static final String PRESTO_PAGE_NEXT_TOKEN = "X-Presto-Page-End-Sequence-Id"; + + private PrestoHeaders() {} +} diff --git a/presto-client/src/main/java/com/facebook/presto/client/QueryError.java b/presto-client/src/main/java/com/facebook/presto/client/QueryError.java new file mode 100644 index 00000000..6bb0d0ee --- /dev/null +++ b/presto-client/src/main/java/com/facebook/presto/client/QueryError.java @@ -0,0 +1,116 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.client; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; +import javax.validation.constraints.NotNull; + +import static com.google.common.base.MoreObjects.toStringHelper; + +@Immutable +public class QueryError +{ + private final String message; + private final String sqlState; + private final int errorCode; + private final String errorName; + private final String errorType; + private final ErrorLocation errorLocation; + private final FailureInfo failureInfo; + + @JsonCreator + public QueryError( + @JsonProperty("message") String message, + @JsonProperty("sqlState") String sqlState, + @JsonProperty("errorCode") int errorCode, + @JsonProperty("errorName") String errorName, + @JsonProperty("errorType") String errorType, + @JsonProperty("errorLocation") ErrorLocation errorLocation, + @JsonProperty("failureInfo") FailureInfo failureInfo) + { + this.message = message; + this.sqlState = sqlState; + this.errorCode = errorCode; + this.errorName = errorName; + this.errorType = errorType; + this.errorLocation = errorLocation; + this.failureInfo = failureInfo; + } + + @NotNull + @JsonProperty + public String getMessage() + { + return message; + } + + @Nullable + @JsonProperty + public String getSqlState() + { + return sqlState; + } + + @JsonProperty + public int getErrorCode() + { + return errorCode; + } + + @NotNull + @JsonProperty + public String getErrorName() + { + return errorName; + } + + @NotNull + @JsonProperty + public String getErrorType() + { + return errorType; + } + + @Nullable + @JsonProperty + public ErrorLocation getErrorLocation() + { + return errorLocation; + } + + @Nullable + @JsonProperty + public FailureInfo getFailureInfo() + { + return failureInfo; + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("message", message) + .add("sqlState", sqlState) + .add("errorCode", errorCode) + .add("errorName", errorName) + .add("errorType", errorType) + .add("errorLocation", errorLocation) + .add("failureInfo", failureInfo) + .toString(); + } +} diff --git a/presto-client/src/main/java/com/facebook/presto/client/QueryResults.java b/presto-client/src/main/java/com/facebook/presto/client/QueryResults.java new file mode 100644 index 00000000..bfb950e2 --- /dev/null +++ b/presto-client/src/main/java/com/facebook/presto/client/QueryResults.java @@ -0,0 +1,285 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.client; + +import com.facebook.presto.spi.type.TypeSignature; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; +import javax.validation.constraints.NotNull; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Base64; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.facebook.presto.spi.type.StandardTypes.ARRAY; +import static com.facebook.presto.spi.type.StandardTypes.BIGINT; +import static com.facebook.presto.spi.type.StandardTypes.BOOLEAN; +import static com.facebook.presto.spi.type.StandardTypes.DATE; +import static com.facebook.presto.spi.type.StandardTypes.DOUBLE; +import static com.facebook.presto.spi.type.StandardTypes.INTERVAL_DAY_TO_SECOND; +import static com.facebook.presto.spi.type.StandardTypes.INTERVAL_YEAR_TO_MONTH; +import static com.facebook.presto.spi.type.StandardTypes.JSON; +import static com.facebook.presto.spi.type.StandardTypes.MAP; +import static com.facebook.presto.spi.type.StandardTypes.ROW; +import static com.facebook.presto.spi.type.StandardTypes.TIME; +import static com.facebook.presto.spi.type.StandardTypes.TIMESTAMP; +import static com.facebook.presto.spi.type.StandardTypes.TIMESTAMP_WITH_TIME_ZONE; +import static com.facebook.presto.spi.type.StandardTypes.TIME_WITH_TIME_ZONE; +import static com.facebook.presto.spi.type.StandardTypes.VARCHAR; +import static com.facebook.presto.spi.type.TypeSignature.parseTypeSignature; +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.Iterables.unmodifiableIterable; +import static java.util.Collections.unmodifiableList; + +@Immutable +public class QueryResults +{ + private final String id; + private final URI infoUri; + private final URI partialCancelUri; + private final URI nextUri; + private final List columns; + private final Iterable> data; + private final StatementStats stats; + private final QueryError error; + private final String updateType; + private final Long updateCount; + + @JsonCreator + public QueryResults( + @JsonProperty("id") String id, + @JsonProperty("infoUri") URI infoUri, + @JsonProperty("partialCancelUri") URI partialCancelUri, + @JsonProperty("nextUri") URI nextUri, + @JsonProperty("columns") List columns, + @JsonProperty("data") List> data, + @JsonProperty("stats") StatementStats stats, + @JsonProperty("error") QueryError error, + @JsonProperty("updateType") String updateType, + @JsonProperty("updateCount") Long updateCount) + { + this(id, infoUri, partialCancelUri, nextUri, columns, fixData(columns, data), stats, error, updateType, updateCount); + } + + public QueryResults( + String id, + URI infoUri, + URI partialCancelUri, + URI nextUri, + List columns, + Iterable> data, + StatementStats stats, + QueryError error, + String updateType, + Long updateCount) + { + this.id = checkNotNull(id, "id is null"); + this.infoUri = checkNotNull(infoUri, "infoUri is null"); + this.partialCancelUri = partialCancelUri; + this.nextUri = nextUri; + this.columns = (columns != null) ? ImmutableList.copyOf(columns) : null; + this.data = (data != null) ? unmodifiableIterable(data) : null; + this.stats = checkNotNull(stats, "stats is null"); + this.error = error; + this.updateType = updateType; + this.updateCount = updateCount; + } + + @NotNull + @JsonProperty + public String getId() + { + return id; + } + + @NotNull + @JsonProperty + public URI getInfoUri() + { + return infoUri; + } + + @Nullable + @JsonProperty + public URI getPartialCancelUri() + { + return partialCancelUri; + } + + @Nullable + @JsonProperty + public URI getNextUri() + { + return nextUri; + } + + @Nullable + @JsonProperty + public List getColumns() + { + return columns; + } + + @Nullable + @JsonProperty + public Iterable> getData() + { + return data; + } + + @NotNull + @JsonProperty + public StatementStats getStats() + { + return stats; + } + + @Nullable + @JsonProperty + public QueryError getError() + { + return error; + } + + @Nullable + @JsonProperty + public String getUpdateType() + { + return updateType; + } + + @Nullable + @JsonProperty + public Long getUpdateCount() + { + return updateCount; + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("id", id) + .add("infoUri", infoUri) + .add("partialCancelUri", partialCancelUri) + .add("nextUri", nextUri) + .add("columns", columns) + .add("hasData", data != null) + .add("stats", stats) + .add("error", error) + .add("updateType", updateType) + .add("updateCount", updateCount) + .toString(); + } + + private static Iterable> fixData(List columns, List> data) + { + if (data == null) { + return null; + } + checkNotNull(columns, "columns is null"); + ImmutableList.Builder> rows = ImmutableList.builder(); + for (List row : data) { + checkArgument(row.size() == columns.size(), "row/column size mismatch"); + List newRow = new ArrayList<>(); + for (int i = 0; i < row.size(); i++) { + newRow.add(fixValue(columns.get(i).getType(), row.get(i))); + } + rows.add(unmodifiableList(newRow)); // allow nulls in list + } + return rows.build(); + } + + /** + * Force values coming from Jackson to have the expected object type. + */ + private static Object fixValue(String type, Object value) + { + if (value == null) { + return null; + } + TypeSignature signature = parseTypeSignature(type); + if (signature.getBase().equals(ARRAY)) { + List fixedValue = new ArrayList<>(); + for (Object object : List.class.cast(value)) { + fixedValue.add(fixValue(signature.getParameters().get(0).toString(), object)); + } + return fixedValue; + } + if (signature.getBase().equals(MAP)) { + String keyType = signature.getParameters().get(0).toString(); + String valueType = signature.getParameters().get(1).toString(); + Map fixedValue = new HashMap<>(); + for (Map.Entry entry : (Set>) Map.class.cast(value).entrySet()) { + fixedValue.put(fixValue(keyType, entry.getKey()), fixValue(valueType, entry.getValue())); + } + return fixedValue; + } + if (signature.getBase().equals(ROW)) { + Map fixedValue = new LinkedHashMap<>(); + List listValue = List.class.cast(value); + checkArgument(listValue.size() == signature.getLiteralParameters().size(), "Mismatched data values and row type"); + for (int i = 0; i < listValue.size(); i++) { + String key = (String) signature.getLiteralParameters().get(i); + fixedValue.put(key, fixValue(signature.getParameters().get(i).toString(), listValue.get(i))); + } + return fixedValue; + } + switch (type) { + case BIGINT: + if (value instanceof String) { + return Long.parseLong((String) value); + } + return ((Number) value).longValue(); + case DOUBLE: + if (value instanceof String) { + return Double.parseDouble((String) value); + } + return ((Number) value).doubleValue(); + case BOOLEAN: + if (value instanceof String) { + return Boolean.parseBoolean((String) value); + } + return Boolean.class.cast(value); + case VARCHAR: + case JSON: + case TIME: + case TIME_WITH_TIME_ZONE: + case TIMESTAMP: + case TIMESTAMP_WITH_TIME_ZONE: + case DATE: + case INTERVAL_YEAR_TO_MONTH: + case INTERVAL_DAY_TO_SECOND: + return String.class.cast(value); + default: + // for now we assume that only the explicit types above are passed + // as a plain text and everything else is base64 encoded binary + if (value instanceof String) { + return Base64.getDecoder().decode((String) value); + } + return value; + } + } +} diff --git a/presto-client/src/main/java/com/facebook/presto/client/StageStats.java b/presto-client/src/main/java/com/facebook/presto/client/StageStats.java new file mode 100644 index 00000000..b7ad76c8 --- /dev/null +++ b/presto-client/src/main/java/com/facebook/presto/client/StageStats.java @@ -0,0 +1,312 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.client; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; + +import javax.annotation.concurrent.Immutable; +import javax.validation.constraints.NotNull; + +import java.util.List; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkNotNull; + +@Immutable +public class StageStats +{ + private final String stageId; + private final String state; + private final boolean done; + private final int nodes; + private final int totalSplits; + private final int queuedSplits; + private final int runningSplits; + private final int completedSplits; + private final long userTimeMillis; + private final long cpuTimeMillis; + private final long wallTimeMillis; + private final long processedRows; + private final long processedBytes; + private final List subStages; + + @JsonCreator + public StageStats( + @JsonProperty("stageId") String stageId, + @JsonProperty("state") String state, + @JsonProperty("done") boolean done, + @JsonProperty("nodes") int nodes, + @JsonProperty("totalSplits") int totalSplits, + @JsonProperty("queuedSplits") int queuedSplits, + @JsonProperty("runningSplits") int runningSplits, + @JsonProperty("completedSplits") int completedSplits, + @JsonProperty("userTimeMillis") long userTimeMillis, + @JsonProperty("cpuTimeMillis") long cpuTimeMillis, + @JsonProperty("wallTimeMillis") long wallTimeMillis, + @JsonProperty("processedRows") long processedRows, + @JsonProperty("processedBytes") long processedBytes, + @JsonProperty("subStages") List subStages) + { + this.stageId = stageId; + this.state = checkNotNull(state, "state is null"); + this.done = done; + this.nodes = nodes; + this.totalSplits = totalSplits; + this.queuedSplits = queuedSplits; + this.runningSplits = runningSplits; + this.completedSplits = completedSplits; + this.userTimeMillis = userTimeMillis; + this.cpuTimeMillis = cpuTimeMillis; + this.wallTimeMillis = wallTimeMillis; + this.processedRows = processedRows; + this.processedBytes = processedBytes; + this.subStages = ImmutableList.copyOf(checkNotNull(subStages, "subStages is null")); + } + + @JsonProperty + public String getStageId() + { + return stageId; + } + + @NotNull + @JsonProperty + public String getState() + { + return state; + } + + @JsonProperty + public boolean isDone() + { + return done; + } + + @JsonProperty + public int getNodes() + { + return nodes; + } + + @JsonProperty + public int getTotalSplits() + { + return totalSplits; + } + + @JsonProperty + public int getQueuedSplits() + { + return queuedSplits; + } + + @JsonProperty + public int getRunningSplits() + { + return runningSplits; + } + + @JsonProperty + public int getCompletedSplits() + { + return completedSplits; + } + + @JsonProperty + public long getUserTimeMillis() + { + return userTimeMillis; + } + + @JsonProperty + public long getCpuTimeMillis() + { + return cpuTimeMillis; + } + + @JsonProperty + public long getWallTimeMillis() + { + return wallTimeMillis; + } + + @JsonProperty + public long getProcessedRows() + { + return processedRows; + } + + @JsonProperty + public long getProcessedBytes() + { + return processedBytes; + } + + @NotNull + @JsonProperty + public List getSubStages() + { + return subStages; + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("state", state) + .add("done", done) + .add("nodes", nodes) + .add("totalSplits", totalSplits) + .add("queuedSplits", queuedSplits) + .add("runningSplits", runningSplits) + .add("completedSplits", completedSplits) + .add("userTimeMillis", userTimeMillis) + .add("cpuTimeMillis", cpuTimeMillis) + .add("wallTimeMillis", wallTimeMillis) + .add("processedRows", processedRows) + .add("processedBytes", processedBytes) + .add("subStages", subStages) + .toString(); + } + + public static Builder builder() + { + return new Builder(); + } + + public static class Builder + { + private String stageId; + private String state; + private boolean done; + private int nodes; + private int totalSplits; + private int queuedSplits; + private int runningSplits; + private int completedSplits; + private long userTimeMillis; + private long cpuTimeMillis; + private long wallTimeMillis; + private long processedRows; + private long processedBytes; + private List subStages; + + private Builder() {} + + public Builder setStageId(String stageId) + { + this.stageId = checkNotNull(stageId, "stageId is null"); + return this; + } + + public Builder setState(String state) + { + this.state = checkNotNull(state, "state is null"); + return this; + } + + public Builder setDone(boolean done) + { + this.done = done; + return this; + } + + public Builder setNodes(int nodes) + { + this.nodes = nodes; + return this; + } + + public Builder setTotalSplits(int totalSplits) + { + this.totalSplits = totalSplits; + return this; + } + + public Builder setQueuedSplits(int queuedSplits) + { + this.queuedSplits = queuedSplits; + return this; + } + + public Builder setRunningSplits(int runningSplits) + { + this.runningSplits = runningSplits; + return this; + } + + public Builder setCompletedSplits(int completedSplits) + { + this.completedSplits = completedSplits; + return this; + } + + public Builder setUserTimeMillis(long userTimeMillis) + { + this.userTimeMillis = userTimeMillis; + return this; + } + + public Builder setCpuTimeMillis(long cpuTimeMillis) + { + this.cpuTimeMillis = cpuTimeMillis; + return this; + } + + public Builder setWallTimeMillis(long wallTimeMillis) + { + this.wallTimeMillis = wallTimeMillis; + return this; + } + + public Builder setProcessedRows(long processedRows) + { + this.processedRows = processedRows; + return this; + } + + public Builder setProcessedBytes(long processedBytes) + { + this.processedBytes = processedBytes; + return this; + } + + public Builder setSubStages(List subStages) + { + this.subStages = ImmutableList.copyOf(checkNotNull(subStages, "subStages is null")); + return this; + } + + public StageStats build() + { + return new StageStats( + stageId, + state, + done, + nodes, + totalSplits, + queuedSplits, + runningSplits, + completedSplits, + userTimeMillis, + cpuTimeMillis, + wallTimeMillis, + processedRows, + processedBytes, + subStages); + } + } +} diff --git a/presto-client/src/main/java/com/facebook/presto/client/StatementClient.java b/presto-client/src/main/java/com/facebook/presto/client/StatementClient.java new file mode 100644 index 00000000..f2be1202 --- /dev/null +++ b/presto-client/src/main/java/com/facebook/presto/client/StatementClient.java @@ -0,0 +1,320 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.client; + +import com.google.common.base.Splitter; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; +import io.airlift.http.client.FullJsonResponseHandler; +import io.airlift.http.client.HttpClient; +import io.airlift.http.client.HttpClient.HttpResponseFuture; +import io.airlift.http.client.HttpStatus; +import io.airlift.http.client.Request; +import io.airlift.json.JsonCodec; +import io.airlift.units.Duration; + +import javax.annotation.concurrent.ThreadSafe; + +import java.io.Closeable; +import java.net.URI; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import static com.facebook.presto.client.PrestoHeaders.PRESTO_CLEAR_SESSION; +import static com.facebook.presto.client.PrestoHeaders.PRESTO_SET_SESSION; +import static com.google.common.base.MoreObjects.firstNonNull; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.net.HttpHeaders.USER_AGENT; +import static io.airlift.http.client.FullJsonResponseHandler.JsonResponse; +import static io.airlift.http.client.FullJsonResponseHandler.createFullJsonResponseHandler; +import static io.airlift.http.client.HttpStatus.Family; +import static io.airlift.http.client.HttpStatus.familyForStatusCode; +import static io.airlift.http.client.HttpUriBuilder.uriBuilderFrom; +import static io.airlift.http.client.Request.Builder.prepareDelete; +import static io.airlift.http.client.Request.Builder.prepareGet; +import static io.airlift.http.client.Request.Builder.preparePost; +import static io.airlift.http.client.StaticBodyGenerator.createStaticBodyGenerator; +import static io.airlift.http.client.StatusResponseHandler.StatusResponse; +import static io.airlift.http.client.StatusResponseHandler.createStatusResponseHandler; +import static java.lang.String.format; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.MINUTES; + +@ThreadSafe +public class StatementClient + implements Closeable +{ + private static final Splitter SESSION_HEADER_SPLITTER = Splitter.on('=').limit(2).trimResults(); + private static final String USER_AGENT_VALUE = StatementClient.class.getSimpleName() + + "/" + + firstNonNull(StatementClient.class.getPackage().getImplementationVersion(), "unknown"); + + private final HttpClient httpClient; + private final FullJsonResponseHandler responseHandler; + private final boolean debug; + private final String query; + private final AtomicReference currentResults = new AtomicReference<>(); + private final Map setSessionProperties = new ConcurrentHashMap<>(); + private final Set resetSessionProperties = Sets.newConcurrentHashSet(); + private final AtomicBoolean closed = new AtomicBoolean(); + private final AtomicBoolean gone = new AtomicBoolean(); + private final AtomicBoolean valid = new AtomicBoolean(true); + private final String timeZoneId; + + public StatementClient(HttpClient httpClient, JsonCodec queryResultsCodec, ClientSession session, String query) + { + checkNotNull(httpClient, "httpClient is null"); + checkNotNull(queryResultsCodec, "queryResultsCodec is null"); + checkNotNull(session, "session is null"); + checkNotNull(query, "query is null"); + + this.httpClient = httpClient; + this.responseHandler = createFullJsonResponseHandler(queryResultsCodec); + this.debug = session.isDebug(); + this.timeZoneId = session.getTimeZoneId(); + this.query = query; + + Request request = buildQueryRequest(session, query); + JsonResponse response = httpClient.execute(request, responseHandler); + + if (response.getStatusCode() != HttpStatus.OK.code() || !response.hasValue()) { + throw requestFailedException("starting query", request, response); + } + + processResponse(response); + } + + private static Request buildQueryRequest(ClientSession session, String query) + { + Request.Builder builder = preparePost() + .setUri(uriBuilderFrom(session.getServer()).replacePath("/v1/statement").build()) + .setBodyGenerator(createStaticBodyGenerator(query, UTF_8)); + + if (session.getUser() != null) { + builder.setHeader(PrestoHeaders.PRESTO_USER, session.getUser()); + } + if (session.getSource() != null) { + builder.setHeader(PrestoHeaders.PRESTO_SOURCE, session.getSource()); + } + if (session.getCatalog() != null) { + builder.setHeader(PrestoHeaders.PRESTO_CATALOG, session.getCatalog()); + } + if (session.getSchema() != null) { + builder.setHeader(PrestoHeaders.PRESTO_SCHEMA, session.getSchema()); + } + builder.setHeader(PrestoHeaders.PRESTO_TIME_ZONE, session.getTimeZoneId()); + builder.setHeader(PrestoHeaders.PRESTO_LANGUAGE, session.getLocale().toLanguageTag()); + builder.setHeader(USER_AGENT, USER_AGENT_VALUE); + + Map property = session.getProperties(); + for (Entry entry : property.entrySet()) { + builder.addHeader(PrestoHeaders.PRESTO_SESSION, entry.getKey() + "=" + entry.getValue()); + } + + return builder.build(); + } + + public String getQuery() + { + return query; + } + + public String getTimeZoneId() + { + return timeZoneId; + } + + public boolean isDebug() + { + return debug; + } + + public boolean isClosed() + { + return closed.get(); + } + + public boolean isGone() + { + return gone.get(); + } + + public boolean isFailed() + { + return currentResults.get().getError() != null; + } + + public QueryResults current() + { + checkState(isValid(), "current position is not valid (cursor past end)"); + return currentResults.get(); + } + + public QueryResults finalResults() + { + checkState((!isValid()) || isFailed(), "current position is still valid"); + return currentResults.get(); + } + + public Map getSetSessionProperties() + { + return ImmutableMap.copyOf(setSessionProperties); + } + + public Set getResetSessionProperties() + { + return ImmutableSet.copyOf(resetSessionProperties); + } + + public boolean isValid() + { + return valid.get() && (!isGone()) && (!isClosed()); + } + + public boolean advance() + { + URI nextUri = current().getNextUri(); + if (isClosed() || (nextUri == null)) { + valid.set(false); + return false; + } + + Request request = prepareGet() + .setHeader(USER_AGENT, USER_AGENT_VALUE) + .setUri(nextUri) + .build(); + + Exception cause = null; + long start = System.nanoTime(); + long attempts = 0; + + do { + // back-off on retry + if (attempts > 0) { + try { + MILLISECONDS.sleep(attempts * 100); + } + catch (InterruptedException e) { + close(); + Thread.currentThread().isInterrupted(); + throw new RuntimeException("StatementClient thread was interrupted"); + } + } + attempts++; + + JsonResponse response; + try { + response = httpClient.execute(request, responseHandler); + } + catch (RuntimeException e) { + cause = e; + continue; + } + + if (response.getStatusCode() == HttpStatus.OK.code() && response.hasValue()) { + processResponse(response); + return true; + } + + if (response.getStatusCode() != HttpStatus.SERVICE_UNAVAILABLE.code()) { + throw requestFailedException("fetching next", request, response); + } + } + while ((System.nanoTime() - start) < MINUTES.toNanos(2) && !isClosed()); + + gone.set(true); + throw new RuntimeException("Error fetching next", cause); + } + + private void processResponse(JsonResponse response) + { + for (String setSession : response.getHeaders().get(PRESTO_SET_SESSION)) { + List keyValue = SESSION_HEADER_SPLITTER.splitToList(setSession); + if (keyValue.size() != 2) { + continue; + } + setSessionProperties.put(keyValue.get(0), keyValue.size() > 1 ? keyValue.get(1) : ""); + } + for (String clearSession : response.getHeaders().get(PRESTO_CLEAR_SESSION)) { + resetSessionProperties.add(clearSession); + } + currentResults.set(response.getValue()); + } + + private RuntimeException requestFailedException(String task, Request request, JsonResponse response) + { + gone.set(true); + if (!response.hasValue()) { + return new RuntimeException(format("Error %s at %s returned an invalid response: %s", task, request.getUri(), response), response.getException()); + } + return new RuntimeException(format("Error %s at %s returned %s: %s", task, request.getUri(), response.getStatusCode(), response.getStatusMessage())); + } + + public boolean cancelLeafStage(Duration timeout) + { + checkState(!isClosed(), "client is closed"); + + URI uri = current().getPartialCancelUri(); + if (uri == null) { + return false; + } + + Request request = prepareDelete() + .setHeader(USER_AGENT, USER_AGENT_VALUE) + .setUri(uri) + .build(); + + HttpResponseFuture response = httpClient.executeAsync(request, createStatusResponseHandler()); + try { + StatusResponse status = response.get(timeout.toMillis(), MILLISECONDS); + return familyForStatusCode(status.getStatusCode()) == Family.SUCCESSFUL; + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw Throwables.propagate(e); + } + catch (ExecutionException e) { + throw Throwables.propagate(e.getCause()); + } + catch (TimeoutException e) { + return false; + } + } + + @Override + public void close() + { + if (!closed.getAndSet(true)) { + URI uri = currentResults.get().getNextUri(); + if (uri != null) { + Request request = prepareDelete() + .setHeader(USER_AGENT, USER_AGENT_VALUE) + .setUri(uri) + .build(); + httpClient.executeAsync(request, createStatusResponseHandler()); + } + } + } +} diff --git a/presto-client/src/main/java/com/facebook/presto/client/StatementStats.java b/presto-client/src/main/java/com/facebook/presto/client/StatementStats.java new file mode 100644 index 00000000..56b19590 --- /dev/null +++ b/presto-client/src/main/java/com/facebook/presto/client/StatementStats.java @@ -0,0 +1,293 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.client; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; +import javax.validation.constraints.NotNull; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkNotNull; + +@Immutable +public class StatementStats +{ + private final String state; + private final boolean scheduled; + private final int nodes; + private final int totalSplits; + private final int queuedSplits; + private final int runningSplits; + private final int completedSplits; + private final long userTimeMillis; + private final long cpuTimeMillis; + private final long wallTimeMillis; + private final long processedRows; + private final long processedBytes; + private final StageStats rootStage; + + @JsonCreator + public StatementStats( + @JsonProperty("state") String state, + @JsonProperty("scheduled") boolean scheduled, + @JsonProperty("nodes") int nodes, + @JsonProperty("totalSplits") int totalSplits, + @JsonProperty("queuedSplits") int queuedSplits, + @JsonProperty("runningSplits") int runningSplits, + @JsonProperty("completedSplits") int completedSplits, + @JsonProperty("userTimeMillis") long userTimeMillis, + @JsonProperty("cpuTimeMillis") long cpuTimeMillis, + @JsonProperty("wallTimeMillis") long wallTimeMillis, + @JsonProperty("processedRows") long processedRows, + @JsonProperty("processedBytes") long processedBytes, + @JsonProperty("rootStage") StageStats rootStage) + { + this.state = checkNotNull(state, "state is null"); + this.scheduled = scheduled; + this.nodes = nodes; + this.totalSplits = totalSplits; + this.queuedSplits = queuedSplits; + this.runningSplits = runningSplits; + this.completedSplits = completedSplits; + this.userTimeMillis = userTimeMillis; + this.cpuTimeMillis = cpuTimeMillis; + this.wallTimeMillis = wallTimeMillis; + this.processedRows = processedRows; + this.processedBytes = processedBytes; + this.rootStage = rootStage; + } + + @NotNull + @JsonProperty + public String getState() + { + return state; + } + + @JsonProperty + public boolean isScheduled() + { + return scheduled; + } + + @JsonProperty + public int getNodes() + { + return nodes; + } + + @JsonProperty + public int getTotalSplits() + { + return totalSplits; + } + + @JsonProperty + public int getQueuedSplits() + { + return queuedSplits; + } + + @JsonProperty + public int getRunningSplits() + { + return runningSplits; + } + + @JsonProperty + public int getCompletedSplits() + { + return completedSplits; + } + + @JsonProperty + public long getUserTimeMillis() + { + return userTimeMillis; + } + + @JsonProperty + public long getCpuTimeMillis() + { + return cpuTimeMillis; + } + + @JsonProperty + public long getWallTimeMillis() + { + return wallTimeMillis; + } + + @JsonProperty + public long getProcessedRows() + { + return processedRows; + } + + @JsonProperty + public long getProcessedBytes() + { + return processedBytes; + } + + @Nullable + @JsonProperty + public StageStats getRootStage() + { + return rootStage; + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("state", state) + .add("scheduled", scheduled) + .add("nodes", nodes) + .add("totalSplits", totalSplits) + .add("queuedSplits", queuedSplits) + .add("runningSplits", runningSplits) + .add("completedSplits", completedSplits) + .add("userTimeMillis", userTimeMillis) + .add("cpuTimeMillis", cpuTimeMillis) + .add("wallTimeMillis", wallTimeMillis) + .add("processedRows", processedRows) + .add("processedBytes", processedBytes) + .add("rootStage", rootStage) + .toString(); + } + + public static Builder builder() + { + return new Builder(); + } + + public static class Builder + { + private String state; + private boolean scheduled; + private int nodes; + private int totalSplits; + private int queuedSplits; + private int runningSplits; + private int completedSplits; + private long userTimeMillis; + private long cpuTimeMillis; + private long wallTimeMillis; + private long processedRows; + private long processedBytes; + private StageStats rootStage; + + private Builder() {} + + public Builder setState(String state) + { + this.state = checkNotNull(state, "state is null"); + return this; + } + + public Builder setNodes(int nodes) + { + this.nodes = nodes; + return this; + } + + public Builder setScheduled(boolean scheduled) + { + this.scheduled = scheduled; + return this; + } + + public Builder setTotalSplits(int totalSplits) + { + this.totalSplits = totalSplits; + return this; + } + + public Builder setQueuedSplits(int queuedSplits) + { + this.queuedSplits = queuedSplits; + return this; + } + + public Builder setRunningSplits(int runningSplits) + { + this.runningSplits = runningSplits; + return this; + } + + public Builder setCompletedSplits(int completedSplits) + { + this.completedSplits = completedSplits; + return this; + } + + public Builder setUserTimeMillis(long userTimeMillis) + { + this.userTimeMillis = userTimeMillis; + return this; + } + + public Builder setCpuTimeMillis(long cpuTimeMillis) + { + this.cpuTimeMillis = cpuTimeMillis; + return this; + } + + public Builder setWallTimeMillis(long wallTimeMillis) + { + this.wallTimeMillis = wallTimeMillis; + return this; + } + + public Builder setProcessedRows(long processedRows) + { + this.processedRows = processedRows; + return this; + } + + public Builder setProcessedBytes(long processedBytes) + { + this.processedBytes = processedBytes; + return this; + } + + public Builder setRootStage(StageStats rootStage) + { + this.rootStage = rootStage; + return this; + } + + public StatementStats build() + { + return new StatementStats( + state, + scheduled, + nodes, + totalSplits, + queuedSplits, + runningSplits, + completedSplits, + userTimeMillis, + cpuTimeMillis, + wallTimeMillis, + processedRows, + processedBytes, + rootStage); + } + } +} diff --git a/presto-docs/Makefile b/presto-docs/Makefile new file mode 100644 index 00000000..30612cd5 --- /dev/null +++ b/presto-docs/Makefile @@ -0,0 +1,178 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = target +SOURCEDIR = src/main/sphinx + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -W -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) $(SOURCEDIR) +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) $(SOURCEDIR) + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Presto.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Presto.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/Presto" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Presto" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/presto-docs/pom.xml b/presto-docs/pom.xml new file mode 100644 index 00000000..7ee24741 --- /dev/null +++ b/presto-docs/pom.xml @@ -0,0 +1,101 @@ + + + 4.0.0 + + + com.facebook.presto + presto-root + 0.107 + + + presto-docs + presto-docs + pom + + + ${project.parent.basedir} + + + + + + + + + com.mycila + license-maven-plugin + + + **/*.conf + **/*.css_t + + + + + + + + + org.tomdz.maven + sphinx-maven-plugin + + true + true + true + ${project.basedir}/src/main/sphinx + ${project.build.directory}/html + + + + package + + generate + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + docs + package + + single + + + false + + src/main/assembly/docs.xml + + + + + + sources + package + + single + + + + src/main/assembly/sources.xml + + + + + + + + diff --git a/presto-docs/src/main/assembly/docs.xml b/presto-docs/src/main/assembly/docs.xml new file mode 100644 index 00000000..34157e72 --- /dev/null +++ b/presto-docs/src/main/assembly/docs.xml @@ -0,0 +1,19 @@ + + docs + + zip + + false + + + ${project.build.directory}/html + /html + + **/.buildinfo + **/.doctrees/** + + + + diff --git a/presto-docs/src/main/assembly/sources.xml b/presto-docs/src/main/assembly/sources.xml new file mode 100644 index 00000000..6a22da3e --- /dev/null +++ b/presto-docs/src/main/assembly/sources.xml @@ -0,0 +1,15 @@ + + sources + + jar + + false + + + ${project.basedir}/src/main/sphinx + / + + + diff --git a/presto-docs/src/main/resources/design.graffle b/presto-docs/src/main/resources/design.graffle new file mode 100644 index 00000000..e22f7fee --- /dev/null +++ b/presto-docs/src/main/resources/design.graffle @@ -0,0 +1,7371 @@ + + + + + ApplicationVersion + + com.omnigroup.OmniGrafflePro + 139.18.0.187838 + + CreationDate + 2013-08-08 16:12:04 +0000 + Creator + Martin Traverso + GraphDocumentVersion + 8 + GuidesLocked + NO + GuidesVisible + YES + ImageCounter + 1 + LinksVisible + NO + MagnetsVisible + NO + MasterSheets + + ModificationDate + 2013-09-04 18:34:53 +0000 + Modifier + Martin Traverso + NotesVisible + NO + OriginVisible + NO + PageBreaks + YES + PrintInfo + + NSBottomMargin + + float + 41 + + NSHorizonalPagination + + coded + BAtzdHJlYW10eXBlZIHoA4QBQISEhAhOU051bWJlcgCEhAdOU1ZhbHVlAISECE5TT2JqZWN0AIWEASqEhAFxlwCG + + NSLeftMargin + + float + 18 + + NSPaperSize + + size + {612, 792} + + NSPrintReverseOrientation + + int + 0 + + NSRightMargin + + float + 18 + + NSTopMargin + + float + 18 + + + ReadOnly + NO + Sheets + + + ActiveLayerIndex + 0 + AutoAdjust + + BackgroundGraphic + + Bounds + {{0, 0}, {576, 733}} + Class + SolidGraphic + ID + 2 + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + + BaseZoom + 0 + CanvasOrigin + {0, 0} + ColumnAlign + 1 + ColumnSpacing + 36 + DisplayScale + 1 0/72 in = 1.0000 in + GraphicsList + + + Bounds + {{320.5, 706.00000095367432}, {85, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 101 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Distributed Plan} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{302.49999717655345, 486.00000071531622}, {61, 41.440422058105469}} + Class + ShapedGraphic + ID + 100 + Shape + Rectangle + Style + + fill + + Draws + NO + + stroke + + Pattern + 2 + + + + + Bounds + {{269.99999717655351, 541.74607157713262}, {91, 157.2547607421875}} + Class + ShapedGraphic + ID + 98 + Shape + Rectangle + Style + + fill + + Draws + NO + + stroke + + Pattern + 2 + + + + + Bounds + {{380.37499145717373, 622.11997141434006}, {61, 76.880859375}} + Class + ShapedGraphic + ID + 95 + Line + + ID + 94 + Position + 0.43169048428535461 + RotationType + 0 + + Shape + Rectangle + Style + + fill + + Draws + NO + + stroke + + Pattern + 2 + + + + + Class + LineGraphic + Head + + ID + 88 + + ID + 94 + Points + + {410.87499145717373, 654.24607768753674} + {410.87499145717373, 668.873046879958} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 0.5 + + + Tail + + ID + 87 + + + + Class + LineGraphic + Head + + ID + 86 + + ID + 93 + Points + + {300.50001108886909, 648.24607768741544} + {300.50001108886909, 662.5000000050793} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 0.5 + + + Tail + + ID + 85 + + + + Class + LineGraphic + Head + + ID + 85 + + ID + 92 + Points + + {300.50000901314581, 612.87303081253674} + {300.50000901314581, 627.500000004958} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 0.5 + + + Tail + + ID + 84 + + + + Class + LineGraphic + Head + + ID + 87 + + ID + 91 + Points + + {343.34077766157264, 577.09762799475391} + {400.53422233853991, 633.64844969774083} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 0.5 + + + Tail + + ID + 83 + + + + Class + LineGraphic + Head + + ID + 84 + + ID + 90 + Points + + {323.65654004767418, 577.11546991538057} + {309.84345995232593, 592.25756090211416} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 0.5 + + + Tail + + ID + 83 + + + + Class + LineGraphic + Head + + ID + 83 + + ID + 89 + Points + + {332.99999382637952, 516.24607770217517} + {332.99999382637952, 556.49999999031957} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 0.5 + + + Tail + + ID + 82 + + + + Bounds + {{398.375, 669.373046875}, {25, 19.746077692494694}} + Class + ShapedGraphic + ID + 88 + Shape + Rectangle + + + Bounds + {{398.375, 634}, {25, 19.746077692494694}} + Class + ShapedGraphic + ID + 87 + Shape + Rectangle + + + Bounds + {{288, 663}, {25, 19.746077692494694}} + Class + ShapedGraphic + ID + 86 + Shape + Rectangle + + + Bounds + {{288, 628}, {25, 19.746077692494694}} + Class + ShapedGraphic + ID + 85 + Shape + Rectangle + + + Bounds + {{288, 592.626953125}, {25, 19.746077692494694}} + Class + ShapedGraphic + ID + 84 + Shape + Rectangle + + + Bounds + {{320.5, 557}, {25, 19.746077692494694}} + Class + ShapedGraphic + ID + 83 + Shape + Rectangle + + + Bounds + {{320.5, 496}, {25, 19.746077692494694}} + Class + ShapedGraphic + ID + 82 + Shape + Rectangle + + + Bounds + {{141, 499.74607849121094}, {61, 28}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 81 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Distributed \ +Planner} + VerticalPad + 0 + + Wrap + NO + + + Class + LineGraphic + ID + 80 + Points + + {181.625, 468} + {247, 534} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + + + Bounds + {{132.125, 426}, {82, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 78 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Optimized Plan} + VerticalPad + 0 + + Wrap + NO + + + Class + LineGraphic + Head + + ID + 70 + + ID + 77 + Points + + {205.12500901314729, 333.87303081253668} + {205.12500901314729, 348.50000000495805} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 0.5 + + + Tail + + ID + 69 + + + + Class + LineGraphic + Head + + ID + 68 + + ID + 76 + Points + + {136.62501108887309, 369.24607768741544} + {136.62501108887309, 383.5000000050793} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 0.5 + + + Tail + + ID + 67 + + + + Class + LineGraphic + Head + + ID + 67 + + ID + 75 + Points + + {136.62500901314587, 333.87303081253668} + {136.62500901314587, 348.50000000495805} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 0.5 + + + Tail + + ID + 66 + + + + Class + LineGraphic + Head + + ID + 69 + + ID + 74 + Points + + {179.45680854777677, 298.09778492480621} + {194.79319145222317, 313.27524589268853} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 0.5 + + + Tail + + ID + 65 + + + + Class + LineGraphic + Head + + ID + 66 + + ID + 73 + Points + + {159.78154004767413, 298.11546991538057} + {145.96845995232587, 313.25756090211416} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 0.5 + + + Tail + + ID + 65 + + + + Class + LineGraphic + Head + + ID + 65 + + ID + 72 + Points + + {169.12501460389518, 266.24607768617187} + {169.12501460389518, 277.50000000632281} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 0.5 + + + Tail + + ID + 64 + + + + Bounds + {{192.625, 349}, {25, 19.746077692494694}} + Class + ShapedGraphic + ID + 70 + Shape + Rectangle + + + Bounds + {{192.625, 313.626953125}, {25, 19.746077692494694}} + Class + ShapedGraphic + ID + 69 + Shape + Rectangle + + + Bounds + {{124.125, 384}, {25, 19.746077692494694}} + Class + ShapedGraphic + ID + 68 + Shape + Rectangle + + + Bounds + {{124.125, 349}, {25, 19.746077692494694}} + Class + ShapedGraphic + ID + 67 + Shape + Rectangle + + + Bounds + {{124.125, 313.626953125}, {25, 19.746077692494694}} + Class + ShapedGraphic + ID + 66 + Shape + Rectangle + + + Bounds + {{156.625, 278}, {25, 19.746077692494694}} + Class + ShapedGraphic + ID + 65 + Shape + Rectangle + + + Bounds + {{156.625, 246}, {25, 19.746077692494694}} + Class + ShapedGraphic + ID + 64 + Shape + Rectangle + + + Bounds + {{383.875, 440.50000095367432}, {54, 28}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 62 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Relational\ +Plan} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{277.625, 278}, {52, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 61 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Optimizer} + VerticalPad + 0 + + Wrap + NO + + + Class + LineGraphic + ID + 60 + Points + + {325.625, 312} + {270.625, 312.126953125} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + + + Class + LineGraphic + Head + + ID + 51 + + ID + 59 + Points + + {445.3750132425497, 354.37303081253668} + {445.3750132425497, 369.00000000495805} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 0.5 + + + Tail + + ID + 50 + + + + Class + LineGraphic + Head + + ID + 49 + + ID + 58 + Points + + {376.87501987114558, 389.74607768741544} + {376.87501987114558, 404.0000000050793} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 0.5 + + + Tail + + ID + 48 + + + + Class + LineGraphic + Head + + ID + 48 + + ID + 57 + Points + + {376.87500928453386, 354.37303081253668} + {376.87500928453386, 369.00000000495805} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 0.5 + + + Tail + + ID + 47 + + + + Class + LineGraphic + Head + + ID + 50 + + ID + 56 + Points + + {419.7068085477768, 318.59778492480621} + {435.0431914522232, 333.77524589268853} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 0.5 + + + Tail + + ID + 46 + + + + Class + LineGraphic + Head + + ID + 47 + + ID + 55 + Points + + {400.03154004767418, 318.61546991538052} + {386.21845995232587, 333.75756090211416} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 0.5 + + + Tail + + ID + 46 + + + + Class + LineGraphic + Head + + ID + 46 + + ID + 54 + Points + + {409.37501074990246, 286.74607768617193} + {409.37501074990246, 298.00000000632281} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 0.5 + + + Tail + + ID + 45 + + + + Class + LineGraphic + Head + + ID + 45 + + ID + 53 + Points + + {409.37498761828465, 254.74607768617187} + {409.37498761828465, 266.00000000632281} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 0.5 + + + Tail + + ID + 44 + + + + Bounds + {{432.875, 369.5}, {25, 19.746077692494694}} + Class + ShapedGraphic + ID + 51 + Shape + Rectangle + + + Bounds + {{432.875, 334.126953125}, {25, 19.746077692494694}} + Class + ShapedGraphic + ID + 50 + Shape + Rectangle + + + Bounds + {{364.375, 404.5}, {25, 19.746077692494694}} + Class + ShapedGraphic + ID + 49 + Shape + Rectangle + + + Bounds + {{364.375, 369.5}, {25, 19.746077692494694}} + Class + ShapedGraphic + ID + 48 + Shape + Rectangle + + + Bounds + {{364.375, 334.126953125}, {25, 19.746077692494694}} + Class + ShapedGraphic + ID + 47 + Shape + Rectangle + + + Bounds + {{396.875, 298.5}, {25, 19.746077692494694}} + Class + ShapedGraphic + ID + 46 + Shape + Rectangle + + + Bounds + {{396.875, 266.5}, {25, 19.746077692494694}} + Class + ShapedGraphic + ID + 45 + Shape + Rectangle + + + Bounds + {{396.875, 234.5}, {25, 19.746077692494694}} + Class + ShapedGraphic + ID + 44 + Shape + Rectangle + + + Bounds + {{424, 189}, {42, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 43 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Planner} + VerticalPad + 0 + + Wrap + NO + + + Class + LineGraphic + ID + 42 + Points + + {410, 168} + {411, 224} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + + + Bounds + {{320.5, 62}, {47, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 41 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Analyzer} + VerticalPad + 0 + + Wrap + NO + + + Class + LineGraphic + ID + 40 + Points + + {318, 86} + {373, 86} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + + + Bounds + {{484, 103}, {70, 37}} + Class + ShapedGraphic + ID + 38 + Shape + Rectangle + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Tuple descriptors} + + + + Bounds + {{484, 73}, {70, 30}} + Class + ShapedGraphic + ID + 37 + Shape + Rectangle + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Type map} + + + + Bounds + {{423, 34}, {108, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 36 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 SQL AST + Analysis} + VerticalPad + 0 + + Wrap + NO + + + Class + LineGraphic + Head + + ID + 29 + + ID + 35 + Points + + {457.00001068774537, 110.50000451341333} + {457.00001068774537, 129.49999548658818} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 0.5 + + + Tail + + ID + 26 + + + + Class + LineGraphic + Head + + ID + 28 + + ID + 34 + Points + + {401.64201507082316, 110.12696115375265} + {407.35798492917684, 129.87303884624734} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 0.5 + + + Tail + + ID + 24 + + + + Class + LineGraphic + Head + + ID + 27 + + ID + 33 + Points + + {394.75037992234024, 109.49924015531948} + {384.24962007765981, 130.50075984468046} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 0.5 + + + Tail + + ID + 24 + + + + Class + LineGraphic + Head + + ID + 26 + + ID + 32 + Points + + {434.16528347908735, 74.228263389274844} + {450.83471652091265, 93.771736610725185} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 0.5 + + + Tail + + ID + 23 + + + + Class + LineGraphic + Head + + ID + 25 + + ID + 31 + Points + + {428.00000886257573, 76.500004486967441} + {428.00000886257573, 91.499995513033838} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 0.5 + + + Tail + + ID + 23 + + + + Class + LineGraphic + Head + + ID + 24 + + ID + 30 + Points + + {421.83471737756685, 74.228262384921635} + {405.16528262243315, 93.771737615078365} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 0.5 + + + Tail + + ID + 23 + + + + Bounds + {{448, 130}, {18, 18}} + Class + ShapedGraphic + ID + 29 + Shape + Circle + Style + + + + Bounds + {{401, 130}, {18, 18}} + Class + ShapedGraphic + ID + 28 + Shape + Circle + Style + + + + Bounds + {{371, 130}, {18, 18}} + Class + ShapedGraphic + ID + 27 + Shape + Circle + Style + + + + Bounds + {{448, 92}, {18, 18}} + Class + ShapedGraphic + ID + 26 + Shape + Circle + Style + + + + Bounds + {{419, 92}, {18, 18}} + Class + ShapedGraphic + ID + 25 + Shape + Circle + Style + + + + Bounds + {{390, 92}, {18, 18}} + Class + ShapedGraphic + ID + 24 + Shape + Circle + Style + + + + Bounds + {{419, 58}, {18, 18}} + Class + ShapedGraphic + ID + 23 + Shape + Circle + Style + + + + Bounds + {{150.5, 62}, {36, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 22 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Parser} + VerticalPad + 0 + + Wrap + NO + + + Class + LineGraphic + ID + 21 + Points + + {141, 86} + {196, 86} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + + + Bounds + {{56, 34}, {25, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 20 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 SQL} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{235, 34}, {50, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 19 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 SQL AST} + VerticalPad + 0 + + Wrap + NO + + + Class + LineGraphic + Head + + ID + 12 + + ID + 18 + Points + + {291, 110.50000451341856} + {291, 129.49999548658141} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 0.5 + + + Tail + + ID + 9 + + + + Class + LineGraphic + Head + + ID + 11 + + ID + 17 + Points + + {235.64201507082313, 110.12696115375265} + {241.35798492917684, 129.87303884624734} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 0.5 + + + Tail + + ID + 5 + + + + Class + LineGraphic + Head + + ID + 10 + + ID + 16 + Points + + {228.75037992234024, 109.49924015531953} + {218.24962007765976, 130.50075984468046} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 0.5 + + + Tail + + ID + 5 + + + + Class + LineGraphic + Head + + ID + 9 + + ID + 15 + Points + + {268.16528347908735, 74.228263389274829} + {284.83471652091265, 93.771736610725156} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 0.5 + + + Tail + + ID + 4 + + + + Class + LineGraphic + Head + + ID + 8 + + ID + 14 + Points + + {262.00001860838807, 76.500004486954282} + {262.00001860838807, 91.49999551304839} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 0.5 + + + Tail + + ID + 4 + + + + Class + LineGraphic + Head + + ID + 5 + + ID + 13 + Points + + {255.83471737756685, 74.228262384921663} + {239.16528262243315, 93.771737615078351} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 0.5 + + + Tail + + ID + 4 + + + + Bounds + {{282, 130}, {18, 18}} + Class + ShapedGraphic + ID + 12 + Shape + Circle + Style + + + + Bounds + {{235, 130}, {18, 18}} + Class + ShapedGraphic + ID + 11 + Shape + Circle + Style + + + + Bounds + {{205, 130}, {18, 18}} + Class + ShapedGraphic + ID + 10 + Shape + Circle + Style + + + + Bounds + {{282, 92}, {18, 18}} + Class + ShapedGraphic + ID + 9 + Shape + Circle + Style + + + + Bounds + {{253, 92}, {18, 18}} + Class + ShapedGraphic + ID + 8 + Shape + Circle + Style + + + + Bounds + {{224, 92}, {18, 18}} + Class + ShapedGraphic + ID + 5 + Shape + Circle + Style + + + + Bounds + {{253, 58}, {18, 18}} + Class + ShapedGraphic + ID + 4 + Shape + Circle + Style + + + + Bounds + {{34, 62}, {80, 56}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 3 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 0 + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural + +\f0\fs24 \cf0 SELECT \'85\ +FROM ..\ +WHERE \'85\ +GROUP BY ... } + VerticalPad + 0 + + Wrap + NO + + + GridInfo + + HPages + 1 + KeepToScale + + Layers + + + Lock + NO + Name + Layer 1 + Print + YES + View + YES + + + LayoutInfo + + Animate + NO + circoMinDist + 18 + circoSeparation + 0.0 + layoutEngine + dot + neatoSeparation + 0.0 + twopiSeparation + 0.0 + + Orientation + 2 + PrintOnePage + + RowAlign + 1 + RowSpacing + 36 + SheetTitle + Planning + UniqueID + 3 + VPages + 1 + + + ActiveLayerIndex + 0 + AutoAdjust + + BackgroundGraphic + + Bounds + {{0, 0}, {1152, 733}} + Class + SolidGraphic + ID + 2 + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + + BaseZoom + 0 + CanvasOrigin + {0, 0} + ColumnAlign + 1 + ColumnSpacing + 36 + DisplayScale + 1 0/72 in = 1.0000 in + GraphicsList + + + Bounds + {{862.88311767578125, 68.365104675292969}, {266.05844116210938, 14}} + Class + ShapedGraphic + FitText + Vertical + Flow + Resize + ID + 267 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 0 + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural + +\f0\fs24 \cf0 Client reads from top stage's output buffer} + VerticalPad + 0 + + + + Bounds + {{862.88311767578125, 111.05270385742188}, {266.05844116210938, 84}} + Class + ShapedGraphic + FitText + Vertical + Flow + Resize + ID + 266 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 0 + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural + +\f0\fs24 \cf0 Threads pull data from the operator pipeline and put results in output buffer. When output buffer fills up, they pause.\ +\ +All stages execute concurrently. Data flows through the pipeline as quickly as possible.} + VerticalPad + 0 + + + + Bounds + {{895, 421}, {61.25, 40.2420654296875}} + Class + ShapedGraphic + ID + 265 + Shape + Rectangle + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Data Source} + + + + Bounds + {{895, 472.86651611328125}, {61.25, 40.2420654296875}} + Class + ShapedGraphic + ID + 207 + Shape + Rectangle + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Data Source} + + + + Class + LineGraphic + Head + + ID + 233 + + ID + 264 + Points + + {894.52147569676288, 483.56507800817781} + {740.81040954589844, 437} + {662.8142704614761, 386.53808499467948} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 4 + + + Tail + + ID + 207 + + + + Class + LineGraphic + Head + + ID + 217 + + ID + 263 + Points + + {639.8902291093292, 519.5181208159604} + {640.10753424977759, 503.36054028730734} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 241 + + + + Class + LineGraphic + Head + + ID + 241 + + ID + 262 + Points + + {640.25593942739158, 581.47511683072889} + {639.86635453740882, 548.27015119673092} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 230 + + + + Class + LineGraphic + Head + + ID + 218 + + ID + 261 + Points + + {808.8894045856739, 518.61825087859586} + {809.09371215450392, 503.36053475585572} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 240 + + + + Class + LineGraphic + Head + + ID + 240 + + ID + 260 + Points + + {809.25684385822387, 581.47511564883882} + {808.86346577218796, 547.37622914218457} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 231 + + + + Class + LineGraphic + Head + + ID + 243 + + ID + 259 + Points + + {801.73095211210318, 338.48713881040561} + {805.30679472958275, 324.42951985232833} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 256 + + + + Class + LineGraphic + Head + + ID + 256 + + ID + 258 + Points + + {812.26151199857281, 374.54346004237306} + {804.38212737580795, 359.16278967152476} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 232 + + + + Class + LineGraphic + Head + + ID + 256 + + ID + 257 + Points + + {764.60045475807317, 375.51277828329677} + {787.00347285423152, 358.19345506317944} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 220 + + + + Bounds + {{787.3990478515625, 338.97170766593428}, {23.39447021484375, 19.746077692494694}} + Class + ShapedGraphic + ID + 256 + Shape + Rectangle + + + Class + LineGraphic + Head + + ID + 242 + + ID + 255 + Points + + {640.58218976789442, 340.47626521584084} + {642.92532399590141, 326.06437750626043} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 246 + + + + Class + LineGraphic + Head + + ID + 246 + + ID + 254 + Points + + {649.30454202169221, 374.5307008824546} + {643.43582130278082, 361.17362622402135} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 233 + Info + 1 + + + + Class + LineGraphic + Head + + ID + 246 + + ID + 253 + Points + + {607.9976142475615, 375.29467644319357} + {626.807439463376, 360.40963429586088} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 219 + + + + Class + LineGraphic + Head + + ID + 216 + + ID + 252 + Points + + {808.90952276005612, 296.78220339264874} + {809.07071500695861, 285.9092307493633} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 243 + + + + Class + LineGraphic + Head + + ID + 215 + + ID + 251 + Points + + {645.33257674634933, 298.19212946761371} + {645.5086384478966, 285.90923434643111} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 242 + + + + Class + LineGraphic + Head + + ID + 212 + + ID + 250 + Points + + {766.6096483396243, 80.442351988157014} + {766.80280441873208, 66.411160944513966} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 248 + + + + Class + LineGraphic + Head + + ID + 248 + + ID + 249 + Points + + {712.09297816002368, 166.40424535941779} + {757.94841836354362, 105.94370766028622} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 214 + + + + Bounds + {{751.5997314453125, 80.608428955078125}, {29.624969482421875, 28.351470947265625}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica + Size + 12 + + ID + 248 + Shape + Patch + Style + + + + Bounds + {{627.19952392578125, 340.9697850585124}, {23.39447021484375, 19.746077692494694}} + Class + ShapedGraphic + ID + 246 + Shape + Rectangle + + + Bounds + {{793.884765625, 296.92497253417969}, {29.624969482421875, 28.351470947265625}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica + Size + 12 + + ID + 243 + Shape + Patch + Style + + + + Bounds + {{630.314697265625, 298.34570026397705}, {29.624969482421875, 28.351470947265625}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica + Size + 12 + + ID + 242 + Shape + Patch + Style + + + + Bounds + {{624.884765625, 519.69119262695312}, {29.624969482421875, 28.351470947265625}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica + Size + 12 + + ID + 241 + Shape + Patch + Style + + + + Bounds + {{793.884765625, 518.79261779785156}, {29.624969482421875, 28.351470947265625}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica + Size + 12 + + ID + 240 + Shape + Patch + Style + + + + Class + LineGraphic + Head + + ID + 230 + + ID + 237 + Points + + {639.61891952365409, 644.50003498082503} + {640.11906302280727, 602.22112510913041} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 4 + + + Tail + + ID + 204 + + + + Class + LineGraphic + Head + + ID + 231 + + ID + 236 + Points + + {808.8571163849839, 644.50001501858651} + {809.18487091941358, 602.22114507136916} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 4 + + + Tail + + ID + 205 + + + + Class + LineGraphic + Head + + ID + 232 + + ID + 234 + Points + + {894.5239670442005, 431.12845943629566} + {863, 421} + {826.05576267914978, 389.01796155764538} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 4 + + + Tail + + ID + 265 + + + + Bounds + {{639, 374.98846435546875}, {23.39447021484375, 19.746077692494694}} + Class + ShapedGraphic + ID + 233 + Magnets + + {0.026192160213692972, -0.29203043441626964} + + Shape + Rectangle + + + Bounds + {{802.28326416015625, 374.98846435546875}, {23.39447021484375, 19.746077692494694}} + Class + ShapedGraphic + ID + 232 + Magnets + + {0.026192160213692972, -0.29203043441626964} + + Shape + Rectangle + + + Bounds + {{797, 581.97508239746094}, {23.39447021484375, 19.746077692494694}} + Class + ShapedGraphic + ID + 231 + Magnets + + {0.026192160213692972, -0.29203043441626964} + + Shape + Rectangle + + + Bounds + {{628, 581.97508239746094}, {23.39447021484375, 19.746077692494694}} + Class + ShapedGraphic + ID + 230 + Magnets + + {0.026192160213692972, -0.29203043441626964} + + Shape + Rectangle + + + Class + LineGraphic + Head + + ID + 214 + + ID + 229 + Points + + {804.30375229919696, 265.33151526619798} + {715.8133785227825, 186.88039688895341} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 4 + + + Tail + + ID + 216 + + + + Class + LineGraphic + Head + + ID + 214 + + ID + 228 + Points + + {648.58954781302407, 265.23998494983061} + {697.82568838080215, 186.97192720532072} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 4 + + + Tail + + ID + 215 + + + + Class + LineGraphic + Head + + ID + 220 + + ID + 227 + Points + + {806.78851118173361, 482.67730713945173} + {758.22909559474238, 395.17172049421526} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 4 + + + Tail + + ID + 218 + + + + Class + LineGraphic + Head + + ID + 219 + + ID + 225 + Points + + {800.29766397588344, 482.8982621380519} + {608.05635162505257, 390.68838641038855} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 4 + + + Tail + + ID + 218 + + + + Class + LineGraphic + Head + + ID + 220 + + ID + 223 + Points + + {645.18064160340407, 482.77751407598913} + {741.31630448001658, 395.07151355767786} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 4 + + + Tail + + ID + 217 + + + + Class + LineGraphic + Head + + ID + 219 + + ID + 222 + Points + + {638.32965056353964, 482.65579914887081} + {600.3900122654228, 395.19322848479624} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 4 + + + Tail + + ID + 217 + + + + Bounds + {{740.81040954589844, 374.98844798804726}, {23.39447021484375, 19.746077692494694}} + Class + ShapedGraphic + ID + 220 + Shape + Rectangle + + + Bounds + {{584.2110595703125, 374.98844798804731}, {23.39447021484375, 19.746077692494694}} + Class + ShapedGraphic + ID + 219 + Shape + Rectangle + + + Bounds + {{797, 483.114501953125}, {23.39447021484375, 19.746077692494694}} + Class + ShapedGraphic + ID + 218 + Magnets + + {0.026192160213692972, -0.29203043441626964} + + Shape + Rectangle + + + Bounds + {{628, 483.114501953125}, {23.39447021484375, 19.746077692494694}} + Class + ShapedGraphic + ID + 217 + Magnets + + {0.026192160213692972, -0.29203043441626964} + + Shape + Rectangle + + + Bounds + {{797, 265.6632080078125}, {23.39447021484375, 19.746077692494694}} + Class + ShapedGraphic + ID + 216 + Magnets + + {0.026192160213692972, -0.29203043441626964} + + Shape + Rectangle + + + Bounds + {{633.42999267578125, 265.6632080078125}, {23.39447021484375, 19.746077692494694}} + Class + ShapedGraphic + ID + 215 + Magnets + + {0.026192160213692972, -0.29203043441626964} + + Shape + Rectangle + + + Bounds + {{692.60552978515625, 166.80262645484419}, {23.39447021484375, 19.746077692494694}} + Class + ShapedGraphic + ID + 214 + Shape + Rectangle + + + Bounds + {{754.71499633789062, 46.165130615234375}, {23.39447021484375, 19.746077692494694}} + Class + ShapedGraphic + ID + 212 + Magnets + + {0.026192160213692972, -0.29203043441626964} + + Shape + Rectangle + + + Class + LineGraphic + Head + + ID + 210 + + ID + 211 + Points + + {773.27996211385994, 45.877718982348419} + {803, 25} + {889.50030656619572, 28.030568273779433} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 4 + + + Tail + + ID + 212 + Info + 1 + + + + Bounds + {{890, 9}, {61.25, 40.2420654296875}} + Class + ShapedGraphic + ID + 210 + Shape + Rectangle + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Client} + + + + Bounds + {{778.9814567565918, 645}, {59.431602478027344, 40.2420654296875}} + Class + ShapedGraphic + ID + 205 + Shape + Rectangle + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Data Source} + + + + Bounds + {{608.74998092651367, 645}, {61.25, 40.2420654296875}} + Class + ShapedGraphic + ID + 204 + Shape + Rectangle + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Data Source} + + + + Bounds + {{465, 415.74996948242188}, {15.000000000001137, 15.749984113272603}} + Class + ShapedGraphic + ID + 196 + Shape + Circle + Style + + + + Bounds + {{451.36329650878906, 400}, {15.000000000001137, 15.749984113272603}} + Class + ShapedGraphic + ID + 203 + Shape + Circle + Style + + + + Bounds + {{436.00617980957031, 384.25003051757812}, {15.000000000001137, 15.749984113272603}} + Class + ShapedGraphic + ID + 202 + Shape + Circle + Style + + + + Bounds + {{474.35554504394531, 372.49154663085938}, {15.000000000001137, 15.749984113272603}} + Class + ShapedGraphic + ID + 201 + Shape + Circle + Style + + + + Bounds + {{512.19917297363281, 280.63690948486328}, {15.000000000001137, 15.749984113272603}} + Class + ShapedGraphic + ID + 200 + Shape + Circle + Style + + + + Bounds + {{493, 272}, {15.000000000001137, 15.749984113272603}} + Class + ShapedGraphic + ID + 199 + Shape + Circle + Style + + + + Bounds + {{458.68748474121094, 355.71976470947266}, {15.000000000001137, 15.749984113272603}} + Class + ShapedGraphic + ID + 198 + Shape + Circle + Style + + + + Bounds + {{444.6875, 340.96976470947266}, {15.000000000001137, 15.749984113272603}} + Class + ShapedGraphic + ID + 197 + Shape + Circle + Style + + + + Bounds + {{521.52938604354858, 213.96593856811523}, {15.000000000001137, 15.749984113272603}} + Class + ShapedGraphic + ID + 195 + Shape + Circle + Style + + + + Bounds + {{500.58207225799561, 206.50339889526367}, {15.000000000001137, 15.749984113272603}} + Class + ShapedGraphic + ID + 194 + Shape + Circle + Style + + + + Bounds + {{542.47666931152344, 221.34951400756833}, {15.000000000001137, 15.749984113272603}} + Class + ShapedGraphic + ID + 193 + Shape + Circle + Style + + + + Class + LineGraphic + Head + + ID + 189 + + ID + 192 + Points + + {303.79087608799273, 206.14826346198484} + {234.61755660320458, 58.80407556836262} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 23 + + + + Class + LineGraphic + Head + + ID + 189 + + ID + 191 + Points + + {298.23736604885892, 134.66640466936249} + {239.09931244549858, 58.74592506053969} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 66 + + + + Bounds + {{173.75003051757812, 30}, {108, 28.351470947265625}} + Class + ShapedGraphic + ID + 189 + Shape + Rectangle + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Data Location API} + + + + Bounds + {{16, 328.22372436523438}, {350, 126}} + Class + ShapedGraphic + FitText + Vertical + Flow + Resize + ID + 48 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 0 + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural + +\f0\fs24 \cf0 Each Stage Manager is in charge of one stage of the distributed plan:\ +1. Call Data Location API to enumerate "splits" for each table\ +2. Find workers closest to where the data resides and schedule \ +a task if necessary\ +3. Send plan fragment and stream of splits\ +4. Send list of upstream and downstream tasks so that task can set up input and output buffers\ +5. Repeat from 1} + VerticalPad + 0 + + + + Class + LineGraphic + Head + + ID + 178 + + ID + 184 + Points + + {333.10920770126415, 247.78824189821202} + {531, 445} + {665.35554122924805, 477} + {722.74833072262209, 510.04123076666269} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + Pattern + 2 + TailArrow + 0 + + + Tail + + ID + 23 + + + + Class + LineGraphic + Head + + ID + 176 + + ID + 183 + Points + + {333.25690933944031, 249.84439675074384} + {571.64720620693254, 512.00633947169501} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + Pattern + 2 + TailArrow + 0 + + + Tail + + ID + 23 + + + + Class + LineGraphic + Head + + ID + 181 + + ID + 182 + Points + + {404.07070615369759, 366.97598768952071} + {404.07070615369759, 374.48842738375311} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 0.5 + + + Tail + + ID + 180 + + + + Bounds + {{398, 374.98842738152007}, {12.141380935418802, 10.756231326991861}} + Class + ShapedGraphic + ID + 181 + Shape + Rectangle + + + Bounds + {{398, 355.71975636476191}, {12.141380935418802, 10.756231326991861}} + Class + ShapedGraphic + ID + 180 + Shape + Rectangle + + + Bounds + {{389.25822141248665, 349.50350510034042}, {29.624969482421875, 41.440422058105469}} + Class + ShapedGraphic + ID + 179 + Line + + ID + 182 + Position + 0.43231341242790222 + RotationType + 0 + + Shape + Rectangle + Style + + fill + + Draws + NO + + stroke + + Pattern + 2 + + + + + Bounds + {{723.1816520690918, 507.80058288574219}, {61.25, 40.2420654296875}} + Class + ShapedGraphic + ID + 178 + Shape + Rectangle + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Task Manager} + + + + Bounds + {{723.1816520690918, 483.114501953125}, {108, 127.83871459960938}} + Class + ShapedGraphic + ID + 177 + Shape + Rectangle + + + Bounds + {{557.35554122924805, 512.37626647949219}, {61.25, 35.184249877929688}} + Class + ShapedGraphic + ID + 176 + Shape + Rectangle + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Task Manager} + + + + Bounds + {{557.35554122924805, 483.114501953125}, {112.64445495605469, 129.885498046875}} + Class + ShapedGraphic + ID + 175 + Shape + Rectangle + + + Class + LineGraphic + Head + + ID + 171 + + ID + 174 + Points + + {445.09960249810234, 237.5994973717429} + {445.09960249810234, 243.00158569592588} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 0.5 + + + Tail + + ID + 170 + + + + Class + LineGraphic + Head + + ID + 170 + + ID + 173 + Points + + {445.09959914378715, 222.75338977670486} + {445.09959914378715, 228.31204634565677} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 0.5 + + + Tail + + ID + 169 + + + + Class + LineGraphic + Head + + ID + 169 + + ID + 172 + Points + + {455.84267909500466, 207.64952181529731} + {449.71364225462594, 213.6171344298524} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 0.5 + + + Tail + + ID + 168 + + + + Bounds + {{439.19299583965051, 243.50158571063315}, {11.81318681318681, 8.2874510224924638}} + Class + ShapedGraphic + ID + 171 + Shape + Rectangle + + + Bounds + {{439.19299583965051, 228.8120463345432}, {11.81318681318681, 8.2874510224924638}} + Class + ShapedGraphic + ID + 170 + Shape + Rectangle + + + Bounds + {{439.19299583965051, 213.96593876532597}, {11.81318681318681, 8.2874510224924638}} + Class + ShapedGraphic + ID + 169 + Shape + Rectangle + + + Bounds + {{454.55013869679328, 199.01326645733127}, {11.81318681318681, 8.2874510224924638}} + Class + ShapedGraphic + ID + 168 + Shape + Rectangle + + + Bounds + {{430.6875, 192.61117553710938}, {43, 66}} + Class + ShapedGraphic + ID + 167 + Shape + Rectangle + Style + + fill + + Draws + NO + + stroke + + Pattern + 2 + + + + + Class + LineGraphic + Head + + ID + 164 + + ID + 166 + Points + + {340.84249653899042, 166.49935038768814} + {531, 244} + {669.99999618530273, 252} + {722.36755533994994, 294.60940523498692} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + Pattern + 2 + TailArrow + 0 + + + Tail + + ID + 66 + + + + Class + LineGraphic + Head + + ID + 161 + + ID + 165 + Points + + {339.70882494459386, 170.4057994693498} + {556.9235117654448, 296.95254163276235} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + Pattern + 2 + TailArrow + 0 + + + Tail + + ID + 66 + + + + Bounds + {{716.85942459106445, 294.92497253417969}, {61.25, 40.2420654296875}} + Class + ShapedGraphic + ID + 164 + Shape + Rectangle + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Task Manager} + + + + Bounds + {{716.85942459106445, 265.6632080078125}, {120.64445495605469, 138.3367919921875}} + Class + ShapedGraphic + ID + 163 + Shape + Rectangle + + + Bounds + {{557.35554122924805, 294.92497253417969}, {61.25, 40.2420654296875}} + Class + ShapedGraphic + ID + 161 + Shape + Rectangle + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Task Manager} + + + + Bounds + {{557.35554122924805, 265.6632080078125}, {116.64445495605469, 138.3367919921875}} + Class + ShapedGraphic + ID + 159 + Shape + Rectangle + + + Class + LineGraphic + Head + + ID + 154 + + ID + 158 + Points + + {344.90517628095427, 76.901142701805284} + {664.85618863384116, 93.198756563517321} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + Pattern + 2 + TailArrow + 0 + + + Tail + + ID + 65 + + + + Bounds + {{478.76993285175439, 63.891892021826806}, {12.1413809354188, 9.408870973643312}} + Class + ShapedGraphic + ID + 157 + Shape + Rectangle + + + Bounds + {{470.02813720703125, 59.126960754394531}, {29.624969482421875, 19.746077692494694}} + Class + ShapedGraphic + ID + 156 + Shape + Rectangle + Style + + fill + + Draws + NO + + stroke + + Pattern + 2 + + + + + Bounds + {{665.35554122924805, 74.663131713867188}, {61.25, 40.2420654296875}} + Class + ShapedGraphic + ID + 154 + Shape + Rectangle + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Task Manager} + + + + Bounds + {{665.35554122924805, 46.165130615234375}, {127.64445495605469, 146.64715576171875}} + Class + ShapedGraphic + ID + 153 + Shape + Rectangle + + + Class + LineGraphic + Head + + ID + 146 + + ID + 94 + Points + + {203.27506253138952, 196.54553034851099} + {203.27506253138952, 204.31221832594099} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 0.5 + + + Tail + + ID + 145 + + + + Class + LineGraphic + Head + + ID + 86 + + ID + 93 + Points + + {151.10105885661631, 193.1795464344703} + {151.10105885661631, 200.73695602467993} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 0.5 + + + Tail + + ID + 144 + + + + Class + LineGraphic + Head + + ID + 144 + + ID + 92 + Points + + {151.10105229045394, 173.33536166994165} + {151.10105229045394, 181.10204964737179} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 0.5 + + + Tail + + ID + 143 + + + + Class + LineGraphic + Head + + ID + 145 + + ID + 148 + Points + + {171.50804468836736, 153.22929670258313} + {198.23077107919141, 184.58747312461011} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 0.5 + + + Tail + + ID + 142 + + + + Class + LineGraphic + Head + + ID + 143 + + ID + 147 + Points + + {161.90168786549816, 153.24515992760882} + {155.66312723252824, 161.36144122101504} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 0.5 + + + Tail + + ID + 142 + + + + Class + LineGraphic + Head + + ID + 142 + + ID + 89 + Points + + {166.46373157996118, 119.12789931967441} + {166.46373157996118, 141.27123947541787} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 0.5 + + + Tail + + ID + 82 + + + + Bounds + {{197.36635141900709, 204.81221830215782}, {11.817440695250784, 11.07749679078805}} + Class + ShapedGraphic + ID + 146 + Shape + Rectangle + + + Bounds + {{197.36635141900709, 184.96803358150609}, {11.817440695250784, 11.07749679078805}} + Class + ShapedGraphic + ID + 145 + Shape + Rectangle + + + Bounds + {{145.19235074947477, 201.23695604477348}, {11.817440695250784, 11.07749679078805}} + Class + ShapedGraphic + ID + 86 + Shape + Rectangle + + + Bounds + {{145.19235074947477, 181.60204962358864}, {11.817440695250784, 11.07749679078805}} + Class + ShapedGraphic + ID + 144 + Shape + Rectangle + + + Bounds + {{145.19235074947477, 161.75786490293672}, {11.817440695250784, 11.07749679078805}} + Class + ShapedGraphic + ID + 143 + Shape + Rectangle + + + Bounds + {{160.55502365330085, 141.77123945489907}, {11.817440695250784, 11.07749679078805}} + Class + ShapedGraphic + ID + 142 + Shape + Rectangle + + + Bounds + {{160.55502365330085, 107.55040254940516}, {11.817440695250784, 11.07749679078805}} + Class + ShapedGraphic + ID + 82 + Shape + Rectangle + + + Bounds + {{281.75003051757812, 129.86509704589844}, {66, 52}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica + Size + 12 + + ID + 66 + Shape + Patch + Style + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs22 \cf0 Stage\ +Manager} + + + + Bounds + {{281.75003051757812, 49.365104675292969}, {66, 52}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica + Size + 12 + + ID + 65 + Shape + Patch + Style + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs22 \cf0 Stage\ +Manager} + + + + Bounds + {{281.75003433227539, 203.4920654296875}, {66, 52}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica + Size + 12 + + ID + 23 + Shape + Patch + Style + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs22 \cf0 Stage\ +Manager} + + + + Bounds + {{152.04646501808384, 101.9404296875}, {28.834555296411907, 23.247965976151754}} + Class + ShapedGraphic + ID + 100 + Shape + Rectangle + Style + + fill + + Draws + NO + + stroke + + Pattern + 2 + + + + + Bounds + {{136.68379211425787, 133.21382639380036}, {43.015484130712835, 88.219500327391017}} + Class + ShapedGraphic + ID + 98 + Shape + Rectangle + Style + + fill + + Draws + NO + + stroke + + Pattern + 2 + + + + + Bounds + {{188.68379211425781, 172.44646186348388}, {34, 52}} + Class + ShapedGraphic + ID + 95 + Shape + Rectangle + Style + + fill + + Draws + NO + + stroke + + Pattern + 2 + + + + + Bounds + {{84, 30}, {282, 242}} + Class + ShapedGraphic + ID + 3 + Shape + Rectangle + Style + + Text + + Align + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural + +\f0\fs24 \cf0 Coordinator} + + TextPlacement + 0 + + + GridInfo + + HPages + 2 + KeepToScale + + Layers + + + Lock + NO + Name + Layer 1 + Print + YES + View + YES + + + LayoutInfo + + Animate + NO + circoMinDist + 18 + circoSeparation + 0.0 + layoutEngine + dot + neatoSeparation + 0.0 + twopiSeparation + 0.0 + + Orientation + 2 + PrintOnePage + + RowAlign + 1 + RowSpacing + 36 + SheetTitle + Distributed Execution + UniqueID + 4 + VPages + 1 + + + ActiveLayerIndex + 0 + AutoAdjust + + BackgroundGraphic + + Bounds + {{0, 0}, {576, 733}} + Class + SolidGraphic + ID + 2 + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + + BaseZoom + 0 + CanvasOrigin + {0, 0} + ColumnAlign + 1 + ColumnSpacing + 36 + DisplayScale + 1 0/72 in = 1.0000 in + GraphicsList + + + Bounds + {{64.5, 466}, {54, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 61 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Operators} + VerticalPad + 0 + + Wrap + NO + + + Class + LineGraphic + ID + 60 + Points + + {151, 473} + {131, 473} + + Style + + stroke + + HeadArrow + 0 + Legacy + + LineType + 1 + TailArrow + 0 + Width + 2 + + + + + Class + LineGraphic + ID + 59 + OrthogonalBarAutomatic + + OrthogonalBarPoint + {0, 0} + OrthogonalBarPosition + -1 + Points + + {173.75, 395} + {153, 395} + {151, 551} + {174, 552} + + Style + + stroke + + HeadArrow + 0 + Legacy + + LineType + 2 + TailArrow + 0 + Width + 2 + + + + + Bounds + {{4, 248}, {175, 42}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 57 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 0 + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural + +\f0\fs24 \cf0 Task Manager controls \ +lifecycle of worker threads \ +(start, interrupt, gather stats, etc)} + VerticalPad + 0 + + Wrap + NO + + + Class + LineGraphic + Head + + ID + 18 + + ID + 56 + Points + + {431.6130228338281, 639.51545142917905} + {409.20515334850069, 551.48454861198059} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 53 + + + + Class + LineGraphic + Head + + ID + 8 + + ID + 55 + Points + + {319.49999437135142, 639.5} + {319.49999437135142, 551.5} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 52 + + + + Class + LineGraphic + Head + + ID + 3 + + ID + 54 + Points + + {207.38697716617193, 639.51545142917905} + {229.79484665149934, 551.48454861198059} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 49 + + + + Bounds + {{399, 640}, {81, 61}} + Class + ShapedGraphic + ID + 53 + Shape + Rectangle + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Data Provider} + + + + Bounds + {{279, 640}, {81, 61}} + Class + ShapedGraphic + ID + 52 + Shape + Rectangle + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Data Provider} + + + + Bounds + {{159, 640}, {81, 61}} + Class + ShapedGraphic + ID + 49 + Shape + Rectangle + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Data Provider} + + + + Bounds + {{39, 10}, {135, 28}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 48 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 0 + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural + +\f0\fs24 \cf0 Data pipeline for a query \ +within a worker node} + VerticalPad + 0 + + Wrap + NO + + + Class + LineGraphic + Head + + ID + 25 + + ID + 47 + Points + + {123.48129097404015, 328.99646940320275} + {262, 290} + {379.78831701626052, 329.67606467916147} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + Pattern + 2 + TailArrow + 0 + + + Tail + + ID + 44 + + + + Class + LineGraphic + Head + + ID + 24 + + ID + 46 + Points + + {123.48952756717347, 331.34774350285079} + {255, 304} + {297.54989976937384, 326.42940452959243} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + Pattern + 2 + TailArrow + 0 + + + Tail + + ID + 44 + + + + Class + LineGraphic + Head + + ID + 23 + + ID + 45 + Points + + {123.49999999857067, 337.99999186754889} + {206.4999845025153, 337.99999186754889} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + Pattern + 2 + TailArrow + 0 + + + Tail + + ID + 44 + + + + Bounds + {{60, 305.5}, {63, 65}} + Class + ShapedGraphic + ID + 44 + Shape + Rectangle + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Task Manager} + + + + Bounds + {{517.5, 395}, {35, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 41 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Pages} + VerticalPad + 0 + + Wrap + NO + + + Class + LineGraphic + ID + 40 + Points + + {506.5, 586} + {505.5, 222} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 4 + + + + + Class + LineGraphic + Head + + ID + 25 + + ID + 38 + Points + + {404.49999091837316, 394.49999998658706} + {404.49999091837316, 363.50001441377549} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 20 + + + + Class + LineGraphic + Head + + ID + 24 + + ID + 37 + Points + + {319.5000078890514, 394.50000001439003} + {319.5000078890514, 363.50001235066134} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 10 + + + + Class + LineGraphic + Head + + ID + 23 + + ID + 36 + Points + + {234.50001796246036, 394.49999999286825} + {234.50001796246036, 363.50002816092814} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 5 + + + + Class + LineGraphic + ID + 34 + Points + + {315.09473684210525, 172.99999999999997} + {280.5, 70} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + Pattern + 1 + TailArrow + 0 + + + Tail + + ID + 26 + Info + 5 + + + + Class + LineGraphic + ID + 33 + Points + + {343.18253854292595, 172.62037169991973} + {432, 69} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + Pattern + 1 + TailArrow + 0 + + + Tail + + ID + 26 + + + + Class + LineGraphic + ID + 32 + Points + + {328.80000000000001, 172.99999999999997} + {358.5, 69} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + Pattern + 1 + TailArrow + 0 + + + Tail + + ID + 26 + + + + Class + LineGraphic + ID + 31 + Points + + {299.86203110420303, 172.61350916144281} + {214, 68} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + Pattern + 1 + TailArrow + 0 + + + Tail + + ID + 26 + + + + Class + LineGraphic + Head + + ID + 26 + + ID + 29 + Points + + {389.15419233392106, 320.60002910302865} + {341, 266} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 25 + + + + Class + LineGraphic + Head + + ID + 26 + + ID + 28 + Points + + {320.60828872621897, 314.22031193882287} + {322.83239210694569, 266.49945783438147} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 24 + + + + Class + LineGraphic + Head + + ID + 26 + + ID + 27 + Points + + {251.19318236296036, 320.70634345132169} + {304, 266} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 23 + + + + Bounds + {{295, 173}, {49, 93}} + Class + ShapedGraphic + ID + 26 + Magnets + + {-0.31632653061224492, 0.5} + {0.071428571428571175, 0.521505376344086} + {0.43877551020408134, 0.5} + {-0.37755102040816357, -0.48924731182795655} + {-0.091836734693877986, -0.51075268817204256} + {0.19387755102040849, -0.51075268817204256} + {0.45918367346938727, -0.48924731182795655} + + Shape + Rectangle + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Output Buffer} + + + + Bounds + {{377, 313}, {55, 50}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica + Size + 12 + + ID + 25 + Shape + Patch + Style + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs22 \cf0 Thread} + + + + Bounds + {{292, 313}, {55, 50}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica + Size + 12 + + ID + 24 + Shape + Patch + Style + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs22 \cf0 Thread} + + + + Bounds + {{207, 313}, {55, 50}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica + Size + 12 + + ID + 23 + Shape + Patch + Style + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs22 \cf0 Thread} + + + + Class + LineGraphic + Head + + ID + 19 + + ID + 22 + Points + + {404.50001406241336, 514.5} + {404.50001406241336, 491.5} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 18 + + + + Class + LineGraphic + Head + + ID + 20 + + ID + 21 + Points + + {404.50001406241341, 454.5} + {404.50001406241341, 431.5} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 19 + + + + Bounds + {{384, 395}, {41, 36}} + Class + ShapedGraphic + ID + 20 + Shape + Rectangle + + + Bounds + {{384, 455}, {41, 36}} + Class + ShapedGraphic + ID + 19 + Shape + Rectangle + + + Bounds + {{384, 515}, {41, 36}} + Class + ShapedGraphic + ID + 18 + Shape + Rectangle + + + Class + LineGraphic + Head + + ID + 9 + + ID + 12 + Points + + {319.50001041268933, 514.5} + {319.50001041268933, 491.5} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 8 + + + + Class + LineGraphic + Head + + ID + 10 + + ID + 11 + Points + + {319.50001041268291, 454.5} + {319.50001041268291, 431.5} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 9 + + + + Bounds + {{299, 395}, {41, 36}} + Class + ShapedGraphic + ID + 10 + Shape + Rectangle + + + Bounds + {{299, 455}, {41, 36}} + Class + ShapedGraphic + ID + 9 + Shape + Rectangle + + + Bounds + {{299, 515}, {41, 36}} + Class + ShapedGraphic + ID + 8 + Shape + Rectangle + + + Class + LineGraphic + Head + + ID + 4 + + ID + 7 + Points + + {234.50001872897533, 514.5} + {234.50001872897533, 491.5} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 3 + + + + Class + LineGraphic + Head + + ID + 5 + + ID + 6 + Points + + {234.49999018947506, 454.5} + {234.49999018947506, 431.5} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 4 + + + + Bounds + {{214, 395}, {41, 36}} + Class + ShapedGraphic + ID + 5 + Shape + Rectangle + + + Bounds + {{214, 455}, {41, 36}} + Class + ShapedGraphic + ID + 4 + Shape + Rectangle + + + Bounds + {{214, 515}, {41, 36}} + Class + ShapedGraphic + ID + 3 + Shape + Rectangle + + + GridInfo + + HPages + 1 + KeepToScale + + Layers + + + Lock + NO + Name + Layer 1 + Print + YES + View + YES + + + LayoutInfo + + Animate + NO + circoMinDist + 18 + circoSeparation + 0.0 + layoutEngine + dot + neatoSeparation + 0.0 + twopiSeparation + 0.0 + + Orientation + 2 + PrintOnePage + + RowAlign + 1 + RowSpacing + 36 + SheetTitle + In-node Execution + UniqueID + 1 + VPages + 1 + + + ActiveLayerIndex + 0 + AutoAdjust + + BackgroundGraphic + + Bounds + {{0, 0}, {576, 733}} + Class + SolidGraphic + ID + 2 + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + + BaseZoom + 0 + CanvasOrigin + {0, 0} + ColumnAlign + 1 + ColumnSpacing + 36 + DisplayScale + 1 0/72 in = 1.0000 in + GraphicsList + + + Bounds + {{20, 14}, {68, 28}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 31 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 In-Memory \ +Page Layout} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{450, 290.5}, {31, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 30 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Rows} + VerticalPad + 0 + + Wrap + NO + + + Class + LineGraphic + ID + 29 + Points + + {441, 229} + {441, 380} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 2 + + + + + Class + LineGraphic + ID + 19 + Points + + {324, 303.5} + {353, 303.5} + + Style + + stroke + + HeadArrow + 0 + Legacy + + LineType + 1 + Pattern + 2 + TailArrow + 0 + + + + + Bounds + {{368, 187}, {54, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 15 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Column N} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{262, 187}, {52, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 13 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Column 3} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{190, 187}, {52, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 12 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Column 2} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{118, 187}, {52, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 11 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Column 1} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{116.5, 136}, {53, 28}} + Class + ShapedGraphic + ID + 10 + Shape + Rectangle + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf390 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Row\ +count} + + + + Bounds + {{374, 208}, {42, 192}} + Class + ShapedGraphic + ID + 8 + Shape + Rectangle + + + Bounds + {{268, 208.5}, {42, 192}} + Class + ShapedGraphic + ID + 6 + Shape + Rectangle + + + Bounds + {{195, 208.5}, {42, 192}} + Class + ShapedGraphic + ID + 5 + Shape + Rectangle + + + Bounds + {{122, 208.5}, {42, 192}} + Class + ShapedGraphic + ID + 4 + Shape + Rectangle + + + Bounds + {{90, 111}, {405, 306}} + Class + ShapedGraphic + ID + 3 + Shape + Rectangle + + + GridInfo + + HPages + 1 + KeepToScale + + Layers + + + Lock + NO + Name + Layer 1 + Print + YES + View + YES + + + LayoutInfo + + Animate + NO + circoMinDist + 18 + circoSeparation + 0.0 + layoutEngine + dot + neatoSeparation + 0.0 + twopiSeparation + 0.0 + + Orientation + 2 + PrintOnePage + + RowAlign + 1 + RowSpacing + 36 + SheetTitle + Page Layout + UniqueID + 2 + VPages + 1 + + + SmartAlignmentGuidesActive + YES + SmartDistanceGuidesActive + YES + UseEntirePage + + WindowInfo + + CurrentSheet + 1 + ExpandedCanvases + + Frame + {{472, 129}, {1319, 1008}} + ListView + + OutlineWidth + 142 + RightSidebar + + ShowRuler + + Sidebar + + SidebarWidth + 120 + VisibleRegion + {{-16, -68}, {1184, 869}} + Zoom + 1 + ZoomValues + + + In-node Execution + 1 + 2 + + + Page Layout + 1 + 1 + + + Planning + 1 + 1 + + + Distributed Execution + 1 + 2 + + + + + diff --git a/presto-docs/src/main/resources/logo/print/dark/FB_Presto_Logo_PRINT_DarkBG.ai b/presto-docs/src/main/resources/logo/print/dark/FB_Presto_Logo_PRINT_DarkBG.ai new file mode 100644 index 00000000..19503485 --- /dev/null +++ b/presto-docs/src/main/resources/logo/print/dark/FB_Presto_Logo_PRINT_DarkBG.ai @@ -0,0 +1,2578 @@ +%PDF-1.5 %âãÏÓ +1 0 obj <>/OCGs[5 0 R 36 0 R]>>/Pages 3 0 R/Type/Catalog>> endobj 2 0 obj <>stream + + + + + application/pdf + + + FB_Presto_Logo_PRINT_DarkBG + + + 2013-10-25T11:03:04-07:00 + 2013-10-25T11:03:04-07:00 + 2013-10-25T10:52:53-07:00 + Adobe Illustrator CC (Macintosh) + + + + 256 + 176 + JPEG + /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgAsAEAAwER AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9C+S/JcXleK7jju2uvrT IxLIE48AR2LfzZmavVnMRtVNGHD4d7sH0z/nH+Owghjh1+SD6qWa3htbWOC2BltorKdnh5PyeW3h YFgwPJ2bcHjmG3plof5KWWkXDSxao8q/ULrTI4ZIVKRx3LyOJYxyokq+rxLLQFarxAI4qrdJ/J29 0nTF0+y11ViTjJEz2fJklWWdgF/fhfSEd06BCK9Dy2oVWQeSvIEPlSG9gtLwtDdXhuwyxKkpUsx9 KZ2MqyfapyRUNBiqRWv5L2okFxeal619DMktpewwtDNGFvUvGBczSs9fSCAk7VZqFnYlVuy/Jz6t aeVYJdae8fy0bmst1AJjL9anWfnFzcmCWIxhI5KvxQlab7KvSMVdirsVdirsVdirsVdirsVdirsV dirsVdirsVdirsVdirsVdirsVdirsVdirsVSby3pus2KTjU7r600hUxH1Hk4gVr9sCmZOoyQlXCK cPSYckAeM8XxJ+95/rflD87ruz0x9P1mxtb6xs7WCeCS9vWgnurdZg91I8UMMjci8ZMZHFv26hQG xnMT/S/KPndNV0+51fXDdWtsYTNDDPcw8lSO89RWVCFlJlltjyelQhFBSjKoG28p/mnDe64z61DJ a6nJqy2Aa6uC1nHeNGbGVFMJq9uIyPTDBVr8LbnFUx8vaB+YlprGpXeoalavDeRO9mrS3V0lrPLN 6vpC3P1ZJIo1bgr8lYgDZQaBVBa/5Z/NW98wSz6frcVporajZzLbfWGD/UIlIuogFtQySTH4R+9I A3+0K4qiLbyl54t9Q8tSPrT3drp9sV1cyXU0bPdPKkskixrGy3KMgeFEmZfTUhhVt8VZ5irsVdir sVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdir84P8Aldf5uf8AU36r/wBJ Uv8AXFXf8rr/ADc/6m/Vf+kqX+uKu/5XX+bn/U36r/0lS/1xV3/K6/zc/wCpv1X/AKSpf64q7/ld f5uf9Tfqv/SVL/XFXf8AK6/zc/6m/Vf+kqX+uKu/5XX+bn/U36r/ANJUv9cVd/yuv83P+pv1X/pK l/rirv8Aldf5uf8AU36r/wBJUv8AXFXf8rr/ADc/6m/Vf+kqX+uKu/5XX+bn/U36r/0lS/1xV3/K 6/zc/wCpv1X/AKSpf64q7/ldf5uf9Tfqv/SVL/XFXf8AK6/zc/6m/Vf+kqX+uKu/5XX+bn/U36r/ ANJUv9cVd/yuv83P+pv1X/pKl/rirv8Aldf5uf8AU36r/wBJUv8AXFXf8rr/ADc/6m/Vf+kqX+uK u/5XX+bn/U36r/0lS/1xV3/K6/zc/wCpv1X/AKSpf64q7/ldf5uf9Tfqv/SVL/XFXf8AK6/zc/6m /Vf+kqX+uKu/5XX+bn/U36r/ANJUv9cVd/yuv83P+pv1X/pKl/rirv8Aldf5uf8AU36r/wBJUv8A XFXf8rr/ADc/6m/Vf+kqX+uKu/5XX+bn/U36r/0lS/1xV3/K6/zc/wCpv1X/AKSpf64q7/ldf5uf 9Tfqv/SVL/XFXf8AK6/zc/6m/Vf+kqX+uKu/5XX+bn/U36r/ANJUv9cVd/yuv83P+pv1X/pKl/ri rCsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdi rsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdir sVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirs VdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsV dirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVd irsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdi rsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVZL+ X3mfQvLXmNNU1vy/B5lsVikjbS7lxHGWcUV+TRzCq/6uKpP9RvrwXV7aWUps42Z5WiRnjiU/FRnU UFB44qml7r+hTeTLDRYdHjh1i2naW51kFec0ZMpEZAXlQeov7X7OXyyROMRr1Dq4OPTZBqJZDO8Z G0e7lv8AYfmhrGCfQtZs7jW9Ll9BG9RrS5iKeqg2I4yihGOP93MGY27m8ZI5IngkD7j+pV1ZH8w6 7d3WgaTKlswQrZ2sPL0wEVCeMS0HJgTkso8WZOOO3cB+plD0RAkUkdHR2R1KupIZSKEEbEEHMYht W4q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYqyzy3+Zvm ny75V1ryvpskK6Vr6lNRWSMO5DIYzwcn4fhOKr9S/KzzZp3kCw893McI8v6jIIbZ1lBlLkuorHSo 3ibFVTVfMHmr8zfMOm2UyW/6Q4vDarGDEhFDIeRZn/lzLnlnqJgGrdTh02Hs/FKQvh5nr5Kdvf8A mT8uPMN/pzw2z36pGlwsnKRAGVZV4lGTs4yWPLPTTIoW5OKePVYxON8J/sVdP/LjXdc0WbzCksMf rmWaK2PIM/FjyodwtSDxrk4aCeSBybbtktRGMuFhea9yXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FWe 2v5E/mzdxaNNa+XZriHX4BdaXLFLA6PAyJIJJGWQiAFZV/vuPWnXFUR5s/5x8/NryrpUur6robfo 63Uvc3FtNDcCJR1Z1idnCjqW40Hc4qxLyn5R8w+bdbh0Py9afXdUuFd4rf1IoqiNS7nnM0aCignc 4qyCw/JL8076PVJbfy9cfV9Ga4j1C4cxxxK9mWWdY3ZgsxRkI/dct8VSnyX+XvnLzrfyWPljS5dR mhAadlKpFGG2HqSyFI1rQ0Bap7YqyTzX/wA49/m35X0qXVtU0Jjp9upe4ntpYbn01HVnSJ2cKOpb jQdzirC/L/l3XfMWqw6TodjLqGo3FfStoF5MQNyx7Ko7sdhir0iX/nFb88I7YzfoFHYLyMKXloX9 xT1aEj2PyxVI/wAt/wApfO/mrzLNbabo/wBcj0K8t49ehklt4vRDSsrKyzSJz/uZAQtenyxV7T/z kR/zj35u1vznYXP5feVrVNEh0yKGZLN7GxjFyLid3/dNJBU8HT4uP07Yq+ao/Leuza+3l63spLjW luHtPqMA9aQzRsVdF9PkGoVO4NO/TFXor/8AOLn53rY/W/8AD4Pw8zbi6tTLT/V9Xr7dcVeYX9hf afeTWN/byWt5buY7i2mUpIjrsVZWoQRiqHxV2KpnP5n8yXGjQ6HPq15Notu3O30uS4la1jYEnkkB b01PxnoO5xV6B+bf5c6f+Vuq6LP5b80fpa5u0nkN1b8I3gaPioAMUkn2hIfDJRkYmxzYZMUZxMZC 4lf+Vvkax/MS61bU/MupXM93AYkoki+s1VKh3Z1ckBUCrmx0enGcmUybee7X7QloRCGGMRE302Yv q2v6/wCWbnVfKmn6n6umWs89sjhUqV5kNxYglSe9D1rTKZajJivHGXp3dvpuHNCOWUalIAsj84fl l5f0jyk+pWtzIbu2WNmldlZJubBTQAbfaqtPx65mars/HjxcQO4+1GHUylOi8uzTOc7FXYq7FXYq 7FXYq7FXYq7FXYq7FX1n+b3nrzX5W/5x8/LiPy9qEumSanpthDdXNueE/ppp8bBUlHxR1PUqQffF WPf84nfmT5r1Hz1c+Vtc1O41bSdTspnFvfyvccJYqN8HqltmjLhlGx+jFUu/JLQrfQP+cqbrRbYU tdPutWt7YVrSKOOURjf/ACKYqu/P78+vzLh88a95T07Ul0zRbGZrUR2karJKhQVMkrc335GvEge2 KsusNO82eVv+cVdJXyJYXs/mHzLOsl9caZBLLdJHcGRzKPSDOgEUKRc+1dqE1xVL/wDnGY/nPpnn z9F+ZtM10eWdRt51uv0rb3Yto5VTmjhp14qzceHX4q/LFUb/AM482nlzy7/zkH590GIx27q11b6P E1Fb0o7rm0MdepVAuw7LXtirAPOvl/8A5yh8oaveare3+t3EKSPI2q2N1NcWrJUnmY42YRx/5MiK B0pirzzyJ5v816d5stW07Wb2xOqX9sdS+q3EsAuP33+7hGyh/wC8b7XifHFXt3/OXfnfzpoP5k6b Z6Hr+paVaSaNBM9vZXc9vG0hurlS5SJ1UsVUCvsMVW/84s2sGk+U/P35m3Uf1zU9Ktp1tnlPJqww Ndz/ABE15SngCf64q8htvzv/ADUg8yL5g/xJfSXgl9VoXnc2zCtTGbevpen24haYq9T/AOcwNI0u 6fyj54sYlil8xWRF3QULiNIpYHb+ZuExUnwVRir5yxV2Kpzdf4Z/wzafV/V/xB6p+ucq+l6VZKce 1acMyJeH4Yq+O9/t/Y1ji4jf0su/IAfl6fzItP8AHht/0N6Mvo/XafVfrNB6fr8vg4U5fb+GtK5j tidfnbYeW7v8zZ1/KK3a4s0sY/0kNBSSS3FwXcSen9XBT0/T9OvD4a1wiRHJjOEZbSFpz5FX8ol/ LVl1r6iL4RyjVRcen9eEoLAemG/e1p/d8P11zc6bwPB9VX173ju0fz35z93xcNjhq+GvPp77eGy6 hfzW0drLcyyW0X91A7s0a9vhUmgzTmciKJ2eyERdofIpdirsVdirsVdirsVdirsVdirsVdir6k/P TQNb1T/nHv8ALKfTbCe9isdPsZLxreNpPSR9PjAdwoJC12r0xVIf+cQPJWuN+YE/mW6spLfSdLs5 kN3OhRDPNRAiFqbhORYjoOvUYqt/JjW7bXf+crrzWLQhrS+u9WntmH7UTRymNvpWhxV53+f3/k5f Nn/Mc3/EVxV9BaH5u89XH/OLWjap+Xd1IuvaCy22owxQw3cxht2eOSMRSxyj7Dxy7CvHFXkmn/n9 /wA5M6ldiz07Ury8uyeIt4NKs5ZK7bcUtSe+KvPRF5980eZdT1e3t7zUPMMTvqGpTWcLCeN/UAeX hAq8OMjD7IFMVe3fkB+bf53XXnzS/LupSXms6PcOY78X0LPJbxqprMbkqJAVI/bYg9OpxVhv5y6Z omlf85HXNtpKxw2o1GwmmijAWNJpVhlmApQD42LHwJxVlX/OZ2j6tc/mZo9xbWc08MujwwxyRxs4 aVLu4LIOIPxASLt7jFUf/wA4tyxa5+X/AOYX5eFlh1W/tppLWOTYn6xbtau1D/vqQR18OQxV4Ra/ l154ufMieW49EvBrLSiA2jQupVq0LOaUCDqW6U36Yq9u/wCcv76wsLXyT5KglWW60OxZror+yrJF BD3NOXoOaHelMVfO1lp9/fTejZW0t1MAWMUKNI3EdTxUE03yUISkaiLRKQHNNtK1+103RtT0q50p Li5vAUS6kIWSA8SuwKMag/5Qy/HnEISiY2T17muWMykCDySl9O1COyjvntZUspW4RXTIwiZhX4Vc jiT8J79spOOQHFRrvbOIXXVOfMWtReY7uyj03R1s5o09IQW1JGmbrXiiIa/fl+fKMpHDGvd1ascO AGzb2H/nHL86PK35ZWeuaL5ssbq1uLqaO5S4jh5SHinH0ZUbgy0+0nbc9O+NKJiaIotoIO4eQ/mN 5msfNHnrW/MNja/UrTU7p54bcgBgrd3Ckjm9OTUPUnAljeKuxV2KuxV2KuxV2KuxV2KuxV2KuxV2 Kvr78xfzK83eQfyY/KzU/Ld0sE0+nWUVzFLGssUsY0+IhXVhXY71Ug++KvGfOX/OTn5reatGm0e5 u7ewsrlSl2LCIxPLG2xRpGaRgp78SK9DtirBfJHnXXPJfmKDzBobRpqNssiRNMgkSkqFGqp/yWxV C+Z/Mep+Zdfvte1RkbUNQkM1y0a8ELkAbKOnTFU5/L/80/O/kG8luPLWoG2juKfWrSRVlt5ePQvG 1RyHZlo3vir0G9/5zA/OK4gaKGXT7Nz0ngtauPkJnlT/AIXFXn/k381fO/k/X7zXdDvlhv8AUSx1 AyQxSJMHf1GDKy0X49/gpirP7/8A5y+/OG6tHt4pdPs3cUFzBa/vVqKVX1XlSv8AscVeM3t9e317 NfXk73F5cyNNcXEjFpHkc8mdmO5JJrXFXsOmf85b/nBY6ZFYG4srtoUEa3lzb8pyAKAsyuisfcrv 3riry/TvN3mLS/Mv+JdMvXstaEz3K3UFEo8pJf4aceLciCpFKbUpir1V/wDnL/8AONrI24l09Jiv EXi2o9UH+ahYxV/2FPbFXj+ta1q2t6pcarq11Je6jdtzuLqZuTu1ABU+wAAHYbDFVTQtf1PQ703u myCK4KGMsyq44sQTswI7ZbhzSxm482E8YkKKBmmeaaSaQ1kkYu56bsanKybNsgKTCfzFqs+hW+hy SA6bayGaGLioIclyTypyP942WnPIwEP4QxGMCXF1Qum6jdabfQ31owS5t25xMQGAPyO2Qx5DCQkO YTKIkKK7VdUvNU1Ca/vXD3U5BkcAKCQAo2G3QY5MhnIyPMrGIiKCEyDJ2KuxV2KuxV2KuxV2KuxV 2KuxV2KuxV2Ksh13z/5w17RNM0PV9Se70rRkSLTLVkjVYUjjESgFVVjRFA+InFWPYq7FXYq7FXYq 7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7 FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq 7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7 FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq// 2Q== + + + + uuid:609c1edc-17c6-c545-8b7c-4b32feabde86 + xmp.did:b7e44fb9-7449-479b-9994-d7777fe74ee0 + uuid:5D20892493BFDB11914A8590D31508C8 + proof:pdf + + xmp.iid:5117f072-579b-4f03-afac-e7e8629aefd0 + xmp.did:5117f072-579b-4f03-afac-e7e8629aefd0 + uuid:5D20892493BFDB11914A8590D31508C8 + proof:pdf + + + + + saved + xmp.iid:a478fe57-ac17-43dd-a456-3029de78c422 + 2013-10-23T20:23:29-07:00 + Adobe Illustrator CC (Macintosh) + / + + + saved + xmp.iid:b7e44fb9-7449-479b-9994-d7777fe74ee0 + 2013-10-25T10:52:52-07:00 + Adobe Illustrator CC (Macintosh) + / + + + + Print + Document + False + False + 1 + + 6.955882 + 4.317257 + Inches + + + + Cyan + Magenta + Yellow + Black + PANTONE 2718 C + PANTONE 297 C + WHITE + + + + + + Default Swatch Group + 0 + + + + PANTONE 654 C + SPOT + 100.000000 + LAB + 23.529411 + 0 + -37 + + + PANTONE 2718 C + SPOT + 100.000000 + LAB + 56.078430 + 3 + -48 + + + PANTONE 297 C + SPOT + 100.000000 + LAB + 74.901962 + -20 + -27 + + + WHITE + SPOT + 100.000000 + RGB + 255 + 255 + 255 + + + + + + + Adobe PDF library 10.01 + + + + + + + + + + + + + + + + + + + + + + + + + endstream endobj 3 0 obj <> endobj 7 0 obj <>/Resources<>/ExtGState<>/Properties<>>>/Thumb 44 0 R/TrimBox[0.0 0.0 500.824 310.843]/Type/Page>> endobj 38 0 obj <>stream +H‰ÔWËŽ7 ¼ÏWôŒV$õ¼zädAù€A¯Çÿ¤HJ=Ý»½c{8˜íRSE²J쇟—‡·qyóÃãrzxü5.—K´¿åãåýéá' ýùñ¤9ÆÐ8-BøŸdùû÷Ó§x5¦ÁycñÛò^×%]—|ÍxÐõh¡È¡dê q±Q^.O¶ÕÓ‰B¯u9SÈ–H–… /-´,\NTBfž˜Z üãÐ9O@bOj:^RàRL¬¹/\C”ºô Ü&¸œrš'äÀ”V/:œc TFzh¾´°Òf•¾B +­ãØ¥ë¬sÆ* ³ðNýî„·%ÑË¡Pp(` +~]q É+Eí)HBFb l‚íIáð)àÚ&ºœÆsB¦XÛŠ4PTÔÖ16`Ž¡ÏcíŽ|Aü²MhVJµ*õ™Ðs +‰‹®Ùà9æwÁ2Œ8ciìÌšÔˆP*§u@$”Z,È{O#9dI†%$”És¨ˆ] =YhÉeªk^b¬#dÃ) µjy}vª‚7É£„É­ó¦Lk¨ {h*ü‘pÌt… %ÁRGÉ¡rðµŽx€²D.i–ŠðWÛÜÊE+§`3øEôœ™´*SÖÐUT„f;I™k&L^ß"Ú±Cɧdpraá¬ÖZ³YžÕùjÉV—EKy¬Æ «Ñ9!Ò%(ò´̪†£Û­EŽ êyî‘—ó•ª1kd:Op™n Œª¤®¢26›”%«pÄêÝ‹rÄÚYi¯å¬©¿V¦„ŽZC =1(†JV“‰TJÊ(Il…XznPNp +‡ˆÂµWA—S +}<+if´ñ„™ÕùQ$Èëà"‚å|G{RKu±ëi0÷QÄi¬ù›aã•Á‹èI” +¦].3Î#S-묨•é2¦’îˆA:ZÆ$Ö¼ª¦"ƒ8mµ‡fë k¢tQ1L+ dšœ €å#¡$ã ׳ɸnTIÅ²Õ Ï϶3²QγKÌÙkQˆÜyVâ»Õ¶( ãžÁZì1XT»L¥Xu² hø]÷Æ”ç ém<”®ƒ‡ ’ ^g Îyµ`{Þl9ퟸh_1’¢W‘¥¾™°&xáࢲߵæüÈÕ0ƒØ5#³xo÷ø ÂVÍ.7ÊË·-axÃNئ‘Æ«’>x™œ,6€‚¶ Œ­Sˆ&Kq}ºœô ­X§ñ:m uÕ}OŸ¯Z=ªûÉôBZZGÎÖ¨¨7€ ¼z;G¼½˜ó]:â*'6,^›2Ѻäð-ÇÌéÐÕYçº4>ìÒ¤ ‰§žU:÷´¨ÙÚ´Š‚B®K¼¢¤˜F#¡ +¦ +›Ïp0&¸¥ÿZÉè¨ZÈ2vpWß ýÙíŽh-µØÕŠÞµ¶s^¥eþËVÄ©b’-¦’«³}ÜÁ°u#ÿ5ÙÒQµXWuó¸Úø¨ê3uýnM»h°¢—Ë;×2Ö–ù]Œ¬}G­Ós½[Ùî­ÜÔmü×/êj×ÞÝWqãh&Z >jv¾œÿ¹"T´Š3×ÓÎiIÞóyW@ÞB?©µinë×îÅa>šO3ÿµNB×cýâjåÃ6ß}Ë«ïëwêŒÝ‹rÀÈ%®u¤­k7({d¿2."”o´6ÍQ¶ÊÎUM†±ƒaëFþkÐQ·‹×!yÓÆGÕ°.¾“î5|Ç P ’&’Ÿ¸íÜnX¶–n[·¯#0ïĈ€~]Ž9Ì;óçp¸ý{æcס=©jÛ 2´Žà‡¹ß&qÛ˜û‹¹¿N㺥±5$·yŒ´ÔÚ—&œµî¢¾a²ì˜,“¢·‰L·˜œ?ƒÉ4H|@e:¾Äð1³Ñ7ÅRw5$ŪÞþ¡­µ¶ÍQòl. +ë‡;PÛ<ü×ú\]A-æ2n=˜4íü•/¡Ïu¬}˜ 4À=G6 +—Vò÷¢>e£>¯PÂYjLÆÞ®:·Ôgß>”{Õ§~Z}êF}ê-õzŠ~á gÆ'k¥cõI;õI.,ü\}ø.õ‘CõiŸVŸ‚¾;önÎŒËëKÔçú}Dz)AåN j·$êÙô“•„C‹(‡/‘ Í3 z¥‘¶¢j:Š²¾ß$qýj×{HüJ aúC0‘˜úÓýX=Þå> ™e +-ó+ê)»ÀË÷øµfÌu*ß¾fäÞ¶m[Ìh‡ët3êå«£~×õZÇŒF³¥ò¿*÷ß>.ø÷^òæx endstream endobj 44 0 obj <>stream +8;Z\q_%":)&-THj$HTp9!jAAL'MLSp^l]/L+IhF,h8g@&0^RKnNoU%hS02C"+hD;ojIbU'=Dt'=b3<.,b!!MYF9)~> endstream endobj 45 0 obj [/Indexed/DeviceRGB 255 46 0 R] endobj 46 0 obj <>stream +8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0 +b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup` +E1r!/,*0[*9.aFIR2&b-C#soRZ7Dl%MLY\.?d>Mn +6%Q2oYfNRF$$+ON<+]RUJmC0InDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j$XKrcYp0n+Xl_nU*O( +l[$6Nn+Z_Nq0]s7hs]`XX1nZ8&94a\~> endstream endobj 36 0 obj <> endobj 47 0 obj [/View/Design] endobj 48 0 obj <>>> endobj 43 0 obj <> endobj 40 0 obj [/ICCBased 49 0 R] endobj 41 0 obj [/Separation/WHITE 40 0 R<>] endobj 42 0 obj [/Separation/PANTONE#202718#20C 50 0 R<>] endobj 50 0 obj [/ICCBased 51 0 R] endobj 51 0 obj <>stream +H‰b``ßâèâäÊ$ÀÀP\˜ì“˜¤ÂìçؘÀ 1¹¸À1 ÀÄÎËÏKeÀß®10‚èË: ³0åñÖä‚¢ }ˆRR‹“ô N*/)Š3FÙ"ŽFN@v›‡“‘#œ]’ZÒËàœ_PY”™žQ¢`hii©à˜’Ÿ”ª\Y\’š[¬à™—œ_T_”X’šT µ8@~u15 ÑÉ„ADd”„…›Ü´C‹™™‰"†°ñ™˜YXÙØ98¹¸yxùø…„EDÅÄ%$¥¤edåä•”UTÕÔ54µ´utõô ŒMLÍÌ-,­¬mlíìœ]\ÝÜ=<½¼}|ýüƒ‚CBÃÂn‹Ž‰‹OHLJNIMKÏÈÌÊÎÉÍË/(,*.)-+¯¨¬ª®©­«ohljnimkïèìêîéíëŸ0qÒä)S§MŸ1sÖì9sçÍ_°pÑâ%K—-_±rÕê5k×­ß°qÓæ-[·mß±s×î={÷í?pðÐá#G?qòÔé3gÏ¿pñÒå+W¯]¿qóÖí;wïÝððÑã'OŸ=ñòÕë7oß½ÿðñÓç/_¿}ÿñó×ï?ÿýõÿ÷?üB†ÿÿÿƒYÿ!x¤‡Ë¨ÿG´ÿ í,<ì endstream endobj 52 0 obj [/Lab<>] endobj 49 0 obj <>stream +H‰œ–yTSwÇoÉž•°Ãc [€°5la‘QIBHØADED„ª•2ÖmtFOE.®c­Ö}êÒõ0êè8´׎8GNg¦Óïï÷9÷wïïÝß½÷ó '¥ªµÕ0 Ö ÏJŒÅb¤  + 2y­.-;!à’ÆK°ZÜ ü‹ž^i½"LÊÀ0ðÿ‰-×é @8(”µrœ;q®ª7èLöœy¥•&†Qëñq¶4±jž½ç|æ9ÚÄ +V³)gB£0ñiœWו8#©8wÕ©•õ8_Å٥ʨQãüÜ«QÊj@é&»A)/ÇÙgº>'K‚óÈtÕ;\ú” Ó¥$ÕºF½ZUnÀÜå˜(4TŒ%)ë«”ƒ0C&¯”阤Z£“i˜¿óœ8¦Úbx‘ƒE¡ÁÁBÑ;…ú¯›¿P¦ÞÎӓ̹žAü om?çW= +€x¯Íú·¶Ò-Œ¯Àòæ[›Ëû0ñ¾¾øÎ}ø¦y)7ta¾¾õõõ>j¥ÜÇTÐ7úŸ¿@ï¼ÏÇtÜ›ò`qÊ2™±Ê€™ê&¯®ª6ê±ZL®Ä„?â_øóyxg)Ë”z¥ÈçL­UáíÖ*ÔuµSkÿSeØO4?׸¸c¯¯Ø°.òò· åÒR´ ßÞô-•’2ð5ßáÞüÜÏ ú÷Sá>Ó£V­š‹“då`r£¾n~ÏôY &à+`œ;ÂA4ˆÉ 䀰ÈA9Ð=¨- t°lÃ`;»Á~pŒƒÁ ðGp| ®[`Lƒ‡`<¯ "A ˆ YA+äùCb(Š‡R¡,¨*T2B-Ð +¨ꇆ¡Ðnè÷ÐQètº}MA ï —0Óal»Á¾°ŽSàx ¬‚kà&¸^Á£ð>ø0|>_ƒ'á‡ð,ÂG!"F$H:Rˆ”!z¤éF‘Qd?r 9‹\A&‘GÈ ”ˆrQ ¢áhš‹ÊÑ´íE‡Ñ]èaô4zBgÐ×Á–àE#H ‹*B=¡‹0HØIøˆp†p0MxJ$ùD1„˜D, V›‰½Ä­ÄÄãÄKÄ»ÄY‰dEò"EÒI2’ÔEÚBÚGúŒt™4MzN¦‘Èþär!YKî ’÷?%_&ß#¿¢°(®”0J:EAi¤ôQÆ(Ç()Ó”WT6U@ æP+¨íÔ!ê~êêmêæD ¥eÒÔ´å´!ÚïhŸÓ¦h/èº']B/¢éëèÒÓ¿¢?a0nŒhF!ÃÀXÇØÍ8ÅøšñÜŒkæc&5S˜µ™˜6»lö˜Iaº2c˜K™MÌAæ!æEæ#…åÆ’°d¬VÖë(ëk–Íe‹Øél »—½‡}Ž}ŸCâ¸qâ9 +N'çÎ)Î].ÂuæJ¸rî +î÷ wšGä xR^¯‡÷[ÞoÆœchžgÞ`>bþ‰ù$á»ñ¥ü*~ÿ ÿ:ÿ¥…EŒ…ÒbÅ~‹ËÏ,m,£-•–Ý–,¯Y¾´Â¬â­*­6X[ݱF­=­3­ë­·YŸ±~dó ·‘ÛtÛ´¹i ÛzÚfÙ6Û~`{ÁvÖÎÞ.ÑNg·Åî”Ý#{¾}´}…ý€ý§ö¸‘j‡‡ÏþŠ™c1X6„Æfm“Ž;'_9 œr:œ8Ýq¦:‹ËœœO:ϸ8¸¤¹´¸ìu¹éJq»–»nv=ëúÌMà–ï¶ÊmÜí¾ÀR 4 ö +n»3Ü£ÜkÜGݯz=Ä•[=¾ô„=ƒ<Ë=GTB(É/ÙSòƒ,]6*›-•–¾W:#—È7Ë*¢ŠÊe¿ò^YDYÙ}U„j£êAyTù`ù#µD=¬þ¶"©b{ųÊôÊ+¬Ê¯: !kJ4Gµm¥ötµ}uCõ%—®K7YV³©fFŸ¢ßY Õ.©=bàá?SŒîÆ•Æ©ºÈº‘ºçõyõ‡Ø Ú† žkï5%4ý¦m–7Ÿlqlio™Z³lG+ÔZÚz²Í¹­³mzyâò]íÔöÊö?uøuôw|¿"űN»ÎåwW&®ÜÛe֥ﺱ*|ÕöÕèjõê‰5k¶¬yÝ­èþ¢Ç¯g°ç‡^yïkEk‡Öþ¸®lÝD_p߶õÄõÚõ×7DmØÕÏîoê¿»1mãál {àûMśΠnßLÝlÜ<9”úO¤[þ˜¸™$™™üšhšÕ›B›¯œœ‰œ÷dÒž@ž®ŸŸ‹Ÿú i Ø¡G¡¶¢&¢–££v£æ¤V¤Ç¥8¥©¦¦‹¦ý§n§à¨R¨Ä©7©©ªª««u«é¬\¬Ð­D­¸®-®¡¯¯‹°°u°ê±`±Ö²K²Â³8³®´%´œµµŠ¶¶y¶ð·h·à¸Y¸Ñ¹J¹Âº;ºµ».»§¼!¼›½½¾ +¾„¾ÿ¿z¿õÀpÀìÁgÁãÂ_ÂÛÃXÃÔÄQÄÎÅKÅÈÆFÆÃÇAÇ¿È=ȼÉ:ɹÊ8Ê·Ë6˶Ì5̵Í5͵Î6ζÏ7ϸÐ9кÑ<ѾÒ?ÒÁÓDÓÆÔIÔËÕNÕÑÖUÖØ×\×àØdØèÙlÙñÚvÚûÛ€ÜÜŠÝÝ–ÞÞ¢ß)߯à6à½áDáÌâSâÛãcãëäsäüå„æ æ–çç©è2è¼éFéÐê[êåëpëûì†ííœî(î´ï@ïÌðXðåñrñÿòŒóó§ô4ôÂõPõÞömöû÷Šøø¨ù8ùÇúWúçûwüü˜ý)ýºþKþÜÿmÿÿ ÷„óû endstream endobj 39 0 obj <> endobj 53 0 obj <> endobj 54 0 obj <>stream +%!PS-Adobe-3.0 %%Creator: Adobe Illustrator(R) 17.0 %%AI8_CreatorVersion: 17.0.0 %%For: (Brent Couchman) () %%Title: (FB_Presto_Logo_PRINT_DarkBG.ai) %%CreationDate: 10/25/13 11:03 AM %%Canvassize: 16383 %%BoundingBox: 0 -612 501 -266 %%HiResBoundingBox: 0 -612 500.823529411764 -266.93212890625 %%DocumentProcessColors: Cyan Magenta Yellow Black %AI5_FileFormat 13.0 %AI12_BuildNumber: 256 %AI3_ColorUsage: Color %AI7_ImageSettings: 0 %%DocumentCustomColors: (PANTONE 2718 C) %%+ (PANTONE 297 C) %%+ (WHITE) %%RGBCustomColor: 0.356772124767303 0.530214905738831 0.853949785232544 (PANTONE 2718 C) %%+ 0.432647913694382 0.769827246665955 0.912153005599976 (PANTONE 297 C) %%+ 0 0.226927161216736 0.43734136223793 (PANTONE 654 C) %%+ 1 1 1 (WHITE) %%RGBProcessColor: 0 0 0 ([Registration]) %AI3_Cropmarks: 0 -612 500.823529411764 -301.157476056923 %AI3_TemplateBox: 396.5 -306.5 396.5 -306.5 %AI3_TileBox: -127.588235294118 -744.578738028462 606.411764705881 -168.578738028462 %AI3_DocumentPreview: None %AI5_ArtSize: 14400 14400 %AI5_RulerUnits: 0 %AI9_ColorModel: 1 %AI5_ArtFlags: 0 0 0 1 0 0 1 0 0 %AI5_TargetResolution: 800 %AI5_NumLayers: 1 %AI9_OpenToView: -78.3267011539992 -229.604058893753 2.513 1625 1075 18 0 0 46 64 0 0 0 1 1 0 1 1 0 1 %AI5_OpenViewLayers: 7 %%PageOrigin:90 -702 %AI7_GridSettings: 72 8 72 8 1 0 0.800000011920929 0.800000011920929 0.800000011920929 0.899999976158142 0.899999976158142 0.899999976158142 %AI9_Flatten: 1 %AI12_CMSettings: 00.MS %%EndComments endstream endobj 55 0 obj <>stream +%%BoundingBox: 0 -612 501 -266 %%HiResBoundingBox: 0 -612 500.823529411764 -266.93212890625 %AI7_Thumbnail: 128 88 8 %%BeginData: 4324 Hex Bytes %0000330000660000990000CC0033000033330033660033990033CC0033FF %0066000066330066660066990066CC0066FF009900009933009966009999 %0099CC0099FF00CC0000CC3300CC6600CC9900CCCC00CCFF00FF3300FF66 %00FF9900FFCC3300003300333300663300993300CC3300FF333300333333 %3333663333993333CC3333FF3366003366333366663366993366CC3366FF %3399003399333399663399993399CC3399FF33CC0033CC3333CC6633CC99 %33CCCC33CCFF33FF0033FF3333FF6633FF9933FFCC33FFFF660000660033 %6600666600996600CC6600FF6633006633336633666633996633CC6633FF %6666006666336666666666996666CC6666FF669900669933669966669999 %6699CC6699FF66CC0066CC3366CC6666CC9966CCCC66CCFF66FF0066FF33 %66FF6666FF9966FFCC66FFFF9900009900339900669900999900CC9900FF %9933009933339933669933999933CC9933FF996600996633996666996699 %9966CC9966FF9999009999339999669999999999CC9999FF99CC0099CC33 %99CC6699CC9999CCCC99CCFF99FF0099FF3399FF6699FF9999FFCC99FFFF %CC0000CC0033CC0066CC0099CC00CCCC00FFCC3300CC3333CC3366CC3399 %CC33CCCC33FFCC6600CC6633CC6666CC6699CC66CCCC66FFCC9900CC9933 %CC9966CC9999CC99CCCC99FFCCCC00CCCC33CCCC66CCCC99CCCCCCCCCCFF %CCFF00CCFF33CCFF66CCFF99CCFFCCCCFFFFFF0033FF0066FF0099FF00CC %FF3300FF3333FF3366FF3399FF33CCFF33FFFF6600FF6633FF6666FF6699 %FF66CCFF66FFFF9900FF9933FF9966FF9999FF99CCFF99FFFFCC00FFCC33 %FFCC66FFCC99FFCCCCFFCCFFFFFF33FFFF66FFFF99FFFFCC110000001100 %000011111111220000002200000022222222440000004400000044444444 %550000005500000055555555770000007700000077777777880000008800 %000088888888AA000000AA000000AAAAAAAABB000000BB000000BBBBBBBB %DD000000DD000000DDDDDDDDEE000000EE000000EEEEEEEE0000000000FF %00FF0000FFFFFF0000FF00FFFFFF00FFFFFF %524C455B5AFFCF5252527D52527DA8527DA87D527DFFFF7D7D52527DA852 %7DFD64FF5A85FFFF5252527D527D7DA8527D7D7D527DA8FF527DA87D27A8 %52A8FD75FFA8FDEEFFAFA9FFFFFFA8A8A8FFFD04A87DFFFFA87DFFFFFFA8 %FFFD04A8FD65FF3661FFFF7D7D2727527D52FF52277DA852277DFFA85227 %527DA827A8FD64FFAFA9FFFFA8A8A852A8A8A8FFA87DA8A8A87DA8FFFF7D %A87DFFFF7DA8FDE4FFA87D7D7DA87D7D7DA87D7D7DA87D7D7DA87D7D7DA8 %7D7D7DA87D7D7DA87D7D7DA87D7D7DA87D7D7DA87D7D7DA87D7D7DA87D7D %7DA87D7D7DA87D7D7DA87D7D7DA87D7D7DA87D7D7DA87D7D7DA87D7D7DA8 %7D7D7DA87D7D7DA87D7D7DA87D7D7DA87D7D7DA87D7D7DA87D7D7DA87D7D %7DA87D7D7DA87D7D7DA87D7D7DA87D7D7DFDFCF8FDFCF8FDFCF8FDFCF8FD %FCF8FDFCF8FDFCF8FDFCF8FDFCF8FDFCF8FDFCF8FDFCF8FDFCF8FDFCF8FD %06F8275227FD04F80C28FD04F8282FFD71F827FF27FD04F8430CFD04F854 %5BFD78F80505FD04F80028FD74F8272727FD04F80506FD04F80528FD71F8 %27FF52FD04F8673CFD04F85A5B28FD70F8272727FD04F8062EFD04F80528 %FD74F8272727FD04F80528FD04F8285405FD70F827FF7DFD04F83D3DFD04 %F85B5A54FD57F8277D27FD16F8277D52F8F8F8052F35FD04F82F5B28FD57 %F852FF7DFD1AF82727FD04F80635FD04F8052F05FD31F827F8F8F82727FD %0EF82727FD06F8272727FD04F827FF7DF8F827F8F8F8272727FD11F8FFA8 %F8F8F805434336F8F8F85B5B5BFD31F852FF52FFFFFFA827F8F8F8FF5252 %FFA8F8F852FFFFFFA827F8F8F8A8FD04FF27F8FFFFA8FFFFFF27F8F852A8 %FFFFFF27FD0FF87D7DF8F8F8053C430CF8F8F82F5B30FD31F87DFFFF7D52 %7DFFA827F827FD04FF52F87DFF7D5252FFFF27F87DFFA8527D7D52F87D7D %FFA87D7D27F87DFFFF527DFFFF27FD11F8527DF80505F8063605272827FD %31F852FF52F8F8F87DFF27F827FFA852F8F8F8FFA8F8F8F852FF52F8A8FF %FD07F827FF7DF8F8F827FFFFF8F8F827FFA8FD0BF85405F8F8F827FFFF52 %F8F8F843433DFD34F87DFF52F8F8F852FF7DF827FFA8F8F8F827FF7DF8F8 %F827FF7DF8A8FF52FD06F827FF52F8F8F827FF7DFD04F8A8FF27FD0AF82F %28F8F8F852FFFF52F8F8053D673CFD34F852FF27F8F8F827FF7DF8F8FF7D %F8F8F827FF7D7D527D7DFF7DF827FFFFFF7D27F8F8F852FF7DF8F8F852FF %52FD04F87DFFFD11F85252F82752F8060D05FD34F87DFF52F8F8F827FFA8 %F827FFA8F8F8F852FD07FF7DF8F8277DA8FFFF7DF8F827FF52F8F8F852FF %52FD04F87DFF27FD06F8005305FD04F82F28F8F8F827FFFFA8FD37F852FF %27F8F8F827FF7DF8F8FF7DF8F8F827FF52FD04F827FD06F827FFFF27F852 %FF7DF8F8F852FF7DFD04F87DFFFD07F8275A28FD04F8302FF8F8F852FFFF %A8FD37F87DFF27F8F8F87DFF52F827FFA8F8F8F827FFA8FD0CF87DFF27F8 %27FF52F8F8F827FFA8FD04F8FFA8FD08F800FD05F805FD05F87DA852FD37 %F852FF7D27F852FFFFF8F8F8FF7DFD04F87DFF7DF827277DF8F8527D2727 %27FFA8F8F827FFA8F827F8F87DFF522727A8FF52FD04F80028FD05F82E00 %FD04F82805FD3BF87DFD06FF27F8F827FFA8FD05F8A8FD05FF52F8A8FD05 %FF52F8F8F8A8FFFFFF52F8F87DFD04FF7DFD05F8285B28F8F8F8275B2FFD %04F82F5BFD3BF852FF27275252FD05F85227FD06F82752522727F8F8F827 %52525227FD05F8525252FD04F827525227FD07F805FD05F827FD05F800FD %3CF87DFF52FD7DF852FF27FD7DF852FF52FDFCF8FDFCF8FDFCF8FDFCF8FD %FCF8FDFCF8FDFCF8FDFCF8FDFCF8FDFCF8FDFCF8FDFCF8FDFCF8FDFCF8FD %1CF8F8 %%EndData endstream endobj 56 0 obj <>stream +&1âfxê[ú³×1gÆ~ÑŸšùª¢E£•%j¢KÁti¦qàÅüx-‹B;Š—XZà?V2s=V9Ät˜¢0í„–ˆÿíù²ºÚ„ħ|ÙnB/æÓúÒK˜/m#ÖÄÐî'ˆT=Í|4°ˆFó +ð9º,Žˆ10Œ¼Òbrº@DšØfÀSTIäÌðŒh‰Uè1 ÑcÑB°^¦Ee‘•d…ðe‚aÌ‚„'4,ó³BÓ¾@BŸÅ!d%Ý BDË*1„„ ž^ç{&’׆ޥñ%L\4ô(Àâ…,òƒh¬x!Њô…ů·”ž0~G! +­G$æ#g 3ôXNÇ"±5¾¥T’sC"eŸN¢eejÞúXQb$eKI*ª+Zj¹îå½)ì ÝG”‚˹iÄžÿ#fÎä9§ã„‘mKgÜÒÑr†˜odɕ׎ˆ„AÄÍ“@aÉ»B54 ¡yÈB/l<±fÍÜ„´»0æ!CYÂtH†(Á bž'z%q*^0èD‡ü#½ÉˆÎB˜),¶\" 0‹ó¼Ài‘ ‹*º/ÒXuè•J³D1PìˆÕ3Ðb ½',?VÊ`¤9Ñ*· ¹`.1¡뚥ùu´h6qÄ‹F¸aJk \Á “ZðZùh 2ŸGl\Ðj'Ì £!­qš¢«"Ò ,=Üc½2a‘ʳ!­›ª–J(Z=óŠyI{/é÷E# ÎƒäGC²ðˆ4­µÉm‚-ЄDžbÉL‚¨Ç£Š<ÚŠ5Zû8–·ç×­+ó0+WLRaHaID/‚‰¢ñhÏ!5˜P– Ai°å‰Uìó}´?Z6†Ô=Vp¡ß ++5°yh4X¢€—«8†nè¼-W¢4Úx±N0'/aD ^íåúYÐrs[¡@ M +¨üó­Ve‡5tè„ïRŒÇt“o|ÃöŒtA@,–ŒÔ%méUÃìžã›ˆ-cg¢ÓÛ41ÇJyM€¤¬~‚†aOCÑ?² Ú ±ŠJï1,‰Éü(‰ck*³bGö°UdzO’ K9…— 5á{4L02œ"ˆ\¦Æ0 ®L¨'º²Æ@èYÙd\Ù[bpeZíëÄútS,àóRÐzŸB0A˜‘㊸YÝ%d‰NË?ÃËÿ¤±ïåé`¬~È{ZX¤™®°æÜ …3&V˜yam€JÐD±i‹å'žÊÖ˜,9¡vEýЮ9£60Ê°C\º>"вè¡O,Ï!ÃŽ™¤‚p8\‹¬u Ö'è=Sy´ÕÌÉÀÚˆh,B²U”†KXeDt¬"Î`¬Ù$V„gZT=üÍr¯0Ë‹ƒ äD«@x_¦“_Ál†ß›wDôŠU‰ lvÔ²NÌ;èo¬$”‡¨×ü»òvÒžAA¤Í!ÖÈ¿DÆ$¨`XaßÃ;I¤y‘¸0ÃW^Å.æŠó&hÔ1;RŠóè’c˜=%bGpd2®û³è@ãgå»:y­|åV€ujp@^ýtȾáØ0£‚xsc')æp§jÍkh€ÏÅQtÅr*9üáN.Z„ ò‘…:b ¾âKhãxíT€‡À§  _zìŒ }R…BÄbB+«x6ÿïBÐÏœyŒ—xÌbaË’M:jB[Š+ˆ‘iþ›E(IJ2|ñÜ+­›,¨&Á&ú[¼^ÐG4k·J”ÃHÜ×F´_ÞC6i +ìg[ { žÆJ[LÔŠWj-ÚOÀì™ï`¯™€ éÀ•eL#ˆC!Nõn¨É*…ºÊá°Äº=f,w"– •–¼Elgo¨•8bPpØ!"¹Û€ý †H@)ñç‘|ŠØŸ:”g´>›ƒ¸Ý"…çÃAG˜ÅÓ,`Ï.@ìÄ#2âØð7„;¸]"”"P…Ui®õ¡Òz”V¬´ªõ¡‘Èr´2Ϭ9$‚ÀJ 3Q-ÞŸ”òm +ò‹@žÇkìö¡¹ØÏ»¡Bp¨;*Ç—˜Ÿ¢PÐ2ø>Ë$¢+Ÿ¤I)â2ó°^šrLL~­c˜ÂaY–yY9ÅOr|l%?!}Bc¼ÊGÅ,lᎠ5Wआ¨ˆíyèÈ8{ÓY˜‹“‹Þ«B–á©KŒYcVC‹WFìHö;?®Š¡e“Æ>Ä:˜'Ö K´0°xC²IÞ„B6BiA.^ÙÔP¬bYmdx4$+Ëèä±7ƒF'+°~ŽÒdÁÝ KRƒÖÊ.ø<(R±‹_Âo å| ›m ª sÀÙÀ“F6–'NJ¦(\#ʽ0àÿƪ!.*·0‡®`w9:Œ¨rÝHs Š•Mžˆ¹‰ð›G*I!àD{ ­]&ö2@¬À`;IÆúÌ*<",¬«c"nÁœAV[ˆt8ôe$ˆ Yûå{|TÀ ŠD¾Ë€‰9’&Ï(aN€÷H¹g£Ý½Ìš†‰ 9`‡«¶Ž ·©‹Ñ‰ù±ùg³  V!Ën ¿ˆ%o†6æË=ÊÆa”ÜSŠQãëÅ|9H?Ób»jÝ¢å8« ”£±Åˆm)®[g·ƒPJäB…´ZüÀôØ_j‰†ý 6úC¸3ª‘’JúÅ°Ç^m-Øg5§Šb‹˜\k‰ïÅe~7Ô*³¢˜}Ñ"¡U¶C)%âÝ»c`Á&‘røK ÑqÙy]BŽ*àáÌŠØi“¤ì)…-’Þ µLY(§52ê²Ï›'ö¥£({•%Æ´ˆ¹×»bƒÉæ²heŒ„ÅÆásÄ+èð(H8ëOK(È2^PŸµ.ðžRñ8hlç¥|Žù³Ê™…PýLëp~>æÿŽK]:œ†HôÚ†}a¨k&“$ AÕ°®ËDâ%ÈË„’ZG"» +ÖÈÆ­9âH/Ž…V…Ãåò“Ïq%«hBëÁ‹8y`˾Be½˜Ú¥ á ýÀ²fo·áüR‚iߦ´iÑ¡”g$†œ(DªðÂÊ™2V +U±8‹ƒÛMCĨØ=™&jðØíÊà¡6•.2JrG•DKsæ|âÊ”W¯´Æul Å9Æ c¤ W³#€õ:ö¸Âà4JŒù$7WbÛ³° +8Þ ªv}Q_¢ì«b‰ÿ$iÒŠr%’’Áoœø+íJWl3`¶±É©›p[øµoÙ´àf±IóyÄ)7îTð9c… T2³X‹ËB¼z¡8¯%%…—&0{%ÃÙØ,¨úl ð•³%22ÎÛ‰X+¯h}¨Åº—÷¦¸uŽßÚâ±eÉ{AŠ‘åÔ6gšsb$ɾ^M‚Å„›wC-ÒrÊ©;¥ÆêØ)v–·Æ.Z ÂO*ígw’/JÙ-G`½â›ãÜßcíCR"f²ðqÈŠˆWâ:PF$)C³dçà•ª¤Âˆ¢ÓËZàb ]ó8=ÓñK«ÉÂ!áŒ_°"ÖEa‚‡‘èÅF”šRJS}¨EâS)5ª˜>U瀬]Oô©4ŽÍÖ«1’®%ª"ÞÈ¿˜áp8»;°êÌàK¹ùï†Z$ð—Rü‹éFõ¡rNR9m©ErÓÞPiƒ'²PÆÒƒêC-r©Zä[³²fZæn“YÞ µÌy)åÅ“gdU + A夡V©E±Um¦c„Þ ¢!‰†’Àu™X‰`ÓdͶ„€¤†ã2ìrJ„f]fGÄnWNþ…¿KÖ\\Ìp^ÊZbÒZ}zȈÓìQ`T øXí«€/x( xG+x[ x«" €2¦#AlÒhd%'[aõDy8†ØŸ Ç­$)ÄÙ€`ºu|ŸØ ?¸yÌA +\ƦԇZ¦ªÓÙJiM"Ì +¹Oåô¨B•0«+ˆ ¹Ã¦‚Y-—pö V6Ã?5òwé àÈ]bà +ÞíÀ:^¶˜ +i9 ûfüX<ö¢aß]dõFÚð¦'ì“àˆ€$H (PJœžy|éZÊ!›eHWIbIáHØš¦‹8Öç 4â&@¬_}·?‘,AGöeÙÉGµ„’ÙçI €3íHªÄx0‹§ˆÏºá±!Cr#ž/ã˘Cá ‹Ù÷9éÌ…º%Kš3‚ ·B› u˜³Œ-ì—ðÂü––6½ˆ’êFZ ,ä§Êß6{´ «1’ÄC’ó [ ˜V'2.ŸyWJÍ+¤ïÉø]Hψ¤†0ðØíd_”ßi+Þ$äàÄštÄɃ2¬ ³Ö„e6჈ìÈf%®ËgŒxÅYqØíX¤)â_Ðò| '•ÐAØ\eJH•G¼™ÈY˜•¨ª1]l\ê„„âÔ? s…Ýd“K¹„…Ì7a5…ì¸Rþ\!Ç®Î9vÆ&0ØĦ¤ÊñVèî^d“ž#ÉÔãô©î<$RÅì÷ÓH&BRk [¢Ýxœå!9¨±M®!µ‰ÛAbÅÓ…UXo1’Lˆ0”4g´ˆݱõZåû•2á +9eYg¥¬´b>›]P¿9ÑINÎ…ÖBñÈ„c‘²ŠûñÀO¬›Ù°‡'‚‡ÂÍ1ØÐyaÕ%“¡Õ_C{’ˆ< Ÿý€FâébR°d¥‡+–ë!§·Ç‘¥ËÌ–W ŠXRÀ#!ž…u{³ËY NÛ[ +‰”’ˆWH¶,¥c¶°;rŽÔlv3E–›ñÉ6¢¦÷'æx>bõ¼d­v/¿¿%(" c–a{:¶†£\=©¥PÌ)dîXÈ;,e&’eÿ„{Är@¤Â‚I²9žì0‡ ”UæÍ!‡8±B˜†°YÀ¥ØÿGª)t-Ÿ‰•â¤d*³"ÅQ?`dœfðŠÃ1Mø&*#Äy+Hwc•¨¸',’|ËááÐôD#C#à”:9ß̶.ÿM*›‰Ëog7\a„¥9ç)>8ö‰óÒ@h†C ZWEY%™À*92hk€KÌgr•)%?²W½YÊ l™di™³îÅcââ)—éâ d©±rOŠ?‹5„¡khd[ŠU ä•¿šG“Vbsh8î¡L <Ç&B€KëD®b¹Eg)ϳ” ZÌ­7ÿ ‡DÒ $ø§mò+š"ý"¡…‹'4qvN(\Y†1’êíRÉXiVq)ƒ·>Ô*Ç·˜œÍž)å×ò]7(9=¼èâó>Jó=Œ[:ñ¾†\(¥Õî÷¯¸¿E @† $,*ö•ði"NþS8ΦT)™¹>TLw.fCç³¥ëHá‹™|#Îi—D@ó|_ +>@ëàcLJ¦äô#b¥cÎ Ìg!³” yÌŒl5ÇV.BáÇ{Ö cn‚}hÈ™XèÛ€xPú$ƒµ9$`›w0Sܸ‹?Ê[ÌêÄÈ5ìAGz¹gBpiƒuçÍ e ‰àNÀa'7Íâ´‘m—Y•ÂŠ—´Î }À¬Ø¥k›vï±rħu±EQ(BÌîкG +yÊï†J‰Ìù4çBôLi‘º‘…¦Ç™ƺ$Žt$©²y=’«‡ð Äh‹¤®bÞW)1¬”°”#Ø*‘°Uºa9%±Vj‘:VN/+¦ q暥YìJñƒ|vßRL@*')gľL26®Ì¶'Ç ga›¹.l,3bÊY3¥ÜÉgbϪq$’£<¡WJ)1‚ï*gO”3, +rIŒ(ÄÑ[ÅÚóq@ö¤”¢…-cŠùÈ£uPcí #rÈ)²h‡pë#å0œ ß•ƒuå€^9ìWŠ –¢‡ùˆ‘¼Œ×Y[/-‘ƒL.EGä–b¥bÉGaøšHÄÙÒ@]¾†­lß]/x¿ +ƒ¡…¼™oþh,a‘8áC&Æê¿E÷(ßRð ]¬7#|ÁÓט÷M1u%ì"ì¹TöK D|\Ì”2652ç¸)zv +¾¸ñÊgpó‡tKXëCEÉáPK/JÁÓRrÇ€7jaœ+¥Æzw +ÀZÏû Š~…‚m ¶]:—Z8·š·9!…òViÑj-ZNÐŽò¶UÁô*høuVùîb?šŠË*lt²¨_ׇÊxQCÏ«§¬ äÎú϶:-XV7ÿŽôâü“ µˆ_Öà„+ç|û6Ñ6äê?º“T‘t¢"Í)Ô•ØUeM“gè6_kìäDÀ Ki˜Šjœš®9”Ñ(^‰À‰#뻨±SŸœ¤aP€hB9ñát sÁ^$ÒÔ†ÒôÓÀž¾¬5²odÅV GÙPÈf»[J¹*‘ôD `˜ÕCpvtÃÃ%#WWåˆ-Ãä+*ÝÉùÌIÂV®ÖO]FNãò”¬¢cnòZ#ÃIüú¬õ(Þ¶æââÊÜ»¢8³jšÁÖ‘Ðöåy²i&B`÷8`•[ Üh"NlÖÊ"^ðО愿˜5^>Ú%é[Ë^^ìPè³½©íÉÝ"¢ÂdÝâJ»CaåêµÊö¦/顉Ow‘{z­üB…â2¦|2˜ã7L;âósþÁZó*ç`ÜŠ0å0,½3½Ì½ V~§°.G«8¼É…iØÙ®¤¨Š’ ãZó:X ±%9åŽËå`Q"VEz¯»,}E­Åkk\®Iü6¾¥AIJi%¤SK¯Šrç[õ8 ‘àDó.wŸRwñ]ò~-íà•fk˜£é¢¯*{>´Ö¼LÛC2x¾Ò¶$Fd7s7¦W¥Ï¯µx§(‰Õ7‡®lLžåú°\Ù£Òkš=2¤»E‹~wÏŽwλ•ÕÓãW\ë‹Ôæ[ýá.ÊË·Á@¢ ÿ‹žíá`¤7ó¡XüƉÀéQö‹¤æñy; .Þ:\~¾dC ÐÝÞüâ~òš_ÙšålŠ•Ðê5Ó/i=®n£ÉnåÕéÎîAãø¬èk¼0 ÖHVÉý{ú“¡|ÙS¤³qR‚*:fI¿…žçÛwɵ¤e…œLK:„ÁD}¤¹jx·å:2ÔeßAÚlг#~‡½ NùŽ_Bj ge|¢¤2]ϼ ±Ðˆ4£ ™ëO4­éì´db`=¤¯{¤îèÐLj>èm(íÇ0ØC3xuþmpárŠ™!Ê#®’ž÷!/ +ñ"äÄ‘ž"‰£ð¢Ÿf·|Ùœ˜^–àŒo ­:v +úôôÏ*•7 ÊM•šŠS Iö5~‚$é'ð$Fè­B¿C‚I0£Á4/ˆºGìFhJÔŸ}ÆBžÃíÒç°#Ðáöö‚6ƒ0×Qyã£&Êý…v>üש€:¡ñú*ŽhTãÈ »ÄÎRÓ0è~ª™hÇp´NNCj&½+ü°W¡²¨GJ%dü—¨”*vˆHL'¦Óí–G"8#mÈ\À ‹o2Ä%ìé(Òˆ6|Ø»Èz¦w‘}ïü˜¨ø®Ÿ +5äu¬™jÊΑ$#5Ëÿ)Øy\Né‡a`+›@ ë:ÌdÍý1l$¢ w`þK³âYájaÍq»=Ò¿š/“ 9óõŠ9óÃ7 ÿÈ›†tˆÃ?8³á[âêÍ ÍKfRÊ٠ܥœÕ4÷sñϽ¶4ôÜÏ–Qk¯Å“øJÃ¥¡8&[ÌXÆl7ÿã½æÇGøc“˾°0“Ò"Δ׵&!EŽ…TÃ,ZçB#Í€ce]Ü:‘:G<ýA˜À GgoÄ: -‚˜2f•¤ú•|õ^1'¤ÚüD•‡SyrÓ°s+Ë—YJ¿¥Ýpæ|q®l·šxúþ©_Ùþ°ÉÓ}NÿÊö‡-À•ícè)SÇ'Ç?æañ*#Ó''‡táå¯íœ5NçŽÑ¬}áü`W:Ç^Ùvm³}ûæäôì¾mŠÚ¹«fíô¤Þxû¶²Þ¨Ÿí¿:,zj¼Ž=5Ü҃肔tÕ\¤3PKE’ã¾íôIò-ðq ‡ªJ×ȹç0ÿâÂ^ài¢4$­/!ß|ÈÌPùšáâEµ¸¨ÅËr#n5äüÄK—Ì ñ5PrMNb®©ñ5xHbBœó.]ÁÃ5…gä‡Ûò’ÂÚ”®)¯L«KJ/ÊŽµÅPóÓ-¯Èûñ¥öƒÆx{¶¸GZ’JB^ÍóÓ†lBS PFÅIŒ8\ TVÑÒž óKE)zDl'G|ÆW!2YPù=NÇñ¯˜fn Â‘Vù!/C§^²=œ[Á&pÒZ^á7 C©$ {1´uÑO4±¢³ŒK÷"M' ‰Å¤•#ãõCf‰ØæáÃr\)°à,ó=ˆ@2"™¹¤û¡1qüA/‹åeæ8tOüôGÚ3t™ s†@² èaaF0Çs¼ æ¨Ü19á1³d:B¥êº—…ìà7™E+ú~J~å ¿rƒ¦u-øMFñð¯ÿúü˹¥¦÷[ëyɵŒßªÉÉ7ÁËP(!F{ PoÄòçzc +ÙîI@ã‹Øe–0QÁ(?Ь°ÚÑÚy>Àõ®_”!¶¶pÂz¸$­‡ÛãýQ$…\:pü·8Ø,:A‹îløJáË…¯–_f'Î~m·TË‚†›0wc P’:•+*2=éX^?xùò丵7a—.žÖŸjíÌFÙÊ7• àÎfîǹλIcϘ&„3ù87j·1æ¼P[”zʨ?pa Ä”£Pª/p˜@#û›3;ÑïÓãôØ   ++®«â£ì­­GÔòômó $%„šÂqq:÷êÓ12tâó®ÿȸ5WeG‰EÃÁ—5Ö#¦žÈãH‹Ž")7†tUÂA¯ x#%‰£AšOÂqãQ)·áëTœËÇ¢¥Ì¯ZÈ©m9 R«qÀ íê¸úKk©ÿ¹ÞÍå:¾hH9j$lD±Ç8%W˦Ci'i;(øMJ³å,ÐX×G"¯$à2€+Èy\T7ˆ\Ùq%g‚ì™wgWc)—oÙh„ãɶˆF(Ç”5ËÛ¾+&H$êäkmëç¡LMMNÌq¹Cœ1æÈïkן!’j´ñ¨»‡5òy ؇˜ZKJ¿:U«]«Œlœ¿ª\ÝØßyÓ˜>lï.ïœÕ÷ïŸL7öNN-1_ny%‘þþY{Éݼ¤É®â¬;6Áès9¢ qhîMÄ-|ÑŠ‡‹ +)ý¼ùtjQ¡— Ú©|hý°(\¦Škg‘ºGÏöŸ|æ3¨E —”³ô`žIO¤ uÕoЕù $ +(¡¹µRÅ\ìQÚ|`Õ˜ îš’´“Èö…‹Œ%#º4â +QƳdÄ%¦Æçä¹Ú,× +ÆéwZ*ò}©chÐŒžœpÿ& ¢¼ 5?9Ñ'êù˜ã€c¸ç^”ô“ y-mTb¤¶M -EÆV âgPŒ¶RŸ‚ÙŠv–OæD„d>sR©ê@Ø#y6¦/•püVšM£^U`­>_K3#Âl wÕ’¦\1H fƒBOB°[4*°E˜É<0‚ŒÄ´Ô¡G…%–“t¯QE8Ô"ÕÛ¸©"1ITÛ‘ÃèŒã`ž8;Cš$(|ÇXÌäJbÜ;2\Tͭ˹ªŽW°d|pmÛü° A}$€fh®È ËÅ©}if+õ.Éaˆ?q—]Á ½€‹¹’œ6Ë5:8l•(%ØàC¨q‘/%¨ +õD ø¼0`±`¨W±Ç&¶0ÑüÀ0=Ëц:—]—Ú'ÜO„;[ÅhG%%šIb)&©è­ÐoÛ†®ø±\Ž[Úì°KÁ¢"ŸtJÁÅ›Ù*(‚yx–‰¥.§è©¾âÒ»•ÚÑÖRG‚xA$GApŽû rÉgÅ…ûB%¨¯\5g%½íµæù¢ž±ؤºÑÜIN*3q:(Ñ3as>„‹(fm k[ÏàHµÇ¨'ŒÇëq6TK lxqhkt#‡Í$ÌæBœÕhÒgXB’âNžˆqBS'¹i‡Ï]daV¸c€æ£-Ûã• ¹ã™Ø©cç +}iÍa›thKøÐ+È(ä×CÁ\îYêÛ¾8rQׄ——×Ìg:aå â€iŸË>6*ÎP–**Øni`ãI_'éÅ¡T(}Ìb{‚Ëf«iûHâÜ?d” 0AÒq¥°7WЪ³Õ'=¢à:ÀÙTš–µåKÊ—~P´yùÀ®²l¤‘p±[JMT¸´c¨¡pÒχ{ ò¥Ãz/ Q7máN/ig0‰@Ë8@Û ×–Bœ²è\ç@s³ßÀ’R­œod®n¸é-#£Vöì<×àfs|*ГbÆjIáx¨”–f³!ªx D4Ëd’…AÚ=]RpæXIïCêÄ?c( \›0F ª˜QePZ†d<+¥áâ +uU"®Ʌεv"J¯^»Hh_°`ø¯H[ÔV¬3F%¾ U |eUD[s\ÛSÙ(ïës]EÛEµcHáæ]±0ƒÐ›%ÆÉ6Äi39 ÈȨX"ÉAnúËÑy…³‡é‰RV߶“ab¶íß8ÅTƒ“izA*Q`…»/€Îjüì|nt˜K0Rç)ŠBÛ§$ù¿,wd/¨+Å/áªf¬*riJnÃALÐ81m–,YQ(ø‘0VÙDÅ$–åµ¥‡¤í¼ØPÄ‚ÈR½\#ãŸ^Å µ…l‘3ŸÊÁbnwÆì ŸúÇn$|Æ•Äâ‚–¡bUIqM6`Ø7äLE¸44MSÛzµ|G„rÏXw.ôjÆZÂ)ŠJñÚHÐVL3i,•þQ %¿ÁÀ¹(7DÒÌäC©:lu9ÛZ¨«(u¤]7¸Pq)Ttá ÎæRs?äK,‹ßíO3ÁqÝ\AA®®í:Ú*[÷‘‹j)#Ú<¡”´+ãÂ6R÷Œ{Á éç…EK¢Øª•,œA ¬¤k´bRö>R>"ÆXš¸/ê/ŸV\vFê‚aP‘3†áTâÓßüx-E ‘…ÍHèÙB¨Yl +Bã‹ÙÌL\»GÅ…£´°o.1‹AbE±Ï4Mz²´É‹”¨o¥|Ää–TxIäÙØ +´ß³f±J!#"š °›* „!lÛ΋í) ÁeÑ©4ÞÄÖ.!*%ýx¦è¦Ì’$Ô2.“àШC$$ÜÃ͵ÒâFªÒ Úé|Ü8Þj +-p'¹‘Ði¼Ä6’58ò8y΢“Ñ\MÖFl$(°‰ #'CÍÖÄâoXµ•x¥Ê]" @óÜúŠE?wOAá?‹‹&âzÉÚ–ÑÃ(¡8£\Z†H¸Ì9ŸžûöERŽ?‘ +ÌàTb[A¶ŠHæÞÚ—Öc–^Œ±Yü1$£ {¶§mkI|?ÕHÈHyx4m ¤^c(‚8Ž¥ÏDí5Ì;ÑMœàT;W·æ“¨¯¹_E$ÎûZjÆÛ=,¼*¦ +õÁD{¹í‡±=xžkÞÊ•ú|®‡×hõ +õÆÄ\ˆk)®KÅuŸ¸†:#V¤CV›pæ åÐQ4[s½ìÄV"áÕ^é]l¼sÿƒ ËDä³¢Œ€)0¶Œ‰Ô +`•UšSløÜöI˜¼Ôp‚E!j– :¡FW…H¬Òõ0n Ê¾¦ëGqì˜ND3Cµ`–:Ø($•W}Ói]qÁE×Ç-ˆÅAâKq8#'½mdn‹:JI;ªÒdF8Ç"‰C‹Æɯgž©í}¾ØD@n®Á ¹S×WæÐ.x‹²æ’ÖÒ[¡‚µÕb5ŠÑ>çNó*Š–Ç;FF°#&WÉ'dLDÇÁ ¤…ïp>Åb=5ðW +6¢nbh$6Ø«Á²"äó-Æ*ü\zÂ&F~z”ž-p1?©p‡ò¹Zž´ÓùI)hÔœ…&1/û¹H5×¢NHÉWm‡sQø=j +ãXjŽs}ïDJ‰[Ç’Ä:8 –{€%¶{1”¯¥.ëîÚnœèR( eåÙù_m‡2ã° W=ìõa8E®i3h†lä3úÒõ¹¢ms[AD)Áƒö¯‘¶Ænd‹`)Æù}ØÒb6ˆœ2ÀýªXmyc¹šØ)*4ÖÃf*Üš:=ZÀ ÖaPi´Ëãs¯ÏŠ/È|MÇGoŒUbHaŽmÅÎ~l_if¡m~Cf8ê(“шÂ(èƒZúÄA‘äï%?<@mÈç°d€îýùQñ™¯ÍšÕùB4Ki¸ aЖ³žÃyG0äý˜£ÌhHCWàÈ3­ßooÕ·ãc+CÆç^Ÿ_ùšŽ4M¯ +¤o 7QȾQda…žÇ­X ZFúd,0Û&­U‚Q‚RîØú* {~T¾Êm·“(„6,â;í"™¤‘4 ~B#†¬ƒ«M£lÙÐت&‰øMÉŒJ%ù¯é.ÄnpÂÛ ùø¦ÂŒ4nB0O3½ú›pÒHQÒ„ MD!z¸üðèBGÀéàT“>2#ŠrßÒ¡ù„cô|R|Ș࠽&"àtLÒoB.D¨IÕ%Ê68 ¿”÷ã‰d4MZh’B:“ý’D%0øÇ5©)\-‹ì(ÏÍD¤köþ“žFŠy¨„ŒøÇÆË8Tº*)ʧoWQî[:²ÈCyN*§ázŽ´.©tIÄú¬Ç6ÎÔòÑ$‘‘èyTº$Íè$÷ÍE*NòI`¥$lú¬·q|VE´`dá›ôõˆt4‹Fj.ŽœûH÷IYRrÝ Š1_‰Ú 2_[*“d>¾0z Š&aÿ}+•.Jp*)Ü/Ü9=6\N—Ǭ}Û,@±pb@œ)G_"ßÞ*Í)ÈÞuCw§¶îûVS½ÑpëˆüuHˆ‘g…ÂF”…»“Í­¶ï”‹h)ÂÜßíÏ›Bco`@Ŧê»É±r#.¦C 8"  õˆ +HÅÍ;(i.K(v ï«e—€sÜ@ÙèÅ!I1祊ä:)Å+KÊÍf-Èí…‰tá:®½Ï¯`·aÀ¬TÞqº€+õ±­PèEË:j×U@œÙçµ Üv°ï‡A(ñàØX|b·½•eƾP†¸%g‡‚*¼)„oDYmS¼Œw’ŸÏÆ#z‹GJæÄFˆ½Ó ÍŽÌ^Ð.°O,›áÆÀMPÀ-jÝ­)04î¨êÀ ›»pýi†Ø|/;6-wêPÛ7Hc€¤ú  ï¦ {+×ÍÌ^‡êZ^ᦀ…´)Ø/+*›ôù`{ x¾ÛRÃXT]óNrJ&á9J”–Iv£[hA↠GKŒGŒ±'ó’& ‚Ù™‚RÌtñ:nN§qy¡,»p¾+Çà»\Î@’È/A¼(]¤hûtóÃÜò¸wvgtÜdC*´‹É,U5[UÊÓ¤ I,~œt‘»¾\~U–<´O³f6v‹‹²äBˆØiNâÐÓå[ÌE~,^r059,TŒJÙëŒÝn>%HÎjrSEÈË"‰t©UìC‰dlp2 b­½Õ »²íÃ|áƒRdS^8âMÒብqA)žga˜øAž;„‰4iÆr—¡Á—–—†)ßæH´ /Å|2§ìuÌKyg"÷4?pü-'DmCä +æé(A;¶7ø‰TÇšX‘òŸ„>N"„æò,åægPæ M,SˆÛÑå´ n…¥ÚEæ«ëÊÖ¨ãvG# +¬dŒ´ìêQ"4±±C ätJSùÊu‘:áÑNš•nä»FÝÌd'HÒ^XéŠ2 ”EùIh‡pì,w³$Gð+|ËyEüòPât(èea‡ìX‡y^QR€˜$å•)L;iͳäYˆº áunðý30rª+b€H.#³ú0r<–+¥9 ›;KÚ0Ž;`Xq†‘Ê€¥s:Ó;MP6¥R×—&÷rz¹€XväDb’ÙèèId n¸PäöŠ… ÆO…‚žôcåvÁ>‹—Aædü&È!WÒ9TeçwîÖ +Iþùœ-ãPqS)‹Ç……vÁå G†'¡VeXä]äúg‰É)a¹í³‚+KLAUšöž›ÇçypÀ¢J@ÎpÌrud.QŽõÛè[^Dáš’$]ò¦¸É‚R©”Zá…‡Y±ä$^š8¡œÑMÑ9+19:’«X(;ƒ¦LN)'+“ÉØËß,6FìK—‚z å'§Eb&gu€[”¦Ÿ×^j©*J´¨ŒÒ©R°,¤6Í·fås.LjÊíÒ›ºf¥*œUÖ8ÛÙ¤z[ækuQ×ìx Ýò¾ÅØÔ)e:rª¦¨†( IˆòRÞês<eä&·D°)_CN (,A2J` +D즒Ç„ó/e£4uÙÁqkdqž9@:vÚw²u ‡æaö:G6:{¥€pº&û +Έ.Œ$¨J3å̈k+fæ•© é7¡»Ó=Ì­eÀ}ÓóKØdóÌÖr:!» ÞÈo‚²ÒÛÁó°0 +š¼•UšGë•qV¼dvå€Â®XsÈq4–¢Da“i² `âD°pÈж(€2¢µ ´¾ '2 *'™„ï¤S/‚œBŒ•"a®vpâFÉ‚‘;P½°$ ´³—v>E Z¥æŸèóqE96cÑvxMœÐÜžŠ§áø·FîLÖ —&(³(M`ºxœ!•_bqx¶ÂVsmn˜hL¹Må)(•aM”Tä vÚpìP"½Ð¸E ¢4ÙW;S:<ôXò‚Â$à^qW%Îט°Y’,(kn¤À”ÉqÞç„IX†|2+coÄùÍÌÀm¿o3ÄråŒôîÀ+% »ãr”îӹ׊9ÎøÔ4‡pbMfá笷 :eA™5iÓµsk.púÎìF¤csÛ$áK}›2sÏbŸPföoXØäcÁ–j!hA‡ú!H +ð8µ8 C´ÄÔHd ð)‰âRÄ2¢€CÊ'%b²0Žã–šŽ" [<¬^lUšj1ƤÊUð5ðS óÕ˜.j1´[³燃$C ªœ ˜ !®S[’†î!/ 4„ˆöh°àt±ɪ$rA>Ò–‚œ0æ“Sùë<76g7‰óÞ Ã1O¤§ÈpµS¯ÙB ’4Œ˜¥‘Ä 00éJ ¾›'ŸÁ¤Nt¡!m8‘_Ëh›Öq”4—¨ ´áD¼"’¨Té`·> i8ÑY táD¬š'«æ¥¦…ı(Ý—$)\ÚÅuÑÄÀº,1©4šˆ¦ÍZ6ËEJdp.š¡nÇÛŒ&"U4’W¸h¢³5²ÑÄ *½±AÓíK³À$ '¢áŠ]r'tA>â[¤µç¯“†²ü +‰ö!:aºPDÙOì"­äi. ˜‚âL@1tE~Clï䥌3ñD{Ð@O´Á\Ù8¡‡H +rÞ$™ôîP9vÎ茾 Ä…¡~B‹âaønOYM( ':í-ˆÓpb`í€tâƒH™¦âÇj?`6š؃X"Ï‘‚a+ÁRÄãÂubzãiIj‡vÙÒhbà¶9N£‰0½Yß/AÒhb袉A5HW׸xXA¼ºq†!%ö:Ïi}8ÚÉ Õän¬s”F{ªš€^ª‘óA¥iî`Í fHÆ—7£‰|ä}8ˆÒ(¡±æ•9’áîº rO³¾.ccæ¹¹"èÈ® '›dO NÌ€šáʸp¢±Ó IÉƪLºI:^ñIã[íÞXO)@6N˜áÙx"÷ƒÌ^p3o¥Œ›“»EyM‡XàEŽj÷4ßYyjã‰$ÛEƒÓE³ñ)aÀ`X™ø”’ÅN2ñ)9ȳvJ(R ”ÑUíá¿$ŸòØïÆC—ãó¡ߊcþnaS_˜¶É#¹›HtZ³æ÷v$n‡Qjø}RöN'uœÖœ¤ž° Ìú ñ¨È±zåÞùVDÄÍ (ËÁR£H‡@¬#S[KÕìbl; ›:åŒòΉêj!€’à€2¾cÆƾYª‡oßò(ç F‹U_x¥õg!éNg€.@PbYCä6‹m3(õek»sZŸ·¶Z_œúŵôq.B奼§¢â£íÙ{qÄÆKr/ðå(º ¤iiå®KcTr†§åbT)(£jŒ{óúÐTlç™õµ)Çe›1*Õdnδ5ãŒ[]ËñÏüæû6±‡µš¦±ÉR“5ÕŒõúìªo´A*Ä'Yd ò›:A3H…†º¡½N¹Ô8‘} ri]´­³$3o^‡ó,…§ñi`y«Ÿ¾Õ)1ÍÁI²O!´Sê7nªöÎØ)]ÙE‘B2…åã¢Õ%`ª}¤bõÓ8ëJuâ7 SÁ?o,©¤ù!¡ÃO¦Ê‚2•›˜'L4‡Ÿah'‘Åc.ü“Ç÷(Åã”*$%GQ5r$ÈyÐìÍA†TÒ8—sÈÓv`5õŒì¶¾Iž—‹S9H6N•Âœj'™mÖ„7ZËj¢\å!KPŸ¹Ìíbª:³¥öVcnóá—кÀŒƒ¦\Ë•˜{G%Žr2+eE ¥¶2"%]ó$§j‚²š˜º•“OÍ8•âÛœHt,2¢Óúù2â5°»”“Í)é$¹Ü>0—½Ù–úÊ‹)DQP"VbsêÖÌ+‘vfs&{$¯Æ4Ué‘I„´ñi®œ3EÊM`РQr¦’N +z‰VÅ¥‹<”cIéM•}9‡*_«¥)r•V9]e ´6]DŸz^¬E*É¥î–Ì×À¨ãvÇEä +CùaFiuÊ°e”V¤‚L½ |è f84âšPeXI({]]•^Þ¥„ÁÅ€RPj©q½³(ÀE'¶ÂÍ>û“ùi⃔݋r$ês(™‡ìH™ ÉÄÁ7AÆЦ DÒ”r\†3ü(Ç‹|9âex–-4e8›$98Pjë&ÌÍžMaÚ²|âMèC‰v0@x’²r_YȱüDnÌHv!”Š’2è”Ö@*ï5³CCkV®I™HSùGòÚ^—JIŸËÜ;C/v®8`šãÀçW³7Çœ¸•{…$4ñP2‘3öa˜&¯H,ž—KqIA¹dè•1›Î€¤áJI¬à%oÆ+cv£å¶ÆR\vÿ¸ÂC +r~–u…½_Žå‰¤ž‘ær«ÐA€NmE #ovš¡±¹º~˜Q[qÔ8°×YEÓØš: ò¬Ï@)“Ù[ùÐsáºØâpóiâdá·úM†T´ÉŽN|]<‡ÐÎAü8n®Î“Ç1ìܪଳW\?¸!¼¤ ä”ÌŽ˜ªZüvz+¼~ad÷·é†K€ñ@WÓ$=§¸fA:ãEt@‡|ÆéΠ(ÞêÇTÆ蔟CyõˆLŽ0pÚF«UÁƒc2¤f EÝæÍMjI_‘Hõ¦,$OÛç¥N§D¸`˜j®PvšÀЮºD;3ŠÆÁNç##`'9ªrž©ÜZ9–¥*šÒöKy„;6r®¿dßB†¿Ó{U䤆bEjSZ˜ª´`lÊ”tÑ›‚' JTh™±ùq‡*‰ÊI茻Njä5ŧ±ùT!klÚYNB§Ô“•ÐÆžÉÝ,qŒ¼3NB77[²§2JÊÉ9zÛÙW\˜^—©å½sJgöó(ëLË ‚oK…d^­ ˆ…‚£–5ý:‰ã©_§ ÉøuR ‹Ödæ…²4™—rN«óθÁyœÁ&S°¼+ãœQ^Fò¥A”¹.50<¿ps Ÿ~þ) e‡bË–4‡ìÛ|ÆÌÄ2 \öq +t ÚéÚYÜN&e“½åhZfoÜA©ÜFÒb¶žŽ57aìÈ´°Ä¢-c¦3M¥RÈ^dR'Etˆµji‘?õøöºôxX“@çT½j‚L33© Œ]b±ør +­ØÈÅWI}Û¬‚,‘Æœí±ÿ'GáÍEÉÚ¦! +K–€Ê<çý8Gõ¾Ñ9ª÷¥®y–ê}+Ýy².ᤠJ% LJbf–ØnËm…o}¬™-s¦ÌÆúMUŠA)Ap©B1ê9ïaORª“#Y '›ç^,*Enx\*¹8 mù~s¶:TQaMš ÌÚ5)ÍZ½ ;»§ùŽïûß.;r;›N¡¹ÿ¶k¡$²‘C +¤~ È¥µ2„Ç?•TŽWJ ƒ‡ç¥l6òÜ,$žãÞ|Ê‚Ò5ÉSFîæ8óÎæ>dÆæö A†ŸÐNÉ¢SföY”ðÙ…¸s˜åe9é–úlPÉuItäAÃA +/CR7#BKv¾ç53åAäš M¬— p7dšåìØ)vÐI]4~ó€½ß<`¯š§ê3_ýnVèx ÝÍâѦœÅ£M)‹G›R6-²x´)fñhSÊâѦEOþ:Ï-ŸÅ£M‹,mJY<Ú”²x´i‘Å£M1‹G›bl¾¤™#Y<ºYÀeçhSÊâѦE6¥,mŠYÅ‹GÑ'¼x}‹Gѧxñ(ú„¢Oxñ(ú/EŸðâQô /EŸêŃ’”¢M±𤧊ã6öÛ | €«öŠ6a'@mΖLØ #Â×YÐN hSìP° =úí¼Èg'à…>;¯1µø† v‚ÀT¨j/L™ëÿ„¢I;'¡³€Àsn'pÃí> R;/í¼¬í/C²íÜ‚å³ M~tìŠ>a'àF2Ÿ +v8¸v´ÃíP°à†ôÛ  hÂN€ò›ßN€»Êo'àEu?P&íÁBzöèvÖo'Àéí¸ ¿ë·ø€"숹À‡ +n'PÆíb¹€ vM0;p« ;A "•û‚…¦mfÒˆ>a',í¾ÕRý߯hÂN€0öÛ ~;×J|vfÝ%í‚¢vñpM9Ph2q7h'†å:;N/`'pO1OÿèI,š°°ó: Ú |xæ´XU¼ÃwÒN jà’m°pÂNÀË\; r ‘„UÆl ˜’‘µ䈔0Â$‘ž ¢íýÑÀç?:‘'Ú ˆ hà©¥hP§—íº†Ù‹C#{qh„Ø3¢bþýÌ¿sšß˯sïA¸uÏh Óœ)XȬzŠj4ªk4¡Èb-]£Îس¬r£ÄŸFQ@¥/bi½dE¼%ú"úêQOìŒÝ5QOƒ‹ºÊºcF‰÷ÅÀæ7Ä^‘k4 +5ÝmI#¿r£Óe¡Ä3pýÉ&¯'Ó"ïâÖ+²=y…Ìh+@ÝMåFÅ’UÁhÀoÆdÕ5Ð4¢´È½òÒÙ<›àJÑÕhpvYu:s¬E¹F(¤-¹ÍÀµê¨®Í@§¹š°ÈµèÌSÜq¼Ð;PYum¤È6hožÍ@g¡Ð2ù0‡8·ü}¹*Ø Ü«!± þƒCÐÈ G+ÌÃ5œ=«®Æm°wë7f2p‹Ád r“á¾ôä6ƒ¾ø„0ë®ÉÀó(ÂÁ5x÷þ®-À`žû²â»²¶M +}Á´`ÓÝ*Ø<è1E®Énlé~rm:‹Hó@1œšý(Ø t¦a ¾'ùão×f ³§÷PäÚ t&CA!3€—uJp@Ж幄O]b„züøszŠeÁV<£Îð¬¸6eŒ‚"u¢Äµ…Üf}1è2“ŒÈæ%ªÑ,5¡âš ô­${ P§\I°Ó¥žP®½@g™}0Y&߶&…±`.àÉù Pâ3£±e {“0 zAÌßÂb=L­Š½1sÁ,b¾uBÆFÜ\`¤, ßÓØ„"Ï\àªÜ\`0Ëì=f.0¸©RîÈZjÛÝÜ\`pt¨®@` ž¹€€µB=ð?±(Ã3\–Mãlá1$¹TO-j²êš tžpAuÍÁ£Ôg.И7¬øü'Y¶MÕç?)Q`«B F槲W"²gAòGÁ×ôxt•hš„SŒ+gÓ‡ãþ t,Ѻ?(•\} Ée1( #,ûƒÒ±×õbP:÷ÄñË{E‚c±WèÆ¥c9w…¸tÏ“%Æ¥3,vXQ“®KaœÞ LGÝïy_?£?1.&š¡d/>IàÌS(¤¶>Y nf0®èÙ I;W6™â-˜*¸kšgÏðJD —zéØá#Ä¥3$‰NC Lgȼ¥ûJI|bx9~yãs¡”tC¨çºß³CÌs¡”u.”2‡‘èBI_æÈbü/ú@H£„¹E¢ ¥WÈ](©º(+‚ %õù—Ÿ ¥Ì9­çB)yεf   ¬øì>6£"öm¶EJÇMöG§ãg‰N±þu×$ìÊbt:–{]Œ(GÓ ËbÔ9M²…"×pÎ` Ô3½ÞhtVÙŸÎb¼ÙÑË=Y PÇÝ+}ê,.t‰P¡Qdä@<2]™,s…L:ãÓ1NáPÇÍ ÂåÁ ”¾Gñ $åz¤ÇÕ}@9Ç÷²¡ëj€à —ÝmA̾=åÞúü'e!ÒóŸô¶Šë?‰j…wë.Q æ~®¤þ“n‘è?éráŽÃZðŸ”ÙñéFeS±}[ +.#Uÿ–Ÿ_Õ¬©¤©A. ŽÅšàǺw¼ù +MvÀy”²ÌRˆñc€¥œ÷:fíOìªàA鉄n¡ëA)ë’ÿÀÓ©tç?ušZ<@áØbê¶ë)®r-l)º{üAêè;V¡1Dý“ÙYî=£`àô½¶PXrDOª *…(zÐxó¾å…Ÿ¥â‰ÉîňÂnmZPX'b@Ôæä!\Œ`‚C‘AgãÚÓ¼ábÄ-ô.FÜμ‹wPï6B˜¿µP˜Û´°#×·›ùŠo;ó+ßñ*³ðCÂňl0/^ŒÐl‡Â”UÌfæ[˜P$J^!$ÜΡVÃUá—¥µöEnÆz>Qšå~õËá.TD«µÍ ?¾B…™a<[6“)l|…ë†âÎW çs#2®–³½"Á˜íº›ÏÒ™¼îY³-Ψc¶esᜡLa×µbšöÖ-â{J мñG‚ò®F”™°B  MfÍôÆe)HÅÙ±3À¿ +–«V\,Mà‰W$€Î+tùÜ…ˆ°xo®çÞ€˜¦ë®ÁC?2æ#(vMÐ}T(t•a·C~¢¹ƒJ2·Èº7#`:àk ¢’Ì.›b‹\ˆˆ… r^gxÝ1E4pŠp‘å‰.JejV}÷"îÊEzpEFï^D(9™ïpsïE ž–QšÌP‘,´ÏëŠb@8_¢âc*5^’%ØäÐ0À>6]‡×(¦)an5žA‰=z…’`P49ð*ÖK#àçç— à!ÁåØ=Càgò{Æwýc³ý^Ž•”ã•Ø¥™ñ†Z•ÝRWïYÎ-¥eú›Få.pÈ–$š2ÚU]è“rIÐ\d ž"ÑäÅj  ²s Ùi©d»îŽ²EóËJ†ëžHmŨ.¦çHÏtˆÎJ£Hc¤õNb®VÅfj£Š)YÝ2.G¢uÌ-ußUÊz°½¥êÁaè[K 5Ù{䈬ü…;0@Æn\ŸëÊJüpà…ÀxoXiXŠ øá1zO¦MÁ ⓾]vË8PU +ÒƒA—7¥"3‚œ×› ñ¶—ÒIR+;°>7u æ•Á¸‘ºçH~Œ/© +ð¢1(%x舄ˆ–÷í£ðƒ¤ã®ZLÑ&cX¯¥JËh Kdü1©ÕE@>g;–fîcE•@±ªÊžï Ññ6PÈÒ©{3›–aùæ­²§¾°>ÐŽ¸íŠHTæ62jJé jb‘T·ú~ÑÞve×.m3Êð0©rÑ×Ã8دÆ9]°¼c>šre +‘¦Tîƒ,¶e¾ÙªüZX¤nô)˜ /, •¼­â•x‹÷ÊA ìôl44n_iI¦…b¿k°§Ç5]“8WFÛ§„Ÿ]A ½Ï$ÌH¦é~æ +1Ñð஌и,ÿ7ŠÃjèç:Y`I¹uçëf‰XË XˆE®+æëf_Ä4ß䣗Á»Lû‘u`ª6¤®$¨ÔÜ|à2d°‡r¢â(´&ᛃ0ß üŽ?cø™£óD¶3`&S˜ÉD&`)ÏQ¬³´ üËÒuÿ_¬þUGe‚Kø‰óVþ¤û’º ¸Õ…¿TÞ§*\= +íåÐÄ8¤zŽ(0¤ÜðfM·°É0F{d6SS¤ô|á+uÃ8p§÷IŽ»R¶-Ͻ=Dµ(¡:ýÉVŠ{ÕÝ¿x%ü‹°}íµÐÄ8l¥×.R¨Vù¿B ¿úûo#½"eâ_b(.ùðÑ:Â’EDH"Ègäp´©‹ÿ΂Ÿ|Î!—¹Dôîêc5Ir,kôn*ˆÄë’ T iJc2º@‡S†‡ +>2vçÄèÒ,Äð P¢ùñË"“‘5ÛbI0By°š@°ªª3•<ä§. P]Äô”ÆdtP¦ |tæÎýogTÿ³äò÷ò­ÿâü¬L`ØnG^$/~6ƈÉ`› nU~˜2 +ðQ«ÂjÄP·Vs¢P<`' yñ~‘ƒ³S—)QÒÕUK¬‰­©ŠÍ¨p¢T<”§5‘§1 î™=P²5Yî!B/;NdUg-t¸+Ô£@©x¼Ok¢Mc +Œ¾àùcßü3öÍwìÒ?´Kÿ¨Œ>Ü3 (8QN@Adú)#‰[oB^ +nÔé{B›l汩{‰Jvo²ãà†±Å'2Ñp3™ +".N…Ý”ÝÏå’)›|æŸAS÷Ä´5ô;oBŽ›ÒpÊŸJ‰TFœØS†êŒ=8eQÁ†ÓvûT¸}õçÇÎúg_Cû±ÿ´}ü5Å™˜EO“§éÌÃôr!ú¼žŒò­:Œ_üÚ E²mÂÍ‘fë’$xAx9Y–À±Î²ME2Én ™&®`ý©¶Oª£AëÒ2ËÈj©:9ƒ®]n·êN¹^m·º¯{ƒVãÈù5Z#ÕôÐúAwä«,¡òkß¡IÿÙ^¯5õRë§]rM§>*ÿÚ©õÚ…nµÖv>x·²P›÷K«ì[ gèÖ +^ø¥ßŒ¼a7ÉÿìP,º¹^ˆ’ŠÒs¡Û câoí9KæÛe%±+2£Þ DÖØqâÅòÿ›ŸÆ®3Tèu3ão~%?Éï¤ï—B÷R¨A6ÝÍHÛì)¤eÀ͵/Kôõ“å&ËÐYÈ$ R2Çy¼æ­r D±Ç#¤ÌÖØS!z+IJTê&-·Ð$Éckj¸5,šX5”bÂå a‚)QvlL#/Óê’-Qu˜ 6’$dåÃËgÀPþdF&¾±–±–ba2î½:6ðÕ 6ƒ'_‡ÊlãY¥T6ã^{ô©ŒÁ¹ä ÃÞ”ãxšn +fNBd’x ºà¾Ò€äg +´ÁU 4™LÄ a@5¨cÊôq+<ûµàŠ”`‚çÜ‚N–`S§zÀ#¯ÿMb$Y!xI÷¡¦M»Ü‚‚I™xõK˜©‚¸TS’A½qøÆÒ¨_) ã¥éX‹=È&@Á0Ñ{NÀcÓWRX¢ØˆÉÞ„¶[à°³0|©ŽÉ©Áy@"ðáE¸9ÈÀëÀ$kÄ×7¤DÇÀVÆ’}ãÙndžÅë|™½ò 0 É¸wjÒ´(æ´l\ ñ#Ã;@¸Ú4á鶃÷?ðäË„[z¶ºÏ ‚UQÙ[bô20%v>AÊ@ä¼G-Ó$Ç»wŸšÎòr‘9`Nl̳ˆ¶¨s4ä6T€*,2]8×sÐȨî,ø@^S˜˜ŽAçèà-ÇÇï¦Eß¡i<é˜ ŒLìÅ?=˜3M­ gÂ+U(²15DX&»©°9C¡)!¯¡~²r 0ÔeKÈÌ žpþ€—¹ðÁÄ$Ù6Á5¾c6t–y +Z“ù*m UÉ + |PÇ6²=dœÍ°i83ÈôŽï‰¡ ‘DBº•¢cX¶,“ºÅB¦‘ña/ë<áù iÈo[J7ày-œª:° ÌŽ­Q¸Á¶LÙ©øŽ”=`pj7Z¤Õ›´d«X `*X”­)´:»Bºfœ™Â^”@väü…'¸'NM^©Ó&¸ÒYHÝ Ù¶uHl@z€EÁÒ¤N§:øNè¤O3eâU€®Ð¤+ÊÈt™úÂ`úˆD—àÑôJ]¹ a£$ÂLCR¼ÐÈšB38ðF€¦ƒKšä@³á¸0 +l.t`#Ýk +áÓ¦ÆBù”iªŠdgÚ˜ä‚â¨Hf*}Ó pÕáUÉýì•ÈàNnhôÍ76¢ÎìƥĎ黜õ|ôÌ6ì ÌrDÃÙ4¸9®Ht£÷K,÷›Îßâ’šmIçùÖU™F`Þ§²Ðú +¤Ó…Nu ™-±1KSp<ÜŸó­"<£O‘ýéd¦ð!L¾è‡¨ ,À|1àù‡8%Œ’†… .Ç úÅÇkð˜Ì"u&B¨àøJM¦xˆü+I‘ÉÓ=À£P"ËaÄx“º;êðZÅTðİЫ ŠT mÓ·~:x a¼wHPE8㤉k„(¦º”(åÜp`àpÈa®NdBI ž?Ñ~h´)M¥èÂüd˜B |=ÉNÕLúš™ô}&A¢&bœ«av4=œBÓiVuhbÓÈ$€q`RàætãA>Â'IÀsØÓ'€ÐY,YÒõÛ%ÐÒD¶xöéåÅ:Fˆ@Þ`ê²1Ù‚3 +d"8"€è$Ô +€Kc6gxFEÄ ò[µP RéÃ6Â60µ´![ Ä0¥Ü6hNÂyˆž£ÒÝ{öµ.Ñ&à…(‘"½aƒ>€ÉøDšQ± €J$4’èÑc¢DEy2 +‰pßCâ5á¢1 àóW8‡A”ÿ˜ +{“E +tˆ¯gbÖ2$f0!-nS“f}3!' Œ„$L“Ðâq¸­ ëå£xáa/ˆg…V61&-H¢ñVH# ®8ÀÍŒtLi:Y +#i«ŒB&¼CÝú50…N!&Œ” ⬠a61ËŸFé»Uq?X¦Æ ¼½®Ò$5Ài¶P4~š*nKب6}N%˜`”pìàÍ4ç„E»25š…•nn ƒ¼Ùy¿9vÜ6䜣ŠY|ÈÖ…SAb™oÀ%ÑÄhðŒí£¶-¤6,!¿)16¥ô•“)yy• ¿éS£òŸªQaÉ€ÐìØ«A#S,ÐÎdàåÐF¥aà –0x¦Å •ž I¢%cZU¨aê¬ Fu2Ø»J•åÃÖ!‘â­†3Q`¬ÈA\qT0ÑPƒT¼WGVf €1\4ƒ*C ФDÈkG#4æl ô(þÙ¹;yˆA`£%à Ð"SƘš2Uà,¤¡zl¢+å¤"¡Ë®!AU ‡O ¦‰qÀáè@Ýð<žÑœP&ŽH¤â<Š¦ÂroÁI¦ÒcÞ°i¸'K¢A )ìQnAAÂSQEáýNáÄ3Q§ÙcUþ€º® Îé&r@)N†è&‹MØa j 'dB|§Fø¬…;ù”ŒûÍ„Ä*ÖA%v °Xf  Á QX@jGéˆÂ,‘ÉÊðM9þÆ09p0Â1¿‰†„ Ñh<&Èvˆ¤Ä!¿ x,Æ„°5WÀ² R<¨)øÎžÈ ;2¸£®mW¡b–Df£ÎÉ4K‹ÅˆVØÁ +J?¼{ŽãŽA§‘€ÚÁu„"™žøÇ „wS?pÐXÀQ]w˜B‰Ù“@eN_:¸5[T,iY‡g« ÔÒÛeèV8ë2‹ IT<Õ%žTŒp&ÍÂóÏE¨Iº¸ / ™¥ +[UÇçÜ* - ˜AXuƒT@>û0˜QšÀàÃFpÑx´iŤr 5Ñä ­ AŽÀ¶€» èH¶ÙÛàR¨d+Oõ%ÓàŠÌž2«º ˆ¨ÄDo:U†—4¨&Û" r°xBGp WÜä/6Ur ²Í1©ƒS¡ÆÈÀ¨eÊ`†A®+¡RBZ¨ì+€…ˆñ!Y¦ §b£÷¼‚Ó’$@«Í2̪6 ŒG?§™ðIˆVAQ¨ádj2eš¨˜­FÃãWc’ÐjÊfù“MH£‡ @±ŠÖAæ­`ª)Z€dd+vk)ÈPe 85*„ÁÓdšéŒIVHáo‘€ÙA&Á&EH´h0hÜAM¥§Ø»$Œ›ÆÖ”CsÍç<@ȤhÁ-êªÍ2_ò­EÍu4R!%6íô© ŠWI²%KT(šÙ,X?©$«g˜õ sT-Œ íf›‡ÁD$*¾°¨ç  ZT(B²$„JCwéd_P¦²´Å“ƒìž-m1í€òu6è} )Ù,”<}†@xS¾OÀv ùelŠ‘б%Óc› 'c Kof¤Øãc›* ¦ŸÄGñ&!dÄÉhÙb2leLgŒï&èVQhdõ¤ ¾oQ“U|hx -F|3°0¹® 67 S®‚NÁ„<þø„uÜ°f`Ï¥ìŠÚ#eƒF[§ò gP%J¨@¶Xd9®g%e“îl˜¤Â%é<‡·ÄRoÈ€-‰êt6 qI„XH¨ËbÎihÒÀÝE£,Zì•=84ZŠ7œÎãé¤i:ˆ\&-1ɽa(ÑÒI:ÒéhºÊ„Ù¦舅&µ™ âGH ÁTS£!¯@—-ºÎë4=ÔAá’¦¡öM“Šl€9ME;ŒÍžžJ á0ÀrƒoiT¼?£:‘ÌÂôZ‘–A)R° ã*ÔlD“[Ø\þƒ2U&NEZµév#½Ó·ôD³ixž†PA EËh|#0…ápÀªABÓ;šoàŽGAKZ­ñ:PJwu64´,ª4Ÿ=RÍd\ +Á”.SK(Ë8aC,Å>£EئÍÄà˜œt%zB2ùÚ§„3m½4Ô¼Êts"ÒĨŠD™°7\”Bøhg²÷„@RÀ‚¤‚.&Éžd b ƒBWLj·¤Š °1 9)e†©²˜- ~˜šÂ€ic &Ðê…wÀuÙ9Ìž aS¥È³YâW‚B¢Ëb<´~ þW@ `,Tè>7t–×à÷„è,Ìvl`Pw…šÝè5‹7MBÒôÐŒ,ÑZ4m(‘8lÌáÁ­dÁ²JFоéÔm|5+ã¢6‰Q»=-l|¬²Ç{IÔ^ µ¨®•Pç.$Jš`G¢vAùÁ³Dåq:e&g¹ìÈÂÜ#Ü.EŸÆBž[”8U‹‡ºJÑ´§¬H NQÊ«1Û,jóÄ[ L'€…’±äÒ2¾Ü“‘ó£KÂø3×ù%µlpã:X„1,8˜¯É¹Òí ¦ÌܶÁM,®Íð"Lgš¢Ìc@èW²F…Cy0Êpé†&Âw@ 7O¦HÄ)j`ã:A¨ƒHŒpÝÑe@^ÀŒEì€S€SHÒ9È®,sžOëÉÀO8 ÏWeÐ$³©,÷´L…P<¢LÊ#àzú4-Ý`BuÆo¶Ð6*£Ú¬ª4;64±é]|¡–Y‹Þí‘îu©˜4Ø?ÆAÒéc,RAF—i€wd} +ßÏ +ËŒ!À@¡¦…‡µƒº`§Ùœ&öñÙcˆ?™¦’ F¡DɇJ 6„­†ß’lzâÌÐN9¼µDÁ…"Bû9Èe²*Ü›©p±B9(Ù1¨Øª +½L)7š +w-&½~C¹ +, +;°u„¤*,E« —PDCÓ@ë‚ùÂ}7AšaÓ“©¹W£—°ô¢‡¶hÀn÷z6*ìbÅ@½%°";“œ¨6•o€Ö cú>Ìw„Ü:uvÕ©ÒxȽRÆK2®JAì° jVÑl€Éf$j¥¦,Rg@9Q,‰Î'6KärCbÙ¯á¨ejH5Y 8" ™`>"¼‚Oç ¶]wІÇâx©Âbó!«{ªF÷8Ȃǘ`–R4›²U¸\6à†.!Œ†¡ÐWÁpРh“1ñÊbR? VŒa˜Ádcr¥£Wþe€ö@q(ã…8ØíTÕšAm-)&ãAŒUÒÀ:G~Ò{m¸¸D;ŠDý`7`†´Òhg6ØB3 +ÝRüù½gŸ8:xïijôº]Á+Þb©ò[r8ŸÁö7ëª ö°Ùpnc˜Èh!Ž[n T<%K’ž[!YcWƒòe0í9Âí?˜U‘ÔXš@´˜ ᢠJCŽ«EËÛ`—AÙ.(¨ª«±p±¦5þ]‘ɲÁæŠ 8‹h¸WA“„6¸¶ðFuGƒE£î\w£‚ j¤„H8 nAdz© 7K*µ’¢£‰jÐ{#ƒEñUÁÐ¥b [>MÎSÝvµJgb€¡žàFÕ)Œà‡¨`ýCÞ Â"験€ˆfžU–h ¤™Ì]…TÙDÖ1O4Й9[¡Âf$'‹&-2iÐ+!bš·ðÀC5—^ïÑî¥ üÙÂQÏŸu…/¯ÏÀF©ã*!’²†à±\DƒYÁ¤`Rh° ™ˆ>'¦³+š'•þ`Dƒ;š$ŠaˆÌ…–4Bb„ˆ°à'†WA@6±¾A VÑE› ÂƒG,„Î&; @ã eSÙläÜ-GeQ¸MUéýÝpL’ˆC6]«<µ|zâ‰l¢—”`8nø +¯ÊÎD/I£øÄKgðèÀ ,ZÌTLhâo`þ! ù¢ + &²p/`Ã.u× S ãªQs¹ÈQa‘ù1.Ž‚P@¿E¢7ƒØd#ø üB£w}À¹Ñ‚ ,AðaxÔ|ô¡UQ´Qá6”L /Ña\ˆ÷F~cü|ä$Í£v‘ô04?FµÒ³)!€ÄHN;ha¢yR¡5ˆ‡ªE¦j0´v°™ÈhåWL̆8é±#jŠE¥€R˜% ”õ@5…SMÖ© J/`AÖéÁ >¶A­¾6å‰2Þ”†ðÚžk˜4~·bƒyUEB׸e5˜&D&‚!™š#Tfb‘ÙY@«+‚FÆm€2k'#¡Df£¡, z€–ƒ¡dÐÓMAæÅÝ,4ðÅج¼óTˆ ýÒØÄÛ;¼¶¢ŠŒ§–]”†÷—©Ù_¹ZÈ4êûÒ¡D5@M¥jÚ£p›S‹¯Ì"sNê=Šë¸TªÑ„’ŸÆ"c¢¯Q׉ÚìæX†.R˜˜.sC ‹Ä®@¨?!NçELª‡»;°Y»æQÐà~ƒœ‡x\AÏ@hc³¸öOŠT3H¶¨YPŸ+ü4fÆQ*âp«ìX¸ºƒ[||97p]k0ªF³¥Žæ'WÊD÷p8zdnØ·i>Ò‰K›†Ì‘Ê™Êej0s±QÅÌ.VÈÆsXÙ6 (ã1MÂãkÀeáÒî T¬€òL°{¼º²hx-¾Á: +s`"%&ðAømQ&b'&ÓÂA¸HÏd´2¡0™­C˜ê‚ÌUÆ+#†½t}w©#+WKp)ôA’I£QÅ+µ6a´I8óMf™Eg<”$ég¨&WB*6ì@5=;©o$z‹K…WTàr¤ƒEüÞše”1D>úaÙÂt 0ÞéÔ< 7—P„Î1˜¤!‰]hÊÑx9S]¼%Ï Z +Õ{~oÜm„†oÕ¾êô¢ôŒà0†(ëðdB“4°‚Ö«âëB’E_u3¬) ŠüÓÜÅ¥¿Ë]\‡ŸÏ§½niÐêŽÈê’IZŒNä⇅Ó>|±è—R{Lþ=«½“,Ä2^Í eãá[è¤Ú­¾:ƒÐÙ á âó¿…èÇ\µÝn½ªý·VÕ¬Õ¯‡ôP” +]¬OÖ‡’ 1y~ e5ÔÇ.Hƒ³ŸªíoÖ§}«bsêÛÕ¯OHõ¬K0õæ«—Ã.Þªƒz¯Ú%CE‡Ôpóå[?Bp+a ZA¬vÒëó*ŒB¦õåt[ÚEÙ©vÖã¡E5¡ ¢ÿd‚™˜/! 2aò¯ÂÔæþQUú‘'pж¨:ãþÉ N:ì·«¿ÒŸñ”!0y&îä”õç`/ûlÛ!jÝ×Q7—ì±3om„V§?h…ØŸâ:¼x=̘ÿ_Jˆ@ ,\Õñrè~rºgâ†ð t6ûÑoFøì¾±­¢ñÆC–C”O‹Î4i{‡ ÿ7 /‹³YÈî¾;€¿„ù—ÒÈ †e)Îø?Ãÿ^°hÊŸ–ïÄ'¿ˆßµÖ ¿8õ1Ì?`ÛiÒ_H6õÿ ðWj‘¾!µœGDl(35Þ6 ¹Š–"<µMx mH(¬Jã¢ÑHؽA(PeÓÐLI‡w{pCaüC“é„ ÛÄ\}ªX$“}Bà ôß$qFõC*ú«ÏLã‡XôƒH"áºVöm:½ØqAó¨làÿtzÑÿ +)â¿P`Tärô4Òÿ!Fÿa1:ëüä´÷š£• ýwA¿L§ÿ±ðö;T„¹Kø[¬Ÿÿ^ž¡Ó€‡å`æ“ÑÍGRlC·lð³G0pSû“Y,U»Šãß~û5tR~Ì×¹°U fª\ü.ÎS°æ™J(vAà0~i‹)‰p•5 +´"Ô› ___„ÉáÐkGtÚ­Nœr ôŒyïŽúÿõ'¨®1eC£áÏ=Hÿ×ÎÔü ×•ßªÞÏ?ÕÛ¡úOÚß@ÿK¹³‚çqg¨HëÍäÎþÝðµcŠñÞúpP‡„»Ë~µ5€tåâ¼_­Oçß7-úïeÍú†õö€At‰?9ƒ‘ã¡S*gÛãA\W­ÝmøÚ½õ¿AîûêàJT^Pã}Ü €Š¡Â/ý*áÏY§Ù8¡+g0lÏÅ?À +ù½Xû¤¶<ýßËÓÿGu oX5Ô†dŒ·¯ØÔÛ^ñÙ‘TT»CÂT:ó·ÚÌÓEèèkg‹ ãa½ÚvŽ[]ê +/s†ˆÅûÏÅ*=ÓôË 7ªŽœç¼ó:p¦5ºò7BnÚwr½~‹Vw‡&snô:­ß_ÿÞOdàÒh8mTÐ×ÈJqTI +Œ[ru‚ʼnޮxo›N¿Õ-õZnM6§Ù&ÌþvZáÍ´÷Ò#Ž æ§8K¹üA?âŸm%Q¾f%ù{ÝÑLí/y~‹ß8å8æ¡Ç'iD—Ä«Mx”øã`üÁŒ?âê`ü‡‹ê_bøq(‰9a,Ù´uÍ’!˜®<ëŒÔé ±È’ÚT¨òh[ÕÚ–øƒ%þå¼,Ášþƒ½üýŽl“¹!H ÈÜ2>7Lɶ¥þ`(?Ê¿š¡üðŒý EóI(*䆢BH¤c5ºò*)ýü°/1TàƒÒÿäõ?¸ÿ¯¼º,öÚ­ê«ó¼Wþqùo¾Àüþ›Ôe±ìl ±wŒb()tVkà¸pè%!wdr A€]ˆ¹¨¡þ_¸¿ÿ€ÂtIãDVlIÓðu˜d+†®˜¦ÉÔ0*‘m“ÉV Ͳ!`‚Òðù#H?¸áŸÉ ñuâ+C7?8âŽø/tÓûo×[å–Þª~Uo•ÿ›ôÖïȤK„uV½ñà‡Ðú/gÑÿdË„üÅÞ©B¯`Oø{ÅÛ¿Òpõo}žûUÌBðRˆZ~b]K½Ú‚$:$²RLŒ¡è†ªCÒKÒ,˜ÕDø2ð0GŸpü)Y¬"â‡UãO="0nf(×tÁ0¤„úé;Ö©R˜o +ó%1¬:S + tô;ïäsïäÿõGÜÅ÷}Ï1+´Ý"ÿ"¸üK¡öÛ¿ÓÝ“Òê¸=z\¹Õé·]7ÿ¦Ci D¸òH€ƒ-t^(ØoÆ“- ‹HiRF|Rà\¶ ¾s0LÛ°]W,Y£É SRÙ†\µ†¦A Y‰¡IŠ­Ûä7ø2b÷b„þÇ8Âûæ·…`ùB©¾€¶&87 ÿQãT!dÂÂè µÁ’ù.eêîÑã$þHeY¢ŸhÊèæÿ‘±½º3’ ÖGÕî+aN ±+ /¡½A«?Úíq§Õ­Ž<JPX!C'ÕÁÇ0ôû¹5z ÙILmñ@´jµ^—ü±ç jΠ&Œ £Ï^|¾Wwœî(_U!Aÿ¹9|ì~ßœŸöÎÔ›¡Ø/v—|N­mЪG<÷Åzf0¨þ=]ü ý µêo­vcàti¨€…FnöŽØrwøüSu0Übä‰Uª¶Ç¼.”gÔÕ—Vc3ú~ýK¡Óíu/¦Ý«8¯@†×\û¾ëªµº 2Qù k#ÄQvF§¸ˆo¯O¬ý'¡ÿ?„„<_Z Òâ|…ð¿‰ÕÓFßøéË[ª~gŠ†åÕÇÃQ¯ó}9Ù_G‡Ã*Hópæ‘-öUrüË÷EbÃþc¦òß°K‡ÍŸÿÁ§ñwÞC&ÿåXV!Œd–%ðþàºYK~sZ¯o_ÙdnÍïË‹u ²}x6ío­ïçV#s}sy¬â÷]ô­åüò¥=ù½—‘4då[+ùõ++ùõ{¯Ä½Å™µŒZoDÄ‚c§9:´ˆöø•UM¶ùˆÈûʽñ îdÁ¤ðÝerR}ï)tœQµAD¢ÿtö8HƒÙ ¾B]Bed˜Š¦Xn¦M;TŒj½ê ª÷Ú½AH /L÷‹u\½4t]Õg×”GMaüoÖ­¹0‘ ÕRݪÜF“«vªËÌ/Gؾ™Y•ÎÐüä„*Î/£P¡ÑUk­vkäe-¥,+ÛvœlÏ+üKnÐëgN•^ïMJ7æäNà`˜a‹a–•23ŽVã<.œv¥wAç€s*õ†-X,~Uø´’ª$‹+Q¿Ü…ØL|!^ÿªŸ®eŽÈæã~è¸Ú}W_P©×÷jøàÏA Ü)†:øªE(‰Óè·RJö{lg©a¿Úhz«¶[žXœ9eÆ£^è¢:9‘:gz 3\-ßÖ¡Ö°×®Ž²4´Ì­üÑíÕ?zãÙf=¡ªêWûÎ 4luÆm¼9ðŸŠPÝÂúUÂÍê¿’nZ R›/á›ÃÛ±±û§ÑwBYÀ˜Ý^ tb!çÈé’iâ…*öÂËÁð'ÎðÍ…2.GM@®×âl<êèÌoãJÑ{í^­Ú¾púãöÐýêç %û‡ðµÒë ß8”?2¥ê€,ŠÌaxýÖª¿•½f«í¸‰š]òóU…‹uo*ò´ šÓøZ‡¥|ú$P£¦[&ß8%R«ï óƒLÒÏÅv¯7ØóX¸ª˜†9¯nÖ]ý7«^ð3ÄCòÔzgýjݬb(ò¬ÉOæª[¶%Ï©êM¦9¿0Q”öÄy~cI,/¥ñYUsN»#ÂgH3û„ŠÞ©¨Ìžæ~l‚^w_Ðõ” ŸžÜ5ÝÝZíŽZ!«Ãi[!‹R°°&thŸÔ[vÚûÕá d2Îà ?N…5{õjx…XW¬PÃŒÊ8”¬¥¤”$YdsÇí6çG,%ù:¹pIX8á·íV× ˆœ1)ˆL«9D7 'M`¨Œ÷ÁlÎÇUØ“¡c¸Ëã<“ȇ +¿Œ\APšG€ÝIÊA±#X’È3tY²-‡,Mûv›QÕ=âÙgOûªåüLK¤šQµ[w8š·­ŠÕº“ÁkÄoTFjÏÞ +XÛ·eEñtjm—Ì‚5ÖúĪ>f5—·ˆ˜öä +Yy.y%‡ñI¯á +œäŒEô}sTo%þ¾Œzè Ô®vºÙƒ]˜öïè‚BûYv«£‹ b@ÎüVÈs3A(?è6œ_Š­Áðó 6);õ^—Q ÑXUÙø2L€¯‹˜· ók˜eRÊ€Zìá?Ä­×Ç×ëÕŸŠ&ôùù +ü¼~‚$ê‹ÎÄ9í•rl¦Aq»ïs ´÷“3èƒ÷Áp~ƒz»Õ'²4ÜÍüBtêWÂGY ÓS”}-(É&¢þDZõبð +Ä'õ•Ç5"l{d6Àª'…D2†*û(m†ˆ„JÔ3¾~®® žkÓ<èØöÍÏœÖ=SØz˜"{e„·æsê´å:Š°â~ Þð­ÚpÎpng.Ú.Yv@mTô×r‘#Öú¥Ÿò¡ÃS3µƒ€þZδš¯Áš3ê¸AoŸ<2§•Fæ×l·ús‡áe?QCV±×¯÷¾Qa8ãX¡1ö˪ +CF¹?áöú¿G ôäYÚ¦ÍeÐ SN¤ÊÆ7j5ÇÝúŠ¢uئæT›?=lSív¹}Âc'jÑËìÙ®wRW\ˆ]¦Ê©ÐµS åzèõ+_Ÿ•â¡Ÿ”ù3"Ýô©Þ9=¤á!N+(¼LôC9g½óëÇlÐ +{4¸Ü¬šï½ZªÖuªý)[hÊí|¤j ¶ôšÍ5à3¡pfõxª«O[ž¿óñÐ!úªHï¢ûís–´®H 1BkD#m„j¿†ò"ä æ#zñqÅÉê¨Í®$mNW=Á:2§³@µInµÐº9ŸO4œaëµ;Å:¬ˆ»£ÆnÏæõˆÆ8·^uHÕ—ö´šHÕ۹ϪÁ%¢=ÎY ÔtEÙZ»Zg¼ÙT¦Ö4R½ÄEªÎ—X b“<‡Z`§Öê³ûƒy´€£¾¦æ«óÓ„ ¬…ϧy_߬÷Óü5ëývý×Ù\‰Ö©wƒJe°ÎˆhB‚”5c}OíjÿÛp`õæ̬oÊ9XkÈŸ|“r±:åÓÞÆøJBò#x­Æf™­gɄŃ¹ÍÏ‹æœÇߨSôæÁX¥Oøk«Ûœ#ábµðvã[ƒ‚1§Vª³% +ïxùBå‘°¤oÕΘ/ˆAÞ4¾PÙÆê +Ó˜¶‹šÝQªÑžÏòhþ ÙëÎãwPmH´e®\ÎÐ@=íTYcH$OP|çjž]çµê]1ÏÔ‡@›îU7aÈ¡ ¯òŒÜ¢ÍZ÷nÚJ­_œvÉ@Èfÿԫÿ‰=Èû§óÕkXY¼qìö<óG¨ÕEó°ñù÷„¡ÊÌ»ü¶86A|vmIÓ•'<“ÉÚè‹”™öŽîv{þ? ¤2:0¹Áüþ†­>9a»_bŒÐ‚ßÅþŽÉò&ÃiÔtØ«N6ƒ–)gì:£2õâ«äÀ¢•c­ Ñ¢5Q•÷V/Gü—Mp³·LŸ¥A´aOôzw=2ÊŠV˜o“›*”Ês/¡£Œ{þ)CgSæìwUJ³!%MÔ¼Zg&Ÿ +{9Øep¥¾ëx +Œ= +’¹Ñûùy7ò´õ ¦ƒÐ['ØÈ?ÍúöX€Ÿ´ºíሰ”±g¥Ct?BðMØFëÙï;SÎXzÞ µÝ³åûÄÖõöÊfõfíP]>KfÓƒ½ÎÛÆk7|X ¯ÅVr­jj¸d\îŒÈFúroçDÛÝ8~X9IÆu³XPN¬¨¬iIæßó¯kÒRzó)µšÞZëÓÃ#e}!šÞ<x¥ÃQöuÿü8½¥9å\k{§žO¥V^'†:nÜ’ñÌ|1ºaÞíòïYí.¹–éôŽ‡™ƒòè-±cDÆż¶t}o¯\/DóMé°6µ³%ÓnšWç÷™J.u5{P±ÞÆczë£ø˜Þ¦:‰üZt\Œí5š QVñåùlœo>^›Ùvº}³Ñ̾roæìÇËr¾.¦·vW®i?dÊÃÜÓëSüµü™?h„³Ië})SNFºt7ÕÆx!j¿ÇõB]?åÞ´çÍ­LT]NdO×^éÜÊe1çŒWw®#o›õzõþj% +Íã7:²,­WÍAkée£õtØȶ£»+ÉAâaœ9./ÂüãéÍÃ7u!jl^=¦3ÝúJ'±}²¹nv¶[¦¹>lª™Aý@N|lÈnõüáðŠ€Í\qÌkUjl´rëU‚_ùd;–\s²m³Ô¡+¸=Ž¦s[‘ëš­ ^îÈŽ™ë=%¶®÷J-òˆÝît£dA;ÆjPro\ç]€ÓNö#n$i^5Ž%ù1r’_¯n-É»ŒbÀ‡'ì«,D¥Úâ†'vŠ[쯭ë­ž[+¼ÐΔ[å€î”ØÙ)¬)ùÝ×mÖÏõöÖfãýô 1éN˜ôw–ÕÙ(¤RöÐÀ£79¶}• Ëôp6ÿŒ Î;Ã]͸3Þë™Jþ=‘o®}ªÕ•¥¬Q»<ßÚÏ?ïd*oõQ¦´T?ÉT•`?c>ÞFH›Æ]áæew삈R­LŸ>¼Î¬vj°ÏvÛË7Ë…“t['v"›×CÐóB´ð,'®²ÚÍa1=¼]j'×»ˆ!Ko ‚¼ÕµD¶g?Aé_¸w'ŠXèj!º™ØÇŠù¶œ•õ òÏžÔ¢ýìµ^z³2ZÌTGãIP0)À#þf†² +ácŸ©V&§ñ…ícËýxî͸¸+Ô¤­Õ¼3¬INi{Ó‡ Œã½Ìó¡ŒÔ¶•|)ÂN=JåÞÍ*Ýû¡Vù³s”9{ÊžsÍCK’jb®Ñ¹Eæ9{Ù¶±}íõmŒŽ·ï²û•Èv` Q2 ç,¿÷uÈPç[ÀaT©¹qÝ›œm°^üµ6Úë/Öl;±© ²y@š\kØ2€[&ïKJÇÃo¸ Âs»ÙRz-ð•a¾øN¡õ*9D÷z…µJ}›@ñüšl웥Äζ¡â×Ly-Ý͵Zç XZªTòGGûÉâVAãWXËëj—V¯œ¤£Ùö¡”\Ý{ûÓ´v[«ZúÊ‘Lý®{)Å:òO#+É ýëðvÛ&Ýì²jZ,“_³„[zmXÍ*kx{Íñ†ï䯸]Â6¼5Ëè?±~‘ÔÛ½ ½çi¨ £Èý‚8=¨$öƒ£tòÁ.„É»-?y6:%˜éGÁ©à2±GøÏ–ië W…£@ë ˆØ”'=ÞBØÀ(Øcì~Wh}8:Û"•.wÉ?÷”´òSšCqë\…çÝÊ)ïå)ã®Å‡¨9¨ýˆ ×â"B ïq`‘‰²ê÷;N>pÐzÞOJ/H90²o¸—þµ|%¸ú_û*þ$£Ì"Œo‘Å]øX\•G*“4 £Ì…7B‚ƒCX3Tž +T¤ aÍý"§ä)ˤmØxnþÙˆmq¹÷;¾A¡?2(âÅÅU +;è‘Α@bÚVap‡f°D"¥kA â÷ßê¯Ñ ‚üf)™,ŽµqﵡÛð5÷‡Èiw¥0Ž7Îø_¸ X])ÙÓG½þ9aàï*;÷ìL7sÑ|="båQ?p`d*WGÝbZ–?¢{ѽg"–è1CMÆçÑ­â󨲚ޔJáDáêfŸeÉ%Aœ¤‚ –$Ö[ˆ¢b@År'ÕL¶‰Âr”5Vˆè¿Ÿ©M™›wÇÅŒú±tˆz€A T¢VïP2 ɵ¦–?ÚýÔÄQ²ÏNæ¢[¾Ì”GBâx=æÿÚ.¦íhŸ+qOù¢ÜW>3g¹l d&]u|€!(Haþ• +†¹Y¿¼È˜‡ùÃÁêË´2úþãAæèúœÈÉlf—ѽá™3`¢ß±‘%ÇöCÊÕRóu‡ùšH\w€ÎLóòÑ9Ê7χåõVéždá3GåmUY¼Ÿ¯ ÍT…¢¢2äQ(,jØ7™³bþýú9×z7Ö7›è+‘oWe“Ó¼hô`rIO>bbl¹ýèuE(™uöߤY«®=`'ÿšŒïÒÎÕ"¨­äÞ·¹õÖÎöÕN>]äƒ}ªè)„È8(í#øÉ jŒÇ”æq +™¥T,ïP:÷6@i¿M4 Oþ÷KÿÂDíç(GÔH_žwìüÞ­Ù$êxA‘”=í$Øíy8þPR(ø°/çùæebOrjżoèDMU^‹ålʸž¶–?fí+³øô¸)ºã%jw,†ý­µ¥õ{ˬfÏKi²–ÛÎ)SðÇ·ÑÌùiùª˜7øØ{Ë’ù1®•¤ã£ZßÓ‘ƒ6åîƒà%÷öÞHìì=?sÊzL ¦Ž²' "Ú—šÞžÝKËSöCÙ{\C€´Ñè´%B¤ò)@ûγvoÊØöÈpŠ±±ùÖí§Î»WÕO>@ÅLoÝ.õñ-ìïû,SÞk¼¼l”ôeØF ¶ccÅ’kâÂÇv߆cD²}N˜*å©Jº¨ Ï}¹–Þ¼\I F2Eo/n6Ç¥÷Lå²zKØhKŇ´Jf³Ÿt¹sЀ±µÛÅ^sÅÈnÍ +ß½bg_ܽªhQœ¿%ÅÈ,çkšSþæî&»÷•Pr1^joóú’ع}\ìñ Z¾Ðí±°{³ÇÏoO‚—ãpœO]4’éÓ÷÷ˆª‹ÅnŽ™ ñ%ûŸ½V~ýf3Íá¹â¬¿9O/Ï!›Ü^~›qÐëmŽy%Û9Û!§˜Oð8Õ²gcõŽ€úbXH¬MáÄòèHÉXŠ|ZéëÏÖfs¿öšÞ¸)Š]QŽMuw»¶Å{kíÖØǾ…³›Õ”bDOÕ þî6óNÿìÅ #r¬E LÉUN½áÍ«öq…t»K…g©û˜oVòÍ…ha­w2ÎèéÌ}¡^jÇâ³L¾F?CµÍáúéU¦rœ†mh'''¿K’ö8SºXÜh=YM‚ñëU‚A¼¡ÇÑö:¡˜ãU" <\Ÿ7ßÖƒƒ*;Ÿ‚ä×pús·îxWãŒU뿦äý«5VFPLä±ÊÒg²ëÙñÀ'¹v| x]¸ç+Lù4™mw¤&¡—;cgãì6½Ù·åÉ¥±z€RÓ´Î{«3+a•F£ö9Yå.™—컕bÎ1>2•|Uζ­ˆ%)kŸ­ôÆÙy&p˜ *Âû‰íúÍ=Ù4ù“ÌE%ý9I‘a®µ}2Lo£Uc׸ÙËæ®v]éÐ`狹ժßçꥬv­Œ&ÿ “ÃC:/Æwíý‡âˆ Á ‰)BhGÆæÑR¦™è¬éô€èî·âKÑg î­Bmý®ï—¼é̬ÊòR¹øY.“õ% +Oï£:Ú 'á¹7*Ô²«±…èÆIB½ÏT*Ëa±ì¸Ý3+#‹cÊ„ùχ²)æ2DDLÜ>–_«è®ÍÎÖþÈ*Ó¾xµµ—l‡,2+ͬAT¥x²»Vqųm•hI»é­þ59+Û]iyJNöcet˜Õn/÷·´•/­”‰y¾^b©åãà”¨…ž¸ïŒtNÃC°u’åêj„¨w¶×ņl…O‹ñÛÒ(st+7„A·NùFÇ^ï,ÈÌb„ËŸ}¼zü—2áÞË›.ìT÷ö‚2ÜÄiï=ß´ÛùÚj+j¿ ¶Š™vwÍ¡z!؇«¼ÓÜúCþèhkŠjÊy9Y,ÈÍXq#s±Èz#œ¿²nl3;Y#Ú×óÕvt‚ÿ´Kq›Ç ३üÑÁbŽpKÿÚZ—n‹iiôLpÞ­ëk2Qa/¡­£¾m¸k ݪ«±8U.ÍŠùÄï®ÒQÿíÂf1vÐÉØo§‘b¾tR.Fûa.×ÎkZÍÞiÞ^dbùË°ð/c–#¹ ùÿ~z+™ŠO*-ï±øT*¨“{,ò‘ÑbÙËBlX{F"% ×îdã&ogŽR>Î*ñfejòÊaTæ£LïílUÔœæíT${®ñÍ%|×ø~·€+%_-ï=ìE÷-B/c¡òè¸øH°Ÿ^BQï(AN6Ò'ùÔÙÛ–@µ;ÆZ¯¸_3ˆ¤E‰*‘nãeã¦cׯØœqx)®T4Aì*Å͵EOŸ 1ñØî7šÅØÑž-"iõü!Ú™r­ß¤Ú¢b,¾f;‹Ö» )¥÷ûŸ|Wzç Žr­nUÍÍó³R¦<¾T&}?è´†äØÊvÈ ÛFñå$jÏ–Î6¶.ÄNÇ‘éèæ2…e÷Öë…Zo?ùz•åëÇBõå`1ÛI, 4§1AúØ=½!˜n ÷ö΄}7±R8PšÏ‰û«ŸDä>8å'2²Ï÷ÁÊMqyÜw²Ú8K4¢i :9VéÄ,ÂAS…vöý½”Î4ÎGùÃX¬× ôÞ(Ó®vP4*Œ–Â|äÃ1‘Æ'…×ãi,c‘ÐÝZæ|õ™œõŽ,²ãÃñH:¸ßÛ +ì¶" Ô+§OµqñåIé©îeúHJÛNV`ÿ°Ï)¢ÒÇ=9nbùær?bŒŽFÛÅçvjÝ7Jëií•`c¸jn<%Êp”âEwU"hÕŒ¶²Ò³åë£õŒµ7Ûèª7éaüf1tøø™w¶ž=„¡Ìÿ¢‘#ºÙ'ìãaÝب-µ5´µ‰rµ¿_|=&‹é§â¿Œ3p£PÃ9šÓÉ7‹Œ·¶·wgU +µJæ:ÿª/·ÜËe\œg¹§4åS %ÄÅB¯ so÷[ŸÅL)Ýß8Ž{~Ó a|ÊöE¾ž«~äW[ëusK-8…»æAÁ5Ob•2Ùñå"œMÕŒ•L-‚-dÙB¬tgV^ö׋ùq¯%ωı’Ì»™r~VâÙÍ­$P=¾WWžã冬ÓóŬ/®>ÖØÈ¿ +Ü’}¯D”ì.3½ãcç3þ!ªÌãìÊÖqaí1Kô}CéxÆY9{Zb;Ÿ«‡ùÚsAvÛÎþÞísÿy¤»}f@ÞE³nž,FŒÁùþ.}v ‹>úÌ┸uT9€ž¶ŒËRcìÔ˵bTŽ½ùüb-nJâ +hå¾l\}63x$£ÇæF o·2¡±âòÝk7ßì=&Ðt4Õ~ÍX}Öëpeoœê?eŽîâ–‡}pX)ÔÇ‹OÖÙ¾Z±ÕµÃÍâóÚÕF`-n?•B/plûûyɽ¾WàÖÛòMÙí oìezŸÃuðî8Ë?¶n#âmµÇ6«W'Þú}jô9‘OŠ+{Ñâ~ÇU‹ˆ‚t½”Ê&"º8r{´T¨×ÖàEp­ì²ÒÎâ2 þäµ-Ÿ¥ûÑLo>%Ç™JÎ|&ZgƒhØëÉj¦—qŠÞäh/j,³™Þ|¼ÌšÝƒŠœ|}Tˆ¾_n§×yg„_”dÿµ¶È‚}¢h¡¸¼wÜ÷Ü‘RáeûÊ0[¯F%y{§ÂÉìú[·Q]²_¯d®ûÄ#+çZáû²_«õϼ´”ìâñ®‡s«2Ñ :‹DÜ¿¬íå4*«ÿ·ãúÁ)àM:ëƒ[Ý0”ÇkÏÆrõ'çdܵúm'ã÷K…<¢Ï¥Ø—æän{±{2N÷½Aè¿°‹UÞœ¡ªœÐèÍ ±Wd¡¡3‚ÐòÃÐÏoN74¬þU»!qPx窡˜¦ó)å‹nè¦Tèrˆ]’ýýÚ‡úmÒ¨× 9|Á¡iw¯V¨åh-Ds›vQº¨;¡ú`WCíê¯&ªÚï·[ui8®¿Áôºy|åuCGëVÛ¡1™]¯é ߆ÆÝH’šJ@Yî3éº>hõG1Œ¦U.ª„¹55ì–=¡œí~ Þ˜P³2hu0©ÀÜN±*·DÈqn¿ +í—ÀR˜Àä+Þ­竵+s_iøª^øžÔÍ\[©úêÌ}¨Âk^8¯_„ ¤×êÎy±Ë««ÃѵSƒð/ßî7z`^¿ú$;ÿµŒºÞ[Ì9È¥aÃÈg²QYh-à¥Ù<¿ +¾Æ3çuÒëöêoƒ^ÇÉ“½B£oOø8ÏC{æ,ìòe¹jŸ†5l9ó¢|=lÄ7žp°ô3¿ž~õm°ÀF-ú¼Òk;ëWp×^;_$oúpšx«ÝøéTf½#eÆߌý4sŒ½AõWŽZݯ½|ö“!dì^AÎ[Œ'DóœÏö ¦ÍrÞ°*ÀVGNåmÜ©u«­öœ—X|rF"j+þ'³ù ETEˆiøZ¸ùIAž»½½*QeâÍ‚¿Í—~‹Œ´²Ha_ç"™ú W«ŽŽ«¿:ƒù|]¡Íãtë„ù¬}‰éýüf¼´y+œÆg…§V™A÷_Dˆâ6Íáê €þØÖ>h“¦ÕlñgbSŸEºç#>|/’UIåçay<èI`ã@œ#|ÿä±êß7A™}?k _QfjÓ0wQ2µÞOη9$ÃÜa‘³vàü¹úלêöÄìQÚ\ÃÖ˜ £ßÕì·"¢ÌléÉ[3B5}9JëïÚX¢ÛÜ£c^TËi€E…!L•è¿ä ÝC9W ¾¯³ƒ¯jûÕ®ã½%¥:òD Å·ÞÏû­FðMßDµ®KÂÓ:é„à-}*ÒÁ'·¢Ž= ¯à4<6†Xûö·p8\Û¥ÃìsøvgGͯŸíÛ›Åjt°-7O"Çêæi8<¸–ÂËÚþEzã,=²[Å“‡ß¬³Ûñ.~ûí·p8þ’‡WWÃáØáâBÔÒ g¿yÿ!£„•ôàü@&, àg%c>¾¶ÉÑsòÏ¢µ—M;{8Ÿå7ø®§¯?ãä•üYx©ÂÏMü™U­Í]øyF+Wîêð³Ž?3Ö¹Ú'Ä¡ï­ŒUzo’¿âÛð³²Ý[IeÖÈß«Piñb/úqª’¿ÖN¡u!×VÖá'NTϧ.’CòGRƯŜ3ø„Ÿ—ð3YxÙùŒÁUò­g•·ÚâZl23Cïxß‹¹æãöoÆÑ+Ô*Òbq+?ΤÔãÝU5»ž,e*ŸÃh±]U¬c…ÚÒÆ=^¨æÆê‚ ©‚Õ?‘‰„ß”üz¢ä—î7¤—+ùâ·ß–ûKƒô„©³“àøXˆÂ¿ˆb@Þe“!oõ㎠+_—4‘E€sâ IkR|H–¹rôDŠ?Z¼îâ`çì™|9¸ðfk"µdÏË»–úpš’÷²·w™‹Æîr¶°Ñ`>±ÔëçS"¿ó±]XîݬîeŒ—ìÇFc-ýyÛÊ +¤äõzFè ú–ßXþ׉×qq«xpfGuxи¿‘(œËàb& +ÇÅSC]×ûmýãð(œØ Lnc;Ü`ÏP^£…¨ÙyØIÁ­ŸY¸k³x»²~ÔLhfç^¾ Ìædó`¸V¨IkãÌÑm¤I0ùžÈ&íó{xɸ·æ鶚mëÑšï¡ýÛcñüŒ|ÜË”Çá·D¶ 4÷2ò kÜÿ æ³'';A”R\þ†4ü[8—“áÈz½^l쿇ÃùkÐì ïŽ`ž‹ WñÕÇÅpüôæ3¼ºa¯"#^I ¶ccð+RŒõ,é7ã:ÙN¤‡ñW >%¶ÃËáÅåÇpd-^RµpÔ$[wyûL'´¿²!í…c»íûp¼Pî…W6SáÄùb6¼võtN>ÞéZ2^mm‡¥Né<,M'¬FFKau|i„õå­Ã°±y +›òó l¥OÔ°]Ôö×á§ðf½N0Þ^¾6Ã;;‡gáÝ«í×pº+'ÂÙÕ|8wy–Kábé3ÞöÂgýHø(9΄«Ñçðén".-áóZ¦.œháKéæ2|5~_ ßôÖ +á»çí72ÊÃÍ¥~:o?„_*úZ¸vuQ7núKáfcç8üÖ«ŽÂËÒ~¸³qûî¥öŸõëÏðhMÙ_ TÇ‹‘~ödq9Y^Œ½Ý\.&r»Òbr}Y”Nª›‹ª|ÒYˆ.êÖéJlqs£û°¸³RÝZLn?soåÅâÓ‘¶xðxÐ^<~Ø?[¬dzÚÊÁY¹±RzØÙ]¹nÅú+Oã·“•¦q›Xé<¯Œ>Ò»„ÿ­ìl‡c©¦}3lc#¶ýnb…âæMì„pÅX¥[^Ž=¿>&«÷œØÜ_¬%ò/R3qº–o'®On‰êâ0šx?±R‰Ñâ…¹]‹=G²kJñàtm+Ù»_+ ‹­µ³ú0²vûXR×ê/ja­Sû¸N.v+Édt'‘47’™dæôó:yÔnö’Wúƒš|y¸>I~¤.Þ’ã×;)•ÈÞŸ¤ŒåZ;•~ë“QRGW±ëÔÕñÖRªzt¹ŸjvÛ닇êÖzò⢶n5"úzn¼ÿ´~¶1R×ï.ŸÖ !ê럥§š‹e¶$õ2ò!íêÕ¢t8*E¤«×üµT;Wu©KÆ•£±ã…¨,ŸÆ×åírüM>¨J'òeÏTäšœëÉÝýÒ•ýlee{%¡ìô +må(÷t«Ü¬¦ö"›ÊçqdEçË]ÕÐâ/jN¹TKföP}´—Òêûñ«¥-^ŸI Qm}”KhÛ[æ²vx›XÔnV—Fšs»4Ô†êÒPO|¬ŽtûÚ\Ô÷r¹¨~¹~×ëÉWIÿ\[5ÕÕì®aÉ/ûF1»^6*åg£ÖŽµOûvÙ\}Ð ÓŽ7Šä<Û{:º1¯v”³1譚Ü-'g×ÖÖþ~ß:Êe5ëngóÔj¥ßíÈYZ±ç¨dg—¯úöyámË~q–È~·¶Ö6V[·çÅÅ0áY»{·Î[£UÙÎ/D7—ŽÚ½M-·WÜÌçW†›•üãñfãúpesT•®·¤XDßJçÞ[[¥·úÁVÕzˆo}6ï^¶“»·¹íÕF|û´úVß~¾Z:ÚîêêÎÚÞÑ`g;_{Ü9=Z=Øy¾;!Øßé·úË»Édîcwçtð¸{Ö?.íV‹kùÝA¿j§×ÏöÖÓS]I—cÑpºÑùègÂOµzßÊ·¯™ë×ëfæ}¥ú–]ÙúxÏÚçãNö¨/ ²OÙ½H¶?¨'¢¹Ô骚Ë(Ç;¹òht˜kÖö®òK7ãFÞ<;æÏR©üãõ[&ß9)Ö—wZ…¬µ/\>„3…Öj綸rý>,n®¿ÙÅSçã²X; ÷Âñí==i=íÄOÉ({ÑúÉÞg">Ø—ì£Ü~¾4øØ¿y+ììwVÛ¯k•£ƒL<þ~py_ϼÛ{ƒÃøX9=Üq"‰ÃråíñðíÊÙ9ŠÝ¶ïž®Žˆ"g~ÙǬÁíñö³’]ˆ—W2«ÇoW÷ï'q9|u²ÓÊæN*—-åä=»>M˜¯o§£pz­GNO;[ù³ÔIqó,ÿ$)g÷‹ãµ³ÏÂÇJIy«FJ»ãÒóèit~lŽÏ­boñüÌŽE¢çÍ„¿ˆ­—¤‹]õݼ¸²Öw/:§—‡åugù²¼·~Q+?Åû•ðâC¢bUv·*¥ØYåuÔ¬]®Ö¯"—Ùû£Ë»rúürPÙü¸ÒŸ¶å«“·“«æráý:ž-k ÑëÌkóòúΊ.^š;…cÿñýæ,Û¼yý8©ß&^–ÌÛüãeíöñ^±îÂÕVónãíx÷®²¶Ñ»ëâÇ÷rs¸z¤¾>Ý7žj;ñízø!mÝ=<Ôú;áÇÔòBôqót£öxy^>~ìUÚæ“V5"Ogá»æSË\½|N]^žb²ù\¿yK¼ÄµƒñK®+¼<ÝêÕ¥ƒÚCu7}u]½K—ÊÕñA鼶yQ*Õ®W¥Ú`µvQ·NGW Ñzy(ß×{Gÿ¿·÷\kmç…¯ ÷0Éè…ÞB ½÷š “N€oŸýç\û'Ù£¸’²Î^Ï;7‰Ë–e5Köíƒ3öøü°{¯µÞŽÎ+-k]³[;Óg ­×Åɽ¿æê×ýßíã“ß¿/çÌGÓ0×·÷‡¯_¾?ŠOæîûäÓNýíèéõ¥Ýþg]T&ÿíNœÿ{ßj‚õùììܯ=ï_Tßž?צ_üáׇ—£¥y÷åûùóöuª±ë½žþê­·“·™·¹©ý··+cyý}¤ì¼7Fô‹÷ûº6ý1æÖ~>ÖWÍ㧻™©OCÛüÜÙ¿¾ùü¨× ¥/ïfÆþ:^»hÍZ«=W»´ò‡;y=@v ?m‡h~>6Zíwé'KK|”»n·îÿqÏßDý´Þ>_Ùkó£–“ÛOiòäaÓácÇÐFžçËÎ_öøIÁÿùÙ~ß¹ ï÷›”þ'£çíù§Þ1hCŸ€ÇN¶ æ°ß(cÕûÆ­ÎÝÕµzus¼:ûôcâ_†5µ;aF »Ñ_¤aÒœ=øYXú민¬îMß.ýÕNg¢V£:½ç<+æêtq¼>²^@uæe +<¦3¿8öôšnþ‚Gó;±_Û-ôvq±²k}ß›0¸¥kvûfÆ\ðL<ÿ·3rw)[µÕëÖb¡ÔnÏܾ¯¼=ß-ÜlîÎÏ},WÇ‹'õÕù³ùUgvqù`Ö:™«ÿL}ï×ß´Õ›Åé6<™©Vòðf‰öâßPHÛW‡ËS ë³8)%šßí¶ñ}[¬ž•×‹Õ•¯™bùA«Ç÷gÁEóÆ5˜éóxš§–45klO«[û{Q‡IëÌgeê“Œ‹ÜŸ„óºÝ[þºYŸ%®Õ¿í/økÿS"|{ËïFÙ?^gf…KÒ¾ø~ôjfÓû­Î¬AJÇT†n½óxrð/|\y…_Ÿ.±3ü>k_Nîlã¸é\X¤+ú…uòÔS!…u¹|\XÑ +Hã¹^LBz×¾ªWÕH·*ƒ‡Ÿ¯M‚´PÐ~^9 IHWgvÝ÷#5R«rR¼ßÞT!…¹üÞL”GKSÅÕ\µemv*©38äMþõž^hË »)Éææ:°ü9QÚþ·¹§Dº2øÑLD:²Þ´6UHq¿L~¶/îuD[‘WuàØ,Ù?#ðCëCd¥Ë‰¡™éÎȈ°¦Ö»òJ‚0:×àWõ²}yt·—€tâÚ>|x¬+‘^l©‚GƒhÝúßý‚VB +Ò§ý=QýV#Ý%à_ é÷ÈÌ©N¢´çj?¡¹¢FjU.«S“oê™,ßÚƒ_Î呂°PVÚØ]H °3X²={6é鵶üqp Fº24=Ül=Jª¹j+»ÞQÒª®kOiãâe™#ï@å{dnŒw4F +X(ÚÕë7ûkôS¤î§ˆ´¹¶u =/ 3uíZíLÔ:[Кo»àÑh¤k¿^óôÞS"Ý~m­&"ݨ}}Ž H E{^Õö›_¿j¤æUsyy¶¨Dz°öo-éþ¦spC4²j®çKÚ‘vå«‘6Ç®G'•H®¦¿¤K€ö¨ÜºþLBº©Ï.$ mTç¼ö¢éÙØÁìʤ¹>4^8ÚåíMUtëðßËDik,F +Z,F{uurˆô£5[~LBº©Ý4šKj¤Ë‡z¡4pò³»¦šk»½5> ½3+¦©ú£ “"½øYáeÒDû×\ÔéAé}‚v{ºüuÕ8™¤Ómiuâãt,@úâ +HKϨ"7Nõ5^<à­3­óD;.kÕ­ñ¡wsr.þˆ«Úh<×(ÒÙrcL„#Õ@&™C‹¼¥dUZÕioë‘Öe¤Ni`útu®¤Ecaû)@:±[f +ÞÙÇtã…¢=l6yƒŽ{~ú Z5°õãíç­hL¼ß%´þ͇·Ï@#KíV6ÍŒÝRÿÚª´6ÿ%´žîh©ímÔr,aΦµæœivYÀŸÍjͽY+©u^k>l8I­`Ãh›Åc/©½¡m߯í%µ^kû‹/? ­çcÚþÑ`1 ˜¢½¦Ìm–“~mk‡Å¯šºuª¨µ‹nЪ’É«ÚÑaqš¶‹Í:_׎Çg“Z›Úñ?ŸÔºXŽÆbRûŽv6è^&µ>k—'g£ ­®vùüw,¤˜ÜîkW‡“fÒ¯ç´kçÚOh½nëµÑ [h)v[Öýc3á×wƒúÂðÕrRë¾¾±;°žÐzÿsÙz1þ%´?TôÓçau«}õ~09õs¦n5¾wŠ#•õ­€bÆÈLy•oŸ/ÖVçfH»$ûŒŸ×┵þÌ´.Žîa x7ðÀÊÕ…ï-*f¦?Kè%ÆRi8¯µ@„ÏŒ¿:ô,x íö¬^çrÝL÷télùô`é¬1Uƒï´åÅ…ÚýââB}£Êzo7¥Wj1÷ 9BœA"çÐÓ9e_}Ó{¿òä÷Æ9ˆÇ¿S‘79Xÿ7}W=4Ðøöv'v{¬=`ŒLïŒS5‚ž#pY¤öú9j¤ÖéI"ÒB Tæ/åY¤ÄÓI@ +¦-ø9·IHob¤„“¹¹¬Œû Ò‡ááÁ)±þ#¤¦@^´þ'C¤+¯1Rô’¦Êì\­ý¡)øƒz-)±þ:ƒhý_Äö‹çzžˆÈûf$"%Ö¿ +)ñ’œA´ÿTsE¤­d¤ÞÖîI2R´({L$0Ú×IHw%Vœ Г¿>ŸP¬¾r2WÖE3±PÈÊ…¤òÂh:|@ŒÛ¸Ë¿àkœ/ÆbÁQáRÖŒ~6ÒrìP‹þ9gÝõ jôÉê;?Úwmø‡Öøþ+˜ýº]*á==ðÿpAY#˜~Áx–Ð÷[& BìiÆ_9ÜÃ¥àŸÛÆb&Á-6¦àçKzãò{…™03ä™F)øglóƒR‡Zí¡8Žg¸È®~Lù…úSk©„ÿKÎjk „!E CkŒ‘`W¥1—Ý`d,ÑÓ‰ÿÌxÕÜüP&ÓîYQÏü³—±~`'o +ëx|ì +^ÿŽ†ƒ§>j~Ñ"çY?Ä’¼‚“Õ‰•Ð±-'Çrt–‹ÙWN?Ut‡¹tÌYÚ_}à4kçpt§qK%å³Ø4÷ΙÔbNî‚X¼è©}H¢çtì“tObJA4ÿW£ÓXa:ˆøb•a—jÚŽýpÓ•FCd;ù'  ›*weã⨘$¸Õ»’º.Š©ÍêMajŠ]™1µñÁô©ÑZ;µazš@£ +•WvÞ²g5Lg¥fö«†Öú~9ŽÇ¯~Gûä5„©æó•a¢{Q¿(˜¼q±Tïœ02Y®Û¡Sèi­õsXx'âdê¡(;»ûNTù¤«B)µ3~ßéŸÒ¾»3¾r̹cß-ci-eß]ÿŽ€­·9É$ñ¨.æäÛeãòwa#kAñŸ`ð4 -óÌ—fSÂö±…r0(vdÕ†½]Öþ.ÖÎ&¹½Û`Ô;Ï\ëKZ’¯.icÑS(% +³:ùN2æ²-ÆmF‹­ˆrUTL™ë?¬è·C#«‚Z¢ö˜h?eYO/~1‹XÓwåxHÁ¹˜jPÆ™¿®Ö•Ñhò i0EWÆ‹—®<Âõ{J4è +Ԧȱ‚tý&]Žõ‹¬ ŽXZëöú°?Ì µž[G÷Ú‡Î8…"tEbtöwäë¤oË°ë:×Dù¬o$Z§ âZaægæKp\…“«D!D-…BNëøq5¿+(+h*ÇVñ„e5§½§Ü• '/}Ú•«x +²Ñ‰£\Iä±ÇUshr£Ù%u‚Ñp2‚ØÉS纖ÛUHž  =.Óµ$°.Ò‡¢H†à¢¶-q(FG4Q¶}•fDäó '¾Ô*êgæ;EÛ…¬Rà|Cr,¹ E)_8¨8œ £y^)pÛÈÁ‹…ä°¥Ýå÷{Ž F,<**”ÊðHñ‡“‡ô3˜áïçk €Dæy-¿HŸßOu$\—ˆNù ÕR(”d®M°ågËK?úèÂórçó㳡è +>åßi>9x<+_¼Ç×±¢-žÈ …ÜÄʱÅ™WŸÝâ5y‹¿­ó[<ÁIK‹.shb` ÇXÂÛºàçÅ£)”:s*fu(È +ð!Yµä‡©é#=OÍœ=:ØdÎø2êø_ZÞðF´ú»¯–£ž'Dt*gÁv÷™Õ×yy®$K!aì„I×PÉ¿ž±Q‰.ÒÀ›Ò³;?‚)](™³‡¥ÁÄlSús#1fV(åfªÙC½”:šd5(F7DE¨æÉVM-VƒÝï—ÙÃéJ^>OÔÈŸ¢úë‚Ï'v‹DïÑÅ<΄ •¬\±¾ÔçF¬ïºŸ€EPuJÂdÛ²H^Ñ%rr.[v0Ž0G±ð×"|WNvJ컓ýç*Žàã–+;o‰ú®ÃvE[S÷KŽx+tÆÛ ]ÉdÜ4z¯±øEzÊSî¹ìe4±—B©“~ª]Ž†Éí!ýŒõeVãB/lÍH^ÍuVË«DÃüä”°x¥"Û›'0Pi™÷‚Îú¢nˆ_‰# }rŸñ%’òaØÏ¡Ôë i \»"H4ønB4ãëÄ’89ìH¢%VƒYÙ7‰vôQ(õë :K”hù-%ŒK›}ØûxŸ,‹òî}9÷!‘Ç2ûeHz/Œ¥$ô“G|d¦.dpG/OÕé‹Ô¶¿`Õ´#¡’;Ö•VJèlôyFŤ.ø.C”çιZ„†µBHi™Åg¯'G=ä2]ñB6ñ|?‡UÉÙ*1S(¥$ ¬œ~ÆÞt—¦4YµB<äžúáCZb/…üýt’¡ŽÃÐ~²]ê£Q%ñ'¼i&¹ØY',¤Xª6¼X©ŠÚðv ™Ã§ata¢ÇwrÜ¥}Ï1éÅcÃôª ¡³T]XèD^|å°ïU Âé—¿ú@ž˜K†¼¿Ø싃~:Ó> Z úé9ƒˆô¢åÊ…Ëî‡÷¡Ôº0唇_°ñtm˜® iF§ 'Êc’6œ(çö"Tºµ”`(ç?éÉSRFâì'Ê )ù“wÑ°ˆ7dÒé¸ +9,ÝB)ßî†Îz°t…¨Âu;×îÎÁ/e'…b9—°’Gy¬ÌBêùéÉIjè:wfô’A%‡(r˜š‚Z2NNÇcµbs'º ÷žœªÔRlõ±¹kI™kì’,œ<%†¡ÅÍUP:©,³,Ö2|£üÙ¶ØY.Õ’šPuåäÒ/y˜xát`8ïJR½Ÿ”…ˆk™+ù–v– 7î¾1ÒËgK%e3f&F’!©Ã”“sl¯Ov[<á2Ë×JvV€TËÍl?­¼Ý^JKƒ¿ehîjù`f§Ñe ]DEe]¡ÔŸºô +:Ü•ý¨¡K¯ ƒÕïK ]z±aúPC—^AGîTéC „”« £»²÷ºô +:¬ìG Š¼q²Z°‹ºô +:r{CjèÒ+èÄJ®nkèÒó¡iܲ÷:>!Yp±F>_Ê•ìÂØuÉX_Â$-–>(fHùS`ëÄâ½PJ©˜âCL]篞/‰‘Þl:%ù¶;#ɹäL.øSb*‰gâK’¢—R‹9—+­/JÖ ½¤îùé#‹5I^_/•syçÅ”pP0gÚø£’x2’›èY‘«Ôý’»h.Ål–ÙU•m{Õè%z–Om3î6¹Ê“ ¯A#ïÙ\!;l|Õè2^Åî}˜ÜÅ`9Xì–– +eBf»¥FŒsEz±Ø­ç—F†ÍŸŸ0)© )nˆš“óVhåIÁš¹3>Ђ匈ì`q>ñpgüfEHòÖ Á®s$PuéE¦Ë©gÙyg¬uQH#_˜$!pƇ ÑÒ#F#“W0~˜+‚fª;ªó~²,Áêø®~ÓÕd'u|Y¶×(´JRKYøûy‹ ƒÒ´äøí=EfZ_V|þ:¾RVÍ{u|BSi[æï,£ÀFÕUBe +tVïß$3o 褳äÓ‹.(–‘íßÅÌ^&É~ÇÉÏ¥:=®fUÏä²eWã˜oRÅP¼Ù“*Â2ªå¸”çb«Ù‡ìn=S:?®W³-»uö~f38µÀXÏI] çuõË°Ä,‚”•N¢IV¡¬PtÆ—¸ÈÂÉHŠ–Xk§gïýLräpáÒOyØ*¹\ê-áL¢PÂ)Yy'”•Ê_^j‹U±—ß?5V¼™XéÓÈ(/M.?ễæ HÆ[ë[xçyMîðöX';è”{»R “L§ÎÂ;I•c$ º£ðN7•qqõS®Au‘ †éJ~P=ð7$ñN•îé”#"SH,ûã#2Ó?bDK’òFdاe{[ï9"cM”‡ +Ý–çðk°ž‘‰c9ùÞÖûA“³Ê½—¡)"2Þ§DÊÐ:ŽÈH©X†Öm‚0³L|DF…ËWŸ×Aq9ãK*Ïê$–çtTœC#½uÙXþÜȪCÍe,oc¹×šÄ‰ÝR– þ~¯Åœ=ts”Ž&‡t¼‘7w6ujÕÔ¼‹|ºÑå½aÂXW—œºÐA]/d“¬‹¬ººÎr•^ÒFÎ’„ÒQž6Íæä¤â:!QdäˆxŽ +ßU²ró•åª‡Ë¾¯¯õpÑYW×yN0.AÛuÃc×é3Tû]×[†jÞz¸ŒŠÔ>ÕÃQË“gØK=œ$-óÝÜ×a=©Jë¬T£‹z¸äø~ÖÓ*â²Rú:­‡#÷ÁJqÝë¬}ô¡²Õ[Æœ£Œ’m‰å9r"¡+£Ùé£óûy®üO¬í°מO¼çk-Èhxç9©N<»Ÿ®kì™J®ÃüGy™Ü,œWvX\'nÃÆĄ̊XˆÓ¸XüÌð_rnÃÓÜ—ÂFµ êò£Œ[SòBà#_Ég¡¦&¨½ÅάñÅ.kKû° „MØ¥÷JúIÙ†yïî ýô|Õé庇ÉiÚ“Îî’/STÞD‘–ß󓲊ð»T‡º×¥Æ¤½å Yr츩Çý¬H=îOEêÅW*Rõñ¾T¤B?}©HÅ~úQ‘Šýô^‘ŠÕk +÷0òø2ë×Ä ’riª”d¤¨æ·áu[Þ†×íŽâZÉÙ¶ý,…£÷öä,|꺮»» ;-…cÞ¯TvÖŸR8J1±Ž¨Û*¢¤R¸¿2Apu[ +§Ž*ô»Žœ½²Åp9Kár†wßD0%ÕVwv#<>ñ |¾¡ú]ž´ºº—t·Hyb“°. '_C9á GpØÕO©O6Ì©xyÂYRŽ{la-sݯ&_ÌW£/Öê)J¯3åCŠnú±t•ƒì äf¾(ãŽØçOùR·B©|7´÷’PwV¹H+v»ODª-/8Û\&$_Å=-&»]¥»íD¤ÀÉ++íÓ­X•Rw6U¼L)ÛÛã,%¡Ømòõp+©ý4:w<ú™TìvšRa~dWU,v»\M®°+½Ôî’Þ¦VØ­h‰HÛß냉H‹ƒÎaâ{|ű´§ñÖ†…UÅí:NГ¿ÂJ¼ß‡D8*ùCÈíÁVž¶§‹9àÚ¿×/¥@ŽQ剳–LÑpë¯ÇÊ‚:Ms©¤”[Å]j‚»3ü&¦É‹ñÔÌÒžäw¬ø2—®ý‚!Í}u”s•\Ú“ñF‹˜sÕûKr*ë—?KêÇKr!Øwä”/åå¤ÓÈ@g9W)ï¢e¾8Âåõ¥¾ûÖ§GäŸc­ñ¼|ý؈8¿„w¬¤»Ô:,Ae‡”ýÎH¢ ±ãÞö‹ö·i]äóüä­¾¾UÓÉ#¼j 'Þßj:•Àx}ª¦S¼„HoªéT!gåÍ“=UÓ©@äÅ^«éTµt)wÝtYM×QÔºëj:5'÷»šNUK—•£Øy5j +¹OjòVÓ©üf±‚»÷j:n\›ig¯½TÓECbjé’kFº­¦ã¼êø…Ü>WÓ©V—õ÷ûSM§ª¥S¼—Ôc5ª–.0}¬¦S­É!ék5]¶¥Ôj:UWä|¿¯Õt 5#}®¦ë'Å’«éº XÕtÉëg5]UQKG*¸ûZM§’‹á»<ý«¦Su€ëÒßjºÔš‘¾UÓ©j¿xïµÕtªZ:á,©Õtªb,Ø>UÓå÷+É‘»ü&éä½_ÕtªZºŒ;ˆ’ŒRR c'ó•ßRñNy)ÓÄH(£¼¤çµ¬g*ó>qÒ"Ûºèô½:•Á“a]tñ^]¢u‘ý^]N:%½lËäÁæ¤ÓS¦ú–Y@u ¾{—öm'Cmþü|)½.—õm\)œ5¨ŽJcÅ!Å•öþl'tròK˜DÈò_ê¼G´.Þ."ŸWæ ƒõòÌ]L±”‡î:3ÉÏÜe¿3’@þŽž¹KŒ]°ÝuŒ‰Ÿ¹ë)?9÷3wéùÉÁCwE©Tt…RÏ9P9ž¹“_šHHSèé™;Ô•™Ýe?s—ó^8Ð ƒ½–†lô±Îâs£óÊ¢„s1˜Zϵ<±êTå*ä/¤Ë›y˜’kkÞsìÉýÍ“©’ñ@¨+»©J#oÓeëÌ$²ðT ŽÛC™POTeúŽÕ~ºªË«è04¯,ìê0;jeÜ´–?ŠØüýɆ"µÿ=óP;5*åcÏoL7Qô^SKzIRy‘´ÌÛOæÌõNbŸš¤]…¶jª´Ì[XûÜ*K…µÏ­Ì³¤¼Ñ5ì,Ï“¿…|…_Ï­Ñ\ÂŒÑ] ,1÷ßÓŽ–”Ždjõ=а}ôån€E’9Ò{eÊa?_0<ìç †‡½_@^ÊK²ß;«|¬vYÈÀù/ØOχ»´—Þo  ýtbÄ«³;è³tÉ)Cåªâ;‰•2dnÃÓO¹í±>mÃÔîr×WöôÂ\ûÖ¯w«ÙÆ;yá.£òq¬õD䥼޷aö w¼”׃/;”—•¹—¯>DõÂ]’„ÉYX›ó…»în5_¹øê(½)AŽq—Q’žÛÌ®ÌBƵ;ù kõäWÒó¿õ ôn娽ÈQù؇ÂÚcU4«óœ+ÒOþÂÚÄØ8é§÷ÂÚãŒ{á:¬o—.ª–ªÛÃL•¼/zÉÉ5X*u˜~/Æ }U6{^·S¶af“¨ÅNÒ˘:+b™¾+òëÒõÅsØÙSF…z!ÓÈôKŽ]™·ä)–í½çó+¯Û±ŸÇ¯LÐlåZÞ"¦‚ÒC•âç2 ¹\8R%™†ð]¯†aüRÞÃoÒÒvúÜãbM¼§õ,:­q½ûÎ{b¯Kb+tÖëÕTñ=Wwßýzîq±&'{x)ïä:÷+ É·7œvY㪽 8ç;iÊ!Þ½3ö滀Ox—§ùÝn?CA‘ÞºŠµ}øÏ\±z_[Dz¾ERÛW3›þP´tÃÂà‚¿ÂÒ´ Z°ý­—Úìþäßa0&ü„gî†ëáÚ¿7µqÞ纛2“_œKy[Ï:=S!%Õ¤`ëYgYR¨ýû»µŸˆtX_¿¾OBú@2ZŒ‡maŸAÊ—¦}>µ’êá¼Õ™¯¡·h¦AiîÊ Ê1¹ <ö✯%Uþ9ƒC{oÆU¬ÅÄ2¼”‚Ãåo+éÊØÍ‘€”X°Ú‘VÙ}Jªr¬¥!mŽ$"mÎý"ÕþÍ]Žp«ÚòCôä¯`!*‹goép……¼ú}{ÏìÑüºjœlgÂÙOßñZ—t1/˜aD¦ü&©Îåßv²8Šà>±¾o­ŠçA´Ô¦»´IÅ+f;#Ë !&e„$åºÈæMzÅ,ï+_ù1 Ù%`ËÉ÷ñäµt© ³ÔŸÔ*B'UbUB&d:’R«º¨JMt¯:«Jà ÍÔª´ø)=K+nÎ/3'=a~‚g%n™yZ9‡Äd³I7¶uX +˜/K+Ç~™Ly•žŽFÎå…UEõfÊU£/1æ†üfJ1ØÓ±Ÿ¼Á­¬·Ÿ` Gé©0yÜ_*a}8å9ON¨Ékô׊ãcXØó‘5Ö*")¼´ÌU˜ë’Rž,¢®lôïÊÆFœ¥¥ŒÃä~’;Üñº|¶sg| ¦ËªÙÛåþùÈwæ@ÏÁàðÄÛ;]vÄ®Â^—˾yH8n{ñ>ÅgeÄ£êõÒüÕmÉv[ž;"øRÀ¬”ø\Y«¤þÅ˸砃²¨»¢ÊxcÞ¯äE’«Û’ï9ÈesƒzJLÖïôY=öõû¹K9Óß¼)ä^A\¿¶ªP'3(*¸±³,7%ÿ¸H5õ}±N:Kåø)–YËÓ Åz¹sDì +%Z¿(–ò’7S)v–h*«ó:³Á~é² +0o `Î[5»ÈWˆgIÝWvOîª +Å—V(½ÀžÐ…º +0o É†êº +]¡´ÀB‰W-Uæ¥'òX÷U€ÌÎI­TøÈT¦‹É§oéU€Û‡êYe>ÊûŸåëà‚¨nå“89½j«ËGÔDÏâ¿y”/) +—³p8ç£|Œûå£VßÚü(_Z®ò%ÕŒô÷Q¾þ¼_™õ(Ÿ|ßxÊ Z‰»wŽŽ&ûn¨ÞÞõKórßÖûp7yׯOwCe¼ë×ÑÝP]¿ëÇMMzÕ¯ó<%õ»~éQ!Åû•]½ë'Mˆ{Õ¯Û»¡Äwýº®¯ìS-­IìÏ»~ø‹äWý’niÎÐÝèÇûb9ßõK9Ä•\½½ëÇLMÎíøýÊ„wý:‹[vû®_ú«~‰¯3tø®_7ª¿ë§f>)G±Û‹³ƒwý²9¹ïú¥+:Æëé]¿\Ui}*wOzÕOçÏqáü®_7<Öù»~ªì¤øU¿ÞßãÍ1š<ïñåy×/Ï{|½¿ë—þª{bÕË»~éin‰·Ðtø®_º§¦Ìµîâ]¿¤}û%£<ïúi©¯úq'‰=¼ë—þªŸ‡IM¸Nyׯ³ª´nßõ“„÷ª_šmÙÉ»~½íý¼ïú¥—I’¿ç~Òoñèè=¾ëT§Õ½½ëÇõrrÆ×Ó»~âÙŸØZȵ ³ßõSù¬ñ&$Z¬ïú¥ViYCùËÒ^õK’c¾ë×™5Þí»~é¯úõò_þÇ5Óßãëzr½\dÝ9œû]¿ÔøW©Ó´ÏzׯÓuº{×/Ý¡f$LOïúEsV¾êÇ{¯Ý¿ë—næPNîý]¿´*\ä±þ¼ë—^…ÛÁ{|=D³¸÷øz—¿ ¯úu˜s¥x×/O1|Ò;#¾ë—žCl˜>¼ë—þª_²ë´æ)íU?”cýx×/ÝmO\—ßõKsÛÑéWÍSÚ«~ú•‰ïú)ýʤ|×ïú% IO‰*tþ®_ú«~)µ¢½ë—^þH«k{×/½ V²Çº|×O¹.Ñ«~ÅÜïúå©®íý]¿ôÃáÞž®ßõc÷§üª_r¾egïú¥sM½+¥%3’ï’•C Â÷’’C»'êbh¾KÉdMJ¼ß +9~úbX°8;‹ñ¶ç^Õw–¤H ªÆlÛ÷Å€¢agÁ¯[‹íöÌÞ¿¹úÏÔú¼îï#3Kë©V«;·íbé¢:\Ä@Pqäjõ©X›~ž¯NÎ|ùթɽSXýƒ/Z£ñ\×ÏãÚòÆî’¶üñ¯©­ìúãZsmëZk¾ý}Ô¶_[OÚþ¦ékkÿŽ´Ã刺ڑöóª]MÿjÇãGeílì°¢]Þ lkWW'µë=óG»1O°Žïf³¼Ûn·õö÷ÅÇdûW?jÿ^{åïw ‚½¬ìÿ÷°º½ºåÿ;º<{,Ž—Nv†¼É×…ÒÎþÊúðÓKi`À¯o•_ïKk–?¼}÷|²45¼Ç×þ½)þŽ}Z[7dIhÙÛüòÁAIj=Àw;J¬ ©.ýþÆrÒfq|áÔbž€ ^œ@-¦&Ö„äøýÒn¦®*íöÖx5e¦Vå¤:eŽÌiË  Úòßûume{ëí{ðʹÃÊÕbP‘Š Õ¨N{[çZ}ùºˆ5‰»ZãȽ&ÏüiõíÖ˜°›øíÃ=ÙwS~‹£­djŒÿ²%Sâg¸X™Ú²Šãõ‘#¬¸]*-yõâXen¿[/Vw¶°ìv f‹ãóæCqlódµ8ú53½ýò"«où¥·QE¹9¤.ê“‹ÿnë:Îêzn²Yl“ÅYn›üµÿ©=>ÎÀ_G_Ä­×ê/5ò[sÈûÑ´r­N>¢H,“¿0 +÷Ü…߬VèƒG¾ªø±|œ(ãÇñàcÓ +º)ðÓ¸ùòµº½¦Í¿}4¿ç7Ž/µÕ»µ“åëÊù†>¦½ßa_lc´º4Vú].¯®­éc‹¥»pcláþ5Ô؆ÇéßL“ÁžÑç! Úü+X²ÁŠöP.OÄZ™«FàWzãjb*lXH¾;|׬S#©¾²­a¸JiW_94°çQâ¿üøA žh þqßáãÚx(f~GŽÙiÖ 8ù†{šqù}§Ã?øñ„vkμӵ2*CµJÕê§kcØ^eWÿt»F°˜ žéM¶®*Ÿœåí…WoÐcü}Ê> »iWbô(¾ÓzÜ«÷§Ç°¿C-î¯Pr§'ç‡'oŸÜÏùƒù÷cP2MßX8½"ÌbTîOþ… `VG'«¸.7ñújõ»æX´`·°3joðÝ^ 4F^-_"«?âÿhœüDߥ-œ™«h œ‚ 0µFþ +–öîÑFüEé6Ía />ÒaÕ¼Ò=Ï—É_äþäÅé`iëû3ÃWG¡¨Ô¥ñó*ˆÂé±páQd‚ß-ÖЮ;¡!ø§¸ör=)¦"UZ}»™±ÊèýÔX73A¦QfeÛÌ‘hŠí}ÁǵºfßmÌÆRµP +äê碶´>õ¡жËC± K·2=ªƒ…ªe«JVZY+W5T[T9è+Ÿ˜³ÿ¸G«‹7/ó-’‹rneéöAñdßÜhû/0I›SfÂû¶|f~!Ö S»nz¿ñ =”Þ—Úíé{þY^´ §6¨K€Åƒ;+¶ƒÉw…¾³¼vqS»ø½Ûù;Äû¥¯hŸ댹D¾+NŸ¾…vÛ÷!wîët§Pb»°?–Å.̵ó¸‹}ÚEH"›.UFË×#FåmÁEŽ/ÃZ=j¸G*ä;ä±Û¯ÑàÛ—*ê¤XR=0›”î&œóï þÜûÏ· aªËô²4EHC¥<{þÊžXiäÕ`zmÇäÛ^t;^áÄOMã@¤k+Âå$kpRü¨7èí…Ós²­É½ +ö>÷}‚ÛZñè5Ά#tŠý‰¥HIi}Ÿ‡&bå_Ó÷1vA‰0g2D(MoB"Xõ˜Âããô +/J‚¥ï}îòˆ¿çÛ…RD„žcéD H͹Ýr9 Áíì6GòL µ¢/D–$›³úúwÀû •ˆ«—µçCJ1'"€ó£Lìn¿D0~u–÷Û›I|œËG÷¿Dop ¬”Ú9Sì˜?¸‘EGc`™ë€®K®.H¬»-Ecã#ô’îW£Ì®†ÌÓ‘/\ÄÓev5Ø.ò’’ä…ÄLžÆìÄË{ØÅhIè"¦Câ`.©ò)flmuiÿ.º ç‡#ÆéHpÁÎÁ®pcËc1d—<ÆÊÜny TB­Ë1„<ÖãŽ/³tè’ÇXJ¤‰ÞÄÐæw|"›²]ˆÓ`·k)‘ÇÒ(ÑËjPæC:„K¦DZ*ù«£Å¤.v|ÞÕ¨ÄkQ(uÇT•Dù«ê€ê© fA»šF¼œdÑ]L#Ip%¡P»ÈÞŸÅ‘¥‹Ý  ãûŠï`Tµ?)ÅòRb4ÞŸ‘–£éš(ÇX®ìNZŽÆ|Ù¹¢Òr´#¾Tüy}ùxù3±ÄóʉÅø pÚÍèc•¶Wƒ˜oø1ðÔ¢ô09úHO¬¢¹T‘™òjÜάʬJÇE"­çZÑ«c|xù ï-<+Ž{ãg;_¬8¶]#n(žÏD1ÃUðÔ§?ñ4³é0'ä ¬õ3ç»5öD™ãíÐÛ ELŸÚAt+M‰#=xïöÅ7~¬‡Q…Õ‘è¯2µ¾_FùàÀý¸X«à ~†w¨†~×íW?Œ%lTØH¨ÿs…H«lC³z5Œ³ W.xâQS=ÂwiM>·ø÷caàù3±S&¹²8Æ4DÑ%ƒ]«ážn<¹ Ñ'vÏ¿$@>ÆîÔÅ59j‡ï– µqC¶ˆÓ€J.§<¶Ýí:¹ãÎ#˜ðÅ¡NC¨4š·snÖ7÷_ÐI;¨Vg§I5ž6ã˜ïMˆEcí1.k¤xœW†¿nõð¯‡8æ{D×~ž-i4öÏÑòhefÒ=Ø\¾Ù,‘`é‚9|ýíjë+UƒªN´÷i”uó=Žžâ™ÔHpò¢+¡ÐØù¨xòB‡Hö=ïù7°3÷|G³kÔi 6šmQk¶À~œ¨†>T‘8‚_iT\aÚ!ÖH4CgDz%Þž½ª‘Ä€(}o8lØ(ãm_ññ="‘+á\ˆ~w¨×¨Ø°b¡„Å6Ú>ÓÔ¢à¦Îˆ½•…/øàqƒ’€àôƒTè™AhsqŒ~w;°öIe~$’ÿôyáuà}Y__Ƹ×Û÷êìJ˜ÝÑ8÷ÿÎ\×·ÿ¸žgÿ©ïý¾¶ÚÛíÿÞÿŒ& õù5]?|øXn·Z­ÿó³ôqÿûÖzÿù3ñ§>¿¿¸¶æÙK­û‡Ör?‰}ãFâb<lÀ×ìQ>—ã9‰öÜÂÒ_åeupoúvé¯v:ÃljK}|áa-êPP¥ H¯΋cÆò~±¼ú=†Oixˆ*R!!‚æ±i4:¸Ôj/ü6ª›Í㥵â~œåF"CþÍÁÊt¡Ô8^híÍÕ¿§ê«ógKgËGûsõíƒèâÀêàŠZ˜X~ÎØKwǬ²’ué昵£CV|I'Ç1k,¿gÜJÜÀÎ`fºÊ40G@3‹ã…RÜÄm¦µ:s¸Ú«yXxá?1Hök ü'æA¡”C÷l‘“¼=vi§?±ðŸ˜xg ü'æÌ…7þó€Ù/÷³üæXP¼À˜ùNÉ&’”C þG/—Ñ{zz>)Ž]5@(Ÿ+[gå@nþNì H]Y:{P¬ü{)Ž¿ï^ ,õU÷à 6}JE]tV>å“ƒÈ €n¿PζÊ«ŒŠýú':2!–JËj -ñ¸•‘– +1F抗dÙé»ccÄw&'ûõ'?5jpü›¨’œDcuUý ø[[ÂêcÃ!`qÛ±h¥rì2Ø‚\%–ˆTzàzù}÷Bít»"cF‹EX'oe¢‚¼±‰Kç’üXO§t\9,{‘õ˜B+Rl«F?®ÍÕƒ³Û•m +OFb‡)+Q¶\”!™tXñØ‚TÃjvñ‰Õ2aª¡Æ•Iî +ñêÁ³Çš÷Þ†t2úSþŽºØ‘OFGWjÖêü[xúú1ÊWDÒ#ÜñMæ·ìŸšÜ®3X,- -¥œ/Ô™ôå“Í* ¶=p:1ÕëÿfÄ8@F›!|~wô~wÚ®ÿÛ~¯šCSS7„a Ëqf~/§rT„уÉi#át–ØÏI§rqð¬Ëpªê`c€ÜÑdÚé,¹–áŠ%‰$8cI£á*‰èõ¥=œÎFÑ\ ˜yÚ)ÒxD„iüÆB.à+À=QÓ§Ï3Og %Õ9=ÝØ­ï—J—ý´Ó£¤’Sš‹¥.ôjÞ1çþRå±Þ¦±X·TüE'Ó iÓ½œ•c”o´žFWº*tP,/%ÐóÏšÆíÔVÜyþª{l- I/RN#')õñ¡5tð=°²ºPžÇ(”FÏÛæ î~%€ÄW\"È’µµt#]WÒÑ—{ᱦU‹;Hä±Ô.®æ´^ylä©Üiï•ËÝE³:–É ld<]¹µ^Vs‹ëª€Çòv±ÒÈÞ®©YRhðå–}ê1\ÕÔ¤Ä1üØÌÕ3sǧ°¬|Nô¶hÕ¦“’è—´i¬Ž/w¶7¤18M½·Œ/ŒchÏÔuF#¯Z?µÃ)¦‹»«Ro:àî±Z 5rê(RÆð©'3•0&Åý;ÞGÜO×bR2Ò2C±]8‹Ê-ž8YZÞϮͲW·çØ»‡óA-‰­Ó„¡°È/Þ[K³Õë“¥ßÛ•æâåº?eGÛsqàé‚äÌÒÛè-'zL>³r\¦YÍq(šÉúÝn½“—qãzõaŒøÜ42?D2œIêwá|8@½é0º48BÊgÉéùŸ^7‰gÀXéÇËï‰Çï&ˆÃ bzŒÜ^v BÀ¿»j(oùqú/ã\Ç¿ƒá)lت~ss :2ÝbLAdFG¦[Ì‘)•cxô±õni$²MÛ-š‹›4ê{·JA`Ó +íŽGÂ=‡ïj’Ú–»¹@ù »Ý º½?;×Ãåj²¤£âLD„9¨Q,ZeÈÓŒ×ýRc̯\ÍMjGCLŽK™h¡=ʖ꣺¼ƒzzªþ +¥Ô5¦G½>ø=U=œl;3‡ÖÖ¼{ùP&ÌnTÎö‹a4ò<Ž£ÝÄÎs¡dT~^©3Ìb,­N£yL¹ÛX:[£‘cé~Ûþz=4‚¿~ϯHÄhTn¯ƒ¿ŒG ºÛ8ìâúl{<«‘ `ãúµ©Ñ__ÿî}ßTN"n;c^o&ž£‹©açrs\\Œnö^k£ó;÷›Âi +ÝûòiZ‰¿cÁ,ý®E§Ëzpš`‡VP¸1yn-ÝFå6—&•ÃGoóâh¶.——^îç÷.Fwãs¤rg +eIµýùt`W’fË×öR‰UQa-ÆW‰Qà®1û¯*1hMWáöTb –°#_%Æ÷ÿ)LJxZyÝx`O* ¥|³ßúùýDûz¡õøï½yû¿­vAÿCÿÓà?ü×õÿè†÷Ç°mø`ã·Í»B™ÀþÑ+šï…Òu}¾ý³ôïþçßÇûmûÿLàW'›Íõ¥?(ì5ÀNþ)Ãh´k€†¦ +^ï ÚŸyøßÉÿòyüß'|Z‚ÿm´škk†æ{šgZ¾£ù´šãúŽã1<ݲ<‹|£†î{–é9–…ßøšîXšáÛ¾g˜ `¸-h¢ÿ ÖágøêþèÚŸÍ?ç—ÚŸÊ^ÁÖ´üÖ6|K×]Çú3îèÆŸ7h$4ñSÓkºíZ®£ÙŽo˜ð­ü³\@´Ë¿„¿ðßè/þ‰tЂ•ÀÿÊÇ«k +ù|ò +ºnÔ,Û²=ßwmßñÿŒ[¾^3]×Ò5Ë×,݇ñêšW3-Ëñ5Íõ]ßRÁ5Ûò\ßômÏÁáXž_su× @<1jŽ­{>Út4A¼šn:nâþ¹/È@6ôìšQ?ÌÑwjš®ë¾fúðsÓÏ dÙzÍð+B×tzÍ1€-4Ã‚Û @F HÌÎͶkøȬù¾m×x¾ì@À7" $£áX¢yº_ˆV³5ÛiËázX²£VÍr5r\'«èÇpÄÒ<à^ǶÈýšgÚñ¼ .2½a²Ë!yf Ƭ¼aÖLÇ2<ßÕ4“ YbŽl&»ÆÕaê˜H¢{1­‘-X5€t×t€$>³ ºæÖ\À´öMö ¡Á‚Ç´Ö‘h&Bù¾å)@€ºkor|-öcÀN4Màw؆8×N0]†Ò"n]Ëàè,€8À—(¢>`°Ña!< –Ȥ<èyºÉÍv§ó}h{ +{²•5Ã5a»ª~€º0UåŸë8¤ ¦ë§ne`/׶ã]̳”¯°ê£‡TRÁÚÃH0ÂÿÙŽFÆæY4‡•E°?ahÌŒIGçÀ‡˜Ý,§æxÈ´@Ãöéè ÍÐã XÈ´&0­Lc!ˆÌ¿fEòqŸ9»ïM§ælFÓªÚybÆB1’Á7 V'æ=š«S©&¬xrP@è–kPAcÖl C¿æàÖù{(ÀʼnkDªåÚ¸Í`e=*e\ËŒ;Y̳dñ\M³uv/¹&ì7^—7d¢ƒ9Ï{åCñÊèŽ6M‚R³Ý`Ozºƒ¸Ð‹çŠÛ–õõPí;i@À¬°]€`'|cnL'w”âHs /ŠHÏqC‡ òáWHL øR·¨îó¡gvÙ kOw`^:ð +U³,d$Ö™4dyÄ7#ddê¨`<Øt¦c+@`<ŽQ3l*ÚrUýØÄ9;, +%3Ö +mŽcsT‚oÀ b$Žc +;@˜+ð›J¡¹v ö#0„ ö¦«£L@©¤(,%, É%Œë§3<¨q@ G@Häåf† õž +\»Èc÷2–éãü\Ã1]PS äÖtÏÕX1"Á¢€¡Ä²>¨G¯V&jI*i`‡x «Á^p袀\0 Èj <Àü‡ÝŠªËUõ“ÔÜäAÔx¬ͦÏ=§LÀà‡8¶çÚ&Xe* à* Ô®ë€iDŒ6È;[³À+pÆp‚M æ#ÀÔDŸ~n›tskØ,oú.W`Ùˆ«0 ž fí, L#“ªLPƾ÷„C6CÇ!ƒœ7¨D‚=á1 Èiÿ®m™¶ë*@î †ö*8`©¢ìGG­…ªÈ„®¥0³T @/×t9TKt–Çôü Ž£3žH°ñtÏàfâÒÇY vœi›1_#•‹Ñp‚Õ²L„ ê™ÜjÙlãj¥Œdø&c;Ê°\Þ`“`@]ÂZÆ0¸–¨7q-ÁKSÈ"dQPœ_!É4¤ ð‹ƒ”}¦ò@m&”å&[]¡Ë 0/Ñó`È  ꜀<·8L(¨A%¹† {ÚPØ}M²–>xL€ ØÔ£úük†AaÀ°[a7€Àt]•… D5‹Ɔ¡²eïU[Sê\.CCLØΞI Æ“Î,‚k bĈ68¢’`“{Åí:a4(@ø3, ë zæ Vqr@òÇÚÁ“ŽaÅb¦ŽÇ‡µ"£ñ‰fÆ° °°¨\‹YNįپ[]€Jò\P¾É ,/,&È<ÃTŠ¿L1Ë] •¦¸hFlÁ( t‚Ìp‘»ÂqMÖ(7À•FmŠÄ­“Fò¸rÀXz ¤kÊð ´K(h¡Ò5Üt 6øÊÁ± ÝK„ÑuY¥”­ž¾[ XTaÑ‚¨@‘êLüÃ’€û£ØÊšJK"ëßqÀœ$^’B‘¢· ˜ÈíYÊ~îG¤ëäY)t&®5.«íš¥ŠÁ2Ô4鉃‘A“’$âHƒ‘(œ½RaxL5}A+â¢ÑAÚ_C¥:ÁãBî´q ø&òÛ]ˆbþ + µD~ÌN´?äY)öìrWö·1Îh)ûñ0`ã ©mp|Õh ò¬d ‰:òh$g¯U :IŒþä¡àü)Wþœ“ð}||’ÕÄma£Œ­îÌÈö|p§ ,HÞ‡í¥1 +æ°kƒÂ7ÿú§¼3¿u°½Õ€á  ÁÀ¿VÎS4£ 2'¦0ëpš†„@¨°q +`Àh`ÔD@`Ö0ÚtBm¬­d@R‚:v)*èÆð·‰!uÃÌNÒñHÁˆírŒÏ7,DB…F¶i9 ÍCßBw#Ôÿ£˜¸ "P +˜_‡½á™¶6¤©²DœxöZ!'™.¸Ñ˜î /Á²B¶5@¨šÆ.)øUà€‚™k¢ÏŠòÈ„ à2-pÞ@¨Ëo€e(A4 aS›`ìºD‹¨pò°à-ⱃ©ù¾q\vò.•ø„ø`¶úZä˜8JTzÍ·<ÖÅT YÇ(¼ã0³’&N@lD& °7ÆÅMlD6H#¦:ð4S5ïì¥"+êá`¢2¾ 9K3ØõÀñ#7’r`þynlÈ™xhS9b‚¸6• @AÇH¤!áx;XLÐÆ7i&:†ÇÐP¹B{*°WÁd½QÅM-8ŸéGš8aQI$¨ (qß×z2U#&ZAxPšwöRQ%>ÿY°4°ã`º ÌÑ!·hô<óØ„eõAuˆ fÇ +\mOcF0–š>kÅH &º©àÑ€áEÃS<7:×ä‚å6bºËv#€Mkê>#%LÄ‹cbˆI0ð!PÂdH-M›€˜q°HA>‚Êq˜IYx \@àvw\q¢SψæGîs¬®ª2}¡<_ùSßÿiÿ{üSÞºýl-¼¶ÞvnžÂÌ…òF"ÈÁÿ~¶øô†O0l×À Å`U½êΕóñÁÓ!‰ 5’Ö‰GàÏ1\0‚}F¾WÌcšAÏñ¢‡èe £Å]¢ ‚- ‡¬1aÓ´è’Yx”D"s2žzº›$ž§@ܻԼ·Mˆ;f2ly›9²Q¡ÒÀnp•4d š²¡#ÅÄ ˆå0 H@pý€:9oP¡òÑG*›šK‚ädȋʧî:1måÄ3׊( ð‘!0 §„¨W  O H¯T¨`<`„‚%jc‡jÈÍpa&΂d®Ud×Á^ªA7¬A +dCÛݪ`µz6âÁ@³ÀÃÑŸq Àöo( [Q3#£<XØ†í¸¾j¸D# .É “fÒÕr¬Ñÿ+‹.%-œžØ0rÌ_¤vŽ ‡ÉÆùL´#à ´ZW=;7`ºàÑ´ >=,–!<Љ ²`Óú-˜€ôÑ]:9˜E ðé¨1jèÙǤW Âè–Æ¥#¶èˆI˜XC­m0|+Ï›‚0¤HýlG +ã,˜ly°ƒ¥2qbi¶jâÙK•ÌP ýv`+ƒüs6ÈÔÀ*ƒ1Ú,ãh]‚é +öˆN£`¶2;ÒÐ馵pƒù¶l]°Ü@œ¶M?ETHc´™ÁüEª kÆ* ϯÍTThå[“*†ŒÞ‚ÎجŠ‰! C HÌ*ðoàGß›†rÈb8ÄŠ&ÎJ½ìµ"Ö˜þ`U£wàè9õ±Ðr4ã\ê¦ùxxv:šw„¨[ãèó€ +Ãw×%4¤Ì®AÓ T.ÈKpŽÈé>ÅÄ)S£yA rÁ~ө棠 hœ;¦€Š8r€Êµ©QîºÍ¡Gl¢¯ ±˜4mD"Ÿ“…Þ îsƒ8Yª+@¤yg®T褒²1Ë÷ifÚŒˆ£NL¶çÓ¾l´?fã€Íc¸X³ aDÙR‚ ¥Ë¥å‹¨¨“FŽ;@,?˜Ô +ò4uöˆCA´À–rÀh`胨|Ê:(’åCòp³:i0d‹™Øú +La`­¨@´xñ\ÁÖƒ4.ŠŠ=äÁéiFÅÁ¼u‰ØÄrñ/:¡)p&Ÿ²LQ¡†¨Ü oÚÙ¨¦bÈø3‡IÉSLœ€0T¨°È‚Û‡Ò)Œ|uÕ¼³—êÿaXð£cÜ lûßœàÒè# êTØ5°O9™C¢˜VÅ$:øƒ‹•ÓX(m %~» ‚&ÄTMS* +ß:46K`L14+A訞oDFD +K,N_ÉY¡s‡ªò´ Sß¡ Ÿ +½à!ȬCÒßÈ£jÿOouå´3ŠèbêÔ 1q)àÔ‚õ¨«Ñ¼sŸ÷”©lv°«„@sÚÌiËP` +-wߢf99R…Æ¥•ª@LŒ 2‘*,çйHj¸BM ­r+0ܵhÄl ÚöáNŽf¥yg¯YPð Á2…iy>ü¦kY.ŒG—“ž‚¤²ˆnôu 3ƒw ¬Œþ.:I¾ÄS‰ETÔ‹ÇÀ$›¬„9ÕLv¹ +„$Þ3ç|2*Ì +÷<>` k¿ØzÅÄ)c7ÈT Ò±2w‰GÛÑ#Ž€`r‚ÐTXqâÙkúbÂV&`^`RÊ\º^¬ÐD˜ "‰B•Jæ’‰ÙŒ9-C8‚j•*û&‡H0Y-6!Fž¶´æ2ùT¨$™+ñ $såig.Tx¸¢Ùš´o¼Ø©£v»PLFKg<æÁPl[ â‚'n5Ö7˜†«@E³ `×@?žázF´Em†/ HÓHCE +7ø\yÈd¹ØSyâÒ¢êh·ƒcI•ƒµR-o’ìG³}M9ñìµ¢Þ5Ú­8/ü© +5 +N&¨ +óÄ$% .%戡«ª¶® òƒ©4—,…Bb¦::? ð13C¥O ¢U ’µ›bÈ¢"UL\T¤„€è;"5&Û¨X†c‚Ü0h½Ž¨G)ˆF@hÅž4ïì¥Â•Ò#c ›µµIá×p݃ŽÅi°“|ÍC}ÜÀ<2Ò!¸_oR+æ¼y˜r +Ÿ°Î¸Ú 15WnÛl@tM1³MhÆÌ3/þ!Š3]‡Ív¤F—„¶ÉM +ø–™pÍ°gQ·9 `…%q4Ú?¯í ]T‹©¤ˆ*¡‹ Õ$•W&‰ßZaY·ÃJf¢Ì´Âþ –¿QŠHÍÒ¼E Æù°v‹LÀNèvý9Ü#G…Áè¸ÖûË „R46…Fa^Íd¢ððxÛ&½"Ð=Ïw¬„f‘oÇÊš Š˜¡³DX`R @¥»fj0:KæDhtjX÷m$p¹á× [#öž®BŠžX6ÁRsƤCÆ‚©;1…õX©nȨoØ X(EÂéñd†` 0¼ Ìáñ9š©ø…“ „ŒeÅdÖI¡6!õ5 …íÇC ›R#G ̲Á°©&¥ÔÌ/„‰uÔài¦²3×t #¹Ù´ñ„Ech*W·-ÃWØ-Ј ññ±¤#@ŽÜè×4Móƒ¶û‚Ôê:`Ô”¶ƒ¨¨I ‹D"¶ÀºQ¡Ù6bMzgl)aÙ0ÙÎ×)ì“FjãÕôãÝ#µcé¹jq%ž®dG 4^ eÛ*éu_ I‹fdH«-6 ¬–¶Œ¡pÃS/V)at/æÏ·‚å f¨„g–˜.XèV ‹Tí訤“›±:þÚMÑ( ŵEl2FE-Ð/Ø k ±ÍM¹Y˜U3‹0H?$0ë®y”Ü„r˜ít¶÷¯G KyXª0°ªãÊ¡„'ü,@èôö…€Ñð +‡[7`ÒÐ|Å’Spq,GÕ¨ Û¬"3³f:YxM^ c™jö“Zyö¶MÌÎKpž„F´«-Ï6#sËMyo!‚Û{i#çˆÇ +†õJéZñ"½ÉÂ<`³à9·Úò[±wÝÔ°s:BKID$˜°Ž,ì“ç” ÷¬~s9“œû!:ö:Ãv,J©‘0ô-ðóu0o+É€JËífDúÝhجÒÆ,SÝÔG ¾é‰l˜œ‰ËCÈLÀ£Îb’ø +,ä!¹ý"5 òšÝìûÑZ€ƒ·\ðn`:ÊL7¶s ÁφÑãE®²Œ"£ÙÄœ0B±.Í2·ñS× Þù(Ï=6¢“ôp×p—Të‡õÕ][ ¶dð(šYc` êÀåM L¯7Ò?ÐÚÒá§juØHUµ;¡ž–A©“?]Of„…°›mév8,¼×!¶¥æû‚¶²/8E\¦èÔZÑžÛ@•›XµîH¡ÑXÊbÞÕ±ü#ÜîÐ*Å"ìÈŽ…®Á4Å3Q6BgZì>ó0׌]x»¡<Æ+–™ï ͱÙ,ñ?™šÉ’]Ú ·?\,Ÿ6­$×›5÷ wÌ1v0äsö®ñ°¬@d3¥›çG:F®×8å"l)†ìO}\ÎsñF¯ˆ÷ÅVd Ï&(u “ñM Œnoà#—Å~1ÖºòQE®™0¯[x¯meH‚Ø}¬#£S”š}̦ É¥h Å.6ñN2±Àv[ô¤>9˜Èb†H‰EçáAvÂ4¼ØˆÆK€uÝÓ(hëc*XÄ1žS3<š4{v¦ï†¦-ÞábŠ­­`G ¯…¥Å Ak`xß´˜à$Æ5|?tÈ=«{±‡…òH§þ/nÌ‹M% ©ÀëJ#7E¦NF3K¼˜È>Þ˲o7HÍ( 5ÍŽ,z¶¥c: 3¤6{$Ç6$yû×ÆSÁ¸»[[~p±P´„èòí¾°‰hp_À“ÔØ/)$¶òË›J†|f Éà}Å^tn!¶âuG~+ƒV—7ƒÅ·AãÁ‹ºÌÐöM~‹ +g~pm,¹˜uáÞ@¢t1DÚJn†oÕbÅ#ÿ\àBm +‡·ùX¢£›ìAK$ßäVØXvlføx­,ÈbéÒÔ˜62/Þ e‰YÈœ­! •Ú™A“®¥f0'\¦{Ç5Ø-„Gíñ`¤óÍ„ß…à˜ÐLS +ÉqYp;ña}Œ†Å+"4¢iŸûa2},TV—¶2ßLh î—I‹e„€VlŽ†LúäÚ«QÚ[·@Û†Q;i5𸋉Zá„M!vÎý^$˜ˆ>™Úüí±ä]Gxߤvv¶n…–(>@Ò­bé †‹NPu¼Å•×ë<ÞÔNò™1e8´‹Ä>ð‡1v1;%äÑ#tÔ®ÂKª­ž¼Æ&µ}µø ·I@b OmS“Åá`‘©ÃypžŸ(æÉÜÓ‡ v‹J}xc{pF‘Ÿ0Êür¯JðFÍ +zÑ ±y +]* dLi© —‹,ô㹕à}Ä°YNÎm +W…®’oGô áX&ÔLXÉÂ:¤ͤ˜£¨›N0Ó°Ô—´exA˜¾£’.B rùÏnAƒu?6†éâ¥Pd—yx͘e)`ÁfGþïjŠSÌš’Àšj047ñáÛw]WGïXƘFF÷—}âå¿Îyò´œ'¼ŸÉyâRaÒÚð–C." Q\†Bƒ5÷ÓÆÍ\©ó +~vb+?#?qÄ"³™_æx|&zS±{%ÐĶ5/ö€J$ïÝer\‡±yÑGæ MKÃc£º¤{v'nVB3ÞúZÕð­HŠXèy0?—"ÓQjÆÛÒYY 8$ù8tú ryZä ‡ÏèE>ëQÍxig;¾€¼ŒE«dCq‹ùÔ\TÒs¹å¸'Ëg°J ,&bDÖ‡Y>Ïá”*®w,…¯0úPšŸ-hT/¤e(üÞfýBM¢8ž$4Š õâ=øq °†L­ãýøFԥ˦$òjˆ¿ö1•‘Éwð1+ °˜Ä³cb‚ øwп‹ãbN爦‚\då´+?¶Csð»>u?3Û^tv…m/6ã›"ñ‘­iâq¥›‹ÍÂÁ qL›éͼԓqg4cÈO‹¥º<µL‡)4g6 “K£;“3cQ{ <ÆbôMàÑXzªjšùîi⛡•£M!·Œ&éeE+×wÆÜ™ãp|׋Nl^tcDsæ[ƒŠ¸%æ2‹¤vWH.RpùE&žÊÄ9¬Àclz>ˆ`ëNœ‡‚õ|~‘)s)—b„@VTy>Ÿc$¶ã2\’‘ sYF`[Xl>Ë@x58ë:³m$Gs쎦ÔÊóFÚ’†9Fxá;—‚˜K2‚ÁklªŒÍ6ãþàÒ1Fr+“_$6:\vâ5Ù¼JäÜ0^€Ã6†d2Œ0'ÎcÛšb›'äe 0QÞ]ëøP/¢ôãÔw¾·{àAn6³Hn‹äS&¯È¼XE‚cf³@Ù6‰`œâÛØé4S(!$Ù\ö;¯…F‰r y¬ÁS[%·i˜#©\ÜKy—XdÕøÈ€Ïç™5.¥]e>­È¤÷iyŒ Ï&²ô÷øvÁ‡ÔÅÌ"[ˆ]H;C†àv&ø˜ÊmAÛ¸â'Œ{1y"º‚æ5›±å‹F[DN_™Œiq³xxmX5CÿWÄ·X; _e’‹¤fWÌ.’ >½ÈâízÏ᲋Ìëõ:Bj‘Á©á§¹óÈâ3™Er»Ë¦©šC=€msX€9g"Y{¦ÎFøÌ¢4YÂH·fq‰=ÈxýX¤ËpăÈ×¹ÚKÀk01䜈ËH×|J2j|râ“¢—±±ñròÐÂ<Ý`li¡ýŠ¸ÜA±̉g#€ø~N,ï¥fR®Íp³ØîpElrA¸â»°|@H>Ñ0‹·ò¸ÜÉnV6‹mèÝkìÙ2ßNÖ)HÁ纙À`š‰¬"vhžˆ‘7î÷ƒÔ‹Ò£¡Ug«`˜>£03·Aè¢KÄå¤IÃc§! ×W \*Õù¤LUb´¾nÂD0w!VíÈŒîó->#SLŠ$fÁ$Ä7Iž«S9•ëã ̹1Z+Lr$æcpE«h‡±Ù‘ÐÇ›|{Sn·¹Ô> ©Ž2/R׈cä°¡}>‘Chw„D .±G]Âa‘®‰u»Òq®ƒþò˜YŠ}¸¬Í@#Ÿl’…Ë.>ŸÈáà•+¬%àJ‰W#œ9áYA=.)æ);!­ÀNák¾]ô-!‘CðÄDÕk”&£\dy +]*‹;»p„›P#q‰gß)"$׃3±ùD®¸Ca\"‡Í ^I—(æ(ª#¤Ÿ›$õÁo LÎå9ÒvTŽDðo@ZàØxc2>ac¡G{ŸÄ4<ü‘ òËé`cb‚ka¶‚×~úf þç2©¾›žÙÛ.Gf‡–ÙA´tFÖFÝßBs¡Pš_³¯ïÍÛÿmµÇÇ ¥ÒÎícë }ûïµÕ.<~ßþ­?·ïï?·?­OhùóØn}ÿ|´[¾Ÿ>þ¿Ÿ„à¥Rc{¹ðÿ†¾Ãº endstream endobj 57 0 obj <>stream +%AI12_CompressedDataxœì½í’¹•%øþ±?ÚLšYe;¾íÚšeDdöhM­.“Ô3ÓÖ6VÆ®bKœ©"ËX¬îí}ú=ç\À?""É,QÛ¬Ñ0ÁLfàp¸¸Ÿç^üÕÿöÅï~qÿõ›zù‹p7¦¿ú«ÓÛ—/Þ½yû˃>=üê›o~øþÝ[~ô³ßþüàÊÝŒF÷¿ª_ö†ÿùåÛï_½yýKU©ò‘Wÿìøöåëw‡Ó›¾úã·/^ÿü𳟣ê÷¯Þ}ó•Ç/¿xûòûwo¾üõ›?¼ùò‹ßþê7¿ÿòüâíÿ8þÍÝ‹W?Ã@¿çïp›ÿÚ§¿váàÜ/çp¸ÿ[¶xñú_^|ÿý«ÿ—õ9Ԁώo~xýõ«×8¾ù~y˜¿ÈÎÒì¿ð9£ú?½úíËïo·™ïªÉ·è\ÉQܵà¯mÎ>áâó›¯~øÏôÅÛ7_½üþûÓ›oÞ¼ýþ—‡Ó¿½x}øÛ@Í‹Ã?¼üæ›7ÿz8~óâ«ÿ1á-¥/_}ó/äÛï.ðõÜÿÊù/?¼úæëßüðí?½Ä«ò)óãð¥ºüûïѺåïü¸|ù«oñÉï^¾{‡Aã†óf,'ÌÍ›oÇP~öÅýo~ÿw¿y8øâêáÄ×ø7¶²|ö_þÓ¯~ÿÀßû7ÇMèü.¤\ +ž;–\Þõ|—Âì]ls*¡ÖàðIM¡ÅVjòÁ§oßx¾‹ÁçXš ¹ÅP=>)¹U_|Ì9§–>iÎ;Ü`N©µVòÍáÎhç}nè“å0®¬ÞKˆèÛûPZX/Ì)Ž Êîi·³ÇégùÙ?þöå^i•cÅý·Ÿ÷Ùxûæ»o±$¿Ï* ³»s©à]Í ã våï_~ûÝ7X·Za¡å»Ä–üoûGo‹õ¡v¿p¾Ü¥ºô_¿(1Þ¥RñÚg_cö‡ŒëìÖeFS¬k—뮉uº.Õ—ÿòêå¿þòð›7¯_Úz¼ûîw¶ebœgûi5¿ýá›—oÿþõ«w¶ÆîÕlAþ훯_~ƒöËõß¼Ð:TqëOkðûoÿðòöÙ›o~x'ÂPÇ°àýâß^r©:»Áß}÷òõïßügñ¥ÞaÁ”ÙaA`1xlBßîòù¬-”þ.‘`C‚$ü¨º}ÌÌÆ[Ú}yÞcܼ`)|=õwo_ýáÕë_6Lo™½m¶¿yûêëu¯¨öCOx‡Gá—sÍÏÍ·ç~ÒôU²KÕEÿœOìõàM¿{÷òu] §¿ÝÐùîo‡'yxýõéÍ·œîïI±’_c‘óæV·ü®\þÃwöVô÷—X_¼}õš}N¿QMýò‹o~@Õß¼}óÃw¿zýÏo¦Ÿ+øÏ/¿½ÇâùúðwÿôßñˆºhÓá÷o_|…ð÷Òdü»Ÿ¿·»/¾yñúÅÛƒ>Ç•öá¯_ý j^`@» 5ëgîôüòŸAl×kíÓ‡×ÿòò›7ß½\?_>yñúëÃyñö»wÉx»éAŽÿŸñ´XyßáåèYÔäâÉßÓ`SõŒ½x÷G0 —¯¿þ~éÛþÜ¿QûìÃýýî+®Á·‡ãÛ¾ÿãá÷oÞ|³t»¯ZzïëS¶ÿiÜã ]ðúï^Û º¾Sopy'¼ŸÜ]Ðúé; ò§ÜûéÅ7ß¼úÃÛßýñÕW·np£~¹“Õý˜…õoßþÓ›o^}ÿíºž6Ÿ|ñâí»W_}óòwÿöý»—ß>{r_¿‘{b¿·ÍïþõÅ»¯þøëWÿôöÅÛW/ß»û8ÿüêõ×Xû¿ûáÕ»—ë zóíwa¿ûã‹ï^ê1ÞýñQ-·t˜¾éß÷_üâ=TßåÃñõ¦þoÞ¾øú +DÊ¿ýúÅ· øèÜϧ[’ Ž_Oÿ8ýŸÓ© d”„OáäUÊ|šÇ”3Êéȯ{•†RQ +JFILjPütôGwœóý#ÊÃýùþ„b×ñ«¡T”¢’QJD (Å¡Ì÷s{líÜÎS;¡ð–ºVœ·¢”–U ›NÿWθâ„&¬L-4WëC=ávµæ«¯®<”3FÝJ)‘‹+s~ÀÓÑSÉ)C*ƒtø˜Îé8a\5A@M!¹øðˆ-Bn‹)B–sx'cÑ<âmºâ±ˆ"–ô0Hö kúˆ uÆ2{Ä{vXr /aù,†¥xÄ‚11;9àí&ìïb»„×ñJ»6hb’6qé]4mÖcïè,ÀÎfv4eì4ý¨Ü¢Üç§ã‰_g””G.Š³Cñç€ÂÙLg¼¾3^ѹž¹¿ñ4¸ /á×Ãt~`=`ß<øì2ÍtzÀ;À{}¨xxø«/oI¾îqãÚ$ã‘/Ͷ‘m•¾§ú—þˆÛø>mf1u˜ÝÌ{‡)ö%¦¹Õ94ï×Û`i?bˆÇÓ½½ÅŠ ØÓë­´w7· 1‚f@´ú@i° ¬+›NLèúXÿˆ¡$P•è±©¡Žç…éN : ’?êãPÄ}ê ±T¨XÒz@rÒúTãõ,ˇTãûóÙgõà¡0e(̇¥ÎA?à  ¬|ùiqü~Òu•Í6f ×eÚ”½Ã–£³!9¼Í +² +<›¢mToÏóûbz×{f‡‘„õÁÿïkÏ +~•3^k +øzföqFo)UpžTtÏ:',ØÀöª> ÉwuŽ¡ÔL /5‚Ÿb›ÙKܽz÷Þñ¸;²ÆÍ{øéŒI墨=‚7ŒW¬ëR©óP1Ä’uw(æ ³­ž† ‡4wvµÉ&æ·÷r¼*lè?É›˜óu@óúz~ÃÁ›9žArȃÏç›2â¼]ëm÷û( ²þEÓ_YÊž¹F~OËÛ6‚ÓÅ_£Ø×þ/7õÿçµ@ éE=¬KHYËi)&©L]4jËGmWê®”MÉ›²ùk½ —o+a) ;J0¯¥HB¦¹Ô?i&çüˆBöMùHÙ»¶HpÎÁ"²ëôcǦJ‘Ž”ïÅtJÓ%ñ+Nªƒä+GA­ s+Ž]ÌnàÒEÂvŠü +Ñ£ð5=¢Pô¦ð}‚ÒÐ(‚OxšÂ°_j/yœù#Êrùrù‘ú…¤sZ¶ ö]””=¿œÞö# +$ö R ž#Õ‘¾Þ‹VvîKÕwIžë顯#)>Ë¡´Ç\O}Š}—æÝf¾ð¾Æ9?<<œ!nÝ?´‡ +i'Aöñ‚fÈDŽ•¦³`Ä)!>BÌ:A0‘4T¸ ã¦æv’ÆV¥¥QG“jÖ•2ªcTÃ’T/OaF +×Q:V•f¹¨DMZCÔ£lõØÊ™Ë#VÍIk†+&K§¢åÿÂÚ¹ëU•iVnYÒ®lE$iXXZÎpL’®å¥mqÒÏšðû>×)D̳·9ž0Á'MpÓäRý‚èbÓª)=÷ÙlšÅÔgÑuêð ù»ï:Y6vÕÌïæ«ïÌýÞ|yï×ôþêÿõ¹ÃåcÎuŠE¯ÅíŠß”°+{S x²ÿ¤’¬¥lJÝ•¶)÷»r´2õ_N›rÞ•í×㶶Ïa_š/©ÍTœMuÎâZÒ8h:’ +}_{Ð@fiÒ¦KS›¦>MÓPéZu›d9:Ê’t6õZ +¶©Ø^¦§(CTîªv•±é¾+ÜÔѤtõ™V®¡?§E®‹þlÚóihÏ]w2­…®’K–ò õy’þ|\ôgiÏÒŸM{ö‹öœí¹.ÚóП¹Rl™»©¯é°¬Ú¼,̶,½Ó²¶—õã—bË¡ØÌO}šmJÏ}öl¾Ä`ÁÝœoiÎ ÑÓq/.}ÆFz-u æ‹:,./»m<2Ëx0ˆI²B¸Çû=á±0–YòtRS Ÿo‘™>l’¹²Èä÷Ùd¦çe>l“‘åDV“I“nn“mm“|ìÓ«©í³j3j³i3i³h3§>yUófsfóµX5ºÔÓåžôóОõ}ë4qˆ,t‰4÷1h6#HÄ0ËïQöÞ,ƒvÝYÆ© Å €8o¨Â†XhÕqõqã–N›¸<;ý2®œoÈØײõ*a_c~g«J{ÕØs¶ëÖ}×-W¶÷VÏû錆¶©j/mSî7å¸)§M9[ÙÎ4(Õ+I€ŠZÓLõmåÔgúÜWÁãXZ'60í”±O¸K¸G´C–ý‘°5öc»-ú¦°™o¥öYÞ²’Ón]l¸¾ú'Ãt~»ÜšïÐMæ±Ì×’Õ6Igî%$Ì]ÐŒŸ$’µZÌ‹V W ××Qgg/7Ÿ»äÜö²³Ô¬!=ßK~.]‚¹¯Œû«ÒžQêeÙY7 wðøí¦Iðý­d£™[ó\kµ%ï…þ í| á¨ÈúGÑŒ•}™CU$»ÏÓi5«ô³³Òö´ÚÚ„Ä9ȶdcªþŽÛk5ÖüÄÆ¥w…}™f½ú–÷A `œÐ« ôZqL º% ¬ÒâdyÕ†Vå2-îsØ#°€nZ¶&YŒö=V¿ŸÔ¨lMAÈ¥&‹JР䳇Íq=ïßJ +nÆ3FÍÒæ¡_Î=·8kD»‰â“¥1–Y6JŽh7uî¦áï'2ž³üÅ›–¿øsô¿XþºåD/JfÙ5†×Ž?ÒEZÙœŒvQL¦lØ‹±²g2GÝOÜùÛ½8Øy#F®‚d”U‡Âä'‡@)‘r’íФJ“+‡dIÙòÔeˇî4ß^H²,@Ää¶/]Ðl&lN’7Mâ|6l þ+2bûÙq÷ÙöÓ¥fÚ5¸ï÷n•Žœ`{†à{”8\¥ôPÝ¡Øa¿<ÊV|†Þ7ìÅEãØmÆÃj|6»ñ´H¨Y²i0ó±üÚ´5œeD¾îôŽx '$У@'³*O´yi+öçܱ?÷c˜Ÿñ³à}Úã$û³)ˆ+Þ§uU± ¬Ì;4óø“ã±›Ú¨¢Ñ¢‚‘M²X›×þJ÷~PO’Ç£dró„øî éIéV:ÝÒÝÓ¦/Êýe1EçqQjO}^ï»ÂSu›Üg8Êþâ5ÏNî…GÍö˜ï“,7÷“¦½é±‹&?ËÌsCgÑR¸Ö[¶š Ê´S_V&H›4;€9Ìû¢y0íW&+­ - {A°Œ&™¶Ê‚03ŒYZ0?òálðfsÖqg{6ðg(Ó±ßæ¸~Ý/¥-¥.e;/yWhµK“ý§o”ðT3úxa¬vÎaû¼_fyÌô˜í²˜1ò0½N‹=vXi‡ív°¥á¿\áñµŠWòbU^ÍÍ«z5NoeˆÕ˜6e±~O}m²5šïÍé[Cû|Þ™r¶VíiY”û¯Ór|¢ÜoË´ÿsYá,[kÉÆLhFÂo"ô"pIÛ¿M²…œn ·fÂû™ðÒPh +ˆ§ ¾j‹®J£a[,TÝ6Õ~UmŒÂíˆf[8ÊFgÚƒÜ3ä'j”ou+oåñ!‘; +ôØu|’LNgåñ{yxϺ·ÈãAÒ8t¨.…ßK?Ó_ÁÁCÄ 5•'Ðï!kŸå¢ LáqºHp>ŠFóÑföØ粈EðÅ?„çèÄ=råÖ§Ez]a‘ç‹Ÿ+rȶçé +ið°üw„}Ùex³§îÎ~”Cû$‡vÓÈB3l]ÚD1Ü D<\ÚAŽNí‡áÓžžáÔ®·6q1sºœúŠ¨¼;ÔXà3ß)×bÛiØ^ÁÚW‚Û^n›:€`/¶­BÛ*²™¸Ö…µœ¶Jh’ͦ Hå±ÃTÎ]êºï3gs—;`%h5‡³ò°E­LœN³õ/²OëòÎz»® "Ðt-½Gº®DŸ©ó¸UäÙŠ:[g#Ôld¿Ì­3R1qŠ·¬nqIïóÞãü¸ó úëؼƒiï*nâÖÄ÷çð¥[øxQîqiý­M½³­ŒlÔZÚ€¹ª½ó¹!f­Ï WDÔÚ¥.ÀÕÌÈõÍuŽÕÎU¯·£È¾¶´óéf½â¦—~”+ï^“Áï³¾Oú~çÆ7f°Odä—ßFlË¢¬ždþxì–Z6Š¬÷2P<ò[ˆ {Â0–ÁÃg±æ@–<-îÖ­³u¸ZÓÎÕJÆOU}˜ÁhÌân»ßzZ§[®Ö[|âiGKعXÒÆtX»º|2y~õÐl‡²v£!M†ëp2ÚÃßëÑ'™£Ì€Í„´³|]5ll]{_ÐÞjqi§Xtü­”0Xúy~7 ÄnØÚâÂQVÌiAIæ^°Ì¡ØÄê@«Ë™d‹ âG^íç öî,ÞtIXxUÇ<‰U%-€‰GžÕ¹–±ï^ü«uPV‘5uNf¸<7Iîà¼ÎS8ØXîwV€<[c}¤øï°ñ ãÆ/È-ºZß:£ø°Síƒü|ºÅÐßç£ù=„;,½°ñ ¼·‘œiÆîõsªn†LÜdÚ/>àAŸ²ðõq®Ë!c×aÊã%xº¬î|šã¿õ@…ÕEàÛ!ëDÿ§ƒÞAc²‚ˆ6Øn™Tƒ/ynÁmò#•ÄÜ.a^g : œâjÁŠËb°ÅÙc? ww òÔ!Ç»Ì;.oâ“ÄÞo˜+wRÅ*ç›Mqö$ËÞµZšn0‹,ADôö ^: ‚(H,nÛ æ~ë{Kº*íÂ.†…›l^ÎOjXçæÊ7Ý\™÷°qs9À“=iú´š 9ƒÝ/lÈËÀ_e“7Dœ;ïçßSgº÷ƒ¶Fšèi›]8,¹+ÝG§Ž¡hņt\Èta‘hÝ"1ì—Öˆãæ„m³ ²jƒ4ÑÑcÁ¶¶ˆºØ"Žkô—-vmØ$¶V ¿·#¿Xâ½v‰§YÑ4Ç-šæµ¹"5/‘™{(æ5³Û¼¦+üå颜/Êõׄ2=ÂÜ!1?PvÍé¥ù§—.¤O·c#ÿäòÚá*®žîÜe½£Ä9“Ѓ<«„~~m†Ô=zæÌ~œ$U†îÇ^ý×ç r …¼„”IlAAN´ˆuºa²ù †/{èæ0ßýËYBP“@D:bNaßѵ©fÂ’j·ˆ¨öaÓR]OxeÅ[óë(ÊrÞèX=›ÌÖ9-ŠÖ}·xž´ÓÎÒÿLFtSVMuöúûnµ" ¸ ®4?AXÊCœ7eë iÛ²ÕÑ×€Õí×U0(î­B7uËÀ­âŸYvÑÎÓûB¡ŸQòRÌ“¦ñËŸ«ü…vx­ß®NôÛzíª6† ÿø”˜vx‰ñ{é?GˆXYtÍzã{xÍËêLO>ötñ[Yê·7(Ë0znü “~õ›Ç xà°èÆëÿñÉÿwŸLË;¹i J·[n5À Û¿F÷…Îî.šl‹»ºÕø|3œéâ¢ýXo•øžÒù¹ÍßÓÍv„·ÇñÞ¤7€ÏK™þ¤1½§ü…v¸â/:'Øœ±y2#ó$KFå9È¥gù+èlì‘x4ß÷8µƒtE…¦NŠÆWìeÙû›Ü=ÛÄ[Þ¸Óý®s +]JÔ·ÒÝ„ í›Do—öÌR·eÚÿù£K¹,ÓõGWþ";\‰À‰y…Å\И§ sÅcÆŒ' Ô´ ðŽ= âvˆ‚˜ÇyÛmvÃÁ,çò´Kà¯òâSÞ@é†3yëN¾r&O7¼É ç¯}Ê7`؇®î¢¡NK¸‘n|µ¥ÔMÙ£'ó¦tê0_nì±pUžb¯n-ÓöË?÷þrµvY¦‹>¶LOT<Ýv£Lú¥ÿ u¸ ˜{q*›\]-ÏÄÙ­2ŒÞúDb!Z'GÆÍw3ÃxZ‰!ǘ˜8ëFìÓDIy×ÆÉ\6ÇÚÞcóéÆpü~²ÌÒùàÂ!.ÞÆý‡£Õ])áT—œ»§Þt÷ÔÏQMŸ£š>G5}ŽjúÕô9ªésTÓ稦ÏQMŸ£š>G5}ŽjúÕô9ªéù_ÿ“tø9ªésTÓ稦‹¨¦m4Ó6¢I$»Ç1•ž0o´„w2õ䕧‘õp—ìOøš>îò«¯3Gx¥P-Ή5`ŠkwLqq¯@>n<˜»¶É@ïv/(>*jB2'jÒœ ¨ðZ‹˜±¸ ’°y.}ù´åû¨ïžñ¤EwÖ2|4Q`|k™ºžq|ßö6nêkú¬³NòÇõS¿­±Xu‹uîV?Ãzšf =Ü¢‘²Œ}4nœ Žåä¦.S””t‘”»9ïØE‡Ç•åeÄÛä¿œzXÖygmanã³VâØuQni1M”(tò3œ¥ÜC1¨ç„yÝŠ ŠsÊŠf[b¥†Qu vFÕ½IuÀiG¸—Ái·Á^²˜Ní%;©½Ð*9ì,ù+t¬=%.“µˆ•­BÙó¾ kÂTš@pï»ü„y½ïÛƒúàÀ[_ÏÏí¶À¶›é(?XnàÞ‚ýÉÈ´ic!»QžN{*0íœ.ÃÒ– ´1šžÄ­£5-_%‰w] ‘h§©ãˆîðК*>.à · €dÏuÂxYD§.¯ôì©=kê°¼׸·‡MÜÛ%¦ãªã©È·tú@äÛŒ}›±ï‚ßs復ghBôû © 鎧á~ʈº>„OPWÓfÙÿêÆHì}L¡‰‘ÑœŽÒðÎSvsÄ -îX½$s,^Ý b³ä„ 7ccëÜ–ílèôc·IÂèt¶‹«» —ï ¨ûI ëã<¬í¦‡µý\›´{Xcåi)çŌݵ“ÅX·O+?tz·-ãHs…žè}¤{%õœk)›RûOþ_'ý2[jÿ¾ï‰âﻩ~5¿šùþÔ;õTò§màòð!>½g°[?Ór†Ìš`uþX9mÊzÁ*PlÊYÖÉÓr$ÏúFGÆç5\eI3¯à†û//‡¬usëI¹¼o•Å¡‰ZÎi²œû‰3®Ÿ5s Ã œ*šf+.u\ 1!Y 1a¡t†4ë´“‹!# +ýp˜r{²ì­~49¹qð¼Èx†¸Yu6QÐ.gý1Œ ôÎÝeÕ]×µ¬_y)˜8 c컦îx²kìò›?,(Êsw«,å}_·u‘§¶˜JCUÆŽv¶9qÓ \åÃWyÜeØ"*ó¥¤ÐŹÝaÃUكݮlQZÓÆÛ±ñx, ´ç¸=vŽé†ßãÚÕ±º4ü`m ”’Û•¨»ƒ%ŽËÁËÁ¾[Æ×c%êbò‡ö8ݯÇIÄÍQë1ÃN¾?$àÆ1f+ŸnŸÐχxöQç +‚¥@!æÑubÁŽÚS+r%Úo¡A«ôs[íGúír;E„ï¹²rùÁËè^ܦOþ”ƒ”+¦šœÓù~Ìx=3™ ³^;Ëü½=¦-öƒ ÷£§«÷zñ‰„»ˆ­O8†âÿþæ©ÂÞíê»×møí·^ûã…ËÞ¬6$Á«Ã¾uû"Éã8Ѫûë6–Ãõ$âA®m„+>õAðú~ñ´C ®¸Ó-Ît”^‚F÷Р9Ün«ˆpÜœ\lŒ²-'[1Š2p£€ÚL‹X³Ä½,xš•Ï—e/Mõ"9kÚJ´Á\—Ór¼,ÓõG?¦˱ æyegE0‚1Rç~BYÒA«]Ô¨Š‹òxL.ŠÄ +òòGè¿÷CœH`¿³¸ìq9Û9/„N Ó{6&¨#žûdËtE‚…R9„E¾ UÁB“Æ9dfJ±FDø 3Â0"ÜëDè&\ ¥(;gX+·¿òÐ[Èp¿œ;7NZÌýì9+véÜ?\ò §Üðëíá<­{ÉFB£íY…[«ÎÍÂÛö¤Ÿ¶1´—Ë„l Ì’‹•Ö2fÕÅthfÃäëÎpè{~ë)²:Ä’ó0âïÇADôøîºÍrÝŽÌX§nxu›à{;ÞE&×i$L¤¡!÷øÇûnríÐFÙG<›w•†`2m$“ûÍ‘Wk^‹K5iÜ]F”hµO y)´jA—_{íçF™–µn•@䟛ÿ±—›Ì.”mX<ûeƒ³5{üðì?Ê*?/`Û·€Û¹µˆžq‚ùyã韗ÃÄWì‘I‡k÷@p)§.›&éa‡É{¬”ïI:w tSW†"UaêpÝÙ]=Ç%'š`ĬÉû¶g˜býN‹öâw%ìÊuÐLºQ4Ìi£6­åù_õ²L×ý˜"š²I³aî–÷6öôqcËiNÓ²ÉÇNãH§õرqðXÝf·=Ên{vìVåX•ŽEíX²Ûe·=ÌnwœÝ$Ud=Óî<|]1GÃãíBW©Ò’qoÇÝÕ©Ÿy7ÎgÇðNý¼QVep› r£=NËQóÛâo–kõ²`¿N—±{?®˜1/ttŒs)ÿmeÀ{;ðR \ÍT[óÕ…HxC"ì2átC4¼!Þ oɃÓ—r«L˯W¼çfùà×ôÞÚ?&LEPÊ€ 3´ô®£›†NýÜ´sêævˆ¤[`AÞ÷*¯û¹ûÛGF.CÐ=*íM<åË#h/að—XøÑP(]ðc =æ¼ #ºéO²‡Þ¢rV'Ù5  +Ø'µbÜA¿Và?å Ì×1ã*lj¨:9똮’g.È™¼¯òà/z_°#B¨òµu!DKÜ·;¯Œã¾HÓg>î ÄTbõl|ÂAè-”æSãÛEËlörÚà!?µæuZÙŒA¦ìHg³«ÍOàµXÄy(¡ßyM8ájŒù¢ ïæýÕñ`?!}”¥'¤[–|ŠŽOÏ“ØñÛŠ‹ØºIŒÆÈ°=HÆ F,’”rs;Ÿú™à¥S#7 Ý~݆díˆk£ì.¾>°xwïN4Òõ¾ã_ßsø«.Þ)¿*Ÿ{Øz¸¼NFÑcvÈüžâ]¶\Š•Z¯¦zK 34ž”æ ó ±™\à +ƒR츀N¹çÝ¡squ5—|7ƒÎoN¾û÷¼«ý7‡rÃþq9Š8F(þt°r’›ÙîåéX­ÖÝl­íâFzS·„WuïNÝä¬ G A›Çþtƒø¨=|ó¿¸óÕv¦ÿ¡hØ÷çè^ó †i»FÁîˆ^ã_ŸqDôt#1bìÚuûSŽˆžÞwïsâI÷±†û Âm¶¼ºtk¥Û8Òã¼bòGôèˆ ]¢Gû‹Ÿ;jÎi´DŒæŹ[ûœ4‹ís³êkÇ:C}‡‹×[Ìç$vîžE.S‹ÜlÏ‹Èœ/hÿö]Fcê­yⶑ˜—q˜Û̹泗 +ºœ61—uÞÇYîÿº_¦`ü¼¿ñ—(‡õtpü±1OE4îâé™v®‘˸wŒ¬QŒ[ŸÈ8ˆ»,Gq+­ÐÈÀþ£2 Ýνnð³é}™×Ÿ¸=¼û2&pØ£»¦%¡Ê6ð}§2oc/Ïd1šìý*¹Ê.µÊE^•áõžûK_ó©§Åk]7á£Ýõ¼¸ºû]\Ý>ªnS·ä¹½O·¦ÛGÒ-1t»º:7í"çös#Vn · Š»ƒÃ×´ yÛµí"Ón£wõSõŸ +¸W?Íý?N*¸éÃŧWr»!Œ<Œi=-ì¤ã0ï— +ÒV"ØŸÙ1wŽ²B¨Ò‚1ß)²ÍÝ1÷±Ù肱1†¸_΃í¹<.ùޖ˹‹\#“Dœ÷Ù^÷¹\ÙÔ6Äiù6¾Åûÿ\|fÇeÎeúq\fŸ k?Š§²a ú=Ʋçz ”]‡ôtÿyŒëC¼¥ìxËo¾6_bÍmlÓi¾3‚³üùè–‡Þ;·y’x0üG¬TZ€†á î™y}k " ³YNÊÅy 27lSôˆrl1 é ÊõÉFðq´Ëߤ]žV‰5ÃÏ|iñY²>ch‡”¯¥¶L06Ú†ÉöÌð].êR‘YG7±qÞÚ~IF¦®éÑy§WÕfë™ñ+»Ëoe/ö›¿ÃZ¿ÓO¶67m«-8²mDDHépMHº\H©póä’:ô¢vL§Ÿº¸‘ÿN]¢Y³å­;RBŠ¢ñl/®ùòRO©iI5¹ñNÛ<]Žûÿ?°×žá©“öIbŸ8x¡—´–i 2D1ã×-_â“jéâ@€\…ïOÛø} {_YÙ>1•±_cÀÆ‚¾t›ïÂ᧑ŸŠzú…5Ê¡îýBí@ÄѺ$ŸV#¥  +ÖHt,&J.¢…äÊ*”®l&þ)9¨Õ;ðô­uêSŽ¢[«Z5Aàh5+Äèd¨Õ{ïMr¶/沪Œ¤^›u—;qX´Ÿù­‹ÀåõþÙßჰ5V}²1 ‹]ÄJ µAÈM’usžg_ÁWð‰%§ƒ Œ%‚¯} +ö–1NÁÕ€üSÃìÀ‚JÙÙë>ÑÆ*ˆaž $@ü/ë':Åk“ýçõûœ×ïs^¿Ïyý>çõûœ×ïs^¿Ïyý>çõûœ×ïs^¿Ïyý>çõûœ×ïù_ÿ“tø9¯ßç¼~Ÿóú}Îë÷9¯ßç¼~Ÿóú}Îë÷9¯ßç¼~Ÿóúýy0u)ßôõIq}c Ÿ3û}Îì÷ 2û¥›¨Ðävñ^›„q*O•-·›ôß.sß~­Ç6Ç¥¤ —ÕpV-çýnS¦Wœ:ÞåÜ1/Ã\dÎ[/á.ö#dó.iD›–ÌÛÜ=s„¿Ê±:tï{ʈ5Y„LHÓ.‚…Âàq‘%ÕŒ–Ô#XÖ–-ZhÁ MÝ{ Úæ°¡ pÈ‚Ñò³KyN™÷æ¶lƒÒ×s6GÂû.†¯ÞÎÓºˆ¦ÅåùÈ•ÓófÙ¬Ëd»<Ö…Q—P fÙX ×ñ¸À ÖºG,‰¶ØwyD¦žHd›F$]XKæu!)Iü8/~ý{ª¥v!M·ÂšÆ²è 2›Ï?Ç×f/Ný—°+qSÖS–Ëf©ä«(=來ÌùÔÓOÞ/îÕÅmîN;vsmº¿Ì»ÌV˜P©öx¥ûÅû|ZËÜR‰[ìÃñ&Z£ïñi—f?«#YPÓœž ±A)Js6ùDÖôAÇi¢vµÉsŸÕËè3ú¿²©óStøá\V]×ÚjOyA”¬ŽõÓíI3ã=<»<ýµñ4M7ÜM·ýIk>²³ j·#N?Þ”¸;”º-VÄnAœnL=o|ôÛ|~÷KÕ¥«~cõ®¼õµë`C ë‘m‹v[[t±icàÝëbOD¹™.fóùøìò¬™žn:Ÿ?»Ws;m'w7³›¹}ÖÌÖÛŽÜÌbn§gâ0â³84ì¡c¯‘‹šÛ©Oí6|ñznãåÜj>w.ã÷ÿdÙ»Ó}Ì·–Áå¼o#ç®e·×y~zŽÇüj†§‹ Ç§“jn¸¦Ïì´Û´–€sØL.­&OíØÓ6uº0ž\¥Þ´ž\äŒaRHèjÍ‹&S Zì ÄÇ ˜þàf†ÂHRÁPQ +Ô_è§.· $õö‹Ê]¼Ê¾7Ú=ôâ§âã‘ní(¼%zZ.¶Õ8ã +tÑ¢RÑæÄ¡Ò•æ2®2ÙÔ <<ÉhrŠ–7/án¡l1ÊŸp §5ßL±€O„ÖüA-ùBEöÓ ùCúñÄy©O7”¡¡ +…§uãÛªñêì/²*VËé°I¨8´c·<ç ÔYçI‹Çžq·úqW„®5ä]LMW‘§Ž:ÞëÈ7Âk6™À×<àe“ïÔÎÏÑ¥Ÿ¥?oÕæi£5×Zó©kÍW:óVaÞªQÓ{4æ²,•¶Ó˜ÇYæºüÎt%·?Kç~ZxŸ6‰KVÍìfÞ’ÿ¤j6-ù§õîv¶Õ¼w‚ü.¿Ì¥'ôJûîžÐ àž££?oîw6—Ûúøuñ+E¼Ï÷ôM|ÑÓ6X­=NëÆlO]í–&¾Ó×z(ø-…m™ïéFLÄå|_jãÃÒrKqÛø½÷ºÛs|ßW3þÍýƒšú¥š>½WG†þ”z>݈TðOF*<=ëËœO—[üƌߚï65›íéj{çgyÙ 'è2½WK¿mW{å°×çyóŽB¥ÍIó­-âôÏ ý“êb‚úˆ»êÇåÃ×R pÒ©zR3^¹bãÖí:ç'4úŸÌˆ>‘NÏÄ£ñëôË>J§¯·TúJ>]ÄßþzŽœsQ¦bÐSI>ž… {fìI ÙE‹«>³éÍr5úËGþè²íð)pÞŸÜáŸVž@þÙÊçÄ…›ÃÃ’C¬lr‡=Äê…¾ÈnÚ¬¾:'‹=ê T|DzŽä))°µxP 6kG‘5l{´ÈC|˜®bâî,‘û/²±vÈ.æÛ˜ÅuÛð´ž²;Ä0þ»ƒ@zÌóþ´qÆárhÇtu*Ç>º b»LßÅOµÃ5;Äøz¸*·ÞñéfYôO›\ýÛĹ·Ê‡N SˆûtëÈÄ]¹ÌVó2mÂå?¦\$û3–çtL™~\óÏþ¤;Üf{Ýæt¹möÙ\z*—‹Œ³WÙ]wǾ¦žÏ%\X¶vv­é¦!skÊܯ÷‡ /N-Ô}}øë8þuwú×Í“¿.OýºŸ®Ž~½uâ×óÎü’emºqìZÜ2ß.ƒ²MO¼[GÅÞ.GÇN—üÈruÐØtóô±(ÁŽ}Ë]Û°cö«S¦Õ#V)3¬åVåμÇÛæ† g§xÒ,Úx¾È{¢ñxÒö2‹ñ¬“yo'‹}Ø@¶ö¡nÈÊéYan« x5›¹¸xÚˆMœ®B×Àć«ÀÄËÐÄÔì­Ñ‰nÚ„(>öt2&Éž–`Å£Qˆ¾9‡:NÂgvö¾§8¾ÂRVÀÎN¿=qwŸ[vç͘ÂùªÜ>ñé¼@Çp¿–iûÇEiJ™.>¸©Çÿ˜2}|?ÕWÏÆÜ‹SùÑÚÁP1¦Ä3©›/JÆÍÅ¡'—  jX« %U:ºPžJ‚»Ç2ýé…#Bå¾O9ˆã÷“S„i>¸Àð8ôµœåt£f´¿Ë ÙÝ°sñ.»w×Ýn±¿¾ ×äóÁÏwÁ·rãúËËõ¥\dn½üøãœá¦W#ì}ØøWÿï}ÙS:Š»ôàçNñËðf9z 1UÊH:¿<ô”/ãh³‡žaèÔ„?yIÛ»›AÌ#ÌŸ9ÕF¶¹6FÇ ×gRõÌTà‡¾=È÷¡gêÇ÷Žƒ{S?®w5­¢mJw²”õ7s¸g].<¶­a[K!W0a:ÿZàºÀòð â`‡h&¬%ˆMY§CÜLs]¡žŸÌÎMZèŒ]½´ŸrÝžÜ\Z* «v³íñÖôNX(ÌˬÒUàx¹ÎFýÄ) Wçöý;Þó}D +|ù›7¯¿xûêõ»W¯ÿð‹_lh׶búÍw¬ VóÅ‹wï^¾}½!jý“ AŸ¼«<]ÔÓóN;þÁÕöBŸ(L‚§„š¬Åìss>À‘¬…s)´•¤à^?ÿë¿â¿ô«ëy þë¿éÏÿ¿þw|ø¯à9‡¿=üã›_Û%¿ÅW£¸¼Çá[´úàH¿¾Õêê‰~}ëŽÏkuóŽ¯ñý×÷oß_}õîÕ›×/ÞþÛá—bG}|óæ›ÃÏîÕ_÷—_¿z÷æí—Ç_ýLÛ—¿õÍË/ûò«w??üï¸àÿÀwÿÁ^âßi5†<‡ +ù;”™©&¸—fÈ.F -¸Y.,¦ÐÈý%ÙsåEW}¤Ü6ã”Ýÿ*y¸Êáî—l \Úõð__`q.Lf +PœË¥Z¾ øU³ÂJ •3Óè/•¬à­Y¢žYñ.U ¦+0¦È +48|¥ÞÚÞnªO…>o,W‚€ÆSxÃ`yŽî}Ühy`[a2Ø âÜû€¯~Š³Q¦€§äÒÆß@ | ìëf7Š¹wŸîè­ns‚™"5×Å\]¬ ƒ+4ƒMc;ý`åÇ‚5|€~fÞª6ÖÞø\½¦¨MØ°¦cà û5˜ž +‡e›ø0Ô!¸N=½~7[ +ÏÉ9¿Öýu£r‡{‘ª€b%oll®R7–{jw%%N +¦€³ÂçÔ¬\TØø.÷7®ÂCk©ñ.‘;ÝA†;sjÑ€4Ü6¢ƒXƒþŽç84ê'e¬¶âKF+hXÖ +Rx`«ÂËÔ‚çܲ+wÌ-"zC“Š†B€g•Ñ/5[ ìõ&‘v¹Oàœ£M)ÙÚTž"…6 I½`YñQ°+5Tƒ”6ËMï-@qÙ"Ìðñ½lnRZnÐ* _¨\Ž~âž7‚ì“jЛÃÞâ›õÅÝh•Î$bXÇXŠ}j®Zeî~, Pùêàäp.¼:Ïc€Ñd³F™ ï‹Œê¦\?Á‡R¯[Ø*Ãœ YÃ<àåÅý¼C‚sÔeFÔ².•\Œ%˜ŸZÔPtuâÄ¡DÇ\ùjÑjàø²© ±yn^ýNY+­æD +Vì—­˜êÀÐ +y'êšÙZx*—0«„&n²Ëd7Â(ØOîíF͉dCxFˆ5˜.¼NQ*Úz Ø“QñN¸{Eª Fq£ÇöF‹€ÍC”“¢«ŒYT‘g¬#\!Q ó‰Õ‚EîÊ*I†%™€%€nêÝL%"&s¤‰Ýî‹”Ü9ixÝÞcn I`ÂÊîArÐpWÈC•R*)¨ƒ`¥ñ\jÁ­ +ãQ$’Yw:³SHÓv)Ä-GZ*µÞ Pô+ŠìD#¬ÃL†2hS7=Ô™/’-´äЂ’[p&Ð/…L›7r”Ôp#%ýFäžd’3)FßÂ[È”\0 Ls™7®¨i H¸Êi9óÚ#§¾§ òˆÉ`ÄœJwðú ¸Ú&Beûg%+±"¦Wq1¢2_c6DV…gücgÞüÌqJÐ8ó½ tÕÏ„ VšE²} j‹7«>x†¤ú)s³+&ßÊW » ÊXéØ&ïh{9ĘL®Ç;ÆëöK…z¶N=”=‰´ýª™fb?SŠwµ‡¦×²ðsŸ¼t•2O›«± ¬tÜ‚¬ˆYúÕT=¤Š ݹR}Ôͱ:|_M“%õˆÇ7E© D‹ W5®¨WÂ~]]ÙE%ÇLa‡ƒ¨1rr=)Ø!cWò"èÛ2õ¼8¿T˜ºVœÒ-\®ò ÿ2"`ŸÑO’Ñïu$áÈlÖÝE%æ’"¸ + +ÈH5:qÑV–=…Wn…;R|cWè%’ ¬}Ú<ú P^ÓöCŸ™‹wLM˜ñVZ¦á«‘}¿é•©»ˆ€ñTêdvqÀÊÆÕà•4é@®dqw™ÊijÊÄBM„y?9XÊIê •TZ™ýQ§-â*‘mP{T€—`7sÚè÷@EaæKÜBt¦ÄGÚÈ+ž¬‚• Šg%dL» +ŒËŠ–¾çÖ7,*Êlm“ŸQú`cpô¬[è‘XAº[ÅÖ=+æz…sªõEw49¸N{ e4Èúxp…¬¥ 1 ++žr-3®¡e‹D©øh å&3$˜:vß·LÔW0¶«Š2K‘ïkWÑ +2z‹:ѯ¢o¡… +“0\é4z%1 æ»3¿n¡8ÐeH畯ƒ|ຟ+–Ö[è ´ÊêŠ]@s(¤^ª’ –x(JxNGëUs¶(Ijx9®1wô·S+«LU BÚÅÝ™îU³ä 3æœéyCäx0EIÐŒ\®[è‰0äŽLÒotƒ ¨˜,b{üª]Ã×…_—µ°«ÄD4J. â Ô Œ ¤)Ê6pGµÿÀ7é #…e^>WoƒÔ;¼eô\/š™²™)œ1‹•v{’õz£"ÑêëGw—•xÇ4$ÛD|&ÕHäš:eœ/Ä`TŒ™§½€3OÅqsØgÖiÚ­ðI@n™@Ôpi½Æ)¦C'Ni¬TVjŸÒfŒeB…9€A¨B›…º¤YT§ÐÌw‘ Ê­…”n¿Ñ<´Þ;(µ¬œ“ÓU>)hï´žàeªˆiH·Ñ‰CZwxC^gÎd½Ú|.“ñã—Êynê4+šh¡6FªWXwž)§7Él©5PŒ’v"ë×9ÝÔe%å>«#}¡ª;ûDfš"ÅÑB<ƒ0+ +ùu¯Pwž²%'”nÕ°^䳌’ày\²ÆIó®*ÀÞhÆé½]TÏz¡QÝM¤šy(]Þkãã`0¶xTŒ%kÚ;äHH£ŽfÐG3 +n­xÙtý)¥GÚLÁ½¨$)Ô‚šg›ä芆=‹µ‚!c@8ÄÉ.ÚRaŠ¾¹™˜K=c–«Õ×DÊ‚›ñZСšÉЙéJ%mQhÃf£'J›ê¨;°‚^Â*;íoýu†fB¿e‹± ¨ÆŽÞ*=“TðA“I7:žœ‹cÅL•R­íŠª +O‘¡’Æظ*¹×‰b$ wýÅú ÈW gSMåŽ-hSŒwÜN ‚Ðq±G…‘È*“'9ƒ›6Wµ¾Íá@gÁ×´ ‡¸ûJúºT +£Ó•š§„HŽ®pC|j6) µë.™iE•uU¢sz+J ú*?ÿl‹¸¢Ï$`DðùD€””^uRÈuÐ;X%+¤àé¶z +Œ©p1›~eÜÜ™f‘y§ àéiÊŽÒ,h•r éÍ×[ä>ƒG]Vb È’dý-’èzĵ$&Ò˜áÑ.îÖše€ÓrøÖ¶×A¨ ’ÛðÆHò1Iø rªa“DuˆG¢ýÁx?t We•¤é™v$S³H«ÉõI«3M–µ› ýÙMÂl–Š¯†é–fqÌ1¶¥’:2 ¸C–ÎÚ\ êõuÅ–ü^UfÌ<4"\ ›>ÈÀ`=ºtˆ gе0wÉ©ÐH†JEÿ ™©@˜AvÑ6P+ +ÕHQXœò4Ò–›)Ü•"7UZ©Rºö˜ƒ@JÒO÷8 ¥Þ(I3Îä”=Ççÿ"q6ÒC +œ97ý +Ï ÚÞ(ZÓF’š(Ý’W¦è@©uÇ4ëHK›25«£æ¯ +구€Ÿ;I²Šh´G¨¢»¡Ynj\=¨•$UÈÿfN£ +M+\’šŒäXá(O[wÐÌ„ÇF_ÑU‘Ö*VdÙ ÐÆ;@‹üXˆžÌ[›”C}F;ÿ&?¤ ·©"S ¥ñ‰‡Æ'jûôŸ§nÔc¥ž`Ï%I|,Öè+0LV4ÂÔÜ"èÎãsqçÇ€ >S†Ø‹>#¹/E<#ctzb1Ü*œF‡md¸ Øè¿4#Ƨnoñ…vPðyê´•VzzDc# 1š°«‹©´—ŸC?ÈœJ³éíëÇÊÓ+²§üΩ3ˆ èh oW¤ +­q«ŽîƒPM¶@= [ñ4Ò¨DÌ„FMÞ)#-#t«yÖt'죙•T£Qò%f6ôH“%¯ºZ4£«• ¹[¶@ʼn¬à3Ù|Ñ$1©‘|Ø™Z¨ælèâP(Ó«…0ï1Wµ€23Ëé›ášüï ê±-wJ2€sgEׇãt¹äGTDqL¼!0y—¯ ЇX˜E&Z4¬_Ž8ó 4—Tàˆ—Ã¬Šx‘&v¯¹ÉyÈ}—Å>L j¤TÃûdÆTd5h3½&xO¥ KlU˨ º:½D{­¸"lP ZžØOLÃýlÎC´ÀËŽ6úÉ2]oéÄVÔÊQ ICkëëNÅ©_è'ãrKöR°+@Þ”C—òå.S ¤’5> +ÕZ¾é`îm'M ¸šk¯˜ ^"ÚOœð¨W6Ïl2YóE-_4ñ0p@ÊŠx|'‹ãøÛ ^³ K›Ì…ÈÍfNûÜŸ¢E„Òç0Wý¾½ÎÍTK1¥žŠ›iC ù¢bFŽÇ™½ÇÕ3÷,δ‘Ñ.Q™’ÑU ›¢Ä@Ìñoõ3Ë•¥™JœOHr2u^¢×êªeÜ£ÅãÔ¶Îø¾aGŽöÈM4ÚB/”`XuÜM;Ð}'³àl6·èôÍš! +Åœ¡‡»“­*§ë̦Ñ{“@í)YDq×@Ó*=×Ö€8)>DÍ)iSí-HÒÙ¤öå>ÆÌqY lËÑI\L¤›‹-¨æT" é)ž‰á;¥ÿ'[Ѐ¬ ȸ°ˆ$˜ãºjdK ?.(غf–è»áœEÌ=•‚DµËkí™–ZNX>·}!Ÿ,bQÁc‡<½QôvVÊ—6­±OëØgW­¨4>ËÌ™bUé£Ƀì9SJkÆN)2C,¢†¥èn´øª;þe-™¾Òt£¾ "ðfq¿¤×*ÿª^5ÄRúWeË?ÉJ‡JŒaê’]‹r#S‰%£å{(Ò‹i!/fK麂Œ‘nt¼—œ†‚xÝÊ“ðBè¬f7„â„É5p ¼É› ›TpRB‚„Ô=/´ªZÄ)ƒAekÁ–­œ[hª"wDLâaÉ0b쇯W-˜-èV P"GÀN¡ 8îd<,‘ø[+¼»ÈVô´ªŸ¤“mš (KîÐ< ÊîD‹c¡^ ”i2¡ä¹ ©™ë­Ò®BÞ¦ ¼/u§"›^Œ£±•nïê3qj*XW-²cÂsb³@cÆ3]µ‚nÀ=J¤ÕQM:ˆ&]$Þ$»—¢MbŽ2Èe‹¯º(ã+ÑFè-£Wý`ÕfÚb× Yx)w´šaAè¹n1Üâu]Ž†J¤´T‘äC<8ÁgÃvÈœ§7‹^f’(•„ø²ÅB/ˆ´Å”8Z(¯û¡á³’axkÓ1uI D· }¨Œ¡Ä]"Löº£½n7\Ñr¦&µ¢NÇF…h.;ÆN©íZl ðp¡>íÔÂkÍóF’9Š¢3Õ‚"%[Ь¸ÜÈÈî°»Yt21ÄS¨¶pD…13SK…Èr5`ÔiN¥¦f.xìÂwŒߌ݈îÞ‡ª‡)%5RÎ$¶3ÐËõ’&RB”ˆË¦lN”³®[¸þVš‹ÜÅv£ËVô1ƒ ¡ñ‚xÅ`2:!¬4ʤL3þŠ&òvÏ}›™HÏ‹¶ì¢ˆLmýUpÙ‹Ã~lD²F>±é;‹t&AÊ PHEˆã–ž¡ÓºØØ$•£¤°@Ìdö´Fé|UãTeu˜…¨%%7à·LÐ…ñÓ sËýd7£èƒºÖHü$.ÓjC«K#ÁÂ}œäðÀëºÇLfNÅ‚‚$ã²…ùÍ£HÁ͉X^´"Z\òüLȇ3ˆÐ›D+ÉwÝ¢ud›g°á¡¿lE€o¡ÎàÝ) —HÔ-v°7Øvè°~¬g¢€Çg‚øh-Ü‚—`vJÂùdj¯¬ +²Yé [ Ø{±ã¹ í4ÏôúZ Ó­•©ÒðÏ +€^5w«§ZimT¯y›1AìgáAÕã—Z‡Y9fÇh8›…g ¸l}@¿W‹Bgçô@L Ú‡é Í3;ÑŠ~eƒÞØS7¢ñ«^+Å@¢Óo¶À’ìø—•´-…C®ÁXÆžú&sHä1Y@kAFÚu {WÑd¾V{«ŸÜ, ^㛩’fžb&‹G!)I‚Q56±´ste|NcAeª¦ëV¿îÃ!¶­j +Ê+Eè“Þ?ạ(sKÓpæ9Ô;ÒÒë”ÿIm‹€«ÙfתîThüÄŒrã{ßѪA€éƒ#&†ä6OÍ.’Ì$‚‰oãºÅPµˆ§ÙO G¯û©=ô¥èLRÊ´åкy¼i¾Ñâ’?›({Ù +ÄLøÍ,Û«I‰QUP{±*&©–âåiˆ O‘ÀÛøzÐÚ7(i=4u¨³âhþ¡»í åÓç ‘ÓSÙwÂ.4ú¸©2_Óß\=sæJü743ý¨4„3¢èêc3ÇG36ðĜʭqy--¾„$bäkÉÔnãÑ(Õ\¶ 6’ºtUI¡€zÂi%k2ÑTr¼bÒZSn­,Ħ7nlÂ\ÐskÊ­#6…þlºòXA •DlrÄ4\üo|‹Ü«X"^1 a¸Rô3°â8¤AAR¦p÷š°"BÖDïAa‹’¡ Þ˵Œ:CÏ”¤f º;ˆ:bfZ­=©g&`Wt`®ä| ×†ªµ3ãi>„Ÿ‚â`}d»ST¦ƒ™Æ=JhEEî@Ÿ¦¡(6êSŠ'X7(­ rÈÒ€$5Ì »‰Ü>yñó §šIg¤(Tƒ»)hˆà‘YèUP×`£1èœeð,4‰2¼-ÖyHej%ˆ=*…z¦C¢Ø岹ɭn-È(¬¦zp(¹úÔë‰Ùe÷ópÿQ3M ‹Œ(L£ i@ Ì»L#‹ é΢Ý [¾Â¿ VÐæ Äì<\¼QA8tŽzQ@·‘ÎÞG„O‘‡X~™ݧÞl¼Þ$¯| +—ÙŒ ]èØ<©½¿ l®b‹H3¬§ +±œi °iù& üeÝB`=½¿ +ÜQÉ>*ÀøäŽò®r¿@l.׎§]Á‘h4ÆŸSaã¡¿]¯šF0FJ½•¨w®? +n¡hI£e$£bK衃 Qî˜J¶"‹c«L»[ÿ¶Êæþ®pä(ÌXÚÌØ §Žº5Ã(fòVÞ(ÇY7rÃ)M7¾@t4³RE+ÅR÷­ÎnÄtbÈ\+ÜÏ„;1¢'ͺ—݉éÝÉÓØ7±(^"+^¤Á:y'NïTéÅÆíÎh‘¸’’V^¦ :·RxÂUsÊI§Ê‚á”e5w£’B +ô™zÁèÄÓ^î©t’¶Cbr—iíéóä†ñà¥X˜¿ffI‚]ݺ ~Âõú™H䘅*šÀË›‰è ¼iè,˜ 7¾œÐl¥¸-*Tz³v K(´£obúl@ ’µŠ¥©Fà©x²š¡·v=&–zø+ßLÉ☵¥I’$êF K `­5‘àòÄ·Îvq™Fafs±AB§Í´#"œ¹3å$¾uúY¹…"×¹9ÂlÁè4ÜδòZÛìŠâ ÓŒöZ×ÌERÑüÀ_méi/š7±È˜ÈLcQMÁ|´b³™ÑæVMÀ­¤j”vœ"ƈü¡¡Ä¹£.ÊFÑe"µ1“Ú¸.3‰±Rª¢Ä©^³‰^è÷‰oC¼‘¦ág¿``>˜°ÿùá¯÷îí«×8üìx¼ÿ꫾ýí›w/Øv—ŸÁbõ fç$¯0 :H¤6axkǘ$"W9© õ:­÷|Ç”Eìeþùˆ)™b´Ïu›ÎàöªÃĪ-#—YÁ¸xU8‘:gΆ‚3QQ“+FÚuþèí«”ÄåÇS…!ÆfÒ V”,^ààß\M¶æ2È6±h–©†¦·­-½A¦=„TŒ”Û^4`^Ãæ è6¡j6_24!º—•,j]¼U8g¼ ŒO»»d4 mô”²Úç7 _ì…ž/XuX%Z¦J€GHÏeø1­=ª üÐM +V<8õî¢^ȵÔX¾ÂÜ ŸÓ¹aÐÈÞ%4PÕÁ”gk’uB&Ÿ5Át¤K•ROs?}hŒF0|åëêôg\´É‚…oòg~”Zø*×·½•ÔCæb +¶RÇ&ÛŽÀXIÙ›jÛ m7‰çfü‰µÑ*4×9™ÏZºÍ8±Öm)¤’Õ\°ÌlÀV SÅú!öŠýPe OþL™ñaIr(¤Ž@)7³½Ørt`®$uxÁ§¤¬gƒ‰€bÑ»O ðt­Ë9Eg9— +y,³•Ð@^I|…¡šÄ¹ +˜<ÝpÒ,Duû;gj¾ g•Xx`p›§í +úA’½£1€R2mˆm C?­­g­hâ"ª<+¬/ ¾Æà¡E-Æøn«S ò ¶¨u±]6láç¼ÛQßQ+*álÕV2gcò؆I‰Q¢(ªÏA•iv¹«g2YÆ›èP !áyŠ’`4‰á†‚ÿЍ&0kFÚ°zõØ äâ€f½9™Ã(¡Ðº8o|î–>OD6ÙÌÑÓˆtˆËl¯u‹4fFÆî;-j%@EéSX”¥²{RfS‰‘#Œ¦;B€¸nhU‘ß¡{d˜½¦¨ÅÌ2›æ, +P¼,”qø+óq’¿Û´ølB–%,2^˼CrÞÒB[3®sÅøçPŠYÈI£C4pP¦ZMØLô]飑+. ~O·]¤l”çd\¢¦F³Þ±ÿSë-˜·„ÇsÄd€¾HRAݳª¢*êqñ 6¥Öñ…Ô +ãR«Jß ƒ_IDAq¡ßµS¾4 +5ôW° µÓY‘å•ŽÆå³ hP{éf’.IºF]2èb¬q€[p,†ÇôI\…˜ËÖR¨n0Ü·X`y‹J–‹^’M5™!<×éO9RŠ€˜1<·Î#˜™ïšýÜ ò!ª÷Èè-.³13x<Á‘ù05ž? Bº'–I9u€áír‘9¦!<Œ¬«N¹^ÄfÙ“i»[vðlK X©D +Zd¿LXI¼+*!!|ኢ& ™½¢„9Q¿ °lÁ$And+_3Ñ… ‚abTá=[tgN&VWõ<–iþœJ@à$Áhtœûn£–àDê‘”Â;#™¢Ë)T‘¬ +¦àq ï¤^¦DWðA §íÔ"b;æQ•‘÷Š4,¨RžVXZ©ÈÈgîh‹dõåF¥¼ùe˜?‚ C*­ä´hRégÞFXgz+Öð‚)-"ÛôhËb*3"“ªâŽC•½hE™uãÙ©UY—1Ž!éÉ R$sîHx²3&7¨<â^v‚;Z <¦üÙ¨ð–ÈBNœ‰ðªôù‚20Ó±\ô“awc=¬95èÂ%ê6í¥x“w=NN+kIp8&-ÍôV…5®Iસ­‘¥†¹}TÖ<ÉãË”6õ t8ÕŠ|3[t%Ê•¬§4׳<ðRº Z‰Ñ‚û¸Î²fnV^ß3`ÚŠ3#?â7N`6è;b }—ä:…&’†à&Ôü’%FK¤Cá˜ê2Ó`µ®à²’ÉüXIˆÚRÉjž!á0 €Â/¹ÞY!5 sCÙ^¡ a åI.|P%sò0^_¹ÔìàjEƒ²åM¥9ªû¸À¢©úéYÎu{AëuÎUâî·ŠŒáò Ÿ[ª %C£¾l 7ò@P"š‚ï’ vÞDîJªÁ+ÉtÊ}¨4ws*g‡ª†#ÞuølãB‡a2[=P“阀v2ÅFιG#a 6ÉdnƒgЊl°rR¼a +Ì@i’K3VE‘âOªxh<)“2÷G„“’‰” ¦À4„&*À!ô¬Mˆ€õÐ2£Ø†&`‰qLK,ªâtøØ™6‹;°æ\Á*¬ŒRÐÊlèï¦k„rÇy>g³+7¥MDÚ’šbñèƳɔ9KvMlÞzŽCf1Œª8=¤’Y")ˆädÉ"Ä‚wq0H a4ƒêÁ˜£‚YŸ¬—ª@#æ5Ê[È"š0X¡“T—…zµ`VÐËŠ ðçfÁ¼£;E@~¥tÚ‡|@Êöüœ *‚’Ë0Oè3s7Æë+3Ú`ðY¸[N4YèîM› qÚ$[³e6¤í€éÁå•Ä1^G}í©;Óèh™}”6CI ½w–£…TIqŒp7HV;ÞW6Öƒ +ˆ ¹£Áä’48b£þ".’èw¦ôà{2Pz¿¼¼öqà=TI¼ãL_eTš¤‰GOþ$Mgö)VX>¡â»²¿ôòjitGG+!SzUÒŽÊ +zàš¢¶š*”ªÁ*|T'³Þ1yÓWz£i’îŒh +Ù2ZÐÓM#¸b÷rOÉ(!™M8 ˜”bªÕ]ê°6±3³¡6a!˜ÿO<”I<…K ÁL˜Œde&LÈDDzÛç•Ê‡LÄ`2…Ì AiJðáL¢CâQRÈ’’ÙàæYFÍœz¦2K4Z¢RQlUÚO9á&ÐfFõU’™by…×2a.3‹œkè¨?Ü‹‰E¨F¥h §œ‰I•æÐAYMâDÓ-ƒ´÷²”@#%^i•ø¼ë1LNãFø‰±p¼T3ÓÐcÇ)ÚÒ‰¤…p–°{\ +Ö2sÐ5jzÉ-£¤/™ÉÚƒFÌŒâé)$xDÚ’‰1G¼öÌjŠbA‘ɳ$d0(ò¹v½ŽúŽ€ÛÝMõE™ aÎ $ +ž „+ a ¡Vµòlñ’D +‰µ¥2$[QReV VÓf%J@‘/{Š]Kû¥Š–À»‰f>T†“dº©ÚcCo¶ª®· +½›l â<ðm4ÈBØöÀ]¶0d[P)Vê¸Þˆ|š®â·ØŠq§l%KŒAG,—žZ0/ [Ò4¶0H6‰“5È6ÖX†) !=6  ‡ôäLáØ2fQ †)!:F-D#Ñ"0£.[È"]•ÅØ[:ZcAò*cO]Te‚fŠªxîh9bÐñJ–;Δ¡2•wTI(Á8Kr“EÁ4„G BŽ­é•F¬µ‰Ð´Ùˆ1jeXh"€ÈµRI¥Ù¥ÐœI:Í[¹4ª.5Ô¾b~ËÒ¯$ +Ánö²¹c/m£¤²*~¤ÚÕÔ4Ø eSÄëÍûÚû—ñ13”%.ú…dšJN[,%¬rAÚ`(ˆÂ¹äƒdÿ¾D]Rm©Ògã 7ÒÜjNÈÕˆ5«PGÎ]EPŒx52¾éÔ%ñÂŽÊQ³§$ï‰eÉ‚™ÐëV8B†= ÁÔ +†±pÚ•‹åw%°ù]iÊ=¤ÙY +ƒ”>õ’ ]ÌÒà„ ÒPeä© +eR<ä_¸t1cM”F*)mM£>ÁCHIh“bh-ßóš[E™(s—ÙÊò ·VñÏQP'FHxµ0‚L·ƒòlDs,»Ú“³ÓÏ8ÐòE«tI•´dé*))¸Cò­…>Vxp¦ÆÕ : a-ä½Ã«>3vˆi8—y šS…™ªë^8eºdšU¾?×ñY…²˜à]Þrf)ü¾˜(S# AÇg0×\Q’Eò–¢VûȯÁÝêGØÐe‚LÔ¼UŽgY$µPJG´`âmkA­ˆ-F([Í–ðÓYÖn¡"$b™²Æ…#÷$ÅE L…YÃØÀØ/×tGd N°q õÛÉéŸJõWÌ'ùV”ØÝ\Y4²’hP´fH¡‘/XÞÜ‘ò’žR´™Q‹ÐDP9“!síiŸ*{­(Ýpð†¥Éô+(ªvbEÍ–[†fZx1¥J¶gÄ)õp°hÙt1~fR$ÜÒîdZ!BЉ + ü:f£pªÄëÌG7!e +§ýAšÁìh@enÈVF&…d¾©¢H?‰‰F9þYix­iˆR.4+Ò5%{ ö¿üK#€˜ª€–“\i¹Í¶ Ÿ™”£¦.Ìïc>KyšÐoõ¾À5)ÍŒ ‰>ÿþÙÂI%¼$ÛȲ:W¼§Dȹ’Qs5+ψ¼›ìÇñð€¤tä夫sÏÔµ0ÔɨÅpŸ–,úQóXd$*k{‹ÙÂ^’BîËíþ-rÖë—‰…¦;›k³Ì=ÌÒYÖ‹D¿ Z$†Ã©… _Õb®™A©¼šRñY ÔÙÇ0àq9(ÕÈŠËš[*n™¢&ýd2¦'5S>梓X…EÇoñ…çq Jd(“xíBÆäôÌsp@˪–x°¶Í +ÂæC“±2¥¯²vgë\Ògú¤¨ z+PKú|vâ<aYvÎC6ÂE;掅J Ge +JÕQ99­uÜQc¢–>Ç5‘Ò·F@`mI!Wàv47c•y‚‘yÑÂâ»d”' R a”“£M4¾TƒÄ‘˜k„§|‘º˜àÔ ªXy,E•lM¶B¦L²ÀüÖÅÌ]ÁÐ!xFaú)…a‹Ñ6MìŒÅQ((ÔYIÑÈm©¥ƒÌ·…5²…4Â8W¨”Û‹¤œ¬Ðš¤¬\™)qfîŽh"ÌL¡V–Þ ÑLèS°{J²¦€CýR±LZÆا" ¨}Ö­?UÈXºMá–†öaZ#Á3óYÊ/B”…Kbve,²Ú•¼!DNþØSg(#©’D’‚B¼óL«ÂÜ2B²Ñ`AqXL>¨€v¦e$k˜»A7Q†ôJ>™AŒ£&½.Â00ƒV’ý€T +hÂä Ié•Ä‰g•rBc/«`û.¶mò–6÷„4”Œdðb¤;u,–dš$î.7ëGZÝmR™^H±s°9Ì{$‘œr|›G"fÁ‘p~'íP;<ÎÜ!SÂPÓdž°Î”—z•y‘—Z\61¬r̹ñEŽ}6ÒqÎr' wj +Fç*““²Þ?(·=-CU/‘® +µ™GUI +3>b3@ã ",¡ËûI’µˆ®H§|þ”Éd†˜™Íx ^-ßSi9–é`¬C(Þ¨9Ï$o _Ú‘1'„‡cH¶vVd•ØaøŽ*ÈY_M¡gõÌÅê +*ü G\”©§j“Ë=6Ô;K—*81zÕ”?c·ÊÛë–îå.¤=X‘Le¢c‹„*êGJ(™÷Õc1BÂ'ïwê#( ê°Š[sL®¿ÞI8†² j£¤)–÷S@a PÑÆœâuý܃VkOKH#ZÊ-yå,ŽfR~²BS/UƃU+v˜ðƇlÛ‘çˆ*Õ_U@l’ÝÈ^Q?&™ÇÛÈ3A÷!‘¼‹áC B`·Ö{;(ë@ŒÎt¼ +SÍ_›q…™c³"9SÎ2ø™+ßgY‚èÖI:‹­7ñ‰Œ“1¤ŽÂ/­>2½(¤µãKÕŠ¹vÕM±V¤blE ø¯' ýðÖ3ŒË,1+_O¶-]uìZ£¸eù…BÉGÛ‡ÐSnŸ:“!X_Út¢JÍËŽ "œuH¥“gò(À2y”’šsÔR3 #ö"'"SbψÊ?&LlMÌÓLK*¤ ž™Ç‡UIjÊ1m¢CµÃ>¨ð14„Ç!ðtUL7¤T‘Šø«ªDº­+=LVV¼M¢XJÊÕ…j(J‰Ä÷´h +X‹„ÍÒcU”Ú¨º®ÅÓ½rœ—‘ K¯¬Dÿuå!çbÀ§R³3¡3ÕÒ7ÍÿÐÕ9ÅÈÈëÄDƒÞÃĆÓnN!AiÊΨJÓÚ£[¼3mAJ?Ý—{j9Ð)Ë „E¨”ªlâ7ŒÙaè ½‘#ã 9ЯØ´wtoý…æ_efW¢Üªù©•è¾)œ¹0•ÈZ šO‘) O L!Š™g¥™ö„® F‘*ÑO´mÏhhÑPð9\é6œ…rI=\0’7…¥BÝ1!beH哶^$så7Ç4\r\ë,€ËÏ™…¡çn¼ªs‰€7(Y`òvX%[ +yÊQJز·¤°ýsõEÌ2‘‰Çf +ðe×0á©"Å£¤(£ÑàÀ ¬IýD%fÀ ¿YÇ dâWŠ·$øJÄÍ +Š™¨(<^T«N¹J‹Éì‰aU (•²¥$âùìVÊ–˜xÎ¥ÎUˆâçªPxvÉfC󟱷‘H/1jØOŒÕ¦Ø"qJ·å`êDDJ»”C®>ŸÍGn½]TR‘-øèLú"xŒ‡Ê)?pÕ©d3½3<>Û*Ʊ„j´õ³,„„çkÓ-ƒý+ÇéUÅva\Uð¥|É<©'2Õ:Ó°y³i ãÊ çª,´pm÷ ®2h;”™?QŽn£ Myç‰p¬Œ¡¥¨pY)دYd+IY)؈*èxaÝâô )ÂhV:~žˆ²¤ek,NÖ9¯Î +aîL8^ú4&È’˜Ÿ+‘s³‹ùw艹+¼ò zžÓžHúháòÀY‘PAéd3:3‚S±@Éb‚— +3«XpßÒ˜RµtúDã¦#ÐL&;åG® +[œ™šÌØ}SE'–w15ôƒ†-¥µ¢”­¢~ä„ä¹…f|ð„˜dqÒ<Â0ßËß+xáƒÛ‹Ã“VDn7¦cŸ £¡Œ Ì!×^fnÁ/%1Z.@¹šgí +ZNY§œí(çIÛJ‡Ä<*¡…/*=7q‹ª '4í3q¬Á']O¨H¥`Ëþ<º3>XfeûNóÿÇÚÙ­J“céù +|ßáØeý+âÐ.Œcã{ŽŠ¦º žéfÜnðÝ[ϳ¤È¯2wM3P]MQ;¥ŒˆŒPHKk½?!b¨ú 7yWÛlGeMåækÚ°áÞ +ÖÆñºÂã@uïöƒ[kè!Z³]P#‡zìB>àŸ„ÕÙ$íæ"¬°QEߦQŒWáæ€åÇmhíShc¥Y¥¬s5¶ ={JÕ}º­0"Ñí¢dcó>ߺM*ÆT-…ͨF»ˆ ½?Ú‚§Ù©ÃgA•F‚{ˆÆ$,ÖçpÓýüRó4 +qN¬bÃÄmÛ‡°ßçÛQâN䲔˚ã~û•A÷ão8¾Åäšÿ(zMXYWÉ’wÞC|¤ä‘ó? ·²#)Ge]Czä^6Ä nˆ:*àÍ”]19³yP +L”åUÃϺ¾~ B¸ªÐ)|Xå¯}7À…Z ñh8›m•Z8'OÍd[[£àIyŽÔÇ-(ãóóbؤëN4%ÍDCÒ"¹½ÕI¨üGgï¨|Œ ¾|ö"LmRiy¾3èFÁ¢#H Y&lRsË`Ó+7êš^& ÒU¯ ì»Ökõ‚àêq|mÇ‘v½{dÇW ™A’¯³*Fö›°ýïU/¹Í\‡ÙœïŽ(N$\ •GU¶.CB£žV²ÄnSëáĉ¨ÌÓcøäŽKÌdÛ‹d§Mö'òåI>EžÀ ÔUr*Š¤ T.óFáïÒ¤d ¯Ð³Eûhô×’DÒÖàöx +ìEu:dË/“Ÿ¹3T `¹ßùè!¾ÓN™÷qtùìÕwE|gÀZŽ„¸ <ƒK±)‹õc/Ú.~‘õ];$ÉCo=~ÞIÙ`†÷Ü10ÿâ8o5ˆ s\‹1’ Tùhk’Ä7˜Ùáø£"•šyR{v?rF‡ÆÛ庰ÌɆÊ‘Šè5øTF L;cž3µ bßæZ!­È©ÊaESù«GÛ‚l·˜Y¬(/;Ä]¥Ì»Ý˜‘Rçè:@ ’‰…qjn‚Y^]ÛõÓI‚i`rbTòz=`—F`nnÕ“ ôëœàںž!L0”6Rpçf±¢)*é­WsvÝŸ¦aÓGŽµÖ7î“ }ïkŠÿÀy|ßúnÖ.Æ«GÚ*rIXæ¨iûöyLlǸm=Î&¸ðýÛTHÍ /§ò-kÜýEµ¦F±a½ ÏG/½þ¤bMñ’Ûg$ÞEÿà^õªÛ¹âp,ûQ +B,9µ; ÐK…)è£N`0¢â +ôÖJne¾ª·‘ÉH‰E3^9zk‚B ÞøæÞÐ|$WóFø…„£6af,šœ‚Š‹%øÒ/"%³nëÈá0ï’ !‘ûÇψ†¢ðžy¦j¦¸[à™jw+ê§z¶ö4פ“wæ„Ñ´Ð& wÈB„§¸Ì(…å$Oò£G9Áe÷û<”&˜2z݇èz@¯Jõêcyë:Ü°—`ƒ•7€€°õý©Ó‡ØQµ‹=_Ù¢þ1 0ƒ¡©q­! B}Ù +õó¸ë„·þëA…Eàã(ò–à&˜ø¾ÑÚY«ö×å0È„+¯ø›L÷Óà5¾;áœFD’IrÃPžƒw8'3 +7…3¼F£Çˆd˜Âó㨼7ê·,Y<]Î,m­¸Û¥ ãþ¼›²“ó+@n[¢l°{Í éò]Il|ßìԮ褌 á°‰éY=±AÇK©ˆ÷6ŠñD=Fh_tJ[‡_e<ž_ÙòÔi-i3ö sÃØ2ØÍxˆA¶ë =ΰ ên]±ÿW½(ÖM‰š +hÑì»@[\Ú»B²”Í× »}+‚ç óýÐ 7Šgõšl”¥Uùº1 ™<㤖áwèL,ÎËòßš,Bó¶mÊlñÞ½r¡´nv¨¬8€ 4ñYï[’*y…(Ì)´¦í!–žÌL µò¾ø{ùSêŽ~Ðè<"'"eB&0S»Y!^¶Þš +M9|&(w´ÃÊÕ•›×tÔÏñTZØu@ Á…¼‘[ôñtç#‚‹ÙJ͸ºi%ùÙ£ÿ°ý94[ÙgúèE˜§‹!² ðz[hQ¨SG°ˆM¼Mf°¯o·RHsöÙ#nJñM%–:î/“¢À…ø”{LâP +ª"DodÈë;(dè÷NÌ€PBÐŽé5œ­ÂN½!PÕ˜fB@)œÇBc'#ç4>{œ—uN^ÒŠÊíçqp*–3“Äb Ì ˆÙòe¶G‚ ØÉ‚mÛ0={]1«(HÉ B@È5(5ve@Ñê@+â"V`à!G@ÖP0þG¸y9ð$7|ç¨zÅh'–j)b©¬ú®Æ7”³É/8<uJÍØ °^âïIÁÅÚi(hÉà¿U£G˜¥,šÁj¡t¥~vzW£—›0óŽãËsöRâ8Íë'\Ý«È ®È_I­¹ÙÄ÷öP«yê×U’r¢% +D§ÚjÚª#È¥[ȽccØ+ÊÉ_ƒµV¡¸;”ÝagxêÖ{°(+zç½d 3T ²b˜ª†¬Ý ŠÀ_õ¡xÄìžö³Š + Y@%;›ª2ñ³¢¸jþ¤á~$L´5„¬E >º\%¦tuSa…Ài»¬ˆç„d‡P¨ù¤ôÓªÝGzÒ:=Vôz=ÇiŽÄ5ìUMï5„Qö‰"*Ãa›­U» ƒðx %Hß*Cf9èqHüUQö–âôÉ7Ø=2—<ß+b~ôU뇠m:yMçLx_ô¸Ä(­gFôPGß{©$ÖCI캆1fH¢¡®të›ýš‡En})f/yòÿ%7u÷šzï´¨ÕÏPúˆcôŠ­tHyT%¦Û™¹“©ˆÍÍ4C¶“å×j·úcU°9ËfE'g®+…oäQW_°iD«qm¸“LuÏÍŽ>”=J$Ñ‘1A¸{ì]óÔ䯨-®Ä¬ŸZÌ#'Þ7v•×Ž(W”Ç{BÚÃRTŽÒÐ6öøV“Z:´v ¡+)Dœ„†éYŽ,‡Më`C‹ÊU^i^,‡TÁ˜ad¬Ò‰§'ý3‘ôìBÏÝ´¾nÂz†ž"ÚÕ§ê=“à­«¨æ!6 ¹_@îïr>¾%¬!¯'Ygz׿‡½¤ÎtÆëL¾gT.&þtçãHfÏ`ïc— Ëø +õL­8³^Á›ÒV¦€}ÊŸ=°üÈõäö߯;°$Ï-Ñ^ú‰Â–¿¤]κÓþ}\Ñ¡òBVLóՙ紶Å&ô÷ßÇŽõy–Sì¹j1ÔÞ§†¤ä¾µƒ°›Ä˜ëE7–¾»)™O”¥nsžBHÔ¸îð†¡@¢"õz/×»ä!€6 »”`HÎð7ªùj‘BÖÝ(7Ä«S\ÇÏ*çBœ×™Á‰qõ“’*?¦»¶iÙL+”“H›uuL¯Pûhˆ‘ùD||‘·5[§Åvè˜]4èôû³‡7ÿ9Á[#Î3≼¬ÖVQb$þõÀUãTú$Ù̽›¾7œb CÛžF!7r"}óM`ù­¤Šä“ “ õÀËÄ¿g/G0¤lÀDÕ®Øf^ILëíGwMÑíí‹´ê{›Mû|a{ðд±&U»ºŽàh\„„-FJKÂE0V\Ddu .Y ämA´û 7$¸ÅŠ¬n. ˆTñ%_ŽxÛ©¥÷êd'#F¬¿¥Í†Òô-,æ”é-n‘+ð(!°Ü˜*¥õ£²áCoad 4»šø8?×PŸ¤Ëi.øÞ Ôêì­ ô¿ê0"ºž;äTúYòʦvEçó˜%#Sh€[•jF¢ˆ§pýFX!ªýýó;·S.þEãQs¤F«ë;ÕEÝÅ€´²¸…k8d= Ï{¸dç×·Ö ­¶ÇDZ¢Êé݈œ°÷äl oä8¦ŸeßÕ-€Oüè—Ñ«NÑp¹Òõ©Yay9¶Ñ4ÉF…B9?u½òf¯tD<šRyMë§}½8Ók*ãØK^cmòæe¸=l À[@I%Í +FSy†÷Ïgü”8TÙ ‹‡Ê[Òj±ú‡»0l¹\EýµAfP†ˆÎBšŠœð…ëÐó­|…f1ÕF™Ð#Ö X‘¼,¸Q™Üw”%},£ ¥]GŽ ÃN•±@ÈÆgcIêEÂj“sye‹D¯ˆÿ‚¨ÕÂ]dtáõ%]§‘¦k‘2‰©w79¶",x¦S‹ªÐ¯oµ-ó°vmȆ28ý{CÛ<¤8Ü{c iA¹hÁ°+(ˆâN÷¦yª€Ýàe£/ßÃy¬[péóŠF‘ì Gî@¨ÅCÅÏϧDÊæ–PþÃo ‘ªª%`nz¨SnA@ž ‘Ô;õúDz0LXó }C°aH­Ê)0q3°l£îß®ÝgæMÇèâ£ñ +ÉÕ~¥ëGÅû¬Þ"¨žÝ@š£> q¸k»Þ8¡´·¿•6õ¤¯µU¸"h‹ª+G€: uŠdql4·®F ÜMúÍ¬à›¿Y(‚€Á’/{È5‰–/¢n>ù<Þyˆ™¾ÛÌßÓ:ôÞ ³Ü%à‰ø OOØ(#„ia­GëãÖ7KèÒO…TV°g¢Ù¢à <Áë½ Z0;¦¤ÂÈì¯%‹§ª0p'û ©×#ŠÖ¶’Žßr¿¸‘!wNÄ…ÝÊ” %fn„D«ïºZÕQkª¶.ÐhŬèŠ"îz¡WÍÞ‹ê Á(+?$ò׌ +•—RS/OF[õã¼ELnjìkvVqËýE«dØÚÊñ4¸A”b‘Zí4¢£‡D/+¸|þV܇z€Žr¤˜O#¿— +ˆ=dÕ‘$ æî›*;ºÍ­×eÝU”±ÒbÎ +? +”ëP1d~a‡þãÆ° ¼†å +Ñ«ðv¯“=“Zº½%l·gγ]Á¿ù-Á6²•Iø¯GkÛ e€e¢­™¡…§õÚÆc!ŒŒcƒhL’a-‰ø˜€µ\{:phŠ€HA Oêë/õ%bó F«:¦×—4˵Š„Bj…ô@°>À±ü‰F1Gkœ(ÓFÌsUp‡` 0ÔÛö#6äµ¢Ùâ¿…½€¡˜-‹B`L¡Šf Í쪣)È4tÚlß +¬ KÜq€4!y™­ÌÚxx|p Hc®Nø—™ÓTdîçÚ|³G»í±"óPsË+–£G"Ç #ºŽÑò΋Þûø[  KþëÑ!mOƒ&µj[=†ÂTÉAÒcÀ×Êô0¢Gk݉8AoAû¯þÈò¡JAéU +@”Cv˜•í™ÚZoê*þô•râ‚kÖ>]Rw*½í„ëÎçƒêqÍÛšõ¡µ¨¯‰:_WØÒ•Á +´ß·"†“ê0®¸ÆQ¨¤¦cj*Ûñ|Mšåg‰Wê(àÃp_+1ð4½°’>Ãýo°±*]G;ö¢¹ï¦”ŸI·£¡Š€tBÍ7:axH'~ÂÞ%ªÍð5ù|Ì8:"*øå©Ù«¸¢~·¤”çðN±¨¨<Õ¯ís#ö¶þ²ÁDÛnáTo-k3v¹ôAV‡ò „@ûðá/¬µ4Ö Ö²`­t¡:·¾ uã9F€h¦}R!¢î†uõ|ÎQ³i?~IQíçùεõº £@‹Ud—–û—=øý<\±Îûðæö¼‡÷§§!4\¥ÇׄN~ׂüP÷bœ˜„÷O° !¨ߺc™_?zíq=¹yÃÓüv‡ÅšPº,,>êìqغK™¹¬£Wg2XÄ+¾~˜ëŽ_!Bó ¨C¯ C³Â ÎLC–î9OÕÑ1o= ITÊy•í…ƒž¡–çQ蜮GG¤}!‡gê¢Zd“„‚K¬çÚmë¿HÚ¢ÄÞ:ۀܖðÙãrÁø%'·ÏôÞ ´î RìsÞ]6`5«T®åÒ€Xºu /L¼ n2/ÂËß{x&£ÁµØÜÂ/áó8 +7-§Â´½–¯$4|ïñÕ0þèà+[v‘`¶½*¿CKvA +*;Rn …0•ŠC}¥}Kj€eõLÒÂB¿]•¶‡nW?6n˜%“îñ.lüw¹M~Ѩ**Ÿ­@Ï(ç* h_bÖ¢»â2Gä€ÆWt'Ô|ÍÝÍ4`ÇE-݈—¾ýÑ<¡®(Ùiˆ BìN…ÏšÊë[Ìy(õƒL%Í[rH¬ñ³^2\Ìõ"Yß{±§N<†oßã5ŽÖbB&‡°ÊØzßqr®*B®‹Årw&T]6"–ÏT§CŒc…=ß \rtŽ-Àzñ(¨q9âó5#ðy&½Tq ÝP¾‚®˜Ò›ÄüxåQ*@|uL.k#§hNgÃÓL0®Ÿ®ÃÎA§VYpG?ГiÔιº‡çU¥¸Cú¼†“ÄGÊSÙŘÏFÁ›ëÌe$Â×v³iED50Ö„"õZj~âêÂ(‰u©à¶ý|«¶˜~›TÀ¢4þñyrgÇúeS~7£ D6x`‡Þ!ÚIì#àø,y5DkÕ®']ï͆èz| rD9VÅFe”"›;ï^4Óè6Þ¡nö-4€[¾ë¦M*94TÑè+–EWbìÆV”ZöÓ_Ái~<ƒ¢´zÙÈZ ÷”¢þ÷ÙÐCf!FÙ[ã`>ªñ›”ಠ$éqEÉ‚·>5kÚȆx-k0¼*‹s¿_ßÙa6ç‚rˆ®~4(L±™Trû:œfÅkñ«+ôÊ¡ SÖz°‚|dkð+ùqD im‘ßT“€( ¯ jÊ“Ï“!Èý¨Ú+h°Ã óhÂpÖT A?«ö‰5ü@è^›opÎJ4‘(wxŽAääžwwT™itóZÍ‘âgÒ.ÄLä)ÙI ºbÃÇä‘RÐ5° +oj³ÄUÇ „+¯Fïå6æc¤ûøX +x]iÐ0 LýÁr#5JX˼£Ìz¤WôEÓ…H  â@mÈ +‘“X)¤UL÷…S0Ѥ:äùXY,ÔX䮕§Åbòå:v¢G¸^@© ƒµÊ*Ñ»÷m朷SKf¿xQKTs +öâH@"ÔKQ~¿21Æ~ Ê{¹^Ì4†EÑ + ñitV¦2}¥í™ƒ×Z†iЯÆMMÕ]0AW$èÊ»Pl£ 1m½n)|s@C«À¨ÓêÑõàeI‰ã …x#XKÑÝ\yUŒw©À ÿE„1õ0‚£cûÍý\2…üæ +êíæ(E¡ó¾"gE0S®)s¢Av­7õ;o'táS¨§Ñh#R󩙚×9t„àš ì9hÀ’˜Li¯£…¹Ó6z úÜ-;«Å¬×÷ÑØj¦*`¥×³ Z!ܼáЋØæZY¨P@¯ÏéþhõõßÚÀô̤ÄpIvÓµˆ&)³yõ[9ˆûDɼõðF~Tn5o®_}]ô*³:¿¶Q§a®˜—­d­Óºa(âÿt'†í”H<ÀZ¦îú ºÊM3Ø-ˆ«dõ­ nÄ“å¯ñ X›˜µ æÔ/zD™‡gž= #õã@÷9vü-•ØÖ;á£á çÎhøE£ò +˜g!Õ¢5ÚZ¹yì®ý„ïmÛzqEùä4Dô¿õG3â†p8ηÀZÃAÀZ=—¨Ê†žÂ`/÷Þx¦’€¾ÃTƒ°p’MÊ&åëÓpþpóÆoä×·zHÚb%5·Sæe{ÚÝ‚›–næk[l±ÛÃÑȫʶ‡öÌTÇmà}á[ÝÏîo³RÒVDy‚åae@Úe÷qª\ +¥&¡`Æ«FV•Ÿ›°yVÁóó=~Nì(ŠZÖàèÆ¥s¤9ì±2ÊÿdS}5F¼ä‚OCܺ€°½:7aµ¢‰ŽjyöÏÌøëü£qÃIªRºœ›wórê‰uŠA@©g-}ƒéiˆßØ…- f’þî[w$*“÷Æ‹Ðhô£aà<øܲ·F×@ÂTÕÑØïæõ‚PŸ=[2)Œþ(ºåi8C66ðÈÀ­Àùù^;hf% Ö +òù)d9¾…ïªo3¤À£û˜»8ÙoøŸQ+¯Àbná§ÈÓ¿ô +¦pbãã¾mk mmnüX¸a.¸}~ [»±Mò6ø*U©ûÕ¯ACÚØZÔsA&\ëö UÞ“u})Î/^àx¦R®†²WøÇ̹'",2Î9’õ ±¿öÄ*ëhGÔ—-UñFh”ÿ€På}ç”0Æ!ê2øÞ5§Kô U%ö,dNC,PY +ËBF¯oapñBÀð2©|nÔG¹á‡!õÞ8‘.ŠØD؉!'C+ãÖ¾* +w˜Ï^q¬ÏTü£Ô =ÜûP0RãmJª‚7à¦ö~B•¹¹ô]7µ¤Øðz äì88ä|èºfríÜ'4!Sl­â‡µØT¨IyÛïÐÈ[ZIõ˜Ï†n—?ï€óMû‘&ÖJkÀ@ìò‚µè›MbEi‹¼Zža1:ªÐ(žÿ=à˜†l˜-蕾ã{ô\ ×^œ`—œ)‰B—}1û\àc¤Øa1I£Ë”Â+÷íÚé‡J“+X ä¦ë»´„ù(ŠŠä£Ûž/a È 2˜3û^@p ¿˜vßQ{ÑóÆ %ϵD@¯D]WÛ¢šÃÁÿ(žºtÔ½ªiž ì¶  8 ªÓ–ª(ð"€Å Ìe¬Äf[²Û芃ًÝÙgÅ&L=LÀ#±- ¬«*Øs(ÒêçqƒXcÊáý­OÓÚ€†1ëz™.¢ê‘"|d*ž·x´M’غ–˜Bm»w©w,ðƒžÈÑÐNCÛ wˆˆÞ§±*5D’ÈíR€s-ÏÕoy³WCi!Ù,×Â8¦•±Û™²ãpëÅaȯû‡a;Ò:h¦`Î+ç‰5Wž–(—î°í=›Î5(;ºsLDZSôXS¹<œã ÔmªÉˆgß'”À/ó™9€z'ãK‹ñ.Q»P2O¼9_ጀö}òy{Kƒß¦öÙ©gÍy‡ü¿Wƒø?FE„<$l~ÆeàÒ “ Üµ&ôô‚­$Ó²Y ·–¬È2¤U°·InSð;Õ¢FÊz¿ó › x(Бlh¯úƒoèGÙ™›·FàîãÜ23ZóñÒ…"kd9)¡¡ãO³ ²CXQñ¶`NGº=“î¾Àxí ʘ‘¡õ®h¹ãL0ΑuGY%¯º^.ËŒ,³l™®®ßSkͤ kÐÅ·ÀHÚ9Jx"M²Œ$ +U@a·N"°f噢‡ÀŒ¦ª³=ô†_]t'½Q¶§4éýœF½Â‹Ã‹É~}Äí€ò^ÃÜ4Çq?|ƒ•µ^“ÖxÔ;÷¥qâò×L|E5o3WhÂhvÙ•7î*Ö É³+6ìµaÄe”æöN1§€bÆ>gŽÝXývÁ´Ìd@Ëç½Vîî5r@ËU£lÙGW@uœcun¯J•D‹™Mˆ3¡ŠÔ8o΄îx¨ÁÄ/poÄu½Ç¦Eæ´ÈñzÒF ë~êáSB… 0ÁˆIt€C#µ®újHà—*v@£ÝêÓ(}½ÜÅØŽÒÂkëâP`‚ŽQ¨âóºî¿#Ý:<ç3–8Bù”[ü¢HÑ_«eÊ“{)Œ6èÖ„¿ë¡§ù àåÛ‚Ð î•N+$¹ËE®ù‡” 5!ßÌ·ñ†J÷7T‚¾8εÅQxœUË̼mìFýÿÑC¾WÝv[„æ‹^9LK`œ]Ö‡¶Aè¡h ÕL +ÖH·hæ#ž‚™åÛÎ5Ù  Âz¡B3ê*4IhA ¦¨{‰3¡”¶Ð;¦‡a%"i;„%™ÕûçD®åEÿž—Cž Kž=.7Ó—,zàE +3;l(Šþ‡QÕÍ~ Aj¯ac¡8Hep¹!ãF)òP„ºO8L qðrµ¥½ôJ|â¼Å'Ï·X=©¢×5A±È1ÍŸÇcmû±>¯Ù[§¼ƒ-«;2®iîkBœ¡Ì]2Χ"ŸåvQ…¢²ûÞãç]ò7û©Èó,ÿIÈ +¦„ù—Ô,í{o˪æ0aÿ#ð1q¡(KšîÞa*½Ób6¹ñY±Þ?B[«nm­³%üèEE…05Gº°` ¦` Åù+ì>áêKM Á•p§§Ž‚<…ãw­_*vÑK!NICÓàþˆ­MÕ*Ùš(ÜXe¶í&=äÒ#D§PÁK¯3¹záZ¥ÌCqªš:b=ð äÈ<àü#z¸Ý +/‚Ò¹ϯ¾išåÊÊpLð¡RÑw•Â4ߊ’{@jwŠndýÚ=ˆI†­5½¤S¦~ï…j%]O'Lœ¯¿‡Ål»Þƒ¾ºÑüuQ|ïñóŽ`T(¬FÚå‹ã Ç®À0„°Œ¢3ÄJ?{¤ƒZülS§lmR)Êà¨Ñ[°Ì ¸¡jk~2 +'™‚KÁÀüÞã™(ÀÙÂ'ªŠA½r-¸¿ÂôÕC 1áãŽB!©øbòHg2Òußî·ûXšÑ«Aã`ØdÚ¶öÆ-*9=He9°Dä°•Îö(ŽyÎd¸! +ò²‡¡d‡ už½œqO¶}öÐ]ã|Î}Å /n!°5´“Ø‹´#…DÍ^dÝ3°4 Œ,Ç8zêÜ›8Ó¯ŽãÔ0SD²ŠhÜIЉtÕF +ÌîXÈc½wÈû®Ü¹õ-öÑIM¯8yq]sìÐØž·Æ$5¹dc&ÄθÆËI‚XùãäÕ·ÝcHXü{8ÁŒ@o½.Tá¾:š[¤5øÃP+Çq‰¸YŽëpQfÄ_àh⡺yÙŒ‡kƒ* Éöû³×ßíëqÏÔ’îÁ® ‚ó»»l²ƒ”? ˜é¤÷[c¨¬Ì¢Ÿ=¨¥0Ï’t¼Ø潜3âAƼô`D§v$ñ®O!,ÖmÙ7ìÅ/¥²Øa'×7„â~måMz)¦f§4€$¹¯û0„b¾÷x_›#KúÞ G@µuUÂŽø°æ«”žJÌeVäoÀADèLÒ™"l/#ˆQR&/I2W%ÿÊTDùŸ[3¯É‰:´‚ÙIÎt [ê¯9AªÛfB†¡îS36ˆÄ<©ù“TBÒ­¢§8w‡lýZÃQxEp™’š¿ßE:•žÉ Õ´ŽÁ* R¿…:ËzM*†åˆdðš¬Ù6ÐËSæ €wß?Ž”þŒ CbøE§0øwÕµ] 1‡°¸+iÈϬ¹}µ·ÆÒj jÊ/¯Ó"ë5ƒå°Ó ¸oá–7zEö Rƹ@ ç$‚ÙJ+žJö2/Ú¯â] FRáw ;oßSÛêVñ%Èú«ä8c®y­ó®ñÓIŸƒqìMXm)Qr˜zbEãÒT&@“åª2 +ÄÏÐ5KC#R´ÍÇ¿M»Ne®†Á7Çâ»[Às +D›ðQ‡%S‹Ã»mŠÛÚ³«ò£Z’Š¸ž]õUN[ÂÊ©i8ð±0µõå¤ð/;ÌÊb°Öuõ ÁÜ«d°BJ}j4ìñÈ!¶ð…¥Wks÷ +y¶¸LLPè}Ý ‚BÑšbÝú ë¹Êå ´^{¯¶ý¼¢¬Ñ¼¢è×JÀ¸Ø^ó{±œXO™·ÊŸU# jØpO··3R].ˆÕIzDŒE˜Ö‚J¼»LkèDN`çzâú–¡NÑ‘s°{õåO—JãÖaêNÐ.ò–ÐÄÚ‚µÛWO„9$s|‹'‡ ;Õð+oHQUæÀÏ¡ÃÀÐPèR”\Ù/S{#x'«Ô¥úÍxÔÄôâÎàØæ×Õ¥,JNØ@=†I '‚߰ຎ›Nf¯´ƒ©²F׌¯›÷Iè÷’Ö±ÇT‡’™Œƒâ"<ÔºÛÑãðÇfBèe ²gÙ”."?é(0Kž°ña + +ƒ`,²H»µM êOFÖáà¨f°¶’2Ÿ¬‹{“µŽ1€ÒPæµ(³.»³o¶Ô ²,¼#[øb Ÿ8·¤¬Mrü‚Ûbpxˆ”5õˆÆ\q'­$ר)—õ„{Z–¯Â¼(g<²‡z˜PÑŒ8zhè þN$H™°QÜgÂ^/qW<Å‚QýÃâ"&¼kCˆtñtp¹uã/s„‚GÚd9 ÷ ìÓtE¯IÎ7v ܘø&BZ£JIË¡.IŽ> Ò Š§Vr85Τæ(…§ðŒ eìõ:hª­T +t¶ù9ÎDa—MÞ.hFðœHâ]s÷`Ì1µd2@Q’¦Ïr˜c,®·*A=æÖÄL¾‚‘‹èã +ÀÛˆÆØ¿S` ׈uŸ9£l"£5¹P|jß"?WtIb›»üNXÅøE e¹[Sò Œù•5¶:)ÉzïdÐúnSptÂ7–G#4Õ-ä}­])ÙR÷ƧkÏ”3ñØ8é¬oo•¸ ðiwä àsH,®¨2Sé…TEúx7Ì;2§içŽ UéúĤ H€ËI^$€˜ÑWx".%2” yMŠÛÔ;ÌÈÜ9r¹@äLË”„-Ëã¼RYäGsœ"¿äŒ´_ˆ_ºEf:ÉÉJ‚G»½Lg³9ÊP¸Ì…˜ug­}C µÇ˜á¢6â4þÎ5ÒëV7òXSh“L‘bçbF&€—†v0.ç•›Nö&a +ÍéñZÚo„†lL-ã”»Qã½Y×Ôq$â‡Ê4'+6_àeã¸nZÒ:-ÎLÆ€käPiÚh¢Ðã¾á°†þ5%ÊY5i¢”F_e©²k¾§äg2i0Í"¦IòÀ ÊJŽ œíõDüÃV"exGÞ9%²2;6r-5z"¤à8P1;•Î{‹1L;ßoaC½9x‚çD ®½ˆYÃÂáwx€‰Øà +ê“í›C}#ÚY7 Žlø‰3pÆ#¸ÇñBµ\#aŸt<]sI·/o¢¿mŸ»G!S- +û‰É(€òÁ>ÓxÅ·&> cWélX¹Øq Ì?Yæ–2C:äU˜Ú´\b*ÿ+XãŠì>š¤<¨@ˆÈØFèŸ ÔŽ=R2ñšVóÁ×ÄÊ«H«}Üá‹“aq-OkT?8ýŽç¦ 6Þän#Àùª4Êú'«º)ðŸô_4`u‘uÿöl<(Þ£$¨‡<ɹ¨ZaŒ2 €+Ñ +zÿ Q\½®oO7ýgÜb+œQT®Å~~-ps2UA°êrâ´µ…­*ÐC‰ðn-pG«ò„†Õkxô"—Xø­ô7Ài«ÿ²G8S&•ŸìáKIÒ-ª2}¢Ÿ±Ã¡»mzUËË8`Ô¸ÞtTÙs¨²cHC¬»J‹·Æ—Hk=ä.x@Pî²CwçÐÚ}×NX×HX°µ¨Û¡°ž úv."ëòêãzê3¼ÊfôZؘË×£ºÑc¦Wµ¹a@Û¤e¡Ò CŠúPQ  ™õ#ºÁKÉÝYQ`B@–Ý6õæae!TlÖÁȳgÑ;¸[by Èp&¨¤"5z„ƒ ”FŽÒv5<»šö1”nú$›Ì†Æ׉­Z™[ŒO¬[ ~lÚºÞYäíÜäe%Ö;y’ô Ç/V”-Jì˜ÖJ¡¢jEd•[c°jœ·{Ŭ­õë5kTŠl¢óSý»o5-Y£W×àBÞׯ‹OXafj{/Ê-#ª¡A{ ³Ub  ;?‡O‚ƒ ++fŽ£º§í’‡‚nd®öÕ§¶OÊ“â¤mÿ*2ªëWõ|èXHHhfš£Àß×ïä!1¬õ “bªáªV ,§q=”ÓU¡+0iM 8¹m“tØH2Wö–!†qàEõr*þ²3j­Nó +:íêÀö[Xt(BdëL°- ‡&ñ¨?D™Ån`oc/¶ë2QTñ§êpìðzF/z¨æ .F”Ù–­\ïµÁ– ¼e™Ìxð(SÐÅ+`0âæR"2ü¿\h³È“<ú kº/7ÛB6ö–k¦ÀŒ[/úš#Jp-»Ú—ëW‹?PŠŇõûë™Ç®´V>„°¥çmTYÑÅ yv`jºÀµ3T Ùöç ®{Èð,rÈo±C‹1pÇ@Êñ­ªHØ/{¨ÑFz³¹Ä—ðiY=òÜñ%7&ÞE‚Qã’“ˆP¤zɺ®‘lZñK‡ç|Åïù«Ío¹` l ºîöçÊ‹6lZ¿l½l<Á;ìAå/ÙÊj{óˆg‘õ…ɽ' ×ðD ¶¾„¶mÄ +¼?ZÈܱ ¾Pe[³Ù8}þ1çy4ûÂ÷šb|£‹g¨§?òŽW¤ƒ+I¾ÈÜ.¼w.p2”íuçؤ$ Ëíza§Ìˆþ¼ýèLå†ÝA>å4UɉÁÊÌ!ÀÌ¥šå¦~†1$„cªÁÂS™6×|_ñ½ÜìÐ@Ÿã +Ku%Â17û&›&EG¨‘§!6þ´àÜ·6§òHe˜$(á#/Oªbl,5R +¾éâðÕ yœ+KÜIà¡Î%è7Ëfsl%¦ª{h#-#§Â1©×Ã.•³Ž g˜e†/·E;PÝ Ö4p¹ñ +\÷Þk åLóŽ9Ò‡Ân‘è]ï]’¦v›Í&‘C¶pÍÛL€lk63’´Q€ñIc «ûઠkÙÈ8…Šº%~7©oWB?c›Ñ„ñØØ:9‹²·Y4æ‚=XT¼!ÝA²¢Ø )÷.]zjf‡hŒ·ÐÍ~C`U +)n|~ß ¿=Cfºœ< +˜@iy¼nµ4÷@Å ÿ¹îd•"£6ω-(¦: ¨a‹ìQ /Ëz³$Ód0Lù‚dÈs(½N KZƒ)È©>JzîWÚÅš¦C.–Œ?Þ7µL©ŽÔ2r | Å'3ð"'ÖýH 9@¡*€Ø.!'Ó}ð©‡ÖÛjX+bõ¡ÑOCä .”ÝÕ,yœch´hµÑö´‘Ì) Êò­ æ¾çQ†ÇVÿ ¿*UÃëõhr¸ҟ2HrÖe(MU­çÉŠÐl½E9.è:¢H0Ÿ#g9·à *]ÔT„9° —ëõª²O%·G4LQ($·I ¯W¢xݹŠNM%jò‚#4«(jŽ!-f!sy¤D@IŸX©É¾5ÇB;ÙÚäá°¦HM]xàÃÞÁÑ[/¾é×"äŸar5ßœ^òÉP aWÜØß“µÐ±y ]›7ÜjäÈᮥU7&üØá¶oO ‰’÷õùÞºXáùENa_ø 1CqÌjqjÌR’23WP?LÔu;ˆÀdoFÁò\ò ¸Y·F+¸P‰žmn^ˆ¬žÏ!€èaE[Ä­»Ä`ÐrÝ) pœµ%"kèŽE%ÇI%,GN$xвI ’©Ìò‘؈`?˜ržîÙÑ”lV.w1Hʼn|°õ™°¨èƒ,^Žœ®¬ˆM?[{-t ø\Yša¥Hh®@oR1Üræ$ˆ†\(Q‡Wôêk\”P/”ž’q:®ŠÀÇØŽ´.‰Æ¥Â^,5ö®8íŒéýç–m!©JùDÝŽ,‘å„/€Z0¯X¶®H"?Ãí1ͬÉ[N&¦¾õ¥’’;Ô¥S¥àû¾äïÓcÛwÆ4¼ÞIö‡ž€"r¾¶Ê:õÄSñçUœq8@³4’²ò[îGÐH¶îK?Öœt}Œ!h| +²]­~Õ2´<BXM:r)eã ð³gRÏ)^ÄõÈ&' +8ðBOT'-+W ¨¹‰÷ÀHsFÆz⡸ޛJÐTã¥Óâ“Ò<•XžV@LáÀ¦Hô†§Ú +[›úwW¬âë$ßä*ž…ÆШ[m“ÔIípþÊñƒ[H´çD±|° ¸Žñá،ɊBÐ*›)a×%yšB‹à›tIö|Š)Ü•÷íõxc‰ˆ€ Öá[™r*÷±hs;ì ¨&ÕˆKH\ +*>’ €ìšÂ‡8δÕãJݤzí¬x Ô<‹;ƒ4N1ØéZ.x˜Cj¾‡8à0g¤ÀpSüuÍÀMáÐ;G u2¬D¤%[ RIKÌQ‡\Ù[1y!NšHGüÖ1µ—‘¡ÎÚéÚìVœ¬ ›ìÃÁË´kApßA5_(ÐDow‚$…xÉâ­ËG®AH9 +ƒ¸šÝ‘_Qo›²r›)b³{tSX’yç©AJÀ-/qú£Šªúƒ²Š¨½qw3ŒÐð½ec¬Ó=Ëb£ìÍxX¤œÆ ±¸¢–Û†ö­†ŠÐ) Ì,689kšÏÑ®\u NÕ«Sq#)Ù±Êê‚7ÍF¡G‡}AXÔæ3ƒ‘±mÒn£†MÒ" é/¢]ÔõB´ 2£Ûóza¯½½{¡4Mw®CCø¯bT2Åžì¥pï7ðª'&™N j: ê\igp«ÎzÈÃÀˆ¥nNuK³”<€ (3ÉVG-ŒKèŠ%ÍAw%ð(|˾Š©j3m]b"¤F‰‰0ùVm2Q²q·s”ÖÔ +Æ‹X½Õݳ"ùþNGLˆr©xÑ ¯ V„€%>²D ãS²ûüÑOĹ7xë•ÁT¹ëHQ¦¨â3´P*§W³\…·Ïêš!€µ~_‹¡ :Ü×¾cTUÙuWÀªô@ªxbª‘¾ÃÎ*Xá_…)h+zé4`VN±^°xÚ¯ˆN‘F Kù¤TCb™eðJ2}EEqM“*½y (¶ÓHZ³Üw„†êK]e²ÐÇŸµ +âÚRC®ÞÆBÌ“²‹â‹×Vä#çi´nt½5Íy¥ob‚¦Ý2ë›ÕÍs&!Z‰ÉöËœaÓ‘ÇúV-Sƒc<‚MÀÒ_A¬±] Y¶=ÔìXóÇ=”œ¼¢MäÊd?JlkY zoøœ\öf+x«‡YFysóS)* ˜ïé…•´âûËò¶RrÔÈÁ¦¡t+…y˜.ö52ÄkÖ +DzDIןp‘‰ôúÃW;°Ö¼àì†@b‚C¨nó‘ºb&tð[(åë£Ämñb!ÏD(¢0Ç%¶l¦±µA‰ëÙÒ-mà÷ïÚb«ÆAô,áJ¦>‚§Ø«e§Ôósx“ƒj«no”p ZËLh¦ØŸ!>äËžyd=¹ºÏ´¯@1ûè! MúïÎ$¤…´‡©^zmAOv€¥å‡q5K¾µ,J⣞#,*?êp=¼=:¶%S¤Ãw"TÛ¶µ2hcÓ‰3dïÅÛ8 ÚÑé3àäÆÙ¯X¶ŒæY,kVàâ¤)a‡Ú|@AkzÛ]`ߎ>Õå³è‚bÜÄÛs7#pëY6Æf2'ÜíÉQÀéj‡á7êŠe ‚4—F•Æ”‹Õ$µZ¹¾F‰^Œez¡Xðwÿ&Hæ»(ª°ÿUó¯9¿…°bRQGñeµ´-dÄvÝ÷5 ÞŸëØÁ€ÈwM  ôÞ›)–ö¼T~ÞRÔcÙNðø¸¾¬ŸB¦ŒtìTÈM±Ä¥J Ê!¼©íDx¦+“ZÞ.ëÈ'Îm·Z}ñ«ƒ¬´Dg_Z©Øè¬.€³!ý'ò’[Oeä"õL $ˆ‹ôZÃ&3"lMÕ™‹Í®p–á€téÜBVP8±À] ûÚ9î2 V5&"'„Õm_¬qêßïMœü—‚ +ŲQÁ9<¯)i¿PåfôD×€tžüÚÀF§€é5^ßÊp20”Râ]ú–Å\*F¸ÌT3@ó‘ú‘‹ÑÛ‰´ã@#ØmW·Ú^1%j³É´MÆá–ÒÝÍö£x‚®òpiZch î†Xú€1$¨‘†#“†ÁQ ÿ5Êo‰tùw¿ê¹ø¯<Ї™aü“ýç+[AùåV´á÷ür¼Ö Dr7 ö‚Ü›½nó©ï½dl8u]°ípÍ z¸&òùåN´ì¸ž,[¾ž%|*(]”äx±TlÈ›/–e2D‹­Ú ľ}?Þ±Q¥ø[¸Q +¸h Àfê bhÈoÌýä7žZç"[D.óÉUs›€ÌõµFf$êä9'zv5`fÃ}²žm;‹d·()N‚ÓD^r…JÊ»2cs…%t'˜ºŒÛPö¸c—ÂïYfd™aÕ§ žN•ä(ÝCÑm‘²¬§‡2ZaîºÂ%+eÁÒ: ô}*†ÀIj•y?û²®Šç÷ó©î²·™RÛ<Æ%ÏŽ±¸ósÕ§ïËà‰†p ?ÊGnú @‰Ja*\ûh`ùœ<´b¾ªçsš¦™ókºÃA~ýèbÄÔY}õ;h«R£À½¦´äµ]Î}ìb¥§Óಓ#¸#S¨÷ù’I)2ˆUÈO`ÂÇçk(ä­µøÑ–cÙhyêjAý[´!,¿_QQ¢g`º¬¥ñy\WÒ|‘2Œ™¬óJËTÝ°Mqf$–\%ÍÉßá}Rd [Ù„Fµ.Xœ [(ŠÙ€Ó¹Ö‘M-i4šÓ°æ˜äK`E£$¦ÉÒÚè4Œ){ñóq+Šß™×›ù”ÐI\›øæÑŽî]#–Ø‚á‚u[o´ÑRäk˜QäpœƒŸþñyúÎÃþ½‘$4)¹†0~1*Þ;H.–: µâ‰’…ȸ0,¦ÏK)+Q>esXZ ‹† ~1”p*h”7Ø©Ý•¨Š¶“©à/~©ÊçT³‘°žÓЊ» +‚¶3û‰ªM÷öi%Ð<³„»"m£PùNauÔÌ5‰:IJp¢`|jÜvÆä´Fwpd,ì‚UdtÉD‘‚¤:+AF‡æÝ# æiˆzöÎgÆÎnÜ' ÃÃöSÆÏÉMÁô€s̹ ÉXÐná@pýóÓz+üM®MVêÜλì rx¨¡*Ð{M5Ô÷PÖ=¨S·ÈóÁnÄã-JDŒ­oqÑ,æ]ØÀçÕÐáT!!”èrîm3…"¤J»u¹Å%%±>O%Ž†y{€×šç´KÿW;·YÍš ›ˆÐ‰™É·¹å0W3W—0ú_p?þ†c[Œ­©‹Ë,ªêj(YÌÎ{xßÔ׈hBÓ^7xI»£f®Ë FÞenð@Ýà´˜’© +Ê<…ê\¨>—Wu4zÉq^p¡S×°~_ûn€×´ðè(nUÙ\Qq)û«º}®Œ§$²m¨×0YËcÐàÈmÜ8w²ìPjQñ2h 2IÉú°ÊÓ¸2ÊnYAaï=bŸ[v]#'a‡ô))墬  Ã©©ÛMˆ§âò _ä>EÝ1±¿ Ül%róúךô›üÛž4 V¬uµaW~&¼‚éN!“)WóÝÛ3áäûœ)²ª®öàõžY7D\¼–{}§9@áÕ&Mô€ÆAÊõª$´øŽÁÁ|ìazL¤‰óµ9û=WS¤„âneÓöŒn争!ýW ÈdIÞWÏöÙY6žÕÈ´ŽemqÝ@sÈ –,žÔI –ðsB«p`yï!pÓ,Íš.Ë•^}×¼+xµ~!J³ˆDÓ^Ëñ8dŠ.õ¤u©7—Ï?שׂ$ï´‚”PÝü8ÎR­¡IÝÚk± hûDÔ˜k–$ä &¶Ãþè Y³MJË.á;Îͧº")ZXÚd£•÷(%)ñW#V`Öóœh†hô¹ðÚ†"=*‡U“$H3õkïÇÈÕIˆßºžAÉÆ|$Å«F(çC™è:@’…(Tç¹y§û¦A ?WD=fAVˆ`ÑcÄ8ŲJ¾Êf™åî Ò&·T×ï8ŒH"ËÎ ê"¬ýú.F¬;W·-¨±ÍÀû«5Le)`õûÌï½ !PÐØ…ï[ßÍÏ7ퟶ&ã`—–6<“÷Ïcos5ÌUŠ¨Á÷oý(õTÅP•˜úø¼7€ÔiTÖ ùHè|ôÒœ/æpoøÝ>Ê aÕ3@†çØìGåç¾êu”ÖP] 3 ÅJƒõçÃN@,¢ *ÙŒBmèZ¾ë ¦-òùáC¸[3gÂÇ ñÙÌ.hƒ÷Bq[ Ãá,œëÞ +‰Ô$0ƒJwä_ +Yý;ìà]s!?øñÀ/"õÈr´§žiš9îT&ëº n@ƒ=½ÐË5ëä]ä¾%RXÛ®]óÝ^úz¦óqŒ110˜@LEí+ú¾¢œ/zÜ,¤©lIÖڑшãgµGÇ«< á +õRÉ9ù57H.ü€ÓNÌ7ÌØf4¨Ï]ûp˜Í›($¶f–ò@„šÍnñ–É$;q†kÓ) r¤¨²ÃàB‘㯇D¿åæ¼½´ÿ}’1S%(`$AÜÃÐ9φۅ¡B°º[i’Ú¶/qëk¶o›³myk Šr+SªBˆ£‚Ò6{Iôëùy$÷«jýw°C÷á5VTPR¾lIùylzrq`‚í0 ‹ÀÀ‘X*Á<0Ó]ÁZ«¶ +Œ0ÁȬ¸£=Ÿ{‰ï¦5Ï—ÊŒ¤v£ŠÅÔK⧩ Òvìc÷¸=Ç‘òxoÔY¬±€¹`© ‹Ø¨0‹ÄçöáÌôŒ_/wwïe‚ÝãP~¥¿wE¬MgÚ«]ÑKU&ß°t™V0•,ÎCÉb¹>ï |CŠz,Ë>û¤-›¯À¯o9é*ÍǘIž¡ +FDTwÕã£Ç4SùuûæW½(Ì —¯Ä~:ÅܱõÂ]ÓtH“á4š¡(Àc) ÀÚsŸ Ó‘ZyÇ^!¤š«r£qœ;‚D5©°1’“F†bx¥Ð©Ý =@Û!"‰* 9Ô³W¸íÀoWs émõ`ó=ÄÉWì B]<ïè:‹y&ç°µ‚œ„ µJMo ÊdÕè°Iç F&P#ˆ¦Ô9Ú¡Ù‚0¨×g8Á cŠεËàóÚ½F<]‘÷ (×dÙüÄ*lmýªGÿa[iàHqÎôÑ z(O·Ôl ÄâY€²—Æ TH&.$3èÔ·Ä]ìõêýÙÃ3e‡ xª¯Žƒo»^EaW œÅa T…Þ{X½Ó(SØ=¦?zÔ2„ƒé5\¨´kT†•¼ì¤ 46¢Ræ?Š1lÙœ}ö8¯iñÞó¾ï/Žƒ‰4 Ir,*—€Š-_ö`¯ äÁºÑapm,ž½†³Š#šÀ(&Y’3—*ˆ¥dÏ‘É›Ž<ÕÖzÉŽ¸‘û4ÔµTdm gêõ«-Ðl4ù v>:iŘi<PÁD}ƒ˜O þ/š ù¿¦àŽú:kRÎõÑÙê1­Ó¨ +âh¸–~ôàbw{¸ÍɾÀ,5ô8zBòЙõË}='µ ÂÞÕyoÖnCëdŸ)BÓõËKœÉ¬-zLpìã™”ýL®ÝCõÔ†NÀal^±Ñ_Áv Š§ß˜ó:™W}z|¹@~ôZ‹‰òâhM#ÝRîþP¾„u¯I[ìX¦À£Y7PògÞô¾)x=çïÙÂ߳ˌڦ’§FV+Œ¸=)˜KJÂÏ‚ÄÊü°H¿H]Ú„êNrǧï5©—¹D™DÀs£¨ö˜…ñƒ ðÑã/;GäÔýñuM™°aîª(ëÇ\Œ¬Ÿt°\€Øp•ÃâRÆßš_düå#e™"º²S™»›NcîØÃìÞ=fôX‹Ûî1wrzÇÑcœ3m·âÕ«›º‘€˜$ ®™#Òj)ˆ‰Ûö|0$tehå#~=,q۪θ-¤–ƒž 6nÌÐÓ±þÐ¥nÐzÓ¼#iœ³ÃãE%ÇðV('-xd:)!ž=‘–78^Q +0žEÑP k9éhâóŽÖ]ÒšWÉ +–ÍEÂö"Õ£U:¶Tóš ïòë!Ö5Ñ ð8¸ÃÑËà}òXsÊ5ǯyÃüá€þøÌy9!Õ2B@_­AµØ1_õPÕcP‚%Ãq­½¯¨àèìËçBÅ~>†Æ%ÌªÒ +Ÿ«l¨)Eˇ);Äê E•3zo>[F]!b»²-~ûN fk¾²ºË¬UwJ!Cí_ö„\­]$¾g˜ÿ~Ñš˜”If§{šBüØ^+P½M,1'$T«L`lYS†!)^pøQÏöÔ˜=ÝÒ°9sX««)R%zˆ*­4uîBͱʬ铉 Ë`ÑvËQîË#t6iNƒV‚«bÎ7íŸÁvz°ô—/íÿj +‘\Î@„ˆ„mFóò +Ù‘1‘Ù ­oƒ+<™mùØ×cËG'M- ZZÁ3ÇwÝ}Ý]B8 2­¤q!,’„=ÜjTqÆÆŠ mëñ¥&‹Tð·p"Q¬ûáFÐÂDø‘Þ°‘ Åìᦢ¿eg„9ÛB©âˆ8?i TÐyïçÆž;&B}ÝÞY-At–¯3Tyòš) \ÛüÆO>¿Ýßiß||K²¾/ò°˜ö ±L“C½¤¼Á;cCEÈ~>÷¨ÀÊdéç!÷|Í ¡ÅÊý^›½hËN€–ï=Tv9²ïï¨f‰„\Ä•à`†,‰d%ÜnòŒ¿7/nÚãuÃ_yRS¡!!çïc¢ú<Í)â\M +ñ3ØK Ù@;~x8Š~Oúî¦øIÈMçtŠ"ëNµ!¥PÇð;”¦1–-qˆƒã£U˜ÈÓø7¢öb‰#î v³‚‚2”pв9â¼Xx_V<¶®~Z®Î5*÷%M2€•ÂTƒµb!Èî½!îÍ{$ññuìq4qG„ÁÑ–"#·fX’Ÿ=¼ùÏ ÞáQòžÅeu@¶(›T9ZÙò ãØϯ³«f§#¶15žFá7ÄrÀo0Ç]÷,°/9RåXÜ=¤ÉÏ+ØË‘é¼³bˆ¦(‹pr²™íÖ“ú« +.Õ©Ü_¤Wß{Üœy¾p>º_Ú8•ãêÚx#ejuD>z’ÀyFÀœ×NYHu âX ‚°âMgªDX[ÀB™Ù/! ¡Z÷Mšº5]}×;ÕVà­Õ¾HÌÛÙ WËËQ@š!‘ +F¨@/õ—V™ +IÀÌ—$Û*pø8i.0FàM?;òïmÀþÙÓɃ0nf E±~¯zllÑ'Ðïö4Ä49"i˜ƒ£úúiOrFè-)+ù‡~xÿüŽîÔŽÑxd«Þ,É¿.¿#ÐìàQÔ/¸©·= Ïk®µëò6œo­Zª® ÙÙÅȱ,M1^oÉ – +²Óϲ¯êÖµç)úe €),׸ÉQ&o-/×|Žbª<ïRûQé1¥ÌÉPõp4(O›Å$ŠÔƒZ³GĽ‡d|¥ i(pTMnÈ—?>Ÿñ[âP=‚ojŠ=ªò`ľ;±È}ùíj»ÔÀ¹ž†x„¾”׶’ãó-˜:šÌ­!ÌœD*Õ•BÑÞŽ>¤ðˆ){¸!"_HÁ‘3ço\“ôÙX¯.‰üÖ6P¾ß±¿$-ýØ5w¨×·þ3‘?0Ínæ%nu­°è ¾$.¿ð%ñسa H߆z®¯oÍ-å€ 3_ˆ?ÚÛçm“Žâ`om5´%œ‹n]à "@º7•lÙº´Ž> lòï=.V£ò@-@’€›µÚÕ#ˆiÔ-9܎Ș 4;Ñ{x– +ù3Ðûëxå“_Ê^.ŽR:¡5¤°©ÊÏv•øéÊË}|ŽóÒcZñÖv…‚j=xR„¯)^·;Ôõì†+“w= ñû62)Sõë[uóLØH®»Ý—´3i-p}V

Õf¤'÷ûÞØÚÖ“½s¸b”íÇö7›b;ˆêDQ%´[ÜÙÜB«RÈ&€ža©am2•Ø¦p›õJÌnš¬ÊÐk&Pì´ö§b뢼ârbh؈Ú*>Íž«‚­[W$C‘¿ÿ7¡w)Y·"ú·¾4ƒAW’ +N‰O,=Ä»[DBš7? kuØNVçz}‹Õú%w"6»Ÿ¿•ï¡Ú4eQÂÝÈôÏŽ$5 }Š˜«°/ƒh¨ŒŽ^q¼D("c‡…hŠ–8PÔde°-ãw‡ªÆöC5«pa/k»ƒÝûúœ=x˜’º`‹À_Ǿü–Hù‚¿ÊüW,áÇØ¿'ú¿_ÿ1¾ýÍ¿ýö?ÿǯŸ,û÷ÿñüßßþæ?üíûÝŸÿü‡þ§Ÿþöÿüô_~÷ÿôÓüÓÿûéÿë§ÿôûøóþç?þß?ÅI¿þÆÿßþð»?ÿá÷?­Süââîç +>¨ÿ•K¿¸¦%ê<@E÷´µðŸ^¯™UÂ,m8/dDw\þ‚LMgå²ò‘Ÿ¿¤æ[kÚ_Á®*',·_*çŠaE”­Ý?Æg{´ó4aêåXj‚rF6³/îO)ìÈ«$‡R?nMh"çðuÖC»f]wv¯y ^#•x™o̽î×ï¾~àME‡vU˜õ£hhw4(ËJ‘†ä™ßh;Á[SÎ -»BР<¿n5¢ò·¾DÑ·A]Y • ¡ûßGWÒ†±!Øk]O¨AÎ%EjÙ¿•z’ œ8ͦ[wM=v{È%W{+ì›ôY;<¯:°î›pI‘mK A·ÍOrÜæƇ” ²\3*¯½–xX°ía+!l&IëIoµ·É~”6—vØèÝÏMA¯ÏÇ î¹½®E¯Ïï‚O-qÆÁÂ(å&3ßêLbu“Ñ‚¢ù:K+qe{ÿ¥Yn-÷ˆëæ!´FX•G“_˜ÜÊ6–ógL¼1B÷G¨Ë+~`Þ ¹×¿oh!;ôzŒWÛÜîë¬Tš÷ý @¾@¸Ñdošgµëý³!ð ¼³_4bÍ3ZXhã©)¹n S𥠚Žå;·²µ&Å} óÍ5ä +ñZeä}9°•“0îQè©F}•Î 4‡ôa  B0‡ =åoÚ‹\@ 4¸9·C£]S†–št•x}~ETËF¦­×n#@¢SÞ ²ÝQzô ä»^ôŒs'ñO’ÐýÞà+ Ò(õÏÆ$–0 ?/É|ôÅ H Ê’úc"ÚÊ®CíoEîM öçCE^X<Ûµg$a³Lþeü² ×ÃïXá´F¥Ï‚èyÊÅlË™uî-ø:q¢%” æØX)gE§QÜ¢öR‡­rPV"Ὠm¯‚T)9w˜š¨ÐÌò&Ve¶mwˆ×Êr: ÃÀByßu"£mlÚçÊŸŸs4¡,å‹FL˜ú ÝÞ\Ò³þlrŸlfZÑv¤]±†Á^x_çNx CfP4n˜Aˆÿ£'¬ÆQ‡ê¶C»¦!t}[δ‰^ŽXv’︲«:Ê^¬lRD)úŸÙE-_¶ÀªNiƒ”×IYÕýrÅ‹êÞKضPàY¶¶Ì-ÿ™‰†·wh!]4öþƒ)@'SÄ%ÝcÞ!¨ÛÛ¶*w†á†3Ãä'Hn`›•J¥ÝÑAö.ȼìA3ã&¢Õ:Ün¾):Ê"cEèvlD‹b€ÀQ†³Ö —Í5_ª¸4x\ßV¯÷›B8FPÅzjµŽÀ¿¤ú†)HaÓ®wPÎíÚ¬yÝ|¦Mð +M² +èšzÈÛ¡YpÏ sè°Áv¨=4áSR œ"{°Ž×i‡Ð© .ÐMæÎÚ*jID(TzPŸî`t¦"a®AS[oÙ}Å¥nPßÚ§o&3°zäK9U‹e1py ¯[˜>íníîa ]·ó_z ~<¯ ûa<•Æ¿ι¢Ô3Þ$l“(Àu=ÕåÍ:ƒßr87\áÀ†Î dp8§4Œ¶—&Éåêo@f¨ eq£=ÀÂR¢nÙ]ø÷Éo<> LTY‘Ü5ûñ*Há&ö¾´ 5 Q +ÔQ²ub­û4¸ŒPRTì)>ª.`6c\u+’pBÜcrÛUvÖ +‡ÈêÕŠ”‘Jzæ’oÜÐÆØ@Cº¤¾×gÎYâ>È.(¨Æ„GQñâÜ(ôÈI¡Y@TäÐÏYà£OµóSÂÁ2D6:¤%}Í?#*Xv"úùLàš2ñcuÖ|â*Ó-ÑÆùÛKÃåeÎç3œßHÇçË×ë﵄n$øùˆ=üôSÅü‰h§f3`ÝTÐ? ÁPV³ñYó&!8}Tˆä;\©1òÎ5‡ýÌdé‹i½æŽ‹?«ºýUãõe^÷•KëŒs šý)*Ïß=ˆÝ~g†‰¯þ(×Ê%‘zνþv¬Aj×óÙÍC¡¬C›ñüm‚vÓ0Îgž…„½€ ˆï¦~j +XüÀüä‹­äÊ0¡ç- æÆØ:·ä&á‹“Â&ª;Ã!‚'"¹«Lq” +ÑÌ'ñê½Ìì™sÑZئ¶Å/Xž=€ˆÔD4Ózê{ß$€¨³*âA!hlUbïIVT7êùøÓØÈW5ÁÂù;¦°4¬À“ЋY/Û iå5"ðYaBa#{‚wÑ¿¼•?‹×*K¥ÔÉj@‚žáa6Àßš6q0MƒuséŠs1-]PÆpf"ÿÇÔØb’ûr¦=Ñ ÜÁôØ +9^à?@¤ æÅŽ§Éz—õÓmô’ñ0¹ð4+âÜî4YH<#2È1÷&º21×Óû6/F¥Vݾ(S58‡ zSÖj_í>†’Fæi´SXýØÆ ,çpezžKzÇîÀ^@C„êK)ÿÁÑa{ÉV?,Muièˆu=e«qQÖ#›( ¥ËËæ·¨? +h­ÆQrÉR7²ê~fU9S¤é4¡b©ÒJîÙ4éÏ'K‡^ù3ÖcgAMßù'd)ÿ¤BŠ&ØN\’v>ë.ÛHš­¾nI—aÌ/3RßG2rZ ò8éõ,Ñ}D¥„4#ÑŘ_Žx4Cy>&ëÐé}o r¶¶ïm0Æm‘‡¼0‘F\CÖ™Ú1†Z'€'®C@¼¾èá{sáÙFÂz=!wÇ”BT·¶‰3ÄVK(C!‚õ +°?ØÅg'œ •³#9p»¡ƒ¤C ÄÖ/RûÌ;¥£ É«Ôu\éPôã½GìÔ±ºâ5RßzñR<=K;ÒºPQ?Žæ †_4J¼ó°q$`ÙU À-Ä…ˆþÓàe".€ú;án¿_ðÈ®­5v…Õרë‹Ïùì1?¯(ª‹¯Û†I1Çʇ€Âz«Y˜Îççáv´ëõ¥K¹AìrªÆB'|t˜p:ùÔn±ÒRX®Ç›JcbšQ;³×e5¬ý¹©8Ñ5Y3Vv-UX6}¶i[)E›0î QÓ…‹Â,ðŒ—õkN4¬áÅ7H~ùÇm‹ŸÔÐÀì¬kjôÈ>æRûžkX㘫ÆÝr„¯FW£Ìçóx¬º`S'Œ|5r»QˆãvORyèφŠ ɾ¸Æ]kv_šœ_bÕÂî®AÝ$B÷Ó?‘@†Ç ø.¯oÝÚ€{¸î¹uüh¨‘;öÞˆÕñ¬r ¸†ÞFµ±q˜"Ü•öï>?#—¨öéËàhÚãúo±qŸŸã•ûì¶ßUÝ•z1ö³•ümí<ð®ŽJ.ÚUÍûiˆŸ‡S<ò’ë<ßÒçHM‹A=(Ö\ˆEzbE¹/õ‡”BôŠ„ŠÕü¸il'qâ!áqEÖÝ ë¦VaÍ°xx¤y¨]‘ëUÿ=ĆÂ#p5P’¦J nýüƘ6ÜÂ#¾sšLÔ®¡ÜñÚ¡ð­ñZéGyîZ×F†Ÿ¸­­Qž¿cjüܪê{ÜWçà B‚,GR&¸a~4ì{ØïL«;9Ây€_²­2zä¢ÀÆãâ:¶ÎÓx8\±+1nÝäzî@íÐÐ1®mí ¦ÌŠ5ÞrÎa«u¿‚“ó²|Rlí<«!!»§¦ÚèûŽñs€B“ØM±±‰Ã![ͳîaÝSà»Ë+nl ’[büfP}ÕÃÝîçtþ¢±é9D^ÛX‚„† ×SíÖWÆÊj¸ú«á£®,ÀÓª£A1 ¦¿;—L˜x_è7ðƒØ?Ç’+æ¶^•Œ¥ÌØ71/#pϼÜp¹ö¦:SýâýfG}}·Û6e ” ¢µÏ·pñ%d^*f£‡(ØGÃ/¦Ø÷F¨pª²ðvJÅ­èÃŒ3Õ”é¡n=Iò†*ó׳Ÿ¢N¹(½`¡&´ä¦5Œ`¤ü²`š£!­N¢Ÿ:î®#;±=#ÓM4“*¡¤¬˜?™c ßr¡Ïçæ®q-`æÁcaoýýNPª¨êZÁ‡€hÆÛ"wQþo{"ñÑIñÑ¿èFuìYÇ¿Ûþ{ÆÀ…^kmQc= ņܿgJ×Z~«« +>Çïx¼Ágb³1Åu3Þµˆ]VR¡Ä6WœÅ(÷znÕ ž߀gȳ¦ÓnÊ€ã»ÈE‘“ÏL I‡» œ[ÃzÚpwÒ@n„ \mC$!8¯¿ÅÞðRÝ8gŒ]„A'ZY$Bo=kåˆoH¶éÞ·–f®¹nÇ4\ð£EAy­$ÄCwý9¤ÏßwP“Ù‰ý°Ð„‡ÅĦjÍðw‹’±âÖbn27ø-r·(+¶y©ß³ +.3¢…Mˆ cÎãBÈTï½ËaEÛx”pׂÀÎu; ¬µÆiý¥d©áÞ%䵌li:­y­\3+gšQ‚5íÔ_ñ$=?¡ðÖ;¬WìÁ6”WãD·e½k¤M, W'M éC·(/'‡sÄqôËq8'o:btFC@6&Ø@m±Çë5Å:´äM|žZÕTÁpl?B ä?•Ï™íÖx‘µ½@ÊšIípÃüÃœ~î˜!+«ë×$XÚ½ª‡Ñ£™nîWÎW>p6·Ó´äK’ØÏâ €<—B‰cïk¯ªSØš +6õ'† VÀ” L1_óòšµ  Éö’ý²¶A²†r +ÖÐx=l—fr¥ÆN%ˆº7µ†ÿõÎq”"ê«!¨ÜíQŽ°ëiÄûÞ w¨‡¸ÈP;OJ‘T $X^õið¢¨ÓÍ×zÐë¦?ߪbëá_îuNÿ£ uÑŠ¦e‘ݱ‚¼÷Ÿ/kª€£®JFçW«°8¡Šò€e¢^{*òñŸ=âq±$ñ:‘3/ã‹ãøØyuÒÖF.=,ŸØäS€øè!a¢n»§-èðE¯"ÿÓûÙÃR)9¹$÷“År×GdªÖÍŘ7&äç(v¶S®*š0$ý–ªÀ~xÚjŽÉ’L({!:’cò²¤ŠhMG‰œ‡ê×Øg*aàc¡ô9“Ëàê%×ëRFɾQ´:B¢ïJ±ML¼·æcÞ&%lÑé04"¢Hz‰gW®øœÈ7¸2~ð-ÝÈrü<5ÖÜjzw͉ÅAjbsw”§!žÀÛÒþ| ³-ÁôEq/Îãøhˆ'Ûö“}ÞÈ÷^Ä×Ã&÷G7B.kÆeÝ› +§„‰‰c©”ÈñqÇFø¼¼wøy—lÍâÃâýqJ{¬„”öÔ a/Á¡Êè' r~kC1PÙþÞR[T‘º„À³’7»…:%oH,õþñy(ÖÔ­X³7X®í»ÖW¢¨H¤bòÞ¡kõ™:ѳg&N8vÄzÌ+²’X7Q ¾iY9xV†õ±k^¨ +ÇéÛ³‘Œ±–Ýlzßj.ÈK¥×™bÍ#_ ؤåTÛ‚¢Üv‡gA2€ø€”m»Nò†¤¤ô½=¶r4yª=Þ‰?IײÊv\)€¾ÓýfÊÆ{Bæð¸)fµ/z vâPJ>nŸ½ævPFÃQ ÕR"¾.‘Q^‚ „¬ºÌgÃÏ;ô)Sb4IïÏ^kçsÈV5ˆTÖ¾ïÕþéùHa‰²…%.=WÜT,´o° õŽî¸_Ñã~ÞNß{<3BHu®qhhWä]5¥î°´½ˆáGUùÚ÷¯Åý;?tj³Û©ÌÓ+n×,–sÀl­´¯áŠåFZèàÄ(ΑEç‰I4«Gªçv»&+Ö$Æúì¡¡·Fîœû‚;Å™*‘8J „:ba·nW #³´æˆì!Žö07$N„¾!OÖ0ƒ×Lþn³9(¾@'éñÚ'¸¶û«[ÉH³‡|&ÿ÷^ +àô–]*ïÈL(d×cº¶“à™qÏëãs‡Úy~´¡‹À’ . +i;RçfÞ¡›J±7s.‹XìÛág„gû€øàL$ie—.¸² 1 ¸dnûn ¢yö¨)á0²@ ¨©DÌß±âä eßÿí³ÁýUŠ*òG£45ZHT÷€[ñj¸7h@mOßO#x>Åñ^–}sÛxIî ´^¿$•Eé”áváÿÁøÉíœHjÔ®“g?j–¸êe¢;Iኢ]ãï,>î¢xÓ”u¡áË]kkO¬ý÷»È²~ +=Úl‹J]䱿`ö¾`¢¬¯uH5øÆu4læ“Acée¯‘WM±M^óL¶–ú~}¤xɈÁØ2œ‹JGì³5Ì>ŽŽÎ[ã â¶R„jl2ñlG~%»‚Þ?ÉþV2‚F}óÜ°h$ÊZ’jÛ„ +-ÈÆsE}».Xj¼E¿ +d8H)$ßñ)ä7|OdÈ¡ÖÈx$XèªËjĉPXÞζ˶:ãq¤ìol.[$T«qÄjˆÔ’øA®ËmKÈǺ¯â¦G +[9e<´_Ý%MÄB™zËØ®±cIn2oþšœÜèÊx%†·,–Á´ÓÜõ@˜|ª|¦V¯XâÑgÒöë=PÚžÔ¢{ 2æú˜Ø`=í1Ü}¹KB•GÿÏ×yêùkÀ‡~I +´2飸^+#0N®ã8³°QOc# Èø‚:צ mô/ïlQ߆q’êš4ŒZ0KŒ‚ÝšÐÚîUqîÈál²×`›N#Ö¥ê (:Þx,7è+Ìw,Á¹$ÿë ES]YŠí«•ì‹¤sI(Ϊ‰NîãPÇÑ»¦íðOÔt×ÚŒ\ÌTÆ‹-Å7ªT:B¾—ÍËëZh®FmGqÇ­[?"j¸>m–‘À\@£Û‹Œ°_'jQ|·„; Ó +#úr%nǵ¿SÙ“ÇŽà[_;Àg oÚ3ŽcžÛE­ví1•c¶È3“®Æ +ÿŸµ·Ù•eIÎ+Ÿ@ïp†’%ÿwa÷…  Ðž´8*E6  ©"$6~{ùZæžûÜÌ]¼(à²@²ÎˈÌws³ïm¯sUÆ…áø¥¶ÚÉœõÙ¸Á ~Ü´e™Ö™Ñ›@ηßƦ¼¬’€ígè¼·}—à>Ø jê÷‘ÛÚÂE¹] •+`¯·ï.mô*¦Ÿ.ð'¿¯Ö;—Óý«lÎÌŒ)ú‹ ÙLô¬€›´*4C@}Ø"Ð>aE&TÐ3FíRÑOuº—UIxü¼ÂŽNë\ñ0 ¤®Çb‡©Ì–F–¿™ÎëÎLó²k›õYÇQ7Ú–-`!*óA©†ˆ^³™²L“±q. œåðWQaž;æŽÃ(™GlÍ”®&¢%DŒ¨þ²uî.Qîîhq‚ø{”×[\Àí„0\¼ë|¼ ÑÓØ8ÆeDwÞZ.®] Päy‹3ØòAG¥ÝÒ‚n;Cƒ/J#g뀕",{-Ä/Ï?zåqQ%m¡’¾o¼ÿû ^8[Š!K +"*°Ë)Â…Ì'xÛ"¿‰„(xØÇãfjÀ`²+ Ù’ÿ¤vzø‘ᄹNT<`vŸÔÇÒä¨D!BãQ€ÿfvó¹ßÃ=ôpw„9aÌ ªRÜÿx}\§xy 8=7¼#æe€È$MØÁSóKº°Ðî;aûr°3ÀFÔ¨¨cr €p·'‹˜VÜ Š|™A©ÿF)TcÒûþsp)^5Uƒ‡ÐñáëD·¼ù¤ìùx€Ö H_È¢ŽÇ@M†Ì¢‡¯{ûþ…‚Yê!I•úlúqz¿$Yv3!gB°óÑד¦J9ÁR*Ìw›‰I +®Ó~Ëz¬#—KA9ò@דƒý°òTáÙFÉb¶k $æäGI@„k¿Ð¸²õf5½ÛK¨Èl-á=`JJãôñ2¨[ÇÙhR‘®¹k,¿Þx¶8ädŸÕP wq²6êti´·‘×~;À~ùÇ20ƒ§Æ&óÙ{Qe˜(eücӀέ\У$&)2ßÂ-Á¤ÚRJSj‚æ\õT+Ö¥.D)#Ë&¢ +SÖôS²’Q•aIØ¢uq¨x@ltæ-”ƒ(bRÜ@#Ü ö`/†Ñ:¦O±IGLSϨ;‡„%,0A+Ã;¹@Wn²½ž±ˆé@ůFþ”"Šv†¿ s8€¤îC)ÅCg9ÓÅv|¾˜–y÷œáü»Ö¸¦ñ¢ÒdD’9Xü⥔†wõ#d›CÈÁÀávœè½ÂÖ8Âþ8!bÈï]ê(«À®¡ÀI¡ À ®6ˆÏRÔò›]ÔüÜkÛº…ø§œBžõÔ,ñ‚¨ ‰J¤ÉþŸø¡L…úÍ>0Ï Vú´7Òˆ½§Ê°J©ÿ…ɉ}kÇQVßÉ ÊãYS¦.’fªØÛ‰kš-LN +’踛_éŠæpÕ(¼VYKüÕbïS´Iñ™ï€…*EgŠý†ö×2I‡àÖÄ8ÔeòÙS&k]£ïReË&²„ýD :Õq!éTjŒ²=}×êwaœ±púêÝ`$Lï¦øTÀ\(• xdy{ÍSM¨yvlÜ=²q)HØ8³uKN×û%º+Êr¢DÚ‘i—ˆU<ã›ãÔ8ÞÎ/:¸xQÇ\_¡n~)d5*dùòSp­lª‘K`½ç•îè3„JuErΚ’˪֬í§øÅ2½îGU‡ï yŸÛ>¢Bì ȈÁ“¡í¹%kc…‚t§¥ŸÅŒìéE¬³ Ôg‡SÔ$ÛO§ýOÅ]iY´ K Z)—“=RRhœ !êJFc¿ƒðH« +â%Á +W1ùï °”‚ð;TlœÑ¤€ßË8Dÿâà×Ø)xÐ/[x¿åˆ-Öà ïõ´iðºÞ—~Æ© ÷ããèh½àpËý#à> +8: ‡Í_Dí`ñOm‘%M}0è=‰"(ëbØðõöî7I¹á=Oì+hÄÏâÛ¯r‘Öí gHlŒW :ï¬d¯ë  _ó`ý· ÿ;{¥-Ͳ¼ÚÁÔÙdŒ"镯ªè^U<»–£5¢HüT–2ò¢u@x|ñ‚ž ºéiè:­ç€õ}»Þþž * 8Èõå”?YË!Ô¡‹Z¢_ðÚTñ†|»vÈ—/yNþÖ²žØ–ß¡;jhÞ‚„jåÖ¾$ž0œ°ÀÚ^çµMÇ9PË¢;Ǭ6vÇeÉ‚ýß)DB{‚Di·>B Ù2 ¦§SÅ)ÍÂÙŸÊÇ¥IÍôSšZó’­’÷~ìì.à|Å_dÿs¿ÿ¦dÅ¿)›·*aç#s.ÕPE‰aÏW­.­jxP)»× +3ù©'VIµÖºï›´¡ýà FÎ’!†¦.“ýÊÅ_,,w°TDO,ìµü½SoŠL£«Ù¾ í ­Ùqoߧ%rt-,i}{¥Gºy”Cu°¼¦'x㠯曢—…G¨L–q‘Ü«£¥˜áŠ–ø³7œY­VÆÇÕ»pÐúEa*ñ(y‹Qƒ¢·\³Ej4¨p4¼tj:ftFЇËüK—.clI–ŠÂõ‹=0ܶT“A¿ÚÅô"(,Á ÷8f­g|)ï@&^ÙÖ51ÔäíÇïŸEˆiS×Kµ¤çÂL$a~ï'ÚpIËqÐãv©©±£úñœ}ØX6Ë÷o¬MäXŽPSÖ—Ä}S¾> +š™`Öx›ýÌ’ÁÔ7^)Fº¢ñˆ½(ßÌÜKC/1Ÿ(ÁO&»ãrr.¿–7µ%îÿˆ«°ž €¯|ô»VDûÂÃÈÆ)(üàB=vç‰û5iù«›ÊYéð@´Å•ÚOÀÇÁÂ×~)-SÄÃzKƒ4²–tD+a/Á•s*0ŽQJK¯ûû‹÷N|‰vÃ|wõ¤¹o:¨w UQ5]H¿îµ˜Ì—d¾važˆ òD©áS›ÅÓô)Ž–Äλ=¡&Dºy¸:íò­û±¼Ç«‚“hfú•»œç~ŒrøxX;ÿð}) fyѲźºT2žòϤ”f#­2t?½^q^Xç#w{ßJ¨qïUφքsµz¹²ìŠ°Ïuàfr”†%,\Þts¦?¢ÎÌL®éþݘñlö˜¶iL°¢ µÎªT÷¹˜úoã—¿IlZ(}¹r 8Ì)кbÄ¿_rášù'¹¶O4•<¥ÏúïòC=Ä+ µµAX½W 'MÅm"üÄ™¶¤®EM$àì/uF¾×ÐÚ%o-“zíÖ2¿k1äÕ†Hš5Š Ï,X—ÞD•ª¦x õ´[¬ñt©(˜¸Þ”û³Ù§Ñ'¼ñr o¼À)ä€ê}5›b/.ƒÖNizªG·FDs¼K@/Z ÕgÔ?oš9Ä&׈ åAëc?Ø'…ˆ¬ èË$ŒºeÁ ì"’X´iÐUË‚¥¨x{½ý4X'w„8Æ¥*vù DíâC_7G¨+Í$ᬌ¦tx" Àˆ|xÛ€mv?™¤,rkÐBç* Ð, ­Á:B2´‘*æÛ‘˜ÆOÝÛ×çHˆH§ì©nâÌ·Qša•Ø¶ä+@3CØŸ¿‹?¼T¦ýÉ¥€ZÓ¢Ô¾ÎO1aGƒ‹Xšu”½í}Îù±ÀgD ÙC„Æ@$Åć*/!Zâ^h@ Ø5*;€6–ç¡ßeï˜Tì¥)UÔVxyÊ,&úz…ò“°.‰sعڹÆÞÒŽs%ÖmìO¤zé4`m&ݤ¸•0'!Î- ·×„þi‰iQJÈ&]ÃW\#û|ŒìOóÓ(Ö!¢VïÒà IMž§عî ]¯Òâ=ŒÉ”ö”1Êu7}W’ù°f¥;AÖÞÔn=HÓ†¦+(ÌN"—³EU +ß<Ö8tØî@*ˆCʃ”õ`Û!„Šj¶ª2PÒ)œï)@°Ì; +‰TSÛG™”ÂßÞÔ×|Ló fû¿âmÏêÆÐÏXí‘NéòËþbÿ>^?÷ùÂb‡Û”´R&ƒDøQ"šDÂ’÷—ÎM¤.ÎU‹ݽ¬e»(,…êN˜®.ŒVÖ©@#À÷6u‹x ô,03͵‡‡`k jx¯‘ »XúÈ Ë„Øí=z?~¿Ó?ÉG%êä(B£Õ·Ã8à‰âE`*¥Jí4œö£íay½œ8¤Ã¬#|I}ÿ4àˆŠÊu#±‰+éšMĉD¤ƒVÝ÷áŸU¥GûuÄõ¹/¦×‹ßM¹ƒÆÑ>!:¹@ J=âNG\4iùpâñ0‘§¾‚FÜáìÔ@Ïò}™ê竾…ŽLKºKþX%ªY®h{dÏò0Q" J˘5|_ +/y ««Š{~RUþDCW7JFNt¨mÇJd?|Š(Ç ­g¨÷…ˆwôžß«›8ld³UxÐË +ïL¤¡´Ôk €äìq¡ºóù€Èð.ªˆH÷¢ñ‚<§uà-¨GÓéš"•hQÓXR_¬xÌPÖÖüj‰6ãÀ®ü +PŠ׆@ªeTB+Å•luC5ÒÖHn̘ñÏh¢i˜K)„Òh'#âyú|=p*( ‘?ó°J1 ¥T84>„MìÙ-š­ãBŒë2~Ç]uÐ4¢i1ú²ÈÈt“ݼï# Æë/o+^ëÌd,ËÓAîk¬M× )LÅfvzN·E„ËTóâ´§ÖI4Ì’«×¥ÑשIéS‘÷ºœ5!#Åv‹Ü•×ê€K‘ /¼ ™N£s#|µ#¢y@ó¤¡0ÿÂyƒ¥£o{] Ö‘=³âKœ‡8“ƒÜ)íb(˜æ¡ÖDŠµ‚o‡ <Ѐžà“hœâ9n-%‡qN¶4~Qáp.HÕÊ<.e²sñsG²Y³gókI|f„îÝÆŒˆ»ÝfºT´#¼3:LÌSŽàß”#Â/V‘î‹ÆêäôœB–i­º¶ ´e¬Óôqá@/÷’|ž@F22›ÉnŽ’±\]V29Sdt’éó#f 7ˆï‡[MûrȾ¿É BîÆQ†% ê4}ÚY-šc'~ÐjZ±__éQGÓh§šòr£[{5šöL Ï[xÓ¥0¸bËÿÅ6g1^~jô`1ƒršS¨àL-ñR²y‹íêÏ@._ËQt¸‰r¶K#ÌD´}FK¦tÚgÀFD8§ÆëÙˆêÂnµÇ•lZ•á¼ +< ŽTÑ=0ÞA…M]€pÉKéE"‚>wˆADøeûšÂ(<ÑW¢à„ž«Ò:5èÃêW,†ÞYàÐ÷¾*”NôÀê±ó¸u¶o_yH¬ŠCGÅÖÎZàÝ€*ÝèÈ°ÀíUTž{p&˜æÎ缚±€ïÙSt=ËäD˜rf”1B¥·ˆdWSŸ@þ´ÆѹÆJØ$}Œ€Ãò嚬Ž«èˆµ èGÙÒ’?ä¶)ÞíáuCR£ô8;9êþ +ýƒôAPkµÈZðUÈ⟗ð+Øb¡?{À?±ìBKî@".nD•Y~óOöƒo×íáŒóÀ•ösëܱæ†Wp`DèÖVwÒ’~®©¤ƥǕ +z’©X+(+ÕsÖØçéEèêKÅs´XdX Wú²AvÐØÑeËa ‡ Ó @Lr©§G-B‹®ÄK¸`<3`‘Y[LjæxN-òv­¡gÄ’h_¥† B§˜%CÉ‹üì7óוÄKе’]XdŠýjáÚùšüaþÊd@Ò‘Êí¶·è¶?±Bí¥šÎ…/y`dë q=ªw]jK› +m« K}pŠF씇%uZQqË|„)ê:²”¯6<ÈôX+pÜØ›Pœb;Ò²^î±€UötŒ»¥ Ö¬Ä/èÇšäQ{Øë˺dÄED˜!“Cð6úÀÂæšÏÔ…³1×PÀU‘oè¶Bö&Þ¬nXdx ý8/ús„Õö‚‚Ú¥P-;׸ N‘|¡T7ÀÙkø>¨u«W@‹]`,º{À¤Ãqaºy ªóç´éÇݤQbX-DŠcþ@jå\ .¼çqOˆô¯ðO°¾£ú8RÆéÈ¡QPo°îjü]g axP)n"pã+j|64oš¯yfœ(a c»×iv×y¡°ÃªÕŸocâYñ^PÉ&&wðNÌ%tÊwzUëŠÛ»‡„µ½nlfJÅ_ k°•î ÷ÅråwÓy§ÒóJÌKp¨ò‹\¨"Y>°ê, +·‰1L£ k‡ &ÀŸæÒ¶ÓG½,‰7?$j=£>5'à“ÇDþ›øiuÚ0GPþN‰B6ÈH (\Thbõ=ÎÓ4±@4ïÈ.Ôzû=A¦ðö:såÔʺÑn¨ÙÏÈ¢«–ñRÁÌ· @…* *#>6SÆ ûf[p§Ø¢ž_nH h 2þ ´2¶0-Ôã|AéË`_- /v‰`‡”9£6•ù¬¯Ç:… ô~íæ±½¬á–²!ݬ‚Øt¦š=öIìûýŠ=>K¤âà¥hªí¬Œ Þ¿Ú2<Ì*•0pÜib¯ój¦Ðq䇌ªeES«¦%Þõ@Çiaõ·gë'}TeN¨Š®êžŠÈÅŠ!-US5…`ÙU(…öÂzU"Z +ÉÎÑ™a“§© `õî@Ti%[ò®d_DƒÖÀRÇeýȧ]3k4ßòŠÖ"%ˆÇ¦†:•¿>‹ÛŠØ¼6|´^9·è;¼7ßÏÆþ¿ÿ‹f6å‰>\bâ?Ùÿ|ç×"¤¹] ÕÕÌ»w"Ü¥Ï H5ïçŽZ–RߣÄL‹L4îN @-–À}`DI!2{Jm—k¬Äp’~’g¤ÏÓÚBCƒ¼¤š"óí,I±4G½Ø™­¯Í/îJK­½ +7ñ‹zóz Ù P3t“ BMè.¸ ]e§ÛÔòRfÝèìá•Nú¥þÆ^Cfjͧ±qB‰Ý¿?·ºB2üB + EþÅ^M§bVuÑs;ƒÙy]_­mc¢-±7ɹg4™Bk,á{³@ õ(C…“6‰%0ŒÂÞÃÒ˳qÊ ø‘v¶7©.4lÇEÛëöì®y"Ì€=¦X*èþjvŒð†|Én°¼i,;;E Uð䀄M¨p¨É4$ç|ă²þË-ž-¾*ätEIDuˆ¹LLθ\ù÷û¦|¹éö´u`Ò¢ô-‡Bèˆ%÷QðnaPÚ讞žEáp|iÈFÆ×Ábe +Š¨Ã¸!üqȵ6ù8˜cþ/P˜1v¯  J’fOQÅkgXðÎßã«ÑõÝù9‰­-Þû™„R\H` 1L}ìaÁÂÜ€öõUàå¾}Œ‚´Z¿´Ä<@:É^;ÌhîsÀÜ|ØïœBæ/T­œìƒO‰K)‹UàçR®À’Õf2Œ^²šV¬~q–«”T´?=ß^O¡ÆT§«5HèóïéËôã`=fÀEôSfEŸ7-ºÕ•«Æ+Á f÷yý©ŽÅv´fé{7j5~õôßv 0$¥ Øé4íñÉQÔ—*(ì›^#§_ì3’Ì×ë@œYØS¼ý wà~ìßTÀ8¡ÇÛÿ/~jg +þ)Èðn=€Ó<Ô]ŠƒCŠ×è¯ÂŽêQ¬ºPÛìÉœµ@ôÏóö<î‡V Gp/ Máßu][#jE秕ñÆ‚1KÞf·–ù>l÷ŽÔé6óë@ì’E¦iúÓ§R:ûtD,i¯çS£©¼äþìú„,–vT¬Wé⛉êkT‰õP¿× 6’þpCÚŠ{Å,QP(à(†+æ¸d¦uåýW3Œ¿Òåîp[òsʃ{Š`~øƒ‹<•wìÅ [ñ.»Eõ¶Ñ¸éfM¹„Ž¹iMWˆ¯$Nwà”©¦`ú\̘²ð©æ886N§ô,äé¹¼§‹f<j”eŸ( +ä£õÌÂ-Gá8 +ˆ +Q V¬Oà²zÙÁ‚@³ñ=,ÍòÜ/4Ðe?*úøjË**ŒÞ7îz &%59XÈ‹róΣvt: +˜èþѠЫop¡Ä¨¯F±*)^Ü éÿ>(¹»[ +Žj’“<Ÿâkk/›BnØ':éê>¡ùUtº¦‡¥H:ÔˆY^i±òdÂzà%3“Ž¤ÿDÖˆ’µùf(¸;`ýƒeÎB€í¿0²~ù=1È.Õ|oSjÈ© bÇ'Ÿq\˜¬¨¥ä=|îÆ«×QÉ}¢9®-Y`¸œh´ÈkF` „Xú@Ðd._mxˆ´DeÍÁ¬@t™Úº.³ý`sD•‚^j…>ôóÌ.øýtΑ”˜õí²ˆÏ ü³Ž"e ­{Φ¼ÂpðA)„ŽS1¦V„V €@¼£ÁLÒûéi?- ˆîv=Þ¢ä8ËA[Kä«Š#˜üpJ‡K–‡2nÏñØ ë÷/Ýg_DµÃÊÔ-B%Hv"|IÛÕ݃¢—ebf Åé –®=2kp.¤ºù¼ªCýT(|Q‡’à)Q’™7˜¦‘ï•D Îú“ű@#úÒaµY çÛrW oΗþŠI Ó´ó²d5R¼ÙJ§Bnc^xÌNÉ`oXçQž^^ƒŽt+´÷÷*£­?´ÊXš„#¶øÀ(ÈD½£wêt«R¬I{§-éôaèÆ|O4Wþu°ŸöžîÁ_«‹.®yס°Š­u ­”|Æ­Íî7ÓŠé[ÄŸNÅTú$òäŠ;¾GÉCLáOkÒã¿%$%g owùí÷ v:_Ñ ¶/qŠÕ¸$«}|(øÈfe¥ÞTóóé-»Î3Œy/BT›TµóÀV9ùXR¤[UÓi{Ô:¤Â¼]xçö…–‚™]n{Q¹c¥ñºšsD !qTzDÁ²ôW¼_ÙÑž­ëÄTuÜePãšôqšnë;þa'AéŸÀÝ:Wüè#JC"ª²yÿ‘`#dm)Ìcº) 3J›*‘Ù69L©~50ª¼ÚD +Vt¡7ÍV;9³™·ÒÌüC—eÈBõïj`!Úeî칇ûã•ØLÌpc ½/®ne\\DÆÕ/ÄÈB‹†¹Œµ;0;” ‰u +¥Â_.tÂJ …ý°ã)¦…ï›Õ=±”*\»š§wšÕÏs-‡ÃNêš½ÑíͪAOÂR³æd–ã¬ú!evÄìq<™Ï9ÈŠÆA¤@Ü“Î( *~ÊéL©Ó¡gPâó‚ôi)q{˜ÿ9‹½‰ÀpŠÔä]×iE¢¨ÏîýÉò‚ï«bèF¸È[ÿÓfÀœÁyÈ—F;‚X½_Ù³&ꀌ*KàW÷DYK¥wUΞb ùý¼ ¬,÷˜ÕI‹o„Ú +'ßèz¡éÁ´¿æ›â|‰yÎul;kFz¾žÔˆ»;o0=ØÞõSýàrIÚXXI”ùr!åYn§ lë¹é$b)l†ú¬ݕɨ¢¨˜=f¯¨}(wýŸàãÖyP»ù’4œŽí5ËHÿ!{nÿewpó¼Ç¥x€n‡a°WÔç9¾Ëªé–£¦;ËK×ßT?]¼å•"„ʵ[RS ’rôåóå7]¥ýû7l +ˆ–Qi5¸c#Œ#L~ßçOúŠ—í%ïF|¯%k2Í=VÀÍ®uèæQË>N: '´…^1^-ìùàB¨Iª+ ÉÆŽÓz¾‰hTš];ëµ(ù&(e_Žâ>…¶æ€°Î$Ý',jžI˜¦¿8nÎ3þêàRØ5ZùUÝ;Z÷!Ÿ®H.6ËçU^»a!+dÕ¸¤æ³šrS¸Ô²Õþ)ïKã] š¬iÔAÏÖiµcO)Tñÿ½ÏNK…çàs |^w®QÔ:uŒxº°]Ý_ñ¤7¢)j¼™ +Õê—-MýWêCÁ¤»bÅ¿:àƒ¦6~ Ýáàð6צ+5è¥ 4˜ÝŸô?•íýÕŽÎü×AîZåÖLÌFÌù ò`Ny‘h=}ð7²Õ­ß| +ÿϳ Øg­¢Ã+l«ñr9ØïK,”óÀ%î1 ú*ît¢j4ÑŽ c.ع&-8ôXù8ppA›tn¶ïQ””ͶÔA4–oŽ“+Û‰fƒvåõóÁOÓBŸUB9Øï:n£MÕ dËÔµ¸Ù3ÿbiÁOC4ïûï iíwCÑä9‹ endstream endobj 58 0 obj <>stream +fK±h†ol?éê|âépõÏ1=Ø%¬ÓfP/h¦h4Ü1èT%ì‘[(¨ô™^g![ŽKss‰RüTó;Aóµ^³¤9ñð`oG©zÙûöd¨FØ‹›çÆuèÐ’ð› ÑDf&%6ÔÈå:æÊÔ{é=â»iû3 +6'œÉ=IË‘ýÜQ=ôòàè‹ÖTÏ¡.Q)^ÒÎYàþäéT§Ó#¸9:jðÝL¼"t²ÃÙ‹j³Š_AE +$3Ê!£’¿Lvw±·LÇ2Š³™ ¡±<¢^”SHóÇ9{ßØ­¯½jçç›eLHÛ‹E=µ…È,Ñdd1ÑHã=I¦Ú^|òU¿ªašcP¾Al< óœF„ʨóD̈PZĈy"¬]´Ðí1bÜ+£º¥˜¬d©$YŠöŠu‹·›QíüìüzfZþÉ(4DЮ~|!ç|„·’F-Q6ïd¥=G¶¿wª!HƒKø )¡‚Èr¹Ê©W΋éý ¾¦¶û Ôº:•ž«™ð­»‘Ã…Xux³9Ô•¨ýìÇ”®%‹~%ÓBÒR@ª\({xš¢Î‚=;î= MyÏ<òq‚$³¾.°ØÁjÈÚC,—J $}¡B7°•VcFÓ@‰ \U^¨&¨ÙÎ[ŠÂbLÛãF»å¡§òØ~¡3§ø¶…á=‡6wòÜöz7/j—çdú”Ô²jEQn»B+•ßÞ8©»ðV¤n1½pmõH#…Ašu³`J¨ÓP! /£GºEe +\¸ýS³ +º7…Œp‹=ê³€ém’HôÂ,Ý*z±}”¸¿ÔhÑ•è a“»¯ðVíúË¥®È+‡; ¯O;“™¬z#\b +ˆ–ª 'p÷p!›Á'’»SÜý“ðx¹;Xýb@ƒƒ&ÜI~pÂ\ýxÈDOü趘¡A/•=Š§*É_Ž}0œöñæÁŸjR²Ã:æ'ØïF‡GÄu.ÍÞƒ]›Ò)»„³HxÝgÙÙÑËDØëjg‚§nö€ˆ¥{ËÂXwoSЛÇ/ÔÊÔc]ÆÊ6»+5¿=@F¦¥Iªþ’ˆøø”¬h‚^¦4É°¤šÏ©k$+ÝY}÷ø{ ëáïSиâ3ÐðWø°ëJþ”^{låüLg®·6ùvp='cãb fó$™< ÅíóÐbOý‘RäëKC;Ù§WŠ2$¯O…ý¤CG™§Yýö÷kúõz¨?›¡çõ—Äø––cÑRì\q€¶>z‚}Ý?ýÁ`î]*Š£Z?¶z¡ Ce©P+v³O°.؈?¹,ïèý#ãØ8 °ƒžÑñßt«ÿOø.âv¿‰3䨨dóí§U-ñÌÞ;…¢‡ŽüÞ¬û¢= ßQÞÍ8µψ¸m'³øø8Š¤àë(ü₪ÜA •äùŒðqÜó¾˜ÆùZ-¾Ö5í™(}Ô\y9n£°(}?!\4^0 3Ø$a4!µßO=T¦àGFð3û]1Ò:p=9*bK7«½‚ +tpÂHš9ó½,öqÓ/D!³Ó™Ç‡š +™v8÷!¤8|·LµN§ÏÙïqæaoø:[?Þ¬“ðñ܆Ž` SHæÀ^”•Üé+ú†‚÷ßuN©K¼'s¢rÍàÈ Û{)´÷Gh) Â*㨫’Ù*5Nd}ú¤un^\á„»EƳd‚å‹Kß8ÿ®S“˜JâOÀ£³ÐO¦\kh9ô³ø±‹”ü†t T†8£ì9JÀ¨jK¸=Ÿ‚ôM +ÇTbÉ +N}þý‰uî6Æ~uðÊuуJ’{ö€C¢Ð»#Ã9B†s¼þþz³tUK)Ûׇ`Y(1°ïMIbóD¨äð}EE ¡Üœ›*!ÃïLš2mWªÞCŠ¤fPœÅ¥oÏç”PaQ¯ëKÈA6¬8Û°µÃóc]Ëññi¿º0TH—Í‹5œáV\§ŒzOWÎé2ŽtùqÛµ3ZÜlÿ-R¾ùûŒŸciE’ãÁ=½¼|Ó­Õ+Ðkuц ’ºû[P»âõš¼ZH)¢Jα³,¬çÐ;K $ª‘P2â7€¡ôµ2Ó(u2M?‚^EîÒ×uÙ ï^ñ·9tª#jIbÒCHÞíãÄg3Mv[ ã¨íkA·÷fx ù÷yûÝëô÷ð§iâ‡òaš#¤â-„jÕÏíðHø~°†T–¼i=›ïåEêsHf¿°üŒW˜ZvÓPí*ã¬6•ã¨­SHÈóÀ”¾5Ývƒ~Á­9hGtáâî'@Ò¼N á½fûð˜;Õ‹ŽÁVG´NÅ—BŠFN㙿IùüæP‚ú8@÷ú%jý~p…ð^¹ˆ76SðˆðºFI CÀ÷£õ×Øø¬X¾a¸!~p8 Ž.d¿¡º`™Ç"á0’ëæþ=Þäcó´ôâALÈÿæ\ë ãœIžj+¡Di‰GËÂè„óoîòõ <Ó[V·¾|p¤Ý÷“üòç•?…È€ÕôJÁ¡'÷v’p¹íÚ}`?JÙ©?¡î&áùôÈ/¤ñ_yÎj„6»„!²Z¾”m[¯P; · >wïøP€ýû4JÝŒœ?Q1Ò8—E¿?¯«ú±t}r9wçuIíipi¨˜êŠÌ”‹ê†æ-Þ—Úb Ð\o÷}³á†ì„?9µr|çhÐí‘‚ÂH,)š,ï%£„öûþøïÙ„—=ôDåÉFHýgŠÿå辶þø·ÿîÇý¿ÿòÅòÿð¿ÿùÏÿïû¿ýçÿóïþùŸÿáü÷?þçÿùÇÿòwÿí¿ÿñ—?ÿÓÿÿÇ?ÿ?üÿßþù?ý?ÿÿýþÿ×?üÓ?üÝ?ÿÃßÿq_âW_îy}ƒ¢ÓÿÁÿÝ9Z‚ѽïqRȜɸÑc ×Â=»‘¾ !dµkÙÝgó½±wš« kžÝv–YùKýúþ| +Æ-ËÚ ®oéUåa!ÿúQH$AÃà7¾a%SWÇEòýEßMßG +bEµ}pò5ðvDtõ„ùéû¥+&¹çÕI¶ñ{ MqÒ3xd÷V6†;m 0ÙJjzG}׬O÷z+ßoPMšáì‰K¥X[-:L„@y’†GºU-V-äö'Ì+š¸\GDâs +pD”çœðqü©É$¯—·ñh&–o>…¶ £m¿ÁÕ5O÷*tý;”|SàÀZëlþ7¡Š ‰*Ý;D· ª¾D‰ŠÈÒÃjœü`G*ªk+Œ…òò»¤hñ'tüÈÔˆ@AeGÔu@>DIH¢^©”(‘ð­f|áFwJ µÇÑm“5=Þ+væL}ÜPÈËO»=qîË·œ€:%ùŠ²<åH +@OªåiŠ[X¬ê£>MÊàöõ›ˆ>È{$—+Eþ…ή·ó)Nðð„Æ¡ÙßùƒšIÙ´ªxBcøì7ˆÑòc¬„=í~“®ÉÇY̦YO˜¥å1z†rø:ðÝнì¡=Þ›KÂŽºóÊcPcÌH(øB¢#Šz,±—‹ã²Zé ·XsÄS²8¨3ܸ£¦3Õ*'õØjOÐ(‡ä¨šlv,Šz|œò]f µb„ªUT0+©M/¶ER#°)"‚ á¹›3 B;*#mS)oÔÊÌã+c-Q8P‘~6#ÆêF`j@ÄÞÆwÙÛ&«Y³òA…(OÕ¨«˜>«Eá‡áø ±ÇP¢‹9¾f¸´ã4¶ÇQX‘#)¼þá•–àI6Ä4Õ¿9OS†g¯Íwz'4äþ¬£{1K‘¥ä¥C¡YJ2ÓÉí DŒªE\̤=l¶ƒóÙêëDð[EÎÀ•3cbÊ cBgè·3¯›‚5®Ñ|hdx^±Рدß~õ©9·ÈÔPh"‘™®©¬95lö +¼èp7ª ¹WÄ(y{†šÇðV'[€ºÓv#íÿñ½`uTÛ÷†EȱËÒ6¦pÕpDDd`LDgQá_1QD¯gÒ][ÃáHU,£”‚ÝQ++ªŒ\çJ5•ˆ  HÄ~¹ŒØïâX"j¼+Zõî瞜סÅUzÎ+ÈIêOJʤ?&BR¬"¬’pËAcÐmìÔ0µ@`–R;3Of1ƒïárçMW8 sæBÊ£ýïš I ìnfS•ÆUY‡ß’DÓìj±ZÙֻ̪ëdcÿì«îUz©_Ÿ£?^ö4 Ás–Ì?˜|Œþ|³G=൬£Ä„ tQ´èuz5¾&ù(ß,…+.¸Šìó#çAª?ÌF;ó1‚ç°€ªÑ ÷:À eÕt/Ô„Ðï[™#ÁÆÄqEV¿ÔÛ†úÎrIî‘€áû‘|î¿õŠür^©†LÛ­ÅêÁSß"r8ÿx¢Z…RÕ1Ä$ŠV„›pÎÃè$ Wáì9ª/ð²¨í Cÿ”ãµW‚u[+Ì<3€ç=-")Ô FÂ+ÝU„M÷x/fÅ})qõ‡m7°Žrï65Ó€‚ ;¯ ­õ{:XìÛ÷à‹Þ˔Χ‚%Á"¿ÿ"‚‚<·¦t.µ‰Òab╼¸=ûíû8.‘Èy\\…QEáÍ£Cbû0<ÃY 1À9>"Šî¢žP(£ <Ï@t„[#9³ÈŽÊê©£—fd§mè +#ÆV>#¢šBCJ ZÏí›ó¡²ØS®d/1±k/È}èô…l+üÛÓUý& +íp8’PN¬î+ºJJÎ~¨ûV"¬ŽêC­Ñ€má¬YÑ»oämùTk@s±t»À)™î8؃–¼¶¨Ó7: ¿”jïy¨Fi[?aŸNGe=͆’áþ‹íee/ÁÜ_Pök Q»½–Щ@Ø”õªxoôý56ö¡ô7ý­C?Ú=ÉYt¢(^ì(TMBXÍ•¹á F%Ž{šÜ«½ÞÐDÈóÙ×΄ Ì®t-š®¯°àŒîÑÓÝãcÊ^ö‰ …3pš”%wĈm‰Š‹5hþÁúKZ.œ+›g¹Çæ|Òóc³=PãNñ5w2§Ð.s…(ßðç¢Ú-b^yÓ®x[âK $ÕŽ<‹æÍ´ÀæÓ 'ÂÏX‘IwÌNá’‰Geè)u][ +óòª¬Áýqºc‘ª™åÞ@9 ”ÇV•ñ™ t@",Mx{”Ф…%È +ùÔu ‡UƒN‚–òtY­õdéðzlȉ°UmÄ+-S”ìÁ ˜ãHÛ¢Ú!¹¶]'åë¢^ÃE}Îøô~F~z€+V/€•ú;øo*;¬ñÑkHÙ‰@™Ã>¹Ž€¬ßèµâïq?¦Š¾Í¼DßÏ{ûo"”lœ¯'ñëƒ0;<­gDíMe@2•z§JSð•~t Ð|5v$Âã=Âë¸;Bµlkp[ŸçIÚHbJ;šð&>c•ïºâía¯ñƒ γOúá\ ’¢*Õ¹´-â~7±é)Ø'àZaå ~Ù é+Éûˆð'a3OÁˆ1ßæã4€úâÙ/® +m¬m•:i™ßDeZ4æ÷ïéókpý: +?!Óéýò#[Õ+tƒŠú.(é ЄRG8ˆ·±€R.ȸïǽ ÚÞç=ñX…z? H½槆ˆ ú”:°!âk| Q„p4Èõy=¼>¢h3LߦLdšCûïp³a·6õ¨Ñù¥ë3âµÁæK¿ÞùÏó`–µ“Ê]ÞŽñ‚½Å_K‰Ïí‘` ”ºÉ¿5MaMå½X—Íóœ)hùËF5+(bÞe#|¹)ˆ°·-G›9˜-9¶*‘¯.JxI"0Dy]É–'ݺýñˆb°í´:Ó¼!¢@÷Â¥)dXÃ%dGè@N„é(ª1õü¦HšìãÝß„õc/ˆ 8ŸPÜó‰•N/ÆäàÍt8™Hî»çlûVwÝ„ä×q¶+õ›“—±Õg/Ô"=gCO«‚æÒwǨh¿Þ—ùú„ð•ÊÎØ +ð+˜veBf‰‰9’ýÞ %žL‹bÓñÛq/‚§‚ë$ õúq +šB¬a–_´¹ryÝo"fôÍûH>¢*‰™_JÂœø²\Ç¢ÕÒNéF¦Íž¶;’Ï…û±‡keûúq_ͨ Ö¡ÍÛ{åÚ=Egʵ ]e¾JÿæÏì©ö{㞪Õ[â|B§Þ—aN ¾SQuYóýKö´k©`‰ë-"îX—!Š»ê¼õqžJ1­3yëµp2±~21îX}bÏ +e¬ÐÖI‡×ÐIÂSÒ8"é±ÙGù¹?>±å—RÏLùÎl€[s˜Ä|GOA5M×"óÈ,# +Ó'å˜û©®xD{8Žÿó‡©Á”–8Ã2Ò„×$µësÁ,©“Ü‘`ç‘ XˆCm¯$ïˆ÷´Pu‰Ã(4+ÝM‚Ô¢‚È‘Äú¿GÄ"¶lÿÉì·êÛó<çmÊÌh5ܵ—’ÖSƒÏˆ•UmÓúÊ^ߢÚÑ„ªME²*ú¿‹½+XÊ[A¼-ìºýI{a%ç«?z(y¿y.w]n7sC1öD‘±tR‘¯UÒcG4Eý´•¯€þÿ‰ÆûÝ̽¾ÎKBaÇÔ¥-Eõ­Lò‹8/N\Yù²5£<„8 +w×ïæM(kDPqAÏ©gS’tKƒ/™ãñ?r«Ø?‚cëa7ÇÎ)E%½ç¥"[Ÿo"8«ªqÓn¾yæGT7˜>@èÄŒ™è=lº%¢Áº +ncùuà5€wŠÆ.(W¿>UU»À… Y æk¹¡†:"áþÂ9ž—n•eÖ‚fh`×öG•ã\$ô¡te§¶‚C(v°¬«IÖ;ãVƒ÷¶ÖÚJl{Z›ñ%éâÕ:>#þt[jô4'œ¼ù|FQ^§½7X{}1©Ø÷¢ª®^øGÄ7kãgh> +´î óYzî;­Æö{/zS5TüŒÌGëó=l´DE« àÚ£åhõHHtPêã FtoÀõSjmÊÇPKÈtÿ²ÅMœö ^2?ª¯Ô»V3dé5€ÊÒ^O•†ÎÅžUöOP¨„¤€S³WŒªê³½ü‹E¤s8‹â ©}?¥#ûκ÷„„Èžg' +yÔ±À«°5Ë¢uÁ\ÝšYÄ8¨¢«Õs‹²ˆ PFÛßz'uðîõF´ˆ°ðjDÒØÂ]ç½[w¢v¶Àj\Âá´±Ê?qžà‰^¹]F8bö»Ö4ñFØ®[’Û£<¡ÁJì¡ÜÒÞž`—ÓkGÔ>ˆÚtQ2ˆa±þ w¼/¾å¸åòˆ¡IqE]ÖE†V[õ8ˆŽ0Røø­tiÑRõ¬#剸öv“žW:]6Ê,ß +œ±)ñ5ó¯êý¢ ÉÁª¼³BwüŒˆª>¬û€ÐZ-7e#œ%¨û[¬˜ïÌ`ÛH¾ç·Î€VV½D©18üNØj™‘èžZ<>ÁI¢b\§\½f(Ýõ‰ü=®¦/­ê–l~¶Ñˆ5ò<Ùý V»†4çôÕe‘zPNÕÛè³Ý™ºß~údL°|¢Â©ÌçÁ$Œ¨Jcå««db¥Ê«ËÖÆÒ`µQê£"!kKþ°Âì@Á%!}ÁvÜ; 0+Œž@râëÜFŒhs„`uEκO›0%qèÔŽ0)ñ÷ÔvZ8Ò­@x¡ýMW5¶…šRBdœY (£îMS¬Pèi7A4ã7) œMQ¶wT£:O”-ˆOt2ˆ€šNDf1BÐá^ù¦UpzáÌ[l^p%¨¼}³½®ÔmǪñ‘Ï÷É~ܽ (ä7ûfê"ï'†!?Ú= {Oq ×Ïy§Ú,Üч­xYðåJÅCKôü©ôÓñ¥ªUzeÁÚÒ´ÙcWê‘F<šs¢£pªOD*ƒl´©?{'Ê °Øã‚^÷¾AD°­üÑz +hÌŽÈ4§ý + < +=ÊóZ´cƒ +ÅèA‹"?q!삹 |"ÀdøÕ(|íÝú"²Vr»õz&Š‹™ñá—èx²ït¾µs±Ø½Z&š?À&Ÿ\÷Â6/²ö”›¶)ÉOw÷–KÅÓ6ÑðÈÑ™y?%ù$†ñóSãÑ—56 ØFàwsÿÎÿ#ÙÃR| ™{lΨÊú™L‹J»ÞC (°Áhˆ9²µÃñQÕø÷ˆxhÔW÷Š´ÿ7ëôyž$bÃǪœþ\„F×ô3äÚÅâö5:~5VXõT1)û4#ü¥ï ¨aÚîý üÙÔ¹§Sh¦?¿îú¾D±Dd_EVt´2JÀ€ˆC=µ{¸ùÐj­sœE„ô5^W‰4T#KóÔ–Þ¨G´:l§—îKÕŒ’ø¶Eˆ”þv‹üŽ¡†œ¿°õu% w@Lª˜ùqÊ0bh!á~C2~=:7e·ü(—&F6Ã4ÚPìëg@L ïiÛÇiHÐCM‚¯BóæAmOC…‡@‹!ïûûµ"eµèÓûù¾ó|ßàODv¾§L{€žØÎÉ`§?#þt€p¶]&Ô~úðçˆoB­'BV¿Cjh3âÈ–È”‰ø1FDÀy¬Ïx\êÎS®€¨iI‡¶á<õðÞ¢Þ¾‹˜4{<š‡ÿE‡|ß l¡i ú?pöì¦èÀºÙ[A… !áB»3´¤ÄA#Wý=£Ñ-ð×ȵ»B­¶zq³Šq£pÿ[ˆÌ3U%ªát¦Ý\ýºR¬–hwÕˆzH%K ‹Q#˜a5€ñ̲b÷ä•ØUó]Ø¢±Bd8]‚}Ûš±jȯT”¡sìì*»©™QÐ_¨a–&2LŸ»oÜ®ò{D‘ÄyO…÷'}Dµ¢W‘Ø,g\ùÀ~ŸÂïÇΛJ…ƒüŽª3·ù#âO'kÂG×О"¿9O±—Äò§Ô¿@eUDzö”ó›ˆ^œÊæ7Çr`p±ø²…Ô''f‘öëÍÛüƒdeÒOY–÷ÛGÀkB±² dÀAx? Ì4ðœK¦|/ImÏ´lHÅ õ°l1Žnq¯ëí²C›ú©6öÍMÑX÷º±#€J-rp4.ÎFø ¼‰T‚j©+2—ï…bÆ>]7à¢Vs ™<s ¤ýSïS§žIÀd>Åùâ4ÑÃåíÀ¤[¿Kƒ*Î;(…!©*²YøˆÄÞžCÁ ½‰­ë^ÕÚ7¨“zW¨ââϨ N¢£Ó‹(éVc/ë—\J›û½*€Eˆ=2`K¼GĨkN1û}Dǧ~s˜ t‘÷˜BW>mÿ–5Ø¢B»ï"“ˆ´Ä›KÍìì–éÏÛóšÞÐW‚z°÷ð´LZ¾€ï#È @|Ju+vÈú +‚Õ-_úÔ¤ø4<Ó,sÿ¨í€#*eJÜÎé†À29PØÀH‰S§ÚcS¸€€>5§/POv¢üuP}Ž–Y{˜A+‚´‰ó0ö]v6u•U>£Š|ï"Eæѯ)ýÐVöë[‚ù˜˜§B½—„?h?¹ˆÈ/Lµ/8V ˜uÊ¡…Xâ~ÄH@Fïp¢Äïä# ˆúã (È]ì™ÖëBÓQ³ÞïÙ‚¦6# ¯OJJdsëˆÌU +„O„÷èñ^E1f¬â T‘g¨ô_ä)bYâqJ BS*Þ3*à ýÍ]ÞU¡þûg ¤³¿‰ TZ_ØÓ_Ì!®Q…|CØ{ÒD£gZÙ£ìg:k +xcD,"?#â^…ð#$£½š|{žqH³¨Í÷Q•0/’½³‹ÍŠés]|õËìî¦JZÿŒú›óuàfì¨uÈu§œg`{-YQ{/ÎËŒ œ¼_Xª¾‰¨AÚK&tƒ¯Ô¯¢ÐcV±;gà˜dZ$¼sZi•‰•€½#Ὄ‰Põ7w’)]Ûoγ{tÂÏu¢Ö„NW*ï sdµïQÈ¡ÈlX*6KÚ49œ²šÄ™P AzÅŽaÏ‹8Ó#Ã(rÙÐ’IÓ¢Mg[iäQÀ,T$' xHÚ¤*Ô¨ÑçMZ©¦ l¸C¯ð@ÇN?ŽT% SÈIMŸÂ¥¤"l¤A$ Ñïó£8[: +¶úï™ùoOW)ö…F\§žH? ö#Ý\0+²y=K(Ãì‚ 7 tQ+ÿ{´QZ4´5Nôñéc“F>Mºb6¯ƒ$Ú›|ä3‚•W—º}úC¶ÿ Ò¨`UeÉ‘}2]`,|5 ÈK§)–FL²êi tÍU&êÀ<輤‚Ýç¯îÔuøã6¤”]\þ6âïb%úÿÄz—…þ+º¹B³¼Nð™‹l¿w^~Lvú!14‡Y²ç S"ªûî 8¥}š!”kˆñ„òìèñÆÉM ËÜŠ¡;¢Þ}Ã3¼…”·rAÿ }$p×xŽrscÁ>ãAÈÝ÷!j9X RT›uŒÅªs\ˆôò µkf”3ÚÞ4îE¿{1É”ò׈˜ŸnWQ‚$¢â@°ŸçJhsƆ6}]©>6sЋ¾Qà&’}–/ºÚq+Ok„s[1C³‚r¼ï”qmï†Ùtˆ¦”ê…¢Â{Àæ3L‡|O}y)‚»&9Ú7@Å´˜ )‚™yNäîöy°ÝeŸ(©Ñ*‹ƒ(©DÃéû†’ö8”ˆ(¬…X­Cðb…r\ÈŸºãûv‡DV2\0×ù:ˆ24†ç ¸ ÀR6ç4P¨æ'*SÜz"xÜŒT&"ìúíŠûñ’£ß9쳕Î[Dw‘÷£¦€¡Ò›ð'<¿¬DÔz‹‘lrË•Žûß7,'„'犓ÁiÞ¡TÑéT³¿Ø›³õ6AØÌÐÔDÉuÚ Ãí,÷±t „nù9EFÔNFdLT§X§OÆäºJNEÕ˳¨’ÀÖÅå_ÏÞn"ø{XÅí=P«Ùt%#o…Ër~.ncºIÆz°;ýWä‡Ø:øéˆÁG#U&À]oågmÌr¾¨LŠˆâ?+E©Œ¡ïa¾F‰jð@Í<t:›žîÑ8Åñç€I ÐÒ´}FbÍ +×r/y¯«á~¼U0òiD6dæ¹R "q™¯FHmrKãJB-8z©étÆî@T´F‡å$Y\] 6EÎDÕS‘™‚”3—pCǪ#p—Ê ¿DŽKËÞÄ-E¯»%ëŠVáS¡íš#™GÂøz‚6µ$?’ŲŒ@JQ› Û=Ÿœ¬À̶#@¦ß"ˆõ—Ÿ„§ïË£v¿(øѼ¹D§"AA=NzI3TßL8QkÆ4Ícû*O]~ ›Ìj€èëЀñz~:ª|²n"´‡jTX.”Ž–õâ½O1Êq_+¥¥.Ð;yë^ËBb¨%¤úã·__~ÇW’’ÀJR˜µ»%XRÙ‰QJúÇE©Ÿ¨Š\Qò¿ pxù*tÏ3“ô< …wâ1Fó ™Җ4PAÚåm?§–Dž±¢qM«—(êq€€œG. s)‰*Hˆzõø.Hk­ã2Æ„|AVÈÍìJ"EµBTTA¸øð«í1£é ¢ô‚]9v? 5%V SLÀ°Ô+ðU–«:’îiBK +Ó¥'*ÍÖ9€µ%æ^R#ò®Cín/ÒÝŠ<ËHZ÷Â}Uà×ur཰"*g%¾¤ÐW÷<äàD<‰¹$áãÃÖA"4\$Â×%ÎÆëJ{gõ¢Ø~:Œ[+ïIüX™ÉñKvæ@¯Ü>“~¯üE°N¤-¶?dãÔú‘¶ ¨ä7e“Ùdyšîõ*í@¼’žhÁiYAæw;WÌ–: ìÚ84ÛÙYIãø¢øLò>m¿°9h´a²ÃZ *å†ÎCÔ‡ñô‘RlpAXÿ£v©ý ˆûÛå¿ÁA¤¡W…R¡á‚ê«Š±³IŸäíÙ”ÁY|>÷Jû~‹”ÿ¢ó5édçäªHMnêAhCáoC‡€, €†À&êKxàÙÇfe‡UzÁ—ÐÚ#Cw„” Ù{ÒÐóÖD…ŸËçDÀ·ÁÈŽÆ O2wÓË_ž@vEåùÈÏž¿Múàà[Úƒu®™8öÛ]é·Aʆ ûí¤êÌx‘~,˜P+œ±5f^ck 9Ø•òäè4¯Ë‹ËuçùaYiÏC]0$UôuØöˆ Y¦ƒ½®sàuÑtS!+ÙM±Û(nlÂ*~Tû.³ó §¡”ãöÃjIhBQiA +Ç*ñ4b%`$=µêT<ÌYT[V›oÇiå.iô3Ä_æ}¤Ö j€V¦æJËBV¢({Û#æ*Ú^¤ìöˆSøS|/·w[Ó%§W 0xøq?@ñ*R}´â)Š‘£böêhy§H‰[‰öÎ"]°.øBô€¸ù%ÊÕBÚ‰ +¨¢2ì#i­Í¦“çÙ#xVàýïQÑ™‘?îŸ:Ú‹‡¿“6 O‘(ú›æ>(¯`ÙÁw ØíïsR­7jæÊC"£|tde-{LÌÜ•¥ËÇc&[ÿ¨’±IÙ;Jï·œí¤ƒ + ÖƒîÎ¥ð“(ÿ-µ›H(1î²í ‰¼¿Mh$½@x(ÈÉ:Çç8#ø;L¼¬îAä ÅgvèßoɤI HÜE˜–!ïYŽø€u\FÀ¾UqN1øù™#}X"Xþ㼜‘Â?—èT.j¹j!çF…í):vUI¤í<Ú#gå/]~3§ù=75ìíÉp™.B‡ù9Õx$…ÆÎXÌ p2ë¨$´¢Ù†þ`Ø&Á¤°=ÕŠgð#ê—eô³GzG& àIš EÁ†cp E9½D&wžlXk¡©DT0$á¹+bÙÐÿ€•—ᚊsñÔ¢ˆˆ=$yBùT8@‘”+$2aGDQ¾ ûµ=øR\‰¤šFb|^ÅýSèr>Uf~¼ThéÌ*÷&«WvàŠ Ñ' ¼81ýöŸ4í؇ÈùÊFè%µ}D|”Ñ©v:¨fœŽ-êr{ÃKåe¨µ*Œ¨yÕ|¨cÁ7'õ mqU‚(1;Ø´Ü  ,2ÙŒ©)öµ£Ü¥d£ˆšªIJS<4™Ò‰ leƒ™žˆ£#±¨£Á##¢½„PwMFQ%Ž(Þf¢ÎiVxÏ•f3/B _8ÓW!"´“ +–AYhG”\. ‡º×ô õbÓ¶W56.¤Ø–—ökÁ18¨(×CûÒEKƒT u‚TøË{ø|á €PŸ8vØ×ù¦ÄB¾¼÷“¡…Mšð>ÑõÚhG´¤GAïiìÏ©Á gKá DslÂ=‡rºï]ÍÊ|WefH@‚Rçó>¢² +ª$%$UY0yë¡}ÔÎfמ1k1ó=í<äxhç¡úBôŠmØåsEOoÙòd}Ç@ZƒÊb+ë%@\v„U%FòÝõ CŠPùh6—ŒŠåáû"í€.ŒÈgØÌ<ÎæT"//]´ ʼnç GŸB~Ä8¯²H÷ÝUPL°JtN©JB„V1·‰ë‘5¬5=Ä €el’ìôן”UœâAJS\L©m„N£éʸfA¨)B9º^•–ºÕT` yØ{úX-Xä ë ¥GWVwØ?H² ®O1´›— + Ή:1¶ªQúx.C‰—æp7ÈBÁæñ0ëúâní#Dû¿O'ˆï?O¯l"xÓÚ +!‚¨!îœàF éÄä¸oÙ4‰*9:s rP½£¸Ç®RN‰Z_ÏQ‚*@Þ]È'˜)̨ªêƒm/,Q&š*P¾)_[ßð±9®Õ4ðD4ÖÃ~TjªûªXô¶Ðm qÎÖE£;ºç¨ò‚Ç°Žû©{ß…±\.XÏ‹Ý—i¶÷(M–µmÅŸëgP¦«Ým”µ¡ae ıò‡¾‰ƒÆ¼ç!aS›zù]eâ«S´Ní©Z‡-€uîÏ<:X’kÑíAv|Ô`dbˆã£¦Ûa„Ôø¸êè9„é‰}‹.(ו̤ΡK±g¢~ZdDÉvÒ T¿bÆê¼Ù;ü†€G=j áÝhŸ +ùª†WQ¨+.”âË®»wG^x<ƒÒLQª#ø²» 3ƒ +±UùòbÕ'<§‘“^á–t\ñìl¹ÙêÑs„€ä÷I™c+­N˜2è{¸­½©Õå¾ÂÜèÍ¡—`¶¥À“#€ôØK{rD—•vÙÀï’ü`”¼t¤è`í4”„yÏ$92ˆ(&¥W!+šOǺ§™gïܺÞõ(¤˜jä-4 uAØQn¶ˆ(`õÑû_$² XKÕn–SDãõW"£Ÿ„¥£¹ùºöä Íl¦G\ª_‡Š-4؉¬O„T?{#J3ÂA†3µª ºï Èãtôk´p¦köæ´q¤k˜2Ü)vpzÊe$Ò6ÑHy,vÏzBÿ64ÁÌ]¦ô §ª6iªÛA*Œg##kçá¾ÃtQS@^ Ú^¥Œ7ÚÊϹ@· -¾ª– û©>ÂàƒÛGó¼kN>IWîÔêm.ÓÆ œ€mƒ0Ø6¨G-8<è­£¦àºw,••¢ïºdì!ñ¤“¢[£ÚPW4 «´&ö@y.¨¢åø=Ÿöz˜ 6^¹C@ …¨dfè|ãC¢sQìž â³nD©,[‰‰yyß™l`D1aïœeÞGr~ÒC'K"@¸¡´å"KóäË‹ïn ·C¿Û·á&æiü1vþ(2Dö#@3ÍFŒÃ‰ú»JxQGcƒËœ™ .ï]¸Š˜CŠÒ¤Ý³ÙÇT-hŸ ö º&rHfÜGƒR‹d0vƒTî5gn€œ£;Ò¿œ \P¯a¤väõvvNÅ”¥Ñú[©cžjî€-YYÛÙíÏ'¬n«˜wšû aèáþ6ŠpGÁ×>çôFDPyV¾§œ™*ûk +&‚Zéùº’öðè+ÐkFTÀ(Õ…ÛaùP”ˆªZ%­þø.‡%RÔ‘'`Ä—eE~]ˆ²=m²)ͧSߺr žÎüŒšrÆ3úDK’ÒÑB›“}Î3šªÖ+lšºš¾è7‘¨±âè!?ÐzyE¡½¨ð»\ièO¦IØè’ZƒÈl†oDLÝfu5²3`¨¸É;H +L[ 3„ú£‰À²IÈщq¾Îk–GÕ*6uy^›UÞ °W˜ò@Ê$Ë!{"ƒ +œÇ¹€æ:óÚ›q.‘NJã2C“o!T`Á,ù În{¹^\?Í+|$¬Ð’Ù«\#4Ú˜Xê¸Óùlèf8™/¾tÒeêxÏp¡¡Å¨Þ,Ç+\p@î‚\Gv:ö¼3]id:tHêdɳjŠ°_x@KôóΖõÀ›‡å·eåuI‘wµÀÙ¹Žo`£0O!¾ õHæ=lJǃ I¸õ(éI :]9,Þ¶ùãæã¨ð3Ïìp‹Â`O Ð$bõSëEî^ˆ¹T£È:Þ´ÎAà4(&rÀí,ª"½ÿ¬¤Â`Gj¬÷ hŠ ÜÁ_ÂoI.ÈîÝmK4ÏÕ¥‡ÈÍ[º§‘œ–Ð’bY +ï +e ØoæI”‚4D©y‰ ‰0N»ÈŒ!€nOÉ;-[FDÿx¯xèÖ¡!Å 3!×Àž˜€Ë +$*…8ÖˆÇ(¸E%Î4â *DÌ_…ò׎HÈçA}šˆXŒÐç9HUÁ#~OéK’;SHÖ`¹îÀñG‡E'c›: q£šú¼©(²ŸU™¥‘À•"ýÊÍ…B^RÚ‚/˜…ƒÈ€½»ë ;ÿ¸„ZNÒö\öÛ)Ì‹iÌÔv¤òVºeþ¬µ W +®hÝ› È$aønP®ò­›²¿ÉúÔ€)žeB#ux~Ñ^šh¤—ÑŽx ÷ˆ_´ztçÔB‡„A•Á=?¡5‚uDGZhßûú~ÀX5ïïuUVàž 5 ´óîxzÌ#«zÃäÀ +ìåýù&àŽh7ç½BkwÜG3‚(¯Is{g(¼$ln¦ºCvNâ2@”(å+3¨Â‰ŠB,ë̱éȈ¾VŠ}̪.lDËгÅsÝËæRƈv"Rðù¥ËUµf¾;»z¨Ê¬[…™û¸ƒJKë0ÂÇa„“=Mfb„MFD’%D„¸èó¯4¼*@ç³1gp¨Ëƒ ˆõµ9Bž Dý«6bg 3™V¯t,a®(~•µ7 +Âß%*œ.¦Óâ¹Ð‘á] RÙœâJ!’ÿú™Îa‰ÅC[žzNÓEÜòB‘þ¢èé1)»~4”_ÎJUƒv2y#fIÞgµ<üT•Sü¥Á.Oà9àµSRæ-Q<¸üér>Y¬À(LZ ,ËËš¦Ä®ÜBQ G+${˜:\‚K]ê8YwGÈ\Z<­ª§ å•MáÎa ÚÅ õÀ(èX7‚*­FôÒ5ëð˜½?ˆBÄ*/zeÍ,ã)yÓ’2‡_«úÒ-ÕÌ(®†¨~—ÊŠ³ä,‘Í)Ö8ƒÕTÖWqùÜØ«¯½Æ‰4Ó_Ë-Q@g={gšPÒe:æ€peš³Xp ª†Q/%ˆW´¿NL @›5.j®ÉtIR´ó¦ÌÞêQ  BŒˆó•Ì/ª“õUü„&7Š„u‘Jjª ph²Sk3ªbÄn®÷®X {Ù"+G(õ•~7›~LFºÂ‚LYI@ŠEÕ3¹oæîË8úeS*º‘ŒàU0¥ãK®6»ãº·@õÒ`++>ötKúÄ.soÏ`†ýO…dŸfOÍ‚ÏöyÅQÎX‚ÁìWûë+@ùàñæKa°„ßÁ¸Âf­k»ðˆäKã@‹µï¦’ÆŽ’®2“=ˆÄ;cY(R Ó¬‘¤ô°¼Ñ›] 2JŠÙ©öNEà¬S¦í¥â‹ ÎV½‡ª"°[T»Íóý]a¶#ìz±'J/ŠfËCcP¢,…WRáRë¡W Øl!Œð¨Ð{ëÀaì€ýŽµ æXqù«?ÑMÎLGŨýÂ2Ô¦vhF°„dÖÁ+f¦hks¥'¨CG™ h–ÜE¤û¥ôÀ ìYb)Qå6¡0Tà%õÐUy·‚Rzœãý@g¹svÊ‘îÒRq‹·‚ºŸd~ Ëž’ù‚jÐ + ‘©}¬$%T%Ñõ´¤ª*À‡AÌt=e¾Ys¾®äoÚWBtW‹Ïaªµó6yOU»,ÊdKV M=h+mW0ä³$5µÝé|‚bÿå ¸àpì߈ð¨$s]˜öK/ÌÜJçiH…Ú–œ¥hÊ*cŠJìýÐÆ¥²úE €å‰¾+æW•IY +‰Jˆ|»ŒS“&Ëî+D8è¹ ÎÛ­}¡"›µ+%Ê$pdÓë¡Ì9 Ú@õ@ƒIhx|¸®å,P" ÓæPr¦´¬Èj¼~GS²J:¶ÏzÅ%Ð-ö‚*~мÝâÉ^–—´Õ|œ·¥ÞW¦ªü²bj€lb¹y½Rø%¤ðeLå&–YœGhá»ÊŒ†¤³ÍŽ®IÄùcO•Y"ÒÕÿ§DH#ñ!zD…¿dÝ“;Å÷äe6C)؈`SÁê.¯é’í8•æypSS’ž¨zÅȘvjÈ-ˆëSj8hd“½ÅЦ¦Ä»L¡ýx¬á3d gd4ö¤SòšaÀ²®Ár± +›¯)‰Ý•,mÇbgQþArÈ€g.w8 ÁÐ/ÃR]EÞ¾4EKðƒb„‹ Šö Å¡ýp +s˜ìO3#ŠŽ¨FüÍ¿ \‰`¾Ã!€õN~¡ êhñaz2;B&2ãH¿|´&Øï°Ã#ž±åï4+€Õ’ƒ~G §BGÞ›s†òó +ûùÐæ@°kâiVAJg¬Ž®% £ÚîT¶W?¯@‚,¡ÒVtd÷yeÙ]0AY[ƒ>RJ·K¨NŽ—Šü +A6Ê&7ZËêJî[^ƒ$»Ý„Ã0¥´0IÃmiXðœí›ðÔö½b“vv “­6©Fu´þ@ÊIÒ×þí`ûí“Zcm5ú-ìÚ‚þº¤Ðüö¾apS_’îŒE ô¾R²÷Å ! _³#ÔiUþ‡ +aû$@á{v³Òíg²\æçð%4Ö+rìEÐŽ¶ +°E¶oO¼îS@Ø#ØåÀÌʸ 9 !ÌÐ÷ðKš‘ÍMíÝô¡ÉšË"ÛR xª®Ctd+ÀSëÒúÓ5xØ}ÁteAôÇeA„ß¼Ž=WvÞ +Ù¥³ìó°¡€¡y,ÀdÏä¼T·ªGÁijà åííë +r+GÏcáeGaJDTÆbô4G)•‘ÚÇyr´Ç ÊÿM„Þï‰ü÷Á©þ¯>чß|ü'ûŸïœß¥ó•G¹æ©á%Uû9¤”ÃQ:ŸsÒÀœç3H²žòP-Éê pµGJ™Íÿx΄íÙ»u-²²»ŠÙâ¡èÁÀl îOØXÇl`>T]Ø›žRD´=Íx™€òËû‚¾$|x†‰âØC"4áÉK§çQKl")FëðÎmàÉ^ž£Æ9¢Oœ™yÚëJ6vTÏQ!Ö›L0d4àSÚ|bYTŸiÉ\©Å5BéµIRVBæ8¼q0+º«n>ÆŽ#´Š@[˜;….÷ÞËÔTîÞÝ4;ºeVÔzr"·D$ųŽ£Lèžqài^É8…„rWX*©¤D ݞػ'‚Š@ø4‹gig—Mçs¥ÞµOÞ“ËPŽaÚ°uêE¡s‡(@JeöRæ(Ùb'¦Á®í.vvF×à ü@l9šƒ²}Éõ;Y[;Qì|Ø7Èß;dˆ¥ÐeÆl@„j);b&¢km2™âÆ롱å`Ä⧠¡‹Ô>âñÔ)YáÄ rfR)hñ]„OÎ#ϱov»bîƒuhÐßMf§`œØ[hróqtöê~{MpÍÁž‰µ¡_Ùí-¸c^i/¿P¾ÏÏ7§±öIâч Xyh È)Ÿ{g%²Â½Ì[µÖúÆF€Œl5;3„"Ûjÿê Ã¤*¡kxÚ $e¯Gn2sÂýúêj¥´ƒ?ÅÝʪÖ~NˆSòQ!‚ýxðêuM›8ˆø°:M›€ë¦†»Uâãl(:A#ÜK¦(‹ qÎئuÀj+"\Ü¡|ýë{ÃÌó5:Šw; +Lú>ψæùŽEÿl„ÅÛ.° .ï Ý^§ K±¶ã¦W9¨±A…ˆÓœdG³ô¸ ‚bþ<ÀpwƤD}EO>£Ê’çÅÖ%Ë‘M×) º¨K…ê˜/PŸ‚ä®+IßÌ'¢žˆÃƒé‡j÷qpàòÍ6{/åÇ@µÚfkÖ2ú7ßÖϨ6PN¢÷£ãߊõJgå™A|‚áÊòÊû4­XAy¸b'šHð ±X‰©47íYž¨Õ€‚ âfÔz+Õ8è£&¹ó~­e›²“?Z#³šž*U,„(oáéÚÊ=Qí½oQ¤FÙz˜eéÞ»çk(n~œÕÜaN`R¦ŒýHHëèÈ¥¥s•4V`ÀiEìy³»Ö>Éßô‘Â#±¸†V~ÕÖ”íy¤v{èy|FDÝ0úŸçŠ­ QgoëÔÃ'3S [‹¥AB™4ò6Lä1åê²¥* =’Qôˆ¢Hâyìú ['•R>kúðã·3²5ó¬òdž¿Ž^/æÁ<󅦶ó௺X×d’|Ö´Û±ç†#¶+ã™ÔC@X¬Ûù¹,ÂDÇèl&Ô­–GLJwHýv +søQ&÷Ò¿eè‚Û8½¤Õ 醌ü~É’ú˜ù¨8“Y´¸’¦”ûÖE»ö|7hgpŠé)>®(”^¡¹Ç'I µÍÎb'…uÜEìv# ´ëPÀd®kƒá<ÍeL*2"ЀÍAm-ÔFX Ô ¢c ÑØðŸìÓÝ¡®h¾ô—9«µuÞ$؃¢FuWÃ3f…šy˜z ZÀ‹Êf;ì[Išª®KNßþ>˜,èªÊή՚^–…;p¶îê,`ÜG{TóŽ'Ôa]9«¹_¸¢q§#BA°¼2¥ÑKD5ƒìc\)ªXòNUƒ/vóÃ}€ÜH¹íq>¾ÞÃ#5"PS¦h¶-"é£î÷y<Ç”íï2¥ZZq3”ݽSˆ Â+±ThÏ„¼úÛ/Í/¿ã‹*EÜ¢œÐ—l“È7tк'‘¿q0Edɵ)v©i=n^;MªMRp@®âë;õ“®Z/º‰NbûÂþhP ²Gô+¥Ô%Ɖ’ðô<êÃÔ«Ú&,ôꆅÆx;DðéÅ=¼(( •'¸ðú~£²ÑºMbH“`ÀEã<¡ýªÉoÝAòÚ>"bõD§t“õ3ŠÝPSă=ƒ=,)>i³ˆò•*Ñ#xÇ6­è«Åá Y ïáì%ä”>ßÌa}æ\1Ž¼=®óIͽ¸R¨ø 4CHÅS"ò-ób7áv®dœæÒÔFMÍÆ›®~ëD'¿²ˆ L;´,ˆ êy©ã«OfsªàÌ +qùÊA(GV°Q]Bƒ´XNõ T¸xýÚ®k$´ uÜzHúïý}ú×ÂJ!v/Ð BsFÈVž¢#0‘§4öNªß¢q¶ÇšõÍqó–UÝSˆøˆ:¤¾‚Ó +ëÀÐ#ÃY +gÊ{ô~ðhÄì]¹4à·ˆ?FIhÕtÔJûwç9ÍÁ(Çw1™ô€Œ€BÊÆ/É6ѹÀGnXöö\—kÜz›V o‹HÇ gC*æE*ª¬ÁŒ6ë±H—î•ZÈ#Ò+|ì~@2eNÑ^2ò«h³5˜!-«­’’Ñ)ä©â¶"”­*`ýÉiõ«„úÖ¢WçáŠ÷ˆ2ìy‘‘ò iTºõüØÉ5ÀEóÐÌÁ”O|î8[g¸Â>þìG]ÂkÝï”°ójn–ôP4¿O¸GL@ývÊñÛyæ—ãbª%pOxq‰¨ #† ;Ùw}–Ï¿ÿ‡7á{ŽôX$™8sÛ¨‹<Ö‡”] bD„ž—ڨݙâí d•çC(MʱíaDH›{, YÔÀ2‰ðÞm˜rgs{^FÜrò8Ý\7TƒÚŠ ±¢ÌÏá¼×3áð{à sû&"÷èJ•d>ü*<ü:J‘"RˆËqâØ;/möM.Ì!E¡®š6Ïéâ}DܱŠ¼]ßEÑÍžj P6g# œ Z¬LV;dùÜôé%‘<‡@µoÊ5]Ë—G †¶£:üpà + ºôÄi|qSÐ¥±½ËþyÙ +ÇÔ\‰ÿ€s\úøí¹Û¸E¡—V8QaŠˆ"(Ê:"} c5€Ëq):D FDŒfÊÜüŽœN‚´¿I¹‚fâÛÂh8ë|*k¡€ä£IÆ +ƒ-äá°{ŠyœyÔψx$-ÌÀX‡ºÒ:˜L¢HHÐwl²ô£ó‹Ò^"óîS«‡ˆP]±3Wì3 +mÊ$R +O¾Q}hDî +B©*R"í—yëøˆ¸gdÜÍâgUâúó<)Ú3|r‰YžŸLRö`û™„Tæ¼È‹_¡:E“kÏÑ;¯åµÉÇslo¦W´N5Þ9+Ób¨éíEËÉó=⾟ð)— ÇÒ¾9Úd²ëxj¬*š.lßE¸Êár³Jþ*«>Áè¤3JˆMj'SFç‘£Chº+a®¸'<. fM™œ±÷Ü–û7qór’@ª¢ù<Ï•ù„Ýn¾ÕRä[¬Åϼ€D‰ƒ(§3¨¸«ÌK”û¥V–ü{RÀD]MgØÈ{o­4/{:½kÉ(y3u éár7ÍDép÷ å÷ÄyÌ÷…÷8Oó@Ú:@ÉØÇÀ‘â„tÄs|“`5K°̈W”1‡2Bû>‹"ø0ô7§†BÔ©,™B'èºçß„Çó8å¬òÊRªZÔü<êïQê:˜Z$³‚ÞÊ„¡FËÖõ›ˆøTÈÚ“ÿÕôÞäP1yLZHà˜*c_ý!HIï +²ˆñ‰šÉ¥^:œ%ætêWìFúñ£˜ª ‹t B `•!à* ÂÂzìÐ]¼Q`ó°žQîpDq=óÊûDÊM]çpèë¹j5â`O¤ÄéGU0J!ŽÎI1`º×༔1ФÄn6£Ôˆ¸ûÞ¹7Ø£¥;jôœ.aÅûˆè%|ÛšwÛó¥pháPeí#6=ó>øôß©XÐ<Á½Gnl¶%þD©fÃk<„§)ìùPrtöØogÀgt„¨ °íQ?Ó»½dyýr&a7†=æÜžt¦ëkEè|¯¯{cNQj}àG73ÚønÐj‘m}øÓmâé© ®J]Ó÷óŒ~ »­ªš¢ÑDÖ–È5Þ¾]ß‚XMªæ¤Ôÿbímv¥G²óÜ+8÷ð å3(3þäÐ.†6ÎÄö¨!´d@€[-Èm¾ûÏósWen©, »ÑªÍ•d&ŒX±ÖûCY¡Æ—ÁqÒPeÅjòÞX9št²M+¦|Ìûy71vkA›¹qP}ØSY‹îøŸÂw¿(©i=òÑ€šl>À1 5ÙIxÔ<â!è¤Tfúö¯?PPï¦îëp§?HEÿ´KHÎÜï‡F–®LLZ[õ—}ª¿Ø)£†MyïRà† LãNݤòØ$Í$WDÍñDQÊir”÷y,—0ƒwÄÜejS‰ C ú~Ên$Œ¡@¡Þ¼™ÏG˜ÏÒ7´B÷ïñ¦v;Rówx¤ˆÀ_w¾c¥lÁ½æŒ×ùEpεeŸwÌ-óõ +–†ûeAôøi¨±°v)RnØzòÔ'HÂsÛc£HŠ=öÖ¸†{b¾òφH£ÀѦµèº9éçþlÕê5xj,vc *¯Xà¤vº†TÝ.ÃD…B·õˆ%ÔD.¤v£DÀ-`§¶I€=&÷œç ЂöÂ<öª¯óêƒ{ZÄð˜j„]*†Ì7ªœ‘Тõ¬b ë÷¶ð‘6ÊRõp¢%wÊ¡ºd0Uí +ðΤ‚†$o)ð×gX³¶z²»í[O½Ù‘~Ú:8}i+©Hâñ. ™j/!b‘ð´.ÎÆßgÎÖlŽºyZ4Fy¥ +@ ( Ïk¸ƒæt(E—]³žµŸ¹6Ñ +8à©Kš:Á¢ˆ*-?QÀażÆã tD⸢ Wü¢ŒÂmÖC2Î 9M7:)è¹Ý—k¯²ªÔµÎ;]_ûx„ãî+ýC^ø´ªÁQÕBg`„î®~ ‘œÇb,(6búeWV+²S’¬HíëÌmÀ—@ü†Ú™VÄsóx“•œ>zƒ(¨ãòQ*±ÙUÖ†ÿ|œ´î°³1S®ñ_€Ö€0V•¾Í`~1Pqoé Îòª¡pªMiçäh››^BQÊ­ÀÒ’Ù¨‘(€WDt‰ô§VYF(È‚žß”›;#f¾ ø‘u\M¢ç:TPˆ‘MÔ”½zCsí¹N‰^ËÉÞµª¥åþÖþ&V¶& “|ÕÀmÕ²*¯'#QfÝÞI \Û7f?9õ=¦*ÕMžÙOyeˆ *jADø - +_[Þ­AD{Z¶çãj™†Ä~Ôº!À6Òºþ"çnÛÌÒæxþœZþŒà ð ièû#héÄ^fJtR¿A;¡ø†/ïG„z`0S[D‚“r¼G!˜I1¢©Q×$ÂÛ¯¾LCùˆÖV62ᯟÐiÏ3^–ôÌV˜ßÇüæã 8¼£'g²²†·ëëÀ•RÔšT‘Rú@3\EÇÖ¾ ÁqÁ 3Æöî~­³3žˆéT×øŒvÝñ‹Û\ü“â⤧šAÏùt˜PöK„p@¢>JåMs¶ò"±'kño€Öé€%û)}“&ÛJõ6dý<¡ODHÁ¹GÄ£Qk“Û`ë +B^±»õkÃl®Ä_Þ…aÎZå+5y(‹à|®­5ú~ oÍ;åûããØ4níû ÐAi(*œ|᧟ãÈsO=ð# +ö»ÓV}¾cë»Þ1|öŠ0U¼Z;zY7Š¿/ ûÊÈù¢õß?£DRÉ—JýÌ[€ÒÎÞ…I™Ú0HqQUéTÝ@¬õ=N•¹6Åà¨.²;sÕ:ú7ïˆqÅzˆbõYNTw…Š!¸tX9´BŒžQ´~D\¦àÓõÖe³…™ë‰†Ìq<òjë=5Ók[a®Tø"±Ô1E%¹2;"xLD(ÉiämõèI¢‡$c#ýaûÖ¯ó_Ú)‰!œ‹Ñòq-©æTõ#(^ñ÷L×¾pŒ1†ìuUiJ#np=³Ô;v8 Ãîb:ÔfèR·O[ŸÓ> ²À¾×XT‘ã=aÐGýøý ´&5¦u•˜2ÂܱÏ=ÔG®ÉX†N1äFu‹ %k?²ˆ´8Z±¯Aêá#JyŸS¿ *ómE\Ê) yg†Z3ï#Šðõ!®Msß, É­p>Î}Îì4Ë;‡Z:sªòòñšC°C Ê.ÌÇyÖº§È”Ö³…E'pAw3£•Û½kn÷zhû(Eͤ¶+5£ÓD@²ÉyKÔ&HÛ‡º>ÅÞ jœà£¼æ¦’îËL‹Œ ;-î^9Mˆˆ]¢\(¾b—ØÖë…*uæ—Øî–øë°s_h2áyu#~`pÐ9 ÀdwÛ¼Ä_`¬z P8¹Ÿ¢¡Qš#Ÿ6óJvwˆÌ3êFnG•#9’Bt„ë® óp«cûŒÈ¶!`;X£h:sHœ·jVtfïRèÚ&CXO Õ¿CSG17óDÍX‰íPu~ <ÊgLñ&püHˆ†´öè€è±@B QP,é›kBcúç R ‹{=®£EþSUz[¨[¬÷¹©(ïµÆÅ+äþC æíH…›²-ÔóQV¨{#V5:ùæ<} $ï36¾÷\KÍý›ˆòp[›|žÊgÔýeÌç½×!ˆ×S©éc;øâäËßtYÛäÞ´ªËÖzkŽñl«ÏtôøåhV%"ybÎyÀ´ò¬G#^Œég OG+,_AW +Í 5Û/¯5yÕ<:1Úse#%Ÿd(ˆ¨È£jKxlÛ¯öW òÜUø‘NEJ¬)rJgX¿jr#Â1îœA‘ží¹§ÑÄ=5ãþ.w×SMÜÉEú5¶w+´¸-N}FÆI9·W/ùtçŽ9Ö>#r¥Àejx$„ßÎsl>"ýUu~[RdÅRJÙ T”tåÝÏó…õûÈ_” Öˆjz>¬(*œDÁø«ý}î°ÆLj»Í¾tL%J>´ç Ù‘]ÊøŒÀ²cÊ‚Wiý¥£_E]\¬ó ÊJU¨£ËDÀ2ÍT«¤iuyŠŒ)ór~m‘dþ1× +Qnʪ|q–j䊇BŠuGPÃèô‘ÊçB›Òò{T)qeè¨O¨yÅlïŠí¨Ì’¢® +(5Û–g|‘ÞŽ¹- F, æ£&ºU±®©„Ì{±e´ÓšL%REÀF-1@0ö-¥Ô%ÿÛ®ðU¤H*å¯*®œ‹ÈÃ<HPÑÙײ† ·œN1‰!õÚ·ì¿Øj½Vl:6áæ“· +ø!N—¶õµ¦B Y€‚\a"5\•}—{Ô03@•}Ýð[÷ºõ®…=õñ€š‚å`kÃÐ{BøC=Ùˆ\Jä +px Ghû8”ó>Yð©È®ÝiÕ+ãWZ†s@d•_DXZèÍZ´XÚ•­ó\á‚´¹…ŒOÀ€Ï]‘èœ"TÖœúSÀ½+S†ÖÔêã:ño"ïþœ8?9øÿ,ÿ_y²ËûñK¢½ÿð7ëÎño~ü·ÿúÏ_¬üø·ÿþüŸ?þâßýåùë?ýéoÿñï÷—ÿëwÿé¯ÿîï÷óÿáÿüîÿãwÿáoþîOÿñÿø¿ÿ!ýþÿßßþÃßþõŸþöo~·.ñ«/w¿¾Á‡\ÀæÿCaÃüu8§ÿ=Ð×+¿núY‘¡úüÓÕ'Aë=ïL”«×ÿi ‡¿†Œd’Æ_Ú׿©B•=²2ý*Ð_ߊ@úWíA@ÍIuAÛRÁ‚©4Àea­Îê^ÿ-J)¶µ+°¯Ai`øõ +o‚rxÀhFŒ©2•ÂÞ#¶µT8LU[óÃ{Oç ÷;» 2]SÎÀa]†Öȹû  ¿Ô_1€‡íŸ¨¸‡‹ ¢"FôÛˆ•ˆE•¶Lü279+ç û²Î-™=˜{Ÿ?¯üI‘íªÈZÔ1u[ºí q:ÃÜ¿võjîê@j"z÷T_rÑ# Ô ŸÓP@»Jų®ØŸ³#’¤ëªøúÝŸXÌjÀÿ(Ž®¬0Ú±‚-`^»o§­¯.T!Ès50ZËa%a±Pi=Õq^õ »Þ3DAŽ`âJ´–*Ò +[p¸*ª­.?õîè7‚&¤GŠ¬Áň^SãÆ8{é–󎳅„WàäÎ}‡Uxì½Q(…UC¢*èù6¡û<:Öˆ øñ{æÀÐÓÕ™†›AoÀŒbúE•L¼)âZºQªIšÓ°†ã 4œ†‡p4íÜßÀ.º”wàú{ò5…Å&š¡€Qƒ HÑÎSìÔ8+h_?(À>¼”ÔS‘4=ÛówÎZ,ïl¥U|}c.ZþCLc(-Pßr¯¾‰À“G÷j«EcŸÿ#ª§–N»q;•gðºÑN"¿ÁZ× é!ASÏÐHòý€`c 4þãSw†õ«g–YŒ…ȵPç¥ÚXî˜ +ð-²uÚùø·zâ8•ëÑ=íÛ·9§5´¸ž¶Sæ?~…ð—ïg”ÂœœŠÜÉñºRÃ[‰ê÷Øzb›;Ñik@"J@«Wnf|ÜÓJ¬"Ù®þ©ìË ‘t¨êôH½éF`ÁÍb‰Ï8,Ó„N¢!Åøü.b¸Oc¯ýêóHÞ£È$O A”­ÞÔé);ˆ•t³LW¡%6y†`u“áIyy ð:sÄ‚Bƒ Þû÷³˜É¯)nP +gŽÝA›¦ R˜Ìý=â»AüEͬDV‰Uð3rzðzbV Åœß1ƒòFwIPÑÑžˆ®DãϾ$-~æzÉ q^ô¡î°uÖˆFgü‡žkž»ØÍü“·!œRÑâÆÀ¬ì åªi*ðŽÂîg}½Ã­Ž(Å¡W¦*ûÕ=¶$TWÆ(»Õ&{÷㢇Vê2µx$‚”]âØ5Œ€óMDÄîíX³"Ö¤xí+Q‹! +…ô’å v&Ë ²†ö/ïÚ­‡Ë:2cUˆC /Ê@x¶ùá$Zâ_¸Êy~wJéÁ»®C-t—! Õ’¯¨Ý“tå0åÑ‚ô÷;1 +Ø #½NÒžq…Ñ϶ ¿< äÒ¤NjH mþv +öäâQhîà˜ «Š;Ôìša`3V-®‚ fSoVXö3´/óÔZgÞ¹`r/«‚ÎZôðÂmºÁÖs*ªd4}Sÿñ:½HÀœ‹°&¶†w‰‡ þ£gæͶçÍ#çGx¨=Œ}¥; kon Os‹EãmÏãC¾}©D€" ¢]aj@ü¿mè%`öSÄ âí^èÝ8j løZÂZËgÊH DSxÚIqôr5¢°O +![Æïv9§óŒ¶ó lü½×[uêê@áŽ/¯Ôt()ª3çËΪ꺪¶®@ë½]ïΈIÛØJž·žæ.Y’ÝÄ ­÷ÿl¡¸z®‰¾™N5Ú¬¸Ur7vÛUÓ×é1™%¨™8Qý"ǯxØV"ýÕú8‚Æ%‚AG„ ÎEÝŠò=6Þ½CÚd3ñ +jü%ùK@ëÇõ«™;tVäîý¶#©XÝ/ño¿#?ïwJnKS­LÜQ£S•!ºQØQxË@8G$ëèHÓ;öãiõë4H^±åóèö–‹¿›ò Úr;©á ‚«|ºs߈Ro…4wξ=íçö´ocGj¯ˆK:ÆøéÎ××V’ó_»ÊüZ´¯dg¡VÙ©#Ó30“ÿÎH­¨@Ü=â’à¢ÌAYé÷ˆlPJ€ŠíT.ð›óÀ'ú^~_³<¤n0Íj3Jlx¼Üš“ŸÌ#$5IÖOZ¼¢4¥f@ƒ¨“6 DVœ¶‡öIE8¬¡Zx§mH±Û¿³Ðñ÷±PL%öl)‰2Õ:åÃúñì_Ö¡c$€6*>h-nØYr\+ + ­ÁG»"Ø)Cµ +é]Q§­gÚ NhaP;¬•¾ˆ”¿ÍîníxÝHMÑ5}` ô¸£žÌõÁ@ZÐØ +½z5?G,~½VÚ´4”Æîç3çW»-PŽö•>¢êÈw•äOM³°‘;Õµ™c¢…ä‹SdSW¾*#¹€ò‘ßCVVèŠÔÛçyp¯g²¥ÙäiJ’®uÎŽÈÜG@·çQ|?µÔ’«RÇ ÚcUÙiØÂÑfÖf†gšµžÕ99§KQÂ8¨àMõäìØ{¼å,ZðÕ­¨®E‡îWáîu…[ÆÞO#d¬=W±PØ¡xÛá«Û ñTòËóÀ?¹±s/C• "î+Wj²‹ÖcIE1š''–»;“juLàçJ²ð¦²iJüS.ž°¢ÏD‡»,v`hùM„üuÜ`=9ÛgÔecðÅs?ؤú¡ƒZöfÙí;𦫖Cô‘\?À6âGDf­Vpþ9îoÎsÊ}ÜZ *¥sEv¿ÃßØêäõD `“¾‰‚¤G× +2ÖR`N+éV‚PsaüŒä7”p¼ÈÀYQÞç—©Ðú.tÃ4Ë0Xƒ–qΈi ›*qål;t?¹¶}¦Ïf$-õBÀºQõ@†-A-O'¯#j4â£Ru2âIs2­8$Î)¯ßq|†]\tÐ8ýÖd¼Ò!ðJ@oµC§¾ìö–BÀCá©ìnŽìnxóbƒ“’vJ¡ ®;ª)=`sUzj¯½‡ÀÐwúºU…(X^G Ð¨As|ÜO„]E:fof¸_UøÉzsûëôçù¸½3¾å®^ƒj4@Œ“ k„ÞñÏ7p«Ã)è,}èf¦Ú5&jî<ÊÖ€îz=ºÉRê»Ò䤴w¢Øbq9ú9zv"PžA +C‰î @ä±R²Cwx#Y¨ ý …ŠËz‚ö±v¡  ¦«]Ö™Ik¤Ë¯•ù¸ô@=TÛÀl¾77›ßº"Ú‹+sl§Sˆy#_\£‚пֳ^L­UïD¸nôÝáÊÜÙ•q•djVÀ4'~\]‰ÊùÝY#FZ5>”zæÜXFÑOEQ®¯F¨l 'ºsHT¡‰(eùÒu•Rõh±q§Z‰û?áê¶ÉÙ"[൷0Ÿ œ›ìƒ|gO¦VRƒd8u/P5%j±üÈì±6­ÚyŒ´Œ'µR$LÛÃ)£=µ’(¢ÐÑ5*ýÄ¡¿ý4°Žn«ºÖ¶-ÆK–]Àœ­Ø¡R´x=¾.³íi¥ ñ +à +â'ܘ‘fŹ>Ú<ùrÎbÑÑc¾ÆTt ×ŸyŸÌè©løæ’šQÄÝŽôðôìí¿‰ +]çëIüú +O8$´ ©oºxCƒ_´L@ý1ùÒ †…d‰û€}*àø-ÂëôK‚áM:Zûæ<`ôQ|/ÀLf œ‚Û)Ã;…Œ +È­í;NÑým/ZI݈“h÷{nå„F9¤F¢*Ī ž4Âñá´‚ó‡ùes{ +m[Þ#Ò[e§_¨ÕDáóEé.[1¶çôz¬…zj‡¤ØEkŸÏh;­¦Ps9Ë7çq®€¥w¢¡Æ‚g—c-9§4à ä­Ï–÷à‡Wc‹§Å,$”QK…KÌÚ6Ì/Dð~ÛK@œ¼Q!fS¾ìZ²àTñmkîƒÏÂDJús!»ŸEë¬+QrH +ÕÃó öÀ†wîDD“ñ<$„”­iÙ?)‰S‹¦S®Täpà­·òžÅ=1ÅCJãÐë/àr8±nT(·Ì=Ø4–æm×pê- iC¿?>Üõ‘Ð~ó,IÐc½Þ‰ŸŸG XAi]ÏŒóô†;´$h!ŒyéÊÔœTd`âÁ¯D-fýˆ•À^õýxîÕSFs©­ãü«sÜéÞó|Óîâ±FzéßFÌô;ÙÈ뙼G¡´+ÔrÐûÌ·½-ž­¤OìRßÕ›¦zÕ•Šbõ† KR?#žW3åAÔ‚ëøæ<Œpä¼ ™Û¨7ßü™}U•ý…ÆSç|:¶Õ +éÅò]ÛÃoÈÊYÄ ô/W + øÌë)s½Eä–]ÑRÂDâýyí¨§1Öw:6v:ÆOÕÙüÜêLùxR‹*IéÞò©9t ÓaüàÕÌCƒO}dŒFUûÙ7è2–}Ž´ki9ܯ+I´µòH +™9Kãë8®¢,7ô6Ž[ì +q”‡Fí %«AûþN`Žä#ltÀã™Æ3Y›uØõñC7\œÆoZ5Ťelh·ëz­Ë®`çgD²"û”t†Òà7çÁ@3©g ó…Ò@U˜ÜÝ\ü=â*Q#Ðb­µW +ûÕ#;90Ú>“ +SZ¡¶åNjÄùŽ_æœ_2ý­×öÇÌy¿7YÛ‰eh¢„âW=.Ï3â-säƒE`È~§¨v“¥ZÛë¼.|Šò#RVVÞ*”º|š×× O¿ÞLihèµ…?GÞ:[VÄKõüÜj€^aW:»…©ë”M-wB·Ms±”È Ou/Þ#€ Q˜X+Xí\é3ªÄËràÉpfFo€$”dù…†d£c³•"Ž Ï×°=î;t‚ñ)[WìÖN:0_Ëíô@IŸŽ€’QlƒV +M©‚Ÿ +ëšÌPû1“<MuQ_=¬_%e•ñ؉ª°“2sͺ鳂ÿm$‰/¹R„ÛRî[ÀïŸfš 6.£sšµK¶5m‘ƒÂ×QÓñbÓ¢&ÓGÄwkâG"*l{è+”¹m¤¨æ¯¥½ÝCâ­;íEãCäÁ†6ÈX<Âm#ÝFïÝ1Ÿf¯D‘PÕn w¬WÄT: òRPéÒÞ(„Ún¨© b3e_Ë)÷lÇê.Ñœrüííª^ÌPŠ"}bEö}îײ„Ô2w3§µ¯cì9Èøáí\n;ëÌv(뤧pndƒ^Öã*õªq0šœ‚œöÁS“Ýù¸E2ÁweGĆæní‰è;âÞnÀçˆ`Ãù\I­”û˜Õ4÷Ú?µ;çñI®ˆÚ·qÎ&ë¶iyˆ"£gm`F"4jì[–2WâòÌ=þû4*Üá²&ýF‹Z§DÜ-ÚüXÈð÷Æ$V®X†Û&ûÌ;ûL+þ +†ËQ(JZwʧShH†â…s l°u¾UÁs]ðè¿(݇îu¥Ì·)^Ý,Júkjê 8õ+ӌЧ È9WÐk“2Ⱥ¿×£Ä* Ž +4¸s$J9Õunš6ÇçüoîSñ{/ׄõÒ! *fÑî$îéu Q´-ò0³ì q4,158ǯ!qUïàŒ¸ûš[AHnÅ xœÜcVņrmÙO¦EzpÍJ|uÕb¦,Š’-Mø«(Æ Ïk9 ƒÒôŸWX  îÍïüŒÂ¼šÊš¨À©0‚èdLªJu¦Ë™ýºY’®ÂÁpÿ…X9Dl#ŒºÇðLßæ¢U Ü“¹½­|Öb2 †·5YPãÌÈ‚@çV)~`­i¦¿Yåä'aØ-”Å c"Ƕv•t18®(G+TEÏDŒt]‡” uSã‹ÇT®# +uýŒºòD¡NAT)ý6Û`67¤´ví–,ÍU>Á8äTÖߣͲΠ¹6§=6_HèJd@:jWÝG„T –Û×PØTdÁ_•« B{PÍÞâú ?ƒˆñPC‰²`G]ÊGŽD“Ux/w,>ÀèÒìs§O“¢ï&ÔÚ‚ºÞ¾GäѼ§mç™#áxøUh¥­¯‚˜Ìü pô òz}ß‚Øš°®MÃûÛÎýmùÃq쎡‚ øpo©ÁÞí3â÷/a¨Ì߉† Õ J⌰DPNÇ\Z!0Ì·tŇÙ)p%Ëóc³oAMQ^4 뮂+Bw÷»årÛ–ËÝŽGŸQ8þŒ™B‘«ÄvP”×@¿{™Ê–ùr>™ 1 „ýyÈ#ä£t#Âk”*û~ÑCÛ^@«®“5P늆–#Ðô¡ŠÑŠmj÷¾®ä:‰›¤òlбH"«óëíyP b؃“ü.ù!;'¯t«s†7\Äbi|qÌë7…T{£}qEIÌdžµ»™#5ü•™!íFjûb£NyQ¯2˺7uÿ¢Ï(L˜t:¹œluf¡$¨Iòʬ•Ï(Ö*Ǧ Ù{Äïw²¤ysR¿9ÏÚC÷I Ù‚ªEìúxáÞŽ•o‘}Œ‡hWXÇ ÈÆøæEö±¿ÜÀø1˜ß^3 |ðv|í³ +#K‡™oDçœ}Ö u¯W—!ˆÂWÛ÷ºç^?þáDAV&JÖ>Qhe)"(Î!§S)FtÇ;W2§¹!è]F˜²ê·ú<W»¹W;ÞŠ¾Ë…¥©[èyü)w+Å)?ôy³žjÄt\RéœAš´n9GßèhîM®¤ðq)iŠÌ,™,Ð9S[\܃¦—ß´ñb‘ò= ^û®@ÍÙ’UAJñ<¼äÆœ{ pX8Ý’ÐW$¡Åvœ°çÕkûŒÈ^cK~29\*ý½Ÿg„aŒð“î1©dE’|ƒZG¹¥“ üRŸ}2†R`òÖæúF[æ& Üwàˆ}£¼Éúú¹ÝÒO]R¦ä%,Ãõœ“štÊ[™»ŧk(1>X dt̤#F ¶¾…ªÝ‚€ÑL¯Ew¿c‹~ßÁ;Í'ã¥?®Ï³hn]bn]5“)¹.“©€3Xﵧç|pUïQ5Ή÷®´ ´ÎÒe,yOd—ùš¾¼HÏ–+ìáQ^Hº¦L¿ö"›÷ ½@´¹kî‡ÅµNǺ”-V.µ‹I)) €þ{(Ì.6K×ë:z4üš=5¥¥Z¥2,,6Ä’‰ˆ¿È¼%ƒùdZódî–sX¢(Hîïú©õ|pû&Ѫ +BvF{Ê|À"Ïx”Ä+cmÏ0.ü&beOb÷qñ5c~,,‚t]Û \MØ™àj¢G.6F$çý3"˜É Û›÷ÍyÖk.xíñqÚóû ¡ÒÑâNYgó:§¹ØZÞ¤]ß©bÜŸQµ¿Oš‰+—tú{¿ëqT¦­[Í錃ƪÝ'Kûý]D ð«§^ð{Ô8"äˆöèJôe%w $5®OáZlMPº/ä ¾•¶%¥)Ô}0"ÑKçC·lÞhµ>m¡§ Úk½x囈÷E9…ê÷(´$å2PÊͤÐÍþlœá¸ƒƒj|§j’}‚æ%‚ø[U>]˳ Þ{.`häPÔ{@(4„¹Q¢"“¹ +WÎáŒ÷ÇšN¥Ãqct×1y,l*Øe äÝdí!°ñB÷Pä6üT~K%•µdÉAýﻇ’ä#‹žÀ(Z·˜Ó¢P Ö…ê†C&€ E3˜O|»>ÿž.ÍLã`÷ø]ò6ko¼ßZÀbÀŽ´ † [h„ƒPx >‚z$æT×R’îr¶¨òÍä x‰½Þz…½[¯Ë}FÛG+™µç)c›õ½`>õa{Ž/H¯-¢)Ñœ8¯Þ?~å÷àeøÿ‚î.ýü—<ó¶};È!Ñè<Äö—¹1Þ?ì(Á?DÅîìq8ä5îÉÜ šW*N?C.¨oÓ£h&#žPsëŸÔôžn óŠù(Mä/³ ò¿Ð_%çgMó¨OúÑÐqk,š¯CߤrJñ¨›+ ¶±‡cƒ%Úú´Nd+:èíësöŸCºHîN¹ûìN,_l{às¨ç¿­IÝ¿_@1“¨.Ò™¨pgÑ’¨}a‰P|‡¯rÆiqÐ^G‰&?@Ñÿ³{wEÉ}ØZ`Ï0¤°ó4Y'ibëí74Õ,”LRò)ªžÜþ({m. +}4ƒxÁÄ‹»Q)]¥Š5=Çï±jD‡›¢xcºS÷­3îŠÇ>hkäëo'gew'xú‰ë§Ý%ÆL€@ÖÜ,cSõÉ¿‹SávŠ¦ €mµ¼ý˜«5ëWOW:ÄÁlTs×Á\ùqeç«Ú^ ßÅ)ŽGUÀvù ÂùÕ+ö4ýñqž”¯eyi=Áµ_ë;b*3k´„âŸ2r<Ô¶#”ÿ]x’)ñµd\ôr@Û³'ѹ9‚ì¦\Él%kè€Ô¨GÓrÃIˆë˜¡Û®7i>›szⲤW~Q²ŽìÈŸŽlV­/yˆý@&‘Í÷¯DïPÓ>C¬Â‰3×—Ã’ŠÿLæ4ꎉÙNYÌâÁªh}O“Z®äêêìÅϧM~ÁÈ|N»‰Oë¡Å§fLʹׯ›Ü7Xc²ªáªØÅëdà­ÜB[ ºÖþv˜¢i„íþf¸üp§®ÄLzí%=µJðƒ¨‹!ËebúMb.Ô*Á‡~–\¥=û>Ï« D@nƒŠ[Œ’)h>ßr%öKDôÛ®€ãi=;‚­2çÌw…Ó¯ëu¡;†‹!oL¦ñ£Ôa¶4tPZgà´ü „:üñÀM“‹à甤áö€±‘ç¿n7pA¡…–Šýõ¤iJ‚Dcœò2¯”ÀOì $‚™œ474º®ý¨†®kAÙKtÛ>=kæ:Õh$Ê®´õ„eÇ•xh\‰D%7j¸º`Uu' [ÔÓGPÖ#@YÏþ….·{<¦ë1}…§+ôº„:M%¹rpÌæç Ù©]EÞAIJ›=°Ú-{º=OíÀͦzÃË­ñ V\, ;ø!-*2òfòöÿ“§i¡="ÞG€5ÚÞõs¨H€”ª¯ DnΈѿ¸Î¹ü´‹®Ü™+ù[×»?úÞ¸¦ k•¤ì÷aÇïåÌy(¹4º7Âzýªwž-ô}ºyëºû=]LB/n9|B_º†¼8?³ŽRÆÕ¯=ñ¦Vð¡®ùZ÷3ý%c¼)\o(\,wø[ÛŠ–ë!Î×Ê@X×ꪡÁ#†ÿ´zÍb5¦PCç\Ô¶ùȼúdEååqcÞÓ“÷è²Q¯]~¢‹º3#—Rs'÷¼W|¯ +»û+[‘9K{¾o•œê\ëîÿ­’Ó¦³‹Eûô57cžú' Œkg†£èé h¸õ]Z‘Ðn6Ñz*kª¸ÃÐBȆikÜD50æÁûEëÔIýŸÊ>)~¡äa~6©<0uå;aŠÊ¤øüÄõø؈iÈAÍz®”å˜ãÝf´Ž<÷•‚t\;e¾ 8x¯]Òh®t=ü9Ë'x.îÆàƒl³Ÿ+`êp€ÌõÔ B¨Ö¨sjÿ‹Â&]ãPøoYñ륊ö®Ü•¾’ñãÜSGoiŽ¯{k;Ýá… ?Ü|Ž@.K¤éü*ªãQ݈ÏøÜŠkQ¼Ø²½“­’›ÏMü-¨Þþ™…—Ÿ¤ì†þS¤€ŠòoÖ¬òxC2)·Ž–LWD¸05æA¹\˜| mtÙ¥Š®S÷òö‹êkHDp"+Ë–úÞv¿”¯j^O•1W–=œ\G„Aq![N’%rÊMÉ‚–JEáaôÐ{êf§íÇo¿?ÿßID·c˜zqï qAékjFØQ§Rpê²þÖuôÝUˆ=H¥hóázâ™ó^SÉK°Lâß +ÉÍî\7=ÇÂ-üý¾RjF…GŒ\¢È rv ÝP@¢J«ºíÁVÙ¨îÖµ/#dCÁ{0ÖÂDÄÀe,1jóT'ÜmÓBfæÕœp‚·Žp¨Pž +´/dýs“õ ¯±‘ZÃâ§û˜¨E ¤SM¶ ±í5 ¯dEwÞ´=þ±(æ«GòÕëxŒ|‘ÃZQ§ß‚(jy¿E ºl íû #x9P+²Â°— "Ì!Ê6ÄŇã®_bËÖlæë(æ‰5L¹P=©©ZòKL&:J‡l³EQÊXOâ*_Ôiè¥0ÜC}97úlícõaµ¾¥ZO + Ž–%Z¶q Ø–¬ÔýâÕwdp)iö5Ý _AËíkb;Πγ¾¬1gÏËζd†÷½Ñãö窧½E-·g¥¶+5V(·]ñdη¯%¼ÀAΣJFYO™L‚â#BIÀëÜöYeÆ>kZu1+3¿÷¾•y 9ºñ\èÐ \‚/†e“„ZX‰°½uЦøŽ°÷Ž!mI~Rê’¹‘©«÷õÕ÷¼o5ÛYä¥Hå¥i~\X¥Æ¾w×Ö4VfÇãxMQLžÖäp²ð«Êj_²fNc#,„Ë  ÓþúÂ$æÆArÕ¾I%ÎöÌQëÇ«Ê4@õtMÔ…ôÿ°Ï…ÒÚØì1z4ùF4OVÆ~ü…pØx2CÑlzßxÕ{qd ·ÕyìN›cݤ:¨÷†QQפ·ÃCqE|Ò7Î+ÛoýÞÔÑ[Yðg  +½à£Li•ÎD\oL—‚É1}]ÉêðÆ`EzÙßscÚC:ñè‘Áæ¢hš äªSdG,i>šD̬ØÚŽC¢¦ËÜSÀB6\òR[ý–mÙ#`¶5Ž˜eo›áÑuìåk’æÙ hÑZrý]_sÈA´@=ô_üzL¬O­zc- ƚ䇽¥nöÁbj#0G‹´Õê름 š&ªÀvÄXSyöƒV#ؘ-1Æ£”„.TÞ¹÷‘ëm?%$R"ªbò¤(T¥Æ9’cJ¸ Ÿ@c~øx=›Ì¨Y13(¸ùÈ\‰u]Ûß:ã%•ržr ²g<¦žV–°ÿckZ•ÈwÖ-*`¹–ÀöÒ0…AÁÇ”fë¿R¬¥Öãyñ;¨.Æ\õ$“#· šÔÙ_yÅZ>‡j#VÏP~=/¸$ùÁ¿œÑü9÷4¨˜âvY5%JA—<ð¹hò'ÄR@¦”>î‡|tݨ҈Lˆ©òèG÷ãvë2‘ Ÿ›\¯*¢;ÂTÎ’¢Ô&q –÷\(ßFmQ³,?úLÎ}¡|š9ÎJPþ>÷‹Œ%N™Ûæ —Ä ^„™+ýˆF™yW”áü9còuÉm;6ÏL3s ªaLl3Ì%7N$tðtóHÁ£õ\Î+È‘h¸ÓÉhk*ŠÏêDvÅü¤Ý3ƒ +:´EÂ*̹tÔîvû–¬¢±ª­Å¼1'Ê5˶ÊÌØ7¡!Ëq¥À+žÔضeÚÚÝ‘%`ºüBªÌ-|Rm¤<¸Æ±S̸77/£ù¸ ö²~¸’fl.¹cê{´3KäÏlÃñ·ÞžA|eK¹^ûý®{ë;`Õ9·p'Ó®CĈZz¶¾° áÕòë[»`Í£~í{>Ö-³¨Ü®S4Î6\¥FŸ?sù¦Ôån=D‹5—âlí—5™ëÁ&>ûb-"ÇJZiy×pæŒ2‡4ž±ëü\µ”HYEJ;|_MIç#ã]¶T˜Io÷厕$ ê§Õ÷ ú) +^“ u&Ö˜Q÷}A2Kí\ríâA6µ]Ì\¢ú Š;`xlåûÚ®¯$@Ä;CÑìuý±F í~ÉÏŠCûÞXx'Žr…-Ñö9aìž +3»gõhâ®3”%‹=Ø¡è%°öÓŒ1>RkÍn…•N”-1;Aÿkĵ—ˆä±;ªêF3[ ¢Š3áz›$täÈšÊóˆÿÀÔÀOB®L×$ëLrij=ðÉynÕØ‹)Ëú–ÔØ c±´ûÜá«DsnEÄ% ñY7Çι.˜ÂQKž2Ï|]Èf4™l1±»ˆtx•®;îOw®Y§¡ öŽ /]û™yZå(à‡\ÿº]צ)Ã[†tiW÷ûxú ¤3°¡qzbj¸%Rê€vRg“M +“2ïµæ佊HžWï[_f0 +ýl¹P Ja³‰‰Êšq§|ùXóP.aFA ˜ô®̾ÀšfDc£€Õ¶Ddo”_‚çà-nÿJÆœT«ŒãÊ6M(å5i¤ +›]¾‰ 2ÿLì7]ìZœN}[²m¼7•PM¼uÒ&LJ]wJÝ®M(½B(m­s}ŸÌ-ÔØàÓó„› ;ïÜÏ Õ¤5p[ µ!¡ã|fヾ¹å ëëË­}±-çÎ5]¯ š¯.S%´-N¼øî¨dÈ«šõB˜Ñ³ŽÍþ±m¤$)·wÕuÎ+µ9…wðK>SDÉ»=‘/F…pÁ‹ÈÖîÞ„6–Ήٮcz¬›DìçuÆìBшµÓ«%Ÿ +„Ke“lQEp¯-êŒq  a’âη&mÒø¹€=€î2óÚ 7)k½z@Èj{#2zË#à;?1Þì±/wá$AÇü°¥ð‘°@ƒ3Fo¤>%¬ØLM5Oþ.ÚÁSÄ^Ùâéº1u²¿šr1ü¼vŠ¢ÆÔ'k»1¢7.Y®àz (ø(A1W¼\Â4_HŒEÊû3ŽwÐú»7ÅÄÊz±)]‡ kE¥Rs~¥ÞËØ;Oo{§Ëxé66]&:ݾãQµÂzXø' ‡¯Ç4_¶!}³ô,ê}ÎÏ>æÚ †{ÛÛƒ.h,ª³^SÎÔ+£k±[b„¦)ÉŸ×Y×qÚ4 ù`Oiìj]Á2»fÀÉ·´M®4ÖVÓebã;× ìGúÄb,Ôw ­X%ä5½¿2N¢Ô•¡¬¦jåÜ“ë·¯‡wzžˆì uìâÉf£øRm¨¾è*$€²!/Š½Çèë77/«Bhh8\Dh‚™µ®Á+˜$¡Š68¹Ð³@Û „ֆ혡;«öXãûÍúß(É𢤉ܧ^ág9Õ:ŽèÎÞ=œ–)Ó¼#¾#ˆÇ"ñr¨P¸åÅŠ}·xã;˜¼ÊFš îb¦¨©+êWTp©_¡&êÓõL·ÔÝu<}M%½RxŸCWC—ã†šÕ +:…¤hŒ¤)ÊZµ6Åû´.³Þ¹ã¹UHbÓša¾FènbDH°G/™:l†TOO@Vî#. C‘ ~Ûwš³ ¯hÎÎGˆ“]‘CðÒQê‡/&œ;í¶ +°(6~«ýY˜ÁÝ›iK4t'öù“êLy WnÒì8Æ»äAµ“„éÆh^•*}ž]§ˆ«ŸU‚ƒQF;úž>Ù¦´Y”….¡a4µf˜ù²ßöïÏ.b‚çs•ûu™,$lW®¶ÉÝç&w·`ä~†:Z·@ò¯( $å{_íU"Ø~ï õì`ˆ'hžÑÙ¢ùo;óØ{êªÏäí Lj%ÃËÅ$ß„ÊêÊr ©Ù©q_hKéÖ]‘`G +°óØwf(4BÖÎ~Î3\,â^)sW9&`3\=4õÚ#ì 2°€ë"3eçÁ;­,Ë +é‡çæŠ`7ªF‡$ì .UCógý3éÁ^-–œ6.û]|'«¶°Ì&˲ե‡ØÍN¸9E•Œ U2&H¯'“ŠÃ²ãGŠúÏ¡>+ö¼ì¹,‰;\¿­{á•Ähž)¶ª]Óu +¿Ötæ liÆ&ÑE¿\Ÿ¯5^RÃÊ.$Qwâ u|z¸èÙò +nx”Èf‹ý7Ëü×R[b)8ÛÞ†˜†ÕmÍpnT2*TGËy®8žOÖúçt¡Y¬Ÿ猖4½]=6ƒYYS„¸rá©°ÄÚIIÀþFæëÚ„ûü]Ÿ^¶‡Yÿ¹›Y¢78ÿ84pEô”¾vrîªÛàÍn!SðÞاa"Ï6s¥OÇ/À‘Î~’Mø½Ôd)Q“ežU¶Ïу^ŸcfÄd£¡ÅFÀ½I´¸îÝ·µùiû­ w)ƒÞ6]PùÑlõ׎|ƒ¨) Îâ¡Ñ%êÈ"Œ6s±—~yôn¾„ÇƦ?úd¶‰®´‰ðœwêà(†3çq+I·Ô2 jppÛ\l~~ÙŒm sˆª>)6 jÎ Aù ‚SÈš*Nm+YîeúìÄüI lqÉîæRˆQhh_`oô[C„fÓÓ~ú‘ºw#.ÑKÜ$¯TZ|COÅä¹u@.Ž¦Ko´ )¸\ÿ‚* ê1µU¶‰ ;56^cXÃÁ·_b–Ž(˜µ²)Ö\éÝ`H¦‹w(Рý¡‡\Á¬9m*OÃÍ„FÕ¤­ŒHy2…­ÕŒºª3`^R4ؽh +ÕD· WO–ÛÀÖÏGÇusʱ·dç–?m d¾\[.XcÀˇ-„°TSÅwó¡e9o"/¯+‰ú\WŠçêâÚÊMŠ‰mlëšÄRÕ8¾A䱩ÎN£ÌúÏ{¼…¦±Râ±…û‡Ù?åÜØtqä[I ±ðÍ©L*>%>¨øŒç‡% oó@xP…«ù0GMSÂta«W˜áKxù[QãŽ5 ”.s„Y:ö•w[ãûx +:Âû×$pÖ@Ûü’µpûl]Éœµz¼·bLyZR°Ÿ·ßqðÌ ¬Å€x%í+¹°‚2FqK‚ÿµj“Ml˜ѽ7g ?ž7¸ž” ¾ÜmŠ×./wÄŸó[SÿŒpÚiR±°*AÁ,ƒöÃú£:¿…þ!/ß1…Fð{²")Á7[*PÑò€òóó^µË% +1;£ÌD­§÷WÿO¸mähÉ]:pKcê‹Â+ºá2ø·­îŠ`û‡pÞ + +¾@º ¬¿÷Ú¼£Ã;ҠàÛA[cd·Þ d +¤ej…ˆ–»ôcãØ+51¬+çª8E‘¢gHÕJö5öÐÇØñ‡r…³=oê-QºDÑûÈ‹ÉÈ¢`ã…&ÝžøÊ6\ÿQê Öª(äš´[ПÙå’%r%€œÔ!ÕÏß²*Ù²­X$Q¾×\‚‹ž*ý° ™9CX o–ÉŒJS, Z`–{­€}/ j€mÒìÝ÷\œw¤‚ô|z]ÇõŒ¶Uÿ´)ª˜_ç†EÐ êû¿ï=¬TÊֽ¯{s"ôË1€5颀L ÿ>Bfë‘ŸÚúž’Œ›·w03¶##èÒ´(Uõt “ê-=J{6µ‹¬0’V8Œü®5P\ô˜!NUŸ[Õc}¿ã‘ö`á»4ãÆbhÄ +•*ÛmUÉú^í-Ò‰—hÃ26T²Á»aÛ¯Ùþx4Ø·ˆëš“-­kœ‡.Ÿk"1ªÐAÝP¬ÿÈßrž’^¬@½¾~÷çÄûý¿ÿ¬­ý¿òD~ñùoñ¿ß9·KÔ[CÞ9HÑHfÁX¬é}mV·q!L=g"ª„Bߢäá9 _1&#u˜›9=Ã0ÔÙõ€ }vk:[•îñ ‹ž@R%•{ +´óž¹À$H1¯s°-ˆBŠìÑÑ•–Ná¥{±¢š5äNOæ2ªFÅ\tzž&všöþý”rF×w¤ +ymÎqäi +æþº’-ìúÜ +ÂúIà÷z…/ò +<èò€Va¼Ìã!¶»UH"t´)ñ:ÍVnÛ1Äf2î¡Úª©MKðfn÷Ï[çía mºû^&~P6çõgßÙ%ŠÛð ôI8ÞŠ’RHTîí½»I!ûÐS‰r +f v³s%j½à"_%( ÍÓ…ªFyF(› ͯ ’NÒ;ÚÛÔ,ÎëŸ,ä]ºów–ƒ˜xé œ”‹n=Ìs@“™)€ÒU"Æ!Þ åv­R˜ÕÎ×ùÈ ÊýÀ1÷÷_£\’9ïÞiVÄ0Á;©ô|Q‘sKkLœŸÕ–fÌÒµ%-7ëdÖÙ)“&ÝÑVXÀuª¤Kí"ƒ‘ÒZ »X<-¡™^Ÿ¦ %Ù8÷`ÞOc…³Í¸*s$p 'Ÿ0«AŽà²º±JATóDñ¯Ô«#`ÔÅüÒÕ6ìißu…ËÙ –„þ:oæu’0æçSÜ#Cò9xápÊÖcBL’OÚºã–+”g×ö<†h èpÔ +ôv +d†GUóéAéûl–Úˆôjð§ìÆÌp:¸ð€4ðõý©áñš‹ÇÒ³· (¢¾~Çmkœö‰¨2’€ì#.ÚÚ9ë1É… 2p¡§G× do¤Äïs#ªÁœ”8Oꌸ7WóCîmëßà\Αԡ7Ú훨ºUß™ Ö¥ ‡y0 "GrÓÀ*á` Ü6µ“Ìœˆ¶#6·¥W=†>Žu˜VC×WPÝÀáØEC<ù3âÛ¡úUA¼êt³ö0ÿ¡ŒöªHäîàj…]§àKÑiÛü¼_=îA·Õ¤™;×ô^xÑš{<%€æVù·¤µ^ïëÜУ ýSÐ>ÆVµÚ¢'¥u#N^”¼Õ%?•ºç‰/Ö·Íñ¼IúÏ{VªUn§ÏâÖåÇé!±QsÃC™/Ö¦—‘-ˆ2:ÉyÛC}ÚçFCèÖà«õŒ?¤cúÊ0y™§®<,êZ‚¨ŒÑ”L.äLä½G¤ÒýÇA7€V¦¦Iž)y«ÆßãA¡Ž¶  ™h%.õVц]ö>Ö ác+ªÚk‡»2$V™8dNŠÇ#ÍþH%ü‹Ø¿˜inÞË¿Ž-/¢Á*;„+Û`«†èÕðK¨RÚ3ßQvÙ›œ÷¤L­_ûšR_òqYGK#ƒó5;Ê ˜¦$æ­y8ˆàU'fJ.1c²>@ï¨çÜCG«I\»3v$ •³ì[py­¾€¥‰Ðac@á}¬WœfEIÒ6êÈÇÕrB3m;& le ¦¥Ýƒ‹ࣩ3š Uì©@ÓÕ5™IŠ¾Ð@:9N$Œ—ÛQz¼~ãB¦Få5.àä&wŒVÑiÛ· ú¥§z¾voGã®ýæ.WtÇ5ßhƉ¼¶qb‡žÚð%ßJÕ»3Ö§Û ‰æÓ‹5áf›ìQÍ„ZÓýŒ7SÄ\/¡¹ë¸=‚8êË4_î°V"…°çéÊîЙ‡¦œ¶F®–M»¸Äpî—±ÕȬD¿î 퇬á[SÍQ_«Gìüx»µß¤dB­VWǃ.SÙhš @ª×šj©Õ~^ÙÕ8àXTãì>¡‘Ž”Í¡ü{»R¬ +ëÕ7Ç…Â[8Üò´r>¢6A2›à1LûTÁÔ”º Ž§(§˜Å;p*õ3â÷»¢ìÌš5»‚LŸçÙØËú•ïBº˜”6Ö³ã(³¾öö>¢ nÛ\¸|¯½õÚisê5ó4%R@xíÍ´Æ‚qÚiaÛø\hFÐ&üy'ê:›¨—ΪhDºhë©ó0t:U#ٷΈÜÕ |æŒj¢J{ÜÈöU¢‚Ö¤xÜÃú‰šÇ¾Ò8Œ/¢e*?üÜè7êè`áC/Jø£„¸ÇsÙ£õdw˜–…ŸpËÒÝÚ-Ãuëv•5"÷ù´þˆØTÓê:ôÌïQçþ4ê¹÷ø&gpáBËK»†b-ŸÏ@ÞæÖ…ûsÖo> ØÁÒ•pÙËN¡]¯m) Å}©û}F±§”IÛQh|”5=Srä1£æ*m§<Úõ‘ œçx9þÖÐÍ ´QÀ#Ïz+yƒ²`Š‹¡6÷Çh@x%53–Ÿ¨WK/7»ˆ}%¶gè6—ðTÕJ;h°¸¤[ëùæ?—ï¬Â5¤†¾ÓȸÞÊêÚ¥•R=+ÑžzÔ´±i^©ú)øy[²£ #\{›v‰au–Àï´Y–?ÎGP–ƒ%€V Öœf»4¾bífØ”Ôôõ)EuUà_(|åôòD7üâ9ý¸"Á^¸Õ´jï¸Ê {Hs7ÊÖ^ÒåÅÖÞ:ß:Pd¤úó­QkŒxX‡YC°Ék͇é tZ­fÞA†¿‘ ý9·Gìp”ïYÙHµ1ØŽ@ñÊiq`ï)é^ †eê6q$œ¯mUÛbUë,ˆôŠýfX03Þ¾*·öé(Eß(Ãèj;õÂø2e?]ÑæJ{»[•^gd`®¤Â\IüNÝN4D(¨óp kŽ¿¶ÇUGôõ “ +2î½ +ÀªÖ¾ÿz¿¦ëW ‘«=õBÍ: ¢zÎ[¾éLùç6’¸Òʪ=÷3É€‚e]\n{®­ÐÞ¥7µêO…ð4• Ë {¹!ÅÆ}1‡~·½¶miSß/î’Ôl¡¦ç抄ê–-¾Û0'ªógµ3çÆn=%ÔdŒRvž=áB¿íXzwjPÓ{îYdUüHw¯lÒó•î´ÝŠ¢µYûºtN¢êþ>™¶èqß›<%:¢h¥îÅK¼ÛÝ¥2¥‹ø{º÷+íÏùëOÁ›Ž¤†(@[_ÃV<. ¯™Î¦ë¯ìgâUIG€$=tT£æŽšuó‡¨.ò–áHQéìM%o`¶‘kö /\:ÿžîaß:8xj^_Ÿ‘ÁÉFãñ»ÛØmÖ­»vžñÐÒ §nƒœ{~Ù#êæEv{¦B#j6Þ ¡2Ùhi 4«çtɦ¸¤aà—ï~ýÁÓ`UIO¥Ó,fêÃ*Ö²È}áA¶#Îq?ŽgÏ\ñvPè³4jôWl 5‡Cl‘Ë£cíiÑATY\Ù©A»ç‰8vÄS®QîfhØ1ª`uФW¢„ƒÎãqÉ ‚_‡N°FVe¤ UÕϯÒï‚tü˜qü8÷ƒÐ9£Ékt5QÑ ‰3)}¦M€ÅvÏî#âYSÃœuûæwQt®eâ4DÍ÷Îìΰé"y?¦‹Xùv_p£ÖyüÑÆÞÉ𿳡Й¢›£Œ3ŽýÞá?W2ÿL=†6³Û‡êÿê¯Û¦m§{7£b΂À§C%,䱶+3lɈšHÇÕ±|­l½Rpç'b®_|MOÐÌŽ¦ö Õ%VŒ€N¨÷1ªNÅz<æ ïý«ˆ\`Ækl~šÃ5¯%ÿû£Ôð#¾({Frï!Æù3¢Š+:æÑõúŒÕ6C¢EEJ{²QZŒ4®¢¬¡¬SûX9™XI·û3 A’ë +Rêóô¶­ÉÓ®ÀyœæùÍ-þ×aîê7¹Ò[TµEÈOZo.cû¢«üfus—tçQ‰Ñ>Ò›†Ä…îooÏë6äÌîû›óÌ;ÒµU²NV EÊ€öï"ÜD•Ó\µ|Vïð5‘ºwF‰˜Ø\‰ë)›]6ÏÖF­N@ÞpPÁå¯ ö.u¤.ƒî'^žñøñø8^ Í?ºŽôïž  ¢aÎ#$µ¾ÑÙÊ‹Þù¦å]n®ƒ°“9ñ‘XìÑ4F*ßgQEFMsŠyX3h9Þ!Òèõ.å³ð:TR½/1]FÔqÕW¢"@ X ª(Õ’^r§Nµ¹þ*k"¬¦%0T4ÜýgTφ»ÖÔM:¹èý™"ö°ÈóòKç—Ü.™h‚œJ­®ì쥫92¯sP¯·j;2ò‘Á¿ut»VlHjl –"ù@eK`%ŒG,Å(Pd2ÛGÅ{á¥Wxg<ù+€ª\ɺ.ú‹©·ïú†åÚ–DVbPž&;(e”3:—ÃÇ+¼QJv”e4BÉÅÂ3Êr}1F°ƒhÍg÷ó¥èˆhÙeø +³ºúùOíb+¿³ÝqørÙ]«%NU^ä3ŠD*u¢cáqdÓ]€›•4>nDÓ›}IåúyOÃ:~×ÜYLî¶Ç&J2Í×3àô;.ãlw>~ÿtñNKû` ç7§¹¯‡ •cd;K‰í,³ägÄ·kä{ JìQè¨8ì>„R)uƒÊãGÝvbt›Ôƶx1P4?ïfFZ ˆ—³Ò¹¥_©á<ìyÄÃ`Ïw}‡×OÁ‘µÒ_¯1|>†<,ÕcÇ(Û¥ëÖ~ákOíwî*t3J3°Éú£ùKêÐGÄ?íJ’³÷ÇÇÛ™² +Úw0óî}ñÅý…ëÐ(q6C’‰{Ó¿cöÇIɨ¶£öÏ ƒyE5­ §j†_ë½#æŽÐ$äØDƒ¦‡ÏŽ —kìOŸêYcxN#5ž'ÈͲ4t._N+i·$5§÷c½¸Y"ôÆdc|Y5 Ú,ˆÌl#ÍûqY,Þˆ“졆Âz#eÖàaÄC_ƒ,fˆ¸Y#2Š›õõœÿQú¦KùlŠt÷¨òö˜Ó"ˆÈ2ÛUë5vªjùçîïA—>ûVz³„ŠÆßvnzæb8ÿbÆgâO³¢†¡PP±ZlvöyÂf?MnèH©ë +K>çuf>P;c¤¸pEmJo‘o"Ôs"1 UëyÅÌÀ¥êJeµz*¹g1çqõ³˜@V½3ÿ¼µ´€!hÇÓ–Fc²c Ê2Éð¶Ñ¼B´u†ãlj ~œ2RW€=…‚)ó¹å>­[Nº¿‹X_„Þ1J«óÕª9âß`”t»PjÙ¤·h­7ÚQæ +î‘u»†ÃoªŠîJ +°ù*E á;Ÿ†Hî +CµQñHÄßèÌ´í¸CÍTp4é•jG4·Ëæ–°(‘•Qf`g°éúÞÇãÒÑJ:‰åŒ·Q¨“5ˆ o%žÍ+P…e‰`U ÏôËž¬m9ƒÈHëxû2ÇVýç; nCÍœïHŸ ó}åì[Óuèj„ƒ®œêÝÝÀê󑿯¡ac|´H­ÑyAúªJÔöÄïC½$+Ù7…O³G¡@Œg GdŸšü½¸W+JEÈ`f™sãè´Šâ;ØåÌË! üHÉö0Ê/Ή—å°ãÔ¦›¯ŒdE(;ô\ÈJŠ=ª’+Miª·;Þ\‰"7Wb«"V(°ý]¬pá>sÖ\)8[«ÊëéÈŠY·øTX‡)pM½]óLÆ÷¶¬y@®ÃØ6m”t~ ",æ«[7•ÛW¯öíÇy`³Z/¤7±·:ëå¼~ú¼w‰’¼Maëñüݳ‚~TؤÀI=_ŸA‰HÚÞ×fy{BÍMuû÷,TŠu8nãÃLxgêQØL“~³N¡£iÅËV4ÂË×2-„ uÉÔ[#y=N2|{5n>>®ÙfôæL­üâÀ•ªTc¿xþbÍ0h”[/³ H†¹™›ì‚)¹k«µ‘¦ÜIB)k¿¸Íù“ºád©"—ãé6…û%­ûJñI¿*_sôEXOêâß|ëtóØE}hv›…¹IiÕV<U;ÇUhiìçŽñúº®2•9AÐÿ¹IÌÊÇ\›N¥pФê ‘ç 8#ÜbÅãý@îî;»ûããX΀>_·kè« âi +ùöyôo"|Ž#Ïq”Gæ=JÔ=ÆÄlôÞ_t 4j‡ÙžhËåqà’Á¸&»Sƒ½„—5/^ yߣüIdÒ ªúG´RI;»©{DŠš)E)êÙ½§±AYk-‘57¥-ŠŠ ªXÒ× +DÚÏÂ÷Gĸb*DÙúA.'Šm'Q3*š Q¬tKA¶(ºd¢gY/–?7¼‚ÝÖë‹ãxDÔÖ/zh{%V–¨ªÏ㩇¨+¼Ò-«™kž.žu ]Ǹ"=/ŠžÆ’â +ŽvK)k€ÿÚõ›FzÙb´|«ÄHšlÔ]vÓüLÓœ1kt*fh~ÇÛbÑgº6è#¹t¾ë3"cjË8bRfÿæ<°CÁRâøçd^ÛVEÞï +ØM™º¹¨TÅÜ‚Ž*c (:fn)Ósn>«"˜Bm+ùA÷™=€Ýp£ªžYsmƳ¥¾ÓÒCI>£Ä$¯½»~%'c'HÏÃY_¢36î- ¼sÆ@4¥+¨J!˜!Uûe–ËÕ(´ÒGÉǵ½.37ÓÉšקº‚ĶCaý5xÊðÄ[g¶‚!˜ _E¶ÑÙ˜ÁwÎ I}aõCpp¯iM<ÄGh+ÕF•záH4¶xm}ê6Xü°Õçß=–‚îÜk`ë3"·kl¼ì¤~~wž¶É‡u4thH‘k(}“M'Bãq؆ï£oÑS§>øaEi“(ú뵿ÏÙß<¶Óê»RÉg`­çâ&¯£7Ðé^–ñýÆ”ï~SøEÍèWQ +xJ¬¬ûœ1üÉž¯fWÕ Ò4‹\fürÊùµ+ +ùþ%dEÃ6=•–*|ÁÜDñ kpÖ¡pøìósmMfúµ¦*M:[ µ KŒóêvµ#@"ÔÕ÷¬–‡“Þòûº= +F< +棺E¯ÖÏ)ÚìÔl½4%ÔpeÔÁ¶)”×C_QjQæGéB[fU•ùÕ»•qñx˜û5ˆH3lpv×oºN!NxÑ4%á@×t7z?¯1D³F¾É/R íãª}˜ /„k ˆQ m½³ q—v´6Óå©­+ ²:Ðu/Ÿ¢)@ŽAYøúæ<”ºåK“«8NÍ­}@pÐö-l(ç}²Æ»<ŸA "Æì\¬ HtÿÔW\kVp?(3Ñ1CW8aÚ!´dRéûyƒçT÷BaçÄ +‘¯wKloÇ= ¥úø‡Mõªü6ìîÏ ò“mÿÏ2îÿ•';𨿤Ôû³þáüñÿæÇû¯ÿüÅÊûïÿøÇÿùã/þÝ_þ—¿þÓŸþöÿþwù¿~÷Ÿþúïþþw?ÿñþÏïþø?~÷þæïþôÿñÿû~uÑûuæÂÿæÿå¤]S4Ë`¦Óìi½ÈëfžU]±7n&ÒNg~âzyኄ©ó¢l¡¼ÎrÙ¥å̯ã5äSk›8¸Á\ßÊ Lð45¾P¢a±xBî9H;âó zkjô· Ùy?±WFÙLÀTN²½½Kœ¦¢\jÓî’}§"K{Þ°ÃŽûFHZ¤ì±k%$™aQÍâÇ+£VQí5r ß9 –~ÝT)?Ñw“$I°¸þ¤†umâ +¸²ÚQ€és¨æÍöuæìQnpˆ3ü©»+*”âa]Õ¼ˆuhkò.#)jÚ’ÀÀ»wí6}Šž¡œ’Êê»r'ÊkÑ’ÅûCd¶x¤’ɯ’jo^-ZL*_xYD[ã­üPÕ¯Ö½½¸ìéÜŠ#Q“(ꫬ$åÙÏLVÏOÉE(Ÿ:ÅYÐÀ¦%OOÏ[ˆ"Ó¡*PÇz¨>˜ÍÍœ.öŠsÚ@ÜsCÒ„I Ñ¥œJpÍ—ë%žmK”ˆ,q”K²ÀÌSO¥•ºض±ÇÍ¿f²ÍÝRÕ_5¥¹qÀ÷µÞÎûë@-Ö–þŒé¯ƒ=5_6„‡IÄObØ"AQï]q:zc|Ô‰ú›ƒXzêãGÙì^J`•]Ï ¼T7l5ÍžŸH- n9l‹·ÜOµ­xꛥ³ ׆´Õšíñ`ð=fr?µ-Ǻqû·Ë>ÕΞ$ƒ¿ÓTô}žòÐÉa ¡…{bKñi+—•54S|+õîž¿dü…•ê©:íctcP„*”œôB6>Âê9} ~XPíþø»ï0xÅc|;D-[´§’Ë]=¾G€…åG9ž™èž—QÌF䧭£Úž]±=^Y6¦ýÚs’(}š¾ëEgSp%qCWb%n×V·~jΠÞ``C6üL&žs릎BRûoµ£lÜ­ê–‡ÃA Œöü)?ÓL²â\bì¤]ÀšXòß7DM.@ ´-›(Àù@ Ì[G`Y +qˆùé§JZ:UîÏ¿s6qqõ›ƒ§†:ÑùgŠ[KÐñµÅ´òbÖ"ŒíË^¸‚±p­ɧ֣ÙKÝyo«ÃæŠöýù$>!n¯áòSIJÖÛ¸ IÔV&ŠËiÁh +Ikô›¢Q×?K¸ÌsÍ9 -àu­Ç¨z:îñ¨6TÑHô|¦Àð +iáô|ß´j|¾×‚’ávŠB jèÄ6mEÐ:4âVDƒÞ«‰[¹Öµ•Z +ÜÌëÀÙm­%5&mVÔň&be¢\itY”DÐœ%T¼‰¨ýNÄÚe +ÆRÿ:W‚öÊ]\¡(̳m@p#._À@ëà=7„N)càoîâ²´´×Ö’êž”òrÚj¨ ì·ýHäå„p©Œ|³“ƒÑdÅzûDN(÷!ÁuMì” OmTb÷ÐörÀßÈÖý™vQ¸EA†³çCØý XóÓ€îíkyô8>-ë8¶¬êlaªê¯‹Pÿ#¦âª"®–ì ¯e¤sˆ¨ˆÂ HÁÛÇ%€žtë@P&× †¸C¼Ø²BŽ’²ë1º)€ôLÖ¦jÂc4ƒµßXÏø7ÞŸŸ7¦)>°³’‰„—Ú‘ôWÞùÃŽBׂí’‚ú¢GáÜ„çq|ã‚¡œØxDÇÖ½lÀ…æBpí 8¹¡ƒæÆ^|‰¢o¡»ÖPÒdý°¡ô÷ZÏf÷<뀠/È“;B£¢5Ì(ë7 ë^éŒà•hÆp¥k÷–ŠÆrÛÆ}²ê*{ŠN µ©Ëê°-rå5fÈÄ•†Ö1¾‰pb©¬ÊÛÁ~å®(†`,Äv ÷J3µ ²iyy=ÚÇG‰Æ/=ž´L†RE|ȺwQb‘_ÏñD”žÀßéó÷±¡"P(vxxâªMU‰dÕt–°á[ï ‡xæWcqá {Ä,¤N¹2N{oC ÖèSøâ PÎkŽ=…=O£ž®ídÛT“ž mèúcâUqĘ4ÞI'ò!ãÈ!¬C§Ø§{Ö ú¥LBõË: +µ`¬4 ,â(uŽE¥¶òyÀŸ†HùœŸqѶ¨-×-zgP¾`nüÜ ×߆zo܇x)õˆ­ã/| +üc¥X0YÒÊêÄpbŸE}ô˜ÄªßO‡ü½íE¼ûÄ«éúèøiU‹Ù”Ùw1ûĤ¯ç`ÖÊZ©ÒIù6‚GñAcE»<[S»KҵαÚòÞ:ÚäÍØü©>¦Psw!ñ+[Sõë€dX„É[&O÷:xGþ´ÛÌô]x~R#Jgàè:yMÝÿ9à«xþ¤ðÊÇAö‚ú¢p 9DÑñõwô*5±…#¼AÄ_¡ëPý_/Ô>ú=j•Èoîší®µ¡ý¸ÉBK” =œÌ6¼VîLIz?ˆ5Ö[rcTP¿yBÊÇÜqË˭Çcsk‹„U즪Ñþ†Tì×. ÓŒ‚$må•:@°¤H’;zÅTJ«n­zÝc6´ oÍ+Å@¯T.À€ÝaqfÝXoãÚi¬©wÅ°ûŸ›'}ØÆh–ÖÅa¡Š6Ó´’ +íq&k +8UQ6JÇ™ƒ…¤Aä€ ¹fë¹ùƒy÷+ȇaÄ<&µ 8Q=Àb;q›K:ë7¼›sóµNëêß6ÏsѨY-c:¥íò]H›?„þrh]ÛQpñ¾*»výÚ~ý×”‹µ.„ƒ5Ôj: ÕûÇo¯ÖO&¬9 éÚí5ys0›í—×¼nŠ]ô@†/_ÍÚyoÙ½q¬+r·ÉØ=¾õÆŒ3Ë­¼X|‡.„¶LÕ'#¤± P–H½áVhÐÙz¾¸ŽZ5ªž *™þÊ™— øþ%ó› 2ƒ%7¤Øg’xêÂ5«?L™4Ddß5°—\ ‹æF7»Áûo9Åxs ä£WŠÿûJëN\Ò<×ègzõàn‡eÄè£^¼!ëÅjÇvbÆB'æ ÎeQŽ)„wÑæˆU(+ó¹Ž*¡Ô¶×Kž>±™èzŸ‘›I#õžñ°ÔÔú}É´t,Òz"ãßü…'T ÛC$¡4=uÐü™G›NÂЩúGŠ×ùûu‹ÌÍ?ªiOWpÍ›¥Jýˆ³öŽDÑG÷sR^½ á»Ra_#Òº«ÉÈõ‘\Êï8ôK>†»`”ùc¨¤û­Î‘×Ë9‘é‹;ŒÓ9*gˆÚf‹ùM„uµùz¿>ˆ¸Ë!YÊuÀ7¶=ÃÙB t-¢<ÈeÆçUa/?®o"œ&.* ¶^Üfžg’ôEV_KC`%ƒ¤gk/f>wMá|oX$'î8Á 63@©ªöØ‚³ÅE²ÌÍKG@3žÂnÝ=6I&¯HW?#Rv EÏzÅ•£~FñD¢P×VµÖ}æÛÞÖwTã[chkâ¡ÎTP¬ÞLL>ž7Ôæǧ‡"ƒÇi —nÊ7fçS%y¤¨’ªÛ{Ô±­SB.–Î=æØÓOõ>;»eDµ;hCࣟ¹OÙ„† gàoÎC2r(þuBóØÙؙ諑½"‰ãI"°=<³¯5ŽÄÙ!‹ÒŠ›óIá‹=•-Í£î'q×ä½Cúõx}j7ÓsŸÛ(ÿ&¦ã®Y]_v`Í¥]÷91º[Þ;Zã)s¯m€‹ÈÚØßjX£Öiè}¦8Aɱ™è(T’ÜÒ?±NkõŽqÓ)œdÄÓ0G»FC=} íÌ]² fÅ6"Þ€E¥×[™Z\¸K‰tÿúÄÒ‰(ºm·°áÜ[xÉNZĺwˆ¿ùÞù7`,Ÿ[ùjϼ]ÄÇç@P˜*¦€ÏéZ&jìÖ{ñ.ð)Ã=j]C°ÊPì4«8bz¡†œ§ë\ûŒƒÄ>\…W– '5E”@/þ½i§dnïuïž+´5Ýÿ°éY#wo?Œ.†ÃãBë¢CßÝ+´ëY™ËWµù½ëAŠ¥ë 3YªTþýÔ Hw/œª 7jÃSÿÇëQ<<ÝͧZa£þõÃ?ùs4¶ïsOfÛ•Ë).¹°uu´:ð^v/J1nÝ\¶ʃA0¦oÙæy­IµéŠ<kMCë1hCÁw4K›z›.—Q!¡„„Ò‹Ë×+@g™S›èô:8);ã´"Ð…{]iÐ4ƒUI¾Oñã§Þw8’]þ»«'a»-DP¡áî$3×5oÀ:猈@HïynÀ1¿¬œ–2éÁQÊ©ìí`kБŽ‰„7UÉ„}%;&ÛøYõs0Š Ä¢Ñ=UAl°‘ ¹UK͵êžß]ú¶¿ ­÷Jcjo±Š½ªAÐun~|Í@)äÎ…&kݺe™;¬¿#eŠ_šõJ­bÖ±?Àjúåc⊢ÍMüaÔ-[…_7qp»Úk+£›»(ø:8îøR4̲Jž(Ê\§Û !WÂàa+Hߌþ·Â$|ß÷¿/Åo‹'¨¦²ó`n©ÉóZ>9žŸç—7ÆûïFaõ&Þnãú8u³â-Rû¼^ÑN.ìƒY¥NJ¡*é¿EäÙÂóÄHãÖaû›ó8DxϘV[œÆ…2°Ž“ü}@Õm1d~x²ŸQ´à(–7±ëûÖäÇ´x¬Ù°™£”-LŒãEBs…Uè¬Y¨[ϯgrѸBkPѽꓰ7j5àÄv õ§óz ¸¨Iƒæxgt µ{îëˆYã cCiˆ2XQLµù6.<4ðɃªS*B˜fdÒ-çûVA&õL¡ŒÙ€ÈÛ÷KžäxHSDYæ¢-G¢ªqiO~Ú G‡RÒŽ%ÃîyVFÊë[äBö1w«Ÿy6ïÏÇy ®ÈŠüjÑóܺ]èt±üˆpômËýzÓߣ®çÍ5îŽ1öžùÂ÷V¡¯0uÚH €hI„zøý†0Ù¥À½”©ÿó4 ]X„AºhÏ€V ôÖõGîP¢u€å¥¦GßYB b†bÌH= uA.sˆa‘Òe!YR}Âñ3@qɶÅ%·?ÈG N ,=åvô×IÑDòÐÑé~`ÍóŒù¤ÑÞúèÏ¿ÀëÓÊ»ë0Jajhñl„Šb,AëÏW4……|¢<éRE|î$×#"®ØPüºRVWÚwT„ï©û6sòíyxj"„6-XÙáöÃ+±)…+žˆKúJ§¹¹_V.Òùu§Î—wɸUº)h)ƒ OÜÀ m[éUQ¯˜`NÕç'}DòU’ËDèrê• ¢·®zòGßÖ›&ɵÅ?ü~gY•[ ÊâQ7N¡*…õ-ªÆVvÍůÚ«?-ùý§x# ;°6p-©oæݘUÞ×d0“G8Éøáj¼¼¦Œ/ ÔÛø< ²Wìؤ ¾ÀM€Ð~ˆO˵o^ÏÍ{\r ¢¦G ñ(ý ?ˆÒÛ #(TÎïsõ[‰²É.i¼q©ª+ÿ+"Êxî5QLÅs·T ’¦„ñþÂ'{ù¹¾®˜t9BCM¡i “N& +¼>g¼CZ™œ¢?(Ó²çüy ÝÐVÒ»4ÛókÔ°¬ø”^g0¾ÇÁ=]›‰I>÷NÙÛÂÈïϺñ¥%ˆÎÁ7Rç;•"¼’îëÑE½¢‹:Öݼí_ŸQâœæçÁ VÝw©é”!øÙº2º7—X¦û?7,úÖ…–ýa”}ð÷=Õý‡¾ Œ]›’w¡³dä×ã甬öÎQ³¹3zkf`&™þýïn + ŒÞ :Õ%: ªÅ’°¬Ÿùõ÷3ðغ²ÎsOܯƒ âႈ§¤¢³oÊÐË2'oõ³3ò§fÉB› þ&@AØêÏX“áܲ¸ò#Ø\û÷¢¼Løêšn 9$8ø§—Ó‘¬gö9r·žº›nŒ"ŸÚÜIŸeæyÕ_HYU ÜY¶©FtÖ/,]ë/ mÒ¸]þÆ\µÑ{K@ÏBǾû³ÛoD†dF¼Ä•¤GHGlŠæ+š%{_¾xîêJIöQ%¦ƒ•K­:S¦]¾B+h½ +T'\ +mP‘õ Pùë@ ÄD§¤Ý¿Ž#Z\š¦]?®+ÐðÑA– 67ÛçGvìC–MBlÆ8ÎoÎÓ¯Jh^b²©k o±~.¼Âψ÷å#ÕÇ÷(lŽEK#c^ÌSºéHÛB…zò…í$‡M]<Ê3[Ãt@`gÑyB…¢)ƒé*EÑ5ÆʶþÜÖ;aÛïFä„Š+ w”xÉS$5ø4ûMapLÁ+i2•æØOO1 fð”ÀµOé¢òûÚ»Êwúï»INN•˜&®DÙ ßZo +RF_^’×FàHZz¼7=*ÒFL¼ÖåÎù‘zø‘­8@jqçÑᘇOº;Ʊýr×»tÕ~@³›N&­æ‡ÄûÔ# A”Ka×®n5X¶PÁÅMèwtSõ×W¥Í±­•±lѯ·0U¶ª Z¥ÚÀÞl‘c5¨¡Å«FVÕo· —<Í_%þ’UX¢»Îø]CÀ•¶éзûð‡%žã„ôº•âReßáNa$˜òΉ3„¶‚ÂäÈׄÇÕµ¤êU·,6O +‹ DKc;³ñÇkqKK%7=±YåHqÏMë–×AÞaÕw;õìF«"~óØ 8£Ü³üÿ¬½Ýª.K–žwº‡u(ë`;#ã7í1‚66>±uÔ4¥6,µÛ ¾{Çó¼‘sU­ovEa«÷Ê13óËŒŒ1ÆûÓ¯ˆÙ<-ÓÖÞ¼\˜mÛŒAÄfŒUTöO¼öOÈΦ32•òùs<æÜa]?¯Aƒ¨zÙÁ+ü YÌ•'0Rá4h +fÜZÑ”ØF©!¿D9ô>(?œóS4yä0+ÐEÓgŽ·M½gÀvŒCºRe(ˆ4ñ@H–ìÄIËYǪ3~;F›6¡¢8ž¬›Ú/Ю‰2s]‘²§4‚í§ÈE[¸ÏcƒV‰3³víŠhÿÝ@õî¥~À· tৡf1bÏ!¨à=~¬æz€’€`5pØ1XúòžN©¢ú£ Q–ºÍœ* +žÆ2îAˆÙ'oX@å¨Î[Þ4Ç(࣠TdËÕcÏ#ö$÷g9£»¦ŽSÕàg¨§˜+Ú‰ëŽešQ®ôf/b)qŠK9Oš­Í%¦?k>‡…>m¯_ÊùÁBz3¯‘%2:"(Råà7½ûGe/رHqÑŠšN3­=:Ä ¾X¼Èz mðñ§"‘eJ@øO¯Œ´+{ŒªWS£ksM,Œ"0­UZªSØÁ”m!Â…ŨëT²ÿu¾ ({èVÌcnvw­÷êÊh°}:-ý¨Ì\åõV½†žJ?ί_ øÊx߇öoWCa©Íª +ZÛÏ\¦Ô-Ñ¥ŸwXðsçŽ$¡§GÉæĪ_Ð;•”ìR™{…ê˪}Õ\‰í›€¢ÄèŠ(ø%sa¾(Éœ#úÅÈ ’)¾¾®ôdx±ýš>‡Lß”PEµFamG½åšŸDïðÒÎñI¢@ù‡" +6€ácÑéyáµ´ËšxX\‹; Búlo‘ÉÛ¿s¤ßTc{¬ô)ù‚&|š—-Š ¯ZúÈjEÅ¥](‹ïšW¢ Í•$W¤ÞèÀx^Ç€˜% ìmh†³ýÄGqé­l¬×¡–º`YÀ0eWS#œuB!+xuš½&ÝcEÝ籜Ùô¡Y­¾‰ŸƒÊ2,ˆÆPÛ±ÿ^(étº––€JfaÒK½Û&,]'kw&iÕ(Š@ä¯P2Œ]9XÏ9Ѻ²yä±W]¡4M-¢úG ¶C„-£âûÞŠ ™Þ~¨Z8ÂBÕjX4ÞW¿ÖËå’‡5«åv¡€“·¡‘æP%'ß +M.Ùd‡&~7,©#ÆŸQX sAŠH2ù(Åö°>3Ì*0Ž+¼s,œÀÓ`ád‘M"¶ñÓ|È=ŽðX2’k&ÉQ%²7ˆ.T&_/‚ä71ÍLq„nßI|\MîÔ,4&)L+[?Ÿ„½ùÜ·rŠçždÿ*ÿâ q5ÈÀu —]öu¿…b§–{/ —]¹ÉsH äOÏ$É = +IFÿšëE¯£ |ÿÊÇqP5.$¾=H…¤ +k-Ï%(„ÖÓÍÂý–D¨»³]ØÁ¦‚ÃoðrKýºÒ³ŽîíЛç¹;žû Ÿ®É[ËyµK +š£'!PÏèHV™Ò8£Ñ3j¡ƒ,â©W‡†3ˆ¹‡b‘B…SŒ(xžî­OØ‹{¶iÇœBªÁÞY,D¶³Eéá²5QdžX2$4$ÅrEyñâZ–ãÉEŸ°%+“Û +Yü•%“Lò +yxGÙ6Še”Ÿ„ñvѯôäK89U“_Ü»˜ ˆØ(A eA ¶7ɇ²PÍ;œÛÛŽR”Òâ(­@Ùò†ßeÇ£ úM +x.D׎ÜuÑ6Ì»Û¬¦»@d bRí’B ›@u·½¬Ú"u‰.Uÿ¿üüþ7ü6ðöšM6NꑲÌüç(0+Vª¢¢¸ÓŠ‘fWü Í›–ÆÞ‘È)¼EK³*•Š D{Yy–r8¨½ö­süí_©lT”,óo$TäNKà¿ã¸óéE”Qkr;»æé¦ÿª&äVß4h‚Ý×Qõ×+vgSõ´¬Àj"@I¦åc5³jÄBbòì{¤qY“^Ic°î/Ãë½ R÷‚3 +†’ÿ~,+•×svÒ〴{N«äݲÿu¶[1º}³}ðBƒ›`]OµÔ(±ÔÀ°žÀÂÒÍG¶Jñ¥V^h§“àøœ°g†ÏfuW‹ßN>Å@š4/¯"3•èûTòZMÅvÏ°~H°žÑ% Q.€d™wö¤…F¡"ìç›ÈžÆËÈnÏSéTX_[ÆÉÌiåo[¬Àvœ´†i Zê%ئ D˜€1VÊ«y³aHƒ±xx¼ÙÒ1 µßŸ„WŽR]øÑÙÀ>¹œ…Ÿ_Ë…M± ÁOiŠQ¶'Ÿ=m´ì0—pßÖîõ~óv­n_ +÷§g õêylÑOõBçÙƒòðö S¸1‚ûB¾o”D¦zÔ¾n_ÒÓw¶XF¹]dº`»(h•JD´ó“èèzWñô ⧲ª\Ú{~Ü1EGs©F_ÏN6þ“¼AƉØh,ªp;ÒÇ€ ÓMòÖ¨!è³¢Ò]Ð}›[T<.§#ú}¤xUŠ›®@!lq£±Ê 1œÜ?òùéé½ §-WÌ)Å»Aàc¿Í˜U?TuÚÙ‘[èx" ø<§)r_¯¥3ÎF§8~;¾ª%Æl£ögη§íó­ƒõE"a{é+$ý¦  ’T˜šI÷«$ÒÔü§šûd@Á÷¥Ÿqjã嵜߹h{yÁ”(Æq %@hb:?¹}…^Ô¢Œ , íjÖÑ̼­Ö´ªxK^3r¬2P=Ù,RØë!‚Ùb—Ѹ¹VŒÇ†5ìûû¸Dá«"3&Àó9õ'0ã;oA ‡¦™Žg/úx½‡}\=hsÞéÆxêŒZ ³.qL¼!¹_ç;b +2§fÛ•*„¯ûüàë·^~ Á´Ì6:¿È ±¬§UFòBèJ}(ov go‰‹N`û­™]òõ/‰«ì˾3äPÐöìTÌòþ÷qÍ0a x€ªp¼ó€ª®ξŠì’V¨qæ„Áz ’d'&êÅê‰rà–»—Ý÷lЪx‹W;ù÷&zÓÎ+îCa +³ÿ„) (=x¿Bm™U àÀŠ ++*ìtt¨h±¶Û±Ÿ7ƒ¦I :xa3*Ý©R]`“ò¿÷â »èà<Žˆù¥Êr½z…’@ÀóÍJ+ËÑp)K^:Ù‘<¸ç·öOŸÈ–X!e2e1¡š‚‘1Ø[‡t\ýN9L”qâ¡ã)ÒÍnmÁF:ÜAáÚµC0t9Ù¹¾È:zT4aöÐsÁ·ü H»›îâcøj§ñByø +w0¹ÊB†»hÝÚê“Ù“g U;g{Ü_êv쬈ْ*œ…}î{КJbš{Ô—ó9ÜpV%Rc:²W3 +MÔK4¿¡"‡Âù$ˆÀZµ-†V‹ðËŸRˆ +›P¶Ø_îƒ %5G±€Õiqíõ íy…&*Ì2@Gûr==§zårØ2Ó•©.63ò­WÀ/lÉ©ˆ´Î8¨þõ`á"g÷Ç1l+EjË=%ÜÓs`3FWí} ­³C×ìÒŠÅu&V»(_>Ѿq·}ÇÏ'îÍÏ+Æ+9÷…Xå•B…5wŒ))O±{Z)qòɯûȪ8ó)²ÈsžäUóàYJêEj`ç;‹­E T_1o>U†ë«i`÷E¨lœ.ÖÎy“ï4lqyö¨Ö¬;ê¹Ê1 •+(3’â.÷mÁkûQR Ážœ’Jÿ‰jËÍ6•àêûÍ/˜‰5bÃ6¨»·ÃFˆ_£h½‹ã&ññ:g؃ÝGC5 ̇°+éú{@E‡UdßfXµÔ%îŸ[鮳*^ÐjŠiu =9¸Ž$œIW^8bK:ViËíù êصBo´s*šËç3CÄÛª%v‰o µŽl<»èF‹—úMiO.1ÚG9³} ÎÜG¥Ðå¼ 6ŸU2H5ì&öv°ûÛû)ÅÃP£ˆ}Ôª»2Ô±iDË¡@hî*Dì%Tè^%„ƒþ´V€úƒÔešï§LbU–|¶=>+îk ¸L´ýî„Íw›çlW®¿ÞmÏž0”Ätœ¸ÎV@X°»b 9™ VöðhΈÞk¾ 6 j`Fkz'Ñ^ï/ÿ7 (”æã'€Áª˜*– H lm°]Îú^íÑ*lˆH](kÿ€9NãÆ1ª¸Š§-ìfdÛͺ‡œnÁE”Ôxù %}Z(\ù«X_ ~G¡b¥àCxöã=[;ÿ”LYu:¹!Q c²ôÇsÀ§‹Â<õq ! õžv>:,³°·|Ãì-óA<¤ÌW ¢²Ó Uó(Uܺ&­f8Šå)äWºwì¦âCµ—âzè@ !G^=bŬF¦îçEãÀé.{ +4©þdéÌÌY·˜é8õƱD¡KøtÀü‚ îi6ÇVV.F^èãÌ¢ÅGmQÞ0édÉÙ3¶ž÷3Ú8¹‘-wc©:yÁóN|ÿ¼%ŽÛý|Ë~^qù٨ֺΖrD—MSDO¸‰eÑ Œ¿6œ¢Ø-—'>¢ þåÔ „iï;`J iªÃ½/4QÚûñ÷GÚ®¶#yZàóCîóSÑH8ÒÀ"÷P]àò@¾‹Ò/Ûv×8çyPµ¢I$â%Äî?µ“=´¢?/@WbGØZzy^jóç44ì=f"PÙ¼í­å6x4]8‰¹GVŽ¯ äàSp¡A©’(>¢T!5gê¼£A‰-†ûCáalšSØ wSQ˹™ +Æ'ñå1/€0ZÍ Å^ÊØPãEpÎ#¾Ï¾29¼Ú1T½ Åk–!‹SÂý®ÍD‘¨¸¤k÷<ðܽ}¬fâÒ-!lÔ}ÇÙ£(˜ò÷½#Xs~WÑEª6€€#¨ÆÍl0l¹ªý¢ßhª;1Uít©£šKˆÍÙúkƒ}ê>ˆ*E%å:‘a{øTñª²AÇrÕOl$;¤åÊmEÿÐTö=°dæÛi–¬q.!Ke*ØS«Èý’¬*peK?È¢®”ñJ[«þÛÔ ]‚Cï0&%õ¨ˆJ’€(Ùä Pš®ñ¥-´ÇŒJÁž"èt|]»Uùýÿ=© °ÙyZ³Ž€š3ûñËøu…$+Ó~¢3šAÏB«a2˜ +Œêf´|®/r4]ß/d¦p¹Hî·&àŠ2{ +Pÿ½²[Œ.û–u–ìÄÃ+ ¼®Ó‚jd‘¹-(ö£·FXɃjj5"üg=Ððp¹~× Ó“·g.à™}BÅ&èk Ž{C¤;OûJ•6€Ã97n{t×ÝyýS:êÆpÎÔot0¿gP6’õ÷ÔŒÈ%Q¦{L*Pljˆl쌤Ù#kÔã +îvûï_<z$b@¯“t3‘2jÒæéåº]Oãý K´”×`\Õœâ]‚‹ j¡G{€±ìS~¤Ñõ&ìz› b à`Ä'ò/ù3T,îcPý¼LA’´ÁGT去νŒ‘ªû‘*\5è=‹é{‚ƒA©”¼•LOtüŠ·fé U‡k¼Ó\y)F$6D]ouR¯$ fL¤ñ§ÜÆŠ0ˆÂÊ´‡ÑGu0R0èn¼ÙçQµLܨº¿êçÊxE’µ( }Q͘•ð¤Šwo|²ê9# Êeûí«®> ´4_*-©?ã9]Ó¸yÔYØq!@trèõÞëôèìŸ +užÁyìý1 Ÿ©¾V8ËçÝaA¢U!Ÿsõc²L3¼G}¡¤š‚ŸÂ1—[6º2 ˜wrŠ¥"ßl?— +Y0·çêïã†Ì;®ÌÜ-* Ëzôñ§Þß #ö»³ÊòÜÇë:;x£\QÈiçáøÃñãƒjúaîq`r­Ò‰쇾±(®‘Š¸˜g4kI[l]ÁW'ýT©¤ + 7Þ¢·0†¾ŽÂ$TÒ\Ur+LÝ'°†x_êzT÷.èÞ$”¦ì¼ÎsAngXŽ‰GFNÓŲñM²…E€ã ²›ž¬Kfu/ñ n»?<Ýêœè'–™ vR© ãÐ/š·Ì6v¯ùSбh}¹>}D…í]q;À6œµXKàÇÅ{ÐóB¹JfjB¬¸[ŠöI±ÉEe9>¬]{n’hÿæPÝUlçŒÖ]MDF8oÊeî¹c¯Š#53uÍáD»ñƒÖdmäRÜéçj¾RÔŒPoîÅbhR €ÿ< 5ÄÑùƒ Ø! ×|ØZÈÐúZR‰†CÔíL\Ï.ÃT3z7 +¡È_#‚ÿï¸j®T]-ç÷\é êÃ={¯³&Ô#Q–P.†Rý¶Jpì_dð¼=Ooâ1Z@C;bjæŒO…ûYÔ—¼¹ç&F´mg¿ Ñ#.#ž+-–aîbŒnvòÓ^µ<#Ú ‘qG¼ÛȽ˺þ§dÚB÷sŽxת WO„KDë)vË:)’.à»÷²ªH{t2Lbþ!ÊËè2¢8x€“X‡ªÈvÁJu½œ˜ ݬ º‚6°ž¦¢ŽºëX»¹óÔØI½ä +æUá¶<€xl;R/$»€Ù¡P&°àŠXÕÝsw‘´•Ý*‘®‘ÁÃ’ EŒÅ¾¬$ÐàÈàÊ~ØT{o÷Ês€][Þ¸³1FK_W¯~Da0>mˆ31YïOPµ§I­(•Ú³{`wwÞWŠªhG9¥æãbfÎg&l·òèšûouÍ ¿uÁðpºŠG2Ë÷\èƤ®b¡qt­/Örö]ãŠñ°U׊õÓˆ¢ê£2òÒ ¸XÝ´«¼ß²I \©±­D¯€ö$Ô… +ì_Åž©ŒÖ®›Ì„ù<*ƒu¦®,Ûq¤ÓEb[¹¾Šëà}ûh¬U/«´KWäš"¶È2ÍÇŒþ%E?°nMY§úM3…º_¼“¾®$ëÆWyÆzi˜rí)…éǶ¹HN„#ºL†®UËö¦«ÙVäz(mKÛ +ØèïgÀ#ýP2á.j”pË6ud ÄÛ#[  Š“á9Ç-]#ÓÆ…}Z'…6hŒìº+¯Ž¢ "µ÷H\H羽ºÂÐØ° ½:÷¦‘m‹Rd &ž/Ò¶FöɯÄÉuà#"ˆ_® zÃ1£ÆS#VHÌIŒ+˜Ë~~;²xìQøµŒú+=GŠv/f½̪­¾É¦¶[6h0ú3¶Fw™Úgè`Ö²Q5€tŸ,BÀ·u¼*ö´O»UÆ„{#'VgÝ×1˜ôÂûœ×‰€.WÌsÆ×ÄÑ Y©¥‰Ôu(o ªrÍ¿k Kû¿–h_tÚŒ¨ºˆ›A|©£±/»ºß-DBª[(ÚˆÏA·j„©_¸º"‚zûábÌvä¹[,å~ù¸MW‹²û¾Vž/¦˜Œlànå Á#¹å éÐÜ­+æ>]F é±O¤Ñ9ÿuô²EǶPCi¶àOmúEWØ}*¾ÇkÚÏæ—¹0î´¶ÊEa:¦X´ ˜|È:DåÇö¢ñwÿ&4¥gñeÇ©žÌ‘ ë¶{\zÞ^hQÔ÷44"n6p3‰ÆÉö6ï¨5ï@åÐjª ­Áñ¨`’«Vó5ùD]ä–-jOœ™ngÏ‘pfÙßóG9Ž™êÎ’kÍS/ `}7[‚”In³Lup©b+jËsuMÊM­Y%øÂIšlΫŸª²ydÁŠz];[úÜmñ +BWHÑÉvÜ’!n‚Ø¢Ô’MrÃ;&£1ë^JÖ!=w|ûh¹ø½l…j>Ù‚äŽîíÁ¢ ªÝ¾õ./l¥ËÙ[©yô·„7ý»Õô¯<чgþWüßw–˜âT( +À•©|„ÔYƒ1c/Çž® ³ÍŽZ”®?¢|<ÂÍ)©T£¨~¸¢óÜRÅɾ¤Þ»qÓòã’Uˆß¢;‰‰ +ë;fm„YŸ{u“àGmT­FÔ³YÝÓŸ`éz Ö;¸¾Èæ{1‘¬FÝÆ%ž§6A4R–ÀŒñÄî^ÚSH YéëÛ=-ÌíëB¶@wܪ‡—¬´ÉM-ÁÊP°¾¦£ t„ª¹7"F¶§Ã Úà×ðVÖñYBï9’¤ Q䈘&ºÛgvnÛNn(Ѫm”!É~q½|Y¤wö†ß½l;ýtP×-V5Éì·ñéÆ!iážò‰Q;›1Ž¯s!|že“è°ŽãÓ© DîÛáÙö¯—îä-îåcÎ<“žÌ’«êqG§³¾Öž¤ô ›é§k,:Âu“6Y°{tTàg¾Ÿy‰pr]ìC.<&”›gš_#½PvÏòK&Ô÷ór+ð(÷áöº1)´ÜJÖÇÏ«y8íSùã³ÉÅì”^ñ µmfͼ:¬2þïç,,4ýyi(+¤Z²°ÜŽ³íþ“/ÐLÀ“ÜÉãø&ê¶Ì9ý4ûôhÕÿiê¸{öýŠ˜?OzÁzLj:5åáç›æmtåø¶_!ÇÏ(IdÈ1ZSÁªQ³åk³áIÆÛgt%„ô—ó|¿]{Õÿr=ìa‰ï 5Iëzóö÷x]nÁäÕ–ñAEýT¼*]ñxÖßö–ˆ½Ä’Óõº_ W‚ä*§•UÕˆ‘>_Ø)«{{ÐR +™Ø5ua®v. Ná°síKÓNUÖ¼…„Q³ÅSÐ[¾¿è}" ¿¥=jìË3¦y#es˜¾Fm> +mFI/½S+‡Å'¤ +ÇI²gˆm÷ªFÑs<_g3Ur¢¢nûr[ÈDÓ “šHDQ ¢J÷ÄJÀ:Ú`ᨡE§ÝÅð±è)xn±†­(ÈVðýAb’8Ï…p~4óDÁ±ðÁŒƒDî”{G€Òêò ë%ô²£¿åƨѪ‚Ù¯eP‹Ûo9Ö_ ‡ý0ÆúU¿›'­Ó=†°‡g?NØ°ŸL!¤Ô¤Yɘçt5ƒéºÏïê:#˧z;§C)ªë&0£´tç/V=Usñ8wžc›³Ø, ì[pwã»ãº + ïàý"F)ÄlIàþÖQ?#Q¨ïôÄzyå0j$R +ûtiÚkîªé%Ø·)çëzIû ô¦+8|¨'üÆ*et³BéøìäA±R’ó ÒÄP)– å•Iì $V9Æ£^I +$S=K ˆª4šv«Fôæ'eceÝ[JîM¹}Áì}WÌÎr2²0¡ $[T¼/¦›AAêe>`cÜ쾃÷ßQoÉòøñÔœ<(ülu¸½õïݹ¦%§ý§ ”õþ§1”³çWh¬%K(?†ûF˜ +BÎP™¼7À3¿ì™Èø)>ˆdµœÛ +£­žJVró¼ËAÖüÉnéËûè׃ãŠ7?Pמ(6,90Ž×K>“óOdOU(>µå½g…y)sÀ~ oeìéc øµ‘—y189ƒ"À,8ØVŠY@ïžÓ«ƒÍ¢WÚ7XÔÜ.°¦z_›ä?RÒ|FÒ|&z.‘¯ÈÓ°‚Ò5î?tGšõóÀy»ÓÉâ׃êHÔ`p€Èñº€$`…™D®ç0röÏ<`aÄ|Oè"àºAgíbÕw¤_á€î縿¦Ü`æ_Å#ñœÏÆ +n“âÖå:fJ¸íOOÏC²Çr…ÙÁÔåèÝ1d<ëõ¾È} 1P5_ ôý5B!ˆ@½è‰\%ˆ¿– rÀ·|aQ€S@y¦‘g —Òkï=I:FªëÜj +‹8U·Ÿ©|òŒ¯ý UõI2a¬³dѲÜß8T ˆâü‰hÝ×oþJ?ÊŒtí¡¹gFøûïŠ7pkhŒ#æs q’ZÐ÷¹GÙ:V¡ëìܱT!—GñO¿ïؽ‘›(‰®oþœ²–¢Ç‘”vnÓeŸõó¸Ét‰¼=þ“_¥±'ôºÓ"Eñ˜¹óGË;÷«0Vt©ËªêL¶ßŒÄÙsÖveaÝ ì¨ÁÿRÕÆÝ«Cˆ8"R5‚§Œ.þG]˜? ;*"ºæ=ºæïnh ûTòW‚*Š>¿þUËM•“zBÇ}¼)Yáˆ#Çà£?¢W“Úvð¯ÏîÎ¥ÖØd]Þ˼ Ì~N{%ÜKâ:ÃÎ÷9p¼èûqùû8HçÝ|ê' ݽ+Yl&~(íô}Î$øu°!ƒƒ,ƒˆêîÁ!* 'ˆŠ«ê× »ªz§óÅ ;qP/%i¥ªОç|?йR¤—ª*Þýq=HâmE ¹(¾•ýÄ8`vr=k䢨ùùÄüãl+Ñ›<Ë?Û$ 9z|Ôö `DÿhR'š:MÅâôhíïL€¬„íLaždðë Òg=ÒgÔEí–0¿þ*¸ßuíúF‰ÿè"ä j õLæ23Û7áÆ¢‡Á +ƒ-ýêGÌdz$¡üžYF.½/;‚i¥PEÛÓuŒP`ÚZ7â»Ý¯±Toà×xûCc à=í3 +b_„ø¡±(’S'Gué3â»™û#ê¶UÃŒÔpàv„Ëw­0øqã’·4ðÑú¤0&¬}Æ#~×tü?ÅÙS2­áÈ„æ4lWiKT4D‰#ß°O+©”ôa‹ H»Eþ•vÈOžÈô:¾¢$üLT(س÷Ä"ÙÛ—ìðqà_Î9[úµz+Ï7¾§F°67…~†Z‚$ h jëÀ +öæÞ­³5ó‘©ž{{ÝŒª'ª¼QÖ  ©s!7—`áDÌ¡ëy=8h"¬p”Èôco®tìœQ÷`2ˆy…ˆ †ÜÆT§£ñÓDóÍ;ÐÛvÀÍLlÛ3æ?Rä÷:ªVS«Òºk‡°lžåõ[—ݽ÷ñdXT>P\D¼Ñ4Lª%dÇPs Ì¯cj¨Mߣ³¤Ä)1VnSáSžÛ#æF/YŸpZÔ5„û?‡vxñ~½ë ”+@òÿˆ–>m5¢z4‰žßQ”€¢‹ça¡Üç¹lZ>ƒ‚fûdz¾î¯ ,Õ#vN§¾‡þŸìx#Œ˜ï"TÌaµ¤©äXã¸Àë, sEâ­Xá'Û`¹\#€S˜‘·þÓO)®ëÃ.o… íÆQä9|íUÏð®{—©\̤±²Û¬)ž" ûŠ«U뀴ûшúŒØO”ÞJLó-75O”X†S_«H FN°qF­ck`Y{0JÀ É‡M‡¥ÐPÌ\[¯Õº¿„V¹O37ÊÏî+ÆÓªìïÑÌ‚Iµ{¥]Øû€ €T½”šy`o7¤±— ÛÒðèKŒe¢ÐVdwŠª¼¦öÜ-Eêû”‘ˆ ;KbUÏ_ŠÓ¾ý$›£FCaGE©®GVo§}ý â&B—)ÐÎ~x¦09‚“÷Áø'«ÒÓ^vç¯Q`]¨2àrHNKÉÓžM™¢*R9ª)ð¨¥Û*Õt0š| FŒDÜdÞ‘"ïæùæ<Œˆ Œ)y-9Rí2®¾ øÅ1ök$ýqÚ{E!dRmÁRy¶+¦¡îúà—AÑ//Ž‡.TØëøùFô›­`šUH%1Õ¹°Õ™¾3ßša Û)ëEÞñoæ2;†pҦݧ›—”ö[Ïo¼ ‘¿à‡üD’Jâ +Þçuâm5ëͨS˜g’.oršåóèÒâѱZ y¸…^Ny½(Oð‘ø©¾g# ñTÞ§”³™W%M Góá›íy³ý•oýˆ¾îL¢îsn´åFÇQEÓó_ˆƒÔ˜|ç PúcAâñŸQÁ<ú=ÚzâIyÑÂ' aÎ#° tB±ÿ¤e ´’Z¥h¡zÕ£I¦»Gjâ¯Ëψ¾b>@!ò —mõ +¢ÄÝ-ƒývéq}•h9Æmè>…C¶ÖX3ÄÝÌBÃ7>"Mû½¦Èýx\‘Ȭí=ýïPNGŠ†dƒ‰`srl±Z€ò¥Iw­£¦(þáýóü®¦#LaXhÙ*Õ9‰Þ8™´M¾@…™RV®ê?U¯ëz¥‡ƒÀzg ø?nt*B¨ODø[ú8.HeO½ì|¸šn|™Eóoò+èawÕ°§ “l‡ªîàfuæ.]ux&½u(ÏCCJù5"Cÿ9øÁÜý›ó )BÏ’c# +]Æ~s€ÚyÉÚåš/7û—¨õTš——DÍýaÍèñAɲñæy4úúkÀ×,¡íð…¿}sš½FëfdòØã†Ú7,0ˆŒòÓ¤õö|î2:#ÿ\ úÔ0ŠÞ~¢(PÊ>PÀ[  ³ÑÕ!Ç`t]EO>"*Ò™͈w>"Š¶ûŽ²j@”þ[-WΪb">Ê!…«Šó:÷bA¼[E~©ÿ÷¹R‘kó½Ç„®ý dÄ°ö0ŸþäƒÕŸŽÞ³ޕa%m6Šö`AÉ¡&ÌÈ­<1¦0·ØïMV±lý"kÑøŒÈ˜:p¾Zä½;ªT1ðwrkµ÷ÅTòU •æ ¹Ç~é¾Õ³ ∆&øê½7ôuH–ÇÃZ oao>ÛÌs´Ò‘Šêé[œÄ`µà÷ƒ¹1Ibìô£ò§4=Œ«˜ê“9bTQ…[ aO±cŽ0ðŽTõŽEºðu_å›Ó”£Ñ‚¸—y€ˆK€`WYø#¢¼œ9ÄNÖ‹!þˆº¢'Šwùåu$p&E#n'28ñkªôÌøIOÕ­£ÆÞƒ^ýE–èå +F £Žh91‰MxÏŸkUÝ4<„¢7à¯Qô7Â܉°^è[ü4X‰}ÌÂ"þ¸ Š&Ý#>óÏý[ÿJA9éõI÷–us)ʼ‡Ï‹–Gü0ÚÚÅu_¯þ6Ìÿ8Ú ð-êŠ];ΡN­ò]€¡Då…^Ý |û%ªéÞÇò8+Å •|‚·H EQíT#4ûgDJ+™ôpå~è¬p”ù£K ðƒ•_QÂ=Å*Ü•KTjž ùÊùbæå╨{% ++’¿;÷óÄÐsº%³çwkð7L˜©úŒ<[ +té_¬¸È¡EΫþQ­ˆƒ }Sãcâ"€fAÐïŠàTK Ø“°‹•DKTÒø¹=‚o…»ÃŒc…WÎhµ°¾:|‰cOª:6ݾ¸_QÏù…¬9Hºj+#ªgh!vàL^ÓäJÏÉÙ÷xeAëAëyt +¯£»’m±sãʲ²Û ©>K_ì~€š’†RÏŠv)õ¬p¦@ï·¥ŽÆ·¬b8ûÆ©“jýdÀäà\±ŽF—C².‚?Hí@‘jÂÞ ÁÞu¤–ÀŒ6×g¿*šR{d(þôëÇH­à—cÔžÍôXTJIäávVÎtÊlx>â$<Û×AV^ìkh(Â7¬8Ô¢ Aš}G@à æ~Aöð'U·ßƒ¸Ä# U¼ziDܯ9ÉLûã_…æü-ñ?roÿUþí_y² sØþÇ[ÿÿHƒæÇ¿ýï~üŸÿÇ¿~±òã¿ÿÿéŸþïÿöø÷ÿÛ?üó?ÿãû/ÿïÿŸ¿ÿ_þá?ý—¿ÿýŸþëÿ÷÷ÿôýýÿôÿÓ?ÿÏÿíŸþßÿš‹~ÿÿû?þ×ü‡þÇÿø÷ûrsÏ×|ЄÿWþÿ{ÿ°'åIÆri†M¬Ñ¹oŠÅí)ž-…ß3ÉÉοW±¯gDfÿxpdˆ¶ o±h¶ræ¯ÿââ¯PßÐo˜Ìn}KNn¬´ÃvÞçGc“£¨}¹éõ£PmçˆO-¾˜("¸7":%ö¦KØ-Ò¾š}çÀFàÓÜŠlÓ%|ˆýX³Ñ¨<ôV€)ﵤ CÅ»áq>Ý“b«œ¬ª‘Xœ6ôR­ïm.4çW’úŽ™ÃR^‹ ¶€? PèÂŒ»±7$@SY±{"®¦ÓFÄÅñ&zþ ù\”} ©xw$“h~'bàôT%bÿF¡èÌȉØÔÃr„•S…ËI ¼åÂëÙº= ðµ¨TÀ¥Ò ½`ÑHkçÀ½ð·ž^þþu(†fÂn •[ÁˆWêåZÞy5£Ý/ä’à°_e¿‚câ`É€ôÀN}ßÂŒ´ÊbÚ";ãx]JäƒÈ¸O°g²h¨“š^±QAžïò"ˆ—=GÉ•Š~ê“)ã AÞWznQ7T±PQͤ:Q2˜[j:”(ÞÎd¯q{šqþ}oÆýkp( aÈY7Ø7 ú¿Î](Å‚‰ÁLTC߈Žc+ûÈ’ûÜ»GÊÖap/ír¡W€{‚~£¶ˆsÏì Iá6‡böû¯‚sE“š¯DåQrpjf+sþݳöæwè€uãëoÚ+Ðn‰/XL$6 ßG ùÍëݯo½ Ù¯AO±€½1XUOÖ Þÿ ħÞokuôÑóº~9ûßYG»¿ù+ +œÃ.?[„ÇÇÅ2·oïÖ…i\ÇO]ïà© =@÷/Äÿ;Ÿ²Q`ÁÇçeˆµ"êáÕ^Ø1¼—žg©zy+WCû{E&LR“ê[ÿº¤PDœöÑÙ&þŒ§i4¼µ•. Ø©¤µìÇeê㡱Ð>râ/Óe„éB—¯a~×ÐB@”>ìˆBñ &¥ýèèb!Óäÿ|D<î&¹¾…üo¢˜ö~šB”RË°{ÁåB&Ù;ïm‚‰K总ñÐ&À+ý#"ÃL‘ìý#j'ïúæ<¦TŒ®a{dßBÓ¤âëÀw£÷=6áZŠÛ`QxàÕ€Òßi¹ýš½Û{èA½ ZTWÜcÉ4 +J)gÆoÒ²è€ævf¾§ÐR½™$œ¹¶*ËGÍp©H£J÷ åÉ‘8pk$Wµí"mM"ðù8è‚ñœ‰¹vÀ ï>ôçaÎ`wT©N_ +QhzžÀk˜=wNb² D<å°“öyçV0a ÛóíVl‰Ñ৺‘UlO DíISl¾ÙÎ7 +D±d5oðD ˆË)úFSf²$œ”>"¼Ò¹M™¤Î7çi`J;Œ¬ýË÷6´uU.ÌS®ä)e]Ì©æ)—¹ÎýÊÒÅ<ÎS„ãj¾ƒ¢&ù’~«.vjÕ¡z¹ŠièŒü +ê tzvÖÄêÔ×æW@ùa|΂ɾÓß jžùDÐ Oeo„±œ1]«öø÷ð(?/ &(¢Z¤ÁhO‘ØÓçÞnĹÈ44#§Èbî€Xé¢tæ…h€äy JŠÒýzI~m¥ä¬ŠrápU;O[Öåø`4ùY|ù†ÿÒòûù vVä2´o‰öðNÕ:¼ð[!éÕ.v”Œ]ïôz‡ÀËó88/p(‡ J6(W"FImwH ¯ 4Í)Jè…’°7EÒ_ñµ"€ð íQ6Õ¼Ã1Uy‡Õ÷µ²£Í%0<%3 $ÒoÒÚ‡2öžô˜ù“§ŽPIa'îVØM(â÷ÜŸ^ž[¿›$Ç|÷ã<²ï‹Ý„÷kn‡ò~}(*¸SÖ«ÌX*„ƒià°uI hqÀ!¯m/¹–6ž3¼‘G‰Û‘›düÄŽ·bKxó‰Ô +­žAUvÑ •õ™n[ %ÀpO„Æëê;æühŒ¡LèÙE÷z“±§;c<{Y.·Œèjw'Q8Yí±µ'iX,–—Œ¨=v>{´ X,÷‚ª”mÓt ¯Ó?‚L–ƒ¸!€Æb—ägIÝ«# ÄŸƒ{Uþ.*¾9 áfût#¥~ÆÔgÔ +0.+ø¤=.÷›)°Ïîq"e¢š¤ªøíºwWWFR÷€ü ?ö’ûùyú\ô¯i—ïC( -4Ì?v®ör©>ŽÝJ©ùˆí©Að¹Øµ1Œ©“A&A¯Ù¢’°E-ç”ø¹R@ðнßÑu¦´Û@PéªÏa´Íïs Ÿ4¬yvÁÓ€õŸ¦Oân åÅj”] +g0ÿK¹ìß÷Áp' +Maˆ¨Ö$*Ò¹S‘dì·ûÑÁªJàÒù˜ì3âLHÏPæ~#$ÉÜÛðly´ß/´ôÇwÇnM3£7Uûˆz†"¥þ"Ü0”6žŒtp¯†Å´Lèçk¬þ‘" +ÍwerR´oÎÔÍÏÂ5µŽ%mª³À¯>#ÖQõ&ú*Fa(Ð…öЫÚçQ¾¯ìƒº_#•‹×îJ-éfóq»É¯¸ò\¨bÕu +¼ó·¸AÜpŠ~ÆÍýÐMª!4öëÇH©‚#¼½Gm€UÏëÙûÒ£æØ„Iîùƒm¥[ÕÓ)»:XDºÝRt“Nb£ëª—ü↟9)»Otéü¥îžqi¹=&Š’úéy=OVäÊô•MI½²¹JËhŠq:ià©üAׂM~kµƒX±?ùBŠè¼›1y"ꆚ(ZœDuëƒnãÒ|F/%#œãöÀ'Q zy~iÉó|iT¹`Ù½ ’qîx‡,›Ëtï6BÕüëB4wÀ&Çs ÄY­\D¾Ðf} ÜYj¡šlåT¬•µÕD’Íñºu6uU˜4±F.+ùÉŸÏ„ÞMÈÎõ³º’¤4;»¬SÒZR¦N‹õž Œ’€"| +÷B[Œÿ`ùu¤1$ÝRVæIt6šÌ‘{×îͪŽcÉ·““UØŠ6s+7­‰Q£z‹µ¬! <Œ`µæJPÚº{ïý} qMýH¢ñ%@ó\ˆJz¥iÅ…Y÷9?™çÀ%äÊ}²Ã ¢BÓò[D;ç“ÌPÁ+ yë©7tµO/nMCn³à~Qm݃§‹1ÚßÅà§Q}E*šõŤ Ëfq¬Û`fag?Ž;4›ºêþsšlÉД'ßY /p?F°¦Œ_Ó‚8‰ei/”t+T;\dö]_ãzÎì÷DÇv‘W¢læ]Õ½®¤QÖ¥¿lÀ“vbQ`ûn"â'ˆ´j±ôç…®,FRm.`–Wn³ïWäŸ#ËãrÕså^燠¿d5!ÏöëÌ4 Pºž|JIä endstream endobj 59 0 obj <>stream +¯Ç‹çŠÜ3!ÑZCxûè_#€—±ü<ˆŸ±ß Î-7`? +û©ƒÜùÑ”“äÕP{ÚaÇ¢Òc6­×>"¼æj7Jš ïÎsiAß y7ÊÝ{Ä +[TÛ)Ãt´ †Õ4ZÝÜm÷óÁŠ?5çlγO‚:?êù.(¬,(%§#àèÖLiÒê¢Ý™Çè6ÑÚe]RO?"üI• >>,™í›Ó€vÍÛ_ʶîue'r"XAþÝßDƒu÷¨ ô®?‰©Í›CïIµ1H+Ä%šÍ@QiËÒ²¢Lzh©Ã°}˜3à…®è{ïg·W7Èu§±ÔØ)ÖHíÒ ‘puäø¿‹Ðãø’ÕpžÜGýðª@Wèqó£~']dQ”oH÷(ðòb\‚~‰x[=[Ãõæ›ó|ÁòäêÅ” z• ¥Ä½®þFCjGàOyÛ`D³÷ÞT·Ï•ž3-jèÅ(9ów“óøS qKxŒ2vÄ~ËË»”h¾ƒ©&â†&rÝúº’ÝÎÏ‚DÁѸœ9=ÍMq†R¬t%ª™à…\È|•ªz~R²&Åï÷'!ÇÊàß“”ú%L)”Ùã\VÙ $2"•Z¸x\f®g½ãm?ªóµ›“ü‘+!9Q¿ûs’3‹k‹!nR¥ÍNÑA½}p,/tðÞù3ªÅbRÞ§üÁ.0™Ÿ“‰Ô½º¡VV–1Õ,Iþ¿Fx%LÒ]1ш¯ßœØ«¥Î#7¬ûuäö?#¢ÐÀˆŒGF.ôKT ø™’¯ ³7üh¢ôX·rÝÅ|tê5śDzÇmmó3âýFSD£YÂm$£ôƒ©¾c>òùÏn¯0ÇCÔiŒ·Ìùk\)¿ +$Zq&qb æ¨ò#d±p: Æ'ØÓi™ë׈<±®ðÄz¾·oÎS)¨!éØõr:YY?YO¬>Ù½Ž½ÎÛ$ɼNœƒ`_9¸sΖƒîùÙ'÷韃‘Øc:¯>¾3öÓ5À]úˆ›)§° ÆšûµÞ<V‰*J @\il÷Êò­°Tï` d°ñMÊŠÕ¾Â\¯Á¯QàÉÀ¼“œÉj HúÁvì…ÐÛí ÖýCPy._¾†ïÎÖ¾7Šå_e‹Í&‘„ñŸ“b®ƒ*·€ +Š9/b?Z/Dlƒ_ërù>ø–鈳ÊR 10j Ÿþå<°Y¢#œbëþ¿lÏÜ#m£A¨Ý»-w0üA »ô ly¦h4 é†Eüó– /Ђ{Ð>Nš)š[jûû¼RöAѱ€Þûõ²F-pÞh‚à«6hv¢j»çŒa$–Vßáu)ŸF‰ºR!ãóE¯UX,BâAqp<ç¥@†ö;rŸŸÇ»Ô‰…Úåý  ÒﶲMÛ»Û§;‚‚ç,¸k¨AA vAß®¡—GÖêôC4 +‰ Úôñoâ³Nÿ‡ïàH®T5¢lÚ¿²AÄxè +@Ò‰q¡Hÿº>Y¢Ö£†Gz¢b7áÅžJQ¶wTóÝÓ[eO€Ä'M "P¼ Šz"zº¯ ŒÀaÃÖH»Æ•D¡^°ž¯+u[²¼ýVÎýÿÜ-~hã:½Y íO†¡£§;oaØ‹ÊÞu؆ Ny»¦’ì(Cä P\/éñô(¡‰i¹í¢¡Žð~8d˜ìµ+¥Iœ?—îÁç: ¿÷`½=ÈÚÇ+¢HcàI¥t"–u¢~³#äç{LkûÊ»Ù4ªb µ£Š˜×ûŒªíN®4‹ç¹§pÜ[.“÷F l«à"ˆµ,Rï0K]Ìl°?MU¤ÜgË&vŸf'gÖséLóÞ—\×;tÎA ¾(Œï§ß9™ä++¯e[HëÉÌê¸A€v÷Ë¿§8Äøëß å›ø,Ù/ðY–ñ|ý3áæ1>ÏÁ±¾Çvn)ÌDf-F=°¨Í/Øß áÒ!Å~0Û:Õ´ûý‘F.²×£ÅŽp´ïÎY9_)"ùàD¬½f#Ó÷MÒ[\z>~~?‰ªZÊ^~þ7ýˆ>’Ü/]–_]3+¹Ró_Èà3¸™€ªkeþ|êûq…ºAthxá̳žFl’ö=âÀÐ0oºô¥ Ö:ùxw¦óv{Ç›Çàœ3ïÜ k˜6Á¼‹ø5Î +Y¶$€§»Xå0ÀÊXNåÏúº [؇ žeÃyÈc ³2LÓ‘bë\?2-ü’¶}žé7ñ‡Tö}vlß,'õ†Zðg„ƒ Að"¼>¢Ø(š³d?ú¹áynø´÷¡…hŸ~O;}$‡Ý·|ñ‡ƒ…³3Ý óø8Ø(áØ([¡¡¹ƒJK 1G¶GæÐæø˜l +uââƒþý`—ÄXé®ÄÓ=-éPOŸ§4ªžÒøìßD´;`3uE^ÄGò-ý¨î!€ +³CQ_, ý Ü÷ÅVؤ„¼Vlœ½=QË e=ºÜìT„䤫m×·ÉøÐTŽIÊFw‚Lqܲž‰ÍmADî?¯”Ų©KÔC2¹7©)=§%bИc"Ê}¡}Wážš{q¡"BO=._Ò!Àó.Ù~Þ¤ö‘ö°å}Ö2[0+η Ãîi‹ù׈',H€¥~5xÿ$Š>ß8}KNG@Ï +!ëPUx9…OÒ;êÏp;>"þp’¦{±aØ{¡BYëã<è9SX|ŠF&–i©HmÒÃ>"vo_è„?9Q.j¦Â¶T$gGkqŠàÞ´Ÿò¡ìÙô¯ñwŸ_“ +´ ™ÞøÇyPz­£ÔCÍ}JUVÈof©éRö7RÖyÜ-»¿]÷¿[>@§Çìka:äµ°ãì +b#ºcž ™Ý úG ƒ]’×A£èëB™¶OŽ"/D¢®rn˜ù†”ý­_ä€{JÀuÓ~pÎ'ÈÓýÞ +ÏÒª¿ëÃíMARQLPûÑõ“áîMz½º@^IN“"0ʯŒç<:ïFí# +e/éø\Q´i­f7[š†U‹›‹ì2èg±ÖgÈÄ~ È°k™dön¾þõ,0L¶*¨p”À𰙺!c †~_›Ï!•ð!½æÒBoOX +À` û8îÙró½ç$áú+¬lÐÒ±€DÖXžÁH(ÒWæa.ñçL…¸ßîõlY3ZQ*C;ŽJ7¦Ì±Ý-0¹Iÿt·Ïˆ¡š3mˆ=LêgTUƒWŒÛÍN;˜^.=5þ.Q6Á¸ÓܹÒGÔ£y.ÈZ[Ä`°Ä÷?0Ð;(aÅȦÓU¦kðØï!ýLDùÂ×íñ@²†ÄÇXç`ÀÖ ÿ<ƒ‰ÎÃŒ +&¸S‡ÈC;eC“(¯=óá_£hx£%tµ5êHõ +"„6÷Hpp[–eïÜKÍn²ŒœÃ’Å^¸®·–j5hT½“A(kIïAû|%üÕÝåc5‹œHT<ßFÌß^›’oFÝ¥Šþ†¸÷\ó¿Ç1;ö»Ý_PNÄÒ7ú3"Ϫ%S|fà7ç‡ü'¦x`¬Ö‘Ø¡‚rˆžÝÔìW½Jlt,è sè_£þîÜ ê(‡d—Î0‹Cö®9–©½!ç‹~fzp{îØóA3k8ÙîÅi¼¨(¬ÊÈU*3MBHÖ‡]j«J~%Ú©½0*è¤àzú]Ä»Uƒ‘G”Âî7çY‡F:iÀÚã¥âd1y Äôñ±<'¹ý5 +­I?Õ$‰Íâ6­'®rX;ÿ%•”l® + Õ ÷)/m¡mBÙÇ0&]Pjìe„@` ´©&c^jb&o¥y§ë{i±ŽÅÙÞ¦ íÿ\Í‹k¸jÒ=ÅAqŒBí$—A€ÙƒŠÈFÃQh <ܽRÈî§Å]eÏ—¿³ø‰œ‚¶€Ä]$Ë-& i çI BBŸÖDûMìÝŸþsú(-% ]Uø.~ýÛãkIªÑeÓHè©¢k{ûô…žJíím}D‘,\/ZÒäî;éÎÐu'PÜš¬ÁN‘ šD{`²vé;©Þ«Å´ žiìõhR;î°ËæqŠÅzvT/¼œ¼ƒ¿€ÿ{‚Òýÿˆû.ýOHç42@üNØŒÒhhÀ³%Û8í>QBƒÐS-Mš~h OssGÎSU9ƒÅ]Œ“¸[ô2B‚]¶x·TC§.Úg^|æºÑñÐÅ’C£DÝ‚¶1ú3Ô © rÇ悽F¾¸eÿ`ø%÷ºFXiˆ,¡›[uª}Q€o‰¡qfÓdt•Ë€2å|›KlXiñ(•2s™›šý<×Aó7[Ú?ºEX¢ºph¢äú4rL¸ˆ¨Ö]†¾>èô‹9òÔ®`îó‘øJ’¾7B¨-BŒÔAzo£'Í©ŽCA ’'}x±ð*î1 ?“U´º4tº¿d {;6<ûïÏÚöv2q±çÒv"ªî·d­ÚgÞ)+TëN) Ê Z î‘&ùÃ,ÿt19giÈ©ÔGÐH¥tÂî—¤cñÁïù“öÖ” oßÄa¿ýÖºOZåPŸã+Hz¾ÒÛït¥P0dï³ðѼä=âÅË7z—üÉÐmxÿh|^ÛÅ%ÖÉg!†<ø•gÎW5jà¬:½šcï.«Î[ÿ‡\ÚgÕ‡ cüüxƒÖÂÏæO€³©`èô8bÒ­ÔŽ·þuc&H‰ÝÂ8Ã;YÝ3µ×zÄ@KŠ®~Ûrbm)ev½§øÊzQ]ÙV@&ÚL]ÃS€Ê$jP;h#‹y˜ +þÅó\aH+b2½¡®Žìôå´ðdÑW¥œˆZÅõ ßø“.Ù-Trç>Å|=#î'i‰cÌù§}ß ³"N†ÖrXÐÇÙÝ[#ð$ô;lˆ°ï‡(á:U9±¿´÷ðÁFˆV>!EIó`ÈÙøat.iç—¸ýHp¶L‰É'wÞ2ßÜïfûŠaúæ\tuÂÂûyJDì2ï½ÙHÉMPöÜùc½RÐlÊÞ<9 f\ÂI¬îWº;IQ;I‘1)O5ùºI«XZI«ì™p•†ºB«ýó Ü› ~«øâƒùä¯òÕ„µ˜±`‹röÀ5Ä…~R0‰ûN ¶³ù™Ú›Û0:-Ä.JÙ»†c;4 +’Áñ·å`@+¶KwRCPÌ>% æoTùJ`ÔE0̓ÿ×HDJx¡,€4Ù)6¯¯u¶Q®ï2»O/¹ßœ%°È ÁÊPë% 9-¹ÎªÙ`’<×Ök3Pgå¤t(Or€ÕÍP Ô)ÂT´ù|î¸jII¾&áÿ‘C:ØÂvÓÆý}4÷HûZöj´Ì»ÏYù62±ÕN*-œÁõ„Dµ$B’8±„´/¹ÝóIÐ +j¶€›wš juë¥U5"ÖèùÑ|¸²¸`ªAÓ½´éˆ…Ævù ¡‡B“ hÙ©‡÷ˆâˆI6¢JÕãîâ÷qt­<;;O®Ï®‚Íév€Ö8Ûßn­xïï ¿þ胟EO"m킦†FLkÝ7"ì}’À?ûeüþ7ü…=ß=ŠmÙï¥ìüÒ‰lúV}0†P¥ŠbQŒ ‘ì¬i ©qû˜M=`aÏ#ç!¡‡jîiêC=µ)ýª–e 鉃]SŽ[ 3çÈyXVˆtJÄU® ¯”PàV¬ôÄßôÒæ_üê f&F Ë2…¨(N¥ +[*àˆ¨6»`O)Í[ OFFHýãú_ 4€í®v…Ä=šˆI¡y[f¶Àñ åÖ“ŒH8íj{&È·Æ‚Å·ÖmMàÓ¼•ÂâûÈö¹¿f¢˜BEÛwp÷.ÒË@cMD[ìùqêêG=<«FرdHeþ¼&q—ÈÎ\iM¢Ê¹öÜe’à§}n•CþGôD<.ÐíU‡»»·¤·ôž³ŠûBèܶN-¿)òÍ.ËÓØÔܧiP£[ž`ÊMÒN½¾zW÷8•Î²ÇðK´?…ªÇ—Ú3Ùûô‹§˜ ˆÃÇî'Êúÿ~²SÃÒ·á~ek‰h‡ß•ß@Í–p}R¡´sž(kT,žÒOl‚ &7VÂæÙ;)»¢¯–UV‘7KëkZ8)ZªZ¤ô<%!™[%¹Zûó 6JCy¹E÷ÙÉ¢l3A‘¾Ð |l@®žMaâkží5Ï&R¼vŸa“þ¢T$¨'6ÔË? ÇlÊ w°m·ª!—ÆÙþ¹XÌãUØ“{£Ï ¸SéÀå×|é=ÿ%bp¯YŒEwÇÅšM»­ ñД³ÓRãlçº|¿\wžF*0!0¾½¶ýó…¶#0bìßÂ{b4¤¶¨ŽIjBö¢ay,ñ\®mï}#å÷¸ý°dâÕË-Ö5À!(£L«ªB®˜Ç† ì—ÕæV~žV¦ úÎihˆÃ¤Od« cÔaè²[u]ñΣ9Æœ¼Š½¯‹Í{³¥€O-…û•B8?du­|EnQ€ã òc¤úçyUŒè‘ ÚmÃýÝ+IqK¥bh9°Jö±(`^çM<¯*Åóf¿<ú$¶¬Xë¤zSc±¨3¥‰€QˆbkÍ’Ÿ +Fã ¼@V<ìk8" €ìj œ ±øÛ»£]&¦Ã +îÂØìÀÉÔA|™dUˆµd¶ŸóùÂñÞÒ÷_q²ÞämÐÇ  ï>ÛIúür5fB ]?ÚV׊ݟð |ùÕä#¯ØšQt¢QÀê:çiûfGÀ &Ö ÁçµWÔÛ\Q¹ºŽÊUÖ'ùcë0jè*9°l;ãäÅÄb‰€2óvîF÷ö™_3¹ÿ„à½,hc•Šx×{è`P’”üùäæo¹¯Aá è‹&',óÔä5Ù“J!ÛÐQNh·Æ6Ú¢¼_u‚©ƒ£¿«g\uI`&ÒàH‘”¢ZI×1|ÿ\%^8¢÷Ó(¨$Šî.QRJT=Qβ! ›|¶z‚¡vðXmDg¦ÆaD{µEæ#þè¹,Ñ(ï¸!Ã+‘\qGêrF%œ™ãO~ÑuëÈ­|ÆRT¢ß§6…œšÎ~Âpó-Ä“e+€”½W¨%Þq!L¥kŒDàšü¾þÔÞ‘/ù„%¤$Avn0Nù̯‘Ùl‡£y€8ù%â~r×ü”¾@}æ°Ÿíy¸6=±®«dÔr]7© +²WÈ¥-HÝŸ«dž*½Aqv(Ã6æKÚk£!úñP\ÇD ªM'^ŠiTÖ€ÇÝ8ÜÓý@ö’]2»@ HØëºHÎèØ^Q\]è¥_!S ”Ñ55te½ÚögMŸ)‹:*IÈR aΰE{—a oë ß+¾a¡· ˆ´ö^e5ûtÑ­ÌXrR(ÝÂyy1v^/¼’.?¤b¼ÕBÚƒƒ´ž$á0‘ý”Øà/îz?½aOs1ü©/úáÔ(ГÄËm“³ +P‚5»©h ƒ ¼rœEigÓ€A C ©$ ¦‘ØYÊ‘ŸŠÄòíQ¿‡lÍg2Òz-—¢Ô ì[DÌR*}…ðT€¤Á¡g÷uþ¬–`,JVü`÷Œìäæ~0Ôh0ñ *DÈ)8*ñNTŠ÷b;R/W‰Qs.¤ï =^f]ŸQ<Ç- ¤~v¾¢NêV$ÂÝ1Ö-å)³3*\l»ÿp€Oª%D=5U +Ö ¢*³P®”ú×^P˜X{nJ¾e뇹…°‰èrɇW`{×î·¸d» 6\mÂù3Ú OæO!¼û@d³È§ƒåAG¨ÛýœþUKjP,‘ðÿ¸ßÉ`³ïŽ#f Óõ$Â’¡TÓ˜aÿ5$´fŸÙÞõ†ÄBp9Ep‚Ç0z½l£®CèGó³Ô\D¹%y\ã€`ƒ«›^0¶û àyÂÕbxЧóË,áòUv|gmE™Àsuœ|x\ªƒ`uD +…nšö¬‡ãÈ:£º¯ÄC5â_ãHCå˸o“­Ú +ý\öɃ›<ˆ‰ì-lp‰¯Í¯µfüêü€Úè„=‡@õþœÌç壚›vYè1¡ntH¹Ù/xÌ5ÒlyŽ²zX‹]Aœ;â${ÌÞ¡9Þ§êO÷JmµU·«àh`óö•—YWÔÐ$Îâ̪¼«ä¡é_“¦‚°/¦‚jTø´©]'¤îôSÈnã®{-E˜ªŽH>>½ÈŸ­#“N™ Aà\2û+Òñ¹À"j4BêªÆS¿®$â]Ÿ€È€1_qã—«ˆ’”GYsÚ}~Zçu/.4nÀ–\èÊÍ>ïæl ·ˆaˆrgP¶ÈOCí Ð§º; ÇE& &¨.ij¿º$ ´Çžùêu+Κ{?GHÞ*HŪp˜"÷]ŽôžNO;y„Oèr šëM˜5 ÜkhûÒ i²*Ò‚«(ɾ‘d­e(¶XCoP{hI‡o÷¾4jfRsµó©ÎÜÁᨢLžÙßµ(™wç6ªK«µÍ“âï¼JuG„YÐÁ ÙÍKÁÎûÑp‡u´Ý'[AXêT“Õ®“Õ*ªvŸ¾Hňº{šì6#Ê“ˆèCáwúœÊê]µ>ŽSË£‚²“‚ç®Ã粯3®£gYF2°vp¤¢µ-<~ÔžNŽ†6 £{–#UBÁëMcáÛ+ N†áî¨ + ªšù)vWYËf¦9 +P”,RìA‹!cwë¸ 3Üã\ [—bÕ«+„’´J_¨ÃÀbŽ7X‡¨MâÐ#±¥F )¹"²ÅrreÇÿû¿‰½´½*¬Ó+~í Yrsu‰Ï¢Hä+µÜ8#©õ}RëÃÅï4,ÔëÝøMEۨд(Fµpš®ƒ =€N¨€Õ1»!Fè Z‡É¬|Cq©V†N%—ã˜ÔP.æÌiDØÓ—ˆ¬B‹°³šòT8xý‚¥á,=”a¡LVNÕ‘Š}{ɾ$[5á¨Æ¹î‚þ'ƒù^fꙶáP]’èce®§*’ MáëÔZ¨Y€%|0n¢`Ú_!x +,#”Æ +iù~ =ˆ1èÀx.ôw0IÓ]ߤˆŠè¤"Ãw <’\-öQÇCCMw´ˆ®TKòS÷«¨‡ôÓw£¹wd¿‹bù#ŠJq"F"ÚõF@æ âëJ’UöoºJKç«öóJä_²Fd›‘pì(2Ü ÀÇU#oEovRýzBa‚MÀÈÍâ…ö^gTÿŠL±&»r–"óM¾ìZvG+6´©ñ£5ÂnÑÒ¥$’ +{¦_üûÙ.Å€ J 8@ÜJ’Ÿ²û8ØwØ‹+ Ý+Ë:;AÔ`l<Ýž&¢T²Ð$ù¢,Ýòu+Cpz†¾’§Q)“Ói‹¡E.¡?ÇJ`O ‰t_ç5Ç[ð§jþ*/¯-UJ]èj +—?fg’’8`døÉž‚ò8§·€iø«Ø݃5kVÛ»‡hƒãÔÅ׋)z §­ +AþÎF{¹Gÿ²~caá+a÷ºnm΢%_BoËôÀJǃ_g3‡j*š±Üµ2þ(R½ÕòùÄ +£Vh³,{ä$åt Ù Á®ÒáwƒXgMòdõ”ô̪3‚E…nWo¯Ø{’>èæþ2eWš¼ wDk|âF;µ_˜ÂYTuIIe=Cñƒ+%Åï’,=òã¤ãRü˜è˜º¡0Á§ +-Pev¹c ã~E­OÈq¿÷R·5hvÁ=X”{eÍmþ•›Y|nûKèóŸVìçO‰uØI*v°;íA…·ß½¶q´U‘g°‡®Dýíúê¼+t›ܱ݃ȓý*$ôÒìj¡‹}ežD©KC”^èÒTáóx€ÜžGÝDû1/PS6’ÙúÂ﨩Oì?ë‰@“—2þ÷JW”úK,yD_…7°—‘ÓÄu€„<›QîE¨8ò-”§ Èr̘~Ï… 7Y}åäï¹³ÇEg¹ôìO_ýëÑO—ùÖÆõк˜ì¦†§Zbœ™ˆ„}€jÊ2ìLöqÊ NZƒŠWʳ8‰Œ ö¨½¤i®3Wµ»„}6,êZκߢ…¨Iæ¼í<)‚)òóäJ¡Œ’?Ê' £ÇoÅo›å2¨(ZÑFÎ/z‚›êO-)©; ¬_3…Ø@F~”2+8HjpÒ’ÙóRrÐì¤c k)Ép.ÄæȈ*§°ï“ÍsÉê‡á0ñ ÀÀv0=RÖ;Š€Pþf{ª9Ö€p2ŽYÂÎ]z>í[nAßwÎœ1~!3£Ïи{©óÓÃ…ÍÚþБ—ýÓ„ §¬;Õ3 i;oÃÅi¢%`Ì„*Ü~isËÞû÷ç<Ào;Aúº‹ÊÚßî:ìðv¸Ù)œz«ϘrøãIÆ"^BD$GÕ“aÓkŸ£!Oˆó뺈H›®•<ëx·CJÉÜ­s†B>®,ÞË*‚N|ì%”hé„çJGž—ü¡‘X‘í©È¢ÀL¼$1°‹ñ, +B?îgÒÑu×ô;º”h7šgŪ 2Õ¬‘©’9GOÁ?UªLs¦U%n +U|*à÷ïûÉ‘ß•0 <-—5¾J|´àR`JG¨E~åÁ’uDuâ$ß5Tç#Z½ibò•UéÕAÚ*;7vQ¾‚W;Zeê³ëHu<ÜqñÔ ŒÖJý5Ú7KὟ‹;ã›yÊ«®{º…®ã6(o‰¬NáÆfUU¬ö²M÷Kï!}¸S ÓŽmX9¦Y*-ô’bõg=©—L\æ¾ÞÊŒ~Ì`ÔöÖ +“ØÐ|».æCPmœ¼bn©{tÌŸï¯ÁCo`àa{xô¯!]Fû=ô³›¬MÅ‘}ß¾µ„{§¼ëy+¸OµdŒQ|G¥wå €R6€êõ §} +‚T7×?Vø¤”n™Ð \_Òò+–ƒ öPÖcúVeReSœîx¯”JÈ(­OîÅ«#½ìà0šbÿN¼I{ª_ÈÀ yìӒ씿ý4J­¬$fD,$äÁîÝ–Ý`´9ö˜(zOwò@[zp(¿qù£ ¨«~B&è¡„ø8[Zi‰65é›­q˜7°–ðrlI»±¹­¬‡ºï×Nf#Ìa fÄÛ³™i15Ù_~PÀVöÑö“¶V„¹šÂ}><¿]hZ§ÅÝÆ*®p*’cÜoha¼ò ,bû÷§ +èà𭨧M•í®³ì?ßãÑÓï`LöÔ×EþQÑyÔý@G;Î)Çy/NÊ¿ºõ£ç±s¡¿èmÔúåô`¡Û"õ<}6Ùž_™ò ¦÷·þ»źþ¯>ч}þWüßwVðÒúîG†ƒÜº‹ ]µZV°úœ’F±ÜúKŒœ=çãqLÏH"fØ~YëÁÙg Ãþ%\é›UÌoÀ}1o¡ÌWV€âiÉÀtè±?|xÛñÛYÍiÈKã þøÅ +ÞQÕêò­ò¥Q|ÀDÝ:Õ¼zGpï Q5 +ñ&‚îŒç¨9‡²rTÊËÁç~è5ì¨bŸ”⸓(:ÖO°ú²XÈG}¤¥¨H ¶Á ;‚zQ"Èĉ¸ÞE·›ÇÑ"!æ*Ñ=÷Ÿ–@Qf2wÀŽ%' +ÇžŒˆ›ú¯ØçÛdq#›ÆÑÄXDjÆK½ê•s¢Â¥Ú§WÊ +‘àeª*8KûNMiäbØ–+QüE÷õUób*#tnpc™€ËëdÒzgSã:šƒW(¶Ýµn‚‘èšÄ$ˆÀÑ•ñK®ßON`TaèÊß+L?ëqKé™ ˆ0‡Eª ê$¼¾,uÖ13Jµ ˆ#ÈOÙg•©N„ˆ<åJV\„Ït÷Û¹!”íÌ6Ëæwí¥ç··}AÁ N1óÚ^ÞèÒ#çáÊD±*@LqYtîa{‚É êg„W::! íæò|sëŸäÝpI1‰¥W½‹\×_&Œl°" Ã÷2¿u×zÚÀ ™öYZ:4ý~íáìW®*:¼ªûHÜŸžT0h¢NS™Iáý+ítPMi/™Öf ù–‘>Ke?¦¼"~ÞOŽƒô7é–iE"` ¢•?g?“‡Yƒ ZD ;vsf›ÖšNDƽÓÈŒ77U³2ÏmôÚx¯C4Ú;š=Z®5º†¹—~ÏSÀïaý¬w‹g +äêÛÌë QúÁ{"i·“ç@Ç QxšSƒÜŸ\€`®¡ÔhŒ"Ÿéý%¿F!k;1{\³sMNê‹Wy r‡302†*FŸ²¬d}àŒŒ¨'â@‚:¼²ç›ƒ‚´†ÈØÀÝ6’þ}úoŽ3V?£бj\ƒ­Ø¯¦iÃè*1~†Å]?â½HuìŽdØýö +aœÝwÔ,Vp#ö`ðš¯lÀÿRªéxÔÕ@-w¡¼ÈÀ(É¿$(âmë¦MÕ9«6ýŠŒí5ˆôéºÍ=)ö¾“vJ·çÝsb,´Š¨aVÔ ,½v"•¬dòz{ðj†z9r@–öd„Åý Üsõs¼J:X¢ù¨¤ñÈ0®¡nî´Rh-8Œ¨0XK‰‹òó‘²aˆúŸÎÆÐ’U§sîfÊáÔ{Üp^GƒÙ‰ƒ‘€DÓ^}½Þ%üª;èþLh¨uÃ+½ÿÌË:™”ž7PR(Ãý¥|ìϦU¢Ìó×qìE>Xg¶ÐQ¨ŒÙšÐú¥­õÃ*e»‘ÝJMu, ìyî$…,›ä\VÝNÎÓä()v³‘ Ì€#‹ËÜÎ`®»¼@†G!ªÀz¡âã2ôIn”ŒÒ9ÕzÁ¾hÅàp¸RD{g#tð Wº^Àk/vƒ’öˆcL?”)µ¡(UÃdöHX šj,RÀvH$L¦F;b¶³Ž†Ët.“Æ0Ëþ2¨…«Œ?â(¤ý¾Íý½•³ñI÷˜ ê“Vq¾ [-­óA 7ªß"WdÍãï¡uÚp׳F´W”¨—¬¾}?M8çó[6u–¬ÚÏÂý7üÑã?E”¨«¦ÞëêËi²2=¯OÚˆðl uI¾(ÐZ"v€Á×–\h9ò§¶xP>6ÄݦÐÁlN=á¶Ze4¢Ø¸ÛÔ»üln°Q÷Á'Õà“ªíOp”—Wê®i× qº»…ÁTô[GÌy“03x4¬`„j?X¶ï–zã8ê=œ{‰°L‚4ÿWµdŸÉV|Q¼“!5,‰ÕK‰Ç©ý‚0¢ÂT¢P)ÁuIkûˆÈ&êI—t„Êú¥ü”J¼è™úSPDªED)'2•"Q¤gº%-j·/B„(ä-ŸrÏž9F^3ZLÐœ+Æ‘º'Âe~ØÒ\FDʇ/F‘HÕS"Úû¥¹Y¯²Î#¨CmA+§qË…’H9LYDL¯,í;¾5ûgæBÁõ¨>ôÊ—Ü™™h“©|u¿ŠDòŽ£¨£TI±¼êv°y{3¡JQ}$”ß¼TRÚ_×Ï…Õ2ìÀš¬Ey&È­âu¨d¢Π¿þÖŒi™Í}í8~D€›oêSJ<¥ˆ_£f¨}Ê_ÂåÒ!0tPø–ê™B}&ìŽu”’Ò‰a­™Ÿ8’ˆÖì½NÐç9XÍÁ/-¹—hšð³=0Š +!ýVÏ?¢à|Û{د5°úv­t™)¥^X5Àã˜húH»ÈÊÞþ¸güqÛÜÂ-6¤+ãô§¤UD<¥èìûJ»eÛê;|™Ì¶c׋O§ºŠ}aœ‹Ú€0™ž|X‹Ö+,Œhç«Ù¸¯‘¬¹`¸Z²Á{(¯ékLYýÁ¡ 5ï|›ž#ýR‘ëÝ cÎ/êYhLåÖ`ÓrC ÝP>.£bbkôÎð›,.ºÚ;¬YÖj® „S?j= êƒÙý)º¥OÃË^Lb"ˆ!Ò–ÙláØÙBO+ŽéHÕ®:2ˆóž)îtj•ETQ†FÂäeœÒ´ô¦jH m¯t¦[¦Xßnú•ð¤íqQí¢ƒ±"Î hÔ],¯ÏùdÐ\Y#D† »‚jgšñÄÍ@’½$;ìãiíZ¬ûžÝiÁHkp0¾]_YHó,l4Žu4›Îfs½㛈±³ó¯]>KÔÜQ÷Ã6riÌäq»N†šÖdÆÙþ®¹ÃÝM÷Ÿ@ ªK|]/þ$CÚ?‘õi]¡> nÈ3 OèŒK—î;U¦Þ—#na7kÅ;î\ÁÕ®+Ö;©õòÏØ¤×”Æ ¹\ÄJ,©"ïžþ‡Qá{”v˜1¥ +Æ=¼h­€±˜OÄH„txMÕu‡÷à£ÃxàVÜ!›Ü" Ü€›5#ɽrúEØWéÌõ´½ˆ¸Ë—’y*9mï†þ¸A#®mÃ׋zl?í½ >"®šÆÔzÙ~é–÷sf"s›‰#rÓ(4å×N‰ÿŸû9Ò¡áD|Ê·ã<Ãð7uã¥ñÔSÞClò²btåd5`Ý[,oîŠÎš ‹Ý´þ”Õ[ cç{Äó}ê ‡¼‚âûi#r·([Ö3³E 0OÚ¿‹`ýäÒºÙí«²z‡ÚyÜ"ëîº%ÈVfp•§pÞS8§†Ë‚u.ü–pW‰*:×·Mµo"róJI7ùf½¿ÙÏ£ù‰«s{Ò­v$Ý[×"€”•]¡Ìé‚WÔõÌÁ÷Ìz鎯%ÿždmh¬Å †v/°ýVxï#ª-é¤E®+*—x†<‹f¢´»[QSÈ(Gw=‡SÕÌ~„Ë©~¼’YÆ4ÀQD?âÞJ›¥a®æ‘XQ§ðŠâ¾OÄî¹-Œ§:OÁ°’= Tlˆà+;p[Í9ä% í"Pª¼àž +¶ìå3JmóÊŠòcQÓ[¡°µ¼Y¸~F7£³?òì#}D¹Ðæ ¨—€ú½l9+Å!aµX0У'm™Z "C•Öz^/-κ(Ïí G[]ËŸúhæø Tßm3æŒQªŠðÈ ªqBkc¶ëµÿÈh¡C½ Y¨¯³àÞØ•ú$®x`EµŠî +èè²æ‰Ô<‘¾•S¡}¼{¶\h@6S¶–¶ SǺ~š¶RMûØ944_(9“¾”Ö¾‰¨u# ¯ã±MVÿˆºòÊÒ~;w>2¨Ø iUðæÁƒÀŸï·>ÒóЊ¹dÃï(uiן—#_gæ†IîKZ³Ü@ÏZ<µÁnDEÓ»=%zýzÁ® ?ðë'5Õ2ÆCÔZ§)«Ùlέœˆ‚|Füöéâé¯Ù’‘ßìgô‡¸»¾½’E,ÂHÙ ’ÿðíÜø„ÎD²õv7Éí£Æ.ªHÏjÞ7¦Ç9Y$mÓ‹­ ò ‡ù‡ùYãB«–6óJ}8öØ^ɱ— ¹.D»×5æß=nÒG—Ÿ5­ÉiÞ(µ¢lýÔeÚö²GÉ D4Æ,z¿C ô7RÑGÄ?ïú‘ãöûFx¬S¦§-ìšgeБd…ŒYdMAŽÁ Eõ¸øt&hö'ˆ2Az#ΧT6ðsß3½=š‰k™4å~¹2GPC‚ŽüçcóŸ¯P™Žüz›T#?=ów–Æë—–:ŒÐ¥¸Ì/Zôv´@ÚÜ(ª§vpÛ`j2ÚX§„!÷‹·Þh×Öö#/œk°9#ì”5™S7E`MfD¬éàQz\QŠ®À€ÒœÙ(ý Ó³AL&ýÐ0Ë튲 ‡{Ïí¿H£’ˆ­1ØÖ&ÓÅs”Óu² -¸§)s•TVêÇLM› +´«V5à;Nœ'­I–Ó Òv ”c|¹I‘™˜Íz×îhÌ ?æÙÊ6òosw!<Äê%}ÌÇ&2ôK:]¶›¬W³öc/p[›6÷ÐÅòd‡ªHCw×#µ‹,òðóçðgôN0= =?A¢H?ö2S—“ó]£ +ÐFý&Ÿ•bÕ Ö£½ùUϹF§HîKM§šæ›¤Ò)¥Žš>lñCäòÜ0²áåLJÆWÅg~ós­:›kŨ0 Zå/6ôT G¿xwf4SçÄãÎlrÞ†qÖ¼²u…7š [cüë&ÿ-¯]î±­ +˜ o{-#¶"Sâföö`XÙh²Âßx¼uWkô?š •ä]5@M‹¨6݉y‘s;Ê ‰¼Ñ>Ëët[Öi•™’”à[ÚÜ|&ñÂTŽàÁô 0fĆÓû†|1ïIÙÇÏñ¥ÙÂ÷SV??F&ô‹P+ûŒð)ö<Åæò$äžq¢¹îÝçÙSEeÒ¢J˜¤'zn¸Ä`®Õå³_¬(Z,-ÐøŒòŠLS2Ë@dÒž‹£pÁ™ïš¤6ô?7¬nÖ:¡*׆s_«B¥»D›öMû#¢öØÝ(<³F‘¬ßˆ\DbSkWJÚMɬ{‹îžÿ*S€—ùx` 6UG …c>Òj}½û½½¶æʀ׈°-SP? öåˆíN oÿNIîÜË®v Â¯êcûÚÿõhœ’®1²¨»q=HS¹€w¡Û¡|ƒ>¶ˆ'“gÔT»–¾^ß…zÌMgçЪ /Í…@$Ý5.U¬·ùMÄ,Zg¿ï¡2MqÌi„%E•¸N­NÁcMöº%Ö—NÓç= 3ȹËÂUúg”Â>Ôö±ÙUXüfHs«Ö/<Á[Ì›¨6ÝüCÓªaGÈp³ló5t0"˜Ÿ¯Dö.uyðÄûÈ +zt"ot†8üàHð2 )Þl:¶7¢O¦…HÔÌÆí¨ JøÈâÃý"~A"´®ì×̤OÝ{‰`éKÄK‹ j7ÜŽ1”n4ÃÑ”F)MuChýå8öèJÖmà,¦z}M…\ÔdžKú»œ ÕÐMF›"‘n;°Âr@Ù€9ïîW¿Ù•Ô;0^Ü’©T²fõ2wW|¤+. +Ñf'ëÄ^…{W“Šjý<6¬C«¨‚îù‘ÅB„u9ïû›ýÀ ½±:¨jÛµŽD(¬°D w*‚!¦¬žš‘‹¼{J|§&äOg0·sHÇÚ=N ß|]æµA ÍK5‘ac(ä&ûq¸:]Û‚žý„¶5ùÝOu`hfi„Íɶ£¢¢G¸Ž¶éü‡°§²íŽˆ4}]¯À Ê—å— ×7ûi[i-éŠÂõ¢z/+“–Ø7õÄsq?•(ÐýCáúaI¤mÉ)ÈÓŠKϘ÷B+ÊpI ú¤ÚPÕb‹ñø‘Ì¡¯9Q£Ø·£„! Üu7œÈNÎNmRñÐtðGŽ£L÷Ò“B+C—¢¼Ž£ƒNX¶†Öù9´V~NG26CÞ’ôýSjAf›²Ãõòçz‹/EAi„ç«D£wxb$´LóL¹}ë<…/œ©¬œw¹èçôÑh”(µ_”‹~ŠjèY¬ÝeZ.«âAÎ2C3ЪUP™…¯™Ycó¥Âx…éÇH J„ ”q²`…îüRB¹¤e9«¾5²”F*Ÿ³ljÉïQkÜŠÚ7Exö+6{WÜFíÁQ=èJXX3ö6ëvÎíbз‹Áã6߶ ½½SFÝ%’Ì9±GEm"(à‡rÚCÖj÷kÔÑïGøB› n‚þÊá®otŽ¨Ã$püUîø¶ÝøY€vʇg Eºµ¬½X +j Ê«lüº•9È‚o4»¤»!û¡lÙ4æÑÁ¤£ªúm(Ìâ9&Àê~míøÐÁ"Ü8«š[m™ôʲ™r‹pð…Åh¤Úì?*ê€#¿·‹ 5M±¤_9“ ²5U¢Ýèt\i‡Ì¹× ­ÎCïks›ô­XÌ_dÛkÜ5¢ÝF´úTÖÈê/)Y=¢K×>ZÙ—{ïK»®?)¯}­T­¬¥.« kâT^š¿c4Ä®DÀX¬ÉG"De”™=M€õ€W2ƒB®J‘³ZnÖò-‘²n!B!÷§ìU…«…Ÿ~ÙW4c…WHÔïéLUú)h㪨Ϣ¢<5˜'ºRDc%…]ÝÎ@ŽTqä7ÊJwîÚêJÈ)ì»ġȼÒê5 ¶­ÐxY\Oc]ŠÊ…*4S§O×P  :kïõ,¨ÿv”÷/”TîàÚsuuOsÓ¨#ŸvY4CHT ,¬°0äïê PÏ bÊkë—§~ D„Ñi3]¯ý;#D¦à«{}()Oôœ'V9D¬Õ] v†uÊÔ´*gÀ¬·u›HCÆÞý{ŒP®ØYk@<L»*F´L¡mݶ ù°aþQ0øÄ‘a­ßFçówöZ¬éí¬ª¾~£Qšsº ÆT‹‚˜^™ùø>ó,/€ô²wÿäýp øÎÚ ºê¬õŽ².†ñ,B"€vAŸ<wÐfÍû¯î$ ë¢W.qƃ‚Pc 6kpŠ¥!€^Å÷×—ô¸¶º_ÄÉl_Þµ3Õ>ËÏi@uJŽW”´SýEÅ3ØëÖÙ}DL€+ZÛGèDA’kßòa›)ëFw7d¢D¬ß¦ø©Öô¤6'¢Ã9ë(•µoD½s¤.r[Üq·™F²v83•;´)Ó„@Â,©¦ñg„¸ +}Ýêóy&QWä½&m‘ÎÍ«"çoÓÔo€3¯"ØšQ0B,WÓUì&ÇSwá=Â#!µ‡ýÄú|xÖßìG£ –”¦qm:õ_o˜Q~ñÝ{ü¨tÝ˸3Ù\ž8Èüàû×Í÷uX—ÇÀòÊ X(`´Ì2ak€X£ma¹¨oqëj ’Ý#ÜIvJñŸ½ ›‡2$J¥˜Ö¯›†E”ӔݖupzG–;Ôù|&–-9æÆKGòœÆ8Ó» 'ˆ'4ʱ¯ñmjíH„aº†ÅˆÞD4lÄ©F”IßjœEdŒZïhs7Ô&ˆ85×S¼àÞÉÓHòô/'aO6³ q:kðcª“Ò¼hs…#ƒM/ªµU°À, cÉZXùÿ8ëÞbå‚/ßäÔââME'踇ˆ±QUlë¹õº¿È9ÌôŒZW€Kƒ™|ws‡%<#òJT¯#ûWj¹²‘á'Œa_¼·gx¡çÃf¡n]nm܈蔗ÿGÊ[ €ìO¬Ó†A%Û=%?@W¦ÂM,kQÉŸåô›ö7˾7tíÕ ÃÎL¾=<µ$H +·-·•¯Ìk}Ÿ±ö=MíÏô4[sõùÍv3³i׈2òž÷>¢ˆ×-"qµâˆúïµÒž¿R¤%ôç[Ù§Óéò‰º.lo¹ž8°2;W‰ß÷‚c=-¹õ^­Ü.Ó-ΣCÉü È¢/{ßVUSôëÅVô5@¦¸„îvo otûX˜œéÍÚÊó~Æ€f#êÖ„ 8³îŠD±Ûíp©s îxÿ»¥ƒPí#5yn¯ÃX"l‡ma£â~Ø* U÷ÓäƾŒŒ³kº·RÚfÄ)è wé ÙÞkϨû…,&¹)sÆšq(¡j¡Ý*ZL4€¸Å(,¢ö²ÆÔoîmwÉ„ó¤jQ÷ˆ†ñ­¾Õí £ŒÄEWR +¼tù(ðB ° +¢m䌋èŠ÷ˆŒVgØ¿çJÑ…|ì‡â‰Å¼µî*ìó pü&mZæSò ± ßDÁƣݷúÊ›T½@Lφ¯Ñ6ÚnqwÖR+‹üUj¶ð®5l"²~½÷+:—n~¾_€™+¹˜—œ¤P”ñ¿{QBî ×åÚ +L²z÷q@pƒèF F2Áõôð˜äÖe WM¥¯R®»Ó“LŽºÜˆÖ Ÿ•Óº ÀÓÛ$ý>b’^Á&ÊÒEE¾ì®–óñÉ𵎄'ë©aWˆ#8ûš4h‘C† …Æ‚=Ö\òšàæn‡\™Ÿ£ôs«rPk5©‹åqhüÍö~?Žpô¤.âÃ̲ßn÷šeBèqH¡ã׶Ì8dz-CòrÀŒ¹FlÞ:#Õôð5l98ìâõSø3S;ÔÆV¶tæã£ÿ¬›ŠõŽ¨7VZדÜDÕM­@~üù$èY€h¤¨Èz O½EaêX¬Fœ&ÙŠ@úê—¨…ˆoNÌ‹¤Ÿ£…È¥‹vŽQËuK(Úƒ¶÷pJìZ™+Ëž9«¨D°î&BÏõ•®] +D[5ý:Îv£…¤F[;ršÝѶI¼ÍtÕs@Ys!’ËâqÑ÷íØô8ô#ŸSùµ°ÌÜ5´®ãåÅ;"l§ádæ ò!ì¹Î׃øy#ZN‡ÜH¬±¶º¤¢ëUZÅôŠ‡·P(Ž_4S!Æo©]R Wzúüf?­(\Äb·Þ3ðÍ¡ $ÛšìÜ1Éí-žóœèÂV“¨u>hàï+K½ÛÖWdiî…aE3­h7S_¬'A©)Ë ~P?;¿‰HO•¦t±ëžê_¿ïÕŸ?*g3Ö7(É€d¾¡^|FüŒ&^¯Ÿbôh¾ãÑŒ7ML•µŸF¬êˆž xí‹«à¿íg qɳˆ,ªoMÖ®d.ßìg­œ %€FÐãgÔD^Ë‹ò]à"¯RQ¿}¤÷(aZÌ † :§­¸ØCÁ¸µZW?^ŸëbûìüŒxÞ·a!…rË&ãþ¼Ÿ—ŠÁò?’¶7£¾õ|$Kˆ•áÌ°Oó˜× E,4ªHÑcã…(Ù0ãq(Sy+¡¶*=þõ«É8Mï‹7‚š´°úúÚ%ƒ‡J”…;êSr‹TÅ¡(°^ýõwoâjŠW •)núû@«ùž¦uYjŸy8oiÛç~`æ';Qß잊*/¶%ÁT¿ø´ýll×G‹CP¶SÝUOwîÓ…ž4Jô(.”z)hÿsoÁ9¨ƒôñÛ‚³û¢1Ì7AB¢NQP7«FuO%ðC¢§ŠH³<(¥­[ñaq +bÉJ}Ý pPS¤½Ãº â*… ÈÔ¿‰P!÷T!wÞýðE¯výº”4jq Ô ÐˆcV3•õòaralÕñ<åÒ +´ikÄÉOòì+ƈ§>ˆð #=®DN_±÷Ö‡ÅT Éikh%*PP'” ÝÒ7·§•[É#tP€&&Âåt áBã%@ÄKG¶ÞáÕ®/%Â$«Ù¶i +[Ñ_³äaß…y&fؘʹo"î.)á´|_ÝŸ¢BŠ¶ ²BL kÄ’ÓŒX%Ð XûuF¨–1Dƒˆßî\IÑñÓuHýf?'ä)¨¨_t Õˆ\+êêd÷ðw{A~Ú)Ë)K¡ZÌ›®ë· ô7èçú<''V/ý'Þ#^£ ¤ˆÓ§*oïûAæbúbøë‘7?´ë« ‹SøYïŒ[<ÜûÜ·»åv?¾áD5ˆ›<êò蜡»ÊÍ…h‰ Y©~æ‡ÀcÝG2±¹nšFhü7IØ®¯#eÄÞ­7õÒà3¬¨£ì3FéŸÉæV„Sš$ ëHÝ ¤¥{qz@\Ÿ´„ÕõÌ x|†ÈJ!eJ–÷žŒ¤f7¦x8äf?x¡sWZ0Á9Î[ +¼½EÀØ{€£À½íè&$¯¬H¬—‡žöG@[ê¾è¥ÂßÛ^zXÆ@{×õ÷@½C¤è°ô#6Ü¡”À„y–ÊXIË;åÞQ#  nÍšV 7úmlt}`DÉ.zÈòÊ)=)"óá-µx€03]M•’ƒŒœÞE²£Ð­OÍ’³iË• U¡Éyh-ñôÓÜв*xó›ýÔÓà ° "8W¸òÖFð} ¿šî#}DÝqM\ƒ‡}aX€úo¨‰8aKIÜ;‰’פˆ>š*§Ü´7lÚ©\|µ¾7–0%Öa­ÑÁ.e‹”Ë𢑛¶ë¬¯C# +Ák <[ È Þ ‚zÛräÇ]QNñTÈ`³)“¼"b,BY°æTpÎñé@h1‚R +OL)ªŽwÛtZ´O/ú óA|ZïCÑDw4ƒ®ä·W=—ñ±±©:S…{ãC©üÌ·i­îÙ·qÀLkŸ9ÀLzxBQ™ßîgåÜ„×:eÏPÍÏ•ÂNÙƒ›á9“‘Ýåy¨+Ï>v nEïvoé¶÷¨¿Ùçcg± ™™è31€îÖªr.e:æ ô£ZwÃ/¶}FtñZŒ[kbð#ªÌè8’{"m³^çÍò@ÍZµŠ7ÕÚ*†ý@Äw‚ùÿ^×–Z¡úƒµô,ßì‡â­X¤j=}Fê15ä]ø˜šS±~ZãXh kœ°•‹8°Y Ý€"«9ˆöDZ K†”¢d³#ðjºpšÔ5‚∮T@@ ô -\!*`#ƒ/=^}?Ö˜0˜ó\ŸŸ¨c¶ô4b‰JÁAAuŒ× åÖWšŽ<Ÿf»TT }®u‚B´pn pÑf ¨‚"Mßí—v  ÑÆ7BUU +ÐIåÇz ·¼ÿ9]š™:FQã› £vkod?Ï(€Åw]øöø&£‡®^ý:µ=üFј‹¸Öåêx‘e@3ƒ6H¡ŽÅmfEþpô­h«W‰þ1kÕSv÷.v×'ðÀXçºH¿oõ "¯¢âdgmÝ~üYÌß”çÿ Ò»$ô_²ÍÏmÖA:•&êp+±Ùx»û”"J Œûñ6ì¤7=móI+MaÛ¦w+]FÈG¨H(ÔìÃ(}Ïò(4à'®…>ò 3ZÐ:•Á£ƒ„á$ªÊ®+`¼ =ÜÑ´M-/kÍzÞY©V% WîñÚÚ´Žb”hÓ°ˆ¼~|hçÁú”~ú£ +•Û<ƒßPR–[LI³‚ýÅþ)·Õ„=%mPÅèìCX"ÐàñL¼@¤”«L3:9}˜Këô™Áöþ‡(|Îö8a=Þ˜ë¬%ñ„EG#yö‘Õ¼éAõ¨’YasîÔ<à¦@™·”å Ä|s†ÁØN« ¨šÑá.—½í#ó"•ôö±Ä¥'¿:k"]í^ßj³ÊÎTßí••¸1mŒ`°§ÒOþ]ÐØÚÊ 1&ÀÊï6D¯?Á‚þeS$ö+ckˆRƒ6k-VUørƒŠýÀ ÉpŽ*ÕÊ ´ùÓªOÊfÔ±7jßFݤèe…i=µbk;búóC% `*É©9¿#tGúÌ^,ÕH·hè€rŒ'ܵ=ᎾUK¦x‹¨:A +T"’@îµ3JÐhhI¸ZÕ> ŠÈ2(””I4”3%÷±EOìXPÇ6Q…Âå)®Uœh<÷A`x®àŽÔÿʈZS +Ÿ³©±¡ÐMECG9#ÕRž Ð¬`g9^/;å(Ý_¹#|ùOë©iP£°5üzOfœÞaÝȪf«êÕ_#re^«†ð^<¼ŒÄhÇЗÀ¦÷¬š¹8ÿ Ñ¨©Âdñ”ˆBdÆ0¶1‚Z±eä«ln†Ã;Þ3GÂM`L¥þ¿` ‚t×Q¯b” cnI@"P$@jÁ "t–X]HŠÀ[U¢pN5š× ‚º‚îÈvz¨Þ‡r›°ghVˆ*ì¼<Ǿ¢µrîr@m»`9†Nàh-à HFÍ“æ´$`EáÊ2¸Nܱ+`Öº%‚üÉ~>„Àw£t~tÙ®Îð0 *ÂFH„^ Á¶©”‡zs#òB×.æC§°ÉtI¸œr•=,‹À§Ü›VynZ%/kyJö£özlNÃà©PÔ~EmÄýEhÇÌÐrÙëW-ß ‹²‘û¡ç +•\M~¹ÅûD[ãˈ!#²—^©Çkçmo›®™·>€LE§òvè«¡ ¡Zø¨õ\i‰-I –¹$5ÇYRÏ ¯é`ùZ¬Ñ7©O›qZNý©ÙgHúÄljúä’·oÇ+”´˜Íÿl¢öä|ßJ:ç` ùÿ)é´YíSEôÒC™ú€Ä£MðPrÃ^ôòA|ŠgÕçk¿ÙE랧 æ®ã¶„­[Þê¦G°’||gÛÀNz\8)~Ý`&»?¨Ë+|ïÜâÐ7b”#Mtàz®ÇFO«(L³æü:P¦¿¾¾¦¾tñƒË^u¬Ø3¹]CÆ[²JŽÙÜaéDR\ÛúI"gûx^ÁU‡’bn£©*DµF¢”[µ4—@g¢R1¶úniËÝ4X²h;Ó_wVeZß­ÖJÑ;a–ß‚¨-V òÉ“jX½‘ ¯t´OæURó¹Ùÿmí¶ÄT‘WÔ¨.ÞlDm(¾*ayzÇHÌî õðº#çfŒ­ð3&§ÑK„8Aøwsj—Ú9ë+€¾¢»§gÊ ç–Uó`eý¤Îè0 í',dË)·K‡§Ö7ÓE9×B—5æQ"#ÄÞKù³ŸÆ¯ÿ‚_#E'°îwW\uÃaPùšBêïwΔ¦¬=ªfÜ‚né ±ˆ°kÑÐ(°D/uä ˆ™M!©Ù½ë3rÝо_ôÓÔŠ +üþfTáþ]TE­¾A¹jžÊ„š¡ì7IÆŠXù@É©@j˜›±jWPùfëßõ!ºÞƒö_‰5›˜'öuË.]0èt¶ûöÁ¶ÐD­gÚ¤kÓ ‡l—EÁ«‹©ê–=·ˆ2©>éj)ÙJÆš­'$“!EgñÛ¡5ŽAc ¥ïÖ¯$L‘-#¢ +²X W´3‰­Å¼VVò9ÀI½'LÙ¯ÇH•¦bs†Êß^6¥h½Ì¯ã°l#ˆâQö²Ñƒ¤IȯŽÇBªÄBª9Už×/i4'‚ñ’ˆ§Q…8`0$»"¿ƒ6ÈyÚµëõgêõæ W\¨×^Z1ÐÏíÿµÝ,©ú½ÚSýÞͶF¼®‹¶÷) 5=ËËr’n§EÛ +L\Î.í±ªEÉ~<¾]tYÄÝY‰íqêyw<&Ž£\P¢Û¹võî²Õ2N)–ÑKºYߺ®tv ïŽòiÝÍ.Â9)½-GÂÑ aF¹þdýYçvΊÎÆLÜš€xʽW®l•†=M +•*»ø$?$ÍÊ&bªçí­± zj¾#ÔWò2(OHKQvªòçWóó¾Un2b6Ü@:«ÍÏxÇJ¶Øܲuš÷Þ¬i¥’2MÅ¿gï‡,Ö0z­Õ m¬…ñ3óŠÀÎÓ?Ú>*Š£¶}Yä(ý3VÑW©¦ó0ðAË¥ø?ŠkÝÒï´]b;‘3¢8PFÔà +uúŸó–Z^l¶3|zôÛêÊ9ëÌŒhˆ±î¯êØh*fqº;D_ [ùÚ¯ü¿J*ÓãÎ,Æ’N0<^Q%_N>‰#]µXÎÈä²»uÖÁpoç€V ƒúHÙÁf ½ÇŸ‹¦#Ù…¶S}Çõ1Rd_cŸæ¤‡iXÕiO¥þ™VsKqÂgÙ˜ÉK•õõsüm¢d¶ÕŽÎŽÞ-mï<¶óØž2is®Ó Çž†Y—R!äRÉ ¹T­±ìïEZvòýiÝÖ"‚‹£|(…Ž¼àZ^¬£o$þ“æô&’Jå… Uö-OmÛxÞ/n•–ÿ(õ&W·€õÆ^G®cHd$;^Ëü Ú®HÁÊi”;×qÄòŸ2t5û#ªfÅšn3ØDrÁÝØ5wYÖ,7)«z:ÐÈS,.SO7Kü?¯ Ï¢DË“ü1/©ä ð.¿ª€úÀ\1~×±Å`O«–-ûÅê ÷ªÇ«†“o‘\™íeÄkW&¿f•GdÊà)Ž¾×\ÏŸMmþ’ËšŠ‰ ‰îºc0|—ºÖÞ×Z˜¥¹‰T¡€*\*‚ãgÝIž´æ%×ÞÎ;´%V ë™ +f‚Ô¸àÐêqnþQ *ë·ûHkJ‡JJ¢ÀØZ“£ÓYËÖj4R¹6»ZÝÀÛ©8hŽGHýÚªÖH†>’!¾ +x®Ç&åöÚg¼¢æ™#™\s‰Öö8eÉ‹„êÀ dq[ãR,5ý}œS[Y|ÚG$%ÐNi*ËS¯ûAí5)êŸ +ññe«u¬Ý)B²Yßd³‹ƒz UÔ<|>D¼‘ëÁ­þÆNy(¢¬×¤©BÔ4A*á—2’ªCÚ—yÚÊêyµ’~P‹ÜfÑDR„Š>+®°çïs‹&À³ýÂnŽ’|“Uÿ:ÎÔÔÁfáó!%xõönšc$Ï÷öѳt™‰ˆ2’%©DhÕA†ô%¨#Ù¾—2 Ô©²lƒCÊ7~Þ©+Q¼VîÛV<J?¯Ó*8éa€À§‰ð¼>¢ÑÊâå»ÑrÒ„¬HÏÅm_µ¹¼­Q5;Ù´‚jüP +8Úˆ“<:ìPx’àXI¿ €Ù¥FÆZ@7ºC.©³ƒ¯ÇZ|·áçBè¾ôÝxDJ(¨²‚ g„ h±¨ˆÛ +Ž~èØ0X”ü‚'Lœ/JÝÑž\ŸšxË·¦ÿ–žöÝ@jó pž> +kc´ö¡_¹þêM8olÃH¥ç³ºB^_”’kíÞLéÉ·Â>ö;1}¹Š`‘rƽU}W¾›G¬îú*5àGÐnÏZ€— ú"³mNvC•÷2¯¸=½#'x½‹+ù?EU7Ù=ëE.ÛYd5¥Öì65‡ï¶KIŸfîQcQT ×~¾ P¶ \›ÖTXK½$Zãu€Ì•UÃÈ)àÑÖ+6\ÅuüàÀ³˜jYð‹i#Gô[@M©€Œ‡†Ä‹³ÔHEIfl¶?¢¸%· UÝç&Ü‹SÒ{0_G¬³¤8erF}ëØÆu;|5ójêEŠ%£ʯ›¾z¸‰1Œ¯sÌÇŒAN™Ûñ šP< öþ¯4%NŠÍ»´ds —nX•5=ñ2·¶Ç£B\íÞ"b n34¦Ž?ãëz{Vr +Òz2ãŠO¸,~Y/§p*0¾R"+ÈF¾Õ¤EKŒéjf…s NˆÏ9_Gþ¢šEG@(ògüò¸Cbk€SÕÈl ¢Ã'BÀ…çž=sÇ äËî×z}¢ß}£²^âËäH÷²ê™.Žˆt[#YK˜ØŸ³ªU{>ZÜ-Ef—o3íù¸ìðö('=´Ygö8l©a ­hŒ‘•#—û +ÊžÀŒ¶Ýáaöxò¯c7TQTÕÛÅR + NJð”¯Éz «ÜÙ¾‡ù륀׋nÄ:ų®]×<\{^ÙcÏ£¨/Ô-5åà¶KS‹ûñþR²)Š^Ôm–vâPHÕì*‡a’‚DÔSfÒ@;zEmM”Vªçwõ®aϹ¨ô„WÖ½HK×ð)ñ›Þ| ¦3»¿D;›µmª„6Ù÷ô|;!·Jw3À¡nÃŽë#šê¨¢¡©ŽÒ+ÙK4G(ríùIíˆ~ë°sšÄƒLBE[}@ +ÁêØ"Ǩå!þÊQ÷È"ë¢,ÌXG÷²ûñXü 8ß„c¯´ç Æjû"Ru¿½Œo±ûÎú×æ]3Fò=1Öbõ„Û¼Ý^™#2NÉ•û§Ëvœ/¦³˜‚]»9Ê^3Þ!T3†n´ ÙEõÅ&뺾õ©® l<ÔÎrddi”Ø@2Óõ´¯#‘t1Òhœy• Q§Ã4:7ÚŠ²Åöˆ47‹žSÙÔÖ•Š&bäd1HzŽCÿ™’P¿z@cÇ•ÝyÌÈë¹j¢«Ð à¡*£Jd€òî¨^Ëf(+j  ²t -UX$AyæÚïg‡Ë7b i9pÁŠæ*Ë?…Иž¡ÂT´M튎鋅älj‰Òâ8$2.@‘Q.½7ï¿ b„âˆq±Ø¨Þ¥¸M?466šØUaJ~—Ç–ý¤¾ózŸbžÐç@‰“S&øQç³{;ë9=~—UOVg(ÖÜ Gá; +>^­ì¬ÙÑŒJ@’­´!ÿ…tw§ŠëLõâÚ1I‡Ð¶ÐÂãöÜ +´õøð Ø¹nÑ›ñ+jñ냥 Èj®EïKP)4~‡e4؈íéFÔë6QBþzWÙ¨7Kw#É‚¿rÝŠŠè"ü3Îü²@þŒ:Âúá|ÖFŠ¦¨ƒ¶64¯›â\Ø0µ€âì–«%#çt¼ëÙºhÓ‹ÅY ÿ´ÆH#ë ýdœD)/CYm¨Ž·I`, xRwoMúP¿ug³cÌjÏgÝ#‡¾"h+$Y]"ÈôŸ#Û?ADÑ_@5^ñ€½r*b¿m[&]¨¢!NŽs>ÔT‰'ÜX-*'Œ_‘y¿Çý› à¤@…SºÉµk[€fÒ8•tþ,ú +?‘®)#½¼#þ#·c%QÚšw‹ãšàÇt<ÊØ `Sc©vä-Öo!>±”zÇ6×4ÚwºFêù‡ë|ŽúgO5nûÊe©Í™¯šˆ¢?Ô87×{X–A€éÑ*5°]$bõ§¸j™AÒÎÐRÂ^qá•1˜´È}1ã·v¨Œ,CR¾°.Å´#pµ¢¢ŽiS>M$êHÊ6e"gÕvç2©¥‰3kÔÒ•)±ŸÎ½Ùï¤Ug>Ã*_€Z•ÙãÁ¶W…~ÓsÑ0|®Ç£“Àå¬Î>(_K±þr¸¶¬p¹ýÊZÙÏ[ð‘¡KxXaù–¾,8¨Ìlßî?LÿõÎÑ^x” Ew—s½GˆÞŠDô䪔+ H „&ãY{ï5;\kå‘ât×5*¹ Û†& &Ù¯cBùϲHI\ùëê¥Õt©!1@2øëH²æÎq§Ûé 凙 I1Ö õÍáGf×^Vt(¨¨ƒÓT¾”¤<ô±V#@üQ‘§S«ì°*ønìÕ0Ôg±©÷üƒDnIW`àΊRø«zdËôº¡5G³O„Ï*Ñ3Ö!OÐæâ¥Õã ÉÖw—"ã0ƒ79³å\ŽŠÞ¥9x"…ÅYn©F$Ä…èÔûti (÷…¯Á£€ºÕ —Ö +¦]÷†#§å ëàH†‘ÝdEÙQÿÛ +¾€ÐæU Ô›6ŒT綻 –ÙWÖ®L+TÒ·ì¦t5T`OϴǘhYiΨ#Ž¦5 ¸(ƒœ]c<±ïmC.úqêo”…o+GÛòÛ +O®Km 8)Ûy9÷½ëêôëÚ›ˆuh]`{ò;ùÍ(E³ˆª¸UÛ­Ö}àÈ ç – éÒqE Ývˆ¦ö&' +* ÔþE®`ªt5/ö„OŘPÓýv;ÖHSï(™¤ZgYs›žA5-¹JR]ê÷ýhºnâA¡Áí Ž*|nc=/5„©’û _AÒƒìîjûnV0êD]và›çæëH"@w¼;‡ù–@y›˜zòwFyèU]-Å’…M\»†ô3–ÈZ0 l0ßdu/‘D:›ú’AvTà¿£O*gDÐÇ'‚É5†Üssû/Dš˜s_lÈ› -—ÐecòäY¢Þí¢¯0Ä—pô·ºÆ¼„ëöÈd°¹­ú\wê$ë!­\õÁù‘.£[qþKöBóá:•Xmµ˜qŧ²Ñ'êö3óù\€Á‚5Œ÷‘æØ*ëëT§\ÿ¾jk*5VÜÄ'kÙ<ù÷s}¶äa %êÉ@¦÷—>íH UûíÑ3TqñGw-y¨:qè +Õ¦%wÔs:f%B/$ˆGÚŽ âh‰n%j+¡S„?"Á–ó¤o>Éî27¼;šfe€læk¸Ôce¨sŠ“ºä≣×]z]¬~š×òñQð¶Qd¬®©07îá­§Ãôß®¸Rðø›Ê…¬È„€ì ,H5-®X©â\Þ@&©ù>·ÁÄo5Bm×’@ƒCÞý¢ß&D`˜ëƒâÚž‘—ðÔŽ(Ra]¹å%D›ü«TÑcJy±ÖºøoþŸ°Ü¬Ìј»Ü†þˆC_Ä^A¼Iä?¶ŠÉ%S(öa¿Tü€ðÐ*øØÀE5pëBÝU·éTaÊ‘\¡Ê¥>O+Ö-+&c¾ÐGöŠ åqm¡v4vÓÑåñ`1’…ƒýmeC3Z”n f½NB@òzáQôÐ+pý_ï­Ó1XQ€›¬•d¥K’È@sRŽDE¹Ô(¶fÕv‚dºþ„üµg–˜—_HöÂ|£Nó€•h—é€ +U~ªŠ-ïÃÕšÌÖu½Å¬¥Ö'kWecë!ÔOn½±ñô»ŽÃ~×TIô1/ªÆŽP0„Ëþ?öBVÊaE¿†ÝvD½æäÀ5öR(깡nY³RN9øÀ +ƒ&ƒ–ùˆÌÎø‘¬A⼶h;ÝzËQcDY«Ewoí§¥Zr¨Ñwªþã´‡¿‚v$Ä4i t<Ì|$…ë,Z-[=žvV9aÆï;à ,•ê¥ý…T/x/½§n¥;¨ÁÜ^Î6̃Ǘ NÜ¢Ñ:Æ@½%öj”’³»JFM?Ôý8ÏУNâÏÂðþ’ ¿ÿ÷OúÜÿ+wôa Ÿÿ/þÿwVîâœê­î2š}[DY¬ÏšuÌïwP†K ?”¾û9È›ã(¼nΙŠ:iü}Þû¦1i àꢢü³ZÓ˜ &9œÒÕ%>þyËP`DѲ+ªÔöàÐ3s½7GéæËwE­§¨«j£hSUõ—!¢iê׫rßt†°™2R‰¼¶©û8²Eá(”‡R/¯³2¦tTö¢ìja¤¸¡W`êÐ… h¥¨&Œû™ʨÑzל†²o"Žg¦mÛœ‘–åE»åHºymÄ¢Q~c€é.Ÿù´ãú'¿à€<Ò\öÞ‚s€¶@ó©þ·ˆâá¾&À-ê¬æ˜šTEÂHá<ïO—)YS©F­5Žƒ(-/,“À$ÔM­' ;xÙ+1»\ÁXe XÂ${ s|j7×rû¶:‹G¨f¯ÉŠ¬ž{ç¢ëüßv ¡~ëhž ÚšôŒÞu²U&ËcsöúÔ@eå½:f&ð<›kç9žÉž/Ðoš…æùØZu—Õ9•Qd„Áßø¥ÃŽï=PŽõàˆÎf,[Z¹ðb.brøЬv–©Ò¬2Q_œ‚. ×g„YBI2Nã]ÈÂÇn¬rBb<ƒYü¡ýÖ‰ùùü ˜ÜA?Ì“e`ó´ñ, 0|¡‡]0”æÔåQ„e zw5G²!gðD2èåëWÈ£ƒ£_ãU›[‰¿éí¥…•IÝ¡;ö¹’á÷Gæ6Ú—k[W¶.}p3p®üz›þZÃ=øø}ÊÚ§í–^üpÄ[«À‘ˆ`†Ú‡¨'R¶õ-‚º ù¿ô(»¨+·ÖµÅjMLîF–——6Ö¥ñap NF8ÚnÖ1„"5ÌZm,±æ/Âý¤Î(Šmlk…ëlßmhk"CéếGÙë†ÎÝ| à ˆéé,ΘÅÀÇIGQ“Ék\;âÜñÓjŒ‡Þ·Ù/¸-z`\Læi¦‘Ú³÷Ïíß½¨Qð‘h= +ȉSÝßS‘¿J4,[+…¥«*ǧ4$kÒÿÜÍ@ËeMÈEëÌFiRÒ*£âFm€ŠÛÜ’ÿV´ö½6‚À(¤¢HuÜÙZ™¶cWî†þ/G•…ƒuÀ öR…8b9÷ùØHçÎ[Uý鸫h¹¶—WE»öØ6<”[Á‚³žY"²áTCŽìÚoú´Û ÷Ööë(iTÝÊ7ä›Ï·<µèaR¿ƒ–eá•X1¹%շψ”ŸB¾ÿØèòϺÔd²y`V¼é·Ÿy§(ŽiGÁ·dâ2ÑÑ¢¬P^´[¥E<{;ç6äS|¹ÊÆ!qšª +¢wìŸË¿þÅ4s_þu”yq ÖØé#”ˆ½`ÿõLÎ"G×TŠ´Ç. diÒDJm·áŒÿùH©E›Œã ëªÑšUƒàÿ¾We-‹êCåð +ó³S!ëÀRàj¡8°dDض4ºÊÉþ?Êï%GÒnƒòÙñ`X¯LÍŠ’hÔ¾’NȦm÷d±­À0{Û‚ÝQz®§ÏD Ĺ"f«Žü­ðø•—Ë2ÅöªÎ{9w£´XþLYì¨Voîf,ÁILî±+¤¢áâÝ/cU‹ç+I:27 ÑÖê<"@®lj‹œݾýIJ’‚p"ßj ÁU1£[ÛÖ¸TÀ_¼ —Úd*íŒ?íO¢¾2@l.Ä0Á©÷ãd†JM1BÒŠè}$B& .<*s ’®9 dí¾ˆ’JX"*¹6uà¼TÖ§KF@ÅÍs „Ž &…Þƒ7ÛÞ¡k(Löpþµp×èVKg4—Ô¨ÔPˆøsŸË¯ÿ‚Ÿ¨”•kׇƒÓ(ˆ›ˆB +2êù8õ“CŒÚˆ!ÀeDõ[h®Kj +4<:ƒ<âQLW¾#*ºA/­|·Î/l®âDE‰l’ø1x1½º…‹Go –•ù ¬èÎÖýSä„8ÀñjŸ¶ùZÏï˜á·µ6€Ó§4e*2º{·(î‚ñ;Ñ϶ ~RÔ>"RWª»Ê';¿ÙÍ@µœÑ)Æž¯™ÛTø!jVÌèQí[Þ­]Q›} DQ:8è/ Ú²¢Ô¤pyo+]ljcKÒá ?À‰Ýñ_0ˆ6ÁÈ:tÝø…›Ù'2Ôú£=¤ÌÌŠ:Žì'ë+iÅûHðSVÄy$…ûÖcl-è¢_t‡k¢ÞõÕKë ÿzHó°‰Î-ðPf %Àf¥mÔÝ$m0™EéÓwÄ/¥…žº¾æT +­·í¼™ ²PÖ¡*‰‚6ǽspŠÂ´K'ËúmIMS2á~!Þ£fhz +V%u#ÁM‡8¦S½KÁ<+Å*¢­Çù´Z@«ÔψßîNˆú3êåjù~ìgƒ0Ñ@­» …¼öRS„ÃΠðÇî‘ûŒâ“°¹0Š&Wc·¥¸ù¸Ø f”p=×\HQql)aŒ¤•,žÉxÞ. ]¿¦¸8£.rv6û™Üj·ÑÖª‘”GTõ’'s§¹«¶ýDE÷ƤŒý:È¥[©>jÈßÝ(ïs$Ë|+¢i_@ÄÈ‹&ï¤rÕÍGµ{¥/ý‘qtÊñ–³+F ôäNp½S‚UT pÒE0|wU`µVÏt´‡ž)h˜Üê’ªŒJE‚e5qÛÛÃK0«£´X +§ñ²ÊtYÓ`›MŽ5ÛÚQƒ€ þj-¤Õ˜%žº–uK̯䵷tmɸ‰ørôtE}mëܯŠ v‘ZZehžé¡è/พ}Í“9o@ÉDô2 `¥+Ÿô|¦k‚KW¹ ”ϳªkpo`ÿzÿ¤õýùœè/¹H‚œ¤ ÏúJ«½AÝr`·*Ó1ü²ÒZ °£!Øïd¶uZäGü¨ˆu Ò:ÞE[…Å–3Â7Ê®«âºV|Åú~„åÄv L7'댯ƒžf#£§“]Åiðt?¦WĈL'j/ZKÔíKC„Ê:Á¹¾«ðF‘®Qt%ù å€p£®µo¿àÀÄù8=IäE¥w7Œ9¿XeÂoDßc»J «¶R…{jî#ù€êeáÈMåÚJí@4æ­ŽbPë<Öjb“VFàk0ÓOhÚ Š\[ùÝ^Ÿ-mjâû»½À“š/ÈZ™‘€2g&GQÜävL«¤e×v¤ê™(‡ŽPÓBkžJyi>·L²¥=Ô$>_iOÛ¯Xçƒô3 +N'QuŸ£QG#ÖLOCk&/rL¦spÞ^}:mß«³PÓq¹’2¡]õ‘<¢aƒeB¯ÉglòN¤,½GŒ˜‚= +êFÍ%ü²©ù&¤ÎÔ`+T ± ³jYæ²Ã¹²ü=ýöq€Ú\_¿’ærq¼0† ~`ŽÈÍÄÅ +Eàª]ƒÞ—]"~r8¯Í‘"M°³ëša ©¢Î=Íêî#¤v\'ýWˆÂò˜…5dz ðì?<?vSgº*~1#tKiÄÉWf³cGÜÿÙ£àõ¾q2cIh¸ôOïÃ$¾4%¸Å_] +?Á\ì»?´®ÖÓæ1V¾”yLÎP½¡‘k¸„xœL—ýÛ2oŠ=Ъ¡è @ýà3g·jâqüÒØî§Í?æ6ÿPH¦oS‚£™UµŽoáC 8ûí®Ý{ÀóZM½sô}TUÈá3¥Ø­ ßéò ¥¶Â¢Uú~Üñn~7¡H­;}þ‚qî ’äg= +êCZ0ÆÞ¸¹šµB7¦fƒ–tdÐÌHÁÊ÷Õ`·Q{Ú?ÒÊŠaWg¨„Œ¼"Öze&B&ÎÚË="ò.ó/eî Jæc"ˆ °&¹( –45\ìû1Ÿ2ËèñÆš7Ë×­ ¤Òtá¼>#r€ë1IJM≚W¢H¾y¶r{Àý苉ÌAZ¹€Qò3âWxÌã-÷¥—ÍÙãeÓÃiøí#Ž}§(` ë¥úȭξÒçýá‘ Ë@zÙÆwûÑY^ ‰Ú@=ŸÃðwAD£lðè’|ÄP†¦ÇEÿþ¾r¾š¤«'mg—uÖ|XÀý2*ÒmC%†åÛgÄó…Vï F÷ýÍ~°Ý‘E‡+[É|¡VŠ¥ãÛQÁ)QHýª­Þ!n¢zï€I±¡ (•[eîRËïÙÂrnM!L¹ÑýY÷zßgDî] üï\3Ÿûy"þy—‘»?~¾&j*kõ­šyîuϨIyÓ 1ŽºœâÆ‹%êq9:’]T碘CÍŠv<3$úï1!ʈ¹#꾟ˆr¿ü”Ž#,§nyGŠó±)ÎWøJG¨ÏÛSzŠ6ñwV¾ „Z›˜%¤’‡ùܶ;ÅšÀÖÝlᜊü,A¾"Ï©n%P…JÀÎÔi½¯ˆ8"êjÌ(“¡àQü¦Mù,ˆ0Ñ÷ùL3À|á‘ç}—¬Qk*'Ú߃0H=^6©ž²œüE ?Ž3ð0¢ÖPiãâ¥V¶#D¬5ùÜû¹c‹^ ‡ 6ô_ì×aù@ElĨŘ˜< üø6B3²‹ùàʨZs`r· ƒBE$æ)âŽεH§L&xÇU§«©øƒø AJ— ”µ¥ÇÄe’ÜÕmîÇ~Ï©a…· +,Þê-#uýÁv‚…±Õ>«i~~Ùgĺ¥r²Ñß|uiŽx8E¦Zwí~ð¹o‘\l¾nP97èqͤ0Ä%KÅwŠ#EƒZL–p™ß80É ö‚”9ã´>8¥sÛî@XÇìŽéÆ­*+¸è’©`5Í Ú4à¼C/ÚZÒG¤…hÑŠ:@j!áy3ˆ¦"J(åX"jáácz¶ëí×&?XäÁ„¶Ý¤˜Êÿs(܀לùµÕâ‚Ô÷•ýGHìŸ7?-.ùµ#"Mòxdðk˜Ø!cå!×`2 \VcÓ7¦~`ò‚i3cŽ{ωêîØÓÑ +Â8‚8"÷ä¾×óªÑU)BCwP7ˆ¦Râi^þH°GÊ® ¦|Ã÷]Bz ‰êp¡¬Ž­SFɯÃXC±=UrÙª÷Œ7¡GÅ‘\¾ªš ŒÆ‘Û-lÉ9N ¶k´ãËDL> ‚¸C¢ë!”µ@sGÏ£!ª\Fj`ù2·‡¾°öMUªF¶Í}¾·{ÅÊìFÛõÜZ›¡‚dÞn:/<;7ÊZ÷çïîÔ£º&2êxý!"égäPCï®Ñ°§öÁ·@rx3ˆ?<Ø(eQ#bv8zÚÖ8S2üBñˆ QÓF-v†eî #?y×¾.œýuÕo~΋0#6­%JH 7¯ =Å(„¹Æ/ÞžÞÌÔø®Ï1ÌMØdí ØRÖð¥ÜÇB%]Mºã79ŠwÈzŠÝ ;ž¯Ê™[+ê;v!¢ÆÞÛee£ ŽZ ©þJòn“¯¼¦ºx€ŸŒ`z%Bý¾zG±ß¡ò,¯Óu~©ŒbýÏ”@‹Mr#Ö6ŠL¬;pÎÐJÆ*Aµ÷fóé}Cnî{föñó5Ö¨€°nWí úåMTŠn}Ùß#|Š=Oqc^>ƒ ƒ8R4Àû<»ˆXÔªèa‘úÐúçÚº@¸õ¯ñÂÒ\uC]X Q$zN 8êÜéÀ”JZ¬=Ô jPsŽ…õÆ:Î"Òµ…´r ¸¸6œŠ¹\ÄE½¿©uDÔC!d„¸r¢XlŠÜ˜‘y‹œhT ”,Š¢. hpY‡*k¼Ì$6H±jÅr_acZµ.%¯Uœ,o”)$CÌíb„‚ÿ³¸¤úö¾lÙ0ʱ¬9·Ì(ÀUæðöµÿL‘PK‡n1·?×bª IÛsz ï‰èÈ)yåRTzÒØü××gl?±Bcw"­j/ðÒ8ÕTÒº¹mùÄ|£;©êÆ{Ä,Züæƒ~߇‰rc€7æ’ˆÕq1Ÿ#[2¢%üIñB18œ¤/ï™BF¬ªJ„ ¾Ù:>Ô +Ññ9Ä=vE’?7Pˆ4¹jý¼Å¼©eÓÙ?¤BCݽ‚ºì×áb~7ºŠ9·’E¯ñƒ6*zOSs¤÷ý¬)ÏÚ2½{¹9,tÅ^Áw5#»È½î +ÿ_/†âW/«º PÉÛä~áé€8¨,Á-a¿&',•!â€tCäÙ_û·ëRvkŒ ê7i5C„hFrOmY5ÐmPŸ¬-–ETo ªº¡V÷÷#/÷>Fgà'ÛAŽtEL7l¬ NËdžYö-/‚I‚´EBŠ/@JU¸JR€Eì{NU K\«éâý<6È#Yü½ë3"¯ÔÖo„šXfûf?½Äch‡ÌIü•…F®OÏoÄ17ü”ìqV?=^åéŒ&k8kP2±æö¸[ƒÛØì؃r™—·ÔÒ%±ÔÌú¬·<û M›ñˆž·¢/t¶î(FâÂbdm³öuM¡<0·ÁÔ˜‰:óú‚öâ£.ç7û™[úÈ×¹‘½—¸Úy”ïîê‰y⃲z ‚~3”¢†sËJÁžs¨¨#Ê%—\Ï­o0-ùX W ³…#(kôð@¦ô±£"wGµ¾ƒî<ðé' ‰¦‡?r 1UÖª¤&…P¶^Óg!jäç]\vÇ@*?‡Ã*innf©½¶BtÞõ¤²¡-yX,zˆùØp+¬¾Öÿݳýg–”僽꾥ѹµÇ9¾  3¢¸ÿA™ä…ý)ˆ÷Ê µ8¶À»ñcex•A§hYR||NÄæë3"7¬'ëCzAþöç~ÎM7¤³•Yª:ÜE¬œ´Í/] ¥ù´xîÜÁ*~+D]‚/êDÕkG­ýoöùÜüÉaü£o>HüyÂñú龸ZÜu¢Ÿ(»œ&O°l_u¢Ÿ¢‚LoR‰.B²…zFCPgYD«› ôyIjyÆ}”ªd›Ã*;¬CXY[à ç”~åÒ0àÛÀ¦Ö渾M¬IJߣR1¡v®j2i™W·i¨dt¨mà|kbÏí¬Û– o[‚m?ÈŽ£¨ ùó” éRZ¹ÚC§˜·»CuÂQ\G~–ZÞ­ð÷Ê»T¿¡ÛõÎñt˜ø¨¾•ó§;‹QqæÐP<9ÁãÊ-¼âû v‚ëÿø"yŽö-h‘é4º$º¡êü mU´õÍz#PnÓhª?(aýTµž+à˜ˆÂàF·üÍ~¨nK‘Ö-"ÊdAòµ™Á`¯ú9˜Æ³‘9’¢^íC¥f§ˆF§-ºÄÁ•…F'Ko¬‡}vò’.Ê{¿ÞH9弚‚,³XAáž +IÃ5 q ûñ ëzüøó»¿$¢OvýŸdØÿ+wv`OßI¡÷?þnýÇøñWÿæÇÿoú`åÇ¿ý÷øÃÿúñWÿî¯ÿËßþñ¿û§øÍ_ÿïßü§¿ýûøͯÿðÿ÷7øŸ¿ù÷÷üÿô‡ÿó9è÷¿ø¯¿ûÇßýí÷w¿Y‡øéäî×|ügþW¶šÑë«ìŒ´‚ªt³V. â +Œ"N¸žçíë%$4žõ—ŽRöza '9¿þ…a ¿Z“¡Ú­õúë[ù‰ã¨ˆª#(Yåáiÿ>QûÜ8‘Ý¥èLéJ5©íåü¡‚‘JǯEÖÄw‰ UtM'ùsÿ¡†¼Y¡cÅô>"hGÜÇIÌçÜIþ}ý*à?@欿×F„°¡ÝÙÀZ À Üp´ÝAÉš#(ײþ¤Êõµr*Qn WÛüÚ +ò8nÐÖ ­ù éÅìî‚]J‰(uÅà ­?èzò®0gjÚ®Ú€ÖR¾jÚžmáD¶’Ðz±X º÷‡m¡J×V(B`§­]æ úAÔbëú~àkHÐ;| pÅŠô¤tQ‚…Wà+'b¦ÝàñÁÕé:™šîßeáOÔŒ™9&$’é†z//&ÐÚ¨Œ5Òq0âÿ ÓüWé-¡i7mùx8~ü}O›ÍõMDŠŠʃ$ÛõV)ºdÛ ËJnÊþgFåý’æOxÚ¨rzµ ƽৠ—Âû—2™¹!¯mÌÍ +öãÀ¹vÊ<À=§ƒªÉŠ'µ÷Ï @ñE³Q PÏú.‚’ JðÅ#Ø©~ÙÀN_N¢Ô êÖËNS u®Ë_¡¦j3Öz†'Õ#`œ$¦n84ÏS¡vï Â¥zÐAó#(H¶@“ H+Ž™l×Ä[+ —7é¤;ÔÂJ± ®‚YµuÿǦ¦håõwÞ˜0°\î§6õl»ÔŠ_C§è*V™]iPÐ"u¢pþ£€êßò}ƒ_1PÆ£n<„4[ÚŸh̯;x|ûV–åØcÑ|ÔéÑâgÞ…€cþŽoó•o¶H^߃’ð}ìÑÖ`AötÙôWŸ^;X5úà®)awZË O2òœ[T<ί0_##’Æ«$­li)6 +nÜ´*6‚‹áW&¸¡òC”Ë·De×߯¾ ì; +üÐíÖÆzhš§ïf4²PW:ø‘ÝÞÊkЙ½n#ø"‰(eãsKd 0~[“E ¸)ÆHzŽTñÆâHkb(I…‡EòQ  (~ëà}'`B{å@c7ãKW|Òü>"Ð?G³œùh¬¹GÑlh"Çdã<ô•I»]/û}aŠ›R 0û½ïO ¹ÜÈα˜¢" °ê8嬗XL…2 è’Û7"4Y‰ĶÏóo@|×Ý™~݈.˜–¥x2³LYó M©¡bÔ£†’tHfAÕ¦|¬ AÛñ¶’ ïè›QñPî°!ƒ-Ž31]š3'RTÅõçÊ©nlqem²$ùkr¨–©3ð`m,ƺǔ÷B…dý¡kÚ=þÌ'ðëç{A÷ 7×Ñà6ªTÝŠ·]§oáïwT;”[™W¤êvǼNœ¸_oN’8ë#Fv†4i*ŽTLK¼„jÄõˆoEsCó- ƒ¦:žà îÛý(€6ÀB׺#šX^ØäÙí‘Ò$äHÚó] +pî#Ux nhÜ"š)fÖZã  ²¥~‹Æc÷\ðÖ#r¬îý›§°.¬|nÈø[ï¿J/ó¨h£ÿ$=⦦Üf“ù- tE®´º±Û£‰ÍÛ¯RA c´xøüµA…“ž ]=¯Y÷üªòÝHc}­‘ñÀŒtð#“U7ÜJ|áeãÞ&Œ4Oý©‚(ÉÞKkêfØÆ®o ¹Š{ýôÓA‘áêlWU-ø8vkw2ÏômzÛ¸hÕgÙ#Æ¥ñVÂvõëÏÈPÔiÅü[ûökcÀÀ¬·è¥ÿ°ÞBÉ ­s?0)óL¶õþw/ ûÈ9?¶½²ðW—öJæØ>~m@2rß›×ߪ2pÜA_H+!Š~¬'¼^»aåJ%Y»Èµ¬Ì”LOyx¶œkìæ'óÜ?9•@l ßÙ— qÚj· þì®G´óÞÎkm=Š5OêœÑ@ÒÅp]Ó ¸ÈyÝUÓ”÷=êûQs9ØÚÐyÖÌ/J»¨f’UäYÇ/þ~oË<Èß;¯xm¼GQoetnÎá–®ÉÛD* #èõ«©«ø7üq6… ó¾ñFêDÌRWˆ›|Ô׆k›Û2} ,þÚ‡öÀ©/×[Çœº-v›¼'s“{–ˆC­GF}½ Ùøuv'iñô| ‚ë›j}ýæ Ý$´ºmI–5ÒƒõuÍé{XéFæ$5}†á5Dô|HЖ԰S“?™Ì+—"U†³Ä·¶Å®è†i‚\ƒÇ$jM +}ÔjÛùâä£]‡aãh‚Ø̓>ìr!8˜ $ á̉\Ó´Ý’ë<x¸=>«éD¹K7JçÇÚõ&. Õ5ë”DøÑ6¢˜šÙ¡¬á,_»­¯uìÍ# +!»ŽÍ¨ÇRMšëv?¨H4+FIŸIú\TË©,(ÿå ÓšN¤õ¬hô#«2Šw„é´ä”´­¤a¹½#]žÉó_ž¦Ÿª#´3±DL'ç°‚¥°«„]DB:˜Ö“tz—Ñá[ÏT²©ý-<‡>e/úvÜX¬BNÝÚPëF¾“锇zbbh”î‘F9–c×57uåG"˜x8ÒÐp²&/&¢ .­˜ðïþø5ŤO+T žªV™9RžËåÛ :v¨bÚN=L‹MqºÌ }vjýÞA±øzèœT—%}‰wѵšöñÏ@ñQËîZdžÇög¦0v),bõǦ˷ØmiÂnAîãRš”¾[ßmäb¾¤VßíaVb¥<Âp?O”–m¦MTýO[IÑP>ÆC,¡pô%Ë´¤Ž-tºæJe;¾3_Müaü;¨˜‰±nü˾Œµ¢¯Ý#àK”îTz4D?牞¦´©œÛ\b*é9n¹çÇuŠ¾#(>çÏê†9®Š¦WÛ™'fç:®ã¨¡‰0œÏƒ8ŠsÂgD4Ÿçë9ü¼8Ú‘¨ÂÉG< ëæ05p þÇZ⺢BC¥«"ðM„ã„"†4fÖãuuý±tŽíEæ¨v­È"I“¥ìˆÑíPÉ‘›j%úù¡;«r u:\¬‡\h6ºhi˜ÝÆf¢Ú‘]ädÄ0µûAƒýþŒHµÌÄIr¥À¼EñLo×O1§+M L)b£ºÖ(4°2×€âÒÝ›2ÿŠ¸t0=X_ѱûŒðŠ¦ŽtÍÐZû&ª ž4V9º¨I›êj¤‚ ù.‚&‘fÔí_+¾÷(Ì ‰x·R±¦MÙ}!Ëv­œÍ¢…)zK©µþð¼oƒ/d½oµ´ë›Ý\ +Õãý©Ï›ß­¸Ú®U‹;(Ãr§aÒƒ €$ãxìG{F"¢'âc£ ŠvÌÙŸøŠ€nDl{×ÜÇ‚¤$#Dè6wÓ{b°+Úu¼D…‚ üÛ±Áäî]$ÐL@4Úàßç8@òniƒûŠ’G•è»ä@@Äù ˜ØSЫb€® Ϊkðâ|×7é5Ó:ö Ï/ß{Ï÷^çg@Þ;Еˆ}oü.Šg¼2Ïzzgbц󻀩ñâ|Oy*ŸQoˆ¤‚Ùä‘Ñ7Ù +t1%÷Ög-JeÜŸ¹e€~xÝ7¹ë7û¡A[J‹›šmÎ(H¶SIýˆ¸›$)ˆìÑç-JíMíQEÏø¶ÆƒÎÄÙ|Ê ÈæÓR_ ÷ßÛÃÉgÀó©ZºúøuGë‚2ÆáÿÿùgAU1}f¯§òö5ã«¢J2’¬7oߌ#YŸJ€l»ð - koƨÀwÞ#rŸ(Ò…§£,”÷ýÜ~U½?³³¾³3”]ôJÞxŠÇ:óãå÷Ú[ ,”YŒö8>ë ÓT?Úš_ÁÈMD`HDu‘ `ƒ2€»½Žtur1¼¸¶†dÆV'5Nͤ¾D\QÉ¿véjpOãíÚ‡DÜÏ¥ O,gKØ´ð,ÊR¿joMÝ7áËôEÂù„‚vjl,­ä}C² /§°ñ¤üøyÏwÓ’“.FÇ®Æ3?#.Yȼìr®Ÿ¼õç(娡³Z-0¥– H"O¥ïU õAÁÀKSdø* Ň¢K?Ÿ¤ÿØéÚ™ë Œ[蟡†ˆ¨íJcѵ]ƒf"æ `PÜ#’ 7™1-Ùó!‹oEuÞ Øì©3­l9€VÚŠ8”Ç^V[Šž+ÏCán¬ÓWÏÊ+â%E¢Þ]ôx¾òøõ{.¶aÚBqê8EèÓ ÎªÊí߀N9m[PÀŒíߣÎKÛ:p¾Pšª œiªPöƒ”báSd*—ŠÏß_oïèá³ JÛugm¡½öí9¹¶CˆêæïÃ¥ÍJ–¤ëÂS(\)'w„҅;`)Ê!óâY6A݈υÙôÈ ^*êk)¢ØDJ{æû€šp}üöéçÀR¥˜?º·½¬»¤—ÏáTŸ Ÿô)r|ú&âÛIð=J™h»|ë]ZâGÄÙA +អoLÛFš ¹Ö´VšgX)tL°õrÓèü¤»µ›´ÆSç^맶åÜ€ÑD?‰i~ÝÑóa½Ľ•pZ«k%¨€¬J“É*ô+·õ'Š>/i°´beQƒçµÁVæ12‚S~J þÊC n½Ö®9ïæbèÖ¦~õ7óÈþKÅ,+]ÈÚ_Ï߆ö{ ¶vgØΆ±7´g±ú½wè‘ +CÌ`J¶tg]òÞ¯âçùíËÜÂÚ”Ô[N›3%( %v¬±‰î xÚ"¢¢‡G{ËEløÿØ{צ[Žó<ïð?¼_RE¹ +›Óç#ÙaB%*Z¶¥r¥PQHD@EÁTôïó\÷Ó=k­îy¹Vâ²Í2ÅýN¯Y³fzºŸÃ}¨99–®)úh'ÍÓYÊí: ²¡°+úÛá$ÁmdJÇéÔyE¸¾ +tça±Šë]Ò*–8¥b뼑HÔèïºÉ +i‘·ì2Â;ÅB•º©½æè¹ù½“Ž—f¾4MAâ[b +Év‹.¶™»Y&ÊbO/î…˜nœ°Ÿy¦t€Æ Óø,<5ÇG!RM_'º- ðpÈúoØ»ž(Oy5Õ’€[cf®«©îçÅúÑ^>\@ÎdÅ) ‘+BÀa9VltN:éô»…’²tã¯H¯QÇÉêïd5¿ R…Yf¡ŒtyJW:F^ŸÔTƒäy•Õ£îß$x×2æ¼¾‰)È(^üz‚>^匀¡ëßÚ<x¨u‡Nä¡GÑ÷¸ð&};QÒÂå·[ +o?á¶Ò„t¡ #¨c¼Å +$bLÂÊÊÝ.(jʾ-IdØ¿GVkùF<>FÉB‡"Ö©ó¨@ˆâLB©ê n#:­F›DÀ¢*¦ZK¹Võ¥gªÅ¨„X.ƒƒ>ïSYÎþM½î Ç Cq¬é€*@”Âdå+æ½@faìë (z¢‘&?Ñâj7¬WÇá¬Rù‹v„fì]°ÒÑoV< +„×A1E¨GŠKUñ(¾'Ó‚„- `Fâ &¹^àžo¤m€®„`¡ +:IæËYTÓ‘­C÷zÕÂÝ$Ç Ë+wbü÷Q9;ôªæÐE$„ƒwµršû.‘ú–x#ᶎðç +”øT=ìæ<š]˾`è ˆÝ¯¥b[EnF@æMîÕ<™´û(\Zµ¸œXÛy\Dç8ØD å ÕÝÒú†Õõȵdv@x<“N'ÂBÇ$8hWGƒ‰ÁÚªó8¸€ Œ¶è”š¹Fdf2¼u|StK—GÓ‘QŠl”cù¸)8 M$Ä!Q:–>I©M3‚Ø]ØÃ@žGÕÖZ‘ÀúõE*w©Aל*ëÉ4åÚ/‘×9»>DÐÌyNX„DOÔ¶l#üé,Ï~ì´EžŒ@Vº®±„®Æ]‹7#4ò˜¤³"Õ"’îè ~Ó7¿âsÈTˆÔÝ©HXÄEK ]2‰õ­¾(&u,°6ÕÊ¿¸ {0†$ÄE•×¼*`s„ QÁSÐF÷àP6H¢;ú><…(‚±HºR5e1ï¬ûéN&éN¶s´Ú·AÀ ì³(eR{Çm€ Mp +8Xj+#8ñÆE`„ ø.ÕÞ:ŸxÛO[ït5FðcwÀrù‹±ñçä#ôºÐ,ÝG`l#pÖ^´×üü&ß\éuµš¥Ž*Röö+°jÚîPhY^e/}))Jr•%»\%… ò/jEèíŸ}ê¦ö¡›ÁTT$QÖ¿kJ¡„Úv [:‹ ŒQ«¦k&3/È…B ù%MW‡ÿçŒý•":V–¸ ,E´Œ LK$CÐÞ‰ I £è,l5v–yá_ŽèÉE!ö„ËD%îm‡í€ÃÃÁFÛÁBçZpF!°%;©Ž\¨õ ÆØäeåpàëà)oXñJWA äÁ<Óæu(ê׈r L T¥ãLd-q Æ6cÈæŠ&AŽ­ŸèI1-§I¸jÇ8]Ó3á&Êf·€©‹|ª*ݱ®iOÑ/ê{ +¦„:@òmûÑ1 t5# æÏä@v“B6¸NÛ¨jÆ®Ã^il=@þ9MØÝø[–v¬.‹OžWáJÚퟕƒ£D$Ârë*½ m,ìϱùm…1è…™¤ç¼üµ•mÓ¾nÒôcÁ ànÙ}×hÞV±7  < M^µlXÞWGœD›ý³º[Û(:à›2!›yÁI /æV»ü!T•Â-î€i°'wǘÐÃ8„Φº¡Â`M”•¶Û&âeÈu^ÈNc°ž<^É +K’Ë +³,»q ÈÔèLçf½:î²Çƒ +`ÚqàhÓ]XR’zN/½Ÿ‡c¡;±èÍ94V%óÕ<³L´ó„mµìqŒ‚6;‹‘£ë‚6?7 1ò,\¤«Y"{¢ +øW£!I€)Ž–®¤Ó( ¸Ž”],•b; ÙÃC['  L M¤½ìËjÛxAüðdÜöË PÅzÙó¨è+mŒÃN·#ÎQoFX"AG“ü¤äÙ­ØF® $ÝW6´pºZÒÍ‹dT;wÀ¾Pí·é|”![(Uúš\Ýþ”Y×4MNÀ¾ŠTß…ºµp0ÉʽTÅVæ>暨/±:Ÿ(Ž¢>s ƒ‹²3uíùk;¥qJŽ`o1¯èïÆ(;* Y€&çô±*ìÎÅ‹ÛÃ88xTIEj+*ÜhDXYMÊ^g6ï…šïˆ-á® Àæ®4ôåJóš¾roô_W˜ ¾uɬú]ûŒÃ1j.ø‘F)ýM±Y RÚRéÞ0Hk–½ì< €'Át>UH%ƒ¢\o¿0È’¤iDMª»ÐyŽõôEéTåóLq(jéRõó¨µÆãc>éRôS;Æ{ƒfêŸKÂ~Ê8?¶Ý§XL'. +–=67AZãjTÛâ—‡©HIH+KnlÐ! õ&"ËؤÍß¼  ®à)ÁàÓ÷ ¡ùëV¤ ¡@â´ÃuîOu-Ò‚uñ°²äç”® +1§ÌŒP·Ò‚ ¥p ?Ä ÏKùÔ\p5´/ª{w„çw>]âÖ»½< )SM!¤6Iü. ÇÊA_ªh¡ >G!;¶F¤ÑŒv‘Zå¶ìѳë(íÏ G£@“f¸[Ù¿ÝÝ{Ðóˆîr¢Šˆ°,*ŽMç9d®Ì,ú‰=º­š Où¦º[‰“4ØÊ6¢lù`¢Â`ÔIëMv"ç"d#ð`û5yR‘ÚPKùNŠ=@=ûr´©á]Õ"?5’=§ B¬°•VÏ k×}JÀ¬“ˆ²¸šPÓÝ$Žþq"Å`G·í0Ks“›"s”N-'!¶lDð.¦H°þ©õW×7ÆÉ}ã>­½°Oñz©«çðþÈÃ,÷G€ÅòÄd0ÜkâNÍ.%i¤Ï”×Q1NÓK#h+ÔALE„Hj®fdž!<RS#¨`ªÌÙƒFøaÄ tS_€P´}Än®.¿@–ÅûXŒå·2¹…ƒ…ÍpjD¢öÍyÚ‚cÐ ÈåÈÏáÇ`ï$7€E~òVÙ4‘ã`”² (öYfÇ… ErŒ˜ÂÎ+ù´ðnív¦Yß(ÓÇO6·QŽ½XYÚÍ“¥q/hÊáøu´.ˆ¹yPêZ.ó(˜iuãpê…°Qö)pQËRÿ¥gðëE‚^ös–ÂV¯2ÿtueF*àXz&Á(³Z˜eDËþŠJF$WÄ‘I0`@’g_¤j§h³‚`ˆ-Ý ¿ ø{ž:06–&ÇU7f§?„ßbêñ€ •ý êCÑ +ï³ê#¯MÖ–6^¬ê÷ãDêÏ6CÜÎmÄ1¦"NbwÏê³ûkW¼Ñf§ãŽ@ó/¢ŸËâéhIOª´I·(²ý)$RŠ#¸,# ÛD1R’FÌà‰]Î$W†µ¦WG²8}eB‹¢ }\ÄdùŠ¶·Ça3¤»Õç  Ï‚ñ£<ŽBˆiXÖBù†í¹{íòᡨiÊí–=EázÉáˆNÎA +!€"|g†â(Í'ÙÛÍná÷9Äô.9šcñ¨qÛÒA*4'€·¢Oƒl…HÓ‘Ù†Æ'<€ûŒHyY<N~®ˆG òàõEg:9ê†QÖ®ú³ü´Nq ü¬ˆFsVKOmúTÝvÂ×år*Õâèki`Ç<5ÇvwPED°æ’†MD iA©™í)ÜÿšrÚ‹Õ›BàŒü¬zÕ…,VL‘œ¥Ï‹0Êí¯º”.ÚÑíDzb,ÛÛ‡VíN™ñHòp1p„|¸ÚsT£P r9ƒ¢Ãœ “ä| 8FØÚÉ^L8AÛ d†ÀxvtûU‚˪u¥R3ëº^°w ¬ü­Tük­ANeè»Ií̲3ÎÈ&73¯HaÏU13P»ã”h¸µ˜8à>]—"¸ØÅf¶À¾"Ÿý„o%b"gòôúºÌúFAðw~¤kšÂp·-‘õX¥"·ÁênûƧ`í¡~Ñ\©îššiâIÊ(áØA¼·9˜Tµ"äÎÞ0•iŠ8,…Õ×Tyœîņ²`TjLY”yâ“h-’Úôè½yÚ3Ù¶c(þù#sm‡î¸ò—!È”2ST\#ß'd}Ñã.oäC€øˆ  %88¹§ªRø²ƒT I¿NEÀÝå°™“Âþc—™ ÏeÞp,²c UšŽ’–­d\4wÝîºÁ¦Žü|`ëæÀ)ócèz“hyà,q@€à©ÑîW}·¨æ—çâ|ëÕ¹¡*‘É&ŠÉèCÀyµ°Ô„źI•Njè-ø 4»­[0í@˜7:B¨Í—ê Ÿ +¡Švôі ÐT:÷®‚Åk…ÆÎãvXùÙ»;ªÅÒP—ÜýpÜŽ™ë4úa΄8Ðáàý%"þ)¨‹0^‰î’2E²äxëÌÀ¤XàÉ—[ÿ’ž;?h=À„6£’v~pGíÛ¢„éžõ¼JÖ ²ÿ‘i+H&Òn§“‰J׋JHSâ„rX@I¿ÖUÔSÜ’”ê<ÑyÔªom# åî«à+û³Ûãéè!½‡ ’þú"݃–ÝTJ#«#Ö,qƨyü$B]}¤’Ó°°ac…VfåPÌUØþN†ƒœ mº"WÜ\ü%«N½u—d´,*^hŠ/H07㵯õ øU•IØL}§ž“ÔΩ)’ÀÒ¶t-š>‚ ¾šàCâ~ݽªÝ™^íŠ >‰V•ÛÐRîptÍŠ/ÒBÄÜÞú7š4ø~e:3*‰’4¶sb‘ÄÒ*ŽÜVqRž,e9ü!_ ž.®¨7žae³E’$ÏeI½¾Ó%żŸVÌÕËPÔ;ù7,¼/F +FE°"#Òí—T>B!ô¥@P$1ŸCÂe2™À8c*ä÷‰µDõἎß{}“¡ `VQ—öîÆûSÆÜY2)ÎRõro§Úå»;dI.—é:¦d>Bex”ÚÃ–ð„½ +ÔØ +.u0±[p&öPâ`–óvôP¢§4¬@/‰ !92ÚÊ’wb>€Ö¤~I<¨ü™,1Žƒºf|ÒéÔîÔ÷H¬4Kv…´S¬îó-Ö{Ù¤=§Cá8e[» ¿™Þ`pçÂÕMåJć]É äds Æ"Ì·wµN$˜ÅK¦F£‹wBLbÁ•à•ÓÒ%ºxlöË'6—Þ†æ½t ¨nòRÈouèÀªasγÑsóƒÁZr™SBýù5œºëH„7ؤ Øæ%íEì^ÂálÙiÀú¹8®^¤ÂÛV`°‚ý%"¶1*½gr;-zJZ’n†Å…ì.û$ò&X^RXMÒƒ¦„ú蜰ïG”ä@d±bJ™ãAˆ‹74P'ñæ‡(‘PùiPéâªC5®ŠÅew©#º+ðSAa€~¡ ŒÙ±PrÝ¥„¸½"&»”:壣NœÖß¡% 6¡üZPX­eÀ[ÎÅ7`ø è{äY²§–Y„‚õ ¼ÿRøe‡ð²º!ÇY/G/×þ¯ýfzÙš~®,CÈ=@YMŽF®Š|HÉ£Èàx®,=3¿P°¿9 mýsÔR«òN™ÅµÙ¨ÿØ‹ÊvA:ù´ŒûP«=AdyŠSÆ>ƒûò7QDiÊ©À[Ð¥ˆ˜ +¸d,°õ +27Ó‹•Ê ¼yŸÒªó,Zê£Çb³¦¹w^e¥¬Í0$æ$+#ìJ;Žïç9Ó·Ô¸EqÆq:À%§k¯è@-ž@k禆p 1jybò&÷ z)kâ5×麓`³¼g(ÔÚó¶ ðWL^ä™@¦Çk.=“Ã몽C {·©ÄÊíÒsÁ5­ê%©öóÑarۇćn';·2NâkMÖ‚¨¯Í(–ÂJƒ¿i¤å¹¤²z°>sìÔ”h.ˆ€†ìk§•ùÙä¦fKˆ°€Â>p¯’à`Î; _¼©”ì´Û.^ݦ_ D[Z7§–gŠuƒA|rbñ4ëÐ# ÿñjgHØÆ!Œá€ ¥Ê‡ËE/ÔĬì#IúÆT†ó#IûÕbO²r´ü´JÂu6~°q€àV¡ƒ‡c²§¶Ç׉ºâ3-ËêÒlX ³Áô”Ë‹ÜYê—–™AG“ßö(`RF¿"ŸCê’f°@ó áµèR«³ÿd n~P<+ý é›ÄS©¿òۋ븠̄Äú¡ÚóòáÜ“6µ† §¨¾ôCtЄî©N¨Hx_\f!Ò<ÝчP /-íg:­üÆSvNb½¦-ô¡Ï³þýçÌ{rtqLÍí³¤0RŒíîkÛ!­LåòI²’:a•õšZcßt±kµc­„T$d…-çðÈZæß’pP®@tè~A»{R‹&h—ÂE’°Å¾®ªe#ÕO'a\LV›. +;‚æ2’-ŠcNpЩ¹Q¹Ìôš@ISXà€;e@Ùk: QìPÏbSÑ4¡jÍ£ÇTÖ- \³¤åéúsrÿùó:—ˆÓ¼­ à°óáß`ç’»ë¹>:ìý°·‰Nn>û+‡#ÇÄ$ÜCªP†•¼iÐ5YÚé«A]`Ù1éj7}PaŸõdØÕô¡FÁ¢i™töÛ™\µ2¹f°j}¢Ú³GD=_â²Mk©ûLƒ4è15ïÿP8÷B~Â1º1•;VY¼S%(óþ€£ ‘$ÌJ~$¨”£žˆ=“—üØæIê|±€ƒë‰Î)ù_‹Š‡lÊ“å¡ÞBËM\¡CjÇôƒG#y"+BF¥³J³{]cƒ—†d?a‘úAô“…P[ó”cë¼€ Yë¾ÀæºÂÑ¡–[Ù|öl^Fá( .€,à1ouè𖩈Zñ)J*8í/ËT±PùÆu<&ìÎýaT Õn$`Vu-"èAARtøÇO¶âŽÚ–ð).tAÄYUq•þ¬ ;pÿ¯‘Eßb™fTèaœgÀ&…×P| °Òò¤^÷üøº6ª°V'r`žH +0 @¦Gà4Bb›5Z¿‚®öh\ (õúžŠæÍ2¯º" Öý4ŸDÆT¬‹*ŒH€ªÚxÒD¥_¿(ÉSJ‚C|D÷æ³ÒfZ_¶ä‚ˆsÙaBd‡U²‹‚^Šµ!ë<ÁsN¡û9F¿KK†¢—å“Â5Ô%yîÌQ†™I£` 0Êñ¹`5hF±.¡m¤Líâ²b÷Îæ_p1hŒ‹Û£Äáúbaße2ž½•Š|¡MðÆ}(>TÇ…{1æ6=ƒ‚ädI šô¿TPrÑNÚ7¼¨éí9`=íÀH’£®ôIùDÙ••õرòUPÙ…™%.š5ËžXz¿{D!8‘ÍÛïS§JP˜54ùïpÇ÷ù(ÎdÓÙÎ% +\5·²+¥a]{Ðli®SPP',,ÈBR›·ñÓ£ `”[–¨nó@›rÊÎ Èut™qr‰ +ŽõW¹(#¼3ÛæøÖHQ¦ íV²°¿ÀCvb u±$6Òô¢²ìºN*¡-oŠ”ïià7»™¤e¹HÐúÙz1Ê9ûXZóc³»úH¡àr-1JFØFùïú\htižê÷ŒDªóüJ/s çØtÃ|æx³¥ì_,¤%$äìÐØ`j%?Cšï³PÕŠ ŸËJls™(¬+%°®‘p‘}ïÅ"bXÿRÀ!ëÙ½P˜V(Añ’FX9öÚC‚ŒÍ¥¬*ígûªaºÓ@¡Ä^ñŠÆ_Ð¥Qø_/@ˆ/Ha2P ¢ÙfmL¤“5µAXl¢9ZhH=£yID +¬¨l~[T„P•w#Õ6â•‹ãªìნ3=/Eù–ßTTò©xf'AÕ-B@m‰ ,¢íô >`g“@>ûÐÅË +3É#,ë‡Xly¼¡µYOJp­náòhÖu -8ÎÍÁ5‚¢–éqóâkÀoü¤è„H4+Äí,q¸13×}¤ (Å:üuc™0¶(E)BJUÎQ”»k»à mšñ%,§LK¾º`CW=z:Yw +¨ïœ×IŸ¶úÎ/ðÅÖf"ñýªóý$·Í§h¿Ø;™Ž6r9=ý¡v¬šŽÿ!ê!L•HYØõ¯bñ É’îqr–Ùµ*UK’܆ž|•‚v¿Öˆ¢»-ã »Ûí"T§¡NX ¼tÍzÅ¢è$žþƒ¤¼SUÌpà ?KÂ[ôbâË‹Ç)„7æ@A[I„ç:&Ùéîrr{“Fµu8S³DþÉ)ƒlg¾bFzÙ|íÿáêšÓµJ*Å‚V›¿çÈÅ©ÐÐL’Ÿ0/”X¯DsÖ1…KѵG¤}¢ãñÔ3K_üŠ¥ +z‘gsÙ57éd§ÓÉhVmYÕ«6 +ØQp†t–+rJ]ºCM½{YS‹=~ÏRÅ¥EW•üo‹Ó!Iqú?`˜ +âä/7U….ŒìngpFE-ÇÇxÀ¬*Ñec¡#_0äóBÒŠŽÕ¿)i˧_ü›¸¾^ r‚4Ë®ºú .&qÍ”çu8¶2­38 FFb¡‹-]^Œðx£WÕ†ý›< F©ÔcëžšáÀ`— ×!Œ s4»H£À‚ôé°¾,€Ó!ˆ¹˜É$À°QËq K¶âêv·( }t6WF%Ì€Ðe Ý +y—C×8PÊ©¡Øç$ðâx%‹©°Hv£@m’ ¾Y7$¸Q”á»`; Ï0ù@ÁB Z¨QÓS¦db`vµ'˜¦·Ç¡jªV’}!J#q!JÃòH`í3‹^Šz§ŸÅ“I€E±ZCÈxKºÓø€i¦ËX‚æ€pŽª‘*)®OÌu@ÏÇÄ]㈞³2ª"Óš OUÄÛsÜÀIÊE&qØ ž,ºI”¶‚xÊ¥ª*œª´@˜ JÐü<§ƒ¦°pó{—•‡ŸòHÓ±ÑïÉe~S²’Øÿ&‹^¢ðýx„h;:}U²FHZ°å}“Dª0<óÇAçÈ®Xfù$8v)“_"``ÊEõ(UH(­«¦„ÜM½Ð¸6Ÿ®ÔT$ÑæƃD Šn© LÇøÐè¤anÐ]Nàp˜‰†¤)Ì|䪧-T+wT©cNZNÍU4'¥ë›Çb-»§ª.G(é[Pïœ0€´¨ÐЀfOsŠ<\ƒ8è6BmfEÀÚ>Ò üÄ VF³ÅÚÁC$/CákNŽD¥Œ¬RlÕú–›4 œáÙ1¡·W€©=!¼‘®â)ÅnËà ¦«¡*DÖ¤ˆÔ}NªuëÂÒA&Ìî¤"IC‹z>ô³¹ER%l¥P$Bg=]íè ä&îŒdãO½¥i¼Е²Ö#ÉZ¿Çrp‰ZÛÍJØUá^2Nó¼;a¨ W0nsÄßA v/{ƪp±Ûr)nÅAwRXjl7}\ %|¶žÝïHËŒ€ «þÅÍMe5Bœ¹=*\_ 8ì‚;CG–¾O†³œüïXÉòip@ !‹“à—ršáù„aµX^ƒÔ„Ð@Ïd \IÙ‹S–ÄÛ¡iW (’,‚;8€³ÀÙ zªYG™vZnÅ B…-ê—8ܼ¹òWPÆj5uBdÝd6°¹àCH[8Ùì QY'ÑžÌ]3Åkatªz‘k $m?M[#¥5I|U5²i4hÌ>ȉÍÅ ³Ýí×?sB‰iá’K6XbÑ]£ b5t!½J(Sü<âÂkZºšuwî‰ æ2é`1.'ÆQ€9 š@VAÊÕ€¦Jý€ŠWÌÉ#ŠÄNÇ’Šð2/?éI¦ÑßN4¹ò,ž5MòsT DÔ“2ZKìIG긨va ñgà{˜B KB•v/OžyHªRàwceYˆ„õK…7»uHT„"hc²,1¡ìO”3N2®6ªZ¦í484(`TÒW¨‹ðnÜ*x@%Á°A‚óPšÇWi±1ìžqõêõ5YΊ q*×o)W©ÎS²°X8¯ýüËÔ%Ô:õ†y5œ×¦ªkÈÀùº;ò€…þÕHEªIrWj88¾9‚\pð¼pâ5 °‡ú =4  y{ÕÐâKò9çhÓšœR›„àk®R$Ì¥C1ãfL>»ÊS’h¿#£þpÑFQ¢¢Â”ˆ¬q +€¥2¸Dl’êˆ îƒCJ~ŒÞÕv'œ¦åc#s8ÿºcNy Â«—î9uÐfyðb@Ê{uõœ4›ëÛ»ð£Ÿãô¯Þ5ý‘'Úœ9ý?Aÿ¹óÈgE83˜ÜPkQ{ß,{¹7i)2A{$BŒ€ç0R%ZSURÐíçv z%ÇÓH©3q“È!v¡û8F•-®A°ž÷ûB ¶%)KÐ;­蔳ó) oµ— +ìºè˜ +á­O„f'á¼áö$qLðI£Øé…À¤Î“²`4¶vv‡fTa)BU*­SˆG K­¸8"Ò” ÂÅ^ Þ]ߥ(Þ}û£¦&s9b4•kí>Ä¡ï(€úI]¯úOªžJƒUäÁ„æØg&VòðÐÈ¥?ÑáíÊží%%{¶E:Ae‹ô +g #>…à°[y K¢=%‡ì%*5Õ÷˾˜½'yìÁV—xÖ¢íÛÛÅýÝq~=è°ÇA\ÔÄ`tçC–H2P‘}R…MZ¯š”Žà &ËœÂ+âûõ.I;[uˆéÚz<¤סû(ܧi+ˆ>²(î: iøâ¯?#$/i¾Ë\ï ‰Ð }}sÓÅwÿÿ%6c%:̈ò½[Ó;~£(©öKñí1»Vë ðëKÕëSØ¢†[SÞ—1öf¨†ñ; -CöŒ˜ÅZŽò>À·g(&ë}X§¿x­ÞŒŠ*uBç´}¦‡æN& êa»Á†‹ °-!G”¬›QEÛ4=^©ZöGQµMTªuôÕ $~éÁXÐœäÊäüPúØQ§éOŸ +.³Ì£4÷â!÷¤bŒÿ‡›³|Ôýñä›1ãYBZ·ƒYž¸86¤Šõ©ä ŒÐ +&©@Nî¦Ë¥c6̺F8D3¹ñü&UólÔÇe”˜>U¿"=hÊdILA7ø° OçaAÑj´úB\;f×.²ÉîKåÐ$pÖ|*cRµÛ”ƒÞȲá–åã%XGÕSò¹€LÀ©ÈÎbˆÛ'òÒ3Pe£G9¦Ž¢‡ÄdDeŠbÒ¼JŠb0ÈÒ9àÕê·ó…)²ÀÕy}O€¥‹›6×UWÍú8†<>BhA¼ÄXFéóôq Iƈy55¨Â„Î •ƒ®íDm©Sühq +'æ?ñôMÙ ê-/ M sÙuŽÏ}ᵟAwe% ·ç!¥S +•rŠx(¦ªÒM+: PŽ.f¯"Xðx&`ÉS\O`?ÑfÓém# wàÿ¤ÆÄuó-bŠ^8«. LĈá?xýÑøòGúÊÞ*`•×å*䎎o‚²–F°ÈŸ`;'øež\da˜(€KÅ#í ‰ÑdÅ)Ì,ECµ´Ý¸P»²ŠqÃ[F©ÒFÏwÕú*qõšdº)¹xL|$Ú ”ï Ï‚-ù&I³ +Øåß$¡r!¤Êü&Ð Ë…X9£GNK6…XÖŸ€¼ Þ½viW—Ä4nŒÀ±ÁF´<öMÁV@`´Žzy­bµQšÀÝÍIéÇ +)‚¨¤Ä î—?Q´Í²“sY;ÁÙRrHÕ€r9ªäx Õ(Û#>—¼1»ŸVv@™[Ë·Ž”ø”L_£'ÑLî(…ôPjjn&O“aÊÞZÈ!Ù 7d9ÆVËiô› òÜú1ˆ baHÕ;Ëšgªqþ¬ö¸Šg”¥m¦Ó0¶ÁIjµ [ñŵp«"¡‚ýXƒM6Öh:Mî"ïLf«Òiö „o˜õ]äÛH%„{/>û)ßEÿä—þÈZãBgÑB!‹„ôF@Ö0ßmFáæÉ( cŒ*òÕá<¼$*¨Å8¬(9¬Hþ2€H¥‹ÔÆé0‡&i©€x|ˆŠ@À#¡Â /`bÌGª”P†H¤«E\‹\PXéN…wŒs ÖÁ7W£´9¶KÙrŠ°*…^o¾ÈH¨°ðö«mqŽNå[LÞ1QvÒ@ Öž•ÑîLQšrÛ(rœ, +<Æ蛀L£$ÞD¸vE˜AT·¿ãVM£@u&ê°e–9‹3ÄÅ» ÎM•r•È3»Ÿ‹V†<•"˜ôbQµ“¦[¡™Šõ’Íš¬•~<ž@É'H©x› ø*pXÂLÁ +ˆpuTV”²·‰òp!X_ñ—U_¤®/ç%A™hÛeBI£Ð-ærBõoR)­²{$§G«áÕœ\Ô9¸!J³ªF@bD mút`IœçÀØôhÓ4¾ºÔI.”|«c¦ƒË"&xûxôS¦GyÊpXÞ.È,+~Ž(ƾumaCÍA¬Vû3·»DƒÈ‘§Q†E6Nš·,tÃw ¨¦”·£?ŸMzs‰>i¹£÷@Pä-Ð\r)ú'uœ§“ÁƒPÝœ`@êÿ-”wá½+*T œ7 èB´všØü]n»X8Ÿ_,… ·Î m½©‹q¸bŠÝ+jqCNwì¦YbZ ‡Žõ“ºßÁpj‡u:Ç1 I¡¶*ƾ*‚eåÑA½àÿñ”»/bÕA¬+yÈ«W¯ûÛ4¡s;Ð-rˆ/‹>UªŸ¨•Ò'mrxóÛVó áÜ%Ÿq2ÔK|¥ç8+ Pg“äI²ó@€¢™QxÒA¡€ÉœÏv”{.I¶ +ЄldüS½1‘د ²º08§è’ Т@g+¢œÄLyE†Þ²qœ((ˆ˜§wRÊ=”l§§ð´æåWt!­ë1Ĺ3êBÂ:&êp~Œðñ'våóCNñ/D@dÇSAôyœî23yöb8p†K‚¢«ÃAz:H+¡¯9±ƒ¬ItoFôè {ºÏÆ=¯ƒ¤nÞ†º9J }jŠã*ŒçQýñ4_—¿GÛš–é×c2cNÁ¯‚EÔÎfKlusØδYõS¤,aoªX@¥1Ï1+•®áLÔú ú»Ð¸9Žô +ÝuѽÐJOŽe¢Âµ©Õ¦"y8Š„>e8^g©øµ«Á[“¿5¾zu±k´“\ÈIâÓ á倞2=òq.ƒÄ{'û= b ºöQ}8¬)\Äpž¹Jÿ>׿/¢´5_2Êز€—ÙûC¹A|ñõ€~#Â÷éæSÞLAÆѬ4TI¯¿“§$=êȢ瑪nè~Øöúþò¡àSî—¿Ðh|qÁ.Ö•†+ +=G Ýu8P²LK"n:pÉ°ìÁïótYá ÅGTš¢NÓ=¶|ê†_2•>EI%m³Q-ðe¼â`ņF¬•%Ö„ƒm—c¼Ø°ã@$ ÏÞ!Bûé»CÏHZ(ª[bŸÞD÷ …ßóFNÅ‘:(-;;hQ\dâ2.ByE c1A#¡ïe(ƒéö4ñP>ó5F|z=lé¦A¬u£÷Co«ÂQSœAèá°ør6 ûÁœ’§]FÁïs&!æh’AoÎXF®!✼¸[¶÷Q¬B+=ãyâ0×Irø$‹ ¬m9 w@¬=棲ážL@Ó0=EÖAWæoW_"EYNØSk²ËhhEqNŽYVpmn Ù#ùõÌ?á1Š›©B!bNT¹NRe7ÄV€¼øÃH=¡wŒÞÍÇe¥E“Á© *ø´øI$àUH¼Yª.ñ4 3s‹zèô@HnI¨Aa"9UÑ´ÓqC/£ÍG”<¿¨qü˜á¼Üd댸‡ ÒÝEI¼´ê ºq Hr5ìÓ=±+ú“*hR?…d¿`o^tM®3z©ý[H;Äeu'±Å;82“¢‡<`Î{­HTo­OePxó}Ø¢2(Ãä…LñcÅ +i`Ôæ:6˜îNk‚Š† þu²®(:=U$] +ouE}šjŒ‚Ù¯óPùDF:Ï鮞QËsàON—eDyúEN°'UŠ¬¸ ÈGÏ–hanFÈ•ú<=%}û_0Ý„Cb«p–Í€ŽêXS±¢åÑôž±¾ù±‹¸Wl-#ž,v¼›=¹mwÈåÊ`õ@ :"ŠÜ¢º)ò°S[-E…”t4òÝÈÌ·.óò«vç +ÏŒiGi `qtÆ•.&Æ­ŽÉÂW$;ÑXŃ€Åºy úèƒ;Uû >e¿Ð¸0ͱ:¤ö!×ÈÑ‚hƒ‚£ïrà=õ’íÉÂ¼á Š\ •fowPï‡ûÇ舭–zOkv_A\a@V1"ãtXNÉC!¨Jã£iñhF€ƒRR@L<Èm˜Ë@Ã.#4ÿ¸*]ÒÙnU#\† +Ij^aÎÁ$“ 9°2áSðºŽ)“[œ +̸KßçöÅ]¥Zt3¢þ‚§g.†$®xÎCèS%wi¿Ñ|9zLºšÅ0ûm×èeŽ*Å^SЭ€eqîj¬7)ÈS´ÐäxƒŠ<Ì1®„Y{6 +ïÑ.@WM„9tìémòbþÛ¤è?F?}‘ãœ] øÇü5(ûï!ñÐN¢Ý®cŠUn.„,OÙÝйsOZ\Æ:îPh–W +®nd£-6……׳@·Cx‰ÆPX›b¦þ"ò­{ƒ[Ê$—ÿ»ƒŽÓPUH„Uùú Ú&‚,S—meL£³%‹¾HÁ#7®mj î£pT éVþÔ½›.ARPâ.š’åšâîEô¨q/’ðŸFTq™œbR«Yb©ƒ”·ó”äìf‰¸xaȹX‚rkðj{M¢ç1ûWm ™w_Y—è @ýpZ¹11”Ž[.€~”‰ÚãyÈu66È:è²ã˜ èn¤‰GG%ÚGH(<Û bM¼Sƒ,S­ã ÑN‡êŒ>…]‚.B&í¾ƒWø\Ð |º•Ä‡m„Ũáº^í45Û¦«ÄˆK™ö¤ ¨w[ea‡†‡¼š ^RÏyá÷}DjÛÇëáDxŠíMD³ìe-UÀˆì#ô\‹?×Ù¿ß´=uÉÈ Çuf¿Î:ÄÐdú‰#„¼Î«±¸p¡ %6ÎX÷Q‘ÿb±>r‘C„½ŠÓTIƲ ‚ሠ(KøÐÁË“]NªÀˆú€„È*4‘9Ô½F¹ˆÅ@]_hl¢FÁ(â£Pè©Æ°Ä_¤­â"Ž2"FšMÌ`âb¥0A¦äÂø &ÛÛÓ´W=¸âžûΟ.4ˆü<±—ÞŸª@{í€ 5— –ZZ~œ_›h?XLg‚Å“G«ýàpcŠŠjWr%>ðÁŒ((™2ÍF$©^"ýGw€£?^æªô -m´Q|qf ;‘2¤¢ãF-uhAF|¾„Ž¿‘öŸ€¿Yhô†–‘%Ùƒ¿%‰££ršÛœòÑ„&¬YGøÔ?‡}V0(Dnç/NàÎ~Óü’GúÍêi +GzŒÎé2fQM ’—%ú» :Úxc+â± +ïèí1èbo®%B®ÃvWÐOÙOƒâ‹dnp§;Bë¨îh1Ò¶H ¼É¸ëˆö I<…oØ·«FÑÍ÷Q¦â×VÂF˜­"™ig’¾@¡ÊdÅÇ^F\Š@  ¹Ýàƒ¨°œb ~În:B•\šœõ²rTƒ4BxPOsüR(sŽó²¹&q×7AÛÔ“=? ?5PØ8¤ï#÷ +&w?ÐÂøåõ‚¿û Ú ”,)ió)!ÂT©•ä¦YZ{‡mfÇh\KV»ÊT÷>¥†öÛöqРelg<)\€>vÍ>½ÎI,_—!ˆ4¶‘‰u¸|t–§§>•ó(<*¼CÞOb§ zˆépƒŒXÑ¥‹°ÌÄ&†¢´Õ1 ù{ðÉ¡-M¢ˆ7§ C™Ð0 ¾Ã»`VÉ͈I“C÷ó˜°áuÄ€*õbsDÔ s"àëÍ øNFĸµ_túŠ¢˜°Ö¶‰$‰tr€e¡«ï +ΓÉíè ÙTgÙ9B6i}V??º°AMJ\ g¹@œ ×¹m -ý:ì µl8Ž.% é* *Æe'x‹gƒ¾Ý!“™ùäc‚ÄÙ+y¸b³«nËy²9c×+2¸Âµæ9UŒ£Þ@ƒV’Ð6ÓÓü¦u³K£ ÉÃf`X)Yt©I¦¡¥PÓjBù²Ã\ö^pèåÁGÕh(̵2(.ÃêrÙæ¬ÉÑg°ßdb£ûâO8ã-3iömàpÓå£b£,Jÿõ¸žÓ½<©XR›/ª*_RêÑ<ÔŠ€âÐF¡HG”‰’äas-קú3œyûPD—FÃÝ3—våRHŒÚ…$\âþ’|ËÎ/J^*ŽÍ½¿œåe‘v\‹ª(¡nÂ_¬êËAjå,Ðs1'”éèˆEƒR”Ý’>ô• ÍR2¦­õ”äÓz`à/¼B°Óó<\xYAêø*Š]Dñ&š?‘ë`ѦÙÐ!BÂ2Þ.ú%â`R\ÃÓZ(-„ÜsiOH4†N©í3‡×”8 »³Œ¡Ðü.ÒtMA{rèÔ¾‹Ãù)Á>âھ˷ý‘';ð…-Ï„Zý¯hɼýüOÞþò?¾ÿeáíŸ~÷Ýß½ýü—¿úó/¾ÿþëßûù¯þáó?ûâ›o?ÿ컿ÿ§Ï¿û›Ïÿô«o¾ÿ·¿ÿî?ÿ½éý'~óõßýÅ÷_õ¹}ÅËŽ‚ãí¿ùú‹—ò•ßýþóÿù›ß}þç_ÿþ˯¿ýþóÿíëòóæ·_üêÛïoFó¾øë¿ûÚ/ü×ß}ùøLyï3þû¯ÿðÍ×ÿhù»x÷ç±ñÝßn·åßüþ»oÇO¤î9LXè÷Þù‹oì²þã7_}ÿ·×ÿøØÿåëo~û·ßìŠ~ýõß|¿^Ò»ƒ¹™vg¾ùú~Ä}ùÍwÿø·óOö?ýòWéó?ýö«ñIÿ÷§_ÿö›oÇ_þõÛÏÿý·ß~ñ»¯¿zy‹ò³›¿y•¼UÂ*|`¨‡—RA…% Ü4¼/ ¬Xþié‡æú/õßù?sêùd‡×7âµÿùÙÿÑòõ·?{ûOÿçñö•ð7 +º—KX¿I0¼^¶ÏmÔü9¿¾û¢?zðöüßþàWy<ÍO¿øòÿ¶UħÖo¾þòû®¼ýü· +ÛƒÙ‡†d}Ãu»l¿åL€0¡¶ÕóRÊعû éÈt~5 Æ?¢6V¨ŒÔM”ؼå. ·Sèä¶cZÄà>ãdºÛéø ô4ˆ’dÞ.[ºÎäÒ‹÷‹: ¨\8õ§³,<Õ8¨Ñ<*u^ú"Ezh]–ÏåG_œÏžÞ°:ú²Uª ÓØñfáåÄ+x]\²Tð$ì®vgrÔ´âh¤I”ë€8«}œjçWâ go²V¹»3Å r£@M¬.z¬¾*À, RÖ[y’“¯J]â„ÀpŽ§3ÙÊAežU6ùxµHÎÑn…ö–xÄh¯ÃCâ" NW†t9Ö’j*C¬ë‚wTµ¦À¾ f h´·²m’Ò˜ªC¡S°<ˆ¼­¸ê¿šÓ‰úf¥Ë-‹/ ~i~-!ªM2‹ƒ’„=ﲂFøRÏ›væñM"€ÒN•ÅŠ+ªË˜®'dÚ±8qp!¶›±ETD-Ñ$@†±P·e¦%Pº6£ASb×mP)–½‰¤"0³ªN€-yÄ^cõHÈ!ßrU3Š^DîRYG!hlc eP½¥ÐáÔÛz@¶U%)äÑBP½D¤öúãÁþ‘t–åøE«VöþKxÔ¢…#‰ÒÔRÔŒ«M•Y@WN¯Fººi¬NMú¹ž¦[•$ +öžíÌaüYdJ=Î6" +ãyð‰+M™=‡ÁO ^¡Ä«øƒ]hãÝ}{÷…ä >ýkÅGWìùÛßñÕ7–U¼¥ò'oŸà}%¯‹ñ +e²AG÷Íí(I+–üD¦7($Be,ð&Ðä²¹ñöéoí;>‰I)zs5oËÊ>¡’ÝzåšÒ¦¶‡Á(~¢b4¥H›˜ªÙg¢Ü®©Û¸<5Ôö¢Û.HÜœoÉ:óI‚âŒðž=z˜Jv}'шF§=,‚œ]dËø€‚a£{Éú‘k8/Çøö)kÍ'AEUXöÃì=ü„æ´ÀLÞB9ñý¶Ó‘ÓM!âÉoGÓ)˜fÌD>ý Û'x Aæj =ÃÉÛH,´GüéLì·O?½­ñüï·_üïß}oQôw¿ÿʦ’']tn(Óû³/,“ÿlðÛÏ?ûå¯þ혀ñ7ßýþw~hÄä¾õÝ_ýù/u’Kÿ»ïÿÉböÇ×/‰ð4_ó;þó{þ·&ó¸‚D½ÂH¯QižãƒÌ+;7f&,‚`ÐîÇÝÕµíE-‚ÉÙ#¡(FŽDdñV©Fƒ% a€½ží¶Ú´£(ÄLXZQ v5a%DéxÕ ío•ƒx$9áP…3¶wûz5ãØ¿v5}àM·Oá0„{O­Ã¡$×9e,‡Û: +Ô¨®Iâq^§ +ÖNQÌ­¸pÀ¨_±| +Ô› m‡·ÝÎ…@P€ Žbþ¶þc‡ér¤wí:ºïÜ)¹€Ú¼IZE(ULƒãä¢m?&Æ ñ>°„„:b@5ÄŠ"žÛn?N÷R(£Éæûs……H,f /ÐWêq4÷ìòÝ¥ÿ"äzBѾY­3 © aë +Y¿ÉŽ[¨Eõº+üM}¨:†? —xT8pÙm,ãi°cHšŽ¤˜ƒ¾âWd÷u1`'Š°ÁüâJs\¨u4Ú`í«‹Ÿ]t‹ó¤ãRA²y•ð.á lßI_ŒÀÎÒ@'»âî ÄB“Ä3­èP³®í†q-Hº¡ÿg»#a;H¶Ð$=8NלlDbôÓ Îö4SvÎV1²rÙ`Rüt(–æ¨:©§éX\ÿA&·Rî¨Ò‚Y„ŠéP§¨ÙZ…ážNFÜ É4É?DéQ¹¢™}H€“@Õ.VÂnö6D# ÌßËõg=Ü8*ØÒñÇYûúL÷š‘9IºÛÙæÍ~€"/ Y?ÝzÐÓÞ*y"nÜd¯?f'å,®&œ™ÖÓõè6eH¼š§«¾Ú¯;P‚Ý–í­ƒ0L$¶ƒÈ9:g®º4=°8Blyò†¬ r2g°K²-·¬1ŽÇ˜üÄVR*ѼÊ؇Q6Oò ÿ—„l<Éü‘x{-爠‘ýoIÀTË׃H)yeô>iön Æ°ÉàùpÇ…8´e9+äµIyLÁ› rˆáZ©¢ŸUv@»ÈƒĩRÃufñöíÉð’ Ù¤—‘0V•{¼}› ŠÎr\ä¦eÑïÔŠ'¤‚9YJPdgÅ<¸5ÎXÅч¹ö +©b!´9It3‰§ SiZ3Íëé˜d–õ&ÅT¦ÇÚ'ryìò³¢™Ék©õ]ô-x¸•|w¶"\Vea^¥*¡ÕülÍí8Q3°/|Cݾð™Ã="!¡9Õ´óû%¨M‚ïLÌ5Ø]5ó£|ÀƒÒò ®¶MR´OßÙçRXm´ÚdJ§ää [Ž5Ýšk‡ºr ÈŠ.ñHZ¢ÃMѬèÝ&Uæì(ˆŒ=)ÀocG“'ìAª«Ê7öྃ;äE*yºXQ\,à%QÜçûMD¯ð@µ O)#j}Hñ„Ž1Z³URŒ§nh«å™3¢ • 4X¤Uôï°ÆæYIU¯|#"´ï¶Ü/vð¢[M¯¨)»ÁC§I½ÖýêšÛ8W^=÷øY‡j uI]>ÁŒÊGÌbrëîòEàêì±ÛuPä³Ø0‰†- <%’a”8TÅPO)ÎIA¢X?ºxDu‰ ½º(YIÀ>…]Î?o·ïoQUó±ŠÏc‡â±C8ë#Þ §sœ«º:Y·çnËYZgã–ÄPCPàâ×â†j§§5#ý£Fµ6•DypkiMføƒ.³;WÅ~ó)²Áx6 sqÑU…]tMÃ)ÃjA¯á¥Š ˆæщä›+u4¡ÖG±CÞöoŽD5¨ Û%9ŒõàÓ›†Oï1uŽ¶;!¹|02ô¾¹¡±~°x¹É âÙ_)*[ž*:™$ö÷õy}¢|ѹˎùffð“~šMRÍcf¬3,ѺᇳG÷rz™œA˜Chp°ïæ¶ãÈF0îl·íÕÉ#tYé" ¶¾]1Kh«Œðö—Tîò<»³¸¿ìÉÍ#%×J©|.tßl"À]èÂòε5€f{Á)Ìo|Ìò—HTR†8&ËâEßO‘¿0¶ßÖ>š“Gƒ¯ ­¤ûE~;Êm±•~ ¨vù׃=x]©iŸ[¢‡¬«}K~ \^–yQö£ˆîêQ¾m;É(XGØ7(™G¾’€—]Énúx—'q“亟ѬF®jÝÑY´ßÀ•å©¥´í ð'x§àOtÉ]6_i‹7wc±w,î{7 rÜQµ üÙÀ+¼lýŒ’ +¾}Äü·%fÀ–¥§¼ÇtëÅÇž9¥èÖ …›,„Ɉf‚Œà'¥ª0s …˜€0Æ¡ÖŒgKÅËX¼ÅlË$w ÁhÓSØ´y sé&‚ƒKïˆ5[ÐÏ!0ã>½El˜¶Wò ú9PTߊŠ,69ßoR¸-3Ì\¢T­}:?+¼"ü%À•,„œJªËó©Eò+_Ãk §n(1>åè52—?2oúØ«ˆú"÷>îКÈô8ŠJˆßù¶¥AÒÂ7°ñºg$Ûˆ/GÌÿ’Ðìç)8—Dæ à,àã@•Úàé)c{IœÀ¾°­9{„5MÆök.ÇbȦ†[s¸Õ5 Ò4Â^ç¤÷s“bBý“É àÊÛtâ+Íye·É(¯¤£¾ìʺÓKö+(çgWDù\ CÒ$ ˜\ß®j"6Ú +ü®»¤Œ*%ô tn¤¨[ óp'œ­è)ñvÈS–2Iÿj-™Jyç(ÒÕiÏ•®—Š+Ô:8ÄPë¢rù¥V {ÌÕ³ìf¨þ±Vz M.î^ÂŒ½–:1§AUÜÐ!m÷µÄì“Á…|ºžÕZ¡Ö¥¿‰¥x}©oƒCµxɉ€C|ÛJãPiUãZ ê +#£ hv†ÔÔV”éF„DYˆÖR¾™`úìêËÚ],q‚½Çü!KAìOM­Æ–û¶wHÚ˜×Yí©­qô—hèo„ßåU²¥íÞxÁö27jökÖŽ ·]îkŸ3oÍ0[ŸSšëöÚ¥QÈR™ZÜ4…xKQyˆ}+БÅ÷>§NÄ>jýÁk[ +«nÚ˜6Àîý¹w³0E +6Ðÿã-Í°õ4{‡m½}ÄÝÚGm7fmþí7wí¾óÔpœOvkLn“b63oçÔl|.óqtG×y¼uTï^†­»½Rk'w{+·Fð;o÷ky® [Ûy[V¶®õíò´5½·enë—oKåh²ß­³[c~[®·žþ¶âo€ÛcC¬ÐŽE[׎`¸ÛùvĺƒîЉuóÝ€Ï{÷×X·þ é±…Pä6 Ùp&[83‘)3Ú`,·ñÓ‚Ùâ° ?3"¸ tsÿm˜-Žœ(Ÿyn -pýìP4ãÞ †´…ÍÐe-(¦-üž¸§-rßÐRw ÀµÚòˆ ¥µå"Èë6§Ù0b[n´Á˶üjq—§í£–to¢á¶Dq…ÐÝ曯à»-iÝ0{?dÄMÞ|3jI»7°á–±/HÅÛ¼ƒ9îgY’[ bGZÞÕ29ëˆs+lÐÛÌ!ÝJ9út+]àÕ§"Ò†tÝjPHv+cmÛÛ*ØÑݪi¸w«Èí á»ÊÞ†1^ëƒ8y7Hó]9rDÏbæ¡Þª x}[D]ÑÚ[-v»·2î +¿«oˆòYJ^!è[z°?•°7ÔûVßó[}ÃÛßã7¸þVÒßþ³°ñn› ûÇצÄÆLرá¶A²ñ"¶þÊέX[3;Gã®ÅS‰uõ³ yokiéWó«“^º¥ÛDT¥G #7éuºéZUrÊf -s­¯Ý/ _>zA¢×¼5Ð= ºv=ä +÷¸Šð :üÐZÃÞÎS éBze¤¼·mäˆVYÖiɬ­EHɽc,˜°g|nÊÚ=Xï¸n"Ÿ³ö9ÙœCÌ4ÊʹìýÒ¦ÂäCLùÒ£Õj[ŒûÙ£˜E%ê¹A»jíïîß¼öˆ·°¶š¿¼½ kËz¿kë[èñõ±xß|}šk»}Ÿwmûmj­ÝÿmvNàÀ6¹ŸáÛ»1A +ûkµâ>N¡âºžöD¼ªÑ%vp£1¹øV’%aµWO––H0RìÜd‘-x;OÙ?¾DËö0,Ƀ^]Ã7¢u&“Úe„~7:U'jóPƒ»ótyFèèAq jk£BÇøi®>£og÷+[±.¶ïA0 ³×J3ß\ï:Âçèú»·ól÷ŽX:<ægE´—wnýã9ýöglŒ³59X3q•´õ§!(²Êª‹g±ï·º°‰›zÙ¾>‰Ži›ìþw?]…š“¼ÍŒ»ïÝF,×ÿå,½Œ’´¸\9 ®@*Mv;Ñš°EÒ(š7€·û5Ú-ƒAò(­YKDjZ¢úq7çYïå~-{&_¾>Ø_^,š"IX + ö$-¶Lü7®«¶Ð +ùÖ F{¤ÈÞ‰jvyg¢B·Oä³0ƒ$}%ßLy°]Çd{i Ò7é Ð3‹¯°ýÓËû¹_Áújï¿än‰ØïÈvží®®ËÕósXß²uþ¡Vòx¹–ƒ Xœ8«Œ®çéæ¥ÒÎ'nߥǧ–iõøžu¾«ûòöÕgƒ˜µÏôn}·½òX_MèþžÍO­·èúžwîÝ| (†DL`¢ŽR^7™ØG•åϳ]U4h$Šø£jMBðd‘ƒ³PiWFo3»†Ÿ6ÅŠuÙL8ê:ó’Æ :çøÍÁZ¤,‹n2fM¬a¨´SHìÃÍ3e—Þ‰ê(K #Þ_Œ²ˆßGüQ’2~¬ÉÑn—Ýb€ ÀÚS½b£³E ÁQðT¤Èí!Rìsß82/™ëAžw…:™!i²¶GYIB ‰‘ʨ> ¬r+5{Zï_C '¸àòðjO°S©Õ†Fª´†xì®™Œ‡'Öž©¡\sŽV:pM‚”éOïéÖ ¨B²2xM¥Î^âHXÐùÆ…ð'êÚÅ¡[U+ÆÚèÜÌgYÆ øؤ}4‘ÔíD˜—Hl?žlº%ë% q¦û_ЫWfKÀ7ìîNPÙ“É…¥Ð’Àß_ÁZPþ!e½{*Œ*B×Xàá ÂòPíípúu>p Ͷ/ڒ̦ûyÅ Ë4ÝÈu~–>”­Š½è@^Ö)üFš*%{»ÿîM)mØ!#Ær*|ZÞ¸BÕ•Š!•Î@r·¾µ×O½{ç…p¡h¯<])βv 䀑Ó.¿<–"ê‰Ð(ÕÓ뀣-ÞûÔö…À{!úŽ±‰Q6®—p‘&LÃÐÙ.!‡°ôAÒïoû-óˆöQ `}Ê~çïò–íù±÷fœY"N£{h‘g5„ÚÒÝT2eš‡ÃÑxÛæϵäÍB»Š„ §Oõ×I|Åb§b‰'Øú2`Ç…r‘Ÿwy—äÞe{ àUŸ–©?½Å¸a+,°%ŽõÞÏëï>>.A{ñ²Zœ’‡½‰£›¨Äu©z|hYå_³.’··-¶ëoÛìy[æ2ÿ|÷Ía}û³>Æ}£º›û†·Î¦}Ó\gä¾ùÞ&äÛ&¾¾[°¾bû¾{U÷°d}ÓßhÞ‹ƒXZ$½r ©ÉOÿög÷:C=ýÉ~ ’E/,›©o³%ú‡×?~Ò¤0m9mÍHj:8˜¦ÃVAj”®¦ó$oó¯ÞÑaýoBãæåFß Þì7úKÚ ¢`˜Öà]ŽO™ôbĤm’ÊIQ@‰Žg2øoØ w„Â=2zì\ª°Íu[B$’W¡p¸+Ž¹ƒKo©í¨H W NMÁa+àí‹ÑùÏ;D'&\;Ò‰oì+Ø¡É*+12ô‘—C‚ kíÜ(øá¼ÔF@& G­WEmæg—ƒë{#«ãבJƒ¯±ËQKÓCÖ‘} m]‚´ž„À©f£É¦­@?§÷Š4¢­ˆÚ†§ _‘Ö‹u‚°f—éjã à¤6´ .óüäU·;e|oÏ¢a‹j·µ²fI_ªèf¿X9—ÃNóø&´Ñú)9É&)Hö«"¬¦0ÜèCZŽAæ9'S\𽎼âzŽ°^š_Çä•Ö„òâ0>:Feníƒ2f\뤷ØmjÜ»Yê絓ӕ”ØèÄ2ú§%vx`‡Ý„•îbµŸwh¹6ãdPq};>ûÙPW³ûf?çx;Qª¦ác‘Ò'ׂRhVâ›ïÕ–õÿXî1aø*6¢¸@ÒX+EP¨¹`=>êU+%à^¦òɧßPòû£AÚ‡¤ÚødÄû„ÁÉÙ©¾4*ÔöÓÓùc¿MBú£¸A}Àøô´Xæ·«ª=> +ÜCßOãÓÖùTÙ„ÿ9?Ð65~ ;#ðÕÈ´Âjÿ~!_Øð‹¦ÕAGòÿÊj›%ÿ?V»›Ü?dçᆉpZìgëO”Š»­ Ú +i1Z±¿Ð± ¡U±ë‚ŸÚ/m¶5¿Ó¡ECÁu•ím¶­#»º28hÂ/u›£iMµp +ì`RÝPÒ Ä’'€C ôjPÅÑž*µÍÀ¾¥˜yàN]]w endstream endobj 60 0 obj <>stream +¨:ø葯Òèš1¯ã':F*I‰žìIô¤ô Úc æíô'É«A­Æ†qƒŠ 1ª×ä™öBŒ/h:¡·äh•b"„|òâ¶Õö*×ä©d®ð&TýD¼œ)ª^tj>Ì¢›imçQë¡2J.}E«¦\bßÒÎÃÍå – ØêÉÅ/g9Ãn J–ìÍÇìòØ•ìW”ue… õÎï8FÂeßv´ÇMVE,4AÛnŸmÖo‚P·üzóéÐPeÄ—§Ý=:õ¢ßåÜ<þ.¿¬„·»ÝÄòÎ4‚Œ ơض%»®m:¢ž +QÀjÔs"¦H|·h /•ã›uuîNÜÁ›ô²Ö/¶lÙDóŠÀúÂ{6t0'eo»ÊJÐ/>»8†úå^’¡dµ-õÑ¥,"à?õâ;zŽph{žä5ör« pzuúu"`â§Éäg7ó)8À×½óªbmÓP&irBÒûÔ>hIñ“滊ØFtŸ[öfôY¿¾aûyÖ—t¿–õ]¿ÿMëJ±ß›uµÙnïºVÝ>¥mÍÛö¶nnóe[ïæݾŽ¯ó÷fXÞuߘ=—å!òAX¡#PœíÁ‰¯w§¨M|î‘7™óܵ)£n"Úˆ®S¥°×‚]”¢ïD´¥¶~fÞö°‹höêc˜kF®"Úö9,Q›nc¾Ñ& IMþ »ÆMDY{¹'¬ò.¢­Kž„ô÷"Úvin‹œ¤¢Ýwíˆå5¤U;;¢ÆÞÚ½d6?ws~¦ý-o¢Ù±‡ß®ZÙ(Ï9Ù<,á^3;È#+µ˜’ll×̦ö<ßÞ{Œ~Ó®™Ýãö"•íj”˜Új•Êæ¼ýœ—`xW¶øK°2l8À¡ßkes÷H—í®-üs=×ÎF g¸GÔ)û¦h‚vj÷e÷~ôøφu‚õŽ2=`ž]6­±ü¤>±ùÚ›%fý‡}]Å ®€½öêÈ¡†—˜,;mYV$ñìHùr YžÔ³mÊxý¢Õô]ÉâBØ;o÷¡ž7IÞOõãðcv2#° ¼Ô³m…È9äøßGŽ÷ÓÈgÿËÛëÀŠñýÈC‹úÃ^ê Ê*n?ÏJ´úìàoX º¦CK»ÏŽX1áÕ^GisNx}÷Ùê¢mÍ¥ñ‡Õg†•°¥5[û“ÏŽ1˜ƒ‚Ç+ñÆh‡ —1mÆøl÷F;Q^ÒœZÈYw£wïß4Úa õwн3Ú¡Nêe5***Ñ.F;‘ä¦à O~×ê´Ú hñ ¥ B¾sÚ‰âã‚â²-oÑÍi' +3qŒ¬ƒ‚òâ´hÊoŠåbËÙÓŽLbTKä§B\vĨ +TTm;µî9pÂx‚¸¼tùµÜ9íh–ÊÖ¦E¢¯»ót1ÆOyÙâê2¬vx¾™ð®hÉ·û“k8…Ýa¨œèÛ«±D*{6 £VaouˆŒô­öÐNQÌ_`6BÖ©¡êr[»,µ\1æ¬x©ÞÚê€n?Ž[¸ c­dQ*w[Þì²±m_y<´ÍV‡_ü³Ó¡êŒÉ+UgØD»ÎG_™Ypñ>½-·a-ºmÃÀ†À,gë¾@c ¦ËŽQ¿Ùº/ÜKŠìv/Y0ëK÷…¬À)IÍpgõÂc°[šœÓÞ…ÉæP'#RŠ»ïÂ`ÝxïÙôcK{FÁ<èi‚ù®Y¹ta¬³èHs¡öÒ…Ñ2¬_Ù-‚ê{†ó«5ZcËÞƒ¨ôC,ê´'q߃ 3†-@Ç ’®=g¢åÁDô¥ƒWµÌcñìŒ9Ü÷`ÄÑ7ÙOÒ£Yz0šþp +&ŒgØ{0|‘’ +‹r¢îØÞƒqf4÷Å–Iôªf†TPþâ&ãÙ¯ ¯·¿œNã}†©GÙvP»ö Üä>NLN(~m=˜¿ÿìÌëDÿŒ· ŽÑ.8÷v±¤Ââó€u؉ n0ãƒe8rWû =È߶Œïòû6›°Ó3zAöË×·þÞËÿi–.­—Ñ…ÿé_wÄ¥ñò߉§Í?·ñ²¹Ø„îPg{KOÏï]lÐ$¡ÉiÕ–]ñ2 +3‡UyÄ ¼_b™ŠÂÈ«m™G>ëEY“õ›Ðyªoá†d1ö•íÒëdû&ƱÿÏ\~5¯Ù>5µA }=±àå8Ã¥&Êzuè¶Î§³¼hr:ü-,@{fê– h'ÌÚlæ5 +oØ“ˆáz=룲aç'Óî”™¶Ü¡Y=ÔWÛQ¶¢2Úaü=I¶*¬«_D&‹—Î+kµ…~Å&Ã% K±ÊÞêop<_ÔdµP–„ËjKƒÖâQ¯¯e‘³.6<Ûþ)~[½_]lH8t¡¨ y\/i +â# ¦.6Ðé=:´;zÂì»SáUÀ£‹±S§KÁ×}ç[pßùþ0±‘½»¦}`ÂXâMìÜ¡I^ªÂìyòZªÃGS¿—,Þ6Ó©xÌFé›mVt©¦FrÐÿ ZózQVæh€H + cÇ©ÇLüà +ºÎ¶àLg…8X{â¨Àv'MÝ.ò‰ ì©M=•f•×&|èTs%hZd‰‚?éTóã4/- +èñ¡aÍÊ¡{ËŒ8àV9ìíÀ³vö~ГJ˜æèñP_ͼªóïÈ™(äkh†¦ o=uÒEta¦ò7dcÞ©)>ÿý¬'>ÿ6ÅÇ•&Ü\›èL¹r.ï†ßáß=‹œsëü ciÆ5h;œŠf-û<è͹–ºËIübÛPФx`—ž CLi;’éC¶ìG4{‹ºŠÄœ†Ð;3Ì'QŽöÅÁF%v©f0ј~6"dgXæ½ 9]/ êÔÜ ¡äë¥BlÌ5ÂØ@ˆ÷ü&ÂÞ_?ßZíšçõvSyƒDò¼ôäx®`Ð%»`wãáX#€rÚ@·õ|]Ÿ¸*žœÛµ–aӜχO¢$Š™Ï_¼=¬®ÓÌb¢ô° cK®/cìëª!L72»çc‰o‡¶›W‡3¿G—}nÿ·û¦±SÛ—¼x«•hxÎÝ«Igé¼v9§ýeG<%MumžaFÉtФ´>·[X©Ôžmm¤“ýâC–¦Â´k+]n1ê„è¶Ûæ¯˜Ñ Ñt=rpä#ÄÐuµ×pÄž²mjB¨d™ü&Tñö‡Ôk}=1Jȇ£¹mØS¨y•âWœ³ðÛ‹j³">"±õÀKض¼b<" ‚V7ÛˆÁ  ~´1[gò« Œ¸Å»î«Öuºú±Æ÷ÿþÓ®¯HD šëHA½ºbgä*6¼%“úpÇä ÍÂÑŸÑAa· +-ýØ­¢ût f/;#vJ;ôÊ&`'k®ôTÛ#ÿ8Êx¼‰|ÿÕF/xÐ .ÔЕêpûék±^S½’# iZ_OïØ‚r1ˆX‚ÔQoffDé(õÎÌ4:øsšg”fÅÌOiT…™1’]jço¨q¹K7=êÍW’ +µêHf»4ãKâK™‹ï¹¼]Hôø°tɾ<çÞ…’*ÑÇ4piD¾¿Üh-kÐÖ³c-p.Ì=‡»m¥á¯‚‚æy{©9Øy’³@Ѳ0M— oF³½T=òf¹^%äd)ÓÌZŠEvÄ¥Ïu—9äá0l÷ù¨æè7ôüê[lWÂ*7ËCö#ô8·‚ë¢ú¤ äZyÇB… +53s“áÚrz¦à6O¾ÿâ“Â7†‡= +` Y”$Í;¢*Ð2XÊU“EûÓ4>á¢^Íß œ%¥r›Í M³ßq7 ðáú.X£üIȶEëæxÁ´]ægÉ<ßfU2¯âΡäºöEöçúÁw~$ãÖì.$ó^®jE׸õmwñÇ»ŒŒ¹p«Ð4æͦïtM´UjŸ§wS×4ߪÆû°‹[—ç^k¼i›´Ö|%7U®ñúÞjz]J`s)صÃƺq+=6Ö˜M¸l®E›æÙX·^tÒ|eÛÅÕÆ +x©±UòÖÐc¬¨›Ü\zwûŽ±Nß»vŒE}“©›«ÿ¶›;ÄÞÜL¦„ÞÜlVͽ}«º“î›ûÛ¦ü7öÁM3pî™·’ƒsƒÝ ·-yS=Ü·ö'ÑÄ- ؤ×PbSkÜ‘[ÕÇ-ŽÙÔ#·h +O^ÓFånmZ—[¤¶éenÞ­îæ'Þœg 17Л7J¢7£EÒßXÂè[EÔ- +ß„U·~•dÝÜ)»î£V…Ø™olâ²[–r+R»%9›Øí–m‚¹#§ºSÛéצÕ;ó´MÝwÉín­5¶¼pJ oÙ䭵ƖŒNã‘´nBÈ#¿½•Qž¹ð¥½ì™ò.Ö<Òê{­ç‘ƒ?¢=Gߤ¤G>/Dí©ÿî‚1K›ö('¼èf‚Ã&¶= «N÷UŸ“ù¾JC|-¬ªâ[]åVœ|+Ël"ç³³é£ÏªÏºú(m‚ì³´i¹oå§[Mø­xµiËÏr×T£ßjc·Zö³¢¶Iáo5¸UM-àÝIòoÅ¿UØ/.†kÅñÖL`«Wn^[©sÚlÒ[¼ºy(ìeÛÕ‡aqçç°ZðVPÞAhK!úÖ¡b«co§ÙŠãÛ•l#nÑ6j»3kÝ~¿»kÙÿ§´t ®‡¼´ö鱶(î§ÙÖáX'ëÚÙ¦ûÚX¹}iÖ¶Ìöæ­ ýå]ûA÷ïþÒMºVŽµû´¯5Kïê~ÉZ;_ÛÒ·5ͶUsë¹Ý-¹[Çn[°·f߶ÖÏáýV±6·gv"ǵ·-ïöµ½ë¹n‹{¿tÝQ·nëó6|µf×={kænÛýÖ ¾ ¶Vòjl]èœl½ëÛfë|oÑÖ4!ÔÚh¿ »¶6ý´mþåm¸€[‹ UpŘ+áÖÆbƒ2ìØbä DqaoŒ-<ßÀ{ˆ¿b?îS…:²§l²')óÀmŽ3®ÒŠ{Ù“¬6sŸ¬­ ›=é[á:?dÄ]Þ¹Zó×f´§À+Hé>•^1NûyVxÔžÖo0«ÛòÀŠÒºŠ ++®k¯G¬°°ûºÆŠ*Ûë#+ m/±¬¸¶çÍÀÀme 5·—†Ö´!ô¶zÔ +íÛ +Y;B𮶠×rÚ†Lœõ· ÏxW´Ûгҷá'·ÚàŠ¾¼­,®ÐÍ­.¹>·’憽+ˆn€ÓYEÈԭܺáYŸj´v«ënøÙ­$¼Áoo Êzw«GoÀßYÁÞà·uïýãkÕ|â“·:û†j¾-Óo è­Ê¿«×Áоë/¥4%™õ]ê¤KŸB?–À˜›oüÒXéÁPRâ}$JgŽ7ÎMýÆÁ"ª«(­‹yoÌDZŒˆ[û§­ÛÖ3/Y•„ê÷™ö®ÉF8gk¼ö”µ¡Då!+Æ=í¦5>õª––øAj»ßXVð+9±]^‘ÆÖ7‹ªkWƒ-’–I¦yzuª¸¯M»ý{Ö~ßu™wÆ×o[{‹û­¹ó£Øn±·3÷g³v?÷g|×<ݦÊÚzÝfÛìÕn“õ¹±»NõÙÞ_’µküq6—½R°P©F½Ã;šSÜhÙ³%õåÆæ ð0ƒžW•xCÔN$³q¯6OI$±î#ǧy;O2a´ E¾N»ò}ÔNoÍâú£½NÑýËèþ~«pÿþCî ö²g»©«Á»ÏäñEŽy·ƒ‹?Ä^rg‰íï·¾ïÚ]×)õZñ7ß»¸ñpØG­^ìs)j>\}÷“دñΗ‚ˆ·u‰ŽHúwç·p¿„wnü—/ÏøU9_(0v‘¶Õ¤ã}3í$­Â;vL”ÖoÜ*HsÛyc.QÝzàE\ë¼zIÌÓoî0ó¢îÍeÆOY^?~]EÖ;µÎóuN´ ló=Ns}¶H–àö½±{(¡;\:Á:î¯]¶êNÞr¯7¯îúCoWÎÛôëb«éf!‘†}öùi ‚;¯ŽqNµ¢gåö÷?µ~ߪ¿]õàüþãWáúý®Ú÷ûƒ¸K!öºjñÏ©°ªø¯ÓèÎ `û±«¥À6›WW‚í¥x65Øީ逰½«wÂþRßy0l‹Ã´pØÖ•iú°-KONÛ¢v}h]¯¯Ù–ÓÛ«Ü–åí׎ýº=ë>ðrS·md{&ÛV´=ÖmK»Ûָͱm{ÝfçÜo'÷¶¹o/É l/Úöco_Ø-`ÙÞûwc%0byùÿÚ¢RÍ(„wÊÿÃâÇCü Ès3ï[CÓ¶[xÑ–¸š{ÉòW¨¤¥:å)TKÀ•S{ad—Fe;ó‘ßÑé-¤h/rø›PsP>vgi°¹¼*DK³hÙ@qðFž[-œZ§ 5¨½ÏÈh +=}£fÍgT¶²%¦yçU›lGê]zïoD¹÷`óI”› KRY¶vÓ_»ûx·=Ù‹Äz¼]@vu¬š,@¢Ã|û3ÒØì’,m÷€†‚öa›ñ¼¹iÔu“ÎþË=§,¥T +{¹yb’ "2ËZ…ovFµ˜émÑOï¨rG´kì6ÕnT¹Õ&<¹ CAlN[”Àr$¼ºQÞ|hQ$Þ¿xU5ÞÀ:ò~#V•åí^®BÍû“¸|ޟ袽O…UzzŸQwÖûÄ\¥°÷ɽÊiïïÆ,÷öŠí§Y_ÒýRÖwýþ'b»#ÛÚ²ÞÔmeº}6saÛí¶&n³c[[ogÙ¶Fo³u_Ý×ÿÞæ0‹Zÿ5ĸw¿—b-⤨}ï†ä’Ë6Ê‚_‹BnÜZ‚4]m„%š˜lnH°á9Žjò ¦à´EjöÄ,¾8iÆÞo|‘€8tÚ1¶ach»û")`æèÆ´[}‘*´üxc‡TAPú·Ø†¬p·Cª§ÿ‚p¡Æ}›/@l’mÄúÒi›/RiÞÂK€Øú/RanÐ3¢0QÞóE*ó› +ª“õƉ¸˜}žnCq÷Eª +ZÔ1rãõM›/¸¥Q'·¦AªÜŒ‘5³Ùkß&#Ý#¡È *(U[¦Gçf’Zp²T’fÂî‘DºA¡Ãr# +/7fIàç)ÛÝWGZß´¹%qžbkçAtwwK¤^:ÝÜárã–´¾=Ÿýì¿Ì-IÉñp‰dd:…Ì*ï˜%uYúÚí +°ÙìÏ«±Ï'«ÀÜ­W’;7€ ª°6³$ÞlÉ/å$”- +¾uúã_h©€©Oð8ÕOÊ€²`KºøZÁbŒäx/;—Ò$Opêl Ì~m#ê¸;–K¢7ƒ»Cƒ}«rtåã¶E÷ÀH Om»2„‚D„‚h¥ýä ˆÐ81 +µÛC"5íÔÁŸ?›S“`ª2áŸ?=âêl‰ˆ˜1Uƒ–¢ò¬™ xXûüÍŽÇUø“eéüÙ{×_’ëÊï úê‹Ñ}”qÍÌb{ì‘Ý A¶`„Üê‘hL“EQÐÛ{ÿ֎ȪŠÅsÈiIÔX£Ñ…ÿ“•×}ßk1;ÈHd¿àbé Ñ.dB±s`Ñ[®Ú"VgÕU…EβâÀÈöÂZ4¼¶º8ŽµyQŒ±‡ †•Ð©•ÎXÎâ“ošÄ2Ê’¦²GÝJÀ3¼ìœx +µ†T,ÕNR­n+gI'ÑRË8 )Q{qQÛKÆÚt­™Ø¦AÞ+‰BÚÑ)–Ê¢S1#Ÿ"¢‹ÚzŠà}¦Ô×,L»¥¶R„WÍ&mn#é:“àÉbYS¬ûòùë1=ˆ„Ú©I´k~™µ*È‹íÆ'ó´è)P‚8ƒX³ ÎLc– +p–Í,²*ð.6‰z"D'åµ6eRÌöRxoŠ=p©âáò»H[uzêýÚl…—…m¢ ß]Y¯‚Ï|©Ÿ.gj{w÷Ò€F¥ À£U5e›‘Ûaª1¥b'¢ƒ8í׿-—Ûï&n|ŠÞñ+\]†ˆ‰ñ@‹ÿ®³ezZ¦&L‰s÷–Ë·ÌlFóeÙØÓ”“-´Éz‰¢øåÎvü`É8Fòhó!:óQh”¼;Õ¯ÒÿÕæR¨ü4›·³k—‡ýŒåÙ{»—{ôAÝ4 bûÛ!•Ù"9µ^EÀy|rHϺyB´Ï ¾û¸ˆá)»®R©b ÎU©ln¢¶uÇýÔ윊²ë+I0Ù¡¶£ ¨ÿØt;û:y—ᇵÃѤèºÝ|ˆÛ±¬/0Oèm+*eEosÉM"õª}{‚†'nÖœVãB“ù-%[ +- e˜ä« \jlíoˆt퉜=lÚÚÁ“C èØùq‚PivãjÒ^&¶ )´,š²„úÄžj Ca‡?´/®7Ë$@»«·¾tÓ¶I¡–Ù¬SèVÏ·éÐåì¶T´LöYÚjj–•u€ü„Î#óÔ\‡H<$±j†wQOhÏ©ÚÏ£óm'•ifz逯íd ƒD* ueUoÛŽÒO8œ3ÚsÐ9t©™ÎÁi/²¥ì\ÚTÌÞîsoOâNL {>PUëaž„¦»ê”¸Ê±ÍâÌô"›€è]h]6YÓÏ'¡AuL~òD›Pg¨p%ñ3!vŠíîXa' :½_‡¨s`½áøÈ`£ ƒÍ¼¥,+FQ.¨Å¨»È§©0Âm6ës‚7Ø|«”° +ŠŒÓ.ðjê’0ÃÙƒ\¶æ‡za/ZòЛè¶ì»Ck|¼Åž¸<„•*wŠZì2ê¸AxQ©Bªô ™-Â(í—švh€ù“WVÜÖå½L.(×î«ûpèéÒ2óv`I\°/Ù¦ÐÎ <½ªRÅ1²5¨:Š2׳Ýd\Œø°¿µk¿èèf‹„Ù#+Ý<2¢vY¸.."û¼™E ì ¸"ç£HeUËkŠŠö1| +¹ðY´TH +É—F#¸[ûªqÏt£¦KtœÄã/È9Çæ]»b‚‡ÓÓ¬…° ºLLc©¡4Ù~›Ú³.¥‹näË l¬5§°!U\Uç BoeºÓ³uä‰âÎíã%if‘‚¿TO ’}B§–•6W-Œæëô÷㸺×ÀF(\Þaösõ–HÊÚ`â“b©« Là݇¶ºTm­¨œ+¢!ÀzÓCÜ`ÃÅ0Ý2De ž¯EQ˜Œ jg0=Ríº<^¶~vJq'oóÙeWC¥Ç|ÂWõžQøØŽÑñŸZ§ =ì}˜4¥¸ƒ2…˜aX©j ËŒ4Ó%×:÷7¹-oŽíIt?&*²¤®†(£ %<±›R|j]ØwIZÙPðK\^‡•°i8ÿŠÁl·iMœ„í& Bk]k€7烖ß,ÛâÃíéj´çéýÔ‡¤Íálpžê ¬ÞÍ©f²ÓûùÜß }èhú)âÍÝb÷2±-b´@è.·Ól‡±¤æ)hµ½ã0g¨Ó‡e©"r8i#ný¢6#+xc{±#}O—y[;Eñ§XÑ ÂÄi+g¡Mq„ë°¤jÝ‹bBB2†Ñ;9Þè¥VyM’ÒÔ®HÈ$µ¾›¥@ÙVƒh¾Eæ{¢\WMDp’ôE2™Ê Íãb~úAÕìV+ÚœÅmUª è[4;Á´&¶¨˜|iæsÆÕ|¼Óï [ÍÎÞe ~¨çm5ÌÞsî瓤b55#I/à/º?±1Yemp—›P0ÉtFÓ‡˜''Ùž ì Dó‰ð‘='á¾î­/Þ“Ey‰R¢×p¼ ï«ÑdwÈs‚Øz[‹’¤ZKÌœ]Šøb™¶é¬Ûª­[‚rPh]:ÞÎjû‰;=’%ëE¬c5²-êtväÆú¯5â*È‚æØZÑèç–€¹è7§!:>ëÓgMÿÆëà‚] ¼à<¦ºñ]HŽªÓAZô¤¶"„=ÑuëFÁ– 7'žAŽÇ kD0:Á윢ÿŸØùñ©ÕŸ$¨åݼàˆÜPCøo¢Tˆ}ª ÎþA”Ä|ÕQiÐN¥´C´…Hº®á@s„0M:C %7ÛpOSêVáð“ºÛ6 +ùÿ5ò¤½l‰.À¤}‹%Æ¡á7á/2 :^"gêq»èR[¹"p jxI±Ú‰ÅHï±=&?fo@û¯Ñ@äŠ,ø~€‹µ*¶á»ÏP$Y5h°Éª§®ÄÅläöíTš€µ?¬e m”ñix$bLµÁN s×r„c+•í`sPQDL°‰æVš;!xþñЖ`p;È:ž›É:nâÉ(b^ >EVdî5M2CË4§ý +!l”"x6Ð"ØbÜM¢³»Itµ}0d 9;õ$u1„ ÏÓøme†óQïêñ>p‹¶©2‰~N`‹Ú“Ià¥\^ZhÑ I’pòê;­x¥íRñ-žA›*eqºéƒ(¬=dÛü ‹O]þš½QºQWÇvºçM¡Yt»±µÉ安$ö)gP×è´.ç(&=Ðsں䖵Bþº[dÿ +Ìž&lqhé?lâ§]y +¹|}ÀàTÁ‹RÏgzÈþ1cã +ðº-…Û¬Œ< ¼:¼­7©ª6+R0'Þ结è£ç_bÉ¥D½Ü$±÷äìW¼9\Â0沌ÍæÆfÒLº»Fˆ;²tXTàšàƒ£¢%«¸3èÓn¦[¡a"b}IÇ;¤R€v¨þÜ.4•,¤¾Øt²m¥KŽœ*Xç¨Ðw˜—ø@è¥x­žq¬µí6žî¢ƒˆ,¡Š +ßF¥b‚²-Ñi$¿C5® +â{”ÒÄ +p‡–—8–ëôþ®Ã´'--qN×-Š—ï¶KrîOšÝéš:,­™èfÕñª^<;$ȳÛ>…D°Š»LÉ"’7‹âd=-Ìkö Œ ò:" XÇá%láàVÕP¼ÉõÀ²žûªF7ÎÁ¥òºï¦Tx{{*!+JÁ6ìØo뼪CXØìlmsÖy ËôÜã÷ò€ ¡6¬„ëe +3ÏI»plyú#~*m6K% óµ«ôª”tØ1„§ÐÂ(=ù@ï¡Y¡»àÝ„ ξ›•(Æ@ iYåvE ÛîÛ¿šÎ¼ƒô¯Ôl“ž¿È'LÇmÕ8dûªtì¢+þpAM DmP^•8Þgiõñ ß)ÁJË|6ךœFEïôh‡*Jsçˆ*-\Õ´wh¦iŠÔ Ì+Ù ÷/Ï^»ÚæY¡‰‚5QQo† ¬Zm¢h{1b*6v÷øA¶Õ2»J&–³¡ÏÚjR±Ú0ýŠÁ^•Û`É\&âÈ)Zp0€PΚº¦·‚ÜI[ÐÂLreWcÒfnŸåÊÑ(­¥øëDÛ(Šr£ÔmZ]§““ÓMÕ®}fSBÑ66µÉ"…<¸F¹N0Í‘„ú‹m5ï9•„a²éLÑM`ªtÃ\à Â!ýÍ(ÜyçnT¡™qMMà*²^\– +ÀgΨ Ž9×ç¦s’9™¶šþÈêAiåMгò؉/Á «¡ºXùBQsôæÂß ÏçWß3J¶–:bZ;LÁ]oþ¡«æçòæ÷nh%ðb—àºG›Kpzã«ïl§_à´j¬Èµ=»þÎGJÝÑŠEüø*txlm•C¦ÍÒ ÌÜ‚{à¶ÒùÎm-$CH¢r/-¦Ïr‹ ¤•$éÚÞtÞÖ Ò™ 6äiÀE¿„ O˜˜‚ªèÄÊ‹¡š + +ßEGÎAÉÅøÒò©TM‘[£EÕjlPDÎ=ŨS¥Í†£I]¢ ÙgûßE¯*L <­@‹Q0¾$õyÑ­xzŠ8DÓ*»ªRªHÊKˆÈ™ý$9`œšÜÙ« ãyj’öÿ1â5BÈSÒ\ ábm„(£Mù>ð˜Ó8£•µ¿z¬ƒw±ÎÚ.¢ªf‚ä/å 3¨Q`ŽDâ®ØžŸQ±ߨpÇ6ÔGlO¨³ˆ·h­Ô71T||p£ßŽÁjO‚p 5,;JaVIªi‚°µ8[Q*y‚ÈE@S؇¢7"T ýª˜‚Ž +ÕP[]ªª‚Î •YA݆¯­Ú…bAý‡b³Õ‚5k[C$Ô¾ƒf–Í[(Tݽ±¤BÍ^°ÃBÝ_°åBýàÖ& uˆÁ¶œ%ŒÁ, Á¼ýfWIÌäP‘Ìí_k Keg0ÛgQè4ôC!éÖO©ÁßE­Ág µ±[ß'”Ö®.T¨Î ^X|bçÍŧV¯p‡r­RÞú¥ï ƒs +¥¿à‰½yjqÏCíwðì— +òm| Ô¡Ç·¬µì!Vkâ7!¯§á’PˆB.¡ º !{ Ö0ÒÓš°‹B…Ά5˜š#B8,4Yl£i¡Y#DåBÃGˆìÅÆ‘]„0ô„@cè]YC”¡fé ­4!bºpf¨uöîìâ³kãψíÎF¡5º‹¶!åФ"ÓkŸSn‡v©m<´]…`{hØšqúÐïµ ó‡v±,-g!á׶‰‹øž‘÷s!e:ﶩ—ÐÀ27± pÉùÄ^Â]ê¨Oø†b§Ñ3fK +JsÅžf®E—oÉbaicÀ,ºV1Öñ>±nž‘y¿æÔÌüú„FË7Éá–;ÉÛ°áÐk¹Í0¿Â ¨)¦etá—ÁÁ„×4c‡ÔLæ-(zÔ+,ÙJß "Äêa¡÷‰IÏ~9¡–ÙÕô†>)S™7÷)ÄF›j§67f\OEÞ +;Ím²¶&c.ƒ‚UÛìo|ÏšEŽcY3Ña&kBû»í‚¬‰ñ¸®k‚]=4aÖDýºÃkª?“]É@8mkåA8°¡z!œû·ÅáÚ¬¥ñæ­ÕŸïf"+@‡#Ô,<“‚ y]ñ\¨‡§NJ×þŒsaR3Þ>NÜKœ(–QFi}d›ºnpDU}ÐÚÿüƒ¦}0Ûñ¢²œ¼ç¯®SLÛº+&‡‚¦¶ewj_†gÂv«Š G|ý•œ­á(„†Ûftë~Z×É…÷ĺÇ)‚ŸŽ^˜Ï.ôëöüõW£®ßüœ:Aëâ6æq¿vpó”) zo‚Zv€T0a–¿÷扮?UXí þ\lÀxfÿPñ|9üÃ2ðïfêÝS' ovcmŸ2ÖC¸»ž€¥…'šð/3ïKý-ë mÑí8¢‘‰x¦'Y‰r›¡æ)¾øžuãX>·߽߼\÷‰4KQý€ÔŸ­ç¤A Žan"çP_õzÖH³42×'qñöÁ™µÃ,^ Z? ÏÂS&Ñ?Us¸ƒñþ˜¸útž*°aå*†âküÎÆŒË'ðF$ÄùÏ¿ŠK·È íª¯·j9vôûaûwþ´=hvœd@Æî‰[º½L›?_Î×ë—×ãýnwùM@}ÂÜ7#¹š·Ö_ÂÙ7»G.fôµ‘j™Š£˜SÚÓ öî&†·„E ãø`Ùçµ!Ò‚ÉiéGÛ˜'? ¯ßc) ¬Ž3>¥ʼnj‚ºÉYáEÄ$E³ ^‰þL÷Âi¡žf¨”ƒI;¢Ê–¤×$9ˆ%^í¸,òéÑ­]ÐFÆüóSÑT¨©áÜYövÕiUXÔ²ž©1èÈÄ«¢ +Ç¡xšùOùÒ·?Äê ò=ÍšüMv§¹y¼£§4§±N–¾rÀ-ZÕ5ãyKÃ’ç ³}C‰*Àáåh”dcȧ4êÌuôrò'-Æjq9ⱑ‰Àð»ª6Ÿ¸BSî=rùæÓ]@”a»$L„‚f®âò´4QÞDÉÉ‘;!¬à‰»KR)¹£yQ™­œ‡¦®¶ŠÀ¥.f“TåUI(‘ÚHÇýZÞÂ.jÖÏ_„ë£ p)½Îž#‚d¦[ˆ@lV +aÿ‰v4íWŸ§š +{2QÂ;n^ãø_›}g'P›cI5èæø4qKQ g÷•µ §°]µªÙ­¦eb=ÈÔût埫7m®PˆÐ ÓcFÏs™@u{¶Ò®õ>ÓÛÝaÒÕP)Ò3Ê`€¼.…á´<ÐHi§žœñ /÷øì{ÂXˆ’\hcº1(9žS9Û¤™túTÖ……BSÅ2$1Ã’Z<7°soÂ6ŠÚNŽN‚í%²3•‚©G çI$Üø¨4ê˜Çœ$i$:î´¨í œd` iþ<Èdäx!p„ÇdÒkÅA¸XʲvÜn[Ø2À9ß]É~xqç¼È iɪPÑùôŒrýi÷PŸN:h} b£¤ë‚ò$âøAüPºO­ƒ}³w}Õ°ï„Øæ=‹ ŒCYåév"AéGA¾®ÜÔÛ…jdÙ¿¨‰Ö#ÚîEŸžÄ¨\—ÃUôÖã~\¨`¬W1Nuw¥£¥²J„/°v>k5!þ¥™ `¿ì¬tóaǪú +@˜Žø7fƒßI-¸ZâÚˆMšvVœT. Kyâ| Æ9³´‰¶ÒQŠ;yT±Ø]rãÐd@á$Mµ–††æ^ €Ž<Óé˜ +ÅqÆíÿ wJTM¼Wuç{ûõqÀt‹Hˆ—zñ*”.·h¯3àÞäÙo”4 'm>qŒW¤&Îáo†ßzáYŸð³©s<Ã.lW”ñÝô^EʼnÑÒê)H½ÄQÃÁ§øÅd$CÁËW©ãàÁŒø?Î-üM8·P4«WMÌÉ|ÜãêŠÓΫI +ˆw S èå=Eu„ä‘„'Ô>ÍÎ&KU“ÍÞì'ÃíIò*$:ƒ©9A¯Ô×;Š=©¡ç>F“+)öñR€uÛ¯Cð×üc=O³×ƒšI3`H½€º‡Œ.©šB ‹ä:U$À…I + 䀌§¢Â WÌR<vš„šàú!ÍJˆ€¬†ñ¡ÖµË4 çJª2;GU$À ->à±Éªe†z”U#[Hß +ŒTnP[É&ß^“QD\SQG§†KGZÓ#Ï¢_9yv>Ó#Ž’·)Éã &îH^ôD'Óð<Îþá-ûæ«/À€®‰*õCÔv mBJ„êróÇ#›h.áh?¡sÛÁNœÒ´žœ°GÓûm7(ÎDÂ`ÙLv%™.xÌp-¿F/‚²æøJ2$=…DmW ŠAÚMG#õå¶ÏâТäʶ\d ªó‡z7Ø53Éu³ã¿¶›u›Ú%‘R×MÑðM~5OS>) ¼Ó–çLé&Ÿ‹-ADµ9påZeQßùù¿yN_ŸEu¿&¸¨È6É/¢Ð.½™Dæo|äó)©ÎÁïŽó—åa:Ê*à¤ØGŠT›bf~%”X=q@‰•=U¨Ô2+"Tô$ÒY˜† åšEeE‚8áŠhd‘Ú k‚ÛQ¹h´Õ}Q<"ÅEõ•‹ú$³I§&÷ó¦õ…?½¨ ÄTdäø½ç(ÉÇ;&ãvÈ.v0\¹± àaÓ^x÷a»Ûü‹ÊH‚å[ @¦Ê¨B£v•n'Q˜EUrƒ×bŒŸã/o¿:ú’Ðr‘+ýö~ÎkëÜÜpâî°}-Ol'Âþ2¡òæÏßêÖ€ˆyý`3!¯¢Cì£ñÃÙ³·çfÚtª’þ1E¥åfåLa@r§²ñ³~°h4Ÿfuôs·¨5#Ñî×# ØØR­÷þQHLFËø9esù3Z#˜eäŽÞæ&è&Î ºØV»8øæ=§#Dº'à_­cMhxÈ=oqN^íª0|jOàëýŠàr‘vINŽ–E5ÓC̵ ½Æ­1÷K\Ç­?Óo;Œ µÞ5yýý!9³Zì êïêæ°Q•©V£N3½¶wgµÏd‰ Ÿ"ñxÌ{ߧâð¹ÏCÉÃ5™GÂ%{þ|½–ñËëõÞÏ`•q%VIWs•XÛ] ’/lnšá|DáY”áëiÝèåĨFfâ蟂¸Ç¹S!}lи’s®°‡hÒŒ® ñ[™ÐƒßÊÖº9©a% *ä!cõ²tf´ß®RåÙâ(Fóó†áK™ˆ<Ô<‰‘‹ò~¹Ø@Š{E±Å!15 L»þAP{¥ÛiýO9¢ƒ q/Ñ(’€ˆG:†ÞªH&€YçÇ`ƒÕÚr&¤2Xã”cWðï!ËÞêæ̺úQš°==y¡BZlp‰B}H~ +W蟻™™Õb7;4#(32ÙÙçÆŸC§BxÔhò[qÆ›°eD‡,¤…ªž‚Þµíƒ`8Ùk +3Ê^  K‡ª:o”¯+]Îôcߌ½IãCäšSüv8ÛkìÓD-µ'@aM‰V°+\E¿ìݲƒ‰Þ‡&2ícPžRË=è:…_ÂÄ@qE3j|ž¤–Üu¦0Ù\¢˜†BÜJu£8)§J(¸Ô@‹ü † Û.óãLJ¼'›^"”§¿¦LÃÁ¹HÈ"VÄïE‡§ŽHÃæ²|‘[ö£QóPFëÜ<µD´=½!®Ñ5¨ñÈ€²Sw¢Ê2§Â.È' +Nغ)É{⨠h7s`ÍÒ ¾9ÑhÒëvJ`¡æ±7QêHšÜìê=5¿?Q2ٹÅδße†6phÐ>Ù5†Í”‚˜•šçÇ›¡jNáJ­U|0÷­¶Þ£æùQ¨y(íUìðV̯ìUùTõ£ÜÔ–æ´QYDhÚ¹)†»ËFe™SE«mw‹¶‹ˆÂõ0Ù‘Žk£±Î¢¦0ì• £WT]ëÉ Š ÓÙ^u©êÑ4Uö¦êºŠPaÌD§Í®oT—™¦4^Ý`OŠ÷j§¹d‹Nü‘m£º.Òª¬4é~Ôî²ÁVM Kîêè.Þs$ùFý¤E}úzÏ‚fnÉ¢¶´/æõäL@r«½Æ_šŸV€ J“|p´¯9hãoó9H¦H +~Íÿ•éêk Y¶Áºöºëw˜^.¨®}z|.ISþ *®ß|þ5ë-óêA“ ‚ÿ •àZIòBç*µE ämþ(à€;ó:S/øFoü´kÒÈ•U»RÁ$V»œžèäpIÑš ì¾p’ž|ŒÔ¥³fËpdhqëbJ»&¾9ôªÙ: [õLR"utPÝÞU‘Šgnoµ'Hi%'²'Ô¼"yE\>“û•’IRpö1¥àš¦ah§wYM¨yžä´ªÓ¸j(ágêlšÀÂ:Ááè½Ê3W;I–ƒŸîäõ”Ãvé +rì™NŒ‡XS²¬¡Dšà»ˆéjÏ"¦ËÇL@%'Å;¼Õ®UÐN©¿®ÛºU>\Èå³.¾Õ‡Ø¦mººØmš)Ò¡)ØZô {â=#Ј®£9˜:ûŸÞÆlªñ:ÀH¿è'¸€RUÚq´ÇÓ +v àšÖOÇ®Þ-˜©?±IîËÁ®ChéU­²)¯\øRñ’K`¡‹Ê’¥8é°½a{ÿ0f A®ñ.ÎÐ y.A-¸4•Pô??ÏÙkïOü°ÑÙ>gê*²Mm‰hJ;ké´¬ž%ÓžöœSWa¨Qƒ.¸¤1!‘o"¬º#Ÿ¹j?Rúìë'¹ßhbt+;–$(f«x™íP}­Pc·zè¿öP3-CÎüAeàuC9âÖø¼(mô&˜žà; ¨ú(D3¥ó |ö§—Å~ØŠ}ÌÇbUÜ‹ýú¢¯Ùº‘nv+*lwpæ’³¥8ÝûÓ¥”G£ïÎ\'»Sö&%ÒN¢ w;ÄkúesK2âˆÒjwÒâ_Åszis…÷#Í&›‘ìD+º} Äé[D½$º„„ÚçNÆïû±RjÿÜD©Ð nî\Ô﬚ÛÅMrž±8n±ÖBØÚˆNíE‰ÃÈç'X 5Ã$@¡afº8Zõ&°Ž9hû:‚¯þåaâ,¨{,ÐåCëøÀé˜ÝñÏGÔVÐNÝk@±˜ÔŒx±ŽéPõ);y‘”!ã I3;(UöÜc“8L“0FÍi7ÁO(í¼Xm¦Sµ+r„iW„e4y*}iÁ§<] ïæmÉ’$Z«ý€H èµ Š”’D!¼óD xqÉq±5`í¥•ǪÙì*[ÏOà™Ì Þ]=““ÝÉÌ KûŠòU×6ú”“¿YUSf£4HY@€à*(¤!Bl—®X5Ö¡¦"KŽ f_†Ö¼ +ùˆLñ`•%'DïLåéÅ,êõ%ŸÃ"3tÃz~âbÚ0+µ&”æÒ2þ®¦.)|õé ta"_ÿ¹Q +lv s33ìñNP¼íýäGœ›}]ÂAÅÏ)×ã ÐvBÔ÷Ç*®û—®ÿïÑÚ”"]×—´uÌ ñšgÒèªY]ª”6• ätpجLƒ +Üp¥·½+@ñ©Ê:P +­O æâýLû%^‹'Ο ¦m>·¼–#@¸ÖEé;´Áê R˜Î¼wLêʉ$„kŠŽÀg×w:UHÝ<¨´DÞ;à¨U‡uFš¢#@ŒA°U”ÑßWô„”Fƒ>€ƒ4üÎ)¦L¨‚B~ë):&n?!\¦/¥Ö‚r”ðÎ`9Zt„Ðp<=*·Õ”žK˜MfWuÐ$·ž€ꉡaµœ—€'° ´`Ô•­[ªF*1¿Œª¬à0í6¹Óz/'UH©(RᡯA¥QH5# 8½ôÎ#v}_Ô®µGpwï̺Q‡×#ŽD"Ê_p[€d¨P·ÚÀ6˜eæ¨0Ë* %Á# C,è/Ó#X@{€©b.u¦=`±Dp³:Ÿ¿Óy¹°mÛ”¶*•(÷LôºÐ}·¦´Õi N ¸ +´îïSÚ"(’€1“™†™mJ›KF•MÈd«„ °4°wDI´f²Õæ6ß»&°ÕæÇ;ˆ”×ØB´a@´9Õ„û>í‚ÝÞ°·M`Öu©ÔlÉtÆ6ãl.D +øaM`STxê’t®IÞ'°Y`[ø›Ô8úk›½½…çe=pKÖ 6£[Î4÷läPA†€2!¡²f°ÕªXY«Neí&ƒÍ$m:Rb¥ölæÜ% Ixï#ƒí”M¤…~Á˜Á.87‡8Bz~`É`ë=\ÞˆZHe³$~!. ð7©ì­¤ûoHeΖŸËFY}íÉlÈ,Œ¦{M¿bÇ|A.0“±¤. +:oÉe÷SŒ¯/_ÓºÙÉö¢³¨hù­>G.›QÃÄbƒ™qÍ\ïšÉƺöT6’-û&G&f»Ç‡Ó;¯™ìirO&R• §×Ïÿ_Xó¿k‚ú÷ ”Ÿ`qÆÑ7ɉDÙÇòÍtV[¾™¶·ËO ÌP÷D˜úÆC,@f¨)qÒð†X¾Þsƒ~ÆàG$ü]0_c¡ßËÆÒBZƒù‰®ˆžºbÀiÊO  ׋gŒä§ËíÔm?Á9Ü7qû°œî0mvÕlç¡C=÷{ +΄ÐK}ÅÀ½×“IvAš•¸§"N᪎k1°…Càž—KG÷Ó÷$ñ«; »ºFðS­Ì„>S¢<î¤6ûù„ðS鎎G÷z&j¼†ðUE¨²1“Íy`wÏþæÏ—~¥˜¸§ÏC¨Tk[‘Bø¬ådøbvŒz áÛ˜?Ýô˜vr‹!|ó·Ì: Æ°ÀLÜö±üd$(¨ Àà…ÆX>Aƒ~‰-4É)Ycú “ht]…P¾‚¨3Û³9FòíYÇIP*ŸüÚÉO¡¸¿]`iÉO8Ÿ™êNÂ?¹ÄH>õŸŠI~örL»ýßÿ½Â+n!ø<Âz¿°ÑÛqpSÎ Ë:"æ¶5¼´ƒ³t¾˜·çvu=v"×Ád+[IÛ† Ììa'½Úæ&ŠW§%J0[‰9ÃUÍü€%®êT ¸A[n“aŸ€Î¶»ÁÙ ·©Î!"m'Lïµgf :éFcã#m(v"Ì6»ù˜ýüxÜïßsÜ^Xj»\ð¶ô/¦“º”WB€›µ9S¢YQ.èà‡s.ðÓ)¾¬¶møห’ ß.ê{:%¢ëq9¤;ëb‡¯b^gìˆêÇô¢]¬óH—:@? s¼~8ÐW‡òǃq¸¢öd̯N‘Î;ùÅ#‰âA¤ÂîVÕ#%5Ûk†j{eWf€wßlªÑæò©àÐg 6ä<Í·ÈÔsÈ| ›Pt"Q®˜`.Ñål怳ÌÎ*ꮸ'η`*§Za)ÍNì< Ъªº(ÙM Ÿ’h¦­ÎÃYª—¬Û‘[¼ÂšˆY<…ö.(xékÌ`S¡'*à=ºAuˆ\(ÓOœõ½Waýö8Üm¦Msq +,4Â3ü^ “œ¾ZÆö§‰»@˜’žðˆñ« +„ôƒËÐvÍ,rê`ilÀŸ›Ïy*6L7˜<\Ñ4ÂFŒO–ì€ëìÖáQqëŠ ›ÃaYiM»Ècä"Â3 +µÜy6h(#°aW9ÓËB $ô4®KpÄr>Ç—©Uñ ž7Íj™,â$Ï8²Ô6£\íVÛÞ›8´©Þí©NÎ÷s‹åÈ„]êªÔº1ªœRwM8LÁŒˆl"ÐæÈ…>a㵕Ri—¥˜Lõbˆ÷7‰ÃOW•V‰%tÜciws @¼y¡˜îÆ»åvLêã“·¿/ÿà¤2®Pø«6¶ˆ0ã÷åØ_Õ<>‘Ù36êTÌm| <5"›öRÇÿ¿‹'óšÿØøÄ1Cù#Ù©"xH˜fÑ> Š´x"¦Ô:€ìKÈ^ï®K0˜H€ë!´–¡ +Ûáò̵“ݪà¼5çå‰O!9Ë…«.Sõ¬If3’œ`t¯™˜fk KÍaÈçðƒvÑìZeeª] Œ)êÂù±ÿÈÿ×®Ë!f¹‹`2ùaÇñ’°F¶Až≮vf3fÔ£!ˆG0vÎA×e9œþ&7)$0”®:Ü ‘pËžŒ6l,˜¶±àŠÛ¬)!„L—:Ež.M&FApSKn›PÂ_è=Ý1òÍ~Æ\³÷8aî È{pDÕ'LJlÔì9Èh(Ek»¤ÒÁí¯z‚»åqÅâ6P_z¿I€ƒ$”¸O±ŽaEüFıDGuU_ ˜Å… Ú¼ ö[±ØoFH^L¢°gâ§%¢G“É{\¢#¡¨\¼)À츑íi’׬Ì,rfQ(âÊpçõ=˜âˆæ˜Þ*$älöPU^îæf£‡@­Æ&•õÄb’ –‹¨Ñšƒ²šÞ·iÒöibô@D3AŤ—è1n§?ÝŒQ"¶ XTÈôÌ™·Oß³/eЃ¤”(H@ÏS3aî1Ô‚ã.ñ4†b’MøJ*ñ>A/#r¥Vu:aìø¦ž=<‰I^Á%ƒëæÀIéžîšÚªçÙ[/òÊÝ"–‚ÿ×}MK} +õ0Á€+N*AîCߪY Ž±à²•¼9IJ <¡þ$,„¯9›q&…nÇóÖ©!hUëZã½½ÕšÞiŸ2 ©·Øušäò‘Ðà~8u%éò&T2ô«`;X6÷ŽâA':–Õá<&Mu©eÿïÜŒ‚Ž\ü\ÃáSG܉§LÀÕŽ5s‰æos ¸x‡X½ÓªÍ6µ€¡ ËžI„Å}Ÿ.·å㊠H‚»oªq/†Aþ{$ôº›8ü°!P¨—/é âv´IÁÔPô¤UÛ}xùÁ—»8H@ð¹Î’ßtåSðCmÞµ…‡ ¤léo¿´¨¼°¨æa'YhCtÒh9Ç(o:KfV½Ç(ÀÕ@“Îaó!Xû÷)T‰ñÀë‘ô²£rP¢ã›>þq›$—>6oƒ±qʣ̞Y'&‹‡1ѯ‘]ÏÜ7Þ„:¥h2-M›/‡'¶O…ySS„™VŠN„LC¾I ’튇^ô°sè+Ò˜äÞ{Þì¾ ’"vB$`\~pxZ­Òdªis눜&H=h ´)d¾ƒl²í…P»nÓ—›‹E2E +Yš~ñnÂ~l«º½ØNÍBÒã‘Ä FmÉ*Yhå«éÁD5 ´Ïæuœjm_œÂCT§7=¯…¤–C7J°‘µf§¸=» +j˜ Jà‘#ŠÂž$ ™t³S.³Wš&'/G‰]• Á¬Ý‚£ýlQ_Œl9sÙÞ+A@¾mÚð¶*°‚ÍpiP 9ɽ ‡3€-¤R‰ºÓwºÕçYðªTÚÁ½ï]¯YÏ¥òœ;ÚÙÄ´{‚ê}=Mªi1¤1»u`µtˆù¨iÔþ?ÆI™UÊÇí¾#ŠñH²•,>¸}\–ÚeBn!¬«ªî>¸C: ‰54ŒR–…„Â>ÄŒ²ƒ…¦"ªRb„ZÕó`(äNá_Qn7ŠN7Šìøî£åFMPˆ¦OÊSã +±yÔy!Ò¼‹ì—#lc#lÌð~Õ†qýÕéK’tþûM8 VÓm®"¾fÍy°‚—:¨šj=¤M°o¼Ïá:³Í¾¨ ƒl_–ŠÞdqThÊ%Ë…ªÃM&HÀì’  ¢3J¾vÔ×*O¥Ûž¢fº +i¸²¡š4æ5“FâÄ/ªÁ8c*"5cLÇ1~†*I Í:[äòTêO™V˜‰C„[¨ß©ê¨§ñ +ڌƽ´óÅÝ “ÔõY‘uuK„ið&ÂHŽ’€t± œ‹¸ÙÊÔ“Ò0·BÛ°B«‹JÜ£J²WmïÍ‹sSÙÅ.uµ…ÌÍc„Þ"^¦?T+W@åºá,®*  §ìp¢…Iµ¿ìMËÄý€ýµÆKí¯WžH;ͪ«Ö×lo²óYa;;@øx§ƒXEJ…ň7/¥%VMJ&‹¦ 7è<óhˆµƒ¡çñ%ßëN(j²šºl9¿Aÿóká8ÔŽ·èqæx‚_“¥­žn¼wæNô½Ó€ ¢Cj˹gr´?°­îöÚ†÷Î|ÁT…|SµÐ"L |IR._-(\ÕK~¾Š±Ë¹¬–˜)r3Ž‰(Øüe‰­ÖœMÉïî!ßuc +Œd¶\/6%ýƒjê\ûôz°÷f)=”vN©:l*]­[Ê‘¼å£Rœ2ÍÛÕJ>|2ØœÇÕ4Wwa"k2¥Ï˜ÕÂï÷xåê‹{âx`xi| @;àk1¹õ +Þ<”ëp +™àÜ€ÎK•´¾r‚ïœ$ +‘Ù 8©îikˆ¡¶B˜î=°øi¿ñR%W=ú{á ÷,¿qóžÕC…ïA‘¡I´ÝÔus÷€ +Î×Þ‡`ýÉ íDžî—èOÇ'V·œ|†N!xºóH÷žÅK²×ôU|!2TJAP"EŽ¨‚‚¥Ú!9ÙÐj¤0§¢™—ãP;RwèyR1DÂÅ-"›ÐçÒãm¾ µPF©#h. U/!\Ãj×0»#4†}$j`<Èä‡[»F¨‹óXš HIá]Ü !ó4‹°6Å°UQËUŒ^';ÏûðãƒâN*ȈQ4>¤{-h¦îI䌳]òtB¢> x»5 'l’ãZƒ€ü\u¸ì¶š]ñø¬ïB‰<%…ÅO…šœ5‰ÎäM ÝW k:$êå¨×Èg„ð¨ØX +ÁßN'Rz !V¤•êñUùÊXÖH-OÐL>]¥wÞRL„S¼`Gà¦ü ‹Ëhò¢”/'ÁéU{Tð?O§l ^s#AƒZ†úŸ‰ÜË÷ás¤˜š…ÐOˆv ÂsÍ\‹VÊÖ¼Wé] _5DVÈæ#©×|€VûÖž\’Ô3ú.¯ÀkhtDˆ™ûàjVü`R¦)æ1ÔtítÐϤXò!ÚŽŸ˜ÎjÇèz 9¯ªûÊkEfJF_„š3ƒ`bié×€9~—ùÑ¡¥·Ò¾–ÒkÖƒ’.¥Bß ­9' @òr!]%05ÍÛeE?¤½Ø:5ÌÝæ°×ôZÉÏ ©á’,)8µˆ]êÉ2«§ÅLKÑš&ü!Cq¯A>D4êïT?éšUdgn-ºtË&;‰ñùL9ïL´Þc'º§•ˆH™5/ +.ÓÕ•½æ°¦Ív O•[—® +ém~5L8äzŸ³‘!.‚Nè4{ؖ϶¿‘W~ èøúùÛÑÍ “[óäqÖ|û~}C\ا‘jˆ[¼V ìÊZyNÜZ¼íZ±?ük1E¼Dk=ÆsÿB5Çöú†ªõò¯…%A|„•·B'Ô·Qjd‚¸[+m¶R3ìL¡j}‚˜5C[q¿–­J#Ô.½3KŸ¶j+TNM¥Š®‚¾ Å[[½ŠÀ‚þ…dÁõhoMˆPÎ,P¬˜PZ·µ†fe^0¦BQ_0ÈBMàÖ°[K §QŠ£=¹7»ô›]‘d0oCe0“­qª.3Ù¡¢3ê¡2tgï‡Óà6„"Õèz¬5®[&ÔÊW(”Ùw*ÖñîܲøÔêÝ…‚áà †ºã­£ê—W‡5Ö@þ‰ó¼yjqÂg9wðßgøÞý_kÈ£ûÊÏC("”±oC¡~ Ä‚úU‰õø» L¨æ±Ð0ÃB¡Ÿ`UŠ} (ÆÐÎ0¢W¡büŠMk-6f¬¸Øàñ&ŒûCÖ(`ì1Y#‰¡Ee‘\]B\3´Ê„Øhh¹yZ ;!2º~f,wö +íÀ±Óh#Çn¥5»žv1íØ3µÆÆCÛU«‡ö­mx>´…è~ì$[3±#m—iˆïY3±9nÍzÄ&»]ö$6ë­Y˜Øð·frbãà.#Tí À*ÕaKfI³(Üst‹Ù)ÙaÚë*à¬mŽ‹-èw³õݪ_SeàB`{LˆR$Óm•$²t—¬ƒÓU´d&6Ì^k1éÇÚ“Kt»ó¼câ»{vÍ:N-ûÍÈȸ˜¼¬¬0¬V3~„£¾æ@eáœ$‹0*Bi%æR«hvdN¨ùÈö¬ùØ–ºšÑ©Á#J·ËêÆ·¬Éá8’5Ág´&ª¿Û®Ìšð~ÖuM—«/&lËšv[»f±KüǃµÄÃ¹Ö Ä3¾«eˆwe­‰ˆ÷m-«ø|Ã/srÀ‰7mÂ…~îŠ-|\\­G⇡´ä*\„¡¯6Ò¬ ˜—mÔ©D ™YÞf^8:€35à›Ðw¦åüú„_òCÐ9LÌø@1EÍ’Éö=ñ.F…!Am©"úIîìþh‡·Üž(.h(̈u¦Iro†6ÓYŸðí«þ<.äíu Blnæsûñº‰ýÕ¨î§þ ¤BI;ÈhšUnæ³£VćÀqU¬çL +kŽHW9ü™ú+W¾^ÿ@„þÀÆH>ö»»Ûÿ•ªð +¨G´Óò½øÀûQ7BWïªÅ!êÌ,4¡Ñ6 Bf<À÷ ±_éå~?’&áà'“å =Šßç°WJ/þõºnaŸ[þïÞïÖÁñ2 G¢HbsÙplLˆ’û·²¯ëɪCE¢Ëþ_O~\Röü<¡AßR 5š¨˜úõð„•&S‰×=X)/ç¿ÿ€çþ_Hç»…Þ!vÆ…þ}Cè ð[„α:Ä~@èŒû;„Îb?tŒýйbìoð9ÂþŠÏößâs\ý‰ÏùùÅüO‹Ïðô>犧à9žþ«3àé¬Î‰§ :°þ¢Ó‘õ.g@Ö›Y Ðõ7ïy¬€:²þ¨3 ë Î€¬€:²þ¨3@ìG Îb?u®Pûï€:W„ý¨3ì Î°¿ê û¨sìO|ÎÏ_ŽoÞ\£w< ™$ÍÇfHÐõ±§Üã÷à‰ÆzÖ å.å,v0é;=K«‘räOª`ƒמr7‰é)Á -èëH¹‹àQŠ¼Ñ7Ú7”»iÖ@Øu†fzÄÜÎÝDÕ³Yov¦h~«‘s—ön­n Ú¥ŠX .l÷“¥œ»Gö`¾eF2®œ»æ ॑¿+ç.csnŽÏ1à`&ç.¨ +~h3}éyþË°éî1ì.*ÊÓÄ ¤»â8#Ànƒ£7’î‚­‹Ÿ_µ Î]ؘ¤©z"í;>´rî¦îž+»iiù›8¬´‰|õH¾»>á_ZYx±(']ôɅׄ—ÖLÓuÕ8:ÞR½u¡"y§x t¼8´TVì9„×BÌKg5çµr¾7̼ëüæ«“™÷ý*ÕÛ¶­¾'æµGÞñòÒܹòÉšùžNözeæE>P˜‚U{xµ Z’IÇ•™×4ð!”¥I^KEŽT¹»=ļ%v¿÷.z;WEð;f^DË[b^³¦¾pfÎË[!Ð:`Ê­,±‰,S·T‰þäÇ›ÙCË«Ný+ñ1øô²‚R”˜üh´¼‹ßðþ‡Š+òÞí4_d–\5_d–\5_`–Üj¾À,4ßd– +o0KnÕ\ – j.PKN5%wjî¡–\ÕÜJ-´\ –Üj¹@-Ô]$™ê.pKî´]ä–\µ]ä–\´]¤–Üi»H-¹j»H-¹ê²È1¹Óv‘lrÕv‘lrÕv‘ur§í"ëäªí"ÿäªï>$…7åÁ›€GÔw ÿ¤y ïé'¯À™X_ÞS&*Š3Ù'Íá èUßzK +ì“æÔus‚FÄï}ì ÷d¦?ðåkpð3G€ƒnÇ>I•àòÉ‹Mÿ²™9÷$ t¤Wug~]'r½ROþî{ˆ'©aT3ÞTw”vÛÊÆUóÑ¡øïFÛ)ÿ²cLP@¥ú¶s‘ë†1Í6¸tº+Aæ¼aL3ù'Ü8søOÁHm)Óª¹ Ôؘ½µòégˆ)-4ó½eJ3Ñ#I3•MÆž¦4fBxØfÒE˜ÒTÞª~t»Þç$U_©Òèù×ô’ƒ`¦4.Ø€xÀ Q¹MÄ ÍNAÇ–(MB93Þä}\(­²””¹»7DiÒM6ͯRÛ–(Í䃉x_97Diý)WWÌ‚niWÓ´ 4l‰Ò„b ÆO»±­Dž´2ê);;æixG˜F“–“¥PÛËž0Í»šw!QuÓ°6åÆï.‘0m{I¾Hùìø¤¾~"ìo,é áñ—°¤}=©½t@¦ØS{ubxT,ÑöMÚü`–Ï’)zý2nlšÎÒœå·þ`7f'þi÷Ê¥§Ös(ž/¡Jûf(ò²w\iehŸ‚Ò´oÒqaK»~<‡ëÃsò{ ƒ~wº4;ÔXóÍ,üL ­GD›Ù +ÈÚÉèD¤·Ò“ü­&ûÕéî2QÌÈìy3*ZÆùÑ`"²¿å_ÒlS?EReÚ‡¹2‚û¢‘œ`6ªŠ.Ñ5R@Ñû­^ØìuSeðd6Ûúë bRlˆGò䊽«ªÕÆ 1T~رrOOJ“6[†È¶ lâ4!‘o¶ÙÝDïÃ;0ŽÀô2?jVUÏ$Óu…6R5ÇíÅ­ž•ÆˆÆ]lƒxŒ*tá€Bãîõ™pLÚ +Uÿãý£ÁãÕ—²È6Ë"mpj(=G¾ý°ëžÁØ!’JwgÁy¸ßäï;à4lE>I„½})(ŽrÜþm±~Úfq‰ÐA„ð’Š­×£`C8uXpïN”™{‡'[hº6'ó†t¿™7”}o5¢6j“{åƒ2ØÍk@LÀ71ykFëP°÷Xär Tv3#o/ŸvÚûõ ·OØËãlí%®)B½¨… æã#ì ½V1Òd1+ÂÖRP4N³d ­§£‚˜¢÷˜¡e5œ2<uøš v—özÊ/˜ ‘š‚ !0,‡›ÄžC°çÔÞç)F»G¢seÞìõ^…?w3|?Üñíðƒ¨Ë¤LXÊ ­¶[¤^ØÚ 9ÃñˆøÍéšr;É(ò×cý9Í1SdÔWÜNƒÏ´XÅü€×u!Ü=-kW.c] uˆ£ÑŸO!Yn75Ú(\ÀEfYgÍ™ÕÚ% ˆœÐFaF¼‡j_ªº s1Øi"2s„ qQÈMPáÅ\¢Œ“m"AÐ=›îCÔ2*¹ˆ~âv„œn6EA*bÔ+ªZRýCŠ!—m‚‚¾YÊ.ÉÓ vTB] A‹›ýBtk‹6‚ºª[ATdw€™ZKdc±-~òO3=>g¿Påð‰pxw”V|›õï¾.[Sþàåÿ³ùD?ÿë—÷“ŸüÑwßýýú‹_ý%ϺÿóÅ!ÁÝ9úWìŽ.Vª®åi¸eÛÓ•J”‡°«çÀR2UJö ›;}¶aFQñi’R_`̔ё%-ˆõA‹C_›`b•$%k“K©Ÿ2•0‰Tø1òfo¨“YcòÌôf²:Ýw­dd&àT€}O{¹Š¡³ÂÒÿ;òGáo I¡€¨Rº&ü¹0†r;Dâ!xï[uÔõšÎÅÅÔ2…s4x§Ã(Öà-×Á1“”Æî­Rô§GEé©HDæÉG¥sDs6ñ=eOR&ƒ8ÞŸ½™Ü!L¢BkoÖª«}ªÚ•L"N^ÆlzF ²…1#°ÿ$6öI=ÍdƒªW— ®@wx!#Vª )+¶%ð….44]h]Fáå´(f0UøÚR“£6íHžÃ‡_°•¡›75[ôI…gÍJ] +ôAU}`ã2S]û õ¹_UøÌRÓé™T¢å4!öV';²#“„¨GIÜå°O™r1xC1¾#“"Ø2³®Ûw¾ejGoH%ª²°¡ö°ã4P½›âÖ\;dð ÓÊÇNY°yf`—ƒØªÙØI`:ç¾80’².ƒæÛÖUµYbÈ8üt‹ ÊnØ»¢°¡œq7 +ñÑ<€M‚¡¸ûBæ÷%Ív¶`FåÖû°|.Ù‚«5›Ðü3›^–Š¦œë!jªžªS\“õ¹)è6•ðX"Â4 Àì˜ËaYNÓµG|nÆtÛÇnëÈ2J"ªž™Ä3ŠèŒèå#!8-_Y©â[JÈ.‰Œƒâv[V"¼â˜,[3g8g‡‚¾«0Ë(ŸðEU£‚ÙM–ø„ŠD@ÂãQã‘Ëæ5Ðê—¬J¥9|j"ämn„HsÂæþ™€B„ôãìã;á¡î`ž`/_NH‡Ð¢×²JJ­ôsXÚ”‰“ÿm´ÝZ^ςΓ4­Áéd³Í=±KLuó;T@ íØûðç?&|¨Æ ¥ÚêTý®øû9Þ— ½î½£ŒSe…Çé[Y[€–œÏ‰òsgNÅ)v$ª4Tš@G ·< z"4Á“2²IÕ> |ž8{–Ñ”/”—_q»à?˜sK%´8YЀ6˜«ÈÓ¹áŽÊ Z@Kowý´ï˜^ì~Güôgª +î½qSÁùÂïó}nñÒ>Ÿ +Ò ™ +lë¬DirëÐä™æL”7liGQ 4çE©«Fõ¸ðhn'RÉT`g2I4ï6plîàÈØh’WªÉ-1#9 .[ŽÜQâ•mÁ!ªÕÿJ©¾¡$–:Ž|z«!7Ú?”œ_L>ñ):XÇ€é±cISS渼ªÒØÕ„Ì wcò%D»(˜ÍqFW<Çît'r¨$»ïQéE›Gþ¾R<¨ú¨Y1Åî-=BŸOÞ½¯¦|µ¨}@ÔP¤Æ\žƒ|A¦=ƒPÉ_UM_…dƒ«’Í,Ág¾rþèµw¼ûìÜ'ö¾ûUÍ+2l…`ÍIGw¢ß²opJ¶9ìá­: ¹d¹2`›8–©;r™ëN걡½¹FXÀÁ¯ÛEi”g!\ê+v£Žh'ïî"9ÒKC¥B¿p›©ÈLG¥cX ‘s— 6×K…@ä‹OG¤¾Nîª'VSÞÛ>"}»ME +¹,ß&©\¬©$Íh øLJ"­Ô¼Çš9 +¼È0p½Ç…#*(9CΖ´ßa{q€Æa%ÞÐeÔÃ#=áè:©:Å^jÕ_AX8¹ö:¾&ÅñÇ}Éêà:ê”y\Ž£ RÒÍ*x·» XÌ¡`ãE0¥Z:ÆՇѻwR@²Š&Cüƒ—µKpÊßviÕÞûá23íånO€§%î +U˜cO7ñ‚[<ðÔš£¹`öv‡K~/êà'« g n~àôÑ ƒR´ª)+`tŸ#ù:ä)”+ðª§shRÑqôº¤™˜ °-¶÷P o[ŽgØÅeXÑÇ$ZÓ\ÃWõ.U¶©¾fá©u¾ìžšajµîbiÏÀíéÍë·Û™½Õ:OA7S ¢8x$2˜]O¬c Olçž +kƒùJwçÛÌëúÖCÅú漚KåOI®isæIÔbÊ›÷Wp¶W‡I.¢Ë]Û]ÁZ½e¹»f%ùJVÁ$€ô.žþÜL³|Ö :NÁ +ßß½¤,»‚¡Bn•„ðJ¶;ÊP¥½HÁ—XÏ(Áú›˜üY+H·ðO`/7ÃäÊíþ@w0s!¶j°/Eų*­ >†Ü÷ªÒXu@¯šL/ñø,:–´•7ZmÕg‰ú]ñÊA9ñÕöæ±Ò{à:¿³Fˆ}OÀçw† nfëiouAlmÌ(õ]w´ÀlÝÎzíí·{ܯÆ_šN±Y1v(ëÆ€4X ª&rÈÖGCT» º§žJƒ³ ´à Ò®l¢Cd‹Y ˆ1 ¸ÒÕªègÝäš×¹5æ"%S +óM&Ø\»övPá.§`\ÝŠ<ꔄ®ÞëÞ;IEɧǥÁ‘G>Ó­j¬à …'¶ÎTx*øbŒ/fæÆ¨Ø Ôè³­7˜½ë2¸’¶2ÀÎ8ûŸy`ãÒ†‡‚g ó'‰3Ñ|»sÒÁf¦¨Ì–ç¨æÖIÏEâ#‘7ï)ƒ#ùdz €g: Pb¼!×O”)Ýã@yêPr›ØÍé:t…@êýA ›$bMA\›PLöî,¶¾8”ÅÍ7Z*Q4ˆbP(Qæ‰)Cȯ§[£‡ 3D£LfeÇöÞ³®Á›°FÀÌ6)bEYƒgéd}”ˆ ,·18÷vë¹»9nÄâÖXÝwÎûPáYÆ ÖHc#ÄJ+ÁˆM’5‡|rÑ„=Þ­ó%jFuï1DRA“û»ø«³KÕ¤KЪ¿ d ÷JÇŸû1¦N–°„˜AºH× IÓ·ØxlÇ>ÝŸè÷©þô+Æ˯&—skþÄÃó¨¿ ÷.žO\À3! G!úhÍ!@}Î +¡·Ù4™PZN +q(ý\³:AÇ'ç‘£;ô¤z•ÀºV7Ú¨ù¾Ú_°ºb®fUçL®¾?Ä궗› !ÙŽMóc€8(0€ö¶é +‚øÊÄ)ПDù*\ÈÜ,;?2h¼Y¶2›\ÚI¶ÀP fíÑ+0¦¾l@щ*¦ùÞÚÛÌs>_p^Ùc/Z¡´…&m”ÃÓÓ=ê^ ‰U۱Ж ÃzU¯®ÊAhû ÁDÍÓû0Ó¤ÄÏ_ƒûÏD™@ååùDŒàP>BiÊ3.æŽø¥˜“°û-àH®)ž©_£' < “Ö„ÂærÝóA ÖÇú +©þ„™u½î +CÂ&:è\ͯc'Éæ¢Ï(Ïà_Ž­§©¸Ó“¶¦yNÔmŽô¿/%ï¿÷¡‡pM€]hˆž³xUG4]oÛå«ymÁ¡‚Wãs.XÅd.þ¼9p $`í>tÎûª¹Ú-(BÈãmó–DNÙå$ð <€×Ù=î=Hõh$Íïïn>¦„ˆoˆÛæeJᨋÃΤíÆ#‹D×’Y-wõ[ÀŸã»Mñi‚°§üÅu2ÑvYýègµÜ !A*vØ ÀÀÇ›’ Ê.ÕöÚêO1ÍX ¼&6Ö4ïþî´çfh Ìp„>mj(>˜çÐUσE òKØ©eø7Øï¦:¢{Ÿ;c[C-Èõ(1A“WZ)—~ÿT}âmÁŸ²ÿNúT‘ìI bivÔ°v¸Õž¼†† +šsl¯pŽŽÂÐè4%¹"8¬¶®Ó ¨˜7l 6…2x§ñ £xÒÔqº¦ v¼7À+AKz4_7ǯ£9дhqûõ!Q¡ô—ÕâŒ\ð˜.¢ñ`1CúO.² v§;é]Wå7qü;…ït6-¶!°MºÐ'0¥iZÞÉáZ“¨ÓÞ[ S­s‚y u4ÿK0nAÐHôæ£OUã±Éá‰ï†eùÎØÞ¼g1ØÙZ £.‘§Ð Ÿ "/pSsÔû†¿“µ\Tçî%þÃâ¶TØ£:-6ì<°P‚ûƒØÁw½Ô3xäèhU¹|ØSβ¸y‚ MÌ gÿÞ»b¨¾ŸÐ=þÓ R  ²1Dƒ‰T¡±êÅ0—ƒ'Š8$ªvDP!‚_½Yž05Vx“bzÂð<Ô ´‘à ÒìïŸî³@lìtM?H,¤.,/«“Rtç‹\R~öõ«ºD1 ë¾C¢M€sDDˆ%”»ZCà‹—vŽY /#ha²ï¸ðBPô2:Õ€ýØF룛ÑþQÊ€ûŠfžñQÆ +˜´…È«2$bZÉ©—Ø"7c<tÒ„¿× ñ»R‡ö{™l#˜TE¨ô‰ªé^fð  "‚=÷‰XUˆˆ3M¨üÓ@èa.°ãñæ ±a«_f\¬ÚÿqHã$Ú9ÛMo„ +Ì`0]‹Œ '®{ÌJ3ù&zØ `“7\ûÔZÝOèå]ä°²æñÌX#;í"öW 8‰¡ä3æ£Y@çnxX“?Ê@FÚñ(TFÏ8¨¾L‰(»D’v¢ês5ës6›,X‚±ÂN4ݦ—Ç«É21‡ÂÀ²…¸E`á£_pÆ…€U°¶ ZaåÖð3 ê&ïÅõ†Ql1†T€áÛ Ü͘À íßDN”­¾„€:¨$@HصE¥— ÅÔ'®Â~9Äìէ䞸`›NÍ=gÌ+€q5ÏÈš•¨êÓLì¢òª1ÿdJ•Hñl¹7O6€‰ ’@î~|jeÈÖS8Ÿà€Tð‡Ö¬O¥TÀAÆó93$Kö(¾gÍ@ű„'¶s +O…µY³jq}×ìÜ»ÝYr{ak—¤às&Ö”âþH­©Éx2×ôf<ÝkštKÖtk¼lkÂ6^Ø5ñ»¿ïKþ8Š5%Î’ÉÞÊ­5¾J¿Rr3¤æ·R7¤øƒÐÕSÌÏš‚·*a-Dˆ +eÔ0UJ ¶š,”REÊ1‚ U[ <‹B‚Þõ$«Æe)[s!”·c#”È;%”Ú¼1nB¡N°ˆB±O0¦Öš¡­)J‚!Ê—‚ Ê ‚)ùÍ®œ*š¤kYV0m=êhÞ•wEy-›&u¨/Ûâ¡N-˜ñ¡Ö-ºkÉÜÞ¥X+î‚g2jõ¢+3ÿaë Í ŽÔZR±µ2qïÔ­ŽÑ9\k$¿à‰­ŸZýÜ¥ê3ºÊkñèÞå^‹Pã{ÖBÖèþ‡‚Ø]a­«1ˆµ67Æ1Öß}¿½Îp ®Ëz®—£@Ñ/¦¶ÿx3¤Íæ숶½‰¡ôrõ§Û¾ùsßü׎ÿŠ>ÛzŠbh lB&ìÑÎÔŸë=}¤ +#¦ùHuP$tqx|f='Ï Ñqdñá´%ý݈‰8›Ù#µîY ¥ ™+m—Š˜:Šhj³³³`¶ • Ä° ¹ggóàCã©\)‹™…ŠÎÓ•ûk@åÀâÏÅHþ4í +á£MàœÜö˜Wš¾ÉèKe,Y¸}¨-Þú›qÒe«æ‹èeÕ!)gpž—‚QtÀ-ô ´-¸Ç98BL›5r©’(²N© N)Ícoþ|`/âr…c!”¼e¤ÜÎ@#NÞh _÷‚öM`”Í{¨meìZvå–‡‚Xv‘UcÈ’vQÞ¨ºx;*®`0Qº¯BJ¢>ã ·<Ó—EŸ¬Ò™ÅÓ™ì4ÍÖ@ZÓlM¸4OGWČ²;¦¾P¥qGvDxêAk£[•–6ê÷i0OUƒÚUßt€çàÖŒ24º.&\NP‰‰C„8;)î›î[¡êíÛBq˜y âÁÞÔ(̲œ—.}e +’ TzÈAjyªd’šü½1üE¦8± ©8ѧ-!£4~†5áô¼hW¨÷L§H1Ò1^‘š2¥^Ô˜ Â‹B‚6$!óÌlqð]•ÁPBÝ °Z×D1¹™ÏÝ"KuP'Ç°Ãö¤UÑÇQ:ÍJ¥Ù¡$G-µ[5XÒe+¥Ùy}ðJmé^~ŸmbLä%S3×¥ªQR§•h’í“è%'b cWtî·ïlÀŒç·¶Á U`ô†[šb;q„(esJˆÔKLeTxõÇÂÍíà¸j½¤n !¸¥†Rl„ê.«ÝÕA“(Îýdw +TU³­QT‘h©@BI)’ÕØŠë>;¶ÊØ؉Q•E}X¬4ç#!'r¬}(«—B-S99d­/ìM¬Ÿˆ*çú˜RÅ 4Kþîäª(T$€í¬Cí{ÓbÙEõá½Ïߧo¾ú Ä£€zÒÞ— +®Í~™DÍs¥‘e§Õþvz`¡icŠ–ê [Ò0éð}–-«8 Ž:Ô•bvòÍ“ÚVì¼:vx_}ùÃ?6¥ûæuÿá¯~ö«_üR¯üÕÏxô§öû½¿µnlžÿöÃOÿäû_~g*ü§ÿë÷Céøvþ×_þ?f&|ó‹¿ýÇŸ~û‹ïÞü ïùÃïüÑßÿêÿùoÿò»Ÿýü¯úç¿õ_üÇ?ÑË°ŠoþŸù‹óýhüòËïý³ïÿÁ†ÿ_ÿîs¯ü³_üíOöóŸþO¿üÅÏÇæ–’X&ÔYòáÐÿìg¶DÿÇÏþêW3þБ ÏL˜~óþÇïö×ó«Ï ïÛïÿ˯–ñ}ü0»l[ö³ïÿîuÃ>»Hú‹ø»í©ü_ýôÇå§ÿáç5þ’ÿÜøÏ?ýO¿øùŸüòg?ÿ•mÚ×_ûÏ?ùþ¯m˜oþá«ÿô·üËåÿò“_þýßýÍ|ÏcVö?øêxù#ûïÿó¾ú{û¯i/Úÿhÿá±ÿãÿµŸþ᥾üo/ÿ×ÿ}¼üÏþéW_S_÷IEâü¯ì3ñÖûüíÛ÷¿ð‚Ø1üöÍßî~ûs æÿ + Ä(&îI 2ÐÿjMñ Î/u}b:˜\–qJÙ3 oçIèîøô&´Œyú¢„É V?¼³_1_} +‚2é*Ëf +lÿZ‹yú1,9˜:œóW» ¢Ò¶ÜãYºÆ Lûû[Í"¿_Â׿ùê¿èÏ tNúý¤{Œí ï}ÝŽû*cz‘Ÿû`¾T6¤‹^æ»óUÆy'=4gx–q£I†ÊóëòŽ!uÔÃS§à¨ý0~.µŽ¥>¦ÐQb EmgyýyyÇ|9X‹óyy+?ŒŸÉšùÏ èÿ±W_¿J@µŽŵøjÄ|3^pÅŽlóÙwÞ‡<Ž)ôM‰˜Žô¼àÎ×íˆã­©§))mi·S˜Ç ¼ùšÆÏKcÜó£ `¼ý7 gç¹#è=ž-¥ÏÃxŽ #øæÙÓ#]óx‚š{zÔv¾¶üx»ZÇòpÓ»{~îìeÜ)zîÚ9_@kÙÜþ”^åï¸S äã4låe;°×§6¥j†õ‡ßüAš­ÇÏ~þ£]©)Äi_?’Eò€;½ïùè1…Ý4¼t׫˜Ÿ‘Ýù·ÃŽMU4f}LƒQ³þ`)æ³uŒBgæyëE{„Ö2‚).ÀïãÍLî‡ñ+½#ÃЫç5eܘóéfµ?ySTØo +ÔÖî!êó9ìæ›Æû)VúÐYë¾{ÖìJÇüÙûÎ2G&è¼)8çéEýš+AÇy­LI­êW†¥Í‹>÷íëÞÝuèõ«ßéGx~ò®·yâsZ(# Æa•¾þë½ïçÀ¶^žyÏŽP­1~Ëe½_$‡:ï×q>#;ȬúȦÐMo7Ó§»Àâó±¦pL˜?ó‚äa’Ûo½¿ëš?^yÌ¡Tq$ÏŸã“ç1Ï-MÍÿüñ2¨×óÑÛ”S“i®£€¢ÇɾçBÒd7žEùŒŸy•üF'Ö\§¡AAíØâ6W·ÜŸ9"§0?ü[Sùä‚Æz;þW…x?“,®©)Vi\ì®Í9GžJî6¾æô—øùžVß=®<¤½e¨ê“ úvßË•2ÃÅö0D–>æûZ}¢ÍIŠ SIt ˜3r]!Uz=Zô¨%qü5~…O4ùZÄ2õ{QÛã?j©GÉoÍü>ÛŸü€õ—É(ì¦m³9œ#:æ]:Ì_JQ5(à M)!`΄N×”üþx\ùB†-/‰S,‘ÂŽs¾ È w$Ù´|²ÏÇ®ìP4G:O?V¾Sþ×h@¹:€l™))à>ÑÑÈ¡)®Kð‰ ø‰¨‘I‚p…Ž¥À—)gáÇg°>w8r69X ¢LgK"tì(md/>¡­«ù +¬Lã*—ª#M‚uX~<Z4{#Ȉ¥/rÑ@ :Êpµü“d‰ûèÔ¹: ®)¸BƇ¨‹ ÌLbÓè”gB„ÿæªs‰-1žå»˜pT‰…qv>ù þ·q¼uÁàR b~·ø6A¶˜’£"¢%ˆ®è©ˆD&†‚è£V$:2À(Ⱦ òY=êУö}brfØÈ"¶ñg†-`] +îºrd êJe‰ N|¢fr \±W°?3$+£BZêbårvü€Ú'f@Žh,pR:Lð8¢vraˆR>*)èãýÔËŸ’”+ÞFHŸc¤º™òË¡¨£K€?ŦIÓµ›õQ¶‰K¾c!7¥Å(š¼~rr,ôØ;R4‹õ} ²«›d"VUL¹BKb™0ÕÒ÷$ˆœžu¥€ùheµAk åïëR¦§œÐ ]‰>òæpú—‘pÚ$C9µ4iŽruÜ6ÇüÙë×óÒU¢ˆèKIóÒän¢‹SqŽ¦¹$ÈBÁ[ p[7Ônr‹„†¸f‚ž%\ÀL?á㬧f«Ä,NSS³ÖâÈ4iyL—Ò<°îïo¯@v›žU D8ªîüíƒáªG±Kœ•J^—"}ò $£â²2\,Eüƒ$›2‰ƒú¾éƒDÝ·‡¸ú^$Œˆº´F°ªÑ÷ÅèÒ“Dß‹†D‰Ë?žA?ìøÇ$.pTLD5›È>£!ÑgvQíçÛ¼H´9 €2 àGfYSø©ZHô£5Hó³;bÈÏKŠ#¬µõ=|w].Á±ß×À‘ Gòª­ÌE=šÍÿö 9óyxsiÙ}ü:œGô½ØÄ‘6Ad÷ ÉÒôIJI”#Iv ß0®q1,uðc&ˆ§¤ÿÇõÁ¥Èq,ú]H„߈veø éø`‰šKŠS£l)lÇà,]÷ýéIv5R~†ës  C>+*K}"Ó4i=˜¹Ýü©’9÷17äú=Ä¥41ü@¸è ,+óÊvh§Á+é@[ÇöýŸ2˜$jR ï ÿŸì¢5·º~‚’èu' î¦ÌbÀßË ê´õ©GcIšwX½amý‚&š­Oå1â*h~<Ëe¾ +Äõ70r l,˜+š˜Ðïu?AÏèIË%φ«òæpX›l]ÌŠ·ˆÙmé\/£Ý +™^fËqE_g×ôCÊȵ§L?©ç ¹ÖÅÖËšde›ñ@÷ ñòmGR¥ÿ‡e¾OZÛÄょ‘ÀÇ5äaÀgä°XZ, 6`:4×"K “Õ¨:m$ˆB5/]7tbÝ’/ÊÏ'bQ¯O”þSÿòA¦f Òø2¼dË'b«X¨º.‰2è$¬sý ÂÐYÛMþÒØak>¤F¢\ÝwÄùDÛ±ål™â›R…?€~.ãP¢]„¯S\9#ѬÐÿ©Eù{¸®Ÿ<ébÓJZJóã>Ñrh]˜N'`Ñ°",Ú!²KþDW¦ƒá7Ø4/[:ñ°Y¹ND¦9†×P&@1RÆå"øåAŒ*yD¿·(¨/p”U=Ñ ’gºNƒÚ~Ù‡0 ÔÊZä*U²—©²j¥¥í)½¯õ¬cn>¬'’:™­Ó ŽE_%²ÔhS–Ûðõbc:á؃MÅ ¤è¥òÑUÝ"õãP¢4<-2‚4ű;‡C@PHŸH‰P~3²4y°šd9ÃQ¿·É ´¤©êh”ׄ'Ì÷2$„)…†|Ñ‹WFH¿?ÇVyWdYcºEö^*ö·mJéHÒ5å1²ÑZ³‰½$r¶eßáÚú÷kN´8½ßP•\öø A;C +ú¸$¥ îëJ…¤³a—û4Cš)¢õŸ¥~/³A7@3>k)«S*:¼|Ä&y¶¶´°‹FÏÚº¥ž5 퉫å“â®'ÿª\¿Äÿ,<ëI¶eÞWÐFtoQbÃðá±h¨°ÉM5®á;›Ä©óï$m®äƒí'ÞáýÎ鎥VÆ¡€2,£ÄŒ*iê0¯CB\'µÌhÁPÄp":„˜° ‡Œörr!#,Zµ¾@µÅ½%21)Úk[*Ç@´"³ä³ºiI¼áøÞ:ìV#ù®ÏÅþïMR5"õ–H†3.ýžwä³ëXº&¡6“4•ès|€g[û/ªÉê„ŠÜAŸ`Q®!ö:¥Qu’ô\eÛ&Õí‘'GUæ½%¥Mž‰>²º¥€Á$y½E‹O÷gð»ùrNýH¾h)d’ÁËHUØ&ó ”lªañ¤Øü¨ +®¢Kª•2 ðöCÓ „¥L[ ZëÒ-ˆ?ºŠ¤ˆD›°·«Sì‡u”ˉ䙎É„’]éãÂV˜Ìøˆ½ :Ð@xšŽ†i}ˆÕAm#? 7Z<‹á;õm™t„×¾ù¾M]_/×ÚÍ‚rɈ¶­$×)—G5È_Ï\”¼ À÷*§M\ùNYƒT™‡]Õ-ÂÞ½îM›"¶–ÊOÄNZ.í˜&õ¸¸œ„Ž3¹-pTµ¦CùH6È ‡L¾LÖ¡Ôß\$®5¶ë®y‘¾Î6-õ¬K¸É’Lk‰>ÅÄ´¿‘,3X¸Š`ƒ?kJHU_ÔZBÉi‰äÀRÅ)‰šÂÔ†/,Ôk|BéÐ#ÿŠ{? ÈB9D©”qo­«î˜_CAQ]ešP5²âdå观 HÆÄ.[þÞ•ÃÂ0”#¿J“åv0T„Æhí³løaH‰L(²7Ô: +F)»R, w aäñVU©Y)°c)&Æý5Õï©*€«b. wÊL4qS1MÀÑIj Ç. `ËT®\–B­bV¾ßËZçr°ù˜ä ñ×O§ÉÊ¢×µY‡ßê’="\Fj \Sn¶¬bÄà·®°™ÌéÀÖ*¶MÉ›œÜAØqΦòlª'²LUTˆÐC}¶ÎTÒI‰{uzÖñ“Æ,ÌÒ(›Íò]1–®êßªg2À­oXÑd}YºªÇ# 5 +2bš°z3Ièh²À §å¸¤_‚«ŠIEv‡¾LZç pm99¦<ÙØfÇw¶ +ÿ†a©µÕ5zUºZ"MH`ËxŽÉ)n€‡CåÛ"ÿQª +Á^ €iMÀ•I¸\U…m¸7pTàH`J‚âªv¥§°–¦¸82Š0|»ž–æ燈to‡‚ºT)òˆ‰(6"ŽÖÅÄ-5eX™i6e„1•ãïªsc2‚þšð=[’(ʾå“âêŸlpÀ”l’ä?6%1qE#xg“Mc╉„Ú„«¯#É–C%+\Âd j* ÕÀÁ"½Lp0·Ã¡° Þq½`bs|"—êO¶ (ÁÏQÁn\\•–á2_ôŠ¯¥òW•ÑãÒZe\ò7*Ï©¦’©²,+P¬dªë•í°tÍ–üù˱c¦ÂtØ–ÁY׊‹‹)!ÌàÖ×锌l¨GU**åÿ§Ââô,eâꆡBèe¯é2‚)â䪨Ĥ:{1®ª†–ÎATuö”íŠ_àЖÊ+…%P%I"«Ó'2—’LK§sÃ,Ê1²ì y°®ÇVØ)Šêëðz/Idëêj•È£ùi䂪Š†¸R ŽovÉßs*ÄéÎ4•»»¾Û»Cd‹ò†Dc +)Ö *Û£T}|RÕºh +ÌbX?ò!rÀK `*5·ýŸh­[kHù‡\S™ k|ܤ Wn gjbÒà³DN«ÊWTÚÕØ(™¿êÙ¡do±ë©Ì¥õDƒ©Ì :‘´i¿œªe«Òg`QƒÐ”FçÁ´}—â.SÕÚšJã`}ˆFë°ÖdŽrËÛ~€ÛGt¦F­¨jÏrU6&Š*r³™Òœ-ôÛ)kªÚÛÁ«´|¢J¢†\q÷}a 6²I6ÌVÊ?;ÒÊvU9$v·Uvþúk)Õ¶Õz;~ÎOÔ•rÂF…Ê-¢[”ô'®—D“ò×~J|àWqÖIôâQ*ú%-ÞE*Ë”©gHTâIå—q׬M@úe4ÀG ÀòYGÕ"“sÉVuŽ2:ý‹!i +ÙâªDW>k«sn‘CkµT©*%ÿåÔfƉi(¼a¯+Í? +‰*ßUÔ+ŒœKÈôùÒÖù‚¦Ì`¶EŠ“CbX- >«JÀ]ùóuf!õGÀ{ ª¿áL_»â,ƒ +Ñ©œ{Wk””gX&9É’q6êÀl×7ü—é>žA¤RdTo‹f‡éá…íÊ˹n³"‹xÑKª* )‡Jäú¦,ýÞt©„ÁÔtz›KË/ɾ[”lÊ{¡9âdj:&|Ú +®â}@ŠìôóK“ltý;ÔëÊñ¡ :¯mRÐx ‚r¶ÐrU6&3©‚}rû9%Oáz©ì6$[Ž2Ò˜â…8 +ÆÒãm-ƒ+Q†£+‹‹K ŽÊ-Bþ²m +opyqqM +ð*“ÙQeSŽ¥º;¸’Å5ª?Œ¯Í“´NÚÃÆæ6Éd—¢n"ÑÃ_AYMèØ*`ƒ²D½ÜQÆ·l:äigmßÕ“q¿uµøe +ZëZtÿÐ:ýUí<­z¾èUÒ¦ÂiñÖÐ)® +ßJDx)ï¨âì-Ÿ¥ÈŠMágÝt e•µŽ~bSYR÷:EJ×YëÎ:þMö´ë÷ª¡$ûèú:‚ïp—´”ŠiÓ¢b0qêª";ˆ†I]•t[)ÌŒ¸‘t`ѹ #ßÞH•#­ò(OáFW¤NÓdm ºQ@Ê ìnD[\ +R=¶)ã“®®Ô‰«£®N¡1ñ$𱵪F‚H(úŽ$¯½:*“ÇPê϶%i–Mïr\•v´îæãúi‚þ°æFå–&ã¼®€¨dLËN|®¹Q‘-,Ù²ÀUGÛ¥>5˜»cšT#Û|¹èÙ¢Ú8ª}tír0Y è?KzÎÅ6‚*ljº¼1epaŽ“’8Ôþ“¤”g(ïœ+.h%8-‹:][#¬µ¯kû N–´¸À‘2Úö±‘@)/’y®£’zQêë8iUKú]T…êÐRª“£:EØ¥„x{¬F–¿ª +q7*@¨öÂ]g} þ§€ËT^²ã +úÄuå¸ÈOô‰¶¬˜Fæ á2ÕIÌUUæ.÷Ómýõ’Q7¦m({‘œ$`Ú†eÄ´"" +lâ+c0£SÁ *X`˜àJî&Íð~LWùÚ(myÈ0«Õ drª?aæFËCú8‘¨:.¹*†Î¬ GSµ¹HVåh"ìKdÆxR™&^%£„*^]-‰”Õã&U%3³7"㆚˜­¢ˆ|û ‰¶ª†gd¥3g9S†§Åúù‚»…ªk·#å¡ø2‚X­¥“ +  (cªIĺ[ã*Ò‰[NÂÉÊVÁkèYe©Ðq«yLkS3‹øºÒÅ¥zrŽ‚•,@C¦©pƒR<ð tbŽy¤/,‡ò_anP£ +GŠ*Žx‘ï•[ RÒÙªA +>+ý"èÅôciý„údfШ¨ë7™P¿·Iˆ^*IfÚZëù®Gna1Õ¹Èv¡HÔH.¯“G¸­*Ý]eãŬʆ²¥‡3…Ù€¨‘!ËÙF“ÕuíFõë7õ"²å¨×IÍÉ1 zOÈt~ÎU¹Üó2 kR»H¼4¼¦Š\NÇFÂøs®i”éºaÂàœÖ>JQ¡5~ÅöFºú=¡ ´ É9ÅE}½M:òéu›á­$‡² 5?M‰®I§YÓebW´©>µ¾ÖÅg'{O6GO*™$¬v5€FîH™Þ%ˆ–"ú.$êëQum=S m„ï¢#É2CLl¢”uHæ:WJóG6°4’W¯ã*Ü+Ãågþ³–NY³¢,a¹º$2KSêÍ¢íY;\Ù kë*a×O +A"b_&¥¦rß„&ƦG5ƒzìè¾ã‘‹õdÔVÀØø=%ä9Ã"ÍUJÄõïbô‰#‰f‘ÕÌý‚j§#€mè~.-ÕåǶÔЩK†™e2›žåšÒÚ¶%‰.Sœéÿ˜Sv¥‚5EB3yÄñjIAÔ)3Xø2¤gQU{ +Y´‡DW!]Íï£+žt)ˆbP7 , \wa3¥ŠTWO±V’¨ÀºågÀÂy÷{9øàœÉaÍ ï“ÌAâÚ°ÙÄ“ªÏŒA©—`–¨&5Áï+N_ û¬ÀþX‰¶ÙSÍ€+/’ý]ù,S‡V–;ÿR¥'>«ð‚ŒÃˆg-2Œuy\¨«#@1`$s‹©‚R_s쮓cº~¬¡û1ÿY?4ŒÍÒ×v”)&ø¨kSÌ`2sÖe['FRž‹vå””˜Vý ½Í”œ.ÓU ßO]ñË]©ÎAZb¸2¦kR> ù‘lè”Φ›“Ïʲ|]“‰H[ÌX®L¿W¥š³:æÐdek!®+—H:f4óµ–LµärÁu ¢äÓ’\’Ó˜gã#7d:ÌiX–bEÉ4”¦Ò‘dÛTY•~i!×¥¶ß Lð²¥RçÈ݇dÍ È¿¬ØàºC­ñY¿ó™˜J‚F@VWât ù2s#¥ÂoÍŒ4À™dÒK Ž§ÉªµU¶¤HÍâtHM’…K€0T¹‚‹~ߎ$k\åõ`3 ¦Sf’¥É3àHÓÈX ÊÓ}eG²JÆ0E…íÈg¥ÃNÔšÛªÖüçeá$ìlI¹u$YiÅ9g$A]U±‡÷lÑïeµ®(Ï–“¤yÉ‚K.TῶBƒ¨µ5¥SÉÒ½(Ò´-Rù®Šsßxõ‰lÛéG¹Pr¿-—H^kF)p C + +ò)AW¦ p/)”K¶pŠþ„¡qeƒU‘&ço™ß}ŸÐí:•0}¤ÊçÉ5äï5Yi’#QSå"å`DÂyia! X‰QÖ–ãJÐB"N9HÔ%Š+¹Š Ç¥|Mj  Áib”òÄm.n´"§Ìÿ½IZQSæk7€Wñ¥]èí“5•Y+¢rT²á¤pv¡JJUéFÂZ$½J•ü8•ã j›6VfêˆóJù”›ÙSr15ÒÃ/àJæ!Îý ¤ÿ$ÚÞ—ê Tá.-HQLK)˲"@\IyãÖÚgä Ôe]G’Áß,Oñ i¦Š +d"w©©´Hª§w7ÚŒˆª´Ž$Û6é/GV—p—®ƒÁi™&Â¥‘ôv†ÜUøD÷{ÜúÏÊh‡†þƒ'Y,rýßË&†þæÊ«¸ã{’Å ”,Ämÿg¾FsÙÚÃà(Ydûæ få7Áà"IÕÈSýW=>Eá¨d/›üm¸\¶@F¤EÉ+9«¸E>!?÷œÑë0² +‚[u»®Ê¿ç–s:|$ÚŠAéâ n‘!d„a¨t•ð 0›ü %*Ñ¥_@ܽ®äE“Ö’8(Ë,U"ÂH­ +’I’JØ×DÖõ¢Óí \µ1éãþ^˜ÿIÚ’ûyrÜ khDý±¶vMé6(ªC‡™Êf¦¤% Êh¡ß²t®™ +ÔR:4:í¨ðF¦¹I_Ÿ&‹ÊFðpU=ÄdE,Ç ¨CA#Ê®”b”ÇOßÚTËÏu…Uƒ A”Øy³2¿JÖÆëþU>QSLKñX¼Ý7û‘‚Œ\SíÄmô2™˜$*øɸºj¾ Qz+Gì£JÂä™aTo Ÿ]—wÈTôšÚꈭÊøuK8©Z4Jrg„‘‹–)T¯¢wHeTß¡IÖB¢Cò€:@⬌ y¶v;6í!¶3NIÇ™)ÛL—7søD*ãí£:ƨ¬n¨Jt¦âœ%ÔèÏvH¢RH‰ +ÐÓm:L…oH—<úQTFm˜Èw#Ù%;1uU ~€¬dX™¯Ó +¨–)PºBªRTZ:ˆÅ)€à¨Î T€€ñ‹ŠD]Šea°B•¬S(ª„†éÈЊ…NjMâ#YWeè~ Jô !Á%ܬ>‘t¥àrùk«sh•Ëb",W8ÖÄ»Ðè;“!14Ù#Ç턉—Mz:8) ØýA§Òn¯û<ØêŒY²ù³«’|õjþ&@=ù\•ˆÅ}6dæ— + Ë(š¿®Åp-`× *ÚëkD®ÄoªÎ‘‡û–J1øÙ¨/œÀR›‰”ŠL5SÞ2´‘6/û¬K¢J37r#-S5·µ(ÁÞ4ÉGýv?~?NLó7ÔZ·/õ;Ü=Uå€P¤ R¨Il·$Ú*ûL!I$s +Q§Ì5èÒU¢‘Ä)J„KDö)‘U⽫®ªOàÏç¿fVa“¨„_º¡D5%2«`">lh”F>aÀÐvÈ@íÇÃâ+£SZQÌ•Ðle3Gµ½”^õr-A¤*AQTŸø[¥þ2¥Ÿ•ÑtATå÷–j ð³l´‘TÝ_LêÊ$¨¤ÍEú$ÚÔPDú¦Yb+¬GAz¦r`(·úSí&?5+ÙD + ¹t2À¶—­Æ,T\¯%g É¯ +mU‰öKEg–ªH”]ñqFÎK +ü~ꆨŸ®ºsÖ—ò¸‰ó"÷Ÿª\¦¢ßߊ›Ÿ]J§¥n÷?Ë&¢Ü ãÐNL^Ágº—HÉÆIÏkÝ-_:+5æÛ9ª[¾N ŽaSskS—8®lDn!ˆ$¢º¹É…cM¾ÑmßrUSÙ¦ÎRmj™*wüx^ë6Ôm–éÓ +KDZH¡•”õõ¬qe]‚ô­[æ¹ YeçYtg†³ͤ*u*凿6éê*ëe$*]J·‰ø³éÓw9*ÍÝõÃ-Ô™ª‡\•¦‚µ(2—ÜUL·/8”$b¹ª8CYJ  –Ž?¢ðµE-ñ>šÁº±SÙ¶ê~®8ªu-9%ð»&ÕPËÛ7iÊò÷²ˆ¸dL¦¾k*ÑÅ!âG¯_3µueþá FÐmê~.’b設»”ÛÖšú³1:ëöŠÕ W·UÓLJ®‘å®TÝDú&øP£«ªLY?(úcÒi1T>–à¨ÙQò´˜վ醪ªùp^åu—Ϧ ¹ë“UYŠ¡*Õ “QYŠ±¾»ÃvÖ#wS‰í8¨EV'9ð‰/S"Qu®âŽö‰‚ïxÀU'v¶ö-:Ÿ„4Iê¨%ýƒ$K“wD§a¬µ+—©òp,ßTå¯Tµ‡õ5»°UÚ¾ŽS¯yK¶8ÃéU=¢ˆu|rb„ù U¬$;Vý¶õÂQå¶l%D g*&¥ZêZ#á}T5£²€@­Ðº+Šê>¬š W§Iüle¥Ý9§ºW¡I»3F{ʸÚSÀ”¹d”}H2×éB¾ŽŒ¯ïðX¬>¢#Ç6|cÆRIDV7µÁ¿Ñèò:rLVtÖב¯Sßðvñùûõ]Õ2îöñ»¨ñ¸CÍD3Cºˆɲ[¥î×htˆ¬¢•L–ϸ™lèCðC"uUF‰t¿eqùÓ¯[ßtÀy‚e²1@‹póP+xMõLÑýÎXOAƒ[ªý§á×܆ƒ†#,Ò „,P21G4üÝœ&Î[|€F„ó†î|%7A°¸¢d r½a>Îù…˜ª¶Óÿ{²çñœœ·pAºXÃ|™#¨¢]¢~Èf®ÿúωâû.Æ’²õ쇃âõìéý’è ¯HúðYÇ¿=ïÃQ×Äf°IV³]ºùY.ÀåÒà½ØED.þçl,GëÀîæhêЭ_ºAZOOײ1Üú“7Þ¼¹<o93íž5€•~6êšøÁû7Éj¦ëQ×ôóO¿üEa)$ÛÕgŽZ¹÷ÿ¿-−D1LŸ¹¢ñŸæ+zAôbúd /z!åðý¹¢º‡aÄg ¯ËPŸÀs¿ë‰+š #ωF\ÈÉx-ñìZ ¹ØGóòCFd!¬¶,µœ‡À(Æqׯû’ fþÒ¬\Ñ*^ÿpêLÞŒ¹þÌòg>Y„õ¢l +©ÿ/wïü³MY…¨¾lC§ù›gøY ‚ìb„‰ ñùVn9mßšD wçRî¾ ã¿0:_ÈqL¯¬Ÿ@D‘­!žeò䉌I‹^f£ïéS³ý„daÂÊýè“°nÃv~öI r¸õá'!îAëígŸ„Õ XÚø³OÂZÿ“БOd‘FýÁG1¬×4ø‡õñ|ÿ/˃ÿÇhÊ0>ÄSúNñõÿ-P%zX¸(ÌÞQ‰'8Zé¨m7þ¢ó³¿°ñžŸºæ“Ä![mÌQÇ9Â÷Ú§µû×iÿ_ŧο€¯þ  +æZÿ,U¹þòþòzT/í,zŸ­Xb„„ø7qÜÕëw0t«kþöîÿüiüúý/Û1avº+& +ú,s«œ¿;Oï [KÿlÑü/Ìâ4ÆÛ‰lשÌÝÚÒ-ÏMo–ÊÏíÚk]Ø•C#?Ž9M«8+ö®3­3Ö8Jæ‡Ñt'Ê[Zah'VqšJwÒÕY®}\Ýä×¼}é¶.¬úY¦{Kw©l ÁãvÅ.3ÓÂð>;|Æ»7Vó$Q…Üfipžíë…q®sS½¨Í¿pgzaᶯŒòa"3ˆ8M£´pê'¬qRž¾H•;Z)–ékÞ&¶êZv˜ï\Ôç¯sƒ§yI·5o”,ôìÚä€Õ¬ê¬2U[}Ý<ÿmnòJ+/¢¹~$Ý©N_¶Ï¾«®ÞUæošGßfÏŒúQ8Ýqj+³‹¸õˆ]ËÔOrí §|u:!£–Lw³õ£îáðF-ÛçÕ£½ºÍFvœ÷¼OÜú™Ó8·ª'fåhϨ…ìV"=Ðrc=7fÕc*Áû¦·ˆ²nÀ(Ê%2ÃïÆX'Û8Í·.´ü8‘¬æ“höi4»—*SE=Óµ #ÖÐi†ŒrŠ·öâ™Ç!{OóÂN~e=Ó[ùi23 ™uØ»zOw÷âÙ¨ÝêÕ½„—Lìò‘麕“tó"Ó<ݘßGìr*7δ.‹ãûl÷Ñ s»t¤åG!»jäNuÏõRÅiyúzúìß/þÝèîÇD±oW¦…ÁUnt“ÝZÍóòüýøê÷£/—7?¶Vo"¼ üã î@vez·zåØj\8ì$]?IeÀ½ûze_¯êÞþ3lÖž†y UÒócàä€ÓÜgXa‘*žå³Âä­V™ïéEXç\ÿYÈiíi•'±BÈnÂÎFzœ·ALÁ?énÌnÞ^,¿Ÿð@,Ķ×:O׎vb9`* o…Ó;Ñìv,vZzq‘íÞ³æ-o\û/½î³¨ÛuJ l ’Ù‰U>1Jǹ޳Æò+£4vv9ƒ q¬ÒDˆ˜v$žÛ}°¬ÍK±ŽU<Ò¼ÃÄÝžS>ÍÕOAVdªó½Ta'U ˜m£0ƒ… M•O“¹¡‘ë§2½­=;âÕcxÀò–¼rT‚zùlñßïÄÓí“Úì,rÏ[Æs“Ta4« ù#bŒÒ,éc9)£bëìäÙïA8€6Ô½ «¥[W)o¾gVƒVÍ,L +‹Òð&‘°æ‘QY˜µ#­¸°+GÉì0bÕê“gëï‹ãÛêô6?º¶ë«L÷2?¼ƒZ y½Ygñ&?¸ÜÕ½€QѲ“lûÚ,êÆÉݳVÔŸS ÚͲÚE®{Ï µRù±UšƒÂâUº}Æš§ùÁ}yñ.?y¡U•Ù«Êôyºsæ .Îqóø›ã—^ÿÞ(ÏÜæ]ƒï‰ú4žÛ7*…î]çäÇÆòë|ïÄ»åÍ@ì€h… ê®V:0ë  âé¾Q˜Â—‚Ö‹å'ÉâJ/²Æmeþ x¦_›¼Êöïf}7UÞKU­âÂ(L£2]-79“Â5iìD2£6ªÁ¤·Ï'Ó}³0 ™ÝDq?U u#‚Î~ªK÷YýôÀmÍæÙ Y aN’™(D`›kÃßjÞÜ©_‚T‘ŽŒÇjF ;Ê:Àq·mæÆZfð4œj•@Â{tlËD_Ôí³Ò²>¼-÷.€¥÷Rå Ñè•°ÝÔ‹‡éÎCeúÆÜF\”{û‰ün<ûôÀÚ +s»0®Þåº7©Òa¿+uK ŸÝê(ÊlïГ…¹V˜æZéê<æ€ jå%k]yÇÊèž×µl¯Ô¹_ý.™GY+žîÙÕ%«ŸqÀÕ#8\…Þmcù:é¶R^<ƒgçÄòà`göò_þú¿^û×xqÏ÷ìæiº Ìöºuøe÷äk·q_G,ÆzºwÈ[wùáÛüðM®÷"Ó½…³“.Ï +ý›³¾V˜å»·ÙÞïÜ4f¯o¾øã‹oXÃ*/Ë“WÓï;ç?vÎ(Œîê£g¿ÿçÿ¡¸¸Ýµª jµâJ+fš…ö%¯!J1k°Ývy‡(Âúð'ê€xékpZ ÓGa+ Áçô€µ +‹€^ºƒdf굯2­ãÇñì^²ã“tçEaø*ßÁ›—©Â 8lM ™ß¥÷“¹«jysV^±ò¡–FìVØj€f ¦¼¨ÓÝ·—,mǽ ^j5`­8ë†æn*¿Kÿj7µËR•° j´ ìšÌÂT’uŸ&²{F ¦L›ëܹµS³¸„e‡ h?â´@ÎlEÒ{qo/^ŒZ33 ëª)ÞÛŽòσæ“PÔ– ÒŒfw…$€=Ö éUÐD©üø–·®€UœÊ±™ŒÝߎ°Ç3fš÷4é=M”ÜòjvñÓ~ +dæ0Èvš·Ìvž5¿i-¿p•. º³ûÆâ•];2ËK @{eZì^æìœ~v:¹æQº~åÁä_¿èœþP_¾ sZÅá-ì²B©¸(Žž7W_Ìn~üþÏÿãòåï’Åkfúw™þ=˜ ¼}‘>³j'€ß@EÝèopè‹Õ/²€ð'¯¼þýÉÝÏþ⃚åU¦y–ë\¦á t®êÓ—£“¯ ½«xn`W–v$Øeuþ®}ú}np[=|ýÇÿÜ8~³•*²à4u®±ƒDN Kàß÷µRŠwxã”·.@ÎÛ m¼¥U>`±Ÿ*ì'óz`ƒS9±Ù>ü*Ó:]>IfA¼Oc ž@SWN\@Ý…eœ·ù§w/‘=ЋQ§ayÓ€Ê/¯²õó8€ŒŠ»€*»»‰RШƒHÙO–öS¥8ë€x<°ê»‰ÜÖsÚde+’߉{€xýþÛÌ*ˆ}½0°ÌÙ.¯ÀÖ6ƒŸ›ÞÔ-˜ V‚J–`þQ» k?Y 9 ‚¶£ …`dü'Ì!b7loêÉëß%³}€÷Fqn–`m—Znx •Wa’Fn”Lw¶y8ƒfiîVæ œ÷’y4 AH¦°™ÖmûøÇÁÕ½Á3Vžçª³£›ßŒ¯¾õçÔŽÜúQ®{^›Ü÷ŽßçúW? +í³\ë0^}úº2ù¢2yW¼òÆoÌê +Zº´è¼;pë€Æ­Òaïô‡éíÿ<{ø'>¸csrýÛòüM8;Jx3³²ªÌÞ4V߀±–éÜÀ”²í3ÐìÖÑg——•é«ÆòËÙÍG²s½|¢Š(ÏYíˆ5€ ïò½³|Ú–7NöëÞ´:{=»ÿSiþÚ›y®§;É<è&£xRˆÕÎvoVˆF=É?ÌÒ‘+€ | ˜Êm?åw’¥Û†WÀÆÜAœMöµÚ¾^ä³ËŒð{m{óBó¢=ÿ"]¿;ÝÐ}nÏ(Ìü PkÒmÈßMäAiÆú…W„š–è°ÑˆX Á4#qÂgúní8,;pB¾sŪˆB£nË*ÎókXÃDº¿ŸôôJ‚õ÷%à +cêöE„ Öi¶Í‚ò=0*ðg/²´/JeF Ô™a¶v²§—ŸFøÓg;”Ž:-PÇ™Æ ¨Ý”‹–H·¶ÂÖ?lÅÁœ€³à–A~žfZ™ö…^š‚Š7²ƒBýx|öMq|Z/ß»µêÇZih¹Ð]¹U˜v}³7z~‡7 Ш¼Jä¦A§—ÌOYý¤:zží]ìŠQ;µÃ|NyçB«®’¥yeözñüÏåù+0¯¬Æ™Y=Êõo‹“×¹áC¦{æ6<áà.àXº›é\{ã·éÞ ½~aÖ/o¬z27âÍS£4c°.ÒTnV¼2¼IتfZ§XpÞD+/´Ê!ؼ`±ûÏš‹w‘t'Ìšñl/žíÃØ»ídyW«€†ÊTj£û8Ã0ƒí€ñ­ÊŒîPªJ•“¬»³rBFÅ*®r½—™Ö=«œXùEOq`ÌÓhúqØýüÀ +[`#,b ôÂdË^Ê{8L` XótýŒ‘Ý Û-0XõÄIfû¹Ê2ÁZŸôXX.•¹:Jf'ôS|«G­ê¯v“»±L öÔ.`…›o_ç;7QÖ +˜ŒD-?¬6`H¯À+@(m…ÝÇ6¹Ý¸wûI>D9™9ÙjÞV4³ &FÂáv¬Ûëx°xuÿú¿|Ëüî¶͘ÙIÐrë0™®ãd’^Ä®? ›ÿ°u°²@¬ñ Çf˜ÈvÁÔ +E°ôAœæ[ÅÎU®sMã‘Év®ôêbß($sm»4Ê6³íÓêüM®”K,?<ŸÈ¬ÊQuú¦6{•nžíÚžî¥ +Ô7 +§Û`€¸¨}úS l÷:QV&åÙkä¢L5/ê«ï»WÌŒôʈzq–,Žƒ¬‘îÜxÓ÷¹Éû pNeM÷Ân¤eqü<`À.‚R@s) &äl/UÔsã|÷¤ÓN"Ë›gÕÃ/ ã—p:t0—<àÿF®s‘íßðÎÓD¸¤œ>X+»´Ð Ø @¼ðáOÀŠh°‰`nǦ™Ày„‡Ý²k°¤!ŽöXËMãl2íi$ƒÀ›µ£n3 • í$‹À<`ÎSmEs{É‚Ðtã0ØJéZXåèâ½ †ÿ~²`͈]Ý‹§3åq¡} +š: ×öµêV´@ËÈŽCFù×»1woÓÞjîch¥°Ye•ãBÿEÈi>Ž¸ûzÉ)ÍAæŒL¦{OC|'–êÕÜ‹?Ø­¯ž¤ŠÛ!‡"*ë(mÆ¥Þ ÞíxîqÈýUÀ@ùé6 oœní¢¯¯Í*“=ÿ4¤= &¶ôˆYå••rƒ7bz¾Ü> +e˜XĪ#ÞóÆÝK¦Ni´ª ßáó#N¦ÿ’m۵ÈUÎuOôÒ$Æ;p@öŒrЮHk}—íÞùieö<Äë°à!«ƒ‡¬ªQV§/J£gùÎYmõÚj™•C·yžÈÏÂé~$Þñwí³ïSÞÔ©,eçv®cùq(Ý-Y}Q[½sª‡€Óxÿú€µöúŽV“ ¶£6y @ ®}r[;‰R@¯‚Ag½êè¡}øÞ©¯âv½wüEfp¶käwò¾]ƒA4o2¹üíèîÏVëÌ«êôyª0Û< +À°•¦7oƒ•×8ÝŽ§CzAÏöžFœ-جx>H¹dð^cOKÇ3ÐG1·sA¯5ö“ÕýÀÚ…[Z„Ì2ü…ÚŽå€ÃV DÁ´žº¼v !ıLÌ^ØD´ÈiEø§™ëGÌŠëµÆWV‡½ˆ:k ˜œnÔjîD˜S'Ýf0UÔÒ£˜ÛK𾞂]çd²Y˜À<ûšüHUA@ÅœàÿÝD&¨å.̶öÝ£}È(ÐŒ £¼ÎÕ×ÿô?%rýGûú~<(àý¾VU°`VoGOƒÉPÜv² '×K¥A*·4cnËÌ Gg_j¹‘‘ÙaÌíhï¥r©tÃkŸò܉´zÂí$Ò-7`Õ%þÓ›Ìn~ãÔÃéÝda6‚µÚq÷ä;<Åöåí7ÿõ¶ž êE-Ó7A‚yÓt}¬•ï^ —¯®¿þWÞ=ßÕÊ!»6 U\–ǯ–/þÚ½øI+!j*Í^Fsý0ïðs§…áóöéo*‡ïš£»·?üÇòüٯ콈ì8`Å|÷Þ®ž€€žÍMë¹×½ñú· IüäðÙïÌÚèWûqøÆ”7êÖ—÷˜¿ø3ï^–ÚgÇ/~tÊ¿ÞO>šfa 3ýRé߃‚cbÀì< +êÛQLåÂ.n× +ÀQ•Òè&ÌeeáÏVXÓÈϳk0µÀš8 +€ð×óó}ýÉ zh7€µvù(žéF3­$¨Q”'a£ÖHH8é ·UlŸ4¦7;º'ÜbC0—Àv&ËQ£fçǥΰ‡•¤cðèè VBE†¯èOÞ›Åñ“° ºŽƒ–™pLpp”ïå ø—-ØÊxn?ž‡‘÷“å}­8íìÕŸ÷Íâ~”™¬½ŸÈ> 1О¨O­n27iÌ_û;!Ëp+N®·ÊOBî~ªl§ùö9˜íÀ “Ð4ÖJ€07η‹“Lc"}7^¸bäû°° 7­ü¸>z¨Þ®ÌAȧÒío™ »M­0Ë×ëœóݪUyl—æ`³˜Å©–¦r½ÑáË—?ý§Âð6f~f†L¾yQ>ké4Qæ€6wÚÇ;vÙ.O[‹WÀl 0„ S§qrxõÝŸþÛÿ}zýíV,ëë¢ }W+9Õ+oø:ß{V]}¨'ذк…¾#Û‡÷JìT݇ÜÚÓ)dWëÖcé†^k¥‰îæGoÿòßüoÑtãQÐÐ2íÚäÖ(Œzy+–žÒIyt htM2ÝŽ»õT¦· øõ]«Ú»þâŸþs4Û}Í$3}0lSéA*3õ2ëµÑÍ‹oþmvóïöÇ}/Æ’078¶ñX‚ÉÜø3™®u`5ÁRÛ2à„¤Û)4Ï+ýk·6ßÑŠ 1AN>óh§•)ÍÓ¥y¡q¹~cx Úó)§==¤yI§&¯5f/Ã1ßOäÉÜAªHÀNk4Z¡±ðZG ‹ðKÃîÓ ½ÏÃEÜVШîkÅG!ÓõfÅÖ%Èê¿~ÔÃzÉ,¬ìÒ!Hixf'Â#Z)¦kÈÙŽð$ؼ]Jæ¦p$…Üí8èß²‘j`‰g wbÈÀ{ —“-ÛÑ2ÝlmÕž¿ÉôoôÒ,‘é%pakðE©\˜ŠÕV¹î«ƒ±vœÊÊ£ûêâ¥UžY¥™[;4Š ìæ•áµ]?Ü5ë{Z $'˜ó mAiîE³8aµ%¬s¶“ð&feá­]`õ¤8¼ïž}Û8û>?}™ª,w4ÆÒžQ2+Kêfùˆµn“ÅUyü¦p*gåñ{­8߉çBF TlPÈ‚iw"¬o×.j‡_ƒ€MO¤{Á”·uz9—4«½bdúÍñ}P‡½h%`XÖ<@³®¼f]ÛÂiÒ3ýG{ɽH&å´cvôã6@ÇXf?2¶…Cã «’«H³1½ÉÀ€I>& ÷Rp XíqÐ؉¦·£h8G@ò^¶~v Wƒ‰¢W=yr˜Ø §ñíN Æ'!¶—(ì¢g¾¨š`§¸¬±ZÁT9ÖêGÐõ`·`Š‚®é\¥ëG1ص›m²:žÇss­t\›}Ý=ÿ]uõ•VÛ¥ˆŽkŒ ËŒ@kï%=#ÆÎQØ­³êú)?} K4o®—᫇»€Ç-¯z'?æϵÂ"F´^‚%…¹m…\tqg&ní­§„ƒ>VØàe­0´ëˤ7ÔQv-SÙ)è‡1•`Ó;0 Yã"ӻ׋Ë8FŠÁÚÙtåÁfW¬yefaçôiXËïǽ$¶€»Xð$¬ŒYZ`ˆV/îàžfw£™ípz/Y +³^ª°´Ê'±ô$šž˜¥c^=‰¹uÐ#°VÅÁìsÛ(.c¼gys¯{¨Mkn;eÕB‰BD¯²òI2;I¦‡Q§µƒ ãˆÐóS½°pªÇ¥Þ׺$ò1£Lxÿ¸ø»Ç¡GdhçG ”Ý–Y¦x7QŒ‚­‘ô]°MZ5 ÕÃf£>ºÍÔWpú Ö‰ +ð 0án4 2@#/Û£·ƒÎ“ ³Ë}´ÿqÏH6ëA½[°Ÿ( Ï!7…¯ ™ ¸ +d9¬7,ov`T@Ï‚"C˜•KýûTvàåEkñÌ÷Ðñ?lÞÇïZ«²ýg°õpÒëÓS2@ê–Üò"×:ÏunÝÊ€ü½d‘—íùë €gDA×¾^ÿ±ºxŸôf`ýF¬u4ja»Q^~›Ÿ|‘*ÂfÅEaô²4¬h†™Î©ñµ›X¦0ª‰Ì,D^?õ›x/‘EØ ¶›⮕ʀæTV lX¬CtÙU/ÙYÔmïųáT9[]^ä[ت¦Û笅ú:h ù÷¬8zAÉYM`4°¯æ`‚eÐë; OÓÕ,?90ÀðXõ}°©,] ý9#ЕA½þhß«€ Èð³}`uÒÍ›Êø]qðà”ÓÕc-×uÁ>Ý7*¦71òV™:7Q³šrj¼8ÝÙOöýhž—1L-`-¶Q»`#Á{{è=Ú·òó„Û7Ò`§¿{ùûÇá½hã×»Æ?l¥v¢€ñ0‚ Hl?UI¤‡ÛñÂçA¦ççÉÌ(™™é¹Eu°ó8¶Ü¾H²îçëѾŒ1˜¹…‘Œƒ4ãã7¤UJƒWpôv0?á ›í§ÀXýXe +»r;‘l'<€`À.a5’¥§áÌVI×ν᳠W¬ò¡]^ÅÁ0OåŸÄÒÛ) ÞêôÁ©.ÃV=‘éÇ8j¨Z .¯š—ÙöùN‚Åy]/ŽœÆʬc¨(Ý8õz7™Þ]"!•T¶rª»©ü^ª+‘: ½~–HOÎ/~¹šïßGÝ&+‹Î·b PÄí§r@ÚÀ9 ô S¬VÿV,0*0g°ñ3•£tåÄÈ-ÂVpxŠu‰,˜fdÅ*Ì“hc¶CV{G«GœAoþ.Sï'X*ÝÚ׋Fa7æl…M8Îke½‘W?|0øµÒAÐTÕv˜mG³À™ÆY E¨“°;n~éW .œü2¬Wÿq;ùh×܉"2ÿ|× ÆKA ¥¨ÿ¬ÄYŸ7®âéQÐlÆÒÃÝÞÓêÛÉʾ^6íPî݃ÚK¾”é^, ¦å“€Œe¢V~©RH¯j˜áPÙN€~¯š™žžéÁmÅó`?²l'aµó»0m½ +Â-™€¨Ù‰ó­ß‹{¨£Á"¶òÕÁ´I‚í™î=‰ðG`G$ò۩€ +ì8«º·pê§û& n’7nÝ(ô°í‹@<اõɃÛX}ãÖÍ´Î2cÿ!§ Ç9âtc¬r^c5§ÐYq`·’¹o^å{wý³Š‹7;@Óz¦u¶ö“0û½0Ò²0uáÔhLdGªÁÈ +ó&¢‘Ò<Æ»¸ì Eo@w(Ü%¤Â;‚Ÿç‡{VmT¿UwW8D,ýÊð¶±|Sž<$r“°ScÌÏ°UŒñ:ìhÕ\í¼>~eU–ûvm'YlÍߺU8×8ìôŒÀc¤mä'`ÎØÅq„wÀl4+GݵÂÔ.’«v`5à?w$ ·“¹íXzУÑàµS^?£ö1¤dÂAHå‡Vå°8¾/ôoŒÂ¡[;)÷yÀØŽ²ï00NAªºmÀÃÑô ñVHòï¼zË¢åç¬zêz 4I¬*œP  öã…½x!ªÂÑc¼·x˜Ýüæs·È–ã¨ÓÙŽæ@Oí%kûZ=Ê€c[[‘ìN8³JïD2„÷4”~rÀ‰RÌjdj+àX€Ü ̦óÌÝ(`$Æ°Ôµý”÷4ƒ©B – þx/(lyàÕ¨ÙìÌÞZ^ﶂ¿zÁô¢™½Xa/æÁqhW`£ýj+¼vÁ4b=£gÇ Ûyã ˆñGá잎¹Ua;l…Ó!\ 0÷&Òk ý¬Ì0nVí˜Zìí´0¿Ëí¹õ£tûÌ®,6¤rC£tÈÛ¼u–€3Û:Éo²ý«€Qƒ? b@’˜Å9üÁh”^ó¿JÃ8­k†Égð·Ùö…]Æðeººô:'…ÞY®}a”WnuåV–¹Ö©–X¼`µ#0ÓìÒbϬ™«»?òúa‚5RÙîŽ^ÂS£×Ãnß*-3­€mìòaÐn?‰·âE@ò. RœƒôÖÛ—–¼u®•¦¿>°˜`›h¹±]>´Ê+Í[Åȱ£ t€ y'W¿Ÿ\ý!Û¾·+§ —àà€¦ã ðí¸ç] ¬ ÀÒI‡hÇ*¤—À‹§Ç™ÆU¡ufHuì(ú@À‚1ŒQ†Õ°ËóB÷"×»ú<’“˜dÚn$sºq6Ž²qÊÃT“°Ó‰ ÁŠƒî《LŒP<«ŠáÔè™A¦²ŒÞ~,wº€·ÂÀÃq6ily“@4ýw‚¿ÚŠî„¹ŽJ¹»o—Ž#6àºÄöád{€îfÈk‘•YLjU„Á'c<1š{?æ`þ*È%ñQ}ø´[ %þîiü×[ÚS`x½åC‘ µ„ ègF¶ l ŒF{³ Ÿ“ÈN²ÝëÚâuiü¬8¼©Ì_Øõ#·y–î^½2{Y_½ož|«•…iª0‰¥û`q ÕæÍbÙ¡Y“Û‹|³ë`ÇYÕe¶sa׎Ãþf» °nxméuÏK£{Ö8ç3»´üœï^-¯7:û®Ð»L¢-6Y¤yÓtë@2Øb¬ºâ­ã>©"œ}°ÊSÞ x>Û¿Íôn4Ðqv{_€WáÃÇFyœãÍ‹ã—­£¯Ìú!°ú(€ÜÛN•ÁÊËô‚Vë)Þd>žîd{W˜põhßh‚lLå§Ní$Ó>?œœé(ë&b7¼kæùÎ|{s®ºogpRŽ ïÐ,­B¬ËÀüŒò@}àŸ€Ž9pØ»¬ò2vûÑ4¨øún97ØŽ¤÷¢ WÝGA' W¢˜=Ûs*‡åñCmöPŸ=d:WqX»2*{©r˜÷ÁªeíËlïzpùUeùÖPeö"ݽ²%†7­ã/‡w¿¯î\ʼnži×ç/X뤄]¤1Ïõ. ý+Ö:.M`åùÞeíðU¦{ìïª,^÷ξiQš¾°§Vå¨2zÀž7@þœÂžf:çùÞÅðü›tç,ž•#·}“¼ÈÒ½;£z ú.×½l¾ÖK“êò‹úÑ7nû2Uœ³Ö…ŒZ?Îõ¯’ÅQÊÅr}£<…Çr½óâøYuñ¶4}¼Êª'6Š¬Y4=ЊKT«qà4À<Ô¼IЮ'²Ã(ïƒ Çªج~b•æ ¯£¼qª¬ºyž*ŒáÃXí¸:y^Ý&tª+Ö?¸6J3P¹îÌ°8ip‘\f;çƒÓ÷óg?§%½pcmñ +øªuüEóäKÖ½ãí»tóÌ* oÌ2í‹êüEeöÜß'K3üœÁ-k¸Íc›¹Ñ‹òá—ÕÕûÆÑW¥Ù+¯{±zøçêá»P¦ï6Në‡_ÖW_•æo‹Ó—v}¥ÇñlŽC"2°Ëk‹ÞéûÑõ÷“÷½Ó¯@&Gs#Ø…Lç>À$XfqvZˆèÌZ"7ìÒF/ŒÊ2Ó<¯Ïßä7¼}ŒZ=ç­£d¾Ÿî\d»pp¼ÉsX¨îÙ×Í£/*‹fóhǬdz£tž¿ö&/àŸÉüž¬Í_.Ÿÿ¡sþUv|×8z×<†ÏyW?ëžÕ\=w¥›‹þÙëñÍ·ÍÕ«dnd–è®.ÁX.ö/ o4ÁB©™l_Üg{7 àí¥É3,©Î?“…‘Y^–FÏf·?MnrÛ jàtƒ…0ª1Þ´ŠãÒè¶óíèîÇ£7ºÿé?ð±sÞ9³k'!ÐIº}Q›<_ÜüÔ»ø®0~J'Âg6Œlß­:r +N›‡¯{ç_6W¯KËo¦Èto@Ú§0Eí¨<}Q;|™~ÀèöĪ€ùvXÝäÆϼéCíðÍðú»ÉýÅñEqtÖ;ÿâïÿ¼xñ[»v˜iŸåGwÙÁ•7}V™>ËuNGÇ_˜Å™îM½ámax—ë߶Žß/þ8¾ÿ©±|µ¸ÿ¾yø"–­:+OžõξÜþ8¸û‰u.’…Imú\¯LzÞ®LË—•ù³ÁÕ÷§_þõö·ÿ]÷ú‡áÅW?ü[eú"ž§oËó·•lâw«7ÿ®qüµQ^î¥<™Ž7*.:gïk«·­Ó¯Woþeüì÷ÀÛíÕË£»ïÀr, ˜¥Ã\÷®¶úª{ý»Ù‹¿”¦£ó¯ú + :·¾Ñ»øiùð/¿ûï¿þËÿòâ»}øñ¿jü$½‡—ãæ•öùOìì8¨sue朂IÁ æœCVÎ]Õ]]]“º[R«•eÅQ°•,YrÐXŽ²d+Û–e9ŽÇó};³çìٽԞãÓ]ª&÷½÷y~ \Æ›Ž`É_˜÷܉6_˜ËLŸ-­\'¥®žŠé\!3u…kÎp3T݈6¶h©ÛX»;ÚÚÒ³I=wŠe:Öð—\©y:·l¶±PþªÂ¸=ÿ„%Ø8)´vÍ“Bã$“êgº'©‰sÞlßë9£]2>ÈÏÇ;;þÂ"”ݸգ&‚ÐD_'ãÝ@i=TÛ6y€b„Sa^%b|ŽÍôƒÕ5¡²«ŸÈööLTžåË ·x°NŦ <Äòf¸~’Í,›=Ùa .OÇ»lnÎkã¡š3ÒA…’ΓDhÀ*ms¹EXáXû¤Ý—×Ú9#!h,^£;EEºÉÞn¤µªmĺ{âb{õ¸´a°¶^XºT_¿ÖܸV"4¾Ìêµx“ ¢d¬#–×RÓ§“Ó»ñææôÉ[ÉÎ,²Æ)Z½)¨:Of>ÜÞñ—V½É™Xë¤Ò)ªl)#LbXc·zbby¥¸zwqåÊÔö­K¿å¸ØM÷¯ú kF:gfŠŽPËÌÕDlÌâ5±I³¬ŽU™d—Ë-Jk ¼Zw\Ìõxþ215¤%H±’éîQ±¾+>ç¯lÓ©pöÎÊU%hl@VS¡ê‰pý”ÔÛÔ7ú'n-œ{ ”|é~®¾´t±¸x>Ù;YXºRÙ¸Y˜:sõ‘ï åe#eÍPeN$Ñ9•™=[X¹*µ·jó§Û;·P.•énVV¯µ-"Þ“¦÷+›·ÅÎYw¸Ä¥Ž`VCh ìŒ÷˜Ü¢';ªo„›'¨D#Ñ^ö—ˆPÕmšÙkxm:ÞÉÎ\Öôdñ§ØTõ§1>: ² §Ìf£õ“±æ+3± +.©D‡/.úróáÊj²µ>½}£¶tÔÒîËÂÆIíüì~¢µ®mñÅ5‡P/vOçΫ0?”(”D´¹ª®û‹Ëù¹‹Õ…+'.?‘lo+‰ˆ+Ö%¥Y({èg|ŽNoÆÚç\^a„’@âøì´;Þär³T¢Í¦zÑꬂ‹A@Áû3S‘Új ?çò* +n(u÷\áº?;(, þ‚?Û«+žô´‘-ºãíXsU(ôí¾”PY"£U>ÛKõv`ñ™d;Z_9sÏs|®ëŸ9S^½ž˜ÞV׹̬¯ßÆJkÓ'îægÕvÚÌÅkp.gŠ‹gSýsÅ•»!¬QÑiÈ°ÇT(„*¹™»–š¾è+nø*›¾ü"#uÂ…¹xu^nc&m„) Œ&3Ŧûv¡‚ûRRmÅŸžV;"x¤ÃäWÈÔÀy½©©Q =nt\¢Ñ%°±.È&[¤µjž»ôD;°8©*–gÅy.;ªRn¯=ôÜÛ°>*‡ßoæî†G~î +“ë'«K7o?{íé˜èXnv¯¼z#=w ú7¿p¥°x•Œu7÷n_~äJªé]1+V×ssg;'îItv*Ó;wž|-T_…H46ÄÊJ¨¾ÙZ¿±zቩ½§Ùôr©µQëŸA}Y‡0ðb2Öã +©ésw¦Î<‘èîõ–ÏÖfw.»æIöœáº+Òà³³­­ÛÍ“ú²ó ЂP2{Ž@ÁÎaÅ4®¤Ú)ùÒ‹R÷”Þ(qG`gA¯œ‘†;ZO5·ÎÝó\~ú¤‰‘S#­=¡¼–_¼ ‚Ç€ ­t’KÌ@§ëð—hñù~uùbsãzfvjV&›®míÝxeþT+Ý=™êq„›ÈxnÁŸ_¤¯ÔõÄ[2+­‡½U…eŠÊÂ…Õ‹§»§¦–ÎöVÏcþ4üs±²5ŸŸ;·¸ÿÀþ}ωù¹îüî…ÛÏBÙKèÉ8„ k¡|*Þ»”èóD›SK§×v¯ûbE£+€‹U<:EJs\f±´|/“ž×à"mrÅe¨B(öÐéyX7!?wêÆSS[Wv觉JkÛ|eÃí«þòÊÉ{/Þû-Öì\Ž//s¥U¾²•¿8¤1w›s„qËžD/ZÛHÎœ›§™ì²ÙÏ·vÜbNŽ8Q.(ÌÇë©Öz²¹T˜ÞÚ6àN!©lFª'„â*¸˜3R‰UWöîom]×;Åpu,€Ë/HÓgµMRšv‡ëë»÷ì\{Ì.Ó9‰`ÓœrÇšln&9µ,/5ú»/¾ñóLoÇÉg›Kûg-®ß¬¯ß˜Ù} ¸|“ꛧnm_xÜ x^4\Ý–V"µõôìÙXç.V–OÞh.ž±x¥`i)ÔØôfæØÌ\¬±Ýܼê_ñ$¦+ƒ+ÇR‡ÊWÌ\Ùä…× +„T#Qž7¸CN±DA ÕÁ +—šMMò¼LLŸ£´2qg¸bâòC~¾ÊçVä¨Oƒù}R› WÓ½ùþWX0sE™Š–O]ÆÊ 3Å…ó‰Î.•šÅ…,‡°?…I„‘#¬dpx9Ä—¡öWÄÚ–C¬M˜½Z\ÄØ-è0…Õƒøò6¾™ÂÂ@F«âÁʘީǼt8ç‘j© x±ˆ7øÜo¹ø´T˜m®^âr3ˆ?‹øs¸P&#mg¨ãIÍC5Â6Aq† +ó±æV¬¹moÛ‚eµK$ÅŸ™R;X!Y3uê¡Hó¤S¬é B™Š™=&³Éôê‰3ñ-µ\ñ&“žÒRa«7ž>»icSþ̬ÔÙKMíFªËn±`õ„',ôˆÆ)3yŽ«¬#ZJ4ÕÞ‰6vâ½ótvž’:*»×,FÊktlÚèÉÂY ›¢ÔXM´6evNe÷yc=(u.Øø‚‰¯@B·yÓÎ`Ùª*.ZÙ¨,]çsK€+™ö 65«°óL ¿´sƒ‹•GMn&3'T·“Ó ý«tbÆÂõTÜÅçVwn +RK‡ ba¡2w^jœàs‹ ‰ÀùþD'ß9ÁÆZ͵[éé‹\²OŠ 2ÜD¸<œi¨ŒKŒê2Â&{žx'Ó9Y^¼lç +“€ÜlüʽÏx‚eÊ[™(âàÞ…to¿2YaöˆÉVeñâÏ+Q¡NA=ø(Ðop„ŒXPeñ4æσCW!ŒãW=bse‡Ð8®v~í˜VcºCí#2ó +´x€<=P6C0É©¥Kl~Jæ`´.Aç +«° ;Üvƒ’¡Q%bq†A¨¡öØÌ<…€)é]¹Åkq‡\ÂFé¶;ÚÀÃu&Óõ`bM>Þ˜]»T]سxBF2ÈLç{»…ÙsÁÒš;ÖuˆU½;ëÊõa³P_>XÚ„ÉMï÷¶îm¯ß´û¥x¶9¿sw°0+C|ƒ! “á’³þÌ +€Üê³y’‡oÒ@ؽqÈ ¥¥+™î)ˆWSÛg/’îâ…lï4êÏAÔ‚ (G|j‡@[¨'#3¹'Ín+rópäniFK§ÁžÜ‘†ÚÍŸÓßD}ÇÁÁ›´si8Îq½Sg÷;}¹a™éȘf ±v¨´«mø¿Ýƒ3œ¨mf§Îhñ ‰Š¸Â 0À<Ü—F<%BC@f“”ËX<’‰’¬Þ#ÍĪ›¹©}ŒË)m^#ôFê ++­AYèzO¼TIF{v&«Cx+…-3ޞʹ#d{·²pµ8%TY“Ù¼#ZLivËÌN„8Èͤ¶ÛŸ%ƒ…!¥ÃBK´Ôñ¥{ÞB&,”„zR Sª-®í? ²³†U“Z»ÙÁSB™ 7töÀ˜wxâ/ÿ½úÔæ°Â¢C&Ï0ïM.¹5!·<¡D9H^®ø…¥Òà›7áà3±Ð]<#T¦eiòˆt¢æŽUì숩µ2þX›Š ¼LïaÍ•ˆ×@AAœíþ ,ÚÌÆåpeÁ«Z|i=1ºg -–ç‰ (éO×DŠ}—˜W9|ÇõN—s‡ç–9iJ‰2„X –7 +³gRÝbqÖ*¨ž\snn÷nÌ/”Û,Þ‚Cl“‘Ž“¼ý+Gü€¬B~ã%…ÉáôÅ£5€úò¥|_jmÐѪ/VYß½>¿{kHGȨàyS“Õ"~ l"!˜\œ  +ó9Bu$Øp„ÚBùD¸vÒ@ÅÇŒL>¡Þ¸ºåP&e÷¤PZÒÚ|Ãã†q¥Ù`÷"ž8ªbþ<Ð…ñá|úRꄉ ägøÒ"š²0 MÔG …DmÃBE†Uèˆ`#í5üà2å56ÞÜ£ÆL®p(?L‰ Þ¸'Öˆ56àØL´$·xä&‚$@{&{û¹Ùséή_²29„ʱyb2¡¶ÑðÈk¥å«©Þ¾Wš…b>¦Â´oå$·ûÒd°Æ'g…Ôœ“Í îÝ?ª°0ɵvµÉe%D™Þ=$GÇ4¸Ë—Yݾ›­®0tÊfíÞÜà“/"4¢!`=Åô,J§ojÜàPZ¡gƒl¢žè¬¤§WÅÚ”Xí¥:sÍõÓÝí³ùÅm"(yM +h6Ú²QÑQ=®²Q§ßé—<‘“¨8CW$Ì·N\~¨¾¶/”:f6bõ'X©,u=Å® …ß÷E2mo´4ª¶Ù(ÑB…'­´¿}ŠLÍê©î +Å%•©DÁÊÇ”.Ÿ™ ÑR:Ý_š³6d¤Dg¨j„E³R‡Ç52“ÐÌ.ÙÞÁüAÙY‘Œ,Œ€ qw4‡ò ¯T.N/×V¶ÉdIãò“á:ë±I#}hD{dL¯0‘BfÆ—ÈIhì>À3¾¸äNôÐ`Å@'À— ”À¥ÊX :‰ºåÆæ—¢õu3%iˆ££:½…²»D;6`~……ž0SjÌœ‚¤Æa øþöåÖúY*Q<ª·Ûh7PP¼ktGLMšGÕ˜ÎÁ¡Qã«;<˜¯ì›¾tŸ‰uq¦¿}¥½º'–{FZ6`*Ü댖!+³ÓVOP,ÍCXF¸ ”´ÚΛ¨L²u¦8{‰ŠÔF¡6l4ÊÄ|©6DŸ#—‡¾ŽÔ#E&^âÓ­@v.˜_bS# +û¿’}ó¸ÖFż¡ªBOR3Êõ´ÓWˆ—–C¹EÄ“Q9†dV½Ãg÷¥¾9a¸kBXn±yóB~“O-Úé´Ì@»ƒE§/ÍÕ: [³k{›çn\¾óÄ=O¾ðÄ+o¼ùãŸøÉþò÷~úÅß¿ÿ³÷o>‘în`|np‹ºÆes†"RCÊ6R¥^¶¹Pî-L­l¯Ÿ¹|õ'ÎÝÿÄ©{Y¹xóÄÕÛû÷=qáæc/½ööso¼Ý[ÝY;-Oc¾„Ñp‹y2\B¼*O”Ûõ™¥©åÍÕ½s§oÜwíá'n?ûòÎ=n\yàÜýO?úâw_þÁ;o¼óþ«?z÷γßí¬!’çf§ÙÁúÂÅ€TqúÅ@2Wì-´WOÕ–¶²ÝÙÊüj}y§¿sá¾ÇžûÅû}ôÛ?|ç?=yõN±·ÆKu¥‰R…3ãAAjÇK‹fgpLëÀ½ÑBo5Õ^ —§ù|;T™žÝ¾º{÷×ï<ùüwxñΓgîýÖÙ;O»#E…U[½r½ ÂéË»!³û³f—ÀÅKB¦/ÖÓ͹úÜÉå½·}þ¾ÇŸ}ë_½÷ñï_ùáÏO\¾]fìÞ$à¢ÊÊÈL$â‰blRmóÊŒ”‰ñÉ^¢¹ȶùlcuÿúÿöÊ}OûÜ}_~à™³7Ù»ûÁ+=óÊÛï>òÜ+n=¸rî»/©´P +“ËFI±ÌÆ».AH¶1HÔÓõ…Ù­óµ©ÙÕÝ ›çoœ¾zß ¯}ÿw_þý÷úûúÛŸ~~ÿ“߉æºGd¶cr‡‘ˆ±±éPveBG©ìZ+E°I„NjíÊYI¡Âb²6¿qvûÒ}+gož½õȽ?¿tòJaf/ÙÚ¹ÔÑIä뇵¸[(V§gOlž<öâÕ‡{òÕ׿ÿþŸ}ò»?üêÃO>üø·ÿùÿüôó/_ûþO~æå™­ ±ÆºõïKŒ/.F’…J{qsÿÄ™«{—oÞ~ä©WßúÉ«?úÅÓ¯ýà‘ç_yãíŸÿò£Ï_|ãÇoÿû/þþÿ|鿼óÌëËû÷ŒE‹ý\w=Z™¡CéHºØ™;uöì­Û÷?þÌsϼôê[ïüüÃßþñ‡ï}ôÊ¿ÿòíw?úòÏÿ¿ÿŸÿ÷OPäŸù£_þæêÃÏf{ël¼‚iÁ¿(¥‹©¥5xÌoìì_½ç¡§_xþµ7ÿÎw~ñ»Ï½þýŸ¾÷áo>úä‹/¾øßÿ×øÛ/yö•í ÷…s]›³“I.Ú —VôÁ`óxùD®Ü]Ø8uþîÛ7yúæ·ž¿û±ç^zë§?yïãŸýú£?ÿíoþûýúÓ/>ýý¿óÆ;ý­k‡8—gBM—õJmo¬š«÷» ›Ë;g¯Þ÷à}ÿÛ£/|÷…×ßþéûŸ¼÷ÉïùÑï~óÙçúË_ÿëý7tëO~õÙs¯üXÌÏŒ\Ã*;(ç„Á­F8 #…|$Ûœ];yùþG®<ôØ·¾óÚOóÉ»ÿöŸ¼ûòßùð·Ÿüû?üòƒÿø§¿üÏÿüϧ¿ûü™WÞÜ>3˜i T@n°MèìZ„X"§k½Õ•ç/Þ|øæÃO½ôæ~þ›O~ô‹÷_ç¿ýã_þöŸÿõñï>ÿèÓOÿã?ÿó~zí'rÕHy™ Õƒùlö9Š‘"N‡¸PA*Ïæ›ó…Vaçì•;þÛK¯¿ûÁ'Ÿ|þåwÿýç¿úè·ÿõ¿ÿûË¿ýãÙ×¾óá'Î^»Ã„«[°ã\µ>3»°ybgÿÜ¥KWo\¿óÐo¾õæþð‡/ÿò—>ýì½_¿÷â«/½|ejq5”­±‰¦ÁÔXÔÅãnëÒ™ÂòÚöå»ïܸóè­{òß^üÉ/ßÿñ/Þõ­ýðÇ?ûò/ÿ§ó«ß{ñµ7w¯ÜßZÜK7—Üþ-G íP¦” +­©åþÒÚÒêÊ•K—¾÷½·¾÷ƒþðÇï|øég_üõ°ï~ðég¿ýÝßÿñŸ}ñÅ›?ùéÕ;wZ‹[\¢êäS„/îö“¹jkfyc÷âÚÉ3k['/_¿ñÊëoþúƒ?øô÷ï¼ÿñ›?þéû}ð»Ï?ÿä³O?ûìã?þè¥×¿æÊíd}ÎB"èËþÔ´ŒbdÐ/H™\eqeåþ;½Çñ“_>ÿÝ7þÞo¾üËßþöþúã¿üò‹_}ðÁ‹/¿üÜ˯.žºL…‹&WÄÊ-ØxÍ/W:óÍ©ùµ½‡êÙ_~î•×¿ýÝïýü½_ÿýŸÿëw_þõÝ>þñOü§/¿üôó/^zóíÛ?•©/k1~BçœÔJ­C} Ér?žk.®oß~쩧^zõ™×ÞúÙû~ù׿}ù·ÿøÙû|øé§_þùÏŸÿñ‹wýþÇŸ|ôî{¿ºóø{—oÓUœÈMö -J o(ˆ{së×ï}äµ7ÿýgï}ðöÏ~ñÙçŸù—¿~ü»?üú“ßýõoƒçyçç?ýÙ»¿x÷ý÷xüéõs·:[Wq>wLŽªÌ^µÑm°‘r bŘb³¿µwñú퇟í­_ó;ß}ãg¿üÕ?þùÏß~ñ§Ÿ½÷›_½÷ë>úè™—^Þ=efq›Öâ•5/Í›éB5žÌT»£ÓÝù幋WÏÞºsãÂÕs»gOdzåóXq§ÖJèjBN¨™µ¢4ïJ‰|±Ô¨4š «ëók«™Î‰Ýõë7ÏßsßõýËWö¯ÝÚ8{©45ˈ §?íwœ¼ÚäÔš&;iµ»œnïÚÖ…ÙÕ“áx"Ÿ–V–çööw¯ßs÷ýÜþáðÞ¿}ÿ£?ùìã§^|~ûÜ~ejÊŒRÁ´¢+êÑc á ®nžÛÚ¿‘ªMIÅZ³ÙšëOŸ<¹ùôÓ¾ýã·?üøÓÏ~ÿ»·òöw_õå—^|ê‰oß{mem3W›fÀÑÌ.³3 FÙãjÈ5ȸS$ÉÅc‘å…Å{¯_{õå—_}óÍW_}á—¿|çÏþãsßþ·ógO®­ÌªµX¾lÄÜZ›Í€‡ÁÁéœÕÉ¡åã™B}vvñìéS>üÀÓÏ<ù½7^ýõïþÅçùëŸßûÕŸ|òÑý‹{©BÖ€¸ v endstream endobj 61 0 obj <>stream +Àð„–80¤<6n:>i1£,/dB²Ñ™îÍͯŸ:}úüÅ —¯^½vý;/>÷÷ôò«¯¼ðüsÏ<ý­W^þÎ}7olmlE¤¢ÅåSXjeÂý(¥;Œ2#î€_XÞºtýöSϼðð£ßºtåÆ£>ñ“Ÿþâ?úÁ}7/=~ÿ==øÀ…¥ÅRµJ7³Ým&Rÿêrhø²ÚBÉ5¨ÁìŒÄsKKë—¯\ýΫ¯+=öä“>üàóÏ=÷ÎÏÞ{úÙo_¾|waEÊ•ì.âŒÒÊLjðI•YcÂõV§ÙNÒB:˜ªçë³™U+ŨLgw)­.¹‰€}4;y™?ptr\n1سç³RVŒEì¬ÍËøÃi¯Ð™Ùªµ ¨Ëƒ¬ƒOÉQvXƒÊ „‰êqÿ¨=4¡šÔØL¨Gk" åäÀÞvŠ£Îj³Ž†™T­ÝYÚÞë.®ø£1£ƒ×"Ãë¸SX< +=®#& ÁÆ<¡JM¸£x_ äåü>>ŽEÄx"–+•{s¬ CáR¥šN¥Êk´ºt(cÂy”Ž2ñ–ÕÿÚJf$i>ïb’,É¥ R\¼àûóKår6™Œ”ëU!–ƒƒ´ÓA7 þq:ª°ŒÈŒÇ'tCÆ1­KƒÀsÆI.#e{¡h.K_¼|mcs+%E«¥R&›k5êíz)–ÈiÍöCÇ' Œ*l‡†T‡‡5ð‡IíàN|„R¾'Hb4Œg Ú¯3Z$'„s^_,&*µéZ}ÊMQb8ΰ!«Ý#ÓG”öÁ(+çôçÉ@¡ÀYÜ:‹ËE…üÑ24µÓ›LÕÖ‹—ËÓ{n6¢3è9– ‡ã4ã7!„\@|>ÕÞ#CíQ•ãÿøƈÆÊ™ QirÃ.c¸×ï…‰`(Že)Êk·Ùq‡›f>Íó\¤ªܼÀ°±(}ã°ò®!Ù± H¨ÖⶒQ›Šåzs;·äˆâ-êI`l@Âä ™]a•Í;¢D vNx +î:®<2i–Û ˆuG<Á²“Ë8¨˜ÅÌ$Ña£ZDã൘¨Fx;•0büq¥yÒèPX)˜ìŒÊLÈ „™Ž!þ4!äÝ|ZeÆÔf Ê`§¬n?áOjܸÑirEŽ)l_’×ëjQßà†Âo!…I£sB«­ô˜‘ì“z»Ùáwû2“*f÷JŒ£|Q‡W88*?:¬ÕX¼5âA“¡cÓ*9¤@UJm!‡eæáq\ašé&ßçˆ3|§Œ¶á…‰Õâ# 0c–#ðB´ ÕfzLa“é]ãzjÜàQ¢I³W‡ñW0’mͯ"™± ™¬Ó)Ø0vžaD3$³˜¡¡ óÿy`ôè¸Ñì€vMhJ«W…ppí+ JFSMùÆáÉáÁD /-TKSgRÍSfwâа~xÜMv Xà®ý° +›ÔƒÀò$_ +fæøÄ”\ kLˆWm¢ÆÕ…Ñ"¼qp©Û`È$i3žÐ™Ô•é†dz-ê7“1;›ò¥§¬î˜o‡²Ó—B™”Þ4a - F†â¢ñ}ýèä]êA ©q«#ˆy$Ì›²¸ÂñÜ|¬¼~hÜxd\'3Vä +5 ¡Š²)-Ââd0]ž³1ÒÑIã1™aXiÖ Œ?³@'¦œb3/ÒÁ´Ò→×»”¶€ ;øšXØÖ;Äcr‹Êäb5-â×áÃ*Tãq_–𿺥)38üPx°¿°b +“ÇâŠzBU‹'rdÒ ²°n¡FŠ+•PÙE ™0y‡”v=æ›Ða_?*¿ë˜rlÒZ4¡²Y«;¢·û&Jnô|cHT†(-¬ ªl"ü×J&´˜0®!´v¤þÁ$=…cx0X8ÌÄ|º£søŽNšŽO˜l¸0&³<ª<8b8<‰+qÕÇ…{:D’[á1¢D ë T¨Ofe®(íy}¥•CH‘WÌt\af&•Ž£Ãúcf³=,,#.qh |UDؼÜÆË̬ÌĘé”;Ü!‚u5Ê›ˆ„”Dó¤/½`r' l&MÌÑ éMÛ»z”¿ë˜ÊäŽÚ( g’>?–#ß<¬«æÔI³3<¤rXÜ1²ÒI·Ør«8WU8Æ`ÑPîø¤ùؘ^k¡mdÌ1¸Ü´æµ16pĤAY±²‘# +ó¨ÎõÕЂ3TeÝT{;›A˜æÏ¡l† 7Üá–Å›Ub¢Ì8*·Éh±Á=þ£+19#:L ¸¬S¨š0É-j„1’W´ef3cfZ2B¬ØY<ŠCÐP6¯ç ø`ÄTµÌèÖ˜‰…õýêì …Å¥E$³ÂþªÑ V?ï¨Pö®­ÆÆXœü¤“ëÐÔÇaÓµ¨.B%°Í ¬³«þ°Ì<¦wéÜns³(›žøj̈àÿš\a× nºqxs™øúÁ‰Gåp¦¤Øvp% ˜ÔÑ#r ÒFGáx MÉI‹gÜèV!>ƒ3®'bzG¨2s:Q[Ô@é´ÍCÜq›;¡²ùá_ÓiÌЉ®1{0`ú³«õ•û£µ…Í/7S›5Ó{ Žî/QbÅ+Ýþ¼ÓW0;C2¥µ £*(lÕÑQÃð„å0üWŽ€ÓÁ"üC™Ù‹±©11¡wÊ”å7ŽªŽ[µ(¯³óÿr×è±1ÉÎí «ÝÇ5ô1•ë›cæ»Æmàný•‹•¥ó‡Ôö#iõ•ñY¶°.T6¨äæË–ë++gÐ’áQ“{ÔD«ƒ3ùjdl~ÌÂ&h¬ì1 +‹ÐJK–ÁŒ—ÁÈkÜŸ3¹üV&böJz*fóçp±h¢c66ãŠv¸ì2(]!(] @阛Ϲ„œÁ%¨~•µ¸BCƒù-Úcz/0kpg§/k#o_ƒ§€DOhìÃ*DŽ´.ÉÊ–þ²“/:…²‰¸h(}XiSÛX£3lvÇ¡ºØPyvõ"ꉙ4 +¬,h¥@“s¨7o¢zuòËÛ×ùdëШvTaÕ˜i½Ýïðå\^maà BÑäš0•[Gµ𪉌»C5. ^©¾xŽUCiŽ¨Á= „hqFh±âKtFÏÈ„Ù‚ùOü®QãWóQ³ {B _¬åàRN>§°yA 0ºä™L8|…hu«´x ñ@÷y²ÍµXeCgŒ¨ XóQ5Ge%ÃróáQ= Ÿ™÷eæu®Ø˜Ñ;nó£B%TßÉÎìã|aÒäµ19XsÄ#É N…É5®Ì„¶:8j9<¦Ó8dF/(˜ áärD‰hœ }­…K..õ¯‡eÇb‘†Õ•üêþnáÀ¨ 6B¦'î:&S£êÁ‹>%â·*€(B²MFJGa¡0^GŌޚ±úóF*FËÓ+W–/=®u‡ ΠK¬°ñi_|:ê;#C*ê +WÛ;„/ÿÍãÚµC‡î¯ô„[ÅÙ‹±ê&¯,¼EeåR¾Ül°²*V×ÃáÖ)<Ü2QÑ@´rïCÏû»Ð’›G…°L@Ù<—œÇùªÌH©QßÈ`®lÀBJf2ep§¬žjSÑ–ÜBÐCÂ2}uGÚ!˜\1“+Š2iŸq.í鉶´¸àøêk,XÑQ 9Ê+lHШvp=¼ÂêC½Y&Ö¶Sq§‚ ê r‰PW“z7H±ÙÕb¼Ñ±2I”˨1~LïÕ`ÇÀ:BÅQDFê‰é³:Oò¨ +UZY-âÓ9üð +#³zmT!ã`ÓC +äkCÚãæa5¡¶ùmtŽðWÜbç¡5ŠVÎ1bcÓFgHe㌄hÖJr#‰“¡½‹œôõ£Êc“µ;˜4;ƒÇd™Á1tÅ%ÂÁøÓÓÒmÀV¢¾œ H8\3ââ±IÛ˜ŠÐ"A‹+s§¿¡´ +#·¡¼QOjTŒ©±#c¦ÁMgV¿•Ìxb]!38¢¶CTÌbµñ£*bpo2*à|‰“zpNoœä£#jÓ¨ÕuõXñ& ddÒÊ |ÞÄ$”¸@§¦ƒËÿfÓ½ôÔ9_~ÞÎÄ…dÇ›l›Ý¢÷ë0nÒBQÛmd4YYÃ|™C£½= h"¢._.R^f“ÝpvjõÌýV ¤£Íu©·›œ:ªoÊk6.kõD‹õÅï¼ö“¥³·å6óã\–à+|z9TܲûJ¤PŽUÖ5ñ¸7¹$,ÐpÇúþü‰Dç“žWã‚ã˜pE joöŒœFW˜à N±ló¦©h[*-ì]y¼Õ7¡£ú#“æ1ó˜Ì6¡§@Š0®`çò([0’â/âbÝ(ë]AøP£;î VÜB‘ W*í5=ÆM5$&„’@“Ém‡Çô8›vxãû¨ÒŠ¸ƒ ¼l²cåÒT¬ƒpÄ›ñ„ë6ZL²BE×a¢Æ’›ý¯É –QÓ3à;ä:”›€Ã›´"Ð΃/æH‰ð˜ÚAòy……ù—c‡Gôr=e#%ÈwD JnÀƒ¾XƒRGä£j›ÊÆèNêÑ;#•¼Òl¼ušŽ·™T UѦ+Þf3}"ܺ¨”—®Üx¦0wúð„Qi¡tƒë9ØG*Ü CQ¡ÐãfWDffßÐa¥o›ê Å%>¿`¡D€w©½aó¥í|UèD'T\N·¶+ýýÙûlLDm§˜p•OÎ:›ƒ 3WˆIt¸ÜÜ„Õ{TŽrR?Ù;—š>Ëf¬Þ¬ÆS`‚× ±lñ&mló¾VÜњݟ%ã5.ÝL6—¹LψséÆZqéj¬»j˜PÇ„Ú¸Ù[l¬Ÿ¾þt0ÓÖà_3’³;H-‰Å;”âË2½gLœ;S™»¸zîñhý^wq¿2}jD9X)TYŠ´w¢ÝÂüÕæÆáÊ).+)â¬0󚨸ٓ™0s@Ñãz'ÀÞQàįBÊÁˆÎˆž”ìbƒ+oÒÙe•Ôá~·Xpðy“B˜”ƒÏ¹Ã5±´LEêz;Ú¢2Qc'm,,¾’âR¢`IfÔJ‹ÂH˜\"¸¼'1Ŧú6®`õæ\áN¬±,¬C’=4fÒQ¨g°ÜW0:•£“uŠ A PKÊÁÔ//¸›™’ì ì‘æ¸Îù/FGåÖã“ȶ“ZBnfìÞ B'íLÖêŠNêÀ£éq ¬kyA(, +‹•©Ógî~Ö­#l*TÝä2 ÎP#ÛÞ-ôöÄÊú˜Õ;¤°Ù¨8*H¨“ËC# ÊIåpûS¡BßäÃù‚‘Ñ:#õüÅ%2Þ¥„B¥³µ{ëy9âÅù\ °­m‹K‘úf¸¶áMÍj0¿‹Ïzcõ}°ÃWôgëë·RÓûƒ¸:ÜL†•vÿ¨‘Ò`:BtëÒVcå—h{­`qÎ@©¬§à‰7k+vïyvíÒãÙÙ3v>+ÆŠý­k®Hõ¨–êóçúÜ<&4…üRajË%dVN,Ÿ`}+qª äƒiužXiQª¯jñ»†u“&ð€+T±ûsƒï‚H6}^W1L¼ž=]Z:—šÚŽ67ÙÌÊgQ:üès?غxGGøŽ«PFš ·÷ÅÆ®+ÔÕºóµ¥éõ»¿qܨ²ñx¨Å×N$æ.¦æ¯0…¦ƒ…ŸykfýÒ0•ó$z±öÞô©‡SÝsr {ì܆8,·×õ .—‚t1Ai ¬NÁJE!´RñN¸¹#MŸ­nÞnl?õs`ÄœkmÓáÖ„Œ¸„xbõÙ¹Rzú"¤uˆÀJ3 ¼Š{% ÂNHà±I› s n5K£üAà™ÍŒ :Äg…XAI,4¡¯ôš! kì&œÑ8&, lºÁ-¢\šKÍDk+ðgH…‘ÆŽPÝb³‹©öiдÁ8\ nÂ8§?¥±sr+#3sãzfTCªø˜“`’L¤¤Á¼*Ô3iõX˜d¤²–Ÿ»_ºb÷Äó­õXmEëllŠËöii +õ—Âåµd{—‘º@)— dç¹Dœ–:H ¢Á#ˆ=0nD ÌÎó¹ea•IÍšÉ(Æ¥Bå\,¨1?!”¨H-Y_^ؽµ~þ¡úò'e«óg®?Hw²BåÍù½Çöî=¿v›ˆNëÉè°–@)IÌ-Ž ®At,RfñBü§¢¹…94a™Ð» xèNƒ‹2N„ë `Á2hH5U_X8yƒ ULdHëðš¼I£7 ¥)/Îí=ÈL# uÏ‚:Cõ‰Á$dR‹r¡d'ÛÜ[4à"Âem\ÆæË»cSîD/Îåûg6ï~&3}ú[¡ã=g¨iö$!C“ãcƒ^Îálò_)Ç”3Òc3kîHŒtGµãÜV*¢FY˜:qcëúSÝí[…ùKt²¯À‚GƒQ0§î~1˜[ÔÔãa:ÒÒã(9!· @ø»†õ* Éöœ¬tø¸ò_ïR8¬þ +“_÷ÖùüªÙ)iQjzT84¤šDMÚÉ1¹‰¾’›<Еùî:°÷„‰|=ùr@#x´­@}Ÿ#ÂU#™è$jÙƒMÔW4"F†m® ò Œ€Òx ÆU ŽÑ½RaQÈtïUUXåVÎÁ—¢õ-©wZ¬,z…ÌöÙk û +Œå³ýúêÍÒâõXs·Ø¿˜Ÿ»D„ÇT®Û½I°´3bf +Î`+TÞpò‡@„Uv“3ظkñärF*‰ ƒ»#m\Ûo£Lgã©ÖZfz75u*\]l,ž/Íž!Å +kH­ídk+Ó=áNt•DtÌ âñE¡ìÂ;*C0_ŒvÝ¡–+Ôrø+ÇU¸Â챺£PÒà8E\ºŸí_x€S Ù¹ó×}É,M).8ÅV¬¾Ý?ugãÚ³éÙsPº“’K¶¨DG…ú'Lô!™}DçÖã!p"­CT ÞHk!µ]øš?·8sêÞs¾ºvý¶°2bdì\Fìqe "§Dø˸¿ Ut`ÌOë‘ú6&?iöB|Õ‘+äU . )®8½ëlðé)&ÕGu [‰ cÝôÌeKÓ{èøàÅñÿº‚Í?¤"OXµv´¸¨±1ß<4úõƒcrX®àŽOÛU-:"wÊ‘ ‰L ÉÐG“zZnbåfb”Ë7쇂¥%Ä +§»FgÌNÁWX£’ ÎH×W\C¸ŒÒ<ÀE`W3“†Þš„ŸÈ ¤Ì@©Ì^ˆ-J£4ÒÀ¥ßÑ1@beBµÁÄõ1Ãq%: +ñŠ!¾¬3T!Ã%_¼’ª¯CǼ‰òÜÙòÒÅx{“ŒV¬žˆÞÒ8ÓÉ(¡hÆ +½’»+4-OÆ›ûŹ»õƒáÏ /qO¤¦9ntË­-u†ZT¬‹ú‹%²íõt}Õâ ˜ìÞHa¶¹v}nÿáųîÞóíDkÇé/žÚ½÷â½Oc|Z‹´îÂWüÙåtg¿:ÕèJ~sÔt4Ä â]2¹2 Ý=¦Â¶!‹Aè†s‡ëPh°l¡ ..Ζ/YÙ¬Òê‹•ÖSÍ3bv±Ú߯ ®ÎMš(O {þþÈXû¨Ü¡BƒØ vmÞ<ô ¾ @;HŒœ€Ž¾t?_ÈMŸZ>÷`kí—<áÆÚé{ÌNQiã¨p3éƒÀ +¹eÌ_RácZB†Ô7€ù « +á…Âr¢»lƒxS‡Ÿ—“F׸Á¥5³÷X{çÁüÒ݉©}³7{׈ل…–ö±x'LÃ:·…]ˆE†J'BåÖCÇ•2f¼ßb5cF*ÜÌä©Ø,Ð)¼â¸Ê¦C™aå`Ü„Ž[˜Œ#X©-]Zºô„ΈIèì<ª3XrGêîXÇ“^ ³+&OÒì Åòsˆ'¢E½È lÆÉ€ú ¨ÍÌÑ Ã]Ç•‡GµF´C +;týàÛ´Úâ³:r=îàÒîpÕ—öçûÑæ :ÑE¹¬)V2“> ñVÀ¹$ü%V¹Ø””&u‹<0V‚°“´QIè=ˆyÒˆG:&³B³C ñ ѸäfãòÁlŸ +•Ÿ÷yÂY2ÒINoæö£å¥hn®·r1\œ1Ó6Ñ+`szN(­»c³:„¦‹/8ØÔ¨–˜„l¨uàáQR‚p‡x²¦A¾ðWý¸\mc˜HË—œT¶kë÷¸C*›Z»êINLIõW7w T’‹µcµµ£*ü¸ý‡¯0¦wÝ5n80n˜0’—ñDÛz\ÐØ(.V±’¹‰RéÙ9%΃gZ'!ì(­ lºuñ$Uff\ã„øhÌ0®qØÈФž\Á NybB¬’Ñ&ê/ØÅtûzNGÓU©µ ?1xR +, @9œ/y³þüÚ¸‚°Ã-ÖXižIl,BmÂè»kÔ2®!²•…Zoc0&N(ŸC¨ð¥Õhó¨ú¡q‹W,‡ÒÍ!¨„#€xA*\e-Ÿ|!Ó Þ]QÙü.ƒù³6OÜ›èJëþÂZªw^(®Q±¦óBúN7·cAëCÍ~™ŠcDe‡Š¶ÿÆWßË04VÏØ\ #&Úð/ý]€—š>µpþ‰…‹O¥æ.º¤ix!†Ovf¶šý³.¡$·Ò23õU"ï —'7`ŸtûRXL@DbÒä;ªÄÀ˜&ôÎ1 vdÜ0µm ƒwh«nðñ™ÆáüñTs©Òßj­žïœ¸»·y­òžòü9¡ºn "‰ÂÌàS&ª´Q¸?ë +ÖQ¶ì;F"À¬C’uZ+§|uZÌà«Ì´ÆêÐO‡ÈF…]þ”Îáƒ×2Ó ’#X¦Ã•duiõô}N¡ ;ðåÁ7ÎýÒ l žô¤òE*‰âü¸?0¬בjT ø2t„Íi0¡R¶µ>®ÅGÕƒVÙÅto/7{¦·yŸ]„ÞÏ6–»«—Æun•Õ£>X¥I-®µyìtº)Vî;¸$Tš +ÚÍê%‚e_qYê_l¯ßë—:S §_{û7l¬3ª÷"lÅ“˜”W7/=¹zñ)"Ô>8f59‚—ƒ^køJ" ¹ü.¡MGzî@à|TÂOÜ#¦tÔ°Ö#7rt¸áð§' „èq¨%%!7¹õ.(x7„P ƃU-¶Ó)H¬j+†¨0Ó`úÃjÇ°†×»µV¿Æì5Ûý\8Ÿm/3ѪT…¸w9XšË‹l~ 6Œd‚‹3KçZKwœ¡!5*7»­d÷&éô®µvrÉHvÊLoª£<â-Ù9pÏ̸‘†×cô88ï° +L·¬LÜÈ:ƒ…duñÁ§ßxö{¿*Ívð_fšËÌfº§âÕÕRëDsæ%¤i!KK®@‰ 5¬TfBϳ|B¥%Ó»‡ŽC£ÆƒÃÚa¹uL ¹>è Õòííƹ¡D}:gXMq±,,‹Å52Tƒ`Uh­ÅK *+7‘&Po" æát'–:6f<>i5'PqÝ?¢t€ÎëPóHL¤¦2{ XÀIÁŸã’=÷àšº,æ+:)ß\+uO@7ãA6á‚ÒHê/ ¯e}ñâë (krŠÎ©Sñ6<Éá Iå­½[€ëv_%TÜ —¶™pSº\¼m¦“Gev›Še§qŽê&ô#AºõÅûªË÷Bõ˜ÜNp)&˜S]ÿzTþõ1Æ”[V2Í&ºF2¬%ür u× +Ç6„Mù óúp¡â˯°éy„Í +ƒ;£ë +‚uǪQÀÝÞé©­ûÖ.<¹}íùÙ“ ¬„y%+ÐaAè£ã*ûáQýñI³ÂDh+$ѹFµ.•‰W2à,.HéÖÊÊ™ÛÅùS*‚ÃÁ ˱ú)>¿4¸Î6Ö„W 4ð$Lp„vŸ ,Ï›FMTO¤[§œbsTKí<Êd0®h¥AiùQq#x Ëâ)7º†U6••bcµÖò¹ÞúåXm©µ¸×?yÝΣLŒ•Út´Þ^>Ÿíž4¸ÂÔCÝÅ3bº iH‡úTH@ƒ ŽJKzħµ0ÇeæáÁ8>,×Фé¸Üâð¦| éé>ÕÃ9ijåJº¹i E,TŽ¶O§¦ÏÃIÑáZsá\¢¼™ÝF%ü©i^švú¾=qlÒôµƒ2…Î 3`†Ç&mƒ÷íQpæ¸Þ%7Pƒw‡ô®#2“ãZNˆ––=¡2ÔÒq9js†)>;®¶5¨Í´ÚDxXðàq¹mDn™ÔÀ/: |䨜pzc„'ääâ¨'ÎÅ['¯<1»u°Í슾"xº˜_F©˜ cPw`Òè׳¸£<¸7ï`rÞh×쎫1Âö…ò@8ÿrXÁlNaô Élà +dDƒÉRé9:nU£*ZÄ ‰šËƦŒtÌDFQ.­ª¬R¡lûZ}éB¢ºšïnW—.äûûþܼM†²=w0'7¹ vnÒàïSÚ,ÁÁ$i± <–¬mhàLefµ…B<‰`iaæä½Ë¿•íîì^zx÷Æ3©c¢âV:e¡$œ/³R?Ö<›™»îIÍ`L"Uß W×èXb‹n0í–S¡"-vLÎØ7†µß<®=¬°›¹²~âÉÊ߈ê±{’baÁB…´(#¤Ú‹ÛWo=úí¥3÷Ê‹_} šë­ßÝ\»ês:Â牵óÝxyÑ©†2-'Ô¢\´Ñ’Êʌ需‹“zÒê×Ù³3 +û~ð¸fDf•ë§/K‡Zˆ·`&%.\ê,žæ¥ºÕ#²éNyùrçÄ= #í="Ü”›½‘doíìƒzwà¸Q˜H0D½C4`â„Úe°ùI6]ïè­\>p\s×Q9†Å¥ã=Éæ‰ÂZi‰ˆS¬ý½÷—ç™çûOÜ ¶%’bhvFÎ@¡"*P…œsnt£slv ÙÌ¡™D‘JT–­`IcyÇöȶœgÆöŒwwf×;ï9wïžûë}¡sptšBÕ[Ïó}¾Ôû¾Ocñz¹w1Qœ‹—¦Ý´Šò±P¤bô@ ½Ò„“3{D¤K‹ùææ‰!»ÙCÙáàÄÀŸèXò +u·«½ @'dqz‰ÑÊ„8èáEie/›ô‘D¾—,ÍZ döpL…ÒìaG þ1³Ÿ× +J¢n°c¥zs{dzûO¸Ì‚'X°áÑ©¥£O~ø+=Ù³ÀÝùé!T©ŠUã ©JŠ¹ÂÌ«[`N#“Ф³{9'b…¤7¶ú„I›„þD×yç˜tB‚ÉÁ8 ÉÐ¥xëÊñÛãNÂEi~>Gê\¶/—W´b¿;»ýàÏrM€ÃéÖNvúPÈβñi¿XXR ·†*¥¿:5tâÜ€Ä!ÚIÖ– 9.¦«s >É‚H6\usÃv +b3„\·â'r0/ŒºG]¬ÓÈH +f±`ª¿r½ÐÙ ´R(3kíæúWÚkw—ž”/óÙéúìÅ7¾ÿ5m™1 ˜TÆ’‰NÓ•»è¼TckÐ3å›mßì:á ú¨4*ž3cç-0¨ ÆwŸm²«À`§ºû¤Z&B‰JgKÈÏ%óíÒô&å PÐEF@TÄ€Í1;HÖì [½’ÅA»` €ÞÙ ?P{RÊ [‘³fœÐZÙÙ«jm“Ž¶¬DÄ‚È #øxÕP=t Ï¡xo°‹;>¤ ;ddÒŽOØ0ðŒÕ5a: oV®¹°0)fÉp–O¶ª+7º›Ç½Íû³;çv”[ >\²ûEˆŠ:ü¼Ù3èÙä! Q=¤7uðEZiÜ Û=!,OZ0—_òe²cç'½æã³€z<„xzÒò¼Ûè`&쬇LRjò|¤êÄD€<®@ø|1?SY¸Üß=îï>*®Ü°S*Üòü¢TÜ\ÊË¥]tâ2ÊA¡ÜÛ0ø¹oŸ›0£VŸdprŽ€Á°€OÏTû;×½¡˜•øükŠ¹™äÌ^¸¹í !µ±wÜ[¿nñ Tñs9>Ù¯ö/µ¯Ié>x¹Ð}úüÓtçâ_ž4œ7û >΂jBb*ÝÜe¢SfˆŸÙ¸ÃÆš/Œ:ÏYP3ªÚQ@j€‰ÄL/Qž^>¬/^2c&gC©«âJ.^_^Ü´vó]Ÿ˜WŠ‹ZmÃÌYQÅ ªªš Dšš_Ð + Fˆ;9æ‚ÀuÔ;ß4Zš|á3FâÌ 9wbÜ6æÄ\„Â&zBq5RßÖë;6LÓ’ãgŸ._~ +4­WøÜ4—é1±6Ðm"š})³í}8˜?÷o78(€ '†].<u¥³yéΛþÁcR@k`jÃHjS-¯w¶sñF£¿»wëy¶³mðð&q°:èQ…G|äà~ŸÅCÂgðÐã€àìÔéIdÌ4c6ÞW7ÃÉfej«»}oñèµ…£Wg.>dâmƒ_€ƒÉXu5;uÑKEH¹à¦“ »puÔJŽYÈI;=i%œ¾`,Ûgåò·NNœu™]´ÑŽ›ì¤É#Œ98?›žÞ¸ªÿÿuÂxv>9 6¹xÏŠF¾sÖ1馬(oÅx/!"%`ÝkëwÓý«¨ÞE•2ŸíG›ƒ-‘ßûf.Ð|8¿ßFªCöÁþ½mq³£fôÅAû*ûy3lCE+žð‡F=t@+’±²‹S¥üLyùF¤¾­®¦;uRÉÿl,`É¡|ÎMFü.¤òõÅ£—™HíÅsf7¡‚?p©ªV¬˜î¢t9;í &N;A¦Oú;®äÑ ´?žkÍ®ízk6„ת«õû³{Ïf¶Væ ý½PºŽÕŸ~ð·—ž~6á—¸–lí6WŽcÕ½Xõ"íý< Ñp¼† ™;ub 99Ž Û8#¤y˜l83sáƳÃã·¸4â¤&aÁFF1¹œl¬§[{±ö%g0ëÄe¹4´ÚAéƒåöTÜ ‹$œí]´ó³ì¸êaÒnB·B"¨° H÷’šëZ‘°ÁÄå’^ßÈNLmÜ®- n\ +ñÃk/ï˯³¥; H¨€ +e®OzBÀpb¡´—ÔΙý `¬`©¤¶åy).Ì,ì½ñÞåËÍõ[‹OÒ 7S³×š›¦·îö·îDk«ñòü_ýþöÓˆHK2\³x˜u¼Á†Ë@7N { ©`ô±§F'‡,ß:1qjÈ @ÒŠÇ‘!W"…yR.ú…i¢J´©Ä̤—ÅÂY9?2" •"õµÜüQ}ýNm㸸rWiî’ñ®œìmì?™»ð2~¡Ì*YÃõ94<5hïnB_sîàçŒþ1GÀ Kvr°óŒ\˜• +ÓñÚÜÌ…{3»Ç«G¯´Önè ¿\qQø›ß`íˆ0éÌ7CÅrº}±0s%œ[€ø츋¶ºãçGìØàM.ƒ†L|!œ[!”ò°“s’* fÔ¤"5%ßUæÁ½ +Ñj¬¶œéî¤ZRvV-/©¥ER­„cÕåÝ[í•&|‹T–[ÙèMXª :ša'\ݽ-ÌŽ9P¬ˆnÇ“à[ËM_¼ñ¬4µ °âRlfV,¯æûGÛ7ß½òôõõGn&«Ì­îÝC‚qc‘âj®«¼ø Ö eŠ³‡‹G¯'º»Jq ç¸Ñ:À™åןÙ\<òP &2s9£‡39ñÁ ‡1ïKÞs“x065bÃþâÄØK£îa#2i£¬~Å*ÚAŽÀaF.ƒO$ïîööŸ-]/¿pÃCE“ÞæñöÍwê³ûn½Y[¹.Ïâý+|y=”ší­ÜÞ¾ö¼ºpP—˜ŽÖ7ÔÒJ Ö·CFøÅ!Ë©Q—‡P]xØà(D¸É,æ°pÏÍH¹éù;Û·ßòÝH}!ÛßlOT^K´.gok•u+&Ù±ÁmeN”Þd3„>M'æ¹á¡’Xu:æ f€°èŒ›ƒ–šøLF’Û@ñ˜Ÿ‹òé^núReñpq þq1]èm ëaT©0__¹½võùÌÞ+Ri‘ŽU]”ˆpZkñ +©d¤ˆ„‹ŸCs (‰å§ùhÍêLî Ñ=hÎb ,‹©^¬²êȉ!›'€C(%R)ÄjKõ•ëryÙA¨ó‹ûwž¼Ï©9O@Ív/¥:—ôòë¹è!3ì‚Ž+O²õµ3ƒ&³‚›NQzG.®&»‡ÑƘÖ/Ýxeçè>@x‹_$RY¾³uûƒ¥ëïóÅ5 ¢¸1ñÍ~Ð_;ss/ù&Ü!::#dWüÁÂé ŸÕÌ·7õÚú9j¦@ÃÂ%>=LNã|rïÖkW}ÈÇZ6DÂ?Å÷ôòJoãÖ…;ïúW`¾±iR®Oº™“#ö¿>5~✠¸AðQ>TêðwÎZ,FIöÆ­ÔÐê¦Ó-GÝ̤]hæâ¤^56“í=H(Ór>×ÝÍ÷* ‡ëûÇ×_ù^¦»Á¥»«WÞØ{øýÞÅWA½¸õì‡Õ{T¤ÏÍÍ_xJ¶F\” ¡€²:àÜ ;á„yY¯”€ó% nh.µúz¬»[_¹Y^ºÂīݵ£üÜ¡R_—ëlzUkƒYÍÑ:¨\l¦g +È®PÒÆfÁˆÕ_ž€5'›AÔŸ›÷Ó°R‚Õº—/¡R +å!.m‚€ÙfýtÔKk.:¬•Wš›ós×’]µô­$&êÓ»÷BÙi1ׯo>P;x' U±N-_v`¡qg@),3ÑÌ°‘ºQ .ÚGi!½œÒwÎÚOyM¾ Ê'£•5:Ò4CÒ„¢uÊÛýF7H~ïÎ[û·Ÿ·V®k¹>­–Lî€ :5è@gðÛ ‹W–㥅pª;jÃNœ·Ú;¶ÍPÀ yhÍGHÉbQRg&ŒR©¯Þ˜Þ¹¼pYÊ/x¹üàÇ.z÷ٻًc °„ŸËXuÜ Ø“3{YNVæé['œŽºКHÛÏÄhåúÃç—î?á;9dµœD„ÒZbr6o#”TÙ Ä”y°=WL¹±`¢¾”éíæ¦wÓM¹0Ãê•Tcµ»s·³q”ëmñ³3D¤éåR¼^-MmSJi0•%4ðó~P =b†­>ürñ1 +hÅKG‚‰vmùÆòµw¦vg÷¦æ·üÕoï½I 6¿êGÛ{áâª^ßno¯_«±z+ÓÞ¼òø»x×€*Áì|´}11uuæà­™Ã7Ë«·sÍÍï~ù»W¾û•SÇÜüˆ+8æ Ùèd Ú ç—Òµ•ƒ/OïÜÎΖ–®fg÷åÊBqþ ¾x´rù•‹÷ßÕ0ÕXÛ½ûÞ•'Ï<ÉÏ^²}DÌíÄå¦G„S­˜T—*ŒÞ€ù,®!ŠV$4b‡€7Õo.V¸ìDMPÀW¥¿OEJrºž®ÎjÅy&>ñe›óÓIÌŸý¦‹4 ›Ÿo™t±ƒÆ6ÒGň žôQ:¨JñMkuàÊÎY`£—qˆpARNó‰fqf¯±|Ö*ä`MÇ73îü&7ãÀË&ÙXËÊF7À4ÊàM:èqnñ2&DŠ7Ö+×rÓ{|fÖͦÆ+åÅXÓMhŸñb~:ê5?7ú8Ï`é2bÆ­^þÔy×É3¶g-#&Ì…ˆT@¸8ðü/¼d2؉3òs–“£®slÔ˜°bX(î ÈR‘òsáÂ2ÎÊ`‡êK¹Þ…hs;˜œ +HY1VÓJ}TÐ`N µÎ)ÅåHm °žÁ˦•€T8; ap +€‚½¤npR ÆÎŽ;ƒrà!UBÎ ¹Ùds³>8½¼·{ãqsq/ÓÙš;xÚÙz¼råÍþÎýDm™‹ùX=U™[¿ô€M´Åìlyájwã6€Hu9”j“z1’i_¾óÆέ׬¸Lö©hWÌ-Íì½ÖÛ{-ZYnL­úåÏ÷Ž_¦Úzu ØíÒÂþî½·_ûþ/ÞÿÑ?¾üÑW›—^~ï{{çõï …y!?ϦfµÊVméîÊÕ÷Û[OýB‰¥J`ðÓÓ?é ºiÝAÅèÔ\~ö&Ÿé{(­Ð^&„,­5‚é©@´Ì$.W³Ý½ÅK¯£R’Õr\´ŠÈ•L÷°0w+˜Yq’àüéXКFC‰I¡Újö…¿iÎ" ÛH?—ŽU×1!g‚_([Y¼•šºHÅZH“óVd° çÍ>ÒK…Q>I9<œ£#eTÈ€Ò€ûµ~#Zžˆ‡Ë6Xœ´l šHeăKO©%36Á!(˜¤½Hõ®4à`gc8óR:£– .vȈAtŒÔP¡(ØH‹_<5ìQê!4'&¸p áQ Dx03ÐÉ@düsØŒž™„ÎY$9‘AÇ.Œ×cõQc%ÛF…„lr‰)@IÁXGÍÍÉù>T‚FGr^Ze¢-.= Øø„ËJ±^895 +ØßI9Á +q“Nê̘óäy+€'ÆB9&ƒ"‚ˆÙl{kïþƒDkc|¼R_¼"çºz¡«d;Ÿò’*­TXµÊÇ>&ªg•€’÷ó9' *F©Y^/ê…Ÿl|œ^Yæ3ÓÁD·2{˜k¯«ñbµÞ»qü¬¾°ÍjùÚì…íÛ¯>~çêÓï]~òþÁ£·–®«>ÁBh¡l_olw·Ï>/­Ü6fãâí{Ï>Êt·Ýl’ˆ4¼BÉ'Öò½+ —ßNN8 eÿÆ+éæÚˆ“X=æ +Ù‰˜ËgÚÀ¢3j¥ÒÙj®Üo®cJÉÐQ©ˆTc­ ¿”7z)FÍ?‰ ip-ì¸äeL´^š¿zðôù™}æÁx˜Ãqë`㣋ÅΡ±Ž +Y<”§ÚàtKLrÖÌ90ÀÔe ΔÚ2y€ |¢%e{˜T2á¦Ï }Þ³®€>j#qsxzÂÖ[ðan#îîÁ:|r2¡ +k3}6Õ¥£MR«¼"+Æ}Œ:æÀœ ÒáܼZÙËë¸ZˆI%Uc¢e3Ä„b­x{O¯ï$Z>ggaÇGM~^.ˆ‘’ÁŸ³ŸµŸ7€…ýtÚG&P&™i®{˜.¤Bñz´¹F'Ú˜œco^€¸È¤j€%¥¸ª”7{^á*("\´á²@fmˆsIV¯‰@(ä"ÈúI5l†ý”föPN܉‡½¸È†ÔH4Í4 >Ê…“ÙÚLqz9ÓšQómZ/²©†\ê'›óµ¥=CÕÞæÖíçµÕ`$Y9Óšá^<+ÍmÞˆV«õ¶’êóñ©Ö„bõ tPXÜ:t*,TBé…DóBwûaÿâ£æÚ-!=Mõ›wŸ~õÛß¿p×'äåʆ־TXrñÁ÷çvï§ ¼žyôæGsŽ'<Ás&œŠÍ(µÜÌÑêÕ÷•Â¼«ß|øÞî½÷F\ì¸7‚Ä”ÊõãÛï¯Ýû(”[HWï¼ùƒxmÅä šüädkofïéÅû—çopñ髯G + 'F\/¸F¬¤Á\V/,4–®øôþÑÃw>ûÊøÄ*š§â}.½8³ÿÆòïÆ{ND¹tç­Ãã·ÅTŒù£d–Ér1LªŸžÄ‡ ÈÊæý«/›½Ü·^šüÖ©‰¿|ÑðÂËϳ`‡ÃÀûå;˜˜'µ"äç:ƒfF—åÁ¥`¤`ƒƒ6„÷21&Þ‘K+ù©ÝÎÆm.Ýsb‚žŸ ¥z&t°ÌjÜÅ[ ƒ‹Ah•âã6a°z‘@È`ƒÆ혓ŽºØ¤y°Ä/å¢/Í(9?“šÙŸÞ{¹¹u/;C,¬9 ;Ä0jѬ,®Ò±QO f´ÂÌ80UNvÀ!™!ÖK#Æ ‹f(H„r ²[½¬Z˜)O Y„ŠZ¼!*†cU5Ó†yÌæóQ +Â'¨H±8{qzûvcéJ(V—SM>Ùvl¡NIi”ÖÝ$?)O8g ù›[vÈ ND"&R…²ÉôÑq,˜vãaw@ö³:x»¥ÄªÅÆbº¶…w3‹ýK‰Ö¦”i©€ †£¼qÂÍœ7c— ˆ9?sá’ÕÇXÝ$ÁÆÄdW^ÊÎËÅ%.Ú,÷÷›k×cÍuo@Ndj‹Û7´ì´7 +¹¾”æb BÎ)sã²Ý/ƒ}½"ç °ÑrSÉP|FÊ,ÐJ a£ I»ÅîxXYJk‰Þ•d÷@ÉÏ凘˜f¦—zë×ïV¦šü K +¹q :ª©”»yüö'ï½Ü‰Ï•÷k¾$—Öåüb0œzýƒ/n>~gÌ„žrŒÛi`þÕênnùˆ£ÃhUNÔܘDÃÇè|²¼_~j;Q[ò2:ÉE:s»‰ÊœÑ´Á.ù¹ÌFmÞÀ„Å7jpØœP€æ¬N—ÕíG¸˜Þº@vðîøù^××Äìíæ:ÅÔ´óõåDyÎäfÚ`ÁŽë_ÄEdbÒ¤ÅÍ ÑZµ'ɲ@²—Š[ýaà„™3Éù˜d0Öô:DE€¡2yYÿ`öð<¨Ÿ<ïÁEˆ”g0ûø!Ó7€ƒñcFÏ„™´áà‹À Ï#*ÜTÜà +hùž”ëZ ¢R΢Á8%¦y%FŒv”à¢Zºaóâ7âÂÈt8Gð)p:`  +8^ 4˜ÊÜxÆáe(VA)ÕáçmàªMR)û訬ˆ„Hãâl8íCùI+|nÔá^ Ÿ:o?3âš0ù $€“¼)%ò}áÇn@ìhÌ̹é¸W|dÈ`’ÅfHËuօŧsÓ‡Õ•;¥…#³›u#D„0ã†' [ÐGɸ˜âã5>VƒYᣌƒÑìfü¤Îdœ ÇÌÛÏ:<„Û‡‘LÐë‡1ŠƒH‘OÕa1mÇeâ„øÜÒÅõý»´œ©*gfÓÍ}1½è!T‡—4ÛpRv/ëB»q@œ—!&A„«(›vA¡\_¿råÁ»6\4úh&Ú|!'0½(xðVxÔ¿7i#,^Î +‹FopÒE›}ÊÆÁÁûhŃó±Òl¤4/eú|²Çé4”u "¯•CzÄ• WÄä”’[ór5Z&7<á4Ú¼§à\ä…³¦“çmc¶€—LúÙ¼‹ˆY¡0`1)5…ÓÀ¨8°P]‹_²ã0Æ@í0~ÌeqàvÀÎFïÙ 70$NBçÅÂn482é +J95ÞqÁ¢ÃË9ü‚—ÐÀ!Y<$ðŸB¼DwMx¬ž ‘<¯féPÒ ‡ þõ©ÉC¶—†çË`ýc˜*Ý(7lô¼pΌӔR“jQJiÌŠعI÷ gÆÌvŸ Z ¥Ã©Ò”ši±jÞ…ò&2jvDZ ¸Ñ–h&Û›gLþ¡IçИõü¸Õë'0‚ ) „…h¢Ip‘Bu&–n€?œ`˜µ –$nÇÂCFØè X=4J²áœ MXýF;bvávP=̘sùEAɦ‹S±bÇOƒJä²ÑÍ‚7º ä‹çlç'|6ïàÔN7Ž9+Aø"­6‚Ñ6«•ì>ŠbÙÝ€œ€\\@.K‰N¬´Š¶mˆ4éÀý”3ê`fHØE‡—vù£ž°M˜œ?m÷,î0`\©p¥A$˜½,(ŒZšƒðU>¹L)uð .Êh'Œ6äܨÕì"@"®gä Âê#&èÄiÜèiOo.ïÜöâáq3d²¡vE²ºb>Æ`CÏÚ]0‡Ð + + Ï.åƒ)c½góø& &—Çc ˆ,˜„ÙÄà>µ i´ã/œ7øÇÐÐyÛé3¦³C¶1#H^ ¨t(|‹Ï­î\÷Sê˜ 6ÛI *7csÓ~,ìôr#“!R“b‡/äÅe³›>?áýöIÈ ³ûBàx^<=îÃxŒÕ 6lÒŠŒ¡3#œnˆÃ騑¬ÎJé¤TyiÌñ3“ýÒ؉!3ÈFJÂ$/HÚÚÅ«A-7jCnÚ‰‰‡Cy†#¤nl]ôÁßMBÜ_¼0tjÈhu6Š1!’"Yi}÷joí²ÉG›pM:ˆ >aÇ}„ˆ2" ‡#=ß'BˆT=Ä UQZtCJ0t(f‡H«¶9 ‰z¢À‰ûf3 „HÔ†ndÂ鳺2”¢¤†Å+Üì™ '8eû`ÜÏhv­~ÑŽ‰Àg‚¡°xp:œÖÊ‹vL²z‹›ü_ŠOprvÔêiÔ6f…†ƒ»³øÀ‘(Ñ”¦éàÜ VϘ٠ô<­éå…o~i!ýt$]_–’m @¦Àe2åÁD?­SbÁ‹«`ØÕX5’¬[]0t‚¼çE‚ÂZœ¾I‹SŽ&×÷®x`jè¼uðõiÙsV 0(©‚r‚ÊädPØê€-V×ð˜éìˆÉds <ŸJè™tœ5¿Üã24 J˜åܘcxÜãòquæ¼å…“£/¼8j°D0ƒ1q›3€‚¤¦äH–•2 ¤ÄðZÃî_šxé¬qdÌn´À­» n|Ò54j3B&{À‹©nL7:èÓç-/$C .¶ Ox&­¸? ‡¤Œ•ŒÖÀЈ×ê Ù1l6úln žÔ„ÙƒJ ƒ&ãJ±Yó3´Í„Ôx²P“¢I?´¸Ýn[«ÉBCLT^2Ÿµ[œ@­¢’¤ÀIpd.ŸZXâéq'4löY¼,D8'„#ŠžÔõ„6;;uýÎý\½nÂ~vÜzvÔ„à IQ¢È3,ëp¹yžÓu%@¡Fó¸ÕfBQ¦Sius}nm{¥¾4/Åy9H ‚〠ºýü ÓiN…yFàBr!eFÌ‚è¶glÒ¾"–)¦êJŠj©DI.˜rùi“==bûÖiƒÑÃy±sã0t‹— iÙr§93Gy  £&¯ áY äN F<”œ(M1J† +gÐ`l†Ø</ôa Ɉ(vAŒQ.…Ò*I)‹F$A +“  Qć¢>÷V*‰©~ £€*b/ewânˆõ"‚ÍÍXœ”ÓÇA¨äô«Åf5Z­fŽãŠ©H·[î·;7{Þ\ZéËaɇí^ +Ô> »ÿê…á“çL 4Ä`py\†bHàC!– :Ý°1E£Üë÷á £L€ÒBRÖâ&N›^89|â¥És#ö¡ÁG®ÑIôÄYç gl§Î;ÆMˆÑŠÍ«Ã«ÄÒn(àöâ\H·;€çGa"‚3Ñq³÷ä¹É1£×磿ÕlD»ôåL»¦÷¦KÙ’M +åbdg©~´¿¸°ÔÌçÕ\&"GtVÌNŸ8=::nv8<II4FÖõ›ÉlA J+²AE&耢rÙ¬0ÝÐŽ¯oÜ»³{ãÊâÍk«©|òܘùܸÝáÁ<1 bp†DÚWÏ…ûíD5/èTÍðËS©ãëàñÙó£ßýÝ[_ÿâÓ·Þ¹77›S42ÀR4Ï…‚ Gu¥XPµD­’ ó´QqŠ1XV›=ıùB¦Ò®Ì,Ìß~òf²Ö²@¾ ›wh ˜ WJmú˜Ô™QÇÉ“§ýWS”l6OD\»Ã5F‚ò™ û¥f+¤ªý•y5™°C¨É¼s>Z…HÞëuðxèšžJå”°Wظ¯ôó[k½F9QˆóKÔ­£•‡·¶ž?¹ôøî…£ÃN§¬ETËkw@f¡2Ðdca¦pZ§«9©œ•º5m»»¹Y|vcþý§>|rá¿úâ7?ÿþÓ»;ë+Ó¥RŽe8—±»€Üá~RÃHÅBÓïMHÁ|LJÊt³ Î´’íjja¦qå`ýâNÿúåå'÷öܹ¶»µ[*”ÔˆJÓ„Ãa±˜Ĉzé¼}ÔŸ:=yêôÄð¨ÅjõÚ-v†@£2_.¦%žáH4“Š¥ÓIUÑ55áö'ÎL€„5Yý‡†˜ûåwÿå·þò³¿ùìê¿|ýêÿþ¿ýÑ'ÇOn.^\¯ÅbºÇG9Ý$¬¨Ê'Ø™ª¼1i§‘©{~gñ½{ýøɳûçÏÿüû~úÝKþÕ³Ÿ~ru{6Î\«}ÒàôùÁ€@˜×"“¶nš¸¾šú›·÷¿þÁãOß¾üÆíîÇ/ÏÿÝw/ýñ§Ï~öñÕŸ¾ñÏ¿yõO?¹õöíêÍíÂÊt.ª‰±X,(ÆBáT @*¬§ñÏæ™+‹ÉW®ÎÜß-_,~ñîåß~õüÏúÑïùáo¾<þŸ|÷ÿûþôÇ_}t¼WûôÕ­_~ùèõã͈ÌO=gGFŽB»Õc7ù±áñN:°?ŸÚ[Ì­wÕÝÙ轃ÎçÞçñ…wž^}rÿ†¦Fl>Ò±$‚$…@3Ž-—©kóò;×ÛóÆÁçoìÿüã»ÿágÏÿß?ÿü÷¿xŒÀÿöÁŸ÷ÝÞ8Zîe#2 YQŽĽyNÃZô•YùÑvú³§«?ÿôî—ï^úäÕͯ>¼ô¿zú?ÿñ»ÿåwïþî‡wÿáË¿ùâ棣¹t$HR$0!‹ËüDHLI|/A>\S_?̼uTúü•…¿ÿþ•ÿñû·þôõ«?þààw_\ýן?þÓOŽúÞÎÏÞßúÕ'û?|kûÒZ.—Ò- \äû­â|#6[/ÍG>8žþ»÷/~úÊâǯ¬ü᧯ýúËóüâß¾¹õÿÓÇÿç¿ý¯¿yë_Þúo¿ëï.¶"×tndÔhóx<O’­t°,»J¢u¿Cß^‹Ý\‹><(½ûpáË·w>y¶üóOoþ×úü~üô_½úoøä§ß¿·1_nu§ô\ËðUOk¡j4°\ÄÁÛß½ÑúåG—ÿþ‹›?xsë³×7úñÿò»÷~õùí/Ÿo}ýñ•ÿüë7÷éÕOïÕÞ½–ßl…’ + ¨‘QËÉ#N£QØšQïNƒ»<¯ï•Ÿßžûù÷ïý믟ÿ‡Ÿ½òO?yùŸ¾zòõGW¾þ`÷G¯Í??*îM‡+Q\ °Ï`$8«¹j¥Þo¦›Iª›@ö:ô[7Û_¼±ó“üÁÅ_}qýŸ~öÊ?ÿüÕ_~ó×îüó¯ÿç_<úýo|úrïõƒä½R6.Ù]°Äa8!ŸRr^ÑÑínøú²r¼¡?;,|úlå÷?:þõç×~ùÉÑÿú_ü¯ÿô£Ï^ÝøìÍKw¯­£áô€½,B»Ë2“%v;âƒò'OÖ¾|¾ÿÕ{—þí7ïýŸÿöË?}õÊß½»÷öÝå¥éœ"PõÀ +&CRRt‘&cAßT&x¡—¹¶V¸·•þákëÿðƒûüé{ï?Z{ÿnﯮüìÃK?Û¼y¡ºÑ/r… œ"ù$@$ÀbƒŠ¢&Ò‘H”Ck:¾ÑÑ·§¢‡³Ñ7Žê?~gïWŸùöág¯n½{oåÉ~ûñNåêrf®Î'õ ,.œà²¢Z àL\¢‘àÑ^.ts£ôÉãÅŸ¼³óÛ/nüýüñǯüë¯ÞýÅ'×ùݽ?ýäß~ùêo¿·óùƒÒñvº“å`¯Ýhóñr!šh±œÊ|+Š¾)Íue:ôx¯ðôRõ»—þë?¾÷çß½ó‡Ÿ<ü—_½þoøèçß¿ýÑÓõç÷¦[‰F(҇ÓÙ:GÓJŸ¯EëQ¤ónT©›Ë©'‡ãÝÆõÕüÛ×»¿þôæÏ?¾ýé“õîôŸv.öSY^¢ g TNfhÄC8áµK˜­¢b 5}½—[m¥æ wwʯ\ê>»:÷ÊÑÜVSÙn„7›ÚTä:ptŽSÀó‚Qœ±BŠ FÒñDRá"5[ŠnOe/t"OŸ¼¼ð›OþõOúá•ïÏ~÷N÷ñzôÎœr¼šÍyÄŽøüp‚‘–—Ð&-°Û‹!n»ˆ9³"2Wn5èÛ+ú£ìûwzüÉãÿýç¿ù×_¿þ»/}ïñÞn¿XÏÆ€é5C¼"];䲇$*ÒY•ª'‚³%e§ŸÙ›ÏÞÚ¬|xõ£§»ÏnÌ­•›@žŸÑb3ØÍZI´Ó•9Þ¢i‰Á°|TÞœŸÚßš«Åñ ]ùþnóýW®mM,À{WÚÑJJREZä8 Xýa¢úèΧ±PJKTEQ @¾%d>!s•éWô™ÂÁRýâBy­›iåâÍt¼×T‘A1ÔˆÑEL8h˜Š{‘Åâ´[,~—‹'µtj¡•¿0¿¶÷ÎÌ—Ï/|ïáÒß;úñ;W~ðlóóÇËß»;ÿürcF•h·×eÇ)Þê AˆW vÂëÁS‘X)®k¤£¢úÖêÒÅ)íÊ\ôóWÖÿËß÷?ýîý¯?½ýÅ›—]鬖zõ¨ ñ´˜äõDE JvBEKÅL)¢"‘՘ȄÙfZ«Ç¥NœÞí%¯®w¶{¹²B¦¥  2Aƒ6â§G|C€™¢˜A\Nòi’˜Œê™h4§«QžÔX"ÆÓ•q{–Ë嚀ÐÝ~Æ ¼ŸdZ g`­ˆÓ…Òd0ËMuW¦ê½¼šÉ…/-òÁ‹Ó™vr>/.¤^&<•Q3"c6L:ì>åò8|zØþ“cãf·Í"=ÄäÕ`œó5ãt?ÜŸÖŸ^é½vmîÎVk½UPá¡p†ÏÌ¥ºW`* +ð¥HZà‚EqÁ ÏeS19®ã*+…¡Íò‚Ÿäì87bó¼6?O)£Ø1‹—#-8¸——‰% >—CfY€b‰¤È+$ÁÚæðÙœ˜ÍM˜]Ĩ6!N"Î.Ê…%˜KÐR!%¦‚|Ôã#Aš°¬\­Î6› årOk½@òá¬Å…œžsàv"æç+©Ì†‹0! †GM~¼L„¨(ÈQEÊ'•©Z|g®°ÔŒlÏd/Ì5ú•T=®&´´¦Øìî“C“ŽÁÚö³&ß ç-CfÌÇ€!žKã©&I§e*„c¬o¹ž:¾¼}ÿúþ…åþt¹¨òŽ!ÙˆSí˜2îf(½l$ë{Ñä4I…»Ý… ûw|59áD±$%b±B*]ѵ4àíVÔå ŽŒ{OŸw1+¬½’ÁÍ;Q•&,fÛáµ;áƒoÌ gnõ0.$¦†šf F»Ó ´¸(ŒÑ=XØOÆH¥Œw žà·N 6‹„aB)EJªB¸âƒHi…lÉe¶‡y=¢ù {q?„P~ܲ9QÅO%ÆÌØ_~{ø[/Žž÷xüAžW1êóá>/LŒÄñ„×ÍÁž á Á¸Á˜`ZkêÝP¼ËE»ÙîN0^¶£Œ—R­)­Êùy­¸Q– EŠÍÙa›÷Ô˜cÄN!ᓘa¢=T(øØ$J§¶qµ2âáô`Q†›Š›|¢ û1qÒâ;5l™è%t\(úÙŒQl¨lDü› ·Gô¨“wqî@”QË¡hUòáxã¢à¤˜s⪛Œ¢b‘J¤\äbMV¯|ŒrŽ@Ì KV$löË®@Ô‰G Ή +­RRÅ)¹ %;B¬Tr~„ÆHŽu’O l“6ÈK¤Üä2«N:9jÇ_s½8bs±.*CªM£—±¢#–Ážü0pívÃ,#ƲM_@´!ƒåÉ›v`;¦¡Ð‰‘Áö×T +H­° +ó%#¤˜¼A"ò‰®˜ì Q£“ñQBÌ‘Z͆E ^Á +Ë„X#3ád†Í“ÕËl´PêN2~ÞÆ­$ÅêT( +æ ÁÌœXZŽ56ƒé¾ŒMz8_@Í6·P6¾:ëJ™Eµ°Æg—Ï»…“ÈÉ Èä,ú«cg'|„ÒË›JyõŒnþÔÃâb'lÌ·OÛNO@.*AE¦ÄÌ«÷Œ<îæÏÐq+†PšÓÏ¿4gâì°cÒŠ»1™•ò$·OšÜ^?çA÷³ nnÌÁLxB\7!ê·^œà2óžP ŧ—®d§¶l sn6‰ËyR-£­\ïâ$4Bœ?”b-'›ïµâ¸ÖN5/®\}K­®¼dÁ °ˆÍ…ò[x¤çå²Xœ,KW°oîŽÙqÍI& µËÄû¸R'0¤YƒO0#² S `žŽ¶…Ìt@­º¨h(ÞÖË+¸˜£ä²_Èa‘–\» ”7B¹&ÖÆÄl~æ W-0OȃvZT¬‡+N.ëfY`[¦ö;ë·tôœ5xCþPÞŠª.&é ål˜TJùÖv@k ö—ëN®à U1¹g€;1ùÅ1æÀ¬P0 æq¥ëc F'mvÑ”TBÄ¢SÁñ‡K+àùÄšO¬Ú@#2Ìç@®Ù` eÌþÁ&Û.*æ +n.?ìO0­=ÔydömxËp(ð`p’—CÅ£wÆœ¬Õ”›ÉÛ (4XesÚŸôÓêà–帋4ú‚^!PÒ[¸\vSQ¡Ôñ“°lg2\n•ˆõ¹ì2—Zzi¶p0‡‰yvàq 3ûu În2nñ‡‡L˜‡Mq±).:eAÔó|Â#BbUÚ¬R±ù¸³ãÞoŸµ` f7ëÀu£W´"²ÙrãʘÁd .ëÐñi"Öã=—{iÔï—°T§{ËÇ›«F<ìdãvp}S=ƒíÜ#Ád›ì4ŸîN"¡!7=á—ýB5Þ¾Ÿ¾¦6v=ƒmcE4\¡â}*1çáËv: ÆŸy”§ã]#6@b@kó…m6½‚ʃ-Èì„S6B1A<.±‰®R^Ó«[ZyMÌÎÉ™ÙéÍ{¨Zñ…Kjk¯¸|·¾q¿¼rWk^Hv)¹ª&:­õ›F,ì c ´¨Ä|¸¼ï^Žu.‡Ëkj°æ”PëˆXñ³ >Q½…)ÕêôAoû1&—O™`ªðÙ¥Hã0\>€Ä¦-4ƒ¬!c—2CœÙÏ 9™17oõK.B÷²+åÓ|fÖI«¶€j§3°Üƒ©µIõ­¨Ù'»Øh œïy;édS^¾äêX¤‹E bV/˪e:ÒD…¢–€è´Ž] +VÂù¥qGÈ€6……›à»LXd ’ÏXh3ªóé›9k‚íˆ_*áz‹Lù¥Ê$$ ¡Æ¦t(08A—e’s°Ü ã3~¥eDuBëˆÙÅaKÞŽÇA²€ÏgbSPG0݆jt´kôH#r°s/$ÙóË~±6f#F,ø¸3òèÞ §'¿}ÆôâbÂâµÍĦ±.‡RÓ$ †—Ð;lj.1uÔÜ|š»e¥À‡è‰n{ᲑˆL;F=l‘ŒöåڮܼHD꬚—ò}¿\"´:QL®‘¦XXæ3}¯æ’íþÁÓÖ…ãÁ¶Qåu29çær ß3õµöúíóN —ôú…hûJræZ¤½‡Çzv6˧ûµ•;‘ê†Á+‰™ùÖÚÒÂÕ\ÿ¨»ý˜JöLÒ÷M×Zˆ#M«.Þ+.Üõn åm7•r’ Û¨V=m%'\Aˆ+Ðú_às«ryÕNˆ„Z +klf™ˆÏ&zWò ·»úÄg’íƒ@†Š… x¤Í$z\zÓg&áÁb·P´]›¿âc/⊉v”ê.A¢Ó¾bð…£¥u>>5ìâìdÊŒè.:ä–C…-\ï›±¸œž^ܽo‚åÓ&üœÂ… -o5/¼ŽJÕK/?ÿø+½´xÆJC2¦µ£ýkùµ‡ÉÙtrv’”âP§QwzqÐ K âG«î†ÒËŽÙ+€c†‚Cf?ЮÓVzØ#ÀJ‹N¯¨ƒo9ÏNxlˆdF³_³¡;7@ª3‰9©¸6áæF»oÉN2æbÒBaÝE%=tÂÊ!J+Ž·.WîúŬê€Jkl€  +æ©h= Up­’è^ eú˜Rñ‡+Áô¢Þ<Ô[‡\jÎËÆü¼Ž2‰®7”†„"¦u0½ïj`À³Ó—ÓÝ ÛÁdèX@oÃR-›f2³¤Vi.NïܧãmPÅÀ{™h;9u_=f²‹|¢Õèï.ÕXºº}÷ƒéÃgÅåÛ±Î!ï ^ܺ½ï#,;ȘšÉL_IÏ¥fÓ—mÄÎѳÆì W!.‘è–—‹ ·“½kÙùc8ÜÀSÚôÖãp¦oô‹v2 +"ÁŠE€ëv1)X,ÓóZeä2Jwú;jaÚiµ­nµ×Ž»[SS{\jŠçfû—'<¬ÒÈhS«ì¤{7"Í=­¾#—I¥)/a“ÓáÚŽXÛ$@úˆ%½´Q]½Ï¦§´žì\ÔÊ«¥™½¥ý‡õÕÛ¡ô4%—š3û¥þ*æµÂüòá³ùƒ×Z›kë÷c‹´šÞØ»·|ñ‘P͘æ—ʽb \Ù¦S PÖrýíÛÏ…LE¹¸Ê¤¹üZlêòìåçõÕc15;·ó°´tݯ–B¥e"µ©STjY*íÆ;—Qµ‰‹™í믗g«ɸG({¥*¬Ô|Á\´¼‘jlO¯^.Ï]òˆ5dЖºm)IÅ€Ò–o d“Öå¬VßJK±Ö!¿ œ0?j Dé²×¹®¬kÍ}­u)Õ¿iб(@GOç÷€÷«s5h°[\‰5/† ËÁD8X0U_«Ýq¿vÞ „KvLÉõ/÷ß(n>’»J}[®n!joØΓáj¹·ò«Ÿuá²Ù¯ø˜0lˆXµÂÚKãÅ/áR͆ëgLƒOÁ¤*µØä2€SPôh»™¤#7Ãa03Ø@ÞËYÍÌPÖró\¢ ‡‹6TÄù4°R|fF)/j lq•ˆO½4á&„V²³Ÿè\ĺî`z xr.kï’J &3ÓûååÛ`ü›ë/§úWÑHÍNj¹™/,FéUDšÓP óZy •‹Jb5)Y‰–g¤ü,Èn8\&Âi-?ÃÅšçí8"À[¬¤b!e2Þe…ܲ”›ç3].Ñraíÿgï½~$;³<±wAý Œf{šdU¥ ooÄõÞ{Þ{“‘ÞUVeù*’EÙ$Û°gº{zvÜŽfG»FÀÊAôèEÿtnµv±Ò“(±ÈÁ¬ŒÈˆŸ9çgnÜøÎ/þëËoþ+wþ"˜¿hªtŽôhqvû e{?´O®¿lœýR¾„(ÊRNspöüÃ_už?È3”5‰}úñ×Þü…7~R_O_ýí¿þü›WÚˆÚcƒCkürùò—ßÿóäùÀ®>ÿâÏÿþŸÿ×ÞæUNhð­+¶ù˜k^®~}ôÕ?5ÞW¥º?ºt‡—w÷‚X¥¬±mê€N_þåõçQŸ=>¼ù¹Û¿£µ\?¬¨½³€òìm¶ ¸ .øp±¸üvsûc8{Á83ÁvWÏ£ñ%댵ѳèð«úÑ—Áò phE¨W…Z}ñ ½!µAs Šö#K†„6h.>ï}¯ÔÛ‹g-H"¡Y¤¼"SObN‰m(Ó`úŠÔG ˜•æä]YˆÀÈÃÌ‹µC¥sÛSDíPfG­/ +¼'Õõõ«`þÌÝ‚ë\ü‚mlŠBàÏž˜£«‚RØ¥!Zî™ | V—kÓ/gï£Å3£)4P»O8ãhþX7ú”ÙW­±iLoÚ«[wt™e]Þîù£Sˆd¯k´6àeÚG_À +áŠõg #ÕÖ™Ô8̳¦RŸ„«Ûáù×ëç¿>ù1Ç:cYõùìúCì[¥x18œ~1ºøbxþ%h$Ñê­SÀ꽊YLžœ¿ù=(wx üîW§wÁؽ&W¤ÈîŸt'‡Ïž~þ›Æä‘Ûfï‰=xnv®Àæ€e üµ×¿^Ý|wòâWU¥]UûÎìmãìWÞò £wenho †ýüåozëç`ÿ¥K»óÎæýñë9üýàè]kñ\Ä_ÿãÿ´¸ø¦jLüå›úq¼­zûôëpöœÔz‡_þóÿò¿ý—ÿ- v‰ ”ö¥?{Û>þ¦¾zíôÏíææÉ›¿xöÍ?°þ5º¸;ç£#³{-5ÎhwEèÃhp¶ºùÀxÌœ¡ú8ÏƧ­Ä`ÖÙ¼Ü<ÿ%®6Eo¢×¼Œâ¿cíi–1¯šµƒ£'¿ä¬ÉvNLVLPkãóËw`ýp}tÏþòþûï÷(Enå…¢<¤umò²1y¼)Ú}³ . f“êfçˆóæBów—|´©->¦¯ƒÙ30¼àÇaÝ•Ö¡X; aO¸èp{ùüÏõþùªpî „ Ñò9ãNp­K]Þ5ÞHÑ`Ê^‡«×ÓŸOo{õõ¿\~‹]1œzãËé b“ÐA;- kÊx ¡¶Bµºß;PêË<âÆ0ÏÕ | 48k¯Œ?‡ƒ_½ýÚX*VZ'Zÿ"\pGíåË£·¿¯(mÆ@Ð +î ²>šÝ¸ã«J\z¯©5Ö¨ÒÈ“¦ÎüÞñäè¥Ý9ÜC´]DÏÒaŽl€_µÎj‘Ù\5W·ë§ß-žþ-oc†òfzû,O{¤Ù÷'O7¿:zó·Jï¶$t“U+œûãë’TÛGÕdE-°uÆ^šý§ÖäyŽq«b4½ø$"ð©wQ¥ V·µxÝ?üÐ\½KXQúþì¥Tߌ”Åž_aÜÎøúæË¿š\~Em€/°½£s0S·U­W‘@P§§_væϵÖÆ߶¿\Üþ&Z}^;YÌöÛdz‹/!‹9{&5ŽÁ¬1 ÆOFgÂÑcBnœÞ|»8~õq'g×;à³æOµ~þ”9ô+Ñ« µŸÃ]„‹r”‡é=­ydµÃ6þ6™©µÎ£Õ—WËUÕT¯R®h´øpÍú+šÐÖØ_m0_Hq}¥‹ÁÅ`ÇŒÞe‘õ+¬oç®4¬Þù»×;ý~Òñê;©±IT¤æ„ÙÇ̾Ø>ÂíA xÖ›F³§ hYwÐ78ûÊ>µF×Öø*\<Ñúç˜Ñûo¯ 4$ÑZjœªís.Ø`ƨ$· C‡¯¢ùÓŠÖ¡÷•eŽ€‘í1#Þ1Þì]ÌŸü¢,Ö¡cã³÷íÍç 3ÌÎy8y +ñ\r0ÓjËx;>¥£4•î˜ÄªÚ$Í©7½Î±Ó=ÊârÁfî–T@Ð:ã[Ö +jíðôÕæñ×\¬?aÃ%åÍ­´Æ‘,󔥅“p|iu½>tû´¢Oò4¸òvoýÖ꟧p­*¤Ú)Ð!ç,´önϲ¤#€ú|÷[pÊ[HäŠÑ:Y\ÿ8»þÚ}²UÒ2¤Çù«4fi¯@¹„Òua5ÃÉ­ Lê“KÖjcJX‘ë„9V›Çþè±Ù»0¯As~Ó>x«)·N¹úqš©árt‘Ö<Á¤¦ìŽGçïA¸Â A ‘æ(Þï‹rΞ|³8z.Z-%GãóÎæÅôæÛÎáÛ,éƒ[6̉dO¶Ò|ªbrþ"˜=œ~muN+\˜B4\í†Ó[³sö0Iìæ„ñ Ãp3{×­Ão­éçÖLÁ¨*‚ÛãÝ!iöb +V˜5-©?1X–;š>‰¯LƒÙ+¡vPÑ{±™!LZoUåØ­{úÍàâ;«ÿÚ\ +ÑB ÆýÍ«áÙW¨Ö¬¨ªÞ~¿üò¯Ÿ}ÿOÓÛßhÛ2€ WoP½PŒH]À.wp}ýî÷³Ë/Ýáù<fÓ"A áÃ8~ûð«áÍŸƒhínýÔí1öˆ0†T|°2Z—0X®½’È:1:P`i:§eµ“aLivW/­æáV}”®¤1 xU`ö´¢÷aY¥uO@¡%1]jœh½ksôD]^N8y°=Ï¢ÉY™³+R˜çk¤9õF/ÌÞ ,k7!×\ {´[Vòt€S¡vÚX¼ë~Ý\¾Ú.ˆ”Ò¾†ØÎàFžr l,©´.hE¹Ó<¦.N¿è,n!*óE­<å§PCòg~ÿ<]•ö +Ì÷¦â!÷ËR+EÇ…~ÂÑu4z\æÂéõw 6£õ«ÍÄ;sºÉ²ÈÈ‘×^CÌ—±âüÑÓÄ°H‚ÝÙ<~ߟpzMŠ¦e¹ÃÞè©Ðúp?Ï¥¥1yÜ?z½¨YÌJ”Õí¬¡ënœþu•¯å1M¿í5&Äf¶jÈÑ<æZ‘Z„Þì‚Tz‚d„Ö9W»¹Ö5‘δ,„A¸Ú.Q£x¥÷Ÿ¸óϵîeQôÒ„ŒƒŠn.$oÄ;µ¾Î> /¿ç¢MŽÛ;®ÍžvÞ¿üñôõo‚ù-,z§ŸÏnaMžìa}8y¥ÔA-\ÈÍ ¢×;^~[° +Vÿ²¶ùÐ>ú +äàƨ*7Y³ïˆ +.!×Õú±?y>»þÅðäƒÞ:zíôN!2*ó”"M%£´ˆoÈãLrT þ0ËBÐÂ_½­¸]Zqd¯#ù# >˜œ$"ºWý³­rѵ+{ã<©¦ª"¡¶ó„hCäè:û‰U›ŒVO•˜ý “,Ȭª)+Þ+£~bnA‚X-ñ5>XBvBÊpÁJ®“áâ­Ö;Ï1þVžGø¡u{@9#½sæŒnÄÚš4ÚŒ+a!yJ4_Ù‰?£ùÝèñ/+Æ(Q•IÙ5ÃUkõ™T›ƒƒ{õí¿î^Ë4Q½¯××2€§ѺòáÙ·ÿææÿ\ýy·«ŒëtŽÔÖ1i ö¤ú‰P;k®¿r·˜Ú)‘ºY_Í#ÑŸÆ•GÔ^¢bæh_¨uŽ?8Ã>˜v^»ãkDn²aÚ@m]ÔÖïýØ™¶Ž_ýð;féè@è\ÐÑF¨m¦Wßt^Ñf×ëv¿ÖÖ»7¬²¿TëåóÙùWŒÙåìa4}â3ßèís\µí—!#\æÀ(|xS»ÿ˜w–Zý4@žýác%Ze1³*6){¦÷n‚Å›æúsÚ–’zg|þ•­PµKÙó ]ƒ•"„3»{9ZñFaÿÄh,½á5iR¸‘BDÍÚõu>þÒ\h¶Ï¤`ÍÙSTj•ùÆ^IÞ+ +zý€w牲†)]L2n\w S»ñÆïEÂi-£Ñ5„ÖŸÄ{ýI1â!Z™‰Š„›ªê;¶9»^\}C(ÝIž©g(¥SÀͽ‚ðYÍã6B‡„ÞÛŠ÷çäÖ1C9KÆ_ƒDãø ¾/MšB°û0:ðùQßäø(Mú”9-2nÒi£Õ=x{úÅß÷/ꇩøˉ&­øŒÙbݾÖ:´FÁI õÐÀŒQ£‡mÆlËõ•ÒXƒ4è¯ÅÞ%)£}DéªÄœ>}|òüÇ׿7F/K\ƒÑ:¼3¬ˆ!*7½á£} Æ›¾–ç%¾Q"-#šóV;QfÊœ++„kÒæÈ^C.“F_púõ-mõË|ÄûK¹!÷uíðk–°îj0j­ž+ƒ<ë)íóÁÕ÷¯~"yú,Àãôç7­åÓ< áqkóõñË¿¸úâ_.žýÆŸÝV8?ìž>ÿekñ´À8YÒÎ6ĶÞ8­ £´2Þmm†Çï![)shPd#Î]ÆZ´/A +"\Ø]¾ê})7O…Ú!aŒÒ¸ëÈÛ %^M°Þ´±þB“½QdˆDãõ¦`69£Ñ˜>ÑZ§àžªBwgÊ{”ãŠLˆðõTEÃ¥–ä¯X{NÄ{J‡@[¨ÜIWMà‚f!\ù:¼)̤‰?¸ô†K|ó_lW@h•9H-…¹¾Q .:«—vë¸ ¯"=\î" è¤Nª$ïã:°#0ð +_OÒÁ')r¿,Il Ò<­Í__¼û»³/@ýÁìßdH  I­-¥`.4Nøօܯ÷apþc\oˆ³ÆLo.µÈè8Á·÷QÇiœ×oÁ} ‚GêuÚƒƒ›\~NŸe)-Qe15RšGZëˆÐZþ²?în^÷¯¾;g¸=FÄ&iŒø`J!dÓ?µÇÏ cà†ÀKfqµzû‚¶§qÍA{äŒ^:“·bíœ6ú‰2ûYª‚ð¾n²¸[ }ÖëCÑŸÙíÕ\ |Èû37Œ;ëÇÞäU}õ^n]ÁÜf=ƒŠ§Ïl.Ÿ³@²z¿ v¹KÛ“hú”±z¸Öi,^Î? _wÖ/…Úº 5@KØ­£ÖôF¯-ˆX¤ÝáàÚˆqgɪ±•åvŠ¢ÕXw×ÏKŒ“Š·±ò…p=½ønvý£%+:­wÏ_þfvöUY¨SÖxÓî]ÛƒÇZë$¾‘ÍöQ8}Œ(ÍGE1ÇF˜1‘÷üJªsJÍ Fë›oôÎIŽ«=*©¸9.KDØÃÛn%rŒ-۳ϩªñ IneYDløChíŪÙ½óÚê D/ëÎm£üâ³Ï:«pÖD<ñ–¯ðPr¡ýLEOeɜԉó³òÃE«Jí•Yáü¶¨—gÐ1ª;ƒúü¶ü¶ònpö¥3º•H¹¶a·ÖúÝâé¿l}Å:WkŒÓ­ª5¥yb^ ­'zÿuçà›áÑ«sž£MB­KÑœ0Ú¸Ú-*-­±Ù<ûíÙÿœ’¬*EÒÜ>0)®ÔÁcÜ™ÕV_vO¿c¼*Ö†ë[¿Rjy.Ê35T=ï¼on¾¡¬9Ìy™ý6—êgàѲŒ_æë¹LngO~¬€Ú¢7#­1¬WYlB.0öDŒ¿PÙMVEcfÿñàøËÍ«ß‘Îj§¬&Q Ô‹Ó>*1`´}©vèNÞn^þõêÙï‘BEP’íåm{ùC–>¾vå­3{GZSP5Û6®ŸR['JÒnA€Áâ3‡ƒù{ÆžV…0QÊ´%¹ƒ¥Ux•BÁ] +ÞšµÇ¨Xý"ÔáO„ Ò«›¡bä ND´¡Ek ×f/íáMUlU—à=Åë«^å-Áê5Ç7Ñø:š=¢CÔ?ʉé²@Æ—TÕ%0ÔúZýHoaR{¯(ºóV¿³xBi<ÿYŠMSS;³Wß6O>ƒ*Gíuÿð ¸ÅŸî`»E%Aø¨9Z7vï6™˜êMVß>JR?ÝFà ¤57+|YŒ)uÊèªõ¥ÑX¨ÑXô:¸–â­´jnç,œ>UkÊèÀœ(Ø"ÖÕÁu2`¾j+ÂìJÑÚè^IÍsHÎÒ,ŧ›rãÈ<C Ñ †Q —¼;õGW|í Ay Ô µ®Ó;S¢e0¼i}`ÂÊ[ Z/M»E˜g‚™Ã$fŠÁFn\0îŠÐ† st°_}RÇ•&¨‹tE*q€ê5½q.…GR´ÌÍ’m *R;I¸„=£ýµu ®$6S„±S¤!„8«ÀKè:Ø°á!ôßß& óa–Éà&,wUnãzOˆ6´·Bï¬ß“gr}MH5°c´=_–¡Ã½Š^ }­~ìŒ)½<÷ +Ó lÄzk­ý8‰»yÚ‡d$”¦h÷ÓïV-ÀÌ·…`†’"U5«rGŽv™0Æjó¼¾þ +4Cñ¶²L®¢œ}ÙžUY7›EÆÄB¸¥ ÍéóÕõþþßÉN7iÌ‚däÃSTŽI0JÙª`¶6‚;ÊbF‰ÒUdˆ(Æè“R=‹L£Œ pÁ!ªM÷‹b²Èg+¢du@“’ “ÃìÆWd¹ãgRãb¯êül—ÍÆ›c£v˜Au§}¢µ.{YdbÛÙÄè-Ñ›¸½3ÖèÄ›Ÿˆ~‘Œ} +.ÕÔpi¶O½Á¥Ö8‚©ÑËçUTÆ‚¤Ã9mu(£Å{#1\ÈÍLîjO ×fÿjòø—³ÛßZ£Û"çÃ`+B-6eb«¬ šâ½†—j}mwNA-€‘Ǭ1¢ h°TÃJ÷1pV Èžr/ê- +1“¬šßV§´5ÜJa©+¸Ã,á°öU†)ÜϦö…pS–z Ä„Ú ío¸ð(ÃEŸä¹OóL +×8kXå|BðæDŽÚ½âjLjÞO(.LiŽUņѾ𧯽áÓåã_{à ÙêõE²"î#RYlÓöÜéßLο—êçE¶¾_’‹”“#¬ý2ø&ç¯iîíKPøÊÙɱYÜÇ´6øÁ²ÔÄŒhx¥~f÷nsü¨‡U DA +õC³{™ÄF¢>¨÷ÏÂ@ÌUUÐYTK…dI)áºå ZƒÓþü2Uæ÷‹B|BOíȵã áíåŸí”Ê´íÆõd7ËQjWº¤”Þ‡èz”$>Û)SR«éEiÌÎ^pò¨ôWå£"ìä%ÎuׯP¥‘%L¸»]€ìèÐæ~î”°{üíïþÙkŸ|– ¶r®@™ä ­Ìº v ¹ Q ƒ¸¢Ôf¼O¯ÓW£¹èMA˜­§­4NâS.Œ•j%Æ.Ð&p¡µA•¸TAˆ³gÕËpöDŒñ&lF'GZÛÒ¡!ˆXð&rýƒÐ`¤­<í¡J»~ð•=ykŽÞh½¸:¡wAE“FHîà¹Ñ½•¢©vÂzËxÃØ U¡ÅŸäIG­íÆ=€#ŠêÇq\™]«q¢¥"÷(oS’G„97;WVû²HØ´®.Þ±Î$í^\¹Þh‚Í,qÞ^Yü¹óñœ$¦õ2±âÊ0^9Lc2®€$˜úà´ÈÕwÊF +umd¶.Ä`¹Ò·A3#R7@tÒ¸¾‡è„9 g¯ÜácBi=yÿW›Û_Ö¬ª {šû˜•£=HÏ `hª­3Æ7±¬Ò>P-PØV†ç­I… !„0¡Fëc«qbÕc¡¾Sà>–¶×·3Ø^–Ù/kUu=SÀ$RÁ£43X¿>Ýʉ{(,Y ÛÌhFëƒ?ÛE?Ý'6*Ò^¢¤ngxøY #ÑY¨á +壭,(K DUƘêÐÌ~™ö¢ÞÑhó +ü~Ùb½c E¾žgÃ’ØÊrÑÃ’´S³¸É;3§w!… L®åi@W9‹Ê¤y…ó7öð9eMkLêýn}–¦¶ B 00­[`¬XHˆXÚ™’æuÆpË0ÞNIJ¡:DN +W0àh$¸s‘rí +m¬þµÖ>¥¬qUi B–P€3Ú+KÂàí‘èÏ!ت|H)ñnœ~ÿT4$E +Qa€´Þ¢$ÀUYCÎ'Â_s® f5˜¿Ó{O!,Íö9˜…øt·Ùÿž!ÌŠÜ´û7z÷1iÀ‰çÿQ‰Ë3iŒÁhçpò(GÕr èöNµw +€ ºÛ;&hµÅèDï>U[WF¸r¾‡H€NÀtÒÞ©ÈŒ³‚1¸nÅ'C@y–YÏ®ÍÞ}ÿ««÷©QÕG¤·$œYU¯0Z§EʧԆÓ?OT4À¸$:¡ÆTiݘýç ­;Û¼«0>$lÀœð2LÝ¿ÿ\’U‚Q.L£:øMˆ·½’ò('•…žÖy È¿›+,ptf•Tš@XYÂlt.£Ù«¢å0n(ßRÂ#æ2€ÜJ¢ºÌ.ø³ÝjÑ+BCôJ¸ä¬Q‰ ¶3Ò~Éd¬YI¨3ÞaÍá†YÓ4@¨Ä§£­±,ú9{¡5/1mXæ"L®ÓÖ@‰6õù«ÿ¤"õ‹\;Ç4¡›¨ZY€PƯHuBïƒÿÀ,‚ヺÝQgŸ5 B³ ïRÓÕ¸h…u1)âü±Ú>n,ßx“ç mP©NëÎh×Ç݃WÀìU}ˆ!sZmU8D©hõh@ -¤µZUª0òB¥Âz©²Ä꫱‘œ1©¶9lÚŒ^I]ÝÍ“§mÖêA^hÍ#&®e6åÌØCµ±)‹uðãb,à_ýÇBtB;+àˆåƒ=`,€®v–\p Ɉªm©~$?}T´Úç¬=žÚ/‹ ãÁéwß÷Ž¾®È­"ëyƒ+HŠ2ç*HTµ4f@Ì NßÏŸý¹Ü<µ^}ñTb²´›!=·¢Bk]­¾Lô•qq(s +îxa¯¢íø +Jg7Kƒº{XÕF¤½Ô:„ѵ¶“g!¹à…ɲÂ{´³!´ *DÃèÝ,µŸŠ¤÷ IHR&£gÖðÙ᛿yöËÿ.½|d +˜UÀít¼Wˆ/³'õamú¢±|͹Ó¢¿Ãƒ"SÛGí%P)G«ÇßóWÿs\•Ø. -˜Z½§¼·âøX­£†ÊíŠØâPSÇ .>§Á¹ðø°XÚi2f‘j—rí¥õr„ÉÂ;#)X‚ŠK" +¨¸2’ÒÇÔ^ >ÝE¶³ÌVžßE´jA´3~fX;ƒ\jePS­:¸ÔF‘rÓõÁ~ùa¢ +aƒK=QÈ8oörñòóïëëÏÕæEªê~²G L,´&©é¿è­ÓÆüµúX2n~ñs£qÚ¬À·ØöM÷âûÛþé‹ßüÓãw¿ÍÒ^Œ˜ÚÃÕ>ç-J|4³ˆÁšó¸ÑOãö~ÕN™ +mÒ¤@œdh·¢¶‹~‘‹OÇq(4(}@êÝëƒôŤ†Ußt–/!3”Oê½a‚4•e,hk r0äë„ÜÄ(ƒlSZ¸1©-ßF«7°ÄnÎ+Pf‰?­(±A‘­a +˜…#Ñ_ÿ>ÌÑ»šT¨•ø×G`!Íî“`öŒ(YwÒóf=®4¡wp{Dû+!:b½EŽqó§ró¸¹ù<\¿#yEd«ÃŸ0­‰ª ²²Økǵõ×õõ×[Y*Q–Q©‰ðqùBÐ]ˆÔe‚#TéÕ´ÿlMØ<¦¦ÊB%þlqH#Dj–“£yY 8gàO^½'z\ørJYðÑ%±&3¯ž%-ÌZë\¬­Ak•˜(YÖ¶óLUý ¸ùév¹D»¢7ãìYžmì"ú.¢î ô”I‰ A‡PƘóÖ´5ƒuÙ+‰»ùveèt\LÙË€JÿXæŒRGJt’¥|>XJµ 0;˜Óy~»hTägÌæÚl®vó0&,ënIÙ)i¨Öå‚9¬õæ`ð5h9­}¥7Ž%wL‹µ4"…6Ä6(ê$nïV4¸eHWòÎà‰=~ηÎ+ú$ £_'} \íŒ iw ¼ ÑX#iÜú¸ãh“n•[°š9ðÁ‚E‡$—«]ÊEˆL"V¶ÍÅ»ÕË¿mžÿ€*8H‘ P©ABè-èv_ëûœ«mª`a@ýj@oLjcJ“‡Bp&5nôÎMUéîW@BàÑä¥;xƇD{Ÿu–`X8k°]ÁøGÓ[öQ‡Y&‹*ø Ÿ¨µÂÚU©&‹Éù·ëÛ?gƒy†õÒ´ÅyÎ_e(¯ õÞ­5z%ÖÎòLø(K&á#™ø +^® iK¹̘€Øía +‘q©fpüÆ_¼ ÝMY€~ê%Æe­A‰u 4Ø]W̓/Z‡ï[›÷ˆÜ¡ º(‰Äá´c“exN(3„Sy;CïìªPû,íä` +Þ]†óY6xTàwÀ’v‰±Òñ%=Ê^I~b‹t€ÅÔ¹^$}\êÐV|åCü"êäè: ŸÙ{ #}˜å0¥ë oÑ ©vT¤з[f(¹•¯hŸíaYÜõ×ëg߇óÇU()þèQ«5ƒ†çÃÃpñ¶{ô]IìT•P©ôÓ”·‹(»e“šµñíàä磫ß«÷IÜÝ)‚ tìV’Z}ÈG§¥ZçLH2HŸ’ +aY•:rtR –2QÕ}ö9ýG¼úø]@*©„ÚQ£µÝ¹B)° ðÝþð™Bª6Š ˆÀ¾Ö9Áœ)îÎŒÁ“ÁãßZ£—YEa¿È•(KræFó’4'\¸±ÀâJþdèüTfU‡^Y½K1ZCO0uP•ûR°¦Œ~ž«Å6ë®êë÷Ñòs><ª*=pǤÖý±Õ>ˆ·|í ˜¿êœ|Ý9ý9ãor!SÜÞU8{Y‹²4 ­ ÎH4ñ’¾ÌÁêÔ×_ΟýAl]Äl%6YkÂX=Êj½#¡~FKT‰Ï”¸h¯$ÁXò¸IÈ51œ`“jÇ òYkÆ“TÅØʱYÌDØZî 12xɘ3PÚRä϶KœŸä)2a¸`ížÔç¯xwšªðR³šk»uúÉàö6"Garä"`ïN‘'õ1à¿R?Ë.;ë"ÛJ£žä‚ËNcÖƒ$™']’ØúËÁ^Qy”¢ª´Çê-Bk#J‡ +G?Ÿ\ÿè Ÿ ÎRI +æÝ£Ï70÷ +€%ý#©q\$,ˆLlúÝÓhxMnanUŽœÆ= ¯GE.EYˆÚ—Û×Îø™Ü9GƒÁx9 ñL@v×Uu ¿ï JÕŠ\¡{©õoŒþ çLÁËøÃ<Ã*pî’÷À›sΘ³UÛYÚbýy´x1ºúnùúoøÆénE9‘BDÉêÒZ+OZ)T®Q[zëJ +6»yf¿ÀƧI¬m +\˜¦4ÎÄäú<¾ÎAnSÎì›\;D”¸-68æý­wi£ÍÙ=½{.Ÿ-Ÿþ8{úksü¬,rL2@ç¬0¥‰ë!<‰+{6O*z?Í€Ø+œ¯F+#¾^÷¤jNHÖLkž‰áð)m÷Û›ñy‡[ŠðHsæ´ÌÕK´·›£Îq:ÇFûTnœU¤nŽ 38gC‰`•ãR8bCk Ñ!fŒŠLc§˜&ƒøôaž°âr?\Í‚ÈŸ<Óš§`lS¸iÛœÞ*ÞüO>ËÁD)õcøTl^ÓÖ4‘™¤RUD gO ”›§Á?¼ ¡ŽÔ0þü`$QV*\ T`]wÊÊÈaBŒ PðZ|x-\¼Ñ뇔>$änU¬ÙíÃþæe‘°Q3´W[¸=K3Ñ^ÕÌ2Qž‰h}"§"¸X¬‚Ö¤¾Bø^–Šà-¶JBEí ú°,·rBˆZc{üª{õ{gùnŒ ¡Ò \Íhž×ï…Æ5ªO³l=_»Ò@„0.1oÏ€OaAË1öò¼R3ÀæúÀÄ_¾˜!öúU}yêީª€cO„ÚiU )­•Âµ$* ñåÆ>¢%+FµÓU£ÊÇ Zƒ³Ûƒ³/¥æ!BTí’®fGŒV´3Ë10x|éWíPí\±î"ϸ`Þ¼‘8§ br—Ý“_è½[è-*µ³˜ÆùsTkçX¿À‡jçdvýýðì³yÀjM\ˆ@Þ×Vo0s •£k`s8ÿ ±zkvOÒ¨–­e¦Á ÙŠÛó SƒdÙ- ¤ÖÖë«çÓÎ’°V|pÜ?þÀW{¨ØJ”U +d0¨&©}Œo’Æ’´ÖBpAòÉ>–@dÚzß«˜uàÍ_˜³·þìÈ’iæAŠúóEƒ<ß(¶çJýžTûž²  °*X¿ÄXí O VtP¦ü,o€ðu\$Êú^AÉ!MÆÆßÝPšyÊ}˜¡fowNƒŠPËá¶×9Gäö®§¹ ­»‰3ÒX­K{¤Öa ŒA +ó¶‹z„‰/¼Ñ{—­ Ê“´Ç´³¨Íã­ÈõÑmžkî£Vž ñ†QUnAÙàŒ¯]³þ!§Ìù¤ +XÚ3ÚWã«_Ûã[&Xåi¯¢ÂB”%°äF +·@®Xƒ—Ñâ½Ü:*Æ[C( Oª ¯ÌAŽ v!Kûb=þR¦6?M²,ä`@€­Ç5 +õ WÃŒ¡Ü:ìñ£‚ŒðÍ îf0¯*µªr^Žc³[ÑF[e%IÝ8Ö:Á*µ•èž¸’3°š‡´3a¼yw·Jà¼l€åÒØG½u88}·|úƒÙ¿H`¨ˆáÈÞÂë_ÆŸc +M­qju¯ãkÔkG;%á§[¹ ªSæ²&Ç×Ùèܽ ×q.RïçHçAšÊ& ˆº,aoçø‡i7ޛǗ Ò?¸þxIƒ¸º@K*, „R;”ƒ%¼5er\˜ã;»x¸[õ0mÌÞ8½ó"í)G/~~øò¯ÜáSX¦­¼¸èÇÕÅà@c¯bÐöììý¿mý’p ÕwócNƒá3ÈÙÿâÏÒ?ÛÁ*b·÷äÃë_ÊÑôæ2û)ÔLa.HÌ(ûâòëÿFïaÍÍìꇲÜFõ~E惡2È6.8 Œ~\š qs->4¾×»7ˆÐ TaÑ-’T¸]õJBOŽ®Üá;1:Û)))ÌŠ¿9ymôŸ•µÞ>aíW5RngIûQŽÙÊ1e6ÔÛíÑKÆ[%qãaŽ+Ð>Üåø,PÞÒ¿_ÿ®uú=ß8KQ0¡ôˆ±•÷ã‹Õ£’ÐRš—›××8øa#ºÃ'Öà™–„Ú.ªï¢cŽµÚa‘‰Á³,´pm`÷[ý§E±Ÿ‡á?â€ÞE¤FQ¨øXTéJ'¶BˆZd#!Þø"Þüî&\<bÉTäa¤Æ¹;}möo²¤¿w b5“e)‡k\ßÉÐÛYájrp †p»EbÃl‚˜ßÉ Œ½ê]þRé\qî†Ô'ñœøÓúð¸H©?Û¯Tõ)žƒ×õϹ`£þ,I˜õƒÕ“H£³‡¨?M»ƒA-«q]ïøa­¾NmæaA*IÒ˜7–_Ž.]âŸl£)D‡p­1„ýƒÀœ[#,˜½+Bí¦pÇ›¼Ø¼ú»Öá·Jûz¯êÿÿ‰´ñÿßøÿªÝ䮵ûܵv?»Öîr×Úý@îZ»È]k÷¹kí~ w­Ý䮵ûܵv?»Öîr×Úý@îZ»È]k÷¹kí~ w­Ý䮵ûܵv?»Öîr×Úý@îZ»È]k÷¹kí~ w­Ý䮵ûܵv?»Öîr×Úý@îZ»È]k÷¹kí~ w­Ý䮵ûܵv?»Öîr×Úý@îZ»È]k÷¹kí~ w­Ý䮵ûܵv?»Öîr×Úý@îZ»È]k÷¹kí~ w­Ý䮵ûܵv?»ÖÆÿÛ"ÍÛúO¤ýçûûêÉ\™\M~Ò6~²/šèîG“Ë«ÅÅO¼ŸìŠâÅ•r0»:8=™\<Ý¢à¡R©n£Åäh+õÇgnÁ“¶ü‹ƒÕÁ >ºo!šþIiK„ÿÛ7?¹†Ê[¥ÿµŸÂ ~ÙÀC7[åÒ–»Õ”¶æñS£ŸäqœD·p‚@·Žã;U¬€c¼ïüÇ÷+ÿuþÃóÿŸ÷?>ÿÞùü?¼w9~ïó¥-¨?þúq4ÿ×ä¥ÒqÿôŸ•ÅãƒÙ"Ò¥­¶÷“ÞVþãô}ì~üËÔ ¼R¨àxÛl!¥2 ‹°þûšøÏÿþuÿþ_b«=-ã;]ÝúxŒŸìïìg¼èÔV!‘òOÚæVYÒÎ3.¦´p­ƒË­ŠÐ,r-TÖUºWûc­oÑ[ÐÖ˜0ú.ÌPN†Œ«g b½*6Š”C©MÖÈÑ’ ¤=íiUÓîA–r÷ŠÌ^žÌ¡ +*ÕhgćL¸!ÝeÕæÅ(‰›R0aœA™¯±ÎRŽŽ´Ö%fŒË|·çˆÜÍÓA3©éCÂÊ£¢ÜÞ*Iq¡f¡OUõrÎTo±Á3iÊ. Q÷P­ž ´ªÚTZ§Zï‰9yÃÖOKr;ÃúiÊQ'öø™Ú»RZçfÿ– 6ˆÞOQ¦õ*r+Ïøû¨¶_ ¤Qf\©£Js¯,Â{!B+EºiÒË2AEé@Ǫr‹ÔûU¹];vçFm\”¤^’tw+Ú~UÏRŽè/X{œÆõ4ªÑúµ§˜ÜÍ`NÑ +”Ëè}·{ ïXb|Rí'Êê£ “ÁìéåI/G¸>ÆŒIUV”þ¢%Q+O%¶Vfk„:€CåI¿"´3„»‡È0cy:Ì’n–pcÄYÓWËSá^Õü,Ã<È0;Eq¿(–iå# æ3“ˆ\$­ýiÝ) )Ì€—g¯"t®Q ÃdE‡U@ÕnŽrwrL5öËêN^(P*÷+BW†”9¥ÍQsòÄh¤Q¹ÈÖhk&Öw’BõT ÞÂG˜®qIê$q7Y5p¥+‡kÑ•)O W¸ÞÍ2^7ŠlHšSÊšab]rû DÜ/‹x\ õˆrªú5fU¥WðjéªRæC>\ƒÇbã„ña‡–©qxóëpzSä‚"pþƽUÛOQm”c£L\'ÓÎV7Òx\©£,6áOy¶žgjûm§ßËf\ŒHk¸¨,µ9möŸÕf¯Fç_%?C›Œ;S:WîüÚ{‚ÚsÔÚ`uñ‹ÎÑûœPKb†‹÷ÖðjÎQsZàj0“yÊ+1üc}.®{I:°[9v'ÏÇÅ`¥6ÌvYíæùFo¸fQì”åfòcÉ T§É¸fl·Ó¸•¬Æ%Ȳ¸YáÃí¤1=‰¨Ùª™ƒ?!R™®ùí «y +qµ¦÷‹Òv–}$ˆš‡(’z„1+‹ƒ×aŒg--OºE&¬Jˆ"ÃoúD  lðIÿ³D"“Ò‚³à­yö>K‘‰’šÅâ-Ì?Ù¯lgÈ2é`rW6z”¡¡EÚß- +ûˆ 3!ü’‘ƈ҂ÐÄìE‘ Sˆ”ÅŒm˜„W¢Cµ~®7¯Ôú%©¬ÆéËÿ8¹øòOeYo—UœÜâÖ8‰“(Èî~U7ÃFMHÞŸsΤÊGߘ~àƒY7¶J*4ÍÞ­Ö}Rëç&JÒÇ‚HsDløFŽkVõ‰KÆšHKpÆVÿ’¯-«zÕ°¾¤=—ëGýÃ/¤h]â\nKµ¡qŽê“<]¤‚)*±A–´¶ +ì^Y‚pCÄNm¡ò°Ì·P)Þ4=‰ª`j#ÇzE±!7ΛïÚÇ?Ö¯ò¢* >˜³Ñ’ŽVUs"·®kówNÿqgùÊê§ãZ:¬»hoUVUcć­Sú0.šZ”vËÊnY- qUTE{"÷ŠR™«A$ïaæ.ádùvQ"ò˜¯_””ÖNY„yfýM³vJÊgY>‰š°²LÏ‘6ÀüÌSn5÷óÂN–ÛÍ 9̬ ¥õ·²,<þ0Eme˜GY:…Ye±Í¸„¹"…èŸî&ƒ»˜ÔŽ‹È±µS¯ÊCD°ÞÆèÜ R듶•gqªR  ºÄÃÝOöÐOöª‰’P$œªØ/ Ý,äp“G¬>¬ ÕÖN‘ß*ª{á›0XXТ<*°!ÂúEÚ{˜&SˆRf"RÀªB‡Túdèì5,ñ¹•£ì¡Ö<†Ëî [/òýŠ +ÈŸþ8ˆÔ,µl\L,­ñpóÀØ°,Ô‰¸Pɼ(´v*ê~U‹+9S)\Æå2Ì>¢´+Z¿$¶Q¥_`ÂtUÓë›pþ\¬­ÔÆŠ‹¨Þ£Ý®ág˜Wh:ís.˜m—…=D)1uÆ^T¤nY€d¬'P#EX ?LÛGÍ4Ú”uHX»_äjU©„E=Êf¼k¾Ü¾âêG%¥­4O•Æ!包`†9sðtpòC¸x‡È­n&P ƈú Çî" +ï®á+£ó$.fŽâʨ4Àx:°¨Û%)W·s”ð )°^–«Ä^YÆJi=%‹Ë_Ÿ2þÁ^Eß.Ê;Eµ*¶‘xWx;O»%¶8SŒçÄØJÓiDK!ê~AØÎqʯðÍdÅØ΋»E%‰è#Ÿìc»E1Kù„>Jàö~ÅLTÌd8&aXˆ‹Ju!l’„ - -LŸª¤ÇGh D‚ÀÎÄC·+l­DRÔ~IÙË Ÿíãy"‚i„„èËà>!uôp%{S颼{e%…še±K9ÇJã\Vi<ƽÝ<·c$ªS$Ê×ÃÞë.‹Rw?“Á%Àg\íQ2Þ/ð­ß`­)¥¶²‘Èš á±z·Äx’3«ÍßØZ†°r”‡ª¸*èµÉÅ{+£sV¢‡E!GÃÁ´1!–ƒ% Ó<ù懳¸ý!'9ÎCÍåB°YÝÇîð nÄ Ų„Wº¤µæ .Ê2{E%Uu!\ãòÔH2÷AžÙA$è-ë¬qmT;0íÑ –aòÓ˜8ó0Míä„œ˜©:º‘*ƒR‹¤÷(Cþl¿òY’ÚŠ+Á3q]Lj=ÂM–U`¢"W‡¸%­9„ +¦ *hlÿQšøt¯’EáUávIxPä%\î5§¯w‹€™!hÀv%¡Ã8³ûÔê<qEIÛ<0Ú§¨Ö¯Èˆv¥!ºSÐœÎè6…9¬Ù§ôn†tà(}1àŒ^êsÒYÌì2 $¶ÅèÐì]6—¯žý¯:'o bDX#Ú_ÓþXÒžrᦪ A¿ep8E@}ú”…—Þ:®¿hoÞ'É M†¹G›cÖ™QÎ\oœDÃ'¼7ϱªtP%.Ý©¶®ìÑs6X©Íã'_üµ18XAU!›œ9(v@Tˆd@€,Àï»%©H:q©k +8ÚªÜa ±[äw \¢¬ƒlÀ”!À¦Ý½‰+ºçyˆ“ðވ˕S+CT7ßÉ‘á£<÷ ïä™DYÌ`FUh° ”>)÷}’#À¨ªÒÝÎKûˆ²[v‹RŽpU};Ï>L`YÌÞ+(ÓÜVNÍ@ê¸AØìUÔ¸*/ßHô•{à5 Ìàå¡ËÝDÅØ/Éû€B úŸA]»1Q”%?KbA2€Bpäø'ô!š?¨ÐzüuñAÞ#b«"ÁÜvJl˜(É«ÐI„ +”ó0ÏAV¤ÖÇêåîN‹-!€$߀µ ­•=xÌ¿‚ !·XµÙ_¾¨ÍŸýaZ×û¬;ÑêÞàšõç ?x{ÌZqý7½q¦Ô/•úœ +µóŠÚ@£¤¶7¼Jà:¨ñªÔõF/«ÏÛÇ_5¿$ƒ)˜ÍúâÜ:O1Q^hV”žÒ<7zOÁ¬ÑκÄØ㸺#áô¡rGiœÇÍå ÈÓL«,K "ä¡õ ‚pÍyËŠÜö„‰%!Èþ²ÐP›g̓÷RëL¨ÚÃë_ÛE´²Ð„ð'²S‘S¸jÔFqÉhÔÚÊñ¤ØÔë‡e±±Uà€›± (Dhã<êmÃLƒˆ^ @?G©f ð«T¸ý Ëm¤$nÃ[ÀÝ,äˆún)®Êg+Ë€Œñk +-ޜڭKJŸ¦07܇{ßB¸&¨ÖnƒÈßÎs@š¹êÞ"…h%&J”b¤«@‹`Á$Ö“ñs|\B‚ôC$pÎœPcšÁ­ªØâœÌažòw B¢¬ä 7/ATŽ¨ÛÍ‹€„yÂ4{˜a€|ˆ·<`©oT¤#u2Úp§,?H“Ø£$•Á, cÚín˜´T£CÆ›î¡ +"Ö0­ËysÒ‘δ¤ö +RKižµ¿–[§`¯ªÆ¸¢öY%ÖÏØð˜vÀnÃóӤђ¥\ÚYµ Ê;*ëÓŠ>½½WÕ lDš#Dj€Y­ @Zd›bpŠõTU¥­QœP/Éí’ÒÏ ŽUô7fû*M9)ÂÌ1^Žñák÷¨ o—`(ZíkÑAÒ0EÀrÀñ«JLw²¨&‹rp`u&±$¢TÅëÐÖ¡ «\{/Îbd̃ õi +ÿY¢šª‚Ghg à…`ËNQ? ‘ÖæœÒ`F’¨“B-0„:Ä “ŒÏ*˜+Tj—ù:¬(^øgàâ÷J°ˆ`·s˜YaÈGXqX€ˆ$ªÁ”&+ÚµÛÈ!`Úƒ4 oÂÎàæëÑmD°óT3ìNÿÈtµx%Ê‹–Ü.N”e0þ»>O˜iTÝÉQ´\ãí0õ^YÛ-©3<-„©%ù϶³%ÒÁ„F¬}Õ܆À(I©ŠJ(Þ?Jbæ§i|·,aR 0'.Jy’äV–Û/«ÿb'÷ î•D0¼e&LÅGI’b¯(—c´©IÞ€÷QŽý4‰ÿt‰ñ7¡Fýíø\ŸM(õD™{,=ØÏ?L”Ó•TzàidËœl÷ãÊ®( «ë=¡¶W +”Iõýª +üÃOct~a¬ ªuÓU™u‡e©ž%HDÞGuiVÿã.®¡4“¤ž¬Æec“UáCµq$EÎk½³ªÑ¯(]Üœä¹fŠòó€xƒgöøyQh`Jt8;ÜYd¹Z’ò€%Õæ¥Ö»ÂÔ.è4Ò_$kÑ·J2X6X­~B ”?k’¸µ•—öÊ*¢á©Ñ±Ý½Æô^Õ½Á%Œ·n‘wQ RêõÙ›hýuÕš‚½R‡E>H¡ ïút7|F4zdð Ð5qÔÌZÀÖ¦=tzÂÁÀjÈ€µN¨àÀ =…¾ +&aÂ+Ð Ä^(â ‚‘³RðÓ…uÖ…)r¬®ó +P ½;8 5&wHï ‡u^73{¤1 eõ©bBÂ6, +¹ú8ÙAEÇ,xžQ+ ø×X8 (ƒ;þÿ° ³â&Œ–€|·oÔqðÖƒ2G‘ÁúÊñGLXxߨmÔˆ‚K{?je@ +5Nâa$7É÷Ž™'Œ.·_tcŠÅ¬h‘ ÙEÕò’SíXÌîÜPŽÀˆ³øD2Pç9l Æ­‚Éóÿ“¹—Ë ~’ñdsÓͧ÷k}‡ÍÄa(„7àæ ¡â:*Pk­Þ1dÃÆl” ;€Á_±žhá¡j43ÝX¹ˆ„*‡­Ì„K„ è¤2Ll:Ó»ªîXék¢“Sz,¬E‚`üÜb‰ˆNJ›lvARÛs[W™T÷+ã®qŽr@u\\*ZY×cq°Á`$ ݃3'CM2Ü%uãñlwÏÁ«_5Â-d(Ô#d3£©Þ)$T£åBooÌÍ|uÔ¼Ìá b0Ð6ÜÁ‚6qƒûÆlCzï˜7ÝÊÅ€v+ˆbiµ©EÀeùáqHNL²ã)¿Ø€¨iì(¯j·ÚŒ»@G€¸@ÝÀÖº˜¼ éQyØ 2:à­…´ nòÈT (&šÃ6òÖa±(Ä%Ènƒoº63z;ïÂct° +ðpbx $=f¥B6øˆP´¸è b´Ð>h+ãž +ìø(®`bþqJiÄF8¼ó¨™µ²àÓÊÓ§FÔ¨ÞëðFMþƒ^PÏž:Cf,.¦¦©puxÂi÷°nL2:™žQ ã¤x ±Ý{ëÛqÑØJƒÃß>ßEÅQ1”~ØH€]„Øñ0,,è¦ ê$¸z›’·ø&D‚žÕz$+¡Bò%ƒϘ‡s ˜‚Ìâ Vëì7MdÜÁ¦uˆ| NÄ©h'T^ËxbÊÂfÆÝ@wÁA·Ó#vÚÁfÀ¨;˜¼Wn™©›eÔ>˜«q'côônÖmÂ!k]þ‚5ú€ÎuÛ¸õÎeƒqƧÅ윘l3]…qAyèç­}ÔBÀRÁ"VÝ|Q„ ‰CüÑØÓ­-T/ɨ$$æäòúaÇÀqACÁÛk]Ã:Ï°Þ …ðI"ÚôE7hpóã ß¡q7@(ôµÖ£è‘ˆ›-3±E+•6bv¤ + +4á„auÞ°‹¯òÙà(ºÉ§ŒYÈCã Ë˜ÙÁél¬ K±Î˜ j!›àm½Òø Ö‰€m­7d'ÐM64¼oÄ<¢C-î€Á5ø2ä!°ŽtÔËé¡I ˆÉÉb

Ý€Àx`Â;b"Ž¬ÂŒ8“7â¦ràUtNyÌÂhŒ­°Î ZéXÖ@­ Ö}BÞàïòËy€ÀÕF,fÄRVºÀ'WB•=.·l%b.: Ô¡óŠ€a°‘·<@Ø€ª Ú#fÒŽBØÉk=‚—K¡|Ö'Uùô"Ÿ±PÉ! åàrN.|}Å—˜üž˜ƒÿ2ø"V2ec`ÖQ‹ÃãarJqLZ‰´B´†%…±šð q£q_õðË­D0Ù[œ±Q—1“QÛ€»2ÈPï4£cÖºY†^±Š*•1vŠ!m©.vp(f)ÌEå¼RÝN¤DúôTkÅGà^L‡]Þ øIXlÑÚ¨áAMý‡õèÖ7b¦µ^ÅBdœLÑà‹ë}q]@¸¢Á#€ŽÀZ‡Ø qÛNe ˆâ$Sd¨­«¯÷Y=‹“Ÿ0:çeŠfÜì‹êÝòð0$P„ O؈´›+ÐJ›”«n°‹c&òŸ‡Lÿ°bŸÆê¼Oã±;tkËÒ‚„›(=d 3¹oÌÙDcå4VAëµ… +9è>»7Œ +ö <áa½2aEÎÔ¹ýcîcîvÛ˜ëŸGì`É´aÌÆB FMÄà˜–€ÙM8€¸™ÑÚD'™·³ ³ d„½Lw,~€0i9Ý7ãÊàÀ;¼PjQ±9·åw¡ôÐéB¢ï…!Ùui“Æä +lyØ<˜ü3…P™@jf ̳¨ DF{ÑÆ1.½h&“þµï•ëcv^ë™ÌŸ·0Y(–JêZ(:ˆ(,ÙûkMÖØ9„ˆ%'("ø=ªê¼-d77ÐlA!ã—ÝlÈÖNCbÙq5“?©÷FŒ~­…ñs9;~Ó:9_ â•z=漜•.¥NéÌÉ™»a‘‚lÉTc†M¤ ¢«ƒ90n‡à¯q +£@°–Î08ž£‚VŽÙ„}£N0¤r06ÀáãŽÀ¸3è“šllŠôÝLÖǬXø ÞùtÔÎ:ȸxÙlêœÅÍ#TbhÂu`Ô>ªÇfpD-€­Þ%ƒÙ0!ÊÈàèh؉§Lž°Ý8ýÃAÝ?î׎è}Œ¯¶ÿÓ!Ë°<Þ`œØ¨…5ù¢CFâ¶1¯ O™QÕŒ&mXš‹‚í,b™@Õì ݦqîµPuÑŠDXÚîS£ P€2î÷LXY:2 ­wØD@ü„F˜Z ,ƒ>ò ¦ƒo“FÂ&_dÈD‚=€UƒÕ0ӵ踕õñ2Ús“Y'“u19#s ~Àà²x¹DßÍe´NÁ„† È@¡t­ X\„ÍRͨ ›¼FD°Qª[Ì9„ÁV‘O,‘JUÚ&|°¥bñ‡'ÜÜa >b!Àb™Ð¨[¬Ú„²É¯T·‹ýc·0jcGÐ.W ¸©àÈ­…RS`›d£ +¨TsIã,Ш‹LM.^l/_Ð#!1d‚$.8±0 9Úvø£ƒõЉ;u›ÎuÈŒ»Ù<^ÅýGò2ÑÔ!Hç [°88m@Žip\ˆ²xƒú0…1CÆGÙ¼-Ú±´Önñ†4&?D3™u)ó c&œa« sG”ÔÊÆFM^‹OµQãvâ°Á}Hë€v¶ze?©’Bv¿Æ®Ão¥ÇÍà¦ü`¨†´Þ!½ ŠeÀBÝ&WЃg6=dDöYG ÈvÂJk­Œœ¶™˜°ïr~åAcÂàz»àBUŸ‡ 9na:åçS·ÔyNá#N Ê78©F‚ÿê!ÝþÃV€¨Ûrú1@DTm‰™Y&Þ7aq­[€0ñSë¤ ˆµUÅøŠ›v²™Q?l¦äÔœ‡ƒ¾f¡Ù5ƒ#c`ð Œìx⌋Šé ÄF›ën% »$5N~Ü)¯‡Á˜  Ç!36dð ƒ{´‹_B„2„¨ã„“ `Á£N6KÅ:D¸i'²¾ ,w›Æ>¤÷Z Â)° +€Ð?¬÷©`‰Ç­„‰@xG¸,,‹Oy¹’‡L"‰“ƒS1j$FŒ„ÆÂAk@WÒýdsó60·XÆôîà1ó£VAïÄʇtþa-zhÂ7¬C5&òà„ïÀ8¢1ѧˆò9@,XnÓàl:r¿ÆqX Œ1,5?j!ê1 ¡1øÀ¨ï±€‹ƒ,XÕ;¤`rÎI*ÿthì+µ0<=:b F $´³ÆÊ‚¹‚Œö•CÚa­¢ÁØ`µùcÀíˆX«4¾Oë± έÒÜʇ´¾‰Áj@Ü‹6¤l<°ŸܾûàÔ2€·[œßåQ.C‹„RÆU;“óp9›Áä’AZäâ=/Ÿ‡˜æ¢Ó#î°†Y“W´øCÃ6zÐ56Aë ;é *Á¶±·3Ù1Wà€‘:d¤ÀÉ{àM¨°· ¼=A䊕N|uܹÜÙÄŠÅ\LÖÉä¬dÆNå€ÇÆí üì(o¼~$^?êt\l x gš2z|ÈH‘u°Áš/mF ‰Th«  qÌè‹¡bë+€ÕÄ> > Xã VÃŤˆPSê·é0ˆüBà´Ã:Ôà½1½7f!§šhÝA½ŒÚ?æÙ¯qƒÝzjR…®±¡”Íèíä¨Áot‡ÀÒ¢à‡Þ°±“Œkô¾Ø7ö•Cúa-bˆ²ÕwÑ |ihÜîö+àîÀfƒÉ‡´2pVa°c¥ó”û‰zò¾Á=8xéÖ¤ÂðsÉÀÿpÐøÕCÖƒx¯G¢·NÐJƒÃz¨¤ÝX𴃼€é˜üq¨Á§gèX—Š6ÙTÏ%ä=RÙjÀßÙä”[”ŠkV&o""nð…!q R™4ø£:W1¿¹ÈqN.ãV]|\1Ô×Áj"|† Uhµã+ˆXvÑðÏx¨žiì©åuB©™Y, +\d%>¹&²˜—Ë!raÈÇBAïC*·IÀ¼?ÜB•¦4ε‚Áã|¼bÁΤuàsÈ›’óË! P¿µ¡¼7daòP¥?æ”Bóšq£/èWê( ˜ËÚ%àF žpóE4PŸŒ*@Ånt.Ñ„„ìD +¶aî¦Á9W!+ð‹Ð);™uй oÈ€©?õH¬>àGcœcÍèr2 ØÖû@â…ÃF¿ƒˆ™0ýñq·M‡…šàZµ® ’17lldü-'ž´ùã^h*¥1ú’á’„€®ÁÖ‘¶âIHâÀuapòÃ:/¨ªÞÔ –Hp±£Š—ŒQÁ’Æ‚[I„Ks‰IÀ­J¡1 0eÃ`×IŤd«1ð9¾0TÐÅ•ìLÞSÆ"C:߈xÕ³oÌ­±±úÁÙ³Š›Í2±>Ÿì É>¬a\„±0Z$ ©Ö¨ù•F¤¶Ìfú†ØdϪ»Ѧ\XŠ¶ð…_°f§â64 ¤z^¹,ábÁi¤0¥F„ë^¹@ÇaåÓ¸Rã³Óh¨ +ðƒÏbÓ3Jy5P˜§=—Xr²yVcˆÀ?%¨)¬àJ5ZYõËF\µ³yO ‰Fz¸Ú÷)m;W½ÃB51;c£ã\f^ȯz5 •òÊU7U(`ẙR-¤jÀÂv&OÔ +ëré9:1 XõrE×€²’z_ÄJe€©4NqÜ-B<´’ñ1—`òGõH8|0)¾ïéŠN:z­G$›óràs ƒ‰¼|‹OÒj<¡›Ëy¥¢‰LŒ!Ê8o˜¤â=BmÛ˜Ôa ^xÛ„S(8ÅmщI·Ttñ@¼q3ã+ƒ5$£¯Ta8ØŒWÁ%‘ Á%Z¼² Ò„WF ×øÄ^›…•wÐ+‘œð„µð¹ž¼ÄF$}B‰ ·!ÄÐù¡õrOÒ"*ØXKÊOq©¯¥&·Ød½U=bÙ+Õdœ‚¤ÖÅxÛÁeôXįÔ?ÃfÓQ3û eÐOGË,äaã¨\ð%ÈqÐÑDd + µà/n.qØB ·NÈ÷ +Ôò3h¤É¤¦ÄÜ,­ã‘2"gIµi¬…[›D¬…‡«t¸œëÁ# ;‚ÂBU!6¨ú#5°)-¦ºÛ€43™4òéiÀ•\˜—ŠKÞP ´}RÙIEìdÄŒ«h Ê¥zlr’ŒuÌtr0HË+=RhS{Lv‰Ë-Šùe:9M†ª¹þI.»0†=bIÈ. ¹e:5G%¦\BÎFÅŒ~ÚÁ„†>­”ÕÆF°¸¨”–“õ˜ +U@ƒ5˜,˜IH*¡uËGçàM˜ +ÞØÆFÄÜl•*Bj4‘@€Ê%'9oÆþ`ÕªBãñIX¨PyEÊϳéžCÊ;£_õ‰ðüïÁO3…gò©©ÌäÑ`eÙk‹ù©ÓY bÝPeYÊõ]>).ÏÄškRnÚŒ©:=8Ìe ,SᚌŽ9 ¡ðöD¤ãWšÀðét¼;¸<„K>Í„ê`2´ÚM¶vâ­O  +TÝ ]cç ˆä¤b´Ú +7×Ôöv~öDgçjdð´ +,»øℨè Tùødº¹£T׉Ø$ˆŽÎ >S´ûÃ! îÈ#”¤ìŒRY’r3t´j@dP +4Ô¶· NQË3‰Ÿò»Ûq' ñ-K¨M,Ö%}>;m¬Ç;[T¬J©e)ßçsS\nJ*AÛrqŽ,…k¾PÙF'ë –¨D—JLFꛀ.(ki=¢o98eèšKÎÓ†ÊËÉîÑöæ]j{5åŒy6ÞÞÉΟÉ-œ‰4×ÙdWÎt+óG«‹§Ò½]ŸEe\mû#u2Ñe],XR ó*i#d´EDÛX¸%3ýc±ÎŽ˜™Nw6¤lÏàUM2ñ®R^Ž´¶#ío°j&â|bÒÆÆ56ÜÅÆÅÌ›êFꥥ ­Ý¡ÆV´ºÜߺ“MôŒþ•˜cRslŠ¸ž›½],¬Ø™Ìˆ…Ît“*©Ë‹|nN.­äfÏĺGÛÜT¾½î[>¸€(â ³X¨Íç–C½dï4è«•·ƒ@çr@Ju'Ó?Óß»åô“½õ‹ýíKb~šN´£ÍõHkC(̪͵êÒÅöÎ ±8ï–r.>哳|ºÇ¥§S“G³SÇåâüÔÞÝÙ™ãn¥ä–ó\²#禢­-¾¼.×·Éì,•êï6*<áfàµ@,‰©Óñ™³±éÓñ©ÓÁòjuþt°8Í$ê¡Ú*Ÿ[à²ób~1ÖXÏÏŠ67@tzÀÎ& ‰(µ +.æçcí#©ÞI$Î!ÇÆë6*„HIJ­+ÕÕÄä^¼»—럨-œC¤¼¡v€o™D_Ê-<’céþi¥ºí ÔÆ]"p ¨¼œŸWêk|n–Iõ¸Ì4ð o»%< 8ÉfgÃõMXáÜìi2Òp’a/w !00^¡,eæK g33ÇR½£¹ùs±Öæìî p4hÃDïHsëJÿÈ]ÓGïê¬Þ.eºL¬©6Ö‘@ìß`»<5]^¼”›>ïìÔ–.@ã›ü!4TssÉÎ^yéöÒÒÙüô±¥Ó÷–æŽÃ";¸¤?TÔªëéÙSÑön¨´œ›9må’6L +f:x°0î ý\²³ÓÚ½»µsmñä½Wžþ—œb’ó•ÕëÑæžW®û‚-:5ãSZv6§CC1gô@ +åÜd°4®oÅÚ{@¼N!Ÿ¬¯>òÒ;baQãdÅd·:NÊ­òùµh÷¤\ÞeŸÛ¹î’Š:L œÕbjòDº¦¸p!Ó?ºzâÞKO±v¤²Z_½£½u¹µyGiátsëZ÷èÍæâùëO|;ÞÙöJY¥0ê‰æÎTW.6w®g÷ÖoŸ=u/.Wçuw¯Å{ÇÙüBqéB÷؃ɹ‹Bº®Lщšƒ±—_Ö7µµTÿhzú„T˜*ÌnG;ljRÈNû”²˜› +fåü\mùj¬¹çSx´¬”ç‰h…RÀÃ@›0e¥¶™íŸÎMŸ’s`®ËÄ[RaNmmFêëéîniæÈÒÉ{z[—-ÉH +Wœ=ÕX¹P˜9™îW[{t¼ßš¿½µv‡ŠDÙéS©É#ÑÖvcíòäƵWŸ)Íž´²>7/WöÐ \~M®ËÍ^¢Ã „©§Ö–„üt¸¾"f•òBvrIL‚ŠAà£ÕÅLo7ÖXãâ TÊ‚çÏñé~´¶knÑf´¶šœÜ T–¼JKÈÏæ¦wãÍU2RŽw·Äì¤Z[(/œ‚Å–f³ýó÷½¨ÖWaËç;»7 +K“GÂÕR›kï-x(ÑX±“²¨ç{0—ó­Í‹åÕK­»!¬IÙ%È°£6B„TÚ¨®ÝU^ºitE›Áâ\º¹–Ÿ\7cA#†0EÐäÁä,*•U2Þe"åbo'ZY²Ó&3lìˆåò†Ê‹ZTÖ{Ÿôòqj‘›Ú„±efΦ¦Ï€\²s°8âd²³k­‡kËÀ*Ù½Ç^|ÖÇFGCù©æÚÝðh¬]à뫥ɭ›~í®ç¿‡È¹úʹÎî=•µ+пkÍÍëbnþع¯>ñšTìkÜ<¥BG¬$'Ô×.θ¯0wª»têágßHõw…©£ÉîNªlæÈ=»w>³xîy¥²Ýž9Ú[=ODjt| ÅbnAmî”—.Mxxñü3…ùs Û{+§ðpª(-pé>Ÿ™Rk+3Çœ>ýh¤¶·oû:Ö$Õ¬˜ƒ/Ù¹b¤²Yœ?ãæÁ +f„ qøŠËL Ù~yúø¥û^l,F‚ÅÂâåÌ̹xg¯±y:…Éo€3ôË¥pa:ÝÅÄÂ…µ±:¹}yúèêÊ<Þók•Þñs÷|P +ÑòLeþtyá<žfÆëÑƦ\˜ çù“_vC-R“±æ6EwãÎÝËOWæÏ,n]\ؽƒŠVàåÉî`¾±vióÂ#x1ÙX›_?{çƒ_Ø€—p‹y à®ã3ù…+……KìôâÖí{goDr-/c’“LvQ,®…«›ííûƒ•u“³ÓáÖ6àŸwÀöÈ•uX·xcíÌ=Ï-¿f!#ÀŸˆTNôNªÝ£|v¼:í윾ÿòý/ÊÉ®«íp{Wí¯­_;d2çAÍÃ<È':ÂB¶w´´|)9}{°¶íãó™SB²nÆ9"œ‹5×óý£å™#¥é­æÒIpûㆋW3Ýc™ÉñÖ.¨—é溛;çš9~ÃÍ%Ó“; áÆFqéb¬wL,. éþ‘³÷ºë)>Ýuqlb*PZrÓJ}¹´x6ÑÙšZ=ûò·~Z]8Å©µé­Ë«çŸl¹Ù?rÏòÙGZÛ7ÙxÿØ™{OÞù4¨ø øÐôäÑD{'Ó;RY¹˜›;Ã$»Û§ï™Þ<†Š‰öVjêX¨º¦T×rS'§=X^½(,ugŽ•‡-880x„Úõ…;HBÜL¢¹/O:ë!Å%Û$ÜTNtÃå•òâíà<Á^–.‚Púƒy.ÝE ªj‰$®Nªõ3qPÑHqVLOVN4VÏ…›¾p ˹æö™/ð©F¢¹ÜÚ¸£0wV*¯0ñ&›hy%$TM´vB…'eb!Ó“ k²3r¶Ÿh¬óÉnoñdõ,©§4×/ÖW.Ô–oo.Ÿ®Lï„ÒÝ<÷æ;̹fÄ"^1+õHmSmŸˆ¶ŽJ…%Ÿ;w屧^üNº6çs3gÓ3çrs*«wÕ6îaÓðÇrwõb²½¥óÊ$ 1Ù&ß®.Þ~á‘×A»ƒù~¶³aÁ¾P\ ªÔÀW¤;»ËÇoÎí\¥ÃÕbo3ÑZE‚·PÀ•¶œ_L´$[»N&®$;áü¬GHCE@C¹Ü•œÁCÕPfÊN†t +¦£Õ¹Úâ©Ùc÷Ä{»ÞPyÌ-Œ»X7§B#ˆ©ŽZÛŒuåçN¶7/ÏŸyP*-à¡bvuë }ó§éîÜH÷¥»;…©cRnÊJGM˜Âg¦=bÆ+¦•ê2À4·¾r{eñ›n{øH0×QëËÉÞ^~îlzú™E„l$×Z€,™:˜&7•“ÝÂÜÉÌô1¹ +rôS +‘ʼ›"Tnm*Þv )#¦8IuçÔ½b~öÑïfRd¤>Ÿ¨º„?Z˜kÌŽPr3Ó{÷V–.‡K«brJLOãáÌ4®,\кhTÌ(¥…@~®:wº³y• 7`¹•üµû_$:6BõËL¬ÅÄÀ÷nT.tׯZ|di¦»y'mX‰€—½\Ü>Ø +Œzè”—JØÐÀÔú%Ppã6Þ]2Q"HÊ…žë’q¨Héô£¹Y)3Ð2·„5·â!›>r&£UX´å£WÓÝ >7‰F*n9ãâ\¬’쬳 P1Z­É´VùdÃFGÆÜ® àÃëÛá⢕²Én¢s´¹r¾<"ÙZáRM¨O¯­½›Š™14Ô¤“³bfŽS‡Íx,k¼±C©E Bs‘|vjLHûJcõBq樜ŒäºGÎÞX?{¯ÆÅzÄëÁ ©`͉GPD6Žðq˜ ŠÐ©>ž˜¢S³ñΉtï´GÊ뼬˜¨€}"By t+&‚e2P&䢋Œë=z«ÏC†ð@^LMRѸ +aÔô#¤T"ÆËj{S./¢Á„&‘âÍBï(*eÆmÄ„ÌFEHMEAe:{J~ap ŒBøtª±>jÅñP>›ÊM…±!rÑŒ̈aÁ”ÂÄ =K ê+—*sgã-°n!€,3!¬“á×ÚÛ×Ë BÅó¨r²ª?\ÄÂy2R=µ´/¯qJRd ?bÁF ˆÙIÚÞÏ&MnAc&t†TwOÞˆ¥[czŸŸOC÷J Õ;_ljÂÁÂz&++„\ÆCe½‡¶ú¡gJ¡_˜Û©,í&{‹ÉÉ…òÜÚô‘ÛçO^llždó €ÅPaZ7›Á¤¬ÖÍØ0 å¢\´È´ƒ….—ªò™Z¢1sâêcý½ ñöœOÉø£¥Ø)ÎmZó&žÉTgCÙ¶ÖŽaR•ÒF¿œ„Œ?{F,¯¸¥ ÍÆ[sR±#š~5gå#¾pJ.V*«›ñé’òJI.5é…EóKÃz‡ áÀZ€Ø•fOQÑ ÂJ¤’sM4gây!['ÔB¨Øi-m÷vNŠ¥¶ƒŠé¾œ›ór9£WšpÖ¹-ˆ¯.GJ`r +2öLmm …"ÑõÈÐe—;T,k$3Ä¢ÅlÿˆO*êìˆÖåF%’O’bÚCE-¨lðIv*ÊAÖ.-BRáð°êêÉ«3G.J…ÖˆÓc².(?ﲇ ÞQ£Ok§\´êåRZ}늃éðÉéHe5˜›g¢ÕÕ“×fwÏ%; ^99î¡lLˆËvðxÍDÊþ@"Ù^‡°Œ‡«i;©"Rµ4s¾µrEÊô´€ L&‚¹Hy¢­ÖÙX“7 ¯3ýÍÌÔf0ßV+3±ÚZ¢±¥',äW†LûÇœ˜” ¥&-nöÆ2¢óšÝ2iæÛÛ©ú&¨NØhÉï¦#d¤¼ßà9`p›Q,Ôˆ7Ž©åMR®˜<²hq‘\¶Þ›Û8¾²wîØ¥{®>üÌ}Ï~ý™×¾õÖúá'¿ûÓ_ÿþéýîO>¼pó™ÊüQJ­.Qwð—ʧŠµ©r{¡6½ÑYØXÜ9yäüÕëÎG+‘t+VìrÑd¬To-lÌîžém¯Í¯t×wûÛ§VOÝùÀS/þìý>úõï¾ñ½Ÿ¾þpkaO-ö­ˆdõhØÇ$âÅÙ|{ÓÇ%tNš e› »åÙÍtgIm̦ºK+'¯Ÿ½ûñ«?ûÒ7¿ùágÏßÿ/~^È´,˜b÷‡Ìn,iÙ£5çÛñêd¾Õ¯L¯õ×NoŸ»çÞ'_zàé¯}ç_¼÷ño_ûþOO\}°¿z:×\&C%°‹6Єˆx K)%;2y%„M©¥…Âô^¬6«Ö¦v/Üxä__{àù»ôÀãWyáâÍ'ÎÝýèµÇ^xííwŸxñµ;ï}tçÒ}d¤dE% ÂcbBLv”ü<mÆK3Ád=VèWú+Çïè-®ìž½óØ÷Ü~ý¯¿ñÝß|ù×ßþá¯_üá/|úùCÏ~#[Ÿ?lÂFÍ´—Í)¹¥TmÇà’t6Òé—X¥„Ë%'sa¿˜Ä¥t²Ô[?zñä•v.Þ¼xï÷?ýÒÖékÍås¥™ãL¸zíßþö»}ùÇ¿þ_ÿ÷ÿóùç_þà翺þø×j G”|e‚Ñd±ÒšZÜÚƒÇúÑS®ß÷Øó_éo=ýo>þò7_|ó»?~ïÃ_}ôÉ_|ñ¿ÿÏøë/žøÚk'ï| ]Ÿç•:)–ÂÙ¹t{ÇMÇ=X ¤êù£gî¸ûÁ›O<ó_^ºû©_ùÎôÞÇ?ùåGüË_þø×ÿúå§_|úÛßã[﬿ Ì!nSÓt¸*Άr“õþêüƱíS¯?ðèOÿë“_ÿæ×ß|ûÇïòÞ'¿ýùG¿ùÕgŸÿáOþ¯ÿõßЭ?úÅg/¾öÃdcYïáÇm$0§Á#Øñ0*åÄx#S›^Ù;}õ¡'®=öÔ¿|ãÿê“w?þõ·~ôî«ßçÃ_þño÷ó>þýþô?ÿó?Ÿþæó^{ëä7Õ)\Š™=˜ÁE:ñ Ä’bk©·°»sâŽË7¿ùøs¯¼õƒŸþê“üìý7ßùÙ¯ÿ§¿üç}ü›Ï?úôÓÿøÏÿüÙ‡ŸÞõÈ3õ¹ÝLg[Lõ,ƒû³‘f‚“Œœ +§šÅÎJcz½9³ºqêⵇŸü×WÞ|÷ƒO>ùüËoþûOñѯÿëÿ÷—ùÛ×ÞøîÍÇŸ¹x×ÃÁô$¥4I&<Ù_^Ù8vâÔ…KW®\¿çÆÃ=òÖwÞúÝï~÷åŸþôѧŸ½÷Ë÷^~ý•‹W¯-nî¦j=¥0í¡àUFˆ(‘x¥ÚÜÞ;yõî‡ïyøÉ{}êÙ}ùG?ÿ‡?{ÿõïüàû?üÉ—ú˜Î×_ÿöËo¼uöÚC3›ç*Ó[B´¬f;Ùælª:™(6g·W·ö¶vw®]¹òíoçÛßûþ÷ø·Ÿ~öÅŸÿåx÷ƒO?ûõoþú·ÿøì‹/Þúѯ?üðÌæñpa’SËl$/„¥úäÌòöѳ—÷NŸß;~úê{^{ó­_~ðáŸþö÷?~ë‡?~ÿ£~óùçŸ|öégŸ}üáǽòæwÏ_{°Ô_CÙ8Ê&ÉN´¼„‰YJLDãÅj½»¹³óÐý ãøÑÏ_úæ[?}ïW_þé/ùÛßùñ‡_~ùÅ/>øàåW_}ñÕ×7Ï\•Ò-„OY ñ¦’ïEòîÜúôâúÞ©s=ýÜ×^~õÅ×Þü·o~û§ïýò¯ÿ_¿ùòÏï~ðñüÃ?|ù姟ñÊ[o?øøsÕþ¶“R .Îè`­ˆì""@¥Îj¾>½yääƒO=÷Ü+¯¿ðÆw~òþ‡_þù/_þå?~òþ~úé—üãç¿ÿâÝ_¾ÿñ'½ûÞ/~ú™sWïMT&%cFHƒ“âÕPªË´֎ܸÿ‰7Þú÷Ÿ¼÷ÁÛ?ùÙgŸþåŸþüño~÷ËO~óç¿üÞçŸþø'ïþìÝ÷ßäéç\ºwîøuF­š ›/d÷ +L4;p?lM¯?wùƃ¿ôÆw^~ó­o|ó[?ùù/þö÷¿ÿú‹?üä½_ýâ½_~ðÑG/¼òêÙ;®-ožT²½|w/ÎÖ[Ó•æd¾Tœ‡µ1µ4¿¾½vùúÅ{¾çÎë—Î^¼=_+J‘€Ÿáœ~ÖK;a°ã&'á'd5š-­öTwjzc÷ÈúÞîÜò܉³Gnܼã¾n\¸zíÂ]÷½x¥½¸L¸h%”Ÿãb ;Â9}BŠ~’ç„ÐÞñ;WvO§ó…F¥¸³½vîÂÙ÷ÝýÐ~ÿûß{ïƒ_¿ÿÑÇŸ|öñs/¿tòÒ…îâb ‘•?DW"ংl ±{ìÒñ ÷”{‹ÅVozzfmuéôécÏ?ÿäÛ?|ûÃ?ýì·¿yûGoóÍ×_}ååçžyôÁûïÚÙ;Vï-AÑ|¼‹Ù e̹×;)›‡Åp>—ÙÞؼÿÆ]¯¿úêëo½õúë_ÿùÏßùãÿâ¿ýëOïí¬7'{¹FÇK '† 1xxè°›û¹0ÁJÑd¾Ú쯬l^¼ýÌ£?òü Ï~û[¯ÿòƒ÷?ÿâó?ýùïýâ‡Ï>ûä…ËçÊÍšç=dÌ°ÁÉÔXGõȘõŠ¯Å⥩¹¥…µõ#gn¿ýŽËw^½~ý®ßxùÅï¿ýƒW_íë/½øÂóÿòÚ«ßxàæ=ÇÏ[(±ø9;&!L”Ó©Ê\0^öáB,™ßØ>~åƃϽðõÇŸü—+×îyòÉg~ôãŸ}ïß{à敧ºï©G9{þÜÆÖf{r6U™®ÍŸ fú·N‡¦A—í¨dv—É×·¶Ž\½vý¯¿^é©gŸ}ôñG_zñÅw~òÞó_û·«Wï^ÝØ)ÖÛ$¯@œ±úƒFc´ùãös>R”ã•D¹ßè¯L-ïú¥ §\$oõóf„…:ú8ÕäfŽõfÔƒ}tÄå—ü”‚“ +F‚ FÓ•P¼àBp‡ÏïD ‚à¢B«e3¡Œ;³‡EØ„›‰jÝÄÁft`p"¬—8µÞ›”ÂR0ìÇ0–eÒÙt¡ZîÍÎm<7¿¹Íæ¼´¨wâã¿ÞMYЀ‘õ.ÖèaY%HÕ )0!JR#±T(¨±t.“ÌrõvgaM‰'©t»;Y)—ƒRÈëç]DaTBÎó3þ@þ«›É+Êjƒ–”p¦^ióÅdLÇÔÕõ­N§V*e:ýÉx®ƒ$å&å=DTo#´tÂä3¸4¯ÎÉ;pxϼ®k ©l½«\¾z×ÑcÇËÅìd»]­Õg¦ú³ýv®PwúÈ¡1D­Ò؆Çð£sp%>Î&¤H9/&³åD¾ÆÊQ—×ωáxºŠärÅf··Ôë/ +’”LçƒJÊOL.ö°•ÜŠÁæ¢ 1VÇ%PÁ…ò¼”Šf;ÐÔ\¨Tî™Ú¼ÚY:'(—ÇVÂét^Fœ5»q@«6ʳçÄÔ¬ÖFÿã¾ ‡?ìc“VD€*SL(M¥Ò…D*“ÎÕ$)Db$C ²R¨%Ñg&-ƒ‹‚JnŠµ÷ [hL£P¨üb–VʹúÂÚ©{Íxâ-(PJŒÂ¥||Ú†…&¬¸‡ ÛñÐÁqË1ëa£wÜŒyð!d‰®ÒRÒƒ{’¸(­wЪ“JÚq•” +^J³úŒ^Úâ—9‡G+l¼!¨›²û(pPRò Q6ZrÒa½—Cø̨ûªÆ<¤wƒ×u‘Á5lãF/gp3v¿¬³ãfit“>:*Dª“q)G†Š*,E²t(~Hkw:Ð.f½L VåÜ*•4†JvT7ùÆõ.³1˜\³Ë…0AµÂH1”’1&fA‚Z'3 £Càƒd„IÚ}²Î‚™Ü¼Þ-é=+3úB.JEùD¦6³¾w>ž©ê 62H'Ç(Eï0áИP„Ni ¾>¨Ñ{}t ¸Ëàä¬þ Ãxù¡Ü´‹ŒŒ™)ÑJÔ×eˆç±ºŸTïð°Ýé=œÒØà­9oñVzdÜ=jðùÈX©¹óIt5‰+ 3¦š|Š ú䲞c};¡"l BJaút¤²€ Ž1TjÍžuêQ"d1©ÈKµ7ãû‡-f>½xÚÇ¥56r.6å—KBr:^ßeÂm­…ÖÁ¢á1£oTçv¢2&æèÁé¦=!5K)CˆƒPh%ãW2‡->­‹¿uÐ&—šT +óåÙ“±ú2ÌQÑ:¡TÅô”žAC5+•4a±3©1ãÐbƒküµ¨ŸÍ \ÆEÅÙp‹w‡ >µãA¯˜á³3>¥ªóÉv"ϵæ6o§b- 4ò0ª‡ÜbPmò +»qäÂäÊ Ê;ñÀ›T1¹BGÚl¬¿ÒR&”lð†&œ6ÂEòë†J«bzÁÇe“å9ݱøì=L aL¸î—Š"Š3±bsÍ/ç'ŒÆä‡úÚ‰„[üÑ`~ÎF(&œ,ˆrªÑI™Ý44õÝÉëâRipl_ÖÙäm¸:lòéܼK(Jé™D}…P*†[·±qø_„OcRNï\tC‡ê¸X¸íáàˆf*&gépÛÇŒ.yÂLAš ä,Œ‘Á+Ñ€Þ+Øðˆ‡Ë»Ùœ›Nu—o/ô6]l “ „\Áø.ä1¡`âðªËáƒNäu.apƒ&­íöwÊöNY°¨Ù'QJ Ø@çxèmKÉn(Ù¢ .Òôq)“Gr’q­ €mÑzÆ è0ü4ãÀÆr¢/4ùB”RÖ¹Xƒ›3{%€å¾Û¨Þï$T©þÓí¨Î… +¸q»0æGmü~ï€u[ݹÜݺcÈNNxE¤ÃåW”æ‘x÷¨TZ¤"µNgçâ#N1­E-"Û錇+P‘ž˜[סaà‡_5 (¸\$ä":¸ÇËà–×L´ŽðQ0ã ÝR‹Ö™d ‘s˜Rå³sáÚ6ëxù@×ÁÆ9'¨u>^÷ðq;µa +ʧ4ƒû·8êÜ@_ Ö Î\¤†‰ƒÃ× )@Ñ9nÃÍDÌÉýJ‡Žv8µÅÅ;g;àÒÇ­˜S¼\Ú'ä]Jª³²{™ä‘q‹ßC%üpr5©`p³§nŸ¼¡–f†´N­ÅïðÉn2JGt¸aGƒ0H.ÞB„ÔÁ3bökøUDÌ ©ž‹‰ã¡bó›ênJsØêéa“(—‘“ÝHaÞâ L|(ÅùZï­‡ï Öç¢ÒÔT$7C‡ËœZ·`¡Q^ÞŒ|bŽ4³“ÇÛ›Wðt_ 6½—ëu‘± ; k®µ30*?7û†µnp,ju=R]wñ97¤Ç¢D¼›êŸª-_`Ô¦ aÁ:¬9(š<œáõ¶Á½¡­iÑaKç MÞ(ˆ  ›=â„•™pp@}3Wøpù+æÑ™„ÅØ”Ÿ/ݺ¾;~P‹@!LnöÀ¨Yg'ìƒÅˆ’±.X”xiVÌ´G`¡(Õ%弡²\^öG^)'&:K;׶¯<íR.Á'»J~)’_Š•W¹ÌÜ&øôäì)6ÒØ?朰Ó.bp}e =ÓZ¹œ›<¦æ»[§ïPùÃåH}%ÑÝMNIOHÏœaÒ3ˆ”e»÷?öRkõ,´¤ ØpÅAÅ ¥.­3ê¤É+Ù‰ÈÄྲ1T,úIJG(ûu)5+eg̨!ë¤T¯ñKD¸j§T›Ó:¨QN ŽKy"P3ýÂÒEW 4b#¬~ʼnG\t>‚Mþ&p12­±à_Õü¿$½×s#é™îùOìIå‹„÷@zƒ´@f $¼÷Ž zW4Å"‹,ï]W›ê.µwòf4Ó£‘FšÑJ:s4;gw6B»çÜìnÄÞî‹ÞD2¿|ßçù=i¾Ïuv&0á`¨†ŠUFks‰…Öh 2ìc +”|¬aG“ €uõ¦ÅÇÓ¼qóÁkFÉã’íòlÐ*`Ç“6~Ù4{9RÎC×C Ø­´knÓé>J\­ú„“]¸<‹NÛ†r´Òfµ¾ Ñ'\€N@yãRqÊŠM;È‹ÓþÑCgˆ†ðe)³¨—×'D•Ñ\¬htÊÎŒžMÆu:ÚTòCØ 6œå£éI‡Ê…›@u©¸‡Œaá‚—OÍ"2­ùåœÖÅâ2;ºýoµ´p³´tW­mrV/,„ ó.á£57©ÌÅ‹åÓ…ö©–ÏOy=D ÐϤCj5ÕÚ‰“•¥Ý[¯­„ÇJé¹ýüð´°tÃèÄZ{¨RA¤t£·õã¿þçí;ïXPÉKj´Ra¢íhiÇhj“×[™ö¾“JŒÛh(OÆú\fM«]Ë-Ü—KZ÷Šœl»@íÒ¤—õ…’L´Î&Zh¸$¤çóÍÍ›?n­ß7 ûTåâ +Ÿ_¥Œy>³Îm™XÑ|ÒAê:NF*N$jñH&OtXÏ/°jj{ÊÉ !¬€ŠµB‰n¦³¯7v&9í øhÍ”ÿììô…IÅ# |ò«ƒ’{鸚é‹zñܤeÊÚQÙ=rRÉæ +…ó«ÙÁ 1;/‡¤ÑaÓs¡ì|¤¼Æ$ç€.Ú­íÇ/¾¬¯ß¸`òÙ‚‚{tß ÇQHöy£?åf¬:J™òh…DĹpq¨7¶£µÍ xÏÏ j‰ˆ–i£-æŒÆNipÔ^»½zü6*§„ ';ÑÂ*5š±9:ã rnA©®›ð% ®ä× +ûÅå;‘Ê&®8C+©Sj5Yßd­`¸€F +¤ +øÚæÒ]B«ðÙ®Rš+Ìí(å¡VJý½Æö“Ìâm<Ö%õ©wgáFÿƳ/âåá„“&£]_pµXq;Ñ8ÖJ°Ù .Ñ2{Xpœi7pîJ{ýÁîÝÓ½k°y‹[·ÛË'“’Šäövjþ8½pZßx2wð¾Ñ>áBø«x³°_Ȥ²) EÏxX€½KÀ3˜fÇb¶Ñ)Ÿ'}¥uU¬ì¸…‚›Ö¸DŠÖP¹ˆÉE*Zå’ÝDsGHõ<„Úb÷ Ó'Ñ ¾’¦ŽÙp°¤j[Ðêcü¡¸¼”[Š×P¥Ž„«¡äB¦¯ïC’=?í÷0i¨g°Z­ûX@å4&WØX$jÉ6šõ+ îò{jnÆÍþÙÙ©) 2>„l;ëb,™—1±@È$”žuƒG‹3N Xmy]¯oÅê[í¥·žGJ÷°HÑè\UÊ›¬Ñ¯ÌŸÖ‡7íýi$~ÌIº™ïÅš‡ý+/•Ü|87ˆ7Ö½|ü²ñ²º”ë^¹úæwö~\Y½ED+‰Lcíði(Õ¹äb€ú´êZ¬ºAêszm»¾tÒËVDI´®É¹5D,³±äËh¶:)ÓÜÊ÷ö§\ô™ ÷¬_mB«ŽÖ‚H¶|~×JÊr¶WY½Ñܾ[\:JÏ]”×ñh“~ï‡Þu3긗óëÉùÛ‰þiȘŸrqÀŠµîöòþóoŽûìh”6ÑîµÜúƒâÆc¹~ÅÊ$Åxýõ—¿²ÿp€ +U¤Ü03sùäýââ]K0rì\JB \° ã.ÆTput»¤3ˆ 6/ƒ°:"¤!´ +Ù…äÜq~ùNçê;ý£oƒAýœ TGbr`ò +£).!ž *¡4KË ­C¶DàU:œwb‘Y/<6‹êîÐèÑQ/°4=ücF¤îÆTb…÷’†É ^@v~Z™tR¦  ÝË%p¥¤WÒÝ+ð7¤ÂTÿXïF*[Åù i£ép´ŸTX­è$ "›ÊŒGžrò,üÌ‚ÉÈ9Õt’a;.Í"RP.¤Ú{µõûµíÇ„”­ ö3Ý+.NG#E¥²&æ—p­™líæOåü"P +©Tc• %7ó X¬í¤S$ˆ’œ>;ãD ¯lD«;Z}W.®ø4©Ö&¨;HÑ›Bª[èílž¾Ü¿÷íÞÎ}TÉW:·ž}+-e­«7?ºùê«ÚÞ;LzÙç'\ .äÕ­éÑ}#˜›ÒÁ"ÍÁ0Ä!½` ÊçMA“‡óÒГNxù,“ì‚Å[ !bosóú ÞhûyÃE…ýá‚/\‚ÒJµ¶Öo¾Ž•—¦¼Œ“ŠåFÏ4š ™wáŠQX¨Ì‚-zé¦TP¥Œª5.³Äå†ÑÆzmíÖÕç_–—oœ·`+bvÈs©ê²…žõr•Žþâ²mÆ'°©a¤¼Ç¥†|jqÊ9ŽC„”¸éØÒµ‡Ï>_°·É/ÌŒ–‡ˆ’jh„NÏ[q•ŒV™dÇ/çýb1D|W&AòI4·A~aRp±¯ÑtU£ ÇD€Þ|}K//ž™r\²"D¡¢Ítï0?¼‘ho…õòÑ×ÝÍÛV2­¬õvßhn=ËÌ6ÖÔÖ2Éþe;)&{D¸à&"#–fS¹ÎÆF뀊Õ΃Û ?k6îú@<•ªO(ÐúèéHT)`‡³ó¸œ£#Ùâ`¯¼|Z\:Iv¶ú[÷š«·øD[Îôóƒ£Âà°¼xË-Ú˜ôtDRS}£²9f#/™1R­óéE΄Œ¥µÇí´5 !\J H)­UÖî<À.ð±ÊúÁ½gþ„‹7M>ÁKëlbé­¼{ðô;¥Õ»Pº³A^) „Ü‚×L~ñ¼™˜tsÚ'rQ ëèä@ÉEÅÆ„Ó¨XW«n­œ¼u÷õÏöžý R¿2é“ ¥ì ¥ìDlÆ/x™TÍ3Z‹ÖZPEg§½ðµR~ •k³0„ ÑRG!ƒ3:ÞДÒX>ª.DKKrq ‹õ‚‘:HŸY,­¸ô[“ž³“^H¬²Ñ͸>í·áS¯S+¬Ñæ“M5Û.öö!Œ“á\kýNkûAvþ*Ÿn#RÊÃNj4;™ 7tÌêa!¹‡Œe½q=;w»±þÜ3šüä%+¥z`š3>΂H.&Í!³ˆk ˆ•ùýRo7ÈÆüD8U_Û{¶~ûý­;¯OßüQnpÌj“Ó·¼õ-¹è˜‹Ë`ѶVÙ)-Üîl<ñ… +ßšòŸ árBv‘OA®LBwOÛi€mÈbº¡Æ¸dO +·‚Bb#£”’õúÎC$R±!j¦¹_œ»•¨luÖnwG÷Wgý‚«Ü{õ>3ÉBÙñ89ʃ‹h¸}¨ï§cÐ3^ÐQ-­Åj›Õå“»¯{O±hSJö÷n¼`6T’s±ò¬^Ý!µÞ˜žvQo80eó&ÄŽEõúNnñØ ½”Í÷g}¡oJkåæGóǯkÛÏsK·áʙɀŸ4¶o~ çΙünΊÃQÈEÍkFëø‚)x~Üfv“ÁÑùÿ”—Ÿö‰Pá¹&dVNág쨗'l£é&ÜL2(—©x»»ýpûá§6Ø#9ç&¢P¨l¼É¥z\fA*mŠ•+~©`Lm“R.ÞñõØPP2ó™îÞ%;=n!épRëӞЙïÙ¯ÉÇ“JYJÏ{h݉ +J¦ð)‹_°éŠŽ‚—×!ìض4¸†I{@žq²ÿÏO{gœÊ³ft;$t±(e˜D‡OÏáZˆ5Jó×HÐs1]êïæWá¯T´’1+®ÐÑf8·ªÕöf¼„).Ñä7äÀÆ6£wM>õÌTpÆÉTÚ›ÝáÁhš8+fÅTJoG›»é¹Põó3Áp¢e”æÆ .âÅ…d'’0Zò…Ù9:»bG5J)“Z•²áÜB¬¹¯Õ÷ŠÃ{zcOÈÌÙÈ0¤ïÒÜp,hÝh€fö@B¡&íT,°ý7¿^—al4Y½Œ†r>2ÒF4Ñè¯úżâòÉæ½O7|^\Ê/ÃÉÑÂÂÊáÜÚÞ´ ¢9 |ˆ>2ª&[>R±xI!ZàÔ"?Ð#“›õ«—l$“ÉÃN;É‹3ÞÑTÛ^Þ7:CÛ¡”Ñå3'¥’álqn»½v8ؽ·píùðêÓµëo¶6îê}¯ÊÕWF'Nå´ h­Š÷ðH+”Xð1Yf7ìv!Šg´tZÆK%íщ„M.†’ +4D6!ÒŠnJ…ß +ˆ y*Þ“íBg{÷ÆÛ¬^‡ùòhÅ™†–_â}\*Íz5ÕÎ56fœôÙ ÷Œ›wà:mAG‘ +†l4+ƒý=å]X‰U¶JÛÕÕ[ëoF+[Ðû•þÎâîÃ7çD"\…QšuÑ.T"Ä4tS¦µF)¨4;´fâ-µ±“_{0¿ÿ––_XÚ¼ñ׿ú·HfaÊÆ"m)·‘jí^}øÙîƒÏcþÜ4â§âŒR…^køZ" ¹´>/¦†\¬p>å áÀ§f¼Ü$„)·0á’,>ELö)­dò2¢g l”„ÅÏyBPð„РX¢ã:֌䒭íHeI­®½C©¼Á¤út¼^î÷ßâ“Ýi¯`¨.2IˆEH¬DC´D0ý 5ádf<œ Ñœp€Ð”d­2¿#§;ùĽGñæz¢µ©máñ¾Ï)‰ÆÊöÝÁös/kŒ9pK€Cø,­50¹Àj£{­Y¥ª,˜Øè¤:ÅÂMB÷,ÏøDØÁ73í¤ ÇÁy'ìäh&|"†ÈY*VaãõBgëõ?ÿÎßþ—æú *ZVËËJyµ¼x’íì6×æVN½$ê>Þ Åš¼ÑG„²É#_˜FÎMŽ®ÐAi™=Ü„•:?å;7áš° Ó6ÈõqÉèÖæú›w'½Œ WÝlÒÁÄéD3^ßI4öx£ Áª>ØË67íhÄâçý ÞL^Ì“¥…Leéò´o| 'Y7¡MÚ(Ðy7®R^NuíÉKÆPH +ZU) ¹Ñ=uRmPr¾6·×\¼<@ÇQ>é§u›÷`a@_Q³]Œ‹Íx¼xÄÏ&(­Jõ„ì<|ŸR|ëðæKÀuBmãdóHNÎéùE%; —ÌD(RÌT–qÏM¹MÉMå˜ØboëíÎΛŒÞ¹l!¥(Ç«6_è/.Y¾1ísò–` áK‘Ü¢OºÍÎŒÙáuÙŠb‘¢V߀ÐGëmµv%RÚÀ"}ôdtÏÊD¸L' ¸;¼±tøöÞýÏŽž~õú{x$O†óˆ˜s“qè£q;qaÊ3>°úÅI'ƒˆY@âIwhʲûyp%/¡õ|ipåÊ­w'vF¡Áë;™ÞI´¶=ºÏ63¿>šÐ@Êùa Ë WQsk¥Á ›˜›rñ>"ŠËeRi "(mtʯÀkÆ'^Â0‚xZ|¡ ;jG„H¦;ع;Ü”én¶n®]Æ%k¸œ‰äçÅto~ç^eñº7”tâË-nÝJ”æ! ¹qÕŽÅœxÜK¥p1ïÁTWP7&FÓñùa¸Æfýã– .ª 饕hqH+ù¥+KsW½|‚4ZéùÅå{°Sb²;·y7×چ̎ +9­¸Í/³êׂàa.ÏúÿòœÙêA˜½äÙ ëåYttÞW3g©,Ÿ>|ÿôÅ—R~Á/d±òt´ɯeæî”ןIÅRÎ{W“=13±Å=šíV±ã 1±àg3ßœp}kÜuÁJ”ïHóhET‰ +‰úfP0\¸¬ç·Žž¼üðGÛ·ÞŒµ¶¾¾ Zî?ŸÛ{Â&ªnF•2óµÅãlk+œêåIϺð @ż‘§Ý,àâ¬G²!š›ÐlŽû¹q礱¸V­ˆÆ ×|^I6¶nDó=DJDJ ­G ×Þ„H˜š¿É$ç,pª0Ü»óÚÃÅƘÕσ!z¨„—L˜!/ªñ‘RoxmxåÑÙqç™K(Œ`(-f‡t¬‡L-[_[¼òò²DLÊ…4*¤ý¤&%¨˜ž´¡DhĨ³zÖÍØýÒ”s"² _š²»PRo*-]_šÛ¨r[­næzWA©í#‚õËf°ghÖŹˆ8kcböòlZÀ…í~Á5ºF™šÙ‰OYÑ1S Å 1õÍ ³ß8?ý—ç¦ÏYÇ­¨ óJ6©ÐZ‹fû³œSò6,Œ+%ÆèÊ…eµ¶]Þ)Ì]]XÜýë_üëÚÑ3ÈŒ†ôÚšRZ†”*d–ƒá‚ •I>¹°qÏEÇ/NÙÇM³ 3V·A$ÄX•Qràû>Ö •ª˜äç®T‡×²ÍÕÛß½þô1ÓA#¥XõJ´¼•ïŸÌí½Ý\T_¾-¤æôì`óÚ:–µà¼‹IxFó‰•ycÀè};›ñ Èà¤>á OúÂçÌø˜ƒ‚œ¥ÖåÜ +)m<©-ì³ñªœMç’›¿ÙÚ|ºvüîþ³ïç抽›/劘êòñ.ü£¥Ø ˜X‚^›tâ64ì"õPtNH,BÀ÷R Œ9¢RRiÒNž›ñdzè,˜²ÜN| á]\Í£RÆÊŤØD··ù 9¼ž«¯eK!A)™H²e­±!Q³Ov5ŒI›ÕþÁ¹ #È{ˆ°yÄ')Lk jÍÏe ‡íáè$„,9Õ&«Öðâ&"å6™«óµ'6mÇA9Hë`”Ž 4mÅM\1jñ\×ê¡I>QX=}%—6‚áš›I/nÝýÑÏ›ÊÏ›\,Ð.äXµÅF{”Ö¶õ 8­R[¾¡dº³NÊiÚ‚Y\´‘}XDRóH(æBUR,Œ[°?‡êšô™ÜœSí^Ñ‹E‘P*š»óâÓYëç \)2É®\^Ñ›;F}eaõðå‡?­Ì@.Î]+/ÝTË«Rv ×ÚæBpª^{üöO©xã/.Lœ7CHœ°&w„SÖòîÍ—¡Dä Õ*D´Î÷ôÚRi¸Ÿîmå´ZZLu¯²ñNÒhvˆ+eÁh3ZCIÏç;Û¬žÕŠmˆ9V4ê$£nFŸ ÈS“J¬Þu1IŸðŠgf3~ÉA\r —épaeçAm~Ÿ7‘Òrfrg°ûlûÆ«Ææm¥¼Ô]½þáO~Ã¥çtœÐjt´`)¦—¾^•{Ò ^¡wu´fÊ×ÓÛ¾žÔì £|‘ŒÔÇô¤“„]}-“ÝÀ.,œr‰&ɵ毦 òËùê`¿±tŽV¬XØÏ%¡*BZ¦É#B’u 1uz?… 7fÆAí¹hiÊEŽ9Ö˜+¯ÞKt„ôœ‹M:I:Bɶ}¡DPÈØ€>G²ÃÑ,į̂ÌdÄâaÌnÞq¡‘; ¡ûúÉ5?ã´2++ù¹öÎÃ…ƒÃ7V¯½¹vü²9·2Q®a|Ú‹+Žàhͦ —ó‚F…R‘T??d4f„' ºÅIûqäËî¡'-&•ì*¤ž «]²àç'6¯höHA.Ï'ú´ZU’m­Aäñ‡bÀùZu¹µq{åøÅÊñ[õ‡>’Û\¿KÆ[¹€ÈE¿Ãä)ƒÔšÃ}+.ó‚Éì \hÔê“ÍÞÕr·—Ú+× ‘Œ•Š*Õ >Ó×*Ëùå“XÿÕYµ°òb¸÷À‰«^*ŽË%¿Ò^¹5ؼ-®0—k ï}üãâüõ??otàVTvR†š[,öÅô¢S–÷ŸJ™þ™߸“rP O( I 2‘Væš«KÛ7»›·´JëåHaNδ™x%ÛÝÞ<}k÷Ñç¨V×7Î>®¸¨¸܇JØI*-QÝ0j6L>oòcpSó_/´š¶£±Ë6öòhsùܬÛä£ýl\Ê Õú•d÷0Õ½æ¦ #?ÿâõ·o¿ƒR-¥²$—†bfºí&5*sZ½<8%ÂÕñ¯¹Ýêå!&œ›òû™ ukþàÖÓðÑɱhÈèщž+”cýDsoþðm9Ûë­Ÿ<þ¸<h *vè8"1Z£ŠI¢ÜèzŸ3ȃñYƒÂ,$8ÉBšüa”²+ÙöA,ßo-^]8|¾y÷ý»ß^¾þ¦˜Xq•ç3í+åÅëŸäôZ@ȃû™ÄŒ‹399‹G°¸XΔW$½ùó汿Ã/Ø<ŒÝÃÙƒªÉ+ãRqiÿ)¸ÿÿtÎ6f!“C‰¾œº¨ä·Æ¼– R’M6Ý;{ÏŠ+÷¨Ôo*å•t4%2ðÞ×÷­Çª;Ðøn.1áÍßkõ +΀4ã ÎŽ–¯òL:7¥¹˜˜Ì…Qç2M¿œˆ endstream endobj 62 0 obj <>stream +V—›Û“ÝýtûJqþ(×ÛãâàgÏè– +¥T\pbt"]-T»›×î¾#&;gÇ60Ñ:¸ƒˆ»è”ŸOéå%4œ»0ëƒN· ª‡1 òCõ‚ög+s«»§µá®›TŒö•ÞÞ«'¯—ßl­ß¬­œDŠ ±L÷½ïüÝ­÷~jÆ£^ÆÈÏ÷w^dÚ'™öu1½`ÃH£±l‡QKÓþœ‰·lA$:VÖ«ËÐ!£‘ìîVÖïv÷žvö_ÔwžÅûÇ\vAÏ÷O_­½ÄÔ®Ö’£»Jv™Ô[-ïn§.š³£+̸ 7yC"êáF3ÏèµÕhm)ÛY[>z¾|üâÊÝwçv¦zû¸Þòriâës°RµøF÷›QZ³8¸^[¾«l`JyÖ/¸ÁwpeÚC›!oÊ%*Ö³±ÊoNùx“ òI53^2 ŸìÄ«+™Ö:üSLµ1!‘él—®æö£åÕDs+ÑØä­X¦½}üx°sÃMG!¾%[[£©lR}"Ú­4ê |XøÊñ³tmÕäÁÂ\dÊÃäá…mT–®?|ÝX<€X„É©´ª5¯TWî>úüÎ{ÓÝ{+ +­µ+'ÏÉpj,Y¿RYyÜÜ|™éß•2 @@ï$´2ÁpÙ K(8´‚â3—†Hî!!ÅÓ¸œVŠÃÊÒ­Öæ} 8¨F+Ö†‡€°A1­­wwžìÞûxùäÝhcSÈ´ý¼FÊÆÜæ.^òr«cJ†‚sX(™©.)éŽ Wí°-0ZœÅIÄ„®†™Ö&øȹ w0”„€ÃÆ\¼–éluwèÍm/›Xß<}úêK9Q †å…[…ù[©æ¾œY1ù… á'´“;¯ÊÝÝË£EfÕ€PàSózýJ~áfºwD©[ß½v÷ ˆðN< +I¤µýôê“ïl=øR©ï:Éx€Ö>úÎ߬ìÞ5ä‹&Ôˆéeµ¼ƒ‡k—̨ W©ÎÞ¸rbª—ŽC@£c ¥¸Î/1Jþäñû÷Þú®’™s“Qft*~˜jî ÷=ý¢¶r‡P˜Täô®% žŸöüå…Ùsãn Aø*T§Ž}kÌé ŠñüpÖÅO˜1ðM-9P ±è§b„œåRÝLï ?8ÁÔ¦ W+ ÇÕ•­›{§/¼ûýÒ¾\\¸rçÓ72¼þmð‹Ç¯Þ»òœOö²•µõ£·"ù¹i?oÃ4V­QR +r®ÙÃúEOµ@þ‘¼5À&0zÝèîeŽ»;š[wÄl{a÷nuíf¼»§w÷¥â*•èŒîjNwÁ¹¤ÒÐÒý‘¼[*A‚ÑÚ×ÜJÓL>©D&:Je=.ñ‘è"JƒŠ6±H“‹v `[Â…4"~!f4wúûoöÞ¬®ÝÏÏ'Z o -×]:~)/i••îÁËxïZ(;2ZX·o{éȬ/¯m‹éy2\’’]·ú”7"©.Ò·Æ<—Lˆ SJ>ÝÚ’}5»95ÝõRʬ·Hò'O?9}òñÜΣ²"$ö@ÈOFÀ§F+ÐYñY‹Š™lk;Û؈fÜô¹I÷Œ›õ1À¶`(ƒ ”æëC1^¸löŠñV÷ÊÃ¥Ówš·£Õ D®ŽNÈég¯?ï­^7A`Y\.¹ÈĬ²§ì@¤ Ë·Ö…Q¶²Y|2ÊQwBF?”àJMK·¼ùñ­§Sr!Èe›k‹‹·æV´°éÀˤg\¬á+sGÐ5—ͨrŠÚ"ä"2<¤~vÂkqQÍþö·¿ø‡I3z~Â3ã ùØ$oÌiùÕXv@òÑöò>«£é¹ÚTr1@‡sÝ­Òð¸²t\œ?ÐkËRªUè]Y¸öl~ÿne¸â©•—Ùd‘ JªÝX<äãÑ­,‘Ïã¤aÚA¸P þ¦ä¬ÉMAZA„d87èl?ܾÿÙâµ—õÕ“ÅõÃøåïo>ÿˆM~µ’œÄêWRÝÃÁþ‹½Ÿô®<. î¼ý½PvÁJÅÃåõôàznñÞòO–o~Ô¼ò¤Ò?øÞWx÷{¿tÐ S@™ö‡MHÄ-äCéa¬ºUììÜxøÎÒµ'åÕ›­{åÕS½µQ_¿Ñݼ»sûÝëo|nXèí?ûâΫ®ßxU]½£–WHmto'£÷­˜6ãe=tÂE ŠL´%¦z„R&c-¦ÓjÝEF¦=°ÿú`ÅšPÚŽ…!|µVNùdC/v‹íU£¾.f—1¥‰J\È{ eìëU¤¡$ܸÿ‹Å/ps(Ÿõ‘:¼‰ò)p%LÊ@} F¨lÜIØ1Š91Ödž9½¨äúõå“ÞöÁhØÈÑ3_ßq‡Û¢¯”—2s^J· ¦ñÖ@ÄâfÝŒíd4ÛÛëíܯ,(¥Õ€T0yy)ZÕ2ýkÐJ)ÊàBIJtp1kCåàèÑrÚÁ¸å¤ÿüe÷¹1ç´ö“ZRI9 Ìæ¢Ýêa§ä·ÆçgüãNzÆ2»h:’ †ô V×bµmFÒ“€CÝ­Êð(Ý? çCѲ–éJ5YŒ +«ÄëÛÉÎUÈzVD†æc­P´6fÁ¦lì¤`„KY}<ÔØج1ØAK°zU­¬æûÝõ›KÛ'Çßîož”毮ÝxoþêÛ;w>Z¹öF®³-'ëJ¦[h­íÝz)åZyµ¹qoaÿ @²½) ¸T=YÜ~úáµÇﻘD8¿Â§´ÊÖòÉûÓ÷Ó­íÞâÞ¿úõÉ‹Â…Aª½¸ÝØ8=~þéû?ùç/ÿþ¿¾óƒ_Üzç‹ïÿÝÓ~¢ÖÖÕêºTX5ZW;[Ïvî}9¸ú®6ØH¡ƒ_\²¢Š „”—Ï…µêê#¥´äÚ`›UË‚Ñ Cé.À$£·Ë '›·> ¢yɨÈé6©·J 7kkÃ¥Wò2CÖèÛ‚"¯åý¬Þê@c_/Îrs¸\Ì´÷hµb'T4Rnm>.,^ç3s¡d—Ö«.r4 ‡Q(‡ð1JÉÑÑ +«É&¥–À(À¯½‡éæ:#kº Íâ ¹ÁM¢M7¦À¡ç ³,œrÃdûˆ‰÷ˆp‘‘2ŒœAø”˜hZýÒ„Æ„Œ”ìPjR*`¤×.L Jƒ¬á£u^26ã¤A„GwúDŒK¿N9¨ËlÜÉZ±¨/”­˜#gÃÙn¦»ãe“‰L-^PjNÍ÷åÜ"¤¤pf>QYÓ«+d8R !YA„„˜ž“‹« sH¹Í cùÅÈþ>ÞGª.L¶øøË&ßùI„£#?­ƒ‰Z¹<¸ºÉ÷ôenn›Ò2J¶Õݼ£WRµ…xyžT +—â-)ÑV2=TLõÕ\o'¯âJÅÇÓ|¢¬¤ê©ÚPÉ÷¬¨œjm+¥¥pn¡µz³2ØKdëíîðá‹×ÝCɨvVŸ|póíÏî½÷ýÛ¯¾¼ñÖ'[w^ÎmÞ}öjïΫÎÊýû5·Ÿ¶w^,ì¿è¬ßIµ¯…T,ݾÿî:[w/Z° '…G +!£§·ŠÃÛD$Šdž½úRŠ×m¨êdHy%Õ;\¸úöêÍ;Ïcö¯?yþú¥…À”g“=Dm Z§:¼³qûÓüâ ?}øn±¿;í!V›ü›Aåjip]L´ZóWû; P³ý=:Þð„RT´J¶3sûx´jCx1Qž¤Õ" E¤œ˜î6ÖïÝxïoªË§Ðæáì<Àá¬k4q„Í/¢¸az{”Zf"¹Xa»“›¿%æWÑpÅKC¦n‚8ó‰9;¢BdPrsÑòŽÖ¡BÌazÒO„ËþPjÆÍAâ8¼dÆǬĔ“™‚à687å=Ça%ÎÏbv*.dáÒŠTXÒ}ÎèZMÒ²¨˜0yi_(JªÅXe=ÑÚ×›{L¢ÒòñBGL7˜ÉÌe'©îµÜÜuR©8a/<ÌŒWôš–lX½Ìe“çÒŒgÒF@Æ…"Êå(1_êïÅ$£"Ùnº¿+ä´^1Éö09iñ‘ ¤Úˆ×¯Ä›£9¯˜˜ˆœîájdÖMÆ 9/¥:…^‡®·øù)ó†#È›}Œ‰!Œ&EÉt5]ê³JZŽåËåúÒvin9Q©ºTèé•|½³ua¨=<¸úäãΕ‡0’’^%ÃGÊ^„IfkÓí ȪáÔ ^XQ²ó\¢ëeã® )„ÕÍ«7l‚P[‘âF®´pøæÊõ·ú»ÕâN=zöÞ/ÿßVŽž¡jUoíƒ[µ½W×_þdíøBoCI•ÞúèkG/ÌÁð¸á3ËñεÊòÝ+÷¾Œ×ÖõL÷Ñ›_?ÿbÚ/Í¢Š 3Úœß{±ÿäËÝç?ˆT6ŠíͧýM¶³cGÂv\†€œŸ;Y>yïú?l®?”³K×î}¬mœ›ö_œöO»8“!ärª¶ÑÛzRŠ§wßüì§ÿDEJ¨Ö +ë|vE.n.Ÿ~¸ýð{ÙÞ‰ŒßzúÉÍŸj…ŒùeeG"6CG»—,Ì„•Ü9xxzï"ã¢åÌ~ÖzfÂo'€y6N*9>Y¯¯^_:|ÒÛºÉtõB_É#,LñÑ"%¤¤B†¢8§›½¡1+æøú†hÕGj ¡pŽäXH·è¥ÃÅ „t\JÁ p+ži×{›ÅÎ&]ͬ¯ÜÊÍDKs¡X!¤¦Ã±´¢§ÁÍqÒAÓr>¤Up)ãg¢.Tt8VÊhù&Ñ3•hy]¯oÉé~så´¿û ÓßCBz®ÔÙ<|h”—P*^Y‰–—äLÕË eF÷à*;š×+9n%lÁH€ÏG²ËÑÒ†ïRZT’…úB}a> µvÕÆnnx'¿p#^]åÇÄ BˆK[7†{€»%€j~t–S+³NjÜ4ZyôâÓ§¯¾°!ò¹éà„KœÅ ÒÐ{zu3+|ðŸ=zû3“:?áõÿ‰öqeû9TˆÍCÓBBÏutDSJ~دºx˜ël!bŠ““ókǹ֚ »‰(kàrÒn$dv¢3V¯Û‡…Ùåó»8)gRsGÂhïy\©¼Þ[Ø]¿úX+-†Œ†ZZVJ›>¾dŠ8«…Âi»—qø¸ ÇØ-"é9ÈeX0.#ª!R¨4×{ë·TÔËê>Vw"a³›m3¯'+‹³n‚ð„Ÿ²Q>*†A¾³¸”„€lÈh9%+Ùn¶»«•7¤ôBeþºVXòRZµ»k®ÙäA7¡z˜”MúÙrHkÃ&YœYMG'¤l¥°ˆÓ>ëÂc@ÂâˆL*¨˜gjª‹ñI*;"᣻§€y(œUƒ´d4ŒÓ¨2aÿ:àЊÉ4;I‹›"C£u)­à³VȨ£•'&b|œÓËT8ËkE%^ +…“6ÅÊi£Øs#Œ3@úiȺ«°JvÆÁêåxØèV. xÇ‹ˆ¼§ø„WÜäª.ÞD…4*ŽžˆÄ¸-g¥X¥‹‹ŸñÕéØ…IÏåi¿ÙŽbdˆá”x²‘«®@…ˆÝJz¨ ®„¬‰£\‰ùz?bTÎÏFÏ…e—*K7Û;Ow)@ªób€}$[åuF+(ÙŽ’éRJTÒ¢–ÁXÍq.û çg4“ à’7ÈPšÃNмŒqšRèZÑÃèn""«Ùµ­ë{§Ï½­ª—V‹ýS­¸d^„sxHØ)"ù)ÕƒŠ^LFXsl¬MIE?‰¨ù½½;w^~îf4*xiÍF|½¼³‹˜±aÀ{7ëDd¡Ù°Å/8P™’²°ñ¨2J¦±šl¬GK+J~(§æ©HÙKjŠÑŒ¤ÚPW~&®åã•]Dî@iÙýò”Ùgs#¼gää™1ûùI·ÉB¸<.UýlÆ…Å ‹E ‹d¸ â¥“ ºN<êa cF¡LšüN/ãìlCÆÌ›‚‡Ð±ž¶øÃÑJ";ï'4/"{qa Ø$gþT³M“ e"Ý”` TÄå'n”ö-Ôm·ÛR$d |ÒM(³n@wÚ†Ûýü¹1Ç”ñaŠÍòÀñ"_P´¹/*Õ­èô³8Ÿ 2F‰cœ=89ëŸvÎÚP”T'f}æ + ’15YUe!’1ƒyÁrnÂ}qÊ;9z 79 6‡ ä)[ð̸ƒ +ùx3 S˜ãã “‹´ùèqKàÌe“Ãú¨°(!Vh,&JsR¢ê§»Ÿœqø­^ãÁ â*bäúùÁÁe;>añM˜\“³.giVŽÄs¤ ©é©¦­AŽ•“µör¦Øƒ?|0L¹NبB÷б aó…\A‹ä¥XÅOEÌ.Üæ!~ÆMÚkj¼\¬/fêó8ÇóÐ˶€ÿ£“  ÏŽ»'ͨíÚ…I›É'º¨$©Ô…D/œHFÃò²šé­‡ô2 ¸ÞŒææ3Hzà&£/ƒóqBLŒî¬C EÀE/"øqÑæ#§Ìn³ÝÄO0ä „ À\ùX“‰¡ˆÆ!&º 9¤ÒVòÛ|¼ëfðó6ks“ã3.‡Ÿ…F€\/ê%RJMÛ±söY[p°t°}í ÂÄf˜ÝMyü<'¥|˜äEE«›Ÿñø ™â0 Ï~”G ž %„–ÝAÔlµûƒ(A‹P-t8OH¹Ñuj' gó0gÎÏÎZñY61é¾tÙ>6á6Ù ye2”"Yà#[¹ríÎ'LvÂáá@UœÑp:æCäiKPMv¢™y/AÝ&ÍÈ7Ï[§í´Àöœ½4‹Ò +-¥¬nÚâ"§mØåi/g“!Q—/Dñ).Úºhò~ë²å//šÎM8 ;Ähžà5jì^¿6*3nÊ|´†‰Y"R òÙŽHbÿꃷ¾óLþ³3&l.?’’’–Q“NŠîßîÞ¶£ü¸Ùoñ²f7cö0(«Q¢Æ…c±d)U]a#%ŒKÙQ«R‚ÀhŠ…Hƃq®áö‘ÑRMŒ‡ÇmbÕdÚMræiö¡®ÉE +|´çDTk@ºlöÁ.{F7ðgqÑð@hÅ5 ¡ÂwÂP8ƒŒ+ÍMu!¢3ÀƒAÀå•œ¬—g\øÅ·ÉF#¡¤•LN¶$ž.F +öÍê +šèy$ÝI57¾>ÓÂáB²ØÝŽæ  SpXA¦‚´† )^«!L†=‘i'ó]—ŸIg9!e„ c¤äô¡§OOç÷Nî ~bÒ5 ùú’õò¸ †â`÷PTvŸäÃb./átù§Lö±i»ÝíW¥K•ŠY<¿9€q™°€…9ÇMÞ©Ù •^þò¤óÌù™3gg¬N– —h1ëö…hV& +z²,EKPÒÞ õ¬5eœ½h¾8f›6ylN‚R~Lžµø'f<&f÷„: S6¯piÒyqÌÂEr(½<åœ2-.é‘h §¢6Whbqy¹QwLy¦l¨;âÉ›A’å)–âE*Ÿ×û\Ü8IdóµN4ÇÅ°3j,‘¯õ´\ëì„clÆãôZ¥£ÑTvBæ*¥ìâƆš-Îú°)êD$ŒUY%ã©|*•3VW<}£Ò]7{Æf]c3v’9ž×4E”$¯? (r*ñ”Í1ërÛ)*háB1q°·¶{¸ÓÝZÌ6²ŠTÕGË Ã£Õ~­8tº «‘˜"ªrDO’œ.je¨^À“Å?‘)Õ Ýy>š6 +Œú Þ v/uiÚýKV[P†2ã³A:«#F¹9ß_^ +(ÈŒñ“Š…Þ)“ád×sE1^âc%*œ1»Iw0„Pa”9Q£ø˜Z£ä%$8>̳t:U£1N”‘0E¢…2 ÒjåWæhT‘ö"¼ÇÇ0 !Uw@túx*cTÔäœ.§Ûes¹²,× É…zf{X»y8ý`øòÍG[;+z,Š2aƒ÷‘tlj6ðg¦ÎÛ­N4Äjõý4M²ZDU"I +û„ „˜¨ƒà(Ãr%†x#-;ì¥)û™óSç.ZƧ=£9Žü3êܘïÌe÷…Iהּ¹h›#èò"ñL1€…#GR/0?E°IFLÏ:óã“ AQÁçÅ]Ez´0ZɉƒNj¸Ô(7Ré¼Ú¬'¯muïžnnlõ«ÕD¥”Ô“)IƒÌ.œ»433ëðzƒ2ÇGZ œI[éçË5#å%M +ÇuVÅr¹¬.õŒöŸ?=~xgóÑý+…j~ÜäŸõxƒ È“(J‘QLÐn%¶2ȵ«j*ŠµKÊöbáÅÃ=xýôã»øÇO~óÏ?þä³çk«•¸Á…$Ò£Ås‰8ËÅÒ©x½–èurV>¦©d‚áE«Ëër{"²T­•ZƒÖòÆú“Wå;sN 5»‘ ÀèUˆOôQ±pyÆ{þü%CŒx¼\.esIÐãõ†‘µjéèôF£?I$VvÖùœ£ì’UR„œTÒmŒS$bBðJ©B¡iÙ¸”Õˆ•êÕÝa¯™«e•­ùÂã»;o>¾úñ«[o?;º{s~¾i$A?âñb7‰Q:h2IKF!¢˜Ú•h³]è‡+™Gõ×׿|ï軯Žþý·?ûݯòÞ³k{;KFEeôøAîœ3h.„ÒÄ‘\4\ÍDóºÐ¯%–çòƒvac¹wçÆÞõk+no¿z~òòéýã«ÇZ#‘Lëõz&¦/;mÅ.Nzf¬Ä…K– —ÌS3N— ñ8="K¥u¥Y/FQæ¨R!S,æñ”‘ÈPöÜe34¬Ý…½nð*”­GâO:w¯v.|öÞÍ|òà“·÷>{6ü·Ÿ?ÿÏß}ù·?|ùþÛ7¶×çS©Fq›Óa%}¶”ˆô3D?ë[®×W7öjG;UxÝ:ì=ºÞûþò«GKŸ½Xÿ·_¼ÿß~ÿÝŸ~zãþ~)#l»Éæ'˜0ÉHHáPOSÇö»âñPyqTùü­/½~8øüåÚ¿üìÙþþ»ÿòÓ‡¿ûé½ÿüÍ·ÿïÿþû¿ÿÑ‹W6¯ïu2™Tå}+ˆTsÒr[ß_LŠäb•mæØ¥nje.»6ÈÜØmôæáÿ·¿ýŸþùg/Ÿ?Ü»²³=k³Ú=¸“.Í8Ï\w[ÍQ>`HH:‚F9{”wµ‹‘…–­±·h¼÷üàÝ—§/]ýÎwñÕo½ù°T1æ—{k×æ÷^ðñšÏe„иˆÕãÔÁ@vÜ}tµqu ¼¼Ñùç¯^ýæ«×?ÝüâùÊ¿þâõÿù¿üÕŸþøƒ_}ïÖŸ~ûúW?ºw¸šUB~¯Ëc±úP£§Î¹Šìƒ+…¿þôô7óö?½ýá“…¾³þß»õï¿zýO?¼÷«/¯ÿéwßþ_<þôIûÑamg©’6´L&Ö2‘X!ââR°™ÄW«âÍü»÷–ß8n¾¸^ÿÙç·ÿËÿôÿÇùîï¾zñ?þýóÿ÷ÿúÿí^œt~üí«ÿòÕ[¼8HêŠÅ›ñÙ¼ Šx=® ÇŽû¬é +³óÅÐézád³²·8^M?¿1ÿWß}ã³·>{ïÞ«7‰¤傘đd^ õ³ôv“¿¿®ö`ð×Þø«OýÃgÿë?}üÿüé×üç/`þýï^þéßûÁ‡w·‡å¤.‡8IÓÓ,M…¤ªSKEâhN¸³ª¿uXüé{W~ýãg_}~ëGß>øåwoýo¿|ïü×ïýøü?ö¯_=üÝϽuw­˜ s<buúýÀ­U†9îÍÝÄ7KŸÜmüÕ»ÿå'wþû?ùß|û¾sã?»÷¿ÿúíÿøÅ‹_}q퟾¼úÛþ$½÷³dçyßùG¸l‹$€™¹3ssçÜ'ôÉ9÷ sŽ÷öÍan˜t''` ƒI¤D‰A"-ªV’%K–hK^oí®lYöVyØòïû4TÕ5Ìt8ç¼Ïóý~¾'¼ïüÚݧçÍf5>JÑ8BìM;‡ãâ~Ï|z˜û­wþío>øégÇ?þìôoÿô;¿üÅ×ÿÍþ·ïÝùÿþÇÿëÿùóø¿ö·¿x÷üͯýþ÷ŸOs8º¾²Œ!Bë²ù`»¼ÕÐ-g § ™Å•3,·èXߥ†ù›Ûͳiõñaûý‹ÞgOgßzóà³w&ÎÝqööÄÛjC¯Ñ #ósj‘‹²QUÔ\­T®8zÙö»…»[{›¹ÏŸŒò£ÿð“Çÿðï?ÿÓ>ÿ÷û½Ù§7 ï8ž5ö›ªNÆI,GÍMQÖóGˆ4J“é¸I'&yÐVïŒÅW§ùO.¿ùÞößýñ§ÿß?þ›øåwÿêŸüΧïïuF"@oד8ŸJÄñT\cÉ‚)6\aTV÷»ÎÅ^ýáaãÝÛý~tö£Ïïëíýç½ È‹#¡µ@<ŒˆNy£Ö?µKSQ´$šnìÛ‡[î K̽™ýÑýÉo~öÖ‹;[ÚðÙÓB¿j¹¦hêÁ9ÑL6Fº˜Xfô­U½òÀ4]Ç4–.ÛzÙVê®´×Ï_ì¶ß=8êÏêÓfiR+ JžkJ š ¦X_B$„Jj‘H2‰dR)况êÑ´uo§ôÖÒ÷ßÛýÅ÷~çãôƒôÏÿ[·öéÉï¼øųñ£]×Óh*ÎzÓp.‡2N ΢SÍ»¥¼Ç'ú.v>²lyÏ +?ûìæûëßþ/õ›þÓW?ÿÞÓOžï=>ën +†¥‹fEÏq!‡ vÑêÔ»9M(›lÓÊ:WÏÊ“š7*Y›%ñþvåÍ››w·›=‡¯Yª«ª+à8ˆóËAfa[ôAf*Ð’L%yó,³RÈ× …fÞ-è¼'³E]¬»ŠÉ :8²ÒlN ¡§3RØ #çêSèY˜Á(™LQ"¯VŠÍ­ÙéÖh»åh»ÍìÓ£öQK}°S¿µQ9l™Gmk»žÝª»uSŠ¬.ü‰8¦SJ+Ad–ã_»´¶¼NÇ’Ëæ5©åª%›”Ľ¦úh'ÿùóíï¼uðÞéÍiÛåXg´l]¯TgÏ ¡ñ€‡ ^4ÕEUV³Q-Ú%O-¹²¥qš&ʺ‘á•8£¬Ä2‹4–ÑY£ŠIÞJœŽ  ++yêüZ^½Xic©„-q ˆb助;<+q¬K`±$K³á»!—Cd’-dÇvû¡”E«FòNª^@0ÚD–íÁ`29êõ¶í¬Ñ zPÏ6")òÒ²-ÁÄÙbFïsVOÎvÖZ\,¯†2$¼Í3Œ‚iØÇjUœ­aéâ }c’»»Û¸w0ÞëWG•ì ìÕ<'O_ZôùógÛ¯…°×–"‹a“àT;Ç¥ê„gYC"j¶PR‰¢ŒŒª>»ûÑËG÷NövzWWšDp’s ÚÓÎzZò•éãÊèa¡²Ã ÙÙìèÞ£÷0\ðû’­YV¹XlWký¼W8=¥Rˆº²Ž.,%—ƒt”ð‚¨HëIÊeÕr$Œ¤hýu‚²8«íµÏ½ÄªÆHS/ÏÌÊör +&%Lȱf“÷†1:@(a³fŽŒ/)-‡9Ljä{·äÂŒsFI¾´“‚Q^ó‚V€ÈOèmµ~`vOŠãÛjm/Îýˆ‚qncr‡’«ðÓjqfÕÝö¹Þ8YJoøÉK><„Íú×o¬]óa¬36{·ÞM¹¸Lë—‘”ì‹I_]ˆ-øð”Pr[fý\Îoq{=­_ PëQš¼dF¿²Çwm9á2iÚ–­¯WÖãþPÍ(9¿žH+k ɇh&"ݯ¼îSꇈV#µÒÎç­» ¹ˆ›Í´\aìïöÔ´¹ýÀOªA\ÉhU©ÉvJ(fŒvZi-#æB€ÆÄ|–BxScLŽ6{„Ö"u88\iRfWÊo®%å(å%¸jZjŹ2®ÍŸ²Y×üÑ_²\OñALEVÚè Z[ÈO»— + ÖƒÖ^A ?aÇ¥ºÒÄÄÍålÈN?†)×Öѯ^‹.ÐÀpZN0ù jFI;ŒiiÆY d@Ö påâ¦XÚa‹ÛLaQšWV3{LX½lmûäÁ§¥ÉYÉ&åRÆ·º Ý1ŸÎ=7žßlÓØÑk3?©-¦E_Æ΃ÒÆÓÒÎ[îø>2Ÿ6Ö¤²}¡´'”½kpü%è£Æ¡Xš‰l79oCoß•k§”=Ÿ‚,Îæahgc®SÙ®\ž9½óüàŽ×;7v}çö”ÛDz]wú°sòþèÖG½Ó÷½É½Êì‰`Üòæôæ;A:›‹PZBù0Û»[š=+n>ËöÎÂü™SÖ‘fQPŸT~J;ƒÁÎãí»ŸÒvïrˆPŽÞ¸‘?Éöãæ$ÆUÂÐ5|Wªa\ g”Ť´–Ö£+ÅæQ¹e +JyG¯ï'E7ƹq±NØS8˜¨Mu/ʘљݓ SØߥ8Ÿ”«¨ÞÅŒÛ&̈X•e·'æ&”щˆçmÙöi¶uc=¡°ö˜ó¶èì~+DçÖpûjD Sy½v„Éõk!"Îå2V—ÉoÒ¹­ŒÕ÷ãÆ|!ÔâVB¬Ã¦”†T9 ì1_ÚÍ8Ó •g½M³q¼œ€²ÔãL š¾_*n!à#t>FybaD¬•?Ÿ¹7@6ÍÖIÆ®ÅØ•³žT¡ï@÷^[ðõjèu¢Kœ»!wH³·åýI‰1 MZÚ 3¹›‡vãœ) tap7ß?AHÉEÂjËÕí¸”§¬íy(Bwh5ö*wÄÂh×ho˜/à˨Öp•¨ÒK-.ÛÓª;2 ‡—ÍoÊÕƒòÖ‹ÉíÏïF…2pH¾<Û8zds>À ª€È¾°gïÛ“ln$»-«µ—±»¬7‚¥í››˜í½¾‡5¥²±÷øóé½çÓFõnò•ƒ´Ò„~¯Î7n¾ZJòt¶›Ý+l<¯ì¾•ÛxÈ·ãrC¯í OßË nPˬNÏßë½ÙÜ{1»û©PÙKJìËUkq +ÆMÐ9ú¨¸ý®Ñ»›ªI>ºMyƒ…(ïK©¸ÒóbéHožÙ½³8k²nÏhŸËõ¶´_Þ~Þ:zw>«Oi·²ñ˜ËAa¸t¶Íä6¤ò¶R;¤ó»~bþ°›VØ>ÇärÕ¡®¤Â¦3¸ÏvÉ¢÷X¶Ð½©—¶–SJœ¯†É|J¬KÕ­}‡Éï…é’]Û9¾ÿQˆ°BÌõ¸ ÂE„öîLî}—²·ž~ã‹ÿI¾{|5*®ã6ímöÞj\Ù[¬ìûqËéÜuZM( ~2!–#\ êÇÜ×j'¾„F °˜0®.†3 ] Qq1g*ÖNÝñã¯.&¯ùi…I'œñbT.Ζ¸fJRùÀêœûÒÊê|ö-;ÉSRÍhßL D,cZ“t¦\a§4}Ú9}?c6Œê&8]q| Š*Û> +#Îë3^¿<{ Õ÷h§ŸÉöÕÚq~ò$?}¢TP½½–ÑãbA*ÏP­†ÚÛ¤ó{ˆ1„ÞØyV›]€n«•mÐ1.¿AXC®¸#Õ÷y¯?9z²sñ‘XÚƒÏJ…ÊÖãÖÙ‡RãX/OÇ{_ÿÕŸWf·‚œ—ÉŽ½ÑÃÚÎËÖÁ;ùÑͤà-A SkˆR‹ònÚ¨¹<åÛïŽo~ZÜxH{ÆTgO{”klq§°ùDmœ0¹™Ñ¼¡Öv/Ñu„2êS«9“Ê›¾šR{|n~¿ýö­÷wî|pÚ6%Sæ°1{98ûTn]`ÖˆÔÏ>úq¡wÊؔ՞¼ûô“=ýÆOÆŸðåÝW’¼ñGßþ™Rܼ“b\U(eêû¯6oVœ>ÆÕjwç~®s°˜`ƒ´‹ÛS½{xöÖÞ‹ÎÞsÖ!rÃëß nˆpXoÊ•vIwê—Þïî¿€ü"º»w—Ío1¹ ëQ¹Eíí[ïÜyõë I¾¬×Žõú¡XÞÆ­!ª¶|…2•Ùcøû…àG,©¼¥·ŽœþÍÂäÞoTpk»ýç« q!@á0jÕ3:7cÁÝô~Zé^‹ˆviãþ‹o)Nï_¾¶v=2[»1Á“+»NÿöøäÕé“ÏOÞüõöÁ £ºÝÙ¼Ûܺó§{ZÛyæôïÇwƧïî?þÌîl7§·†‡o‹å¨[Do&¥üPÆ)µc*;¼^gï!í"lnž5ª‡ (v~«¶÷ÖàæÇP“reÏèžÏ_M.¾án<Å쉟΅(W,Œó“[ëžË÷0«Oº›°aV÷œ-ls^ ÓˆTá½ãvÈü„,ÕZóÄlÆ;N›…Þ±FU¯o—÷žÚýS§{â nyÓ8ëj#m>Ê3µyhvoóÕ ‡ÜèB®lûPµ2¹]ß}&”wåò.(¦µ­úÁ)Õñ7ï¾ÿ[;O¾Õ9yUÜ|¢”f¨ï¼zôÁ„à‹Fu·¾ó¼¶û¢ºû¤¼ó,Æ•/^|k¼ÿ8Ÿ¸R.o?é|Ø9zUÙ~«qø!‘,ÅBkF([­í+#±¶ŸqæˆâjQyè,\ï-°kab!HÇÙ +i8g"{SJ~}1L=…éý”ÜÄÔŽƒ<åíÜù4[ß fÌ8_€JˆÒ9 î”T%ÌžZ;ôú· —i­¶¹wá¶w’œ-ºƒÂàÎÆù‡³;W·*Õ-6Û„`ØØ{æCä¸àñ…‰×¿¨m¿›<ôFVç„w¹Þ ¨¹²“^˜ÃÛ,´ÙÍwo Î>’k; 1_Ù|àõκ»o<úxtöJ«ívw²û¨»÷2[^ûðäÉ·gzûãáÍŠ›D·vëá'>‰±n˜ö2VËO¡²ý»bõ7z^sïî«/Œú6€¢Ý9“jÇJ뼸õlÿÙ£³ÍêþÁÅÇÝ/3nWëž°ÕcÜݪ'V÷~ióåN³~÷åw{û/æO;ò%Äè¡Ö€p†˜Ú,ônUÇwwΞõž"æœ/K½‚–Š ´Ýã·ç²)æíö¾7ºeuo§÷ŒÖ À;„*=‰0iw³ý›Þä‘7}ZÝ{/7y,·Á€n½øüð!°_àj¾Ànç´8ymŸ¨åM ØŒZ¿˜¿°ê ö|aC¯îzGG÷?Ù:{'”±DoÜ;x>¼ùÒ>?ðžßdœñéÃóÍí(¦ÎŸxÞÉ Îõʶ\Ú%ÜÙzÆ[J¨\¶§æÞ³í'¿Ú¹ý‰5¾ïŒîÚƒ;¤»½×ùì ·ýˆÒÛÑŒœbìpÆÁ¤6i¢„wed,ÆƘüÕÀÚšRÖT®œ@8Ó‡ –* ®&²µ>Ÿ@U"¬‡¨uÂZóP)ψl'F™Œ^”Òë»Nï„u‡àhÀþÎ[ÚºâC¡´*›Ê›Ï¸â,­ÖրɕZsã>ïtIµRßyÔ;yÇróãñ­oT÷Þ¤rÃ8ï5w£ + 'ä¤ š3vÛ‡^ïewÁ3ŠC«Ò/ôv­Ö>t7‘í±Ùš×ÚUŠ“¥8CmøH”w"¼Í—6´Æ±Ñ<±š‡z}c,ÑnŸ?ÿÕ­‡ßR[7ŒÖ T%?â­öÆáP€´\+Ξå¦ÏÍÞ]gã_9*ZK+nyãøÁGùþñë>4-Õç9}|_kÝÐjûÙÚNcxúéw~üò“ú©\˜-bÆPªtN>Üzúýúñ‡Wï¼óÍïÿAqpºN8oswqw§¼ýÖèÞwÝÑE”²õê–ZÙúrvß +ÀjZªÑÖÀuºûÁÎíwíæîpï±Zš‘V¶‡¶¸Ž›à,@ž¥ñ}17àUžeÌv{ëÑàð¥Ù¼*MBmºÇVm Sj\õÈÞ³GwÎ9xh„°£DÖnAÆ\Ž2¾´Œë¦t`t[¸ÞJ ÅêæÍñù›à& Ö‡é˜Ñµû÷²#«¶':ƒõ„€°†ÝØKqåÕ¸Ut-,¬¥Ì$WvÛ·K£§Lv˜kyÐD„HkÔ^Š+AÌaœ©Ñ8MñUfÆíCß… ‚<y2;dò³„ܳù´˜gí¶?£QÙ¶Ý;5ZGZõx,?{Ž9ƒaèÍ}±ºí'Ì´q©OX]Ð=1?Ók‡Õt¶2¹SÞ¸°ÚGBi‹pû1¹”TjVë¸|B(¥Åã 8gà4örÝCµºµ†©¹¨W§IÁ¢µ‚à ËäFwA` ³‹éMÀHÖÛ œ¡»nv+›÷{ǯû/×1%ŒJ’Ýjî<˜çVj>GiôËÓ;ÕÙÊæ]`$RmðÞ´úZ„‚.0êû›çïᨕð/øËëQF)ÎP¹¸§#”%—&ùön}xtpûm§¾¦sbq_.‹ùmˆ9ÒzO+ít÷žLn¼erQ¶¤4o:oj;Bq[,ï!ZûæÉÛÅÞ1Äÿ0S@ÔV~p1>ûzk÷iytËkïCŠøøÛ?iÏF…ºÞ9·ÇóiÕsÓûfó8Ň³»ßÿ7ÿñþ?Åâ“ÛÒ›7sã‡v÷L)mÊî`ÿüÝ£‡Ÿcz+&j+cÄÂål j7ÉW¬òFwïªÕãb3Æ×|Øü´i4óƒ“Áñ‹ë’ZÏöµ2ìÅóþîLn¬%æY1Ûí¿À¥ú•ur)"­Õ6Ÿ9[ý|Ý(l|ðùïܸxïÒrê²ÐDéeë'Ný|“”Kbi RÕ8e‹ù®µwšP;kmß6gFó/äqwÆ’Ù~J©'å:nõA·;Çïð¥Ík1W› $˜Ù¶:ǨZOp…¤PÈhU·NYm)¥²cvÏòÓÇÃWÛ÷?+o=JÒlhµ­õ”#Ý$ìÔIJ TkÙnŒ³õbŸ±;>ÜLnû3Y`*HÖ€¯¨Þ‚/ß¾ù6ëtŠo•v Â µšëœŒn¾ar¨T†¢%Ô&t½ÕÜSkÛ‘ùÒ{.çôbŒãK‰¤ÙÔ‹ãúèDί…¹«a~ 1’bUùemŒ³D·ëv{OÚϬÎáÜ¡´&ŸÛð!HAJ,éõƒöÞ›£óO™âa(,E%³¼©×v‚TözŒ]Š°~ÌFåŽX:êÇë¨%­Æì> "øQŠ/ĘD]¯}V>p» #LIožPöd$DZðþªæk;{w?ªoÝC„ÈÄÞê&„©Ã(WŒPTµÆôn¾uÚÈy¹v˜Þm¾muoÈüZ\Ösãæì.t1.7)g a ‚¡QÛ¯n<0«»IÚ™î=jO¿œÉYMðyÈY­ƒ7{Çï¦Å¨_10®Œ²¥õ„Æ­õ´ç‹œ;’¼¾S™`ó§ÉDÎÛ´ºwÃxv=Ê.û3Ñ´J +^Æìaz7)Õ©¦×¶…Ìj¾¾Ò¬<{qL(n0=‚éóéÜG*nþóæ§O뀎ÛO(g°“j%ÅR\,‘¹QB.Ágµ†Õ< ÅÔ*H_yãžÚ8ª;RmÛlïs¥Í¸P„ø/Vvü4Qr¦ln7q¡¤=èÐòðÔjD¸<¾ÏÓbÔ>.ÌgŒ‹³ÖþóiÆÕ6.rƒÛ€b~Ó¬@=‡H‡6š\¶3ŸŽÉ3î˜)l@HŒ²nJôR¼«åÇJa´– Wb$ÄÌ«A&Æ–¡h•Ú!&U6;œžvïƒpaz3;i­nÅ9#ÊèøÒgÖÍÚ–Tj%Øìi„¯ûHå¹bï¦TÚ\NpQÒH±y?bâJ›Ëm'äæZJ!€>o½‚¤|y\¼I{çesçm¶°9È­¦4\ï®Ä…¢ùÓj’ñ€.$ghÖ9I£n×·0)gÌm'ÅëŽõê®XÜ1†ÛÚËõÏa4ioŠÛã4› ‹ÀEœ;‰S.­Öª›®ðA`¡”XÏ÷•V6ö¶GǤä1fͪmæ7{òÛk)Ò"¸!!Ö)¹~y%³q½m4ËÓûR~ÁÍå0—` fãPÌo¼±”¼ºŽ†Q 6^bqÇ>’·¥„‚j”2µ˜Q+)±8W!£—A¶ åG*[}«}fj4O‰l?Âý¤»šÞ‹Ò¸[aú°<{"•va •òaµI£VœV6îÅ87Â:QÞߺûñÑÓï6ßæ*{¡Œ!ÔìžÇø"Hq˜*€v©å[ï7·îª•ÍÖÌÜ‚Bɘ}øþÜð^eï€R.”zjq„ÊÕ¤P¢Ê]ÁÛ‚áº$1¥NZ}†&? ±ùUT‰3n¡{"¹ÃË«±K+‘•8 <Ê–ãr#—`XQƱ + ´¥8O9®¸#Vê¢ðñ¤âƒØÓ?²ê!\ŽP¦/“M‰ ­zC,îÁ°.&Dè5,@®^ 1>Ĉ ";uÚ·JÃûnçôŠŸL3¹òð j{5!øÒª3À%o†èÝ´ÚðÅÙöôN¾}&¬PªÈó¥õå˜@éM½´¹¥®ùÑžÿ9=ßåRˆò–‘ùB?fuǪî†p³±óhÓêK¥t>3§º"QÚÒr=¨ùª¡Ò|æ½z0×Ò ¤BÎv/ÜÚç³”ÕÑyÜèkÕÊè#|åº_ 3N}·4:»f×âÒbˆ½²F@éªå=¥´Íd}qŽŸ?íUK’îZT ­ÖÜSôF„ò’|´ Z ì ^Ð\~“-ìâÞbŽRJ#D˜ A 6Lk(_Îè]¾´¯¶ns…­©­$éP´Û¦´jF©³ö\­”7T¶žâÖ`= ±·–mF7Ç'/§go­CÔh§·›‡Ï¥úþµ8FoÖOhaF»0D­8ml=ŠŒ‚TÚÊäF÷·p³ŸªQÚÅÄ"übŒP“´ÍÚc½~ÜÜy^™<àÝq˜²£,®¶0xéMPNÈàbù¤t ÆÖSj„ÌBmûU)ï76”¶žÝ»Q±ÑF-Žk›äòvjØàæ€ËÍàãóÕåÛ­NO~ŒÈµ¥¤1Õ:i¹®ÕŽõæ-ð™Å›dK‚»Aè}Bï ùÈé×"ì¥ãçÓ¦aZ-BÚÑŒ s´ÑIKUȧK )Œ©½‹lcÛk¼7vz·´ÚïÍௗbâË©«þ ­ÕÖDÈ›ýÊìÉ·Ô;}_*ή®#±4_)Å)T&H¥/­€¤@2Ln1LÏ'äQêëiü5 ŠþåsŒZ@…Öò”^ッ³¦¿v-ú+—C«ae ´Vó¥Øå(™ds¾4”¨C›}ÚêLjüu?…±.ÊÙËAôú*ºä§1 <” ¤¥ù\öD.¬3ÙŒÑî„–Á.mú ³}“+n®£úe_&œÉ&¹*—ÓJ•Ïo(Õ=2ÛK 9Æñ$¥1Vre~~æ½êP]ŒÒ)ZͲ”­qv“ʶ AT7m?úNaçêc|‰·{4ˆ'iV¯4ypôè{{¾(o¿³–£¨ªäG¬7N‰e¨=ÊžÙ ·wO-ÆÙ|0Å‹vWpG¤Þ˜¯<Â#â:¢ö(?~ Tö2F£0:Sk;aÚe‹seÖ›e{zâLŽ·ªãÓwÁßãr±úD~†X";hl?Ì÷O± å‡…ñpm¾°GÁ(ëÖîç;ÇÍÍ{¨XÀåŠÕ8ø4j{|n3ÁWÁÚ®‡(!Áì¬C" , |øQ¹´›Q:œ=ð!¦^Ùe¬îZ\Œ’nZnòÅ=£}îön#r´4Åçk›÷H«c i¹µŠda‚¨‚‹…kë£UÍÒDp:Ze'%W—Âr˜ä´Šl÷|ó‡æL1·A=\nÄ(/”q®ék‚·ûµµââL!ÎWPu¾îXœ-Ì'~$¯cUw ´þÅ|®?j®xa.„Z¤ºåü˜ÛÜio?L2…×—PxÃj<«šfòþ„xÍO¼¶ó%ä0b&ùâåùüœxSfÒJÕ{€è|@Ÿ!÷­¤DÂè@|¨ŽA|^rµ½õŒµ’ÒÓb#€ªþ4^¡sz盥­—„=\ž?œ("ŒŽŠ¦–8o(Uw!Iö²iFI’2*æh»Ë8=@#Œle‡,nAr£4ïD cîéÝÉñËáÉûBõ$ˆ;(—Ï(•iÆhW«ì ¹-`­qF;›ÁŒLI‚ÕÊH¹ÅÂUÒè†q«Rez9%”¥”ï"R)”±2z‡ÎBÉÝÏïX¸³FÕë3N߇iLn³¼ý´úþüŠäô!a´!ã”Z{^çÀ‡Y„9ö÷Ç'ïnßùzûèm½yÁu3?œ¿ðÚ~TYKÉkIj›wF¤TGo5’Q½Ae|Ýšë ÌÂÕÎüF‹Ü `7 Óâè.íN‰ì0)TWŒcF–8›À´†Ó» %˜7»À ¨E?žá]BtqÁqûœ7…ô%ìŒÚô§µKëx5Ã{9Â%(Ò»˜ÜJÎç”6Á¶bt~%*‚øqs Ê5cÃÂQ‚6ÑË[Ze7˜qÿÕ•€V‡‚ä–ãª?ãè"ß=‘½q>•Òt! DGå—ƒôõÀ|XèØñHÆ^BŒ¯.§®‡ˆ@Š‡ȸÓlëlvëw„>K{«) ¤‰Ív(£E8“Œ7£óõ”7_Î×ÂeÁiònT+e3¹ë1Eq¦•ÞMHaBKñ6¢Ô ÁÕ·žš£µ4·Åâ¬Å¸#Î%9ÄŸÖk…ÁYiû ™ßHȵ0馄jÆèÅ(:H)MåÚl¤!È’k “Ê|n†ÈùšƒrU©ž(õ›dvJ‹!ìµåH8£“æ`-¡úS:&ÕxgHêM97Üè@8cfô&à ª6H{¬ÕOíîímñ]Mò«1rzüÒíc`²|ÉOæÃt‘ëVã•Š .ï´OÊ›*ã³|ï„Èöü”,!{#¯±Çg;‹a2€¨ëI%ÁUQµ¹.¯á Rrz…ÞqU–çÓXé„ÙkÌž4w^RÖh)Â#|aóäíæƽa§¥:ø¦\Ü‘Ë»œ7™ß¦ÅÜÈlì†÷R€\Ǭ¸P§@÷ô~š²q&+ÕÞÞC>?Ydz—‚lB¬…¨|˜)Ë•Ãõ„´¸ŽJV'×¼õ¼^_J]^ÃÂdÄJëj€Œ–\ÜÌvÏ¡z1µ™äÊëi}1ðY”îzÒÀ¥:_Þ×:g d(:FôÕ¿ )±ž-ÏýèWBo,¦¶œf‹!LãúbbQчÀsUWÊvë°4¾YšÜ*oÜUªÛ@‰i¡Lg{öðòz·ÚïU¶^8£@,À 6‹*…(›e܉T=%¼}¾t–ï?¬ŒHùÍuDL²6eµ’B.ÁºÀ1ÆãœÁàèÕÆoƒ§,E™@J Ô8iÆì²åÝ„ÒÌvï¦OP­#³•Þ¡^šˆ¬·|h6Æ@‚nåûîàaZjÁ1aÀo-ÊÞ€Œ¶†ê¡Œ¡sFý°¹ÿ2q€Ê‘Z3%Õ`¼B¤ ½€Êurþ@ea)JŒ‰¥Ýòøîàô½”Ò]±K1èEÉ‚(mÊÕúÍÁÉÇÝ£÷P½½#$sÃ\ç Nš ýüÞ•›JóVJjÕ\ñcóõS²½Å uÕO@ÄçgwÖ*7¢„¹"BˆD©eš‹d”úcj‡Ðz˜\‹‘6ˆ ©·Ã„ ÿ”½ +«é9iå ©W¯¬"¤T5Ê»Ùæ‰\Ù‹’^U“ÑJ¬Vˆe$B*ºµ=«¶c5 kj—ÖÉ•‘šßRe/ дWâìïmĩܵýµë‰ŒTÊ·÷Óœ³à˼¶Œ­¤-4»!w¹“‚Ó÷Çh+×+ Ï!-þë…øÕ³˜Ôcb‹ðöäâ¡/.¾±gµzwöèÒRú__ ÃRRK,ïF2tqœ±ÓBµ;‚Óf­©å´œO¥•Uófã€uzi!oPÊ@°å0iƒªCêD!|e»I±@Y=¡°M¹›Ðþ¸ +1Ä ÎOº´3’ʨ¡z!0’f'£6ôêv&Û_Lk‹1!Å”âcuŒÊž7z€šý´ÖsÅD ÀqPêq±²Ic@;3Tí&¹ +°ñ:b\ŸØ ƺX‰PAT=Ë;›”9¢¬tîZJC„r„Ê-%Õ¤ÜDô>ëmA‚ ’îrRX PB¸TáMòyÄ`æ¶_©.%Å7ÖÐÕ„Ã¥s ¾HXDë‚¡ç{çFýˆ¶{I* q ‘«ËVóZ„÷#:gÁãLqÁ‡Ï·*Îû1 Óz\nw)¡úš1ɸ¤\Z gæ³Upäs„Ñ…@M±£tÇ:¢.Fè¤PcÝM»w˜Áj—×ÐõÛ߸ëT6¢˜êKˆT‹V‚0áµâO#8ßêî|øÍÒJa%&¬Ä%hÆŒ9ÑsôŨµ(!zB­®Å… b¬DÀ$€(T(¥({-Løã\Z¨cF7†1®q=@.2k’’òÀä_]L-E±0¿#K­QÎìZTùÊÕ0[F¬ ÙájŒWrΛ¡r'€ÎctÊ{¤VW‹˜ŸO~BêÔ<§$¨,kvÄÜT+oqNŸ2”Y “ú|ù¼‹ +@Òl!R>-x­JšmÚÄéb’-’fO,m×w_4_IÕîÃ`#DvÊH/Ä”Ašæ3zU¶X»'ç§@ äãR-Ì•ˆT•La<+$7ð/oê…šYŠŠáLŽu¦ˆT¹¼_öc„ZYK*˜ÜŽ1•å„¾š2âl‰0!ª$Fd'ˆ>ÀÍÑ*n}Õ‡͇.'8\ªDq=Iè‚;Èa ÛxvæKKIˆ+Îx A cQÒr3½q¦U:»oi•=( Z* +v{)B^S!2‡È-¥´Wß|JÙ›̾¤ie=)]À»¸ÞCÝs[@øþ´²°Ž­%ñ8—ƒ<¢Ü¸P†gì ¹¸—k—‚ðµ ”(0cÅÂÖRB4"ù²]ÚD(r=ʬŸűd‚ ^ÒÊ^yZjm-‡2×Äü„›§³ãÕ¤v5@e!BduΨ“«kxšÍ'˜4iš/Au]ZJ¾¶JSÙÌ|5=k%.û’š?©øbØ_4cPcÁGábµÐ;1ÎZR„ÿ½â‡îÈ#bþ\ð3faüè½ïk¹Ék‹éËë\‚/™ø’\SÁv“´ U u•fÝù<½J‰µZ¤Ö$½¾RÚaœÉü” ªÇ¨l•ýˆ‘är@DAÜL±ÀrC³yätOÌæ>iµç“° ùõ”µíO)ˆPˆ…lBÛC(${êù-Æäìþ=¹~S¬žsÅùê I¾ +`â ÒQËÇBá²&Tv‚iù„±«éHZaôº/¥°Ù‰ÛCÕ>|3¨(ŸßוXœ!@K„.¦µA®&Å–˜ß–r[¤Œfwv Sª€Lˆ>Ljó•ë…Übf×®…Jo)_ž“„b‹sÅÕyÂC°¿´¹§ A#ÉWAN¸½–cJ’«ŠÞŒ4:×20s˜rð@~%Á_ óI±n6OÕÊn’ñö/>¾™”šQ¦œ”>¹—Ö Ús5b(²ÞªÎ'±Œ":X-XØåÕLFªG0J(Nd¾&9É¡BJ}Á¹´=e5~m ½â¢l¶B’˜6.­ åÞ øéåuòšŸ óÅ%À6Ñj"|ùW®Æ¾v=Ƭ¢-Ù+«øÓX¤ÒfÍn,c]^CCÔb˜*CÅJ”p}I=„hVqTœÀ_I˜6HdlfIo ·ÞR Ar-!f”¦RœQf;Ng}¨+½£SŒæe¶ÎåÊqZj R-Å—–Òk+é+~ÂPq®àG¥9HDDi¤Ä +¦Ôൊj Aj9ÆC…„!I%˜8 rT%Ô`$íCBA¤ªTÚárÓ´T‹2È t$£k!ÊŸ2r•Ô[PlÑŒ™fæ³qê¥P44År˜…Dø"eR•TÁ¥x"ü+*–!µÁQ5Z·ø┥˜Û„°0?Ý- ¿¯&ÅíÊ¥=¾°›’ªÄ×QýR÷¡FJ¨AÐ^OÐGëéì: +Üž_ŠÉ ~P^-ŽSB t%&“Ö„/°Þ¶¯l] S Nàt«)y!B£J—0By'7šŸ ò ašœmÞzúywû"B9Q¾šÒ:I¥eç÷Þ4ÖÓ¬£”6#à|Iô´Œ·'–Ž®ÐÜŠ :4ì +ˆyR[Em¥v£ºù˜6Û—–¢ÉŒÃÍ•yêíZ¹´N…ˆ"—ß忺NF0ðèÕã‚a­%UHÁB~Ëjž.‡¹õ¸ ¯XÆcÌs(mn-ÅxÒh…qãW®F—Â|„pH­Í˜\ªSÆ•UêzPD¥f°Q­Ÿ”ZðŠKÔ€R™ŸŽ–j„ÑëÇå6çnŹJ·â´HeÆØ­s©´¡J<·Žºa¢°•Ö@BQ=BÙI¾ù´¾è¸ ªN®ìƒ5„ 7¿!W¢ó@#˜§,\¯±¹±Ó9×êÇÀ61ÊFø<.äìÚ¬Ð?gòeP Úh!¬Á€RR*" œ‰pÙ(aD)pä”JÓ–CÆç%g@)µ›Ãظ2½ \]æñ!2&¡/8w„Î×2k9£r…u!Ò†ÅW²Nç W‹a®:¼C›ýš½“ÓV€JuwŸ>üèw}ªÈ\ð’BC*d´Ç—«udct.Bzhj ½àÇçç4pµ Z{%e€³PÙ-:»¹’Ô«×S0peÀ^±œ4îÇ­¤PÔj{¾?ïC²!ÌLÒv’²@¾b S+Œ7˜ßý›ß!ÅÔF’vPÊ$¼05¿v£×öåÂ,Œ[Á´BÊU\(¬'é…ÕÐZŒð§DÚº½[i©²žV–Â,Êæ!ë1.A»”Õ—«G\q'B;KóE–5àR?¦¤¤r„-çPuG ]먚1Ûjí@,î$Å&4¤ÑŒ6€ìæOr+QŠÔ;zý,-×B7»WÊW®ÇƒI™TªkQf)€A‡ßå77’2›iÕf²#¤‚µ1T_Œkí?ïÝx¥Ì«®ÑÜx@óxH9cè[I1¥4W\O +Ð,¥J ¸¥0‚BbJq¶èG¯] _YC/û2WÃÜbL ´£ºŽ0_SÊ{ Ê[‰lvšä!¥:´ºa_¿zc1 +e“ ŠP¢ÐqZó¤}òa~ó©Ý»Íº³å¨úÕkÉ0:­7–ÒÁ” +þÂ{S§uÆZÃ$_—‡ŽkÍ ÎØÌŸñ°Ü^aöôðÙwï¼ýÝÝ[¯Ö-AŒ-&Ø®µƒ˜™µú¤ÑÃõvB(­$äëQy̔ȥÄ*H +ÔÉ*¢FØ\àˤÀ秈çuH8i¾œâ ~LôSŽdò(ÂÕ´žâ‹ËIÐ(Á”ÓB‘:€C1ÃŒ¤=PŒ`ã%„z¶sÓêžÃC»EpÍŸýèüjE3X6Î@X‘zü÷uäªI1NŒ´‚3ÁW!BŠ…}£yöh)H.¬¤ãsß´ç+Mðù„\Eô.a0­½Ž*‹×´;v·ÍÞ­”ÒŠ0 l6üSœsc¬F"2;ÎöîÛ½û —×Ò‹!:F¹áÌ|ùBà®0U@QŒ)BÔµízlÑùâìrˆˆÌ¯-V’B5LyÀr´Õ +‘®”õú©PÜçç _6ÒRrtÌRFS+m®¥$fÎÛ$³=`­ j-…¸+>t5Æ‚ûûâ×®„‚ˆJjM\nú0çj˜¿f¯0ô×W “ f‡¤…®õ© ãr-H^õÍeçÒ*²2_LY[Jÿr™³4[e¬ÉZZÏ*;g‡púº/s% Dè*.WE·'ºÝ«>8Œ˜& ëÕ ³äb\7Z£Ç» Eà×Àr\n›wÆ”ZCÈìJ˜9¨m ꥄ|5ÂÁk5¥Rz[)ï˵㌷áë+1Bó{|)€Ïn ÆQ»0àËP!ÂYIH_Î8ê"à­´£¹ž}`С‰ e'Ø r*39'[·}«{ò©»ù,Æ”áK˜£œïÁfó{}ñì +è7©@ õŽS¹8“ÓÂØ œ=>¿e +×#€)hp«~¢–2Æд—0¥—ÊW‚$«q¨7ÏÃle 5!¢ >•}6Û cr”Ê’F»¾ù¨wøf´V1m‘p­ŽëÝÕ´àJBñPªž’Ù j^ZK-…ÉpÆ‘™ßÁ‹»Ð¶iuê{¤\YÓ Ê°_Ÿëí)u"`;ù ªbR9ˆ©~â O›]·Ç^xƒ‹0Ð.Z +ÏËéÊ:¶‚÷˜„Ñ gÌå}e¹î' °£DöµÅø‚§È¨³uc 3.ù3  Sr•Væ·ô0ׂôëËX1âsr×)=Aåi~çÃübLYGlP?±¸ {úÆg +ZåP©Þ ²£¢ß^ö£×ÃDšö|îµkñµ„ª•wzGOÍÖnTKš_:Cäì1X30|Æší›…Ñ“ ™2Õ8H%SZIkWÃÌÕ§Ülí°É…NQ‹Ûfó,.µCT‘êiHF ´‰¶É„pý¾Ý»Û:úôf s·"]Lª£R1-yBqDØI¡cægH‚¸u-HÁ¾øb’Î’f€ÊŽò1©‰ õåˆpy[‹‹a,»ï3 1Á22z›@Ú¯/§¾r%ˆóó“<Ôôcnô´ê¾Ý:ͨåHÆŸâ$·'{Ó¯^‹ƒøC¼]È¡«qº¸Ú»Ȥøè?cOçË.+½æ­Ä4JBÊ^‰K¯/¥|)Ê#HÁþV“Ƶsi9E4Œ÷’\.ÌäÓæÈ=®ï¼Ô*G„Ò‚V¢ŒVat|ó:H¸¤>¢œ ð‚¥¤•']½0µ*[VýŽíbT…o^Ih`^—ørZ +³%:·£ÔŽèü$š8ì/öoÀ¡ƒ/L‚ «½([ƒÿ^3K1.€óç +[\iO(íáJ²<ˆ?g\ídô>ds\©á2¸jn ‘0½eµoT·ŸtÎ>É8Ó«pb9LRRá<_JZŽ±à5¬7ã½mÊ\õ¡×ýØüô`Jh€Èe?n€¦1Îi®·æ÷9й´Ò†øFg‡a¦i 3ƽ‹ðDÈár‘/LÍÎQçàeóà-±v¢Ëëh6-çt㌛àë„9™¯ìéN"|iØ&#¸ÎZ]a~¿î$*ÖÃHR“s7H³~ŠÈ¥ÜàÆü¨w_*2º¹É£ôøY~üdÉJ˜_ q@`¾h»—ªèáÌD¯AÙ3FÀs-ÊÑ«‚õ‹A•Aðx`ÅP6%½/ À¤ +et¨åKèQ*¤ÆÛ=ïw7ôZœÍ®F8€YÀ[9;aínJ.ǨL®y„iMÊ +‹… ÷kÝ5Â3öÔ®Ÿ€ØcÌ&•`wCdn#iEH@ïÆ«}’2[ <™Ì€s§å‰÷UäVÿA\¬ù '.Þ”p­3 +‡RùLÈïÀqP1Ï€¥m»q:8ý63xÀæq.ïKTKn‡(äŠÓ}\š¾Öê»Iï«!tÐ'8è«t7&¶09Êå•Š÷KU¤Q»¤×¢ô` +@@¨x—åJD,“vO«’Î`-¡aR-Be#dWë¸Vƒ·Sö Ýy2û먂¦®î™Ísðƒzy®äO²ªÛuj;œ;äs“•]GÀyej@–GÛOèV}§{ð|vù!Ý9&¨ˆíj¹i®sâ]Ç”kfõÀiy÷¨—w7ùÓõX„°Øtº&&U„ÒQºÿ¬¸ííÀÅX㮄Ù†"Q¥31i5̃ãÃ"å&Þí‚\!ß=ûxKƒâ'²@«ˆ©ÒË;ZaÍ:ý˜XŒIÍ-ª¸…çHsR?uÛGI.›d]«y08þbçñ¯³½KHÓz\Yô+Ja áKÙ\f|øúïê‡ßÐî´úV\åÓ£Bï +zö¿úIø³M2¥´Ú¿ê}£•0O`X6Ý é™©@Úe/OÞþ½Õ¼àó³0èj©Ê ¹ŽÒš@ï Rݬ,8§!Õ dœÈâbKÉíÖ·_p…©Ÿ¶íÚr|úÕ„ÕIY­°XRÙ&´Ýñv…‹TzTš¾«.¾²Z÷1¹ +(%w`W—Jid‹x‘ÛZé4Û{®”7=D:Þ/E¯íÎj¶ý´ãÇMFkD™ÌZŒ_ñ¨P´ç™þc>7RöjLLpyx¬Å¤([`s³ÌàÑàìõƒ¯¤êaˆ…Ti½Àìõ¨â÷nV/!r]¯,¯_]¼‹%:ÓËö.œî•\ÜAäòam6Ÿ˜å$ï'*×)³›iŸ;ˤÒñÎÃHqÀjaj5)—R,¡·h½éY!ÌH +%Ùûâ ïËà¯ÌσXÊSZ•vújõ(;ºNwîG™üFLÙJ("DÕe&(k3ÂmDEL,k……R\‚ûÛJŠPéêˆù͘Ìgæí“oô橘]2ÖÐ[“ü¨ÒÛK²Ægþnøâ‘ݽ®n!–õ½ ®,æ»éÃŒOÌVÊ–Š{ – o_oYY§ƒ6³šPµËØ“êìUÿä[D¬ÞÙ B˜åD9(û• ݘP¦X½SÚh…(77|´|òûúÎ{½qæÃóÿÏ?‘1øÿ{ÿ_Û@nÚ¸ ä¦Û@nÚ¸ ä¦Û@nÚ¸ ä¦Û@nÚ¸ ä¦Û@nÚ¸ ä¦Û@nÚ¸ ä¦Û@nÚ¸ ä¦Û@nÚ¸ ä¦Û@nÚ¸ ä¦Û@nÚ¸ ä¦Û@nÚ¸ ä¦Û@nÚ¸ ä¦Û@nÚ¸ ä¦Û@nÚ¸ ä¦Û@nÚ¸ ä¦Û@nÚ¸ ä¦Û@nÚ¸ ä¦Û@nÚ¸ ä¦Û@nÚ¸ ä¦Û@nÚ¸ ä¦Û@nÚ¸ ä¦Û@nÚ¸ ä¦Û@nÚ¸ ä¦Û@nÚ¸ ä¦Û@nÚ¸ ä¦Û@nÚüÿDFnýŸÈø/ý~c¢O‡Ÿ4ìOüJšèÃßKÓÓéñ'¹Oüý¤r|ª/Ƨ‹ƒýáñå: O! _O–¦ÃÝõП^¹/ZÏ/æ‹}x²<îNÃë1x)ÿ}‚¬£ëÈÇÿ5.?‰SN%PÅIAh_§hœN`CQ$MÑ)r}Ï{J¤( +#1%pú¿h×{ññ9ŽC/¢>¾ MQ(£øŸ^ô>î?ëEÿèã¶?©²ÿIñf=^oÔ?9úOq¡^\—Î\ìNÙ?ýQÂbü‡u„7@ìÖ'I}z¾OK–ºÞÈ}ÒöÞì-åŸ~¬ÿ§x¼hSd‚"׻낮£é½û?ïÞŒþã»þãÿÓë)˜%J}œ0¾î½ùÄïÿ8G/÷ì:™b0ô“Fú¿H)Õ(“‰óYR¯Sf“Òê)¹–ë„9 ¡·0±ü§-¿•Ü”s´ÝIˆÅëFo L©àJ5ɺ¬Q2]­4 S&3¦œn ¸ì"Êf}IÞgb„N¨eÎíKÅ_\2ÙžîÅ•RJ«…!ïvQ©,¸3­´kÖOH{€J *3Á´Vœ+„É4¦6H«GÛ=­º›Ôëˆêí×,W`ò!ÜBø¢èŽ¬ú¡P˜‘én˜Í r)!å³ Mܨéõ³}‘>*ˆÖˆù0ëÕýÌàÊhŸêõ£tç_XbV'ĺ¤ÙNiõ8Ÿ÷¦QŒ +.¥W½æCø,L®‡˜l˜ÉEùBJoÂÄp­ÎX\k J3Ó¼oTµd²[)Ó[QÖUòS!3SV˜09«'dF¤ÖŠn3l–·:ÙÖ |"Âç£@µ!31&gr1¶@YÒâF/¥w613H8q¶€eT(ÓFgò)¹¡³>Lƒ‹sÅ(“Ò.o÷Eg„ˆå8[ôáé{~%Âo&RA¹,!•HXC2Ä´$ãlƸ»Ab‘C¤ oй”ÜÄÄj‚+Sd0Z16»ã#„íG͸œ` „ÖIÉ-Jï±é—î׆v}&´¤P朱R^ðÙaˆ°B|D“a¹ˆÚ RÙ nSzK+n+Ù1Êæô✲ZQ>¤ì¤PdÒ#Ö“JEÍv˜âGÊÛu—u¸5"ì1®·½¼år×Q©(·íî¹RÝçóÇ^D„4Uwî[ÝOŠ…¤PóËLÿ™Ñ¸$Ì~L(E¼í23QÚ Sv˜ò6ì@•üS\¨Äù²?en&½=ø"tÚÛ“Èl$Ī6ÄüvºsU?é½Mªù—æ³c½yš<7ÚDfBØÚìοnÉå iK…âôµÓ{B¤'Dz”Ë°’q6‡ð8øÇmbDoûKÆ…D¬Ç„͸äí «6`µQ£—ª ©–kI¥‰jµàǃkf¼­cÃT&L9AÜÛ‰,J¥SR1Á¹aÒ +bFOÇàŸ0 + åÊùƱS;€ºÚsþ¤ºV‚L3âPEj›¶Ç¨ÒEÄ&oÏEgH™q&›ä‹¸Ú„HŠ~¶†Ja'!î„©Ÿp¨LÖìÊîTr& .w/Ä#Jzßd~ÇŸÚˆ0(ã’Z‡2º)¡´á`I.¿•”ý˜+¡ó›ˆ&]¨Ò„\#3Ó¤X aj”´7`b"ÂÊ‘U;5*'ŒÕwªßýÍðøÕŸ­E…ÜÌÛ]qø€rAÂþØD…(õã†÷ØDZ@ÊODwˆK%ZªŽÞI…q€²×™kéö³u+LÌõã¾HLi$¤jL¬áÖP.ìØÍÞ&GvNçD*Ïp«AX]È/“™h•ÝÎÎKµ´ˆJk¨å}¹zDXÃ8W¤‚%B„B”qÖ‚U¡ÆÜ0¥™ê„ÖC¥:¡zß$ L,F5&ä’JU«V—_6ö¾(m?‰+yB¯J…‰Pšq¥9žjõ³òä¹Û9oΞ8í£°·¥NA.lvq¹9ªwq{Šy‡­°VÏÛ;5©n¡új ²·9H(e®„_REÅ2T²LoÑnTj$•¦ ¤Ê1¢×7QÖYÈ/ƒ¤³‰è÷¢RHCf#¤c2Sð3Îf£DÚ—7£âV\Xˆ‘Ù²fg=*@QÁó«!v=¯E¹é JƒÏ.èôœ±§J~_Î.#T–TÞ^rB9ÁWp­‡©]!·´›÷1µ~'D®Ç€A`\mÄÀ´#€xTöŽ¸ãÈœ¤]\é r+JbTŽÔú‚Õ¬àŒúfRZO¾T“j,$4©õBòI.·fB˜Žò%Æè p¹Éè:t| )þóõ›é™µ=(°8àžÜŒ •¤Tõ§ @þðÇuÀÔZB.G½=ÅJŠ3è-Ÿ8¢r…öö+™$åúfÊð㦷ñ;R‹3o׌tÓ)³ƒ( Bï$øb7­Ê²8y¨”çFu.–¦„Õæ²c±¸ ?q`^¹æ6ŽÄÂx•}˜Žð>3M©-T†f¬;D; ?Òôé0]¤Í‘]06°v')–qµ„ÅØm63 ÓÞ—çkS±²‹è ½v WwXw ƤÛMw/»ûŠÓç˜VPéaB\€¨+1a Ó¥ì¶Û{b7/¼}ÁÒ}oƒT`¼ +ðcÆ¢¼M®‹16IUˆX/*VJUû´=×ëW bÞ.Ø|~áKYIm3iàJó¾>粈PœIzkb¯‡¹0f†0ß7bb‚ͧ¤Z0eoÄ•­¤Ä,€‘;~r+©DÙSFˆ’Ï-áù„TG¤ªàŒX£%ˆ2˜Ö¤‰\ÜÓK Æj!|NuÇåɳ„PŽÐNŒÍFÓÛ ô€Ñæ’rs»y˜K«I9ÆÁÁ»œ=d– 3@·¶ÿù‡¿>øS +11G¤ûlŠíÐig{”ímÜ-¥s¨Übœm±x,„Ü.—Cï°ZMÊÏ‚LÈ:Dç©&fç|n›qgvípvòUyô @Û¸ÖÔ*nÿ¡;|âK¥m«´|þ比Æ|7€j¥¨-.=’2cÆìx*%eBº oSÎR˜ÎÃ#B¼äèV©z'„û'­%5|¨¡ + ®*g&œÓ½ã7z”©°î®T<ó»Lzœ”jP ( 5¾„¸e·B7p¹NkmZk!l1L8!Üfñ'å™îÛL¨k1ÙZ~Ä„ÒŠÑÙ™ÞHŠ«QöÓäZ”÷%õP +h4 åêíRMƒ$Ë®ÄùML…éAÑ +î6eöSJ–:Ô2,~˜tgVÃìfLÞŒ)ÜMqÕ +ÈH2¹µó™?u/ÈmyÁ#ÞöŽ {t6ˆÀDI±uË8(Rï¦DÐØùµ0}×—Šð®â"¯$䕸JiíÚèz+ ˜YÍØ‘›¼»L·.æ%ˆ+V-dk »q@˜”ÖD@´ëU%;Íéö„HWHwX«a\8 ÊßC ¸ýÇVó„t”t”â²L() ¥´“nŸÔfO¾ýëæþ³„R¢>—ßæò ° Lf$—¸Ùý@¡\๰õE[#¾·×¢·ý²±|d +a¦˜ÒÚ\z ¸cÁXÕýRïBÊMbBЛ„îíàiÔO3ý‡BanÔö.^þÖî­&@8t“;ň +• e ðç-DM2®·c‹3œ'mä&®u@Ø@"¶’ÒVB  ÈRïlfZ÷½ÝãÔI‚x¯z»–Së= +T·ÔŒ1ŵ¸¸¦6ã|U"¤ËUÄ€Þa´6o cL0*FªÌnÄU?fA¤l%Ô­¤£]€ÇnmÄ…Õ%3¾„¾×c2hÆÂÊÆ—2¼Íy¥j˜va΄Ö¯eoOÉUJkR¶Ñü€B æ!\][ %Ô€%ïI€ µ Ùû si¨Bn=ÉùíŸy)õ” +kÛD„bÑ Va’˜PJ°îj\„L©õ›˜g7¢g $¥*ä‚sæ™î“Âä¥\XÒZ]0jÙ£òä +è4;”Õ²C³²ÈuÏ„ü䇔Ž· œU=Ô+'zåT*È壔Ñ@cÕF®w ,Pã¸ÚÊõWç/{oj{¯˜ÂÌfeúT«…øR\®¥ô¶^;²Û—`Ö8wSâ3o“G: ÐGhM½z`7Ïk³— ÈÃ|Õz¨­N›Ú†"Üs³”Öö„…eìÈ~T®µÃÚâµZ?”+;™ÞYB*oa&*× üÀ‰l¦´å€Z`̾·s4á¬Ç$F©Y•T©®'Dà&Li +Ñæ Nä6`%áIÌJРŸK)µC}f ðq(**³×jÊÀGÀ_£T!FW¶o !P>ëQd<ˆÈ5!×¥ô(S?a­QˆÌ€û¨&Õ1±ª5Ae@äoÄE ÍØGõ ÂL„/P;„Ùah¬1˜ÄJÐ{Mž2{Ðà°ìP ¢;¡ O…F(Wê¢;…5Œ³ù­„@õ8ߊ«P€c ê¶â + aœÎš­Fx ߦÃc3XªÂ%¹€ˆ:Œ+òfoÕVÂÌJ€\ ²Ò:æìÐîzR†E‹³Îjÿ‹Õ(âmUƒ^ 4ÀÏ>猸ÌU«@ñ_¬nyp©”·õÄÜ·ºˆZµ,eÛ”ÓÎxç +jG¨XæeAií¸Põ“¹„X¥­žQÚás#¡cJ™4[bnÂ8}Æ!F;¡ÖõÚacç­V?{…Ûƒ”Ñòs¥r(÷¸ìì6¼≯P]P!Q6˹S¹|ÌævQk”²Æ ·}¸•JLº©U0Ë uH“BM)`r%„œÓÇÁÁÉDk z ãóð€Ü­%´ D†âŒŽYZÄ  C4¤ŽëM0ÝÁ¤Lj Ú…ì¬É ¦ãJ[Èís΂Ö{¸Øðy]ÌŒY‰°wCÔg<„ƒGhDià…&`ËfR? •Ö +Öœµº`F‚„"0´Ñ#a’|^ЛqÚù̇®G9(¹$¸ZJp òI¦ XÝn|º‘؈r¾$ (í*¡µ@0ኙ©èÎ"t84³·]¬XƇRDuø¥Õu7@€ÛˆÉ1*Ÿ`ŠN&'3~D^p`1â2€[ ,å€;.4‡ïW¢Ü_l ë.ÅW$PËN+ÁZÞdr˜°VB©¿X lq€50¿oßI%ÎgÁjù1œ>À©èŒw"¸Ãëµ ïNP£±…I !C¨%Þnñ™¾Q?ò3Ø@.Q±z>.–p½cTÌÚ›îû s•“R1)—Bl88ÐE™þµ + ›Æ¥¢^ÙÓj‡^q9:=²Ú³“—\yÏÛ:–ñÎ6$”²Ÿ¶Yw&WÏ„Ê•£·#l.De-•òŽ­xû£]âÁBÖ6“ +*”Åì Ði=Î3éÑ:—ÊûÐ(Ø%êßÜŸŸw%.BõúA÷ÁZj•* P¼ø=pñ>’v;F¦S|ú2Ùˆ&,i0­]F„jŒ.¦­„9OxÓ™•þÓ¶të Šì<ÕjDØLH™®¯Äæ<‡¥5€‹¨Æ+!Åét˜06c,§•¥L˜Ú‡š[ˆ±‘@ha|9ˆi?Ùˆ"ŒKÊÕ0X{<½…¨¡”Aë])¿$ÓwÃÔª’j0Ç۔ͭ™õ¨èG¿ÜŒ­„ ¢€áEùb ©¬ih +_RC=´)«¹ïZL¸¤>õa~RiL.³vgÃ;×—¡õJW‚ÈŠ?¾@Ã)ƒÑÛ2àcGQQËt¼ ^ @WËÓ{rÙ‡Ê Ö&ÕŠ7€ß!ü0iÃôà¼3$ÌVׄlU+QÆ…ÙÄ4?aHs:W|v†‰U½¶d,Xð îíÄ L*Õ]µ´ÝÙ>ÄíNJoQéa\¬…Ø|¯{•ĸ üH»/w2ýGzë4]Ú>~ü3­¾ü4@P/ø8(E1» ŒTqx* ƒAH€»e.ggr~LJŠ•ÖòYÊ,}ºƒ“r ”²ZÍÅ‹úî[&;V3ƒîî3?©ýd+qןJIe˜ ð‹ž_aA›¤$0ƒî?º¡ýI1þÑCµ#T”®–f!TÕ(±4&Öy{ +V Ü4ÈQ„?ñ¥>ÛBïà.`7µ„Ö‰qÙç¬'€F=< a:¸-@Hèô8å(™ž]­£òÇÓbE°Kàݼ ¯Z3 ±¬º#(\(° 18Ú¨‘y‘-öÎRJù^ˆîƒv@¸ +'ÐT ç@GbN°ð‡UHeLØŠ‰pä­„¶…è Óo·RÊV„NÑ™­8¿¤==>ų ¡b×”üh=ˆc”N +é®Ý R[I Wªbf¶þ¸I®g&èFÐù„Ráì6@úFL¹‚‰yXXàM\,[¥Põ„^O²™8“†ž QiD*ó•Ý!Ÿ2p ³K¨uð,)¥ŠˆÅ¤+µö÷¯!ç~°ù\ŒŒ˜)Å¥Ó='ÓæxÛ +gºë„FhU§qŽ½I»×š\½þþ_U§V£<¥€ÖõN¡o *iLä⡘[틸ÞÓEÉ™¡o‚Æàóð¹ ÒA;©¥E2WÂ4 ¡ƒÖµ¢¬ªeD­ r©Þ9~÷ÝßEXûŽC¸ŒY™cRɇj«Qñã™ÒŠVò6™®I°™e%¹Üè[ï#7=yõÛŸ½á\Œm’-$¹"Ð_0e™¥Ùîå·µÙãO·°»>t3J'`nж1 œ`B(C}&Ä"è:ÐQ< Nm#BC%$(WJõü”2ë눌 8y7Äø€AH‡Së¬Z—ì¼òvq +ì¹Âi "r‚tÀ²1fÇ®í‚0†6ߊ ¾„Hª¾¸>Ü"0šd7d§XäE¢VüÄfL„ˆÂ”ãÇŒ-D¹LQrMqÆ€Õ~/´âGC¨š’Ú„Ú”†×¬‡™0¢úÁºɵ0“`@æ>Ö¶šªÐ’w‚ÔZ øWÄ"Nœ+$®G½Þ^NHï"\–7Û™ú—Ÿ¡j-ÎåâÞšQRÈCQÑf[ÈŽh ÌZ7)–´ÒÂhìãZ Wk”Ù »º^œVk#em"& 'Øy`[ Í-LI)ÚlÂ:óùY\®¤ôF˜q6 Ô¥ŠR\dìÁC±ºŸÔ›àÎõÌ8¦nbjJo‚POiÚ™'”¶V>ÒJ{ ®¸c3Ò„u ¦œîÏèK©÷ÂÄgd5L 0ÏÊÝ:¶ks·¹ô%e ΄®Áï…°­¤K!¹sÆ‘f/ÂdÁ‰ƒýñ¡Rüã%TÚgÔ·ªÇÎàj#å).h(8ìzˆXSëÁ¦»RqFY½0íFI3p`W$@HôuˆÊE˜©´ò¢Ô×cBSª AA¦í†é*€{Ih(Ú¼ëÇÖ#ìZÄ3ÎaR&Ç[ƒjøãŠlôî ãë!Öût2†ñ^ÞŒKÞ™UˆÈˆÓRiƒV ãŽ?©ùbPZù0 \îØñ®q'¬Õ‰Ò ]³¼ÓXÊ1¡Ž¨]³v‘>3Ú÷©L¨5€Ž0mC ƒŒü¨òQ®¬½™1ÌN'DY´QçÌ›™3µr˜TjkI%e´q£ x}-TεεX=†Š²D®£D]ô +JkçzOÄÂ"5Â`¢Q–涤¼SÜ\…2G”9L~ !âV ÔK|ª‹vAOÂʤԆw‰UÖ½œòn-Än&ÔKJM\ëEÙJ„­¤Ô.cô¢”<kRl/ØmLiF™.×åìv¤~„E¨L7ƒq)Œ´ÖKð•[ŒÎºW02@*VQ©A]5·-;#_\Œb¶?.ÿåZüÏîïø0À(`ç;>Ènõã%Ë$“߈+ð ùŽŸoâC b…R¶UšsVº£]@TO  7"ó¹‰$ ¥,?ªC +¶â’wÎA¨BtÁ—äKh!ÔÆåZÓgÈÀÓZWÍ/’| &Àh §±—sÞ‰wxcz®”Oöc>¿„ÔC§[Õ=¦„ꪔÖœ¡àÎ)½"3¡0J3S?ôƒxÆ +²rq·8}i4Îr ÜŸTÚ£‰3C„­5ˆ•“¤Ö‚daJC*í«õS(Å”TäÜ>æ]_›E¹¼3â\"cõž ‰ ÷â\)LBàÝH€;'ÉÇzÀSÁ±½SvÆ8Î×"Tf3Ƈ’o´19ø 63¤¯ýøGãœ[*¥ý°‡œFÜ»˜G¥:xCp¦>ÔZË(X×”v/€ñ÷áÖlR…¥‹zçsJÀ•~Ôº³…Cƒ+aHe¸Ë¦gzùT)ì‘Z‹5ºˆ_‰PàO·0=%W0±@ëuÉERF’4¥º$îma[‘Ѽû(ÀjAiØFÄFœÉmzg€@ó¸XSyŒ­@9ýÙJøÏï†6#,ÆO6°¿XM®G@ãyW0A‰m%õ8[\‹IŸùiT¬'¸R‚«¡BÃ(‚ììAÅj™Q‚Î~æÃïlaP¨á”0…”ÐÀØ` +`Æ»*ˆèjáZo#.ý„F†2ÛJ‚Y~4L½M¥™|œ-¬Åe` Ë~X„ºâˆÎšC¹¸ôãðb×Z„ÖŽ1OŠ÷¢ìZRÃkT÷H£­8—2C…Á<"H\FïHé1Ÿ®Çéc¡J‰´Û)Ë»TÄÚ}97ãrÛqÑ»¤’äóAÒØHŠ›I $Vœ+’öµq¶2=éí½ô‘Öª{g¸‚èŒR¨)wócDÙú¾(M£J—KSr-¥À<{Q®HÈõ³¯·ïˆ0™µ¨´'náB†SÜNñ¹XJ§Ôê&¦|&V"©wÔâઘ_D¨4­A‰ÖW£ H 0•O +PÚP9qï¼’¤]pý«QÁ‡é0gðøœÞaõ&4B¸ :Üå"¸íôKªAÔ@¼;ôµ8ð»‘âr(—ƒ­ÆDð°,k Xmq¦n ¶P³cVƒÌfLö81’Ïk“ïÉæî…™;à#ââZR +ž b•¤ÕßJY Œ§,Lʃ`Ûúx!ü©UÙ£ìögQ&Lg9gÀÙ]Àÿ ™v“Ù(œGh“”ò€ÂI5&=sÛùÁc¥q´Ž4µ8g^û^ˆþ,HÜ0Üÿ F;J#B p tB÷%5PkˆX6jûíëÒøì*%Áä@‡`<øè£Ô(¡ÈˆeÁj¯Â" Z„É…ïº0Ôà Ät±Ú:µ•T0¹–àóPù`£V‚P4Ø%@*X1ÅÚƒõ¸ÌDÓ üÖcŠÔ&ž†\xÖ,¡lDÅOבŸ¬'7#|”t1ˆNkÆ©tÓX½±cîú‘Í(ã@J"jÑB ´RÝ…ºõÅx2‚YWbÍÉ@RcÔ:oÖ?²³gNá#V‚^ú¼›j"°!(🬆ïn P¢$ŸÅù\œ)DÁH2%13^‰ w#üVÊŽ°Þ©˜—{fBvãJ@%`+@üÿùJ¨6NXÑ”±æ} Å—€,Kwü)è>T*!|¬.t ˆÆ8_Q &+Ĥ=5¢Ö£LÖ[vÚƒ,Þ îqë•e9Bºkxj3an!V„†ŠuVÃüzˆ[ ²ëaΗW‚ì½ã‹«QÜæÌ6T,Hî¸w7|×—Úˆ€Fa Kmn%å•0ãOJ¾( BýîfTxy¨ÕH*íÖŽq9÷«þOWBA˜^„ÛŒJ›QÚÙ‡è ®À£}ºZQ` üÞtåË€íŒ=©0~'Äo¢Þ½U¾Þa5ĽջWñ.H¡& Îc)ãÎ&æÝZåM:Þý]TŽ²:lf@èM I¡ˆ©-&3bœAzÖé‰ÅŸŸø0@1€$)¥ïjjz½ïbá8NN‡@ÉóøW>3"4ïò%k4e·'åBf„imÊhSzSpúˆ˜·hTvi³6P›)c#š¹ÐÞ~ÉX­8m'ùì:ªz]ƒZ!*«MÎélai-?‘¹SVc +(y +¢Ô½QÐöj“q†ˆZýI¿H7A„2¡µp­ÈMLiŽ0 žùÌ[™<¯L^ð™¡÷— q`ÐP%F{|-&ǘ,â]`m€–N0ÐD%h« ª‚‹±eΞHÎl ºìôÀ‚‡ÂÐ`5­.eGBnòYXËE˜¶æ¢d6F—#t9){·š„H7‚‚)wýÔ] DöñJ\S„®A¹§7#˜¼åcd4ðjˆ=£ €Æ¸\ñEØ?»ãÿt5²bP”s}Bí† ÐuñµFò9Pw ³Aäƒ[ñ”UÊò®X…iÙ»žáÀïGIïþUÀ¥Aåáá'@‰?[‰ýdY‚GÍSüxƒV&T ¥Ô0> e …òüf‰ó>;5‡jy©gz}—°:TzÀf§ð¼^Û·ÚgéÞDëÄ¥jRªDÙ<8ϵɵ(_Léй¹(àaÃ&ªòËg°fŒÙ”³Cµ´ í!cµ úYÌNšÓg¥Á•”'Iz\yR®AÍóù9—›!ÀqDf gøÐvÓaÐ9r])ï;û)«¥øxApo-©A5‚Ëãr{~ÜYæMˆ1Öås<¶°4`cR¬’fË  “¹@1¸›0aÇ™,&ÕEwb{÷\e&ÃÛÐ)]Ln¥ÔvÎF…ØÏS©õãC½{  Ù¡ºp­™„ Sù omÄø”TNxeœñ•™ƒ¦²3P­!ÂAâOë1ïBöZ”‡ÒÂÅÊWhh¥î‹ñe!T—wiIj b œ8`/.Eqs=L«F7ì-‘…°s9Z.+nß—ˆÌ £ºðñ†RhÌ.„õ®:•„t¼•t›‡ FÓ:QY(¬…ÙÍà*uÇOúP=âÝ=›#õ–VÞ3k{Vms'1X +búfR 1ypµtfÌ禅ñ}½¹nH¯í²Ù %Qœ9Ýóâös³{ÁºcL© \ƪïÒÎP‚ÐAiÔ…ÜXÊOh§«V`åbnl¶¸ìÊ>Koæ—™î‰ZÝ%ì>®wôÒ{ÆüéCN9w(æFÅá%ëbb Ó;TfÆvÅÒ›ÛÆŒ.ðÛ­CT­Í«sIeÆI¥N;# +Õê +ùIB)%åRTÈcZ^&ä†Jyi4ŽÕêÔ*mô²j¶€(M@*nHì!"Wü„ç‹&îevᘴÕÃÕ:ðu„I‡Iƒ6š€çI© C Ðfר쨥mЄ¤Ñ¦Ó½¸\õ3¹¬)•]©´jõ 0@ 4¶Š[]Üî‹Å¹ZÝ!Ó=Âà­¤´rB, rÖP..èô¦‘Ò›I±*1ƸQÂNÒ +n‚vè5³ ¶—Ð[°ò)µ‰Hµ •ÁçRx *ÕX«¯ç·ÁÄÝ óÐzPrQ* n‘³ºzyžîìõ…Z×wëµ=pôQ®DÙ:=Á ð8ÝtibW¶SF3"øÜêg=©CZ‚—¡F)S-’B–Ò+œÓ¥¬>ø8èh©°/dçð iT7’óñ†|ÊKÐÔêr…™Vß·ÛGBq"ŒÓ’KãÂôA~þH*ÏÅüHÍÚ{ÏÅÂSkPBv3T*€#¾0æÝa¡V_>JKÈ5¨F³quåtOÒ½s:»Íd¶ÙôW +˜\Hˆ%.32ê»zmG./jÍ §0§•îl +¥]­un´ÏìÎ}µv gGí½7Fë4Èå)»oµÎ­ö}µ~¬T÷ «*埃vˆ €YÆläúg¥éC·w–ëßLŽ%ÈçŽ!X“à8RJ5D:ž¢K™q¡ÚЕʤÞäÒC«~$fL¦…jÔv§“ó¬;â³#h¹² •\¤;'zc7•î¬§¬_bmxýT®ìÂÏ„X„WšõýæÎ wxŸ/oÛÓtÂ9•ÊËìð~º½ÕŦùÁayö Ý>H¥”ÚðÎM0ËJ~ŒÉE +Š 6Ê^*,øÜ p>]­,½_1êPŸ ©”ÒšjiY›_Wæ×TfPÝ ݇Q&+eµ4ÏÏ”¶ŸtŽ^/®Vð^6dÜaö‚PÍŒÌÊNcv]Iå 0 :ÓÆø¥ós*;ËN®õêŽRž@ÔzëT®ˆå=±¸“mŽÏ¾Tj»>ÚÄë™Í±¸ðVÛy7-;}ÀƸ˜ S¡´òĬÏ3½£Âôaº{Êæ'¸ ²#¬ÔIãl\oƒ¾Šó&Ý÷d¤\ûVÔgº÷‹Ó'¹ÉUntX.ÎíA¥Ìíö•êR©î& º Yry¢lLméÝ2pmÔi³ƒûµå‹íG¿(m_‹å™Ó‡9U¶¯['Ÿ·O?/Ì®ôÚÒi.‡'/Fgo»O ³Åebi›/LäêR¯.·_Ꞥ”*Wåâ\*n ù¹Ó=kî½,/®íæAcñ0ÝÚòÀª5­²Ì îæO +Û×´;JH³ºƒê*zÅnîëõeaò°þaþôÇìôqqtïñ·zu7Æוê±V?ÖkÄ«öÑv÷Óš›I0“”KJaäÎÌö±Ó¿h}^^>‡Úδ÷;ÛW4Èrïˆ +)µ%d·ÍöýìôYm÷ZÝ+ /H« †Ž²Ú¹Ñusïó½g¿»x÷/w¯¾Þ{òÝ9P«ÛÅÙUaþÐê•fFç_o_ÿh÷NÈt›0ë¬Ó2»Fã ¾ó¢µÿÊéì?ûeëð™ë“NǨ-œö~qþØ\9“'rëH©ïÁ_Q%$5x/Kuÿ]åð}ùà]eÿ;¸¼s{Zu’_šíS£ubwÎÊÓ«ÎñÛâì”]„Ï`zšH)ÍÁíÎIyûy}÷ “åÐÖ+TÉ2éšRšäF—Õg•å³öÞëñé—Lº GJ À[­º—nŸAyÔ/{ïr£'lf lÀ@`y§s’›<0ÛGZ}×hÎH•m"Ó+Ѐ;zë(?y+Ü>z'¦¸œ§õJŠË‚€¡­AºyÒ?}ß<|Yß}Ñ>ù²<tôô{p*íµau÷ùìñ÷{Ïqðâ‹Ë/ÒÍ¥Vž•¦WLfòÏ»\^?œ}Ó>xSY\Ï?@ãÇù,—íŠÚíãÚâÙàü‹þùûÎÁËów¿é¿‚EN5>;€ªËŒ®Go‹ÛO³ýûíÃwˆQC…´Û\ˆn7’ùL»¶¸ž?ýåüú‡³7¿ùþïþQÛ×j'ÃËŸgÏhgºsµ~Èææ˜ÞsYÊnÇØ P¡ÓÞqû'ùÉãòö3^ÜêÔ&—ø¯ÿ½Ý=óáº][ŽN¾L·/Í΃âò3xÌ~|ýs"Ý SŽâ)«³úÎëÆÞç½Óͽ—¯ó𛿕ÊÛ…áåäò§Û¿›?úiÿôÝìñË¿ž}õóñ¿TOèt+×=¨/ŸC ÝãÏG_Ï®Þ;zµ{õÅÑÛßHùÁèäåòé•ÝWzç´wþaùò÷µã¯­Æv~¸¯VÇ)½"•FçÔ<ÊŒÔ÷^4^§»ûÝ£'ÅÅC½¾cµØÜÀnïgºGNçx|ÿgåÙ3Ò®‹ÅAnp"‡Ji +8 ° !çÆZ{ïÚoöÂm/µÊ<Ý=.Í&WåÓþáóó7¿Ú}ü ¥\CâzGo§º‡o»¯Jógjeo~òÅüÁOQ¥% +%Ñ:x[ßy^œ?™>ønçá¯ö÷ý£7ˆÞ4Û'vïÊzÁè-O•)—nöN¾4{ÅñEyöP*ΊãËÚÎufxNçæVç¨}ð´2»” ƒÊò±ÝÚ)O§oañÝþQkïú«ßþ±4¹„u˜ÞÿjñôÇîù‡êÎóüèB.퀾mo?;ýWÕé&;VuÒÙ…X¾š?úzpùÍüú—`ÖÒ­sð°[¨&"Ý8zð‹Áùw…ù‹ÂòeaúÈí7f:;W Á y0S +MDÎYnx)W–ZaÐÛ½.Ï1µ©5Ýéµ=ð˜7;8 qN„¶(³F›rÑ>Ø„¹5ß×>ºÌ´Žaq2½Úâ¢<¿Êïª,ŽžýÍÿwXT-f;û³¿„ÇôÁPxî䲿óø׿ÿ·¿ø×ÿã´'_.žþjøà{èßéÃf~n·O^~ùûŸý‹ÿ>ÝÛó‘¦R‚Ž¸¨í<Ÿ<øúøõo»Ço—çoÿú_ýõ½§PÝýµåu}ïåáó_=ýöïϾü×¹á“íû—_I…±Zñ¸ØnŸ–f׃óoö_ÿõÙWß=ùòôÉ×»oÅü²–éŸ=³¹__¾úýÁ»^_¹ *Ûl¦«–gri+–2û˜Ñ+ õN>'M‚M« I¼¼2šûVkopðê›ßþqzþŽq{ݳ_VϦ~¢u‚2ä~¾{:ÐÊùîaiz¹ó仃?Ž.>ˆ•]Þw_}ù«ÿFÊu‹ƒÃáÉ»ÁéWjã@Ÿ<,N9Ýãlï$Ó9Œó ¹¨ï”gO(–¿}úÝß O>?{üõéÓŸ*Å!¼½¶| 5?}ðÍ£øðÏþX›>8¹zÿíïÿ-” h Òî€Yu]Y|Þ9ý¾{úM¦upöø‹gï,´ç´YÖj;ZëÌî=Èm?ù;¼Ji5»uŸ?ú×+ =Îð +Ö­2}ðù¯þáìÕI¹øɤÕÝ7¥å ³uZ]/.®ßýî»ßýÑ©íÊùIiñ$¿ý´´|5¾úä`n›çS¢kV™îik÷Eÿþ7µƒ/ÜñÖìLßZµIB4¤|»<»êì½>ï<ž¿µ 4£2j._6w^WæOŌ沽|týå_¾ú‘4jk €üôaïüëòîK»wn5öž¿ÿíÛ_ü­Ù˜o†^ÝÏôϬöAnr¿ö¾ºx¼ùþ¿ýŸÿÏÑé[£4>xüÝåWÿrþü×{ÏuÿýæO~­Wö^~þ›7ßþ°è øÐÆ΋êöus÷ùðâëöñçZmùäݯ}Åe{ÕíÇõý—ÙуÜèA{ÿÍÁËß.ÈtÏ—Þcƒõ¤JžTZ²ù“wX=¬ ö»‹+ʪµí48Üú®Z]想/@y‚¼ìž DÉ»£±dòSW +I5±´Sš\'¤BJ)zGvcgxúzzùe~öÍÏ{О=ùüÇcÖ§ÕÙýùßvߧZe¦WçT¦ÏdGÕùu¶{ŠkE­<´š»ièšÖ¡ÓÚ«N¯ÌÚr÷ìÍÞå{¹0ê<Ÿ]}=¹ø0¾ÿÅìþ»áÁu¶±üñŸýÃÿôïÿï£ç?Ä„mwÄܤ0~TÚ~]œ¿HwÏY«ýå÷ó·üwñ1Ÿéµß7¿l^þbüðWzž,/¿®m?ÓN”É‚C¬í’?}ñáÿp·ÛÙk-&…<›ªárcÐÅÓû¯~}|ý35?êí>ªÎ/·KZ]1·ítΪÛÏkó§¸VÉÕùÎe5 #À¡Fû\©ŠÙQ¶¹ÉÙ0¥ˆn£8:Ÿ½=zù«ÊîS:;ð“V€ÐI£`×¥ñ£òòeçøÍö£ïN>ÿ}º*f{-ÏêN€ûNÞþayýccïecyÝÝ™nï#j1.äÌæe7i»‘݇2Î\|1<{­7¶)³à¶¥ÉýÚî³ÎñûÆÁ[µyÄX­B{`¼xj·u gº¤\tjËîñ›æÁK§ òøNÈu ë½/•À¸m+•mҪDŽ.—®ßþÆî­ÆxR«Ë… ètNfDX ø|w¯³ó8.dš‹G{Ï~Ó=ý6Ó<=ÿ®uò^kîµ—×Ïø.7}üÍï—>J»Îðºqúóìða˜q»ÑØ~ +258ÿjò໣§¿9|òcsùª4}l†ßþºð4Êg@ŽÊŸ²Ú—éÞem÷Mºw*¸Í/¾kŒÏ«­”`U8½ ½~àt/jË—ùöáòòCLåG‘ƻݔV±Êóöâao÷ —é:P#%¡ïÀ;€ ,ÏçåþY¡¹k5>zSß{¥7Á«2™1"WÁÑhåÝåÃŒÆ."—„üÔhì÷ß÷ŽßÁcpô9¸ìêèüù×àÍ*L©¼ó*=¸‚÷õï—¿«L®X»ó滿YÞó iÝé<s`?+1ÑMˆ9`Ð3¼y±0 .ίk»¯ÔÚn”ÍâZMÉ r­EP’|F,L…Ò<ç‚GÛѪË0iJÖiL2½ÝLïä Ø"½±_š?¹/Çbq¢UvóȨgWP&(Îúìª}ðª}ð²uôF¨.0³f׶K£3LÍUú{—ŸÿõÙçÓÓˆrN0eÄ™Œ僸%:8zÛÚÛ9ý©3¾J÷ŽQ9kVçÍÅ3§}NgÆ#¼‰]ëí?í¾ŒËyT.dÛ§Pê„VJ3¦´‡.d‡FuaÕw1ßZ¾X>þ±4y retô:7¸HÊ%·<}üöWùö"ÄXîèAeçMÿü»ÙåÏî}.?'Ó³4yúöוÞ!¡Tj³‡Ë?íí¿.M$‚Î/v§ÇŸCGäÚ‡Ï~3<ÿ.ß¿´kûvã@ÌO!Ò*¨²|7D¨œÝÌõO3ãÑñ»Å£ŸÉùY $w®óÃïþM¦º@¥ï´ò\+ƒî}8<ý°¼úY’ÍÔú‡ËGߊÅ)"e0Ñ¡ +æ] +,RjVª(—Ù¿úÌ8*ºˆRòîzÄÎ/Ôʾ3~²…§äªU?Úˆ³>TŠqPž =¤ÜHwûg¿ÏMÏ⪋›Âl JÕjY€v=„ˆœÑ †ÚË®Ò-0˜=Òl&¸,g5Ô|WHWËÃ#«µ¯5öÜÑ% ‡Û>(uö/ž}¿óðK.S§íjyt>=}?»ø¦ºýÌjŸ¨µÒjÂzÖ'—,©0­n¿„ŒLÎ?œ¾úÝÑó_ËÅ^g|põö—ÕÙE\,x—Õ:çŽòý‹âè&à B¦ŸR 1J—³ð Û|öêìÍ?Ÿ_|WîŸ<úv|ú…Tœ€Õ˜ ˜Z±«‡Rfg¬kñéºQÂÌ­Þ}Ü=YÍ}Ú­8Áõ2ÀowïmŒC¶/ç‡0ÏirÑ(Lqf#œòP¢}Tß~ÚÞ}Qùm5)£ÑÝ}9>û +תLºi6ö @æi…¡˜i"¢9×?–ò#.ÓcÒ=>;q{÷Û;/'g”ü²´VÍ6÷’¼“’rÐõ™Î)¨J»u*»cB,ñ餌öNOM¬æqÿèýòáÏçW?Ô—ÏâB6ˆ+kÅY#%çÁΔ'WåÞ‘UÛÕ™Q9§çôŽ ÃSµ8åì.—îI™AŠs·w=ûðTέÐ.³j)]Y¸}B.‡1MÍtþ»ÿõÿÚ;{Hr„èº8ÂU¶ÿ°2yV™<ñ¾,‘òà¼ÌŽaa©RZYÈvÕÒ(S›<úª²<+6“©9Ý]«½”+‘LçÝbû(Ýô¸Œ´j°æˆ˜¥ô*à!€³\Á¢Ýñ³Æò¡ÙÞá +CÒiÒVÅ(k‹+½ +*cÀ5Íù¥Y›¢jÁOB~bŸ<É÷ÎÉÕkËêâÅìâ«ÁÉëÚü¨ÏP539xðàý/•bo5!pÙ™Z;²›ÇFÉ;ý›‹ Y+Ók¥ÔK2ªQè´öŸÙ{òýôòCïð…ÓÚ)´—Ïßÿxõþ7>B§ì®RÞ—íž)î‹)H¢^aÌ +ˆ*µ¾'V÷ÕúQeñº±ûŽJw´nW‡ Ÿ¤l' Ý*å%w g’ÓÃ…B BEÿ—¤÷ðrÛ:×~ÿ‰ïœ«Ž¦²w Ñ€@A`ï½ ‡Nï3ÒhF#4êŲä"Ë5®±ã¸ÛqšOœ8±c'îIÇ©'çœï»çܵîºwSw-®Y#ŠC‚ïû>ÏØûÝ°÷£¾$£4ˆ` ¸ *’Rä# T5Ä„J3Ru‰ËN9ù€&3&²r9Õ<êdcÃFlÄÌFΫŒÊÔÖÉ^ Œ‰€U§•¼'V—&Ž_z¨µ~F®vá@ÌLÒµtwÉW™Ôbàõb,ßñÇ«£&a#N6ªqqÀø&;kccd0.WºlºÆ¦Ê.)aðˆ° pé\nnInÏÚŠƒÐJÃÍÅR™µ ¬»Lg›Æ ŠÅ&Qvò2)'½ñ"&¥üéZez¥¹ºÅdªfO‰¶¸D×A'4îàˆåð˜M1r~FÌ““2ã"°gReÙ›êaáºK]¶³²­¡¸óêÜ<LÇ[0›3S£V›“Å=œ‰Ú‰ ÞÉ©aÖDiÀÚ™)@ +@8ì”4·uibcMUlˆ +á¼À%'Þøaµãˆ5V·ä •Q³ûΊ!Ö<‘¶˜›ã“d0?·u¹³¶©õ\dØNI?¯¡rA‹s._8R]°Œ +yÒ&\‚Ø|fâteö"kŽ‚Ø@8ŒOˆÙ@·T¤Be\yk-ÅÆ—ødUÊM„ +óáÒr 55¢Ç¿qP{×a~¥¡·Qõc£År²º¢—P_~ÄèÔºln³w©íûÔ¶C:'â/É¥cRv çrZ;ç Wh1/6»‹›³ë»ÇÎ^¿t뉛O~ï‰WÞ|ëg¿üø³?þõÿùùWÿøÑ»Ÿ¹ç‰ÜäQB*ö—¨›=­ÄÒãéÂx¶Ú+´k½Å©Õ­Ó—®<øÄٞع÷‘Õ ÷¿rÿ™ûž8Ïc/½ööso¾Ý[;9³¾¯MbÊA‡¼‘­¢þJ¦jÖÌòÔʱµÝ³§®ßwõá'îöå훽üàÙž~ô…×_þñ;o¾óá«?}ÿÖ³¯w×OS¡´•ô0 »b´J×é`$”)Vz‹µæòfar¶¾°ÖZÙžÛ>ßcÏýêÃO>ùÝ_üñ/N\¹Ué­Ké–b Þì`2,§;ÉêL‡Ç,nÒ/÷Ö²¥hmZ*u”úôìÖ•“w?|éÖ“Ï¿þ“ ·ž<}ïwön=íUôHÀäòël`!h±äÌ,ÀYHVå|#YiåÚó­ù+»×o<úü}?ûÃw~ýÁ§xå'¿<~éþÖ܉Dy÷g€]4ºx-Ä ¾8Ș¿ÖÁB”"ez©öz¨Ð‘ +ãkg®=øÝWî{úûgï{øÒƒÏìÝóÈîÝ·/?ôÌ+o¿ÿÈs¯œ¿q{õìM\Ìœ¬ò L˜‰ÔÉIO°,g&øH1”jåZ‹³›çšS³k'Ï;wýÔ•û¾÷Ú~ÿõ?þðç|õç¿ôù—<ùb¼8yX‹ѹT"˜V +«j+;fÄ-.– +dP.cÁCVLp1”F2Í…£{[ï[Ý»gïÆ#÷>þüò‰Ëå™ÝÌÄ&)d4è·YH¯\iLÏ,?vâÜÞ…+=öä«oüèþøì÷üõÇŸ}üéïþãŸÿñù—_¿ö£Ÿ?üÌË3›çã&"bC¥`¤Ê‹ÉH,S®w–Ž9~úÊî¥{îä©WøóWú«§_ûñ#Ï¿òæÛ¿|ï“/_xógoÿÛ¯þñïÿùÒß»õÌ+gn3¯Ì'7âõNÉÅr•îìüÎÞÞûxü™çžyéÕ¾óË÷§Ÿ|ðÉ+ÿöÞÛïòõ_þñÿ?ÿïŸAùõOßû핇Ÿ-ô6É(˜N’FÒ¹ÊøÔò:x,Ý>såæCOïù×Þ|üÅ×~áõçÞøÑ/>øø·Ÿ|öÕW_ýŸÿë¿?þÝW<ûÊÖùû¢ÅIO ˆ3!ÞVWmnÙŽøüRªX›\<ºsîîûïyäé{¾óüÝ=÷Òñó>}÷7ŸüåïÿË?þë7Ÿõùþôâ›ïÌm^æJ¼Òv ºãO4Š­¹ÉÅc+Û{Wî»}ßãß}ô{¯ï·ñág|ö‡÷>ùýo¿øòÏýÛýïÿÙúó_ñÜ+?‹”fTvÏ°•Sm÷šPÁÉ&¹+´g×O\zà‘Ë=ö_ûÅo?{ÿÓß½ùó÷_þÉ;ÿîËOÿðÇ÷>úôOþëÿüÏÿ|þû/Ÿyå­­s÷„óã(ÒÙµ· <À’teºÙ[[=~îÂ=ßóðS/½õÓ_þö³ŸþêÃ7ÞùÕïþô׿ÿÇ}úû/?ùüóÿÿøÕÇŸ_}ð‰bw-V[a”¦¾ßŸ Ña!9EPÊéÚl©½Pž˜[ÜÞ»|ëÑï¾ôÆû}öÙ—_¿þo¿üõ'¿û¯ÿóß_ÿýŸÏ¾ö£{~bïê->Ú eœ­™ÙÅcÇ·Ïœ½xñÊõk·zð­¾õÇ?þñë¿þõ“Ï¿øà7¼ðêK{—.O-­)…f Õ¶»Ãfy$Ò+D9—/¯¬o]ºûÖõ[Þ¸ýØ“ß}áçï}ø³_}øêú“Ÿ½ûõ_ÿ|ï½úƒ^{ëäå&–vsíeo0+ÅkñrGÉ7ÂéòÄÔÊÜòúòÚêå‹ðƒþàÇ?ùÉÏÞùøó/¾úÛ?Áéxÿ£Ï¿øÝïÿñÏÿ⫯Þúù/®Üº5±´)¤´”¥Ä¤×Î3+GO^X?qz}óÄ¥k×_yã­ß|ôñGŸÿá?}ëg¿øð“~ÿå—Ÿ}ñù_|úñ§Ÿ¼ôÆN_¾?ÓšwR²“ +û"µ`vaâÊé|±¾´ºúÀ­‡ÞÇñó÷žý­_~ðÛ¯ÿú÷¿ÿó?óéÇ_ýÕ¯?úè…—_~îåW—v.±Ñ +äQ@±òÊå@²)&kõîB{ja}{÷¡ÇŸzö…—Ÿ{åï¿þƒ_~ð›üçÿþý×{ÿ£Oö‹Ÿýùë¯?ÿò«—Þzûþ‡ŸÊ·V,„¤¶Ò3e€8+&‚‘©Í%‹í¥­û{ê©—^}æµ¾ûáÇ_ÿíï_ÿýßßýð£?ÿüë¿üåË?}õþo>üô³OÞÿà×·b÷Òp®Ab:W[0VÎû•B(VéÍo\»÷‘×Þú·w?øèíwõÅ—_~ý׿}úû?þæ³ßÿíïïóÎ/ñîû¿zÿÃ|üé³7º›WH©xD‡a¿Éáµ#ŒÎŒº¾ÒžÛܽpíþ‡Ÿí‡/¼ñÖ‹¯¿ùî{¿þçþçï¾úó»üö×üæ£O>y楗Ož»<³´ˆ7“õu¿/VÚ¹r#™É7&F-ŽOO.¬Ì_¸²wãÖõóWΞÜ;•,¤YÑç"i‹‹²¡¬Ú„©M¨Ö‚¹0N +ÆÓ©R¥:^o/®m,¬¯ugºÇOn\»çÜÍû®¹tùÌÕG÷.V§fùHŠæüÉ.*™ ÚÓθpíõ¯ožŸ];M¦J¹ôêÊü×nÞýÀ#÷ÿä'?þà£ß}øɧŸ}ñéS/<¿uöL}jÊŽ³áœ  +æ³<å ¯;»yæz¶9•®4Ûí‰ù¹é'Ž=ýô£oÿìí?ýü‹?üþ퟿ýú¯¾üÒ O=qûþ{¯®®+6§y h°¦C&,0d\ƒª,„Ñîf!™ˆ­,.Ý{íê«/¿üê[o½úê÷Þ{ï¿üåOÏ}ÿ»çöN¬¯.”ÍD©æ ¼j ‚xCàaw 6·à¢Œbƒ‘d¾Üš]Ú;µsûáŸ~æɼùêo>úð˯¾üëßþòÁ¯öä“ž¹°›-ì¨ÇŽ‹À «-ÔþAÃ4¤qÂX@’ !93ÞîÍ/lìœ:uîÂùKW®\½öâ Ïýä퟾üê+ß{þ¹gžþÎ+/¿xß=×7nÆÒ§GÔ»hÂBdã¢J®ËËYõ†"ÉÅ•Í‹×îê™ï=üèw.^¾þè£Oüü¿úñO|ß=àæc·yûáÛÏ?÷Ü;ï~ðô³ß¿téî¹ÅÕt±Š{g .^c&5FØ ‘6 ã 'çÂÙV©5;>³æbyJXqÁåÑA80-imäþJç´#<ì­.ÖEP<€àÀÇòÁhÎ/§¬j†]'†y|(pKY6c:;QaµaÕF0Ÿ¢ì(KK)à½qV`yÁ… EFãÑT>Ûìt—·v'—Vƒñ„Ãͨ,è°Ù¥²z§Oq*+¥±ST áSŠ†H?ÁJbHñ AQ +E±H2•(Vk½ù€ +Ñj½‘ËfyÖïpy¬‘ÆÅùä„Ë—üæ Që`8©äá3!VÌ•ÓÉt$$É!ina¹V+d2±Z«!'Šà q.Œ°I;T±Q½sDëR[ÕŽ1‹ÇŒ‚÷L2B>]è)ñb*‘»péêÑc›Ùt¼Q­æ ʼnñV§UM¤Š?8¤0ªG ›Á/K%>J…Y1+ÈéH<N(.hu¸hF£E¿˜H¤Ëõæt³5åeÙH4ÉîÓZ©Ã¼ßŠÁ%ÐÁ*¢,P¯Õéñ°J0^IMû3ÙæÆøÒ¥Úô®7³ÚmB@ˆF“„PJgCAQR)ÛÙe”ΨÑý¿¾=bv 01@^p– Ò *J4VbÑDeý8‚“n/Hù¤\ \b }ñHŒS¡ê·ö j¨í „Zœ^w²‰bo~û†õ¼Å|)"F¢Ø5"þjÇêß?¬ß7d8¬q ë;êǼ1_¸F y7›pz£ýž$VbÔ‚šÝ’…ˆ˜P gSB2À‡[ïbí(ἦ´v +æh0GÉ%¯”3„ &€ƒ²ã¬Ë¤‚‹[P9hÈ;¢G¾9¨;¨²¯kÁÄþ‚*èddƒVÛH“‹3¡:;®±á°;èó“Q6ûÓvB`ŸÛ/Õ [ÌN?ÊÄdáó\bÚÉfõ˜ÑÉšœÌ°VYuzH­µªuV+DòRŽdCN‚CÈâG-ä¨0cÎÃjðADFL07¦G´6Êƪì>ÒÀ~+!9=áXabaý´Ë©$`: ´Œx‡ó Ö ¹•A5ü¯ûGTصKm¡ .¿Àñ€È÷'ÚV\ÒBl¸..pÏCE0C³ª·×Q*t`ÐÞÊÎæà@\ª:¼Qâ7`¨™"댳‰)©´©¬e&Ï°ñÉaêĘx¶mEøoÒ ÷;Zø9¹Q:mïÀÞÔÁaÛ°ÊÏtíDh߈mØHhl ÀJŒT çç¥Ô”ÎD›!Ôo‚X•ÉmvòTrô§ºõ›Lr±Îá1øÚzXcÐZµ6 „™ÈŠ¹)—7!';Jaš²Ÿµ¹Ãv*êäÒý–¡dÄ€ŠßÐì6öCÈDºÜa—&üY§'š,.$jUŽÃ*«ÖA¹|iÒ¦äÈZÐÉ„sµy„OhG´öalFù`~‘KMÑ‘:ÌP²Â…s§„œÊæ1 ! uKÍHyËæŽÑ9GN4-¨_e%‡ˆ:3!‘b+w–T ”ÙÝAxàü‚ÓC>§'îSN_ì°Ænt¼r“‰Ô]lʈGœ\^ ù ¸ÕVâ[º}G c¨Ej#ât‡\Þ˜ õ«sø¾=hТgÀŒ…Hüt1) !«Ì”õ÷;ééÝÃýÆÂQ>1.åºV·8 †ÔBÊcZäÀ€áÀˆý6f—(D{VTÔ¹ÀcÄ€¶‚`0b¢Öpxâ\¼çKÍ\ÊD¸hæ’z˜×Üö#jÆC™ò +ê‰ Ž] ’‘´p@ ñ0—õF»T¸eÂ$ˆ +HIµOˆ¹EÈ›a£ø5Äø3•ÎI&í;b„¼q„M“|Æ'•À3Ã:ô®CzmO€éè Ñíô&¬”ââ2ÞH[.®‘BuT CøȘÍâä&áîO7mz•(ÌXÀˆ¹±ÃzxÔê¹Ó´L+@j2ÛÙ +gP>A‹X ÏDÇ½Ñ §¿` "Z$4 Ãu(H±þÿQ§‹ŠAtÌJÈ”P åúA5<¨sšPÞÁÄ<ñ 8ƒ9ÆˉJwéª ‚„BüvR²“ý ªµ¯¦7Î4fë êáùò—s‹U*Ôÿt³1¤ ïàˆeĈYñ>O¹CufŽ‰ö`:Éö¸hm@÷%fìd"äPt±i3DÉPº<ïâ’#frPëçׄ… ¨¬wùd׈öXÌï¤%…ÐÙÜ ©‡ÀI·P ê¢l¦ßæÁ8Ûɨ•iá1›ÇêM³Ñ‰pq äÔwÚŒ˜0ü/ä‰"lBeí/ºqû‹(“úÖõþø¦L¤ãªf4¤±r#:ÐÁÅÁñ8R;Ó§rx¨h§“6*as+õ™S©æ’• +!\ +ãrˆ'z“ˆ7eD‚à¯Ƭfd¢gÌêí7Ø!ãÁÂZkõxs[u0K2 ŒÙ|vw‚ VÙHÝ©xƒ%Z,ô¢µ³\5‚À6ŒÚ‡ÕÎCà§Õ˜ WÀja?ÈŽY)µÖ9X–ß0Q¹,˜dÅ¥Ù7zdÌ +áàv†MÞ!3wÄè¹k Þ§B€ºÍ­^¨/Ÿ;hÂGŒK¬ÑÉÙ@yC®e3S„X¨µVW÷´0ÑQÈ; +q&wÌN§±É$Æœ¨ fWàˆJåÒ—vö{¼ô[^“Á"ä ºøìOÛØ,’‘ +Ä%@Þï +…2Tsxºf*„q ¯TôÈE»G6¹ƒF$àô(ƒýþ-–ýc6P¾€Xu¦ÅÂô/_M%ZmƇ¨ Y˜I¹År¼±Y]ºˆú@öù +íõDý¨˜(0æ£&•‹ + ëàC£6àX¤ü‚˜_°zc¿ + br]imfÎRYù¾Æõ¥µvZyTÆ~ïAVF‡Æ¬cf·Öá +DЊ +:;3b GÌ4(}‹=Bö‡´GúÅD`Bã.OæÎúnyÿ(N„ÖFí;¢3a¦þEÑ€ñPX9ÓabÕ0P„de–Ëθ‚%›`µéÕË+·x;öDêä´˜œeçèX÷ Ñy¢Î6%–˜ÜV¬¿¾Ò¨Ì^H4ŽIÉúò‰ë ¨\BV,Άëk‘ÆFtüxtb‡ŒN@l<¯ßûÐ󕹓 %͈ψÌ„ŒJBf”ZkÂÄ‘~_Ù“IÃLÖîͺ|EVé°ñ “PÛaAwVô˜Ü2äI@ž8Æç¨`)VY"…,HO_|ÂBÊî;S¬‰pÝʦt˜¤GP‚F-ýùðz—ˆù |¢ƒ³I7—dÃy3æï7òD@\il^PŠaoÜBHoÌÅg0!o"¤1=j&ŽéÔ£(›Ä|i&ÖJMïY}™#fp,¨huÁGè^ëò#l +e’@¦õè7-ûUð°‰2!A„+RÁº7Ò"%¾c ä´bDÁו«:C2Êî…Û”þÖ€áˆÆiB ÇÀLÂtøˆÖ©µ{ > ²Æ鉀ƒ æ¦CÕe2>N%&!à„£M9¢AÆŒ” ;=)R¨ÓÁqƒK1{a2ÂóeGõ蘉8<õ¹‚.&ïKLÊùy`GL8@•~/VD5RýµÉ˜LJU!݇Aû“Œ1A£L ª®;l#B¨?cgbI%ˆOH™ËNÓýé³¹înnê¬XZÀù¤œéú3ØqA+!hœÜaŽ0ñL}óGí6<¢â±«­2“ÑÂÔÚé\ÁÊÅÛéÞÉÌÔ)¥u4T[G„‚˯´–^|íçË{÷럒B’êRnE©lâb•‘k‰ú†Ù2'M„ƽ‰¹`éxª{žÏ-˜HÙF|´nÕöØi‡'JIe:RCü96ÞIWw/?^›?¯…ûùì “žu+&1ãO-g¢Gà•&B¶‘a"P0»$ͧ¶y@–Ó]Z̃Ø5S.¤Xw¨æ‰4 ¹²b£#:Ø  œ ¹žpŒ’ÜÚvJUwfvû>„™p–6¤Ì¬»ß±9 êŒÝ£ð©®PœW»ü:LHÏezg³Ó{¢Ë_0{zBv‹Åhy‘ŠÔœþ È"°¯uo¼‰ L²)äÚ™öŠï9H!7¾^Y¾’˜<ƒ…š„Ü"ä¦ +öWÆ7N]{:œï ›IBj:˜2ì-…²Ë‘Êv0;\Š7RÓÚh 8cVàsgêóÖÎ>o‡7¹t¦>½3b"Ü´R_Žu¶ãÝ“å…+í£+õP¸\L„ ì,0f~ˆM¾¼€‹VÙh`ö€ŸAƒF4dè·èŒÙ˜4jǸŠ•ÍXÉ 7RvK%„Ï¢|Ö-½Ñf¤ºÂÆZ6\µÅ±c'‘|3 5T4`@’`Ô§ÞAAžPy_j*C„²Ë_ôD»‰ñ“áò ÙƒcŠƒxr@Še ¬rå t¨J¨ – ý®_~ n0›ÆAaµUVú_öŽê\C'`[…ÒÁ<îÏ£\ç .O\cÍ©Ì0Àë"ý#/Ëå¥Py©>uêôÝÏúâ-4UÇ„ü"­Œ:'˽ÝH}cÌåÔ#›”PZ(D臓Ñí f•òä‚ï ”ˆ‰·øt/XYf’“¬\®w7OÞx^‡úI©*¯Ä›GÕåXëX´yÔŸ5ATð'Z#úÌn`€Ýb%˜_jmÜÈNŸé·Àµ’05àÁQ3h&¬TÄn…ª›ã«7„TÇŸšWæíLøˆÑe§e_²Ý\=òæ³ë/̞ƥB$Q™Û¼ê‰5,p}Áâ\¨¸@Èm¹´\žÚôÈy½KˆÔŽó©9—§C Àº~·:_¢º”nmŒZÈ}ÃV ä~À£Ôñ`±¿@²éàsõÏ'[…ÙSÕå³Ù©­xûX ?IŒ‹>úÜ7/ܲRâãÓóÑΙÈøIÒµxW,5—§7îþöÈH¤2!5§æ/d.óåU=åÂåÛÏüpfãâ00TˆàKõÝ釳“guÎÀ! ç¾(ˆC:dÈB©&ö§K:˜`°S.Zv±q­l²mo§§÷ÇîßzhˆŸý#pqb‹‹N¨íl¿Å%À—ˆ ÕÜô@ë 0ü*éO›Ñ€ÆÎ?¦Ad«§¿tÔ¼4&þG‹À„lEEÀ +6m'µh¥,lÆ!R1»ÕNœt»7‚ 9!;o®‚ßÆÆ·åÆf °”íœ5­ß×LB„@³f\йx-,¨lü¨™1¡à€ç“â3|¬j&üF̧qùœ|&V_/ÍŸ/-_Æ}ÉÒÄF¢¹jñÊH +æ¸ô¬Fkë™ÎI>= \ +!C…!Õ +Î¥»h¨n&c(J||¿Ê, +½paA*®Ëk|vfâ„Uj‹d¤l"‚”\ecÍLkeñäsµVÎ#BºÐX8}í‰Pn¸,¥vla÷±ÝÞ(­ßOŧmL|ØBal:R\ëÏA­nH¤ÖéøÏÆ»:'PíTÛ¼vRîÎLFìL’Š¶@ ×@ id[‹‹'®3Jb‹Ûù3„V¬¶4¿{;”ŸµSfw(=¹d”VZê~'dÆ‚ J¦[hoY´“T( BKÞÄ”7Õ“*ó¥¹ÓÇî~&?}ê Ž²Â%{´Ò†}ÀPGtäX?—‹d ó#•ƒ¥c½@~Ýë1±ÉQ à8¯‹™°€• M¿¾yí©É­å…‹\fNO„ôýV0;w¿..«i#£\lÂFÆ@ÈÉÅE=*í¶L¬Ð£éCC†oìÔ»]Á:_ÚËRi ¦ÓLfä¦ “š5Ø ~DGè ø+äYYšÜÞ[ ±ªþö!!ã=&R‘Š6 > qJ™ÀÃmL¬ÀT„`¢ˆ'lüBÅ#`\<úíªú Ç8`zÓå%9?¹oÔ4 wé\‚[ªÆ[›éÞ©H}É/ç·ön7Ï艀T˜k­ÝS]º–hŸ¬Ì](Í_¤¢ãGŒmáþŒô½4ƒù2žPjGÝ¡ÒAP„8D+À6»ëÅS(:Ø )÷WG"Bl²ƒñ)2ÌN¬ç§Of§v¢¥ñ¥sÕÙÓL¤Î'ÆÓ[™‰ÍüäqojÒ@ÅÇ` >16® Ä€%Ä2Ÿô*e¬I=ìsyã ¤âS$äæ +sçy_ æž»öèKÞpUí`í¤LG&­­¹[G¯>››= BWãd„̛걠âjñ«×F*@‰,q gq‡M¸ ºCÍ`qifçÞ³·_]¿ö½@yuÄÁãBÞæ‰ñ +bíTÌI§©` Ö@í³ƒ·õ¥ç¾¤ý‚ú[y¯Ò°{€›*Ó[ÅîQ)7ÅgçÐPË(ƒÁ$&s3—¬žô˜ÍÇ%ûWUÿw$8h¤©]<¯,™þ®ƒ£ß:0¦# ”½Éi<Ô°Êa­CÓÔbûë56Ntp`”Šêú ö•pu¥•hnÒA‡¯ –×ÙÌ"›+ë¨7À}»¼+Ìç@ÏhíŒÖÎa?ÀÄð¥wØöرòJ³ßq}Ì>dÀF^Q +*h¥ÎD«b²žmm'ü©Úü^mùB²sŒ‰×]¾˜Í«˜Ýýîd¬\ÉÞFr÷(ÓråD²}¦2·­ßü”—¤/Ö¢©rxu.Ÿ…ŠÓÊ›˜Ä‚€…ÎF®µæ¤Cî•gÛë×æÏ<¼´wûäÍï§&¶é`eçä½î}šr2dñ&P©,¬äºg WžÌ]£ÐAPC¼)69ÉÄWFAvI`¶‹è1æ¶XàBÃ5'›ØH ¹he¾¼rÑ(\b¢º‘mŸŽ–sgšý9ÀE ÄúB…s|Ittn#&ú<8‰øK ï€Õ‡ÈH`$TvXG17*-§wVÎÞžX¿ŠJU_t|ýÔM˜Ž¶Cù9P`åâ +l É1‹e*ôͼÚeD%¹¼’šÜÞõgíî ”×8<*»„ÖÌîcíÛ¥å»SSg`aß Êòî#Nꀶzõ8 Ià"•êq¥¶}Hí<8dÐZ gÿz 4jgƈp˜/±‰YàNÁ'ªŒˆ㇠ývV*êäóîp½¹|qùâðø”—@ Òáª7Öò&º¾Ü"WX…|˜V¥yÔ³`~7`@ž–ÊÀõ:0Áü€Ú¾oÈphÔ²Ä2¨ÇAÖ÷wtp&§è¶ÐÒÙH·óFba:Xš‹·s©IL(ˆÀ %ª0#:€·2)dÀËØHCHL™1Ù1˜7b'C* %h€à3›¹`ÅÄ/‡úÒG´.ìC@ñÌÆ2%7ÕqߨSe¦ +õÅfïh¿MœÕ£¢[®KÕµx{Tõƒ*§?RSríA ‡Å ³ÑF 5Aó€/´æþÕ#t y"X@|IªªnËëÙÞ9¹²Î&ÚÂè;×Þ>Ôº~B8¨µBqq±ÀÛûξ ƒýfõ<âI9ˆB*R¤2>wâbð²Ó;‹çžX¼ðTvþ‚'= >ˆ—2Ý™ÍöÜžG®ê\œfïqÄAHb´æ `¥ŒWÌ`€{¤RH0@˜Ô6zÌLVÙû­¶íŒ£…¶áú·ÏÌn‘ð'³íåúÜæÄÚ¹îñ»{ǮθY[8+76ìl,Užé_8åã„%ƒO¸…jžH×A%a¶bÀ$[-.ÁÖß:-awG0gvùÕÊíËÙب'˜µºEðY0 CÚ®qÑz¦±¼vê>Z.ƒíërÇ™J0=ÆÇ1_NcgÅX=UYP™ÉýÃV••1a2%Õ@Fàp¼R-Ll¨,䨩c%TXÊõv‹³§{ÇnJ…%û…ñ•Éµ‹*«×ì +Ø0Œ’ÆBZÎÅA6%jsn!"ÍÒÍå§Â5±²’ž»ÐÙ¸7˜îN-žzííßÝQ› Ô}©…XmíØÅ'×.R[ +”–°ð¸ƒI ‘ÊÌòÙ‰å»í´2hÂt°×Å$É`å3t°?ך2±ÂL…úÕ1 õWq¨g^åàÀTY©1³ä8PÞa#Ñ\|Ò*Ðár¦±tûé7ŸýÁ¯«ó§ÜR^ÌO ùÙüäN²±V8ÞžÙaå'˜pÕª2ʸ‹Í«mü¡1בþ:ZZ›wXï>8ê80lÖ¹Æ €ëÃ>¥Yêl/ž±SL´ÒQ&#Õpy%RYg”&«òÄz²ºhD:ˆ@õ¦¢àÀ<šë& +SGÆC |›´âÁƒÔy+&¾4kaŸ!€‚E!ÓóöçÔ±âæÓ¥özuò8p˜ #L"eƒƒ±¡~`}mX@L6QoxPe³cˆŽ¸ƒEO¬Å&;à} ·¨¤k›»7€]ÇźRÙŽV·øh[NO +ÉÌe´¸'M¦Å=0jUÛ|VwŠ +M¶–îk¬Ü¤äÆN Y>\48<ßÐ}kÌ>hft΋ÉR“&j¡‚:'»oÐGôÈË úH¹.–V¹4Pû+£[z*àM4âÀîöNMmÞ·~þÉ­«ÏÏžx ¤ ÚÅ¥¬DäÑ?4jÒÀzˆ1S.. ,ñˆÕ3jñ!¨’ r:7±ºzúþÊÂŽ‘H Žå•DkG*-÷çÙ&ÚàÓû |)!."@òüEàQS㹉:Òµ0\Âø × Ò9Ýþ¬JznFÊöH!=µz9×>fg"„R‹wNe§Ï/ÅE›íų©Ú2`v„M³ÓRzšïuD}ó€Voõ€ÐÚ‰ýÃú#¤ÝÏTÙ<:;Û¿:dóÖBBB¹4ð ñêŠO©XÒae¥‚Ê„ŒÚM0g‚8`<œdxH‡Œèœ3x¬¶@Gô(°´?AùZHb¾¤œ8qù‰ÙÍëÀ¶Áž$%V€¦GJ+›€ó†4/P=§7 é/¹ù¢?> {*Aù£¢Rç_é8™Ó;|ƒZdXŽ˜ ]¿TúTöQŒŠõ¢É…².1qL(‚ÔSêk¬R˜ÛºÚZ>Ÿj¬•&·ËçKsg‚ÅÃÆÚrrbñø2”ÔaŒ…ŠØúýÄòŒ2AÉãF,¤r°0`pBvøGþZlÐäœ%dæùÔ È-½RênÐá"Ÿî·sIuvk‹Wç¶om\{>ÕÙʶVVvoð™.ÖdÂ5ŒÏ@ý­Ø3(—väÚˆ3 ~ !{¤6™€owGÔ`Ì]¢Û—1T0™62 DÙÔŒÜ1áMLL#¾8+ £#ÍÖâ…jïDª<—¬LÁlÄ-$Ñš¾¿JÀà’´Þä ¢T4[Y,Ž=0l39î×öýI V±y•îf½·ê$€,>Vá”*ìïáÅ(U—/í¢£©b/]™S™Ñ1#jròNRBirúÆô˜Ú„ J)œjêm$ÁDJ½¹ë³'às NÉJÅ'—Î~ÿÍwcéŽÚBw‡±)Z¬ÑRˬú›z¸½ÁBiú”hjÌ8§1ª³6ï@>1íò„,ˆHp™!ú¯ ºFj«×ŠF;gG%—'&%Û{ןÐ8hˆQ0!KE›|~F®®(å™îìæG_.tŽζç§vÅü¬/9…ëÚ¾…ðŠréò}/»Ã•o>0¤8¬ÇÕV(ìn.V©M¯íÞðDÊ |!Á.åüé–\šÊõ6â­E's“±æ1:ÜðMö0!Ï*u*Xâtc™–“Ál`Ž‘Ì„d¥dÌÚÔ—£å¦…Š:˜ä°Û§‚UÏD*ÞhõçIffåB©³Á(•@n:ÑÞ.ÌìM¬][>õ@eñŒŸjΞxô¥w¼ñ¶‰ ãÁ)•±äâSwvåîº/Ó:Öß3åN[`ÃŽ Z‡a²D H©hL¸Ípâ²ÎLB˜Ê—ÑFŽè\(›’³€zœtp@‡ vNkó9½i&2NŠE!ZwA€<'|~°8][83³}}fûÞòÊE%·:–×`>ãâ³›BùÁ9(U{zŒÿö!µÖä¶ ’ÞÁkí=ä1ƒwÏÕgŽ_pz·$˜Äx°0žÞ ob¡2-f6v®÷Ö/˜1Ñîc|AHÏÔgNO,ž—²3àåR÷ÁÇ_ÌvNüëAýˆ Ó#¼Ù­ˆ©Éìø6Ÿ4¡ÂôÆU_b|ŸÊ1dv›Ü›'H 0Q0×KUg§–w›‹§M¤HÊù@¦Í'êT¸l./ž¼wíÒSH°./* —¿`q‡@}Ü#¡€H‹”Ò‚åª!œÇXçÎF«q#:b ô÷1çh¬j Ña_ª'–W£ÍÍXó¸•T”tçúí—ÏÜÍÆjBaŠÏõ¸Ä¨ÛV"hBxo°œŸ8‰û‹Cw|»ÞÎL80 +AT8êZçèé«aý‹c’Gi‘‘–Å“¢#ã‘êzgó>>ÙjÍlï\~<ßÙÔ;#È8<ÒߣŠŠ"Þþý>³“§w²@p6f@G¨!¿‰Œú’3ÉúÑPz¼6y¬»y÷âÙ‡Î>4}â&—œÐc"îO'ê«ùÉ.&ê•K0›Z Q•Å«6{u6Vg¡ˆ?‘ŸñÉÕoÔª Äl”Ñæ5:EµÇ|Ù©«@ýÿ×àŽ>Ùç“=‹;z× ]3·`!—/JG+Àº7Ö¯egιc]w¸*ägâãý–ÈÀïÝ™ 4*®€Ä·z#ö~ÿ^½5Ã>•É½¿¿}•mÄ„[ÝA Òb•“õ(eo¢ +ñ©8]]¾mnÄë«ÙÎVªµî €¶õ°ÜBöÆè_H3Åæâñ³÷sÑÆþ!LGÀ/”TêcÂÃ2119?…øS‡4é:D´Q +@~½ ö' íÙµ“¥Þš•”újkýžÙÛÓ›7k󻥙@¶J4|ö­Ó¾¬Å$;¥¤ÛÛã+×õDýï0Ðh(Ù Äܘ9 &jˆQ+o@'—妷.ÞÞ½þ„ƒ’ÆŒ­Þ8)WÓ­õl{'1qÚáÏ;(Y®ÌƒZmgbýåöLÒ„ÍD(ß;ÝßÎÏæ±Q'—…é˜ … ÝIK‘B×B„ô°Ÿ’+±æF~òÔäÆ•ÆÂÙþK1¹{þþçßx'ßZ³q¸¿DJn±j§b:gN2uy•!¦urd°Æ +—*±~[žû¢å…é…GŸ~µ³|f|ýrçÄÙ…K™ÙóãGï:vmæØÕxc5Yõ§¿¹òà "ÈJ%o¨av‰8t¼å UAÝ80ꢥ’ñ³6ë€öа€¤…Jz¢Z®EKó^¹Œ‰*:î7©è“šÖ¹|d(/§AFx”J´¹V˜?Û\¿Úظ^^¹ßö&»rº·qò¹­¨˜ÃÄR´?«dŠÍ¹C“ýíÝîÃjXÓ¿S@ 0µÝcÂ%›·ßyF.ÍJ¥©dcnzëîéíë«goµ×.ÆZ˜\³{ãøk°6BÔ9úóÍÜÁjvâDiz/TX@…¼b­@w0aÌFjoò9w¨Å%B…:\u0j‡×ÉDÔ¨ì`@&Úgµyð“‹ÕQ6’h,çºÇ3í )?©.E*‹ÞH-”¨/o_žX9e%%€oÑÚR¿•Ml—jýFM¸õ¯n_‹—fÕvH˜…ˆÙ¨4x€ÀV +S'.Þ®LX„ò_n6X]-ΜݼôÔÞƒ¯7×ï…¹l¡6·ºs7áO‚‹–W 3—«‹7ãg}‰.p@6,°¹{o²¶vH‹ …µzlt¼/¬¥¥ÄÄä‰KµÞ&x¥“IàRÙ-×”Ò<ŽÔÄv0Ó†0šïyCåA#5 AûTîË{”6Ÿ4"‚ÊJyärb¼¯é(—B|)LÌXI‘ —Õy”+Ïî.ž}$ÕÝ—çÉPÑA…Q6pfy÷þë¿1¾xÖɤ¸è$Î NÞè ú3ԮãÎ!åOLŽYÉ9 >¬‚G „ÎÊX°0(Û@Žà!N®‚w$˜ìn÷NÞ^ºðtqᢓ‰§[½£×7/=Ùœ=¹uù±ÆʹPu¶´x69³'T×™ÙÞÊ•Íó×öµñ©©xs#RYñ$flžÔ°ß?l>¤‚œt¢Bz'@!öFñ` •„´T˜š?~uóÊcb±m.ägNöÛU×RíÓåÙ+JmÝBJ6²[„çRÓtlŠMÍrËɤÍxPå lÂéÏa±9Ø -£øì$·€âIŒ Ù^aêtmñ…z¢‰â”oX0Ñû ps36ãr0ÓKÔŽ¶:=Q8t¸â —¥æʹºl§#ó‹'¯>ð )8=‘|÷t¦s:VÝà3jˆ6áÜÙ{ ß\;ÒßdV„Ù ëÈåÕtw7ÞÚÂÙØé‹·ŽŸ½ ¼“‰Ô–¯»òìÒ…g„òš™Ãdð±g_ŸY;«†ùÃjD Øø´˜_Áü¥-bAüʼn£±ÆúÑmFE;€F†*BvÖŸž¢„ôÎå‡ÏÝû]!ѶÕ¿ß‹UWz—·®>]šÙÃ… +êËzå¦æŽÙ¾yHs`È +Ü x+„JºkÐlvrátOca†µ(ÐMëo9 +‹8—…Ü!œOzcÍDëhzb«¬\,t·‹3§j »ë'¯_¸õ|®»Ág»«{îÜ|©wâ! —o¿ÙZ½›‰¶’…¹ù­{éöÄÐ -–ܾà\­và‚«U€ó¤õ°ÔJ.+ÍõDw»¹r©º´Ç%ëݵ³Å¹Ýps]nnø²³îH£?«9ÞÊåËõŒ +¤­¾ ˜`ý¸U¨jqÅáË‘†P˜wú³x¸‚Gš.¡â–ªh ˆòY# +̶cã.VØR]߸Ù:z³8w>ÝÙŽÔ@}«SÍ©í»ù©`a¦yôF¸uÜ“ìx” ÖÉå3v2 qxÂ¥e.Þ!ü9_´i#ÂzˆE%k§t× m@í2"~·Ž×ÖØè¸ •´V¯oÚ݂Ɔ` +üÎÕòx{å‚R˜a##ìˆÐ©þtzLc£.‘¬-'+ ¡LWe%ŒXUVÚF„€mszÂ`Ðœ¬‚ÐRºÜã™#Z;®5W/N¼¿ºpF*.¸øbÿâ¿vû©Öì µXãs"¢qöäM.Ÿ“ +¥kólŸ­ :0ÀQ7<ʸ': ¥`¼váæ㧯>îæ3No²:w9;yF©.I™. ;WYh£‹)´·@ÖÑ"ెóYÄ£Øyÿ°]gqWÇ—zúG#Zäà°Meö8è(£´ƒéÙPr‚`¤úô̘úí¹êîè$LúSÍ¥\o»0µí•KÓ¾X-ÓZí¿ÖÙ8[èm€âÌOÓÑqŸbõÊä&®ô§²ú~ó*aÇL¸ñßÝ|RmuZq±Qj¢±|qùü““Ço”gw&ç7ôÓ÷vï~Œé7¿š‰Oì„Ê«±ææÄÆõõ ßi­^ÎMÝ»ï9O²«w‡ýùùøĉÔä¹éSß™Þ}¬ºz¥0~ô¹7Þ¿õÜOMdD c_í +XÙ´'Þ —²•SïŸ:~%?»[Y:—Ÿ=)×Ê󧚋gWÎÜ:qÏÓ@ 3­µíkOï=ðÂü©Š³{b~†öçvRò¸ ªì´ŒXHTEJªq±.ä‰P̀ʤX¶1 +¼¾s²BUØ7¢~_µ™“L´"g›Ùú¬Ržç’Ó¨PE|ŒMÛqaðÎ.Ò $¬˜þDùúX½“t2xab@•P_Ä7«4+2ã{Bf”vÐ~¯œRãåéÖòV©9Ypý5wfÜaF˜sàõ¥}‰¶Ý-`€iŒèì¬ÆJ™]œ‘’­õÖÊùÂÔŽ›…}µñIÅ`b¦RÈ9= ŒÍù" ŒKÞÙ_z@Œ™(‹K84˜h(•·¨à¼È*úÿ#é=»ä¸²ôÜq—¤™Z˜Bùô>"2l†·&#½÷¦2«²²|¡<ª +@ H‚¦éÙ†¦›œfk¦{ÚÛu÷h$ÍH­‘½k]]­ûõžl­ÈBFEœsö~÷óVäÙ/Õ­Dëx=$€fâ˜V97L:Q0à‚!ÚvcçfƒÀƒr¡MJ/+¥•lw¿½vu¸uz|çQwã´Ð?X½ò¸ðhûÆ;££3­-!Q•Sí\cu÷ÚK|fA-®Ô×o öîH4·¤ÜmW……ë/|ýèù·¼¤)fGLr –6—OßZ:}+ÙØê,î~þÕOO¾-æìæ&ÀíÚúåãï¿õŸü×ÿæÕO~´íÕ¾ýW/¼ý¥²¦”×øÜŠÕ8hmÞß¾õñÂÁã¨R£¤\ L~~è€åyH ³v€I±¹ÕòÊ]¹0Š0Vea‹RŠ¬Õó‹±dÀ$©7‹ƒÓkoãZ–·JB²‰éÂàjeõy±°¤ €üÙÔeuŽQ³!ÊµÕ ÇÿôrmÒGG…|ª¹K(%ªÀR±±ñ|nñ„Iõb‰6¡—½Ø¸ Ÿ”Ý0 1q\ÎZ‰Œ—ØDW + 4à¿vï$ëk@Éx݇ªóþ˜T­îCd°ôŒYsãq*!b–Í,%š—H£ƒŠy’O‘B +blά;Bü„“@ØŸháJ¸T€‘ž¨úÌdDi„²‚„ÌE‹O{ Âão9„N€ÿtãgç‘óÊhÁXbüÆ!-¦Û©öv€J˜©ŠQ\À•Œ’í +™Eà’ÄTß,­êå&š1Åb%ˆ5¹dOȯo8ŠZj)ž]œÞ?È1Å‹óAæìLðé ^`"‚hœJ!BES‹Å…ƒUà|/¿”émájJN7Ú7ôÒÀ® Œb“sm²Fƒ7›rªsI«º’élÇŒrT.iPé,Æ,ÊvÕ®,ÉÙŽìÆ–\Š™Acåjia×LW›í¥;ßh¯òV¹µréðÞÛW}pëñ·¯¿öñ•WÞÛ¼ñRoãðæý×vo¼Ö]Ù{îúÖ Í퇃½‡­µvób„µãÉæs¯ÖÚ¼ùì<2áÁ£R.fuôæ¥üÒuTÊƤÔý×>æªV<”%Gvçppðhå껵ícöNî=xã“Âà0Ìg©DRj°Ú*/ÝX¿þ~vñJ2.ßy=ßÝ™ +rÀVÏ„$?•‚…raá +@tÎl4úÝí» PÓÝ]¨ùc6®Ub‰fª·ÕÊNˆáÌàIBɃµð“Äg¸d»¶vëÊãï•—/ƒ4Ó}‡³ÞqãgˆˆâcèìâJ‘”2ñÜN¦Ë®Àb)@O]â̘=¤Ë gzZq‰Ðª BæÂìy€ÐB¨X Åìi €Ã3sÑstÒCNã6~j24ÞÇá@ŸžE\¸Á¦ÄÂˆÏ Ød—¶ÚHåÕ4Ì™3"Ó0%/­™=½¾Kšõ˜š5r-.Yw#œ”ê¥NíöQ¦w‚É%…ŸœvEe½¢&jŽyvÆfÚÁ‰/eó0Á¹l¡»á¤’“Òídw‡Í,z ÌIº{ óA ¨¦ÔŒêE£¾?îyEš ˆÉNT)™õa*dy»¥¡Ð« ëçC̤2–;ÂÌÉ ‡H•—ÌD²œ,t)9)ijÅÖru¸Uè-›åÖ®ò¹Ž^e»k­ÍS`†šKû÷Þm]¼f’×Ë(k1!‘‰TmuÿN²¹¼ªh/¹‘œîÓf;@ÞÆŠÊÆÁÕ0e¢JCʯgº—‡/N^éî<¯ä‡´hß½ÿøG¿ùw£K÷a¥¬7ö¬…k•Ý×N^úÎêñ‹¹Îºl^yç“ÕKç"âyɤ–ÖQiùæÅ[•5=Õ¾ûòGÇ>š +ñ³°ì‰©Õû»÷î}¼óà©´žon¼ðÎ÷Ò­m$º¢0ÈÙÞéòéã“?­¯ÝÒã[o'*ëOM…ž +Myé ™B…¢]YïlÞŽÉùË7_þà»?Á¥¬6ÙÜ“ ùåË_ߺó­tç4ˆ×^xïêÃ÷Õܘó³Î(È,$…©¡µÏÌ“l{ÿÎå[¯º!áÏŸÿógæþå“Ž'&B.0Ϻö+÷÷µL[-L)Ÿˆ_f„ 9`²"¤&&*>Tôa2Ä¥¸t_¯m—û{÷„üRPìòŠ”[ráãmV³!~ÒK9BÆšŒœöE(‡Âb’ÇÌú‰ › ñY÷x‹_.Äf˽¼œ[¾<<}µ{ð ¸vG­ìÌ)?ÂqfÕP–´€ÓŸò1žˆ„‹«²Žˆ…aêÙ>¸%7»Pm*À¹QÕHÀ‰°J Tv/Ä›•5òŒRʤ’"¸O5ÍÂÂé3>f LÎ0‰juådxx¯³yCJµõ\WÎ.„ÇXh3Zgí0&c1-JësØ9âþÓ# T +JSÄÄ F›HLwED˜Mb>LÆÃ1=ÊÛà¸e¤šÕÎF¾µH㧙ÕѵLo_+ôbñ\LIŠñ¤¬'AAœ sÜ!dcj)ʧB¤æ…9o˜¦ø”šfg."kÅ5½º)$»õÑåîÎíTwŠé™BkãðŽUB1Û(´âPHu(½¤,Lêþ¨Bûz%Î;PgD +3Y)½¬ÖY£…ñINN䪃ê`|@mì(µÌÒìàŠQ^Êp)军W–voî(ȨfƆ¥•Ò¬ŸÕT+Ý}øþ ¯}ä„„§¦"^n6jEäš^ÛÕËb<÷ö7¾¼ûèƒþôD`ÖÏø7›Ç¥­ Bœ~‚`M=Ó + ˜³ål°_yñ0ÓÚ„8›ýÕãLcÕ ‹>T£âµ¨Cù¤ŠÍyàiGÀDb¬à †¼á(&¤ìÞ%vÜÁ»•Ë(£w;kÏ«…ŘUS +Ëra#È\.J©11é +î ! „² >'%{À—ÂÐ)N)±R®T_ë¬]wãZ€Òƒ”îÄ95¾gFO”g}0ÂóÑI'ÄãðB:Ê'@`oHª?.Èévº½£×ùä Ô?Qsî–Û[™úª+Ì?èC?i;àDˆ*ÆÔ&¸¥yOXP’’Y›ö’c&ybÒÞh07&“ÌeÅÔ‚b·&€ÊñÑñ·§óàQJ‰r„TZÌà†å ן !Ï8#slÞG‚ a±ñ{qµfÒŽPÌ*/i¥áÆ õ".¦5/…˜˜púqJHZùŽ"=a,D_ ³ñ%çÀpÀ<8 ^2þ* ð“Ä1¼3f *ûà«öi£³I˜ïˆDh›Ò|<ãò¼=?ˆV'âÏ\ðŸ +͹`‹‘´l$j™òDø˜Ø˜OAb)̦ƒ¤ÓR4Æe«]É*==ï KKëÍíjë7Ýa>Œ)¢\å‚6ftRÍÉé–œj¡¼ÍÉINM!”êsQÚã"Õ7Žò† š¡(J0B«r®ªy?©ûPIPÒ«›'»—ï³z¤ª^XÉw/«ùe ÚíÇÀ üÂ?Ì¢t„ËPñ&ÎçCˆ$)ÙÝÝ7^úÐGªN˜ ª–‚zqpÈs^tÚ‰Þ›÷QHð¢ªçC¬p> nf)§j+‰ÚšVÉÙ%ÁîãR1€©²U—ì&ˆ«i¨ÙE£´ -Z®09tú F0H!ñÄ9×Ó|3¾Dg£|9D¥¼Hx1-·ˆ‰y*"T×Õü¤À£•/Ì„<Ò¼³:7@¤làã "ÆÅ©ù¨•Ìt?„ªHDˆ²À-y"4àO%]ŸñFŠ’ðn&ÎZ.yC˜Û£€}sU§Ûç „#¸Å,˜IøPyÖGÐrF]!æ©sîÉ9(ˆÈNÌÖ “ƒÎé#0_]ÜgÕ¼'DE™D„´"¤ÐÈÁ ³ÁóSžY' cÊÄlpb.âH,®$ʲYd¥l3øgÏÌ?5á{v2pa¼ 6:ãA)Ñ60Œ “ÎÈçݸ˜gŒº&×cŒÚŒs‰óóá'Îθýp=‹³ñ\mÑ,ôx³ÂeW›v‡ +a@-0¸deºÙ…ý³®èÄ|pbÆ{aÖ E)‚$#ƒ±ª’,`JÒ¡)!Qi.§òðA0M™–h•÷ñ 'ê Ƽ––²|¼Â¥9oÔéÇÜ!Ò*`„›q¡¨ªÅ|u1UíG)Q4² —aœBxOž÷]˜ƒ}ÐxhÏ\pÎ9/žÀä*kvÄäoÕü0#(©ÎÊqL/ƒ(.¦×µL?U[—’ >L›QÆ@9süÍ:$p1±¡(ç b“s¾9W0eý‘˜'À•‰×I)"Á ñ ppfh&7åìc´}€BŒÓO9}Øùi¯;DD¾žÓ oO¹§&\³ÎÈÂpëèDÆg݈ˇûC ÍÛA„ÀœÃ‡ŸŸö‡Pc 0@ŸC0£ Jð!ø"ðœÃŠÀ(Áh!Ä,ÊgÆÏ©=`êh§Ÿ|âéÙYGtÖ‰L\ð9ë:7á›q‚ä°˜ÉJiÀ-Vºtñèv”1g\¨ÛOUñ„9_˜ñ $LÍG”DKKõ°‘º;Ì^˜ƒ¾ö´cÊEøa ÜÏ“gfaB&xÛá#æ½Ø”9;ÁF’MB˜æ ÆpƦµÆ³3¿8;ÿgÏÎ<5áÙÁiY”–ÍÚ9¹%Z¥iî ³ABE¸4*•#Lz‡dîÜ~å3ÿ≉g&œÞ㼚R9š×vo-í\wÁÌù¹Ð|€šó‘s~¦TœSi1Oìòˆ’ +mF¨qªâ¬FœâX)åGhoõ1„T»Ž0`àQ§ÛMÅ(%‘ôaô\› ÂÞ0FK9Fëx ÅæÏÎÁýã/𧣜å¦5ªú1  +ø`*<’ç­ú†ŸÐ¼ç 3 @€e䌠§½Ñg§}3^Ph4–ðÃüŒwb$s–eƒ±9¼‘7ô\J¶ìúúŸþÒBGÙD¾½¥e€b™Ë +d*B¨QÖfÔ +Dš`ÚÍT3‘m{C(tŠV L€0ÁxOž÷õdv÷ôFe&.x§€¿>ã8{Þ §MPîAP¹‚|‰{¨Çšœq›r¹|!E–s»O  æ×À¼L̃æ9?˜œ„`Á`Î^ð<ñôôON;<%.í ÆJÑÌœž(òZ„t ‚x¬59~òÙ¹gÏ9§füNJ°vfçCÓþ'âòÇ Â ¶3Àž¹àyöÜ<-e`R;;陜‹Ì{ÉhL—´BלÞØÄä Ðãì˜ôO:a_ˆ'3çŽ`ƒS8ÃáÙ´Qí¶¢ë‹b’™ÎVZZ2åDO8Ž„•¸™­tÔLãÉ ÷¹i¿'Ô*©iv B K…ôâúº’ÎÏ‘I7ìx„RHA‰' ;kÛkeeñö /–Ú£ósþs³ÞsÓ.Œäh†QU™ãù@(,Ë‚m1wºg½>ŽG,KÌåÍýÝÕÃíöæbº––u‘U” !¿íיΠ+Š—9EôFëœZÑ °gf>.‘*Tsí>£%­\ a´Ê„¢¬+€Ÿ™òýù‡3"Db©ó³0uÄIV±Þï.¯²¢ dÚ…0™×@î11aôLm‘3 +L¼€‹©9æ‹Ä \„ ŽæTœ‰‡"T\Èá¬I3"CÉ„¦hqšãEIÄ1Ça’„Ìâ¨G0@‰Äøƒdá!Lñ…9O ‚kÁíñz|^§×ë¡šK ª©­¥ÊÕÃþÉþÒK/ßÝÜéq &E?Ä€Ú‡ñÉÙð¿zbòéó.‡‡âp„"!‚À(URdIây1F½@ˆ‘P&)ŹcIZѦÎLºžxzò©gçÏOù'Æ=ŽBÓóøSç‚Oœõ=s!0ëÂœ^ÂéŽx‘ʇ‘X"Éöóã(• ¹ä¬zúüüŒ‚a6ˆzÝNó«"\Êp -{iX+ÖìdV©WG›í›—7Ö7»å²Y*$ô„Í«À³³O™žžuf4–`QOBFFÝl±b‰ë¼hè3L¡XT†ëáí½/ß¹±q÷¹‹¹röüŒûü¬?!}ãÓ×·ÿã·~õÕKùîÉ_½sðÿý§ÿû¿ýìŸ~ýÞï¿zþ¿þî½ï}xu£—@ ×ù©i§/‰2M÷òb]ÕTïå>{o'uw'ùò•Ú‡/¯õþÑgolýôó»ÿåï¿ø»<þÃÞüçßöãï<Ø[«÷‹v©Àä„iç-©™ŒmUIpú‡wz¿øäúß~y÷{ï|÷íýzç?ÿö£_~qï«w~öéÿô«w~ûù­Ï´>|®¼ß“²£˜©iÏÓOMN=æë&¡£Žp}Í~xZ÷ÞêO¿óàŸ~õî¿ÿÉëÿÃWÿþG¯ýì“?ûÆñ_¿µöîÍêé0ÞH’†ˆ¢p$Æih,7KÍF{ÔÍw³Ì ƒöÙ÷î.|ùõ£~óê¾qòË/oÿýO^ÿ·?}óW_ÜýÕ7þí÷oÿ§Ÿ¿ò»ïßùüÕ¥·¯dÕŠiÍ¢c\‚”rÎ(YdÃÆñÛ[ÆÃ=û«•ÏßØþÝ_?üÕÏýâ³›ÿó?|ù?ÿã_÷ͽï¾síþs»8F¡Xx/ŽH°áº…-©ã¾úÒQý³×v¾z÷ò>ºöÏ¿þèÿ×_üÃ^ÿ›Oß¿¿µ9,J ÇðÊñjVÒ²Šb«,áÅ‚xi©ðÜNåÁAþûoíþÝ÷^üÃ?úø•ï/}ïÍíŸ|óÚ§oìß½ÔÜU+¥Š¨çh9 ,ðbãŠbfò‰DRÀ[6¹×·“WW’_¿ÙþÁ§¿üâáWï_ýî›>Ø~íò££Æ­­Âj'^Îô XÖ+ÉLL9†•ãDU…­Ð¡ôè´òøZó[/oþ—óÑûÁïøò?þòíþý'?ýνOï¾û`}ØËHjc ˜Œç‹me ‘\k%ÛIl‚öšÌÝ­ÜkWû;·/–ß¿=øÕçwúé½Ï_ÛýÆ £ÇWû'K™Å¢¬1Er®’™˜ + IA~ð5Lb½eï.•.örWÖ*÷ê¯_¼qkõõ›«]ã°ßïZ‹ë€è’dóÇÄ$ɧx%'ˆ‰|:“5äŒÊ¬Ô’‡‹ÅKýÄã«Ï^]ÿõgWþéçüÍß~¸ò­v“/¬/WJ¢Œù18 + Ž˜èA”5ïAÃ…ý*,ªØjE<è°÷¶íWŽŠ¿°ô‡>ú_üËúÕÛ¿ýê•o?:=UÛÅ€^7":ð#!¿DaI•-šL;#®ÔŒ£Qát­øü~ã›/^üäññwVnîÔ»@ž(ØéqÍ8üîkdòm=ÝcY#ˆrRß_[¼|°ÚJ“—ú‹ÇÝ_îæÁâ•õ +8w{!ÙÈi¦Êª²‚Æ o4îÃL˜ÍržrV¦©ªf %ŠÈèrF +&7jØGË•+›í“õúΠÐ+¥»ùt3m™*‡ÀPcÎ5`Q& a’Çô{<ÑPH¦b­|n½W¾4L?·™þð…å¯Þ½ôí—7ðÑÍ|pã{oìñhëÛ÷×Þ½Þ¹¼ljl +ùIFöÂK@¤áðSP„Ì%Rµ´mц ï´µ“EëÆjò‹×wÿóß~ë?þöãŸ}~ïËw®½rctåbm©T4™U³²ÝA˜ÂèA„aX­Z¨%$&£RE‹ËȱBœïæ­vZë§Ùã¥ì­ÝþáR©nÐyM4E‘£‰9üô¤“<3OÌÏ”$¢ +Òlij6i’É’m&eÚâ©”ÌLA%#21x¡TꇎrAÀQ>QèœÓéÅ‚!œ¥Ålª´8Ø^l/• i¹¿¶^Y/‹'ÃÂÞBv­¬®W´¥B|±`TÎ3=é˜øaÊ4~fÒÿOÏLN»Ã¾ DQ¶Ä•M1-ÀÝ4;*‰—‡öãKo=·úÂAo·W1c‰R¼ Vsƒ(“öŠ34«¢Â0‚(*åR1—ÒÓ–˜6yMŠIËËJ”ü¤0å‹N8 _T¦”ÌYS~ g‰ãgy…T¶‡:++–ɪ²AS\Œb}Ø$|aÊ¢¦=ؤ RÉxqC¯l¢B†Õò­!(#ÊÉLƒ4áy½Ù\év×ëõ%=nërPŽ=!ìéÉù™é§RQ¹Óê|¼ŠRÚĤcrÚÅÀÇ,EIªŠž4´rÖXl¥V+›ÝÄárñÒjgÔȵ³ñfÆÊ[†Ï~zbn>0ÞÛ~Î?qÁ3á&`Lñj®º‘ÎuiŠR84¯3iMñðV;÷ðúá‹·/_Ú ëUSH‹ Ê'„é'ŒÙ0ÇØ ÙÞ•lû4™ÒL|0X¿tùaæç‚8!iZ&•ªäò ÛÊ31ÙïÅCqj:s!8é$¼¨å„4GXâ&%f<îH8ùƒè”žq#€Ì½.„‚Ä´0Ú‚PÎáôƒÀÒžCpv„ˆGém´ÄôÀÿüŒÓᣠ,ŽRºdT­‰ŒÐ Ò endstream endobj 63 0 obj <>stream +*ÅZÈíËv®ʢBd\žu"ÏLø‚¸e23nâ_~mòÏŸœ¾0‰DEY65 Ã$ ¡,Åi‚LAaˆTTŒQŠWÄ´ÃL¹PÖêÚc)=’ƒâàHL×ý81&g5µ‹zy f0ÎsR¢Ú]™ôAÏ̦ü oq™e.¹„+˜ÏRR¾ºxHš©ˆdÇ›2ÂLÚ«a,%Ôyṳ̈d"DÙ¤Rò?føp݃¨äŸnOÙé 0±$gÖ¥d‹7Êñt“’à´Z +’f˜Nâj•Òj´^R]ÞnRrŠÒJXÊj^,îŽê¡X2H&!ˆ+k2Z”ÒŒ^Ѳ}%ÕRc ZT›–3ß¼  ­w…ÂÅ ›ö“Ï΄žœòÏ„øS Í®⧼ø”gÜ“¥À\‡Ã(Ï©©bŽ©>l¼=ãó"á','"=55nÀµ˜V±*Q¹æD $ú0UÎ ÔìÒ¤w9˜IPj‰¶Z>"á€/ªSjÌÌ\›tÇ`®h×÷øä f´ƒtú‚szi†·) ,?*WĪZÛJuöÅüÈO§æ#3‹ÝœÏK‹©VØ0+;rqëBXyj{zqÁãMCÿê©™ss0etÔú¾QßåSKΰüÌDÀâç|Ü×ÎøÎÌ!!&Ã$ÕÂo/9}6,Ÿuà³^c¬`T~vœ¹s“y/&t^+ÓBjjÖ?ï +CQ!‚Ÿg9ÂÂL€›‹HÒvaæŸ?9'Ö"R“ÒÃÍÅÅßBÔR˜Ï’z™6ëb²WZ:™ÇD'"D¥—éù,8×K“ÖB®{²}ë=³¹ý¬‡p V,µ*•ÈÄ$=¨:7Þ–nz:æ'­ ¥Ì—‘F›6À”°âÆta b™M.(…aÌl†˜¤”^°ëÛ¤ZbôzT)‰žÞº¤Ô÷¤Ò:—Z Ôbyù&ozP™ÒǯÓbR}4Þ +Å°Xä¶,^îïÞ ²ÉóÜIQ©ìÅÍ—…¤’0E£VîƬθ—¸Þ +•ˆÔ$ô%bøÉ„+ªN¨™áEĘZ&ÌWœAÖb­†©U/a‚û׶ÁÁj V›>䘎Ê%k>TÃ¥‚;:n²bRQ¥Ê“õŒƒ€YÛa.¸7,úÈ¡ÖQ©ŒÉ`r²ˆPÂÕg÷g‚¼·±\˜+ûcDï²9ã@ÏÍGYsüÈr6D;aRÊa¥‘*ŒÝ#õz˜I( ¤öTD™Gu?WJ©ÔH(n ¹ÍgçðIŠ%B-»Ðx€L{Д;jqÓiO4>á""|NH- +ÉEf^ðsQ›¸±À ,œ›…¾vÎ;é@ºÃ|€´êÅt7,…IcƲ—OõÙôJ-‘É¥ˆPzv:Õ;¨Vç—¶N¥»d<ȧý`}sK ;ÆíÜñ—mŠC9?˜Ç¤‰0;Õ£J3½p-=|ÎìGÆmcU<Þ`Ò#&³‘ë~6æŸyT\cÓ'w jÌZ+‡|~×Ç-Èü” n€0:>Êp!2¯ñ™Qß±›V}G-®ê…•áþÜlÀñšÙ;­nÝoï½Xß¾ou/eW½ifú½Ý»N"`S ´˜ÌZ¼~˜\Oõ¯Çë;f¼ç”2Û˜ÚˆˆEŸ¸Ý#Œfsxeéð¡×Ÿq¡Ü‹›‰ÎÕxý +¢v}±¬d B„œÜQa"ÈÍ„eoT Q6ļdRÈ åÂJ5}1ÓÏP½&Ój“ycI7¬T—ødŒ÷‚Ÿò9H®ÁJ›H,¡jˆ˜ây³Î&º¸Rõ¢½˜ÕWŠ›ñÊv¼¼9(½³‰x\ËE$fý¬‡u㶜_‡ùÂ9ê%¢Z´ûDb1ª5æeü"ÔÔb€Í E.»Šê:½5zNܦ¬¾Zܘ €°”ýd$ øý\j1êaûp‹MœmÊC;÷" +Mµ¼U[3>jÊCÎEw@÷ž83ÿµ³®'ç0‘Ž™ \jˆ©õY/=äH¤IÙCXn2á£ln1£:Ù<´;@B| +Õ*|nÉÏÙ¸V&¬ ‚ÐliÅQvá€M¶g‰°Z!P è $±¬Whø¹r,^—rCÓKÙ}>·šY¼ÙÝ\\}ÞËd‡Ø™ÁÂúu'•˜8'#|•NŽôÖ±Þ=¡mÞ,kåQT¯QV„(¡7©DW­lÉ…¤ä…ìÂèÊãÞ¥‡ã¶Qõ]:»J ß í…Ý{‚4¯ÙíKÉ…Ùåç §djÉÏåü¨µýB¢¹ç€4µ°ÖÛy¡¶~«4º98|ÄdGA. ÿé­µˆ FšÖÜxP]1µô¼R? 3¹ º[Í3^z.$"B…µWÙôº\º¨×/ú)•2ëJe‡/lQé•ÌÒòúóã®>éåì•X†IÄ+dbË, ù5Â^žGǛݤäBkíÌg âŠKöæ1•\ƒÈÜpÀñdmWN/N†?scvˆ-p¹-©r@Ú#7‘ÖóÃã]¨~ÆEž÷ó@¸p ¡õƒî¥·q­¹wíÕw?ý‘]Û8ëeg°’£çÊ;/gWî°Ù•yD3ª›@¦™y,Àf<±4ˆ«y,å·æ’R@‰q#â„; +´ëŒ—Œ(¨ÑcóÛfçÊ×&‚çæ">Lsc†;jùð„ŸJ;ÓM¦¹ÌªVÝ™ Óãî[zN…¸¼RÙ 1Ù›¥fôbÉaºw­º}?ª•\TºTgU¼²Æ$Û1«AZÌàD*Œ£7Äü†Ý½j÷® +¹UH®ÌDe?›ä2HÊ#J•°ú„=Š(-0áÅáõüà趘]:³P­K ¹Â +m5ºëW‡G/²éPÅÀ¹\r!»x¥|ñ!WÜ3½Îè襯™ì9cV4Þ±Ú§ùáíòê]»½d¬ ÀЉùˆ÷ÒfX)8c6nu*«Ïwv¥N «KêÍÜà*ª7ƒ|žJ “ý«bq‹L ”Ò¦˜_~Æ ÍFp¥ÐÓJ.Ó÷йX§ãïÛ/íÝ<8 Ò6Äãj«8¸Ý¼øˆ/ÁZ“Š×_ü4YßvEu\k´¶ž¿öÊ'×^ý¬sô +YöÅÒœÕyñÍ/„Tÿ¼óÅrLz=Þ¼\X¹×ß=Õ»‚ˆ¹Úð8Q]PNÂDôž\;n]|µ<ºYÝ Œv„/Z]jºPƒ²z±ô2f€ú%Zǵ•›À¿°fW¯Rö"™èRVâË„RYÚ»{pï} A:#ç7ä›YB´$VtW›ÙÁðó3.f>¢q™E¹¼n4v“ÝK3€x½Œ™_®®Ü˜°g8V-w‘H (PÝäFX¨ó°zzáøæ‚QÿOÌœwDž‰ùíüèîèÊ; Ö̇¥ƒ[ïH™Eª91Öû>*é£R 6IÅ‹ÙÅ;+—?è¾Ê™Í½Û…þ./svKÎ/gºûùÁI~xEkl“ñJª± ®3j µA…¤ Ÿ[ItŽ¹r Ø™ÍÓôÒó¹µç­Ą́k©ÞÒÁ‹jyÙÇX|vÙhìw¶îm_}¼uëýÊêM%·Tí–X»kÔ¶óÃëFã Õ9èl?¿råu½ºTêíµÖî°™!ˆÛˆ\ +rip¡¨Úòx¼ÉXõêè”0›*1ö¹5(¶½˜=×Ü}Ä$Ÿ)µÖνîÑ«æÂ5XïÎ n²ÉŽÝÝ›Ò1»k ÌìƒÓj;Tr0ç97á²´Õ&Í*fw±Ô*“Û”J[jiÍGê~BMÖ7˜DSrra)3º¦7¶Ú–ÕܳªÛ~Êz(×¥ñ*ÄÒšZÛ§s[€í#>»4‰Ùî~aù:“Yæ3Ë @Ń¥ŠVX]>x€*%XÈu6oÞÿÆðêÕ­{©þU!=†zãàÞåŸ8P=@§”Ürax#¿|3·|53¼î‹eŽn¾ÑY¹"MDÈd–®Ö·V×ïe—ž+®=DãÝ >•J\ó+BqͯDñ䀊ˆâ¬“öá6È,D®ŸqÀçÜè'᧲˜ÖŽ]Þê͇ø''|€ ANÁr#Ä—`±:?e Å #gTõÓI ^"¨;ÄåPµ.æ׬ÆÈeBÊ÷GGfeŒé¬ÙL6v^Î-ž +¹E*^Æ°8º>áýŒE'»Vã(¿t'Ñ=µÚGZu‹6š‰ú&ˆ>;Œ·ŽÔÖ>ÒG­Ùµ½æÅùü0ÀÚÙþ‰U¿X[>ݼürûâ=)?dôZwùrmtŠ«e«²¶uõµ+oõö_ní¾˜êŸ°f~ïôÁÖÉ+>ÊtVT«ÇìˆxãÍ­#JÝ*ォ–(êÕ‹\~C(勞¯\·}ñ¡š[Y=z¹¶y;jÖ¤Ú•Û@ÌE&·¥ÕŽÓýë¸Ù%ÕÂáí·ë+7Ç»étD©CZ5Z°XJÖ÷rÃáÅëõÕkµ…_K½à)ɤ€ÒÖ6îŒe“µõÊŠÕÞÓj›©Þ%¥¼H ÀÙQºå!-L¯Å»V÷²Õ»–½è^aSK íÝ|¼v +دàjü‚Ýêvª{¯l‰™> ب˜koÜìnÜÔ +@íèä‚œ&ëëëǯ,^¼ëŠj¬Õ©¯Þhí>*Ûã ïvŸ4:Û§/Û¥%/,Žw¼öÍ9»Ä§—Qs0µ.ÄX¼æ'ŒÒèúÒÕ¯W÷_Ñ:ÇFûPo`æÒ¤_¦ãÍúÒe\®x£|ˆÔÝQæ*Ø0µéE­ggOT#µ–´ÏºbØ ´®õøì0§ è£æ²XÚÆ£baÜ@<” 0k¥5!3@ãU®’r ”\X6ê[”ÙŽØ_½H¥Ÿƒ„ƒÐÊö/gú×c©AXÌÏ&ò¥…cÚ¨ab¶0¼\ߺæ¿»ûrgïÕÜèžhùi«´|ÒXŒÝÄt 9³²fÕ7q½a,%ÕÒ²d}Y+¯€ìFãu*ž·ÊËBª{ÁObJœâ¥ ­Ó驸¡”¶´Òš\øHÕ+;7¾¾xú†XÞTÊ›&P»Mk•…µ«@Â|>5¸žèÝPë‡ÆÂM:»¢h&,˜™…“íÆÆ“sP˜+Œ}zçX*oJù•x~Xlm?zëÓÛ¯|sO¸©¬´¸üVuëáâµ ]Ý8¸ûÚ‡ßO5·gQ#j-Áæ2b3Kϵ/½m¶¼¸.çÅì⟺ûf¬†¹<¡5u N‡†ûÏë¥åÖ芘`ZÐ[*5‹¨ ²òLwŽÙD‡GfU+•ÅË͵ÛjiJ¨XLÖ6´ü",äc¹u­uIo*ÕPC=¨îEãzexÌI/9æ¹J¦W•Ú‰RÝCär˜Iåú»[ šœñPs° +5½q)^\×ò#ÖhΘ¥èÅQ(–™ö ŠÎ¹™™ŒeÌÊ~º}Œ·•u $j:Â’Ò/ø'lFO)n‡èfÒl€¼s¡0ò`æ±x‹´¾è¦ì0kSze>*áñŠ^ßVÊëRn ð˜=¸MªÈ¥6·4ªaØ¥ªÕ€î±ö@ί« âÙîAfáH«¬3éEÔløøtPÈkåMPåL:̦I£3šFq”¨­‰¹ÅXŒò)9× 2!%« ¼L¢}Uk°\IY ¸ÑšƒYR/¨µµlÿ¸¾q¯¸r{ÜÇéåÒðdì[ñqLidz¹ÁA¶ ‹´ÕZ}΃ƒ,P ++ýû€pÄìÔ/ðÃó^RH >5é'<¸Æ§»ve¹ÐZ_Ý¿cÝD‚M­ð™ Ö^6X†°\—ÒÃÚèjwó–—Lx©´PÚ5nIÕ&µÄfF© + {ëNª¾ì¿›LFIJÝ<ê\|©¼|-ÓÞ³*+ÀE¼üæg•Á©—)ÈÕ½3n«žè«¥P,Õ~ø—¿9~ðPl'¢‰E¹´›èœêµ‹BºÏ›Í•ç×OÃrÙÇ$b9ªµÙä7"b-HgµÌBmtI?[òÑù9xüg+L)ÙÍ­æÆÍebRŽ7¤ ÅÆòU˜/ÎÌSl¼Ñ^¹‰p…gg± ÐZ¾ݨîë  JráÁãoo½ðôdè™9ÔAd#B=^Ø2 + nb|šM/—fÕë¬ÝF¤2jöb5ª5ã•}¥xQ)­à ü8XwÒjañFH(ù¢5€nW7îÒéþ9‰ˆ% $°ZѪXÄ’A&•rfc×*@¦„ìP­]´{WŠk÷–Ž_Ï,^0IL-JùÅÙàÃÌ Ø©äŠTAã5_L—S R¯Î!j€ÉÎ!ú|4˜ +8k€¯\¿|i÷eÔ“V7–‚GÅ\¢ºÕÞ½ï!—A‹Š%õZi$æ—<ãWï™1£î#¹‹©%9Õ)´·x»uÎ;ë¦g"JÍñ@~)Ži¬Y3kkõÕ«•ÕëZum\¡¤X˜‹H@ +BlZ.¬VF·Ú;ÈÔšM^ðrj¦/ç‡N<~ÞG]ðPó°ñU6½Ê6f!Ñ‹iÅÁ1@DPBtÒG&€Õµ*Ó­³v,¡‡LË¥-\oqaø¼íüptøbañR„Iù¶7×fjÍKypTùbïÐ.¯mŒYM>¿–hVÖîhµ}fÏøy9Ñ) A#| 7:À¬c¨äWr 'jn9H½ÑåJgûOœÅmŸU^½Ußx>Ìæú9# +Ë@Tz6 ºm6,ùéTÌlsVÃÈváñn26fõµÚ¡‰Ïz©Éù¨7,bŒUë°\ r…——óKLÌ>~¿Ò 3¸ì“ZtÀ²–ÇíÜIƒKõÿÏí¥z× +—®âFs ŠT9ȦýlK´|Æ ê¬TÔJ«ha1¤/³pI,®r¹!—_R++±tßϤ€ýg³ÃùH¢:nô¨DQš~&ç$,¡™Ö¶V^õÄì¨ûd*Ìæ@EjïgÆãÙÔ ¼rÃ…éàÆò G‰æ>À Öî«…UÏ.Ì ”R,^·ã#mÒìÉ`½”b­mJvGH¶gÄ”6ó¬“ôQ´B~ æ²(oõ¶›ËÇ@¸`¹«Õ°TÕ*f´q¥:æbjAÍ/rÉ–”·ÝóÐ…¹på‰T}—K÷'1/¦„({>¢"B%–X +𥙀úÜ»œò3ÓPD®0V·2¼]Þ¡’+Ï8cÓ! ‘kS~Æ‘æÃb´]pFK-¬Å€H*½°s ?©z=Èæ)³#ç–ÙÔ"sª˜åQ¢±V“°zˆÞ™‚â"¸(fvý¸Iˆù\ÿ€+8°PˆÍû}………•ÓJ{ã,RÍkù¾ÝÜ,Ž.Û­Ý™ Ü"¨†([ÀùÂ3SÑI‹È¥´‘ésvσ¨“îX€JªÅ5Ö^xêBðì,ä†$pÃà`SC«u™+îsy` +r^\AÅTT̆ØÔX…”šŸ+:© ?L©º ^+®h•‹ ˜*¥m4ÞðЩyÌœ²Úò¨nÉÞifp•K/ƒ…2‹¨VÁ”|º¹]¸ä‹™ÊðÒ&¨ï‹‡/¯_{»¸v'–¹¢ +0¡jmÇG§€»ñ$Ð.13îÝ/-ŠÙ~y fE¢@Uà÷'Z—²£»Z0>™®¯Š©6Äç‚LUT©1Ö",®sN  +˜Ö ÁÒØ=eOC‚Ÿ4“µ-Îl=3í{zÊ3姀÷R?_ôÐi°¬ihÉ. ´ ~7º±ÔÍm¨ó‚Ӄ°=u­°àBx®ÎEã!¶(å6ÙÔ,ëD€¹&‚ÀçκȹˆâgŠh¼gTöÒ­c³ºýì<&™ÖEÛÓf.,Îà +¨’¤5ˆÈµ°XœóS•Þ]Ys£š+ +¢Èš Ë“>—Krº?åÅÏÍCÿ§ÿsx<ä´ ·&#ãý¨¹¡–[v!jqxЦVßáÒChÜ™S¼à B“uó.H‚¸qç9·:ÖLu„”·›ËGf¾‹Ðq\+ºQRnW:{~™r“Fa9ݾxÞMÍø¹ õì +BWÌŒ„ôÐÏùcôx·W>ˆ™3^†ÐÊãš"=¸¤“@»@*ò1»O%—kQÛ!¡èBU A*á K‰Ê5:½"–÷cÉE&M‰ h³‚K¹¨P ô& p1¿šY8É.^C´ælØÞ|¼´šlïv¶n÷.ÞQÊkRIõöKk7¸ÂÊ9 zµ°MꀄÙQJõŠ‹—=˜VK/Æ›'‰ö%€[ˆÚ09/aÂl +\чŠAB§ôŽ\Ø( od»'´Ùqãú„—BÄ2 ¹”xp6³ +¤è€LΆD±=…Ì(Õ¿’쟤¯+µC/[ÖFLuòý>³1lvµK Àéã·Ë‘º™ëm¾áó‚°áT ó)¿!—ö@™ðPA*͘ ¨Ü@å:cO?ç¡žž‚`zÜ6 –òL÷Fõ‘ ”j˜Ëz!À¹a±><Š—扶:F}OʯÒÖüø‚}j2tv>JH…33‘c«Å•ìàêæOêÛ÷¹ÔàìlĦsí‹Bª"Hå\X $C&&Üĸ!P˜ « ‚?5ƒ ÿ +Ñ RLFHl\ÎÂ&ç‚›ø‹sÞ?{Æ5íŽAT’òs!jÒ‹©Ä\„¨A¨ BkøPûü<S&Ó'ÐùièÂ<s †ÆanÜ+Cïò™5VÑxT©‚ì)ƒ(5Bï(…Mµ²Kõg!ù™¹¨;Æ’Ÿ 9Ú^r#,^1‰y?âô#A\"µð•öøÍ ¹å›&7á%B„Ȫ.žé%<^"׿¼tù­äð2d¶|tšÖëOLAµzº{²~ùÑÉ»™¥»3Þ ‰‚ݦ¬NˆÍ€ØÃõ._0ë—ÄÌšŸ²!šÕkŒÙÆäâøÍ#TjÂÃÎFdToÛ!;Š*Ådû¢˜º (›?–¡¬A¼~$—€IÐZ®³ý<¨ï~>Ѩ=ˆhM4Þ,.Úí›”ìV²sª6á`•å*¥7ìêF© b“ŸÕŠ>•üˆNôt”¶ó.„£Vg#@5@øà¢|z9*Tcz0À\D•³Ë¤V›ñ³^Ì ó%:5R*;f}?Âg€–†h;ß¿„i5• óåéHŒÈ É B6yn6BJ95ÝeŒª”†øÜd€™tc1)Ëëõ¹ñ¦9•M,àJá‹>ÜrEsN✥õFT,O¸b~2駳8~JŽ¿;‚‚UÕrCZÿ׸×>Vé‰p —k0_Ž{J« lù{ÊË‚Z0¨3 \£:¸(˜%&rfQÊ.;£æ¿|Ö@Ë…€€ŒMúÅù¨á@ @vm‹·:.pVH +I7 :Üžtçã÷À‚÷Dõ åk“¡ó.Ô¢ $Í^¼|q°÷ꡇlz4â€4Qñ*®”Q£µ„ ¼ÞIæÿgï½~dI³ü°wAý ìÎöô½·ª²Ò»ðÞ{“‘‘齫¬,ï®7Ý÷¶™žÞ±=;œÙÅŽ°‹¥H!BÒƒô¤ÿA’t¢†$HBø @¢¾Î[]••æ;ßwÎÏDDå¹ûcÚoH ýþ‘7Øj «¢6*0QÔ¿Yœ~ îƒÔcÁëIÑ +ÜúÅÉæCMt‹´Â:{pé/ywàoµV“ó¯g/kŒo¹pEÁ_jíSÆL ‚¢ÙM¸ú7^²ÆyJ0÷FÏ¥p“ö —Ñòc´þÖèÞIþ¬H(ÏÊ©µŒä¼Æ5Q¡¥+¯a´ŽÂÑy08 µDk¼‘›£w¯?õN~°†/am«¼WeŒ›/ÿ8Ø}©Éz3Ô“ÖD +×Í;9˜rî¸üq~÷ËÅÕ×ãÓz÷5û %ÂáåpóÆëIÍ:qîRn•h?SS³˜ôO'§_ârTN?ƪ¥'§›ç¿=zõG³sY¢<É›Ü}üÛ£Û¿&ôž¬7Ãé«pþÚ^§W#Vct™l^“öà3êJ‡õ×&à^ëL4{ªÝm´—§o~å¯ëj÷w¸ÆŠ0Ǥ=ïë\P¬ËAg7:ú +ò¹Lû{%!SSHcà©•Ã Zï„Ó»îÉ7½Jóˆwçu±Uä@|Δè¤Î·Õ`íÍßÆ»¯mðPÖˆ”ZUÊ+b–ÙXwçÏ‹¨üE–Ø/Š’3)¡´HµU$ÁM¥ z#EõhÞÛ¾Ÿ]};»þn~ûóhùT¢èÏ­î){¸ O¿;~÷§Å‹ß÷/ ŠtçtåhB;]{p,?é÷ÞìëñÙ¯—¿ Æwu©Á;=³³åýç @K0öÐퟟøûÛïÿ5pJ‰¶1Á×›3`R-9q毹è¨{òóÉÍoåø„1º‹Ó÷­Ù5¦wµƒÈ]ƽŸý08ÿ•laÍ ôÛÖìÝ‚G«É-BëQÖ¨½~ôöØsdÄGB°‚ý"ŒÔ‚®ô*'%Ú1Ö˜½ž_ýüüÓŸ„è$K8%Æõ.qŒvËì^4×ßžü§“’[ÇeÆ%9Ú½íÞ±FÃ֧׮|}'P5‡¨’öOéžq3‡êP#lzäðu{ûƒnh=):!fsŽŠ.¥EŒ ø“èÍŸ*áŠ1z2Fë˜Ô{ð+¾ÒkRSϯÖò°*Á²=Ý=ú.ÞÐÆ’›¼Ûñ̉'ŒèÁt°zÓY½ê}©w.uP7*„.¤—TõŠ¸˜&¸3·wé oYs”Ǭ§N fãã·¢ÛÏ"Ú³²R;r÷6<ùõàú—~ÿ e¬Îètvñ ¸Åϳl³‹|‹ilõá›púaûEÖ‰×'Ï}P??$áB°mÌ_SZª˜µ{¢?qz;¿ìtVF<æ¬O?J«Ûß&›wNÿTôÇð€h +vN=@up2˜¯î ߘ˜SòÒÜAù«M°!<=<8°ú—ÁüjÈ^0ŒF²Óš›Öò¥Ö=+Šq‘ñwMoíή½x3¼ü¥œœ‰ñ1éN+RƒuˆÖlcQbFûÜê?—›'¼»m\—Ú +ôI³ .*”‰«€ê]¯g&—fç*·&Ä’?§ÌQ‰oòá‘Ô:s†/ÀÁáÆ ÌûYL‚Rƒ)/ï¥ö¹’\Àü£ÕûßدÉU®ÛM[#Λês)>BŸ~Ó^°z§¼Ù;&…KðeU)ÉS*µÜÞ°3kO³ˆšÎŠõP¥£Ä§îèu‰k"R Š‘·F8«ZúiÕ:¬üHoŸ€!‚¢(Ó Úƒã¨KÍ"eñþÊÜõNÿ4*Ç™š\§œ³ÛŸ÷·´ÒD¸&ǬÞáônT”To{òêþåÿfE“ +ãWØŠQKn+%A„1k´ÞžëÍeõq©]¡}I ¢d&˜½©£¬+úk¥}¦¶/wSÀŒ¦Õ(à Ơɟ…å7&éYÍÕ³ÿТ3%ºAj#§#‹L™-£ŠÞ\ÔøH {QæZU¡Í:3=9'Ì)(1½{-µÎÕ䲪vž êSD.s®,hµÅë-p"Ç™¼T»W¤7+ñ>(.ÖŽÑFß=om¾Žïv¯ÿ/Þ@JXÁÔï—(£@š„1’Âm4{³¾ûÑìÝaJ¯€[˜Õù @€€¨­S ¤ûè(|TŒ²u¥Æùã¬;?H˜Ö_‚†·{·áô ßXàð²6¤((a»wј¼(qH#Û÷fw² i`Ôiô@q‹˜^Âmœó‚x>œß̶/Ê„VÀôô€ž3¶ºWU>ÎaÖYœÂfªQ¯s5UtÆœ="½d×A‰–%D³«¥Ýô:6Døå#„ñþh­ƒÉí,bªåäôc÷k|~7Ú»`¼>¨kÐ]uáâ-P©xʨÐiPJi²fGm­œÑU÷M¼þ´ cö$o¬ú£Þêùäì0;íÍ1¬öVr†”(5‚©ˆá&’Û¥õ6m#o!U(%.¦âƒþ¹­g¤‚`sç`ôptõ4"…J0…ºp—rÚË씳.œþ9aôÀ©€ÿÊŸ½Ö;×RtŠ-°‡Œ(èêh§¶¯ gdöÎJBô“ 0Ý)áxª@ ãÁéO.~˜^þ‚²†˜Çó—P„ƒ*(Òn…õ!gæ7?l?ü5¸äÜiïø[P‰¨Ö©Iͪ¸e1[r'noW¹÷•is¨ÆÜðBžr³¨F)mÁçj¨;°‡´»Â;~ÎûPkYDâ‚'–[‹Ï¤èœw׌57Zç²7ÉÖÄ¢cB¼Wâ$E“å‡`ñáâ›þá÷ÿ{{ùq¯$£l€ra¶®çÑô2{Á[t7_õw_«ÍM‘t—ß[É&w LX;˜ *åòäõ¿úÇÿWeŒp}Èû›`úN‹O8î»utkDC-5uµ€ªé1 µ¹›¥]ÚÀ,f÷…Õ½«ð-ÐÕu!MºÈLÎ^¥Æ t>¨qTíðþ4^½Ñxý1"u %á­ov¾-Vš {xž^ý;~@ª47¼Õ—ÍD÷û’?$ÍôÜMkõ6œ<'Õ.FF¸TýI·²U¢Æè¨Ð°:ƒÓïÄ`Q£éÈÎH÷ÇãÔ—³fç,\~p§¯(«_J›,Ç KQ%‚9åLA瘃+€Jgp ØU—›ZrÜ\½kL_ñ#(:p£Z|Þ åÝ +m­]kýµ®9ª&'9.ú¢Àâ|hDËm—0ü8¼/ ²ùÊ‚Så–tVÏÁeSF[j³r3=·}û7§_ý}”fÝæèö—f;µ‡fÿ +6šs‚=Ýi÷¡X´hi¶w âJ¤ *Ž€D²g¬3EåöÓyX“3ˆ–#Ý"ÓÀ@´Ë-VØ[Eó7œ9¬2 §{Ã{àRû˜Ø¬PÎ^Ø/Ò6œ9……Š‹>ü‡ñݽӟ9ƒçeºù$Ï“r*´öK".4_¼áMûµÓ¹à½°Æµhf§sf´OÕÖ1çÏ*\X Ã2©>K€È“ªÔ¤œvïô15=Dœæ¡Þ½¹àMP¥Ò—5ûAï|¼ûIX[‚7-ó Fœ=ýc)Øb µo 1möó×ÝÝ·“o`‹¡Ü(5FÅ*§g+p¥)]Ö³pi´N÷ëR•»Ï\K8o ²1yÛ>ú"*áF¶"²)oöÒNÞ˜ —RëDï\*ñq]ŽŠ”®¶6Öàjpþ³äô;!ÚR6 [~źÆé‘Fßè^uOÑ;ýÀE¦& ‹1¤–¶/ÝEš¹}ÉØS°º€öÏ +LUÖ):•ž[\ðþ’4‡ å¬Î–0Új4o­?ùÓ·^Úør#cðѸÑ5ÛGñì®& ˜ÝáÑ=­…Ëá"r•q€ýQ®ñôÀ¥¦©á¢ôs¤—#¦¡ïU%P&¸’€ý•ŸJÁìK7rH +;U©’6SŽ« ÒïÛœ‰ÎÒî\×Ä–ÖÞ™Ýs`v0§{ˆvˆù”µTÃecpÚœäXÆ&lk·³¸Ë¸µ½ÕÚ§Þà°ø´œ;zéõ¯ÌæJ2ºÒÂôä6(êæ(nU¡i¶Ž£ùÛpõ¥6¼£¼u,‘^€- j÷Vn_HÍØàeÈFBïCT¸àþGp«5„ݬó€l:¸lΙ€TÆ 3ùTÙŽ¿;ùø? î~ÇØsxLi3f_€ð†0ívz­ï—j÷œ ê—ÀPz³æˆµÇ¬µÐÛ·fÿ7~CÛ“B€ï¬?6ç´öª‚hŸ)Ñ ‹ÌqŒgó¾uô é,jrUðY¼uº'¤Òf×h¯ï~}úþï”ö¶ªÄ)PãµÚ:©Š1àÎüéû`ùÉèÞ"rrPJ¤Aj™ô +^ue+6ÏY bÏeÒâÌ>˜ýùÕ7­ã¯„æ9¡Ã<=\n*ÁWš¨vƳ’“ÁÙ÷ˆç?Ö„.袙¦Óa])ð˜Do‘ZR¦¬ÃªT@uHlZï>+²YD¦Ðš»dûUMi ZŒ¡ârPI/é±ó¸µWV0©Í¦Ô¹Ž -ÎKAzåCz‘‰êRЯ1} ‘î×T֞ċ÷Ñò+³{‰IèÛ *H]´†å>˳5®Ï_~ø1Ù¾¦} ¤ôÔ9¢Û»j ¯%Éñ·“ËßâƘ¶—,@¥=«ˆqŽ´s„Åšƒîêýüú7Ë—Û>ù¡Ä5³ø •`7ÜRÞBëÜ@–ºã`:@’AùdqÒ’6ÇVç¤le‘ö€}ò¤Uù3^Ýÿ­ ÈTÞ;ÓpüUúà»[‹V¥ÚÇd3w|ÍF®yäÏßÎ_ÿ}°üXùéLÅÅÀŒ¶þà…ÐX«É9Ä›k¶v Û@ç—iXUfL_S˜ ëÌikf¶OE†¨]Ì)Í“ÞéÝÏ´ä’¶§àŽwh´VÁè¬sü^ëžµ·ŸÆ׿ßüFnäB¥4§/“£¯Ùà˜0çR°ÁÙP&q‰Òvÿ¬wúóí‡0†ÏR¶2J°–ƒ© ýé¥Þ»åýc§GHpµ“ÇMˆá¼Õ5’#lf÷ +D¾)þºLù™ºRc¤Ò­²À}m(L  ­µ“G ´÷ʇ¸ê¥y09A•ŽÚ>—o{ÛOZsS¦4TpƒÁi8¼y’güÁÞ–AäXKÖš‚\ìÍbšà­ÿíÞMÚv9:Å”a…‰Íø\v… öJ"4!=pc +ôWåÛyÌ>(‹´+ÞwG¤=“ËþåoÖ¯þ/>èÑJÉlo'—?ÞÀ,K¶.Íþ-pA‰ XcКÜt/:ë÷°¶Eº ¯\áb ¯L-‹é̬ѫhõÁß‚£a!^èd|–^@nžÒÎ +¾Ï’v‰q1µþýÂä…;{ãÏިѼ<€?¬3ì‚ÚÜi­3ðæj´RC`ÕQM +”Ö¶süÕòåow_ÿs­“£<eÒ0ƒ‰ä!(3p3|î _šíó"P%=<(D` ¤pŽªmÀ4»kt@®oÓ묑ƒ}³º¤=·¥´¯´Ö‰äM$¤†Sor“ì>ìÞýñèÝ«„5¯Ë]ÑsÂÚÎ[ëÉuÚÙspMy³Š bÛ Ô–Ó9ñÓëu¯éÆš¹ƒ[#9>•ÂÙèü«ô8 ß‚[™…ÆÌ)¡öp)ÎÕER¢ñ•?º±ú·”9© I•ƒäìÛØå‹´ŽÑwûWzç‚õ—˜ÜÏb€iˆÝ[ |¶ûQ»dþúƒ;¸c[æ<(ÛÁæ½oÿâYÊîù‹wÆà•lê™%±Lû ÕpƒŠMDlë­ =>祓¤çFŠ„M©]P€u."ì9 )'¬ÜfÀ;¸éadàµäø¯w!z ÞšÐF7]ÌÎ?b|ˆZ•bÌráQEîäéFMî rGòæ r(½É¦*èTðNHmZ;ð\§œ1ã-kX×&X…«O“—ÿ}´ûžó—PPé¨Úõw½ãôþ+ÆÛÔ”^%½v¥OêIÚb><>…e-'‡+¨GðJ%Ö›Ï{ópžþñ5(ÀªN?õvoÔöœöÆ´8öVïÞÐF"ºÃ2ç–KO/ð ¤[¢ü*VhŸÖÒÓŒÛWÃÑüöçæà!ãÌIW«|dtN¤è¨.·ÁÓK¿ºÎø¥Òļ* “^xãM_P®¤è¸»M?ŠÜ[¾GÔA %AÓ4èÐÖf¨´oµî+¥u¯C¨-Á,ú£—«—Wïåö "µò”A˜`Éý2€\ æ;Ç?XÃK,ýhô úª1¯«í©×¤–ÑKÿ¨ŠuOKüAM¤”^zªQïUÕ.ë/¬á-¬P‹ÔU®YecÚÒÖžÎù«Æì=å.3„]†¢î_¹ã×àíî‰ÑÐÈS¾Ýþðo‡·¿ç£cÐê9Ä”›öâÔì÷ÓÊY–2&Ó·ÿ°xõ{«sóÆu³2Ó(³M +¬Pöý‹_üÏÞø­ÜÚU@Wk½šÒEõ!é¬Xo µChC·w&ã*k–`Ç™&­NŒørxú3©}\à}p~ôòw„5b¼åM*jHd›Ú>ãýYÚZM¸Æ¦süËþÙÞä ©÷AQJÛˆV~ÿÜè—ÄäŽq}ju^6ßÛ,n—Ù ý£Èõ×þìáN |P ]ÁÕ„ð .gê2¡$Þèu¸ü(Ç'%Î߯«¨Ô‚ÛA]«‰m1Þ…«¯V¯þ4¼ùQëß–EX@“·§EÒÏÔŒBz±zׇöàÅù×ÿ¢ö˪ÒáÃEsñ6˜Г \ïæ/Çørcåv/09OBrî<œ¾fï0c–‡ÑîqÀ›fÓ»¨ÖËÀØÞ§Vˆt0¥£§|‘~øüX$pñ2ˆ¥æš²ú|°4ûwÍÍ×Ù›šÐ:¬9Ô¨2aÖ9å¼lU:¬©¤ÚµÚgFrî/‡©þˆùl]—Óé‹ßÛã—jó\ðÖéš´6½Å&:_(ÚÛÈÉ?ÿºúµ}Q?+ñÞÙÉÛß þ8O:Ÿ…åkɨe'íëžv{çÀé  @Àì£&nÎÛßý|ùâ¸ÚrÈ”IÒ‰ Vö{E©ÈFu¥Ë°z/ygRæ¢xýÕù§1¼øµ=z•§[ÿ×%cõÿ÷þ¿<´ñÈC<´ñÈC<´ñÈC<´ñÈC<´ñÈC<´ñÈC<´ñÈC<´ñÈC<´ñÈC<´ñÈC<´ñÈC<´ñÈC<´ñÈC<´ñÈC<´ñÈC<´ñÈC<´ñÈC<´ñÈC<´ñÈC<´ñÈC<´ñÈC<´ñÈC<´ñÈC<´ñÈC<´ñÈC<´±ú?ÿ+qæ¿’ñß +ÎõÖ^¿\6ò?+ f ?wÖ/^?ÿ,þ¬°ÄŒç/í³£—g7×ëçï2"Ü…£$Á:ÇëËLùÏÌÀƒ2­çg'g×pg÷h}y\ÉÔá¡üû Ïüþ¿Ñ»ÏŽã)”â8’% ‚¡ù Çó4JS Ãq”@2|æêÿñA4’¸Àq,¿c3—郘ûûç'áAÜý³Š#xš ÿ_ôŸ½ÝåÉœ.?;ýløÙõgÉgB¦\ÉŒ†Ÿõÿ£ï`ÝøtÝÌç¯^œ¶×/aI®ÿ|§y kòŸÜ]î__¯¯Ž·™û{3pw†$Ù +¬’ÿFo>{õŸ.ž à›s¸ëM†À3ÍÌtŽg¶éC;éÄ&,óçE£Y”cÿüsôÿLÝÿ?úÿϾü5¼óÝxo"}ïû|pÏ.Å?{Í¿Kˆæà}†ÙǯώŽ;ž™ÅŸMáNä>Aþü~º_\†ùóÒ3™y†Ä ˆ„MŸ~ú_ô„t>ÿþIÿþÿ|†‚9ÜýtéLúDü³Bá~†i:‹vŽølÔøo(£_BDn²ösÇœ5¤ô¦wÅÆžj÷Ï]ÌøX +V¼?CÕ¤*FU!í B=Úècb$:%œ[Ú>Â#.ØÐÞJjžÕÄf“óˆPglÆìJÑRKÎää\hîèÆ1:%®a¶×r4'´®í¬Î¥;|Áú+Bqá–´&ˆÔ®° Ò±Þ‚÷Vÿ³FÜL[Pë=˜|™öp9Q£7¼UÚ;¶1¯ˆ!®wP-fÜ^‘wig`oÜéÛÆú¥wƒ[£ªÒªˆ‘Ó¿WœéK{xט½—Ûç¤7+‹ëN)kˆÈ­ãp|B‰8»Ç؃.ª*lSÿâÍ’ÍLmcJ[m‡ËoÑ;Æ]Ö•N5íÖø Âù.íABø¢ô¹[ Ü,–¶¬ò´Í’;BÕaŽÔÖicö¡{ôiy÷ ÌlU¥†Ü<²Ç/›Ûïœé[&Ü2þŠwç'Ïÿf|ùC]ï–X_k_$Ç?‹OLcË46¨Ú…•DÄ—Ûðâ÷oÔ´£§ÁFdêJÑÒ6·æV›p&ˆÖGµª0cLXƒÒ}3$Æ[U„´n… +\P¢Óæj5®Ai *EÖ+‘NnÔáW¤ ‰DHÝÖèy0¸¼:¬HÌ<¬){%¡H:d‘9åý#˜ãêXöOÔ`W¤\DhbrB›cÈL…ð‡¼·6Ú¨Ò~Rá~Z¤!3Ew®GÇZ°E¥øYY(âNM?œýI:¬ +„±ÖŒsæ”Ò9¨J0 Ljå0½@Z°U¾•Åý +A–¢ú€ 15)“fõaê*.%Nïμtz/oôo>þò_¯Ÿÿü/jJ¼KF®ßsÁªÄø÷EÔ®ñÍí¤óÍ4 ´ÖVÖ´ÖáµþÑÍ/µöQ‘ó3¸ÎèƒÆô½;yK=Rmqó¾ÕÓ–4F¨Ö¯«Ú[ëí üBÖ¨èÑ*˜½Ðº;Ú1ÞöW·Vïrvñ½Ù9ÅÕ6gÌîµÞ¿c¼5"u©`‰p¥]‚ ªä r À4ƨ2d¬¡ 3ý8øãj›uúu%ÆŒ¾Õ¿íŸÿvtõ›Îé'Äh1v_ko•ÎNêœÐµ5|ÕÝ~Í^wŸ‚é]%íÔÖÛ§€]R|BØsÚ?&Ó—í‰Þ"m‹™9ÂΡ§ýNÊ”»Wò˜I¨]Èä<ÛÈñQMaÆ‚´VZï9n³„무ÎKlÅíg5­Ä4`g«¬WB€)øŠˆÍÓ( z¶¦æ`¡Î†z°ÝY¦¦@RÁýûe1S•jR™ c$7ÏøƉà­k½y^嚬9JÛã)]TîÑÖ‚4çJ|îßæðI™Í +À 0mŽâ +`ÚU@<®ù$Ï<ÉÓE\Çøˆ6f¸>©ñí:³ÖRñ–€’3ÌbZsòTHj6³–¨’J “âýŠP&mBîÎ@ëcÁž™P¡G_Ãÿe¦.† wp †îéãºÒô~rù+÷ë@šTïÖÒ6i#X-οp6$ôŸ¶`Ùbú0K9ÚM{)E3Ù¥@3ÒQî 7FŒ=Cå¤B»^ï<Ù~itOœþ‰Ú9f¼©Ô¨ý±niTø„w7JóLðµg˜Ú¥Í!–àOÅpÅ7Ò~Öè¥Ú»Äí‘=¸±ûb´ÒÛGl4oÌßͯ—GZÃ"×(2.ĈºWWr¤­5O£Å'ü6muÖX¦=_%€ñ>L @:‡¸YLûv'u±Ej}ˆX¯¦öPcJ˜KÞ?±‡@ ÔÓÆÞ7rë,Oy‡˜•ÅÚ‘éç݇ˆÔÄ•à –®‰Ÿ©HÒ-“NÕë**¶(mP¢üCÄÈav‰ôFžØfÔÄï-‹\X EªQ¢ahÚ.kiSâCø-®YïP =M<Þ-’&$v• ê\H)]\jï•ÅnçýYCø,#,@_•kñæØKN¬x)Ŭéç »Ì4c"FWvÿNoŸT¸÷rˆzX—÷Šô~Y`´^2}©4w˜9)¤qÉUÎ|朥ŸÃý¨6ĵ¾lDgXcˆBÒóÁVO®ìΙàMp96££îö[TéVù .ÆŒ3Nû½pfP\Z|âoQ½³éu ^|.ùk`¹½Œ׿úÝÿtüþwu£]Wc¦±›l·Áäusñ–óÓ^Pb5>&ô‰œªÉs5¹SâK©yµ#Z­µ+ - ë2ßµÚ<‘ãS!ÚùƒÛÝ‹»›÷EÞ§­±Õ»‰–_FëOÑú£Ö9õ:çßýõ¿4F'‡´T‹SÜœH î,U)” Ûͤ}F;¾·* ðÒ¡Zµþ“2Ç!œ­¥ò„WåÚ¨Ô×íÌŸÖå,jׄž]jÉÚºG˜6€ E[“GÕÚ˜C•"íÐú·¦¼5ÁŤÂeÚf)`z•m÷eQ󠮯€»Zu¾Yf‡˜º_??Äjr³ËÐhÒ5m¼Íƒ$kî!r–4az´JtʹKÊòCEƒZ†Å¯°àÌ~EÌÖõlݨÒ%õËH ⃪ðEzV¶Ò^Gð‚Õ´c% +bo–˜S{·B°…Taí9¥‚ÆnTø§yªÆÀ³’C\ßCõ=Ää¬é`óuÌL@3¶ƒÀÀõ±7&ï‚ñ;W¢ÙnÎüÑ ãÎ(kŒƒh·ûFsš3Z¾/³‘Ò˜‰Þ¤*Dð +,(ïr Z~ôÆw ¤kl`$'°Ë,€’12:é‹ÁîÓ—¿øÇ×ߢF‡–RëTješœÓîôQ•‹€Sôö¨/ÞÛÈ ðÓ¦bg‹ÓïGç?”„vEH(k*5VJt$B!D[¯ÝY¼Õâm]i3ö˜±Ó¦¤Îðe¸üRiŸ8ƒ«·ßÿ“?¿ÛÇ @4TS´Åˆ +™ PÛð}71!J›ÐÀyÐFÓÖ „ lDÓr¨Z$< ¬½Ø 'oÒ^õˆy‚Êïý´ 0µ½à@uk㺠ê^…Ë"r‘0ª¬Oë}Ä€=¬©ì­ëB0ªÎªl"fô ÁRr¨™ÃÌ:<iïQö‹l ó¨½_Q3u4ƒà­ái“§œ´ß°Ö¯ðÌ™±¦à5 Íàé”Þç¬I‘ò ¸UBM˜•‰@tåP£ˆYÀ’ÏJ,@ÐAP^9ý +s¨0 ÈF=é­STn¼'!eÂÚŽq%)âä*L’T:¨í#*Ô eïû²7³¨šZBI­{!'áüS{û½Þ>ç­¡â f»¯ºÛ@¬;ã¼™Ò\»½³xþJimA~háJ ÒÎv^ÿÖî½°{/µöÞ½£œ)šhŽâÅË"ç§ÍI¼üØ?ùÙèê¯W?Ú0›½ão¬á]Yî ú€²§öàΟ¾³&E;˜’®Ò¾•| ±ÆvÿÆ¿ì¾A^‘‡„µÀAEXCÞñ>$á©ï(kì +ø ý„Þw·ƒ³Ìá­Þ»¯P­›#]B@úÉRV™ @-î2m†Í™º&¯wAý ª7‘ÆPˆwWÂJ¤‡ò Ÿ;”9ãî…(|’Š ÷jj5K\o?Ö¸vïåð´+(ŸLMâöšÑ‡Zc_ˆÞ¦Ì6‹À}\LjCR€jE¹Dþ!¢iÖïÕ/¼E™tq¹S$ü2éWh E°Æ`{¥ô1-Î]@òC&¨Ñ–wRZåÚªÑ1¬!"¶r¨^$l„oå²p D]1 >4Û¯Ê@¾EÒ†[,5á0©ࢎ”Ù]d k¯"ìÙƒ’Xe cÉ_íf0 ƒý2ý“ýžö¾@-pàçR +6R¸!Ì>P<)·5oÞ]½3º§Àzj|B{sÜì€ZÖšSÎi‡é±‚Á¡ö xEFÖQú6FÕ>ï-œÎ…oòŒM]Ö¨ñV–B´Á)jíÁíèâÖðìí¯(g¦´NŒÞ­’\IÍ3°ÛðøŠAvA†ÔĦëÝçb|IxÊ;½§=Té%iöÁ,ƒÖ ŔѾ!õ^™v¤`IƒƒÓ{¸5Âí x^p¬Fë¼1zY£2ߨËq]nÁ öîµqJrfnç¬eXæa;àõi{ ¦»„9%ÌBùvg¿Ä–H›6¦J|-g¼½ ÕQ>­b dÌ^U|Zæ¾(Òe<¨Æ/Œ[²˜~2¬$¬¹èÍÁŒ”˜¨Ì`xgÁÂ$å–b>ø"Odj¤&®vP tB ÚŠ7¯ÒÎç‡èaMÊc€x ´ûŒ5Á„«†Çj´«ò!p(hæ´®ÚƇTX"lx ¥ý2÷´È€;¬ëu®… +IŠ“(àdXÀõýªtÑÜŠ``¹Üq{tsvû뽚ô“CÈ‘^·À²Áv¸½kZ ü•pYâ‚ bæ  QÎU8yÅzÓ:ãÅóR{uHª‡¤•c\x\ïõŽ¾éœþ‚6`¯œþ¦µË  ¶’Ò‡!¸<yPK„FÈñ^…݇ͪËPø€r¨ zÏÏâb]Š€j\Tã€×üêä0µ#Ε( n°P5R±L»ÏR÷ÔÜ ˆ„ïפl/lbêAÈá|¥”V…²%ºÛ +ïÁ^TÙ(…5 &¶Y¥CHÂL…gµ.Ê5 +˜‹#B‹PðP€É”‘)4O×!ÿó˜Uc#Ðÿ‡ˆTÀU„ƒÙjàïžä¨½ûÎÀŒ€Qz´}ûó…(­'9"W—@%‚¼ÏáPažöÁ¶§ÏÛ³«½Zª3¬ì³JŒ‰€Š-PJÒY½Æ•©tI9©±°aS0Ñ×Ã(ÏLM+âÂý»í¼3N¿ê½Áî+Ö=-‹‡¨vÁ‡¬;o.>€à1£“wÿì€P +„K- +,m?…ÔR››d|süöo„æú·JŒ6ÆV÷f|ù»æækÜLU“9¸®*­²ðcý¥–\„˯ìÉËFçôùÇ¿³†çŸ™"á‚⩨6Ïg•¬?T•È`àîA™ëÍÞ:&eÕÞäü[Êí|ž«CŒ˜Þå¼ÉøìgÃË_Í#3\Í/¿-°ÖOsèÓEi]˜ ð‹Ý:‚2¡40ƒÑ“qPå ˜ŠÜûbÈv\ƒŒ²Íή,€Ê’á¶_%Ö Õ¡ìƒÕ7 ráOóÔ9âY‘àvYËX³ºÔ¬JAMñ¤LÚà¶!¡Ò.0Â…ßßeýþ°Xv ¼[Úõª¤Ë¨]3Ú@zÐJ[!DÀÑÜL‰,}‹f²xEÝge¸Ê—z œ@SœŨ±âà›}Øʺ’««ðÊ9ÔÊá6è´ÕÍ/r”‘«òæy¯Ä{¦|J7Q¥çoŒÖ&S¢IÎf•F¶ž•¸fÑF_ ×`Ûùû¾¿©5ÆMP˜ Aç3FOò§é‡u ä"d©¶`a7iµëu.@Õ3ö@CDh@Í–¹®uÀùêÑ’§À9´Œ9gÌ!xÊèãj‚)qgr}ýõŸ´ä¤6_jƒ‘Q#9æ¯ÙFŠ9i§äpža,Æê£H6DMÛ7²þb²ýðÃßþ/ýã÷û5™3@릇Ðq“u¶zr«ÆçÎô-bÏ>Ñ‚-z4†Ü‚÷•ÚÉ윕8w¯Â +16h]¯&ú„ÙÅÍ¡w†³ç¿üã¿­Šþ“‰K¡Û;!µNž°ökêý‘ÒžÕIûæ× bXç«J¨Ôc‹‰mLJ€þJ”çvv—ïþ0Ø}üÍÙÂÜ lë8ATéB~¢jºtT‘n€S;¬ò (iµÝ:æÜa7€1'Ÿ–…<0HæP4‡š¿€JËOŽ=÷@8e‰®£l–Mpgþà„1”yQò¨RÄÌ<¢O·Œ¦ù#=˜¥‘–¹½“­«Q… +¤“Ã'%ŠÓFpXý—ÏÊ{¢L˜”6eÌ  4<&S*¸YëZb**€ŒïsÛD•>”ä“wPþµH%ÁÁ‰Km€ÄL-Mà,ð2ªár„KMÙ†Ã;©µ#Ì"ÅHº°.D„)-H*Þ*Í ïY›cjÇêœ9£kÚÐæ€s'¤d7´“cÆ›R^w9ÁÎÛiæHƒ2z¼;†u–[;DïQö¨"‡êZÏHΚ«÷þêKµÙã" p¥fœ4³¤IÙcê”5ãƒÔ˜ZÝ;«sâªH[u1¬².¬[‰ +Ê ¸¿ `ž2ŸU˜/Šø~…!`ž½òÜœDãó<¦¹H.T ý¬Læ0 –B‹Nú‹ªÐ'ö'OhÈý)TÞg´ôúσՇC*U\PPð²™2“©p™*!6æZ²ã¼E…j¬[ÄÄý" 6êºÌÅU¡ÍÚ+«û +7†™ºR"M *Ø  ÓŽ*|‹q7îä-àl:"ÆLß//+(åT›”ZîY€½xY¾QLm¹]曤އj"¤Ö“,š­HÖ˜´ÍóHÇš”Cc*iM@h[qRÀ6Š n “èôÄGpƒ‚âݧ2Sª©q® +±ì­Š„S@ ÝY<+B"™²˜¾;‚a|VⳈv˜Y…ˆ„o³Æ´J… +˜•¯Cjµ*,p=¸ã V¸&ڊެƃvmÊÁ `u]éÖ•!nÎÝÁÛæú[gú׺Œ9è¨ð>ä0ÈÈ{ ЪI`í,ª“˜Y™óxg(¹±±qG¯ÌÞ-f 0ƒr¦´3<„ºVz¯­Ù×jÿ9üª&¶q}HXu’g <œ5ŸÔö®*`¢ –æ¶_âÒCÜRs7œ»ÆîA°÷ܵ„ñƨž)v1¹`XeÒbÄ$6­2kƒ1äýŸƸžž)·ÕaìôPD.Œ1¦|cKj#J@½Ÿ–q5Wõ‚ìCvñèIXÊ¥§h #“î©|X•Êb5Ë|ŒicÚZÔÄ^UìQæ\p5ε)¶ì6iŒkBLëC½yZ©_q.Äh·„hÂá­*÷P1©²A&M ‚Pû„6b¹ŸêÁ&¨5Ò/ ú_ ñ´ô$OF;?ÉÓ@vû÷§,1¡uˆUð¨þ¤À7ÉãN÷Ê”ïuN$o +ÕGò *È'Є‡U2¤D#ÃÎó§öYÍÖ”/ +Ì_eIdeÊ+6lAÑÒcJ¢+Q\ZµÊ„Oëƒ"iÏ‘æ­¹Ù:ÃäL@°FÁè +UãôÀ;<±qbt_Órë¶*Ýë_ñ0%P×䬑¬•è„³g ò³¨!ãpx[ñL4õä29þÞ½Bõ¸?­sÅÛé–ß¿W{/0k›E#­sm_B*RZ"EK2=¿¶«I­<é R¢à-ž`Aï!R§Â·ËàÝX€»“Àã¯X{ +`KšàX“ôs„ȃ*fër³dgJê-À·2íˆášR¾.Ð÷Æ9>7:ו9$=Ø"´!xCp¦yÂË :Ö•²žI0þyÚËÀb&,]-=žÓ®,Þ“ ®„ `x‘ +‹t$6vv÷¥Ñ¾b­‰èÌq¥µWåÀŸæH›Ò{¤Úæí¡íª”ƒ±®`ôJ̳™«ª‚•^GV R À¶Ê 6!ΦG€@[´:D¸)ö þb¯ò—OËÙª‰ñÓCò'ûX¦ +/=ƒ J,‡Ùˆ˜Ôµ/ +<¡Q©ƒJB9 ÈÎd¬nP¾ùEž~’#!Q+” mJ‘b0 + 0ãÓ"WÂm³}¥wˆh`?¡!Ír˜eàGÈ4í“-´±}€è ÀÀ.`5Ps¯,q[t×zr^ áÁ6mMkZcŽ©Ïj⦃áuúW¬3.Ó"µjBÊP0¸W°gZãHׄ¯ atXJyé©"Ñ_êñNŠO5=¥‚É­ëbjÓ@b!RÂúÂ[!bo½ù´¸ú>Ïz9ÂN  ˜ç¢&%Œ>¼xõ7§o~Wƒšv€€÷h¥#HN)9®S6gö³¤ñE…ÙGUÖž™ÉàªÚ:«r Þ‚î×@®…)=PÚ9Hz\ÈÀø\ÿ~MÉ“6Ì<¾dÏD{A*£2Çøf‘ÁšåQ›Ö†hê1Ãfp¯Â¶ãáKÉîæƒaIí°Æî—)(gœd½£{“§y2‚7‹(¨)ÕA™?¨Ê’¿ª¥Ê"LÄ©cΘ\°ê¸L8u€>9¤2ÕT™qHêf!³JÂW»Î·[;ªQ“fqïµs„ iì`Åg@[à " ÓlMkù,OjR•öÓƒ~˜Y"<½ÂÁ>@€ßJŠ )† Ú¯«àaYPXmõ¦M8n¨Ø¨ÉÔ…ý’­ë)Gƒ#f@òy`mPðžbü¬"<¨˜VdRA>Ä*¡Xo™£À,p¨à#œGj-l¹ûñàO½ÞçO¿¨ ¾)+ÉŸþ—ØʹÂ6k| pç]VkV™UBc«Æ§­ÕGct—!AšzR°¯ý¬ÌQbžV…"í‚ÿ£]¤¸2€<P¦ó˜j W»ÎàztúuçèØURBˆA‡2øèX0œ’jWñ¦û°¸Uâ2“ž†üQ‚•ÚXÞôÏ£r˜AêTnAæƒÚ+Að`—©`ÅŒhín2ˆVÌ$ ü2u#j“nÀ^¤Ö 5kêçü§,[•klDBtÖáÒíÑA]xZÀ³5!MÊ.áf·Ê ´Q­D€î¢?߯å#uDwF²ˆY‚9”Ýá=;§æÞb¯”n_zQMU6þÓýÊÓCR”•›´#B»FRè¨áÑ^]yZ•s”_ÓC1÷§{vJó¸.u€JÀV€øÿ˽2P-Âx5Ê9¨ÈyÔÈ£°ËÚ“ÕGh\nƒÕ…ªшÈÕ`²ÊB#U#æ°&4ÓeçSÈá ê.7Ä -ÞÆàéj’¥Ý ~ÚcAW°~QÁéÛɉ?¾³zWˆÒ+³˜q°ŸeÚ¨ 찪⮽î msŒ›A`øœs ®m(ö|zd ˜‘T{`g£["°”=éŽk} ]0’yÚ-Ò>üxÕPå &f@=’¾à.o¦ö±D 2¡05¡í‰Ñ=ÓZ;R›pî +Pî‹âÈ N;<âÒX¹1{xÔÖyûKM†­ì¨ ûæ1Û7¬ d_]„TåAÕ\¢‹ˆ½G5î1+øÎaøá1»llöÊ£Îÿñcÿtçè¡a‡¡/Ê<̾ŗ±€¯?8h´ºypw`³ÁäCZé;+Õ¿c5b‡.÷ï'Žº ïk¬ýõ«ÀK_uJ€Ç1KXâûÚúxƒÔújV & І)F7°à ÷ófº3î–Ý\ÉëúÂu,T&¢M •´1Y'W‚ç ¥M©ÓLzA'Çш•5NG?µy;d" ry ð›…‚g&ãn6oñ'ÁÃüº9¬²Ã÷r9ŸT³Ó9µøâàŸ®/mHÙE”/hûY,\¤÷Fœ˜dÈbvRuRƒ@>: jR¹Î«æÝBÅÅ—õ q–àQ=ÙÅCº±ÐãΈF¶ +}ﯹâôŽ ›†JI½ “O²sñsÔ!‚Õü úk  Ø]f<®ƒÛ„Q'HŒ¹1·¨q´ÌˆbpËv(,:0æîK†% tõo-¡1=¢@®G5fÿ¡;¨ê¨…éebwñvoc3:Ï Þë cd¤øjA)f +º¬éßu’<Œ +Ùêø§3h!3F<©.{ă#Î#£À«¶o³ˆÑþêYÞJ$ðp˯´(¥åb‹c0jÈHÑáÃR­=Xpó%±0KÄ[†¥é䊀D¨HÍ„ª›þÔœ“-1Ùà +RѦ=–°à4¢¾€ +E{ å“aäc_ð':..ðƒ÷"b]>;LMú"M 1IBj€±wÐÀ?˜S›Cø|(7ïd³cˆd$’¶`Ù%6©åä«F2zçá +t¢kðÉd|’JÎÛ‚µòV*•òE-&é¼’Æ#ñ¼ÌÃç°pŒMø"ÀªL[ú”¥Œ:E=¦0ÓƒVâ¡Þ+³PãîШCïwÊŸ‚kÚ©´Ù½u0#VÒNÆÏuh:°ûS¤ÜðIUð„VRµ3éqo䘃tÀLn¢RÕ€GCˆi°Ãe#f*e¦3H¨â‹4¬LÚââ•MxX‹ˆzoÆЪٙ4ÃDÄuˆ.qÌÁj,´Î0@š°PkþÄ^ ‘€‘7ùâzT² Ã𾶠ü‰UœT†ªâîqCéä46Ò¢‹Já +“l“ÑšO.D+„Ò‚D¯qI6:kgŠf2NŠ‘Š´\5‘ñQèæ €ŸC:zÝQÓ:!eø46¿/”Õy8!»)•ŠmWg¬dä°u|µ ßÖŸ •ìºÄ2mÓjÏ*"bÖHx¥‚XZ*Ëh¸‚yŸU[›ˆX2ú (—‡b2°AÞ-ÜlNÌLGë«€4­W4úcÀU 5ɤgì\Õ¬:™¬^Q‹H®`žŒ6 ¥á ×´>¥ß±b¤mL +hÓ#5ñÄ ©NÓÉYŸÒñryµµC&¦†\‚ÎP‰JõE'°HÛB©,<ææ¡Æ=ÀœÃã3ÓRi‰MOó™YàäQ³àb ÐY0“8LXdØè;:“Ü#÷¶1 a+w19*ÚCIJ#˜ ’JÃHjÁÉæÝ\ +Ç+7` ¸ì“œ$bM“¥mÄBnŠN–ó…+ Ǥ:e*6®Ì׈H Ne¡×DbÊ«tp 58µ[˜>ƒ)Í{&TÏŸDBµþh³ùþ¢å@¸qáGl¤Å'âá¢?Z ¦{bi‰IM9…¢Ù•R… ŒE­d_²Í„ +þjÜ-:˜LßFz•¯ÈªOõÁÔl¨´Êùü<`¸\l*ÈäOØÙ ©c‘†X\tÁdyÕamDÁ[ö—L]“J˜–ËÎ*õ­êò]Ru —hsO®®%&O©S§Äò"¡Ôñznr+?½k®[ü W0‹HU·XôFêD¤îa3RjÒ„)oĪ ¡ªG¨RÓñÖv¸¶FÇ;±Ú“hjÜ ª +.×ùì¬XY«kv6¯Ee¤a äb!d:Þ&¢u±¸”™Ù¯¬_çJ+¡ülkå"iŽ¹£XdN +Lâ¢Ú;M§æŒxüˆÎ œiõJ˜˜g³Ó~u"™S{§ÂõMÀvPm'«‹v°åý D¢É—ðpU¿:Ë•6”æI_¤%å欔 +ÎF©@|~-Þ:ÕÚ¸wîäcÍÅs­Õót²ã‹TCåE±²D¥zRy!?s®ºvNOZÕâ: ¬IÆ:ÑÆV¢}<žloÜè·òk I*µ€ÚUVüÙÅ@qÕ›èaÑüÓ€ CVþˆ%Ò>)w÷“rû$›ÏOždÓÒØëjëDaꌃIÁP©|‹GZŒ: ðPjÛ±ÖI>¿ê -4p ¨| 9Éüj6Éxx•«–`‘¡D¢'—a„ÕÞI¯X2{;!›\;•eâ“™©½xw;ÚÜR'Ï„+˽õË@à6¦_†‘æfyårkó®ÎÖ]µùÓL¼Ž‡ËRiÑÌ‚ýëß.v²ÓçÕÎŽ\[+ÌìCá»9—¥Õ ¥¶‘9™ÙKv¶gNÞÊL‡A6‘Š›Ëê‚ùÅXo7T]ç2³j÷¤žT †×65hòºƒªR[«¬ß]Y»:½sëòo‘JW&só×Bå { èd+¾h×ÉWŒ„:ââl´:æ ‚Ô›™Š+áꯙJ*Åù_x‡NM˜ Z©ç'Ï0ê¼?¹ªï²K ìk×,LzÄÀúÎj:Ú8kJOíÇ[[ó'n- WÅÜ|qþBuåReùBfêdyåj}ëfyúìµG¿+×VíL‚Ou¢õMèHjâT~î\yíZºw¼¹xº·{ ²ùÉíúúU¹yœHN¥göëÛ·•‰sT¬*äÚ¾HÁDÈh¸F&§Øâr°°mmÅ:'˜T;Õ[ Õ–ˆhƒJtœ|–VÛÁT/œ(Ì^ —7¬t eùì$ÊaR xhºÌ–­“jg7 ÖXµŽË&5!U–Åâb¬¾žénÎìÜh®\¶ôŠ˜¸to·4·ŸêîĚǥʆOnU&OW.°@ ‘èìF›¡ÊjiáRcéê‰+Ofz;z"îW'éôÀjL.rÛjï¼O(Y Œ0i 8©0C%;BqŽIõøìT¢±G bPøP~:Þ\—H¹äb †éÉ3þX+T˜ ——ÐP9T˜WkÁÜŒ¯PÉžÚY—Ëó^1+×WèDC*Le§vaðÙL/ÑZ;{ÏóRqÆ¡4{¶¶~=5³il +ù9¯Ô«V7fNÜ)ͽ*RL6¡/g+Ëç²óç+kwCXc3aPLf)¿pWvæ’XÙëÛbi™MOÄÊ ÉÆ¢ÖÃŽySMLÎ4Ÿ›÷Êu\̦›k¡ÜŒÑÇãliÎö•—ËN»£vÊæWì~™€¹P'6¡mñî^´s +ä2˜˜€Á ¦Jm.\Y +³À*µÞÆÃÏ¿ ãcð…¸d»¼p7ÓX¹yû¹»žù¾# çÎÔÖoä.Cý––®–—¯Ñêäö™ÛW}•I·¬~L‚Š˜S›Å…s'îIMìÖgvxêõhk0jo)õµhk»»ycýâ“Ógžás«ÕîVsþ,*|r_‹iuJ*¯egηO<0}öÉÔ䙩Õs͹]DȬ3Sd¬å·¥Â\÷øíÎɇÄÂ" nA®:ƒ)_¸ì•*0b&ÆH¦ÅÜrzò”ÕV0NÅa瀯Èx›J´²ãçïy¾4sÒÁ¦SÓ—âÝ3rm£´|*O.3t2Bj*Ý‚‡…TW*Í7V/u¶®ççö¹éf ¹æñ37^DùT(ÛÍMžÌNõÅ:Ðxq)TZ¤&¸ôd0Ùw¬0ÑF¸¼ +DQ_º¸~é‰Üä©é•sSë°Pþ\©¯æK ç—÷Ü¿ïy¥´0¹¸wñösðV: aܵ\;•œºœš:Lt¦WNoì]ÕŠÝÆ•ž˜¦Ó B~¹ºz/›[4á +è•UÀ?!×Àör‹0nriáÔ§§_ÕyEàO“4w¤ú–?1 ^ÕÖNÞ{éÞçJÓ+¥ÚªP]—êÇ ‹WÁéÌý æ‚ aý‘Z05•hnefÏ+ÓlaÕéO–º»”RÔ"$*¨áòb²µ•ínf:+å™pûƒ6œ”óñúv¼qB®¬ƒŠ‘ñºZ_^;s÷øu+©Äk Bi)=s.ÜܦÓ3T¬µ¹wÏî]ûc•£’ˆ´ƒ™iJíðÅÙÌô^¤¶Òžß{é;?ËOí’R¡³riþìc•Í›­Í³{VVorkûÔ­‹O€ºß€75¶"Õµxs37wN8…+õÕ“7:Ëg]\:R]‰¶·¹üŸ_PÛ;íÛÙù«ÁÔL½¿r,{H‡€ƒ‡Ju§Psp⺑ò’œm§j‹6*J*Un´é‹Ô…ì\vú48O°—©™s ”n6IÆê¡4à’†Q‘RqM‹Š&,$¦{t¬‘›:Qš?#”—œBÅAgÕòê©ëÏú£¥Hy¶²t!5±Çdçp¹LD*¶`ÆÁå#•5.5eÆCx8GÅ› TM¢H´"¥E¿RoNï´æ÷¼b>ÝÙ,/ž+ÎífO—gOæ:k\¬~ý¾§ßxçƒÞæÕ1h§“_ ËRõD¨²Å¤fœ”zæòÃ?ÿV¬0á¦Õî^¬{FØÏÍßUXºAÄàÉl}þœR]±4¢Ò&_ÍOŸÞð5Ðn6ÙJÔ–tÁÉåÁÕ¸øøŠXm}öø͉µ+>!Ÿn.G*ó6e¥R_ $§#ÕM¥²nÆe^© ÉžŠÁŒ€†’ê ¦t.ÏÅÛF/7bÃ6ÊO¦w{Û7äæºË³RƒÂJJPt´&–ÃõíäÄNuùÒä©ÛLf +áÒ‰~Ô-‚öMî>X_»kmÇêk©ö6£¶õ¾Ð¸‡÷Ç;6:n§c|~`š[œ;›>AĪ6¿Èª5©8«47’{±Î®/ÞsP Qm-@€LÍ&:Þ`Êê ”zjb'ÞÙd@AN€Þyø”˜›¤Ô6*Ap«brÕJEÇ<¼Ù+­íÞ¢“½;ÇÜV<ê‹às\às‚y •‰R­dce܌ז[·RSƒ™•ÒÌ¥Ääo©õµÍ«O»øìðW;;ú›>¤f ·›ºÆå–F‹U— CÁLegÎ.õÖouW¯ÇëÇ¥Ò +)æ–vof:ëwì(°ä)JgÒóJs‡IOyØøÂÖ¥XaÚA©˜£ºHÏÑN 5§Ô·µ[Ÿß—!T~eÒÜlÊ„ËT¸¢Ö–ÒÍUW0i%@€b&oê²ÂpyYÌ΄3Ób¼ cUèíD[ljx²ª#XÐ{#hðp³¾t•Œ5õ^É#”ÈX;3±—ž8 lï¤ìH~fó܃nšng²³`ø›'n×W.ÉÅE'ܹôp}vg¬H£äÂCü”ÇV‹ð$Ð`?ð +ˆ˜¢UÖ”æqŸÒÔ893®`|–OÔ†,˜ÎDÄ’GªC¦p±Ñx¤>b%­ˆƒéf0Ý{±ˆˆµ¥â›ìú¥\º<×Y¿,g‘P q¹FÇ{dt"˜]4Â48£åEµs\íl'z;žHÍèWh¥*å§>^δæO=0}êáxç$©4-„M‚Pæ‚„âäŽ{Æ­L²Év Ýõ';lnÚÌÄÜ\²0sfÓÃgCù¹ôÄ™ìô^¼±J)ew0¦q†Lä¸#xÌà2ãÑlo7ÑÞMN]™ô„ÁËù#•xm# Î؃˜#dZI·×SÝíq¯`ðŠœ:P·à²G*;¤:$t—##5*ÚÐ#B¢¾U_¹.WÀ®ä{'øìœÎ+±áÒÊî A­ ;(6¿ 7v23—ÊóשY—P±2I¿T\ß½)§»LVÊKõ… éö ©¸ ”>?”š(Mœ‚ŠàÕngãVnæ’™§•6ë B zW&¤†->ç3SÁäD~âdmùŠW(åæ“Wï}6©PÉÍfñpƒï]ÊMíׯèœA%Ó­/_DB%=4";)û·C6_ÔŽE ®`{ñ<(8„qÂê1©¿ê [¨ùäö1#ùµ£f“7BE{‡ÇtÌç Þåó›™^¹Ì—¦Ç}¬Ù/[ü1¡b= +8Žë¢ìñùE&3mõǵ.ÎEÅ|BÊÃD¹•hã±›Ÿö`ÕŽ”lÏm\n,q£v:ÎÏ”¦öÊsç#Õ Jô) +‡ñŒça²P±©nÃŒgö§ŽßÛÛ¼é ¥“…ÎâîÝ‘òÜ8"öoú¢.6/dæBù5h€Ö-z‚“O³^. ¤ºr5?y +âÕôÎC•¹KáÌÄäòÅÂÔi4T„¨P‹ˆFŸLGºh0?î Æœ”›‰’‘´œJÏš9'*ÞöA¹…Šf" ô›jí† 8p¯ƒvŽZI‹7DŠÅÁqÇáSŸ%Ô^´º®6·D°ßTÜFÆRÍíÂôY3q0q¬ 6sH0®GùÌ*ä]Á´ƒI»¹"›žUÛÅé}L(ê=œpñ–Î0¡¨”'—ÏÊõ™qŒv•@ªI©u¯ 3’†FšÝlHí1ñ¾–Y)Æ\p6"|äì åaÐf·®ÄêK~µásÖ@ÜNÉd8§Ô‰¨ Ê‚ÖÄ+ó~¥dð‰Ç¬¤G(RàË«BzZ²„RÔ¶Êsg³“'”Ê-|Ábgaaïn,”¾SëqqeŸÒ£ã¤ÔÿøW‹„À²Ê¥5LJë>RL&Ú`BZ«—KóûéîV ÑÕúæÞõŽ[ÂF§°p —šÆØ‚ ™` Ùá—¡ƒLôE[H¤í‹öäÚ‰Xó¤IŽØ :’û„rIT+* lÖÌ¢´Ù#ŽÚFõN›—C‚I:ÚÀB%pzDÄ¥ +Ô#¤Tƒ—f¥êr ;íbSšL¨ÈÈåTsËÅÄ èÌFŽŠ¶C 2µ >9ÕßcÄþX´´xT \2¨¶Õö´ÍHk]A­ƒ†°`JÁÃPž™©ýâÜùÜÄž\Zq³Eäx‚긃0zð äµêêµìÔ>—ž05`fBr iôŠ9:Ò”2srväóõ“Gtž#‡Öì5:ünB·RZtÄ„ûÅüúÎõp¬rlÔéöÇ úP¾àåŠý;_DtÈDÀx*¹94E¸ì¨Í§wCÍFøT+5±–›YWšÓJc*;±ÐÙ<=¹s®´¼C$AÓ\ªÃ€›Mt=LbØŠ<Œ‹ ‘¡t0^eSu2š÷Ç ‘R÷Ä•‡[ûruÂÉÇÝ¡Ÿ®¥'–ƒ•Éq^/Æó=.Q6z<ŒâbbcwŠÎÎY™8JÈ• &]cRe·¤êý¢SˆÒ¹Üü²Ü™³ñQ;£Ñ†ÍÍ5;H° v™Þ.Š;ÆË+´Zv±2.'©D•R\ºV™Ym®íЙªÉ¢c­€:a'Õ1{ààùðˆUç åü¬˜““2yE°gRe…JM¡‘º-]¶1²­aáÄJi}¬'”N´6LzÄD¶X]Œ×¯xé˜ é\“1b!²vf’‡æw®t7Ï1©Ê«gÔ À%'íTâ°Æ~tÌ9lÄ,>ÉNF‡M¾¯v1±æW:bnžU'ñP~~çjoýŒR›²”AfÀ92QCä¸7àF”ê"„eDȤ^ÉÁä3ݳ•¹ËL¼9 ØðPV³=ˆ>©H„Ë~¹uo-ÇÛËl²*åºáÂB¤´Â§¦‡tÞ:8þÍcf£rцÎJÜ9 ;2b×Z¤XNVW£Åe$˜2øÆÝVŸè³ßÔØîÐXi]®$—¶¥ì²7·¨H…ÕD±9±t|nãÌöùWxòž§^|òÕï¼ù£Ÿ}øÉïÿü·|úÅß¾÷Ó÷o>™›Ü¤b‹ºÉï!£ñt;]hg«S…ÎRmjizmgóì•k>yþþ'OÝûèÚ¥›'®ÝÞ¿ïÉ‹7ùõ·ŸÿÎÛSë{³çµLLÙÉ0¥”èXáâL8™ªõZ³+Ó«ÛëgΟ¾qß]œK”§Ö³½åXmF*õ¢õ™¹k{w?rå§^øö.=ðÔÙ{ÿùÜÏPñŠÎÃÝœÖê AŠ% +2{¨àôËB²*çÉJ+×Yh-œ\=sãÖc/Ü÷Äso½óË÷>þÝ«?øÙ‰+·[ó'Õò¬—Ë€]4¸ÙqŸ1z¸q;ã ¢Rf*ÕÙzR¡½¾ýÁyõ¾gþõü}\yðÙs7=s÷CW~öÕ·ß}ôùW/Þzhíü=^1£w1:‡ßCGh¥Æ''ý¡²œé²J1œjåZKsÇ/4§çÖ÷.n_¸qúÚ}/¾þ½ß~ù·ßýño_üñ¯|úùýO}+Qœ<<î9ªõÙ •Wg¢…5…1xÍn†à3H cö†-¨à¦„‰)™æâÖ¹Ë÷­»yîÖ£÷>ñÂÊÉ«åÙ3™îq\ÈC¾~ÈŒSr¥13»|bûä…s—®=üøS¯½ñ½÷?øì“ßþþ—~òáÇ¿ù¿ÿǧŸùú÷~üȳ¯Ì¿¨¶7˜bE¤ReŤÏ”ë½åíýg¯¹róö£O¿öÖ_ûáÏŸyýû¾ðêwÞþÙ/>úü¥ïüèíûùßþý/ÿ<ûÆêþ=`Æ•ùâäf¢>ˆæâ¹ÊÄÜ©sçnݾÿ‰gŸöå×Þzçgþæ?xï£Wÿío¿ûÑ—úÛÿýÿü¿þåñëküõGŸ|ñÅÿûÿú¯óţϽºsñ¾XqÒϽtFHLĪkVŸló9)U¬M.mºp÷í›>sóŸ_¸ûñç_~ë'?~ïãŸþê£?ýõ¯úÛþêÓ/>ýݾõwæßæJl´ã +\ºÇ©bk~ri{u÷ܵûºï‰yìÅo¿øÆÛ?yÿ“÷>ùÝ/>úí¯?ûüþËþ¯ÿ‚jýñ/?{þÕ)¥ÙQ›ÐàæÔØ(#"¸•–KñBgnãä•û½úðãÿü­×òëOÞýø7ßùñ»¯üàóùÇ¿ûý/>øøüóÿ÷úÛÏŸ}õÍ 7#ù6„µ6Æâ5#,Ä’te¦9µ¾vâÂ¥›Ü|äé—ßüáÏ~ýÉþþïüü7øó_ÿã??þíç}úé¿ÿÇüüÃOïzðÉâÄz¼¶JG›ºþùlÞ!-ŠÑ +ˆ +Ñrº6Wê,–»óK»ç®>ðØ¿¼üÆ»|òÉç_~ûß~öË~óŸÿû¿¾üëߟ{ý{7yòÜ]°±Æ—½¸ÐhÍÎ-mŸØÝ?ùòµ×xøÁ7ßzó÷¿ÿý—þóGŸ~öÞ¯Þ{éµ—Ï]¹:½¼-4ùTÇ拘Ü,ê—pJäE9—/¯nì\¹ûüô³/þòw˜Žw?øô³ßüöoÿ÷ϾøâÍÿäÚt— ©)e 1Iq‘L±Ñ]ÝÚ»´qòìÆñ“W®ßxõ7õÁ‡|ú»wÞÿøÍýäý>øíçŸòÙ§Ÿ}öñ‡ôòß;{õv¦µà"d *µPvÆC'0:’Óùb}ymíþ~Úñã_¼ðí7öÞ¯¿üó_ÿú÷üêã¿üò‹_~ðÁK¯¼òü+¯-ŸºÂÄ*ÈŠ’Ë|²)&kõ‰ÅÎôâÆŸxú¹—^yþÕ7þõÛßýÙ{¿úÛ?þ×o¿üË»|ü£Ÿüè_~ùéç_¼üæÛ·y:ßZ5c’ÆBŽ™½#`AE ˆLm>Yì,oîÜ~üé§_~íÙ×ßúéû~ù—¿~ù×ÿéû|øé§_þéOŸÿá‹wõþÇŸ|ôî{¿|à‰'Ï\¹É5p>®ux5f”‘ó\´ŽW¦6¯ßûèëoþÛOßûàíŸþü³Ï?ÿòÏùø·¿ÿÕ'¿ýË_ÿ +×yçg?ùé»?÷ý÷|â™Íó·&Ž_Ã¥âQ-jprF;eóÐZâÆØJgþø™K×o?òÂëo½ôÆ›ßúöw~ú‹_þýÿøÍüé{¿þå{¿úࣞ}ù•½ Wg—wøD3Yßà„D±ÒÉ•ÉL¾1 1j©=3¹¸ºpéÚ¹[ܸxíüÞ¹ÓÉBšƒnœ4» +ÂhŒ¨ÆˆŒ›Q7B‰tªT©¶ëíÎÒúæâÆúÄìĉ½Íë7/Üsßõý+W÷ﺵuîruzŽURd(Ç%'ÈpÉè ÍNÒá¥Ý^?IqÇ/έŸŒ%S¥\zmuáÌþÞõ{î¾ÿÑÛ?øÁ÷ßûà7ïôñ'Ÿ}üôK/ìœß¯OO# &’sCtEƒVŒ%‚‘õíóÇ÷od›ÓéJ³Óé.ÌÏœ<¹ýÌ3½ý£·?üøÓÏ~÷Û·üö·ßxí•—_zúɇnß{×ÚÆv±9¢9ýN2lDùcFÈ5Ȩ3Ø|4-$ÕøêÒò½×ïzí•W^{óÍ×^{ñ¿xçOúÃóÿú/ÎÜX[,7šj©fÇ(Ùã¡Âð°ù«Op“J0!%™/·ææ–Ï>õÐ#>óìSßýÎk¿úàýÏ¿øüÏùÓ{¿üÑSO=¶éL¶\°!~›W3¬1ôGGÇÆ\N”—äBXδ'f¦7O>}áÒÅ+×®Ýuý[/=ÿƒ·øÊk¯¾øÂóÏ>óϯ¾ò­ûnÞ8¾u<ž®¸ü¢ÎM=Œ¡X47ÁÊY'B…•äÒêñË×o?ýì‹<öÏ—¯Þxì±'ü“Ÿÿ‡ß¿ïæå'î¿çñ‡Ü;{fie¹ÚèEsÂäo}µÚºlt1Zjs’ñdqeeóÊÕkßzíuðJ?õÔC<ôÂóÏ¿óÓ÷žyî_¯\¹{~i-]¬zý<Ľ›3ác§É[ݤÓKä\$Û*µæÚ³ën†5"˜Åë×»ýZóè$¥q+~àÈبÖeó°NŸhq3nŒG¼¼Ç >– Årœœ²8“Ómv¡¨?ˆÐ¼OÊjQ~Єjm„ƒˆXñа=¨1Œ™<4hv6„!¥xo/#0¬àöx%b©|¶Ù›XÙ93¹¼J¨v=jFMîQ+¦suŽÀ¨…³¯£E”‰8pc$1å„(…cj\I¦Ôbµ6µÀËJ$«Ö¹l–e8»ÛoAY.¡›ìºƒÉ¯ Æít@*ùÙ /Ä‹¹r:™VÂ’–æWjµB&¯µ²Z„Fz“´¡¡Q:¬s Ûi,ûˆÙoBàšIZȧ SÑD1¥æ.]¹kkûx6hT«ùB±ÛnõZU5U4;½i  ë< ‡Mð˘¹¿!"Œ˜ä´’ÈF’"²ØÝ$-ȱ"'ªjº\oÎ4[ÓÃ(±$ËGÝÞฅ8¬÷öbp d¨D‡‹ÊBY\~? %jPÔ$—É67ÛËWj3g(>n±Y^ˆÅ’6ä@­R)Û;CG{Ãßÿñ!“[pŠÞAÁ,c8 +E£±T$©†á¼/î£|*(åøHIˆ7týÍ ,¯¶‰põ‡ôw ŒÕØ€BÍ.ÊM'||V-N-ìÞÒ"Ä[4˜Âø< uúc7¤Gl^ÁˆpuwÓ³j=6„C©x0R#…¼Q]T¬&‰6#&ŸdÆ#"y™”“ŽécvŸÎÍØÖáe NbÜF8*Êr‰’r'ftbà l^ÆM…ˆPÆìFí¤Ã?ªó|m@{pÔ +^׌Šý 5DÈEËcvRcÅîÀˆÑÚ¼cV¯Ó¢Ä<Äd„Q½\Ú† Œ˜ðqòÃÚ#ƒf“‹Cè„xØ|@q1™jp1F=8îµhu͸E£µX8+åp&ìÂ<¬s°Ãf|fÄuXopàŠÑÑyÆ­þQ+3j êÑ𘓳`’ˉº‹gåx~DcÀ!Ót’²ãGá +C¦q—ÃÐ8ÿÏÃGFíN_¸Kc&õn΀Ð@>§v,^ñظƒ‰T"ÅÅÄópÑíåM¿ÉFÔ;¾sÀ—²19'_E!W†«v*¦õpzT6á ²”ÒfÔi©´¡TÖ3“ûLbrP¸P:‘íX<ì7 öO´àr£:}6Û9å¤R­ƒ£¶Df†…ï²°1+¬DKÕH~AJMkøÀ ÉpF3jô™\¬‘ìý¥nýC&ñÞáç!åð˜éȸe`ÜjFCNZõòY17í¦T9Ù‹f0!‹²Y«/b#b®@ºd(®èñëGÆî4ô!dÄݾLc\Öå%‹‹jmóà¨ýð¨eÜN¸ƒi´CÈ ”Ïš§#¹Ú‚‡M³· ê&„ å—©iR©Ã`†“•@$§wQ¹Q«_ï ›±˜Oj*å«O9ªu~YmšnÔ‚<€:&áb+_m©À€Êl¾æFLçºü‰`´á +ÆÙ .ž’›´Rw3)ƒWqò7 ÷Z1QcÁ¾~D{ÇQýȘ¸Hcð¸|a7·zEƒÑڃ߰Gô.Þ„F ~ºé”“GM„Ù+Bê§ó öŽ±j[ÊMX|â‘1Ç1ÃË#ãž;èï²Cõ¸É- +±) "hÝðÒ£-*Ž»y»?HLSóz·€ÐJ Vw’:';¦÷´Õ8Þp¦¼Šø•ÐUáKZ4îäǬ3¥bD¤eD%†’êœsK*°s°G4šËTz{VTºã¨ÁA%0î†ù5¢="ëÜ!69a@ù;†Ì&ë"¥13¦µú ¨Á¤› `]„ÉôOÀv²0Î6U¥Î)*T"Ų“ŒŽÛ³W6° G†mƒ×!ø©E€‘ +üḓÃø숅ÐXI­X~ãˆáè¨ÛŒJ¯ô?ï>:bqxyp;ƒFê˜)pÔàÿæˆóŽQ¨ÛüÚ¥úÊ…ƒFïv‹529Ç—7åú“™ÆÄB­µ¶vîA3vPÃŽ€Ñ·‘)LlÒêâˆKN0¹ù£ã (<H£´«ÆKÿÈk±œh¯._F‚P}ÁBgC­oY¼á!#c>lÄ¡Un"<¨u¶‚c‘ò‹b~ÑâWGìܨ'„Êõhk·0»Kå1ça‹0æH0=n#uÿ¨¡ö ”ÕîC#–“oÜ΀‚ZAk£‡ôø‰êë.]ö Ù:4~´O&n»ý™¯öwˆ0ãV⎣Ú#jìßXõHÈ®ƒE‘3=:^=…IFµsÙ@vÖ*Ù•ŽÔfÖ®®^~ÂLEmdįÔù䌘œ gçÉøÄAƒõǽ]B,}ó˜yÈè³ ýý•ÁX·2wImlKÉúÊÉ*·‹s‘úºÒØŒµOĺ§ðX×Á$‰ú½¿P™ßƒ’4y‚„7a2Ê—„Ì".5Æ팇úçʆ]tÚIgmTÖ,2Ñ“èj]#+$,ÇW;zŒ>ÙáWþÊæˆP)^YÆ…,”g0Ñ5ã²ï«%ÖX¤naRZTÒy  as=¼Î-¢\U{^&é $™HÞ„rý‚ü +àjÌJ;©„“ìTÜÍfP!oĤ+9lÂŽ‚tê„I¢Á4o¥fÎY‚™#Tïæ͈hñ…à-tvÜÍy˜B'A¦tÈ×ÌFƒFÂè yE"T§”.AiTÜ,ô1îásv2jðvBñ‚ו«Z;ÓÑ3—"„ô×莹Œä̤“ŒwÛ(ŒMCÕ¸ü +4&”› WWðD¦‹p±¦WŽŽyF „‰¸ü)\¨“¡¶Þ-™('®¼Ñ`vX‡Œ±Ã#Žþ¦3wÈMçƒê¤œ_8dôBTéŸÅꑆ Do2*ãRUHOA3H.IK‰!£cØŒj€u}+F¸ŒŽ¹YT*9Ø”—Ù²¿üo.7q&7}^,-zÙ¤œ™à2='¥ØñÆ\ÃF¯‡Ndꘘ?8l³zÃ0€"á‹ñÚ*Ÿ™Œ¦×ÏÞïåÐp.ÑÙLOíe¦OG[[áÚ†G(¸ƒ‰Jkù[¯ÿxåÜm­'hÃB¸P ¤º”[VŽ{Å*-×Ôú¦É§Óã ·)u>T:‘š¸Èæ¸lÅ6V7Û;ƒC6ÒîR™Tj.Ç$zéêÒ™«OÔ.Ž;!ûÙì,žóE{´:Ë¥Á™è<<¼ÒˆÉV<‚ñ“[ÒZƒ«xXNOb°=l"Üþ(HX_¸æWšjcS®¬ZIEë¤ ÔÀD€æú#U%¹µë’ªÞpìå1½wÄä5¢kaR•OÏJÙy=0l=<æ±GÇ=+T„ e¯PBù²N#¡ +®´¼ášÕ×Ø©$©Sr…Õë½ +ÆŒÙM® $&„I'ÕzXq>ç ¦FMÞa½¡"@¼|fÂ-äuÊ—ÆZž@º’ªØˆ¤SLÞ¨ÖÒØ8 ’QQr³ ;`È-¨ æ¹(çþsdíDlÄ裥’ÎÅþÏ#‡†¬Z+ã¡Óïˆp˜Ü†GDµ³wi‡ƒ‡µô•4h%㕸ô\²{:ì±Ù),Ú ²Ççç‰XÜE½¶rõƳå…Ó‡4v½‹±ô× +0L¬MGÛÃBgÅþø¸“íC‡;€p).;%WV¤Ò’‹QÀ¼§{[1ç•òx´HMD+«¹îN}~n÷>7z6Ö2s¾þ‰Íà›?ʦ&„â‚ÆÍÑ¢Bz>3u>;sŽ/,¹¹‚ɯê0Ù'cå%B©¹¸Œ‡Ï`"Ø×:•hzC:ÙrLgUÈOÙq!×Þ¨¬\S'÷Ñp“[˜Üur•öæéëÏDòSƒ&“švºì¤JáìŠRÙ å Ù*¸J©[IPœ øÜÙúÂ¥õóO$Z' y“Ëûõ™SCFÌǧ£õ•xo71±W^¼ÖÙz$Z?Äå¦<\°1`Ì8“tó§.zÔJ‚Ù;~ °¾DgÜJ§½J[¨m +«&cÁC”RöI%›EجO*R±¦R]eâ-«Wn18˜ˆ“ßI ô(H’µÞ¥³¿*LMóÙyPvsElBmïEÊ›dŽ8¬Dð r€‹e; V9°2\ +Æ,éû§~q nN&íbwF-äÿ<0<¬usA¶3Z'ëåòH ãe nbÌ5a`Áëzú-/Ëååpy¹>}úìÝÏ-„ÏFÛB~‰Œ¶ ½½òÔ¥¾9âæt“ƒ +J +%(„>œ >*”–çT ú JD'Zlz*TY¡““Œ\®Oß»õ‚áp©.¯&š[‘ÊJ¼µknqÙ9òKNm Aè3ùÀûÄJ(¿ÜÚ¼•ÙïkÁtLï Ûéf!_¤®o¯ÝR=.ÕTltä¨Ám#å`²Ó\»¸wÏs—Ÿ(ÌõJE­Ì¿Ëo1àúBÅùpq“;ri¥<}Ü/çunA©`Sóî@ž 7 hû§ÕÕêrºµ9lÆï´Œ9‚àüѺ7TìD²™“ð¾:Œe“­ÂÜéêÊùìôN¢³ÍçP©€b=ÿýã—°â1ʦb½}¥½çö†ÍxÅRsefóîo³<íJÍ©…KÙÅ«lyMGÄ‘òCϾ5»yy •G¦¦ÔÞ™™Sd'Ïk]ü!ó` 0pHë9f&4.ûË¥ ALÐÛ7)»™„V&9ëì¦gÎ5¶o·w ür»;XWccúG\B$rÜÅAügZ{PãÒX)wgÂ$b-`°H 8¤‘m--¼AGë:jöq.cçr­xmyáÌCáüô°0ùÂéÉs £d´¥éŸ„L›Q!š™(tŽƒ,Úp +!ïK”:M¥¦¤ÊBiþìöÝÏægNÔb +äí8ƒÈPGµøH¿–‹8Ÿù§£úQ;CƧøüŸ¢ã“ÃfÈq”›‰QÞ‚‡§OÜ8~ýéÉ[åÅË̼‹Ñõ‚9u÷K‘âXM+ Ä»V<“‹K:DºcÐjpÑñÂɧÓÿÓÃ:Ÿ;TgK›byS*­;É´•i¹iEåƒÆ1ôè˜÷¨Ó:XðWZGª²4¹ Þ[ã`Fû_!abÜžèéP“ŠD¬á`ÓŽ@†ˆv½‘*Vœ„‚Ñ1?¢‡üBÄ!# 2<úÇUõ €éM——åüäÃÆ#:·Ö-ø¤j¢u<=uZ©/sr~çÜCÍ¥}ÆK…ùÖúÍêòuµ³W™¿TZ¸LÄÚG X Öòr‹—ï{i2îdËd¤­mùÂ¥ƒ@¯ƒŒ‚m»kòŠv&ƒËýÝ‘!›KöP6…óÉlw#?³—>k,·—/TçÎÒJUÛéîN¦{ïó w—BÙÞünwñ2£×Ì`òóß±áÅCj¬îųçCYVÍ`ñ÷H¥'}ê)+ +Ó„›sà'Ç=ƒ¥¶=œwp‡¶I(ƒá3¡âÁT®»ÖXÜÚ¸ÚÛ»×ß¹³xø°¾|Eonyøxº2?¸q*%¬0OjE&ÒFå:cô¼T +f +L²Ë )îÁÖiI³ùœpR„˜%²ñ1F˹ü–_†!CDêB¬‘m®mœDëбƒº<Øq¦ªeæùHó“^7ÒÕåqùü°kÜÅÙQ +ÕAF`r8 )Z+Nm;ÉQû``%\\Í÷/”.öw†Š« ÷‹õ™ã.ÖÉnT½4é$°ˆ MÉú"¡dA¤Ù@ºAA*RW«ë™ÅëÓ[/j™ÞìÊùïÿôr²7ê"rCL/Çë;7ÞÞ¸þ>6ùˆ¥”@.€ÒðD€äÒ}Zˆ÷Ùp˜óQ; ,¨SãvÀ”‹vŠf¯"Ä:„–ŸðP¢ÇA ÞT Ìcù^²8{zÌ;4 ùOàS.L±@ç]¨‚‹)Þ²ùE†)h%%ÛgÏÔqµJH™rw³6³ÜOF`.æ#u«—s#A`}ݨ¬¦Z93îö ²6­ÄÄÛ|j|P£™úî…À®cj#Z=ˆÕö¥XWÏÌ(©i¿=eÂ9—,΋{lÔ5á]Dš +Ï´W5×Rzó´£”œ)Y½Ì_2kÌsÆÁ™aˆËËé/sRš9À?wÆŽÓ‘sZe@©7ÔòY9¿ŒÈE}03ºm¡d6ÙL»Û??»ûhóÚÛûw¾»pø•3x0 iy4dÃNŒº‡&ýŸ0â  !,ñˆ‹u26ª’‡”I=“Ÿ:{öâKÕå#¥ 8VÖ“í£Pymðœm² ~}° ˜ö3ÄT”¼` xÔts/?uDÝQ'çÅB¨TÀ•*$¥ úpŒ{%`/A7ñ4{™alƒx9ÙšZ¿Òߺ™l­M­^X<¼ËÆʨ””3ÓB¢=½~µ8sèabTäÂé™Õ‹F~Ð UmHØF7w4Jˆµº+WÒõ5Àì0ŸÖrs¡Ì­~#nêô¤ï¯™,.€Éƒ??l9= îÛ£*ð™ãnÆìáw‡ÜÌI“ω‡!|B¢¶.Fë –†Ì(LÇøPqÜŽõØý‚Ý'ã #CfxĘt€7è.Ô‘SØ :˜¤Ä(­¤P1¥¤¦o½µ°{Ø6?“¢Ô*¨éFyå“>\BÙð¤—U/À&€á!ƒeB*3~69nÇ©`L–Ãù‹f ÌY¼âC†êrf1Ù½\Xº+ææq)kïÄš›B² °Å5XíV±¡†`ô|tòÛÃÎï 9OX0¿RÇÀ+bÑ4ØUÄĬQY ðQ'*é¹éÕýÛ^ÿdíâÃp}õ›aÐRë^wó6m”\”*&§Ë3©új0ÞŒ¦h91éD¤ $¹h`'Ý¢Ò\˜î§àºrŒ˜ ³‹¢Õ¢B‚?—QbµÞêùP¦ ‰†œïÕ×oöö$ŒO_ b]³?Ïö7/?u³á!;bñq  º ÃvÆkœœo÷÷úgo>?äxî”F€I©>®ƒK ŠZª²8söàe11à0ŸðášhTa!1b…1fàQ'Ý䤋²ùÄQ DÌI.8xjÔæ„yLŒ›= ˆ´De¶»På’ZZI·w€Ra@>Ö®¬O2à`&¬‹á"¤NO@ +¸ ÍÇ;c”†×LtÔŸ™ð#Lâß>1ù­ãc}lìØ˶CANÉSÊÖC©Î¤e•Œ ¢JžŠ¶¤ìœZ^Ëõ/g»;½™ïÿä÷ïf4¤—•ü T>9f­°„s±ÞòU'99jš°›œÈÄ`w !\¢”4¨û^:Š+%!=•éž-õ÷Rµ…K·^>¼óšlÂr>\:*¬f:GÝÍGµ¥›•¹K|¼«§¦Vön“á”圔á¬'Và¢S”Þ±¡áq/ï ŽëÃÞàˆ7xÌ„ž±€³”ì’”ž§äüêöíro‹Ž”¤Ì`9—ôô…úÊŃ—·î~7=½Ÿk¯¯_x eç…x‹‹ÔQ)ëlÅžE„<°C ×F¨:q uyc¾‡0&@ŸC*!æGlø±q0™n2Š² TCÒøÆ„·P5‹ ÐWN*N­öÊõZÿ0]YLUgý¼A(I9V· ö@‘­PÈä•ì ¡b¹êJ©³}lØmpn,hø“8¢Uaµìc“ÕÞn£¿t@–¯ +Ñ­ öðâ¢5HÌ@t,]êgª‹ãd̆ØR€ÔA¡´Ä1 :aG•h9’nYÜ$ÎåþÖâÑý…s¥ür XvQ‰™Õ+ŸüðWñÌô„“îåÓ´Z§CmBkX›z¬V,ÏW’­IÂiÌŒ˜¤’¼ˆ,ªˆ ;a²Cfä/Atx'\¬QmÁƒ„ &Ju/ßkÒKû¸(ªä¨XK*Ìëµõhe¾·°ûàõÏŠÓÛ‡sݽÂìµ° ¦fQ­aXVÕË·}FDªubøØ @â°›p1@Ø !^­Ïm\xÀ _°VÄBù`¦­—góý­D{% %ÔüL¼µCGšA@£©>ªøhƒÒªJb:Ó\£õ”–ḵÀ!rQú¸_usˆ˜§õ–“Šy¹Ô°GxnÜ?îíd”u‘` fçׯ—§·¸hUÎÏ%»ÅùËSw×Î?®®\R +³­…Ã׿÷5›èÚɦ•ÉPK!1ûÍ®ÜÓ€n€Á˶w{¦|³,°õ›AMÞ Ìåp¹2d'G¨ ƣσm²À`g{çX£FËéúôN ¿”)MmUg÷ÈPÑ‚}l D£lN¸@²v8ì„BïÃBôΘP öl(?êÄÏØ):Ú-,\5šÛ|¢ë¤c\¡¤^Æði`l€>Ë©þ`wjH&7dÄì¦L.¼â„åq  ùfæš ³Z ”L·±~£·}¿¿ýÂÂÞÃѵî2L…ܨ†p ªØƒ=›lÚ4Š‰ËñNüP´:iÇÜ9€éféCU _679b†>§¤õhí”=>â·z“[ °ÎèjI‰5¼¤ÇÇ„Ï×JsõåKó÷ç^¬¬ßpsÜÚÒýÍF« >m¥Oö1—ŽMº&¼¤Žˆé¾Z9kíÆ[{.2ÍLßúéÚ¥§ @óñºRœ•ò}!9tÛ…kvXbµJaê, }ãÛ-`±QŸJG]ŸÞ¾xç tps,ÄDÛ¤Ñv2iÚèµÍéÝGRªÝž?8ºõ¬0½k (6q˜1Ø£ŠŠÁì`¼Ïà@á³øI@pnðídLLͧÛáL§>³ÓÛ½·råÕå+¯Ì>RSTÅ‚™dãlaæâb¬^öóP‹}”1îd'¬ÙÍ›´& ó¢^ûÖqÓ™qŸÝÇ[Ý”ÍÍÚê„GBÅÜìÖPýÿ¯cÖ3fødÆèH©¾“ˆ}çŒÇì眄â$HŒÑ±*°îÍÍ»¹ù«D¼GDjJa>Ñ,‰ üÞ7Ï-…Kë ñ]¬1ì¬ßkñð¿8n'žl_å±c.BsRa*x&Za“5Ÿd„Jsµµ±ÖV¢q67½Ÿno²‘"ðÏîÁ–"¡ýl؉Át5[j­ì]yIˆ5Ÿ²ûiüA…* úرˆ“Œû¸¸^˜…ƒé“^éfXuSQ€ü zö§ŠÝ…såþ† W¢³íÍŽžÎí>¬/](Ïɹ^8Ùzòþß^|ò™ y¨h¦{ÐY¿Ÿl%‡B¢gE@£áT“RócnîØ~|uIV$ +áüÜþ§î¿å¥Bc^ÎŒ©.6AêµL{3×=JN]ô ^J׫K@«=\|0ÝžKÙ1͇ ý‹ƒíüÜŒ›2BÎOLj*,(ÁÒtÈ(öœxØâRz5ÞÚ*ÌœŸÙºÝ\¾2¸TS®½ôÝ/¿.´WÇÜ,ãr™Pk*nÈÀp’rb£CvÔH­ ú + UãƒeyÅ*ËsËG¯¿ûÅôÚ¥Îæ­éÃǹå›Ù…kígwîÎïÜI4ϦjK_|õûÛO>öâ*³á¦R1Ôñ¶®Ý86 +Ñ¡²OŒyŽ;¾uÌtbØ @ÒI¥˜Ø4­×cå%V¯ j‘ŠuˆH‹ŠMqé93$’á‚^šÁD«±ÖFqéJkóNsë~eýn¤sÀ¦zz¦¿uîñâþDÍ£j96xªdƒŠ/á™Áöî6âä„r0R@ YÑ cÇBnv°òŒ^^•gSÍŹý{s÷Ï^y¹»q#ÞÞBõº‡M`ß܃uãªÙ;xÞŒÐj¹©ÃòÜåpqQ +“>Þꪌ¹IàM)O„ÛBj9\\§#µQ/7áe\ @͸‡ ÈÅš‘Ò|²¾þ+Äo$›kùÞ^¶»*,µU£ºÂõp²±vpkjý¼‹ |‹ÕWKÙÄ;X¨>ØiÔŽy‘àÙƒ»‰ò„G%̉ÇÝT °£ÅÙÃO«3Û‹)+æ´ÚÙÒü•Ý›ï\~òƒÖæ‹~!W¬/ž=º‡S Æb•³Åù[µ•ÉÎ1ÙÈÊ»^LÕ7N˜ÐaPa]Œ›N +ky59uäcÅæj½¿ Þà’X¨Bèõhy ŽôÔ–íC+ôÙpåŒ:5‰ ¨\,0Ñ®”˜±Áʸ‹bôJ²3¨éˆ†Å4ªf]¤*D*ÉÚÊW.¬\y-Ý;ˆT–ÈpÉKE>pfíÂK÷Ÿ}ÙY¹àÒBl“ŠÖ€dóRƒ'& “£!3LÎŒ¹È¿86qrÜ?jÅÍ.ΉFP¹â9‚…½¾ `ªwÐ?÷tõú»¥å.‘ioõ·ïïÞ|»µpnÿÖÍõ«áÚByåJjþ²RÛ”³ ýõۻמ5–/j“Ò³‰Ö–Q]g’ón&=lÅžvœ÷hÃG…-€B´ŸaZ‘ —•â\¨8»´wg÷öj©k-æÏ –'ªm¤»+ ·£õM'r“ƒaeN\¼#¤çèø,Ÿ^Âõv€Ë80mÜKød X°Ââó~tZÖð™M$wã€âITJ(¹~qöb}åpq þ)-Wîï ŒPy©µ~{ãê³¹£—CÕ>Ùðq.E»+—ÙHÞÃjx¸‚(%И”F˜X²4«$šNTµùƒVÿ`sq`º–í'ë+ Žv˜:Re#ådsµµ~]¯­yhciåÜÇïIF1À…ÞÅìôÅxmKJÎOøøa;æô£Ë ­ÓƒMfU?ŸåâÓzål¦w!ÑÞÇøøÅ/ï]y ¼ ©¯ÝÙ¹ýþêõ÷”ʆøIí÷0¿qeÂ/œ€M~™OÌ©…u4X>e‚p°4µonÙ¢zÈ42\Ur ÁÌ,¥dŽn½zõÅ”d×…‡¨Á­ø~¼¶ÞߺµçÝòüeL©"bŽÕ[f¿p|Ìý×'& ¹€_  R‡¿sÆá‘LÒÉ ›P7Ýd|°å¨_Å„œcRŠ·’ííÌÔ¢Öx½Tì”æÏ×—/lž»ýåïæ{[R®wöòëG¿×?|Ô‹[OØ>{‹µSÅÅ¥ýåLwÌÇYVË„œkrÓ^LÑãõ*pþrÆâg&Pz%ÚÚLöZë7k«—…T£·q¥´x!ÒÚÔ[[bn0šƒ§š-P¹Ä|ßÆè>9ãó€`´ÆžK©™°¨WÌãFS).‚9,RÅŒ¤T‰P ‘Kˆ”³!Àl‹(Ÿ€ø¨Gkë­‡í퇥Åk™é£ô­ª¥[³÷ä¬Vœom?ˆ´÷˜Ô4­bY»ä!åI/)¯ ‰i<˜c-7±øx˜‹ÊñpJß9ã>5Ùà ¡dõ >Ö±#!“‹U-¡LºQ«Ÿ$tçÍs·ŸuׯG‹ó¼Qµù.ƒ:5Ø΂NºiXH¦êk©êr8Ûw‘ÇF\ã.Ú‡m 0Ði> +Ó¡L¥/D²§M!Ro½1{î¥Úò¥Pi’Jƒ›RâîÓwÚ ‡ °4*å¸1éì)Ù!1@…3õ%~ÀVV³W‚9ਛL´ÃĦP¥¬%ê×>»xç!elª¶x+7s)Z[ e{€\ *.¥€çî¤Íâ¦Çìøw†ÇÇ}CrÜ͘œ$)§Œ`#¡Òb¸¼F‰z Ø¡Öj±¿Ÿèì33L¨ %›Ñê<¡F1Iå£E:\ŒTÖbÍÀzH1Ì…ëL¨|ÆŒŒZ1Ð@Á·x9cg&½€ˆA9°­—ÔâB¦³ÝZº0»vtpãQgå(?½³xþÉôΣõËoÌï½n®I±Š’leë‹›ˆé)­°P[¾ÚÛº  ÖX“³Sl¼ËO]ºóúÞ­W”ÌÌs‰žV\;zµôj¢¾ÖžÙüôËŸÝ-˜Š7VÝ®.Ÿ;¸÷Ö«ßûÅ{?úÇ—>újûâKï~÷oï¼ö=µ¼¤––ÄìB´¾Ó\½»~õ½©'¨Z¥ålt~nÖ+f(èçã.ÉgK 7•ü|€‹–§ÖhµÀGÛÁÜ “h3IéBïhåâkD(#F‹R¢ëõ|ïByñV0¿îeóÀùóÉ>íX§e|tÔV;þfs–Ш‹E¥\²±IªE¦Âr¡¾r+;sÈ%»L¬Eê%'>X…ÆM)v˜…¸0¡¤ÉP‘ +ùXPó 4À~mÞHÔ–€0Ráš ÓÌnƪI¨æBpé9£j'Â6LF‚>Ý5ö©H æ(1IIIˆ‹ FÍ⇭$Â'ÅX“P+€Rt Ú‰Q?ˆÒõ’:€ w@„Oz„ŽÚ‰ÓfdÈA[—‰ vÌ‘RÁT+ÙZ÷Ð1#YŽ¦5­f:RzPR09mõÒ<45ÊÇŠo‰®”[l|.BÉ~833ØßËyqÕ‰Hf/wzÂ{|Ä Â‹…I¹è#uPDp­P˜ÚYä{îAº»FhI%Uo­\Ö‹½x¹)LãJb >R†’lÃB"ZYH·×™H UŠ^Tº(g”x%^î+™¶–âõ5%?L÷ê ŠS›FªÒhõoÜÚZÞ£¥æÂþîí×.XhC{“P ”œg§@sÒÓ…Ì,zHÀÔ5 ΜѵA*@%Ý úd¨"Ää燀…ñaÁ‚‰»X@Àž2¡g,بƒà6æ?6êÌã°`Ç'á“SÁü¼˜íñ‰mY MÔR°`LxH/ÂÕ\¸¸dÔ·ôÚ&eÔ-É6…DÍŽr²›š:Š·öÒÝC\):@+ÜÔ¸ Uô²«Z<Ôé ÷©q÷ˆ,Œò9˜MB&ßÙ 1JÍÊ©V¢³Á§§H½ú$ÕÙG¤˜Ù‹5ÀÕj¤r6RÛ¬yE ˆH‰6ª€Ìºð&eÄxSB¡W@Ö›}ܨC¹¨=À™¼”— +C”&ÊF,QJä;´’™Bs®2»–ïÎ¥)>^³m½:Ÿé,5W 5úÛ;·Ÿ5ÏÞ=)ê%ŒzqÉQ±duqûF¢± X5ŸŠdç•Ô4k´|÷àÞ»c>qV¬ 1CµéÍû[·ßÛ¸÷‘\\Î5Vî¼ñƒTsÝm¨9Ó=š;zrøÂǵ¥Rjvïêk±òò±1ßÉ1ߘ“õRIL*ÄËËíÕ댒;wåáÛŸýŒó°Öà³K\j^Ê­Ì{}íƇ©ö‘\¼óæ…ûoiÙ)Ð秭(È,$ûé$j2SÃ|}ûƹ«/Ù!é['Íß:aúËç-Ï ûlð<Ën, ¼_iz‹ÔJl´‰«EÀ¹f°™.ed¨P0VvaA®@BRHMëÕõÒÌÁôÖm)×÷’j¼´ gû6b0ÍjÒ'Ž:i‹OÀyƒSR®mqB8#[\Ȥ›ôò Ÿ˜±¦øe}|ÚñB¤¬—æ²sçf^êìÜ+,ÝÐÊ^Ú‚Qq+KEé¹8G@&‚ùhy æ=¤¡g¦Á)Ùц…Æ<‚Óìˆ H„W‹ ²;!Ñ(/”çÔÎ% ´p²aä§AŸpÁ0Á•4«Tgwo·W/ËÉ–ží(™)ÿÀƹPŽàã~\Á™Êê&sÆ‚Ø¿Âp£²ŸT½¸D€ ¦qÖ@ÝÂ|Š æüTØÏ訰[‘d£Ò^É5Wy0šY™¿˜în‡ò]&œeÔD0œPô(ˆ&¿0b'I)ÃhETLú¨œ~–“Z¦GmS@ –ôʪ”èÔæÏu6®';›£§óÍ•ÝÑÂ,ÄÄ#ÅùPaVJ¶i½¤ÌOénT¥ëzņ,˜5 û¹Œœš å—ùH‚ËVz•ÞxƒVßP«éþåLï|¤´”’&Ì®žïo^¾ XA˜jnp–U‹“bTÓPñæý·î<~× +IÇÆÃNa”ª^ÝÔK+Ápöµ÷¿¸ùèí q|Ø3éæù7ŵ{ B¬n’ä =Ýô“! °W2]àýJ3»éæ*$ÄY)6½x®/Zá  Ñá**e11á‚“·x\^„á%§×çô£¸”Œw÷ùÁ +ÞÓ¨RÂ8½ÝÛXÚ¹¥åg˜hUÍÏ)ù/—·”Ö˜`Âæ¡ì^6@F:NŠY9Ñ\€a“‚Zäål±¶Ô^ºd'BZ÷Òº +š\ôàœ9=Vœ™tÑ„‡Í訕ðað…”BÅ À†”–v’’j¥ZZaYLôŠÓ‡ZvÖCh¥ÖZº¶hó‹€]˜ê¦â8æ£ ŒÖ§dvø%5!Õqp’’e@a²Ñ!.åDÃÀ gR„…L09¥Æ[†Ê‰èàé)ày”V¤ 4„Õg°ÃÊ°íÀ!• kÀäÀÍ. +üÎ öy$´²ŸKY|L´Ô{D@¸«ˆ`ŠÓrJ$ÏcV7AK‰h®í‚(‡÷‘€ t>\¤•,h苇Ž× ån¼âNŒœáA¸j›Ô`> ƒ‘'¥”ÎÁ„bvbCãžðêdøĈûô˜Ïdƒœ¡X%«¦Kó ÂŽÝ‚»‰$,úù”—ŠÀ¬Œ2B¦Ò‘£ÅããþÁ¼°ÔlqöBcýNuùŠÝ/úq¡Ã^Lðc‚†-s:¥e•TSI611.( AK"´f÷ (í磴 ;âGEO€öÃ$+!#9 a5%Û´œ›Ò]˜,©©ÅÕÃÍswy= RUÏ/ä:ç´ÜJ€6<kwã QnHôª<ˆÑ:"¤épƒs>D–ÕÌææåËÞqQšæ=¤æ‚e/0½8“·"Àï™]´’œ˜f…‚fo‡%BL“‡ùH€R’Õ…Xu)”ŸW2})>MÈ®)Ñšo€¸òQ-3)n@R„–Í'š¼VÄIJŠ=wÆv|Ä5áb 6ƒŠ%t"aÀb¡ì Ì£â!c@uhÈME1ÂèÆ”‘ ŸÃC¹;[¡3&?0$^:8"Ã~"8föCE#5íÃ4$yP¢£à”øO5U›p¢@E)°›AðQˆ>Üî‚1à}³«Ýåòø„ 1Q˜‹¹0eÒE£;fEm>îØû¨ ò"ŠÕ͸ÁõÂo@°º(,Vf¶y-çðÑ( PÑAØ(ÈÁ‘IïИcÒ +ø:<é6œ9€‡ÕXI1 +¼œñcaƒ}Â|lØurÔ32˜‹N80:è'¤Qkà¹!;Ìq‘Z˜l—‹T'œ¸ÕK™ýÏž°»a/t <Á‡³Õ#ß’Pl>|Üî³xh„µ â'ähº“™Ú>mC‡ÍÞá çȤBi’–äHç55‘ÇÕ„%ÀÒR¬Ü˜KæÚà/è¦t3-w“áa+fõ2ÎÏÊ1\ô²É‰ZݸÝG¹A vÒ‡jj¤«Ì$+Ó( F2 —­~|Їˆ Ÿr˜`4hÚ‰ë„Wp1\©ðF;˜˜£U7ÌIj²½pÀèÀÅ1z-”žNV—åÄ” ™=ÊE0Á$àzAÏãb|̆¶MZS³Ûk{·!*³‡6¹(“›‚i46ÇòñÒ<-çÖЃT%xÍ-ðrÒ°N?æòâ)kñ†£V»fh5–pá¬É›¼°Ó³r– µjñ‹§M^Ðd÷àþ*DÝZQ͉PÁw‚®p(>œ‹ÖVÜdÈ ? +ø¿œ’–ô¸=9îšp‚B#aLÌ ‹œI$‘Fã mg`Â=—Íxmù›;-,ÊÇr­µPf +()pYLH åãœV†(t»‘lÄ2-§’N³*„KDpÑá…ͯžÈl]`Üðˆs ðõ)Ëé!'P‚5@¹AeóŠ^$ìô`§otÂvfÌfsùTEɦãù\J5¿6úeØ J˜chÂ3:ðÁ’ÃÃq3é<3nÃ)å8MSQôøüŠ"Åã†#¬öI§ËFh4˜ÍÛ›‹»ë­Õ™T5¥èA^U½¤dx°Û¯™ÎKªVU’õÎê‚VÑ lÏ„Ù~"™¯d[Ó\(Í6.äÃ8ÊÛ<Ä©1×·NY¬)À$‡& ë,H£…Útgn‘*@AÆmWÄÈŒ8=]"y.œ'‚I“ wˆ¤À +Á…}ˆ‘!e Þ`¹ G“‰XH …YA ÊA‡ ¦(¨^OÏÌwI¨"é8·—ò#"„«.¿àðr^XBˆ7À:œ—ÓêtÚ%Iªdc½Jr­_¾°;}¸Ýððæêú¼ÁTÐ q öádxtÒÿWϲY$ЋÅð‘$Nk²ªÈ²(½~Ì „˜ã †‚P˜¢YŒ.*‡ +?}jÔöÜñÑc'ÍCcîáÁG¾q3qìŒ÷¹Ó®#žInu’V{Àé"Éœaü%Éq·x~£c”˜´CLJÌV†y¯uÚ­îÖ‚p1-L5ãýÙj¡OdÔZ%¶·ÚºrneyµS*Å|LÅE 0;ìÔøø¤Ýã H,âIsÄd¾“)”£Á'jb0¢Ó<1¤BAmGï_ߺwçàÆå•›×ÎfK™¡ ûФÛ €< ‚( ”Ê#·Šáù©t£¤ÆCH#¯¬ÍdïßØÇgÏ®üîïÞüúŸ¾ùö½Å…b$Ê2"‡ð‰Áæ¹X„fÉx¤R6ÚÍt³ž +|UŸ[^ºýøL³ë@`“ žæèÃXÈž÷?~ +E h$R(äSé˜/àöøÝHóÁr)¿î|µÓ• c~}ÉȤÝaóã´Ǥ˜’h ¬A~†bÀƳÙb$¬¥"bJÃÖçK;ýv-]N)«ÓÙ[WÖÞÚyöø⣻ûW.lMO×¢1#àƒÜÄîÂBšŒ“"†`<ƒåâ|£ªB½ftw>ys»òôÆÒ{Oö?x¼ÿÇ_}ñ›ŸïÉݽÍõÙjµ( +’/€»}@î(”’l$B…Ò¡`)Êè|§lÌu3Sìò\ûòùÍýùë—Öß;zpçÚÁÎAµ\5bÏÓ{xÌa÷Ðcääˆ{Ü‚8e>qÊ4:îp:!·Ã-ÐDBWj•\H$–Èg“¹\ƈģFÚÓÇN›@ÂÚœhÀãâ1BX‹h»¹³ß¼²Óºy®÷ö“ ½yýÍG›oßíÿá‡÷þå7ïýÍÇ^}t~mi:#k¶:ì–IÜk P'‰uRÞ¹v¸`œß,ﯗÀqq·}ó°óèÚÜ㛳oß_úÃO^ý¿ýà³·Î_ÛÊ'ØÕn›°ú0*ˆS"€XØ]Ó‘­–pÐWîïßyaõµ[³OoL½ó`ñ—_Üý—ß~ðËÏnü泫ÿòõ+ÿûüöGŸÜ|såp³™LÆ0çõ³@°†\J‹s }k&6•ÃgJt-M϶âóÝÔâTòüFㇻ¿ÿíßüê×?|çí÷nlž]Ÿ=iµØܨ O;ž;1䲘Bœ?*B ±¶çlää^= +Rcs&úäÞöËÎÝ¿¹óþkW~òå»/>¼‘/F§ggÚ‹{Ó›÷¹HÙë´É J„ØžÒï´nîTw¦”盿øòñ×_>}vgåÝ{óÿð“§ÿþÏŸÿù÷ýôËþÕÓŸ~ruw!¥0>Óm¶xatBBuõrôõ³Ùï¿uîë<úô­K¯ßî}üÒÒß}xñ?}ú³¯þô½Ã?ÿæ•?ýäÖ[·7wËë³ÅDTK&“A-)‡³ ÃFÄ@-†.”„Ë+™—¯Î½pP»XùâK¿ýêÙŸÿô£ßÿòƒß|yÿþñÿïÿùÓõÑý£æ§¯ìüòË_»¿Ó³5pfÜkõP#{Ü΀ۆz-"î b“Ó9æÜRöh¥¸Ù3÷ÎOþÁ o?ÚûÉÕÇ/܈1Ì‘ÅñŒÊtRäZ»¶¤¿}}êû¯Ÿÿüõs?ÿøîüÙ³ÿ÷Ï?ÿý/Þ=ðÇ¿}ðçß}øÑëWÖú…˜.1¬¨é š$‚TÒ‰Ù¶ßå//è/îæ>{röçŸÞýò‹Ÿ¼²ýÕÿÓWOþç?~øo¿{çw?¼û_ÞøÍ7_¼²˜‹YŽ&Äâðù€ŸµlHé§Ù‡Ækòo^©~þòòßïòÿøý›úú•¿þw_\ýן?úÓOîÿôݽŸ½·ó«OÎýðÍÝ‹Åb6îyä#‘6ß­,µ“ 5íâRìýû³÷Þá§/¯|üòú~ú꯿|ðýg‡ûÆÎÿýOÿŸÿþõ¿þæÍ?|yë¿ýþͼsa¥C ÛÐظÕH…e»¹`M÷U5ç¹iþöFòæFâáùê;—¿|kk?ÿôæý§ÏÿáÇOþøÕ+ÿþ‡O~ú½{[Kµno&^ìzp%fÄsQ¹‘`Ö*øø;7º¿üèÒßqóoì|öÚöO?¾ño¿{÷WŸßþòÙÎ×_þ/¿~ãwŸ^ýô^ók¥í®œ‰ð Í;ŽóZ­:ãê$ ½¶ti)~ÿ¨öìöâÏ¿wï_ýì?þìåúÉKÿôÕã¯?ºüõû?zuéÙ•ÊÑl¸ž "A ƒŒ˜pØ(6ê­ùN®“áziühšóæÔ¯ïýäƒ ?~ÿðW_\ÿ§Ÿ½üÏ?åןßüõ{ÿüÃëÿå/þþ‡7>}©ÿÚù̽½j!rûh`‰q!&Hj))F©zœØí…¯¯EîoÅŸ^(útý÷?ºÿëϯýò“+ÿë?}ñ¿þó>{eë³7.Þ½¶Ià´b€½2ÆûkQ|®@LköjŸ<ÞøòÙ¹¯Þ½øï¿y÷ÿü·_þé«—ÿÎ#*CàDD-#‡2ª×x6„gòÁý~þÚFùÞNnþÃ^øãOß}ïÅ÷îöðÊúÏ>¸øñÓí›û­ùJ¹XêYVÉD,6¨(F:‹%$¢§¶¦ã»3‰ ‰×¯´~üöѯ>¿ÿå[>{eç{ëÏM=Ú«_]Ë/¶Ã¥ °´ÃGÑRA3ê %¤"jB¥Ó +Ñ/Ê7·ªŸ¼Ó{´™¸³¹¶°P *¸‡Q8ÁX¢£fæ‡HÜïÖHoAÃËÁ6{=þâ^á½;ý?þäÑÿþó÷ÿõׯýîË¿ûèè`¾Ò*$éµ#Ša}7âsË4žÐø‚ÁµÒÁ…jdo>´T¸µ]ÿà…³=9xzcáÊF­䉆­Û„Åmð‘ôT®¾®§º<H²”з—fÎí,6SÔ~Oá óÞË×®ìÌœ_.ƒÏ®O%êÙ¡ñš¢bLĉ†]¸óiJÉ‘r6šnhšÁ °L“i]IëRÞæëñ½¹òùÕÖárm£—ïS\ª‘Šš@¨q«6yxŒKA¸ìpxÝêó)4ÓÌe—»¥ýÙÔµÕÔ;wæ¾|¶ÿ݇«?~÷Êß¾üƒ§ÛŸ?ZûîÝ¥g—ÚçæŒï‡|nŠSœ°Œ01ˆŠXÜ4 ²±d5²žºo´B‡3ÑË‹‰Ï_Þü·¿ÿð?ÿ?½ýÅ_¼<þlµßJ¨!…×2J¼p1„Ó½Çñ¡J¾“¹´F¢BZaòa±“‹¶R¡éÐÏ\ÝœÞík6 +Á @sÂXÜ쨕:5›3%HÇ}^£!-“ˆç‰bÜH(lT¤“ +Ÿ7$ +(X "JÅbº¼À b,ß9 ÓêĽ>‚gƒ™dq¦·>Óê—"ò\1|q¹¼\ +Îæ·¦2K%m¹êçÃ3y#¯ ŽñQ‹Ùã†B*y°ð©Q÷wŽOŒŽÛý.¯LÓqY(Á”wRü|1xn6þärÿÕk‹wvº›Ý²ÁÐBÉá¼’_Ìö.c\à90<Á±¼*UŽ“‚AµT,d“z*LbHfd™e%7%¹Ða äBZÍÂBtÌM: ‰¢ÁÁX^>™)Ã>.0€b錦DXZ`hÞå]^Òå§í>zÜÚp/Vôò*&¥ùPgCÆ•DfAšˆ¢Þh,t:˵Z_GzT‡?>jžðPn:‰*u&TÃŒ ZFÇm(ÞUÕ„¦ê‰H¨”‰Ì4S{‹åÕNlw®°¿Øž¯g[™p#ÍE#.·ÿø°ÉìÌm?cƒŸq ÛIX]¼˜­¬¤²–¦UËé\*ˆ%Ex­•½i÷…ëçö×ægkC‘( 8&Æ<¤á&#“~‹Oeºç3­£Df–å½Þòþ¹;0™M^‚”C¡t2YÎæêñhŽc·“ð‚c“Щ凞tbQ+²ø/aÐÁ´Ãð{ ·³Àv8sg@ða 1£8…0Ábu{½€Y‡#…x€ £l’4ƒ©ž%üÖ)«ÅECx£u9RáB „ŠÀ "­\¨úìî°Å+J0‚AŠB™´"'†]^"‚ré ;ù—ßýÖóã#“TCÕ0LÁÆÓBHRhÈ/a Z•Âj0e± c6Œvâí9Õ“½Bo/˜ª¹ â !ÚˆUÏê¥%˜ø Qc•Î¨ :1ássx¸)¤ç„DŸPË°˜¡å\ef—2êcÅË&eø¹” Öüx%5³>1j™ÑqJ­ bÞG\„î@4꛷ǽü¸WšôI~&!59Ñ#¥pªAJ ðV+z)ÃÏ&­B‡ª¬^‘’1Þ •$*z˜¤ 9ñ°Õ}LÂKÅ,ÉK¨op¡<%§8½ÊL«ÉV0RDqžd%I‹³JzØâ2»bõŽ”?ëå3ãnêä„ïù1÷„OôqyÖèX!qÌIŒ9kòcl Àµß‰‚–,t`Fsáƒéɸ˜ó17µ"ò±±Áò×"Ä„ÊÑòYL©Z‘ˆ +ºpMI÷´LÔJX½ÌÅh­ÈF›.2fT'¦ÓZ ôŒÉ+ŒÚX(Äk[b¢ÇDZ^65â¬N–㜜È)å`~Q«®%ÛÛÁܼ›MšÌ…Î!fÁO“½P~Å(o(…µ¿zÌŒ7!6x0i诎Mœ1Át¤­Õ¶#µM1Ù·ú•Ã‡O4¹„oŸr2!>.ÍÅf´ü†ï[}Ò¯œ¶“Nç¢^T99 ÇtfÔcvR~RC%VJŽMºÍ6?„J|0žeñKÁT܆ßzÞ$å—r—S³«— 3»1‰hE¿˜¡ôkÔ‚‰n±hƃVDBå¬îzÅ ø¬›IQÑ©lçpýê›Fcý¤ƒ´`Q&¹(—v¨X’ +L3 ¦¥GÈoFÇÜTÔËfh£'¤æ©H‹€.-X`ÕŽë.2‚K|bJÍÏ2FÃÇ%äÔT¼¶NiEN¯¡j‘Œuõæ¾ZÛ’‹ËBrŠÔ +¥¹+d¸áÀZl§Å%§±pÝ+üÁ‚lË̹éÍÛ^>1ä ,ŒÊ%'aø„ $]¤ŒTKÝ]&Ú¬%®·¼R9 7H½oA"n*fCµ1=á!HÑJT¤‹e«—·ûx.TŵŠ“4Àù‡«ëàÁZÖ.与)Ek.,DÈy;:XdÛÇ%Qµì—J£í”…„ù¸5ÀØ;tQ1R«ar W@çd©HhU!>=áDÔÃdýBÉͤy0Ëæ”;cFyc0d9éc­pRK~µË\¼Ké5?—ðÐQÚcÕŒén!/ÏÒÉy©°&eWOšˆQ‹‹¤V²aa•r`I;âìgS4ÈŽÁrî±öàa›Â¬’ë™qyØÏ›PU©©‹©ÙkFû 0X6V#Âu.5Ï¥JÍÍç@ÿ +K|ªgÅÂDc¢SJyWÌ­ú` 27'@FÚ.:bC"\Ó½Hm#Þ؉Ö6´Â¢ž_˜Ý¾Gu8\5ºG•µ»­­jëw£ýLï§7Œôtwó¦• {ø$-.½®í¦z—’ӗµ 7˜sJ-\«‚ŸD¼KFÙóýÝG¤^;aÃ,DD)¬ÆÚµóˆÖq1;È6‰HY;"ÙQiØ+Lø'òÑqHÌ;©„”žUò ^Þp1†›Ïcztf¨MvÞÉ$ì°Zé틉.hõŠYH©Âj‹Œõ1­DÌ ‰¢QãcB­8±=&:­VÃåõpiuÒ#Ñz›‰Îáø-›@ôÓÞNÄ•Ü2,æÏØ07CCU*>MÆfÐPÝŒ¨ƒP“3>4Ð'„Ì"¦·ÙÔéZ‰8Ö ++£–Š›Jdß/$g Žqå=k 4æ`+÷"*M­´†jÍ =æ &½Aw@÷ž;eþöiÛó&ÜF¦cJHÎâZmÒÉš½¥‚4)9ȨŠ¹è8H7&Ò:ÑØ×7€ øÄ$*‹Ù¾[ˆ¡m³ f¨0Ÿ™Úá­ID&£M¨l’ &ã”ên¡Ä„krvÖ‚‡A÷Òñi1»˜ž¹ÒÙ~RX¼åäÒÀ‡ÄÓ½©åKV:fv‚HÄ +›˜×›zçþÿÙ{'IÎ,OìN#´år¦Ñ*+Ud†Ö᮵–ZëˆÔZTf Tª +%€n Ý l‹Ù™ÙÙKήÑHšñÆ /üÈç5Ü=ÐH³=ÐŒikùu º*3„¿ï{ï'<Ü¿W]j•¡;<£ÊÁ_BŠrå¹PÝuF7Vï ·»z{ÿìí_î}õc°mÔô©Ô¾(ê¨÷ÞòÉþÓBˆÄ•&µåWõýoÚ§¿¬î¿áÇy­ouÏ·¿©ÎŸ%p×é]î=ùÍäꃳo_üQnŸ!j›øص–´ aÊ€ióë߯~jÿÚž¾(ÊDªn³þ|5+ÅPƒÔGJíBi^YƒÇåéã¼à•©=z¢õn„æyëø›áÕ¯ƒ]}š§íý·b£Â•F|u_mëÝK®v§ƒ›ÝÌúþâòBk%p òJ­xóWBý”©Ÿ`Ö,A”ꓧVóh ÕóR'ÍÔP¥§vnÌÑs¾v–æšåîÉõ«ŸRty5Åoä5. tú|÷«¿aÝù³÷ú»û¿Ö&×kY%J–9¿~öËá“ß·Ï¿WÚçqÒõÆÂ}5ΔVFlBþøóWf÷&V0Ó¸ “&Í4صšU¶0›öö”îmeçí§›Èz Ë1nšñÒ”Ÿc«y¡™ +i¾©¶.Üñ“XQ»o•©ª]{ô•Û˜Ò"Ìãí‰õ“æÞûñío)§ow€é;Ï ©J£K¹¾ýïÏZ‡¯ÍÞçͨÒÌè^×vßÕöÞé ÜE(+¯ÔÕÖ!nvI{Ìù\í ³0áý“Ý×€ÛFûpL¬íÓîBlœ¨½sÉŸí^½;yù“Ò܃תõýöÑÛáãÕþµÕÚÛ9{ù/þõÿÔ>|–}ª´ã/ßtO¾^üª¶|ŠÈ~ ÑÅônVªí^R¬±þÎèâ×;OÿØØÃù»|yÞ9|G—çˆÖ'õƒwFÿ†¯ÚƒGF÷t%‰G1Öîí¹ƒCµu‘:¨1•ªÁõöÇÏ~{òüwkP¶¨Æ:‹þáwóÇÔ†/ wɘý?ýÛúô6E•Yw¶¸ùõû?üãû?ý»—Z§9±©ú;?ýõÿ¨76rjNìÈÍ«ÒüëÞù_þËÆÞ[ÒèLN^UÇ›!ÉUÈòž5yµxü§áÙ·ã³oo‰i}ö4EWR´'ø{bó”©úU¯&çß‚Q*»åé ¡vÄWw׆œ=:~ö«ç?ü=À#"µ¬îµÕ»TZǤ»ÀQAj³Î¼}ø~¾š’㘫¶Ž¬á•7{Zßý*Š7+Wº§ãóoÂe5Á’°jÇ\õPv³fE}²žQÊÍýWßþ•îMÿùg‘òÌèÞvÏ~uööokâEóù/þÖl%h7ÉxDù 'ÔsB¸Éì?n}þõ}ðâOjeöèÙw½ƒ§\i¨ÖV÷´µûe÷ðu÷ä­;»åK£Æì|,(ãM ´X¡L´Îyuç%i ŠzÈ®2Ó<þuçò×þá[Æ›º½ãç?9ÃÓœìkíSoöåÎÍ·ïþòæ?ºøÖî^ Ž^*µ]orÛ=ùàÍž7vžïÜþúüí¿,{Ï—ß+­È[Ì j>ˆr–z÷š-Íe:>{ÃUæ¡xÎ%ˆ];êžýrþô÷“ZûÌž‹R’X›À¹“'B}4çzšÃÔ¶ä/ùʘ©í2 ¹óÈÜ8ƒË_ÎsN}z-WçŒÝ±zÇ­³÷åÙ­7¹ñçÏüñm^ðÍþ•¬ò¡1¸t&_JPÕåK­}Ãöî—½ÓrëTkAâæÈí]œ>ÿm½³óè/~ûßž¼û«ñ̓wzó õõó¾þÝ?&èrAjØÓÞÉ7ÝÓo;§ïZ'rbëå·µsþå+¤Þj¿›Þü8¾ú¡}üËþåti7”ÓisaËF÷\ï_)ÝsÊ &ÅZRʱ5¨,Òš®&ˆõ4½šäòB›q—¢·«ù{qTû|3šjŠ°f¨6 Œqœ?åŸ<ÿc©w–¤œ¼T‡LÈrUPݨڡ©Ñ½ôgÏ –9³{pö²2:AIJR™×çÏ÷Ÿüxøü÷£7zçH( ÀöÏ>Ä0-/ûR}ן½ì_Ý}ã/_ºãÉ›W§ C´öIiñÒY|)@ù8“ÚäÙüñOZ÷¤ ÔÚ¯ýéãÉé›G_ÿ~ùø³{"—'»§_OÎÞ°ÎÐ]Þ¼û«Ë·ÿjïËß/žþÔ8x­TºÏÞüîæõrB%Íù”;k{¥Ù ¥sEÚSpö⇿³{Ç ËãÇj÷Z>i}8ÿðwËÇ?:ó‹—¿Ÿ<úŽªLÌÉй&+GrçƼj|`+»¼Ó{ñÝßLÏ¿ îv”š˜=ÅÝ9í-cPŸ>ëì¼8yüazñsLЖz?%)7i'×ß°©ÔÊ£sù̽ºzõ‡£Ç¿JQ®âïL/¾Y<ý>º nx¯ðÞÎí›ß×ÇYÂîxÝ{^?±ÚÇZó”®F)?T0ÄÒ$Ïyƒ³ÇïþõøË?¸;¯¼å‹òü9S9ÞÊ[Ri>=þšµFYJCùršòu‚qæYÚ%3”Ë»‹_[K‰ ÂãÜ=ÖÝÓÚ7`NôÁhÕvAl¦éeô‚ äq=#ø˜Ñ“Á¬ .õÖ!]çX‡·º ¥¬Þ©7½* ppÈþñc¡yô0†ƒ‡Ôj|Ý:ø 6‹F7š\ïö_IÞ„1Ú½“¯§7?Àüï>ýýγ?uÎ~ÁVyÉœ¾Åu4¾\›3eÀœÊèÒŸ>bËcLöíÆÂmÏêÓSwxÕM—¦B©ëOõÆn(Ï3ö^’•¼ŒT–šûfÿÚܸƒK«w˜ã]¥zóWÆð‘=|TT©-%w´ù ¨u‡ª{ß8ÓÞþ·Rû²(RÔ+­ýë×?ÕfןÇð¢Ú |úÎ+søÈìž—º'ýÅíÿÕ¿ýîÿ&ÎVÓBƒ°j÷f|óãÑûè]ÿvõúù¯þâþ—Æü6J{”LTNÉÊIëø—˯þ¦²|™eËVçÈh}ÜÝ· bµ¨v9w^tzñ»“/]œ.ÎÞÍCÆråEFhDI˜”gsç•RÝ‘áÑ:¤œÑèèëùåwÎà®h£_Ÿ\»Ý#BïŠ+wñUyùÂ?ÍÐå,]*®ÀcneùXQ#­1ß¼°'¯íñ3ÒåFçàéΓ_›¬f„aö¤<ûªÔ¿r»gŠ7dL°Ëý3Tl…ó:dÑzZŽ "¶*£/›Ë÷|iQ]ùPDt%Q4x9”ד„Ç{{vÿ•: ˜ùÊ ê.E»`äaæ™Ò‚¯´~Z¨•šPÅ)“-ÊÓ[{xev.AÕ¿!¼y‚¶­Á¹Ò9ŽÓNQ»4£Ý àžR;´º—`Õ \©½û¼µÿÒ]ÉÍ#º2ËiMDïºÃGÀò¹YTš¼7½¹×?«N.ÎQ„0(­auöÙå̺ìÏÁËT—/`igBX‘‚¿Ïz‹¡ðåž3¹l¼š^ÿÐ?ÿ.Jèi\UËÃÁÉëÀ·²ÁŒ=kí=ï>o¼Ä}É߬^Ï°PvïüàÉoAáíà/øáF–ׇ¸ÖØÊsÖÕš»µÑioquñå÷^ï(ÍU•Æ¹ÖºVjÇ`sÀ2­©Ù<™œ½Û}ô‹,_Í +M}ðÔÛÿ…9~.7Ž•ÖfŽÁ°Ü|ߘ^ƒýOóuÌÖæ/wÿ‹áéûÖò™?:ñû¿þw£Ã7Y¹gŸ”w‚mÕ«{¯œÁ5*6‡/þáþß_ýî¿ÄN’6_=²O«;oÊ“Çzó@«ÌÏŸüúêÍ_Ö0'× Ær—Jý„õö1c‚Hm·µ?9{›½¼2ÈIÝœ¶bìAm~3¿þ¶ T³'•ff ¢øfvúŽÐúļ ”fËóoIµ÷0Ê„2 +¨µîÁoü ¬_AêÙõýßýåÿèåol¡+1:Áµ1}ZêÝx½kàMFk*Í#pI0«y¶¬Ô–¤9¤+{cL¹óÒèK»ÿØ\á?ëÎû ¦4Cõ¢õHw¸=¾þ•ÔB¥zŽ¯‚ÕõG›‹×•ÉK°„¾i nØò`$Ÿðü nÔº'g/~ê}…ÉU€/°½0S—Y±‘aAPuû{/jÃ+ÀFÑŸkÝËêâÅèò{wòe‚©EòšUݾ€*&µëí€YchwÏ;û¯Î)Ây{g_vn?îäl¤ø¬áÅ/¦×¿.*]@¿$fb šÑ‚‘&ÝhÑÌK ±²Tý™×Þ%‚»ÉÑ?p'/Òd)š¶âT¶h0²O9Sš jS»V÷X®‚€yÎý•[‡ÀŽÉ£ae+ØÎ÷ÔÆÁ?^cï}¤ãñ;Ö›of¤†ˆÒÌ+M¦º,h­$ð¬Ùw h £Ð×ÚÿÊè_¨µ{ìŒÎÅæA^n€ýWÚ'q ŠhÊz{Bõ€´çy¹“ä|¨ÐÖâÖ^dļÏ7ŠJÐ>/;Æ+Ãáù7)¦ ÖÝY 2C©8½ Èçãqö@,ƒíøø_Ùáëû`³BU|Tª˜µ½¾Œ¸í6s-Éç„$­Þ½$Ô6-”{·óÓW\„Õ#œqÑ[‰Þ’µÇ±¢*:=§{¤Öf{/#õb¸òjcúTmlÄ,c£B-Ž9¤>«ÇmAuÔç³À)¯„1 +²¿;:ùnpò½P?_IŠaÔ$­Év^N`f¼h ¼êBõNïR´{åÞ¡Vó¼“áʈÒ*;VçTi˜gh»2<«ÎžÀjrþYÞÙÆK®ºH¬ìæÙ +gt;/A¸Â A ¡J'Ø﫨-¯Õç®Û=¨ÍõϾ®-žFP Ü"°!­ôX­·²MmeÒÙƒëÖÞ+µ¶—!­´XêNÿR©íBÖ¢x7á€á¡4NüÅ×jÿKµ ¦ “emÚhPFU +Ù“¼ÚO +-H?ƧpÍퟻ£Ç@¦öà–.Í2R#ÎTˆ‚I~–sÝê{oZ‡ïÔæ),´Þ:¢Ýcw›óÛöþW9±’¼¬T~?zñû«÷Ó¿ü^lŸ¥(L¨3y’“Åi¶Øe´NNžývpôÂh aÖO.$èÊ™ÁûW_µÏ~¢…ÑêÍé…ÑXâZ‘ÛT”=‘ý#€k=Ézqg<,Mm/%Ô¸žç+õÉZY¬„s¶3Ûy xVhåµ~Fj²â¼çÖwA¡…òë튥s ¢. /GôØžÙ•ÛÛO‘Z†ubT Uúfç‘Ò8ƒeÝ,(PkP€ÖYKñ1ÌÎË}º´çž5¯*ãÛ‡q¦ÈW[‹ÇÛá‚+q–äýCÌš~,/Œöž×F—iÚMQE~¬hmådÖX̓í,»Çÿiÿçbr3Åú[XÐèÇ霸ÓéôOÞÚt§OÔæ ìÌi„R ιfu +9ŸÂM\ vþ±:&0N¢(ÓZm~ú²ÒÝ%¥ëöS\´gf炵g˜ÔÞˆ‘ÛiÞë6—7ÒB$¯n¦„‡R×héÍ“,UŠåE)¸Û«‹0•HVæÜaÀ)V?ÃúˆTì‚Rz‚T„X;ꧤ‚9KTï§h ¨ T“E—Z”5‘šçÆðK±~”`Ìm„+€Š®ŒX³Cé=¡<nt/Zû¯ÛGïIw-‚íí–õåÓ›ïöo/q{ÔØûrpùÚ;_Ï‹@ôNï–/ƒZ8ä*s D³±×?ú:Ãx° +jó¨4]]~r‹tf¹“å*„Ò€OÌÑ•…òŽÕ»œ|ÓÞ}-UvÒly3+Æ€‡5ä®´.úr|=Š¦¹Ç ½uÖ8x[?xÝ<ú`O^d•>X£±Ó=x­µŽÈáÊ.éÌÅê!¼<è.Ç—+½›7¿Ç´nÑÁ†ã渨õÌîµ5x<³™¡)WöikF[S¹v>}=#<ØÆ )Ø60»¦œ¥Ê®ÊÙã¢Ú*¨i˜ž¼,õã¤)ù;Þô™Ù½üCøq(§|±…®Å)Îì­F0L®9ýóöá»Gßÿãôö·jãp-ŠåŠRgùXoìAfTÆŠ: ˆ4” _ÝLsÁ†—«G aÌþcÎ;HR^UewH©ÕÍž" Æž¤É +¦tÔö Ô2*7i½Y›^bj3E¹”5æJr¯J‹W ,aÝ»ãO®yo#L¾zÐ:~?»ýmðäÞÚÇiÏüñEŒpigÇŸ¿Ú¹ùõñó1ºúÞ\fHË©-ö®¿õGq\ ZÑ ·%oɨ=œ÷ÃÊðçí—P­E¥h \ÒZT@ +¦I§>¾m,_p•=º´@äÎvA…u¤4Ð&³ïM¿„”.Åî%2d3NRR…V*¤ìyýsÑß÷”¥Ë”1ˆÍQ2;iª¼• ¬ÏZB"ÁžÒÐVŽ«mgà‚8éD ]©2|(Ì”‰Õ:2Û§Iªò_=Ì€ÐJ‘âVÞˆS^‚ö@]Ô&7š¿“‚W¡f«§Aѱµ­$·‘úÀB@àªÂìO·Ð@%°|e¯4||øìOûÏAý¨4Ϩ +Ð$”Ƭ=¤½]Ê?äjàõ^·¾ ú ‘šì ¤ÊP uw6©êFN×½½öô)¸4m¢RÓ»ààzGïþU¤(nf‰¼àò•¥è/Ñðç¬n}þ¸yüŽ©í´nš© r‡²§9Ö +Ò›{Z÷ + ÜxÉHA"Ô–T=Ä´~ÐsPëè½÷”)`rs3E|¶•ISãÌ##ŽZ„Ú•¼c ´ê\­LÒ”CY7¸ÑgÊ;fﶈ2Û) .©*o&iÀ4TlŠå¥äïçÙêz‚ûùFR›µÑyQôVcÔg[ÄvÑÅKûÚäëÊîkÙ›Åsœ[6OÀ-þÙj~-Áo"VNÒþ™Ö¸Œå•/6ó‚Ù›~ý Tü³‡ixª•Öi†r¡Šó|¹(×…òXöF‚ÛeÌZs’ÁVZ%£¶ïô/oZ”kð½ +¶•fÊ€êà:q0_¥ ¢ÔYw*×ÙÊ”?i€ ©$ƒÓƒÎ[ª­ 0Ô½`gL}«sL•f›Es3'£b]oìóîØnŸùË׸3+š£´ØØÆŒ̃ÞË+íP^aì9çâÆÛ £˜½‘}R.ðPÛ6Iª—$ï€u–¬;‡Ê &&·2l5„ˆ6À¬™àƒK2•-D^M`B¤ÚàE¤fÏ gǯw/CˆòEXî,W-H Úcæ½6}b÷®¸òaK`Ç0­¾,Œ9ë)ŽYbyìœç«128ª¼'\ÂœŠÕÓPÁˆa#ÂW­¹¦‚ݪi˜ù*mOÀAQle•,WÇÅŒÍ ‡È]¡rPž~š!Ž›+<šfû/¼ö~–0b%›yÚ-Ð<¶ãEŒ”†““ÿâßpz};'oçU(FÊÙËq Ærl$K+þœ6:‘¼œÄìí¬ 2 D.7Q¶IÓñ¼X”{„=#íENìo$˜P‚ŠdV­&ÿt ed¥\‘et¯Xïp=«ÿl ÍF)]¹´ç$½º+ú‡¸6Nàí‚jÂ%Ÿ1{FcŸkÁæ'Œ•@ŸR`K‚3Vª{fëHôf¬Ýgvš±‚öy—Û igˆ©µ¢ìSf‡qF\e7Ï5¡Á8S¥yÜ;ývpùƒÚ¹L<1C—SÆø)¾ÐìèÕ>ÊS­¶jŒ|^í¦Å–ªýˆ¯Ÿg%a´>ùñ¢>Т3¡¬’¦ª‚·‡©í•­üVœ vÑ m”ãÛ[+ŒÚy¡I;óÛ%F—v1kN:Ë0é~#÷ +"©¶³¤…Ж\™ƒÈêÇdi'-5Cˆ Š+ÏûA€cYÆ“«‡Vÿ±Ù¾ŸþÒlŸAJpjC.Bf#ͦ˜*¦ õæYïà=[>Hå$—(êQDÝH€¯Öé^=…/ê«Q"RÆóbü`Š­äåhx¾¼¯5Î¥û oËCŠ‚æË ¥~*è ©Unà<¤Í + "9q3A‡’|² ©fËoí5‡G[)j#A'ô„WÚ #æZ‚ûÙj2…iF Qw×"dQ¨ø:iQjBv=!Ÿ­¦Šl‰ +ºé¹Ûy-†˜qDåd ¿,å&p{5Æ’J§>½Íñ^QàŸãP5Léß«qÞ©ï|ý›0«»ŸmW¢bAj2‰!bŠ0@í"\² òª(T‚}zõ¦à³’@ñgzó„÷vƒS.¸8Ç–’¸ÇàD¬‚"J’*€–[8ƒ+orã ÎwlÂ&×¢¨ +¹GuLnƒˆo•ˆ‘ú1ÌÌñÕòì+­÷Té<AwDªƒŠFå:xñŒÖµ\¿dÝ]¶´K˜ã`ÃØp1SÔy«Cu¡´+WÏpcï (*ÕNƒ¼RꪷђáEsžä:ˆ2TjÇjõ(hãLŸz$f-3è\/W`3“¤¹ž¢Yk¨<' É–áÀ‘)ˆ—s¶ó\IÐG¤Ài‚,¯¦ä­œŽˆÅ?dìñ:H£‚š9ÍV +2èÚvAZOKˆÒs·Fûáýó—?Í/¨ƒ,ßB´~Œö6òj3¡<ÃCEð÷q#ØÄ2‹Y@µ@a+aŠR{ÂÊÓ%LêªÞ®Z^âr R}5N~lm/= ç×#øFJÌ +m8B0$@bÑ~°·¦7À§+Qf=Î…Åò*È6Å`RëÏ×r?ß@Ò„›ÀÌͤð0LÁŸqÌeô‘àLr”»Á6SìfZU†+í,]‰!V +3ÝƲ3¿¿‘R sDB‚*Ç'ÉøÒý"É®&™HA¡ôÞ8dQž+Å0@W.’ãPÞòr†O´öuQíãj•š[õ³íâÃ8ÇlHŒ¼Xãj $¬ ¦÷Q¥Mè]x„qs5Énå$È48©ŸçŽ:´1É•fàP0µ£6OÄê^Qífy`ª €œÑzŠ#2¥ukÉ–¥œ"ìÆi5AECQl¥“@”¸*µMª]àDø-®´ÀµÁ¬ÚÃgRãÒR©€YNw+uðïaDÉp­y&ÕOQµN<Š[’d ·Q¹ F;Z¡Ž¢ÅRÝ^ å´Õ8 ƒd4vP¹ºÓwWª_þ±\®®§Y@'`º0ª­f8\ŸÐö\nT—ÁÉPž)ÂÔJƒgïÿrrü2ÃzY©ƒšcDd…àÙßK­¢àé̓͌´D/º9¹ÏûgJó¤-ÖógÜ‚‚Ý0GÌ0^Ö»:o9gô ”E('G:Û9 ü&äÛz’eStC¬ò¯E endstream endobj 64 0 obj <>stream +™ ]‡YEù +V1À˵#wp»•£y 9Êç%$sªhƒÜ +å$ƦIûÏײ¡´”¡=ÆñΘT;IÔ~f7’ +®’t7gˆ:„G^íoã6¤Jp:ZíÒö¨ŸÔFbå(/¶S¤›çʘÚâÝyyøDmžgØf‚¬FñJš®ofÕ@(neØ2"5ÁÿàÁûƒºÝY§µÏÒt%Ÿ’a¶³AÐ aäY—´ºBuÇ?1{× mrl“j¤\-wë³[`ö¬ÔÄàì!&øRQʨ Ct0±”¥í, Œ<„TÉæVŠ%¤šêÍY½‹ +U›Ø£—¤AW7@óÄ0PPbe‰½Ì œq­-xóS?Îþ‘Ü<¥Ý]LŸGÄ‹ØCÆ8èj}LÚ;PŒ9¡Ê–g!Tÿç€jõ€ÐzÀS)d<8ýúâecù*Ãù Â4[ÇP)ÒU°™·ó2äLkïåðêW\eYåÑSP‰qÊ`F5ÜV<&ÖÅòxû£¯ šC)}pÀ ëq5Neåkk ÔØìØAµ±X;Dä:¨µÕÅ/ ¥xÊœaú{9®ÅXs\ª¯FŠ1:šŸ‡É"€IçJm_-žüáêÛÿÎîÜ|Âãy5^ÐV£Ðñz<¸Ì•Ú¥þ#oü˜4ú›i±³xÎ9³^ÚÈi›E7Á‚JYNNß¿ù鈫bªIÚGä¾Ú¸ Ì)ÇÇn¥WÍ0>¥ƒšÚZˆ“Á9 ÒèLJłÒÞFm`¶tÄ•¶ tu5AS.b9Ð+¦4A烓."7ÌîåÀû×bX)E8WFXà+G™„Ñæýypõoí €”0úçá¬CË&ûi6øîÆêžkõÃ4é&‹:£uH¹E¸Õp*’£ã¨Â¹‹ÊôYQmG‹z(-àB•–k±œ͉®Âº3­s%6N2œ +š,› Kã„Žª­ŒÐÃVv*…Ê°+Š”32ºJãQPtàF)sÞ-ŽˆÛY–±ÆVïqQëäéLÖ +úÏ6òIDcôN$ˇøqø\@~§ÿHöÁ)ƒrsÜî!¸ì cã@m|7‚/ã†çßLý Y×ì¿fíÀ²Þ,tAh£|³(6¢ˆ ÅBéÖƒŠ ¥yPq)H$¾™qÜþùZúa_‰Qkiq3§$@´ãVfXêê­³ë‡sŠPÚC$p©^¢hlg„Ï7R_lf!m +lR*ÎÜŒn~¬¼/O¿*‡[YãÓu$Bë‹P1‰À/’¿ç  î‘ÚÀòPqÃ÷²· Ú,NùDõ¬~øþòÃß<ÿþoNŸýÁÌ 1¡Qš¤9JReÐÌ‚;cì)i +rs» mdµ- SºŠ*€È“0fd„jâ£ÓOÁ)â i¯(µP©',¾yÖSËóÚø’0\´P©±…( +|«(0u r(`H•ÎÄHlãý‚Ü+Ÿº“'°ÄPnÒŒ•8|[‘$ìQÊó`–Œ5þý"Š­Å1”÷rŒ›¤œ‚Ô ©ÔÏíÁsˆ(”dV·‹ù€7ËA§ ©VÐ:˜5¡Ý%aŽ¢¸¾™¡I«ÏUv*ó/é3Tfx@¶2ü*/Vr‚D–b<¦´Sš¾*O_\¬DŠ›).ÇVÒTоtWš­ãö2Ç7ÀêÚ¶‘ÛŒ±¼°•¢3Áw‹mDî¤Y´çSŒMê-«w+7Î¥ ñe¿¨ÖÀG'™kÌæAUA0‹þSš‚ÖJân(%>Œááœì/(?˜JbcHm#¼µ´´–Ö4úça ”I’p@‡å.iN1u벞dÖbì<cÛA3e3 *ýc›³¢ÐáÝÝHÑ¢ì1[š³ƒ9ý¬f|PaÑ¡ˆÀe„:Håd&(ÛÊèÙä敃9¾o’ ìë¡’‡m×ú^“¥y, ¨_DC èg«y¾–çÚ´½ÏzgRí,Ë×72 !P(p·wc´®({'A´7 } †…T[“ ·i ž¤…vwÀ¢Ò>ís¡4IZ–-1ö¨wðõôòW„= æ6¦’f´&ᢙ›rãRíÜ2¥ýî<ˆ ¡4“¦\™à +^²e[4æy¹bÑÚ[i®Àz`ö[;O¬Ñ#Ô˜§h8N)‰„ÚJF;#qΤ2{î/^úó—i®BtQ(¤ÓÃ(JÁsÚ¤)g+Ã= cq;K—>Û̯Æh` +Ê;ÃGÂ~§VÁ¢ZW·ƒKzøõ$÷ù‘Àì|@@ÈõjئW>ß!æô(VôS§é2Ï×Íö¥ÞyÄ–– L}»Ç7Òt‘ócñ³õ|¤`˜­“éÕ{gxš•’‚¯ÎÅòP3hxÊY8£§õå»$SËò<@%ßÜ.ški~-ÅåÙJ©{ÙÚ}Û9þÞž¼ ŒÕø.Ž9ÀnIÖÏHmÊ݃,kG`:@’Aù¬&HË,[ãÜj°”›Y Øg=Ímÿ^}¼× +d*"ÔwªÕPℾÛj_q”ª—ÀA6ÅÚn^ïŒÜ:oþ vn" ?ôF‚LUVÊ•#Té‘ÎbÅe­1È6Ðù[Y˜U ŽJm1îŽ$/´²\“µ§E¹#K ¦J“òô¥;þ’r–Y¾î}ÆêªÕ™;º¤J3{x[Û}UÛ{‹[s€\¨£qì çÕQŠmaj¯Έ‡21C*EÂêÏÊÓëÿ@ `+¦B¨=\mU_n,éò>"s|p†$IºëIb‰„+1Î[Ú‘O¨Bîmeä•(É+i¢Î÷ÙP˜@”5Æ•(íϷП=L’Rp’';qÂ%í©Ù9/o)£¿•¡â¨¨V¦š¿÷ézÀì툮“ç {W*uÿùò^ÐvYŸ&;g²æ\öv^ý<„ÆPÒ#É4€þˆ½žàl³˜IH>"VÓ|­è,½åÛÞÉwfûŠÖ‡PJ¬=¬/¿ÞÀÜ(–´–¬·\BTÈ„ ®sàªE}ö+-Ò|ÜaïPÖ“ê˜\%µ†TßsÆWã‹ï¿TºW)®ÅKEtÎ$ÏW +RvvƒÎž•ÝŒÔÜÆAl3Ò܉\¯»›UziHê@¬ì3Î øÓšÕù£à< bÁc 1Qeæ4E–“˜¹-¦I]¯íÈÕ=ÎÛÏ°õ(ê„ œïÂ*/‚V8Œ'z;´»ÈËî­&Ó8?´ÔŽ!jÐî‡,©ù½+±²Æv« AÙVú—¼9ügŸEa¢øòLn_0•LíG!3CÅ­¬ "Ôúñ¢+Ú´µ Í9"t'øþ`d3ÅgȨÀºí‚žâ[@.iÜÉãv¼ƒœF^sFO¤ò¢(µ®žeJZuÑœß$5Œ™ Æ/hƒmÜ]Ï*Üá.&µ@ädh#¨ )*MÒT#Rtá#V’tF¨å¤vŠó£´“S»Z÷¶~ü[}ü¼ w  ÒãdI®”G/iï$'õ#Dy;¸vÅKÓNÐb^ŸÂ4‚–õ.Ô#x¥P^›H-­Ü| +0ŒêÕémy|FÚ­¬TË +€cçti/Ë8EÑß*ˆ¡G—Èi1”‘Ã9m;+g©à냜è‘Zµµÿ‚­,@æ„& àjÑw‚éƒ(nƒ—~•Bí˜0F1ÜÛ˜¦LÚìpN‹Ôë öŠÆ¸¾ûÔ¸„£Í±ÕH^$­aN¬F +N9Bmwpò¾½ÿF©Ì±R ]÷¥É“¼Òƒ‚Šb%°9¤5ó&O•úîvNŒdå^†ä„j-hÃ0^‚bYKÒ¨X•Ê“8iaúQ'”½ÓÜy`R9ÆßL EÁ šØZpûUAå1ªNi{IòéF~3ÍaJÔûzFÉ-søH<µÏ@–|¾¾…}€ùBäVŒò’ ‡µ!_>€´G…&€çƒD¬ +Ö/ +9VÚÁ€f§ŠV$l¦Ê¡µ™’Öã| ©àr;¸wƒ¯ÄŠÆa `ð–6†E¹•¡KÑ‚fÖÒ\uµ m“v(¸­»’“¨<’ý#{¨XÃ!äÖVÞ|˜Ây@˜à©q”ë !aïS¥ÂZÀû¤H Krõ¸{üK­{‰Û“f­gXˆ –\Þ*¨ WÔÖ;zÉùËD°5ú$ úJiEI{-MG0‹)7Uå…ÊÏCȃ5˜ ÊÁWt9L–òr›ó÷ój÷AœKS•pÁçÍ,ëg¹ +¼¼ w•æeF쬤ø-(joG¬‚äKÆ<1X½¥V˜ÞÃÍáfÁXI‚óÒj@–‡Qy#ÇKþ¢µ÷l|ñAinæEPQDçÌ‘Ù< +¾Ç¤+¢·§ÖO‚kÔKËÕ$ýg+ÑpN**¨š(U&Ü¥óÔ™¸P©EõÏ·‹QD$QA´‡Qê‹m7Ê— b¶Õ:ùxI³‘3€&¾H +°4B|iÁÙcøè¢Ú‰’N”ª­œµ¬™‡öà‰Þ8H`F¢¨Kµ½îáÛÅÍOFû–i%Ƭú‘eÆžh¬gdLì¿ü;ÿ[DV_‹±¸Ò·ÛWP³ÿÅŸoÿl5ŸaêóÛ'ßrî Ž桨4·rÊVÞ©—Êž½ú{©vŽ[ãmÐÕT9B”â´Ÿºyiµ“¢|±<ÃÔZ8φ`ÅsF–¬3æÒŸ~‰Ù£ D–+óÁñ‡WÍIÍŒTß&K@Ê ÛH{†ÈÍ +4锾;zíÍÞKõ³4íÊ6£weoθ£PÑy˜5“tƒsö3ÆÝ_Mò[y5¸)²÷Xn^¥ÄÆ¢ndE”«FPíA_‰â)‘ª§Zç7'¡‚üE”Œc<D©HÑ.šc­û¨{òï=åíoaY„ol¦å•³\¬î&iŸ¯ÍÿÉ›½.¢µö¹Úº¢E’.­å¤µœŒ+]±´Hàx¦h¿ ¶´Æ©Ú¼H0Íà< õ¤zšõt)Ny`r|ákJ Â¥ƒ/‚ÍàŸ›i\<bÉèe8Q;¬w`ô+ͳj=Œ2kq&œSB)6Zãi5Œ=Œi²ÄÙ3Æ™ƒû[KŠ·1¿¥qmÒ8ú–¯“Æ•zÁœXýr{'Q~¶‘ÉJ}Ü9[½é[ÒžCÔŸ…¥<›œ@åÚzZø³Mt-#SΨe!èë|#,–çÀé  @À|g“l •‡ÞøEçè—IÒûôan+-A:Ô.¤ýç›Øf^%D…Ù;F„úVA7{æ·ò_óÕ“õ¬õü'2ºÿÀÿWã>»6î¹kã>»6î¹kã>»6î¹kã>»6î¹kã>»6î¹kã>»6î¹kã>»6î¹kã>»6î¹kã>»6î¹kã>»6î¹kã>»6î¹kã>»6î¹kã>»6î¹kã>»6î¹kã>»6î¹kã>»6î¹kã>»6î¹kã>»6î¹kã>»6î¹kã>»6î¹kã>»6î¹kã>»6î¹kã>»6î¹kã>»6î¹kã>»6î¹kã>»6ºÿÛ"Ã\ùOdüçÂîï÷>©ÊŸl0J®ÿv{GÇ£ÃOÌO6: æ𘟠Žg{»½Ã‹•"ü(OgW\Ùú§g®À“V¬ÃÙd¶ ?, zËÑöJžŠÁŸ$WR+Éÿ«^|+rH<ƒ¤ói$›M"ù•‚dãù$R@ÓZȤVvþŸTÈÄs¹d&—Iæ’éÌÊ2x?Ë¢(‚²ù|ž”ýø*MÐd¡ðÿò¤ÿÛÇ-ÿcŽiùÉôÿ“ÝOœOЕ­í•ªÿÉÁˆ+ÄõqêÄÙrTü§¿²#˜Œÿká»ôI‚Î#WbWªæ'•¼Cìãßà0ÐÜÉ®´VÒÉÔJ*™^>ýzAðËÿ¢ÿÿÈJŽ1Uøx¸Ù•ïðÉÆÆÇ# V¾¸’Ï éÔ'Uå?Ë0^Õb¸‘çý‚X+p~†®$H?'vµŸãëi²ôO ¿s„©]DnÆI'\ÔÃhÐB#Í”³Œ—(êE¡Bh-ΓöÕµŸ•º˜1‹õ¾C£9>Ç–0½C93Ü™£Æ8«´cŒ*(¬ÝÃõVŠ*ú˜s—¢”—»)ªZІi®Ãìí¼’f«y©ÈmÎ[&¸êJ’ º5Óe8ø­¬”ÄRïKþ>aóJk»¨%i7N™9±¼‰ˆY¡Âû{bã\é=!Ê{I®&¬í¢.x»Z÷JhóþÒ¼ÄíyZjnõ¼ØÈp~ ·6râF’‰£rŠÐ |9ÇWÖS |Všö·Pc5#¸ákp`YÎG¥f–«¦˜šV;¼Ã$Û¡ÆZFÜÈJ‘¢ÎX#Bën¤íœˆImBëç¹z8¯‡Òb¼hàRÓ¨Á'&q š›)áAçµ(jÆP3Z´ R7/÷²B;Ã7WÓb(§ÆŠv’(¥ˆ"´à­b¨•¡«aÄXOs0c1̉ FÑq¹Cªý$YŠõ¬òYÿ<Œ¯&˜“ÂŒåæaóJ(Í%Pu5Šý<”[MÒ[y^FÌ ]K“^sB V!'Ô£Ec5Š‡sòFJXÑñ¢ãšº^àÛE¥)Jï\ögÛ9.A”0uÀ”f¸ÑÛÊI[Iø+MÃtu“l-T0BY¹À×9gʃTÑäIAªGp3T„ƒ*ý¢:È3eÖhn¦™SZ¢.‹ú,+õsò Ë7‚6Þti;˧(‡r¦rë”ñvq Ö±&a™¼ÅÙ/þY‚´„MZs­óT¨^äÄN”pÃA³L-‚¨Ûy»´ëH1øUŒ(ÇðÒFF\Møˆt$«qÒM±UÒš*Í«Òà¶sð*ÁZaLÁ_;6†Ï„ÆyNæä."¶&‡ßÔ–/£t)”—){áŒ^ªíÛœ2Ì)ý8Y‚™ŒÍ$nÛlCÍ/Qb%J¬Æ¨ #,[…ÙN õåÅ©Jœ¬$˜ZŠ«„>ö ÊIÝm4h»]ж j(ô!‹” åÄ1};/…ÒB$«DáWi)…•¬ê¡ZÙƒ¼z¸m$؇âóº™bEl‘)¦•$k¸"õ{'ìO· ¾™…Ì,Š-ZQê0Ž™Ÿm¡›I!’ö1ÿt#ó0Œ¦P=Ï5 B+C¸ÂF³ÖôFšƒ#ÖjRÞÎë¥qº’×F ÒÙJ³‘¼ü&!J&1G(H•c¡|„JÕÛ»yý×½ÃÿìA„0ÇAoÅÞeAí†ròÇ"²#ˆ±‘‚±s +”e I½—¥\„ò{¯){°YW’tŽ®(K±~žeÊiÒØL²»" ÓL5NyQ²’•z´½kG¸Ú‹£*­wÕæUg¥jNjÁú¢Ú+/›‹ç¬;M’v«²¥]Ú;ÈI½V¤‚)JvUWâÄzŠ…pK3µ8áç¸vŠòsl°sz('¤I;/xQÂL0çí{ówÕ·îô6ÆX9Þ£ì!áŽ1w’UzœR>Ó›§µñ­Ú8ØêØ´=ìÂÌIŠoeåQ:xÛrQjSìZŠ_K ):h ²•?ßB×lŠ,A&¯ç•5DPÕÓNs]ª|˜äýÕóLXóP^]MòŸE¨PN• ç¥(ªLÁŸ±¢É)1z5B®Åh€…h^£Õ^Ql®DH*øù[Å•0þ ‚måÕSÅ¢LPyÄX»´1Œ<[ :É¥8^Îrí4Û"̹\;K³þ§[ù•0Œ“e«qpØa@¼‚ñézîÓõìf’N z–i&éz±£3Ïu©X þj‚ZIë-MU XXÐ׉Nš°˜ùÅ6º•æS¸‹ +-xB–®¡|“… +<†%þ/W¢E­-Vv Áb€{t-J””·‘ù·?ÎCš­ÄéR$è(æ2j·=àl˜¢ËHЭd˜ ýÕŒ°‘ƒ¶CzŸuÆAÏ ¥™æ«±™dª9¾Çí¬(•çÎðš)MoBº£œÔÀŒéLáÏ,0/]Ñ«¤=x˜¢×Ó|/ãÚ(ÃÖS4cy3'o!jè//nä”mÄAÄ>aÌPX»™ KYÖÂBåFQë"J°u>W=&ËË$_å+{¼·(ê]Úäõ–Òºhí~pFÏÒœ¿YP6s"Ĉúy”XKó”1ÕÛ·rí<è +¦t‚ö¨À¸°‘&Ù͠ŵ-ZiʃHõ"d9Î4Rl‘'¼J ôÀÞíÙzFz˜àVB–©¦ƒ­áµf$‰2àL"˜yeÛN‹[ia#N?Œ’ñ¢•¡*¡Œü0Ƭ%øPZùt#¿–`"E ‘:›m#£lf”P8&¡:KÕ!mBˆ¿MÒ~^ª¤‰‡ˆ›i;ŒèÑ‚–!JIÌþ|«¸‘ä×côg…âÂ4BÁô… ÂÖ$g™}HéÕ·‘–×SüVNI1õ¢¾Ã{´=Ù.¸·#FñÏ7³_l¡9ªì4Ž cœ`ëA\x¸À>„&%nÎáçqÊOR¡ö‹‚Éii®†¨CÚÙáÝ*Õ“¸ÉêƒÒðiœ(…5Z4sB-hz@hBqQæD®íÇi÷‹ÅàÍ[˜ÜC–í1 ^Ù}óáoG—¢Œ%ÍœÒ)lûjýÔhŸä m”X1StU§¤sH:„¹ÄŒ ÔN‘«PÖ8„Z@Ö[ˆ•¤*¤1ÁÍ)ªåÊþøè}©¹‰ÈY®Æ•÷ôε޻Õ{7”;•Üù³¯þ‚©Nf Ú$ÓH²uLéSÚ›JɈ°Ü¹ %§»Xðç^¬$T+å}º•]OB8f ´U]OIá‚Çiœld3]Qnën  NŒÈ{B¹g 85[tu=6àG4®16i¡´NŸLxØ<Âä0¡¢G" QzPepÔ@OX(0”q#=n¢õn äqÒ.Œ¼C“N3 2²C|DO3 BP6*§Üš—HhÜŒÙÁä k@™Á¯ÛÈ„‹ÉNÚ|ffTÈHÃøµ  kÜHMšpÉÃSN a-¨¼²òÆ qˆP?2 öD†ÛF, xo¥R6Ö6cöF'Í Ô* Òê•iÈ€CÚèÔÍ[˜ÇŒ¸ A$‰ìêo +;‘ú麙”—Kæ›[±úØŸ“Ï»„¼7XáãPaÉ®~²×¯ÜNH̲ñ6¾HDdlÎÆå@ÐnVn ä3¢Ãã~`¯dŠ«Ô‚G¨ªÁIð>WÈJ¤¬x¨Õè +äp0MýMú…·P[y3&OZ|j«Oc[„h !1>¥<'ìâ‹Ðà°ìP ¸Tws +…j]~;•Â¥¬¡Á7’“Öàh¨ +Ð1€ºqJhp‡@͆´˜ï¤•…ǘ´”†72¡2ˆ@b|qÌÂÑ G&ÃS­ÓvŒúŠ`»#&Íàñ©íÿ4¤3+·‰JB/¸ÐÏꯢª…N€Å[±!bå5*Ö×ÃC-»P0Ó2Ð2̹8v@9Vœ³àqh^ “3xÎO¸…"'Oc¡ªÊÁZ©˜“Ïâ¡:â/!RÕÌåŒtŠMΦ§O1©Ä+»¯lãòÞp‹ŠÏz£}4ظ Ï× TTˆÎD¥›÷„z¡jjÀÛ*»`ôʈX²Ò ËÀº ¤&o’Š ¬d\mçPÉ ŽŒ›™´™ÍBæ…ÄJ…»bzQã‘ÔnQ…ôX°wÃFfÔÌ‚C¡\ž—;zhCµ¶^ßÎf tO™¸)ctK°;CSÎ)+k§rÞÐ êï¸Ù¢O«”.FcŽh=‡Ô®['íj;d„´Î ¾m3‘g¡ @aÀš{„„‘)‡¤vø!8¸¹¢‰…½lÆàöߪ²ŒèP(9 +º*Qà„° ‰x…‚ÖÎÝ2jÕ¡*(vÂÁdÀpñ@—šZw<˜Y¹Y,žÇ‡R‡ 8eaá-@”†Ô®C“¹Q=©w…HTÑI#èd`ÂLiÑQˆÄm¬Ëé8’tfÏÑ¡ÿ4jÑ¢6,N-û³F  ÆHjµퟆ&G§ì k~'•»NR,QkÂJAÒ9ÅýUJª{¥ŠÖ£´ &Õ-\zÜJ½-c¾,(q©9o¸ € æ¢Ãeày.ÛÙ<—˜ã“XšpðcÒDDM¤¬ö @‚. +”vihØ`Ã@DÙxŸIÎ*U„†ÜbUÈmëh¬¯Ü8QŽ6©Ø„Ûç‘šdbÉ_B¡rØœÖR» –TlZeÀ¦”»£A\ B&ÇL”ÅÃMP§†ˆe.»LÄf ;,—H¨ŸWªbáÖ$"1àP½ ~Ð}°V:m!â°@¼0ñÃâUfØDˆÛz§hÃ"аã°; S–tÊ­3{zw4íˆUÀÛкÄÿsSº#ÅqŠjHë37.¦†¬ä ) ‹IƒOZþãFÂà5nLïA™(S«,ü¸™ÒZV,6ee¾2ª3#’“Lh ÚÛÅQ( 3­¶qn¶@„{SNñÆ5n¡t +4G¹¨'td +ÑáîŸÇôG4•™‚ÀkÁ¢“&jxÊ M¡21Embt¨ Â;¬÷šrÝ¢²*úé­dÌãË*Çún6>iÁL™L†&-‡°9tñé,8È+·wu€º + +ï‘1•…4z|N:>açÀßaú§†ß`þŠƒÏjìŒ7X´Ðq"AƒŒY™ ‡æÏo`Á¦O°Éé)D€Ÿ²+÷Ž²sV"Ê%z´ÜÅ¥2Ÿ›µûò66ë+<©ö„  x…@yÓD&œl¸’KjèðØ”'.É%øÜ¢“˧!áƤÛ?nFÌ D6Ø>> äï ”¦\þ­²pˆôî'÷Ù%§Ó;„Pa”G­ø¨•wðð"f2¯íÉíSvâ—˜6µÀƒ€€­´‘©¤<_iX,tDã‚ÍÒcÐø rF xÏ7föèQ üHç’t.ð5߸‘7Ö¦]tzÊÆÀjXç…RTÛy‚ÃJz +"| aÊíÓ¡Ä^ØD% È™)øjó†56Öá ùcu[€½Ð:%EÖÀ˜œA­=E8¢q;‰˜Ñ%N˜(³GÖ¹B$lñF!@_€&Û¨è¤æ7“Pÿ*¥sJÀÿ£tÂŒ\0ZòÝmã¶#7ïv ÎEJõÕã¼áÛÆ-ãz(ð~ÜÌ€ªì>ˆ‡‘Ü|$ß?2aœÒ;œ˜Ïé ™< ŠP-¢Îå·y£ryÙì•­Þ˜‹êœ°1“×äñ‘"爎˜4 ×ÿ;s7—Q¾’ñdsËɧ©=£Fb6Âpò…`q€‡ +ÔZkw [¼ÊŒ†m `Ê}ÖsPZx°Í «‘`eÔÌL9|íT†‰ 2½ Áê®™V¨‰NÎh½a5"ø9}%":(m±ÙEQnÏo_aRÝ[&“ˆr”"ì8¸"T´²¡õƃ$ Ý™“Á&n“:ñx¶»oãå[Æõ0G u ÙLçXªw + Öè@¹ÐÛŸp2_7š°ÙˆŒü… wÀ° Ml„Aé¶ Ë°Ö=a 7s1T»™€Šbi¹©F€²0x iÄD+žÂ| ˆZ¦G¿¢²Ý:n9<éáw¬u0y=Ô¢þ#ب¢'j+ i :ÝàòS¢/ѱ7‹E!.AvSnwmd´VÞÇh© +åa÷F<Àˆ=a¦#SÞ"-.Ù¨Øaµ ¼ÚÁŒÆœ€©ç€£xÈëKÃ7C°•z︇W72ãf8­<85n£Æµn›;0nÀŽL¹Á=?µÞ¸/5 ÂÕ‘)»ÕÅ:½¢ÞΞr›;•Àˆíî›·ÈU¢±™ÂnÎwPqÔ—IÕ€‹P!V< ¾iÇc‚< Tï`S ò&OÀ€ˆÐ³j—h&dH¾¤Tâ™pqv³à SYlTÂŒGMÞœ™Ù½›ˆ¶& æ£2¸X¥¢]aÙ)*š£ÜT8Pq0&áO Ø ¸r§C§¯˜­oœ¸ëÙDc}H‡¹(`]åú¨™vru2:‹‡º\nÕÀ î(ᯃ¡c`ax_ t`'ZîL¹ø#7¨ƒÖtŸ…Ž™é¸…”SùùÓ—ŸÖz|·MXÍh€·¬„¬²0C:üæ‘Ò8#+·˜¯1zz—`BC£À·Šßù¹PcáøýZ,xX‹Ñ0[“'bB£`S6—›½µ;“Íí[Æ­‡T–1Ûcƒ¶ÕÞÔ§×GMÚEHj£Z7T‚Ñ%b… 7\|jÄLc‚NR#*p§¥S:EøŠð«7ì‹6À=8Y¦Ì¤Ñé‡È†ðy_²` m>nðªŒÞI­2!-‚£¾4éσ)3U»ŽL8Æô8ÌHãòOX¹q3uÛ”ÍE&) ´ú«LXÔÚFätTž3¢A4fz¢ë”sXƒàÀÐÍÚ¦Þ´ämS®a=ø/cõFÍÄÑHâˆN)à1ðe#aÆ$3Äø\ 5‡†›:i@Ceay˜‘Ɇ¢ró9o°ê ¬L¸ÌÈ.=cg’v:éâ³V +Ì.ÅF!;jÆÌ<('Äyp[0Íq+e£ân>댅›2ncÓÄ? +¥NÄ©h'X^÷•7ñÄŒ‰ÍL:Aî$%Œ[é1+mc3ê6&ïö·ŒTŽ‰Í1ràjÒÎè=­“‡u›²ùÕH@•>¬qÜ:iÒ8,0ÎøÀ—÷%[R¦«2‘`.(]c?¬¶Ž›X +Bj!¾ª“/j‘ $qˆ?* a¸y +ÕÍC2* ‰yycÔ¦4¼ìˆÚ1¢qhÝ°±@D›.¡¨qK:'?iqð M:!ÂF@_«]!-q²e&¶d¦R#z«‚ š²Ã°%;ìà«|vt6Ýà M˜È¡Iðe¯ÑÆi,¬ ‹±Î„öÂo€—u‹“J¬óAm«ÝA+™€n² áÛÆŒcÔä èÊ‘‡uè¸4–ÓB“êƒõò )=ÀIvåÄGtÏ åæMXG´ža­œ5 ¤HÊ“nÂ@‘\ñð$T aDíQÞÝ€ÀxxÊ=f F•#«0#ÎàŽ8©°ŠÆîŸ01*=”VX㯇tìWA¯‘ê!¯s»1 hµÞÓ{SfºÀ'Wƒ•}.·b&b: Ò¡qû †#o2@X‡ÊàÚcFÒŠBØÉ«]‚›K¡|Ö#VùôŸ5QÉaeãrv.z}í/3ù]<1ÿ¥óDÌdÊÂÀ¬£*Àãbr¡â™6i „h K +cšr)‡¸Ñ¸‹¯ºøŠéæ"˜ìÍ Î˜‰¨CÈɨEÑ®Œ K€jJ3šP›°ÚÉB0tûªh¨c¡2zåL1¤-ÙÁ*‡ò`FÂTÎ-Ö­DÚF$¡_ÀOÕf|\ôb‚êrKÀ“°26:­œ¢µP#Êžb£ZtXí3ÒjwÈDdìLQç‰k=q]@¸¢Î%€ÀZŠCì…¸m¥2:$d'Sd°­Ô×zÌ®€ÉÎO…s3E#7z¢Z§D)$‚',DÚÉèP›ôWU\gõMÈ6üá©ÛTVÐ(pçÛTv0»¡›§,MHxÔ@i!kÉÛ&\MTfNeÔ6Ÿ ·P!ÝguK ¨€OÀ„£Z *dÊ +Žœ Èó‡&œ‡'œc:ï­Ž³’©m„……-7Ê1of7eá"TFFmñÙÉ䤕Ÿ#ƒ ìf +t¸cÂd¤ýé¾)ÞáÅ[ô綱p¶:]HôÝ0$+¨.íbÒ^Å+µ\l ÌH!T&šx¶‚Éh/Ú8àÒKF2 éûn}ÂÊ«>&³ŽÇLL6ËJ¥ y†N-B)Úˆ(*•¬Êùµ¦ «¬œ@BD„Øl"ðž•5›äÎoB!ã—lÄÖJCb*‡ì¸šKj]1=¦61—³’aÐ7µó*n¿â×ö›Á9ԥ䢜œA9¶)ȆLUaÄ@Z ºÚ˜Ã“Vþ*»0k¢aétÊñ¼rÂ"Ü6n‡†T`>i LÚ%Ødc‹T¤ïd²®`ö†h]OÇ­¬Œ[ñˆ›MRSkãLN¡ÃSŽÃãÖq-Ž0Êuµ ´@lµ?À† )GÀ@Ãv¢F'ͬ‡¯Ñî„žÌÚ™¬ƒÉé!˜›ðÃ:Ï°‰„ÀË%úN.£¶ 4¬C‡Ò@x43€¸›'ĨŒÜzD°P²Ó—³ Ê©"¯D†šh¨mÀ•S*&,<åäFMø˜‰Ä2 Q§¯jÊO¼RÝ)öTNaÜÂ*GÐî¯Ú )iì挂©X°E´PT¬ÙȤ‚quhÔA¦¦—.¶W.h‘À°Ž6@ìÞ0†?Ú¶a!½uщ1+u«Æ1dÄlžŽNƒ®âáŽÖ%º(ÑÔÒ¸Â&oH*Ç ¢Ln RÿΫ²²0fÈø(›÷°E«7­¶KÀá&wPeÀ š©Œ¬H•Œ˜²FÌ‚Æ ¥Q66np›<þq 5i%FuÎ!µ ÚÙìöc¤L +ÙC*« +€ßLO¦0ªaµ{X‹AI ¾²N‘P§Á!¹ðŒ‹Ê\8ñŒÚÂýó°ñ¶QÛˆV!ó[GmzzBZk…¯¬ÞF|u½Gž°‰:OtÊ3 ÃFvÜÂC€;0¡Ø$˜˜é˜ÎÑò°Ê>¡CµvŸrÐÏDOY8³r…;lçlhÈ‚†`ƒ†ô8äGX–a#¬6> +öp nFO¤fD M!czRñhHÄ@>¢²§'tXƒÜ9€›ˆI‡TãV-dÚ)”Æm\FÄgp V" À6~óD<äS!Þwùr·ê;ˆú˨¯ú?å @;kœA; :ovóN" Z1éð½ID¬ã¡v¸¼M¥çF¬€¦ê¯BÖ>¬vß:å8¤E&í<ä_Ú“6ÑìMB'¨í*´fÆc\r&ÝÞ•kkWÍPH8ÄŠAŽ!TÒå"xÌ+ä†`ÌŒ ©Êya¨¯¿Œ‹EDÈýŸƒQã&ÊJ&X*bÔ‘)¨7Ä%P*X1JªøÒƒ¡Í´ˆ~#zJ´ia/”hf¤Fuø-#毌˜Æ´˜Î)YavLÆà5VÆæ‡õÈ¡ ó˜Q +ÀÆN™iµ™Qi‰) p—ý–!Êà…j­‚•=|‚䤉AèƧnº³Ná-ŽL)Û§\T£õ‚eHshÔ %êÄ‚v,d@":’ˆŒjGôÞCZlÜæÓz”C17O÷4½Á†•ÁJ Vüÿã5X­Á!èlÜ°S)•v™¸mÂÝg!d3¨ ]ÐhÀd€jYjDTh„Né ²ìnE²¼îq3Ð ¤“7N›à×ñè˜ë· Nà +§oÄ€CÒg£-_fŽ‰÷ Þ¸Ú)@‡ø©¶S:D€½Wõò!6°³™q?b¤ü©y}ÍB³«”#cxFV<qÆAÅ4ˆ±ÑÆæÝÍDl‚¤ÊÎOÚ}ðÏQ#ò8lôë<#@V—¡ ¡öqÊBdB#˜ð¨ÍR±nZ‰¬‹/ƒÊݪ²kÝ&DrC8U"t€‡µxÒL‘„w„˲˜ñ”›+¹È´Iìt(@ĸžÓ*­a<”î'›[·Ü*eÓ:¥a­|jÌÈ›­*Ö?¤ÁFÔèДgDƒª ä‘)ÏáIDe uvÊç b¹ ÊÕtä!•mT Œ` KÍ›È#dÂD¨tõCc& 8ÈòP«Z›(%çíd蟆&n9¢ž‚áiÑ11¦#¡Ufà +2Ú-Cêµ ¢Á„rµ`1ÐvÄWT¿MY”k«T7³ÃÚ3¥¬Ľ¸rBʃúÙѨÞÆÝ6fU.-ƒòvú•ë»\!—÷Ê6Ø`òF­t TÙ=ë/âÑ&®«¬<<Àb@IlT +ÊÙ( ¯ô>ü–^Ç?åÕ@òXþ TŒrúÒÃeH©H„ÊÞ@ÕÊä\\ÎÅf¼þ’BZäâ=7Ÿ‡˜æ Óc6nÔÌɵ!kpûLXpÄB+]cÔ®°Î þ"`€½•ÉN8‡õÔž’wÁ‹P)Po °=Aü3øʤýФ ²‰Ùs0Y;“3“+•›´2ðsÀ?pÞxýh¼~ tl t gš…Ò»•>¬'õHЬœ`MKh"ÚjÊBCÓ{b¨¯Nøë+@ÕaW¤Â8«á`RD°ê ÕoÕx!òC‚¦jP3¨wǴT.5Q;%­Œ:4á:¤r‚‘Ý|jR…®± ”Íh­ä¸Ó;ƒÀÀCjxXÛɸJëù‡Û&nÒŽ¨‹bÊ!Ø}]Ð8€ë ÓV'ºÌȇ´¢•MPÎXiÜ0eå|¢…¼¯s*ׯ‚.ÝœT?¨Ä?ÑeÈ| +ÞÂk‘èÍ ´Ò@˜P.*iÅ‚P–Pxj%o`:,Ž|z–Žu©h“MõBÞ%–=ÁüœMι%±¸nfò"a"â:O‡’ÚȤ‹ÚXèÜôÍ!@Ž³sLª:ø+v Y #—¤ $fgC•e17KG«:ÄN› ö&åµ<“èñÙ êA9»·³ß²„Üôƺd¢Ïgç¢xg›ŠU)¹,æû|n†Ë͈%ØǶ¿8OB-…kž`ÙB'\bÎ#)H&dç"åõÚâùÚò¹Py‰IöÐp VØD›KvƒÕ ¡´뜦“3V*Š‰ÐtP9:Ö;ær‹îpËlë»lbšŠ‚O•aÖlv‘LðXNs³µ¥sT²§rûaãÀõøÌí(«-U•‹–ý%ÐFÒ¸8abu>Õ +ç"M±°è ×íë–JT¢K%¦#õ-¨.Ø,2ÖR»|VØR¹däšK΂ÒË+Éî±öÖÝr{5ý%ó\¼½›]8“[<in°É®?Ó­,«.J÷ö| ”q¹Eêd¢Ë&º^©$lTÒB&Èh‹ˆ¶½á–¿°”éÄ:»¾Ì ÝÙ³=®šdâÝPy%ÒÚ‰´wÝRÕHÄùÄ´…«,¸ƒû23lª©o––/´ö®ÛÑêJûN6ÑÓc)*1ϤæÙ$lâFnîv_aÕÊdÆL$h¦“”©HU*/ñ¹yi57w&Ö= +µÈÍäÛnÀråDõÛ|n%ØØOöNÓ‰¾\Yu +9t.!ªîfúgúû¬ž~²·q±¿sÉ—Љv´¹im +…9¹¹^]¾ØÞ½î+.8ÅœƒOyüY>ÝãÒƒÔô±ìÌqqafÿžììqg¨äôç¹dÇŸ›‰¶¶ùò†¿¾Cfç¨Tþi¡ÂSN~„%1s:>{668Ÿ9-•×ª §¥â€IÔƒµ5>·Èe|ù¥Xc#?*ÚÜ‚ò¡Ób+›€&¢ä8¸/¿kMõN" ‡¯[¨ "&)¹ª®%¦÷ãÝý\ÿDmñ"à¹zË$úbn Ê#Ù9H÷O‡ª;ž@mÒá —÷çBõu>7Ǥz\f:CÄÛŽ@ CN³Ù¹p} V87wšŒ4ìdØÍÆmhÆ-”ÅÌBiñlfö Õ;–[8kmÍíÝî•6LôŽ6·ïê½{pìîÎÚíb¦ËÄšrc ”ÿ”Óå©AyéRnp2ÞÙ­-_€Æ7`A4Xõåæ“ýòòí¥å³ùÁÁòéûJóÇa‘m\ –¡êÕôÜ©h{/XZÉÍž6sI‹W”2\*LÚH,Kvv[{÷´v¯.¼ï®§¿Ã%g˜äBeíZ´¹ïö×=R‹NÍzB-+›Ó A—/§÷À +ý¹i©´®oÇÚû ¼v!Ÿ¬¯=úÒ;¾Â’ÊÎú’ÝêÂ91·Æçףݓþò&8ûüî5‡XÔ¸ü”BVK©ééþ™ââ…LÿØÚ‰û6/=EÄÚ‘ÊZ}íŽööåÖÖ¥ÅÓÍí«Ýc7šKç¯=ñíxgÇ-fC…Aª{&R˜?S]½ØܽVœ;ÞÛ¸}îÔ}D¸\]8èî]÷Ž³ùÅâò…îÁCÉù‹Bº®ÌЉš±—_”ê[Úzª,=8!f +s;ÑÎ&›š²O¨ìËÍ +sþü|måJ¬¹ïô¥ðh9T^ ¢Jn€ƒl”Cµ­lÿtnpÊŸëH¹.o‰…y¹µ©o¤»{¥Ù£Ë'ïím_µ$#5ظâÜ©Æê…ÂìÉtï¸ÜÚ§ãýÖÂí­õ;,TJJ";8•š>mí4Ö/Oo^=qå™ÒÜI3›ás ¾â*”=ô—_÷Wrs—èpÃaD,‚Äɵe!?×WÅÂ\¨¼˜ÞgÓàbÐPðÑêR¦·k¬sñ*fÁ ‹ çøt?Z[57‰h3Z[KNï*ËîPKÈÏå{ñæ)ǻ۾ì´\[,/ž‚Å—JsÙþîùû_”ëk°•ó½ë…å ‰é£áê*)OßæÚûË'N4V­¤_HÔó=˜ËùÖÖÅòÚ¥Öî=ÖÄì2dØq !B,mV×ï./_Ž´ŽEº‘Æ–TœO7×óÓF¯¤÷†!LQ`4y€œ¥PeŒw™H¹ØÛV–­t†ÉÌK]_YqÞ`yIúµnÁÅ'Ý|œ…½È-€lÂØ2³gSƒ3`—ì<,N 8ì¬ÆZáÚ +¨Jgnÿ±߆õ±ÐÑ`~¦¹~<ëW¡ð¤úZizûÆC_»ûùï!þ\}õ\gïÞÊú]пͫͭk¾ÜÂÁ¹‡®<ñšXì«œ<%CG¬&§Ö×/Ο¸¿0ª»|ê‘gßHõ÷  +3Ç’ÝÝTÿ`öè½{w>³tîùPe§={¬·vžˆÔè¸âžܢÜÜ-/_š9ñÈÒùg + çw.öVOáá2ìZ ´È¥û|fF®­Îhpú«‘Ú†´o{:Ö$嬘/Y¹b¤²U\8ãä3B6qôŠËÌÙ~ypüÒý/6–O#R±°t93{.ÞÙol]Naò›@†˜¿.¬@§;˜X¸0+7Ö¦w.Ž]¯®^Àã=LªUzÇÏÝûu"Tˆ–g+ §Ë‹çéô€¯oF[þÂ|°¸ÈÏ0¿ö"5kî€Pt7ïÜ»üteáÌÒöÅŽ;¨h~=Ù݆šo¬_Úºðè…_L6Ö6ÎÞùÐ× l€%œ¾<„ ëxçL~ñ®Ââ¥@v°´}ûþÙë‘\ËÍǘä4“]ò×ÃÕ­öÎReÃÆ$}ÙA¸µõÏÆ;€=þʬ[¼±~æÞç–Ž_5‘ÐOD,'z'åî1>»¬ÎF;»§¸üÀ‹þd ×åÎN¸½'w×6®™AÌypó° —øD'PXÌöŽ•V.%·KµŸoÌž’u#Îá\¬¹‘ï+Ï- ¶›Ë'ö'] ¯fº™éñÖ¸—éæº[»çž=~ÝÉ%ÓÓ»`áÆfqùb¬wà+. éþѳ÷Ÿºû)>Ýwplb&PZrƒP}¥´t6ÑÙžY;ûò·~Z]<ÅɵÁöåµóO¶ŽÞè½wå죭l¼p澓w> î¼ošž>–hïfzG+«sóg˜dwçô½ƒ­óh°˜ho§f‚ÕõPu=7srpðPyíj °ÜU®+˜p ÕH4WZ›wæÏŠåU&Þd-W „«‰Ön°°hg¢L¬"dz"tMvÖŸí'|²Û[:Ù_;KFªÅÁÑæÆÅúê…ÚÊíÍ•Ó•Án0ݽþàso¾óÁÜÑ«zoÄíËã¡z¤¶%·OD[ÇIJGȻ뱧^üNº6Š¹Ù³éÙs¹ù •µ»k›÷²iøa¹»v1ÙÞÖ¸ý:$ 1Ù%ß©.Ý~áÑ×Á»¥|?ÛÙ4yÞ`¨ Õ€+Ò½•ã7æw¯Ðáj±·•h­!RÁ)ðPÛŸ_J´&[{v&JvÂù9—†årËTrVƒ™+Ô¸(\JG«óµ¥Ss÷Æ{{î`yÂ)L:X''C#øR¹¶ëäçO¶·./œyH,-âÁbV‰ºuð¾…Svw¯§ûéînaæ@Ì͘é¨Áâ3—/ãö¥CÕ(ðÜúêí•¥lºíâ#R®#×W’½ýüüÙôà™C„l$×Y€,™ZÊÈ@ÁIFýÉnaþdfpà/ƒœ¿ó† +‘Ê‚›!dnm*Þv +)½7d'åÝS÷ùòsCzÌɤÈH8Î TB,>\è秷 Þ@¦³Õß¿¯°xg ´ÝX¾œ]8Ëdú¹îîÑ«Ï¡¡²úæ';”}È=e7½x-XÙÔ ~Ä—N·7!CÁN•—Ï××/ÏíÝ7»s=Ó=.7¶¹HeóÔÒ`O‡GAå O ¹5±¸–ì‹‹^)³~ìrº¶„9J†U]÷WÙÔÀ_XMv¹ÙîÚ…8„Ê›†Ib­\g³ØÛAy' ”¶‘Qè;È„±æV¤¼+-E23°Vµ¹“©þq63 Y ÔÌd ëu7¯réž™”½á—ž)ÍŸ-Ο†Gyî ¤ìDuùèÅG1>CŠMË+ü½u·/Çë_þäåǺ+'õJ Mùóëxâg\KF<Ä *7ŒGª ÔÑÖn²wœNötž IR¡r(Û™rP&,€G^¹ ™• £M3‰®ÆÉ9© ?]{âðb›ž‘ëëR~–—+Åæê`ï®p}Öðh‰w|™9.5(o@5Â6Aq¦š¹ÁñÜà ;wÒ›èXù¤/Ù–«KV:/õ×Î<²tæ±Ìà4—ì9Ø$ B + Å7x N–ä¥üŒ¿8ËçReÉ.¦±`¾¶|vÓ*G««Åùs女™é!ÙÄiꟲq$0aÁ¦ì ”hyîTvæT~ñmC,Î[È Ÿhe:ûþܲ;Pƒ=B!›ø’Å™½Âì [ÈH0·¥î`â^¹‰È]HèÞ`…Kt„Ô´g»ÇºÛ×åú6àJuîD¨¼j"e)ÖØ>uo8×Q#‚T]OŸ,-_n®]óVÐpË)æy¹¾wêF¼8ë âÉæfwýŽâÌ ¹¾’œ-Ì7æÏ@G„r³ƒýû*Ë—Ã¥5_rÆ—àáÌ4T.¨4êË„J‹ü|uþtgë +nê¹Cù«¼Ht,„ŒIe&ÖbbÀ½›•Å Ý+&O Yšín݉Gf"`Åýn.nUNF]tÊM%,h`fã88„q .™)Y¹ê ;Ü¡ã3Vî+ãv™Rs£ÊBèÑ'°ª²Riiû®PcÉ@Kv>îàÓ*!¤çÐ@_JmÆQ. B µªnˆY˜E'Ÿ1¢ATHÓá‚WLÄ*sBv†I÷¥ê¨‡”Èù™Õý»¦7Ï¡”Û—ˆU—‹g›«—í}!·@'§BÖ3U_ƒÍ""Dûv¤¾|añøsGoÑb¾6Ø8uO¢¹jÀ#Ê©C:…JÕpi5ZÝ…±ˆ7P²Ñ½‹%ƒyÈ íí«Õ…3¯–N~µµz9Vš_غ³¶x;­CÔ‚ hÄ#V:îK̪ôS\¢#Š+vìIÈÌÐÐnѺüú§¢‚%2\qjœƒŒr‘ú¤ÕØ•ÈÍ¥Ú{¹Þ±à·qqéBï ¶tÞÎ$1çgÀ ó˜Hd̸r¨4O„«h ˆˆE,X—Š+¹éƒúÒ*\7{ƒn&ÌôM˜ßF„ ëùE J_v‘”j\ÆÄ,l™[9ٰБ 'ç ×àðúN¸¸d&$6ÙMtŽ5WÏ—N$[«\ªi¡õÁúúÙ{¨hqÈèEƒM:9çËÌs²rø׈GYã]J.šš‹ä³3û!ý»kŠ³ÇüÙéH®{ôìõ³÷©¬ËW b=¼`a‰’jvc¥>jlŒ›q<˜äfr3Ç`lˆ¿hDFÄaÁ’ÂÄ =K‹ê«—*ógãmLª›ˆTŽ73 ¬Õë‡o ¯µw®•/‹«PÌãÊÎÊX¸è çÉHÅ—èÉ¥Õxy Uq!Eòc&ï˜1ÚI+ÂclÒàTFBccøHuïäõXº5¡õ`|ºÕÈ`]9óŦ¦l,¬g²²JøËx°¬uÑf z6*ô ó»•å½do)9½Xž_½}áäÅÆÖI6X "ÐlvÖ+fÕNÆâQ.ÊE‹L[*t¹T•ÏÔÙWëï_ˆ·ç=¡ -„ŠâüV µ` àù‘Lu.˜m«­^¯˜DÅ´ó'!ãÏñ•Wb†‰fã­y±Ø MLΙùˆ'œò+•µ­ø`ÕJ¹Å$—švâaâˆÖf@8@ 0»ÒÜ)*šAX‘ %}¹&*Å™x^ÈÖ ¹,vZË;½Ý“¾RÛÆG}é¾?7ïærz·xÊ>ªqš_¼º)äldðLnm …E"Ñuù àË.1.w¨XVOFZòF‹ÙþQXÔØØ1µÃ‰Š$Ÿ$}i5¡~G´RQ²vi ’‡‹•×N^™=zQ,´Æœ^­×/åÜBvTç×{ÔVÊAËn.¥¶Ñ7?q ">9ˆT֤ܭ®¼:·w.ÙYtû““.ʹl× ¤ $’í Ëx¸ +%m%eD¬–fÏ·Vï3=5Ô†×OH¹Hy¢-×ÙX“7 ¯3ý­ÌÌ–”oË•ÙXm=ÑØ–¦Lä-ÆCv¯˜ ¦¦MNvHeÓ¸N?iæÛ;©ú¨NYh•sÒ2R>¤sÖ9GŒ¨7؈7äòé¯\~!Ñâ"¹l½7¿y|uÿÜÁ¥{¯<òÌýÏ~ý™×¾õÖúá'¿ûÓ_ÿþéýîO>¼pã™ÊÂ1J®+Q·ñ^.•)Îk3åöbm°ÙYÜ\Ú=yôü•k>séágÎ<ðÄîå'®=táÁgî¼ñÔ+o¼ýâ·Þ^Ü;»²1ÛY¦"7’ _º3b,_èÌõW¶—vöÎ]ºýÞï~ü™‡¾öê©ûŸ^[XínìõwN­ºóÁ§^üÙû}ôëß}ã{?>}í‘Öâ¾\ì›Ñì–lhØÃ$âŹ|{ËÃ%4vš f›‹{å¹­tgYnÌ¥ºË«'¯½çñ+<ûÒ7¿ù‘gÏ?ð/y^È´LÞ < iÙ£5çÛñêt¾Õ¯ Öûë§wÎÝ{ß“/=øô×¾óÎ/Þûø·¯}ÿ§'®<Ô_;k®Á࢓ ˆd©PÉê Ü"¦äÒba°«Íɵ™½ ×ý××|þß.=øø•G_¸xã‰s÷|õêc/¼öö»O¼øÚ÷}u÷Òýd¤dFEÂ{} _²Ê/ðÑf¼4+%ë±B¿Òß\=~GoiuïìwÜ{ûµ¿þÆwóå_û‡¿~ñ‡¿|ðéç?ûl}aÔà7Òn6Ê-§j»:‡¨±vLdC%Ü_²“1Æ|I\L'K½cOÞõàîÅï{â§_Ú>}µ¹r®4{œ —Çôø­#vFˆ·¦—W¶Nœ¾ãâåk=õìëo~÷ý>ûä7¿ûŇŸ|øñ¯ÿóoÿùéç_¾ñÝ=þ«+ÇïÌ͵RI'.G“m)’OfJÍîÜÖÁ…篻rã¡'ž{ý;?zý?{þï=ñÒkßzû§?ÿèó—¿õ÷ÿýgý¿¿ò½Ÿ?ò›;î˶Öê G³Ýª’©´æW×Ï\¼xßC?ý‹/¼òúwÞù釿þý÷ßûèµÿùÛï~ôåÿúÿ?ÿï È?ÿò?ÿյǿV[<Ê÷A0QFŠ&‹•ÖÌÒö><6ŽºpíþÇžÿúKo|ëéo|óñ—¿ùâ›ßýñ{þê£O¾øâ‹ÿýý÷‡¿þ≯½vòÎÓõ>T'}¥pv>ÝÞuÒq—7” õÎÂæ±3wÜóÐ'ž¿ñ//ÝóÔ‹¯|çÇ?zïãŸüò£?þå/üëýòÓ/>ýíï¿ñ­wÖŽß pÈ„Rj@‡kÁâ\07]ï¯-l윺xíÁ¯>øô¿>ùõo~ýÍ·üþ'ï}òÛŸô›_}öùþôçÿú_ÿ Ýú£_|öâk?L6V´.~ÒB‚rê\‚£bÎodjƒÕýÓW~âêcOýË7Þøñ¯>y÷ã_ëGï¾úýw>üõçÿöw?ÿàãßÿáOÿó?ÿóéo>áµ·NÞq#QÁŘÑåÕ9H;.A,)¶–{‹{»'î¸|ãñ?÷Ê[?øé¯>ùÁÏÞóŸýú÷úËþ×Ç¿ùü£O?ýÿüÏŸ}øéÝ>SŸßËtv|©žIùûl䔑 |IÆŸ +§šÅÎjc°Ñœ]Ûòä}_}êÙ}ùG?ÿ‡?{ÿõïüàû?üÉ—ú˜Î×_ÿöËo¼uöêó[ç*ƒm!Z–³ls.UN›³K;kÛûÛ{»WïºëÛßþη¿÷ýïÿð?ýì‹?ÿ ¶ãÝ>ýì׿ùëßþã³/¾xëG?¾öÈ#³[ÇÃ…iN.³‘¼L”êÓ³+;ÇÎ^Þ?}~ÿøé+×ï}íÍ·~ùÁ‡|úÛwÞÿø­þøý>øÍçŸòÙ§Ÿ}öñ‡ôÊ›ß=õ¡Reã(›$;Ñò²×—¥|‰h¼X­w·vw~ä±7a?úùKß|ë§ïýêË?ýå/ûû/?þðË/¿øżüê«/¾úúÖ™+bº…ð)+!Þ å{‘|§;¿1XÚØ?uŸûÚ˯¾øÚ›ÿöÍoÿô½_þõïÿë7_þùÝ>þáø‡/¿üôó/^yë퇮Úß±S²ÎÁém¬ñ;ˆHD©³–¯¶Žž|è©çž{åõÞøÎOÞÿðË?ÿåË¿üÇOÞÿàÃO?ýòüü÷_¼ûË÷?þä£wßûÅ#O?sîÊ}‰Ê4ÊRg'Äx5˜ªÅ2­Åõ£×xâ·þý'ï}ðöO~öÙçŸù§?ü›ßýò“ßüù/×yç§?þÉ»?{÷ý÷}úù£—î›?~‘ëãFÂâ ZÝ‚Ëë3ÚpŒ’Zƒµãç._èñ—ÞøÎËo¾õo~ë'?ÿÅßþþ÷_ñ‡Ÿ¼÷«_¼÷Ë>úè…W^={ÇÕ•­“¡l/ß݆³õ֠ҜΗªÓ £6g–6vÖ/_»xß#÷ÞyíÒÙ‹·çkE1ÀÎŽ±N\ÔY 7Ø ŒðËÑl±Ðhµgº3ƒÍ½£û{ó+ó'ν~ãŽû¼~áÊÕ wßwìâ]í¥U)Y࢕`~ž‹5¬g÷péÃHž‚ûÇï\Ý;ΕâîÎú¹ g¯ßÏÃO<ôýïï½~ýþGòÙÇϽüÒÉKºKKDVLT0ˆ®DÀIIl ±wpéø…{˽¥b«7Ì®¯-Ÿ>}ðüóO¾ý÷?üøÓÏ~û›·ôö7ß|ýÕW^~>ôÀÝ»ûõÞ²Žæá=\ÌJ„&¬kp­²¸hŸ/œÏev6·¸~÷믾úú[o½þú×þówþøÇß¿øoÿzÇÅÓû»Íé^®ÑqS‚Îîõ +1x¸è°“c\˜`Åh2_möWW·.Þ~æ«?úü Ï~û[¯ÿòƒ÷?ÿâó?ýùïýâ‡Ï>ûä…ËçÊÍš ç]d`Xgg¨ÌãZdBzˆ¯Å⥙ùåÅõ£gn¿ýŽËw^¹víîëßxùÅï¿ýƒW_íë/½øÂóÿòÚ«ßxðƽÇÏ[(1aœÕ+"L”ð§S•y)^öàB,™ßÜ9~×õ‡ž{áë?ù/w]½÷É'ŸùÑö½|ïÁw=ýðýO}õѳçÏmnoµ§çR•Amᤔéß¼š_¶¢¢ÑF¸<\&_ßÞ>zåêµo¼þ°ÒSÏ>ûÕÇ¿úÒ‹/¾ó“÷žÿÚ¿]¹rÏÚæn±Þ&ùÄ3&émŒÞâ±!Œã<¤Ï¯$ÊýFufe%+N9HÞŒñF„…}ôp²ÁÉÓk¨Ë+yèˆ1*„“!/ +EÓ•`¼à@p›³£Áp_ˆ–ËF"4i#Œ.aN&ªvÃ:‹ÞæEˆ€a]¸ÈÉ`oR ‹RózY–IgÓ…j¹77¿}òÜÂÖn4›sÓ>­Ÿ´aZ'eB&įu°zˆrT¤D9KÃшKç2É|!Wow×Cñd"•nw§+å²$Ýï $„‘ VÊÏbüWTƒÛç—¼T +…3õJ³˜/&cr<&¯mlw:µR)ÓéOÇsu$éOxż‹ˆj-„Ú„NÜ:‡JçÖØy¯™÷…«ÅÚb*[/ä*—¯Ü}ìàx¹˜n·«µúìL®ßÎêv9<¡ƒ 6y‡U–‘I|£·+ŸÄÇÙ„)‡ãÅd¶œÈ×XÔáÆ8_8ž®#¹\±Ùí-÷úK‚(&Óy)”ÂÈ€ÁÁŽšIåO1`a.ÚðÅê¸Î"8PžSÑlšš –ʽ£3[W:Ëç„PÆár†Cát:ï—¢Î8T+7Êsç|©9µ…þÇÛ¦lXØÃ&͈»L1Áh4•J©L:WÅ é%Zð‡ +¹J4™i“òá)”›acíÛḞU†q $ÔŽ +˜/K‡Ê¹úâú©ûŒxâ-(P¡*€Â¥<|Úâ N™q¶âÁ#“¦ÃæQ½{ÒèuáABÈ.\¥Å*¤•¿Iâ ÔvÜFËv*iÅeR,¸)yÂìÑ»i&ºp !%‹‡5¸X?‡G+l¼!È‹‡²z( ()bB”–ìtXëæ>3nò~EeÖ:uíDDù@ E}q½›Ó9+æ×Xq£‹Ô;Iýÿ‘ôžÍqdYšæŸØUE B†ÖîáZkᡵÖ@@k€¨AÍ$3I&+eev‰ìªÊªéji=ݳ#¶wwÆÖlV~Ù¿²'¸fa4d$á~ýœó¾û½çÚ¢ÕL&ä¥Wã´)[EFw®-ûæ#á¤NHÅ›Áµ¦RÚIʵy?LÊ¡¤´èE]QŸq{£n_4Š°ZªÁÊé$­àlÚhËv *ÌJò¦¾HAØlUVü¸7&¸b²+®È´Õ£t*)d +­éáù3§Ð\qY`:NÞÁiß°ž÷&&7ïFÿüêòœ+2i¨]îÀô aÂñ@ä륵(e-x9ÓË´Àót£Œ0"„ãÜpíœàÒ×æƒðQq¹}¸2ÝOˆy®Hs9Ì‚ÈŠÙU¹´êœg{gµÍçrqs1@$I©X_‹âÚOoxg-tÅõ·ŸÕ×.Q±r}1¶èŠkq:}e)¶¤=1(°))ÕÏ4R•m_ˆ_ #„BdWˆ 'µ‘J̦ºÍšL*…õ›+è wô¦'<çÎ{cÒF¥eÔ­Æ6&–œòz®µC›uR«Ç˜LœË'•ê¬e(› ÖOæ, .®»7]Qo‚ÃÔª[ãœiÔ#„ÁJ™ÆàתsžÄ-o|1€† Ín)•m>;„ÁL—{J¦HŠr®˜ÀÓ:ϤÆÙ½åKÁ)#„‹A¢.L§X«ÅY½K*h(eqƆÀƒë #æGÔ¤PTs£¤Z¸é‰“†èŒ¥ì“+A*›TšnDŸP1ÚrGéŸÌù®Ü +¬xP‹ÜA<ɤ1±£,?"ûêOçcs^"4Âd&ˆgá_LªDhÇæ"”Ô?ë¤çgg…óZi5Õ؈2ÖœYp#8ë¬xñkskKñr1À†1ËÌoE gÞ‡Ák)@^_ŒB0IË‹ ¡¨·ÔÊ~3 )«ä‡¨Rö£š'ÀÌ-Æn¹Q”J׺§„_]ÍFLJ§¼¨áE4T©‹ù .3 ‘)„K¤TÖY#D¬@ØxmÎHz­·þ8F¦®Ü +"b—«¬VSSxgÑGüì†ß$Ö¶¡|~>È$ÅR”ËaJMÌ®9í3Öì/û™4Ò\ð ·Vb‘¤‚K%f6Ýt,æÖi£sm “c0£pÓ.G…]@»|ndT6ëëÒí]B+Ñv›4šR~UÌO“z+@g½xzÎGÍûH±Ùÿå$ƾ¥ÎlñÎðº÷%C„– +BqŠÍT ‘šSêm?¡Ó½yH(\³©8;k1QíMˆa”;ºx>Ú»ïO +B…ð&Õ&®4«Ï¥'ðŸŒ\г]¼ëK‘¥ ¥f<Ť‡zm_Êo¡|1[ßRòƒ9?:“`TŠ³i„Í°f“«aÒ&Øtµ{€)å¥0;ïÅàú†ÈL€pü˜­•7‚¤qe)Ƶ$ŸòDh_Œ¤^€‹á êrmÖÕ`œãl>H¤nxÑ•˜«r~šiï‘FÃý±ÍHˆtàÿ"B—K®èlÑ £· ©ò“kî«s>8S)»Î˜ý0‘öD•% 4A+E8€#wBò$UWB Vœ/ǸRŒÉ wŸTÆÇQ.+RiàB‰˸X â6üÕÜJ4ŒB& ++QqÖ`‡-Ú­³Éío‹ã‡~Üö¡2mÔ ¬ÄÔ8Sbí¾œêÙžhwx«‹ò9o\ŽPÎr;8·_t'oÀ¿>ª±’éÁzQ6ê+QÎã} Âò§sÁ[.,B¦¢TêÏ®,ßZ‰"”ng1$.„•[Aág+èê¶ûõðäÕõµ”0kÀ—÷Œî…3¼#׶i«5˜Ü¾ý⻈”_FÄeD 1…8_¡­±T:\IšP˜qË ‚bJ•TªÉY—YËkÖn#‚iT¯Æän·ÙlQJ¸ÑŠfë”MBB7Ì¥I¥$¦Ú‚ÓŽ Nˆ±ƒ¸‘ró³þ-‘«+1(_ ֠μÕÂ¥ÙíkÐ(Ñî0µ$|d:"T1cÀØ>ÕãÂ9fq.}1€‡p#ÁçQ± Ñeä{g¯Iµ|Óƒ,ú±8Ád¨ÉmRï rÅãH>uúà}ª6½¾YöcaT‰Q6cu³Jjp¼ÓCÄÜuw|·-Ghð«ˆTsã(ëzurü’Ë gMin†@=ã\6É”ìЪlúê’MÒ6¡–¯,'>¾Ð«Ëh”Ϋ¹U«4eÌ:ŸjûqýEBð%UTª0V·8º×?~C¨}jkí¼4¼¥ÒK!Æ|9ÄÂQa\zчÞXŽcI5­æaT(­$tn“Î07yØÚ}ΦºDǵ6Œ9¡V½qÞ®à¬÷ ¤Õµåä•èJ˜ñ&tPÁ(aúâÒR€] +óPú¦Go³þ7¼·fÅĔҫ˜Pû¸¾Û¹ºŒÀ…ðƸ+·|+!24{°h›JÁ¢8µu©ÐŸƒ¢SQ¹”ÐëJ}³; ¹$e;·?;}óCDÌÅùŒå«¼“®ïó…ëA†ò£õ‡œÕùÙBd)ÄDÉÙúJ5?íí½.î¦ÊÓG ¨0³nµ÷2óìè"¿z??½dóSD.¦‹Ã¯þêO½ýÇ’a\ F˜vH£cÖÙÔÈ›C¤µ4ë+›NJUTªÇÅ:¦¶åܺ\œú’Êœ;„…|\ÑbD(!B‘ÔœÝ)ôŽY³é©§Öa>N±¦3è\ñ‘)?nB ZŽÌæÃû1‹Ô[Zi’ËŒR–3Í0©Ï Yˆ+OL„RŒŠÅJˆL«‘f3D§Vbür˜¾Òé'¹LªU©0©ì¼ˆªµ¹ ÀŒaE¾Âk^LÇå +!•A¦çýÄ_ÎG®ºÐÅÂm\isöPÌNؤFÓà ¸ÑHð¹ n&¸,^×éû+åž¾þž3«?™ Üò$C¸ r få3·¼Io\¤µ*dMRÈÂÁØtÿ„-®Â¥$­6N8?N°Ù[|%ÈEˆLR¨°æ·W˜³Q6 áMªõe?±¢o® ³Eg˜IMµ´é4À.…(@•Y/V<µäfk“I‡MõÍꯗ¥Tq)„,GH7T]&£Ó„^‹K¦‘©¢U¬£ÔwøÙô¿½ÆÆÓÆöK«sHie§¶¡×ÖQ1›`í(mz’ÊÍ…KÅÚ𜶚חã1* ˆpEÁj§Fm3ßÚ>{ö-f7Èt£¸vQÝz\Û~’›ÜIÎq³…©ÅÞäøþý;yñWã´Íš-.5L5Ns½{”Õ—œAixf² ªtzU,íÛû•O´Æaˆub´©å‡¨ö¨ºçBžKuùì×rq½Ú?zúÙƒƒO¼(°O[«ïJÕ=&·.•võÊ!8?nÀo†h'Æfh£ÆR¾˜êŽ P‡êo5!¶—Ã&ä@À0é—FNï4Æg}¨P4WÈôa”œÉÃdªO¥û`/ÔJ˜ +‘¦”ŸÐ©¾QÝMÕ÷ã\îêrì¦]‰ò·¼¸;&C)¢Í.evH£›ª„Ýc³*=ˆ ø¨ ±Ìg†¢Ó3òÃáúyŒ6æ=‰pRb"ä*Ôä[>üÆJŒ5ŒZq…©åFˆ(¼Fm3riƒ0»„ÞTó\©Î:Y‘Ù8WŽÒÙ0•ó¡¶;®#ç#Íê~mëe}ç…Ñ:ÂôVX(ùi‡±Úùî—$õnÔh ìëP,Ž)»%•Çfc­¶vj6·¬ÙX=ï¼-m>'ÓcÚ™ÐÎØ…ê½Õ‹'ïÌ4·Ã,'¤.*vÒõ“lï¡Ý€Ã.K³oŒÅY‰‚Ïݼ>{ùCqroóøùpçr)D3F57<)¬?,n<î¾]»óËÜð +&eÙt+.ƒ1Ó¹ŒªM7j‚‹vÅx0{sàg;H¤³…˜T¥²«æà®Ò:ʵ(k‹Ù.“êàZÐêLª-æÇÙþ©\˜Ä(jK‘W'q? ¤FXó$  $ý ² òjeÛ¨ïãfÓÛB~£´ú8Ó½’½¾‚ĸ"Ä3Èku;ò®Ó=Nw‡ÛOž}þGµ8!Œznt×lñ¹ÕÖúãîÖÓìðbÓçý8.—Á B åÍ$Â,œ‚Œh×sÝ}DÌÃù‚IʼnVݲ{'RySvºÃ{¿ø“ÐÙT;Ý=-Žïdz'…ÉÝüøŽ^ß Ó¶jé¥É@_˜ÌX=»y<¹ø¢¾ó|Ö7Ê¢R>@ÙË i>LG¹,“™¤û÷VoaVÖõÊ4Ó;ˆK™[A,Î;jym|û“Ç_þñüÍ­½gTª•-õöï½ +£¹®Ïnï§Û‡´³ætNºÛ÷§éÇÌìà¾VÙÇ”&ŸøfÝêÔRÿ¸:¹XŽ°W£D? 䆔ݞíH¶ó¾×OkZyÒÚ{Ò?yYß~P\»k4ÈT‹Tò¿þ›ÿrïõ/¢œµ$µêA~ýyvõ±[_Žˆà;㓋Ϻâ)67MïW^×?Óº·ý\^Ét¿ÿÃÚ½x³† +7ÕÊViýéÎå/ë›/}Iãȹš‡¸áÃ"œ;i’ÖlºÐ`B Îa¼ƒÉE€V¹¼‘_{XÝy1ºûÍꃿ ‚ø¹º„¶§”üÔ—g-.O0‹2û×@ë€ÀT¿ÊêÕ0axâø1îD…ÙÒÑ8xi2u üGi'JX`…\Ó9w´RG…ÚKaÆÔà¢ÇÅ,i6Ìúnq|~*,¬>tF÷ŒÖq}ý Ô´Y;Ü0‹Ð&o×ÔéÃ4/jºbÚrX +Yð?àù¡`rZM+ôô$U¦&µZaxÞ9ø¤sò¥–;Ó‹ÒøvDtp£n¶ö•ê6i÷óƒóÚúc­º .…6ÛéÖ¡YÙWªDzf 4%­xÕ•‹ —i¦Ú§v÷L«ï¡R‘6ë¹Á›í†h›súra\›œ=þââÕ_MN?ÁÍjktøìýoÓMpY¹ÁÝç¿yúí¿vοáŠ;1©¸áH¹šm¯ÌæQƉô&uÀ¹¸áKj×ÝIwLŒ³9pwa6—Ê\~,3€2ªOŽŽ}rCDÊEÑk ½¡U<ý>ÝÜ^Žsa&]Ý|2Êç&îY'd)Bš¹ÚFkíÈbœÍf 7›¸ÕKÛbe+Õ;èì?»ûùš;O®ûh¥¼ÅçÖPµ uËÇ®Ìr¹Íµ¿¸p%d¾°e4ÏÅ–TØ\ŽÇ‰˜\‘F”Moßÿpïýï7|Ñ=|£ÔöýtfÎ?ksùùßeÚG`5cl^)LclBÎiù‰Ô•ÅX0)Z[¼Q½±ø‹+Ëó~³‡ZçÂê^¤:g(_ŽäŒc¤s}>4ï!oy¨[>Ú‡hà¯|ˆ +YÙÙ¼ïíFd×l{ˆmµÁ°Åu?iÑ©6—!ZQj\nJeÖH«‡rYZÊãB&ü€H¥ ¯Y»ªYÃ1Loµ{ì47¯,‡æü˜3™T¿8¹WÝz’ëNóÁ‹ïÇGÏý´‘jíOÎ~Þ?~_Z{ÜÛÝ9xÃåWoi%?¡ôZ”2f^š/ Z—ÏLsƒ;LºsŠpBøØ°» (žf;!×Xg¶:7`°õò:©UX£\Ÿž7w×·/ó£ãÕãWý½gRv¨•V«Óµé½ææ}±²àŠ+($ˆjVs­£ù=ç%h«+7ÅÜTÈM{¸dý¨Š‰EiP0Efc¿µÿ ˜8)Ý:¸óêý¯ÿQÌôÝ 9Î:|vZš<Ø¿üÅwl콄Ðõ$%³6•+AÒv#Êu/µcl”(Âdý³›“žQ~ÂfÒc»}¼{ùÕËïÿùüýßÝÛK 2›1¡¤Ò.DŽs…$_åìk Š®®ÄácÕê>®u<¨4ÛêHȉ¹Q\7eöv´7î¤ÛZ}ŸHO’FJ„TÚlì~ª+1U)Ïî(ºþÿî +¸=än¸±e{Ça\ûÙõåŸ\[ñÁ˜]±¼C¥G6wÓÇûˆ "Õç½äÕ›~OLñ!†5£â\Þ7[°ŸËôO>—ol&ø ˆœ‚Õ=—kG|aÓêf3€Îì"xWTk@î@LÂ;Þ¸äËATl Z ¡€/ýÙRìêRˆUËg×Wâ rðŠËV‹Ï ¥|ß*ë“ €qZ¯ ^ N^—×ïJÅ!¦bb.Ì̺“ÉNeÓþä.ävœÞ£òÚóÞÁç±Yóg(/eµ0Ñt%D¦F¸"Ÿ›Ê¥MÒîJ´Ö/“³$ŸF(½ÐÝ[;ðü—Ç/¾üåßW¦y»wùø«×_ýH§6KDjh·NÏG‡oBígËÈu¨!bE.oJàÊžÍn{YM·^}û·Ri}ÎÇÉ =ãÁM\ï@ÞÕGØ4¤ Wœëh5öÓ£öÎåéËï§çïˆT_ͯž?ùå³Ü”ókéæ>X§}JÛ“ù »a)"Ì™™wcA"åtO+›ÁÛz=ÎØ©êª'!¸â„ÖîÓ߬?ü¾sòyeû9ª·®,¡;yú«¤^¹æF£¢Ÿ„«P™ëßÏ Þp'¯/¼Q:9»ß‚,Ç¥•„Žj¹´î¾ÑÄ£¤¶˜µ›ˆrù¤Öd2ÃñÉ›“7¿ Ài•(•‚@å3}±0KjãHiÝFÔÊçJB-DH1š|ª ®è „jsîø•…ÀåÈեȼŸ‚¬Ÿí˜PBI [Ȥ}1–1b~dµvìÎ~qí¾RÙ$Í–©ÔG%+)Þ:¬Yƒ_“³#³´&"‘b6Φ] %„Ak5\®A.D© ­6µzË‹A²†@ñ ^ + >Ô¤ÍN¦µ/ç ÆJ²–šoIÈ”Ú΃îÁóâà¤Ø>غý:ßÛE•‚QÙÈïÐ`›NÿB,íE€¦œê2F}9Ây€ #üÌ<)RªÜj ™ñ…}ÕúÉ‚/„kZajÕ Ãã‹/ÅÜPN··Ïߪµm¨ž„ú¸¸ƒkfi½4>Ÿ ² >šÕ»ŒÕ]‰ W\ñ«®¸;!ÑfS-®ÇX'ŒËfiˆI"ÀéQf€MA)nNì0 lczŸPkATs…yÀÿë+qW˜Á¥œ'ÆÍf°¡+uµ´ÁeGRq´»Tº×X¿OC=WŠÕ³êô.¼Wë~:í'M6Õ×+{vçÜfÄìبj0'œ3v'¬+ËIW˜k Æ[wfmâü„Ÿ°g˜êŸ×.¡ª_w%õì ×X›‡H¸ /#çGFeÊÙMà oxvw%ˆÛŒÙ¤í®–õÊFºawÏë[¯œÞ¹\Z Ð:ÐwcíøX¨u³&„¨í¡0KA +"¼ýO?îË0?kV¯áB%Agq6—ÊöV÷#J¯¾syôê·G¯_?x-Twà‹´Tmc÷ÞÚþ Áéû0ŋʉ8› SV~ M_œ–S5Ѫ° ๊±æ4“;Ư„é›®ø¬Õv\JÌîÐŽsöø,ÌX´^®¯ ÷ïMÏ^mÜÿ|ëî»ýG__:£‹¸\¨twg7Nµb—Y»%d&¤1² ® †9J‚IŽF036Û:­gòAT cº;Â1jd“ó‚]2|ª€a¨2™’ÖF'gO¾æ. ìL—g;ÎôìꮜY%Õ†'.[…a¥wè +³W£®¨".5€Œ Œ8 -×oM/\v94{°’n7¶ž¶÷žmÝý2Õ:†Üo­žnž½qEÅ0fÄH FÉa#¸J)EȦÒ`Ÿ1kiAH7Lç2«wZݽ~ñ•]ÝØ>zòïÿçÿÇ(m,Çtª•ÃÂàìî›ß½þ=—[¿¶‚!L†3Û K$—-8ëJaKL÷Àœ/‡X°p S®¸¸0•#ª/a*ùUÆn¸ã@´ â!@@Hø1&@À‹¡I¥ÁfFlºoT7òƒ£µmµ÷s“{jó+¬²™nsõÞÖÅWR~¼—½¨¡ó”Rb a&¢U@ôCÌb˜sÅÄf‡Q¥l3ßi­ŸjÅQu¸÷i¦c2³š*f¶·{òrzòyœÏ͇H*bR™µ{„VãíÙ\kÞ¬ZÛ(—žÝT'S„Þ§LPϦ+¡À º¢ÜJ˜å] Ò³NøTÓÊLºÅgºµÑñ÷?þ‡?þÿGÿà “jZͳ¹×ܼ,ÎúÓûk»—²ÓPœ–”é é¾”[Åä¦;¦ÝXÁ®-ÍžÐAhyc⢟¹¾œ¸¶Yôa+àúŒšwÖ¬½\ŠsÒŠòù—a³ýL÷4Û;—rc«îô¼Ü? +↑¨Þ\^æùÆF©µ}k%±àÁPð r9JÙKê|”4iµªÆATÓiHÁn›µ-q6§®E[=F«vÖÎû›÷ÀQ6ƒKy„u )Fè`}c¤a•Ç„˜™wÅâ¤ðYÆn …‰\^‡ÏA+WÜ{úØuÊæzóýZ~Í©nšåuT©Íy)Á¨—Z;`q¯-GÝ15ÊT¸ôæäøëÑé—œ3ºå£8³®eÚ„ðs¾Ÿ¬ÄçÃ’/™Æ¤†QÙLHùgû’ò•ù ¼nùq¨ÛÝC€>ÖZÛFã0ZÎleôÄÏbiT»»õdûÞ×çŸüîÁ»?í=úŽ4ª´^Å”J”Î@-©˱êG”¥0‡)e°ÄKQa9" T)άSmLoß~öMïð2È™,ˆc÷´4¹LuNfólKkðí³†j#¤,$OoƒG­Œî7¦—|vm9"%¨©5i³‡)PiSˈ /WB{ ÃÅÓ—ƒx“ÒxzúrëâÓÒødzütÿÑ{1ß!µ’Q]WŠ“õÓW­ÍGq!&U)]Ù<~–m¬ EI+H¤Ãd&ÎH¥#¬HR[𢋳v| ×¼Yð%½nAIoì¦ê[¬YݾýYcín\ÊÒ¹AqýI}眔’¯½¬ N€Ùq¹b×wRÕÞúXbÜ-ò—×¼þ¨àÓWý·<øì¾=iÏtÅ_\žÝŠ 7½H„NJ|B±ªæK >çórªå +QsËñª„ŒG’Í,øð%_Ò†_pÜQtdÎO€àõ§æx³Lªe³<}ôÙo÷î}Û† +eÎê¦g;§¤\BhÓž„ª—‹`xX½Ãhm½¸‰Š%Wˆæô¼•ë€Ãù³>9B÷â‹~b)Lûf¥RsÅ—C$•¡Ñ@ráF=¡”©HšmH½ÜðLεö¼›œ|Ru6ŒN>éì?·Û‡”Q˵¶ÄLÛ‡qÊôÄÐŽ•ždF«)Ùø±ÚøNÎÔ‹†’2¡V2ý£ÝG_¾þëÖæÃÇo~ùøÃÔê"—1¥ž”«lj`T÷Kk/šïÕú.­Uê“»ùѹRZl‰ÎºÝšA2«d7¾ôÓÅÈÏ"7üj(xGmyg;¢ª”ZËv’r.BjN}ýøÁÛ/~ý÷'ϾLŽ?>mo]|¾vþ–϶£œ¥–Ö;›˃c½0Ê5§¼QôDÈ$›Å•jÓV¢<ØEOL `v”rP¾×ýÚBxÉ‹ù¢oµ”ܔл¨T5óýã'©êS³FccpúéÆý/ ëO¹üšÕ µ­óßÇÄôBˆð#bŒÉÆé¬;$Äq[2“­û[·?½º¾2çƒÀH +E¥¼Å¦p)AÔÊÝýÍÛo€—£PÄÔjR.âr¡m5ÛÕâR§„™GõÄXO” "겟€"Æ´(®Ï-#¸L©_\€H+v·×NUž[í£Êä.T* +ÊÇÉ뙃E4/H@XðDÄ•aÒCB)ßò$!¢„DäÈìe¥mo˜\öãón”2”Røé ÏO®¯ü嵕kóþ?ÂtɬÃ!Õû©òª'DŠf5@è¤Ùàrc­¶cuNê[/jkw76Ïþýù?÷¼fr:ûfc(U.í$õZ×h)¿qø*Âfn.Ü!o„pÏv·ÁÁH(é6gV@÷|Ž6ÛJeZ]»ÝÞº_îï=ÿìÞýJ)p£‘nßN5««—kç_÷>íî<— kNyztÿ-›.ûH)Âec³~bM)7åœÕ ™v%dœvúRB¿æ%çC p–Y;Ð*»œÑ8¾ó¶³qÁgÚZuÖÎ¥²þtpônÿá/.Þÿ©²þ >9=}ú…VÛU +c)3 µ2ÛŠ½F( °CkKa2€ëÚRkrv?ÎdÝ0æ˜Å¨¥ }Í…€ÉŒ±eå ¨!›ýhÂǤUÅÕ"ŒU„+ðÙñäèuëQ¥»_îm£r–1KF~àŸíb°”7¡…’6Áåë½£öêk‹±PRŠQºwæO +„Ýí"–z÷†[ Ndi…ž’ëóöl/)×ÇÔ*Æç+í­joß&V‚D(©%Y„2”TWü¤;Dš¹N¦2öÇXZÊv¶.ö/?ì=þVk&õN”+n¿üûÿð¿ªëî+¼5àSÆf›z0¢Ýêì<1KcO˜‚pZñ¾ôa¨VÒÜ¢•Ú‚øsˆ®¥„;*&+WâD + +©òÚ‹¿õ$xDÊ‘f˵æ®Ó?Íuw7öî}ñëj­ß®¯Ýon?µš{jy›´‡Þ™…-§óÙ×ÿÄdzqcñÚ‚ qÑO¹£vF)ô;gO¿²](_¸Ý¢R ½:q:Û­‹âä(©­Æfa|—ÏŒt Ñòi6åܳ{fq½::á²]æøñT˜NE9Ç…jË1‰P¼3Žpù„T^Œ+W\¨ QClN̯z“Õk»§¯;ëR®g4vJk[»/¦gïOž|Û;zn6·Ç{~ýÿU,®…Ø ewØTŒ¥RÜþ¸+÷:Ð ¼ÚäîlÏ”m;‚z:.Õi£»b—Âè#ÄØìéól›ì!ìÚÆc1ÛçÊ`ýn_«¶§½íûlªå'tDÌCTv `ÓS€dCx:‚¥Âq¡Rzó^ª½˜j,GèùÇçÖš{¯²£;rq-Âçôa–‡ !›”+`l >å­YwnHÞeÄã¼QÞ‰à†+ÈtW®!lZ´›bºiV׆§o6î|غóó½û_î?ü¢¿vˆs©iR1Nš¡älϦ¤X‰C +Faµ_”ëyBT,i$)ÇfÒ‚òŒ±K>Œëfy¨'ÉÛs>òúˆ+Þ˜š«Rv•µÚf~˜`m@DHƒÏ·Û;ƒÃç»?ì>üª{ú&&e¡äö^Ò™ªÕ0­ŽÈBkÐÈA§¿uá'µŸÞp{CLOùš7.ø! ŸÞîÞ%?“2Û‡RiÕníTw.Ó«÷Èt—·j—¶Î_‡I+ÎdH­eVw‡»Ï¦GŸ¤ê»ðrgã»þ¡¾þèϯû—B¤×ÂLΪlÖW*ÅÍaî\¼SK«W\‰…0b²1¡¤Ld7¶*ý½í“§ã£g!Öb¦Q[ÓJC.Ó*OŽuöéïq»éåF˜ÞŠ0™¨“ Ò9ˆ´lû0×9 Úu7BÀu,¬ÜhµÄÓ·ü­Ù>æÚ5OÔ`>£V¶¬îíüø^a|?ÊærÕõßÿÃÉóïA åÂÀlmk-¥4…º¥í®‰v·9}Lé텾ݗ®-#WG=X¿óìÝoÈÙͱ”›°ÙID¨ðÙÕlÿ|ýÞ×Zy2Ù}xùÙÍõ{þ¤„Œ£²³=ª¸<.Ξ÷…“Ÿ?){€àbÒœv#zˆÍ«åÝòðNºº:ؼ»qïó£—¿<|ùW;¾TÊS?iQzµ4¼ÝÜ|„IyÑé r´á²®ˆè‹¾˜ì‹ð \/5wU§ÿ“ëÞyBä@Œ ÆÄ`ÒrÇ5R­o_¼õÿ®æ}4ød!»ª•·"Lþgóq*E3š˜šçó=°î£ó÷õÝWLaƒÉôÍænquÖüÞǹ@éö)$~TÌ.Æfý{ýq9Œª®su¶}Ul)DE;Â¥½¤áJÊB®+–úˆ–Mµwú'oòã‹âðv}ýAer.fZàŸc³,-Æl¡bìÄìFºUkî¿üFÉ®.„P> ?p©.¨OˆÊDØ"œæ6®Wnxé>ÜŠq9@~ˆ^¨ýåÖÚÞÙãÎÖY”6sÃÛ“óŸï]~¿sïËÁÁÓÎî¥QßH—Æßýñ?>ûd*ÎåªkWO?”†—¥á#¥¸ M ÑtyÄY•˜tÍM_÷ÐËQ-@ä’J3ÝØyðæû§~›àR+ ÉGYQ±È:ýêä¼¾vYš>KèÍç8½¨Õq©0[n/•C”¦ÓÍ­g³íübBŒË&•:Ê"„ +  žäSÙÖF„NûQsz…ñEsóÉæÅÛÑáËÙƒK«üô“oþô¯ÿµ99^‰)”Þ¡cõã\Á—4Àp²Fs !Ò›TX»cE¥z…Y[ž¯óÝÃÃË_ÿøÏë'ÏWÏ?[ômýðÓÚÞ'«w¾Ú¾û~÷î»âèv¹ðÏÿö¿ýîï´-§:bzÆ,JŸ¨é>ÔkËŸêpõÆJüúbø'×¼7’®,ä×ygïˆN—´Z\~•ÉŒ¹üTªìø0•M7öd„ëåÇg­ƒ—ãów£‹ÝÓ÷™Õ‡byén]<þvÿÁ„Õ ­N~6«äŒ+ì3éÍÙöîAæ¦õÌžp ÒBT*&Î:Ï8½Tg»<ÚßyðùÎ÷_þbíìMarA:ƒ¸X¤>ރі/1›oÆØýúôQgçEºuH˜M"GAwHs%Æz7µ“ž(åÃtë”Ïô—’;!&¥<@+ÎJùQ¦½[À¿JaHÈÙÒ褱q¿¶v‘jîeûÇÙÞ‘˜¤KÓ‡ŸMOŸDÙà[~pýý‹ïþe|þªÔ[ƒýÛ—ŸÓzb,ß½ÝÚý¬ôEiõ¥ZÚ#{O¿*ÎnxÉEPبã‹3aí—¦—ˆPlŽ[÷à7“R‰Jugë€pT¦íÚÂ|sKLwçƒÜœ‡˜Q¹ÚrkZq3ˆ›®('8ÝÒêLÓ ¥‚«ÒªEYKÉtKý!Õèî==zù«ÊÆÃL÷€M·\† €3'O¿ùðÿ®½LJ%¿Ii­@R &¸Ù 7vs9¹àãôÒæJ”ý³kî›.t9@û¢R„ÌF79B¥§Ÿ$XÞx¸õøûã×?¶ß$¥bur±uçýO7Þ{üà³ßŒN_¥û{£—åÝfÿܨím¾½÷ÉÃÃ@mZe»8¾ÈöN…ÒnL¨,¨«‹á.$Ég.íO +ñ¨˜§ì›î˜­Tkûàþ»{ocµ7òãÃæîãY{¢þYeíYwïmnpaS1vöXÂI*¬*•¾°-Whg’”ªaÊv%ø¤\JêÍ–Ü@E´šðY,’Çh x–ÔŠf}«µýlpô ¸8ˆήw¶î…M*ÙTç`|úöìÕ;—¿HõŽäÒ‘lZË­½3¸hÓé.a¶a((­BùR{Û,Ž"¤Dõ:Ûœ%Le”c׶Jƒ#Бk‹Ñ¤Àá3=1Ó)ŽÇ§¯þIœÏ=~÷í´l+)d›ÏjëÏ +ý ­´ëFäÅ…Pöå‹o›ã³[³Mf-T®I…u§{»ºñ´8y@É…go~qÿåÏáÃd +HdpòîîÛ?¿þƒÙ= Ó”µóÇÙ={éFµ›nÜ‹rqÇjž’zg΋Gp½=½S/™0aÅÙ ›î™õ=½ºÍ™ÕËÏ~ùê«g–Ö¢tŠ›ÝŠß*ôO·.>{ðîÇÎî Êìj]tÆ>T¹¾ûËžk QpƒðQ¸JþÙ|8œT2Õ-ODZô ›1¶0Ûrµ(¥Ž0iJ+‹…qir§:½$¬¾ì´[Û»O‡OÏxý‹?56.´úÆí¿¾üò·ýèÅgßÿ‡ÉíÏ¥ü¤ÜÚ?xð•Q][A¤aóV‡Q À¹ÞŸ L§0èó7ª~T„šÀ9ÝÜø¼´ñp|úiÿø…Rnœ½lï?ÍŒÏñ…Zßc²£Ù¬æâ”Kml1ªQµcïG;—Ê%Ô™­ƒ¤^§2=*;ÆÌ“êF›ÐêA̶JÊELÎ!r:×?]½ørrçËöþ'Õõ‡ÙÔ·ž]o?üÜhnÛ­Ýñ/2“ûBy]È €X7OžÇYÓ2¥¸Në 5?ŽÑ?"ãRÎ(ŒÁ)ýl>6çÆ‚¸Î˜ÕâàLί†ˆ”7*ZÅqœ1=12€r@ò—ïþúñÛÖN_çZ»r¶D„6@§f;ÐùIOŒÇ•RypRî¦k®({m)êŠò1: ¶-)d`Ð’rçSÕî–’©ÝòÆ•Ì`|ûÍöãoú‡ÏSíCLkÏnhÅ÷ßÿ~²÷È„Ë“Z#Bg= `O-„©I.]È3¶ +ø.£ ¹U!?%ÍŽ]¼þò‡gï~`´ZR,÷÷?«o>ÏõSµ  ƒ8WtEø &µÖ@ÖÜòâpŠ5 ´:.äb´su1î‹0ýÕ“¿úñ?/yñë‹1WXHðy)·fW÷Òå)-¥†;¼] ÍÚs ™ü&Êê•ñqcëakûa}ýŽÓÙQ ƒÚäöÆý÷ë/[[P<íæŸ_Å´šYö6ïI™Þl*‹1óó¤T"¯„¨®ÂÏŒVvG LÎë•éèäÍÉ'¿Û¼ÿEwïróàÞþ·ÿþôóßH³æW»Åéeº{»0¾7½øpþú¯'·?kLï¼øúo„ò†ŸÉè̓âôQeóÕΓ¿Þyú›þí·­Õ;ó¯ÿû/þæßBlÖš+ˆîÆŒ¨\Š[éöq}túäÍ7Û÷ß6÷žöŽ_5÷;ƒÃîÁ“ñÑËÓç¿xôóA k“³‡ï|ñíß<ù¶½÷ÂjîÒöln'ç¬ú Ûçcl6Âæ *r©R˜Pf“N„ÃZÝm¬Äðf\æãÅJ÷Q¡$t€¯Áîc)ßsêãúp/×=PÊ;„ÙÇÕ)Wã”9ÿqi‰(iŸøu¶q@TÄ¥r‚vàM\*€*j â[ÎÁ•-„©¦ B:Lð ^ºYYíî\NN^˹AR†ƒœ­éø8㎠¢J€W­ª¥µ8ãPÀ4ɾ¸ì‰raL Ò©òä|rúIkûÒlì¡jÍ—ÔTÛ.­¢|Ž5I¡DÊ 5;"•r×’³¥ôJˆ‹`æ%äú­èµùðJEh;ÉX´VÏåfÐãWBôÏÂ×]ÈB˜uÅo„erRp’b&ÕÞOwN8ÕɃ·¶WïéÕM!Õ´K£\o—±r”fɹŸneº'ùÑ]`=?¦A KéêÌûˆå§Œ‰B‚›÷$€ˆA’b–wÚVk¯ºzg|ðtûäòᛯW.ëw÷Ÿ|·~÷ëÓ¿Ù½ÿóÊèDËwÍÒ¸6Ø?ö…Z™Úͽþá«‹·ùá‰Q›Š…n¾1}þî×÷?ûe„ËêÕ]©¸a·Žw.¹uùËâàd²yþÿú¿^~ø•^›†Ç`·{‡~þÛ_þãûÃú¿¾ùÛ»óì›ÿôßýê­ÎÕ>Pk{¹ÁÝÑñûÓW˜ÞýŽ´z¼QëÁà×·ý¸éÃtT.Ä¥’\Ûoï}j6v“R®3=á­¦œ›èõM¡83É9ÃæÆåѳ_1©ªškiÅ!í O;ûŸéÓ„Øç/—¶øÜj ©Hvás ­!<ýqs–ÔrT$µzixÎZ­ eáFspôYmó‘TZòcÖiGèYšg†p“ÒŒYaS-.Ý’ó}Æj€40`¿ÎßûP¹t?JÙ¾˜5Iõ£„ —^ÊöBL:H„^•+[ùá.3¡ô:§–8­„I%Û÷#êb€%ä’š1V(ld˜´o,£¥I>—`€‹8v…Y(³™ …óðŸË!æ–Xó~"•ò³s´²^—Ƨq>Ÿ-u2Í)cU¬êªVÙJÒKëÙÖ¾ÓÞ¥õ¬`åä| “³JqM«ï‚Ï¡µfª´•®nº€ýR‚¶"„æKH·Ü‰ëK€ˆ•fÂ: "´ÝlNïîù>þ¢²vÂØ%³<½pZ…ÎF¦¹N›5LÌÊ™šš¥ ®sݽÊäTÈ´I³•AérR¶iº…ΖYøq­081Ûzec°÷´5=Ï–»ÃñÖ›ß啕öhïÁ½·¿zúõï^}÷§çßþáÉW}ü⋵£{/ß{þâÛÑî“‹O~Ó?y7<ý°qñatð¢0¼” éâð“_üýèøåM±fH£&ä&ÎðA}ë9eT£ôþÛ?¨™n·Â|Îhî&÷6î~½÷ô‡Þéç`c.½ýüû¿mlÜCÕ*ŸŸ`V·Gí­‡Ï[Ý|’à3ßü¢¾z¶’P«ÝˆãK¸ÖnLŸ€EW²ƒÁúÝÕÓO!PË«çl¦ +Lª#䇥µ 2Õ`’’mŸd­:\‹—ÂÔŠR÷^=ùî_Ú;!Íõò:˜COdÖ8"€¨`Q¢p“sÆjrF%]›ÂéTÖŸ)Õ=\oÅY`ê>g)»Ä,@³²–jn±©.Dˆ•ÀB/!”ÞD„‚+*q€9œó’ó~j9Ì-¸­ ×–‘Ù:?uÝC™Œ\šê]µ¶!WÅÜØÙª]Æ•¬;Î&„mÕÓ­ƒìàÂéŸsÙ¾`W3µ‘Rì‡Å(­•§—…ñýÊÚ#Úl…á,bœ+HšNÇÎ÷üqî–;6çŠ-(`aR®ãb…QªÕó¤’笚QWÏäÊ”uZ0&åÕ„–÷%h¨´ÕËtogúwf=¯¸,ˆˆVœVÊl”ÎPZU-Œl(N²Þ‡HË!Š”r¡¤äMp .q¶jdóÅv±±Ê›E-]mŽvºÛ'µl{*ºjmâôv««£ãK€¡áÖ»oÝ~#©:mJÎ%h-ŽqùRoÿΛâðXU/L3µ]³¼.fÇq>IÒ²nÝ}ŠòYÊõÃÊêƒ{_î>újõì3«¾-ê…Oß÷oÿýÿÝ}ð·ÚÎà"7}Ö9ÿöÑÿ¸ÿðçµÉ¡Yh|õ›¿ÝðÁ›Ô‚œTÚÉŒî·v^Þ~õ‡LçÀ)?ýòLJŸÿ¸‚¨Ü @b¦úëç.Þþáìó¿5Z‡õáÑ»ßüKytÄô © W×.w.¿{ôó¿ë¼ÑÊÛ÷_ý*ß9¼¶‚Ü\AV"b‚+QZ³Ð9œ¿Ìúã—_þîŸþÆhàöP®Hå]­~´óø×'oþ¦<¹LЙgïþúé‡ßÚµ)Œù­ ™Ä ”/±©ñœ[ôÓ§wÞ<~õMÓ~rÓ÷“Þ?¿ê¿²ˆ)ð<‡1* Þ¯½~ÁÚm17¢­pn\˜mfDk5€¬$—Òó(¥GiSJJyÝé¶7®_¼Õê[ Ö*´÷ŒÚV™-³ò êr„÷# +-g%³MòþF †?JxblB."j54[âWC䊓•LÇiïÔvo_~³z÷óæÁ»sæNð1BQ²Ý(XY.¤¿•ÂIƒÑ¹Î!¡7âlÖ©®Ã!…5H¥VâJˆ²C„$"[-Pö¦f;ò’Õ¤¥b3’Œ. ³)¡8î(ŽKÚ¬HùnwïÑö½·“ãFiìÔVÍêÙ‚”ª3r¥MZH‘¢ã ó~"ôñFŒ4PÖJÐ6A¯Ðb–œ`RÇå2«×Q. +©àv+Sv'GõÑaÌžfvwŸUÖî¤kBº&XE=]4"¢U–B,«U»Eª%„KEp%‚Š¼Z²«\vâMš©æÓ=ÖŠ«ýÝÇ«g¯K«ç˜àT££{orÍmL(dZ»©æ¶VšðNJÊ91Òâg}½ò ~*4P©j”wRC93¢Õ¢bækÝîÆü‚=8³zg•­Õ'™ö>T~B)a”²}üdëü5ø.°‚*˜jivV´Zž0ã5Mµ>ýðÛwßþÀ´k+Éňâ!sI³çôÎö‘ž®ýêÿüé׿s™ë‹qOLóŸ>l|ˆ±¬œu*#”MAÑÀ•‚Y]ï×Þ¼WcJAÔòëû+ƒý®G©Ÿî‘ZR‹QLð†q—?M‚¬EH%i­TX{ Ï:x¯“f›’œÉÆÙÁÝÏìƦëY³q”Á¤Bò¶ ƒq.”“l†à ¬Z3ŠkÀe,„XR¬–lÔZýƒÉÁó“ŠóN‚w˜îò³c–œ|kÓå„}är€I0iøB+“jس+1F3ËãòøÌnªÅÖú#»¶gìöø¤Òߢ*ð`”²b\Áç¾)ØC8$_Õ¬¢‘í¹Âà$5ÿŒÂŒ0á`R9B¦Á +3gÒ•ª^šZ…1!åÁP1•œÍžÏü•dÍ$g¢ž!„›‹Á€Ãšî@Ò¦}Q¾ˆfû<2v•Ê~Dȵ·R­0¡RFtšŒ^–캙iz>cx­˜«O¢Fi„.pät‹7kp:0þ¸Ž7L̦rwâ˜"©FÊÆI3JWÝ3}\.âÊlE$!X­¬¦ë8cú"Ô‚+ž¯Î¦o,Ån­ Þ NÐ'š™|¯ÒÞ…Ÿ9v?cJ˜ÞBår‚Ëà¢A +Jµ»jäZ×]èl]Xy»µýtxú®wø2„ª(m|:A)(¥$h0l:.9œ]3Ë#³4¢Ô‚b»DðvUH±ç ‡p¶;D ¤Oò(ΊŠŽ‘+i„h›µ1e×cœ¥ Í*ï?:ü^vjªNc¯¾úØ®%ùlC1N*†©cÅp%NhïJ…OµŽ†aUÏÏ_¼øâ÷QÎàrœµ£¸‘ÓËÀËôF(W€¿ç‹òaL‹PvÓ}ˆÂ5F-ÃÁãr&É™¥Þ^¾wjìšÕ-­°ÎÍ8m›¹¾QB\!\Æ®nfZg˜6‚Ð +"Ú²7ˆb’–á´ü•ùàõ¥¨;*`b•TÛ_Ši`±Tm“Öë`Tâlªn˜LŸc(Œ1Ê\r#á8v`ó^ I‚/Çaleô¢§ZÙò:BÙqL‹“ÆçàÂIü§Uî»#$TQÎvË2rcŒB‡¢8Þ·Ö „¢Ñ8šd LÈáR>J™ž( Fw%@éÚ|hÙ‹%3bp½h3‘TQ.Ž«ÝÍ;²]#<)å“\.Ée19¸äI,¬„=§­EObÑ›Œ$$¶òm3Û”*J¥ÿò†ïÚbôær|i¶ –t‡)^Ï¢Œ¶H^Y1z]Êôu00µ5)ÓsGè@‚]ð¡Wn¹C1<ÁèaBfät­·™m¬©Ù6˜A„v…œ'$Ђ ʹÊjuzçV\ô%Ý‘%O#y–׌L…–m«Ø ­¢?)òZ¾3Ü)Õ'ðC†©2Òsm ñ›^ P„IÊ¢QUÓ-„1¼2£CL*•iÖ»›¥î:Éëz¦ +¹@UøC„P! ¯.D—¼x›Ú¥€;¡D˜!Á‰“Pˆx+_ŒÒ¢¥ÿ?’ÞûË‘óÌïý#î±w%‘œ™ž™ÎœQ(TÎU(T¡sn Îa:NwONäÌpÈ&1‰%R%‘”¨e¯´Êq×’vuí•-¯}mßs®¯Ïýõ>ÏÁá9œF¨zßçù~?_ ê}B1_g•"§/z‘„3"ž_Á)Æðçâ‚€Ð×8HDކ¥ødÉjíH݇ÞåÔ¼dTf}ñs³þ9„1é@LœóÆàHR™¢eÙpnN_t΃€ž+™®ÝÚþË7-lœO—z»za d +¦d*JjqÞæ´:B™0ìf¶“.ô|a $f.!¸Œâ¢7sxCF¦pxýnã&§|3¯'œç/ú@aÖ»‡¢r‡Äšô1¯/<=ç¾0ãvûà U-æír)'ç·–`\&`aÞ‹sÁéùh8&yƒÜù)ïSgfŸzzÖé¥i¹L +9ˆ!é„ntEÔËPÒÁ(õ¬5½yúܹ ®™¹€Ë‹‘¼F¥yGxr60çBÝ!Íi»‚üÄ”÷Ü«äc”~~Ú;½uø¨8c(z9Nè.39ƒø‚ì¸;¦Ó®˜?âÉ-x¢8Í4Á D!—jô»q÷ÇqÅÌê]=Sˆ ²7‰D#‰¤Y¨/jùöÓ“ž ³oÔ*£ë6'!±Õrny{;‘+͇ÐiOÌ‹ˆ( ¤D2² ¶·66–ï¿ðRµ·~q!paÞwaÖSËqš¦ +¢ GTU²íÃ.ϼÏï&ˆ¨eÉÅ’yùpóàd¯wi9×Ì©†Ì'!RïöëŒC§óRBIªBBRŒ4΂Vêì™s„á#²åF±7äôŒU좜Ƹpœw‰‰ÿg&œ®¨e²ç£0tN/"(V¥5ì¯mò² + +2ëF¸*êÐ;\NG9#ß\Re.Y&äì‚÷G„c¤À +Á%è€! Þd9™£ÉLZOèIVeE&ðAÄ( +i·óËë’U$ƒQTDð„?"xC\(&¡„Š²^Ÿ×ïsù|I’Åô¨‘Ý]©ß:^»¼òò+/í­I=FÉ„ïÃÉäô|䯞š>sÑíô’ !Ng8&IœÖ”„ª(¢(‡"˜„˜ã †Bâ1Šf1B`8KÑ+Þ=1í~êÌô3çg“ã5ŽÂ³â™ ¡§ÎûÏNçݸËGº·_¬.Îy.΂Q +äIDE <ªñ±^5¹¾”ïÔ¶ŽvÊêîrñɃCx|çÝ{¿ý»/ÿìçù½Ç›Õ”Å2"‡ò™ñæ¹XŠf“;Õ¨›‹Ý|·]Hª¼6)Npú‚>@‘ÄZ½Ü^j¯mo=zýK…îÀ‹ÆüÈäÀèÙý˜P¼ÜxëÁÖ×Þ¼òÁëWþðËïþú§ß~óÅÓýÕf³* +R8Š wTœµH6…ÒŒ#y]®eõ‚Á÷ëæÚ °Ô)n¯-Þ½yxítýþÝ×_ù…ç®_mÖ›fÚäy: LÎx=AzƉž› +Ì:±³Ž³ Ó³^Ÿ xMd µÕ(éª ±D¹˜-• +fʶÌ|$F?s~Öí‹Gƒ~ ª„«šB.R/\éÞ;î=¼1zïÍ[ßúòý/¿vøÞ‹+¿ÿþãþõ×þ͇/áµ›»[CÛ¶Q‚u¸¼ç<rÙÒÏbý\h­†]Û0oÖ¯ìÕàqûdñáµþkÏ­½þpõ½'[¿ÿáþÏß|ð¯Ü|Mb.{ÎÆ(§D$Š°±@Ë@zÂÕõÉ•êû/]zçùÕ·,½ÿòæ/¾ûâ?ÿæƒ_|çÁ¯¿óì?ÿìíÿùß~ó·=yýáεÃn6kGc\(‚`eL¥–×:ÆÑrz©„/×èVž^íÙëƒÜæRöæAçK¯œüî7ÿæ—¿úþûï½üøÁáþÞ2Hô¼ËéÄý¨81ë}êìE¿sAç"–ˆd”˜ÎºuÎ×))£¶­q¸l½ùøò/ßxòðøëïÜûá§_}õ•åª5\]^Ü<>áRõÏ­0±”€6RÄå%ãÅ«½‡ÇÍã%õå›ÝŸúúÏ>}ëÝv¾úxý~øÖ¿üÓ'þÝ·~üÛþå[?þèÙ“œÊ„ƒ¾€ÃŠÅa@Pñ¬T¢ïïÿæ+7~ö½×>þÊ/>}øù­¿ûÆí?üø­Ÿ|øì¿víÏ¿~û?|þ+:Oê{«ÕŒ¥e³YYË*É"ð)1ÚJÇ7jÂÝÂÏ®½tµõäZã»ïßùÍÞýóÿöw¿øàן>ùïxÿÿûþø‡_~ëÉõîÇoÿâÓWßyr9m¨WôÂlȤF |Ñ€;rŠ¸OÆæ‡%æÆVñúNõpd^ÝÈ<¾9ü䃗Þ{íÊ{o>ûúK,3í±QTdq¼`ú9r·Å=·e¼wéo¾xó“/Þøé‡/þûŸ¼ûÿþù§¿ûùWaþðo_þóo¿ñ­/ÞÛ]©¤ ‰aEÍÈÐ$!SHÍ VKØ•wÃxõ¤ô7÷úñ‹Ÿ¾û£·/ÿèƒÛÿáGoþ÷÷ÿüÛ÷ûýÿáÓ¿þîÃWïm–Ò2˱!No8 <¡hE]]ɳ¯˜ïÜ*ù^ó“7¶ÿþÛwÿÛï¾üÇŸ½ýƒ¯ßüíwŸýÓO_ûãŸüø«§?ùÚñ/?ºñý/ŸÜ>¨V‹¶ú(L¢Ql}ÐØZÌn´´Û[é¯?Yý»¯]ûøßØûý¿ð«O_þ›w¯ýÛ/ÿßÿøáÿú¿~ö§_ù÷Ÿ>ÿ_÷åï½kgF÷Å™Y—?’*ËJrË75ß!ÿè ûð óÊÍæû¯lú•ÓÞÚýéÇÿË?~ò?xó?zû_~ÿÑ¿ýøh«5-ÛÕAWÓ¦]²”N†ÙmPðò÷ ~ñ­;ÿ݇ßûÒñwÞ¹üãüçß~õ—Ÿ<úôÝãŸ}x÷?ýêK¿ýøÙwß®vy RmVrz LãBZµbªjQm›8%ï咽ÙoݪüÖÞïþöɯ>yîÝûÿá»ÿã?þíwÞ>úΗn¿øÜ!Ó!„IBöÈ4iYøZ…¾:Ô^>m}ôúÁ§ïÞøÑWoÿ˯¿ú¿þë/þø£7þîýë_yq÷Òj5•`œˆb‚¨½HØÏfåØrY¾²R~î þø¸ôý/þÃ÷^úÿúµW¾öâÊ÷ÞÞûÉ·?|ëòÃ+£õF½Z—"« "A;Š™/¥Ó‰èÚÔÑÐ>YÎÜÚÈ|ñ^ïï]ÿå'O>ýÊ­ï¼}üþã½×o,½vÚ~v·¼¹˜¬=ho˜¢¥Šf¶JÈ¥™W‰•ªòð¨ùÑk;?|ïô7ß}ð÷ßù?xãO¿|ÿçÝÿÅ7®ÿùg¯ÿË/ÞþÍ7O?y¹ùä¤4¬HpùcªQÏä¢dª ^K’ -¶l…ï®*¯]¯¿y»óW.ý—÷Õ?ÿö½ßÿð•þå;ÿòûoýôÛ¾õæá»·WyEKâ\*F%K•žÄó)™Úêfz|”EŽ:ÜÃÝâë·†O®.Þ߯}åþèW?üé‡>~ýðë/¬¿ykxm%¿\Qu¥)%@2“3A ¥h$ “þ¶InwíÕêþ xs«þâiëÛ£·žÝ|ãÞæq?u²˜¼Ü·–ëÐë@tEqÀüŒœ¡Ä¬˜(Jrº”ËRj^ã6š™“åÊ•aúÍ[‹}~û×ÝüÓÏßüñw¿ùdã/Œ^;̼°™z²_Ù¨Ê*Àcq8rz€Ð–ËE42TÑðͺ|¼È?Ú³_=­|í…•?üðµÿùç¿ùÓ¯Þùí§¯~óµëW×½J ×ƒª!” h8 ÐxFã+&×ËËÍÔézùúVåùËí^ÚÿÖ›Wßz°qï Õy¢c.¯{ÎðDùT~©ÔÞ3rž×’¬eŒË[Ë7Ž7»9êÊÈxéjÿkoÌñWW +ÏOVª­[ÒeS–šCQÆ`§]ÔÄLlr2S†Œãx8Ä¢1K× +»œÉTm3£²–HgU¾lJU±hJ”ªÕ>$ôH\ÄÅty= ÓåÃCa‚gåB¶º<Ú[î­ÔRÊZ5y{»¾]“¯­–– +[5m»®¯”“Ëe³¬ ÞÙi§#ˆ©„T bɉéÀçÎÌMÏz"þBÓ¶"ÔL9'Åú9~½*ßXµß¼»ò…ç6_8ê&CS(¥$Ëjy³8º‹qˆç<Á±|B’'Ér¢V­³FÎ’s¦¨+Œ¢ð¢šˆ³R€’füñI'â«t¢¬™éE$Z°äñoyål¡  ©@Ë45ÅÒCóþ`Ì"ýÚ¦g½ø´Ñ™deǨ_¤<¯—pVG1NV3Ñ m"ŠF§³Ñïo·Z+FÒ‚è=¨&+Þ0~fÚ1¤t6®¶½%&­ON;§gÝqžf%-adRz­ZîæN7ë—ú铵ʕÍÅõv±WHvòVÉJù‘3“ ŽàøÞö îØSSÞI`ˆ7‹\±ÏÒtBÀJ—“±¬ÛíŸÜ9yéþ+»ë«­†©J‰GQÓAÒ ©ùˆÀÙK…ÁÍBïz¦°ÊrÉÑhûÊb(çX¤¢ëùl¶^,µm«Ä1jÀG„£òÌ<21šv‘>Ìr!º3¢†“–ó^O4D!lÆ›ó @澨Æ 1-œµLpº¡d@ÖæHÁŽ’É8›eS]97rFåÏL¸œ~Á“m(©§wP*CY¨´z¥ö’ª¶ªœÂ*ŽÉ(¡Î»Ð³“þ‘Šsù9ù¯?;ý™§g§æ£Ñ¸¬ªfBËÄbT ÁxZÐ%•F"•é¸ÌÐ )™sN':ãÆx«o/^Ur#)3ªŒNå\+@g +V'ÝÜ7j[1>!DAI7úÓ~äì\p&ÀáÉ®_2+D¢ ´Rj,ŸPf{&ª†øñM.çŽi<'5‡7vvÚˆÐ6•hÄÅrOù ËjÔ_Üž ñ³!i>,E˜Œ`¶”LWLÕ’¹)eà ¬V Qf„ÍZƒÖ›¬Ñ²}ÑîÐj–Ö«A&ëÁtžôÄ0“ QigT + ”79½L)9Ψë…a"Û“SÕ8Γ¬$i6«æ'~‡ b:kô¥ò~ˆ/̨ssá§gsa1Ì•Y³ïBÄ1ã¯É±iב& +Z¶Ò1šßžŒ‹¥ ™– Už™/$tF¯[õ}LmºÐ”‘ý¸¦æGZaeÚE¸BBŒKÓZ•µº~2íD>Ì µŒÌBH˜ö01¡b·ŽÄ̈IõBlnÊ/¸|,'Úœ’È©u¹¼©5w³‹—åÒz€Í:¢RŒ1+ýcB,ÂGËÙ‘^Þ1ëjew*’xÆŸY@ݱñMCõÌÜ……ZÔZ—S­C1»âŠ¨g'ƒÞ°¸à>;áŸX@Ã\žK/kåÑ^q¡Æ|D=ï$æ}$ÎY¡¸znÎÂ…é ÃGEHCÔk¬”™8Ü$.EññïYΈ4¢Š—²Ý¸ù™§¤òVT)áJnõÒÝÊòIPÌ¢Z5"(£Æš-93¨®\sಠ•âJQÈBb^`r”µTì_Û{öËfgtb“ÝTjÇTz‘*^L[ß–ž"ÿòëX€²Bl6GBnJõØ iÅKxpÃO¦P¹Æg–åUÆ섹Œ’[²[{”VåŒV +ñ™‹^‰(q¥æ#Ì°P@”ªŸ4åT³68a¬ÅñZâF/$Õ£J‡4Vœh*@¥Ýqm&HÏI*3ZJbbÝâ=ažÓ›¸Öð‘&²¹sÓº1­ã‡"Ç L­B¯ù1PÊžøx‘í0—'ê©6Õ&œdŒ·]QnÊzb²ŸJ“Z Sj¸ +ƒS@¥*¡5{8}„dŠ¡`ò¨2¾Ëf‰]pÄysü“å|˜uÅd$Q‹$Q¥ÎÙÊhE¸L¶ µg¢ f„²Tݧ³ëReW*^:·@L{YL®’ZÍ%ƒT΋e=qÄ9Âæ¼ñ䤛ŒŠE)»,e–½¸9奢ªuˆÔ’˜jûcÒ…yä³|ÓN 4Ѓ”íB4nxbJ„JÍ9ã kP¸bvÈçVéì +•Y‰JÕs³ñ¸±ˆé­die÷Úk¹þ¾‹J†Ä\æ·¸Ý1^Î=½8¾Ø¦²ª–F\™Œð q#žèä–nçVŸ3¯FÇËÆjD²ÍåÖ¹üfTmøŒ¿}TÙâs#–t¢c-©õ±´Gã%È´ @¦ýtʪD²)æG©ÖÝ9¶ZZeÓ(o¬^~L˜íX²i®7v_ì½ÔÚ{Ñê_)ŒnqFÇ̇]d2Èg¡´¸üV²u’ÝÉï$[An|Ï)möp­•+PŸ„= SÎêÍ•“×H£uÖ9‰”Z¹”^¼•lÝDµ¾Ÿ)x kØ,*=¨ä‰K“!a.¢úâz˜¶±ì£2R~U-o„xÓϘ¾ŒÌ¨MqÝÇd<±DctEÌ à|§lH,"j3–è‘éLk€ˆùQ4[|ºO$>LÑc¬a¢r)YßKÖ.Í%ÚXd¬e2Ù‡Ïr“é9Ô8ïå=„­–¶cbù‚ 0é¸Þ¤ì!™^Žëmšo„š]ò%P`8Á°T +›˜±ÈæÖâ©‹°ik¨Uv¦ƒP–j€ÊA³Àû Ùå(øiû ‹ÏŒ\Q}ÆËŽWîE ›Zm7®uçüôŒ—šÉÐw {OM8>{Þýôî&sŒ¹$dWq­5ïc!J@›Ô¼¤å¡Ò~Ú†vcR˜èLçÄn€ „Å,¦×ÅâJ@° ½FZ‹,¡ÙÕ+ë…¥c>Ó›GÒê†Á Ø<¢TœLÁ'µBI¶”âªOÂðÒöP,næ—ïõ/¿YÙ|ÞÇåCìühiûŽ‹N/N™¨Ø`3ëF÷ªÑ¿F§{¢YÓkëq£I[=(QÒèÐé¾VßUËëH¢$–Öo¾9¸òd¼lTë-lF¤*ô{¹w°tøh*Ēɦݻ’Yº[X{.½tʮĊZZïî½î9]+o ^hn?[]¿7:y+¬‡„Bì/»Ö¢*ŒšÖÙyÜØ~)»ò|¢uáŠ!6 ºMX »–Q©ÎÛ›|n[­î­ý­Ñf+Q?Ë»tn#¿r·¶ýüxUŸÜZaé&“†Â0ÉdJ/ ù©´EÚkl|³›’Yên݉y'¢B] ™aªs•Î¬á™Õ¨ÚvÆ’™æ¡š[žK¶èÁí0_Š»Jý˜²×=dÎ(­î\}Énêb@á"@B[Çý+ïzçèöçßýðGvs缟G ÒZʬ?W;x¥°ñ€/l8P=Õ¸ê4”&xÏ{™ÔÕ¹ª”v‚ŠI€ÅxPyÒíšðñÓÑ–ð¥=sñæg'C¢~\÷à)OÜòés¢¦‡Ê ùM½q°‘fÇ«o!6J‰úa˜+Dù|L©â©“YÍ n7ö^Œk•DqN—]<‚¢JÖ·¸L±Ú”Õή)åu2ÕŽ'ÛriÇîß²·¤â&¢Öçâj€Ïù¢”ÐDƒ´†¤½MtaÀ+«wJ£SÐm¹°:ÆØK˜Þe²«ByƒµÚýí[«§/ñ¹%p1x­Y*,߬í?*;j~°¸~úò¿[¹+ž\´z×K«÷k›íÞaˆ³¦ ÐÉ¥¨Tò±f$Qv16a-Ö7Ÿ_<|-»t´ú”Ñ)ŽnaF'$–èìjfxK®ìRéQ¢zI.­u!óQ"QèÕ‘zÙbXn±éñõö+G/®?>m ­[Ýïì¿&ÖNczW*w^ú0ÓÚsÇ BowwŸ¿ýê·nþ£ÅÓWÙüšŸÉ ÖâKo"e‡ý‚Ÿ)r¹ídçFyãÑðòÙÁMT.6W¯¦›“AÚEš¨1P›W»ûŸ¯­ßk¬ß¥S½¨X±Ú‡nÌtc)Ú0¹5Üú¥»W›÷ ¿ðfßhÐö2•îÓÖ""ÖÈD}åèáñ£¯€<†Ø¼ZÚQË[|~Õ»ˆ\²BëF7áß'Üœ#ª ùeµ¶jfúWæ€x}œYZklÜ òN…Y+î“é #Ró‚—7rKWï½%¥Zÿ꩹‹Î8à™\Ú+­?\¿ù%ðGD9~öKJ~Ù‰é.<3†~:㧳àMJe¿°ü`ãÆ{Ã“Ï fûÒÑýòðLÖ»«–Öòý˥ѵÒêM½½G%ëÙö%ø˜P<Õ„Öc…6‹éÅST­F¤"˜Ù¹ž[y¾¸õ¼5º‰§Zzv°rü’V[ós–XXKµ//î>Ú»õæî³_©oÞKWÓêò)o÷SͽÒêTû8»x¼¸÷üÆÍ7ŒÆJupÔÝzÀçW¡n£j5$äàƒâZO*íÉgµë×I³ã¥Óã¬QÜ¢@±íåÒúsÃW &ÅÂz¢yÐ=xÔ?ý¼¹t;fôdÚM˜|fÑîÍÇYÆnÅô6náÀôæYæ¼à!£Bµz”ÙÀí>žÝ䊗”ê®VÝòSF€Ô2­.ÝÁEµ¼’_¿m´÷RÍ]«sd5ö´z¨T¶•ñ,äê–Ö¼ÌwÒ½S±°²€È…þåòÚ.¿&æ×À @ñbJ]/o®?ÆÕ˜T\¼ôìÉ‹__½õVc÷QvxKÊ Pï?ºñø[N̲ÙDq­¼z·´v¯¸v+¿zÇÏäOï½µ¸q3L™¨”ϯÜjí>il?*¬=é&„žŠ©í°XÉ G ò”µzüZ²¼îŠk6•à#Ó@Ýa¡ˆi-¹´eµ —I¥4\?5ë«!ÆàÍN¦s¼tðdtüJqùºT\¦“U†•õ; Q1ÀYl¦oµOK+ÒýëVïTo첩Nºu *D,¬&»§Z÷2 í£5íæQgÿ%±´äíÂðšÕÚo®]¿tã•Þþ#¥´ÊÍþÚæúuB«Yõ­Ý[omÝüÂàò+ÝײÃk¼Y:ºþx÷Ú«~ÚôV\o1öj Ù>á‹Ûh¢eU×O½›(¯(}¡´#Õ²Ëw6î¼ÛÛ¢76O_i^º7›Js—.î æ2WÜÕ›WsÃ;„Ù§´òÉýwZ÷Æw;²¹h¢…è,ÕÉÕL머x²º§µy;ªuññ¶ÔKnhI. JÛÜy0–MÞ6êVïHo^Ê®$j»@b?fªt×KY¸ÑL¶­þ kp»¸þBº“Ï®€Ý{së:°_àj¼Ánc/Û¿–¬ïÊù!l\.övîõwîéePû%6³¤W3­íí«¯.ï?tÇuÞZlmÞí>–ê{ãÞí!•ZÜ»þŠ]]ñÅäñ¯ƒãtç@-¬ˆ¹5ÌÍÇ­© Ì$›2U]¿³rë‹Ë¯ê‹WS½£sŒ›+Ó•MvZ+7µî‹‹aÊðÄS1¡À†kf›G½qÒ»~Ê>ïfœ±©} v!œ‚éCÐŽ… “ó`ɸ\/ H^ÚŠÊeÂZuKÊ°dÃOh”Z”RËk©Ö.mv!Á‘€ý}:·|n‡Ò* oä‡w˜ì("—æ€É¥Rué*›jâr¡¼z£µûÆ¿øÊâÑç‹ëÏén€µªk7 Æâìn€æ,šõ-«u‰0QÎJd»z¡i­éµ èn,Ù¢“%«¶&eûS +OÔá%>6åe 6·¤TvÕ]½º¥–G~JçúÁÝ/._K®]JÔ.™ *vÕëK[·@"b);º“ÜÕZ'©¥{laªh."™ù¥k/Ùí§ˆPçôÅ«Jí’RÚH–V+ݽ׾ðáýW?pi%ºBi·±ûdùöûå'WwŽ¾þþ÷³½y,·Vbæj®æWžë]yÇìúC-.˅忬î[X%Rï N'W/?oT׺ë7åÜ×[¤ÑõÒÙyTgòÌ-^åÓ‹<ò£¸V¯/ßèlÝת—©ŠÉ•LsG/-ǤSÜÖ»WŒÞI¢qêÅ –4êÛ1§}ÔBDDÕ•ÛL4¯%G¨Z‹pÙâðpñàYp“ /½Sc‰¦Ñ¾’¬lë¥u>Õ™rQ:aTÖÃL~6 A]ðpsa-ÄäÍúå\ï6•ì¦ëÛ4f:#Š1¦’+–¢RƒDe/̘)³ }çÆtò0òx²KÙ£ XñÐv„·i£îˆ+D²n´öµm¥¸Èå"|ŽJu˜T'UYO7·äâò\LŽ‹Yµ8q:©d8«Y&Ý;Å´fL­FÒÖ‘ê.ÄxÊ(kÍ­ÂðjkçQeãþ|Lò ‚`Ôª«×ƹ•¯'ÚùÁqqt\ž#ár…µ Õ¼tA¢¼1· ) F5@¼ÝC•f‚r#®w’õˉÊ~¢º ò8Ì;euñd;,•CbÕÛ Û‡lnxÁO¡r”$¦ÕõÆ"—ƒL&ÄeâJÑlzdJ*¬jÍ}{p³²õhåêùåA.ƒk¥´<–ü¸b!¡‚(u,Ùô3†šmSFcÕ‚\a5ñ$0$kÀWD­Á›¯> SM bÊê3¹U¨pL.¦»½Ã½TòP´˜\…®×«ëriÅ;ÞzÏdR-?•Zó¸VU³‹åÞ®hw/x˜óv.šñEä—6bŒÎ›M³¹ÕÚ¼Uß¼£7¶Æ¥TÙôÒBT)ó9µ¼Y_¶wð•Ýra™)Ÿ å‡jiÕE$/úé)/툈Øàs›Byg‘}¸^]D? +³?•†¨kÕ÷sÝkfó"¡—Ê©Õ]Â而¸qžïEd»´º~òRyùJ”Kƒ|Aì-!Lmù˜¬— *U'vm´‘±:bi+Ý=©o=Л—¸=Õôbut]ŒŠU"µa ‚a¢´Q\º¦×Bdj°~£¾¸÷—•œå kCΪm>ÛÚy>—@ý\ÑDŒÉ#tn>({P}>¢Ø,cö«*ôcã»ÉxÆêÍšœ÷ÑÓŽ¸/"ãœ×Z1µÊQ¡¤–V¸4Ì11Þ_i”Ý8Æe—1ÕSÇ˹S)!;ü߇—Ü.:®Ü"RI˜T-Äç|O÷‚bÞ>«Tôê&mL.‚ô嗮ȕM¡¸*”V´ú“¸,ľ°êˆBµˆÔ€NÑD'À]¤šïîéµM/cGÁ÷©l„/‚#ƒÚ¸ñŠñ|vTÛ¸ëÆ 8°ÒÒiºs0ƒ·‡ZyêÙ§ÈD•I6ÆËñQ6e.R™%‰>Ú óV˜5{QÊôæ‚䌇˜yÞEùé<­TÚŠ ŒNv{µ« \1µÓ¥nŤzD¢±­¬•–…LWÉÁa¼ly! +©“]å‹;u>xyHZ€ØÓÞÖËKnTôÚB<æ+JñŸ]‡i òÐk2X€X<廉‰WÁ’ƒTý(×½j6öÎ9ð•Îw÷¡¶gƒÜBDvÄà’”5ŠªÍˆ\YÐõÁ±]ßò`º;Ud-DÔi?G¨U57œñÈÿ^ÿ92>圛°¦£ã~´âª^\s£ZeõЦÞ:r«ÈxeNyÊ#¤®¤[PónDA„ñÊ?jqs¬ ¸æŒp˜hwÖNÍRe“„^q“6šh+ÅM"ÑŽ²…‹ 茇J•×r½ý‹z. Lºéss”®œ_—r«¾xr!À°ã»½J!Üœóq¤^{ŠZñVˆÍ€vA+=Á:‚±‡tf µV£Z/,Uܘ¤Ó®ˆ‚°ù¸Údsrí2“YvâÊLˆ E›uB)Æ¥2mt€ÀåÒf~éZaù6ªwæ#{KÉêf¦w¸¸{°ÿ QÛBõìàruë®PÞ¸`Àèµòe-ŒH³†¨d•å^<³ ä–“kéÞÀ-Tk¹¢4c|>ÑÉ!Ò Eµ¼S]½[è_cÍEaLúhT®Åà¡VA9!ƒóùM>Ð?•™Ë^< µíˆÊR~=;¼™^Ë-ßI4O||¢œ], ¯‰ù•Ô°ÙGµ“ÁËÇ»ËQ†Yì^%*–¦BÄpDiDIJRÚQ«Gà3“^:Dç8s SÛ˜ÚâìUÈé¼ô™$ÆŽ—M‹)%/nøâFL“‰FD(B> + +ž˜ÜZ=MVV¨ÂZ‹©Ö‘RÚd­üó”Ÿf:|Þ'•òÄ\4ÊÙZe£0ºuéÁ·Z{/ +ÙÑùù¨?Â{ûRv• R¹‘@Ò0 *=é!Ç òHåùˆþÌ\ ŠþŠ°iJÎD)‰TlB-‚ñÁàLyÈÏ]ðýõY÷¬‡Aè ©”Âô´Ñé…”hŠÔÚ¤ÞöcöE£M„1¦]ÈÅYdÊAÆðPÆÆke}1¿°êŠ'ã‰t'´ šh’Æb¢|I«2Ùá<¢ž]ˆ{âÉ“AÄ|D*²ö’T\Ç“­0—vPW  +¥W!WÚãßh^(®ÝórÅI&e^Ë ÉcT‰d DqxcåÆ2«7³ëgs¬Ñ"A<ñ¦·rýkÛ7¾´~íÝüÊù èCdÉîÑÖb˜ÏCíFK.™­+r~+@Û®0ËMÎìáje¼óôòóQ3zöâ5©°OT2½}¹´ê!MP¶“§­Q²uªV!ΤY½¸¸÷<ø{@,Dõ6f¢zKv*+×íö^”Ï(v7³x ®ÍfÖ ˜eµAm»±S^Aø *ôÊÀg¢´Î¦‡A¶ÖvÑMA qZc¦áÇŠ¹µ¸Ô`ŒE`€…¨¦Ö(½9à}¸«lv=Q?0[—£b´4ÌÚ¥á\oúéLD¬ÍF“pF.D… +AùÌ…ù(¥µ\ŸK5”ÂjX,N¹iÎ(Ñh-ŒošÓøô‘h¡bÅOXîxê‚‹¼àÄX£—k“n&@el‘ÇûŽèÌxáwgH²zqJëÿ¯õGŒÏøÝ’§}ì„#fVWë+×CTæé)ž0ëÏ*F(Ûä/8°§&ý AÑÕBlöìx}NÔ“f"RQ[€è|@Ÿ!÷Í„y,Ñ€øP\ñ¹Ï”ÖçãúLXð'";"l”³2íÃÁñë¹åû˜ÑßœÈG)á­˜œc¬®P\ƒ$…}``„KF()„‹Ÿ&&•jA`¤ «xv‚—îEØ”KŒ=½²Öß¹ßÝ}‘+îºÐÂØq©àÅ5?i*… .½ £TöÉÔÐO¹Â§×âBzÒ¸QO4=¨å‹Baz9Ìå0)g·¶¢BÎ×ãjƒLBÉ]Mv¯X¼Ӊ¢ÕÜ¡Rí…˜B¥‡ù•Ûí½Ç¿H®c‰:dœ\mÝjl.ÄtL[´:WwŸ_9~¹¾ý@­nyQU³»ƒ{V}ÓHsaq.$Bm³©.”ÊšõÆe«SX<…nðePgLGåÆøB‹ô2  Õ2½lï„4X²âŠ3Aæ1.KìƒMÄ”JªuJ:8nö”3–€ +™t qÖÄxåR©Êc =ù0#.WåÌ<êD4Oܘö2AÂ"ÔfL¬…ÆkJk`[~ÒžññàT›ƒrð¡0JÐ&j~Y)¬¹âæ¿>çÐr£PÌt@vÄSN,ta7wEkÑ ¯ ++A2ã¢#ìiyÑ9ÞzNÜ7¦¢‰ÏN‡/º1g˜…H™ƒdmtôù¥c¡'|n}6,€4ÑÉ‘¨a©~Ü‘6d½kùáýñ~C¨È¥ª¬ÙÕ +ë‹“ñôE¿$¥…Ö!¤¦„Y#*• Á•—ok•í¹3é‹h2{ŒÕ 1ˆ?©–2ýÜÊ-Ü^ +Š%n†¹b<Ñòt”ˆ¥m80HC%ç‚lLȳéQT¬Œ÷‹RqW*âÉa”ËMºcOM{=q×:sAÙVcB‰Muqµ*¦;‚Ùðĵ¸Z¼Aä +n,*å=£yJZ+0¶³!vÖv,›sඇÌDŲ^ÙD„l±SõÝüðZaqßníbÉ–ƒHKˆVϪ¬³ÉƤwFåùdŠˆ\òqgçÐ '.¤Z™ÖŽ ‘¦ÇËX©˜ÖªŒnUWïzoÊËFÙÌp÷AuéŠ3"B|SÌ®Šù5Æê¯Fð|º§UÖ<”yƉÏÇôW&@÷Ôv„0P*É'Š­õë¬ÝŸG“g\t/¹ ÛCåÅÂÖ|P˜œG½‘®^‚zžöqOO…ÏÎÅ3á~f2¥ó:뎩Tô@,Ê.ÄÀcU—òFm+·x˜ëå—N¤â +Pb„Ë“É€=<¬ÖQ}ó…Âò½Tï pBN"RÆG')³/÷0kƒÍíÛíë…Þ5ÁÎGùmz-Ä¥ƒ´ ,á§,&Õél?Z:~5;×#B ÆÜ~«Æd´9DuÇ /™N”·ª÷½ˆ4®TÃB æ˛РˆXÆÇ7Tf¦|8ÀŸ[Ë/žtö^KÍ 7=åg€^¤tÏ…@ÐV‰dW.vv_in¿€¨õi?$™nl¥›\S`êÇ×®JÕ£°Pª9çˆ÷OI¶&]Äy=s¸–¨"bŇi“nÌ9ïˆ0Þ¸ä'@4Ln`J+&–ü¸"ƒ«ufÀŸB< Wf62¦ %ßÇÕâ¹Ù(.ùµduW,¬ûpË‹È¡¸B)9ZÉøã&dÍÒº^ZÕ«;˜Þõs¥3óøŒ /©2&]hZ˜É1Fµ–Dú‚“üÜÅ`\ÈÙõ“šXˆ?5›‰èHrIlÞ0û׸TÛá'õt+×=€´øWóNj2¤úùf­‹Ù­…ÿÌd€VÊÍÑ3S‘¿:ç'„…Ÿ_óÆuèâeD¸ m4¸TÖK¸bIÍ5^J+)ÛKZe“Nµ"œ Oò@°yn€ªCêD |%›!>Cè-.³B˜ChT†bºÆ_šdª'ä7!PCõB`ĵF\®¨Å•x²=Q&ý\˜ÉHÙ%Jo$ +ëV#JÝÃdg¢²ÆA*øÂT€Ç25Bäfˆ)ÏG½À'F2.f¼„ UO²©!¡õ½;V¢\ÞK¤§BrH¬FÕ6m-C‚sáætˆ›pF¡„P! Âbíh¢ÓºpüRik*Ä?3‡Ìy˜n™²YLïD•&ºÝ:H”·I£"’Ç¢brÙlT»àeQ•1ºàÎ*;±€Ž*À:bzLi1鵩 ¼U¡C”‰‹¹O|¼Z5#ŸÆMDÐÓ>ÞGÚ8æ£ò¤— q%Ú­+À D9;‡Ì{éöÒIª°ä‹É AÞ‰(Lb0 +ára˜ó`ŽáʱDMtýL墟rÆç¼8!ØÀäŸ Oy9>3¾"K.m©ÑŸô™óa`¶8_â’ÝY?+¥ûŒ5BĆÇ.è&„µp¥,g—bœ=^üWáqN IZkðé’_fRm"Q!´‚WÇÛçyi„+ÒàZ-*ØΊ+E\«“f?@fCt×Z|n¥¼v¯ºõH(n9Qž“èÅ’ãP†[n*Ò4^Ñ«°L-Ñ-@%“B¤*\¢2kàY.˜ ±‚þå¢>`Q¨™)éÔ *ÎN¦1L.Ì…¤˜X÷S…é :Nè¦uÜDH Kö£jÕz³¨þÙôs ÈtA…‚UC˜Ê™€:³‚&=ln*Äq( $ṫ§¸ôH­ì+…ÍÆÚsJaJ‚²œQŸòâ=„OGÅš”[/oÆÐ3.ºHgDš Ýð&ª¶¢€îée |GDš˜Í9ñ“†<è&ÌW†§Œ%1»âKg\ð¶”(0etùÌòTP4ÂÙ¼‘"”>ï£æṳ̈›rQ® +(y+?ÈÕ–§Ýñ‹Nlü…m“ÉÅÙrÞI~fÂ厊ò˜QûççÐm© 4i„ÍAu™ +=5áŽÉøx7=}& .„GHZðs`¾¸îD Ê3­=?•š ñð¿çÐv”¯À'”–Y¼ñÂûJºÿÔdäì<dó@& !Æ“ÁvC¤ U u¡Íñ:½RŽÖk¸R$à­¶”[¥RýñW.ˆì'’.DtDyðˆ“"r¡Z˜–ëjÕíTsW«nàz}¼gχ¨mGXŠr€XÈ&¤Ñ…ò1p¦ÖBTñSi£}E,òÅ&;Þ!Äf€¢Ã\L<ˆ§äü—Ù"ô>‘ìÇ”ÆxÁØÙˆ7"Qjy!,ÑÉ>—^Gä6¼3¨(k¯ëŠÏ©.@‹—ÌF”Ž‹,†øo¯éegHŒâZst“Š€LQµRÆ;×sé.ÄLª\pc„Z“þò$[€ÉÎŽsê†ó%µ™¤ *!¶rêD 77í—BL‘·Fx¢qÐ((3{3ÈØ3Aö‚‡ ñe­º'ÖB”µqúRgëÙPõQùXYÀRÂ|Töœ ‚ò´µ„ÈãE,}Q¬,ììl<.”½1 J(€%£lIHõ£‡py(õ ú—­íÙs³ sÈE7㣠p„ +PÄHâÌ ’oí‚ŸžÇ/8H°°…€ØÆëÕ(›ÿëóþÏ] ybº3ªLºès³qø¯#ªãRÖšþ¸~v.:é&&=4PÂ|˜¹RÝQEÏöŠ=ø‹n!¦,$8ãÆBLsáÖª?ã"&\ø\KU);"´z€L.DA]É9?¦t0/­v v"BJa67žš‰œs`Žh +#Àdˆ0 <Ñ&*UÂ|!&•à1‹(.bÚÏB…x I© rTÄä`$™lCB‰ +E!·Ê¤¡ä£,Aè2 +HFÜ„#ÄÅÅ"®Ö Ø|q-BWãTs# hhŠi 'e³`”!HUBJà‰ðW„ÏCjƒQMÔŽØì&”%ŸBXÝÍg ¿Ï†x/iŠ¹u6³ŠÄçõŒ ]@a®A{>ÈAÍG’óp»=å'  ¬œ] sY0пˆë}6³I[+œ ^Y»à!@ÀéfÃâ„—D¤&–èpùÕtoüe§;¦ˆÉêÑí7›+§^"åc‹a¥’ª>z| g œ5B§¤ÜpÒË€Ž·Dè~®BYë|nÐc2ÕΑQ¡ag@ÌCÊ,bH¥KÅáMR«Ÿ™ò…âšÕfü,äM¨· .êÌ<áƲŒ½Ê~÷ÆÀ£30ªaÊÚ É‚9{Y¯îM{˜ù€Ü¢´³;’Üšò³x¢æA}Þ7åa½X +Wê”Ö@…¢+œ87K\tñˆPua¢´CB ¡2ƒ$ TÆ_G %,ÑëGÅ:c.˜‚Õ¤ò”Þ1jBnÃKäœhz1=XfÒ'Ì„"ª—0Blò/>h¼?Ð5pTXØkð`¦>Å‹ÏøÆ€zcr€ÐQµD§S¥¼lã'Œ(k£\Ú(2í=pv›Å µ(myQ  ²QP F‹2I–ðàÈ5(oL™v1ÖRB*…é4 +ÀÆä!è¹0àê,0ÏBTŒ Yè Æì!ã½Ìª@ΈX S7n@Çlj˭az?*5Á#â!£¸Zj ‰EhF?&ŒöTXúWgœ €BzËàSÝ8`<$ýL÷4Û»ê%-gLQò+ÐnT*˜ô13j&?8­m?$Í^ÉõC DG\Ÿ‹Ê³aÄmÂIE™ c4fþ’+Ç›CñHà ¼Ì„#î%”}~. +tñÐÇÃbƒ±G!.´6±ƒæ‚N¹©¸ÒŽJSö“y\í lfb.rqs†•§§B ’“â¶PØ}ï‰âîÓSˆ# 8‚âÄ|ìø‚c|™}˜-$+—R}T®Lz˜b÷˜ÔÚN$yÑ/NFt'”Òk®Ý¾þÒ· Uáif…¸ŠÝŒ+-0Ž¿ìÖ‘ô“i/nÅ% ©Eè:þN•Ëña² µg p"¹L&‡3!¸z>¬SZì5Êç€óƨâ²Ji=®ÁûÛ Ñ¤;¦…H#Dè _þ¸“ ”Õ_ýk¯ƒÆäJˆL!„†q©(gyˆño7jiCÌŒ<¨îŠH¸XD¹Ì|ˆœ˜uÏù1G˜'õ®Ù:Š…ùˆ4å¡:qö‚Ÿž÷3AÒ$ô¶XÜf²«^255ÞdY.uĤ°÷ÒYàÂ\©¤Íh×<"ǵº\Ú䳫!¾ +Mi4®t »9BÌŒÀÕ†ZÞˆå —Eµæù ô™‹WHĥ✚rÆ Ãç‚òk•KœIÈMÓK#HÙ^<€µQDÿWÛ¸ÛºôHW]¥ºtHŒã!‘Z„‰Ò…0•‹0ÙùÍ—ŠD¢7å¡€âÜPHT.@gHâsç=çæ³ ñófÒÏ;ÚÕ#Ì–¤üz°fý<„XH©)gDžñÒO_t?3郲 Y(Qè8¥º[ß}bo­Ë´9šöÉŸ½ò cÐzf*â +Ëà/¬5HÕöi½b àòÐqµÑÿÏÞ›.·•%i‚/0ü16?zªº2±(BÁ{νç.$Eq'E‹H‘ÚEQ (2ÄEA‘Š%;·ÊªJ«žÎ®š¶ênëë1›ém6624ãŸû9wA„eˆ@fD€Ž»œÅw÷ãþçÅÍcÒͪ·ÜzøÍãÓúþŸÿûßÿ§ÿþîÿåÆØŠO†ØÜ“hîéÄJû}Ÿtæ¹õƒ©µý‰ÕF´øôZt÷Jp÷* ÓɇÉmb)„'×Ç–ý¹‡#léLÀE <œÜ]x–,<®ÞZ%Õ7œÞ\ºÿÕ£½ß^]Mž\ïjÍ>]lŒ-í‘:dˆÞ¾Ï< Ž¡Hm›}-îlìýíú«?йù+ÕÑ;ÕqD+¼[k#·6ÂY2§V÷IþþòæØgÕ±dvÓL­{·ïE ÛdBÞyüíZýïiF_zSŸ^ !7ï£ÓÄ£èîöØê«ÉõÃ[+›ãµ/üɉ՗3[G[_ýݽý?&µ]–8Û}ú)œß2s›$ÈÔÔæÔÆÑÆþ?ÞßÿGb•£_¨3½¥o£}!é]zúñøÚ¡™}B¦.qû¿¹b¾¨Þ箪I±Åçñ⶞~@ºÜÌú®šZ›¨=[ÝùÝâ“oÐøòåèÒ#²£½©éµúÊÓ¯o$K¤0Ï?øzjcŸt-o|ýK5ÿÉðøu3GÒ¿Ýùן(olyj¥>q·>|kó3½ð™žûld’ú/®‘fâݺGzÈè⋉•ý±¥:íËçÞÔgÃ`;¿º>v Í”W®“–ÎmÎFç¶g×oŒ®Þ^Û›ÞøŠ$;§¿¾ýÉÈ¢?³=qwûÎÖþ­WŸ Ó2®“ФmýÌ›ýÔ›7ó'Övo¯í/l½!^Dòšt¹ù‡g ›GÓË/Ʀ6®é™‘ɇ„Û¤QÝýÌŸ§ÿ_O–§WµgßÞ}ñon?øÚ_عF&†BnÀp²J +ÃÄÆ›ñµ×c˯h#H.6ªÉM¢‘kÑWÝ#Ù:ó€vófLüa‰6ˆˆ¬ìhî1©Ê#„™14Û­Æ_ýöÿÚúúŸÍì3zÈÈ­53½™,< a¯!×÷ßLl| CÚo\#ƒš¸w8ý0œ}Î<Ÿ\{3½ùÍ£o‚ÙÇW|R!"ðõß.?ûõíµ×Õ RÚŸÞªí‘Á2±ôìoŠ ÿõ—߯Öÿ çžß¿G&ê$1ŸçßÎm¼Ò·îÓSk¯ÿÝþ÷ÿùÖÚîõ[+×Æ–&Vv&V_]]!0ÿtñÉ÷KÛ¿›Úx3<~ïW7’/õ”¾½NL¼[D¶£Ë_…‹;¤ìMÝ}~UÏDÓ›dì?;úÃjã7ÉòWj’ƹà/ßZzæÝZ®Ž‘9³0sïÕÖÁß?xý§_ýIÏ<"E—ô¢/5Ðé“›·¾Ttͽɵº¾}ïª?óÉõ±+ÕIBì`rão¾?ž$Iq{yïÞîonÜZûUõö§d&w½ñ¥kHé™ýÜ›ùÅÕ[#ck!Ð&©ë#Éj4ýhl ™ˆ!šÚͱûÄýî™ýÕÕÑ`låÖƒxþ¡ž}4zïpóðÏ;çÿqåù¯'k»DJÓk»ÿŽä&1Ì+Ä +HJ®No¾!Yðe¼D˜Nm­>>Yþv}ç{ZÛ/‚ezòµh…„ׯF&®Ž.鹧3Ïk/~=óè Y4!Íw‚Ä©ñk´tôÀ˜òò~0÷‚¾ªg¿4ó#k8¿ðøíüÓoŸ~3Q{I¶<1ZgÚ…‰å½Û«d›OÔ^LÜ%©úðÆØÒ­ÕÝõÆo¶ÏþýÞïÿÏÛ›'Ÿù ¤N\ÕSÓKÇæ 'KWÍÉš¹§ Φ׾úlxüJõ܃ILƒ±»ÏªkÄÓf7ßL­“º¾‹<‡™‡£µ™o3¯õ첶n­Ý^}5¶ðxlñáÄÝ' Oîíýzï»ÿXÿî_î¼øµšyvs|ct‘ôœWáìV´°3yï=·Žý…§×ÆIÙžò'VçÖ_-"_÷8¸³£IAZªÏo½™ºw@òtìîÓ‡_ý~’x•þ5^Iîì’qª&î{c+ŸÝÕµÚ££Å‡'3›oüéÇ7“{×#BÎÍÙuÚå×h…3µ9¿y4¹þ:\Üßüt„xÚ )?“ χã%´û™ØX"ÌßùõüÖ ¶W£"Û­—ßÏ®ìþOs“jöþÁâó禮ÎÇ–^Þ$Ìürôj°HJàÄÝ—ÕÑåáѵÉÕד+_ÅsÛs÷?"6ò…šõ'6Hë ^w-ª©Ùg$\ôø½p|Íí072ɵ{?,Ü=ºð<žyLmÜ}øúéW¿‰—ˆ£^[™zÝ­__ÿ<¸sc|}x|}lá)9þär-h?Yx¥o?¹1ºN¯¨x“þÜ#³ð\Í<¸9yÏ,½¸ûâwÏþ÷ÚÞßG‹ÛDPÄÒ«‹[_ßoüiróÜ,¼¼qëþ5ä®lêÉ{h1·Nò”–‘t¹ñ»/ˆÉVú2\$3?^xv÷_“x=©=ÜÿÝý½o&Öž ‚9âcßNnœS÷Fç\æ¿43“HX¼¢ç¿ô¯›»×‚Åà6Âf~sâîÃgoþazë5)„fî)qâ«×ãÚÔú«±ZýæøIp¤~m¼ž{tvk¹1<¾Lf£¾½2¹²=sïÙDí1){£Ë{ÿ×…'ßÓhÍôÃáüÄê®™xóÖjõö½¹GÇõózþæ¹³upk~+š\'õ~ãÕÂ;;DP7Ç6ÈÌ™X=Ø|õ·w_3ó7‚E5~Ÿ“¨5º»{}|ƒˆå3o2™¸pÿUubu¬¶/½º½vôô蟈™DsOÌÔƒ/ÔÜ(©Á¤5M?Âñ±Û[Éâ^²´?¹öšä¯¯„_è™±;Û¤½îßñçž­ìþæNýoWë$µä×ÆquŒÄ_ñâ³áÛ›éÃwwgïMhŸÌ=%æù«ã$hHª’éw“plã )<`VckjtõFˆ¢úöýhîÙjáóêì˜0dk|ñ9ÎnÌn .ÿòú±Yâ·“Ë»£‹ÏüÉ›ÑÝ•G_뙇ŸF ×&־ıî-³XO‹Þ’²—Ì?'LX|v5\ùddázH‰7 OÞúóIóL56vQŠ|aûûቭ+fiøÖ½* +¬3h„·ÖÞÜÞ8¿µúšž£&V“9â¥Ož½8û—»/¾_{5<¶ú¹?G¡¦É$_¼-‘º²ôì·ë?Í<8AiˆYÒOÒ¯î<»9±ö™ž¼1¶:u‡ªÂ¹­ýeü«·ˆ}b·î#Ô8yÿúÄF¸ø|æÁ›péůª3úöÖõhùz¸L?f¶èöhñŧßûóÛ5{•ˆzóhþÑ;²g7^M­?Yž®=[Úz=VÛ_Ùý"Z®xdyÝ%VCjùõdñŠ™]xðúÙÉ÷¾ûç;OO¿çI‹¸×fV+Oß"Ž9¹5¿y²ôø9ꇟz“ÿªróºY½³MTsóöý[ë_ßÙþÛ{ûèÀ•,<½™Ô~qmôf|‡„”ºñÝOnÞþåµq²¸É`¹½²‹tÁ±µÕgçœÒ0uÅ,“˜ø¥7G[C(4»ñzfm^=º´}sâÞÍÛ>‹î}¬„ó»kõ?Ôž|=2¶<2Z[xtòâôϯû,?ÿŽ¶©2½>öÉ =±1³v0uï+²þ>™ ܸ³ùš”ùOoNŽß}õäí˜}t6±üU²°ƒ5Y}yÿùÑÈèÜ_]ñƒ…—ã÷¾^|öûÍý?O¬}E³þ›/ã;÷^}ûÏÉâ£ÏõÜ¿ú"ùÌ_¼}å9ôõFDxþþW$ÓI7 æ—ÕioúY²¸»¹÷ÛoÿÅ›ØüëOÌU½@è-½ ´ÿÅc_„µ›·6â%Z½³xîñÕ¨¶²ó›¯~÷ß¼þw³Ï?Vÿ¿KòyÑíüTŸÁDzí3˜H¯}éµÏ`"½öL¤×>ƒ‰ôÚg0‘^û &ÒkŸÁDzí3˜H¯}éµÏ`"½öL¤×>ƒ‰ôÚg0‘^û &ÒkŸÁDzí3˜H¯}éµÏ`"½öL¤×>ƒ‰ôÚg0‘^û &ÒkŸÁDzí3˜H¯}éµÏ`"½öL¤×>ƒ‰ôÚg0‘^û &ÒkŸÁDzí3˜H¯}éµÏ`"½öL¤×>ƒ‰ôÚg0‘^û &ÒkŸÁDzí3˜H¯}éµÏ`"½öL¤×>ƒ‰ôÚg0‘^û &ÒkŸÁDzí3˜H¯}éµÏ‹ÿ÷’|V*—äó?\¹2w¼;»s¶3ôpqèÊÔ³M¯ï¼=kœ­ ]Ù™:=›=¨Ÿœïœ~W%WÕAed½±sX¹*WVè¢ÊêéÁ«ƒcnÔw×*7éÒ1úgÈ«¨ŠÇÿ{øÝÐpQÕ׊cMÿÑ•(Žüª1žo|ÏxÚ¯á"WýX‡:/Ë⢘`A’Äq„a@|W’¨(ñ¢H.*½®­‹J¯Ûz0tÛVCßžíÎ6Þì`@¨ØÙ›.|r/Ïß#½~˨KÂÂÎùÛ·;ÇÓïc/*Øm£Ýnû|~·Œ¾ý‰´Ïæw»Áç;¡•^gj'{{og琉þ¥þUžaÿÑý!tedÖOONG¿Ù¯‰“•@߶ïm´W_8îµpòöüto§Þ°)¨íÎ*wÓ€Iüsyû¦Q_= õŸç´mó??Ü999~{¶sÜþÔÊ7v!üÙé,ç¾}srÜø€Y6oì'K…ìOú´»Hß·½*ßwÖ6L¥Çݪ“méÐ1Ð%Udíäàø¬Ö‰Ëìãø*–bkVÇè?íèr†:´½®7|¨ã¸CoïQ¿™KNéžÐ¶´xýWDf?péÅça´=‘÷¨°Ù‰¨^Ö°_·Oõ¯»AôíOÄo"~&²szp¶Ô8k?ªÝOòóR†í;V +z]‚œ­í¼ÏÀˆÐöv W¤oçžäŽ ¯Kû¹Ü8}ÕÀJöŸJÔ)©]â-ùxã$ ~hNê²%ýŒŸ ›¶÷®Ç}©PVϧXÍœœNŸ6ß· ½ŒùUªÚöÉèÓ݃óöiÎ]~á{»{p¸Ó~ˆ»ÿÌÒK• ×öDú%ÜþÎ춿3m2•£šžœ¾Ù?9ƒÒËm°^d{lG“Jûçiû±@JÛ–yßð¶gôø@¯Pß^G™D{‡‡$¯va[Ž;mç`“Á^_>i?6sCïò—½Ó“£ö·‰/¾øSmóÀæ¼ÒׯððÛœ`á®.8mÛ&°zýüèüý™Ùéen¹x ÿ=ºx&„€pí‡äê ŸÏiƒmȶ·kw÷àìà]›•Þpñi*‡ßì|×öv‘T>Û9íHŠËõ½18;iß^>éÆDÚÞœÝöë¸Éµ]²LvŽŽ:`eéŒJ—¿Ži3=fçÕ/]ÚLû3ê—È mæ‡D[w\Á`XSÿåïYäÍô]ÞLûeîú-q¦}lìÙÔω3mk£ý‘8syÄRdÒó‰3õK—8ÓþŒ‰3ƒÄ™AâLG»õ3HœQ?—Ä™e¯ äK—8ÓþŒúEïëÄ™¶£ÇIåR'ÎÔ/]âLû3ê>ð‡ˆú'ý§ƒ­ì”‡ti+û¸Xa iƒÍèߪý³oÝÃ¥)9ug{–ëêlwæ_ºdI¬í» Å’0ž®—‚ûØÊ»Åb? <Ï€¿µÅߟ3k{òþ6àoþÖWümî”õíR±·ötÀÝÜmÀݘ» ”·wp·w»lÜ-6Úî,8~ɘ\Û“ÿI"¸}%QDýœ‰¨íɈh@DÙƒÃ^Å´(Ó^ï 0sÇ…«d?£n[Ä +×¾m®î|·ÝÙyÌKÆOG'ï«©Ð_5iŽw{Çïm‰šÍ{ÓØ9›íà æŽ ŸßÛ7¨ºÓîÜú©æN›ìrP£æǧ7¨Q3¨Qó3®Q£ÚÇÁ¨írÑ“©¨1íU”¡{úgŒ¾ÓÇè‡ÊG?Ò³¥ãB6½¢J]Â΋k%_t +píãûå¸@'sú™¸psä2†o:³grŠêùƒý'GoNÞ’¼zþÆõ°¹.±†7Çþã m;_¿'Þ“Ù\zñ&_Ûy£-;Õ ]»í‰´Oú¯»AùíOä=Ù`Ù‰øÝ0QOÎögí³å~’¡ ûC–vhõ“$ýøå(/(íŸðÇmmߤ! N!âσøóL´'âÏÕ\ÄŸñç®ÆŸ/oÏ—Aü¹<ÃAüyÄŸ;ò¾\¦ø3«›ˆ@ko¬#Õssî]Ko»1çAÌys¾˜ÀîÁÞÞùÛÆÌÉ1IËãöq­tß…#ÝwÃÓoÚçáÁ«ý3ú}¸Ž"¤mO³x[ïFr„ø7:ê»çâ5‹¶Ùüùé©ÀÍ-So³Ä—Ç–Ö/›ÔêpZ—¸-ñÀ7pÁõŠ ®A=ðà üp?Ü_<µKë‡û¨=~»b:m4ŽÇH»lŒ‘Œ;xu2öîàä°q6vÚØ;9Ý9~_œ}à »h,ŒÛv™î|pt~öž¾‰Yžá®¿pl ÛžSãþèÈA—¹£kÞŸÙvkÔ Œt9wfVÌ”šÕ‹úˆ3]z/ÍÀ—Ñ'¾Œ·ouR‰O/âHÄ…‡ÛFB»{}Ë7ö2©ÙÁÎ}û†ì¬˜eóÆ3gàÌ8sÎœ3gàÌ8sÎœŸ@¢Áu#ÎëÙaŸÎÀ™ÓÛ¶ìÀ™ó—8sº¡}\¶¼¤òLmX;©]S—³4Çø8zÝms)O_¤/ªKÜjPž£ûå9Ú/Ñãå9Úß‘/ÏÑþDå9.}‡’Ïzìy)zxp¶¶sð>÷þ@„Dè ÂÕ ÂÕ@„ö4sîuéùA¦u¿HÐAe«KUÙªãmTµú€qtw —¦ªÕÏ«ôÓÆþÎîÉ7?ï¾C—°pAÛu¼… zKAèïÃþm·Ý}WÍæE|Ûk"m·ÕåK{šVz©ìí½mœ2N»±ë~ã«<ÓŸ•ðá›{‰…^ÙœËÝ!}`Ä]:#NùÞ•vñt¿ÑISzùÅ;šƒöçôÍÁn öê‹è?S{X™¶7¯}ݸªq'3i_9î†nÜtü¬Ë^_B÷G8pô§áÓßîø²¸?ÚŸÈÀýÑr¿qûcàþè%¾>p Ü}åþøÖ0âÎv:HZ»Œ&ÜÞéNýlçpåä ý|j¹¹Í=voºàyÕ?^ÆÐEçíVÛ®órçmcþ´ñõyã¸Þ¾z]¸ëâ·ªmJ¿T1:>Ù8;8«¿Çšõ%àêû‡œ~ÏÝsñ嶳ÉÏV‰Ã¼ë`jÙ[zÅ´õ—õ—zIþížµˆ|ñÅû•Ú—уjR?>½nV“T_ê»êK^µí¦\g'íÛ7']˜ÊG­#ÕoRÇÕ”>Š'éþùéËóCâÌ}èj¼œu`:PÇ{ÜŽ¿˜ò/]rÀé´öGíëK˜…ÖÁœYh=æ,oëz;­ýyô|ZSYÙ~O}€ËÂ8kSi믆Wõ.¹ßû6ðü÷›ç¿í¯¿Øñß%Uv`öš Ø £X+p`¾QVàÀ +X—Ë +lÛ^¸ŒVàeMdóªmW”˜…© ÌÀ6ö‘r`ÌÀ80fà‡›LæšÌi`ö˜8¬.‹!ØÉLzÞ|pr²ûêt§}éíÀKÙð¢mm{P•¢7 ÀŽªôI×¾þ®°1(0úcTØø •¿‹¯°Ñ•^6žö“Õ ¹ho÷!ézÒ‘~ôåáNýõXE@'ovêgßvà6~{öÝaûnp{õÅ'‚c®—¨:šT¿ÐÔ<±ÿHêr:M;ðK+e{¿Ñå× ÞrÅÌ™ËÈòú¸¿aÛuRlÓÍ™“cnÞÞ¾mQ¼ïÂï›ýŽýÚFðÃmpÎÌ,‹·]¼C½3v±Qßé@éËÝsñ§]ÛÖdÏO÷vêÎæ–¿i`ÿsé°/o¿®NÛ _Þº0Êo›w¾?8:ï l—^áÛ¶]˨qHtäZÏÜqáób‘ÔÍjFEÕš=`5¢ÖI˜û#U ˜¾P³¢¿ÿt¿Ÿ…b4ÐúD{ø€¾Äý¢A´­œ»5èØÊ*ßØ…“ÎrîÛ7'ǘeóÆö4О>žö4Pž>šò´aI¹µ§Ë0ø)ÜëŠÅÅ$[÷þ>õ[3>öª·-_¿'LŸÙ\Ú»šëë÷\™ˆêÂDÚß‘ö)ÿu7¿ý‰¼§*Pv"~&²szp¶Ôè C?ÉÑK™‡Ü©'¼ç¥èáÁÙÚÎÁû,ЈЋ˜H‹Ðöu¡íOd B»(B;eν.=?È´HÐË#AûÇU=h*ÙÚÑGç‹ý³%oý‡—µÒ%ì2ékû¶[=§ÿBù—¿´Óööë’ éçÚNG;ô¨¶k‹ôWÏþ¯Õ·Òî”ù[ûé.ïÙëRmn}ûþì¹~¶ON—Û À^d—PºÔeQ.ô(`—ª¿KŠ _šš"Ãm›#½^TäB\V<§Õ‹?Ü%~Яå8.g*`ÇúÜå%¤Þà N™Ý¿ˆrÚ— ÓÛÓëä¤ÚïÌÞ +xvËÚ>FÙo$Õßø¥“PÀ¹‡{ÓùƒÓ^ð¢ôÊ>Ÿí¼lûÁç©+m;Ùyî[95s÷t¯Øùq}½ÿ˜Ê¥C¶jTñ*?t[ [÷ÑMý\˜Ût—UØÂ9èû§;Ço÷ÚïvÑ;Ø9='¢ÆõºjúZúÁo"Së{à9ÉogãMöÀÖôÊ’|ÎÒ@?`ÝÇ´°º2uGyÛsÇ»i++€ Û+'Çkô®Ç4,àéÆ«ƒãìC+oøü´ñÝÑ˓á« Ó—$O¯ y•)úçá7CçCÍ<‡ßÑKôå+}S *Ë•'ϼÊ.®\ÖºšÄ*¨h¿ê%*® á[&)¤–B†é‹ +â€@é}­`Í;y(«C^ÕÓ§¢0I¼PÇ:¨xÕ( +#$òÃXy>ABúª”è IL 1üj¢• ãPGñ’˜~¤Mx* ½(p›Ÿ¹Àó“Êáaeª‘¢ïÃa•ÀQDóÖ^U›(® GÕPzØ°VU––«Tb*øªÐ£Ê°©IäÑ•TÃ(¦/A5òbO$®ãÅ€xq'•zcXUÚT†ýjš˜ž©üªï ¡á9ª¥ðß¾‹–L…xïû’ +ªO#Áëãȯ”g23´×ÜU¥EBl›W lˆ¢Ñú±_QQ52ø3JLRÙ¢F½)®ª$ѸÉo>&æe¥¡úq@ûUMÐ+ …&ÄS|Ïp…¡™™„o*L¡fò"‰ ˆ†wDŠVûhˆ$¡%ˆ–¾hì4ˆ€ð¦G˜–Ð/žÐK¢4V€4ô¼Q?À°‰<í1-hÚEH¬eµù6Ÿ(‡060>¿<‰tÈ¡r]M`•Ï­¹Æ§•¤7%]K"ÙÒøq•†B{Hdx‘ö9è! I"¥|Ÿ›^ækå Kñˆ¶‰8<šžGk†‡«„É’¶E@uZÚ5†Ð4–""„fŽ¡ í+Ý£Aš–‡COÕ €xrteá)‘Gø³…I¾1­€ÿ„˜ÀÜJ“rè‚ÿ%Aà\Š<ß +½0"4»”ñBOùqäiÚuAB§ endstream endobj 5 0 obj <> endobj 17 0 obj [/View/Design] endobj 18 0 obj <>>> endobj 37 0 obj [36 0 R] endobj 65 0 obj <> endobj xref 0 66 0000000004 65535 f +0000000016 00000 n +0000000159 00000 n +0000018089 00000 n +0000000014 00000 f +0000620935 00000 n +0000000000 00000 f +0000018140 00000 n +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000621005 00000 n +0000621036 00000 n +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000021317 00000 n +0000621121 00000 n +0000018562 00000 n +0000025359 00000 n +0000021617 00000 n +0000021652 00000 n +0000021795 00000 n +0000021504 00000 n +0000020429 00000 n +0000020756 00000 n +0000020804 00000 n +0000021388 00000 n +0000021419 00000 n +0000022711 00000 n +0000021971 00000 n +0000022006 00000 n +0000022593 00000 n +0000025433 00000 n +0000025785 00000 n +0000027323 00000 n +0000031853 00000 n +0000096231 00000 n +0000161819 00000 n +0000227407 00000 n +0000292995 00000 n +0000358583 00000 n +0000424171 00000 n +0000489759 00000 n +0000555347 00000 n +0000621146 00000 n +trailer <<35E1595A434A4A59B366C2FE27BEC316>]>> startxref 621351 %%EOF \ No newline at end of file diff --git a/presto-docs/src/main/resources/logo/print/light/FB_Presto_Logo_PRINT_LightBG.ai b/presto-docs/src/main/resources/logo/print/light/FB_Presto_Logo_PRINT_LightBG.ai new file mode 100644 index 00000000..1a077204 --- /dev/null +++ b/presto-docs/src/main/resources/logo/print/light/FB_Presto_Logo_PRINT_LightBG.ai @@ -0,0 +1,2534 @@ +%PDF-1.5 %âãÏÓ +1 0 obj <>/OCGs[5 0 R 35 0 R]>>/Pages 3 0 R/Type/Catalog>> endobj 2 0 obj <>stream + + + + + application/pdf + + + FB_Presto_Logo_PRINT_LightBG + + + 2013-10-25T11:03:57-07:00 + 2013-10-25T11:03:57-07:00 + 2013-10-25T10:50:56-07:00 + Adobe Illustrator CC (Macintosh) + + + + 256 + 164 + JPEG + /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgApAEAAwER AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A6V+TP5M675E12+1HUb61 uorq1+rolv6nIN6ivU81UUouZup1IyAABx8OEwNlHeYPyv8AMV95m1nVNNm0/T5NSumnj1cCRr9Y JtIi0t7cgIo4RsjXKgyMrOE+FacswnISqf8AI/zCY44Y9UtnhsbGbStPUq0ZNpLFfJGruFeVPQ+u xIqLIUdE+ME8SqqK1r8mtavvy/j8rR3tubqLU5L5tVevrzLJBJGJpi0UoNxG0wAIFOKLxMe3FVEa h+XXnh7BrWzm00Sw6hqV5b3U8lywkj1WK5if1IhHyR4vrIYfvX5UIJHXFU78neRNV0E62nrQQSah O8ttqdusDzcXuJJgHQ2sbbLIF/eTzf5PEbYqkXmz8pPMmueZfM2qLrEYs9YtrOCxtZByaFbV4ZGi VmSRYkkeFy9FcPz+JPh+JVE2X5W+aYbjzG0/mKORNb0WPS7eSG3kt2tZYldIzEizMiRIr9EoxJ+0 CORVZP5E8szaBZ38UmnaZpQu7r147LR1lECj0o4+TeoEXmxjqeEaD5mrFVk2KuxV2KuxV2KuxV2K uxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KsE8gef8AUvMmpXFpd28MKQw+qrRc6k81Wh5M fHNnrtDHDEEE83E0+oMzRSXzH+dF7o/mvU/Lo0n1pYNU06w0645NwkhuIbaa9lkIHwm3F5Go8S6e +axy0ri/PfVH0y0vfS01ZJ7W/vPqckoWSZ7EWRWyt/TlnJuJ/rzCMMoYlf7sYqiLz8+J7a21VXtL ZdSsl1Ca0smkYPNb2FrqE3rAdSnq6csbMNgWp1piqP0385bzUbTzVdRaV9WTy5ov6Q4zsQXvYo5j dW56DhBNCYSw/aVu2KofT/zzjk0a6vdQhtLG5t4bG4S2knHJ4Z76W2uZlFfijiih9XknJaH7TLRi qmPlb82JNe86HRYktv0c0E1zaXMTq73ESXN1DE8VZVZg8dsr1SNh8XUbYqmPmj8zotEvdHtkshI+ qxXc/oXc6WNwiWkZcnhOBGAWAU+o6faBHLeiqWfl9+aut+b2s54tIgSwmtrq6upIppXZUguJLaAx B4ovUFy8TFK0oFavaqqa/lx55vfM7Xf1mfS5vTt7S6EOmyyyS2xu1djbXYdeKyx8KfaBO9UXuqzb FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYqgrG90i4kZbKWGSQCrCIrWl e9MsnCY+oFqx5oSNRIKx9S0ISXHqT24e3uEtbrkVqlxJHHKkb16O0bxsAe3HK21L7DzJ5EuUS40/ UNPmQGVklgeJqGOMSSkFelI6MT/LQ9MVWWvnXyDefpD6vqtjMNMglm1Iq6EQ26fFK0v8qDq1cVR0 Ot+Wbm3eeG8tZrd4o5JJEZGUxXMjxxkkdpJFdR/lA4qoQeYPJtzq8elw3lnLqrpKsdqpRpeEEktv KoHWiSQSIw8VI7YqmUh0qC6t1kMEV3KGS1DcFkYIpZljr8R4rUkDoMVS+38x+TNTvLWG31TTr28u EZ7KOOeCWWRBy5NEAzMyjg1ePgfDFVXQ/MfljV5LiHRNQtL17LhHcpayJIYwa8AwQmi7Nx7daYqm aRRR8uCKnNiz8QBVj1Jp1OKrsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsV diqSaD5Wt9HuZJ453lMicCGAFNwe3yzKz6o5BRDhaXRDDIkG0p1P8uY9SudbW51B/wBF67dRX1zZ rGFdZora3tPgm5V4tFajbj1Na5iuakcP5H2clxLcaprl7qDyxPFxd5ERC0LwBo1Ejen8DglEpHUb Ko+EKsjm8gWcvlnXNBN3IIdb0/8AR0s3FeUafUxZ81HQniOW/fFUmtfykex0ybTtP1ya3t7m2WGV jCksizR3dzfRyxs5NAlxeE8XDVUAFianFUb5a/LQaJq8Wptqkl3O0MyXxZDF601xd3d7LKoidVSs uoPReLUAFD3xVV8xflvZaxPpTNODbaWk6C3vUa+M3rin76aeQyuqblVZitaGlVUqqh/In5U6d5Uh t1a/n1OWGO6BluK1M99N6tzMtWZlZwFQfESFHUkmqqZ+TfJ935cQwy6q+o2sNtbWGnQvbwQmG2tA yxhnjXnI5DfESQu2yjeqrJsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdi rsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdir sVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirs VdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsV dirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVd irsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdi rsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdir sVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirs VdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsV dirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVd irsVdirsVdirsVdirsVSnVPNnlzStTstL1G/ittQ1FlSxtnJ5Ss7hFC7d2NMnHHIgkDYMTMA0U2y DJLrvzDo1nqcGl3Nysd/chTBAQxLB2KruARuVPfLo4JyiZAekMDkiDRO6Y5SzSq58z6TbakunSyM JyVUsBVFZugJ+nL46aZjxDk4s9ZjjPgJ3TXKHKdirsVdirsVdirsVdirsVdirsVdirsVdirsVdir sVdirsVdirsVdirsVdirsVdirsVSLWvI3lbW9Z07WdUsvrGpaS6yafP6syemyOJFPFHVGo6g/EDl kcsoggHYsJQBNldq3nbytpGs2Oi6lfpb6pqRRbK2ZXJkMr+klCqlRV9tzjHFKQJA2CTMA0VW/wDK 2jX2sW2sXMTNf2gUQSB2AARi6/CDQ7scnDUzjAwB9JYyxRMhI8wgdU/MHy5putpo9zJJ9ZJVZJFW sUZehUOxIPQjoDluPQ5Jw4xyYS1EYy4Sirzylpd1qo1GUv6lVZ4wRwYrQCu1e3jkIaqUYcIacmhh LJxlbc+cNJt9U/R78+QYI8wA4Kx7Heu3fbDHSTMeJE+0McZ8BTzMVznYq7FXYq7FXYq7FXYq7FXY q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FUg1ryF5T1rXdP17U7H6xqulmNrG59WZPT MUnqp8COqNR9/iU5ZHNKIIB2LCWME2Ug80fnb5I8teaU8t6k9x9c/d/WJ441aCAygMvqsWVvssG+ FW2OW49LOUeIMJZoxNFONV/Lzy7qeuLrNysv1jkjSRKwEUhQADmCpPQDoRlmPX5IQ4ByRPTxlLiK G1P8zvL+na8dImWUmNxHPcqF9NHNOtTyIWvxbZLH2dknDjFe5jLVREuFMrrybpVzqZv3aQMzCR4g RwZh9Fd+++VR1cxHhap9n45T4zahceedNg1NrNonMaP6clwCKBgaHbwBycdFIx4mE+0oRnw1t3sk zCdi7FXYq7FXYq7FXYq7FXYq07oiF3YKi7lmNAPpOKuVlZQykMrCqsNwQe4xVvFXYq7FVsksUSF5 XVEHVmIA+84quBBAINQdwRirsVWPPCjqjyKrvsikgE/IHrjSr8VeQ/8AORHnzzV5T0/Rj5fvfqT3 0lwly4jikYrGqcQPUV+P2z0zN0WGMyeIOPqMhiBT0vytd3F55Y0i7uXMlxcWVvLNIaAs7xKzNtQb k5i5BUiPNuibATPIMljzQxsqyOqM5ogYgFj4CvXGlX4q7FXYq7FWC+ZfyY8j+Y/NEfmTUoZmvlMZ nijkCwz+kAqeqvEsaKoX4WGwzIhqpxjwhqlhjI2Uq80f85A+UfLvm5vLt1b3Ev1d1jvb6ML6cTOA fsk8n41HKn0VyePRylHiDGWoANMj1P8ALTy5qOufpef1Q7sHnt1YCKRh3YEFt+9Dk8faGSEOAUiW mjKXEgdS/NrR7HXZdMe1leGCUwT3YIAVlPF6J1IU++W4+zJyhxXz6MJauIlVJzP5I0qbU2vWZwjv 6j24pxLE1O/WhPbKI62YjwtUuzcZnxfYhJfzBtE1AwfVmNqrcDPy+LY05BKdPpyY0BMbvdql2rET qvT3sszAdq7FXYq7FXYq7FXYq8V/5ynnnh8n6S0MjRsdQAJQlTT0JPDM/QD1H3ONqvpDHvK/5Mea vP2gadrXm3zLKlpJbxfoywiX1OECoFjY1KRoSoBNFJPUmuWZNVHHIiIYRwmYslItWsfNX5G+cNPl tNRkv9AvyZGhoUSeNGAljeMllEiBwVceI9xlkTHURNiiGBBxS8nvn5lXSS/ljr91bSVSTTJpYZFN Kq0RZSD8s12AfvAPNy8h9J9z5z8gab+cfnDy3+hfL941r5et55FubxpvQUyuFZkkkXlM4CsDxUU3 37ZtM0sUJXL6nDxichQ5PZLCC8/J78pdQkv7qPVL23leW14qyoZrkqqIankwDkux2NK5gkjPkFbO QP3cHn3kT8pdZ/NDT283ecdduil3JItnGnFmKoxVmHKqRpzBUIqdsycuoGI8MQ1QxGYuRQ2tWfmj 8jPNWnT2Goy6l5Y1EsXtJKqrqhX1Y2SpRZArgpIKfKlRhiY6iJsVIIkDiPk9M/Prz1qGhfl9b3Wh XBim1maOCK9jNHSCSJpWeNuoZgoAPUVqN8xNJiEp0ejfnmRHbqwDyh/zjhF5k8s2Wv6rr8y3uqwr dBURZQqyjknN3bk7Upy6eHvmTk1vDIxA5NMNPxCyXqn5UflrqPkWHU7W51h9Vtrp4jaBg6LEsYfl SNmkVSxffid6DMTUZxkranIxYzDq+e/zf/Ke58iW2mSza4+ri/eVArwmH0/SCGu8s1a8/bNlptR4 l7VTh5sXB1tnfln/AJxvvLvTtJ1Y+bZkjuIbe7Nqtq3wq6rJ6Yf6x2rSvH6Mx8mtAJHD+Pk2x02w Nsz/AD9/My98n6Bb2WkyCPWdWLrFP1MMEYHqSL/lEsFWvue2UaPAJys8g258nCNubCtD/wCcaNR1 vTY9W8069PHrF6gleIJ6zR8hUCWSRqu2/wAQFKdK98vnrhE1EbNUdMSLJ3QnlXzB5t/Kj8x7fybr 1+1/5dvWjWB3LFFjnbhFcRByTGFcESIDT7XXY4ckI5occRUkRkccuE8n0nmrc12KuxVA3OtafbX8 NhM5W5nCmJApIPIlRuBTqMtjhlKJkOQaJ6iEZiB+osL8x/kV5D8weaf8R36XAuZGR7q1jkVbedkF KyKUZ9wBXiwr9+WQ1c4x4QmWCJNsf8yf85J6BonnGfQW0ua4srKc219qSyBSkiNxk4Q8TzCNt9sZ bDQylHithLUgSqmX3v5X+WtR1g6wzzcLh/XltlYenIzHkTUjkAx3O/3ZKHaWSMODbbqstLEytK77 85LS21qS0WwaSxhlMUlzzo54mjOqcengK/dl8OySYXfqa5awCVVsyBvImlTX31sSv9XdvU+rilDX enL+XMUa2YjVbtJ7MgZ8V7dzJ8wnZuxV2KuxV2KuxV2KvEv+crP+UO0j/toj/kxJmf2f9R9zi6r6 Q9D/ACq/8lt5a/7Z1v8A8mxmNqP7w+9uxfSHmX/OWCKdB0ByByF1MA3cAxio/AZldn8y0arkGW6k 7P8A847h3PJ28swsxPcmzUnKY/3/APnfpbD/AHfwST/nFb/yXuof9tab/qGt8s7Q+se79bHS/T8V X/nKSSZfy6tFSvF9UgWWn8voztv/ALIDBoPr+C6r6fiwzyJ+Yn5w6X5R0yw0Xyb9e0uCKlrd+hct 6ilixbkjhTUk9MyMuHEZEmW7XDJMDYJb+Y95+cvnyys7TUfJs9qllK0sbW9vcVJZeNDzLZLAMWM2 JMchnPmHreu/lxP5y/KLQ9CvWNhrFpZWU0TSqR6d1FbBGSVeoB5MrU6de1MwoZ+DKSNxZciWPigB 1eW6fff85CflrZiyOntqGiWlfTQxi8gRK1JWSEiZF9mIA8MzCMGU3dFoByQ9z1L8p/zq0zz20mnz 236O1yCP1Wt+XOKWMEBniYgHYndT08TvTD1GlOPfmG/FmE9urC/+cs/+Of5b/wCM11/xGLL+zuZa 9XyD2XyV/wAoboP/AGzrT/kwmYWX6j73Jh9IeBf85GVm/Nny3bS/Fbm0tfgPT47yVX+8KM2Oi/uz +Ojiaj6w+ls1Tmvm3/nK9Ej1ry7cIeM5t51LA0ICSIU/Fjm07P5FwtVzD6Otnke2ieUcZWRS60pR iKkUOawuaFTAqAvdcsLO+t7GYsJ7kqIgFqPibiKn55bDDKUTIcg4+TUwhIRPMq02mWE91Hdywq9z FQRynqvEkin0nIjJICgdmcsMJSEiNwoXOvafbalFp0pb6zNx4ALUfEaDf6MnHBIxMhyDCeqhGYge ZYJrv/OP3kTWvNT+Ybr6yr3Evr3djHIot5pK1YtVS45n7XFvlTLIaycY8KZaeJNsS1v/AJyftdM8 1z6Zb6L6+i2U7W0tyJeMzCNuDyRpThQUPFSdx3XtfHQExu92uWpo1WzPX/KvyzqOpDV455fql0Rc /VV48G5/HsacgrV6ZKPaeSMOGhY6qdJEm2dgAAACgGwAzWOW7FXYq7FXYq7FXYq7FXiX/OVn/KHa R/20R/yYkzP7P+o+5xdV9IVfyu/PH8u7XyZpWk6pftp19pttHbypPFIyuYxx5I8autD70Ptgz6WZ mSBdpxZ48IBYF+bvnKL80fN+ieW/KatdW0DtHDcFGQSyz8eb8WAYRxIlakD9o9MydNi8KJlJpzT4 yAHuX5hWEOnflJrWnw19Gz0iS3ir14xQ8F/AZr8JvKD5uVkFQI8mG/8AOK3/AJL3UP8AtrTf9Q1v l/aH1j3fra9L9PxZb+dHlK780fl9qGn2KerqEJS7s4h1d4TUoP8AKZCyj3ynS5BCYJ5NmaHFGnnH 5I/nR5W0fytD5Y8zXB0y7015Et5pI3MckbyF+LFA3B0ZiDyAFKb1rmVqtLKUuKO9tODMAKLOdR/5 yC/KyzZUTVWvJGYLwtoZCBU0qXcRx0/2WY8dHkPRtOogOrI/PXn3Q/JWlwanrAla2uLhbVRAodwz qzcuJZfhAQ1yrFhOQ0Gc8giLKQx/n5+U723r/pwLtUxtb3IcHw4+n+rLPyeTuYePDveTfkyo8w/n hqnmTSLRrbRYmurhgRxCrc8kjVqGgZy3LiPA+GZmp9OERPNx8O+QkcmR/wDOWFlcPoWg3iqTBBcz RSsBsGlRSlfn6Ryvs87kM9UNgyXyZ+df5bR+V9DsptWEV+lra2sloYZy4mWNYytQhX7Q+1WmU5dL k4ia2bIZo0N2O/8AOT3k3ULyx07zVp8bSNpQaG+4AllhYh45dv2Ufly/1vnlugygExPVhqYE7hkH lb/nIj8vtQ0SCfWb/wDRepqgF5ayRSuPUA+Jo2jRwysRUd/bK8mimDsLDOOoiRu8w1i+k/OX83bC HTIJD5d04RpLI68aWscheaVxvxMpPBR8vfMuI8DEb+otBPiT25PqPNQ5yX3+vafY3kFncFhNcU9M Baj4m4ip+eXQwSlEkcg4+XVQhIRPMq9xplhcXMVzNCsk8NDFIeq8TyFPpyEckgKB2LOWGEiJEbhR utcsLXUYNPlLC5uApjAWo+JioqfmMlHDKUTIcgwnqYRmIHmVWbS9Pmu0u5YFe5jpwlPUcTUZEZZA UDszlhgZcRG6lPrlhBqcWmyFvrMwBQBarvWm/wBGSjhkY8XQMJamEZiB+ovOdY/5xy8jap5ol1yW W6iiuZTcXWnRsgheRm5NQ8eaqxrVQfkRl8dbMRpTp4k29URERFRFCooAVQKAAdABmG5DeKuxV2Ku xV2KuxV2KuxV4l/zlZ/yh2kf9tEf8mJMz+z/AKj7nF1X0hOfJ35W/l/5k/L/AMt3WsaLBPdNp9uX uELwSOfTH23gaNm/2RyGXUTjMgHqzhijKIsM18r+QfJ3lbmdB0qGykkHGSYcnlZevEyyF5KbdK5j 5M0p/UWyOOMeQTfUNPs9RsZ7G9iWe0uUaKeF/sujChU/PIAkGwyItCeX/LWg+XrN7LRLKOwtZJDM 8MQIUyMqqW3J34oBkpzMjZNojEDkmeQZMV8xflX+X3mK5e61fRIJ7qTeS4j5wSOfF3haNmPuTl0N ROOwLXLFE8wh9J/J38sdKkWS08vWpdd1a4D3VDWtR9YaXfDLU5DzKjDEdGS6tomj6xZmy1WygvrQ mvoXEayKCOhAYGhHiMqjMxNgszEHmw+T8h/ymkn9ZvL8YetaLPcqn/ALKE/DL/zeTva/Ah3Mv0bQ tG0SyWx0iyhsbRTUQwIEUsdixp1Y03J3yiUzI2TbYIgclTVNJ0zVrGWw1O1jvLKYUlt5lDo1Nxse 4PQ4IyINhSAebD7P8jvyqtLoXMOgReqrc19SW4lUEGuySSMn4ZedVkPVrGCHczllVlKsAysKMp3B B7HMdtYRqH5I/lXf3RuZ/L0Cyk8iIHmt0qf+K4XjT8MyBqsg6tRwwPRk2g+W9B8v2X1LRbCGwtq8 mSFQvJunJ2+0592JOVTnKRsm2cYgckyyDJDz6dY3EyTzwJJNHT05GUErQ1FD88nHJICgWuWKMjZF kIjINihLp9jNcpcywI9xHQRysAWWhqKH5nJjJICgdmuWKJPERuFfINiHk0+xkuVungRrlKBJiAWF OlD9OTGSQFXs1nFEy4iN0RkGx2KuxV2KuxV2KuxV2KuxV2KvO/zs/wCVffoCx/xt9Z/R/wBa/wBG +qcuXrem3Xj241zJ0vHxHg5tObhr1Mq8lfob/CWkfoT1P0R9Vi+oerX1PR4jhyr3plWW+I3zbIVQ rknWVsnYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX//2Q== + + + + uuid:a2f90594-125d-6947-9bb5-b14fcebc329e + xmp.did:5117f072-579b-4f03-afac-e7e8629aefd0 + uuid:5D20892493BFDB11914A8590D31508C8 + proof:pdf + + uuid:0dd95273-5bdf-bc4b-ad5a-8acc8eb336aa + xmp.did:a2c08580-f82a-4993-b0de-979ce9677d23 + uuid:5D20892493BFDB11914A8590D31508C8 + proof:pdf + + + + + saved + xmp.iid:a478fe57-ac17-43dd-a456-3029de78c422 + 2013-10-23T20:23:29-07:00 + Adobe Illustrator CC (Macintosh) + / + + + saved + xmp.iid:5117f072-579b-4f03-afac-e7e8629aefd0 + 2013-10-25T10:50:54-07:00 + Adobe Illustrator CC (Macintosh) + / + + + + Print + Document + False + False + 1 + + 6.955882 + 4.317257 + Inches + + + + PANTONE 2718 C + PANTONE 654 C + PANTONE 297 C + + + + + + Default Swatch Group + 0 + + + + PANTONE 654 C + SPOT + 100.000000 + LAB + 23.529411 + 0 + -37 + + + PANTONE 2718 C + SPOT + 100.000000 + LAB + 56.078430 + 3 + -48 + + + PANTONE 297 C + SPOT + 100.000000 + LAB + 74.901962 + -20 + -27 + + + + + + + Adobe PDF library 10.01 + + + + + + + + + + + + + + + + + + + + + + + + + endstream endobj 3 0 obj <> endobj 7 0 obj <>/Resources<>/ExtGState<>/Properties<>>>/Thumb 43 0 R/TrimBox[0.0 0.0 500.824 310.843]/Type/Page>> endobj 37 0 obj <>stream +H‰ÔWËŽ7 ¼÷WèFI=¯Ù9AC>`ÇÁÀñÿ)’ROÏîìØ^Žƒf»Ô”D‘¬ûÍOáÍÛ‡¾ûþ!lï·„RìYBIøÏ9œÖÀß¿m¿†¿¶7¿¤pþ(„gÀÿø°½Ç@ÂJk¡ˆGLJ8?núêq£8Z 'Š¥S(‘¤cyØpè±Yà¼Q…yaê‘ðãà²qL#«é|I‘k ˜ØÊÜb’Fî œ·¥•92åÀêÅ€s,‘ªÀèÏmÄžàK+=bV;¤ØN€]†Î:¬Ò1 ð ãÔï6¼­™žÅŠCSLðë‚SÌîX­jOQr’"alO„cÆ?¤‚[_è¼Í-&挌±%õi ¨ª­cl&Àœb#^Ǻ:òyû}ûù˜Ð¢>ÔfT5ê+¡§3W]³ÃsÌ‚eqÆÒØ™5©9VÝDbmÕ¼|c¤Ä"Ù°ÄŒ2¹`Ž ±Kqd -""¥Á¡†æ%¥6C6ÂPo–×'§ªxS<ʘÜÊ´ÅÖ±‡¦Â ÇÌØQ,1”*/Qëˆç(Kä’Lð!4„¿ÙæV.Z9›Á,¢ç,¤U™‹†®¡"4ÛYêBX3£`ÊþÑnˆJ>g#€“ µÖ2H˜Íšð¢ÎØj±º¬ZÊs5]õˆÈ‘.A‘ç}`U5MØn/rQÏ;ñˆŒ¼œ.TME#3xórcbT% X°ÙB ô,Y…3VïžåÖ.J{-gMý¥2%Ôí‰A14²šÌ¤RRgIb+ÄÒsƒr‚S:D®Í¸jLÚ88o9Žù¬¤í˜ÑçVdVç'D‘ ¯“‹”oòiY-ÕÅ¡§AÂÜG§±æoA„ˤ‘t Z—³2™òšŸ@ݬµ µj«ˆ²º™+ÝLÀ´ •Xð ›KŽùTÑ“(L»œWœ'F¦zÑYI+ÓdL%݃tæ$Ö¼ª¦"ƒ8m³§fë k¢tQ1L; dšœ €å#¡$Ó$ד)¸nTIŲ·ÏO¶3²QΓKÌÉkQMˆÜiUâ »µ¾(+ãžÁÚì1™XT»L¥Xu²ÝÐð»îÍ)O2ÒÛy*Ý3$¼Ï@œËjÁŽrØrÙ?pѾ`$E¯8 "K}7aÍðÂÁYehÍù;)©aÿ°kAfñÞîñg„mš/\nT*–ïGÂvð†°]#;V%}"ð(19Yl#l[§L–ÒþtÞô íX§ñ>m¢}Õ‰}OŸ˜.Z=«ûÑôBzÞGNÖ¨¨7€¼x»F¼½Xó]:Ò.'6,^š²Ð¾äð-çÌåÐÅYçº5:tk—LHE&O£¨t^Ó¢kÓ + +¹®é‚j”jj˜Œ„*˜*l>ÃÁœà–þk%££j!aîà í6¾…ú³ÛÝ¢µ´jW+z?ÔÚ•ó*-ë_±"^H“l1•\í㦭ù¯É–ŽªÅ¾ª›§ÝÆGÝPŸy®{ÓïÞµ›Æz¹rå:CÆzXÿÐÅÈÐw´¶<×»•íÞšÀMÝÆÍñª®íá}7Nf¢â£fçËùï-·s‚ŠVAq–K¾rZ²÷|Þw†ÐOjmšÛú{qšO¤æËÌ­“Ð5¦Å\¢´[ù°Í÷Gßòâ;*ŸoW>$W9R5´”ëÊ/®ú—J6Ykæ¨X×dgi¦9ÓØÁ´u#ÿµ¢×Q·˜‹·)sËÆGÕ°ßÉGnî2|» 82&Rž¶_‘¶ß%­I+wHÛ¤í_FZˆÞ‰?zt¹Í[¾â- +oû¿Ç[¾í:ô&7m5A€>pëßá-ûÄíâò¸Å\/S·©kMÈ}îâãm´ö¢gmWQ?°W®Ø+‹–÷ÉK÷Ø[>½4‰{ƒ¾é6}ñÂ’ŠÑ7§Ú®jHªU½ýC+k­š£<%Ù\Ö w ¶eù¯õ¶º‚Z¬eÜz2iÙù+_BŸÛ\ûf2ÐôŽ’Ø(\{-ߊúÔƒú¼@gm){‡~ÜÜSŸë–¡¾V}ÚÇÕ§Ô§ÝS¨§èW úd|¦6º­>ùJ}² ?U~•úÈMõéWŸŠ^;a.ŒËësÔçúg½C~.Aõ•ÔïIÔ³ëg* ÇžPŸ#AÏþjôBól-DÓtTeý¸KâöÅ$n¯!ñ -„éÁDrdÿM÷cõø*÷iÊ,Sì…_PO¹ +¼|ßkÆ\§úõkF^Ûv¢mKàtîF½~qÔ_ug½Ô1£Ñì¹þ¯Êý‡·ÿþ`€ÈÚã endstream endobj 43 0 obj <>stream +8;Z\q5n3u?#Xu3B7TXmWAqPm/K`=+3kd6aOUMW\Kog]&iiWB4i[^LW.$.')2CXk_D +MJC=V5^=.0OGH84p%o(SI#YJ,ki?au1?@kqFA1pCr*,WFc'(R6B?`ei6mBfgac4Um +1qG'VA?7VU8c=K0Z,bZj'W^A>rPgd>E)QEn5/$EQo?Oh9 +B$$+&['d=7a*J-oBGNN8~> endstream endobj 44 0 obj [/Indexed/DeviceRGB 255 45 0 R] endobj 45 0 obj <>stream +8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0 +b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup` +E1r!/,*0[*9.aFIR2&b-C#soRZ7Dl%MLY\.?d>Mn +6%Q2oYfNRF$$+ON<+]RUJmC0InDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j$XKrcYp0n+Xl_nU*O( +l[$6Nn+Z_Nq0]s7hs]`XX1nZ8&94a\~> endstream endobj 35 0 obj <> endobj 46 0 obj [/View/Design] endobj 47 0 obj <>>> endobj 42 0 obj <> endobj 39 0 obj [/Separation/PANTONE#20654#20C 48 0 R<>] endobj 40 0 obj [/Separation/PANTONE#20297#20C 48 0 R<>] endobj 41 0 obj [/Separation/PANTONE#202718#20C 48 0 R<>] endobj 48 0 obj [/ICCBased 49 0 R] endobj 49 0 obj <>stream +H‰b``ßâèâäÊ$ÀÀP\˜ì“˜¤ÂìçؘÀ 1¹¸À1 ÀÄÎËÏKeÀß®10‚èË: ³0åñÖä‚¢ }ˆRR‹“ô N*/)Š3FÙ"ŽFN@v›‡“‘#œ]’ZÒËàœ_PY”™žQ¢`hii©à˜’Ÿ”ª\Y\’š[¬à™—œ_T_”X’šT µ8@~u15 ÑÉ„ADd”„…›Ü´C‹™™‰"†°ñ™˜YXÙØ98¹¸yxùø…„EDÅÄ%$¥¤edåä•”UTÕÔ54µ´utõô ŒMLÍÌ-,­¬mlíìœ]\ÝÜ=<½¼}|ýüƒ‚CBÃÂn‹Ž‰‹OHLJNIMKÏÈÌÊÎÉÍË/(,*.)-+¯¨¬ª®©­«ohljnimkïèìêîéíëŸ0qÒä)S§MŸ1sÖì9sçÍ_°pÑâ%K—-_±rÕê5k×­ß°qÓæ-[·mß±s×î={÷í?pðÐá#G?qòÔé3gÏ¿pñÒå+W¯]¿qóÖí;wïÝððÑã'OŸ=ñòÕë7oß½ÿðñÓç/_¿}ÿñó×ï?ÿýõÿ÷?üB†ÿÿÿƒYÿ!x¤‡Ë¨ÿG´ÿ í,<ì endstream endobj 50 0 obj [/Lab<>] endobj 38 0 obj <> endobj 51 0 obj <> endobj 52 0 obj <>stream +%!PS-Adobe-3.0 %%Creator: Adobe Illustrator(R) 17.0 %%AI8_CreatorVersion: 17.0.0 %%For: (Brent Couchman) () %%Title: (FB_Presto_Logo_PRINT_LightBG.ai) %%CreationDate: 10/25/13 11:03 AM %%Canvassize: 16383 %%BoundingBox: -1 -505 404 -248 %%HiResBoundingBox: -0.000003315938557 -504.737810490416 403.320695133076 -248.93212890625 %%DocumentProcessColors: Cyan Magenta Yellow Black %AI5_FileFormat 13.0 %AI12_BuildNumber: 256 %AI3_ColorUsage: Color %AI7_ImageSettings: 0 %%DocumentCustomColors: (PANTONE 2718 C) %%+ (PANTONE 297 C) %%+ (PANTONE 654 C) %%RGBCustomColor: 0.356772124767303 0.530214905738831 0.853949785232544 (PANTONE 2718 C) %%+ 0.432647913694382 0.769827246665955 0.912153005599976 (PANTONE 297 C) %%+ 0 0.226927161216736 0.43734136223793 (PANTONE 654 C) %%RGBProcessColor: 0 0 0 ([Registration]) %AI3_Cropmarks: 0 -612 500.823529411764 -301.157476056923 %AI3_TemplateBox: 396.5 -306.5 396.5 -306.5 %AI3_TileBox: -127.588235294118 -744.578738028462 606.411764705881 -168.578738028462 %AI3_DocumentPreview: None %AI5_ArtSize: 14400 14400 %AI5_RulerUnits: 0 %AI9_ColorModel: 1 %AI5_ArtFlags: 0 0 0 1 0 0 1 0 0 %AI5_TargetResolution: 800 %AI5_NumLayers: 1 %AI9_OpenToView: -64.2675463759806 -225.919869956014 2.6145 1625 1075 18 0 0 46 64 0 0 0 1 1 0 1 1 0 1 %AI5_OpenViewLayers: 7 %%PageOrigin:90 -702 %AI7_GridSettings: 72 8 72 8 1 0 0.800000011920929 0.800000011920929 0.800000011920929 0.899999976158142 0.899999976158142 0.899999976158142 %AI9_Flatten: 1 %AI12_CMSettings: 00.MS %%EndComments endstream endobj 53 0 obj <>stream +%%BoundingBox: -1 -505 404 -248 %%HiResBoundingBox: -0.000003315938557 -504.737810490416 403.320695133076 -248.93212890625 %AI7_Thumbnail: 128 84 8 %%BeginData: 5328 Hex Bytes %0000330000660000990000CC0033000033330033660033990033CC0033FF %0066000066330066660066990066CC0066FF009900009933009966009999 %0099CC0099FF00CC0000CC3300CC6600CC9900CCCC00CCFF00FF3300FF66 %00FF9900FFCC3300003300333300663300993300CC3300FF333300333333 %3333663333993333CC3333FF3366003366333366663366993366CC3366FF %3399003399333399663399993399CC3399FF33CC0033CC3333CC6633CC99 %33CCCC33CCFF33FF0033FF3333FF6633FF9933FFCC33FFFF660000660033 %6600666600996600CC6600FF6633006633336633666633996633CC6633FF %6666006666336666666666996666CC6666FF669900669933669966669999 %6699CC6699FF66CC0066CC3366CC6666CC9966CCCC66CCFF66FF0066FF33 %66FF6666FF9966FFCC66FFFF9900009900339900669900999900CC9900FF %9933009933339933669933999933CC9933FF996600996633996666996699 %9966CC9966FF9999009999339999669999999999CC9999FF99CC0099CC33 %99CC6699CC9999CCCC99CCFF99FF0099FF3399FF6699FF9999FFCC99FFFF %CC0000CC0033CC0066CC0099CC00CCCC00FFCC3300CC3333CC3366CC3399 %CC33CCCC33FFCC6600CC6633CC6666CC6699CC66CCCC66FFCC9900CC9933 %CC9966CC9999CC99CCCC99FFCCCC00CCCC33CCCC66CCCC99CCCCCCCCCCFF %CCFF00CCFF33CCFF66CCFF99CCFFCCCCFFFFFF0033FF0066FF0099FF00CC %FF3300FF3333FF3366FF3399FF33CCFF33FFFF6600FF6633FF6666FF6699 %FF66CCFF66FFFF9900FF9933FF9966FF9999FF99CCFF99FFFFCC00FFCC33 %FFCC66FFCC99FFCCCCFFCCFFFFFF33FFFF66FFFF99FFFFCC110000001100 %000011111111220000002200000022222222440000004400000044444444 %550000005500000055555555770000007700000077777777880000008800 %000088888888AA000000AA000000AAAAAAAABB000000BB000000BBBBBBBB %DD000000DD000000DDDDDDDDEE000000EE000000EEEEEEEE0000000000FF %00FF0000FFFFFF0000FF00FFFFFF00FFFFFF %524C452F06A8FFFF52FD047D525252A8A8277DA8A8A87D52FFFFFF527D52 %A852FF7D7D7DFD5EFF01067EFFA87D7D52277D525227FF7D2752A8525252 %7DFFA8A827525252277D7D7DA8FD5EFF847DFFFFFF7DA8A8FF7DFFA8A8A8 %FFA8A87DA87DA87DFFFFFF7DFFA8FFA8FFFF7DA8FDFCFFFD62FFA8A9A9FD %05FFA8FFA8FFA8FFA8FFFFFFA8FFA8FFA8FFA8A8A8FFFFFFA8FFA8FFA8FF %A8A8FD5AFF5B30A9FFFF277D27FD047D527DA8277D52FF7D527DFF7DA852 %7DFFFFFF7D7D527D7DA87D7DFD5AFF305484FFA8522752277D527DFD0452 %2727A87D27527DFD0452FF7DA8277DF87DA8FF527DA8FD59FFAFA9FFFFFF %A8FFFD05A8FFA8A8A8FFA8FFA8FFFD06A8FFFFFFFD04A8FFFFA87DFDFCFF %FD5EFFA8AFA8FFA8A8A8FF7DA8A8FF7D7DA8A8A8FFA8FFA8A87DFFFFFFA8 %FFA8A87DFF7D7DA8FD5DFF615AAFFFA87DFF527D527D527D52FF52527DFF %A87D527DFFFFA87D7D52527DA87D7DFD5EFF6061A8FFA8527D7D5252527D %7DA8FF7D277D7D7D527D7DFFA8FF527D277DA8FF527DA8FD5EFFAFFFFFFF %A8FFFFFFA8FD05FFA8FFFFA8A8FD06FFA8A8FD05FFA8FDFCFFFDFCFFFDFC %FFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFC %FFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFDFCFFFD1CFFA8FD6EFFA87EA8FD %05FFA985A9FD05FF8461A9FD6DFFA8007EFD05FF8508A9FD04FFAF613685 %FD6EFFA8A8FD05FFA9A9A9FD05FF8585A9FD70FFA8FFFFFFA8FD05FFA9FD %07FFA9FD6EFFA87EA8FD05FF7E5A84FD05FF616185FD6DFF7E0059FD04FF %A85A305AFD04FF84615A85FD6DFFA884A8FD05FF7E5BA9FD05FF6085A9FD %72FFA8FD07FFA8FFFFFFA8FFFFAF84FD6EFF532859FD05FF5A5B7EFD04FF %AF606185FD6CFFAF060028FD04FFA830305AFD04FF84615A61FD4EFFA853 %A9FD1CFF7D2F7DFD05FF5A5A7EFD05FF606185FD4EFF28017DFD1DFFAFFF %FFFF7EFFFFFFA9FFFFAF84FFFFFFA9FFA9AF84FD4BFF5306A9FD20FF2F06 %53FD04FFAF545A5AFD04FFAF606160FD1EFFA87EA8FF7E5953A8FD05FFA8 %7DFFFFA97DFD04FFA9595959A8FD06FF7E7E537E7DAFFFFF7E84280653A8 %7EA8FD04FFA87E537D7EFD13FF7E060606A8FFFFFF7E54305AA9FFFFFF60 %616061FD1EFF5A0159280606060153FD04FF53067D7E06067EFFFF7EFD04 %06015AFD04FF53060006010606FFFF0600290606000628FFFFFF7E280006 %010628FD13FF2F2859FD04FFAF545B7EFD04FFAF5A6185FD1EFF53060028 %59A859280059FFFFFF590106000628A8FF7E00287DA97E28007DFFFF7D06 %067E7E845353FFFF535906062F59537EFFFF7E0601597E84280628FD13FF %A8FFFFA87DFFFFFFA8FFFFA97EFFFFAF84FD1FFF7E0653FD04FF7E2806FF %FFFF5906287EA8FFFFFF2806A8FFFFFFA82806FFFF7E017EFD09FF5306A8 %FD05FF53067EFD04FF28067EFD0DFFAFAFAFFD04FFA906062FFD04FFA930 %5B5AFD22FF530653FD05FF28067EFFFF59007EFD04FF840628FD05FF2806 %7EFF530653FD09FF28067EFD04FFA8012FAFFD04FF7E0053FD0DFF8536AF %FD04FF59060606A8FFFFFF5A54305AA9FD21FF7E007EFD05FF530084FFFF %53067EFD04FF840153FD05FF590184FF7E012F7DFD08FF2F01A8FD04FF7E %062FFD05FFA82806FD0DFFAFAFAFFD04FFAF28062FFD04FFA9305A5AFD22 %FF530653FD05FF2F0159FFFF59007EFD04FF590606532F532F53060659FF %A9530006062F59FD05FF28067DFD04FF7E0053FD05FFAF0606A8FD0BFFAF %FD09FF7EFFFF8453A9FFA984FD23FF7E017EFD05FF59017EFFFF53067EFD %04FF7E062F0628062906290684FFFFFF7E2F2901062FFD04FF5306A8FD04 %FF7E0653FD05FFA92906FD09FFAFAFAFFD05FFAFAFAFFD04FF84060628FD %26FF530653FD05FF2F0659FFFF59007EFD04FF7D0628FFA8A9A8AFA8A9A8 %FD05FFA87E060653FFFFFF28067DFD04FF7E0053FD05FFA90628A8FD08FF %AF3685FD05FF8536AFFD04FF2E06060684FD25FF7E017EFD05FF2F06A8FF %FF59067DFD04FFA9062FFD0FFF84012FFFFFFF5306A8FD04FF842828FD05 %FF7E062EFD09FFAFAFAFFD05FFA9AFAFFD04FF8401062EFD26FF530653FD %04FFA80128A8FFFF59007EFD04FFA82806A8FD0EFF840628FFFFFF28067E %FD05FF06067EFD04FF53007DFD19FF8459FD27FF7E062F7EFFFFA9280653 %FFFFFF59067DFD05FF530628AFFFFFA8A87EFFFFA959A9A8FFA853015AFF %FFFF53065AFFFFA8FFFF7E012F84FFAF7E0629A8FD05FFAFAFAFFD05FFAF %AFAFFD05FFAFAFFD2BFF53FD040628060606FD04FF59007EFD06FF2E0600 %2F06060028FFFF5301010606060028A8FFFFFF7D060128012FFFFFFF5300 %06062800067EFD06FFAF3685FD05FF8536AFFD05FF8536AFFD2AFF7E005A %8453285359FD05FF7E59A8FD07FF7E53282F537E84FFFFFF7E5A2F2F2F7E %FD06FF7E5328537DFD04FFA8532F2E7EA8FD07FFAFAFA9FD05FFAFAFAFFD %05FFAFAFAFFD2AFF530153FD7DFF7E017EFD7DFF530153FD7DFF7E015AFD %7DFFA8A8A8FD5EFFFF %%EndData endstream endobj 54 0 obj <>stream +F„¦c—2^è)?ŽG—Å1†WZLîQ’ˆCÛ xŠ* €¼€ž-± +=&!z #ZÖË´¨,²’¬¾L° ŒYÐ℆eAb–‚ S@hbÚHè³8„¬dB Ûb #AˆcYe¢3†°ÁÓë|ÏDòºÂл€¢4¾„‰‹†X¼Ežb €µ/cZ‘ž °xàõ–ÒÆOâ(D õˆÄ|Dã¬a†Ë ÂãX2¶Æ·”Jr.`H¤ìÓ A´¬ŒC-Â[+JL€²l)IEÕbEëC-×½¼7…¤ûˆCp9 w "0ØÓàÄÌ™\ çtœ0²ƒ£Í`éŒ[:ZÎó,¹òÚ‘0ˆ¸yb2(,yW¨†&!4ùCè…'Ö¬™›vÆ3 _Œ¶A}–u´´Ì[X³HäM 48‘b­yñU±G'¢Måñ€M¢zá,¡‚ûƾô‚WÆž˜¢Øb c‹à>»‡HÒùpöWâÑÅ£4©íÜ#¶ôN›³»€—ÑQ”§¡3À´V­gVš}i} +kXgjb œ&™½¥BÿXao"m˜ˆß¤˜·±‹ßDŒJ´otmd`åÓø ç5ãŽç‡lY�_l1&m‰V4Ž^Ÿpë@Üܯd&²ÐüJu…v —ðø b#ÄC¬ +qì˜<‘d%ý­ãˆe¦,œ”.ÃŽHxÀ  Â‘ðåýÂè¡ž±’¥\È÷QLà‹‰#Íú¾ÇÊ |Úž/*]îöÖ4N ü§ÄóL¨XÉ SD{²ÅV›HRÍE´ùa +"Zô§nÎ3-Öá._š¸¯™¹ÁS}4Dv]̘ȃȪSÌ3Å„¢ŸÙ’ßäu& +Á­(”ž¤p!E`1ýíѾáwßcÛŸŒVßÇ‚k‘µ.Á:àÝ¢g*¶š9X‚EH¶ŠÒp ‹£ŒˆŽUÄÌ‚5;‚ÄŠðL‹ª‡¿Yîfyñb0Á“h¯àËTbò+˜Íð{“àŽˆ^±*‘mÂŽº@Ö‰yýõ„RàáõšW~Â.@Ú3(ˆ´9ÄÙã—hØ +ì{x'‰4/fòÊ«8àÑÁÅ\qÃÄá:fGJq]r ³§£D âŽLÆ5p_¢cöQhüì¢ÜbW'¯•‚¯<À +°N È«Ù7fT¯`nì$%ÃîT­y ð¹8Š®XN%‡?ÜÉ… @‹°A>²PG¬ÁW| m  +ðøôK¤OªPˆXLheÏæßâ}Bˆú™3ñY,lY²¢IGMhKq12ͳ%II†o"ž{¥u“Õ$ØD‹× úˆfíV‰r‰ûÚˆöË{È&aå‘]âÌa+a/Á³ÀXi‹‰ZñJ­Eû ˜=óÌã54¸²L€iq(Ä©Þ •"Y¥PW9–X7°Ç,åNÄrÁã¡Ò’·ˆíì •¢G +;ÄAd#`°äÑ (%þ<’OûÓBç‘ò¬#‚¶Ãgs·[¤ð|8è³ xú‚ìÙˆxDFûþ†p—¢K@„Rª£*͵>TZÒŠ•Vµ>Ô"YŽVæâ™õ!‡DÐX‰a&ªÅû“R¾BA~1¢Èóx½À>4ûy7T•bGåøóBô +Zßg™Dtå“T#)E\&`¾ÖKSŽ‰iÃO²u S8,Ë2/+§øIŽÏ‚­„à'¤¯±@hŒWù²˜…-ÜÂ1¡á +œÔõ±=OY‡§ˆ|ÄOés¬IôâìÎ$Uˆb”äÑ»JX93TÂÝZküέܻ¡rô®ß+ÆyãÅœ'”õÁë4˜·„(æÎÛ(f'6‘RqR褘c~PÞÃ{¥ sKͶ1öpv…´ì3Ã4K{Õ D;±—q +xÙZbLi¬µVAÃRh±Ö2ü˜Q¾ƒÖc÷Àgo: sqrÑ{UÈ2³C*mø´”¯À1ÿbVC9󡪟iÎÏÇüßÁqé±K‡Ó‰^Û°/ uÍd’¤á ¨Öu™H¼y™PòBëˆCdWÁÙ¸5Géű°àÀªp¸\~ò9®£dMh=x'¯âl¹ÂW¨,£S»´!Ô¢_XVÃìí6œ_J0íÛ”6-:”òŒÄ…HÁ¡b@˜B9SÆJá *ögqp`;°iˆ»'ÓD »]<Ô¦ÒEFIaiÎœR\™òê•Ö¸Ž ´8Ǹa,ƒ´ájv°^ÇWü‚† B‰q Ÿ„ãæJl{VÇTÕî±/ê C”}U,ñŸ$MZQ@®DR2˜â¥]éŠmÌ66ù"õcn ¿ö­#›–ƒ<Ã,6i>8åÆÀ +>g¬P JfkqYˆ@/絤¤ãrÂf ³d8›EU?­¾r¶„CFÆyû kå­µX÷òÞ7°ÎñÛ@[ü!¶,y/H1²œÚæLsNCŒ$ÙÁ7À« cC°£˜pón¨EZN9u§”ÃX;ÅÎòÖØE‹!PøI¥ýìNòE)»å¬W|sœ;ã{¬}HJ@ÄLÞ"YñJ\ʈ$eh–ìü±R•TQtZcY \,¡k§gZ ~i5Y8$œñ Vĺ(Lð0½ØˆRSJiªµH|*¥Fӧ굫â‰>•Æ±Ùz5FÒµDÕCÄù3gwV|)7ÿÝP‹þRŠ1ݨ>TÎI*§-µHnÚ*m0ãDÊRzP}¨E.U‹|«bVÖLËÜ­b2Ë»¡–9/¥¼˜bòŒ¬J!!¨œ4Ô*µ(¶ª ÁtŒÐ;A4C"ÑP¸.S«1lz€¬Ù!‚°“Ôp\†]N‰Ð¬ËìˆØíÊÉ¿ðwÉš‹‹ÎKyB BìBZ«@qš= +Œ*k Ýcõ ð%ïàho `oU$P†Àt$ˆM¬òd+¬ž(GÂû“Ḓ$…¸#L·Žï{á79HaƒË8P”úPËTµb:[)­I„Y!÷©œUÈ¡ƃ`uñb!wØT0«åΞÁÊfø§Fþ.½¹KlXÁ»XÇËS!-§aߌ cƒÇžC4컋¬Þ¨S»Þô„}D J‰Ó3/]K9d³ é*I,) [Ó´âb1g Ñú„F܈à«ïö'²‘%èÈž ,;yਖP2û< p¦I•ÒfññY7<6dcHnbÄóe|xs(tÑ!ã>'¹P·dIsF¢áVh³¡Îs–±…ý^˜ßÒÒ¦CòQ=âÏH+c…üTùÛfO¶a•#F’xHrž!b ÓêDÆå3ïJ©y…ô=¿ é‘Ô»ì‹ò;mÅ›„܃X“Ž8y0@†µaÖš°Ì&|‘Ù¬ÒuùŒ¯¸"+»‹4Eü Zž/á¤:›+¢L ©òˆ7Ó9 ³U5¦‹KýƒPœú'á‘b. °›l²`)—°ù&¬¦WÊŸ+äØÕ9ÇÎ؛ؔT9Þ +ÝÝ‹lÒs$™zœ>5ÃÂGƒDª˜ý~ÉDHJ` dK´³<$5¶É5¤6ñq;èO¬xº° +ëB#F2B€ †’æŒQ£;¶^«|¿R&\!§ìÝP1묔•VÌÇb³ ê7':ÉɹÐ:P ™Ð¢c,RXVÑc?ø‰u3öðDBðP¸9:Ï"¬ºb2´úkhÏ@±ƒ'à³ÐH<]L +–¬ôpÅr=äôà8£tyƒÙò* AK +x$dÀ³°nov9+Ái{K!‘Rñ +É–¥tÌÂvGΑšÍn¦Èr³#>¹Ã¦SÔôþÄÏG¬ž—¬Õîå÷·„E$aÌ2lOÇÖ°bTƒ«'µŠ9…Ì y‡¥ÌÄBò¢ìŸpXˆƒTXp IX6Ǔ愲ʼ 9ä'Â#VÓ6 ¸ûÿH5…®¥7±RœTƒLeV¤8êŒŒÓ ^q8¦ ßDe€8oén¬÷„E’o9<šžh$phœR'ç›ÙÖå¿Ie3qùíì†+Œ°4‡â<ÅÇ>q>BÍpˆAëª(«$X%GMb p‰9àL®2¥äGöªò#K”-“,-óqÖ½x A\<åâ2]¼Áƒ,5VîIñg±† t lK±ª¼ò·âCóHc`ÒJl Ç=” t‚çØDpiÈU¬3·Hâ,åy–2A‹Ù¢õ¦óôH„ÿ´M^ÃcES¤_$tâ±pñ„f"ÎÎ …+ Ë0FR'®‡@Ä6[Wà±=Z)­UW˜`)Ç•ðÎÀƒî˜±H"³è¼Øw’å¼ð¤¿ÉrøâÄNăˆ™ñj$¢€•fZjµÅõÊ/)b½â#àìmT[°¡sÄÆŒ/z$FŒ³’ Ÿ ª’y…X*ȹ ûn¨œÀ›Ëïm{±êN9;6±œ JkÌ ‚Õ^è¸ ŸX‚™²²lÍ“¨b' m!§#«³B™E¬#°À0M¯ +lðŽ$û"}ëÀ„ÑQ«,ãBr1K¹˜Ç\§Yh·ôŸÀ§]C*+Í*.eðÖ‡Zåøs€³YÂ3¥<ââZ¾ëÅ!§‡]|ÞGi¾‡qK'Þ× ¥´Úâþ÷·ˆÈ”„Ež>MÄÉ +ÇÙ”*%3ׇŠéÎÅlè|¶t)|1“oÄ9í’èq¾‘ïKÁh|Ì‚I‰Ã”œ~DÌ¢”cÌ9ù,äb–r!™ñƒ­æØÊE(üø/pÏšaÌ B° 9 }pJŸäq°6‡,b`ófŠwñGy‹YÀ@Ù!°†=èñH/‚#áLh.m0°î¼™¡ $Ü 8ìä¦Yœ6²í2«RX±â’Ö9¡˜»4bmÓî=VŽø´.¶( +EhƒYýZ÷H!OùÝP)‘9Ÿæ\Hƒž)-R7²ðÁô8³ÂX—Ä’Ž$Õ@6¯Grõ~m‘ÔUÌû*%†•’ÇêÄl*¤bã±b\þGe”ÏùÜ’‹‚SiR0[ø1‚ +¹L”…|§bBT‹œ)-y/l¾2ë¢9sÂ3âI˜)-Dìõó!U™EnMÑÂ*²‚ˆs…’¾V CzéÜ€€_¤¼ƒîBv‡û‰¡ßH±ˆ&*l§ø´*îªÊmê”*òcvxã¤oœT\Ú¾Çöt—ÓÀëCå\ñéä…ä^ÜUH.¥—ÒˆëÈ7‡ºoÍ6` ˜¸©”¤YJã,d?r6\>C²”CYBg CãK$QÀܲ§Œ3{¡–€ÑoØ2ˆR~îɧ–r[%¶J7,§$’ÂêC-RÇÊéeÅ4Î\³±4 b€]@)~Ï.â[Š Hå$¥bâŒØ7‚IÆÆ•Ùöä²á,lÓ"×…åbFL9k¦”[#ùLìC5ŽDò`”g#ÔâJ)%Fð]åì‰r†E!B.‰…8z«X{>Èž”R´°eL1y´j¬tD9EíNc}¤†³á»r°®Ð+‡ýJ±ÁRô01’—ñ:kë¥å1r‰ƒÃ¥èˆÜRŒ C,ù( ßC‰8[¨Ë×°•í»£ëÅ ïW!pÐ"´÷1óMÂ%,'|ÈÄXý·èå[ +Ô¢‹µàÆc„/xúZøó¾)¦®„]ä‘=—Ê~)pˆ‹™²SƦFæ7EÏNÁ÷7^ù nþnék}¨è!9jéE)xZJîðF-Œ p¥ÔXÏòNXëy¿AѯP°­Á¶KçR çVó6'¤PÞ*-Z­EË ÚQÞ¶*˜^ ¿Î*?Ð]ìGSqYÅNõëúPY/jèyõ”„ÜY¿âYÀV§‹ç ‹ç sùÀ +„rR,ð£%ù–RÆbƼ~„ÜÝë›CPÞ¦Þ y¬Á‘ž!E&5Ìít4"ö 6®ÇÆù¤.pš‰Ç–½ ^…7FMŸˆá“—y•ƒP_ +›Kâ•RV€àôM ËÀ=ÍÇjààæà‘^œßb4âBÀ¡ñËœpåœoß&Ú†rBýGw’*²‘NTÄ£9…º»ª¬iò ]Ãækœ8a) SQSÓ5‡2¥Â+‘8qd}5ö¢sê““4 ŠM('þ"œŽa#Ø‹DšÚPš~ØÓ—µ&Cö­‘Ì Ø +ä(û +ÙlwK)W%²ƒž( ³zÎ.€nx¡däꪱe˜|E¥;9ÿ9IØÊպ㩋ÂÈi\ž’õ‘CtÌ­C^kd8‰_ŸµÅÛÖ\\\€;`WgÖBÍáB3Ø:Ú¾×óv(Ê}sÃ"EÆ“‚Ü€Ýs +3’㤜±š{±Ç[ÁÕšˆÛÐrðé\âŽ1)9ñ ” ¯ Îñ$v%EÆf}:^hO¤É9L·ªà[Ž‹rT0–—{N5h&Õ¢ž¦ÏŽ{‘o9{J +h®ªƒ ³[CTdyÊ°²gº3Ëê@í2 äFœ>¨0'^W– +ÝF`IbC±Qу¸¦³~°¶\jÍËGÖI¶‡(…MPhUäÌéUéók-Þ òöÙ• 7›;:MòÔñŸZz 3à*Ç O6!Æ*Í»Ü5AzÊ«ø.ñˆÑE1ã‹âô§#°L-ö·Ïõ|[ö2-ðPS‰3“¹Áýì[+¿ ¯‡Ù¬ä8gæ$¡)sB¦Z*%Ý•pïÆ*´§Å …±!¡²77¯KßRkùn“vÌ3â¹xÎHqú ëõÐøj™ëàt”£¯x§­B˜ID${«»Ì½¡Öâ­59ìblʉì@ÉY—D˜™²NœZæ2m3X’†š‰9#º “ܽéuöµò[íP|.š¡í4x(rêI”+EéU¨Sj5—€øYo—3s£»J7W©øF; ©bí‹ÿÂ88=Ê~‘Ô<>og!ÃÅ[‡ËÏ—l(ºÛ›_ÜO^ó+[ó±œM± ÒZ½fú%­ÇÕÍct#Ù­¼:ÝÙ=hŸU}&ÁÉ*¹O¿b2”"{Št6îá@JPEÇ,Iø·Ðó|û.¹–´¬“iIá‘0˜¨4W ï¶\G†Úãì;H›zvÄï°7Á)ßñË CHí‚á¬L‚O”T¦ë™—!‘€b¤"Ó`àqý‰¦5–L ¬‡ôuÔúѽ ¥â{Èc¯Î¿ .\N13DyÄÕCÒó>äE!^„œ8Ò“B$q^ôÓì–/›ÓËœñ¡B'ÃnAAŸžþR¥òTù±©RÓCqª! ɾÆB$=㞲ѽUèwHB# f” ’àQ÷’ÝM‰úó£ÏXÈs¸]úv:ÜÞ^Ðfæú¡ *o|ÔD¹¢Ðá·ò:B'4^_űâj™a—XÂYjÝO5³íŽöÃÉiHͤw…ö*T¶õ(B©„Œÿõ ƒRÅ®‰éÄtº]ÂÒâHg¤ ™ tñM†¸„=E£Qц{YÏô.²ïý€ßõS¡†¼Žu‚"SM¹Â9’dÄ£fyƒâÿ!;¯€Ë)ý0 lec]‡™ì¯¹?†D´áÌÿ`iV<+\-¬ù#nb·GúWóer!g^à¡^ a~ø†áyÓðqøg6ücëQ\½™¡yÉLJ9[ ›£”³º‘æ~.þù£×–†žûÙ2jíµx_i¸4Çd‹€˘íâæü±×üølrÙfRZÄ™òºÖ$¤È±j˜Eë\h¤p¬¬‹;C!R爧±"2áÈáìX¤E3@Ƭ’T¿’¯Þ+æd‚T›Ÿ¨òp*Onvneù2Ké·´Îœ/îÕíVoBß?õ+Û6yº¯Óé_Ùþ°¸²} =eêøäøÇ<,^edúää.¼£üµ³³ÆéñÜ1šµ/œìJçØ+Û®±mö¢oßœœžÝ·MQ;wÕ¬žÔoßVÖõ³ãW‡EO×±§†[z]"‚’®š‹t*`©HrÜ·>I¾>aáPUé9÷æŸB\Ø Àõ‹2dÂ6ÃNX—¤õp{¼?Š¤‹Ç)ü-xKq=\’4ð:J‚‚Up¯ÁNQ%­½XJüù‘1…¡áb ?Õì +î~øÆÕû q•X}ÐëÀMBœrà·"´–Ù?ÉR*¶ŸŠêHÁߢh8ô˜ð9.´häôÙ&&ôØkéñ©z h^D庅€Âõù·æªì(‘‚¢h8ø ÆzÄÔyiÑQ$åÆ®ŠB8è5o¤$q4HóI8n<*å6<ƒbŠ³`ùX´”¹ãU‹9µ-£Bj5˜¡]W b-õß!— ÑÛ£¹\Ç ’)G„(ö¸§äÊbÙt(í$m¿Ii¶œëúHä•\p9‹ê‘+;®äL=óîãìj,åò-p<ÙÑ嘲ÆayÛwʼnD|­mý<”I ©É‰9.wˆ3ƹó}íú3DRM€6u÷°F>¯cûSkIéW§jµk•‘³ÓƒãW•«û;oÓ‡ãÝå³úþý“éÆÞÉ©%æ«Ó-¯$Òß¿3k/¹ûƒ—4™ÂUœudžg®žžª×ÏÖOÎv0¼ ÷PU¢^ncÏ-ñ‘7îæK R.bî•è)iPÁõ`Ôý±Hl Ôü2ZJÉ %¡?  AyŸ„óªh­¥Y´=”ˆm÷ô ä6Ï!žBÆ)ö¨œC4âP2w[A!ʘSî¥ñŒFÍ{)6…ÚïÍ'}.Gr!ͽ‰¸…/ZñpQ!e¸—7Ÿî@-*ô²A;µ€í£…ËTqí,R÷èÙ¾â“Ï|µ¨àr€r–Ì 鉂¤Ž ú :£²#ŸD%4·B +£˜‹=J›¬óÔ]S’vÙ¾p‘±dD—F\!Êx–Œø±ÄÔøœrQ-eD›'”’ve\ØF꾓q/$ý¼°hI[µ’…3(•tVLÊÞGÊGÄK÷Eýå³ÓŠËÎH]p# *rÀ0œJ|ú›¯¥(!²° =[5‹m@Ah|1›™‰k÷¨¸p”öÍ%fQÃ#H¬(ö™¦IO–6y‘õÍ ”‚Ü’ +/‰<[¶ã{Ö¬1V)dDD³vs@¥0„ÍbÛy±Ý!4¸,:#•‚Æ›ØÚ%D¥$ Ïý#À”Y’„ZÆ¥buÀbˆ„„{¸¹VZÜHU:A;Ç[M!°î$7:—ØF²G¾'ÏYT`2š‹¢ÉÚˆ6täâd¨ÙšXü «¶¯T¹K¤hž[_±èçî)(ügqÑD\/YÛ2z%g‚‹CË —9çÓóbß¾HÊñ'RÁ‚œJlë1ÈVÉÜûAûÒzÌÒ‹16‹?æ‚d4a϶ã´m-‰ï‡¢³zâ1 )¦Ôk EDZô™È£㢆y'ú±‰œjçêÖ|òÕâ5÷«ˆDÃÙ`_KÍx»'0€…WÒÔB¡>˜hï!·ý"6 ÏsÍ[¹RŸÏõð@â­^¡Þ˜˜kq Åu©¸î×PgÄŠtÈjÎ|¡:Šfk®wÂØJ$b8´Ú+½‹wî a™ˆ|V”10&‚À–1‘Z¬²Jsª€íŸÛ> “—N°(D-Â’A'Ô誉U£¾À TÙ×rýã(ŽÓ‰hf¨ÌRÅý•c”¤Râ*¢o:­+.¸èú¸±¸‘ H|)g䤷­€ÌmCG)iGUšLÀ§âX$q¨cÑ8ùõÌ3µ½Ï›èÁÍ5¸A"wŠâúÊÚoQÖ\ÒZšb+T°¶Z¬F1ºÀçÜi^E±ÃòxÇÈÖbĤâ*ù„Œ‰è8”´0ðî¡À§øA¬§þJÁFÔM ­ƒÄ{5XV„|¾ÅX…ŸKOÁÄÈOÒs£.æ'î°Q>WË“vº"ÿ#)š³Á$æ¥b?©æZÔ ã)™âªMøp. +¿GMaKÍq®ïH)qcëX’X'Ár°Äv/ƒòµÂeÝ]Û] +E€@´ì¢<;ÿ«íPfœ6áªç‚½> §È5m­Â\cF_º>W´mn+ˆ(%xÐþ5ÒÖØl,Å8²[ZÌ‘S¸_« !o,7B;E…ÆúoØL…[S§G ˜¡Óú1 Šgê‡ ”mÇ`;î²Ã"`‹Oû^"â›QvœÞ%´†ðl¥„rÀCëµaJh¢GC☥œW$V:œR-ÖV×¼x¹âÌZ'œœo>·³Ð2»™Eú»Â›â&À…ëÅ=ïÍRÀ‹ôº(–¥¥Q+;èínpÁOöÐË„¹©+»è}ñqso6¤y\ì²înœ6‹"i죹£âJ³lñÀÌc;/{éã¨Ùÿ¸¹„ZzÆ0•Å׃3LGìJ‰à ÕÒ5#%(I U2׌Y›äB‚â ÷9ÎÁ"Û¬i ‹ îàrØ sk8¹áÄ5‹ãâœçfÏÎ]_—^(Úc](b‰Ë®yŸ•Û¾Sšñ‹|gAšFEõÎ-o„³ihªâ•‡»Y:y’žK=9듇Ùoû£–0ïƒóS{gSëSŸú‰|ó0SÉÐÛòͻئ²‡+ß”£Ÿ?Imû§G•åÓ×o+O¯~sp¶_I†7ê§×rÁB{J«Ýò žWI¸¡Ž4øÊüY“?ù_µ¡Â×æEǶd^d {\ýG*õFIˆÆ‘w1†„ø$kPM”¨Ò™ mÐ&Ö~è}õÉ:MýíÙ)¯ö³k¼¾¿f­ƒ«· ³bI_íWßÃW±Îš_¹*$ +è@¥"ÜF40‰Ô? ÙõNâ‰ò€xR8ð®CäwA‰ Èj…Hñ=D—~htUeÇ'vy|îõùAñ™¯éøè±J )̱­ØÂMâ+"Í,´ÍoÈ Ge2Qx}PKŸ8(’ü½ä‡¨íù– н??*¾ óµ9@ :_ˆFbI" 4¬ÚrVÀs8s” iè +y¦õûáí­úv|leÈøÜëóƒâ 2_Óñ‘æ£éUôáf" +Ù7Š,¬Ðó¸«AËHŸ,¢fÛ¤Õ¡J0J°CÊýà;@_¥tïÏÊWù¯Íòv…ІE`§]$3‚"’†Á@hÄupµi”-ûÁ;@Õ$¿I"™Q©$ÿ5`À…Ø Nx{!ßôQ˜‘ÆMæià w@N)Jšp¡‰(D—]è8œjÒGfDQî[:4ŸpŒžOŠ´×DœŽIúMÈ…5©ºDÙgáá—ò~x4‘Œ¦I MRH`²_Ò¨á¸&5…«e‘å²™ˆ4cÍÞÒÓH1•1ÿØ8b‡JW%Eùôí*Ê}KBy¨ ÏIå4\Ï‘Ö%"õ.‰XŸõ8ÐÆ™z@ž ú±‘$2"J—¤9ä¾¹±HÅI> ¬T€„MŸõ6ŽÏªˆŒ,|“¾‘ŽfÑHÍÅ‘sé>é#«SJ®±{A1¦à+±ÓC{AækbK¥q’ÌÇFAÑ$ì?p o¥²ÀE NÅ#…û…;§Ç†Ëéò˜µo›E(.@ìï(3åèKäÛ[¥9…Ù»`èîÔÀàaßj +²7n‘¿ 1ò¬ÐB؈r£pw²ùÏ£Õör-E˜û;°ýySH`ì  ¨ØT}79VncÄ¥ÂtˆG$´€B©¸y§%Íei%ÀÃäb5¢ìp.‚(½8Ä")æ¼T‘\'¥xeI¹Ù¬¹½0‘.\ǵ÷ù’B à6 ˜• +Ò[#N70`¥ ¶õ +½ÈbYGíºêˆ3û¼–Ûöý0%~‹Oìﶷr£LÀØÊ·äìP PP…w …ð(«mŠ—ñNòóÙxDoñHɜرw´Ù‘Ù Ú6à‰e3ܸ +¸E­»5†Æ½Uds—®?Í›ïeǦåNjûiìTtáÝdo庙ÙëPB]Ë+Ü°"ûeÅC%c“> b/Ãw[j+ƒªkÞÉ@NÉÃ$®È Çæq,Ú¯‰šÛSñ4ÿÖÈ’É:áÒe¥ L3¤òK,ÂVØj®Í )·©Ü2¥2#,£‰Ò‚Š¼ÁNŽJ¤:·D”&ûjçoJ‡‡K^P˜Ü+îªÄùÖ2K’e͘r"9NÂáœ0 ‹ÃOfeì8¿±™¸í÷m†X¡œ‘žÃx%ã dw\îÒ}:÷Z1ÇŸšæN¬É,üœõ–A§,(³&M`ºvîaÍNߙ݈tln»‚¤À"|I£ÏcSfîYlâÊÌžà +›|¬ ØR--èP?I§'aˆ–˜‰ ’bá>%Q@\ŠXFpHÙà¤DLFÂqÜRÓQ„a‹‡Õ‹­JS-ƘT9°j¾~ªd¾ÓE-¦ãvËcâ¼ñpdbˆA•Ó ³!ÄÀujKÒbÀ=ä”†Ñ €.–#Yµ€D.HÃGÚRÆ|r*çÆæì&qÞ»a8æ‰ô®vê5[(A’†³ 4’˜&½S Äwóä3ø€4É.4  'rãkmÓ:n‚’æ56œˆWD2•*ìÖ$ ': „€.œˆUódÕ¼Ô´8–¥û’$…ëB»¸.šX—%&•FÑ´YËf¹h"@‰ ÎE!Ôíx›ÑD¤ŠFò +Mt¶&@6šT¥W ö hºýci˜¤áD4\±Kîâ„.Èç@|«‘´öüuÒP–_!Ñ>D',B7Š2û‰]ÁAÄ¡•<ÍSPœ (f€. ÈOcˆ á¼”q&žhèâ‰6˜Ë '4âIAÎ;ä/“ÞÝJ#¢1ÇîÒÑ—¸p"ÔOhQ< ßí)«©¥áD§½qN ¬ýNƒ@| ÓTüXíÌF{ÐKä9R0l%8PŠøa\¸NLo<-I­ñÐ.[M Ü6Çi4¦w âû%HMÌ]41¨éê¯+ˆW7Î0¤Ä^ç9­G;¤šÜun€Òhb`OUÐK5r>( 40ͬ¤Ã Éø2àf4‘¼Q%4Ö¼r G2ãÏ]DîiÖ×elÌ<7WCÙµ³áDc“ì ä‰P3œ˜N46p$i8ÑXµ‘I7IÇ+>)p|«Ýë)ÈÆ 3Ü!Oä~Ùënæ- ”qsr·#¯ébâp ¼ÈQMàžæ; 2/Pm<‘d»hpÚ¢h6>%  +ŸR²ØI&>%ÇyÖN E +¤€2ºª=ü—dâSûÝxè²`|>4â[qÌß-lê Ó6y$w³‰NkÖÜàÞŽÄí0Jm·OÊÞ餎Ӛ3Ô–Y!9V¯Ü#ߊˆ¸9e9¸óQjéˆudjk©:½SŒmtSg£œQÞ9Q]-„PÒP@ÆwÌØØ7‹B•cãðí[åœÁh±ê ¯´ã,$Ýé ШJ,kˆÜfñ±m¥¾lmw@ëóÖVë‹S¿¸v‚>ÎE¨¼”÷4CT|´={/ŽØxIî¾E—4í#­ÜuiŒJÎò´\Œ*ecTM óqo^RšŠí<³¾6å¸l3F¥šÌÍ‚¶æbœq«k9þ™ß|ß&ö°VÓ46Yj²F š±^Ÿ]õà6H…ød ‹ì"A~S'h©ÐP7´×)—'²A.­‹¶5r–¤cæÍëpž¥ð4> ,oõÓ·:%¦98Iöã)„v +RýÆMÕÞ;¥+»(RH¦°|\´ºLµtC¬~g]©Nü¦a*øç%•4?$tøéÂTYP£R`ó„‰æð3 í$²xÌ…òø¥xœR…$ ä(*°†@Ž¤9š½9ÈJ§âryÚ¬¦ž‘ÝÖ7Éórq*ÉÆ©R˜Sí$³-ÚðFKcYM”«üZ˜qДkY ò“cï£ÄQN`…⬤ÔVF¤¤kždâTMP6P“SW¢rò©§Rœc›‰®‚EFtZ?_F¼v—r²9%$—;Âæ²7ÛR_yñ/…( +jBÄJlNàÚy¥#ÒÎlÎdäÕ˜f Ê#=2‰6#Í•s¦H¹ 4JÎTÁIA!Ѫ¸t‘‡r,‰"½I£²/çPåk¢4E®Ò*磫lÖ¦‹(ðSÏ‹õ±H%¹ÔÝ’ùø]tu<Ð\a(?Ì(­Nö£ŒÒŠt@€©—=Ô ‡F\ +  ë! e¯K£«ÒË ”0¸P +J-5®wå¸èÄV¸Ùg2¿"M|²{QŽD}%ó)sá ™˜#ø&(ÚÀ”Hš@ŽËp&åx‘/G|£ ϲ…F¢ g“$JcÝ„¹Ù³)L[–Oü£)}(Ñ&@RVî+‹9–ŸÈÉÀ.€Rñ‘B²BÆÒHå=@¢fvh(cÍÊ5) `*ÿH^ÛëR)és™{rèÅÎLsøüjö昷r¯„&J&rÆ> Óä)‚Åór).)(— ’]¸2fÓ4\)‰¼äÍxeÌn´ÜÖXŠËîWxHAίÀ²®°÷âËñ£\"‘Ô3òÃ\n:0Щ­h!`äÍN346W×3j+Žö:«h[S‡Ažõ(e2 {+z.\[n>Mœ,üV¿éÑŠ6Ùщ¯‹çÚ9ˆÇÍÕyò8†[œuöŠë7„—”œ²“ÙSõC‹ßNo…×/Œìþ6Ý0b 0ˆâjš¤ç×,Hg¼ˆèÏØ#ÝÅ[ý¸€Êòs(£‘ÉNÛhU *xpL†Ô줨ۼ¹I-é+©Þ”%pƒäiû¼Ôé” SÍ5Ê.@ÚU—hg†CÑ8ØéœcdŒâ$GUÎ3•ÛC+DzTE@SÚ~)cÇFÎõ—€ì[Èðwz¯Š‚œÀP¬HmJ S•ŒM™’.zSðdA©€Ê­ 36 #îP%Q9 q×I¼¦ø46Ÿ*#dM;ËIè”z²ÚØó ¹›%’Q€wÆIèæfKöTF©@99' So;ûŠ óÏë2µ¼wNéÌ~eiTðm© ÂÀ+ u±PpÔò ¦_'q#õë4!¿N +tÑšÌüP–&óRÎiuÞ783Ød +–weœ3ÊËH¾Ô ˆ2×¥†çnŽáÓÏ¿"¥¡ìPlÙ’æ}›Ï˜™X”Ë>N.A;}X3‹Ûɤl²·MËì;(•ÛÁ(Pº@ÌÖÓ‘£æ&Œ™–X´eÌt¦©TC*Ù+†RÊ⤈±V-­1ò§ß^—ïk蜪WMif&5±K,ÿBN¡¹ ø*©o›U%Ò˜S£=öÿä(¼¹(YÛ4DaÉ#C™âü£ç¨Þ·":Gõ¾Ô5ÏR½o¥;OÖ%œ4Aé¢d)BÉAÌÌ»Ãm¹­ð­5³eÎÁ”ÙX¿©J1(%¨N õC¨3F½3ç=ÌáIêQur$ ädóÜ‹E¥È K%§¡-ßoÎV‡**¬I”Y»&0¥Y«7a'b÷4ßñ}?ãÛeçAngÓ)4÷ßVcÍ#”D6rÈ£QÔ/¹´Væðø§’ÊñJ‰aðð¼”ÍFž›…ÄsœÃ;ƒOYPº&Y`ÊÈÝÃÜgÞÙ܇ÌØÜ~!ÈPàÚ) YtÊÌ>‹>;€À¢w.³¼,'ÝRŸ *™£.‰ŽáÅ£èA/To…÷èÅ£è^<Š>áÅ£èS¼x|õ Ü º‘è/EŸðâQô /EŸâÅ£è^<Š>áÅ£èS¼x}‹GÑ'¼x}Š¯õâQô /EŸâÅ£è^<Š>áÅ£hS¼x-èÅ£hA/E›âÅ£h^<Š6áÅ˸=ÈöWã^<Š6áÅ£ˆQ˜¢Mxñ(Ú„¢Mñâqíl”¶´ /E›ôâQ´ /E›ðâáE.áV õâqퟞéRÑ&½xm‹GÑ‚^<Š6Å‹GÑ&¼xm‹Gñ?”§^<Š6áÅ’²é>@?E›âÅ£h^<Š6áÅ£hS¼xm‹GQ'¼xuŠ¯½øU´ /ßZ¹¢Mxñ(Ú„¢MñâQô ¢Oxñ(ú/EŸðâQô /EŸâÅ£è^<Š>áÅ£èS¼x}‹GÑ'¼x}ªJRŠ6ÅNÀ “ž*ŽÛØo'ð1®Ú+Ú„µ9#X2a'PxŒ_gA;¢M±@aÀN€ôè·ð"Ÿ€úì¾ÆÔNà‚Ú S¡ª½0e®ÿ Š&휄Îv̹@À ·ø0Hí¼H´𲶿 ÉF´p –ÏN,4ùÑ°(ú„€É|v( Ø àà +Ø Ð´@aÀN€Òo'€¢ ;Êo~;î*¿€Õý@™´ éÙ£OØ pX¿§´à2üv\¬ßNàŠX° æv*¸@@·ˆåv^$Ø |4ÁìÀ­&ìŠTî š¶˜I#ú„ ° ´øVKõL¼¢ ;ÂØo'@Løí\+ñÙ |˜u—´ŠÚ |ÄÃ5å@¡ÉÄÝ @–ëì8½€À=Å<ý_ '±hÂN ÀÎë,h'ðáA˜[ÐN `UñßI;A€$¨I€K¶Á ;/sí&È-DRT³-`JFrÔ4’#R“Dz6Tˆ¶KôGŸÿèDž h3 >$ §–¢U@^H´ëd/ìÅ¡bψŠù÷3üÎi~/¿̽áÖ=£Ns¦`!³è)ªÑ¨®Ñ„"‹µt:cϲÊE•¾ˆ¥Eôñ–è‹è«G=°3v×Det> .Bê*ëŽ t&ÞC›ß{E®Ñ@(Ôt·%üÊL—…ÏhÀõ?LX$›¼žL‹¼‹[¯Èö@ä2£¬u7• t#HV£¿“U×h@ÓˆÒ"÷ÊKg ðl€+ET£ÁÙeÕ5è̱å  ¶ä6ת£º6æjÂ"×f 3opLqÇñBï@eÕµ"Û ½y6=@†BËäwÀâÜ`ð÷åª`3p¯†Äz4øAO ƒ­0×d`pö¬º·ÁÞ­ܘÉÀ-R“PÈM†ûÒ“Û ú⬻&Ì£×dàÝû»¶ƒyîËŠïÊÚ6)ôÓ‚Mw«`ó Ç¹&¸±¥ûɵè,j ÍÅpjJôGÁf 3•³`ð=É»6=½‡"×f 3 + +™Ñ¼,¨S‚k ‚¶,¯È%|ê#ÔãÇŸk4ÐS, ¶â t†gŵè,c©%®Í@(ä6è‹A—™ `D6/Qf© ×d §h%Ùc€:åJ‚½˜.õ„rí:ËìƒÉ2ù¶5)ŒsOÎ…Ÿ-Ù;˜„aðÐ ²`.0øëajUì™ fó­2F0úàæ#e¡ØùƘÆ&yæ·PåæƒYF`ï1sÁ%H•rGÖRÓØîæ惣CuÍcðÌ$h¬êÿ‰Ežá²lg !É¥zjQ“U×\ ó„ ªk.¥>sƼidÅç?ɲmª>ÿI‰[12—8•½‘= ’?ê 渦ǣ«DÓ$œb\9›>÷¥c‰ÖýAé¨äêkl H.‹AéhaÙ”Ž½®ƒÒ¹'Ž÷XÞ+‹½B7.˹+Ä¥3xž,1.a±ÃŠš¼p] +ãô^`:ê~Ï‹øúý‰qé0Ñ %{ñIgžB!µõÉbp3ƒqE϶`HšÀع²ÉoÁTÁ]Ó<{†W"j¸¼Ð LÇ!.!Itb`:Cæ-ÝW +LâÃËñËŸ ¥¤B=×ýžbž ¥Ì¨Cp¡”9ŒDJú2GãÑB²%Ì-](½BîBIÕEY\(©Ï¿¬ø\(eÎi=JÉãp®5]eÅg÷±ù°o³­(zPÒ8n²?:?K„ètŠÍð¯»&aW&£Ó±ÜëbD9šfX£Îi’-¹†s¡žÉÐèõF£³Êþøt®ãÍŽ^îÉb€:î^é Pgq¡K„ +"#â‘éÊd™+|¸ø`Ò©ŸŽq +€:nf.ï N ô=Š¿H )·Ð#=®î Ê9¾ ]Wo¸„ìn jdöí)÷¦Ðç?)ë ‘žÿ¤·U\ÿIT+ü»[w‰Z0÷s%EðŸt‹DÿI¯ wÖ‚ÿ¤ÌŽOŸ0*›ŠíÛRp©ú·øüŠ¨fM%M rYp,Öì?Ö½ãÍWh²Îó ”e–BŒ,å¼ÿ¸Ð1k§xª¸`WJ¯Hô t ]JY—üžN¥;ÿÁ¨ÓlÔâ +wÀS·]'HÉp•kaKÑÝãRGß± +!êŸÌÎrï§ïµ…Â’#zRU)Dуƛ÷-?(Ìø(OLv/Fvk+Ђ‚8 ¢6'ábŠ8£מæ•#n¡w1âvæ]Œ¸ƒz·Âäø­…Âܦ…©°Ø¸¾ÝÌ/P|Û™_¡øŽW™….Fdƒñxñb„f;¦¬b63ß„"QBð +¹ ávæI2g¡DÖ¸¤â +/ôݹO‘øI >¥Þà¾Ý,ŠW#ô¦å{ÁB)3ÒõÒƒN,ÙLäÁ”±ߣá®÷,ÙììSS¶k.òLÙ^‘`Êö +-þê•Fó µ†¬ +¿,­µ/r3æÐó‰Ò,÷«_w¡"Z­mnøñ*Ì ãÙ²iœLaã+\7w¾éìü;Ÿ‘qµÜ˜í Æl¯ÐÝ|–ÎäuÏšmqF-³-› ç e +»®«Ð´·nßSZ€&à?”w5¢È„jø Xh2k¦7.KA*ÎŽþU°\µâbi’H¼"t^¡Ëhä.D„Å{su8÷nÄ4%ˆXw úi”1A)˜°k‚v裚@¡« »òÍT’¹EÖ½Ó_•dvÙ,“XäBD,dó:óÀëŽ)¢S„‹,O pQ*Ss°ê»qW.Òƒ+2z÷"B¡ÈÉ|‡›{/bØ𴌈Òd†Šd¡}^WÂùS©ðò,Á&‡†öy°yè:¼F1M s«ñ JìÑ+”ƒ¢ÉW±^/8?¿l .Çî_“ß3¾ë›í÷r¬¤ì¯¤À.ÍŒ7Ԫ얺zÈrn)-ÃÐß4Â(wC¶$єѮêBŸ”K‚æ"cð‰&/Ve[xÈNK%Ûuw”-š_V2\÷Dj+–@u1=Ç@z¦CtVEË ­wsµ*6SULÉê–q9­cn©û®RÖƒí-UCßZJ¨ÁÈÞ#Gdåà‡¬(Ü2vãú\TVâ‡/Æ{óÀJÃRÁуx2m +Ÿôí²[ÆÁ€ªR º¼)…™伶ØïóáI}¬¨.r˜ò9Û±l4p+ªŠUUö|/XˆŽ·B–Nݛش Ë7o•=õ…õvÄmTD‚¤2·‘QûPJgP‹D º…Ô÷‹öÆ°+»vi›Q†‡I•‹¾ÆÁ~Í0Îé‚åóÑ”+Sˆ4¥rd±-ó­ðÈVå×Â"uƒ¤OÁdxaa¨4àm¯Ä[¼Wæ©Tg€¼'<«”¦ËJ€ Üû"bt5}ÔßGP*åì'(ú¾"Ä+ª®›(´Ta†q1²G‹åAb‚L뤈È/pQφ¡ À1„ÉÏxi ã¹ëåqvðÆÁÏ09‚ü¼Q¥‘óE~ ð’i™¬s"MbGª$&½£™p0â·K-4Æ—¸ªè– '‡Wêž0Ð!=¦½sˆÙ3'–ênVïlãa˜Ä#PeOüG(h¶“0#™¦û™+Ķ<÷öÕ¢„êô+[)~öª»Ÿx%üÄ Ø¾öZhb¶Ò¿k)T«ü_!†¿_ýý·‘^‰2ñ/1—|øhaÉ""$ägäp´©‹ÿ΂Ÿ|Î!—¹Dôîêc5Ir,kôn*ˆÄë’ T iJc2º@‡S†‡ +>2vçÄèÒ,Äð P¢ùñË"“‘5ÛbI0By°š@°ªª3•<ä§. P]Äô”ÆdtP¦ |tæÎýogTÿ³äò÷ò­ÿâü¬L`ØnG^$/~6ƈÉ`› nU~˜2 +ðQ«ÂjÄP·Vs¢P<`' yñ~‘ƒ³S—)QÒÕUK¬‰­©ŠÍ¨p¢T<”§5‘§1 î™=P²5Yî!B/;NdUg-t¸+Ô£@©x¼Ok¢Mc +Œ¾àùcßü3öÍwìÒ?´Kÿ¨Œ>Ü3 (8QN@Adú)#‰[oB^ +nÔé{B›l汩{‰Jvo²ãà†±Å'2Ñp3™ +".N…Ý”ÝÏå’)›|æŸAS÷Ä´5ô;oBŽ›ÒpÊŸJ‰TFœØS†êŒ=8eQÁ†ÓvûT¸}õçÇÎúg_Cû±ÿ´}ü5Å™˜EO“§éÌÃôr!úœÎF…VƯ~ ­“"Ù6áæH³uI’ ¼ ¼œ,KàXgÙ¦"™d·†LW¾pªíãêhÐú…´ Ųû²zVœA×.·[u§\¯¶[Ý×ÝA«qèü­‘jz(½ßù*‹p¨üÚwhEÒ®×kcMý¬õ‹Ó>sM§>*ÿÚ©õÚÅnµÖv>x·²P›÷K«ìŽ[ gèÖ +^ü¥ßŒ¼a7Èÿ v(Ý\/DIEé¹ØmÐ1ñ»öœ#óí²’Ø™Qo"kì8q‚â,ùÿæç…±ë z]ÀÌßøïͯäËùðNŠð~)tÿ(…dÓÝ\€´ÍžBZÜœQû²D_?Yn² …L² õ sœÇ+aÞ*B{¼|6 åOfdâkk)&ãNÑ«c_Í`3xòqˆ ÌV1žUJe£ñ0nàµGŸÊ,‘K2\àM9Ž§é¡`æ$D&‰—Ѩ î+ H~¦@ PJ“)ÁDÌTƒ:¦L·Â³_ ®HI&xÎ/àd 6u¨a;xÿO¾L¸¥×i; {ðÌ hQ•½%F/SbठD®Áû(pÔ2Mr¼[p÷©é,/™æÄÆ<‹ˆ`‹:GCnC¨Â"Ó…s=,ŒêNÀ‚ä5…y€YàDp®ñ‡Þrap|ünZôšÆ“Ž™ÁÈÄ^üÓƒ9éÑÔÊ@p&¼R…"3PC„er°› +›3šòê'Û)¿@C]v±„ÌÌà Gá¼Ì…LL’m\ã;fCg™§ 5™¡Ò¶P…Œ¡°À7upl#ÛCfÁÙ ›†3ƒLïøžšI$¤[):†eË2i¡[,dö²ÎŽï†|· tž×©ª»ÀìØ…ÜhË”‘JïèAÙ6§ vC¡E:P½IÛH¶Š +¦âEÙšB¡³+¤kÆ™)ìE©dGÎ_(Ðy‚{"qáÔtà•:0m‚+EÔ šm[‡Ä¤X¼ -Lêtªƒï„Nú4S&^è + 0@º¢ŒL—©/ ¶¡Ht ý@¯Ô• 6J"Ì4D Å L )4ƒ“oôh:¸¤i@4‹Ž £ÀæB6Ò½¦>mj,”?A™¦ªHv¦I®!(ŽŠd¦Ò7W^Õ˜<ПÁ^yA îä†Fß|c#êÌn`\J옾«1ÀYÏGÏlÀÎÀ,G4<‘Mƒ㊀D71z¿Är¿éü-.)¡Ù–tžo]•ià}* ­¯@:]èT—ùѳ4ÇÃýi0ß*Â31úÙŸn@f +Âè‹~ˆÚ0ÀÌžˆSÂ(i¨Qêy¼ _|¼É,Rgb „ +Ž¯„Ðdê€G ØÁ¿’™<Ý< +Õ!²FŒ7©»£¯ULO ½Ê HÅ Ð6}맃ÇÆ{‡U„C1^Ašhà±Fˆ€baªK‰R.À ‡æêD&”Ôàùí‡F›ÒTŠ.ÌO†)ÔÀדìTͤ¯‰ ‘Iß`T j"vÀ¹ªfGÓÓÙÈ)4fU‡&6L¢A€&>lšA7à#|Ò™<H€=ý×xÅ’%½Q¿]²ñð-Md‹gŸnQ^¬c„ä ¦n “-8£@&‚#ˆNB­¸4F`Óy†`TDÌ ßU "•>l#lS;@²Å@ ÓYÊmƒFá$œ‡è9*ÝM°‡`_ëm^ˆ)ÒØ6èèŒO¤ 0 +@ DB#‰=&JT”·!£™÷-0$ ^žñ(>…s„Aù©°7Y¤@‡øz&f-CRÐi"Ðâ65iÖ7r’ÀÀX@HÂ4éÁm  Ù +ºnQ>ªö‚xVhecÒÒ$o…4ÒÐéˆÜÌHÇ”Q “¥0’fиÊ(ÔaÂ;ÔÍ¡_SØébÂ8@¹ Κf³üi”>±[÷ƒeúGa ÂÛë*MR‘f E㧩ⶄjÓçdP‚ F 'ÑÈþÞLsNX´+S£YXéæÆ`1È›í÷cÇmCÎ9J¡˜Å‡l]8$–ù\M ‰FÏØ0jÛBjÃ"ð›cSªA_9™’—W òk‘>5*ÿ©– ÍŽ½42¥Á}ðaàL^mTÎ` ƒgZLÐPéÉ$Z2¦U…¦ÎÚ`T'ƒ½«TY>l )Þj8ÆŠÄG 5H \À{ude +ÃE3¨2B=@AJ„¼fp$1BcÎÆ@áÁ»“‡6ZÒ1Ì-2eŒ©)SÕÎBªÇ¦!ºA±2QI*ºìTÅ qø”Á€`š<0ŽÔ Ï㽑Á eâX€D*Σa*,÷œd*=æ ›†{²$„šÂé$<UnÑïN<u*í1Vè¨ëšàœn"w”’ádˆŽa²È!Є½¢ÆpB&Ä·1qj„ÏZX€±s‘OɸßLˆ@¬bT’a×K€å`º …¤v”¾H!Ì™¬ Ñ4èw “#CðhH@Æc‚ü`‡ùAJòÍ€Çb`L[q, "Ńš‚ïì‰ b±#ƒ;êZ0QÐv*fY@d6êœL³´XŒh…¬ ôûçà0î¤q ¨\G(’鉴@x7õÕu—)”˜= Tæô¥ƒ[³EÅ–ux¶ +J-ݸýX†n…Ó±.³šDÅS]âIÅgÒ,<¿ñ\„˜¤‹€ÛðÒ€Yª°Uu|έ²ÐÂP€„U7Há °ƒ¹¥ >laG›VLÑ)¿RMÞŠÙšäl ¸»€Žd›½ .…J¶"ñT_2 N Èì)³ +¡ÛÈ€ˆJLô¦SeqIƒj`²-² ‹'tzÅMþbSµ!¿ Ûœ“:8aŒ ŒZ¦ fäº*%¤…ÊÞ¹Xˆ’e` +p*6zÏ+È1-I´Ú,ìjÓÀøpôsj‘ Ÿ„h…>@& &S¦‰ŠÙj4<~5&Ù­¦l–?Ù„4z«hdÞ +¦š¢øøAF¶¢a·–‚ UŠC‘Q£B¶ÙÁ¡²`úI|oRBfA\‘L€–-&!ÃVÆtÆøn‚n…ÆA6PßA:Áàû59Q҆RÐbÄ7 “káÊ`sÓ0å*èLÁãOŸ°ŽÖ ì¹”]Q{¤lÐhëTÞcÁà ª„C 5È‹,Çõ¬¤lÒ “Tø£$çð–Xê °%QΦ!.‰  bYÌ9 M¸»h”E‹½²‡FKñ†Óy<]˜ M‘ˤ2&¹×! %Z:IG:MW™ð/ÛÔ€±Ð¤6TÜባ!˜jj4äˆBø²E×yXb•²‡:(\ÒT#Ô¾iR‘ 0§©h‡±ÙÓsB 4Xnð-Š÷gT'’Y˜>@+Ò2(E +¶³a\…šhr ›ËPf£Ê¤Â©¨Q«6Ýn¤wú–žèb6MbÏÓ*h¡³ho¦0X5èOhzGó Üñ(hIB«5>ÀCJé®Î¦€‚†–E•¦ãS GªY‚̃K!˜Òej e'lˆ!£ØGc´ÛÀ´™XÜ““€®DOH&_»â”pf¢­—†šW™nNBšU‘(öƃ‹RŸíLöž@@ +XTÐÅ$Ù“,Á@ŒaPèê‘ã–T6ä"'¥ŒÀ0U³ÔSS0m $ÂZ½ð¸!{#Ç€Ù£‚Á!lªy6Küj@PH´sYŒÇƒÖÁŸà +¨Œ… +Ýç†Î’ãü€…ÙŽ ê®P³½fñ¦IHš>Cš‘%Z‹¦ %‡9<¸Õ‚,XVéÁÚ7º¯feÜAÔæoã#1j÷£§…σU¶óØc/‰Ú+¡µõªà|ãÁ%€DIìH4À.(?x–¨…ïg…eÆ` PS‰ÂÃÚA]°Ó‚lN{Èøì1ÄŸLSÉ£P¢äC%ÂVÃwI6=qfh§‚ÞZ¢àÀB¡ýä2YîÍT¸X¡”ìTlU…^&‚‰M…»“^¿¡\…غ@BR–¢Õ„K(¢¡i uÁ|á>› Í°éÉÔÜ«ÑKXzÑ‹C[4`·{=v1Èb ÞX‘INT›Ê7@kÐ1} æ;Bî@:»êTi< dƒ^)ã%™W¥ vXH +5 «h6Àd3µRS ©3 €œ(–Dç‹›¥ÎaR¹!±ì×pTÈ25¤š,†L0^Á'‡óÛ.‹;hÃcq¼Ta±ùÕ‚=U£{œ dÁcL0K)šMÙ*\.pCƒF‚@ÃPè«`8hP´É˜xå1)ȃcf0Ù˜\éÀèÕ„ =PÊx!v;Uõ€‚€fP[KJÇ„ÉxPc•4°Î‘¯ô^..ÑŽ"Q¿Ø ˜áíƒ4Ú„ ¶ÐŒB·~ïÙg ŽÞ{š½nWðJD£·dªü–Îg°ýÁͺj‚ýìF6\£Ûæd2ZˆãÖ„›CO O¼S'[ÑJÑGÕTœ°€»jà;†f^¼8F€Ì×´¨\&ãk°™˜`BâQßÀ†¢Pu#ä‘ï "ë¡…Óµ<è+oaƒžâ`ⓦ2™¤\"x @¦}ڴ讄ïोª–d–2b$Ä +ÀÑ$&¸SÛžÕ÷úWè½”`}…Ê>èš „0¶ ¶Æû0Ròäf¡’_Ь64Ï^Ÿ%IÏ/€¬±«A ù2˜váö̪Hj,M ZL„pÑGP¥!ÇU€¢e‚m°Ë lTÕÕX¸ØFÓÿ]‘ɲÁæŠ 8‹h¸WA“„6¸¶ðFuGƒE£î\w£‚ j¤„H8 nAdz© 7K*µ’¢£‰jÐ{#ƒEñUÁÐ¥b [>MÎSÝvµJgb€¡žàFÕ)Œà‡¨`ýCÞ Â"験€ˆfžU–h ¤™Ì]…TÙDÖ1O4Й9[¡Âf$'‹&-2iÐ+!bš·ðÀC5—^ïÑî¥ üÙÂQÏ?ë +_^ŸRÇUB$e Ác¹ˆ³‚IÁ¤Ð` 3}N"Lg V4O* ü5À:ˆw41HÙ -i„Ä4aÁW ¯‚l b}ƒ¬¢‹7Aÿ„+ŽXMv€ÆAʦ²ØÈ <¸1ZŽÊ¢ pšªÒû;4ºá˜:%‡lºVüC@òE@Ldà^À†]ê®A¦@ÇU£æ +r£Â"óc\¡€~ŠDo±ÈFðø…Fïú€s¢ +X‚à+:Âð¨ùèC«¢h£Âm(™^¢Ã¸ï|ÇøùÈ14HšG#ì"éah~Œj ¤gSB‰‘œvÐÂDó¤B%jU‹"LÕ`hí*`3‘Ñʯ˜˜7 pÒcGÔ‹J¥$0K(ëj +§š¬S”^À‚¬Óƒ|$<lƒZ}mÊe¼) áµ<×0iü*nÅ󪊄®pËj0/0LˆLC24G¨ÌÄ"³#²,€>VW ŒÛeÖ$NFB‰ÌFC X,ô -CÉ §›‚Ì‹»Xh,à Š'°3X-xç©1.ú1¤±‰·wxmEO-»( ï/S³¿r?~µ hÔ÷¤C‰j€šJÕ"´Gá6§_™E.æœÔ{×q=¨T£)%?EÆD ^£®:µÙÍ?°, ]¤01]憉\PCœÎŠ˜Tww`³vÍ£ #Àý91𸂞ÐÆfqí1ž©glQ³ >W ùiÌŒ£(TÄáV1رpu·øørnàºÖ`TfKÍO®”‰îápôÈÜ°oÓ|¤—6y !X "•2•ËÔ: `æc£Š™]¬ç0°,²7lPÆcš„Ç!Ö€ËÂ¥3ÜA©Xå™`÷xueÑðZ|ƒuæÀDJLàƒðÝ¢LÄNL8¦…ƒp‘:žÉheBa2[‡0Õ™«ŒW>F(8 {éúîRGV®–àRèƒ$“F£ŠW$jmÂh“pæ›Ì2‹Îx(I:ÓÏPM®„TlØjzvRßHô— +¯¨0ÀåH‹ø½5Ë (cˆ|ôò…é`¼Ó©yn.  c05HC»Ð +”§ñr¦ºxKž´ª÷:ýÞ¸Û ߪ}'Ôé5D?è=Àa QÖáÉ„&i`'¬WÅׄ"$‹¾ê gXS@ù§¹‹K—»¸_ŸOzݳA«;"«K&i1:‘‹?,œôá‹þrÖ“¿§µw²‚…X¶Ñ«9¡Ü`<| W»ÕWg:4œA|þo!úc¾Ún·^Õþ[«ÎjVÈêÓ!=Ô¥B@éɺñPr!æo Ïoᯬ†úØipúSµýÍú´ï`UìbNýR»:âõ ©žv ¦Þ|õòØÅ[uPïUÛ¡d¨äÎ`~£Bë'Bn%ì¢A+ˆÕŽ{}^…QÈ´¾œn«C»(;ÕN:JQTÚð!úO&˜‰ù’ &¥¦þƒ0·ðGUé'"y„ŒPvÿ97 [¬í`Ë£Vlüç|™ÔÔŸõÏ¥j÷ïZÉ š!+)¾‰ÂÊJ¢.dúÿ¼¹þ¡¡U—^q`"À‘#*öeœ{õñ§ë‘=Kô;\¥ÿ±ÿ¢Aݵ 2˜4@³ÁÅþ0b—ìáö ëý3ép¯ óÊO0R¨„Ò¡Ën·Úq!#âÿ’Ù¦\éÂLyσd÷“„{,$ÿA€Î˜'cjÉÑ*¤vHLxƒÅb[«Õ:„z8NDD\ƒ4e `¹†ÿæ.àÏÄÌ,î «Fö œwæÂEž^¬û‹éIÂ~cH%û^@è4`°Ï*(lú AtyôkÛ.¤»½Ÿ»ø…i±l·G¸sú„L*é,à~rø¯é<ûJ­6Ô'$Üê†hZ§R«²FêôUkØ"2t8ÙCyT­üŽrÕa«.L ×mŒ[£¯4ÝX¥ÏªƒÑ”^Ê£AïÃùú,ü¡}:` I¢°KØ]8õ¨ð#­Æà¢ñ(%ÉV4¢qJ²Ž÷‚šAÔP¢â›àUyw‰ÆlCƒÑy‘&R-:Ó¤í&üo^çrÝ9|w ó/¥‘ÿ ËRœ ð>†ÿ½`Ñ”?,߉O~¿k,¤‹¿8õ1ÌÀ¶Ó¤¿lêÿ àÿ®Ô"}Cj9þˆØQf j¼m@r-E6xj›ð@ÛPX•0Æ=D£‘4°{ƒP Ê¦¡™’ïöà>†ÂøA“é ¶%ˆ¹úT±H&û„†A!è¿I.âŒê‡TôWŸ™Æ±è‘DÂu­ÜÛtz±ã‚2æQ ØÀÿéô¢ÿRÄ¡À¨È?äèi¤ÿCŒþÃbtÎùÉ!hï5Gÿ*Aúï6‚~™Nÿcáíw¨s—ð·X?ÿ½hE¨7¾¾¾“á׎ +/è´[8åèóÞõÿëOP]cʆF7Ÿ{þ¯©…A¯*¿U½Ÿªÿ¶CõŸ´¿ÿ(€þ!—rgÏãÎP‘Ö›Éý»ákÇã½õá  w—ýjkèÊÅ9x¿ZŸÎ¿oZôßËš=ô ëí;‚èr#ÆCç¬|”kq\µv·ák÷Öüº{ì7ªƒ(QyA÷apOP$*v„Š¿ô«„?çœfoà„®œÁ°>ÿ+ä÷bícÚ~ðô/OÿÕ¾a=ÔP’1Þ¾bSoGxÅgÿE:PePí SéÌ?^Üj3O¡£¯-.Œ‡õjÛ9ju©+¼Ì"ï=—ªôÏý*f!ø¿)D-? ±®%ˆ^mAY)&ƃPtCÕ!i‹%iÌj"|x˜£O8‹þ”,V ñêñ§73”ï ºÎ`RBý¿ôëT)Ì7…ù’V)…:úwòƒ¹wòÿú#îâû¾ç˜Ún +‘ÿ \þ¥ŠÐûí_ÈéîIiuÜ= + ®ÜêôÛ.ƒ›€ÓΡ4pF¸òH€ƒ-v^(ØoÆ“=#‘Òg5ñIsÙ‚lHøÎÁ0mÃRt]±d&;4LIQdrÕš%d%†&)¶n“CÜàˈ݋úãï›ß‚å gõ´åi +÷UµMŒ$•R%ˆeÛ¦ª¨¶!‡ C·%ÅTU rº+·dUÖ!¨Ù C±³ìIåô¤‚\©yW Ýü²(†a1=†JtNHÀ£AfOË5M[Y²yèMÍ4ÈÙn³ 5ôð‰]ŒÛÎ@d|B,ÕÀ1‹©E¨…!‰Ó—ÝVP§oT×áé,{Á°/Aþ,ï–8”4dqN»Ò» sÀ9õ†-X,þªði%UIW¢~¹ 9°™øB¼þU#>]ÃÝÇýÐQµû:®¾:¡³^Üg¨ àƒ¿ú!5p{¤êàcf(R%>L£ßJ(iØï±¥ºg\¿Úhz«¶[žÔœÝeÇ£^è¢:9‘:gz 3\-ßÖ¡Ö°×®Ž²´ÃÌ­üÑíÕ?zãÙf=¡ªêWûÎ 4luÆm¼0ðŸŠP½ÁúUÂÍê¿’nZ R›/á›ÃÛ±±ûc§ÑwBYÀ˜]Z tb!çÈé’iâ…*ö¢ÊÁðÇÎðÍ…2.GM@®×ât<êèÌoã†ùÛm÷jÕö…Ó·‡î¯~¾q2´Ÿq¿Vz}á7åìþYu@Eæ0¼~kÕßνf«í¸ù™]òóU…ûto*ò´‹šÓøZ‡g…ôI  2FM·L¾qÎH­¾ƒÌH?—Ú½Þ`×cáªb漺9wõ߬zÁÏÉSëö«uw²Š¡È³& <]˜«nÙ–<§ª7U8˜æTüÂDQÚçù%±t8”ÆgUÍ;ívžkœ!Íì*z§¢2{š{=² zÝ=ATf,|zNp#Ôtwkµ;j…#¬§m…JÁÂn˜P±}RoÙiïUG„3É8ƒýÂP8fÔ<êÕ«màb]±Bì>0*ãP²–"ê™ d‘ÍQ·Ûœ±Ø“ä×É…K ¿m·ºNhDäŒIAdZÍ!zg08iCe¼fs>®Âž ÁU§à™˜D>Tüeä +‚Ò <ìNRŠÁÊ;ž¡Ë’m9Dtôo·UÝ#ž-pö´¯ZÎÏd±DªU»un˜¹`[•ªu'‹·Œß¨Œ Ôž½°¶oËŠâéÔÚ.˜k¬õ5ˆU}Ìj.o1íɲò\&òJãã^Ã8É‹èûæ¨ Üm ßJü}õÐÁY»ÚuèfvaÚ¿£ rt +ígAØ­Žž5xˆý9ó[!ÏýýÍ¡|¿Ûp~)µÃoÌ/ؤìÔ{]F5D `U¹íä 0¾.bÞ2̯a–I)ÿj±‡ÿ·^_C®W*šÐÕç+ðóú ¨/:ç´w@VþÉA²™ÅYì¾Ï5ÐÞOΠÎ Ãù êíVŸÈÒpuó Ñ©_ e-LOQöµ $›ü‰º[iÕc£ÂãŸÔW׈°Qê‘Ù\«žÉf¨<"*Q?Nùø¹šÖ§yПí›?s +H{¦°t˜"{e„—êsê´å:Š°â~ Þð­ÚpÎpng.Ú.Yv@mTô×r‘#Öú¥Ÿò¡ÃS3µƒ€þZδš¯Áš3ê¸AønŸ<2§•Fæ×l·ús‡áe?QCV±×¯÷¾Qa8ãX¡1ö˪ +CF¹?áöú¿G ôäYÚ¦ÍeÐ SN¤ÊÆ7j5ÇÝúŠ¢uئæT›?=lSív¹}Âc'jÑ»îÙ®wRW\ˆ]¦Ê©ÐµS å{è¯õ+_Ÿž=ÄC?)ógDºéS½s6zHÂCœVPx™è‡rÎzç×Ù *öhL¹Y5ß{µT­5êTûS¶Ð”þÚùHÕ@mé5›)jÀgBáÌêpd VŸ¶<çã¡Cô T‘8ÞE¯ÛyàîµÁ¬Ägæ›o8j§´GdàNß /hÆê{Ûà+mú ¦v÷Ë“ê7¾Ü9¥d·ÅT6Þ|“à¡¡UV¦"–ÔîM{f-ñ2U¶fÕjuª¯Nªí4¿XsĹ—nkók¼áM}æ,i]‘@c„ÖˆFÚÕ~ DÈÌGôâ㊓ÕP›]IÚœ®z‚udNgj“Üj¡us>Ÿh8ÃÖkwŠu4XwGÝžÍë+ +Œqn½ê2ª/í h5ÿª·sŸUƒKD{œ³¨éŠ²µvµÎx³©L­=h¤z‡T/±@Å&0xê´ÀN­Õg÷óhG}MÍ%>Vç§ A9X _Mó¾¾Yï§ùkÖûíú¯³¹­Sï•Ê`Ñ„)kÆúžÚÕþ·áÀêÍ™;Xß”s°Ö¿9ù&åbuʧ½ñ•6„äGðHÍZÏ:’ ‹s›ŸÍ9¿Q§>èÍ‚±JŸð×V·9GÂÅjáÉÆ·cN­:UgKÞñò…Ê#aIߪ+œ1_ƒ¼i|¡²;/Ô¦1m5»£T£=ŸåÑ:ýA³×Çï ÚhË\¹œ¡zÚ©²ÆHž øÎÕ<»ÎkÕ»bž©.€6Ü«nÂCY^9ä¹E›µîÝ´µ~qÚgÎ"5û§~TŽøMì~Á?¯^ÃÊâc·ç™?B­.š7€Ï¿'.•PvÞ¥à°-À± â³kKš®<á™LÖF¬ÌÄ°wt·Ûóø!•ÁÐÉ æ÷7ühõÉ Ûýøc„ü.öwL–7N£¦ƒ^mŸp²´„H9eו©X%­<³h]ˆ­‰ª¼·²x9â¿l‚›u¸eÊú, ¢m {¢×»iö)'Za¾Mnr¨xVž{ í4`ÜóO:›2g¿« +Tš )i¢î$àEÐ:s€09øTØËÁ.ƒ+õ]ÇS`ìò˜ÃÞÏÏ»‘§ –Oˆ0l„Þ:ÁFþ‘èl~ïP´Õ·Çúü¤ÕýhG„¥Œ=û(r¿û‚‡jÂ6JçZ¼ïl9¿¿oé66ü¨íœ.ß'6¯·V6ª7kêòi2—ìvÞÖ_»áƒRx-¶’oUSÃ%ãr¯hDÖ3—»ÛÇÚÎúÑÃÊqf0®›¥¢rlEeM‹HÒ°ð^x]“–2O©ÕÌæZ˜*é…hfã(<à•F¹×½ó£Ì¦æ”ó­­íz!•Zyê¨qKÆ3 ¥èºy·;*¼?æ´»äZ¶Ó;f÷Ë£·Ä¶— +ÚÒuî½½r½-4¥ƒÚÔΖL»i^ß?d+ùÔÕìAÅzë™ÍÒcf}˜ê$ +kÑq)¶Ûh.DX¥—çÓq¡ùxmæÚ™öÍz3÷6Ê¿™w²/Ë…º|ô™ÙÜY¹¦ý)óO¯O=òiù³°ßØç’ÖûR¶œŒténªñBÔ~%êź~Ë¿iϛ٨ºœÈ¬½$2ù•ËRÞ¯n_DÞ6êõê|j%ŠÍ£7:²,¥«æ µô²Þz:häÚÑ•ä ñ0Ε—?aþñÌÆÁ›º56®3Ùn}¥“Ø:ÞH›‡­–i¦‡M5;¨ïˉuÙí±^8^°™+Žy­JõV>]%ø•·bÉ5'×6Ï:t·GÑL~3r]\³õ!ÁËþ½Ù6ó½§ÄæUã~]©E±Ûín”,hÛXJîkã¼ pÚÎ}Ä$#Í«Æ‘$?FŽ éêær)œ¸À(üð„½`•…¨T[Ü×ðsb»´É>m^iõüZñ…v¦Ü*û„to¤ÄövqM)ì¼n±~®·67ï'OˆIw¤¿ÓœÎF!•rî½ ȱ­ ¨ähX¦‡s…guÁîhÆñ^ÏV +ï‰B3}øY¬VW–rFíò|s¯ð¼­¼ÕGÙ³¥úq¶¢¨ûYóñ6BÚ4îŠ7/;cD”j}dúôáufµSƒ=Ž°Û^¡Y.6ž¤Ûj<±Ù¸¦‚ž¢Åg9q•ÓnJ™ÁàíR[?¾ÞA Yzk`ä­®%r=û)JÿÂE¸s8QÄBW ÑÄÖ8V*´åœt ¯“?»R‹ö³mÔz™Êh1[9'AÀ¤wŽø›AÊ*„}¦ZÙ œÆ¶SŠ-÷ãù7ãâ®X“6W Î`°&9g[îD(8\`ífŸd¤¶ÍäK vêaª°ÿnVéÞ§µÊŸÃìéSoX’|Xk”òÎ-2Ï)8Ø͵­k¯oct´u—Û«D¶sXˆ’Y8§…ݨC†:ߣJÍõëÞälƒõêäÓÚh·¿X³íĆv€ÈÆ>Qhò­aËn™¼?Sâ‘ý=oUÎúr‡ìä‹8×qbëàî€úú@ø˜J~­îŽžlùå nmÅJ´ƒæÊž‘­÷^3W•ýj©hß,D7RñÁG¿¸Ö;QJËÆú ÁùØ.Ö?>—ù¸AÚUʘ—ïWŽ{ÜZÉ^´û«b½óêa+šyñ¶šÊÞ–» Q…óߟ3sù4ôY‘}GÆêr¡™]yò¥Òs<ü†Ë <·ë-¥×¿2ìÃモFh½JÑÝ^q­Rß"P<¿&ûf)±½e¨øk¶¼–éæ[­ó,- U*…Ãýdi³¨ÇñWXËëj—V¯g¢¹ö”\Ý{ûÓ´vZ«ZæÊ‘Lý®{)Æ:Eò§‘“åF–~:¸Ý²É§›mRV͈eòkŽpK¯ «Ye c¯yÞð|ŠÛg؆·æ`ýë—H½ òé½@«@eEîÅéA%±¥Sv!LÞmqðÑ(°ÙÐ)ÁäH/8 +N—‰=Â×x®LËXg¸*ZAĦ<9èÑ&ÂFÁc÷;BëƒÑé&©t¹CþÜgEPÒÊOÅÍsžw+'¼—§¬»¢æ öw "€\‹‹%¼ËE&ʪßos8ùÀAëy_)½ åÀȾ9àZ\\ú×ò”à +è'¾*ö«ø•Œ2‹0¾Ewá#qU©LÒ,Œ2XÞ aÍPy*P‘6„5÷Kœ’§,“¶aã¹]øg#¶ÅåÞoû…þÈ ˆt?V)ì G:G‰i[…ÁšÁ‰”®1ˆ¿ÿP^ä7ÛHÉdq¬‹x¯ ݆¯ù?D†HÓ¸+…pì¸qÊ?á‚`t¥dOöúç„¿«ìܳ³ÝìEóõˆ•‡ýÀ‘­\vKYþXˆîFwŸ‰X¢Ç A6ŸG7KÏ£ÊjfC: 'ŠW7{ü(K. â„ µ$±ÞB*¾#8!¨f²M–Ãœ±BD¿øýLmÊܸ;*eÕ¥Ôü J ‚°µz’YL®5µÂáΧ&Ž’{v²Ýòe¶<ÚïGé˜ÿ×v)cGû\©¸ˆ{Ê`ÿ¾ò™=ÍçÎ@†aÒUÇ¢‚æ_© l˜õË‹¬ùppQ8¬¾Lë «ï=îg¯Ï‰œÌfvýØž:&ú9rl?¤\Ý!5_w˜¯9€tÁuèÌ4/ÃBó|XN·Îî I?óTÞV•ÅûùÊÐLUh!**C…ÂÒ¨†}“=-Þ¯Ÿó­w#½ÑìD_‰|»*˜l¿˜æE£“KzòcËíG¯+Bɬ³ÿ&ÍÂXuíÛ…×d|‡p®n¿Aít%ÿ–¸Í§[Û[ë W~8ùt‘Bö©’§#âàlÁOU#`<8¢4ψS˜È,¥by›Ò¹·ÎöÚD³ðä¿ô/LÔþpódADéåyÛ.ìÞšM¢ŽIÙÕŽƒÝž‡ã%…ÒX‘ûr^h^&v%§V*(ñF‘î@ÔTåµXÞ¦ŒëisùcÖ¾2KO«‘R±;~Q¢vÇbØß\[Jß[fýè w~–!k¹íœ0|ÍžŸ”¯J…xƒÿà{oY2?Ƶ3éè°Ö÷tä ÍA¹û xÉ¿=„×Û»ÏÏœ²Sƒ©ÃÜÇq‚ˆögMoÏ®ƒ¥å)÷¡l…½\C€´Þè´%B¤ò @ûγvoÊØöÈpŠ±±ùÖí§Î;WÕO>@ÅÌlÞ.õñ-ìïû4[Þm¼¼l ”ÌeØF ¶bcÅ’kâ„í¼ Çˆdûœ0U2ÊS•tQŸûr-³q¹’ŒdŠÞ^ÜhŽÏÞ³•Ëê-`½-•2*™Í^ÒåÎAÆBÔn—zÍY#»5+|÷Š}q÷ª¢Eqþþ•#»\¨iNù›»W˜ìÞWBÉ¥øY{£TÐï”Äöíãêd_Ðò…n„Ý›;z~Cx¼lãBꢑ̬Ÿ¼¸GTÅX,uóÌ4Hˆ/Ùÿìµ +é› ‡çŠ“~sž^žC6¹ÝÂã ×[:óJ®sºMN1Ÿàq¢åNÇêõÅ°˜X5šÂ‰åÑ’±ù´2ëן­æ^í5³~S»¢›êîvm!Š'öæÚ­±‡} g7«)ňž>ªüÝmœþéŠFäH‹@™R8¬/zÛWí£ +9èv–ŠÏR÷±Ð¬š ÑâZïxœÕ3Ùûbý¬ˆKÌ2ùý Õ^4‡é“«lå(ÛÐNNN~3–$íqöì"ý¸Þz²šã׫/‚xC£­4¡˜£U" <\–ž7ÞÒÁA•íÏAòk8ó¹Sw¼Wã¬U뿦佫5VFPLä±ÊÒg²ëÙñÀOríh_ñºpÏW˜òI2×îHMB/wÆöúémf£oË“Kcõ/¤¦i÷VgVÂ*ëFís²Ê]² Ùw+¥¼c|d+…ªœk[KRÖ>[™õÓó6"Là0T„÷[õ›{²i +ÇÙ‹Jæs’6"Ã|këx˜Y/E«ÆŽq³›?É_í¸Ò¡ÁÎs³U¿/ì×/ÎrÚµ2š@üS$Lé¼;Ø9²÷J#&3$¦¡‡KÙfá5ò¨™ëýTžˆC­U¯+”ú ³¦Ó¢»ßŒ/EŸº7‹µô]ß/yÓ™Y•å¥ré9²\&ëK<ŸÞGu´ANÂswT¬åVc Ñõã„zŸ­T–Ã>bÙÙw»=bV*FG” ó¯3d/RÊg‰ˆ˜Ù¿!|¬°VÑ]›!­ý‘-V²¦}ñjk/¹YdNšY‚*¨Jñdw­âŠg[*Ñ’v>2›ÛýkrV¶»Òò”.œÜÇÊè §Ý.^îm=h+_Z)ó|½ÄR0ÊÇþ Q?Š=qß™œ†`ë$ËÕÕQîl¯‹uÙ +Ÿ”â·g£ìá­ÜÝ:hj«­¨}ô2Ø,eÛÝE4‡è…`®òÖOòé‡ÂááfšŠjÊy9Y*ÊÍXi={±Èz#œ¿’6¶JÙíœíë…ýj;:ÁÚgq›Ç ३ÂáþbžpKÿÚ\ŸÝ–2Òè™à¼[+…W×d¢Â^B[!;G}!ÛpǺUWcqª\šó‰ß]e¢þÛ7„Rl¿“µßN"¥ÂÙq¹ík„¹\;¯5w§y{‘‰Aä“'`á^Ö,GòYòÿ^f3™ŠO*-øT*ª“{,ò‘Õb¹ËblX{F"% ×îdã&ogŽR>Ê)ñfejòÊATæÃlïítUÔœæíT${®ñÍ%|×ø~·€+¥P-ï>ìFw-B/c¡òè¨ôH°ŸYBQï(AN62Ç…ÔéÛ¦@µÛÆZ¯´_3ˆ¤E‰*‘iãeã†cׯØœqp)®T4Aì(¥µEOŸ 1ñØî7š¥Øá®-"iõü!?´³åZ¿IµEÅX|Íu­wARÊìõ?ù®ôÎåZݬšç§gÙòøR™§pˆJ÷äp¸‰šËýˆ1:m•žÛ©´o”ÖÓÚ+ÁÆpÕ\J”á>(Ä‹ïªD ЪYme¥gËׇ鬵;Ûèö«7™aüf±pxðøYp6Ÿ=„¡Ìÿ¢‘#ºÙ'ìã!m¬×N—¿ÚÚÚD¹ÚÛ+=“¥ÌŠSñ_ƸQ(‡áŠÁ9š3É7‹Œ·¶»{gUŠµJöºðª/·ÜËe\œg¹§4åS %EÄÅB¯ óo÷›Ÿ¥ìY¦¿~<÷ü¦AÃø”í‹B=_ý(¬¶ÒusS-:Å»æ~Ñ5Ob•2ÙñåœMÕ¬•L-‚-dÙBììά¼ì¥K…q¯%ωı’,”ºÙcr~Vâ¹Í$P=¾WWžã冬ÓóÅœ/¥ k¬^nÉ~¯D”ì.³½£cû3þ!ªÌãÜÊæQqí1Gô}CéxÆY9{ZbÛŸ«…ÚsAvÛöÞîísÿy¤»}f@ÞE³nž,EŒÁùÞ}v‹>üÌᔸuT9€ž6˳FšìÔ˵RTŽ½ùüb­pnJâ +hå¾l\}6³x$”¢Gæz o·2¡±ÒòÝk·Ðì=&Ðt4Õ~ÍX}Îëpewœê?eïâ–‡}pX)ÖÇ‹OÖéžZ±ÕµƒÒóÚÕz`-n?•b/plûûyÉ¿¾•VàÖÛòMÙí oìf{ŸÃ4xwœ[·ñ¶ÚcëÕ«coý>5úœÈ¿Ç¥•Ýhi¯ãªEDAº^J]¹=Z*Ökk ð"¸VvXigqÿ òÚ¦ÏÒýhf6ž’ãl%o>­³A4ìt²šíe’79Ú‹Ënd6/sfw¿"_¢ï—Û™4ïŒð‹³Ù­M²`Ÿ(Z,-ïõ=w¤TxÙ¾2ŒƒÖ«QIÞÞ)Åp2—~ë6ªKöëõ€Ìu‚xää|+|¿BöKbµþY–’]<Þõp~U&:Ag‘ˆû—µ]°œFeõÿ¶]?8¼IC§}p«† +c-àÙX®þäÛ£V¿ídý~©Bô¹ûòÂœ|Ãm/v1èÑé¾7ýñv±Ê›3tBÕ½9!öŠ,4tFy~úùÍ醆՟ ³j7$ +ïBÕ!Ó,>g…’º)ºb—䯿³_{ãP¿Mõº!§Ñ‚_phÚÝ+djùZ ‘Áܦ]‡@`Ôƒ.êN¨…>ØÕP»ú+„‰ªöûíV†AŽëo0½ýn_@yÝÐѺÕvhLf×kz÷†¡q÷r†¤æƒG–ûLº®ZýÑD £i•Ë£*¡ÁAcnM »eO(g»_ƒ7&Ô¬ ZÌ90·S¬Êã-rœÛ¯Bû%°&0ùŠw+„ÃùjíÊÜW¾ª¾'u3×vV}uöç>Tá5/œ×/‹Òkuç¼ØåUKÕáèÚ©Aø—o÷=0¯_ }’›ÿZÆ?]ï-æäÒ°aäg²QYh-àg=²y~|gÎë¸×íÕß½ŽS {…çžðqž‡.öÌYØåEÊ +òÕ> kØræE#øz؈o<5à aYg~=ùêÛ`ŒZôy¥×vÖ3®à®½v¾HÞôá24©¡”o‘N…`Ö;RfŒðÍØO3ÇØTõà°ÕýÚËg?B¢.áä¼5ÁxBÔ8oÀùlbÚ,çí«â®˜ `uäTÞÆZ·ÚjÏy‰Å‡!g$¢¶â9›ÏPDU„˜†_¡€›Ÿä¹ÛÛ ©‚U&Þ,øÛ|)pá·ÈH@+‹öu.’­zµêè¨ú«3˜Ï×Ú 9Þ~·N˜OÁÚ—˜Þï¡ÁoÆK›·Âi|Vqj`•tÿE„(nÓ<¾¡èmíý9iZÍ&6õY¤{>âÃ÷Y5‘T~î–÷ǃž6Ä9Â÷O«þ}SÄ‘ùÐ÷³Æ ðeÆ¡6 sûå [ëýä|›C2Ìý9kΟ«Á©~aOÌŽ¥Í0l¹0úXÍ}+"ÊÌ–ž¼5#TÓ—£´þ®5!ºÍ=:æEµœöXTÁT‰þK~¡{(望Á÷uvðUm¿Úu¼·¤TGž´øÖûy¯Õ¾é›¨ÖucIxZ'¼¥ïQE:øäVÔ±'჆ÇÆkß~㇋cûì ÷¾ÝÞV éÓ={£T¶råæqäHÝ8 ‡×RxYۻȬŸfFv«tüð›uz;~CÃÅo¿ýÇ_ +áðêÊb8;X\ˆZúQñô7ï2JXÉ Î÷eòai_+YóñµM>DÏÉŸEk7—qvq>Ëo𻞹züŒ“++øµøRß?€¯ø5§Z;ðõ”V®ÜÕ×ák¿f­sµO>Ä¡ïͬuöÞ$Ÿâ[ðµ²Ý]Ie×ÈçU¨´x±ý8Qɧµh]Ì·†•4|ʼnê…ÔErH>$eüµ”wŸðõ¾&‹/ÛŸ1øP%l=§¼Õø×b“™zÇû½”o>nýfÜý·b­"-–6 ãlJ=ÚYUséäY¶ò9Œ–Š¹U…À:öX¬-­ßã…jáp¬>!Ș*XýðÙHøM)ì«ÇJaé~]z¹’/~ûm¹¿4ÈLX:Û Ž…(üEò.› y«w´Pøui@@Y8'ž±´&Åd™+‡O¤ø£Åë.®vNŸÉ/ûÞlM¤–ÜyyÇRNRònîöî"{ÑØYÎÓ æK½~>% +Û[ÅåÞÍênîÁxÉ}¬7Ö2Ÿ·­œ@J^¯§„Π_aùåpx—6Kû§†qX‡{ë‰âñ‘ !f¢xT:1Ô¥qý·ßÒ‡áÄöh`rÛÁ:{†òÚ-DÍÎÃv +nýÌâ]³”ÃÛ•ôa3¡™{ù20›ãýáÞi±&­³‡·‘&Áä{"—´Ïïá%ã +ÜšgÚj®­Gk¾W„ö#lÅó2òQ/[‡ß¹¢ÒÜÍʃ$Þ9Á<®â«‹áøÉÍgxuÝ^EG¼’lÇÆá[¤ëYÒoÆu²È ã¯ü”Ú/‡—×ѵÂIxIIÔÂQ“lÝå­SÐþʺ´Ží´ïÃñb¹^=ÜH…狹ðÚÕÓU8ùP|§jÉx8ýÚÚ +K³ó°<4°-…Õñ¥Ö—7ÂÆZä)lÊσ°•9VÃvIÛ ¯_†ŸÂõ:ÁPxkùÚ ooœ†w®¶^Ù®œçÖW áü}ä9\\,…KgŸ™ð^´ÿÞ?íG‡Éq6|T>‡OvñðÙ²±>¯e[áòþ±¾”n.ÃWã÷ÅðMo­¾{Þz#£<Ü\šá§óöCø¥¢¯…kWåp㦿n6¶Âo½ê(ü±,í…;ë·ŸáÞij7üY¿þ Ö”½Åð~u¼é玗s‘åÅØÛÍåb"¿#-&‡Ñ—E鸺±¨ÊÇ…è¢þ¾~°h¬Ä7Ö»‹Û+ÕÍÅÌàös1ÿvQ^,=j‹ûûíÅ£‡½ÓÅÓƱ¶xÑ>ï.^Ž®*‹·éç!Û^\|.‡Ÿk2rsc_Yl]’:‘þãbÿÌÚ'£ŒV.Èby°YÖvÞ"ñVí:’,{Y½µ"ú²šˆXê0²ù²ÑŒd.Þï"…“ƒÓÈ^Y*EŽ®?¶"gçZ¤ò¾•ŒÜFâ+‘G»¿©žUçµÜ‰¼+‡ï ÑH÷"ÿ.g_—OwšK+kù×¥ÄÓîÛ’´qø¾¤‡/;KöÃóçÒöa?¼”/È+K{j6¹tœ¸Ö—.Ö†[KשõÝ¥‡ôÕÙRu=r¿Ô<>|[úxŽ—ú£ÝT4¼1ÞZˆF—oNŽ¢‰ñÊCT>©v£Æ(›ˆnž,oGsR³Ý};‹nÅ£e3•‰Þ.Žn¢OíÏhã½eDߟ_΢ýf½³nTµå•Æùrj¹Ó_ÖÌÅåõ3ù~9ÓÊƢ˻ÆÕáòñÍGw¹²¼¶µ|w¾[[®J-mùÕÑï—»…ëÔò8¿^Y^'W’NúfE»zL­lœo<¬ä{ÚÊþi¹±rö°½³rÝŠõWžÆoÇ+Mã6±Ò9Ú^}dvÿ[ÙÞ +ÇRMû&fØÆzlëÝÅŠ¥›Ø1኱J·¼{x¨ÖcƒþQ¬“ŒØ(w¸_É9õx:Ÿ8‹›GGÛñ§a*¾ûYÆÏRøÍAþ:þÒ Æ[Û2Jü³mé«ÑÂgb5µ|Y5 Ÿ«;{ÚÇꞺ䬞/÷ª«·=çiµÖ®>¬~´ïWG½çûD<þú˜P¬Þsbco±–(¼HÍÄÉZ¡¸>¾$ª‹ÃhâýØJ%F‹æBt-öÉ­)¥ý“µÍdï~­8,µÖNëÃÈÚí㙺VQ‹kÚÇur±[ùH&£Û‰¤¹žÌ&³'Ÿ×ÉÃv³—¼ÒÔäËÃõqò#uñ–¿ÞI©Dîþ8e,×Ú©Ì[ŸŒ’:¼Š]§®Ž6—RÕÃ˽Tû ÛN/¨›éäÅE-m5"z:?Þ{JŸ®ÔôÝåÁSš¢žþ<{ªI±XvSR/#ÒŽ^-I£³ˆtõZ¸–jçª.uɸrt?v´•å“xZÞ*Çßäýªt,_öLE®ÉùžÜÝ;»R¢Ÿ­¬¢l­$”í^±­æŸn•›ÕÔ®Bä`Sù<Š¬¨ñB¹«ZüEÍë/—ê™™;Pí¥Œú~ôji‹×§ÒBTKò mkÓ\Ön‹ÚÍêÒHsn—†ÚP]ê‰Õ‘n_›‹ún>Õ/Ó§q½ž|•ôϵUÓX]Íí–ü²g”ré²Q¹(?µv¬m|Ú·Ëæêƒf˜v¼Q"çÙîÓáyµ­|˜AoÕ6àl9>½¶6÷öúÖa>§YwÛ'Vë0ónGN3Š­8‡gvnùªoŸß6íg‰ìwksm}µu{¾¾^Z žµ³»~ë¼õ×[•­ÂBtcé°ÝÛÐò»¥Bae¸Q)<m4®V6FUézSŠEôÍLþ½µyöVß߬ZñÍÏæÝËVrç6¿µ½ÚˆoTßê[ÏWK‡[ý]Ý^Û=lojÛ'‡«ûÛÏwÇûÛýVy'™ÌìlŸ wNûGg;ÕÒZagЯڙôén:“5Õ•L9 g~6üôø‘U«÷­l±qûš½~½nfßWªo¹•Í÷œ}>îäûÒ ÷”Ûäúƒzb!šO¬ªù¬r´/Fùfm÷ª°t3nÌÓ“aáà4•*<^¿e ý—ãr1½¼Ý*æ¬Õxñò!œ-¶V;·¥•ë÷ai#ýf—NœËRít4Ü ¯Ç·võ¤õ´»?!£ì>FëÇ»Ÿ‰ø`O²ó{…³ÁÇÞÍ[q{¯³Ú~Ý_«nïgãñ÷ýËûz~ÿÝÞÄÇÊÉÁ¶I”+ooWÎöaìô~x¸u÷tuH9›ðËÖè(f n¶ž•ÜBô¨¼’]=z»º?ŽËá«ãíV.\¹l)Çï¹ðIÂ|};ÉÅû“k=rrÒÙ|(œ¦ŽK§…'I9½_¯~?VΔ·jälça|ö¾j.߯㹲¶½Î¾6/¯ï¬èâõ ¹]¼1ößoN±›×ãúmâeɼ-<^Önïë.\m5ïÖߎvî*kë½»N1~t/7‡«÷‡êëÓ}㩶ýߪ‡òÑÖÝÃC­¿ý~L-/D7NÖk—çå£Ç^¥m>iU#òt¾k>µÌÕËçÔåeñy?&›Ïõ›·ÄK\Û¿ä»òÇËÓý¨^]Ú¯=Tw2Wÿ¿·÷Þk«W…¯À÷€ƒÁ¬^è½:t„„NHBh¾sÞε3Ò*£²ŠËóîýÛÙØ’5Òh4}¤o×ßö¯?7÷öo¦öönNîNönÚc7·ÁÎÇI¥v{ønžß¾lo^ßyãïöoöÝÓÉùè½³e¸÷{³ß—îÿ-Oü²7Þní~;ýüõøkÁ~°-{óa÷ðËåÃãûKõ·½ÿ<ý{oòéä÷¿ÇvûóctúÏþñÔùŸçhŸ½½ÛÍ¿‡?O_6gÃ/ÿîOVýÇ÷¿¯×ÿfV÷ƒgŸæýÓàéÓÜÓÂÌáÓÓ…µ¶õàëª}dJ³^ü÷ïðíü{É‘tx²Žbî+þP_òvf ³',^¯ãxŒ•þ|õlhí÷~‰zìH}ÚÁ|~¼lß·Ÿ•Ÿ¬­ˆ^º}ûGxþ&çèþéõ½6?i9½~UϦá>Ñw mìUza²tø²ÇGÆ þïÇîóÞu|¿ß´øŸæoÁ¯>߉/Á£Ó†¿ƒÄmæÌ¡ßh}9÷ÊoÌŸÜL“¯ùß6þe93ûSvÒ°ŸüŦíù£¥•_áúãÆÐÁìõÊ/ãl.iµ³Þï꨽1[˜9+ 1÷8Ó÷°:þû4]ý‹æsê°:þõt¥:a|µŒÉÙ³:ïV—G÷wëý+Lnåљ߽š³—;Ëÿé;»;ƒˆ¦­ÆÆåýr¥ÖnÏ]?¯ÿ;ù{³tõuqqüe­±úmyðtrcñûöâ†7¿¼v4ïœ.L~̼ï·žŒ«½åè.Ÿ<[©1:àÍíå_ë v/ŽŽ×fV¿˜ÅÉ1Ñzo·­÷ëjã{}«ÚX›«ÖïŒfuâpL´`€•þ=KóÌQ–挓ÎáA‚Ô/¬uîutæ•Í‹ÝŸ„ëº>XŠþºÚšg nN¾»oð×á«Òð¬=Yõðlìyó™¬ +·¤ýãý!hÚ­à³1·^.b† ÖíoÓC¿àãú?øõÙ +]éôû÷öÏé½]œ7_ ºnþpN·Æu@a_~>,m¤`% žõw²šô¦}1Ùø¦º³4:tüú¯Å€VjØ÷¡ o) èÆܾÿ|¢ꌞ6¦ow¿ê€ÂZ>¯¦êcµ™ê•n­Æš1?“Ô¦…@Ï~kGKû (ËÖ:¸ö:UÛýóõ@ t}襕 td«å|ÕÅó2ý~Üþqkm!ØQuW¿Ù5÷c~è¼È¤ôsjx.º72"í©sä¯ÿc@Á +[¼YwõgûçÉÍAЩK÷øîaR ôbâhG,ëOþ:a` À}ÚïSw=Ð=PP"úU€¾Ì™ (rKy­îo4#×õ@ÑŸ™é'ýJ׮ݡ7ïó« pRÚÞ_Ê@°7Tsw>èÙ¥±ört¤º><û¥uÿû¸RÓ­ÕXßN²vuãËDµù›]ýñ¸& wpô}daœ¡w, +P8ØË'÷mìÕ þ« ´µ¹ó3z6Q—Vê»Íæw=Pçû’ÑzÚ‹FÝü Zg·èî¿ûL ÛÍ·×q (Bá`ÏÆaëíStÛ¾h­­ÍWµ@6ÿlf=üê]1‰¬[ëùŠqb\„z ­‰¡£»Ë±i-Г‹Ùw (ƒ=©ß_¾fýj|›˜_ʺÚø¶´—µ@¿]À©ÌZëßÁ£ÖRОñóúª¡ºsüçqª¶3ž)–‚½¸8½Ìúr?_ÈúÕ¸Zm­è®›•ÚàéÇþ¦n­íöÎÄPôÆ•M#[µ9ÐëÁu‘'Mµ?íeŽ3 ‰Üg`wgëo«§ t¶-mL½œG@Ã1 hí×ù äÖÒ™¹)²¼ufuç|ÁN¨RugbøÙžÞ Ëò®®®þmr óõÕq‰ŽÜ5"ždO-o‹š’3zߘ vÎè¤ +Ô« ΞmlЭª´j-íþŽ€Ní7¥•‚uö2»úÈÁηZ"‚AÆýýý¤j¤ëåíã©jM=ßd´~~©ÚwO¯‘DVÚQ84sî½þ×Îhˆá럌ֳ=cufw•·&b9å0ßgÖ‚m±v•ÁŸ7ZóNVë¢ÑºÛö²ZA‡1¾V¿Yí«ÆîíæAVë¥q¸üø‘Ñz>nž U#ŒiÚ›ÆÑÂ×zÖ¯]ã¸úÖÔ·ÎTögÕZu¾ë[­÷½êÈèÖN„1kd®¾!¶/V› s¬]á}Öǿꌳõ—´.OŒ £x?²ÀêwG¥—÷Îæf_kh%Á†\i (ï~‰1Ÿ¹pcø¯d¶Ûóf ŒË43ý³•ïkgG+ßWgšð±¶¼Ô¼]^^šÜnPëíªögcÇRŒÀ²G˜1ÈøZ:ç)ï›ü<€]yú‰gãØ㯙Ěšü3{3 +ghpõ=ØŸÚ“ô±ö 52»7ÁÅZ:„áR îo´s^ô@³ÓL •ˆÌ;‘ËS ÌÒÉ +ª-Ø9×Y@¯R Œ’…µ®O„èÝ—/C)P¦ý'@m ½¨ýOÇ@×ÿ¥@ÑJš¬Óµ:‡Ã)P°Íf&P¦ýgõ†Pûÿ‘êc,®õ<( ÷ÉÊÊ´Pf%yC¨ÿßéÖŠ@ï³;û§Ù@Q£ ú˜Œ`Ô).³€î+¤445gEt>¥Ù}}ÏéR#:?Zùý +ï9ºÇ{r~aµ<Ñ!&ܵO°µGΗS Óà8s‰1k'?[n9~lŽ'ÿœSs=ò½ÒÓtÕxýGû`®}yÁÉ¡6~¸@‚ßrk5¼§þ7ôU‚EçÌgm¿5ÖEò=Í…ëÇ{ðñK-úçú”hÌ̹E}JÐý|Å\ýù¾NL¦<·Z‹þÿú±õö˜§+ +\&仟b~iò÷ýJ ÿ’œ76¥IISJºdLi|uœý§2Â(÷¹ìG3£HÿÌGzúO´>fU ëCžÌW¸W¥$¢_ûç `ÿ@Oþ*í_dñѼü‹'ÏmÝú^’M.³%{§"+c(¦[N—¬±¯Ÿ½êðk防Œ_æàYÑÉðÎý–ZÌ‘ié“3m¤”ܲDÖÓ|QXÏÙø+žù”€dýîÆ*.c L8E ³ÅF¿0ºÔãîlüCX®2ÆÛÙ?î˜ÛT{*WœT³·þTrÓE³´ys»%-Ms* –61”¿4þÏý^ó &p¯‚‚åõ½§âU}á«ÒûŪqÿþø-Cºû-hüU”¶žÎ×·¿0Ù‹òECä«?V&;GŒŠ–ËvÌÇ4rÚ¸ÿ8žŒh'¡dn¡h»yÏùl¨J-w0ñÜ™¯Ê¹»±ÞJ¬¹RâÜ­¡i3çÜ]~Ž€®÷u<áIr¨.¥äë5ëççÒvцâ?Ñä¹Z¥ XŸÈíÔ§„íãKõhRtf ‚½^3~-7Ï3¹»Ÿê`\;/ÜçMÙ’Ç ÏiSÖS©e2ÖVuúž¥,kŒ»DŠ­Ë|UL…ûw¾[7¯‡G6$±Äõ1Y*ÒžÃj²foê锢¸˜nRÖÒ÷pK/+“Ù”›ÒPŽ¬L7/_xÄû÷8œ©ÐU¸NQbùþiTºû—h²ŒûëËãþƒqÿ÷þ$²^û0˜ P¤¡˜ï¢ƒÁ~¼ö cz]GóšªïÆ$ŽÖ)Æ"¿Vì‡ù˜{“ Wkéô"“ qM¡RR;~Ø(o +ªšó± Œ°l”T°´§réôq°O§r£ ÛÊ£™4ö°aOo·ºÄN4G0=¹sì\6K› +Ùkal¡Çmº4Ò‰DÚEþT´)àÂDôº%NÅê'ú‰À±oðŒˆr¶áÔ›^D}̽çH»˜T*‚mÈÂ’©“›P’ò…“JÝÙ0›¿›À®WKÐb%Û­Áq÷óý¹„S#e£:œ”Nñȱ‡³§ô1T`ï—e›3€Læïfy¿¾ÆH¼/= ÓA®¦P©©T›¡ËÏ×W>̱¥¿k¯Ŏâ;ø»ü̳ÉÁâYÿñ&Z|= +9â™ÄP)¬G<“p÷éoªGüiK<âFZžw)òØÃSƒƒ=úž¶$;/M¥Ö™Q1orGA‘#@tÉê9?,ÍéyiöüÉÑWã+ppè'¾eÝÉî+ä¾PN^z^“©‚Û¹ßgÞÜúù¹-•ˆqK &ß_Ã9ÿV®ÇFÇ +Oƒ¨JÏOí}Hªt¥fÏ×J8‹Ué×íLŸY¥Vš¨æÍZîl²Å ìQÜ–¡ž"MX·´T v^ægGËÒy¦D~Ý–Å_t>µ_erç(–ñpf,¨æ”òõåz8·Sy×=xÝ(’¨Ó"¦X—E숂.“’Ké²#yh„„bá¯eø®žíì®Ô:8w§‡%â:Šý–ë{O™ò®C•x[sÏK + &ê ]ñd<4f¯¾øeå©÷<Ž2–9J¥ÖÉ8.gCr{Ø8ã}YÕ„4 +­)+ù’Áše…hœŸœãUõÍÓ£Ž" œ[fù½`°¾ˆfWâÌâ°Oé_&*ï¾XrJ¿/TÍãh`º¸£Gƒï¦d5ŽÐX'šÄéqG-+Z jeß8ÚÉK¥Ö¯ –ÉÑÊkJè—¶ûpö1ŸÍ‹Êž}5÷!“Æ +Ç‘yHþ(DS’Æ)Ã>Šg3)e¡—ß/Ù˜Šôº€¸a¼ÐËB‡“Z-%JôŠŽ +ÚÔÐåWÐÉ•\ÝÖÐåçCs¿eï5tbB²ÌàR‰|¾R*Ù…èuÙXoÒ”)–?)2¥‚ü)ÐuRö^©åTL‰.¦®óWÏWdOo1ž²lÛ½‘ì\râ…+ƒ'ÑÅT€§KrL|EôJjÑ`råá%É:±•Ô==½‘&Ëëë¥r®ìúŸNªâÌ›’Ê)É‘‘ÒH/ò\åž—ÒEs9j³J®ºlÛ‹Õ^¼‡qùÔ.1·‰çªL2ˆ¸«ecs•b·ñÅj—þ*zöaq?†zÌyÀb·¼T$²¸Ø-×c\ÊÓ‹Ån=\V tþòˆÉIÉ1Cô”\¶B«L +ÖÜõ‚¬ D;‹Ë±‡ë³ÈCR¶N¦tY"2ª³È/2]Ëe—qœQí½V97I†ãLt¢¦;Δ<“×1«ýP'îÖ%É&U vT|i‚Ôñ]|æ‹ÉNêøŠt¯1ê´ÊžÒ½¶&²÷ËF¥iÙ9ð»šͼ:¾¢øòu|µ¢š÷êø$¦V·,?XAn¨ŒÊl²‹,¼% “Á²£]`¬ Û¿3ŒÙ½,RtüŽGœ_HuzØ(ªž)¥Ën¤>߬Š¡ô°gU„TË hãbÅ ‡n³;?l0SOÐ-»5ö>æ (µB´ç¬!¾”5õ3Ë°ä,‚œÎÂIQ¡ìPãËÜd)2’c†eÖÚ™Åg¿%L¸ü(­’+%Þ2b•.É)» ¢TþúJ[®ŠýùþðQPc%ê‘™•>«%ði¢ÙåGYТIô±Í¾¹wþnJÎQëäÄžJWÎa²ñÔ™{'«rŒeAwäÞé¦2.­~*5©N=2Ñ”Y)Nªz¦$ß©Ò=žJxd*™e¢GföCöÈ`IRY ½qZ¥±§­ž=2öðT}¸ÒmyŽ¸[ù™ÔwQ"‘ïi«D°8§Þ{šÆ#Óé}J¬ ­cŒR‘ŠehÝ&“m=2Z/\¹ú¼ŠsXŒ/«<°“YžÓQq÷ôNªÊòëvQj)ey›)˽Ö$Ní׊³÷KX-öü±_¢t4Û¥C$òvÙÜÙÜ¥ê–ç]”£Ðí.ï “î„ĺºìÔ…êêD&›¥]ÕÕu–˨µ’¶K–d”ŽŠ¸Ah1%g×I‰zÀ#Gä8*|7Z”£X.VVª®ø¾¾~ÔÃ%±$¡"®óœ:i^’´ë†Æ:¯‡Óg¨ö»®· Õ²õp©}ª‡ã4V&Ï°—z8…[–»¹¯Ãz8V•ÖY©FõpÙ9ðý¬‡c4¦TÄ¥ôuZÇîƒU*⺠묟¼ô¡²Å[Áš3½ŒŠn‰ƒ•b%r"a(«Ùéc‹‡e®üˆ#V„vÜkHO™%Þóµl6¢ñœU'^Ãévi½²qrŽaÙ»;Ø8=_uÁF¹løaJªöl°›ì˵7Qäå7Àú”¬"ü.× ®”5©±é DùBûÖ¿ŠÔoý¬HýÖŸŠÔo}¨H5'úR‘ +ãô¥"ÇéGE*ŽÓ{E*V¯iÌÃÄâ+¬_“HÎ¥©J’‘¦šC>†—mõ^¶;òkegÛö³ŽßÛS²ð©ëR¸îî‚î´Ž¼_©¬?¥pcrQ·UDY¥pveãê¶NïUèw)‹½Òb¸’¥p%ÛwƘ²j«;» ŸÄ 1ßPÿ.O^]Ýc¾Y¤ØdìËÒéÛpgFxF‡ú¨õI‡9“/"ψ%•¸Çö²Ôýjê5Áb5úrs2Gèu&`JÉMß1”®rPS…}‰Ýì¡—dܱG.ëÃ>¾~ˆO¥·ðñËêØö´Y_9øV?¹8¬Ž_ÞUÇŽ=ükû-W'¶/]còôÑ„ÑìË#pìqÝR(v«ç»íM” +ugï#Ã/ôùS±Ô­R«ß NŠÑ–|»ˆ¯,çëå™»c9Ýu¦’kž¹+~g$ý=s—é» ÝuéŒIŸ¹ë)?¹ô3wùùÉÑCwy©tt•ZÏ9P%ž¹S_šÈHSèé™;”•…Ý?sWò^8 C½–†l÷±Îâu»óÊ¢Œ¸,­çZžíTtêrÊÒ•Í<ÌɵÆ=ï¹v›åþ–ÉT)x N–•ÝT¥±·é +˜uaYüF*`Çï!‰Lª'ªK†2Çê0_Ô•tèš×vu˜u +nZ+Ÿ ÅtþþdC±Úÿži °›U¾ò±ç7&£›(z¯©e£d‰¼„[–§ð–z'±OMò¡b]5—[–-¬ý{_W +kÿÞÆ’Êz×p°2OþVÊ~ý½+Å̈ì’^¡È<|Î -i ÉÜ êЕ œ¼ôån€e–9Ò{eÊq?_0<îç †Ç½_À^ÊËÒß;«|ltYÈ Ø/8NÏÁ]>Jï·ðq:QâõÙüYºì”¡ÎrUñÄŽJ +áÙ«ZÈ€úXŸŽaî w¥ë+{záN­}ë×»Õô…»îµñN^¸+¨|ïC={)¯÷cXüÂ]/åõ`KÓ—òŠ2÷ÊÕ‡è^¸Ëâ0% kK¾p×Ý­æë?Þ:JoÊàc Ü”¤—Vs`(»RpíNùÂZs0û•ôòo=¾ïKÔ^”¨|ìCaí77«óœ+6NùÂÚLß8§÷ÂÚo÷ÂuXß®\T­T·Ç™*e_ôR“k°Tê8ÿ^Œ!þªlñ1¼lçÃÂ"&YŠæ—1uVÄ42{S÷¥ë‹çp°ßê•òeL#³%Ne¦ßRÄX±õ^ή¼l§|»2C²MÕ›e‹˜*Z MžTŽý]J1ráXa”¢Âw½*†éKywŸY[ÛésËMùžjYtZãzó^6b“îKf+ ÖëÕTé=W7ïýzîq¹);'{x)ïô²ô+ Ù·7œuY㪛¿ ¸ä;yÂ!=½sî×g žô.Oë½Ý¶>†£"½­ð kûŽðŸ…j㶹…e}ˬ¶¯i·Âádë¾H“‹þŠKÓ¢jÁö»YkÓó)¾Ã6hM…ÏÜ ~ɬ‡k^5'DK\|ènÆzÈ~q.çm=çì»(«æ`[MJ’Rí߯ÃL _Ì­ËÛ, w (‘bâ;lK‡¨Xšö>ôû>«.ؘ{~JV•¦á©Œª³ËðÁ³y/Î…FVåŸ74|ðd]¤RL.ÃË)8\{w²®_H@™¹¯û¿³ª›y@[#™@ÛïÇsU"_”Ú¿…ŸG®އ1xöW´£ËßïžòûUj¼çÅçÓsáˆÞÐÛÅêéna?÷wDw‘ÿ„×%ýX”ÔÎØ#SRDçÚg;›%‚N2Ÿ¨-&j«r<ˆ—Út—6©yÅlod-ÃŤõä<@—è¼Y¯˜•}å«\"f¥¸l-û>ž²š.×aVú“ZÅð¤K¬ÊÈ„ÌÇSVjUUic™æUgUi˜¡™[•Ö=ågi•ÏíÁõæ¤g¬O²,°Ä­0O«ä”H6›rc[‡¥€å²´Jœ—éœWéùlÔÜX‘YêÞL¹Xí‹yU}3¥ ìÙøGYçVÑÛO0Ñ“üT˜2æ/ç0«}ˆòœMd'Ô”ö­öâ×JýcXØsÈk5ž‘[–ª,uI©ˆYV®öïÊÆÕ4KKë‡)ý$w|âM5¶sc½I¦ËªÙëµþÙÈ7ö`ÏÎàøÄëÁ¯}»ì +%]…/½.W|ón{ ^åged£îõÒòÕmÙz[™;"ÄRÀ¢”øRY«¬þ%(¸ç ƒ²¨›ªNy#ïWŠA‘ìê¶ì{Jiã¤~g&ëwú¬pû‚úýÒ¥œùoÞTJï î_[W¨S‚4Ü8X‘™R~^Ìšû¾X'ƒåR|‡+¬åéc½Ü9"…­_ËyÉ›T +ǃeê‡Ú*À²Ælt^º¬,[XòVÌ!ÊÕb,©û*ÀŽýÉ]URxy5€Ê ìCè«ËÖ²l¨®«éåÕVj¢hé¬ +°,>‘ƺ¯$''·Pc#wP˜«,fGßò«wõ«*|”Î >Ë×ÁQÝ<ʧPr~ÕV—¨É–Åó(_–®dápÉGùûåãZßæü(_ž®òeÕŒô÷Q¾þ¼_Yô(ŸzßxΤî3OïŸMñÝP½½ë—gå>mõán(ö®_Ÿî†*xׯ£»¡º~×OXšòª_çyJúwýò½Bš÷+»z×OYðª_·wCÉïúu]_Ù§Z^“ØŸwýðÙ¯úeÝÒ\Þ¡»Ý÷ÅJ¾ë—ïsH+¹z{×,MãÎíøýÊŒwý:ó[vû®_þ«~™¯3tø®_7ª¿ë§'>%G±Û‹³£wýŠ)¹ïúå :¢õô®_©ª´>•»g½ê§ñó—¸ðF}ׯëü]?]vRúª_ïïñ•˜M™÷øʼëWæ=¾ÞßõËÕF¬zy×/?Í-óšßõË·Ô´¹Ö]¼ë—u¢J¿dTæ]?#÷U?!’Øû~ù¯úÉ~˜Ü„ëœwý:«Jëö]?…y¯úåé–¼ë×ÛÙ/û®_~™dÄù{'ÿŽÞãë°îA­îí]?a”Ëœ_Oïúɱ+1±µRê¿ë§³YÓCȤXÞõË­3Š*†Ê—å½ê—ÅÇ:}ׯ3m¼Ûwýò_õëå=¾òkæ¿Ç×õ1FùQtçpéwýrý_µJIÕ¾è]¿NoÔéî]¿|ƒšp˜žÞõKÖ¬}ÕO´^»×/_Íá”Üû»~yU¸Hcýy×/¿ +·ƒ÷øzðf ïñõÎ3^õë0çJó®_™bø¬wF:}×/?;‡é0}x×/ÿU¿l>ÖiÍSÞ«~ÈÇúñ®_¾Ùž¹/¾ë—g¶£ýÒ¯š§¼Wý:´+3ßõÓÚ•Yø®ßõËš’™ãUèü]¿üWýrjE;z×/¿ü‘W×öþ®_~A¬¢uù®Ÿv_’Wý +<Š¥ßõ+S]Ûû»~ùÁéÞž®ßõ£çS}Õ/;ß²³wýò)‚xGsïJ¹WâŒì»láÉ…ø½¤l×î¯ã©IÙµ ßåd²f%ÞÇï‹I…‹=½J>,Øœ½åôØ ¯Æ3E+H*…1Ûöy9ÂhtáÝ`åj5ªHÅZmÌ;çÆäÚek÷Õÿ’=ógLîÞK§I<>“}Wõ§ÔÛÊ–Fì—_ª£3;Nubrä+nWªÃ+Ádu|ta¿Ûª6öw°ìvæ«‹ö]uüëéFuìmVzý$ZßÚ'ro«|sØ[6§—ÿ\Oš¸ªË…éVµÍ6gmÕúÀ_‡¯æøÃÃüuòÆÌzcòq°É~k‡O†QoN²Èëì/ôÂý½ƒßlŒò;¼5ðc#ú8UŸÀÑÇ– \àcõöñ-4&ÝMcñé¥õ¾¸ýíÛOcãfótírô|Û7ž¯Ø4™-¶=ÖX¯}®Õ767ÍñåÚM¼‚íqÚplÞ% MÚð0Ûkx`½.1´ Î¿>5 +[64jÜÕëS²ÖI÷ sõbj&nX›HX¾;|ךäJÒäú®~àÇÝäú±…#1ûå#Œ†ö„Æáÿ>nNÄlæs,¢˜½V“u§ïðñÀ°~¾ß˜ðÏ~<åÃÚóGÏ|¯¬Ñáæhä5&Ï6DZ½Awÿl·É ØKLß_ŒþZþí­í.ý †bïsòÞ͇’½Géö0âÁdFŒÇ;6Òñ*5ïxvjyñøËôÕòoûuñhñù™Vh-]0b±FoOÿÄd7VON7p_®Òý5&oZãɆ]ÃÉh>ÁwMDÐ8{µ|…íþ8°ÿ“ ö ýpŒÿµôÝÞ@å L€™MöW´µ7.vÁ_ÔþÁ¡9nâÅG&ìZ0Â`äÅ:û‹ÝŸ¼<ÍçñÚJæsŽçã†'>•ßÝd-7öüñj‚WÐà}ŸX™¼žù2õð-ùHñuØ«ÏÈ(ûiìúm,úöq°2)åTwäòÓ„kþ¼Å_xÿù8Lc_Öªk­ÏŸÿ£+ƒ½̯í˜~:Hn§À‹"ÂÁô©iœè rmE¼lN«/“«ü‹öÒÙ9;Öì^÷7>÷ý·³ó1›‚Çédû¬2M‘£Òy?ÿ«ˆ£§Ø0¤yøš¿7ˆ¾ Ž„› ¡6S½Š‘àL¦HçWxq¬¼ +Ï'(ø<ß­Ô$œŠHÏGj/ì×ë +®çw°gj¸ýƒÑX„„hKŠ‘0on½G´¿4º `ãgsðﱎ*µ”@Çù0""öw#$XŸ&EÂíî×,:ˆâòÉý/ÉÜ)åÁbŠÓ"±÷£[ÅYt4J Â|_J Á<`Ý)îá”t¿uº*M'¶PuM×énÐ!Ê¢’åÅÈÌ^ÆüÔãs<ÄXM"ÅCæ`-¹ü)%lccåð&¹ çYìǔӑ肣¥}éÆJciÏ.iŒòÜni DB³Ë9Ä4Ö㉯SÄCŒ±lLä ¡ã¿º9)¦ ‘qâËîÆhº•ZwD5šÉupù¢ A6´«e¤ÛÉ‘ÜÙÕÁ2²Wö*5yˆâóYYù± a½_ˆŒéÎ'ÇXYLŒ¥ç³£9n9–/‰JÌRewÜr,¥ËÎåç–cÑ¥n`Ï››äãÏ©­ô#Æ+§–·Ó/À kµ’ ÑÛÞˆ|¾ñÇÈRK>ò`rò‘G¬’µ4¬‘¹úFÚ.ά!άÁçÅ<÷ïQ‹ÞÝË¿ñÞÂïÕ‰`â;úNÁ«ŽïŽŽ33ã3‰Ïp,õÙWŒf¶<9`°û…€é­©%JŒ×ÃO?¸+jdö̼£XiÊ é‰Èzw¼ãÇÉØ«°1’üUçþ±û÷Ç1Ñ90ÎÄ?.7›Ñ4£Ÿáª±Ýõƒz»ÀÔ} Û£Ô~\'.Òmh5n“† Úpáƒ%ž4M&ð~ÚÃÓ‹1<ÉüG_XþÄwJ|ëËã¤!ñ.1ìfÏ4Pãéæ}Bf÷÷“ùØÇÔüO†¸d¡vønaäý30 àv@c»mè²;Éî¸sÇY¾86¹ •{óöÎíɯ‡h¤5sC3C̹ ÏZ©Ï÷*†¢÷ÇQ}LðÇ%ÎÅƧ"_:^¥–?bb#Žnûa°¾¶0r´²y7[ý­§fL*GVºñÖè¿Ë!æ¨Ã}9»vˆ÷”8ov¹³4òÜOFÎç›SüimŽÂ_çfä=»¹°æÂM öêæÚŽÿºs"l´µÜ£ŠaPøx;ÕÏø¯ “øNGæ[s±ïôÎ& Uû’ŸÊ«¯oµd-O^ëôÙaýqÚL€Þ2RA(†¹n.Ôá¯k3þë.õùÞFÞµ¿Ž2÷ãdmltnÚ?™üºvõµÆœ¥Kö—ËwßØZo¸èTõ’³Ï½¬_ŸSï)ƤF¢ÈO䈙ÆÞK„ÅÓGbÙ<ÞógpïKïyO0æ6¹Ñ4×áÚü8Õˆ=|À©v¿818»Â´,Bl2o$ºþ¾3î•y{ö†Á’ô½/qÃvoxKÃC<8ÄŽϸ{ˆ R)!‚ç±1n46´rß^ú\m|m}[ÙÞ¬¦YnÌ3^­ÏVj«ß–î&ßf&7¿o¯|_;9\˜ü0^˜,Ž´¡¨…øòK `-Ý…Y; +²²}é&ÌÚQ_Ò)fMù÷œ?š6ÐÌÍ6H Í-OTji“p˜6'IpµWõ ˆ +¢°ì5ª ü'êA¥VBœ÷¬Dž“²#v©DÑŸTAøOÔ¼GPþõÖ"*ÿ‰z@ÎËmD,ÿz”¨ õ \”l*K8DììçÚ'ZO¿ÿžVÇO.V)~;­Žî|¯G|ósjYêðÒù£êèŸsÕ‰çýÈKCÝýðц͞qV—ÄÊÇ’|r`™Q§ë7äsgmÀ²ƒÈª£`¿üHB¦#L3@nÙˆ¸%†[ ·ŒDøˆ5²Pýɶ¿;6ÎlgÙŸüsúÑä +ÇŸñ©ËI>¶º¥«Ÿ{kg[Ú}l8(~;e­œýŒŽ€ÄW™æ,•\¾ß°ß) +`6B%¿¾´‡èlâÍÕ¡ ö™çE‘&$D@Ó7J¡_î)DÍŸ>/ŒÎVjº8=?Ø÷ï£]úùÀ§ÇØ,§´5*C˜ƒ²sˆâþÊSõñÞ–±ÜœHTúE'Ë`iÓ½ÄÊÑË7Ö M£)݈0VhyO-ãzf'À>ÿg´–„¥i—Q•æÄð¦Éx\ßXj$ÏcTjcçmûôs4ꉯ¸$=kÎÎÊ•r]IO4Æ\î…ÆZN3 “Ær‡¸X0z¥±‘ßõÞhÌxÍ ±ÒC´ã…B`‹iìÂoö²˜[<©h¬ìë­Áâãš›%… +_iÞ§ŸÃESJœË-Ü P0 O|ÞÊúëTo»Zm>*™|É[ÆÆÄZggC™ƒ×2{ËøB¡U0‡öܤI$ò†óÑ<ž!CÜ\Ôz“7F,‘sg‘3‡W3›¨¤9L<:‹Ûa¢'Š¸˜m¦¨$ܲ@Ñ!¼eíÏœƒÊ-oç7çéÇÝúqÿx1ª%qMžð!Õ[ge¾qyºòy½ÞZþ¹%ÙÑîBêxúÁrfyŠmò–“Ï­«ó¬æÔM²~wïŸ#Èéã„u¹q7Îlnî™fÎ,õ;Êp>äÖtì]aå³,:À>¦ÑÈêfþ ˜+ÿøóýžùãF˜uùaP0=$f/¨äðï®$É[~˜ýEŒëô7 0üŽvF#»¹5˜„LwhÈXf2Ý!!SÎÇ0ô±“„zw æŽ@Þ€ªíOÅCšŒ½ßà]àÐ )íO$Â0ˆÃwMÞ’ë–û»@é ‡Ý†½ý~nÆ5 kÄeÉÖ¨7• æ¨É¡£Ãaý;¬­Ž‡£ ÓÆÉpÄ“ÓR&^èÄCÙJ½sR—w4™?¢n¼J-wDƒŒhN½Ï4Ž§ÛÞܱ³³èÿ¼«3b·F¿VcoäyêG»JçJÍý¼{Œ—þ‹µ²1‹zä7NÝÖÊ÷MîI±VnwÝè¯ÇVô×çùsƒX«£×—Ñ_Ö:ݾ¹¸âòûŽø½É2€­Ë-ƒÿúòó ûjô4¡¶ï4ôz5}÷7ipH ]ËÕ·êr¸9Xú×|[Ü»ýÕ’¢)üì«Ñ´šx-Æ’]ûÜL<ÎÌÖ.Ýÿ¤«¢âZŒÿ®£"\cö_Ubðš8¡Âí?¨Ä@(q-F¹JŒ÷ÿ7W™®Ô0Zy¹ú|G#••Z ¾9¼ÿø|ÅîåÒýßçÖõÿÜ·+æÿ¯ÿÅýpÀ´‚Ëu჋߶n*uÖwÀh=Wj—“‹í•?·^ž¯Ûÿ30…_~mo® L ð¾—Ðwz ³1.¡74bxôfxY1á§ÿ§ò ÿû„¿Wà»0£8–ç¡eÛ¡ox|c^aèÛ–†ð…éyž–oÛÀå`®vÓµBÇ4mÓ Í¦¶´ ìo`4=‘¯+ã.ÀAˆºÀ7®å§x +ðD…ÀŽ Æ29‹ #|Ø?“Р¦‹RÐrªdZn•!ø±ÜS»•Ã) +èÙ@²C\Ÿoy¶¯éÔÒvò›fà”¨`S@Q¢¤âæ€o’œÓÀ ©ÁYðø¦_°4]Ô@x€² §E—¯ …Å« ¨ -ÆÏ­ L,Oœç¾kƒV¦£ð'èTe€Øõ=P˜Ò&w² £i8Ž ÌDJPwUÓs%€Ÿ»6?Ü@.¥ÍÐå +4;`± FlسEöÁ&À¤A5²¹Èa:éH8e˳Lœ2ðy‹s$8éŸöﻎíú¾¦ËmÅ2A_uÁæHÀU´ã˜(µPÙðÑw4j–® àË·}”ÒÉg2+ ã=†ãl@gœt<3°„U» qUÀH£g»vJ׈e bTœ`·\@v la·@mvq·@‚rB²B›èŽj8 +¾¨°)}@\Â^¦}p/Qnâ^‚á¦áEH¢ »Báiˆ 1òLg€ØHÈË™i¨‘娗¨‰0e +7N€Ÿ;$dÔ ’|°9ÃÀÒè}-¶—!XL È4àòÞ‡éÙØh»Âi†éû: bŒ 'B§ËÞꎦ2 ˜\–<ЛO”'“l‚Òö؈•p¥v‚Cæ•pê¤Ù CæOHöä*¬´Š U¢c6;PÀr'€­è¦}†A…®åçwÒ@ƒ¯<<ñèÛ0ƒÌ>¦i#i —rõË×@+Ó 6EX²!úN HMâÿ°%`þ˜ÐteC'%‘™öïy N2+I#HÑÚ‰Dèí8q4Û@Çã]À@K÷Ä#ˆc8<€ߌÅ:0»téËeÎWjmŠ‹"ŸÒ™ +pÁ0 À‚‰ëºcd¹ÈñA}õô ›d¾GŒIMÕ•¤é*§:}:Ž:EÖ©«ÒÈLÜkÜV׳ Gçƒmh†èÒ“'£vÑ,Jí¤ G™Œ‚áâŠÝcºåKR7•©ˆZÐ.Ø:Ñ RtqAê1#Á‚o»Ý×uѬ_Ó ]-‰«Sg£œuUšs§Ü7€ü]ô3:ÚqtØxˆjŒ‡P7MuUj';êlïU$:+ÌSWñ꣧ßXÄ  #cÙ>¬Üôí4H|tŽ¸!òí²)5M´Ï}—…8X(Ž–`À[ü„ ,çiÈÀ†­t@7Q À|\èz@…ÔµÑ/ G:¡Ç5Fc€–“tµ°‰îG@Šg}s€Ä-ÈgŸƒ²‘„àmô±_&GËă•*êº.0¿ÐrHjݶã‘MÐLŒ ÓOLÔp@³pµ‹‚@ (8 °07¥ÒÖMYÓE^xñ^!iÙ>ØÕèò @AÕB:¶€ËY†A·,RÐ{m4b‘AÙp"|ââk¸œ ] ƒci»x¨'Â)·Aûõ™|—Aáâa3À|Ä8„m„¬|ãùtñ>¬ 3B@BvÒÐ2›¡P›S3eÝòžGV¥,œuqiP@Þè(·Õ1f¡Ì˜÷0YÀ°uë.Þ*¶£hÐ J ûH³s ‹îhI˜Ø•L³}0ðSÍÎÆ(,ìbø·­íô<èi{d/ƒâk +Ʊ@g<ÎFKÄ +Õ.°VhÏ +,X†Ô<ÕLÙ6Q¥ É8ÊÂy +JA e  @ª‡¡‰`$[7cÖÈz0TÖ]¼U\ª/¾V;Xn`aD«#NLõTA„m A–Y4hωlo`îé€0—¦FUk”.6Ú­`â€&ÆýŸ Rº6¾~ß¼ç.hf¦O‡Qº€’k›!ᇠ+$fÖç'BR& t˜° ª•e³.vê=Ò ò<²(´@eÐ ìfŸË3Žº˜¼K`%ëN]ù%v +wU›=P_˜<ühÿy~¨þ¾~½_úwÿ|·wýñ;N¨ogv9úŸ×{1»àt@ˆüÒEû ØœÑÔì 8HFh¹, ˆ´`¨¦jœ¦ÅMÀGû"UlDŒÀ¦€1ù øtHL Èk`°8l ;élÿ‚m;|ëŒ11—Úƒ„ÀáÎh@•À)q€ï;‘DDÆ›¦ }—Ärt  Ð|O¥L½©Ô§¤Y8ëâx¤ "lBD É:P!éˆeÛð™÷œMÙ  Bndz.¶«]xá^1Á¦£aûzê1|ˆò%@«ÆÕA 6-?Iâ°¹œ" ÍÀÀÏŒY?íBŸã4™M kyëÅ ’z§lCÁ€8˜-h¶LÎ{è ',FÓÅi¢— ÊEÒ¤Ú¼fÊ襶žfáJ ¨€® +S>°Ø#hÚ)kºÈ /Þ+¶¥ úó=ËçfæÒ ù4\apTP¬<>FµÑñ‘l)h¿ {=Ô~áWÛ#ê¨ýÀ¹F!ÂÅc0{P·]׈t¯DŸb]ÐÇ +]檆ÃÄlÃTaGPÈÖs„À‘´© ç +;LÙäSf27 $5­4 g]ˆ¯Dƒ@ (ÐÆ}PZ1À9Ã0ž29ÊQ‡u±XN[8QüKìU¬°;>¶áñ‘c¦á¥[ÁvÛ›0kp +œY®CµIL1ƒƒy‡® †Ï@†Iƒ¤ðm (\¼×tŨÐfˆIN)•. VßwÉâPp$a ‚a¤N7ÅTa—λ¤æ}„À#Eµt `> Œ‚Fêbf‡nʬ ªãØ…,œv)Ü«D¿ƒ³Ô„a¨b +hC‚UÐ^acài `é˜^H ÐAÔL'i&<*À„Íôhàw¦ _= ÛÅ¥¡[;ØDf]Lƒwa +°gà©L• (“퀒8]—%N ª v¥Ü* â8êC™7a˜4mËõüP7]ÞÅ`]|–>ÀMòØJìÑÿŽf÷¿‘ +6TT +@æGruÉûŦ~DÕô jI䦣”ÚJ aè;„.!F«=­Àùàì‡wUH` &MèäaÆ×s,Ë%LVÓÅj¢£ ÝA (Ô ª‰fìð37´Âß"䯮›w!Äú[þ\O +Ý6°i  < *G7eÖÅ÷XÇpu /Þªlº\ê·ÅØ h=p5GÊÌÑ¥„c¡’ +¼˜4¨5&wf€öK¶eò³ïà9 ]mP™A©`¹.K,•A!ŽQõ-Y±Áµ%¤u]ŒÛ¹ ÐXp"ÕL“¨¾š…³.C ÇÈ´$(0“àGÐ ¾·-í”±‹åñ.N²pÊ<‹÷Š)`A€rŽF†gZ,ªä j§¹ ÜÚ 19Ô}Ô±*²fªÔ£é’0DÓÉg8ä=È©A $7°]°±Xö‡$È'ÛàyG ¹š\›­‰È9MÔñ=[ÅìAå»\·ëß@É3¶Ñä0¨!§,[í¢ OÉA# ϹÅl5ÝŒ5]”uîTlë¬s1Á+ y檟„Åq[–r䌥e°!T'Ë7C4œBËq´]PaÒþePÜÖcá`‹@6W¦à6#ëâñ.F¤’y {ü ¨¸² B€e±<_¢ýq[¦ìð)3•ÂA“ƒ¸g4 Ç.V¢Ú…2P¦`¾ÃBA•> %"+Sf],I}¨yZb¯¸­‡ª%&ÚAÛäÊ †Ò%[åæ€ÉcÌ`FÍÛ2ÅàŒfN¨–黀ÂlcÚE`ºÜÏAÑä4äP˜b`óŒuTÎ]Ÿ ‘uqx;R"ñ< +¶ž + »Ptþ¨S†ŸõHœêÂY—€šƒzp-Ž@ÓL@ ž¤:¹â@7eÖ%ñ{zñÂ=Ú¥p¯RÃÀgÙÒ¸®ÐçGÕPâù柠ñˆób017†O‘y·Aˆá×mE×ÅCÅqP4f„Ë3 t²ƒ–ì3¶‰3Üh¬ (ú¬ O±³Å”h +m9åGyÁÐN¤š)ãÏ<’ò§Y8ëBü±:j@a‡p•)ó.0 (›ºuoÕÿŽ}p\ “0qäþ_Šs¯&w5šœ3¸MPX&ļ£NäeBFðÅs+pqæceþµ êDa䫵m ¨Ä-ìqŸ/ëcË._¥‡‰òŠ˜'* VÉâL00SÚªËf]HA‰}:P ƒÉÀ<¾Ë·cSNʨKä6µË.Ü(&T˜~hrý0âX5%äœs•6à*­ÁÝCÑ犱)ƾ¶ê×V¤_;–R¬Ê‡×ÓYÈŽœÇªëb£g’x€4 °~Ä<@êŒá nZͲ±K@ÜQ}\“¸œªéN¤ÉÉŒ©o+û‘&ÏB¿Êº‹wŠm(˜Š ªÂ²‚þóÃ_pò£ j¡V¬ËaÂ24‘cbsH `´šBm_Î]–Aq³ž4; +“¸I:»® Ëô'qD¦¡èAP¦ŒÅf´ÀF³pÞ…(*5 L,EÁÓÅüÜn2e¢Õ±.˜ ˆ]xî­¼ð⽊3é(³nAê¸Òò\¾_”i(,Lí¢°B”Žç²…¹D¿V{x’¬U1,‡¶H™0Û-šp£.[Ùs}:P +ÏUhPá¹ê² 7*Ú®¡œ› µò¸"/U¯±Í2i8*Žà`^LÅuµ]|0-ÀÎÆ‚ +Ûò5 x–œ'°üÀJŽ¨KèBÓÑc[y X¥ˆ˜+¢N™mÍ*P®l:C ‰Š±²~]ÍìD4ºšt?`Ò© *»ºa`áÇ÷5\WL¬†ƒ“ž§§M6 ØcOJ+àLg¤›Ë +û€ª-ædóÕVÐ@D[3ç¤fÌl ’¦Ð` ÎõÔF }ÝìêüëZ„f0,P¶y @…-ñ >’¸˜¯‚âê—sQ‘”NÂQ«ËJ½læÐc¸B—²éÆ+TzX˜Åˆ<Ó‰ÇVbb½LjҬ¬[éa *n`±[L€.ðzÿ9N<`!ÈhvBëm«sCi[R£´®V6RÄ’{¼Þ“ê¢1˜AzNŒBô;KO(¶c©I J`€lV´4£®J‹Û´™Ù©R"4zM,4·2¨Ü +›€i¦ï™: hyf}¡4,:&,Xº—bØn‚–êÇ„ú„£€&€\$^žØƒ­´JÛ =Œ”8Z¹Ä• ˜Œã¤h¶Ye8C*õM…¦S‰£€ ÌÞA?‘­G¥Ò,n„…Û`¶¶3ãL#µÙFº4 ‚áÑÈm¦°Ÿ( Äv`üF³ô¾;”Ý€¦G¦ìÊûf‚lŠÁG˜¨„‡ ´ôLˆÍy;“@•Lé×B/pÂXP¥oú÷-3a¹´jÇf˜%#¶Ð@ãËÖµK8TzÀV;ieÈ€ÈnÊKšoe²“ÚU²Ø*‘t>‚&åRøÓtKÑ… +*¬åc +@ÂèñÒ†dÓ”vqO=Lå¢H8F~³t äÁ š1C cËýµ*ìÈí¬CèS;uã£el†¦-P<숃ãÏýôìÉ‹à&ëû䞇øtœëC’9>Uðôcž¶jô”©>UY°†=@žÒˆ™s†Fm·¥Õ÷@©0íFɲV 0f˜’B’š]+eÐlt¢KIÛ†S Â%°`5EÒ ÇÑÓÓ£´ƒ¸D½•o@¶ SKŽ€i¼ƒÊuuÜë¶Â’!íDPv[n–H-ocæ†a0*0‡:Héó©â„’*õ¶˜ohèNÄ‹tíh¨E¨S›A%_@»-+e1»v˜NFÄYÒã‚Þ`Peˆ6·ÔfiU­"Ä þÁbT“Žn†9L¶KN:mÅ3šÉGFRÖFG¬kG¿rÌá=K=l~ÝCDhxg'ìȱúŠ5®`â8ž®Qƒ<Ú¬CYY+-¢$`:º­'ÿ'¥U$o˜“—i*í ÷¢Â( Ó2*B±‡FOp6! KÍÔå‚“+ÍìR‹H?"GŸp†ÔQ(µãü«Óä“Ñ4HÄ!wÀ\ã”Ü&:E/=â¤5GÅ)Cúµ¡¨V^³2yµu;Ñm6)iKXËÁ¸x—\àba•ÆèŠxÀkËga¥çC@)ŸNo_²2›a>ºL…Rø=»Í24í˜ù•Ø‚jâªBà¶Äêðúµ˜§â샜vž_zêêî5-qwD7»e× +…©xCNlåá :žh«Àâ¼Xd@³íKA¬w¼ÆÓ±—c¡‘þž°¸Çð5§¾U B¬þKÐUØŒ.$º,е¨0bIÇ%R–C©ÔÛ° Ⴙ‚—§^Ø=Û§ª«ØÌhCô¢J¿‰—TôlïµÚŽù—abK«í.–&š‚·˸|/f«ÊÙ›ùî™VdõÀ[yE Ñ+¦×M <4H’ ^Îg +©„—Eú˜ ï¦Ê§‹—$AªÐnJA BF¡Ãî×ñâ…ÍkF} /ò’®à:)ÕÖN`FQ» ÜÂò·n ‘ÚoÙí鶛¨¬šðzÖTs—kj½`{*dg0ñîc‡Œ:‘*xkY­kñûÛ‰‰ÆÕs›‚¥•¨kJ)ubû-»Ôß‘Bâ’Š‚ŸÆ”fapx©£\|\ ®d”+3—Úoù#Ž¹”‡@ú7ˆ&èàE³aJÞKbexolÞ™j£N3Ös[¬ÙñCËw1'ÛKwˆóðà ƒA]¤l00ú5¨)LÛUê‡ Ò‚ã“u£˶ÿßz}dœÀXõ¶7 ±3à]UÀgAí€c>7ÇÁð¬3ÄÔ¥[Kß y¨‹—³‡¾ï›h–«ËôQÁýŠóZX^ùœØZy‰-¨¡‘Ä!ß!¯ ïÎÜ®˜õ"„¡Kô°¨Ê›7¹p¯"‹¸:¹U\eIÚJA³8¿<Èéül¤êÔl–'hã½ÍT#,±äfŸd]ø‘ó˜ph¥î+ ®;6<=Ø–ß hRjÆ»AA‡3–è!òÈyl?Õ¨> t)ͨBRÆ vðX†i¬MY쮄ñH¥"NŸ*ÌR3^Ùb´R@¯g¡ËŒÆÔÅ€I³fš„v_ØŽ[¶}–áIxz8XHjÇA, M·/ð‘Š»#ÄPA&BQYŸ+IU ¯9¥”~ïò·2àxñj¬MKòÆ­¨Ò¥"  ÆDm + +:Î7uùHênÈ¿1_µC”©õ…‡ÁJBŠ0åPP+o1ûÇõhVêÑÒô¿Ë­ ]"Ë ;ˆ§>÷<“cïKÙ,Ò±—›ñ¥ŠÔÊ´mŒI%Ú]a³4E mt—°Œüf‘ë©° šƒ&ÉXli–VØA`žqa4K‹ËÃ;IŒÀiË è}R;ˆ`3W4(Íâð<;„¦á”h§ùìªÊ,¹¬iÆ.X;‰yš¢·‚‚a1]!V@[£wo„-ÒG”v_Ê Ñt’HlLL­ 1šC¼6 L&–úÜÄ$[¥R!ÄÆúeª‚PL$‘Ûñe!“Dé` +©$ [84èO M7Ç CÝùe‰†—¡w´”V‘6ò¶4¶Úñq!êXÈ$É4BêÀ2vRnñt¨­$‰Dnô„„kÓä9¤Ü8|ƒÓ¶$‚$i$˜e‘Ç´­%·RIBqÌJ­+ÜëÔÏ>Â0Ío›]Ép:ÒGÔf){Dê€tJº,¹Šn$®d8Ó6a‚ÛèrZ9˜2F\!ÅY”R£DÄ»Ú:•à¹­ŠÙÙ&µ¥KôŽXÎÌÓ%âS:žm) ¥„µ‡°ϔѱñfëX +0æb]ZjgÙB*Ž<&>S ¾Ê’ò{¥™Õäj–Û=¡RI.1W|mTôˆÌqcKZíÛ@3èÞ,·¡uoÛEjgûÄ÷ÿ¤aDI5“IEVìP=‘=oÂï:®—äÀbúM5‡i†D`,ƒ$mMF®À ž*Ò2$fùMÄuâ‰y}R«èw‰²£Ç ­ÍeNf|û¨®&¾Ö.O\lfTAÊo¤f´#dm‘8{Õfyòj¹!±¸TB\.ÖÅäµÐoRé†OdÐ̯駢ɂȾ…0<êÁ¦ù{˜ø‘áß¼eD¦ +"7ÄgHþ-j+aêó=©2õ°Ô7ÿ?M M±½¥¶»BÎŒ„ÜFO›Ç!ì4•HÞý'µÝ“28йDC]R°£ì +äpFêù#îÉ*å1|ª3pÏ'M±ð¤óÍó8<¼Wƒj¾’Èá R̉eé RÐH³N9세=ET®Å1LÙ¶ô¤|¥C gtÈ=P¼&Ù&"xÉDV— uà[å‘QyO*gG‰$dux‚~§ñTàv:‚ŠM“:ð ,’–Ž®0!§Ã¯"K4k”Åâ‰Ý÷ãe¸"ä#è´ŽüU˜Õñ¿už&‡]‰Ì¹[Ff“ÒYeú¨àþÓÌLPHØ9¾«Eh^6ê¦L‹g^D+§¿máý4þ)5âKa©]¦ -µK?–R:ò[ñÝ2!ÝDÔ¤æPÌÚÆ’'I¸Š®ÄH¤¦ø[ ,/’å\Ø,N;sH’ +¦Ø©](í•Ô*B·Ùõ®nÍr“à|Ã{©Þ“Ûè’Ï SNBb§w0©ÓM<§É–ü Âz +ZÅ-ÈÁ/‰Éã-ÀvZ˜.î‚Ü*¡y1±0¥VO*¤§Í¶G$Qð.U—Z}CºëFn·„bì]NY[ÑÆå\l$f8‚K·D¸éÇM6¥LÏB`úÄA¢6+«—{öèM?8º~¤óôΘ½KïZñ–H8ש>,´¶äVie­ÜH÷d°÷P‰†+‘žÜ,R—c4é$3ZÄ‘ Qȃ0uÝíB”Jé!Íñ ù­8º£R:X$R„ÎÑZ|Ù5ß "to á)ÇÅÄprƒøS––ŠE r£)é|rižpEDn£„kµÝVL•z|–‡hz>IStl)Wþ¹BðBI­_¼¬>¤)°+´*úÖ!ê™+ÕíÒ£( ­†DŠÊÐRñçxc^¦â"5ŠÛÊÒèm3Í”š­&õÊŠ +Ì$ê–ôc¼l“ºÙ +š%”äì Uƒ~4îk…[TŒN ¹i³‡FŒ)p.»UšC1ÏGÓN=6x²`ð !Ï#ÅéŽRŽ‡µj¢t¡)>XbN¹‹)$øÈ­–˜Þ#7Û4¹ßÞ¦wP´{¶# ,r,j¹â†*Í"9äîd,Ôññpâ’FåNÈîÁ'š„¸•ÔÃÃRKzknIðѵ“µ9’|<9.(•c Ž3§ + mc7N8ŽIòQš¥eµ +ƒèÃ+¿…ÛVpßS·ž‡Ï +M=Š$ÞndÄ k}Ô¦éƒY †°mfš²ƒ.Å£ÔÑfêlšñ“‹Q‚-ñR(Ä6‰°QÈoE]LHèÁ¾NX¢‡xî2çM"ü6|Kój]Jé!.‡ÍP…&E‹[©6ã;4¦(hA¾+ÆŸ%Ê÷°™ÄûM=<ò½¬8‘6iq*F&V¤VEkR:ˆš €®¼ ²æ_à;e©×X@Ùv ~Y¤šS`È=…ó¡4‹xÂû˜…•7 .À”B[…·@€ó5%ZÐCp¨ +še²Ê=„ƒž»B¼šÖAã– +¹>stream +IÛ/Kô‰AO‘Xd×ë +·ÐH]TjvHO²7êBêbOÑaH®ÐWðˆi<ÒBÄf\ÉÎBŽA¢Érk(eÂHí’ãR%ðDLeêR{šFÀVuErÂÔvÍ”.Íæa3JÔdæ¢_ÌçRêÄöÇÊ„iZKý¥¿!$ö°„K²4ˆkó !ŸY +Ÿâ£N†M‚T¨¸d˜ {N39æ˜àƒ †ðª€Ø£¥ëáÓ< –ŠÚ}ª©XM†”ì£ô€¡„tMÄå¯ÝÑLÓ±áɹ>&+*Ld§üs¼]2 …èÍô11FJ®ô@r}`øÔÊ9>&KÅðD–fù˜.5È9xš #/Ï—S|L·I«”ß#qÙ‚ò.ä÷Èíâáør+Îî‘ K³>stream +%AI12_CompressedDataxœì½í’$¹•%öþ¡cFJêÇ7@Éd–‘9¢ŒÃi#9«]«é®!k·»ª­ºzF£§×9çþY•Í¢¶z¹èÌ® Àápp?Ͻø›ÿéËßqÿÍ›~ùE¸›ÓßüÍéíËïÞ¼ýÕAß~ýí·?þðî-¿úÅï~ypånF£û_ׯzÃÿðòí¯Þ¼þ•ªTùÈ«q|ûòõ»ÃéÍ_ÿ黯yøÅ/Qõ‡Wï¾}‰ÊÇãW_¾}ùû7_ýæÍß|õåï~ýÛ?|õ›WüÓ»ãßݽxõË1t|~ñW¸ùo}ú[Îýj‡û¿g‹¯ÿõÅ?¼úYŸC øîøæÇ×ß¼zýÇã›ÿçW‡/Üá‹4§Cœãá +êÿÏW¿{ùþÍO.µPS*¼(Þ•Pª›c›£Ëè!Ü?ç–\sÉêî®ï|msö ]Ÿß|ýãwxæ/ß¾ùúå?œÞ|ûæí¿:œþýÅëÃß¿ø#j^þÓËo¿}óo‡ã·/¾þ¯ÞbúêñÕ·/ñ¾{ñîà_ßý¯ÿêøã«o¿ùíßýóK¼JŸ2¿_©Ëü}¡[þ›_—¯~ý¾ùýËwïðH¸á¼Ë s÷æ»1”_|yÿÛ?üÃo¾¸z8ñ-ÿ/›/[¹ú.§hßýî¾p“»r)xþXr ˜’ù.…Ù;¼°TB­ÁᛚB‹­ÔäƒO1ÞÀ|ƒÏ±4r‹¡z|Sr«¾ø˜sN-%|Óœw¸ÁœRk pkØ3ÚyŸzÏhŽqeõ^BDßÞ‡ÒÂígÛÎÙ¯ØÊ/þéw/ÿøJkËð?ÿ²ÏÁÛ7ß÷âíå‹>|ÛÒŒõ!ù+K-ÌîÎ¥‚73'Œ&Ø•xùÝ÷ßb1kÕ…–ï[òÛ?z[¬Š¾„}¹Ku鿾(1Þ¥Rñ’g_cö‡ŒëìÖeFS,z—뮉uº.Зÿúêå¿ýêðÛ7¯_Ú*¼ûî÷¶bœgûm5¿ûñÛ—oÿñõ«w¶²îÝlþý›o^~‹öËõß¾ÐêSqëokð‡oÿøòöÞ›o|'rQÇ°Ìóâß_r:»Á?|ÿòõÞüñ‹ï|.)æPR«3·žOX­æÖRž]<ø»ìb ðø5üªºÌLÇ‘[ÛyÞdܽ`-|‰­ôo_ýñÕë_5Ìo™½í±¿{ûê›u‹¨öKxWEAf皟›oÏý¦éS²KÕEÿœoìýàU¿{÷òu_ §¿ßlÿùîï'yxýÍéÍwœïH±”_c•ûæV·ü[5¸üÇïí­èﯰ4¾|ûê5ûœ~«šúÕ—ßþˆª¿{ûæÇïýú_ÞL¿0ñ^~ 6€ÕóÍáþù¿àÐz‘¤ÃÞ¾øàï¥ ˆû÷¿|ow_~ûâõ‹·}+íËß¼úWÔ¼À€vjÖï>Üéù忀Ʈ×Ú·¯ÿõå·o¾¹~¿|óâõ7‡ÿûÅÛï?Ü5&ãí¦ý9þÿŒ§ÅÊû/GÏ¢&Oþž›ªgÜèÅ»?ï¼|ýÍKßöçþÚwîï÷_s ¾=ßþøßxóæÛ¥Û}ÕÒ{ÿZß²ýÏã_ê‚×ÿðÚ^ÐõzƒË;âýìî‚ÖOß•?çÞO/¾ýöÕß¾øþO¯¾¾uƒõˬî§,¬ÿîŸß|ûê‡ïÖõ´ùæËoß½úúÛ—¿ÿ÷Þ½üîÙ“{xøæˆÜÛø½m~ÿo/Þ}ý§ß¼úç·/Þ¾zùÞÝÇ ø—W¯¿ÁÚÿý¯Þ½\_Лᄃ\{øýŸ^|ÿRñîOjùû¥ÃôHÿ–¸ñÅ{¨>ßãëMýß½}ñÍ+0H’ÿøúõ‹ï@ñÿØ¿:¸_N·¾$>¿™þiúߧSAÉ( %žÂÉ«8”ù4(g”Ó‘Ÿ{•†RQ +JFILjPütôGwœóý#ÊÃýùþ„b×ñÓP*JQÉ( %¢âPæû¹=¶‡vnç©PxK]+Î[QJË*E§ÿ‚+g\qBVBmh®>Ö‡zÂíjÍ5V_]y(gŒº•R Wæü€§?¢§’S†Xñð1Óq¸j‚<šBrñ1>à[„àS„è +åã!œðÈPOB)@U¨ý€—wôÍW¹ÇS™Ý^æ½k“+=Avóãü€·‹Ïu†|8£·-õ ³£D¢Ÿñ/¿üöËïÐì7;úÍ΢~'ýÎþ‡ÇD)(U¥¡Üëgü>.¿1ë('ýŒßçþ/ý5mªÏËï³~?à72ãQâqWNï)ç÷”‡Q¦õŸ½<>]Òüžâ¬Lã»â·Es•§dȶX¯G,—3Í#Þ¦+‹(b)Aí‚hß°¦ØPg,³G¼g‡%°ð–_Á"lXŠG,Èó„uùˆI€ÜÙ¨%¬XJ•÷Xæ'¬ú‡öˆépXÓ;#a—왆ýsÄn:c_=b’öZÀ‹MÇÜÝlÜ.ÄšEÐnès¥Á‚²®l:1¡ëcý†’@U¢Ç¦†ö¦;€è@2Hþ¨~CïöÔ¸B¤U¤HëÉIëcPk׳,_RkïÏgßÕƒ‡Â”¡1Z½ úeå«O;ˆã“®«læ°A0k¸.ƒÐ¦ì~µ ÉámVU¨äÙ m£{{œ?Ó»Þ3û;Œ$¬þßø¾ö¬àW9㵦€ÿQÏÌ>Îè-¥ +ΓŠîYç„Ø^Õ$ù®Î1”šiðñ¥F°¢âSl3{‰»WïÞ;wGÖ¸y?Ÿ1éU—¢Ç@ð†ñŠu]*5`*†X²îŇ}4_R›©8›êœÅµ¤qÐt$ú$¾ö ÌÒ¤M—¦6M}š¦¡Òµê6Ért”%élêµlS±½LOQ†¨ÜUí*cÓ}W¸©£Iéê3­\CN‹þ\ýÙ´çÓО»2îdZ ]%—þ,åêó$ýù¸èÏÒž¥?›öìí9-Ús]´ç¡?s¥Ø2wS_ÓaYµyY˜mYz§em=.ëÇ/ Ä–C±™Ÿú4Û”žûìÙ|‰Á>‚»91ÞÒœA¢+¦ã^\úŒôZê@ÌuX\þ^v!Úxe–ñ`’d… +q÷{Âc?`,³ä 褦@?ß"3}Ø$se‘Éï³ÉLÏ1Ê|Ø&#ˉ¬&“&Ý&Ü&Û&Ú&ùاWSÛgÕfÔfÓfÒfÑf0N}òªæÍæÌæk±jt©§Ë=éç¡ÿ>ëç,ÖiâYè"iî%bÐl"Fˆa–ߣì½Y"íº³ŒS0Š & *pÞP… ±ÐªãêãÆ-6qyvúe\9ß±¯eëU¾(Æ*üÎV•6öª±çl×­û®[®lï­6ž÷-Ò mSÕ^Ú¦ÜoÊqSN›r¶²i:QªW’;µ¦™<êÇÊ©Ïô¹¯‚DZ2´Nl`Ú)cŸp—ph‡,û#akì7Æv[ôMa3ß.Jí³¼e%§ÝºØp |ú7Ãt~»ÜšïÐMæ±Ì×’Õ6Igî%$Ì]ÐŒŸ$’µZÌ‹V W ××Qgg/7Ÿ»äÜö²³Ô¬!=ßK~.]‚¹¯Œû«ÒžQêeÙY7 wðø×M“àû[ÉF3·æ¹ÖjKÞ ì-,Úù@ÃQ‘õ€¡+'ú2‡ «Hv!:Ÿ+¦+ÒjV/,ègg3¤íiµµ ŠsmÉÆTý·×j¬ù™Kï +û2Íz-ô-ï9‚À8¡Vèµâ˜t'J@X¥ÅÉòª ­Êe0ZÜç°‡:aÝ´ mM²í{¬~?«QÙš‚,KMاxýµ|bö°¹#®çý[IÁÍøcƲYÚ<ôË9£çgh7Q|²4Æ2ËFÉí¦ÎÝ4üýLÆóq–¿xÓò‰þË_×¢œè¥QÉ,»ÆðÒñGºH+›“Ñ.ŠÉ” {1vB6âLæ²»à‰;»;oÄÈUŒ²êP˜âä(%RN²šTirå,)[žºlùÐÝæÛ I–ˆ˜Üö¥ šÍ„ÍIò¦Iœ’€gÉ&y’­“±“µgÙ¾ª|“5ì(6«?K)„˜2‰çÏ2¡Éê:$D»yQ?Å>RuJëÅÜäÇ^NV¦r^ÊVé|Ü—:_– ïÔ2·[>9~·¥{?(‹'ÉãQ2¹yB|÷†tˆ¤t+ÀnéîiSŽåþ²˜¢ó¸(µ§>¯÷]᩺Mî3eñšg'÷£f{Ì÷I–›ûIÓÞôØE“Ÿe湡³h)\ë-[ÍeÚ©/«¤MšÀf}Ñ<˜ö+“•Ö…нÀ XF“L[eA˜Æ,-˜ùp6x³Ž9븳ǎ=ø3”éØos\?÷KiK©KÙÎKÞZíÒdÿS‰7JxªŒ}¼0V ;ç°}Þ/³Ÿw¦œ­U{ZåþsºQŽO”ûm™ö.+üƒek-Ù˜ ÍHøÓM„^.iû·I¶ÓM#áÖLx¿3^ + d%ñ´ÁWmÑUic4l‹…ªÛ¦ºÓÃ/ ªQx¢Ñl G¹ÑèL{Û`†œãäQò­n¥ñ­<>$rA».O’ÉéÌ£<~/ïy@÷yÚ,Âû\±ƒ£Þ£øG˜ðÝ€¸B®Üú´H¯+,ò|ñ{ÅBÙö<]! –ßçîq\çÞõy¹œÓ2ºþE—%ŽKÃûî€Æ*TíÿU†“lî)!ò£>ÿt¨gßáâsÌpX³œÙçŽp /» oöÔÝÙrhŸäÐnÚYh†­K›(†{ˆ‡K;hÃÑ©ý0|ÚÓ3œÚuãÖ&.fî@—S_U‚w‡ ìTû ?Ÿn1ô÷¹Æh~áK/l|ïm$'Fš±{ýœª›!7™ö‹x†Á§,|}œkàrÈصA˜òx ž.«{ßf…ôo=PauøvGÈúÑÿéÆ wИ› ¢ ¶[¦$ÕàKž[0G›üH%1UK˜×Ù‚'‡¸Z°â²…lqöXÁO#ÈÝ]ƒ,`ZªëI¯¬xk~Ž¢,çŽ5гÉlÓ¢hÝw‹çI;í,ýÏÑaD7eÕT×a¯¿ïVP+Ò€;êJãð„¥¬1ÄyS¶š¶-[} XÝ~®¨‚Aqo¸©[nÿ̲‹vžÞ +ýŒ’—b~œ4ü¥Ê_i‡×úíêD¿­×®jc¸ð/@‰i‡—ÿ.ý÷+‹®Yoü ¯yYééÂÇž.þU–úí Ê2Œž¿Ã¤úÍãVgF·~LÝb&5n³vÏF¾2™±èIsÑ´³m#9«‘ŠÞ{—[ãѹów$cLò8*ܳ—Ž¤“OrÄ~&9QFY?~SºÑm²Ñ •Ú‰ãþáª\óî-ðþ4mþx³Õ£PŸSvÀ°+ò}ÿ¸òWÙakêDàþ±B®Ž <@¼ï„ ÷ RpΠˆÐc4/ä€N ÃR1,·ÓzÚ˜z`î ÃJzËFº³NéÎHº±‘+i\ý;¨éÜÑKæEé˜:õHr§‡•†,Ž´áJ[CÀ/MÓykšž6Öi×…ÇڨͰTÕ¹ïp\£>µƒtE…¦NŠÆ'ö²ìýMîžmâ-oÜé~×9….%ê[éŠnÂ…ŒöM"€·K{f©Û2íÿüÉ¥\–éú«+•®D`ļÂb.hÌÓ¹â1ãÆjZPxÇпFq;ÄNAÌ㼂í¶@»á`–syÚ%p‹‹Wyñ)o tÙ¼u'_9“§Þä…ó×>åŽ0ìÀCW÷GÑP§%ÜÈ7>m)uSöèɼ):Lã7öX¸*O±W·–iû‡åŸ{¹ŒZ»,ÓÅ[¦'*ž‡n»Q¦?ÿÒÿ:ÜÌν8•M®®–ŠgžìVHFo }"±­“#ã¿æ»™a<­ÄcLLœu#öéF¢¤¼kcd.›cmï±ùtc8þ0Yjé|páoãþËÑê®”‹pªË¯?ÎÝSoº{ê稦ÏQMŸ£š>G5}ŽjúÕô9ªésTÓ稦ÏQMŸ£š>G5}ŽjúÕôüÏ'~ŽjúÕô9ªé"ªiÍ´hÉîqL¥'ÌÛ-áL=yåid=Üe û3>ÓÇ]~õ9s„÷W +ÕâœX¦¸vGÀ÷ +äãȃ¹k› ôža÷‚⣢&$s¢&ÍÙ€ +¨õ°ˆK€ " ›gpáÒ—O[~Žúé9OZtg-ÃGÆ–©ëùÇÏø×þïÑÆM}MŸuvÃIþØ£~ë_k,VÝÄb»ÕÏ°žf‡hÏ·h¤,c'ƒc9¹©Ë¥%]GdE'ånÎ;vÑá±Geyñ6ù/§–uÁY›D˜Ûø¬U€8vcÝCG”[ZL% +¼Ã gi·ÀP ê9a^·"¨âÆœ²¢Ù–X©aT]ƒÝ†QuoRpÚîepÚm°—,¦“E{ÉNj/´J;Kþ +kO‰Ëd-be«Pö|#†°oÂÅš0•&Üû.?a^ïûö ‚þ8ðÖçùù ÝØv3åË |ØS°±?™6m,d7ÊÓÁiO¦ÓeXÚ”6 FÓ“£a´†£å«$ñ®‹!#í4uÑýZSÅÇäÐÂì¹N/‹èÔå•ž=µgM–×ã÷ö°‰{»ÄtÜBu<ùöÀ#ƒNˆ|û‰±oÓ"ö}@ð{®Ü7õ íOˆ~%!u!ÝñðÛOQׇðiêjºÃ,ûŸA@݉½€I #412šÓQÞyÊnŽx¡Eë—dŽÅ«Dl–œ°áfllÛ² vì6IÎvqu—áò½u?«a}œ‡µÝô°¶_j“vëb¬<-弘±»v²ëöiå‡Nï¶eib®£Ð½tž`-eSjÿÍÿ×Iÿ-µÿÜ÷Dñ÷ÝT¿š_Í|êÿ:õTò§màòð!>½g°[?Ór†Ìš`uþX9mÊzÁ*PlÊYÖÉÓr$ÏúFGÆç5\eI3¯à†û//‡¬usëI¹¼o•Å¡‰ZÎi²œû‰3®Ÿ5s Ã œ*šf+.u\ 1!Y 1a¡t†4ë´“‹!# +ýp˜r{²ì­~49¹qð¼Èx†¸Yu6QÐ.gý1Œ ôÎÝeÕ]×µ¬Ÿ¼”L±Hö]Sw<Ù5vùÍå¹»Õ –ò¾¯ÛºÈS[L¥¡*cG;Ûœ¸é®òaƒ«<î2l•yRRèâÜî0ŽáªìÁÎnW¶Ç(-Ž‚iãíØx<ÚsÜ;ÇÇtÃïqíêX]þ°6 JÉm‡JÔÝÁÇå`‰‡å` ß-ãë±u± ùC{œî×ã$âæ(‰õ‰a'ßp㘳•O·O +èçC<û¨€‹sÁR Œóè:±`Gí)ƒ¹팷РUú9ƒ­ö#ýv9ˆ"Â÷\Y¹‹wÇßüàåt/nÓ'ÊAHJÈ΃SÍNÎé|?f¼ž™L†Y¯eþÞÓûÁ†ûÎÑÓU„{½øFÂÝ ÄÖ'ÃGñóTaïvõÝë6üö[¯ýñÂeoV’àÕaߺ}‘äqœhÕýõËázñ ×6Ÿú x}?ŽxÚ!PWÜégºJ/A£{ èÐn·UD8nN.6FÙ–Œ­E8€Q@m¦E¬Yâ^<ÍÊÇç˲—¦z‘œ5í%Ú` ®ËéF9^–éú«ŸRŒåØó¼²³‚"Á©s?¡,é Õ‡.jTÅEy<¦ Ebyù#ôßû!N$°ßY\ö¸œíœaB'é=ÔÏý²‡å º"ÁB©Â"_Ъ`¡Iã23%ŒX#"ü†aîu"t.R”Ž3¬•[Ž_y耭aD¸_Î'-æ~öœ»tî‡.y†SnøõöpžÖ½d#¡Ñö¬Â­Uçfám{Ò‡OÛ˜?ÚËeB¶…?fÉÆÇÅJk³êb:4³aòug8ô=¿õYbÉyñ÷ã "z|wÝf¹nGf¬S7¼ºMð½ï"“ë4’ &ÒÐ{üã}7¹vh£l#žÍλJC0™6’ÉýæÈ«5¯Å¥š´î.#J´Ú§…¼” Zµ ËÏ^û¹Q¦åD­[åùçÀæêå&³ eÏ~ÙàlÍ?<û²ÊÏ ØvÀmà¶Cn-¢gœ`~Þxúçå0ñÕã{dÒÀášÀ=\JÇ©Ëæ†IzØarç+å{’‡ÎÝÔ‚¡„HU˜:\w@vWOÀqɉ°&X1kò¾í¦X¿Ó¢½ø] »r4“n sÚ¨Mkyþ§^–éú«ŸRDS6i6ÌÝò¾ÃÆž>nl9ÍiZ6ù8Ðié´;6«›Ãì¶GÙm²ÓŽÝª«Ò±¨ËAvۣ춇Ù펳›¤Š¬gÚ‡ƒ¯+&ãhøq¼]è*UZR"Ž£âí¸»:õ3ïƹñãìøqÞ©Ÿƒ7ʪ nsAn´Çi9j~[üÍr­¢^ì×é2vï§3æoã…®ƒ€.ƒq.å¿­ xo^Š«™jk¾º oH„]&œnˆ†7ÄÃÛ‚á-ypúéâàRîo•iùçï¹Y>ø™Þ[ûgЄé£JÙäa†–ÞutÓЩŸ›vNÝÜñƒ´` ,Èû^åu?wûÈÈeºG¥½‰§|yí% þR ? +¥ ~ +´Çü€·aD7ýIöÐ[TÎê$»TCû¤VŒ;(ã× +ü§„ù:f\…M U'gÓUòìÑ9“÷UþüEï vDÕB¾¶.„h‰ûvç•qÜiú̇àÔ˜ÊA¬žO8½…Ò|j|»h™ÍBN<ä§Ö¼N+›1È”élvµ™ã ¼ë‘8å!ô;¯ §3\1_´áݼ¿:ìç3¤²ô„tËÒƒoÑñâéy2þµâ"¶n£12l’1†‹$¥ÜÜΧ~&xéÔÈCC·ŸÛ¬ýQ¢q`”ýÑÅׯàîÝ©“FºÞwüë{ÕÅ›#å×Cås[—÷ÂÉ(zÌ™ßÓ@¼Ë–‚K±RëÕ´Co)a†Æ“Òtž!6“kXaPŠÐÁ)—çeÌç¼;t.®®æ’ïfÐùÍÉwÿ-ïjgÿÍ!¤Ü°\Ž"ŽŠ?¬œäfv„{y:V+¤u7[`k»8‚‘ÞÔ-áEÝ»S79kÈhÐæ±?Ý >jß<ä/î|µé(öý9º×<ˆaÚÄÁ®Q°û¢×ø×g=ÝHŒ»vÝþœ#¢§÷ÀûœxÒ}¬á>¨p›†-¯.ÝÚ_é6Žô8¯˜ü=:"H—èÑþâ玚sš€0-£yqîÖ>'Í"Dûܬ‡úÚ±ÎãPßáâõó9 …{€g‘ËÔ"7Ûó"2ç Ú¿}G—јzckž¸m$æeæ6sÄ_®ùìFू.§MÌe÷q–û¿î—)¿ïoü¥Êa=ÝG jLãS»xFzF¦kdÄ2î#kãÖ'2â.ËQÜJ+42°ÿ¤ÌB·s¯ülz_æõçDnフ ÜGöè®iI¨²|ß©ÌÛÀË3™AŒ¦{¿J®²K­r‘Wex½çþÒ×|*ÇiñZ×Møh÷G=/®î~W·ªÛÇÔ-ynoÅÓí£éö‘tK Ý.€®‡ÎM»È¹}Ä܈•ÛÈí‚â.Âàð™6!oû ¶]dÚm4àN ~ªþSa÷j৹ÿÇI7}¸øöJ.p7ä‚q€‡1­§å‚T`æýRAÚJû3;æÎQVUZ0æû#E¶¹;æ>6]°#6Æ·âËùb°=—Ç%ßÛr9w‘k`d’ˆó>Ûë>—k#›Úæ‘8-?Æ÷¯xÿ_ŠÏì¸Ì¹L?ËìsaíGñT6¬A¿ÇXö\랎ã?q}ˆ·”oYãÍ÷ÑæK¬¹mZ"Í÷qæOp–¿ÝòÐ{ç–#O†ÿˆ•ÊC Ð0œÁ=3¢oA¤a6ËI¹8/Aæ†mŠQŽ-4=A¹>Ù>Žvù›´ËÓ*±føÙ/-2KÖg íòµÔ–ƒ ÆFÛ0Ùž¾ËE]*2ëè&Ö Î[Û/ ÃÀ4Â5=:ÏãôªÚl=3~ewù­ìÅ~ów¸QëwúÉÖæâ¦mµG¶ˆÈÉ!® iC— )nž\ÒàC‡^ÔŽéôS—7òß©K4k¶¼uGJHQ4žíÅ5_^ê)5-©&7Þi›ç£ËqÿÿöÚ3Iì/ô’Ö2-A†(füºåK|R-]ˆã«ðýi¿oaï++Û'¦2ök ØXð`—nó]8ü4òSQO¿°F9ôÀ½_hø¡ˆ8Z²ƒäÓj¤TÁ‰ŽÅÃDÉE´\Y…Ò•ÍÄ?%µzž¾µN}ÊQtkU ¡&­f…8 µ:`ïà½I®ÃöÅÇE2>º…vŸ[rÈÏe<Å÷o:#Òç¼~Ÿóú}Îë÷9¯ßç¼~Ÿóú}Îë÷9¯ßç¼~Ÿóú}Îë÷9¯ßç¼~Ÿóú=ÿóßI‡Ÿóú}Îë÷9¯ßç¼~Ÿóú}Îë÷9¯ßç¼~Ÿóú}Îë÷9¯ß_S—ò]A_Ÿ×7Æð9³ßçÌ~Ÿ ³_º‰ +MnïµI÷¡òðTÙr»IÿÛe®óÛÏzls\JºpY gÕrÞïÖ9ezÅ©ã]Îó2ÌEæ¼õîb?B6ï’F´iɱÍñÐ3Gø«Ì«C÷¾§ŒX“EÈ„4í"X( YðQR͈`I=‚eaÙ¢…¼ÐÔ]±—¡mÞÚ‡,-?»”ç”iqonË6(}=gsd!¼ïbøêí<­‹hZ\ž\9ÝÑ9o–ͺL¶Ëc]u e±`–Õp] j]¡{ôÇ’h‹q—Gdê‰D¶iDÒ…%ñ¸dYBRÄóâ׿§j@QêaÒt+¬i,‹"³ùüK|6{qêÿ»7e=e¹l–Jî°ŠÒóNÚ:ÀœO=ýäýâà^]Üæàî´c7צ›ñcþëØe¶Â„JµÇ+Ý/ÞçÓ‚ÐXæ–JÜbŽ7Ñ}O»ô0ûYÉ‚šæôlˆ JQšÓ°É'²¦:Nûµ«Mžû¬\FŸÑÿ‘MŸ¢Ãç²êºÖV{Ê ¢du¬Ÿn‡LšïáÙåéÏÆÓ4Ýp7Ýö'­ù<Ê΂8\¨ÝŽ8ýtSâîPê¶X»qºq0õ¼ñÑoóùÝ/T—®úÕwºòÖ×®ƒ -¬G¶-zØmMlÑŦw¯‹=åfº˜Íçã³Ë³fzºéT|þì^Íí´ÜÝÌnæöY3[o8þq3‹¹ž‰ÃˆKÌâа‡Ž½F.jn§>µÛðÅë¹—s«ùܹŒß_ü“e#ìNô1ßZ—󾜻r”Ý6^çùé9ó«ž.‚Ÿ>Nþ©¹àš>³ÓnÓZÎa3¹´š<µcOÛxÔéÂxr”zÓzr‘3†I!¡«5/šLu4Dh±s€ƒbúƒ› +S =JCE)P¡ŸºÜ‚ÔÛ•»x•}5n´{èÅOÅ þ<Æ#ÝÚQxKô´\l7ªqÆè¢E¥¢Í‰C3¤+Í!4d\e²©xx’Ñä:-o^ÂÝBÙb”?á>Nk¾™bßþ­ùƒZò…Šì§ò‡ôãˆóR9žn(CC +OëÆ·Uã-ÔÙ_dU¬–Óa“PqhÇnxΨ³0Γ=5â*:oõã®]kÈ»˜š®"Ou¼×‘o„×l2¯yÀË&!ß©Ÿ£K?KÞªÍÓFk®7´æSך¯tæ­Â¼U£¦÷hÌeY*m§1%²*ÌuyÚ,’½Æ\–…r¯¤«×Ø›Kû‰VÈ´Cßœ{JVÓš}ךÇZsí9@ça«6OZ,«Ž5‚¬ÁW²¢ß +Á*Ë¢év•?_þ°pm•éi{TÕ.=þC“~Jž=úñB6ÇFèZôªG¡¾÷jôý:ï“&ýLJ×£ý,veukú èBès4×É’*—&m3ýÐgÚi¦×lªY>‹1Ë»€‡‘.uê$Á²òæÅlfsÜgzq™Ú,‡Åuºfí\gùS)¡Õ~XÃö;oæVÅ®DÒßÖ±‡–-OÌ&œÇ(âÃÆólQ<ŠàyŽ6þAíûR5›nèfÒ»ãûð;Ó•Üþ,ûiá}Ú$.Y5³›yKvü“ªÙ´`äŸÖ»wÚÙVóÞ ò»ü2—žÐ+í»{B?€€{ŽŽþ¼¹ßÙ\nëãÔůñ>ßÓ{4ñEOÛ`µö8­³=mtµ[šøN_ë¡à·¶e¾§1—ó}©KË-Åmã÷ÞënÏñ}_Íøs4÷jê—júô^ý=úSêùt#RÁ?©ðô¬/s>]nñ3~k¾oØÔl¶§«íšåeƒœT Ëô^-ý¶]íI”Ã^ŸçYÌ; +•"4'Í·¶ˆ?Ð?3Xôoª‹ ê#îªo—C^KÂI§êuJÍxåŠ[·ëœŸÐè6#úD:=ÆO¬Ó/cø(¾ÞRé+5útk|ûó9ç¢L7Ä §’|< Aö>ÌØ“@³‹W#|fÓ›åjô—üÑeÛáSà¼?»Ã?¯< ü‹•Ïþ„ 7‡5†%‡XÙä{ˆÕ !|‘Ý´X|u$8N{Ô¨øŽeÉSR`kñ lÖŽ"kØöh‘‡ø0]ÅÅÝY"÷ ^dcí]Ì-¶1‹?ê¶ái='dw4ˆaüwô˜çýiãŒÃåÐŽéêTŽ5|tÄxÞ”'Cÿi˘66/U²ÄîKÌiüã¢õRúicxÀØ£¹«ùgmÌØá;÷ÚŠ4CM²FÄ’ÐÆi1/…ÅrxìV$ƒÝç%zæäÇ5“†äªI?-˜N‡2¥Åt|¯ó•ú).Ëù-A²v?µeZŽ´³Z섯53ëµ%b㸸²³ÒÃ3ÝÔRYŽFQ¼fRÌfî¡¥{yͪÐÏË[rZOÙ–hÁÕ—–7e«ïèÂ^c¾{;šö}Ÿ«ÝÛåùŽûM¹°%< q|v™>¾‹Ÿk‡kvˆñy¸*·ÞñéfYôO›\ýÛĹ·Ê‡N SˆûtëÈÄ]¹ÌVó2mÂå?¦\$û –çtJ™~ZóÏþ¬;Üf{Ýæt¹möÙ\z*—‹Œ³WÙ]wǾ¦žÏ%\X¶vv­é¦!skÊܯ÷‡ /N-Ô}}øë8þuwú×Í“¿.OýºŸ®Ž~½uâ×óÎü’emºqìZÜ2ß.ƒ²MO¼[GÅÞ.GÇN—_üÄruÐØtóô±(ÅŽ}Ë]Û°cö«S¦Õ#V)3¬åVåμÇÛæ† g§xÒ,Úx¾È{¢ñxÒö2‹ñ¬“yo'‹}Ø@¶ö¡nÈÊéYan« x5›¹¸xÚˆMœ®B×Àć«ÀÄËÐÄÔì­Ñ‰nÚ„(>öt2&Éž–`Å£Qˆ¾9‡:NÂgvö¾§8>a)+`g §ßž¸»Ï-»ófLá|UnŸøt^ c¸_Ë´ý㢴?§L_ÜÔãJ™>¾‹Ÿk‡«gcîÅ©üä/í`¨Sâ™ÔÍ%ãæâŽÐ“KHІ 5¬Õ…’ƒ‡*](O%ÁÝc ™þô¡Œò _ȧÄñ‡É)Â4\`xúZÎrºQ3Úßå„ŠìîØ9„x—]Š»ën·Ø__Ðkòùàç»à[¹qýe‹åúR.2·^~ýqNpÓ«v‡>l|‚«ÿ÷¾ƒì)Å]zðs§øåFx³½†˜ªe$_zÊ—q´ÙCÏ0tê‰FŸ¼¤mŒÝÍ  æ‘ æ/œj£GÛ\›?£c†ë3©zf*p‹CßäûÐ3uã{ÇÁ½©×»šˆVÑ6¥‡;YÊú›¹Ü3‹¿.ÛÖ0­¥+˜0-p]`yøq°C4ˆÖDƒ¦¬Ó!®PÏOfçÆ&-tÆ®^ÚO9 +‹nOn.-†U»Ùöxkz',æeÖWé*p¼\g£~â”…«sûþÞó}D +_|õÛ7¯¿|ûêõ»W¯ÿøÅÚµ­˜~û=k‚Õ|ùâÝ»—o_oˆZÿfCÐÆ7ï*Oõô¼ÓŽp5€=€Ð' +“à)¡&k1ûÜœp$ká\ +-G%)¸×ïÿøoøßú§ëy þã¿ëÏÿ ÿü/øòßÀsø§ÿ<¾±K~‡ÿ]âò‡ïÐêƒ#9üæV««'úÍ­;>¯ÕÍ;¾ÆÏßÞ¿}w~õõ»Wo^¿xûï‡_‰ýíñÍ›o¿¸ÿuÝ_=|óêÝ›·__|ý_1m_ýáÕ·/¿úÝ˯ßýòð¿â‚ÿ ?ýýýÏöÿA«1ä9TÈß¡ÌL5Á½4C6p1bhÁÍrùc1…Fî/Éž+/ºê#å~°§ìþWùÈÃUw¿dcàÒ®‡ÿøCˆsa2{P€â\.íÐò]À?5+¬ÄðX93þRÉ +Þš ê™ïRØ`ºcŠ¬@ƒÃ×ê­Ýáí¦Ê“V<È„sþ.5ælâa¯-¢k˜‚tp>Ý5ˆ>ÐkðTèÛîÄÊWðÓæg´á®Ajj‚¶®Ý xž&[Ø*§†FñÒWe#p › bSƒvªÜ…-fh"ÖbŽU]d’@´ÀsY÷D²¯âì=.šAC¼FÙ•á²à­E®ì–ÛZ€™ñÆ «ÍY‹ÕGÈ «ºw˜Z¼ê ÎXÞ ¦žý€Œ:¾ ¦8ˆèô¹ñ<߯›ñ¦8"ÜiNw¾à ZÉ//¹ÞQ›Â„à‘0í'M…2/ƒS*ˆî„rt ßt{ÐûÝãq1C9  ôƒmê&ó ž=có "»‚©Ô6½n*ú+;5$% q±Aæ«Üq '£Ÿ¦…‡~x¼Ö„¥Ó*Op•çb¥1CHj³%8B‹ŠU€Çd³b“ÂFÔÙ¨ñ¤5ò¾ª rÿ¾æ¨‹¡QëûTèóÆâp%¨h<…7L–çè>ÑÇV‘F°&ƒÝ`!νøê§8e +xJ.mü­ÄÀÇÀ¾nv£˜{÷éŽÞê6'ˆé± Rs]ÌÕÅ +:X°B3Ø4¶ØV~,XÃè÷waæm¡jcíïÕkŠÚ„ k:º_ƒ9àY pX¶‰C‚ëÔÓëw³ºòœÜ™ókÝ_7*w¸© +(V"ñÆÆæ*uc¹§vWRâ¤` +8+|NÍÊE…¿árã*<´–ï¹ÃÑ d¸3§ HÃm#:ˆ5hàïxŽC£~RÆša+¾d´‚†e­ …¶*¼L-xÎ-[ð¸rÇÜ"¢74©h(XqVýR³µÀ^oi—ûÎ9Ú”’­Må)Rh˜Ô –¨ñ`5»R@5HAiÓ±ÜôNЗ-ÂÜ ßKÁæ&õ¡å­ú…ú‘Á%ñàè'Þáyp#È>©½9ì-¾9P_LÑXéL"†uŒ¥Ø§æªUæîÇ’•O þN^ç«ó<-@ö1k”¹ð~°È¨núÈõ|(õº…­2Ì š5Ì^^¼ÑÉ;$8Ç3WHÅ›Ù\*n-ÞQ‡î 5×(Ž€»`Mfl¨ Ë :Ç{)s­`/ae‘©äÑ»&trÇã€HV±½b¶uƒyÀãmx¬mpHœ`º¸Á¿êYÑÊ%UFÙó@Ðе¾¹çÕP)½€(Ê«Pø9~ —§¹ “º©…çžQ¸¤wý]¢¹Z¹Ö¢ZiŸ ÈDS?ÞH¨çÉgÖ‚H™ +XVP¡ÄÙ{ +4Q§.0!}çBÒ +lY¹Eã`ž‡`ƒƒaå;-,îPÊáÎ896Y0Ô.ÕŒ=À]SxÔ´¿n¡;UHxØŸ¡Ô[ý`[ÝñjâLgfr…b’D”ÙD¨4ØAQæ΂Y âÍÊ’ÀÌ(ؤlWñÉXAÊ&Q¯îð¤ÅîþE²â™WÂ^Áë$¥`7óœ}’SW­4ó@Ó¯$3áý¡LQÔÈ”5À—]ó”Ê\ÎÍ6 (2¤+P3ô™\à JlFuŒëC]fD-ëR™ÁÅ(Q‚ù©E EW'NZ@AtlÁ•¯­>€ ›jÑû‘çæåÐÒØjN¤ÐhÅ~ÙŠé¡l­w¢®™­…§r±³@hâæ »Lv#Œ‚ ðäÞnÔœH6„g´Xƒé‹á¥¢­z€= ï„»W¤ +b'1zlo´Øw‚d +ì6hôÆX/x ‹ÿ̃>ÅB9={ñM$YyaʬŠcƒtÔ.{².lQŠýŽ"f–›BPÓYîWTEÑ*¨s°‚¾½¶®éªrqC‹hùÆØü\T/õîñ‰L qÒ¶NŒ«]¡[iÙI¶á£cGñ=áAqËD}c»ª(³‰ñ¾vuÝ  £·¨­ñZ úX¨0 ÙN£QÓ`¾[0óëº]†t^ù:È®û™±bi½…NJ«¬®Ø4‡Bê¥* b‰'¢„çt´^5g‹’¤†—ãZsG;µ²ÊTÅ ¤]Üé^…0K:cΙž7D>€“Q”ÍÈ庅 CîÈ$ÍñF71È€ŠÉ"¶Ç ÚÑ5|]QðuY »JLD£ä’ Þ@ÝÀ¸@š¢lwTû|“ž0"PPæå{õ9(A½Ã{PFÏõ¢™)›™Â³Xi·'Y¯7*­¾~twY‰wLã@¢Ñ¹@ÄgRD®©SÆùA FŘyÚ 8óT7W}f¦Ý +Ÿä– „QA —Ökܘb:tâ”ÆJe¥ö)mÆX&T˜„*´Y¨[AšEEpZÍ|™ªÜêPHéöÝÉãA뽃RËÊ99]å“‚vñþAëY^¦ +‰™†t8¤u‡7äuÖèLÖ«Íç2?þQ9Ï-PfE-ÔÆÈ}cUõ +ëÎ3åô¦1™-µ +€QÒN¤qýº‚"§ƒº¬¤Ãgu¤/TugŸÈ,@SÄ¡8ZˆGxfE!¿îêÎS¶ä„Ò­Ö‹|–Q<‚KÖ"iÞUØÍ8½·‹Ê¢àY¯14ª» ‰T3¥ëcØs`m|¬ÆŠ±dM{‡ aÔÑŒÚâhFÁ­/›®¿¥ôH[‚)¸•$…ZPól“\]1Pðg±V0d¬‡2ÙE[*ìAÑ77s©gÌÂrU úšHY° c3^ :”B3:Ó#]é±¤í¯“# +mØlô$BiSuVÐKXeç¡ý­¿ÎÐLè·l1vÕØÑ[¥g’ +>h2éF§BØ“sq¬˜©² Bªµ]QUâ)2TÒW%÷:QŒdᮿ¸@¿ù +älª©Ü±mŠñŽÛ T:.â¨0Yeò$§cpÓæªÖ¢£ù#è,#øຂvác_IC_—J Cat:à¡Ró”ÉÑîbˆOÃ&¥¡VbÝÅÁ£#3­¨ò ®JtN¢RcEiAßCåçŸmWôŒ>Ÿ’Ò«N +¹z«d…<ÝVO15.fÓ¯Œ›;Ó,2ïï4<=í@ÙQší±R®!½ùz«‚Ügð¨ËJ¬Y@’¬¿ER=B¸–‚ÄDó1<ÚÅÝZ³,pZ.ßÚö:uArÞI>Æ! @N5l’¨ñH´?ï‡䪬’4=ÓŽdji5¹>iu¦É²vD¢?›# I’ÍRñõ0ÝÒ,Ž9FöTÒâCGwÈÒY›+A½¾®Ø’ß«ÊŒ™‡F„« aÓ'¬B—Q á ºæŽ"9õ‰ÃP©è433È.Ú*£‘bE¡z) +‹SžFÁr3…»Rä¦J "UJ×s(BIúéþ§¡Ô%iÆ™<ƒ²çøþ_¥"ÎFzH3ç¦_Ca£à9AÛEkzÃ(CR¥[’âªÃ¨1µî˜fiiSæ¯fuÔüUA½–às'IV­‚öUt74+ÁM«‡c•£’¤ +ù¿ÀLÀiT¡©c…KR“±‚+åiëšYƒðØèã+º*ÒZÅŠ,;Úx¨ c‘ßNB£„,Ó<>ÆäQ©|9 H~ñèB79€ºá*Nœ­‚¹[GAÁ Ñ“ykS‚r¨ïháß䇴1aã6Udj¡4>qãÐøDmŸþóÔz¬”bÃì¹$éźÝa†ÉŠF˜€º“[Ýy¼`.î<ðÁgjÂ{Ñg$÷¥ÈgÄbÌNOÌ!†[…3Ãè°ƒ w[ý—fÄøÔí-¾Ð +>O¶ÒjCOhl¤!Bvu1•öò{è™Si6½}ãXyzAö”ß9uq ôÍâŠT¡50nÕÑ}ªÉèC¢'a+žfB•ˆ™Ð¨É;e¤e„n5Ïšî„}4s£’j4J¾„Â̆i²„àµB—C‹ftµ$7cëÀ¨8±“|'›/$&5’;Ó@ Õœ-½@ +ezµ&#â=æªPff™ã"}3¼Q“ÿD=¶åNIpî¬èúpœ.—üˆŠ(Ž‰7$&¯àòáú° ³ÀD‹†õ Âg>ƒæÒƒ +ñr˜U/"ÒÄ.â579¹ï¡8ЇiA”jxŸÌ˜Š¬m¦×ï©ta‰­j•AW¡—h¢5—C„ jAË[à‰i¸ŸÍyˆxÙÑF@?Y¦ëm Ø +’Z9 +4ihí r}Ý©8õã ýd\nÉž@ +v¥ÈÛ#‚rèR¾Üe +$‚T’¢ÆG¡ZË7Ì½í¤ $·BÓbí3ÁKD[ã‰ÓõÊæ™M&k¾è å‹&æHYïdq›Ák”ci“¹¹ÙÌéb‚ûS´ˆPúæªß·×¹™j)¦ÔS‘Ã`3m$_TÌÈÀñ8³÷¸z枥љ62Ú!*S2ºjaó@”ˆ9~°ãâ­~f¹²4S‰ó CNB¦ÎKôZ]µ ¬€[c´xœÚÖß·"ìÈѹ‰F[è… «Ž»iºïdœÍæV¾Y3D¡˜3Ôâpw²U%â4`Ù4zOc¨=%‹(îhZ¥çÚ Åbƒ¨9%mª½I:»€Ô¾Üǘ9. ”m9¢"‰‹‰ts±ÕœJ„!=Å3 |§ôÿdk@£U¢¶‘s\Wl ôÇ[×Ì}7œ³ˆ¹§R¨vy­=³ÓRË Ë÷¶£/ä“å"P "*xì§7ŠÞÎJyâ²Â¦5öiû쪵ƒÆg™y Sì£*}T y=gJ‰`ÍØ)EÆbˆE”ðÝ_wÇ¿L %ÓWšnôÃDdÞ,î—ôZå_Õ«†XJÿªl™àg YéP‰1L]²kQnd*±d´|Ez1-äÅìc)]W1ÒŽ÷’ÓP¯[y^Õì†Pœ0¹”"y“¡q“jNJHXºçÅ£‚VU‹8e0¨l­ ز•“c -@U䎈I<,FŒýðõªó` ÝjJäØ)”ÇŒ‡%k…wÙŠžVõ“t²-CeÉš‡ÁBÙ¨sq,Ô Ô‚2M&”d‰’ŒØõRö™“– hÂÝg»_Z@à%­‡¦õqVÍên;hùô9CäôTö° >®FGªÌ×ô7WÏœ¹ÿ ÍL?* áŒ(ºúÚÌñÑŒ <1§rk\^K‹/¡‰ØùÚA2µÛx4 +A5—-¨¤®]UR( ƒžpZÉÇšŒD4•¯QfÄD‘HÊ…íºA–PFáÐÄA“–k•Þl jÕè-' ++ÎlÜOb_8æþÇi‰©#ÐZ‘nÛEYωú*Œ¬ q‘¿1q7-Û×Ýé_˜í’p[4ÂZeO zÓŒð„ÓŤ9µJ;5æ²´ $œ&},4©ÅLOK‹*tÕ©tç8Å4Ά`~ŠèÀŠ¡ý|&‘‡@E¾1`(Ò–@3*vÖ·ënPg„÷õxZ¹øžVg14G®:ùUÔJ4*SÄS'‚¯Ñb]³Õ3E*g‘»LÁ…A~¥ß‡ûjÊzŸ@é­ˆ)·Vâ@Ó› 76a.è¹5åÖ›B6]y¬ J"69b.~ˆ7¾EîU,¯ 0Ü@© + ú™‡ XqÒ  )S¸ÀûNMX !k ¢÷Œ °EÉPPïåZF¡gJR3ˆÝD13­ÖžÔ3°+:0Wr>ÐkCÕÚ™áÑ4ÂOAq°>²Ý)*ÓÁL㥴¢"w OÓP õ-Ŭ +Œ€”V9di@’ f†ÝDnŸ¼øù„Sͤ3RªÁÝ4DðÈ,ôˆ*¨k°‚Ñô Î2xšDÞë<¤2µÄ•B=Ó!QìrÙ‚ˆÜäV·d” VS½@8”\}êõÄì²ûy¸ÿ¨Œ™¦…EF‰ ¦Ñ†ÀŒ4 æ]¦‘EtgÑ ‚n-_á‰ß+hsbHv.Þ¨ :G=ƒ( ÛHçï#§ÈC,?ÌîSo +6^o’W>…ŽË€lFÐ.tlžÔÞ_P6W1ˆE¤ÖS…XÎ4Ø´|þ²În!°žÞ_d`|r GyW¹_ ¶—kLJӮàH4ãÏ©0€ñÐß®WMˆ?##¥ÞJÔ;×·P´Æ¤Ñ2’Q±%ô‡ÐA¨wL%[‘űU¦]Ž­Àÿ +[es×8rf¬mfl‚SGÝša3y+o”㬹ᔦ_ :šY©Ž¢•â?©ûVg7bº 1d.4 Ü 0Ý¥7 $…]-Ê‹Ó„™Œ¶&¹·œz"±DÚ=‰NPÆ Æ?1êƒêD¸"qrà’LÎxyŒµP´ Ö@CàXÕp¡Ü£^îô(åŸG¢ƒ ‘g‚{íž™†´0()Îy$¶4Ó Ÿî‚gÂÑ“fÝËîÄôîäiì›X/‘/Òƒ`¼§wªt‹bcvg´H\II+/Ó‹[)<á*Œ¹å¤SeÁpʲš»QÉ !… HúL ½`ôb‚i/÷T:IÛ!±¹Ë´öôyrÃxðR¬NÌ_3³$Á.n]P?ázýÌ@$rÌBÍ àˆåÍDôÞ€4tL_Nh¶RÜ*=‹Y»…%ÚÑ71}6 ÉZÅÒT#ðTZH±ÙÌhs+ŠŽ&àVÒ5 J;NcD~ƒÐPb‚ÜQe£è2‘Ú˜É m\—™ÄX)UQâT/ŒÙD /ôû‡Ä·!ÞHÓð‹/˜&ìyøÛß¿{ûêõ¿8ï¿þúÇï~÷æÝ ¶Ýåg°X=ƒÙ9É+̂ΩMÞÚ1&‰ÈUN*H½Në½ß1e{Y >bŠ†G¦†˜ísä¦3¸½ê0±jËÈeV0.^N¤Î™óŸ¡àLT@Ô䊑v¿zû*%qùñTaˆ±™4ˆ%‹8Áø7W“­¹ ²Í@,šeª¡émëDKoi!#åö† ˜…Ä°9(ºM¨šÍ— Mˆîe%‹šCoNÆoãÓî.ÍB=¥¬¶Àù È{`¡ç VV‰–©àdzA~Lk*AtÓŠN½»¨r-5–…¯0wBà÷tnX't ²w‰ Tu0å™ÁšdÉgM0é’F¥ÔÓÜO£ _ùĺ:ý—0m2… ÃFá›ü„_'¥þ•ë»ÞJê!s1…[)ŠŒŒ‰c“mGà ¬¤ìMµm¶›Äs3þÄÚhšëœÌç ­@]„æFœXë¶RÉj.Xf6`+Щbý{Å~¨²…'¦ÆÌxˆ0‚$9RG ŒÙ^lH¹:0W’:¼àSRÖ³ÁD@±èݧ †NxºÖ圢³œK…<–ÙJh ¯$¾ÂPMâ\Ì žn8é– +¢:ý35_†Ž³J,<0¸ÍÓvý ÉÞÑ@)6Ķ„À¡ŸÀVŒÖ³V4qUžÖ—_c ðТ +ãN|·Õ©ù[T‚ºØ‚.¶ðs^ƒí¨ï¨•p¶‚j+™³1ùläÄ(QÕ÷ Ê4;ÜÕ‚3™,ãŠMôN¨¿„ðwKŸ'"›ŽŒlæhˆiDº +Äe¶WŠºE3#c÷µH ¢ô),ÊRÙ=)³©ƒÄÈFÓ!@\7´ªÈïÐ=2Ì^SÔbf™Ms…‰(^ʸNü•ù8É_‰mZ|6!˯eÞ!9oi¡­™ +×¹büˆs(Å,ä¤Ñ!8(S­&l&ú®ôÑȇ—¿§[Œ.R6Ês2 Š.QS£YïÆ „Øÿ©õÌ[Âã9b2@_$© îYUQõ¸ø›Rëx‰Bj…q©U¥oÁ¯$¢ 8ŒÐïÚ)_…ú+Ø‚ †Úé¬ÈòJGãrƒY4¨½t3I—$]£.t1Ö8À-5þ9öÛrÛò¶¥?±…‰Fšµ…§¯ôlTg(pÈÄiŠÝDöçåâa2)o!»†i*„ý{]Jøí,~fÈ»òFÓ¸ »Si`0{¤(\ˆð yæcy%ZüˆçÂÂsÊ’a¯t«à˜šdË‘›¤n’ÄÀ6®P…}L*“#u>œ2+ d®Ÿ™Šn”EÝ5µØÇ{™GÔ`½"EIã憢›œ›‘Ög¢õ›\YráE+Jü³I‘Få{pObß(Ckl† –ï€Þ(±¥l¢(gKèN@ÇTˆM>>³¡;¢f½5;ebÌ[W`)Ï#Ip±ç3r´ `Ûs'8Ãcú$®BLƒek)T7î[,°¼E%ËE /Ɉ¦šÌžëôŠ§)E@ÌžÛçÌÌwÍ‹~nùU‚{äFôŠƒÙ˜™ < +žàÈ|˜Ï! Ýˤ‚:HÀðv¹ÈÓŽFÖÇU§\/ â ³ìÉ´Ý-;x¶%ƒ‹ + ¬T"­²_&¬$Þ•¾pEQ†ÎÌ^ QÂœ¨_PX¶`’ N·²•¯™èBÁ01*ðŠÎž-º€3'««zžË4I% p‡ƒ ’`4:Î}·QKp"õHJá‘LÑ唪HÖFSð8†wR/S¢+ø ÐÓvj±ó¨ÊÈ{ET)O ++,­Tdä3w´E²‚úr£Rž¸þ¥×ç¥;hX)XœÒÛôAPÀe…oJtv'3+šn¦yË‚‹{¬V .—&™™Ž"+¨2ÔõXјð òe¤ñˆ‘!­^¬™4¸(Æ¿BúëÖz¥lÁ‚`¬Gh!'Ê ditÁV& 1¶„QøZ’%9Ãc¥rj4â°àÌ”G^W)ø3årVTÎ+$!b-Sª¸+¯Ýa[(e¯JZ³X¡(`ú› ãb{UX´ÓT±€$fgÙ0š….0àTú +ÅT™3=çÌÁˆ­Ê\›$ÓBMz7B]ȵ-T€j7mÞü2ÌÁ„!•VrZ4©ô3o#¬3½kø Á”‘í‹z´e1•‘IUqÇ¡JÈÞ´¢Ìº‰ñìÔŒª¬ËG€ôdƒ)’‰9w$<Ù“Tq/;Á­ SþlTxKd!'ÎDxUú|A˜iX.úÉ°»±Öœtá +u›öR¼É»'§ˆµ$8“–fz«Â×$ðUÜÖÈRÃÜ>*kŽäñeJ›zP:œjÈ E¾™-ºåJÖÓ šëYø©Ý­ÄhÁ}\gY37+/€ï0mÅ™‘q‰§ 0ô„G±Ž¾KrBIÃ@pjþNÉ£%Ò¡pLu™i°ZWpYÉd~¬$Dm©ä5 Ïp˜@á—\st¹¡l¯P0Ðò$>¨’9y¯¯\êNv +pµ¢AÙò¦ÒÕ}\`ÑTýŠô,纽 ‰õ:ç*qw„[EÆpù„ï-Õ„’¡Q _¶†y (MÁwIP;o"w%Õà•d:å>Tš»9•3à UÃï:|¶q¡Ã0™‹­Œ¨ÉôNÌ@;™b#çÜ£ˆ°›d2·Á3h E6X¹F)Þ0f 4É¥«¢H q'U<4ž”I ™û£ ÂIÉDÊHS` BàzÖ&DÀzh™Qì?C°Ä8¦%Uq:|ìL›ÅX s®`VÆ@)he6ôwÓƒ5B¹ã‚<Ÿ³Ù•›R‰&"mIM±‹xtãÙdÊœ%»&6o =Ç!³ÆUœRÉ,‘”ÈFÄ r²dbÁ»8¤…0 š‡A +õ`ÌQÁ¬OÖKU óš +å-dM¬PŠIªËB½Z0+èƒeEPøs³`ÞÑ" ¿R: íC> e{~OPAÉe˜'t„™¹ãõ•m0ø,Ü-'š¬‹ ô÷¦Í„8m’­Ù2ÒvÀtŽàòÊ â¯£>ˆöÔit´LŠ¾ J›¡$Þ;ËÑB*ŠŠ¤8F¸$«ï+›ëŽADÜÑàr I±QIô;Szð=(½_^^û8ðª$Þq¦¯2*MÒÄ£‚'’¦3û+,ŸPñÝŒÙ_zy5Ž4º£#‹•)½*iGe=pMQ[MJÕ`>ª‚“Yé+ ½Ñ4É@wF4…l-èé¦\±{¹§d”̉&œLJ1Õê.uXØ™ÙP›°Ìÿ'Ê$žÂˆ% †`&LF²2&d""=ˆÀíóJe‡C&b0‚Bæ„ 4%ør&Ñ!q‹Æ()d +IÉlpó,£fN=S™%-Q‰F©(¶*í§œph3£ú*I€L±¼Âk™0—™EN5tÔîÅÄ"Ô£R4ÐSÎĤJsè ¬Ž¦q¢é–AZ +{YJ ‘¯´Ê|ßõ&§q#üÄX8^ª™iè±cÈméDÒB8K؈=.k™9è5½‚ä–QÒ—ÌdíA #fÆ ñô<"mÉÄÀ˜Š#^{f5E± ÈäƒY’2ù\»^Gýˆ GÀm„î¦ú¢L…0 gOP•…0P+€‰Zy¶xI"ŠÄÚR’­(©2+P«i³% HŠ—=Å®¥}„REK`Œ]ŠD3Ÿ*ÃI2ÝTí±¡7[U×[…ÞM¶q xÈ6d!l{à.[2-¨+õF\oD>M×?ñ[lŸS¶„%Æ #–KO-˜-i[$›ÄÉdk,C +”ƒ ÐCzr¦ðFl³¨Ô£¢‘h˜Q—-d‘®Êbì- ­± y•1Œ§.ª2A3EU;q ž¿0‚,ƒ» ç!aŒ¢sÇB¥ У2 ¥ê¨œœÖ:nŒ¨1QKŸãšÇHé[ +# °¶¤+p;šƒ›±‡Ê<Áȼhañ]²NÊP)†Ç°J‚ÉѦG_ªÁFâHLÈ5ÂS¾H]LpjU¬<–¢Ê ¶&[!S&Y`~ëbæ®`è<£0ý”°Åh›&vF‰â(ꬤhä¶ÔÒAæÛÂÙŠBaœ+TÊíÅ +RNVhMRV®Ì”83wG´ +f¦P+Ko†h&ô)Ø=%YSÀ¡~©Ø&-cì€Sž9NnMœ¬E‘„TÈ>kŒÖŸ*d€ ,ݦpKaCû0-‘à™ù,å!ÊÂ%1»2YíJÞŠ" 'l©3”‘TI"IA!Þy¦Uan!Ùh° Ž‚8,&T@;Ó2’5ÌÝ ›(Cz%ŸÌ  ÆQ “^a˜A+É€~@*4aò…¤ôJb€Ä³J9¡±—€U°}Û€6yË›{BJF2x1Ò:aK2Mw—›õ#­Žî6 +©L/¤Ø9H Øæ=’HN9¾Í# ³à +‰H8¿“v(Ègî)a¨i2Ï +XgÊK=HŽÊ¼ÈK-.›V9æÜxŽ"Ç>é8¶Šò`)…¹%æ´ ZŽd Çw3A[PµÑ£«•OÅQ Èlj¹çyƒ«UAªãÕÊÓïÈûóÀµ ãd‹™¨<´àIl©.v§fØܬ´K3áfÈë<`ÕɼŒ+”RèBa¡rŽ)%¦Ž½–Ì®¡¬MI©ÍÀ¨h<,†¿2Hâ@á³CQzV‚5%|ì™ûRÏ­E€r0{ƒ ¥´¹(k§ë¡p$‹ +N½<g¹“Ž;5£s•IŠIÙ ï”Ûž–¡ª—HWáÚÌ£ª$…™ŸN± ñ„ +–Ðå}ž"$ä2EäÚÃ*CÞ t¥, ì'2™¥tÁÀi(27*T€ä‡fÃËŒ–ŸWØ,£•˜#®.'%£Ó¹U‘6e±ä±Hñ^TnTN2Ÿ$ÉZDW¤S>Êd2CÌÌf<¯–´œËt0 Ö!oÔœŠg’7È…/mÈÈÂÃ1$[;+2ŽÊì0|G䬯¦Ð3‹zæbu†þ„Æ#.ÊÔSµ‚Éåê¥K +œ½jÊŸ±[åíuK÷rÒ¬H¦2ѱEÂ@õ#% ”Ìûj‡±!á“÷;õuX Å­9&×_ï$œ CÙµQÒËû) °¨hã Nñº~î‰A«µ§%¤-å–¼rG3)?áY¡)‚—*ãÁª;Lxc‡C¶íÈsD•ê¯* 6Ɉnd¯(Œß “Ìãmä™ {€HÞÅð!†?!°Ûë½”u Fg:^…©æÀ¯Í¸Â̱Y‘œŠ)ç üÌ•ï³,Atë$ÅVÈ›øDÆÉRGá—V™^ÒÚñ¥jÅ\»ê¦X+R1¶"Pü7“…~xëÆe–˜•¯'ÛΖƒÇ®Ž:v­Qܲü + ¡ä£íCè)·O‡É¬/m:ÑŽN%ŽæeÇÎ:¤RŒÉ3y`™™ö„® F‘*ÑO´mÏhhÑPð9\é6œ…rI=\0’7…¥BÝ1!beH哶^$så7Ç4\r\ë,€Ëï™…¡çn¼ªs‰€7(Y`òvX%[ +yÊQJز·¤°ý{õEÌ2‘‰Çf +ðe×0á©"Å£¤(£ÑàÀ ¬IýD%fÀ ¿YÇ dâWŠ·$øJÄÍ +Š™¨(<^T«N¹J‹Éì‰aU (•²¥$âùìVÊ–˜xÎ¥ÎUˆâçªPxvÉfC󟱷‘H/1jØOŒÕ¦Ø"qJ·å`êDDJ»”C®¾ŸÍGn½]TR‘-øèLú"xŒ‡Ê)?pÕ©d3½3<>Û*Ʊ„j´õ»,„„çkÓ-ƒý+ÇéUÅva\Uð¥|É<©'2Õ:Ó°y³i ãÊ çª,´pm÷ ®2h;”™?QŽn£ Myç‰p¬Œ¡¥¨pY)دYd+IY)؈*èxaÝâô )ÂhV:~Ÿˆ²¤ek,NÖ9¯Î +aîL8^ú4&È’˜ß+‘s³‹ùw艹+¼ò zžÓžHúháòÀY‘PAéd3:3‚S±@Éb‚— +3«XpßÒ˜RµtúDã¦#ÐL&;åG® +[œ™šÌØ}SE'–w15ôƒ†-¥µ¢”­¢~ä„ä¹…f|ð„˜dqÒ<Â0ßËß+xáƒÛO‹Ã“VDn7¦cŸ £¡Œ Ì!×^fnÁ/%1Z.Àÿµs[™%IÏóøÖ¡ìƒvì#óÐnŒÈØøÄÖQ3ôŒA`iy<à»w<Ï‘µºêo ‚ÖˆaVETfý¹‰ø6ïÆVsòm!ÐʪNå(Ú癀}‚yT0è‹Êsƒ[t€N”öŽ ødÞ‚ŠlP’aBýù.ZøXÕ¾{ +CÕ(¸É»Úf;*k*77ØÓ†÷V@°7Ž×ª{·L­¡‡hÍvAê± ù€Vg“²O˜‹°ÃF}›Fñ¼ +7„,?æhCkŸŠ@C›(Í*`«‘2ôì=(Uó +t[aD¢ÛEËÆÖ}¾u[TŒ¥Z6 +ɨF»ˆ ½?Ú‚§ÉÔá³ €Ê Á=Dc +ës¸é~~©y +…8'Vqàâ¶í‚CØ‰óíSb&rÙ‹GÊe­q +¿ýÊC÷ãoø|‹ÉµþQôš°³® “-ï¼qð‘’G +Ìÿ€4ÞÊŽ¤üu ™‘{Ùƒº!言7SvtÅÖÌæA)°P–W?ëúFø56á +¨B§ña—¿ö=j @Æc ào¶ýUzáœ`<=“mm‚'qä9J7¶ <Ÿ˜ÛÀ]w¡)i&’ÖÀ©í­IBå?fD8{Gçcõåsaj“JËýA7 +Õ@iàÈ2a+š[›^ѸQ×ô2I®:xÍ(`ß=°^kWãk;Ž´ëÝ£:¾f4ÍÌ’|U‘0foÂö¿OTA¼dš¹"³9ßQœ(¸*ªl]†,„F=­d‰Ý–.Ö͉Ñ™gÆ:ñ©—X5¨¶ÉN›ìOäÊ“zŠ’«y#üBÂQ›Î03–@MMAÅÅ|i€Q’Y—uäp˜wË…H„ýãgÄCSx¯<Ó +5KÜ-ðLµ»u„S=©½Íµèä݇9!‘†Êz©ïöë³Øß +¥lÝèù@AÌÅZ<0ƒŒˆ¼2Ã8;hE^×™c?+f:žþµ@‡+¾«’“_s#éÂm8í²½TÇÊ~¯§*”Ó¶:_ÑÒQð—»z…ü–à´ÂŽyiF`DôµØ+GŠ6<¡8µŠ¿ý–ù¨|¨`+\Ä2¾¨ß( +Ç­x]Çàú­§X(:Š„ã:Dð/óÛŒ›§•öÜ4o›_•[ÝÓà f1Îéß‘yÌGa3MÍJ#ÀîtÒ¥C²"H\>ÅuPIÂFxü½o˜ÑêVt)qŒù²@MÛ6=£çáà­Ï.—›+’­U,©pQVgèq/ú´;Ðê/£HŽÀéJµd˜›à©d-À=¦;© 'Ñxe%,W—ª¿& ×$Û+3ÑçÁnPë ›@å!5ØÕDÚòµ"ƒÞ*¬Y˜‡ÁBŠ`¡AC4²A;BƒnÄF@ͬ6å¾<5üÖ,¨Ý1Ë•üO®¢²’ðËî¾Û`ŠÑet€÷ b&fH†¥GòÈÿòO®R„œÕöïaÑbV-êkÐUË¡‹˜fç"¼d+ËéŸd;¯œ>"ÈÝ}£¯ÛÒu–>Ñ´Ð& ÷‘…:Op™Q +2 +ÊIžäÇŒr‚Úî÷¹)M0e̺=Ðô€^•êÕÇòÖu¸a/A‚•7€€°õý©Ë‡ØQµ‹=_Ù¢þ1 0ƒ¡©q­! B}Ù +õó¸ë„·þëF…Eàãhò–à&Xø¾ÑÚY»ö×å0È„+¯ø›J÷3ào|wÂ9ƒˆ$S䆡<ïpNVngxÆŒ3¨0…çÇQÿxÔoY0²xº œÙÚZq·/JÆü7x7eçW€0L[¢m°gÍ éò]Il|ߜԮ˜¤Œ á°‰éY=±AÇK©ˆ÷6ŠñD=Fh_LJ[‡_e<î_ÙòÔimi3ö kÃØ2ØÍxˆ‡l÷A>fœÇ&¨»uÅþ_Í¢Y7%j* YD[w¶¸´w…d)›¯!vûVÏAæû¡AnÏš5iØ(K«ò3tc +2yÆ;I/ÂïЙ2Xœ—í¿µX„æm Ú”Õâ½òCmhÝd¨ì8€ 4ñYï[’*y…(Ì)´¦!–žÊL µòx|ñ÷òO©;úA£óˆœˆ” ™üÁJm²B9½l½5š røLÐîh‡•«+7¯é¨Ÿ3â®´°ë€‚ y#·˜5âî +ÎG³'~(=âꦕäçŒþÃöçÐleŸécm`î.†È2Àkèm¡E¡N5Á"6ñ6™Á¾¾M¥æísF\4”â›J,uÜ_'E€â]C)¨Š½M!¯ï ýD¿Ob„‚vL¯álvê ဪÆD0{Já<;9§ñ9㼤¨sò’VTn?ƒS±<˜ œ$6eN@Ì–/g ‚ “?Û¶azκbUQ’A…€ /$jPjìÊ€¢ÕVÄE¬Àƒ‡UCÁø3ââåÀ?PÜðyœ£ê£Xª¥ˆ¥²bè»ßTPοàð\ôuN(5#a½Ä¿'eHWkg  %ƒÿVa–F°h«…Ò•úÙéu\^nÂÌ;Žc,ÏÙK‰ã4?áê†\E]pEþJjÍÍ&¾·Ÿ€ZÍS¿®ò”s=hQx#:ÝÎPÛÐVA.ÝBîÃ^QN¾ø¬½ +}Ä=¡ì »ÂS¯°ÞƒEYÑ;ÿ˜%cؘ± •ÃT5deƒ(5#„â³{BÚÏI(* @-dý:ÙÙR•…ŸuÀUóï@ŠîGrÃD[C(ÁÚê£ËUbIgP7vœ–±Ë +©¸OHv…Z‘AÚA?­Úp¤‡ ­3cE¯×süæH¼QÃYÕò^CeŸ(¢Ò9|ìo«µj7a7¤Ä é[eÈ*3‰¿*j@Q`)Nï|ƒÝÓ!sÉó½"æG¿Qµ~Ú–“×r΂÷ÅŒKŒÒºgtAuô}–Jb=”Ä®kc†$úçJ·¾ÉѯuXäÖ—bö’g ÿ_rS÷¬©÷N‹^ý ¥/€ÈñŒ^‘J‡”GUòašÎÌ]LE|ln¦²l¿v»Õ«‚ÍÙ6+:¡8s])„x£Žºæ‚M#Z߆˃œ·?ŸÖ–ÆR½éýyšA«[/rŽ uÖ¶Ï _îï“ô½dmÆbû¢^ Jx0Å× H@t_^³6&¹¡y“ÿ¨½ôÿq·(vã@]é,UBŠ]%…ƒ(»M©JëQ,Š~+=±Y>Hè[J’F‡ƒØ(]ߎF§d’HÄÓ‹ä¹(PbÞ©3þ²‹C.ÛÌ(”ÀaMæ»IhP¸ˆ‚r¥æ*+.šhŽI\{¨tÀ¼q‰Nª1iö3‰›Í¤1÷a¬ƒaHÕ÷Œ3âΘ1@^Ì „cÆ8gÚFÈS·¹Ù‰IvâD§§ŸoŵР'HôZß$ÿ‰|ñ§­=© ßq[îız(a^V:WNDõ‹m1ùi]9îù\5¶“£údX4”#»~$=±(=ùî9ºsDÀPv}0‹ØþS†/-½²»x$1aEQ¼zð¡Ö,çF§pH˜ÈQ º£-<·(Sºß”ŽCkÒšÎp¹v½ŽËºÌ·¬ñ­1Õr˜/&èî1è¼ÑŒä¨öÝÑ´[žøóTfyi!: I—‡܃̘ÐJûSò½íÒê’çf©¿E\W¶;2v<çC¹ªá+Ûnˆ©c€$Ç$zò…Cð‹å9¯Wãsü1™”¬Mun¹df5 XuÉà +¹Uô .¯ÂËZ¸óÑ,ã_@Â’jïç1„!`oΖíµHÚÜ"öD_‰—:úN÷å&ƒßUE?`¤Þ¹2’¸{o[Ö‡Á$…ò +gÂ5pQÇÐz1ÂBÀŠ¡€+ñv(Dƒ5>“LuÏÍŽ>”=J$1‘g‚p÷Ø%>24º æ©É_Q[8\‰Ù?µ˜GO¼od•×Ž(W”Ç{ÂÚÃRTŽÒÐöøV“Z:´v` +)Dœ„éYŽ,‡Cë`C‹Ê]^i^,‡TÁ˜ad¬Ò‰§§ü3‘ôìBÏÕ´¾.Âz†ž"ÚÕ§ê=“à­«¨æ! ¹_@îïr>¾%¬!¯'YgzÕ¿‡½¤ÎtÆëL¾gt.&þtçã(fÏ`ïc— Ëø +ýL­8³^Á›ÒV¦€}ÊŸ3°üÈõÔö߯;°ÏmÑ^ú‰Â–¿¤]κÒþû¸ +¢Cå-„¬˜æk2÷i¥Åô÷¿ës/§ØsÕbè½O I©1|ka7‰1׋n,}wQ2Ÿ(KÝæ<èqÝá CƒDEêõ^®wÉCcviÁPœáߨæ«E +Yw£Ü¯Ns9?W¨4ž⚸ΠNŒ_?i©òÇt÷6-»‚i…re³®Žé +bñd¾_çÉÛš­Óf;tÌ.š +túý9ËÿœàmçñDþ¬ÖVQb$þõÀUãTú$ÕÌMßN1СmÏ 9Q¾ù&°üVREòÉ…É„ú`€eâß³—#R6`¢jWl+¯¦õö„£»¦èööEYõ}ÆMÒ>_Ø<4¬IÕ®®#8!!BK€‘Ò’pŒ× Y]ƒKò¶ Z›ý…› ÜbEÖ—Dªø’/G ¼íÔÒû +u²S#ÖßÒfCiú–sÊô·È/ð(!°Üy0UJëGe ‡ÞÂÈiv7ñvº~®G}R.V¤¹àû€P«s°·1Ðÿª7ˆè"xîSégË+›vÚ-Ïc•ŒJ¡nUª5`ˆ žÂõa…¨ö÷ÏïØÜN»øƒGÍ‘­®ïTuÒÊæ>®uâõ <ï]àb_ßZ+´Úi‰*§Ctx jÂ^““ÞÈqL?˾«[ŸøÑ/£Wbàr§ëS ²Âörl£4’…r~êzåÍ^éˆ:x4¥òšÖ% Oûzq¦¿©Œc/y}´É›—ávô°5o%•2+MåÞ?Ÿñ§Ä¡ÊhØ%R6Sr@ù¿Aºb¨b”€¹é¡N» y‚DRïÔSèéÁ0aÍ'ô Á†!µ*§ÀÄÍÀ²ºÿv5è>0o:FƒWH®ö(]¯8*î\gõAõìÊõˆÃ•ØÛõÆ ¥½ý­´©'}í­ÂA[T]9‚Ô©Ó$‹kìÃÆ qëÊÝô¨ßÌÁ +¾ù›"ä´xÉ!×" +X¾ˆºù@äóxç½f8¶"fún³~LëÐ{ƒÌr—€'â0<=a£Œ–…µ­[ß,¡K?JYÁž‰2"ªEÁx$‚?×{´`v,I…‘Ø_#J6OUaàNöR¯G­m% ¿å~7p#Cîœ:ˆ ›Ê” %fm„D«ïúµª£ÖþtmÝ ÑŠYÑ;EÜõB¯šÜ‹î Á(;?$ò׊ +•—VS/OE[õã¼ELnzìkuVqËýE«dØJåÆxÜ Z±È@­›vÑÑC¢\>ÿV܇~€Žr¤˜Ï /{Ȫ#IÍÝ7#Tvt›[¯Ëº2ª(c¥Åš~(סbÈúB†þãÆ° ¼†å +Ñ«ðv¯“œI-ÝÞ’¶Û3çÙ®Œà_ƒ¿ü–`Y„¿Ê$üW,aïØ¿§ +ú?~¿þÇøö7ÿöÛÿü¿~²üíßÿÇ?þñû›ÿð·ÿíwþóþùŸ~úÛÿóÓùÝ?üÓO?þñOÿï§?þ¯ŸþÓïÿáÏÿùŸÿøÿ'ýúÿýúÃïþü‡ßÿ´Nñ‹w?¿àƒðø_ùï`aàs´Òn(lm­ -<­WCU„02Ž ¢1E†µ%âcÖråtà +Б‚ŸÔ׿ԗˆäAVuL¯/i–k …Ô +é`}€c øƒbŽÖ8Q¦;ƒ XçªàÁ@ `¨·ä#äµ£Ùâ¿…ÜÀ‹PÌ‚E!0¦PÅ +³„fvÕÑd:m¶oVЊ%n„8@šPˆ¼¬Vfm<<>8¤1×$üˬi*²÷se'ßœÑng¬È<ÔÜòŠå˜‘(ÇÅ1¨ˆ®c´¼ë¢÷>þÖEÃÖÿºuHÛ3  @­ÚÄŒ!‚0Uj̘ðµ²<Œ˜ÑšÇ@w"NÐ[Ðþ+¤?ª|¨RÐú@•eÁÇ ³’ž©­õ¦nQ BàO_i'^!¸fïÓ-u—Ò ÕN¸î|>è×¼­©QZ›úZ¨Cðu…-]¬@û}+b8éƒàÊk …Ji:–¦²Ï×¢)P.p–x¥Ž> ÷µž¦vÒg¸ÿ «Òu´#…È |?0¥ü™LÛ5&¡È$Ô|c†‡LâAØ»D·¾&ŸGGD¿<5{µWÔï–”òÞ%µ•§úµýbnÄÞöqÀ_6˜hÛ-œî­mmž]~ú kB +yB }øðÖZ kPkY°VºP[߆ºˆ‰ñ#@4ÓŠ>¥Q÷ úz>ç¨Ù²IQíçùεõº £@‹U$KËýËüýÜ\±Îûð“æö¼‡÷§§!4\¥Ç×*ú® ù¡î;Í81 ïž`@BP?¾uÇ6¿þèY´WÄõäæ OCòÛkBé²°øè³Çaëneæ²>ŽYœÉ`¯øzøa®+~…e¬3 ý5pšŽqe²tÏyªŽŽyëI¢RΨl÷(Dx̨ôäµã«Çøc€¯l;ØM‚$ +l; zU~‡–ì‚Tv¤Ü@ +a*†úJ?ú–ÔËꙤ…~»*mÝ®~lÜ0K.ÝÿâUØøïr[übPUT>/Z;6žÑÎUxâf¼1z0£*ËFÛxt^Iï¡h¡bóiÍv^¯´»ù­…É53=@ö±ÒÄLPCÄ¥œ=DÞ¤;ŒÏ²Íì‚ +SleÅSO‡†’UðrË1±§kZΫ¤­ìΨÀAjŸ3\BsØËáF:ÆWÇ)u.㽡WšÄ{I°’"XÁ\,b•MWsŒUœ1šD8}Ä—È©€¶eLÄ÷«‘Ô‰¤ƒÒZO{|ýyUÙOØ&sŠÏÍ4¯žãÁChÕ 9WØ"q_pÙòäŠÞûGž>>õìÝ´ÐÄœì”d+Ã0Ö o픲 +¼ƒ²ioiÉÀ¹q€^nªm:gÜk7dFÖ»ˆæÝЬ ÛÓg‡…G`â¬ñØ+÷Œ€ í0fà^ÁŒN½Ïô׌é‰n˜PgŒÏæ£Ù2õ<”‹XÃÉjú¬ /Чà}«%È¡ë•ÕP( ka¥Æ½R*9”7u-´ò¬ÏOd[¼8ë­÷EÓ3™'BH×êQ)Ô©´Ê†…ØDƒ¾aÓÔ­S¿VÞõZôõÞÅYJWFA5#¶ä×ÊK[ÆR3Ú|Rsìèf ׬õøj‚¦°~H®ãuì˜e ^´¥ä±-fPÅbFÀÞ˜áSGAüŠŸƒ–îø%X:¹Ý·d%"£M›#4 t(ÌÒh¡…gó÷^f +OßþúKòã~©*âDl;¥°&«‚ƒÂj¼E4qY/¦» +9´Àüùúþî ¨ÄÀQ4ó½óhLn\”5Øyñ@áƒð%f`mº+.£pÄAh|EwBÍ×ÚÝl@“x;.j èD¼ô­èÞà uEÉÎ@$9°;>k*¯o±æ¡Ô2•2oÉ!a°žŸõ’áb®Éú†Ü‹½tâ1|û¯çhm&Tr«à[ï;NÎUEÈõc±Æ U—ˆå3ÕéãXaÏ7—“#X/M5.G|¾V>Ï”—*´ÊWе“Cy“˜¯<ÚCˆ¯ŽÉe%Bp*€ætžfqýé:üátz•×xô=™FàX«{x^Uš;”Ïk8I| ¬9•ÝŒù¼¹~ƒ5°ŒDøJ7›VDtcO(R? ¥æg ~]%±/ܶŸoÕËO# @,ZãŸ'3›8Ö/‡šð»¸8e ²Á ;ôyÐNaÇgË«!Z«v=åzxoÞ0D×ãK#zȱz,þhT&,©!²¹ëîE3îàêa߸å±nZÐ  ’Ã@¾bYq%ÆnlE©eßý,Ðæ‚gP”v@/Y ôžRôÿ>zÈ,ÄSö68XjüMHJð³ $éqEË‚Ä[Ÿšµld„ÆÎ@¼–5^•Í¹ß¯oì°š€s +A9DW?¦ØLªA¹‹}N³âµùÕzåP)k?XAŽ>ª5ø•ü¸"д¶¨oªIÈ @”…×5íÉçÎPäzTí4Øá‚y4a8k©„ ŸU{†Äzü@è^É78g%š(”;<Ç rr Ï»;ºÌ š¼ÄEs”øY´ 1uJ2iAW$|,)]«ð¦6KüêX¡“på5èµÜ¦Ñ|ŒtkAÁ¯+†©?Xn¤F kYw’Y·ôŠ¹hºiAbb@¨Y!r ++å‘´Šå a +Æ#šT‡<û€"‹…‹üÁµó´ØL¾ÜÇNô× (d°VÙe z÷¾ÍœóvjÉ䋽D5§`ß!~t!â@½å'üû cì× ¼—ë%ÁÌ`X­ŸFWe:ÓWÚž9èq­m˜ýjLjªî‚ º"AWÞb=` 8ˆeëuIá›úZF˜Pn¨/KI)ÄI`mEwkåU1nÜ¥7 ücÆôÃŽŽí7×#pÉ4ò›;¨—›£…ÎkøŠœÁJ¹¦Ì ˆ>ÙµßÔï¼Ð…O¡žÆ =Ž(ͧfi^çЂks0€% 0™Ò^G s§môô¹[vV‹U¯ï£°=ÔLUÀJ¯{´B¸yÃG/b›khe¡B9½>C¦ûcÔ×C"|Ó0“Ã%ÙM#Ô" +˜¤Ìæ¯ÇØÊ'Œ¸O”ÌÛ O`äGçVóæúÅqÐ×E¯2«óhuæŠyI%k œÖ Cÿ§81l§E +àÖ2}?пHÐU.šÁnA\%«o]p#žl¿€½‰U bNýbF´y¸çYѳ0Rÿ8dqïcÇßR‰m½>rî< ¿T^ó,¤Z´F[;7·Ý½Ÿð½m[/ ®(Ÿœˆþ·þhFÜÇùXk˜"X«ç’Uù1ÐSìÅáÞ¯ÒT +Ðw˜ +r6NªIÙ¢|}ÎÍ&o¤x#¿¾ÕCÒƒ(©¹6/éit nZº™¯´Øf·‡cW•´‡rfºãð¾ð­îçá×·Y)e+¢¼Áò°2 í’}g¤ª—ÂC€†I(˜ñ*d€'«ÊÏMØ<«‚àyŽùNì(ŠZÖàèÆ¥s¤9ì±2ÊÿTS]}5F¼Ô‚Ï@\º€°½&7aµ¢‰ŽjuöÏÌøëücpÃIªRºœ›wóré‰}Š‡€VÏÚú%Ò3c¶@1˜Eú»oÝQ¨L^„F£çÁç’½ "¸¦ªŽF¾›× Bö¤dRý{ è–gà<²‘À#·çç[xí q˜Ù”00X;Èç§Dåø¾ª¾Í#ÝÛÜÅÉ~ÃÿŒ^ys+?E†œøK¯` +'ó¶ý­1´µ¹ñcႹáöù-líÆ6ÉÛà«tv¥"ìW;¼ ickPÏ}˜p­ÛTyOöõõLq~ñÇ3•v5Ä5¸Â?Vνa‘™pΑ¬_ˆüÚo«¬o Q_f´tÅ¡Q"üB•÷•S‡H¨Ëà{×8Ý¢g¨*‘³P8±Ae)lw½¾…ÁAÄ ÃË”ò¹PÔ††ÔûàDº(b9`'†œ<ÚX·ôUQ¸Ã|öÇþLÇ?Aí1Ðý#5Þ¦¤*x&µ÷ªÌÍ%`%EÂë1³ãàó¡ëZxÈoç:¡ ™"µŠ?¬ER¡&ýåe¿CC oiAë+ 0Ÿ9Ü.Þç/›ö#L¬Ö€ØåkÑ7›ÂþŠÒy<Åè¨B£xþý÷€c²a¶ WúŽïÑs5\\¹8Á.5S +…nûböù7ŽAT"Ãb‘F—…%…Wî۵˕'¿`m ›®ïÊÖ£h*R"l{¾„ K$È`Îä½€à>~±ì¾¢ö¢ç7ŒZžk‹€^‰º®¶E5‡ƒþ Py¾ÅîI½®Š@ŽiþøÌÎ?b†éVx„è”έx~õMÓ,wPÖ£ ÇTÑï*}w),ó­(™Ò»St³ ë×¾˜AL2Dh­å%6õû,œT+ézê¸`*à|…ø 8,VÛõ„ôÕ毛âûŒŸw£Ba5Ò._gX8¶q†!„e!ö Qú9#Ôâç˜Ú8ekãPJQGÞ‚e®Á U[ëQ8É4\*Öà÷ÏBÎ>QU êý8kÁý–¯Zˆ w +)Å‹G:“Q®¨ûr·¸ÜÇÒŒY MVѨmkoÜé’3ƒR––ˆRéìŒâ3Ï™ 7DA^Î0”ì¤ÎÝa–+Ϻ ë9ŸsÿbзØÚIäÀ"í(!1C³WY÷ ,M#Ë1Žž:×&ÎtÁ«ã85Ì‘l€"Zwt"]µ‘€³;òÆXïò¾*wn}ˇ}LRÓ«N^\×;4Opm£<å¯Ñ—›v4m™­Èñj¥~Lˆ`‹UŒ™Ô y;JÑÊø{‡8ÁAï“Š]Ò‚S8„–Àåd³Ô@A°ñaèVä$¨^ê½${âÓêŒû ›<=`ßfÿUDõqËû'É`ä믦—Üe LåzKÅXfKÞ™Ù±*˜rÀMBš³áÔvߟ3¢Y>7f€÷Þµòý8hrÉ£–ÌjŸÏ©(Ñ‚ûbƽál+kí´åßgïU2&„gÚ`D(ø€ Ú®" )E]þMÛe²«˜‘”D•ªÈlÖè`ØÐ[ÃôëÓª¾‚ÂyKŠJO´{cF$Õ Sü, QQ*ωÔ4UsÌß97¾ž ”þ(?…˜¾ºkkF'Å\âî€&ãîÜ5Ž¡ ©/ ΃áQm¤mšrJ#ÑK¬ t!à/­Œ3VSw-5j^|1ƒr[}°=oƒIjrÈÆJˆq—“±òÇÉ_ßöŒu"añï3â3½õºÐ…ûê8hnuŠÖàC­Çy$âjT9®ÃE™£‰›jò²×U@’í÷笿ۿǜ©%݃Ýçw vÙb% +0ÓEî·:ÆPYYE?gÐKa¥èx°Íû,8gÄ)‚ŒyéÁ>ˆNíHâ]ŸBXìÛ²o +Ø‹_Je‘a§Ö7„â~måMz)–f§4€$¹¯ë0„b¾Ïxß›£Jú> G@µuUÂŽø°æ»”žJ¬eVäoÀADèLÒ•"l/#ˆÑR¦.I1W%ÿÊRDû?¶f^‹+L h%³Sœé6¶Ô ^k‚0TÓfB†¡îÓ36ˆÄ<©ù“TBÒ­¢§8W‡jýÚÃQxEp™’š¿ßM:•ž© Õ´ŽÁ* J¿…>ËzM*†åˆdðš¬Õ6ÐËSæzï¾%ý†DøŤ0øwÕ•®†˜CXÜ•Ž4äç öÜ>ŽÆÚÛà iµµˆaÇ—×i“õšÁrX›iÜ·p˽"û)ãÜ †óÁ¤ÒŠ§Ò…½Ì‹ö«x—ˆ‘TøÃNÆÛ÷Ô¶ºU| ò€þ*9NblÁ5o uÞ=~&és0Ž½ »--JSO¬h\š‚Êh \UFøºf‰chDŠ¶ùxà·iwÂéÌÕ°1ØâæX|w;x.Ðh> +Ïa ÁÔâãÝ6Åmåìªü¨…¤"®gW}õ†Ó–°ri®|,Lm}9)üK†YÙ Ö¾®ž!˜;b• VH©OMƒ†39ľ°ÌjmîYÁ` Î?f@_÷‡A¡iM³n}õ\åç ´^¹WÛˆ~^Qöh^Qôk%`Ü +l¯õ½ØN¬§Í[åϪP5l¸§éíŒR—bu‘‡Ï"L+ o–iÈ ,ð\w\ß2”Âi:²b²W_þt©t1n¦îí"o M¬-Øë°}õD˜C²Æ'¼xr²Ó ¿ò†Ueüz0< ….EË•|™ÞÁ;U¥.Õ÷l¶À£&–3ƒK`›_W—²(9áý$]$œj|Æë:n:q˜³Ò ¦Êzºf|ݺOB¿—²Ž3¦:”¬ Tá¦Ö=Ž>‡?6Bψ hcP=Ë–tùHGƒY‚ô´SPcq#¥@Ú­…hbP2²G5ƒµ•”ùd_ÜIÖ:ÆJC›×¦Ìú‘Ø}+ˆØRƒÊ²ñŽl=à‹õøtÀ¹%em’ã/¸m€‡HYÓh¬wÒJr=5岟pOÛòU˜7åŒGöPÓ*šG }Á߉) 6Šû,Øë%®óŠ; X0ªXB¤Ã„w%„HO.SW0þ2Ghx© I–Ãpϱ¡Á>MWÌšÔÜqc·Á‰/`"¤5ª”´ê’Ìèè³ ªxj¥†SãLjŽÒPxÏÈÐÆ^¯ƒ¦ÚJ¥@!ÍÏq&»ÌhòvA3‚çDïš{ 0Çà©¥’Š’2åx¶ÃÏâz«Ôc.M¬ä+¹ˆ>®P¼ø(`Œýa +  á±ï³f”Md”â±ÚOï[äçŠÎ€"Il3Ëï„U<¿Ô£,wkJ”1¿²ž­NI²Å;´¾i +ŽNø`àÆòh„¦º…¼¯••R- to|ºxrPÎÄmãL”³¾A¼Uâ6À§Ý'oŸCbqE•™N/¤*(ÒÇ»aÞQ9M»v©J×'e@ô¸ØÎ(òZ  ÀŠ¾Âq¹(‘¡ÉkRLSï0#ÿqCä¨å‘³,SB´lóHe“Íqšü’2Ò~I ~é6™™$'+ íβœMr(”& p™ 1ë8ÎÚ9ú†j;1ÃElÄiü;ד^´ +4¸‘ÇZB›dŠë8?fÄaxpiøèËèr^¹èÌ 7 ShN×Ò~#4dci¹xN¹5Þ›õ›:ŽDü¡2ͩʆÍxÙ8®IKZ§ÅyƒÂp=9tš6š(ô¸o8¬¡M‹rV@š(¥ÍWYªì@Àšï)ù‡LcL³ˆi’<°¨²R#g{=Å°•ÈGÞ‘wM‰ªÌŽÜKž)8TÌN§³Gnñ/†i'àû-l¨7Oðœ(Áu£×1kX8üã0ñ{XA}’¾ù¨oD{¢êÄ‘$?qÎx÷8^¨¶‘kì“Ž§k-éÎåMôï#í3{2Õ¢±ŸXŒR(ì3ƒW|kâ“0v—Î@•‹Œk`þÉ.0·”²Ðù ¯ÂÔΠåSø_ÁWT'ðѤäA‚@ D À6BÿL väH5ÚÄkYAJÌ_w+C¬"­^ôq‡/BL„ŵ=­§úÁéGp<7]°ñ&wÎW¥QÖ²ª›ÿ)ÿÅVY÷oÏó‚y“]OOÑ}ÆTaº=÷"_"B ›Ãº9|¶crôÅ(„¸\’\#yÏ5¢½µ +Ž€Mþʃõãoø S|zóZeô™8°$•ü(šüãž…h.%šbé9kî, ¢ˆ£4κžÅ„ã Ÿi@UáëTyXçìVϵ<€‘²÷¤ì6ê,´ ˜•yøA€ÝÌBcSÕ{ô¿P¿ð§Ðrmøð*"ÝÃ1oèùT‹îÖ¸Á^†çA³kC¿…݇ăæ=J‚zÁCœ‹ªÆ(³¸­ ÷ÅõÐëúFðtËÆý!ö°ÂEåZäókƒ›“¥Š‚]—Ó§­¶ª@ý%»µÁ­ +ÈÖ¬á¯`µ,ÀâÀo¥¿N[3ø/g„3eRùÉ.°Ì$Ý¢+Ó7 ú9“ȶ™Um/ã€Qã÷¦£ÊžC•û@bß-àPZ̸5¾DZë!wÁ‚r—µº{܇žÐî»vÁºFÁÚ€­Eß…õLзsU¯W×ÓŸáU¶¢×ÀÆ|X¸Õ³¼ªÍ Ô¸-Z: 2¤è…âRY?¢¼”\&d)ÐmS/avBÅfŒ:{½ƒ»%–×€Lg‚J*R£G8HàBkä(mÑP`çQó«iCë¦OªÉ$4¾N¤jen1z<}p°nú°iëzg‘·3ÉËJ¬wòéAŽ^¬([”ȘÖN¡¢jEd•Kc°jœ·gŬ­õë5{T4Š¢óÓý»o5-ÙcV×àBÞׯ‹OXafj;å’ÕÐ`¼„Ù*1ŸÃ'ÁA…3Gæ¨îi»„ä¡Á€ •«ýëSÛ'åNqÒ¶ÿ**ªë¯êùбÐÌ4Gƒ¿¯¿“›Ä£`¯™K Wµ[`;ßC;mкr“ÖÒ€“ËÓF†ÞH WÇg Dž¼Oȇ­ÿõxƒ%É¢«è‘M½l×W¹Iá0Ò¾;®”—õ‡ßÕÓA{<¡r¹×zYYDMÔJGV9I´ÍZF÷ÊÀR #КÒiP"ilÀ’÷ZhªDŸ¶I:l™+¹€mˆaœxQ½œŠ¿ìŒ^«Ë¼‚N»û°ýÖŠÐÙ:l[¡ÉDC<úÑæC±ØÛØ›íú™(ªø§êplx½@£z¨æ .F´Ù–­üÞkƒ-yz ðB–e²âÁ£LA¯€Áˆ›K‰Èhðÿåz@›EžäÑY»Ð}™loÙØ)× + ,7€¡¶^ôµF”àZvµ/×_-þ@)NÖß_Ï:8v§µ¢ð!„½(=ʊ.Nȳ[SÓ®¡Zȶ?opÝ ÷"‡üZ<wñH9¾UB ûå 5Ú(o6·ø>-kFž;¾äÂÄ»H0j\BqŠT/Y×5ŠM+¿txÎWü=5¢ù-sR`ÑuÅ°?W€Z´aÓúËÖ;@âÙÞa*I*g¨íÅ#žEÖ +$מ*\Ãg-ØúÚv+ðþh!s Ä2p+•¢1AºéHõKÅ9ÛKº‘AdálÊÛ¶×0ÀÅ/ËAÎÓ¬”èI3@Ìε» •«vJØIº&¿nk¨°V’CÅl!Y!,׆o²´ˆ”£á´¶ÀÆè¼ÑI¸û-bþXœrÈ ´ám—}¥÷t9QeŽÆPU‹ÂFÅïHêm`.?DzÐϤËjS8"¤ ÚGn ¶G RÂb@°EË!tÞ6rÞP5°$âÀƒ€¨Û”–î7²lðIVP]<Énéæ@E%þgL=šBùa®°eKP —È.Z/(Bh¾Aµ +tD¥!(ը·™”d°¬·”',¤Äi•½Ö¢BÆBþÚh5Àƒ˜pæªF€žI‹y6Á É“áÎf†`z,tˆrŒ£MAÁ´ ÞBKÏ‹b#GØ2Jq•Ž=uÃxàÞDàŠ?ˆ@º´—DŠ#ëš¿J‰óñ[’uí‚’ŽT}Ñ”ÄÞBx_ñÕ76ûxdà˜Ð°¢ÉEñé +à…\¶:©«àWÙwöjÖ¶tø™õ:S1…@ oº‹%2?¢hÀ<²G»•ª…—!u´‡NŸÏ¥¢ò%k “ +±çèJŠ‘4Ê_äý81ˆ.¡ º„t™ÒÖÝŽ:áH¨©zÕ3ÈŸt:Ï!hPFå(1`€ˆªS¶¾/TÙÖlNï¬y͹°ÇýMñ|£‹g¨§?òŽW”ƒ+E¾¨Ün¼w.p2´íuçؤ$ Ûíza§Ìˆþ¼ýèLå‚ÝA>å4UɉÁÊÌ!ÀÌOµÊMÿ bHÇTƒ§²l®õ¾â{¸ÙG}Ž+t,Õ•ÇÜì›l™¡F†Øø;Ђ¿à¾µ9•GB(Ã"A yyJcc©‘RðM‡¯nÈã\AXb&‡:?A¿Y’ͱ•˜ªî¡²Œœ~Ä ”^»TþÍ:2œa¶¾Üí@uƒXËÀeâ¸îPk å,ó>s”…Ý"ѻ޻$Mí¶š9,"‡láZ·YIk63’´Q€ñIãAW÷Á]Ö²‘q +)u'Jü9\¤¾] ýŒ4£ ãq°uje§Y æ‚=XT¼¡ÜA±¢8 )÷.]zjf‡Œ·Ðd¿!°ª…7>¿ˆoßž!3]N L ´Ü^S-Í=P1î»X¥¨è‡Ís"ÅTõÑ"lQ=# +äeYo–dš¬†%_Ìy¥×éaIk°9ÕGIÏõJ»YÓtÈÅ’ñÊû––iÕQZFno¡ød^äĺ  +$Èá TÛ%ädº7>õÐz[k'@¬>4úˆºÁ…²»š%s ƒ6­Ö ÚžR9e@Y¾µÀÁÜ÷<Êð8ÀîáW¥jx½MNWúSIº % °©ªõ<ù@š­·(Ç]G©æsä,çT¥‹žŠ0RÁp¹^¯*y*µ=¢ašB!¹M}½BÅë®Ðtj"(Q“¡YEQsDi±2ˆ™Û#-ZúÄ‚´ø(Mö­9Ú¡àÈV’‡ÀZ"5uá†G {Go½ø–_‹“«ùæàô’O•€nYq#¿§j¡cóztulÞp« “¢†¸–VMLøc‡iß^@-‡oëóºXáùEN!¯á¹ 0CsÌjqiÌR’23WP?LÔu;ˆÀd'£`y.y\¬[£\¨DÏ6“"«çó@ô°¢-bêÄ.1´\';e£Ž³R"ª†f,*9N:a9j"Ák„–MIJe–D">‚ý`ÊyºgðPUoq¹¿Y…XŸ#ùñ±UsC·±Bù Î³cÇ@>ÜB£ü¸ADüÅ_ÎÒ®•Yufì óLðÇBŒ=C´7†kÏ Ö쨽NÄæÌ+} ³.­åÊPM§Ì°I"³ +ü1¡ìÞfÝâ5dƈßÊçωh ‘vzr€9ðîY³€uy§žB'iNð®­ UÜk[ÚSN’:Ç:BpPMNc?L#$—¹S7"×?ùWW{cG!¾€n “y»nT­Ð[ÅÀÂHð¸7,ÃYÐ’ƒÙYoXéݧ¿%÷0MÁ—EúŸ~/e‚Ë‚a2V:Ç5ŒXƒ ¿Â¬¡èŠÞæøyb%‹â®5¾­ î€Gi‚F²}_fø±æ¤ëc Aãã¡{ÔÚ.Q¨—4ô×ÒÆÓÞQ8œ´+ï~›Ê8j°Í|5•¬¡á×Cñ"wyŸÒñª›ÑÚ©O9šYŠP9þùZ§è¨RÛã„ÎZŒ¢À@lK­§*Kuê¡ÉHÉ­ç=ƒÚ3^MÝ|Á5‹6£“çcõâÁGÆF$û9E`~Š(KÙ@1™Œm[Ñ‘ŠåÍ…íkG“Êê‘%ÅûŒý]îLV]”'Ÿs*SDÔ:Ö3Ýu%ãÚ ß®¿'0S AÃb‚Ê ¢|… +#äÖ†>&Šp4ågù“/«TKñ”ãK- Ôz¨T×av…NÎõTPÆ™‚lÕõ‘Z+ûxS?m½ÕMÓÃaˆÄH`ðcP}ÁRÀ#F~&¾¢ L„ntÐyj(€æ£ ÛÕzào [†–'@»IG.¥l¼~ö,ê9Å‹¸nÙ$ÂáD¾Qè‰î¤m%ð +57ñiΨxBO<×{C šj¼tZ|Òš§ËÝ +ˆ)Ø…ÞðT[akSÿîŠ]|ƒâ›\ų10uëÏÐòN®„b†JÅ¥ Š,Õ§m’:éÎ_9~` ¥“öœ(¶’€ëŽÍ˜Ì!`¡ ­²™²v]’§™ D°è¾I—Tϧ˜ÂÝyß^7–€2aݾ•)§rgˆE«˜ÛÇ΀jÒh±…ÄOÁBEÃG +]SøÇ™¶z\©›T¯¯šgqeÆ);]Ë3ð`HÍ÷ÖŒnš¿î¸)zç¤NF€•ˆ´d;A*i‰ ê+{+¦"/ÄE©Aâèƒßú#¦ö"2ÔYÙ˜®Í¦âT]0Ø$OHh,Ó®Á}Õ|¡@½9Ü ’⥊·~>r ‚DÊQÄÕŒèŽúŠúxÛ”•ËLû›-Ø£›Â–Ì;H5RnÁ xˆÓ¯UTÕ”UDí«›±¸ a„†ï-cîÙs…`oÆÍ¢ä4fˆÅµÜ6´o T„N`eqÀÅXÓ|Žpåª[hL"¨^“Š‰¤@ldÇ*»sLÞ4‰B û`Q›Ï +FŶI¸5rŽZ4I‹$¤¿ˆvQ× Ñ.È<Ý˜× {íôî…Ò4ݵ á¿Š=ÒÉH4¸³—½ßÀ«œ˜b:=d¨éès¥]Á­:ê!#¶º9Õ-ᙥål@™IRµ0.¡ÿ)¶@4ÍJà9Ðø– |SÔfÚº2ÄDHaò)¬Ú:d¢e9â8¦s´ÖÔ +Æ‹XÜêîY‘|H§#&D»T¼áHÐ×+ ÀŽ Y¢†ñ)Õ}þÑOĹ7xë•ÁT¹ëHQ¦èâóh¡Tάf» +oŸ;Ô5Cký}-&T„.˜p_ûŠÑU%뮀U™TñÄT#}‡U°Â¿ ++RÐVÌÒiÀìœ c%¼`ñ:´_œ¢"Œ@–òI©†Ä2Ûà•dúŠŽâZ&]zóPl—‘´V' ¸ï Õ—ºÊd¡?jÄ ´¥‡\½Œ)„˜'mů­ÈGÍÓh ÜèzkšëJßÄ6M»mÖ7«›çLB´‹í–9Ã(¦#õ­Ú¦Ç 8x›€­¿‚X#] Y¶=ÔìXóÇý(8yE›È•É~”ØÖ¶ ôÞð9¹ìÍvðÖ «Œòææ§ÒT0ßÓ +ÿhÅ÷—ím¥äè!PƒM!Ci*…y˜.òâ5k€cU¢¤ëO¸ÈDùa]á«Xk +á^p²!˜àªÛ¼¥î˜ üJùúÃ(q…F[¼XÈÁ³Š¨Ìq‰-ÛÓØÚ Äõ¤ôcK˜¢óþ][lÕØ ˆž%\É”ÀGð4À{7 í”z~oqPmÕíDk› ÍôùâÓI¾ìYGÖ«ûLû(f3$¢¡IÿÝ™„´Pö°Ôˬ-èIÁ°´ü0~gÍ’om‹2ø¨ç(‹Ê:\oŽmÉé0ä$Õ¶mí ÚØ´@â Ù{ñ6N‚vtú 8¹pgµÀ+–ô‚ç…uËš¸¸hJØ¡7PÀš€Þ6 ìÛÑg º|¶ýQPŒ›x{ŽàfbÄn=ËÆØLæ„»= 8]í0üFýƒbƒ ÍO£KcÉEŽêF’: ­Ü_£Ä,žef¡Xðwÿ&HÖ»hªÿªù×\ßBرN©¨#ˆø²ZÚ²G"]÷ýAÍ‚÷ç:v0 òÍ£ 2Bý…÷fŠ­=oU„Ÿ€…·ýXò‘ãÁ ×—õ'…)O:v*Ô¦XâR%åÞÔv"<ÓˆŽE-o—ŽõÈ'Îm·Z}ñ«ƒì´Dg_Z©HtÖÀÙ¿þ€yÉ­§3rQz&ÐFÄMzía“¶¦j„¬ÅVW8Ëã€téÜBVP8±À]ûÊw„ «QÂê6‰/Ö8õïw'ÿ¥ èJ3¤lTpÏkZgFZà/T¹Y=Ñ5 §¾€6°Ñ)`z×·2œ ¥”x—¾eq'—Šn3Õ +ÐüF¤~äbôv¢ì8ÐH'vÛÝ­¶wL‰Ú$™¶¢©8ÜRº»Õ~OÐUþq.-k¬ ÔÝ+B0†5ÒpdÒ08ô¿Bù-‘.ÿîW=ÿ•ú03Œÿdÿó•­ |Žr+ÚˆðŠ9¿¯õQÜ ¨½ ÷æ¬Ûzêû,.],G'\3ˆî‰|~™‰–×SeË׳åO¥‹’/–Š bbóŲM†h±]tƒÈÛ‡ðãUšO°…­€‹Áðð`L`¦ŠP!†öðˆüÆÜßH~ãéÕy.ªÕH䲎P\µ¶ øÁZ_kTF¢Ožãp¢g׶aܧêÙ¶³Hy‹’â4 1Mä%W¨¤¼++6¿°„îK—qûÊwd),ñžeFC–V} +úétIŽÒ=Ý% Ázz(£æá®+\²R,­3J?ЧâÑ8I¯2ï»s_öUÑáü~=Õ]ö¶Rê˜Ç¸äÙñ1w~®úô}<1ÐŽáGùÈMßaÈ Q)L…k ì ŸS‡VÌWõ|NÓ4s~­Aw8ȯ?ºñuV_ýÚ*€Ôhp¯%-ùÛ.×>²˜Dëé ¸íäî¨jÁ}¾dQŠ +b2Å]˜ðñùzòÖZü˱m´5n;Ïä´GwpäY$Ø«ÈÓ%[E +ŠêìašwŒh€šg õ ìÏŒMÜ'Ç«!,¸Ò4ª³·E”Ì; שg¡‚©<ܺþ¸^Ùi8–„4¾GAÝY!îöƒé3l1eü9¹)˜pŽ9·!Ú-®ßc¾cYo…¿É ÉJ[ùb÷€Tï5Tz¯¥Fƒúʺuj +<ìF<.Тt@ÄØú?šÍ<  ø¼:œ*$„Ò]Î6Ó(Bª´Û— Q\JëóTâh˜·x­yþ@»ôïpµs›Õ¬ÕÀ°‰˜™z›)‡µšÉsu £ÿµîÇßðÙcké¢Á2‹®ºJ6³ó~¼oúkD4!‚é¬Æ ¼¤ÝÑ3×eP#ï27x nðZLÉÔežBu.tŸË«; ³ä8 /¸„ÐékØ¿¯}ÀkZxtSU’+:.eU·Ïu‚ñ´D¶ õzLÖö48j7Îl;´ZT¼LˆJR²?¬ò4®Œ²[V"(ìýcFä¹e÷5r¶ñq@Ÿ’bQ.êÁ + +:œšºÝ‚HpZ!.ÏðEîSÔ ûËÁÀd+Q›×'ø@¸Ö¢ßàßö¤éH°b­« »ò3áLw™,¹šïÞž 'ßçLQPuµ¯?ð|àȸ!ââµÝë;Íq +¯0ib4J®W $¡Íw æcÓcÉ üKĘ¯ÍÙ'è¹r˜ +$%,'0•MÛ3ºQCê†ô_ “%y_!<ÛggÛxv#Ë:¶r´ÅUtÍ!ƒX²xR'5X>ÂÏ ­Âå}†ÀM«4k¹|,W>fõÝó®àÕJø…(Í"!L{mÇã)º|ÔSÖ¥ß\>gü¼«®’¼Ó +RBuóã8HµMúÖþ‚'¢Æ\°$!o°°îðÇ,ÍšmÒZv ß%p.>ÝñHAÐÂÒ&E¨¼G+I‰¿±«Î˜çD3Dƒ Ï…×6üéQ9¬š$AZ©_¹O®NBü­ë”lÌGR¼j„2p>”‰®„ XˆBpž›wºoÄñsEÔcdÍ€3FŒÉ~T~î«^Gi Õµ0ÃP¬4XÞìÄ"ª’ÍhÔ†®åûã`IJE¾£>|wk…" óLø¸!>›É‚6x/±Õ0ιî­HO3¨tGý¥PÕ¿ÃÞ=r!ðƒü"JlG{陡YãnAe²® â&4Èé…^®U'ï&÷í*‘ÂÚveÍw{éëYÎÇ1ÄÄ@À`!0µ¯èûŠp¾˜q³‘¦²%YkGF#ŽŸÕ¯ö4@†+ÔKQ$çä×Ü ¹ðN»0ß0c›1 >wíÀa¶n¢ØZYÊjJ4C@¸Å[&‹Dd:â WÒ) r¤è²ÃàB‘㯇D¿erÞ^Úƒ>É3S%(`$AÜÃÐ9÷†Ë…¡B°º[i’Ú¶/qëkµo›³m{k Šr+SªBiR8·ìÞ,ŸÌC[?Æ äØéRJŽ +™¡”²+za*OŒ`¯çLP~˜UöïqÕBuî¢3ÖFÏ –Öؼ1ÙÍjú7õe z…ÀrÏ.VªsÛUÓµ¡u‘¶ë¡• ˆÂp/ΰ)¯W9'™3ʉG&pÇsSf&¥ŸÂ6¸úš¨e¦Æ5u}Ƭ¤†Ñƒ´»õA|”¶ÙóH¢_Ï7¨#™¯ªõßÁ ݇׳¢€’òeKÊÏcӓÈÓl‡)XŽÂR æ•î +ÖªpXµU`„ FfÇíùÜŸønZó|©Ì(j7ºX,½~šÚ˜ mÇž1öŒûØs)÷Am‘Å ˜ ™š°ˆ=€ +³ @|aÎLßÅøõrws—hì‡ò+ý½+bm9ÓYíŠYª’°ø†¥Ë´ƒ©dqJËõyŸÐ(àRÔcYö9'mÙ|î¸y}ËIWi>ÆLj|pU˜0"¢ƒ¸»3ÎC3•‘_—o~5‹ÆœpùJì§SÌ©þ뚦Cš  Ñ EKaÖö˜ûl˜ŽÔÊ;r…j®ÊÆqîÕ¤ÂÆHNš}à•B§vƒôPm‡ˆ$ª€JäP ÌYᶿ]ͤ·5ƒä9fˆ“¯Ø„ºx<¼£ë,æ™\ÃÖr +‚ö*5½* U£Ã&/™@=Œ r˜BÐçh‡f ÀP ^Ÿ3â3Œ5*z8×nƒÏkÏqwEÞƒ  ]“eó«T°µõ«ý‡m¥#Å9ÓÇ,è¡ÜÝR³';ˆgÊV\7P!™¸Ì Sßw±×«÷ç Ï”‚à©6¾:¾íz„] p‡1PzŸa÷N£La÷3ý1‹ –G1˜^Ã…J»Få`ØÉË.Ú@c#*eý£CHrö9㼦ÅkCÍû¾¿8$Ò\0$ɱ¨\*¶|9ƒ\Aȃ}£ÃàÚX$g.U1JÉ +ž#“7}òTXë,µ»ÏqíZ€°S +ǼçhtAh+­ïhª¥ˆ¦²æÑ}_k#ÌopK*\w¶ô³Åâ‰mô¸}’*Ìk&Ô=PP<À*«Æ ë.kƺm«^÷­z^ÇÕ–eÍÌ´© ½ÎNƒ“ÂkÖŒÙ6®*jƒëyQ +EÄÍ!Úˆ…qÆ¢`Ÿ§Ÿ©Yo®Q[=C¹LäµX¨E|\DJ®­¬Ç›ÐpøЄeÏèã‰Aĉ€­€-ô1KpÄ\§¡®¥: +%œ©×¯f´@³uÊä'Øù˜¤ ?ÀŠH#àŽ€ +&êÄzZðÐ\¨ÿ5wÔ×Y‹r®ÎVeAP‡Dõô£»;Ã4$øg°Õ0ãè ÉCgÕ/÷õœ Ô6WxWç½Y» ­“}¦M×_^âLVmÑc‚c÷¤ì{ríª§6tcóŠDÛ '(î~cÍëT^õñÌùSqQhYVËʬzõ>glPÒzæÔæÿyçO¿œ¥6Xm0ÄŠ=3ôâáùJ)õûkzŸ^®cCÁ¨ïIÓc“-Hr¬t”*+ëWšO#9#á‰)ÍÜ%UÔÄæf’­5L!k/'ëyPÎÖYa¸ +5¾÷[Ídó ^½ÃV’‚Çç„ŸOoKÝö9ôvþ<Ì}ÖXîiªå#4` 7>f|¹A~ÌZ›‰òâhM#ÝRîþP¾„u¯E[ìX¦À£YPògÞô¾)x=çïÙÂ߳ˌڦ’§Fv+Œ¸=)˜KJÂÏ‚ÂÊü°H¿H]Ú„êNrǧï5¥—¹DYDÀs£¨ö˜ +ãAàcÆ_vÈ¥ûãëš2aÃÜU9PÖµY?é&`¹±á*‡Å¥Œ¿µ¾ÈøËGÊ2Etå¤2÷$n7“Æ܇±†Ù½g̘±6·=cîåÌ ŽcÆ8gÚnÅkV·t#1I@\+G”ÕR·í!ø` HèÊÐÊGü!LÌx°Äm«:ã¶BFXz‚Ø<¸1COÇúC—º9@ëMëŽ4¤qÎw Œ•Ã[¡œ²à‘餅xr"-op&¼¢`<‹¢¡ÖrÊÑÄç=­»¤µ®R,›‹„íEªG«tl©æµÞ3$ä×B¬k¢àqp‡c–Áû8Ô±æ”3j_ó†ùÃýñ™ërBªe„€¾Zƒj±ñÄ|5CUA ” ǵ÷¾¢‚£³/Ÿ ûù—0«J+|®² ¦-¦ìl«3XUÎè½ùlu…ˆíʶøí»0˜í øÊê.³vÝ)… Q´;|ÙrµJtQøžy`þûÅ hbR&Yìi +ñcg­@õ¶°ÄœP­2±e L^p„¤xÁáG=ÛPc6ötK?ÀæÌa­® +8¤H•è!ª`´JÐÔ¹ +5Ç.³–O.,ƒEÛv.G»/ÐÙd\8Z ®ˆ9ß´ÛElè€} ¿|i‡øWSˆärú D„l3ꜗQøȉ<™lh}\ᩬhËG^-œ4µ€æÑÒ +ž5¾ëîkv á0È´’Æ…°HöpkPÅ+6t¬Ç—š,RÁßÂYˆD°ï‡A áGzÃA›;ÙÃMEË®s8ÒB©âˆ8?e TÐyïçÆž+&B}]ÞY-At–¯3TyòZi \ÛüÃO>¿Ýßiß||K²¾/ò°Xö ±L“C½¤½Á;cCEÈ~>÷¨ÀÊdéç!÷|Í ¡ÅÊý^›½hËN€–ï3Tv9²ïf‰„\Ä/ÁÁ XÉJ¸Ýäÿ>Þ¼¸iŒ×MæNM…z„„œÕçnNçjÂЈŸÁ^jü ´ãÏGÑïIß]? ¹éœNSd]©6¤ê~‡Ò4Ʋ%qp|Œ± óuÿ¨=Xâˆ{ƒÝ`ƒ`€  %´lÎq],¼/+[¿~Ú®Î5:÷%M*€•ÂTƒµb#Èî} ®Í{$ññuìq4qG„Á§-EEn­(°$?gxñŸ¼ £ä=‹ŸÕÙ¢lRåheÛ'<Ç~~¬šLGlb+j<ƒÂoˆå€ß`Ž»®Y`_r”ʱ +¸{H#RŸW±—# Ò7xgÅMQáäT3Û­'ýv\ªS¹¿(¯¾Ï¸9ó|á|t¿tp*ÇÕµñFÊ"Ôêˆ|ô$=€óŒ€9¯LYHu âX<a!Ä›ÎR‰°¶€„2³_BB5´î›4tk¹ú®!9°ôïnA iÞü ¬Aôa;Uëõ- +Të/¹±Ùýü[ùºMSå(܃,ÿ¬áxAÒÓЧˆµ +û2ˆ†ÊèèשK„"2vXˆ¦hé€EMöQÛ2~w¨j¬Ç~¨f.ìe¥;ؽ¯ÏÉÁÔÔЛX„þ:öå·DÚÈüUÎà¿ò` ?Æþ=)Ðÿñûõ?Æ·¿ù·ßþçÿøõ“åoÿþ?þñÿûÛßü‡¿ýo¿ûóŸÿðÏÿôÓßþŸŸþËïþáŸ~úñú?ýñýôŸ~ÿþÏÿüÇÿû§8é×ßøïøÓ~÷ç?üþ§uŠ_ü¸ûùÔÆÿÊK¿¸¦-ê<@E÷´µðŸ^¯•UÂlm¸.dDw\þ‚LOgå²ó‘Ÿ!HÍ·Ö²¿‚]UNØn¿"TÎÃ""Š<(©Ý?Æg{´ó4aúåXj‚rF6“— óžòêÀ*Éß¡Ô[šÈ9ÚŸuxañHl×^‘„Ͳøs”uòˆ\¿[`…Ó•> . wÔ=hÓ°-gÕ¹·àëĉ–P‚˜ccExÐ0H9;:ƒâŽµ—:l—ƒ¶ï=@o{ ¤JɹÃÔD…f¾7±*“¶Ý!^+Ëê4 å}÷‰tŽæi#iCž+~ÎÑ„²”/1aê3Tt{s?JÏþ³ÉE|°™=jEÛavÅ{á}Ÿ;áM0 YAѸa-þž°n#D|P·Ú5 ¡ëÛr¦MôrIJS|Ç•]ÕQr±²5J=¦éVµ8|Ù«:¥ Ò^§dU÷Ë/ª¹—°m¡À³lm™[þ3- oïÐ$Bºhìüƒ%@'KÄ%ÝcÞ!¨ÛÛ¶*w…ႳÂä'Hn ÍJ¥È„Ò{d^ö‰ ™qÑjn7ß´ }Ê"cEèvlD‹b€ÀQ†«Ö —Í=_ªøiðø}[½ZÜo +áAë®Õ:ÿ’:pè ¦ 9„M»ÜA9 ´k³7æïæ3m‚Wh"ít@×ÒCýÛ Í†{Ä ¶Û@í¡ùŸ’Jàüq؃u¼N8„I±pn2wVª¨%=M Pé=@}ºƒ1˜Š„¹Mm½e÷?uƒúVž¾™ÌÀê‘/åT-¶ÅÀå¼naú4¶»µÙÃ@º:.ç¿ôüx^òa<•Æz~ œsE©g¼%HØ&Q€ë÷T·7û ~Ëǹá6tN@ ƒÃÁ8e`´½51H-W*C-‹ëíñ`,,%à!‘Ý…püÆãÂB•É]«¯‚nbïK ZÃõ§@ý%['ֺπÛ-EÅžâ3 êfs0ÆU·¢'Ä=7¼Yeg­pˆJ¡^­H©¤g-ùVÀ mŒ 4¡Ké{}æš%îƒê‚R€aLx/ÎBÿ‡ššÔÑAEýüx" |ô©öc~Z8˜bCæHâ CZÒÑGÑüóoDË.D?Ÿ \S&~¬Éš/Ð\e¹%Ú8ÿö§áò2çóÎo”c„óåëõïµ…n$øùˆ~ú©bþD´S³°n*èŸ`(«Ùø¬y‘œ‹9*Dò~©1ò®5‡ýÌbé‹i½ÖŽ‹Vu=ú«Çë˼®+?¬3Îjö§è8<ÿîAìö;û3L|Õð@¹ÞP~eЩçÜëß>kУÚõ|vsShkçÐf<ÿ¶@»iç3 OÈBÂ^@ÄwS +?5,~`}òÅVreXÐóÐsãÙ:—ä¦à‹“Â&ª»Ã!‚'"¹«Lq ” +ÑÌ'ñ꽬ìYsÑZئ¶Å/Øž=€ˆÔD4Ëzê{ß$€¨³*âA#hlUbïIVT7yÔóñ!g°Q¯k…ówLaX'¡«^v@ÒÊkDà³Â„2ÂFöï ¢y+¯U–ʨ“5€=ÂÀ9¾5mâ(`š;ëâú¤+ÎŲtAÙ‰úKc‹EîË•öD/p?Ëc+Ôxkü‘.˜K47:ž&ëF^öO·ÑKÆÃäÂÓ®ˆkg¸Óde ñŒÈ ÇÌMteb®§5ömþ•ZuûJ LÕàèMaX«}µy%8ƒ¬Óh§°û‘Æ ,çpezžKzÇîÀþ€†Ô—Kþƒ£Ãö’­~ØšêÒÐëzÊ $V㢠¬G6QJ——ÍoQ<Ðڣ咥ndÕý¬ +*ªr–HËiBÅ&R¥•Ú³eÒŸO•½ê3V¬Ç®‚Z¾óŸ¥ü'R4ÑÀvâ’ô°ó\WÙAÚÐd¶úº%U\†5@*¿¬H}}`ÈÈi¹-Èã¤×½D÷•Ê<‰nÆüåˆGóh ÏÇb:½ï ANjû>cÐuÈ y`Ä5dé³`¨uØqòÀuˆ×3|o.<Û(X¯;d†ðq°A)ô@uk›8Clµ„2"X¯ùÁn>»àL¨œÉÛ„" 1[‘ÚTÞiMH^- ®+àÊ€H‡¢ï3"?RÿÄêŠÔH}›ÅMJq÷,íHëBEý88šó0übPâ5˜7€%˜ˆ#KVA5 —"B +øÏ€?qÔß wûý„„Gumí±+¬¾®è@]_ PxÎ'Çü¼¢©.¾>l#xLr˜ˆq8v>Ö[ÍÆt>?7UtNЮח.å±Ë©ZwA$Nøè°àtê©Ýf¥­°8\7•ÁÄ2?¢wæ¯ËXù¹¥8Ñ5Y3V²–*,›Î¾ÛŒ­…”¦Mw0¨éÂEc–D<ãe¿Äžëñâ¿üÆã¶ÅŸÔÐÀììkiôHs©}ÏoXÏ1¿wȾ=^2ŸÏã¶ê‚ L0ò5ÈåF!ŽË=ý“zÈCTlHöûܽfóÒäú»vw ê&Ò¸Ÿø d¸°€ïòúÖ­M¸g€ëž[gÁÙ¹b1ÑÏ*'1€kèmT‰ÃydVÚ¿ûü<¹DµÏ\Ž¦=®ÿ-6îóSb¼rŸlûmPÕ]©cß[yÁßVæwEpPrÑ®jÞÏ@üy8Å3ˆ /µÎó-}ŽÔ´ôƒbÏ…X¤'V´ûÒÙˆAiD¯H¨ØÍ€›A:‰Œ+²î^è\7ý³ +{†ÍÃ#ÍCïŠZ¯úï!6k€–4tÂ0õóc:p øÎi2Ñ»†BrÇk‡Â·Æk ¤í¹ký6*üÄm­hlˆòü;v ÆŸ[UÝ"Ç}Mîñ¸AHåHÉ7ÌŠ};â}eu‡ G8ðKÒ*£G~Øx\\ÇÖy’_ìNŒ[7µž;P; tŒkF;Ã)³b-§·œsXÁª@ݯà$ÅzÄ ,Ÿ©GCCc $d÷ÔT}_1þ ÐvS$6q8d«¹×=¬{ +|wyÅ´*¹-Æoõ×W9Üí~ÞAç/›ž@ä•Æ$4d¸žn·Ö¸2VÖÀÕ_Ïó1zàšÁ<£:Ú¤jú»#|iÁ„‰÷…~ùslù·bnëUÉXjÁˆ¼‰u{Ö冻ȵ“êL÷‹€ì7õõ]¶mÉ%DkŸoáâKȼTÌFQ°_,±ïƒPáT!eãí´Š[чgª)ÓCÝzŠ4Ô Uæ¯'Ÿ¢Oµ(½`¡&´Ô¦5Œ`¤ü²aZ£¡¬N¡Ÿ>î®#;‘žQŠéš)•ÐRV̟ʱo¹Ðçsk׸°òà±°S¿“”*ªºvð! Ú€ñ¶É]”ÿÛžH|´ž¤øhˆ_4Q{Õñßmÿ{7ÆÀ…^koQc= ņÜÏ”®½üVW|ŽßñxƒÏÄfcŠk2Þµˆ]ÿ¬”B‰m®8‹Qî-ôÜ®<+¾Ï{3,§Ý´Çw5‹&'ŸY&@“w @9· +†õ´ àî”L„ \mC$!8¯‹½á;”ºqλ ƒN´&²H„ÞzÖÚ=ßlÓ½omÍüæºÓpyÀ嵓Ýaôç#}þ}ç5YØŸ MxXLlªÖ +·h+n-Æá¦rC€ß¢v‹’±b›—ú=» á2#ZØ„ÈðÌy\™êý¯w9¬èqîzCعnWµ×¸Ì ¿”l5Ü»…¼¶‘-­Â¤µî‘Ê5«r–%X£ÑNÿAÊó +o`½ÃêqÍ@AÁ¤Ì ½'ºmë]#mb³:eJHºEùsr8GG¿‡sñf"Fg da‚ Ô{¼^3P¬CKØ4Áç©UMüÇö#´HýSùœÙnÙÛ ¤¬ ‘Ô 7Ì?Ìé玲ò±º~M‚¥=«z=š™ar¿f$p¾ò³µ¦%\’D>‹c€òüH{_gUÂÖR°©?ñÈ`LÛ@Àë5/¯UÛ@ú#I/É—µ ’5”S°†Æëf»5S+5v*AÔ½é5¤€ø¯wŽ_@+¢¾ÀÊÕ廞AŒ°ïmr'€Jpˆ‹ …°ó¤I×@‚åUŸ}Z ãZ7z]ôç[Ul=<ðË\÷àô?Z­hZÙ;Èû,ðù²¦ +8êªdèqþj'TQÞ°±,Ô+§¢ÿ9#n[¯5ó2¾8Ž·W' amÔÒÃò‰$ŸÄÇ uÛ=mA‡/fåùŸ^Ï–JÉÅ%™OË]Q©ZcÞXtŸ£ÙÙN»ªhÂô[ªûái«u:&[23¢í…èHŽÅË–*¢5-rn>ª_cŸ©„ÒçLnƒk– \”2Zö¦…Ð +}WŠ41ÑðÞšy›”¢3ahDD“ôÏ®\ñ9‘™7¸*~ð-Md +5~î{nµ¼»ÖÄâCjasw”g îÀÛÖþ| ³-ÁôEq/Îã øˆ;Ûö}ÞÈ÷YÄ×Ã&×G7B~ÖŒŸuo*œ&Ž¥R"ÇÇáóò>áçݲµbˆ‹kôÇahí±ÒÚSƒ„½‡*£;œ0¨ù­„b ²ý½­¶è "9t €g'ovuJÞPXêýãóP¬©[±f'X“®í»×W¢©H¤bŸòÞ¡k÷™:ѳPg&N8vÄz}˜Wd%=°n¢8|Ò²rðì ëc÷¼PŽÓ·g#c-»Õô¾Õ\—J¯3ÅžG½°HË©¶M¹í ΂2d"ñ);Úvä IIé{glåhêTûy'þ $]WÊ.Øip¥$}—û­”ö„ÌáqSÌj_ÌdâPJ>nŸ³ævPFÃQ ÕV"¾.QQ^‚ +„¬ºÌçÀÏ;ô)Sb4IïÏY+s‰5d«D*+ï{ ¤ƒz>RX¢la‰KÃ7ílC¿£ûܯhˆç~ÞNßg<+BHu®qhhWÔ]5¥î°´½ ˆáGUùÚׯÅõ;?Lj³;©Ì3+n×*–sÀlí´¯áŠåFZèàÄSœ£ŠÎ1‹6¶f´hÖŒTÏåvOWìI<볇†ÞzrçÜ?x)ÎŒ8P‰ÂQ!ÔÓ»Íp»‰•¥µFdq´‡¹ q"ô ¹û³†±¼fäðw3˜ÍAó}:I×>ÁµÝ_ÍØJFš=ä³ø¿ÏR§°´èRyGå`B!»ŽӵŘÏŒ{^ŸûH¡­‘çǺlÙࢶ£tnå +€°9 ;™CpYÄbß?#<#ÈCâƒ3‘¤m”]ºàÊ‚Ä0à’¹í»5ˆæÞ£¦„[ÀÈel ¦1ÇŠ“7”¼þ5Úç€ùUŠ.òÇ 45ZHT÷€[ñ¸7h@m/ßÏ x>Åñ^¶}sÛxI® ´^¿•Eé´ávãÿÁøÉíœ(jÔ®“g?j–¸êeÿD3Iኢ]ãï*ƒÞ®¢xÓ”u¡á<¦!k 40×rK[\ž| ©ò=kof¸¢ÚÎ?B #o^uצÙY _Ußö oØŸ% nBsBE1dT4E1õ‹Ír©­|œV{6ì‡qª¤Û3ï´ôÝVÚÇ*GÛücðïâTkŸŽ…ónj")¢-åÛíQ5|Ð©ß à:ź!Ó#?u¯"¢»ý±mb'ºÇ'·Í³-‡ÂV hŽø¿ëÿqœvE·–ºq²¬GÝÞxÃoe|1ã}‰Ò×û,ôQ䕾"`iÆ%u+¤èœÁ{´RÊ2â‹°4*£¡¤¶¥bÊÖØ[tuW´¹ò~®{6ä°’ª±A ¼(×Î@Ê¡Ú„äMÌF+<è½?½?CöÀˆ"¨qÁZ zɤA­»zEóY†ìÚëØ{’`í¿ßM–õ· Ðƒ Í¶¨ÔEû Vï &ÊúZ‡T³Ao\GÃf>46œZPöú˜uÕiòZg²½Ô÷£è#ÅHF Æ–á\T:`Ÿ3èaöqttÞg·•"T#Éijù ”ì +zÿû[ÉõMÌÿqà‘(kI}¨m*´ 9æŠú*v]°Ôx‹~ÈpRH¾ãSÈoøžÈC­‘ç‘`¡«.«'Bayc8ÛnÛè84ŒÇ‘²¿±¹lQP­BÄ_¨!RKá¹./<´-!뺊›)lå”ñ|Ð~u·4? +eê-c»ž[ +p“yó×âdn l(ã•x¼eA° ¦]ÞÀন‚ÄäÓå³Ì°f•ÀÏXˆ8»¶_ïÒö”ͨ˜ëcj`ƒmôtÆ0û2KB•GÿÏ×yêùëý’heÊGñ{íŒÀ8¹Z<Ç™~‰\0 ãïÔ¹’‚¶Ñ¿¼³E}žk”T×¢aÔ‚Yb4ìÖ‚Ö¶°p¯ŠsG Ÿ`“\ƒ4œFìKÕPt¼ñXn ÐW˜;îØ‚s%H"4þ׊¦þúe)Ò5v7:ÙEç’PœU/œÜ Æ¡Ž£wLÛÇ?ÑÓ]{3r1S/RŠot©%;t„|/ÉËzÖoh¡¹½5nÅ—nýÑÃ}ôi³ŒÖúÝYT„ý:‘P‹æ»Üÿ?kg³+Ërœ×'Ð;œ¡¬ÿ™5´/ C žØELQi~{çZ‘ÙûœÝ›¼ pEHâÙ]Õ]••ñýp`ÚaD_®ÄDZEïïvöä±#øÖ÷°ÅÕC›‚öŒóX§ÀvQ«]#¦òcÌy¢‚fÒ*a¬€Œ¶×¹*ãÂŒpüR[ídÎzÈìGÜàÇ?nÚ²LëÌèM  çÛocS^VIÀö3tÞÛ¾Kpì5õûȇmmá"‚Ü.ˆJˆ°×Ûw—6zÓOø“‚ßWëƒËéþU¶ gæFÆýņl&zVÀMZš! ƒ>lh +Ÿ°¢*hŠ£‚v©Àè'‹:Ý˪$<~^aG§u®xÒF×c±ÃÔfK#ËßLgu g¦yÙŒµÍƒú¬ã¨mË°•¿y‡‰ TCD¯ÙˆLY¦ÉØ8PNŽrø«‰¨°?ÏsÇa”Ì#¶fJWQ#ª zŒ¿Qì;_YóD0š ++ñmõuF¶fÜu0Sðw„±©šÞ‰˜Á÷RµhOæå‡&#•{u<»;Côä!»ö£Fm1~u>„¥Ÿ×ÓnÁUÐR$Á¾‡ ÎJC‡§Q‚×o¯ð +–t Â*'k*—ô +·(ÿôX_5èìXÔ˜§ƒ»E!Z¬3®êóòJ,§q.ä5—aàŒ-iÓÆ4ƃ·+^¢~×NÑ‚.ã8à^}=vµòèä‚K) •°fÇa½ê¢Ü´ô=ů‡æ'ÝëçhðÖ*ô¸M(Þ§ T¡þò]K²á=€6Û%|èPƒÃ¹´œö·"æ{Û4r•hú»oŸ"FTÙ:wˆ(ww´8AüÎ=Êë-.àvBŠ.Þu¾ ^†èÆilc‡2¢;o-×.‰„¨ ò¼Ålù £ÒniA·!€Á¥‘‰³uÀÊ–½â—率 ½ò¸€ ŠŽ¨’¶PIß7Þÿý/Ž-HÅ%ØåáBæ¼m‘ßDB¼ +ìãq³ 5`0Y•„lÉR;=üÈpÂ\'*0»OêãirT¢¡ÀñŽ(@6 »ùÜ/áz¸;Âœ° æ„NU )îߟE×)^ÞNÏ ïˆy 2IvðÔüÄ’.,t†ûNؾ, °5*ê˜`ÆÜíÉ"&…÷‚"_fP*À¿Q +Õ˜ô¾ÿ\AŠWMÕà!t|ø:Ñ-o@>){>` Ň5(Ò²¨ãñP“!³èáëÞ¾ÿ@¡`–zF’ƒF¥~›~œÞ/I–ÝLÈ™ì|ôõ¤©RN°” +óÝfb’‚ë´ßä²^ëÈåRPŽ€<Ðõä`?¬ó°Pƒç¯@—·¶Àx § +Vb?ýاèL±ßÐþZ&éÜš‡ºL>{Êd­kô]CªÌbÙD–°ŸD§:.$=€JQ¶§ïZý.ŒS#.B½Œ„éÝŸ +˜ ¥’,Ïc¯yª 5ÏŽ»Gv n# çq¶nÉéz¿DwbEYN”H;2í±Šça|sœºÇÛùE/ê˜ë+ÔÍ/…¬F…,_~ +n •M5r ¬÷¼²Ó}†P©Ž¢HÎYS2cYÕšµý¿X¦ ×ý¨êð½!ïsÛGT’ýÃa1x2´=#·dm¬Pî´ô³˜‘=½ˆõ`vúìpŠšdûî´¢ÿ©¸+-‹t‰A+årbÒ¡GJ +s!D]‰Ãh àwiDA¼d#8Pá*&ÿ=áaö€R~‡Š3šð{‡è_ü!úe ï·±Åz佞6 ^×ûÒÏ8µá~|Ý­n¹ÜGaG'á°ùƒ¨,þI¢-²¤©£½'Qe] ~¢ÞÞý&)÷/¼ç‰}øY|bûU.òÀº½á ‰ñŠAç•ìu=¡äk¬ÿ¶b#ág²´¥Y–W; ˜Ú!0[‚ŒQ$½òïUÝ«Šgb×r´F‰ŸÊRF^´/^гA7Rã ]§õ°¾/×Û_2A¥¹¾œò'k9„:tQëQÔ‚ã ^›*Þo×ùò%ÏÉßZÖÛò;tG Í[P€Pm£±ÜڇĆXÛ뼶é8jYt瘵ÃÆî¸L#Y°ÿ;…HhOð(íÖG$[Äôtª8¥Y8ûS™à¸4©™~ŠCSk^Ò¡UòÞݜXã‹ìî7âñß”¬ø7eóVB%ì|dÎ¥J£(1ìùªÕ% U ¯*e·ñ: Aa&?#õĪ3©ÖRWà}“6´¿zÁÈY2ÄÁÐÔ¥`²_™ 8à‹…å–Š㉅}¢–¿wêM‘it5Û´¤Õ!;îíû´D®€.°…%­o¯ôH7r¨–×ÔàoäÕ|SôR¢‚¢ðï•ÉÀƒ .’{•`´2AÑö†3Ë¢ÕÊøâ¸zZ¿(ÌB%^%o1jPô–k HƒŽ†—NMÇŒÎâðo™)ðÒeŒ-ÉRQ¸~°†Û–j2ÈãW»˜^¥ƒe#¸áǬõŒåÈÄ+Ûz &†š¼ýøý³(1Mcêz©–ô\˜‰$ÌïýÄB.i9zÜ.55vT?ž³ÏËfùþµ‰KÂjÊú’¸oÊ×GA3Ìo³ŸY2˜âÆ+ŨSWô ñQ ÷å›™{ièå æ%øÉ$`w\®CÎå²âF ¶ÄýqÕÖð•ž`׊¨`_xÙ8…\¨ÇîÅÑ’Øy·'Ô„H7W§]¾u?¶ñ€€÷xUpÍL¿r—óÜQkç¾/Ô,/Z¶XW—JƳ@¾ó™´‚Òì`¤U†î§×+Î ë|änï[ µ î½êÙКp®¶B/W–]6Pã¹ÜLŽÒ°„…Ë›nÎôGÔ™™ÉÕ"Ý_£3~ƒÍÓ6 V¤ÖY•ê>Sÿmüò7‰M ¥/W®"‡9ZWì‚ø÷K.\s#ÿ$×ቦ’§ôYÿ]¾©‡x¤‚¢6+ ÷já¤É£¡M„Ÿ8Ó–Ôµ¨‰üƒý¥îÑÈ÷úƒZ»ä ¡eR¯ÝZæw-†¼ÂÃI³Fñ"á™ÅëÒ{ƒ( RÕo¡žv‹5ž.×›Òa¶"û4úñ„7^.á8…€P½¯fSìÅeÐ:ÂÉ`#ÍBOõèÖˆhŽw èE ´úŒú@àçM3‡€Ø䑤¦yP3ƒý_ ñ¶ç@uc +èg¬öH§tùe±¯ŸûÎ|a ±ÃmJZ)“ÁF"ü(‘ŽM"aÉûKç&RçªÅŒî^VŠ²Ý–Bu'LWF+ëT `H‚{›ºE<z˜™æÚ¿ÂC°µ5¼×H‰‹],}ä†eBìö=ˆ¿ßéŸä£ur¡ÑêÛaðDñ"0ÈR¥vNûÑö°¼^NÒaÖ¾¤¾pDE庑ØÄ•tͦâÄ"RÈA«îûŒðϪңý:âúÜÓëÅï¦ÜAãhŸ\ GP%€q§£. š´|8ñŠx˜ÈˆS_A#îpvj gù¾LõóUßBG¦%Ý%¬Õ,W´=²ç y˜(¥e̾/…—<ÐÕU +Å=?©ª¢¡«%#'ºÔ¶c%²>Å‚G”c†V3ÔûB aÄ;zÏïÕM6²Ù* O #™Íd7GÉX®.+ +‡œ©G2:Ét‹ù‘ ³…Ä÷í¦}9HdßßdP!wã(C’uš>훬‹ͱ“F?h5­Æد¯ô¨£i4€SMy¹Ñ­½M{&ÐçŒ-¼éR\±åÿ`›³/ ?5z°˜A +¹Í)Tp¦–x)Ù¼Åöõg —¯å(:ÜD9Û¥f"Ú>£%S:í3à#"‡SãõlDua·ÚãJ6­ÊpÞ PGªè˜Fï Â¦.@¸ä¥ô"AŸ;Ä "ü²}MáG” +žè@ÈŒ‹+QpBÏUiŠôaõ+Cï,pè{_J'z + `õØyÜ:Û·ÆFÀŒ€aùrMÖ@ÇUtÄZPô£liÉrÛïöðº!©QzœÆu…þAú (€µZd-ø*dñÏKøl±ÐŸ=àŸØ +v¡%w   7¢‹Ê,¿ƒù§ {‰Á·ëv)>Ým9d$´7¸•:§_ª¶(`e"lŸpÆyàJû¹uîXsÃ+80"tk«;iIß×TRãÒãJ…=ÉT,È•ê9 kìÇóô"ôõ¥â9Ú H,2¬†+}Ø  +;hì貂å0†C„…é &¹ÔÓ£¡ŽEWâ%\0ž°‡È,È­cDs<§–¿Ž +ù»ÖÐ3bI´¯ÒÃ…¡SÌ’Æ¡äÅ ~ö'óוÄKе’]XdŠýjáÚùšüaþÊd@Ò‘Êí¶·è¶?±Bí¥šÎ…/y`dë q=ªw]jK› +m« K}pŠF씇%uZQqË|„)ê:²”¯6<ÈôX+pÜØ›Pœb;Ò²^î±€UötŒ»¥ Ö¬Ä/èÇšäQ{Øë˺dÄED˜!“Cð6úÀÂæšÏÔ…³1×PÀU‘oè¶Bö&Þ¬nXdx ý8/ús„Õö‚‚Ú¥P-;׸ N‘|¡T7ÀÙkø>¨u«W@‹]`,º{À¤Ãqaºy ªóç´éÇݤQbX-DŠcþ@jå\ .¼çqOˆô¯ðw°¾£ú8RÆéÈ¡QPo°îjü]g axP)n"pã+j|64oš¯yfœ(a c»×iv×y¡°ÃªÕŸocâYñ^PÉ&&wðNÌ%tÊwzUëŠÛ»‡„µ½nlfJÅ_ k°•î ÷ÅråwÓy§ÒóJÌKp¨ò‹\¨"Y>°ê, +·‰1L£ k‡ &ÀŸæÒ¶ÓG½,‰7?$j=£>5'à“ÇDþõß~Z6Ì”¿S¢ 2R +Õ#¤X}ó4M,Í;²‹µÞÁ~O)ü€½Î\9u€²n´jö3²èªe<€T0ó­FP¡ +¨JÀ†Í”1þÙÆ)¶¨ç‡Ú‚Œ­Œ-L õ8_Dú2ØWË‹]"Ø!eΨ€†Me>ëë1Na½_»yl¯k¸¥lH7« 6©f}û>ÀD?°Çg‰T¼Mõ¡¢•1áÃûW[†‡Y¥Ž;í@ìu^Í:ŽüQµ¬rb +rÕ´¢Ä»è8-¬þálýÄ£ªÌ UñÀõQÝS¹X1¤¥jª¦,  +¥Ð^X¯JDK!Ù9"3lò4µ¬Âˆ*í¯dKÞ•là‹ƒhðÑX긬où´kfæ[^ÑZ¤ñØÔP§ò§Àgq[›×†Ö ç}‡ÏÍ÷_²±ÿwÖÌæ¯<Ñ›KLü'ûŸ¯üZ„T#· ¤ºšy—àN„»Ôá9©æýÜQËRêç(1Ób'»SŸP‹%pQRˆÌžRÛå+1œ¤Ÿäéót‡¶ÐÐ /©¦È +>À- JÝÕsÀ³(Ž/ ÙÈø8X¬LAÑu÷1$ßÀ¹Ö&osÌÿ +3Æî•Té@Òì)ª³±xí Âù{|5º¾;?'±µÅ{?“ƒPŠ ¬4†©/ð=,ø@˜о¾ +¼ƒÂ·QVë—–˜H'9Àk‡Í}˜›ïûSÈü…êá •“}ð)q)e± +ü\ÊX²ÚL†ÑKVÓŠÕ/Îr•’Šö‡# GàÛë)Ô˜êtµÆ ½ÿ=}ø€¾¬Ç ¸ˆ~ʬáóÆ£E— ºrÕ8p%˜Áì>¯?À±ØŽÖ¬2}ïF­æíÀOÿóÁ†¤;¦½#>9ŠúRE…}Ók`äô‹=°qF’ùzˆ3 {Š·ôÜý› +'âxûÿä§Fp¦àŸ‚¬OáÖ8ÍCÝ¥88Ä¡øwþ*ì¨Ū;¸ÍžÌY Dÿ<_aÏã~h•pðÂÒþ]×µ5‚¡Vt~ZI` !³ämvk™ïÃvïHјn3¿Ä.ùPdšö¨ß}*¥³OGÄ’öz>Õ8šÊKîÏþ¡OÈbiGÅz•.¾™¨.±A•Xõ{`#é¿7¤­¸WÌ…Žb¸bŽKfú‹+ï_Ì0þJ—»ÃmÉÏ)î)‚M<øUà.òTfhܱƒnÅ;¸ìÕÛ>F㦛5å:Vä¦ 4]!>’8Ýu€S¦š‚ép1`ʧšwâàØ8Òg°<’§Säòž.šñX¨Q–}¢(Ö3 ·…ã( * DZ±>Ëêe ÍvÄ÷°4Ës¿ Ð@K”ý¨èã«-« ¨0z߸뚔Ôüá\`!/ÊÍS8ÚÑé(`¢ûGƒBO¬r¼Á…W ¾Ū¤xqƒ¦ÿû äîn)8ªINò|Š¯­½l +¹`Ÿèd¤«û„æWÑéš–"aèP#fy¥ÅZÈ“aë—ÌL:’þY#JÖ曡àî€õ–9 U¶ÿÌÈúé—Ä »Tó½M©! ¤.ˆŸ|Æqa²¢–÷ð¹¯^G%÷‰æ¸¶@dárz Ñ"¬ýQ4bq4ê]@“¹|´á!Ò•5;°aÐejëºÌö€ÍU +z©úÐÏ3»à÷Ó9GRb>JÔ·Ë">ƒòÏ:Š`”1´î9›ò +ÃyÀ¥:NŘZZ%ñŽ3IïçˆHOûia@t·ëñ)JŽ³´µD¾ª0‚Éÿ·¡Ôq¸dy(ãöÝ°~ÿÐ}–ñET;¬LÝ¢!T‚d'—´]Ý=Ø(zY&f–PœbéÚ#³çRA¨›Ï«:ÔOuÂu( ž%™yƒiið^I”à< ?YL 4¢/V›µp¾-wòæ| 鯘$:M;ÿ‡!KV#Å› ©„p*ä6æ…Çì” ö†uåéå5èH·Bkp¯2úÇúC«Œõ§I8b‹Œ‚ @Ô;Êq§N·*Åš´wÚ’NφnÌ÷Dsõç_ûé`ïéüµºèâ +w +«ØZÇÐJùÇgÜÚì~3­˜~Šøí©˜JŸDž\qÇÏQòSøÓšôøï@ IÉèÛ]~û=ˆ]€ÎWô‚íKœb5.Éj_ +>²YÙ@©7Õü|zË®óÌ#cÞ ¤ÕÀ&Uí<°•CE>–éVÕÃtÚµ©0o—Þ¹}¡¥`f—Û^TîXi¼.„æQCHÕ‡Q°,=Ø ìšeû1Žƒ0»Ð†éĸôä˜÷ÒË:¸q·àæ‡Z¦ÃB»Ð5ïh€§›Ä-WÐoßò6ž¯"jøºÑlêÏ}¯?GñfÏÀ}šçÞÙâžUIl´@G´@Ÿ+Þ¯ìhÏÖõßbª:î2¨q Mz; M·õ ÿ°“ ôÏÛàn+~ô¥!UÙ¼‡H°²¶”æ1݃¥Í•Èl›¦T¿HU^m"+ºÐ›f«œÙÇÌÛˆifþ¡ËŠ2äÛ¡úwŠ5°í2wöÜÃýñJl&f¸±…Þ +W·2.."ãêbd!áEC\ÆÚ Ê„‚ĺ +‰RáO:a%ÐÂ~ØÀñ€ÓÂ÷ÍêžXJ®]M‚Ó;Íêç9–Ãa'uÍÞèvž%±GQÏ ‚ó†Qo?H ÜZr@Óå#âQØ­‡(ßÀ÷›iŒ eÕèÆGãx‚~É{ÎEvÀ(<Ά:À@Qd°zÔ SEyuÁš¡Èêvd\G^äyÔñ»X Öeg'„cƒV/Þ¬°t† h  +;¥²¨Gež† Ä·ŸÏ‘~ÉmE»dÝÏvßR“¨“-W°Ãýl„чÙR>ͪAOÂR³æd–ã¬ú!evÄìq<™Ï9ÈŠÆA¤@Ü“Î( *~ÊéL©Ó¡gPâó‚ôi)q{˜ÿ9‹½‰ÀpŠÔä]×iE¢¨ÏîýÉò‚ï«bèF¸È[ÿÓfÀœÁyÈ—F;‚X½_Ù³&ꀌ*KàW÷DYK¥wUΞb ùý¼ ¬,÷˜ÕI‹o„Ú +'ßèz¡éÁ´¿æ›â|‰yÎul;kFz¾žÔˆ»;o0=ØÞõSýàrIÚXXI”ùr!åYn§ lë¹é$b)l†ú¬ݕɨ¢¨˜=f¯¨}(wýŸàãÖyP»ù’4œŽí5ËHÿ&{nÿewpó¼Ç¥x€n‡a°WÔç9¾Ëªé–£¦;ËK×ßT?]¼å•"„ʵ[RS ’rôåóå7]¥ýû7l +ˆ–Qi5¸c#Œ#L~ßçOúŠ—í%ïF|¯%k2Í=VÀÍ®uèæQË>N: '´…^1^-ìùàB¨Iª+ ÉÆŽÓz¾ˆhTš];ëµ(ù"(e_Žâ>…¶æ€°Î$Ý',jžI˜¦:pÜœgüáàRØ5ZùUÝ;Z÷!Ÿ®H.6ËçU^»a!+dÕ¸¤æ³šrS¸Ô²Õþ)ïKã] š¬iÔAÏÖiµcO)Tñÿ½ÏNK…çàs |^w®QÔ:uŒxº°]Ý_ñ¤7¢)j¼™ +Õê—-MýWêCÁ¤»bÅ?ðAS?P‰€îppx›kÓΕôRÌîwú¯ŽÊöþjGgþã w­rk¦f#æƒ|Py0§¼H´žÞøÙêÖ/>…ÿçÙì³VÑá¶Õx¹ì÷%Êyà÷˜}•w:Q5šhG1Hì\“z¬¼¸¸ ÍºG7ÛÏQ””ͶÔA4–/Ž“+Û‰fƒvåõóÁOÓBŸUB9Øï:n£MÕ dËÔµ¸Ù endstream endobj 57 0 obj <>stream +3ÿbiÁOC4ïçÞÒÚ¯†¢ÉsÍ–bÑ ßØ~ÒÕùÄÓáêŸc ;{° JX§Í ^ÐLÑh¸cЩJØ#·PþPé3½ÎB¶— çæ¥ø©æw‚æk½fIsâáÁÞŽRõ²÷íÉP°7ÏëС%á56¢‰ÌLJl¨‘ËuÌy œgcÝÓËÛÁ^\1<,Hâ;+Ÿl>´Àþì…ç•c¼Ž1§Q`³MͦßÝuoûv…¡]¨GB¼dßØU²‹aÍ÷u.â  +áhXQ×[A‡UˆG¨hz• õ±0æn”1»uŸuOúÊS>•(íq:¤vÞ¡¬P0oX·qwŒîÝaUΈÉµÛäýG)ÇN‡k3ôTKkýmýîD?ê\…Ïa¶Ýƒj¡ôÐBYËrb˜ôYK;Á _É׊÷WývŠÔR™LËwÞ}ôqu eóØ‹˜‘íÍS"¢T=c›6G8n£n£&òÐÒ) +÷ß¡±øŸüöVãã³;ÑÞþsL‚àùàë¡z(Ë™Öá£L½—>G|5m¿GÁæ„3¹'É¢c9²Ÿ;ª‡^þÜ}Ñšjà9ÔÀ%*åÃKÚ9 Ãï<êñtz×#GG ¾Û €‰W’Nv8{QmVñ+¨HA€dF9dôAÒà—Éî.ö–éXFq6³!4–g@Ô‹rÊi~;ð§³÷ÝúÚ«v~¾øøPÆ„´½XÔS[ˆÌm!AñIF4^ГdªíÅ'_õ«¦9åÄÆ“ 1ÏiD( Œ:OÄŒ¥EŒ˜'ÂÚE Ý#ƽÒ1ªÛQŠÉJ–J’¥h¯X7°xK±ÙÕ®ÁÏίG`¦åŸì€BCÔíêÇÒqÁGx+9`ÔbeóNVÚsdû{§‚4ظ„Ÿ*ˆ,—«œz弘ÞÏàkj»Ï@­«SéùÁ±š ߺ9\ˆU‡1Û˜C]‰ÚÏ~LéZ²èW2-$-Å@¤*Á…r°ç§)êÜ)Øó°ãÞÓд‘÷PÁ#'H2ë㋬†¬=Är©òñGrÐW*t[i5f4 ”¸ÀUå…j‚ší¼¥(,Æ´=N¡a°[Þz*í:sŠo[Þ“qhCp'Ïm¯gpcñ¢v p~@Ö©OI-«FQå¶+´R)ðíc‘ºûoEêÓÑ ×V4"Q¤Y7 FA „:­5ò2z¤[ÔYö§ÀÕÛ?5« {SÈ·Ø£> ˜Ð&‰D?,ÈÒ­B ÛÙG‰ûñK]‰6¹û +oÕÑ®¿,PêʼrH°SÀ ðú´ó7™ÉH 7Â%Ö©€h©ºpw²|" ¹;ÅÝ? —»ƒÕ/48hÂé'ÌÕ‡LôÄNa‹ôRùÐó§xª’üUáØÃhoìñ©&5!k0¬óh~â€ýnÔxxD\çÒì=ص)²K8‹„×}–½L„½®v&xêfˆXº·,Œu÷6M±yüB­L=Öe¬l³»RóÛddZš¤ê/‰ˆ·OÉJ&8ñçeJ3‘ KªùœºF²ÒÕw¿Ò°ŽðÞ9+> …{°®äOéu±ÇVÎïÈtæzk“Ÿ®'ðdl\l#Á,`ž$s€'¡¸}zBì©?RŠ|}ih'ûôJQ†äõ©°?ðtè(ó4«?ýýš~½ê÷Çfè9d=Æ%1>€¥åX´;Á@Ü# ­ž`÷O0„{—Šâ¨Ö­^@èÂPY*ÔŠÝ쬄 6âO.Ë;zÿÈ86Žì ç@tü7Ý*Çÿ¾‹x†Ýoâ 9**Ù|ûiUK<³÷Na§è¡#¿7ë¾hÈw”w3Žpí="nÛÉ,Þ>Ž")ø: +¿¸ *wÐÅ‚@%yÞ#|÷¼ŸŠLã|­_k„‹öL”>j®¼·QX”¾Ÿ.¯ƒÐl’0šÚÇoŒ§*S +ðÇ##ø™ý®i¸ˆž±¥›U ‰^A:8a$Íœù¹,öqÓ/D!³Ó™Ç‡š +™v8÷!¤8|·LµN§ÏÙïqæaoø:[?Þ¬“ðñ܆Ž` SHæÀ^”•Üé+ú†‚÷ßuN©K¼'s¢rÍàÈ Û{)´÷Gh) Â*㨫’Ù*5Nd}ú¤un^\á„»EƳd‚å‹Kß8ÿ®S“˜JâOÀ£³ÐO¦\kh9ô³ø±‹”ü†t T†8£ì9JÀ¨jK¸=Ÿ‚ôM +ÇTbÉ +N½ÿý‰uî6Æ~8xåºèA%É={ÀÎ!QèÝ‘á!Ã9^½Yºª%È”íãC°,”Ø÷¦$±y"Trø¾¢"‹ˆPnÎM•áw&M™¶«U ER3(ÎâÒ·çsJ¨°¨×õ%ä VœmØÚáù±®åøø´_]*¤ËÇæE„Îp+®SF½§+çtGº +Èü¸íÚ-n¶¿‰)_ü}ÆOˆ±´"Éñàž^^¾éÖê赺hCPIÝý-¨€ÝñzÍ ^-¤Q%g‡ØÇYÖs襄 ’ÕH(q‚ÀP z‰Z™i”:™¦A¯"wé뺋ìw¯øÛ:Õµ$1é¡$ïöqⳌ&»-†qÔöµ Û{3<†üû¼ýîuúû øÓ4ñCù0ÍRñBµêûvø $ü|°†T–¼i=›ïåEêsHf¿°|W˜ZvÓPí*ã¬6•ã¨­SHÈóÀ”¾5Ývƒ~Á­9hGtáâî'@Ò¼N á½fû3á 0wªƒ­ŽhŠ/…œÆ3~“òùÍ¡õv€îõKÔúóÁÂ{å"ÞØLÁ#Âë%1 qßÖ_bã³bù†á†øÁ=à$8ºý†ê‚e‹„ÃDH®›û÷x“cŒÍÓÒtˆ1!ÿõ¹ÖÆ9“<ÕVB‰Ò–…7Ð çßÜåëAyþ¦·¬n}ùàH!ºï'ùá3Î+~ +‘ «é•2‚COîí$#àrÛµûÀ~xâô;íúùÏ÷;ð,|X]2å/BM/›AÉ3„þŽ¶’BQàZ@ª0)vJ\ÃtÑzø(dd§þ„º›„çÓ#C¾ÆX|å9«}Úì†ÈhùR¶n½Bí€Þ.øܽãCþõïÓ(u3rþDÅHã\ýþ¼þ­êÇÒeôÉåÜ×A&e´§Á¥¡bª+2S.ªš·tx_j‹Bs½Ý÷͆²þ@äÔÊñ£A·G + +#±¤h²¼—ŒÚ¯ûã¿d^öПeý•'K!õï)Bþ—¢šúíoÿÝ·ÿù?þüÅò·ÿÿð‡ÿýíoÿÃßÿ·üã÷oÿò›¿ÿ?¿ù/ÿøÏÿò›Ÿþð¯ÿï7ø_¿ùOÿôÏüÏÿö‡ÿû¯qѯ?ñß÷¯¿ûÇ?þîŸ~³/ñ×{^ßàèô_ù¿;GK0º÷=N +™37zláZ¸g7Ò7!„¬v â »ûlþ¯7öNs5aÍS¢ÛÎ2+©ÿÂ?ƒOÁ¸EbY{Áõ%½ª±<Œ¢1$â_ß +‰$hüÆ÷  DBCcêê¸H¾ß¢ˆà»éûHA¬¨¶N¾ÞŽˆ®ž0?}¿tÅ$÷¼:É6~´ NzìÞÊÆp§-&{@©CMïHâïšõé^båû ªI3œ=1b©«c«E‡I‚(¯@ÒðH·ªÅªE€Üþ„yE3—K"àˆR|NƒˆòœÓ>Ž?5™ä5§M"ÐâCûcˆŽ„~G8•5O¦PfLj€×HÄ:ÉB+Aúų…Âû¾ §™€ XÙ=pÒq¼w©¶Öà¶Ó|GÖ´VÅÅc/F*•uZn{¢xèÓ8qlÑ\KVÉ +ÀjizÎì©„Üi?èªÙW=º©H+¾J™xíš›š’Ë:Hô +‘’$ú¼; ¿±Âˆž"ÆXˆß$/‚rp o=oí!±˜‹ÁA>Þ›²Zz´Ç÷+1ÓAµ7•…(æÙˆÂ=_;ççÀBç‰#BÁáÀi€©Fˆ½ÜOJj齄¢Tß#J!;V6Òïúð4 ¸ €4NÀ 6ï7€³# ¦qöw;‰ÚÉ+»8€T;M¯ˆó‚IÉ´V7€%œͨöéØ4ïË h +fS²ªf»F¹µ7ß½l¬¯ÏPÁ¡Ÿ…H:B•EªàN:´ây(ü~þ@çá¼È_Dµ°E%Ë´SÁý0Póñe„ërÞï5‹ +›Àçòé@üš‰å‹O¡mÃhÛopuÍÓ½ +†Eÿ%ß8ð„Ö:›?äM¨âB¢J÷Åmˆª/Q¢"²ô°ç?AØ‘ŠêÚ +c¡¼ü.)Zü ?25"PPÙuQ’¨W@*%J@$|«_¸ÑÒB-ÇqtÛäcM÷Š9S7…òòÓnOœû²ó!' NI¾¢,O9’Гjyšâ–«zç¨O“2¸}ý"¢‡òÉåJ‘¿G¡3†ë-À|Š<<¡qhöwþ fÒ@ äA6­*žÐ>û b´| +ˆ1Vžv?„I×äí,fSŒ¬'ÌÒò=C9|øjèÞƒ öÐïÍ%áGÝùe‡1¨1æ$|!ÑE=‰‚ˆØËÅqY­t†[¬9â)YÔ™FnÜQSȇ™j•“ÇzìNµ'h„CrTM6;E=>Nù.3ÐZ1BÕ**H˜•Ô¦Û"‚F©ØÁ†ðÜÍP¡•‘¶)”7jeæqÈ•1‹–(¨H?›cu#05 bïã»ìm“Õ¬Yù Â”§jT‹ULŸÕ¢ðÃp|ЇØã(QŽÅ_3\ÚqÛã(¬ÇÈ‘^ÿá•–àI6Ä4Õ¿8OS†g¯Íwz'4äþ¬£{1K‘¥ä¥C¡YJ2ÓÉí DŒªE\̤=l¶ƒóÙêëDð[EÎÀ•3cbÊ cBgèç3¯›‚5®Ñ|hdx^±Рدß~õ©9·ÈÔPh"‘™®©¬95lö +¼èp7ª ¹WÄ(y{†šÇðV'[€ºÓv#íÿñ½`uTÛ÷†EȱËÒ6¦pÕpDDd`LDgQá_1QD¯gÒ][ÃáHU,£”‚ÝQ++ªŒ\çJ5•ˆ  HÄ~¹ŒØïâX"j¼+Zõî瞜סÅUzÎ+ÈIêOJʤ?&BR¬"¬’pËAcÐmìÔ0µ@`–R;3Of1ƒïárçMW8 sæBÊ£ýïš I ìnfS•ÆUY‡ß’DÓìj±ZÙֻ̪ëdcÿì«îUz©_Ÿ£?^ö4 Ás–Ì?˜|Œþ|³G=൬£Ä„ tQ´èuz5¾&ù(ß,…+.¸Šìó#çAª?ÌF;ó1‚ç°€ªÑ ÷:À eÕt/Ô„Ðï[™#ÁÆÄqEV¿ÔÛ†úÎrIî‘€áû‘|î?÷Šüt^©†LÛ­ÅêÁSß"r8¿?Q-€B©êâ E+ÂM8çat’„«pöÕxYÔv¡ÊñÚ+Áƺ­fž@©uðÐ(Ý3jZ ì%JBÆ8A {¨À7äîÇ!{…aà¼òL{ñ»§„=p4q`S3l‡íÙ€Žoì +_DŒ^­w(±Q1c;¡r5°O^ ë Œ<—ó‹óH +*‰íD .”©ùžx ô¦ªjÉÆœÍ\úÔ”@n2ü(ºžÏŽV6Q´?ðe~‘8hxS²! +æ¯Â@eOu÷²zx’5ªnr+5Ú0ª +ð'GÈ~œmËŽ¸–Û)tùÈÑi' B¢å§¥çÐ$³DJæÄs¡î|ñìu9Û|gÓd¦†÷'‚¿ÖÒ˜%% ×Ùa#«ÉšC·„ +Çms6|Šò¡]På:¥#Õ’I})¤+¼•üÙûÑPýÀUx¢'.á<óÏQìMPÒ\V€y4¹®CÝ ©Ž©ârš-–¾R]ˆßT×g_%ÉÂúâ<\š)‚žbö«˜aê- #tKŸuõ`¼ÏOèà)ÚH#W£VÆÔGŽ^O¥žÔ€šûœ÷´ˆ¤P'@ ¯DtW6Ý㽘÷¥Ä9Ô¶ÝÀ:ÊU¼{ØÔL +&ì¼.´ÖïEè`±o߃/z,S:Ÿ +b”‹PüBüÿ‰ +DðÜšÒ¹Ô&vJ‡‰‰Wjðâöì ´7ìã¸Dj çqýq"D…7F ‰íÃð Od1Äçtx‹(ºgð ±ì¾Wz‹zB¡Œ.ð<ÑnäÌ";*«§Ž^šU¶¡+Œ[yˆj + )5h=·/΄ÊbOA¸’A¾PÄÄ®½ ÿõ Ó²­ðoOWõ‹(´ÃáHB9±º¯è*)9û¡î[‰°:ª ´FC¶…³fEム·åS­ÍÅÒí§Ldºã`ZòÚ¢NKxÜè€üy¤T{ÏC5JÛ*ø ût:*ë±h6” ÷¯Xl/+{ æþ‚²_[ˆÚíµ„N¦¬WÅÓx³ —è䨱±¥¿éoúÑîIæÈ¢EñbG¡jÂj®Ì O0*qäÉ{©XËÎhq8é~¸w0_V+TÊ(*éæ‹Ð®¿¹ìð}88Áì5–¨ÔKD…¥pÙÞÞ±³£ýŽìGÁ8S]YZØ¡9€‹6ä1‘NããEŠ¶Ëèá~^ø +d}ňT«HlGrœ#9ÛöÐé§B8ëý)ÀZW>œ|gÈNc¦Ê»Î®‰·xŸ1áè•#ö§ÓÏ&Dw7Òµ1X`w«¤#»|¸æò-Aß“:ušH,ü m1M,±ÉÄh_q˜€ûøD‚­¸4±žÿÖUôÙÓä^íõ†&BžÏŽ¸v&ev¥;h Ôt}…gtžîSö²O\(œÓ¤,¹#FlKT\¬AóÖ_Òrá\iÜ<Ë=60Gà“ž›íwŠ¯¹“9…v™+Œ@ù†ï8ŸÈÕnÁóÊ›vÅÛ_%©väY4o¦=6ŸÆ9~ÆʈLºcv +—L48*ã@O©ëÚR˜—7Pe îÓ‹TÍ,öÊé\ 7Ð ++gð“ÈN°H_I®Ø[„? ›y +¶@Œù6o§ÔÏ~étUphcm«ÔIËü"(Ó¢1¿OŸƒëÇ(ü„L§÷ËlU¯Ð *ê» ¤$@Jià ÞÆJ¹ ã~>îeÐö8ï‰Ç*Ôç“€Ôk`~jˆªßA©"¾Æ[EGƒ\Ÿ×Ãë-Š6ÃômJÁD¦9´ÿ7vk“Qm°1‘_ºÞ#îPl¾ôKà?fY;y Üåí(/Ø[ìñµ”øÜÙ Æ@©›ü[ÓÖTÞ‹uÙ<Ï™‚¿lTc°‚"æ]6—›‚{Ûrd°™ƒÙ’a«ùà¢D€—$C”וlyÒ­Û(ÛN«3Í" +t/ÜXšB†5\Bv„äD˜Ž¢SÏoŠ¤É>ÞýMX1ö÷€˜€ó %Á=ŸXùèôb`LÞL‡#‰ä¾{žÁ¶oÕy×MH~ ˆë°]©_|˜¼Œ­>{¡é9zZ4—¾Š8FEûõ¾Ì×÷ „¯TvÆV€_Á´+ƒ2KLÌ‘„ì÷nø+ñdZ³˜Ž?÷"x*¸NÂP¯o§ )Ä +‰fñE›+Ç‘×ý"bFïˆÑ¼ä-ª’hù¥$̉/ûÈy,Z}+í”ndÚìi»#ù\¸{¸V¶¯Ÿ#Á:´yûD¹vOÑ™rmBW™¯Ò¿ø3{ªýÞ¸§jõ–8?G¡SïË0‰'ßΩ¨º + €¬Æùþ¥{ÚµÔ +°Äõ)"îX—!Š»ê¼õvžJ1­3yëµp2±~21îX}bÏ +e¬ÐÖI‡×ÐIÂSÒ8"é±ÙGù¹?>±å—RÏLùÎl€[s˜Ä|GOA5M×"óÈ,# +Ó'å˜û©®xD{8Žÿó‡©Á”–8Ã2Ò„×$µësÁ,©“Ü‘`ç‘ XˆCm¯$ïˆ÷´Pu‰Ã(4+ÝM‚Ô¢‚È‘ÄúŽˆElÙþ!“ÙoÕ—çyÎÛ”™Ñj¸k/%­§6ï+«Ú¦õ•½~ŠjGª6ýɪèÿ.ö®`Q(oñ¶°ëö'í…•œ¯>þè ä=þæ¹Üu¹ÝÌ ÅØEÆÒqHE¾THÑõÓV¾zúÿ~$ïw3÷ú:o, …S—¶Õ·2É/â¼8upeåËÖŒòâ(Ü]¿›7¡@@¬AÅ=§~œMIÒ- ¾dŽÇÿÈ­bÿŽ­‡Ý;§•@ôž—ŠTl}¾ˆ@ପÆM»ùæ™oQ9Ü`ú¡;0Zd¢Ctô°aè–ˆëþ)¸å××Þ)¸ \ýúTUí‚d5˜¯å†êŠ„ûwX8äx^ºU–Y š¡]ÛUŽs‘Ð‡Ò•Ú +¡ØÁ²®&Yï\Œ[ ÞÛZk(±íimÆ—¤‹Wëxøím©ÑÓœpòæóEy~ôÞ`íõeĤbß‹ªºzáo_¬ïA ù(`Ð^`¸7Ìgé¹ï´Ûï½èMÕPñ_02m¬Ï÷°Ñ­4€k–£Õ !ÑQ@©Œ7ѽ=×O©µ)C-!ÓýWÈ7qÚ3xÉ|k¨¾R\ìZÍ¥×*K{9El”ġS;¤ÄßSÛiýáH·5â…vö_h84]ÕØ>jJ ‘qVd5 Œº7M±B¡§ÝÑŒܤ€r6DÙBÜQêÑÉ j:™ÄA‡{å›VÁé…3o±yTÀ• òöÍöºR·«ÆG>ß'ûq÷6 ß웩‹¼Ÿ`†üh÷€ì=Å^?çj³pG¶FàUdÁ—+-ÑóCv¦ÒOÇ—ªVé•kGHÓf]©GñhΉŽÂ©>ª4²Ñ¦þìž(ƒÂboŒ zÝûÁ¶ò[ë) 1;"Óœö+€ð(ô(ÏkIÐNŒ b(£-ŠüÄ…° æB€ð‰“áW£ðµtë‹ÈBXÉíÖë˜(.fƇ_¢ãɾÓUøÖÎÅb÷j™h~w›~rÝ Û¼ÈÚ{PnØN¤$s½Ÿï;Ï÷ÝùþDdç{qÊ´è‰íì‘ öÁqú=â·gÛeBí§ÿv`Q€ø&Ôz"dõ;¤†6ó Žl‰L@™ˆcDŒÇú€'À¥î<0åÚˆš–thÎSï-êáí«ˆ@³Á£yxðïQtÈ÷ šB ÿgÏnŠ¬Û™}±TBþø'´;CKJ4rÕß3 +Ý\»+Ôj«7«ˆ7 +÷¿ÕˆÈ/db#• AêJ„Ìå{¡˜±O× ¸¨ÕÃH&ÏÃ\CiàÔ{àÔ©g0™ODq>8Môp9E;0éÖïÒ ŠóJaH*¤Šl~D"±·çGAðBobëºWµöEê¤Þª¸G ø=*ƒ“èètç"JºÕØ Àú¥ÅׇÒæ~¯ +`FÑ#¢@ ØŸ#bÔ5§˜ý>¢ãS¿8̺È{L¡+ +Ÿ¶ËlQ¡Ýw‘IDZâÍ¥fvvËô„çíù +Moè+A=Ø{xZ&-_À÷ä„ >%‹º;d}ÁêÈ–/}jR|šGži‚¹Ôv À•2%nçtC`™( l`¤Ä©Sí1Ž)Ü[@@ŸšÓ¨À';QþTŸ£e–Àæ@ÐJ mâǼŒ€}—M]e•÷¨"ß»H‘yôk +d?´•ýú–`>&æ©Pïå!ÂÁÚO."ò SGí Žfƒrgh!–¸1Ñ;œ(±À;ù¢>À8èA +r{¦õºÐtTÀ¬÷{¶ ©ÍÈÂë“R…ÙÜ:"³@•áá=z¼WQŒ«x#(Uä*ýyŠX–xœR‚ДŠ÷Œ +x@CãG?@s—wU¨ÿþYéì/"(•Ööôǃ9Ä5ªo{OšhôñL+{”ýLgMoŒˆ¥Cä{DÜ«~„d´W“/Ï3éoU ù~"ª²æEòà¡wv±Ù@1}®‹¯~¹ƒýÀÝTIëïQ¿>_nÆŽZ‡\wZÁy¶×ÒQ‘µ÷â¼ÌؘÐÉÉû…E¡ê‹ˆ¤½dB7ø(Aý…³ŠÝ98 Ç${Ô áÓJ«L¬ì‘ïèeL„ª¿ˆ¸û3ˆx”LéÚ~qžuØ£~®‹µ&t‚@¸RAx‹ø¼0GVû9 +9™ K’ÃfI›&‡SV“8 “Š!H¯Ø1ìyQgzdE.[Z2iZ´él+"<Šâ˜…Šâd‰B›T…Õ ú¼I+Õ„ wèèØéÇ‘ª4b +9©é³ã@¸ôT„4ˆ€4à}~g+PGÂRÿ=3ÿÃéj#žЈëÔé§Á~¤› f¥Cö"¯r e˜@àFã´“.jåŸÿm” m €½}úؤ‘O“®˜Íë ‰ö&y`åÕ¥nŸþí߃4*Øß_UYrdŸL‹Ä_ (òÒ©Aà@Š¥“¬zZ]sU€‰:0:/)…`@÷ù«;uþ¸ iEce—?ø»Ø@‰þß±Þe¡ÿ@7Wh–× #s‘í÷ÎËÉN?$¦ƒBø0KöœaJDußç¡´O3„r 2žPž=Þ8¹ tÙÀ‚A1tGÔ»oxæ·ÒÁàA.èa îÏq@nn,Øg<¹û>äC-G‹AŠj³Ž±XuŽ ñ€^^¤vÍŒrFۛƽèwO#&™RþóÓí*JDRöó\ mÎØЦ+ÕÇfz‘'*H…rã<¸àQNº–túÅkú´ 3íŸB­ç\=)„Zö>hJiç}Ø£ yr’Æ9€R'Ýk%Æ_maeAÃd)­®í‚†Ù‹…ÀŠýù³þ³Ý&oX1G{QtGypÏ;¿¢¥~p׎ +lµ&ƒQeØjˆKž>#,VOD»éy¯ƒ1ÚŒ¿ +!Û_E8:¸ LåŽÙdRM +M÷mÆŒi{0-¹§èB…ý8å>î·GBø ìvPO¥Ëý‹ùÒH²~ôå~sÎF¹_Ë’X~&ƒˆ`Y-š¨!L$·ô–˜ ÜÌJÜml˦u»ÚŽë_„œð +4Á±L?pTHL˜>{×üÛýJÍ»W§Oí•>9×Ý‹elÐ@Ε–`ÿÊú„„ë'¶â‰*9GñNiß–)ì’/e(°s© WÞtñÒ~‹í{OÜ^)£¡«˜¥7ÛÁ–Å~PèÈöÐ$ó&l;ïnšµ˜ôò3 ó=ÊtØz§¡Ë°U’Ѿ/œ¨ýñ+v¿HHÓ¤UœòvñZM è& Šâ1øÁ)‘„º®ƒ1¤É>#€âñ˜â¡l«$d¿|+.ƒD¶]¡ûJ²KC’+ÐÇÛ³7²5.Ä.Š9™ªrM.Dyé€Û┹‰>“T„õºÐãÌÕö:÷%æõ¤-d+F$4T   û‹v–ÖPæÉ›6ì'o79àRÒ»hmµÂ-ÙîW‹wTqÔS³V‹¹7ôsß]º¬¶àïß+‚®ƒ¼Ç{~‡šW…öwM‹ÚÔÉfŠõŒB«ªsVÑIQ¯ÄCãJ¬–ßÀP\}ßØ*þ®³05”“ÑkÙçÀ7 +ÜD²Ïò@·B;nåipnK"fÈbVPŽ÷’ ®íÝ0»‘1Д’C½PC¸bØ|æ]”“éï©//E0pפ1Gû¨˜–ƒ“!E03ωÜÝ>O¶»ìñ%5ZeÑa%u‚¨q8}?âÀPÒ'’…µ«uC¬PŽ ùSw|¿ÂîЃÈJ† æ:_Q†æÏð<7XÊæœ +ÕüDeŠ[O›‘ÊÄB„]¿Aqÿ ^rô;‡}V¢Òy‹è.ò~Ô0Tz“žâ„ç—•ˆúAo1#’Mîo¹ÒqÿÛãæW€Eà„ðä\q28-Ð;”*:jö{s6¢Þ&›šš(¹N›a¸å^#–„0Ã-? §ÈˆÚɈŒ‰êëôɘ\WÉ©¨zyUØú¯Ø£üåìí&‚¿„U|ÐÞµšMW2òV¸,gàçâ6Ö¡›d¬‡»ÓE~ˆ ¡ƒÿŽ|4Rer<ÐõV~FÐ&À,ç‹Ê¤ˆ(þ³Rô(qÊúæk”¡ÔÌs@§³ééS˜ +-MÛg$Ö¬p-÷’÷J±îÇ[#ŸFdCaž+*—ùj„Ù&·4®$Ô‚ —šŽ@gìD¥AkapXN’ÀÕµ`¨QáLT=™)H9s 7t¬:w©œðK临ŒAàMÜRtÐñº[²®`>Ú®9’iq$Œ¯'hSKò#Y,ˤµ +¹ÝóÎÉ +Ìl;dú-‚XùIxÚñ¾Œ§”bƒ ÂøµKíAD Øß.ÿ " ½*” +  P_UŒMú$oϦ ÎêÌàó¹WÚ÷sX ¤ü¯I'; /PEjrSB +:dI46Q'XÂÏ>6+;„¨Ò ¾„Ö1Š¸#¤\ÈÞ“†ž·&*¤ø\>'¾ Fv4Nh|’A¸›^øð²+ú(ÏGÞxöÜømÒÇØBЬsÍÌÀ±ßîJ¿ jTF0\Øo'UgÆ Œô“`Á„Zጭ1ó[cÈÁF¨”'G§ù€x]^\®;ÏËJ{îê‚!¨¢¯Ã¶DÍ2ìu¯‹¦› +YÉnŠÝFqcæPñ£Úw™_8 ¥·VKBŠJ šP8V‰§+|#é ¬U§â d΢ڲÚ|8N+/pñH£Ÿ!þ2ï³ µÆU´25WZ²EÙÛ1WÑö"e·GœÂߘâ{¹½Ûš.9½Z€ÁÇ4ˆûêˆW‘êû£OQ̈ ë´WGË;EJÜ¢H´wé‚uÁ¢ÄÍ/Q®ÒNT@•ydIkm6<Ïij愈ΌüqÿÔÑ^<ü´©xrˆDÑß4÷Ay˾KÀn÷xŸ3Ðj½Q3W†úå£#+kÙcbæ®,]>3ÙúG•Œ…HʆØQz¿ål'}Tµtw.}€ŸDùo©ÝDB‰q—m_HäýõhlB#éÂCANÖ98ÇñÁßaâeu"->³Cÿ~K&Mj@â.´ yÏrĬã2ö­ŠpŠÁÏÏèÃÁòçÅàŒþ¹D§rQËU 94*lOÑ9°«J"mçÑ9+éâð³9Í/¹©aoO†Ët:ÌÏ©Æ#)4vÆb…“YG%¡Í6ôÃ6 &…uè©V<ƒQ¿,£Ÿ}8Ò;ª0ñXOÒ) +6ƒK(Êé%2¹£ðdÃZ M%¢‚˜€ ‰Ï]ˆþ¬¼|×T䘋§EDì!ÉʧŠ¤\!‘ù;"*ˆòݯíÁ—âJ$ÕD0ã»ð*îoœB—ó±¨j„Ø0ó㥂DKgV©¸7Y½²WlhŒŽ€89­àʼné·ïü¤iÇ>DÎW6Ò@/©í#⣌NµÓA5ãtlQ—Û^*/C­Uydüà@Í«æC ¾9©gh‹«D‰ÙÁ¦à>¡a‘ÉfüCíH±¯å(%EÔŒPMRšâ¡É”Nõ`+ÌôD‰EÝí%|„ºk2Š*qDñ6uN³Â{®4›yéJø™¾ +¡T°<ˆÊB;¢äré9Ô½ž ©?›¶½ª±q!Ŷ¼´_ Ö€8 ˆÀAE¹Ú”.Z¤J¨¼ ’À‡¤Øh\ÞÀä _´€úıþÈ7%2ð彟 -lÒD€÷ùƒ®×F;¢%8ÂzOcÞH ¾=[ +g šcî9”Ó}0èjVæ»*3@tx”:Ÿ÷•UP%A(!¨âÈ‚É[í£v6»®èôŒY‹™ïiç!ÇC;ТWlÃŽ(G˜+zzË–G ë; +ÔT[X/9â²#¬*1’ï6¨…R„ÊG³¹dTì(ïØi¿t9`D>Ãfæq6§yyé¢M(NÁLa>@­PUlë|aÙˆ2ÐtPé€òMùØú†Íq­¦'¢±ö£ºxTSÝŸPÅ¢· €h‰ûs¶®(ÝÑ=G•<†uÜ—HÝû.Œ½àrÁz^ì¾L³½Gi²¨m+þ\?ƒ2]õèn£¬ +K Ž•?ôÅH4¶à= +˜ÚÔ›Èï*_ ¸˜¢ujOÕ:l¬s‡x&àÑÁ’\‹n²ã£#C5Ý#¤ÆÇUGÏ!LODè[tA¹F¨dž u–ˆ]Š=õÓ"#J¶“æ¡ú3VçÈÞ)à7<è‰P ïFûTÈW5¼ŠF]q¡_vݽ;²ðÂã”f:ˆÂPÁïÝ­˜dPˆ­Ê—«>á9Ô˜ô +·¤ãŠggËÍVž#$¿OÊ[iu”AßÃýhíM­.÷ýæFo¸³-…ž¤Ç^Ú“#º¬´Ë~—äû£ä¥#Ek§¡$Ì{&É‘A”@‰°0)½ +XÑ|:Ö%@8Í<{çÖõ®‡D!ÅœP#o¡I¨ ÂŽr³ED«>(Øÿ"ùÅZªv³œ"¯¿ý$,ÍÍ×…´'Ohf3=ªàRý:Tl  ÁþHd}" Ä :øÙQš2œ©UýýÝux_@§£_£=€3]³7  ˆ# ° XÔáîH±ƒÓƒT.#‘¶‰~@Ê“`±{Öú·¡ ®`î2¥=UµISÝRa<Y;÷¦ƒŒš‚ôZЖð*e¼ÑV~κiñUý °ÝOõ¯Ü>š˜ç]sòIºr§Vos˜6 àlì„Á¶A=jÁá @o5×½c©¬ü}×= c‰'ÝÕ†º¢aX¥51°ÊsA­(Çïù´×õñ +ÌZ(DÍ 3Cçz{Œb÷Ÿu#JeÙJLÌËûÎdë#Š {ç,ó>’ó“28YÀ ¥-Yš _^|w½úݾu71Oãù°óG‘!²ç(ši6bNÔßU‹:\†äÌlpyÇèÂUÄR”&ížÍ8¦jAû\€´Õ5‘C2ã®<”Z|´ ƒ±¤r¨9säÝ‘þt +€ry@½†‘Ú‘×ÛÙ9S–FëCn¥ŽyF¨¹¶demg´?Ÿ°º­bÞ!hî'„¡‡_øË(VÀ_ûœgDÒAåYùžr"dªì¯)`˜jUD¤çãJÚ㯒}š=5 >ÛçG9c ³_í¯¯åƒÇ›/…Á~ã +›=´®í ’K,-Ö¾›J;JJ¸ÊLö ïŒe¡H-üM³F’ÒÃòF ovÊ()f7¦fØ;°N™"´—Fˆ,6,8[õªŠÀnQUì6Ï÷w…ÙŽ°ëÅž(½(š-A‰²^I…sH­‡^%`³u„0Âkt Bï­‡±ö;Ö"€šcÅå¯~G#P493£ö ËP›Ú¡ÁN’!DX¯˜™¢­Í•ž " e& Yr‘î—BÒƒ°g‰¥D•Û„ÂP—ÔC TåÝ +JéqŒ÷UäÎØ)Gº_HKÅ-rÜ +ê~^ù%$,{Jæ ªuB+€F¦ö±’”P5j”HD×Ó’ªª1Óõ”AøÉšóu%Ó¾¢»Z|S­·É{ªÚeQ&[²JhêA[h«¸ò€!Ÿ%©©íNçûOgÀ‡cÿF„G%™ë´_zaæVB8OC‚,Ô¶Ôà,ESVSTº`ï‡6.•Õ,Oô]1¿Š¨LÊRHTBäÛeœ˜4Yv_!ÂAÏuÞní Ù¬EX)Q&#›^e†Èá`ÐöªLBÃãÃu-gù™6‡’3¥eEVãõ;š’UÒ°°}Ö+.xh±Tñs€æíOö²¼¤ý¨æã¼}(õ¾25P¥Èà—ûSÓdËÍë•Â/!…/c*¯0±ÌâÑ›ß|ü'ûŸ¯œß¥ó•G¹æ©á%Uû9¤”ÃQ:ŸsÒÀœç=H²žòP-Éê pµGJ™Íÿx΄íÙ»u-²²»ŠÙâ¡èÁÀl îOØXÇl`>T]Ø›žRD´=Íx™€òËû‚¾$|x†‰âØC"4áÉK§çQKl")FëðÎmàÉ^ž£Æ9¢Oœ™yÚëJ6vTÏQ!Ö›L0d4àSÚ|bYTŸiÉ\©Å5BéµIRVBæ8¼q0+º«n>ÆŽ#´Š@[˜;….÷ÞËÔTîÞÝ4;ºeVÔzr"·D$ųŽ£Lèžqài^É8…„rWX*©¤D ÝžØgO*#!ðiÏÒÎ.-šÎçJ½kŸ¼'—  ôaëÔ ŠBçQ€”:Ëì¥ÌQ²ÄNLƒ]Û]ì&쌮ÁAø Ør4eû’ëw²¶v¢Øù°o ¾wÈK= 1ʌـÕRvÄ LD×Úd2Å×BcÊÁˆ ÄOAC©}"Äã©S²Â‰Aä̤RÐ⻟œGžcßìvÅÜëР¿›ÌNÁ 8±5¶Ðäæãè2ìÕý öšàšƒ=k#B¿²Û[pÇÞ"¼Ò*^~¡|ŸŸ/Ncí“Ä;£°ò ÐSÞöÎ Kd…{™OAÔBXë2²ÕìÌ`T>ŠlW¨ý«ƒ“ª„®ý=ài'”½uºÉÌ ÷Sè««•Òþw+«Zû9!NÉG…öãÁ«oÔ5mâ âÀê4m®˜îV‰³¡è8p/ ˜ ,&Ä9c›Ö«­ˆpq‡bðñ ¬ï! 3Ï×è(Þí(0éû<#šç;8xDL ü³o»À‚¸¼.t{x‚.ÅÚŽ˜v^å Æ"NsjÍÒã.ŠùýÃÝ“õ=y*GHœ[”,G6]§,è¢.ªc¾@} +’»R¬$}3Ÿˆz"¦ªÝÛÁË7Ûìý½”Õj›­YËè_D|5Zߣ +Ø@=t:‰Þ·Ž+Ö+•g<ñ †+Ë+ïÓ´båáŠh"ÁƒBÆbd$¦ÒÜ´gy¢V +‚Š›Pë]¬Tã  ŒšäÎûµ–mÊNþhIŒÌjJz:DtªxT±B ¼†§k+÷Dµ÷¾MD eëa–¥{¡¸ùqVspÿi„9M€I™2ö#! ­c #C––ÎUÒX§±çÍîZ@ú$ÓwF +ÄâZùU[CPl´ç‘Úí¡çñuà è¿œ+Z´‚D½­SŸÌL-l-–j eÒÈcØ0‘Ç”«ËB”ª0ôHFDÑk Š"‰ç±ë/hlTJAú¬é÷ŸÏÈþbæYåÉ<½^̃5xæ MmçÁ_u±®!È$ù¬i·bÏ GlWÆ3©!†€°X¶5òsY„‰ŽÑÙL¨[-:Žï"úíæð¢ Lî¥ËÐ+¶pzI«AÒ 3øý’%õ1óQq&³hq%M;(÷­‹víù nÐÎáÓS|\Q(½Bs:O’@j›ÅN +;긋Ø5ìF@/:hס€É\×ÃyšË˜TdD ›ƒÚZ¨)Œ° +¨DÇ¢±á/>Ù§»C]Ñ:|ésVkë¼I°EꮆgÌ +5ó0õ@´€•ÍvØ·’ 4U]—œ¾ý}0YÐU•]«4½, wàlÝÕYÀ¸ö¨æO¨ÃºsW'r¿pEãNG„‚`yeJ£—ˆjÙ!4ƸRT±äª_ìæ‡û¹‘rÛã||)¼‡GjD ¦LÑl[DÒGÝïóxŽÿ(ÛßeJµ´âf(»{§A „Wb©О yõç_šŸ~ÁU +Š:¸E;:!9¡/Ù& oè uO"~ã`ŠÈ’ jSìRÓzܼv›T›¤à.€\Å×wê']µ^tÄöýÑ dèWJ©KŒ%áéyÔ‡©WµM"XèÕ ñv> +ˆàÓ‹{xQP*OpáõýF d£u›Ä&) +À€‹ÆyBûU“ߺƒäµ½EÄ>ê‰Né&ë{»¡¦ˆz{XR|Òf3ä5*U¢Gð2Ž5lZÑW;‹Ã²8ÞÃÙ!JÈ)}¾™Ãú̹by{"\ç“š{q¥PñA6h†‘Š§Dä[æÅnÂí:\É8ͤ©šš6]üÖ‰O~eA™vhYÔóRÇGŸÌæTÁ™âò•#‚PŽ¬`£º„*i±œê¨²3Ú¬Ç"]ºWj!H¯ð±ûÉ”9E{Éȯ¢ÍÖ`j„´¬¶JJF§§ŠÛŠP¶ª€9ôg$§Õ>ê[‹^Až‡+Þ# +È°ç GFjÈ3¤QéÖóc$×ÍC3S<ñ¹ãlá +ûðÜ‘¡rd´h~–"~t3ùûÞß"=u¯Úýœ7çsL;‘AÐ#Ͻï–Ë÷ö¸[A"ØÅ“ +>Š7ùýï1©]ãl4Ò$þ´É›¢–Y°D›boÀÿQW°ûR|”–&Í®¦ÊÖÌè…ÙR%Q†¬ÞáF¿òª%”+c¸§½—ð 5W¥¤ÿxÒÊ XÃF˜½üÕX°qi-%Ò^yÙöùÁ¯ä¤MÏ¡]¡Ÿ*X-j0z¤„ ¸”B¡2å +ôð?à–Æ©âÒéW(g¹Ýû¢qÉú ¸bq}f…pìi7Ѫ:Ùí÷²6»J4u/0®O(¯C eÂJã²ôGH÷rPN6ŽãËÏÎÂŒ©ô[—}YJðZˆ©úlß NŸdo/³B!Q+>Ž<ÂC#÷ ¯Y-iž6ZÑX8è{’«í’j¡M¹T6…ý…;l¨¥Á4—Ø$U½éÀŒ"ëd9 ÈU“ŽÄÏåB¿ä æ ™ú >UÔ¡ª—‡‚ÃÕÇÀ‘µ9˜÷ØZ…žK£õ­âp£Kkµ»ÑehZ²–;a£:Ãûª‡‚ò¤#·ƒWN(“V×9ÛôÙ¯ï†pLdÂ^H}Û'5>ÌèêÑš+qŽU[}]ˆ<ͽÂj‘2ì½ÀàŽdkê~.Â<ðÙ|G(öÖ»zoß±Ûd/Q_Š  uR(`ÜB%ÎPÆ ¯·¯¦äÎŽ@Ö¶)š£`Tî´l'lÕ#× œÅ"ô÷L`çÍ ÿ…"%ÍPüä +éä +ÚPÀnpRIßF,»3Åírûk˜cí(DZ"J{˜!+$ÔȆýôÓ˜n±Ä¦|#̘’"GFØ¿ÈO`F_WBŠ¨v¾³Q­=‡kÝ•eêÁ²x„ù–“¿=G(Š¿Gg¿ +ˆ[öØ©9Šñ,fdhZèòlDí“¥Ø÷ )äs‚W‘‹ìBÍÍ’‚J‚† Pà—à ÷¨‘ ¨ßN9~;Ïüp\Lµò /.µaÄd'û.€çH-¹@ˆ­Áq ïŠë;»°½Ç§Ïòá÷ó&ü…#=$I&ÎÄÜ6ê"õ!eˆ¡ç¥6jw¦øt²ÊsÈ!”&åØö0"¤Í=Ð,j`áDxï6‹@L¹³¹=/#n¹Fyœn.‡ªAmEXQæçpÞë‚pø=pǹ}‘{t¥J2~~ŒÒAd†ƒˆârœ8öÎK[}“ sHQ`¨«&¤Màsºxowl…"o×WQt³§Z”ÍÙH'@‚+“ÕÙ_>7}zI$Ï!Pí›rM×òÁåQ‚¡í¨?¸¨.=q_Ütilï²^¶Â15Wâ? Á—>~{î6nQè¥NT˜"¢Š²ŽHèX àr\GŠ¨£™27¿#§“ íoR® ™ø¶°Nç:Ÿ‡ÊZ( ùh’±Â` y8ìžâEçEõ="I 30Ö¡®´&“(ô›¬ýèü¢´—ȼûÔêá-¢TWìÌÕ{B„€G›2‰”ÂS oT‘ûF†‚AªŠ”HûeÇ:þ-"îw³øY•¸~?OŠö ßÇ\b–ç'“”½Ø~æ!•9/òâÇ T§hrí9zçµ¼6ùx®ƒíÍôŠÖ©æÀÛ"geZ 5½½h9y~Ž¸ï'|Ê%è±´/΃6™ì:ރ녊f€ ÛWîŸr¸Ü¬’?ʪO0:i‡ÅŒâc“ÚÉ”ÑyäèšîJ˜+î ¨YSægì=·åþEDܼ€$*€hÞÏse>!F·›oµùkñ3/ Qâ Êé *î*óå~©†•%ÿž0QWÓ™†6òÞ[+ÍËžNïZ2JÞLÝBz¸…ÜM3Q:Ü=hù=qó}¡À=ÎÓü¶P2ö1päŸø!ñß$XÍl3âeÌ!€ŒÐ¾Ï¢> ýÍ©¡u*ËD¦Ð :‡î9Å7á±ÇˆmÍ»íy‹R8´‡p¨²‡ö›žy¼ûïT,hž?ãÞ#76Û’¢T³á5ÂÓö|(9:H{ì·3à3:BTPØö¨ŸéÝ^²¼~:“°Ãó nO:Óõµ"t¾××½± 'ÿÏÚÛìJdç¹WpîáÊgPfü1È¡]0 2lœ‰íQChÉ€·ZÛ|÷'žç æ®ÊÜRY@w£U›+ÉL2±b­÷'¥Ö~t7£ÍwƒV‹lë{ÄïŸ&žžšàªÔ5}?Ï9Ânoê…i4‘µäïß.oA¬&MsáRŠ²B%.ƒã<¤¡Ê2ŠÕä½±r4éd›VLù˜öónbìÖ‚6rã ú°§²Ýñ?…ï~QRÓz#0ä£5Ù|€c Ak²“ð¨y4ÄCÐI©Ìôí_  ÞM)†]×5þàNŠ>"þi—œ¹ß,/\™˜´¶ê/ûT±SF ›òÞ¥À *˜Æ>ºIå±IšI®ˆšã‰¢”Óä(ïóX.;a#ÊÜ3Ô¦A‡@õý”ÝHCB½y3Ÿ0Ÿ¥oh…îßãMív¤æïðH¿î|ÇJÙ‚ zͯó‹àœkË>ï<˜[æë,, ÷Ë‚èñÓPcaíR¤Ü°õä©O„ç¶ÇF‘{ì­q ÷8Ä8|åŸ ‘F£MkÑusÒÏ9üÙ «ÕkðÔX(ìÆT^±ÀIít ©º]†‰ +)„nëK¨‰\H%ìF‰€[ÀNm“{Lî9Ï! í…yìU_çÕ÷´ˆá1Õ+ºT ™oT9#' EëYÅ Öïmá#m”¥ê)à" EKî”CuÉ`ªÚàI IÞRà7¯Ï°fm ôdwÛ·žz³#ý´upúÒ*VR‘Äã]@2Õ^BÄ"ái]œ0¾Ïœ­Ù#tó´hŒòJ>€Pž×pÍéPŠ.»f=?j?7rm¢pÀS—4u‚EUZ~¢€ÃŠyÇèˆ<Äq=DA¯ø…;8D…Û¬‡$dœAršntRÐs»/×$^eU©kwº¾öñÇÝWú‡¼ðiUƒ£ª…Î8À +Ý]ý6"9ÅX"Pl ÅôË®¬Vd§$Y‘Ú×™Û"€/ø µ3­ˆçæñ&+9}ôQP!Çå£ :"Tb²«¬ ÿù8iÝagc¦\ã¿­a¬*}›Á üb âÞÒœ=ä+TCáT›ÒÎ È#Ð67½<„,¢”[¥%³Q#Q¯ˆèéO­²ŒP=¿)7;v"FÌ|ð#븚DÏu¨ #!š¨){õ†æÚs½–“½kUKÊ#ü­ýM¬lM@'ùªÛªeU^OF¢Ìº½“¸¶oÌ~rê{LTª›<³;žòÊATÔ"‚ˆðZ¾¶¼[ƒˆö´l?ÎÇÕ2! ‰ý¨u%B€m¤uýEÎݶ™¥Íñü9µüÁàÒÐ÷GÐ(Ò‰½Ì”è¤~ƒvBñ _ÞõÀ`¦¶ˆ'åxB0“bDS£®I„·_}(˜†ò­#¬ldÂ_?¡Óžg¼,é™­.0¿ùÍÇ +pxGOÎde o××+¥¨5©"¥ô5€f¸4ŠŽ­}‚ã‚fŒíÝýZgg<=Ó©®ñíºã·¹ø'ÅÅIO5ƒžóé0¡ì—á<€D}”Ê›ælåEbOÖâß3¬ÓKöSú&M¶•ê'lÈ&úyBŸˆ‚sˆG£Ö&·ÀÜ1^_ׄ¼bwë׆Ù\ˆ¿¼ Ãœ´ÊVj8òPÁù\[kôý@ÞšwÊ÷ÇDZiÜÚ÷ ƒÒPT8ùÂO>Ç‘ç8žzàGìw§ ¬ú|;ÆÖw½cø ía$ªx#´vô²n_0ö•‘óEë¿F‰¤’/•ú™·:¥+œ½ “2µa ⢪Ò%¨ºXë{œ*sm ŠÁQ]d?v +æªuôo +ÞãŠõÅ겜¨î* +Cpé°&rh…=£hýˆ ¸L6À§ë'¬Ëf 3× ™ãxäÕÖ/zj¦×¶Â\©ðEb©cŠJre6 +vDð˜ˆP’ÓÈÛêГDIÇFúÃö­_翵SC8£åãZRÍ©êGP¼âï™*®}ác Ù-êªÒ”FÜàzf¨wìp†ÝÅt¨ÍÐ¥nŸ¶>§} e}¯±¨"Ç{ úñûAiMjLë*1e„¹/bŸ{¨\“± bÈêJÖ~diq´b_ƒÔÃG”ò>§~TæÛŠ¸”Sþ<@1òÎ µfÞGá-êC\›æ¾Y’[á|œúœ Øi–wµtæTåå=â5‡`†”]˜ó¬uO‘)(­g ‹N á8‚îgF+!·{%ÖÜîõÐöPŠš9HmWjþF§‰€d’ó–¨M¶u}Š½A"Ô8#Á "FyÍM%Ý—™AvZܽršºD¹P|Å.±¬× UêÌ/±Ý1,ñ?Öaç¾ÐdÂ)òêFüÀà< s@Éî¶y‰¿:ÀXõ pr?EC£43F>m:æ•ìî™gÔÜ2Ž*Gr$…è×]æá&V Æö‘mCÀv°FÑtþæ<8oÕ¬èÌޥеM†°:4žª‡¦Žbn:)æ=ˆš±!Û¡ê üx”;ϘâMàø‘ ?hí!ÑÑc„¢ X* +Ò7×&„ÆôϤ÷z\G‹ü§ª0ô¶P·Xïs;RQÞj‹WÈý‡((ÌÛ‘ +7e[¨ç£¬P÷F¬jtòÍyúH +Þgl|ï%¸–šû7åá¶"6ù<•Ï¨#ú˘ Î{3®C¯§RÓÇvðÅÉ—¿é²¶É5¼iU—­õÖãÙVŸéèñËѬ2JDòÄœó€iåY8 G¼ÓÏ@žŽVX¾‚®šj¶_^kòªytb´çÊFJ>>ÉPQ‘GÕ–ðض_í¯„ä¹ªð#;ÿœŠ”XSä”ΰ~ÕäF„cÜ9ƒ"=ÛsO£‰{jÆý]š¸"’Šô-jlïVhqZœúŒŒ“rn¯^ò#èÎr¬}FäJË*ÔðH¿çØ|Dú«êü¶ ¤È +Š¥”²¨(éÊ3ºŸç ë÷‘¿(A­Õô|XQT8‰‚%ðWûûÜa™Ôv›})è ˜J”|hÏA$²#/º”ñeÇ”¯Òú/JG¿Šº¸XçA••ªþPG—‰€eš©VIÓêò,S0æåüÚ"Éüc®"¢Ü”Uùâ,1ÔÈ…뎠†Ñé#•Ï…6¥å÷¨RâÊÐQŸP1òŠÙÞÛQ™$E]Pj¶-Ïø"½s[ŒXÌGMt«b]S ™÷bËh§5˜J¤Š€Zb€`ì[J©#Jþ·]á«H‘TÊ_U\9/‡y  ¢²¯e n9bCê)´oÙ±!Ô$^ù“"ÛU‘µ¨cê0¶>tÛ&ât +†¹íêÕÜÕ+€ÔDôî9¨¾ä£G¨A>§¡€v•Šg]±?gG$I×Uñõ»?5°˜Õ€ÿQ]Ya´c[À¼vßN[_] +¨Bçj`´–ÃJÂ0b3 Òþzªã¼êAv½gˆ‚þÁÄ•h-U¤¶àpUT[]~êÝ=ÐoMHYƒ‹½¦ÆqöÒ-çg# +/¯À% +Èû«ðØ{7¢P6& +«†DUЇsmB÷yt2¬ðã÷Ì¡§ +ª3 7ƒ2Þ€Åô‹*™xSĵt£T“4§a Çh8 á"iÚ¹¿]t)ïÀõ÷äk +‹M4C£A¢§Ø;¨qVо~P€}x)©§"h z¶çXÞ?ØJ«$øú Æ\´ü‡˜ÆPZ¡¾å^}'îÕV‹Æ>ÿGTO-vãw*Ïà#t£D~ƒµ ®ÒC‚¦ž¡‘äû/ÀÆhüǧî$ ëWÏ,³ ‘k¡ÎKµ±Ü17à[dë´óñoõ Åq*×£'*zÚ·os>N/jhq=m§Íü +-à/ÞÏ(…99¹“ãu¥†·Õï±õÄ6w¢ÓÖ €D”€V¯ ÜÌø¸§”XE²]ýSÙ;—"éPÕé‘z;ÒÀ*‚›ÅŸqX¦ DCŠ 2ðù]ÄpŸÆ^ûÕ瑼G‘Iž@ƒ(Z½©ÒSv+éf™®BJlò Áê&Óòòàu戅¼÷ïg1“_SÜ Î ºƒ6MA¤0™û{Äwƒø#ŠšY‰¬«àgäôàõĬ@Š9¿cåî’ ¢£=]‰ÆŸ}IZ0üÌõ’7â¼èCÝaë¬Î ø%<×Ȧ®|UFrå3"¿)†¬¬Ð©·Ïóà^ÏdK³ÉÓ”$]뜑¹€nÏ£ø86~ +j© $W¥ Ž´Çª²Ó°…£Í¬Í, Ï4k=«srN—¢„qPÁ›êÉÙ±÷xËY´à«[Qÿ\‹Ý#®ÂÝë +·Œ½-žFÈX{®b¡°Cñ¶ÃW·â©ä—çrcç"^†*DÜW®Ôd­5Æ’Šb4ON,ww&Õê˜ÀÏ•dá)LeÓ”ø§\<5`EŸ‰wYìÀÐ ò›ùë¸ À6zr¶Ï¨;ÊÆà‹ç~°IõCµìͲÛwàMW-‡è#¹~€mĈÌZ-¬àüsÜßœç<”û¸µTJçŠì~‡ >¾ °ÕÉë‰À& |I®%&d¬¥Àœ6VÒ­  æÂø3Èo(á8x‘³¢þ¼Ï/S¡õ]è†i–a°-ãœÓ6Uâ">ÊÙvè~rmûLŸÍHZê…€u£ê: [‚ZžN^GÔhÄG¥êdÄ“ædZqHœS^¿ãø »¸è qú­Éx¥Cà•€Þj1†N}Ùí-…€‡ÂSÙÝÙÝð æÅ'%í”C\wTSzÀþæªôÔ^{ 4€¡ïôu« +Q°¼Ž@¡Qƒæø¸Ÿ'ºŠtÌ0"ÞÌp¿ªð“õæö×é)Îóq{g|Ê]½Õh€ÿ&A×½3â-žoàV‡SÐYúÐÍLµkLÔÜy”­Ýõzt“¥Ôw¥ÉIiï E±Åâ"rôÿr.ôìD <ƒ†Ý€Èc¥d‡>îðF²&Pú +—õ7ìcíBA)@LW»þ¬3“ÖH—_+óqéz¨¶Ù|oo6¿uE´WæØN§óF¾¸F'¡­g½6˜Z«Þ‰pÝ>è»Ã•¹³+!â*ÉÔ¬€iNü¸º•ó»³FŒ´*j|(õÌ7¸±Œ$¢ŸŠ£\_PÙþ@Ntç¨BQÊxèÜì=иjà’¢Ø(Ø'ˆ|ò¥ë*¥êÑbãNµ÷ ~ÂÕm“³E¶Àkoa>87ÙùΞL­¤Épê^ jJÔbø‘!Ø=bmZ7´óiOj¥H˜¶‡SF{j%QD¡£kTú‰Cûi`ÝVu­m[Œ—,»&€=8Z±C¤hðz|]fÛÓJâÀÄN¸1#ÍŠ5r}´yò;äœÅ ¢£Ç|©è@¯?ó>™ÑSÙðÍ%5£† +º‹{é1àéÙÛºÎדøõAžpHhR3Þtñ†¿h™€úcò¥A É÷ûTÀñ[„×é—Ût´öÍyÀè£ø^€™Ì@9·5R†-v +[7Úwœ¢ûÛ^´’º'Ñî÷ÜÊ rHDU6ˆU<=h„ãÃiçóËæ<öÚ¶¼G¤·ÊN¿(P«‰Âçy†ñé¯×bÌØá¬{Äv»ixúñŽ/†×¯£ÔWÒ*PÕ›e©1Ÿ9"2(Д]:+HßþÆpYÒÀøu@6×w¼Ö»¨ ãÇYüwòÈ_µ ¢Q(¹Ùnù ¬%û=´¯ó%XËjÍ|ŠÒ]4¶blÏéõX õÔI±‹Ö>#žÑvZM¡ær–oÎ3â\KïDCÏ.ÇZrNiÀ#@É[!Ÿ-ïÀ¯ÆO5ŠYH(£– +—˜!´m˜_ˆàý¶ –€8y£B̦|صdÁ©âÛÖÜŸ…‰•ôçBv?‹ÖYW¢ä4ª‡çAì î܉ˆ&ãy H„ )[Ó²R§M§\©ÈáÀ[oå;=3Š{bŠ‡”>Æ¡×=^Àåp8bݨPn™{°i,ÍÛ®áÔ[@Ò†~|¸ë#¡ýæY’ Ç +z½3??#Ž°‚Òºžç=è whIÐBóÒ•©9©ÈÀă_‰ZÌú+½êûñÜ«!¦ŒæR[ÇùWç¸Óÿ8¼çù¦ÝÅcôÒ¿˜é#v²‘×3yBiW¨å¡÷™o{[<[IŸØ¥¾«7Mõª+Åê A–¤~F<¯fʃ¨×ñÍyáÈy2 ¶ Po¾ù3ûª*û §Îùul«%ҋ延‡ß•³2ˆ#"@è^®@ð™=ÖRæz‹È-»¢¥…‰Äûó<ÚQ+0Nc¬ïtlìtŒŸª³ù¹Ô˜òñ¤U’Ò½åSs0è¦ÃøÁ«™‡ŸúÈ «ö³nÐ5d,ûi×Òr¸_W’hkå‘2s +*–Æ×q\EY0nèm·2Ø:â(Ú3@JVƒö)üÀÉGØè€Æ3g²6ë$°ëã‡n¸8ß´jŠIË6ØÐn×õ:[î„n›æb)‘žê^¼GA£0±V>°Ú¹ÒgT‰—åÀ“áÌŒÞI(Éò ÉFÇf+E ž¯a{ÜwèÿâS¶®Ø;¬u`¾–Ûé’>%£Ø­šR?>Ö5™¡öc&yšê¢¾zX ¾JÊ*ã±Ua?&eæšt‡Sgÿ ÚH_r¥·¥Ü·€ß?Í4A!l\Fÿæ4k—lkÚ"…¯£¦ãŦEM¦ˆïÖÄ DTØöÐW(sÛHQÍ_K?zº‡Ä[wÚ‹ &Ƈȃ m±x„)ÚFºÞ»c>Í^‰"¡ªÝîX¯"ˆ©tä!!¥ Ò¥½QµÝPSÄfʾ&–S"îÙŽÕ]¢9åøÛÛU½˜¡EúÄ ‹ìûܯÿd ©eîfNk_ÇØsñÃÛ¹ÜvÖ™íPÖIOáÜ È½¬ÇUêU/â`499탧&30ºóq‹d‚ïÊŽˆ ÍÝÚÑwĽÝ€ÏÁ†ó¹’Z)- +ö1«i<îµjwÎã“\µoãœMÖmÓòEFÏÚÀŒDhÔØ·,e®Äå™{ý÷iT¸ÃeMúµN‰¸[´ù±áï' H¬\± ;¶Mö™wö™Vü —;£P”´î”O§Ñ Å ?æ Ø`ë**|«‚çºàÑQºÝëJ%˜oS¼þºY”ô×ÔÔpêW¦¡Ns® ×&eu¯G‰UhpçH”rª+êÜ49&lŽÏ#øß8ܧâ÷^&® ë¥=BTÌ¢ÝIÜÓë(¢h[äa$fÙAãhXbjpŽ_CâªÞÁq÷5·‚"ÜŠ>ð8¹Ç¬Š åÚ²ŸL‹ôàšÿ”øêªÅL7X%[šðWQŒž×r ¥é?#®°:@Ü›ßù…y 4•5QSaÐɘT•êL—3;ûu³$]…ƒáþ /°rˆØFuᙾÌE«@¸'s!z9Zù¬Åd@ ok²  ,Æ™‘:*έ.RüÀZÓL ~³Ê7ÈO°[(%ŠAÆDŽmí*ébp\QŽV¨Šž‰éº)Aê¦Æ!©\Gêúu3ä‰B‚¨8Rúm¶ÁlnHiíÚ-Yš«|‚qÈ'¨<¬¿G›ermN{l¾2†+P•È€tÔ®º©,·¯¡xÆHësV¬pÃQtÔ(É› 2’>¿æ}%ˆæ¹¢ÎS¹†½5%P’²ÊF—67RF(ˆYºZ@18A€’ó+ÐŽ­ô'÷kyÅ—æ®:­Fˆ,Cg´õpï\ÞÒêÍʯFÍ # §R5FÜ¿7©<Ï0O\¿Z6þY/HÐ"kUÈcþâÀ «7s•æÌ>8îØó4LKžç-‚c¥! €×°Çn?‚ry?‚üÖÂ|?ÈâʲüÎjþ~nÞÌzù®‡›ôƒ¦.÷Þ<_ä%DxtÉüíš“Vx,HtÀ…¤R¾Öœý"¿Eä™ +­Ýäºèá~žÇGTçmõ•Ú ‚QÉo~€"A‹ý#ð… 5ÿ&%~ï¤÷Ñ"Œ+³åQšŽö•Ÿyš™A eøùuׯ*@¼ÉÇí@ãL¬AÔ-  tFÝ ášbW·f;åa-Á‹æ™¯Ë˜Å¬ ÚOù2®`+ÙõA|°'¨È*‚¿*WA„ö š½Åõ~㡆eÁŽº”‰&«5ð^îX|€%Ð¥ÙçNŸ<&/DßM¨µu½}È£yOÛ>Î3G ‡ Bñð«ÐJ[_1™ùàèåõú¾±?4a]›†1ö·ûÛò:†ãØCAñàÞRƒ½ÛgÄï7.^ÂP™¿  %ªA”Äa/ˆ œŽ¹´B`˜o銳SàJ–çÇfß‚š¢¼hÖ]W,„îîwÊå¶-—»>£pü3…"W‰í (¯;€~÷2•-óå|2Ac@ 9úóGÈG+èF„×(Uöü8¢-†¶;¼€V]'k Ö %,G é+B "¢ÛÔî}]Éu7IåÙ c‘DVç×Ûó 6@Ä°'ù]òCvN^éVç o¸ˆÅÒø$â˜×o +©öFû⊒˜ jw3Gjø+3C,ÚÔöÅFòþ&¢^!d–uoêþEŸQ˜0+2ètr9ÙêÌBIP“ä• X+ŸQ¬UŽMA³÷ˆßïdIòæ.¤~sžµ1†î ’@³!U‹Ø%õ=ðþÂ!¼+Þ"ûÑ®°1ŽñÍ‹ì;b¹ñc0¿¼fø*àíøÚgAF–3߈Î9û¬ê&^9®.C…¯¶ïuϽ~üÉ‚¬L”¬}¢Ð&*ÊRDPœ%B +N§RŒèŽw®dNsCлŒ0eÕoõy4®vs¯v¼}— KS·Ðó ùRîVŠS~$èóf=Ո鸤Ò9ƒ4=hÝrŽ¾ÑÑÜ›\I;†a!ãRÒ™Y2Y s¦¶¸¸M/¿i%âÅ"å{@½ö]š³%«>‚”â$x xÉ9÷à°pº%¡¯HB‹í87 ` Ϋ×ö‘½Æ–üdr¸Tú{?ÏÃá'(ÜcRÉ6Š$ùµ2Ž8rK'Aù¥>ûd ¥Àä­Íõ!Œ¶ÌM@¸ïÀûFy“õõs»¥Ÿº¤LÉKX†ë9'5é”·2w5ŠO×Pb.|°@Éè˜IGŒ2l} U»!£™^‹î~Çý¾?‚wšOÆK\ŸgÑܺÄܺj&Sr]2&Rg°ÞkO;ÎùàªÞ£jœï]ih%œ¤ÊXòžÈ.)ò5}y‘ž,WØÃ%¢¼tM™~íE6ïA{hs×Ü‹kŽu)[¬\j“RRýöP˜]l–®×uô* hø5{8j2JKµJeXXlˆ%‘yKóÉ´æÉÜ-ç°DQÜßõSë?øàöM¢Uÿ2„ìŒö”ù€Ežñ(‰WÆÚža\øMÄÊ0žÄîã ãkÆüXX麶¸š°3ÁÕD\ lŒ8HÎûgD.0“¶;7ï›ó¬× \ðÚãã´ç÷B¥£Å²ÎæuNs±´¼/H»6¾ RŸ?£þjŸ4W.éô÷4~× 8â¨L[?¶šÓU»O–öû»ˆàVO½< á÷¨qDÈí-ЕèËJîHj\ŸÂµ,Øš t_È|+mKJS¨û`D¢—·0nÙ¼Ñj!}ÚB;N´×zñÊ7ï‹r +ÕïQhIÊe ”›I¡›ýÙ8ÃpÕøNÕ$ûÍ-Jñ·ª|º–gA½#ö\ÀÐÈ¡¨÷€Phs£DE& rV®œÃï5!œJ*†ãÆ$è®$bòXØT°Ë@È ºÉÚC`%â…î¡È7lø©ü–J*kÉ’ƒúßw;.$ÉG=Q´n1§E¡¬ +Õ ‡L6!ÿ@Šf0Ÿøv}þ=]š™*ÆÁîñ»(älÖÞx¿µ€Å€i ÿ>"@·Ð¡ð(@|õHÌ© ®¥$ÝålQä›É<ðz½õ +{·^—ûŒ¶V2kÏSÆ6ë{=À|ê Âö_^[DS¢;9q^½ü6ÊïÁÊðÿÝ]úù/yæmûvB¢Ñyˆí7.s'b¼ØQ‚ˆŠÝÙãpÈkÜ“¹ 4¯Tœ~†>\P)Þ¦GÑLF<¡æÖ?©é=ÝæóQšÈ-^fä¡¿JÎΚæQŸô£¡ãÖX"4_‡¾Iå”âQ7W +l7bÇKµõiÈV2tÐÛ×çì1>‡t‘Ür÷Ù*X¾"Øö0ÀçPÏ[“º=¾.€b&Q]¤3Qá ΢%Qú¡ø_åŒÓâ #¼þŽM~€¢ÿf÷û°µÀžaHaç5h6²NÒÄÖÛnhªY(™¤ä%RT=¹ýQöÚ\ú6hñ‚ +ˆw£"RºJkzŽßcÕ.ˆ73, +2(DñÆt§î[gÜ}ÐÖÈ×ßNÎÊî(Nð6ô×O»KŒ™¬¹%XƦê“'¦ÂíMÛj1xû1VkÖ¯ž®tˆƒÙ¨æ®ƒ¹òãÊÎWµ½<@¿‹Rª€ìò0„ó«Wìiúãã&<)_ËòÒz‚k¿ÖwÄTfþÖh Å?eäx¨mG(ÿ».ð$Sâkɸè倶gO¢'rrÙM¹’)ØKÖЩ/P¦å†-’×1C·]oÒ|6çôÄeI¯ü¢d!Ù‘>جZ_òûL"›ï^;#ˆÞ ¦5|†X…g®/‡%ÿ™ÌiÔ³;²&˜Åƒ7TÑúž&µ\'È3ÔÕÙ‹ŸO›ü‚‘ùœvŸÖC‹O͘ši¯_7¸o°ÆdUÃU±‹×ÉÀ[¹…¶t­ýí0=D7ÒÛ9üÌpù;àN]‰™ôÚKzj•àQC–Ë*Äô›Ä:]¨U‚;ý,¹J{ö}žWˆ€Ü·%SÐ|¾åJì—ˆè¶]ÇÒzv[eÎ!˜ï +-$¦_×ëBw CÞ˜LãG©Ãliè ´ÎÀiùAuøã›$&ÁÏ…)IÃíc#Ï0~Ýnà‚B -)úëIÓ”‰Æ8åe^)Ÿ(ØAH>29inh0t]ûQ ]ׂ²—è¶}zÖÌtªÑH •]ië ËŽ+ñи‰JnÔpuÁª8êN@¶¨§ ¬F€²žý ]nöx +L×cú +OWèu ušJrå<à>˜ÍϲR»Š¼ƒˆe7{`µ[öt{žÚ€›Mõ†—[ã4¬¸X@w8ð*BZTdäÍäíÿ'OÓB{>D¼k´½ëçP‘)U^ˆÜœ£~qsùi]¹3Wò·®w<ô ¼qMAÖ*IÙïÃ<ŽßË™óPrito„õúU3î<8¹Ž‚*âB¶œ2$Kä”›’-•ŠÂÃè¡÷*"ÔÌNÛß~?~þ3¾“ˆnÇ0;ôâÞâ‚Ò×ÔŒþ°£N9¤àÔeý­ë軫{JÑæÃõÄ3ç¼"¦’—`™Ä¿’›Ý¹nzŽ…[øû}¥ÔŒ +6¹D‘ä<ìº; € D•VuÛƒ­²QÝ­k_FÈ:†‚÷`­…‰ˆËXbÔ&æ©N¸Ú¦…ÌÌ«9+àoáP¡<h_Èúç&ë?^c#µ†ÅO÷1Q‹H§šlAc-Úk^ÉŠî¼i{üc7PÌWä«×ñù>"‡µ¢N¿QÔò ~‹tÙ@Ú%öF(ðr Vd…a/D˜C”mˆ‹Ç]¿.Ä–­ ØÌ×QÌk˜>r¡z<>R%>Rµä—˜Lt”Ùf‹2¢”±žÄU¾¨ÓÐKa¸‡úrnôÙÚÆêÃj}Kµž,-K´lã°-Y©ûŪ?îÈàâÑ"ƒÍEÑ4AÈU§ÈŽXÒ|4‰˜Y±´‡DÿL—¹§.4€…l¸ä¥¶ú-Û²GÀlk1ËÞ6ãëØ1:Ê×$ͳТµäú»¾æƒhzè¾øõ˜XŸZõÆZŒ5É{KÝìƒÅÔ2F`Ži«Õ×) L 4+LT툱¦ òì­F°1[bŒG) \¨ ½sï#×Û~JH¤DTÅäI P¨Js$Ç”pA?Æ.üðñz46™Q³bfPpó‘¹뺶¿uÆK*å<åeÏxL=­,aÿÇÖ´*‘ï¬[TÀr-#€í¥` +ƒ‚)ÍÖ¥XK­ÇóâwP]Œ¸êH&GnA4©³¿òŠµ|ÕF¬ž¡ü$z^pIòƒ9£ùsîiP)0Åí²jJ”‚.yàsÑäOˆ/ ¤xÎØ@ÕŽÒ8ìç;ª,n +¼Ÿ*fSH¦•ŒuXCFö“ªYß* +SÅ`D¹oDu¾ÎI%Qaœ§4ÅKd=ÑDQA¤l5¸3‚!’pÐW<½ìÒêdõÑÁ>÷Pµs½L"Dù…W“Ÿ·­ÆZ¦ç9¶ù ë`k¥ÛÇ.Øέ‚q© ÑgV)MoYôÉu¢Q)Ó»I +@G š8(ê{ +añì¤çÀmNø±Í­.1÷hF4tj'l>Ì°_w»að)–[µòËþ5ú®¶ðIQ‡R¼ÎXäÕU¶1'•e¤0cÜj¢„¤ä¸#ʪ¨‰ é³mó¶ à„ŒP?~"Ù_×±¿ß!œ;j¨z\û4BazÙŽHÇfŠðuí)ê®’©D˜Á­aY^zˆõ¬„¨H%c½(—UÍNü”®hÅ)Z@"h`Nø<ïËÚÏ“'¡8ðÂ@Î#ä VК:m£ ÂcRIR~#i@¹HM!w:LBâˆêyù"ßÉ#½oGI¯n7¹^U&D=v„©:%E©Mâ@,ï¹P,¾Ú¢fY~ô™œûBù4sœ• ¡ü}îKœ2·Í.ˆA½3Wú2ó®(Ýq+ÃùsÆäë’Ûv lž™fæ@TØØf˜KnœHèàéæ‘<‚G빜W#Ñ:p§“ÑÖTŸÕ‰ìŠùI»g5th‹„U˜?ré¨Ýíö-YEcU[‹ybN”k–m•™±oBC–ãJW<; ¨±mË´µ»#KÀtù…T™[ø¤ÚHypc§˜qon^F;òqìeýp%ÍØ\rÇÔ÷hg(–ÈŸÙ†ão½=ƒøÊ–r½ö%ú]÷ÖwÀªsnáN(¦]‡ˆµôl}aA«å×·vÁš'FýÚ÷:}¬[fQ¹]§.hœ7l¸J$>æòM¨ËÝzˆk.ÅÙÚ/k2׃M| +4öÅZDŽ•´Òò®áÌeiÂØ=fv/ÎëÑÄ]g(K{°CÑK`í§c|¤Ö>šÝ ++ ([3bv‚þ׈k/ÉcwTÕf¶@EgÂõ&26IèÈ‘;4•;çÿ©?ž„\™® HÖ™äÒÔzà“óܪ±S–õ-/¨±AÇbi'ö¹ÃW‰æÜŠˆKâ³*nŽs]0…£—”Âf•'4ãNùò±æ¡\ÂŒ‚0é]9˜}5͈ÆF/8«m‰ÈÞ(¿ÏÁ[Üþ•Œ9©VÇ•mš 6PÊkÒ(H6»|Aeþ™0ØoºØµ8ú*¶(eÛxo*5 šxë¤M˜”ºî”º]›Pz…PÚZçú>™[¨±Á+¦ç 7vÞ¹!ŸªIk&à:·@jC0BÇùÌÆ}sËAÖÖ—?Zûb[Îkº^4_]¦Jh[4œxñÝPÉW5)ê…0£g›ýcÛHIRnïªëœWjs +ïà—|¦xæžDh;5ãmýÎë—Ê&Ù¢Šà^[Ôã„@Â$ÅoMÚ¤1ðs{ÝeæµnR2Ö4zõ€ÕöFdô–G:76À1v~b¼Ùc_îÂI‚ŽùaKá#agŒÞH}<2JX±™šjžü]´+‚§ˆ½²ÅÓucêd5åbøyí7D©;NÖvcDo\²\ÁõPðQ‚b®x¹2„i¾‹”÷g)î õwoŠ‰•õbSº)6<ÖŠJ¥æüJ½—±wžÞöN!—ñ +ÒmlºLtº}Ç£j…õ°ðO_i¾mCúféYÔ"úœŸ}̵ ö¶·]Ð8XTg½¦œ¨3VF×b¶ÄÿLS’?¯ $²®ã´iòÁžÒ8*ØÕº‚-dvÍ€“oi›\i¬5¬¦ËÄÆw®Ø4ô‰ÅX¨ïZ±JÈkzeœD©+CYMÕʹ-&×o_ïô<ÙêØÅ“ÍFñ¤Ú +P}ÐUHeC^?zÑ×on^„W…ÐÐp¸ˆ,Ð4$3k]+‚W0HBmpr¡g¶­ Û1CwVí±Æ÷›õ¿Q’áEI¹O½ +ÂÏr†+uѽz8-S¦1:yG|GEâåP¡p/Ê‹ûnñÆw0y•4ÜÅLQ=RWÔ¯¨àR¿BM Õ§ë!™n©»ëx*úšJz¥ð>‡®†.Ç 5«t +IÑIS”µjmŠ÷i]f½sÇs«Ä¦5Ã|Ð9ÜĈ`^2uØ(. ©ž>ž:¬ÜG\†"Aü(¶ï4gA_Ñœ'»"‡à¥¢Ô_L8wÚ l+&V`QlüV%ú³1ƒ»7Ó–hèNì‡s?&Ô™ò®Ü¤ÙqŒwɃj' ;ÒÑ(¼*U"ú< ºN!V?«£ Œ>vô=}²Mi³( ]BÃhjÍ0óe¿5:ìߟ]ÄÏç*÷ë2YHØ®\m“»ÏMînÁÈ+ü u´n€ä_Q!@H2Ê÷¾Ú«D°ýÞêØÁO Ñ<£³Eóßvæ±÷ÔUŸÉÛA˜ÔJ†—‹I¾ •Õ3”åR³Sã¾Ð–Ò­-º")ÀŽ`ç±ïÌPh„¬'œýœg¸XĽ Ræ®rLÀf¸zhêµGØd`×EfÊ΃wZY–Ò%ÎÍ?ÀnT-ŒI,Ø@]:«†æÏúgÒƒ½Z,9)l\ö» +øNVma' ™M–e«K7:°›psŠ*ªdL^O&‡ dÇ-4Ž-ôŸ;C}Vìy%Ø/rYw¸~[÷Â-*‰Ñ½l³þs7³DopþqhàŠ6è)}íäÜU·Á›ÝB¦à½±OÃD0žmæJŸŽ_€#ý$›ð{©ÉR&¢&Ë<«l7ž£½>Ç̈ÉFC‹€{“&hqÝ).º/nkóÓö[Aî +R½mº ò£Ùê¯ùQ$R@œÅC ¢KÔ‘Emæb/ý +òèÝ| ŽMôÉl]i à9ïÔÁP gÎãV’n;¨e@Ôàà†#06¹Øüü²Û0æU}Rl@Õœ‚ò§,5UœÚV²ÜËô؉ù“@Øâ’ÝÍ¥£ÐоÀÞè/¶†̦§ýô#uïF\¢—¹I^©´ø†žŠÉsë€\M—ÞhRp¹þU@Õcj«,lv&jl¼Æ&°†ƒo¿Ä,Q0keS¬¹ÒºÁ„MïP  @ûC¹‚7XsÚTž†› ªI[‘òd +[«uUgÀ¼¤h°{Ѫ‰n®ž,·­ŸŽëæ”coÉÎ-ÚÈ|¹¶\°Æ€—[=`©¦ŠïæCËrßD^^Wõ¹®/Î;Õŵ•›Û؆W4‰3¤ªq|ƒÈcRF'˜õŸ÷x Mc¥Äc ÷3 ²;~ʹ±%èâÈ%<¶’&*bá›S™T|J|PñÏK@Þæð  +WóaŽš¦„éÂV¯0×ðò·¢Æk<(]æ³tì+#î¶Æ÷ñ<t…÷¯Iଶø%káöÙº’8kõxoŘò´¤,`?o¾ãà™AX‹;ñJÚWraeŒâ–ÿk#Ô&›Ø0)¢{oÎ~ç·xª–ýv9Ó²鈘½[HX\‡E=Ù¬¼¾Øß@‡¬D„­»ÒµGÁŸlˆ¶)3ÑxDEM?,T·úþž¼k d(q%ŸµHƒ/piêŽìà ž‰º¿­YBD^÷Pâ²!|—ôÑHé²· ß”iÁ{È@Ì/ ãjýDA‘}!ð Àz‚! H * ›U¾‰^¢î-åѹ}¦þá´Ó¤baU‚‚Yí‡õGu~ ýC^¾c +à÷dDR‚o¶"T ¢ååçç½j—KbvF1˜‰ZOï¯þŸpÛ,ÈÑ’»·ªÇú~Ç#íÁÂwiÆÅЈ*U&¶Ûª’õ½Ú[,¤ÿ/ц-dl¨d9‚wö…_³ýñh°o×5'[.Z×8]>×DbT¡ƒº;¡Xÿ‘¿å<%½0Xz}ý&ïω÷ûÿY[ûå‰>üâóßâ¿sn—¨·†>¼s¢‘Ì‚±XÓûÚ¬nãB˜zÎDT …:¿EÉÃs¾bLFê07rzþ†a¨²ëAûìÖt¶*Üã=¤J*÷hç=sIb^ç&`[…Ù££+-œÂK÷bE5kÈžÌeTŠ;¹èôXÈ»tçï,ÿ1ñÒA8)Ýz˜ç€&3S¤«DŒC¼ÊíZ¥0«¯ó#A”ûcîï¿F¹$sÞ½Ó¬ˆa‚7vRèù*¢"ç–Ö˜8>«-Í<˜¥kKZ6nÖɬ³S&Mº£­°.€ëTI—ÚE#¥µv±x[B3½>#LJ²q:îÁ*¼ŸÆ +'f›-pUæH2àN>`VƒÁeuc•>‚¨æ‰ ã_©WGÀ¨‹ù¥«=lØÓ¾ë +—³%, ýu ßÌ ê>$aÌϧ4¸F†äsðÂá”­Ç„˜$Ÿ´+tÇ-W(Ï®íy Ñ@Ðá¨èíÈ ªæÓƒÒ÷Ù,µèÕà!N#Ø™át&"páiàëûSÃã5¥goPD;|ýŽÛÖ8ìÿQe$Ù/F\þ´µsÖc’ AdàBO®3@)ÈÞH‰ßçFTƒ9)qžÔqo®æ‡ÜÛÖ¿9À¹œ#©Co´Û7Qu«¾3¬J+ò`DŽä¦UÂÁ¸m$j' ˜9mGlnK¯z }ë0­(†®¯0 ºÃ±‹†, yògÄ·Cõ#ª‚xÕéfía þCíU‘ÈÝÀÔ +»NÁ—¢Ó$¶øy¿z܃n«I;3w®é½ð¢5÷xJÍ­òoIk½Þ×¹¡FAú§ }Œ­jµE!NJëFœ4¼(y«K~*uÏ_¬o7šãy“ô7*ž÷¬T« ÜNŸÅ¬ËÓC"b£熇2_¬M/"[8et’ó¶‡ú´Ï†Ð­ÁWëHÇô•a<ò2O]yXÔµ P£)™\È™È%zH*¤ûƒn­LM ’=SòV¿ÇƒB +l2ÑJ\ê#¬¢ »ì}:­4ÂÇVTµ×weH*¬2qÈœGšý‘Jø3°1Óܼ—[^DƒUv: %V¶ÁV Ñ«á—P¥´g¾ ¢ì.²79ïI™0Z¿ö5+¤¾äã²–F;ækv”0MIÍ[ópÀ«NÌ”\bÆ"d}€ÞQϹ‡ŽV“2¸v-fìH@+gÙ·àòZ}K¡ÃÆ€ÂûX¯8ÍŠ’¤mÔ‘«å„fÚvLØÊ@MK» ÀGSg"4ªØS=€¦;ªk2“0!}¡Üa­D +…aÏÓ•Ý¡33L9m\-›vq;ˆáÜ/c«‘Y‰~ ÜAÚY!!‡C·¦š£¾VØ?øñvk¿IÉ„Z­®Ž]¦²Ñ4-0€T¯5ÕR«ý¼²«qÀ±¨ÆÙ}B#)›Cù÷0v¥XÖ«oŽ …·p +¸åiå|Dm‚e6Ác˜ö©'‚©)uOQN1‹w:-àTêgÄïw'DÙ™5kv™>ϳ±—<ô+ß…t1(m¬g ÆQf}=ìí |DAݶ¹pø^{ëµ;Óæ †1Tk&æiJ¤€ðÚ›iâ´Ó¶ñ¹ÐŒ !LøóNÔu6Q/UшtÑÖSçaètªF²o¹«øÌÕD•ö¸‘í«D­Iñ¸‡õ=5}¥; +p_DËT~ø¹ÑoÔÑÁ‡0^”ðG qç²GëÉî0- >á–¥»µ[†ëÖí*kDî!òi=ü±©¦Õuè™ Þ£ Î üiÔ rqMÎ6à„–—v ÅZ>ÿž¼Í­ ÷ç¬ß|°ƒ¥+á²—B»^ÚR@ŠûR÷ûŒbO(“¶£ +Ðø(kz¦äÈcFÍUÚNy´ê#A8Ïñrü­¡›!h£€ FžõVòeÁCmîÑ€ðJjf,?Q¯–:_nvûJlÏÐ/l.7à7¨ª”vÐ`qI;·ÖóÍ.ßY „kH }§‘ p½•Õµ/J+¥zV¢=õ¨icÓ:½RõSðó¶dGAG¸ö6íÃê,ßi³,œ ,K­¬9Ívi|ÅÚ?Ì°)©éëSŠêªÀ¿6PøÊéå‰nøÅsúqE‚½p«iÕÞq•öæn”­½¤Ë‹(?¬½u¾u ÈH=ôç[£Öñ°.( ²'†`“ךÓAè´Zͼƒ" #úsnØá(ß³²‘jc°â•ÓâÀ&ÞSÒ½, ËÔm<â,H8_Ûª¶ÅªÖYéûÍ°`f¼}Uní ÒQŠ¾Q"†ÑÕvê…ðeË~8º¢Í• +öv¶*½"ÎÈÀ\I…¹’øºhˆPPçá"×m«ŽèëA&dÜ{€;U­}ÿõ~Lׯ"W{ê3„šu@Eõœ·|Ó™òÏm$q¥‡U{ +îg’˺¸Üö\[¡½#JojÕŸ +ái*A—+örCŠûb ün{mÛÒ¦"¾_Ü $©ÙBMÏÍ Õ-!Z|·aNTç#ÎjgÎÝzJ¨É¥ì<{Â+„~Û±ôîÔ ¦÷ܳȪø‘î^Ù¤ç+Ýi»Ek³öu%èœDÕý}2mÑã¾7yKtDÑJÝ‹—x·»=JeJñ÷tïWÚ;žóן‚7I Q€¶8¾†­ x\,^3M×3^ÙÏÄ«’ŽIzè¨FÍ5ëæQ]<ä-Ñ¢þÒÙ›JÞÀl#×ìA^¸>tþ=ÝþupðÔ¼¾>#ƒ“0Æ âw!¶±Û¬[wí<ã¡¥ANÝ9÷ü²GÔÍ‹ìöL…FÔl¼Be²ÑÒ@hVÏé46’-LqI ,ÂÀ/ßüú6‚§Áª’žJ§YÌÔ‡U¬e‘ûƒlGœ;â~Ïž¹âí Ðgi Ôè¯Øj0‡Ø"—,FÇÚÓ¢ƒ¨²¸²SƒvÏq숧\£ÜÍаcTÁê I¯D Çã’¿ œ` +¬>ÊHªªŸ9^¥‡_éø1ãøqî¡sF“×èj¢¢gRúL›‹ížÝGÄ3²¦†9ëöÍï¢è\ËÄi$ˆšïÙa=ÓEò~L±òí¾:áF­;òø£ ¼“ágC¡3E6Ggû½=®d*þ™z: mf· 8ÕÿÕ_·MÛN÷nFÅœO†JXÈ+bmWf"Ø’!4‘Ž«cùZÙz¥àÎOÄ\¿:ùšž< ™Mí +ªK¬ +PïcTŠ/ô xÌ6ÞûW¹ÀŒ×Ø:ý4‡'j^;Jþ÷F¨àG|QöŒäÞCŒógD WtÌ£ëõ2ªm†D‹Š”öd£´i\;EYCY§ö±r2&°’n÷g„‚$×0¤Õçém[?’§]ó8Íó›Zü¯#Â.Ü)Ôor¥·¨j‹Ÿ´Þ5\ÆöEWùÍêæ.é Σ%¢}¤7 ‰ ÝßÞ"ž74ÖmÈ™Ý÷7ç™w¤k«d¬Š”!ìßE¸‰*1¦¹jù*¬Þák"uïŒ1±¹6×S6»lž­Z€¼/:áxœ¢øÓì¨}‘{׃>Â,W÷¶ó<¢0hk;ãÒib-]-º ¬—jèKUâçQð_Î-î™Úö$Õ$qëèµ} ‚Ë_ì!\êH]ÝO¼<ãñ1âñq¼š#~téß3$"J`¨h <¸ûϨž w­©›trÑû3E"ìa‘çå— Î/¹]2Ñ9•Z]ÙÙKWsd^ç ^oÕvdä1"ƒ0~ëèv­ØÔØ,5D<òÊ–ÀJXŠQ <Èd¶Š%öÂK¯ðÎxòWU¹’u]ôSoßõ ʵ#,‰¬Ä ²»VKœ «0¼Èg‰TêD!ÆÂãȦ»7+i|݈"¦7û’Êõóž†u,ü4®?4¸³ ™ÜmM”dš¯gÀéw\ÆÙî|üþéâ–öÁÎoNs_*ÇÈv–ÛYfÉψo×È÷(”Ø£ ÐQqØ}¥R6ê•ÇŽºíÄè6©mñc h~ÞÍŒ´/g)¤sK¾RÃyØóˆ‡Ážïú¯Ÿ‚#k¥¿^cø| yXª9ÆŽQ¶)J×­ýÂמÚï>ÜUèf”f`“õG=ò—Ô¡ˆÚ•$gï·3e´î`æÝû ã‹û1 +×7 Qãl†$÷¦Çì“’QmGíŸóŠjZOÕ ¿Ö{GÌ¡Iȱ‰MŸA.×ØŸ>Õ7²ÆðœFj<%:N›eiè\¾œVÒnIjþNïÇzq³Dè#ŒÉÆø²j´Y™ÙFš÷ã²X¼'#ØC …õFʬÁȇ¾YÌq³Fd7ëë9ÿ£ôM—òÙéî7 Qåí1§Eÿe¶«ÖkìTÕòÏÝ߃.}ö­ôf ¿í +ÜôÌÅpþÅŒ ÎÄŸfE =B¡ b=´"Øììó„Í~šÜÐ R×–|ÎëÌ| "vÆHqáŠÚ”Þ"ßD¨çDb@«ÖóŠ3˜+JÕ•ÊjõTrÏcÎãêg1¬zg ÿykiCÐŽ§-=ŒÆdÇ”e’ám£y…hê: Ç5ü8e¤®{ +SæsË}Z·œt±¾½c”Vç«UsÄ¿Á(%èv¡Ô²IoÑZo´=¢ÌÜ#ëv ‡ßT#Ü)”`! ò;UŠ@Âw> ‘܆j£â‘ˆ¿Ñ™iÛq‡š©àhÒ+ÕŽhn—Í-aQ"+£ÌÀÎ<`Óõ½Ç¥£•tË=n£P#&kÞJ<›W  +#,ÊÁª@žé—=YÛr‘‘ÖñöeŽ­úÏw@܆š9ß‘ŠqAçûÊÙ·¦7êÐÕ]9Õ»7"º5€Õç#_CÃÆþøh‘Z£ó‚ôU•¨í‰/Þ‡zI V²o +3žf(B€Ï@ŽÈ>5ù{q¯V”Š*ÁÌ2çÆÑhÅw°Ë™—Cø‘’+ìa&”_œ/ËaÇ©M7_ÉŠPv蹕{T%WšÒTow¼¹En®Ä&V6D¬P`û»XáÂ}欹Rp¶ +V•×Ó‘³nñ©°3Sàšz»æ™Œ?îmYó€\‡±mÚ(èü@DX ÌW·n*·1®^íÛóÀfµ^Hob?nuÖ Êy-üôyï%y›ÂÖãù»gý¨°I“z¾>ƒ‘´½ ®Íòö„š›êöïX¨ëpÜƇ™ð5ÎÔ£°™&ýfœB-FÓŠ1–­h„—¯eZA;ë’©·x»­×Çñˆ¨­_ôÐöJ¬,QUŸÇSQWx¥[V3×<]< ê@»ŽqEz^=!%Åí–RÖÿ:ÿµë7ô²ÅhùxÄŽÖ°šW>Sŵ¯ ž\v†ÇV3.h¯õ׋a{½áêŠ:”a`Ð8¯T²»™Fˆ´¿‰8º>¿y£ßÊg¢t7´‹˜Ûb%Ø‚rѼAý_šõ´È +LH)ý3"«È¹Õš"QðÍyPó¡nˆšÕRäñìK~ (IF²†û7þÁ[Ô‡j6þC^4DÞKœÿÆ ³ ;·%ŠÊ9kÅäðñšB$î^x¸ßœ ++}Sɦ%Ô9A…èFí>»– ¹Û£z·×3Û@jæ U^Yø©&æFñ,Ï[¢+±’ê,9¯YÊTiĈ†¤1éOyMM=]˜²{eDé¼i‘÷Êy¶[Y(P#äí­eá(ù.z¾«ÚùVûS°{_‰vÚZ¼!Ôø|A™®G3ß,Âëç¥ u¿0ó|V‰‘4Ù¨»ì¦ù™¦9cÖ6èTÌÐüŽ·Å$¢#ÎtmÐGrè|×gDÆÔ–qÄ,¤ÌþÍy`‡‚¥ÄñÏɼ¶­Š¼ß°›2usQ;¨Š¹y¤¸ò”86žVÐ;ã—Ú¦n`îÐ@:R Mhí@u¤»QK{Iˆ,…*®õ㺠¥c˜0 _G´½•€¡Ñuï¾ï*0áðà%(Ìm1… 2¯Áº‡€¿x¯Kûæ#DS€ƒ²ðõÍy(uË—&!Vqœš[û:€à =ì[ØPÎûdwy>ƒ@DŒÙ5¹XAèþ©¯¸Ö¬à~Pf¢c8†®p8´ChɤÒ÷óÏ©î…ÂΉ"_ï–ØÞŽ{Jõñ›êUÿømØÝŸä'ÛþŸeÜÿ+OvàQ?~I©÷þfýÃùã/þÍÿö_ÿù‹•ÿößÿñÿóÇ_ü»¿ü/ý§?ýí?þýïþòýî?ýõßýýï~þã?üŸßýñüî?üÍßýé?þãÿ÷?üê¢÷ëÌ„ÿÿÌÿËI»¦h–ÁL§ÙÓz‘×Í<«ºbnÜL,¤Î&üÄõò SçDÙBxå²KË™_ÿÆkȧÖ7qp;‚¹¾•˜àij|¡DÃbñ„ÜrvÄçAõÖÔèoA³ó~b¯Œ²™€ ¨;d{{—8ME¹Ô¦Ý$ûNE–ö¼ `‡÷´HÙc×JH 2âš)ÄWF­¢Úk2ä@¿s@-ýº©R~¢ï&I’*.`)pýI ëÚÄpeµ£<Óæ8PÍ›íëÌÙ£8ÜàgøSwWT(Åúªy?ëÐÖä]F +RÔ´%'€wïÚm0ú= B9%•ÕwåN”×¢%)Š÷‡ÈlñH%“_%ÕÞ*¼Z´˜T¾ð²ˆ¶Æ[ù¡ª_­{{qÙÓ¹G¢&Q?ÔWYIʳŸ™¬ž!ž’‹P*>uŠ³ MKžžž·E¦BU ŽõP}0››9]ìç´€¸ç†¤ “@¢K9•àš/×J<Û–(Yâ(—d˜§žJ+uþ°mc›Íd›!ºÿ¤ª¿jJsã€ïk½÷×Z"¬,ýÓ_{j¾l“ˆŸÄ°E‚0¢Þ»âtô4Æø<¨/ô7±ôÔDzؽ”À*»žx©nØjš=?‘Z@ÝrØn¸Ÿj[ñÔ7Kg®# i«5ÚãÁà{Ìä~j[Žuãö;o–}ª=I§©éûðÄU›ªɪè,a÷Þþñ̯ÆâÂöˆÿ"XHreœöÞ†¬Ñ§ðÅ œ×{ +{ž:G=]ÛÉ:3¶=¨&=ÚÐõÇÄ«âˆ1i¼“NäCÆ/CX?†N±O÷¬ôK™„ê—ujÁXi@YÄQ ê‹Jmåó€? ‘ò9?â¢mQ Z®[ôΠ|ÀÜø¹A¯¿ õÞ¸ñRê[Ç_ùøÇJ±`²¤•Õ‰áÄ>‹úè1‰U ¾Ÿ6ù{ÛŠx÷‰WÓõ#:ÐñÓª³)³ïbö‰I_ÏÁ8¬!”µR¥“òmâƒÆŠvy¶¦v—¤kc´å½u:´É/š±ùS}L¡æîBâW¶¦ê×É°“·LžîuðŽüi·™é ºðü¤F”ÎÀÑuòšº‡sÀWñüIᕃì5ôEá@s6ˆ¢ãëïèUjb Gxƒˆ¿Bסú¿^¨}ô{Ô*‘ßÜ5Û]kCûq“…–(A5 z:8™mx­Ü™’ô~k0¬·äƨ ~󄔹㖗[‡ÇæÖ «ØMU¢ý ©Ø¯]¦IÚÊ+u€`I‘:%wôŠ©”VÝZõºÇlhßšWŠ^©*\€»Ãâ̺±ÞƵÓXSîŠ`÷ >7Oú°Ñ,)¬‹ÃBm¦i;%?ÚãLÖp"ª¢4l”2 IƒÈrÍÖsóóîWÈyLjp¢z"€Åvâ6=–(tÖox7çæjÖÕ¿mžç¢Q³"0Z2"ÆtJÛå»6ýå Ѻ¶£àâ})Tvíúµý(ú!®)k](k¨Õtª÷ß^­ŸLXs4ÒµÛkòæ`6Ú/¯)xÝ9ºè _¾šµóÞ²{ãXWän“±5z|ëg–[y±ø]þl™( +ªOFHc ,zíР³õ|q :µjT=T2ý•3.AðýKæ7d"KoH±7Î$ñÔ…kV7˜2iˆÈ¾k`/¹?ÌŒn"v!‚÷ßr0"ŠñæÈG¯ÿ÷•Ö¸¤y®ÐÏôêÁÝˈÑG½xCÖ‹ÕŽíÄŒ…,N̜ˢ4Sï¢Í«PV çsUB©m¯—<}b3Ñõ>#7“Fê=ãa©©õû’iéX¤õDÆ¿ù 'N2¨¶‡HBizê  ø36„¡Sõ¯ó÷;ë™›.:UÓž®àš7K•þ:ûgí)‰¢î礼zÂw¥Â¾F4¤uW“‘ë#¹”ßqè—| wÁ( ó5ÆPI÷[#¯—)(r"Ó)w§sTδÍó›ëjóõ ~}q—C²”ë€ol3z†³…èZDyËÏ«Â^~\ßD8M\T4l½¸Íþ<Ï$鋬¾–† +ÀJIÏÖ^Ì|îšÂùÞ°HNÜq‚Am:f€RUì±g‹‹d™›—Ž€f<%„Ý6º{l’L^‘.®*"~F¤ì@+ŠžõŠ+GýŒâ‰yæTÍFl¢É½±c=êü&âEúŒ®_G©¨ÿÕ©ÒM­UDÀS„¦¬Ku‰uäÒ©ô`£ECî#À_„”ºÿ#€Øϳ¬ôþt3@E]×î˜+ˆÚ&ßã#€~ÊP$öîá3J8†ïž[dÛ^q5cÙÕ׃j*$E(+a]ZÆgÀ3ÚN¶ùk´A%üæ4—¢ô —‹‡«Þ­ŠZEÀêŽ(sò k2‚ I›Ÿ7cK&Å$DÔqFÝ!H·f#¥¬ ¤ïÈ*è΋w!»ÈK#–“ˆ~lU€€”Q>ZKûŽ %h¼!I;ï÷ã¹UÛ`v»¼„õãx'B†ÒÄfû#w×5âKÿ6b¦AÒ+jû‘|D¡®)¬j ¬ûÌ·½­ï4¨ Æ·ÆÐÖÄC© >X½˜˜|S¬è1£S‡ÛFØ¿ŽHòÐ’<<ôϨI¹; ¥ùàZã¬Ý¸‡¢­J2DBÝŽÁãDT—›ÍôKfæØù½GgôsCl+ýOjzêi»&ÊD¨$ò­\j.Q½‹Ó*Fòíu碆óëÜ5&4p÷yÆÔ„<°d­­)² G[¯ç¡p7Ö÷‡ªa¥ŽñÒx[ì~dÄË•Ç'g6ŽW&a¦E6Oà9²ªTÊÊÖç›'ÔÖr‡íu®ôUbL7„åe† ‰§eóüdi@.òÙÝzí¿¿Æï9BWiö¡Ë~b‹rò¥ÜPK¦ÍòÜéöç¾íò§§L¸¦²•'‹¼Õ7H‹+óíгXBû”քθOáLX˜y5ÕCÀ^ ¯/ð׿OÛ$ï¿ú90Q©eÛ*ù8Oüg¥ÌrtDpKzú™ý»ˆïÖ¿ U x(°Óuehšü}n¦ç>·Q&þMLÇ]³º¾ìÀšK»6î3rbt·¼w´ÆSæ^Û‘µ±¿%Ô°F­ÓÐû„Mq‚’c3ÑQ¨$¹¥b!œÖêã*¦S8Ɉ§-`Žv†zúÚ'˜»d@ÍŠmD¼‹J;¯¶2µ¸:q—éþõ)ˆ¥QtÛnaù¶ð’´6ˆuï:ó½óo +>ÀX> ¶òÕžy»ˆÏþ 0ULŸÓµ(LÔØ­÷â]àS:†{ Ôº†`• ØiVqÄôB 9O×¹ö5‰}¸ +¯,Nj*Š(^ü{ÓNÉÜÞëÞ=WhkºÿaÓ³F(îÞ~] ‡Ç…:×E‡4¾ºWh׳2—¯józ׃K×Af>0²T©üû©î^8UnÔ†§þ×£xxº›0NµÂFýë‡>òçhlß'çžÌ¶+–S ]raëê4huà½ì^”bܺ¹l”ƒ`Lß²Íó /Z“jÓ/yÖš†ÖcІ,.‚ïh–6õ6'0$\.£BB ¥'—¯W€Î2§6ÑéupRxÂ8ñ@Wñ‡ü`±=gªé87Ñh)TPŽ *òè+½¹(’¥€J£©[twGßÓb\z°{8ÀzžX(¢ä4ÁôȃZn&l Ð +/Âo"µ&›óI‰±¢]³ +QövˆŠFÖÛjWס­ò©âà.I«aÙ,jò"¯ç|vÆ-hE  ÷ºÒ i1«’|ŸâÇO½ïp$»üwWOÃv[ˆ BÃÝI*f®kÞ€?tÎÞ;ò.Ü€ b~X9-eÒƒ£”SÙÛÁÖ #9#n2ª’ ûJvL¶ñ³êç`ˆD£{«‚Ø`5" r«–škÕ)¿4ë•Z/Ĭ b ÔôËÇÄE››ù¨[¶ +¿nþâàvµ×VF7wQðupÜñ¥h˜e•8¢Ä9Íσ¬º+î(RÓ)Cð³ >teuo.±L önXô­ -ûÃ(ûàï{ªû} º6!%ïB f98ÈȯÇÏ…) Xí£fs1fôÖÌÀL2ýûßÝA½tªKt@T‹%aY?óëïgà±ue瞸_AÄÃ=OI Dgß” —eNÞêf;g äOÍ’…6üM€<‚°ÕŸ±&ùeqåG°¹öïEy™þð#Ô5yÎ6}Ü@rHÿpðO1.§"YÏìsän=µ¹“>ËÌó8ª¿²ª@¹³lSè¬_XºÖ_@Û¤=p»ü'¸þ&j5¢÷—€ž;„Ž}÷g·ßˆ ÉŒx;ˆ+IŽØÍW4Kö¾|ñÜÕ•2’ì;¢JLÿ*—Z t¦L»|…VÐz¨N¸ Ú "ë ò׈‰NI;»G´¸4M»~\W á¢ƒ,ln¶Ïì؇,!›„Ø4Œqœßœ§_”м8ÄdS×@Þbý\x…ŸïËGªïQØ‹–FƼ˜§tÓ‘¶… +õä:$: +ÛI›ºx”g¶† é€À΢ó„ +ES>ÓUŠ¢kŒ•m ü¹­w¶ßÈ W>î(ñ’§H +jðiö›Âà˜‚W2Òd*ͱŸžb:AÌà)kŸÒE%ä÷µw•ïôßw’œœ*1M\‰²¾5´Þ0¤Œ¾6¼$¯À‘´6ôxozT¤˜x­Ëó3"õð#[q€Ôâ(>ΣÃ1ŸtwŒcûå®wéªýf7LZ͉÷#¨G(‚(—®]Ýj°l3 ‚‹›Ð;0îè¦:믯J›c[+c1Ø¢_oaªlU´Jµ½Ù<"ÆjPC‹W3Œ +­ªß„o=@/yš¿ 7J6ü%«°Dwñ»†€ *mÓ¡o÷ .à;J<Ç éu+ÿĥʾÃÂ6H0å7œgm…É‘7® «ÿ?ko·ªË’¥ç]îaÊ:ØÎŒÿ<´7ÆÚØøÄÖQÓ”Ú °ÔBn7øîÏóFÎUµ¾ÙU…­Þ+ÇÌÌ/32bÄïÏÞRõ*G–›Š7…Å HP¢¥±9ø㽸¥%€’›žØ¬r¤¸£DÓºæswXôÝN½»Ñ¢ˆß¼Î(÷,ýŠ˜ÍÓ2míÍË…Ù¶ÍDÜhÆXEeÿÄ[`ÿ„ìl:#ÓˆQ)Ÿ?ÇcÎÖõóJ4ˆªåÈ^áÍÛ\y#Nƒ¦`ÆýM+š2›À(5ä—(‡Þå‡s~Š&&pºhšâÌñ¶©÷ ØŽqHWª ‘&É’8Ib9ëXuÆoÇhÓf "T4Ç“5Cb3BãÚ5Q†c®+Rö”F°ý¹h ÷ylÐ*qfÖ®]ퟣ¨~@YêLp ú@Nxj#ö‚€n¼ÇÕ\P¬†c;K_ÞÂ)UT€!ÊR·™REÁÓx@Æ=1ûä ¨õÏyï7Í1 +øhÙrõØóˆ=ÉýYÁ让ãCÄT5øê)æŠvâ*±L!Ê•ÞìE,%NÑ`)çI³µ¹ÄôgÍç°Ð§íõK9?XHO`Fø5²dCFGEªü¦wÿ¨ì;).ºBñBÓi¦µG‡˜Á‹·Y¡->àT!²L ÿé•‘öqeQõjjtm®‰…Q¤b¦µ*@Ku +;˜²-Dø °õbJö¿Î7e/ãÝŠyÌÍJ×Zqo¡®¼Û§ÓÒÊÌu¿>ª×ÐSéÇùõ«_ïûÐþíj(,µ¹@RAkû™ƒË4‚º%ºôóàoüÜÀ¹#IèéF²9±ê×ôN%%»Tæ^¡ú²j_5Wbû&àVbtEü’¹0_”dÎýbd†ÉŠ__Wz2¼Ø~MŸC¦oJ(¢Z£°¶£ÞrÍO¢wxiçø$Q üCÀð±èô¼ðZZ÷šxX\‹; Búlo‘ÉÛ¿s¤ßTc{¬ô)ù‚&|š—-Š ¯ZúÈjEÅ¥](‹ïšW¢ Í•$W¤ÞèÀx^Ç€˜% ìmh†³ýÄGqé­l¬×¡–º`YÀ0eWS#œuB!+xuš½&ÝcEÝçÂñ>³éC³Z}?•eX¡¶cÿý¦¤ÓUèZZ*™…I/õn›°t¬Ý]˜¤U£t(‘¿Z@É0vå`!öþžÿ#Upuïí_]oÿ– 7rçÏB™…^¶dÌô,êÊsòeDI¡Nî_¾'tÓÓ yk$"ö¼l–Æ,㮧{àÞ1QšM%dòßðCŠÜÕû°*©Þìñà 5ç9 Ž%¤­ÃÛwê)ç ÷°‚«çmÎ’nŒ7¡Î¨2ëÇÄ’ûu¾#¦ Sqj¶]©BøºÏ¾~ëå×LËl£ó‹ ËÚxZe„ /„®Ô‡òf×p¶H¼XtÛoÍì’¯I\e_ö%˜!‡‚>°?`§¢`–÷¿k†yÅT}8€ãTuEpöUÌ`—´Â@uˆs0$ Ö[$;1Q¿= ž(ŠÜؽì¾gƒVÅ[¼ÚÉ¿0ÑB;ïv +S˜ý'LaØ@éÁûÝÔ–YÅ|¡¨±¢ÂNG‡Š[a»û p3hšÔ ƒ÷6£Ò*ÕÖ8)ÿ{/º‹Î㈘_ª,׫pSx¾Yie9ú.eÉK';’÷òÖþéÙ+¤L¦,&TS02{‹â’«—”ôAG :ž"ÝìÖl¤Ã®];C—“달£÷HEf=|Ë¿€´»é.>€¯v/”‡¯`qG“«< d±‹Ö­=¬1™=yö÷ÑPµs¶Çý¥nÇΊ˜-©ÂYØçþ÷¸­©)¦i 0°G}9ŸÃ gU"5¦#{5£ÐD½Dó*r(œïA‚Œ 5Q[Ðbhµ¿ü)…¨° e‹ýå>¸PRsÛX×^ŸÐžWh¢Â,t´ßÙ)—ÑÓsªW.‡-3]™êb3#ßzü–œú€@ë ƒê_Ï.r–Q>Ža[)R[îé¸Ã==Æ1ctÕÞÚ:;tÍ.­X\gb¥±‹òåíwÛ%þx>qo~^16€l„Xɸ/Ä*¯*¬¹cLIyŠÝÓJ‰“O~•#«Bâ̧È"Ï}x’W̓g)©©ï,¶w úŠyó©2DX_EH»—(Beãt±&pÎL¾Ó°ÅåýÙ£Z³JÔs•c@*WPf$·»Ü·¯íÇ öä|”TúLT[Fh¶©œWßo~ÁL¬¶‰@ݽÆ0BüEëì}[0n¯£q†=X9*ªI`¦8„]I×ß³*:¬"û6ê¥.Q~nQ¤»ÎªxA«)¦Õ5ôäà:’p&]yáˆ-éX¥-·ç'¨c× +ý½ÑΩh.ŸÏ o¨–Øw|[¨udãÙE7Z¼ÄÐhJ{¢p‰Ö<Ê™íKp¦i”›.g!Ø|VÉ ÕL°›ØÛÁîoï§C"öQ«îÊP?Ħ-‡BsW¡ b/¡B÷*!ô§µÔ¤.Ó|?ezø먲ä³íñYép_kÅe¢íw'l¾ƒÜò™–8]žà±¨ Y¨÷´óÑéd™…õvÁ7ÌþØ2ÄCÊ|õ(**;P5RµÁ­kÒjf€£XžB~¥ûWb7ª½×Cj9òÂèA@ƒä1‚•Špõ)f5*0u?/NpÙëT Iõ'KgfÎ*b¦ pê-Œc‰B—ðé€ù”i6ÇVV.F^èãÌ¢ÅGmQÞ0édÉÙ3¶ž÷3Ú8¹‘-wc©:yÁ³¤¾ÞÇí~¾e?¯¸üì Tk]gK9¢À˦)¢§ ÜIJhÆ_NQì–ï'>¢ þåÔ „iï;`J iªCÙ¿š(í}øû#mWÛ‘<½áóCÊù©h$i`‘ƒ{¨.py ßEé—m»kœó¼¨ZÑ$ñb÷ŸZI„Z Q‹Ÿ +±£Æl-½V3qé–6ê¾Îc„ìQLnò÷½#Xs~WÑEª6€€#¨ÆÍl0l¹ªý¢ßhª;1Uít©£šKˆÍÙúkƒ}ê>ˆ*E%å:‘a{øTñª²AÇrÕOl$;¤åÊmEÿÐTö=°dæÛi–¬q¾C–ÊT°§V‘ûw²r¨À•-ý ‹ºbpWÚZõߦíZ˜”Ô£"*I¢d“7@iºÆ—ö¦õ8fT +ö¹@§ãëÚ­Êïÿï9He@€ ìÌÎÓšuÔì˜Ù Æ/ã;Ô>’¬Lû‰Îh= 5®†É`*0ª›Ñò¹¾d@î£éúæˆx!3…£ÈErG€¸-0W”ÙS€Âøï½ÝbtÙØ·¬³d¯€òºN ª‘EæB¶ Ø%Œ°’ÕÔj +.DøÏz áá0rý®A2¦'oÏ\À!2û„ŠMÐ×@·@¤;OûJ•6€Ã97n{tÞ®»+òû§tÔᜩßè`~+Πl$ëï©‘K¢L÷˜T ŽÙ"Ø H³GÖ¨Ç: Ü­øï_<z$b@¯“t3‘2jÒæéåº]Oãý ïh (¯Á¸ª9Ż߂ª…íƲOù‘BD×›D°k1ÄÀÁˆO:å_òg¨X0”cPý¼LA’´ÁGt#Ë v{#U÷#U¸jÐ{Ó÷ƒR10(y+™žèøÝÞš¥/T®ñNs÷K1"±!êz«“šðx%5c"?å6V„AV¦=¬ˆ>ˆ¨ƒ‘‚AwãíÈ>ªõ`âFÕýU?—PÆ+’¬·ÂÐÕüY O xøÆ'«ž3¢\¶_u ð¡¥ùRiIýoÌéšÆÍ£>Èúƈ ‰¢“C¯·¬Ó£³*Ôyç±ôgÄ€|¦úZá,Ÿ_T‚D«B>çêÇd™f&xúBI5?…c8.E6º2 ˜wrŠ¥"ßl?— +Y0Åóõ÷qCæWfe=úøSïï„ûÝYeyîãu¼Q®(ä´ópüÆáøñA5 +ý‚0÷‡8 0¹VHéDöCßØŠN×HE\Ì3šµ¤½m]ÁW'ýT©¤ + 7Þ¢·0†¾ŽÂ$TÒ\Ur+LÝ'°†x_êzT÷.èÞ$”¦ì¼ÎsAngXŽ‰GFNÓŲñM²…E€ã ²›ž¬Kfu/ñ n»?<Ýêœè'–™ vR© ãÐ/š·Ì6v¯ùSбh}¹>}D…íÝív€m8k±–À‹÷ ç…8s•Ì0Ô„.XqEŠöI±ÉEe9>¬]{n’hÿæPÝUlçŒÖ]MDF8oÊeî¹c¯Š#53uÍáD»ñƒÖdmäRÜéçj¾RÔŒPï-Ü‹ÅФ +ÿyjˆ£óA±C®ù°µ¡ôµ¤ ‡¨âL\Ï.ÃT3z…Pd¯‘Áˆw\5Wª®–Àó{®tõáž½W +ŽYê‘(K(C©þÛ%8ö¯N2xÏÓ›xŒÐÐŽ˜š9ãS£Dá~õ%omÛÙ/hôˆËȃçJ ‡„¥CX§;‚£›] ü´WD-ψvBdÜï6rï²®?Â)™¶Ðýœ#ÞµjÃÕáÑzŠÝã²NŠ¤ ø¬*Ò–ƒN†IÌ?Dy]FpëPÙ.X©®—s¡Â +ª+!hëi*ê¨[±Žµ›k1OýÔ @®à`^nˈǶ#õò@@² ˜ +e ®ˆU•žÓ¸‹¤¨ìÖé<,iÀP4ÑÁXìËJ­Ž ®ì‡Mµ÷vï~°+pkØ%c´ôuõêG@ãÓ†8ƒ“õþU{šÔŠR©=»vwç}¥¨ZvÜG Ô|\ ÁÌyâÌ„íV]sÿ­®Yô·¾±ü#œ®â‘Ìò=*˜ÔU,4Ž®õÅZξk\1¶êZ±~QT}T¦B^:«›v•å-›Ü+5¶•èО„ºPýë¯Ø3•ÑšÂu“™0ŸGrТÎÔ•e;‚tºHl+0×Wñc¼o/ÆÚYõ²J»tE®)b‹,Ó|Ìè_Rô{á–Ñ”uªß4S¨ûÅ;éëJB° +¾Ê3ÖKÔkO)L?¶ÍEr"Ñe2t•¨Z¶7]Ͷ[®‡Ò¶´­€þ~\0Ò%óî¢F E¶©#[ ÞÙ]ˆPœ Ï9né™Þ0.ìãÐ:)´Aëdd¿Ð]yÝp©½GâB*X¢zêCbÃ~,ôêÜ›F¶-fH‘X˜x¾HÛÙ' ¼'׈ ~|¹.è ÇŒOX!1'1®`.ûùíÈâ°Gá#Ô2êk¬ô)Ú½˜õv0«¶ú&›ÚnÙ4¢ÁèWÌØ•42µÏÐÁ¬e£j27¨œ,BÀŃ:^Ýö´O»UÆ„{#'VgÝ×1˜ôÂûœ×‰€.w›çŒ¯ ˆ£A²<&RK©ëPÞTåš×–ö½£u|Ñ=h3¢>Dè"nñ¥ŽÆ¾\ìê~·T ©n¡h#>ݪ¦r|áêŠZè퇋1Û‘çn=°”ò"òq›®e÷}­<_L1ØÀ;ÝÊ‚GRä éÐÜ­+æ>]F é±O¤Ñ9ÿuô²EǶPCi¶àOmúEWØ}*¾ÇkÚÏæ—¹0î´¶ÊEa:¦X´ ˜|È:DåÇö¢ñwÿ&4¥gñeÇ©žÌ‘ «Ø=®G=o/´(ê{… ÜLgb…q²ƒ½Íµæý¨:@M5 a¢5#x LrÕj¾&Ÿ¨‹Ü²Eíi€3Óíì9Î,û{þ¸c¦º³äZóÔ (X—fK2I1ËT—*¶¢¶|1W@ѤÜÔšU‚/œ¤Éæ¼ú©*›GìV¯kgK_‚»-^Aè +):ÙŽ[2ÄM[”Z²InxÇDbÔ"fÝKÉz"¤çŽom ¿—í P͇GïM,$ òØq…Š±ŸE0SË°.ʼn6åœi€ ª¿îF§>Ûèöt¶¬úï¥/t]ö…F%q­(–QMkñš°lc#HÐÿpv¾RFUãÊD+¨frŠ¨yA;Ÿ® +–"Tô#ˆSåÿ‰k8œ2^IUzíæõô•TÂU†ŽB„C˜CÑuB¦0“øC-Ú»‡ãuPØAa_'íb\+bË6 Ù™“ÕÏšž¶*ˆKQc¼ 4Ø£ÊI röÉ| ßpÔp‹oePíö­wya+…\ÎÞJý«È£¿%¼éßý«> å‰>Œ8ó¿Ûÿ}g‰)N…¢\™ÊG(@53öûØÁ•a¶ÙQ‹ÒõG”G¸9e"•jÕÏWtž[ª8Ù÷€Ô{7nZ~\² +ñ[t'1ñ@a`Ǭ0ës¯nü¨M‚ªuÈz6Ë£{ú,]Äz'×Ù|âa¯ &²€Õ¨a\rÑéyjD3!e ÌOìî¥=…Ú™•¾¾ÝÓ›ù£}]ȶ莢zøõOƒ6¹©w°2¬ËÑt”ŽP5÷FÄÈöt¸£!@[üÞªÀ:>KÈâ=G’”!ŠÓDwûÌÎ-`ÁÉ å/Zµ2$Ù/®—/‹´doðÝ˶ÓOuÝb%Q“üÀ~Ÿn’Šð$h”OŒÚÙŒép| ±àóÌ(›D‡uÿ˜N] rß϶—x½üp'oq/sæ™ô,`–üXU;:­˜õµö$¥ÙL?]cÑ®›´É»÷¨AG~æû÷™ßN®‹}È…Ç„róLSãëa¤ß”Ýsü’ õý¼Ü +¼Ê}¸=nL +-·’õ±ÅójNûTþølr1;¥W\ ¶Í¬™% Ã*ã¿x,ÔJKý4ùDȶéwôPtŒŸ?謗S4Àªn=-(žîŒsE£VérÔ®ÚYëQ'³©Ï·äó 'mÆV"/ÝŽ•ü‹{w£¼£f¹S‰·ñÙ”†ˆÎŸË-çé%¦Çè…’}"BÆ‹‰ý•cÞ• ´´Ç9(K¬)læ#ņ17È1­qæ9„Î]÷«›|ÌÔö§}exkUêxuGMVÓ{ŒÁ–´Û†¶>þ/}¿ÿ-¿FpØôïüÓµU™³=Å׳ëÄ ë}),< "ï%¨i¢#]{É蔼)ª©”‚TA\ˆòO>A%Áþo?á;·þ*°ÿË-¡Il_¦ç‰åtp"˜g¥»DCØ Ï€ws\Èxíë…”Ó“ÖEUvù›JŒ?ïA#Œf)e6úþõB?ëÔp +óªG•À†y œ_#²/ê§åYK× ø×(Ò“&û?EªlT;à´î‰ìŠ;à!¨ßÅÊ7E†¾éÕëO?™áDááCÔ> ¬Ô=¤ï¸¸83´£=L„ËûlæJQ¾X¨sFºCu="m?5?ÊчøˆŠCEÃ"Í 7T ã®'çiE T^ÑY ùÃ’+A&‚¶ÄÏVWL¨(ÂM|5<çI3ÃKµ>èýäfPuw‘€Ë̦é•Eíé{ä–Ið¸zWQúê,-€Ì‰¾ÚCwj÷êÔÎ÷\ å¯Ù8ÿ,fÛ¢'TÍ· ð<ì&ĤÈà\$éFÔW‘¸`röÛõøŒ·]²?J»¿Düát1tЇd}¥²èÉ’ìÐÿ²Pn-¶×_ÙÑ×AFµm€XOæRkf›ÔSA?ÌʽaÔçŠZ-`%ò<æ*4i°¼…l¿šÕ‰ÚÁâ ­w²¢Óñ¢õ"¥¾ En]ʦ—]°ªbÊOA¯?Kæèæ…3kèž=Q v= -ewÁ 1â-lMÜîx™¢H±bZ{Ò…âr¡$„ëø[œ —'¨´÷`“!{Ë3W”øòξ‹8D»ÂÓy½ƒ>¢ % Õ©X‘æáwjÑM~m¹0LáÛbî¥ÆÈëC/€Vý¯™¦Ž»g߯ˆùøó4 ¬7Á¤¦SsQ~¾9`ÞFWŽoûrüŒ’D@†£5ð 5[¾6žd¼}FQBH9ÏåèÚ«þ—ëaK|_¨IZ‡Ô›·¿Çë:p &€¨¶|Œ*ê§âUéŠÇ³¾Ø[zT öJKfL×cè~^ ’ë>¨ô¨ªFŒôyüBÀNYÝÛƒRÈÄ®© sµsu +‡€+h_ú›6p¨²æ-$Œ˜-ž½åüEï©ø-íQc?Xž1½È‚”Íaúukó9Ph3JziI­Ÿ*'Èž!¶•ªÆ­ç2x¾ÎfêÎunuÛ—Û’@&šv™ÔD"ŠUº'VÖÑ G -:í.†'ˆEOÁãp‹5Œh·Âlß$&‰ó\çG3OÌ8HäN¹w(­Þ)Ÿ°^B!;ú[nŒÚݨz ˜ýZµ¸ý–cý•pØc¬Põ»yÒ:Ýcø{xöã„ÍûÉBîš4«#󜃮f0]÷ù]]gdùToçt(EuÝf”–JþbÕS5SòtÛœÅfa`ß‚»ß×UH@ x÷ßè1J!fKåXGyüŒÝù+A·>¿þUËMÝ'õ„ŽûxS²Â9GŽÁGD¯&µíá_Ÿ]Î¥ÖØd]Þ˼ Ì~N{%ÜKâ:ÃÎå8^ôý¸ü}¤ón>õ†îÞ•,6?ÜíÝèûœIðë`CYÔ܃CT@OWÕ¯v$4TõNç‹v2â ^(JÒ.þJU =Ïø~ s¥H/UU¼)ú!âzÄÛŠrQ|+û‰qÀìå&zÖÈEQóó‰ù+ÆÙV¢7y–%~¶Irôø¨íÀˆþѤN4ušn‹oÐÿ¡µ¿0²¶3…y’Á¯ƒJŸõHŸQµ[Âüú«à~×µë%þwx ‹ƒª10Ô0™ËÌlß„‹8 VléW?š`>ž% å÷Ì2ré}ÙL»oªh{ºŽ +L[ëF|·ûU –ê üzàoßah¬¼§}FAì ƒ?4EòqŠãäã¨.}F|7sD[5ÌHaG¸|GÑ +ƒ7.ùqK­OîÆ„µÏÃxÄÿï§8{J¦59€Ðœ†í*m‰ŠÆƒ(qäÛ!öi%•’>l‘i÷–¥$òS€'²½Ž¯( ? +öì=±HDöö%;|ø—³CΖ~­Þîç›?ßS#X›‡›B?C-AP´µõ`{sïÖÙ‡ùÈTO‰½½î +FÕu¿QÖ  ©s!7—`áDÌ¡ëy=8h"¬pÜ‘é#ÇÞ\éØ9£îÁd ó + ¹Œ©NGã1 ¦‰æ›%ÐÛv@a&6‚íó)ò{U«©Ué Ý‚µCX6Ïòú­ËîÞûx2,*(."^‡h&GÕ²c¨9Pæ×15ÔƦïÑy§Ä)1VnSáSžÛ#¦ —¬O8­ êÂýŸC»¼X^ï:(å +P£ü?¢¥O[¨M¢ç7D% (ÄâyX(÷y.›–ÏÁ  Ù^âxÖWùºÀR=bçtê{èÿÉ~7ˆù.BÅVKšJ^€5Ž ¼Î2×X$ÞŠ~² –Ë58…Yô¿˜~jLq]vy{(lh7Ž"Ïák¯z†wÝ»LåÊ`ö Ý~dMñaØW\­Z¤ÝFÔgÄ~¢ô¶Pbšo¹Ù¨y¢Ä2œúZEJà0r‚ë4ŠŽ­eíÁ(ƒ6&J4–BC1sl½V?êþZå>ÍÜ(?»¯O«²¿F3 &AÖîý•vaï‚>PõRjæ½Hc/¶¥áÑ—ËD¡­È-îUyMí¹[ŠÔ唑ˆ ;KbUÏ_ŠÓ¾ý$›£FCaG·RyÞã¥XÄö¯­víˆÂˆ""*T(5ð s†ç#MTŒ=¿#àŸ+õ0CA/ÑWõµ,¾‹ÜÑ&˜µ$Y ra &³ç|1áÐ9¥)cžd}—î›pðÙ|Ñ@z=NŽû¼«¿Q=Q‹ˆh¤vn2J¼ŒÕ~@jJ ­ ¤½@½©×ˆõçùy%wÊ´Õ$Í=°ëZ. í_ –üUdvm #ºfsüø;Aý)!s½ñøègWöTA™çöüÚØBß’nÝD(â‹ÐÒ½|í/õ?Í$ êáUˆ/¯Î÷Çy ÝY}¡Âz^s[1$åK¤+øÞ= Ë¿}\¬ÞOûúÄM„.S ýðL` r'ï#‚ñOW¥§½ìÎ_£ÀºPeÀ圖’§=5š2·ªH÷QMG-ÝV±¨¦ƒÑä5b$¢ü‘ {FŠ¼›ç›ó0 .0R¤äµäHµË¸ú&àÇد‘ôÇAhïÝ +!«j –ȃ´í\1 u׿ Šþý¢áxèBõ‡½ŽŸoD¿Ù +æ yP…TS [}‘é;ó­‰–²í²^äÿf.³ãaç mÚ}ºyIi/z&xàÍhȈÜàø?ä'’T +Wð>¯o«YoFÂôð8“tYÈi–Ï£K‹G`Äj5ä]XTàú}Êë·òŸyð‡Ÿúñç{6²Oå}J9›)qUÒz4¾Ùž7Û_ùÖ(àëÎ$Já>çF[ntU4=?ñ…8@½Éw¾ðw?b,H<–ñȣߣ­'žt-|"ðVá<»@'ûï@ZÖ‘A»S«-T¯z4ÉtòHMüµ`ùÑWÌ(D~á²¢^A”¸[ e°ß.=Ž¢¯-Ǹ •S8dk5ã ›™…ÐðHÓþA¯)r?W$2ëŽöžþw(§#EC²ÁD°99¶X-@ùÒ¤»ÖQÓ” ÿ°ü<¿«éSXZö7•êœDo +œLZ&_ ÂL)+×GõŸˆª×u½ÒŽÃA`½³ü7:!Ô'"ü-}$€2§^ö¾\M7¾Ì¢ù7ùô°»jØS†I¶CUwp³:s—®:<“Þ:”g„¡!¥ü‘¡ÿ ü`JÿæDg„þð5Kh;|Aáoßœf/‡ÑºÙY‡<ö¸€¡ö L"£ü4i½€=Ÿ»Ì€ÎÈ?W‚>5Œ¢·Ÿ( +”2…Ôð–ÄèltuFÈ1]×­'éÌŽfÄ;EÛ}GY5 Jÿ-‹–+ç Õ 1÷!Ý +W!8 æuîÅ‚x·ŠüRÿ˹Ò-×æ{-]ûAþȉaía>ýÉ«?½…g½+ÃJÚlí À‚’CL˜‘[ybLan±ß›¬bÙúE Ö¢ñ‘1u$à>þ|µÈ{wT/¨bàïäÖjï‹©ä«@+Í¿)‡ãF¿tßêÙñå@CCü@õÈÞú:¤‰ Ëãa­†·°7Ÿmæ9Z éHEõÀô-Nb°ÇZðûÁܘ$1vúQùSš‚ÆUÌ õŠÉÇ1ª¸…[ aO±cŽ0ðŽTõŽEºðu_÷7§¹F â~ÏD\»ÊÂ÷Ë™Cìd½â¨+z¢x™_^G§aR4âv"ƒ¿¦JÏŒŸôäQ5öôê/²D/W0ZuDË9ˆIlÂ{þ\«ê¦à&½‰ Àˆ¢¿–¨àN„õBßâ§ÁJìcñÇQ4éñ™Êoü+ä¤×'Ý[ÖÍ¥(ó>/ZñÃhk×}½úÛ0ÿgàh3À·¨+ví8‡:µîï"h %*/ôê^àÛ/Qý(H÷>–ÏðÀY)^¨ä ¼EJ(Š*h§ñ éØ?#RzXÉô ‡+oôk”@g…3 Ì]¬üŠî)æPᮤX¢Ró\ÈWÎ3,'¯D••(¬HþîÜÏCÏé–Ìž_Ñào˜0Sõy¶&è»°â"‡9¯úGµ".HôM‰‹šA¿+‚S--`OÂ",V5.QIãçö¾Qî3Ž^9£ÕÂúêð%Ž=u¨êØtûâ~YD=çG²ä m読PŒ¨ž¡…X À™¼¦)0È•ž“³ïñ6îy­{­çÑ)¼ŽîH¶ÅÎ+ËÊBnƒ¦úü-}±ò5% ¥žíRêYáLÞoKoYÅp÷/R'ÕúÉ€ÉÁ¹b.‡d]Ú"#Ô„½‚½ëH-!m®Ï~U4¥öÈPüé׎‘ZÁ/Ǩ=›é±¨.”’ÈÃí¬œè”Ùð|ÄIx¶¯ƒ¬¼Ø×ÐP„oXq¨E@ƒ +4ûŽ€<À+-Ìý‚ìáOªn¿q ˆG@ªxõÒˆ(¯9ÉLûã_…æü-ñ?roÿUþí_y² sØþÇ[ÿÿHƒæÇ¿ýï~üŸÿÇ¿~±ûÇÿ?þÓ?ýß?þíÿðïÿ·øçþÇÿö_þþßÿ?ÿ¿üÃú/ÿû?ý×ÿïïÿéÿúûÿé?þ§þŸÿÛ?ý¿ÿ5ýþ/þ÷ü¯ÿøÿüÿñï÷%þä枯;ø  ÿ¯üÿ÷þaOÊ“ŒåÒ šX£sß‹ÛS<[ +¿g’“ ~¯b_ψ*ÌþñàÈmAßbÑlåÌ_ÿÅ'Ä_¡¾¡ß0™Ýú–œÜXi‡í ¼ÏÆ&GQû»Ðë F  ÚΟZ|1QDpoDtJìM—°"Ò¾š}çÀFàÓÜŠlÓ%|ˆýX³Ñ¨<ôV€)ﵤ CÅ»áq>Ý“b«œ¬ª‘Xœ6ôR­ïm.4çW’úŽ™ÃR^‹ ¶€? PèÂŒ»±7$@SY±{"®¦ÓFÄÅñ&zþ ù\”} ©xw$“h^1pzº%bÿF¡èÌȉØÔÃr„•S…ËI ¼åÂëÙº= ðµ¨TÀ¥Ò ½`ÑHkçÀ½ð·ž^þþu(†fÂn •[ÁˆWêåZÞy5£Ý/ä’à°_e¿ÇÄÁ’éú¾…i•Å´EvÆðº”È‘qŸ`ÏdÑ$P'5½b£‚<ßåE/¿õ%WºõSŸLÙ ò¾ÒSDÝPÅBE5“êDÉtbn©éP¢x;“½Fñ4ãüûÞŒû×àP<³n°o@ (ôºQŠ‚™¨†0¾ÇVö‘wîsï)[‡=À@\¼´CÈ…^î úÚ Î=³76$7·9³ßøœ+šÔ|%*Ê8+€S3[™óïžµ7¿C@¬_Ó^pK|ÁbŠ¨ ±iø>Éo^ï~}ë]È~ zn Øûc€Uõd Âàðr@|ê½X«£ž×õËÜÿÎ:Zùæ¯(p»ülËܾ½¢ Ó¸ŽŸ»ÞÁSz€î_ˆÿw>e£À‚=ŽÏËkEÔë7"6¼°cx/=3ÎSõòV® ‡ö÷ŠL"˜¤&Õ·þu!I¡ˆ83ì ¢³MüOÓhxk+}'`§Ö°ÿ•©‡BÄBûȉÿ½L—¦ ]¾†ù]CQú° +Å_4`˜”ö££‹…4N“ÿóñ¸?š äúò¿‰bØûi +QJ-Ãî— ™dï¼t¶&N,îÆC›¯ôˆ 3E²÷¨¼ë›ó˜R1º†í‘} M“Š¯ßÞ÷Ø„k)nƒEáWJ§åökönïy õ€ê­ºâsH¦p£”rf<ð&-‹hngváûx +-Õ›I™k«²|Ô<—Š40êî¤ü"YC8ŠFrUÛ."ÐÖ$Ÿï€ƒ./À™˜k ú.áƒAæ qGÝ Õ‰âK! + AÏx ³çÎIŒ@6ˆç>ì$¤ýÀc–Ü +&  c{¾ÝŠ-1üT7²Ší ”¨ý!iŠÍ7ÛùF(ÞYÍ<ÑÄå}£)3YNJ^i‰Ü¦ÌRç›ó40¥FÖþå{Úº*æ)Wò”{]Ì©æ)—¹NyeiŒbç)Âq5ßAQ“|‡I¿UHª½æLLÉæL#9ӟϽÞ$<Ìâvð/ …¼áz@cRåWÌTQM3•ùaŹÔ'ÐCÔ ÿp ûØ‹ËxòQ,wòع•“ÇÞ<ÿèè¶Ï.Ž5Và (±x ð~–x{Ô3õØÞÙl¿["öžì& Î3MÖ3M–œŸ)ˆˆû>-:¦[#QT-â¼ÐWhÇži»O§mœ(ë2bÜ앆YÐÆ].„g-lÁ”y]—žƒBDl¯«ŸgïM— >2¦3Äv€Ð¥,BxØ“8ÀyõÖ–Sø-áÜŒJ³ìQ•D»‰ý­#¼<·^š$Ç|÷ã<²ËÅnÂû5·Cù‚N¿>”Ü©ëuÏX*„ƒià°uI hqÀ!¯m/¹–6ž3¼‘G‰Û"7Éø‰'oÅ–8ðæ©þZ3<ƒªì¢*ë3ݶ@J€áž×ÕwÌùÑ%B%˜Ð³o ÜëMÆžîŒñìeù.2¢«]ÜDádµÇÖž¤a`±X^2¢ötØùìÑ2`±Ü ªR¶LÓA4¼Noød²Ä 4»$?Kê¾Xa þ¬Ü«òwáPñÍqh…íSAJýŒ©Ï¨`6\VðI{\î7sÃ>+ãDÊD;5IUñÛu¯TWFR÷€ü ?ö’ûùyú\ô¯i—ßÞ‡PZh(˜ì\íåR}+J©ùˆí©Að¹Øµ1Œ©“A&A¯Ù¢’°E½Ï)ñs¥<€à¡{¿'¢ëL h· ÒUŸÃ*h›9–s Ÿ4¬yvÁÓ€õŸ¦Oân åÅj”] +g0ÿK¹ìß÷Áp' +Maˆ¨Ö$*Ò¹S‘dì·ûÑÁªJàÒù˜ì3âLHÏPæ~#$ÉÜÛðly´ß/´ôÇwÇnM3£7Uûˆz†"¥þ"Ü0”6žŒtp¯†Å´Lèçk¬þ‘" +ÍwerR´oÎÔÍOAášZG’6ÕYàWŸë¨z“}‡?£0èB{èUíó¨ßWöAݯ‘ÊŃkw¥–TØ|7ùWž U¬ºNwþ7ˆ×èöÏÏ8 ¹±ŸºI5„¦1Ð~ý)UpÄ€·÷¨ °êy={_zÔ›0É=°­t+°z:e—@‹HÅ-E7™á$6º®zÉ/nø™“‘²ûD—Î_êî—–è1Q”,ÐO‡Ìëy²"W¦¯lJê•ÍPZFSŒÓIOåºlò¢ÕbÅþä )¢ónÆT䉨 j¢hqÕ­; ºK󽔌pŽÛŸD 4èuþäù¥%Ïóu~¤Qåz€e÷.Hƹ àF°l.Ó½b„ªù×…*hî€MŽç@‰³Z¹ºùB›õý%pg©…j²•P±VÖVI6K|Ä«èlêª0ib!Œ|¯ä'>z7!;Sü ÔÏêJjÒìtì²N=HkI™:M,Ö{60BJŠð)Ü m2þƒåבÆtKY™'ÑÙh2Gî5^»7«:ŽI$ßNNJÔÍVt°™[¹iM ŒÕóX¬e áa«5W‚ÒÖÝ{ïïcðˆkêG/š‡àBTÒ+M+.̺çÈùÉ<.!Wî“š–÷Ø* Ú9Ÿd† +^aÈ[O½¡«}zqhr›5÷‹jë<]ŒÑþ.?ê+RéÔ¬/&%8HXž0‹cÝ3 ;ûqÜ¡yÜÔU÷ŸÓdK†¦<ùÎ2`hxòÁš2~M :à$–¥½PÒ=¬Píp‘Ùw}ëe8³ÜÛYD^‰²™wU÷º`FY—þ²OV؉E컉ˆŸ fLЪÅÒŸº²Iµ¹€Y^¹Í¾_‘Ž,ËUÏÜe‚þ endstream endobj 58 0 obj <>stream +’Õ@†<Û¯0Ó€@èzò)%‘¾.Jœ+rÿÍ„Dk á9ì£h^Äòó ~Æ~'8·À~"$öS¹ó£)'É«¡ö´3ÂŽE¥ÇlZ¯}Dx%ÌÕ +Jš ïÎsiAß y7ÊÝ{Ä +[TÛ)Ãt´ †Õ4ZÝÜm÷óÁŠ?5çlγO‚:?êù.(¬,(%§#àèÖLiÒê¢Ý™Çè6ÑÚe]RO?"üI• >>,™í›Ó€vÍÛ_ʶîue'r"XAþ•o"€ÎÁº{TzןÄTŒæÍ¡÷¤Ú¤âÍf ¨´eéYQ&=´Ô€á +Ø>Ì™¿ ðBWô½÷³Û«äºÓÀXjìk¤viÈH¸:rüßEh‚q|Éj8Oî#Šþx„Õ? +ô¸yQ¿“.²(Ê7¤{xy1.A¿D¼ƒ­ƒ­ázóÍy¾`ù rµd±å‚^åB)q¯«¿ÑÚøSL‚höÞ›êö¹Òsæ¡E ý6J@ÅüÇä<~àCÜ£Œ±ßò2Â.%šï`ª‰(ÐD®›G·¾®d·sij Qp4.gNOS(ÎPŠ•®D@5¼ ™¢RUÏOJÖ„¢xyr¬ þ=I©_”B)=Îe• B"#R©…bÄã2s=ëoûQ¯Ýœä—ˆ\ ɉúÝŸ“œY\[ q“*-hvŠê훀c©x¡ƒ÷.ÈŸQ-(>“ò>Õà§v¹ÈüœL¤îÕ µ +° +´Œ©fIòÿ5Â+a’F|ýæ4ÀÖX-uƹa}ܯ#·ÿ…Fd<2r¡_¢ZÀÏ”|M˜½áG¥Çº•;è.æ£SϨ©(Ë·µÍψ÷M}eŒþd ·‘ŒÒ¦úŽùÈç?»½ÂQ§1Þ2ç¯Qp¥ü*h·3‰ÃK 0G•!‹…‹Ð5>ÁžNË\¿Fä‰u…'ÖÐó½}sžJA IÇ®—ÓÉÊúÉÊxbõÉîuìuÜ&IæušàûÊÁs¶tÏÏ>¹OÿŒÄŽÓyõñ°Ÿ®îÒw@ÜL9…5Öܯõæ‘°JÔý ”È…`Ævσf×´…vh û£‡¢þ#çXFÀ2bö÷— %À„ Þp…¿ìK¹/ú^pÄïa ä('šh'ÿ÷q|è­)‚ëBÙé#"kÚ²Db³¿­oÏóœ/j_0}ÔEtÜdKðMÄ:y±Úë+—ýÓ(¤¢¤ļ¦)1Êl‹®öp }pгü&¬\÷ÕQ.(Fè‚sÓÚ}sÀö&r`¤Nªmà»zõ4Wµ#©pÕ=É8ÊتHÜX_çÍêPt·»w*p]uxÏK6É•NÄÓ@}ãý1áôé½ùÐ|’«ÝR}a£7_§tzMîN¸É;¯t뎺Ht~°²<@+,ÕÀìâ l|€²bµo„0×+Aðkx20ïÀ$g²Ú$ý`;öÂhq{DƒuÿTžï¯_Ãwgk ß‚bù×_ÙÁb3I$aü礘렊ÄAð@A1çEìÇBë…ˆmðk]Î#ßÇß2qVYnh ŒÚ›Oÿr Ø,ÑN±uÿ_6„gî‘6;©_þð¶Ô„ÌÙ•Dù%ˆò:Ýè½=ÞûÑ‘Eå[vü‹ŽêGÄw äGõM©-¬@4=’Ž}é;&šÈùbÁ`3£ˆAY·L4äàïu,oÀ✖B¹»Ož•>gSÑÅ5‰çÂ.Úš'^Š”䇮s?š. ¶©å.ñe°?þÛ¹`AT‡g +´"œìƒ*¢58Û×±œte +gM¤(úõWúDªy«Å¤‹2û£çØ»YаÂ6 Ÿ(+þ`>S{²†©¥eJ¶´HÙyhlUAÅŒ«Î™Ó1Þˆ–+°FØt(×1Ž!O‚P!øA‹vê¤BÉàE’ s¿Gf,¬‰¸»¥¹FeŸˆ aPûkˆU¸:›ÄKfÎÓlg«±Ìyà6øD敇)¹ÜÐV8€°oð:h„l<Ÿ³ñ4—TÈ>èÂW¬!<íYA SíÂˈ¾]Ïë´ÙpöÒ»€ë±w÷v$èÛöDÒm,¶Ä°ÿ¢«ö¬ *h"xî©À‹Nÿ­cO7šÂ ¿.`љĵpCœ‘¨É*Þ í3„Ú½ÛrÃÀ°KÊ–gŠFšnXÄ?oÉð-¸í#ᤙ¢¹¥¶¿Ï+eûè½_/kÔçf ¾jƒf'ª¶ÛyÎFB`iõ^—òi”¨+2>_ôZ•Åb $D×Çs^ +dh¿#÷ùy¼KX¨]–Nøk~·•mÚÞÝ>Ý<ç»ðˆbôízyd­N?ÐH£¸ Mÿ&>ëôÿpø^ŽäJU#ʦý+DŒ‡®$J€ô¯ë“%j=jx¤'*v^쩤eqG5ß=½UöH|ÒÔ Å " ¨'¢§û +rÀ6l4°k\IêëáùºR·%ËÛo÷¹ŸÛ?w‹ƒÚ¸NoÖBû“aèhàéÎ" {QÙ»Û°Á)o×T’€E`ˆª‘«ã%=ž%41-Å(êxï‡C†É^»Rš4Àùsé|®Óø{ÖâAÖ>XE?H"(¥±¬õ+ؘ!?ß;`ZÛwp¿›M£*R;êóZÎØ©ÚîäJóöË{<_ÿ Gx§yÌ…Ïsp¬ï±[ +s‘YE‹Q¼ÕfìïƆpé „b?˜‡mjÚ½|Fä…‘‹ìõh±#í»óDVÎWŠH>ø‘0k¯ÙÈô}ô—^ ŸŸCãO¢ª–²—Ÿ¡ÑG’û¥ËÒã«kf%Wjþ |7Põc­ ÀŸO}?¢P7 ˆ /œyÖÓˆ RÒ¾'@@æ @—¾tÁZ'"ïÎtÞnïxóœsfÉÝ°†iÌ»ø€_ãü>x¨eïðtw«ü&XË©üY_²€ʤ +›¯§:€MÖ‡ba ûÔ³Ìc8y dV†i:Rlëg@¦…_Ò¶ÏÓ ý&þʾώí›å¤ÞP þŒp´3^„×GEsÖìG?7<Ï Ÿ"â>´íÓï©q§ä°{àÞßDüá`álÁLD7È<>Î6J86ÊDhhî ÒCÌ‘í‘9´9þæ›B¸ø ?Ø%1VºkñT¦%êéó”ÆAÕSŸý›ˆV6SWäEA|D!ßÒ€ª T˜ŠúbIèá¾/¶Â† %äµbã|ìí‰Z~}(ëiÔåž`§"$']•h»¾MƇ¦r¼HR6ºD`Š³à–õLlÎh "rÿy¥,–H]¢’ɽI…Héy8-ƒÆûQî í»;*"ôØãòuq!ý¢<ïr‘íçÍAji[Þg-³³â| :¬L[Ì¿F7Ì|CÊþV/rÀ=wÀuÓ~pÎ'ÈÓýÞn"ž/¤Uׇ!Ú›‚¥0¢˜ ö£ë'ÃÝ›ôzu¼’œ&E`>”_#Ïy*0tÞÚGÊ^4Òñ¹¢hÓZÍn¶4 ª 7ÙeÐÏb¬Ï‰ýa×2ÉìÝü-dø׳À0}ت Âq†€ÍTŒú}m>‡T‡ôn˜ïvz{ÂÂPKØÀqÏ–+˜ï=' ×_aeƒ–îˆÕ$²Æò FÂ-}eæÎTˆûí^Ï–5£ð¥2´ã¨„°!pcÊÛÝ“ „ðO¥}FÕœÁhCìaR?£ª¼bÜ +;í`z¹ôÔLø»DÙãN;pçJQæ¹ kmƒÁßÿ<À@KPŠ‘M§«L×à±ßCú™ˆû _·ÇÉcƒ[D$üó $:3*˜àN"í”  =L¢p¼öÌs„rh màÞ¡«í¨QGªWé$´¹D‚ƒÛ²,[r/ü5»É{ä–,öÂu½µT«AC jI>¡¬%½íoð•ðW?`t—Õ,r"Qñ|1{mJ¾9u—*úâÞsÍ"~cv*ìw»¿à>Kßèψ<«–Lñ™y€ßœgòŸ4šÛûcµæˆÄ”Côì¦f¿¢èýPb£cAg˜CÿõwçvàhPG9$»t†YnaïšcIÚr¾èg¦·ç^€=Ÿ4³†“í^œÆ»úˆÂªŒ\©2Ó$ˆd}ØÕ¨¶ªäW¢jÑ £‚Ž@ +®§ßE¼[5yÔI)ì~sžuh¤“¬=^*N“BLŸËs’Û_£ÐÚ‘ä€ñSM’Ø,nÓêp⺋`ç¿óN%%[‡«CuÂ}î—¶Pƒ6¡l‡c“.(5ö² B 0ZˆT“1/51“·€ÇÒ,éú^Z¬£Eq6¤Å´¡àŸ«ùíÚ®štOqP£P;Ée Dö "²ÑpÚw¯²ûiqWÙsàåK?‘SÐp€(·d¹Å$-á<)AHèÓšh¿‰½ûÓN¥¥¤¡« +ßů{|-I5ºL` =Utmo?#€¾Ð“CÉ¡½½­(Rƒ…ëEKšÜ}'ݺîŠ[“5Ø)ò"´B“hìýóÏSÖ.}'Õ{õ£˜Ä3 ½MjÇvÙó@]ÈèxèbÉ¡Q¢nAÛýjÔ P¹csÁ^#_ܲ0ü’{]#¬4D–ÐÍ­:Õ¾( À·ÄÐ83Èé 2ºÊw @™r¾Í%6¬´x”J™¹L!Çf?ÏuÐüÍ–ö.D–¨.š(y…>œ."ªu—¡¯:ýb€<õ‡€ë˜û|Ad¾’¤ïj‹#uÞÛèIsªãPèƒäI_^,¼Šr fág²ŠV—†NWã÷ƒŒaodžgÿýYûÁÞN&.ö\ÚÂîADÕ½HÖÚ)¡}æ²Bµî”Ò Œ° ¡µài’?\ÀòO§“s–†œJ}ÄC5žëÀö?æ_Å“áßì¿Þ"@­JŒÄÞ¥^L†8ÐÚQ²%“RÏs· ªÔ9@²K$™Ü•àÃfg°ìŽ¬¹°Æ|8£8 )NOdÔv¦lgyyAŠSω˜þù…Ž‚öÈrƒf +´Ø ¼i•8ÜìÊö(ãi^qN]š^A@X£Ù‰R­wø~¾P¦ÕTíOa‹2pÉLÞí:r9ÓCÙúbcØýÙèzuŠ:°‰e‰BlÈÈfMܹh^ûtÝC(CÅ”óõ ÔHqØé½qEºÎœ†t«';]lYQì‡0‡¾@ 6úHf¿k¡H¡†ó;¿>1ûZ\ò¨aÍغ‰vÜ?˜¯š—¼G¼xùFË?º ïÏkÛ¡¸Ä:ù,Ä¿ò̹óÊ¢F­œU§×@bìÃ¥qÕykø÷Kû¬útŒŸŸoÐÚBøÙü p6 çOLº•ÚñÖŸ¢nÌd)±[Gcxçá"«{¦öÚBbéCÑÕo[N¬-¥Ì®÷_Y¿UWöC§Õ‰6S×ð 2$‰ÔÚˆÀb¦‚ñ{sÁoŸ@|0ŸüU¾Ú°Þf,Ø¢‚=p ñE¡ŸÜ˜Äƒ}§ÛÇÙüLíÍm€b¥lˆ]ñÉàxçÃû`@+¶K%©Œ! (fŸ’ó7ª|w`Ô·`šÿ¯‘ˆ&”ðBY2i²Sl^_ël£\*ÞevŸ^$r¿9K`‘‚•¡Ö JrZrU³-À$y®­×:f :ÏÊI)èPžä.«›¡¨S„©hóù”¸jII¾&áÿ‘C:ØÂvÓÆ}9š{¤}-{5ZæÝç¬|™Øj'•– ÎàzB¢Z!IœXBÚÆ—‚Üîù$h5ÛŽN@á&ˆÚAÝziUˆ€5z~4®,.˜ªFÐô@om:"B¡±]þBè¡ÐäZvêá=¢8b’¨Rõ¸»ø}]+ÏÎΓ볫 @sº õÃÎö·[+Þûð+Ão ?úàgÑ“H[» ©a§ÓZ÷{Ÿ$ðÏ~¿ÿ ?F!FÃwb[ö{);¿t"›~‡UŒ!T©¢XFcBd û#kZBjÜ>fSXØóȹCH衆»GšúPOmJ¿ª¥FYCzâ`×”ãA€ÃÌyÀr–"qÝWÐWJ(p+Vzâozió/~u3£†e™BT§R‹-ð DT›‚]°'‡”fè“‘Rÿ8¤þW `»°«F!qφ&bR(GËÌ8¤|Ør’ §]mÏùÖX°øÖº­ |x’·RX|Ùþ"÷×LSH¢¨ñaûîÞECzh¬i€h‹=?NýOý¨‡§@bÕ;Vƒ éž?/„IÜ%²3BZ“¨{„\{î2IðÀÓ>7€Ê!ˆ#z"èöªŒÃÝÝÛÒ[zÏYÅ}!tn[§–ßùf—åiljîÓ4¨Ñ-O0åÀ&i§^_½«2N¥³@ö~‰–ã§PÕàøR{&{Ÿ~ñsqøØýDYÿßOvjXú6ܯìq-­àð»ò£Ù®oC*”vÎeŠÅSú‰MÄäÆî°yöÄNÊÀ®è«eU‡•@äÍÒúšNn-Õ-Rzž’Ì­;¹Zûó 6JCy¹EåìdQ¶™ H_è>6 WϦðâkží5Ï&R¼vŸa“þ¢T$¨'6ÔË? ÇlÊ w°m·ª!—ÆÙþ¹XÌãUØ“{£Ï (©tàòοk¾ôžÿ1¸×,Æ¢»ãÛšM+VˆxhÊÙi©q¶s]¾_®;Ï#˜ß^ÛþùˆBÛ1öoá=1Ò [È?TÇ$5!û‚Ç@Ñ°<Þñ\®mï}#å÷¸ý°dâÕË-Ö5À!(£L«ªB®˜Ç† ì—Õævÿ<­LAõÓЇIŸÈVƨÃÐe·êºâGsŒ99x{_›÷fKŸ +Z +å•B8?du­|EnQ€ã òc¤úçyUŒè‘ ÚmÃýÝ+IqK¥bè}`•ìcQÀ¼Î›x^U$ŠçÍ~yôIlY±ÖIõ¦ÆbQgJ£ÅÖš)$?ŒÆ>x¬xØ×qD ÙÕ8bñ·¥£]&¦Ã +îÂØìÀÉÔA|™dUˆµd¶ŸóùÂñéû¯8Yoò6èãÐWÎv’>¿œG †™PB׶յb÷'<_@~5ùÈ+¶fh°„z ÎyÚ¾YÄ0ˆ‡I uCðyí•õßß6WT®®£rU£õIþ˜AÀ:Œ:€J,ÛÎ8yD±±X"  伇;ѽ}æÀÌcîÁ?!x/ ä˜F¥"Þõ:”$%>¹ù[îkP8¸õE“‰–yjò‹šìI¥mè('´¢±öƒh€ïW`ê ÀèïêW]˜‰48R$÷­ZI×1|ÿ\%^8¢÷Ó(¨$Šî.QRJT=Qβ! ›|¶z‚¡vðXmDg¦ÆaD{µEæ#þè¹,Ñ(ï¸!Ã+‘\Q"u9£ÎÌñ'¿è*:r+Ÿ±•èåÔ SÓÙOŽg±©±3´4öNRÉú=G#,µ#ªzú#:¯]8ÝøCJ\§0žl§& ÌÜdUü5‡NÆÏ…NÖ"þ Ô'L奋¦"’ šði½:ºê©ì×~µ>Þ¦³-»UÚLTËs`ký$Å*\N™%íàz–’º>TàNÚîDŒö|]i +:Büœ }æ[6 +ˆ'ÊV ¸kô^¡–xÇu†0qT” +Ö‰À5ø}ý©½#_ò KHI‚ìÜ`œò™_#³ÿþ*ØGóqòKÄýä®ù)} €úÌa?Úópmzb]Vɨ÷uRd¯K[º)>WÉ,SuT’¥@Ãœa‹ö.ÃÞÖ¾W|ÃBo%ií ¼Êj öè£[™±ä:¥Pº…ó:òbì¼^x%]~HÅx3ª…´i=IÂa"û)±Á_Üõ~zÞæ b + øS_ôéQ '‰#–Û&g 3j +&vSÑ@Axå8‹ÒΦƒ8† +RI@M#±³<ÜG~*C^È·Gý²5ŸÉHëõ¾¥eß"*`–Ré+„§$ ö=»¯ógµcqgUÁovÏÈNnîCÏ B„¼‘‚Ó©ïD¥x/¶#ñr•5çBú>ÒãeÖõÅyÜÂ@Úèå°óuR·".h)O™QábÛý‡|zP-!ê©©R°U™…r¥Ô¿ö‚ ÀÚSü)ù–­æÂ&¢Ëu&^í \»ßâ’í&Ø4r´ çÏhƒ<™?…ðî‘Í"Ÿ–E Vl÷súW-©A} °DÂÿã~'ƒÍ¾;Ž˜U$LדK†RMc†ý×l4Òš}f{× ÁåpYÀ zÃèõ²º¡ÍÏ»æ:(à Ê-Éã‡\Ýôc»Ÿž'\-†G@}:¿Ì;\¾Ê.‚שּׂ(øQc®Ž“Ku¬ŽH¡ÐMÓ>õpYrT÷•x¨Füki¨|9÷m`ò õ!@C[¡ŸË>ypó€1‘-bÁ—øÚüZkƯÞɨþGØsäñQïÏÉ|þQ>ª¹i—•ñø€êF‡”›ý‚Ç\#Í–ç(!«‡µØÙ݈sG܃dÙ;4Çrªþt¯ÔV[õp» +þ€6i_y™uE Mâ,άʻJšþ5i* ñb*Ø©F…O›ÚyBênA?…ì6ŽáºÙòQ„©ê€äãóØÛˆüÙ:2é”iÎ%³¿"Ÿ l ¢F#¤¡ +a<õëJ"Þõ ˆ ó7~¹€(é‘Ay”5÷§•óÓz8¯{q¡q öÎ…®ÜìónÞÉz‹†(we‹ü4Ô +}ª»³p\Qd²`‚ê’¦ö«K²@{왯^÷mÅYsïçHé€Ã[©XSd ”ûHïéô´“Gø„.w’Ѩ¹Þ„Yʽ†¶/ Ê‘&«"-¸Š’àIÖzEØökà j-épqïK£f&5W;ŸêL GeòÌþ®…D!ȼ£8·Q]‚\­mžçUª;"Ìj„fÈn^ +v–SDÃÖÑVN¶‚°Ô©:&«]'«UT­œ¾Hňº{šì6#î'чÂïô9”Õ»j}§–Ge'Ï]‡Ïe_g\GϲŒd`ìáH,Dk[xü,>0¨= mF÷¼T ¯7…o¯08†»£*€ªjæ§Ø]Ed-›™æ(@Qò–bZ ˸[Ç™áÖxçݺ´«~€X]i$”¤UúBs¼ À:Dm‡‰-Åà0JHÉ‘-–“+;þßÿMì è Pa^ñkÉꔃœ«H|E"_©5àÆùøI­ËI­[¿Ó°P¯wã7m£BÓ¢ÕÂi\¸.ô:¡V7Æì†h¡/h&³râR­ J +„/Ç1©¡\̙ӈ°§/'Y… `g5å©pðúKÃYz(ÃB™ì>UD*öí%û’lÕ„£çº úŸ äû=SÏ´ ‡ê’D+s=¥P‘Lh +_§ÖBÍ,áƒqÓþ +ÁS`¹ „0VHË÷cèAŒAÆs¡¿ƒIšîú&EÜPD'¾Hà‘äj±:jº£Et¥Z’Ÿº_E¥8¤Ÿ†(¸‚æÞ‘mü.Šå(*ʼn‰h×™ƒˆ¯+IVÙ¿]è*@,¯ÚÏ+‘É:‘mFZÀ±£Èp;@,W¼u½ÙIõSè M„ 6#7‹Ú{Qý+2Å;0Ù•³Ü2ŸÑä«Á®ew´bƒA›?Z#ì-]J"© °iúÅ¿ŸíR XÁ Ä€DQ’ü”ÝÇÁ¾Ãþ[\Iè¾XYvÐÙ * cãéö4… ’…&Ée©È×­ ÁaTèúJ>žF¥LN§-†¹„þ+=1H$Ò t~×oÁŸªù«@¼¼¶T)u¡«)\þ˜5JJ‑á'{ +ÊãœÞ~¦á¯b7vÖ¬Ymgì¢ ŽS_/¦èw8mUò¯@p6ÚË­8ú—õ _ 4¸×U´9‹–üz[¦V:ü:›9TSь宕ñG‘ê­–§xÌ'v3j…6˲GNRNš ì*Þx7ˆuÖ$OVÏžYuF°¨Ðíêí{OÒÝ<Â_¦ìJóWÃbáŽhOÜh§öËà#S8‹ª.)飬g(~p¥¤ø½B’w„ü8é¸?&ú¦®E(Lð©B T™]îȸ_QërÜËñ^êá¶æÍ.¸oå^Ys›åfŸÛþúüg…ûùSbv€ŠìÎE;APa±à»×Ö2Ž¶*ò öЕ¨/®¯Î»â@°IÐ=È‹<Ù¯BB/Í®ºØWæI”º4DéE€.M>Hñ<"è&Ú·ˆyš²‘ÌÐ^¢¦>±ÿ¬'M^"Èøß+]Qê¿cÉ#ú*¼½Œœ&ž¨$äÙŒr/BÅ‘¿h  <âaBs*Å|]ÉŸD_óHùSl¤ž´ýG/Ç=ËNóuì¸kÊÅ:™ãùý ¸09öG¤ølsµyÐl?ª`‚bd¡‹"ÆIË«’åNgùÑj„Èψ~‰R=Ññ©êdµ—mº_zéÃ…œJ˜vplÃ"È1…´ÈRi¡—ÜVÖ“zÉÄeîë­ÌèÇ Fm`= 0‰ Í·ëb>ÕÆÉ+æ–ºGwÁüùþ<Ôø¶‡GÿÒõh´—¡Ÿ½àyÑk ™¾[²Eäô÷&`j½ÿ^2E!_µl—˜•Å÷ü¶ûGÜ“¤ê *Øcz7”{¾+eÅÒùfGLu"Z}ï`g·íD¸%™xÝ_W&‡H=£Ž’:H©žS¤êìü÷ƒè'`%Þš ôý/•{ŠgwRg—IµžÁjƨ2ÓNò‚è>> +GB5ýR™WJÜKLb~©ñ¥”è1¥š-Áþ¸AùÛO£ÔúÈJbFÄBBìÞmÙ=ÖI›c‰û@ï©àNcKå7®#¿qµ@bbÕ@È=”ƒÿgK+-Ѧ&}³5s¢k /Ç–°›+ÐÊz¨û~íd`6ÁjvA¼=›™S“ýåwlem?ikE˜«)Ü×éÃ#üÛ…Ö¡uz»ÛØCÅõNår@rŒû -ŒW„%PlÿþT¾õ´©²Ýu–ýá{¼íˆøí¬æ4ä¥qPübï¨ju¹¨|i0QE§"€Wïî ð!ªF!ÞDÐñ5çPVŽÊÁýrð¹z ;ê¶OJqÜIë'X}aY,ä£>Òr«H ¶Á ;‚zQ"Èĉ¸ÞE÷>6#¢E .BÌU¢{î?½E™ýÉÜ;–œ({2">nê¿bwzœo“ÅlGc©,õ¨cTN̉n.Õ>½ªPŽPˆ/SUÁYÚwjJ#ö\‰â/ºo¬¯šS¡sƒ“Ë\^G( “Öƒ<›×ѼB±í®uŒD×ì &!@Ž~¨Œ_rý~r£î†®Œð½Âôc°·”žÉ€sX¤ +ªNñHÂëËRg=3£TÛ€x0rü”}V™êDˆÈS®dÅ•AøLw¿{BÙÎlÓ±l~×^z~{ÛÑÜà3¯íå.=r®L«4À—E§´0ÛHQ?#¼ÒÑ Yh7ßÏ7§±þIÞ —$“XzÕrËuý5`ÂÈ+Ò°0|/ókPw­§ ’iŸ¥¥CÓËk¯¯`¿rUÑáU•#qxZPÁ ‰:Me&…÷¯´ÓA5¥½ TdZ›5ä"#}—Ê~LyEü¼ŸéoÒ-Ó ŠD À8D+Î~&³@µˆvìæÌ6­+4ˆ Œ.z§‘onªf÷<·ÑoÐÆ{¢ÑÞÑìÑr­Ñ5̽ôr{žüÖoÁz·x¦@~  ¾Í¼Î¥¼'’V„˜<:ÂÓœäþtàs ¥þó@;`ùLï/ù5 +YÛ‰ÐØãš‹hrR_¼Êk;œ‘1,P1ú”e%ëgdD=Ôá•=ß´  5DÆJ;tÛHú÷é¿9þÍXýŒz@Ǫq ¶b¿˜¦ ¢ëŽqð3(îúïEªc‡p$ÃÊÛ+„qBvßQ³XÁ؃Ák¾²!ÿK©¦ãQWSµÜ…Zð: £$ÿ’ ˆ·­š6ETç<¬ÚDô+2Z´×8 Ò§ë6÷¤Øû~LÚ)Ï»çÄXhÝ¢†YUPƒ°ôÚ‰xT²’ÉëíÁ«èåÈYÚ“vÿuô3pÏÕÏñºÓÁz}ÈG%G†q us§•BkÁaü@…ÁZJ\”ŸÏˆ” CÔÿüs6†–¬:ƒp7S§Þã†ó:ÈN܈$ò˜öê³èõ.áPÝA—ðgBC­^éýd^Öɤô¼’Bî/åc6í¬ež¿Žc/òÁ +<³…ŽB퀰`ÌÖä€Ö/m­V)ÛìVšhªcaÏs')dÙ$ç²êvrž&Çb7 Ê 8²¸Ìí æ*÷ d¸ã(DX/T|\†>)Ã’¢@ú#§Z/Ò÷Z18®ÑÃÞÙ<È•®ðÚï»AI{Ä1¦Ê”ÚP”ªa2{$,PM5€)`;$&S£1ÛYGÃe*‡Ë¤1̲¿ j!d§“t•ñG…4°ß·¹¿— ²b6>)àD}Ò*îïÏ—a«¥u¾#„âFõ[CÄàŠ¬yü=´NîZcÖ€öŠõ’Õ·ï§ ç|~˦ΒUûâY¸ÿ€?zü§èuÕÔ{]}9MV¦çõIž­¡.ÉZKÄÐ!¸ãÚ’ ­#GþÔÊdž¸¢Û”)‘ƒÂòíæýçŠ BVU™]{Ë=†‘rÃ6~%ñ));_É9ÀKzšÓnÝ™ªï¦5–çÇ_þd~ÿ~¦RPÔÂ-úӉˉÂdA3®çÌæÔŠÕ*£À ÅÆݦÞågSÀF•ƒOªÁ'UÛŸà(/¯Ô]Ó®âTº…Átë·Ž˜ó8&afðhX#ÀÕ~°l—–zã8ê=œ{‰°L‚4ÿWõ>È0>“'¬ø[ñN†Ô°$V/A$f§ö ˆ +Ó…J ®;HZÛGD6QOº¤#TÖÏ(å§TòàEÏÔŸ‚"R-"J9‘Ù¨‰"=Ó-iQ»}"D!o1ø”{öÌ1ò‚˜Ñb‚æ\1ŽÔ=.óÖæ2"R>|ù3ŠDªžÑþØ/ÍÍz•uAj»ê\9[.”DîÁ”EÄôÊÒ¾ã[³f.\êC¯|IÉÌD›Lå«ò*!È;Ž¢ŽR]$ÅòªÛÁæíÍ„*Eõu’lP~kð6RIi]?VË°k²å™ ·ˆ×¡’‰8ƒþú[3¦e6÷µãøn¾©wL)ñ”"~š¡ö) —K‡ÀHÐAá[ªg +õ™°;ÖQJJ'†µf~Füá4J"Z³÷:AS|œç`5¿ôνDÓl„ŸíPdXù iì·zþçÛÞÃ~ý¨Õ·k¥ËÌÈ£$íyïñUÌŒ”W§-Úú„nTÃþ«°®N"Ø| º£×Ùk2¬ôÙvD; +³š,MlUa{e° +)ÁOP·o =3¿® .ªÒóPÆ»Qc•s¥Ø3RDžH¥]Ï VQwåŒVˆ^°ÍûáÚtŸùN’ÏáÿÆ.Vµ' +›é)îœüT_•4æÙ?ÚÅýqHªËò—CßGÔuàA÷ É_Ò­—ï$ô^“!þFŸ˜^*ÒÿË¿gf{-µ÷û”H÷ñ×à!,ha|ÆÜ«Çìï®5Æý±ÉWhÈ—öÊD' ±b_îÜÒÊYž~³)RÎ,1ŽÈœKiÃÔre†–½Gž)²Xú=RmíP ÐI‚ÂŽèîm &âõN¤4Zª㶕€_áW0êvVƒ{Cí‘^—‚IÄ9ãïÑúbç û/1’{ +ŠºòsT<¸6ov«ôäºm0ò :ΧŠ¶‘ˇVm/üaSx»Ÿ[a±Óœ—õœPfd@éàÜþ?1‡øçR¢¿ä:‰¥\jHѧÃ{²·¦M\ô6gŸR/¬àqL´G}¤]deoÜ3þ¸mnáÒ•ÇqúŠSÒ*"žRtö}¥Ý²í õ¾LfÛ±ëŧS]ž0ÎÅ +m@Ž‚LO>¬Eë•–GF´s‹ÎÕì Ü×HÖ\0\-Ùའ”׎ô5¦¬þàÐ…šw¾MÏ‘~©Èõî†1çõ,4¦rk°i¹¡†n(Ÿ —Q1±5zgøM—]íÖ,k5שµžõÁìþÝÆÒ§€áå/&± ÄiwÓBË‹l¶pìl¡§Çt¤jW™ÄyÏ”÷ :µÊ"ª(C#aò2 +NiZzS5¤†¶W:Ó-S,o7ýJxÒö¸¨vÑÁXçƒ4ê.–À׉ç|2h®¬"Æ]Aµ3Íøâf É^’öñ´ö -Ö}Ïî´`$‚58ß®¯,¤y6Ç:šMg³¹ÞñMÄØÙˆù×.Ÿ%jî¨ûa¹4frŒ¸]§ +CMk +2ãl×ÜáûÏ Õ%¾®’!íŸÈú´®Ð‚7ä™…'tÆ¥K÷*SïË·°›µâw®àj×ë‰Ôˆzùg +Hì?ÒkJc†\.b%‡T‘÷Oÿèð=J;ÌΘƒRã^´ÖFÀXÌ'b$B:¼¦jºÃûFðÑa¢$Qœ›DÑC|T#ª˜sô¡€@¬Ñ©¸•¥8±iŸ¹i”?šòk§ÄÿÏýéÐp">åÛqž‹aø›€ºñÒÇxê)ï!¶ yY1 ºr²°ƒî-–7wEgM†ÅnZÊê­ Ð±ó=âù>õ†C^AñŒý´¹[”-ë™Ù"˜'íßE°~ +riÝìöUY½Cí¢\hsÔK@ý‚^¶œ•â‰°Z,èÑ„¶L-‘¡Jk=¯—gÝ”çv£­®åO}4s|Pªï¶sÆ( UÅÎ xäÕ8¡µ1ÛõÚd´Ð¡Þ†,Ô×YpoìJ}W<°¢ZHEwttYóDjžHßÊ‚)„Ð>Þ½[.4 ›)[KÛ…©c]?HM[©¦}ìœNš/”œI_JkßDÔº‘„×ñØ&«D]yei¿»Tlд*xóàA`‚Ï÷[éyhÅ\²áw”º´ëπˑ/‚3sÃ$÷%­Yn g-žÚ`7¢¢éÝž½~½‡`ׄøÆõ‡“šê ㈡@j­Ó”Õl6çVNDA>#~ûtñô×ì ÉÈoö3úCÜ]ß^É"a¤lÉÿøvn|Bg"Ùz»›äöQc U¤ç5ïSŒãœ,’¶éÅV†Fù…Ã|‹Ãü¬q¡UK›y¥>{l¯äØK†\¢Ýëóï7é£ËÏšÖä4o”‡ZQ¶~ê2m{Ù£ä‰"c½ß¡ú©è#âŸwýÈqû}#“øa*Gð@Œàz³bÃé}C¾˜÷¤ìãçøÒláû)«€#úE¨•}Fø{žâsùrÏ8Ñ\÷îóì©¢2iQ%LÒ =7\b0×êòÙ/V-–è|FyE¦Î)™e 2é +OÅQ¸àÌwMRúŸ V7kÇP•kù/‡Õ ¡ÒÆ]¢Mû¦ÆýQ{l‡nY£HÖoD."±©µ+%í¦dÖ½EwO•)ÀÆË|<°›ª£„Â1iµ¾Þ‚ýÞ^Û seÀkDØ–)¨Pû€rD‰ˆv'з'‰$wîeW;PáWõ±}íÿz4NÉ×YÔݸ ¤©ÜGÀ»ÐíP¾AÛÄÉ3jª]K_‹¯ïB=扦³shU—ƉæB ’î—*Ö[ˆÎü&b-‚³ß÷P™€¦8æ´Â’¢¿J\§V'H౦?{ÝëK§éóäÜeá* +ý3Jaj„û؈ì*,~³¤9ÈUëžà-æMT›nþ¡iÕ°#d€ 8„Y¶yŠ:Ìψ×"{—ºÒ…h³“ub¯Â½«ÉEµ~Ö¡UTA÷üŒÈb!ÂŽ:‰œ÷ýÍ~à…ÞŠXTµíZG"VX ¢…;ÁSÖOÍÈEÞ=%¾ŽÓò§3˜Û9¤cí'†o¾.óÚŒ †Œæ%„ǚȰ1r“ý8\®mAÏ~BÛÆšüŒî§:04³4ÂædÛQQÑ£ \GÛtþCØÓ ÙvGDš¾®WàåËòK Ðë›ý´-‹´–tEázQ½—•IK웈‡Îz⹸ŸÊGèþ¡pý°$Ò¶ääiÅ¥gÌ{¡• å¸$ýÒ m¨j±ÅxüHfÐל¨QìÛQÂîºÎ d'g§6©xh:ø#ÇÑΊ¦{éI¡•¡KQ^ÇÑAŽ',[CëüZ+?§#‚!o Iúþ)µ ³MÙáŽzùs ½Å—¢ 4ÂóÕ +¢Q +ž!Ñ9:ÎçžF wfª˜}F®R‹t½ëçs¤÷(^3£Ö;q~} âÃþBDŒcÐp2`v;ó3"Gª"‹µæV~Ûϱˆ}å` +üB¢pu8O (e“N;jäÅû’Üðf}/J`jDZ=¬¨zí¨5FýÍ>Ÿ;<1Z&‚y¦Ü‚¾‡užÂN‚‡Ô Vλ\ôsúh4Ê”ÚÆ/ÊE?E5ô,ÖnŠ2-—Uñ g™¡hÕ*¨ÎÌÂ×̬±ÎùRa¼Âôc¤%ÂÊ8Y°Âw~)¡\Ò²‹œUßYJ#•ÏY6µä÷¨5nÅŠaÍ›"<û›½+n£vŠà¨t%,¬{›u;çv1èÛÅàq›o[‹ÞÞ)£îIæœØ£Ç¢6ðC9í!kµû5jè÷#|¡Í7Aåp×7:Ç Ôa¸Nþ*w|Ûnü,@; åó†" +ÝZÖ^,5Ž†ÀåU6~ÝÊdÁ7š]ÒÝ}€P¶lóè`Rž%Ýh*O(‰ýÖ½n}k!L½E<(¦`7 á±ªy‚*¢—v%5æEZãΡôõÞ€aÐWëÃÝP×F&|æê €¡ sÜYºC@Ä]dôª1GYo +gtnG»[ygúu¾_o¬œžäÔî¢ÍÙ¤ðqÝ1›èM Åý¸õ5[¶xüË0»¿$¨OÎýŸäÝÿ+wvàvßI¬÷?þnýÇøñWÿæÇÿoú`åÇ¿ý÷øÃÿúñWÿî¯ÿËßþñ¿û§øÍ_ÿïßü§¿ýûøͯÿðÿ÷7øŸ¿ù÷÷üÿô‡ÿó9è÷¿ø¯¿ûÇßýí÷w¿Y‡øéäî×|Èügþ7¤5L_çiúЭÒÕZ9 ®HW0ŽÀSÅ™ap½ë {Š0 +ýu¤‹(kSÉÉ_ί);•õ±¯êÎ_ߊ¬Ù6÷'0—§G³ºG­5×ïw”0Gª©õŸQUý6fñ`u¿¶v|èˆ`nœUÍ­¶LzeÙL¹E8x‹Âb4RíöuÀáßÛEК¦XÒ¯œIÙšªÑnt:®´CæÜë„ŠVç¡÷µ¹MúV,æ/²í5îÑn#Z}*kdõ—”¬žÑ¥k­ìÆ˽÷¿¥]ן”×¾VªVÖÒŠ —Õ…5q*/M„ß1bW"à¬Öä#¢2JˆÌ€&ÀzÀ+ ™A +!W¥ÈY-7kùŠ–È +Y·¡ûSöªÂÕÂÆO¿ì+š±Â+$ê÷t¦*ý´qUÔgQ Qžš Ì]) ¢±¿’®ng Ç?ª8òe¥»wmu%ävŠ]âPd^iõPÛVh¼, ®§±.EåBšÎ©Ó§k(PPµ÷zÔ;ÊûJ*wp킹ºº§¹iÔ‘O»,š!$ªFPVXòwõ¨g±åµõËS¿"ÂÆè´™®×þ€"SðÕ½>””'zΫ"Öê®;Ã:ejZ•3`Ö[‡ºM¤!cïþŠ=F (Wì¬5 ž ¦]#Z¦Ð¶n[|Ø0ÿ(|âÈ°Öo£Œóù;{-ÖôáVVÕ_¿Ñ(aÍ9]cªEAL¯Ì||y@zÙ»r‰~¸„F|gí ]uÖ‹zGYÃx¡À» ŽÏ €;h³æýWw†uÑ+—8ãAÁ¨1P›58ÅÒ@¯âûëKz\[݈/âd¶¯ïZ„™jŸåç4 :%Ç+JÚ©þ"‰âìuëì>¢ &À­í£¿t¢ ɵoù°Í”Àu£»2Q"ÖoSüTkzR›¿Ñáœu”ÊÚ7¢Þ9R9‡-n‹¸ÛL#Y;œ™ÊÎ Zƒ”iB aTÓø3BÜ …‡¾nõù<“¨+ò^“¶HçæU‘ó7Šiê7À™WlÍ(!–«é*v“ã©»ðá‘ÚÃ~b}><ëoö£ÑKJá Ó¸6ú¯7Ì(¿‰øî=þTºnÈŒå Ü™l.Oœ +d~ð}ëæû:¬ËÎcàye,0Zf™°5À¬Ñ¶°\Ô·¸uµÉîî$»¥øÏÞ…ÍC¥ÆÒÎ Lë×MÃ"ÊiÊîË:8½#Ëê‚|>Ë–sã¥#yNcœé]ÐÄšåØ×ø6µv$BŠ0]ÃbDo"6âT#ʤo5Î"2ž¨º;xã¡o¯%(ÀÉ"r ( Ž#ó:ÒbW\sWÄmåV†üœŸ¢%Ö7z¢ã»ýTä¯A~7«ŽáâÃ$c9’±œãqã¼b¬YYoüv§Fž ª–óqhF­w´¹jDœšë)^pïäi$yú—“°'ŒÀY†8µˆGø1ÕÀIi^´¹Â‘Á&ÕÚ*X`–…±d-¬üœu o±rA„—o‰ŠÀ rêFqñ¦¢tÜCÄب*¶õŠÜz Ý_äfzF­+À¥Á̾»9Þ÷ÅZy%ª×‘ý+µ‰ÜGÙÈðÆ0Ž/ÞÛ3¼Ðóa³P·.·6nDtÊËFÈÿ#å-@ö'ÖÇiCŒ ’퇒 «Ó á&–µ¨äÏrúMž5$÷õI@ &“V3«@Ë uÈ÷–C&dœ9F:¾ó=kð“N ¶Þ€‰ôs nw vžú£²x§æ®Ä3kKÖmçö*ŒßJµ_¿zŒÝ8  ++©½‘A3ò ù¹,²ëÁ£¢×ÝÀK‹JÑ͘ÌMBÕßòuœe‰:Í›¨~Ô09p?”‰ü–_:Ú†gN¡„¼zÜ9“o¯¬Ž÷™!oJ;%éK@ê—,‡u¿©8C`E·\Jdú5F#T²ÿÜ'òëýEˆ21­OºPå@}îVbpedEý~GáŒUùM (µþ®ë5®,Õ8„–¾Fýû<¨9‘Ë;¡æQ5bªÉøâ³·8r"²½~ެ孨H¬Ê¡6KK†”ôO<M½¶uú׊èVÖØ¿V’¬ìw‰£j‘Hr»`|´6"s\Êgú£ª³ÐAEå€,¾¾º +׎)…–A,¶Þ"²>)b×wÙ)B³ŸóTFIÿD[ €í²c»qQÖç<èfÓØämš’ÒàÏÇ%Œ(­D)7 +ÿÈ[ˆ" µ3(°#ŠŠÓ)”€Ê·¸²µ˜)²‰Ž ;Á`›¥t„§N:‘kaJËŸˆ¬_Öp@oìL•ŠÕ X—L@Nìu`ÖVL{s4:Ñ¡àÍ +æl§ç +#†ëÊó‹¿ýôSDüSû +›e_‹ºöj†a ç +&ßžZ$…Û–ÛJÈWæµ¾ÏXûž¦ögúŽ­¹úüf»™Ù´kDyÏ{QÄë–G‘¸ZqDý÷ZiO‹_)Òúó­ìÓitùD]¶·€\OX™«ÎÄï{Á±ž–Üz¯ŠVn—éçÑ¡d~dÑ—½¿o«ª)z‹õb+ú S\Bw»77º},LÎôfmåy?cH@H³ukÂ@„ŠœÙwE¢Øív¸Ô9÷ ¼ÿÝÒA¨ö‘š<·×a,¶Ã¶°Qq?l•†ªûircßFƈÙ5Ý[)m3â”FtPŠˆ»tÐl ïµç ÔýB“ÀÜ”9cÍ8H”PµÐn-&@ÜbQ{Ycê7÷¶»dÂyRµ¨{DÃøVßêö„QFâ¢+) ^º|x!PXÑ6r ÆEtÅ{DF«3ìßs¥èB>öCñÄb^‚Ú@wöù†8~6-ó)yÐØo¢`ãÑî[}åÍ?*^ ¦gÃ×hm7¸;k©•Eþ*5[x×6Y¿Þû‡•Ë +7?߯ÀÌ•\ÌKNR(Êøß½(ŠG¡w†ërm&Y½û8 ¸At£#™àzzxLrë2«¦ÒW)×ÝéI¦G]n Dë…ÏÊi]àém’~1I¯`e颢G_vWËùødøZG“õÀÔ°+Äœ‹}M4ŒÈÇ!CBcÁ‡k.ù@Mps7ŽC®ÌÏQú¹U9¨µšÔÅò84þf{¿ŸG8zR—ñafÙo·{Í2!ô8¤Ðñk[fœ2½–!y¹ `Æ\#6o‡jzø¶œ€ vqú)ü™©Hjã@+[:óñÑÖÍ +aÅzGÔ› +­ëIn¢ê¦V ?þ|ô,@4RÔ d½…§Þ¢0u,V#N“l E ý õ‰KÔBÄ7'æEÒÏуBäÒE;Ç‹¨åº% +íAž6øYƒ\F¾”èß= 銊±éZ¦ôžó>œÊˆrV:¶í%’§(ªà*4ZÆg#D\% Zš`Oîq ½ŸäŠå´Ž©MñâCIgÎà–à~(^¼înÝÁÀ! ‰ÛqAn½¯dÝBÐK=¶©*¨{p0gX7§N8|”WÓvj”ÇL¥ÍQ× ˺ºŽŒã9ÀØ´t:[´bªçö®£c ßò{FÌKëv®P÷Ê€ÓÙ?Æã)5ùcôrŠøÙ9Û{8%v­Lˆ•eÏ‹UT"Xw¡çúJ×.¢­š~g»Ñ R£­9ÍîhÛ$Þfºê9 ¬¹Éeñˆ¸è‹ûvlú?ú‘Ï)‰üZXfîZ×ñòâ¶Óp2sùŒö\çëAü¼-§Cn$ÖØ[]RÑõ*­bzÅÃ[¨” ǯš©ã·Ô.©„+=€}~³ŸV.b±[ïøæPP’‚mMö î˜äöÏùNta«IÔ:4Hð÷•¥Þmë+²Ž4÷°¢™V´›©‚/Ö“ Ô”å?¨ŸßD¤§JSºØõ?Oõ¯ß÷ƒj‚Ï•³ë”d@2ßP/>#~F“?¯×O1z4ßñhÆ›&¦ÊÚO#VuDO¼öÅUðßö3†¸‚äÙGDÕ·¦ëW2—oö³VNÐÀ#è¿ñ3j"¯åEù.‚Np‘W©¨ß>Ò{”0-æCÓŽV\ì¡`ÜZ­«¯Ïu±}v~F<ïÛ°B¹e“qÞOKÅŠ`ùIÛŒÑßz>’%ÄÊðfا€ŒyLˆk†"U¤è±ñB”l˜ñ‰8”©¼•ÎP[•ÿúÕdœ¦÷ÅAMZX:*Ñ®MýÈÆga<£ÇΈc§»ÑøeMtsA"Á¨ì°f¤,I˾¢dN%êM9P‘°|d­[•¢µxÄñWÆÙž/ªêQ`Õè‹_Ïû¦“tÑEõüŒÈS +âû›Ÿ7]#4Ý%IzüŸq^9¿ ˜š®ÂÞ]‰Ê³ÿ¨7Ðá‰Ó¬e0§+#tòÔH? 5µÙJö¯ú‘{Ö…•Ñ]j}K7¿íçND_ƒ3gÜœJª6ÕßDE?x:‡Ê>Ò[”úºš$óäõ¶>ä#…ߨ‚z¡€ƒ–ÊT‘ýÑê}A‰¤~F<ßhJ„hoM–Ÿ÷ƒeÞED` çæóÏ.±$èâ25žZç{ÔŒ¿’²è0sy5ˆ:knÑD÷FDª¿Úç°Ôõ‘[v)ŸÊUÊÏý¦ªŠ¯¸ÚvjÖwjv'Ou«yúT~™jW¹IÚ¢7‘Ã÷^ø³Vîñ‚W'Ý=-ÊÇíS3BaVa™ÌÞßÞ‡E50ZÏœ3C¯% +-EGè̃|6î8Š¿ÂňÔw7~òЗÝGï[}v‹Fðßíë»îv.ô©0¨œ=@àŠ(ÃvS\aP4ÍÈbJ4Ú¡ÛòøŒHòQ¤šJ‰ûزDID7xžBhÕbé³|q©B<£_¯„öç(„~OáexÌ̤w4‡]VF0•Ï¹”éÅöøb!øÛŸaŒëMG;cƇèq÷§¬5vú:£àïîÖnòd°ç# áFóW½yñ»ßÕ·ŽBv;‹j·Å;¶"¬ÔØý(¼t“^VÄKêMî+8¦W:U;<€Hq¡£®ÅuìÉ@%“³ç{0´Ó–MËýv}D­YK¼«ñ‘=mÆ©-ÐzÆ€@"­Ðl] ŽS›…½áõÒ¾ç‘ÞãëW´]i\5¥` ‹XYƒvÁŒ?„K¢9N‹ëÒSc]i'ÂÃ&•8y‚©‡ûh9¸])°¬÷l¸öÄ´†å¦5M?º ­È³5½q<Ç5Ób‰ù¾ý·O3 ì!—ï;9zÓ€@!y1H(pÍrß%ÂGÄ·ã{TÄL.Uºxk?—ü¿QµvKÛžºÔªøVJ¼WnDC¨Ç×ñ¤—xÒmÜ}¦c>Ý„AéTç-׎llíçØbiN-û9Pª¥OÕÅ~CMmg)ûš¸LIR£Y½ÝKªzokpÀì§õ¼³‘TûÄk¯kà¶×†¶˜tfèf@;¿¶U˜æh TeV ƒ´#:-fƒ¤˜u„t©“—ZXÍÔåÆûÙ¨L䟟[-WvDÛã‰h;âÞ¬†ˆãu õQh*¶+2ùI(èm›å„¡­ßD”nYîŒ2ËkÆ{ QæH+Iã£]¢ x£ÀKEBj}ó`?EuSmgôøQ'áï'OX¹âvlôÁʈӤ__n fõEAÔ`PG»Ž:U†öÉ«ˆÝšÝakJ(}«dç=×ÃO;bHör£²‰ÑI÷çPp[ßHÑ"¡‡¶ÏÏ>[ùg”hà$`šM¡¯ý[nXA§šFG‚*³ss"J5‚;F„F–ââøi1Hŧ×6Äÿ&‡Oãȉºatá­YMƒRÀ»²þ ¿ÞŠ•ÏÔØÞ*ÏNse ø΂å=t¢#¤y5˜WÚƒìä~:§‚ƒ·”ðíÀºõR†!°¢Ó£¯¤QïÉ-û. Çï®1zNü=ªQd£õŒvÂÝ}‹뤤Õ „Î,¼”‘HX‘f)@yKhˆ\ç~gšpïØHŽ„ñí½ì¡çJ³/8IGTÎ)ª®;’Yj}g=Š°ç†p®ýíUESYÒ3˜-btJf;bk;%Ò‘ˆ® +íçaf´"°–Êq„¢)Ø<ÇÁ”KJϦøó‘ûÁ‚õ”7m•_ðò åÐê–d ·U÷*v×íàô׬u­²å?@"­½tJnœE‹©v?ÑÝà«ô¹‘pÔiD$ÆñsÝéñØjWû\coÔ©‹ +ÞíÏ­†’‘I_Ñ/"âyAÅT˜-Â;Ö”g•iÔI‹_{ÕˆåÅ9‹¨Ou#u5q´òÜ(áþ©üKN¥E¶ÇTµ†]xŠTü)¯:ÆFägñ +ågP|m€äã%ßèàï×æÙX‡ž<¸©tܪ™=ø|ñ8Ë‚‘†%ƒÃJs´Õ¿§4¿Å/߶1·Ê3ÿ™Ïþüù mFð‡šôlÃÉR,.=×BÛ~J}º¥-ÍèÄËah5¹"Ö2i†PçUŠó-"Ïk(¬v#;]Xá~ìÇç¾¾žû tB‚"È3Ž¾ßD EpÆþøŒ¢ßéˆúýûGü+âƒ1bæÃó~Rî_+‘NIRô¹-œ¯_Þõk…Tæ7_.û3zT ·@„° ùGuG†Óß®¡joÍbÊ€h—Ôûz:½÷Nc²m›É­ò Uô¦‰¸,A]¢uÚqÚà  øW5x }}í’ÁÇC%ÊÂõ)¹EªâPX¯þú»· q5Å«†Ê· }Š} Õ|OÓº€,µÏˆ<œ·´ís?0óÀ“¨ovOE•Û’`ªß| Ú~ 6¶ë#ˆÅ!(Û‹);÷éBO%zJ½”¿´ÿ¹·àÔAz‹øíFÁÙ}Ñæ› !Q§(¨›U£º§ø!ÑS E$Y”ÒÖ­ø°8±d¥¾n8¨)Ò‹ÞaÝq•BPdêßD¨{ª;ï~øŒ¢W»~]Jšµ8ÈêèÄ1«™ÊzùŠ09‰0¶êxžri Ú4Šµ +âä'yöcD„ÓN Dø…€‘W"§¯ˆˆXŒ{ëÃb*пŽä4‰5´(¨“FÊ…néçÃÓÊ-Šä:(@ár:†p¡ñƒ â¥#[ïðj×€€Ša’Õl[Œ4…­è¯Yò°ïÂ<3lÌfÚ7w—”‰pZ¾¯ÆîOQ!E[Y!¦„5âFÉiF¬h¬ý:#TË¢ÀÁGÄow®¤èøé:¤~³ŸòTÔ/:jD®uu²{@ø»½ ?m‹”e”¥P-HæM×uƒ[úô €s}žˆ«€~Œï¯ÑÒ +ÄéS•·÷ý  +s1}1üõÈ›ÚõU†Å)ü¬wÆ-î}îÛÝr»ßp¢ÄMuytÎÐ]eæÂŽ +´DЬT¿ +óà à±î#™ØÜ7M#4þ›$l×ב2bïÖ›ziðVÔQö£ôÏds+Â)M’€õN ¤nÒÒÇ=ƒ8= ®OZÂêzæ<>Cd¥2¥?Ë{OFR³S¼€Nr³¼Ð¹+-˜àç-HÞÞ¢À `lŽ½ÀQàÞvô +’WV$‚ÖËCOû# ‹-õ _ôRáïm/=,c ½ëz‡ˆû ŠÞ!RôXúîPJ`Â0¢d=dy唞‘ˆùð–Z<@˜™®¦JÉAFNoŒ"Ù‰QèÖ§fIŠÙ´åJªP‚ä<´–x‹úinhY¼ùÍ~êéŠáXœ+ +ÜÀNyë?#ø¾„ßNÍ÷‘>¢î¸&®Áþ0H,@ý7ÔDœ°¥?‰$îDÉkRDÍ?•SnÚ6íT® ¾ZßK˜뉰ÆÖè`—²EÊexQ€ÈMÛõÖס…൞-PdoA½m9òcˆ®(§x*d °Ù”I^1¡,Xs*8çøt ´A©ˆ§ˆ +¦UÇ»m:-Ú§}Šù >­÷¡h¢€;ŒAWòÛ„«žËøØØT©Â½ñ!€T~æÛ´ŠV÷ìÛŽ8`¦µÏˆ`&=<¡¨Ìo÷³rnÂk2‹ç¨ŠæçJa§ìÁÍðœÉÈîò<Ô•g»·¢‚w»·tÛ{Ôßìó±³Ø†ÌL ô™ +@wëFU9—2sPúQ­»áÛ>#ºx-Æ­51‡øUftÉ=‘¶Y¯ófy f­ZÅ›jmÃ~ â;Áü¯kË +­PýÁZz–oöCñV,Rµž>#õ˜ò®ƒ¿G|LÍ©X¿G­q,´†5NØÊEØ,ÐnÀ‘€ÕDûãÇØ ÈŽ%CJQ²Ùx 5]8MêAqD×*  ú…€.Ž°‘Aˆ—¯¾kLÌy®ÏOÔ1ÛFú1ƒD¥à ‡ :Æk Žrë+MGžO³]*ªÐ>×:A¡Ú 8·ˆ8h3PTA‘¦ïöK;PÐh㡪*褌òc½Ð[Þÿœ.ÍL£¨ñMÈQ»µ7²ŸgÀâ».|{|ÑCW¯~Úþ?£ŽhÌE\ëru<ŽÈ2 È™A¤ÇPLjâ6³"8úV´Õ«Dÿ˜µê)»{»ëx`¬s]¤ß·ú‘WQq²³¶n?þ,æïÊóÿé]ú/Ùæç6ë „Ju¸ƒ•Xƒl¼Ý}ÊF¥ÆýxvÒ›ž6Èù¤•¦°mSˆ»•.#ä#T$jöa”¾gyð×BùŒ…-hÊ`‹ÑAÂpUe×0^n‚hÚ¦–—µf=ï¬T«’†+÷xmmZG1Ê4ŽiØD^?>´ó`}J?ýQ…ÊmžÁï()Ë-¦¤YÁþbÿ”[‰jž‰’6¨btöƒ!,hðx&^ RÊU¦‰œ>Ì¥uúÌ`{ÿC >g{œ°oÌuÖ’x¢£‘¼á ÛÈ‹jÞ‰ô zTɬ°9wjpS Ì[Êò¿‚Fb¾9Ã`l'ƒÕ†TM‚èp—ˆËÞö‘y‘JzûXâÒ“‡_5‘®v¯oµYegªïöJ‡Jܘ6F0ØÆSé'ÿ.hlíå…“ +`ew¢WˆŸ`Aÿ²){ Ž•±ˆ5D©Á›5‚«*|¹AÅ~à…d8G•jåÚüˆiÕ'e3êصo£nRô²Â´áZ±µ1ýù¡¿P0•äԜߺ#}f/–j¤[4t@9ÆîÚžpGߪ%S¼ET *‘GÉ ÷Ú%h4´$Ü­jEdJÊ$ÊŒ’ûØ¢'v¬¨Çc‚Ž¨Báò×*N4û 0‰ü§õÔ4¨QØš~½'3Nï°ndU³Uõ꯹2¯UCx/ÞFb´cèK`Ó{ÖÍ\œÿˆÐhÔÔa²ø + JD!2cÛA­Ø2òU67Ãá ï™#á&0¦Rÿ_0Aºëƒ¨W1J…1·$ ( µƒ`:K¬ˆ.$Eà­*Q8§M„€ëA]Awd ;=ToC¹MØ34+Dv^žc_ÑZ9<’&0j€{ÆIVÒ$¢ÚçƒÏ¦… V~¥rÀ£8GÅJë1Þß•J äŸo^Vt¯j~Âtê<ÞžÇ=íô9ÞÛsM”[›¦}z.Öû(W]i4 e5šò©ÝÓy#‘Ż½Ԁ°ÛÃg[:øft_€s+±ÇSfêåë!á¾ jÊÉ•½0•0 ¯dFòb»(¼ó‘ w9 ¶]°C§p´pP$£æIsÚF0Ž"‡pe\'îØ° +kÝA~‚d?Bà»Qº +?ºlWgx˜a#$B¯…`ÛTÊCH=¹‘y¡kó¡SØdº$\N¹Êž–EàSîM«<7­’‰µ<%ûQ{=6§aðT(j¿¢6âþ"´cfh¹ìõ«¿–ï†EÙÈýÐs…J®&¿Üâ}¢­ñeD‹‘ Ù˯Ôãµsƒ¶Œ·M×Ì[@¦¢Sy;ôÕІP-ü Ôz®´‹Ä–¤Ë\’šã,©ç„×t°|-Öè›Ô§Í8-§þÔì3$}b65}rÉÛ·ãJZÌæ6Q{r¾o%€s0üÿ”tÚ¬v©"zé¡L}@âÑ&x(¹a/zù‚ >E‰³êóµßì¢õÏSs×q[ÂÖ-ouÓ#XI>¾‹³m`'=.œ¿n0“Ý Ôå¾wnqè1Ê‘ˆ&:p=×c£§U¦Ys~(Ó___ÓŒ_:øÁe/:Vì™Ü®!ã-Y%ljlî°t")®mý$‘³‰}<¯àªCI17ƒÑT‡¢Z#щNÊ­Ú ŠK 3Q©[}·´ån,Y´éŽ¯;«2­ïÖ k +¥è0ËïFA Ô+ùäI5¬ÞÈÐW:Ú'ó*©ùÜìÿ¶v[bªÈ+jTo6¢6_•°¼N½ã$fw†zx]Œ‘s 3ÆVø‹‡“Óè%Bœ ü»9µK휋õ@_ÑÝÓ3åŽsK‚ªŽy°²~RgtÐö²å”Û¥ÃSëƒi¢œk¡Ëó(‘büÙOã×Á¯‘¢X÷»‰+®ºá0¨|M!õ÷; +gJSÖÕÇF³nA·ô†ØDصhh Ø¢—:òÄ̦ÔìÞõ¹nhß/úijE~3ªpÿ.ª¢Vß \5OeBÍPö›$cE¬| äT 5ÌÍXµ+¨|³õïú]ï ‡Aû¯ÄšMÌûºe—®t:Û}û`Û h¢Ö³ +í Òµé…C¶Ë¢àÕÅTuËž[D™TŸŠtµ”l%cÍÖ’‚É¢³øíÐG‰‚ ±„ÒwëŠW¦È–QY¬†+Ú™ÄÖbÞŠ++ùà¤Þ¦ì×c¤ŽJS1‚9Ãåo/›R´^æ×qX¶Dq({ÙèAÒ$äWÇc!Ub!ÕÎœ€*Ïë—4šÁxIÄÓ¨ÂG0’Î]‘ßAä<íÚõú3õzsÐ+.Ôk/­ƒèçöÿÚn–Tý^í©~ïŠf[#^×EÛ‚û†Œžåe 9I·Ó¢m&.g—öXÕ¢d?Hß.º‚,âî¬Äö8õ¼;“âÞQ.¨Ñí\;‹zwÙj§Kè%Ý,‹o]Wº»†wGyŽ´îæ᜔ޖ#á‰hаF£\²þ¬s;gEçc&n Í @<åÞ+W ¶ÇJÞ&…J•]|’’fe“ 1ÕóöÖX=5ßj€+y”'¤¥¨F»Uùó«ùyß*· 1 +n Õæç ¼c¥ÎGÛ lnÙ:Í{oÖˆ´RI™¦âß³÷C k½Öj†6ÖÂx‡yE`çém•GÅQÛ¾,ò€ ”þ«è«TÓƒyø €e„ÒüŵnéwÚ.±ÈÑN( #jŠa…:ýÏyK-/6Û>=úmuåÀœõ fF4ÄX÷ÆWul4³8Ý ¢/†ˆ­|íWþ_%•éqgcI'¯¨’/'ŸÄ‘®Z,gdrNÙÝ:ë`¸·s@+ÎA}¤ì`³†„ÞãÏEÓ‘ìˆBÛ©¾ãú)²¯±OsÒÃ4 ¬ê´'‡RÿL«¹¥8á³lÌä¥Êúú9þ6Q2ÛjGgGï–¶wÛylO™´9×i†cOìK©r©d\ªÖXö÷"-;ùþ´nkÁÅQ¾”BG^p-¯@ÖQ7ÿIszI¥òB*û–§¶m<ïH·JË”Æz“‰«[Àzc¯#WŽ1$2’¯eþ…FmW¤`å4ʃë8bù‰OºšýU3ŠbÍ ·l"¹ànìš»¬ k–Œ›”U=hä©@—©§›%þŸW†gQ¢åIþ˜—€Tòx—_U@}`®¿ëØb°§UË–ýbõ„{ÕãUÃÉ·H®L‚‚ö2âµ+“_3ˆÊ#2epŠGßk®çϦ6ÉeMÅÄ„Dwݱ ¾K]kïk-ÌÒÜDªÐ @•.ÁñˆŽ³î$OZó’koçÚ+ÐuŒL3AHj\phõ87ÿ¨•õÛ}¤µ¥ÃG%%Q`l­ÉÑé¬ek +5)ŠŠ\›]­nàm„T4Ç#¤~mUk$CÉ_<×c“r{í3^QóÌ‘L®¹Dk{œ‹²äEBu` 2€8-‹q)šþ>Ω­,>í#’h§4•å©×ý ‚všõŠO…øø²Õ:Öî¡Ù¬o²ÙƒÅA=*jž >"ÞÈõàVc§<Ñ?ÖkÒT!jš •ðKIÕ!máËSÈ™Ý_¢ÍÚ6U ›ì{z¾‡[¥»àP·aÇuMuTÑÐTGé•ì%š#¹öü¤vD¿uØ9MâA&¡¢­> …`ul‘cÔò å¨{dÈuQf¬£ûÙýx,~œo±WÚscµ}©ºß^ ƃ·Ø}gýkó®#ùžë€±zÂmÞn¯Ì§dÊýÓe»ÎÎÓYÌNÁÆ®Ýe¯ïªC·ÇNÚ„ì¢úb“u‚ ]ßzŒT×6jg92²4Jl ™ézžªÞS†âs#5éÎ\§·/ GÏ¥g,FÚœâÁùnrî/"žæ£wj£|«øT½ÌT䮀²3%)÷[aáaÍ ÕTx¾ÛNŠlÚÔ€á%wª©í¶ÁÉD³ Àâýô l¼]6YS‹”|Üê+!|<Ôê3Bƒë% ¤o!:íÔTô¦Ž€‰Vº#îÆhëþó®ùIÑA@£ƒ¬šÑ΃ØY¤::² ÄÈ»m1exF9~î%QV·pͲQH{J\ßF1ë Cvn}i#æ/2¯#É@¡èwíbÚבHºi4μ€J¨ÓamEÙb{HDš›ÎEÏ©lêGëJE1r²$=Ç¡ÿLI¨_= ±ãÊnŠk#ESÔA[š×Mq.l˜Z@qvËUÈ’‘s:ÞÆõl]´é‚Åâ,Ú c¤‘õ…~2N¢”—¡¬6TGŒÛ¤N0P<©»·&}¨ßº³Ù1fµç³î‘C_´¬.dúÏ‘ŽˆíŸ  ¢è/ ‡¯Œø@€^9±ß¶-€.TÑ'Ç9jªÄn,Œ•ƯȼßãþMpR Â)ÝäÚµ-@³FiœJº }…‡ŸH×”‘Œ^Þÿ‘Û±‚’(mÍ»E‹qMðc +:åì°)ƒ1ƒT;ò–ë·ŸXJ½ˆc›kí»F]#õüÃu>G +ý³§·}å²ÔÆæÌWMDÑjœ›ë=,Ë Àôh •Ø.±úŽS\µÌ igh)a¯¸ðJ€LZ侘ñ[;TF–!) _X—â +@Ú¸ZQQÇ´)Ÿ&õ $e›2‘³ŠŒj»‰s™Ô‡ÒÄ™5ê@éJÈ”ØOçÞìwÒª3Ÿa•/@­ˆÊìñ`Û«B¿é¹h>×ãÑIàrVg”¯¥X9 \[V¸Ü~e­ìg‹-øˆÈÐ%<¬°| K_Ô f¶o÷¦ÿzçh/<Ê„¢»Ë¹‰Þ#DoE"zrÕÊ•$ ‹ B“ñ¬½÷š®µ‹òHq-j;øã5%N·®dX1ïuuÕuòöEŒëÍP‘ÇI%§‚Ÿ"(wTA +\]và>Ò–Ø­ç–é#‰)j¿w®H½¸jòÓõXËnÔSë~™Øl®/ŒQ“÷BÇ“›Kb¿e#¬’Ä‹ u–¼“Jt +ô! !Ôãá<F+udÈ,̧0¼ùC;_ó R_kù8ãÐIuƒB°ú!€(q€h6i–± àÊoì¼›]T ï·Xô¦É+£’~@Ùy0ôéÙ†M•1Dè•hÏä?%5ó÷õ¨Î¨`¸N¥\«[ôUEl.…ó¾¦vÆLj”Ö#E±HsXÞ~a'Ü3²;¬#¨o(uŒÌ-lö‚7ÍaÝ +}åa<c‰Ã 6:„ë•\‹†mC““ì×1‹ ¡üŒgY¤$®|‹uõÒj ºÔ üu¤NYsç¸ÓíÀtP„òÃ̆¤ë„úæð#³‰k/+ : TÔÁi*_JRúX«‘ þ¨ÈÓ©UvX|7öj +ê³ØÔ{þA"·¤+°NpgEŽ)ü‹U=²ezÝК£Ù§ +Bg•h‰ë'hsñÒêqd뻊K‘q˜A‡›‰œÙr.GEïÒ¿<‘Ââ,·T#âBt ê}º4‚”ûÂ×àQ @Ýê„KëÓ®{ÑÓò„uð$ÃÈn²¢ì¨ÿm_Àhó*‹êMFªsÛÝŠËì‚«kW¦‚‰*é[vS:È*°§gÚcL´¬4gÔGS‡š\”Aή‹1žØ÷‹¶!ý8õ7ÊÂ7•£mùm…'×¥¶œ‡í¼œûÞuuúuíMD:´.°=ù|€f”¢YDUܪm‰Vë>ðä‚óˆt鸢„n»Ä S{“Pjˆ"W0Uºš{§bL¨é~;„k¤©w”LR­³¬¹MÏ Œ‚–\%©.uû~4]7ñ ÐàöG +>H·1‰ž—ÂTÉý„¯ éAvwµ}7+u¢.;ðÍsóu$ „;ÞÃ|K ¼ML=ùŽ;£<ôª®–bÉÂ&®]CúKd-6˜¿o²Æº—H"H‡M}É  ;*ðßÑ'•3"èãÁäCî¹¹ý"Í̹/¶äM†–KhŒ²± yò,QïvÑWâK8ú[]c^Âu{d2ØÜV}®;u’õV®úàüHŠÑ­¸ÿ%{¡ùpʬ¶Z̸âSÙèuû™ùüF® À`ÁÆûHsl•õuªS®ß@µ5•Œ+î⓵lž|û¹>[ò°„õd ÓûKŸv¤†ªÎýöhŒª¸Žø£»–<Ô‰8t…jÓ’;ê9³!Ä#í?Gq´ÎD·µ•Ð©G‘`ËyÒ7ŸÀdw™ÞM³2@6ó5\ê1ˆ2Ô9ÅI]rñÄÑë.½.V?Íkùø(xÛ( ²À@V×T˜÷pÖÓaúoW\)xüMåBVä@B@öH¤ŒW¬Tq.o “Ôƒ|ŸÛàa·¡¶kÉ  ÁΡï~Ño“"0ÌõÁ q mOƒÈKxjG©°®Üò ¢ÍþUªè± ¥¼Xk]ü7ÿOXnVæhÌ]nCÄ¡/b¯ Þ$ò[Åä’){‡°ß* ~@øh|l`"Ž¸u¡îªÛtª0åH®ÐåRŸ§ë–1_è# {Å…ò¸¶P;»é‚èòx°ÉÂÁþ¶Æ²¡-J ·³Ç^'! y=‚ð(zè¸þ¯÷ÀÖé¬(ÀMÖJ²Ò%Iä@ 9)G¢¢\j[³j;A2]B~‰Ú3KÌKÈ/${a¾Q§yÀJ´Ët@…*?UÅ–÷ájMfëºÞâ@ÖR듵«²±õê'7„ÞØxú]Ça¿kª$zŒ˜UcG(ÂeŽ ÿ{!+å°"€_Ãn ;¢‚^ óFrà{)õÜP·¬Y)§|`…A“AË|DfgüHÖ q^[´n½å¨1¢¬Õ¢»·öÓR-9aÔèÇ;UÿqÚÃ_A;Hbš4P:‰f>’Âu­–­O;«œ0 ã÷p–JõÒþBª¼—ÞS·ÒÔ`n/gæÁã˃'nÑhc ^Š{5JÉÙÝ%£¦ê~œgè‰ÑG§ñgaxIÐßÿû'}îÿ•;ú0Ïÿÿÿ;+wqNõVwÍ>‹-¢,ÖgÍ:æ÷;(Ã%†JßýäÍq^7çLÅF4þ>ï}Ó˜4puÑQþY­iwU\HÌ“ŽNéê Ÿÿ¼e(0¢èÙUj{pèΙ¹Þ›£tó‹å»¢ÖST ‹UµQ´)ˆªúËÑ4õëU¹o:CØL©D^ÛÔ}Ù‡¢p ÊC©—×YS:*{Qvµ0RÜÐ+0uè´RTÆýÌeÔh½kNCÙ7Ç3Ó¶mÎHËò¢Ýr$]Š¼6bÑ(¿1Àt—Ï|Úqý“_p@i.{oÁ9 @[ ùTÿ[ DñpßàuVsLMª"a¤pž÷§Ë”¬©T£ÖÇA”––I`ê¦Ö“€<Šì•€]®`¬²,á +’½†9>µ›ëŒ ¹}[ÅÎ#T³×dEVϽsHÑu~„o;P¿u4ÏmMzFï:Ù*“å±9{}j ²òÞ 3 +xžÍµóÏ¿ƒäNÏè7ÍBó|l­ºËêœÊÎ(2ÂàoüÒaÇ÷(ÇzðeCg3–­ ­Üx1±F9| hV;ËTiV™¨/NA—„ë3Â,¡$§ñ.dác7V9!1žÁ¬þÐ~ëÄü|~Lî æÉ20‡ùÚx–>‚ÐîJƒs + j‚ò(²½»š#Ù³x"ôòõ+äÑÁѯñªÍ­ÄßôöÒÂÊ$ÈîÐû\Éðû#sí˵­+[—>¸8W~½ÆM­á|ü>eíÓvK/~8â­UàHD0ÃíCÔ)ÛúΖA]ü_z”]Ô•[ëÚâ?µ&&w#ˇˆËKëÒø08P'£Fm7ëB‘f­6–Xs‰ƒá~RgÅ6¶µÂu¶ï6´5‘¡ôpÝߣìuCç‰n¾†ð…ÄôtgÌâ +`€ã¤£¨Éä5®qîˆøi5ÆCïÛìÜ=0H.&ó4ÓHíÙûçöï^Ô(øH´äÄ©îï©È_¥–­•ÂÒU•ãS’5éîf å²&dŽ¢uf£4)i•Qq£6@ÅmnÉ+ZHû^A`Ò +Q¤º +îl­LÛ±+wCÿ—£ÊÂÁ:àÎûŽ©B±œû|Hl¤s ç­ªáÞtÜU´\ÛK‹«¢ÝŽ {lÊ­`ÁYÏ,Ùpª!Ç ví7}Úí{kûu”4ªnåòMÎç[žZô0©ßAË‚²ðJ¬˜Ü’êÛgDÊO!ßltùg]j2Ù<0+ÞôÛϼSÇ´£à[2q‡ƒèhÀ +QV(/Ú­‰Ò"ž½sò)¾\eã8MU…GÑ;öÏå_ÿbš¹‰/ÿ:ʼ¸kìôJÄ^°‚ÿz&ç‘£k*EÚc²4i"¥¶[ƒpÆÿ|¤Ô¢MÆq†uÕÀhͪAðß«²–Eõ!„rx…ŠùY‰)‹u`)pµŒÐ X2"l[šF]eŽdÿå÷’#i·Aùìx0¬W ¦fEÉN4ê_I'dÓ¶{²ØV`˜½mÁî(=×Sƒg"â\³ÕGGþVx üJ‹‡Ëe™b{Uç½ÇÇœ»QZ,¦,vT«7w3–à$&÷ØRQŒpñî—±ªÅó•$™‰›†hk u ׇãÄE΂nß~ŠbYIA8‘oµ†àª˜Ñ­ík\*à/Þ„Km2•vÆŠö'QH_ 6b˜àÔûq2C¥¦!iEô>! +“•9PIל²v_DI%,•\Žºp^*ëŒÓ%# âæ9BG€“ÂïÁ›mïÐ5&{8Z¸kt«¥3šKêTj(Dü¹Ïå×ÁOTJ‰ÊµëÃÁiÄMD!õ|œ‚úIˆ!FmÄà2¢ú-4‰ +×%5šÇFAq‡‰( +¦+ßÝ —V¾[ç¶Wq¢¢Ä6Iü¼˜^ÝÂÅÇ£·†KÊ|PVtgëþ)rBàxµOÛÆ|­çwÌðÛ‹Z›@ÀéŒSš€2ݽÛ@wÁøèg[P¿)j©+ÕÝå“ßìf ZÎhŽcÏ×Ìm*ü5«fô¨öÀ-ïÖ®¨Í>¢(ômYQê +R¸¼·•®ãı%é‰p†àÄîø¯GD‰`dºnüÂÍìˆjýÑRffEGö“õ•´â}$ø)+bŠ<’Â}ë1¶tÑ/ºÃ5Qïújƒ¥õ„=¤ùGXˆDg„x(³…€`³Ò6êî ’6˜NÌ¢ôé;â—ÒBOÝ _s*…ÖÛv^‰ˆLPY (ëP•DA›ãÞ98EaÚ¥“Žeý6‚¤¦©N ™p¿ïQ34=+’º‘à¦CÓ©Þ¥`ž•bÑÖã|Z- UêgÄow'Dý™õrµ|?ö³A˜è  ÖÝ„B^{©)ÂagÐŽaØc ÷È}FñIØ\E“«±ÛRÜ|\ì3J¸žk.¤¨8¶”0FÒJÏd6˜öA׹ȽÖ(8´afÜHU£ñQÞišN!º‘t•¼S¶šÍ#\ÃZ}¼ÇÀZÇríìbzàñçaC{-AcpÏAcD¨ûýu ]ž²Y¹Ó²Ù z8}mgN a´I*,/7ô7Ò$:W@aÅöÓ¹5Ÿ£zr§F¸Þ)Á*ª8é"¾»ª +°Z«g:ÚCÏ4Lnu IUF¥"ÁƲšŠŽ¸ííá%˜ÕQZ,…ÓxYeº¬i°Í&Çšmí¨AÀµÒjÌO]Kº%æWòÚ[º¶dÜD|9 zº¢Æ¾¶uîWÅ»H-­24ÏôPôp\ϾæÉœ7 d"z™† °Ò‰Oz>Ó5Á¥«\PÊçYÕ5¸7°½Òúþ|Nô—\$ANRÐg}¥ÕÞ n9°‚[•é~Yi-ØÑìw2Û:-ò#~TÄ:ÐFiÂbËáe×Uq]+¾Šb}?Ârb;¦›“uÆ×AO³ÑÓÉ®â4xº Ó+bD¦Çµ­%êö¥!Be‡`ˆŠ\ßUx£H×(º’|r@ ¸Q×Ú·_ p`â|œž$r€¢Ò»Æœ_¬2á7¢ï±]%ÐU[©Â=5÷‘|@õ²‰p ä¦rm¥v óV G1¨uk5±I+#ð5˜é'´íE®­üî¯Ï–65ñýÝ^àIÍd­ÌH@3“£(nr»G¦UÒ²k» +RõL”CG¨i¡5O¥¼ˆ4Ÿ[&ÙÒžê +Ÿ¯´§íW¬ˆóAú§“¨ºÏ‡Q‹¨Ž#k¦'‚¡5“9&Ó98o¯‰€>¶ïU‚Ù¨é¸\I™Ð.úHÑ°Á2¡×ä36y' + R–Þ#ÆNHLÁu£æŽ~ÙÔ| “ Rgj0*Ø +Yµ,óÙá\YþžþaÛ‚8@m®¯ßIs9Œƒ8^˜ Ã?0Gäfâb…"pÕ.‡AïË.?9œ×æH‘&ØÙuÍ0ÐÔQçŒfu÷R;®“þ+Daù ÌÂ2½xöž‚»©3]•F¿˜º¥4âä+³Ùˆ±#îÇÿìQðzß8™±$4ÜGú§wŒÇa_šÜâ¯.…Ÿ`.öÝZƒ@×Fëis‰+_Ê<&g¨ÞÐÈ5 +\BÁ8÷Iò¿³žˆ?õ!-coÜ\MZ¡S³AKº +2hf¤‰`åûj°Û¨=m‰ŸFieÅ°«3TBF^k½2!gí‡åy—ù—2w†%ó±ÄX“Ü”Kš.vÈý˜O™eôxcÍ›åëVRiºp^Ÿ9ÀŒõbÙ&ñDÍ+Q$ß<[¹=à~ôÅD æ ­ƒ\À(ùqŒ+<æñ–ûˆÒËæìñ²éá4 +ü€öG¾S0†õR}äVg_éóþŒðHe ½‡lã»ýè¬ /Dí + ‡žO‹aø»ˆ ¢Q6xtI>b(CÓã¢_9_MÒU€“¶³Ë:k>,à~鶡Ãòí3âùB«w£Œûþf?ØîȢÕ­d¾P« ÅÒñm‹¨à”(¤~ÕVï7Q½w@‰¤XДʭ2w©åŒ÷la9·¦¿‚¦Üèþ¬û@½ï3"÷®~„w.ˆ™Ïý< +ž0ikë;ájG.l£ÎcPVâ¬pÉ}ÅÕce¬÷|pdÍtoÊø”p´Î½¡ÂJl…7?~0°ªZQ p;}ô8}Ô× ¤g³hM)­É˜@±È zàĪŽ@χÐ05³pu®KD;æ+K (‰RàG”Š É-幕þZyÓÝM3Þ#Ênëk{ûî?£´mâ ¬š441a ˜¢fî”>#¾ ?¢è{¢µ‡ /âPõî/R)”K˜ÐÕ¶—L=šAòÆ6Ø +Ñ(¿0„o1„ç«…ðVýeï¾ãXµî*C™4Œ©=k;åTœv‘¥a©è(»Q?î ºLÊ3s[Ï»ò½Í*:ãí_ qÞ©Cÿ¼ËHŽÝ?_“5•µz‹VÍ<÷ºç@Ô¤Š‹¼éÐGÝGNqãŃõ¸É® ªsQÌ!ˆfE;žŠý÷Ž˜‰eÄÜõ‰ßOD¹_~JÇ–S·¼#ÅùØç+|¥#Ôçí)=E›ø;+_B­Í‰@ÌRÉÃ|nÛbM`ën¶pNE~– _‘çT7‹¨B +%`gj‚´ÞWDuµFf”É‚Pð(~Ó¦|DH˜èû|¦`>‹ðŠÈó¾KÖ(5•íïA˜F¤/›TOYNþ¢„ljxQk(´qñR+Û"Öš|îýܱE¯‚†Ãú/öë°| "6bÔbLLP~|¡ÙÅ|på +T­90¹Û†A¡‡"óqG?çZ¤S&¼ãªÓÕTüA|† ¥ËÊÚÒc â2IîêÆ6÷c¿çÔ°Â[– oõ–‘ºþ`;ÁÂÇØjŸÕ4¿F¿ì3bÝR9Ùèo¾º4G<Œ"S­»ö ?øÜŒ·H® 6_7¨œô¸fR â’¥bˆ;Å‘¢A-&K¸Ìƃo ˜ä{AÊŒœñ ZœÒ¹mw ¬áC vÇtãV•\tÉŒT°ƒæmpÞ¡íƒG-é#Ò´hE µŽáˆÐ¼DS%”r,µðpÈ1=Ûõök“Ÿ ,ò`BÛÀnRLåÿ9 nÀkÎüÚjqAêûÊþ#$v‹Ï›Ÿ—üZ‰‘„&y<2ø5LlŒ±òk0P.«1‚éS?°yÁ´™1ǽçDu÷ìéhaA‘{rßë‹yU誡¡;¨DS©ñ‚4/$Ø#eWS¾áû®!=ÐDu¸PVÇÖ)£ä×a¬¡Øž*9ŽlÕ{Æ›Ð#‡âH._UÍFãÈí¶Šd€'Û5ZŽñe"&AÜ!ÑõŽ Ê‚Z ȹ£çÑU.#50|™ÛC_Xû¦*U#Ûæ>_Û½Àbev£ízî­ÍPA2ï 7 + že­ûów÷ +êQ]“u¼~ƒ‘tƒ3r¨¡w×hØSûˆà[ 9¼ÄìGˆ2Ȩ1;=mkœ)~¡xDШi£;Ã2÷†‘ Ÿ<Œk_Îþºê7?çE˜›‹Ö%$†›×†žbÂ\ãoÏ ofj|×gƒæ&l²öl)ë?øRîc¡’®&Ýñ‹›œ?Å;d=Åî…O‹‰We„Ì­õ» +Qcïí²²ÑŒ…¿ G-ȆT%y‚·ÉW ^S]<ÀOF0½¡~_½£ØïPy–×é:¿TƱþgJ Å&¹kE&Ö8gh¥ ã• €Ú{³ùô¾!7÷=3ûøùkT@X·«ƒvPý‰ò&*E·¾ìï>Åž§¸1/ŸAA)š à}ž]D,jUô°H}èaÝó m] Üú×xai®º¡.¬„(½G §Pœ +õît`J%- Ö j5¨9ÇÀÂúFcgéÚBZ¹\\NÅ\.â¢ÞßÔº?"j¡2B\9Q,6EnÌȼEN4*JEQ—4¸¬C•5^f¤Xµb¹¯°Ž1­Œ€ZŠ’×*N–7Ê’!fŒv1BÁÿƒ€YÜ Ò@}{_¶l˜åXÖœ[fà*sxûÚ¦H¨%ŒC7Ș۟k1UФí9=÷Dtä”<Èr ©‰N*=il~Žëë3¶ŸX¡±;‘Vµxiœj*iÝܶ|b¾ÑTuã=b-~óA¿o„ÃD¹Î1‡ÀsIÄ긘ϑ­@Ñþ¤x¡˜NÒ—÷ˆL!#VU%Âßìj…èøâ»"ÉŸ(Dšˆ\µ~ ÞbÞÔ²éìR¡¡î^A]vŽëp1¿]Åœ[É¢ˆ×øA½§©9Òû~Ö”gm™Þ½Üºb ¯à»€š‘]ä^w…ÿ¯ Cñ«—U] „Ǩ‰ämr¿ðt@T–à–°_“–Ê +ñ@º!òì¯ýÛu)»5Fõ‚´š!B4#¹§¶¬è6¨OÖ˃"ª7UÝP«ûû‘—{ £3ð“í  +Gº"¦Ç6V† §åcÃ,ûŒÁ$AÚ"!Å ¥*Ü%)À"ö=§*†%.ŒÕôq‡~䑬þÞõ‘Wjë7BM,³}³Ÿ^â±F´Cæ$~JB#קç7â˜~J ö8«Ÿ¯òÈtF“5‡5( ™Xs{Ü­ÁmlvlˆA ¹ÌË[ji„Ž’ÀXj æ } ÖÛ?žý„¦ÍÀxDÏ[Ñ:[w#qá@1²Ž¶Yûº¦P˜Û`jÌDy}A{ñQ—ó›ýÌ-}d‹ëÜÈÞK\í<ÊwwõÄ<ñAY½A¿JÑùe¥`HϹ TÔå’K®çÖ7˜–|¬†«†YŒÂ‚”5zx SúØQ‘»Æ£ÚßAwøô“„DÓÃ9И* kUR“B([¯é3Š5òó..»c •ŸÃa•477³‰Ô^[!:ïz +RÙЖ<,=Ä|l¸V_ëÿˆîÇÙþ3KJˆòA^ußÒèÜÚãßÐQÜÿ LòÂŽþÄ{eÐZ[à]„ø±2<Ê S´¬¿)¾>'âóõ‘Ö“õ!½ ûs?ç¦RÈÙÊ,Uî"VNÚæ—®„Ò|ZJU²Ía•Ö!¬¬Æ-ð…sJ ¿r iðí`SksÜß&Ö$¥ïQH©˜P;W5™´Ì«Û4T2:Ô¶aP¾5±g‰vÖmKз-Á6ˆƒdÇQÔÇ…üyÊ„t)-‡\í¡SÌ[‰Ý¡:á(®#?K-ïVø{å]*‰ßÐíú@çx:LüTßÊùÓŨ¸ó h(žœàqå^ñ};Áõü?‘„þ3ÿ+[ ÍèõUvFZAUºY+Pq … +F'ÜFÏóÀöõ’GÏúKG){½0‚“¿œ_ÿÂ0†_­ÉPíÖƒzýõ­üÀÄqTDÕ”,Žòð´Ÿ(Š}nœÈîRt¦t¥šTörþPÁH¥ÇãW"kâ»Ä…*º¦“ü¹ÿPC^‡¬ŒÐˆ±búŸ ´#îã$æsî$ÿ¾~ð sÖßk#ÂŽ?ØÐîl`­À`n8Úî dMÈ”kYRåúZ9Š(·†«‰m~my7èë†Öü…ôbvwÁ.¥€ŽD”ºbpÐÖÈ t=yW˜‚35m× m@k )_5m ϶‡p"á [Ih½X¬ÝûCȶP¥k+¡N°ÓV€.sýŒ +ˆ j±u}?ð5$辸bEzRº(Á‡ ‰Â+ð•1Ónðxàêt‡LM÷ï²ð'jÆÌÉtCH ½—hmTÆé8 ñiþ«ôÀÀ–䛶|< ?þ¾§Íæú&"EEåÀA’íz«]²m†e%7eÿ3£ò~Ió'êôhñ3ï À1Ç·ùÊ7[$¯ïAIø>öhk° {ºlú«Ï¯¬}p ×”°;­åЧŒ yÎ-ª +gW˜¯‘ +IãU’V¶´7nZÁÅð+ Ü€ÆÆPù!ÊåŽ[¢Š²ëïWߊŠ…ö~èvkc=4ÍÓw3Y¨+ž¦+§þùwö&b®~³qP ›±@Ô‰Žÿ×$t8m­„3W( ì© In¦®5vð«õdöd'ŸÉn8Ù}?E>)RXÝ…uÆÊ1Ñ¿¨›ªD-ŠÁ4þÞA41ø܃ÙgȲÙv—…¥Ørb˜à’oßE–hQ.FÏg,Ñé"Âê….Ñú#§Ã¤Ìˆµ0ÑUáO7üÈnoå5èÌ^·|‘D”²ñ¹%2P¿­É¢Üã$=Gªxcq¤51”€¤ÂÃ"ù(P¿uð¾0¡½r ±›ñ¥+>i~蟌£YÎ| 4ÖÜ£è?6´N‘c²qúÊ$áÝ®—ý¾0ÅÍ)‰ýÞ÷§†\ndçXÌ@QPXuœrÖK,¦BtÉíš¬Ä bÛçù7 ¾ëîL¿î ÄLËÒ¼™‰Y¦¬€ù…¦Ô €P±êQCI:$³ jS¾Ö† ŠíŠx[I†wô͈¨x(wØÁÇ™˜.Í™)ªˆâúsåT7¶¸²6Ù’ü59TËÔx°6cÝcÊ{¡B²þеaÍæøõó½ û†›ëhp›NUªnÅÛ®Ó·ð÷;ªÊ­Ì+Ru»c^'Î?ܯ7 'Iœõ#;aC4G*¦%^B5âzÄ·‰¢¹¡ù–…ÀASaOp÷í~@`¡kÝM,/ìŠ@òìöHir$íù.8÷‘*< 74nM„³Fk­qP‚„ÙR¿E 〱{®øNë9V÷þM„SXV>7dü-÷_¥—yT´Ñ ’qSSî³É|Œ–º"WZÝØíÑÄæmáW© ± Z< |þÚ ÂIφ®ž×¬{~Uùn¤±¾ÖÈx +`F:ø‘ɪn%¾ð²qoFš'‡þTA”dï¥5u3lc×7ˆ\Ž~úé ‰Èpõ¶«ª|»µ;™gú¶½í@ܴꉳìãÒx+a»úõgd(ê´b~­}ûµ1``Ö[ôÒXo¡dŒ„ÖŒ¹˜”y&Ûzÿ»—…}äœÛÀ^Yxƒ«K{%sl¿6 ¹ïÍëoU8n /¤•E?Ö^¯Ý°‰r¥’¬]äZVfJ¦§ÆŒ<<[Î5vó“yJ 6†ïìË‹…8mµ[Fv×#Úyo絶Åš'HuÎh éÆb¸®i\ä¼îªi Êûõý¨¹lmèêkõÍm™¾Çm„¿C{àÔ—ëÇ­cNÝ»M ÞΓ¹É=KÄ¡Ö#£¾Þ„lü:»“´xz >ÁõÇMµ¾~ó„nZÝƶ$ËéÁú:‚æô=¬€t#s’š>Ãð¢ z>¤hKjØ©ÉŸ‡Læ•K‘*ÃYâ[ÛbWtÃ4A®Ácµ¦†•>jµ€í|qòѮÀ‡°q4AìæAv9€Ì†pæD®iÚnÉu<ÜŸÕt"ŠÜ¥¥óã íz“ŠF‚„êšuJ"üèQLÍìÐGÖp–¯ÝÖ×:öfŠ…Ý ÇfÔc©¦ Íu»Ÿ T$š£¤Ï¤aÝ.ªåTH–?”ÿrÐiM'ÒzÖ 4ú‘UÅ;ÂtÚrJÚVÒ°ÜÞ‘.Ïäù/OÓO Õ Zˆ™X"¦“sXÁRØUÂ."!LëI:½Ëèð­g*ÙÔþ‰CŸ2Ž};n¬V!§nm(€u#ßIŒtÊÃG=114J÷H£ˉ‚±ë„›ºò#L<ih8Y“Ñ—V‹Løwü‰bÒ§•F *OU«ÀÌ©NÏåòí;T1m§¦Å¦8]fÐ>;µÀ~ï X|=tNªË’>‡Ä»èZMûøg ø¨ew-2Ïcû3S;„±úcHÓå[ ì¶4a· ÷q)MJß­ï6r1_R«ïö0+±Ra¸Ÿ'J Ë6Ó&ªþ§­$‚h(ã!–P¸Fú‹’eZRÇ–G:]s¥²?ß™¯&þ0þTLƒÄX·þe_ÆZÑ×îð%J÷ *=¢ŸóDOÓÚÀTÎm.1•ôœ·\Èóã:Eß”ŸóguÃWEÓ‡«í̳s×qŒÔÐDÎçŠAÅ9á3"šÏóõ~ÞíHTáä#us˜8ƒÿc-q]Q¡¡ÎÇÒUø&ÂqBC3ëñººþØ:G„ö‡"sT»Vd‘¤ÉRvÄèö¨äÈMµýüŽ€ÐU¹„:.ÖC.4]´4Ìnc3QíÈ® r2‹@b˜Úý Á~F¤Úfâ€$¹R` +Þ¢x¦Gž:ËÖ®å + o ØŸ?cKŸ·ë§˜S•¦¿¦±Q]k X™k@qéîM™E\:˜¬¯èØ}FxESGºfh­}UO«]Ô¤Mu5RAÐ|A“H3êö¯ß{”€ f†D¼[©XÓ¦ì¾e»VÎfÑB €”½¥ŽÔZÿ xÞ·Á²Þ·ZÚõÍn.…êñþÔç͉ïV\í@תÅ”a¹Ó0éÁŽ +@’q<ö£=#Q‡ñ±ÑE;æì‡O|E@7"¶½kîcAR’‘ "t›»é=1Øí:^¢BAÐþíØ`r‡N÷‚.èG& mðïs y·´Á}EÉ£Jô]r â|Lì Œ)èU±F@×gÕ5xq¾ë›ôšiû„ç—ï½ç{¯ó3 ïèJÄ>€7~ųF^™g=½3H±è‹ Ãù]ÀÔxñF¾§<Ϩ7DRÁlòÈè›ì„:‰˜’{ë³¥2îψÜ2@?¼nˆ›Üõ›ýР€-¥ÅM͉6g$Û©¤~DÜÍ ’ÀDöèó¥ö¦ö¨" Šg|[ãAgâŽl>edói©¯‹…ûïíÀáä3àùT-]}üº£uAãðƒÀÿü³‹ ª˜>³×Sy{šñUQ%IVŽ›·oÆ‘¬O¥@@¶]xе7cÔà;ï¹Oi‰ÂÓQÊû~n +? +ƒªÞŸÙYßÙÊ.z%o<Åc‹ùqò{mŒ-PÊ,F{ŸuÐiªmͯ`ä¦ "0 $¢ºH°A™ÀÆÝ^Gº:¹^\[C2ã?«“§fR_"®¨ä_»t5¸§q‰víC"‹ îçRÐ'–³Ç%lZxå©_µ·¦n›ðeú"á|BÁ ;56–Vò¾!Ù…€—SØxR~ü¼ç»iHÉI£cWã +È™Ÿ—,d^v9×OÞús”òÔÐY­ˆ˜¿RË$‘§Ò÷‚ªÀ„Æú `à¥)2|•ˆ†â‚CÑ¥ŸOÒìtm ‡ÌõÆ-ôÏPCDÔv¥±èÚ®A3óÎ0(n€Ɇ›Ì˜–ìùÅ‚€·¢:ïlöÔ™V6ƒ@+mEÊc¯«-EÏ•ç¡p7Öé«gHeñ€"Qï®Nz<_yüú= +Û0í¡8uœ¢?ôigU +åöo@§œ¶-(à?ÆöïQç¥m8_(MÕÎ4 U(ûAJ±ð)2•‡KÅçﯷwôðÙ¥íº¿³¶Ð^ûöœ\[ƒ!Dõó÷áRˆf%KÒuá)®”“;aBé °åyñ,› nÄçÂlzd/õµ”Ql"¥=sŠ}@M¸>~ûôs`©RÌŽÝÛ^ÖÝÒËçpªO†Oú9>}ñí$ø¥L´]¾õ.­ñ£@âì …pOÑ7¦m#MÐ\kZ+M‡3¬:&Øz¹it~ÒÝÚMZã©s¯õ€S +ÛrnHÀè ¢ŸÄ´¿îŒèù°^ âÞJ8­Õµ’T@V¥ÉŒdú•Û¿€zŒÅŸ—4XZ±ˆ²¨ÁóÚ`+óÁ) +?%På!P·^ëלws1tkS¿Æú›ù@dÿ¥b–•.dí¯çoC{€½[»3lgÃØÚ³žö2ãÙ‡Šé(*«u³³ñ½ïOÙ59±³FG 1T—sw({±îe®[7âBRH<Òk!û¡|yñoÑdÒúvŸXýÞÿ½wmºå8Ïó~ÿÃû%U”«°9}îñ7‘&T¢¢e[*W +…DTLEÿ>Ïu?ݳÖêž—a%.Û,SÜïôš5k¦§û9܇Ó鑆˜Á–¬Òê’瘈ŸSäW_æ¬M’zöâ„:8SÅ‚áà ˆ¶6Ñ=O„¨(Σ=ÅE­¨99–®)úh'ÍÓYÊí: ²¡°+úÛá$ÁmdJÇéÔyE¸¾ +tça±Šë]Ò*–8¥b뼑HÔèïºÉ +i‘·ì2Â;ÅB•º©½æè¹ù½“Ž—f¾4MAâ[b +Év‹.¶™»Y&ÊbO/î…˜nœ°Ÿy¦t€Æ Óø,<5ÇG!RM_'º- ðpÈúoØ»ž(Oy5Õ’€[cf®«©îçÅúÑ^>\@ÎdÅ) ‘+BÀa9VltN:éô»…’²tã¯H¯QÇÉêïd5¿ R…Yf¡ŒtyJW:F^ŸÔTƒäy•Õ£îß$x×2æ¼¾‰)È(^üz‚>^匀¡ëßÚ<x¨u‡Nä¡GÑ÷¸ð&};QÒÂå·[ +o?á¶Ò„t¡ #¨c¼Å +$bLÂÊÊÝ.(jʾ-IdØ¿GVkùF<>FÉB‡"Ö©ó¨@ˆâLB©ê n#:­F›DÀ¢*¦ZK¹Võ¥gªÅ¨„X.ƒƒ>ïSYÎþM½î Ç Cq¬é€*@”Âdå+æ½@faìë (z¢‘&?Ñâj7¬WÇá¬Rù‹v„fì]°ÒÑoV< +„×A1E¨GŠKUñ(¾'Ó‚„- `Fâ &¹^àžo¤m€®„`¡ +:IæËYTÓ‘­C÷zÕÂÝ$Ç Ë+wbü÷Q9;ôªæÐE$„ƒwµršû.‘ú–x#ᶎðç +”øT=ìæ<š]˾`è ˆÝ¯¥b[EnF@æMîÕ<™´û(\Zµ¸œXÛy\Dç8ØD å ÕÝÒú†Õõȵdv@x<“N'ÂBÇ$8hWGƒ‰ÁÚªó8¸€ Œ¶è”š¹Fdf2¼u|StK—GÓ‘QŠl”cù¸)8 M$Ä!Q:–>I©M3‚Ø]ØÃ@žGÕÖZ‘ÀúõE*w©Aל*ëÉ4åÚ/‘×9»>DÐÌyNX„DOÔ¶l#üé,Ï~ì´EžŒ@Vº®±„®Æ]‹7#4ò˜¤³"Õ"’îè ~Ó7¿âsÈTˆÔÝ©HXÄEK ]2‰õ­¾(&u,°6ÕÊ¿¸ {0†$ÄE•×¼*`s„ QÁSÐF÷àP6H¢;ú><…(‚±HºR5e1ï¬ûéN&éN¶s´Ú·AÀ ì³(eR{Çm€ Mp +8Xj+#8ñÆE`„ ø.ÕÞ:ŸxÛO[ït5FðcwÀrù‹±ñçä#ôºÐ,ÝG`l#pÖ^´×üü&ß\éuµš¥Ž*Röö+°jÚîPhY^e/}))Jr•%»\%… ò/jEèíŸ}ê¦ö¡›ÁTT$QÖ¿kJ¡„Úv [:‹ ŒQ«¦k&3/È…B ù%MW‡ÿçŒý•":V–¸ ,E´Œ LK$CÐÞ‰ I £è,l5v–yá_ŽèÉE!ö„ËD%îm‡í€ÃÃÁFÛÁBçZpF!°%;©Ž\¨õ ÆØäeåpàëà)oXñJWA äÁ<Óæu(ê׈r L T¥ãLd-q Æ6cÈæŠ&AŽ­ŸèI1-§I¸jÇ8]Ó3á&Êf·€©‹|ª*ݱ®iOÑ/ê{ +¦„:@òmûÑ1 t5# æÏä@v“B6¸NÛ¨jÆ®Ã^il=@þ9MØÝø[–v¬.‹OžWáJÚퟕƒ£D$Ârë*½ m,ìϱùm…1è…™¤ç¼üµ•mÓ¾nÒôcÁ ànÙ}×hÞV±7  < M^µlXÞWGœD›ý³º[Û(:à›2!›yÁI /æV»ü!T•Â-î€i°'wǘÐÃ8„Φº¡Â`M”•¶Û&âeÈu^ÈNc°ž<^É +K’Ë +³,»q ÈÔèLçf½:î²Çƒ +`ÚqàhÓ]XR’zN/½Ÿ‡c¡;±èÍ94V%óÕ<³L´ó„mµìqŒ‚6;‹‘£ë‚6?7 1ò,\¤«Y"{¢ +øW£!I€)Ž–®¤Ó( ¸Ž”],•b; ÙÃC['  L M¤½ìËjÛxAüðdÜöË PÅzÙó¨è+mŒÃN·#ÎQoFX"AG“ü¤äÙ­ØF® $ÝW6´pºZÒÍ‹dT;wÀ¾Pí·é|”![(Uúš\Ýþ”Y×4MNÀ¾ŠTß…ºµp0ÉʽTÅVæ>暨/±:Ÿ(Ž¢>s ƒ‹²3uíùk;¥qJŽ`o1¯èïÆ(;* Y€&çô±*ìÎÅ‹ÛÃ88xTIEj+*ÜhDXYMÊ^g6ï…šïˆ-á® Àæ®4ôåJóš¾roô_W˜ ¾uɬú]ûŒÃ1j.ø‘F)ýM±Y RÚRéÞ0Hk–½ì< €'Át>UH%ƒ¢\o¿0È’¤iDMª»ÐyŽõôEéTåóLq(jéRõó¨µÆãc>éRôS;Æ{ƒfêŸKÂ~Ê8?¶Ý§XL'. +–=67AZãjTÛâ—‡©HIH+KnlÐ! õ&"ËؤÍß¼  ®à)ÁàÓ÷ ¡ùëV¤ ¡@â´ÃuîOu-Ò‚uñ°²äç”® +1§ÌŒP·Ò‚ ¥p ?Ä ÏKùÔ\p5´/ª{w„çw>]âÖ»½< )SM!¤6Iü. ÇÊA_ªh¡ >G!;¶F¤ÑŒv‘Zå¶ìѳë(íÏ G£@“f¸[Ù¿ÝÝ{Ðóˆîr¢Šˆ°,*ŽMç9d®Ì,ú‰=º­š Où¦º[‰“4ØÊ6¢lù`¢Â`ÔIëMv"ç"d#ð`û5yR‘ÚPKùNŠ=@=ûr´©á]Õ"?5’=§ B¬°•VÏ k×}JÀ¬“ˆ²¸šPÓÝ$Žþq"Å`G·í0Ks“›"s”N-'!¶lDð.¦H°þ©õW×7ÆÉ}ã>­½°Oñz©«çðþÈÃ,÷G€ÅòÄd0ÜkâNÍ.%i¤Ï”×Q1NÓK#h+ÔALE„Hj®fdž!<RS#¨`ªÌÙƒFøaÄ tS_€P´}Än®.¿@–ÅûXŒå·2¹…ƒ…ÍpjD¢öÍyÚ‚cÐ ÈåÈÏáÇ`ï$7€E~òVÙ4‘ã`”² (öYfÇ… ErŒ˜ÂÎ+ù´ðnív¦Yß(ÓÇO6·QŽ½XYÚÍ“¥q/hÊáøu´.ˆ¹yPêZ.ó(˜iuãpê…°Qö)pQËRÿ¥gðëE‚^ös–ÂV¯2ÿtueF*àXz&Á(³Z˜eDËþŠJF$WÄ‘I0`@’g_¤j§h³‚`ˆ-Ý ¿ ø{ž:06–&ÇU7f§?„ßbêñ€ •ý êCÑ +ï³ê#¯MÖ–6^¬ê÷ãDêÏ6CÜÎmÄ1¦"NbwÏê³ûkW¼Ñf§ãŽ@ó/¢ŸËâéhIOª´I·(²ý)$RŠ#¸,# ÛD1R’FÌà‰]Î$W†µ¦WG²8}eB‹¢ }\ÄdùŠ¶·Ça3¤»Õç  Ï‚ñ£<ŽBˆiXÖBù†í¹{íòᡨiÊí–=EázÉáˆNÎA +!€"|g†â(Í'ÙÛÍná÷9Äô.9šcñ¨qÛÒA*4'€·¢Oƒl…HÓ‘Ù†Æ'<€ûŒHyY<N~®ˆG òàõEg:9ê†QÖ®ú³ü´Nq ü¬ˆFsVKOmúTÝvÂ×år*Õâèki`Ç<5ÇvwPED°æ’†MD iA©™í)ÜÿšrÚ‹Õ›BàŒü¬zÕ…,VL‘œ¥Ï‹0Êí¯º”.ÚÑíDzb,ÛÛ‡VíN™ñHòp1p„|¸ÚsT£P r9ƒ¢Ãœ “ä| 8FØÚÉ^L8AÛ d†ÀxvtûU‚˪u¥R3ëº^°w ¬ü­Tük­ANeè»Ií̲3ÎÈ&73¯HaÏU13P»ã”h¸µ˜8à>]—"¸ØÅf¶À¾"Ÿý„o%b"gòôúºÌúFAðw~¤kšÂp·-‘õX¥"·ÁênûƧ`í¡~Ñ\©îššiâIÊ(áØA¼·9˜Tµ"äÎÞ0•iŠ8,…Õ×Tyœîņ²`TjLY”yâ“h-’Úôè½yÚ3Ù¶c(þù#sm‡î¸ò—!È”2ST\#ß'd}Ñã.oäC€øˆ  %88¹§ªRø²ƒT I¿NEÀÝå°™“Âþc—™ ÏeÞp,²c UšŽ’–­d\4wÝîºÁ¦Žü|`ëæÀ)ócèz“hyà,q@€à©ÑîW}·¨æ—çâ|ëÕ¹¡*‘É&ŠÉèCÀyµ°Ô„źI•Njè-ø 4»­[0í@˜7:B¨Í—ê Ÿ +¡Švôі ÐT:÷®‚Åk…ÆÎãvXùÙ»;ªÅÒP—ÜýpÜŽ™ë4úa΄8Ðáàý%"þ)¨‹0^‰î’2E²äxëÌÀ¤XàÉ—[ÿ’ž;?h=À„6£’v~pGíÛ¢„éžõ¼JÖ ²ÿ‘i+H&Òn§“‰J׋JHSâ„rX@I¿ÖUÔSÜ’”ê<ÑyÔªom# åî«à+û³Ûãéè!½‡ ’þú"݃–ÝTJ#«#Ö,qƨyü$B]}¤’Ó°°ac…VfåPÌUØþN†ƒœ mº"WÜ\ü%«N½u—d´,*^hŠ/H07㵯õ øU•IØL}§ž“ÔΩ)’ÀÒ¶t-š>‚ ¾šàCâ~ݽªÝ™^íŠ >‰V•ÛÐRîptÍŠ/ÒBÄÜÞú7š4ø~e:3*‰’4¶sb‘ÄÒ*ŽÜVqRž,e9ü!_ ž.®¨7žae³E’$ÏeI½¾Ó%żŸVÌÕËPÔ;ù7,¼/F +FE°"#Òí—T>B!ô¥@P$1ŸCÂe2™À8c*ä÷‰µDõἎß{}“¡ `VQ—öîÆûSÆÜY2)ÎRõro§Úå»;dI.—é:¦d>Bex”ÚÃ–ð„½ +ÔØ +.u0±[p&öPâ`–óvôP¢§4¬@/‰ !92ÚÊ’wb>€Ö¤~I<¨ü™,1Žƒºf|ÒéÔîÔ÷H¬4Kv…´S¬îó-Ö{Ù¤=§Cá8e[» ¿™Þ`pçÂÕMåJć]É äds Æ"Ì·wµN$˜ÅK¦F£‹wBLbÁ•à•ÓÒ%ºxlöË'6—Þ†æ½t ¨nòRÈouèÀªasγÑsóƒÁZr™SBýù5œºëH„7ؤ Øæ%íEì^ÂálÙiÀú¹8®^¤ÂÛV`°‚ý%"¶1*½gr;-zJZ’n†Å…ì.û$ò&X^RXMÒƒ¦„ú蜰ïG”ä@d±bJ™ãAˆ‹74P'ñæ‡(‘PùiPéâªC5®ŠÅew©#º+ðSAa€~¡ ŒÙ±PrÝ¥„¸½"&»”:壣NœÖß¡% 6¡üZPX­eÀ[ÎÅ7`ø è{äY²§–Y„‚õ ¼ÿRøe‡ð²º!ÇY/G/×þ¯ýfzÙš~®,CÈ=@YMŽF®Š|HÉ£Èàx®,=3¿P°¿9 mýsÔR«òN™ÅµÙ¨ÿØ‹ÊvA:ù´ŒûP«=AdyŠSÆ>ƒûò7QDiÊ©À[Ð¥ˆ˜ +¸d,°õ +27Ó‹•Ê ¼yŸÒªó,Zê£Çb³¦¹w^e¥¬Í0$æ$+#ìJ;Žïç9Ó·Ô¸EqÆq:À%§k¯è@-ž@k禆p 1jybò&÷ z)kâ5×麓`³¼g(ÔÚó¶ ðWL^ä™@¦Çk.=“Ã몽C {·©ÄÊíÒsÁ5­ê%©öóÑarۇćn';·2NâkMÖ‚¨¯Í(–ÂJƒ¿i¤å¹¤²z°>sìÔ”h.ˆ€†ìk§•ùÙä¦fKˆ°€Â>p¯’à`Î; _¼©”ì´Û.^ݦ_ D[Z7§–gŠuƒA|rbñ4ëÐ# ÿñjgHØÆ!Œá€ ¥Ê‡ËE/ÔĬì#IúÆT†ó#IûÕbO²r´ü´JÂu6~°q€àV¡ƒ‡c²§¶Ç׉ºâ3-ËêÒlX ³Áô”Ë‹ÜYê—–™AG“ßö(`RF¿"ŸCê’f°@ó áµèR«³ÿd n~P<+ý é›ÄS©¿òۋ븠̄Äú¡ÚóòáÜ“6µ† §¨¾ôCtЄî©N¨Hx_\f!Ò<ÝчP /-íg:­üÆSvNb½¦-ô¡Ï³þýçÌ{rtqLÍí³¤0RŒíîkÛ!­LåòI²’:a•õšZcßt±kµc­„T$d…-çðÈZæß’pP®@tè~A»{R‹&h—ÂE’°Å¾®ªe#ÕO'a\LV›. +;‚æ2’-ŠcNpЩ¹Q¹Ìôš@ISXà€;e@Ùk: QìPÏbSÑ4¡jÍ£ÇTÖ- \³¤åéúsrÿùó:—ˆÓ¼­ à°óáß`ç’»ë¹>:ìý°·‰Nn>û+‡#ÇÄ$ÜCªP†•¼iÐ5YÚé«A]`Ù1éj7}PaŸõdØÕô¡FÁ¢i™töÛ™\µ2¹f°j}¢Ú³GD=_â²Mk©ûLƒ4è15ïÿP8÷B~Â1º1•;VY¼S%(óþ€£ ‘$ÌJ~$¨”£žˆ=“—üØæIê|±€ƒë‰Î)ù_‹Š‡lÊ“å¡ÞBËM\¡CjÇôƒG#y"+BF¥³J³{]cƒ—†d?a‘úAô“…P[ó”cë¼€ Yë¾ÀæºÂÑ¡–[Ù|öl^Fá( .€,à1ouè𖩈Zñ)J*8í/ËT±PùÆu<&ìÎýaT Õn$`Vu-"èAARtøÇO¶âŽÚ–ð).tAÄYUq•þ¬ ;pÿ¯‘Eßb™fTèaœgÀ&…×P| °Òò¤^÷üøº6ª°V'r`žH +0 @¦Gà4Bb›5Z¿‚®öh\ (õúžŠæÍ2¯º" Öý4ŸDÆT¬‹*ŒH€ªÚxÒD¥_¿(ÉSJ‚C|D÷æ³ÒfZ_¶ä‚ˆsÙaBd‡U²‹‚^Šµ!ë<ÁsN¡û9F¿KK†¢—å“Â5Ô%yîÌQ†™I£` 0Êñ¹`5hF±.¡m¤Líâ²b÷Îæ_p1hŒ‹Û£Äáúbaße2ž½•Š|¡MðÆ}(>TÇ…{1æ6=ƒ‚ädI šô¿TPrÑNÚ7¼¨éí9`=íÀH’£®ôIùDÙ••õرòUPÙ…™%.š5ËžXz¿{D!8‘ÍÛïS§JP˜54ùïpÇ÷ù(ÎdÓÙÎ% +\5·²+¥a]{Ðli®SPP',,ÈBR›·ñÓ£ `”[–¨nó@›rÊÎ Èut™qr‰ +ŽõW¹(#¼3ÛæøÖHQ¦ íV²°¿ÀCvb u±$6Òô¢²ìºN*¡-oŠ”ïià7»™¤e¹HÐúÙz1Ê9ûXZóc³»úH¡àr-1JFØFùïú\htižê÷ŒDªóüJ/s çØtÃ|æx³¥ì_,¤%$äìÐØ`j%?Cšï³PÕŠ ŸËJls™(¬+%°®‘p‘}ïÅ"bXÿRÀ!ëÙ½P˜V(Añ’FX9öÚC‚ŒÍ¥¬*ígûªaºÓ@¡Ä^ñŠÆ_Ð¥Qø_/@ˆ/Ha2P ¢ÙfmL¤“5µAXl¢9ZhH=£yID +¬¨l~[T„P•w#Õ6â•‹ãªìნ3=/Eù–ßTTò©xf'AÕ-B@m‰ ,¢íô >`g“@>ûÐÅË +3É#,ë‡Xly¼¡µYOJp­náòhÖu -8ÎÍÁ5‚¢–éqóâkÀoü¤è„H4+Äí,q¸13×}¤ (Å:üuc™0¶(E)BJUÎQ”»k»à mšñ%,§LK¾º`CW=z:Yw +¨ïœ×IŸ¶úÎ/ðÅÖf"ñýªóý$·Í§h¿Ø;™Ž6r9=ý¡v¬šŽÿ!ê!L•HYØõ¯bñ É’îqr–Ùµ*UK’܆ž|•‚v¿Öˆ¢»-ã »Ûí"T§¡NX ¼tÍzÅ¢è$žþƒ¤¼SUÌpà ?KÂ[ôbâË‹Ç)„7æ@A[I„ç:&Ùéîrr{“Fµu8S³DþÉ)ƒlg¾bFzÙ|íÿáêšÓµJ*Å‚V›¿çÈÅ©ÐÐL’Ÿ0/”X¯DsÖ1…KѵG¤}¢ãñÔ3K_üŠ¥ +z‘gsÙ57éd§ÓÉhVmYÕ«6 +ØQp†t–+rJ]ºCM½{YS‹=~ÏRÅ¥EW•üo‹Ó!Iqú?`˜ +âä/7U….ŒìngpFE-ÇÇxÀ¬*Ñec¡#_0äóBÒŠŽÕ¿)i˧_ü›¸¾^ r‚4Ë®ºú .&qÍ”çu8¶2­38 FFb¡‹-]^Œðx£WÕ†ý›< F©ÔcëžšáÀ`— ×!Œ s4»H£À‚ôé°¾,€Ó!ˆ¹˜É$À°QËq K¶âêv·( }t6WF%Ì€Ðe Ý +y—C×8PÊ©¡Øç$ðâx%‹©°Hv£@m’ ¾Y7$¸Q”á»`; Ï0ù@ÁB Z¨QÓS¦db`vµ'˜¦·Ç¡jªV’}!J#q!JÃòH`í3‹^Šz§ŸÅ“I€E±ZCÈxKºÓø€i¦ËX‚æ€pŽª‘*)®OÌu@ÏÇÄ]㈞³2ª"Óš OUÄÛsÜÀIÊE&qØ ž,ºI”¶‚xÊ¥ª*œª´@˜ JÐü<§ƒ¦°pó{—•‡ŸòHÓ±ÑïÉe~S²’Øÿ&‹^¢ðýx„h;:}U²FHZ°å}“Dª0<óÇAçÈ®Xfù$8v)“_"``ÊEõ(UH(­«¦„ÜM½Ð¸6Ÿ®ÔT$ÑæƃD Šn© LÇøÐè¤anÐ]Nàp˜‰†¤)Ì|䪧-T+wT©cNZNÍU4'¥ë›Çb-»§ª.G(é[Pïœ0€´¨ÐЀfOsŠ<\ƒ8è6BmfEÀÚ>Ò üÄ VF³ÅÚÁC$/CákNŽD¥Œ¬RlÕú–›4 œáÙ1¡·W€©=!¼‘®â)ÅnËà ¦«¡*DÖ¤ˆÔ}NªuëÂÒA&Ìî¤"IC‹z>ô³¹ER%l¥P$Bg=]íè ä&îŒdãO½¥i¼Е²Ö#ÉZ¿Çrp‰ZÛÍJØUá^2Nó¼;a¨ W0nsÄßA v/{ƪp±Ûr)nÅAwRXjl7}\ %|¶žÝïHËŒ€ «þÅÍMe5Bœ¹=*\_ 8ì‚;CG–¾O†³œüïXÉòip@ !‹“à—ršáù„aµX^ƒÔ„Ð@Ïd \IÙ‹S–ÄÛ¡iW (’,‚;8€³ÀÙ zªYG™vZnÅ B…-ê—8ܼ¹òWPÆj5uBdÝd6°¹àCH[8Ùì QY'ÑžÌ]3Åkatªz‘k $m?M[#¥5I|U5²i4hÌ>ȉÍÅ ³Ýí×?sB‰iá’K6XbÑ]£ b5t!½J(Sü<âÂkZºšuwî‰ æ2é`1.'ÆQ€9 š@VAÊÕ€¦Jý€ŠWÌÉ#ŠÄNÇ’Šð2/?éI¦ÑßN4¹ò,ž5MòsT DÔ“2ZKìIG긨va ñgà{˜B KB•v/OžyHªRàwceYˆ„õK…7»uHT„"hc²,1¡ìO”3N2®6ªZ¦í484(`TÒW¨‹ðnÜ*x@%Á°A‚óPšÇWi±1ìžqõêõ5YΊ q*×o)W©ÎS²°X8¯ýüËÔ%Ô:õ†y5œ×¦ªkÈÀùº;ò€…þÕHEªIrWj88¾9‚\pð¼pâ5 °‡ú =4  y{ÕÐâKò9çhÓšœR›„àk®R$Ì¥C1ãfL>»ÊS’h¿#£þpÑFQ¢¢Â”ˆ¬q +€¥2¸Dl’êˆ îƒCJ~ŒÞÕv'œ¦åc#s8ÿºcNy Â«—î9uÐfyðb@Ê{uõœ4›ëÛ»ð£Ÿãô¯Þ5ý‘'Úœ9ý?Aÿ¹óÈgE83˜ÜPkQ{ß,{¹7i)2A{$BŒ€ç0R%ZSURÐíçv z%ÇÓH©3q“È!v¡û8F•-®A°ž÷ûB ¶%)KÐ;­蔳ó) oµ— +ìºè˜ +á­O„f'á¼áö$qLðI£Øé…À¤Î“²`4¶vv‡fTa)BU*­SˆG K­¸8"Ò” ÂÅ^ Þ]ߥ(Þ}û£¦&s9b4•kí>Ä¡ï(€úI]¯úOªžJƒUäÁ„æØg&VòðÐÈ¥?ÑáíÊží%%{¶E:Ae‹ô +g #>…à°[y K¢=%‡ì%*5Õ÷˾˜½'yìÁV—xÖ¢íÛÛÅýÝq~=è°ÇA\ÔÄ`tçC–H2P‘}R…MZ¯š”Žà &ËœÂ+âûõ.I;[uˆéÚz<¤סû(ܧi+ˆ>²(î: iøâ¯?#$/i¾Ë\ï ‰Ð }}sÓÅwÿÿ%6c%:̈ò½[Ó;~£(©öKñí1»Vë ðëKÕëSØ¢†[SÞ—1öf¨†ñ; -CöŒ˜ÅZŽò>À·g(&ë}X§¿x­ÞŒŠ*uBç´}¦‡æN& êa»Á†‹ °-!G”¬›QEÛ4=^©ZöGQµMTªuôÕ $~éÁXÐœäÊäüPúØQ§éOŸ +.³Ì£4÷â!÷¤bŒÿ‡›³|Ôýñä›1ãYBZ·ƒYž¸86¤Šõ©ä ŒÐ +&©@Nî¦Ë¥c6̺F8D3¹ñü&UólÔÇe”˜>U¿"=hÊdILA7ø° OçaAÑj´úB\;f×.²ÉîKåÐ$pÖ|*cRµÛ”ƒÞȲá–åã%XGÕSò¹€LÀ©ÈÎbˆÛ'òÒ3Pe£G9¦Ž¢‡ÄdDeŠbÒ¼JŠb0ÈÒ9àÕê·ó…)²ÀÕy}O€¥‹›6×UWÍú8†<>BhA¼ÄXFéóôq Iƈy55¨Â„Î •ƒ®íDm©Sühq +'æ?ñôMÙ ê-/ M sÙuŽÏ}ᵟAwe% ·ç!¥S +•rŠx(¦ªÒM+: PŽ.f¯"Xðx&`ÉS\O`?ÑfÓém# wàÿ¤ÆÄuó-bŠ^8«. LĈá?xýÑøòGúÊÞ*`•×å*䎎o‚²–F°ÈŸ`;'øež\da˜(€KÅ#í ‰ÑdÅ)Ì,ECµ´Ý¸P»²ŠqÃ[F©ÒFÏwÕú*qõšdº)¹xL|$Ú ”ï Ï‚-ù&I³ +Øåß$¡r!¤Êü&Ð Ë…X9£GNK6…XÖŸ€¼ Þ½viW—Ä4nŒÀ±ÁF´<öMÁV@`´Žzy­bµQšÀÝÍIéÇ +)‚¨¤Ä î—?Q´Í²“sY;ÁÙRrHÕ€r9ªäx Õ(Û#>—¼1»ŸVv@™[Ë·Ž”ø”L_£'ÑLî(…ôPjjn&O“aÊÞZÈ!Ù 7d9ÆVËiô› òÜú1ˆ baHÕ;Ëšgªqþ¬ö¸Šg”¥m¦Ó0¶ÁIjµ [ñŵp«"¡‚ýXƒM6Öh:Mî"ïLf«Òiö „o˜õ]äÛH%„{/>û)ßEÿä—þÈZãBgÑB!‹„ôF@Ö0ßmFáæÉ( cŒ*òÕá<¼$*¨Å8¬(9¬Hþ2€H¥‹ÔÆé0‡&i©€x|ˆŠ@À#¡Â /`bÌGª”P†H¤«E\‹\PXéN…wŒs ÖÁ7W£´9¶KÙrŠ°*…^o¾ÈH¨°ðö«mqŽNå[LÞ1QvÒ@ Öž•ÑîLQšrÛ(rœ, +<Æ蛀L£$ÞD¸vE˜AT·¿ãVM£@u&ê°e–9‹3ÄÅ» ÎM•r•È3»Ÿ‹V†<•"˜ôbQµ“¦[¡™Šõ’Íš¬•~<ž@É'H©x› ø*pXÂLÁ +ˆpuTV”²·‰òp!X_ñ—U_¤®/ç%A™hÛeBI£Ð-ærBõoR)­²{$§G«áÕœ\Ô9¸!J³ªF@bD mút`IœçÀØôhÓ4¾ºÔI.”|«c¦ƒË"&xûxôS¦GyÊpXÞ.È,+~Ž(ƾumaCÍA¬Vû3·»DƒÈ‘§Q†E6Nš·,tÃw ¨¦”·£?ŸMzs‰>i¹£÷@Pä-Ð\r)ú'uœ§“ÁƒPÝœ`@êÿ-”wá½+*T œ7 èB´všØü]n»X8Ÿ_,… ·Î m½©‹q¸bŠÝ+jqCNwì¦YbZ ‡Žõ“ºßÁpj‡u:Ç1 I¡¶*ƾ*‚eåÑA½àÿñ”»/bÕA¬+yÈ«W¯ûÛ4¡s;Ð-rˆ/‹>UªŸ¨•Ò'mrxóÛVó áÜ%Ÿq2ÔK|¥ç8+ Pg“äI²ó@€¢™QxÒA¡€ÉœÏv”{.I¶ +ЄldüS½1‘د ²º08§è’ Т@g+¢œÄLyE†Þ²qœ((ˆ˜§wRÊ=”l§§ð´æåWt!­ë1Ĺ3êBÂ:&êp~Œðñ'våóCNñ/D@dÇSAôyœî23yöb8p†K‚¢«ÃAz:H+¡¯9±ƒ¬ItoFôè {ºÏÆ=¯ƒ¤nÞ†º9J }jŠã*ŒçQýñ4_—¿GÛš–é×c2cNÁ¯‚EÔÎfKlusØδYõS¤,aoªX@¥1Ï1+•®áLÔú ú»Ð¸9Žô +ÝuѽÐJOŽe¢Âµ©Õ¦"y8Š„>e8^g©øµ«Á[“¿5¾zu±k´“\ÈIâÓ á倞2=òq.ƒÄ{'û= b ºöQ}8¬)\Äpž¹Jÿ>׿/¢´5_2Êز€—ÙûC¹A|ñõ€~#Â÷éæSÞLAÆѬ4TI¯¿“§$=êȢ瑪nè~Øöúþò¡àSî—¿Ðh|qÁ.Ö•†+ +=G Ýu8P²LK"n:pÉ°ìÁïótYá ÅGTš¢NÓ=¶|ê†_2•>EI%m³Q-ðe¼â`ņF¬•%Ö„ƒm—c¼Ø°ã@$ ÏÞ!Bûé»CÏHZ(ª[bŸÞD÷ …ßóFNÅ‘:(-;;hQ\dâ2.ByE c1A#¡ïe(ƒéö4ñP>ó5F|z=lé¦A¬u£÷Co«ÂQSœAèá°ør6 ûÁœ’§]FÁïs&!æh’AoÎXF®!✼¸[¶÷Q¬B+=ãyâ0×Irø$‹ ¬m9 w@¬=棲ážL@Ó0=EÖAWæoW_"EYNØSk²ËhhEqNŽYVpmn Ù#ùõÌ?á1Š›©B!bNT¹NRe7ÄV€¼øÃH=¡wŒÞÍÇe¥E“Á© *ø´øI$àUH¼Yª.ñ4 3s‹zèô@HnI¨Aa"9UÑ´ÓqC/£ÍG”<¿¨qü˜á¼Üd댸‡ ÒÝEI¼´ê ºq Hr5ìÓ=±+ú“*hR?…d¿`o^tM®3z©ý[H;Äeu'±Å;82“¢‡<`Î{­HTo­OePxó}Ø¢2(Ãä…LñcÅ +i`Ôæ:6˜îNk‚Š† þu²®(:=U$] +ouE}šjŒ‚Ù¯óPùDF:Ï鮞QËsàON—eDyúEN°'UŠ¬¸ ÈGÏ–hanFÈ•ú<=%}û_0Ý„Cb«p–Í€ŽêXS±¢åÑôž±¾ù±‹¸Wl-#ž,v¼›=¹mwÈåÊ`õ@ :"ŠÜ¢º)ò°S[-E…”t4òÝÈÌ·.óò«vç +ÏŒiGi `qtÆ•.&Æ­ŽÉÂW$;ÑXŃ€Åºy úèƒ;Uû >e¿Ð¸0ͱ:¤ö!×ÈÑ‚hƒ‚£ïrà=õ’íÉÂ¼á Š\ •fowPï‡ûÇ舭–zOkv_A\a@V1"ãtXNÉC!¨Jã£iñhF€ƒRR@L<Èm˜Ë@Ã.#4ÿ¸*]ÒÙnU#\† +Ij^aÎÁ$“ 9°2áSðºŽ)“[œ +̸KßçöÅ]¥Zt3¢þ‚§g.†$®xÎCèS%wi¿Ñ|9zLºšÅ0ûm×èeŽ*Å^SЭ€eqîj¬7)ÈS´ÐäxƒŠ<Ì1®„Y{6 +ïÑ.@WM„9tìémòbþÛ¤è?F?}‘ãœ] øÇü5(ûï!ñÐN¢Ý®cŠUn.„,OÙÝйsOZ\Æ:îPh–W +®nd£-6……׳@·Cx‰ÆPX›b¦þ"ò­{ƒ[Ê$—ÿ»ƒŽÓPUH„Uùú Ú&‚,S—meL£³%‹¾HÁ#7®mj î£pT éVþÔ½›.ARPâ.š’åšâîEô¨q/’ðŸFTq™œbR«Yb©ƒ”·ó”äìf‰¸xaȹX‚rkðj{M¢ç1ûWm ™w_Y—è @ýpZ¹11”Ž[.€~”‰ÚãyÈu66È:è²ã˜ èn¤‰GG%ÚGH(<Û bM¼Sƒ,S­ã ÑN‡êŒ>…]‚.B&í¾ƒWø\Ð |º•Ä‡m„Ũáº^í45Û¦«ÄˆK™ö¤ ¨w[ea‡†‡¼š ^RÏyá÷}DjÛÇëáDxŠíMD³ìe-UÀˆì#ô\‹?×Ù¿ß´=uÉÈ Çuf¿Î:ÄÐdú‰#„¼Î«±¸p¡ %6ÎX÷Q‘ÿb±>r‘C„½ŠÓTIƲ ‚ሠ(KøÐÁË“]NªÀˆú€„È*4‘9Ô½F¹ˆÅ@]_hl¢FÁ(â£Pè©Æ°Ä_¤­â"Ž2"FšMÌ`âb¥0A¦äÂø &ÛÛÓ´W=¸âžûΟ.4ˆü<±—ÞŸª@{í€ 5— –ZZ~œ_›h?XLg‚Å“G«ýàpcŠŠjWr%>ðÁŒ((™2ÍF$©^"ýGw€£?^æªô -m´Q|qf ;‘2¤¢ãF-uhAF|¾„Ž¿‘öŸ€¿Yhô†–‘%Ùƒ¿%‰££ršÛœòÑ„&¬YGøÔ?‡}V0(Dnç/NàÎ~Óü’GúÍêi +GzŒÎé2fQM ’—%ú» :Úxc+â± +ïèí1èbo®%B®ÃvWÐOÙOƒâ‹dnp§;Bë¨îh1Ò¶H ¼É¸ëˆö I<…oØ·«FÑÍ÷Q¦â×VÂF˜­"™ig’¾@¡ÊdÅÇ^F\Š@  ¹Ýàƒ¨°œb ~În:B•\šœõ²rTƒ4BxPOsüR(sŽó²¹&q×7AÛÔ“=? ?5PØ8¤ï#÷ +&w?ÐÂøåõ‚¿û Ú ”,)ió)!ÂT©•ä¦YZ{‡mfÇh\KV»ÊT÷>¥†öÛöqРelg<)\€>vÍ>½ÎI,_—!ˆ4¶‘‰u¸|t–§§>•ó(<*¼CÞOb§ zˆépƒŒXÑ¥‹°ÌÄ&†¢´Õ1 ù{ðÉ¡-M¢ˆ7§ C™Ð0 ¾Ã»`VÉ͈I“C÷ó˜°áuÄ€*õbsDÔ s"àëÍ øNFĸµ_túŠ¢˜°Ö¶‰$‰tr€e¡«ï +ΓÉíè ÙTgÙ9B6i}V??º°AMJ\ g¹@œ ×¹m -ý:ì µl8Ž.% é* *Æe'x‹gƒ¾Ý!“™ùäc‚ÄÙ+y¸b³«nËy²9c×+2¸Âµæ9UŒ£Þ@ƒV’Ð6ÓÓü¦u³K£ ÉÃf`X)Yt©I¦¡¥PÓjBù²Ã\ö^pèåÁGÕh(̵2(.ÃêrÙæ¬ÉÑg°ßdb£ûâO8ã-3iömàpÓå£b£,Jÿõ¸žÓ½<©XR›/ª*_RêÑ<ÔŠ€âÐF¡HG”‰’äas-קú3œyûPD—FÃÝ3—våRHŒÚ…$\âþ’|ËÎ/J^*ŽÍ½¿œåe‘v\‹ª(¡nÂ_¬êËAjå,Ðs1'”éèˆEƒR”Ý’>ô• ÍR2¦­õ”äÓz`à/¼B°Óó<\xYAêø*Š]Dñ&š?‘ë`ѦÙÐ!BÂ2Þ.ú%â`R\ÃÓZ(-„ÜsiOH4†N©í3‡×”8 »³Œ¡Ðü.ÒtMA{rèÔ¾‹Ãù)Á>âھ˷ý‘';ð…-Ï„Zý¯hɼýüOÞþò?¾ÿeáíŸ~÷Ýß½ýü—¿úó/¾ÿþëßûù¯þáó?ûâ›o?ÿ컿ÿ§Ï¿û›Ïÿô«o¾ÿ·¿ÿî?ÿ½éý'~óõßýÅ÷_õ¹}ÅËŽ‚ãí¿ùú‹—ò•ßýþóÿù›ß}þç_ÿþ˯¿ýþóÿíëòóæ·_üêÛïoFó¾øë¿ûÚ/ü×ß}ùøLyï3þû¯ÿðÍ×ÿhù»x÷ç±ñÝßn·åßüþ»oÇO¤î9LXè÷Þù‹oì²þã7_}ÿ·×ÿøØÿåëo~û·ßìŠ~ýõß|¿^Ò»ƒ¹™vg¾ùú~Ä}ùÍwÿø·óOö?ýòWéó?ýö«ñIÿ÷§_ÿö›oÇ_þõÛÏÿý·ß~ñ»¯¿zy‹ò³›¿y•¼UÂ*|`¨‡—RA…% Ü4¼/ ¬Xþié‡æú/õßù?sêùd‡×7âµÿùÙÿÑòõ·?{ûOÿçñö•ð7 +º—KX¿I0¼^¶ÏmÔü9¿¾û¢?zðöüßþàWy<ÍO¿øòÿ¶UħÖo¾þòû®¼ýü· +ÛƒÙ‡†d}Ãu»l¿åL€0¡¶ÕóRÊعû éÈt~5 Æ?¢6V¨ŒÔM”ؼå. ·Sèä¶cZÄà>ãdºÛéø ô4ˆ’dÞ.[ºÎäÒ‹÷‹: ¨\8õ§³,<Õ8¨Ñ<*u^ú"Ezh]–ÏåG_œÏžÞ°:ú²Uª ÓØñfáåÄ+x]\²Tð$ì®vgrÔ´âh¤I”ë€8«}œjçWâ go²V¹»3Å r£@M¬.z¬¾*À, RÖ[y’“¯J]â„ÀpŽ§3ÙÊAežU6ùxµHÎÑn…ö–xÄh¯ÃCâ" NW†t9Ö’j*C¬ë‚wTµ¦À¾ f h´·²m’Ò˜ªC¡S°<ˆ¼­¸ê¿šÓ‰úf¥Ë-‹/ ~i~-!ªM2‹ƒ’„=ﲂFøRÏ›væñM"€ÒN•ÅŠ+ªË˜®'dÚ±8qp!¶›±ETD-Ñ$@†±P·e¦%Pº6£ASb×mP)–½‰¤"0³ªN€-yÄ^cõHÈ!ßrU3Š^DîRYG!hlc eP½¥ÐáÔÛz@¶U%)äÑBP½D¤öúãÁþ‘t–åøE«VöþKxÔ¢…#‰ÒÔRÔŒ«M•Y@WN¯Fººi¬NMú¹ž¦[•$ +öžíÌaüYdJ=Î6" +ãyð‰+M™=‡ÁO ^¡Ä«øƒ]hãÝ}{÷…ä >ýkÅGWìùÛßñÕ7–U¼¥ò'oŸà}%¯‹ñ +e²AG÷Íí(I+–üD¦7($Be,ð&Ðä²¹ñöéoí;>‰I)zs5oËÊ>¡’ÝzåšÒ¦¶‡Á(~¢b4¥H›˜ªÙg¢Ü®©Û¸<5Ôö¢Û.HÜœoÉ:óI‚âŒðž=z˜Jv}'шF§=,‚œ]dËø€‚a£{Éú‘k8/Çøö)kÍ'AEUXöÃì=ü„æ´ÀLÞB9ñý¶Ó‘ÓM!âÉoGÓ)˜fÌD>ý Û'x Aæj =ÃÉÛH,´GüéLì·O?½­ñüï·_üïß}oQôw¿ÿʦ’']tn(Óû³/,“ÿlðÛÏ?ûå¯þ혀ñ7ßýþw~hÄä¾õÝ_ýù/u’Kÿ»ïÿÉböÇ×/‰ð4_ó;þó{þ·&ó¸‚D½ÂH¯QižãƒÌ+;7f&,‚`ÐîÇÝÕµíE-‚ÉÙ#¡(FŽDdñV©Fƒ% a€½ží¶Ú´£(ÄLXZQ v5a%DéxÕ ío•ƒx$9áP…3¶wûz5ãØ¿v5}àM·Oá0„{O­Ã¡$×9e,‡Û: +Ô¨®Iâq^§ +ÖNQÌ­¸pÀ¨_±| +Ô› m‡·ÝÎ…@P€ Žbþ¶þc‡ér¤wí:ºïÜ)¹€Ú¼IZE(ULƒãä¢m?&Æ ñ>°„„:b@5ÄŠ"žÛn?N÷R(£Éæûs……H,f /ÐWêq4÷ìòÝ¥ÿ"äzBѾY­3 © aë +Y¿ÉŽ[¨Eõº+üM}¨:†? —xT8pÙm,ãi°cHšŽ¤˜ƒ¾âWd÷u1`'Š°ÁüâJs\¨u4Ú`í«‹Ÿ]t‹ó¤ãRA²y•ð.á lßI_ŒÀÎÒ@'»âî ÄB“Ä3­èP³®í†q-Hº¡ÿg»#a;H¶Ð$=8NלlDbôÓ Îö4SvÎV1²rÙ`Rüt(–æ¨:©§éX\ÿA&·Rî¨Ò‚Y„ŠéP§¨ÙZ…ážNFÜ É4É?DéQ¹¢™}H€“@Õ.VÂnö6D# ÌßËõg=Ü8*ØÒñÇYûúL÷š‘9IºÛÙæÍ~€"/ Y?ÝzÐÓÞ*y"nÜd¯?f'å,®&œ™ÖÓõè6eH¼š§«¾Ú¯;P‚Ý–í­ƒ0L$¶ƒÈ9:g®º4=°8Blyò†¬ r2g°K²-·¬1ŽÇ˜üÄVR*ѼÊ؇Q6Oò ÿ—„l<Éü‘x{-爠‘ýoIÀTË׃H)yeô>iön Æ°ÉàùpÇ…8´e9+äµIyLÁ› rˆáZ©¢ŸUv@»ÈƒĩRÃufñöíÉð’ Ù¤—‘0V•{¼}› ŠÎr\ä¦eÑïÔŠ'¤‚9YJPdgÅ<¸5ÎXÅч¹ö +©b!´9It3‰§ SiZ3Íëé˜d–õ&ÅT¦ÇÚ'ryìò³¢™Ék©õ]ô-x¸•|w¶"\Vea^¥*¡ÕülÍí8Q3°/|Cݾð™Ã="!¡9Õ´óû%¨M‚ïLÌ5Ø]5ó£|ÀƒÒò ®¶MR´OßÙçRXm´ÚdJ§ää [Ž5Ýšk‡ºr ÈŠ.ñHZ¢ÃMѬèÝ&Uæì(ˆŒ=)ÀocG“'ìAª«Ê7öྃ;äE*yºXQ\,à%QÜçûMD¯ð@µ O)#j}Hñ„Ž1Z³URŒ§nh«å™3¢ • 4X¤Uôï°ÆæYIU¯|#"´ï¶Ü/vð¢[M¯¨)»ÁC§I½ÖýêšÛ8W^=÷øY‡j uI]>ÁŒÊGÌbrëîòEàêì±ÛuPä³Ø0‰†- <%’a”8TÅPO)ÎIA¢X?ºxDu‰ ½º(YIÀ>…]Î?o·ïoQUó±ŠÏc‡â±C8ë#Þ §sœ«º:Y·çnËYZgã–ÄPCPàâ×â†j§§5#ý£Fµ6•DypkiMføƒ.³;WÅ~ó)²Áx6 sqÑU…]tMÃ)ÃjA¯á¥Š ˆæщä›+u4¡ÖG±CÞöoŽD5¨ Û%9ŒõàÓ›†Oï1uŽ¶;!¹|02ô¾¹¡±~°x¹É âÙ_)*[ž*:™$ö÷õy}¢|ѹˎùffð“~šMRÍcf¬3,ѺᇳG÷rz™œA˜Chp°ïæ¶ãÈF0îl·íÕÉ#tYé" ¶¾]1Kh«Œðö—Tîò<»³¸¿ìÉÍ#%×J©|.tßl"À]èÂòε5€f{Á)Ìo|Ìò—HTR†8&ËâEßO‘¿0¶ßÖ>š“Gƒ¯ ­¤ûE~;Êm±•~ ¨vù׃=x]©iŸ[¢‡¬«}K~ \^–yQö£ˆîêQ¾m;É(XGØ7(™G¾’€—]Énúx—'q“亟ѬF®jÝÑY´ßÀ•å©¥´í ð'x§àOtÉ]6_i‹7wc±w,î{7 rÜQµ üÙÀ+¼lýŒ’ +¾}Äü·%fÀ–¥§¼ÇtëÅÇž9¥èÖ …›,„Ɉf‚Œà'¥ª0s …˜€0Æ¡ÖŒgKÅËX¼ÅlË$w ÁhÓSØ´y sé&‚ƒKïˆ5[ÐÏ!0ã>½El˜¶Wò ú9PTߊŠ,69ßoR¸-3Ì\¢T­}:?+¼"ü%À•,„œJªËó©Eò+_Ãk §n(1>åè52—?2oúØ«ˆú"÷>îКÈô8ŠJˆßù¶¥AÒÂ7°ñºg$Ûˆ/GÌÿ’Ðìç)8—Dæ à,àã@•Úàé)c{IœÀ¾°­9{„5MÆök.ÇbȦ†[s¸Õ5 Ò4Â^ç¤÷s“bBý“É àÊÛtâ+Íye·É(¯¤£¾ìʺÓKö+(çgWDù\ CÒ$ ˜\ß®j"6Ú +ü®»¤Œ*%ô tn¤¨[ óp'œ­è)ñvÈS–2Iÿj-™Jyç(ÒÕiÏ•®—Š+Ô:8ÄPë¢rù¥V {ÌÕ³ìf¨þ±Vz M.î^ÂŒ½–:1§AUÜÐ!m÷µÄì“Á…|ºžÕZ¡Ö¥¿‰¥x}©oƒCµxɉ€C|ÛJãPiUãZ ê +#£ hv†ÔÔV”éF„DYˆÖR¾™`úìêËÚ],q‚½Çü!KAìOM­Æ–û¶wHÚ˜×Yí©­qô—hèo„ßåU²¥íÞxÁö27jökÖŽ ·]îkŸ3oÍ0[ŸSšëöÚ¥QÈR™ZÜ4…xKQyˆ}+БÅ÷>§NÄ>jýÁk[ +«nÚ˜6Àîý¹w³0E +6Ðÿã-Í°õ4{‡m½}ÄÝÚGm7fmþí7wí¾óÔpœOvkLn“b63oçÔl|.óqtG×y¼uTï^†­»½Rk'w{+·Fð;o÷ky® [Ûy[V¶®õíò´5½·enë—oKåh²ß­³[c~[®·žþ¶âo€ÛcC¬ÐŽE[׎`¸ÛùvĺƒîЉuóÝ€Ï{÷×X·þ é±…Pä6 Ùp&[83‘)3Ú`,·ñÓ‚Ùâ° ?3"¸ tsÿm˜-Žœ(Ÿyn -pýìP4ãÞ †´…ÍÐe-(¦-üž¸§-rßÐRw ÀµÚòˆ ¥µå"Èë6§Ù0b[n´Á˶üjq—§í£–to¢á¶Dq…ÐÝ曯à»-iÝ0{?dÄMÞ|3jI»7°á–±/HÅÛ¼ƒ9îgY’[ bGZÞÕ29ëˆs+lÐÛÌ!ÝJ9út+]àÕ§"Ò†tÝjPHv+cmÛÛ*ØÑݪi¸w«Èí á»ÊÞ†1^ëƒ8y7Hó]9rDÏbæ¡Þª x}[D]ÑÚ[-v»·2î +¿«oˆòYJ^!è[z°?•°7ÔûVßó[}ÃÛßã7¸þVÒßþ³°ñn› ûÇצÄÆLرá¶A²ñ"¶þÊέX[3;Gã®ÅS‰uõ³ yokiéWó«“^º¥ÛDT¥G #7éuºéZUrÊf -s­¯Ý/ _>zA¢×¼5Ð= ºv=ä +÷¸Šð :üÐZÃÞÎS éBze¤¼·mäˆVYÖiɬ­EHɽc,˜°g|nÊÚ=Xï¸n"Ÿ³ö9ÙœCÌ4ÊʹìýÒ¦ÂäCLùÒ£Õj[ŒûÙ£˜E%ê¹A»jíïîß¼öˆ·°¶š¿¼½ kËz¿kë[èñõ±xß|}šk»}Ÿwmûmj­ÝÿmvNàÀ6¹ŸáÛ»1A +ûkµâ>N¡âºžöD¼ªÑ%vp£1¹øV’%aµWO––H0RìÜd‘-x;OÙ?¾DËö0,Ƀ^]Ã7¢u&“Úe„~7:U'jóPƒ»ótyFèèAq jk£BÇøi®>£og÷+[±.¶ïA0 ³×J3ß\ï:Âçèú»·ól÷ŽX:<ægE´—wnýã9ýöglŒ³59X3q•´õ§!(²Êª‹g±ï·º°‰›zÙ¾>‰Ži›ìþw?]…š“¼ÍŒ»ïÝF,×ÿå,½Œ’´¸\9 ®@*Mv;Ñš°EÒ(š7€·û5Ú-ƒAò(­YKDjZ¢úq7çYïå~-{&_¾>Ø_^,š"IX + ö$-¶Lü7®«¶Ð +ùÖ F{¤ÈÞ‰jvyg¢B·Oä³0ƒ$}%ßLy°]Çd{i Ò7é Ð3‹¯°ýÓËû¹_Áújï¿än‰ØïÈvží®®ËÕósXß²uþ¡Vòx¹–ƒ Xœ8«Œ®çéæ¥ÒÎ'nߥǧ–iõøžu¾«ûòöÕgƒ˜µÏôn}·½òX_MèþžÍO­·èúžwîÝ| (†DL`¢ŽR^7™ØG•åϳ]U4h$Šø£jMBðd‘ƒ³PiWFo3»†Ÿ6ÅŠuÙL8ê:ó’Æ :çøÍÁZ¤,‹n2fM¬a¨´SHìÃÍ3e—Þ‰ê(K #Þ_Œ²ˆßGüQ’2~¬ÉÑn—Ýb€ ÀÚS½b£³E ÁQðT¤Èí!Rìsß82/™ëAžw…:™!i²¶GYIB ‰‘ʨ> ¬r+5{Zï_C '¸àòðjO°S©Õ†Fª´†xì®™Œ‡'Öž©¡\sŽV:pM‚”éOïéÖ ¨B²2xM¥Î^âHXÐùÆ…ð'êÚÅ¡[U+ÆÚèÜÌgYÆ øؤ}4‘ÔíD˜—Hl?žlº%ë% q¦û_ЫWfKÀ7ìîNPÙ“É…¥Ð’Àß_ÁZPþ!e½{*Œ*B×Xàá ÂòPíípúu>p Ͷ/ڒ̦ûyÅ Ë4ÝÈu~–>”­Š½è@^Ö)üFš*%{»ÿîM)mØ!#Ær*|ZÞ¸BÕ•Š!•Î@r·¾µ×O½{ç…p¡h¯<])βv 䀑Ó.¿<–"ê‰Ð(ÕÓ뀣-ÞûÔö…À{!úŽ±‰Q6®—p‘&LÃÐÙ.!‡°ôAÒïoû-óˆöQ `}Ê~çïò–íù±÷fœY"N£{h‘g5„ÚÒÝT2eš‡ÃÑxÛæϵäÍB»Š„ §Oõ×I|Åb§b‰'Øú2`Ç…r‘Ÿwy—äÞe{ àUŸ–©?½Å¸a+,°%ŽõÞÏëï>>.A{ñ²Zœ’‡½‰£›¨Äu©z|hYå_³.’··-¶ëoÛìy[æ2ÿ|÷Ía}û³>Æ}£º›û†·Î¦}Ó\gä¾ùÞ&äÛ&¾¾[°¾bû¾{U÷°d}ÓßhÞ‹ƒXZ$½r ©ÉOÿög÷:C=ýÉ~ ’E/,›©o³%ú‡×?~Ò¤0m9mÍHj:8˜¦ÃVAj”®¦ó$oó¯ÞÑaýoBãæåFß Þì7úKÚ ¢`˜Öà]ŽO™ôbĤm’ÊIQ@‰Žg2øoØ w„Â=2zì\ª°Íu[B$’W¡p¸+Ž¹ƒKo©í¨H W NMÁa+àí‹ÑùÏ;D'&\;Ò‰oì+Ø¡É*+12ô‘—C‚ kíÜ(øá¼ÔF@& G­WEmæg—ƒë{#«ãבJƒ¯±ËQKÓCÖ‘} m]‚´ž„À©f£É¦­@?§÷Š4¢­ˆÚ†§ _‘Ö‹u‚°f—éjã à¤6´ .óüäU·;e|oÏ¢a‹j·µ²fI_ªèf¿X9—ÃNóø&´Ñú)9É&)Hö«"¬¦0ÜèCZŽAæ9'S\𽎼âzŽ°^š_Çä•Ö„òâ0>:Feníƒ2f\뤷ØmjÜ»Yê絓ӕ”ØèÄ2ú§%vx`‡Ý„•îbµŸwh¹6ãdPq};>ûÙPW³ûf?çx;Qª¦ác‘Ò'ׂRhVâ›ïÕ–õÿXî1aø*6¢¸@ÒX+EP¨¹`=>êU+%à^¦òɧßPòû£AÚ‡¤ÚødÄû„ÁÉÙ©¾4*ÔöÓÓùc¿MBú£¸A}Àøô´Xæ·«ª=> +ÜCßOãÓÖùTÙ„ÿ9?Ð65~ ;#ðÕÈ´Âjÿ~!_Øð‹¦ÕAGòÿÊj›%ÿ?V»›Ü?dçᆉpZìgëO”Š»­ Ú +i1Z±¿Ð± ¡U±ë‚ŸÚ/m¶5¿Ó¡ECÁu•ím¶­#»º28hÂ/u›£iMµp +ì`RÝPÒ Ä’'€ endstream endobj 59 0 obj <>stream +C ôjPÅÑž*µÍÀ¾¥˜yàN]]w¨:ø葯Òèš1¯ã':F*I‰žìIô¤ô Úc æíô'É«A­Æ†qƒŠ 1ª×ä™öBŒ/h:¡·äh•b"„|òâ¶Õö*×ä©d®ð&TýD¼œ)ª^tj>Ì¢›imçQë¡2J.}E«¦\bßÒÎÃÍå – ØêÉÅ/g9Ãn J–ìÍÇìòØ•ìW”ue… õÎï8FÂeßv´ÇMVE,4AÛnŸmÖo‚P·üzóéÐPeÄ—§Ý=:õ¢ßåÜ<þ.¿¬„·»ÝÄòÎ4‚Œ ơض%»®m:¢ž +QÀjÔs"¦H|·h /•ã›uuîNÜÁ›ô²Ö/¶lÙDóŠÀúÂ{6t0'eo»ÊJÐ/>»8†úå^’¡dµ-õÑ¥,"à?õâ;zŽph{žä5ör« pzuúu"`â§Éäg7ó)8À×½óªbmÓP&irBÒûÔ>hIñ“滊ØFtŸ[öfôY¿¾aûyÖ—t¿–õ]¿ÿMëJ±ß›uµÙnïºVÝ>¥mÍÛö¶nnóe[ïæݾŽ¯ó÷fXÞuߘ=—å!òAX¡#PœíÁ‰¯w§¨M|î‘7™óܵ)£n"Úˆ®S¥°×‚]”¢ïD´¥¶~fÞö°‹höêc˜kF®"Úö9,Q›nc¾Ñ& IMþ »ÆMDY{¹'¬ò.¢­Kž„ô÷"Úvin‹œ¤¢Ýwíˆå5¤U;;¢ÆÞÚ½d6?ws~¦ý-o¢Ù±‡ß®ZÙ(Ï9Ù<,á^3;È#+µ˜’ll×̦ö<ßÞ{Œ~Ó®™Ýãö"•íj”˜Új•Êæ¼ýœ—`xW¶øK°2l8À¡ßkes÷H—í®-üs=×ÎF g¸GÔ)û¦h‚vj÷e÷~ôøφu‚õŽ2=`ž]6­±ü¤>±ùÚ›%fý‡}]Å ®€½öêÈ¡†—˜,;mYV$ñìHùr YžÔ³mÊxý¢Õô]ÉâBØ;o÷¡ž7IÞOõãðcv2#° ¼Ô³m…È9äøßGŽ÷ÓÈgÿËÛëÀŠñýÈC‹úÃ^ê Ê*n?ÏJ´úìàoX º¦CK»ÏŽX1áÕ^GisNx}÷Ùê¢mÍ¥ñ‡Õg†•°¥5[û“ÏŽ1˜ƒ‚Ç+ñÆh‡ —1mÆøl÷F;Q^ÒœZÈYw£wïß4Úa õwн3Ú¡Nêe5***Ñ.F;‘ä¦à O~×ê´Ú hñ ¥ B¾sÚ‰âã‚â²-oÑÍi' +3qŒ¬ƒ‚òâ´hÊoŠåbËÙÓŽLbTKä§B\vĨ +TTm;µî9pÂx‚¸¼tùµÜ9íh–ÊÖ¦E¢¯»ót1ÆOyÙâê2¬vx¾™ð®hÉ·û“k8…Ýa¨œèÛ«±D*{6 £VaouˆŒô­öÐNQÌ_`6BÖ©¡êr[»,µ\1æ¬x©ÞÚê€n?Ž[¸ c­dQ*w[Þì²±m_y<´ÍV‡_ü³Ó¡êŒÉ+UgØD»ÎG_™Ypñ>½-·a-ºmÃÀ†À,gë¾@c ¦ËŽQ¿Ùº/ÜKŠìv/Y0ëK÷…¬À)IÍpgõÂc°[šœÓÞ…ÉæP'#RŠ»ïÂ`ÝxïÙôcK{FÁ<èi‚ù®Y¹ta¬³èHs¡öÒ…Ñ2¬_Ù-‚ê{†ó«5ZcËÞƒ¨ôC,ê´'q߃ 3†-@Ç ’®=g¢åÁDô¥ƒWµÌcñìŒ9Ü÷`ÄÑ7ÙOÒ£Yz0šþp +&ŒgØ{0|‘’ +‹r¢îØÞƒqf4÷Å–Iôªf†TPþâ&ãÙ¯ ¯·¿œNã}†©GÙvP»ö Üä>NLN(~m=˜¿ÿìÌëDÿŒ· ŽÑ.8÷v±¤Ââó€u؉ n0ãƒe8rWû =È߶Œïòû6›°Ó3zAöË×·þÞËÿi–.­—Ñ…ÿé_wÄ¥ñò߉§Í?·ñ²¹Ø„îPg{KOÏï]lÐ$¡ÉiÕ–]ñ2 +3‡UyÄ ¼_b™ŠÂÈ«m™G>ëEY“õ›Ðyªoá†d1ö•íÒëdû&ƱÿÏ\~5¯Ù>5µA }=±àå8Ã¥&Êzuè¶Î§³¼hr:ü-,@{fê– h'ÌÚlæ5 +oØ“ˆáz=룲aç'Óî”™¶Ü¡Y=ÔWÛQ¶¢2Úaü=I¶*¬«_D&‹—Î+kµ…~Å&Ã% K±ÊÞêop<_ÔdµP–„ËjKƒÖâQ¯¯e‘³.6<Ûþ)~[½_]lH8t¡¨ y\/i +â# ¦.6Ðé=:´;zÂì»SáUÀ£‹±S§KÁ×}ç[pßùþ0±‘½»¦}`ÂXâMìÜ¡I^ªÂìyòZªÃGS¿—,Þ6Ó©xÌFé›mVt©¦FrÐÿ ZózQVæh€H + cÇ©ÇLüà +ºÎ¶àLg…8X{â¨Àv'MÝ.ò‰ ì©M=•f•×&|èTs%hZd‰‚?éTóã4/- +èñ¡aÍÊ¡{ËŒ8àV9ìíÀ³vö~ГJ˜æèñP_ͼªóïÈ™(äkh†¦ o=uÒEta¦ò7dcÞ©)>ÿý¬'>ÿ6ÅÇ•&Ü\›èL¹r.ï†ßáß=‹œsëü ciÆ5h;œŠf-û<è͹–ºËIübÛPФx`—ž CLi;’éC¶ìG4{‹ºŠÄœ†Ð;3Ì'QŽöÅÁF%v©f0ј~6"dgXæ½ 9]/ êÔÜ ¡äë¥BlÌ5ÂØ@ˆ÷ü&ÂÞ_?ßZíšçõvSyƒDò¼ôäx®`Ð%»`wãáX#€rÚ@·õ|]Ÿ¸*žœÛµ–aӜχO¢$Š™Ï_¼=¬®ÓÌb¢ô° cK®/cìëª!L72»çc‰o‡¶›W‡3¿G—}nÿ·û¦±SÛ—¼x«•hxÎÝ«Igé¼v9§ýeG<%MumžaFÉtФ´>·[X©Ôžmm¤“ýâC–¦Â´k+]n1ê„è¶Ûæ¯˜Ñ Ñt=rpä#ÄÐuµ×pÄž²mjB¨d™ü&Tñö‡Ôk}=1Jȇ£¹mØS¨y•âWœ³ðÛ‹j³">"±õÀKض¼b<" ‚V7ÛˆÁ  ~´1[gò« Œ¸Å»î«Öuºú±Æ÷ÿþÓ®¯HD šëHA½ºbgä*6¼%“úpÇä ÍÂÑŸÑAa· +-ýØ­¢ût f/;#vJ;ôÊ&`'k®ôTÛ#ÿ8Êx¼‰|ÿÕF/xÐ .ÔЕêpûék±^S½’# iZ_OïØ‚r1ˆX‚ÔQoffDé(õÎÌ4:øsšg”fÅÌOiT…™1’]jço¨q¹K7=êÍW’ +µêHf»4ãKâK™‹ï¹¼]Hôø°tɾ<çÞ…’*ÑÇ4piD¾¿Üh-kÐÖ³c-p.Ì=‡»m¥á¯‚‚æy{©9Øy’³@Ѳ0M— oF³½T=òf¹^%äd)ÓÌZŠEvÄ¥Ïu—9äá0l÷ù¨æè7ôüê[lWÂ*7ËCö#ô8·‚ë¢ú¤ äZyÇB… +53s“áÚrz¦à6O¾ÿâ“Â7†‡= +` Y”$Í;¢*Ð2XÊU“EûÓ4>á¢^Íß œ%¥r›Í M³ßq7 ðáú.X£üIȶEëæxÁ´]ægÉ<ßfU2¯âΡäºöEöçúÁw~$ãÖì.$ó^®jE׸õmwñÇ»ŒŒ¹p«Ð4æͦïtM´UjŸ§wS×4ߪÆû°‹[—ç^k¼i›´Ö|%7U®ñúÞjz]J`s)صÃƺq+=6Ö˜M¸l®E›æÙX·^tÒ|eÛÅÕÆ +x©±UòÖÐc¬¨›Ü\zwûŽ±Nß»vŒE}“©›«ÿ¶›;ÄÞÜL¦„ÞÜlVͽ}«º“î›ûÛ¦ü7öÁM3pî™·’ƒsƒÝ ·-yS=Ü·ö'ÑÄ- ؤ×PbSkÜ‘[ÕÇ-ŽÙÔ#·h +O^ÓFånmZ—[¤¶éenÞ­îæ'Þœg 17Л7J¢7£EÒßXÂè[EÔ- +ß„U·~•dÝÜ)»î£V…Ø™olâ²[–r+R»%9›Øí–m‚¹#§ºSÛéצÕ;ó´MÝwÉín­5¶¼pJ oÙ䭵ƖŒNã‘´nBÈ#¿½•Qž¹ð¥½ì™ò.Ö<Òê{­ç‘ƒ?¢=Gߤ¤G>/Dí©ÿî‚1K›ö('¼èf‚Ã&¶= «N÷UŸ“ù¾JC|-¬ªâ[]åVœ|+Ël"ç³³é£ÏªÏºú(m‚ì³´i¹oå§[Mø­xµiËÏr×T£ßjc·Zö³¢¶Iáo5¸UM-àÝIòoÅ¿UØ/.†kÅñÖL`«Wn^[©sÚlÒ[¼ºy(ìeÛÕ‡aqçç°ZðVPÞAhK!úÖ¡b«co§ÙŠãÛ•l#nÑ6j»3kÝ~¿»kÙÿ§´t ®‡¼´ö鱶(î§ÙÖáX'ëÚÙ¦ûÚX¹}iÖ¶Ìöæ­ ýå]ûA÷ïþÒMºVŽµû´¯5Kïê~ÉZ;_ÛÒ·5ͶUsë¹Ý-¹[Çn[°·f߶ÖÏáýV±6·gv"ǵ·-ïöµ½ë¹n‹{¿tÝQ·nëó6|µf×={kænÛýÖ ¾ ¶Vòjl]èœl½ëÛfë|oÑÖ4!ÔÚh¿ »¶6ý´mþåm¸€[‹ UpŘ+áÖÆbƒ2ìØbä DqaoŒ-<ßÀ{ˆ¿b?îS…:²§l²')óÀmŽ3®ÒŠ{Ù“¬6sŸ¬­ ›=é[á:?dÄ]Þ¹Zó×f´§À+Hé>•^1NûyVxÔžÖo0«ÛòÀŠÒºŠ ++®k¯G¬°°ûºÆŠ*Ûë#+ m/±¬¸¶çÍÀÀme 5·—†Ö´!ô¶zÔ +íÛ +Y;B𮶠×rÚ†Lœõ· ÏxW´Ûгҷá'·ÚàŠ¾¼­,®ÐÍ­.¹>·’憽+ˆn€ÓYEÈԭܺáYŸj´v«ënøÙ­$¼Áoo Êzw«GoÀßYÁÞà·uïýãkÕ|â“·:û†j¾-Óo è­Ê¿«×Áоë/¥4%™õ]ê¤KŸB?–À˜›oüÒXéÁPRâ}$JgŽ7ÎMýÆÁ"ª«(­‹yoÌDZŒˆ[û§­ÛÖ3/Y•„ê÷™ö®ÉF8gk¼ö”µ¡Då!+Æ=í¦5>õª––øAj»ßXVð+9±]^‘ÆÖ7‹ªkWƒ-’–I¦yzuª¸¯M»ý{Ö~ßu™wÆ×o[{‹û­¹ó£Øn±·3÷g³v?÷g|×<ݦÊÚzÝfÛìÕn“õ¹±»NõÙÞ_’µküq6—½R°P©F½Ã;šSÜhÙ³%õåÆæ ð0ƒžW•xCÔN$³q¯6OI$±î#ǧy;O2a´ E¾N»ò}ÔNoÍâú£½NÑýËèþ~«pÿþCî ö²g»©«Á»ÏäñEŽy·ƒ‹?Ä^rg‰íï·¾ïÚ]×)õZñ7ß»¸ñpØG­^ìs)j>\}÷“دñΗ‚ˆ·u‰ŽHúwç·p¿„wnü—/ÏøU9_(0v‘¶Õ¤ã}3í$­Â;vL”ÖoÜ*HsÛyc.QÝzàE\ë¼zIÌÓoî0ó¢îÍeÆOY^?~]EÖ;µÎóuN´ ló=Ns}¶H–àö½±{(¡;\:Á:î¯]¶êNÞr¯7¯îúCoWÎÛôëb«éf!‘†}öùi ‚;¯ŽqNµ¢gåö÷?µ~ߪ¿]õàüþãWáúý®Ú÷ûƒ¸K!öºjñÏ©°ªø¯ÓèÎ `û±«¥À6›WW‚í¥x65Øީ逰½«wÂþRßy0l‹Ã´pØÖ•iú°-KONÛ¢v}h]¯¯Ù–ÓÛ«Ü–åí׎ýº=ë>ðrS·md{&ÛV´=ÖmK»Ûָͱm{ÝfçÜo'÷¶¹o/É l/Úöco_Ø-`ÙÞûwc%0byùÿÚ¢RÍ(„wÊÿÃâÇCü Ès3ï[CÓ¶[xÑ–¸š{ÉòW¨¤¥:å)TKÀ•S{ad—Fe;ó‘ßÑé-¤h/rø›PsP>vgi°¹¼*DK³hÙ@qðFž[-œZ§ 5¨½ÏÈh +=}£fÍgT¶²%¦yçU›lGê]zïoD¹÷`óI”› KRY¶vÓ_»ûx·=Ù‹Äz¼]@vu¬š,@¢Ã|û3ÒØì’,m÷€†‚öa›ñ¼¹iÔu“ÎþË=§,¥T +{¹yb’ "2ËZ…ovFµ˜émÑOï¨rG´kì6ÕnT¹Õ&<¹ CAlN[”Àr$¼ºQÞ|hQ$Þ¿xU5ÞÀ:ò~#V•åí^®BÍû“¸|ޟ袽O…UzzŸQwÖûÄ\¥°÷ɽÊiïïÆ,÷öŠí§Y_ÒýRÖwýþ'b»#ÛÚ²ÞÔmeº}6saÛí¶&n³c[[ogÙ¶Fo³u_Ý×ÿÞæ0‹Zÿ5ĸw¿—b-⤨}ï†ä’Ë6Ê‚_‹BnÜZ‚4]m„%š˜lnH°á9Žjò ¦à´EjöÄ,¾8iÆÞo|‘€8tÚ1¶ach»û")`æèÆ´[}‘*´üxc‡TAPú·Ø†¬p·Cª§ÿ‚p¡Æ}›/@l’mÄúÒi›/RiÞÂK€Øú/RanÐ3¢0QÞóE*ó› +ª“õƉ¸˜}žnCq÷Eª +ZÔ1rãõM›/¸¥Q'·¦AªÜŒ‘5³Ùkß&#Ý#¡È *(U[¦Gçf’Zp²T’fÂî‘DºA¡Ãr# +/7fIàç)ÛÝWGZß´¹%qžbkçAtwwK¤^:ÝÜárã–´¾=Ÿýì¿Ì-IÉñp‰dd:…Ì*ï˜%uYúÚí +°ÙìÏ«±Ï'«ÀÜ­W’;7€ ª°6³$ÞlÉ/å$”- +¾uúã_h©€©Oð8ÕOÊ€²`KºøZÁbŒäx/;—Ò$Opêl Ì~m#ê¸;–K¢7ƒ»Cƒ}«rtåã¶E÷ÀH Om»2„‚D„‚h¥üÿ²÷®;¶dוÞÔ;ä­ê(Ö5"~Šå¶[v¹-¨Ù‚ar©Z¢ÑE +EAoïù¹Vdæš+yÙ%‰jK/µ3vìu÷9Fö!ˆA qlb·7„(XjÒÔÉŸ¿™Gcªsàß~{Ø ÄÙ +enÄ XŠÈ³N&ÅÃÒóÇ(|gÌ2Ù/¸X:C´ ™PìXôV§«¶ˆÕY5¤ÄEU¡E‘³¬80²½° ¯­nŽcm^Ôcìá‚a%tj¥3Ö†³øä›&± ‚²¤©ìQ·ð /;çžC£B­!ÕËGµ“T«ÛÊßYEÒI´Ô2hEJÔ^\Ôö’±6pk&¶i÷Êc¢…vtŠ¥²èÆTÌȧˆè¢¶žâxŸ)õ5 Ón©­áUóI›ÛHºÎ$x²XÖë¾|þzL"¡vjíš_f-€ +òb»ñÉ<-ºC +” Î Ö,ˆ³Ó˜¥œ…e3‹¬ +¼‹M¢žÑIy­M™³½Þ›b\ªx¸ünÒVž†z¿6[áea›(ÈwWÖ«à3_ê§Ë™äÞÝ}„4 Q)ÈàѪš²ÍÈí0Õ˜R±ÑÁ‡ÆöëwËåö»‰‡Ÿ¢wg|Á +O”—™À#*¨ùIrp í J(&\XBþéÁ´h +yðÙŽ§$sfЗpïéæW¯™ŸHíÛäsâwàµo”Mãæ&Û²š¢u¦|n¼_>•íLA~€¯¡ïH€pƒÙ +[ƒýŽzˆC–2¸rÈQ˜óÚÏð„ +!oà®^¦Q~‘Umóû$(âLå8=—w`@$DçD9†L±»W)AîT¨æ +{öÉì«©ß¡¦:-¡Üôê)¤mƒMªÙ¶´µ@<€"GQàOm ôNH³Rt”lÙ©7„èJžô9bng¸.ðW³ì…¶'Ê幬€tÒJa4v KöÅîh'M¦ íì@J«mþ!Þí ßÒÆ@ÕdÇ᪠}Øõ$Ef?ïâÌü³î€[4·\j±mkjuÏéÌàåÀÅ#¥Âw1€µjW™¾ý%ÅïÊ7Ñ$ ªž¢&¼‹ +ÆÌ]l¤¾3Ý“¥Bˆ»Ã©'G§ëb’bßJy\˜½ ÜÖýLfeèœJ<_ðGÜv3âF)0^Kõ$kÛÖ5W ›«Í/ÏÆáª\ëEúÉn¿=EW Ò´™IÌ—J€ý~roF*ƒ "s~ðF°aÃbS€w*²ä¯[ºÝm[SÂY’‚–5œ ›½§z5¨my´õ@χ«Ë1ñ/hñï:[¦§uaj”8wo¹|ËÌf4_–=M9ÙB›¬—(ŠOPîlÇ–Œc$6¢3…FÉ P±ÓYý*½Qñ¯6—Bå§Ù¼]»<¤èg,ÏÞÛ½”Ø£ê¦YÛß¡¨Ì.É©õ*Î;à“CzÖͪ  }õÝÇE OÙu•J[˜pÖ¨Jesµ­Ã8î§fçTì”]_I‚ɵ]@ýǦÛÙ×Ƀ¸ ?¬¨Ž& @×íæCÜŽe}yBo[Q)+z›Kn©W5èÛ4]ÎnKEËdŸ¥­¦fYYÈOè<2ÿGÍuˆÄC«fxõ„öœªý<:?ÑvBP™fÆ¡—øÚNZ1H¤rÐPWVõ¶í(Ýø„cÀ9£=C—šéœö"[ÊÎ¥MÅìí>÷ö$îÄTºçªj=ìëYAhº«NA‰«Û,ÎL/² ˆÞ…Öe“5ý|¤QÇä'O´ u† +W?b§Øî.v² Óûuˆ:ÖŽ 6º`1ØÌ[ʲbå‚ZŒº‹|š +#Üf³>'xƒÍ·J «` È8ír¯¦. 3œ=Èek~¨ö¢%½‰n˾;´FÁ÷Ç[ì‰ËCX¡r—¡ø ¥Á.£Ž„w•*¤JÏ)Ð"ŒÒ~©i‡ö˜?ÉqeÅm]ÞËä‚rí¾ºo‡ž.-3o–Ä% û%ÛÚ„§W5Pê¡8F¶UGQæz¶›Œëƒñ¿ö·vã7Ýìa‘0{d¥›GFôÑÎ`# ×ÅEd?o¦C(;Ã#®Èù(RGYÕòš¢¢}Œ_ƒB.|-’Bò¥ÑîÖ¾jÜ3ݨé'ñø ²Aαy×®˜à!Âô4«Ç_!l‚.SçXj(M¶ß¦ö¬Ké¢ùr[kÍ)lÈC`WÕ9C„Ð[™.Àôly¢¸scûxIFšY¤à/Õ¨dŸÐ©e¥ÍUg £ù:ýý8®î5° +—w˜½„Ç\½%’²€6…øI±ÔU&ðÆîCÛ ]ª6VTÎÑ`½é!n°Æáb˜n¢²Ï×¢(LF@µ3Ž +©v]Š +/[?;¥¸“·ù첫¡Òc>ቫzÏ(|lÇèøˆO­Ó„ö>LšRÜA™BÌ0¬Tµ„eFšé’kû›Ü–7Çö$ºYRWC”ƒQ†žØM)>µ. ì»$­l(øÆ%./„ÃJØ4œÅ`¶Û´&NÂv¡µ®5À›óAËoƒí ñáöt5Úóô~êCÒæp68OõVïæT3ÙéÆý|îï„~èhú!SÄ›»Åîeb[ÄhÐ]n§ÙcIÍRÐj{Ç/`. +ÎP§5Ê +REþåpÒFÜ4úEmF&VðÆöb 2Fúž.ó¶(vŠâO±¢„‰Ó&VÎ(B›(â×aIպń„d £7vr¼ÑK­òš$¥©]Ij}7K:²­Ñ|9ŠÌ÷D¹®šˆà$ è‹d2”A›1Æ3ÄüôƒªÙ­V´18‹ÛªTÐ ¶hv‚iMlQ17øÒÌ1挫ùx§ß¶š½ËüPÏÛj&˜½çÜÏ' HÅjj0F’^À_tbc²4ÊÚà.7¡`’錦1ON²=AÙA‰æá#/zNÂ}Ý[_¼'‹ò¥D+®á8*4xAÞW£%Èîç%°õ¶%Iµ–˜9»*ñÅ2mÓY·U0Z·å Ðº2t¼Õ8öwz$JÖ‹XÇjd7ZÔèì"Èõ_kÄU#ͱµ¢ÑÏ-%sÑ)nNCt|֧Ϛ6þ×Á»@yÁyLuã»U§ƒ ´èÿ HmE{¢êÖ‚-nN<;‚Öˆ>`u‚Ù9Eÿ->±óãS«?Ix–’’ÁRÁ'Eâ{²d<§iãÑ2]±¹#RIM¬n1g@qÄ?žøó·ÕGÐÑ-CæFtu!ÀQaLöúänc +Œ•l£>¨åݼàˆÜPCøo¢Tˆ}ª ÎþA”Ä|ÕQiÐN¥´C´…Hº®á@s„0M:C %7ÛpOSêVáð“ºÛ6 +ùÿ5ò¤½l‰.À¤}‹%Æ¡á7á/2 :^"gêq»èR[¹"p jxI±Ú‰ÅHï±=&?fo@û¯Ñ@äŠ,ø~€‹µ*¶á»ÏP$Y5h°Éª§®ÄÅläöíTš€µ/Ö²†6Êø4<1¦Ú€‡`§„¹‡k9±•Êv°9(‚("&ØDs+ͼÿxè–`p;È:ž›É:nâÉ(b^ >EVdî5M2CË4§ý +!l”"x6Ð"ØbÜM¢³»Itµ}0d 9;õ$u1„ ÏÓøme†óQïêñ>p‹¶©2‰~N`‹Ú“Ià¥\^ZhÑ I’pòê;­x¥íRñ-žA›*eqºéƒ(¬=dÛü ‹O]þš½QºQWÇvºçM¡Yt»±µÉ安$ö)gP×è´.ç(&=Ðsں䖵Bþº[dÿ +Ìž&lqhé?lâ§]y +¹|}ÀàTÁ‹RÏgzÈþ˜±qáxÝ–ÂÆmÖF^ތ֛ÔU›• )˜oŒóÝIôÑó/±äR¢^n’Ø{rö+Þ.á˜sYÆfsc3i&Ý]#ÄY:,*pMðŒÁQÑ’UÜ™tŠi7Ó­Ð0‘@±¿~IÇÍ~I¥íPý¹]h*YH}±9èdÛJ—9T*°ÎQ1 ï0/ñ€ÐKñZ=âX7jÛm<ÝEYB¾JÅe[¢Ó$(H~‡j]Ä÷(¥‰Và-/q,×éý]‡iOZZ✮[/ßm—ä>ÜŸ4»Ó5uXZ3Ñͪã¨zUð4î Ïnû)$‚íPÜeJ‘ô¸Y'ëia&X³ei–õÐiÀê8/a ·Â¨„âM®–õÜW5ºq.•×}7¥ÂÛÛS YQú¶aÇ~[/àUÂÂfg³h›{,ø°ÎY¦ç¿—dµ`%\/S˜yHÚ…£`ËÓñSiû³y\*i˜¯]¥W¥¤ÃŽ!<Í€FéÉô¾šº NÐMØàì{±Y‰¢aüôš–UnW´°í¾ýÕt椥f›ôüE>a:n«æ¨ÀÁ ÛW¥c]ñ‡ jb jƒòª¬ØÀñ>K«gøN VZæ³¹Öä4Ò( +x§G+Ø8TQš;GT)h᪦½C3MS¤e^©ÈN¸¿xyöÚÕ0Ï +M¬‰*ˆz3üH`ÍÈÐj‹ðEÛ‹S±‘°»/À—²­¦ÙU2±œ }†ÐV“ŠÕ†éW ö²¨Üã°˜Hæ2GNÑ‚ƒ„rnÔÔ5-¸ÕäNÚ‚îd’+»“6s;€øÔx,WŽFiÅ(Å_'ÚFQ”¥nÓê:´˜œnj¬Öp=è« 0›Š¶±©M)äq(À5Êu‚iŽ$Ôï ì°ØVóžSIÆ!›ÎݦJ7ÌÎ ÒߌÂwîFš×Ôî¡"ëÅe©|6áŒÚà˜Sp}n:'™“i«é¬”VÞ=+ø¼°*ª‹•/5Go.<ñÝð|Þy…ñ=ádk©#¦µÃÜõæ]5?—7¼wC+»×=Ú\‚ÓŸX}g;ý§UcE®íÙõw>8ÂPêŒV,‚àÇW¡Ãck«2mžf¸`àÜûx·•Îwnk!ùB•{Ñh90}–[ l ­$I/Ðö¦ó¶Hα!O.ú%YxÂÄTE'V^ ÕTPø.:rJ.Æ/­!ŸJÕ$¹5ZTÍ¡ÆEAäÜSŒ:UÚl8šÔ%š}¶ÿ]ôªÂä‘€ÀÓ +´ã—¤>/ºOO‡hZ%cWU*CIy 9“£Ÿ$ì‚S“#{dðSvi‡_#ÒvDò0s[çíe³+ç_Ú3Ù—Ôz°ÄÂM |¢é³‘zÍqó–˜z¥BšÄ™‡x—·uQÛËá…óÓ/ñ}­ûýÄ,ß%*-"—t§î’ÑnßÆ"Y ~÷Ê\ÕÜ„ôT¾Ínðr&— +xÏš!ÑèhµáT°Wk¢EC¡èA±•IŸ³äk°8À¶Y Eý%ä|*xk2KM|Ë´]sG2A¹øÿ™yò‡ï2P6.°×2ë½f±j¹U„ŒTź‰Ù°Ú@ é²<ÃŒo“jö÷q¶ìŸÎr¾„¼\å3å®S¬2)¤÷ÌÀ²oè›xÏôÞ’¬ m"Ô×¼"+¯¢Ý‹®ê3¦'+Û$²\’›Ø•ÒN6œCí9k‚´ö$¼ÊK5-åÞ„KB!~¹„‚þmè&4„Pì-XÃHOkÂ. +:Ö`VhŽá°Ðd±¦…f• !²GvÂÐw¡we Q†˜m¤3´Ò„ˆi虡ÖÙ»³‹Ï®?#¶;…ÖXpè.Ú†”C“RˆL¯}N!¸Ú¥¶AòÐv‚í¡akÆéC¿×6ÌÚÅB² ´œ…„Ch\Û&.â{FÞ#tÌ…”Iè¼Û¦^B_ÈÜÄ&À%ç{ w©£>áŠFϘ-)(Í{š¹]¾%‹…ý¥³èZÅdXÇûĺxFæýšS3óë6-ß$‡CZî$CBnÆC¯å6»gÂü +;€4¢¦˜"”ÑI„_^ÓŒR3™· èQ¯°d+}ƒŠ{¨‡…Þ'&=ûå„ZfWÓú¤LeÞܧmªÚܘq=yƒ(ì 4·ÉÚšŒ¸8 +Vm³¿ñ=k9ŽeÍD‡™¬ íï¶ ²&Æ㺮 võЄýYõ믩þxLv%á´­•áÀ†ê…pîß?„k³–NÄ›·V_|¾˜‰¬DŽP³ðL +‚æuÅs¡bœ:)]û6ç¤f&¼}œ¸—8Q,£Œ2ÒúÈ6uÝàˆª6ú µÿùƒ¦}0Ûñ¢²œ¼ç[×)¦íÝ“CASÛ²;µ¯Ã3a;UEÐ#¾þJÎÖpBÃm3ºõ ?­ëäÂ{âÝãÁOG/Ìgúu{þú«Q×oþN uq› ó¸À_È;¸yÊP½7A-;@*˜0Ë_Š{óD×WVû‚¯‹ ïÑìÊ ž_XþÝ B½{ê„áÍn¬íSÆÚbw×°´ðD¾óe¦ñ}©¿e¤-ºG42ÏôÄ"+Qn3Ô<Åß³.bËç6ã»÷›—ëþ"‘f)ªßpƒú³õœ4¨Á1ÌMäê«^Ïi–Fæú$.Þ>8³v˜Å rAëôYxÊ$ú§êaÎw0ÞWŸÎSe6¬üAÅP|ßÙ8‚q¹ãÞˆ„8ÿù­¸t‹ Ú®úz«–cGŸQ°¶¼ó§íA³ã$+ú(@0vOÜúêö2m¾¾œ¯×_^ÿ0ÆûÝîò›€ú„¹oFr5o­¿„³ov\*Ìèk#Ô2G0§´§ìÝM o ‹ÆñÁ²ÏkC¤;“ÓÒ¶1O~^¿ÇRXg|J1Š7Ôu“³*>‹ˆ HŠf¼:|M÷Âi¡žf¨”ƒI;¢Ê–¤×$9ˆ%^í¸,òéÑ­]ÐFÆüóSÑT¨©áÜYövÕiUXÔ²ž©1èÈÄ«¢ +Ç¡xšùOùÒ·?Äê ò=ÍšüMv§¹y¼£§4§±N–¾rÀ-ZÕ5ãyKÃ’ç ³}C‰*Àáåh”dcȧ4êÌuôrò'-Æjq9ⱑ‰Àð»ª6Ÿ¸BSî=rùæÓ]@”a»$L„‚f®âò´4QÞDÉÉ‘;!¬à‰»KR)¹£yQ™­œ‡¦®¶ŠÀ¥.f“TåUI(‘ÚHÇýZÞÂ.jÖŸ¿ÖF à$Rz;=GÉL·ج0ÂþíhÚ¯>O5öd¢„wܼÆñ¿6ûÎN 6Ç’jÐÍñiâ–¢Îî+kNa»jU³[MËÄz©÷éÊ?Wo<Ú\ ¡¦Ç +Œžç2êö:0l'¤]ë5|¦·»Ã*¤ª¡R¤g”Ày] +Ãiy ‘ÒN=58ã^îñÙ÷„±%¹ÐÆtcPr<§r¶1H3=è$ô©¬ ! …¦ŠeHb†5$µxn6`çÞ„mµ+œÛK<dg*SΓH¸ñQ hÔ19IÒHtÜhQÛA8ÉÀ@ÒüyÉÈñBà ɤ׊ƒp±”e;í¸Ý¶°e€s¾»’ýðâÎy‘AÓ’U¡*¢óéåúÓî¡>tÐ*úÄF!:I×åIÄñƒø¡tŸZûÍ~ÜõUþb›÷,‚0e•§Û‰i<¦ùºrSl>ª‘eÿ¢&Z@Th»ãz£r]sTÑ[?høq¡‚i°^Å8ÕÝ•Ž–Ê*¾ÀÚù¬Õ„tú—f6€Qü²³ÒÍS „ «ê+Da:âߘ ~@&µàj‰k#6iÚYqR¹,,å‰ó%l<çÌÒ&ÚJG)îäQÅbwÉCs…“4ÕZš{-:òL§c^(Ç·ÿî$”¨<šx¯êÎ÷öëã€é7/õâU(]6nÑ^gÀ½É³ß(iNÚ|â¯HMœÃß ¿õ³>ág%R æx†]Ø®(9â»é½ŠŠ£¥ÕSz‰£†ƒOñ‹ÉH†‚—¯RÇ Àƒñœ[ø›pn¡hV¯š˜“ù,¸ÇÕ§W“>ï@¦@ÑË{ŠêÉ# O¨}š?L–ª:&›½ÙO† Ú“äUHtSs‚^©_ït*ö¤†žûM®¤ØÇKJÔm¿Á_óÇzž"f¯5“f(Àz u]R5…ÉuªH€ “@ÈOE…®˜¥x":í4 =4ÁõCš•Y= ãC­k—#h@Ï•TevŽªH€A[|À'b“UË õ(«F¶¾ ©Ü ¶’M¾½&£ˆ¸¦¢ŽN; —*Ž´>¦GžE¿(ròì|¦G%)nS’ÇAMÜ‘¼è‰Nþ¦áyœýÃ[öÍW_€]Uꇨí„Ú„•Õå ç;þF6Ñ\ÂÑ:B綃<8¥i=9a¦÷Ûn,Pœ‰„#À²™:ìJ2]ð˜àZ~^þe7Ìñ”dHz +‰Ú®‚´›ŽFêËmŸÅ¡EÉ•m1¸È@Tÿæ"èÝ`×Ì$×ÍŽÿÚnÖmjO”DJ];4EÃ7ù9Ô:¿8Î_~”ÿ…éx(«€“b)RmŠ™ù•PbõÄ%VöT¡Rˬ4zLˆPlГ|HOda‚–ku– "àH„+¢‘Ejƒb¬ nGå¢ ÐV÷EñˆMXÔW.ê“Ì&šÜϛ־zQˆ©ÈÈñ{%ÎQ’wLÆí]ì`¸rcA +8Àæ½ðîÃv·ù +•‘Ë·€L•Q…Fí*ÝN£0‹ª(&ä¯!Å?Ç_Þ~uô%¡å"Wúíýœ×Ö¹¹!à8ÄÝaûZžØ +N„ý2¡òæë‡ouk@ļþ`3!¯¢Cì£ñÃÙ³·çfÚtª’þ1E¥åfåLa@r§²ñ³~°h4Ÿfuôs·¨5#Ñî×# ØØR­÷þQHLFËø9esù­Ì2rGïNstg]l«]|óžÓ¢Ýð¯Ö±À¦´ µ'ðõ~Ep +¹H»$§GË¢šé!æƒÚ†^ãÖ˜û%®ãƒÖŸé7‡ƉP‚Zïš<€þþœY­ öõwusبÊT «Q§™^Û»³Úg²Ä†O‘x<æ½ïSqøÜ‚ç¡äášÌ#á’=__¯eüåõzïg°J‰¸«¤‰«¹J¬í®É67Íp>¢ðÇ,Êðõ´nôÀrâ?T#3qôOAÜãÜ©>6h\I‚9WØC4éÆׄø­LèÁoekÝG ŽÔ°‹ ò‰Æ±zY:3Ú‰oW©òì q£ùyÃð¥L DjžÄÈEy?Œ\lƒ Ž¢Ø☚¦]Ô^évFZÿ*r@tP"!î%å@ñ¨S‡ÂÐ[IÂ0ëü,p°º@[nÄTkœ2b¬ñ +þ=„`Ù[ÝœYW¿A”&lAO^¨\¢E?$?…+ôOˆÝÌÌj±›š”™ìì‡sãŠÏ¡S!š­`W¸Š~Ù»e=&¼MdÚÇ <¥–{Ðu +¿„‰'€âŠfÔøUx’Zr×™Âds‰`` +q+Õ +ज*¡àR-ò7& l»Ì?ä=Ùô¡<ý=À0%`ÎEê@±"~/:QpÂÖHIÞ×à@]@»™k–õ͉F“þ[ÿ`§j{¥Ž¤ÉÍ®ÞSóˆñû%“;\èLû]fh‡í“]cØL)ˆY©y~¼ªæ®ÔZÅsßjëý7jž…š‡Ò^ÅoÅüÊ^e‘OU?ÊMmiN•Ed€¦›b¸»lT–Y0U´Úv·x`«±ˆˆ!\“é¸6ë,j +Ã^É0zEÕE°ž ¨˜0íU—ªMSPõhoÚ¨®«ÆLtÚìúFu™iJãÕ ö¤x¯všKV°èÄoÙ6ªë"M ÊJ“îGÝè.lÕ”°ä®þîâ=G’oÔÏs§»°_M­Ù²8ÚDÐ]^QvMþ·t×=‘÷;6ºë¾Û L® x\_>»Â“DU\ zÐìoÁØ›&'zn—ñ8gÖ“ä¿)»q8ªÂÄ<Øà Ò² ë2ÜËÝm;æÌqã1ïL³c©Þ5ìb­ +¦‚žËêÛª—ËWzª]ƒuâyEq±Ž[`¬µ¶6¢“C{Qâprçù ÖBMÀ0 PhG˜Y†îAŽ†V½ ¬cÚ¾ƒŽà«y˜8 êÞ#Ët9ÁÐ:~àtÌîøõµ´S÷P,&5#^¬c:T}ÊN^$eÈÁxBÒÌJ•ýî±I ¦I£ Èæ´›à'”v^¬¶ Ó©Ú9´+Â2 š<•¾´àSž.Ðwó¶dI­Õþ@$ôZEJI"ŒÞy¢† ¼¸ä¸Ø0ÈvŠÒʂᇪÙì*[ÏOà™Ì Þ]=““ÝÉÌ KûŠòU×6ú”“¿YUSf£4HY@€à*(¤!Bl—®X5Ö¡¦"KŽ f_†Ö¼ +ùˆLñ`•%'DïLåéÅUYJ¡µà Ô\¼Ÿ b¿$ÃkñÀùÄ´Íç–ײqך¢h"}’68BTŠ Ó™÷Ž€‰ãO]9‘„pMÑøìúNG  +©›•–È{Gµ*ð°ÎHStˆ1¶Š2úûŠ~€ÒÈ`Ðp†ß9À” UPÈo=E§ÀÄí'„Ëô¢ÔZP@ŽÞù,G‹€zŽ¡§S嶚òÀs ³É쪚äÖÐA=14ì –sã𖌺²Õ#`KÕH%æ—Q•<†£ÝÂ&wZïÅ#ठ+)E*<ôÕ#¨4 +©f¤§—ÞyÂÎa£ï‹Úµ¶ñîîY7êðzã`Ä‘HDù«në¬êVØÆ#³Ì•fY¥¡$xdˆýez hïp"UÌ¥ÃT£G,–nVGàó—b:!/¶m›ÒV¥垉^ºïÖ”¶: Ô‰WÖý}J[E0f2Ó0³Misɨ² ™l•0–öŽ(‰ÖL¶ÚÜæ{׶Úü8c‘òZb[ˆ6ìˆ6§špß'°]Bðc·7ìmØ°e]*õ[21Í8„‹‘>XØžº$k’÷ lؾ“GM`³··ð¼l¢nÉšÁf´@rË™æ>ƒ*ÈP&$TÖ ¶Z+kÕ©¬Ýd°™¤MG@J¬Ô>ƒÍœ€û£d! ï}d°² ’´³Ð/3ØçæGHÏX2ØzW‡÷¢RÙ,‰_ˆ üM*{+éþRÙŸ³åßç²QV_{22‹£é^Ó¯Ø1_ËÌßd,©‹‚Î[rÙýãëË×´nv²½è,*Z~«Ÿ#—ͨab±Á̸f®wÍdc]{*É–ˆý“#“ 3€ÝãCŽé×Lö4¹'“ ©‹Ê†Óëçÿ/¬ùß5Aý{ÊO°8ãè›äD¢ìcùf:«-ßL +Û[Œå'f¨{¢ L}ã!–? 3Ô”8ixC,_ï¹A?cð#þ.˜¯±Ðïecé +!­ÁüDWDO]1à´å'P€ÎkˆÅ3FòÓåvê6€Ÿà}XÎ÷ ˜6»j¶óСžûÀ=gB è‚¥¾bàÞëÉ$» ÍÊ +ÜS§pUǵØÂ!pÏË¥£{ƒéŽ{ ’øU‡„]]#ø)Öæ?a BŸ‚)QwR›ý|Bø©tGÇ£{=5^Cøª"TÙ˜Éæ<°»góõ%„ŸD)&îéó*ÕÂßVd…>kE9¾˜£Cø6æO7=¦‡Übßü-³¨1,0·},?™ +*0x¡1–OР_b MrJÖ˜~Â$]W!”o† êÌ6ÂlŽ‘|{Öq”Ê'¿¶FòF(îoØcÚGòÎg¦º“ðO.1’Oý§b’Ÿ½Ónÿ÷/…ðŠ[>°ÞoàlôvÜ”sòŽˆ„¹m /íà,/æí¹]]Èu0™CàÊVÒ¶!3{ØI¯¶¹‰"BàÕi‰ÌAbÎp•@³?`‰«:¨€nЖÛdØ' ³ínpvè-¤AªsˆHÛ Ó{퀙¨NºÑØøEŠ³Ín~Ì>~<î÷ï9n/,µ].x[úS‚I]Ê+!À€ÍÚœ© +Ѭ(ô +ðù +øé_VÛ6|p +܈Õ@IÐoõ=Ñõ¸Òu±ÃW1¯3vDuŒczÑ.Vˆy¤KG ‚…9^?è«Cùc‡A8\Q{²@æ‰W§Hçüâ‘Dñ Raw«ê‘’ší5Cµ½Œ²+3HÀ‹»€o6ÕhsùTph€³rŠ‚‹æ[dê9d¾„M¨G:‘(×L0—èr6sÀYfguWÜç[0•ÇS­°”f' vhUU]”ì&ÐOÉ 4ÓVçá,U‚ËÖNƒíÈ-^aMÄ,žB{¼ô5f°©ÐpÈÝ :D.”é'NŽúÞ«°þN {œî6Ó¦¹8– á¿~¯†IN_-cûSŽÄ] LIOxÄøUBúÁeh»f9uH°46àÏÍç<¦Ì®ha#Ƈ'Kv@Èuvëð¨¸uEÍÇá°¬´¦]ä1rá™ …Zî<4”Ø°«œée!‡z×%8b9Ÿã—©Uñ ž7Íj™,â$Ï8²Ô6£\íVÛÞ›8´©Þí©NÎ÷s‹åÈ„]êªÔº1ªœRwM8LÁŒˆl"ÐæÈ…>a㵕Ri—¥˜Lõbˆ÷7‰ÃOW•V‰%tÜciws @¼y¡˜îÆ»åvLêã“·¿/ðRW(|«-"ŒGÁø}9öWõOdöŒ€:s?ž‘Mû‡Ôñÿïâɼƒæ?v >qÌPDFþHvª¦Y´¨b-žˆ)µ û2×»ë &àz­e¨ÂÄv¸ü¹v²[œ·æ¼<ñ)"$g pu€Âåaªž5ÉlF’ ’î5Ól d©9 ù|¢~Ð.š]«¬L5ð¡‹1B]8?öü¿žp]1Ë=X›É;Ž—„5² òtàaƵ3›1£íA<‚±+pº.Ëáô7¹I! ¡tÕánˆ„[öd´acÁô°WÜfM !dºÔ)òth21 +‚›šXrkÜ„’˜þBï鎑oö3æš½Ç ëpL@Þƒ#â¨>9~ÈFÍÞ˜ƒŒ†R´¶K*Üþª'¸[W,Îa¥ñ¥÷›8HB €ûëVÄ?aDÐApTWõ’Y\ØP Í›`¿‹ýf„äÅ$ +{&~Z"úx4™¼Ç%:ŠÊś̎«Ùž&yÍÊÌ"g…"® w^OÐ#܈)ŽhŽém H²AÎfUåånn1zhÔjl‚QYO,6 ¹a¹ˆ­9(«é}›&mŸ&FDð8S´QLz‰ã–q*ñÓÍ%bË€EA…LÏœyûiâ{öKô )% +ÐóÔL˜{ µà¸K¼üà€K„]$ ø\gÉo:Èò©ðCmÞµ…‡ ¤léo¿´¨¼°¨æa'YhCtÒh9Ç(o:KfV½Ç(ÀÕ@“Îaó!Xû÷)T‰ñàõHzÙQ9(ÑñM\gÅ&É¥ÍÛ`lœò¨³gÖ‰ÉâaLôkdä3÷Í×›P§M¦¥ióËá‰íÀÃSaÞÔa&†•¢!ÓoRE‚d»â¡=ì\úŠ4&¹÷ž7»oƒ¤ˆ —ž–G«ô™jÚÂ:"§ RäÂAm +€ï ›l{!Ô®ÛôKHƒÍÅ"™Š"…,Í ¿x7a?¶UÝ^ì§f!éqÈÈ⣶d•,´òÕô`¢šÚgó:Nµ¶¯Ná!ªÓˆ›žW‰BÒ ËˆÆ¡›%ØÈZ³SÜž]5 LP%pÈEaOLºÙ)—Ù+ M““—À£D‹®ÊÇ΄`ÖnA‰Ñ~¶¨/ƶœ¹l€ï• ß6íNx[XA‡f¸4(ƒˆä^ÐÃÀR©DÝé;Ýêó,xUªíàÞ÷Æ.È׬çRyÎm‹lbÚ=Aõ¾žˆ& +Õ‚´Ò˜]„:°Z:Ä|Ô4j_ÅÖ“¦±I+´³µºx­¼ÞÅh½ e]1ÁF²ÿŽ†QCûh«Âg¯VcÉNKĆÑè,Ã鶳t6`X‚ÝJ.)h'LnÁbþúa«#Zت0V¢O£‚okìc…ÿZã4ÿ!±0hœ†é^Dç… aûÆÔ÷žc†¥ï]î”Ã!<9é«ë¡Ä )‘Óõ‹û" ÂÚ•9¾ø÷Cµ€ê+“ÊsêÆyB†)# +ÿüð»âvn[xjz}”J 8ˆmÞ°w‡ìÂ?‚arïýÎ +Š ·-éK÷•’Á*½Ö +•_òÄÆ•O—œÎ%…2(»…ó+¸õü€ÂŒ²•¤ðÌ&* ÜGÚRÁ}l´/‡÷̽>&¢ÿŒGÆrª°ÙDÔñQ8£ž‘%j¼ ‹Ø@ýÔ“WG,,úàuìAµîÃ3|ÏA\@hDw.Kí2!·ÖUUwÜ!„ÄF©NËÆBBabFÙÁBSU)1B­êy0r§ð¯Æ(·E§Ev|÷Ñr £¦Pˆ¦OÊSã +±yÔy!Ò¼‹ì—#lc#lÌð~Õ†qýÕéK’tþûM8 VÓm®"¾fÍy°‚—:¨šj=¤M°o¼Ïá:³Í¾¨ ƒl_–ŠÞdqThÊ%Ë…ªÃM&HÀì’  ¢3J¾vÔ×*O¥Ûž¢fº +i¸²¡š4æ5“FâÄ/ªÁ8c*"5cLÇ1~†*I Í:[äòTêO™V˜‰C„[¨ß©ê¨§ñ +ڌƽ´óÅÝ “ÔõY‘uuK„ið&ÂHŽ’€t± œ‹¸ÙÊÔ“Ò0·BÛ°B«‹JÜ£J²WmïÍ‹sSÙÅ.uµ…ÌÍc„Þ"^¦?T+W@åºá,®*  §ìp¢…Iµ¿ìMËÄý€ýµÆKí¯WžH;ͪ«Ö×lo²óYa;;@ø?ðN±Š” +‹—; o^JK¬š”LMnÐyæÑk!BÏãK¾×PÔd5uÙ:s~ƒþ*ç×Âq¨oÑãÌñ¿&K[=ÝxïÌœ*è{§AE‡Ô–sÏäh`[Ýíµ ïù‚© +ù$¦j¡E,˜@ø(’,¤:\¾ZP¸ª—ü|c—rY-1SäfQ°ùË[­9›’ßÝC¾ëÆ*Él¹^lJúÕÔ¸öéõ`ïÍRz(íœRuØT»Z·”#yËG¥8eš·«•|:ød4°9«i®îÂDþÖdJŸ0«…ßïñÊÕ'÷ÄñÀðÒ0ø€vÀ×brë¼+x(×á2Á¹—*i;}åß%8I"³ApRÝÓÖ>– Cm…0Ý{„añÓ +~ã¥J.8®zô÷ÂîY.~ãæ=«‡ +߃"=B“h»'¨ëæ½+Àú“Ú‰<Ý/ÑŸŽO¬n9ù Bðtç‘ +î=-Š—d¯éªøBd¨”‚ :EŠP7KµCr²0  ÕHaN-D3/!Æ¡v¤îÐó¤,bˆ„‹[D6¡ŸK·ù.ÔB¥Ž ¹`4T½„p O¨]ÃìfŒÐö‘¨ñ ní=¢.Îci‚ q$…wq't„ÌÓ,ÂÚÃVE-WI0z<î<ïÂ_Œjˆ;© #FÑø!ÝkA3uO"‡`œí’§õaÀÛ­=a“×äãªÃe·ÕìŠÇg}Jä)),>*Ôä¬áHt†$oñè¾bXÓ!Q/‡D½F>#„GÅÆRþv:‘ÒK±"­T¯ÊWƲFjy‚fòé*½ ô–b"œâ;7å!X\F“¥„x91è N¯Ú£€ÿy:eKðš© +Ô2Ô׸øLä^¾Ÿ#ÅÔÜ(„~B´KžkæZ´R¶æ½Jïùªy$²B6I½æ´Ú·öä’¤ž1Ðwy^C£#BÌÜW³â“2M1¡¦+h§“€~& Å’áÐvüÄtV;F×KÈ©h|UÝW^+2S2úåA¨9ó7&6‘†~ ˜ãw™Zz+í×RzÍaPÒ¥Tè⛡5ç„H^.¤«f ¦y», è‡´[§¦€¹Ûöš>C+ùy!õ"\’%§±K=Yfõ´˜Éc)ZÓÁ„?d(î5#ÈÑú;ÕOºfÙ™[G‹.ݲÉN"F¼C>SÎ;­÷؉î©D%"RfÍ‹‚Ëtue¯9¬ió„ÝÂSåÖ¥«Bz›_ ¹Þ§Åldˆ‹ :Ͷå³íoä•Ÿ‡G:¾~þa;ºùÇ0¹5OhÍ·ï×74Á…}©†¸ÅkÅÀþ¨¬•áÄ­Å ñЮEûÿSÄK´Öc<÷/Tsl¯o¨ +Y/ÿZXÄG(Py+tB}KU¡F&ˆ»µÒf+5CÁκ¡Ö'ˆéP3´÷kéѪ4BíRÐ;³ôi«¶BåÔTz¡è*èËP¼µÕ»¡,èïPHl€PöÖ„ålÁ %qÁŠ ¥u[khVæc*õƒ,Ôn »µ´p…¡(1Ú“kqc°K¿ÙIó6ÔX3ùקê2ƒ‘*:ƒ¡*Cwö~(0 nC(R®ÇZãºuaB­lp…B™mp§bïÎ-‹O­Þ](b¨;Þ:š¡~yuXc ôçŸØ9Ï›§'|–sÿ}ïÝÿµ†<ºÿ¡ü<„"Bû6¤Êá×ÐH,¨Q•X¿ Ê„jþÛ 3,ú vQ¥Ø—€b í #zš!¶Á¯ØT±Ñbcƈ‹ oÂx±?dÆ“5’ZT¶É¥Ñ%Ä5C«Lˆ†–›·¡Õб"³¡ëgÆrg¯Ð.;Ö8rìVZcѱëiÓŽ=Skl<´]…°zhßÚ†çCXˆîÇN²5C;Òv™†øž5c›ãÖ¬Gl²ÛeOb³Þš…‰ k&'6î2BuÐÞ¬R=¶d–4kÂ]0G·˜’¦½®ÎÚæ¸8Ñ‚~7[ß­ú5U.t¶Ç„(E1ÝVIr KwÉ:8]EKfbÃ쵓~¬=¹D·;Ï;&±Ë±g׬ã4ѲߌŒŒ‹ÉËÚÁ +Ãj5ãG8êkTÎI²£ ”Vb.µ*€fGæ„šlÏšm©«<¢t»¬n|ËšŽ#YÌqFk¢ú»íʬ ïg]×t¹úb¶¬i÷¸µkæþ9»Ä·¯›ø×_ê~úábA*”´ƒ ¦Yåf>;jE|WÅzΤ°æˆt•Ã/©¿råëõ"ô#ùØïFìnÿ-U7àP,h§å÷âïGýÝ]½¨‡¨3³Ð„FÛ€ +™ñß'Ä~¥—ûuüHš„ƒŸLV”'ô(~ŸÃv\)½øíuÝÂ>·ü߽߭ƒãe4ŽD‘Äæ²áؘ%÷e_×3’U‡ ŠD—ý¿ž2ü¸¤ìùyBƒ¾?¤þj4Q50õëá=.+ L¦¯ z°R*^x†²]•PxËruã@ÖÛ'´"버·Ä…]¤Øg÷g¹|ë ¥‰Añ´RÊå Éñ©^Ä$A ®ŠižV$ø$1+ˆ9ÖÜ_ÿ@ˆ¾Ðj)ï®ßæ[Ë¡ÜüàúÄ:ðïvbƒÞŠ&ð‰æŒã‡½»B‰1Þ[O¨×ú:³!ˆjÐtQL=?Í`ïîoüz\áuŸÝƒy 4Ñ•‰~Â*²Ós¦B‰ÐŒ2{!î¿â”& i ;T;䀛¹TE`|ã2ÿÔHCMS3=j®¯â¶ƒ móz<% gPˆÀN%ä¦˜Þ +ÊS×­P«y ”EÛÉTß›´¢q&ÒvJuVF&½N'_;µÅ¬T€Eõ„„hÑ'u@ô‚Ø…PÅÏåCVÈÏO±ç:[3m ´™i{e ^bY¼Ô—ŠzóºB˜¡¡Ãõ¸ ˜tæÜ‚•&¤l–XåÆ.šÁ¨äÁ¶”ä—ƒ|ߎWª¶÷ª¼·V¼‰èVy~(¨„'MS›¤(>ú,A6 ^°òðÌ€Çd’Çü¹yʲôzz±™-C{Õ,LIÜ0ì¹Gµ(­ +d™;|ó[UQYQl«Àpý½Nœ±F“ôõÆsX΢¨lÖéÓ¦ÛEb¶¾.ŒLš±ö©°ÞÍYzH6÷YÖ}“ÛÈ@=Úfëû„ê»@ÿèµÛKÏÿæÐmb8W‘Unn8Ì-;¸z‚QH(å»KA¦)h¶;€?›ËÕº¹óhpŸ{&»»Õ|·KE7nodƒè©ai†é(^rÙvŒ¬;êÓYò›ß›$4Â"®î×f>˜>Räæê”KŠËùC;Dý\}¡ë’’ä¼(û#T§_Z·fçhÅ-.¤Æ8ÒY/ñ”Àñli8«÷æ´EÞ’u 5OÛû‰#6’Éï|I^ejÃÜÇ{sqróDg¥¶yd~â½S’’LMÉõï®ý’ÂWæîuJÝ¢  ø ;…ôh™¦A”dg袱†Rß(„D¾ªz½äµpS†‰•¨.†ÑTÎï%_øzžë‚ ÞNdŠð°A臵 Êc»'« +[»j±y(V¸=RA“†£´ñzºƒRß^’`„ËìŠpa×™nï}0rVùñY;é3¶bLØ‘Ç‹ÿë'óÕõ*0I_† +Ï, ÝÖ×,Âæ_^?Ÿ³•|(ði± ð¯¹÷'±2Á¾Áçü÷ðÜÿ«é|·Ð;Äθпo~‹Ð öBg€Øb‡Ð¹Bì€Î€±:WŒý >g@Ø_ñ9Àþ[|΀«?ñ9?¿˜âéoñ9ž~Àç\ñôö”»`ü<ÑXϺ¡Ü¥œÅ&}§gi5Rî‚|âIl°ãÚSî&1=%¤})wÌ•Üä´x˜³‘PUt‰®‘ŠÞoõÂf¯›*ƒ'³ÙÖ^õH“bC<’'Wì}XU­6^ˆ¡òÃŽ•{zRšì°Ù2D¶e`¤Á¨9ˆ|³Íî&âxÞ¹€q¦—ùQ³â¨z&™®+´‘ª9n/nõ¬4F4îbÄcTÑ Ûè úèw¯Ï„cÒFP¨úï ¨¾”E¶YiƒSCé9òí‡]÷ Æ‘Tº; ÎÃý&ߧa+òI"|èíKAq”ãö»}lÄú³àã ËIPì‡/íq»Ôï*B]Vä7iø“y]?Š›×?Zk÷Ë&5H-öÑ6ÛˆK„"„—Tl½©ÂËxp¢ÌÜ;<ÙBÖµ9™˜7¤ûͼ¡ì{s¨ùŠÚ¨Mî•Ê`7¯1 ÞÄä­­CÁÞc‘Ë%PÙÍŒ¼½|Úiï׃Ü>a/³µ—¸¦õ¢‚šOŒ°7ôZ5ÆH“Ŭx[K}@Ñ8Í^1´žŽ +bŠÞcZ„–ÕpÊð8Ôák&Ø]Úë);¼`‚Fj +&„À°n{ÁžS{[œ§í‰Î•y³×{¾îføýpǷâ",C2a)ƒ´ÚnIzakƒä Ç#Jà7§kÊíp$£È_õç4ÇL‘Q_q?: >;Ðbó^×…p÷´¬]¹ŒuÖ!ŽF|>…dAºÝÔhw p™e5g6Vk—€ rB…uòª}©ê6ÌeÄD`§‰ÈÌ‚ÆE!7A…s‰2N¶‰AOôl¸EP˨ä"ú‰ÛrºyØ©ˆQ¯¨jIõP ¹lôÍRvInžµ£êºXZÜì?Ñ­-ÚêªnQ‘AÜfRh-=ò˜IŒv^zÍ J¹Yâ—ù3ÿ[³½xYðMĈY†äv~ìø#°—â]Ò. +Þ­½þ; ƒâÛ¨íÂ#ÖE]Ô±µ&ÿ [{²¤2%ã tÐê »:Ñ¢lðX²s” ‚`¡½éÔ˜ (*ŠÎj§ò+3d/…–±H…SÝPVÅ“Í·N¨§÷ÍÑ}nÂÛ0£¨ø4I©Æ/0fÊèÈ’Äú Å¡¯M0±J’’µI„¥ÔO™J˜D*üy €³7Ôɬ1yfz3 YîÆ»V22“ŽÇ p*À¾§½\ÅÐÙaéÿù£ð]BR( ª”.€I?D.Œ¡Ü‘xÞûVu½¦3Aq1µLá Þi Åðkð-×Á1“”Æî­Rô§GEé©HDæÉG¥sDs6ñ=eOR&ƒ8ÞŸ½™Ü!L¢BkoÖª«}ªÚ•L"N^ÆlzF ²…1#°  ›û¤žf²AÕ«ËW ; +¼Œ+Õ…”Û’øBŒ.´.£ðrZ3˜ª@|m©ÉQ›v$ÏáÃ/ØÊÐÍ›š-ú$B³f%‚.ú ª>°q +™©.È}z€Ü¯*|f©éôL*Ñrš{«“Ù‘IBÔ£$îrاL¹¼!ŽßƒIl™Y×í;ß2µÇ£7¤UYØP{Øq(ÞMqk®2øFÐie€ã§,Ø<3°ËAlÕìì$0‹s_˜GIY—AómëªÚ,1d~ºÅe·ì]QØРθ…‰øh À&ÁÀPÜ}!s‰û’f;[0#ˆrëýX>—ìÁÕšMh~„Ç™M/ƒNKESÎõµ UOÕ)®ÉúÜt›Jx,á š`vÌ到,§éÚ£>7ãNºíŠc·uä%‘ +UÏLb ŠEtFôò‘œ–¯¬Tñ-%d—DÆAq»-+^qL –­™3œ³CAßU˜e”Oø¢ªQÁì&K|BE" áq‡¨ñÈeóhu„KV¥Ò>5ò67B¤9á sÿL@!Búqöñ;á¡î`ž`/_NH‡Ð¢×²JJ­ôsXÚ”‰“ÿm´ÝZ^ςΓ4­Áéd³Í=±KLuóˆ* „vì}øó ªqC©¶:U¿+>Â~Ž÷eB¯{ï(ãTYáq:ÀVÀ %çs¢üÜ™SqŠÉ„* •&AÐÆ-„ˆž Að¤LãƒlRµŸ'΀e4å ååWÜnøæÜR -N4  æ*òtnG¸£2H ÐÒÛ]?íwL/v¿#~ú3U…÷Þ¸©à|á÷ù>·xiŸOiL¶uVH¢49„uhòLs& JŒ¶´£¨šó¢ÔU£z\x4·©d*0‹3™¤Nšw¸?6wpdl4É+UŠä–˜‘P—-G‹î(ñʶàÕêßdª€o(‰¥Ž#ŸÞjÈöJÎ/&Ÿ¿x‡ ¬cÀôر¤©)s\^Õ?iìjBæÀ…»± +ù%D»(˜ÍqFW<Çît'r¨$»ïQéE›Gþ¾R<¨ú¨Y1Åî-=BŸOÞ½¯¦|µ¨}@ÔP¤Æ\žƒ|A¦=ƒ  +“¿ªš¾ +ÉW $›Y‚Ï|åüÑkïx÷Ù¹Oì}÷5ª›WdØ +Áš“ŽîD¿eßà”lsØÃ[urÉ2reÀ6q,Swä2×ÔcC{s°€/‚_·‹Ò(Ï +B¸ÔW ìGÑNÞÝ/Dr¤—†J…~á6S‘™ŽJÇ°:"ç.0 l&®— +ÈŸŽH%|ÜUO>¬¦*¼·}Dúv›ŠrY†?M:S¹XSI›Ñ@ñ9EZ©y5sx/a"6àz GTPr†œ-i¿ÃöâÃJþ¼¡Ë¨‡GzÂÑuRuŠ½Ôª¿‚8°prí:¾&ÅñÇ}Éêà:ê”y\Ž£ RÒÍ*x·» XÌ¡`ãE0¥Z:ÆՇѻwR@²Š&Cüƒ—µKpÊßviÕÞûá23íånO€§%î +U˜cO7ñ‚[<ðÔš£¹`öv‡K~/êà'« g n>àôÑ ƒR´ª)+`tŸ#ù:ä)”+ðª§shRÑqôº¤™˜ °-¶÷P o[ŽgØÅeXÑÇ$ZÓ\ÃWõ.U¶©¾fá©u¾ìžšajµîbiÏÀíéÍë·Û™½Õ:OA7S ¢8x$2˜]O¬c Olçž +kƒùJwç·%˜×õ­‡ŠyÌ:¬€H~´OCXuºiEƒ±î7hnË„#Ôó¨Þ¸´ÝuÄÈöœAu«÷ÓŽ|ôÍy5—ÊŸ ’\ÓæÌ“¨Å”7ï¯à„m¯7’\D—»¶»‚µz-ÊrwÍJò•¬‚þHé]6$<}ÝL³|Ö :NÁ +ßß½¤,»‚¡Bn•„ðJ¶;ÊP¥½HÁ—XÏ(Áú›˜üY+H·ðO`/7ÃäÊíþ@w0s!¶j°/Eų*­ >†Ü÷ªÒXu@¯šL/ñø,:–´•7ZmÕg‰ú]ñÊA9ñÕöæ±Ò{à:¿³Fˆ}OÀçw† nfëiouAlmÌ(õ]w´ÀlÝÎzíí·{ܯÆ_šN±Y1v(ëÆ€4X ª&rÈÖGCT» º§žJƒ³ ´à Ò®l¢Cd‹Y ˆ1 ¸ÒÕªègÝäš×¹5æ"%S +óM&Ø\»övPá.§`\ÝŠ<ꔄ®ÞëÞ;IEɧǥÁ‘G>Ó­j¬à …'¶ÎTx*øbŒ/fæÆ¨Ø Ôè³­7˜½ë2¸’¶2ÀÎ8ûŸy`ãÒ†‡‚g ó'‰3Ñ|»sÒÁf¦¨Ì–ç¨æÖIÏEâ#‘7ï)ƒ#ùdz €g: Pb¼!×O”)Ýã@yêPr›ØÍé:t…@êýA ›$bMA\›PLöî,¶¾8”ÅÍ7Z*Q4ˆbP(Qæ‰)Cȯ§[£‡ 3D£LfeÇöÞ³®Á›°FÀÌ6)bEYƒgéd}”ˆ ,·18÷vë¹»9nÄâÖXÝwÎûPáYÆ ÖHc#ÄJ+ÁˆM’5‡|rÑ„=Þ­ó%jFuï1DRA“û»ø«³KÕ¤KЪ¿ d ÷JÇŸû1¦N–°„˜AºH× IÓ·ØxlÇ>ÝŸè÷©þô+Æ˯&—sk¾âáyÔßÈ{ϧ®à™€£}´æ Ç>g…ÐÛìšL(-'…8”~®Y  ã“óÈÑzR½‹J`]«mÔ| +_í/X]1W³ªó@&WߎbõFÛËMÐlǦy1@‹@{ÛtA|å +â”NèO¢|® +d n–4^Œ,[™‹M.í$[ƒa@(³öèS_6 èDÓ|oímæ9Ÿ/8¯ì±­PÚB“¶ Êáéi€u/ÐĪíXhË„a½ªWWå ´}`¢æ‚Œé}˜iRâŒç¯Áýg¢ÎL òòüDŒàP>BiÊ3.æŽø¥˜“°û-àH®)ž©_£' < “Ö„ÂærÝóA ÖÇú +©þ„™u½î +CÂ&:è\ͯc'Éæ¢Ï(Ïà_Ž­§©¸Ó“¶¦yNÔmŽô¿/%ï¿÷¡‡pM€]hˆž³xUG4]oÛå«ymÁ¡‚Wãs.XÅd.þ¼9p $`í>tÎûª¹Ú-(BÈãmó–DNÙå$ð <€×Ù=î=Hõh$Íïïn>¦„ˆoˆÛæeJᨋÃΤíÆ#‹D×’Y-wõ[ÀŸã»Mñi‚°§üÅu2ÑvYýègµÜ !A*vØ ÀÀÇ›’ Ê.ÕöÚêO1ÍX ¼&6Ö4ïþî´çfh Ìp„>mj(>˜çÐUσE òKØ©eøìwSѽϱ­¡äz”˜ É+­À”K ¿ª>ñ¶àOÙ€“>U$ûcÒ‚ƒ˜Dš5¬nµg ¯¡a§‚æ›Æ+œã„£04:MFI®«­ë4*fÀ ƒM¡ ^çi<è(ž4uœ®i‚Ý¯Ä ðJÐ’ÍÀÍñëèAÎ4-ZAÜ~ý¨PúËjHqF.xaÌ Ñx°Ç˜!ý'Ù»Sôή«ò›8þÂw:›Û؇¦]è˜Ò4-ïäp-‡IÔiï­†©Ö9Á<Ð:šÿ%· h$zóѧªñXäðÄwò|gloÞ³ìl-Q—ÈSh‹O‘¸©9êýÃßÉZ®ªƒÇs÷ÿ°¸-ö¨NG‹ ;,”àþ vð]/õ 9:ZU.ö”³,nž H“óÂÙ¿÷®˜j€ï't†ÿt€Ô¨l ÑàA"Uh¬ºÄ@1Ìåà‰"‰ª]TˆàWo–'LžÀ¤˜ž0<õmä0ƒ4ûû§û,;]ÓÏR © ËË꤃Ý9…ð"—”Ÿ}}çꃮQ è:€¯Åhà\b å®ÖÐø"Ä%…cÖÂËZ˜ìû®¼½Ì@¤d5`?¶Ñúèft„?Jp_ÑÌ3ž"ªÒ¸A“6£0‚yU†Dl@+9õ[$0ãfŒÇNšAà÷š!>WêÐ>/ƒm“ªU€>Q5ÝË >àA4@D°ç>« +q¦ •? „žæ;o¶úeÆŪýC'ÑÎÙžhx#ÔP`¶ƒéZdÄà9qÝc6Pš‘È7Ñ;ÀN¨è›¼áÚ‡  Öê~B/ï"‡•%0gÆÙi§±o%à$F„’Ÿ1ÍÒ:w3ÀÚ|)iÇ£P=ã úeJDÙ%’´kU?W³~Îf3‚K0VØ逦Ûá Âô2ã¸À`5C&æ°£BX¶·,ücô β° +ÖD+¬Ü~TÝ佸>Ðð!Š-Æ +0|;»X‚á ýû‘ȉ²Õ—P• »¶è±âò¤˜ZàÄUØ/‡˜]¢ú”Ò¬sÓ)¢¹çŒy0®æY³U}š‰]T^5æ?€L©²)ž-÷æ À0±AèÃÝO­³ Ù`` +ç +þКõ©” +8Òx>g†dÉÅ÷¬¨8–ðÄvNá©°6kV-®ïš{·;Kn/lí’|ÎÄšRÜ©55OæšÞŒ§{M“îoÉšn—mMØÆ »&~÷÷}ÉG±±¦ £ÄY2Ù[¹µæÃWéRêAn†ÔüVê†Ú¡:`ŠùYSðV%¬…Q¡Œ† ŠB ÄV“…RŠ C9FС¡ªc«gQHÐÛ¡ždÕø¡,ek.„ò–`l„™`§„R›7ÆM(Ô Q(ö ÆÔZ3´5ÅBéQ0äBùR0CT0%¿Ù•SE“t-Ë +¦í¯GÍ»ò®h ¯%bÓ¤õe[C<Ô©3>ÔºEW`-™Û»kÅ]ðLF­^te涞Ðücp¤Ö’Â茭•‰{§n-pŒÎáZ#ùOlýÓøÔêç.UŸÑU^‹G÷.÷Z„ß³²F÷?Äî¢k]mŒA¬µ¹1Ž±Öøîã!k­pŒ«„rãšY«–·žQó¼Æ‡Bµt 1bëm`*Ôj‡¸ÖZîb±j|O Eç!. +×׈^¨ßÆC}'†ü€\ øwA˵ `Ä9CûÀ ÍÛ¨jèaAÙÙþ¸¡{b]!†ú7fÔ9´lCÕ¡{$DºCJ’‡>–mŒ=¾gægßLˆà‡¶›m tï„üAìZ2±‘h—·ÈÉ)„=ìʲ¬ùMSš)r”×̉L0m'iWì†MÚ% šrI¸v9×kÒ†fzAtVéXC0V` 1ã¦osE"À¢Û»¨‰¸ÄL“,O°÷l˽ªMRaq¹kãÀ^3\Ó0TökM‹È‹pÀsh˜2ÌÍ[¡Õ*|ÇÌ[Và뎹:ªž]r¸rj»Tßóõ55yÍ*>ã]s‘ß½åšÁŒk´&@ÿâ«Qtýn­×üiܯ5ó·}—¸ §gMû†Æá¿M3¯·`MMÇ{´æ´?ßØÇp¢ÌÈ5Q73°%›‚–ù A +¯„ZÒì›÷Œµ‹Cø`Å¿{·10´Pµ»½A`W!é,ÛßåÌoN ˆs*\Ú8]¯ßò[ö¼~\Çg,oîî3ðñÙë ÑðẬçz9 +ý`úaûÇ›!m6fE´íM ¥—s÷Ä­¯nøæë¾ù¯?8þyŒî»7·äàD~$›/•éÈYpUmvÉæò©n@ÁêÕÀn W ¼%¬\Çk;O0¡Šž7Êÿ‡pñÉõks†ðö ?5Í⪄CJ.TT&¡,â1˜ÖƒlOú ´™kú´‡HnXLÔýËLäf.P¬Až1n_¿eN*Ý«Øv6›(’`4;,TzHµô¨Ößœ á]R £O ì ±@),,Ç­ƒ8B§œò`úá‡IRûA3¡ítè(êN™wæ_€ý §÷Ô£µ(:‰¶‡j1±‡Sš-'˜ä©+@lÐ +$a¢@Û L”J±45It–J)›Ã›@éÀ®tA”¤ ¼Q}ñ”9òx`ƒmOÜãÚè_å¦]οØ4TÏ[…“ÍNŒ×ì+Bljîžp¤ÃÝšå-Éc’ÝL>•ÖÄq$±ÔÛ {ðƒùPDWÝ’ª`|1Êåa'óÞ²ª7Ã2Êqe!€DBöowXõ&¶qê ›Yº³i™Cäœëy€~‹œ NÖ½?Ta¼9‘…\o·¯Ú"¦qfߟêŒåÍðìê×òÁå(ΘâpK(ˆy£Š©AlÕ`‡Ü¡å.®“ÛÞhXЈ²“wÈ3ó3U3a÷¦©ê?¹{$Ÿk!ˆŸ˜… +ŸyÏ:˜à]9ÝÇ a!h¬÷Ý×k]™ Ô.ÛËi¿°¢$õÎÝ~ì<¸¯‚'j0v;yÎ騥¡ÚC,á-œ%%Õp‰Ô9úeŠ$fh„!lAÄ*éž”ýf“p1pTHæá¨<é¤xÁ”·:¶…7§¾„› ’/¥áá^÷6Ì 3è(ô{½ÈjÄUÁŸÚ¯^‚€öWUÝvyMm’¼&‹%ʤø1˜‚¸‹ïYEfJ¼Û¹=dJú°‚«¢ØïDP8놎DP}Û“Th8ŸA ¯{êðíµšÞª`3„ ¦¸½ØÁ† òáóvÐGfꟉeðÞ± Þ\r"&I§˜m¿—5háL_üËàïË Ñîâd—ZÉ +Yz:r¢\ÜC3äb°\àáTs 1rõD +ÐØ¡j<Ø}52Äê ñ&ž 8;'ÏPü~rRðÑ̇@jQò«ÅKÀAi¢ûÌ…p J'ˆ¦š`H"Ϫ#q`{qxŒ"HC»°Ãäýy˜&šÚx’ÃÀf`ýh >Š>ÛzŠbh lB&ìÑÎÔŸë=}¤ +#¦ùHuP$tqx|f='Ï Ñqdñá´%ý݈‰8›Ù#µîY ¥ ™+m—Š˜:Šhj³³³`¶ • Ä° ¹ggóàCã©\)‹™…ŠÎÓ•ûk@åÀâÏÅHþ4í +á£MàœÜö˜Wš¾Éè—ÊX³púP[þ¼õ7ã¤%ÊV ÌÑ˪CRÎà8Y¥3‹§3Ùiš­´¦Ùšp!hžŽ®ˆ…!ewL}¡JãŽìˆð ÕƒÖF·*-mÔïÓ`žªµ«¾éÎÁ­yÜ^ë^o_fÚš¥þ³ø"PVNŽ•ª°¿Iúl/È´óçÛ™«¨qºÞIÓ!Ѥ¦—Ï]‚oÞ\RF{$çXPNÄ|eht]L¸œ ‡pvRÜ7Ý·BÕÛ¶+„â0ó@ă½©Q˜e 8/]úÊ$3(¨ô82ƒÔòTÉ$5ù{cø!2ŠLqbRq¢O[BFiü kÂéyÑ®Pï™N‘b¤c¼"5eJ½¨;1A…7…mHBæ™Ùâà»*ƒ¡:„º#`µ®‰br2ž»Eþ–ê NŽ;`‡íI1ª¢£tš•J³CIŽZj·j°¤ËVJ³óúà•Ú$Ò½|>ÛĘÉK¦f®KU£¤N+Ñ$-Ú'‡QKNÄ@Ç®èÜoßÙç·¶Á U`ô†[šb;q„(esJˆÔKLeTxõÇÂÍíà¸j½¤n !¸¥†Rl„ê.«ÝÕA“(Îýdw +TU³­QT‘h©@BI)’ÕØŠë>;¶ÊØ؉Q•E}X¬4ç#!'r¬ýPV+&.…Z¦,rrÈZ_Ø›X?UÎõ1¥Šh–üÝÈ TQ¨HÛY‡Ú÷¦Å²‹êÃ3zŸ¿Oß|õ/@ˆ!Gõ¤¼/\›ý2yì¨ p¯Âyìni=³B +Ä>¥ÂlfÛ¾Ú »Ün»²z“Ð^Fs8’ö"¯œèGÕÉo!JYüL×l€„©•å„Œ/¿c„ üç<Ùmx¶!TK|ÙÌ(©£î¤]šè  —×äÀÕ'à033<)<5-ƒ^·³o¿¥ÌiÖ!néßØ¿˜韩ö<Á³½Ê dØÅ×ñ’¼g]Ÿ:3žéAÆ[ˆGÍÑœéú5Ï•F–VûÛé…¦)Zª'€lQHä À÷Y¶¬â48êPWŠÙÉ7Oj[±óêØñ°A‰‡Œm*»Ø…ªÂƒ’òÄ^#ìÉÁú»µÇ +…^qà +ÁHy÷?x‚ž»ÒUMÁUô]mëTwŠŒÇFøŽÅ'ûmWúœåc$ªHØS$6œî .šmtÇ,¬K%Ãü߬$w+«Û27úàŸ¯—±ÞÄxð8žß£f”²ÂâœÊ ·ŒðWBýÚÌš +%…êRWÓxX/ø¯‹ÇÇ*åÅûu'Ó­ÌEË¥—?üÉ/~ñ_MåþñŸüå¯~õý/þÓ?þ»Ÿþé÷ûý_þêû¿Bý¾S·¯ô¼¯¾üá›Ò}óºÿðW?ûÕ/~©Wþêg<úÓ?ûÇ¿ýÞßÚ· 6Ïÿ?ûá§òý/¿3þÓÿõû¡ô?|;ÿõ—ÿ™ ßüâoÿñ§ßþâ»7ßã=ø;ô÷¿úÅþÛ¿üîg?ÿëŸþùoýÿø#zVñÍÃÿó/ñc¾ÿO~ùý¯öý?Øðÿëß}î•ö‹¿ýéÏ~þÓÿé—¿øùØ`ÃRË„:K>úŸýÌ–èÿøÙ_ýêoÆ™ðÈ„é7ñ?~ÿ³¿þ›_}nxß~ÿ_~µŒïã‡ÙeÛ²Ÿ}ÿw¯öÙEúÓ_üÃßmOåøêø£?.?ý?ÿ«ñMþ¹ñÏ?ýO¿øùŸüòg?ÿ•mÚ×_ûÇ?ùþ¯m˜oþðÕú[þrù_~òË¿ÿ»¿™ïyÌÊþ_/dÿþ?ÿá«¿·M{Ñ>øGû‡ÿÅþÇÿkýÃK}ùß^þ¯ÿûxù+žýÓ¯¾¦¾î“ŠÄù¯ì3ñÖûüìÛ÷Ÿð‚Ø1üöÍwwŸïþ\ƒùß¿è1Š‰{Rƒ ô¿ZS€@<ȆóIA]Ÿ˜&—eœRöLÃÛyº;>½ -cž¾(aòÆ‚ÕïìWÌWŸ‚ LºÊòŸ™ÛãŸÖbÞŸ>Ì#KN ¦ÎçüÕ.ˆ¨ôíîñ,Ý ã¦ýý­f‘ß/á׿ùê¿èë:'}~R žÇ°ŠW#éñ›ŠWßüq5ñ¦¤åáÛýK¾oBã6O{#l›ücÑ4û‡´ù‡¶ùÅð*Çx3¹¸1iqض10%æðLÝ>jó<ëø¸A>>¾QKþ¡™?ãÍ0¤½lÇ;—‰ŽÍ{,~'»óÃø8—2>¾®1‘L²gGÚ9><²˜âäRÇD (÷ùÁLŽo=>¼æ‡¦ç‡g/PÓŠšrÒŠ=>$1>ìóûË ^§öþÅ?Œ3Sl¾ø˜{­ê<Ó¸Þ:TÖçøübr®ãE/>&¹56Žšþy +-þ +À#LJ¥ŒÏ’€ø D®1 +@CæÝxöø-däqüK¡×hJÄt¤çw¾æhGoM=MIiK»Â<åÍth~,qpÏ{Œ‚>‚ñ<ôß04žçŽ ÷x¶”>ã9V,Œà›gOtÍãEjîéQÛù^ØòáíjËÃM ìîùsg/ãNÑs×ÎùZËæö§ô*ÇJ §ùa+/Û½Ž8µ)U3t¨?üæ¤Ùz|ŒáçÚ•šBœö¥ñ!Y$ÿP¸óÑûžóQØMÃK—q½ +)ñÙ!p;ìØTEcÖÇ45ë–b>[Ç(tfž·ÎQ´Gh-#˜âlñ>ÞÌä~ŸÒ;2 ½z^SÆ9ŸnVû“ç1E…€ý¦@mí¢>ŸÃn¾i¼Ÿb¥µà»gÍ®tÌÏ‘½à,sd‚Λ‚sžnPÔ¯¹Ôx ™×ʔԪ>qeXÚ¼Háç¾}Ý»»½~õ;=ãϧ©ÕÇж¾~üþ%¯2ï¼Ï±Û ¨<2ï‚‘=@™§ûÙëâºß?ì÷#¦gZd\¦ bÀ8*徦ì×-©ÎGÞõ6O|îC eÔ8¬²À·Ã½¢÷ýØÖË3¯ãÙª5Æg¹¬÷+ƒäPçý:Îgd™UÙºéÍâfút·˜#C|>ÖŽéóãc^,¯KffÚœ®öüð±Âï×ñ~ŸZz|ôØB¹œm^Š»ýæÃÐröٜڌ§ü†kÙžñöçÚÔy×Ú~óÔ¨®ð t##Ês-¯ácR7æNjÁ¶rdžl÷#ìÃãÎóI4º ¶’4žeAü³»ŸS¹|æZ?" M‘õöÃN;Øû¼~Ô:?/hÃß<ÐFpU?L§ŠÈüÁ4zÍ ;k}¾/òêNÓnt¥œ#|¨×øðvØÉí~AsìˆCP*þ‚¶àpÜRõpI!.}xõëU8-{ðíü˜Tž>~nj Ÿþ!uØ/Û½›N˜iÅ~Woò‘oÛ¦n¥þ»=jiü í¯Csè”6Â1ÌT5>{9Î9—-å¹—mª°T®çÃ(¤ÉyÄcnùuXï'ñí#âÎÒ½´GÄ™»1ž?ûð&*ÕZçbp•Ç’ã”ÔW›¿QK¦Ëˆ•Ù‡_©3MßµHCÇ4;ÎüL$=VÃýêP;Ît~< ?snùzÁu>1—{(óV"EÎùÖ½Rû4É5n/½yÑÒt{>’òÏ`kNvº›|vÏ ôûyð¾_—°ßÏkŸ¸Å»“ýÇóŠÇU8ÒîÃÚ^ßÛó#Ÿõöˆçû¸Ÿý:ûä>__ð¨Û4„¿õÖ-™“}~¨_íͺ̷>¦PysE2:,Îôº0óN©ŸŸ•í×¼ éΟÁø¬ž¯Ã åõ3ë÷úì‘Óól+›]Ùl쇢#?3(÷”ÁÇ9ʼnÎööξ‰ã »â2㦽ºS©"}bAl“–{y>ô‹piÓëHCgÐñêg]S6ƒø²ýýו±’”íÕûX†xeožžúõjH)ÉÒƇuÞQb_#`Ä ^g‘§Ìã[î§v¤6f1O`›/Íó ÔMeh*åÍÚ”'ZȦ<žÞ°\ V1s¼ñ,s½gð5 +i÷®>ïíÃ$¿pÍ´kú¶ÏÛ¥}k‡MŽ¡<Í°~Ý3’IÄäÛñq馡ÀŸïsÐ|h\0~¿Ý¿d¼ý²Íí’áÀv§³ÉÑßYx“ߎ[kãããvLJžàÃæ[|ïX¬óžÁ\þP§Žæãÿ¹÷j’$IÒÄÞ!ÒOKfºHÒˆ áÜÍ‚zpÎyFržU•Å«éLwÛÝÙÅíÍÊ-p"‡—{ xDð¡jnjÕU="dk{ºµ<ÌÍÍÔT?¥æ»ñ(,—éH²˜Hiû‰MŽÂmŸŽ+_ˆ«,åÞ±Éá‚ T xRl;öJU&ŠáŸ ¤9Êg›R!âÕöÌP +Ñ‘+$†U:Qã4,¶C£½uÏÎKNØÌg¹Rf¸ØæÈÒÇÀ|_«O´9IC‘a*‰.sF®+¤JO¢G‹µ$nƒ¿Æ¯ð‰&_‹X¦~/j{üG-õ(ù­™ßgû³°þ2…Ý´Mb6‡S`DǼK‡ùK)ª<²)% Á™pÀ隒ߟŽ+_È°å%qŠ¥"RØqÎ÷€YôŽ$›¢“–OöùØ•ª‘æHçé§ÃÊ÷aÊÿ (W-“"å"Ü':94Åu >?52Ip®Ð±Ôø2åì üøé ÖçŽGÎF «A”élI„Žý¥ìàÅ't¢u5_a‚•i\åRu$€@¢I°NËO'@‹fo±ôE.DG®–’,q:W'¢Á5WÈøu„™IL`òLˆPà_]u.±%Ƴ|Ž*ñ¢ ÎÎg?Áÿ6Ž·.\ŠAÌïß&ÈSrTD´Ñ=‘È„ÃP}ÔŠDGÙTB>«GzT¾ÏL@Î »YäÃ6#þÌ°¬KÁ]WŽ D]©,qÁ‰OÔL.+ö +ög†deTHK]  \ÎŽPûÌ ÈNJ‡©GÔNî# QÊG%}¼Ÿûqù³C’rÅÛÉàsLƒT7S~9Òat âòÏ Ø4)pºvb`³>Ê6qÉw,䦴E“×ÏÎ@®‚…{GŠf±¾DvusƒLDªŠ)×AhI,Ó¦@úž‘Ó³®0ŸÌ ¬6h ä±ü}½CÊô”3š¡+ÑGÞ®Bÿ2N›d(§–&ÍQ®ŽÛæ˜?{ýz^ºJ})i^šÜMtq*ÎÑ4—Y(xK"në†ÚMn‘Ð×Lг„ ˜éç/|:ƒõÔl•ƒÅijjÖZ™&-éRšÖýýõ5ÈnÓ³j(ãGÕ¿~0\õ(v)³RÉ+âR¤Ï~dT¼@V†‹¥ˆdS&Q`Pß7}¨ûöWß‹DƒQ—ÖV5ú¾]Zb’è{Ñ(qù§ è‡ÿ˜ÄŽŠ‰²fÙg4$úÌ.ªý|›‰6§0°S¦üÈŒ kj?U ‰~´i~v‡ Q ùÓyIq„µ¶¾‡á®Ë%8öû8ôH^µ•¹ˆ G³ù_!g>¯bo.-»O_‡óˆ~À›2Ò&ˆì$YšžX6‰r¤#ÉŽáÂ5.&ƒ¥~Ìñ”ô¿ã¸>¸9ŽåÏ¿ ‰ðÑ® 4Œ#QsIqj”-…í|€¥ë¾?½#É®FÊÏp}nA¢aÈgEe©Odš&­Ó"·›_ã U2ç>æÆ‚\?°‡¸”&¦‘w½ee^Ùí4#x%hëØ~ ÿs“DMŠáôÿ“]´æV×OP½î”ÃÝ”Y ø{yA¶>õh,Iók 7¬­_0ÂD“ õ)@"£!¾C¾íHªôÿã°Ì÷)bCk›x\ð0ø¸†< øŒK‹ÔÆL‡¦àZd‰`a²U§Q¨æ¥ë†N¬ [òeBùùD,êõ‰Òê_>ÈÔ d@_†—lùDlKU×%Q„õo®?A:k»É_{ã#l͇ÔH”« ûŽ8Ÿh;¶œ-Sœ`S* ðÐÏeJ´‹ðuŠ+g$šú?µ(7Àõ“']lºà‡-«äJƒeÆ5‹^â¬>äM:3)j?üg¥hvMå‹@a Î9ëòx ]á?+2 åVù-àÃ\Sãj—)i‚5"û¾yq>}¿ª«r°ðeæ‰ÎŒd=¯ãMØiÂ¥g™€ÏÄÏtUjê_Û^iKýbKÎÀ#A N _.¶t¦ãi 7Í )å‘süè„«Q­˜”äw jDä*âæb† .Á£>Ñ¿¢Pì‚å4¼/Ë?Ì”™ä{ýe„-S²„v·ä”:ÅÉIô9—‡Ov·¥7½œ¢{$9ѯácô|jÄï¢ÿ‘.C‰x}†œ +DMú,¹«Óca”Q_ôå˜Ò Ã _›æEi[Œ’XpL—|ÊØAý jå Òå£dŸ1?+Ç”ÆÞ”ê:k_’ŠOêk·‘%c¼ˆÍZØÏû²¤òÃú2Z¬«!÷¯j'¢’æ0ÒLWà”4€kjrTìA;+Ž½é×_àH3OJ†Ãßk¤¦4nÒ +JµÐ]ânIv›N—òÆÙRhjâ¢k¹*2l…à˜óõúŸÎ¼b NÈR¤ûdw6‹Ëû|²Cñk|ŸtÌcEŸ´–Pïëþ±±I{ ç¡+E"¥w ÷ž4uÚq[* $M ²ðç2¾ÌU¶·ãl¸ýð‰Žˆø]­¢C1]¤÷ÊGIKi¾cÜ'Z­ Ó à,V„E;DvÉŸèÊt0ü›æeK'6+׉È4ÇPãêÃ(C"Fʸ\¿|"ˆQ%è÷õŽò£Š¢'šAòL×iPÛ/û¦ZY‹B¥Jö2UV­´´}"¥÷ñµžuÌ Á‡õDòQG#"³uÔ±è«D– `Êr¾^Bl,C'{°I¢x½T^"ºª[¤~J”†§¥@BF¦8vçp +)ã)Êo¦Q–&¶B“,g8ê÷6„–4Uòšð„ù^†„0¥°Ã/zñÊé÷çØ*âŠÌ kL·È¾ÂKÅþºM)Iº¦<í6¹©Æ5|g“8uþï¤Í•|°ýÄ;¼ßY##ݱÔÊ8B†e”ø€Q%MæuHˆë¤–-ŠND‡6áÑ^N.d$‚E«¶Ñ¨¶¸·„B&&E{mKåˆVd–|V7-‰7ß[‡Ýj¤3ßõ¹Øÿ½IªC¤~ÀÉpÆ¥ßS`àŽ|vK×$Ãf’¦}ŽðlkC"cÿE5YCP‘;è,Ê5Ä^§4ªN’ž«lcÛ¤º=2àä¨Ê¼·¤”¢éÃ3ÑGV·Ô0˜$¯±·hñéþ ¾c÷ _Î ¢É-…L2x© +Û¤b>’M5, ^ƒ›UÁUtIµR†Þ~h„°”ik£AKb]ºñâGW1‚4‘hövuŠ}à°Žr9‘<Ó1™€P²+}\Ø +“Ÿ°—Aú#ÈOÓÑÁ0­o‘¢:¨mä‡áF«ƒgrÃ"|‡ ¾-“ŽðÚ7ß·©ëëåZ»YУB.Ѷ•ä:å2â¨ùë™Kƒ’ø^å´‰+ß)k*󰫺EØ[¸×}¢iSÄÖRù‰ØIË¥Ó¤—“Ðq&·Žª–Àt(ŸÉ™âɗɺ"t‚ú›‹ÄµÆvÝõ"/Ò×Ù¦¥žu 7Y’i-ѧ˜˜Ö¡à7’e WñlðàgM ©ê‹ZK29-‘D˜c)L¥æ¶_˜â­uk )ÿP‚k*3aÏ@‚›”AàÊ ”àLML|–ÈiUùŠJ»%âW";”ì-v½#•¹£´žhÐ •¹A'’6í—SµlUú ,jšÒè<˜¶ïÒ@ÜeªZ[Si¬ÑhÖšÌQnyÛpûˆÎÔ¨õUíY®ÊÆDQEnV Sš³…c["eMU{;x•–O”@IÔ+Îá¾GJ“ÌFÑ^¥](‹‡¼â˜ëˆÈ5›vÂ%ÿZG.u‹b2¯¨ÊŠ1²Ù¨ôJ8(¸4údz/-—|(ÊDïòù Dµ¦¡“1kjÄÏëÊ'lÁ¦Q6ɆÙJ‚ââgGZÙ®*‡Äî¶ÊÎ_-å£Ú¶ZoÇÏ9ð‰ºRNبP¹Et‹’þÄà’hRþšÀO‰ü*Î:‰^ë¨Zdr.ÙªŽÀQF§1$Í@![@•H"ãÊgmuÎ-r`­–*µS¥ä¿œÚÌ81 …7ìu¥™ã‡a%Q廊úr…‘s ™Þ _Ú:_Д̶HqrH «¥ÁgU ¸+¾Î,¤þxo¡Cõ7œékWœeP!:•Ócïj’ò Ë$Ç#Y2ÎF˜íú&ÿ2ÝÇ3èTŠŒ +ámÑìP"=¼°]y9×mVd/zIUe!ÅâðA ’\ß”¥ß›.•0˜šNÏbsiù2ùoÃw‹’My¯14GœLMÇ„O[ÁU¼H‘]ƒ>Â`~i’®‡z]9>tAçµM +¯APÎvZ®ªÑÆd&U°On?G£ä)\/•Ý†dËQFS¼GÁXúb¼-À ep%Êptaqq)ÁÂQ¹EÈ_¶Má .Ï#.®ICe2;ªlʱTwWr¢¸Æ@õ‡ñu ³y’ÖI{ØØÜ&™ìRÔM$zø+(« [lP–¨—;Êø–M‡|"í¬í»z"î·Î¡¿Lá@k]‹îZ¡¿ª§õCϽJ @ÀT8à#Þš£2eàÏUá[‰/åïU¼‚ý±å³Y±)Üଛ.¡¬²ÖÑOl*Kê^§Hé:kÝYÇ¿Éžvý^54€d]_Gðî’–R1mZTÌ&îAýOCdÑ0©«’n«à/…™7’ŽÀ,:dä»Â©r¤U¾¥cã "ÜèŠÔiš¬ícAÃ!ªˆC9’Ýh‹‹AAªÇ6e|ÒÕ•:q•cÔÕ)4&ž¤>¶VÕH Eß‘äµWGeòJ}ãÙ¶$Ͳé]Ž«ÒŽÖÝ|\?MÐÖܨÜÒdœ×•ŒiÙ‰Ï57*²…%[–¸êh»Ô§swL“*bd›/=[TGµ®½Q&kýgIϹØFPå8Q—7¦ .ÌqR‡Ú?`’”2ã åsÅ­§eQ§k«b„µöum?ÁÉ’öW8RFÛ>6’(åE2ÏuTR/êA}=Ç!­jIÿ ‹ªPZJurT§{£”oÕÈòWU!îFÕ^¸ë¬Ôÿðq™ÊKvüBAŸ¸®ù‰>Ñ–ÓÈ$"\¦:‰¹ªÊÜå~º­¿^2êÆ´ e/’“ÄLÛ°Œ˜¶QCDáMübe ft*¸A \Éݤ¾Óé*_e -fµ”LNõ'ÌÜh`H'UÇ%WÅЙµáè`ª6ɪM„}‰ÌO*ÓÄ«d”PÅ««%‘²zܤªdföFdÜðO³UTÑ‚o?!ÑVÕðŒ¬tæl gêñÀð´X?_pw£Puív²¼"_¦Qð«µtReL5‰Xwë`\E:qËIØ!YÙ*xÍ=«,:Nx5imjf1_Wº¸TOÎQ°’hÈ4nPŠ~NŒÀ1ïô…åPþ+ Á jTáHQÅñ/ò½R “c+!CJ:[5HÁg¥_½˜~,c¸—PŸÌlµuý&ê÷6©ÑK¥#ÉL[k=ßõÈ-L"¦:Ù.‰Éåuò·U¥»« a¼˜UÙP¶tâp¦052d9Ûh²º®±Ý¨>pý¦^D¶õ:©99¦!Pï ™ÎϹª3—{^¦a âQj‰W¢‘†×TñƒËéØCÎ52]7LœÓÚG©1*” Æ¯ØÞHW¿'4¶!9§¸¨ £¡IG>½nS#¼Õ‚äP6¡æ§‰ Ñ5é4kºL,ââŠ6Õ§Ö׺ø,ãdïÉæHâI%“„Õ®ÐÈ)Ó»ÑRDß%ƒD}=ª®­`ª¡ð]t$Yfˆ‰M”²É\ç +@iþÈ–F’âêu\…{Åa¸üÂÖÒÉ¡ ëB`V”¥ ,#W—DfiJ½Y´=k‡€+;!pm]%ìúI!HD @âˤÔTî›ÐdÂØô¨fPÝwÑa$Ñ,²š¹RPítä°MÝÏeC¢¥ºüØ–š:uÉ0³LfÓ³\SZÛ¶$ÑeŠ3ýsÊ. T°¦Hh&8^-)ˆ:e _†ô, +²jO!‹öè*¤«ù}tÅ“.Q ê„”ë.l¦´@‘êªã)ÖJX·ü X8ï~/œ39¬¹á}’y#H\²1›xRõ™1(õÌÕäÀ¡à ø}Åé d?ƒØ+Ñ6{ªùpáE²¿+ŸeêÐÊrç_ªôÄg^qñ¬E†±. uu(Œdn1ÕBPêkŽÝÂu²cLׯƒ5t?¦â?뇆±YúÚŽ2eÀum +’LfâúlëÄHÊsÑ®œ‚’Ó +¢Ÿ¡·™r€Óeº +äû©+~¹+Õ9HK WÆtMÊg!ß"’ ÒÙtÓbòYY–¯k2ië‚Ë•‰#â÷ªT@“bVÇš¬l-ÄuåIÇŒ`¾Ö’©–\.¸.A”|Zr‚Król|ä†L'ƒù" ËR¬(™†ÒT:’l›*«Ò/-DâºÔÀö; ^¶Tê¹û¬ù—\w¨Õ">ëw>3PIP"ÐHÈêJœ®!_fn¤Tø­™‘&"“Lz ÄÑã4Yµ¶Ê–©Yœ©©SR¢p І*WpÑïÛ‘d«¼L`&¡ÂtÊL²4¹`ib Dùaº¯ìHVɦ¨ð±ù¬t؉Zs[Õšÿ¼,œ„-É"·Ž$+­8çŒ$¨«*öðž-ú½¬ÖåÒ²àb’4/YpÉEâ*ò×Vhµ¶¦tê#YºEš¶E*ßUÑaî¯>‘­s;ý(ê@î·å ÂkÍ(ånƒaHA!@>%èÊ4.à%å‘rÉ6îQÑŸÐ#4®l°*Òäü-óû¢Qb£ïóº]§²¦¯ƒTâ<¹†ü½£&+Mr$jª¼BD¢ü,‚H8/Í ,d+1ÊÚr\ ZÈAÄ)'‰:£Dq%Wô¸”¯I­48MŒRž¸ÍåÏMƒ¶@ä”ù¿7ÉA+jÊ|íðŠ"¾”§ˆ4¼_U²¼EšI™Ã®o`â ë8Y‡PÄ 'EŸ•ŠÓN zÅ.¹7å ¤ÜHª3à”Ÿ$¼q>‘ò?Bb¹ZDŽ¢â7êÐÙ äk[FäkRˆ²3Ì6)ןû*5¦© _&uÅ€¢Q Ù)œ©µ’ ¦ÅÕÍD”Š‰Ô£%/) M£EBQ˜kû‰;´Z”îŠö£uõ³9è¨ 7‡¼ÑhÃÙ”¹èŒÌ/]eï‹ÂÆÄç ½}²¦2kE”@ŽªQ6œÂ.TI©*ÝHX‹¤W©’ç¯r¼AmÓÆÊLq^)Ÿ’c3{J.¦Fzø\É2Ĺ”ôŸDÛûR}*üÑ¥)Ši)eYVˆ‹#)oÜZ[⌔º,°ëH2â›å)¾!ÍTQLäâ.5•IõÀáîF›Q•Ö‘dÛ&ýåÈêîÒu0˜ -ÓD¸£4’¾Ñλ +Ÿè~[ÿYíÂÐÐà$‹EN¢ÿ{ÙÄÐß\yõw|O²”’…¸íÿÌ×h.[{%‹lßüÀdà ! ü&\ä#©yªÿâªÇ§(•ìe“¿ —ËVȈ´(y%g·È'äçž3`ÝFVApk£n×Uù÷ÜR`N§“D[1(]<Á-² „Œ0 5€®~f“ߤ¤C%ºÔã ˆ²×•¼hRÃZe9€¥JD©UáC2IR ûšÈº±^tº„«6Æ"}ÜßKó?I[r?OŽt ¨?ÖÖ®)ݦEU`è0SÙÌ”´DM#ô[–Î5SZJ‡F§ÞÈ4 éëÓdQÙÞ®ª‡˜¬ˆåxáuè1hDÙ•Rl‚òøé@›jù¹®ð¡j$ˆ;oV¦ãWÉÚxÝ¿ +Â'jŠi)‹· ûf¿ R‘kªý‚¸m€^&“DC?ùW@WÍ4Joåˆ}àQI˜\#3Œê ä³ëò™êƒ^S[±õÁA¿n 'U‹FInâŒ0rÑ2…êUô©Œê;4ÉZHtHPHœ•±!ÏÖÎ`Ǧ=¤ÂvÆ)ébã83e›éòfŸHe|¸uTµ“Õ U‰ÎTœs£„ýÙIT +)"QzºM‡©ð¯à é’G?Šê#À¨-ùn$»d ¦®ŠÁƒ +óuZÕ2#JWˆBUŠ*PKq£8Õ„ +0þ`Q‘¨K±, V¨’uªÅA•Ð0Z±ÐéB­Ibd ëª ÝD‰$$¸„›Õ'’®\.mbu®­rYL„åª'âÏš˜`}§"ãa2ä" †&{丢0ñ²©SOÇ …AÀ»?èTzÃíuŸ[1K6vU’¯¾QÍÿɨ'Ÿ«±¸Ïæ’ÌürB¡aEó×µ®¥ì:AE{]`ÈÁ•øMÕY"òP`ßR)?›õ…Xj3‘òA‘É¡fÊ[†6ÒæeŸuIT)bæFn¤eªæ¶%Ø›&yã¨ßî§ïljiþ†Zëö¥~‡»¢ª*ƒD +5‰í–D[eŸ)$‰dN¡1êôƒY¢]:¢*C4’X"E‰p‰È>%²J¼wUØUõ üùü×Ì*l•ðK7”£Æ¡DfLć ²ÂÈG" Ú¨ýtX|etJ+Š¹š­¬q樶—Ò«¾Q®%ˆT%(Šê­Ô_¦â³2š.ˆªüÞR ~6ƒ6’ªû‹I]™•´¹H¿“D›ŠHßô/Kl…õ(HÏT åV®Ýäçf%›Ha!—N&Øö²Õ˜…Š‹àµä 4ùUA ­*Ñ~©èÌR‰²+>ÀÈyI!ãOßO=ÐõÓUwÎúRw#q^äþSõËTô[â[qó³Ké´ÔíþgÙD”ûaºÀ‰É+øLwã)Ù8éÓy­»åKg¥Æ|;GuË×)ÁÃ1ljnmªâÇ•È-‘DT7×!Ù p¬É7ºí[®jÊ!ÛÔYªM-S原Îk½Â†ºÍR }Zaé8é/´’²¾ƒ5®¬K¾uëã<$«ì<‹îÌp6¢™T²N¥üø×&]Be}¢ŒD¥Ké66}ú.G¥¹»~¸…z Sõ«ÒT°Eæ’»Š©ñö‡’D,Wg!K©ÔÒãG¾¶¨%Þ'3X7öc*ÛVÝÂU£Gµ®E"§~פzj9bû&MYþ^6—ŒÉÔwM%º8äAüäõkF ¶®Ì?ĺMÝÏER µu—rÛZS6FgÝþQ±ºáê¶jšIÉ5²Ü•ª›¨C?ê qtU•)ëEL:-†ªÀÇ5;Jžs Ú7ÝPU5Ï«¼îòùñÀ4a!w}²*K1T¥ºa2*K1ÖwwØÎzbäŽb*±µèÀê$>óeJ$ªÎõBÜÑ>Qð¸êÄÎ6¾¢Å@çóƒ&0©Sµ¤di2âŽè4Œµvå2UŽå›ªü•ªö°ÞC£f¶J;Ã×qê5oÉgx#½ªG±ŽÏNŒ0Ÿ¡Š•dÇŠIÖUº ‰ò–î_¨àeI?ºsIUZ5q *äÎç߶^8ªÜ–­„háLåÀ¤”CK]k$¼ªfTè ZwEQ݇U3`áê4i€ŸÍ ¬´;çT÷*4 iwÆhOW{*˜R2—Œ²Oé¨féÌ¢Bz‘/JÍÒMƒœUeëadÉcM‚½ö`­kÖîI8ò‰ŽIÍ*\éLÀ:ŒÛ1ÖeÌPÝàt‡Þ%†ŽA³úÙ¬ûÀÿldú4KEeTµ†¥À¡j;"¯á•¦e’‹"êgfÉN\HT]8mã—¾ËïìoÔŠRÑIæ:]¨Ãבñõë€Õ§Ctä؆oÌX*ÉàÈê¦6ø7]^GŽÉŠÎú:òuêÞ.¾#¿¾«ZÆÝ>}5w¨¹ƒhfHq"Yv«Ôý‘U´’Éò×!“ }~R¤®Ê(‘î·,.þuë›#O°£L6¦hnj¯©ž)ºß«óù1hpKµÿ4üš›I¦À¥á—IùDòˆnzäÑ O‹±NÒøÅdY톧ýÓàúO¿à;áÈÎbþÑ¿‹MvZ}yUzûáÝÝàüýûë·/}béúöþåGäðôåËó‡ë«AÝòŽ›­ÿ¿øZÜO¯ïhâÿß´à_žéë]Ûéµ+|tôEÂu¹…mþ\Ѫ+ɨÛ~UéšÜù˜Œ=ŸÐôèlñYâz€—ðÎÑâž›ƒnÐ6^xï‚açXš¥ÙŽm.îé4ˆ»zSÄ=*Žm»`¡sqY/ºLà˜ÙCûÙÆmþ·‰}ºÐžÃzà83‹"Ô-XŠg;èÞ„±vº8[¼ÉÒ4ÄG`3yÿN_×Å6«ÌÚ$w¾ÃãiaÚØÃÄ(bi´¶8·Ð Ç¢à_œ“a‚Øqàc8ˆ`8ÂÂ! BØÁ%3pDÃßÍiâ¼ÅhôA8oøáÎ×rD‹+J6бð ×æãl_ˆ©j;ýÿ·'{þ ÏÉy ¤‹u1Ì—9‚*ÚÕ)êÇlæú÷¯ÿœ(¾ïòg,)[Ï~<(^Ï®‘Þ/‰þòŠ¤ŸuüÛó>uMüh›d5Ûõ¨›Ÿõñ\~$ >ˆ]DäâÎÆ"q´nìnŽ¦Ýú¥¤õôqý!í?yãÍ›ËÃñ–3ÓþäYXég£®‰½“¬fºuýA?ÿôË_–B²]}ᨕûðÿ Ñ"Ž(IÃôñ™+ÿi¾¢d@/¦O¶ð2 Rþߟ+ª{F@|†Ñ@ñº õ ü7÷»ž¸¢™0òœXaÄ%€l‘Œ‡ÑÏ®š‹}$á0/?hDšaËRËùxŒBawýº¯HÐ`æ/ÍÊ­âõ§Îä͘ëÏ,á“@X/ʦúÿr÷οؔUˆêàË6tš¿y†Ÿ• È.F(‘ˆÑŸoå–#Ñö­)A´pw.åîÛð1þ÷ £ó…×ÁôJÁúð DÙâY&OžÈ˜´èe6úž>7ÛÏH&¬ÜO> ë6lçgŸ„± ‡[â´Þ~öIX ‚¥?û$ i! õ? ùDiÔ}ÃzMƒüQŸÎ÷ÿ²<øŒ¦ ãc<¥ï_ÿßU¢÷…‹ÂLá•x‚£•ŽÚvã/:?û ï)ñ©ëa>KܱÕÆuœ#|¯}Z»qöÿU|úèü|õ°P0×ú©ÊõW÷—×£zigÑûb…À#$Ä¿‰ã®^¼ƒ¡[]sð·wÿçOãßÐ/èÙŽ ³Ó]1Q€øøÐÁ ˜[åüýyzÝZú‹æc§1ÞNd»NeîÖ–nynz³T~n×ÎXë®ùqÌiZÅY±wi±ÆQ2?Œ¦;QÞÒ +C£8±ŠÓTº“®Îríãòè&?¸æíK·uaÕÏ2ÝûXºHe ·+viœéœ†÷Ùá3Þ½±š'‰â(ä6KƒólçX/Œs›òèEmþÎiœé……Û¾2ʇ‰Ì â4Ò©Ÿ°ÆIyú"U^ìh¥X¦¯y˜|تkÙa¾sQŸ¿Î nœæq$ÝÖ¼Q²Ð³k“V³ª³ÊüUmõMóüw¹É+­¼ˆæú‘t§:}Ù>û¾ºz_™¿i}—<3êGátÇ©­Ìò<‘ííZP+&yCÏuÜÊÄ®ÌzÞexó0ïFx/–˜•%LÌ*ÏyýÈ*/ôâ²½üº:}«•V!ÞÝ7kA«KwŠýë\û,âÖ#v-S?ɵ/œòaÔé„ŒZ2ÝÍÖº‡ïàZ¶Ï«Gzu;š:í8ï%x/ž¸õ3§qnUOÌÊÑžQ Ù­Dz åÆzn̪Ç0T‚÷MoeÝ€Q†Kd†1Þ±N¶qšo]hùq"= XÍ'ÑìÓhv/U ¦Šz¦kF¬¡Ó åoíÅ3Cöžæ…ü<Êz¦·4òÓdf2ë° võ0žîîųQ»Ô«{ /™Øå#Ó;t+'éæE¦y:;ÿ¦1¿ØåTnœi]Ç÷ÙîyØ®‡5xEßð`¹Î´Ò2ävCVí–‡wÅîU†·ný0–í…ÜF*7äÍ‹tëÒ)NJÝ££Ô‹neá ^¤;÷VýÂn\Z•«ZÞ8bUô°0¼kUœ¾ÌöaO¢yئéó¯ÿ}xñu*?Håùþ³öéï«‹oíÚi<7Ž³vŒµ"n#â6aôâ þ*‘›$²ã YÛK•`ÚQÌ?uj‹d~¤—ùþ]óèûñå§oþœ*õ£™f¶{YY¾ï^ý¡ºúÆn_Ù3V;¾}ûoËÿ÷Æ!§Q<^ÿcëäG»ye7/’ù1¬d"ÝÓ²ÎêçÅÁódnð(âþúÀÎL׎½Îu¡u•Ìôž„ùV9§‘ô£ ¹ÞqÊGnõØ̶£˜F*ÓßOyA£ +eý=­q:À¥Ioæ´¯SùaØ(ÅœÆ.,B<¯e†ÕÉ›úì}uòŽ×O[ÓW?üóÿrþöþf;–ëÝ ¯~×>ÿÎm…ì†8Dƒë­ê¾Q=°›p +ý«|çÜ*ŒXazùêŸ ƒË·±£y¶7k®¾«~c'F¾{ •´L·Ø»2Š‹daÏϬú¹7xÞX¾Ë¶Î“¼åuÎZGï +㫾°ëÇ°¿¼}Už¼8zþ§ÒèNËÜò¢4~éMßØõóDf ’ +–HË b¼µ“Ìôð7£¸LæævùD/ÌíÒ‘–…쪑8Õi<×K§åéëé³]<ü‡Ñ݉bß®L ƒ«Üè&3ºµšçåù‡ñÕ:G_-o~l­ÞDxøÇÜìÊônõʱո6pØIº~’Ê€{÷õʾ^Õ½9ügج= ó@ª¤çÇÀɧ¹Ï:±Â"U<1Êg…É[­2ßÓ‹°Î¹þ³ÓÚÓ*Ob…Ý„:õ8oƒ˜‚&ÒÝ˜Ý &¼½X~?áXˆ;m¯už®íÄrÀT@ß +§w¢ÙíX&ì´ôâ"Û½gÍ[Þ¸.ö_zÝgQ·ë” Ù@>$³«|b”Žs½gå×Fiþ(ìì$r AãX¥ˆ80í(H<·û(`? +Xš—b«x¤y‡16ˆ»=§|š«Ÿ‚¬ÈTç{©ÂNª0ÛFa š*Ÿ&sC#×Oez[6*zvÄ«Çð€å-yå¨'ôò'Øâ¿Ý‰§Û'µÙ0Xäž·Œç&©Â4hVAòGÄ:¥YÒÇr RFÅÖÙɳ?€pm¨{V9J·®RÞ|Ϭ­šY˜:¥áM";`Í#£²0kGZqaWŽ’ÙaĪÕ'φW¿)Žo«ÓÛüèÚ®¯2ÝËüðþiæõfÅ›üàrW÷FEËN²ík³t¨{p'v#ÌZQPN-h7#lÈj¹î=o€Ö>JåÇVi +‹7Véökžæ÷åÅûüä…VYTf¯*ÓçéΙ7¸t:ÇÍão_þexý£ËµNãÕ§¯+“w•ÉûÂà•7~cVW ÐÒ¥Eïäý[4n•{§?Loÿ¸xø§ÙÃ?ðÁ›“ëß•çoÂÙQ›™•Ueö¦±úŒµL禔mŸf°.ˆ>»¼¬L_5–_Ínþ€<’ëå PDyÎjG¬Lx—ïݘåCО°°¼q°_÷¦ÕÙëÙý?–毽Éóöɇda¼oÔtoì–ÈžY»-@ ¼v +X^;ñ/Îê“çzqº“̃n2Š‡ …Xí,a÷va%hÔ“ ðóÈ,¹˜·€©ÜöÓX~'Y +¹mxügÌÄÙd_«íëe@>;±,Àx?°×¶7/4/ÚówéúEØé€îs{Fanäg€Z“n@þn"J3.Ð/¼"lÔ´ìè@o„Fĵ¦1‰“>Ówk'pÀaÙò+VEu[Vqžï\Ã&Òýý¤w W¬¿Ÿ(W€P·Ÿ(‚$L°H³­h”ïQ?{ ¥%xQ*3á ÎÈ ³µ“=½ü4Ÿ8Û¡tÔi:Î4N@íî¤>°Èíƽ¸ÛOò!ÊÉ$ÈÉvP󶢙]01·0`ÝXǃūû×ÿíÓXæïvµhÆÌN +€–[‡Ét'“ô"výiØü»­ƒÝb Œ_8&0ÃD¶ ¦VÐ(‚¥â4ߺ(v®róhL¶s¥WûF!™kÛ¥Q¶q˜mŸVçorýØ \bùàùD~dUŽªÓ7µÙ«tó4h×öt/U¦¼Q8Ý‹,8ÀEíÓŸJp`»×‰Â°2y(Ï^#ez¬yQ_ý¦{õ§ÌøA¯,¨gÉâ8ÈéÎ7ý›|ÈçTVÑt/ì¶AZÇÏì"(4—²`BÎöRE=7Îwo@:í$²¼yV=üª0~ §CsÉþoä:ÙþíïÐË`øï' ÖŒØÕ½x:SÚ§ ©zm_«nE ´Œì8d”½ÓxÇñ¦0í­æ.0†V +›UV9.ô_„œæ㈻¯—œÒdÀÈdº÷4Äwbù ^ýû½øÓˆЊ`ðêÙáAª¸bp(©²ŽÒf\êÝ€àÝŽç‡Ü_ ”ŸnÓðÆéÆÑ.úúÚ¬29ÐóOCÚÓ`bë@˜U^Yy 7x#¦çËí£€Q†‰E¬:â=oнdºá”&A« +ú>?â4`zð/ÙÖ¹];ŒXå\÷D/Mb¼dÏ(í:€´ÖÑ÷Ùî‘ŸVfÏC¼ ²ê0xȪ…auú¢4z–ïœÕV¯­Æ‘Y9t›ç‰ü,œî'@âß>ûMÊ›:•%à"°ìÜÎu,?¥{ %«³wµÕ{§z8÷¯Xkߨïhe0Ù`;j“—´ùçÚ§!·µ“(ô*DqÖ«ŽÚ‡œú*n×{Çï2ƒ³]#¿k”÷í ¢y“ÉåïFw¶Z`^U§ÏS…AØàQ€­4½y¬¼Æév<Ò z¶÷4âlÁfųpðAÊ%³€÷{Z:žé€>Š¹˜ z­±Ÿ¬î§Ö.ÜÒ"d–á,Ôv,¬¶j +ž õÔåµ !Öˆe:`öÂ&¢%@N+Â?Í\?bVì\¯5¾Š°:ìEÔé XÅät£V˜p'ÂœÂ8é6ƒ©¢–ÅÜ^‚÷õÜì8 “ÍâðÀ¬æÙ×<àÿ@ª +*ætÿï&2A-Ÿpa¶°ïí› à@Ffåu®¾ù‡ÿ9‘ë?Ú×÷ã@‰ï÷µ2¨Â€Õóp°z;8zxL†â¶“m8¹^* R¸¥s[fn8:ûJËŒÜØÈclG{/•K¥^ûçN¬p Õn'‘îh¹«.ñŸÞdvó[§¶xNï& »°¬íÔŽ»'ßà)¶/o¿ýOÛz.¨µLß æMÓõ°V¾{1\¾ºþæßx÷|W+‡ìØ€VqY¿Z¾øK÷â'­„¨©4{ÍõüÀÏiœ†ÏÛ§¿­¾oŽîÞþðŸËóg¿:°ô ^°ã€óÝ{»zjxþ}47 @¬{@æ^÷Æëß‚&uò“Ãg¿7k£_íÇáSÞD¨[?\Þÿqþâϼ{YjŸ¿ø}Ð)ÿz?ù8hš…1ÌôK¥ + Ž‰Yc°ó(¨oGY0•O»¸]+GUJ£›0”•…?[Q@bM#?Ï6®ÁÔkà(Â_Ì/÷õ'6è\ ÝÖÚå£x¦Í´v’ FQž„ +X[ !á¤'ÜV±}Ò˜Þìèžp‹ Á\Û  ˜,Gš—:ÀVnŒÁ; £ƒZ ¾¢;<ù`ÇOÂ.è>8ZfÀ 0À9ÀQv¾—k,à_¶`+ã¹ýxFÞO–÷µ +à´³WÞ7‹ûQf²ö~"û4Ä@{¢>µºÉܤ1Uì_ì„,í8¹fÜ*? ¹û©²UœæÛç`¶3LBÓX+ÂÜ8ß.N2ˆôÝxà"pˆ‘ïÂ޴òãúè9 z»2!ŸJ·¼ g6ì6µÂ,_¯sÌt«V4æ±]šƒÍb§Z~˜ÊõF‡/_þô_ +ÃÛ ˜ù™2ùæEqø¬uü•ÓD™ÚÜiïØe»¼P:`§Òè>äÖžFH!»X·K7ôÒX+Mto4?zûÏÿÝÿM7 -Ó®MnÂ( —·byá)”G×€@×$Óí¸[Oez»€oQßµª½ëwÿð_£Ùî“h&™éƒa›JR™!¨¿Y¯n^|ûï³›~µo<è{1–„¹Á±ÀLæÆÀŸÉüpਫ –Ún”'$ÝN¡y^é_»µùŽV ròq˜@ƒ8­Liž.Í xÀÈõÃkОO8íé!ÍK:-0Ùxí¨1{ÀŽù~"HæR¥@¢v"X‹ Ñ +…×:Y„_vŸí½x¾(ⶂFu_+> +™®7+¶.AVÿí“ðÓ ÖKfae—AJÃ3;ÑJA0]CÎv„'9àÀžàíR27…#ù(änÇAÿ–ÜPK<3‘¸CÞ½œ,hÙŽ–éfk«öüM¦£—f‰L/ [ƒ/JåúÀT¬¶Êu/XŒµãT~TÝW/­òÌ*ÍÜÚ¡Qe7¯ ¯íúá®YßÓj 9Áœm Jsß(šÅ «-a³ý›„71+‹oí«&Åá}÷ì»ÆÙoòÓ—©ÊòÀq×AcÜ(í%³² n–Xë6Y\•ÇoÊ£WV9žnG¬[Èl…m°þ:`ÌÒ“ˆý嶱u˜çäUãðmcvÛY> ¤}`0%¤nÉ-/r­ó\çÖ­ÈßKyqÙž¿x6@t½á‹áõŸª‹IoÖ_aôÀZWA£¶ååwùÉ»Tù6Ë(. +£—¥ù{`E³0ÌtN Œ¯ÝÄ2ý€QMd`!òú)¨'ØDÀ{‰Ì(Âa°Ýw­Tlü3§²ak”Àb¢Ë®z™È΢n{/ž §ÊÙêÊðú ßÂV5Ý>g-Ô×AKνgÅÑËJÎj£}½0Û,Ó€^ßIx:˜®fùɆÀªïƒ€M•`ébèÏ® êõGû00Xål@†˜í«“nÞTÆ§|˜®k¹þÓ¨ öé¾Q1½‰‘°Ê¼Ð¹‰šÕ”SãÅévÈ~²oìGó¼Œy`jk°Ú- ÞÛCï(о•Ÿ'ܾ‘ž;ýÍÓÈß>ïEÓÀ¿Þ5þn+µŒ‡L@bû©J"=ÜŽ¾ 2=?OfFÉÌLÏ-ªC€'À±åöE’u¿ Xö `ÔˆÙÐøÀÌ-Œôd ¸!­R¼‚£·›(€ù Øl?Æ2èÇ(SØÛ‰ô`;á<v9«‘,= g´Jºvî Ÿ-x¸b•íò*†y*ÿ$–ÞNy`ðV§Nu¶ê‰L?ÆQCEÀxÔÊqyå¨Ð¼Ì¶Ïw,Îëzqä4VfCEéƩ׻Éôîy ©¤²ýSÝMå÷R€X‰ÌÐi\èõ³Dzr~ñãÉßN}_¯ ç!3È·.Ì2 ©Îžø¢îüeT°ÞÔ‹Ç™æ¥éÍÌ"Ìó$–ÚÞüù‡»ûú/QÞÞŽ¶`‰×­\Fkxgf{q³â–¦{Fñˈ½•Ì;•£Òð9ÈÕ|ÿ>ê6YXt¾ã"n?•›ÒÎI _¨˜b°ú·b¹€Q9ƒŸ©¥+'Fn¶:€ÃS¬HdÁ4 $+VažD³²Ú;Z=â zó÷™Êx?ÁRéÖ¾^<0 +»1g+lÂqÖX+ë¼úáã€À¯•’€¦²¨¶Ãl;š–È4Îb(B„ÝqóK·¸qáä—a½ú÷ÛÉG»æN‘ù—»f0^ +¢)E øg%Îú¼qO‚f3–†ìöžVßNVöõ°h‡rïÔX +ðE L÷bi0-Ÿ¬`,µèôK•BzUà ‡Êvô{ÕÌôôL6h+žû–e; «ß…iëUnÉôDÍNœo…ø^ÜC ± ¯¦MlÏtïI„?;"‘ßNlT`ÇXÕ½…S?Ý7ÁXp“¼‘pëF¡€m_âÁ>­OÜÆêË°n¦u–iƒü9m8Χc}ó«9…>ÈŠ»•ÌÍxó*ß»ëŸýP\¼Ù1šÖ3­ °µŸ„Ù—!ûq”X5°ÁÐ>0›ZnppBØ +¤Ê€Ö´ü¸:{¹¸ûitù-˜«°ï1²`G÷xqææ† ƒªMØõ˜YÝŽdÉb »\x4áôé…‘–€© §@c";P FV˜7”æ1ÞÅeg(²xºCá–( =Þéü;€¿Í¶/ì2†/ÓÕ¥×9)ôÎrí £¼r«+·²ÌµNµ|¬Åêä«™f—{fu×ÌX+ˆ¬Ì:F¬" >ã‰Ñ Øû1óWA.‰êß Ý)ñ7Oã¿ÞÒžÃëµ(Š­ Là@·83²]`K`¼0Ú›møœDv’í^ׯKãgÅáMeþ®¹Í³t÷è•ÙËúêCóä;­|”(LS…I,Ý‹­6oËÍ +œÜ^ ä›];Ϊ.³ »v¨ö7Û…uÃkK¯{^ݳÆ9oœÙ¥%àç|÷jyýûÑÙ÷…Þem±!È"Í›¦[—’ÁcÕo€ðIáìƒUžòfÀóÙþm¦w£Ž³Ûû¼Ö86Ê‹ào^¿l}mÖÕD@äÞvª ÜV^¦÷´ZOáð&óñt'Û»ÊÀ„«GûFdc*?uj'™öùàäLD1X7»‘à]£0ÏwîàÛ˜sÕÕx;Û€“rlx‡fibÝXnæg”êÿtÌ1€ÃÜe•—)˜°Û¦AÅ×wãY³0N"wãÙÉÓƒC—ëÞj Û$A³ºÇ@öv, ¬eågzvÂàÈçxU†Ý&îÂÐRa¡åg`‰ƒ¬ $ +1«¶a U£v'‚KT·@°gzÌ;§Tî@óxuQ> ¥p0á“cuåš+°­‚€sÒ}ØA»zj”bðɹÁv$½¹ê> +:½ÅìÙžS9,j³‡úì!Ó¹ŠÃ:ØõQÙK•Ã¼V-k_f{׃˯+Ë°†*³éî• ,1¼i5¼ûCíø›tçÒ(NôL»>ÁZç %ì + y®wYè_±Öqi+¿È÷.k‡¯2Ý `?xWeñºwömûø]iúÂnœZ•£Êè9{Þùs +{šéœç{ÃóoÓ³x~dTŽÜöMfð"?zH÷îŒê1è»\÷²qøZ/MªËwõ£oÝöeª8g­ µ~œë_%‹£”7ŠåúFy +åzçÅñ³êâmiú +x•UOlY³hz — ©VãÀi€y¨y“ ]Od‡QÞŽU;†1YýÄ*ÍA_Gy3âTYu ò–¹3Ö¼²ª`ã7GWÉY]Fsƒlïøg'U¯€Ï¥–Lƒ•QŠ¹µÒð,•ëº•I¦uìÖOÁŽƒ]¼ÌuoâT§»© ù.nÐuýèufpSž¿l¬Þä†WùÁoz£ËÁõwýÛßÆ·ùþE©¶zøC~pm”f  rÝ ˜aqÒà";¸ÌvΧæÏ~NKz3àÆÚâðUëø]óä+Ö½ãí»tóÌ* oÌ2í‹êüEeöÜß'K3üœÁ-k¸Íc›¹Ñ‹òáWÕÕ‡ÆÑ×¥Ù+¯{±zø§êáûP¦ï6Në‡_ÕW_—æo‹Ó—v}¥ÇñlŽC"2°Ëk‹Þé‡Ñõo:'z§_ƒLŽæF° ™Î%|,€I°8Ìâ4ì´Ñ™µDnؤ^;•e¦y^Ÿ¿Énxûµ:{Î[GÉ|?ݹÈv/ààx“ç°PݳošGï*‹fóhǬdz£tž¿ö&/àŸÉüž¬Í_.Ÿÿ±sþuv|×8zß<†Ïy_?ëžÝ\=w¥›‹þÙëñÍwÍÕ«dnd–è®.ÁX.ö/ o4ÁB©™l_Üg{7 àí¥É3,©Î?“…‘Y^–FÏf·?MnrÛ jàtƒ…0ª1Þ´ŠãÒè¶óÝèîÇ£7ÿxÿÓàcç¼sf×NB.( “tû¢6y¾¸ù©wñ}aü”N„ÎlÙ¾[?tä6@œ6_÷οj®^—†1ÞM‘éÞ€´OaŠÚQyú¢vø2 ü€Ñí‰Uóí°0ºÉŸyÓ‡Úá›áõ÷“ûŠã‹âè¬yôP[½¬®^6OaïZ'o=à¥þeº{¦—¦ns•î $«¾œ}wùþ?^~õ¯½³åÙ‹Lÿ:Ó=¯L荒gÝ‹ïë§?Œïÿ¹4{i‡Ù&ºH¹ÒøÔquõžõoÝîM÷ê§Êôyqzê ¾ºrøÞ›½ÊòÃçÝÕëËÿZœ½°lh½Úò]~x«Ý¹À¤åÖ)ÈÆD¾q«viP_Õæ·í“7ƒëß4ߧûWV NÊ] áâÜ©¢Ê¶*+ÀW‰ì€7OFz3!¬PÔ·¿^ÿØ»ú¾wñ üåÊtNà™µCÖ9-NŸ§ÏW¿î‚ÍòÆ·a·a[bʈëêì5HÚîÙ׳g¼ûíÝý”ß´NaÎo&w?¾û—ÕûÜ|_™=k-Ÿ¿ûãŇ?/^üήfÚgùÑ]vpåMŸU¦ÏrÓÑñ;³8Ó½©7¼- ïrýÛÖñ‡åßÆ÷?5–¯÷¿i¾ˆeA«ÎÊ“g½³¯·?î~b‹daR›>×+“€ž·+“Æòeeþlpõ›Ó¯þrû»ÿ¡{ýÃðâë‡þ½2}Ï΋ӷåùÛÊ 6ñûÕ›ÿÐ8þÆ(/÷RÈLdzµÕÛÖé7«7ÿ2~öàíöêåÑÝ÷ `9 ÌÒa®{W[}ݽþýìÅÿAÒ{x9n^iŸÿÄÎŽƒ:WWfÎ @ AÌ`ΙŠ+¬œ»ªºººªsPwKjµ²¬8 +¶’%KËQ–leÛ²,Çñx¾ogöœ=»—ÚsxtºKÕ$ð¾÷>Ïï!ËG°Î§gôî:ƒ;Áf–#õÓõÕ{gv­ÍԗΑñ¦#Xòæ|Åyw¢Íf3S¥åë¤4¡§b:WÈLG]áš3Ü U×£MZšh¬ÞmmêÙ¤žŽ;Å2kø‹‹®Ô[²GÛX¨Ua܇ Âlì­Ý@sGhì0©^fb‡‘šx0çÍö\±®3:AÆ'ù¹xgÛ_X€ò¡µzÔDšã‹ààd|"PZ Õ¶L ‡!äT˜×D‰Ÿc3½`uU¨¬Æ꧲Ý=•€g@ù2è-¬S±I(±¼®ï°™%³';¨#AÁåéø››uÅÚx¨æŒ4AgP¡¤ó$°JDÛ\nV8ÖÞ±ûòZ;g$Å ct§¨ÈD²»im„j뱉½@q¡½r Ü@õÛ0X[+,^ª¯]k®_+÷ÎP‘ +(ðù9“'ø×ÿ¸<ÔLMž‹5·„òrvj_fõZ¼IPQ2ÖË«©©3É©ÝxscjçV²³ ‹¬qŠVo +ªÎ“™ ··ý¥or:ÖÚQ:E•b"e„I jìVOL,/Wî.._™Üºuéñ·œb'Ò½«þª‘Ι™¢#Ô2³E5±x dlÜì+¤cU&9Áå¥U^­;.æz<ÿ™˜Ð¤XÉLìQ±ž+>ë¯lÑ©ypöÎòU%h¬OV“¡ê©pý´ÔÝÔ×{§nÍŸ{ ”|é^®w¾´x±¸p>ÙÝ),^©¬ß,Lž½úÈ÷„ò’‘Š²‰f¨²'’èœÎÌ–¯JíÍÚÜ™öö-”Ke&6*+W„Ú&ïJSû•ÛbçÀ.qé†#˜Õ(;ã]&·àÉΆêëáæ)*ÑH´—üåy"TuG›f6EÆžD›Žw²Ó—…U=Bü)65úÓŸÙ„Sf³ ÑúN¬¹MÇÊL¬‚ E*Ñá‹ ¾Ü\¸²’l­Mmݨ-^µ´û²°qR{;?³Ÿhm…k›|qÕ!Ô‹gŠ³çU˜JJ"ÚÜU×üÅ¥üìÅêü•S—ŸH¶·”DÄ› ¥({èg|–NoÄÚç\^a„’@âøì”;Þär3T¢Í¦ºÑê*¬‚‹A@Áû3“‘ÚJ ?ëò* +n(Mì¹Âuv&P˜Gý¶'V—=é)#[tÇÛ±æŠPèÙ})¡²HF«|¶›ênÃâ3Év´¾|öžçø\Ö!?}¶¼r=1µ¬®q™;_¾•V§NÝÌϨí´;˜‹×à\ÎR½sÅå»!¬QÑ)È°'T(„*9Ÿ™½–šºè+®û*¾ü#uÂ…ÙxuNncÆm„) Œ&3ɦ{v¡‚ûRRmÙŸžR;"x¤Ãä—ÉTßy½©Éa =jt\¢Ñ%°± M8¶Hk7Ô< vé‰v`q wa?{naÿýûžó³s»n? e,¡'〮…òéx÷R¢{ÎmN.žYݽî‹®.Vñè$)Ír™…ÒÒ½LzNƒ‹d´É— þ ¡ ØC§ç`Ý„üìéOMn^QØ} Ÿ&*¬mñ•uWtXð——wî½xïs´X³s9¾¼Ä•VøÊfvî +àÄÜnÎiÆ,{Ýhm=9}Nlža²KfW<ßÚv‹99âD¹X 0¯¯§ZkÉæbaj hЀ;…L¤²©žŠ+àbÎH%VYXÞ»¿µy]ïÃÕe°.?/Mj¤4å××vïÙ¾ö˜+\<¡sÁ†'9éŽ5ÙÜtrr7X^lôv_|ãç™î¶“Ï6/öÎ>Z\»Y_»1½û@qé&!Ô7NßÚºð8¸ð¼h¸º,-Gjk陃Xç4.V–vn4ÎZ¼R°´jlx3³lf6ÖØjnÜNõ®xS•þ•c©£ +Hå+f®lòBˆk óBª‘(ÏÜ!§X¢ á†jŽ`…Kͤ&Ïy^&¦À(­LÜ®˜¸ü€…FE„¯ò¹e9êÓ`~ŸÔ&ÃÕt÷T¾·ÇæÍ\ÑD¦b…¥Óןq…òÁÂtqþ|¢³K¥fp¡@‹OÒäÍ‹ËÞDW‹ûñ@Ú©QÐ5Ñ­ós.±R›Üª÷ví¾ŒÔ\+Ìäfö³Óg +Ó;éæ²7\¹~ßS¯¿óA{íʸÍg$ã›óeøÒ)qJL™Ý±½K=öÜ[álÇê‘b­Ýpk/ÖÙO÷®eçoaøaªÒ;K‹#FzÌä…„(ÖAÉ—2“göx¼›‰×£åy…3{3@56 \.¯LoÞì,_vp©¶,öLLBïN l‰ŽOKkbqE‹ ¬Xæâmƒ; ;êŒMab ñf¼‘†Úî1`ög:ÙÉíöÆ ¡¶bô¦Nê݃:Bïä¡ÈP™Ï.*ñÎViáâÄéÛT²‹x¥h?êæÀû&¶¨,_×7•åDcƒŠ5”¿Ìƺ"M1’a63 ež››9“ž );˜™Z;xÀê +Â!ª›Tj€¿vêveñ¢›3“ñ­‹U¦·Æû4DÇgâ§0Ž0r„u‚ ö/‡ø2 Ôþâ²XÛtˆµ1³W‹‹›b£å!¦°z_ÞÆW SXÈhU·¸’iŸbS3 +;Ïò‹Û7¸XyØäf2³Bu+9u±Ð»J'¦-\QOÅ]|neû¦ µt˜ æ+³ç¥Æ)>·’œïOtòÓÐl¬Õ\½•žºÈ%{¤Ø ÃM„ËÙʸİÎa!#l²ë‰w2òÂe;WäfãWî}Æ,«PÞʤð@÷Χ»û•¹Ë +³GL¶* ^‰zÔmt +êþG~ƒ#dÄ‚*‹§1w¸ +a”ß¿ê ›+;„ÆIµók'´{Ðj“™Tè¸Åä 첂IN.^bó“2£u :WX…Ýá¶4 +‹3 B µÇfæ¨(LIïŠÈ-^‹;ìà6*H·ÝÑ®3™¨kòñÆÌê¥êüžÅ2’Á@f*ßÝ-Ìœ –Vݱ ‡XÕ»#°ž¡\6 õ僥 Ø‘ÜÔ~wóÞöÚM»_Šg›sÛw 32Ä×ÿèв0.9ãÏ,ÃÈ­>›'©qøÆ „݇ RZ¼’™8 ñjrëÁâÌÅ@²3±p!Û=ƒúsµ ÊŸÚ!ÁêÉÈLîq³ÛJ…œÁ<¹[šÖÒi°'w¤á€vóç´Dä7QßöCpð&í\ŽsTïÔÙýN_nPf:6¢é«D¬*­Äjë>ÀowÄà 'jÙɳZ¹1¨°è†‰Ã3Ìy“óBnUÈ-õ‡E(Q’—+> D`aA©4xÀæM8øŒG,L,œ*S2Œ4yD:QsÇ*vvD‚ƒÔZ¬MEú^¦w‹°æJÄk ‚ ‡ Îvmzýr¸2ïŠU-¾´žŽÝ‚3ËsD”‡ô§Àk"ÅžKÌ«¾“z§Ë¹ÃsKœ4©DB¬Ëë…™³©‰SbqÆ*¨ž\svv÷nÌ/–Û,Þ‚Cl“‘Ž“ï¿ý+Gü€¬B~ã%…ÉáôÅ£U€úÒ¥|o_j­ÓѪ/VYÛ½>·{k@GȨày““Õ"~ l"!˜\œ  +ó9Bu$Øp„ÚBùT¸¶c â#F‚ ¦ŸPo\ÝŠr(“²{R(-im¾ÁQèÒl°{Oœ U1èB‰øp¾ý)uÌDòÓ|iNMZ˜„& ꣄B¢¶n¡"ƒ*tH °‘v‡~p™ò*ïöoQc&W8”Ÿ;¡DoÜkÄëpl&Z’[À3¾¸èNtÑ`Å@'À— ”À¥ÊX :ŽºåÆæ—¢õ53%hˆãÃ:½…²»D;6`~……3SjÌœ„¤Æa øÞÖåÖÚ•(×ÛFm´((>atGOŒ›‡Õ˜ÎÁ¡aã«;<˜¯ì›¾t‰MàþLoëJ{eO,w´8hÀT¸×-#BVf§­ž Xšƒ°Œp(iµ7Q™dëlqæ© CmØh”‰ùRmˆ>G +.!}©/D L¼Ä§[ìl0¿È&&‡ö="ûæI­ŠyCU…ž8< 8>b”ëi§¯/-…r ˆ'3¤r Ȭz‡ÏîK}sÌpטþ¨Übóæ…üŸZ°Ói™v‹N_,š«uæ7gV÷6Îݸ|ç‰{ž|á‰WÞxóÇ?ÿð“?üåïÿüô‹¿ÿgîß|"=±Žñ¹þ-ê—ÍŠH )ÛH•ºÙæ|¹;?¹¼µvöòÕž8wÿ§ï}dùâÍSWoïß÷Ä…›½ôÚÛϽñvwewzõ ZžÂ| £3àód¸„x#T ž(·ëÓ‹“K+{çÎܸïÚÃOÜ~öåí{]¿òÀ¹ûŸ~ôÅï¾üƒwÞxçýWôîg¿ÛY=K$Î)ÌN³ƒõ…‹©âô‹d®Øo¯œ®-nf'f*s+õ¥íÞö…û{îïôÑoÿðütçêbw•—êJ¥42 gƃ‚ÔŽ—ÌÎàˆÖ{£…îJª½.Oñùv¨25³uu÷î‡/ßyòùïþðâ'ÏÞû­ƒ;O»#E…U[½r½ ÂéË»!³û³f—ÀÅKB¦/ÖÓÍÙúìÎÒÞ[>ßãϾõίÞûø÷¯üðç§.ß®÷vb…i»7 ¸¨²22‰x¢›TÛ¼2#e"B|²›h®²m>ÛXÙ¿þÀ¿½rßÓß>wß×xæàæ#{w?xå¡g^yûÝGž{å­—ÏÝc÷%•JarÙÈ )–Ùø„Ë_’-FÌõt}~fó|mrfe÷ÂÆùg®Þ÷ÂkßÿÝ—ÿýŸþþÅŸþöÁ§Ÿßÿäw¢¹‰c2Û ¹ÃHÄØØT(»<¦£FTv­•"Ø$B'µö€嬤ˆPa1Y›[?غtßòÁ̓[Üûøó‹;W +Ó{ÉÖ&Î¥Ž#_?ªÅÝB±:5½pjcçüÁÅ«=ö䫯ÿý>ûäwøÕ‡Ÿ|øñoÿóÿùéç_¾öýŸ<üÌËÓ›b55&êÞ/–_\Œ$ •öÂÆþ©³W÷.ß¼ýÈS¯¾õ“Wô‹§_ûÁ#Ï¿òÆÛ?ÿåGŸ¿øÆßþ÷_üý?þùÒ~yç™×—öï‹{¹‰µheš¥#ébgföôÁÁ­Û÷?þÌsϼôê[ïüüÃßþñ‡ï}ôÊ¿ÿòíw?úòÏÿ¿ÿŸÿ÷OPäŸù£_þæêÃÏf»kl¼‚iÁ¿(¥‹ÉÅUxÌ­oï_½ç¡§_xþµ7ÿÎw~ñ»Ï½þýŸ¾÷áo>úä‹/¾øßÿ×øÛ/yö•­ ÷…s.6g'“\´.-ë‚Áæñò‰\yb~ýôù»oß|äé›ßzþîÇž{é­Ÿþä½öëþü·¿ýùïÿõëO¿øô÷üÎïô6¯â\ž 5\Ö+µ½±j®Þ›˜ßXÚ>¸z߃÷=þo¾ðÝ^û§ïòÞ'¿ÿåG¿ûÍgŸÿé/ý¯ÿõßЭ?ùÕgϽòc1?=jp ªì œc·á,TŒò‘lsfuçòý\yè±o}絟þæ“w?þí?y÷å¾óáo?ÿø÷øåÿñOùŸÿùŸO÷ù3¯¼¹uþf0Ó@¨€Ü`ÓÙµ±D*NÕº+˧Î_¼ùð͇ŸzéÍýü7Ÿüèï¿þÎ/~ûÇ¿üí?ÿëãß}þѧŸþÇþç/>üôÚOä:+‘òª)úóÙìCr#Eœq¡‚TžÉ7ç +­ÞüöÁ•;þÛK¯¿ûÁ'Ÿ|þåwÿýç¿úè·ÿõ¿ÿûË¿ýãÙ×¾óá'®ÝaÂUŒ-Øq®ZŸž™ß8µ½îÒ¥«7®ßyè7ßzóø×ùËGŸ~öÞ¯ß{ñÕ—._™\X ekl¢ip5Vuñ¸ÛÇú„t¦°´ºuùî;7îûô³Ï>þðã^zýûg¯ÜNÖg-„`!‚±ìOMÙÈ(Fý‚”ÉU–—ï¿óÐëp?ùåóß}óçïýæË¿üíoÿøç¯?þðË/¿øÕ¼øòËϽüêÂéËT¸hr…@¬ÜB×|ñr¥3לœ[ÝÞ{èñ§ž}ñåç^yýÛßýÞÏßûõßÿù¿~÷å_ßýàãÿôÇúòËO?ÿâ¥7ß¾ýðS™ú’ãÇtÎq ¡4Ñ:Ô‘,÷â¹æÂÚÖíÇžzê¥WŸyí­Ÿ½ÿá—ýÛ—ûŸ½ÿÁ‡Ÿ~úåŸÿüù¿x÷×ïüÉGï¾÷«;?±wùV0]ÅÙˆÜdÓ¢”ñ†²H±;»výÞG^{óßöÞoÿìŸ}þù—ùëÇ¿ûï?ùÝ_ÿö7xžw~þÓŸ½û‹wßÿÇŸ^;w«³yçs'ä¨ÊìUÝ)× VŒ)6{›{¯ß~øù×Þzñõ7¿óÝ7~öË_ýãŸÿüíúÙ{¿ùÕ{¿þࣞyéåÝóW¦¶Øh-^YõrÑ\±™.TãÉLubÔ|cjbniöâÕƒ[wn\¸zn÷àL<+Q>wj­„¡ÆÔ蘑iQ+Jóþ¨”ÈKJ£9¿²6·ºÒ™îœÚ]»~óü=÷]ß¿|eÿÚ­õƒK¥ÉFL8ýio¼ã äÕ&§Öì4ÙI«Ýåt{W7/̬ì„ã‰|ZZ^šÝÛß½~ÏÝ÷?rû‡?üÁ{üöý>þ䳟zñù­sû•ÉIO0JÓVˆ®¨G1„'¸²qnsÿFª6)kÍfk¶7µ³³ñôÓ¾ýã·?üøÓÏ~ÿ»·òöw_õå—^|ê‰oß{myu#W›bÀÑÌ.³3 FÙ“jÈ5ȨS$ÉÅc‘¥ù…{¯_{õå—_}óÍW_}á—¿|çÏþãsßþ·ó;«Ës…j-–/1÷˜Öfsàappzgur(AùÅx¦PŸ™Y88súÁ‡xú™'¿÷Æ«¿þàýÏ¿øü/ýó{¿ endstream endobj 60 0 obj <>stream +úñ“O>ºq/UÈ—ÁîÓ‡”'FM'Ç-f”å…l@H6:SÝÙ¹µÓgÎœ¿xáòÕ«×®çÅç~øö^~õ•ž¿õÊËß¹ïæÍõ͈T´¸| +«Sm£L¸¥Ã¡t‡RfÄãóK›—®ß~ê™~ô[—®ÜxôÑ'~òÓ_üàG?¸ïæ¥Çï¿ç±Ø=»7¿¸Pª¶Céfvb‹‰Ô¿ºÚ¾¬¶Pr j0;#ñÜââÚå+W¿óêkÀJ=ùäƒ?øüsϽó³÷ž~öÛ—/ßÝ›_–r%»‹…8£´2ã|\eÖ˜p½Õi¶“´¦êùúLczÅJ1jÓÙ]J«Kn"`ÍN^¦Ç•[ 6Æìð鬔c;k³Ç2þpÚ+$t&Dc¶j-(êò $ëàSr”Ô ra"‚zÜ?¬GŒ©Æ56êÑšB9ù°·â(†³Úl‡£áD&Ukw·ö&–ýјÑAŽj‘AuT),…‰Õã‚`cžP¥‚&Ü‹Q¼/òr~Ç"b<Ë•ÊÝYVƒ¡p©RM§R å5Z]:”1áB)_Š$1š +Ƴí×­N’Â9¯/“ +•ÚT­>é¦(1gØÕî‘éˆcJ{ƒ•súód ‡Pà,nÅå¢BþhšÚéM¦jk…Ëå©=7ÑôË…Ãqšñ›B®G Š>Ÿjï‘¡ö°Êñ|cHcåÌ„¨4¹a—1Üë÷‡BáD0 DzåµÛì¸ÃM³ Ÿfƒy.RUôo^`ØXƒ”¾qTy×€ìĘ$Tkq[ɨƒMÅrÝÙí[rÄ ñõ$06 ar†Ì®°ÊæR";§F¼‡wT7ÊmÄ‹º#ž`ÙÉeTÌâ÷g’è°a-¢qðZLT#¼J1þ¤Ò­2cj3e°SV·Ÿð'µnÔè4¹"'¶¯ ÈŒêuµ¨¯C á·Â¸Ñ9¦ÇÕVzDÈ öq½Ýìð»}ˆÉ³{%ÆQ¾¨Ã+–Ôj,^„Œñ Éб) •P * ¥¶ƒ2óà¨N®0ÉtcýïsÄ>S FÛð€ÂÄ kñ!P˜˱1x!Ú„‹j3=¢°Éô®Q=5jð(ÑÀ¸Ù«Ãx‹+ɶæVÏ +‘ÌȘ +‡LÖél; +Ï0¤YLŽÐÀ˜ùÿ<4||Ôhv@»Æ´N¥Õ«B88¨|o¬©³ûNÊLT°ÌÍÑÏ9«Õ˜\Qi®"Dàð€ +žÊ@¥Íl …\(Ýa¹Í«D¹a &ëTl’ϯŠÅ•äÄ>T"”Œ¦š:ó£ãƒý‰^Z¨–&Ϧš§ÍîÄ‘Aýà¨!šì°À]CúA6®åI¾ÌÌò‰I¹Ô˜¯ÚDª £ExcÿR·þI:Ò>6b>:¦;6®9.Ó ÈôZÔo&cv6åKOZÝ1!Þe§0.…2)½#h ÂZê ÅE%âûúññ»UýRãVGóH˜7eq…ã¹¹XyíȨñبNf$¬ÉjBeSZ„ÅÉ`ºHýýIz +Ç`°p˜‰5øtGçð73ÙpaDf;|\yxÈptTâ« wuˆ0 ·ÂcH‰ÔA1¨PŸÌÊ]Q:Úõ$zJ+‡"®˜é¸ÂÌŒ+Çõ'ÆÌf{ YXB\âÀøªˆ°y¹—™Y™‰1Ó)w¸Cëj”7)‰æŽ/=or' lÆMÌñ1éMÛ»z”¿ë„ÊäŽÚ( g’>?”#ß<ª«æäŽÙP9,Yé¤[l +¹œ+ +#°h(wrÜ|bD¯µÐ62æè_nZs‡Ú›? ³ÂþªÑ V?ï¨Pö®!­ÆÆXœü¸“ëÐÔ'aÓµ¨.B%û°Í ¬³«þ¨Ì<¢wéÜns3(›ûj̈àÿš\aÕõoºqxs™øúá±CÇåp¦¤Øvp% ×ÑCr ÒFGáx Éq‹gÔèV!>ƒ3®'bzG¨2}&Q[Ð@é´ÍCÜq›;¡²ùá_ÑiÌЉ®»?`ú³+õåû£µm…Í/7S›5Ñ{ Žî/QbÅ+Ýþ¼ÓW0;C2¥µ Ã*(lÕñaÃà˜å(üWŽ€ÓÁ"üC™Ù‹±©1¦wÊ”å7Ž«NŒZµ(¯³óÿr×ð‰ÉÎí ªÝ'5ô •ë›#æ»Fmàn½å‹•ÅóGÔö!#iõ•ñ¶°&TÖ©ä$æË–ëËËhÉð°É=l¢ÕŽˆÁ™À|5267bá@4Vö„ …Eh ¥%KÆKä5îÏ™\~+1{%=³ùs¸X4Ñ1›qE;\v ”®”®† tÌÍç\BÎàÔ¿ÊÆZ\¡þüí¡=Ș5¸³Ó—µ‘ý·¯ÁS@¢Ç4öA"GZ—deËÙÉBÙD\´ ”>¨´©m¬Ñ6»ãP]l¨<³rõÄ›V´R É9Ô›7Q‰1=:ù¥­ë|²udX;¬°jÌ´Þîwøò.¯¶0pN¡hr‡ŽŒŽË­ÃZ xÕDÆÝ¡š¯T_8G„*ý¡4ÇÔàžB´8#´Xñ%&FÏИقùOü®aãWó¡a³ {B _¬åàRN>§°yO@ 0ºä™L8|…hu³´p ñ@÷y²ÍÕXe]g © Xóa5Ge%ƒróÑa= Ÿ™óeæt®ØˆÑ;jó£B%TßÎNïã|aÜäµ19XsÄ#É N…É5ªêÏ„¶:%â·*€(B²MFJÇa¡0^GŌޚ¶úóF*FËSËW–.=®u‡ ΠK¬°ñ)_|*ê9##*ê +WÛÛ„/ÿÍ“Ú!µC‡öï¯ô„[Å™‹±ê¯,îÜ€¢²r)_n&XY«káÆ©pë4n™¨h Z¹÷¡ç‹½]hIÍ£BX & lžKÎá|Uf¤Ô¨o¨?W6`!%3™2¸SVOŽ +µ©hKn¡é!a™¾º£GíL®˜ÉE™4áÏGŠ 8—‚öôD[Z\p|u‰5¬è¨„å6$hXÛ¿^aõ¡Þ,kÛ©¸ƒŽSÁŒõö¹D¨«q½¤ØìŽj1ÞèŽX™$ÊeÔ?¢wk°` +¡â¨G"#õÄÔΓ<®B•VV‹øt?¼„ÂÆȬ^•@È8Øô€ùÚ€öШyPM¨m~#ü·XÇyh¢•sŒØØ´ÑRÙ8#!Úu…’ÜHâdhïâƒ'}ý¸òĸEmãÀŽ&ÍÎà ™EfpcŒ]cq‰p0þôT ´ˆG°•¨/g׌¸xbÜ6¢"´HÐâJà\Åéo(­ÂÆmÆE(oÔ“V #jì؈©Ó™Õo%3žØ„™RÛ!ªôg±ÚøaÑ¿7p¾ÄI]8 §7NòÑ!µiX‹Žê:‚z,€x“22neP>obJ\ SSÎþå3éÎ^zòœ/?ggâB²ãM¶Ínшûu7n¡©í62š¬¬b¾Ì‘aƒÞ€4Q—/)/±É‰pvråìýV ¤£Í5©»›œ<ª¯Ê«6.kõD‹õ…ï¼ö“ŃÛr›Ç€ùq.Kð>½*nÚ}%R(Ç*k‡xR‰›\h¸c=þT¢sIÏ©qAqL¸¢µ7{† N£+Lð§X¶yÓT´-•æ÷®<^ž½ 3CöÉ1©iRšq„ÚdlÚ›˜2QØXøM5&èñ Æf5V^®÷Œé] Ã‚Ôqú2PÛÃÂê +58e—X‹Uׄâ’Þ)ÊÍn5°๮` VI¨o[ø’=P¼<©´hìj”#ÃuŒ/±Ò4ŸêˆÐ¡aý±qóˆÎyBfÓS EW°sy”-I ñq±n”õ® ü(€Ñw+n¡È†+•öªcÆ‹BI É'䶣#zœM;<‰Q}XiEÜA^6Ù±ri*ÖA¸âÍxÂu-õ'Y¡¢ˆë0QcÉÍþ1ƒ×äË(ŠéiðrÊÁá[hçþs¤ŒDxDí ù¼ÂÂüË¡‘£Cz¹ž²‘ä;"P%7àA_¬A ©ÃCòaµMect}'õèÈJ^i&Þ:CÇÛLª‹…ªÎhÓo³™n]TÊ‹Wnƒ‡*t¢*.¥[[•ÞþÌö}6&¢¶SL¸Ê'gý‰ÍAЃ+Ä$:\nvÌê=.G9©—ìžKM°Ùy«7«qŘàðåÂ…yB,[¼I›Ä|€¯w´f÷gÉxK7“Í%.Ó5â\º±Z\¼›ØG5L¨cBmÔì-6ÖÎ\:˜éjpŒ¯É‚Ù¤Åâ¶? ‡Jq‹e™Þ Ž3¢ήÌ^\9÷x´~ +oba¿2uzH9X)TYŒ´·£ÝÂÜÕæúáÊi.+)â¬0󚨸ٓ3s@Ñ£z'ÀÞqàįBÊþˆÎˆž”ìbƒ+oÐÙ%•Ôá~·Xpðy“B˜”ƒÏ¹Ã5±´DEêz;Ú¢2Q#'m,,¾’âP¢`IfÔJ‹ÂH˜\"¸¼'1ɦz6®`õæ\áN¬±,¬A’=2bÒQ¨g°ÜW0:•£“uŠ A PKÊþÔ//¸›™’ì ì‘æ¨Îù/‡†‡åÖ“ãȶãZBnfìÞ B'íLÖꊎëÀ£éQ ¬këyA(, + •É3gï~Ö­#l*TÝà2óÎP#ÛÞ-t÷ÄÊÚˆÕ; °Ù¨8*H¨“ËC#ôËIåpûS¡BÏäÃù‚‘Ñ:#uýÅE2>A …Jgs÷ÖórÄ‹ó¹@a)Z[#õpmÝ›šÑ`~ŸõÆêCú4`‡¯èÏ,Ô×n¥¦öû#pu¸™ +íþa#9 Át„èÖ¥ÍÆò-.Ñö&ZÁ⬠žPY NÁoÖ–/ìÞóìê¥Ç³3gí|VŒ{›×\‘êq-ÔçÏõ¹9Lh +ùÅÂä¦KÈ(¬œX>Å$zV:ã T!ÈûÓê<±Ò‚T_Öâw êÆMàW¨b÷çúß‘lj^W1L¼ž9SZ<—šÜŠ67ØÌ,ÊgQ:üès?ؼxGGøNªPFš ·÷ÅÆ®+ÔÖºóµÅ©µ»¿qÒ¨²ñx¨Å×N%f/¦æ®0…e¦ƒ…ŸykzíÒ •ó$º±öÞÔé‡Sçäö(ع' 5pTn;©%Æ,êë_.é b‚Ò@X‚•ŠBh¥âps[š:¨nÜnl=õshÈœkmÑá֘긄xbõÙ¹Rzê"¤uˆÀJ3 ¼Š{% ÂŽHà±q› sõo5K£üaà™ÍŒ :Äg…XAI,4¦¯ôš! kì&œÒ8Æ, lºÁ-¢\šKMGkËðgH…‘ƶPÝd³ ©öдþ8\ nÂ8§?¥±sr+#3s£zfXCªø˜“`’L¤¤Á¼*Ô3nõX˜d¤²šŸ½_¼b÷Äó­µXmYëllŠËöhiõ—ÂåÕd{—‘&€R0.ÈÎq‰.88-u@EƒG0%&zhÔˆA/˜ãsKþ +“š1“QŒK…Êó¸XPc~B(Q‘Z²¾4¿{kíüCõ¥ 6NÊVçÎ^"žÊ +•7æöÛ»ÿõüêm":¥'£ƒZ¥$1·0Ò¿nÑ9°H™Å ñŸŠväæȘeLï6à! ; .È8®ƒ‚Ë !ÕT}~~窘ÈÖá5y“FoJ+R^˜Ý{0™6G@š8u†êcýIȤåBÉN¶¹ ¶hÀE„ËÚ¸ŒÍ—wÇ&݉._œÍ÷ÎnÜýLfêÌ9¶BÇ»ÎPÓìIB†:!ÇGú½œÃÙä¿žPŽ)g¤ËfVÝ‘.™ÖBŽs[©ˆeux`òÔÍëOMlÝ*Ì]¢“=<®è‚9}÷‹ÁÜ< ¦Ó‘–@É ¹yÂß5¨WYÈH¶ëd¥£'•ÿz×ð€ÂaõW˜üš¯°ÆçWÌNI‹ +¤Pӣ‘õÀ8zbÜ~BŽÉM ð•Üä®ÌO¬{™¨Ñþ×Cð˜/4‚GÛ +Ô‡ñ9"\51’‰N¡–=ØD}E3!bdØæ +*!¿È(]€G\Uà Ð+„ÌÄ]Ãêã +«ÜÊ9øR´¾)uψ•¯Ù:x°6¿¯ÀX>Û«¯Ü,-\5w‹½‹ùÙKD¸qB…ÑáºÝ›ÔÙÙ>K;#f¦à ¶BåuG DXe79C€ €»FO.g¤’¸Ð¿;ÒÆ¥°½ñ6Ê$p6žj­f¦vS“§ÃÕ…ÆÂùÒÌYR¬0±†ÔÚJ¶63§Ü‰ %1Cƒx|‘F(;? ÄŽËÌW £îPËj9ü•“*\aöXÝQ(ip€".ÝËö.<À)ììúùë¾ä–ÆŒ”œb+Vßê¾³~íÙôÌ9(Ýq É%[T¢£Býc&úˆÌ>¤sëñ8‘Ö!*úo¤µŽÀ€Ú®@üŽ@ÍŸ[˜>}ï¹_]½þ[X22v.£wETöÀ¨‰2‹S"üeÜ_†*:4b€§õH=“7{!õ¿êÈr‡ªÐWœÚÊuÖùô$“ê!º…-€D±‰ôôeKÑ{èxÿÅÑÿº‚Í? "ŽŽYµv´¸ ±1ß<2üõÃ#rX®àŽOÙU-:&wÊ‘ ‰L ÈÐCÇãzZnbåfb”Ëû7쇂¥EÄ +§'ŒÎ ˜œ‚¯°J%ç‘ _qá2Js]ÍLzj~"32¥2{!¶(MŒÒH—~sHhȉ• Õú×G '•è0Ä+"„ø²ÎP… —|ñJª¾aó&ʳåÅ‹ñö­X=½;¤qô§“QBÑŒz'$wWhJ(îÄ›ûÅÙ»õýáÏ /qO¤¦9jtË­-u†ZTlõ!JdÛkéúŠÅ0Ù½‘ÂLsõúìþà îÞóíDkÛé/žÞ½÷â½Oc|Z‹´îÂWüÙ¥tg¿:wÕèJ~sØt4Ä âdreº{D…lCƒÐ 5æ×) Ð`ÙB% 6\:\œ-,]²²Y¥Õ+­¥šgÅìBµ·_ë_œ7Qž@öüý/±öq¹C…±~œ°yóÐw€ú&<í 1jp:úÒ½@~>7uzé܃­Õk_ò„«gî1;E¥£ÂÍ@¦+ä–0}@…hR#\æǬ*„ +K‰‰m`Ä›28ü¼Ô7ºF .(­é½ÇÚÛæïNLٻ†Ì&,´¸÷ˆÅ›8»½Ÿm,M¬\Õ¹5VVú`•Æµ¸Öæ±ÓQè¦X¹çà’Pi*h7«—–}Å%©w±½v¯_êLΟyííß°±Î°Þ‹°Ob.R^Ù¸ôäÊŧˆPûðˆÕä\z¬á+‰€æò»„6éºE€óa5>5jpA˜ÒQƒZÜÈÑá†ÃŸ3¢G¡””„ÜäÖ» àÝB-tVñ@‰•:áò"›ôåz¡ú¦'3GDx°ilv×î%õ%3û´XØN§ ±ª­¢ÂLƒéªƒbTïÖZý³×l÷sá|¶½ÄD«RâÞå`iV,/°ù4Ø0’ N,N/žk-Þmp†Ô¨Ü춒qÜ_D˜¤Óß¿ÖÚÉ%#ÙI3è¿©Žòˆ·dçÀ=3£FNpTGŒhÐã༃*¬? ß°2qG ë ’Õ…Ÿ~ãÙïýª4{ÆÁg|™).3“™8¯®”Z§šÓ§)!M Y2XrJd¨a¥2czæèˆõðPÿ:(-™Þ=¨p6ÔÊ­#JÈõAO¨–oo5æÏ %êÓ9Ãj"ˆ‹¥`aI,®’¡«Bk5^šWÙX¹‰4zax@0§;±ìä‰ãÉq«8Šëìþ!¥t^‡r˜Gb"5•ÙcÀ6H +þ—ìºû×Ôe1_ÑÁHùæjiâp3´‘a.(¤ñúêQÖ¯!îàÀ¨Þ€²&§èðç\‘:oÃó˜¾TÞÜ»¸n÷UBÅípi‹ 7i‚‹·Ítò¸ÌîbS±ì îáaݘޣs$ˆÀD}á¾êÒ=„P=!·\Š æ”F׿—}Ä0 !å–€•L³‰ #Ö~¹…ºk@ +¦ü…9}¸Pñå—ÙôÂf…þÑuÁºcÕ(àn÷Ìäæ}«žÜºöüÌÎ(+a^ÉJ'tXúè¤Ê~tXrܬ0ÑCÂJlj‡t®a­Ke"Á• 8‹ Rºµ¼|övqî´Šàp0ÇÂR¬~šÏ/ö¯³5áÕû < ¡ÝgËóæ€QÕSéÖi§ØÖ’F;2Œ+ZiPZ~ØÄÁcÔÈ^Â2‚xÊ®A•Me¥ØX­µt®»v9V[l-ìõv®»Ãy”‰±R›ŽÖÛKç³;WXƒzÈ@bbᬘnCÒ¡>РAƒ#‚Ò’ñi-ÌI™y°?ŽÏË50n:)·8¼)HzzšOuqNš\¾’nnH •£í3©©ópRt¸Öœ?—(/Bf·Q jŠ—¦œ¾¯AOœ7}í°L¡sAÈ Ø¡Aʼnq[ÿ}{Ôœ9ªwÉ TÿÝ!½ë˜Ì¤Åx„–€¢¥%O¨ µtRŽÚœaŠÏŽªíLJ j3­6Ñ®#T&Ï°ÓXÍ{|X¥µQvODnpA¥E “Í%ˆ*û¾Ü|¢¾JeùX¼Ø'X# иƵn­=èT:~bÜ- C¼*¥íF)š1¿Lƒ+lcfÄ´Ó‘oÿú‘‘¯9< 8©°©­^’KÁ!%Ë=>ÞW£nNR"^”K¡“œòåS݃ds£3±òÚÞëm]‡ÌiHÈ÷¸ô¤T*6eñ&•6#ùóZŠT ý(!£Wsrfá‘ti!ßÜ81`7{(;ëëK>¡à"c¥ÎVµ{à$0Yœ^b´2!ögxQZÙË&½D$‘ï&K³ÃhÈ™=œ“Qš=ìÁ?böóZAIÔ v ¡ÔBw}vïxfÿ —™÷ 6<:¹xôÉ~©'Û#V¨;? „ +!5P±jìõ@I1W˜>àcõQ Òih·bv/ç„B¬ôÂVŸ€0©óãÐ_‚ìºà±‘NH09$yºo]9~kÔI¸(ÍϧñHËöäò²Vìuf¶¼þY®½ìpºµ:²3l|Ê/VÇú‚ä­ǟ¡Jé¯N œ8?L‱°£Œ^ªL¯>¨E_>1K™`²!¦2ÝõhcÁÃE…̤^ß$”Z¸Ñx×Ïgi­Š‹%>ÚNÖ–9.¦«Àæ|’‘l¸<ìæíÄf¹nÅ#N*>à`^v»X3¦‘‘ÌbÁToùz¡½Ni¥Pf:ÖÚÉõ®L¬Þ]:xRZ¸Ìg§ê3»¯ïk2Ú2c +,0©„%úf*w¸ ðRÍþÌ”oÚ¿é:æ ú¨4*ž7c,0àGcý»Ïý1ÙU °S}R-¡D¥½©ËÏ%ó륩mLÊ  ‹Œ€¬ˆ9`6Gì p²f_Øê•,ÚKÀèó´'¥Ì 9gÆ ­•¹ªÖ6èhËJD,ˆ *‚WÕC'€°øŠwû]Üñ~"Ùq#ãv|̆g¬¾Ð° ¦úfçš “b– gùd«º|£³qÜݸ?³ýpvçA¹5ïÃ%»_„¨¨ÃÏ›=ý™M2áÐCzS_¤•FÍ°ÝòÀò¸sù_&;vaÜ Ñi>>\‡ÏŒûO^p̘õIJmbBžT˜,+:_ÌOWæ/÷vŽ{;ŠË7ì” + ·7ÅeºLlචÍ>Ž‹Ù‰}8˜?ÿn78(`N º\x (êJ{ãÒ7üýǤ€ÖÀÔ†5 Ô¦Z^ko=æâFogïÖól{ËàáM â`µ?£ +øÈþý>‹‡ÄgðУÀÁÙ©3ãȈ+hÆ"l¼¯n„“ÍÊäfgëÞÂÑ«óG¯Lï>dâ¿“±êJvr×KEH¹à¦“€‹]¸:l%G,丷N_0–í±rù['ÇÎ »Ì.ÚhÇMvÒäFœŸMO­ßìÿïNÏ#@'Ô&ïZÑÈwÎ9ÆÝ”å­ïe#D¤¤{mínºwÕ;¨Ræ³½h³ßè½oÖÍ…óË ðm¤:`ï÷ï58h‹›6£/öÇWÙ/˜a*Zñð˜?4ì¡Z‘Œ•]œ*å§ËK7"õõhu%ݾ˜h¬‘Jèg{Kåsn2 +äDÿ‡t!•¯/l½ÄDj/ž7» üKEÀ>fX±bº‹Òåì”/˜85ê•>îì¸,?È^€ýñ\kfu¿Ð]µ!¼V]i¬ÝŸÙ{6½õ°2wXèí…Òp¬þôý¿½ôô³1¿äÀµdk§¹|«îŪ»L´côóÀ†ã5\È Ù©#ÈÉQdÐÆ!ÍÃdÙé‹7ž¿åÄ¥!'5 62ŠÉådc-ÝÚ‹M\r³N\–Ks«”ÞßnOÅÍ°hAÂÙî¥þ8?{ÀŽ«&í&t+$† Lº‡Ô\ÇŠ„ î .—ôúzvò`rývmþ¨ãRˆ^{é»_~m,Ù8X@BT(;p}Ü‚ ¥½¤vÞìó0˜X±‚¥’ÞoËó8RœŸžß{ýÝ/ÚK—›k·Ú»OÒó7S3ך¦6ïö6ïDk+ñòÜ_ýîöÓˆHK2\³x˜<Þ`Ãe€'½„T0úØSCŽ“–o;5àFÒŠÇ‘6!W"…9R.ú…i¢JLP‰éq/‹…³r~TD@+E꫹¹£úÚÚúqqù®ÒÜ!ã9Ù]ß2{ñ$düB!Ò_U²Šë³hx²?ÞÝ„žqöïàçþGÀ Kv²ßyF.ÌH…©xmvúâ½éã•£—[«7ôƺ_®8È(üÍo°vDwö×›¡b9=±[˜¾ÎÍC|vÔEÛïøù!;6ü&—Aà &>Î-JyÐI8I¦fØHEjJ¾«Ìÿ2z¢ÕXm)ÓÙNµÖ¥ìŒZ^TK ¤Z ǪK;·&–l˜ì[¤²Øoe£7a©ÒŸ4j†Ppeçn´03â`…YÝŽ'Á$¶–›Ú½ñ¬4¹lÄ¥ØÌŒX^É÷Ž¶n¾såé÷ëkÜL:W™]Ù»‡ã Ç"Å•\ïVyáA¬yÄÆ:@Ùý¡­ÃGñÊê©1ÿ`X[ÀNDûÄZXŒMì¹Ñ\m±ÒݯôP1X*¢rE+ÌâHL숩„‘l— Ï™ð3£Pß•³Ù€Ö⢓&?lÃr1Öìs:Ä$|lÂ/¤l˜À(ÅXy. eŠ3‡ G¯%:;Jq ç¸Ñ:°3K‡/?ÿ²¹pä¡LdærFgrâý#ÞÓƒžóãx069dÃþâÄÈéa÷ ·QV¿âí Fà0#—Á§'ïìt÷Ÿ-^7?ÃCE“õîÆñÖÍ·ë3ûo½Q[¾.ÏŽâ½+|y-”šé.ßÞºö¼:¸6.1­¯«¥å@¬g$Œð‹–SÃ.¡ºð°Á¬á&#°˜ÃÂ>7-å¦æ¶ïlÝ~CÈw"õùlo¿ßž¨¼šh]*ÎÜÖ*kVL²cýÛÊ (½É$¦ }ŠNÌ!rÃC%-°8ì$µs/”s½úÆ¥±ˆ·Z8ÖÉ¥Ë,4ê (…%&ÚF‚6R·#ŠÁEû(-¤×RúÎ9û™¯ÉDùd´²JGšfH³‘B´î@ùQ»ßèÆ“ß»óæþíç­åëZ®G«%“;àBB€§úè þQ;ácbñÊR¼4Nu†m؉ ¶aaGÂ@¶y +š‡Ö|„”,v%uvÌÁ(•úÊ©ý—Êó—¥ü¼—Ë÷à¢wŸ½Ó˜Ùñ€%ü\ÆŠ¨£Nà=9³—õàádeŽî{+㸓óQ@Q×Z3™ðó1Z¹þðù¥;ÏQ.å!ãåÙ[éÉËZyQJu€;pàÑa+aòR¹ÖEP5gÇ|.àS„ +Ì¥}ÍŽÈ/8Æ­h¹¹ôÊ»?º0æ;9`¶œD„ÒZbr&Ÿ@(©:½Nˆ)s¿=WLº±`¢¾˜éîä¦vÒí ¹0Íê•Tc¥³}·½~”ë®ð³ÓD¤éåR¼^-MnQJ©¿”%Ô×ó~¸zÈ [},øåâ#6¸/ &&jK7–®½=¹ý 8³79·õ£¯~sxï ªßüªØ WôúÖÄúñÚõ7+·2Wˆw ¨ÌÎE'v“W§Þœ>|£¼r;×ÜøðËß¾üáWfLqóC®àˆ7d£“h7œ_L×–n¼4µ};;sXZ¼šÙ—+óŹƒúÂÑòå—wï¿ Ø0ÕXݹûî•'Ï<ÉÏ\²=Dì¯íÄ妇„S­˜P—*ŒÞ€ù,®!ŠV$4d‡€6Õo.V¸ìDMP˜¯JoŸŠ”ät=]ÑŠsL|âË>6秓˜?÷Íi6?Þ2îbûƒl¤Š;<é£tÀJùMku ÊÎ[`£—qˆpARNó‰fqz¯±tÖ*dOÇ7+îü&7㆗M²±–•n`Ó(ƒ;4î Gm¸Å˘)ÞXk,_ËMíñ™7›qP¬”cM7¡a|ƈùé «ÖüLÜèã<ý­È·zùS\'ÏÚNœ³ ™0"zPáâ@ó¿pÚd°Cfä;ç-'‡]ç-Ø°=0fÅ°PÜ=¤"ågÃ…%œ•#@ÕsÝ‹ÑæV09²b¬¦•z¨ Áœ@k9"œSŠK‘Ú&ðz/r˜ +WRáÜ84h„Á)ì%uƒ“9vnÔ 1 ©r^ÈÍ$›õ¹Ã©¥½› {™öæìÁÓöæãå+oô¶ï'jK\¤ÈÇê©ÊìÚ¥lbBÌΔç¯vÖoƒˆT—B© R/F2—ï¼¾}ëU+®“=*Ús‹Ó{¯v÷^V–“kŸ~ù³½ãׂ© ½ºävi~çÞ[¯~ïçïýð_úè«K/½ûÝ¿½óÚ÷„ÂœŸcS3Ze³¶xwùê{›OýB‰¥J øé)ƒ÷Ý´î btj6?s“Ïô<”V˜X"„,­5‚éÉ@´Ä$.W³½…K¯¡R’Õr\´ŠÈ•Lç°0{+˜Yv’ üéX—КFC‰I¡n5ûÂß g‘m¤ŸKǪk˜3Á‚/”­,ÜJMîR±V RÇä¼éw¡±ã¼ÙGz©0Ê'0)‡‡st¤Œ +@ (_k7¢å9Œx¸lƒÅq{ÀØD*Û \zJ-™Ñ° AÁ$èFªq¥Ó8ù˜—Òµlp±F ¢cl¤† +EàRŒ´øÅSƒn¥Bsb20$Öð1Q­8“h,”¼ŸÏ9IÀt¥fy½¨º|²aðqze‰ÏLÊÌanbM«õîãgõù-VË×f.nÝ~íðñÛWŸ~÷ò“÷½¹xåAkaëè+Oj½ƒõko”—îT—;ëǵ¹+zuÅCëáhõÚËŸÔNCÔJ´†\½˜î^†CÉ@(v÷É{¬R4ú ¡…²=½±ÕÙ|.Ÿ™8Q+•öfsù&HÔxs SJö€ŽJ…@¤k­û¥¼ÑK1jèILHƒkaÇ%/›`¢õÒÜÕƒ§ßÏOïƒ2ÆÛ@ŽZû#Œ.H8‡Æ*dñP"œš§“h_b’3¾`ÎO]àL©-“W–O´¤l“Š CÆÜôy ¡/¸à`ÖЇm$p@žóŸ3Àƒ|·!÷‰AW‡>9 +™P…ŽM3=6Õ¡£MR«¼"+Æ}Œ:âÀœ ÒáÜœZY—Ëk¸ZˆI%Uc¢e3Ä„b­øÄž^ßN´v>ggaLJM~^.ˆ‘’ÁŸ±Ÿ¶_0ÂÀ ûé´L L2Ó\ó0\H…âõhs•NL`rÄ$Þ¼q‘q'ÐJJqE)oô{^á* .Úð Y³6D¹$«×DrTý¸‹4Ã~J3{¨1'îÄÃ^\dCj$šfšåÂÉlmº8µ”iM«ù Z/²©†\ê%›sµÅ=`†ªÝÍÛÏk+7@$Y9Óšá^<+Ín܈VçW êJªÇÇÛ¤ZwŠÕƒÐAaaóÐM¨°P ¥çÍ‹­‡½ÝGÍÕ[BzŠ ê7ï>ýê7ÿ¡wñ®OÈË•umâRaíÉîƒïÍîÜO5æy=óèf/y‚çM8›Vj۹飕«ï)…99V¿ùðÝ{ï¹ØQo…)•ÛkÇë·ß[½÷Q(7Ÿ®.ÜyãûñÚ²É4ù9`“­½é½§»÷?.ÏÝàâSÛW_‹æO ¹N¹†¬¤Á\V/Ì7¯øôþÑ÷?û)ÊøÄ*š£â=.½0½ÿúÒã='¢\ºóæáñ[bjÄü¬Ñ*Ëä ¹‰&ÕÏŒãdyãÆþÕ—Ì^î[§Ç¿ujì/_4¼0à2Á@óÌÛá0Ð~ùö:&æI­†9àsþ0#„K“åÁ¥`¤`ƒƒ6„÷21&Þ–KËùÉöúm.Ýub‚žŸ ¥º&´¿ÍjÔÅZ ƒ‹Ah•âã6a°z‘@È`ƒF혓ŽºØ¤¹¿Å/å¢/Í(9?šÞŸÚ{©¹y/;wC,¬Ž8 ;Ä0jѤ,®§?d£,žÌh…y(˜q`ªœlƒC2C¬ –†ŒÍP8ZÈf·zYµ0Jž²µxCT Ǫjfbä›ÏG)Ÿ "ÅâÌîÔÖíÆâ•P¬.§š|rÂÝ—…:%¥QZw#<ü¤<æœ3@æonaØý!7&8€@ ˜@H +È&OÐGDZ`Ú‡ÝÙÏêàä–« éÚêßÍ,ö.%ZR¦§B4Žòr☛¹`Æ0.s~6æÂ%«±ºI‚‰É®6Æ<¼”“‹‹\´Yîí7W¯ÇškÞ€œÈÔ¶nhÙ)o@Wr=);ÅÅ„œPæÆe»_ ú}½"ç °ÑrSÉP|ZÊÌÓJ a£ I;ÅÎ*xXYJ«‰î•dç@ÉÏ䇘˜f¦ºk×îR¢šêÿ K +¹Q : +ØTÊÝ<~ëΓw^îÄgÀÊŒú5_’Kkr~!N½öþ7¿=bBO8Fí4ÿju'·tdˆÑŽa´*'jnL áct>ÙÚ/?¹•¨-zä"íÙDeÖè Ú`‰—ü\ +f£6o`Ìâ68lN(@sV§Ëêö#\Lo]¤û¼Û~>Sr£³:·yKÌL´’™æ3 N*cò0~B £&nv’LcS¡h ø2`X 2Æ9:”Ê•çs—ͨä d'![¼Á1Ñ?fJŽä&Gm0ÂãþA#êDÃð\ÜÏF@boˆ‹ ;Êññz¼¾*fçÙh'×ÞSSTÌ×—åY“›~Ð v\7ø"."«àÆ-nNˆ†ÔÒ°(IÎÐwa! ${©¸ÕJ˜é+“œIc‚^‡¨T&/ëﯞšõ‚ã=¸‘2Ð f?`úÆà`üˆÑ3fAÆm8ø"$ПóˆŠ77¸Z¾+å:ˆ(…”³h0N‰i^É‚£%¸¨–nؼ¸Å¸0à d:œ#ø8ƒƒŠ×õ—r5žqxŠUPJuøy|Õ©”}tÔÇôwDB¤Žqq6œö¡ü¸>?ìð­Ž…O]°Ÿr™|ÀI^‰”ùÈð¾b7 v4æ æÜt܉+>2ä0Éb3¤åN»ûûÂâS¹©ÃêòÒü‘Ùͺ"ÂN˜qÃŒ‚-è£d\Lññ«Á¬ÎðQFŒA„hv3~Rç *Î…‹#fÈígÂíÃH&èõÃÅA¤È§ê°˜¶ã² qB|vqwmÿ.-§@©Ê™™ts_L/xÕá%Ívœ”Ý˺PÁîcç%dˆIá*ʦ]P($$×Ö®\yðŽ >Ú‰6_È D/ +ü˜6B@ïÛ‹—³Â¢ÑwÑf‡²qpð>Zñà|¬4)ÍI™Ÿìrz eˆÈkå^yåÂ19©äV½\ ¤–ÉÅ Ž96/Å)8yáœéäÛˆ-à%“~6ï"bV( ¼˜”šD‚i TX ®Å/Ùq cŒv˜¿0â²8p;ðÎFï¹17$NB>΋…ÝhphÜ”rj¼í‚E‡—sø/¡C²xH ?…xyÄê(Š‡€wSQZó¢!« 1Û|0о©¢Ñl³9Ü4ä h>*bƒùQ„îÑorQ'ΙǼNˆ7Úvp½ÞéaŒ6Üác‹“´˜¶¸?ñàšW R5xaÔy~È2jôùa`Ô90æ±zB$,Dò¼š¥CI7fð¯OŸ°t\èoƒõX`"¨èF¹A£ç…óf4˜¦”r˜T‹RJ#VÄèÄλ_8;b¶ûœhÐÑ(N•&ÕL‹Uó.”7¹a³Ëà +pâFCZ¢™œØ8kòŒ;F¬F­^?\HI ´(D3ˆ5xH‚‹ªÓ±tüáaJÔ‚Z8q;0ÂFgÀê¡ÉP’ ç\hhÌê7Ú³ ·ô0#fÌå%›.NÆŠm? *IPËF7 Þè‚X/ž·]óÙ¼ýS;uÁ8âd¬há‹´ÚF'X­d÷QœkÌìä,0@Åä²”hÇJó¡è„ ‘ƸŸR`Fí¯¬óɹèðÒ.?ct"ƒc¶1“Óã§íž€Å „+.ã¡4ȳ—ÄÁ¨u€9_å“K”R·ÍࢌvÂhCÎ[Í.ðõŒœAX}È0=SKÛ·½xxÔ ™l¨ÝE‘¬î„X‡1ØÐóÃvÌ!´BðÙå£|0c¬ãlߘÁäòø`ŒÙ‚“0›èߧ¶€Ð‘F;þÂÉÑQƒÔ \°9k:7`1‚âå€J‡â@·hñÜÊöu?¥Ž˜`³¨bq367íÇÂN/74î"5)ÖvøB^\6»é cÞoŸ4 ™0»/ŽçÅ3£>ŒÇXÝ`ÃÆ­È:;äÉé†8œŽzÉê  ”NJ•Ó#ŽïœÿëÓ#'Ì :) “¼ i«»WƒZn؆Ý´!&‡ò*> GH]ß¼þèý¿‡¸¿xaàÔ€Ñê"l„cB$E²ÒÚÎÕîêe“:?æwc6|ÌŽûeD2G2z¾G„2©zˆ~©¢´è†0”`èPÌ‘V7ls"õ2D÷Íf"@‘¨ !ÇÜȘÓgu#d(EI ‹W0¸Ù³cNpÊöþþ¸ŸÑìÀ´úE; BŸ Bañàt8­•ì˜dõ27ü_ŠOprvØê?=l±¢áà@ÄîcG,>p$J4¥i:87ƒÕ3bö<Ekzyþ›_ZH?I×—¤ä@ Sಘò`¢ŸÖ)±àÅUv5V$ëV /Ây‘ „°§oÜ┣ɵ½+˜¸`þúŒáìy+@”T݃¤29Y'¶:`‹Õ58b:7d2Ù\ϧz&çç—'@\Æ…YÎ8G=.gqPg/X^89ü‹Ã A3·9!HjJŽdY)RÚá¡A>­58æ~ñôØésÆ¡»Ñc´î‚¸Ñq×À°}Ä™ì/¦º1Ýè Ï\°œ>7N†>\:;hóŒ[q@I?*­!¯ÕAö«cÐ>hôÙÜ<©1³!(”@)MÆ•b³ægh› ©ñd¡&E“~&hq»Ý·V“…†˜¨¼8`>7l·8ZE%I€“àÈ\&>9?/ÄÓ£NhÐì³xYˆpNG=©ë mffòúû¹zïü˜ýܨõÜ° Á’¢D‘gXÖáró<§ëJ€BæQ«Í„¢M ¦ÒêÆÚìêÖr}q2^Šór'ÆîOû5øA¥Óœ +óŒÀ…äBÊŒ˜Ù dÏȸ |E,SLÕÛ”ÕR5ˆ’\0åòÓ&zfÈö­3£‡óbçG= t‹— iÙr»9=Ky€ Ã&¯ áY ÔN F<”œ(M2J† +gÐ`l̆Ø</ôa Ɉ(vAŒQ.…Ò*I)‹F$A +“  Qć¢>÷V*‰É^ £*b/ewânˆõ"‚ÍÍXœ”ÓÇA¨äô«Åf5Z­fŽãŠ©H§[ê·Ú»Ýo..÷ä°äÃv/¸Áƒ£î¿zaðäy“Á‚ 1\†!„øPˆeƒN7l@LÑh÷ú}8AÂ( ´”µ¸‰3ƒ¦Nž8=~~È>ÐïqäGOœs¾pÖvê‚cÔ„­˜Ñì±:¼J,í†n/Î…t»h~&"85{Ož1z}>Úéð[ÍF±‹A_.ÁLÔôîT)[Ò£I¡\Œl/Ööæ›ù¼šËDäˆÎŠÀ³Ó'Î šGRÑ°%ÂC½f2[ЂÅŠlP‘ : ¨\6+L5´ãëë÷îìܸ²póÚJ*Ÿ?jwxpO Æ\ !‘öÕsáÞD¢št ªfø¥ÉÔñ5ðøìùÑoÿîͯþé›oß›É)`)ˆŽö‡ç +A†£ºR,¨Z¢VI†yZ¨8Ŭ«ÍâØ|!S™¨LÏÏÝ~òF²Ö²@¾1›w`ˆ€WJmú˜ÔÙaÇÉ“güWS”l6OD\»ÃÕ#A ùÌÅýƒR³RÕÞòœšLØ!ÔäF^‡¹­B$ïõºx»1÷ÞÓ‹<¹ø‡_~ñëŸ}ïéÝíµå©R)Ç2œËƒØ]îp?©a¤â©é÷&¤`>&%eºYP§[ɉjj~ºqå`mw»wýòÒ“{{î\ÛÙÜ)JjD¥iÂá° YÌbȾ`6À§ÎŒŸ:368l±Z½v‹!Ш̗‹i‰g8ͤbétRUtMM¸}ĉ³c `MV¿Ça£asŠw££Ü¹X;Ú¬ßÜï¼ýôð£7¯¿ùxíí»ÝßÿàÞ¿üú½ÿñƒW,͵u]‡PrÜh1F§Qg¼ÍÜŒ;§óðîŒz°V¸¸œK[›»ÍÇצŸÜœzûxî÷?~õ?üæƒÏÞ:¸¶ž‰…a£Ù4btÁxÁY¯ÇKúìeZ¯3;]þøbîû‹¯Ýšzvc⳿øâî¿üæƒ_|vãן]ý—¯_ùßÿã7?üäøÉͅݵZ,¦{|”ÓMÀŠª¡|‚®Êë“‘‰42™'Ê bª®÷ZñÙ‰ØÁjõ‡[¿ûÍ¿ÿå¯~ðÎÛîÝX[Yž=j4˜ì~Äž¶¼pê¼Í0&QnõFC>‰4I”µšu*(µIíé½—ìßÜ|ÿµ£ù729­=5Ù˜Ýn¯SJÁi5…>…Š +º1!ßÝ©ßÜ,mNðj?ÿòÉ×_>{~gáÝ{½øñ³ûçÏÿü»~òá¥?ÿòÙO>¹º5ç.‡Õ>npúü æµÈ¤­“&®¯¤þæ­ý¯¿ÿøÓ·.¿~»óñKs÷á¥?üäÙO?¾ú“÷vÿüëWþøã[oÝ®ÞÜ*,O墚‹Å‚b,N¤ÂzÊÿLž¹²|ùêôýòñnñ‹w.ÿæ«çþã÷‹~ýåñÿüÃ;ÿÿÿûÇ?üò£ã½Ú§¯lþâËG¯oDd~Üè97ì4:p4rØ­»Éï4°ˆ5¶Óý¹ÔÞBn­£îÌDï´?ÿàþÛ/¾ýôê“û745bó‘ˆ%$)šql©L]›“ß¾>ñ7¯|þúþÏ>¾ûúüÿûóÏ~÷ówAþð·þüÛ?zýh©›È\€dE9J`h÷æet* _lÑWfäG[éÏž®üìÓ»_¾sé“W6¾úàÒúêéÿüÇÿõ·ïüöwÿáË¿þâ棣Ùt$HR$!‹ËôDHLI|7A>\U_;̼yTúüåù¿ÿÞ•ÿñ»7ÿøõ+?zÿà·_\ýÓÏÿñÇÇ?ywû§ïmþò“ý¼¹ui5—KéPG. òÀ½Vq®›)‹—æ"ïOýÝ{»Ÿ¾¼ðñËË¿ÿÉ«¿úòÁß<ßýÛ76ÿŸúøÿü÷¯ÿôë7ÿå­ÿö»7¿ÿÎáB+yM燆6ǃñ$ÙJ˲«$Z÷ÛôíÕØÍÕèÃÒ;ç¿|kû“gK?ûôæý§ÏÿáGOÿðÕ+ÿöûO~ò½{ësåVgRϵQõ´ªFKE¼ý­_|tù￸ùý76?{mã'ßø×ß¾ûËÏoù|óë¯ü—_½ñÛO¯~z¯öεüF+”TèA [NžrrÀÖŒz·Üå9ýx¯üüöìϾwïO¿zþúò?ýø¥úêÉ×]ùúý¾:÷ü¨¸7®Dq%Ã>O€‘à@8¬æª•z¯™n&©NÙkÓoÞœøâõípø£÷wùÅõúéËÿü³W~õùÍ_}°ýÏ?¸þ_~þèw?¸ñéKÝ×’÷¶KÙ¸dw@#L„á„|JÉixEG·:áëKÊñºþì°ðé³åßýðøWŸ_ûÅ'Gÿë?}ñ¿þó?{eý³7.ݽ¶†"„ÓïÅ`Ú]Öé,±Ól—?y²úåóý¯Þ½ôo¿~÷ÿü·_üñ«—ÿî½·î.-Nå!€"¨fX1’’‚ ‹4 ú&3Á‹Ý̵Õ½Íô^]û‡ïßÿÃOÞ}ïÑê{w»ßeù§\úøÙÆÍ‹Õõ^±+åÉ'E^¬Ï(j"‰D9´¦ãëm}k2z8}ý¨þ£·÷~ùùñ—o~öÊæ;÷–ŸìO<Þ®\]ÊÌ6Âù$„Å…\VT+œ‰+BT <ÚÍ…n®—>y¼ðã··óÅ¿ÿÁƒ?üèå?ýòŸrýîýùë'ÿö‹W~óÝíÏ”Ž·Òí,{íF›— ÑD‹åT>€äÃXQôMj®+S¡Ç{…§—ª>\ü¯ÿøîŸûöïüð_~ùÚ¿ýþ£Ÿ}ïöGOמߛŸj%Bb¡NgëM+A|®­G‘NÌ»^¥n.¥ž¶w×Wòo]ïüêÓ›?ûøö§OÖÞ¿Ó{zØÞí&&³¼DAÎ@¨”ÌÀ†pÂk—0[EÅækúZ7·ÒJÌîn—_¾Ôyvuöå£Ùͦ²Õo4µÉ¨u è(§€æ£8c…Œ¤ã‰¤Â'Dj¦ÝšÌ^lGž6>yiþןüéçOòÁ•ïÏ|x§óx-zgV9^ÉÎ䂸¿òÑÓg7fŽVËMO„Ïh1ìf­$&Ò•e9Þ¢i‰Á°|TÞ˜›Üßœ­Åñ‹ùþNó½—¯mNÌÀ{—'¢•”¤Š´È p@±úÃ6DõÑ œOc¡”–¨Š¢€|!KÈ|Bæ2*Ó«èÛÓ…ƒÅúî|yµ“iåâÍt¼×T‘A1`¨£‹sÐ0÷"!‹Åi·Xü.OjéÔ|+q*~m1þÎé/Ÿ_üîÃŽ{ô£·¯|ÿÙÆç—¾{wîùåÆþ´*Ñn¯ËŽS¼Õ‚/®ì„׃§"±R\×HGEõ­Ö¥ÝIíÊlôó—×þõï?üÏ¿}ïëOoñÆ¥GWz+¥n=*H<-&y½Qˆ’EÑR1SŠ„¨„Hd5&Á2a¶™Öêq©§wºÉ«kí­n®¬i)¨ƒ AAPÀ`'ø™!ßÀðLQÌ .' ù4ILFõL4šÓÕ(Oj,ãéŒÊ‰¸‡‡= +ËårMàÐÝ~Æ ´ŸdZ fÁ4Z§ ¥É`2–›ì,OÖ»y%4 _š/Ì烻S™õ‰ä\^œ/HÝLx2£fDÆ2—MÅ丌«¬ +„B4Ë ~’³ãÜÍ?`ðÚü906îèïm?gò½pÁ2`Æ| ñlª¸O5I‚8-Sñ c}KõÔñå­û×÷/.õ¦ÊE•çp ñ@ÌF˜jÇ”Q7CéÉÖA²¾MN‘T¸Ó™¿¸ÇQãcN IR"+¤Ò]KSÞnE]žàШ÷Ì砳šÑ+ܼU‰`Âbö¸^»2øFÌPæVã‚Aaj©yaÆ`´;À’…1º ûÉ©Ô‚ñŽÁüÖ£ÁFx‘0LÈ!¥HIUW| 2­-¹Ìö0¯Gô"T`/Ê¡S6'ªø©ÄˆûËo~ëÅá £?Èóª F}>Üç…i‚‘8žðº9Ø$üÁ!pa!7 !LkM½±Šw¸h'ÛÙÆËv”ñR*£U#¥9?ç£7Ê2¡H±93hóžq Ù)$\cÓL´‹ +›$Béâä®V†<¼“îoÊpSq“Ot#a?&Ž[|§Í ½„Ž E?›±#Š •-ˆÓp{ØI;¹QçDµŠÖX%ŽW1. +^@Š9'®ºÉ(* ©DÊE.Ödõ*ÁÇ)çÄÌ°dEÂf¿ì +DxÄàᜨÑ*%eðPœ’ R²-ÄêA%çGhŒä8Q'ùÄ€Á6nƒ°DÊM.³â¤“ÃvüôˆëÅ!ûˆ‹uQRm½ì²ô{òÃd˜k·f1–mú¢ éoOFØ´‹Ø1Í…N õÛ_;P) ´Â +Ì—ŒbòmˆÈ':b²;hDNÆGE1Gj51x+,bDfÌÉ š>&«—×Ùh' Ôdü‚1ZIŠÕ©PX~˜/3³bi)Öئ{v26îá|5ÛÜDÙøê`¬#eÔÂ*Ÿ]ºàNŒ#'Ç “¯¿iè¯NŒœóJC,o(å56Ö5ºùS‹‹³1ß>c;3¹¨™3«¬Þ5Bò¨›?k@G­BiN?zœ±sƒŽq+îÆdVÊ“\lhÔ>nr{ýœéßÏ2¸¹3æ YpÝ„¨ßzqŒËÌyBi$ŸZ¼’Ür°1H̹Ù$.çIµŒ¶rÝÝq$h„8(Å$ZN6 ÞkÄqm"ÕÜ]¾ú¦Z]>mÁ °ˆÍ†ò›x¤ëå²XëoKW°oîŽÙqÍI& µÃÄ{¸R'Ò¬Á'˜Ù†)P0OG'„ÌT@­º¨h(>¡——q1GÉe¿Ã"-¹vQ(¯‡róLl³ùé#,\µÀk¡Í¨Î§ç}læœ ¶"~©„ëm,2é—*ãЄ›tÐi€Àà]\–IÎÂrƒŒOû•–Õ ­-f -y;Å>Ÿ‰Mz`º ÕèhÇ葆,d¿s/$ØóK~±6b#†,ø¨3êàÞ gÆ¿}ÖôâbÂâu‚‰M!byÔJŽ;\e’·`šØ”[@i ­né•U.6K6Õµ3:*å1­A‚$TkR¶—œØ¤£õQ(„i5à2á e ¤•«Ø™| \¥¦ H„—ÐÛlj61yÔÜxš½e¥@‡è‰ÎÄüe#rzØ"íɵ¹¹KD꬚—ò=¿\"´:HQL®‘¦XXâ3=¯æ’½ƒ§­‹Çý¶Qå529ëær Þ3õÕ‰µÛœ$.éõ‹Ñ‰+Éék‘‰=<Öµ³Y>Ý«-߉T× ^IÌ̵Vï”æ¯æzG­ÇT²çd’¾o¦ÖBKµ@lŠÉÌZ¥98µ}ŸŽOïe¢ÉɃüÊ1“]à­FoûÁë_$;ëÆ€æ7´ú^zêz~ö¦^_sRÚ`è‚i—¶’ª[È:ª5 +³·kc{˜ÖÄåjªsËU'›&bSÑöa0»„G:Bn1˜ž>eôŽzP!Ó’r&Ѷ)W°LFúëí»ëw§6ïeëbQ±–í\¯®©Ž„²—ï-/›ü2*UjK·.=úèÒKŸ4¶‘‰i[ Îhû¯|ÎÅÚçmŒ-¢âóáê~fæv{ãåXë +¦JS;‘â쀃0b*$·øÒNmå¥|ï¨Ø»B(u›Õ*k&X5Á +¡µñiDíô‹ÔvJ3GÀ¿ÐjS.oú$iZÃËæ1¡Ð]¿¹yû-N2Á§øÌèBRÍ,8È$*V“ðü5Ä$ŸŸW*kÑæÅ x­”šž.Î\vÐg (®Zj‹tÀn|ÅÍ•ÎYh9>±sôŒSÊñÂÈyƒȳ`z9Ý»Ù;xp͸;´yõPbÒKFDñÉmµ1ÀM¡ìJròÆÌþÛí­—µ²¸~=Ó^ÃÂyF¯ñééDs#ÝÙMOH•e<\ˆUÁ÷€ Š(%PÚ€±‚2aS3‘Æ6ÄçÜ\ +ZÝ‹wo¥æniD)K±Vw󾘟¶Q›œV*¥ÛˇO—®¾U˜=RÝb{+7¹MëM¥´œžº¬T6cÍÆò­™ƒ—åb7×Z¯ÍÝ S o=|ÎÉÄÁùÅ:—^@ÃUJ+{{˜Zµ‘¾×HÍá±õÉtïZuí!ÈI6ÙJ«µÕÛÍí—Ô‰K>¹9ŽEL¨JGzs}ÔOô²Oª j˜TZ%¢@sž3c&Iju\-"z‰ÍR©ÅPnIÌÍÙpÙŽ‰Ñò©"BŠÏt½KreY)-iÕu­¸l'4€‡¡ì|¨•;ÁÜœXÚ SK@9DêÛl²;æ &›™éËTbšML‚ˆç ¤Ììôæ=XÈù¸TcñêÖÝ÷§Ÿ—nÇÚ‡\¼ õÂæíý{`ÙAÆ„ÔtfêJzú(5}˜˜ºl $¶ž5f\¸ +q‰D÷°¼t\œ¿ì^ËÎÃáæ‡rT¦g¸ì<žñ+ýàFŠâ¬‘´¡:¨,ˆ/Ÿ1øΙá3FÌN$©Pš¬Öw±/Ø€&5åã+.6ç Ç}ÀOiS›Ã™žÑ/ÚÉ(È+ªÛŤ`±LÏi•uPËX(Ýîm«…)g@¦Õj´º9±zÜÙ|˜šÜãR“D8Œa¶wyÌÃÚ)Œ6µÊvº{#ÒÜÓêÛRq‰Tª‘ò"È69®m‹µ ”XÒKëÕ•ûlzÊAëÉö®V^)Mï-î?¬¯Ü¥§(¹ÔœÞ/õöP1¯æ–ŸÍ¼ÚÚxX[»kïÒjz}ïÞÒî#¡š1Í/•z ä@¸²E§æ!¡¬åz[·Ÿ ™.Šrq…I/pùÕØäå™ËÏë+Çbjfvûaiñº_-…JKDjR'©Ô’TÚ‰·/£j3[×_+Ïõw;’qPöJUX©ù‚¹hy=ÕØšZ¹\ž½äkH,õ„ ”$H[Z¸Ñ‡MZ— 3Z}]*-ÆZ…üPb@ü¨5¥K\CäR¸²¦5÷µÖ¥TïN¤y@Ǻ€€ÖžÎííWâª?`·¸kî† KÁD(X0U_8j.I€ödt‚OMEËóó;&Wnšü­5ʳWjk÷¸ÂrûÞÆ•ÆòÞC=×µú‚ý¯­ÍHu•OvÙø4¬vFýÚG0.Ù1%×»Ü=|½¸ñHjì(õ-¹º‰¨ÝA;O†«åî>ʬ~Ö…Ëf¿âc +@°!bÕ +k§G!‹_Â¥š ×ÏšŸ‚I-Tj±É%`Né£íf’Ž@Ü ‡ýÁL¿¼—³š'˜¡€YËÍq‰.ÚPçÓ@Jñ™i¥¼D¨5àà0 û‹+D|òô˜ˆpZÉö~¢}9븃é É¹tnb‡TJH0™™Ú//Ýño®=l¬¿”ê]E#5;©å¦¼4¥W`NC-ÌiåET.z(MˆÕ¤d%Zž–ò3 ºáp™§µü4k^ø¿ì½×dg–'ö.¨„ÑlO“¬ª´áí¸Þ{oÃ{o2һʪ,_E²È"›döLwOÏŽÛÑìhÂX9èA€þ½èÿ‘έÖ.VzÒƒ%ù1˜•qã3çüÌßÁ1XÀKªz£¢×õþ™7}Ì^F³[r*‘Y_¼ùÅ}ùÍåÎ_óM@•Î‘-În¿ ìqïâ‡öé/Âõ—³_ê×EYÊiΞø«ÎÁóy†²&±O?þÚ›¿ðÆOjãëéá«¿ý×ÿãóï +RQ{lph_._þáòûž<ÿØÕç_üùßÿóÿÚÛ¼Ê ¾uÅ6sÍëÁÕ¯¾ú§æÑûªT÷G—îðòãî¾C«”5–£MÐéË¿¼þü/ê³Ç‡7?wûb´–뇵—ãB`Pžýã¯Íö±·Á.—ßnn g/g&¸Óîêy4¾d±6z~U?ú2X¾­õªP«/žÇLU•Ÿ]ˆ}«ï!ƒÓ/F_ Ï¿$ºS½u +X½W‘ ‚É“ó7¿…㯿àÁýªâô.»—ÂäŠÙý“ÎâñäðÙÓÏÓ˜\"rÛì=±ÏÍÎØ° ”¿öú׫›ïN^üªª´«jß™½mœýÊ[~aô®ÌÁ í-Á°Ÿ¿üMoýì?¢tiwÞÙ¼?~ý/翽k-ž€‹øëüŸßT‰¿|S?Ž·UoŸ~Ξ“ZïðâËþ_þ÷¯ÿò¿Ä.qÒ¾ôgoÛÇßÔW¯þ¹ÝÜgÜ ®u £Ë{£æÁ)ZL9Ãëpõºsúóéío¯¾þWƒËoq£+†So|™#Tl:h§%aMo!ÔV¨V÷{J}™çBÜæ¹z¯¦g ò•ñçpð«·¿Q+PÅJëDë_C„ |yôö÷¥ÍXZÁAÖG³w|U‰Kï5µÆUyÒÙß;ž½´;‡{ˆ¶‹èY: Ì‘ ð«ÖY-2›«æêvýô»ÅÓ¢åmÌPÞLoŸåi €4ûþäéâæWGoþVéÝ–„n²j…ƒs|]’jû¨š¬¨¶ÎØK³ÿÔš<Ï1nUŒ¦_ƒD>"õ.ª´Á궯û‡š«÷` +Jߟ½”ꀑ²Áó+ŒÛ_ß|ùW“˯h£ ð¶wtf궪õ*ªñôôËÎü`£ÖÚØãÛöá—‹ÛßD«Ï‹b'‹Ù~ûxvñ%d1gϤÆ1˜50†ÁøÉèìC8zLÈÓ›oǯ>îäìâz|Öüé¯ÖÏÿ‚2Ç€~%:`µ£ös¸‹pQŽò0½§5¬ÖAcxÂÆß&3µÖy´úáj¹ªš*ðUÊ®YEXÚûã+£ æ )®¯t1¸øì˜Ñ»,²~…õãíÜ•†Õ;ÿc÷z§ßO@:^}'56‰*Ôœ0û˜ÙÛG¸=(ÏzÓhö-ëŽúg_¹Ó§ÖèÚ_…‹'Zÿ3z`ÿÍáu†$ZKSµ}ÎÌ•ädèàðU4ZÑ:4ð¾Ò£Ì02 =fÄ;Æ›½‹ù“_”Å:tl|ö¾½ùd†Ù9'O!žËbCfZmoǧt”æ±Ò=“XU›¤Ù"õ¦×9vºGY\N£"ØÌÝ’‚ªZg|ËZCA­ž¾Ú<þ€‹õ'l¸¤¼9°•Ö8’‚ež²´pŽ/­î¡×‡nŸVôIžWÞî­ßZýó®UÅ€T;:䜅־ÂíY–tPŸï~ Ny+C‰€\1Z'‹ëg׿Q»O¶JZ†ô8•ÆŒ"í(—PZ .¬Æa8¹Õ$ƒI}rÉZmL +r0ÇjóØ=6{—æ!hÎoÚo`5åÖ)W?N35\î.Òš'˜Ô”Ýñèü=Wx!h!ÒÅû}QÎÙ“oGÏE«¥„ãh|ÞÙ¼˜Þ|Û9|›%}p‹À†‚9‘ìÉVšOULÎ_³çƒÓ¯­Îi… Sˆ†«ÝpzkvÎ&‰Ýƒ0tnfïºuø­5ý܃)U¥@p{¼;$Í^ŒBÁ +³¦%uá'Ë2cGÓ'Ñâ5i0{%Ô*z¯ 63„Ië­ª»uO¿\|gõÃB;ƒK!ZˆÁ¸¿y5<û +ÕšµQÕ›Àï—_þõ³ïÿizûmxSæ0¡áê ª÷Š© Ø宯ßý~vù¥;<ŸÇÂlZä"$Ð!|xÇo~5¼ùs-¢Ýí¯Ÿº½#ÆÆ‚ŠVFë ÀµWYg"F +,Mç´¬v2Œƒ)Íîê¥Õ<ÜÊ Ò•4¦‚¯ªÌžVô>,+£4¢î (´$¦K­wmŽžƒ¨«ÂË '¶çàY49+svE +ó|4§Þè…Ù»eMà&äš `vËJž0c*ÔN‹wýï›ËWÛ‘RÚƒÃ×ÛÜÈSn €%•Öí¯(wšÇÔÅéÅ-"De¢¨•§üjHþÌ«Ò^ùãþÏT<ä~Yj¥è¸ÐO8ºŽFË\8½þÔf´~cõ¯™xgN7Y9òÚkˆù2ã1V¼ó?zc‚)C°;›Çï›ãN¯IÑ´,w¸àÀ=•‚Zîç¹4¢4&ûG¯÷5‹Y‰²º tÝÁÓ¿®òµ<¦éñ·½Æ„ØÌV 9šÇœâO+R‹Ð»€]J@OpƒŒÐ:çj÷1׺¦Ã#Ò™–… WÛ%Êcôï¯ôþwþ¹Ö½,Š^šqPÑÍ…äxg¢Ö7 ÀÝñÓÁÙ‡áå÷\´ÉQ`{ǵÙÓîÑÛã—?ž¾þM0¿e‚EïôóÙí/¬É“=L¢'¯”:¨… ¹¹Bôz§ÓËo+bVÁê_Ö6ÚG_ÜâÂÜUå&köàQÁ%äºZ?ö'Ïg׿ž|ЛLjTOTUγpóg€œàÁÍÁS€>ÀTéæH·"Ö ¶ ´ë nzç?ïžè_þ¬¾¬šS°6nïx|þÁ\ÃÍ.Ühí xy\]N©7G§/¿ùkÚ' l8ã-){âŸû³wÀ3‰ŠJ¨}£y&ø‚¿6:×àÓ÷*ê£4Ãêñ¶i¬7®ˆõ*_Çå¶,)kþ4‰[뮯ßצWÎÓ[Çõ;oüTo]ÀÃIÔ|˜"w ¼ìMv²4mtÂé“áÅw/~ó?¬_ýÞê]ìæh”ÒGG¯Þ)D&@ežrA¤  d”v‘ã yœIŽ +ÁfYZø+£··K+Žìu$Ä““DäO÷ª¶UÎ £veoœ'ÕTU$Ôvž‚mÈá Bg¿ ±j“ÑꩳŸa’™µ€Cµ"eÅ{eÔOìÁ-ˆ@«%¾ÆKÈNH.XÉõã`ò"\¼Õzç9ÆßÊó_#´.c(g¤wÎœÑX[“F»€q%Œ#$O‰fà+;ñg4¿=þeÅ%ª2)»f8°jc­>“jsp£óo¯¾ý×Ýëo™æ!ª÷õúZð!Z÷O><ûößÜ|ø·ƒ«?Ïâv•qΑÚ:&ÍÄžT?jgÍõWîàS;%R7ë+£y$úÓ¸òˆÚKTÌí õ£ÎñgxÃÓîÑkw|ÈM@6L¨­‹Úú½?;ÓÖ£Ññ«¿~Çì! :ÚµÍôê›ÎÁ+ÚìzÃîñÀÚz÷F‚Uö—jý ³|>;ÿŠ1»œ=Œ¦ÏA|ã½}Žë# ¶ý²!d„Ë8!…oj÷óÎR«ƒÈÓ¡?|¬D«,fVÅ&eÏôÞM°xÓ\NÛÀRRïŒÏ¿£ªv){ž¡k0¢ãC„pfw/G+Þ(쟥7¼&íQ +7Rˆ¨yC»¾ÎÇ_š Íö™¬9{ŠJ­2ßØ+É{EA¯ðîK yÜFèÐ{[ñþœÂ: f(gÉøkè`|ŸÁ÷¥IS–`FÇ>?j㛥IŸ2§EÆ-P:m´ºoO¿øûþåBý09ѤŸ1[¬Û×Z‡Öè18)¡~˜1j”â¢Í˜m¹¾RkF`ÕáµØ»#e´(½Q‚˜Ó§Ožÿxøò÷Æèe‰k0Z‡w†1Då¦7|b´/AÃxÓ×rã¼Ä7J¤eDsÞj'ÊL™sÅ`…pMÚYÃkÈeÒè N¿³¾¥­~™x)× ä¾®~ ÂÖ] F­Õs¥qg=¥}>¸úþàÕïãO$O¿‚xœþü¦µ|šg#!~ùW_üËųßø³Û +ç‡ÃÓç¿l-ž'KÚY†ØÖG¢5a”V¦Â»­Íðø=d+eN ŠlĹËøB‹ö%HA„ »ËW½£/åæ©P;$ŒQ·`y´Äk  Ö›6ÖŸCHãq²7Šl’(p¼ÞÌ&g4Ó'ZëÜSU¨óî¬@yr\‘ ¾žªh¸Ô’ükωxOéh •;éª \PàÂ,„+_‡7…Y‚4ñ—Þðq‰oþ‹í +­2©¥0·À7ŠBÔEgõÒn—áU¤‡Ë]ÔI•äýb\r^áëI:ø$Eî—…"©ƒ Tš§µùë‹wwö¡?˜ý› i4©µ¥Ì…Æ ßº;àõ> ÎŒë q¶Ñ˜éÍ% 'øö>ê8Óáú-¸DðH½N;cpp“ËïÃé³,¥%ª,¦FJóHkZ À_öÇÝÍëþÕwbç ·ÇˆØ$¬Q)„ rú§öøt ÜxÉ,®³Ö@o_Ðö4®9hœÑKgòV¬ÓF?Qf?KUÞÃMw ¤ÏZc½q(ú3»½±škÀ„yò†q§býØ›¼ª¯ÞË­+˜Û ¡gPñôùÍåsHVïÄ"wi{MŸ2V×:ÅËÁù‡áñëÎú¥P[¤h »uÔšÞèµe‹´›#\1î,Y5¶²ÜNQ´ëîúy‰qRñ6V¾®§ßÍ®”¢£dE§õîùËßÌξ* uÊšoÚ½k{ðXkÄW# ²Ù> +§¥ù¨(æØ3&àž@IuN©™Áh}óÞ9ÉqµG%7Çe©ƒ({x›Ã­DŽ±¢e{öâ9U5$É­,‹ˆm­Ý¢X"»w^[½èeÝ¡ r”ŸÀA|öYg•#Κèƒ'ÞòµJn#´Ÿ©è‰¢,™“Úà"Q`~¶S~˜ hu@©½2ë#œŸ@Àõòlz#FugPŸßößöOÞ Î¾tFW )c ×Ö ìáÖZ¿[<ýÝðò—£ X@'àjqºUµ¦4O¬Ñ+¡õDï¿î|3<ú`uÎs´I¨u)šFW› %P¥¥56›g¿=ûâS’U¥H‚Û&åÕ:xŒ;³ÚêËîéwŒ·BÅÚp}ë÷OŠB-ÏEy¦†*à çƒ÷ÍÍ7”5‡9/³ ßæRý Ì2Ü„å®Êm\ï цöV@èõ›`òL®¯ ©vŒ¶GàË2t¸WÑ ´¯Õ1¥·“çâ^azXo­µ'q7OûŒ„Òí~áãݪ˜ù¶¬ÀAR¤ªfUî€ãÈÑn¢"ÆXmž××_f(0ÞV–ÉUÔƒ³/ó*ëæq³Èx˜áB·t¢9}¾ºþÃßÿ;Ùé¦Q#YŒ|xŠÊ1 æQ)[ÌÖFpGYÌ(ÑAºj€LÅ}Rªg¡€i”1aƒ.8Dµé~QLùlE”¬hòOd²b˜ÝøŠ,wüLj\ìUŸí’ ÙxslÔ3¨î´O´Öc/‹Ll» ›½%z·wÆxóÑ/’±OÁ¥š.Íö©7¸ÔR0•Â!"úqù¼ŠÊC4b8§­e´xo$† ¹y‚É=Bí‰áÚì_Mÿrvû[kt[ä|x,bE¨Å¦Ll••@S¼£×ðR­¯íÎ)¨0ò˜5F´ –jøBé>Î*ÁÙSîãE} E!f’UáÛj㔶†[),U`w˜%Ö^ Ê0…û2ÀÔ¾nÊR”˜P;¡ý e¸è“<÷ižIág «œO¾ÑÜ€ÈQ»W\íÑûIÂÅ…)-€ À±ªØ0Úþôµ7|º|ükox!![=£¾HVÄ}D*‹mÚž;ý›Éù÷Rý¼ÈÖ÷Kr‘rr„µ_ßäü5 Ò½} +¿@9;96‹ã˜Ö?X–š˜1 ¯ÔÏìÞ aŽ•à° +„((a¥~hv/“¸ÒHÔõþ9£@ˆ¹ª +z ‹j‰¢,)%\·¼AkpÚŸ_¦Êü~QˆO詹vœ!¼Ý¢ü³R™¶ÝX£žìf9JíàJ’”Òû]’Äg;eJªñq5½(ÙyÂ+N5€þª|Td‚¼Ä™£îúª4²„ w· ÚœÂÏ‚v¿ýÝ?{í“ÏÔVNÃõ(“<¡•Yä1Ð.!7!ª¡cW”ÚŒ÷éuúj4½)H³uàô¯•ÆI|Ê…q@£R­ÄØÚŽ ´6(¢’*h¹Ãpö¬±zΞˆÑ"Þ„ÍèäH b»@:´1 ÞD®BxŒ´•§=Ti×¾²'oÍÑ­Wg ô.¨hÒè‰ãbÃ<7º·Rt"ÕNXoo›¡*”£ø“<騵£}øpd@Q½ó8Ž+³k5A´TäåmJòˆ0çfçÊj_ ›ÃÕÅ;Öd¢ýC‹+×íC°™%ÎÛ+ ’?w>ž“„`ô^&öA\Æ+‡iLÆSBœ¹úNÙH¡¡ÌÖ…,÷@á6hfDjâèN×÷0'áì•;|L(­'ïÿjsû+šU•aOóBc³r´é™Á MµuƸñ&–UÚª +ÛÊð¼5©°!„&Ôh}l5N¬úc Ôw +ÜÇÒöúvÛË2ûe­ª¡‡` +8€D*x”fë—À§[9q¯ …å1 d›Íh}ðg»è§ûÂFEÚK”Ôí ? t$: 5\¡|´•¥e)¨ ÊsXšyÂ/Ó^Ô;m^€ß/[¬w "¡È×ólX[Y.zX’vJb7ygæô.¤pɵ< è*gQ™T" ¯pþÆ>§¬)cI½ŸÂ­ÏÒÔvA(Цu Œ 1kC;SÒ²ÎnÆÛ)I)T‡AÀIá +&w2R®€C¡­‘Õ¿ÖÚ§”5®*-€AÈ2 +pF{e©@¼=ý9[•)%ÞÓï_€Š†¤H!* Ö{@”¸*kÈYcàDø+cÀµÁ¬ówzï)„¥Ù>³Ÿî6»àß3„Y‘›vÿFï>&­8ñã?*qy& 1ín@å¨ZŽÝÞI¢öNAw{ǤÑM£¶èݧjëÊèWÎ÷ Ð ˜.CÚ;™qVB°1×í£ød(Ï2ëٵٻïÿauõ¾"5ªúˆô–„3«ªñ5Fë´Hù”Úpú牊—D§"Ô˜*­³ÿ¤‘ ug›wƇ„M˜^†©;ã£óŸËáâQ²Jð!Ê…iT¿ ñ¶WR夲ÐÓ:ùwsb…Žî¬’J+K¸à‚Îe4{•B´fà å[JxÁ\¦[ITƒ9¶[M"zEhˆÞB —œ5*‘ÁvFÚ/™Œ5+ uÆ; ¬9Ü0kšf•øt´5‚%P?g/´æ%¦ Ë\„ÉuÚ(Ѧ>cõŸT¤~‘kç˜&"tU+ Êø©Nè}ð¿ø€Ep|P× » êìá Dh–á]*bº­°.&Eœ?VÛÇåoò´ *Õi½Ãíúø¢{ð +˜½ª1ä`N«­ +ç€(­ ˆ¡…´V« +AUFžC¨TX/U–X½c56’3&Õ6‚M€Ñ+  «{ yò´ÍZ=È ­yÄĵÌf œ{¨66e±~\Œü £ÿXˆNhgQ |°‡ŒÐÕÎ’ Ž!Qµ-Õ’¤ó§Š€Vûœµ'ÀSûed<8ýîáûÞÑ×¹Ud=opIQæ;|ó7Ï~ùߣ—’L³ +¸½“ãŽ÷ +ñeö¤>¬M_4–¯9wš@´ÑárxPdjû¨ ¢¢*åhõøûoþê΃«Û%¡ES«÷”÷Ö@«uÔP¹][¼jêr¡ÀÅç48w R;MÀ,RíR®§ tuŽô@S.’ Ð+möAçƒ/paô¼ñ Âñ;yºVfCB®Rð…òë•Ö&¾ú·s@ʺSBn0R( Úh!RüÙ?~bw/.*QŽh8£›#äL9‹ +Ò”£Ãæúe s”“DTFm F'ª9TÃå¦Ø£gZïº"7’q‘etiuHkPQ{ s¤æ1@¥Ú<ìÊ1..ÜñS³wM˜3H:p£¼·ïV ´tUý¥?yMÙÜèqájw~¶•[tFÙª’,²àÇá}ùÃé £N”[/ÀeWÄ€jS†Œ7ò‹õ‹ß:qÔMgg¤ ¶‡RãW‡¤Ò§´^Ž0 Yxg$KPqIDW†@Rú˜Ú+0Á§»Èv–ÙÊ󻈖@Í"ˆvÆ/À ëcgpƒK­ jªµSB—Ú(Rnº¢>Ø/?LT!lp©! +çÍ^.^þ¡sþ}}ý¹Ú¼HUÝOö„‰…ÖÃ$U"]à½uÚ˜¿V£CBËCÆÍ/~n4N@›øÛ¾é^|ûÃ?}ñ›züî·YÚ«€S{¸Úç¼E‰¯ƒfV£1Xsþ7úiÜÞ¯Ú) S¡Mš#€ˆ“ íVÔvñ£Ó/rñ)â8…¥H½[`}¾˜Ô°ê›Îò%a†òI½—"LF¸2 Œm-A¡†|[€emJ 7&µåÛhõ–Ò­ÂyÊ,0ñ§%6(²5L³p$úkà߇9z·@“J£âú,¤Ù}̾€%KâNšÂbެǕ&ônh%DG¬·È1N¢"pþTn77Ÿ‡ëw¤3¯(€luø¦5QµDVbí¸¶þº¾þàb+K%Ê2*5>._º ‘ºLp„*=°º€öŸí£‰›ÇÔTY¨ÄŸ- c„H-Ðrr4/‹ç üÉ+£÷D _N)«>º$Ö¤`æõϳ¤‚Yk‹µ5h­%ËÚvžÉ *°7?Ý.—hWôfœ=˳]DßEÔÝ¢„þ Cƒ2)±!èÊsÞš¶f°.{%q7ÃΣ Ž‹){PéËœQêH‰N²”ÏK©¶fsú ÏoŠ<âì‘Ù\›ÍÕn¦1Ò„eÝ-);% Õº\0烵Þ<,¾-§µ¯ôƱäŽi±–Fä¢Ð†ØEÄíÝŠ· éJþÂ<±ÇÏùÖyEŸ¤Áb”ãkò¤‚«1Á!í®`!€—!ËBr$[wmÒÀ­r V3G>X°èDà²qµ R¹‘IÄʶ¹x·zù·ÍóPe)²*5H½Ýâk}ŸsµM, ¨_ÂC èImLé`òPΤÆÞ¹©*Ýý +H<š¼tÏøà°Àhï³Î g ¶K"ÿhzëÏÞ ê0Ë„`QŸáµ¶BX»*ÕÄ`19ÿv}ûçl0Ï°^š¶8oÂù« 崾ѻµF¯ÄÚYž eÉ$""| _ÁË5!m)wƒ{¢=L!2.5ÀìŽßø‹¤») ÐO½Ä¸¬5(±n;£ËáªyðEëð}kó‘; tA%‘8œ¶sl² Ï …`†ðaª"ogèý‚]jŸ%°¼LÁ»Ëpþ"Ë +üCÒ.1V:¾¤GÙ+ÉRl‘°˜€ ׋¤KÚŠ¯|ˆ?CD]ô3{a¤³¦t½á­3z!ÕŽŠ´úv«Àì#%·òí³=,‹»Þàzýìûpþ¸j%Å!jõc fÐð|x.Þv¾+‰ª2Â*•~šòve·,cR³6¾œü|tõ›`õ>‰»;EðrÝJR«¢ùè¢Të\‚éIé³SR!,«RGŽÎAªÁR&ª:°Ï"§ÿˆW¿ëH2•P;j´¶;@(¶¾Û>“CHÕF‘Ø×:'˜3ÅÝ™1x2xü[kô2 ò£(ì¹eIÎÜh^’æ„ 70X\É_‚lŸªÂ¬êÐ+«w)Fkè ¦ªr_ +Ö”ÑÏsµ¢ØfÝU}ý>Z~·GU¥î˜ÔZ¢?¶ÚÑâ–¯óW“¯;§?gü @.dŠÛ» +g¯1kQ–´5¡À)&^²Â—9XýƒúúËù³?ˆ­ ˜­Ä&kM«GY-£w$ÔÏc‰*ñ’í•$K7 ¹&†3lRíD>kÍXc’ª[96‹™[Ë`À}$&Pï/sJûAŠüÙv‰Óã“üWê§qÙeg]d[iÔ“¼CpÙiÌz$ó¤ áQ{@"Ø+*RT•öX½EhmDéPáQãèç“ë½á3Á™C*IÁ¼{ô9ð&æ>@°¤$5΀ ’„‘€‰M¿{ /£É-Ìm¢ê‘Ӹäõ¨È¥( QûrûÚ?“;gàh0/ô2>€©ƒÈŽá÷DI¢Z‘ âï/t/µþÑ¿áœ)xy˜gXÎ]òþxsÎs6°j;K[¬?/FWß-_ÿ ß8Ý­è 'Rˆ(Y]ZkåI+…ªÀ5jëBo]IÁf7ÏìØøô é€5 íA Ӕƙ\ŸÇ×9ÈmÊY€}“k‡ˆÒ·ÅǼ¿¢õ.m´9»§wOÃå³åÓgOmŽŸ•åAŽ©Q蜦4q}"„'qeÏæIEï§Ûb…óÕheÄ×ëžTÍ ÉšiÍ31<>¥í~{ó">OBøpKiÎÁœ–¹z‰övsÂ9NçØhŸÊ³ŠÔÍ‘a‡àl(¬òa\ +Glhc!:ÄŒQ‘iìÓd?‚>ÌV\Yù“gZóŒm +×!m›Ó[Å›ÿÉg9˜(¥~` ŸŠÍkÚšæ 2“Tªj€äìiróT ø‡‚·!Ô‘ÆŸŒ$ÊJ…«ê¬KãNY¹ Lˆ1 +ÞA‹O#¯…‹7zýÒ‡„Ü­Š5»}Øß¼, j†öŠb ·gi&Ú«šY&Ê3­@äT‹UКÔWßËR¼ÅVI¨¨T–åVNQkl_u¯~ï,¿À$@z«Íóúâ½Ð¸Fõi–­§ãkWˆÆ%æíð)L#h9ÆC>‚WJbØ|B؃øË× 3¤Ó^¿ª/o¸`PÕ;Upì‰P;­Š!¥µR¸–De!¾<ÀØG´dÅÈ vºjTùøãTkpv{pö¥Ô<Aˆª}@ÀÕ áˆÑŠvf9&/ýªª+Ö]äl#Â{‚7’ÃçtAìQî²{ò ½w ½E¥vÓ8ŽjíëøPíœÌ®¿ž}c6X­‰ ÈûÚê fN ¡rt lç4VoÍîIÕ²U£ÌÔ!8![q{žaj,»%ÔÚz}Uà|ÚYÖŠŽûÇߘàj[‰²J Õ$uâ¯ñMÒX’ÖZ!H>ÙLjL›#Pï{³¢¼ù söÖŸ½Yò ͺÍsÍ}Ôʳa!ÞÐ ªÊ-è!œñµkÖ?„ã”9ŸTK{Fûj|õk{|Ë«<íïUTXˆ²–ÜHáÈkð2Z¼—[GÅxkôIô•9ÈqÁ."di_¬Ç_ªÂÔæ§IâQ–…¬°õø£F¡žáj˜1”[g˜5~T¾™ÁÝ æU¥VUnÂËqclöo+Úh«¬¤ ©ÇZç1øA¥¶}ÀWrVóv&Œ7OàîV œ— P²eÂscðº±þ9l`ÔŸ% ³~°zòitöõ§ r·bðá1¨e5®ë"¬Õ7Àé  @À<,H%i@óÆòËÑå¯K\ã“m4…èN¸5†° ˜“ck„³wE¨Ýîx“›W×:üVi_ïUýÿã?‘6þÿ»ÿ_µûܵv?»Öîr×Úý@îZ»È]k÷¹kí~ w­Ý䮵ûܵv?»Öîr×Úý@îZ»È]k÷¹kí~ w­Ý䮵ûܵv?»Öîr×Úý@îZ»È]k÷¹kí~ w­Ý䮵ûܵv?»Öîr×Úý@îZ»È]k÷¹kí~ w­Ý䮵ûܵv?»Öîr×Úý@îZ»È]k÷¹kí~ w­Ý䮵ûܵv?»Öîr×Úý@îZ»È]k÷¹kí~ w­Ý䮵ûܵv?»Öîr×ÚøûO¤y[ÿ‰´ÿ|_=™+“«ÉOÚÆOöEÁýhryµ¸ø‰÷“ýQQ¼¸RfW§'“‹§[¬ã0ÃÁ25o~NoŠ\PdÎßØ£·jû)ªrl”‰ëdÚYÂJãF+u”Å&ü)ÏÖóLm¿¢íãâ{ÂŒ‹ií•¥6ç¯Íþ³ÚìÕèüë¢ägh“qgJçÊ¿S{OP{ŽcB¬.~Ñ9zŸjIÌàƒÃpñÞ¾BÍ9jN \ f2Oy%&€ƒ¬ÃÅu/Ib+Çîäù¸¬Ô†Ù.«Ý<ß(ðÍ×,Š²ÜL~,„êã4׌Mãv·’Õ¸Y7+|X 4¦'5[5sð'Dƒ@*Ó5¿}a5O!®¶Óô~QÚβ’dQóER0feqPâ:Œ±â¬e¢¢åI·È„U©1Pä`ø-BŸˆÁa >Iã–¨BdRÚ@p¼5/ÐÞg)2QR³X¼…ù'û•í Y&Lîãê ÂF24t£Hû»Ea‘a2„¿S2Ò˜QZš˜½(ra +‘²˜± “ãJt¨ÖÏõæ•Z¿$õ‘Õ8}ùá'_þÉ£,ë-㲊“[Ü'QãcYÂݯªñfب )ÀûsΙTùˆà³Ó|0KàÆVI@…¦Ù»ÕºOªbáÜDIúXiŽˆíßÈqͪ>‚C£sÉX“i ÎØê_òµeUo£úÖ—´çrý¨ø…­K\€Ëm©v"4ÎQ}’§k€T0E%6È’ÖVÝ+KcnˆØ)°-T–ù*Å›¦'QáLmäX¯(6äÆYcó]ûøçÑúU^ôQ¥Ás6ZÒѪjNäÖumþÎé?î,_Y½ót\K'‚5`í­ÊÊ j,ø°uJÆES‹ÒnYÙ-«e!® +’ªhRä^Q*s5ˆä=ÌÜ%œ,ß.ŠCDóõ‹’ÒÚ)‹0Ϭ¿IbÖNIù,Ë'QV6ƒé9Ò˜‚ŸyÊÍ¢æ~^ØÉr»y`!‡Ù‚5¡´þV–… ‚Ǧ¨­ ó(K§0«,¶÷€0W¤±ýÁÝdp“Úq9¶V`êUyˆHÖÛDj}’¶ò,À 0NUjÄ@·3€x¸ûÉúÉ^5QŠ„Sû%¡›%‚îaòˆÕG€´ÚÚ)ò[Eu¯b#| Z”G6DX¿H{Ód +QÊLDªxBUèJ_‚ ½†%þ/·r”=ÔšÇ`yÀ=¡“cëE¾±_QùÓç‘š¡–‹‰E¢5nÞ8–…:*™…ÖNEݯjqÅ!g*…˸\†ÙG”vEë—Ä6ªô L˜®jz}Ο‹µ•ÚXqÑÕ{´;ãÂ5ü¬ó +M§}Î³í²°‡(%¦ÎØ‹ŠÔ- Œõj¤+ô‡iû¨™&BB›²îik÷‹\­*µ€°H£GÙcÂŒwÍ—ÛW\ý¨¤´•æ©Ò8¤œ±Ì0g`žN~﹕ÀͪÁ¸QäØ]Dáݵ3|etžÄÁÌQ\•o@öu»$%âêÖaŽò¾#ÖËrõ‚Ø+K#ÂX)­g rqùëSÆ?Ø«èÛEy§¨VÅ6ï +oçi·ÄÖgŠñœ[i:h)DÝ/Û9®@ù¾™¬Ûyq·¨$`ä“}l·(f)ŸÐG ÜÞ¯˜‰Š™¬'Â$ qQ©.„M’°á¯%¡…é3@u€ô8ð-HØÂxÈáv…­•èàAŠÚ/){yá³}Á¸b¤X–ðÊB—´Ö\xÁ…ç¬wD»+ÈJnòþ2Iú@Ö)Â/ñMÎ]1Þšt–Fólyù}mz› ŒªÜ‘ë§Îè¹3yåL^òÑZ6ï¾ú{±½Ú®ª@µ%±W’º´9åí©õc•RÑ`¹Ñ¸g”&|¸e0€¿ÙÊ7>IU÷J0/Z‹oï•õ è†`ÏikðiŽÙ)(Y²N9G|xÊùG¤9+òMˆ@P°4{n;KíØDU­ +-Bîr·D…iÔJU `–ý¢ÁLྂô('ì—õý’¡•#Üfn¹‡Yê§ÛÅGYf¯¨¤*@£.„k\žšIæ>È3;ˆ݃ e5®*b¦2Ô2L~³g¦©œ°“3U§B7Re@j‘ôeÈŸíW>KR@[qE 8`&®ëX±G¸É² +LTäê·¤5‡PÁ”A…í?JŸîU²(¼*Ü.  +ƒ¼„˽æôõn03ÍØ£$tgcvŸZ§ ®()p›FûÕú¹SÑ®4Dw +šÓݦ0‡5û”ÞÍå¯Ï œÑK½sB:‹Yb¸‚UƔĶš½ËæòÕó¯ÿUçämAŒkDûkÚ?Ë@ÚS.ÜTµ!è7¢ î§Á¨/BŸ2 ðãÒ[ÃõíÍû$¤É°"÷hsÌ:3 +Á™ë“hø„÷æ96@•ªÄ¥;ÕÖ•=zÎ+µyüä‹¿6ç‹"H *d“3ň +‘ ¥ø}·$I'.ÕbMçQ@¡S•û l`!v‹ünK”u ˜2Ø´»7qE÷<qR`Þq¹`jeˆƒêæ;92|”ç¤ñ<“(‹̨ + Ä€Ò'å£Ord•ÃAUºÛyiÑ!ÀRv ÒnQÊÀc¢ªoçÙ‡ ,‹Ù{åašÛÊ  H}7›½ŠWååiÂ>£r¼„¼¼"4p¹›¨û%yP¨ Aÿ3¨¢k· &Š2°ägI èQPŽÿ„>¤QâÚ@O‚¿.0>È{DlU$˜ÛN‰ %b:‰°Qræ9ÈÁŠÔúX½ÜÝ)p±%ä°´µ²¯‚ùB°!ä«6û˵ù3 ?LëãzŸu'ZýÀ\³þäoY+®ÿ¦7Δú¥R¿âƒS¡v^Q{h”Ôö†W \5^•ºÞèecõyûø«æñ—d0³Y_¼‘[ç)&Ê ÍŠÒSšçFï)˜5ÚYB—{Ww$\€>Tî(S£ó¸¹üyši•åa T„Ü"´>a@®9oY‘»Àž0±¤1Ù_jó¬yð^j õC{x]àk»ˆVš~àDv*r +·@-Ú(.Z[9ž›zý°,6¶ +p"v…mœG½m˜IxÑ èç¨"õñ ~‚ +·d¹­‚”Ämx ¸›ÅƒQß-ŵƒ@ùleñ ~`­Q¡Å›S»uIéÓæ&€ûpá[×ÕZÀmùÛyH3÷QýÂ[¤­ÄD‰²‘BŒth¬1˜Äz2~ŽkCHp˜vˆΙj¬B3¸U[œ³€9ÌSþnAH”•<áïæ%ˆ +À1u»y0Ox€f3 oQඓ,•àŠtࢡCFî”åiòA{”¤2˜tLC Ý­¢“–§¬‡©êŸ>Ì–â +QMÈ\üÑÖ”¶§e©0¯jã§bm ¬Çy«ª>(I¨eÞíá*tÛŽÏ4ÏË\’—i$÷òlcó +\ƒÐ‡jtÈxÓ=TAĦu9oNZ#Ò™–Ô^Aj)ͳöá×rëìUÕWÔ>ë¯ÄúÓîØmx~št º B²”K; ¡vAyGe}ZÑg ·÷ªzHs„H 0Ë uH‹lS N¡žªª´5ª‚ƒê%¹]RºàyÁ±ŠþÆl_¥)'E˜9ÆË1>Ü`íäí’ E«}-:ÈA¦X8~Ué€éNÕdQ.¬ÎÃ$–D”ªØc½Ú: ”a•kïÅYLƒŒy¡>Má?KTSUðí,¼ÐlÙ) +àg!À@`ÀœSúÌHuR¨ÆP‡t’ñY¥“'¬Ÿí•·²4„\‘\ +4è¿H¬>ÈTÕŸn¶³ô^”v•» €p9{Á9Ë a‡‚fŽëÄr `|uð€É²o ô0…š@AÈmç„îÈ0ÆÉऽ_fèm°yÀ-·ÀíÓƒ³odé?Ý.meè +SçA-[Ý¥Ç)iTªüéÃÄv² +°æ7œóŒ VkÁéœrÖTtæ¬3ÉPqÊ0쬶w¾ÀÚ¨1F—±Gjëœõ— °\²\z>ÏEU¥¯6εæ)eŽöQm§,ù°(D)ÊGt‘=z-Aº‹<*õc¹yGíæTï=wç_еã¸f,Ÿm(ˆµ} œ¥Ð¸fë×4DŽÒËP^ +·-ÅÚá^¶F»Ä€…lîÅ2[ãÜ% ÓVž!ͱÚ}Ì×N ;Ê`—ˆƒu¦Œ¿J΃<Ñ èÙs…Jí2_‡µÅ ÿ \ü^ ìv3+Lù+«‘D5˜ÒdR»Vb9"L{¦cáMØÜüc=º­‚Áv‚êa†Ý)ð™®–¯Dy±Ã’ÛÀʼn² Æ·Àç 3ª;9Š–k¼=¦Þ+k»%õa†¡…0µ$"ÿÙv¶D:˜ÐHƒµ¯šÛ%)UQ eÀûGIÌü4ï–%LjæÄ…@)ïA’ÜÊrûeõ_ìä¤Ñ½’†·Ì„‰¢ø(I@RìårŒ65É[ð>ʱŸ&ñŸî!1~â&"Ô(£¿Ÿë³ ¥ž(s’¥ûù‡‰rº¢’JOÜ l™“í~\ÙtÕc½'ÔöÊB20©¾_UßaøiÌ€îÁ/Œ5Aµnº*³î°,Õ³¤ ²ƒÈû¨"Íê?cÜ%Â5”æa’Ôa“ոll²ª"|¨6Ž¤hÃ9c­wV5ú¥‹›“<×LQ~oðÌ?/ + Lé€.g‡;‹,WKR°¤Ú¼ÔzW˜ÚFú‹aí"úVIËË¡ÕO@hògíQ·¶òÒ^YC”#<5:¶»×˜ÞË¡º7¸¤ƒñ6Âm#ò.ªÁAJB½>{­¿®ZS°Wjã°È)„ÇÿÉÒ[x9ržy£ÄBâ¡îi3•JÅ\’JR‰™±¥V3ó0xÈòØcf{MY;Þ˜âÄ!otìÄœÄqÜlöû¾Ý{Î=÷>š{ÏÑé3Ý#xß÷ù=?¨·ªD ”v2釔'‡ ÞI+aõ)µ®CP,ƒXÎä¿'ŽX¼4z¤wônÐ5qÔÄšÁÖ¦ÜtjÒÎÀjH5¨àÀ =¾&aÒ#êÑÄ^(â ‚‘³PðÓŽ…´vÖ‰)r´¦õP +0 5&WPçð‡µ5¹¥q3eñªz·bDBV,¹ú8ÙNE&ì,xžQ ø3s@PzWüÿa#:nÁn-ùnߨýà­/:eŽ"µ•ã±Ð¾Q먗ö~Ô€Ž9Dˆ‡áì\8×;8nš48]>Ñ…)f/°" EÒ»e;QKKLµaQ›/¢wA9ü#fÌìIœç°ž˜°F÷ÿÿMæ.=øIÆMŸÚ¯ñ6‡¡¿‹Ï ë`x(µ¹zÇ·R4d|Åz …+‘t¿¾r –[˜I§ÐA¥™h?ݽ¬ìXèk¢Ó:,¤A`ü\b‘ˆLù‹›lfAR[s[W™dç+Î +Žr@¶\*R^×a1°Á`$ ݃3'ƒ 2Ô%uá±LgÏΫ_5ÀÍd (Ô-dÒí£Éî)$X¥ý¥|woÜÅ|uÔ´Ün'¢0Ð6ÔÁ‚6±ûÆ­C:ϸ7ÞÊÅ€v ˆbiµ¡AÀeùàqHNL²áIŸX‡¨iì(¯ŽÙoµ˜p‚Žqº­u29Ô¡ò° dtÀ' i :Ýè–)AŒ7†­ä­ÃbˆKÝßtmbt6Þ‰Gé@àáÀÂ^ðH4zÜB„lðÁHaÑNEhÜ }Ð4Æ <Ø9ðQN\ÁÄüã”Ò€pxçQ3jaÁ§•ú§FíÔ¨Îc÷øG¾ƒ“PÏž:‚&,&&ûT¨2<é°¹Y&ÌI÷¨™qPqÜ_†Øî¹õí¸ƒhl¡Áa‚oŸï¤b¨˜J?l À.BlxtÓGu +\½“MÉ›½~#"AÏjÜ’…P!ù’2€gÜÍ9P̼“NBf±Sq 1cŠš™žÞ¹‹ˆ4Ç!æ£a2¸T¡"9¿ä’œ3ø>a~ØÉ8™¸œêØŒøàK]b!S[?qåÙx}íÞç¦À롶Ð.®FFfp¥ÃeWŒlÞè‰r }<†/Ÿ .¼­¶'ÝüA­XÈÉ‚×ô^ÑJG-tÌJªÉÜÜéËOë¼â¾q›õó±¦PǬÌ!=~ëHiŒQß. Zcòú nÁŒ*‡ÁßôNæ”úüñût¾àjBClÍÞ°€üMÚ^mtWïL4¶¾2jÛ?fÑ{L06h[IЄEŸ&<¾|Ô„C‚¤vXç$˜ÜB*³¡º›O[(PLàÉýd Ä%£tÒK' ±O°a!1Rõ<ÆiÄ:i!M."ÂçÄDŒ1´ù¨3afzÌH@N„´ŠFˆ)RÎ fªqwŽp˜‘Ö-Û¸Q µoÒî&”\®þÇšƒãV•¶Y'–†ç k­…‡è:éÒ"&| r Û´ ‹CKî›t@±@GÃ@‰Ãú€G@—M„Å° AŸõ'gÑPÃJ'Œ¨b,,32c!•‡ÏbÁŠG€°–7ã*£¶¹Ô´ƒI8è„›ÏØ(»$©;…Ìa»0bá9!΃ڂhŽÚ(;óðiXg_¨a$cv6¥EäÃu"FEÚÁÒšXÚÀãÓf6=Ạ¸±Ñv6 FÝÎä³<E7z•q3yht3Ù9­•µ¡!)Ú·B-d#¼­GšÄ:°­ñmdºÉŠ†ö˜F´¨Ùå×;_†<ÖQŽc94©1:XŒJØèÒ¢ð›äl|Ä€÷ÌÐP~ÿ¸mXçÒ ‚³ˆQ|BiÂÊ)’+˜‡5ÞÁ§»üLzFŒÄáÁ‘U˜gô„]T¼ŠÖ!›™1@+¤uÖC:–Ç Š‚Öj^!§÷€w úä@àj5`I ç+Áò—]¶Q'êÐzDÀ0ØÈ[ ¤GUPíiC!ìä4nÁÃ%Q>ã•*|j‘ŽÍ˜©Ä™²sY—>„¾ÆbKLnÏÁé½a ™´20ëȘ“Ããf²JaOYˆ”B´•†%…±štq£17_qóeó­D0Ù[œ±§6‘뀻Òf_ÈPç4£cÒ¸X†±‚*m+•6 vŠ!m©Nvp(f)ÌIe=RÍF¤ìDúôTcÁG à^Œ‡]žøIX;lÑZ©áAM}‡uèÆ;b¢5ÅL¤LAïé¼1;G¸‚Þ-€ŽÀZ‡Ø qÛF¥õˆâ “d°¥«¯óZÜ~³ƒŸ4Z+ça +&_Ìäè\òð0$P„[‰”‹ËÓJ‹”+cF\oÇä?ÿaÿä¾1p¨ó¾1ˆÝ¡[[–f$tØHé k˜È}ãnÈ&cnÌ"h좠6Q! Ýgó€QÁ>'<¬óB&m Èi¿:·Üu`Ü5¢Çnwþóˆ ,™Æ.Œ[Y(Á¨‘sÀâ0»I;1fb4VÑA&&l,è,a“§Cm³O… LJNõL¸28ð/”šTtAÎnùB(=tºïy`H6`]Úͤ0¹Œšn6&ÄD!TÚŸœól*’‘n¤~ŒK-šÈ¤?BíyäÚ¸×8E&½†ÇæÍLŠe£R„:M'Šv"‚Š¶ÁþZC†Ælœ CBD„"ÈüžUµž°²› èN6£ñK.6 dk£!±F‡ì¸ªÑ—йý#ŸÆÌø¸¬ ¿iœ×_öȽwÜ +ÎJ‡R§µæ䌃ÝÀ•HB6„d:f†¤¢«90aƒà?æF`Í4,~p¢ ²§W9 EöAŽ0âCfbÂ90TãÀ¬ZÉ”K(ŽÚ!,¸Mˆht 6"†môÖF<äS!Ös‹ÙÛôˆÖDå*æÿ']~hg­+¨÷„€ç-ÞE„€+&œ² K R WZ¡Ò•š¶5P¹Yû€ÆsÛ¤s¿™pð!hOØ% –€Ÿ qŒ™pk<Ê%¦S­µº +qÕ@ð!6äh¡n,‚àQLÈ‚E°0:DÑ8û€L.áR²ÿßÁ¨Q3e#&_1êà$`Àq ˜ +VŒ +”ÅTØH€3­¿a5nÓ!A-ÑÌDÖã_¶|uØ<¢óé]ÌŽIÝ’ÖÆxÙÔÙ?nÑ#ØÙI ­±0pÚ&bÒ +¾Ëñ•Cú1#ÔÙ'ªzùÉ 3ƒÐIŸ¼¥Îƒp +qprP¾ÁI5: ÔøWi÷¶D]¾ Ã§‘°‚$¢âþêA¶_絋:ïàP̭힬P¤b˜ÿ<¨©5:½ÒúÆLÔ˜ ªLì·C÷Y Õâ CÔ…®Óhô©`ª!diiàFè¤ –Ý3 ,0Þàîäf¤H'n6ÃËñȈƒéw.ð.q؈CÒg#M1=ËÄzF,¦q Æ!~j”  ª_¢}›uòÃ&JNι9èkš}lpd „¿ AœqRQ-€Øhgs`Ý-Dd‚䘃Ÿpˆðëa0&èqÈ„ é½Ãàm"¡¡ê8i¥ÀdB#˜ñˆƒÍPÑ6j؈Œ›/ËÝ6fÒyÌHÀáX@èöƒÖyU°Ä„„!¼#\–Å‚'=\ÑM¦‘ÄÁA‡‚©5#bÌÌAk@WR½Dcó60·XFu®À1ñ£AçÄʇ´¾a zhÒ;¬EÇŒäÁIï dÌHë"Êg±`¹ƒ³éÈýcöÃ:ðH`Œa©ùQ3yP‹Œ›‰1½Œúþ3¸8Èò€U] +$æ¤òO‡Æ¿rP3 ÃÓ¡#zbDOB;YX0WѾrH3¬qC4l £V_¸Ë`Õ€Æ÷i|#ÖÁ¹Uc·²Ã!wr°÷bƒ )+ìç@#;·oÄ68µ àí’çw¹·óúKN6 ¶ÁŒEltñW¹d„ž• x¤á ÕÆl<<@b€IìTƒÝ(+?è}x•ÞGžôHpò¾0ü¯Ï_q2ƒíK/—&B)aþŠÉº¹¬›McrÑ‚!-r±®‡ÏALsÒ©;wØž9œmC„ŒÑ#š}Áa+=è« q‡t• `ÛÀØ̸ۘÓÀ@2PàäÝð&TØÛ +ÞžN#rÙBÇ¿:áØ?a‡lbÁ¢N&ã`²2m£²Àc6þö”7V;«õùÛN¶¼3ÍJ<ƒ>d HÐ2Ø`M—6!ÐD*´Õ¤•†8fðFQ±FÈ5ˆÀêbP,„q«ád’D°‚)µÛ´D~!pÚa-ªw ž¨Î5“ƒSM4®€Î FíwïsÝzjR®±¢a”Mëlä¨ÞgpÁÒ à‡ ž0°±ƒŒé¼ÿ°oü+‡tÃÄ:eªï¤óZ'ø:ãЄÍåSÀÝÍ“ieà¬ìÂ`ÇJë)öu(ä}½kpþ*ðÒ­I…à1î”%þá á«‡,ðV^‡Dn •‡ tS ›/°àiyÓÓ1úb¾`OÍÐÑi°É®Sȹ¥’7X‡¿³‰i!»(Ö,LÎHÄÍDLï Aâ¤62¡÷Eì,t®¢~s +ã\Ú¨8ù¸b¨¯/‚Õ@ø4,ÓjÛ#–±ä¤ÓàŸñ`-]ßSKë„R5 ²X¸ÈBƽrL2d1—Eäü™‚Þ‡Tn&€y_¨‰* hœÓ?jƒÇùxļIiÁçI*:-ç–íB >qkCxoÈÌ!å¡JoÜ!„æ5áoÀ§ÔP0—µIÀf<îâ ¨¿<>U€Š!Ýh¢ Úˆ$hÁ܃s®‚Äï¡Sò62c§³“ž S!~ê0X}ÀϘupŽ4; ËÁ¤Í0`wHç‰|v"jÀ8hðÅ&\ +4l€kÕ8`HÆíÜ°a°‘=¤÷´xÂê‹y e¨ä˜Á7 § t ¶–ˆ”O@®3z?¬õ€ªêœí`‰;ªxÈ((Ž™± ‰p).>¸uB)4f¦¬ì:©˜”…l5>Ç‚ +:¹¢ÉéaÊXxHëѯº÷»Æ¬¬npö¬âb3L´Ç'zB¢‡jX§0icGÌŒ Aªõø«>¥®.³é¤!6ÑõkN€D¤!ç—"­#|~Ũڨ˜õ É®G.K8YpIL©¡šGÎÓ1Xù®TùL V~ðYljF)­úóót¼ë‹6ǪS`ìø§5Ee\©DÊ«Þ@É€«66çö7ÐpW{^¥eãò wX°*ff¬tŒKÏ ¹U·¿j¦’¹â  +y,T3Qª™TõXÈÆÄái˜R¦¢.5GÇû€UWp(+¡ó†-T˜jÌ!N¸Dˆ‡26ˆ ‡&Åçá==BÁA'A¯uˆ¤uq. |n&¢0qÀ€‡Ïs±)Zm'tqYT0’ñqD™@à T¬K¨-+“< !¤Áowy‡XÄ#M:>å’ +Nˆ7fg¢&×퇗X‰„W(²¡„¸Z´@Nï‚‚CZD…<mJ¹i.Ù¦cÕäÔ›èA¢×£ª[,y¤šƒƒŒ“—ÔškÙ¹´ û”*àgØÌÂ,`: j&/¤ ZïæéHÉŒÝl •ón¡9:šOcÁ&üÅÅÅ› äÖ ùîAêBn 7˜ä´˜Å"5<\Bä ©VÃõµPs“ˆ6ñP…•²½#x¸n£@PX°#¤bÀ_¸ê ”ÃÅÅdgf"€F>Õ\Éùy©°ä ¶Ë+•TØF†M¸Šú+\²Ë&¦ÈhÛD'Ó 7=rÁ-å61µËd–¸ì¢˜[¦}2XÉöNr™…I4ä‹BfIÈ.ÓÉ9*>í²V*jð)ÐF 80ˆð)¥¸¨Ö7…E¥¸ œ¬ÃT¨¨ÂdÁLBâ°SqK8:;oÄTðÀ6V"êbÓ¨T’³x¸ø T.1…È9ò*¾`‡ŒMÁBK+RnžMuíRnØ.|ªW„ç×ÉX~šð<“ON§§ŽÊ˾hKÌ-Hy˜ÎíËËR¶èòJ©Pi&ÚX“²}¦ÚéÔà0—†°L…ª622n‡„ÂCLØá¶OiÀ§Ó±Îàò. ø4ªIÓj'Ñ܉5wÜþ +P t7$ô1§G$¥Õf¨±¦¶¶s³'Ú;Wç•‘@ÉÉ&Ý @¯¿ÂǦR¥²ND§@t´ð™¢Ír pGn¨ (ef”ò’”¡#="ƒR Á°½ypŠZŽ‰wùÌ´ð0ØÝŽ9XˆoBm`Ñïñ™ÙH}=ÖÞ¢¢J-I¹Ÿæ²ÓRêØ’ s$`)TõKV:î–²ÞÀÀ’ ™Ùpi­ºp¾ºtN)-2‰.ª£Á2oq‰N°².·¢íÓtbÚFE|4]XŽŽ6AŽ¹ì‚'ÔtÁÚŸ¢" S%˜5›Y }<ÚÃ#SÁìLuñ•èŽyd(¨ŸžÇ#íÁj*ƒ“–å"p£W´nÎI‡™hO6ý…Ùp}CÊ/xC5ÒÒA¦’.n Ù6 þÊè #Rq`#ÉÄ-²P½?¿©o+µu¥²X.4P€²óO HÅ;T|*\ÛtA±ÈhSãmxËÁ)@×\b˜6XZNtŽ¶6ïR[;x´!a̳±ÖNfþLváL¸±Î&:rºSž?ZY<•êî:ù ê/ájË®‘ñï`¢šŸ·S +'#M"ÒÂBM9¿˜î‹¶wÄt?ÕÞ2]½T5ÁÄ:Ji9ÜÜ·v<Š‰ˆññ)+³âN6&¦§Ùd'\Û(.]hîÞÖ·"•åÞÖl¼kð%©ø“œcPÄõììíb~ÅƤGÌ$p¦‹T©p%PZä³srq%;{&Ú9Øög§s­uØòÁDa;Á‚->»¬ï%º§éxO-¯¸„,:·ŠP*;éÞ™ÞÞý+§Ÿì®_ìm_s}:ÞŠ4ÖÃÍ !?«6Ö*K[;7ļKÊ:ù¤WÎð©.—ê'§Žf¦Ë…ùé½»33Ç]JÑ%ç¸D[ÎNGš[|i]®m“™Y*Ùƒ_­ThÒÅÀkXâÓ§c3g£ýÓ±éÓÒjeþt Ðgâµ`u•Ï.p™y1·­¯çæNE› :ÏocãÐD”ÚsóÑÖ‘d÷$âçec5+D¤¥Ö”Êj|j/ÖÙËöNTÎ!RÞPÛÀ·L¼'e‰ö±Tï´RÙöú«N8T^ÎÍ+µ5>;Ë$»\ºÕ‹TW¢ "ÒˆTWS;þò’Gi +¹Ùl7ÖX%Ã¥XgKÌL©Õ…ÒÂ)Xü@q6ÓÛ9ß‹jmÖ¡¾|¾½{#¿t!>u$TY!Õ)ð·ÙÖÞÒ‰‡âõ) ñZ® s9ßܼXZ½Ôܹš”Y‚ ;j% DHÅÊÚ]¥¥ËáæÑpçX¸¾(Ì¥k¹©u0`!SML΢R^%c&\*tw"å%fÒsúŽX(o°´¨AeGpó c¡Ùy M[zæl²äÒŸ™ƒÅñ¦í•hs=T]ViÏî=öâÛ°>V:ÌM7Öî†G}í/P[-NmÝ|ðkw=ÿ=DÎÖVεwï)¯]þ­o\kl^³óÇÎ=xõ‰×¤BoÌÅS*tÄJbêHmíâ܉ûòs§:K§~ödo0Ÿ>šèì${ÇfŽÜ³{ç3‹çžWÊÛ­™£ÝÕóD¸JÇZ,fÔÆNiéÒô‰‡Ï?“Ÿ?·°}±»r +• jþâ—êñéiµº2süÁþéGÃÕõ¸…XËëÏÓÑ©6aÅì|ÑÆÂåÍÂüV0-¤¡ˆ+ÀW\zZÈôJýã—î{±¾t ò‹—Ó3çbí½úæUè&·ÎÐ'Cùeèt' ågÔúêÔöåþÑ•• x¬ë TËÝãçîù:¡ä#¥™òüéÒÂy:ÕgÆk‘ú¦œŸ æý¹£OvA-’SÑÆ6EgãÎÝËO—çÏ,n]\ؽƒŠ”áå‰Î`¾¾vióÂ#x1Q_›_?{çƒ_Ø€—p‰9 à®cí3¹…+ù…KþLqëö½³7ÂÙ¦‡2‰)&³(ÖB•ÍÖöýòºIˆ™~¨¹ øgcm°=ryÖ-V_;sÏs‹Ç¯™É0ð'"•âÝ“jç(Ÿ™¯ÎFÚ;§ï¿|ÿ‹r¢K†jj{;ÔÚU;Ç«ë×ÀY€ÌyPóðñ¶?¿é-._JôoT·½|®>sJHÔL8G„²ÑÆz®w´4s¤Øßj,·?áf¸X%Ý9–ž:kî‚ŠqéN¶³¹sî¡™ã7\\"5µªo–.F»ÇÄÂ’ê9{ß©»žâSÍQ'ÇƧýÅE!ÛWjËÅųñöÖôêÙ—¿õÓÊÂ)N­ö·.¯ž²yäfïÈ=Ëginßdc½cgî=yçÓ nà7àCSSGã­t÷Hyåbvî “èlŸ¾§¿y â­­äô±`eM©¬e§Oö=XZ½æÏ/ugŽ•†Í880x„Úñ†ÚHBÜL¼±+MçÛën!É%Z$Üd—ŽwB¥•Òâíà<Á^æ—.‚Pú9.ÕABõ1TÕ \Rk;&"l§"ᬘš*/œ¨¯ž 56¼¡&"–²í37^à“õxc¹¹qG~î¬TZab 6Þtû‹H°oîó &ÂDËBº+A×dfäL/^_çîâÉÞêY2\)ô4Ö/ÖV.T—oo,Ÿ.÷w‚©Îž{óf\3`a˜Ã•Z¸º©¶NDšG¥ü’WÈž»òØS/~'Uóù Ù™³©™sÙ¹ åÕ»ª÷°)øc©³z1ÑÚÒzd=„„˜è“oWo¿ðÈë Ý\/ÓÞ0c!o°®Uªà+RíÝåã7çv®Ò¡J¡»o®"¼KÈãJKÎ-Æ[GÍ]SíPnÖ-¤ " ¡\v‰JÌàÁJ0=m#ƒZ7…R‘Ê\uñÔì±{bÝ]O°4î&œ¬‹S¡Äd[­nF;Çrs'[›—çÏ<(ð`!3ˆº5оùStvn¤zÇRüô1);m¡#FLáÓ}·˜öˆ)¥² 0Í­­Ü^^<Á¦Zn>ȶÕÚr¢»—›;›êŸ¢Ó³ˆ g»@  S2}ÒŸw‘9ÑÉÏL÷ÉEP w˜’—ç…ì4¡BpkQ±–KH0ÅAª;§îs³‡ >“$Ã5ð9(øÅ)ä@âCù^njˈùÓíÍÞÞ½ù…;ýÅ­úÒåÌüY&ÝËvvŽ\{UJš[Wv .úP»ry'µp=XÞÐ"2"¦R­ ÈPP©ÒÒùÚÚåÙÝ{g¶o¤;ÇÕú.oœºYìïê}~°£Àr§„ìªTXMtOJ…,^;z9U]D„,¥Âª®É…6Ù—ó+‰Î±Pv¦³z!¡ò–Ióòv&&D›ÙöF¡»ús.(e'#ÐwÀF›áÒR´¸NOÃZUgO&{ÇÙô dUÄ_µqH4L´ÛٸƥºRÅBu.5]œ;[˜; ÒìHÙñÊÒ‘‹øø8 ):u\*-ƒáïžx°³u9V[÷Š¹“—ë,Ÿ4 iRέá +ÄϘ˜p…Þ®QGš;‰îq:ÑÕ{ƒ&A)%%ÓžtRfŸ×1µ™ @F›bâ­‹sQA9UóºþBì Ä"65­ÖÖ¹^-+ýÝ+¡Ú2©â‘k‹éY.9ç/­¡LÎdc=Û?žíËÌžÄâmŸ-µ²h£•X±·zæáÅ3¥û§¹D×É&`HÊPH(^eÔˆ],áÏrÓra†ÏõåE‡”òsÕ¥óPML)E*+…¹s¥Å³é©m!ÑðùSzTž´sFÄ?nõM:€hiöTfúTnṺ.æ¬d7Óí=9»äñW¡F(d1Q˜ÞÍÏ3’!+fêN&†© Dí@BÇ‚e.Þ’S<”éílÝPk[`W*³'”ÒŠ™TÑúÖ©{BÙ¶•µØÔÉâÒåÆêu9¿Œ†š.)Ç«µÝS7c…'K46:kw¦O¨µM Dðù‘ü\}î t„’éïÝ[^º*®Š‰i1ÕÇCu˜i\Y(¯qÒ¨˜VŠ þÜ\eît{ó*jÀr+¹k÷¿à·­„ê ”˜h“‰‚ïÝ(/\è¬_5{ý‰âLgóN9öŒXZqIi&’‰5ç¤B[Ê7|jÖ‡½¡¤\(—W7cý·’ôH .9åEóIÃ:»áÀZ€ØgOQ‘4ÂJ¤’³ 4cb9!S#Ô|°Ðn.mwwNŠÅ–ˆ©žœópYƒGšẗֺ«,‡‹`ròv2 öLmn ù"ÞqËyÐe· •ÚT4c À"…LïˆW*híìˆÆéB%’ObÊMʬ÷J6*ÂAÖ..BRáp³êêÉ«3G.Jùæˆ Óa².(7ï2‡õžQƒWc£œ´êá’;}ëŠ?nó‰~¸¼ÈÎ3‘ÊêÉk³»ç휘pSV&ÈeÚx¬j$eŸ?žh­CXÆC€´T©Rœ9ß\¹"¥»À&l¸4 Ñ€Vkl´ÁÇêÐ×éÞfzz3k©å™hu-^ßRò‹“fò+CÆýãLÊ“Sf{hÌ<¢õ˜\2näZÛÉÚ&î¯LZé1£ÏE‡Épi¿Þ}@ï6¡X°«SK›¤\6ºe!ÞäÂÙL­;·q|eïܱK÷\}ø™ûžýú3¯}ë­þôÃO~÷§¿þýÓ/þúÝŸ|xáæ3åù£”Z\¢nç1.™.LªÓ¥ÖBµ¿Ñ^ØXÜ9yäüÕëúõï¾ñ½Ÿ¾þpsaO-ô,ˆdñìhÈËÄc…Ù\kÓËŵš f »¥ÙÍT{I­Ï&;K+'¯Ÿ½ûñ«?ûÒ7¿ùágÏßÿ/~^H7͘bóM.,® Ù#U/ åZ±ÊT®Ù+÷×zk§·ÏÝsï“/=ðô×¾óÎ/Þûø·¯}ÿ§'®>Ø[=m,“Á"ØE«/`DDÜŸ¡”¢ =Â&ÕâB¾¿­ÎªÕéÝ 7ù××xþß.=ðøÕG^¸xó‰sw?zí±^{ûÝ'^|íÎ{ݹt.ZPÉŒð˜m%7ÏG±âL Q‹æ{åÞÆÊñ;º‹+»gïù[?|ûßö×ÿøû+ßûùÃ/¼¹}á>0c™æjmþH¦³,'ËérsneíÌÅ‹÷>øÐÓ/¼øÂ+¯çŸ~øëßÿ½^û÷Ÿ¿ýîG_þñ¯ÿ×ÿýÿü@þù—?øù¯®?þµêÂ%×ÂD™@$Q(7§·öà±~ôÔ…ë÷=öü×_zã[O㛿üÍßüîßûðW}òÅ_üïÿóßþú‹'¾öÚÉ;HÕæy¥FŠÅPf.ÕÚqÑ17æªùZ{~ãè™;î~ðæÏßü——î~êÅW¾óã½÷ñO~ùÑÿò—?þõ¿~ùéŸþö÷ßøÖ;«ÇïsÈ„êdŸUƒ…Ù`vªÖ[ß8¶}êâõ}àé}òëßüú›oÿøýOÞûä·?ÿè7¿úìó?üéÏÿõ¿þºõG¿øìÅ×~˜¨/ëÜü„•æÔ»B¥¬«§«ý•½ÓWzâÚcOýË7Þøñ¯>y÷ã_ëGï¾úýw>üõçÿöw?ÿàãßÿáOÿó?ÿóéo>áµ·NÞq3^™Æ¥¨Éé¤@,)4—º »;'î¸|óñ›?÷Ê[?øé¯>ùÁÏÞóŸýú÷úËþ×Ç¿ùü£O?ýÿüÏŸ}øé]þÌÅ»¤¦(¥A2¡©ÞòÊƱ§.\ºråú=7~ì‘·¾óÖï~÷»/ÿô§>ýì½_¾÷òë¯\¼zmqs7Yí*ù¾›ŽÛ}‚W!¬„cåJc{ïäÕ»¾çá'ï}ô©gÿõåýüýþìý׿óƒïÿð'_þé?`:_ýÛ/¿ñÖÙkÍlž+÷·„HIÍ´3Ùde*^hÌ,n¯nímíî\»råÛßþη¿÷ýïÿð?ýì‹?ÿ ÊñîŸ~öëßüõoÿñÙ_¼õ£_øá™Íã¡ü§–ØpNÆ‹µ©™åí£g/ï>¿wüôÕ÷¼öæ[¿üàÃ>ýí;ïüÖüþGüæóÏ?ùìÓÏ>ûøÃ?zåÍïž¿ö`±·†²1”ûíHi 3”Ä +•Zgsg硇{Æñ£Ÿ¿ôÍ·~úÞ¯¾üÓ_þò·¿ÿòã¿üò‹_|ðÁ˯¾ú⫯ož¹*¥šŸ²b %× çÚ¹õþâúÞ©s=ýÜ×^~õÅ×Þü·o~û§ïýò¯ÿ_¿ùòÏï~ðñüÃ?|ù姟ñÊ[o?øøs•Þ¶ƒRõNÎ`g-ˆì$Â@Åöj®Ößøõû}üÉg?÷òK'/]è,.úã)^öAt%ü.*Àúã»Ç.¿pO©»Xhvûý™µÕ¥Ó§=ÿü“oÿðí?þô³ßþæí½ýÍ7_õ•—Ÿ{æÑï¿kgïX­»Eóò^.j#”qä\ç ¬nZC¹lz{cóþw½þꫯ¿õÖë¯ýç?çüý‹ÿö¯w\<½·³Þ˜êfëm%è&Dáá¦C.:äãB+E¹J£·²²yñö3>þÈó/<ûío½þËÞÿü‹Ïÿôç?¾÷‹>ûì“.Ÿ+5ªnœw“a0Ãz{pÌ2ªCÆ ¨—PÔX5+NÏ--¬­9sûíw\¾óêõëwÝøÆË/~ÿí¼úúk_éÅžÿ—×^ýÆ7ï9~ôxºÐDù°ÙÇÙ0 a"„œJ–ç±’¢‰ÜÆöñ+7|î…¯?þä¿\¹vÏ“O>ó£ÿì{?øÞ7¯<ýÐ}O=úÈÙóç6¶6[S³Ér¿:2îÝ:š]¶¡’ÉN¸½\:WÛÚ:rõÚõo¼þx¥§ž}öÑÇ}éÅßùÉ{Ïíß®^½{uc§Pk‘¼qÆâ ìŒÁêµ#ŒËÇyIQŽ•ã¥^½·2½¼ë“6œr’¼ÅÇ›êèåT£‹98bЙP7ðÒa§OòQ +N* >6I•ƒ±¼Áí^Ÿ%Þ‹ +­–L„2a'Lnaã.&¢qCz«ÁŽ!„ß°n\âÔ=jƾ:fÒ¹Àë:ˆðà‚6‚Š1ƒ‡Ó»›OÖÚp“›4¸H/ˆɸ”%ƒ7’Â:;¤1L8ìh3&Ž*rv •ŠcfŠJ6Tœ0z'tN“Ñz“Ó‰0µÌHQ”’1&jF3 £Eëáƒd„Iؼ²ÖŒ]¼Î%éÜ~ 5xƒNJEùxº:³¾w>–®hõV2H'Ã(Eï0i3¢Ó{ÿù fDçñÒQà.½ƒ³ø‚V<ãä³}'7"R¼¯­ËÏ£5©ØÞîf;ý=œ³Â[¹¥²Wi+£-2aA ÒØY!1-eÕú^¢¹[œ¿ eæ',8Jˆ™R߉ö &w´ʱ©ÖâùRÿŒWÈM¸&tîLqÎMELº&¬”Á«Šj+^YSó‹&36aGð  ‘t6ÚŽ¸êœê6¸É¤œž=¬õë‡ ö£sÌèr¯˜%•R¸¼è²±Ül²ºD…JD ä¢ãn6…Ê…Á-C™„ß6b80a@ÈÆøè8å/PÁʧrµõlûÈÎsXç4zXŸ¿À'ûllŠPJ\aÄx¹½† +#ϨÑ=añÚñ@¤²!ç¹D3škÊñ²r:oÁ¢*E«ÝD㤋NŒšP+ÂDz]Ô9™ +¨³S*®²áæ­K*( 27àA}aÅ̈å3þäêO6¸­¨"ĺb¢ã“òV2Ê=³.*¬wR·˜ŒZ´p‘ÞŠ¡tÔ'¤]dØŒH&ߘkĈ[PÅNÄ­X~úļƒŠé쬃 CêÜIÏLO n,œ +d§Õòœ“q=‚11­;4b94é6Æî ‡R N<6fòÁcÒB M8 V"lô)>#güùU‹/„‹ 9ÕñÊ9³7`°Ð#®Q½×KF‹mœOŒiAW¸R7aªÑ«‘€W. ©96Þ³*ÂF!¤äû§Ãå DÈl H`DˆÁbsö¬‹PŒZ!ƒI&Pô«uøË„ ß?l6Yñþâi/—³Ò¨u²IŸ\ýXm— µ4fZ ‹F„Æ ÞQ­Ëʘ˜¥§›v…ä,¥ÔM"vB¡•´OI6{5NþÖ]@\rJÉÏ—fOFkËx KEj„RSÓBj V-TˆEGLä˜ ‡\ã¯A}láÒN*Ɔª\¬3¤÷Ž™Pðˆi>3ãU*Z¯l#±lsnóv*Úƒ†Â‚nFu3ƒ[LªÁîe7Ž\˜Z9aFyîxþ +&—ép‹öàWZJ ÞФcÒJ8ÉAž¢£`qUL-x¹L¢´ §Ú#fï@‚½¢›‰"Lœ Õ|RÁNDp&Zh¬ùäܤ3ú ¾6"nÁcf_$›³ÊI‡  œjpP& M=Ew°Àº¸TÜÛ€uv3)+®½Zï +Rj&^[!”²þÖmFlD þáS˜”Õ9ÝÐÁ.æo;¤?8b‚™Š‰Y:Ô²ãQƒSž4Q&(9ãp¤÷ˆÔ¯óV<ìær.6뢓åÛóÝM'Åä‹ 9LÈ[±¼jDë´{¡y­SÜ`‡ÉDª»½‡2ÝSf,bòJ”R6кün:ËDZR¢L4…H 7¼\Òè–dLc`[G4î =: ?M8°±oÂ Þ ¥”´NVïâL `¹oÄ:ªó9ÕIªÿt@3ªu"¤ngÂ&ŒÛåQ+¿_ë= Ã@ÝVw.w¶î²‘“Ñns¹¥q$Ö9*©pµÝÛÙ¹øˆCLiAƒÈ6:íæòT¸+f×µh8ÁîSF ( +.¹€îñ2¸å5©!|ÄH{ƒ—”Å"5&ÑDä,¦TøÌ\¨ºÍDÛ> е³QBÎ +jÕÜ|ÌFG¬˜‚òɱÁý[µ. /kPg.\ÅÄÁákРh½œ°â&"êà >¥MGÚœÚäbm„…2mpé̆).år€.%Ù^Ù½Løs‡ È„Ùç¦â> 8¹F눔׻X‚S·OÞP‹3C‡Æì³{e¡Ãu:T·¡$k"BrHï1ù4 +ü*"æ„d×ÉÄð`¡·y‰Mv7¥9lõt³ ”KˉN8?oöø'õ^”ŠàþÜçÖÃ{PãuR)r:œ¡C%N­™±à( +oBý^1O‡™©ã­Í+¸ºÏ_íïe;GdtÒÆškl ŒÊÇF'LÞa ‹ZYWÖ|Vë ê°ë${§ªËµa@‚X kŽû F7gFxupïAh«CtXëÔÚi£' +"èÄC&·8ia&íPßÌÆ>TúÊ°qt@&!1:íã‹·®ïŽÔ P£‹=0jÒÚÛ`c1lÁ#d´%VœÓ­X(JuJYO°$—–}‘ºGÊŠñöÒεí+O;„¤›‹ó‰Ž’[ +ç–¢¥U.=7d¥ >55{Š ×÷;&m´“\_éOÍ4W.g§Ž©¹ÎÖé{T¾P)\[‰wvSGRÓ'R3g˜Ô "e¢™Îý½Ô\= -iÇüV\±S1B©‡ŠëŒ:eôH6"<9¸¯l ^±äJ>MJÎJ™*è]°[WôØèÂg>CÊl¤žnn2¡´§?3ã`bô­S¬©xÇ)åM„jÆB@AÇà|x³/L«ì,)åh9'Å+v"8¸AŸ\\P±WÈ8(Õ#¤}"ªØ(Uëâ4vj¤ÓŒãRŽðÄt/¿tñÿ%齞IÏsÏb%‘bû. ¼Ò¤2H á½w…ªBy×eºº««Ú{7=¦gšã8ŽÞˆÒPÒ¡(‰’©Õ‘«³« î^ì¹Ù݈½Ý³ˆ‰ Èüò}Ÿç÷¤ù>T¼äÀíHÔ©JƒŸ°¡²‰ Bãs`Ó6ì[î³¦à”“q¢*Ö­Ã%ût Z£‰È°i4Zö³†UüL’ÖÕ[V?OóÆ­‡o¥ðíKöËs!'ª€LÙÄeKÈâãH¹] +'ac´òJ¼µCgp(qµNõütòò:ë`ÜX"ÎÓJ‡ÕvDŸvqA: åK¥6ë$/ÎÆ!ÂW¤ì’^ÙœvUÆs±¢±3~6×éXK)Œ`3ØHŽe¦7nÕ¥^2ŽEŠ>>=‡Èx¬óvZK+ìøö¿µòâ­òò=µ¾IÈ9½¸).¹¤ŸÖ<¤2/: ”Ï;û¤Z9?ãóqÀ“ «µt{7Z\JU—÷n¿F´2/gæ +£ÓâòM£oï£J‘2ÍþöÏþüvî¾gE%©ÑJ•‰ubå]£yD¨-^og;.*9i§ápÙu­~-¿ø@.o:iÝK*rªãµJÓ>ÖN1±›l£‘²Y(´¶n=ù´½ñÀ„ìS“K«|a2øìj$¿ dbC£ðI'©{é­º˜Õ+™½aÐa½°Èª¨íƒ„ °@*Þ'{ÙîÞÜõ²IkƒP<7œhÁ(éýãP¬EÄ[€—“vbÖE8q…OõÉX+ZX•Ö}ŒqvÆ{q.8ëa/[P³W)"•¡ÔñhÃÏ0­I'ûD¼í 'à3 ~.Ç&:œÞŒ¦:…}/˜ó»B$&L(€&_¶¢f½t´LIy“‹˜±#—á¥,d1¥E*RªŠ…ñLVxÒÇäžõ+î +{zÞäaÿèìÌŒ™œ A¶s3Ö LD*˜X$ä*ÎÌyÀ£E“‹ÖEÇ[ÞÐÛñÆvgùæíß—2},Z2ºW•Êk ª §Ñ­dç`‰LØPTÈ ‚„²Ja\NŠÓJFc=À¥`Á‰øL_.Œ´æŸ[ôFgñèôÕ¬X„ŽÕâÝLï0ÑÜI÷¯¦z‡‘Òš‹Ô±j$ÛŸ†Ð碀)µ©U¶û¯J+wÆSàzè Ÿ²ÚŒŸŸp‘&I%úñÖÑàÊ+%¿ÉÍ Ÿ¸ì@|¬.åæ{Wœ¾ýýýGŸV×n±j2Û\?zNw/¹ >­¶¯m’ú¼^ßi,…õŠ Q’íkr~+l¼ ¹À:ž­Nʶ¶ ýƒ7}fÊ3€ÂF‡Ðjã5€ ’­Ü€ßµ‘²œëW×n¶vî•–¯gæ¯F+x¬Š‹©ø룇ï{uÒË…ÔÂäà4l,̸9`ÅzogåàÅŸNúhŒ6†±ÞµüÆÃÒæ¹qÅƤÄDãÍW½zðh +€ +U¤ü(»pkåäÃÒÒ=k(zì\JA \°¢“nÆRpu|»¤3ˆ vƒ°:"d ´ +¹ÅÔüqaån÷ê{ƒëß‚ú9;¬ ¯‹©¡Ù'Œ§¸„x‚¨„Ò*¯<„´ØWéHÁ…Eç|<ðت{ÂãGG}ÀÒxìð ’ºSˆBÁGfxe$YØEheÚE™C2t—Ä•²RZÍô®Àß +Óƒc½{­n—n‚¦§ÃuÑRaµ’‹P¬ˆl *&¯<ãâXø˜“‘‹rºå"#\šC¤\Lwöëê;O)Wd{WÜœŽFKJu],,ãZ+ÕÞ/.œÊ…% R©Å«›J~.±xÇE§I%9sÖäD —¨nÆj»ZcO.­ù ©”Œöl8IÑ[BºWìïn¾:¸ÿÝþîT)T»›·Ÿ//eí«›·>¹õúëúþ{LfÅËg¦Ü .’µíÙñ}#˜‡ÒÁ"-¡Ä!³h ÉçÍ!³—óÑЋNúø“ꃂ%Ú !ÝRkëÆKÞèxÃME‘¢?R†ÒJ··7n½‰W–g|Œ‹Š–Fß<ž ™wãŠQ\¬Î-úè$¦TQ¥‚ªu.»ÌåG±æF}ýöÕ_UVnž·’`+bnÄóA©겕ž÷rŽÿä²ÝäØô(ZÙçÒ#>½4ã†Ç!BÚ‰G=t|ùÚË£ç_,]ÕØ|$×mdâ’m<ÌÉ‹Ÿ$j[€š^:%¦‡^: %§×¶lXìÌ”×âÓÕ-\˜´ÿÉ™™ …h¹~ 6bõ½ [pã:¯÷¼¸~~Â91‡_ž#.[Ik@¾²$èÊúÒ°·9 ˜ÆËCÄHµ4Bgl¸JÆjLª ±ÈC"1«Í “$ùNØ!¿0iȸ؀×xºªñ„c"@o¡±­W–ÎÌ8/Ù+¢P±V¦TÝLv¶#zåúÝ7½­;62«®÷÷Þjm?ÏΟ6×Ö71©Áe)¦úD¤è!¢c–fÓA¹Á&†FûŠ×σ;ˆk6îúA<•š_(ÒúøéHT)`Gr ¸œ§£¹Òp¿²rZZ>Iu·Û÷[k·ùdGÎ +ÃëÅáQeé—_²3™Ù 4ˆ¤¦FukÂN^²`¤Úà3Kœ1 CJëL:h[PB¸ ”48@‘R^¯®?x€]àãÕÃûÏ?þ9—h™ý‚ÖÙä0Û¿¾~òþá³ï—×îAéÎ…x¥8ò‹\3ÄóbÚÃyiœÈM%mã“e7Ÿp6L£â=­¶½zòν7¿ÜþãhãÊ´_&”Š7œvqS@ð1é[`´6­µ¡ŠÎÎúàk¥Â:*×ç‚A㥎Âgt}a )¥¹r½¶x+/Ë¥u,ÞE |v©¼úØ.Ìz%17>£húÿgW@µ sÁŒ¸ -ÓÜv¡òwÎÏ|ûܬF@ip¹"ÞuÓÆE+kž4aÁÏ^´ÍyEk j F!Fù˜”uüÀ¾‘hí`¬‘*/ùÙ˜ì‚ÚØŠ[lzImîcJÅã"°kP.Cï@MÂ;oñ Ž`b‹= Ûý"péw¦½g§}Xe£7žq}Ö7iÇg ^1¦VY£Ã§Zj®Sê@'#ùöÆÝöÎÃÜÂU>ÓA¤´—3\Ôxv2Aoé¸ÍËBr+zóFnþNsã…w<ù3ÈKNJ÷Á4M~ΊHn&ÃC!»„kMˆÕ…ƒr/ÄÆD$ÝX›ß¾qçÃí»oNßþi~xÌjÍ“Ów¾ó%+»é¸›Ëb±ŽVÝ-/Þén>õ‡‹ß™ œ áòBn‰OC®LAwÏ:h€mÈbº¡Æ¸T_ +M´CBb#£”SÍÆî#$Zµ#j¶uPš¿¬nw×ïôÆ÷×悯Þýc>»pÉJ9ð9΃Kh¤}¨ ãÐ& 訖×ãõ­ÚÊÉî½7ÃýgX¬%¥û7ß²I;ª©ùxeV¯í’ZÂAϺ)Œ7œ˜2†y3âÀbzc7¿t lƒEJ>J‹sþ°É†ÒZ½õÉÂñ›ú΋üò`¤zf: […"ùsæÀ”‡³ápr@‘FëšÑ>¾`Ÿ´[cò5ùÌ~žT*RfÁKë.TP²„O[‚HPìt ¤¸2¼aÇŽÈ@°åá5L*:‚²ÉÅBü??ë3¹(”7æ¼ÌøvHèbIÊ.2É.Ÿ™Çµo–®‘ çb¦<Ø+ ¯Â;>©d#ã6\¡c­H~M«ï›|„).Ù‹6å<ÀÆ£÷Ì~õÌLÈäbª­ÞèpÖ˜pâÖ ‡ð9Zkbr‘ÕÆ÷Z³J1]]2ññIu<†EZ„îY1ùEØA“‡™uQÐãà¼Sr<>G䯲‰F±»ýæË_}ÿ/ÿçÖÆM*VQ++Je­²t’ë×æWO½,êU>Ñ +Ç[¼1@„ŠÙ+_˜EÎM¯ÐAiY¼Ü”:?ã?7åž²"³vÈõ ÉèÕ®¶îMû;®zØ”“IÐÉV¢±›lîóF‚Uc¸Ÿkm9Ш5À@½™¼ ˜§Ê‹ÙêòåYÿäNrB›¶S ó\!¥‚œî9‚’Œ£´šRqã{ꪤڤäB}~¿µt xN |*@ëv?ïÅ"€¾^<ªæz—˜0y}x4À&)­N÷…Ü|O€RBûèÖ+ÀuBíÍãT뺜š× KJn!(/Yˆp´”­®âž›ñ˜½’‡Ê3ñ¥þö»ÝÝ·½{ÙJ0JINÔìþðŸ\²~{Ö7áâ­¡8—£ù%?Ÿr3š5$œ™pÀë² Å¢%­± ¡Ö;jýJ´¼‰E«úøÉ辉rÙnpwtsùèÝýŸ_ö£µàÑ) bÞC& &Ä…ïä\Ч] "执=áwØàÁ•|t”Ö åá•+·ßknž8…slìfû'±úÎø>Ûì<üúxB)€-$T,/RFÍw¯•‡'lr~ÆÍû‰.WH¥‰ˆ ´±™€/“_¼„añ´úÃSÔÑlo¸{otð8ÛÛnßZ¿ñœKÕq9-,ˆ™þÂîýêÒ _8åÂ%>ž_Ú¾,/@òઋ»ð„JãbÁ‹©îgPtD˜´¢ÓÖÐœ > ›=$øÈ%8ÁF²Œd°J—rJnxãÉgkG/Û‚á£6ÁÓ“õ]\ÈHçâs~\/ÄexèH’k‘ÌRËšœ$I©Fç.X!8€ÍÙüÒ„²aÓ.Ò:–Jé’É7ãÄTÜX 4-ùÅl€ÏàJ ZÏèì Fuýú³þ΃|w¯¾t½»ó ¾~G«mÑ¢Qq‰š5öÊœ/ Þ1kGC”NÉE1Ù+ö]°§– 3$`R>ÑÚZ½ñÎîÃïU—ŽO}xúò+©°rˆX + :ÖŽÖ³ów+Ï¥Ò*)çKý«©î¾˜‡ØâÏv«8ð¤˜\ °Ù?rgÒ}ÁF•6ïHUËxET‰ŠÉÆVH0ܸ¬—¶¯?}õñOwn¿oos´6:x1¿ÿ”MÖ<Œ*eêKǹöv$Ý5*C6š™sã!:‰Š"ÏzXÀÅ9¯dG4¡Ù ÷s“®i bõ0¬Z!iù‚’j-nߌúˆ”Œ–Û»¯½ ‘0½p‹IÍ[ƒ‘tq´÷—‹O:1[€CôRI™4;Ã>Tã£åþèÚèÊ㳓®3—¬P¡pFÌèx%˜Z®±¾tåäeˆ˜T TÈHMJ6Q13mG‰ð˜Qç¼ôœ‡q¤"æBd¹4ãp£!¥­¾0TZ¦±<¿ QåŽZÛÊ÷¯‚R ;Ç-`®ðœ›s *ÞÁÄÜå¹´€‹8‚{|2$5‹ Ÿ±¡æ NbúO/Ì}ûüì·ÎÍž›°MÚP'á•lR±½Ë æœ8§ìXWʌѓ‹+j}§4º[œ¿º¸´÷ç¿þ—õëÏ!3BÒëëJyRª] EŠvT&ùÔâæ}7¸8ã˜4;-nÌ<^Ýã5FɃïûYƒTjb~X˜¿R]˵Öî|Z_<`5¹0žÎ%¿p«½õlýøýƒç?Ê/\/õwwo½’‹«bºÇ'Ú¸\ Œ—b/bbpzmÚ…Ûш›Ôñy!¹ßG%Í0æˆJIåiyÎÈôÒ90e¸!üÂ{¸Z@¥ Œ•›I³É^ëakt#ßXÏ5—ƒB’R²ÑTÛ6^%jGb¿ì i“*5·jƒÃsS^gˆ÷˘OÒ˜ÖDÕz€Ë6:£ë “²ätS4Z¬6^Ë7ZˆT@ØT¾6*4×M.lÖ9CrˆÖÁ(!iÖ†›¸bÔùžÍK“|²>:X?y¹vúZ.o†"u“YÚ¾÷Ó_ýSº°`v³@w¸gÕ6ëSZÇ>^Ôƒâ´j}妒í͹(§Y+fuÓ^DöcQI- á¸UI±8iÅþªkÚoöp~LuøDCÂéXnþîËÏæül€7p¥Ä¤zreUoíÕŵ£Wÿ¢ºpq¸4­²|K­¬I¹e\ëXÆÁ©zýÉ»¿ Í?¹0unÒ!qÊF˜=avJL7Û+{·^…“ /T«±r¤Ð×ëËåÑA¦¿’3jy)Ý»Ê&ºH£¹®T£ÃhM%³Pèî°zN+u æØИ‹ŒyÝ”g¼<&•Y½çfR~>7åϘ‚¦€ä¤ .5E*t¤¸ºû°¾pÀÍhy%;\]½;Ü{¾sóusëŽRYî­Ýøøç¿å2óN:Ahu:Ö°3Ë߬ʽé¯Ø¿:^3å›iíßÌjñGP¾DF“NzÚE€?B¯>—Éî`O¹d‹æÛ WÓùåBmxÐ\¾FǪ6,àRPa­ +aÓì!É:Ѹ‰¹|B€ˆAЛ°à ö\¬<ã&'œ kÌWÖî'»‡BfÞͦ\¤¡ä:þp2$äl@Ÿ£¹Ñxwf\H/2bõ2 ï¸Ñ¨ÉACèÀ¾yr-@Ç9­ÂÅ+Ja¾³ûhñðåèð­µko¯¿jÍo¢LÌ‹kŸñáŠ34^³)Äå} Qát4=HÃÍ9'á EC„nuÑ\ùrxéi+‚ %%·©'Äj—¬øùé Ý'Z¼Rˆ+ðÉ­Ö”TÇOkyá8p¾V[ioÞY=~¹züNc÷‘—O‚ä¶6vP."r) ä1¹LÊ`õÖèÀ†ËzÁlqRn4fóË_Ø»àÛËÕk‘hÖFÅ”Ú&ŸhÕ•ÂÊI|p„ǬZ<8y9ÚèÂU•ÀåªRXí¬Þn=ˆ•VÈËõÅ>ýYiáÆŸ·M;q*»(CÍ/•Çbfɉ)+ϤìàŒÉ?颜TÒÎ@RƒL¤•GùÖÚòέÞÖm'­Òz%Zœ—³&QÍõv¶NßÙ{üªÕ-£{€Dªn*á÷¡’Ò€JKÖ6ú¦“Ï›ÇôÂ7 ­fhü²½<^Ç\>7ç1ûé›ò#µq%Õ;J÷®yhÃ(,¼|ó³;oÀ …t[©.Ë员‚n{H͉ʜ֨ O‰Hmòn·ùxˆ çf& DÝ^8¼ýì||r,6út²ïçÙä ÙÚ_8zWÎõû«Ç'O>­,ÙBŠ:ŽHŽ×¨bR(7¾Þç +ñ`|¶0 ÎË_²’æ@ÄI§¤Üj®s/ ÚKW^lÝûpóÞwWn¼-æ†6\%"…lçJeé§8½ +àÅ&irsfgõ +V7ëG#Ùʪ¤·¾}Þ2a +8‚ÝË8¼œ#¤š}2.•–žûÿOçìV89œȹ‘›J}gÂg ònJqÓ +"¥ØTн»ÿ¼´zŸJ/R‰–RYÍ ÆS"ï}s/ÐF¼¶ ïá’SÞñü½6Ÿà +J&'uv¼|•wÚIx(ÍÍÄ endstream endobj 61 0 obj <>stream +-xÔÂFƒË¶r2V[ií—¨?{Ç°T)¥ä2€ãéj±ÖÛºvï=1Õ=;é ²Iøƒ‰5À}œDÂM§|Z¯,£‘ü…9?tºU½Œ‘ª´?W_Û;­ö<¤bt®ô÷ßZ;y³rôv{ãV}õ$ZZŒg{|ÿ¯nð ó1Faþx°û2Û9Évnˆ™E;®@續ZžõòçÌäù9rÆ#Û1#$Vâå•ëÞÜzù™Ÿ‰Íúy+¡z¸ ­· +ýýÒüIvxÛ©ø]on€Vûøôøq{>ç$4¯Œn—óó†½L2$–‚lÚià°`ÁÒCl,Y]t“q[0ÂèÍtï ²tséàiwóÞøÂ¥š»õà½}ýÛJ{Ö+‘:­SjËǤ­¡('-!œ1éÄ-!‘Öê0VD¬™OËónª±¹²yòñ—¿\ع3زpãuióqqíÁàðå«ÏW¯>Ët¯äZ¿üÍ¿>ýà'~Rbu.Þu!*Áƒ÷¥x tãÜ ÂÆêvTº0ë;?åúö9Ë…)?I7“ §X½ªopzW«Lj@%zLjÈçW¬ˆDÇ+zm:"l4S½½êƽÞþ³îÁËÆîóÄà˜Ë-ê…ÑÁéëõë¯0µŒ«õÔø®’=&½NÅ—ÆË»;¨‹æàÜøJ3iÇ;°“ˆy¹ñÌ3z}-V_Îu×W®¿X9~yåÞûó{Òý\oû¸ ñÍ9X/©ZýãûÍ(­UÞ¨¯ÜW71¥2<à;¸2ë¥-7å2Íxu—M´fü¼ÙÏ…ø„“ ȧº‰Új¶½ÿÓLHf»;åÅkÅùƒXe-ÙÚN6·¸d;žíì?îÞôÐ1ˆo©ööx*›ô€ˆµÇ+: ?¹rü:„ ‰ÉX}£·ûtïþ§+'ïÇš[B¶à5R6æ·îr‰²ÓÈxSj0„œÇ©lmYÉtݸêFìÁñâ,."á"t­8ʶ·ÀGÎMyBá6Ñäõlw»·ûPoíøØäÆÖé³×_ÉÉj(œ¬,Þ..ÜN·äìª9 L9‰¡Ü}]éí]/2«…"Ÿ^ÐW +‹·2ý넾ýèýk÷Þ‚ïÂcDÚ;Ï®>ýþöï”Æž‹Lií“ïÿÅêÞ=sP¾hF-Á¨YQ+»x¤~É‚ºÑHmx˜îîO:(¦úè4:ÞTJk‘Â2£Nž|xÿ(ÙycƧâGéÖîèàÉõg_ÖWïJ“JœÞ³Åó³Þo]˜;7鄯BEpêøw&\®˜(ŒæÜü”ßôÒéñ’£A•K*NÈ9.ÝËö ÃLm z­ºx\[½ÙÞ¼µúòáû?*/È¥Å+w?>yûç£ß¿xòæWý+/øT?W]߸þN´0?àí˜ÆªuJJCεxY?¡èévÈ?Z°9ÐFo½ýìâqo÷qkû®˜ë,îÝ«­ßJôöõÞTZ£’Ýñ]Í™8—T9Âz ZðHeH0ZçšGiYÃ/•ÉdW©n„"%"Ñ$’=DiR±­arÉlK¸A# ÄÖîààíþáÛµõ……ãdô­©å{ËÇ/¢•e­ºÚ;|•è_ çÂFëÒÎó‡õ1³@FÊRªç%¶€€òF4ÝRú΄÷’q J)dÚ{BjàÄb§fz>J™óâö IþäÙ÷NŸ~:¿ûШ® +ɦ#Qð©ñ +t6|ÎË¢b6×ÞÉ57ãÅE“‡>7í1yX/l …0h!Á@ÙX¡1ÅËŸ˜h÷®}¯µy'VÛDäÚø䀜yþæ‹þÚ s–Åå²›LÎù!{ÊND +1ñB{Cg+»Õ/£<u7l ©!®ÔµLûáÛŸÞ~ö)%C\®µþ¤´tÇhmÇŠ‹|LÆäf_¿]sÙ‚ §¨mB.¡aÃKêg§|V7Õì|÷Ëÿ2mAÏOyM®°ŸMñƼVX‹ç†$묰ZÑ9žž«C¥–‚t$ßÛ.Ž«ËÇ¥…C½¾"¥ÛÅþ•ÅkÏîUG žZe…M ¹¨¤;Í¥#>ÑßÊó<ÎA*f„•àoJΙ=¤DHEòÃîΣŸ/]{ÕX;YÚ8ú/¿ùý­ŸðãɯV3ÓxãJºw4i]yZþðë~ÿ‡¿qÒIsP™ DÌHÔ#™Q¼¶]êîÞ|ôÞòµ§•µ[Ííû•µS½½ÙظÙÛº·{çýo} nXìï?ÿòîëŸlÜ|][»«VVIm|o'£l˜fò±^:é¦ PE&ÖÓ}B©ñ¶Óiµá&£³^ ØŒI|s°â­`8ãÀ"¾Ú«§|ª©—z¥ÎšÑØs+˜ÒB¥*.|„2ñÍ*ÒP\ÿÅÆ x8”ÏùIÞDù4¸&e¡¾£T6é"ìˆ Ç]ëg#œ^RòƒÆÊIç¡`´Cläø™Žoî¸ÃAÑW*HÙy¥ÛƒÓx[0jõ sÆ…ˆ2–ëï÷wT—O”òZP*š}¼«iÙA5h¥ +gq¡,%»¸˜³£rhüè9ëd܈ra:pþ²çÜ„kÖAH-D©¤œæ?sÑaó²³Nò;“®ó¦À¤‹6yÃ7MGs¡°â±Úz¼¾ÃHz +p¨·]]Ï Ž"…¥p¬¢e»Fs•R BV£ÊÆ«‰ÆNª{²ž ‘¡†ùx;«OX±;»)áÒ6?561ç‡D vâ’¬^S«k…ÁaoãÖòÎÉñ£w['å…«ë7?X¸úîîÝOV¯½•ïîÈ©†’íÛëû·_Iù¡VYkmÞ_2nrÑ Âã;ý"Æ¥à_gœÔe+6ébmXÌNWÌ‘s‘\/ÛÛõ±©d¶ž¨ )5¯r~ RR$»¬®ëµU2’ «†ª"BRÌÌË¥5ȆÀ9¤\‰eGñÂ’ ²¿Ÿ÷“ª“­~þ²Ù~Ú !ÂOÄéh5@ë`"¤V© ¯®Cò=}•Ÿß¡´¬’k÷¶îêÕÅt}1QY •"Â%…D[Jv”l3Fc-ßß 'j¸RõsàtŸ¬(éFº>R +}*§Û;Jy9’_l¯Ýª÷“¹F§7zôòMoóH2jݵëGO?ºõîç÷?øÑ×_Ý|ç{Ûw_ÍoÝ{þzÿîëîê̓Ÿ´vžuv_.¼ìnÜMw®„„t<ÓyðþO»Û÷.Z±)…G‹a£¯w®—Fwˆh!Í>ý•”hØQÕÅÑÊjº´xõݵ[Ÿ6w_ÆÜxúâÍË‹GA©À¦úˆÚDµnmtwóÎg…¥›~6qúèýÒ`oÖ/B¬6¢^6‹Êµòð& º˜l·®vC¡æût¢é §©X=œêdçðXÍŽðb² +ô÷)µÂDóñâv'¿p[,¬¡‘ª†LÝqæ“óD…È äçc•k@…X‚Â$ ôt€ˆTá´ÉÃAâ8¼dÁ'lÄŒ‹™à6<7?Ça#ÎÏa*!d‡‘òªT\2ÎèÙMÒr¨˜4ûh8Fª¥xu#Ù>Ð[ûL²Ö +‰bWÌ´œ˜ÍÎç†'éÞµüü R©º`/¼ŒÉ+z]K5m>æ²Ù{É䶅q¡„ryJ,”û!1ŨÅh®—ì ù!­WaLrƒë˜œ²úIPRm&W­ÃñœWLLDÎôqµ2ë!„\Ò] „Bo@×[üŒ“ÀyÃâ-~ÆÏÄF“¢ÉT¦–)X%#Ç •îJcy§<¿’¬ …tC*öõæja°ÑÝ>0Ô^}úi÷Ê#II¯‚á'e¤²ÍõÃG™Î&dÕHz˜(®*¹.Ùó± wˆ"êÖÕ[A6I¨íhi3?¸¾xôöêw{OÔÒ2I?~þÁo~ÿßW¯?GÕšÞ>0†·ëû¯o¼úùúñ[Åþ¦’.¿óÉׯ¿´„"“†Ï®$º×ª+÷®Üÿ*Qßг½ÇoyüâËÙ€4‡*vhÌXkaÿåÁÓ¯ö^ü8ZÝ,u¶ž}ò¹î®‰8praþdåäƒoý¤µñHÎ-_»ÿQª¾yn6pq60ëæüL–+éúfûaX)Þ{ûó_ü-£ZG(nð¹U¹´µrúñΣæú'~2qûÙ÷n½üL+aÌ/Ûqè, ²Y:Ö»de¦läîá£Óûï9ùÛ­ß¾`ù㳶3S̳é%âÀ~µ…Z«qF—T«s}áñbF¤\„bb‘TÝCD<¤‚ˆY1· 7wkKÇ OåÒÈO«éÚZ´8rPãǬæÒŒ›µDRHòJÎbmn„ GmlÎKû…L@*8ÇøBÞ†b¢®×VŠ+§Ë'ï ®¾¨l<Òê{f?ëÅD1ÙðÊ2$ýYï +E©HÙ¨ob‘²Nê…Ø$'&9ˆØ¬Otš‹BÔ*8»‘’õ hy^­|Æ…DC”Ïv’å!&êfŠò RÉó©FcíÆòÑÓþöÝh¶§Jacaš•(!$2Ã9Ýâ OØ0ç7—0¼x4H«~RGò$—ĺ#A…)™x0¬ãR^€[‰l§Ñß*u·°èøjfcõv~þ0VžÇ‹a5‰g=†h ŠÓNš– a­ŠKÙs£¢;ȱRV+,2ɾ%¤Ä*zc[Î Z«§ƒ½‡ÙÁ>ÖóåîÖÑ#£²Œ„Ó‰êj¬²,gû¬^) 2ºWÙñ¼^©IaEƒ|!š[‰•7…D—”2¢’*6‹{𭽧6÷ò£»…Å›‰Ú:(?&fB\Þ¾9ÚÜ((TóãÓ°œZsQs঱êã—Ÿ={ý¥‘Ï͆¦Üân„”¦ÞÜ×k[‘xñ£ïÿòñ»Ÿ›Ôù)ßœWøOvŽ«;/ Bì^š’z¾¤c ¨˜V +óÀ~µ¥£|wÓœœZX?η×íhÄCÄØx—‹„”ñ a‹ 5Ù|?d·?à⤜MÏ_Æ3x/àJàõþâÞÆÕ'Zy)l4ÕòŠRÞòóeGHÄY-É8|ŒÓÏ…èƦi©ÍÌC.ƒÀ‚qYQ­ +ÑbµµÑ߸ã¤b>V÷³º ‰X<ìx›y=U]šó°„§¬øŒòSq ò…œÃ¥dCFË{)YÉõr½=­²)e« 7´â²Òj½|kÝ” zÕˤmh*ÀVÂZ6Éê +Êj&šlš\@’²mœÂ¢.LGøœ ‹c2©¢b!’ªéƧ¨ˆ„ïžæ¡pV ÑJˆÑ0Nfp¢Ê”㛀C+f{Èâ"­~ˆ ×y¤´zÏÙa£6ŠU]˜ˆñ N¯P‘¯•”D9IÙ½+gŒR߃0®  !èB¼Ê*EØ›âuaã[¹€Fà"òR‚â“>\ñ`«¹D 2¨8~"ãÒ´œ“â%”R¬nbÒä «Óñ ÓÞ˳‹ÅÈ0Ã)‰T3_[… +»ôRY$R +9?“@¹( AÔ¨ž7ÇÏ…å–«Ë·:»Ïš›÷œA)Hª÷bý$[åuF+*¹®’íRZT2¢–ÅXÍq. û `4³ â’/ÄQš#NмŒqšRìZÉËè"*«¹õíû§Ï½­ª—×JƒS­´b“>„szIØ)/"(Õ‹Š>LFXól¼CI¥ª…ýý»w_}áa4;*øh̓Fý½¼‹›0Ù1à=«‡u!²›ÐìHÄœ¨LI9ØxTH„%Û\K57båU¥0’Ó T´â#5ÅhEÓ¨«“Ð +K‰ê"w¡´yÆâ·{^N0rêÌ„ãü´Çì #\—j6ëÆâÅbÅ%2RPñÑ)P]ó2€1£—P¦Í—ñBv¶#– ‰ŸMCŽCèxŠÌZ‘X5™[š‘}¸Š°l’+Ī¹–ÙƒŠ2QÈnIJ0*êNJûv§Çã †¨(6P>å!”9  ;kÇþÜ„sÆ‚ø1Åî {áx‘Š?$Ú=Œ•K‡‚VrXœO…#Ä$0΀œžóOκæì(JªSsþ)KÈŠ†È¸šª)ÉŠ-‰8„Áo]°ž›ò\œñMƒÅÍ.‚$ )yÆ:3é¤"%>ÑŠÀçùDÓì&í~zÒe'ìþ°;$pÑ‚¯¨¨ÅÛ½¤3ÀxÁC¢ÙIpMMTJ¥lcg#‘DzÙ”à `äÙIÏ´õ ã]»0m7ûE7•"•†ìG2CÉhzQ^V³ýµã°^€Ö[±üB¶¹Í =dÌêcp>AˆÉñu¤¸èC„.ÚýäŒÅcqøC¸à …]Á0€+o1ÑT‚‘À8Äd4‡T:Ja‡Oô<À ÞîeírÒävXhÈõ¢^&¥ô¬;7嘳‡†Ë‡;מ"L|Ή9<”7ÀsRÚI>T´y¨I“7@Ȥ€¡} ivN/ªâ +Šž €Óq?"ÏZCjªË.øÐ(ÂèΠ0mAþô¼mÖA{Ñ(lÏÙKs(­ÐRÚæ¡­nrÖŽ]žõAq1™2sûßæbí‹fßw.[¿uÑ|nÊ Ý!Æ +§¨1cïÆýˆQ5y({PðÓ&æˆh-ÄçLÑäÁÕ‡ï|ÿo¬˜üGg¦.LÙÝ6,¥$-«¦ŠœÛ?¾?Ú»ã@ùIKÀêc-ÆâePV£D‹Äã©rº¶ÊFË— ±ãV¥-ˆÑ+ +ѬãÜAÂã'1:ª¥[;ŽÛN6̪©Œ‡ä,AÒâGÝA’‹ùXß…¨¶ tÙâ‡]öŽoàÏá¢á…Њk^$B…ï„¡p…!^2Z[^:æFDWƒ€ÿÊ+yY¯˜ÜøE“Ç죑‰pÊ‹Jf +[’È # ûfs‡ÌNô<šé¦[›ßœiáp!UêíÄ +CP )8¬ S!ZÃ…4¯Õ& ÞÌvR…ž;@€¤³œŠ2BF0RrùQ«Ë¯g +û'wC?5íž…|}ÉvyÒ +CqI°{(*‡_òcq·p¹3fÇĬÃá ¨ŠŖ˥œ žß¸LYÁÂ\“fßÌ\(€Ê.yÚuæ¼éÌY“ÍŲ‘2-æ<þ0ͪ±dQOU¤XJÚ €µf,Á³-'ì³f¯ÝEÐB:€ÉsÖÀ”Ék¶co¡“A:m÷ —¦]'¬\42±Ë3®KÈêfð°•q*fw‡§f·wÇŒwÆŽz‚ ž¼Å"Yžb)^¤ +¹DcÐÅEÁƒ“Ñd®PïÆ2\Œ¸‚Á`(¨Æ“…z_Ë·ÏN9'L^—Ô*‹¥Ã°2W-ç–67Õ\iÎÍ8Q"a¬ÊÈj<•HÒé¼±¶¶ôðÙ[ÕÞê¤Å;1çž09HFäx^ÓQ’| ¢Èét"ÌSvçœÛã ¨aDŠ¥äáþúÞÑno{)×Ì)zDPU?-ƒ WûµáÐ邬F㊨ÊQ=Erº¨U z{ÌÖüD¶Ü(öøXÆ(v1> ø.8|Ô¥YÏ·/Ùì!9ÎNÎ…`èl.DŒ•ÖÂ`e]ˆ(  & )½S!#©¯ç›Kb¢ÌÇËT$kñžP¡"(-r¢Fññ&"´FÉEJHr|„géL*¦Æâœ(E¢ŠD) +e¤ÝÎ/­ÎÓ<¨"íCx¯Ÿ bBªž èòó~Tƨ˜?ĹÜ.Ûîv;eYnS‹ìΨ~ëháÆáèÕÛ·wWõx e"^„ï#éøÌ\ðOÎÌœŸtØ\4hˆÍhšdµ¨ªD£’ñ 71/PaÁQ†åJ óF4VqÙK3Ž3çgÎ]´NÎz§ÆsLVêÜ„ÿÌeÏ…ißœƒ´»i»3äö!‰l)ˆ…ƒ#GÓ^0?E°)FÌÌ9‘ó“V³AQÁïÃÝN;EzµZÍ‹Ãnz´Ü¬4Ó™‚Új¤®m÷înmnjµdµœÒSiIƒÌ.œ»d2Í9}¾Ìñ1WJÁV…J݈ÄxI“" ‰¤\©¨Ë}ãåÃÏŽÝÝzüàJ±V˜4;'ç¼¾ò$ŠRTdTÓ´W¯󚚎a²²³T|ùh^¿øôÞ?ÿÍ÷~û?ûÞç/Öת ƒ K<&dÆ‹ç –‹gÒ‰F=Ùïæ»íB\Ò©$Ë6·ÏíñFe©V/·‡í•Í§¯?)tç]jñ Sf€Ы0Ÿ bñ²Éwþü%CŒD¢R)çò©@Èë Œ‡‘"õZùúéÍæ`>šL®în$ y/F9‚$«¤ 9¥d:§ H0Ì„á•6ÒÅb5×r )§»«µ«{£~+_Ï)Û Å'÷vß~rõÓ×·ß}~ýÞ­ƒ……–‘J†ˆ×‡9=$Fé É$-!„‰RZèTc­Jl±k­f6Þ<Úøêƒë?x}ýßÿé—¿ûûŸðüÚþîr³Y•D9"½;ç šK„ 4q$‹Ô²±‚. êÉ•ù°SÜ\éß½¹ãÚêÃ;;¯_œ¼zöàøêq³ÞL¦’‚Àú|Þ©Y—ÓÇÎÚ°‹Ó^“¸pÉzá’eÆär»¯Ë+²TFWZRLeŽ*³¥R!™HÉ|eÏ]¶@Ã:ÜxÈçŸBÙ« äp1ñìz÷ÞÕÞãÓÅÏ?¸õãï=üÞ»ûŸ?ýÛ¯^üçï¾úËŸ¼úðÝ›; ét£8«Ýå´Í‘~{ZDYbó¯ÔˆkÉ›ûõë»5xÝ>ê?¾1x÷ÁÊëÇËŸ¿Üø·_øßÿƒ_|vóÁA9'ìN‡Ù ˜ÉHHáPoKÇzâñHyy½úÅ[Û=Y~óhøÅ«õüåóÿüýþñ~÷‹ûÿùÛïþßÿçïÿú§/_?Þº±ßÍfÓ!”÷9¬L2ZËK+ý`)5,‘K5¶•g—{éÕùÜú0{s¯óÉÛGÿúû¿ü§ÿú«/>õâÑþ•Ý%è9»ÍáÅ=˜tÉä:saÒc³Äø !!™(ã1ÞÝ)EÛ´Æþ’ñÁ‹Ã÷_¾||õûÝûõ×_¾óö£rÕXX^ê¯_[ØÉ'ê~·#F"ÖHP‡CýùqïñÕæÕ¡òêf÷¾~ýÛ¯ß|úlëË«ÿòë7ÿÇÿògø×ÿíoÿáŸÞüíOï­å”pÀçöZm~‡ÁhÄ¥sžÅûðJñÏ?;ýí_¼û³Ïî|ütñ'ïmüÍoÿûß¾ù»ŸÜÿÛ¯nüáwßý_?ùìiçñQ}w¹š1´l6ѲÑx1æR¨•Â×jâÝ­Âû÷WÞ:n½¼Ñøåw~ÿ›Oÿðý¯ÿøƒß}ýòüûÿïÿõÿþO?~yÒýÙw¯þã×ï|ôò0¥+V{hÂä·û*õyÝ!¯÷Û$Ò!æJáÓâÉVu1y¼–yqsáÏ~ðÖçï^ÿüƒû¯ßzd$S” aG’5<ÈÑ;-þÁ†þùÃáŸ|óÏ>>ýûŸ<ÿ_ÿîÓÿçÿ¯ÿð%ŒÀ¿ÿÕ«?üóüñ½Q%¥ËaNÒô KS©éÔr‰¸>/Ü]Óß9*ýâƒ+ÿ³ç_qû§ß=üÍnÿo¿ùàü·þïÿüÅ?ÿêù¿|ýèw¿|üνõR*Âñ@ˆÍODµbLå¹·÷’Ý*ïÞÿGÒ{>Iv^gžÄÄŒD@wWw—Oïóš¼Þû¼&½÷¶*Ë›®êjWíÐŽH€F%’¢D#‘+jd(‰2³2£Ø]Íh4³³6æûž„"2:€î4÷Þ÷œçù=×¼o÷'Ÿýíï<ûþîÛÿô‹¯ÿü7ýÍO_ÿç?ûøŸþèý?ùîùŸ~ïö_þèá|ûΓ³f³š@¥h!ö¦Ãqq¿g>9Ìýæû;ÿñ{÷üÉñ?9ýû?ùÆ/öåßûìþÿö­Ûÿï?þðýß¿øç¿úößÿìíÿñwßþýï<>žæp4tue5C„Öy~ZS{vªkFnŠ/ÏŠo>|Ôý·G?ûõó}íäÏ~üÖÿÇŸü§ŸúüõýûýÉï¼wó°7må›Ó©çÜ|ÍÓî¤ÃÀÇ¿óæô/~ðôoúÖïëöï~óÖŸüðÍÿö7ßýËŸ¼üÙg·ñÃgÿõ—ßú›¿þã÷†ßy£ukªU‘c…•ÕÈ…×V’Á ÍÅ&ô|¬<=Ì¿ÿ ÷Ù˃?û÷þù—ŸýúÉ?þÑWþñ¿ú‹<ûÅoÞûÃo~ö¼ó`'Û/0ŽJÂIÁe³nsÐíMj“Š0+“6Åo¿µñÓ_;ÿ£ï?þùoÞÿËŸ¾øÇ?ýä?ÿÙ×ù“·~ùýóÿü/þëŸôwð濲ýÍG•÷λ’O±€Ä¤”“£UušÓÏSwfÙ'Îû7ó_{Üþñ×NÿîßÿåOÞø‹=ÿŸÿçOÿçùÃßýúÍßýÖ“w߸A‘lå²½$:'¦{¹Û`ïmš_>ïýè«g?ûìá÷É¿þÕwÿ×ÿø‹úãOþãwüú»'×všŽÁQ$…’lV4«bySä‹*¶UWïn×ß8k¿w»ö߸ñŸ~ÿƒø“ï~サýû_?ýÓï?ùá×n½uwps¯Ón¶U»ÊëˆHÅæŽâ–k¹\A¡†yææfþÎVáñ~áמ~þþò'ïÿì×ÿî×oç½Ó¯>Üøø¼ÿúIý`œmU=ØHŠa•†éö9F*9FÁ`Ë:µÝÔÞºÙýÑÇÇôçýÓ7ÿö¾ü?ÿäŸÿò;þ£ñ[þå_ý׿øú_ÿöùO¾Ü}ÿNm³¡h<Ãt»](OeÅÕ9²•¥;&¶å¥žíh?húdð[^ûïÿûwÿåo~ãïÿèÃÿë/¿ù¯ÿƒ?û—?øôÆgïíLËš™%c²µÆHEGe‡…QœÑ›á­“êWo¾oüâzë×_Ì~ùã·þì‡/üÕ¿ùÎÞ§7ïo—·º%à,#á”$³¸’ p†Eãë»ôÑ0c»y}Z}tØ~÷¼÷É“Ù×^?øäùÁí‰sgœ½5ñ¶ÚÐë@tÃÀüœZ`ä¢lT5W+•+Ž^6…ýnáÎVãîfîÓÇã}åè¯~ôèŸÿüÓ?ùþ³ß~ÿ·Þ™}|£ðÎóþõÆ~SÕÉ8‰e ਹ)Êzþ‘Fi27édÃ$Úêí±øò4ÿÑyã{ïlÿÃ}üÿýËïýó/¿ù7?ûè·?~po¯3jzøžÄùT"Ž§âKL±á +£²ºßuÎ÷êoßêÿƒë?øôÞ×ÞÜ~Ö›€<±X0Z ÄÈè”7jýS»4EK¢éVÁ¾u¸õðöÁ°ÄÜÙÜ›|ï“7žßÞztԆϞnúUË5ES7Ήf²1ÒÅÄ2£×h­ê•¦ér8¦±tÙÖ˶Rw¥½~þ|·ýèÚèþQïlVŸ6K“ZiPò\S¢hÔd0Åú"!”PR‹D’ñH$“Jé,7¬U¦­»;¥7®•¾óÎîÏ>»ûÛ^ûùwŸÿü7žýþ×nýäã“ß~÷ð³§ã‡»®%¦ÑTœô(¦á\eœ@œE¦š+vKyOô]ìldÝßòž~òÉÿö·¿õ_þæ{¿øñËŸ~ëÉGÏö]ïn +†¥‹fEÏq!‡ vÑêÔ»9M(›lÓÊ:WÏÊ“š7*Y›%ñÞvåõ›w¶›=‡¯Yª«ª+à8ˆóËAfa[ôAf*Ð’L%yó,³RÈ× …fÞ-è¼'³E]¬»ŠÉ :8²ÒlN ¡§3RØ #çêSèY˜Á(™LQ"¯VŠÍ­ÙéÖh»åh»Íì“£öQK½¿S¿¹Q9l™Gmk»žÝª»uSŠ¬.ü‰8¦SJ+Ad–ã_º°¶¼NÇ’Ëæ5©åª%›”Ľ¦úp'ÿé³ío¼qðÎíéiÛåXg´l]¯TgÏ¡ñ€‡ ^4ÕEUV³Q-Ú%O-¹²¥qš&ʺ‘á•8£¬Ä2‹4–ÑY£ŠIÞJœŽ  ++yêüZ^½Xic©„-q ˆb助;<+q¬K`±$K³á»!—Cd’-dÇvû¡”E«FòNª^@0ÚD–íÁ`29êõ¶í¬Ñ zPÏ6")ò²-ÁÄÙbFïsVOÎvÖZ\,¯†2$¼Í3Œ‚iØÇjUœ­aéü }m’»³Û¸{0ÞëWG•ì ìÕ<'O_XôùógÛ¯„°W–"‹a“àT;Ç¥ê„gYC"j¶PR‰¢ŒŒªï?½óÁ‹‡wOövzWWšDp’s ÚÓÎzZò•é£ÊèA¡²Ã ÙÙìèîÃw0\ðû’­YV¹XlWký¼W8=¥Rˆº²Ž.,%—ƒt”ð‚¨HëIÊeÕr$Œ¤há:&dÉ,6&gÆÈùãɤ\Kй8íqíµ•ùô× Ê⬶׾NèÝ î„P5FšzyfV¶—ƒT0)aBŽ5›¼7ŒÑ¹jD ›5{pd|Ii9ÌaR#ß»)fœ3Jò¥¥˜Œò‚œ´D~Bo«õ³{RßRk{q¾èGŒs“Û”\…ŸV‹3«~ì¶ÏôÆÉRÚxÍO^ðá!lþÐЯ¼¶vұÎØìÝrz7äâv0­_\LDR²/&}q!¶àÃSBYÈm™õ39¿Äíõ´~9@­GiRð’ýÒ"ß•å„?ʤi[¶Z¼R\YûCi4£ äüzV ­¬%$¢E˜|ˆt¿ðªO©"ZÔJ;מ5¶î$ä"n6Ór…±[¼ÛS Óæö}?©q%£U¥ò4)Wà³q®ÄxÕÉýÓ׿íN/EèáqÅ­u›Ém£J#B˜¾ùcéýùÕ±8ã%ù +ëΤÒãŒxi#€aÒŽÑ®¶Ä†QßáÜAJ(h¥|ï”1›‚ÝËM:7µ‡wÞM­y$7h³ÑÚ}NgBgíùrZBq“Èö“J#­6dÀ–­‡›7^&ÅÂÕ@µŒÖŠRnJª Z3F»ªÓmMïpÞx>—¸=J*mDÐövwâL.”1WìZ‚Žâ*g¶g†Éí`R §DÁê’f'J»°ýÙî)Œf1sƒ"'mBoB¯Å‹ÒêáÌ|’í”PÌí´ÒZFÌ…‰ù ",…ð0¦Æ˜mö­Eêpp*¸Ò¤Ì®”ß\KÊQÊKpÕ´ÔŠse\›?e³ ®ø3¢;¿d¹žâƒ˜Š­´ÑA´¶Ÿ2v/-¬­½‚~ÂŽKu¥y-î)¥zí’ZŽð„Ú¤ÍVˆÈ&˜R„(†3yç4_Šd²‹!‘«JqK)lEHw)Âø7”³!;ý¦\YG¿x%º @Ãi9Á䃨%í0¦¥g-YƒÂ•‹›bi‡-n3…mDi^ZÍdì1aõ²µí“û—&׃L6)—â0¾ÕmèŽùtî¹ñüf›ÆŽ^›ùIm1-ú2vÆ”6ž”vÞpÇ÷ù´±&•í ¥=¡|€è½¸Xƒã/A5ÅÒ,Hd¸ÉyzûŽ\;¥ìùdq6@;ãë„pÊvåòÌéå·½Þ™Ù8°ëû;·Þ£Ü>–íºÓ“wG7?è¾ëMîVf{à–7§7Þ +ÒÙ„X„ÒʇÙÞÒìiqói¶w–æÏœ²îˆ4ûˆÚ€ú¤òSÚ vmßù˜¶{CD€rôƵÜøq¶÷7'1®†®á‹¸R ãJ8£,&¥µ´ÍX)6Êõ(SPÊ;z}?)º1΋užÂÁt@mª{Q®ÆŒÎì®\˜Âþ.Åù¤\Eõ.fŒèÜ6av@Ä¢¨,»=17¡ŒN”°@ô8oÓh\˶O³­kë …µÇœ·Eg'ð[!:·†Û—#b˜Êëµ#L®_ q.—±ºL~“Îme¬¾7æ ¡·b v0¥4¤ÊaùÒnÆ™©<ëmšã唥gJÐ,ðýRq ¡ó1Ê ³ b­Døù̽¸²i¶N2æp-Æ®D˜õ¤ +}º÷Ê‚ÿ‹—C¯úÈ]âÜ ©¸Cš½õ(ïOJŒmÒŠÐ^˜ÉÅØ<´çLa  ƒ;ùþBJ.V[®nÇ¥öBˆ¹—A¸(ÐÞíÉÝoRÖà擯|öÃ?Îw/GÅuܦ½ÂÞ­³+ûoŠ•}?n9k N« eÁO&Är„+Aýxƒ{ZíėШÆÕÅp´k!*.#áLÅÚ©;~ôÅÅä#­0é„3^ŒÊÅÙRwÃLI*X3_ZYϾe'ùbJªí)¡‚ˆeLk’Δ+씦O:§ïf̆Qݧ+ŽoBQeÛ‡BaÄy}Æë—g÷µúíô3Ù¾Z;ÎO秕ꪷ×2z\,HåªÕp£C{›t~1†pÀ;Ok³sÐmµ² :Æå7kÈw¤ú>ïõ'GwÎ?KàbðY©°QÙzÔºþ¾Ô8ÖËÓñÞù—í§•ÙÍ çe²coô ¶ó¢uðV~t#)xKèÔ¢Ô¢¼›6êA.OyãöÁÛã7ÐÞ„±ÕÙcÂ$å[Ü)l>V'Lnf4¯©µÝ‹At¡ŒúÔjΤòf„¯¦ÔŸ›ßo¿}óÝÛï]†¶MÉ”9lÌ^ ®,·Î1kDj§ü°Ð; elÊêOÞ~òÑž|åGãóøònŒ+IÞøƒ¯ÿD)n^I1®*”Ž²ƒ‡õý—›·>)Nájµ»s/×9XL°AÚÅí©Þ½7¼þ•ÖÞóÎÞ3Ö!rÃëßnˆpXoÊ•vIwê—Þëî?‡ü"º»w‡Ío1¹ ëQ¹Eíí›oÝ~ùë I¾¬×Žõú¡XÞÆ­!ª¶|…2•Ù#øû…àG,©¼¥·ŽœþÂäîoTpk»ýg« q!@á0jÕëtnÆ‚»éý´Ò½íÒƽç_SœÞ¿eíj x¦ÖNk{oí=úx?­Ý~ý[Zy+@XAÒÁìÍ[ˆ±Eð&­q½²õæþÃßؼóÉí_»ù¢¾yƒÎ¶¤üP¯í–'·j³ûµGVÿ”ɶ‹ýkð;0 ¤Ó…Öc…6‘«û¹ñ9®7ÓJÌÎ<(m¿]=|Û›="žUœnßþÀlíÆO®ì:ý[ã“—§?=yý×ÛÏêvgóNsë\ÌOœîimç©Ó¿]ߟ¾½ÿ軳ݜ޾)–w n½™”JðCs¤ÔŽ©ì@ðz½´;ˆ°¹yÖ¨2 Øù­ÚÞƒBMÊ•=£{6<{99ÿŠ»ñ³'~:¢\±0ÎOn®gx.ßì>én†YÝ3¶°Ìy%L#R…÷FŒÛ!ó²x T¯iͳycì8mzÇBn@U½¾]Þ{b÷Oî‰7¸éuNã¬z¨5Ž´ù(ÏÔæ¡Ù½ÅWO€r£s¹²íCÕÊäV}÷©PÞ•Ë»` x˜Ö¶ê»·ß#Œ&¦TÇ×^¿óîoî<þZçäeqó±RšA >¾ýòá{?v‚/ÕÝúγÚîóêîãòÎÓW>þµñþ£ãâJ¹¼ý¸wò~çèeeûÆáûDv²S­¡lµ¶¯4ŽÄÚ~Æ™p ŠËA>F塳p½·À®„‰… g+¤5✉ìMý)ùÕÅ0!ô¦÷SrS;~ ò”·sûãl}/˜1ã|*!J瀺SR•0{jíÐëß„^¦µÚæÞ¹ÛÞIr¶è +ƒÛgïÏnXÝz T·Øl‚acï©‘ã‚Ç&^ÿ¼¶ýfnòÀ[Þäz× BäÊNvxno±Ð>f7ß½9¸þ\ÛIˆùÊæ}¯w½»ûàÚÃG×_jµÁîNvv÷PfËkž<þÚá£oLo}8¼ñAqó¾èÖn>xïäþG1Ö Ó^Æêqù)Ô@¶G¬áFÏkîÝyù™QßP´;×¥Ú±Ò:+n=ÝúÙèúûfuÿàüÃîµ·«uOØê1în Õ«{¯´ù”r'ŒY¿ó⛽ýçó§ùbôPk@8CLmz7«ã;;ןöž æœ/K½‚–Š ´Ýã7ç²)æíö¾7ºiu¯§wÖ À;„*=‰0iw³ýÞä¡7}RÝ{'7y$·Á€n>ÿôð°_àj¾Ànç´8¹ŸmŸ¨åM ØŒZ?Ÿ?·ê ö|aC¯îzGG÷>ÚºþV(c‰Þ¸wðlxã=¥}:à=¿É8ãÓæ›ÛQL?ñ:½œé•m¹´K¸³õŒ·”P¹l7N;ͽ§Û­së#k|Ïݱ·Iw{9®óÙAoû!¥·£9ÅØጃIm6ÒD ïÒ:ÉXŒ5Œ1ùË!.€9´5¥¬©\9p +¦A;-U\)Ld3j}><ªDXQ넵æ¡RžÙNŒ2½(¥×wÞ ë!ÁÑ€ýëlië’‡Òªl>,o>劳´Z[&WjÍ{¼Ó%ÕJ}çaïä%ÿÉÇ7¿RÝ{Ê ã¼×Ü}„*€4ž6hÎØmz½k”ÝAÏ(­J¿ÐÛµZûÐÝD¶Çfk^kW)N–â i´á#QÞ‰ð6_ÚÐÇFóÄjêõYŒ±D»}öì׶|Mm]3Z×\P•üˆ·Ú‡AÒr­8{š›>3{wœç|åªh-­¸åãûäûǯúдTŸçôñ=­uM«ígk;áéÇßøዾï§ra¶ˆC©vÒ9yëÉwêÇïC\=¾ýÖW¿óÅÁé:ád¼mÌÝÅÝòö£»ßtGçQÊÖ«[jeëóÙ}+«i©F[ÔéÎ{;·Þ¶›»Ã½GjiFZ=ÚFØâ:n‚³y–Æ÷ÄÜX€Wy–1Ûí­‡ƒÃfóª4 µQè[µ-L©qÕ#kx×Ý1:gà¡ÂŽY»}s9ÊøÒ2®w˜Òѽotnâz+-«›7Æg¯ƒ›,DX¦cF×îßÍ6Ž¬Úžè ÖÂvc/Å•Wã +TÑ•°°–2“\Ùmß*ž0Ùa®}äAn ­P{)®1‡q¦Fã4ÅW˜·}",òpäÉìÉÏr#ÌæÓbžµÛþŒFeÛvïÔhiÕCà±üìæ „¡7÷Å궟0Ó +Ä¥>auA÷ÄüL¯BTOÐÙÊävyãÜj ¥-ÂíÇäRR©Y­kàò ¡”KŒ3àœÓØËuÕêÖ¦fä¢^&‹Ö +‚7€,“Ý%Ì.¦7#Yoƒr†>LdìºÙ=¬lÞë¿lì¿XÇ”0*Iv«¹sž[©ù¤Ñ/OoWg·+›w€‘HµÁ{SÐê+ +ºÀ¨ïož½ „£VvÀ¿à/¯F¥8CåârœŽP–\šäÛ»õáÑÁ­7úV˜Î‰Å}¹|,æ·!æ@dHë=­´ÓÝ{<¹öz”ÉEÙ’Ò¼ál¼®un Åm±¼‡hì›'o{ÇÿÃLQ[ùÁùøú—[»OÊ£›^{Rć_ÿQ{ö *ÔõΙ=žO«ž›Þ3›Ç)®8œÝùÎïýõ½÷¾ ŠÄ &·¥7oäÆìîu¥´)»ƒý³·|Šé­˜PH¨­Œ5 ;”³¨Ý$_±Êݽû¨V‹Í_óaóÓV¤ÑÌNÇϬKju>Û×Ê°Ïú»1¹±–˜gÅl´ÿ—ê—ÖÉ¥ˆ´VÛ|êtnBôKðu£°ñÞ§¿}íü Ë©‹>"@W¥—­Ÿ8õcðMR.‰¥-HIpTã”-æG¸Ö"ÜiBíd¬A¶}Ëh\7šGx!ø3ÞÌöSJ=)×q«ºÝ9~‹/m^‰1¸Ú%Á̶Õ9FÕz‚+$…BF«ºý3ÊjƒL)•³{=?}Ô8|¹}ï“òÖÄP ͆VÛZO)1ÒMòÀN¤Ô@µ6‘íÆ8[/ö»ãÃÍ„Pñá¶?“¦‚d øŠê-øòío²N¨˜ñ&\i*œP«¹ÎÉèÆ»&‡Je(ZBmB×[Í=µ¶™/½çrN/Æ8¾”HšM½8®NäüðJ˜»æ×#)Ve_ÖÆ8Kt»n÷°wð¸}ðÔêÎJkò¹ ¢¤Ä’^?hï½>:û˜)‰ÂRT2Ë›zm'He¯ÆØ¥ëÇlT©~¼ŽªQÒjÌî"‚¥øBŒÉAÔõÚ×KÃûn÷"a„)éÍÊ€Œ„H ÞAÕ|mgïÎõ­»ˆù‚Ø[Ý„0uåŠ +€ªÖ˜ÞÉ·Ž@9o ×sÃ;íÃ7­î­™_‹ËznÜœÝ.Æå&åŒ!¬A04jûÕûfu7I;Ó½‡íñéç39« >9«uðzïøí´Xõ "Æ•Q¶´žPøµžÖâ|‘sG’×w*lþ4™Èy›V÷NÏ®GÙe&šVIÁ˘=Lï&¥:"ÕôÚ¶€¹MÍ×Wš•gO!Ž Å­¦G0}>;ãHÅÍÛ¼âôIÐqû1å £`R­¤XŠ‹%27JÈå ø¬Ö°š´˜Zé+oÜURuGªm›í}®´ŠÿÅÊŽ&êQΔÍmâÆ .Tƒ´ZžZ­ƒ—GÀ÷™bZ¬‚#ƒÚÇ…ùŒñbqÖÚ"mØ°ÚÆynp 0CÌošõ¨çéÐF“ËvæÓñ1yÆ3… ‰QÖM‰^ŠwµüX)ŒÖôJŒ„˜y9ÈÄØ2­R;Ĥ +Áf‡ÓÓÁî=.L¯cf'­µÀ­8gD_Zâ̺YÛ’ +C­›=ðu©,”«óù¾ÒÊÆþƒö蘔<ƬYµÍüàZcïa~xc-¥CZ7$Ä:%×/®d–#"®·æqyzOÊO#¸¹ælÁlŠù×–’—×Ñ0ªÁÃK,îxÇRã–TƒPPR¡3j%%ç*dtãR#È–¡üH£Be«±oµ¯ƒ™ÍS"ÛðE?é®&E„÷¢´îV˜>(ÏK¥]h¥¼EXmÒ¨•§•»1ΰN”wÁß·î|xôä›Ã7¹Ê^(c@5»g1¾R¦ + ]jygçæ»Í­;je³5³F· €C2f¾?7¼[Ù{  …” ¥ÞZ¡r5)T ¨2FWð¶`gA¸®IL©“VŸ¡ÉOCl~UâŒ[èžHîðâjìÂJd%ÎB²å¸Üˆð%V”q¬Âm)ÎS΄+îˆÕc€º(|<©ø öô¬úF—#”éËdSbC«^‹{0¬‹ zM «—CŒ1âBƒÈNöÍÒðžÛ9½ä'ÓL®<¼µ½š|iÕà’Œ7CônZmøâl{z;ß> V(UäùÒúrL ô¦^Ú\‰RWüè¿Íÿœžïr)DyËÈ|¡³ºcUwC¸ÙØy ´iõΤÒ:Ÿ™S] +‘(mi¹Ô|ÕPi>ó^=˜kiÒ!ç»çnm‚óYÊj„è*lÞ/m=5ºw¢b¢Z×6ïËåí$Ô°;ÁÍ—›ÁÇç«Ë1¶[ž<ø‘kKIb8ªuÒr]«ëÍ›à3‹6É–wƒÐû„Þò;Ó¯DØ +(ÆϧMôZ„´£;Açh£“–ªO—RS{;çÙƶ×xoìônjµÞ›Á_/ÅÄ×–S—ýZ«/¬!ˆ7û•Ùãkoþ wú®Tœ]^Gbi¾:º®§P™ •¾´FÉ0¹Å0=ŸG©¯§Mpð×Ö0(ZøW”Ï1jaZËSzŒÎR˜þÒ•è¯^ ­†9”-ÐZÍ—b—£d’ÍùÒP¢möi«#òWýƺ(g/Ñ««è’ŸÆ$ðP.–æseع|°Ìd3FºZ7º´=6ê×Ìö ®¸¹Žê}™p&›ä +¨\N+U>¿¡T÷Èl/%äüq<Ç“”ÆXMÈ•ùù5šwª»Ï#Bu1J§hU4ËR¶ÆÙM*Û‚QÝ|¸ýð…‡¨;Œñ%ÞîÑ ž¤AX½ÒäþÑÃoíÝÿ¬¼ýÖZBŽ¢ª’±Þ8%–¡ö({Bd7ÜÞ]µ|góÁ/Ú]Á‘zc¾ò[\ŒˆëˆNØ£üø¾RÙËÂèºZÛ Ó.([œ+³Þ,Û;×›gr¼UŸ¾ þ—+ˆÕ'ò3ÄÙAcûA¾Šˆ-?,Œoƒkó…= +FYï°v?ß9nnÞEÅ.W¬Æ1À§QÛãs› ¾ +Öv5Ä@ fgaáÃʥ݌Òáì10€1õÊ.cu×âb”tÓr“/îí3·w ‘Ë ¥)>_Û¼KZÝ[HË­U$ {Du¨\,\YG­j–&‚ÓÑ*;)¹ºœ–Ã$§Ud»ç›?4gŠ¹ Êèár#Fy¡Œs%H_ ¼ÝϨ­Åg +q¾‚ªóuÇâla>ñ{ ©x«º¥õïæsýQsÅ s!Ô +$Õå(¿àÇÜæN{ûA’)¼º„ÂV£àYÕ4“÷'Ä+~â•Å˜/!‡3É/ÎççÄØ0“V:¨ÞD‡àú ¹o%%FâCu âó‚«í­g¬•”žTõ§yDð +ýÓÛ_-m½ ìáòüáDatTô0µÄyC©º IŠ°'ÀÀ¨M3J’”Q1GÛ]ÆéA`d+;dq ‚”¥y'JsOoìNŽ_ OÞª'AÜA¹|F©DH3F»Ze_ÈmÃhë´³Ì8Á”$X­Œ”[ ¡!\%nw±*Uv —SB‰PJùÞ!"•B+£wè,”ܽìð€%Œ;kT½î1ãô}˜Æä6ËÛOú§ïίHNF2N©µçu|˜E˜copo|òööí/·ÞÔ›‡\7óÃéñs¯}àG•µ”¼–”¡¶ygDJu”ñV#ÕTÆçЭi±jÀ,\íÌo´Èm +†q³Ð9-ŽîÐî”È“Bu%!Á8fd`‰ë`˜Öpz· ¤ófw˜²èÇ3¼Kˆ..8NcŸó¦ž¢„Q›þ´va f8c/G¸åQz“[ÉùœÒ&ØVŒÎ¯DEð?n®A¹fløQ8JÐ&zyK«ì3î¸Ð +áPÜr\õgœá]ä»'²7Á§RZ‚.„è¨ür¾˜¯ =;ÉØKˆñÅåÔÕHñwšm]ŸÝüÊÆm¡÷ÅÒÞjJib³ÊhÎ$ãÍè7CäÆ|ÍA¹ªTO”ú 2»‰¥ÅöÊr$œÑIs°–Pý)“j¼3$õ¦œHnt œ13zðU¤=Öê§v÷œö¶áØ®&ùÕ9=~ávŽ10Y¾ä'óaº€Èu«q€JÅ—wÚ'åÍû•ñõ|ï„Èöü”,!{#¯±Çg;‹a2€¨ëI%ÁUQµ¹.®á Rrz…ÞqU–çÓXé„ÙkÌ7w^PÖh)Â#|aóäÍæÆÝa§¥:ø¦\Ü‘Ë»œ7™ß¦ÅÜÈlì†÷B€\Ǭ¸P§@÷ô~š²q&+ÕÞÞ>?Ydz‚lB¬…¨|˜)Ë•Ãõ„´¸ŽJV'×¼õ¼^]J]\ÃÂdÄJër€Œ–\ÜÌvÏ z1µ™äÊëi}1ðY”îzÒÀ¥:_Þ×:×ÈPt.Œè«~1@Sb=[ž-úÑ/,„^[L#l9ÍC˜ÆõÅ0Ä¢¢3€7檮”íÖai|£4¹YÞ¸£T·ÓB™Îöìáåõn¶Þ©l=wF÷X€lU +Q6˸©zJxû|éz¾ÿ 2º/å7×1ÉÚ”ÕJ +¹ëKÄsƒ£—·¿ž²e)PKळ˖wJ3Û½S˜>FµnŒÌVz‡zi ²>Üò¡Ù º•ïŸ»ƒi©Ç<„¿µ({2Úª‡2v„ÎõÃæþ‹Ä*GjÍ”Tƒñ +‘.ô*×Éù•…¥( 0&–vËã;ƒÓwRJw!Ä.Å8 %7 +¢´u*;Të7'vÞAõörŒ’Ìusƒ8ij0ôó{Wn(Í›)©TsÉÍ×OÉöƒÔe?=ŸŸ9Ü5Zç¨Üˆæbˆ!¥–ýi.’QbèI¨Bëar-FÚ 2¤Þ6üSRô*¬¦ç¤•'¤^½´ŠRÕ(ïf›'re/JzTMf4F+±Z!–‘©èÖö¬ÚŽÕ<&¬aL¨]X'WBDj~K•½$@ÓR\‰³G¼·§rWô—®&2R)ßÞOs΂/óÊ2¶’¶Ðì†Ü}èNî Nߣ­\¯4<ƒ´ø+ ñËf1©ÇÄáíÉÅC_\|m1Îjõîìá…¥ô¯\ +ÃRRK,ïF2tqœ±ÓBµ;‚Óf­©å´œO¥•Uófã€uzi!oPÊ@°å0iƒªCêD!|e»I±@Y=¡°M¹›Ðþ¸ +1Ä ÎOº´3’ʨ¡z!0’f'£6ôêv&Û_Lk‹1!Å”âcuŒÊž7ºšý´ÖsÅD ÀqPêq±²Ic@;3Tí&¹ +°ñ:b\ŸØ ƺX‰PAT=Ë;›”9¢¬tîZJC„r„Ê-%Õ¤ÜDô>ëmA‚ ’îrRX PB¸TáMòyÄ`æ¶_©.%Å×ÖÐÕ„Ã¥s ¾HXDë‚¡ç{gFýˆ¶{I* q ‘«ËVóJ„÷#:gÁãLqÁ‡Ï·*Îû1 Óz\nw)¡úš1ɸ¤\Z gæ³Upäs„Ñ…@M±£tÇ:¢.Fè¤PcÝM»w˜Áj×ÐõÛ߸ãT6¢˜êKˆT‹V‚0áµâO#8ßêî¼ÿÕïÓJa%&¬Ä%hÆŒ9ÑsôŨµ(!zB­®Å… b¬DÀ$€(T(¥({-Løã\Z¨cF7†1®q5@.2k’’òÀä_\L-E±0¿#K­QÎìJTùÂå0[F¬ ÙájŒWrΛ¡r'€ÎctÊ{¤VW‹˜ŸO~BêÔ<§$¨,kvÄÜT+oqNŸ2”Y “ú|ù¼‹ +@Òl!R>-x­JšmÚÄéb’-’fO,m×wŸ7_JÕîÃ`#DvÊH/Ä”Ašæ3zU¶X»'ç§@ äãR-Ì•ˆT•kLa<+$7ðÏoê…šYŠŠáLŽu¦ˆT¹¸_öc„ZYK*˜ÜŽ1•å„¾š2âl‰0!ª$Fd'ˆ>ÀÍÑ*n}чɇ.'8\ªDq=Iè‚;Èa ÛxvæKKIˆ+Îx A cQÒr3½q]«tvßÐ*{P´TìöR„¼¦Bd‘[Ji¯¾ù„²7˜}5HÒÊzRº€wq½‡ºç¶€ðýiea[K ãq.y0D¹q¡ + ÏØrq/)Ö.ák(Q aÆŠ…­¥„hDòe»´‰2Päz”X‹q‹b)ȼ¤•½ò´ÔÚZe®ˆù =6OgÇ«Iír€þÂB0„ÈêœQ'—×ð4›O0hÒ4_‚꺰”|e!”¦²™ùjzÖJ\ö%5RñÅ°¿hÆ + Æ‚ÂÅj¡wcœµ¤ÿ{ÉÝ‘GÄü¹àgÌÂøá;ßÑr“WÓ×¹_2ñ%¹¦ƒí&iª6 ê*ͺóyz•kµH­H z}¥´Ã8“ù)TŽQÙ *û<"É倈‚¸™bå†fóÈ鞘Í}ÒjÏ'aòë) jÛŸR¡ Ù„¶‡PIöÔó!ZŒÉÙý»rý†X=ãŠóÕ’|(:%ÀĤ£–…Â!eM¨ìÓ:ó cWÓ‘´Âèu_Ja³!·‡ª}øfPQ>¿;¯+± 9C€–]Lkƒ ]MŠ-1¿-å¶I!Íîì&¦T™}˜Ôæ+× ¹!ÄÌ ®] ”ÞR>?' Å犫ó„‡`is%N'@‚F’¯‚œp{!$,Ç”$W½it®%d`æ0å&àüJ‚¿æ“bÝlžª•Ý$ãíŸ08|=)5£L9)7|„s5.­#´çjÄPd½ TObEt°Z°°‹«™ŒT`&”PœÈ"|Mr&’=B…2”ú‚ÿ|i{þÒjüÊz5ÄEÙ +l!„$1m\XA˽ðÓ‹ëä? æ‹K€m¢ÕDøò¯^Ž}éj2ŒYD[ ²—V3ð§±H¥ÍšÝXƺ¸†,†¨Å0 T†Š•(áú’zѬâ¨:8€¿’0m ÈØ>Ì ’Þn½¤‚äZBÌ(M¥8£ÌvœÎúPWz-F§ ÌËlÉ•ã´Ô@¥ZŠ/-'¤WVÒ—ü„1 0â\ÁJs ˆ6ˆÒH‰L©ÁkÕ‚ÔrŒ‡ + C’J0qä¨J¨-ÀH:Û‡„‚HU©´Ãå¦i©eh|?Ð5pT\Ùkn~%B®Dç €F05NY¸^csc§s¦Õmb”ðy\ÈÙµY¡ +ÎåË ´ÑBX/‚+¥¤TD@18á²QˆRàÈ-(•¦-‡(ŒÏK΀Rj)6‡°qezA¸ºÌãCdL*B_pî¯eÖrFå +ë B¤ yœœü5¡´KXDé‚GøÓ:ÄCF?pµÒÁ14cŒÍQv)¥üû @)·‰Éuð©«!0’~ax^Ý‹Ð^Ó´ò64E×€ +£ÜJ\€š)OÏ[GoÑî(Áíö  DÆZCÔÕ”â¶`®ÀÙ•Ïså|q(±é|áJ„[ðg"˜‘bò—× ;ˆ‡Q®š’;\~– +@k > š >¸b2ZQI®£Ë¤>@ùÂÂZúª¤´W—’ ’i“ê‘T9ž}tôü·ŒêÉ«K¨?.ùòÂzìøŠ~›}Š¯dלÎu\m,†¹êð6möhöjL^L[ +(eÔÝ}òàƒßñAª"sAÂK + ©xÑz`Ÿ¯Ö‘ѹée ©1ô‚ŸŸÓÀÕ:d|,hí•”ÎBe·èìæJR®^OiÀTÀE”9{EÄp>и·’BQ«íeLøþ¼Ɇ03IÛIÊùŠe4L­0Þ`~÷o~„SIÚA)“DðÂÔüÚ^Û— ³0nÓ +)Wq¡°ž¤VCk1Ÿikèön¦¥ÊzZY +³(›#„¼/ƮǸíRV_®qÅí,ÍYÖ€Ký˜’’ʶœC¹cJÖv­£jÆl«µ±¸“›ÐtF3Ú²›?É­D)Rïèõëi¹žŠ¸Ù½œP¾p5LʤR]‹2K ò8ü.(¿Ù¸&x”ÜL«6ƒ”! ¬© êüb\kÿYïÚKe^uæÆ}ʘÇCÊÃ@'ØJŠ)¥¹âzR€fÉ(UÊèÅ-… ¸SŠ³E?j|érøÒzÑ—¹æcb Õýp„ùšRÞKPÞjLd³Ó$)Õ ¤Õ•ûêÕÐk‹Q(›U„…ŽÓš'í“÷ó›OìÞ-Ö-GÕ/^I†Ñ9h½¶”¦TðÞ›:­ë¬5Lòpyè¸Öì‘àL€ÍüËífOŸ~óö›ßܽùr Ñ"ÄØb‚-áZ;˜±™Y«O=\o'„ÒJB¾•—ÁL‰\J¬‚¤@¬"j„Í>Oú|~Šx^‡„“æË)¾àÇt@ß8åHö ß9"\Më)¾¸œL9-´©81ÌØIÚŶ1^B¨g;7¬î 1´[×üiÑίV1#€eã „…©÷À_[G.û‘ãÄH+˜1|"¤XØ7š·a–‚äÂJ:>÷M{¾ÒŸOÈUDïÖÓÚ먲!p½A»cwpËìÝL)­ÊfÃ?Å97Æ:`d!Ò!³ãlïžÝ»rqq-½¢c”ÎÌ—/î +SÔŘ"D]PûW®Æý˜/Î.‡ˆÈüÚb%)TÔ,G[­iàJY¯Ÿ +Å}~¾ðe#-å!GÉ,e4µÒæZJ`æ¼M2ÛÖ +¢ÖRˆ»äCWc,¸¿?!~éR(ˆ¨¤ÖÄå¦s.‡ùËaör€Cu2 b&pHZ¨áZ‘š0.W‚äeß\v.¬"+óÅ”µU ôÏ—9K³UÆš¬¥õŒÑ¡²pv§¯ú2—B„®ârUt{¢Û½ìƒÃhi°^2 A.Æp£•1z¼»Z~ ,Çå¶ygL©5„Ì®„鑃Ú¢^JÈ—#¼VS*¥·•ò¾\;Îx›¾¾#4¿7À—Òðìj µ ¾ Õ"è‘•„ôùŒ£.ÞJ{0šëIÐ šRv‚-* 2“s²uÛ7»'»›OcL¾$€1ÊIAðl¶1¿×÷Ï¢a€~“ +jPï8•‹3ù8]!Œ ÊÙãó{Q¦p5‘‚·ê'jù(c ý8@{ S:Xp©|)HBð·‡zó,ÌVÖP"*âSÙg³Ý0&G©,i´ë›{‡oaFkÓV ×ê¸Þ]MkP®$¥ê)™Ýð¡æ…µÔR˜ g,™ù¼¸ m›Vq¡°GÊ•å0 ûåñ™Þ¾–R!¶“¢*&•ƒ˜êG Îð´Ùuû·½á¹78Óy]ࢥð¼œ.­cK!xIÍpÆ\ŽÐ—V‘«~ +;Jd_YŒ/øpŠŒÚ1[×Ö0ã‚?³Á0%Qie~Ks%H¿ºŒ#>7 p=ÒT‘æw>̯!Æ”uÄõ‹»°§¯­áq¦ U•ê5*; + +ðíE?z5L¤iÏá^¹_K¨Zy§wôÄlíF°¤ù¥s0D΃5ÃgÌ¡Ù¾Q=’ù(SƒT2¥•´v9Ì\ÑqÊÍÖË“GÕí7îùRB]@Ž ýˆ _ÉXS¨R.¿¡ Úg!ÈBYF©&51¡¾.®ckq1ŒeWãà}4&XFFï bHûÕåÔ.q~~’'€š~ÌžVÝ·[§µ±ÉøSœäödoúÅ+qˆ·Ë9t5NA{™_ýgìé|Ùe¥À¼•˜FiCHÙ+qéÕ¥”/¥ByÉ"ØßjÒ¸`.,§£ˆ†ñ^’Ë…™|Ú9£GõZåˆPZÐJ”Ñ*Œno‚`^)—ÔG”³^°”” ⤫¦Ve˪±]ŒªðÍ+ ÌëB_NKa¶Dçv”Ú߀D‡ýÅÁ>ã 8tð…Idµekðß af)ÆpcþüBa‹+í ¥=\i@–ñ‡ã £€«ŒÞ‡lŽ+5\WÍ­!¦·¬öµêöãÎõ2Îôr„œX“”T@8Ï—’–c,x ëÍxo›2—}èU?6?=˜R  rÙ iŒ³AZ€ë­ù}t.­´!¾ÑÙa˜)BÚÂŒqFï"|r¸\ä S³sÔ9xÑÆWB´·N˜1©&×N Ûï*Û ¡ + ’îdz‚»i·Ï g'Æ7Ö0{e~ïŠ&Ìùórü#°*× !+-ňùI¾,—ç_®¦”\ïÔîìáF9Êç£,èØ>‘FI3ÍyË n)FóÛ„«an)"¬Æ䕨ÍÌ/Ä8—så;”; Œ±%PÐÕÕ¤BZ]Di®£8øüÖ¯ìÍocjÛ‡ªÃЪ´YÆ•À^Zí&Ïøâ!lmŒÊ­Å9\oŸÜ:¦û3&›Ÿ4wžT6ˆnãÜaÞg»gq± µŽd!æàzßéÞ “•·B¨ Å Ýš[«hšårHq9Þîþÿì½Ç$i–'v'XbDuefh×ÒÌÜ´Öfnn®µÖîáZfDFŠH-*«*«ºªºº«åöLï`w³³»h’ +Ä’%À ücÈgÙ»{™Ë00ˆ¯½¢3=ÝÍ¿÷½÷~ÂÌ¿„˜çÜíÌ¥Â^gï+ÊhJ=€,È`PMjÓûõ1©ÆØ3ÆÙ– ;P$wüdÓ¸tÔ»/•NÝÜäQzü,?~²d%̯„8 0_´ÝKUôpf¢WŽ ì£à¹åh€UÁúÅ Æʇ x<°â +(›’Þ—`R…2ºÔò%ô( Rãíž÷»z-ÎfW#À,à­œ°v7%—cT&×<´Æ&e…ÅBÐûµîa{j×O@ì1f“‡J°»!2·‘´"$ ŒwãÕ>I™-PžLfÀ¹ÓòÄû*r«ÿ .Öü„Š ï J¸V‡ +…C©|&äwà8¨˜g ÀÒ¶Ý8œ~›<à ó8—÷¥ Hª‚%·C”rÅé>.M_kõݤ÷Õ:èôUº [˜åòJÅû¥*Ò¨Ý ÒkQz0 T¼Kr%"–I»§ÕIg°–Ð0©¡²2‡«u\«ÁÛ){î»üîHTDŒvµÜ4×9ñ®cÊ5³zà´Î¼{ÔË»›ˆüéz,BXlº]“*Bé(ÝVÜövàb¬NŒqWÂlŒNC‘€¨‹Ò™˜´æÁqƒa‘rïvA®ïž}¼¥AñY ‰UÄ€Ô@ éå­0ƒf~L,ƤæUÜÂs¤9)ŒŸºí£$—M²®Õ<±óø×ÙÞ%¤i=®¬ú‰¥°Ðð¥l.3>|ýwõÃohw +Z}+®òéQ¡w=û_ý$üÙ&™RZí‹_õξÑJ ˜'0¬›î„ˆtˆÌ‚T m€²—'oÿÞj^ðùYtµT‰ +å„\ÇŒiM wP©nVœÓŒj2Ndq±¥ävëÛ/¸ÂÔOÛvm9>ý€j Â꤬VX,)ƒl Úîx»B‹E*=*MßU_Y­û˜\ ” +Š;°«K¥4 ²Å <‡Èm­tší=WJ‡›ˆ"ï—"‡×vç +5Û~Úñã&£5¢Lf-ƯÇxT(ZóLÿ1Ÿ›){5&&¸<<ÖbR”-°¹Yfðhpö‹úÁWRõ0Ī´Þ`özTñ{7«—¹®×N–׿¯.ÞE„ée{N÷J.î ry‹°¶›OÌòN’÷À•ë”ÙÍ´ÏÎeRéxça¤8`µ0µš”Ë © +–Ð[´Þô¬f$…’ì}ñ…÷åð×f€‹çA,e‡)­J;}µz”]§;÷£L~#¦l%”‘¢jŒ2”µá6¢"&–µÂB).Áým%E¨tuÄüfLæ3óöÉ7zóTÌ.kè­I~Téí%Yã3 +·F|ñÈî^W·¿ Kˆú^NWó‹ŒÝôaƧf+eKÅ=Pˆ·¯·wEج,ÓA€€YM¨ˆÚeìIuöªò-"Vïl!Ì‚r¢œ”ýJ€ nL(Ó¬Þ)m´B”›>Z>ù}}ç½Þ8óáùÿçŸÈüÿ=ÿ¯Æm 7mÜrÓÆm 7mÜrÓÆm 7mÜrÓÆm 7mÜrÓÆm 7mÜrÓÆm 7mÜrÓÆm 7mÜrÓÆm 7mÜrÓÆm 7mÜrÓÆm 7mÜrÓÆm 7mÜrÓÆm 7mÜrÓÆm 7mÜrÓÆm 7mÜrÓÆm 7mÜrÓÆm 7mÜrÓÆm 7mÜrÓÆm 7mÜrÓÆm 7mÜrÓÆm 7mÜrÓÆm 7mÜrÓÆm 7mÜrÓÆm 7mÜrÓÆm 7mÜrÓÆm 7mÜrÓÆm 7m þ"#·þOdü—~¿±?ч§ÃOö'~%Môáï¥áÉéôø“Ü'þ~R9>ÕãÓÅÁþðør…§†¯'KÓáîzèO¯\‡­çóÅ>AÖÑuäãÿ—ŸÄ) +§(â$Š 4ƒ¯S4N'0„¡(’¦è¹¾ç½ˆN%R…‘Š8ý_´ë½ˆøøÇ!HƒQß…¦(”ÆQüO/úG÷Ÿõ¢ôqÛŸÔ?Ùÿ¤ø ³ +¯7êŸý§¸P/®Kg.v§ìŸþ¨Na1þÃ: v들>=_Œ§%K]oä>i{oö–òO?ÖÿS<^´)2A‘ëÝu A×Q„ôÞ¿ýŸ÷oFÿñ]ÿñÿéõÌ¥>N_÷Þ‹|â÷œ£—{vL1úI#ý_¤”j”ÉÄù,©×)³Iiõ”\KŠuÂÐΈÐ[˜XþÓ–ßJnÊ9Úî$Äb„u#Œ·‰¦Tp¥šd]Ö¨ ™®Vš‰…)“Sη\ve³¾$ï‹31B'Ô2çö¥â‚/.™ì O÷âJ)H¥ÕÂw»¨TÜ™VÚ5ë'¤=@¥•™`Z+ÎÂdS¤Õ£ížVÝMjuDõök–+0ùn!|QtGVýP(ÌÈt7Ìf¹”r„Y Ð&nÔôúÙ¾HŸ +•DkD„|˜uê~fpe´OõúQºó€/,1«b]Òl§´zœÏû Ó( ÆF—Ò+„^ó¡ +|&×CL6Ìä¢|!¥7ab¸Vg¬®5P¥™iÞ7ªÇˆÚ2Ù­”éÇ­(ë*ù©„)+L˜œÕ2#RkEH7ˆ™ 6Ë[lë>áóŒÑ  ÆZ„™“‹3¹[ ¬iq£—Ò;›˜$œ8[@„2*”i£ ‡Š3ù”܈ÐY¦ÁŠÅ¹b”ÉFi—·û¢3BÄrœ-úðô½¿á7“Š?© \–J$¬!™bZ’q6cÜÝ ±‰È!Ò†·Gè\Jnbb5Áƒ) ²@­›ÝŒñÂö£Æf\N°Bë¤ä¥÷ØôˆK÷kà »¾ZR(sÎX)/øì0DX!>"É°\Dm©l·)½¥·•ìeszqNY­(Ÿ RvR(2éëŒI¥¢f;Lñ£ +åmŠºËº Üö×ÛÞFÞr9Œë¨T”ŠÛv÷\©îóyÈc/"Bšª;÷¿-Žî'ÅBR(ˆùe¦ÿÌh\f?&”"Þv™™(í„);Lyv J þ).Tâ|ÙŸ27“Þ|:ííId6b Ub~;ݹ*ŸôÞ&Õ|„KóÙ±Þ<ÍNží "3!ìmvçÇ_7w_Çär´¥ÂNqúÚé=!Ò"=JˆeXÉ8›Cøüã61¢·ý%ãB"ÖcÂf\òö„U°Ú¨ÑŠKÕ„TKˆµ¤ÒDµZðãÎA„53ÞÖ±a*¦œ îíD¥Ò)©˜àÜ0i1#Š§cðO˜ …„rå|ãØ©@]m„9R݈ ++A&€q¨"µMÛcTé"b“·ç¢3 ¤Ì8“MòE\mB $E¿N[C¥°“ +wÂÔO8T&kvew*9“—»bˆ%½o2¿ãOmD”qI­CÝ”PZ‹p0$—ßJÊ~LƒˆÐùMÄ“.TiB®‘™iR,†05JÚ°1áŠFåȪ•Æê;ÕƒÇïþfxüêÏÖ¢Bnæí®8|@9ƒ al¢B”ÎúqÃûNl" - å'¢;Ä¥-UÇï¤Â8@ÙëˆLȵtûÙºÀ• +&fˆúq_¤ ¦4R5&Öpk(vìæ ï Œ#»§s"•g¸Õ ¬.ä—ÉL´Êngç¥ZÚFÄ¥5Ôò¾\="¬aœ+RÁ!B!Ê8ë Á‡ªPcn˜ÒLuBë¡RP½ïN&H£rI¥ªU«Ë/{_”¶ŸÄ•<¡W¥ÂD(͸ÒOµúYyòÜíœ7gOœöQØÛR§ ¶»¸ÜÕ»¸=żÃVX«çíšT·P} 5PÙÛ$”2WBŒ/©¢b*ÙG¦·h7*5’JÓRåÑ뛨ë,ä—AÒÙDô{Q)H¤!³ÒŠ1€)øg³Q"íË›Qq+.,ÄÈŒì Y³³ ¨àùÕ»á×¢\ˆtP¥ÁgtzÎØS%¿/g—*Kª o/9¡œà+¸ÖÃÔ®[ÚÍû˜Z¿"×ãÀ 0®6â +`Ú@<*{ÇGÜñáDNÒ.®t¹¥ 1*Gj}ÁêVpF}3)­' _*ƒI5šÔú ¡ˆ ù$—[ 3!LGùctá¸ÜdôŽ +:¾†ÿùzŒÍôÌÚXpOnÆ„JRªúS øã:`j-!—£Þžb%Åô–Ï€ Q¹B{û•L’r}3eøqÓÛxÈ©Å™·kFºƒé”ÙA”¡w|1Œ›VeYœLGø +Ÿ™¦Ô*C3V„¢Ðiú‰t˜.ÒæHÈ.X»“˸ZÂbì6›ÐiïËóµÆ©XÙEô†^;Ы;¬; cÒí¦»—ÝýÅésL«¨t€0!.@Ô•˜°…éRvÛí=±›Þ¾`é¾·A*0^… ø1cQÞ&לǤ*D +¬+ ¥ª}Úžëõ+P1oì>¿ð¥¬¤¶™4p¥y_Ÿ‰sYD¨Î$½5±×Ã\3C˜áOÈ11ÁæSR-˜²7âÊVRbÀÈ?¹•T¢lž¶ú*ãO¥©tN„Eè%¼½¥ZP6A:ÿŠÈuÒª¤{…G›L…ÂŽÐ.ÔCŒÊ¤„2ÂVB¬Ñ}qùžŸŠÓ%XFhX€¾•§Õ¦Uœk¹”ôfRóc¶ÕCDUZ¬»§W䧺ãòäYB(Gh'Ææ£émŽzÀè@sI¹¹Ý·Í¸3»v8;ùªq‡¥Ò¶UZ>óWJc¾@µˆÒFÔ—I™1cv<•’2!Ý„·)g)Lçá!^òt«T½Â}„“‹ƒÖ’>ÔŠP…W•3ÎéÞñ› =ÊTXwW*ˆù]&=NJ5¨@P_B܈²[ !€¸\§µ6­µ¶&œn³ø“r„L÷m&Ôµ˜ìG-?bBiÅèlˆLo$ÅÕ(ûéFr-Êû’z(4š…rõv©¦A’eWâü&¦Âô hw›2û)¥ Ë j?L:€3«av3&oÆ”j d$™ÜZ„ùÌŸºd¶¼à€o{Lj=:D `¢¤Xºeœ ” +©wS"hìüZ˜¾ëKE xWq‘WòJ\¥´vmt½•Ì,‚flÈMÞ]¦[—NóÄ«²µ…Ý8 ÌNJk" Úõª’ætûB¤+¤;¬ÕŠ0.åo¡Üþc«yB:J:JqY&””†RÚI·Oj³'ßþusÿYB)ÑNŸËosùX&3‹KÜì~ ŠP.pŠ\X€ú¢­ +ßÛkÑÛ~ÙX¾2…0SLim.=Ü1 àN¬ê~©w!å&1¡@èMB÷vð4꧙þC¡07j{/kwV“ +H ºÉ€bD…Jˆ²øó¢&×Û±Å΀6r×: l [Ii+!P d©÷63­ûÞÆîq ê$Á¼W½]K€©õª[jƘâZ\\ S›q>€*ÒÆåªb@ï0Z›·†1¦£@Uf7⪳ ÀR¶êVRÑ.Àc·6âÂj€Œ’_B_ ‹ë14c áeãKÞæ¼R5L»0gBkƒ×€2ƒ·§ä*¥µ)Ûh~@¡„ +ó.ˆ®­„HjÀ’÷‚$@ÐZPŽìý„9„‰4Ô!7€žäüv‚σ¼Ç”zJ…µm"B1€hP«0IL(%Xw5.B¦ÔúÇM̳› ѳ„’RrÁ9óL÷IaòR.,i­.µÎìQyrôGšÊêÙ¡YYäºgB~òCÊ ÇÛΪꕽr*äòQÊh ±j#×; P¨q\måú«ó½7µ½WLaf³2}ªÕB|).×Rz[¯ÙíK0kœ;ƒ)ñ™·É#è#´¦^=°›çµÙKäa¾Žj=T„V§ÍmCn‹¹YJk{ÂÂ2vd?*WÚamñZ­Ê•Lï,!•·0•kP~àD6SZˆr@-0fßÛ9špÖc£Ô¬ÊªT×"p¦´…hs'r°’ð$f%hÐÏ¥”Ú¡> +3Pø8•Y‰Šë 5Heà#à¯Qª£+[ˆ·…(Ÿõ(2ÄäšëRz”©Ÿ°Ö(DfÀ}T“ê˜XÕš 2 ò7â"fì£ú…a&—¨Âì0´ÖLb%è½&O™=hpXv¨ÑІ§B#”ƒ+uÑÂÆÙüVB zœÎoÅU¨ +À1u[q0NçÍV#㎣Pëzí°±óV«€½ÂíAÊèù¹R9Š{\vv^f\¨.¨(›åÜ©\>fs»¨5JYcÐÛ>ÜJ%&ÝÇÔ*˜eк¤I¡¦0¹ Îéãààä +¢5½ž«’_¦§aÖ Ñ韋ñyx@îÖÚ¢CqFÇ,-bІ!ÒÇÇõ&˜î`Ò&µíBvVƒdÓq¥-äö9gAë=\lø¼.æ@ƬDØ»!ê³ÂÁ#4¢4ðB°e3)ƒŸ…Jk…kÎZ]0#A Úè‘0I>/èÍ8í|æC×£”\’\-%8Ð ù$S¬n7>ÝHlD9_”v•ÐZ €pÅÌTtg: +šÙÛ.V¬ãC©ƒ ¢:|€Òjˆº @ÈmÄä•O0E'€“?"¯F¸ °qÀ-–rÀ‹Ã÷+Qî/6õ—â+¨e§•`-o2 9LX+¡Ô_¬6‚8À˜ß€·ï¤ç³`µü˜NàTtFŠ;Üa„õZ†w'¨Ñؤ„!Ôo·øLߨ ùl —¨X=K¸Þ1ªGfí€M÷ý„¹‰ÊI©˜”K!6Žè¢LÿZ…†ÍNãRQ¯ìiµC¯Š¸Yí‡ÙÉK®¼çmËxgJÙOÛ¬;“«gB匃ÊÑÛ6¢2€–JyLJƒÀV¼ýÑÀ.ñ`!k›IÊbvè´ç™ôÀhKå}èì’ õo îˆÏÏŒ»¡zý û`­µJÈ(^ü¸xI»#Ó)¾ý‡ìD –4˜‚Ö.#B5FÓVÂœ'¼éL„Jÿi[ºõ„ÅvŠj5"l&¤LWWbsžÃÒÀÅTã¿•ât:L›1–ÓÊR¦LíCÍ-ÄXH ´0¾Ä´ŸlDÆ%åj¬=žÞ€Â@ÔPÊ õ®”ß ’é»aj UIµ˜ãíÊæV‚ÌzTô£Æ_nÆV„QÀð¢|1TÖ‚44…/©¡Ú”ÕÜ €w-&Ü RŸú0?©4&—Y»³áëËÐz%€Š+AdÅ_  á”Áèmpƒ±£¨¨e:Þ¯ «åé=¹ìCåk“jÅÀï~˜´azðÞf+ŒkB¶‡ª•(ãBƒlbšŸ°@¤9+>;ÃĪ^Û 2,x÷v â&ê®ZZŠîÀlâv'¥·¨ô0.ÖBl>ˆ×½Ê &å*©7A³£ÜiT,Ù°¤Q;1Û§¤ÑÆä§ÚÙ¬uDËé0+û ´@ù ™~rÖãª5ÀÅèœQÚË´ÎH«#¬\÷„+ 60qÓ¶‚È•Êøiiû-îŒÀ^Õ¤T <$(HeJ®gÀåÙýµD%”Ï­„ÉUHVŒ‡Æ”Kð ÷ìM„q.ðQ”r£𚽕0¶’ k”Ú¦4xÀB­E(ÅnÜóÜS–1G ‚´å\°½DÏ ‚Cø™òá”N9§< Óä"Bº¬1‘Ùž"\Ó¤TNPiRAØR”ÊÅ™<*Á@_&§”b ¥ƒæÙBd¨_Ò€Š’.èÿ8çGÄ8³•ÀßÝÙJ­|Üï˜0Jv'¯þòw¶Ð­*äý¢úpìa¡}\èì­øÁAò6)ä’, "TK:J9)¡Xœ#B Ê_Œ’ŽÌfRH²¶œéò\JÄŠSÿaCsÚhz?åJmöˆ4wCìFBÚ€DÐÒìf{W x”Ìx~ùÓ5Tð£ +ÂåS€`ÞNëm(-1;*6¦_3Ùᢠ< ®4µòAs÷Cvt¨žjRkû!b\~¤Ý—Š;™þ#½uš.m?þ™V_~ ¨ Š|”¢˜]Fª8¼ŠÁ $À݃2—³39?&%ÅJkù,e–>ÝŠAŒI¹JY­æâE}÷-“«™Aw÷™ŸÔ~²•¸ëO¥¤2ÌøEÏ/€° MR˜A÷Ž]‹Ðþ¤ÿè‹¡Ú *JWK³*‹‡Çj”Xë¼=«nä(ŸøRŸm¡÷ð°ÈZBëĸl„sÖ@£ž„0Ü $tzœr”LÏ®ÎÖQùãi±"Ø%ðnÞ†× -‚™„XVÝ”.XÐŒ íGTȼÈ{g)¥|/D÷A; \„h*s £1'Ø øÃ*¤2&lÅD8òVBÛBtÐiƒƒ·[)e+B§èÌVœ_ ÒÀžŸâÙ„P±ëJ~´Ä1J'…t ×î©­¤†+U13ÛNÜ$׳ƈ + +t#è|B©pv }#&\„ +ÁÄ<,,ð&.–­Ò¨zB¯È'ÙLœICφ¨4"•ÀùÊîŠÇO¸ŒÙ%Ô:x–”REÄbRÈ•Zûû׿Šs?Ø|®FFL”âÒéž“is¼m…3ÝuB#´ªÓ8€b‹‹Þ^‡¤ÝkM®^ÿ¯ªÓ«QžR@ëz§Ð7•4&rñPÌ-öE\ïÆé¢äL€Ð7Acðyø\Pé ÔÒ"H™+aPˆÐAëZQÖFÕ2¢VP¹Tï¿ûîï"¬}Ç!\ƬÌ1©äCµÕ¨øñLiE+y›Ì×$ØLŒ²’\nô­ÇwŽ‘›ž¼úm„ÏÞ‹p .Æ6É’\è/˜²ÌÒl÷òÛÚìñ§[Ø]º¥07hÛ˜N0!”¡>bt訞§¶¡¡”+¥‡z~J™õuDÆœ¼b|À ¤Ã©uV­Kv^€ y»8ö\á´‰9A:`Ù³c×vAC›oÅ_B$U_\ŸnM²²Ó,ò" Q+~b3&BDaÊñcÆ¢Ü ¦(¹¦8cÀê?¿Zñ£!TMImBmJÃkÖÃLQý`]ƒäZ˜I0 sk[MUhÉ;Aj-ü«aB'Î×£^o/'$„w.Ë›íLýˆËÏPµçrqoaMˆ()䡨h³-dG´f­›KZia4öq­†«5Êla +]]/N «µ‘²6ì<°-榤” +m6aùü,.WRz#Ì8PêRE).²ƒöà¡XÝOêÍ pçzfS715¥7A¨§´íÌJ[+i¥=W\‹±™iºSNˆ÷ç‚ô¥Ô{aâ³²&P˜gåÀnÛµ¹Û\ú’2 gB×à÷BØVR‚¥Ü9cH³a²àÄÁþøP)þñ*m‚3ê[Õcgpµ‘ò4v=D¬‡©õ ‰`Ó]©8£¬^˜v£¤H8°« $ú:Då"LÔZù Qêë1!ˆ©@U  ÓvÃtž0Gfëp’gsþ¤¼^)#Œê—O—~ráÄá°t:àÙ:j;Dg1¹ +Ý„rù;›‰Í0—$3QÂÛy ¤c”ÛŠÆhÒ(ÇuÁHÛÈ®…9x€L½ À½$4mÞõcëv-âç0)“ã­A5üqE6z÷Pñõë}:™Ãx/HoÆ¥ ïÌ*DdÄé©´A«„qÇŸÔ|1(­|˜®wìøÀŠ׸ÖêDiЮYÞ逬Ž å˜PGÔ®Y»ÈŸíûˆT&Ô@G˜¶¡†AF~Ôù(WÖÞLÈf§¢,Ú¨sf‹MÌÆ™Z9L*µµ¤’2Ú¸Ñ<„¾*çZçZ¬Ã?EÙ"×Q ¢.ú¥µs½'ba‘a0Ѩ +K +s[ RÞ)n®B™#Ê&?ž ‚`?zp ‘Š„ÕLÈEÔîf’¯F¯“›|ˆÔÁÒöˆË-P¥ó®ƒÛ*ºw*"F(m:=Á¤FJªA¿Ÿ†q+ê%¾ +ÕE» 'aeRjûD‹*ë^Nù·b7jˆÎ%¥&®õ¢l%ÂVRj—1zQʵ)¶ì6¦4£L—ërv;R?Â"T&‰›Á¸F Zë%øJ‚-FHgÝ+ «¨Ô ®šÛ–‘/.F1Û—ÿr-þgwƒw|`°ód·úñ’e’ÉoÄ•x„|ÇO7ñ!†±B)Û*Í9« ݇Ñ. *È'Є*$ˆ#73¥ã»~òžŸÜŒ +Ÿù‰¿ÜÄ@’…R–Õ![qÉ;ç T!º` +€Kò%´jãr-€éÀ³@d`„i­«æI¾`´†ÓØKˆ9ïÄ;¼1=WʧNû1Ÿ_Bê¡Ó­ê SÂuUJkÎPpç”Þ‘¿™P¥™©úAÎÖâ2È°e?¬FB] qDgÍ¡\\úqx±Žk-BkÇÀ˜'Å{Qv-)ƒá5ª{¤Ñ áVœËG¡Â` $.£w¤ô˜Ï ×ãtŒ±P¥DÚí”å]*bí¾œ›q¹í¸è]RIòù il$Åͤ+ÎI{„Zƒ8[Žžôö^úHk Õ½3\AtF) Ô”»ù1¢l}ߌ¦Q¥Ë¥Ç)¹–R`ž½(W$äúÎÙ×Û÷?D˜ÌZTZ‹ƒ·p!Ã)n§ø\,¥SjuS> « ‘Ô;jqpUÌ/"TšÖ Dë«Q$P˜Ê'… +(m¨œ¸w^HIÒ.¸þÕ¨àÃt˜3x|Nï°z!Üž¤³¾8ÖÌ—Ðq©žðJºD§5ãT:Œi¬ÞX‹1wýÈf”ñ + ¥5„h!PÚ )ˆ‚îÂ?]úâ<Á,‚+±fŒd ©1j7ëÙÙ3§ð+A/}ÞM5ØøOVÃw7(Q’Ïâ|.΢`$™’˜¯Ä„»~+eGXïTÌÇË=3!;q% ° þÿ|%T'¬hÊX ó¾„âK@–¥;þt*•¾VºDcœ/¨“bÒžQëQ&ë-;íAoPw¸ÅUÒÀdzÓIx»XÜÄÍ5 ~Ü"AWöz\§¯çvóH«ìÅ…Jˆ´ÀŒƒý áJ”± Àª‚9´Ê¸ÞÜ"Ìõ„âÔ)úZ‡f÷ygÆ@àÉ`bì ¡”ÃŒ ¶1¥w@º#RhŒ¤7¸ Ý×ÂZ”]õˆÙŒÙg¬˜ÈcU@dB#$Å"®·”òBÊÏ0©E™@¹Ï|ØZ„N2. æPŠÊ€Ž°%ÄDJ00ïŒÑ‚eAÄ:mô)¹áYÜ€Q±“6c’/i@k€Ï5öj³GŸ¸õʲ!ݵˆ<µ™0·+BCÅ:«a~=Ä­Ùõ0ç‹Ë+Aö^€ñÅÕ(nsf*$wÜ»›N¾ëKmD@#0†¥6·’òJ˜ñ'%_”¡~w3 *¼<Ôj$•vkǸœû‹Uÿ§+¡ L/ÂmF¥Í¨ íìCtWàÑ>] ­‡(°~ï:‡òeÀvÆ‚T¿â7QïÞ*ßGï°bƒÞj€Ý«x¤PÐ犱”qgón-ƒò&ïþ.*GY63 ô&Ȇ¤PÄÔ“1Î =ëôÄâŒÏO|˜  @’”R‡‡w5 +5½Þ‡w±p'H§C äùü+Ÿšwù’5š²Û“r!3´6e´)½)8}DÌ‚[4*»´Ù›F¨Í”±‘Í\ho¿d¬Vœ¶“|vU½®A­•ÇÕ&çô@¶°Ç´–ŸÈÜ‹)«1”<Qê€Þ(h{µÉ8CD­þ$€ß ¤À› B™ÐZ¸ÖFä&¦´Ç˜σüæ­LžW&/øÌ‚Ðû€KÐ80h¨£=¾“cLñ.°6@K'h¢´UUÁŽÅØ2gO$g¶PÝvú`ÁŒCah°„V—²#!7ù,,€å‡"LÛsQ2£Ëºœ”½[MB¤ÁÁ”»~ê®"ûx%®Š)B× \Ó›LÞŠò12 x5ÄŽÑ@c\®ø"ìŸÝñºY1¨GÊ9È>¡vÃèºøZ#ù¨;Ù òÁ­xÊ*eyW¬Â4„ì]OŒpà÷£¤wÿ*àÒÇ òðð ÄŸ­Ä~²Š¬@Á£f„)~¼A« +*RjŸ…²„ y~3áÄù +ŸšCµ¼TŠ3½¾KX*=`³Sx^¯í[í³tï¢uâR5)U¢l‡çÚäZ”/¦tèÜ\ð°ÀÇáF“wG„ÙU ùå³@X3ÆlÊÙ¡ZZÐö±„Úý,f'Íé³ÒàJÊž+!r•uÆ ’Á‹ÑF›qºŸ¤½®<)× æùüœËÍà8"³…€À3|h»‹i0è¹®”÷Îý”Õ‚R|¼ ¸·–Ô Áåq¹=?î¬@ó&Äëò¹ 6:[X°1)VI³Çe†ÐÉ\ ÜM˜°ãL“ꢻ ±Ç½{®²“ámè”.&·Rj;Hg£B ìg„)€Ô‡úñ¡Þ=ÐìP]¸ÖL„©|„Š·6b|J*'¼2ÎÆøJ€ÌAÓ Ù¨Öá‚ ñ§Œõ˜w!{-ÊCiáb å+4´ŒR÷Åx2ŠªË»´$5±N°Î—¢¸¹¦U#„ö–ÈÂع-—·ïK +DfŒ†QÝøxC)4fBŽzWJBº ÞÊ:‡ÍC £i(„,ÖÂìfp•ºã'}¨ñîžÍ‘zK+=«¶Ç¹“¬a1}3©…˜<¸Z:3æsÓÂø¾ÞÜ7¤×vÙì„€’(Îœîyqû¹Ù½`Ý1¦TP.cÕwig(Aè 4êBn,å'´ÓU+°ò 176[\v埥7sƒËL÷D­îv×;zi„=cþô!§œ;s£âð’u1±„é*3ã +»biÍmcFøNÈŽíÖ!ªVŒæ‰Õ¹¤2ã¤R§ …ju…ü$¡”’r)*ä1­ +/rC¥¼4Çjõj•6z„Yµ[@”& •·¤ ö‘+~ŠóÅ“ ÷‚2»pLÚêájø:¤äAMÀó¤T†À¡h³kTvÔÒ6hBÒhÓé^\®ú™\€֔ʮTÚFµú˜ [Å­.n÷Åâ\­îéaðVRZ9!¹ k(tzÓHéͤX•cÜ(a'i7A; ôšÙÛKè-Xù”ÚD¤Zʇàs© ¼•j¬Õ×óÛ`âî…yh=(¹(•·ÈY]½å%hju¹ÂL«ïÛí#¡8 ÆiÉ¥qaú ?$•çb~¤æí½çbaŠ©5(!;‚*@ƒ_óî°Ð?«/Ÿ@¥%äT£Ù8€ºrº'éÞ9Ýf2Ûlz€+L.$Ä—õ]½¶#— µæ…S˜ÓNJw6…Ò®Ö:7Úgvç¾Z;³£öÞ£uäò”Ý·ZçVû¾Z?Vªû„ÕF•rŒÏA;ÄÀÀ,c6rý³Òô¡Û;Ëõï&G„dsÇ,ˆIp)¥"Oѥ̸PíhƒJeRoré¡U? 3&ÓƒB5j;ŒÓIˆyÖñÙ4Ž\Ù…Ê.Ò½±›JwÖSVŒ/±6¼~*WvágB,Â+Íú~sç…;¼Ï—·íÎiº áœJåevx?ÝÞƒêbÓüà°<{n$„RJmxg€&˜e%?Æä¢?Å›e/|n8Ÿ®V–Þ¯‡u¨Ï„TJiMµ´¬Í¯+ók*3¨î‡îÃŒ(“Æ•²ZšçgJÛO:G¯×?+x/2î€0{A +¨ÇfFfe§1»Î®¤òN˜ic|ž²Z Ž(  Ÿnæ†çéö¡ZE˜‚ËÎí“Þ-j­ºk¶öy¨ïêv×Á¾µ¤ÒL(/åêžÙ:*N¯*‹ÇJy¤”éΞÙÞ7Úûé>äqÛéËPKù1› j•J·YדdVë¨0x0>ýj|þenp¦Õv¹ü”Ëõê¶Q[fGWVÿqyñN­ícJ‘OCÓåÔòèØhŸÒù9•e'×zuG)O j½u*×ÄòžXÜɶÇg_*µ]í@â€õÌæ‰X\x«íŽ¼›–>`c\Ì…)ƒP ZybÖç™ÞQaú0Ý=eóÜ„NÙŽ€Vê¤áQ6®·A_Åù“î{2R®}+ê3ÝûÅé“Üä*7º€,çö ƒRf‹vûJu©Tw +“GP],¹<Q6&¶ôn™¸6j‡€´ÙÁýÚòÅö£_”¶¯ÅòÌéÃœ*Û×­“ÏÛ§ŸfWzmé4—Ó£³·Ý§„Ùâ2±´Í&ru©W—‚Û/uORJ •«rq.·…üÜéž5÷^–×vó ±x˜níFy`ÕšVYæ÷ ó'…íkÚ%¤ŠYÝAõŠ ½b7÷õú²0yØ?ÿ0úcvú¸8º¿÷ø[½ºãëJõX«ë5HâUûè »{iÍͤ ˜IÊ%¥0rgfûØé_´>//ŸCmgÚûí+d¹÷ D…”Ú²Ûfû~vú¬¶ûN­î•†¤ÕCGYm€ˆÜ躹÷ùÞ³ß]¼û—»W_ï=ùÆî¨Õíâìª0huJ³£ó¯·¯´{'dºM˜uÖi™]£qPßyÑÚåôNöŸý²uøŠÌõI§cÔN{¿8l®œÉ¹u¤Ô÷௨’’¼€¥ºÿ®rø¾|ð®²ÿÎ\ŽNÞ¹½­:ÉŽ/Íö©Ñ:±;gåéUçømqöÊ€.Âg0½ +M¤”æÀàv礼ý¼¾û†É€rhë• ªd™tM)Mr£ËêγÊòY{ïõøôK&Ý…#H¥à­VÝK·Ï þÐR.Œ!q½£·Ó‹ÝÃ7ÝW¥ù3µ²7?ùbþ২R„…’h¼­ïxZ™]Ê…AeùØní”ƧƒÓ·°ønÿ¨µwýÕoÿXš\Â:Lïµxúc÷üCuçy~t!—v@߶·Ÿ¿þ«êô“«:éìB,_Í}=¸üf~ýK0kéÖ9xØ-T‘î?=øÅàü»ÂüEaù²0}äöŽ³«„àÆ„<˜)ˆ¦"ç,7¼”+K­0èí^‡ç˜ÚÔšÇîôÚxÌ›œ…8'B[”Y£ÍŠ¹hŸlÂÜš‡ïëŸ]fZÇ°8™ÞNmqQž_åÇ÷UGÏþæÿ;¬ª³ýÙƒ_Âcúà(ýîï†'ŸŸ=þúôéO•âÞ^[>†šŸ>øæч?|øg¬Mœ\½ÿö÷ÿÊ´iwÀ,€º®,>ïœ~ß=ý&Ó:8{üų÷?ÚsÚ,kµ­uf÷äG¶ŸüÎ^¥´šÝ:ÈÏŸ@ýë•ÈgxëV™>øüWÿpöꇤ\üdÒƒêî›Òò…Ù:­®×ï~÷ÝïþèÔvåü¤´x’ß~ZZ¾_ýr07Íó)Ñ5«‹L÷´µû¢ÿ›ÚÁîø kv¦‡o­Ú$!R¾]ž]uö^ Ÿ÷ÏÎ߀ÚPšQ5—/›;¯+ó§ÀbFsÙ^>ºþò¯_ýHµÆÎ5P@~ú°wþuy÷¥Ý;·{Ïßÿöí/þÖlÌ·C¯îgúgVû 7¹ß?{_]<Þ¿|ÿßþÏÿçèô­Q<þîò«9þë½ç¿ºÿþó'¿Ö+{/?ÿÍ›oÿØ ô|hcçEuûº¹û|xñuûøs­¶|òîW¾â²½êöãúþËìèAnô ½ÿæàåï—?dºçKïαÁzR%O*-Ùü‚É‚‰;¬ÎVûÝÅeÕÚvn}W­.óƒ‹ÁÙ ¾þ™šõvUç—ŒÛ%­®˜Ûv:gÕíçµùS\«äj‹|爲àP£}®ÔÅì(ÛÜÇäl˜RD·QÏÞ½üUe÷)øI+@è¤Q‚F°ë‹ÒøQyù²süfûÑw'Ÿÿ>Ý?³½–gu'À}'oÿ°¼þ±±÷²±¼îî¿L·÷µrfó€²›´ÝÈîCçN.¾ž½ÖÛ”YpÛ‹Òä~m÷Yçø}ãà­Úþf‡÷K¥]gxÝ8ýyvø0Ì8ŒÝhl?™œ5yðÝÑÓß>ù±¹|Uš>6 +ÇoÝ?xå3 GåÀOYíËtﲶû&Ý;Üæƒß5ÆgŒÕVJ°ªœÞ…^?pºµåË|ûpyù¡¦ò£HãÝnJ«Xåy{ñ°·û„ËtH¨‘’‹Ðwà@–g +ƒórÿ¬Ð܇µ½©ï½Ò›‡àU™Ì‘«àh´òîòáFc‘KB~j4öûÇï{Çïà18ú\vutþüë?ðf¦TÞy•ÜÁ¿ûú÷ËÇßU&W¬ÝyóÝß,￉y†´îtˆ9°Ÿ•˜è&Äœ0èÞ¼XPç×µÝWjm7Êfq­¦ä¹Ö"H(I>#¦Bi ž‚sÁ£íhÕe˜4H%ë4&™Þn¦wòl‘ÞØ/M¸C³4ìÍ.ž~ŸŸÜ‹c±8Ñ* »ydÔ3ƒ+¨FHg}vÕ>xÕ>xÙ:z#T˜Y³kۥѦæ*ý½ËÏÿúìó¿i¼3j»„^ƒ))ãÀ¡°¹­¸'u)Óq;ûNïÐì¸Ã3<Ýà³ñùWM!7(Ž.zÇ_ÎÞ7wžXµŸiD9'˜2âLÆòA\ƒ½mí¿íœþÔ_¥{Ǩœ5«óæâ™Ó>§3cÈÞÄ®õöŸv_Æå<*²íS(uB«¥SZ‚C²C£º°ê;ˆ˜o-_,ÿXš<¹2:z\$å’[ž>~û«|{b,wô ²ó¦þÝìòçN÷>—Ÿ“éŽYš<}ûëJïP*µÙÃ僟öö_—&Aç»ÇÓãÏ¡#ríÃg¿ž—ï_Úµ}»q æ§iTY¾"TÎnæú§™ÎñèøÝâÑÏäü,’;×ùáwÿ&S] R‰wZy®•A÷>ž~X^ý,ÉfjýÃå£oÅâ‘2˜èÐFó.)µN+U”Ëì_} f]D)yw=bçjeß?ÙÂSrÕªmÄY*Ÿ (OÐRn¤»ý³Çßç¦gqÕÅÍ +a6P¥j5Ž,À@»BDÎhPCíåFWéÌi6\–³j¾+¤«åá‘ÕÚ×{îèÐÃm”:ûϾßyø%—©Óvµ<:Ÿž¾Ÿ]|SÝ~fµOÔÚi5a=ë“KH–T˜V·_BF&çN_ýîèù¯åb¯3>¸zûËêì".¼K‡jsGùþEqt Hð!ÓO©…¥ËÙxíÇ?ŒN>{uöæŸÏ/¾+÷O};>ýB*NÀjLˆL­ØÕC)3Š3VŒµøtݨNaæVï>î ž¬æ¾ +íVœàzà·»÷¶Æ!Û—óC˜g„4¹h&8³Ny(Ñ>ªo?mï¾(€ü¶š”Ñè}…kU&Ý4û@ ó´ÂPÌ4уœëKù—é1韸½ûí—“³J~‚YZ«f›{IÞII9èúLçT¥Ý:•Ý1!–øt RF{§§&Vó¸ô~ùðçó«êËgq!Ä„µâ¬‘’óÀ€`gÊ“«rïÈ*Žíê̇¨œÓszÇ…á©Zœrv—K÷¤Ì Źۻž}ø*çVh —Yµ”®,ÜÆ>!—ئf:ÿÝÿúí½ $9BtÝá*ÛX™<«Lžx_Hyp^fLJˆ°°€T)­,d»ji”©ÍN}UYžÇ›ÉÔœî®Õ^ÊÈH&‰ón±}”nz\FZ5XsDÌRzðÀY.Ž`Ñî¿øYcùÐlïp…!é4i«b”‡µÅ•^•±‹àšæüÒ¬MQµà' !?±@‡Ožä{gˆäêµeuñbvñÕàäum~aÔg¨š™ ÉŒY¹œju²±a#6bf#çUÆE 2µõ@²×_c" OT)-1 ¨?éKŒ'Æ‚cƒ¸´ÎéÓA €+()d¤g¦w¦8{6×=)—–]|Q…Aä ¾„¢L~¼V]¹’íñ§gA01Jr iDHâÁnJ™Y9;Oò¨WÁ}É=2 †tÜy\TDkóê°13é æ׶®…¢•!ìòDAöaî/öï|Qʈ™ãÉÍb\õgUv·Ár6HµRÝÕÜôZ¤9iô²ÝùöÆ©É­½ÒÒ• +˜ö§Ú,p³ñ „ÚH#Â:i‘Ó¾X•OÕi%ï‰Â¥‰ã—j­Ÿ‘«]8s‰©@º–î.ù*“Z ¼>Ëwüñê¨ A؈“j\\0~g‡ÉÎÚØ)ÆåJ—M×ØTÙ%% ž ,(\:—›[’Û³ö€â`#´Òp€As±‡Tf-DkÄ.ÓÙ&ÄD±x Â$ÊN^&å¤7^Ĥ”?]«L¯4W·˜LÕì™h‹KttBãàŽXÙô#çg‚`rRf<ì™TYö¦zX¸nçR@—í¬,dkD(®Á¼:7ˆéxkfÓcfj`Ôjs²¸'‚3Q;!êœfM„HÖÎLRÂa§¤¹­K{lª2`CTç.(9éðÆ«G4𨉰º%­ŒšÝwVøˆ`Íiss|b’ós[—;k»‘ZÏÁE†í„‘ôÓñ*´8çò…#Õ˨!mÂ%ˆÍg&NWf/²±æ(ˆ „ÃøD0Ûhà–ŠT¨ì‘K ¯c­¥ØøŸ¬J¹‰Pa>\Z¤¦Fôø7jï² l¯4ô6êÀ ~`Ì¡³qt°œ¬®(Å%Ô—1ºµ.›;ˆ³w©íûÔ¶C:'â/É¥cRv çrZ;ç Wè`"^lv7g×w½~éÖ7ŸüÞ¯¼ùÖÏ~ùñgüë?þóó¯þñ£w?>sϹɣ„Tì/Q7{Z‰¥ÇÓ…ñlµWh/Öz‹S«[§/]yð‰³<±sï#«î9~åþ3÷=qþžÇ^zííçÞ|»·vrf}/^›&‚)òFJL´Šúcl(™ªuZ3ËS+ÇÖvÏžº~ßÕ‡Ÿ¸ÿÙ—·o>zôòƒgxúÑ^ùÇï¼ù·¯þôý[ϾÞ]?M…ÒVRÐÃ4죕PºN‹‘P¦Xé-vÖvšË›…ÉÙúÂZke{nûü}=÷«?ùäw|ñÇ¿8qåV¥·.¥[ˆ58x³S€É°œî$«K0³¸I¼Ü[Ëv–¢µi©ÔQêÓ³[WNÞýð¥[O>ÿúO.Üzòô½ßÙ»õ´7VÑ#“˯³y€… ƒ%/`v±{d!Y•ód¥•kÏ·æO¬ì^¿ñèó÷=þìßùõŸþá•Ÿüòø¥û[s'åÜŸvÑèⵃúâD cBüZ QŠ”é¥Úë¡BG*Œ¯¹öàw_¹ïéïáK>³wÏ#»wß¾üÐ3¯¼ýþ#ϽrþÆíÕ³7ñ`ÆàdõaÂL¤HNzIJœ™à#ÅPª•k-ÎnžkNÍ®<ìÜõSWîûÞk?úý×ÿøßÿñÕŸÿþÑç_>ðä‹ñâäa-rDçvP‰@bZ)¬ª­ì˜·¸X*A¹ŒY1ÁÅDP6É4Žîm]¼ouïž½ÜûøóË'.—gv3›¤Рß:d!½r¥1=³tü؉s{®<ôØ“¯¾ñ£?úâ³ßÿñ×öñ§¿ûþÇç_~ýÚ~þð3/ÏlžOŒo˜ˆˆ •ÄH•&#±L¹ÞY:væøé+»—î¹ÿ‘§^ýáÏ_ý鯞~íÇ<ÿÊ›oÿò½O¾|áÍŸ½ýo¿úÇ¿ÿçK?~ïÖ3o¬œ¹ ÌX¼2WœÜˆ×g8%ËUº³ó;{{7îàñgž{æ¥WøÎ/?þÝŸ~òÁ'¯üÛ{o¿ÿÉ×ùÇÿýÿü¿Aþå×?}ï·W~¶ÐÛ$[ `:I^Œ¤s•ñ©åuðX8º}æÊ͇žþÞó¯½ùø‹¯?üÂëϽñ£_|ðño?ù쫯¾ú?ÿ×ü»¯yö•­ó÷E‹“ž@g2B¼­®Úܲñù¥T±6¹xtçÜÝ÷ßóÈÓ÷|çù»{î¥þâç|úîo>ùËßÿþ—ü×o>ÿêó?üéÅ7ߙۼ +Ì!)”x¥í +þtÇŸh[s“‹ÇV¶÷®Üwû¾Ç¿ûè÷^ÿÞoÿâÃÏ>øìï}òûß~ñåŸÿú·ÿúßÿ ²õç¿þâ¹W~)ͨìža#*§Úî5¡‚“M0r)VhÏ®Ÿ¸ôÀ#—zì;/¾ö‹ß~öþ§¿{óçï¿ü“w>þÝ—Ÿþáï}ôéŸþü×ÿùŸÿùü÷_>óÊ[[çî çÇQ6¤³#j+nAy€%éÊt³·¶züÜ…{¾çá§^z맿üíg?ýÕ‡o¼ó«ßýé¯ÿÿúô÷_~òùçÿþÿñ«?¿úàÅîZ¬¶Â(M}¿?>¢Ã&BrŠ ”ÓµÙR{¡<1·¸½wùÖ£ß}é÷?úì³/¿~ýß~ùëO~÷_ÿç¿¿þû?Ÿ}íG÷<üÄÞÕ[|´AÊ8)4Z3³‹ÇŽoŸ9{ñâ•ë×n=ôà[?|ëüã×ýë'ŸñÁo>xáÕ—ö.]žZZS +Í@ªmw‡Í.óH¤7ʹ|ye}ëÒÝ·®ßzôÆíÇžüî ?ïßýêÃWøÓŸüìݯÿúïàë|ïÕ¼ðÚ['/?0±´›k/{Ŭ¯ÅË%ߧËS+sËëËk«—/^üÁ~øƒÿä'?{çãÏ¿øêoÿ§ãý>ÿâw¿ÿÇ?ÿý‹¯¾zë翸rëÖÄÒ¦jÐR– +&½þp¦Ø˜˜Y9zòÂú‰Óë›'.]»þÊoýæ£?úüï|øé[?ûŇŸ|ôû/¿üì‹Ï¿øâÓ?ýä¥7~túòý™Ö¼“’TØ©‰Ùi„‰LX”Óùb}iuõ[½Žãçï=ÿú[¿üà·_ÿõïÿçþæÓ¿þú«_ôÑ /¿üÜ˯.í\b£È£€bå•Ëd3˜¬Õ» í©…õí݇êÙ^~î•7¾ÿú~ùÁoþñŸÿû÷_ÿíý>ýÙ/~ö篿þü˯^zëíû~*ßZ±’ÚJkÌ”â¬X”ˆLm.Yl/mlÝÿØSO½ôê3¯ýðÝ?þúoÿúïÿþî‡}üùç_ÿå/_þé«÷ó᧟}òþ¿¾õø»—n„s 2ÓA¸Ú‚±rÞ¯B±Jo~ãÚ½¼öÖ¿½ûÁGo¿û«/¾üòë¿þíÓßÿñ7Ÿýþoÿ;xŸw~ù‹wßÿÕû~øàãOoœ½ÑݼBJÅ#:ÌûM¯atfÔEð•öÜæî…k÷?üük?|á·^|ýÍwßûõ?ÿó?÷ÕŸßýà·¿þà7}òÉ3/½|òÜ噥­@¼™¬¯û…x±ÒΕÉL¾1 0jq|zraeþ•½·®Ÿ¿röäÞ©d!Í}.’¶¸(ʪM˜Ú„j-˜ ã$1žN•*Õñúx{qmca}­;Ó=~rãÚ=çnÞwíÌ¥Ëg®Þ8ºw±:5ËGR´˜ó'»t¨d‚h LC8ãÂ=´×¿¾y~víD4™*åÒ«+ó»gN^»y÷Üÿ“Ÿüøƒ~÷á'Ÿ~öŧO½ðüÖÙ3õ©)_8Άs.€®˜ÏFð”/¼vììæ™ëÙæTºÒl·'æç¦Oœ8öôÓ¾ý³·?þôó/þðû·þöëo¼úòK/<õÄíûコº~¬Øœæ¢Á˜™°À p ª²F»›a„d"¶²¸tﵫ¯¾üò«o½õê«ß{ï½wþò—?=÷ýïžÛ;±¾ºPn4¥šƒðª-â ‡Ý-ØÜ‚‹0Š#É|¹5;»´wjçöÃ>ýÌ“?xóÕß|ôá—_}ù׿ýåƒ_ÿìÉ'=sa7[.ØQ3¬¶Pû GTÐÆ cI.„äÌxwº7¿°±sêÔ¹ ç/]¹rõÚ‹/<÷“·úò«¯|ïùçžyú;¯¼üâ}÷\ß<ºKWœž ÞE›"EŒ‹*¹./gaÔŠ$W6/^»ÿ©g¾÷ð£ß¹xùú£>ñó_üêÇ?ýñ}÷\|ü›Ý~ðäéÝÅå¥j££äÚ…É->Öº3Ú tÙädufÌÓ±dqyyãÒå+/¾úðJ=ùäí‡o?ÿÜsï¼ûÁÓÏ~ÿÒ¥»çWÓÅ*î œ1¸x™Ôa3DÚ\4Œ3œœ g[¥ÖìøÌš‹åM(aÅ=—GQà<´¤µ‘û4*ÓŽð°;hu±."€â>–£9¿œ²B¨vYœæñ¡LÀ-euX`ØŒéìD…m¤8jê3a> DÙQ––RÀ{ã¬Àò‚ A(ŠŒÆ£©|¶Ùé.oíN.­Šñ„Ãͨ,è°Ù¥²z§Oq*+¥±ST áSŠ†H?ÁJÁâÄ Š&b‘d*Q¬Özó9V¢Õz#—Íò¬ßáòX1"%Œ‹óÉ —/ùÍA£ÖÁpRÉÃgB¬˜+§“éHH’CÒÜÂr­VÈdbµVCNÁAâ\a“vLT±Q½sDëR[ÕŽ1‹ÇŒ‚÷L2B>]è)ñb*‘»péêÑc›Ùt¼Q­æ ʼnñV§UM¤Š?8¤0ªG ›Á/K%>J…Ù`VÓ‘x6œ,Pœhu¸hF£E0‘H—ëÍéfkÊ˲‘h’(.ܧµR‡ x¿ƒK Å*¢,P¯Õéñ°Š¯¤¦ý™lsc|éRmz׈Yí6! D£IŽ!”ÒÙPE”TÊvv¥3jtÿ¯o˜]LE œe‚ô‹¢¢DSa%MXÖ#8éör”OÊÂ%!ÖÐ÷/ðÄ8ª~ûaß öˆÚJ¨Åéu1qw ›(öæ·oèP?À[Ì—"y`$ Z=Q#â1 v\0¡þýÃú}C†ÃÇ°±£~Ìó…k´w³ §7ÚïIb%F-¨Ù-Yˆˆ •p6å ¤!¬q¸õ.ÖŽòÎaJk§`.Š9J.y¥œ&L0”g]^‘3· rÐ'vD|sPwPe^ׂû j(ÑÉÈ­¶‘&7fBuv\cÃa·è æ&£l÷§í„Àãn¿|`T70l1;ý(wa„Ïs‰i'›ÔcF'kr2ÃZxXeÕé!µÖªÖY­ÉK9’ 9 !Czˆµ# ÂŒ9«Áq1ÁܘÑÚ<*«²û XHû­„äô„c…‰…õÓr,?¦6’€é€tÒ2BTàF̃Z'äVÕð¿îP9`wÔ.µ…6¸üFTÇ"ߟh[ñàbÕpqx*ºð€ò˜íT½½ŽR¡ƒFðVv6ªàÊPÕáê¿FÍ$YodœMLI¥õHe-3y†OP'Æijm+Âûf¸ßÑÂÏÉêÔél{ö¦Û†Uöx¦k'BûFlÃFBcVb¤j8?/¥¦t&rpØ ¡~ĪLn³“· ’£?Õ­ßd’‹uÁ‡ÔÖÃó€Ö:¨µY0fx ÌM¹¼ 9ÙQ +Ó„Åø¬Í¶SQ'—î· %#4ø­;ac?„L¤Ë&|iŸuz¢ÉâB¢¶qPå8¬²j”Ë—ö(mJn`¬ L8W›Gøô€ÆqDk6Àf”ó‹\jŠŽÔÁ`†’.œ38½ äT6 Yˆ¨[jFÊ[6wäˆÎi„¥áôÅkìFgÀ+7™HÝŦŒxÄÉåÕЀۈ ÚJ|k@·ïˆaLãµHmDœî˳áA=Äê¾oÚ´¨Á0ca#?]LÊBÈ*3eÁƒ€úûôôîá~cá(Ÿ—r]«;8 †ÔBÊcZäÀ€áÀˆý6fWPˆö¬¨<¨sLj;8lÁ`Ä‚ZWÀá‰sñž/5gp (á¢u˜Kêa^cp ÛŽ¨aeÊ+¨'28t5‚J:DÒÂ-ÄÃ\ÖíRá– “ * %Õ>Ì-BÞ Ä¨!ÆŸ©tNÚ0iß#ä#lšä3>©žÖ¡wÒëŒh{êLGn§7a¥—ñFÚrqª£z÷4LÒÀGÆl'‡0 wºiÓ«tˆ@éÀdÆî@̈ÖãVÏ. eZiR“ÙÎV¨8ƒò B,b<÷F'œþ‚ˆh‘ЀÔ¡ ÅúküG.*Ñ1+!SB–ëÕð ÎiByóÄ'à@~ æL/'*Ý¥SD¨2 +ñÛIÉNö[L€¨Ö:¼f˜ZÜ8Ó˜=®wz,¨„7æË#\άR¡ø§›ù#exG,#FÌŠ÷yʪû3sL´ÓñH¶ÇEkz¸/Á0c'C&…¢‹M›1%Céò¼‹KŽ˜ÉA­ œ_6 ²Þ%òÉ® 챘ÞIK ¡³¹AR“n¡@ÕEÙL¿6̃q¶“Q#*ÒÂc6Õ›f£áâ,È©ï´1a2ø_ÈEØ„ÊÚ_tãöQ&õ­êý:ðM™HÇ-TÍhHcåFt  ‚‹ƒãp¤v0§OåðÑ NÚ¨„Í­ÔgN¥šKV*„p)ŒË!žêM"Þ”Á_ ŒYÍ0ÈDϘÕÛo°CÆÅÂZkõxs[ˆ:˜%P Æl>»;AŠU6R÷G*^±DË0­hí¬—G °£öaµóø©CA5æÂð‡ZØO²cVJm£u„å·ŒGT. &Yqé_ö³Bx¸a“wÈÌ1zîƒ÷© ns«êËçšðã +Öèäl ¼!ײ™)"X¨µVW÷´0ÑQÈ; +q&wÌN§ˆ`“I,Œ9PÌ®À-”Ê¥1.íì÷xé·¼&Å"ä]| ö§ml‹d¤q $÷Ä»Ba… Õ„®™ +a\Â+=rÑî‘Mnшœe°ß¿Å²ÌÊk Ît°€0ýË×@S@‰V›ña#ªÃBOÚ¨¹Å-Uh¹Q²¯—>l@LHÀAGaoDW@©Í®]À|ÉÃhXï²a jró— 6¥¶Q-­l]“2G-£z—æl¸è–ÜBÉääÁAÒrò*ÕökÔB¿ +1I¯Ò´’2êO·–ÎRJ½ß”æ° ¨§Š8é©S“z‡oD ; õ%÷:î<àý£°•ˆú”ñ`bÂ-di©¨GüGP8<:§fRî`9Þج.]D} û|…öz¢~ÔŠ‡FLóQ ŽÊE…†uð¡Qp,R~!˜_°zc¿ +1¹®´¶ 3gH©¬ü_cŽúÒZ;­‡<*c¿÷ H«£ÎCcÖ1³[ëð"hE1#f”¾‰Å‹!ûCÚ#ýb"0¡q—'sg}·¼'Bk£öÑ™0SÿÆbЀŠx¨,Šœé0±ê(B²² ‡?Ëeg\bÉÁ&˜pmzõòÊÅÇ-^ÅN‡=‘z 9LN‡²st¬{ÐèÆ<ÑFg› +–˜ÜV¬¿¾Ò¨Ì^H4ŽIÉúò‰ë ¨\B6Xœ ××"èøñèÄ€Øx(^¿÷¡ç+s'AJšŸ ˜  ”„Ì)5´Ö„Gú}eCN& 3Y»7ëòY¥ÃÆ'tNn@m„ÝYÑcrË'y⟣ÄR¬²D +Yž¾ø„…”Ýw¦XẕMé0I Zúóáõ® æ/ð‰Î&Ý\’ ç͘¿ß Èq¥±yA)†½q !9¼1ŸÁ„¼‰Ælô¨™8¤S¢ló¥™X+5½gõeŒ˜Á° A«[¡Gx­Ë°)”I™Ô£ß´ìWÁÃ&Ê„ˆW¤Äº7Ò"%¾c ä´bDÁו«:C2Êî…Û”þÖ€áˆÆiB ÇÀLÂtøˆÖ©µ{ > ²Æ鉀ƒsÓ¡ê2§ !à„£M9¢AÆŒ” ;=)R¨Óâ¸Á%˜½0áù²£ztÌDƒú‹Î\¢‹Éû“r~˜ÀP¥ß‹‘FTm2&“RUH÷ÀaÐþ$#ÅGLШSƒªëÛˆêÏØ™˜ÆÅcR âSRæ²Ótúßl®»››:,-à|RÎtý™ì8HÑJ'wØ„#LE>;ägÝJ‡IÌøS À™è‘x¥‰md˜Ì.Igó©mP‡åt—æAlš)—GÒ,;TóDš‰Æ†\Y±ÑìPNÐ\O¸ +FInm;¥*ª{9dÀÇ̸ ˜h‹ªôŒ”³SÊþQÛa j¥ô6öÄ´0ßß¡ÃÅ¡þ”?Û“+ËRiÑÉF€yOwŽ"Á.åI¥Î¥ºJe%7±UŸ;3»}ÂÇL8ËGRfÖÝïØuÆîQøTW(Ϋ]þ&¤ç2½³Ùé½@aÑå/˜= =!»ƒÅhy‘ŠÔœþ ÈA`_ëÞx L²)äÚ™öŠï9H!7¾^Y¾’˜<ƒ…š„Ü"ä¦ +öWÆ7N]{:œï ›IBj:˜2ì-…²Ë‘ʶ˜‡.Å©im4Pœ1+ð¹3õù kg·ŽƒÃ›\:SŸÞ1î@Z©/Ç:ÛñîÉò•öч•ú(\.&B† +v3?Ä&a_^ ÀE«l40{ÀÏ ¢ ú-:c6&GÆ…Ú1®°be3VRôFÊn©„ðY”Ϻ¥¢7ÚŒTWØXˆ  ¶!v à$ƒo¤† $µÁ©wP'TÞ—š +dç¡ìò=Ñnbüd¸¼Höàd£â žÁ²ƒV9Žò:T%T K†~×/?P7˜Mã °ÇÚ*+ý/ûGGu®!°­ÆBé`÷çQ.ƒó—'®±æTf˜@àu‘þ‘—åòR¨¼TŸ:uúîg}ñÈ*cB~‘VÆ “åÞn¤¾1æòê„Mƒ +J(-”@"ôÃÉèöŠY¥<y£àû%bâ->Ý+ËLr’•ËõîæÉÏëP?)Cå•xóh¸²k‹6ú³³fBôH¢5 Ïìجˆù¥ÖÆìô™~ \+ 3Q.Ž:˜A3a¥"îp+TÝ_½!¤:þÔD¸2ogÂGŒ.;-û’íæêù“7Ÿ]¿øxaö4."‰ÊÜæUO¬1`¡€ë‹s¡â!·åÒryjÓ#çõ.!R;Χæ\\ž5èúÝê|‰êRºµ1j!÷ [5øRÇÅb €dÓ'Àçê žO¶ +³§ªËg³S[ñö±@~“ +}ô¹o^¸e¥‚CFŒOÏG;g"ã'=JgÔâ^±Ô\žÞ¸ûÛC#"‘Ê„Ô<žš¿]¸Ì—WõT” —o?óÙ‹ÃÀP!‚/ÕKtv§wÎNžÕ9‡€œû¢ é! ¥v +X°?] +ÐÀƒrѲ‹he“Ýh{;=½×8vÿøÖC@ƒ@üì‹[\tBmgû-.ž¸‚¸PÍM_´ØsÀ¯’þ´ hì ðcD¶zúKGíÀKcÒà´LÈV4èXÁ¦í„¢¶­ôÀ…Í8D +#f·ÚɃ“n÷F0!'dgâÍUð; ÂØø¶ÜØ –²S ¦õÛášIˆh1kƋׂÊÆšþx~P0)>ÃǪfÂoÄ|—ÏÉgbõõÒüùÒòeÜ—,Ml$š«¯Œ²BaŽKOab5Z[ÏtNòéIàR¡*,©Pp.ÝECu3#@QâãûU`Qè… RqE,¯ñÙY˜‰BV©-’‘²‰)¹ÊÆš™ÖÊâÉçj­œG„t¡±púÚ¡Ü$pYJíØÂîc»¼QZ¿ŸŠOÛ˜ø°…ÂØt¤¸4ÖŸ7‚ZÝ2H­ÓðŸwuNþ Ú©¶yí¤Ü™ŒØ™$m +®ÒȶO\g”:Ä(·ògþ­Xmi~÷v(?5j§ÌîPzrÈ(­´ÔýNÈŒ”L·ÐÞ²h'#¨P@„<,ySÞTOªÌ—æN»û™üô©ƒ:È +—ìÑJöeCÑ‘cý\.’Ì7ŽT–Žõùuo¬ÇÄ&G-€ã¼.6fÂV24uüú浧&·n”.r™9=Ð÷[ÁìÜýB¸¸¬¦Œr± !'õ¨´oØft2±B¤ ¾±otPïv‰u¾´,oH¥5˜N[0™‘›6L>8hÔ`G4ø¡ƒxà¯tdeirxo5ĪúÛCHD°Üïè± !©hâÓ—¡” <ÜÆ‚˜ŠLñ„ €_¨`Œ+ƒG¿]U¿áLoº¼$ç'÷šô.KpKÕxk3Ý;©/ùåüÖÞíæâ= +s­µ{ªK×í“•¹ ¥ù‹Ttüˆ‘à¢-ÜŸ±â¾—¦c0_¦ÃJí¨;T:Š°‡hØ`w x +E›!åþêHDȃíOv0>E’Ù‰õüôÉìÔN´±4¾t®:{š‰ÔùÄxzb+3±™Ÿ<îMM¨ø ÄŒ+…ÅA1 E‰`™‰Oz• 2áëCFRû\Þ8i 8À ¹¹ÂÜy`ÀW`B…ù£ç®=ú’7\U;X;)Ó‘‰DkknçÖÑ«ÏæfÏ‚ÐÕ8!3Á¦ºFLTCÜA->bõÚH(‘ÅÑ÷/ä,îР ×£¢;Ô‹K3;÷ž½ýêúµïÊ«#ò6Ö‡Tk§bN:M‰5R¬(Ú?foëKÏ!|Iûõ·:ò(^¥a÷7%T¦·ŠÝ£RnŠÏΡ¡–3P%‚ILæf.Y=é1›Kö¯(ªþÿî +ˆ8h¤©]\ŒW–Ì×ÁÑoÓÊÞä4jXHå°ŽÖ¡aˆÉj±ý‡õ§ƒ:80ÊNEuýûJ¸ºŒÒJ47é Ã@ŒÀW–×ÙÌ"› VÖQ!o€ûvxW˜ÏÜ1 žÑÚ­5Â~€-ˆ788àKï±í±bå•f¿ãú˜}È€¼¢4X •:­“õlkÀ8áOÕæ÷jË’cL¼îòÅl^Åìîw'cå +L†ô6»G™–+'’í3•ù»mýæÏ ¼$}±M•Ã«sù,TœV&ØÄ$&VJ:¹Öš“A¸?Vžm¯_›?óðÒÞí“7¿ŸšØ¦ÅÊÎÉ{/Üû4!å,dÈâM R],¬äºg WžÌ]£ÐAPC¼)69ÉÄWFAvI`¶‹è1æ¶XàBÃ5'›ØH ¹he¾¼rÑ(\ÁDu#Û>),5æÎ4ûs€‹ˆõ… +çø“è èÜF,Lôypñ—@Þ«‘!ÀH¨ì4°ŽÁÜ\¨´XœÞY9{{bý**U}ÑñõS7a:b@6Úåç@•‹+„Ø4’c7Ê(&Tè›yµËˆJry%5¹ ¼ êÏÚÝ¢”×8<*»„ÖÌîcíÛ¥å»SSg`aß Êòî#Nꀶzõ8 Ià"•êq¥¶}Hí<8dÐZ gÿz 4jgƈp˜/±‰YàNÁ'ªŒˆ㇠ývV*êäóîp½¹|qùâðø”—@ Òáª7Öò&º¾Ü"WX…|˜V¥yÔ³`~7`@ž–ÊÀõ:0Áü€Ú¾oÈphÔ²Ä2¨ÇAÖ÷wtp&gÐl¡;¤³‘n!ç6‚…i±4oçR“˜Pƒ”¨ÂLÐɼ•I!^ÆFBbʌɈÁ¼;R)AŸAØ È+&|9Ô—>¢udŠ'à³G „P +æX¥æpdÐ-01)ÝÌôVyþL¼¶/Î÷V/D+30 ¤º‘úQØæܼ\Ýð&f­nMŠG*»ÙQ ¥lh¡ûæ•0& àõ >_ˆûU¦o éLÏÇ&‚™…X}«¹qÓ«ÔÙPqjýŠ/3*€ÆÁ˜î,î@ÙŒè$šëFrHGþ²;X³yö©ìûUvµƒ!„¼/Þ±‘²a…DÝÅÄtkN ¤Jq~ ‹67qõeŒ0¯2ÓÿŽÙUf7Â(ÕŸÁËú]*Ò`âmL,ã¡J®sœõœ‹çÆ×ÒÇÀ3v_VO„ô˜@JUjV,­«ì„ÝÞH3^àSÀl,SrSíîuªÌT¡¾Øìí·‰Ó£z4è–ëRu-ÞÞUý ÊéÔ”\{DÂa`ñÂl´HMPbð…ÖÜ¿ºbDD·'ÄâKúSÝPuC,¯g{çäÊ:›h? ï\{ øXPëúMaQk„â1â b·ÿö}ûÍêyÄ“r„T¤He|î$ÄÅàe§wÏ=±xá©ìüOz|/eº3›í¹=\Õ¹8-ÌÞ!∃‚Ñšƒtv‚•2Þ` Ü#•Ò@Á„Im£ÇÌÄa•½ßjÛÎ8úWhn¡ûÌìþd¶½\ŸÛœX;×=~wïØÕ¹7k gåƆ¥Ê3ý §|Ü€°¤Xð„[X æ‰tTf+L²Õâlý­ÓvwÔsf—_m¡Ü¾ zĬÕŸsÀ0¤Ýá­gËk§î£å2ؾ.÷wœ©ˆé6<Žùr;ŒÕS••™Ü?lUY&SR d(§Á+ÕÂĆÊBŽšú7VB…¥\o·8{ºwì¦TX¹__™\»¨²zÍ®€ ‚QÒXH âù8ȦDmÎ-d@¤Aº¹üT¸¬¬¤ç.t6îÓÝ©ÅS¯½ýÛ@¢;jó£º/µ«­»øäÚ…§(¥s`̹ÔP¹¤áN‰É%zäëyC`ÎGM$°p@§Tvï€)+;lñéw‹9µ­ñ`@AHè ¯ÍÞ ÔÉåÈpƒ Uén´¶(L‹sJkÓ—_ bãd¸œßìmÜËD›cvV -Dç²€XM.¢æ€è›ÜÃfJeóZ\¢öø(DK…Î +o¤÷.…«ó‘ÚR ´„…ÇLJˆTf–ÏN,ßm§•A¦ƒ½.&IŠ”ÏÐb®5-db…)˜ +õ/ªcê¯âPϼÊÁ/¨²Rcf7Èq ¼ÃF¢ß ¹ø¤;T ÃåLcéöÓo>ûƒ_WçO¹¥|0?-ägó“;ÉÆZuâx{f‡•sœ\`ÂUO¨Ê(ã.6¯¶ñ‡Æ\Fúwè@himÞa½ûà¨ãÀ°eXç3®û”f©³5¾xvÄN° •Žš¨0©†Ë+‘Ê:£4X•'Ö“ÕE#ÐA ª7æÑ\7Q˜:2æÒ¸`àؤG nPç­˜@øÒ|¬i„}v"„R‹B¦çíÏ©+ÁŠ›O—ÚëÕÉãÀa2Œ0Qˆ” Ɔúõµa`²‰zÃ*› @tÄ-=±›ì€÷ÜA%]Ûܽì:¬+•íhu‹¶åô¤ìÀ\f@‹{ÙDaXÜ£VµÍgu§¨Ðdké¾ÆÊMJnÑá”åÃEƒÃóÝ·ÆìƒfFç ¹˜\ 5é`¢JÔ9Ù}ƒFð8¢GÐ@V,/è#åz°´È- ‚Ü_ÝÒSo¢v·wjjó¾õóOn]}~öăX MøÓ..e% †Œø¡QÛÖC܈™rqI`‰G¬žQ‹Ç1@•ìd€”Ó¹‰ÕÕÓ÷WvŒ”@q,¯$Z;Ri¹?Ï6ÑŸÞohàKAàñ $Ï_5Õ8ž›Ø¡#íQ ãÀ%ŒÏBÅÅJ+Bx¨<°—`AñÔ9<ÃFÄèb‰æÄÊÙÞÆ¥DsybiwîÄ5o´„ñ‰@ºÃÅ[•s…ÉvOÔŒù˜Pjrét$×4dÅ‚F4dÆÂvw ãÒ64hqòCZx¸ßŽÃ5¨†tN·?%=7#e{¤žZ½œk³3B©Å;§²ÓçÀ—â¢ÍöâÙTm0;¦Äì´”ž¦ƒw +‚:¢¾y@«·z@híÄþaý Ò¿nÏTÙ<:;Û¿:dóÖBBB¹4ð ñêŠO©XÒae¥‚Ê„ŒÚM0g‚8`<œdxH‡Œèœ3x¬¶@Gô(°´?AùZHb¾¤œ8qù‰ÙÍëÀ¶Áž$¬M”V06<æ i^ zNoÒ_róE|ö&T&‚òGƒJ 8œ9¤àdNïð j‘a=:b&týRéPÙGM0*Ôˆ$È:¸ÄÄ1¡RO©¯±Janëjkù|ª±VšÜj,Ÿ/Í‹ x £zÞpQy츠±{€vŒ§[vó.R~,Ó–/ô,Äk˜=‹×‚‡Ý¡:Ê%hœ ¬¨ß±–þ=ÊLˆZ36ªGÕ0ê ã\ìÛ‡4ß:8öÍcõCzÄäò3BR¦6'%Ç5&Ì+¤ ¨r”Òä3ÓÁÒr¶·—iëN®½öã涮f4$—æ„Ü4 T61íôg O0ÑîÂ9 >Ýoç’êìÖ¯ÎmßÚ¸ö|ª³•m­¬ìÞà33\¬É„kŸú[±gP.ìȵ3f@üBöHm62 ßÁ˜»‚n_nÄHPAÀdÚÈ$e#PC2rÇ„7±`ñÅÁXY¨i¶/T{'Rå¹de +f#n!ˆÖôý=P—¤uð&§ˆRÑle±8~ôÀ°Íädl¸_Û÷'1T¬ ÁäMTº›õÞ¨“²øX…Sª´ØßËQª._ÚEGSÅ^º2§2£cFÔää¤ „Òäôé1µ ”R8ÕÔÛH‚‰”zs;×gO>Àçœþ’•ŠO.ýþ›ïÆÒµ…îcSt°FK-·X7ô7õp{ÅBiú”hjÌ8§1ª³6ï@¾`Úå Y Áe†tè¿‚èq¨­^4Ú9;*¹<1)ÙÞ»þ„ÆACŒ‚ Y*Úäó3ruE)Ïtg7o<úr¡sàp¶}©x£mÔŸ'ý™™• ¥Î£T¹éD{»0³7±vmùÔ•Å3B~ª9{âÑ—ÞñÆÛ&2Œ‹%R*cÉŧîìÊÝt ^¦u¬¿gʶÀ†;Aµ?Âd‰@yÈDŽ˜q  ÆúwŸûÛd×ÁÎtOz#U:ªuŽÅòóéâÄFeê8)ô¨òFATxÄ€Mµ$kBB—d¶³.ÐÔb Ú{¥Ü¨…4Q´ÒÎÏž‹4Ž²ñ¶…Žš d„¬;<'›ÆÔç@²×ïâNõIk£@ÑÙ(­•ÏX€ÊHè@ï¬\ƒÈWÌ{Cy!Ý®¯\ì½Þ;zÏìñ›sÛ7ªí„’l˜ˆ2q;&˜œý=›œÞ”Ô(O,R*nsœ¸¬3“åËh#Gt.”Í +ÉY@=NZÐaG`ƒÓÚ|Noš‰Œ“Á¢­;H ä Ÿ/§k gf¶¯Ïlß[^¹hc" äVçÏáÌg\|bS(Ÿ#x ¥joCñß>¤ÖšÜDÒ;x­Ý£‡ýjgùÌøúåΉ² —2³çÇÞ;uìÚ̱«ñÆj²:ÿêOsåÁ„ÈJ%o¨avqèx˪‚ºq`ÔEK%â;4f?8lþÖí¡aI •ôD;´\‹–æ½r ¨è¸;ܤ¢LjZçò‘¡¼\œáQ*ÑæZaþlsýjcãzyåZx|Û›ìÊéÞÆÉæ¶n Á,Eû³J֨؜;4ÙßÞÝè>¬†5ý;ÔSÛ=&\²yûgäÒ¬TšJ6榷îžÞ¾¾zöV{íb¬µÉ5»7Žß¹k#‚:G¾™[¬f'N”¦÷B…TÈk Ö +tÆl¤ð&Ÿs‡Z\r!TX¡ÃÕQ£vxL@ÊNd¢pq&Q›?¹Xe#‰Ær®{<ÓÞò³‘êR¤²èÔB‰úòö剕SVRø­-õ[ÙÄÆq©ÖßiÔ„;PÿêöµxiVm瀄Yˆ˜Jƒl¥0uââíÊäQ€E(ŸñåfÅêjqæì楧ö|½¹~/Ìe µ¹Õ» ÄX´¼Z˜¹\]¼‘?ëKt²aÍÝ{“µµCZl(¬Õc£ã}a--%&v O¼ÐXªõ6Á+L—Ên¹¦”æp¤&¶ÅLÂh¾ç •Ô€íS¹/ïQÚ||Òˆ*+å‘ˉñ¾¦£\ +ñ¥°`ÆJ¹p9Q÷H¹òìîâÙGRÝípyž Tecg–wï¿þøã‹gLŠ‹Nâ|Áàäª?ÃAí:<êÒQþÄ䘕ü—êÃ*xÔ@謌 c² äâä*x7@‚Éîvïäí¥ O.:™xºµÑ;z}óÒ“ÍÙ“[—k¬œ UgK‹g“3{Bu=™í­\Ù<ÿx}aPŸšŠ77"•ObÆæI ðýÃæC*ÈIG *¤w¢ao d¨$¦¥ÂÔüñ«›W »ÑæB~æd¿=Qu-Õ>]ž½¢ÔÖ-¤d#û·•A81±q.5MǦØÔ…z¢‰â”oX° öàþæ,fð )8=‘|÷t¦s:VÝà3jˆ6á.îì=o®éo2„Ù ëÈåÕtw7ÞÚÂÙØé‹·ŽŸ½ ¼“‰Ô–¯»òìÒ…g„òš™äøس¯Ï¬UÃüa5¢…l|:˜_Áü¥-bAüʼn£±ÆúÑmFƒv2 U„ì¬?=E éËŸ»÷»B¢m%$ª)¾«®ô6.o]}º4³‡ Ô—õÊM̳}óæÀ¸AðV”:t× ÙìäÂéžÆ kQ ›62Ößrâ\r‡p>é5­£é‰4Xeåb¡»]œ9U[Ø]?yý­çsÝ >Û]Ý{tçæK½½¸|ûÍÖêÝL´•,ÌÍoÝH·Ç Æ€Št°äöÅçjm´äX­œ ­‡½ &PrYi®'ºÛÍ•KÕ¥=.Yï®-Î톛ërs×uGýYÍñ&P._®gôÈP mõåÁˆõãV¡ªÅ‡/GDBaÞéÏâá +iº„Š[ª¢"Êg(0Û>Œ»XbCJue|ãfëèÍâÜùtg;Rõ­"¦šSÛwòSba¦yôF¸uÜ“ìx” ÖÉå3v2 qxÂ¥e.Þ!ü9_´i#ÂzˆE%k§t× m@í2"~·Ž×ÖØè¸ •´Vo0Þ´» 3À ù«ß9yåñöÊ¥0ÃF*FØ Sýèô˜ÆF#\"Y[NVB™®ÊJ±ª¬´Ûæô„Á 9Y¡¥t¹Ç…3G´v.\k®^œ:yuáŒT\pñÅþÅ>~íöS­Ùj'(°4Æç,DDãìÉ›\>'J×æÙ>[ta€£nx”qOtJb¼váæ㧯>îæ3No²:w9;yF©.I™. ;WYh£‹)´·@ÖÑ"à”` 糈G±òþa»Î⮎/?ôôF´ÈÁa›ÊìqÐQFi‹éÙPr‚`¤úô-fLýö\uwt&ý©æR®·]˜ÚÎvŽÊ¥i_¬–i­v_ëlœ-ô6@ñóÓttÜÅg„X½2¹É„+ý©,¾ŸÇ¼€JØ1nA|àw7ŸT[Ý€V\lÔŸšh,_\>ÿääñåÙÉùÍýô½Ý»cúͯfâ;¡òj¬¹9±q}ýÂwZ«—sG÷î{ΓìêÝa~>>q"5ynúÔw¦w«®^)Œ}î÷o=÷SQÃÂäW»V6í‰÷BÅ¥lcåÔÅû§Ž_ÉÏîV–ÎågOʵ…òü©æâÙ•3·NÜó4PÃLkmûÚÓ{¼0êâì^0?Cˆý¹”<®GE•¶‘ ©€ªHI5.ÖÂ…<ªP™ –-D`̆oF…PöĨÀWmæ$­ÈÙf¶>«”ç¹ä4*T_cÓv\¼³‹4 +&€?ÑA¾þÆV/Â$„ žD˜P%Ô—ñÍ*Màʆ̸ÁÅÁž¥´ß+g…Ôxyz§µ|UjNdMÇw˜æx}i_¢mwË`£‡:;«±Rfg$¤dk½µr¾0µ#äfa_Fmg|RQLŒÃ´B +9§'±9_¤qIÂ;ûKˆ1eq ‡F ƒG¬ÍcF"D§;HðIàù÷6êmô˜‰¸kÈ|P ™I•Í£µd éôÈNoX*Î…JËÔÿGÒ{vÉqeé¹?â.I3=´0…òé}DdØ o3LFzïMeVfeùBy¼!‚h@4MÏ64Ýä4[3ÝÓÞΨ»G#iFjì]ëêjݯ÷dk­ø@2*✳÷»Ÿ·"Ï^Ojo•†’Ý#1»ÓŠjªeÕƸb¡‚ÂZ%*^2ªÛ‰Ö!ðzH1ÌÄ1­rf™v¢`ÀC´í2 ÆÎÌ#å B›”^VJ«ÙîA{ýêhûÒÉGÝÍK…þáÚ•ÇýÃG;7Þ¿˜im ‰ªœjçk{×^â3Kjqµ¾qk°@¢¹-å–h»š(,]áëÇÏ¿å%M1;f’µ´µré­á¥·’íÎòÞç_ýôÒ÷ÅÜ’Ýܸ]Û¸|òàý·¾óóÿúß¼úÉ®½úÑ·ÿê…·¿£TÖ•ò:Ÿ[µ‡­­û;·>^:|Uj””«ÉÏ°¼‰aÖ0)6·V^½+Æƪ,mSJ‘µ:b~9–l˜$õfqpióÚÛ¸–å­’lbz£0¸ZY{^,ìé 65¤¬®3Â1j6DY ¶ºáøŸ^΢Mûè¨O5÷¥äBX*66ŸÏ-_dR½X¢Mèe/6éBã'e7LCL—3„V"ã%6QÇ•( 8À¯½;Éú:F2^÷¡ê¢?æÕD«û,=cÖÜxÜ…Jˆ˜e3ÃDóitP1Oò)RHAŒÍ™uGˆŸr›â-\©— +0ÒUŸ™ƒ(PVй`ñYDxòÍÀ ‡Ð ð¿Ónüô"rÖC9-KLÞ˜#¤Åt;ÕÞ P 3U1ŠK¸’Q²]!³ \’˜ê›¥5½<ÆD3¦Xl¢±&—ì ùUà ç`BQK ãÙåYàýƒLS¼ˆ°dNÏŸ>ç&"ˆÆ ©"tPD0µX\:\Î÷òK™Þ6®¦ät£½yC/ ìÊÀ(ö19Ñ&k4x³)§:0—´ª«™ÎNÌ(GåR•ÎbÌ¢lWíÊPÎv°`7¶åÂHÌ «WKK{fºÚlï<|£½qÄ[åÖê…£{o_}ôÁ­Çß¾þÚÇW^yoëÆK½Í£›÷_Û»ñZk|eÿ¹wêÛ/4wö¶ÖoØÍóÖŽ'›Ï½þYkë泋ȔJ¹˜ÕÑ›òÃ먔I©û¯}ÌU'¬x(K*ŽíÎÑàðÑêÕwk;Æì_¼÷àO +ƒ£0Ÿ¥H©Áj«<¼±qýýìò• e\¾óz¾»;䀭ž I~* åÂÒ€èœÙhô»;wA ¦»{„QóÇl\«ÄÍTo?ª•Ù%À“„’ká'5ˆÏpÉvmýÖ•Çß+¯\i.¦û罓ÆÎÅÆÐÙÕ")eâ¹%0œLÿ—]…ÅR€žºÄ™1{.H–AÎô´âЪ BÂìY€ÐçB¨X ÅìY €ÃS Ñ3tÚCNã6~j:4ÙÇá@ŸžG\¸Á¦–ÄÂ˜Ï Ød—¶ÚHåÕ4Ì™s"Ó0%/­›}½¾Gšõ˜š5r-.Yw#œ”꥗.ÙíãLï"&—<`~rÖ•õŠš¨9äé9ÿ©Yÿ9' +¼p”ÍÃtç²…î^„KJNJ·“Ý]6³Dè%0'éîDH,1 ˜R3ªçúÁ¤çi‚""$;Q¥dÖ‡¨åí– +„B¯‚¬_ 1Ón4ÊXî³$ƒd"U^2Ér²Ð¥ä¤Ï[+ÕÑv¡·b–—X»Êç:zmœí®·¶.3ÔÞ{·uþ˜I^/£¬Ä„D&Rµµƒ;Éæ𪢽däÆrºO›íex#+*›‡WÔ‰* )¿‘é^½<¾øJw÷y%?¢EûîýÇ?úÍ¿_¸+e½±o-]«ì½vñ¥ï¬¼˜ëlÈvá•w>Y»ðp!"žu‘LjÅh—Vnž¿õ±QY×Sí»/tòࣙ?ËN˜Z½¿÷pÿÞÇ»>‘Jùææ ï|/ÝÚqA¢+*ƒœí]Z¹ôøâ‹ŸÖ×ïéÑñ­·•§fBÏ΄f¼tL¡BÑ®lt¶nÇäüå›/ðÝŸàRV›lnI…üæÊå¯oßùVºs)ˆ×^xïêÃ÷ÕܘóÓÎ(È,$…©¡µO-’SlçàÎå[¯º!áÏŸ]üógþå“Ž'¦B.0φö+÷÷ µL[-L)ŸˆM^f„ 9`²"¤&&*>Tôa2Ä¥¸t_¯í”—Oúû÷„ü0H(vyUÊ ]ød›Õ|ˆŸöRŽ‡±&#§}Êá…°˜äð!ó~"È&C|Ö=Ùâ— ±ÄrFE/¯äV..½Ú=|P\¿£Vvç‚”á8³ê(KZÀéÏøODÂÅ‚UÙ@ÄB€0õlÜ’á]¨6àܨêF$àDX¥*»âÍÊ:HyF)bLÒI\§šfa áô9 3&g˜Dµºzqtt¯³uCJµõ\WÎ.…'Xh3Zgí0&c1-Jë ØâþÓ# T +JSÄÄ F›HLwED˜Mb>LÆÃ1=ÊÛà¸e¤šÕÎf¾µ‰H“§™ÕñµLï@+ôbñ\LIŠñ¤¬'AA\sçÜ!dcj)ʧB¤æ…9o˜¦ø”šfg!"kÅu½º%$»õñåîîíTwŠé™BkóèŽUA1Û(µâHHu(½¤,Lêþ¨BMúz%Î:PgD +3Y)½¢6X£…ñINN䪃ê`|@mì*µÝÌðFvpÅ(¯åG¸„r£­+ýۀ» +òª™ÉŸai¥4ïÁçA5ÕJw¾ÿÂk9!ᩙȔ—›Z¹¦×öôò¦Ͻý/ï>ú`Î…?=˜÷³þÍæIiûˆ§Ÿ XSϴ„Dæl9ÛìW^>Ê´¶ Φ…Dí$ÓXs¢Õ¨x-*äP>éƒb xÖð‘+xƒ!o8Š )»wtðîGå2ÊèÁîúáója9fՔŠ\Ø 2W„‹RjLLº¤;HG¡l‚ÏIÉðeÀ° tŠSJ¬”+Õ×;ë×ݸ ô ¥{ qÁGMî™Ñ¥åyŒðÔbtÚ‰ñ8ü…Žò À’jÆ rºnïªÅ >9(õ/ª¹QWËííL}Íæô¡ŠŸ´p"DcjÜÒ¢',(IɬÍzI +Ž‰ “<ˆ1io4H˜›I æ²bjI±Û“@å‚øèäÛS€yð(¥D9Bª­fpÃò”ëO‡çœ‘¶è#Á…°Øä=¸Z 3iG(f•‡ZiàA8„1h½ˆ‹iFÍËF!&&œ~œ’V¾ãƒHO Àèl¼DÉ900Žˆ×ƒL¾Êhü$q oàŒˆÊ>øªÚ¨Ãlæ&;"Ú&„4Ïø¼èEÏÎ"€Õ‰ø3çü§gB .Áb$-‰Z¦<>!væÇSX +³é iÀ´qÙjW²JOφ'ûÂÒ£Òèjsç…ÚÆMw˜c +BŃ(F¹ €M„Tsrº%§Z(osr’SS¥ºÃ\”¶ÁxAÆ…Hu΄£| B…a‚æD(ŠŒ€Ðªœk£jÞOê>T”ôÚÖŽË÷Y=RU/¬æ»—Õüf„2íöc`P~ˆáŠæˆQ:Âe¨xçó!D’”ìÞÞ/}è#U'ÌÕKA½88ä/:ëDï-ú($xQÕ ‰‹!Ö 8Ÿ7³F„”SµÕDm]+ŒåìP°û¸T `ªlÕ%» â*DjvÙ(íBB „–+$L/>ˆ RHTž÷tgœQWˆyêŒ{z +"²ÓóƒõÂä`„súÈÌW—X5ï QQ&!­i ´rðÜ|ðìŒgÞ Ã˜25œZˆx#R‹+‰²lY)FãÀ þÙ3‹OMùžœ›lƒÎyPJ4€ ã´3òÄY7.æ£.€Éõ£6çÅœAâìbø‰Ósn?ÄEÂâl¾‘ñy7âòáþCóvá0çðággý!TÀXLÐçÌÀ(ƒë +ƒÓ&(÷ ¨\A>ˆÄ½Ôã MϹÎ̸\¾"˹Œ]ȧPóëK`^¦A óœ LÏGB°à 0§Ïyžxzö‰'gŠ —öc¥hfNOy­B:aA<Öš^?ùì³gœ3s~§%X;„ó‹¡©Yÿœqùca† Û`Oó<{f‘–20©žöL/D½d4¦KZ!ŠkNoljòèIvLû§°/ Ä“YpG0ŠÁ)œáðlÚ¨v[QŽõE1ÉLg+--™r¢'GÂJÜÌV:j¦ñä”û̬ßj•Ô4;!Ð¥BzycCIççƒÈ´ö@¯Óëu ‚PÍ%ÕÔö°rõ¨ñ`øÒËw·vÆz\ƒIÑ1 öaD|z>ü¯ž˜~ú¬Ëá!€†8¡Hˆ 0J•Y’x^ †Q/b†Åc$…IŠFq.ÆX’Vô„©SÓ®'žž~êÙų3þ©I£Ðì"þÔ™à§}Ïœ Ì»0§—pº#Þd¤òa$†HA²ýÀü8J%H.9>»8ç„`˜ ¢^·Çüª—2ÜRËŽjÅšÌ*õjâx«}óòæÆV·\6K…„ž°yxvö©S³³óî@ "ÐŒÆ,êIÈȸ›-V,Qcx• bc†)‹Ê¨c=¼½ÿà…“;76ï>w>WΞsŸ÷"$'Žã%ŽTXDeáv)>^Ê4ËŠ­!Í‚¼½œ{xgß}÷æoÿ潟ýüó÷>x°¶Z2,:Æ3›œ¼<5(:ž´jÅì´2­F6.³vÂ$Îá x}~IàË•Bc©±²±~ïµw²­ž|ÐÔ€ W1ÆìÂ\îôlàé§OEÈ2Œb±Î$B 4™FŠ+åÂ…ËWjÝždšãu3›ñ#¸+ŒQ² + 9ÙDh‚Â12Û²s¹’WÓŸVÑqùpwØ©g*iy«Ÿ{þæÎËϾûÚµG÷/ܼºßï×­„ Aþâöa®MÆEP6†æm¶YÒêEmвŽÆ©»Õ7î¬üøÂ7_»ð‡_~ùëŸ~çñýã½Q­Vâ9!Áü! wd”¶Úˆ€ÐŒBM,§´¬Îv+æJ/»ÔÌm¬tn\Ù»x<¾}}ûµ—^zṓÓZ¥f&L–¥ÿԌǠfȳçü³ô™S‹ÏœZ˜žõx½ßãç(<©Ëõj^“9Æ ¹T>Ÿ5 Û23a˜zêôHX—7 øX4 ãÎ’ Œ.´n¶ï^|ðøê'ïÝ~ïÑÞ÷‡¿ÿþƒüõÇÿúÓ—Þzte{½oÛ6‚Ó‹NÛ16uSh7\)£WÍ+{• ;ep\;êܽØ}ôÜÊkwG<\ÿýßúw¿ùæwß¿òÜ~!Gnל3„’"FòP¢a]GöÛÜÉP~x¡ôá‹[o??zãÎÒ‡/­ýâËûÿø›oþâ»w~ýÝ[ÿø³7ÿ×ÿÍ_öðµ»›÷Z©”™`˜‚•4¥r†_iêûˉ¥<¶\¦êjԶǽôÚRêÊnó—~÷›ýË_}ÿÃ^zpgïüÎ2èy§ÃåúþÔ¬ç‰gÎú ¶x()ÁíÒo3/ H½eëñƒƒ×_ºüðîá7Þ¾ùï>zåå;…’Õ-wÖŽû{£ôº¤lpHÕÀ–ôû'í»‡µÃ%ù¥+­ŸõÚϾzãÝ6?z0þ»¾ñÏÿö‹?þî“ëÚùÆ?»u´š–c¡€×¿èÂQ0!ytÚ7ÈS·ÏçþòýË?ûÞ£Ïß¿þõ{ƒO_]ÿ›o]ûÃßøɧ·~üñÅ?þúÍøáóïßkÞ=ªìŒJIKM¥R¢š’â¹XŒ6øH=]-s76³¯ßZyñ¤þðbõ˯ÿæGïþñþúw¿ø毿zø?þðáÿ÷ÿüÃ~ùÉÃK­Ïß<üÅW¯¼ýð ¡Ë‹Îș٠3@â1)à÷Fü®hÐÁc^ïçc—×s—6K{ód5ùàJÿ‹o¾øÁ£ <¾õÚ‹w,3áƒéÂÓ–UbÝ4±]gž[×?¸½ô—_¿òÅ×/ÿôÓûÿþ'ïþ¿üéï~þ˜?üÕKüí·>ùúÍía1¡ 1šWõ$Eà" •u|”G/ôØ«ú+Gùï>>ÿÓÏïõáµÏÞ<øÑ7¯ý‡=þÿæ[ÿù·þöû÷ÿî«;¿þòî+7×ò ‘fh!O(xBRsš<ÌÐ/ïšo_-¼w³öÅëûÿýwïýÃÏÞüÁ7®üöË[ÿôÓGÿðÇ?þèø'þò³ËßïèÚn©”³= BAǽêz'µZW¯­'¾ñpô7_üüõÍO_ßùýßúÕW/ýå»ÿêÃÿûï?ýßÿígÿôë÷~ÿÕóÿõwï}ï뛽¹ÎÎÌ:}‘H„iº—ëz¨¦z/÷Ù{»©»»É—¯Ô>|yã«÷?{cû§Ÿßý/ÿÅßýàñ~ôæ?ÿþ³çÁþz½7X¶K½&'L;oIÍdl»J‚Ó?¼ÓûÅ'×ÿöË»ß{çð»oüøÓ;ÿù·ýò‹{_½{ø³Ooü§_½óÛÏo}þ õás僞”5ØÅÌÌzž~j&ètê1_7 w„ëëöÃKõwï­ýô;þéWïþûŸ¼þ÷?|õïôÚÏ>¹ñ³oœüõ[ëïÞ¬^ÅIÒQŽÄ8 Åãf©Ùh»ùn–d°K}ö½»K_~ýø‡ß¼úƒo\üå—·ÿþ'¯ÿÛŸ¾ù«/îþê›Çÿöû·ÿÓÏ_ùÝ÷ï|þêðí+ÙǵbZó‡(€Ä—ड़3JÙ°ñ£Aüö¶ñpß~ãjåó7v~÷×õÅs¿øìæÿü_þÏÿø×ß}sÿ»ï\»ÿÜŽQA(Þ‹#l¸na+E꤯¾t\ÿìµÝ¯Þ½ü£®ýó¯?úßÿõÿð£×ÿæÃKïßßÞ• %†cxåx5+iYE±U–N‰ðrA¼0,<·[yp˜ÿþ[{÷½ÿðã>~e÷ãûÃï½¹ó“o^ûôƒ»šûãj¥Tõ-gE^lRQÌL>‘H +xË&÷ûöÑròêjòë7Û?øàÒ/¿xøÕûW¿ûæá‡v^»¼ôè¸qk»°Ö‰—³=(Oˆ¤„¢j6b$—6”¤Bed|X’îî×>{´ùÃŽóå¿ýþKøÁëÿôËþÙí_|ëÒöÚ?ÿâÍß|ûø‹—jòý¢€B~§–õJ2ÓãSŽaå8QUáe+tc$=ºTy|­ù­—·þË¿ùè¿ýà÷?|ùùö?ÿþ“Ÿ~çÞ'÷Þ}°1êe$5Ž1LÆóŶÀ²†H®·’í$6HAûMæîvý‡'ÛçËïßüêó»?ýôÞç¯í}ã…ñã«ý‹ÃÌrQÖ„"9WÉLÍP„¤ ¿Fø&±Ñ²÷†¥ó½Ü•õÊýãúë×oÜZ{ýæÚa×8êĺÖrä: :†$Àü11Iò)^É b"ŸÎd 9£2«µäÑrñB?ñøjç³W7~ýÙ•úùãóÆ·®~ë…Á£½ä kÆÃóÅÕ’(c~ Žƒ#&ze-zÐ0D`a¿J‹*¶V;ì½û•ãâÇ/ ÿðÃGÿëùO¿zû·_½òíG—NÆÕv1 ×ÈA„üHÈ/QXRe‹&ÓΈ«5ãx\¸´^|þ ñÍÏòøä;«7wë] Oìô¸æ~w„52KùÆŽžî±¬ÆD9©¬/_>\k¥É ýÅ“îǯ?wópùÊFœ»³”lä4SeUYAc†7÷a&ÌfH9OH9+ÓTU3†ÀEdt9£ “7ìã•Ê•­öÅúî Ð+¥»ùt3m™*‡ÀPcε`Q& a’Çô{<ÑPH¦b­|n£W¾0J?·•þð…•¯Þ½ðí—·~ðÑÍ|pã{o|ñhûÛ÷×ß½Þ¹¼bjl +ùIFöÂK@¤áðSP„Ì%Rµ´mц ﶵ‹ËÖµä¯ïýç¿ýÖüíÇ?ûüÞ—ï\{åÆøÊùÚ°T4™U³²ÝA˜ÂèA„aX­Z¨%$&£RE‹ËȱBœïæ­vZë§Ù“aöÖ^ÿhXªt^MQä(Ab?=í$OÍÀS À3%‰(†…‚4[ššMÚ…d²d›I™¶x*%³SPɈŒF ^(•ºÀ¡‡£\°A”Oz gÁtz±`gi1›*-v–Ûò!­”â×6*eñ⨰¿”]/«mXˆ/Ì‚Êyf§‹?,ãB9€ÆOMûÿâé¹éYwØ”(Ê–¸²)¦¸›fÇ%ñòÈ~|cøÖsk/ööz3F‘)Å ra-7¸2I`Ïð 8C³Š * #ˆ¢R.s)=m‰i“פ˜$±¼¬DiÁO +3¾è”òEeJÉÁœ5ã'<@q–8y–WHe+p( s±"°b™¬*4ÅÅ(Ö€}A¦Ü!jÖƒM»° •Œ7õÊ*dX-Ñ‚2¢œŒÀ4Hž×›ÍÕnw£^êq X/ƒr¼è aOO/ÎH?•ŠÊ˜VçãU”Ò¦¦Ó³®(>f)JRUô¤¡•³Ær+}¼VÙê&ŽVŠÖ:ãF®73VÞ2|þðÓS ‹ÉÞö3.ø‰sž)7s`Š×rÕÍt®KS”¡yI‹hŠ‡·Û¹‡×^¼}ùÂöxT¯š²@XÁP> L?ȧ9Æ^Êö®dÛ—’ÙÍă —_€fq!ˆ’¦eR©J.ß°­<“ý^<gæ¡Sç‚ÓN‹ZNHs„å nRbÆ㎄?ˆÎ8à97ÈÜáB(HL £-åN0< í 1gGˆx”NÑFKLñÏO9 endstream endobj 62 0 obj <>stream +> +Ââ(¥KF•ÑšiÀ "­R¬…Üþ¸l'ìª,(DFQÁåy'òÌ”/ˆQ&3ç&þåצÿüÉÙsó‘HT”eSQ“0LÂÊRœ&ÈЈHEÅ¥qEL;ÈŒ e­®Ý9‘Ò!9(ŽÅtÝscrV3Q;¯—×aÖã<'%ªÝÕiôÌ\`ÆÏ`ñ—Yá’C\©À|–’òÕå#ÒlÌDä ;Ù”fÒ.X cñ(¡.zàg¦Ý !Ê&•j”/ø1ÇëD%ÿÔp{6ÈÎ…ùŽ%9³.%[¼QŽ§›„ ÕR4ÃtW«”V£õªêòv“’S”V +ÄRnTóbqwTÅ’A2áˆA\AX“Ñ +¤”fôŠ–í+©¶h”¢KЂ Ú´œ™rø}HÕh½+ÎÙ쬟|v.ôäŒ.ć˜mv?ãÅg<“žü(æ:FyNM»pLõa“íÉŸ ?a9é©™Iûë®Å´ŠU9Ê5'b¸ ч©rf f‡ÓNÜä`&A©%Újùˆ„R¼¨N©u03 AnÚƒ¹¢]ß瓃˜ÑÒés>Îé¥Þf¤$°ü¨\ kjm;Õ9óc?ZŒpÌ,vq>.-¦ZaÓ¬ìÊÅísaå©EìéÄO6 ý«§æÎ,À”ÑQëF}O aù™©€'Ä/ø¸¯òZ@BL†I,«…]Þ:}>,Ÿvàó^c¬`T~v +œ…3ÓE/&t^+ÓBjfÞ¿è +CQ!‚Mžg9ÂÂ\€[ˆHÒvaæŸ?¹ Ö#R“Ò£­Å壟BÔR˜Ï’z™6ëb²W^\ÄD'"D¥—éù,8×K“ÖR®{qçÖ{fsçYá@­XjM*’‰!$=¨º0Ù–nz:æ'­ ¥Ì—“F›6À”°âÆta b™M.)…QÌl†˜¤”^²ë;¤ZbôzT)‰žÞº Ô÷¥Ò—Z"Ôbyå&ozP™Ò'¯ÓbR}4Þ +Å°Xä¶,_îïÝ ²É³ÜIQ©ìÅÍ—…¤’0E£VîŬΤ—¸Þ +•ˆÔ$ô¡1üdÂUgÔ\€ð"bL-“Ææ+Π뱌VÃÔª—0ÁýÇk;``µ«MrLGåÈ5ªáRÁ4Ù1©¨R åéˆzÊAÀ¬íŒ0ç\ˆ}d‚Pë¨TÆd09YD(áj³ûsAÞ‹[X.Ì•ý± "MvÙœr g£¬9yd9¢°)å°RHÆî‘z=Ì$”R{&¢,¢ºŸ+¥óTj,·…ÜÖ³ ø´‡FÅ¡–]h<@¦=hʵ8‡é´'Ÿr>'¤–…ä²3ÏyÈ…ˆŠ¨MÜXâ†ÎÌC_;ãv @Ýa>@ÚNHõbº–¤1çˆY˧úlzD¥†drJÏÎF£zÕêñüpûâ£t÷¼“Œù´¬on²cÒÎ=Ñ™|Ù¦8’óƒELš +³ Q=ª4ÓK×Ò£çÌÎIdÒ6VÅã &=f2k¹îgó`þ9GÅu6=p¢q¢Æ¬%¹rÄçwp}Ò‚ÌOÙà£ã£ "ãñŸõ]»yhÕwÕâš^X<Àͯ™½KÕíûíýë;÷­î…ìà*£7ÍL¿·w×IÄl +„“Y×Òƒë©þõx}7ÀLöœRfS±â·{„ÑlŽ® zýêÀ ¹¸•è\ׯ j×˺AÖÐ)DȹÁ¦‚Ü\XöFµeC|ÁK&…ÌH.¬YÓ3ýlÕ{`2  6¹±7–tÃJupOöÀxÏùé Ÿƒä¬´‰ÄU«@ļÏ›u6ÑÅ•ªÕ€èŬ¾RÜŠWvâå­ù€@阵LÄ»àZ."1‡è§=¬·åüÌθP,Õj¤Ý'ËQ­±ˆ(“¡¦–l(0`H(rÙ5TïÐ镨Ñsâ6eõÕâæt„¥ì'Ó YÀïçRËPGÛ‡[lràŒh3zÒ¹Q€lªåí¨ÚšóQ3r>(‚¼º÷ĩův=¹€¹ˆtÌ\âR#L­Ï{éÅ G* MÊÂr“ eƒt‹=°ÐÉæ‘ÝØ‚âS¨VásC?gãZ™°:4B³¥ÇÙ¥C6ÙžG$Âj…@- 3TtIJ^¡áçʱx]ÊXL/e÷ùÜZfùf÷àqqíy/“bgK×TbàžŒðU:9Ö['z÷"•hófY+£z²Ú D ½I%ºje[.Œ!%/d—ÆW÷.<œ´ªïÑÙµ°Pù^hï.íÝ;¤‰xÍn_H.ÝÈ®<—XºD¦†~¾(çÇ­Í}¤©…õÞî µ[¥ñÍÁÑ#&;rYøOo­Ed0:дææƒêÆ‹©áóJý(Ìä‚tè6n5Oyé…ˆÖ^cÓré¼^?ï§Tʬ+•]¾°M¥W3Ãåç']}Ò+Ù¥+± “ˆWÈÄ— +ùuÂ^YD'›Ý¤äRkýÌg âŠKöæ •\Á’£ˆÜpÀñdmON/O‡?scvˆ-p¹m©rHÚc7‘Öó£Í“]¨~ÊEžõó@¸p ¡õÃî…·q­¹íÕw?ý‘]Û<íeç°–’ãçÊ»/gWï°ÙÕED3ª[@f©E,Àf<±4ˆ«y"å·’R@‰q#â”; +´ë”—Ž(¨Ñcó;fçÊצ‚g">Lsc†;jùð„ŸJ;ÓM¦¹ÌšVÝ] ³“î[zN…¸¼RÙ 1Ù›¥fôbÉQºw­ºs?ª•\TºTgU¼²Î$Û1«AZÌà¢TF#oˆùM»{Õî]rk\™‹Ê~6Ée”G”*aõ {QZ`‹£ëùÁ1Ðm1;:³—P­K¸Â*m5ºWGÇ/²é%PÅÀ¹\r)»|¥|þ!WÜ”3½Îøø¥¯™ì;cV4Þ±Ú—ò£Ûåµ»v{/ÈX瀡ó!ï¥Í°RpÆlÜêTÖžïì=J-]"¬.©7sƒ«¨Þ òy*5Jö¯ŠÅm21PJ[b~å'4Á•BO+ ¸LßCçBbNL¾o?Ü¿?:|p¤mˆÇÕVqp»yþ_>†µ6&¯¿øi²¾ãŠê¸Öhm?í•O®½úYçø:³â‹¥9«óâ›_©þYç‹å˜ôF¼y¹°z¯ðzªwsµÑI¢º6 œ„‰è=¹vÒ:ÿjy|³:¾Aí_´{.Ôt¡eõbéÌõK´Nj«7aÍ®^?¢ìe2Ñ¥¬Ä— ¥2Ü¿{xï} A:#ç7åÂ:›"Z +:‹«Íìà +øù)³Ѹ̲\Þ0{Éî…9@¼^Æ̯TWoÌØS«–;O$¨nr#,ÔÎxX=½tró Á¨ÿ‹'æÎ:¢ÏÄüN~|w|åPkÃÒá­w¤Ì²Õœ˜ë}•ôQ)P›¤âùìòÕËô^åÌÆÖþíBˆ—9»%çW2݃üàb~tEkìñJª±®3j µA…¤ Ÿ[MtŽ¹r Ø™ÍKéáó¹õç­Ą́k©ÞððEµ¼âc,>»b4:Û÷v®>Þ¾õ~eí¦’VûG¥åcÖîµüèºÑ8Lu;;ϯ^y]¯K½ýÖú63q‘KA. .UÛB~7«^_"̦‡JL¼FnŠm/çÇÏ5÷^1ÉgÇJm·µ{¯{üª¹t Ö»‹DÂ…›l²cw÷ç£tÌ®ÃZ3ûàÆ´Ú.•\ÌyÆMD¸,mµI³ŠÙ],µÆ䶤ҶZZ÷‘ºŸP“õM&ÑÄ”œ\fÆ×ôÆŽQÛ¶šûVuÇOY@¥â†4YåXZWktnC¢}Ìg‡ ˜íV®3™>³ +PÖªÛ´ÑLÔ·@„ðÙQ¼u¬¶(>jÍ®í7Ï¿ÈçGÖÎö/Zõóµ•K[—_nŸ¿'åGŒ^ë®\®/ájÙª¬o_}cýÊ[½ƒ—[{/¦úY3¿éÁöÅW|”é&¬¨VÙ=ñÆ›Û@”ºUÝ{W) (êÕó\~S(練¯¯^·}þ¡š[];~¹¶u;jÖ¤Ú6•ÛDÌe&·­ÕNÒýë¸Ù%ÕÂÑí·ë«7'»étD©CZ5Z°XJÖ÷s£Ñùëõµkµ…M^K½ä)ɤ€ÒÖ6ïLd“µõʪÕÞ×j[©Þ¥¼ H ÀÙQºí!-L¯Å{V÷²Õ»–¿è^aSCP€öo>^¿دàjò‚ÝêNª{1^Ù3}@°Q1×Þ¼Ùݼ©€Ú/ÑÉ%97JÖ76N^Y>×ÕX«S_»ÑÚ{ Tv&Þí>itv.½l—†^Xœìxí&š»rvȧWPs0µÎÄX¼æ'ŒÒøúðê׫¯h£}¤71s8í—éx³>¼ŒËo”‘º;jÀ\¦6½¨õì<â‰j¤Öò‘öiWÌ„ÖõŸÝæ}`´Ã\6K»ÑxT,LÈC‚‡²"bf­´.dh¼êÃURΔ’ +F}›2[ÀÁû«ç©ôò³ €pZÙþåLÿz,5‹ù9ÀäB¾´tB5LÌF—ëÛ÷Àüw÷^îì¿šßÂ-?m•V®@@‹±›˜4§cVÖ­ú®W#Œ¥¤ZZ¶‘¬¯håUÝh¼NÅóVyEHuÏùIL©€S¼´á¡u:½$7•Ò¶VZ— ©±ze÷Æ×—/½!–·”ò– TÅnÓZeiý*P€0ŸO ®'z7Ôú‘±t“Înƒ(š ffióâ‹vcóÉ(Ì&>½s"•·¤üj%çzAF#¤$c5—I´€À¢j –K#)k 7Z 0K굶žíŸÔ7ïWoÏÂâ8½\]œøV|Ò#S™Þanp˜íFÂÄ"mõ€VŸñà ”Âj÷> 1;õ üð¬—RˆOMû ®ñé®]Y)´6Öî…e7‘`S«|f“µ‡ÀæË–ëRzT_ínÝò’ /•J{ÆÒ-©zȤ†lf‘ªÀ°÷·ï¤ê›Àþ»ÉdD,ÛÍãÎù—Ê+×2í}«² +\ÄËo~V\ò2¹º«w&mÕ½µ´Š¥Zƒ£ÿò7'>ŠíD2±,—öKzí¼îófsu÷ùKa¹ìc’±ÕÚlr„K±¤³Zf©6¾I?[òÑùxòg+L)ÙÍíææÍebRŽ7¤ ÅÆÊU˜/ÎÌSl¼Ñ^½‰p…gç±sÐZ¾ݨîë  JréÁãoo¿ðôtè™ÔAd#B=^Ø6 +› nb|šM/—fÕë¬ÝF¤2jöb5ª5ã•¥x^)mà ü8XwÒjañFH(ù¢5€nW7ïÒéþ‰ˆ% $°ZѪ›XÄ’A&•rfc×*@¦„ìH­·{WŠë÷†'¯g–/˜$¦¥üò|HðafìT rEHª ñš/¦Ë©©W5Àd}1Lœ5ÀWH.ƒ_>Ü»C5@ŤեG ÂQ1—¨n·÷î{ÈÄe@Тb d½V‹ù¡gòê=3fÔ}¤±b1µ$§:…ö6o·Î¸c§Ýô\D ²9È/¥Ã15kfm½¾vµ²v]«®O*”T¢K  HAˆMË…µÊøV{÷™Zw¢És^NÍôåüȉÇÏú¨sjÖ!¾Ê¦×¸Âæ<$z1­88ˆêQˆNúÈ°ºVå|ºuѬKè!Óri×›@F\˜>ïD;?½XX¾a@¾€íÍõ™Z÷ÆRU¾Ø;²Ë@cV“ϯ'ZG•õ;ZíÀÙs~^NtJƒ#Å_Â0kÀ*ùÕÜÒE5·$ŒÞør¥³ó§NÎb€¶Ï*¯Ýªo>fó@ýœŽe *=݈6–üt*f¶9«ad»ðd7³úZíÈÄç½ÔôbÔ1ÆŠªuX®¹B„ËËù!“sˆOÞ¯4È ®;Ƥ–°ìåI;wÒàRýÿs{©Þµ@ÇáUÜhNyA‘*Ù´ŸMc‰v€Ï8A•ŠZi -,æ€ôe–.ˆÅ5.7âòCµ²K÷ýL +Ø6;ZŒ€$ªãFJô¥égrNšiíhå5OÌŽ€ºO¦ÂlTd ö~fÒ1žM Ê«7\˜n,¿tœhÌ`í¾ZXñì B)ÅâÕI;>Ò&Í™\&ÑK™!Ö +Ѧdw„d{.@Ìø0`3O;I•A+ä×a.‹RñVo§¹r„ – °Z KeP­bFWª a.¦Ôü2—lIipÛ=]XˆWžHÕ÷¸t:óbJˆ²#*"Tb‰a€/Í…Ðçþ=à”Ÿ™€"p…±º•ÑíÒè•\}Æ› Iˆ\›ñ3Žˆ´ƒ¤è‚3Zja=DR)è…e˜KøIÕCèA6O™9·Â¦–˜{PÅ,]°š„ÕCôÎ )ÀE1³ëÇMBÌçúÇ\Á‰€…BlnÒï+,,­^ª´71Î"Õ¼–ïÛÍ­âø²ÝÚ› ÉÀ-‚jˆ²œ/<3ö°ˆ\QJ›™Þ g÷<ˆ:펨¤Z\gí¥§ÎOÏCnH7 65²Z—¹â—¦ çÅTLEÅlˆMMTH©ù¹¢“Ê€ðÔª âµâªV9Š©RÚAã ZÄÌÙ ¡-/¡ê–ì]Ê ®ré°ÐBfÕ*˜’O7w²K|1ÓC^Úõ}ùèåko×ïIJcWT&T­íúèb7žÚ%fF£ýû¥å#1Û/OÀ¬è@4H€C¢jüþDëBv|@ Æ'Óõ51Õ†ø\É‚ Š*5ÆZƒÂuƉÁBÓ$X»ç¢ìYHð“f²¶Í™­gf}OÏxfü0à^*ãç‹: –" -Ù„vÎOãF7–±¹Mu^pzPX¶§±¡–\ïÁÕ…h<ĥܛƒe +° ×DPøÜi¹QüL÷ŒÊ~ºubVwž]ÄÂd"Ó:b{6À,„ÅEXU’´¹‹ ~ªÒ;´+ënTsEAY ayÚÇàrIN÷g¼ø™EèÿôO†œváÖtdò¢57Òr+.D-Ž®ÚÔê»\zM:sŠç\DhR¢bÞI7éü#çÖ&š€©Ž0ƒòvsåØÌw:ŽkEa#JCÊ­áJ#BgÏ. 3nÒ(¬¤ÛçϺ©9?7增CA芙±y£ñŒžìöÊ1sÎËZyRS䢷‚thH%PžÀ2"f÷©ä +b"j;$]¨ +$(@%œa ¢3Q¹F§WÅòA,¹ìÀ¤™ mVp) +”Þ.æ×2K³Ë×­9¶7/­%Û{íÛ½ów”ò:¤TR½ƒÒú ®°zÆ…^-ì: …a6AA”R½âòef€UàÒËñæÅDûÀ-Dm˜œ—0a6®èCÅ ¡SzG.l–F7²Ý‹´Ùqãú”—BÄ2 ¹”xp6³¤è€L·D±½…Ì8Õ¿’ì_L/_WjG^¶¬˜êäûùÌ0bØì"j3–€Ó'o—#u3×Û¾ôr„ÏŸ +À†CR5̤ü¦\ÚufÊC©4c.¡r•ëŒ=>ýŒ‡zz‚éIÛ4XÊ{0ÝÕD‚Pªa.üé¹ç†Åúè8^."muŒú¾”_£­øñ9ûÔtèôb” +§æ"ÆV‹«ÙÁÕ­;ŸÔwîs©Áéùˆ/LçÚç…TD&Ê…°@ +H†LL¹‰IC¡0VAjA þ¢¤˜Œ!Ù¸œ…LÎ97ñg¼öŒkÖƒ¨$!åBÔ´ R‰…0QƒP„Öð¡öÙE¦L(¦O;¡³³Ð¹Eæ@ 9ÂܤW†Þå3ë¬:£ñ¨RÙ RQj„ÞQ +[je/–êÏCò3 Qw4Œ%!>r´½$äÆX¼b‹~ÄéG‚¸Dj%à+íÉ3šr+7=LnÊK„‘U3\<ÓKx¼ D®yxù­äè2d¶|tšÖëOLAµzº{qãò;ã‹ïf†wç¼»MY›±‡ë]4¾dÖ/ˆ™u?e;C4«×³ÉÅÉ›G¨Ô”‡È¨Þ¶;…ì8ª“íób~ä&L lþX†²ñú±\v&Ak¹ÎÎó ¾ûùlDk ö ¢5Ñx³8¼d7v"lR²[ÉÎ!¨ÚtrŒƒU–«”Þ°«›¥þˆM"|V+nøTòc:ÑÐ9PÚκHBŒZŽÕ჋ò镨PéÀ UήZmÎÏz13Ì—èÔX©ìšõƒŸZ¢í|ÿ¦Õ|T2Ì—g#q0"'$ƒAØä™ù)åÔt—1ªRvâsÓfÚŤ,¯×&›æT6±„+u„/úpË5Î8‰3”ÖQ±<åŠùɤŸÎBâä½c~*9iüî +VUË@hý_“^øDñÜ1¤9‚â´—>µ›¥Qex)H&Ÿ<ÌzAÍÊ…I{1ÀžYDŸ˜ò-xwD Ò©g&ý97,˜ UH®DÆè3ð}3!UªÀ>ä:@|nÇòãù¨6’Ãlщ‹a:ÂXÉÆ^ïðµôòmToMO6'²R†X Ó1«ÅåV€“Bõ.``ˆ‰‡I!ˆñ› ôiÔÃHeGXj)&ÑÓ†U&5½¸ÒݼÝÚ¾Ï䶈Åì¨õ`ª0¥ì*“X #ÏFß5œ!ŽÑÊQ.1å‚\ˆˆ)57bFØ—\1iTHÛõõ—vEµ¨\%â äNâ­–`Ý)%gÕ6I£±Kd¢Ÿ^kìÜŸ<‘ì]B• +ð8éòت®-Àªv¬æIgûùááK•;ri݃ȪÝêmÞ´*k‹0âç‚<ˆmÚhc\"­YOT´šÙÎ1ÈÖ0[jà€5D¬N¾h‘X(èFÔdu'Õ>"Ìo™ÜL€ëåKœe–ŠFý„t`’ì†V@„L-"QÚDYa £¸³zÀ=yQ=*–ÃÒÓóˆRÝQ}Ú à.×`¾œô”VAÙòöŒ—µ`Qç@¸FupQ0K Mä̲”]qFÍù¬€– ›ö‹‹QÀ.ìÚ6ou\ଠ’n@t¸=í$Î:&ï9î‰êç"ÊצCg]¨#DHš½ÿŸ½÷ú%ÍòÄÞÕƒ0¦ºî½$“é]xï½ÉˆÈÈŒôÞ1™ôîzSuo™®®i[=½Ý3˜f0«Y- -$=HOúô'I'Ø»‹Ý…ìƒ~•—E&Ó|çûÎù™ˆ`žîöëçßýÓí÷ „þ¾1{S€&§»3Û[½­ Ÿ[cðz¿˜ßý!í7¤†~ÿÈ쵄ÎUQ˜(êß,N¿÷Aê±àõ¤hnýâÇdó¡&ºEZaŽ=¸t‡—¼;ð·Z«Éù׳—¿1Æ·\¸"à/µö)c&PAÑì&\}€‰/Yã<%˜{£çR¸I{†Ëhù1Zktï$V$”geŠÔZFr^㚨ÐR‚•×¿0ZGáè<œZ¢µŽ@ÞÈÍÑ»Š×Ÿz'?X×°¶UÞ«2ÆÍ—ì¾T€d½jŒIk"…ëÎæL9wÜ?þ8¿ûÅâêëñéG½{Šš}Ðáðr¸yãuwEÒÀ¤f8w)7J´Ÿ©©YÌú§“Ó/q9*§cÕÒ“ÓÍóß½úƒÙ¹,QžäMî>þÍÑí_zO ÖÀ›áôU8í¯Ó«H«1ºL6¯I{p€u¥Ãúkp¯u&š=Õî6ÚËÓ7¿ôÆ×uµ{€;\cE˜cÒž‡‹÷u.(Öå ³}ù\¦ý½’©)¤1ð‡ÔÊa­wÂé]÷äÈ^¥yÄ»óºØ*r >gJtRçÛj°öæoãÝ×6x(kDJ­*å1Ël¬»óçETþ"KìEÉ™‹Î”PZ¤Ú*’`‹¦ˆÒ½‘¢z4ïmßÏ®¾]7¿ýY´| *QôçV÷„=܆§ß¿ûãâÅïú—¿Å:sºr4¡®=¸–Ÿôá[oöõøì—‹Ë_㻺ÔàžÙÙòþˆs %{èöÏÏ?üÝí÷ÿ8¥DÛ˜àëÍ0©–œ8ó×\tÔ=ùÙäæ7r|ÂÝÅéûÖìÓ»ˆÚAä.cƒƒÞŽÏ~œÿR ¶°æ„úmkönÁ£Õä¡õ(kÔ^¿?zû +ì€92â#!XÁ~ÆjA×Fú•“m€kÌ^ϯ~vþéBt’%œã‚z‰F—¸ F»ev/šëoÏ?þãɇ?Ê­ã2c€’íÞvïX#‰aëÓkW¾Ž¾‚ ¨šCTIû§tO‹¸™Cu¨6=røº½ýA7´ž ³9GE—Ò"ÆüIôæNO•pÅ=£uLê=øßé5©Š© +Šç×FkyX•Œ`Ùž¿î} ohcHÉM^‹íxæÄF ô`:X½é¬^uŽ¾Ô;Œ¿:¨BÒKªzE\Lܙۻô†·¬9ÊcÖÓ§³ññ[ÑígíYY©ˆ¹{žüjpý ¿†2Vgt:»øÜâçY6‡ÙE¾Å4¶úðM8}°ý"ëÄë“ç¿:(‰Ÿ’ð!Ø6æ¯)­UÌÚ=ÑŸ8½ß?v:+#sV‚§¥ÕmŽo“Í;§*úcx@4;' :¸NÌW÷„oLÌΩ?yiî üÕ&ØžXýË`þ 5d/F#ÙiÍMkùR랟Èø‚;‰¦·vg×^¼^þBNÎÄø˜t§©‰Á:Dk¶±(± £}nõŸËÍÞ]€6®Kíú¤ÇÙPÊÄU@õ®×¿3“K³s•[bÉŸSæ¨Ä7ùðHj9ÃààpcPæý,&A +©Á€—÷ÆRû\I.`þÑê}‰oì×ä*×€í¦­çMõιŸ¡O¿i¯?X½SÞì‚“Â%ø²ª”ä)•ZnïØ™µ§YDMgÅz¨ÒQâSwôºÄ5©ÅÈÛ#œUH-ý´jV~¤·OÀAQ”émÁqÔ¥f‘²xå îz§š•ãLM®SÎÙíÏú‹[Zi"\“cVïpz· +*Jª·=yõ÷ÿê³¢I…ñ+lŨ%7Œ•’ ˜5Zo Ïõæ²Æú¸Ô®Ð>È$Q²?Ì^ÔQÖýµÒ>SÛŒ»)`F Ój”acÐäOŠB‰ò“ôŠ¬æêƒÙž§£/rh6­±ò»UÆ‹F×îð¹î09µ]PM²74âusz«øãôÃOŒ&¤>…3»N²kŒnâù ·f¶7f² VÚ>rd’ÆH¶R0ý¡/äØ\³Ö”w¦FrÚ˜½\¿þÝÑû¿ –ï1µ€M¤ônjÊŒ!aÏšÒOôZ¼pz§áøÔy6X‘î\KµøÊž¼ÎÂaƒÂzQhQÈ™Ý µ‘Ó¿‘‚E¦Ì–QEo.j|¤„ÇŒ½(s­ªÐf™žœæ”˜Þ½–ZçjrYU;Oõ)"—9W ´Úâõ–?8‘ãL^ªÝ+Ò›•xk‚Çh£ïž·6_Ç‹w»×¿o %¬`ê÷ŽK”Q MÂIá6š½Yßýhöî0¥WÀ-LŒê|P @ÀÔÖ©Ò}ô>*FÙºRã|€qÖ$Ìë/AÃÛ½Ûpú†o¬pxYR”°Ý»hL^”¸¤‘áÍ{³;Ù†40ê´z Æ¸EL/á6ÎyA<ÎofÛeB+`zz@Ï[Ý«*ç0ë‹,NHa3ը׹š*:cΞ@‘ŠÞ ²ë Ä?Ë¢ÙÕÒnz +"|ŒòÂø@´ÖÁäv1ÕÆrrú‰±û5¾?¢Pc©±¯YÔN&W¿úãÿ®ŸÅLÝå¼9(„w ¥ òh—·Õ01È+ѤŸÓÍœÎÖˆ7 óhöÊî_§‡\ä0cvq9D¥pïŽ@áj"8 å.’£ý“ÉÑ[£sœ~›?® ä6*D’¿ ÞÄê]@zð.D:D¤˜±G½³¿ +×ß6–߸Ӵ;ïM@E þHœ3úÍù—þä½Ù¹6»×J¼K?0¶*Rbd·Öˆ9ÝkôFnžÁ+Šzã×i^5&AÿD eMÅø·–|cÛ¿ F/0>”ŒääùwJ´É$µ.ø8í\ï.Àfâjœ't³µîIB²±î´šú •€x­¤ÂZœ ’`Ã{K€SLíe ¿ÌD¼»l Ÿí]¤‚f&ÍçƒW8/Oz|c}j.^óöðíÿpþþ_ðÁmÏùpƒèýÔ¥ʳÊ6œá­ÜL?Ä’–Z@µ@a™ª¦kJI …X½+y« ô.e©žEÕûÖöÞa•Í×äáÒÎf¦@HÛy~úø4S7ò¨†°ȶFçHòæ?É1O <©t0).âÎaUƒ¯¨Ô1¢c'9a´N¦& ³H: ÊäÆ‚Öß"¤¸3½\ž_ %¾‘€i=DIpcXS;û¸™Å×Т£húÜLŽY«‹H€®V±»ä•l¿ _ŠÁFV‚7+sÁ³Šxˆê¨Ô†Ä`Ý *©0Ú`m¤h#4J´‚[UŽ³¸Yf<Èœg³ÀÑRonAFZÝ3p(R° f¯ÜѬh{0U@Î(O˜(ïkáÒhm!Ùh-íôÓ8[³ç ¢¡(ʤJÞˆ’W,Ô`œ¿•spm°ªííwÞô¤ectf!=Üݘ€¯ò Ê„³7Þäµ,Á‰×åÖ®"r[ðW`´ëœuT»utû¸Ä„YÁkN¯ +ZaB£síMÞ9×þ¸r›'M@'`ºªf)KŽNôö¹?5ºL†€ò$”8ì}÷ã¿>yùeöio)Ä;>:¢ôxƒ‰-ÑéG³»"å¦-ÑÅãoìá›ÆìKFº;9:ÿŽ’[P°s>®Ê½hõÕòî×Vr|P¢y-aÔ¤Âxà7!ßò¸}P7 }êŽ_òçê¥GO`U{„Uã›à‚ýñ‹Îѧ2éÖÙnŒ6´“KHfBlƒÜ*1žÑÞ’jû'9ºDz”Þ7âc;Ù©ÁÚ‡U³€7äà×{r|Æ[¸±Á¦"·!UÒÃÑÁJoï€úÕðؼ`Ý¡vX«'s»sÞÛ~ÌÞRæ SGuy@ê“"ÔBåeöxoþ°^Ô5è.Ⱥpñ¨Ô¼ eTè´(¥4Y³£¶VÎ調û&^ Ú†1{’7VýQoõ|rö ˜öæ€V{+9CJ@”ÁTÄpÉíÒz›6‘·*”— SñÆAÿÜŒV‚3RA°¹s0z¸ºz +š‘B%˜B]¸ƒK9íevÊYNÿœ0zàÇTÀåÏ^ëk):Ž@ÅØCFtu´SÛWPŒŒ32{g%!ú‹ 0Ý)áxª@ ãÁéO.~˜^þœ²†˜Çó—P„ƒ*(Òn…õ!gæ7?l?ü­5¸äÜiïø[P‰¨Ö©Iͪ¸e1[r'noW¹÷•is¨ÆÜðBžr³¨F)mÁçj¨;°‡´»Â;~ÎûPkYDâ‚'–[‹Ï¤èœw׌57Zç²7ÉÖÄ¢cB¼Wâ$E“å‡`ñáâ›ùáwÿ{{ùq¯$£l€ra¶®çÑô2{Á[t7_õw_«ÍM‘t—ß[É&w LX;˜ *åòäõ¿ü‡ÿWeŒp}Èû›`úN‹O8î»utkDC-5uµ€ªé1 µ¹›¥]ÚÀ,f÷…Õ½«ð-ÐÕu!MºÈLÎ^¥Æ t>¨qTíðþ4^½Ñxý1"u %á­ov¾-Vš {xž^ý;~@ª47¼Õ—ÍD÷û’?$ÍôÜMkõ6œ<'Õ.FF¸TýI·²U¢Æè¨Ð°:ƒÓïÄ`Q£éÈÎH÷ÇãÔ—³fç,\~p§¯(«_J›,Ç KQ%‚9åLA瘃+€Jgp ØU—›ZrÜ\½kL_ñ#(:p£Z|Þ åÝ +m­]kýµ®9ª&'9.ú¢Àâ|hDËm—0ü8¼/ ²ùÊ‚Så–tVÏÁeSF[j³r3=·}ûק_ý]”fÝæèöf;µ‡fÿ +6šs‚=Ýi÷¡X´hi¶w âJ¤ *Ž€D²g¬3EåöÓyX“3ˆ–#Ý"ÓÀ@´Ë-VØ[Eó7œ9¬2 §{Ã{àRû˜Ø¬PÎ^Ø/Ò6œ9……Š‹>üûñݽӟ:ƒçeºù$Ï“r*´öK".4_¼áMûµÓ¹à½°Æµhf§sf´OÕÖ1çÏ*\X Ã2©>K€È“ªÔ¤œvïô15=Dœæ¡Þ½¹àMP¥Ò—5ûAï|¼ûIX[‚7-ó Fœ=ýc)Øb µo 1möó×ÝÝ·“o`‹¡Ü(5FÅ*§g+p¥)]Ö³pi´N÷ëR•»Ï\K8o ²1yÛ>ú"*áF¶"²)oöÒNÞ˜ —RëDï\*ñq]ŽŠ”®¶6ÖàjpþÓäô;!ÚR6 [~źÆé‘Fßè^uOÞ;ý9ÀE¦& ‹1¤–¶/ÝEš¹}ÉØS°º€öÏ +LUÖ):•ž[\ðþ’4‡ å¬Î–0Új4o­?ùÓ·^Úør#cðѸÑ5ÛGñì®& ˜ÝáÑ=­…Ëá"r•q€ýQ®ñôÀ¥¦©á¢ôs¤—#¦¡ïU%P&¸’€ý•ŸJÁìK7rH +;U©’6SŽ« ÒïÛœ‰ÎÒî\×Ä–ÖÞ™Ýs`v0§{ˆvˆù”µTÃecpÚœäXÆ&lk·³¸Ë¸µ½ÕÚ§Þà°ø´œ;zéõ¯ÌæJ2ºÒÂôä6(êæ(nU¡i¶Ž£ùÛpõ¥6¼£¼u,‘^€- j÷Vn_HÍØàeÈFBïCT¸àþGp«5„ݬó€l:¸lΙ€TÆ 3ùTÙŽ¿;ùø? î~ËØsxLi3f_€ð†0ívz­ï—j÷œ ê—ÀPz³æˆµÇ¬µÐÛ·fÿ7~CÛ“B€ï¬?6ç´öª‚hŸ)Ñ ‹ÌqŒgó¾uô é,jrUðY¼uº'¤Òf×h¯ï~uúþo•ö¶ªÄ)PãµÚ:©Š1àÎüéû`ùÉèÞ"rrPJ¤Aj™ô +^ue+6ÏY bÏeÒâÌ>˜ýùÕ7­ã¯„æ9¡Ã<=\n*ÁWš¨vƳ’“ÁÙ÷ˆç?Ö„.袙¦Óa])ð˜Do‘ZR¦¬ÃªT@uHlZï>+²YD¦Ðš»dûUMi ZŒ¡ârPI/é±ó¸µWV0©Í¦Ô¹Ž -ÎKAzåCz‘‰êRЯ1} ‘î×T֞ċ÷Ñò+³{‰IèÛ *H]´†å>˳5®Ï_~ø1Ù¾¦} ¤ôÔ9¢Û»j ¯%Éñ·“ËßàƘ¶—,@¥=«ˆqŽ´s„Åšƒîêýüú×Ë—Ó>ù¡Ä5³ø •`7ÜRÞBëÜ@–ºã`:@’AùdqÒ’6ÇVç¤le‘ö€}ò¤Uù^Ýÿ­ ÈTÞ;ÓpüUúà»[‹V¥ÚÇd3w|ÍF®yäÏßÎ_ÿ]°üXùéLÅÅÀŒ¶þà…ÐX«É9Ä›k¶v Û@ç—iXUfL_S˜ ëÌikf¶OE†¨]Ì)Í“ÞéÝOµä’¶§àŽwh´VÁè¬sü^ëžµ·ŸÆ×?ßüZnäB¥4§/“£¯Ùà˜0çR°ÁÙP&q‰Òvÿ¬wú³í‡¿7†ÏR¶2J°–ƒ© ýé¥Þ»åýc§GHpµ“ÇMˆá¼Õ5’#lf÷ +D¾)þºLù™ºRc¤Ò­²À}m(L  ­µ“G ´÷ʇ¸ê¥y09A•ŽÚ>—o{ÛOZsS¦4TpƒÁi8¼y’güÁÞ–AäXKÖš‚\ìÍbšà­ÿíÞMÚv9:Å”a…‰Íø\v… öJ"4!=pc +ôWåÛyÌ>(‹´+ÞwG¤=“Ëþå¯×¯þ/>èÑJÉlo'—?ÞÀ,K¶.Íþ-pA‰ XcКÜt/:ë÷°¶Eº ¯\áb ¯L-‹é̬ѫhõÁß‚£a!^èd|–^@nžÒÎ +¾Ï’v‰q1µþýÂä…;{ãÏިѼ<€?¬3ì‚ÚÜi­3ðæj´RC`ÕQM +”Ö¶süÕòåov_ÿK­“£<eÒ0ƒ‰ä!(3p3|î _šíó"P%=<(D` ¤pŽªmÀ4»kt@®oÓ묑ƒ}³º¤=·¥´¯´Ö‰äM$¤†Sor“ì>ìÞýáèÝï«„5¯Ë]ÑsÂÚÎ[ëÉuÚÙspMy³Š bÛ Ô–Ó9ñÓëu¯éÆš¹ƒ[#9>•ÂÙèü«ô8 ß‚[™…ÆÌ)¡öp)ÎÕER¢ñ•?º±ú·”9© I•ƒäìÛØå‹´ŽÑwûWzç‚õ—˜ÜÏb€iˆÝ[ |¶ûQ»dþúƒ;¸c[æ<(ÛÁæ½oÿìYÊîù‹wÆà•lê™%±Lû ÕpƒŠMDlë­ =>祓¤çFŠ„M©]P€u."ì9 )'¬ÜfÀ;¸éadàµäø¯w!z ÞšÐF7]ÌÎ?b|ˆZ•bÌráQEîäéFMî rGòæ r(½É¦*èTðNHmZ;ð\§œ1ã-kX×&X…«O“—ÿ}´ûžó—PPé¨Úõw½ãôþ+ÆÛÔ”^%½v¥OêIÚb><>…e-'‡+¨GðJ%Ö›Ï{ópžþñ5(ÀªN?õvoÔöœöÆ´8öVïÞÐF"ºÃ2ç–KO/ð ¤[¢ü*VhŸÖÒÓŒÛWÃÑüögæà!ãÌIW«|dtN¤è¨.·ÁÓK¿ºÎø¥Òļ* “^xãM_P®¤è¸»M?ŠÜ[¾GÔA %AÓ4èÐÖf¨´oµî+¥u¯C¨-Á,ú£—«—¿Wïåö "µò”A˜`Éý2€\ æ;Ç?XÃK,ýhô úª1¯«í©×¤–ÑKÿ¨ŠuOKüAM¤”^zªQïUÕ.ë/¬á-¬P‹ÔU®YecÚÒÖžÎù«Æì=å.3„]†¢î_¹ã×àíî‰ÑÐÈS¾Ýþð¿ã£cÐê9Ä”›öâÔì÷“ÊY–2&Ó·¿xõ;«sóÆu³2Ó(³M +¬Pöý‹ŸÿÏÞø­ÜÚU@Wk½šÒEõ!é¬Xo µChC·w&ã*k–`Ç™&­NŒørxúS©}\à}p~ôò·„5b¼åM*jHd›Ú>ãýYÚZM¸Æ¦sü‹þÙÞä ©÷AQJÛˆV~ÿÜè—ÄäŽq}ju^6ßÛ,n—Ù ý£Èõ×þìáN |P ]ÁÕ„ð .gê2¡$Þèu¸ü(Ç'%Î߯«¨Ô‚ÛA]«‰m1Þ…«¯V¯þ8¼ùQëß–EX@“·§EÒÏÔŒBz±zׇöàÅù×ÿÔ?ûEUéðᢹxÌ?èÉ®wsŒ—c|¹±r»˜œ‚'¡9wN_³w˜1KÃh÷8àMH³é]Tëƒe`ì oS+D:˜ÒÑÓ¾H?ü~,’¸xÄRsMY}>Xšý»ææëÆìMMhÖjT™F‰0뜋r^¶*ÖTRíZí3#9÷—ÃTÈFÿÄ|¶®ËáÉôÅïìñKµy.xëtMZ›Þâ +/ +ímääΟÝ?ýµÚ>‡¨Ÿ•øFïìäíoœ'Ï‹BŽòµä +Ô²“öõNÏ»½sàtÐ `öQ7ç‚¿íï~¶|ñ{\í?9dʤéÄ+Hû½¢Td£ºÒåX½—¼3)sQ¼þêüÓ? /~e^åéÖÿõÏd¬þÿžÀÿWã1‡6yhã1‡6yhã1‡6yhã1‡6yhã1‡6yhã1‡6yhã1‡6yhã1‡6yhã1‡6yhã1‡6yhã1‡6yhã1‡6yhã1‡6yhã1‡6yhã1‡6yhã1‡6yhã1‡6yhã1‡6yhã1‡6yhã1‡6yhã1‡6yhã1‡6yhã1‡6yhã1‡6yhã1‡6yhã1‡6Vÿç?“gþ™Œÿ¶Pp®·öúåú³‘ÿYÁh0Kø¹³~ñòøùgñg…%f<iŸ½<»¹^?—á.%é Ö9^_fÊzd”i=?;9»†;»GëËãJ¦•àßgx†Èà÷ÿÞ}†pO¡Ç‘,I Íg8ž§QšbŽ£’á3Wÿ¢y”ÄŽcyø›¹LÄÜß'à8Ï°8 âîŸEPÁÓýÿò ÿâí.ÿkætùÙégÃÏ®?K>2åJf4ü¬ÿŸ|ëƧëf>õâ´½~ Krý§;ÍcX“ÿìîrÿúz}u¼ÍÜß›»3$ÉV`• ø7zóÙ«ÿ|ÁðLßœÃ]o2žif¦s<³MÚI'.0éd™?-Í¢û§Ÿ£ÿôgêþÿÑ|üùóýã¯áïþã{é{ßçƒ{vy,þéÛûhþ}r@ì0ï3Ì>~}vtÜñÌÌ(þl +w"÷ ò§¯ðÓýâ2ÌŸ–žÉÌ3$N@$lúôÓÿª'¤óùOúÿç3Ì‘àî§KgÒ'⟠+÷3LÓYÌ°°sÄg£ÆCýš"r“µ‡œ;æ¬!¥0uȸ+>Ø0ö„T»êbnÄÇR°âýª&U1ª +i_ÒèÑF#Ñ(áÜêìÔö±qÁ†öVRó¬&6ó˜œG„:c3fWŠ–Zr&'çBsG7ˆÑ)q ³½–£9¡u•hgu.Ýá Ö_Úˆ ·¤5A¤v…mæˆõ¼¿°ú—˜5ÊàfÚ‚ZïÁäË´‡Ë‰m¼á­ÒÞ±yE q½ƒj1ãöŠ¼K;{xãNß6Öß(½ÜU•VEŒœþu¸úàL_ÚûÆì½Ü>'½YYŒXwJYCDn·€¨àJÄÙ=Æä Þ‹Ô‡e¡YâšÜ¦ì1LŒ¶†‚7£­aŒÃñ§ÿ7§%¡™£ÜíÕÄÈh+áªÂyÆ•¼…nXkRe£é¢bSöfÍÉ xG\n άH8U¹Ê†u!F„¸.¶9oÅúkÚYPö,Kº%&@Ä6®t ¥Ë;sx)DhQú¨Ê7ó¤+†HIMhÖøHö—j°ÁÕ."&yºñ¬*ïUå,f0ƒšŒÖaa ÙF‰´0!ÈÖ¥§%&‹ëeÖ‡§Wù˜ÒǤÚG¥¤Dy° Œ3©‹Íl]®2~p²ˆŽŠmÆšQú„³bc#5–ƒõ[xVa,LéJÁ‘Ñ=“›ë2ã•qx‹©Ãr­ps\âš%Úç쉕œÍ#BŒíä„ó&59.q>¦$Bc#G¬Ñ3›³"iƒKû¼^ŠÑímÿˆ¶§ior½[¡mBK´äÔŸ¿6ú×r öqQUa›úo~ŸlÞ`jSÚjë<\~ëŒÞ1î²®tªiаÆίpiÂÀ¯¥‡ÈÝåf±´­`•o¤m–ܪvs¤¶N³Ý£OË»Ÿcf«*5äæ‘=~ÙÜ~çLß2á–ñW¼;?yþ×ãËêz·ÄúZû"9þ!X|b[¦±AÕ.¬$"Ƹ܆¿ï|£¦=…6"SW²ˆ–¶¹5G°Ú„3A´>ª Pu€c”î›!1Þª"¤Ýp+\Xá‚6W«q JKP)ª°^‰tjt£¿"]H$Bê¶FσÁ äÕaE*`æaMÙ+ EÒA ‹Ì)ïÆWDz¢»"å"B“ÚC`*„?併Ѿ@•ö“ +÷“" ™)ºs=:Ö‚-*ÅÏÊBwjlúáìO +ÔaU „ˆµfœ3§”ÎAU‚i`R+‡éÒ‚¨ò­,îWزÕlxŒ©I™4k¬‹PWq)qzwÞà¥Ó{!xË óñÿfýügvPSâ]Ú0rýž V%Æ¿/¢voh'ý˜o¦% µ¶j´¦µ¯õn~¡µŠœŸÁuF4¦ïÝÉ[Úè‘j³ˆ›÷­ž¶¤1Bµ~]ÐÞZo_øãr°F…@VÁì…ÖÝÑÞˆñæ°¿B¸µz—³‹ïÍÎ)®¶9kdv¯õþã­© HK„+íšdP%O˜cn¤1F•!c-mȘéÇÁ—‡TÛ¬Ó¯+1fô­þmÿü7£«_wN?!F‹±ûZ{«tvRç„n¬­á«îö»höz¼ûLï*i— ¶Þ>ì’âžÓþ1™¾lOôi;XÌÌvŽp=íwR¦Ü½²ÇLBíB&çÙFŽjÚ3¤µÒzÏq{˜% Xg¥u^bƒ,n?«i%¦;[e½ºLÁWDlÖ˜Fѳ55‡è u6ÔƒµèÎ25’ +îß/‹™ª|P“Êl@#¹yÆ7NÿØh]ëÍó*×dÍQÚOé¢r¶¤9Wâsü†4‡OÊlQqhsPÓ®âqÍ'yæIž.â:ÆG´1ÃõIo×¹˜µ–Š·¬œaÓ2˜“§BR@°°¡˜µD•„TZ˜ïW„2irGpæðZ öÌ„ +=ú¶øÏ3u1\¸ƒ+H0pOו¦õ ”È_¹_Ò z·–¶IëÁjqþ€°!¡÷ø´ËÓ‡YÊ)ÐnÚK)Ú˜É.mÒ˜‘öˆrg¸1bì*'ÚõzçÉöK£{âôOÔÎ1ãM¥æ‘šœÂW˜WD£;µ}tHèyÒÆåžSæ„С{EÆ/óAèu L£Â'¼»Qšg‚¬=ÃÔ.m°*†+¾‘ö°F/ÕÞ%nìÁÝ¿£•Þ>b£ycþn~ýÛäø;Ò¹F‘q!.@Ô½º’#m­y->ùã·i«³Æ2íù*Œ÷aÒ9ÄÍbÚ·;©‹-RëC¤Àz5µ‡SÂ\òþ‰=üJ ž6ö¾‘[gyÊ;Ĭ,æÐƈL?ï>D¤&®ôg°tMüLEªn™t +¨~XWQ±Eiƒå"F³K¤0ò¤Àæ0£&¶xoYäÂÕ(R œ‹°@ÓvYH›Âoq}ÈzG€êéiâñn‘4!±«|ùPçBJéâR{¯,p;èÏ +Âw`¡`úª\‹7Ç^rbÅHé,fH?OØe¦A1º²ûwzû¤Â¥¸—CÔú¼W¤÷Ë£õ’éK¥¹ÃÌI!K®r&à3çÌ€(åøîGµ!®õ•`#:à @’Ö˜¶zrewÎo‚˱u·ß¢J·Êu1fœqÚï ô€3ƒâÒâ|‹ê}L¯KðâsÉ_ Ëí`4¸þåoÿ§ã÷¿­íº3¥Ø„d» &¯›‹·œŸö¢€«ñ1¡O„àTMž«É_JͨÑh­]IhY—ù® Ô扟 +ÑÎÜî^üØݼ/ò>m­ÞM´ü2ZŠÖµÎ©×9ÿî¯þ•1:9¤ ZܘâæDjl´ðHpg©J¡\Øn&í3Ú©ð-¸UY€—ÕªõŸ”é<áÄh-m”'¼*×F¥¾n¥`þ´.gQ»&ôÄèRKnÔÖ¥Ð8´d ( +Øš<ªÖĪi‡Ö‡¼5å­ .&&(Ó>0KÓ«l¸/‹šu½@xÜ…ÔªóÍ2Û8ÄÔýšøù!vP“ó˜]¦€F›®iãm$Ys‘³¤ Óƒ¤U¢SÎ]RÆ–*Ô2,~… gö+b¶®gëF•Ž(©_&@9˜T…/ +Ô³’´•ö:‚¬¦+Q{|³D8ÀD˜Úƒ¼‚-¤ +kÏ)4vë Â?ÍS5ž•âúªï!&gM›¯s`fš°®åè¼1yŒß¸ÍvspænwFYcD»Ý7šÐœÑò}™”ÆLô&U!‚W`Aù{GÑò£7¾!]c#9]f”Œ‘ѹhL_ vŸ¾üùÿ8¾þ5:|°”Z§Rë ,ƒnÔäœv ßˆª\œ¢·Ï@}ñÞF…Ÿ6;[œ~?:ÿ¡$´+BBYS©±R¢# +!ÚzýëÎâ­oëJ›±ÇŒ6%u†/Ãå—JûÄ\½ýþýùÝ>f€¢¡š¢-(v@TÈd@€šØ†ïs¸‰ QÚ„&ØÎ3€6ú˜¶f l`#r˜–CÕ"ál`íÀf8y“öªG4ÈTxï§X€©íª[×…äQ÷*\‘‹„Qe}Zï+ ì™`Meo]Ú€QuTeó1 ¤ ’CÍfÖùà±H{‡ˆ²_dkl˜GíýŠš©ë o 7H›<å¤ý†µ~…`ÎŒ5¯iO§ô>gMŠ”_À­ jÂü«L¢+‡EÌ–|Vb‚ª€BðÊéW˜C…i@þ0úèIo¢r ä=i )ÖvŒ+I· Wa’¤ÒAÅhQ¡)sxß—½™EÕÔHj}Ø )8 çŸÚÛïõö9o g0Û}ÕÝ~úcÝçÍ”æÚíÅóWJk òC WJv¶óú·vï…Ý{©µoôîåLÐDs/^9Ô8mNâåÇþÉOGW5¸ú™ÐÞ€Ùìc ïÊrÑ”=µwþô˜5)ÚÁ”äp•ö­ä›}Œ5¶û7þøõ`÷=òŠ<$¬*ÂòîŒ÷! OÕxGY`OXXÁ_€ì'ô¾3¸œý`oõÞE¸x…jÝéúÒœH–²Ê\jAp—i3l&ÈÔ5Áx½ ÂègP¸‰4&€B¼»B˜øVî$=”ýÜ¡Ìw/Ì@áÓT\¸WS3¨YâBx ø±Æµë|/‡§]‘@ùdj2Èx?°×Œ>Ô›pøBô6e¶YîãbR’êT+Ê… òH³~¯~á-ʤ‹Ë"á—I¿B-‚5“Ø+¥iqî +–2A¶¼“ªÐ*ÐÆPŽa ±•Cõ"a#|+‡˜€c êrˆHˆð1 Ù~Uò-’6ܲ`© o„Iu¤”Èî"KX{a¯È”Ä*Kþh7ƒé°hˆì—é¿Ø¯áiï«Ôg~.¥`#…ÂìÅ“r[óæÝÕ;£{ +¬§Æ'´7Çͨe­9å˜v˜+ÜjŠWidM¥_`cTíóÞÂé\Èñ&ÏؤÑe݉o…`)DÜ™¢æÐÜŽ.~n oÀ^ÑþŠrfJëÄèÝ*É•Ô<» ¯ddHMlJѱÞ}.Æ—„·¡¼#ÐÛyÚC•ŽÐX’fÌ2h]RLíRï•iG +–488½‡[#Üž€çÇj´Î£—1*óº×åÜ`ïPë·¡$gævÎêP†e¶^Ÿ¶Ç`ºK˜SÂ,”`wöKl‰´icªÄ×RpÆÛ ZåÓ*–@ÆìUŧeî‹"]¦Á#Œj<ð°%‹éàg!À@bÀš‹ÞÌH‰‰ÊLÆw,LRn)öáƒ/òD¦&AÊaàj•@'´0¡­xó*í|~ˆÖ¤<ˆJ»ÏX @¸jx¬F»*‡‚fN;àª}`|Huð€%†·PÚ/sO‹ ¹Ãº^çZ¨¤8‰N†\߯J‡`1À­– À·G7g·¿Ú«Iqˆgª%÷4PËÁ½t2¨^a¼½2õûÅà °æ·˜¶Ò4¹ V«@àôNÕ`cD[%ZWÅ´dähK8£©¡JȘÙŸÈáÒÞ)­l —šÚ=¨Úž9ý;wp#6–ÆÍ:¦%˜Þ)‹!8pp ‹Âå×&lóÑ»we nÓ,’b¾±ñ¦_6·ßKÝ«´®m@n÷Åh§÷_)½WdŽ=­Šq™ -îEžm¤-ßÀ.É`!YÌ ”®ÚÜ:eYh¬œÉk­{ ÕA€]Ò!ÿ}%ÚÈ­“¢í!*d/ T¬cŽ­{Š.>Ã&‚Ý®³ JnC=ÂŽÃîD”–´DAiwq¥_çÀ´½Š” +o>¬r?uÚË $ØyHªýª’Eµ{¦ë–Á+‰qê°¬pq‘°ÀøçP áÆÉÖEÉêjá˜:O¸9ÜÙ¯j ´H¹["­ŸÖp!bõ~¬=Ý8„ÄÀÍ2åðö\k]–ØÆÓ +—#LÖæ¤-NÅx¯$djjpþ2[ß«0yÜÃKÈI3J<E³ˆmºf¼à=¨+OKÜçy2ÅO®Aê]ÑŸ¦ÇúBÞî u¯„ïý"Q¡Ážê€‚_#T+œ¥=k@W/Õ{z7Oè¨è³f¯@;Àï~…õazð¬wR¡-¥¹ Ì^Mˆ @²¤U`<iÁìƒÜÜ‘jß\”¼D§ qK´Cj‰Ó¿4;çj´r§·´?£ì ×X#ê ,¶@¼ù‡põ%¦÷Y{ ºœ×ÔnIŒ%Á wú’u& Ó„Öq‘r¤—Á-°l°nï„(%\–¸ ƒ˜yÂCTçc§sN^±Þ´Îxñü…Ô^’ê!iå^×{½£o:§?§ƒ Ø+§ií2ÂCƒ€­¤ôa.Ï_ÔÅ¡r¼Wa÷a³ê2> *ƒÞó³¸X—"à£Õ8à5?‡:9 díˆ3G%Ê‚,ÔAMT,Ó.@Á³Ô=5w"¡Äû5)Û ›˜:Ar¸_)¥U¡lF‰ƒî¶Â{°U6Ja ˆ‰mVé’0SáY­‹rfàb§ÆňÐ"”|Ô`2e$EÊÍ“ÃuÈÿ<æ@ÕØôÿ!"pá`¶ø»'9jï¾…30#`”mßþì_#JëIŽÈÕ%P‰ ïs¸T˜§}°‡íéóöìj¯€–ê +û¬c" "dK£Æ”’tV¯q¥C*]RNj,lG˜ÅLôõpÊ3SÓŠ¸‡pÿ¾G;ïŒÓ¯zo°ûŠuGOËâ!ªÂFð!ëΛ‹ xŒðèäÝ¿8 ”aàR‹K›ÇO!µÔæ&ß¿ýk¡¹>ĭヤ±Õ½_þ¶¹ù7SÕd®«J«,D üX©%áò+{ò²Ñ9}þño­áùçE¦H¸ xÁÇA*ªÍ3ÆY@%ëU¥2„¸{Pæzs§·N€IYµ79ÿ–r;Ÿçê#¦÷B9o2>ûéðòçBóÈ WóËo ¬õ“ú´@QZfüb·Î€° L( Ì`ô¤@Tù¦"÷¾²× £l³³+  ²d¸íWA‰5Hu(ûÇ`µÀMƒAø“<õEŽxVd€G¸€Ý@Ö2Ö¬.5«RAFS<)“6¸-@H¨t„ Œpá÷wB¿?,–€]ï–öðF­*é2j׌6´ÒAcpt7S"Kߢ™,^QF÷Y™îƒrÀ¥'ÐT ç@G1j¬ø#øf¶²®äê*¼rµr¸ :muóóeäª<Ň9DÞ+ñÀž)ŸÒMTéùãµÉ”h’³Y¥Q§­g%.‡Y´ÑWÃ5Øvþ¾ïojq&èFÐùŒÑ“ü)@úa]¹Bª-XXàMZíz PõŒ=ÇÄP³e®kp¾z´†ä)p­cÎsž…2ú¸š`JÜ™\_ýG-9)€Í—Ú`dÔÆÆH΃ùk¶‘bNÚ)9œg‹±úÁè’ QÓö¬¿˜l?üð7ÿKÿøý~Mæ кé!ôCÜd­žÜªñ¹3}‹Øs„O´` „ž!·à}A¥ƒv2;g%ÎÝ«ð€BŒ Z׫‰>avq³Gèáìù/þð甆ÿ¤@âRèöNH­“'¬ýšz¤´guÒ¾¹À5¨Ö9“âCз)ßN|üâgÿX•›Ïª*µÀØbb“ ¿å¹Ýå»ßv?Ï‘OóD¶Æ£07(ÛºNUºŸ¨š€®U¤àÔ«¡XgnƧz°É#jô ˆþ—ÈŸ=-=É“€QÀÎOò4Ýþý)KLh"F¼ª?)pàMò¸“ǽ2å{É›Bõ‘|ˆ +ò 4áaU† )‘ÀÈã°óüi}V`³5å‹ó—Y$Y™ò +„ [C´ô˜ƒÒ‡èJ—–G­2áÓú HÚÀ³@d`„ykn¶Î0¹¬Q0ºBÕ8=ðOlœÝ—Áô£Ü:‡­‡J÷úW@t``— °¨¹W–Š¸-ºk=9/Ðð`›¶&Œ5­ƒ1ÇÔg5ñÓÁð:ý+Ö—i‘Z5!e¨ +˜G܉+Ø3­q$‡ë Â×0:¬?¥¼ôT‘è/õx'ŧˆšžRÁäV‰u15‹i ±)aý á­±·Þ|Z\}Ÿg½a§G¤¶l( ÔT”½¨9¼Î ˜K#JPÌsQ“F^¼úëÓ7¿­ +áAM;@À‰{´Ò$§”×)›3ûYÒø¢Âì£*kÏÌäpUmU¹oAŠ÷kH  +×”(mÈ$=.d`|®¿¦äIæ _²g¢½ •Q™Ž@‡c|3È`Íò¨MkC4õ˜a‰3¸WaÛñð¥dws‰AŽ0Š¤vXc÷Ë”3βÞѽÉÓ<™Á›EÔ” ‚ê ÌTeH É_ÕRe&âÔ1gL.Xu\&œ¿<@ŸR™jªÌ¿8¤ +u³ˆY%á«]ç[‚¿­‹Õ¨‰I‰ ³¸w€Ú9Â…4v°â3 -p +i¶&‚µ|–§ 5©JûéA?Ì,ž^á` Àï%Å„Ãí×Uð°,(¬¶zÓ&7TlÔdêÂ~IÈÖõ”£Á3 ù<°6(xO1~Vž€@ÔL+2© b•ÐG¬·ÌQ`8TðÎ#µ¶Üý‰xð§^ïŠó§_Ô„ +ß”‚•äÏÿKlå\a›5¾8ó.«µ+ŠL€*¡±UãÓÖê£1ºË M=)Ø€×~Væ¿(1O«B‘vÁÿ‚Ñ.R \@€N(Óy̵†«]gp=:ýºsôì*)!Ä CH|t,NIµ«xÓ}Xܪ +q™IÏ Cþ(ÁJm,oú§ƒQ9Ì õ*· óÁFí• x°K€T°bF´öG7D«f ~™º‘µI7`/Rk†‡5õó þ“ –­Ê56"!:kŒp +i‰öè .<-àÙš&e—p³Œ[ePÚ¨V"@wÑŸï×òˆwVI‘:¢;#YÄ,ÁÊîðžSs +o±WJ·/½¨¦ª‚ÿÉ~åé!)ÊÊMZŽ¡]#)tÔðh¯®<­Ê9ʯŠé¡˜ûÓ=;¥y\—:@%`+@üÿù^¨a¼åTä=2ÌHHª=°3ŒÑ­ØFÊžtǵ>Ð.É<íi~<€j¨rP3 I_p—‚·SûX" ™P˜šÐöÄèži­©M8w(÷Ež<¨ò˜ñ`NU ¹ôpUì€$.â*´Á¼ ΖW‡¼³äôQjIh*DD®®eëZs 4ÀŒÇ£«Áî«/@ܦiÙ­²ÑAUžÊ¢n÷ª±÷©Â8‡Ñ0ã”˹ùÂ×GÜù„ÀiGGœcnܵ…´žþR“a ;jþyÂúÍ ÙWWÀ!UÙƒP5z§è$b£Ïñ1׸…|û°üð¸M66yäQÇÿøƉº}ôÈ°]ßefßìMŽ˜Á×M4X\<¸;°Ù`ò!­ô•‘êß±±A—û÷G÷Ç,ýõ«ÀK_uJ€Ç ³XâÿÚíºCx½oÔüjV& Њ)°à ÷ófº3á’]\ÑíxC5,X"" 3•°2W„ç ¥E©ÓLjA‡'&а•Ç$Ž~jó(c® ‘€ÊåÇ€ßÌä8s±9³/®æ×Å`•ì¾˜‡Ëz¥ªÎÚéŒÙÿŒp…XqCÊ,¢|^ÓÏbAà"'ìðçÁ$C³‘ªÝŸòÑbPûʵ0ïÊN¾¤3ŽëÀàù€|ltÒ€GGÀçx"X¨åOÌ©8@}ð« +À{‡µ8 Rž“ož0ùAñjqëâ Nh0™8n`€µHØâK9ÙAðÉN¨Ò͈™ž°s4‚°èûDͧ³\4TJÒà‰½êsK?Gí"X}ÀÏ€¾¿ÆŠÐeÂcZh°Uu€ÄSGÇ]F4¤éØwɃŠÎÍ•Àµ›Y0$'Œä‘ñþìÃc.€– Qô.Ù%ƒEÆ]}É0û„€®þ­%4ªCHâÀuè˜ÉwdĪ:jfGúCD™€Ø¼ÍÂØô€Ö=¨óØÉ(®¾ZP +…™„.õï:InF…lu|ŽC€4“ižƒ.»ÅÃ#Žc£À«Öoœ° è‰ÑþêYÞBÄñPÓ§4)¥éd ã0fjÈ@ÓâÃvR­-wñE1?KÄš†¥áà +f€D°äOÎ+›¾äœƒÍ0Yï P‘†ÍŸ–0à4"n> +›?é•a䣟÷ÅÛN.ðƒ÷"¢>3HNzà 36 Bªƒ±·ÓÀ?i˜S'›Eø\0;ï`3ãˆd Ö@É)6©éà+2 zçæòt¼£÷Êdl’JÌ[y-±ùs*•t  &i=Ò˜[0àax™›Ïb¡íyÃmÀªL™û”¥Œ:D¦0уâ¡Î#Ÿ0S®à¨]ïwÊ—„kÚ¨”ɽµ3#ÒFƀϵh:°ù’¤\÷Jð„Rµ1© Oø„´ÃLn REGŽBˆi°ÁeÃ&*i¢ÓH°ì ×-LÊìâ•xHƒˆ:OÆЬژ,4ÃHÄ´ˆ.qÜÎŽ™i­Í¯‡4aóÛ¡Ö|Iˆ½f"#oôÆt¨2d†á}­ø=ª8¨4!T ÄÝ6â‚ÒÈY9PpH‹N*I„ÊL¢EFª^9©¯Jý˜S²ÒS0‘q’ŒT åŠ‘ŒºEŸüÑР舚Æ)Ã;fõyƒ­›³²ÓŸ´RiÈqPѨØrsexÆB†jQûW ò­ý *R‰ŽS,á‘­vÝÁ"fìþ¸GÊ‹Å¡¼Œ†Êˆó +µ¹‰ˆEƒW‚rs9h!&ä\bÞÅfÅôt¤¶ +HÓx@£/Ú\ù““LjÆÆU슃ɘ0Ñà5ˆä äÈHƒPêžPUãUúÝË6ÊÊ$6ÝRÏê4˜õ*m—S›»d|jÈ)Xé4Ÿ¡ÔYo¤‡…[fJÕc¡qå0áäì¾(Ÿž–ŠKljšOÏ'º%˜'›‡Î‚™„ÄaÄÂÃßÑ}n ¼° Yˆ˜“ÉR‘."–ì•TêvBƒ6çârP8¹Åeæ˜Ä$m™Ä#5î’4¼¾è‘ðSƒᕾH+Vßb³³®P…NL1IèΪqÙYFmºLTÈtB¥FmkÜ’ÑíLÆ ,cBÞà ž0BBñALØ£bÕÅ—€àݽr­¿=„Œ>5¨dÄc^©¦”×äòš5ªê†„>` ÇìŒ y¥²PZ*«‰îÉêÚe±ÿ²¬Í˜}©!+PÊÈùäz´´ÆçÑPDgÄ>“6¸+wd¥ +ÒL¼Ãggµã æÆì~P +'W¶×ö—¨%ðpÃo¹ý»Û²‰€øG¥’;Tó„›¾x7X\”«+X(‡I&Ñô©-Rm1i˜ÇŠ?Õó–„¼ƒËè½a+£:ؾ%£â]1³Ÿ:—Ÿ9Ëg¦q¥áŠN.K„+¤Rãr‹Tz%T=åUZ,èb èD`9o¨ rLªS6¡låJ\a×± èTzMħ\ßkjód~ê¬IÂP© +|‹‡›Œ: ðPªÛÑæ)>·êäÍ4p ¨¼?1É|j4ÈXx•+æ@‘¡ëD¼+–a„Õî)X4y!•ab“é©ýXg;ÒØR'φÊËÝõK@àV¦_†áÆfiåRsó®öÖ]Õù3L¬†‡JRqÑÈ€ýëß.´3ÓÔö®\]ËÏ@áO¸8'—¥ÕžRÝÈÌœIÏì'ÚÛ3§n¦{;0ÈFRqq@] ·íî+ë\zVíœÒ‘ŠÞÍ°±*Â&W@Uªkåõ»ËkW¦wo^zâ-RiáÊdvþj°´aólÙé8ø²PGœœ•VÇB¿ZgÓ“Ba%TÙâ5Q ¥0ÿà ïÐÉéA+µÜäYF÷%‚µ]f ”½·vÕ̤F¬~¬ï¬¦#õ“ÑæéÔÔA¬¹5òæÒ…ÇÑPEÌÎæ館\,/ß‘ž:UZ¹RÛºQš>wõÑïÊÕUç“íHm:’ìÎÍ/­]Muw‹gº{7Q!“›Ü®­_‘;Db*5sPÛ¾¥ôÎSÑŠmyÃy#!£¡*™˜b ËüB¤¹mŸd’­dw5X]""u*ÞvðZm’]¢—Ÿ½*mXèÌð™I4˜Å¤"ð0Ð&t™Ï/Ç›§Ôöž_­²j —ËL²'•—ÅÂb´¶žîlÎì^o¬\¶ôˆy˜¸Tw¯8wììF;RyÃ+7Ë“gÊ wè± @ oïEê›Áòjqáb}éÊÉËO¦»»:"æS'éÔÀjL,ø³Ûj÷‚W(š!Œ0) 8)?C%ÚBaŽIvùÌT¼¾‡ë bPø`n:ÖXH¹èdâ †©É³¾h3˜Ÿ •–Ð`)˜ŸWêkìŒ/S‰®Ú^—Kó1#×Vèx]ÊOe¦ö`ðÙt7Þ\;wÏóRaÆ¡8{®º~-9s®o +¹9T«V6fNÞ.Î<~*\H4 /çÊËç3óÊkwCXcâ3aëQLz)·pWfæ¢XÞkÛbq™Mõ¢¥…D}QãfÇÝ„) „&&gšÏÎ{ä.fRµ`vÆàá±[\£3}åå2ÓÃNÿ¨²ú›O&`.ÔI Mh[¬³iŸ¹ Ä{08T]©Î…Ê‹B~X¥ÚÝxøù·a|ôÞ —h•î†Gqá +-̧ë+7n=w×3ß·ûÕÂÜÙêúõìÂ%¨ßâÒ•ÒòUZÜ>{ëò£¯2©æ€Å‡IPsJ}³°p¾wòždo¯6³÷ÀS¯Gšë€dkK©­EšÛÍëëw>9}ö>»Zél5æÏ¡bÞ+÷µ˜V§¤ÒZfæBëäÓçžLNžZ=ߘÛC„ ÌZ =EF›¾XKÊÏuvnµO=$æYp rÅHzC%T†3úÒ2%f—S“§->°‚1*“8|EÆZT¼™iï\¸çùâÌ);›JN_ŒuÎÊÕâòe¨<±ÎÐåO ÉY¨t3’©8__½ØÞº–›;@䆋Íg;g¯¿ˆòÉ`¦“<•™:ç¶  ñÂR°¸ìOö¸Ôd Ñ™pù-0‘z¨´ +DQ[ºsýâÙÉÓÓ+ç§ÖïÀ‚Yøs¥¶˜/.\X>xðà¾ç•âÂäâþ·žØ€—°Ð  à®åêéÄÔ¥äÔ…@¼=½rfcÿš¨–m¾®Ôñø4ZrË•Õ{Ùì¢Wèx[(¯þ ¹ +¶ÇŸ]„q“‹ §¯?=½sEë?íL&ÜØ•j[¾ø$xu"X];uïÅ{Ÿ÷+ Pª«Be]ªí䯀ҙû@Í#ÂúÂÕ@r*ÞØJÏ^PÚgØüªÃ—(vö(¥ AHTPC¥ÅDs+ÓÙL·WJ3»àö­8)çbµíXý¤\^#c5µ¶¼vöþÎÎ5 ©Dëk Bq)5s>ÔئS3T´¹¹ÏÞ]û¢åãf’·éiJmó…Ùôô~¸ºÒšßé;?ËMí‘R¾½rqþÜcåÍÍÍë³û–WorsûôÍÝ;Ÿu¿o­o…+k±Æfvî¼Ú;+µÕS×ÛËçœ\*\Y‰´¶¹ÜŸ[P[»íí[™ù+äL­¿r,sD‹€ƒ‡J5‡Pµsâ:áÒ’œi%«‹V*B*n¤á ׄÌ\fú 8O°—É™ó ”.6AFkv¡8à”†Q‘êRaMƒŠF,(¦ºt´ž:Yœ?+”–BÙNgÔÒêékÏú"Åpi¶¼tG²·Ïdæp¹D„ËÖ@ÚÎåÂå5.9eƒx(KÅ TM¼ã7ÃÅEŸRkLï6ç÷=b.ÕÞ,-ž/ÌägÏ”fOeÛk\´ví¾§ßxçƒîæ•q·h£_óËRåd°¼Å$g”zöÒÃ?ÿV4ßsRjg?Ú9«ö²ówå—®Qx2S›?¯TVFlþ1; Qi“¯æ¦Ï<øh7›hÆ«KZ·ààràjœ||E´º>»s£·vÙ+äRåpyÞÎ&-Tá+þÄt¸²©”×M¸Ì+U!ѵRQ˜ÐPRÁ”Âå¸XËàáF¬ÂFƒ¹^~z¯»}]n¬Û¸Ì 5h&,¤…@GªR~9TÛNôv+Ë'OßbÒS—Š÷£n´orïÁÚÚµhs;Z[K¶¶µ¥ó'ܼ/Ö¶Ò1ås³ÐÜÂÜ™ìôI"Z±úDV­J…Y¥±‘èíGÛ{ÞX×NÅEµ´Y25o{I‹'èWjÉÞn¬½íOƒ‚œ½sóI1;I©-T‚àVÁ䊅ŠŒ»y“GZÛ»I'º·»,xÄ#Àç8Áçrf*/$›‰úÊ„;«.77n&§î ¤WŠ3ã“ûx¬©ÖÖ6¯<íä3Ã_íìèoúþìZtê*—]±ûít4ZY‚ 3•™9WX¸Ø]¿ÙY½«íHÅRÌ.íÝH·×Ç\°£Àr§(užIÍ+]&5åfc [£ùi;¥bŒê‚?5GDÚþäœRÛÔNmþ@†Pù•Is±I#.S¡²Z]J5V„…Š=A¨;È`C¥e13JO‹±ŒU¾»iî±dU{ ¯ó„!Ñà¡Fmé +mè<’[(’ÑVº·Ÿê‚G¦{Rv87³yþA—/ M +Õw˜Ì,þÆÉ[µ•‹raÑA'v/>\›ÝïÒˆ?±€ð?åq„Õ < 4ؼ"怨ƒå5¥±ãUc΄+ŸáãÕ!3¦u±è–j)œ,d´:®XH Æù£…@ªHµÁÞ@,"¢-©°À&:>)›*͵×/ …Y$˜G‚\®Ò±.é2‹€F˜&g¤´¨¶wÔöv¼»ëW >…V*RnÚàååtsþôÓ§ŽµO‘JÃL(Ð$eNH(þø„{ÂB ›hùS_¢Íf§MLÔÅ%ò3ç`6Ý|&˜›KõÎf¦÷cõUJ)¹Ñ1§ÈHNØ'ô®!Ít÷â­½ÄÔþü"“êé=œ/\ŽU7üêŒ-‡9rB6¡•Tk=ÙÙžðzÈ©Su3.»¥’]ªABwsY2\¥"u"Äk[µ•kRaìJ®{’ÏÌi=*®ì]Ôê°bs r}7=s±4ÕŸœu +e “ðI…õ½rªcÆd¥´T[¸#Õ:)–Á瓽bï4T¯vÚ7³3…ô<­´èhŠÐÓ0¸2!9lö:韞 +$z¹Þ©êòePËÍ'®Üûl \Õ£’‹Íà¡2ß»”:¨-^Ö:JºS[¾ uhÀ€øm¤lèß + Z½Ö;­Å  àÆõ«Ã¤þª'`l¡ê•[' ä׎›Œž0ép èÑqgœ'x”Ïlzzå_œžð²&ŸlöEõX˜Šv)à@:2¬Cœdˆ°Çç™8Ì”ÅÓ89'õ +I7e»T¼…G›ln؃UÛR¢5·q©¾tÖˆØèp(7SœÚ/Í]W6(uÒ«Ô-T Æ3R˜‡ÉBÅb¸² 3R˜9˜Ú¹·»yÃL%ò펻å¹ Dìß:ôFœlNHÏskÐKtÒF¯8n%<\2HeåJnò4Ä«é݇ÊsCéÞäòù©3h°Q 2  ^™wÐ@nÂN;(!ÃEh9•š5ù³ OT¬å…r LDè7ÙÜ BpàÒ! íµfO ƒö£#Æ>K¨ÝHe]ml‰`¿©˜•Œ&Ûùés&1·¾{--Ÿu¸|Q¨>”Ï{¸BÿÎ20žJvõg.3jõê\P³a>ÙLöÖ²3ëJcZ©Oez íÍ3“»ç‹Ë»D0Å%Û ¸ÙxÇÍć-¸ÞÍ8É Lb6Y##9_,.vN^~¸¹q Wz>æ +&ùT5Õ[”''Px½Ëu¹xeØàv3Š“‰Ž»ü +düîi:3gabx0.—{LªÊ$K.IÕùD‡ñ§²Ùùe¹=gå#6F!#u š‹92jœ°“`-@ìÒÝ=,³Œ‡Whµäde\NPñ*%¹Tµ<³ÚXÛ¥Ó£/HG›~µg#Õq›ÿðéèˆEk§åܬ˜““4zD°gRy…JN¡ášÕŸ]¶2²©b¡ø8Ji¼¬;˜Š77LjÄH6[œŒÇ§xè¨ jþ1cÀ‚$díô4$+!Íï^îlžg’åc÷¨ÛO JLÚ¨øÑ1ÛñqÇ°3{%6z¿ÚqÀĪOi‹ÙyVă¹ùÝ+Ýõ³JuÊæW­˜çÈx‘ó¿+V*‹–!6x$;“KwΕç.1±Æ0`ÃíGYUÌt!x¥*ùä"Ôu¬¹k-³‰Š”í„ò áâ +ŸœÒzþéðÄ7O˜ÜŒÊEêZ qû€ö؈Mcñ“b)QY–‘@nHï˜pY¼¢GÌ|sÌzÛ˜åˆÆéæŠrq[Ê,{üÙ «Ÿ +—IQ½¥¹³Û®_~àÉ{žzñÉW¿óæ~öá'¿ÿóßþñéûÞO?<¸ñdvr “ +ý-êFŸ›ŒÄR­T¾•©LåÛKÕ©¥éµÝÍs—¯>øä…ûŸ<}ï£koœ¼zëà¾'ï¼ñø˯¿ýüwÞžZߟÝ8¯Î`bÒF†(¥HG+cB‰dµÛœ]™^Ý^?{áÌõûîzäÉ[Ͻ²wÏc[W¼pÿ3½ôíW¾ÿÎwÞyÿµ¾ûÀsßîmœ#B)3.h¤ÃË‹Ñr(U#ƒJ(](O-u×O7Vvò“sµÅõæêÞüÞ÷=þüÏßÿè£ßüþ[ßÿÉ©«”§6¤TSggt6ÖèxXNu•e1yq.^šZÏt—£Õ©ØÔfæv¯îßýÈåžzáÛ?¸øÀSçîýçó1é –ät‡U +¡d3Û\šÛ¹£1=·¾çö×Ï\½ïÅ׿÷Û/ÿö»?þí‹?þõƒO?¿ÿ©oÅ “G'ÜÇ5^¡òêL$¿6ffFô“‹!ø4âO›ûä·¿ÿ凟|øñoþãïÿñéç_¾þ½?òì+³;wª­M¦X)¨TX1¡ÄÒ¥Zwyûà乫g/߸õèÓ¯½õã×~øóg^ÿþ£/¼ú·ö‹>é;?zûß~þ·ÿÇËßÿÅϾ±zp˜±xy¾0¹¯Íú#ÙX¶Ü›[8}þüÍ[÷?ñìóϾüÚ[ïüìÃßüáï}ôê¿ýâíw?úòOû¿ÿŸÿ÷òÏ¿üá/~}õ‘çòS›|¢ „éÄÙ ’Ê–[Ó+ðXÜÚ;¸zÏÃϼøÂëßyâ[ß~ä¥o?ÿÆ÷~òÞ‡¿þè“/¾øâÿ_ÿõáo¾xô¹Wwï¼/Z˜ôñâ½heÍâ•­î'% ÕÉ¥­ÓwÜ}ëÆ£ÏÜøçî~üù—ßúÉßûø§¿úèOýëŸþöŸ¿úô‹O÷‡o}çù»ÀâB‘´½BžKu9µ^hÎO.m¯î¿zßC÷=ñ/½øíßxû'ïòÞ'¿ûÅG¿ýõgŸÿñÏùÏÿõ_P­?þågÏ¿ú#¥8;jõ ê=ÀœcVÊ€NF¥åb,ßžÛ8uùþG¯<üø?ëõŸüú“w?þÍw~üî+?xçÃß|þñï~ÿ‹>þÃÿüßÿýߟþöóg_}s÷Žá\ aB«{Ìì1!,Ä’Ty¦1µ¾vòŽ‹7¹ñÈÓ/¿ùßýú“þüý7Þùùoþðç¿þÇ~üÛÏ?úôÓÿÿøù‡ŸÞõà“…Þz¬ºJGÚþùlž! ŠÑ +î‘Rª:Wl/–:óK{ç¯<ðØ¿¼üÆ»|òÉç_~ûß~öË~óŸÿû¿¾üëߟ{ý{7yòü]°Ñ:Æ—<¸PoÎÎ-mŸÜ;¸péÒÕë×xøÁ7ßzó÷¿ÿý—þóGŸ~öÞ¯Þ{éµ—Ï_¾2½¼É7ødÛê ],ê“pJäE9›+­nì^¾ûëüô³/þòw˜Žw?øô³ßüöoÿ÷ϾøâÍÿäêt–w„d”2„˜ ¸pºPïÌ®ní_Ü8uncçÔåk×_}ãÍ_}ðáŸþî÷?~óG?yÿ£~ûùçŸ|öégŸ}üáǽüÆ÷Î]¹•n.8 ÙI„J5˜™qÓqŒåT®P[^[»ÿ‡ß€vüø/|ûÍŸ½÷ë/ÿü׿þý¿úøÃ/¿üâ—|ðÒ+¯<ÿÊk˧/3Ѳݲ¢äŸhˆ‰j­·Øž^ÜØ;ûðO?÷Ò+Ï¿úÆ¿~û»?{ïWûÇÿúí—y÷ƒô“ýñË/?ýü‹—ß|ûÖ#Oçš«&L3“ãFBg÷›Q("]OÚË›»·úé—_{öõ·~úþ‡_þå¯_þõßúþ~úé—úÓçøâÝ_½ÿñ'½ûÞ/xâɳ—o†³uœiìž1ÊÈ9.’ÅÊS ›×î}ôõ7ÿí§ï}ðöOþÙçŸùç¿|üÛßÿê“ßþ寅ë¼ó³ŸüôÝŸ¿ûþû>ñÌæ…›½«¸T8®AõÎ`£¬nZcD\[nÏxíÖ#/¼þÖKo¼ù­o秿øåßÿñß|ñÇŸ¾÷ë_¾÷«>úèÙ—_Ù¿ãÊìò.o$jœ/”ÛÙR=‘ÎÕ'!F-µf&W.^=óëw^½°þL"ŸbÄ€ 'M.‚0ctÌ€L˜Pê—‚ñT²X®´j­öÒúæâÆzo¶wróÚ;î¹ïÚÁå+wÝÜ:©2=Ç*I2˜å=2T4ØI“ƒ´{h—ÇGRÜÆÎs맢‰d1›Z[]8{°íž»ïôÖ~ðý÷>øÍû}üÉg?ýÒ »jÓÓpœ g]]Ñ€c‰@x}ûÂÎÁõLc:Un´Û…ù™S§¶Ÿyæ±·ôö‡úÙï~ûößþö¯½òòKO?ùЭ{ïZÛØ.4fXP4‡ÏA† (¹5az«—¦…„[]Z¾÷Ú]¯½òÊko¾ùÚk/þâïüéOxþ_ÿåŽó§6ÖKõ†Z¬Ú0jÌävS!xX½‚Å+¸H%˜ ’È•šssËçÏœ~è‘Ÿyö©ï~çµ_}ðþç_|þç¿üé½_þ詧;¸x6SÊ[ŸÕ#‚3‡tÇGí'Æ”—ä|HN·z3S ‹›§Ïœ¹ãâ—¯^½ëÚ·^zþoÿð•×^}ñ…çŸ}æŸ_}å[÷ݸ¾³µK•>Që" nÆŽQ4’í±rÆP!%±´ºséÚ­§Ÿ}ñ‘ÇþùÒ•ë=öäòóïÿðû÷ݸôÄý÷<þЃûçÎ.­,WêÝH¶ŸÜecͯ–C{A— NFcD­2–(¬¬l^¾rõ[¯½^éñ§žz葇^xþùw~úÞ3ÏýëåËwÏ/­¥ +‡8£s±ãF|\ï0Úq‹‹txh¿œ gšÅæ\kvÝÅ°3{|:—Oc'`¤4aÁÕ8­nÖáÍ.Æ…ñˆ‡w{ÀDzÁh–““f;bt¸LNõš÷J ÊQ•°a ¶ ‡ÇôãF· ˜ì„aH) ÞÛà +¸Ün‚À£ñh2—it{+»g'—ׂqÕæ¥GMÈ Ñ5jÁ´Î€Öî5ãV‚àÕ@¤€2a;ÎaŒ$†"œ¥PT)‰¤Z¨T§xY G¢•Z=›É° gsùÌ(kÇ%ÔgW ñµý„öKE›æ…X![J%RJH’CÒüâJµšO§cÕf]V ÐH?ìfV48ªG‡µÎ¡ Û‰1óÀ˜mÄä3"pÍ-äRù©H¼T³/ßµµ½“IÅë•J._è´šÝfEMLÏác†µîÃú#ƒFøeÜÔ߉aFÌrJ‰g‰<ášm.’ähU5Uª5fÍiŠa”h‚å#.O`ÂLÕyúG1¸2X¤C„e¡ÌNŸ‰ãU(j’Kg›­åËÕ™³3[-/D£ ?´#„Æ‚Š©˜éž¥#Ýa½÷ÿøÆÑ%8Eg§`–1œ #‘h2‰EÕ<Ãp·÷R~>²|¸(ÄêÚþæ–W[D¨ò#ºÛ&ŽYBMNÊEǽ|F-L-ìÝÔ Ä[4Äø ;qø¢z77¤C¬Á€p‡µ·Ð· jÜV„C©X \%…œ—QT´&‰6!F¯dÂ"y˜¤ “Nèã6¯ÖÅXÖîaõbÂJ8ü*Ìr‘’²zfp`à ¬ÆE‰`ÚäFm¤Ý;®um@sxÔ^ׄŠý 5DÐIËã6rÌ‚\þ¢±zÆ-‡7H‰9ˆÉ£z¸”1îåäÛ‡5ÇMF'‡Ðqv³9¿:ãdÒZTïd NzpÂ18jÖhícæ1ÙlÇY)‹3!'æwã!­6áCÀ0#ΣcðF~;®þ­{Ââµ0£Ö€ ;83&9}áX¾³¸qNŽåFÆô8d:NRvcü(\aÈ80á´{#cŽÿóÐð±Q›Ãî3‘:§Gh ŸSÛfxbÂ΄Ëá¢ây¨àòðF»Ïh%jí „Ý> ‡KY™¬ƒ¯ +CÕ¸9* qYJi1ê´TÜPÊëéÉ&>9¨Cœ(Ï´ÍnöGÆû'Zp~¹^™>—iŸvPÉÖÁQk<ݳb¡Û†,ƒzlÜ+ÑR%œ[’Ó>0h´#œÁÎŒ¼F'kB$[©[ÿI¬{tÄqdÌ|tÜxlÂ<0a1¡A­zøŒ˜vQªœèFò3˜AٌŶQ§?Õ?2Wtˆøõcã· êû2à.o ¤0.ãôE…EµºyxÔvtÔHÎë\B+þhÍáOhì¸Î{lÐr|Ìáð„Ò¥Uħ Œ€®*_Ô¸¥ ?agþ íᦕìDBJ²}JÌ.Ù©$ÀfÜγÓ\ºÜÝ· ÒmÇõv*îfR8›HExfPƒ|óˆV£GÚÓ§dt@ïuRª™ˆ¸üiJiË…u\¨ k½#0h¨pbÜq|ÄbrúÝ´êí/7mP‘.Æo²QÞËÇ\|ì¨Ö1lö}u +h‰ŒÔùäd¦»*Ì"¬Š (Ÿ££-*Úqry¦L¸CÇ4ž %Ößã?ìt1;3c2!äI¹vxÌ1 qÖFÇ|ñŽƒÏ8ü”•Õroù *@A¹9+.Yñþ€ê etK›õ¹“Z§Ï„Þh çög½b…5áŸ^&Æ)%Þá!Ó5{úyʪqéy::å ãJfÊ­Ó:úì ­xÈŽ‡q¡àbRF4ˆà¡TiÁåO ñ ̯ ëYë +²‰žåo2ݬ“”ÆM˜Æâ…¢>“n"€u&Ý?ÛÁÂ8[ñ¨‘ŽL8F,>3•b¢paå³c_3b@eø_»/êfÔQsÓ—+ tòë·:¦žÒJ×+TŒHhÜìÒ`&0ÚáhÌF;£6JˆV2a!T‹7R›=“l,›‰ÛŸDýY·OE¨„›JêÝAø«c#f£*Ñ7b¦úìàñ`~½¹v¼±§u5ãÓÀ#–€Õ«âÁ +£Ô8¥L‹¤Xr‘ +còÈÃz¶þØ°upÌy~j`c¸ 8áà0>3b&Æ,¤ÆÆ,¿qL|ÔeB%³GúŸ· 1Û=<¸AuÂè?®÷}sÄqÛ¨Ôm~íbmåŽÃÏv‰U21Ç—6åÚ“žÆÄ|µ¹¶vþA¶SÃv¿Á³’ILlÐêâˆSN0ºøã (<âO¡þ”³ÆKÿÈk™t¨åò¥¿Úß-¶ÃDLXˆÛŽkF ¨¡cQÔ!AO¨ENwéXå &™ÕÆeü™YW°hcT:\Y»²zé ±’aŸRã3bb&”™'c½Ãz/ê‹Ö»{„Xüæ ÓÁkFûû+ÑNyî¢Zß–µ•S×T.!#æµu¥¾mŒvNãÑŽ‰‡âµ{~¡<¿%itôoÄd”/ +éE\ªOØ*õÏ• 9锃ÎX©Œ+P`"]&ÞÑ8ýÇÆ,°ì_íè1xe»Oµûâ(›%‚ÅXy2PžxÇ„ËÞ¯–Xcáš™IjPI뀂†MýõðZ—ˆryVíz˜„ן`Â9#Êõò)€«q Tì â&L²Q1›F…œ“F,ä°;Ò©E&Rt¬™œ9o¤éQ‹7!¢Ù„·ÐºÙ çf’™Ð"_0u ƒ;èöˆ`Rš¸¥Qv±Ðǘ›ÏÚȈÞ-ØÅ^W®hl4NGÎ^|ˆR_?¦;>î4¸c0“2||Â9a¥06Uãô)И`v&TYÁã-˜JT,ØÁ G6\9>îÑ&$ìô%q¡F[:—=ÍO¯Ÿ»ßÌ¢¡l¼½™šÚOOŸ‰4·BÕ ·wâåæò·^ÿñÊù[wÀŠq!OH5)»)ïxÄ +-WÕڦѫœÐáv_ + µ(u>X<™ìÝÉf ¸lÁ6Z3Û;CVÒæ‹R‰Tªn.ËÄ»©ÊÒÙ+OTîœp@ö)°™Y:5çtiu–K.‚3Ѻyx¥“-xãóF—¤±Æ,>àa9Õ#Å`{ØH¸|°ÞPÕ§4Ôú¦\^µŠÆAA¨‰Íõ…+0JrsÏ)U<¡ +ØË:ψÑc@:ÚĤ +Ÿš•2óV"rhØrtÜ1b&O¸Ç, P&”Z«u7,?0n3:˜&œ|\ã>2bÁù¬75z†u.„ +ñòéžKÈ2jJ— D›nª’ªX‰„SŒžˆÆ³rv$£¬dgAwÀ›Qa š7îB œû_Ì‘±Ñƒ—–ŠZ'û?²h,Œ›NA¾#B%`r+Õ–_ÎÜ>¤6¸õnÖÜWÒ€…ŒAVâRs‰Î¢Ëf¦°HŒ·}‰.Ÿ›'¢mpµêÊ•ëϖγ霌¹¿nP€yd¢-:Ò6Z îðÅ&lÿ:\~„Kr™)¹¼"—œŒæ=ÕÝr‹Y”Ã#5²)¯f;»µùƒ¹½ûÜlÌàaØh]JÏyû'6‡g¬¾›ì ……1wLƒ +©ùôÔ…ÌÌy>¿äâòFŸªÅd¯Xˆ––¥êäÒn>‰`_kT¼á æéDCȶÓíU!7eÃ…lk£¼rU<@C LnbrcÔÁ•[›g®=ÎM qLjØè’ƒ*†2+Jy/˜…f«àR(¥:a!AqFÌàsgk ×/<ož„æM.ÔfN0/ŸŠÔVbݽxo¿´xµ½õH¤vˆËE+x(oeÀ˜qv&áäƸèQ fïø$¨GBºþ1 ò(-¡ºíϯš™´RJÉ+Ýla3^©@EJe•‰5-¸EogF Nºy|#$5DС IÔ:§ÖFØ} +¨| 9ÍgæÝBÉÅ|ÑžÚÚ—6!ɱ[ˆ8àäK6¬raód¨ ŒXÒõOýâ@ÝLÊÄkšÉÿyhxXã:1î„l;n"4ÖÃåÚÃæ]¾ø¸4Ú?jÄÀ‚×u÷[^’KË¡Òrmú̹»Ÿ Ä›Ÿ‰Ô·…Üiå»û¥©³JmsÄÅ hÝn&(”ŠP}8é½T0)ÍÛ©(ô”ˆŽ7ÙÔT°¼B'&¹Tëíìß|Aƒp¸T•Vã­py%ÖÜŽ6¶¸Ìœ ú¤<§6‡ ô½`€½b9˜[nnÞÌÌôÀ5ã:ªó‡mô€3Š7Ü UvZk7…d—KvÂå+>®wYI9h7ÖîÜ¿ç¹KOäçÎy¤¼¢–çwîòÅêÇL¸¾`a>TXÄä¶\\)MïøäœÖ%(Õ“lrÞåÏ‘¡:äMÿ´º€ZYN57‡Mømƒæq{ü€/Ró ýï‚H6s +ÞW‹±l¢™Ÿ;SY¹™Þ··ùÜ*åQô±ç¿¿sñ3!žÐ£lj!Ú=PZû¾HwØDW,6Vf6ïþÆ ›Þ-á‘ŽÔ8™\¸˜Y¼Â–Ö´DÔ.=ôì[³›—ÁP¹…@rJíž9ýHfò‚ÆÉ9DG4î&bÌ) b¹¤3ˆ :+á"e‡ÐÊ$zÑö^jæ|}ûVk÷aÐ ÀÏ¡!G¡³ëvƬLÿˆKˆ'.Ñ#T²3!­CÖ9üàWq.eDøq+ ~lÜ-›}ý­£VðÒ¨t;øŸ ·“͈è‚XÁ¤¬XdÌ ZÉ9 =v\2zÇœ,Lº•RP!+dfã5øRa¬µ'×wøür¦{8­®·cÌ=‚ÆÅN8„Q ;l¤ ˆþ#äçý©i4X‰V7ÒÝ}65 . +¡ü¢œ÷§zH¨fÄc?4j‹A/œ_” +«ÁÒ:›™sÐqLÈDªK¸R2`AB®0±Fº¹º´s󎇛«wº…T¾¾xîÚ“¡ì$¸¬Hu{ñìãgq‹ˆÏXèø ‰@™”RXé¯AÌ^$rÂÉAügâ=“=<æ³PV<îΈ+V:AD›À`á*pH=Ó\Z:uŽÔìtÄäåì\ÚÆeZ±êòÂÙ‡B¹éa+aô†R“çAFÉHs¬2mB…Hº—oï€,Zqòn!ç‹”:M%§¤òBqþÜöÝÏæfÎÖ` +þÄi;iÈPÇ5øH¿– 8Ÿþ§ãºQCƦøÜ›¢c“Ã&Èq”‹‰PÞŒ‡¦O^ß¹öôäîÍÒâ%z^‹…iûGÁœ¾û¥pa ¬¦úc ÈÉ…%-"Ý6hÑ;éX~ŠäSGNèþé¶á­×¬±ÅM±´)×dÊ„Ê´Ü° òáÃÀ8z|Üs\ƒiì,ø+=UYœÜï=fgFû_!abÜïjQ“ +D´ngSvšˆt<á6*–„‚ÑQ·/¬ƒüBÄ # þ<úÇUõóƒéM•–åÜämÆcZ—Æ%x¥J¼¹“š:£Ô–99·{þ¡ÆÒã¥ü|sýFeùšÚÞ/Ï_,.\"¢­ãzÌmz¸´ÙÃ÷½4s°%2܉T·¼¡âa a½ÇNFÀ6€Ýµy +“ÆåþîH·ƒÍ%º(›ÄùD¦³‘›ÙÏLŸŽÖ—[ËwTæÎÑJU[©Înº³“›pSByf·ÐÛ’²Ólf 5| (‚V'³³—;Ԉ%àOô?QýÿOWpôÄ‘1—ÉŒ——nö›‡‡¿~ûˆF@(Q‰O¨nÂ#G5¤ ÛéÌÀzè¨vÜâ×Øyƒ‡e%¢šþ†ýH¸²‚‘hvÒF†AŒ  biƒI/‘±I±¼9£oÁ»:Ø,Ô`ž™°ÒVFïà ¶èì¬Îæ_úÍ!Ë¡!+$V6Ò蟸>b=¡C‡!^DÌ“‘­ˆ‰Z¦¹ aã’Õ…óÕ•‹‰î6¯¹1 1zû§“1rÙ‡´’»/2#—O%Úå…»-ýß^XDsÔFi\'#FDƒeˆùîf¶¹î$Cv+͵7®-<²|þ¡ý{þ5ÙÙ#ƒåÓû÷^¼÷LÊšð‰R©̯f{õÅ«6_ú›ÃöÃÀ!T’ILÒ1È•Q¨î=f²„nÀm2àBÃU'“„ØHÙhy¡´zÉÅçu.Q­lfÚç”ür}þ Ñ_\·3PþŽû_¤Õî1W†±~œtsE¨;°úv<åFbÔJ‚u³ó¡âRaæôê…‡:w!R%mmœ¹ÇA*:·ÀDÛ¡Ü<¬\XÅ‚Í=>bò"tÄ€}3?æÒ#’\ZMNî·A¸ŒÕ”R­q›oÔêhÍž}¼»÷PqåîäôƒËß6ä°c‘•³:¹äícöA3¥Eaà"#•“‘êÞ‘1çẠ3æìÞb¶Ò#6? ÜÁuÜ)¼ã¨ÞmFÙA]ÿ¸ 3u²9o¸ÖX¹´réIôˆMš=• W¨X“R{ì’?¿f¤dD-. ˜ å¼Aø)•ÀõA:08ØccÖÛNèŽ › ™´¨úþ·Úü§è[è i,¸WÈRѺ˜Ÿ çãí“þä$*äE0HjÅA‹Nâ­Œ ix£ÔuÚˆÊ:;RŠ‚” <ƦÝLjÁì c,HŸpA±C ò 2ú4Šáü<©Ú¼¢Ñ<ƒJé¥gvK ñêJ¼°0µv1Zžuøc|²§Ô¶0°ÍÙ¹²I©sf/„¦ˆO*yùÌ°‰‡lh"ûæ‘P:á äíý|<4jøú ÁͲ±Ž˜^ŒÕv›÷P‘*Lo\ ¤§Æm´á«Í“Ô®ÚØ8¦ÇOh0œ+yÅÒˆÅwÛ¨õШuÌFcB.ïZpÙèfµæ¢c;£§çt¸Tœëœ‚°£s±à`³“H ­w°£Fâÿáë¨Ñë¦#㢿‚º?P{„R§ãm4Xò„ÊÙîI øÜ϶ÖSmxÆÈh±p©Â%ç‚ÅQ+a/¥4øÔ"›³±BÈ1›xÛ°sÔHäkK©­þ1qZD‹ˆ^¹&UÖãíÓÀê‡GœRdÛ€„£`ñÂL´Î';D0ùbÂØÿtEïz…Ì» .Ù U6ƒ¥ÌÔryƒQÛ:Œƒômï‚®ëBèNX ¡x‡ô@,xûo|õ½ ýÃêY÷ÿÇÑ{>IrœižÿÄ ´.]©3#Uh­2DŠH­µ®ÊÊÒªK«ÖZ¢F7TC„ œ!9 ê™!9ÃÝÙåÌÊ3»½µûzž0 ƒU'R„{¼ïó>¿ðpw6í! „ŠjgñœWŒÀËÍ­\}kåú;¹¥ëlfüÌöæw»‹—ÙpÍ ‹&Ÿð "¨ÅêB5» !˜å´/ptzÒ«² 0M@̘ƒ89î,µíæ=ƒ;´MR Ÿ9H¤rݵÆâîÔÆÕÞÞ½þÎÅÇõå+áæ–[ˆ§+óƒ§rŠ”^d#mL©³FÏC§€avaÀ$»œ° + ¶NKºÉ˜Í':àÀ„“&¥,Mˆ±zÎEjà·|"0 2Rclsmãü#&\;¨Ëƒgªzf^ˆt0)?é´x#]]wPÏ»Æ]¼ ÓÁ:È\)§!GkÅ©­q'5j ¬„Š«ùþ…ÒÂÅþÎÃ`qä~±³>³qcÜÅ9`Â4ÐK“NʉH¸˜Ù”¬/’jDš ¤ #u­ºžY¼>½õ¢žéÍ®œÿþOÿ ${£PURz9^ßعñöÆõwèèô±1ØKFhµr”†o$$—Ά§ÅxŸ U9µSÀÂ:5îæFL¹„a§dö¨b¬Cêù 7 ză!aör žêóT¤I…jJ¦«¯)ÅY­´mïJ…e:Þ¡"•Bg·¿õ"k¹“Os1\ÌbµÃ*(ˆŸŠþ°vÐãç„u‡/àÃu5V.N¯Ë‰f¦ pïf¤¶dÔW•ò*éxø´jTç×®L­Ýs3Ñ3vÌìã`>EéUTÎ2úàYkFÍÆ‹³>:4¸©ŽÑ@ WAõ,Œ{DÐÀq=æ AŽƒÊ;l#+áã!XN‘¡"©d›«Oßýáûó÷µ¥ód° æÔÂBaæ(ÕܨMíuç„p^ ùH Õøh +|b >62¡¡e‚¸a y|ÔslØ9l†Ç¬€ë#R´UžÞï¬\qÓVLs11;¡ŒZ¤²nT7ùh €Uej3U[±!ŠÙË{zÓ1p0å{Éâìé1ÏÐ$ì>AH¹p}ÄJwa*!eäxËæ“ÜD¤ —ÔlŸ1{Ê„³J.Yœ÷بk’\dšÍ´W5×Òáæi3N«99R²zØ¿:eþÖ˜ûŒƒ7ûC0ŸWÒ3>æ¤u³_xîŒ §-ªäôÊ2€>*ÜÐÊg•ü2ªÙÑm ­pÉfØÝþùÙÝG›×ÞÞ¿óÝ…Ã'˜’!XL»ˆÈ£!~bšôY¼âˆƒ†Å°Ä#.vÔÉÚ¼<¨JnJ¡Â™üÔÙ³_ª.Ùh•ű²žlËkƒçl“]ðëƒ ¤´œ!®! äJÀ£¦›{ù©#ÆèŽ:yÄä¡Va(mpÔ«‚cÜ#{ ºˆ§ÙÃÛ,(ÉÖÔú•þÖÍdkmjõÂâá].VÆ䤒™íéõ«Å™C7s`JϬ^4òÓ€†\˜fCC,â&㘘PÍé—‡L¾áÁr|^Ð]g&½Cf?Èi@ÒóóÁ\ŸR3³goå»;nÞ ¢õÄôùÜÜUÐ(1Öê®\I×׳#BZÏÍ3sŒö @ôéIï_3Y\,“›x~ØrzÜ·Ç4à3Ç!Öìw‡ ö¤Éë$‚¨˜>!Q[—¢uKCf abB°8nÇOºí>ÑîñðS‘!32böO:ÀÂ.Ô‘SØ &¤¥(£¦0)¥¦¦o½µ°{Ø6›¢µ*¨éFy’^BƸФ‡UÏÏ%€á¡eR.3>.9n'è@L‹–Ãù‹f ÌY<Ò2lAG„y •Ò©q÷¨FʼnÑ€äB”œGLzù¦–@êEB´¸¸§½v-ÝÜ(Ïì7×®•/é¥e\ÉF‹}.R2{Y7®NºYP;ƬˆŸ “rV4êÀe[ÛÐR“ÏîP)©­Ì¾¸~ýÍâÌÁ¹¯ž»ÿž”éy…,æüB† +Ö•Ìb²{¹°tWÊÍr:×Þ‰57Åd`‹k°Ú­jà Ñèy™ä·‡ßrž°à>µŽƒW¤¢i°#ª„KY£²â¢NLç¦W÷o?xý“µ‹CõÕo†AKý­{ÝÍÛŒQrÑš”œ.Ϥê«x3Z˜b”ĤóS"fl°<æb€]œ„$+¬»ð°I€ë~lÈ1b‚Í.šÑŠbt + T||FÕz«çƒ™6,J¾W_¿ÙÛ{0>}Žu;@<Ûß¼üâBCvÔâåAA„HÃMvÖè¼’o÷÷úgo>?äxŸMˆ©>ªƒK ŠZª²8söàe1)ãˆðºdT11bEpvàQ'!jÒEÛ¼Ò¨"æ€e85js".ÅÍnDZ¢2Û]¨rI+­¤Û;@©p k×Ö+›@ p°“NΉGÈPS§'ý \hÀ會1JÃGè&6jAÎLøP6‚‹ñoŸ˜üÖñ±¿>6vìŒeÈ‚Øá¯æÀ)eë‹ÁTgÒŽqjÆŠ05OG[rvN+¯åú—³ÝÞÌÆ÷ò‹ûw3 +—Õü T!9çd­ˆLð±ÞòU'99jš°›œèÄ`w 1T¢Õ4¨û&J¨%1=•éž-õ÷Rµ…K·^>¼óš˜l"J>T:,¬f:GÝÍGµ¥›•¹KB¼NM­ìݦB)3Æ;i¬'Và£St¸cÃBãÁœ{#žÀ1vÆNÎR³KrzžVò«Û·Ë½-&R’3ƒå\ÒÓê+w^ÞºûÝôô~®½¾~áœã->RÇä¬w°{óÀ\q`V$à$Âl°+3ðݤ1úÖH)?b#Ž{É„¨(Ê6P )ãÞ´ "%@_9é8c´Ú+×kýÃte1Uõ ©&•XÝ2ØE±ÂA“G¶ûu”Žåª+¥Îö±aÈîç!<`ø“8ªW­ìå’ÕÞn£¿t@–¯ŠÑ£öðâ£5XÊÀL,]êgª‹ãt̆Úý²Ÿ +ƒBi÷KclÂŽ©Ñr$ݲ@ÁåþÖâÑý…såü²?Pvщ™Õ+ŸüðWñÌô„“îÒŒVg‚mRoX›zœ^,ÏW“­IÂiÌŒšËT‘´ ̆œˆFˆÙ!3ú— ºF<.΃j6·èFƒ0¦º—ï¿5éa¼|Sst¬%æõõhe¾·°ûàõÏŠÓÛ‡sݽÂì­° ¥f1½aXN —o=úŒŒTÿêÄð±!€Äa >áb°“b¼ZŸÛ¸ð€5*@¾½ˆóL;\žÍ÷·í¿œÐò3ñÖi¦ú˜Z¢ Z¯ª‰éLs §ô\`Ž :ˆ ‹ûäQˆG¥<n9阇O »ÅçÆ}ã^ÉNE¹X ¨@v~ýzyz‹V•ü\²{Pœ¿<µqwíüãêÊ%µ0ÛZ8|ý{_s‰®Šàz™ +V€±³ßìÊ= è¼l{g°gÊ7Ë[¿YÔä |ŽP*Cvjăúbl0ú<Ø&» v¶wŽ3jŒ’®OïÄòË™ÒÔVuv +-hÀËÅ@T°zÀæ$’µ#!'t¸/ wÆ„µç‚ùQ'qÆN3ÑnaáªÑÜ]'sajªáa ¿Æè³’êVq§d‚h #fˆ6¹(ðŠQÆm€ô›™k^*Äé.TP3ÝÆúÞöýþö {Ժ˄0ånLµû{6ù¹´hWâ8ø¡huÒŽC~ŇÍÊ‹i@¾l5b†Q!§¦õøý”;>⳺E$ù¹ ot(­¤ÆJÈãeCÀç륹úò¥ùƒûó/VÖo@¼$·¶t…ˆÔ}r–s^!ÊyBå \ëoY0ùÛ'&LvÒ‰-Ùäf-^Ö¾=ߘ߻+I TKË|²£ç2sG¡Î.ª0Zvëè~óºÓÜd“‹jf¾1qjåZ07O^.÷ž<û47}ø—Ç-#vÌ‚È2ª¥gr11cGÕ¹­;R²óܸgÈAÚIb€Ôéù~º¶0»v¡µrÑNiT¸ d»r²AGŠ©ÖÚʹ7n¾ƒè¥He%ÚÜ‚E'ñ€êC6" +"Í(-GËËVT>>áEÁuŒO³Ñj†„N[™Óƒ}Ìåc“® åe"Rº¯UÎÆZ»ñÖž‹ŠF3Ó÷Ÿ~ºvé)(ÐB¼®gå|_LNÝvº‘9½R˜:‡JCßøv‹›˜plÔ륓ÀQק·/ÞyÜ ²Ñ6e´lš1:Fmsz÷‘œj·çŽn=+LïZüª dn ö¨¢c7ïsøyPø,~aÄŸ2Þ€ŠI©ùTc;”éÔgvz»÷V®¼º|啹ÇbjÊ‚ix “lœ-ÌÂ|Œ —}BÔb/mŒ;¹ g†³“ñ da^ +×¾uÜtfÜk÷ +Vˆ¶AœÍ¯M¸eLÊÍnÝÕÿÿ:f=c&€OfŽœê;ÉØwθÍ>ÞIªNJ…¥«ëÞܼ››¿JÆ{d¤¦æÁ’ÈÀï}ó,ÐR¨´ßÅÃÐ`ý^‹[pø¤q;ùü`û*hÄŽ»HÝI‡L˜2îØh…KÖ¼²,ÍÕÖnÄZ[‰ÆÙÜô~º½ÉEŠÀ?Cƒ ,ER-ú¸°ƒéZ¶ÔZÙ»ò’k>?d÷1øƒV@õ±ã'÷òñpa ¤OLz@¦› ¢£ùAôíO» çÊý ¡FgÛ›/,=Û}X_ºPž?Rr½P²õäý¿½øä3tÓÑL÷ ³~?Ù8J6ÅDÏŠ©€FC©&­åÇ þØq|’uÉV4ê ¡üÜþ§î¿å¡ƒcÞŒk..A…k™öf®{”œºè .†ëE*TV‹sÁâìÒÞÝÛoh¥^¬µ\˜?7Xž¨¶‘î^¬,ÜŽÖ7T¢ÃÊ œøxGLÏ1ñY!½D„Û~>ãÀõqã’þ@Á +KÈû8ÐiYÀg."ÅS˜œPsýâìÅúÊ5àâ@üÓz®ÜßÖ/ÁòRkýöÆÕgsG/«+B²áåuBŽvW.s‘¼›Ó‰PUK +p9²±diVM4˜fó¬¾Áæ,<âÀÃz¶Ÿ¬¯€:rlØågcp˜H•‹”“ÍÕÖúõpmÍÍK+çî<~O6Š~Ö(ô.f§/Æk[rr~Â+ Ûq/®]~\hmœl2«ù„,ŸWÎfzí}\ˆ_¼ñòÞ•Â;° ‘úÚÛï¯^O­l8ˆˆÒßxÿóW&|òÉ ÄäS„ÄœVXÇåS&ĉJSÛñææt š›Š@£BU5·ÈÌÒjæèÖ«W_ü@Mv]DÜŠïÇkëý­[ûwÞ-Ï_ÆÕ**å¸pËìA}bòØ ¸AðUˆ*uè;g¿Éô'ü° u¢âƒ-G}.æ¼d—S\¼•log¦ŽP­&„KÅÞAiþ|}ùÂæ¹û×_þn¾·%çzg/¿~ôð{ýÃW@½¸õô‡í³÷øX;U\\ÚQÉtǼ¼Õ­LJqÀ¹&ˆñàj8^¯ç¯d,>h®D[›ÉÞAkýfmõ²˜jô6®”/DZ›áÖ–”[ æà©æD T.)ß·±a¯’qIy@0zcÏ¥ÖLxÔ#å £©—ü©âF V«d°†*%TÎÙP`¶%LHÀBÔ+„¢µõÎÖÃööÃÒâµÌôQúVÕÓ­Ùƒ{JaV/η¶DÚ{ljšÖ±Î¬]rSʤ‡”×ÄÄ4ÈK±DD,^á£J¼œÒwÎ@§&` ÕL¢¾!Ä:v4hrqZ¢å&ÕI³úh@òGwÞXž«AÆf|T ÝZÍ÷Š³¹éípyNŠ×³í³½½»Ó[WŠý- žzaŽ‰u`9«ÆÕ™]>R<Ê¢ ü<Æ*Æ츑Àߤœšp‘€V`!HO5×n¬]{{fïAeáhfi÷Ç_ýö½7øÁâW󉩣Pål¼µ;µuóú›í³·òSÛ—}Ȧz2(,%¦Ó3Wçο9wáÚÙÛÅÎö‡_þî忲SÆ„Oó&`Å%dØD?TZÍ5×ÏßxivïvaáBuõjaá\¸¾\Y:ßZ¹²~éåÃÞÕ0ÛÞ8¸ûîåÇ/\Z¸¬æ }ðl'îXP}ÜÍ@”ᤢ@é`]Œ·qµ@„êV4Li'¡ŒA(ðft䛋ªùØ„ øªÏŸãcÕp®•k,D+KbjUkˆTÄ„ŒWÏ|³‹4 ¦‚˜½Ò`ã‡ð)/"|T%TJ‚ø¢-àʆ¸}lÈ2&À…sjºS™;j¯]¢u¿Nr0§ã›'î0›Oôà•2R²ë&ÃVÀ4ÞâSÌnaÒE;`ÑFSíÍöúµâì‘š_ðIÙ 7/Kz²ãc¢”š÷³ILÈKFSVDö¦cvÚ «'F¼ÇO»ŽqŒÙ(/¡ûISÀó?wÒf˜1;ñ!ÇñqX““¢””Ÿ û¹H°´*¯ÑR8ìPkµØßOtv™6XГÍhužÔ¢¸¬ Ñ"*F*k±æ`= ,ƒæCu6X>cFG­8h `˜‹[<<ˆ±3“@Ä ø9ƒ —´âB¦³ÝZº0»vtpãQgå(?½³xþÉôΣõËoÌï½n®É±Ššleë‹›Hé)½°P[¾ÚÛº  ÖXS²S\¼ËO]ºóúÞ­W´ÈÌó‰ž^\;zµôj¢¾ÖžÙüôËŸÝ-Š7VÝ®.Ÿ;¸÷Ö«ßûÅ{?úÇ—>újûâKï~÷oï¼ö=­¼¤•–¤ìB´¾Ó\½»~õ½©'˜Ve”lt~nÖ‚¨f8àân>)dK 7Õü¼Ÿ–§Ö­ DÛÜ ›h3I‡…ÞÑÊÅ×È`FŠåDƒ×ó½ åÅ[üº‡Ëç/$ûL´cõ‹¼žñ2QP[íHè›ÍY‚£.“sÉÆ&¥m¸†(…úÊ­ìÌ!Ÿì²±.9‰Á*4­ÚæC¤š¦‚E:Tb5R˃Ò@ûµy#Q[ÂH‡j.\7C¬ T“`Í…ªàÒóFÕN†l¸‚2BºkìÓ‘6ÈÑR’–“0šÅ+ [)THJ±&©U¥éÀô£>¥~&ê¡Â.ÜDhÜA<èQ.þ9j'O›Ñ!cAƒ66Ø1GNR­dkÝÍÄŒd9R˜"µ´–éÈé@Iä´Q\ —所ÁjQ!V„CLtåÜ`Càs¹LöC™™qÀþÞChNT6{øÓžã#N‰ÚȈœ +äç¥lOHt¸hËë’žBDcÂMyØ ¡åBÅ%£¾®mÒFÕ3‘lSLÔ쨨$»©©£xk/Ý=$Ô¢´¢Çm˜.뱪ÅMŸž€NC#V°0&ä.MŠ™|gÓ/Æh-«¤Z‰Î†ž¢ÂEÐ'©Î>*Ç̨¡U#•³‘Úö`Í+ÚEDN´1­dÖEDp9#Å›:Špd½ÙËÚqŒÚý¼ÉC{èLë’bÄ¥D¾Ã¨ 9”)4ç*³kùîœQšâ)ÛWç3¥æꀡF{çö³æÙ '¥p ¢BvÃt,Y]ܾ‘h,V ħ"Ùy55Í-7qú ! ­ì\ð1®Õ•Ürº³ßÛ}8øbgã––›åñ›wŸ|õÛÿ0¿ÑJáúVtêbyóñáƒï-¼m/«ñü‹o|´¸ßä Ùh>9iî箜½ú^¤¼N¶n>|÷àÞ»c^iQ­ 1ƒµéÍû[·ßÛ¸÷‘R\Î5Vî¼ñƒTsÝl˜ 9Ó=š;zrøÂǵ¥rjvïêk±òò±1ïÉ1óÐI\.ÄËËíÕ묚;wåáÛŸýŒTòˆÞ²K|j^έÌ{}íƇ©ö‘‡ˆ\¼óæ…ûoéÙ)Ð秭È,¬ø˜$l2ÓÃb}ûƹ«/Ùaù['Íß:aúËç-Ï {m8ð<ËÞ¯4½Eé%.Ú$´"à\7;Ø̈³²üt0+»ð€‹Pa1)¦¦ÃÕõÒÌÁôÖm9×÷PZ¼´ dû6r0ÍjÒ+:‹W$ƒWS.?cq«X\è$Dy„„WÊØSü²^!m1R—æ²sçf^êìÜ+,ÝÐËBEѨ¸€•¥£€ôÇ\¼Ã¯|´¼ŒònÊg¦Á)ÙQɆÇÜ¢×í¨HDЊ ²;aÉ(/”çµÁ'°â'õP²aä§P1<áB>B¨i>V©,ÎîÞn¯^V’­p¶£f¦|[çƒ9Rˆû•`ƒ6¹Ù3ÔþÍ„)>Jó:6&8eÃ6RT ç£C>6ŒIqp»I6*í•\sU£™•ù‹éîv0ßeCYVKB 5œÑäGì%gX½ˆII/t"¢ÓÇ1RRÏôh£mò«ÁÂR¸²*':µùsëÉÎ&̆ÓùæÊîhafã‘â|°0+'ÛL¸¤ÌG‡!Lcëzņ,¸Õ¯øøŒ’š æ—…H“¢ËVz•Þxƒ^ßЪéþåLï|¤´”“0.ήžïo^¾ XA ˜j~p–ÓŠ“rTÓ`ñæý·î<~× +ËÇÆüÃNq‹úÕj¸º.­BÙ×Þÿâ棷'läña÷$$óo4Šk÷@„X!ŠŒpºé£‚@41®fºÀû•fvÓÍUXŒsrlzñ ]_´"dBULÎâR³&2nq»<(+ÈN×éÃ9ïî ƒ¼§1µ„óávociç–žŸa£U-?§æW<|Þæ1Fg ››¶{8?A™8%e•Dp”KŠZQP²ÅÚR{é’ º™°‡ ;à€ÉŠΙÇŠ3“.€ð°µ’2„¾S˜ØÖÓ)«©Vªµ¡–¥D¯8}¨ggݤ^j­¥k‹6ŸxÐ…k· 1/S`õ8%³Ã'k ŨŽ;€“”- +ShæSN,œ°8p&EDÌ’SZ¼…ò1`¨l°„ žžž‡ÄÍO©~ZG¹0ð vD¶}8”:aõ›„ÙEƒ"ØÁ>¤^öñ)‹—–úÁbÏŠ(áÂ2âõœɳ˜"9͵]0íð^ +pAX5 šúÁâæãu ƒG¹€¯¸a‘—"$o¸1Õ…®Úæ"5DH â`F$ÊÅ)9%…r©šøиÛ¼::1óšlJ°4§FbÕtiDøÀ±[ˆL¢OHyèÂ)+f*%Z<>îÌ KÍg/4ÖïT—¯Ø}’ÐP&äÁE.z`ئõ¬šjªÉ&.ÅE5!êI”Ñí>ãâ ½ ã¼´>aG}˜äö3>„âÄŒá/£œ®f[¸žƒè° Wd-µ¸z¸yî®΂T çrsznÅÏn˜³ChK^RƒÑÊ0FÅ4jR΋*Š–Ùܼ|ùÁ;.Z·"‚›Ò]ˆâ¦—‡jrâãVø=³‹qÀ²×­pÀììˆLJ)pòˆñÓj²º«.óój¦/ǧI¥à&t5ZSâ W^:¢gf"Å Xn‚вyåQ“Çê‚y9B˱çÎØŽ¸&\,Ìe0©äe’N4X,˜!9`TÜT ¨® Btc Œ®ŽLxnìl…Ϙ|Àx˜8à8˜ +ùÈÀ˜ÙÔ´×Ý°ìÆ4˜‰‚Srø9à?µTm‰¥Àn)DaRqz » Á÷ÍV¬v—Ëíó“ +ÌF>æÂÕIŒî˜³yùcgì£&؃ªVˆ…Àõ"T_´ºh7"Uf¶=çð2óÓQ?A¹(ÈÁ‘IÏИcÒŠ „6<é6ù~ÅO„´XI5 +‚’ñá!ƒ}Â|lØurÔ=2˜‹M8p&è#åQ«ÿ¹!;Èñ‘Z˜l—T'œ„ÕC ™}Ïž°Cˆ‡ 8PBÙꌑïJFÉKª6/1n÷ZÜ ÊƒZñ‘J4ÝÉLmŸ¶aÃfÏð„sdÒ c ÅÈJ$Mº–ÈZÂâç9VnÌ%smð‡tSºˆ–‰CThØŠ[=¬Ó/pJF +½¤brbVˆ°{iT@¿8a§¼˜®E +¹ÊL²21@$rÙê“À½¨òù!׈ qÁƒ¦±NxD'#ÔŠ`´‰))Z…^Ö’í…6\€\®ÓÓÉê²’˜rA³›Æø.ƒ'ë`` E`Ý°àÅD«‡5¹L6 ?ëð± À€qåC5ZÉH°Ã(¢ÑšC¨ 5³ÆGZ.༼b¬.bhÜi÷2 ׋án³Q¢€óyþÔ$B©”·¸(³“³¢§ÇÜ 8}¨L ˜:=,Éǹ`ýä„û;§Í}râØ°d‡Ìàœª£‡WÑ⸋´ú¥£b +WJ~>5 ‡blí\ñý¿3£ò_<7|bØêô2¬“ô¤ËrRpóàjã’ á‡L^³›1¹hD#ŒNŠ:…bùxižQò(gø™Aª’‚îC)’% ¡œÓ‡»<J)z¼†ò á˜ÕngXF‹%\gò&âôœ’åƒm¬Y|Òi“4<ÀŸÂÄ( Ó!H„¾t…ÃO ¡\´¶QA',:|<(àÿòjZÆØÉqׄgc"M8p&‘D6ƒ¶Yœþ ; ô\I4ãµåoî´p˜˵ւ™) @¦Àe2å§tLˆóz¦ ÐíF²Ë´œ^H:Ãi0!ÃD%$‡1;<áDfóè²ç‡Gœc€¯OYN9ÂœÊ=*›Gò !§w8½£¶3c6›Ë«©j6ÏçR2¨ùµ)Ð/ÃfPÂCîÑI¿‘nþôˆã¹ããÏ=?nq0L O‰)—‡¥-hdñ‚̃vûÀkš|ÏŸ4U•ãñË“Vû¤Óe#I4ÈæŒíÍÅÝõÖêLªšRÃAÓ<” dx°Û¯™.ÈšREMVÂ1‚ ‹zD/°=f/ø‰d¾’mMóÁD4ÛDù 罘`s“§Æ\ß:e±úe?›šôƒ®³8`Q‰jÓ¹E! ·Á^B•‚ w +D æçÃéêŒÉó¡<Hš\„ËÏÂd¡DNÔI>äEE˜ÒI9K +Çx†JÄ‚Z0ĉR@ B’MÃõzzf¾Kñ@)7ÌCÚ‡J0¡¹|¢ÃÃ{%ƒ?çp:\N«Ói—e¹’õ*ɵ~ùÂîôávÿÁÛ«ëóáP¡̃ÚGP¡ÑIß_=7z|ÈfqP@C,¯ßKQ£+šª(’ðøp'b^ YÆšápRdù¨,8|Ì©QÛsÇG4AÃ5Ž¼ãfòØÏs§]'FÜ“6Â꤬v¿Ó G’9Êú`ZVâx~gb´˜˜´ÃLJÌVAsÚ­$餘§šñþlµP'2Z­Û[m]9·²¼Ú)•Œb>ŽÅ%0»pìÔøø¤ÝíöË(wÄTt¾“)”£ /éR f6bÈ…‚6ÛŽÞ¿¾uïÎÁË+7¯Í–2Cö¡IÈí§<‰¢¤ˆ´& º€´Š¡ù©t£¤Åƒh#¯®ÍdïßØÇgÏ®üîïÞüúŸ¾ùö½Å…b$ʱ +‰Áæ¹x„áB‰x¤R6ÚÍt³ž ©B‚Qã¸S ”SaØÇÒ,8âÑx6[Œ„ôTDJéøú|ig£ß®¥Ë)uu:{ëÊúÃ[;Ï_|twÿÊ…­ééZ4fø½0äFí.%Ã@“ JÂQ\`ñ\\hƒµB°×ŒîÎ'onWžÞXzïÉþ÷ÿø«/~óóï=¹»·¹>[­%Qöú È äŽÆ¸(ÅEü 418 ”’ÁLX蔹nfª‘]žk_>¿y¸7ýÒÚã{Gî\;Ø9¨–«FÌÆ톆Çv73fAOŽ@ãüÄ)ó‰S¦Ñq‡Ó CHdÈDX­UrAU”92ŸMær#iÂ;m ksb~·KÀÝ*i-Fàí^äÎ~óÊNëæ¹ÞÛO.|ôæõ7m¾}·ÿ‡Þû—ß¼÷7?xõÑùµ¥éx<Ž’œÙê°[& 5.Â$ÞIyæJøá‚q~³¼¿^ÇÅÝöÍÃΣksoξ}é?yõ?üöƒÏÞ:m+Ÿ áV»mÂêÅéAK°æ¨F·ZâA_½¿_|ç…Õ×nÍ>½1õ΃Å_~q÷_~ûÁ/?»ñ›Ï®þËׯüïÿñÛ}rÿñÍ•ÃÍf2÷#¼ÇÇÁJJ)-Í5Â[3±©1Sbjif¶Ÿï¦§’ç7o<ÜýýoÿæW¿þá;o?¸wcóìú èI«Åa.T:5îxîÄËb +ò¾¨'$ÈÙ‚¼³‘Szõ(HÍ™è“{Û/?8wÿæÎû¯]ùÉ—ï¾øðF¾ži/îMoÞç#eÓ¦°HDD+r{*|÷ us§º3¥>8ßüÅ—¿þòé³;+ïÞ›ÿ‡Ÿ<ý÷þüÏ¿ÿè§^üó¯žþô“«» )•õºÙâA0Ð!(;Âœ«—c®ŸÍ~ÿ­s_ÿàѧo]zývïã—–þîËüéÓŸ}|õ§ïþù7¯üé'·ÞºÝ¸¹[^Ÿ-&¢z2™ èI%”eY."ùk1l¡$^^ɼ|uî…ƒÚýÃÊï\úíWÏþü§ýþ—üæËûÿóïüÿÏŸþø«î5?}eç—_¾øÚýíXX5[ýgÆ=V7M²Šrú!æ±H„3€ONçØsKÙ£•âfÏ8XHÜ;?ýù/¼ýhÿí'W¿p#jÄ\çG%Ž 2ÛIQk5þÚRøíëSßýü篟ûùÇwÿãÏžý¿þùïñ.è?þíƒ?ÿîÃ^¿²Ö/ÄÂ2ËIz8ÁPd€†Kar6‡ïw…Ë áwsŸ=9ûóOï~ùÎÅO^Ùþꃋÿé«'ÿó?ü·ß½ó»Þý‡/oüæ‹›/^YÌÅÏbqx½ÀO(z6¨öÓÜà 㵠ù7¯T?yùï¿wùüþÍ?}ýÊß?ÿ»/®þëÏýé'÷úîÞÏÞÛùÕ'ç~øæîÅb1w€<òR¨ŸïV–ÚÉ…š~q)öþýÙ¿{ïðÓ—W>~yý?}õ×_>øþ³Ã¿}cçÿþ§ÿÏÿú_óæ¾¼õß~ÿæÞ¹°Ò¡°mhlÜêòûý”ÊqÝ\ öVuç¹iáöFòæFâáùê;—¿|kk?ÿôæý§ÏÿáÇOþøÕ+ÿþ‡O~ú½{[Kµno&^ìº 5fÄsQ¥‘`×*4øø;7º¿üèÒßqóoì|öÚöO?¾ño¿{÷WŸßþòÙÎ×_þ/¿~ãwŸ^ýô^ók¥í®’‰,Ã;ŽóX­aÖÕIÀ{mùÒRüþQíÙíÅŸïÞ¿þúÙüÙËÿô“—þé«Ç_tùë÷~ôêÒ³+•£ÙP=AG8ŽøY1ˆ³¡QlÔ[ó\'Ã÷ÒÄÑ´ðæÍ©/^ßûÉ~üþ᯾¸þO?{ùŸþʯ?¿ùëöþù‡×ÿË/^üýo|úRÿµó™{{ÕB*y`‰ 1&ÊZ))FézœÜí…®¯EîoÅŸ^(útý÷?ºÿëϯýò“+ÿë?}ñ¿þó>{eë³7.Þ½¶IŒfC€½D*&øjQb®ÀLëöjŸ<ÞøòÙ¹¯Þ½øï¿y÷ÿü·_þé«—ÿÎ#K¤%=£3š×.@fòý~þÚFùÞNnþÃ^øãOß}ïÅ÷îöðÊúÏ>¸øñÓí›û­ùJ¹X„³œšˆXlPQŒt.KÈd3NoMÇwg¯_iýøí£_}~ÿË·.|öÊÎ;÷ÖŸ›z´W¿º–_l‡J`=‡—fä‚nÔYZLE´„ƤU²_TnnU?y´ò“·÷~ûÅ¿ÿáƒ?þøåýÕ;¿øäú/?<úó×ÿý—¯üö»{Ÿ?¨ÞßÍMd†¬.D —é®$*K”BTEGf¢Þ˳ʣ£ò“‹®þ×|÷Ï¿{û?yø/¿zíßÿðÑÏ¿wû£'›Ïî-ÏvÓŠ"øB‡r…–,‘½ÔL´D/ o5ø›kÙǦﴯŸ-½u½÷ëOoþüãÛŸ>Þ|ÿÎü“ Ó‡ýôLA ò(C‹(©'3<æÆQš¡ åªÔr3¾Ù/žífÏ/•ïîÕ^¾Ø{zuñå+‹;Èn;´Ý‰Î”A®GÇÓ4¼Ó{´™¸³¹¶°P ¨D œ@¬ 3Q³÷ÁáƒtÊSЉÅr`§-Ü^¿¸WxïNÿ?yô¿ÿüýýõk¿ûòÅï>::˜¯´ +I`zí¨êA9¯B½Â ](|+X¨FöæóGK…[Ûõ^8ûÑ“ƒ§7®lÔ:@žÄê°MX »_ˆ¤§rõõpª+A‘¢J‰ðöÒ̹ÅfŠÞï…_8è¼÷òµ+;3ç—Ëà³ëS‰z6hè‚®j8qb!a BšVs”’¦ºn°(¢0T:¬¦ÃrÞçëñ½¹òùÕÖárm£—ïS\ª‘ŠºHR¨ «—1¹œOÁ„âpx ‡ózU†mæ²ËÝÒþlêÚjê;s_>ÛÿîÃÕ¿{åÇo_þÁÓíÏ­}÷îÒ³KíssFPðÁ^ˆæU'¢ l ¦#ˆýt6–¬¦âQÎ]7Vðp&zy1ñùË›ÿö÷þçß½÷õ§·¿xãâ‹—çÏŸ­ö[ -¨ +zF·Q>†òaÊóB°’¯Æ>­3…¨˜VÙ|Hê䢭Tp:%ô3W7§wûÅZ„ËF 2<Š²ˆµÒ§Æa`¦…„×áH4¨gñ|"QŒ •‹JLRò†¬Ó~÷G$¹XìB÷a¢xLŠå» g`Z„ÇK +\ “,ÎôÖgZýRD™+†..——KÃÙüÖTf©¤/—ƒý|h&oäuÑ1>j1»!D%å’…¾s|btÜîsy†‰+bɤd¤“æ‹s³ñ'—û¯^[¼³ÓÝì– –¡QZ åÕüb¶wçÏáÁIž49 ñ¼h¥b!› §¢”!VQIÕ0N†hyÌ… [`¦2Z£cå€eFŒcyùd¦ŒxÝa‘-Kgt5Â1"Ë.7âòP.c÷2ãbÔFx˜D¨°.¯ârZæ.ˆâ|@Mø¤‰$……Ng¹Vë‡CQ€^ ÕPÁá%Žš'Ü4Ä$1µÎkR¨‚3ÁáQËè¸ #ÀÛ¢š–еp",e"3ÍÔÞbyµÛ+ì/¶çëÙV&ÔHGsш ò6™Ýƒ¹íglÈs#Ža;…ˆ ‹³••T¶Ã1Œ&â¹0Ÿ +àI Ykeï_Ú}áú¹ýµùÙZÅPeš"ü(K17e@TdÒ'òñ©L÷|¦u”ÈÌr|¨×[Þ?wAy³ÉCRJ0˜N&ËÙ\=Íñ¬ +9I¯?06 ŸñŒZ)'µÂA‹OõH;ì~Ÿ†<ø˜™°£À™;ý¢‰%¸(Œ‹+äñä^žã~*„qI.Ò ¤zà[§¬!œ +‘ +l tA9iåBÕk‡Bj<¯¨Ó@IuÒŠžvyÈƧ'ìÔ_~{ô[ÏLúýX@U MO À¸ÀˆAYe`ŸŒû `Mi”Å‚ŽÙp!Ú‰·”TONô +½½@ª‘"Ìb´«ž —–!â#%Q‰U: £.øÄ„{ â‰PSLω‰>©•)Ã(¹ÊÌ.mÔÇüªGLÊðñ)¢ûˆFéfrbÔ2fâ´VÁ¤\õd¡ƒ°º‹LO&¤œ›ŠATÔŠ*ÇÆË_»É ,GËgqµjE#68à"t5ÝÓ3ýQ+iõˆcô"mº¨˜Öœx˜Ñk gLqÔÎ"b!^Û’=6Òòp©—hur¼ç•@~\-ò‹zu-ÙÞäæ!.iöËk:;¤”?Hö‚ù£¼¡ÖF|Ú13qÜ„ÚÁ¤¡¿:6qÆ„0‘¶^ÛŽÔ6¥dßêSO »^Éä¿}ÊuÊ„zù4›ÑóR¼oEÓ>õ´…œtRõ`êÉa8¦3£n³“öQa)XâääØ$d¶ù`Löƒñ,‹Ožp‹&¿â ã6ÂøÖó&9¿äWr„’š]½\˜ÙuKIT/ú¤ .qF-èû‡f"`EeLÉŠé®GÊ€ÏBlŠŽNe;‡ëWß4ë'”²ÉE¥´CÇú°\pàºi0-=B}3:ÑQ—aŒž˜š§#-.º´`A4;vQ4PSZ~–5^>¡¤¦âµuZ/òᦩX7ÜÜ×j[JqYLNQz¡4w… +5¸Ê„ÛiñÉi=á‘œdÔÍf}b bÓ¨2˜esÊ‚Ÿ1c‚1²œôrV$k%ŸVñ+e>Þ¥Ã5Ÿp3QÚc~ÍŒ‡!1/Ï2Éy¹°&gWOšÈQ‡Š”^²á!7ràI;âìãR,4l£üRVNÎȉaŒ8h“_Gõ™’"u"Ÿ™„¿}Æ9jÁÚ}’›Ž[aÝI„íˆâ£# È\)9-¤f™dŸNôýrñä8†…Ûx°Êõ×¥:g­tÈ#¥ p}³}ƒåÜcíÁÃ6…Y5×3Ê°O0aaLk¤¦.¦f¯íÿ`ÙX ÕùÔ<Ÿ^ô«5HÈþA–„TÏŠ‡,¨ÎF§Ôò®”['Ã%È &N€Š´]LĆªd¨*¥{‘ÚF¼±­mè…Åp~avûiÔ‘PÕèUÖ^¨­ßvö3½ |¸a¤§»›7­TÈ-$Ahñé¥Pm7Õ»”œ¾ªm¸ùÁœSÆhzÝ(€ø$ã]*ÒhÌžïï>¢Âµ6ÜBFÔÂj¬}!T;ê›±ƒ¬á’¨œµ£²“‡=â„OubA/‡¥¼“NÈéY5¿à k@BwAgF€ÚdçlÂŽh•Þ¾”è‚öŽ@œGÊÂjÑZT¬ë bNX’ŒšëZʼnè±Ñi­°*¯‡J«“n™ ·Ùè ê€ß²Q± 4|Ú!Øɸš[F¤ü±1,X¥ãÓTl Öͨ6Ø59ãr@A½rAÌ,âá6—šÃ"]+g¢ÓzaeÔ ÂR…èHðýbrÆêw‘Q!ѳúƒcn°r/ªÙÔKk˜Þœp1czÒytï¹SæoŸ¶=o"lTŠ5¦Ää,¡×&œÙ#ÒH“’ƒŠÚ阋‰ƒtc#]p¡Ýx}‚WJâÁ²”íCbœ –¨h›Ah4ƒ…ùÌÔŽhM¢ +mzA-àÒ°R°°§\‡Äª)ÙY ÝËħ¥ìbzæJgûÿgï½~$I³{±wAó PWäÎÎLW—ˬô>#2¼÷6M¤÷>³¼7]Õ¾§}ÏìÎìÌîÎp ¹$—$( +ºy!Hô¦½èÿ‘NôêÞAîƒ.êÛœÞîª4q¾ïœŸ‰ŒøÎúçßeäèZë`ïòCB¨FAN°uLKõÓÒâeiç•P]j•¡;<¥JÁ[BŠr¥¹PÝqF×Vï·»z{ïôív¿üÁß6júXjŸôÔ{oùhïñ÷D⊓ÚòËúÞWí“ŸW÷^ó£œÖ·º§‹›_VçOâ¸ëô.výrrù³Áé×Ï'·OµM|ìZKZ0%À´ùկǗ?6Ž¾³§Ï r‘ª€Û¬7_ÍHQÔ õ‘R;Wš—Öàaiú0'8Beji½k¡yÖ:újxù¿«Oó¤½÷V¬BbT¸âˆ¯î©­#½{ÁÕNb´³›Yß[\|Eh­8nA^©õýòü¥P?aêǘ5‹Åúä±Õ< ¢zNꤘªôÔε9zÆ×NS\³Ô=¾zùc’.­&ùœÀÅ„NŸí|ùGÖ?yÿûü×ÿµ6¹ZË(²Äy{õÓŸý¦}ö­Ò>‹‘nyüÐ)”×WcL^i¥Å&ä7iv¯£y3…Û@1)ÒØLQ€]«%ˆÙtyWéÞT¶ß~º‰¬G±,㦘rŠò²l5'4ãd%Å7ÕÖ¹;~-è!÷­"5Pµk£rSZ„9`Ê»bý¸¹û~|ó+ÊéÛ}`ºÆöHªâèB®/EoÆ{³ÖÁ+³wÊ•gTqft¯j;ïj»ïôÎ9n”•Sêjë7»¤=æ¼}®vŠÙ ˜ðþñ‡îÁ Àm£}8&Ööhw!6ŽÕÞ™äÍv.ß¿øQiî‹ÁkÕú^ûðíðájÿÊjínŸ¾øË¿ÿŸÚO¢G·½åëîñ7Ãó_Ô–Ù €¡3º˜ÞÍH•‚ÝKˆ5ÖÛ·ýøw½×œ·Ã—æƒwtiŽh]¡q\ßgô¯ùê=x`tOVxcíÞ®;8P[ûi©ƒS©ê_oôäWÇÏ~½e‹j¬³è|3ø;mø‚p—ŒÙÿðã¿Ö§7IªÄº³Åõwïû/ïÿï¶_üVjdŦêmÿø·ÿ£ÞØßȪY±#7/‹ó7½³ï÷Ÿþuc÷-it&Ç/«ãóͼà*diך¼\<üýðôëñéWBy‰i}oö8IW’tYðvÅæ S9ô«.^Nξÿ¢TvJÓçBí¯îÞ6® 9{tôäϾÿ'€GDjYÝ+«w¡´ŽHw£¼Ôfyûà-ü|5)Ç0WmZÃËòìq}çË0(ÞŒ\鞌Ͼ +å•Õ8KªurÕØÍšôÉzZ)5÷^~ý7zyúo> oÄ)gF÷¦{ú‹Ó·ÿ\+˜Ï~öfë0N» ¦L”ö³B=+4€›ÌþÃöá·goþëýç¿W+³O¾éí?æŠCµ¶°º'­§ÝƒWÝã·îì†/Ž³ð9° Ly¥ $Ä +e¢uΪÛ/HkPÐ;@v•ùëæÑw‹ï¼ƒ·Lyê6vžýè O²²§µOʳ§Û×ßß¼ûÃõÏþitþµÝ9ï?¾Pj;åÉM÷øCyö¬±ýlû滳·] vŸ,.¾UZÇ·˜5@Ô&|å,õî[œËÞt|úš«ÌÓBÕ÷ »vØ=ýùüño 'µö©=y´xôý΋ßWöÞ¥WM²¥¾]Ûy¡$±6%ÜSÙ‡s'„úhÎõ‡©mÉ[ò•1SÛaçrç9¸vY¾”ãœúôJ®Î»cõŽZ§ïK³›òäÚ›?ñÆ79Á<4û—¦¿ÊÆà™<•:× ªËZû(Ší§½“rëDkAâæÈíŸ<û5m½³ýàgÏõß¿û›ñõ÷ýwzó õÕ³ïßüú_ât)/5ìÎIïø«îÉדw­ãY±õâë¿Ù>{‹òRoµŽÞM¯_~ß>úyÿ⺸Èê´9H³%£{¦÷/•îUö'ÅZBʲ5¨,Òš®Æ‰õ½šàrB›q—byGóvc¨öùf4!ÔaÍPm@ã~Ê;~ö»bï4A99©™áª ºQµC;S£{áÍž@-sfwÿôEetŒˆ%¥2¯ÏŸí=úáàÙo:‡¯õΡP€1ìŸ~ˆbZNö¤úŽ7{Ñ=ú¶ºóÚ[¾pÇ×Ry^>€ ÑÚÇÅÅ gñT€òq&µÉ“ùõîq^©µ÷_yÓ‡““×Þüfùð{³{,—&;'o&§¯Ygè.®ßýÍÅÛ¿Û}ú›Åãû¯”J÷Éë__¿úmV¨¤8r§bmr 8{®t.I{ê NŸÿvï„biüPí^éÃGÃgþqùð§svþâ7“ßP•‰9¹:WdåPî\»“—Íýle‡wzÏ¿ùãôìkÿnG©‰ÙSÜÓåa êÓ'íçÇ?LÏßc΂ñÛRï%¡$å íäê[6•Zitæ-Ÿ¸“Ý/íá5(1?•déuš÷˜Ò¤8{ìí¼ñvßwNYÝy«4Ž€€ž|ý‡‹× ýf ®ü»ã›ÆΫâèÚh탂¥ŒÎòê뫯Ý ýžTß³:ÇõéååËß>üE’ro{zþÕâñ¯õÑÃ{mŸ/oß¼þMmp”! ÿŽ×ÝgÕù#«}¤5OèÊA„òyC,Nr\ypúáèÝߟþÖÝ~Y^>/ÍŸ1•£`Î’ŠóéÑÖe( åK)ªL¨#lŒ3ÏÐÞý™¦\Þ]dùÚZRŒeÎÝeÝ]­} æHŒvAmçÅfŠ.RFÏß@×Ó‚‡=ÌÚàBoÐÅq–ux« RÊꔧ×BeŽÙ?~(4ïGqáZíý7­ýbã `tàÉõî`ï¥Tž0F»wüfzý=ÌÿÎãßl?ù}çôglu‘“¼ÁÉ[\Iãɵ9SÌÙ®Œ.¼é¶4ÆdÏn,Üö¬>=q‡gPÝtq*»ÞðDoìrñFgà"~ó·ÿntð:#÷¬ñ£Ò¶¿­zu÷¥3¸BÅÆâàù?ÿÏÿûË_ÿ7€Ø Ò櫇ÖàquûuiòPoîk•ùÙ£ï._ÿ°†Y¹ž7†”»TêÇly3&ˆÔv[{“ÓW¸ÙË)ƒ¬Ôþi+ÆÔæ×ó«¯óB…1{Rqf¶ Š¯f'ï­F@Ì Jq¶<ûšT{÷#L ­€Zëî(Ÿ€õËK=»¾÷ë?ü÷^üò^]‰Òq®éÓbïºÜ»Þd´¦Ò<—³šcKJmIšCº²›7Æ”;/ŽžÚý‡öà /øqXwÞ[0Ū÷­Gº3ÀíñÕ/¤æþz–' áŒÜñnôòb‘ë”٩̱î`Jo;“‡µÝ·ý‹ï^þuëðM^®3NßìFP=ËT ´ÓQû¸9¢‹“¬X²3¾4Ž’N^nGÉRŒ*‚¦g ò·†ðæG¿ÊPż·#6!Ãi£S_/ÿ*ÍWqµIK¨zwpjtÒ~뽊XžfùrUg`5¶{Ëk­¶XO‰k))ŒÙˆÒÑ~…!ºJeR™\LÏßÎ?¸ã Ÿ¡ÌTÝ‹b&@ª4­ÞùèôgËG¿ã ºȨNkßê'ØâFV¤…Qµ±ÒV›ûÁ¼˜alT¨Å0‡ÔGbõ(¯ ¨Nƒú|ò=8å•$rEövFÇß Ž¿êg+ 1„š¤5ÙÊÉqÌŒ „÷@]¨å…Ó»$í^©wH¨Õ魯¢t…ʶÕ9Q‡æiÚ® O«³G°šœ·K–¶·ðbžk€.+;9¶ÂÝÎþ ®ðBÐB¨Òñ÷û*è{g¯GË+Fõx§ëv÷kóýÓ7µÅã0j[6¤•«õV¶¨`Z!­‘=¸jí¾Tk»iÒ ¦Ä¼PwúJmÁS¸  ¥qì-Þ¨ý§jLA'ÃÚ´Ñ Œ6ª4|²'9µŸZ~Œ=NâšÛ?sGLíÁ ]œ¥¥FŒ©„“¼ ç»Õw_·Þ©ÍXh½uH»#Æî6ç7í½/³b%-”3Røýðùo.ßÿ±ñ­Ø>MR6˜Pgò(+5ŠSl°Ëh?ùÕàð¹ÑÞú¬']H$Ð!”3ƒ÷¯.¾lŸþD £Õ›Ós£±Äµ"·!©({"{‡,×z‚!ôãÎxXšÚnR¨…p=ÇWê“kµ²X eïm¥·rðŒÐÊiý´Ô„eÅù²[ß…ÈIlyGl++ux9¢GÁöÌ.ÝÞ^’ÔÒ¬¥Š¨Ò7;”Æ),ëf^Z3€´ÎZ’bvNîÓÅÝòèIsñ²2¾¹c +|µµx¹ÊËÑ‚#l`IÞ;À¬IÁèGsÂh÷Ymt‘¢Ý$YäE V0+³ÖÀjîoeØõþ§ýŸ ~ÈÍ$ë1¿ÑÓ9v;'IÒ鿵éN©ÍcÜß™Ó$œsÍêr>‰›¸êïücuÎ}L`œxA¦µÚüäE¥»CJEÖí'¹iÏÌÎ9kÏ0©½%·R|¹wÒ\>ÜH ᜺™î‡iH]£uª73T1š%ÿn¯.ÂT™s‡>§Xý4ë!R° J è Pbm_¨ŸÞ1æ,Q½Ÿ¤€ ¼PML\jQÖDjžçbý0Θ[—]±f‡Ò{Bi +Üèž·ö^µß“îüÖ^àö¨±ûtpñ•Ú;[ω@ôNï†/Z8à*s D³±Û?|“fÊ° +jó°8U]~ r‹tfy¹“á*„Ò€OÌÒ•„Ò¶Õ»ÕÞy%U¶Sli3#Æ€‡5ä®´Îú²|=‚i¦¹à ½uÚØ[ßÕ<ü`Ožg”>X£±ÝÝ¥µŽÈáÊéÌÅê¼Üï.Ç—*Ýë׿Á´nÑÁ†ã渠õÌî•5x<³™¡)WöhkF[S¹v >}=-ÜÛ Éß60»i¦”¡Jy®ÊÙã‚ÚÈ«)˜¿(öb¤)yÛåé³{.yðã@Vù"ˆ®Å(Îì­†1L®9ý³öÁ»ßþËôæWjã`-‚e RgùPoìBfTF : ˆ4” _ÝLqþ†t¶|¾»§ÊÝB­‚ÒãF¬ a²WŸ=Þ}öWÍÃoèÒ"èßœ¨`¼…+a4Eo¡vNÀIÑ¥ÐÀ¸\,ð:Âh¸RåJ¾<i†Qh3C0RruYÊÚö9½²sõÍâúWrç:A–q±Féí4ãd¹ŠÙ>“«‡ aÌþC®¼Ÿ Ê T•Ý!¥V7“x’4{’"+˜ÒQÛÇP˨ܤõfmz©Í$åRÖ˜+Bʽ,.^‚°„uìŽ7¹â˳(aòÕýÖÑûÙͯüo$w_Óöÿ²}øuyù + 脼PÄõzF(ò•µsC{gRóamöº½|¥Öö#˜‚%Ö"r5/T@KdyO,Ïç—ßï=û[à”@†£2m4I)g"´Nòú 8y^ß}‡›“,SlO/¬æNœ.FI7Š³<8èamö¢2]P‡0çIôÛ-íG ãV’*¥¹ªÝ»œ}“;ÀVs€ª]X¯$SZÀµãßPYdcJó¤µý|~óKTŸ¬&…@Võ¢W— Œ¶ÅFïñüú7“Ë_âÖ(˜e@IVÇÕñyŽqLXzÿÚ•Çúà ªöAÕÜ~ÿ”ât3Á®Åh¨‘œæðľÀµ~†v6“tSY£+ˆiJϲ€?mŒisJhÝ,Sa¬QŠ.Á¯¤W=TðUÙÚa¬Îýƨ»uR\kíÓ ã¥q¡LÞl +f=K©´Ú¨tOÝî±;¸¢ÝEVîÞ‹0[Iõ/©*m&hÀ4TlŠ¥¥äíåØêzœûéFžR›µÑYA,¯F©Ï‚ÄVÁÅ‹{ÚäMeç•\žÅ²œ[6À-þùjn-Îo"VVÒÞ©Ö¸ˆæ”/6s‚Ù›¼¹(üùý<U‡Jë$M¹PÅ9¾TëBi,—G‚ÛeÌZžsþVZE£¶çôÏ…ò´ ×à z l+Å”ÕÁuâ`¾ŠD©³îT®±•}(ÒRIø§+\y©¶ÎÁPCö‚adœ1eô­ÎUœmÌͬŒŠu½±Ç»c»}ê-_áά`ŽRbc 3â0z/§´9…±ç\ù7&ˆØmÁì4è“Rž¯€ºØJ³ P½(•÷Ygɺs¨Ü0jbr+ÍVˆhÌš Þ!8¸S "òjƒ"Õ/"Õ0{N8 8~½{@”/Âx(¯Àrg¸j^jÐî3'@èµé#»wÉ•¦[;†iðe!ÌYOK1ÌK `çßX’þQå¤áæT¬žòF³ ¾ÂhÍ­åïVMÃÌWi{†Š"˜Q2\ G36Ó"w…Ê~iú%h†n®„ñHZ˜í=/·÷2„Í+qÜÌÑnžvà±+`¤4œÿðWÿ–Óë[Yy+§B1RÎn–óI0šeÃZñæ´Ñ çäfoedI ¢p¹‰²¥pŠŽåÄ‚Ü#ìi/²b#ÎâT8Í°j 4ù§›h -+uÿŠ,£{É–Ö3úOÖPÐl”Ò•‹‹PVÒ«;¢w€kã8îÛ.¨&\ò³g4ö¹æo~ÂXqÔ÷)y¶(8c¥ºk¶ÅòŒµû¬ÓN1–ß>/-àr$ ã 1µV=Êì0Έ«ìä¸"4gª4z'_.¾W;qÒ‚'À"¦é¢oÊ/É·šü½Ú‡BiªÕvA-€‘Ï©Ý”ØÂÀRµðõà¬,Ö'?^ÔZr&QRTU(ïbj{%˜ ÆÚh‡ÐFY¾Ì[!ÔÎ MÚ™'Ù(1º¸ƒYsÒY†H÷Ó(ùÓ(Ì‹¤ÚÎB[re"G¨‘Åí”Ô 2(®ïŽe˜²\=°úÍöùøäçfûR‚SriH3)6ÉT1m¨7O{ûïÙÒ~œ(m$¸xA êF|…´¦H÷ê!(üXA_á¼ 0ž«à“l%'w@Ãó¥=­qŠ(Ý{ x[R”0_Z(õÃ@^iÄH­Rsç! ˜HF=Ί›q:àyI5[^k·9< &©8íŸÐj\q;„˜kqî'«‰$¦¾FÝY “¡–çëP¤© Ùu/€|¶š,°EÊï¦çnå´(bÆ=š•þ2”ÇíÕ(K*úô&˗Èÿ¼ƒê¨aJþ\ñN}ûÍ/ÿÙ¬î|¶YX‰ˆy©Ê$ŠˆIÂy ´‹pÈj80È«‚Pñ÷éÕ›‚;dÌ>HÅ›éÍc¾¼ãŸrÁuÀY¶˜Àµ¦G bQ‚tP´ÜÂ\–'×ÎàŒqGþ&lr-‚ªÛ1TÇä6ˆXð&\i鈩ÅÌ,_-;Ôz•Î#±áwg@¤:¨hT®‰ç™²Ñº’묻Ãwsìo*¤ :oõ¢¨.wäê)nÌàE¥Ú‰ŸWJ]-/@´¤¹FÁœ'¸¢ •Ú‘Z=Œ#Æ8“ƒ'„ÞÉ„Y Äô;×ËÕØÌi®'iÖêÏIB²åÄFÈ÷Adâ圭—çAô©p'K«I9˜Õ±£xŒ=^i”×@3§ØJ^=PÛÊKë) QzÎàÆhŸ ¼wöâÇùÅÏuá[ˆÖÒåœÁL(ÏPÀP¼=Üð7±Ì`P-PØJˆ¢Ô^šp …rt“ºjyG--q¹©¾#?¶¶—î‡rëa|#)f„6!˜ ±`ßÛÂ[ÓkàÓ•³ã€Â¢9d›â0©õkÙŸn )ÂcæfB¸¢àÏæ2úHp&YÊ] c›Iv3%€*Õv†®D+‰™ncٙ߀€ßHª„¹ "!N•¢„“`¼0é~‘`WL8¯Pú@o°Î(Ç£ +Îr(ïy9ÃGZûª öqµ‹JÍ`^ýl«p?FÇ0#'Öc¸ê Ækƒé}Tiz!Ü\M°Á¬’'•çsÀQ‡6† #¹â  +¦vÔæ±XÝ-¨Ý ï B•P€3ZO²1D¦´c !Ù2”SàýÝ8­æ¨h(Š`J€1©D‰€«RÛ¤ÚN„ßâJ \̪=|"5Î!-•ê>˜ÿt·RÿB”4WÑš§RýU;àÄ#¸u/AFq•»`´#yê(R(FpÐíµ@V[2HFc•@ [Yqw¤ú¹àÉ5àÊázŠt¦ ¡ÚjšÃõ mÏåÖquéŸ å™$L­8xòþ“£i¶œ‘:¨9FôAFð¯½ÝxÁ*e½¹¿™ý–è7+÷yïTi^4¢Åú`þ$[P°[æˆÂKz÷Agÿ-çŒî2ådIg++ß„| endstream endobj 63 0 obj <>stream +[Oð÷"l’nˆµ@þµ“&€£ë0«(_ +#¸`¹vèn‚)1’Óà‘¥<ÞYB2' 6È­@VbìaŠ´ÿb-HIiºÌ˜#Þ“j'Ú÷CìFBÁÕA‚.áæ Q‡ðÈ©ý-܆TñOG«]Úõ“ÚH¬æÄv’ts\ S[¼;/ ©Í³4ÛŒ“Õ^IÑõ͌ŭ4[B¤&ø_|À"xP× » ë´öPCŠ®$áSÒÌVÆoš&Œë’VW¨n—ÇÌÞh›,[¤)WK݃úì˜=#µ18{ˆ ^šÔA”2jÄL,fh;Ã#!UÒ„L²„TSËsVï¢B•Á&¶Àè%hÐÕ Ð&ím(ƬPeK³ªÿ›{q@µºOh=à©$2œ~}ñ¢±|™æ¼8aš­#(Š$i‚*Ø̈[9r¦µûbxù ®²Ì‹Òè1¨Äå†1#„šn«qëbi¼õÑWúÍ¡”>¸à…õ´¸£Ò„òµµ0êìaFì ÚX¬ rÔÚj”€â‚’'ÀÃçò;ý²N”›ãvÀe§jãÛ¸á7<ûjúà{ÝϺþ`ïkûö-oÃBç…6Ê7 b#‚ÈP,”Þaí1¨¸@Š—„Dâ›9¡Ãퟮ¥î‡ñ•(µ–7³JD;nÅ`†¥®Þ:ͳ^(«Å]D—ZŽŒ­´ðùFò‹Í ¤Mžm@ŠBÅ™ƒëÑõµý÷¥éS¡rÌŸ®#)ÜZ_ + Ô~‘¼Ýòð¡à.© ,7¨°èPDà²óB¤r2ñ•meôdrý»Êþ‡,ß‚7‰v–-£’‡mû×ú^‘Åy, ¨_DC èc«9¾–ãÚ´½Ç–O¥Úi†¯o¤AB PànïÚh]Rö"F‚hoú ©¶î'0þnÿÂH5XÊÍŒ쳞â¶þ„Wïõ¤™Š5Ájµ ”Qßmµ/9JµÇA6ÅÚNNïçÜ:k|¯v®Ã ?âôFœLTVÊ•CTé‘ÎbÅe­1È6ÐùÁ ̪G¥6w +G’Z®ÉÚÓ‚ÜŒ’Å8S%ŒIiúÂ?¥œe†o€;FE±ºjuæŽ.¨âÌÞÔv^ÖvßâÖ *Åh9ƒ‡9u”d[˜Ú+€3â¡LÌ@šJ’°ú³ÒôùðòÆ;ðÙŠ©jWÕ“Kº´‡Èã,ïŸ!Iîz‚…X¢yኌ3ÁÆ·Aäê€{Á´¼!Â9%EC9à> +(ƒ²Æ¸2¥ýyýÉý)ù'yâ¸#\Òžš³Òð†2úÁ4CEµ2Õ¼ÝO×sþ`oƒ r¸NŽk€\ì]S¨ÔüçK»~Ûe}'¼­¬Éš pÙ[9õóE HÓú !özœ¿,d0“ˆà60¸éWq!ÔŽcÅ °)ʤÍç´H½b¯`Œë;_I 8Ú,[ çDÒfÅj„°b”#ÔvÇïÛ{¯•ÊŒ+yÚy_œ<Ê)=(¨V›CZ³òä±RßÙʊጜÄKœP­ym‹P,k «Ri#-L#ꄲ·›ÛïLòB#Ëx›I¡2T[óo£*¨ÝÈm¦8Lé€z_O+i¡e(ƒÇÖà È’Ï·ðσИ/DnE©rô°6äKûö¨Ðð¼Æh€UÁúE ÇŠ{ x|°ÂìdÁ +çüMRT)/´6“ÒzŒ#!\nû÷nð•hÁø"„ÌÞÒÆ° ·Òt1’×ÌÚ~Š«®æ¥-Òø·uW²ò•G²wbk8d‚Ü +æÌûq)”„ñ/¼‘‡i±ÊÕº˜>*ý­È¥ÎE”¬ldÕ(áÄü Ü çÁöU<&¬¼O’´P°´!WºG?׺¸=‰bÖzZ€…H²`Éå`^¹¢¶®ÝÑ Î[Æý­!xÐ'ÐWJ+BÚk):ŒYLÉ¿©*'T~@î… ¨Á4€Qò¿j¤K!²˜“Ûœ·—S»÷b\Šª„òF(gfX/ÃUàåy¹«4/Òbg%É¡¨ËÛbíü _œ0à‰Áê-µ²Àôn7óÆJœ—P²<„ÊY^ò­Ý'ãóJó`3'‚Šˆ :gŽÌæ¡ÿ=&]Ë»jýØ¿F½¸\Mо e¥‚Òª‰P%ÂÝW:©ß •šTÿ|«AHuaD»¡¾ØÂÁqƒa¡Ì¡¹ f[­ã—40Yhâ‹„K)Äœ=†.¨éD¨ÚZÞY˘9qhéý8fÄ ºTÛí¼]\ÿh´Ïa™V¢Ì + Ybì€ÆzZÆ´ÁÞ‹ôö¾Fôhõµ(‹+}»} 5û_üÅÖOVsi¦Þ8û¡}ü5çÎà8q` +J3˜U‚9¤BN({vøòŸ¤Ún·@WS¥0QŒÑ^Jèæ¤!ÔN’òÄÒ Sk¡€Ï²Î˜Koú³Gˆ,W惣I®š•ši©¾E”A¶‘ö ‘›~WhÒÉ+}wôª<{/ÕOSt$Pš°½+—çŒ; +œû3A78÷Èh?aܽÕÌ©þM‘½‡ró2)66u##¢\5Œj÷"øJOŽT=Ñ:׸9 äå/"d ³àq/B… vÁkÝÝã_z»ï©ò^°È"|c3%¯„™ ÿbu7A{|åpþð÷åÙ«á"ZÛhŸ©­KÚY$èâZVZËʸÒ‹‹8îƒg’öòbKkœ¨Íó8ÓôÏÃPq@ª§Ørœ.ƨ2X†,_Gøšo…RBœpiã óøçfJƒX2zi®Œ¨¶¼oô*ÍÓ0jÝ0k1&”UI6’cyi5„Ý“)²ÈÙ3Æ™ƒû[‹“Jyb~5BãÚ¤qø5_;"9*õü9±ú¥öv¼ üd#‘ú¸³/·–§oI{Q@”Òlrö•kë)áÏ7ѵ´L9Û –¿¯·ÿ°Xš§ƒ6óEŒM°-T–ÇÏ;‡?OåOïgƒ) Ò)¯v!í?ßÄ6sz„("*ÌÞ"ÔƒyÝì=˜ßüÞ[¼á«ÇëëÿøOdtÿÿ>€ÿ¯Æ] ·mÜrÛÆ] ·mÜrÛÆ] ·mÜrÛÆ] ·mÜrÛÆ] ·mÜrÛÆ] ·mÜrÛÆ] ·mÜrÛÆ] ·mÜrÛÆ] ·mÜrÛÆ] ·mÜrÛÆ] ·mÜrÛÆ] ·mÜrÛÆ] ·mÜrÛÆ] ·mÜrÛÆ] ·mÜrÛÆ] ·mÜrÛÆ] ·mÜrÛÆ] ·mÜrÛÆ] ·mÜrÛÆ] ·mÜrÛÆ] ·mÜrÛÆ] ·mÜrÛÆ] ·mÜrÛÆ] ·mÜrÛÆ] ·mÜrÛÆ] ·mtÿ·ÿD†¹òŸÈøÏ76„!ß;ê}R•?Ù`”lþíöFŸ˜ŸltâÌÁ?Ívwzç+øQ"–ʬÄÝQo¹üÓ3WàI+ÖÁl2Û½åhk%OÅà¿O+É•ÄÇÿUÏ?‰æóY$–FR¹’É$ÜJA2±\É£)ͧ“+ÛÿOʧcÙl"M'²‰Tzeé? ŸePAò™\.OÊ||Š&óh"ŸÿyÒÿíã–ÿ1Ç´üdú‰÷ÉÎ'Î'èJpk¥ê}²ÿâJúq}œ:q¶þôWv“ñÍ#¼b—>‰ó£“Ù`äJìJÕü¤±òq"ýwˆ~üšýA ™•ÖJ*‘\I&rþ˧ÿQ/ðùï_ôïÿYIÃ1&ó7³òñ>ÙØøx„þÊVri4•ü¤ªügi¦Fµ(näx//Öòœ—¦+qÒËŠ]DígùzŠ,þ©á7cŽ0µ‹ÈÍé„ +zõ[h¤˜R†)Ç zA¨Z‹sǤ=BµA^íg¤.fÌÂc=Ž¯GÑH–ϲELïPÎ wæ¨1Î(í(ãò +k÷p½•¤Š„>æÜ¥èæän’ªæµaŠ«G1{+§¤ØjNj#r›+/ã\u%ÁúÝšé|0#%p‡Ôû’·GØãœÒÚ*h ÚQfV,m"bF¨ðÞ®Ø8SzˆÒn‚«†k«  å­{)4Žxo_i^àö<%5ƒ='6Ҝŭ¬¸‘`b¨œ$ô<_Êò•õ$Ÿ•¢½ jl¡f·Ó| ,Ãy¨ÔÌpÕ$SÓj§Bù Á6¨±–72R¸ 3ֈк[yi++bR›Ðú9®Êé”+¸Ô4ê‡ð‰ ÜB…æfR¸ÂC9-‚šQÔŒì¼ÔÍɽŒÐNóÍÕ”Ȫт ŠI¢ˆ-x«(j¥éj1ÖSÌXs¨Ft\îj?A£g=£|Â?á«qf#Î$1#K¹9˜ÃœHqqT]`? dWt0'ÃËCˆ™¦k)²Ãœ@Z‚UÈ +õHÁX࡬¼‘V£t¬`g¹fš®çùvAécJ§Ò;“½ÙV–‹EL0Ånô‚Y)˜€°R4LW7ÁÖy#‘ó|s¦Œ1HLÞ™ä¥z7y9N8¨Ò/¨ƒSbæfŠÙH2y¿%ê² Ï2R?+2|ÃoãM·2|’r(g*·N˜ònÁ:¶C$,Syqús§'í8a“Ö\ë<ªçY±!Üß,S #êV^ÞÊûí:’L~%JQ¼¸‘Wã~¾¢ø‰ÄjŒt“l•´¦Jó²8¸éì¿Œ³VSpcÀ׎Œá¡q–Õ†Y¹‹ˆ­ÉÁWµå‹] ädÊ^8£jû&« ³J?Fa&£3Ûðæ›Ä~óKT‡…X‰«QÊïËVa¶“B=J•cT%FVâL-ÉUûe¥îê7ŽÝÊk[y5ñû…óJšrb˜¾•“)!œQ"𫔉”ÄŠVõ@­ìB^ÝßÂ6âìý0ñyÝL QÈ"¶Èƒ$ÓJ5\žêx3-FQ#Ž;¶9'!|‘zŒ½ˆö§[ù¿ØÌ@fÄ­(uÃÌÏ‚èfBçü}Ì?ÝHß¡ITÏqͼÐJǬµ8½‘â`Bˆµš·r:diŒ®ä´Qœt‚)6œ“ïÃ$DÈæ¥}©r$”Q©£–w¯_ýmïàùŸÝ æØï­Ø»È«Ý@VþXDv162‚¿#vV ¬!©÷2”‹PåÁî+Êlæå•¥+JãB¬Ÿe˜RŠ46ìÇ®HÃSQåYÉH=Ú^ȵC\íÅP•Ö»jó*Ž3R5+µ`}QmÈ•–ÍÅ3Ö&H;ÏUÙâ]ÞÏJ½(V¤‚)JvUWbÄz’…pK1µáe¹v’ò²¬¿sz +¤H;'”#„gÊ\y¯<WÝ~ëNo¢Œ•åË”=$Ü1æN2JóŽ‹Ã'zó¤6¾Qû[~C›¶§€]˜9Iò­ŒÎCŠ­ÄèbØï(æ2j·=àl˜¤Kˆß­d§½Õ´°‘ý¶CzŸuÆ~Ï ¥™â«i±™`ªY¾íŒ(•æÎðŠ)N„ò„tGY©ҙŸ`^º¢W÷I{p?I¯§ø^µQš­'i(ÆÒfV"jè/'nd•-ÄAÄ>aÌPX»'‹ÖÂBåFAë"Š¿u>W="KË_å+»|yQл´=Èé-¥uÞÚùàŒž¤8o3¯lfEˆ õó±–â)cª·oäÚ™ßLéøíQ1€ñ2ÀFJ¸Ÿ`7ý×N¤`¥¨2D +¬&K1¦‘d;ˆ<á½KP¿ö.nÍÖÓÒý8·2L5åo ¯E1#A”gâþœÈ+[ØVJ ¦„}?BÆ +VšªÒòý(³ç) `äÓÜZœ ,Dêlæµ´²™VàD˜„vÌï,U‡´ ü6A{9i¨î'"n¦XHì¢C>DòZš(&0ûó`a#Á¯GéÏ6òQÄ…i„‚è å-„­I΄3ûÒ«qn#%¯'ù`VI2õ‚¾Í—÷i{²•÷qo-JÞàŸof¾¢Yªä4Žcgë~\x(Ï>ç…&%nÎáç1ÊKPeBí/œ ÒR\ Q‡´³Í»3Tª'p“ÕÅáãQ !j¤`f…šß ô€Ð„â¢Ì‰\Û‹Ñîq:‚Á›·0¹‡,Ûc@½²óúÃ?Œ.>D;BšY¥S0 ÙöÔú‰Ñ>ËË~Û(±0b&é:ªNIç€tö s‰¨W¡¬qµ€¬ƒˆ• *¤1ÁÍ)ªåÊÞøð}±±‰È®Æ•võΕ޻Ñ{×”;•Üù“/ÿŠ©Nîg ÚÓH°uLéSÚ›¾JI‹°ÜY¿%§»…Xðå^¬T+Uþ4˜YO@8f´U]OJÿ'KïÁäÆ}¥ý~ˆ ²Dr"f9§F7:4ÐHœs`09gr˜Å$Q$E‰Ê²â*y%k­dÙrÒZŽ²d+Û–e9®×û¾w÷Vݺ÷4ß[…š1À?œó<¿§»1­uEŒh‚ ÔQá3²:$î‘zDt€‡{ˆX3I¨@ +Ø•ÕyÆÞI;g'Sn&çf²fOTãð«í>p– ©uŠà}cFzXONX„ 3¥¥wÕNqÔ„é<·Œš†u˜ÊĪm`£A(WåÕn@²à6f¥axP´^©íâK6*Ë ´ ‹¯qúAg†4ž1=9¦§´vɆ&Ô@ ΄„†µÈ­¶ÃS°-å¶@ð‚ZåæŽF€=wpÊ™ð8Ô-â¯C©8Ù‚ Æk܇T6~+:j&É#ÚÅä’ÕÝqhf˜´ÃLf0©+f×ü™5€+ &;¾ôÀÁçmLÆ ÐÎ&¨`˜S*­«’WÌ{„¬‘àœ@þB j@*m ™9iÓOE[°ËN%*MÉÓbn!ÙÜÙ<õHffßHÉn ·Ñp"¨âÑ®/¿i]x +é}¹…*„¯Ü«Sl¤»'¦ˆ‰Ú˜*–½RÍ Õ…ÄŒ\\%Bu½7â`3V¹'—Z ”6½‘—ì¯Üï+Ì ™(@ ;t“TbE…JÐy"ðý¸™6!’r¿tÞjCfìLÀ6bÜDŒñI‹Øàd‹ ›ìŠr[wubÄ@ÞÊ=KÀ©Ù¢ ¨›Èè‘è°?¢q°I ¥uúìd 0Àæ&‡ =Ò»€*ƒ£zÂ*@¤Œéq­wK “vaÔàšt꜕‘Òà#z˜*ð€²QÙ8åÖ¼DBã–`Ì&YÊ ~ÝF&\LvÒæ›03 BFƯuH]ãFjÒÄ€Kžr‚ kA…à••¯0C„úqi°'2Ü6baÀ{+•²Ñ°¶³7:if VaV¯lôHCzÐF§nÞÂ<8fÄ•H"I$`/P+P؉ÔÈH×ͤ¼\2ßÜŠÕ7Àþœ|Þ%ä½Á +ï„ +KÞpðƒ”½~å&pBb–/°ñE"2 cs6.‚æ¡Ó¡ââ¤K·ÓÙPi;Ñ:–îŸLö#‘*„ÍxcIÍ©1Ù@&mlŽMÎùrkÖP© CÂeåî HŸƒÉ°‰/³œlk°”…)š"˜”›Ï»}P„m<Ô´1YpOXXÄWì· .9›ìœ S³d|:P\2±q+o!“P~DÆlŒÚåZ@ø’rßh‡DO TRˆO[¨Äˆo²RYP!7_68B£°’ðC«`t?Ë6:ïº f@øv(*Wàˆ1ÒS®¼üSçŠèÝñq³r! Ÿð{í S„X ¤”:dÀ) o¢4¤vštÈêI½+lD¢ŠNA'frH‹ŽBÄ0 n“`]~HÇ‘ô 3{öˆý§QóˆµaqhÙŸ5ze0FR㎨mÿ449:eYƒð;©Üu’2`AˆZV +’>È)î¯RRÝ+U´¥e0©náÒãVÂè 8hóe±@‰KÍyÃMl0.ÏpÙÎæ¹ÄŸxÄÒ„ƒ³&"j"eµ'‰pQ ´KCÃ"ÊÆûLrV©"4ä«Bn3X?@c}åƱˆr´ÁHÅ&Ü>Ô$KÞø +•Ãæ´žÚµ¤bÓ*;6¥Ü â29f¢,Þl‚:0D,sÙe"6Ýa¸DBýû¼R ·&鈇êõƒîƒµrÐi ‡½â…‰†¯2Ã&BÜÖ;E~„‡Ý‰˜rð°¤S6hí˜Ù›Ð»£ iG4¨Þî€Ö%þŸ›Ò)(ˆóPTCZ¸ét15d%OHIXL¼xÒÂ@ð7·¨qpczÊĈ@ œZeáÇÍܖвb±)+ó•Q‘œdBÑÞ.ŽBa˜iµs³"Ü›rŠ‡4®q í¤S 9ÊÝ@=¡#SȈŸ°pÿ<¦?¢q¨Ì^ 4QÃSnh +•‰±(j£CMÞa½÷Дë•UÑO—h%c_~T9Öp³ñI ~dÊ|dÂ04iÑØ8„Í‘ ˆOgÁ™@^¹½«ÔUPxŒ©,¤ÑãsÒñ ;þÓ×8}0<øóW|Vcg¼Á¢…Žë dÌÊL8€4~ 6­x‚MNO!,ø”]¹w씳Q.Ñ£å..•ùܬݗ·±Y—X1àIµ'lÅ+lÊ›&2ád3ÀEì\RC‡Ç¦}Hí5£°/‹x¸8jÒ.BRÕº¡Œ.‰+l¸áâS#f +tòQƒ8ý(òÐ)ÂW„'X½a_´îyÀiÌ2e&N?D6„Ïû’=chóqƒWeôNšh•€œið¥I´H™©ÚudÂ1¦ÇaF—ÂÊ›©Û¦l.2Iùk ÕÿxX}d¢¶Ð6"ç ³ Ò𜠢1Ó]§œÃĈ†nÖ6mô& %o›r ëÁ«7j†$ŽF@GtJ/ 3&™Ñ Æç©94Ü´ÐI2( ËÃŒLÞ0•›ÏyƒU·a­`ÂeFîpé;“´ÓIŸµR`v)6ÚpÙQ›0fæA9!΃ۂiŽ[)wóXg,Ü4q›Ö þQ(u"NE;Áòº¯¼‰'fLlfÒ r')aÜJYi›P·1y·¿e¤rLlŽ‘ûW“vFï h<¬Û”ͯv@ú“ ªlôaãÖIóÆaqƾì¼/Ù’2]•‰sAyèûaµuÜDÀRR ñU|Q‹!‰CüQYÃÍS¨n’QIHÌûË£6…¸ ¡àeGÔŽkDë†ðˆ"Út E[Ò9ùIˆƒghÒ 6úZí +i‘ˆ“-3±%3•Ñ{§¬4XlД†-iÜa_峫 °éOhÂDM‚/{6Nca­hXŒu&,°~¼¬[œTbj[íZÉt“ ß6fÓ &g@çPîˆ< è¨CÇ  ±œšT‡쬗IØèÖ ðL²+'>â {&h(7hÂ:¢õ k•à¬!EB˜Pž´pŠäŠ‡'¡ #jòîÎÆÃSî11ªY…qwÄIå€U4vÿ„‰Qé¡´Â'x=¤c¿ +¢(xT÷yØ5ˆùóA«õÞ˜Þ›2Ó>¹¬ìs¹3sÐIÛ5 y“Â:T×3’VÂN^íÜ\ +峱ʧ—èø¬‰J›(—³s9ÐCèko|™Éïâ‰yø/'b&SfU9(“ wðÈ´™Hk D[hXRÛДK9ÄÆ]|ÕÅWL7ÁdofpÆLDBÆHF-ŠveLXÄPëPšÑ„Ø„ÕN‚¡ÛWEC •Ñ+gŠ!mÉV9”3‚æ rn±n%Ò6" ý~ª6ããz ÃT—[ž„•±Ñiå­…QöÕ¢ÃjϘ‘V»C&"cgŠ:O\ë‰ÛèÂu.|Ö +Pb/Äm+•Ñ!!;™"ƒm5 ¾ÖcvLv~Ê@h,œ›)±¸ÑÕ:ý#JÁ «[E|&ÕbP!SVpäL@ž?4á<<áÓyopüó˜Lm&,,lÁ¸PŽ9x0»)¡22j‹ÏN&'­,ø,a7S Ã&Ã&íO÷xH9ð¿(¶¨Ø¢?·…»°õÐéB¢ï†!YAui“öú+^©åbóùcF +¡2ÔìÀ³¤ HF{ÑÆ—^2’IH„ÜwûëV^íð1™u<¾`b²°YV*MÈ3tjJÑFDQ©dUί5uhXeå h""”Àž`÷ ¨¬qGÔÝœ w~ +¿ìds ¶VkT9dÇÕ XRë +Œé1µ‰Á¸œ• ƒ¾©íœ'Pqû¿ž°ß Ρ.%Ïhåä ÊÙÀ°…HA6„dª²#ÒÑÕÆž´BðWÙ…qX K§SŽçÈà•á¶q;0¤rÐðI[`Ò.yÄ&[¤"}'“õp³7|Dë‚|:nemdÜŠGÜlŠšZgrò•žr·Žkq„Q®£€¨¥b«uø6 HhL9z¶ã)ƒ+lõÄ¡œþáˆæ©Ç´(Œ¯ŒZÿiÈ4¢ÆSÎ`‰›Xƒ':¬'np[ð”•hÒâMsQÀÎ"T,¨ÝÁ[UöÛÆ­P¨›ÏŒDlÞ´Õ#ƒFA g<4éš2³td­7j ~B#C™› ,ƒ?ò`¦Ê-¥‘°Á6€`—'`5Œô5:if=|…Œv'ìðdÖÎdLNÁÜ„Öy†M$^.Ñwrµ]0 a¢8”£™ÄE؃K°a¶ñ›'â!Ÿ +ñ¾Ë—»U‡hÜAÔ_F}Ðÿ)gÚYã êÜaÐy³›waЊI‡ßèM"bµÃåm*=7b4P²öaµûÖ)Ç!-2iç!ÿBО´‰fop8AmW™ 53ã’3éö®\[ƒ¸j†’@BÀ!V rt¡’.oÁc^!7‹`f´HHíPÎ Cýxýe\,"BîÿŒ7QV2iÄÂPù£ŽLA ¸!.RÁŠQRÅ—Œ=h¦EðÑS* M»{¡D3#5ªÃo1eÄ4¦ÅtNÉ +³c2—¨±26=¬GM˜ÇtˆR6vÊL«ÍŒHÛHLY€»ì· éT/üPk¨ìáó$'M B§0>uÓ•p +oqdJÙ>墭Üü+CšC£f(Q'´c!ÑADd !›±D]è€F&TCÈR#¢B#tJ‡•ew+’à t§ˆ›!¼ytÚ¿ŽGÇìü0X¿]pW8}#’>mù2sL¼oðÆÕNÂ8ÄOµÒ!츪—¯±ÍŒ;ø#åOÍ»8èkš]¥Àƒ0°âqˆ3*¦A$ˆ66èn&`»$Uv~Ò Ça£wXçz´ú¾„e5°S + Á„Gíl–ŠuˆpÓJd]|TîV•uXë6!’Â)¨ +¡+<¬õȀēfˆD ¼#\–ÅŒ§Ü\ÉE¦•Hbç C*À ÆõĘžP™8h ã¡t?ÙܺàV)˘Ö) k½àScF~Ü,hÝP±þ! 6¢F‡¦<#Te LyO"*­³ûP> ÈmP®¦#©l£Z`$cXj~ÜDÑ &B¥ó¨3ÅA–‡ZÕÚD)9o'Cÿ44qËõ O‹Žéˆ1 í¬2³WÑnR¨] &”訋¶#¾ + ÈømjlÌ¢\[¥º™†Ôž)e5 îÅ•RÔÏŽFõ6î¶1«ri”·Ó¯\ßå +¹„¼'Pv°À“7j¥³H ŠøËèY6±p]eåáJb£RðPÎFYx¥÷á·<ð:þ)·¨’Ç"ð¿X ê`”Ó—.CJE"TöªV&çâr.6ãõ—ÌxÒ"ï¹ù<Ä4³q£6`æH®}€YƒÛg‚#Zé‹ v…ítõÛì­LvÂ8¬§†ô¼ ^„Jz[€íé ⯘éÄW&í‡&mMÌÞ˜ƒÉÚ™œ™ÌX©èؤ•ŸþóÆëGãõcX ã`K KÐ8“Ð,”Þ­ða=©G‚fåkXÚˆ@ÉÐVSâ˜ÞC}uÂ_‡Xª®»"} °Æ¡0X “"‚Uo¨~«Æ ‘Š4mTƒêœA½;¦uÇL¤r©‰Ú)i­ `Ô¡ ×!•Œìæ+P“ª(t lFk%Çu˜ÞR£ÀÃzwÔØNÆUZÏ?Ü6qËvDXSÁî;è‚Æ\gž´:±Ð`6@>¤…¬l‚rÆJã†)+çµ(ä}S¹~téæ¤Âð˜pøA%þáˆþ+Cæ#Pð^‹Do^ •„ +tQI+„²„ÂS+y3Ó1`q,ØàÓ³t¬KE›lªçò.±ì 6àçlrFÈ-‰Åu3“7 ×yÂ8”ÔF&uXÔÆBç†t orœË`RÕÁçŠa± Vá3d°BË·¯‚øÊ:üŒë™Æ¾\Þ B5£’Å¢ Ef2áñ×’!‹¹¹â/L‚ø˜(è}Hå&2 5…[h¨isÆÍx<ˆÛW°2i p™¢b3þüŠMÈB©OÞ<¡º7lb !å¡¡þ„ݚ׈ë=ª£0`.?nAMxÂÉÑ@e8 CºÑ8|$h%R¸Ô†¹”k®‚f$€ù S +V2k£sSî Î+CüÔ"@}¨•E¹ÆšªËÎdL0`WXë‹Fõ˜ˆ•2ê±ø¤3Mç 6ZÕ €dÂÆè•ÙÃ: JËŽ'-XÜ -C¥TzL± ‡ŠªK9µD¤Íx’8hÊ@èìüˆÆ ®ªuHe‰;;r“1J*©LÞI3‰pi.1 pó‚RhÌLY§œu’½b²ÕpŽ' ;èàJV&¯ƒ){#ÃϘtÕuÛ„SeaµÊÕ³!'›eb}>Ù’}Tªëa”•31j$ ©Ö¨a¡F¤¶Âfú†ØdϬ; $¢Ma9Ú>ÊV=RÍJÅ-h@HõÜþ +¨„ƒÒHyC5"\wû tV>‡j|v€«P~ð^lz6T^ èDÏá+ÙÙ<+OØ#>Пì)*UðP5ZYóHe=.[Ù¼+ÐD#=\î{Bm+W¿ók¾ì¬…Žs™!¿æ +ÔLTÊí¯:¡P…‚7\7R²‰”uÞ°•IÀÓ¼¡ +ëréy:1€ZusE‡"YI­'b¦2 T*»oÒéƒxh&ãÁ€EµH4\™_€×t E;¿Ö"¢Æɹ¹ 蹉ˆÁÄ¡Ü|‹OÓr˜ÐÉåÜbÑ@&&Ð$/˜¤â=Bn[˜Ô(„°7¼lÂ.ì¾mщi§Xtð ¼q3â3ƒ5$£·XaØØŒ —õˆ¤søLn¿Ò„Û@¯ñˆ½6 +o£3f"9å +«á}]ø ‘ô%6܆wXƒAëAÉé\AppH‹¨P`c-1?Ã¥:t¼–šÞf“}Hô:TvùÊn±nç ãD¹î‹·m\Fë`¡Ôψ‰…YÀtÀÔŒH´ÎÅÓѲÉt±qÔ_p %ÈqÐÑDdÆlÁOœ\bÔD 7/Èw)Ôò³h¤É¤f|¹9o´ŽGʈ?KʵHc=ÜÚ"b-<\¥Ãå\ÿ(iXé$”7X…RqPƒ*©aR%RZJuw ÒŒdª‘O ®ü…±¸ì¶‘@Û#–íTÄJFŒ¸Œª\ªÇ&§ÉXÇH'•éDZnÑ%@6½rÉ.s¹%_~…NÈ`5×?Ée§Ð°ËW²ËBn…NÍS‰‡³P1=‚v0xAƒŸ•–äƦT\ +•V@“µ^v•j0Y€IH6*¡vú¢³ñ¯ ìjc!bN6ƒŠ!5‡GšH …Ê%§Þˆ‡=R V¡qÈø4,T°¼*æØtÏ&æGl‚“=>x~ƒŒ÷à«Â3ùÔLfú˜TYÁbm_~Q,Àt‰X7XYs}¨.˜—gcÍu170zeVŽsËT¸f%£6H(<Ä({"ÒÁBMÐxw:ÞU>Â¥ >„lc2´ÜM¶vã­]W  +RÝ ]eåtˆh§b´Ü +7×åöN~îDg÷JDyZ‘ʾ8å*zU>>nDlLGãÎôY±°Kȹ| %1;ª,‹¹Y:ZÕ!~p +4ص7)—¨å™DÏÎ`PÊÙí¸…ø–%ä¦7Ö%}>;mlÄ;ÛT¬JÉe1ßçs3\nF,Á>¶ýÅyj)\óË:ásIA2!;)¯×Ï×–Ï…ÊKL²‡†h°Â&Ú\²¬n¥íXç4œ±RQL„¦‹€ÊѱØ1—[t‡[®`3XßeÓT|ª ³f³‹dr€Çúxt:˜›­-£’=•Û®ÇgðhGYm©ª\´ì/6ðÆÅ9è«ó©V 8ilŠ…EO¸nç¡SÚZÂTÊÉ)–mgsÀW,‚ˆ%#ÉäM±R¤>PX‰6vBõPu°\¨T„²ñY·T¢]*1©oAuÁf‘±–Úå³À–Ê% ×\r”6X^Ivµ·î–Û»x¬é/Á˜çâíÝì™Üâ™HsƒMvý™neáXuéTº·çà³h ŒËm,R']6ÑõJ%¹°`£’2AF[D´í ·ü…¥Lÿ ÖÙõeéΦ˜íé0pÕ$ï†Ê+‘ÖN¤½ë–ªF"Î'¦-l\eÁlÜ—™aSÝH}³´|¡µw=ØØŽVWúÛw²‰žKQ‰y&5Ï&a7rs·û +«V&3f"A3¤LEªRy‰ÏÍûK«¹¹3±îQ¨í@n&ßÞp–+ ŠØè¬7Øæs+ÁÆ~²wšNôåʪSÈA s 9ˆPu7Ó?Óß`õô“½‹ýK¾ü€N´£ÍHkS(ÌÉÍõêòÅöîu_qÁ)æ|ÊãÏòé—¤¦egŽû‹ 3û÷dg;C%§?Ï%;þÜL´µÍ—7üõ2;G¥úðO žr2ð» ,‰™ÓñÙ³±ÁéøÌi©¼V]8-L¢¬­ñ¹E.»àË/ÅùùSÑæ” XÙ4%·ÀÁ}ù…Xûhªw 9äØxÝB1IÉõPu-1½ïîçú'j‹ç±¯@ÈÐ[&ÑsKPÉÎAº:TÝñj“h ¸¼?¿ª¯ó¹9&Õã2Ð"ÞvJxpšÍÎ…ë[°Â¹¹Ód¤a'Ãn6nCƒ0n¡,fJ‹g3³©Þ±Ü¹Xkknï.p—¨´a¢w´¹}Wÿè݃cwwÖn3]&Ö”H  ø§œ.O ÊK—rƒ“ñÎnmù4¾ ¢Á¨¨/7Ÿìì——o/-ŸÍ–OßWš?‹lã’X° U¨n¤çNEÛ{ÁÒJnö´™KZ¼¢”éàRaÒFb\²³ÛÚ»§µ{uéä}w=ý.9Ã$*k×¢Í}·¿î‘ZtjÖjYÙœ º|9½'VèÏMK¥…p};ÖÞáµ ùd}íÑ—Þñ–TvÖ—ìVΉ¹5>¿ížô—7ÁÙçw¯9Ä¢Æ姲ZJMŸH÷Ï/dúÇÖNÜ·yé)"ÖŽTÖêkw´·/·¶î(-žnn_í»Ñ\:í‰oÇ;;n1* RÝ£0‘Âü™êêÅæîµâÜñÞÆís§î#ÂåêÂAwïj¼wœÍ/—/tJÎ_Òípe†NÔllœˆu¸ü¢Tß +ÔÖSýcéÁ ±0S˜Û‰v6ÙÔ´xBe_n&P˜óççk+WbÍ}§/…GË¡ò­Prtd¦ªmeû§sƒSþ\GÊu™xK,ÌË­­H}#ÝÝ+Í]>yooû2¨%©ÁÆçN5V/fO¦{ÇåÖ>ï·no­ßa¡¢P¢PÙÁ©ÔôÑhk§±~yzóê‰+Ï”æNšÙ Ÿ[ðW¡ì¡¸üº¿r›»D‡#b$N®- ùA¸¾*æBåÅìô>“˜ƒ.€‚V—2½½Xc‹7P1 nX\8ǧûÑÚj¬¹ID›ÑÚZrz7PYv‡ZB~.7Ø‹7×ÈH9ÞÝöe§åÚbyñ,¾TšËöwÏßÿ¢\_ƒuh¬œïì]/,_HL WWIyø6×Þ_>ñp¢±j%ýB¢žïÁ\η¶.–×.µvï°&f—!ÃŽ[bi³º~wyùr¤u,Ò=ˆ4¶¤â|º¹žŸÞ0z%½7 aŠ£Éä,…*kd¼ËDÊÅÞn´²l¥3Lf^jìúÊŠóËKjÔ¯u .>éæã,ìEndÆ–™=›œ» dçaqÅédg5ÖÚ×V@U:sû½ø6¬…Žó3Íõ{àÑX¿ +…'Õ×JÓÛ7úÚÝÏñçê«ç:{÷VÖï‚þml^mn]óåÎ=tå‰×Äb_åä):b59}´¾~qþÄý…ùSÝåS<ûFª¿5P˜9–ìî¦ú³GïÝ»ó™¥sχ*;íÙc½µóD¤FÇ/öååænyùÒ̉G–Î?SX8·¸s±·z +—a×¥E.Ýç33rmuöøCƒÓ_Ô6$ …xÛ(б&)·`Ål|ÉÊ#•­âÂ'(˜2°‰« W\fFÈö˃ã—î±±|‘Š…¥Ë™ÙsñÎ~cë +t +“ß2Äü¥pa:ÝÁÄÂ…Y¹±6½sypìzuõïaR­Ò;~îÞ¯¡B´<[Y8]^Æ$§™ì’¯¸®nµw*6&éË­¨6ÞìñW6`Ýâõ3÷>·tüª‰Œ€~"b9Ñ;)wñÙ`u6ÚÙ=ýÀå^ô'{d¸.wvÂí=¹{¼¶qpÈ b΃›‡m¸Ä':Âb¶w¬´r)9¸]ªíxø|cö”¬qŽçbÍ|ÿXyöhi°Ý\> ´?éb¸x5Ó=ÈLŸˆ·öÀŸL7×ÝÚ=÷ðìñëN.™žÞ 76‹Ëc½_qYH÷ž½ÿÔÝOñéÖ¸ƒc3Ò’„ê+¥¥³‰ÎöÌÚÙ—¿õÓêâ)N® ¶/¯²uôFÿè½+gmíÜ`ãýƒ3÷¼óip7à xÓôô±D{7Ó;ZY½˜›?Ã$»;§ïlGƒÅD{;5s¬®‡ªë¹™“ƒƒ‡ÊkW…å®råXyÄ„ àr×î Aq³‰æf¼(ùNuéö ¾Þ-åûÙΦÉö«@5h¨\‘îì­¿1¿{…W‹½­Dk ‘ +N¡€‡ÚþüR¢}4ÙÚ³3ñP²ÎϹ„4ìx(—[¦’³x°ÌÌXÉ ÆEáR:Z¯-š;¸7ÞÛsËNaÒÁ:9Á—êȵ­X÷ ?²½uyáÌCbi³JÔ­ƒ÷-œz´»{=Ý?Hww 3bnÆLG ÞŸ¸|·/ª®@€çÖWo¯,`Óm‘r¹¾’ìíççϦ§èÌ"d#¹ÈdÈÔRv@ +N2êOv ó'3ƒ äø7TˆT„Ü !CpkSñ¶SHé½!;)ϗŸÒcN&EFêÀ9(pN êò`ñáB??½mð2­þþ}…Å;¥íÆòåìÂY&ÓÏuw^} •Õ7?Ù¡|èCîù+»éÅkÁʦñ#¾tº½  +vª¼|¾¾~ynï¾Ùë™îq¹±ÍE*›§n”{:,8 +*yJÈ­‰Åµdï¤X\ôJ™õc—Óµ%DÈQ2¬êº¿¸Ê¦þÂj²{ÎÍv×.Ä!TÞ„4L*ؘ¸kå:›ÅÞÈ;Y0 ´ŒBßAv Œ5·"ååXi)’™µªÍLõ³™YȪH f&h˜X¯»y•K÷̤ì 7¸ôLiþlqþ4<Êsg e'ªËG/>Šñ Rlú¸X^àïx¨»}9^ßðøò'/?Ö]9©WiÊŸ_ÇC?ãz\2â!dP ¼a?*Kv1óµåó°›ÞP9Z]-Ο+/ÍLïÉ&HëPÿ”3 6eg DËs§²3§ò‹wøkbqÞBùD+ÓÙ÷ç–Ýì +ÙÄ—,Îìf dØBF‚¹E(u÷ÊMDîBB÷+\¢#¤¦Íx8Û=Öݾ.×·Wªs'BåU)K±Æö©{ùŽ¤êz|údiùrsíš¿°‚†[N1ÏËõ½S7âÅYO67»ëwgNÈõ-Dàüha¾1:"”›ìßWY¾.­ù’3¾ô7`¦  ²pAí Q_&TZ äç«ó§;[WÈpSÈÊ_}à…@¢c!dL*3±îݬ,^èn\1yÉÒlwëN<Ú0+îwsq«r*0ê¢Sn*aA3—ÀÁ!Œ[pÉLÉÊUO Ø៙°r_·ÛÈ„š5xTB€<=ˆP••JKÛw…KZ²óqŸ¶P !='€úRj3Žrij¨½PuCÌBÀ,:ùŒ ¢Bš¼b"V™²3Lº/U×@=¤Ü@ÎϬîß5½y ¤Ü¾D¬ºÜX<Û\½”hï ¹:9í2°ž©úli$Ú°#õå ‹Ç˜;zƒŒóµÁÆ©{ÍUQNÒ)Tª†K«Ñê. ÀˆE¼’Žè],ÌCio_­.œxµtò«­Õ˱ÒüÂÖµÅÛ‰h¢d@#±Òq_b–T ˆ ÷˜˜â ¹P\±û+`OBf††v‹Öíl ä·Ð?…à,‘á +ŒSëäd”‹Ô' ȨƦ¨Dn.ÕÞËõŽE¿…Œ‹Kzµ¥óv&ˆ>=˜ÇD*x cÆýC¥y"\EED,bÁºT\ÉMÔ—.PáºÙt3‰`¦oÂü6"]È/Uú²‹¤Tsà2&faËÜÊ᩺™/Íín^km\Mu÷ Þà”2{ƒ‡³‘ap@ˆ3±úF¬8'Dk¾DSe¦QÑ_œTéhõP±HÊ6Tj÷¶ö/Á¼ º•R™ ” ÑîLj]Z³ÇEñ@Þ—š¦¢   3aäô#¤Tâ‹5Väö–¿¼„JM6""Æ›…Þ1TÌLZˆ)+ÀFEHÍDÁe:û¡ü¢ò+…ðéTccÜŒãÁ| 7“›9cCüE#0"> &íYZ¼P_½T™?olcRÝD$ r¼œa­^?|y­½s­¼x!X\…b·PvVÆÂEo8OF*¾DO.­ÆËë\¨Š )23yÇtˆÑNZc“§ 2ÃGª{'¯ÇÒ­ ­ãÓÐ}D¨FëÊ™/65eca=“•UÂ_ƃe­‹6cг‰P¡_˜ß­,ï%{KÉéÅòüúàèí '/6¶N²ypÀb°0f³³^1«v2¯ˆrQ.Z dÚR¡Ë¥ª|¦–hÌž¸òXÿB¼=ï e°h!Tìç·­ÏdªsÁl[mõzÅ$*¦õ˜? ¼ê3L4oÍ‹ÅŽXhbrÎÌG<ᔿX©¬mÅ«®PÊ-&¹Ô´ G´6ÂZ€Ù•æNQÑ ÂŠd(éË5Q)ÎÄóB¶NÈ…`±ÓZÞéížô•Ú6>êK÷ý¹y7—Ó»ýÃSöQÓ„øâÕ•H §`##€grk[(,‰®Ë__v‰ñp¹CŲzB0Ò’7ZÌözÄ¢ÆÆŽ©NT$ù$éK»¨¨ õë<¢•ŠrµKKÀ8\¬¼vòÊìÑ‹b¡5æôj½~((¿à²£:÷¸Þ£¶RZvs)µ¾ù‰ƒéðÉA¤²&å˜huíäÕ¹½sÉ΢۟œtQ&Èe;x¼f ýX ‘lo@XÆÃU(i+)#bµ4{¾µz—˜é©¡6¼~BÊEÊs h¹ÎÆš|¼}éoef¶¤|[®ÌÆjë‰Æv¨°4e"o6š°{Å\05mr²C*Ó˜Æmtú¹H3ßÞIÕ·ð@uÊB« ˜“Ž‘ò!ë°Î9bD½ÁF¼q —·HÅàò ‰Éeë½ùÍã«ûç.Ý{å‘gîöëϼö­·~øÓ?ùÝŸþú÷O¿øëwòá…ÏTŽQr]ùˆº÷r©Lq¦X›)·kƒÍÎâæÒîÉ£ç¯\{ô™K?sæ'v/ß8qí¡ >sç§^yãí¿õöâÞÙ•ý‹ÙÎ2)¸¹˜løÒm<˜cùBg®¿²½´s°wîÒí÷>x÷ãÏ<ôµWOÝÿ䱫^zøù'_þæ«ß{ç[ï¼ÿúÞ}äkßœß?ÏÆŠ&lòp:I·bÅ.MÆJõÖâæÜÞ™ÞöñÚÂjwc¯¿sjíÔ>õâÏÞÿè£_ÿîßûñék´÷åbߌˆf·dCÃ&/ÎåÛ[.¡±ÓL0Û\Ü+Ïm¥;Ërc.Õ]^=yíì=_yäÙ—¾ùýË<{þ¹øÈóB¦eò†¬XÐèä!¸HC€Ì­yøx8ߎW§ó­~e°Þ_?½sîÞûž|éÁ§¿öw~ñÞÇ¿}íû?=qå¡þÚé\s… –-˜d@|x K…JVoÐà6%— ƒýXmN®Íì]¸þ迾öàóÿvéÁǯ<úÂÅOœ»ç«W{áµ·ß}âÅ×î¼ï«»—î'#%3*šÞëKø’P~6ã¥Y)Yú•þæêñ;zK«{gï<¸ãÞÛ¯=øõ7¾û›/ÿúÛ?üõ‹?üåƒO?øÙodë £︑v³¹Pn9UÛÕ9D…´c"*áþ’Œ9ˆ0æKâb:Yêm»xò®w/Þ¸xß<ýÒöé«Í•s¥ÙãL¸<¦Ço±3B¼5½¼²uâàô/_{ì©g_ó»ïðÙ'¿ùÝ/>üäÃýŸûÏO?ÿòïþèñ^]9~gn樕J:q9šlK‘|2Sjvç¶.œ8íÜ•=ñÜëßùÑë?øÙóo|^ûÖÛ?ýùGŸ¿ü­¾ýï?ûëüý•ïýü‘Þܹp?ÀX¶µV_8ší®øS•L¥5¿º~æâÅûzøé^|á•×¿óÎO?üõï¿ÿÞG¯ýûÏß~÷£/ÿø×ÿûÿùÿEþù—?øù¯®=þµÚâÑP¾‚‰2R4Y¬´f–¶÷á±qìÔ…k÷?öü×_zã[O㛿üÍßüîßûðW}òÅ_üïÿë¿?üõO|íµ“w>˜®/ð¡:é+…³óéö®“Ž»¼ \¨w6¹ãž‡n<ñüy鞧^|å;?þÑ{ÿä—ýñ/ùã_ÿë—Ÿ~ñéoÿo½³vün€C&ÜR:\ 点ézmaó`çÔÅk~õÁ§ÿõɯóëo¾ýã÷?yï“ßþü£ßüê³Ïÿð§?ÿ×ÿúoèÖýâ³_ûa²±¢uñ“”Sç¬xs¾x#S¬îŸ¾òðW{ê_¾ñÆõÉ»ÿú[?z÷Õï¿óá¯?ÿø·¿ûùÿþúŸÿùŸOóù ¯½uòŽ‰ê .ÆŒ.¯ÎAÚq bI±µÜ[ÜÛ=qÇåßxü¹WÞúÁOõÉ~öþ›ïüì׿ÿÓ_þó¿>þÍç}úéüçþìÃOï~ô™úü^¦³ãKõLÊßg#§ŒåK2þT8Õ,vVƒæìÚæ©‹Wyò__yóÝ>ùäó/¿ùï?ýÅG¿þ¯ÿýß_þåo_{ã»7æâÝHéi*Ô$™ðteuóàÄ© —îºëÚ½×yìÑ·¾óÖï~÷»/ÿô§>ýì½_¾÷òë¯\¼ruik/Uë… +°aÁËŒ Eâ•jsgÿä•{¹÷‘'ïûêSÏþëË?úùû?üÙû¯çßÿáO¾üÓÀt¾þú·_~ã­³WžÝ:Wl Ѳœíd›s©êt¢Øœ]ÚYÛÞßÞÛ½z×]ßþöw¾ý½ïÿ‡ï|øég_üùo°ï~ðég¿þÍ_ÿöŸ}ñÅ[?úñµG™Ý:.Lsr™ä…`¢TŸž]Ù9vöòþéóûÇO_¹~ïko¾õË>üàÓß¾óþÇoýðÇïôÁo>ÿü“Ï>ýì³?üø£WÞüîù«•úë(GÙD Ù‰–—½¾,åKDãÅj½»µ»ûð#½ ãøÑÏ_úæ[?}ïW_þé/ùÛßùñ‡_~ùÅ/>øàåW_}ñÕ×·Î\Ó-„OX ñf(ß‹ä;ÝùÁÒÆþ©s=ýÜ×^~õÅ×Þü·o~û§ïýò¯ÿ_¿ùòÏï~ðñüÃ?|ù姟ñÊ[o?ôøsÕþŽ’uNoc͈ßAD@"Jµ|}°uôäCO=÷Ü+¯¿ðÆw~òþ‡_þù/_þå?~òþ~úé—üãç¿ÿâÝ_¾ÿñ'½ûÞ/yú™sWîKT¦™Pƈ:;!Æ«ÁT-–i-®½þÀo¼õï?yò³Ï>ÿüË?ýùãßüî—ŸüæÏù ¼Î;?ýñOÞýÙ»ï¿ÿèÓϽtßüñkŒ\7OÐê\^ŸÑ†c”Ô¬?wùúC¿ôÆw^~ó­o|ó[?ùù/þö÷¿ÿú‹?üä½_ýâ½_~ðÑG/¼òêÙ;®®l e{ùî~0œ­·•æt¾T^€µ9³¼°±³~ùÚÅû¹÷Îk—Î^¼=_+Š‘ÆpvŒuâ¢ÎJ謸ÁN`„_Žf‹…F«=ÓlîÝØß›_™?qöèõwÜÿàõ W®^¸û¾cïj/­JÉ­óó\¬aE8»‡CHFòœÜ?~çêÞét¾Ð¨wwÖÏ]8{ýþ{~â¡ïÿ{ï}ðë÷?úø“Ï>~îå—N^ºÐ]Z +$²b¢‚At%NJb‰½ƒKÇ/Ü[î-[½Á`v}mùôéƒçŸòí¾ýáÇŸ~öÛß¼ý£·¿ùæ믾òòsÏ|õ¡îÞÝ?¨÷–%p4ïábV"4a…\ƒkí”ÅEû|á|.³³¹õÀõ»_õÕ×ßzëõ׿þóŸ¿óÇ?þþÅû×;.žÞßÝhN÷rŽ›tv¯WˆÁÃE‡tãÂ+F“ùj³¿ººuñö3_}üÑç_xöÛßzý—¼ÿùŸÿéÏ|ï?|öÙ'/\>WnÖ\8ï"#Ã:;{De×"zÔC„äx-/ÍÌ//®o=sûíw\¾óʵkw_ÿÆË/~ÿí¼úúk_éÅžÿ—×^ýƃ7î=~ìx¦ØBùˆ ã¬^a¢„?ªÌKñ²bÉüæÎñ»®?ôÜ _üɹëê½O>ùÌ~ü³ïýà{Þ¸ëé‡ïê«ž=ns{«==—ª j '¥LÿæåÐ4ø²6Âåá2ùúööÑ+W¯}ãõ7€•žzöÙ¯>þÕ—^|ñŸ¼÷ü×þíÊ•{Ö6w‹õ6ɇ Θ1Iocô aœç!}þx%Qî7ú«3+{˜(YqÊAòfŒ7",죇“ NæȘ^kD]^ÉCG˜ˆQ!œ yIàX)š®ã‚Û<˜%>€ûB´\6¡Iat±›p2Qµ“ÖYô6/BìëÂEN.{“bX”˜×˲L:›.T˽¹ùí“ç¶v£Ùœ›öiíø¤ Ó:)0!~­ƒÕ»X6” ¤ê„˜@˜ %Ê‘X*ŽFäX:—Iæ ¹z»³¸Š'©t»;])—%1èÆx!!ŒLø³R~ ä¿¢²Ü>¿Üà¥R(œ©WšÅ|1“ã1ymc»Ó©•J™N:ž«Ã IÂ+æ]DTk!Ô&tÊàžÐ9T:·ÆÎÛpxͼ/\-ÖSÙz!W¹|åîcÇËÅìt»]­Õggúsýv®P·{Èá DµÉ;¬²ŒLÚà½]ù$>Î&ÄH9/&³åD¾Æú£7ÆùÂñt=ÉåŠÍno¹×_D1™ÎK¡F vÔL*Š sц/VÇEpÁò¼˜Šf;ÐÔ\°TîÙºÒY>'„2—3 +§Óy¿EpÖèÄ¡ŠX¹Qž;çKÍ©-ô?Þ6eÃÂ6iFØeŠ F£©TºHeÒ¹š(I/ÉЂ?TÈ•P¢ÎL›”/H¡Ü kß6b>¬2Œë\ ¡vTÀ|Y:TÎÕ×OÝgăo‰@ +U$.åáÓopÊŒ»È°™4ž0êÝ“F¯ B&èpá*-æP!­üM¥¶ã6Z¶SI+.“bÁMÉfÞM›0Ñ…K)Y<¬ÁÅzü92‚ qу.:#^âòD\ÞHaÕTƒ•Ò ZÆÙ´Q—ÃìTG⺠¾HFØl•>ÜœQÉSüdÚj:•2…Öôàì‰]h:\˜¤“·qZwÂ',…æ= „Éͻпº¼<猣Lj—+Ìû1-@p<ùZi5B™ DÊô2íð<ÝÆ(=„¡7\=#¸ô•ù|TLj zŸ®L÷ãɼ×ü¤±bAd“Ù©´•êœe{§µ§RqcÑO$H±X_àêϯ¹g-4Ùõ·žÔW/ÐdåêbtÑ+ÖÖctúÒRt1@»£P`SbªŸiî§*[Þ ;¿B-ˆHÎ J¨a"ŸMu›5™” k×è5Wäº;4ç‰Ì{¢aÒBÅ¥×ÍÆ–,Ùåµ\k›6ê¤Z2™—OÈÕYËP6ë'ÌæÜ—³ +²“¡•*­ÕB¾Ü>( ί:ã×OœÃ”ª[åì©×ÄΊ™Æ`W«sîø OlцÕjÊ•->;„ÁL—{r¦áO$!äœQÁ§ÃtžI³Ý{Q&{Û ‚]‡ Ía8D]ˆN±f‹3{ï—TÐPÊbŒ×Ḟ( ¡¨äF ¥pÝ $ô¤=³CLª¨lBnºmÞOEiÓ¡?˜ó^ºáw¸ãP‹\<Á¤±d!J™>DòÆ•ŸÏGç<„?¡‡ÈLÏ¿˜X Ó¶3Ä…)¨ÖIÏÇ,Î çÕÒJª±aÌ97²àBpÖvxð+sþ+K±knrÑφ0ÓÈoF{Þ‹ÁkÉO^]Œ@0HÓƒéq¡(7•Êž31+燨\ö¡ªÛÏÌ-Fo¸P”J׺'„w€®f ½ãÅST÷ **דùu.3 ’)„K¤TV˜C$Y°q#êœ µZoía”L]º@’E\ª²jMIuàE/ñá5Ÿ7@¬n=@ùü|€I$K.‡ÉµdvÕnŸ²FÙÇ8`ÐHcÁÞpDà KÌlºé8™[£õΕ%$DêŒ^ÀôÂuºÞwíò¹‘^Ù¨¯ÝK·wµD[mRoŠù•d~šÐZ~:ëÁÓs^jÞK@ŠÍÖø/'0®€ð…msF‹·‡W]è¼7$Ô¸XŠSTo:P9Hªv©·~ôˆN÷æ!¡p-Ʀbì¬ÅDµ'ž ¡ÜáùÓÑî]_B +„7©4q¹Á˜}.=ÿd¤‚–íBà]] +/È5ã)&=Ôj{b~å‹Ùú¦œÌùЙ£bŒM#l†5Ú˜T ‘Á¦«Ý}L./…Øy×7Hfü„íÃ,µ¼ õKKá®&ø”;L{£ $õ\ô0U—j³ب +ãcó"u̓:¢B$Y•òÓL{—Ô®÷mF‚¤ ÿò¸TrFf‹n­Mˆ•®¸.ÏyáLÅìcôCDÚ‘—¼4Ð-áxŽ\qÑPœñd€0c|9Ê•¢Ln¸ó¨2>Špi\®rJD²Œ'+Ü‚¿šsDB(d¢àˆ$g vØ¢Õ:Üüº8¾ïÃ-/*Ñz ª#ªÄ˜kõ¥ìPËö’V‡7»(ŸóĤ0e/ °s˱EWâüë% Ë™ü¡Õh½îˆp®(ïK–?Ÿ Üpba2¡R?»´|ÃA(ÜÎb0¹’o„è%'ê¶wóåðøÅÕ µ1sÀ—wõî¹=¼%Õ¶h³5˜Ü¼ùìÛ°˜_F’ˈd +1¾B›c±tàHPB˜~¢r•”«‰Y—YËkÖj#‚…©T«F¥nµÙl‘K¸ÞŠëFë„MâBB7Ä¥I¹”Lµ»ì cp=!äægý[—Q(_ ֠μÙÂÅÙíkÐ(Ñ®µ ¼d:,T1}ÀX>ÕãíÂÙFq.}Ñq=ÎçÑd¢KÏ vO_’JùºYôa1:ƒIP“Û¤ÖA¤Š+Ê‘|êäÞÛTmzu9¼ìÃB¨¥,Æì0F'˜Pá y»‡$sW]±9/¶¦Á¯"b9™GX›Ðª“£ç\n8kJs=êã² ¾ g‡feÃW–\h‚¶¥|i9þþ…^^F#t^É­˜¥)cÔùTÛ‡k7(â‚7¡ b…1»ÅÑþÑ+BìSZ«g¥á­•^ +r0æËAŽ +ãÒ‹^ôÚrKªy`6"BÉל¸EÚÃÜä~kç)›êº WÛ0æ„RõÄx"8³ÞƒVW–×GˆñÄ5PÁaxcâ’Ÿ] +ñPú¦‡¯£þ××<7fÅÄÓ+˜P{¿¾Û¾¼ŒÀ…ðD¹K7¼Ž œ=X4ý„E¥‡`QìÚšXèÏÁ@Ñ©ˆTŠku¹¾ƒY¸T3ƒí›Ÿœ¼ú!œÌÅøŒêåm³¼®ïñ…õ«†ò£µûœÙùp!¼d"äl}¥’Ÿöv_–F·SåáñƒwT˜Q7Û»™áivtž_¹›Ÿ^°ù)"ÓÅá¿üSoï!¤dW„¢mRïµ65òÄ¥ i.Íúʦbë±dSÚRnM*N½ yÎÂBÞ¯è 26"”¡Hª ÎêzG¬Q‡ôTŠÓ0k3ï§XÓ™aDªxÉ”7 -‡góá}˜Ij-µ´FIeF.K™fˆÔf ‚„,Ä•;š„RŒ&‹a:O0µFÍ rDùå}¤ÓGR™TªbaRÙ~QjsÒéaÂŒ0|…W=˜†KB,ƒLÏûˆ¿™_v¢‹A.ˆ[¸Üæ¬a2;aS=L…s,àz#Îç¸ç²x]»ï‹¬˜{üò;Ψ~0ç¿áNqäÌ$ÊgnxžX’V«5 ! c5¶Óýc¶¸—’4Û8áü8Îfo¸qG€ ™„Pa!o­ø1{)”DÙ,„7©Ô—}„#H_w ³Eg˜…‰M¥´a7÷Á.)@•Y/V<µàfk“I›Mõê&¯•ÅTq)ˆ,‡IT]&¥Ó„V‹‰7¦’©¢Vü¬-×·ùÙô¿ÝÆúãÆÖs³s@©e»¶®ÕÖÐd6ÎZÚp'äëA +‹µám6¯.Ç¢TኂÙ. NôÚF¾µuúäkÌjéFqõ¼ºù°¶õ(7¹•œáF SŠ½ÉÑ?üó;~ö•Wb´Å-.5L5Nr½;”ÙíAixb² ~ªtz%YÚ³:w+멃 kGiCÍÃPíQe)ÆÇ…<—êòÙ®5¤âZµøø“ûyP`Ÿ¶Zß«»LnM,íh•p&>\‡ß Òv”ÍÐz+„¥¼QÅ ÛÕuÞlBl/‡8LÈ4€%`Ò!;.ÎíÞI”ÏzÑ$@ \Ð\!Ó‡Q²'÷©>•îƒ½\ðSŽ$ 1?¡S}½º“ªïŸÜååèu7êˆð7<¸+*A)¢.etH½«„Õc³*=ˆ +ø¨ñd™Ï “vOχkgQZŸwÇC ˆ‰ªP“oxñkŽ(«7¥â QË~ŒHf ðêµuÌhH¥uÂèZSÉOp¹:ëdEfc\9BgCT΋Z®˜†ð ½lct y„4\pxnŒ€tžmÌQsyGS_BýÙeǵ¥¨7*ábøŽKw¡’ÇØŒYZ‘íú•%ïràjd¦¤J”/+iÕÝòô‘\^Së›tnÄW…òšÞÜãò«à.†ƒãOÞýÔÝtÍ÷'¤ÈlÞ ×Qʯˆ¹•å狲¨Pð êl‡L&´ŠVß´{Ç©ÎaBÊ‚y¯®ÝÂÍ•j²¹¡\YÏõNÓ{ý§»÷¿ÄÕB’Ôü(UÛef›3PgbBN­¬í}¦ÍyI£ºWÛ|^ß~¦·1­J>ÚfÌv¾{Èe ­†ë5Úû:LÇ”ÕËc£±Z[=1š›qÖh¬œõŽ_—6ž’é1mOh{ìDµÞÊù£·?fš›‹!–NãbMvÒõãlï¾Õ€Ã.KIfž(Šãˆ€ÏÝî¿<}þCqroãèépûb)H3z57<.¬Ý/®?ì¼^½õ«Üð +&fÙt+&1Ó©Œ*Mj€‹vFy0{sàg+@¤ý³…¨X¥²+Æà¶Ü:‰Hµk%³]&ÕÁÕ:¡Ö™T;™gû'Ra¥ ¨-DrNâ: ~H0çý$H +@íOøâ"dAå•Ê–^ßÃ.¦µ…üziåa¦{${ÕD¹"Ä3Èkvãý£Rœz=7ºm4ùÜJkíawóqvxîÀ´yŽKe0¨PBy£‰0 §“´ê¹î’ÌÃù‚‰Å‰ZÝ´zÇbyC²»Ãõ;?û“—ÐØT;Ý=)ŽoezÇ…Éíüø–Vß Ñ–ji¥É@_ˆ̘=«y49ÿ¬¾ýtÖ7¢bÞOYËqq>DG¸,“™¤ûwVn~fTÖ´Ê4ÓÛ‰™,ÆÛJyu|󣇟ÿñìÕ­Ý'Tª•-õöî¼ +£¹0®Ïjï¥Û´½jwŽ»[w»éÃŒìà®ZÙÃä&ŸxgÝê”Rÿ¨:9_³—#nD? 䆔՞íH¶ý¾×G«jyÒÚ}Ô?~^ߺW\½­7÷ÉT‹”ó¿ùÛÿrçå7Î\ju?¿ö4»òPÈ­-‡“à;ããíóO¾à)67MïVö_Ö>Q»7}\^Ît¿ûé?휿ZC…Je³´öxûâWõçÞ„~ ä\ÉC \óâ aΕ0Hs6] +è 0Áã0ÞƤ"@«T^ϯޯn?ÝþjåÞ/Aƒ ~./¡íé=9?uŤY‹KÀ̤Œ~cû%Ð: °•Á¯²Z5Dèî˜~ÌÛa¶t4^šL]ÿãÁQÚŽ&X!UctέÔP`á…°ÆRˆq%T¸è±d–4F}§8¾ ?VîÛ£;z먾öjÚ¬nˆEhƒ·ê!Êðbª5œQu9$‰,øðüP09µ¦ú!Z ŠSj­0<ëìÔ9þ„RÊéyi|3œ´q½n´öäêiõóƒ³ÚÚCµº.…6ÚéÖQÙ—«ëDzb 4%µxÙ‹ —i¤Ú'V÷T­ï¢b‘6ê¹Á!›íi‹³ûRa\›œ>üìüÅ/''áFµ5:xòöwéƸ¬ÜàöÁãïý¯³¯¸âvT,.†9RªfÛGŽÙ¼"ÂØ ‘ž„ø/×½ õª+áŠ&clÜ]ˆÍÆÄ2—Ÿ@Ë  †Œê“ÃÃïÄÜsaFC´Z\k@hGû¿K7·–c\ˆIW7žŒò¹‰kÖ Y “F®¶ÞZ½²c³„ÑÂ&nv’¥­de3ÕÛïì=¹ýéOÍíGW½4ÈŠ\Þäs«¨R†ºáe³\n³zí¯oøq‰/lêͳdaS,l,‡ã’˜T’z„MoÝ}wçí6î}Ö=x%×ö|tfÎ7ksñéßeÚ‡`5£l^.L£lBÎnúˆÔ¥Åh !Z›¼^½¶àÿëKËó>³†jçÜ잧:§(_ “¶h£¤}u>8ï&o¸©^Ú‹¨à¯¼ˆYÙÙ8ïíB$çl{ˆm¶Á°Å5iÒ©6—!j‘k\nJeVI³‡rYZÌãBÆü€H¹ ¯Y»ªYÃ1Loµ{d77.-ç|˜3˜T¿8¹SÝ|”ivóÞ³ïƇO}´žjíMNÑ?z[Z}ØÛ{ÙÙÅåWnh9?¡´Z„Òg^š/ j—ÏLsƒ[LºsŠp€BøØ°»q(žF;.ÕX{¶:7`°µò©VX½\Ÿž5·Ö·.ò££•£ýÝ'bv¨–VªÓ{µéæÆÝdeÃÏ($ˆbVr­Ãy?=ç!h³+7’¹©›2Öp!ÀúPK!¤AqÀ½ÖÞG`àÄtkÿÖ‹·¿ùK2ÓwÅ¥kóÙiiroïâ›[oþØØ}¡ëNˆFm*UÖ¤åBä«j)’Œ²9P¢0“õÍn4ÂLz>Hù‹I­öÑÎÅÏ¿û§³·Ö»7—â*e4£B!@¥ˆã + ¾ÊYÖ@]vÄàc•ê®vܨ4ÛêHÈ%s£˜nÊèmßk¯ßJ5¶Ôú‘ž$ô.”±´ÑØù8"TQE.Ïî(:ÿÿî +¸5ஹ°0e{G!\ýðêòW^£›,oSéQ˜Í]÷ò^"ƒˆõyyùºÏ•½ˆîEuÀ¨—÷Îìç2ýc‚Ïåq>b§`vϤÚ!_Ø0{g„Ñô£3»ÞU;“ðŽ'&zbRÕ[üˆêËàK?\Š^^Š±ª¹ñ¬ãº#¶à'—¯¸a¶øÜPÌ÷Íò°>9§µÊ`ÿÙàøeyí¶XbJ!šÌ…˜Yw2Éî¡lÚå܅ܶÝ{P^}ÚÛÿ4:kþ 奬& šÎxÒ‹)a®Èç¦Riƒ´z€­µóÆä4Á§J+twWÏÞî?ýÕѳï~þ÷•é}Þê]<üâå?Ò©F˜M‡“%"5´Z'õ§£ƒ×q¡öá2rjH²"•7Äpe²Û`Ál‹tCŒ%ó \hf*€œÑÈ÷ö»'¯0½åÇÌRÿ¼¾ú$Û:í=Ïæ·Ýˆ¤¤[/¾þ³XZ›ó22CÏxp×:w`õ6 éFÂãÁ:š½tç°½}qòü»éÙ"ÕWò+g>Gù¬7¤üjº¹ÖnŸÐÖd>À: !æ‚„13ó.,@¤ìîIeã>xB«Ç+U]qÇgL€ÐÚyüýÚýï:ÇŸV¶ž¢ZëҊйãÇ¿Nh•+.d1’ô‘pÊà"sý»¹Áýk®ÄÕ¿'B'f÷[å˜èˆËá¨Ú‘J»àNáÓO&ÉÒºÒ8”[7¥†ò¹RgŸP +aRc€Aô&Ÿê‚ë:¢êœ+viÁm9|y)<ï£ ëg»Æå`ÂÄÀ2io”eŒF2?2[ÛVg¯¸zW®lF˃T꣢™omÖ¨Á¯IÙ‘QÚ +‘¶Éd6Ʀ %„N«5\ªA.D¨ ­4¥zÃA²†@ñ ^ + ^Ô N¦µ'åqÆL°¦’o‰È”õÚö½îþÓâà¸ØÞß¼ù2ßÛAå‚^YÏoÑ`›ûvÿ²ëq® †9B‚IŽ„1#:Û:­còTaš+Ì1Jd“ò‚U0&|*ƒa¨2™œÖFǧ¾äí. ìL—g;Îô¬êŽ”Y!•†;&™…a¥wà ±—#Έ$m.5€Œ ô8 5×oMÏav98{°’n56·wŸlÞþ<Õ:‚Üo­œlœ¾rF’!L’&Œ’;̆q…’‹M¥ÁcÔ Òn˜Æef鷺÷ríü «º¾uøèŸÿçÿG/­/G5B*•ƒÂàôö«ßŸ¾ü—[»âÀ&ÃmȆ÷%’Ëì5¹°™L÷Àœ/Y°p SÎXr `*"-†oÜó+ŒÕpÅ8€h'ăŸ€ð"ɨŸMÈ 63bÓ}½ºžë­-³½—›ÜQš\a…Ít›+w6Ï¿ócGLò f˜ÎSrˆ5ˆ ˆ>TÑ_ 2‹!ÎM†1+„j(eùNkíD-Žª#À½3ýýìàHï‘™•¸X1²½ãçÓãOc|n>HzÑ$&–Y«G¨5ޚ͵æZ¡µ…réÙMu2Eh}Êõl:ã2œ 3Â9B ä8(ïb€žu§ҘZfÒ->Ó­Ž¾ûñßþø?þýýGLªi6·ænsã¢<:íOï®î\HvC¶[b¦/¤ûbn“š®¨zÍ]Yš=¡ƒÐòD“‹>æêrüÊbxÑ‹9üÀõ%7î¬Ý[9|¾ãü¤áóA.Ãfû™îI¶w&æÆVÝéY¹Àu/""P½¹<¼ÌóõRkë†#¾àÆPð R9BYK~ê|„4h¥ªÆT‰ÑiHÁjµÍälN]‹6{ŒZí¬žõ7lókûãb”ÐÀúFIÝ,‰dfÞ‘:Âg«-&Ry >aÌ\upçñg`×)s˜ëÝÏ÷ï©ùU»ºa”×P¹6硽^jmƒÅ½²qE•SáÒ“£/G'Ÿsö膗⌺šiûãÂ_Ïy?pÄæC¢7‘ÆĆ^Ùˆ‹ù0gyÒ¥ù¼nøpB¯[Ý€>Öš›zã€Ð[öleôÄÇéÉÒ¨vwóÑÖ/Ï>úý½7Ú}ð-©Wi­ŠÉ•Ó¼1ivw(*\÷ a:EÈUð Åþ‰’@,-xIœÏK©–3HÍ-Ç‚¨Dd0 6³àÅ—¼ w~ÁvEhБ9v‚×Jœ’ã2©”òôÁ'¿Û½ól*”9³šžíœR ¡U2™vÇ“ z‰d «uµ­7Ðdɤ9-oæ:àp~vÍ à2ç‹+ó|ÑG,…hï¬T*sÎØr£&4 H.\¯Çå"I£ ©—žJ¹ÖÞ½7“ã*£ÓÎƽÑñG½§Vû€Òk¹Öf2Óö"BŒ2Ü1´Ãánjͨ59;?Vß +Á™zÐ`B"”J¦¸óà‹“—¿mmÜøêWßý¤TשŒÉõ„TeS½ºWZ}ÖÜ«ÔwhµRŸÜÎÎäÒ*`KdÖíÖY9»Žð¥Ÿ/†?\_óQ¨1 à¥å™íˆªPJ-Û=LH¹0©Úõµ£{¯?ûÍß?ù<=8zÿ´½yþéêÙk>ÛŽp¦RZëlÜ/Ž´Â(לòzÑ&l—«LuDx°‹î¨âǬe£|®û•…Ð’óF8Þlɹ)¡uQ±jäûëGRÕ ¦dõÆúàäãõ»ŸÖsùU/ªj›gϾ‹&Ó A‡ˆ ˆQ&£³® Ã-QoL6ïnÞüøòBèÒœ#!åò&›À¥Q+w÷6n¾^Ž@Sª ©ˆKE„¶”l—‹K~œfÕeÝ.€(Ë>ŠXS#¸6·ã¥¼1"­ØÝZ=Tyj¶+“ÛP©((Ç/gQ= !ÁN†© “rù†;)!´"…gÏ(³(myB䲟w¡„¡äÂϯ¹?¸êø›+Ž+ó¾Ä4ѨÃ!Õ{©òŠ;H&ªŸÐH£ÁåÆjmÛì×7ŸÕVo¯oœþóù?÷î½f²;{Fc(U*m'´šWi1¿~ð"Ìf®/\AO˜pÍv·ÁÁHÈé6gT@÷ã|Ž6ÚreZ]½ÙÞ¼[îï>ýä›o~-—F¸ÞH·o¦šGÕ•‹Õ³/ûûw·ŸJ…U»<=¼ûšM—½¤æ²ÑY?±¦˜›röJ€L;ã + NÛ‹qm)®]ñóA8謒ǥNoÝzÝY?ç3mµ:kçRY{<8|³wÿ›ó·ª¬Ý«ONN¦ÖväÂXÌ Hµ†Ì¶b¯rìäÚRˆôãZ˜¶…Ôª”ÝÀ1YŒ9f2Jc)@_q"`2£lD9jÈfß›ð1iVq¥cæ +|v<9|Ùß|Péî•{[¨”eŒ’žøf{ è~,剫Á„Epùzï°½rëÊb4˜£”æ™ù“aõp³ƒ$K½õ;ÃÍ{P'²ÔBOÎõyk¶‡—˜ëcJãó•öfµ·ç ŽL¨ Ö¡ &‡tI#×ÉTƾ(K‹ÙÎæùÞŻ݇_«ƒ„Ö‰pÅ£çÿoÿ[¡ºæ +óàîH©Â›>5a¬¡¶©“´ZíGFiìQN/á ³QLºbV1!ÆMZ®-x‰¿‚èZŠ»"É8abrŒHaB!U^}öîwî8ˆ9Ò¨sù±Úܱû'¹îÎúîÏ~ó­µ[€ÃõջͭÇfsW)o‘ÖÐ3³IÓî|òå?2™Þ__[¼²àH\ôQ®ˆ…‘ ½ÁöéãÏ„lÊnµ¨TC«NìÎVcó¼89L¨E³±Qßæ3# h´¼IM)7䬞Q\«ŽŽy»lÕ‡€9><¢SÎv¢êrT$”oÃ\>.–cò%'êD” ›KæW ­Éjµ“—µs1×ÓÛ¥Õû­gÓӷǾî>5š[ãÝ¿ùËMWƒl†²:lª ÆR.n½ß•{ è ^mr{¶gÊû¶Àþ÷A=q ë´Þ]²K! +ôblöôy¶Mö vmýa2ÛçõÊ`ív_­¶§ç½­»lªå#4$™‡¨¬À¦+*ÉñtK…bB¥ôæ=$Tûdª±¦çƒŸ[mî¾ÈŽnIÅÕ0ŸÑ6d„QÆ…lBª€±ú¬—7g]ܹY y¢”o”óDXx'ŒëÎ ÐA¼_¹†°é¤ÕL¦›FuuxòjýÖ»Í[¿Ø½ûùÞýÏú«8—Š’!c¤LÌölJ$+1¨QBA/¬à‹r=wŠ&ôe{C,BšP¾QvÉ‹RÝ(ïõ$xkÎK^]Bý1ÙUɪ˜]aͶ‘ÆY Òàó­ööààéÎýw;÷¿èž¼ŠŠY(¹ýýçtf€ª5L­#R…P´ +rÐéožûHõç×\ž ÆS¾¸ê‰ >DÁ§7†;w_bzÉǤŒöXZ±ZÛÕí‹ôÊ2ÝåÍÚùŻͳ—!ÒŒ1RmÕáΓéáG©ú¼ÜYÿö‡¨¯=ø««¾¥ éÃÕ“3+õ•ûrq#HÛço”ÒÊ%g|!Ä™lT(©YÍJwëøñøðI5Y»©×VÕÒË´ÊããÇ_œ~üÜjgº‡¹Ñ9¦µÂL&êÃdt"-Û>Èuü„zÕ…p kï7Z-ðô ?c¶¹zÅqÅY„Ï(•M³{3?¾Sß°¹\uíÝwÿpüô;h©00Z[jcS.M¡nGh+ˆ«I«Ûœ>¤´öÂ{ß€ W–„+£¬Ýzòæ{rvs,%ä&lv*|v%Û?[»ó¥ZžLvî_|òCsíŽ/a ã¨ìl*.'gÏûB „Ï—Ü@pQqÎK»-Èæ•òNyx+]]lÜ^¿óéáó_<ÿåöƒÏåòÔGš”V- o67`b>iwP© +ZŒpYg8é +%½QÉæã¸Vjî(vÿƒ«žy'D$” D“„銩¤Rß:êÿ?\ñÏ{iðÉBvE-o†™ü‡ó1/*†#̘’çó=°î£³·õLaÉôæNqeÖüÞû¹@ûéö $~$™]ŒÎú÷úbRUœAæòlûªèRŠ0V˜K{HÝ™„\7Yê#j6ÕÞî¿ÊϋÛõµ{•ÉY2Óÿ-`i1F MÁNÌn¤›µöøðîó¯äüèòBå³ð—ê‚ú©L˜- bÁnnáZåš;™îÅÍ(—ä‡è…Ú_n­îž>ìlžFh#7¼99ûÅîÅwÛw>ì?îì\èõõtiüíÿã“oÿÑC¦b\®ºzåä]ixQ>‹ë~ÒM—GœÙpDÅ+.úª›^Ž¨~"—›éÆö½Wß=~÷»8—rÄE/eF’EÖîW'gõÕ‹ÒôI\kÆ9ÛîíC­Ž‰…Ùr{±¤¬nn>™mç¢\6!×Q¾&,PX`€ôŸÊ¶ÖÃtÚ‡jœÝ+ŒÏ›6Î_žÏ\šåÇ}õ§ý¯ÍÉ‘#*SZ‡Ö;ŒÙqoBÃÉêu,™[’ž„ÌZ+*Õ+ÌÚò|™ïl\üæÇZ;~ºröÉÚƒ¯ë×v?Z¹õÅÖí·;·ßG7Ëýýú÷ÿûõ·§-)ÕI¦G!̤DÐñ‰’îCݸ²Œñ©ŽW®9bWC\ñ\[ŒH†¹²_ãíA¾³Ÿ´»¤Ùâò+LfÌå§beÛ‹)lºi··!#„\/?>mí?Ÿ½¿ëž¼Í¬ÜO–×íêæùï÷î}F˜ Òìäg³JN¹Â“Þ˜mï`®»P÷ìI·à']1!H¥¢ÉY绳›êl•G{Û÷>ݾÿîæóoVO_&ç¤=ˆ%‹Ôû{°QÚôÆgóÍ«_Ÿ>èl?K·£éF¤èi8¢¬xSm0é‰\>H·NøL9.ºâÉ„˜¨qÆ8`@1?Ê´wJƒ}øW. )[7ÖïÖVÏSÍÝlÿ(Û;LféÒðøþ'Ó“G6ø–ÍZÙV¨Ô`¶ÓhŠÚÍûo‹]WL Ó…(W…v®µõàÕw½[€E„ZS»Vÿf{çùÿðìÛŸ}ÊõÖ`ïæŧ´V†Ëwo¶v>é~VZy®”ÖÁEIýÎã/ʃÓkr6"DùâLX;G¥é"[££ÁæøÍ„X¢R]Æä:û •é}«¶†0ßÜL¦»ónÎM̨\i +¹Uµ¸À g„ìnie¦é„\Á• +iÖ"¬)gº¥þ¾jtw>ÿueý~¦»Ï¦Ûq.CHÀ™ãÇ_½ûá_WŸ'ÄŠœß Ô–?¡âÜl†ƒ »¾œXðrZiÃavÅu݉.ûioD “RïF!G¨´l÷áÓ€Ëë÷7~wôòÇöÁ«„X¬NÎ7o½»óñïÇ»ï}òýèäEº¿Û9|^ÞyfôÏôÚîæÉë;ý0hî<œµ'êŸVVŸtw_çga6eg•!œÄŠ\Ùæ [ReŸ¶' ±¢,gœOH¥„ÖôaI 4 ƒVó>'‹€äQ(ž%Õ¢Qßlm=~.⟳êÍ;`ar6ÕÙŸ¼>}ñÃöÅ7©Þ¡T"¢E«¹ÕÃgÉL#–´èt—0Ú0”Z!„|©½eGaÒ  šm΢2!ʶj›¥Á!èÈ•ÅHBÈàð™^2Ó)ŽÆ'/íþqŒÏî>|óõOj¶•²Íõ'µµ'…þ¹ZÚq!ÒbB(ëâÙ×ÍñéÙ&³&*ÕÄšݽY]\œÜ£¤Â“WßÜ}þ @ø™¿¹ýúG/2º§!:ƒ²Ö÷ü—Óç.T½îÂ=¨.·Íæ ©uæûÍÅçÙ|ðKЋO¾û·ÉÍOÅü¤ÜÚÛ¿÷…^]u ¢Ÿ°x³Ã(à\O”S†]ôÀùëUš„šÀÙÝÜø¬´~|òqÿè™\®Ÿ>oï=ÎŒÏìñ¹Rße²£Ù¬æâ”KilÑ«¥c ïFŒ¾‡ÊÅ•­ý„V§2=*;ÆŒ“êz›Pë̶BJELÊ!R:×?Y9ÿ|rëóöÞGÕµûÙÔ·žUoÝÿTonY­ñ­Ï2“»ByMÈ €X7ŽŸÆXÝ2c¹¸Fk %?ŽÒ"ábN/ŒÁ)}8sa\cŒjqp*åW‚DÊIšÅqŒ1ÜQÒr@òo~ûðõ«'/s­)Û  Bë S³è|¤;Êãr©<8.÷Òµug„½²qFø(Û–20h )‡ó©jwSÎÔnxbrf0¾ùjëáWýƒ§©ö¦¶g7ÔâÛïþ0Ù}àJ@åIµ¦³î8°§Ä”—®ö¥[ù½qÁQ„ÜŠŸ’FÇ*^~þÓ7?0j-‘,÷÷>©o<ÍõRµu ƒWt†ù&¶VïAÖÜðàpŠ9 Ô:.䢴}y1æ 3ý•ã_þøŸ—<øÕŨ3$Äù¼˜[µª»éò”SÃísÞªgí¹†L~eµÊø¨±y¿µu¿¾vËîl+…AmrsýîÛµóç­Ís(žVs›Ï¯`jÍ( {wÄLo6•EŸùy2 T"9‚TWàgF-»" Ð +&åµÊttüêø£ßoÜý¬»{±±ç?ÿûüé÷â¬ùÕNqz‘îÞ,ŒïLÏß½üíäæ'é­g_þ­P^÷1­¹_œ>¨l¼Ø~ôÛíÇß÷o¾n­ÜúÛýß¿ùÛ²Yj8Í…é©*7Óí£úèäÑ«¯¶î¾nî>î½hî>´ÝýGãÃç'O¿yð‹A k“Óûo|öõßí?úº½ûÌlîÐÖln'g¯øËã£l6Ìæ *r©\˜PF“Nü„ÍšÝ0­;¢x3.óþb¥û¨P À×`硘ïÙõq}¸›ëîËåmÂèãJ‹”ª1ʘ¿‹4„D„4àO¼ˆ2Û8 ’ÄÅrœ¶áM\,€*J â[ÊÁ•-„(?&£B:Dðq^KÚu£²Òݾ˜¿”rƒ„9[Óñ~Æ@å8¯RUJ«1Æö£€i¢Õ½1ÉáB˜ SåÉÙää£ÖÖ…ÑØE•š+&*©¶UZAùk4B‰”JvDÊe?®&fKhG cƵ%äêÈ•ù#À"´•`LZ-ƒç¿t=à‹òŽ ýáBèªY±Î¨à ³¬^Nv"™Iµ÷ÒcN±ó`‡ÆG­Í{Å•;ZuCH5­Ò(×ÛaÌ¥šR®Å§[™îq~tXχ©Ãbz ¤:ó^bÙOÁ)cÉ‚/.BŒÍ»ã@Ä ‰d–·Ûfk·ºrk¼ÿxëøâþ«/W/k·÷}»vûË“gßïÜýEet¬æ»Fi\ì=ùL©L­ænÿàÅúùk€üðX¯M“…n¾1}úæ7w?ùU˜ËjÕ±¸nµŽ¶/~µyñ«âàx²qöÿú¿^¼ûµV›†G`·{ïú»_ýå¿ýôŸþ¯¯þüï·ž|õãŸþã›_ÿÅìì›í}¥¶›Ü½=yñÓôö·¤ÙãõZ¿¾åà /¦¡R!&–¤Ú^{÷c£±“sé1o6¥ÜD«oÅ1˜IÎ6×/ŸüšIU•\K-i{ÐXÜÙûDkœÄ“ pþRi“Ï­ø²hU>ÚÄÓï7gI-G’¤Z/ ÏX³ L\o?©m<K«B~ÌÚí0=ëBåŒ žÄÄ4cTØT‹K·¤|Ÿ1 د³WÅþ>F.ÝP–7*D@MRýaÀ¥³½ “P:¡U¥Êf~xËL(­Î)%N-abAÎö}ˆ²èg ©¤äGŒÙJ"­kË(Di‚ÏÅYà"F§!Šðlf`\&’yøÏå sÃK,„x‘Š ùÙŽ9jY+Kã“ŸÏ–:™æ”1+fuE­l%i¥µlkÏnïÐZV0sR¾…IY¹¸ªÖw ÁçÐj3UÚLW7œÀþq1N›aBõÆÅ®øÕ¥0@DœJ³z amÚj6§·÷€|~VY=f¬’QŒŸÙ­õBg=Ó\£–ÌJ™’¥ .sÝÝÊäDÈ´I£O‚ÒåÄlÓ(t M£:ñájapl4¶´Êú`÷qkz–-w‡ãÍWï¾ÜQríÑî½;¯ýøËß¿øöOO¿þéÑ¿=zöÙêáço¿>{öõhçÑùGß÷ß OÞ­Ÿ¿í?+ o&¤Bº8ü蛿=¿î%C ©×„ÜÄÞ«o>¥ôª —Þ~ý“’éúq3ÄçôæNargýö—»è| +6æüÁëO¿ûscýªTùü3{¸5jo>;xú»êÆ£8Ÿyøê›úÊ©#.V»=Ê—pµÝ˜>‹.gƒµÛ+'C –WÎØL/*˜TGÈK«çdªíÇD9Û?Éšu¸Q.…)¹8îí¿xôí¿´·Bškå50‡îð¬q„QÀ¢Dà&gŒÙäôJº6…Ó©¬=‘«»¸ÖŠ±ÀÔ}(Îbv5€™€ Fe5ÕÜdS]ˆ*-€…^B(­‰g$ ÄæpÎCÎû¨å· àæ@¯,#³u>ꪛ0©4Õ;Jm]*®$scf)V—³®R´YO·ö³ƒs»Æeû‚UÍÔFr±$d½´Zž^Æw+«h£‚³ˆrÎiØ+ßóŸ®èœ3ºä§€…I©Ž'+Œ\m¬œ%äÿü÷ÿø¿0z·†Rm_,ï¨õÃ퇿9~õ·åÉEœÎ"øþF”ÔQÖŒÓA«ÐÉ,!Ø„†KeV«£\lR)À ìV¦4ìNë£CBŸ=Íìî<©¬ÞJ5V…tM0‹ZºhØED*/YV­ +V‹TJ— +ãrMòJɪ®sÙ‰'a¤šûv÷H-®ôw®œ¾,­œa‚]iŒï¼Ê5·0¡ií¤š[jiÂÛM(e(gGI“ŸõõÊ/ø(BGŪ^ÞN5¤ÌˆVŠ²‘¯u×»ë§ð ÖàÔìV6ŸU×eÚ{Pù ¹„QòÖѣͳ—à»À +*`ªÅÙmؤÙr‡7¨iªõñ»ß½ùúG?¦^q$ò›Ì%ŒžÝ;³Û‡Zºöë?þÓÇ_þÞ`®.ÆÜQ Ìvx¿uü)Dˆ?ʲRÖ®ŒP6E— Fu¼_{ãNet„É…¤š_Û»_ìùq-B¥øtTk”RŒ`‚'„;}±Hœ$5GÂ(I«¥Âê=iÖÁ{4Ú”hOÖO÷ob56„\ÏlløØ$d’·­ˆqÁx2Áf¾À*5½¸ +\ÀB$K²Ù’ôZ«¿?ÙdR1ÞŽóvÓ<~vÌ¢om¸#<€ð¢—\ö3q&M_¨eRÉC`rV%ʨFy\ŸZÍ¥¸ÞZ{`Õ¶bŒÕWú{TŒPf”+øð<Â7k‡ä ¡ªYÔ³=gœ¤ê›Q˜"lL,‡É48ayæLZ¸\ÕJS³0&Ä<ª¦³ÙSày’7¬‘à,"iƒgâÆbà=ఆ˟ð„ho„ƒ/¢…Ù>ŒÕAŲríÍTk=DÈ„˜IÚMF+‹VÝÈ4-ï2¼ZÌÕ'Œ ¡4ÂØRºÅ58_LÇ"fS¹ÀÀ;1L• #fc¤!€«n%3}\*âòlE$‘,°jYI×qÆð†©g,^M_[ŠÞp žNЗ42ù^¥½>sì>:Ê”0­…Jå8—Á“:)ÈÕk]u¢³uaå­ÖÖãáÉ›ÞÁó ª ´Iðé8%£”§Á°i¸hsVÍ(ŒÒˆR +²Q”­Á[AT&“8_È8„³\A%•X‚Gq6)kI±¢J$-£6¦¬z”³#”®šå½£gßJv RÕnìÖWZõßaÉ`”†“Šb +˜Q\Ž*ÆÛ„\áÓCF©#„®›Õ³³gÏ>ûC„³ü¸c­®ÇÁô2ð2pu)âŠX²J*m„/…‰4°Xª¶Aku0*16U7D¦¢\Œ1Æ(e,¹PŒ‹;û±y +†$΀ã062šÃ‹h©V¶¼†PV Sc¤‰ñ98¤P" þÓ,÷]aª(§»e)‡1z¡ƒœï[ëúƒ‘H M0:&äp1¡ w„£ëð“D¼2\ö`qÂðG…(\/Úˆ'd„‹áJwã–dÕCOŠù—Kp"™ƒ\rÇ!·ÇisÑ_ô$ =A§Í|ÛÈ6%½ŠRi€Á¿¹æ½²¹¾[š-ƒ%]!Š×2€(£.û—‚ŒV3} LmUÌô\aÚg¼è¥®`3Zˆ)]ëmd«J¶0F¡AÄã ´ ƒ2z®²RÞº ½ñEWxÉÆHžåU=S¡%Ë,6h³èK$y5ßn—êø!ÃTi¹6x”M/ú)\'¤¤^UÒ-„Ñ=aÒ¥ƒLÈ® ‹–™iÖ»¥îÉkZ¦ +¹ìGøC„P //D–Ö«&×—òZÂÖÑNYÝ].>ypï¼{ï·÷åŸýüã/¿÷xs£š²XFäP>3Þ<KÑl2c§us±›ï¶ I•·Ó&Å N_Ðç(’X«—ÛKíµí­G¯©ÐxÑØ‚™œx½b8³ŠçgƒgÎLÄQÄJ¥*•r.ŸGÁðxi^®×ÊWnÜlöŠi®ïm™…|%ÜœVmLJ«™ʪa(¶e‹ÕTR˥Ĝ†í­×ŽV[ùzN½4,>oï•çß}ýök/^¹wëh8lYi3FAÔãÇQÂMÆIC1žÁJ6ß©ê­Š>êZ'ëÙ‡—o=ØúÚ›W>xýÊ~ùÝ_ÿôÛo¾xz¸·ÚlVEA +Gñ@䎊³ɦ¢Pšq$¯Ëµ¬^0ø~Ý\–:ÅíµÅ»7¯®ß¿³ûúãë/¿ðÜÕã«ÍzÓL›¼Öí¹µ×®¾÷dë÷?üÂÿù›¾ó•›Ï•³IÌåqϹÂ%㔈D6hèQO¸º¢>¹R}ÿ¥Kï<¿úÖƒ¥÷_ÞüÅw_üçß|ð‹ï<øõwžý矽ý?ÿÛoþö£'¯?ܹvØÍfíhŒ EX¬Œ©ÔòâZÇ8ZN/•ðåÝÊÓ«={}Û\ÊÞ<è|é•“ßýæßüòWßÿ½—?8Üß[‰žw9ݸ'f½O½èw.è\Ä‘ŒÓY·Îù:%eÔ¶ 5—­7_~ãåOý{?üô«¯¾ò \µ†«Ë‹›§ÃÃ'\ªò¹&–ÐFŠ¸¼d¼xµ÷ð¸y¼¤¾|³ûóO_ÿÙ§o½ûÂÎW¯ÿÃßú—úäÏ¿ûÖ¿qûÏ¿|ëÇ={²‘S™pÐp8C±8 J"^ƒõJôýýâß|åÆϾ÷ÚÇ_¹óÅG£?¿õw߸ý‡¿õ“Ÿýñ×®ýù×oÿñ‡ÏåQçáI}oµš±´l6+kY%Yd6%F[éøFM¸»SxãÙµ—®¶ž\k|÷ý;¿ùÑ»þãßþîüúÓ'ÿýïÿÿÏÿðËo=¹Þýøíã_|úê;O.§ ÕáŠ^˜ ¹‚Á(Á€/pÇCN÷ÉØü°ÄÜØ*^ß©ŽÌ«™Ç7‡Ÿ|ðÒ{¯]yïÍg_ée¦ý16ŠŠ,ŽL?G綌÷î/ýÍo~òÅ?ýðÅÿ“wÿß?ÿôw?ÿ*ŒÀþíËþí7¾õÅ{»+•´!1¬¨š$d +©Äj »2àïn¯ž”¾óæþO?~ñÓ÷oôöå}pû?üèÍÿþï¾ñŸûþo¿ÿâ?|úà×ß}øê½ÍRZf9 Äé ‡'­¨«+yö•ó[å/ßk~òÆößûîûÝ—ÿø³·ðõ›¿ýî³úéküá“õô'_;þåG7¾ÿå“ÛÕjÑöB…I4Š­[‹Ù–v{+ýõ'«÷µk¿±óá{¿ÿñ~õéËóîµû¥ãÿû?ü_ÿ×Ïþôë/ÿþÓçÿëï¾ü½÷oí Ò(â¾83ëòG£QReÙAIná¦æ»1ädd^¹Ù|ÿ•íO¿rúÑ[»?ýøáùÇOþáoþáGoÿËï?úñ·mµ£e»:âjÚ´K–ÒÉ0» +^þþƒÁ/¾uçï¿ûð{_:þÎ;—üáƒÿüÛ¯þò“GŸ¾{ü³ïþ§_}é·?ûñãîûÏÕ.”BŠghnfÖ{æ™™Ëe0þ~9]”îlÙO®·Þ}´ùÓo?þÓ¯Þý÷?yãøùüÑë?ûÖÝŸ}ýêß~aëÝ{ë«Év†JÉ‹2‚Ž1ɤYí´{ëýR¿Àòøõ!ÿå‡Kßýâé?¸õƒ¯_ûåwïÿãOÞø§Ÿ¾ý«OþêƒÓúþýÿôóW÷ý~囅ǧÍJN„i@b\H R¢VLU-ªm'£äýÝÔ“#û­[õßÚûÝß>ùÕ'Ïýâ£{ÿã?|÷üÇ¿ýÎÛGßùÒíŸ;$p:„0IÈ^™æ#- _«ÐW‡Ú˧­^?øôÝ?úêíùõWÿ×ýÅôÆß½ý+/î^Z­¦ QLµ‚¢ [ãÙ¬[.ËWVÊÏÔ—¾ÿ…ÃøÞKøñW¿öêÁ×^\ùÞÛ{?ùàö‡o]~x¥s´Þ¨Wë²QdÕD$ÈbcG1ó¥t:#]›:Ú'Ë™[™/Þëýà½ë¿üäɧ_¹õ·ß¼÷ú¥×NÛÏî–7“µ í S´TÑÌ6C ¹T"“ ó*±RU5?zmç‡ïþæ»þþû/ÿáoüé—ïÿü£û¿øÆõ?ÿìõùÅÛ¿ùæé'/7Ÿœ”† C.L5ê™ü@”L•ÁkI²¡Å–­ðÝUåµëõ7ow¾ñÊ¥ÿòï¾úçß¾÷û¾òÏ¿|ç_~ÿ­Ÿ~ûÑ·Þ<|÷ñöê ¯hIœKŨd©Ò“x>%S[ÝL/ƒ²ÈQ‡{¸[|ýÖðÉÕÅûûµ¯Üýêã‡?ýðÑǯ~ý…õ7o ¯­ä—+ªÎ¡4% DHfr&ˆ¡tÒß6Éí®}¸RÝonÕ_‚„§úb +ʤ*å ÐH”*¦³Íœm±Á¶;èé×–­»›™OÞ8üÏÿÿøÛ¯ýìãGßýÒíWï®ßÜo®ô2 ]嵂j/¢\åŒÊq¼Þ(7Ó +—×èŠ%äU¦œû%«—Ó‡9þêJáÙÃáÉJµ•bKºlʲ@s(Ê8ì´‹š˜‰M.@fÊq‡X4féZ!c—3™ªmfTÖé¬Ê—MI£¢*M‰RµÚ‡„‰ !`ƒ¸˜. g!`º|x(Lð¬\ÈV—G{˽•ZJY«&oo×·kòµÕòÑRa«¦m×õ•rr¹l–5Á;;ít1•jA,91øÜ™¹éYOÄRhÚV„š)ç¤X?ǯWå«ö›wW¾ðÜæ ǃÃAÝdh +¥”dY-oGw1.ñ€#8–OHr‚ã$YNÔª•bÖÈYrÎu…Q^TqV +PÒŒ?>éDüq•Nc‚5 ½ˆD –<þ-¯œ-Ôcá !0ˆbù‚¦¦XZ`hÞŒùC¤?B{Âô¬Ÿvã!:“¬ìõK˜”çõÎê(ÆÉj&c¡MDÑèt6úýíVkÅHZ½ ÕdÅÆÏL;æ‚T€ÎÆÕ6£·Äd£õÉiçô¬;ŽÃÓ¬D"£%ŒLJ¯RËÝÜéfýR?}²V¹²¹¸Þ.ö +ÉNÞ*Y) rfrÁßÛ~Á{jÊ;é!c ñf±±“+öYšNXÉàr2–c»½â“;'/Ý¿qew}µÕ0U‰"ñ(Šcb:Hš258{©0¸Yè]ÏVY.9m_¹ñB å !‚Tt=ŸÍÖ‹¥¶m•8F øˆpTž™G&¦BÓ.Ò‡Y.DwFÔaÒrÞë‰F‚H „Í8csÈÜÂ4¦…³‚ NW ‚ ÈzÃ)ØQ2g³lª+çFΨü™ —ÓO#x£ %ÕàôJ¥b( •V¯4Þ@RµÓvC•SBÅ1%ÔyzvÒ"Rq.?ç!ÿõg§?óôìÔ|4—UÕLh™XŒŠ!O º¤ÒHD¢2—:!%rÎéDgÜoõíÅ«Jn$eF•Ñ©œkáLÁꤛûFm+Ƨ"„((éFcÚœ Î8<ÙòkBf…HÔcbVJåÊlÏDÕ?¾)#ÂåÜ1-‚'ã¤æðÆÎN{ Ú¦¸Xà)?axQúË‚Û³!~6$͇¥“Ì–’銩Z2×!¥ <Õª!ÊŒ°BkÐz“5R¶/ÚZÍÒz5Èd=˜îÓž¸f2!*íŒJ!"ò&§—)%Çu½0Ld{rªÇy’•$ÍfÕü¤Óïð£ALg¾TÞñ…Ùun.üôL`.,†¹2kö]ˆ8ã#f¼ã5ù16 á:ÁDAËVú1FóããÛ“q±$ÓÒr¡Ê33ã寃„Îèu«¾©Mšr#²×ÔüH+¬L»WHˆqiZ«²V×O¦H‡´Ö‚‘Y Ó&&TìÖ‘˜1©^ˆÍMù—åD›S2ù1µ.—7µænvñ²\Z°YGTŠ1f¥LˆEøh9;ÒË;fý@­ìNEÏ8ð3 ¨;6¾i诞™»°£S‹Zërªu(fW\õìdÐüÂg'ü h˜Ësée­| Ú+.Ô˜¨çļÄ9+WÏMBÀY¸0tø¨iˆz•²3ó‡;‚Ä¥(>þ=Ë‘æ‚ÂBTñR¶7?óô‚TÞŠ*%\É­^º[Y> ŠYT«FÄeÔX³%gÕ•k\v¡R\) +ùAH,ÀkLŽ²–Šýk{Ï~Ùììó’NÌb²›Jí˜J¯ RÅ‹i ãÛÒSä_~ PVˆ-ÐæHÈ­S©›‚!­8c nøÉ*×øÌR¢¼Ê˜0—QrKvkÒªœÑŠ'ªdz`t¯$ZGJu[È.‘Z¥¶vLv¼˜Jãí´¸ìK¶CR%"WDÀ–åÃÃG!>sÑK8%®Ô|„ +ˆRõ“¦œjÖ'Œµ8^KÜè…¤zTéÆŠM¨´;®Íé¹ éCeF«Q©QL¬»B¼'Ìsz×>Ò„ãO6÷`ŽbZ7¦uüP丩Uè5?¦JÙ/²æ²ñD="Õ¦£Ú„“Œñ¶+ÊM¹QOLöSiRkaJ Wap +¨T%´¦`çB¢°‚L1"ÔLUÆwÙL8± Ž8oŽ²œ³®˜Œ$j‘D#ªÔ9{@­— Ò´öL4áÀŒ€P–ªûtv]ªìJÅKçˆi/‹ÉUR«¹±dÊy±¬'nƒ8GØœ7žœt“Q±(e—¥Ì²7§¼ÔBTCµ‘ZSmLº0|ö‚oÚ‰z"b²]ˆæà OL‰P©9gd +WÌùÜ*]¡2+Q©zn671½•,­ì^{-×ßwQɘ ÀüW ;Æ˹§ÇÛTVÕÒÈ+“~!nÄÜÒíÜêsæâÕèxÙXH¶¹Ü:—ߌª­_‚ñ *[|näÂ’NTc¬%µ~"–öc¼Y€¶áÈÔ¢ŸN¹Q•H6Åü(Õ:°;ÇVë@«låÕË ³K6ÍÁõÆ£—Z{/Zý+…Ñ-Îè˜ùáàð¡‹Lù,”—ßJ¶Nr£;Ùádë Èï9¥Í®µ£rê“°dªÓY½¹ròi´Îº1'‘R+—Ò‹·’­›¨Ö÷3t ›E¥¢•~:šÀR¾´g.ÞüìdèÂBÔë<å‰[~" sNÔôP9!¿©7"Òìxõ-#ÄfÃB)Q? s…(Ÿ)U<5`2«¹ÁíÆÞ‹q­’(Á鲋GPTÉú—é1V›²ÚùÑ5¥¼N¦Úñd[.íØý[öà–TÜDÔú\\ ð!?B”šhÖ´×£‰. xeõNit +º-V@Ç{ Ó»LvU(o°V»¿}kõô%>·.¯2K…囵ý'BeGÍ×O_þâw £#cÅ“‹Vïziõ~mó¡Ý; qÖ:¹•J>ÖŒ$Ê.Æ&¬Åúæ󋇯e—®“VŸ2:ÅÑ-Ìè„Ä]Í oÉ•]*=JT/É¥µ³.d>J$ʽ:òC/[ Ë-6=¾Þ~åèÅÕãÇç¡mÃ"¡u+£ûý×ÄÚiLïáJåÎKfZ{î¸Aèíîîó·_ýÖíÏ´xú*›_ó39ÁZ|éíO¤ìð¢_ð3E.·ìÜ(o<^~#;¸‰ÊÅæêÕtcs2H»H5jójwÿóµõ{õ»tª+VûЙn,E[&·†›#P¿t÷jsãäÞì­Ú^¦Ò}ÚZDÄ™¨¯=<~ôÇ›WK;jy‹Ï¯ z‘ëA¶@hÂè&üû„›sDu!¿¬Ö¶SíÃLÿʯ3Kk»³A~ÂI 0kÅ}2=¢ÁÝÔvDj^ðòFnéê½·¤Të_=5wÑ<“K{¥õ‡ë7¿^ãˆ(ÇÏ~IÉ/;1Ý…§bÆÐOgüt¼I©ì–lÜxoxòyÁl_:º_’Éš`wÕÒZ¾¹4ºVZ½©·÷¨d=Û¾ŸŠ§šÐÚ`‚`¬Ð&bq#½xŠªÕˆT³3;×s+Ï·ž·F7ñTKÏVŽ_Òjk~Î k©öåÅÝG{·ÞÜ}ö+õÍ{‰âJcxR]>åí~ª¹WZ½“jg÷žß¸ù†ÑX©Žº[øü*ÔmT­†„|P\ëI¥"Ùá¬Vcý:iv¼tzœ5Š[(¶½\Z®sø +Ô¤XXO4ºú§Ÿ7—nÇŒ¾ƒL» “Ï,Úý£ù8ËØ­˜ÞÆÍ!˜Þ< 3KÀœÊoI¹êãG7ˉA6›(®•Wï–Öî×nåWïø™üé½·7n†)•òù•[­Ý'íG…•ç*[O°dÊ/aJÕKriCªló¥xj<8à@ç]¬Ÿ°¡³Pµ5áŒ]ð`.2@p½Ç¤ú¢5p„ŧ'ýÀ„ÐS1µ«1¹áˆAž²V_K–×]q-Àf |d¨;,1­%—¶¬öô2©”†ë§f}5ļÙÉtŽ—žŒŽ_)._—ŠËt² +Á°²~g!*8‹Íô­öiiåAºÝêê]6ÕI·.A…ˆ…Õd÷Të^¦¡}´¦Ý<êì¿$–Vƒ¼]^³Zû͵ë—n¼ÒÛ¤”V9£Ù_»Ñ\¿Nh5«¾µ{ë­­›_\~¥{øRvx7KG×ï^{ÕO›ÒŠë-Æ@ $Û'|qM´¬êúÉ£wåE£±/”v¤ÚAvùÎÆw{ûO´âÆæé+ÍK÷ãfSiîÒÅÔ\抻zójnx‡0û”V>¹ÿNkãÞønG6M´½ƒ¥º1¹šiOV÷ï´6oGµ.>Þ–zÉ -ÉeAi›;ƲÉÛF}ÃêéÍKÙÁ•DmH àÇìB•îz) 7šÉö¡Õ¿a n×_H÷oòÙ0 £{on]ök\7Ømìeû×’õ]9?‚ËÅÞνþÎ=½ j¿Äf–Ôâj¦µ½}õÕåý‡î¸Î[‹­Í»ÝÃÇR}o|û=¤R‹{×_±«+¾˜<¾ãupœî¨…1·†™£ù¸5”™d3@¦ªëwVn}±qùU}ñjªwbtŽqse: ²ÉNkå¡Ö}q1Lžx*&ÔØp­ãìsó¨7®Sz×OÙçÝŒ3–"õ¡ÄÂ.„S0}Ú¡dr,—ËãäÉK[Q¹ÌAX«nIù–lø RK€Rjy-ÕÚ¥Í.$8°¿±Oç–Ï- áPZ…áüð“EäÒ0¹Tª.]eSM\.”Wo´vÁø÷_Y<ú|qýY"Ý °Vuí&"ÒXœÝÁ ÐœE³¾eµ.F#ÊY‰lW/´3­5½¶Ý%[t²dÕÖ¤l*@á‰:¼ÄǦ¼¬Áæ–”ÊN¢º«W·ÔòÈOé¼Q?¸ûÅåëoɵK‰Ú%TÅî±z}ië(@D,eGwÒƒ»Zë$µt-ìBÍE$3¿´sí%»½óôÊ㜾xU©]RJÉÒj¥»÷Ú>¼ÿê"í¡³±DW(í6vŸ,ß~¿¼óâêÎñÃ×ßÿ~¶³7¥âÖJÌ\CÍÕüÊs½+Sa¨Åe¹°ü—Õ} «¡DêÔéäñêåçêZwý¦œáz‹4º^:;jà,@ž¹Å«|z‘ƒG~×êõå­ûZõ"U1¹’iîè¥å˜TbŠÛz÷ŠÑ;I4ÀC½˜áÃ’F}2æ´Zˆˆ¨Ú r›‰æµDãUk.[.< n2á¥bj,Ñ4ÚW’•m½´Î§:óA.J'ŒÊz˜ÉÏ$¨¢ n.¬…˜¼Y¿œëݦ’Ýt}Û‚&ÂLgDq"ÆT@rÅRTj¨ì…Ù"3e¶¡ïܘAFOv){+ÚŽð6mÔq…HÖÖ^¢¶­·€ÇìÑÝXªãÄjuƒ/®80-"A\jczt·Gji ¢zLúÇù¥S½¾Íå–1³ís!©¤×.˹\„ÏQ©“ê¤*ëéæ–\\ž‹Éq1«!N'• gu ˤ{' °˜ÖŒ©UÀHÚZ"RÝ…Oe­¹U^mí<ªlÜŸIDŒZuõÚ8·ã5"ðD;?8.ŽŽ Ã`$\®°Ö´ú‚—€.H”7†/áÈ…Uð/øÇ‹>JÊŽ1; ½„.æúv}­ÜÝÞ¼ü U^öi>»!æwx{bD†ˆÚRr«Íõ[ýKÏú¨´ÎIÕÃÔÒ³Jã˜Ë®ðùõ¨Ò€À>Ü}mí@ü÷P™¨\³;§‹û/×Önç{GV}RÄ+oT]÷qeµq`,Ž—UO®jÕ0“íŽNÞÿ›ß\}üUPlš ÒËjõ0½xÝhîK¹¡hv6žß¾þfL­ù¹LP®ÅõŸY%RKQ¹b z~©¹~ QʾêgK ±ñ×Vx¢jwv;;÷‚´‰+e6ÙVòpwÛk·bbe.0OóÉvoã*”ÏÍãS^h­4¼“jAô ²åDféñ›ß¼tú™éðÙÌI¢R+YÞM•wÀ7q1Çç–!%Á¨ƒ·{¨RÃÌAPnÄõN²~9QÙOT·!ðB‡y§¬.žl‡¥rH,£zt»±óÍ /ø)T®‚’Ä´ºÞØAärÉ„¸L\)šíB¯ƒLI…U­¹onV¶­\}#¿|#Èep­¢”–çÃ’7C,°S#$T¥Ž%›~ÆP³mÊh, Z+, †#ž¦‚d øŠ¨5xó•Ãtª TLY}&· +ŽÉÅtc·wø¢—J#BŠ“«Ðõzu].­xÇ[ï™Lªå§R aתjv±ÜÛíîsÞÃÎE!¾(‚üÒFŒÑy³i6·Z›·ê›wôÆÖØ¡”*›^Zˆ* a>§–7ëëÏö^£²[.,3å´üP-­ºˆäE?=å¥1|nS(ïÌ#²×+£«€ˆàGa6ã§Òu­ú~®{ÍlžB$ôR9µºK7®Ãó½ˆl—V×O^*/_‰ri/ˆ½Å!„©-“õT¥ÊàÄ®mƒ62VG,m¥»'õ­zó²·ç¢š^¬ŽN ‹Q±J¤!¬A0L”6ŠK×´âZˆL ÖoÔ÷þ²’³dmÈYµÍg[;ÏGø¨Ÿ+šˆ1y„ÎÍeªÏG”›eÌž`µS…~l|7ÏXC½yâA“ó>zÚ÷Edœ³âZ+¦6CB9*”ÔÒ +—€9&Æû+ò£;Ǹì²3¦zcêx9w*%d‡ÿûð²ƒÛe@Ç•[Dª3铪…ø\€Ïáé^PÌ»Àg•Š^Ý ÉE¾üÒ¹²)W…ÒŠVß`r×…øÏVQh¢‘Ðé!šè¸¢‹´ CóÝ=½¶éeì(ø>•ðEpdPû7^1žÏŽjwݸVZ:Mw.fðöP+oB=»ñ™¨2ÉÆx9>ʦÌE*³!ÑG›aÞ +³¦b/J™Þ\œñã3Ï»(?‡¢•J[1¡€ÑÉî`¯³v„+¦–cZ#¢ÔÀ­˜TH4"£•µÒ²é*98ì—-/D!•§³­C!7œ2><¦mGTC¥:“^ ŠÕ¹°„}=‚¤|v6 +&¸ÂYýúêýêê:³qÖÅ̆TmÎ8gTqDäe]©®VÞb@$e£¼ÒJó’Fˆ/Ñæ¢Z\ã³Ë æ^,aÖÖÓí˜MÒ Æâ ’ ’Yà"Æì“”KÅá)€+¼X(ÌÇë}E¤¥ëõÞ.X”VÒKC»s©²~ÃîÎ…UH‹à†_&ÄòÙ™ø´—GÕz¢º“\ìÕ¦=LÎh•-Þ^zf*t~ñ +0<øìªÕ½!T. %E‘Àäl\.„ùìX…Í€PqÑy(?<Ñp#¢^ÙÐëû`¦‰ê–l{Ù¬7gC|”µ|¤î–\Ïn ¹5˜h)¿ŒéuÖ_>yeûö;•­LaÝO@Õš~6 Rì!2 ]r~uõèÅêò‰\ÖÆ`Vq¢:pH\kÃû§»W +ëZp1“kmÊÙ"C\Š*žhrÖ2œ,דʸަ`j웶g)@™™æ®`vÏÎúÏÌxg4pˆ/›ƒiE¨”žé¡MX"Õg²«|q Î/I {ÚÛzyÉŠ^B[ˆ'Ã|E)^â³ë0­“AzM ‹çÝÔB4à*XrªåºWÍÆÞ9¡Òùî>Ôöl[ˆÈŽX\’²FQµ‘+ º>8¶ë[LwÇ¡Š¬…ˆ:í絪æ†3>â‚ùßë?GƧœsÖtt¼ÑV\Õ‹knT«¬ÞÚÔ[Bn¯Ì)O¹q„Ô•t jÞ(ˆ0^ùG-nŽ5לíÎÚ©Yê£l’Ð+nÒFm¥¸I$ÚQ¶pqñP©òZ®·ÑCÏ„I7}nƒÒ•óëRnÕO.v|·W)„›s>ŽÔkcOQ+^ +±Ð.h%°'x@G0öά¡ÖjTë…¥ŠÓ@‚‚tÚQ6W›lnC®]f2ËN\™ ‘A h³N(ŸT¦¸\ÚÌ/]+,ßFõÎ|bo)YÝÌôwïö$j[H¢ž\®nÝÊ ½VÞ£  …ivÀ•ì ²|˧`„Ür²s-Ý»¸…jí Wô‘fŒÏÂ'ú19D´±¨–wª«w ýk¬¹è!ŒIʵ<Ô*('dp>¿ Ò:à§2óaÙ‹'¡¶QYʯg‡73Ãk¹å;‰æ‰¯@´‘³‹¥á51¿‚6û¨ÖaÒ#xùxw9Ê0‹ƒÝë¯DÅÒTH‚Ž(ˆXVJ;jõ|fÒK‡èg.ajS[œ½ +9ý‚—>3ƒÄØñ²i1¥äÅ _Ü’i2шEȧSAÁ“[«§ÉÊŠUXk1Õ:RJ›¬5‚žòóÏL‡Ï;â¤Rž˜‹F9[«lF·.=øVkïE!;:?õGØbo_Ê 2A*" @$C¥'=äxA©<ÑÀÁŸ™‹AÑÂ_6MÉ™(%‘ŠM¨E0>œ)ù¹ ¾¿>ëžõ0!•ÒB˜žöá!:½M‘Z›ÔÛ~̾è b´‰0Æ´ ¹8‹L9ȘÊ8#Âx­ £/æ·V]ñd<рAMÒXL”/iõC&;œGÔ³ qO<b2ˆ˜HEÖ^’Šëx²æÒŽê + !B¡ô*äJ{üÍ Åµ{^®8é#äÌky!YbŒ*‘¬A‚(o¬ÜøBfõbvýlŽ5Z$ˆ'žÀôV®mûÆ—Ö¯½›_y8}ˆ,Ù=ÚZ óy¨=ÂècÉ%³uEÎohÛfy£É™=\­Œw¡³“^~>ªbFÏ^¼&Öã‰J¦·/—V=¤ Ê`ò´5J¶NÕ*Ä™4«÷žˆ…¨ÞÆìQTï`ÉNeåºÝÞ‹òÅîfÁµÙÌ:³¬6h£m7vªÃ+ŸAÅ‚^ÙøL”ÖÙô0ÈÁÚ.º)(!NkÌC"Àt |øP1·—Œ± °ÕÔÂ¥7ç¼7#b•Í®'êfërT̃–†Y»4¼‚ëM?‰ˆµÙhÎÈ…¨P!(Ÿ¹0¥”¢–ës©†RX ‹Åé 7íÁ¥ ­…ñMsŸ^"-T¬ø ËO]p‘œk´ãrmÒͨL€- òxß±/üî IVC/®Biýãµþˆ±ây7¢;Cò´pÄÌêj}åzˆÊ<=…Àf}àYÅe;‚üöÔ¤!(z¢ZˆÍž¯Ï‰zbÀLDj j ‚è3ä¾™0%Š‹ >÷™Òú|\Ÿ «¾âDdG„rV¦}88~=·|3ºÓã›ù(¥"¼“sŒÕŠk¤0£ ŒpÉ%…páӤѤR-@#ŒtaÏ.CâÒ½›òa‰±§WÖú;÷»»/rÅ]šB;.¼¸æ'M¥°Á¥—a”Ê>™ºâ)WXàôZ\HOº7*㉦5£|Q(¬B/‡¹&åìÖVTȹãz\mI(¹«ÉîUK˜w:Q´š;Tª½S¨ô0¿r»½÷âøÉÁu,Q‡Œ“«­[Í…˜Ži‹Vçêâîó+Ç/×·¨Õ-/ªjvw°sϪo:i.,Î…D¨m6ÕÃ…2BY³Þ¸lu +‹§Ð­¾ jàŒé¨Ü_h‘^ô Z¦±—íæKvC\q&(À<ÆE`‰}°‰˜RIµ.CIÇÍžrÆP!“4Κo¢\*UÙ`¬¤'fÄåª#¢œ™Gˆæ‰Ó^&HX„ÚŒ‰µÐxMi lËOÚ3>¼ÀjsP®q>F ÚDÍ/+…5WÜü×ç¼Zn +’™ÈŽxʉ¥€.ìæ®h-ºáUa%Hf<@t„=í"/:ÇûÀBÀ‰{ãÆT4ñÙéðE7æ ³)s¬íŽ>¿t ô„ϭφ&:Ù 5,Õ[#Ò†¬w-?¼?Þo¹T•5 Za}q2ž¾è—¤Ô Ð:„ôáÁ”0kD¥$¸òòm­²=a&}±­Sf±z!Æñ'ÕR¦³Ÿ[¹…ÛKA±äÁÍ0WŒ'Z~Bƒ’r±´ i²ä\ y6=ŠŠ•ñžƒbQ*îJåC<9Œr¹Iwì©i¯'®âZg.(;ÂjL(±©.®VÅtG0[ ž¸W«€7ˆ\ÁE¥¼g4OIkÆv6ÄÎúñÁÎ}³±“esÜö™¨XÖ+›ˆ 2vª¾›^+,îÛ­],Ùr)` ÑêY•u6Ù˜ôàΨ<’‚L‘«S>îì:áÄ…T+ÓÚq!Òôx+ÓZ•Ñ­êê}BïMyÙ(›î>¨.]qcFD(ƒoŠÙU1¿ÆXýñÕ’O÷´Êš‡2Ï8ñù˜àÊèžÚŽJ%ùD±µ~µûóhòŒ‹ò%7a{¨¼XØš +“óˆ 7ÒÕKPÏÓ>îé©ðÙ¹˜OƒøCiwâ>L³Ãdóª7&WCL~>¢N>s1©9J B™Ío(} +2™öDÕY/;é$ ¾œÌ&Èg&ÜÏLF¢t>BgÝ1Õƒª“ˆEÙ…Xxc¬êRÞ¨måsý£üÒ‰T\JŒpy2Ù°‡‡Õ:ªo¾PX¾—ê]bNÒIDÊøè$eö…âfm°¹}»}½Ð»&ØÃù(¢ B¯…¸t6%ü”Ť:íGKÇoƒ§Lù(g˜Ãä8i\kÒùµ TM6O2ƒ[ˆÒôãÉBkKÍõXrÕ¤Ÿ‚]³Û§fçzD¨Á˜»cÀo5ÂX‚Œ6‡¨î¸á%Ó‰òVuã¾â‘Æ•jX(Á|¹qzËøø†ÊÌ”ãskùÅ“ÎÞ a©9ᦧü Ћ”î¹Ú*‘ìÊåÃÎî+Ííµ>íÇ$Ó­tc3€k +LýøÚ•C©z*@5ç±ñþ)ÉÖ¤‹8ïÀ Gão×µSD¬ø0mÒ¹£!çÆ—üè†É LiÅÄ’7@dpµîÁ øSˆôÊÌFƤäû¸Z<7Å…b"¿–¬îŠ…uny9W(%G+\À„¬YZ×K«zuÓ»~®tfŸqcáñ%UƤ M 39Æè±ÖR€H_p’Ÿ»Œ 9»¾aR ñ§¦c3I.‰Ífÿ—j;ü¤žnåºÿj"pÞIM†T?_ìu1»µàŸ™ ÐJ¹9ºqf*òWç<ð„°PãókÞ¸] Œ—¡—ªÓz Wì ©¹ÆKi%e{I«lÒ©V„³á R6ïÁ PuH„¯d3Äg½ÅeVsíÊCL×øëA“Lõ„ü&j¨^Œ¸ÖˆËµ¸O¶'#ʤŸ 3)»DéDaÝê]C´vD©{˜ìLTvÂ8Hå_˜ +ðx¢C¦FˆÜ 1`ãùhâ¢øÄR&ÐÅŒ—p¡ êI65$´¡w sçÂJ”Ë{‰ôTH‰Õ¨Ú¦­eHp.ÜœqÎ(”*dAxC¬MtbZŽ_*mM…øgæÙ Óí#ÓA6‹é¨ÒC·[‰ò6i´BDâXT,B.›j¼¬#ª2FÜ9@e'ÐñQXGL)-&½6”¢*4cˆ2q17ã‰W«Æ`äÓX¢ šbÚÇûHÇ|Tžô’!®D›C£u˜Á(gçy/Ý^:I–|1y!È;%€éALƒÇŒ#EÙZsõÉëRfÆÏÍhƸ6ð“c\ðs>Œ·:˜\œ p®hbÆÇ&D!\.LsÌ`"\9–h£‰®Ÿ©\tâSÎøœ'˜ü³“á)/ÇgÆWdÉ¥m"5ºà“>s> ÌçK\²;ëg¥tŸ±FˆØp"ãØÝ„°®”åìRŒ³Ç‹Ÿàª3<Î)A"Ik >=PòËLªM$*„Vðàêxû*+ Xêb@˜*О³ACž¶–y¼ˆ¥/ª‚Õ‚…Ç…²7¦A °d”- ©¾`ô.¥>á@ÿ²µ={n6pa¹èf|tŽB +’Iœ™Aò­]ðÓ³óø ¶Ûx½eó}Þÿ¹‹!OLwF•I}n6ÿuDu\ªÓZÓ×ÏÎE'ÝĤ‡*Cø‚3Bª;ªèÙ^±³Ñ-Ä”E€gÜXˆi.ÜšCõg\Ä„ Ÿ òq©*eG„VÉ…(¨+9ç'Ôæ¥ÕÄÂND¨ B)Ì榃ÂS3‘sÌM@a˜ŒÆ ' ÚD¥J˜/Ĥ¹¥â)Ón"ÆÚBªCH¥0Fؘ<=\æYˆŠ1! }Á˜=d¼—YÈ tªãÆ Èãøà/q¹5LïG¥&x„#¢B<atWK 4±Íè§Ó„Ñž +KÿêŒPHcb|ꢌ‡¤Ÿéžf{W½¤åŒ)J~šÂ*@“>f&ÀAÍ䧵퇤٠2Y£~”èˆësQy6¬€¸M8©(“aŒÆÌ_råxs(¾é|á‚—™pĽ±D˜²ÏÏEî ú˜bXl0ö(Äe€Ö&bÐ\ðÂ)7WÚQ©bÊ~2«„ÍLÌE..`ΰòôTD2bRÜ +Û݃W·ï}#QÜ}z +qGPœ˜ƒ_pŒ/³³…dåRª±Ê•ISì“ZÛ‰$/úÅɈî$€Rz͵Û×_úö¤*<í¬W²›q¥Æñ—Ý:’~2íÅ­¸4µ½à@Çßi r2>L´öL8ÎB$—Éäp&¤Wχ`*à"B뀽Fùp>иÕC\V)­Ç5x{!štÇ´i„äËWbr²:ã«íuÒ˜\ ‘)„Ð0.å,1þíF-mˆ™‘Õ] ‹(—™‘³î9?æó¤Þ5[G¡0‘¦<4B§1Î^ðÓó~&Hš„Þ‹ÛLvÕK¦¦Æ›,+À¥Ž˜ò^: œC˜‹ •´ÙíšGä¸V—K›|v5ÄW¡é Æ•d7Gˆ™ñ¸ÚPËû±䲨Ö<”>s1à +‰¸TœóQSÎäqø\P~­r‰³ )¹izi)Û‹'°6ª€Èããjw[—I㪫T—®‰q<$R‹0ÑAº¦r&;â YâR‘H4€â¦<Pœ +‰Êè¬I|î¼çÜrv!~ÞÃLúy'@;¢:`„Ù’”_Ö¬Ÿ§“ƒ )5åŒÈ3^úé‹îg&}P6A" % +§Twë»Oìám£u™þÿÙ{Ó嶲$Mð†?ÆæGOUWf"Eh#xϹ÷Ü…¤(î¤(p)R»( +E†¸((R±dçVYUiÕÓÙUÓVÝm=c=f3ý¯ÍÆæQæ‘füs?çâ.ˆ  ÈŒÐq—³øî~Ü·N¯Ëýy¬Ç¡hýòËQ/Y&ù²ðàds÷÷së¯ã…ç$å‰âvOÿ¼¸yLºYõöƒ[¿y|úOßÿóÿûÿôßßýñ¿Ü[ñÉ›{Í=Xix·ï“Î<·~0µ¶?±ÚˆŸ^‹î^ î^%a:ù0¹³M,…ðäúز?÷p„-ý‘ ¸ˆ‡“›£ Ï’…ÇÕ[«¤ú†Ó›K÷¿z´÷[BÂ룫É“«ñR¢Ùg£‹±¥=R‡ 1ÃÛ÷ã™Ä1©m³¢Å½¿]õÚb"7b¥:z§:Žh…wkmäÖF8KÆÂáÔê>Éß_Þû¬:–Ìnš©uïö½ha›LÈ;¿]«ÿ=ÍèKoêÓk£!äæ}tšXxÝÝ[}5¹~xk¥qs¼ö…?9±úrfëhë«¿»·ÿǤ¶ëÏg»O?…ó[fn“™šÚœÚ8ÚØÿÇûûÿHì¢rcô 5c¦·ôm´/$½KO?_;4³OÈÔ%nÿ7WÌÕ[ÃáÜU5é#¶ø<^ÜÖÓH—›YßUSkµg«;¿[|òí_¾]zDv´7µ1½V_yúõd‰æù_Om쓮審æ?¿næHúW£;ÿúå-O­Ô'îÖ‡om~¦>ÓsŸL’@ÿÅõ1ÒL¼[÷H]|1±²?¶T§}ùÜ›úllçW×Ç®¡™òÊuÒÒ¹ÍÙèÜöìúñÑÕÛk{Ó_‘d'ãô÷?Yôg¶'înßÙÚ¿³õê³aZÆuš´­Ÿy³ŸzófþñÄÚîíµý…­7Ä‹H^“.7ÿðlaóhzùÅØÔÆ5=32ùp›4ê/£»ŸùóôÿëÉòôj£öìÛ»/þÍí_û ;×ÈÄPÈ NVIa˜Øx3¾özlùmÉeÂF5¹I4r-Z⊣[c$[gÐnÞŒ‰?,Ѧ‘•Í=&Uy„03†f»Õøã«ßþ_[_ÿ³™}F¹µf¦7B€…4ì5äúþ›‰¯2aHûkdP÷§†³Â™ç“ko¦7¿YxôM0ûøŠO*DB¾¾óÛåg¿¾½öº:AJûÓ[µ=2X&–ž}âM‘á¿þòûÕúôÜóã÷ÈD$æóüÛ¹WúÖÝ`zcj­±óõ¿Ûÿþ?ßZÛ½~kåÚØÒÄÊÎÄê«ë£+#ÄæŸ.>ù~iûwSo†ÇïýêFò¥žÒ·×‰É ƒwb‹Èvtù«pq‡”½©»Ï¯ê™hz“ŒýgGXmü&YþJMÒ8¼ñå[Kϼ[ËÕ12gfî½Ú:øû¯ÿôà«?é™G¤è’^ô¥:}róÖ—Š®¹7¹V×·ï]õg>¹>v¥:IˆLnüÍá§Ã“$)n/ïÝÛýÍ[k¿ªÞþ” Ãä®7¾t )=³Ÿ{3¿¸zkdl-„Ú$u}$Y¦-!ó1DS»9vŸ¸ß'ïh¦¿¼1Î>^yþ}mû7Ó‡#c5Òo+Õñ+zrtæÁ°?ÿ7Ÿ‡7¢å•gçû¿þ§{»ï‚EI“@œ¿D¢™tøÛ÷^ßküíãÃïM= +f·Cb•³O¯®|¦g?S3áôÖÆ‹ïŸÿyûì?­½úÓ—Ñò§#dGÌTÇî‘tó¦ø Ïo¯Ÿ–Î?zKF©dD>Ÿzs„–Áô£™õ¯IU£­ü"X é󹞹&üŠÏz§"55ž{4·¾÷Ñ) ”ê­M²»WŸÿzæ‘êæÈ8)O燵—Ñr}ñÙ·ÏÞý—¥íßÞ õcdòÊÈ„7º4]Û]Üz›ÜÙ™¸÷Í…6wzuÔ6Òó¯´ª 4ª¥'o§Ö÷i$áܳ`æéôÚþèâÓቑ©‡·–_ÝßÿÓúÞßݾwÌ>!ë8™0µúbéáÁzãûÛk»¿{tüNþ<¾ú±\¢”å'g÷ê¿—júÙØÒÎ(YF³D&+_ú·ÕíþÁýýØýõzpJLÒjjëÖÒÎøÒ“Ñ¥‹O'￉÷Ì,<$ÞÄúçÞ4Íe8ºÏlLÝ«“Â6½qDJþ­¥ú­Å«þbåæ­á}kãzH²o“DÆíÕ½ñ;uÒ´q5ù«O¼‰8yFÆïUo­O¬í¯l{÷w·—_^õoW“ù¥­ý»Nþúó˜?™·WIÉ™ÙgžºH¼÷Ó‘ÛÉ âÿ³÷OÐv¹¶?rëÁ5³2½òš¬ìkáÒ/¾L†“eBoê ‰¿ëñÚç#³¿º:Œ­ÜZxÏ?Ô³Fïnþyçü?®<ÿõdm—Hizm÷ñáß‘Ü$†y…XIÉÕÃéÍ7$ ¾Œ—©­ÕÇ'ëÏß®ï|OkûE°LO¾­ðúÕÈÄÕÑ%=÷tæáyíůg½!‹&¤ùNø 5~–ŽC^Þæ^Ð÷Oõì—f~db ç¿úÍâÓo&j/É–'æOëL»0±¼w{õ€ló‰Ú‹‰»$UÞ[ºµº»ÞøÍöÙ¿ßûýÿy{óä3Ô‰«zjzéñØüƒád骙#Y3÷àtáÁÙôÚWŸ _©Þ‚{0©‘i0v÷YubxÚì曩uR×w‘ç0óp´Ö ómf㵞}BÖÖ­µ£Û«¯Æ->œ¸ûdáñɽ½_ï}÷ëßýË¿V3ÏnŽoŒ.’žó*œÝŠv&çÖ±¿ðôÚ8)ÛSþÄêÜú«Eäëwv4)HKõù­7S÷HžŽÝ}úð«ßÀO¯Òÿ¯Æ+É]2NÕÄ}olå³›£z¢V{t´øðdfó?ýøfrïzDȹ9»N»ü­p¦6ç7&×_‡‹Û#㛟ŽO›!ågráùp¼„v?K„ù;¿žß:!Ãöj´@d»õòûÙ•ÝÿéonÒBÍÞ?X|þÝÔÖùØÒË›„™_Ž^ I œ¸û²:º<<º6¹úzrå«xn{îâGÄF¾P³þÄiÄë®E55ûŒ„‹¿Ž¯²æáF&¹v¯ñ‡…û¯GžÇ3ƒ©»_?ýê·#ñqÔëc+#S¢»õkãëŸwnŒ¯¯-<#%ÇŸ\¡í' ¯ôí'7F×éoÒŸ{dž«™7'w_üîñÙÿ^Ûûûhq›ŠXzubcqëëû?Mnž›…—7nÝ¿†Ü•M=y-æïÖIžÒ2’.7~÷Ñ#ÙJ_†‹dæÇ Ïî>ÃákÒ¯'µ‡û¿»¿÷ÍÄÚ³`áQ0G|ìÛÉ“`êÞèüƒ«Ñü—ffé‹Wôü—þâus÷Z°ÜFøÀÌoNÜ}øìÍ?Lo½&…ÐÌ=%NB|õz\›Z5V«ß_# ŽÔ¯×sÎn-7†Ç—ÉlÔ·W&W¶gî=›¨=&eotyïññÿºðä{­™~x#œŸXÝ5óoÞZ­Þ¾7÷è¸~þOÏßü/w¶nÍoE“ë¤Þo¼úCxg‡êæØ™9«›¯þöÎããkfþF°¨ÆïrµFww¯o±|æM&óN¬ŽÕöâ¥W·×Žžý1“hzð…š%5˜´¦éG8>v{+YÜK–ö'×^’üõ•ð =3vg›´÷Ïý;þܳ•ÝßÜ©ÿíjý¤–üâÚø/®Ž‘ø ã+^|6|{Ó#}øîîìý¯ 퓹§Ä<ucœ IU2ýnŽm¼!…ÌjlM®ÞQ4@ß¾Í=ûB-|^½†l/>ÇÙÙ­áÑå_^#6Küvrywtñ™?¹q3º»òèk=óðÓháÚÄÚ—8Ö½eëÉbcñÁ[Rö’ùGã„ ‹Ï®†+ŸŒ,\‰Ã ñfáÉ[þ1ižÉÝcµÆÆ.J‘/l?<±uÅ, ߺWEAƒõ`æðÖÚ›Ûç·V_ÓsÔÄj2G¼ôÉâógÿr÷Å÷ãk¯†ÇV?÷çh#Ô4™ä‹W£%RW–žýv½ñ§™‡#( 1KúI@úÕg7'Ö>Ó“7ÆV§îãPU8·õ¯¿ŒuãÑ OLàÖ}„'ï_ŸØŸÏ?ÿ3ë4N’8´£wž^5w®†Ë¤*„‹ÄÊþþí?þ? ¾_Ý»Fzõíû7nmT'è¹áÂ.ÑŽºý`þþÁØÒ£ëáô—´ãf9˜x<µrø`ÿïÆÖWâÅÅ­¯êgÿ¬fš…§þÂãk$”Im›X;ˆŸ¢+ôĽèÎËõÆ¿Ý<ø§…ÇßèÉMRü[kSµ‹›_M­7¾½÷I°âM>™Y?[~þÇ©õ7Ÿz³WÃ%ŠÜùýâÓ_«ù'Wâ¥+Á|2óðFr÷W7Ç+7ÇÕ­{ ßÝÝþíøÊ«/£Å_Þœ¨Ž­Òÿuóöѵѕ½»/~óâü{pòO·7ß\¥œŽgŸ|¡+7¦® Y}Ý›|0»õö«ßÿ·Íƒ{ýÖz|÷ùòóo—žýzòÞkorã3³ð™Y¿ób~ãõÈ8˜§š|Í?»ûäÝÒÓïF¦žÂs›ùÀÂc=½92¹Q½½I&ƒ™}Ï>‚)¤çFn­O¢ðŠ?П_è9²âÇIYZÞñg6ã¥íéͯ—_þþÎÓon$«ŸÜœú¬:uÝÜùRMߌæ«Ñ§×Ç>¹1¡'6fÖ¦î}EÖßg#„w6_“2ÿéÍÉñ»¯ž¼ý³Î&–¿Jv°&«/ï??û«+~°ðrüÞ׋Ï~¿¹ÿ牵¯hÖóe|çþÁ«oÿ9Y|ô¹žûW_$Ÿù‹·ï‘¶<‡¾ÞˆÏßÿŠd:é¤Àü²:íM?Kw7÷þaûí¿x›ý‰¹ª¢¥„ö¿øbì‹°vóÖF¼D«wÏ=¾ÕVv~óÕïþÛƒ×ÿnöáùçÁêÿwI>/º=€Ÿê3˜H¯}éµÏ`"½öL¤×>ƒ‰ôÚg0‘^û &ÒkŸÁDzí3˜H¯}éµÏ`"½öL¤×>ƒ‰ôÚg0‘^û &ÒkŸÁDzí3˜H¯}éµÏ`"½öL¤×>ƒ‰ôÚg0‘^û &ÒkŸÁDzí3˜H¯}éµÏ`"½öL¤×>ƒ‰ôÚg0‘^û &ÒkŸÁDzí3˜H¯}éµÏ`"½öL¤×>ƒ‰ôÚg0‘^û &ÒkŸÁDzí3˜H¯}éµÏ`"½öL¤×>ƒ‰ôÚg0‘^û &ÒkŸÁDzí3˜H¯}éµÏ`"½öyñÿ^’ÏJå’|þ‡+WæŽwgwÎv†.]™ºc¶éïõ·gÓ¡•¡+Û#S§g³õ³ƒ“ãÓï*£òª:¨Œ¬7v+WåÊ +]TY==xupLÀúÎaãZå&]:Fÿ yUñø¿Ž¢ ªú:Pq¬é?ºÅ‘_5ÆóïOû•#\dâªëPÇAàÅaù¢C\,H’8Ž‚0 袀ïJ%^ÉE¥×µuQéuûC†Ž‡î %•«×* mf¾ÑºÅX·éÓó·ûk;g´$Çébn¯œ¯Ÿ¿ðtƒ–*ûÃÐÊü¢<ùickaþà{0’~¥µy¸\[9ÙmàknS~ð‡±ÊÕoé§aÖéÁËó³Æ[ì íëéNáŠúþÁáîiã¿ëÊÈã³æoø×Ùwox_¯*Ï»r­2²y|P'ð=÷øUþÒw;‡çrí~ãàÕþÙ_}¼sÄÓXÜå7{yNßìží·=%{õ…ÏèäåWúÙôÉùñ. oúäÛv§·ÇøF—ž½m{’¹{.|ªíoÝ{!3£o{zßµ=ïº0©;ÛS‡oöw¶U»ó9Ø¥+ß3\s³÷ +›<;?}y~Ø8®7Ú]¹µÍuï¹h­z¦Ýù¼ÜyÛ˜?m|}N‹Ð>ÆîºðbßÚáiãíùaûâÍ]~ás:>Ù8;8«¿Gj5çõ–¯¾pØh_äî¹ðêv§v|~´Z?Ûy×Á̲·´Ã~`üêGÆßšûe„rã~›ì¤9îÙƒ ÞœƒãvwçäMãtçìä´í½iÞpá(·qr~Zo,œî¼Ù?¨·-éÞ³YwÜãÜïàø=T—›Œî"ñÌœ½9y{pÖí|Œq°–ÜîFf{•± Ø‹s˜€p`¶c?gpït‡ôÆÕ“ƒ·—Ìl۲؀…© lÀ6öyÍÀØ€p`lÀ¿Ä^ +.¡ ØÁœúÃÖ—Å4êd&=oM7Þ57öwvO¾ùyGÉD€²­xYÄgÛjèÛ³ÝÙÆ»ƒ ¨;{Ó…Oîåáù{¤×O`uICXØ9ûö`çxú½sìE»m´ÛmŸÏïvƒÑ·?‘öÙün7ø|'´ÒëLídoïmãìý”Ñ¿Ô¿Ê3ì?º?„®ŒìÂúÉáÉéè7ûï5q²è»Ãö½öê ǽ¶NÞžŸîíÔ6µÝYån0‰¿`.oß4ê«çï¡¡þ󜶭 cþç‡;§3'ÇoÏvŽÛŸZùÆ.„?;åÜ·oNŽ0Ëæýd©ýIŸvéû¶WåûnØÒ¦ƒ©ô¸[@u²-:º¤Š¬ŸÕ:q™}_ecÃRlÍêý§]ΰC‡‚¶×õ†ruwèí=ê7séÂ)½ÂÚ–¯ßãŠÈì.½ø<Œ¶'ò6;ÕËöëö©þu7ˆ¾ý‰øíOÄïÂDvNÎögíGµûI~^Ê°}ÇJA¯KÐóµƒ÷˜ÚÞôŠôíÜ“Ü1áui?—§¯XÉþS‰:%µK¼%oƒÄ£AâÑÍI]¶Ä£ŸÑá“aÓöÞõ¸/µÊêù«™““ÃéÓFãû¶£¡—1¿JUÛ>}º³{pÞ>͹Ë/|owwÚq÷ŸYz©’áÚžH¿ƒÛß™Ýöw¦M¦òqTÓ“Ó7û'‡'¯Úæè½c¶ ¸[q·KÃÓ.ÝÑÀöOû xÚ(á—&ÿõÒ|Ûa¾¡øÎôÉ÷kúîe­Ð†õ8õ{í+™ýYíàeûJtŸ°·Ë_ë íœø¿¸ÖAÏ"c¿H¦~®Úðž,• uvæ¢[‡-.Pê€Lz>ÏãeÛXÖ/ò§ýuÌÄ.:ÒvÐÄgpBz¹ öÑ‹l¯ƒíèqRiÿÐöŒ>èêÛë(“hïàð°“äµÃ.lëáÁqc§íl2ØëË'íçÑfnè]þ²wzrÔþ6ñŪ£mØœWúú~›,ÜÕ§íqÛ¶S¯Ÿ¿?S";½Ì-¯á¿GτЮýÐ\}áó9m° Ùövíë`³Ò.>Måð›ïÚÞ.’Êg;§Iq¹¾w#g'íÛË'ݘHÛ›³Û~7¹¶K–ÉÎñÁQ¬ì#QéïòwÃñ m¦Çì¼ú¥K›iFýâ¤ÍühëŽ+¸ ëqê¿ü"냼™¾Ë›i¿Ì]¿%δý"›ú9q¦mm´?g.Xê€Lz>q¦~égÚŸÑ qf83Hœéh·~‰3êç’8Ó£ìu|égÚŸQ¿hâ}8ÓÁvô8©\êÄ™ú¥KœiFýÂ~âQÿ¤ÿt°•ò.me+ì !m°ý[Õ£öâ㣻c¸4%#§îlÏr]íÎüK—,‰µ}wá XÆÓõRp»By·Xì”çð·¶ø[øsæomO~ÀßümÀßúŠ¿Í` ¾]*öÖÀž¸Û€» ¸s·ò6ànî6àn—»eÃFÛÇ/“k{ò?I·¯¢„"ꀈ¢Ÿ3µ=ù ˆ({pØ«˜¶eÚëäfî¸p•ìgÔm‹XáÚÁ·ÃµÃï¶;;yÉøàiãèä}5ú«&ÍÁñncïàø½-Q³¹`o;g³ôÏÜqáó{ûUwÚ[?ÕÜi“]jÔüøô5j5j~Æ5jTû8øµ].z25¦½Š2ôo¯BÿŒÑwúïýPùèçQºb¶t\ȦWT©KØyñb­ä‹Nî }|¿èdN?ÓnŽ\ÆðMGxÖãLîCQ=°ÿäèÍÉ[2ƒWÏßø~6×%Ö0ãæØ|¡m'âë÷Ä{2ûK/Þäk{"ïq´e'¢º¡k·=‘öIÿu7(¿ý‰¼',;¿&êéÁÙþQã¬}¶ÜO2ôCtÈÒm ~’¤¿å%¥ýþø ­í›4¤Á)ÄAüyþ‘‰öDü¹³š‹ƒøó þÜÕøóåíù2ˆ?—g8ˆ?âσøsGÞ—Ëfuhíu¤zbνëoémÒ æ<ˆ9bÎØ=ØÛ;Û˜99&iyÜ>®•î»p¤û®qxxòM»ó<|s/±±Ð+›s¹;¤Œ¸KgÄ)ß»Ò.žî7:É`J/¿xGsÐþœ¾9Øí áÏ^}ñýçcj+Óö浯wC5îd&í+ÇÝЛîŸuÙëKèþîþ4|úÛý_÷Gû¸?úÀBî7.0p ܽÄ×îû£¯Ü?#ÃFÜÙNIk—Ñ„Û;Ý©Ÿí®œ´ŸO-7·¹ÇîM<¯úÇ˺è¼ÝjÛÕb^î¼mÌŸ6¾>o×ÛW¯ w]üVµíCé—*FÇ'ggõ÷¸Q³¾\}ÿà°ƒÓï¹{.Þ£Üv6ùñùÑ*q˜wL-{Kcã ˜ö þÒ þR/É¿½Ó“£ö‘/¾x¿Rû2zPMêǧ×ÍjRƒêK}W}É«¶Ý”ëì¤}ûæ¤ Sù¨u¤ºâM긚ÒGñ$Ý??}y~Hœ¹]—³LêxÛñSþ¥K®à86pÃþ¨}} ³Ð:˜Ó ­Çœåío]oç µ?žÏ@k*+Ûï©p¹Cgm*mýÀðªÞ%÷ûcßžÿ~óü·àõ;þ»¤ÊlÀ^³;a+p`¬À÷#ÊÀ +X+ðrYmÛ —Ñ +¼¬‰l^µíŠ3°0µøÑfØ6RÌÀ80fàÀ üp“É\B3°ƒ9 ÌÀ3‡Õe1;™IÏ›‚NNv_î´/ý/£x)^´­mªRô†ØQõ€>éÚ×ß6Fl"ƒ +¡òwñ6º2ÃËÆÓ~²z!íí>$]O:Ò¾<Ü©¿«èäÍNýàì»ÑÜÆoϾ;lß n¯¾øDpÌõ²UG“êšš"öI]N§igvi¥lï70ºüÄ[®˜9sY^÷7l»NŠmº9srÌÍÛÛ·-Š÷]8â}³ßÁ±ÿCÛ~¸ Ι™eñ¶‹w¨wÆ.6ê;(}¹{.þ´kÛšìùéÞN½ÑÙÜò7 ¬à¿`.öåíÁÕi»áË[FùmÓâÎ÷Gç„íÒë/|{ök5éŽ\ë™;.|^,’ºYÍ裨Z³¬FÔ: s¤j³ÂjVô÷Ÿî÷³PŒÚCŸhЗ¸_4ˆ¶•s·[Yå»p²£ÓYÎ}ûæä¸ñ³lÞ8ОÚÓÇÓžÊÓGSž6,)÷¯öt9 …{]±¸˜dëÞߧ~kæÑÇ^õ¶…âë÷„é3ûK{Ws}ýž+³Q]˜Hû;Ò>å¿îá·?‘÷TÊNÄïÂDvNÎö4`è'9z)ó;õ„÷¼=<8[Û9xŸ:¡zéqÚ¾.Ðã"´ý‰ DhEh§Ì¹×¥ç™Ö zy$hÿ¸ªM%{X3úè|±¶äã£ÿÐâ²V@º„]æ/}mßv«çô_(ÿò—vºÀÞ~]4ý\Ûéh‡Õvm‘~àïªâÙÿµú–BÚ2k!Ýå}À"{]ª-Эoߟ=×϶àáÉérØ‹ŒãªQ—º,Ê…ìAõwI‘áKSSd¸ms¤×‹Š\ˆËê‚ç´zñGƒ»ÄúµÇåLìXŸ»¼„ÔûqÄ)0³ûQNûÄbú`{zœTûÙûQÏnYÛÇ(û¤ú[¿t +8÷p¯q:pÚ ^”^Ùç³—íïq?øº©Ÿ s›îr¢ +ÛC8}ÿtçøí^ûÝ.zû/§çäCÔ¸^WM?ð@K?øMdj=¢s<'ù âl¼©ÃÃØš^Y’ÃÙA茣»cøVW¦î(o{îx7med{åäxÁõ˜†<Ýxupœýahå ?#Ÿ6¾;zyr8tu¡qú’äéµ!¯2Eÿ<üfè|¨™§óð;úc‰¾|E o*Ae¹òä™WÙÅ•ëCÃZW“XíW½DÅ•£!|‹Ã$…ÔRÈ0}QA(½¯¬yç1euÈ«z:ðT&‰êX¯Ea¤“$P~+Ï'HH_•ò$‰ 4†_M´2aê(0^À´ O%¡Ånó3x~Ry¸34¬L5Rô}8¬8Šh^ÃÚ«jÅ•á¨jCÖªJÃÒr•JLå_úaT6Õ ‰<ú¢’jÅô%¨F^ì)‚ÄUc¼/Žâ¤2Co «J›Ê°_MBÓ3•_õ½ $ô#L#=$I ”ïÓbÓË|­|a)Ñ6‡ç@ÓóhÍðp•0YÒ¶¨¨N‹C»ÆšƒÆRD„ÐÌ1´¡}¥{4ˆCÁòpè©dOŽ®Œ#<%ò¶0IÂà ¦ðŸÓ"á˜[iR]ð¿$> endobj 17 0 obj [/View/Design] endobj 18 0 obj <>>> endobj 36 0 obj [35 0 R] endobj 64 0 obj <> endobj xref 0 65 0000000004 65535 f +0000000016 00000 n +0000000159 00000 n +0000019538 00000 n +0000000014 00000 f +0000622821 00000 n +0000000000 00000 f +0000019589 00000 n +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000622891 00000 n +0000622922 00000 n +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000022790 00000 n +0000623007 00000 n +0000020019 00000 n +0000024357 00000 n +0000023090 00000 n +0000023265 00000 n +0000023441 00000 n +0000022977 00000 n +0000021868 00000 n +0000022229 00000 n +0000022277 00000 n +0000022861 00000 n +0000022892 00000 n +0000023617 00000 n +0000023652 00000 n +0000024239 00000 n +0000024431 00000 n +0000024806 00000 n +0000026367 00000 n +0000031932 00000 n +0000097520 00000 n +0000098117 00000 n +0000163705 00000 n +0000229293 00000 n +0000294881 00000 n +0000360469 00000 n +0000426057 00000 n +0000491645 00000 n +0000557233 00000 n +0000623032 00000 n +trailer <<23D1102C988D404C987A1831159D751D>]>> startxref 623238 %%EOF \ No newline at end of file diff --git a/presto-docs/src/main/resources/logo/web/fb/Presto_FB_System_Lockups.ai b/presto-docs/src/main/resources/logo/web/fb/Presto_FB_System_Lockups.ai new file mode 100644 index 00000000..831782bf --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/Presto_FB_System_Lockups.ai @@ -0,0 +1,2848 @@ +%PDF-1.5 %âãÏÓ +1 0 obj <>/OCGs[5 0 R 6 0 R 132 0 R 133 0 R 202 0 R 203 0 R 272 0 R 273 0 R 340 0 R 341 0 R]>>/Pages 3 0 R/Type/Catalog>> endobj 2 0 obj <>stream + + + + + application/pdf + + + Presto_Presentation_R3.3 + + + 2013-10-24T12:52:21-07:00 + 2013-10-24T12:52:21-07:00 + 2013-10-24T12:26:46-07:00 + Adobe Illustrator CC (Macintosh) + + + + 256 + 204 + JPEG + /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgAzAEAAwER AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4qlup+YtH0y8t7S/uB by3SyPAXBCt6VOQ5dOXxbDqcsjilIWOjEyA5oH/H/k76u8/6VgKIxUqGBbYncL9oii1qO2+T/LZL qkeJHvZADUV8coZuxV82/wDOY/8A0yH/AG8f+xXMzS9WjN0fN0aNI6ov2nIVakDc7dTmW0Mun/KT 8xIb26tP0JcSS2hIcxLzVqLz/dsNnqu449aHwOV+LHvZcBSfXPKXmPQre0n1iwlsUvef1YTDgzem FLfCfiFBIp6dxkhIHkgghJ8kh+kuahznxnnUurTLTPLusanaT3Vhbm4jtnjjlVCC4MteJ49afDue gyE8sYmiyESeSPX8v/OTSrENJuOTqGB4EAVJWhPQHkKU8ch+Zx96fDl3MfIIJB6jY5cwey/846f9 NB/0Z/8AM/NX2n/D8f0OVpurzv8A5zn/AOmJ/wC3p/2J5qnKfKyqWYKOpNB264qzWf8AJj8zYbm8 tzoNxI9jz9ZogJEYonMiNlJEnw7jjWtDTpiqTeYvI3mzy3a2dzrumTadHfFxai4XgzGPiXHE/FsH U9OhHjiqRYq/VTFXx9kkJjpnl/V9TgnnsLc3CW7RpKEILAy14/D1p8JqegxVGr5E83NNHCNLn5yc aHgeI5HiAzdFNRTfFUiZSrFT1U0PfcfLFXsH/OPn/S//AOjT/mfgKXput/Zi+bfwwKlLMFUsegFT 9GKsRtvzc/Lue1guRrdvGtwUCxyNwkUSS+ipkRviQcyPtUoDU4qmuhec/K+v3U9ro2ow38ltHHNM YG5qElLBDyHw7lDtiqdYqyjFUr8xaI+r2SwRXP1OZXVluljSR1UEF1XnsOY2OSjKkEWlOleRXstQ FzPqs17bhm/0KaKH0ihVlVWAXqvKvJQCe/esjlJYiDKsrZuxV82/85j/APTIf9vH/sVzM0vVozdH zZmW0M0078yDa2Fta3OlpfyQwyRz3FxcTs00rSl45HWvAiOOkfEg1A69srONkJMd13W5tVuzLwNv bAJ6VmJHeOMrGqMV5k0LcKmn6smBSCUtwofpLmoc58Z51Lq0w0TVm0y+W4MZniowe35tGrkoyoWK 7/Azch8shkhxCkxNJjqHm361belDZJazcFUXEckhdWVlJZSTXfhShJpU5COGjzZGbHiSTU7k9Tlz B7N/zjp/00H/AEZ/8z81faf8Px/Q5Wm6vO/+c5/+mJ/7en/Ynmqcp8q4qzzSPzSaw0yC0n0mPUJ4 oXjlu7m4nZpJWmZ1kdAwQqIysRRgeQXr0oqxnzH5hn1q+af0vqlseJjskklkiRlRUZ1ErPQvx5Gn 0bYqlOKv1UxV8fZJCO0jU20679biZY+LB4ObIrEoyoWK7/Czcv6YqmN75sa4t3jjs0t5WWMLOkkn JGQgllNa/F4dBviqQkkkkmpO5JxV7B/zj5/0v/8Ao0/5n4Cl6brf2Yvm38MCpVirDtT/AC6F3qb3 ttqTafH60ckVtb28AVESPgyhivPkz/GGB+E9O9VU88u+X4dHslhMgurocw960aJIyNIzqhKCpVA3 EVJO25riqa4qyjFXYq7FXYq7FWN+cvy58mec/qf+JdP+v/UPU+qfvp4eHrcfU/uZI619NeuThkMe TGUQebGv+hcvyZ/6l7/p8vv+q+T8efex8OLv+hcvyZ/6l7/p8vv+q+Pjz718OLv+hcvyZ/6l7/p8 vv8Aqvj48+9fDi7/AKFy/Jn/AKl7/p8vv+q+Pjz718OL0nKWxhn/ACp38uP+rR/08XX/AFVzK/O5 e/7A1eDDud/yp38uP+rR/wBPF1/1Vx/O5e/7Avgw7nf8qd/Lj/q0f9PF1/1Vx/O5e/7Avgw7nf8A Knfy4/6tH/Txdf8AVXH87l7/ALAvgw7k68ueTvLnlv6x+hbT6r9a4fWP3ksnL0+XD+8Z6U5nplWX PPJ9RumUYCPJLvPX5W+RPPf1H/FemfpH9Her9T/f3EHD1+Hqf3EkXLl6S/ar02ypmxT/AKFc/In/ AKln/p+1D/soxV3/AEK5+RP/AFLP/T9qH/ZRirv+hXPyJ/6ln/p+1D/soxV3/Qrn5E/9Sz/0/ah/ 2UYq9VxVh/8AyqL8vP8Aq0/9PFz/ANVcbV3/ACqL8vP+rT/08XP/AFVxtXf8qi/Lz/q0/wDTxc/9 VcbV3/Kovy8/6tP/AE8XP/VXG1Tjy95R8veXfrH6HtPqv1rh6/7yWTl6fLj/AHjPSnM9MVTSe2gn AEq8uPTcjr8sVUf0ZY/76/4Zv64q79GWP++v+Gb+uKu/Rlj/AL6/4Zv64q79GWP++v8Ahm/riqKx V2KuxV2KuxV4j/zkr+Y3nPyZ/hz/AA1qH1D6/wDXfrf7mCbn6Poen/fRyUp6jdMydPjErtqyyI5P Ev8AoY385v8AqYf+nOx/6oZkeBDuafEk7/oY385v+ph/6c7H/qhj4EO5fEk7/oY385v+ph/6c7H/ AKoY+BDuXxJO/wChjfzm/wCph/6c7H/qhj4EO5fEk+281zlvmb/lcX5j/wDV3/6d7X/qlm//ACWL u+0uB40+93/K4vzH/wCrv/072v8A1Sx/JYu77SvjT73f8ri/Mf8A6u//AE72v/VLH8li7vtK+NPv d/yuL8x/+rv/ANO9r/1Sx/JYu77SvjT73pv5L+cfMfmT9Mfpq7+tfVfq31f93FHx9T1ef92qVrwH XMDXYIY64RV234JmV2wz/nK380vPfkT/AAt/hTU/0d+kfr/1z9xbz8/Q+ren/fxy8ePqt9mnXfNe 5D5//wCho/z2/wCpm/6cdP8A+yfFXf8AQ0f57f8AUzf9OOn/APZPirv+ho/z2/6mb/px0/8A7J8V d/0NH+e3/Uzf9OOn/wDZPir7/wAVfN3/ACt38w/+rt/0723/AFSw0h3/ACt38w/+rt/0723/AFSx pXf8rd/MP/q7f9O9t/1SxpXf8rd/MP8A6u3/AE723/VLGlek/k55u8w+Yv0v+mLv619V+r+h+7ij 4+p6vL+7VK14DriUs61S5ngWMxNx5E12B6U8cCpf+k77/fv/AAq/0xV36Tvv9+/8Kv8ATFXfpO+/ 37/wq/0xV36Tvv8Afv8Awq/0xVPsVQWp2uqTogsL5bJxXmzQiblUbbFlpTJwIHMWgg9EGmneaRcI z6zC1uGUtELMBmAYlhz9Xaq0A+Hb9STHoEAFOcgydir5t/5zH/6ZD/t4/wDYrmZperRm6PmzMtoZ GuseSvQRX8uTesFo8i6g4DMUC8gpianxAsBXv4ZCj3srCUalcaZPIjWFm9nGFo6PN63Jq/aqVSny yQtBQeFD9Jc1DnPjPOpdWjNOudOgkc3tmbxCBwUStFxINa1AatRtkZAnkaSCEVJfeXGQhNJlR+JA b63UcjWjEGLtkRGff9ibHclOWMXs3/OOn/TQf9Gf/M/NX2n/AA/H9Dlabq87/wCc5/8Apif+3p/2 J5qnKfKuKspi13yAIY1m8rTvKFIlkj1N0DMYwvJVaB+Px/FTfwxVJ9Zu9EuZY20nT5dPiVaSJLcf WSzV+1y9OKnypiqXYq/VTFXx9kkIvTbjToJWa+tDeREUWNZTCQa1ryAbFUTLe+X2UenpcqPxIJN0 WHInZqemOnzxVK8Vew/84+f9L/8A6NP+Z+Apem639mL5t/DAqVYqx+TSPORu5ZI/MMS2zSFooGsE Zkj5lgnMSry+EhK07V64qmemW2qwLINQvUvCxrGUhEHEeGzvXFUbirKMVdirsVdirsVeY/nV+Ttz +Y/6G9DVE039FfWeXOEzc/rHpUpR0px9H8cuw5eC2ucOJ5l/0J5qX/Uzw/8ASI3/AFVy780O5h4P m7/oTzUv+pnh/wCkRv8Aqrj+aHcvg+bv+hPNS/6meH/pEb/qrj+aHcvg+bv+hPNS/wCpnh/6RG/6 q4/mh3L4Pm+m8w294n/0Lvef9XuP/pHb/qpm2/lIfzXE/Lebv+hd7z/q9x/9I7f9VMf5SH81fy3m 7/oXe8/6vcf/AEjt/wBVMf5SH81fy3m7/oXe8/6vcf8A0jt/1Ux/lIfzV/LebNvy1/Lubyd+kfVv lvPr3o04xmPj6PqeLNWvqZiarU+LW1U3YsfDbG/z7/I66/NL9BfV9Xj0r9D/AFrl6kJm9T616NKU ePjx9D8cxG15L/0I5qv/AFN0H/SE/wD1WxV3/Qjmq/8AU3Qf9IT/APVbFXf9COar/wBTdB/0hP8A 9VsVd/0I5qv/AFN0H/SE/wD1WxV9aYq8Z/6F+vP+r1H/AMiG/wCa8Nq7/oX68/6vUf8AyIb/AJrx tXf9C/Xn/V6j/wCRDf8ANeNq7/oX68/6vUf/ACIb/mvG1Zn+XP5fzeUP0h6l6t59e9GnGMx8fS5+ LNWvqYFZTfWRuQgD8eNe1euKoT9CP/v0fd/birv0I/8Av0fd/birv0I/+/R939uKu/Qj/wC/R939 uKprirsVdirsVdirzv8AN384rb8uP0T62lvqX6V+sceEwh4fV/SrWqPXl634ZbixcdsJz4XnX/Q4 em/9SxN/0lr/ANUsu/Knva/G8nf9Dh6b/wBSxN/0lr/1Sx/KnvXxvJ3/AEOHpv8A1LE3/SWv/VLH 8qe9fG8nf9Dh6b/1LE3/AElr/wBUsfyp718byfROYje8g/6GIs/+rHJ/0kL/ANU82f8AJp/nOL+Z 8nf9DEWf/Vjk/wCkhf8Aqnj/ACaf5y/mfJ3/AEMRZ/8AVjk/6SF/6p4/yaf5y/mfJ3/QxFn/ANWO T/pIX/qnj/Jp/nL+Z8mX/l9+YkPnH6/6di1n9R9GvKQScvW5+CrSnp5janTeFW92248nEkH52fnj a/lb+hvX0iTVf0x9Z4+nMIfT+q+lWtUk5cvX/DMVteX/APQ8elf9SjP/ANJqf9UcVd/0PHpX/Uoz /wDSan/VHFXf9Dx6V/1KM/8A0mp/1RxV3/Q8elf9SjP/ANJqf9UcVfT+KvJf+hgbP/qyyf8AI9f+ aMNK7/oYGz/6ssn/ACPX/mjGld/0MDZ/9WWT/kev/NGNK7/oYGz/AOrLJ/yPX/mjGlZb5B/MCHzf 9e9Oyaz+o+lXlIJOXq8/BVpT08Cskvb0WoQlOXKvenTFUL+m1/30f+C/sxV36bX/AH0f+C/sxV36 bX/fR/4L+zFXfptf99H/AIL+zFUzxV5V54/NDznomqaha6bpMNxBazQQxO8VwzFZVUtKSrIhVamv xDM2OngYcV7tEshBqk48q/mD5h1HR0ubrQLi7nPqH6xYxmK3bjNLEiqt06y1pEC2xA5A9Mx5QAPN sEizTTLya8so7ia0lspX5crafj6i8WKgngWX4gOQ36HffKyGYROBXzb/AM5j/wDTIf8Abx/7FczN L1aM3R89aNZQXuoR288npRuGq4IHQEjrmdCIJouOS9HuPyt8qJ5euLxNTkF3ESounngW1Qi6Fv8A vEZFlpwPP4ST7UyicyJUOX7GwR2YRqHl3S7XT5bqHX7K8mj48bWITh3BYL8PONelSd6bD5VkJHuY kJDkkP0lzUOc+M86l1b0Xy5+X3ljUfK9nql1qEsd7cLK8kCywqq+ncekq8WVpKsu/TMHLqZxmQBt +xvjjBFpHrXlTSLTUWgi1WK1i+CkV03qSjlGrsSYFZKfEQOnh1y7HmkRy/HxYSgAebHb62itrp4Y rhLpF4kTxcuJ5KD+0Adq0Pvl8TYYEPX/APnHT/poP+jP/mfms7T/AIfj+hydN1ed/wDOc/8A0xP/ AG9P+xPNU5T5n8uada6lrNvZXcpgt5efOUEAjijMN226jLcEBOYB5MJyIFh6ze/kv5Ih0W/mj1mX 61a+qovXuLZbRHjmES+qjRpIVINfgYn/ACQMc0BGVBMCSN3mt/5Y0q20ia/h8x2F3PEV4WMYuBNI C4Q8ecaiory+Knwita0BqZMexV+qmKvj7JIeg+WvIflfUvL9jfXeoSxXtykzyxLJCqJ6M/pBaMGe rqQRtgVKNd8p6TZ6gIINUit4yFPp3Th5BUAkkwKyU8OmKsdv7aK2unhiuEuo1pSeLlxNQD+0Adq0 OFXrP/OPn/S//wCjT/mfgKXput/Zi+bfwwKwP8zPM2r+WPJGpa5o9mNQ1Gz9H0LRldw/qTxxNVY6 OeKOW28MVefeVfzh89alqEEGoaZaxBrsWzQRW12skyfHykgaR/hA4f7sQD3rtir0ew8z6tdapDZy +XL+0glB53kpg9OOi8hy4yMd/s7VNT06kKshxVlGKuxV2KuxV2KvCP8AnKLyT5r8z/4Z/QGmTaj9 T+vfWfRAPD1fq/CtSPtcG+7MnTTEbtpyxJqnhH/Kk/zX/wCpZvP+BX/mrMrxo97VwHud/wAqT/Nf /qWbz/gV/wCasfGj3rwHud/ypP8ANf8A6lm8/wCBX/mrHxo968B7nf8AKk/zX/6lm8/4Ff8AmrHx o968B7n3dmsct8s/8qz8+f8AVkufuH9c6L81j/nB1/hS7nf8qz8+f9WS5+4f1x/NY/5wXwpdzv8A lWfnz/qyXP3D+uP5rH/OC+FLud/yrPz5/wBWS5+4f1x/NY/5wXwpdz1P8jvLOvaJ+mv0tZSWf1j6 r6PqADlw9blShPTkM12vyxnw8Jvn+hyMECLthH/OXnkDzl5u/wAJ/wCG9Jn1T6l+kPrfoAH0/V+r enyqR9r02p8s1zkPnX/lQP5yf9Snff8AAr/zVirv+VA/nJ/1Kd9/wK/81Yq7/lQP5yf9Snff8Cv/ ADVirv8AlQP5yf8AUp33/Ar/AM1Yq/RTFXzF/wAq488/9Wa4+4f1ySHf8q488/8AVmuPuH9cVd/y rjzz/wBWa4+4f1xV3/KuPPP/AFZrj7h/XFXp/wCSnlvXdF/TP6VspLT6x9W9H1ABy4erypQ9uQwF LP8AVoJpVj9NS1Ca0+jAqXfULz/fTYq76hef76bFXfULz/fTYq76hef76bFWQYq7FXYq7FXYqknm Xzt5U8sfVv0/qcOnfXOf1b1iRz9LjzpQH7PNfvyUYGXJBkBzSP8A5Xb+VH/UzWf/AATf805LwZdz HxB3u/5Xb+VH/UzWf/BN/wA04+DLuXxB3u/5Xb+VH/UzWf8AwTf804+DLuXxB3u/5Xb+VH/UzWf/ AATf804+DLuXxB3s3ytmxn/lZnkP/q9W/wB5/pmR+VyfzS1+LHvd/wArM8h/9Xq3+8/0x/K5P5pX xY97v+VmeQ/+r1b/AHn+mP5XJ/NK+LHvd/yszyH/ANXq3+8/0x/K5P5pXxY96Z6L5m0HW/W/RN7H efV+PremSePOvGtQOvE5XkxSh9QplGQPJBea/P8A5N8o/Vf8SatBpf131Pqnrkj1PS4+pxoD9n1F r88rZJB/yv78mv8AqbLH/gm/5pxV3/K/vya/6myx/wCCb/mnFXf8r+/Jr/qbLH/gm/5pxV3/ACv7 8mv+pssf+Cb/AJpxVn+Ksb/5WR5G/wCrzb/ef6Yq7/lZHkb/AKvNv95/pirv+VkeRv8Aq82/3n+m Ku/5WR5G/wCrzb/ef6YqmWjeZNC1r1v0Vex3f1fj63pknjzrxrUd+JxVHyzwxU9RgtelcVU/r9n/ AL9XFXfX7P8A36uKu+v2f+/VxV31+z/36uKq+KpH5l846R5de1TUBMz3azPEsEZkPG3CtITToFDV y7FglO66MJTEeas/m/ymly9q2tWIuo3aOSA3MPNXQkMrLyqpBQg17jK+E9zLiCbZFLsVfNv/ADmP /wBMh/28f+xXMzS9WjN0fNmZbQnk/krzNF6AjsJblrhDLGtsjyngsgiLUVT8PqMFr0rt1yU48PNR ul2oaRqunGMahZz2hlBMYnjaPkBStOQFaVGQBBTSEwofpLmoc58Z51Lq098s+TNZ8xpdPp3pBLMx LM00gjFZyyxgV61K0yrLnjCr6s4wMuSE/wAN+YK0XTblgQCrLDIQwP2SppuDXanXJeLHvDHhPclu TQ9m/wCcdP8ApoP+jP8A5n5q+0/4fj+hytN1ed/85z/9MT/29P8AsTzVOU+VcVZJffl55utTbqun y3cl0jyRR2iPO5SNgjNRFPwhjSvSu2XZcEsfNhDIJckr1Ty/rukrC2q6dc2C3HL0DcwyRB+FOfHm Frx5CvhUZSzS/FX6qYq+PskhOfLvlTVNfFybExKtr6frNNIIx+9bigBPUltsVQ48u6+wqmnXLp2d YZCpB6EEChr2xVLsVew/84+f9L//AKNP+Z+Apem639mL5t/DAqVYqwvy1+b3knzBLMltcyWixAss 18n1aKQB2jPpPIQr0aNht4HwOKsn03W9G1QyjTb+3vTAQJvq8qS8C1acuBNK0P3YqjcVZRiqWaz5 Z0LWmhbVLRblrdZUhLMw4rOoWQfCV+0oyyGWUORYygDzV/0NpHIt9Rt+RZpCfSSvORi7t06sxqT3 OQsppGYEuxV82/8AOY//AEyH/bx/7FczNL1aM3R82ZltCZ3HmbX7hI0kvpQsUZhUIfT/AHZk9Xi3 DjyHqANv3A8MMjfNRsgri8vLnj9Ynkm4V4+o7PSvWlSeuClUcVfpLmoc58Z51Lq010PzRr+hCcaT eNai5MZn4qjcjESU+0G+yWOV5MUZ/ULZRmRyQj6pqTmrXcxqFG8jdFFFHX9kdMlwDuRZQuSQ9m/5 x0/6aD/oz/5n5q+0/wCH4/ocrTdXnf8AznP/ANMT/wBvT/sTzVOU+VcVTe+82+ZL5I0udQlZYkaN OFIzwdg7KxQKWBYA75ZkzSn9RtjGAHJAXV/fXZU3dzLcFa8TK7PStK05E+GVskPir9VMVfH2SQmO j+YdZ0f1f0bdNbesYzLxCnkYm5p9oHo2+KodtT1Fm5G6l5UArzYbDoOvQdsVQ2KvYf8AnHz/AKX/ AP0af8z8BS9N1v7MXzb+GBUqxVjmi/lz5I0W4NzpukQQTEceZ5SUHN5Ph9QuFPKZ9x4+GKp7bWNl aljbW8UBcAOY0VKhfsg8QOldsVVsVZRirsVdirsVdiqGvNM02+4fXbSG69Ovp+tGsnHlStOQNK0G EEhaQ3+GfLf/AFabP/pHi/5px4iig7/DPlv/AKtNn/0jxf8ANOPEVoO/wz5b/wCrTZ/9I8X/ADTj xFaDv8M+W/8Aq02f/SPF/wA048RWgmWBKD/Qmjf8sFt/yKT+mT8SXeUcId+hNG/5YLb/AJFJ/THx Jd5XhDv0Jo3/ACwW3/IpP6Y+JLvK8Id+hNG/5YLb/kUn9MfEl3leEK1tZWVry+rW8cHOnP00VK06 V4geORMieagKd9pOlahw+v2UF36VfT9eJJePKlePMGlaCuBKE/wn5V/6s1j/ANI0P/NOKu/wn5V/ 6s1j/wBI0P8AzTirv8J+Vf8AqzWP/SND/wA04q7/AAn5V/6s1j/0jQ/804qmuKoP9DaP/wAsNv8A 8ik/pirv0No//LDb/wDIpP6Yq79DaP8A8sNv/wAik/pirv0No/8Ayw2//IpP6YqrW9lZ23L6tBHD zpz9NFStOlaAeOKqrKrfaAPz3xVr0ov5F+4Yq70ov5F+4Yq70ov5F+4Yq70ov5F+4YquxVQvNQsL GNZL25itY2PFXmdYwT1oCxG+SjEnkEEgIFPNvlV7pLRNZsWupGVI7cXMJkZnpxVU5ciWqKDEwkOi 8QTXIpdir57/AOctdd1vSv8ACv6M1C5sPW+v+t9Wmkh58fq3HlwK1pyNK5laYA3bTmPJ89f4587f 9TBqX/SZP/zXmXwR7mjiKNtNe/M+8kSK01HW7iSWvppFNduzUHI8QrGu2+JxgCyFEis1HzJ+ZWmT rBqWqazYzsokWK5nuonKEkBgrsppVSK4BGJ5UkkoX/HPnb/qYNS/6TJ/+a8PBHuRxF+hOapzXyN/ ijzN/wBXe9/6SJf+as6bwodw+TreM96Is9Y86X0hjsr7UrqRRyZIZZ5GC1pUhSdt8jKGMcwFEpHv RMsv5jRRPLK+sRxRgtJIxugqqoqSSdgBkQMR/m/Yn1eaXf4o8zf9Xe9/6SJf+ass8KHcPkjjPe9d /IHVNTvv079eu5rr0/qnp+vI8nHl61ePImlaDNZ2jADhoVz/AEOTpyTbAf8AnNHzDr+j/wCDv0Rq d3p3r/pL1/qk8kHPh9V48/TZeXHkaV8c1jkvmX/lYXn7/qZdV/6Trn/mvFUxtPMH5v3rRpZ6l5gu Wl/ulhnvZC23L4QrGu2+TMJAXRpHEFPVPNH5r6ROsGq6vr2nzuvNIrq4vIXZKkcgsjKSKgiuQSgv +Vhefv8AqZdV/wCk65/5rxV+mmKvlD/E3mP/AKut5/0kS/8ANWSQr2msecbyQxWd7qNzKo5FIZZ5 GCggVopO2+NKiXk/MONGkdtXREBZ3Y3IAAFSST0xVL/8TeY/+rref9JEv/NWKvWPyI1PUr79OfXb ua69P6r6frSNJx5etWnImlaDAUvRtYkdFi4MVqTWhp4YFSz6xP8A78b/AII4qk83nryvBPJbz+Yb GK4hZkmhe8hV0ZCQyspeoK03BxVG6br9hqkTTaZqUV9EjcHktplmVWpXiSjMAaHFUX9Yn/343/BH FWSYqxnzz5ObzPb6fD66QrZXaXTrJH6iyKoIKEVHWuZGnz+GT5hryQ4mB6b+RWrWWvQal+mLeaNd Qs7+dZLZmci0eJykRaQqhb0ftUJ3OSyagSJ2YxxU9izFbnYq+bf+cx/+mQ/7eP8A2K5maXq0Zuj5 szLaHpXk/wDNuz0BbIPpRuDbGX1XDqGcSWrW6ipU/YLlhXJZZcUa8ljsUi/MfznZ+bNUsLu0tZrS KyslsylxN67MyzSzFgQqBV/fUCgbUyrHDhDKUrYlk2L9Jc1DnPjPOpdWyjyD5zTyre31y1u9wbu2 a2X05PTZCzBuYNG6UzH1GDxAB3Fsxz4WSaz+bdhqNjJb/oyWJzYS2MbLMAKyxGPm9EBYCteOUQ0Z ibvrbOWYHo8zzPaHs3/OOn/TQf8ARn/zPzV9p/w/H9Dlabq87/5zn/6Yn/t6f9ieapynyrir1/yV +e1n5ciskk0U3P1VWV2EiBm5Wr24+0jCimTkARmbk1fFDhroA0xxUbYv+aPn+x843WlSWdnPaR6b avbH6zOJ3dnnknJBCxhVHq8QoHbMJuYRir9VMVfH2SQyPyN5tXyxqd1etA1x9YtJLVQj+myl3Rg4 ajdOGIVk+pfmzY3tokP6NmiaK0e1jKzrQ8ozHV6ICR8VeOJ5qHmuKvYf+cfP+l//ANGn/M/AUvTd b+zF82/hgVKsVeFap/zjZe3nmDU9WTW4YjqOrT6qQLdxKqTM7LD6qyA/CZOoHXfFXo/5deSr3yta 6jHeXUF1JfXAnDW8PoBQsSx0arOXb4PtE+3bFWXYqyjFXYq7FXYq7FWG/mL+VHlfz/8Ao/8ATr3K /o31vq/1aRY/7/hz5ckev90tMsx5THkxlAHmw3/oVL8sP9+6l/0kR/8AVLLPzMmHgh3/AEKl+WH+ /dS/6SI/+qWP5mS+CHf9Cpflh/v3Uv8ApIj/AOqWP5mS+CHf9Cpflh/v3Uv+kiP/AKpY/mZL4Iey Zjtrzn/lQ3kj/fl7/wAjU/6p5nfyhk8mj8vF3/KhvJH+/L3/AJGp/wBU8f5QyeS/l4u/5UN5I/35 e/8AI1P+qeP8oZPJfy8Xf8qG8kf78vf+Rqf9U8f5QyeS/l4sj8n+Q9D8p/W/0W0zfXfT9b13D/3X Ljxoq/78OUZtRLJV9GyGMR5JR+aH5NeUfzJ/Rn+IXu0/RXr/AFX6pKsVfrPp8+fJJK/3K0yhmwT/ AKE2/KP/AH9qv/STF/1RxV3/AEJt+Uf+/tV/6SYv+qOKu/6E2/KP/f2q/wDSTF/1RxV3/Qm35R/7 +1X/AKSYv+qOKvdcVee/8qN8l/z3n/I1P+aMNq7/AJUb5L/nvP8Akan/ADRjau/5Ub5L/nvP+Rqf 80Y2rv8AlRvkv+e8/wCRqf8ANGNqyHyl5G0Xyt9b/RjTN9c9P1vWcP8A3XLjSir/ADnAqd3NpFcB RJX4a0oadcVQ/wCh7Txf7x/TFXfoe08X+8f0xV36HtPF/vH9MVd+h7Txf7x/TFUdirsVdirsVdir yT8/Pzc8yfl5+gv0LbWdx+lPrf1j64kr8fq/o8OHpyw0r6xrWuX4MQndteSZi8j/AOhuPzI/6tuj /wDIi6/7Kcv/ACsfNq8Yu/6G4/Mj/q26P/yIuv8Aspx/Kx818Yu/6G4/Mj/q26P/AMiLr/spx/Kx 818Yu/6G4/Mj/q26P/yIuv8Aspx/Kx818YvrfMFyXgH/AEMB5y/5Y9O/5FT/APVbN1/J2PvP4+Dh fmJeTv8AoYDzl/yx6d/yKn/6rY/ydj7z+Pgv5iXk7/oYDzl/yx6d/wAip/8Aqtj/ACdj7z+Pgv5i Xk7/AKGA85f8senf8ip/+q2P8nY+8/j4L+Yl5PQPyo/MDWfN36U/SUNtD9R9D0vqyutfV9Tly5vJ /vsUpmFrNNHFVXu34chldsU/5yQ/OvzV+WX+Hf0Da2Nz+l/rn1n69HNJx+rehw4elLDSvrNWte2Y Tc8V/wCh1fzT/wCrVof/AEj3n/ZXirv+h1fzT/6tWh/9I95/2V4q7/odX80/+rVof/SPef8AZXir v+h1fzT/AOrVof8A0j3n/ZXir7UxV4P/AMr583/8sen/APIuf/qthpDv+V8+b/8Alj0//kXP/wBV saV3/K+fN/8Ayx6f/wAi5/8AqtjSu/5Xz5v/AOWPT/8AkXP/ANVsaVnn5W+fdY82fpP9Iw28X1L0 PS+rq619X1OXLm8n++xTEpZjqN5LbBCgU8q15V7fIjAqB/TN1/Kn3H+uKu/TN1/Kn3H+uKu/TN1/ Kn3H+uKu/TN1/Kn3H+uKpziqW+ZzEPLerGWRYYhZ3HqTPyKovpNVm4hmoBvsCcnjNSB82MuRYH+V GmQWur3skTW442yQSLAlyrF0epZ/Wt7VP+BGZWqzCQA35/jqWrFCi9OzCb3Yq+bf+cx/+mQ/7eP/ AGK5maXq0Zuj5szLaHo35YTyW2m30p0n9JRys8cZaa2hCuIxU0mZHJHIfZp/SQyiIq6Xht5/eAi7 nDCjCRqioNDyPcEj7sBNqo4FfpLmoc58Z51Lq0ZoxYavYlUMjC4i4otKseYoBUgVPucjP6SmPNnn 5oa7+ktNtl/0kg3TTJ9YMHBVZKBU9Ke5f79sw9Jj4SeXLz/UG7LKw83zOaHs3/OOn/TQf9Gf/M/N X2n/AA/H9Dlabq87/wCc5/8Apif+3p/2J5qnKfKuKvQ/y4sNXm0meW0gkaE3O8yS20QJiVC60uJ4 OgkU9CN8zNNqBAUb5/jq05MZkWDaojpqd2ki8XWaQOtVahDkEVUsp+g0zFkbJLaBshcil+qmKvj7 JIRekFhq1kVQyMJ4uKLSrHmNhUgVPucVZ7+ZWvrqOiwpS5PqXguIzOYOKIYmXiojnuX+/bAFeb4V ew/84+f9L/8A6NP+Z+Apem639mL5t/DAqVYq8Z/OzytHq3mbTbqRbG4EdjJb/Vb36yKGWT4XBt7O 8NK7faQ/wVeo+UrVrPyro1ozrI1vY20TSIGCsUhVaqHCNQ0/aUH2xVNcVZRirsVdirsVdiryT8/P yj8yfmH+gv0Lc2dv+i/rf1j648qcvrHo8OHpxTVp6JrWmX4MohdteSBk8j/6FH/Mj/q5aP8A8j7r /smy/wDNR82rwS7/AKFH/Mj/AKuWj/8AI+6/7JsfzUfNfBLv+hR/zI/6uWj/API+6/7JsfzUfNfB Lv8AoUf8yP8Aq5aP/wAj7r/smx/NR818EvrfMFyXgH/Qv/nL/ls07/kbP/1Rzdfyjj7j+Pi4X5eX k7/oX/zl/wAtmnf8jZ/+qOP8o4+4/j4r+Xl5O/6F/wDOX/LZp3/I2f8A6o4/yjj7j+Piv5eXk7/o X/zl/wAtmnf8jZ/+qOP8o4+4/j4r+Xl5PQPyo/L/AFnyj+lP0lNbTfXvQ9L6sztT0vU5cuaR/wC/ BSmYWs1MctVezfhxmN2xT/nJD8lPNX5m/wCHf0DdWNt+iPrn1n69JNHy+s+hw4elFNWnotWtO2YT c8V/6Eq/NP8A6uuh/wDSRef9kmKu/wChKvzT/wCrrof/AEkXn/ZJirv+hKvzT/6uuh/9JF5/2SYq 7/oSr80/+rrof/SRef8AZJir7UxV4P8A8qG83/8ALZp//Iyf/qjhtDv+VDeb/wDls0//AJGT/wDV HG1d/wAqG83/APLZp/8AyMn/AOqONq7/AJUN5v8A+WzT/wDkZP8A9UcbVnn5W+QtY8p/pP8ASM1v L9d9D0vq7O1PS9Tly5pH/vwUxKWY6jZy3IQIVHGteVe/yBwKgf0NdfzJ95/pirv0NdfzJ95/pirv 0NdfzJ95/pirv0NdfzJ95/piqc4q7FXYq7FXYqw38xfzX8r+QP0f+nUuW/SXrfV/q0ayf3HDny5O lP71aZZjxGXJjKYHNhv/AENb+WH++tS/6R4/+quWflpMPGDv+hrfyw/31qX/AEjx/wDVXH8tJfGD v+hrfyw/31qX/SPH/wBVcfy0l8YO/wChrfyw/wB9al/0jx/9Vcfy0l8YPZMx215z/wAr58kf77vf +RSf9VMzv5PyeTR+Yi7/AJXz5I/33e/8ik/6qY/yfk8l/MRd/wAr58kf77vf+RSf9VMf5PyeS/mI u/5Xz5I/33e/8ik/6qY/yfk8l/MRZH5P8+aH5s+t/otZl+pen63roE/veXHjRm/32cozaeWOr6tk MglySj80Pzl8o/lt+jP8Qpdv+lfX+q/VIllp9W9Pnz5PHT++WmUM2Cf9Dk/lH/vnVf8ApGi/6rYq 7/ocn8o/986r/wBI0X/VbFXf9Dk/lH/vnVf+kaL/AKrYq7/ocn8o/wDfOq/9I0X/AFWxV7rirz3/ AJXl5L/kvP8AkUn/ADXhpXf8ry8l/wAl5/yKT/mvGld/yvLyX/Jef8ik/wCa8aV3/K8vJf8AJef8 ik/5rxpWQ+UvPOi+afrf6MWZfqfp+t6yBP73lxpRm/kOBU7ubuK3CmSvxVpQV6Yqh/0xaeD/AHD+ uKu/TFp4P9w/rirv0xaeD/cP64q79MWng/3D+uKo7FVskkcUbSSMEjQFndiAqqBUkk9AMVSby1rF 9qAukvjaiaGQiIW0qOzQn7DvGry8CRT9s/1lIUgFO8il2Kvm3/nMf/pkP+3j/wBiuZml6tGbo+bM y2hO/LujWeoJdNeC7VEjb6tLawtPWYCvAqFodt/tr/SMjSQElZWVirAhgaEHYgjJIaxV+kuahznx nnUurbRGdgiAszEBVAqST0AGKpjrGnW9oLdoBOBIn74TIyhZAd1VmWPlt/k5CEiebKQpLcmxezf8 46f9NB/0Z/8AM/NX2n/D8f0OVpurzv8A5zn/AOmJ/wC3p/2J5qnKfKuKsh8q+X7TVUvWvEvQiQsL OSzgkn5XIAKxlVRgag71dcVSB0dHZHUq6khlIoQRsQQcVW4q/VTFXx9kkNqrOwVQWZjRVG5JPQAY qmOq6dBaw20sAn4yp++M0bKok7qjlU5fdiqW4q9h/wCcfP8Apf8A/Rp/zPwFL03W/sxfNv4YFSrF Ul8yazd6att9Ta0aV5FE0V3MkA9I9WDswp9Ct8sVTlHR0V0YMjAMrKagg7ggjFW8VZRiqnc21vdW 8ttcxrNbzo0c0LgMro44srA7EEGhxVQstI0mxd3srKC1eSvqNDEkZapqalQK1O+EklFIvAl2Kvnv /nLXQtb1X/Cv6M0+5v8A0fr/AK31aGSbhy+rceXANSvE0rmVpiBdtOYcnz1/gbzt/wBS/qX/AEhz /wDNGZfHHvaOEo2y0H8z7GF4LLTdatYZDWSOGC7jViafaCqK/ZH3YDKJ7k0UJJ5K89SyNJJoOqPI 5LO7WlwWZiakklNycPHHvRwlb/gbzt/1L+pf9Ic//NGPHHvXhL9Cc1Tmvkb/AAv5m/6tF7/0jy/8 0503iw7x83W8B7l0flvzTHIskelXyOhDIwt5QQRuCDxwHLDvC8J7le50nzvdIqXVnqc6Luqyx3Dg ECgoGB7YBPGORj9ikS80N/hfzN/1aL3/AKR5f+acl4sO8fNeA9z138gdL1Ox/Tv160mtfU+qen68 bx8uPrV48gK0qM1naMweGjfP9Dk6cEWwH/nNHy9r+sf4O/RGmXeo+h+kvX+qQST8Of1Xjz9NW48u JpXwzWOS+Zf+Ve+fv+pa1X/pBuf+aMVTGw8v/m9p1ubbT9M1+ztyxdoreC9iUswUFiEVak8F+7FU DN5C/MSaV5pvLurySyMXkkezuWZmY1LMSlSScVWf8q98/f8AUtar/wBINz/zRir9NMVfKH+GfMf/ AFarz/pHl/5pySF0Xl7zRFIksemXqSRsGR1glBDA1BB49sVV7nS/Ol0ix3NpqU6LTikkc7gUFBQM DiqG/wAM+Y/+rVef9I8v/NOKvWPyI0zUrH9OfXbSa19T6r6frRtHy4+tWnICtKjAUvRtYjd1i4KW oTWgr4YFSz6vP/vtv+BOKoO88vadfTJPe6bFdTRU9KSaBZGWhqOJZTTfwxVEw2JghSGG39KGJQkU SJxVVUUVVUCgAHQYqv8Aq8/++2/4E4qyTFXYq7FXYq7FUFqeu6JpXp/pTULaw9bl6P1maOHnxpy4 8ytachWmEAnkglA/458k/wDUwab/ANJkH/NeHgPcvEHf458k/wDUwab/ANJkH/NePAe5eIO/xz5J /wCpg03/AKTIP+a8eA9y8Qd/jnyT/wBTBpv/AEmQf8148B7l4gneRSln+KPLP/V3sv8ApIi/5qyz wp9x+THjHe7/ABR5Z/6u9l/0kRf81Y+FPuPyXjHe7/FHln/q72X/AEkRf81Y+FPuPyXjHe7/ABR5 Z/6u9l/0kRf81Y+FPuPyXjHeirLVNMvuf1G7huvTp6noSJJx5Vpy4k0rQ5GUCOYpIIKhq3mHQNH9 L9L6naad6/L0Prc8cHPhTlw9Rl5ceQrTxyKUv/5WH5A/6mbSv+k62/5rxV3/ACsPyB/1M2lf9J1t /wA14q7/AJWH5A/6mbSv+k62/wCa8Vd/ysPyB/1M2lf9J1t/zXirIMVS3/E3lv8A6utn/wBJEX/N WKu/xN5b/wCrrZ/9JEX/ADVirv8AE3lv/q62f/SRF/zVirv8TeW/+rrZ/wDSRF/zViqJs9T02+5/ UruG69OnqejIsnHlWleJNK0OKoh5I0pzYLXpU0xVb9Yt/wDfqf8ABDFXfWLf/fqf8EMVd9Yt/wDf qf8ABDFXfWLf/fqf8EMVX4q7FUPNqFjDPFbzTxxzTkiBGYAuVNCFr1NT0w0tojArsVfNv/OY/wD0 yH/bx/7FczNL1aM3R82ZltDaqzGiiv8At0xVXvdPvrCf0L2CS3moG9OVSrUPQ0PbADa0h8Kv0lzU Oc+M86l1bsVVorK7liklihd44RylZQSFU92p0GAyAWlHCr2b/nHT/poP+jP/AJn5q+0/4fj+hytN 1ed/85z/APTE/wDb0/7E81TlPlXFW6H7uuKoi/03UNOnEF9byW0xUOqSqVJVujCvUYqhsVfqpir4 +ySHYqrRWd1NHLJFEzpCoaVlBPFT3Ptiqjir2H/nHz/pf/8ARp/zPwFL03W/sxfNv4YFSrFXEgUB NK7D374qh7HUbC/iaWyuI7iNWKM8bBgGABKmnehGKojFWUYqleu+WND16OOLVrb61FGHCRlnUD1K Bj8BXf4clGRHJBFqGn+SfK2nxxR2mnokcMnrxRszyKsvELzo7MOQCjfEzJURCd5FLsVfNv8AzmP/ ANMh/wBvH/sVzM0vVozdHzZmW0Mok/MzzvJavaNqZ+qurIYRFDx4OvBlHwVA47ZDw4suIpDqGp3+ oyrLezNM6IscdaAKiiiqqigAHtkgKYkoXCr9Jc1DnPjPOpdWjtK1rUtKleWwm9GVwFZwqsaAhqfE D3GRnAS5pEiOStd+Z9duyzT3bMzoYmYKikoTUrVQDTIjFEcgkzJSvLGL2b/nHT/poP8Aoz/5n5q+ 0/4fj+hytN1ed/8AOc//AExP/b0/7E81TlPlXFWVS/mh56lsUsG1RvqcahI4BHCAqiMRUU8Kj4BT riqQ6lq+panJHJfTtO0KCKEGgVEG4VFUBVFTXYYqg8Vfqpir4+ySEbpmsalpcjyWExgkkAVnAUmi sGA3B7jFVW58x63cs7TXTM0imN2AVSUJqVqoGxxVLcVew/8AOPn/AEv/APo0/wCZ+Apem639mL5t /DAqVYqxpPy38lLctdDTVNy7tI8pklqXdy7E/HT7TnFU60/StP05ZVs4Fi9ZzJMwqWdz+0zMSxPz OKovFWUYq7FXYq7FXYqwj8y/+VUf7jv8f/U/93fo365y/wCK/W4cf9hXLMfF/CwlXVhH/WKX/an/ AOSmW/vfNj6Hf9Ypf9qf/kpj+9819Dv+sUv+1P8A8lMf3vmvod/1il/2p/8Akpj+9819D27MZteZ /wDIBv8AtXf8Pmf/AIT5uP8Au/J3/IBv+1d/w+P+E+a/u/J3/IBv+1d/w+P+E+a/u/J3/IBv+1d/ w+P+E+a/u/Jk3kv/AAH/AKZ/hP6v/uv679Wr/l+nyr/sqZRn8Tbjtshw/wAKTfmv/wAqa/3F/wDK yfqP+7/0V9e5f8V+vw4/886/RmO2PP8A/rDb/tR/8lcVd/1ht/2o/wDkrirv+sNv+1H/AMlcVd/1 ht/2o/8Akrir3/FXm/8AyA3/ALV//D4Vd/yA3/tX/wDD4q7/AJAb/wBq/wD4fFXf8gN/7V//AA+K sk8nf4G/0z/C31f/AHX9c+r1/wAr0+Vf9lTAqe3X1Si/WONN+PLFUP8A7h/8j8cVd/uH/wAj8cVd /uH/AMj8cVd/uH/yPxxV/9k= + + + + uuid:5df67309-2f4c-a242-918b-26e4d1ce0a07 + xmp.did:2880c794-5834-4a85-b00d-755386de0bd2 + uuid:5D20892493BFDB11914A8590D31508C8 + proof:pdf + + uuid:8cbad157-d7c2-5944-80bb-19d153ab7c6b + xmp.did:e242cb8d-0077-420a-a1a0-45b74eaca535 + uuid:5D20892493BFDB11914A8590D31508C8 + proof:pdf + + + + + saved + xmp.iid:49BD1761722068118C14E060EE0ABA0A + 2013-08-01T09:33:44-07:00 + Adobe Illustrator CS6 (Macintosh) + / + + + saved + xmp.iid:2880c794-5834-4a85-b00d-755386de0bd2 + 2013-10-24T12:26:47-07:00 + Adobe Illustrator CC (Macintosh) + / + + + + Print + Document + False + False + 1 + + 5.333361 + 2.666674 + Inches + + + + Cyan + Magenta + Yellow + Black + + + + + + Default Swatch Group + 0 + + + + White + PROCESS + 100.000000 + RGB + 255 + 255 + 255 + + + R=15 G=38 B=72 + PROCESS + 100.000000 + RGB + 15 + 38 + 72 + + + R=73 G=103 B=169 + PROCESS + 100.000000 + RGB + 72 + 102 + 168 + + + R=109 G=110 B=113 + PROCESS + 100.000000 + RGB + 109 + 110 + 112 + + + R=0 G=0 B=0 + PROCESS + 100.000000 + RGB + 0 + 0 + 0 + + + R=225 G=225 B=225 + PROCESS + 100.000000 + RGB + 225 + 225 + 225 + + + + + + + Adobe PDF library 10.01 + + + + + + + + + + + + + + + + + + + + + + + + + endstream endobj 3 0 obj <> endobj 8 0 obj <> endobj 9 0 obj <> endobj 10 0 obj <> endobj 11 0 obj <> endobj 12 0 obj <> endobj 13 0 obj <> endobj 14 0 obj <> endobj 15 0 obj <> endobj 16 0 obj <> endobj 57 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.002 192.001]/Type/Page>> endobj 58 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.002 192.0]/Type/Page>> endobj 59 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.002 192.001]/Type/Page>> endobj 60 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.002 192.0]/Type/Page>> endobj 61 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.002 192.001]/Type/Page>> endobj 62 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.002 192.001]/Type/Page>> endobj 63 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.002 192.0]/Type/Page>> endobj 64 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.002 192.001]/Type/Page>> endobj 352 0 obj <>stream +H‰œWK®$¹ Ü×)òO-‰ÔoëöÀ«†axáýíŸqûí¿·¸ÅMº†ó–FÆÚþü÷í×Û/_°ô“o”|£?`½lÖš·Ã¢m¿Û~‘/øcK6L[ -WÙRŠ!I©Ûû7³ûv{‹¡Šlë/…ZÆŽrȃ{¼ß0¹lû<mû2ò_ú´Ðµƒ¸Ûø¬/÷ñüµWýzûÇÉõ$êØR,¡”ZN®7~?ÿÌõ®w”CÑâ®KÈÊÙ¼ÚîFþk®s·˜›Ïµ» gu.÷±ï{éºfœuA b £5=»ÞŠ½¤™1·êËñÒs›u@Ëê&9´1üh¸šÏe›;ˆ» gó\ìcßõÚí´jÚF 2¼îj§j‘Æq¦´# ±ìïƒ7cÓÖü×<ç,-Ú67wwŸuÃ8ÃΙKϳ†:Šey®9KCßOæ®×^›VËðÚ¥\“39ëH¦ü’›cù¯s³~”šãÔÄaø’b +)ª¾¤¦<£f;R³½ ¦|OÍñ@M9PS^Qª¢’…9^¸ç™™ùÄÌz@¤ÓÎM9rSŒ^}‘¡“›Õè8÷ÌÄiqgfÿ3ÁŠ”Æ6RZò53åÄLqÒÉ™ÍT¶ Ìcž™ÙÌ“™zÁLû^zžÚ–Y ¦ôq.@:,8ö—ˆк¬G¶9ï YÎN#ÿµL™)ßêÚÇAÜm8+s¹ÝîšÐÍ^zJú *yI€É‚îèBTò]Tô(*u‰ +£Y·¹­­ÚŸßE‚òÌe =SÀËÌ,¿õjßwD Ø£m²°”†àyµï¦"r”ùaµæúS%/kÉùAQôE±¯wEGEGE©›ÿNEÑ)"só£¢Ô©(ù (ù¥¢j<øÄf¬Iú°¢j}>êÉ*׳Ö?¨‰ÕDNj¢‡:¯KSžh`)àwG½OʼžÄD]'ô.&uÛE†€¶Ëæg«¼øê{‘èYäg5á?Ñ1èXþ:žküêîtì?MÇ© ½³_ï×’O’/=–'·£€”1îBí–°ì®=j¨v`aG–•Ø<×S¬ë)Ö}÷œ}Ñî;À¡§êçx×{¼õ"Þy.‘~îõLw:_'H>%Èu¸õ# ²‡ûyŠ”·€lÔŸø,/=~”Ôã\cêÑgY)rª3ééeb´¶'‰-/»þ(±ë‹Ä>{ý³I {#¢õrÔƒPC*“UC^´ úl¶ÑØZ§™tŽßo%ð¤¡JY…ˆuw&V=G-4HZ ±C°Ð>P¼¥·9~¿¡+i"HjBÊNpCÊmþscF ÓTèø·>™~9D!íÖåke8XÓ†ÉéöÖ¬ +eŒ•òý€‚Õ1ú±&'œ[úQ¿²W‚¿€ÃHÉ܆mÃû$°ÌMð~ã ò‚}ªà¤§9F\*{/G|K*OÉë[NßùÝ¡•’ Aè#[9¦‹huERõŠoÛw2]ƒ²º'ëñJ £Ð ÈøØŠ Ìž3B4Rí«U(AÝùÊؘ´9Z•N=Í™3ÅËðá[jâY(»xÇÂ[QÑxö_×(”Ôv$!•¶áûQÛo¶~¥å9¶>+.„ÓËvÑEÝ=;˜ç‰1RЀÕTñIìþÛ0Ñ…™ãá3$K87´:vÀ1ó‰Þ6$P%ìµlsŽ>”_À>6bKë ¯<…ÿRwJjs¥Pë!¤õœoì|0Ó¿Ïw;êøë4Ç>F.‹ùK0ÃñõûóiHK¡¬!â5åS®©ð‚óæ‘Ÿ9§¡DÒw¸« +æ´{ãÛ(„‚†|~2YŸP +ö'¸Av¦Ö>ÇسQ/!îèž° ˜¶K²n²òæQi!Sp‘õØš§$ €Ï(ì“Ä‚—9¡tIyÓ™Ÿ7¬cÆdì¥Û‰ÕNÍœ ³q@Â$¶Ô¦Ð¼xe#…Gx_Á˜|ÊöT°|!ÄØ+™cÜÔ’ ‡Ñƒ$àÑWsÙÅve"”mZÑ-ωÀc«,<­x]9Ðr–e¤‡kU4ÙÉ^–£ßaÅ/O}8>Ô¶žUÞu˜8i,€¸f‹†CFQM¶{  ?@F\]¶Ìú#‘ks¥B½)ßᲆXøYÎÅÔ=¼Ë­Ï:«÷‡vCĸòD/nøp) ØéZzÛ#§ +òI¬—ÉÖö8.Ä2ÇÈ +/„1Çc»ÈöIÆÒ]@t…—ƒy¯ê&2$Ó\óÆDŠÍ*¦È¬ã“Óª‡-¯[>Ü5ÖÓ÷‰•Dö +'û—ÒÞÑîåÄè)XŸ|©kAÜâ8{²Øìô¦³w=·ùòyÃßÿb£Â endstream endobj 340 0 obj <> endobj 341 0 obj <> endobj 355 0 obj [/View/Design] endobj 356 0 obj <>>> endobj 353 0 obj [/View/Design] endobj 354 0 obj <>>> endobj 345 0 obj <> endobj 344 0 obj <> endobj 357 0 obj <> endobj 358 0 obj <>stream +%!PS-Adobe-3.0 %%Creator: Adobe Illustrator(R) 17.0 %%AI8_CreatorVersion: 17.0.0 %%For: (Brent Couchman) () %%Title: (Presto_FB_Lockups.ai) %%CreationDate: 10/24/13 12:52 PM %%Canvassize: 16383 %%BoundingBox: 541 -1415 3296 752 %%HiResBoundingBox: 541.443774954023 -1414.4341 3295.4546 751.572296928825 %%DocumentProcessColors: Cyan Magenta Yellow Black %AI5_FileFormat 13.0 %AI12_BuildNumber: 256 %AI3_ColorUsage: Color %AI7_ImageSettings: 0 %%RGBProcessColor: 0 0 0 (R=0 G=0 B=0) %%+ 0.427451014518738 0.431373000144959 0.443136990070343 (R=109 G=110 B=113) %%+ 0.059415001422167 0.150419995188713 0.282377988100052 (R=15 G=38 B=72) %%+ 0.882352948188782 0.882352948188782 0.882352948188782 (R=225 G=225 B=225) %%+ 0.284979313611984 0.402332186698914 0.662593245506287 (R=73 G=103 B=169) %%+ 1 1 1 (White) %%+ 0 0 0 ([Registration]) %AI3_Cropmarks: 541.443774954023 -286.4307 925.4458 -94.4301999999998 %AI3_TemplateBox: 3722.5 -1614.5 3722.5 -1614.5 %AI3_TileBox: 355.444787477011 -478.43045 1089.44478747701 97.5695500000002 %AI3_DocumentPreview: None %AI5_ArtSize: 14400 14400 %AI5_RulerUnits: 0 %AI9_ColorModel: 1 %AI5_ArtFlags: 0 0 0 1 0 0 1 0 0 %AI5_TargetResolution: 800 %AI5_NumLayers: 2 %AI9_OpenToView: 171.017193208682 806.529335912314 0.4653 1624 1073 26 0 0 46 64 0 0 0 0 1 0 1 1 0 0 %AI5_OpenViewLayers: 77 %%PageOrigin:3416 -2010 %AI7_GridSettings: 72 8 72 8 1 0 0.800000011920929 0.800000011920929 0.800000011920929 0.899999976158142 0.899999976158142 0.899999976158142 %AI9_Flatten: 1 %AI12_CMSettings: 00.MS %%EndComments endstream endobj 359 0 obj <>stream +%%BoundingBox: 541 -1415 3296 752 %%HiResBoundingBox: 541.443774954023 -1414.4341 3295.4546 751.572296928825 %AI7_Thumbnail: 128 104 8 %%BeginData: 16962 Hex Bytes %0000330000660000990000CC0033000033330033660033990033CC0033FF %0066000066330066660066990066CC0066FF009900009933009966009999 %0099CC0099FF00CC0000CC3300CC6600CC9900CCCC00CCFF00FF3300FF66 %00FF9900FFCC3300003300333300663300993300CC3300FF333300333333 %3333663333993333CC3333FF3366003366333366663366993366CC3366FF %3399003399333399663399993399CC3399FF33CC0033CC3333CC6633CC99 %33CCCC33CCFF33FF0033FF3333FF6633FF9933FFCC33FFFF660000660033 %6600666600996600CC6600FF6633006633336633666633996633CC6633FF %6666006666336666666666996666CC6666FF669900669933669966669999 %6699CC6699FF66CC0066CC3366CC6666CC9966CCCC66CCFF66FF0066FF33 %66FF6666FF9966FFCC66FFFF9900009900339900669900999900CC9900FF %9933009933339933669933999933CC9933FF996600996633996666996699 %9966CC9966FF9999009999339999669999999999CC9999FF99CC0099CC33 %99CC6699CC9999CCCC99CCFF99FF0099FF3399FF6699FF9999FFCC99FFFF %CC0000CC0033CC0066CC0099CC00CCCC00FFCC3300CC3333CC3366CC3399 %CC33CCCC33FFCC6600CC6633CC6666CC6699CC66CCCC66FFCC9900CC9933 %CC9966CC9999CC99CCCC99FFCCCC00CCCC33CCCC66CCCC99CCCCCCCCCCFF %CCFF00CCFF33CCFF66CCFF99CCFFCCCCFFFFFF0033FF0066FF0099FF00CC %FF3300FF3333FF3366FF3399FF33CCFF33FFFF6600FF6633FF6666FF6699 %FF66CCFF66FFFF9900FF9933FF9966FF9999FF99CCFF99FFFFCC00FFCC33 %FFCC66FFCC99FFCCCCFFCCFFFFFF33FFFF66FFFF99FFFFCC110000001100 %000011111111220000002200000022222222440000004400000044444444 %550000005500000055555555770000007700000077777777880000008800 %000088888888AA000000AA000000AAAAAAAABB000000BB000000BBBBBBBB %DD000000DD000000DDDDDDDDEE000000EE000000EEEEEEEE0000000000FF %00FF0000FFFFFF0000FF00FFFFFF00FFFFFF %524C45FD16FF280028052805280528052805280528052805FD04FF5A2F54 %2F542F542F542F542F542F542F5453FD04FF27FD11F8FD04FF7D5259527D %5259527D5259527D5259527D52FD05FFA8FFA8FFA8FFA8FFA8FFA8FFA8FF %A8FFA8FD16FF050500280005002800050006000500280028A8FFFFFF2F53 %2F532F532F532F532F2F2F532F532F54FD04FFFD12F8A8FFFFFFFD11527D %FD04FFA8A8A8FFA8FFA8FFA8FFA8FFA8A8A8FFA8A8FD16FF280528002805 %280028050505280528002827FD04FF542F542F532F5A2F542F5453542F54 %2F532FFD04FF27FD05F827FD0BF8FD04FF7D527D5252527D525352525253 %527D525252FD05FFA8FFA8FFA8A8A8FFA8FFA8FFA8FFA8FFA8FD05FFAF53 %A95A847E7EA8FD09FF00050005002E84537D7D5384050500050028A8FFFF %FF2F2F2F532F5AA87E7E847EA92F2F2F532F53A8FFFFFFFD05F8277D527D %7D527DFD06F8A8FFFFFFFD05527DA8A87DA87DA8FD0652A8FFFFFFA8FFFD %04A827FD05527DFFA8A8A8FD07FFA97E847EA8A8FD0AFF28052805280553 %7D7E7D7D52280528052828FD04FF5A2F542F542F7E7EA87E847E5A2F542F %5453FD04FF27FD05F852527D7D7D2727FD05F8FD04FF7D527D5253527D84 %A87DA87D7D527D525252FD05FFA8FFA8FFA87D527D527D7DFFA8FFA8FFA8 %FD06FFA8FD047DFD0BFF050600050006057D527D5305002800050028AFFF %FFFF2F2F2F532F2F2FA87E7E5A532F532F532F53A9FFFFFFFD07F852527D %52FD07F8A8FFFFFFFD0752A87D7D7DFD0752A8FFFFFFA8FFA8A8A8FFA87D %527D52A8A8FFA8A8A8FD17FF280028052800280528052805280028052805 %FD04FF5A2F542F542F542F5453542F542F542F5453FD04FF27FD11F8FD04 %FF7D5252527D5253527D5253527D5252525352FD05FFA8FFA8FFA8FFA8FF %A8FFA8FFA8FFA8FFA8FD16FF050500060005000500050005000500060028 %A8FFFFFF2F532F5329532F2F29532F2F29532F532953A9FFFFFFFD12F8A8 %FFFFFFFD1252A9FFFFFFA8A8A8FFA8A8A8FFA8FFA8FFA8A8A8FFA8A8FD16 %FF280028052800280528002805280028050605FD04FF5A2F542F542F542F %542F542F542F542F5453FD04FFFD12F8FD04FF7D5252527D5252527D5252 %527D5252525352FD05FFA8FFA8FFA8FFA8FFA8FFA8FFA8FFA8FFA8FD16FF %A8A8A8AFA8A8A8AFA8A8A8AFA8A8A8AFA8A9FD04FFA8FFA8FFA8FFA8FFA8 %FFA8FFA8FFA8FFA8FD05FFFD12A8FD04FFA8FFA8FFA8FFA8FFA8FFA8FFA8 %FFA8FFA8FDFCFFFDB1FF2E522E5228532E5228532E5228532E522853FD04 %FF595A537E535A537E535A537E535A537E537EA9FFFFFFFD112752FD04FF %7D7D537D7D7D537D7D7D537D7D7D537D7D7DA8FFFFFFA8FFA8FFA8FFA8FF %A8FFA8FFA8FFA8FFA8FD17FF280006000500060005000600050006000500 %FD04FF542F532F542F532F542F532F542F532F532FFD04FFFD12F8FD04FF %59FD1152FD05FFA8FFA8FFA8FFA8FFA8FFA8FFA8FFA8FFA8FD16FF050500 %280005002800050028000500280028A8FFFFFF2F532F532F532F532F532F %532F532F532953A9FFFFFFFD12F8A8FFFFFFFD115253A9FFFFFFA8A8A8FF %A8A8A8FFA8A8A8FFA8A8A8FFA8A8FD16FF28002805280528000600280528 %0528052805FD04FF5A2F542F542F542F542F542F542F542F5453FD04FF27 %FD11F8FD04FF7D5259527D525252535252527D5259527D52FD05FFA8FFA8 %FFA8FFFFFFA8FFFFFFA8FFA8FFA8FD06FFA8A884A87EA8FD0AFF05050028 %00052E7D52532E2E000500280028A8FFFFFF2F532F532F53597E597E595A %2F532F532F54FD04FFFD06F8275252522727FD06F8A8FFFFFFFD0652FD06 %7DFD05527DFD04FFA8A8A8FFA8FFFD057DA8A8FFA8FFA8A8FD07FFA8AFA8 %A8A8FD0AFF2805280028052E2E532E5228280528002827FD04FF542F542F %542F5A5A7E597E53532F542F532FFD04FF27FD05F8272752275227FD06F8 %FD04FF7D527D525252FD057D5952527D525252FD05FFA8FFA8FFA8A87DA8 %7DA87DFFA8FFA8FFA8FD16FF000500050028000500050005002800050028 %A8FFFFFF2F2F2F532F53292F292F29532F532F532F53A8FFFFFFFD12F8A8 %FFFFFFFD1252A8FFFFFFA8FFA8A8A8FFA8FFA8FFA8FFA8FFA8A8A8FD17FF %280528052805280528052805280528052828FD04FF5A2F542F542F542F54 %2F542F542F542F5453FD04FF27FD11F8FD04FF7D527D5259527D5259527D %5259527D525252FD05FFA8FFA8FFA8FFA8FFA8FFA8FFA8FFA8FFA8FD16FF %FD050005000000050000000500000005A9FFFFFF2F2F292F292F292F292F %292F292F292F292FA9FFFFFFFD12F8A8FFFFFF5252285252522852525228 %52525228525252A8FFFFFFA8FFA8A8A8FFA8A8A8FFA8A8A8FFA8A8A8FD17 %FF7D537D597D597D597D597D597D597D597D59FD04FFA97E847EA87E847E %A87E847EA87E847E847EFD04FF7D527D527D527D527D527D527D527D527D %52FD04FFA87DA87DA87DA87DA87DA87DA87DA87DA87EFD05FFA8FFFFFFA8 %FFFFFFA8FFFFFFA8FFFFFFA8FDFCFFFD9AFFA87D847DA87D847DA87D847D %A87D847DA87EFD04FFA984A9A8A984A9A8A984A9A8A984A9A8A984FD04FF %A8FD117DFD04FFFD12A8FD2CFFFD050005000000050000000500000005A8 %FFFFFF2F2F292F292F292F292F292F292F292F292FA8FFFFFFFD12F8A8FF %FFFF525228525252285252522852525228525252A8FFFFFFA8FFA8A8A8FF %A8A8A8FFA8A8A8FFA8A8A8FD17FF28052805280528002805280528052805 %2827FD04FF542F542F542F542F542F542F542F542F542FFD04FF27FD11F8 %FD04FF7D527D5253527D5253527D5253527D525252FD05FFA8FFA8FFA8FF %A8FFA8FFA8FFA8FFA8FFA8FD16FF05060005002800280028000500280005 %0028AFFFFFFF2F2F2F532F532F542F532F532F532F532F53A9FFFFFFFD12 %F8A8FFFFFFFD1252A8FFFFFFA8FFA8A8A8FFA8A8A8FFA8FFA8FFA8A8A8FD %06FF5A7E5259A8FFA8FD0AFF28002805282F5A7D84282E282E0528052805 %FD04FF5A2F542F5AA8A984A95A5A535A2F542F5453FD04FF27FD04F82853 %7D7DFD0427FD05F8FD04FF7D5252527DA8A9A8A87D7D527D5252525352FD %05FFA8FFA8FF272752527DFD04A8FFA8FFA8FD04FFA8532F282E7D7D7D59 %FD09FF05050005055453847D7D537D532800280028A8FFFFFF2F532F2F2F %FFA8A97E847E7E7E532F532953A9FFFFFFFD05F8545A7D7D7D275252FD05 %F8A8FFFFFFFD0552FFA8FFA8A87DA87D5352525253AFFFFFFFA8A8A8FFA8 %F8F827FD0552A8A8FFA8A8FD05FFA8A97EA8FD0DFF280028052828535259 %282805280528052805FD04FF5A2F542F5A7E7E5A8453542F542F542F5453 %FD04FF27FD04F8272E275227FD08F8FD04FF7D525952FD047DA85353527D %5259527D52FD05FFA8FFA8FFFD047DA8FFA8FFA8FFA8FFA8FD16FF050500 %280005000500050028000500280028A8FFFFFF2F532F53FD042F29532F53 %2F532F532F54FD04FFFD12F8A8FFFFFFFD11527DFD04FFA8A8A8FFA8FFA8 %FFA8FFA8FFA8A8A8FFA8A8FD16FF28000500280005002800050028000500 %0605FD04FF542F542F532F542F532F542F532F542F532FFFFFFFA8FD12F8 %FD04FF53FD1152FD05FFA8FFA8FFA8FFA8FFA8FFA8FFA8FFA8FFA8FD16FF %FD0428272E2828272E2828272E2828272EA8FFFFFF5354535A535A535A53 %5A535A535A535A535AA8FFFFFF2727F827F827F827F827F827F827F827F8 %27A8FFFFFF527D527D527D527D527D527D527D527D527DA8FFFFFFA8FFA8 %FFA8FFA8FFA8FFA8FFA8FFA8FFA8FDFCFFFD9BFFA8FFA8FFA8FFA8FFA8FF %A8FFA8FFA8FFA8FD05FFA8FFA8FFA8FFA8FFA8FFA8FFA8FFA8FFA8FD05FF %A8FFA8FFA8FFA8FFA8FFA8FFA8FFA8FFA8FD05FFA8FFA8FFA8FFA8FFA8FF %A8FFA8FFA8FFA8FD07FFA8FFFFFFA8FFFFFFA8FFFFFFA8FD19FF2E272E28 %28282E2828282E2828282E282828FD04FF5A535A535A535A535A535A535A %535A535A53FD04FF27F827F827F827F827F827F827F827F82727FD04FF7D %527D527D527D527D527D527D527D527D59FD05FFA8FFA8FFA8FFA8FFA8FF %A8FFA8FFA8FFA8FD16FF050500050005000500050005000500050028A8FF %FFFF2F532F2F29532F2F29532F2F29532F2F2954FD04FFFD12F8A8FFFFFF %FD1252FD04FFA8A8A8FFA8A8A8FFA8A8A8FFA8A8A8FFA8A8FD16FF280528 %002805280028052800280528002827FD04FF542F542F542F542F532F542F %542F542F532FFD04FF27FD11F8FD04FF7D527DFD07527D5252527D525252 %FD05FFA8FFA8FFA8FFFFFFA8FFA8FFA8FFA8FFA8FD06FF7EA8FD0EFF0005 %00050028282800050005002800050028A8FFFFFF2F2F2F532F537E7E2F2F %29532F532F532F53A8FFFFFFFD06F80028FD0AF8A8FFFFFFFD06527DA8FD %0A52A8FFFFFFA8FFA8A8A8FF5252A8FFA8FFA8FFA8A8A8FD07FF5A5AA87D %7E84FD0AFF2805280528055A5A59537D52280528052828FD04FF5A2F542F %5453FFAFA87E847E542F542F5453FD04FF27FD05F85A5452527D5227FD05 %F8FD04FF7D527D525352FFA8A87DA87D7D527D525252FD05FFA8FFA8FFA8 %27F8527D7D52FFA8FFA8FFA8FD06FF59FD04A8FD0BFF0506000500282F53 %282E2828002800050028AFFFFFFF2F2F2F532F5384A9595A535A2F532F53 %2F53A9FFFFFFFD06F8285300272727FD06F8A8FFFFFFFD06527DA87D7D52 %7DFD0652A8FFFFFFA8FFA8A8A8FF525252A87DA8A8FFA8A8A8FD17FF2800 %28052800280506002805280028052805FD04FF5A2F542F542F532F542F53 %2F542F542F5453FD04FF27FD11F8FD04FF7D5252527D525252535252527D %5252525352FD05FFA8FFA8FFA8FFA8FFA8FFA8FFA8FFA8FFA8FD16FF0505 %00050005000500050005000500050028A8FFFFFFFD042F29532F2F29532F %2F29532F2F2953A9FFFFFFFD12F8A8FFFFFFFD1252A9FFFFFFA8A8A8FFA8 %A8A8FFA8A8A8FFA8A8A8FFA8A8FD16FF2800280528052805280528052805 %28052805FD04FF5A2F542F542F542F542F542F542F542F5453FD04FF27FD %11F8FD04FF7D527D527D527D527D527D527D527D527D52FD05FFA8FFA8FF %A8FFA8FFA8FFA8FFA8FFA8FFA8FD16FFFD12A8FD04FFA8AFA8A9A8AFA8A9 %A8AFA8A9A8AFA8A9A8AFFD04FFA8A87DA8A8A87DA8A8A87DA8A8A87DA8A8 %A8FD04FFA8FFA8A8A8FFA8A8A8FFA8A8A8FFA8A8A8FD07FFA8FFFFFFA8FF %FFFFA8FFFFFFA8FDFCFFFD9DFF5359535352595353525953535259535352 %7DFD04FF7E7E5A7E7E7E5A7E7E7E5A7E7E7E5A7E7E7EAFFFFFFFFD105227 %52FD04FF7D7D7D7E7D7D7D7E7D7D7D7E7D7D7D7E7D7DFD04FFA8FFA8FFA8 %FFA8FFA8FFA8FFA8FFA8FFA8FD17FF060005000500050005000500050005 %000500FD04FF542F2F29532F2F29532F2F29532F2F292F2FFD04FFFD12F8 %FD04FF53FD1152FD05FFA8FFA8FFA8FFA8FFA8FFA8FFA8FFA8FFA8FD0CFF %A9FD09FF050500280005000500000005000500280028A8FFFFFF2F532F53 %2F53292F292F292F2F532F532953A9FFFFFFFD12F8A8FFFFFFFD085228FD %085253AFFFFFFFA8A8A8FFA8A8A8FFA8FFA8FFA8A8A8FFA8A8FD07FF84AF %A8A8A8FD0AFF28002805280528282E527D28280528052805FD04FF5A2F54 %2F542FFD057E53542F542F5453FD04FF27FD05F8272728275227FD06F8FD %04FF7D5259527D527D7D847DA87D7D5259527D52FD05FFA8FFA8FFA8A8FD %057DFFA8FFA8FFA8FD06FF5A53532E2E7DFD0AFF050500280005285A5AA8 %A87D000500280028A8FFFFFF2F532F532F2FA8FFA8FFA87E29532F532F54 %FD04FFFD06F8065A5AA87D7DFD06F8A8FFFFFFFD06527DFFA8FFA8A8FD05 %527DFD04FFA8A8A8FFA8FF52F8F8272752A8FFA8FFA8A8FD06FF7E535A28 %537DFD0AFF2805280028052F5A7EA8A859050528002827FD04FF542F542F %532FA9FFFFA9FF7E2F2F542F532FFD04FF27FD05F8285A7EA8A87DFD06F8 %FD04FF7D527D525252A8FD04FFA852527D525252FD05FFA8FFA8FFFF52F8 %27275252FFA8FFA8FFA8FD06FFA8A9A8A87DAFFD0AFF0005000500050528 %052E2E28002800050028A8FFFFFF2F2F2F532F2F537E535A535A2F532F53 %2F53A8FFFFFFFD07F827F8272727FD06F8A8FFFFFFFD07527D537D7D7DFD %0652A8FFFFFFA8FFA8A8A8FF7D7D7DA87DA8A8FFA8A8A8FD17FF28052805 %2805280028000500280528052828FD04FF5A2F542F542F542F532F542F54 %2F542F5453FD04FF27FD11F8FD04FF7D527D5259527D525252535259527D %525252FD05FFA8FFA8FFA8FD05FFA8FFA8FFA8FFA8FD16FF000500050005 %000500050005000500050028A9FFFFFF2F2F292F292F292F292F292F292F %292F2953A9FFFFFFFD12F8A8FFFFFFFD1252A8FFFFFFA8FFA8A8A8FFA8A8 %A8FFA8A8A8FFA8A8A8FD17FF7D537D597D537D597D537D597D537D597D53 %FD04FFA87E847E847E847E847E847E847E847E847EFD04FF7D527D525252 %7D5252527D5252527D525252FD04FFA87DA87DA87DA87DA87DA87DA87DA8 %7DA87DFD05FFA8FFFFFFA8FFFFFFA8FFFFFFA8FFFFFFA8FDFCFFFD9AFFA9 %FD11A8FD04FFAFA8FFA8AFA8FFA8AFA8FFA8AFA8FFA8AFA8FD04FFA8A8A8 %7DA8A8A87DA8A8A87DA8A8A87DA8A8FD05FFA8FFA8FFA8FFA8FFA8FFA8FF %A8FFA8FFA8FD2CFF000500050005000500050005000500050028A8FFFFFF %2F2F292F292F292F292F292F292F292F2953A8FFFFFFFD12F8A8FFFFFF52 %522E5252522E5252522E5252522E525252A8FFFFFFA8FFA8A8A8FFA8A8A8 %FFA8A8A8FFA8A8A8FD17FF280528052805280528052805280528052828FD %04FF5A2F542F542F542F532F542F542F542F5453FD04FF27FD11F8FD04FF %7D527D5259527D525252595259527D525252FD05FFA8FFA8FFA8FFA8FFFF %FFA8FFA8FFA8FFA8FD07FFA95A7EA8FD0BFF050600050028002829530505 %002800050028AFFFFFFF2F2F2F532F53295A84A853532F532F532F53A9FF %FFFFFD07F8002828FD08F8A8FFFFFFFD07527DA8A87DFD0752A8FFFFFFA8 %FFA8A8A8FFA8A827527DFFA8FFA8A8A8FD08FF7E7E5AA9FD0BFF28002805 %28002828857E2805280028052805FD04FF5A2F542F542F535AFFA87E2F54 %2F542F5453FD04FF27FD06F805532827FD07F8FD04FF7D5252527D52527D %FFA8A8527D5252525352FD05FFA8FFA8FFA8FF7D27277DFFFFA8FFA8FFA8 %FD07FFA9597EA8FD0BFF05050028000500282F530005000500280028A8FF %FFFF2F532F532F53295A84A9532F2F532F532953A9FFFFFFFD07F8000628 %FD08F8A8FFFFFFFD07527DA8A87DFD065253A9FFFFFFA8A8A8FFFD04A827 %527DFFA8A8A8FFA8A8FD07FFA87E7DFD0CFF280028052805282859532E05 %280528052805FD04FF5A2F542F542F5453A97E5A2F542F542F5453FD04FF %27FD06F8277D5227FD07F8FD04FF7D5259527D525259A87D7D527D525952 %7D52FD05FFA8FFA8FFA8FFA87D52A8FFFFA8FFA8FFA8FD07FFA884A8AFFD %0BFF0505002800050028282E0528000500280028A8FFFFFF2F532F532F53 %2F54537E2F532F532F532F54FD04FFFD07F8272727FD08F8A8FFFFFFFD07 %527D527DFD07527DFD04FFA8A8A8FFFD04A87DA8A8FFA8A8A8FFA8A8FD16 %FF280005002800060005000500280005000605FD04FF542F542F532F542F %2F29542F532F542F532FFFFFFFA8FD12F8FD04FF53FD055253FD0B52FD05 %FFA8FFA8FFA8FFA8FFA8FFA8FFA8FFA8FFA8FD16FF272805280528052805 %280528052805280528A8FFFFFF53532F542F532F542F532F542F532F542F %54A8FFFFFFF827F827F827F827F827F827F827F827F827A8FFFFFF525252 %7D5253527D5253527D5253527D527DA8FFFFFFA8FFA8FFA8FFA8FFA8FFA8 %FFA8FFA8FFA8FDFCFFFD9DFFA8FFFFFFA8FFFFFFA8FFFFFFA8FD07FFA9FF %FFFFA9FFFFFFA9FFFFFFA9FFFFFFA9FD07FFA8FFFFFFA8FFFFFFA8FFFFFF %A8FD07FFA8FFFFFFA8FFFFFFA8FFFFFFA8FFFFFFA8FD2DFF532853285228 %532852285328522853282E2EFD04FF7E537E537E5A7E537E5A7E537E5A7E %537E5AFD04FFFD1227FD04FFFD127DFD05FFA8FFA8FFA8FFA8FFA8FFA8FF %A8FFA8FFA8FD16FF000000050005000500050005000500050005A8FFFFFF %FD042F29532F2F29532F2F29532F2F2953FD04FFFD12F8A8FFFFFFFD1052 %2E52FD04FFA8A8A8FFA8A8A8FFA8A8A8FFA8A8A8FFA8A8FD16FF28052800 %2805280528052800280528002827FD04FF542F542F542F542F542F542F54 %2F542F532FFD04FF27FD11F8FD04FF7D527D5252527D5253527D5252527D %525252FD05FFA8FFA8FFA8FFA8FFA8FFA8FFA8FFA8FFA8FD08FF7DA8FD0C %FF00050005002800052E530005002800050028A8FFFFFF2F2F2F532F5329 %53597E29532F532F532F53A8FFFFFFFD08F82752FD08F8A8FFFFFFFD0852 %7D7DFD0852A8FFFFFFA8FFA8A8A8FFA8FF7D7DA8A8A8FFA8A8A8FD09FFA9 %A8FD0CFF2805280528052800532E2805280528052828FD04FF5A2F542F54 %2F542F7E59542F542F542F5453FD04FF27FD07F85227FD08F8FD04FF7D52 %7D5259527D527D7D7D5259527D525252FD05FFA8FFA8FFA8FFA8A87DFFA8 %FFA8FFA8FFA8FD07FFA87D7EA8FD0BFF050600050028002E535928050028 %00050028AFFFFFFF2F2F2F532F53295A7E7E53532F532F532F53A9FFFFFF %FD07F827525227FD07F8A8FFFFFFFD07527D7DA8FD0852A8FFFFFFA8FFA8 %A8A8FFA8A8527D7DFFA8FFA8A8A8FD08FFA8FFA8FD0CFF28002805280028 %282E282805280028052805FD04FF5A2F542F542F532F5A53542F542F542F %5453FD04FF27FD07F827F827FD07F8FD04FF7D5252527D5252527D537D52 %7D5252525352FD05FFA8FFA8FFA8FFA8A8A8FFA8FFA8FFA8FFA8FD16FF05 %0500060005000500050028000500060028A8FFFFFF2F532F532F532F2F29 %532F532F532F532953A9FFFFFFFD12F8A8FFFFFFFD115253AFFFFFFFA8A8 %A8FFA8A8A8FFA8FFA8FFA8A8A8FFA8A8FD16FF2800280006002800060028 %00060028000505FD04FF542F532F542F532F542F532F542F532F542FFD04 %FFFD12F8FD04FF7D525252535252525352525253FD0552FD05FFA8FFA8FF %A8FFA8FFA8FFA8FFA8FFA8FFA8FD16FF7E847DA87D847DA87D847DA87D84 %7DA87DA8FD04FF84A9A8A884A9A8A884A9A8A884A9A8A884A9FD04FFFD12 %7DFD04FFFD12A8FD06FFA8FFFFFFA8FFFFFFA8FFFFFFA8FDFCFFFD9DFF53 %595359525953595259535952595359527DFD04FFFD127EAFFFFFFFFD1252 %FD04FF7D7D7DA87D7D7DA87D7D7DA87D7D7DA87D7DFD04FFA8FFA8FFA8FF %A8FFA8FFA8FFA8FFA8FFA8FD17FF28000600050006000500060005000600 %0500FD04FF542F532F542F532F542F532F542F532F532FFD04FFFD12F8FD %04FF59FD1152FD05FFA8FFA8FFA8FFA8FFA8FFA8FFA8FFA8FFA8FD16FF05 %0500280005002800050028000500280028A8FFFFFF2F532F532F532F532F %532F532F532F532953A9FFFFFFFD12F8A8FFFFFFFD115253A9FFFFFFA8A8 %A8FFA8A8A8FFA8A8A8FFA8A8A8FFA8A8FD16FF2800280528052805280528 %05280528052805FD04FF5A2F542F542F542F542F542F542F542F5453FD04 %FF27FD11F8FD04FF7D5259527D525352595259527D5259527D52FD05FFA8 %FFA8FFA8FFFFFFA8FFA8FFA8FFA8FFA8FD06FFA8A9A8FF7DA8FD0AFF0505 %00280005052E2828522E000500280028A8FFFFFF2F532F532F532F54535A %7E5A29532F532F54FD04FFFD07F827F8F82727FD06F8A8FFFFFFFD07527D %527D7D7DFD05527DFD04FFA8A8A8FFA8FFA8A87DA852FD04A8FFA8A8FD06 %FFA8597D7EA8A8FD0AFF2805280028057D7D84535328280528002827FD04 %FF542F542F542F7E84A87E7E5A532F542F532FFD04FF27FD05F8527D7D27 %5227FD06F8FD04FF7D527D525252A8A8A87D7D7D52527D525252FD05FFA8 %FFA8FFA87D52FD047DFFA8FFA8FFA8FD06FFA8FD0FFF0005000500280005 %00280005002800050028A8FFFFFF2F2F2F532F532F532F5329532F532F53 %2F53A8FFFFFFFD12F8A8FFFFFFFD1252A8FFFFFFA8FFA8A8A8FFA8A8A8FF %A8FFA8FFA8A8A8FD17FF280528052805280528052805280528052828FD04 %FF5A2F542F542F542F542F542F542F542F5453FD04FF27FD11F8FD04FF7D %527D5259527D5253527D5259527D525252FD05FFA8FFA8FFA8FFA8FFA8FF %A8FFA8FFA8FFA8FD16FF000500050005000500050005000500050028A9FF %FFFF2F2F292F292F292F292F292F292F292F2953A9FFFFFFFD12F8A8FFFF %FFFD1252A8FFFFFFA8FFA8A8A8FFA8A8A8FFA8A8A8FFA8A8A8FD17FF532E %5352532E5352532E5352532E53525352FD04FF7E5A7E5A7E5A7E5A7E5A7E %5A7E5A7E5A7E7EFD04FF522752275227522752275227522752275227FD04 %FF7EFD117DFD05FFA8FFFFFFA8FFFFFFA8FFFFFFA8FFFFFFA8FD7FFFFF %%EndData endstream endobj 360 0 obj <>stream +%AI12_CompressedDataxœì½k“$¹u%ø ü?Ä~9»L9ÞwfÍ"#25\£$)ÍÃdcm¥îY£î*Zuµ´Ú_¿çœ ø#"2+»›;Õé+Ùé€Ãáà>Ͻø‹ÿí7¿ûÅñ«wÿøúán>Lñ§÷¯_}x÷þ—]=üê믿ûöÃ{^úÙo~pånF£ã¯ê½ázýþÛ7ïÞþRUª|äÝ?»ÿúí‡ÃéÝw_þá›Wo~øÙÏQõwo>|ý•¿yÿúÛï¾x¼ÿâ×ï¾üçïþøíÝ«7?GoçWÐÌÍéã_ºppþ—É~ó×lñêí¿¼úöÛ7ÿ/ës¨×îß}÷ö«7oÿîÿùå!Ewø…‹.‚oùP’G‹ÿøæ·¯¿½lvc(%¶gtO¼‹·ãÆtSäÝî.Žš¯Õ'tu~÷åwßàÕ~óþÝ—¯¿ýöôîëwï¿ýåáôo¯ÞþúÕïQóêð__ýõ»=ÜýêËžð±Òo¾~ïòÍ«ø•Ž¿rþ‹ûïÞ|ýÕß|÷Í?¾Æó)órøB]þý·è Ýò¿y¹|ñ«opåw¯?|À+àüÒ¿ý«ûí0pQåg¿ýóá¯ðïþ?Ìüªÿûa¾‹¾Ääf‡_µ„Ê+Á…æ×ð ¯ðRnmžËb`?nnèÉ9öå\½Í©á óVï].¸‚¿¢k­¡ûZ0e󯟷Õêð L{Kè ¿ÿÅ®ðYCò-VÞXý‹® +ïÙßó÷èÍ×ØJãk8×jäKarƒw5çV›ã•œ}jÁÇ”æìkao%ð-çÀ·ÌÍ:s*?ûÏxóáuïÞ¾î?üöõïßhO`¥þ·Ÿ÷I{ÿîß¼zÿÏßÞZZ¾f|NJ©~ѸÖf~1ýTëåï^óǯ±öµDÞ]ÂÂÌX˜éâÏÞËÊÚ&vK-±”Ùa à¿ù„˜°jÛVZ¹K¹áõíÇ[_ëÂ~ý/o^ÿë/óîík[½Ç÷~g;.Æy¶ßVóÛï¾~ýþïß¾ù`+òø«fË÷¯ß}õúk´_îüú•V­Š[[ƒ¿{õþ÷¯?`¾ûú»¢&u<Ûãׯþí5÷˜·üí_¿ý»wÿIctÅÝÍø…ùœkÆÒ¨s¾ÃR !5çƒæ;æ’}Ä—ÀDû¬§cwçØÇccqÛñ!|Äxv)X¿ÁüÛ÷o~ÿæí/A'2&vv³íοzÿæ«us Ä~©Ë»j_kÒÏÍ·—^±µQ²K;í%Wì ácøðúmÿþ 4§¿ÞŽù—yxûÕéÝ7œñoIC±¦ß‚˜|ýî÷V·ü·jpûw´/£¿¿ÀâøÍû7oÙçô7ª©_üæëïPõWïß}÷Ç_½ý§wÓÏŒ…ü§×_‚O`ý|uøÛüïøÌ@Äìðwï_}‰ð÷ÒŒà?¶»ß|ýêí«÷]Çvñ×oþ5¯0 ƒÝ†šõÚÇ;=¿þ'Pçõ^»úðö_^ýî¯×ëË•Wo¿:üçWïÿøñ®1ï7=èÏñÿ/x[¬¾?âãè]ÔäâÍŸi°©zÁƒ^}ø8Öë·_}»ôm]ûx¿û’kðýáþýwßþáðwïÞ}½t»¯Zzï—u•íÏønxû·oí]?©7¸|hÞOî)hýôPùSîýôêë¯ßüþý«?þáÍ—·p£~y’Õ}Ÿ…õoßü㻯ß|ûͺž6W~óêý‡7_~ýúwÿöí‡×ß¼xr_½‘{b?Ûæwÿúê×øõ›|ÿêý›×Ïî>NÀ?½yûÖþï¾£è²| wßü‘ðáwxõÇ×zxTËß-¦/@ú·Äý¿x†êƒÞ¿ÝÔÿÕûW_½C„ÿ÷oß¾úÿ÷ýÒÁý|ºu‘œøpÿÕôÓ¿ŸNõTP²J:ÅS@ñ(e>Í÷(('þUJE)(ù>¡D”p¦{ïPæûùøˆrF9©ð>ûiÇŠRP²JB‰(Å£¸ã|œÛc{hçvžðë„ÂGâN“ÜP +ËôáØmO¨¬P g6Wëãk]sÕ×¹<à=ÇJI%—óÞù˜[.9å˜}žÓC:§û)µTRN1…äâc<áÕ Ç õ"HœÃ>Ò}hЄ2döœôøh÷*‹‡Ôã!ýBNyÀG<º:AXJ­ •?Îg~Õ¹ÍÒ ^¯0ãÙýßþwY~—þ;ëÆíovUõß¿'ýßqÖ4ÌœÇóòû|ñûÅ~?^ü~ì¿ñoÚ\Ü6zä+Ù+ü‰þÇv¨Wˆ÷»rz²œŸ)k™¶ <>UÒüLqk™¶ ømÑ+äŒÕ\±¦ù«ûŒ5þˆ•ãŠÇšXùë¿bç±sOåŒ]ñˆÕã°CöIÂn)µbß±{Nõ<Õ‡úˆEÛ*b{A»ÄiºÏFÅ^>‚”œîÏ ,X˜'œl/è>Þi÷Úݼßz@(EÔ§õŽ¬«³è»›E½¼hYœ@Ö’È[±«¤% G¤gü9ŸP¹ÎÅŸ +g2ñùÎå Br&!:ò gÜ0Ïç”Çó#66ùƒIÐ'¼B<üÅ÷ïI[ïñ vªz-гG|(g›ÅvsÞí[ƒ”æïm61]˜Q6ƒ +]bš[Cóªäú˜³;=bX§“Ý„ïŸIDÛãú¨BB¡^ðœ’}KnnÐÑ +Ÿ §&P7£¦ ƒôëÈz³¹Äl®oöè'5hÙÑ{¨²¥yª|3´¡Pk†æjD] +oˆeçd +ðµJ+ |ÍÞ^Êú_MêÕxIÖ¤ƒ‡B—i>´zí2sP¦¾ø´ƒ¸ÿ_¡Ôàà ¿¸:;ö„'Ï~N~±÷9sÞå†v¥Ðt]Áx#IÄ)¾Eç +f`Nµê=d3ÙOø:„ìïp)¬ßáÓþDKÞ‡Ú»/´NeLM(•f 4ˤd³4Ï‹+æv¸ZC©2’ù1¹²%­Ó“{v@ÃÀ³×ïò”¾RuñX±$•“€$Ÿc 3zwO›S@;È-4‡ÞÏv±:¶ ›öŽ/Ú,š×ïóÓ¾Ìýy„‡<ûRw¾)ãÎ;!׉8 ¦»ÿ¶rìÿì¿×Ò6¥nJ¡@7JÞcËößqèã¦à»ìþ[e’t¸–õûwó×¼–0vÞñÌÉþoÇÒ6¥nJÙ”ä9éÏÜEÓµÄM ›â7e7ìþÏMúÛ‚ H×9°ø ÉS–ë¾—Dß* Ò²J‚ÄS2K¾”ŒO)ÿˆ-ÇFd ±„ ÂG™!ôSìg§q”ø™Gb•ªÓ7weàLÙB´ +<Ïô‚œÒ”’Ôƒ ù%Å‘ÿŸ%6»ÂPÁÌ‹Ô†¤<˜úðHyAJÄ=•žÐ&¾´‰"‚:—†Ä·|P9Ià†Ôºž‘¥kPÛØÄ\9üÚc& +3R?îûZoÀ²,Zš¶ô´Îº”îÂ}_c1ä>Ëiêl:ÙÓ|áõãC7‹NÂŽõ¡à;'|”i†°ôÙéRT…L•ñe#d-‡‡>@v¡ T¥†Rõ“ôγ4Î&-“:f j •òA*¥©‘T!£ÔÆ¡0ÞKG,T¹ˆœÄØ$Z[9\7¢€Ózኹ×j¡^µPf-’³—ÇÐ]_'êˆGôÃÕ` Ákppú˜~›xN»Ç×~Ô”Û„Soä<'ͲÓG}˜ü¹Oï˜ÚÔÕH§)}ÐlÞ÷™Ì"*ë ’ +ŒÙ³¹Ë¦SÚ¶s6æk·;ý“{õ©òýT«òó¹Ãå2ç‹ZÊöçaS×ò0oŠÛ¿)a’¾­e”´)ySʦÔMi£LøuÜ”ûM9mÊvØ۟ǵ<Š Mö *kñ›6e§IoJÞ”2=–¥ÔMi›rÜ”ûM9mÊò©'~íí¸íGó%šê†iÕQ4'‹kÕ®]ÅÍNT.©bKÉ65Û‹÷Ñ=œ@ĤpOÍ\NGYÁN]ó~ИgÉ*¾kàQÆ´,ãõð&ƒÛ½ p¦UóÝæéÅjõé)µehÕЫ§®VµšŠõã¢XûE±NŠõqQ¬Ç²Ö"ž.ëe!Ú²³U6ÖÔòr\_c1ä1ãSŸ`›ÐsŸ=›/ŒîŒÄ؃$C!+®˜ˆ#>Ð #Ï-u 拺Œ‰mâõ'™}e«ñ`‘â%iJ G|Û^úc™ÁB¨^GS°_n¨™>n©¹0Ô¤çM5ÓKl5Ï™j4ùEæÍùdÞ';tûIéSlÓûÐ &^sš4ŸUsy¯y|Ð:›¿©O^Õ¼ÙœÙ|-†É>«ücЃƿY¤C¿E M,:iÞeÈ6ñ"îÄ <š,ÓÑìÑ-h„n2=C¸0ÁÇöþi³¢J¬ÿf WÜoÁ]^DD«¶Ö™[²µ±î½T½“­ÕÁf­»l°ÆN³½6ÌX«!k±÷<½ ŸµÚæ“L:ÊqSî7å´)ìv6%œ)XM"«)kgý;kæV0/º’×ìs ØÅ°õÏÕok]ù$ë’_|Ô‚ïËÝfve ƒU7ì༥ùøYÿZ8‚Íï*5]JZAÿF1 jÌ}îÊW™Í +‡-ÃÅ;wAúA´ò¤yä|r^«æ9wÑ:jx­ +Jö/Iî…`OÉ÷,É÷Øeß,é×c†ô{ÚÈ¿Ypȶævó{\4’çJÝ—-qƒ°xü×Mcßó­d]¡~Q!¡«Rõ>·ÙqóÌ+PáÂ,ƒyÂç²gÔ™z¿;è?mWöŒD+Æ<¬@¼²ÚÉ„’:È*dcªþŽ›e5³üÄÆeß*:h/CK`²bâŸS›c£í§µ è:f³U<ªKÄûZ«Û>1Ü´ýl­ú3»ŸÔ¨ôæJH™˜‚dûKèÖ5ÐKpú–8 Q!;šW¶®¦"B¢&ß|ÑkÔÝÜ©›Íh0ƒÐnæÜM‹ÝOc8?ÎboZìâÏÑÿb±ëÚŒ&¹9›LZ§á³”}Ì˨–ÄBVöq2–Af1I> ŸH´VvŽuO:oÄ¿U4°È.cbà% +N²å˜4hò %Â&åÿ¾; ºT8wW] !H¢!…C) JDœ$%JN”¤8ËŒè;{&ƒ&‹Îr U1ê&ûÕ½Ø5öYLûAê¾Õ$qÒÉð¥7)2GñÁ²‹‘£qõ*ÎÞÄݭȯ/[ÚIÜ^eÚ¨«.µ5ºEíÙ•kÿp·ÞàŒ¡K=ƒŽ[c$-SuêžïÖE[\Þfñ<éßiçæNmy­õl×Í«$&ü_ÐJ +ÝBF --fäÂI‘=øØ»…íÔÅ•ÕÂ|ž´üºÔò0$Q“IM6í2Lè«3tY†ÒLê²­™‚¸jó¤?ª×”._we±+36q‹ÿ=Ä-þ}î…Ë0-—Îåzk)0yóñ²<åñ¿e-òsL¹çRª•¬;í¦}‡âb„Ì‹x´“ÒÆZ½ýÿ2-«¥lViëKæ¸Y6OÿWݚ˧aÑWšUníë×ÿ}û¯Ój¦ß\Øê×+ú(£vXoOÓÊû{ಋ^è¦ØûØÕ–¡²PaI²ïzYxeãVÞ¶³óv!U"ª‰§Çi‘MM.R)$RY„䶖ݠuð@!iàÎtœ:ô¨È^1`Gs$ÓÍCÇÞ¨u3qÙጠeD½îq’Ýøá&ʨHÕË2ÅÄS‚{î® 3…Q½:™‹b¯£¬Í­Ú‘XRíÔ½±{.BßÄõdf÷A\;±–Ⱦ–û‹r¼,¦È<.jéPE]­)]M}†ƒö6©Ô¬™^çú$Km.2õUM{‘I&iò£ 5~§§l5ÓU¶ÚÊ¢¯LZ«ÒDºâ¢…òØ͇g-—“–Ìý²lš–Ní@6,¡Ik(iÅŽh ‹âé²­¯­ÇÅRiCì}ߟ#×Ü€¼ ØÛ€¾•€%-e‹$ +›"B4m Ù÷Öð½3ˆïN»¢IžúrKr_ÚU©O”e_Nçr£ä—­¤›öH(~°aok×›. {n±ö®–Þ[ƽ½y¯øøí¦Å¦;lMq1ô• Lê–{eÊMÔ;Ñþg6…GyÂ|—¿‡ž |•ÁO¢2ý;eécÛŠá%ÖIÖÙ{{“ÅñÜÒ¸IâtÙÑq—åÄ3)üÔðGŒn–·/ Eëfdê²6'í±›Æ¼†Ib´‰Ï¤@|1r/O‚_Ä øŹjÌÔ&Lh7žý°üÿiež»ƒvÁ\ŽßÐïÇñ›eâ-~»ñ» »³y 7×ý¦Å¬:Þ¹ëœ.xÏ¥óËç?lƵŽ×þkšŠ¦Nzþ3€þ°]wë]ßÂÇÍorA?'4mÛz¡'¬l·qDßË]÷®hÚÃ}ܸ¢cÌRº/z’ZQg´¿áŒ®î蹯ÖÕ]º'}¯ï¾k >àá[pøî6ÀpA÷òÚt!°­âÚ^X[µ­Œ¶JgqHeS‡• Aì¼È\GíõÚg.K¡Ž}þ¼Tî-ÎÄfsIb´®[i‡Ðs)ölŸ­ès-ü˜øS§­ôÓEèÛâÏmÑg+øègº@ù× ³p6BÍ*¹t ä¡Ï. +A½§-«Ûøð¶^¼îÇ[|ynqö^:xÇ®yø¦ wîñIgî’Æ?•Æ¿Åæ?íåãKŠZ^U®…£dO®ÄíÕ*åzÍÝ˵|øÙîÅwáâ—šäyú˜yñÀf}î¦ÏÏ÷âÑöï$ný ~}ñs*ÜÌúÏìy!ÜÃJÆ K+çëLÄLfz q¬u»Áƒ˜²1äὟ—¨ñ_ã¼ä»µ³ÜûKwh7yq;£}“•?tÚûŸW¸mJØšÂÎP”ãP[”ååÕ§'¬„÷«…ðâíÏÝ 8Þâ‡^Ú^yêæ>½kš·¬ìr°áÆPWãE^ðÙ«u¢›²¦Í`èéç…“n»pý dÐw3ÖkˆÉ˜p¢¬Yæþkסæ²3Fq›Ð±p††{ßÙbâî5›†Œ«2hâVCÆ6΋9Ùn;W:‹(Ý‹7‘; œ\^pr†”óÆ«âzG0Œ 4wlÞ§ÌAa$¹C\ n&Ê3YlDW£'Ýî®AL:äx‡]±ñ\}ú¡ØÉÐ,råªM3R˜šŽw­¥œ€ ÆÑÏÛ•@÷dË¢ÉPñWÒÝ@}lº+í.†…‡l¾ÎOjX?Î}•oº¯2çaã¾r2s'‘vÒñ³p륻àAÜÁ‹éT™ìϲ·“ÝfcµÔr»I=0è$Öê¤Ã&1Õ&€ÑÃ.ÊrÝo†ß¨]A= ¹!?Í€™­aµ4¬ +Úž·2432L´3,áZOYÎÂOÌÝÒ0€DŠÈî§%<ëñã²Å·¬—ah݈³…L +"9-øÈ=&2\ÁoÛ¦lÌ\OC÷`ÇóVÀ½;Ž2mþpåÚs»l‘aÚÿùfò%¥‹ÝÓ`ù'(¦®òàêÁÎrX¶nêy”¨äe,ò)e™–³P°&ºªk' ’G'É‘¡û§<ñ8¬aÝ;©h·€‰ TâDws§æf6ü(úqZ ]®{s7t‘û×/†®Ž ãö0}¬ÆÕüÑÝÃò°Ø‹ŽÝ_QGå…¡hÞR^Š¦MD奵hØ‹œÙŒäãf£‡Î¿ß/1—ÇI^Å&'ˆ•Ž““ß1Ë-b%.eýñ›ÒMm“þo Ôß çWå|U¶ùÓ´ùãþ™rǾ]ÚôQ öKËr_ý~dù³ì°‡u"p|¬Š12ðMñØ A$~F¤à$B1 gh^ÈÁ.†‘ò“R·eå†yè–Ò­ô–•´“ˆic$Ý›Iï—ðÙa'–R·øȆ‡ì¸xÈÊÔ©Çp-4äÊEvÚdï Ò;“ô´±J»Î•Må¶éó"Àß/6j³RKµÙª{µ‘¢ñ{Yöþ&ÅÎ6õÎ6Pg§)_&ýy¸Êt+£Ðýó5‰Þ.í…¥nË´ÿó{—rY¦ëK?®üYv¸[Ëç1–+Êògù0-øº}Ð +©3Mr…Ò Oòýy¹fKS÷_$‚Û¤‚[ýÅ× án¸Š§ç|Å‹·øq‰>o’Æ.à~“<Äl’Ù±u¨À€ Œ’—’6e»m®€/N;6é.Ê|]¶QÛ²X§öpUnÀÊvåô\™n\¼ÿ1ez²êøÃÊôCoü_ªÃmëÜ‹SÙäÖj™¾ƒè=X¦yKj´\M.—j –çà!¯FÀ–”Rù*žéFÚ¢<_zÕæcŽµKͧÁý·“%ªÎqñ5î/ŽVw¥\DH]^þq®žzÓÕS?G*}ŽTú©ô9Rés¤ÒçH¥Ï‘JŸ#•>G*}ŽTú©ô9Rés¤ÒçH¥—ÿüÏÐáçH¥Ï‘JŸ#•¶‘J»8¥%Fi@*eéqIe´"¦%‘äEæÈú3ýØ–JgƦKUªKV·‚ ,êaEð^Yp{„ïÞÂ.êIH†&¨Þp²€¦G1z. +s‘d1û*†oLü;Šù7ËDh2Àš„òAÎÓG.T ãßÝÝò÷Ü—t¿ª™:OêÈòZRÀà“ô_k|Õy‰¯2›L°œ°wèF½ÒzfÒ{”8EÝò$9âVÚÉÔ£¬Š\z&.<܈´*»H«ó$Áõ±ã’Ü&eê&ºÖ†Õ<÷° ?]·ÆåUt˜–$€5;ö\èÖÓÁ[[4Uéva7E™:/¿e:=u»•p­ŸwEÓ3€+-xú¿ç„‡z…¬Õ¢>tpì±£é½ð°DÞð-\Ÿš^ýABÅ¥c=MB¸m nˆɸØ/^’oyW¦›É_V®SI³n^¾ˆZ{yIÓS'ÙÜ +8»n¶sè_†š•>ÔE‰küЊZƒÌJAfkˆY‡ mãËàhòFë Ú$eï¦Æà9uæÀÅ”%·R“N#'é&©Ž XcÙ¶K”Æ œÆ“Ñl¦ŽÓx:ší{ÊrÓF˜{BœûÒCøžèþLÂäBºkéÓFÉõ!|â ¹šî0Íþ§$7†b_$ÄÜxêr"sàCKÃFHÙͱöƒbx¦rõ’º±|õ€ˆí’¶Fª\›»ä›‰Cu›„‰ ¦ºnCÔž ’ûI ëÇyNÛMÏiû¹¶i÷œ.Îj¦Úf—7-hÄõïñÈãXßó¥ëÿ§þa‰5‰Ýž6%÷¤ê¥Ç·™…ªŽC¡EdÍY?Ép¥ÿÜK¿~Þý>÷0–ñ{‡ü01`Ç"¯ÊÀ¨ß†áÖ3s¢?f5SœWc‹Ûþø~ÎGØ@×ʦŒ Qj’sÚœ0‡õgdÜNýôsóÕ%ësçiÔÜð[è+œ:¹éÛÖ+W'»œúÉ.¦ÝFó3€i™* ‰³éxKQ“!šØÀ#½ŒÝ™Ia^Ît»—Qå²K“,²Ð:'Åz”Ûˆ#JK¼Ð0¬?ÛsÜ0²©[ ÖÃLÜr˜›A²ÎãØ’~¢ÛýÅ©„ãÈA+~ê9€GÙRÖìA[®ýÔÉ€KÜßúvèx+2òº\X‰¦’ö7ôc²ë@TJº¥1Ь¬ç%f>Q´C%ýæ4ŸÓEZ ü$#à8ÄÇ,©^Ay=ÚÙ=ÛÓ{ ßس鉶./")ÒE‰}¥Ÿ™r\Šä¼>ôÝå7‚EéâÅ,Nëz´íºQ}¸t¶Û«áÔ]Ùÿ§sZþ_Î æ6ÐÝ$•iY:)ÿâp´M¼ÿ¦L—>V¶Én›œ¯ ™Ãä|ÛAuÓ Ôÿ%lîþªîé€ÙòÜQ?—Ûã‰öÇôM/µoÊGܽâŸOß¡–†€—PéK˜ÆC‡iÌ-½â¥7P s¼L¢:}G»/‡µûNCˆz…Q›gf@©RXN+ ºšV=÷07ßÁÕ¡¬c×ôÆOY Ön=uÄõqqìÜw5rÅ_Ÿ7ü­Ù·Qã;ÁÔÕU¿Áh¯å2Ò)Ý(y_¦Í/ý©Ï•éùê7pi\ùÍž>­íéóÚ6GdM¡¾:È:ð|œÛ6R.ÙÙm:u궯«S§E\5ÉãÎ õÐQUÛsדӒ‹²Z¶&;ÐýØuì5ŸÊƒ (sî,×µæÐ5ÐØÏÉZó¬@#º6>ôóU¨]5ùíÏ6WÊyWº™`Zì7òvöìQÏ·/Óå…ósoº~ˆà^v‚{žöÒûMÑ}aý„ð~)®?'ŽŸ.ÊýU9nË´M®ËS?Ïæ•ôÒ2]]ú‘¡¬Ó÷¾aƒ ‘e˜W̸BÓŠVhV!” B»Y4 +q(ÂÀÑ`Ú%g!ˆuÈëé€yóo+ÉK¼€ÕñÍ)yÎôíƒzüûéû€ªÌ?{ÀuÓçgo5ÐPCUηì‚°J;Ó 7Œ,Ÿræš87wñrú”êBÀp¨Óºxú¦êæ\ X®æ™Ýn}>Ѳ'îŽ×Ø%KÌÝéãÃ]ñµrL«+ê§3&}#(ù1”ä"ÆÑä0Í¡ºR%rácËíå)gx_˜¹9®Fäw^/Nv¸c¾˜X>Íû«“Ø~:CúQÖºnYëp/žºRâw¿/Ï Ÿ÷Z†”cGYY‰ + Š[Â8uS hÖAð}l=Æ` (Þ8ôy ÇÛÙ*<ž¶+˜ú¥þÌ1¹ºYVÀa¢)=0/Ëg!{¡Ûhœš»ïáqád§nCج´·ÆÃÑ×ì[å).š÷»j©­§ùEFì.à€’ïfPý ˆâÿϧØ)ŠœßimÓé‚âDÕS„vkõDÔdPˆ@N+Û.Ž ä»L÷êöîîM¦àr³žø ñ£öèÍãΗÞ7ÜÇ¢ŸÒí" ¤›6ñÇkôñ­C²?~L¶ìkÓÍd”?âì©vªõ{y™ÍßtZ‚ÎýÓ^§³IK.›%¦`“Åf›Á¦‰^;® mÂu»§ðåñŒûc÷ь»XÆ‘ŸùFã.†qÁ¸^܇-*ôcÚÅ+î#ýòÁÝEXâ6q|Ȩè5ÌpJ¸‹¼×Ü ÌOÕ*”æ^ ü4ÏÿqRÁM?;®^Éî†\°¢ìO‘ ¶ðú8}T*È—A¼8eÏÈ ±bÿÛ|þßeNqý`“hÌsèšFecÖˆ€…=úvÉ/¹^ØåõØôVÌHç±Éµp¿üÛžÒ²†† IÀIàOÇy6|‡Ñ½?€ïìó’íÇr9šýxö#ÚqÃi?°KŽx1¶>ºïËy–, ÁÜæh+çYãÿ×èÿ5öü_¢þÿt´G×ׄÅÖ2ÁÊ“„ÿ„U2d—írtæˆyŠ»t­Æ™Wê.{‰ËN5ñOP·O6‚GßüMúæi™X³/]˜H€&`'\Xc\À£Ø–¶ÞŽóšÍߤ¦.3éÒ&µìç­ùW¡.z๋LÌÖ9:ÇÚ´.S³UÍàïÝí·²JçÝ_â*öàáÛˆ + Y­==0˜}P“%5Jf¤¼X7ïm’¢áq‹Dˆ‡“äÃN'ÆÎ4IÇöc^wã²7²ß&w¡ë[Î$Œ.+—[ÉÒÿ?½Cõ½;„cýyæ8ŽgOàgk ו~z[=ÝxÍx3}ÂdRK7°êz [˜og¿ ^SlUóÁŠÏ#ü úÞ¶™†HçR²BÀ\CMqŒ}“ S°L°õ+v|´ŒoíÊ”®¬%~×$¬¢Õ;ðò¼1Ð|ÊQt;U«!cúp(Ÿ$çSüœOñs>ÅÏùçÏù?çSüœOñs>ÅÏù?çSüœOñs>ÅÏù?çSü¾Æ¾Ÿt‡Ÿó)~Χø9Ÿâç|ŠŸó)~Χø9Ÿâç|ŠŸó)~Χø9ŸâGr)‰’?)ZoŒásFÅÏ?IFÅtí™Ü.N«;•>^žþÙk=-^©µ¬çáæM)O¸¦†cª+ +ã@ësw½>lÐ+îÁ²šÊÎtÜšƒ&óË’n7l]³u¤mXܳO9gå˜Äï»MȲ¾g^¡á~= ·ë‚ùÙ£~„û™6q)îøgcí³À±úâr•äi“Ïq)#mkd>.¡î«ÓÒëQ:_}ÌîÃtt¨¯îËíšÙ.²Yër a„¦ôu0m–Á¼Y^œ,-Aò·¶Y—Á%uÇt囯KÊŽóFÄB~èbþt\"’ä–Ÿ– ¤ëÀ¤œÔá_6‡áÅ%>Y6‡MO=qgÞ”²ù¯ºIè9DZ/…û¾N}î5Ó“&Û’}ž5Í‹›ÚuÝjuO¯Né¼Ìm•ZP‡ÏyZ&õ^B\:¢X×­»–‚Åæs‹¯¨„ÅYÍ£–»,,cÍ®kÉšÎ]Ï°mû6^­Ø .qeE.©l«º–µ¡ËfCkÿ—5Oþìðy}ö¶N›§2À@C­½‚™¹íþ…åÚtÓ#4Ýp Ýöù¤•oØùºèdvz±Áoõv®Ö¾½¥OV¾ÉÌ|»£º·ùÛbÓÛ:ÑŸq£O7üè˾զV}j{¶×¨¤SM€Ö5ª§t*iT6‡§—ÍïtÓÕ÷±}f>§`¾}v6§Ë£×äl>L‹¡}kf_5ã®/Q„C;¾¥2~Q‘—€ÂEC¾˜OÃ^Ï/-;ÇîãSnÞé#¾Þ[9Ó»9žž4Ë?5³ë¼îgµÏét1©×3ºŸÏãn>oÌætµ9÷sùôμ¿mé˜Ì=rzËÖq™µ…ÞÏÜšdQu T¶çg¨H”B¹ÌW¡$H]š ànŒ»¯.7*Y1oŠa{/”ñضá&é©Ø½ŸÆx¤;±‰âfŸš=©A<¡|2Ï`¥Læ oÉ×y.É,¯;mRq58í){ùðîæ@¨÷ú=>á~œ†{3Í®~ ÷£í…:›¦Úìóºì€ï<¡ÈN‹&kzì +gÈ;=¶íõØ¡ÆîòJö–„µç*]³n󎬃±ƒŒ-Óàý¢Æ,‰¦ æB¹©Í®Q,Û4 =–eZôÙ-›”v¡g*(–Çò½÷£zî…’{œ6:û°Áç^è·‹r{¡ÞN‹~[6hÜkíö~§Ý><­ÜN‹6”d¹-]j²çÞwÿÊÀ»ei\Ú7Zw»ÜOÒ‰Îïb©l‡yà yƒ=7íhÉ:Ù—ÇHq+ÕhÚ¸³v'Ö…Ò=Z/ц?ªý^¨¾eÚh¾×zï~iì´Þ%…ò…Ò;õi^ç­:ïu5ËÓ2ÍØ/¶ÿUêÑÍüFÍ/Mý‹Òk)Gmv¯f7u½×MGùN‰‹Ýb¼kÚl›×>»=óçÈjzàÀÆ µ™Ùÿ‘ +ãŸq‡÷ïú ïᵇwQ‡§MªŸ¡› }ïç5'zëï‘1ZÒ/Ñœ?ª)_ªTÓ÷Ó©V ù)•jÚèTÇô]wÕSâ÷•B5Ý”Á¯ò€\a—n©Tħ IüR«º¥%ï<‹ZÕÅñiÑ«®4åî{üˆ÷ñ%úôËæ{gyNsþs<½ñ}­5‡§çxºNõ²ÓšÓ¢gù Ši(Z7æxºˆ&øøǶµ… •k›go ¹5Ëáz–_ e\«¾P©OÓeñ”>½Ÿ÷t[™ž^¤K/ué§ééæV~jo,#=(þjOÚôÇ,#·-]…ú=pÛÖõ$~`¯{óŒ<¤@ì ÍIK­-âôQÄ~¥º˜J#ž\WˆµÁøæVŠƒòz¸•b2^¹8ãÍÌœ×gÔüDFô‰ôo&⌟Xÿ^Æð£ôïzKý®Ô¾ÓElîåÏzºÀÓ–ü}ÆÐA¥žDX=sJÈó§ŽÌ—Gï=¸Zë§gšÝhþñrëà“Û9>^ˆûÞ'©<ñ)VTÙS?´|îðv¸=%ôˆú²Ã„>v%aA×øù¡ Xèü6¯èámί¸`BÛ6ÙHGeŒÒ­†µ½ßžœ1IÍ]›µ8ùíQ§ŽÈx¸’ߟˆ‘-^‡jíÔ5ÛãîÌ‹ín ‘÷WŒóë6@~RuZåÝgÃâ­„]¹J=™ÿVFý§²òoÒ÷ß*—ç<]v§ L/>ì©òÌc’ògÜáºo-3BÖ‚bdøƒñQV1SåeµŠÚ£:÷ªµsß”§n`ò“l†µïÀs2ŒÁ&Ìptî›kÀªìB=ê-ì)1„Áš Ò´=½j„.£ïÖä»?´dV¢CJÌÝ|”L®cI–ÃHì  rÈÚ‡åp\N9+ÊÚÀrhÕ´Ä5FÅ6&…eºqñ:‹Ð÷(Ó¾ÕÝ.ÓS?´|îðv¸Í]<2íó}¯Ìý–®hºÈ¡¼f*^ó(¯éå—ŒE\×…%zº Òó»°é‡Ëa¨ÖÀ_.§OO›èøõØéí¡ÓÛƒëöÇÖÝ<²nº8h:_7ýôIÓ·¬#Fðùæ¿×iҒѧ'µÖësíž*7λû1åBŸž8<ï—?ãǾå®eá„ýÊÜÁÌÜ°KÓƒå +æÞwêºkxî¼ Ï=]…ç^èF…:Œ]7mu{*¥ó°{ꉨŽKàn[,NÃêÕ h±÷m§›˜ÞÐ#{GÙbt6¦ä Ž¿S‰¦p¾*§›åþ™²9ðyzæ,èöCÊtq¡þØ2ýø.~ª®þȹ§ò½/*í´«DíÖâuR¢ÞìBÃöp•ùèñjsˆW±[ªÊ#ôæ 3öb(ò¦ÝÈÿ¼æú¥ ‘ù~/¼‰:Š1ßphþ„Ætÿí$× ®¸ÀPRô¸œgv£f´¿Ë ÙÝ1ãÓ!Ä»ìRÜÝw»Åþþ‚^™eØÏwÁ·rãþËËýø&ûÜÅ——œ£2ÜôT†ÝÁ'ãä¤îé_ýü[Èü–#PJºÁ`ÃŒ§>y¿ì·êçû‘~ã8¿‘ôê´ä絎4¦ñ‡qøѳ@]žÌè6^Úª¿ø¨¶$§áØò5,GS÷S©OëyÔãïÈ_—ºùw5=Ì¿ãi{ý›Ïgb-wÅ_—= +ƒ©r +À~®Ê6 9ÇãJÊ!ê¬Ä¹om:’¢£??\Ñ„|‘…ü™<õؤ…‹õÒO9 +ËÍx’HÁ]Å噚°W£˜”œ!5ÄæsîyêÓ.ž^y̯2×û 4Ä“§W~ÚQéb{ÒÁ\ÿå†ç£2”|çri‡–1(<å›^jaå Jâ–JV€:V€øgVD|KU$ž+À +LV ÁáKõÖ !¹F±5ËÁ9×¥ÐAié]ä>Å]+{œ4£l»;ÈV„0ìSÂv.æ \„Ë<ÈZ¢Q¼›1½lnƒa³AljÀfÕ ±$´h-æXÕEU-ð^Ö=‘ìRdÒP ± ÑAæM¸-xk‘+»%¶ÂZ0¡ +[|kÁü,l‘“³xwç1kµè–3–7FCZ՘å:~ ¦ ‰è‡‡òð<¿/ˆ{®ž4§;_ð­d\uœ-&¯„i?i*”Í„ø»RAÔð$ £·¦C÷€ÞïfBÑfs›1Y‘È;lP8™€˜HkøÊ™[&Pgõz@¨Žgƒc§LmÆ\ÄvR¹ãNF?M ¯Íz>kÂÒiU‡­Wž•‹•vW!5‹ÍÇ“RÅ*Àk²Y±Ia#jØl„-”­‘NÖF#¾ä~½b«ñ:ôr]O…`qð6(x4°Å¥ûD„ ZEžùÎV˜ vƒ…8÷~ÀVÕtµHoÉ¥¿5Ÿ¾‚=(æÞ}º£ Ø "8rlpk® ËQ„»±BsÀ0³aåÇ‚5|Kcæc¡°cíëê5Em†5ÝïÁ@" pX¶‰/C]ƒëÔÓ㳺òœÜ™ókÝ_7*wx©ŠÃÆ"ñÆÆæ*uc¹§vWRâ¤` +8+|OÍÊE…¿ávã.¼´–Ÿ¹ÃÑ Ñ£x2§ HÃm#B´á¢òxC¬ê1e¬¶âGF+_¢µ‚Œتð6µ ýe ÏÕÏ|=¢74Ìh(WrV _¨ÙZ`¯7‰¾ËsçmJÉÖ†§J³ H`R/XV|¡Æ´Rjv¥JELY|Ù5O© ÊL³ÍC‚Š é +Ô }&×ð€Ò›Q€XXtŒ|gãµD‹3˜ŸZÔPtwâÄ¡EÆÆg ®|µh5ð|ÙT ˆ†Ø1QVêOÊZil5'Rh´b¿lÅ”k¶€öÈ'}{°…‰C‹¢áB(b°ËdÂ(ØoîíA͉d×I(A¬ÁtáÃpŠRÑÖ=Àž„ŒŠoÂÝ+R1Š“¡ ³EÀæ!€RèRcUäëwHTÂ|:Âó1ÌV•$C ’LÀ@7õnN0¶¾›E'Œ- +OC |nï17‹$0ae÷× 9hx*ä¡J)•ÔA° à½Ô‚[Æ£H + $³ßtf§¦íVˆ[Ž´Ž§u.ýŠ xh„u˜ÉC-pêÆCógƒÊ¤‰j¡%‡”,Ø‚3ø(dÚ|£¤†ÑbiÂø ÷àc:“œI1*øVfÜö¦á.CÃÒ…FRš¶€„«L‘–3ÿ±=rê{ +"˜ FÌ©t¡‚«m"TFæ9D%^+±"¦wq1¢2_c6DV…gücgÞ¼æ8%h¬d’¨]µÆ3ƒjQÑ°~u,Ô_V}ð€5õRæ(fWL>ô7ZI@œðÑ 2…G|*>1™\oŒÏí— +õ lá3Á™HÛïšu¤ÝL)Þñ?è y†ð^ï¼Ô |N(‰‡€ºˆ´¬d¢GVbþP™¸“îÒ–B˜FGNŠ „†þ ųö^ +UC!K[1íØÎSò¯”~™ Â*B`÷x™™Ýð1¨¤­Øf"˱zU-jåØ£à)vÑvոȩèPc!¥†Ž ï ÆÏ]S›ÉB° ¡$30û qS ŽÐ8!và‹4®mŒŽ_îª"ñT½Ò‰ÚU%ö(-kÑÑF†•† +9*0­ VŘx6™‚gÖQa£Ã§•˜–´Ëæ.¼Bå–¡CÁñ1à’õú:žê‡N½¯Â†¡P¡CƒžE+fjÎmOñìëmÃó +q‘Ê>:ˆÊÈSt+Vè¬M»j/MßgáuŸ¼t•@ZQ6‚JÇ-ÈŠá‘ _MØCªÒ+ÕG=«Ã÷UÑ4ùX2PøzüR”Ê@”±ØpWãˆú$\à×xÑu‘]TBpÌv8ˆ#'ד‚26q%/‚¾ !!SÀ‡óK…©ë”×´ýÐgæâSf|•–iøjd_àoúdê.â`<•:™F°²q7x%Mº¡+YÜ]¦òñ®2±Pa(K9I½¡’J+3ªB!k1² j +ð2ìfN}!¨(Ì&‹Ç@èÎt€ø¨Qy-<5²’iX ÓîcIJ¢%ß¹õ ‹Š2ÛßCÛä5Jlìyš(¡Wbén[÷¬˜CèPuÇLz@“ƒë´RFƒ¬_i~§Þ’˜Œ—+žr-3î¡e‹D©øh å&3$˜td@p‡,µ¹ÔëëŠ-ù½ªÌ˜yhD¸ 6}2a:U'“Ðp] sG‘œzÄa¨Tôš™ +„dm•ÑH±¢P=…Å)O#`¹™Â])rS¥‘*¥k9¡ƒ/èþ§¡Ô%iÆ™<ƒ²ç¸þ/Rg#=¤À™sÓï¡°Qðž í¢5½a”!©‰Ò-IqÕaŠÔ˜ZwL³Ž´´)Œ¹Y5UP¯e$øÜI’UD« =BÝ ÍJ ±Ò33L•£’¤ +ù¿ÀLÀiT¡©c…KR“±‚+åiëšYƒðØèã+º+ÒZÅŠ,;aÞø¨ c‘×'¡ÑN +B–ic ò¨T~œ@$¿xt¡›A¤•g«`îÖQdðÁB”eÞÚ” êí ü›ü6&lܦŠL-”Æ'nŸ¨íÓžºQ•RlP¹$éźÝa†É +P¬;¹EÐÇæâÎ|¦& ±}Fr_ŠxG,ÆèôÄb¸•;>atØÆA†»€­þK3b|êö_hŸ§N[iµ¡§G46Ò¡ »º˜J{yúAæTšMo_ç8VžCX åwNA\@G}³¸#Uh LSêè>Õd ô!Ñ“°O3!JÄLhÔä2Ò2:¿šgMOÂ>š¹QI5%_BafC4YBðY¡Ë¡E3ºŠ>ÀªéflØ'v²‚k²ù¢HbR#ù°3 ´PÍÙ"Ð Ä¡P¦W a2"¾c®jef–9.Ò7Ã5ùßAÔc[ž¤Ü*wVt}8N·K~DEÇÄ’“wpùð}X… }ˆžÒ `jœùšK*pÄÇabT|ˆH»ˆ×Üä<ä¾K„â@¦5Rªás2#3²´™^|§Ò…%¶ªeTÝ„^¢=ŠÖ Ü6¨-Ol‘˜)Ò.LE-ð±£€~²L×Û@:±$$µrhÒÐÚA'äúzRqêÇúɸܒ½ìJ·WåЭü¸ËH©$E¯Bµ–_:˜{ÛIHn…¦ÅÚ+f‚—ˆÊƧ<ê•Íž5_tÐòCs¤¬ˆ×w²8Ž¿Íà5 ʱ´É\ˆÜlæt±?Áý)ZD(}sÕŸÛëÜLµSê©Èa°™6’/*fdàxÙ{Ü=sÏÒèLí‚•)]µ°y J Äÿ°ãâ­~ìOš©Äù„„!'!3Þ$z­®ZPVÀ£1Z¼NmëŒï[väh€ÜD£-ôB †UGHµÝw2 Îfs«N߬¢PÌjq¸;Ùªu°Îl½§1 Ôž’Ew 4­Òsm ˆâƒ±AÔœ’6ÕÞ‚$]@j_žcÌ·ʶ Q‘ÄÅDº¹²Ž›Ù"’ø°ÅœùMéÿÉÖ€F4 «D2.l#" 渮Ùè#¶˜nNif‰¾ÎYÄÜS)HT»¼ÖžÙi©å„åºíè ùd¹ ƒˆ +åå颷³Rž¸¬°i}ZÇ>»jEí ñ]frû¨JHdOfRÂv¥³´ÈX ±ˆr–¢»ÑâËîø— ´dúJÓ~øˆLÀ—Åó’>«ü«úÔKé_•-ü $+*1†©Kv-ÊL%–Œ–ß¡H/¦…¼˜},¥ë +2FºÑñ]r +âu+O ¡³šÝŠ&×ÀòB$o24R ÀI R÷¼xTЪj§ì˜ØN­ ز•“c -@U䎈I<,FŒýðóª…£™]¹k‚Z€9v +eÀñ$ãa‰ÄßZáÛE¶¢§Uý$%Qbƒ ,¹Có08P({u.Ž…zZP¦á„46Ú“š¹Þ*í*ä}ÑišÀûdÞ +Ùüða­t{WŸ)ˆƒTSÁºj‘& 6‹gµçÅ‘±oÝ@$ÑMƒŽD5é@" št±‘t:ʯTi)ZÑ$æ(ƒ\¶ø²‹2¾m‰ÞÑ2zÕVm¦-¦q½…—r§ÃîEz`®[Ì·x]—£¡‡)-U¤ù/N@GðÙðƒ¤2çéÍ¢—™äÊA%!¾l±Ð "m1%ŽÊë~hø¬dA˜>ZãtLYT ÑmC*#1ñ”H'“}îhŸÛ W´œ©I­zQh<LI‹ƒS7‚q°ÀÃ…ú´S ¯5ÏIæ(ŠñT Š”lA³âò #»Ãî^dÑÉÄO¡~H\ØÂJÄÌLy,FžªcW«p*Í05sÁ`¾c¼øeìAt×ð9T=L)©‘r&±^>¨—4‘¢D\6­`s¢œuÝÂõ¯Ò\ä.¶]¶¢/ˆÙC³N¯LF'„•FY‚”i†Â_ÑDÞî9£o3éyÑÀ–]‘© ¢¿¢ +.{qØHÖÈ76ð }g‘Î"H¹ +©qBÂÂ3tZ;"›¤r”ˆ™ÌžÖˆ"Ï jœ +²L¢³µ¤äü– º0~;an¹ŸìAb=bP׉ŸÄeZmhui$XxŽ“øb]÷˜ÉÌ©X@d\¶0¿y #8£9Ë‹VD‹KžŸ ùpzS8ôÆ]·hÙæ™pxè/[à[(€CóÉzR.‘¨[ì`o°í Ða#üXïDïñÑZ¸/ÁÌ´„‡sÉÔ^)Xd³ÒA5¶°÷bÇsÚiæñ²öѺn­,µ†V@ôª¹[=ÕJk£2ÈÍÛŒ b? ª~w ¾Ô:´ÈʲÀ@¥’mzˆ,ë@´²> ß«E¡³szåTyÒ÷†Çž½>ýʽ±·nDãW}VŠDÿ¦9Þl%Ùñ/7*i[ +‡ \ƒ °Œ=õ%L*æÈ7b²€(Ö‚Œ6´ëö­¢É|­€vÏ7ûÉ=`Á‚é5¾™*iVB»¢À‡’’$ˆUcK;GWÆçÔ1T¦jºnõë>bKѪöй¢¼^>éý®Š2·4 g.‘SA½#-½nAùŸÔ¶h±šmv­*áN…ÆOÌ(7¾÷­˜>8bbHn#AñÔì"¹ÁL"˜ø5®[ U‹ˆqšýŠ¹WýÔúRtÎ/eÚrhÝa~b!3/[\òge/[˜ ¿™e{5)1Ê  +j/VEÀ$Õ2B¼< ä)R˜ãPÔ !m¢l03èÁ¸¢º\éNiZfè÷I ÿzƒ&BîˆÌd 4ººf:š!Ç“Ë›ͳ€3ý +iñ!K”,ŽwQÙg>j‚¢ wŸíAüOn^ŠÕ‰ùkf–$إѭ ê'\¯ŸˆDŽY¨¢9A±¼™ˆÞÀ†Î‚ rãË ÍVŠÛ¢B¥w1k·°„B;ú&¦Ï´ Y‹ XšjžŠ'«zkOÑkb©‡¿òÍ„‘,ŽYûXš$I¢nÔ@°ÖJQÙ .OüêlA`—if6$tÚL;"™k1SNâW§Ÿ•[(2p›#ÌŒNÃíL+¡µÍ (:Íh¯uÍœQ$Íü%ÑF‘žö¢yËü‰Ì4ÕÌG )6›mnEÑÑÜJÚ¡&AiÇ)bŒÈoJL;ê¢ `]&R39¡ë2“+¥*Jœê…1›há…~ÿ˜ø6Ái~ö æƒ ûŸþòwÞ¿yûûÃÏîï_~ùÝ7¿}÷áÛîò3X¬žÁìœäžÀƒL©MÞÚ1&‰ÈUN*H½Në½ß1e{Y ¾bŠ†G¦†˜ísä¦tì^u˜Xµeä2+¯ +'RçÌùÏPp&* jrÅH»Îß½}•’¸üxª0ÄØLÄŠ’Å œàü›«ÉÖ\Ùf Í2ÕÐô¶u¢¥7È´‡Š‘r{à ‚Ì bØ”Ý&TÍæK†&D÷²’JÍ¡‹· +'ãŒ7ñiw—Œf¡žRV[àüä‹=°Ðó««DËTiô‰ã¾ ?¦µG„¿ ºiaÅŠ§Þ]Ô ¹–ËÂW˜;!ð:Ö ˆì]b@ULyf°&Y'dòYLGº¤Q)õ4÷Ó‡ÆhÃW>±®NÂ%L›L!è°Qø&á׉ƅÿƒÊõMo%õ°˜JZ)ŠŒŒ‰c“mGà ¬¤ìMµm¶›Äs3þÄÚhšëœÌç ­@]„æFœXë¶RÉj.Xf6`+Щbý{Å~¨²óšÐ†WF$‡B +à”"pƒ1Û‹ )w@æJR‡|JÊz6˜(½û4ÁÐ O׺œSt–s©Ç2[‰ ä•ÄW¸ªIœ«€9ÁÓ 'ÀRAT'°¿s¦æËÐÁqV‰…·1.º${Gc¥d<ÚÛ‡~[1ZÏZÑÄETyVX_|-ÀC‹Z(Œ;ñÛV§älQ êb ºlØÂÏy ¶£¾£VTÂÙ +ª­dÎÆä;° “£DQ T×A•iv¹«g2YÆ›èP !áù¨’`4‰á†‚ÿЍ&0kFÚ°zõØ äâ€f}9™Ã(¡Ðº8o|î–>OD6ÙÌÑÓˆtˆËl¯u‹4fFÆî;-j%@EéSX”ë²{RfS‰‘#Œ¦;B€¸nhU‘ß¡{d˜½¦¨ÅÌ2'ç, +P¼dö)öQøÎŽk±Û´ølB–%,2^˼CrÞÒB[3®sÅøçPŠYÈI£C4pP¦ZMØLô]飑OÙ¿Ft.Ufn2¦6™“iPª¢³g‹.àÌÉÄêªÞçã2ÍŸR Üá`ƒ$ŽsßmÔœH=’ÒCxGc$St9å*’µQÁ<ŽáÔË”èJ>ô´ZDlÇ<ª2òY‘†UÊ“Â +K+ùÌmѬ ¾Ü¨”'®éõyéŽV +§ô6}pYá›ÝÉLÄŠf£›iÞ²à⫈˥If¦£ˆÄÊ1'$FW™È–áÀ>)拓5“ÅøWHÝZ¯”-XŒõá-äD™‚ì/.ØÊD$Æ– +_K²$gØ]‘r¬TNF–œ™òÈë.ÿ¢‚¹wUQ9C¬„ˆµL©N஼vW„m¡”½*iÍb…¢€éo6Œ‹ìUaxÈVSÅ’˜eÃhºÀ€Sé+SeÎhôœ3#Sþ±[Æ&@‡"½¡.äÚ*@µ›¶o~æ`ÂJ+9-šTú™7‚Ö™ÞŠ5ü†`J‹ÈöE=Ú²˜ÊŒÈ¤ª¸ãP%doZQfÝÄxvjFÕ@ÖeŒ#À@Hz²AƒÉÄœ;žìŒÉ *“àŽVƒ)6*¼%²“Gg"¼*}¾  Ì´G,ýdØÝXkN ºp ‚ºM{)¾ä]“Ó +ÄZn¦o˜Þª°Æ5 <@·5²Ôp ·ÊšÇ€#y|™Ò¦”§2C‘of‹®D¹’õt‚æz–þƒÔ‡î‚Vb´à>®³,‡™›•À÷ ¶âÌȸÄÓ˜ ú£ØGß%¹N¡‰¤a x5§d‰ÑéP8¦ºÌ4X­+¸¬d2?V¢¶Tò…š„gH8L ðK®wVH ÃÜP¶W(Hhy’ TÉœ<Œ×W®u';¸ZÑ lySiŽê>.°hª~Ez–sÝ^ÐÄzs•¸;­"c8„|ÂuK5¡dh×­áFžJDSð[Ô·È]Iu#x%£N¹•ænNåLã0CÂpÄ»Ÿm\hà0Læb+£j2½³ÐN¦ØÈ9÷h"lÁ&™ŒÀmð C‘ V®QŠ7L(Mriƪ(RC\ãM7eRBæþhƒpR2‘2Ò˜ƒÐD8„žµ‰°ZfûÏÐ,1Ži‰EUœ_;ÓÀfqÃœ+X…•1P +Z™ ýÝôbPî¸ Ïçlvå¦T¢‰H[RSìÃ"Ýx.¡2gÉ®‰Í[CÏqÈ,†ñ@§‡T2K$%²q‚œ,Y„Xpà.i!LƒæaB½sT0ë“õRhļ¦By YD+”b’ê²P¯Ì +ú`YþÜ,˜wt§Á¯”Cû/HÙž×)ª"(¹ óDD¼C_¸‘æVsB®F¬Y¥€:rî*‚bÄ«‘©ðK§.‰npTŽšÝ8%yH,KÌ„\·Â2ì‘ ¦P0Œ…³Ð®\,¿+…ÌïJSî!ÍÎR” ô©èDwI®æ +épÂi¨2òTŠ2)Fò/\º˜‚±&J#•”¶¦QŸÈŒm.–ÖLŠ¡µüÎkne¢Ì]d+Ë'ÜZUÄ?[DA!áÕÂ2ÝʳͱìjOÎN?ã@ÈK­Ò%UÒ’¥»¤¤àA É·º¬ðàL«tÂZÈ{‡W}fìÓp:/ó4!¦ +%:2U×½pÊtÉ4«ü~®ã³ +e1Á»¼åÌ #Rø}1:^Ú‘t|sÍ%Y$o)j¹üªÜ­~„m]&ÈD-Ì[åxæER ¥tÔ¡¡· VÄ#‡­fKøé,k·P±L Yã‘{ ‡bŽÆ"P¦Bˆ¬al`ì—kº#² 'Ø8Ðúíd‰ôŒO¥ú+擈|+Jìn®,YI4(Z3 +¤ÐÈ,oîHyIÏ )Ų́E h"¨œÉ¹ö´O•=‰‰V”n8xÃÒdzU;±¢fË-C3-¼˜R%Û3â”z8X´l:Ž?3)niO²­!èD†@~³Q8Uâu挣›2…Óþ"Í`v4 27d+#“B2ßTQ¤ŸD"“x¥áµB¤!J¹Ð¬H×”ì5Øÿò/bªZnLr¥å6Ûn,|gRŽš:t¸0¿ù,åiB¿Õ øJX× ¤43‚&úüû d '•ð’l#{Èê\ñž!çJFYž¤<#òn²ÇÃ’ÒI—“®Î=S×ÂP'£Ã}Z²èGÍc‘‘¨¬Eì-f {UH +¸/·û·ÈYO¬_\$R˜îl®=Ì2÷0KgY/ý6h‘§&|U‹¹f¥òjJÅg-hPgÀÇå T +.kn©¸eŠšôC’ɘžH8ÔLù˜‹NbUá18ÑøˆÆ…¶Jd(“øìBÆäôÌsp@˪–x°¶Í +ÂæK“±2¥¯²vgë\Ògú¤¨ z+PKú|vâ<aYvÎC6ÂE;掅J We +JÕQ99­uÜQc¢–>Ç5‘Ò·F@`mI!Wàv47c•y‚‘yÑÂâ»d”' R a”“£M4¾TƒÄ‘˜k„§‘º˜àÔ ªXy,E•lM¶B¦L²ÀüÖÅÌ]ÁÐ!xGaú)…a‹Ñ6MìŒÅQ((ÔYIÑÈm©¥ƒÌ·…5²…4Â8W¨”Û‹¤œ¬Ðš¤¬\™)qfîŽh"ÌL¡V–Þ ÑLèS°gJ²¦€CýR±LZÆا" ¨}Ö­?UÈXºMá–†öaZ#Á3óYÊ/B”…Kbve,²Ú•¼!DNþØSg(#©’D’‚B¼óL«ÂÜ2B²Ñ`AqXL>¨€v¦e$k˜»A7Q†ôJ>™AŒ£&½.Â00ƒV’ý”(ï±cò…¤ôJb€Ä³J9¡±—€U°}Û€6yË›{BJF2x1Ò:aK2Mw—›õ#­Žî6 +©L/¤Ø9H •Çò ëB‘œr|›G"fÁ‘p~'íP;¼ÎÜ!SÂPÓdž°Î”—z‘•y‘·Z\61¬r̹ñEŽ}6ÒqÎò$ Oj +Fç*““²Þ?(·=-CU‘® +µ™GUI +3>b3@ã ",¡Ëûòw’>Z¸¼pV$TP:ÙÀŒÎŒàT,P²˜à¥ÂÌ*Ü·4¦T->Ѹé4“ÉNù‘«Âg¦&36FßTÑÉæ]Le+E+¥µ¢”­¢~þ?ÖÎeW–$KËOÀ;ìaà ±»¹á¡–˜@R¥¬Bj‰î,5EI¼=ö}ËÌ÷ɈU*)iTÊfáÛ/fëò_lBâ[ŇÄd¸“ŽCÃü‹ò_ <ªøàûoãáÙƒµ‚Èë†{ +Œ†Š (GØÚhÓ¿4b -@[ÍÉ·…@+«:•£hGœgö}tæQ= /*Ï nÑ: PÚG86à“y *²AI† õçs¸há#`9Tûî)D U à&ïj›í¨¬©ÜÜ`OÜ[ÁÞ8^Wx¨îÝ~0µ†¢5Û5r¨Ç.äþIXMÊ>a.Â]ômÅó*ܲü˜£ ­}* al¢4«t‚u®FÊÐC°÷ TÍ+Ðm…‰n-X÷ùÖmQ1–jÙ($£í"‚ôþh œ&S‡Ï‚*ƒ÷)X¬Ïá¦ûù¥æi(âœXň۶ a¿+.&ηO‰™Èe/)—µÆ)üö+Ý·ßðù“ký£è5ag]A&[Þy?âà#%˜ÿ h¼•8ƒ·;:ë:2#÷²!uC ÐQo¦ìèŠ ¬™ÍƒR`¡,Ÿ=ü¬ëá×Ø „+  +Ƈ]þÚ÷\¨5€S ¼ÙöWé…s‚ñôL¶µ5 +žÄç(}ÜØ‚ò|^`^l[tÝ…¦¤™hHZC¤¶·& •›áìÔ—÷Y„©M*-÷wÝ(XtT ¤#Ë„­@jnlzEãF]Ó§I‚tÕÁkFûîõZ³ ¸z_Ûq¤]ïÕñ5£hfFä묊„1ƒ|¶ÿ}¢ +â%ÓÈq˜ÍùîˆâDÁ•PyTeëâ04`!4êi%Kì¶t±nNœˆÎ<3Ö‰Oí¸ÄªAµ½HvÚd"PžÔSä B-P%W ¢(š@å²nþ.‡”Šáz¶hþ¹%Qô58‚=ž{ÑEYÇòËÇäÏÜ*Œ… °ÌwÞfˆï´†Sæ}]ÞgõÝ_Ï3` -GBÜžÁ¥Ø”Íú±7m7¿¨ú® IòÐËŒŸvQ6˜á=w Ì¿8ÎÆ[ "è¿Åɺ| ŒÀ5Iâ¬l‡pü6 ‘JÍ<é=»Ÿ +9O‡ÆÛ庰ÌɆÊ‘šè5øTF ,;cž3µ â9¾­µBZ‘S•ÃŠ–ò׌¶Ùn1³XQ^Nˆ«J›w»1#+¤ÎÑu€ Ïl¨¹ f=fum{ÔO§¦mȉÑÉëõ€]¹¹itO2Яs‚k?°Ð-öa¡µ¡‚™›ÍŠ¦¨¤—^ÍÙu}š†Mo3j8ÖÚ߸Oöu¬:þçñ}é»U»(¯i«<È%a™£¦íËç±°ã¶u;›àÂ×oÓ!µj€¾œÊ·ìq÷ôš͆õB>2‹ý ¡PÊÖž_Ä\¬Å3ȘÈ+3Œó°ƒVäuÝ™9öñ³b¦ãé_ t¸âë°*9ù57’.܆Ó.ÛKuœ1 ì÷zªBÙ9m«ó-¹«WÈo N+옗f$FD_‹½r¤hÊS«øëÑo™_€Ê‡ +¶ÂE,ã‹ú¢pÜŠ×u ®ßzŠ…¢ ¡H8®Coð2?Ö`Ü<­´ç¦yÛü¨¨Üêž_0‹yp†HÿŽÌc> +›)húkV!†t§“.Õ’Aâò)®Ë€ +L6Àãï­xÃŒV·¢K‰cÌO Ô´mÓ3zîÞúìr¹¹"ÙZåÁ’ +ep†÷¢O»­n``ð2Šäœ®TK†¹ žJÖÜcºóÊpí€WVÂru©úkêqM²½2}ìµnº TRQƒ]M¤-_+R0譡š…y,¤41D#´#4èFlÔÌjSîËSÃïaÍ‚Ú³\9Àÿôà**+ ¿ìî» ¦]FxÏ fb†dXêp$<ðŸþÉUŠ³Úþ=,Z̪E} ¢j9tÓì\„—lå`9ý“lç•ÓG¹»oôu[ºÎÒ'šÚä>²Páé.3JaCFA9É“|›QN0BÛý>7¥ ¦ŒY÷¡úƒЫR½úXÞº7ì%H°ò¶¾?uù;ª6p±ç [Ô?0†f045®5äT¨/[¡~wðöÀcݨ0 |MÞÜ ß7Z;kÃþš ™påSé~ü¯N8g‘dŠÜ0”çàÎÉŠÂM㠯ј1b¦ðü8ꯃú- FO„3[[+îöEéÂ8€ÿï¦ìâü +†iK´ öŒ£9!]¾+‰ï›“Ú“”1¡#61=«Ç¢26#èx)ñ:ÂF1ž¨Çí‹Iiëð«ŒÇý+[ž:­-m†Â~am[»ñí>ÈÛŒóØu·®Øÿ«Y4ë¦DM4‹h ò.Зö®,eó5äÂnߊà9È|?4ÈâY³& eiU~†nLA&Ïx'éåCø:S‹ó²ý·‹Ð¼­A›²Z¼³W~¨ ­› •0&>ë}KR%¯ð…9…Ö´3ÄÒS™)¡V/þ^þ)uG?ht‘‘2!“?X©MV(§—­·¦B“DŸ Úí°ruåæ5õ}FÜ•vBBp!oä³FÜ]ÁùˆàböÄ¥ç@\Ý´’|ŸÑØþš­ì3½Í¢ ÌÝÅYx ½-´(Ô©Æ#XÄ&Þ&3Ø×·©ÒÜ£½Ïˆ‹†R|S‰¥ŽûËã¤hðC¼Ë=q(U¢— 2äõ²Ÿè×I¬€PBÐŽé5œ­ÂN½!PÕ˜fB@)œÇBc'#ç4Þgœ—uN^ÒŠÊíûqp*–3“Äf Ì ˆÙòå Ò#Adò‡`Û6LÏYW¬* +’@2¨ò…D J]P´:Њ¸ˆxð# j(ÿmF\¼øŠ€ÞsT½Àb´Kµ±TV }wã› +ÊÙâž‹¾Î ¥fdC#¬—ø÷¤ ‰àŠbí ´dðߪ1#ÌÒ­`µPºR?;}W£—›0óŽãËsöRâ8ÍßO¸º!WQ\‘¿’Zs³‰ïí' VóÔ¯«<$åDZÞˆN·3Ô6´UGK·{ÇÆ°W”“/¾k¯BqO({®ðÔ+¬÷`QVôÎßfÉ6f,¨@eÅ0U YÙ ŠÀ_Í¡xÄìžö}Š +P Y?€Nv¶TeágEpÕü;b„û‘Ü0ÑÖJ°6úèr•XÒÔM…§eì²Bj î’B¡Vä_vÐO«v'é!HëÌXÑëõ?¤9oÔpVµ¼×FÙ'Š¨tûÛj­ÚM„Ç )qCúV²ÊÁŒC⯊CXŠÓ;ß`÷tÈ\ò|¯ˆùÑoT­‚¶å䵜³à}1㣴î]ÐC}¥’X%±ëƘ!‰†þ¹Ò­/rôk¹õ¥˜½äÈÿ—ÜÔ=kê½Ó¢W?Cé r<£W¤Ò!åQ•|˜¦3sS››i†l'Û¯ÝnõǪ`s¶ÍŠN(Î\W +!Þ¨£®¹`ÓˆVã·áò`#çeÆO§µ¥±ToºF¿‡æ_ÐêÖ‹œ#CE5€…í}—{ãë$}/Y›±Ø¾¨¨L1ÀuPÝW ×¬InhÞä?j/ý¿íÅn¨±+¡JH±+¢¤p0Âe·)Uiý!ŠEÑo¥'6À#˧ }KIÒèp¥ë;£ÓQÂè”L‰xz‘<JÌ+uàmÆŸwqÈeûu–…8¬Ér7 +7QP®Ô\eÅEÍ1é€k•˜7.ÑI5&Í~&q³™4æ>Œu0 ©úž1cFÂBÜ3fÈ‹„pÌçLÛyê27;1ÉNœ‚è4àôóí£¸ºä‰^ë›ä?1¯1>1Æi+AOêÂwÜ–;qA¬J˜—•Î•Q½Åb[L~ZWŽ{>Wíä¨> åÈ®IO,JO>¤{ŽîÑ0”]Ì"¶ÿ”¡ÃKK¯ì.‰FL$DQ¯Þ|¨u#˹Ñ)&rƒîh ÏmÊ”î7e‡ãPÁš4‚¦3\.€„]ŸÇe]æ[ÖøÖ˜êGƒ9Ìt÷tÞhFrTûîhZ-€Ï üy*³¼´†$‹KŠCîAæLh¥ý)ùŽÞvéuÉs³Ôß"®+Û ;žó¡\Õð•m7D‡Ô1@’c=ùÂ!øÅr‡œW«ñ>þ˜LJÖ¦‰:·\2³š¬ºdð…Ü*úŒ—Wáe-Üùh–€ñ/ aIµ÷ó°7g ËöZ$mn{¢ Š¯ÄK}§ûr“Á甆0Rï\Iܽ·-kÃ`’By…3Ḩch½ˆ˜a¡?`ÅPÀ•øF;”@¢ÁŸI¦ºçfÇÊ%’˜È3Á¸{ì]óÔ䯨-®ÄìŸZÌ#'Þ7²ÊkG”+Êã=a€@ía)*G‡ih{|«I-Z;0Е"NÂÀô,G–Ã!Œu°¡Ee.¯4/–Cª`Ì02VéÄÓSþ™È?zv¡‚çj Z_a½ +COíêS õžI ðÖUTó†Ü/ ÷w9oß’Öד¬3½êŠßÃ^Rg:ãu&ß3:ºóq³g°÷±Ë †e|…~¦VœY¯àMi+SÀ>å÷X~äzjû/ƒ×ØNŠç¶h/}DaË_Ò.g]iÿ}\Ñ¡òBVLós2÷i¥Åô÷¿ës/§ØsÕbè½O I©1|´ƒ°›Ä˜ëE7–¾»(™O”¥nsžFHô¸îð†¡A¢"õz/×»ä!€1 »´`(ÎðoTóÕ"…¬»QnˆW§¹ŽŸ+TÏqM\g'ƯŸ´Tùcº{›–]Á´B9‰²YWÇô +±·x2_ƒˆ·¯óämÍÖi³:fM:ý~ŸáÅNð2ˆóŒx"Vk«(1ÿzàªq*}’jæÎ¦ï §èжgPÈœ€(ß|,¿•T‘|ra2¡~`™ø÷ì冔 بÚÃÛÊ+…i½ýáè®)º½}QV}q“´ÏOlšÖ¤jW׋€¡%ÀHiI¸Ɗ놈¬®Á%‹y[­ÍþÂÍ n±"k€‹Ë"U|É—£Þvjé}…:Ù©ˆëoi³¡4} K€9ez‹[äx”Xî<˜*¥õ£²áCoad 4»›x;]?×£>)—+Ò\ðu@¨Õ9ØËèÕaDtÚäÍËp;zØ€·€’J™Œ¦ò ¯ŸÏøSâPe4l*cl=H»IÄêë9Øýƒ¥`Ûå*ꯙ‡ê DtÒTÔ„/\‡žoå+4‹é6Ê„±OÀŠ äE`ÁÊä¾C8 -émÙe(í:rLvªŒB6<‰%¥ C¨MÎä•-½"þ ¢V [t‘Ð…×—tFšF¬!DÊ$ +¤Þ9ÜæØŠ°à™"L-ªB~«m™‡•µ!ÈDàô¯móâp¯ƒ5¤å¢î t"Š;Ý›æ©vƒ—!Œ¾|ç±oÁOŸW Šd9rB­(*~~>%R6Sr@ù¿Aºb¨b”€¹é¡N» y‚DR¯ÔSèéÁ0aÍ'ô Á†!µ*§ÀÄÍÀ²ºÿv5èÞ0o:FoƒWH®ö(]¯8*î\gõAõìÊõˆÃ•ØÛõÆ ¥½ý­´©'}í­ÂA[T]9‚Ô©Ó$‹kìÃÆ qëÊÝô¨ßÌÁ +¾ùÃF rHZ¼äk,_Dݼ òy¼ó^3[3}·Y¿¦uè½Af¹KÀñ žž°QFËÂÚÖÇ­o–Ð¥Ÿ +¥¬`ÏÄÕ¢à <Áoƒë½ Z0;–¤ÂÈì¯%›§ª0p'û ©×#ŠÖ¶’Žßr¿¸‘!wNÄ…Meʆ³6B¢U„wýZÕQkº¶nÐhŬèŠ"îz¡WMîEw†`”ùkE…ÊK«)ƒ—§¢­úqÞ"&7=öµº«¸åþÀ¢U2l¥rc<n­Xd ÖM;ƒèè!Ñ‹€Æ +.Ÿ+îC¿ @G9RÌg¿— +ˆ=dÕ‘$ æî›*;ºÍ­×e]U”±ÒbÍ +? +”ëP1d}!Cÿ¶1,o€a¹Bô*¼Ýë$gRK··¤ƒíö Ày¶+#ø×à/¿%ØFá¯2 ÿƃ%ìû÷TAÿã÷ë?ÆÇßýÛÿù?~ýdùãßÿÇŸþß÷þþ¿ýîOúÿþËÿ~ü/¿û§ùñÛÏü?þü¿~üO¿ÿ§?ýçýùÿþ1Núõ7þûþø‡ßýé¿ÿqâ?î~~Ááñ¿ò¿xÌuêl ÄÆh?£Šÿ¥‹ú¡´ÌÅb} kgI—d‘sí `©Ö[±‚ 0)ë½ü»ÿ]ûˆäAVuL¯/i–ë ¡Z!=¬plbPÌÑZ'Êtgë\Ü!ˆ õv€|ļv4 [ü·[xŠÙB°(ƪXa–ÐÌ®:š‚LC§ÍöQ`­XâFˆ¤ …ÈËjeÖÆÃãƒc@sM¿̚¦"ûp?WvòáŒv;cEæ¡æ–W,ÇŒD9.ŽAEt£å]½÷ñ·..¶üÇŠDÞaÄ—@ ÆŒ!‚0Uj̘ðµ²<Œ˜ÑšÇ@w"NÐ[Ðþ+¤?ª|¨RÐú@•eÁÇ ³’ž©­õ¢nQ BàO_i'^!¸fïÓ-u—Ò ÕN¸î|>è×¼­©QZ›úZ¨Cðu…-]¬@û}1œt‡ApåÀ5†ŒB¥4KSÙŽçkÑ(8K¼RG†ûZ‰OÓ ;é3Üÿ‰Ué:Ú‘‹B侘RþL¦í “Pdj¾1 ÃC&ñ‡ ì]¢Û _“ÏÇŒ£#¢‚_žš½Úˆ+êwKJyï‹ÚˆÊSýÚ~17boû8à/L´íN÷Ö¶6Ï.?}5!…<!Ð>|ø k-†5¨µ,X+]¨Î­oC]ÄÄxŽ šiEŸRˆ¨û„a]=ŸsÔlÙ¿¤¨öó|çÚz ]Q ÇÅ*’¥åþå þ~n®Xç}ø·Is{ÞÃûÓÓ®Òãë„N}׆üP÷fœ˜„×O° !¨oߺc›_ôZ k˜…€ì×Þ^aX¬ ¥ËÂâ£Ï‡­»•™Ëú8fp&ƒE¼âëᇹ®ø"”±Î€:ôÔÀahV8BÄ•iÈÒ=ç©::æ­$‰J9 ²Ý£á1£bГ#Ôò< +3£Ãõèè‘´/ä0ãL]T‹l’Pp©Qõ\ Ù¶þ‹¢-ªAäÖÜFÐä¶ü€÷—9Ï/5¹}¦×Y uzdÇXóY¥r-—.ÄÒ­KxaâepSy^þ:Ã3 –u`| yëߎ£p3Ðrp*¬ÑHÛkùJñHƒÀ×_=Æo“|eÛÁn$Q`ÛÑ«ò;´d¤  ²#åRS©˜0ÔWúæ[R,«g–œg@¶‡nW?6n˜%‹îö*lüw¹-~1¨**Ÿ­@Ïhç* ï[-A]/¨¬Ž€B Y +5î•RÉ¡¼‰§k  u˜g}~"ÛâEÀYo½/šžÉ<BºVJ¡N¥U~0,¼ÐÀî ô ئnúµò®×¢‡¬÷.Î:8Pº2 +ªA±%¿V^Ú2ΚÑà“šcw@7k¸f­ÇW4…õCrŸ'ÀÞ‰Y¶àE[J«ÐbU,fì>uįø 9aéŽ_²¥™Û}KfP"2Ú´9B“@‡Âì ÖHZx6ïaÖ°òôñ×_’oû¥ªˆ±í”š¬ + +«ñÑÄe½@šî*äÐóK<äëKø»3 FÑÌ÷vÎg 1¹qQÖ`çÅg…—˜µé®¸ŒÂ9 ñÝ 5_kw³Mâ츨% ;ñÒGѽÁꊒHr`w*|ÖT>¿Åš‡R?ÈTʼ%‡„Áz~ÖK†‹¹^$ër/öÒ‰Çðí{¼ž£µ™PÉ!¬r€l½ï89W!×Årw&T]6"–ÏT§CŒc…=.9&G +°^<šj\Žø|­|ž)/Uh7”¯ k&‡Ô—˜¯<ÚCˆ¯ŽÉe%Bp*€ætžfqýé:üátz•×xô=™FàX«{x^Uš;”Ïk8I¼ ¬9•ÝŒy¼¹~ƒ5°ŒDøJ7›VDtcO(R? ¥æg ~]%±/ܶŸoÕËO# @,ZãoŸ'3›8Ö/‡šð»¸8e ²Á ;ôyÐNaÇgË«!Z«v=åzxoÞ0D×ãK#zȱz,þhT&,©!²¹ëîE3îàêa߸å±nZÐ  ’Ã@¾bYq%ÆnlE©eßý,Ðæ‚gP”v@/Y ôžRôÿÞzÈ,ÄSö28XjüMHJð³ $éqEË‚Ä[Ÿšµld„ÆÎ@¼–5^•Í¹ßŸßÙa5ç‚rˆ®¾ (L±™Toƒrû:œfÅkó«+ôÊ¡ SÖ~°‚|Tkð+ù¶"д¶¨oªIÈ @”…×5íÉçÎPäzTí4Øá‚y4a8k©„ ŸU{†Äzü@è^É78g%š(”;<Ç rr Ï»;ºÌ š¼ÄEs”øY´ 1uJ2iAW$|,)]«ð¦6KüêX¡“på5èµÜ¦Ñ|ŒtkAÁ¯+†©?Xn¤F kYw’Y·ôŠ¹hºiAbb@¨Y!r ++å‘´Šå a +Æ#šT‡<û€"‹…‹üÁµó´ØL¾ÜÇNô× (d°VÙe z÷¾ÍœóvjÉ䋽D5§`ß!~t!â@½å'üû cì× ¼—ëS‚™Á°(Z$>®Êt¦¯´=sÐãZÛ0úÕ˜ÔTÝtE‚®¼ÅzÀ@qËÖë’Â7ô7´ +Œ0 ÝP^–’8Rˆ7’ÀÚŠîÖÊ«bܸKnøÇ("Œé‡Ûo®Gà’iä7wP/7G) +×ð9;‚•rM™}²k¿©ßy;¡ ŸB=A{QšOÍҼΡ#× ç`K`2¥}-̶ÑcÐçnÙY-V½¾ÀöP3U+}Þ» ÂÍ>zÛ\C+ Êèõ2Ýo ¾á˘~€™´.Én¡1PÀD e6=ÆV>aÄ}¢d^fx#?:·š7×/Žƒ¾.z•Yß@Û¨Ó0WÌK*Ykà´nŠø?À‰a;-R°–éûþE‚®rÑ v â*Y}ë‚ñdûküìM¬Zsê3¢ÍÃ=ÏŠž…‘úÛq ‹{;þ–Jlëð6psçiøÅ ò +˜g!Õ¢5ÚÚ¹¹íîý„ïmÛzqEùä Dô¿õG3â†p8ηÀZÃAÀZ=—¨Ê·žÂ`/÷:x¦R€¾ÃTƒ°qRMÊåë3pnþ0y#Åùó[=$m1ˆ’šÛió’žö@·à¦¥›ùJ‹mv{8yUI;qX g¦;îï ßê~.p}›•R¶"Êë,+Ò.ÙwÆAªjp)<”h˜„‚¯Bx²ªü܄ͳ*žç˜ïñ'àÄŽ¢¨e Žn\:‡AšÃ+£üO5ÕåÐWcÄ«A-ø Ä¥ Ûçä¦ ¬V4 ÑQ- Îþ>€ßa¿ n8IUJ—són^.=±OñÐêY[ß DzâoìÂ(³H÷­; +•ÉkãÐhôm`à<ø\²—A×@ÂTÕÑÈwózAèÏž”L +£Ýò œG6xdàVàü| ¯43›kyÿ”²ßÂ×AÕ·y¤À£{›»8ÙüÏè•W`1·‚ðSdȈ¿ô +¦p"ñ1oÛßC[›?.˜nŸak7¶IÞ_¥³+a¿Úá5hH[Ë€z.èÄkݾ Ê{²¯¯gŠó‹8ž©´«!†¬Áþ±rî…‹Ì„sŽdýÂ@ä×~ƒXe}íˆúiFKW¼%Â? Ty_9%Œqˆ„º ¾w=Ó-z†ª9 Õ3T–Áv‡Ñç·08ˆx!`x™R>êm€ÚðÃzœHEl"ìÄ“G+ãÖ¾* +w˜ÏþâØŸéøÇ ¨=z¸÷¡`¤ÆÛ”ToÀ¤ö~B•¹¹Ì]µ¤Hx=rvr>t]«@ùí\'4!S¤Vñ‡µH*Ô¤¿¼ìwhä-­À"Hb}æ} ‡ÛåO;àüÅ`Ó~$‚‰µÓ0»|ÂZôͦ°¿¢4„E>GžÇbtT¡Q<ÿþ{À1 Ù0[Ð+}Ç÷è¹.®\œ`—š)…B·}1ûüÀÇ *H‘a±H£ËÂ’Â+÷qíòC¥ÁÉ/Xä¦ë»²„õ(šŠÔ£Ûž/a È 2˜3y/ ¸·_,»¯ƒ¨½èù ㆖çÚ" W¢®«mQÍá`O]:êN§jš§B»-(ƒê´¥* +¼`1(sY+‘lc Au]q0{‘ yV$aêa¡‰mk`ýª‚=‡"­~þgÄKïo}†VƬëeºˆªGŠð‘,T< nñ"h›"$‘º–XB»÷¥wlðOºó1ÐÎ@ÛwˆˆÞ§±*=DŠÈíR€smÏÕoy±×@i!Ù,×Â8¦•±;Y²ãpëÅá‘_×Ãv¥u00,ÀœWÎ;j>®Ü-Q.%ÜaÛ{,’ÎõPvtçXmŽc§è±¦ryÜ8ŸƒRwa´©&#ži|_P¿ÌgÖ@êŒ/-Æ»DUd¡Tžxs¾ÂíûÔóvJƒß¦öÙ©gÍu‡ü¿×€ø?FG„:$l~ÆeàÒ “ Üµôô‚­Ó²U SKödÒ*ØÛ$7È%øÛ.µ¨‘²Þï<ÃÆæ +td[Úk‡þàú6pç@fFåæe¸;Â8‚mˆÝ¹}t¡†HÇYNZhèøÓÅ,¨ìVT¼-XÓ‘nÏ”»/0^»‚2fThA½+Zî8ŒsdÝQBÉ«®—Ë6#Û,É#Ë50ÀõwÇÒZ3åÂôDñ-0’v€H“,c‰BÕ#PØ­ÓƒÈ#¬Yy¦˜!0£©êì ½†áWÝIo”íiÂ#Mz?çQǬðâðÇd¿>âr@y¯anšã¸>?|ƒµ^“Ö¸Õ»ö¥qâ篕øŠnÞf.®Ð +„Ñì²+oÜUìRgW"l6,Ø jÈË(Íí•6bMÅŒ}"œ9ö`õÛUÓ°’ÒŠ÷Z¹¸×È@-W¶!dqüêè¨ã«sgUº$ZÌ„lB<8ªH3ðæL莇LüæÖH\×{lZdNA‹ŸwÚd]O=|J¨1‰f84Ò몟 üRÅh”£[}¥¢¯—»ÛÑzCxmý8˜ cºø¼®ûßQî +žóÛ ¡|Ú-þ¢HÑ_«eÚ“{+Œ1èÖ„¿ë/BOóƒ +àåۆРî•N+¹ËE­õ‡’ =!ßÌ—q†J÷znè}qœk‹£p;«–™yÛØ!Œúÿm†|¯º-ì¶Í³r˜–À8»ìm=‚ÐCÑ@ªY¬Qn+ÐÌGÜ!3iÊ·]kr:@…ýB…fÔ%Th“Ђ6LQ÷g 0(m¡wÌ %ÂJDÒNK2»÷ωÜË‹þ=3~ur@2lyθL¦/Y4ÌÀ5ŠfN ØPý£ª›| AjŸ5„âC*ƒË„Œ ¥ÈCê>á05ÄÁËÕv•öÒ+ñˆðŸ<ßb÷¤‹^×Å Ç4¿}·µíÛú¼f/“ò¶p¬fdü¦¹â e†è’q>ù„x(—‹.Ý×?í–¿ÕOEž¿˜dûHBV0%Ì¿¤fiß{ÛVµ† û?‰ MYÊt÷FÓé6³©—¨Šõþ>ÚZukk”ðmÂÔå‚š:€-篰û„ÿ©ÿ-=%WVÀž> +ò>¿kÿR±‹Y +qJšõGlmªVÉö¬@¹à¶À.³m7™!—!:… +^ú<“»®UÊÌÎ?b†éVx„è”έx~õMÓ,wPÖ£ ÇTÑï*}w),ó­(™Ò»St³ ë×¾˜AL2Dh­å%6õë,œT+ézê¸`*à|…ø 8,VÛõ„ôÕ毛â댟v£Ba5Ò._gX8¶q†!„e!ö Qú>#Ôâû˜Ú8ekãPJQGÞ‚e®Á U[ëQ8É4\*Öà×ÏBÎ>QU êõ8kÁý–¯Zˆ w +)Å‹G:“Q®¨ûr·¸ÜÇÒŒY MVѨmkoÜé’3ƒR––ˆRéìŒâ3Ï™ 7DA^Î0”ì¤ÎÝa–+Ϻ ë9ŸsÿbзØÚIäÀ"í(!1C³WY÷ ,M#Ë1Žž:×&ÎtÁ«ã85Ì‘l€"Zwt"]µ‘€³;òÆX¯ò¾*wn}ˇ½MRÓ«75½ÄuͱCó„×6ÊSþ}¹iGÓ–ÙŠ¯VêÛ„H¶xPŘIÍ—£ô ­¿wˆô>i  Ø%-8…CXà` \N6K †@€nEN‚ê¥ÞK²'>­Î¸ú·)aÁcÑömö_ET·¼Oq‚ F¾þjzÉ]ÊTÞ©·TŒe¶ä™«‚)Ü$¤9Nm÷ý>#šåscxï]+_ƒ&‘Øž—Á$5¹dc%ÄθÆËIXùãä¯o{Æ:‘°ø×q‚Þz]èÂ}u4·:Ekð‡¡VŽã<q5ª×á¢Ìˆ¿ÀÑÄM5yÙŒ‡kƒ* Éöû}Ö?ìßcÎÔ’îÁî ‚ó»»l±ƒ’?˜é¢÷[c¨¬¬¢ï3襰ÎRt¼Øæuœ3âAƼô`D§v$ñ®w!,ömÙ7ìÅ/¥²È°ÀŒSëBqß„¶ò&½K³S@’Ü×uB1_g¼îÍQ%}…# Úº*aG|Ø s]JO%VŠ2+ò7à "ô&éJ¶—ÄŽh)S—¤˜«’e)¢}ÈŸ[3¯Å‰&P´‚Ù)Ît[ê¯5Aªi3!ÃÐ ÷éDbžÔüI*!éVÑSœ‡«Cµ~mŒá(¼"¸LIÍ?î&JÏÔ…jÚÇ`¥ßBŸe½&ÃrD2xMÖjèå)s= €w_?Ž’þŒ +C" übRü»êJWCÌ!,îJGò}{nGcíep†´‡Ú‚ZÄ°ãËë´ÉzÍ`9¬Í´î[¸åÛF¯È¾AÊ8÷¨á¼ƒD0©´â©ta`/ó¢ý*Þåb$~Ç°“ñö=µ­n_‚< ¿JŽ“[pÍhwŸIúŒcoÂnK‹’ÃÔ+—¦ 2š(W•Q ~†®Yâ‘¢m>ømÚp:s5l ¶¸9ßÝž t Ú„ÂsXB0µøx·Mq[9»*?ªE!©ˆëÙU?{ÃiKX¹4 ×>¦¶¾œþ%ìlk_WÏ̱J+¤Ô§¦AÃb _Xfµ6÷¬`0Pg‹Ÿ‰ +3 ¯ûC€ Ð´¦Y·>Èz®òóZ¯Ü«mD?¯({4¯(úµ0n¶×ú^l'ÖÓæ­ògÕ¨6ÜÓôvF©Ë ±ºÈCˆg¦•@Љ7Ë´‡Näx®;®oJá4Y1Ù«/ºTº·Sw‚v‘·„&Öìuؾz"Ì!Yã^<9Ùé†_yCŠª2~=††B—¢åJ¾LïàªR—ê{6[àQË‹™Á%°Í¯«KY”œp€~ ’.N5¾aÃu78ÌYiSe=]3¾nÝ'¡ßKYÇSJV*Š‹pSëGŒÃ› ¡gDP´1¨žeKºˆü@¤£Á,AzÚÀƇ)( ‚±¸ÈR íÖB41¨?Y‡ƒ£šÁÚJÊ|²/î$kc¥¡ÍkSfýHìÎ> +"6¤Ô ²l¼#[øb=>pnIY›äø n›Áà!RÖô#kÅ´’\OM¹ì'ÜÓ¶|æ @9ã‘=ÔôŠfÄу@C_ðw¢@Ê‚â> öz‰ë¼â(Œê‡é0á] !ÒÅÓ‡ËÔŒ¿ÌAjC’å0Üslh°OÓ³&5wÜØmpcâ ˜i*%-‡º$3:ú,Hƒ*žZ©áÔ8“š£4žÆ32´±×ë ©¶R)ÐEHósœ‰Æ.3š¼]ÐŒà9‘Ä»æžAÌ1xj©d€¢¤L9ží0dz¸Þªõ˜K+ù +F.¢+To#> +cÿE˜h¸Fìû¬e¥x¬Å…vàÓûù¹¢3 HÛÌò;aÏ/õ(ËÝš’e̯¬g«S’l GñN­oš‚£>¸±<¡©n!ï+ˇKñ)dq€÷åmãL”³> Þ*qàÓî“7€Ï!±¸¢ÊL§RéãÝ0靖¦];‚T¥ë‹2 z\lgy-`E_ቸ\”ÈP‚ä5)¦©w˜‘Û9j¹@ä,Ë”„-Ûã¼RÙäGsœ&¿äŒ´_ˆ_ºMf&ÉÉJ‚G»³,g“ +e„ (\æBÌ:Ž³vŽ¾¡„ÚÎcÌpÑqÿÎõ¤×­ nä±–Ð&™"Å:Îq˜\>:Á2ºœW.:3ÈMšÓãµ´ß ÙXZ.žS®F÷fý¦Ž#¨Lsª²aó^6ŽkÒ’ÖiqÞ`0\O¦& +=îkè_Ó¢œU#&JiDóU–*;°æ{J~Æ!Ó˜Ó,bš$lª¬ÔÈÀÙ^OÄ_1l%òQ†wä]S¢*³c#÷R£'B +Ž³Óéì‘[üÅ0í|¿… õæà ž%¸nôÚ f ‡Þá&^`+¨OÒ7õhOTÝ€8’$ð'ÎÀàÇ Õ6r‚}Òñt­%ݹ¼‰þ}¤}fB¦Z4ö‹Q +åƒ}fðŠoM|ÆîÒ9(°r‘q Ì?Ùæ–2C:äU˜Ú´\b*ÿ+XãŠê>š”<è@ˆÈØFèŸ ÔŽ©F›x-+H‰yãëbeˆU¤Õ‹>îðEˆÉ‚°¸¶§õT?8ýŽç¦ 6Þäî Àùª4Êú¿¬ê¦ÀÊ1€ÕEÖýÛó|¼ɮ§§è>cª0ÝžûF‘Œ‚/‘G¡†Íá ]>Û1¹ˆúbB\® I®‘¼çÑÞZGÀ&åÁúö>Ã_¤"Â/0÷ó«’E“޳ͥDS,=gÍÀ…!@q”ÆA‰„Å„ã Ÿi@UáëTyXçìVϵ<€‘²÷¤ì6ê,´ ˜•yøA€ÝÌBcSÕ{ô¿P¿ð§Ðrmøð*"ÝÃ1oèùT‹îÖ¸Á^†çA³kC¿…݇ăæ=J‚zÁCœ‹ªÆ(³¸­ ÷ÅõÐëúFðtËÆý!ö°ÂEåZäókƒ›“¥Š‚]—Ó§­¶ª@ý%»µÁ­ +ÈÖ¬á¯`µ,ÀâÀo¥¿N[3øg„3eRùÉ.°Ì$Ý¢+Ó7 ú9“ȶ™Um/ã€Qã÷¦£ÊžC•û@bß-àPZ̸5¾DZë!wÁ‚r—µº{܇žÐî»vÁºFÁÚ€­Eß…õLзsU¯W×ÓŸáU¶¢×ÀÆ|X¸Õ³¼ªÍ Ô¸-Z: 2¤è…âRY?¢¼”\&d)ÐmS/avBÅfŒ:{½ƒ»%–×€Lg‚J*R£G8HàBkä(mÑP`çQó«iCë¦OªÉ$4¾N¤jen1z<}p°nú°iëzg‘·3ÉËJ¬wòéAŽ^¬([”ȘÖN¡¢jEd•Kc°jœ·gŬ­õë5{T4Š¢óÓý»o5-ÙcV×àBÞׯ‹OXafj;å’ÕÐ`¼„Ù*1ŸÃ'ÁA…3Gæ¨îi»„ä¡Á€ •«ýëSÛ'åNqÒ¶ÿ**ªë¯êùбÐÌ4Gƒ¿¯¿“›Ä£`¯™K Wµ[`;ßC;mкr“ÖÒ€“ËÓF†ÞH WÇg Dž¼Oȇ­ÿõxƒ%É¢«è‘M½l×W¹Iá0Ò¾;®”—õ‡ßÕÓA{<¡r¹×zYYDMÔJGV9I´ÍZF÷ÊÀR #КÒiP"ilÀ’÷ZhªDŸ¶I:l™+¹€mˆaœxQ½œŠ¿ìŒ^«Ë¼‚N»û°ýÖŠÐÙ:l[¡ÉDC<úÑæC±ØÛØ›íú™(ªø§êplx}‚F/ô +2PÍ\Œh³5,[ù½×[òô@à…,ËdŃG™‚.^ƒ7—‘Ñàÿ—ëmy’GdíB÷e²½!dc§\+€°Üf„BØzÑ×Q‚kÙÕ¾\µø¥8Q|X=ëàØ֊‡ö¢ô¼‚*+º8!ÏnLM¸v†j!Ûþ¼Áu?2Ü‹ò[dhñ ÜÅg åøV U$ì—3Ôh£¼ÙÜâKø´¬yîø’ ï"Á¨q ÅID(R½d]×(6­DüÒá9_ñ÷üÕˆæ·ÌH D×Ãþ\YjцMë/[ï‰g#x‡=¨ü%©œ¡¶xY_(\{ªp ŸA´`ë§Ð¶ƒXÁ€÷GïK –[©lÒ-H @2¨_*ÎÙ>¥DžÁ¦¼mûü¸ø¥c9Èyšõ€=iˆ9¹v7¡rÕN ûÁ!I—Âdã×m ÖJ@r¨x‚-$+„åÚðM–‚òq4\ƒÖØ÷#: w_£EÌÂÀ‹S™6¼í²¯ôž.'ªÌÑBO @„Šß‘ÔÛ(À\~Že¡ŸH—Õ¦8pDH´Ü@l¤„Å€`‹–Cè¼m伡j`IÄQ·)-ÝodÙà’¬ ºx’ÝÒÍŠJüΘz4…òÃ\aË– .‘]´^P„Ð|‚jèˆJCPªQ…oŸdP’Á²ÞRž° v¤UöZ‹ +?ùk£Õb™¨z +p$-"äÙ0$Ozl„;›‚è]°Ð!Ê1Ž6Ó.x -=/ŠaË(ÅU:öÔ sà{+zü éÒ^R)Ž¬kBþ*%ÎÇo=HÖµJ:Rõ“¦$6€ôÂûŠ¯>Øìã‘?D`BÊ&ŧ+€rÙêD¤®‚_eßÙ«YÛÒ àgÖëLÅ5¼=èz,–Èüˆ¢óÈíVª^†ÔÑB8}>—rˆÊ—¬L*Äž£+)FÒ(‘÷ãÄ º„‚,èÒeJ[w;êD„C ¡¦êUÏ Òmè<‡ A•£Ä€"ªNÙúB>¾Pe[³98½ÿ±æy4çÂ÷7Åó.vž¡žþÈ;^Q®ù¢Zp»ñ +ܹÀÉж×C`“’€n·ë…2#úóvô£_0• vù”ÓTU$'+3‡3?Õ*7ý3ˆ!!S 6žÊ²¹ÖûŠïàf ô9®Ð±TW"s³o²eRt„ubãï@ þ‚ûÖæT ¡ ‹-|äå)UŒ¥FJÁ7]¾º!sa‰™êüýfI6ÇVbªº‡6Ê2rú!3Pz=ìRù7ëÈp†Ùførp[´Õ b-—‰WàºwB­ý”³ÌûÌQ>v‹Dïzï’4µÛjæ°ˆ²…kÝf$­ÙÌH +ÐFÆ']Ýw]XËFÆ)¤PÔ(ñçp‘úv%ô3ÒŒ&ŒÇÁÖ©Y”f1@˜ ^ö`Qñ†rŊ․Üs¸té©™b0Þ>B7’ý5pu(¤¸ñùE|ƒüö ™érêh(`¥åöšjiîŠþsÝÅ*EE?lž)(¦: ¨a‹êQ /Ëz³$Ód0,ù‚dÈs(½NKZƒ%È©>Jz®WÚÍš¦C.–Œ?PÞ·´L«ŽÒ2r | Å'+ð"'ÖõH P 9@¡*€Ø.!'Ó½ñ©‡ÖÛX;bõ¡ÑÏ@Ô .”ÝÕ,yœc´iµÑötÊ)Êò­æ¾çQ†Çvÿ ¿*UÃëõhr¸ҟ2HrÖe(MU­çÉŠÐl½E9.è:¢H0Ÿ#g9·à *]ôT„9 +†ËõzUÉS©í Ó +Ém +èë‚(^wm„¦SA‰š¼àÍ*Šš#BH‹•ADÈÜiÐÒ'¤ÅGi²oͱÐG¶’<¼Ö©© 7ÈâåÈéÊŠØô³•k¡Åç‚ÌÒ +EBsz“Šá¶k4'A4äB‰:¼¢¯0P_ÏE õBé)'¡ãª\pŒíH é’hLP*ìÅR#wÅiglL7è?Sþ•BR•ò‰ºU"Û _µ`^±m]QD~†Ëc™YŠ!·œLL}ë§BJHJî P—N•‚ïû’¿ÏŒmßËðz'É=Mä|m•uú‰§ãÏ«8ãp€f¤då·ÌGÐH¶ïË ?Öœt}Œ!h|…ˆ uV6¦k³©8U 6ÉÃÁË´kApßA5_(ÐDow‚$…x©â­Ÿ\ƒ ‘rq5#º£¾¢>Þ6eå2ÓÄþ°{tSØ’yç©AJÀ-/qú£Šªúƒ²Š¨½qu34ŒÐð½ec¬Ó=Ûb£ì͸Y”œÆ ±¸¢–Û†ö­ŠÐ)¬,¸8kšÏÑ®\u IÕkR1‘ˆìXewŽ Á›&Qè1aÿ ,jóYÁ¨Ø6i·FŽÀQC‹&i‘„ôÑ.êz!Ú™§Ûóza¯Þ}¢4Mw­CCø¯bt2Íîì¥pïxÕ€SL§‡ 5‚}®´+¸Uç@=äa`ÄV7§º%<³´<€ (3Iª£Æ%ô?ňæ Y <ß2ïƒbêÚL[W†˜©Qb"L>…U[‡L´,GÇtŽÖšZÁ˜`«‘[Ý=0+’‰àïtÄ„h—Š7IúZ`EØ!¸ÑQâ£!KÔ0>¥ºÏ2šâ‰8÷o½Ò ˜*w)Ê]|-”Ê™ÕlWaÚu‡ºf`­¿¯Å„ŠÐîk_1ºªdÝ°*3*ž˜j¤ï°³JVøWaE +ÚŠY: 8ƒd¬„,Aç€ö+âST„ÈR>)ÕXf¼ÒL_ÑQ\ Á¤Ko`^Ší2’Öê„÷Ý£ ¡úRW™,ôñg@­€¸¶ô«—1…ó¤í¢øâµù¨yÚm]oMs]é›Ø† i·ÍúbuóœIˆVb±½Ã2gÅtä±>ªmjpÌ€ƒG° Øú+ˆ5Ò*på`[ÐCÍn€5¿íG)ÀÉ+ÚD®Lö£„À¶¶Ñ ÷†ÏÉeoΰƒ·fXe”07?•¦Ò€ùž>±±òV|ÙÞVJŽ5Ø2”¦R˜‡Ià"¯‘!^³V8vP%Jºþ„‹L”Ö¾Úµ¦.à'‰¡ºÍ[ꎙÐÁo¡”¯?ŒBWh´Å‹…< ¡ˆZÀ—زý0­ J\OJ?¶´):ïßµÅV ‚èY•L |O °wÚN©ççðÕVÝÞ(á@´¶™ÐL‘Ÿ!>äËžudݹºÏ´bö1C"šôßIH eK½ÌÚ‚žìKËãwÖ,ùÖ¶(ˆzŽ°¨ü¨ÃõðöèØ–L‘CÞ‰@RmÛÖΠM $νoã$hG§Ï€“ wV ¼bI/x^Xg±¬Y‹‹¦„zó ¬ èm³À¾}ªËg;П‰·çn&F,àÖ³lŒÍdN¸ÛS¢ÓÕÃoÔ?(¶1Òü4º4–\ä¨n$©³ÐÊíð5JÌâYfŠÿðo‚Ôa½‹¦ +ù¯šÍõ-„í딊:‚ˆ/«¥mà {$ÒußÔ,x®c"ß<š@!ó 4Ð_xo¦ØÚóPEø XxKÑ%9œàñq}YR™ò¤c§BýhŠ%.UQáMm'Â3]èXÔòvéXÿùĹíV«/~Õc–èlâKK8‰Îš8ûÒp"/¹õtF.JÏÚH‚¸I¯=l²"ÂÖTµØêà +gy.[È + +'¸kc×ÉÎ6V5&¢&„Õm_¬qê?î$NþKAЕfHÙ¨àž×´ÎŒ´À_¨r³z¢k@:O}m`£SÀô¯oe8J)ñ.}dq'—Šn3Õ +Ðü R?r1z;Qvh¤»íîVÛ;¦Dm’L[ÑTn)ÝÝj?Š'è*Û€KËë@u7$ÀŠÐŒ!A4™4 Žý¯P~K¤Ë¿ûUÏÅ¿ñ@of†ñÙÿûÊVP>G¹mDxÅœ_Ž×z(îÔ^{sÖm=õu–Œ —® –£®D÷D>¿ÌDˎ멲åëÙòÀ§‚ÒEIŽKņ 1±ùbÙ&C´Ø® ºAäíCøñŽ*Í'ØÂVÀÅ`xx0&0ÓE¨C{xD~cîo$¿ñôê<Õj$rYG(®ZÛü`­¯5*#Ñ'Ïq8ѳkÛ0îSõlÛY$ƒ¼EIq„˜&ò’+TRÞ•›_XBw‚¥Ë¸ýe;²–xÏ2£!Ë +«>ýtº$GéŠn‹’…`==”Ñ +óp×.Y) –Ö¥èSñhœ¤W™÷ݹ/ûªèp~¿žê.{[)uÌc\òìø‹;?W}ú¾ žèÇð£|ä¦ï°d¨¦ÂµvÏ©C+æ«z>§iš9®Aw8ȯ?ºñuV_ýÚ*€Ôhp¯%-ùÛ.×>²˜Dëé ¸íäî¨jÁ}¾dQŠ +b2Å]˜ðöùzòÖZ|˱m´5n;Ïä´GwpäY$Ø«ÈÓ%[E +ŠêìašwŒh€šg õ ìÏŒMÜ'ï§ + ¡t@—s§Í4Š*íöåB—’Äú<•8æí^kž?Ð.ý;\íÜf5k50l"B'f¦ÞfÊa­fò\]Âèíûö>Ûbl-]4XfÑUWCÉfvÞ÷Mˆ&D0Õx#—´;zæº ’[Þenð@Ýà´˜’© +Ê<…ê\è>—Ïî<6hÌ’ã,¼à +B§¯aÿ¾ö=¯i àÑQLUI®è¸”ýUÝ>× ÆÓÙ6Ôë1YÛcÐà¨mÜ8w²íÐjQñ2h *IÉþ°ÊÓ¸2ÊnY‰ °÷·‘ç–Ý×ÈIØÆÛa}JŠE¹¨+(èpjêv "Ái…¸<ùOQw,ìŸ&[‰Ú¼>Áµý¦ÿ¶'MG‚k]jß]ù™ðN¦;L–\ÍwoÏ„“ïs¦¨F¨ºÚƒ×x>pdaÜqñÚîõæ8…× ˜41%׫’Ðæ;ó±‡é±dPþ%bÌ×æìô\9L’ˆ˜Ê¦íݨ!u Cú¯@É’¼¯ží³³m<»‘eÛ9Úâ*ºæA,Y<©“,áç„VáÀò:Cà¦Ušµ\>–+o³úîyWðj%üB”f‰¦½¶ãqÈ]>ê)ëÒo.ï3~ÚUWIÞi)¡ºùvœ ¤Z&}k‹ AÈQc®X’7XØwøm€fÍ6i-»…ï8ŸîŠx¤ hai“"TÞ£•¤Ä_XUgÌs¢¢AÐçÂkþ‰ô¨VM’ ­Ô¯Ü'W'!þÖu‡ J6æ#)^5B™8ÊD×BP,D¡8ÏÍ;Ý7 b€ø¹"ê± +²f@‹#ž3Q,k ä«l–Yîž mrKu/ñŠ“`DYv.ÈPaåë»±®\ݶ  +Ä4ï¯fÔ0•¥Õﳼ΂„@C`¾/}·>ܲÚšŒƒ,1,m¸'¯ŸÇs¼ÍÕ0W)¢_¿mô£ÔSc@Wbêãó:R§ÑMX/ä#¡ó6KC>j¾˜gÀ½áïöVöø «žy2<Æd?*?÷U¯£´†êZ˜a(V¬?ovb UÉf4jC×òõq°bÙ"ßQ>„»µBÐy&|ÜŸÍdA¼Š‹ÀØjgá\÷VH¤'Tº£þR¨êßaïž ¹øÁ·¿ˆÒ#ÛÑ^z¦EhÖ¸[P™¬ë‚¸ rz¡—kÕÉ»É}»J¤°¶]YóÝ>õõ,çã bb `0€˜ŠÚWô}Å 8_̸ÙHSÙ’¬µ#£ÇÏjŽÏö4@†+ÔKQ$çä×Ü ¹ðN»0ß0c›1 >wíÀa¶n¢ØZYÊjJ4C@¸Å[&‹Dd:â WÒ) r¤è²ÃàB‘㯇D¿erÞ^Úƒ>É3S%(`$AÜÃÐ9÷†Ë…¡B°º[i’Ú¶/qëkµo›³m{k Šr+SªB=˜‡¶~ ŒAɱӥ”„3C (eWôÂTž9À^Ï™ ü0«ìß㪅ê$ÜEg¬ž,­±yb²›ÔôoëË@ô +åž;]¬T綫¦kCë"m×C+…á^"œaS^¯rN2ßf”LàŽç¦ÌL:K?…mp/ô5QËLk>êúƬ¤†Ñƒ´»õA|”¶ÙóH¢_Ï7¨#™¯ªõßÁ ݇׳¢€’òeKÊÏcӓÈÓl‡)XŽÂR æ•î +ÖªpXµU`„ FfÇíùÜŸøjZó|©Ì(j7ºX,½~šÚ˜ mÇž1öŒûØs)×Am‘Å ˜ ™š°ˆ=€ +³ @|aÎLßÅøõrws—hì‡ò+ý½+bm9ÓYíŠYª’°ø†¥Ë´ƒ©dqJËõyÐ(àRÔcYö>'mÙ|î¸y}ËIWi>ÆLj|pU˜0"¢ƒ¸»o3ÎC3•‘_—o~5‹ÆœpùJì§SÌ©þ뚦Cš  Ñ EKaÖö˜ûl˜ŽÔÊ;r…j®ÊÆqîÕ¤ÂÆHNš}à•B§vƒôPm‡ˆ$ª€JäP ÌYᶿ]ͤ·5ƒä9fˆ“¯Ø„ºx<¼£ë,æ™\ÃÖr +‚ö*5½* U£Ã&/™@=Œ r˜BÐçh‡f ÀP ^ï3â3Œ5*z8×nƒÏkÏqwEÞƒ  ]“eó«T°µõ«ý‡m¥#Å9ÓÛ,è¡ÜÝR³';ˆgÊV\7P!™¸Ì Sßw±×«÷û Ï”‚à©6¾:¾íz„] p‡1Pza÷N£La÷3ý6‹ –G1˜^Ã…J»Få`ØÉË.Ú@c#*eý£CHrö>㼦ÅkCÍû¾¿8$Ò\0$ɱ¨\*¶|9ƒ\Aȃ}£ÃàÚX$g.U1JÉ +ž#“7}òTXë,µ»÷qíZ€°S +ǼçhtAh+­ïhª¥ˆ¦²æÑ}_k#ÌopK*\w¶ô³Åâ‰mô¸}’*Ìk&Ô=PP<À*«Æ ë.kƺm«^÷­z>«-Ëš5˜iSA1z'3„׬³m\UÔ×ó¢@VŠˆ›C´' +㌠DÁ>O >R³Þ\£¶z†r™Èk±(P Šø¸ˆ”\[Y7¡áð¡3ÊžÑǃˆ[[èm–<àˆ¹NC]Ku@VJ8S¯_Íhfë”ÉO°ó6I(~€‘FÀLÔ'0ˆõ´àÿ¡¹Pÿk +³å\­Ë:ƒ:  ‰†kéG?.vw†i>H>ðÎ`«aÆÑ’‡Îª_îë9A¨m®ð®Î{³vZ'ûLš®¿¼Ä™¬Ú¢ÇÇ>îIÙ÷äÚ3TOmèÆæ‰þ +¶NPÜýƚש¼êã™#ò§â¢Ð>²¬–•Yõ ê½ÏØ ¤õÌ©ÍÿÓΟ~9Km°Ú`ˆ{fèÅ!Ãó•=Rê÷×*ô>½\dž‚Qß“¦=Æ&[äXé.(ÿTVÖ¯4ŸFrFÂ!%Sš¹Kª¨‰ÍÍ$[k˜B:×^NÖó& œ­³Âpj}ï·š ÈæA¼z‡­$÷ ?Þ–ºísèíü~˜û:¬9°Ü=ÒTËGhÀn¼Íørƒ|›µ6åÅÑšFº¥Üý¡| ë^‹¶Øÿ°LG³. äϼé}SðzþÎß³…¿g—µM%'NìVq!{R00—”„Ÿ…• øa‘~‘º´ Õä!*ŽOßj$J/s;‰²ˆ€çFQì1Æ7‚ÀÛŒ?ï‘K÷Û×5e†¹«r ¬k1²~ÒMÀrbÃU‹Kk}‘ñ—”eŠèÊIeîIÜn&¹c9 ?²{Ϙ1cmn{ÆÜ3Ê™AÇŒqδ݊׬néFb’€¸VŽ(«¥ &nÛCðÁ@Е¡•øC˜˜ñ`‰ÛVuÆm …Œ°ô±ypc†žŽõ‡.us€Ö›ÖiHãœî@/*9†·B9eÁ#ÓI ñäDZÞàLxE+ÀxEC1¬å”£‰Ï{8Zw!Hk]¥*X6 Û‹TVéØRÍk-¼gHȯ „X×D/ÀãàÇ ,ƒ÷q¨cÍ)gÔ¿æ ó‡úã3×å„TË}µÕbã‰ùj†ªƒ(Žkï}EGg_>*öó14.aV•Vø\e;@M)Z>LÙÙ Vg°(ªœÑ{óÙ2ê +Û•mñÛwa0Ûð•Õ]fíºS +¢hwø²'äj•è¢ð=óÀü÷‹ÐĤL²:=ØÓâÇÎZêma9ˆ9!¡Zec˘2¼àIñ‚Ãz¶ Ælìé–~€Í™ÃZ]pH‘*ÑCTÁh• ©sjŽ]f-Ÿ,\X‹¶ ì\Žv_¡³É ¸p´\s>´ÛElè€} ¿|i‡øWSˆärú D„l3ꜗQøȉ<™lh}\ᩬhËG^-œ4µ€æÑÒ +ž5¾ëîkv á0È´’Æ…°HöpkPÅ+6t¬Ç—š,RÁßÂYˆD°ï‡A áGzÃA›;ÙÃMEË®s8ÒB©âˆ8?e TÐyïçÆž+&B}]ÞY-At–¯3TyòZi \ÛüÃO>?îï´oÞ¾%Ù ßyX,{†ÎX&ˆÉ¡^ÒÞో¡"d?Ÿ{T`e²ôóÆ{¾ƒf†Ðbå~¯Í^4ƒe'@Ë×*»Ù÷×AT³ÄGB.â—à`†,‰d%ÜnòŒo^Ü´ Æë‚NæNM…z„„œÕçnNçjÂЈŸÁ^jü ´ãÏGÑïIß]? ¹éœNSd]©6¤ê~‡Ò4Ʋ%qp|Œ± óuÿ¨=Xâˆ{ƒÝ`ƒ`€  %´lÎq],¼/+[¿~Ú®Î5:÷%M*€•ÂTƒµb#Èîu ®Ík$ñöuìq4qG„Á§-EEn­(°$ßgxñŸ¼ £ä=‹ŸÕÙ¢lRåheÛ'<Ç~~¬šLGlb+j<ƒÂoˆå€ß`Ž»®Y`_r”ʱ +¸{H#RŸW±—# Ò7xgÅMQáäT3Û­'ýv\ªS¹¿(¯¾Î¸9óüÄùè~éàTŽ«kã”E¨ÕùèIzçs^™²êıxÂBˆ7¥amef¿„,„jhÝ7ièÖrõ]Cxìt[·Vç"1ïdƒ^,»g„.Gi†D*q ½Ôç¹´ËTàHf¾$ÙV€ÃÛ9(s1oú6€,Ø‘'xöON'¸™ŠfýÞõHlÑ'Ðïö Ä29¢h˜ƒ£úù-ÊžÔŒÐ[ RVò!ýðúù;ÜéÿbðÈ4V¼Ù’!] ~G ÙÁ£¨_pÓo{ž×.\k× äm8ßZ+´ +t]A³“Åȱ,M1^/É – +²Óϲ¯êÖµç.úe €)lטä(“·¶—k>G±Tžw«‚|TzLis2ªΠƒåi³˜D‘zPköqïÃ!_i@ „h +]“Ûä‰ËoŸÏø[âP=‚ojŠ=ªò`ľn;±È}ùíj»ÕÀ¹ž¸…¾”WÚ +ÉñùLMæÖ#ÌšD)ÕBÑÞŽ>¤ðˆ){¸!"_HÑ3×o\“ôÙX¯.…üÖ6P¾ß‘_R– +H>vÍêõ­ÿLäL³…›¹@‰[]+,z‚/‰Ë/|I<ö˜Ò7ä…G=×ÏoÍ-å€ 3_ˆ?ÚËçm“Žâ`/c5´%œ‹nýÀD€to*'زõÓ:ú0°É¿GôظXƒÊµI4N$kµ«GÓ)[r˜ŽÈ˜ 'Ñ{x¶ ù3Ðûëxå_J.—Ž G)ƒÐRØTåF‰‚g»JüéÊ˽}ŽóÒcZñ2v…‚j=xR„¯i^·;Ôõì+Sw=ñ÷m dR–êÏoÕÍ3!‘\W»/igÒZàú¬xàŠ±z}º¼„9]„7R6û‡}®Û¥Ç0Ãäq XgpòŸÞÖ_\ÇIØ^3$Z3×°WÊ @ 2|]µÅ'®UûžžH1øspfÖ’—¬Ìˆ@TrjT™J±>Õf¤'÷û:ØÚÖ“½s¸b”íÇö7[b;ˆêDQ%´[ÜÙÜB«RÈ&€ža©am2•Ø¦q›õJL6MVeèµ(vZûÓ±uS^q914lDmHŸ&çª`ëÖ¯’¡È?þ›Ð»”¬[ý[_šA„ ‹«FI§Dâ“Kñî‘æÍÏÀÁD¶SÕ¹>¿Ejý%w"6»Ÿ+ßC·iÊ¢<…{åŸ5/Hzú±Va_ÑP½â:u‰PDÆ Ñ-p ¨É>0Ê`[ÆïUõØլ…½¬t»÷õ99x˜’º`‹À_Ǿü–Hù‚¿Êü–ðcìß“ý߯ÿ÷o?þçÿøõ“åÿþùüÝøûÿö»?ýéÿú/?þýÿùñ¿üîŸþåÇo?ÿñÿýøóÿúñ?ýþŸþôŸÿõçÿûÇ8é×ßøïøã~÷§?üþÇuŠ_ü¸ûùoÔÆÿÊÿâ»­>ÜQ>d… E½Vef?¨¬bù—ñÆÕ‹¿7aL2þ•Äó-D§éê̵ª®tœÔþïêßZËþ:*'l·_*çŠaE”ÔîŸã³Š=Úù š0ýr,5A9£ ›Éˆù O +yu`•äïPêÇ­ M䢮zh׬_Í5oÁk”/ë¹×ýúÝ×¼©èÃn¢ ³þ(ÚʲҤ¡xæ7Ú.pFjʤeW´çGjˆÊßúÅÜue T*„æ¿3Ž®¤cC°W]O¨AÍ%EjÉßÀJ½ +IÐNœVÓí;‚¦»‚=ä’«½öÍú¬ÉžWX÷!\R$AÛRHÐmóS…·¹±Æ¡%¨,×Ì›Êk¯%Ö¤=¤ÂÀfò#=å ªvcã6ÉGsk‡ÞýÜôú|ÌÐážÛëZôðúü~ øôg,ŒRn*ñ­¾Á$v7yZP4_gi%~Ùο4Ëí¡E`Ž¸.Bk„Uyä0ù…É­lc9ÿŒ…7žÐýêò +„Ÿ˜7(îõïZƽãsln÷uv*Íû~P _ + Üh²7Í3€Úõþ>ø„º.(oßbßja¡O¤¦äº=€N À—&ph:>–?dnekMŠû@ç šjÈ="âµÊÈ5 û r`+!&aÜ£(ÐR$ú(*œhåÃ@A„6`A4ô@–¿h/Fp9$ÐàÖÜ uŒv-rXhÒUâóó+¢Z™vR¯=F "D§½Ag»£ô64èAÈw½èçNâŸ$¡ûuÀW¤QêïƒI,a~^2’ù苇"$(Kê…h+»ýµ?ŠÜ›@íχŠ:¼°x$¶k¯HÂf¡§r”uòˆ\¿[`…Ó•> . wÔ=hÓ°-gÕ¹·àëĉ–P‚˜ccExÐ0H9;:ƒâŽµ—:l—ƒ¶ï=@o{ ¤JɹÃÔD…f¾7±*“¶Ý!^+Ëê4 å}÷‰tŽæi#iCž+¿ÎÑ„²”/1aê3Tt{s?JÏþ³ÉE|°™=jm®avÅ{áuŸ;áM0 YAѸa-þž°n#D|P·Ú5 ¡ëÛr¦MôrIJS|Ç•]ÕQr±²5J=¦éVµ8|Ù«:¥ Ò^§dU÷Ë/ª¹—°m¡À³lm™[þ3- oïÐ$Bºhìüƒ%@'KÄ%ÝcÞ!¨ÛÛ¶*w…ႳÂä'Hn ÍJ¥È„Ò{d^ö‰ ™qÑjn7ß´ }Ê"cEèvlD‹b€ÀQ†«Ö —Í=_ªøiðø}[½ZÜo +áAë®Õ:ÿ’:pè ¦ 9„M»ÜA9 ´k³7æïæ3m‚Wh"ít@×ÒCýÛ Í†{Ä ¶Û@í¡ùŸ’Jàüq؃u|žp“ +báÝdî¬TQKz š@¡Ò{€útc0 s šÚzËî+~êõ­<}3™Õ#_Ê©Zl‹ËyÝÂôilwk³‡tu\οô|;¯ ù0žJc=¿ι¢Ô3Þ$l“(Àõ{ªÛ›}¿åãÜp ‡:' Áá`œ20ÚÞš¤–«¿•¡‚–ÅuŒöx0–pƒÈîÂ?¸N~ãña¡ÊŠä®ÕWA +7è‚K ZÃõ§@ý%['ֺπÛ-EÅžâ3 êfs0ÆU·¢'Ä=7¼Yeg­pˆJ¡^­H©¤g-ùVÀ mŒ 4¡Ké{}æš%îƒê‚R€aLx/ÎBÿ‡ššÔÑAEýüx" |ô©öc~Z8˜bCæHâ CZÒÑGÑüóoDË.D?Ÿ \S&~¬Éš/Ð\e¹%Ú8ÿö§áò2çóÎo”c„óåëóßk ÝHðó9üôSÅü‰h§f3`ÝTÐ?ÁPV³ñYó"!8sTˆä;üRcä]jû™ÅÒÓ0z­ÿ¬êzôϯ/óº®ü°Î8ªÙŸ¢ãðü»±ÛïìÏ0ñUÃåzCùI”A§žsŸÿöYƒծ糛›B[;‡6ãù·ÚMÃ8ŸIxBö2 ¾›Rø©)`ñë“/¶’+ž—€žÏÖ¹$7_œ6QÝu<É]eŠ[ Tˆ¶`>‰WïeeÏš#ˆ†Ô¶0µ-~ÁöìáD¤. ¢YÖSß›ø&DUAc«ÃxH²¢ºÉ£ž9ƒzÕX ,œ¿c +ËÀ +< ½Xõ²zV^#Ÿ&”6²'xgýË[ù³x­²$P@¬$èæÌñ­iG3Ð4ØY×']q.–¥ ÊÎLÔÿX[,r_®´'zû1X[¡Æ \ãˆtÁ\¢¹Ññ4Y7ò²º^2&ž&pE\;Ã&+‰gD9fnò +3s=­±oóǨԪÛWeªçp@o +ÃZí«Íó(ùÀÑdF;…Ý4N`9‡+Óó\Ò;®pö4D8 ¾ôXòæ°—l ðÃÖT·†ŽX×S) ±e`=²‰ÂPº|ÚüõGÁ­Ý8Z.YêFVÝϪ ¢*g‰´œ&Tl"UZ©=[&ýéTéÐë >cÅzì*¨å;ÿ YÊÒ!E l'.I;ŸÁu•¤ Mf«¯[RÅeX¤òËŠÔ÷цŒœ–Û‚¢RBƒ'Ñ͘¿ñh äùX¬C§÷u4ÈIm_Ç`ŒÚ¢ya"Œ¸†¬3½c µN;N¸ñúb†ïÍ…gëu‡ÌÞŽ6(…¨nmgˆ­–P†Bë ?ØÍgœ •³#9p›P„AÒ!bë/RûÊ;­£ É«Ôu\éPôãuFäGê¿‚X]ñ‚©/³¸I)î‚¥i]¨¨ïGs†_ J¼ó°q$`É*¨†àâƒBDHÿðg".€ú;án¿?!áQ][{ì +«¯+:PמóÉ1߯hª‹¯Û“&bŽ…õV³1ÏÏÍd´ëóK—rƒØåT-Œ; '|tXp:`Ÿn³ÒVX®Ç›Ê`b™Ñ;s€×e ¬üÜRœèš¬+YK–Mg߀mÆÖBJÓ&Œ;Ôtá¢1K"žñ²_bωõxñ Š_~ãqÛâOjh`vv‰µ4z$¹Ô¾ç7¬ç˜_»ä_¯F™Ïçq[uÁ¦Nù9ÈåF!ŽË=ý“zÈC¿TlHö{ܽfóÒäú»vw ê&Ò¸Ÿø d¸°€ïòù­[›pÏ×=·Î‚o5²sÅ^c¢;žUNb×ÐÛ¨6‡)òȬ´÷ùyr‰jŸ¹<M{\ÿWlÜû§Äxå>ÙöË ª»R/ƾ·ò‚?VæwEpPrÑ®jÞÏ@üy8Å3ˆ /µÎó-}ŽÔ´ôƒbÏ…X¤'V´ûÒÙˆAiD¯H¨ØÍ€›A:‰Œ+²î^è\7ý³ +{†ÍÃ#ÍCïŠZ¯úï!6k€–4tÂ0õóc:p øÎi2Ñ»†BrÇk‡Â·Æk ¤í¹ký6*üÄm­hlˆòü;v ÆŸ[UÝ"ÇýœÜãqƒ Ë‘’ n˜oûvÄë ËêA@Žpà—¤UFü(°ñ¸¸Ž­ó4$¿Ø·nj=w vè×6Œv†SfÅZNo9ç°‚Uº?ƒ“냲|R¤v 5ÝSSmô}Åøs€BSØM‘ØÄá­æ^÷°î)ðÝå7Ò¨ä¶? ꯯r¸Ûý´ƒÎ_ 6="€È+%HhÈp=Ýn­qe¬¬uügày>F\3X€gTG;‚4b@Mw„/-˜0ñ¾Ðoà"Ž-ÿVÌm½*K-˜‘7±.#pϺÜp¹vRé~ñý&£¾¾Ë¶-™@B d‚híó-\| Y€—ŠÙè! +ö6ð‹%öu*œ*¤l¼Vq+ú0ãL5ez¨[O‘†º¡ÊüõäSôI¢v¥w,Ô„–Ú´†QŒ4_6Lk4”Õ)ôÓÇÂÝur'Ò3J1ÝB3¥ZÊŠùS9¶ñ-ú|ní×V<vêïw€REU×>D0Þ6¹‹òÛ‰Ö“ ñ‹&ªc¯:þ»íïƸÐkí-j¬g¤Øûï€Òµ—ßꪂÏñ;oð™ØlLqMÆ»±ëŸ•R(±Íg1ʽ…žÛuƒgÅ7àro†å´›6àø®rÑää3ËhÒá®(çVÁp ž6Ü2郄«mˆ$çõo±7|‡R7Îc7aЉÖD‰Ð[ÏÚA»â’mº÷­­™ß\·c.øÑ¢ ¼vâ¡;Œþ|¤Ï¿ï &«û3`¡  zÔ¬ðw‹–±âÖbn*7ø-j·(+¶y©ß³ +.3¢…Mˆ ϜDž©Þÿz—Ê·ñhá®7ëvX{Ë úKÉVý[ÈkÙÒ*LZë©\³*g™Q‚5íô_ñ¤RX¢la‰KÃ7ílC¿£ûܯhˆç~ÞN_g<+BHu®qhhWÔ]5¥î°´½ ˆáGUùÚׯÅõ;?Lj³;©Ì3+n×*–sÀlí´¯áŠåFZèàÄSœ£ŠÎ1‹6¶f´hÖŒTÏåvOWìI<볇†ÞzrçÜ?x)ÎŒ8P‰ÂQ!ÔÓ»Íp»‰•¥µFdq´‡¹ q"ô ¹û³†±¼fäðw3˜ÍAó}:I×>ÁµÝ_ÍØJFš=ä³ø¿ÎR‡£Ô\t©¼£r0¡]GŒéÚbL‚gÆ=¯·Ï}¤ÐÖÈóm ]¶lpQHÛQ:·ò@ØPŠÌ!¸,b±o‡Ÿžä!ñÁ™HÒ6Ê.]peAbpÉÜöÝDsïQSÂ-`d26PS‰˜¿cÅÉJÞÿí}Àü*EùmP-$ªÀ{À­ø¸7h@m/ßÏ x>ÅWV¦°;؃—äš@ëûNaPY”Nn7¾ñÌŸÜþlj¡Fí:yö£f‰«^öO4“®(Ú0þ®²1èMá*Š7MYÎc²Bs-·´ÅåÉ×€*ß³ðö8`†; J íüÑ)‘0òÖéU·qmšÅðs êÛ¾á û³$ÁMhN¨h"†ŒŠ&¢±(¦~ñ±Y.µU “¯ƒSÃjφý0NX•t{æ–¾ÚJûXåh›¿ þCœjíÓ±pÞMÍA$E´¥¼bÛ =ª†š õ»\§X7dzä§ààUC@ô`·ÿ#¶íMìD÷X¾Émóì_Ë¡°Õš#þï:Æ¿§]Ñ­¥nœ,ëQ7€7Þð[_ÌxÝE¢ôõ: ýEy¥¯XšqIÝ +):gð­”²Œxã",Êh(©m©X²5ö]Ým®¼Ÿë^† 9¬¤jl(/ʵ3r¨6!yÓ#³Ñ +úEïOïÏP†ý0¢j\°¨^2iPë®^Ñ|–!»ö:öž$Xûw“eý-(ô h³-*u‘Çþ‚Õû‚‰²¾Ö!ÕlDà×Ñ°™O §”½Þ&D]5Eš¼Ö™l/õõ(úHq'’ƒ±e8•ŽØû z˜}—ÁÄm%¤ÕH2ñlG~%»‚Þ?ÅþV2‚F}ó¿mX4e-©µmB…d#Ç\Q_Å® –oѯÒA +Éw| +ù ßr¨5ò<,tÕe5âD(,o gÛmÛ‡†ñ8Rö76—- +ªUˆ8â 5Dj)ü ×å…‡¶%äc]WqÓ#…­œ2žÚ¯î–&âG¡L½el׳cKn2oþZœÌ ô‚ e¼·,¶Á´ËÜõ@˜|º|–Ö¬Xâ ÑgÒöë=PÚžÒ¢9s}L l°žÎf_fI¨òèÿùyžz‡DþzàC¿$Z™òQü^;#0N®Ïqfc£ŸF" Èø;u®¤ mô/ïlQ߆ç%Õµhµ`– »µ µ-,Ü«âÜQÃ'Ø$× M§ûRuo<–ˆôæŽ;6à\ ’ÿõ†¢©¿~YŠtÝNöEѹ$gÕÄ 'wƒq¨ãè]ÓöñOôt×ÞŒ\ÌTÆ‹”âƒ.ÕÿgílveYŽóúz‡3”5 ó?³†ö…a aÃ[#B h@€) +2MÀoï\+2{Ÿ³{“®I<»¢«º«²2##¾Ÿ@‰À!ßËæeýZh®FoGñÄ­Û?"z¸/}Ú,#¹€>F7ŠŠ°'jÑ|÷wL;ŒèË•8ð8¶èýÝΞ0J³]c 1— ?J"\û…Æ•­7«éÝ^BEfk ïSR§—AÝ:ÎF“Š„p…DÈ]cÙøõƳÅ!'û¬†ý¸‹“µQ÷ K£½Íˆ¼öËöÓ/8–<56™ÏÞ‹*ÃD)ã÷qpLk:·zpA’˜¤È| ·“jK)M© šsÕSQ¬X—º¥Œ,›ˆ*ÅBR$1M= î–°À­ ïä]¹ÉözÆ"¤¿ùSŠü)Úþ2Ìáhrº¥dåLÛñùbZæÝs†óïZoà +˜Æ‹J“Iæ`Eð‹—PÞÕm![P‡oØq¢ô +[ãûãT„ˆ!S¼w©£¬»†'…6'¸Ú >KQËlvQCðs¯mëâŸr +YxÖS³Ä ¢‚&*}&gøãâ‡2ê7ûÀ<7XIèÓÞH#öFœ*Ã*¥þ&'vô­GY}'7(gM™º\H˜ª`o'®i¶09)H¢ãn~¥G(šÃU£ð>Xe-ñW‹½{LÑv&Åg¾ª`ð<âèòÖôTÁJì§û)öÚ_Ë$R€[ãP—ÉgO™¬u¾kH•Y,›ÈöèTÇ…¤P©1Êöô]«ß…qjÄÂEè£wƒ‘0½›âSs¡T2à‘åyì5O5¡æÙ±q÷ÈÄm¤ aã<ÎÖ-9]ï—èN¬(ˉiG¦]"Vñ<ŒoŽSwâx;¿èàâEs}…ºù¥Õ¨åËOÁ ´²©F.õžWvº£Ï*ÕQÉ9kJf,«Z³¶ŸâËôºU¾7ä}nûˆ +A²8, #O†¶gä–¬ +Ò–~3²§±Ì.PŸNQ“lßVô?w¥eÑ‚.1h¥\NL:ôHI¡q.„¨+qüÂ#­‚(ˆ—l*\Åä¿' +ô |3s/ ½Ä|¢?™ìŽËuȹüBXVÜÔ–¸ÿ#®:Àz‚¾òÑìZì #§ ðƒ õØ'îפån*g¥ÃÑ#XThh?7 +_û¥´Lë- ÒÈZÒ­@„½ÿUΩÀ8F)-½ìïÞ8ñ%Ú óÝýÕ“æ¾é Þ1TEÕt!ýºÖb2_’ùÚ…y"2È¥†OmOÓ§8Z;ïö„šéæáê´Ë·îÇ6ð¯ +N¢™éWîržû1ÊáoàaíüÃ÷¥€šåEËëêRÉxÈw>“VPšŒ´ÊÐýôzÅyaÜí}+¡ĽW=ZÎÕVèåʲ+Âj<×›ÉQ–°pyÓÍ™þˆ:33¹Z¤ûkt`Æo°ÙcÚ¦1ÁŠ‚Ô:«RÝçbê¿_þ&±i¡ôåÊU@$à0§@ëŠ]ÿ~É…knäŸäÚ"<ÑTò”>ë¿Ë7õ¯€ÔBPÔaô^-œ4y#´‰ðgÚ’º5‘€°¿Ô=ù^p@k—¼!´Lêµ[Ëü®ÅWCr8 iÖ(^$<³`]zoTªšâ-ÔÓn±ÆÓ¥¢`âzSZ ìÏVdŸF?žðÆË%¼ñ§ª÷ÕlŠ½¸ ZG8l¤Yè©ÝÍñ.½hVŸQü¼iæ›\#R€”­ý`Ÿ"²., /“0ê–ƒ°‹HbѦu@@W- –¢âíõöÓ`ÜâK”ªØ9䃵‹ }ÝTx¡®|4“p„³2šÒá‰4#òáal¶Ùyü8d’°Èý ¬A  ¨0@³\€´èÉÐFª˜oGb?uo_Ÿ#! ²§ºuˆ3_Fi†UbÛ’¯Í aþ.þðR™ö'—vFhM‹6Rû8?Å„5.biÖQö¶÷9çÄzwœd‘V¨¼ @@„h‰{¡-`Ô¨ìÚXž‡z|—½cR±S”¦TQ[áå=(³˜èëÊOº$Îaçjç{K;ΕX·±?yê=¦Ó€µ™t“âVÂœ„8·€Þ^ú§%¦E)!C˜t _qìó1²?ÍO£X‡ˆZ=¼K 7$5yž`çº/t½J‹ö0&SÚSÆ(×Ýôu^IæÚ•îY{S»õ M[š® 0C:‰\Î T)|óXãÐa»© )RÖƒm‡*ª=تÊ@I§p¾§Á2ŸQH¤šÚ>ʤþöI}ÍÁ4jf°ÿk!Þö¨nLýŒÕé”.¿ì/öïãõsß™/Œ!v¸MI+e2ØH„%Ò¡I$,yéÜDêâb¼þò¶âµÎAƲ<=äž±ÆÚt’ÂTlFa§çt[D¸Lå0/.A{ZaDÃL!¹zq]}š”>y¯Ë)ðW2",dd’஼V\Š}áÉt½@˜á«Í"˜' …ùgÎ|(}Û뱎왘_âÌ8Ä™àNiCÁ4µ&R¬|;dàFô´ŸDãÏqk)9Œs²¥ñ‹ +‡sAªVæq)“‹w˜8"Íš=›·XKâ»0#tï6ÒÄÜí6Ó¥¢áÑabžrÿ¦~±Š p_4V'§ç,²LkÕµeø -cu˜¦ z¹—äó2’‘ÙLvs”Œåê²¢qÈ™z$£“L·˜Ù0[¸A|?ÜjÚ‡ƒDöýMr7Ž2ô(YQ§éӾɺhÑ;iôƒVÓjŒýúJ:šF8Õ”—ÝÚ«Ñ´g}ÎØ›.…Á[þ¶9‹ñ²ðS£‹¤ûÐœBgj‰—’Í[lPrùZþˆ¢ÃM”³]ñ`&¢í3Z2¥Ó>.0"Âq85^ÏFT6p«=®`Óª ç½Pàu¤Šîiô*lê„ûHÞXJ/ô¹C "Â/Û×~D©à‰„̸¸'ô\•Ö©¨AV¿b1ô·¾÷U¡t¢§¸VÇ­³}ûh̃DbU:ò(¶¦pÖšï¼Péö@G@†n¯¢ò܃«ˆ0Á4w>çÕŒ|Ïž¢ëY&'”3£ˆ*½E$»šúò§5ŽÎ5VÂ&écÌ–/×d t\EG¬E?Ê––ü!·Mñn¯’¥ÇiÜÉQ÷W褂X«EFЂ¯Bÿ¼„_Á ýÙþ‰¨`Zrº Ðpq#º¨Ìò;0˜Ú°—|»nç‘âÓÝ–CFâ@{pƒ[©3qú¥j‹V&Âö gœ®´Ÿ[çŽ57¼‚#B·¶º“–ôpM%%0.=®Tè€Ð“LõÀÒXA\©ž“°Æ~6SÆ ûf[p§Ø¢žnH h 2þ ´2¶0-Ôã|AéË`_- /v‰`‡”9£6•ù¬¯Ç:… ô~íæ±½¬á–²!ݬ‚Øt¦š=öIìûýÀŸ%RqðR4Õ‡ŠvVÆ„ï_mf•JH8î´±×y5Sè8òCFÕ²"ȉ)ÈUÓŠïz ã´°ú‡³õ>ª2'TÅ×GuOEäbÅ–ª©šB°l€* ”B{a½*-…dçhˆÌ°ÉÓÔ°zw ª´¿’-yW²/¢ÁGk`©ã²¾åÓ®™5šoyEk‘ÄcSCÊŸŸÅmEl^>Z/€œ[ô>7ßÉÆþßýY3›¿òDo.1ñŸì¾òkRÜ.êjæ]‚;îR‡ç¤š÷sG-K©Ÿ£ÄL‹L4îN @-–À}`DI!2{Jm—k¬Äp’~’g¤ÏÓÚBCƒ¼¤š"óí,I±4G½Ø™­¯Í/îJK­½ +7ñ‹zóz Ù P3t“ BMè.¸ ]e§ÛÔòRfÝèìá•Nú¥þÆ^Cfjͧ±qB‰Ý¿?·ºB2üB + EþÅ^M§bVuÑs;ƒÙy]_­mc¢-±7ɹg4™Bk,á{³@ õ(C…“6‰%0ŒÂÞÃÒ˳qÊ ø‘v¶7©.4lÇEÛëöì®y"Ì€=¦X*èþ0;FŠ xC¾d7XÞ4–Œ"†*xr@Â&T8Ôd’s>âAYˆåÏßò º¢$¢:Ä\&&g\®üû}S ¾Üt{Z:0iQú–C!tÄ’€û(ø·0(mtWÏÏ¢p8¾4d#ãã`±2EÔaÜÇ@~; äZ›¼Ì1ÿ(Ì»WP¥I³§¨ÎÆâµ3,xçïñÕèúîüœÄÖïýLB).$°Ð¦¾Àö°àan@ûú*ðr +ß>FAZ­_Zb ä¯f4÷9`n¾ìwN!󪇃VNöÁ§Ä¥”Å*ðs)W`Éj3F/YM+V¿8ËUJ*ÚŽ€o¯§PcªÓÕ$ôþ÷ôáúv°3à"ú)³"„Ï]‚êÊUãÀ•`³û¼þTÇb;Z³ +Èô½µš·?<ýÏ;P’RPìtšö +Œøä(êK öM¯‘Ó/öÀÆIæëu Î,ì)Þ~Ð;p?öo*`œPˆãíÿ“ŸÁ™‚ +²<…[à4u—âà‡âß5ú«°£z«îTà6{2g-ýó|…=û¡UÂ\À HSøw]×Ö†ZÑùi%Eü‚1„`Ì’·Ù­e¾Û½#uDcºÍü:»äC‘iÚ£~÷©”Î>KÚëùTãh*/¹?û‡>!‹¥ëUºøf¢ºÄUb=Ôïu‚¤ÿܶâ^1K +8ŠáŠ9.™é/®¼1Ãø+]î·%?§<¸§6ñàW?¸ÈS™¡qÇ^ ºïà²[Toû›nÖ”KèX‘›6Ðt…øHât×N™j +¦_ÀÅ<€) ŸjÞ‰ƒcãtJŸÁòHžN‘Ë{ºhÆc¡FYö‰¢@>ZÏ,ÜrŽ£€¨€jÅú.«—,4ÛßÃÒ,Ïý2@-Qö£¢¯¶¬2 rÀè}ïi]×0IÍÎò¢Ü<…ó¨Ž&º4(ôÄ*Ç\(qê«Q¬JŠ7hú¿Jîî–‚£šä$ϧøÚÚ˦Ûö‰NFººOh~®éa)†5b–WZ¬…<†°xÉ̤#é?‘5b dm¾ +îXÿ`™³P`ûÏŒ¬Ÿ~ÉA ²K5ßÛ”²@ê‚ØñÉg&+j)yŸ»ñêá‡(Κó0ÉÃåô@£EX3û£h ÄâhÔº€&sùhÃC¤%*k>v` ËÔÖu™í›#ªôR+ô¡Ÿgv9Àï§sŽ¤Ä|”¨o—E|åŸuÁ(chÝs6å†ó€J!tœŠ1µ"´Jâ f’ÞÏ‘žöÓ€èn×ãS”g9hk‰|U`“ÿnC©ãpÉòPÆí9»aýþ¡û,㋨vX™ºEC¨ÉN„/i»º{°Q¨oïˆh”ˆ –®=2kp.¤ºù¼ªCýT(|Q‡’à)Q’™7˜¦‘ã•Ùc;ƒ÷:±@#úÒaµY çÛrW oΗþŠI Ó´ó²d5R¼ÙJ§Bnc^xÌNÉ`oXçQž^^ƒŽt+´÷÷*£¬?´ÊXš„#¶øÀ(ÈD½£wêt«R¬I{§-éôaèÆ|O4Wþu°ŸöžîÁ_«‹.®yס°Š­u ­”|Æ­Íî7ӊ駈ߞŠ©ôIäÉwü%1…?­Iÿ””œ¾Ýå·ßƒØè|E/ؾÄ)Vã’¬öð¡àc ›•M”zPÍϧ·ì:Ï<2æ½@ +Q=³Úy`+‡Š|,)Ò­ª‡é´=jRaÞ.¼sûBKÁÌ.·½¨Ü±Òx]Í9¢†8ª=¢`Yz±Ø5Ëö%bav¡ Ó‰qé%È1ï ¤—up;ãnÁÍ'´L‡…v9 9jÞÑ8O7‰[® ß¾åm<_EÔðu£ÙÔŸû^ŽâÍžû4Ͻ‡3Å=«’ØhŽh>W¼_ÙÑž­ë¿ÄTuÜePãšôvšnë;þa'A韷ÀÝ:Wüè-JC"ª²yÿ‘`#dm)Ìcº) 3J›*‘Ù69L©~50ª¼ÚD +Vt¡7ÍV;9³™·ÒÌüC—eÈ·Bõïj`!Úeî칇ûã•ØLÌpc ½/®ne\\DÆÕ/ÄÈB‹†¹Œµ;0;” ‰u +¥ÂŸ.tÂJ …ý°ã)¦…ï›Õ=±”*\»š§wšÕÏs-‡ÃNêš½ÑíkAwe2$ª(*fÀ+*FÊ]ÿ'8À¸uÆÇn¾äÀÁ §c{Í2Ò¿ÉžE ÛÙÜ<ïq) ÛaìõyŽï²jºå¨éÎòÒõ7ÕOïEy¥¡²Cí–Ôƒ¤}ù|ùMWiÿþ ›‚0¢eATZ î؈ã“Â÷ù“¾ƒâeE{É»ßkÉšLsp³kºyԲϓÈ m¡WŒW‹{>¸‡ªG’ê +C²±cÇ´ž/"•f×Îz-J¾JGÙW£¸O¡­9 ¬3I÷ ‹šg¦éŸœ7ç™8¸vV~U÷ŽÖ}ȧ+’‹Í²Ày@•×n@XÈ +™@5.©ù¬æ‡Ä.µlFµÊûÒxׂ&kuгuZíXÆS +Uüï³ÓRá9¸ÇŸ×kµN#ž.lW÷W<éhŠ¯D¦B5‚úeKSÿÕ†úP0é®Xñ|ÐÔÆT" ;ÞæÚ´s¥½”³ûÝþ«£²½¿ÚÑ™ÿ8HÃ]«Üš)@€Ùˆù TÌ)/­§÷þF¶ºõ‹Oáÿyvû¬Utx…m5^ endstream endobj 361 0 obj <>stream +.û}‰…r¸Ä=fA_åÃNT&Úd Ò;פ‡+oî.h³€îÑÍös%$es`c†íuå‹ãäÊv¢Ù ]yý|ðSÆ´Ðg•Pö»ŽÛhSuÙ2umnöÌ¿XZðÓÍûù€÷„´ö«ƒ¡hòœE³¥X4Ã7¶‡tu>ñt¸úçÈÎlƒÖi3¨4S4îtªöÈ-”?TúL¯³-Ç%ȹ¹D)~ªù ùZ¯YÒœø@x°·£T½ì}{2T#ìÅÍsã:thIx͆h"3“järs(çÀÙX÷ãôòv°×#W  ’øÎÊ'…-°?{áyå¯cÌiØlS³éwwÝÛ¾]ahê‘/Ù7v•ìbXó}‹8¨B8VÔõVÐaâêšBåB},Œ¹eÌnÝgÝÓ…¾ò”ÏGåJ{œÎÄ)‡w(kÌ–ÃmÜ£{·CX•3bòEí6yÿQʱÓáÚ =ÕÒZc[¿;Ñ:W¡Ås˜m÷ Z(=´PÖ²œX&}ÖŸÅÒN0èWòµâýÇU¿"µT&Óòw}\ÃAÙ<ö"fd{ó”ˆ(UÏئÍ΃ÛèÛ¨…‰<´ÁŸtŠBÀý÷£@h,¾À§¿½ÕxÀøìN´·ÿ“ x>øz¨Êr¦uø(Sï¥Ï_MÛïQ°9áLîI²èÀXŽìçŽê¡—¿÷@_´°¦x5p‰Jùð’vÎÇð;O§z<ÁõÈÑQƒï6`⤓Î^T›Uü +*R ™Q}4øe²»‹½e:–QœÍlåõ¢œr@šßüéì}c·¾öªŸ/>>”1!m/õÔ"³D[HP<@’‘ÅD#ô$™j{ñÉWýª†iŽAù±ñ$hÌs +(£Î1#Bi#扰vÑB·Çˆq¯tŒêv”b²’¥’d)Ú+Ö ,ÞRlvDµkð³óë˜iù'; Ð5B»úñtœGðÞJµX DÙ¼“•öÙþÞ©† 6.á'¤„ +"Ëå*§^9/¦÷3øšÚî3PëêTz~p¬f·îFbÕáEÌ6æPW¢ö³Sº–,ú•L IKq P©Jp¡ìyàiŠ:w +ö<ì¸÷44mä=TðÈÇ ’Ìú¸Àb«!k±\*|ü‘ôU„ +ÝÀVZM%.pUy¡š f;o) +‹1mShì–7†žÊcû…ÎœâÛ†÷dÚÜÉsÛëÜX¼¨]œuêSR˪QE¹í +­T +|{ãX¤î¾EÀ[‘ºÅtôµÕ#HiÖÍ‚Q(¡Nk@ „€¼Œéu–ý)puàöOÍ*èÞ2Â-ö¨Ï¦´I"Ñ ²t«èÅvöQâ~üR£EW¢ƒ„Mî¾Â[u´ë/ ”ºr ¯ì0¼>íüMf²èp‰u* Zª.œÀÝÃ…lŸHîNq÷OÂãåî`õ‹ šp'AúÁ sõã!=ñ£SØb†½T>ôü)žª$U8öÁpÚÆ›{|ªIMÈ ë<šŸ8`¿5×¹4{vmJ§ìÎ"áuŸegG/a¯« žºÙ"–î- cݽMASl¿P+Su+Ûì®Ôüö@™–&©úK"âíS²R  Nüy™ÒL$Ã’j>§®‘¬tgõÝãï4¬‡#¼wNAãŠÏ@Ã_áì+ùSz]챕ó{2¹ÞÚ䧃ë <ÛH0 ˜'ÉàI(nŸ‡ž{ê”"__ÚÉ>½R”!y}*ì| :Ê<ÍêO¿¦_¯‡úý±zYqIŒ`i9-ÅÎE0÷hë£'ØÇýÓ áÞ¥¢8ªõc«º0T– +µb7û+á‚ø“ËòŽÞ?2Žc;è9ÿM·Êñÿ„ï"ža÷›8CŽŠJ6ß~ZÕÏì½SØ)zèÈïͺ/ÚòåÝŒ#\{ˆÛv2‹·£H +¾ŽÂ/.¨Êt± PIž÷Ç=ïçƒâÓ8_«ÅסƢ=¥‚+/Çm¥ï'„‹Æë`t›$Œ&¤öñ[㩇ʔüñÈ~f¿+FZn¢'GEléfUB¢WPNI3g~.‹½E\Çô QÈìtæñ¡¦Bf…Î}$)ß-S­Óésö{œyؾÎÖ·ë$|<·¡#è’9°e%wúŠ¾¡ ÀýwSjÀïÉœè‡\38rÃö^ +màýZÊ‚°Ê8êj€döJYŸ~i@›W8ánÑÀ‡ñ,™`ùâÒ7οëÔÁ$¦’ø0äè,ô“)×Zý,~ì"%¿!(•!Ä({Ž0ªÚnϧ }“Â1•†X²‚Sïb»±^¹.zPIrÏ°sBzwd8GÈpŽ×ß_o–®j 2eûø, %ö½)Ilž•¾¯¨ä""”›sS%døIS¦íêAÂ{èáB‘Ô Š³¸ôíùœ*,êu} 9Ȇg¶vx~¬ky#>>íW† +éò±y¡†3ÜŠë”QïéÊ9]Æ‘®2?n»vF‹›ío¢EÊŸñb,­Hr<¸§——oºµzz­.ÚTRw *`÷@<£^sƒW )ETÉÙ!öq–…õzg)!ƒ$B5JFœà0T‚^c¢Vf¥N¦éGЫÈ]úºî";ä]À+þ6‡NuD-ILz(É»}œøl£Én‹aµ}-èöÞ !ÿ>o¿{þ>þ4MüP>Ls`„T¼…P­ú~ ~C ?¬!•%ïEZÂæ{y¤ú’Ùã/,ß㦖Ý4T»Êx«Må8jë2EÀÄ<0¥oM·Ý „_pkÚ]¸8†û 4¯SCxo€ÙþLxÌêEÇ`«#Z§âK!E#§€ñ̀ߤ|~s(A½ {ýµþ|p…ð^¹ˆ76SðˆðºFI CÀ÷£õ×Øø¬X¾a¸!~p8 Ž.d¿¡º`™Ç"á0’ëæþ=Þäcó´ôâALÈ}®õ„qÎ$Oµ•P¢´Ä£eá tÂù7wùzPž¿é-«[_>8RˆîûI~øŒóÊŸBdÀjz¥ŒàГ{;ɸÜví>°ž8ýN»~þóÁýÎ< V—LEù‹D“GàËfFP`ò ¡¿£­¤P¸*LŠ×0]´>JÙ©?¡î&áùôÈ/¤ñ_yÎj„6»„!²Z¾”m[¯P; · >wïøP€ýû4JÝŒœ?Q1Ò8—E¿?¯«ú±t}r9wçuIíipi¨˜êŠÌ”‹ê†æ-Þ—Úb Ð\o÷}³á†ì„?9µr|çhÐí‘‚ÂH,)š,ï%£„öëþø/Ù„—=ôgDåÉFHý{Šÿ埨¦~ûÛ÷íþ?±üíßÿÇ?üáûÛÿð÷ÿíÿøÇßýÛ¿üæïÿÏoþË?þó¿üæ§?üëÿûÍþ×oþÓ?ýóÿó¿ýáÿþk\ôëOü÷ßýëïþñ¿û§ßìKüðåž×7x#:ýWþïÎ ĸNB‡äâœmÿü½Õ¢`øðñC¤4;9~/JÃeB\%;AÔ +á”ê¶óÌo@(ίãS0n‘XÖ^p}I¯j,£h ‰ø×·B" ¿ñ=(‘ÐИº:.’ï·("¨+éûHA¬¨¶N¾ÞŽˆ®ž0¦Eû¥+&¹çÕI¶ñ{ MqÒ3xd÷V6†;m 0ÙJjzG}׬O÷z+ßoPMšáì‰K¥X[-:L„@y’†GºU-V-äö'Ì+š¸\GDâs +pD”çœðqü©É$¯¨påi ÕbÓgµ(ü0ô!öøJ”c1Ç× —vœÆö8 +ë±4kUxýs„WZ‚'ÙÓTÿâ/1r¤ðÃŒ`ı3#x ¨½p¯œ°PVM÷BMý¾•9lLWdõK½m¨ÿà(7‘‘ä ¾Éçþs¯ÈOç•jÈ4Ð[|¡ 0õ " ‡óûµw`ü®®œð²t¯FççÏy$á*œ=Gõ^µdèŸr¼öJ°±nk…ƒ™gGj<4Šd·ÀŒšV{‰’1N@Ã*ð ¹ûqÈ^a8¯<ÓžE@üî)aMØÔ Ûa{6`€ã»Â£—GëJlTÌØN¨Üc ìS„È:#€Ååüâ<’‚Jb;‘Cƒ åDj¾…'D½©ªZ²1g3—>5%› ?Š®g£ÆÁ³£•Mí|™d$ÞÅ”lH…‚ù«0BÙSǽ¬ždª›ÜJ6ŒªüɲgÛ²#®åv +]>ò@tÚ ¨hùié94É,‘’9ñ\¨;_<{]Î6ßÙ4Y ©áý‰à¯µ4æAI hçuvØÈj²æÐß-¡ÂqÛœ Ÿ¢ç=-")Ô FÂ+ÝU„M÷x/fÅ})qõ‡m7°Žrï65Ó€‚ ;¯ ­õ{:XìÛ÷à‹Þ˔Χ‚%Á"¿ÿ"‚‚<·¦t.µ‰Òab╼¸=ûíû8.‘Èy\\…QEáÍ£Cbû0<ÃY 1À9Þ"ŠîŽÊz,š %Ãý+ÛËÊ^‚¹¿ ì×¢v{-¡S°)ëUñ4Þ,è%ú9jlìCéoú[‡~´{’9²èDQ¼ØQ¨š„°š+sÃŒJyò^*Ö²3ZNºî]Ì—†Õ +•2ŠJºù"´ëo.{#|N0{%*õQa)\v†·7‚Dììh¿#ûÃÅQ0ÎTW–öDèCÎà¢ÃyÌA¤Óøx‘¢­‡Çòã#:B¸Ÿ¾Y_1"ÕjÛœçHζ=tú©Îz +ð£ÖU…'ß²Ó˜©ò®³kâ-Þ'AL8:dåˆýiãô³ ÑÝtm ØÝ*éÈ.®¹|KÐ÷¤N& ?C[LKl21ÚW\ &`Ç>>‘`«.M¬ç¿u}ö4¹W{½¡‰ç³#® A™]éZ5]_aÀÝ£§»ÆÇ”½ì +gà4)KîˆÛkÐüƒõ—´\8W7Ïr Ìø¤çÇf{ ÆâkîdN¡]æ +#P¾á;Î'2Dµ[0ļò¦]ñ¶Ä—@IªyÍ›[Á¥‹ë 'ÂÏX‘IwÌNá’‰Geè)uÝ&áéòª¬Áýqºc‘ª™åÞ@9 ”ÇV•ñž t@",Mx{”Ф…%È +ùÔu ‡UƒN‚–òtY­õdéðzlȉ°UmÄ+-S”ìÁ ˜ãHÛ¢Ú!¹¶\'åë¢^ÃE}Îøô~F~z€+V/€•ú;øo*;¬ñÑkHÙ‰@™Ã>¹Ž€¬ßèµâïq?¦Š¾Í¼DßÏ{û/"”lœ¯'ñãA˜žÖ3¢öƒ¦2 ˆJ½S%„)øJß:h¾; áñ9Âë¸;Bµlkp[ïçIÚHbJ;šð&>c•ïºâía¯ñƒ γOúá\ ’¢*Õ¹´-â~7±é)Ø'àZaå ~Ù é+É{‹ð'a3OÁˆ1ßæí4€úâÙ/® +m¬m•:i™_DeZ4æ÷ïéócpý…Ÿéô~ù‘­êºAE}”t€hB©# œÄÛX@)dÜÏǽ ÚÞç=ñX…ú|z ÌO Aõ;(u`CÄ×x @¢áhëózx½EÑf˜¾M)˜È4‡ößáfÃnm2êQ¢ 6 &òK×{ÄjƒÍ—~ ¼óïçÁ,k'”»¼â{‹=¾–Ÿ;Û#Á(u“kšÂšÊ{±.›ç9SÐò—j VPļËFørSao[Ž 6s0[r"lU"_\”ð’D`ˆòº’-OºuûãÅ`Ûiu¦yCDî…KSÈ°†KÈŽÐœÓQTcêùM‘4ÙÇ»¿ ë/Æþ^p>¡$¸ç+^ ŒÉÁ›ép2‘ÜwÏ3Øö­:ïº Éq¶+õ‹“—±Õg/Ô"=gCO«‚æÒWǨh¿Þ—ùú„ð•ÊÎØ +ð+˜veBf‰‰9’ýÞ %žL‹bÓñ§ã^O×Iêõí4…X!Â,#¾hså8òº_DÌè1š÷‘¼EU 2¿”„9ñe¹ E«o¥ÒL›=mw$Ÿ ÷c×ÊöõsÄ}5£6X‡6oŸƒ(×î):S®Mè*óUúfOµß÷T­Þçç(tê}& ñáâàÛ9UW¡q!Õ8ß¿t`O»–Z–¸>EÄë2ÄAqW·ÞÎS)¦u&o½N&ÖO&Æ«OìYA ŒÚ:éðº"éCxJG$=6û(?×âÇ‘eBlù¥Ôã3S¾3àÖ&1ßÑSPMSgàµÈ<2ˈ´ÅI9æ~ª+žÑŽƒÊ8ð05x‚ÒgXF@š0⚤v}.˜%u’;ì< q¨í•äñž–ª.q…f¥»IšBT9’Xÿϱˆ-Û?d2û­úò<Ïy›23Z wí¥¤õÔÆà=båCcUÛ´¾²×OQíhBÕ¦¿"YýßÅÞ, +å­ ÞvÝþ¤½°’óÕǽ”¼Çß<—».·›¹¡{¢ÈX:©È×*é±#š¢~ÚÊW@ÿáßDãýnæ^_ç%¡°cêÒ–¢úV&ùEœ§®¬|ÙšQB…»ëwó&ˆ5"¨¸ çÔ³)Iº%Á—Ìñø¹UìÁ±õ°›c產ˆÞóR‘Š­ÏœUÕ¸i7ß<ó-*‡L tbF‹€LtˆŽ6 ÝÑ`Ý?·±ü:ðÀ;Ec”«_Ÿªª]àB¬óµÜPC@‘pÿá ‡ÏK·Ê2kA34°kû£Êq.úPº²S[Á!;XÖÕ$ë‹q«Á{[kí%¶=­Íø’tñj�-5zšNÞ|Þ£(¯ÓÞ¬½¾Œ˜Tì{QUW/ü-⋵ñ=4 Ú ÷†ù,=÷Vcû½½©*þ Ffƒ£õù6Z¢¢U‚píÑr´z$$: +(u€ñ#º·Gàú)µ6åc¨%dºÿ +Ùâ&N{/™o ÕWê‚‹]«²ô@ei/‡§JCçbÏ*û'(TBRÀ©Ù+FUõÙ^þd)Å΢8hjßOéÀ„ÀȾ³î=!!²'ÂÙ‰Bu,ð*lͲh]0W·fÄA1ªèêAõœÃ¢,"(”Áö·ÞI¼{½-",¼‘4¶p×y¯ÄÖ¨-°—p8m¬òOœ§x¢Wn—Ž˜ý®5M¼¶ë–äö(ÏFh°Ò0k(·´·çØåôAÀµ¢6]”L bXì„ÃÝï‹o9n¹‘¿Çu`Âô¥UÝ’­ÑÏ6±Fž'»ŸÁj×朾º,RÊ©zÛ}¶;S÷ÛOŸŒ –OT8•ù<˜¤€Uiì¢|u•L¬CyuÙÚX¬" +B}TÄ2dmÉ?¬0»PpIHoA°÷ÀÃÌ +£'œø:w #Ú!X@]‘³îSÄ&LI:µ#LJü=µÖŽt+P#^hgÿ…†CÓUmᣦ”gEVʨ{Ó+zÚMÍ8ÀM +(gS@”-ÄÕ¨Î%@ â " ¦‘YAŒt¸W¾iœ^8ó›@\I*oßl¯+uÛ±j|äó}²woƒ +ù;™ºÈû †aÈvˆÀÞSèõsÞ©6 wôak^E|¹RñÐ=?dg*ýt|©j•^Y°v„4möØ•z¤æœè(œêQ Jã mêÏ^à‰2(,öƸ ×½ol+¿µž³#2Íi¿Bò¼–íÄØ †A1zТÈO\»`.Ÿ0~5 +_û@·¾È,„•Ün½‰âbf|ø%:žì;]…oí\,v¯–‰æw°Ià'×½°Í‹¬½å&íDJ2džćû¡ä}‡åRñ´M4ˆ’|Ãøþ©ñèK„š ›l#ð»¹çÿ‘ìa)>†Ì=6gT‡Š e} +L¦E +¥¿]ï!PØ`4ÄÙÚáø¨jüçˆxhÔW÷Š´ÿ7ëô~ž$bÃǪœþ\„F×ô=äÚÅâö1:~ˆ+¬zª˜”}š þÒwPÔ0m÷~þlê\ÈÓŒ)4S‹Ÿw}ß ¢X" ²/ƒ"+º?ˆ Ú%`@Ä¡žÚ=Ü|è@µÖ9Î" ú¯«Dª‘¥yjKoÔ#Z¶ÓK÷¥jFI|Û"D‰J;‚E~G€PCÎ_Øúº’…; &UÌü8å1´p¿!™@¿›2Š[~”K#›am(öÎõ= ¦…ÏiÛÛiHÐCM‚¯BóæAmOC…·‡@‹!ïûûµ"eµèÓûù¾ó|ßàODv¾§L{€žØÎÉ`§ß#~{€p¶]&Ô~úðoçˆoB­'BV¿Cjh3âÈ–È”‰ø1FDÀy¬Ïx\êÎS®€¨iI‡¶á<õðÞ¢Þ¾Š˜4{<š‡ÿE‡|ß l¡i ú?pöì¦èÀºÙ[A… !áB»3´¤ÄA#Wý=£Ñ-ð×ȵ»B­¶zq³Šq£pÿ[ˆÌ3U%ªát¦Ý\ý¸R¬–hwÕˆzH%K ‹Q#˜a5€ñ̲b÷ä•ØUó]Ø¢±Bd8]‚}Ûš±jȯT”¡sÆ1cA¡†YšÈ0}î +¼q»ÊŸ#Š¼@ Î{*¼?é-ªå½ŠÄf9ãÒÈöû~?vÞT*äwT¹Ío¿=Y>ºî„öùÅyŠ½$–?¥þ*«:–µ§œ_DìôâT6¿8–ƒ‹Å-¤˜ >91ˈ´?XoÞæo$+“~ʲ¼ßÞ^ +Œ•!Âç³ÀLϹdÊ×ø’ÑöLˆT ZÀ¦SÜë÷ºÞ.;ô·©ŸjcßLÐu¯;¨Ô Gãâl„ ™ØH%¨Fº!sù^(fìÓu.jõ0’Éó0×AÚ8õ8uê™LæQœO N=\NÑLºõ»4¨â¼ƒR’ +©"›…QHìí9ÄQ¼Ð›ØºîU­}:©w…*î(~Êà$::ݹˆ’n5ö°~iqÀõ¡´¹ß«˜QôˆhÐ#¶ÄçˆuÍ)f¿èøÔ/Îs.òShàŠÂ§íß²[Th÷]d€–xs©™Ý2}áy{¾BÓúJPöž–IËð}9áˆOÉ¢nÅY_A°z²åKŸšŸæ‘gš…`îµ]pD¥L‰Û9ÝX& +)qêT{Œc +÷Чæô*ðÉN”?Õçh™%°‡9´R H›ø1ï#`ßegSWYå=ªÈ÷.RdýšÙme¿¾%˜O€‰y*Ô{ùAˆpðƒö“‹ˆüÂÔQû‚cÅ€Yç ÜZˆ%îGŒdô'J,ðŽ@>€¨0zˆ‚ÜÅži½.40ëýž-hj`3²ðú¤T¡D6·ŽÈ,P¥@øDxïUcÆ*ÞJy†JÿEž"–%§” 4¥â=£ÐÐøÑÐÜå]ê¿Ö@:û‹J¥õ…=ýñ`q*äÂÞ“&}<ÓÊe?ÓYSÀ#béù÷*„!íÕäËóŒCú›Eh¾Ÿˆª¬€y‘JP?D¡Ç¬bwÎÀ1ɵHxç´Ò*+{G$Â;z¡ê/"îþ "%Sº¶_œgöè„Ÿë"D­ ®TÞ">/Ì‘Õ~ŽBEfÃR„ä°YÒ¦Éá”Õ$È$€bÒ+v {^À™F‘ËÖ€–Lšm:ÛŠH#¢øf¡"8ÀC¢Ð&U¡F5ˆ>oÒJ5aÃz…:vúq¤*˜BNjúì8.} a# " xŸÅÙ +ÔQ °…ÔÏÌÿpºÚH±/4â:õDúi°éæ‚Yé½Èë\Bf'd ¸Ñxí¤‹Zùç¿G¥EAC[àDoŸ>6iäÓ¤+fó:H¢½ÉGÞ#Xyu©Û§?dû÷ +ö÷WU–Ù'ÓÆ"±ÁWŠ¼tj8biÄ$«ž–@×\`¢Ì#€ÎKJ!Ð}þêN]‡?nCZÑX@ÙÀåÏ#þ.6P¢ÿw¬wYè?ÐÍšåu‚ÇÈ\dû½óòc²Ó‰é >Ì’=g˜QÝwÅy(íÓ ¡\C„Œ'”gG7Nn]6°àFP Ýõîžyà-¤t0¸E ú_`è#»Æs› öBî¾ùPËÑÀb¢Ú¬c,VãB< —©]3£œÑö¦q/úÝÓˆI¦”¿FDÀüt»Š$…‚ý-ÈLû§Pë9@O +¡–½šRÚyöhCžœ¤±ƒAàƒÔI÷Z`‰ñ—ÇE[XYÐ0YJ«ëC;F aöb!°bþ¬ÿl·É[ÆV ÂÑDÝQÜóί¨C©ܵ#„[„E­É`TÙ¶⒧ψËÃ@Ä„ÕÑnzÞë`Œöã¯BÈöWDŽnà(S¹ã@6™T“BÓ}›ñ#cÚLKî)ºPa?N9‡û푾€»ÔS)Àr`ÿb¾4’¬ýc¹ß¤³‘E.Åײä–ŸÉ "XV‹&*FÓ#É-ýD€%æ7³wÛ²iÝ®¶ãú—A#!ç¼Í@p,ÓU'R S¦ÏÞuÿv¿RóîÕé“C{¥OÎu÷b4s¥€%Ø¿²>!áú‰­x¢JÀBÎQ¼SÚw†e +»ä‹F@ +¬Å\*ä7]¼´ßbûÞ·WÊhè*féÍv°¥C±Â”:²=tɼ ÛÎûƒ…f-&½üÌÂ|2¶€Þièr#l•d´ïß 'jÿG<ÄŠÝ/ÒÒ4iÕ§¼]¼VӺɂ¢øG ~pJE$¡®ë` i²Ï x¼¦x¨Û* Ù/ßŠË ‘mWè¾ÒE„ìÒdã +tàñöìl ±‹"BN¦ª\“ Q^:à¶8en"‚Ï$a½.ô8sµ½Î}‰y=iF ÙŠ U(Ãþ¢¥5Ô€yò¦ ûÉÛDN ¸”tã.Z[­pK¶ûÕâÕAõÔ¬ÕbGî ý\Äw—.«-xÅû÷Š ë ïñžß¡¦ÀUA¡ý]Ó¢6u²ƒ™b=£PÀªêÅœUt@RÔ+ñи«å70Wß7¶Š¿ë,L ådôZö9ð7‘ì³|ЭЎ[yZ#œÛ’ˆ²˜”ã}§$ˆk{7Ìn¤C 4¥äP/Å®Ø6Ÿyåd:ä{êËK Ü5iÌѾ*¦å ÀdHÌÌs"w·ÏÓ€í.{üDIVYtDI jNß80”´Ç‰€¤DDa-Äj‚Ç+”ãBþÔ߯°;ô ²’ႹÎ×A”¡ù3>Œ`$BÃE"|]Òál¼®´7pFQo Ší§Ã¸õ¸ÒùžäÀ•™¿dg”ÁñÊmà3é÷ÊëDÚò`ûC†1@-¡iË€J~S6™M–ç¡¡é^¯ÒÄ+鉜–d~·sÅL`©³Àî H³•4€/ŠÏ$ïÓö ›ƒF&;¬• RnèÉ ÜM/|xÙ}”ç#o<{nü6郃cl!hÖ¹ffàØow¥ß5*#.ì·“ª3ãFúI°`B­pÆÖ˜y­1ä`#TÊ“£Óü@¼./.×ç‡e¥=wuÁTÑ×aÛ"‚f™öºÎ×EÓM…¬d7Ån£¸± s¨øQí»ÌÎ/œ†RŽÛ«%¡ E¥M(«ÄÓˆ•@>€‘ôÖªSñ2gQmYm¾ §•¸x¤ÑÏ™÷YZc€ª Z™š+- Yˆ¢ìm˜«h{‘²Û#NáoLñ½ÜÞmM—œ^-ÀàáCÄýuÄ«HõýÑŠ§(fDŽˆuÚ«£å"%nQ$Ú;‹tÁºà Ñ âæ—(W i'* ŠÊ<²¤µ6›Nžg@âY÷¿GUDgFþ¸êh/þNÚ€T<9D¢èošû ¼‚eß%`·{¼ÏhHµÞ¨™+C ý‰ŒòÑ‘•µì11sW–.Ž™lý£JÆB$eCì(½ßr¶“> *€Zº;—>ÀO¢ü·Ôn"¡Ä¸Ë¶/$òþz46¡‘ôá¡ 'ëœãxŒàï0ñ²º‘ƒŸÙ¡¿%“&5 qaZ†¼g9âÖqûVÅ8ÅàçgŽôa‰`ùóbpF +ÿ\¢S¹¨åª…œ?¶§èØU%‘¶óhœ•¿tqøÙœæ—ÜÔ°·'ÃeºæçTã‘;c1ƒÂɬ£’Њfúƒa›“Â:ôT+žÁ¨_–ÑÏ>éU˜x,€'i‚ŽÁ%åô™ÜQx²a­…¦QALÀD„箈eCÿV^>„k*rÌÅS‹""öä åSáER®È|„HDù‚î×öàKq%’j"‰ñ]x÷7N¡ËùXT5Bl˜ùñRA¢¥3«TÜ›¬^Ù+64FG@œ€VðâÄôÛw~Ò´c"ç+i —ÔöñQF§Úé šq:¶¨Ëí /•—¡Öª<2~p æUó¡ŽßœÔ3´ÅU ¢Äì`Ó +p€P€°Èd3þ¡v¤Ø׎rH”’"jF¨&)MñÐdJ'‚z°• fz"ŽŽÄ¢ŽnŒˆö>BÝ5E•8¢x›‰:§Yá=WšÍ¼t%|áL_…ˆÐN*XDe¡Qr¹t€ê^OЃÔˆMÛBä¸Ø„¶¼´_ Ö€8 ˆÀAE¹Ú”.Z¤J¨¼ ’À‡¤Øh\ÞÀä _´€úıþÈ7%2ð彟 -lÒD€÷ùƒ®×F;¢%8ÂzOcÞH ¾=[ +g šcî9”Ó}0èjVæ»*3@tx”:Ÿ÷•UP%A(!¨âÈ‚É[í£v6»®èôŒY‹™ïiç!ÇC;ТWlÃŽ(G˜+zzË–G ë; +ÔT[X/9â²#¬*1’ï6¨…R„ÊG³¹dTì(ïØi¿t9`D>Ãfæq6§yyé¢M(NÁLa>@­PUlë|aÙˆ2ÐtPé€òMùØú†Íq­¦'¢±ö£ºxTSÝŸPÅ¢· €h‰ûs¶®(ÝÑ=G•<†uÜ—HÝû.Œ½àrÁz^ì¾L³½Gi²¨m+þ\?ƒ2]õèn£¬ +K Ž•?ôÅH4¶à= +˜ÚÔ›Èï*_ ¸˜¢ujOÕ:l¬s‡x&àÑÁ’\‹n²ã£#C5Ý#¤ÆÇUGÏ!LODè[tA¹F¨dž u–ˆ]Š=õÓ"#J¶“æ¡ú3VçÈÞ)à7<è‰P ïFûTÈW5¼ŠF]q¡_vݽ;²ðÂã”f:ˆÂPÁïÝ­˜dPˆ­Ê—«>á9Ô˜ô +·¤ãŠggËÍVž#$¿OÊ[iu”AßÃýhíM­.÷ýæFo¸³-…ž¤Ç^Ú“#º¬´Ë~—äû£ä¥#Ek§¡$Ì{&É‘A”@‰°0)½ +XÑ|:Ö%@8Í<{çÖõ®‡D!ÅœP#o¡I¨ ÂŽr³ED«>(Øÿ"ùÅZªv³œ"¯¿ý$,ÍÍ×…´'Ohf3=ªàRý:Tl  ÁþHd}" Ä :øÙQš2œ©UýýÝux_@§£_£=€3]³7  ˆ# ° XÔáîH±ƒÓƒT.#‘¶‰~@Ê“à˜qÖú·¡ ®`î2¥=UµISÝRa<Y;÷¦ƒŒš‚ôZЖð*e¼ÑV~κiñUý °ÝOõ¯Ü>š˜ç]sòIºr§Vos˜6 àlì„Á¶A=jÁá @o5×½c©¬ü}×= c‰'ÝÕ†º¢aX¥51°ÊsA­(Çïù´×õñ +ÌZ(DÍ 3Cçz{Œb÷Ÿu#JeÙJLÌËûÎdë#Š {ç,ó>’ó“28YÀ ¥-Yš _^|w½úݾu71Oãù°óG‘!²ç(ši6bNÔßU‹:\†äÌlpyÇèÂUÄR”&ížÍ8¦jAû\€´Õ5‘C2ã®<”Z|´ ƒ±¤r¨9säÝ‘þt +€ry@½†‘Ú‘×ÛÙ9S–FëCn¥ŽyF¨¹¶demg´?Ÿ°º­bÞ!hî'„¡‡_øË(VÀ_ûœgDÒAåYùžr"dªì¯)`˜jUD¤çãJÚã¯$$7öÁêk¯q"Íô×rKPÁYÏ^Ä™&”t™Ž9 \™æ,¨ªaÔK âí¯ÓÐf‹š«ƒD2]’í¼)³wD„z¨@ƒ#â|%ó‹êd}?!‡É"!G]¤‡’š*(\šìÔÃÅŒª±›ë½+ÖÂ^¶…ÈÊJ}¥ßÍ&D…ï“‘®° SFbQõÌG{`Å2Ž~Ä”Šn$#xLéø’« Âî¸î-P½ôXÁÊŠ=Æ’¾§Xå`†}O…dŸfb¬©|ÑG9c ³_í¯¯åƒÇ›/…Á~ã +›=´®í ’K,-Ö¾›J;JJ¸ÊLö ïŒe¡H-üM³F’ÒÃòF ovÊ()f7¦fØ;°N™"´—Fˆ,6,8[õªŠÀnQUì6Ï÷w…ÙŽ°ëÅž(½(š-A‰²^I…sH­‡^%`³u„0h4#„QBä\äØïX‹jŽ—¿ú@ÑäÌtTŒÚ/,Cmj‡fK8H†a¼beŠ¶6Wz‚Š0t”™€fÉ]Dº_ +I ž%–Un +C^R-P•w+(¥Ç90ÞtV‘;`§é~!-·Èq+¨ûyAæ—°ì)™/¨Ö ­™ÚÇJRBÕ¨Q"]OKªª|ÄL×Sá'kÎוüMûJˆîjñ9LµvÞ&ï©j—E™lÉ*¡©m ­âʆ|–¤¦¶;OPì?Žý•d® Ó~é…™[ á< ²PÛRƒ³MYeLQ邽Ú¸TV?°<ÑwÅü*¢2)K!Q ‘o—qJ`ÒdÙ}…=Ôy»µ/Td³a¥D™Žlz=”"‡ƒAÛ¨h0 ×µœJädÚJΔ–Y×ïhJVIÀÂöY¯¸:à¡Å^PÅÏš·[<ÙËò’ö£šóö¡ÔûÊÔ@•"ƒ_VìOMM,7¯W +¿„¾Œ©¼ÂÄ2‹ó-|÷‘C™Ñð€t¶ÙÒ5‰8ß`쉠2KDºúÿ”i¤"þ!D¨ã—¬;rr§øž¼lÀf(±âl*8@Ýå5]²§RÂ<njJÒU¯ÓN ¹q}J l²·ÚÔ”¸s—)´5|† 䌌¦1žtŠ@^3LXÀ5X.Vaó5 %±»’¥í˜@ìL Ê?HîðÌåä"úeXª¢«ÈÛ—¦h ~P¬ƒp‘á@Ѥ8´#¢Na“ÝáifD1õӈ_ÿM0àJœó¬wò mØPG‹Ó“Ù¢è4Gúåû£5Á~À€ñŒ-§Y¬–ð;Z8:òÞ¬˜3¬ŸWØχ6‚]O³ +R:cut},IÕv§²½úyd •¶¢#ë¼Ï+Ëî‚ ÊÚâ,ð‘Rº]Bur¼TäWx ²yT6¹ÑZVWrßò$ÙØí&̆)¥…InKÂçlß„§¶¯›ä°³[˜lµI5ª£õRN’¾öoÛoŸÔk«Ñoa×ô×= mœ€è„æ·÷僛ú’”pG`,Zx ÷•’½/^Iøš¡N«ò?TÛ' +ÿ‡³›•Žh?“å2?‡/¡±^‘c/‚p´U€-²}{âuŸÂÁ.fVÆÍaa†¾‡_ÒŒl†ljï¦MÖ\–Ù–Á+Pu¢#[nœZ—ÖÏИ®ÁÃîû ¦+ ¢?. "„üæuì¹²óVÈ.eŸ‡  Íc&{&o䥺U= +~ ž)Çho_çpP@[9z /; +S"¢2£§9J©ŒÔ>Γ£=yPžøÏ"ô~I<àßý§ú¿úDo~óñŸì¾r~—ÎW嚧†g”TA`ìçR Dé|ÎIsž÷ ÉzNÈCµ$#¨3xÀÕ)e6ÿã9{ ¶gïÖµÈÊî*f‹‡¢³º?ac³ùPuaozVHÑNô4ãenbÈ/ï ú’xðá&Šc‰Ð„'/žG-±‰¤­CÀ;#´{${xAxŽçˆ>qfæi¯+ÙdØQy0I {Ƨy $GàÊ]a©¤’5t{bŸ=©ŒP„§YdÀBȃ@c@NyØC8ƒ,îe>Q a­olÈÈV³3ƒQAø(²]¡ö¯:Lªºö÷€§@RözÔé&3'ÜO¡¯®VJ;øSÜ­¬jíç„8%"د¾Q×´‰ƒˆo«Ó´ ¸^`j¸[%>Ά¢ã4½$`Z€²˜çŒmZ¬¶""ÀÅŠÁÇ/°¾‡0Ì<_££x·£À¤ïóŒhžïààQd0ðÏFX¼í âBð¸Ðíáu +ºk;`Úy•ƒTˆ8Í©Av4K» (æ÷ wgLJÔWôä=ª!yp^l]P²Ùt² ‹ºT¨Žùõ)HîJ±’ôÍ|"ê‰8<˜~¨vo.ßl³÷÷R~ T«m¶f-£ñÕh}*`õÐé$zß:þ­X¯tVžðÄ'®,¯¼OÓŠ”‡+v¢‰ +‹‘˜JsÓžå‰Z (*n@­w±Rƒ.0j’;ï×Z¶);ù£%12«)èéÑ©âQÅBòž®­ÜÕÞû6q%@j”­‡Y–î½{¾†âæÇYÍÁý§æ4&eÊØ„€´ŽŽ YZ:WIcœVÄž7»kMé“üMß)<‹khåWm A °ÑžGj·‡žÇ{DÔ ƒ ÿþq®hÑ +Zuö¶N=|23µ°µXª$”I#aÃDS®. QªÂÐ#E¯(Š$žÇ®¿ ±uR)鳦ß~>#û‹™g•'óüuôz1Öà™/4µÕź† “䳦݈=7±]Ϥ†Âb=ØÖÈÏe&:Fg3¡nµ<êp8>¼CŠè·S˜ÃGˆ20¹—þ-C¬ØFÀé%­I7<`Ìà÷K–ÔÇÌGř̢ŕ4í Ü·.ڵ烸A;{„SLOñqE¡ô +Í=èÚ£šw<¡ë +Ìy\ÈýÂ; +€å•)^"ªd‡ÐãJQÅ’wª|±›îäFÊmóñ¥ð©š2E{°mHu¿Ïã9þ£l—)ÕÒŠ›¡ìîB-^‰¥z@{&äÕŸi~ú_T)(êàíè„䄾d›$@¾¡ƒÖ=‰ øƒ)"K&¨M±KMëq#ðÚ lR l’‚»r_ß©ŸtÕzÑMtÛöGƒZ=¢_)¥.1N”„§çQ¦^Õ6‰`¡W0,4ÆÛù( €O/îàEAah¨<Á…×÷5ÖmC˜¤(.ç íWM~ë’×öû¨':¥#˜¬ïQ솚"<èìaIñH›EÌרT‰ÁË8Ö°iE_í,OÈZàxg‡(!§ôùfë3çŠqäí‰pOjîÅ•BÅÙ zD*ž‘o™» ·ëp%ã4G¦6jj6FØt ð['h<ø•EeÚ¡eA\PÏsH}2›SgVˆËWŽB9²‚ꪤÅrªg òÀÅë×v]#¡©ãÖCÒï·èÓ¿V +±{iš3B¶ò©€‰<¥±wRý+°=Ö¬/Ž‹˜·¬š èžBÄ[Ô!õ팜VX†îÎR83P>Ø£÷³€G#fïÊ¥Šøíi”„VMG­´užÑl€r|“IàÀ(¤lü’l ¼Eá†eïaÏu¹Æ­·iÕð¶Èt z6¤Òi^¤¢úÈÌh³‹té^©…<"½ÂÇî$Sæí%#¿Š6[ƒ©Ò²Ú*)Bž*n+BÙªæП‘œV?Jø¨o-zy®x( Þ'©!ÏF¥[Ï \\4ÍL9ðÄ玳u†+ìÃsG†ÊUÑ¢ùuZŠøÑ͸÷êæï{?46~‹8ôÔ½jcôsÞœÏQ0íDA<÷¾[.ßÛ;àn‰`O*ø(Þä÷¿Ç¤v³ÑHüùÓ&oŠZfÁJ4mŠ½ÿG]ÀîK!ð= +SZš4O¸š*[3£fK•D²zW„ýÊ«–P®Œážö^vÂO0Ô\•VþãI+ƒ` I`öòWcÁÆ¥e´”HwxåeÛç¿’“6=‡v…r|>¨`µ¨Áè‘‚âR +…Ê”+ÐÃÿ€[§fˆK§_¡œåvï‹Æ%ëƒâŠÅ4ö™y±§ÝD«êd·ßËÚì*Ñ@Ô}¼Àº>¡¼– +ËÒ!ÝËA9Ù8Ž/?#8 3¦ÒCn]öe)Ák!"¤ê³}ƒ8}V½½Ì +…D­ø8òÜ'¼jdµ¤yÚhEcá8 ïI®¶Kª…6åRÙöî°¡–FÐ\b“Tõ¦ 3Šd¬“å€þ!WM:?— ý’;$˜K4dvêƒøTQ‡Z¨^WGÖräh`Þckz.ŒÖ·ŠÃ .i¬yÔZìFC–¡iÉ6Z¾îd„ê ï« +Ê“ŽÜ^9¡LZ]ç`lÓd¿¾_ÌZDµö®uW–©ËâçZNþö¡(þý* nÙcÿ¥æ(Ƴ˜‘¡9h¡Ë³µL–bß|0¤Ï^E.²ò8•³ˆš'ê¹# +ŒèUAA°h=öY0é +j™9Ðl¤fv£.áµîwJØù57K +z( ‚@_‚'Ü£F& ~;åøí<óÃq1Õ8È'¼¸DÔ†C컞#µä!¶Ǿ+®ïìÂöŸ>Ë{„ßÿÍ›ðsŽôX$™8sÛ¨‹<Ö‡”] bD„ž—ڨݙâÓAÈ*Ï!‡Pš”cÛÈ6÷X@³¨e„á½Û,1åÎæö¼Œ¸ååqº¹n¨µAbE™ŸÃy¯gÂá÷À @çöEDîÑ•*É|øUxø1J‘"RˆËqâØ;/möM.Ì!E¡®š6Ïéâ½EܱŠ¼]_EÑÍžj P6g# œ Z¬LV;dùÜôé%‘<‡@µoÊ5]Ë—G †¶£:üpà + ºôÄi|qSÐ¥±½ËþyÙ +ÇÔ\‰ÿ€s\úøí¹Û¸E¡—V8QaŠˆ"(Ê:"} c5€Ëq):D FDŒfÊÜüŽœN‚´¿I¹‚fâÛÂh8ë|*k¡€ä£IÆ +ƒ-äá°{ŠyœyÔ÷ˆx$-ÌÀX‡ºÒ:˜L¢HHÐwl²ô£ó‹Ò^"óîS«‡·ˆP]±3Wì= +mÊ$R +O¾Q}hDî +B©*R"í—yëø·ˆ¸gdÜÍâgUâúý<)Ú3|r‰YžŸLRö`û™„Tæ¼È‹ƒP¢Éµçè×òÚä㹶7Ó+Z§šo‹œ•i1Ôôö¢åäù9⾟ð)— ÇÒ¾8Úd²ëxj¬*š.l_E¸Êár³Jþ(«>Áè¤3JˆMj'SFç‘£Chº+a®¸'<. fM™œ±÷Ü–ûqór’@ª¢y?Ï•ù„Ýn¾ÕRä[¬Åϼ€D‰ƒ(§3¨¸«ÌK”û¥V–ü{RÀD]MgØÈ{o­4/{:½kÉ(y3u éár7ÍDép÷ å÷ÄyÌ÷…÷8Oó@Ú:@ÉØÇÀ‘â„tÄs|“`5K°̈W”1‡2Bû>‹"ø0ô7§†BÔ©,™B'èºçß„Çó8å¬òÊRªZÔü<꟣Ôu0µ,H>f½• C–­ë9ð©µ/&ÿ=ªé½É7 bò˜´†A!0-TƾúC’Þ?$dã5“!K½t8KÌéÔ7®ØôãG1'Uè„À*CÀU„…õØ¡»£#D…mú™ÞíK–×Ï{vc82Ÿ0àÖ¤3]_Bçk}]»8 ¤ÔzÃîf´ùnÐj‘m}øýÓÄÓS\•º¦ïç9ÇCØíM½Ð#&²¶ƒ\ã=àÛåñ-ˆÕ¤i.<@JQV¨±Äepœ‡4TYF±šƒ¼7VŽ&lÓŠ)óÃ~ÞMŒÝZÐ&@nTöTÖ¢;þ§ðÝ/JjZOc†|4 &›p $hMvc5†x:)•™¾ýëÔ»)Å°ëºÆÜéRÑGÄ?í’3÷ûÁ¡‘å…+“ÖVýe߃ê/vʨaSÞ»¸aCÓ¸ÓG@7©<6I3ÉQsá¸ûJÿ>­jp@µÐX¡»«_ÂF$ç±KŠ ¤˜~Ù•ÕŠlã‚$+@û:s[ð%¿¡v¦ñÜ<^Àd%§Þ +*ä¸|”AG„J,@v•µaã?'­;ìlÌ”kü 5 ŒU¥o3˜_ÌTÜ[z‚³‡|…j(œjSÚ¹yDÚ榗‡E”r+p ´d6j$ +à]"ý©U– +² ç7åfÇNĈ™o~dW“è¹b$D5e¯ÞÐ\{®S¢×r²w­ji@y„¿µ¿‰•­ è$_5p[µ¬ÊëÉH”Y·wÒ×öÙON})‚Ju“gvÇS^"ˆŠZD>C‹Â×–wkÑž–íÇyà¸Z&¤!±µ®DÈ°Í€´®¿È¹Û6³´9ž?§–?#xüBúþE:±—™ÔoÐN(¾áËû¡ÌÔ‘à¤ïQfRŒhjÔ5‰ðö«¯ÓP>¢u„•Løë'tÚsàÌ—%=³•Ãæ÷1¿ù8CïèÉ™¬¬áíú:p¥µ&U¤”¾Ð —FѱµoBp\ðÂŒ±½»_ë쌧b:Õ5>£]wüâ6ÿ¤¸8é©fÐs>&”ý!œ¨RyÓœ­¼HìÉZü`†u:`É~JߤɶRý„ ÙD?OèRpîñhÔÚ䶘;ÆëëºÂ€WìnýÚ0›+ñ—wa˜3€V9ÂJ GÊ"8Ÿkk¾È[óNùþø86[û~tPŠ +g#@øé#Âç8òÇSüˆ‚ýîtUŸoÇØú®w ¤½"ŒDo„ÖŽ^Öâï Fþ2r¾hý÷Ï(‘Tò¥R?óV t…³waR¦6 $R\TUú£U7k}Se®ÍcA18ª‹ìÇNÁ\µŽþMÁû#b\±¢Xý@–Õ]E¡b.ÖD­£g­€—)Âøtý„uÙlaæz¢!s¼ÚúEOÍôÚV˜+¾H,uLQI®ÌFÁŽ +@ry[=z’è!ÉãØHؾõëü×£vJbçb´|\Kª9UýŠWü=Sŵ/cŒ!»E]UšÒˆ\ϬõŽð»˜µºÔíÓÖç´O¤,°ï5Uäx@ôQ?~?(­Ii]%¦Œ0÷Eìsõ‘k2–¡S ¹QÝbCÉÚ€,"-ŽVìkzøˆRÞçÔï‚Ê|[—rÊŸ(FÞ™¡ÖÌûˆ"¼E}ˆkÓÜ7 Cr+œóAŸ³;ÍòΡ–Îœª¼¼G¼æìÁƒ² óqžµî)2¥õlaÑ $GÐ]àÌh%äv¯ÄšÛ½Ú¾JQ3©íJÍßè4ìCrÞµ Òö¡®O±7H„gD ¸AÄ(¯¹©¤û2Ó"#HÃN‹»WN"â@—( +¯Ø%6B€õz¡Jù%¶;†%þÇ:ìÜšL8E^݈œt(ð#ÙÝ6/ñW«ÞNî§hh”fÆȧMǼ’Ý"󌺑[ÆQåHŽ¤áºkÃ<ÜÄ*ÁØ>#²mØÖ(šÎߜ筚ÙÀû£º¶ÉV‡ÆCõïÐTÀQÌM'żQ3V"d;ÔAßrçS¼ ?¢á­=$z# z,PBKEAúæڄИþyƒÔÂâ^ëh‘ÿT†Þêëã}nG*Ê{@­qñ +¹ÿEƒy;Rá¦l õ|”êÞˆUN¾9OßIÁûŒï½×Rsÿ&¢<ÜVÄ&Ÿ§òuD3ÁyoÆuâõTjúؾ89Bãò7ÝCÖ6¹†7­ê²µÞšc<Ûê3=~9šUF‰Hž€˜s0m <ëäˆcúÙÈÓÑŠËWЕB3CÍöËkM^5NŒö\ÙHÉÇ' +"*ò¨ÚÛö«ý•ð‚<·C~dçŸÓC‘kŠœÒÖ¯šÜˆpÌ€;gP¤g{îi4qO͸¿‹ÀÝõT÷@DòA‘¾EíÀ +-îA‹SŸ‘qRÎíÕÀK~ݹ#@ŽµÏˆ\)pY… á·ó›HUß„YA±”R6%]yF÷ó|aý>ò%¨5¢šž+Š +'Q°þbŸ;¬1“Ún³/S‰’í9ˆDvdàE—2>#°ì˜²àUZÿEéèWQë<¨²RÕêè2°L3Õ*iZ]ž…"cc +Ƽœ_[$™̵B„B”›²*_œ%†9€â¡bÝÔ0:}¤ò¹Ð¦´üUJ\:ê*F^1Û»b;*³€¤¨«JͶå_¤·cnKƒKƒù¨‰nU¬kj#!ó^lí´¦S‰T°QK Œ}K)uDÉÿ¶+|)’Jù«Š+çâò0T4@öµ¬aÂ-§SLbH=…ö-û/6„šG¯›ŽM¸ùäm§~ˆÓÃ¥íA}­©PC  W†H Weßå5Ì Pe_7üÖ½n½kaO½E< ¦`9ØÚ0ôÞ£Ð>äPO6"—¹žÈÚžå¼ÏA|*²kwZõÊÅøU§–áY@å–úƒF³-–veëB7ÚIä7X âš¡!=$hêI¾ðlŒÆ|êN°~õÌ2‹±¹ê¼TËsC¾E¶N;ÿVR§r=z¢¢§}û6çãÌÑC‹ûèi;ÕhþãWhéð~F)Ìɩȯ+5¼•¨~­'¶¹¶$¢´záfÆGÀ=  Ä*’íêŸÊÞ¹ÌI‡ªNÔÛ‘nVñÜ,&øŒÃ2Mè$R\Ïï"†û4FðZŽžGòE&y ¢thõ¦HOÙA¬¤›eº +(±É3«› OÊË[€×™#dðÞ¿ŸÅL~MqƒR8s4èÚ4‘Âdîïß â(jf%²J¬€Ÿ‘Ӄ׳)æüŽ”7ºK‚ŠŽöDt%ö%iÁð3×KÞhˆó¢u‡­³F4:ƒà?”ð\óÜÅn柼 á¤pŠ.à7` `o(÷PMSwv?ëënuD)½2•PÙ¯î±%¡ºj4FÙ½¨6Ù»=´²î©Å#¤ìÇ®aœo"" voÇš±&Åk_‰Z Q(¤—,g°3YÎX0´y×n=DXÖ‘«BbØxéP³ÍÏ'ÑÿjÄUÎó»óPJÞu¥j¡» a¨–|E힤+‡)¤¿ß‰QÀöéu’öŒÓ(Œ~¶]øåi —&u‚PCêmó·S°'¢@sïÄ„ dXU”Ø¡`ÿÖ ›±jql0›Âx³Â²Ÿ¡}™ß Ö:óΓ{YtÖ¢‡/nÓ ¶†œSQ%£é›ú×éEæ\„5±5¼+HSFZ šÂÓNŠ£—«…}RÙ2~·Ë9g´g`Ëàï½ÞªSW +w|y î CIQ9¯X~ pVU×PµuZïízwFlLÚÆVrð¼õ4wÉ’ì&Nh½ÿg }ÀÕsØHôÍtª¶ÐdÅ­’»±Û®êœ¾NÉ,AÍĉê9~¥Àö鯮ÐÇ4. :"q¶(2èV”ï±ñî’Ð~ ›‰WPã/É_Z?†¬_ÍÜ¡³"wï·IÅê~‰ûùy¿Sr Xšje⎊¨ôÑú»…w° „sD²ŽŽ4½#`?ž†Q¿Nƒä¥[>n`¹ø»)ß -·“"¸Ê§;÷(õVHsçìÛÓ~nOû6v©öŠ¸¤cŒŸî|}m%9ÿµ{¡Ì¯EûJvj`•:283ùïŒÔŠ +ÄÝ#. .*À”•~È¥¨ØNå¿9|²¡?àå÷5ËCêÓ¬6£Ä†ÇË­1ùɬÏþe:Fh£âƒÖâöˆ%ǵÒðW°Ò|T°+‚2T«ÞuÚz¦ê„fµÃZé‹8@ùÛìîÖŽ×Ô]cÑÆ@;êÉ\|¤­Ð«WósÄâ·Ñk¥ýG[@ã1@iì~>#p~µÛBMb_é#ªn€|WIþÔ4 I°S]›9&ZH¾8õA6uå«2ò (ŸùM1de…®H½}ž÷z&[šMž¦$éZçìˆÌ}t{ÅDZñSPK] ¹*epÜ =V•†-mfmfax¦YëY“sÚ¸%Œƒ +ÞTOÎŽ½Ç[΢_ÝŠúçZtèqî^W¸eìmñ4BÆÚs …Š·¾ºÝO%¿<ü“;ñ2DPÙ â¾r¥&»h­1–TC yrb¹»3©V÷À~®$ Oa*›¦Ä?åâ©+úLx¸Ëb†fßDÈ_Çm¶Ñ“³}FÝQ6_<÷€Mª:¨eo–ݾoºj9DÉõl#~DdÖja7àŸãþæ<ç¡ÜÇ­µ R:Wd÷;LðñM€­N^OÄ6ià›(Hztý(¡0!c-æ´±’n%8Õh0ÆϘA~C ÇÁ‹ œõç}~™ +­ïB7L³ ƒ5hàŒ˜–°©ñ1PÎ&°C÷“kÛgúlFÒR/¬UÔaØÔòtò:¢F#>*U'#Þ˜4'ÓŠCâœòúÇgØÅEÓoMÆ+¯ôV‹1têËno)<žÊîæÈî€g0/68)i§ÚຣšâÐö7W¥§öÚ{H  }§¯[Uˆ‚åu +4ÇÇýD8ÑU¤c†ñf†ûU…Ÿ¬7·¿NOqžÛ;ã;Pîê5¨FÄø7 ºFèoñ|·:œ‚ÎÒ‡hÖx`ª]c¢æΣl è®×£›,¥¾+MNJ{)Š-6‘£ÿ—s¡g'å¤0”èÞD+%;ôq‡0’5ºÐÿ°P¨¸¬'¸ak +JbºÚõ`™´FºüZ™KÔCµ Ìæ{x³ù­+¢½¸2Çv:…˜7òÅ5:!ýk=ëµÁÔZõN„ëöAß®Ì] WI¦fLsâÇÕ•¨œß5b¤UQãC©g¾Áe$ýTÔåúj„Êör¢;‡DšˆRÆCçfïÆU—ÅF‘ø冋Ó4ÈÁ>·“üjãNµ÷ ~ÂÕm“³E¶À»vÔµÍçç&û ßÙ“©•Ô NÝ TM‰Zì?2»G¬Më†v#-ãI­ ÓöpÊhO­$Š(ttJ?qèoÿ# ¬£ÛÀª®µm‹ñ’e×°'@+vè€í^¯Ël{Z)C¼¸‚xÀ 7f¤Y±F®6O~‡œ³Dtô˜¯1]èõgÞ'3z*¾¹¤fÔ0aEwq/=<={ûo"€B×ùz¿>ˆÂ -CjÆ.ÞÐà-PL¾4¨a!Yâ>`Ÿ +8~‹ð:ý’`x“ƒŽÖ¾9}ß 0“(§à¶FÊ°ÅÎ@!£rëFûŽStÛ‹VR7â$Úýž[9¡Q©‘¨Ê±jƒ§p|8­àüa~ٜǞBÛ–÷ˆôVÙéj5Qø<Ò0>ýõZŒ;œuØn7 O?#ÞñåÏðúu”úJZŪºc³¬#5æ3GDš²KgéÛß.K¿ÈæúŽWÀz•aü8 €ÿ.Cù«¶A4 +%7Û-´ƒµd²‡öuÞ£kùB­™OQº‹ÆVŒí9½k¡žÚ!)vÑÚgÄ3ÚN«)Ô\ÎòÍyFœ+`éh¨±àÙåXKÎ) x(y+ä³åýøáÕØâ©F1 ÉeÔRá3„¶ ó ¼ß¶Á'oTˆÙ”¯»–,8U|Ûšûà³0Q£’þ\ÈîgÑ:ëJ”’†Bõð<ˆ=°!Â;Ñd<ĉ0!ekZöOJâÔ¢é”+9xë­|§gFqOLñÒÇ8ôºÇ ¸G¬Ê-s6¥yÛ5œz ÈCÚÐïw}$´ßómo‹g+é»Ôwõ¦©^u¥¢X½!È’ÔψçÕLyµà:¾9#9ïB¦Á6êÍ7f_Uea£ñÔ9ߣŽmµ„Bz±|×öð²rVqDýÃË•>³ÇúCÊ\o¹eW´T 0‘xžG;jÆiŒõŽŽñSu6?·ƒúS>žÔ¢JRº·|jÝÂt?x5óÐàS£ÑdÕ~6 º†ŒeŸ#íZZ÷ëJm­<’‚@fNAÅÒø:Ž«( Æ ½ãV;‚BGå¡Q{HÉjÐ>…¿˜#ùðÃx¦ñLÖfv}üÐ §ñ›VM1i™ÀÚíºžGë²+Øù‘l£È>%¡4øÍy0ÐLêè|¡ôP&w7¸JÔ´Xkí•Â¾EõÈNŒ¶Ï¤Â”V¨m¹“q¾ã—€9ç—Lëµý±sÞ#BÅïMÖvbš(¡øGËóŒøÀcËù`‘²ß)ªÝdÆC©Öö:¯‹Ÿ¢üˆ”••· +¥.ŸæõõÂÓ¯7SzGíGáOçQ ·NÅ–ñR}#?· WØ•‡NÅnEaê:eÓGgËÐmÓ\,%rÃSÝ‹÷ h&ÖÊV;WúŒ*ñ²x2œÙ€Ñ %Y~¡!ÙèØl¥ˆãÂsà5lÏûà_|ÊÖ{‡µS Ì×r;=PÒ§# dÛ •BSªàǧº&3Ô~Ì$ÏCS]ÔWkÁWIYen‘ 5&*½í™Ê‹ÔšÑwĽÝ€ÏÁ†ó¹’Z)- +ö1«i<îµjwÎã“\µoãœMÖmÓòEFÏÚÀŒDhÔØ·,e®Äå™{ý÷iT¸ÃeMúµN‰¸[´ù±áï' H¬\± ;¶Mö™wö™Vü —;£P”´î”O§Ñ Å ?æ Ø`ë**|«‚çºàÑQºÝëJ%˜oS¼þºY”ô×ÔÔpêW¦¡Ns® ×&eu¯G‰UhpçH”rª+êÜ49&lŽÏ#øß8ܧâ÷^&® ë¥=BTÌ¢ÝIÜÓë(¢h[äa$fÙAãhXbjpŽ_CâªÞÁq÷5·‚"ÜŠ>ð8¹Ç¬Š åÚ²ŸL‹ôàšÿ”øêªÅL7X%[šðWQŒž×r ¥é?#®°:@Ü›ßù…y 4•5QSaÐɘT•êL—3;ûu³$]…ƒáþ /°rˆØFuᙾÌE«@¸'s!z9Zù¬Åd@ ok²  ,Æ™‘:*έ.RüÀZÓL ~³Ê7ÈO°[(%ŠAÆDŽmí*ébp\QŽV¨Šž‰éº)Aê¦Æ!©\Gêúu3ä‰B‚¨8Rúm¶ÁlnHiíÚ-Yš«|‚qÈ'¨<¬¿G›ermN{l¾2†+P•È€tÔ®º©,·¯¡xÆHësV¬pÃQtÔ(É› 2’>¿æ}%ˆæ¹¢ÎS¹†½5%P’²ÊF—67RF(ˆYºZ@18A€’ó+ÐŽ­ô'÷kyÅ—æ®:­Fˆ,Cg´õpï\ÞÒêÍʯFÍ # §R5FÜ¿7©<Ï0O\¿Z6þY/HÐ"kUÈcþâÀ «7s•æÌ>8îØó4LKžç-‚c¥! ,`!Ü~+ä4ò~ ù­…ù~Å•eùÕþüýܼ™õò]7é9M]4î½y¾ÈKˆðè“ùÛ5'­ðXè€ I¥|­9ûE~‹È3;Z»ÉuÑÃý<¨ÎÛ,ê+µ£’ßü.E‚3úGà3 +j*þM,JüÞIï£EWfË£"4í+ >ó43ƒÊðóë®_U€x“5ŽÛÆ™Xƒ8¨[@A茺#@Ã5Å8®„oÍvÊÃZ‚Í3_—1‹YA´Ÿòe\ÁV²ëƒø`OP‘Lîè_¹PìA5{‹ë3ü "ÆC %Ê‚u)9MVjà½Ü±øK K³Ï>yL^ˆ¾›Pk êzû‘Góž¶}œgŽ@„âáW¡•¶¾ +b2ó3À!Ð3Êëõ} bhº6 cìo;÷·åt DZ;† +‚âÀ½¥{·Ïˆßo\¼„¡2$J<6Tƒ(!ˆ3Â^A9si…À0ßÒf§À•,Ï;5EyÑ0¬» +®XÝÝï"”Ëm[.w;}Fáø36f +E®ÛAQ^wýîe*[æËùd‚Æ€rôç!VЯQªì#øqD[ mwx­ºNÖ@­+>JXŽ@ÓW„*DD+¶©Ýûº’ë$n’ʳAÇ"‰¬Î¯·çAm€ˆaNò»ä‡ìœ¼Ò­ÎÞp‹¥ñIÄ1¯ÞRíöÅ%1[X4Q(£Æ³23Ä¢ÝHm_lÔ)ïo"êBfY÷¦î_ô… ³"ƒN'—“­Î,”5I^™€µòÅZåØ4{øýN– oîBê7çYcèž 4ûQRµH€]"Pß#ï/ÂÛ±à-²ñí +‹ ã˜Ù¸߼Ⱦ#ö—?ó[Àk&¯ÞŽ¯ýqDadé0ó蜳Ϻ¡nâ•ãê2$Qøjû^÷ÜëÇ?œ(ÈÊDÉÚ' +í`¢¢,EÅY"¤àtJ ňîxçJæ47½ËSVýVŸGãj7÷jÇ[Ñw¹°4u =’ÿ ån¥8åG‚>oÖS˜ŽK*3HÓƒÖ-çèͽɕ´c2.%M‘™%“:gj‹‹{Ðôò›V"^,R¾Ôkߨ9[²ê#H)^@‚‚—ܘso §[úŠ$´ØŽsÃÖà¼zmŸÙklÉO&‡K¥¿÷óŒ0Œ~ÒÀ=&•l£H’oP+ãˆ#·t”_ê³OÆP +LÞÚ\ÂhËÜ„û±o”7Y_?·[ú©KÊ”¼„e¸žsR“Ny+sW£øt %æ ”ŒŽ™tĨ‘!ÃÖ·PµR0šéµèîwlÑïû# x§¹ñd¼ôÇõyÍ­KÌ­«f2%×%c õpë½ö´ãœ®ê=ªÆ9ñÞ•6VÂùA Œ%ï‰ì’"_Ó—é9Àr…=\"Ê I×”ià×^dó´ˆ6wÍý°¸ÖéX—²ÅÊ¥v1)% …Ðÿa…ÙÅféz]G¯‚†_³‡£ £´T«T†…ņX2ñ™·d0Ÿ @kžÌÝrKÉý]?µþƒnß$ZõO!CÈÎhO™Xä’xe¬íÆ…ßD@m/¬é¯2¾fÌ…E®k;« ;\MôÈÅÀƈƒä¼Fä3ya»só¾9ÏzÍÀ¯=>N{~?!T:ZÜ)ël^ç4û@Ëû‚´kãÛ UŒû3ê/ö÷I3qå’NOãwÝ€#ŽÊ´õc«9qÐøQµûdi¿¿‹haõÔË~G„ÑÞ]‰¾¬äŽ¤Æõ)\Ë‚ ¡ J÷…œÁ·Ò¶¤4…ºF$zé|ã–Í­Ò§-´ã´A{­¯|ñ¾(§Pý…–¤\J¹™ºÙŸm€3Üwpëöô;U“ì4·(Äß:¨òéZžõŽØsC#‡¢ÞB¡!̘,ÈMX¹Bpg¼?Ö„p*u¨Ž“ »ˆÉcaSÁ.!7è&k•ˆº‡"ß°á§ò[*©¬%Kê_îv\H’,z£hÝbN‹B5Xª™lBþÍ`>ñíúü{º43UŒƒÝãwQÈ Ø¬½ñ~k‹;Ò.þ}D€n¡BáQ€øꑘS\KIºËØ¢È7“/xà%"ôzëön½.÷m­dÖž§Œ lÖ÷z€ùÔ„í9¾ ½¶ˆ¦Dwrâ¼zÿøm”߃”áÿ º»ôó_òÌÛöí „D£óÛo\æNÄxÿnG þ!*vgÃ!¯qOæ.`мRqúúpA¥x›E3ñ„šsXÿ¤¦÷tc˜WÌGi"·x™mÿ…þ*98kšG}Ò†Ž[c‰Ð|ú&•SŠGÝ\)X°Ýˆ=,iÔÖ§u"[ÉÐAo_ŸC°ÇøÒErwÊÝgwª`ùŠ`ÛßC=ÿmMêþõøºŠ™Du‘ÎD…38‹–D}è K„â;|•3N‹ƒŽðú;J4ùŠþ˜Ý»+JîÃÖ{†!…× ÙÈ:I[o¸¡©f¡d’’—HQõäöGÙ hsQèÛ Ä .( ^ÜŠHé*U¬é9~U» :ÜÌ°(È ÅÓºoqW<öA[#_;9+»£8ÁÛÐO\?í.1f²æ–`›ªOþ]œ˜ +·S4el«Å@àíÇmò FæsÚM|Z->5cj¦½~Ýà¼Á“U WÅ.^'oåÚJеö·ÃôÝH#llçð0Ãåï€;u%fÒ8h/é©U‚D] Y.#¨Óoët¡Vqî8ô³ä:(íÙ÷y^e rTÜb”LAóù–+±_"¢ØvHëÙl•9‡`¾+´˜~]¯ Ý0X yc2¥n ³¥¡ƒÒ:§å!Ô àn’p˜\?¦$ ·Œ<Àøu» +-´P¤è¯'MS$ã”—y¥~¢`} tøÈ䤹¡ÁÐuíG5t] Ê^¢ÛöéY3ЩF#Tv¥­',;®ÄCãJ$*¹QÃÕ¨â¨;Ù¢ž>‚²Êzö/t¹Øã)0]é+<]¡×%Ôi*É•ó€û`6?gÈHí*ò"–ÝìÕnÙÓíyjwn6Õ^n?hаâbÝáÀ¨iQ‘‘7“·ÿŸ¬Ñö®ŸCE¤Tx] rs@ŒFøÅuÎå§]tåÎ\ÉߺÞýñÐ'ðÆ5Ys¨$e¿ó8~/gÎ@ɥѽÖëT͸óDðh¡ïÓÍ[Ð%ØèébzqËáúêÐ5ä¥Àù™u”2n¬~í‰7µ‚uÍ׺Ÿé/ãMázCáb¹ÃߪØV´\q¾VºVW E1ü Õk«1…:纠¶ÍGæÕ'+*/óžž¼G—zíò]Ô¹”š;¹ç½â{UØÝ_ÙŠüËYÚ“ð}«ä„TçZwÿ_•œ6],ªÞõ ¯¹óÔ?e\;3EO_@íïÒŠ„þs³‰ÖSYSņB0´pH[ã&ª1Þ/Zß NêÿTö9Hñ %ó³Iå©{(ß‘ÓPT&Åç÷@ ¨ÇÇFLCè@ÞjÖs¥,@À„ï6£u乯¤ãÚ)ó] ÀÁ{í’Fs¥ëáÏY>Ásq7Ïd›ý\S‡ƒd®‡$  +DµFSû_6é‡ÂËŠ_/U´wå®ô•Œçž:zKs|Ý[Ûé/ùá–häsrY"MçWQêF|ÆçVX‹âÅ–íl•Ü|nâ?hAõöÏ,¼ü$e7ôŸ"T”³f5Ç’I¼õp¼°dº"Â…©1Ê•àÂäkh£Ë.Utº—·_T_C"‚YY¶Ô÷¶û¥|PóêxªŒ¹²ìùàä:" ªˆ ÙrÊ,‘SnJ´T* +£‡Þ«ˆP?0;m?~ûýøùOøN + ºÃìЋ{oˆ J_S3ú»uÊ!§.ëo]Gß]…؃TŠ62g ž9à1•¼Ë$þ­ÜìÎuÓs,ÜÂßï+¥fTx´Á(ÐÈ%ŠÜ çaÇÐÝ!ª´ªÛl•ên]û2BÖ1¼Óh-LD \Æ£61OuâÀÐ6-df^ÍY'Øxë‡ +å©@ûBÖ?7Yÿ‘ð©5,~º‰ZÔ@ú8Õd kÑ^ÃðJVtçMÛã»b¾z$_½ŽGÀÈ÷9¬uú-ˆ¢–ñ[´ ËÒ.±Ÿ0B—µ"+ {É Â¢lC\|8îúu!¶l]Àf¾ŽbžXÃô‘ Õãñ‘*ñ‘ª%¿Äd¢£tÈ6[”¥Œõ$®òE†^ +Ã=Ô—s£ÏÖ0VVë[ªõd¡ÀàhY¢eÇ€mÉJÝ/ŽPýqGç‘’f_ÓÝð´Ü¾&¶ãÜà<ëËsö¼ìl @fxß­1a¡zÚ»QÔr{Vj»R£a…rÛOæ|û*QÂ;ä<ªd”õ”É$(>"”̃5 œfuTæý\‰Ü@Ä}ƘæÅ‘¬]CÜÞÆî³ÎmŸUfì³f Uç³2ó{ï[™Ç£Ï…À%øiX6I@¨…•Û[múï{ïòØ–ä'¥î!™™ºz__}ÏûV³E>QJ€T^šæÇ…uPjì{wmMcev 9Ž×5Àäi ù@N' ¿ª¬ö%aæ46Â"@ˆ€±Ü:í¯/Lbn$Wíûg‘Pâlϵ~¼j LTO×ÔI]Hÿû\(­ÍÞ£G3‘oD3ñdeìWÀ_‡)3ͦ÷W½Gy›QÇî´9ÖMªÃzouMz;<¤WÄ'}ã¼²ýÖïA} °•¶‘ªÐ >Ê”VéLÄuðÆt)˜Óו¬o V¤·˜ý=7¦=¤ó.l.Š¦ B®:EvÄ’æ£IÄÌŠý í8$úgºÌ=u¡,dÃ%/µÕoÙ–=f[ãˆYö¶]ÇŽÑQ¾&iž€­%×ßõ5‡D ôÐCÿðůÇÄúÔª7ÖÒ`¬I~ØÛXêf,¦–0s´H[­¾Na`JÈ Ya¢ +lGŒ5e‡`?h5‚Ùcö¹‡ªë•`!Ê/¼¢˜üŒ¸m5Ö2=ϱ¥ÈOX[+Ý>pÁvnŒKeˆ~<³JizË¢O®J™ÞMR:JÐÄéÔ @Q¯ØS‹g'­8nsêÀmnuሹG3¢q S;aóa†ýB¸Û ƒ¨H±Üª•_ö¯Ñwu°…OŠ‚<”âu>À"¯®²9©,à …ãV%$%'ØÀQVEMÄIŸm›°e'd„úñAÈþºŽýýyàÜQCÕûãÚ§ +ÓËvD:6S„¯kÏHQwõ¬H% n ËòºÐC¬?`%DE*ëE¹¬Úhvâ§tE+NÑiD sÂç9x_Ö~ž< År!Ïhà°‚ÖÔim`“J’òIjÌEj +¹Óa@TÏËùìl„Héux;"Hzu㉓«zkçÜ¥ãÞ›Bºn’#VáGÔ|€¾£ ´>¹|}K«©lÜBaùO_4‚!mÓ9PùîYÏQ:rÛÀjpµ»žDŸbøÁ·Öþ¾åjíÛ×;‚7/t‘دÌV‰(è”?Ù­áªHØZQÆëvÐÄ¿Â+W’T¬oŠy„jì[ľþr ±r–G P²I°º HÒ °q¤Gº¡Mv(c;ÔU}áð p `Ò)|]ØŽ<à6 nYD!õØÌo¿PGVFj-‡SÏÁËF)À#ÉÒ,>*+ŠÈRf£’;¢f(JHx']iä®×ù³VR¸¿²¹-Š$£ز¶ÃëÁ0nˆo£œR^™™(êdJéã~ÈG×ý€Š!È„˜*~q?n·.+9RÅGr½ªLˆzìSu:KŠR›ÄXÞs¡X|µEͲüè39÷…òiæ8+ABùûÜ/2–8en›'\ƒzf®ô#eæ]QºãV†óçŒÉ×%·m]vû;:'õ{«†1±Í0—Ü8‘ÐÁÓÍ#yÖs9¯ G¢uàN'£­©(>«Ùó“vÏ j(è*®GÂ*̹tÔîvû–¬¢±ª­Å¼1'Ê5˶ÊÌØ7¡!Ëq¥À+žÔضeÚÚÝ‘%`ºüBªÌ-|Rm¤<¸Æ±S̸77/£ù¸ ö²~¸’fl.¹cê{¬;v†ñ¿Í ù[oÏ ¾²¥\¯}‰~×½õÀ°êœ[¸Ši×!bD­=[_Xðjùõ­]°æ‰Q¿ö½Në–YTnש ç ®R#‰ÏŸ¹|Sêr·¢ÅšKq¶öËšÌõ`Ÿ}±‘c%­´¼k8sF™CÏØu~®ZJ¤¬"¥¾¯¦¤ó‘ñ.[*̤·ûrGJPõÓê{Pý¯I:kL‹¨û¾ ™¥v.¹Çvñ ›Ú.f.Q}PÅ0<¶rž%ÛÎ|~ŠXÉeD4Û(–ÕDD_bM?c\¯+ÀE½³ÚÈ7wž û,p"k:‚ìmÿ´ºZ sõÒeu ¿¹íbÔï%Ÿ}m×W â¡hvƒº‹þXH#Ðv?‚ägÅ€¡}o¬<“G¹Â–èûœ0vO…™Ý‹³Æz4q×Ê’ÅìPôXûiÆ©µf·ÂJ'Ê–ÀŒ˜ ÿ5âÚKDòØUu£™-PQÅ™p½ ‡ŒM:räMåÎyÄ`jà'!W¦ë’u&¹4µøä<·jìÅ”e}Ë jlбXÚ‰}îðU¢9·"â’†ø¬Š›cç\Lá¨Ç%Oˆg¾.d3šL¶˜ØÝD:<‹J×÷§;׉¬ÓPP{G—®ýŠÌ<­rðà ®]H‡®kÓ”á-Cº´«Æû}<}RˆØÐ8=15Ü)u@;H©³É&…I™Œ½ŠHžWï[_f0 +ýl¹P Ja³‰‰Êšq§|ùXóP.aFA ˜ô®̾ÀšfDc£€Õ¶Ddo”_‚çà-nÿJÆœT«ŒãÊ6M(å5i¤ +›]¾‰ 2ÿLì7]ìZœN}[²m¼7•PM¼uÒ&LJ]wJÝ®M(½B(m­s}ŸÌ-ÔØàÓó„› ;ïÜÏ Õ¤5p[ µ!¡ã|fヾ¹å ëëË­}±-çÎ5]¯ š¯.S%´-N¼øî¨dÈ«šõB˜Ñ³ŽÍþ±m¤$)·wÕuÎ+µ9…wðK>SDÉ»=‘/F…pÁ‹ÈÖîÞ„6–Ήٮcz¬›DìçuÆìBшµÓ«%Ÿ +„Ke“lQEp¯-êŒq  a’âη&mÒø¹€=€î2óÚ 7)k½z@Èj{#2zË#à;?1Þì±/wá$AÇü°¥ð‘°@ƒ3Fo¤>%¬ØLM5Oþ.ÚÁSÄ^Ùâéº1u²¿šr1ü¼vŠ¢ÆÔ'k»1¢7.Y®àz (ø(A1W¼\Â4_HŒEÊû3ŽwÐú»7ÅÄÊz±)]‡ kE¥Rs~¥ÞËØ;Oo{§Ëxé66]&:ݾãQµÂzXø' ‡¯Ç4_¶!}³ô,ê}ÎÏ>æÚ †{ÛÛƒ.h,ª³^SÎÔ+£k±[b„¦)ÉŸ×Y×qÚ4 ù`Oiìj]Á2»fÀÉ·´M®4ÖVÓebã;× ìGúÄb,Ôw ­X%ä5½¿2N¢Ô•¡¬¦jåÜ“ë·¯‡wzžˆì uìâÉf£øRm¨¾è*$€²!/Š½Çèë77/«Bhh8\Dh‚™µ®Á+˜$¡Š68¹Ð³@Û „ֆ혡;«öXãûÍúß(É𢤉ܧ^ág9Õ:ŽèÎÞ=œ–)Ó¼#¾#ˆÇ"ñr¨P¸åÅŠ}·xã;˜¼ÊFš îb¦¨©+êWTp©_¡&êÓõL·ÔÝu<}M%½RxŸCWC—ã†šÕ +:…¤hŒ¤)ÊZµ6Åû´.³Þ¹ã¹UHbÓša¾FènbDH°G/™:l†TOO@Vî#. C‘ ~Ûwš³ ¯hÎÎGˆ“]‘CðÒQê‡/&œ;í¶ +°(6~«ýY˜ÁÝ›iK4t'öù“êLy WnÒì8Æ»äAµ“„éÆh^•*}ž]§ˆ«ŸU‚ƒQF;úž>Ù¦´Y”….¡a4µf˜ù²ßöïÏ.b‚çs•ûu™,$lW®¶ÉÝç&w·`ä~†:Z·@ò¯( $å{_íU"Ø~ï õìT†òL?R-šÿ¶3½§®úLÞ¤V2¼\LòM¨¬ž¡,—š÷…¶”nmÑIv¤;}g†B#d=áìç<ÃÅ"îeB0w•c6ÃÕCS¯=ÂÎ  ¸.2Sv¼ÓÊò°¬~(pn®øv£jatHbÁ>êÒY54Ö?“ìÕbÉIaã²ßUÀw²j ;Él²,X]z¨IÂÍ)ªdL¨’1Az=™T6·Ð8R´Ðî õY±ç•`¿ÈeIÜáúmÝ ·¨$FóL±Uíš~¨Søµ¦3o`K36‰.Òøå +ü|­ñòVv!‰ºo¨ãÓÃEÏ–‡TpãD6[ì¿Yæ¿–ÚKÁÙö6Ä 4¬nk†s£’Q¡:ZÎëtÅð|²Ö?§ Íbýü8g´¤é ìê±ÌÊš"Ä• O…%ÔNJö72·XÐ&Üçïúô²8ÌúÏÝ̽ÁùÇ¡+Ú §lôµ“sWÝov ™‚÷Æ> Áx¶™+}:~Žtö“lÂï¥&K™ˆš,ó¬²ÝxŽôú3#& -6îMš Åu§¸è¾¸­ÍOÛo¹+Hô¶é‚Êf«¿väD Hq5ˆ.PGa´™‹½ô+È{ wó%866ýÑ'³Mt¥M$€ç¼S?@1œ9[Iºí –QƒC8€ŽÀØäbóóËflÃh˜CTõyH±UsFHÊOœ²@ÖTqj[Ér/Ó`'æOa‹Kv7—BŒBCû{£¿Ø"|0›žöÓÔ½yp‰^zä&y¥Ðh*£iQ#T.¯ñhºô&@Ë‚Ëõ/¨ªS[ea›È°3Qcã56€5|û%f鈂Y+›bÍ•îÐ †$lºx‡MÚzȼÁšÓ¦ò4ÜLhTMÚʈ”'SØZͨ«:æõ EƒÝ‹¦àPMt›põd¹ lý|t\7ç {KvnùÓ@æ˵å‚5¼|ØBèK5U|7Zæø&òòº’¨Ïu¥xqÞ©.®­Ü¤˜ØÆ6¼: Iœ!UãD êì4:Á¬ÿ¼Ç[h+%[¸˜aÝñSÎ-AG.᱕4±P ߜʤâS⃊Ïx~Xò6„U¸šsÔ4%L¶z…¾„—¿5îXãÁ@é2G˜¥c_q·5¾ç¡ ó(¼Mg ´=À/Y ·ÏÖ•üÀY«Ç{+Æ”§%eûyûðÏ ÂZ ؉WÒ¾’ +(c·$ø_¡6ÙĆIÝ{s–ðãyc€ëIyêËݦxíòrGñ9¿ÅSµì·Ë™~”EHGÄìÝBÂâ:,zìÉfEàõmÄþ:d%"lÝ•®= +þdC´Miœ‰Æ#*júaá ºÕ÷÷ä]!C ؈+ù¬E|ûKSwd^ðLÔým½È"zôº€ª— ỤFJ—½ù¦L îÜCb~Wkì' +ŠÄðè Ö Y@jPYجòMôuoñ(Îíã0õϧ& «Ì2h?¬?ªó[èòòSh¿'‹ ’|³¡-(??ïEP»\¢³3ŠÁLÔzzñÿ…ÛfAŽ–Üå¡·4¦¾(¼¢.ƒÛꮶ_qqçí  à ¤»Àúx¯Í;:Üì+ +¾´5Fvë J¦@Z¦Vˆh¹ûG?6νRúrþ§ +€SI!z†Tý¨d_c}Œpx!W8Ûó¦Þ¥K½¼x‘Œ, +6^hÒ퉯lsÀõ¥Ž‘`­ŠB®I»ý™].Y"WÈIRýü-«’-ÛŠEå{Í%¸è©rÑÛ™3„•Pðf™Ì¨4Å‚¡f)±‡Ð +Ø÷Ò Ø& À~Ñ}ÏÀ‰pGjÑ°{klQÏh[õO›¢ŠùåqnX ¾ÿrïa¥V´îm|Ý›¡^ŒéD¬Idbø÷2[üÔÖ÷|dܼ½ƒ™±©A—¦E©ª§c˜4PoéQÚ³©]d…‘´Âaäw­â¢Ç qªúÜB¨ëû´ ߥ7C#V¨T™Øn«JÖ÷jo±þK¼D¶±¡’åÞ Û~ÍöÇ£Á¾E\לl¹h]ã°wéÎßYþ b⥃pR.ºõ0ÏLf¦"HW‰‡x7”ÛµJaV;_çG ƒ(÷ÇÜßrIæ ¼{§YÃoì¤ÐóUDEÎ-­1q:|V[šy0K×–´lܬ“Yg§LštG[a]ש’.µ‹ FJk5ìbñ4¶„fz}F˜&”dãt܃Ux?NÌ6[àªÌ‘d<À- œ|À¬9‚ËêÆ*}QÍ @Æ¿R¯Ž€QóKW{Ø°§}×.gK4Xúë@¾=˜Ô}H˜žOipŒ Éçà…Ã)[ 1I>iWèŽ[®Pž]Ûó¢ ÃQ+ÐÛ)Uͧ¥ï³Yj#"ЫÁCœF°3ÃèLDàÂÒÀ×÷§†Çk.KÏÞ& ˆvøú·­qØÿ%¢ÊH²_Œ¸üikç¬Ç$‚ÈÀ…ž]g€R½‘¿Ï¨rRâ<©3âÞ\͹·­s€s9GR‡Þh·o¢êV}g2X;”.8Vä À4ˆÉM«„ƒ1pÛHÔN0s"ÚŽØÜ–^õú8ÖaZQ ]_a@u‡c Y@òäψo‡êGTñªÓÍÚÃ@ü‡2Ú«"‘7º€?¨v‚/!D§Il;<ðó~ô¸ÝV“vfî\Ó{áEkîñ”š[åß’Öz½¯sCŒ‚ôOAû[Õj‹Bœ”Ö8ixQòV—üTêž$¾Xß>n4Çó&éoT<ïY©VA¸>‹X—§‡DÄF=Î e¾X›^"D¶p Êè$çmõiŸ ¡[ƒ¯Ö72þŽé+Ãxäežºò°¨kA 2FS2¹3‘Kô‘TH÷ÝZ™š$z¦ä­…:Ø6$d¢•¸ÔGXEvÙûtZ7h„­¨j¯îÊTXeâ9)4û#•ð/f`ÿb¦¹y/ÿ:¶¼ˆ«ìtJ¬lƒ­¢WÃ/¡JiÏ|DÙ]dorÞ“2a´~íkVH}ÉÇe- vÌ×ì('`š’<š·æá €W˜)¹ÄŒEÈú½£žs ¬&epíZÌØ‘€VβoÁåµú–&B‡…÷°^=pš%IÛ¨#WË Í´í˜,°•š–v.€¦ÎDh6T±§zMwT×d&a(BúByèä8‘0^nGéñú Œ ˜•×¸€“›ÜA0ZE§m ß2è—žêùÚ½YŒ»ög˜»\Ñ×|£a'fðÚƉNxj×|+UïÎXŸn3$šO/Ö„›m²G5jM÷3ÞLs½„æ®óLÍêã[v)+õ³Y>CÎ3‡ZS§.ö·¤NEÿXÙv¢æVŦ +ˆDwÝh¬æ˜Fˆ¥S¬ôqÙººÁŽÜÈòhão›Ð5&42Ò÷Ë6ÝŠéôu¸Tè7ý?~û…ùùOø’J)Qµ‹8N$!߲ׄ'ƒxKì\ß±m´)1A=JÕT¹.™)ð<6ê¨u$b¤­*é,|Gt\Jð…ê)æõa*—t‰^:=±¸Ÿ6öZé’º",€ýÙº?zå8¯‡H@Ý:p¯Æ)¬v½¹yL ©|é3Yî¡þRw'È +/M©ÚCV]Ó…µˆÔ–êC¼×ÇiRŽ%Æ–oÀA•.!EÊe¬Mý™ .Æ24Ã$á¥ZYS=8è0ÔèÅ]ë¢`c2ç‰cËÐáØ°Ì•"ÅÃKO]—Þ8"Ê«Œ[wùàöâ¨/Ó|l|¸ÃZ‰ +Þ§+»Cg6f˜rÚ¹Z6íâvù_ÆV#³ý¸ƒ´²BB‡nM5G}­±ð?âíÖ~“’ µZ]ºLe£iZ`0©^kª¥VûyeWã€cQ³û„F:R6‡òïaìJ±*¬Wß +oápËÓÊùˆÚ=Êl‚Ç0íS5NSSê28ž¢œbïtZÀ©ÔψßïNˆ²3kÖì +2}žgc/xèW¾ ébPÚXÎŒ£ÌúzØÚøˆ‚ºmsá:ð½öÖkw¦Ì c¨ÖLÌÓ”Háµ7Ó Äi§…mãs¡AC˜ðç¨ël¢^:«¢颭§ÎÃÐéTdß:#rW'ð™3ª‰*íq#ÛW‰> +Z“âqë{$jûJwà0¾ˆ–©üðs£ß¨£ƒ…a¼(áâÏeÖ“ÝaZ|Â-Kwk· ×­ÛUÖˆÜCäÓzø#bSM«ëÐ3¼GAœøÓ¨äÞ ãšœmÀ…-/튵|þ=y›[îÏY¿ù4`KWÂe/;…v½>´¥€ ÷¥î÷Åž:P&mG ñQÖôLÉ‘ÇŒš«´òhÔG‚pžãåø[C7C,ÐFŒ<ë­ä Ê‚) +,†ÚÜ£á•ÔÌX~¢^-u¾Üì"ö•Øž! _Ø\nÀoPU(í Áâ’vn­ç›ÿ\¾³×úN#àz+«k_”VJõ¬D{êQÓƦuz¥ê§àçmÉŽ‚ŽpímÚ%†ÕY¿ÓfYþ8AY–Z%XsšíÒ øŠ ´˜aSRÓקÕUm ð•ÓËÝð‹çôãŠ{áVÓª½ã*3ì!ÍÝ([{I—?P4~X{ë|ë@‘‘zèÏ·F­1âa]Pd5N Á&¯5¦ƒÐiµšyEþF2ô§Ü±ÃQ¾ge#ÕÆ`;Å+§ÅM¼§¤{Y,–©ÛxÄYp¾¶Um‹U­³ Ò+ö›aÁÌxûªÜÚ¤£}£D £«íÔ àË<–ýptE›+ìí"lUzEœ‘¹’ +s%ñ;u;Ñ¡ ÎÃ-D<®=:þÚWÑ׃L*ȸ÷(wªZûþëý˜®_1D®öÔg5뀊ê9o ø¦3åŸÛHâJ+ªöÜÏ$ +–!tq¹í¹¶B{G”6ÞÔª?ÂÓT‚.W4ìå†÷ÅøÝöÚ¶¥ME|¿¸HR³…šž›+ª[B´ønÃœ¨ÎGœÕÎœ»õ”P“1JÙyö„Wý¶céÝ©AMï¹g‘Uñ#ݽ²IÏWºÓv+ŠÖfíëJÐ9‰ªûûdÚ¢Ç}oò4–舢•º/ñnw{”Ê”.âïéÞ¯´w<ç¯?o:’¢mq| [ð¸$X¼f:›®g¼²ŸˆW%’ôÐQš;jÖÍ¢ºxÈ[†#Eý¥³7•¼ €ÙF®Ùƒ¼p}èü{º‡}ëàà©y}}F'aŒÄïBlc·Y·îÚyÆCKƒœº rîùe¨›Ùí™ +¨Ùx'„Êd£¥Ð¬ž?Òil$[˜â’X„_¾øõ?l?NƒU%=•N³˜©«XË"÷…ÙŽ8wÄý8ž=sÅÛA¡ÏÒ¨Ñ_±%Ô`±E.;XŒŽµ§EQeqe§íž'âØO¹F¹›¡aǨ‚ÕA“^‰:Ç%/~8ÁX |”‘.TU?s¼J¿ +ÒñcÆñãÜBçŒ&¯ÑÕDE/$Τô™6Û=»ˆgdM sÖí›ßEѹ–‰ÓH5ß;³;Ãz¦‹äý˜.båÛ}uÂZwäñGx'ÃÿΆBgŠ +lŽ2Î8ö{{„ÿ\ÉTü3õt>ÚÌnpªÿ«¿n›¶îÝŒŠ9 Ÿ + •°WÄÚ®ÌD°%#Bh"WÇò9´²õJÁŸˆ¹~uò5vÙ<[µ:y_tÂñ8Eñ§ÙQû&"÷®}„Y®îmçyD;aÐÖ>vÆ¥ÓÄZºZt;@Y/ÕЗªÄÏ£ à=¾œ[Ü3µí;IªIâÖÑ5jû@—¿6ØC¸Ô‘º ºŸxyÆãcÄããx-4Güè:Ò¿gHx‚‚ŠB„98Ô6úFCf+C.z瘖wa¸¹R ÂNæÄGb±GÓ©|ŸEa5Í)æaÍ åx‡H£×¸”ÏÂèPIõ¾ÄtQwÄU_‰Š-`I€ª>¢TkHzÉ:ÕæVø«¬‰°šj|D”ÀPÑxp÷ŸQ=îZS7éä¢÷gŠDØÃ"ÏË/Aœ_r»d¢ r*µº²³—®æȼÎA½ÞªíÈÈcD`üÖÑíZ±!©±4Xjˆxä•-•0±£@yÉlKì…—^áñ䯪r%ëºè/¦Þ¾ë>”kGXY‰Ayò ù˜ì8 ”QÎèt^ÿ­ðF)Ù!P–ÑX% Ï4*ËõMÄÁ" 5ŸÝÏG”R #R e—á+Ìèêçw<µ‹­üÎvÇáË}dw­–8AVax‘Ï(©Ô‰BŒ…Ç‘MwnVÒøºELoö%•ëç= ëXøh\hpgA2¹Û›(É4_Ï€Ó︌³Ýù øýÓÅ;-íƒ%œßœæ¾>.TŽ‘í,5&¶³Ì’Ÿß®‘ïQ,(±G £â°ûJ¥lÔ *?uÛ‰ÐmRÛâ5Æ@Ñü¼›i1 ^ÎRHç–:|¥†ó°çƒ=ßõ^?GÖJ½Æðùò°TsŒ£lS”®[û…¯=µ7Þ}¸«ÐÍ(ÍÀ&ëzä/©Cÿ´+IÎÞogÊ*h;ÜÁÌ»÷AÆ÷c®o@£4ÆÙ I&îMÿŽÙ'%£ÚŽÚ?/ æÕ´2<žª~­÷Ž˜;B“c š>;‚\®±?}ªodá9ÔxJtœ 7ËÒй<|9­¤Ý’ÔüÞŽõâf‰ÐG“ñeÕ$h³ 2³4ïÇe±x#NF°† +ë”Yƒ‡} ²˜!âfÈ(nÖ×sþGé›.å³)ÒÝo@¢ÊÛcN‹ þ!ËlW­×Ø©ªåŸ»¿]úì[éÍ*۸陋áü‹œ‰?ÍŠz„BAÅzhE°ÙÙç ›ý4¹¡ ¥®+,ùœ×™ù@EìŒ âµ)½E¾‰PωĀV­çg0W”ª+=”Õê©äž5ÆœÇÕÏ6bYõÎ@þóÖÒ† O[zÉŽ-(Ë$ÃÛFó +Ñ:ÔuŽ'j$øqÊH]ö +¦Ìç–û´>n9éþ.b}zÇ(­ÎW«æˆƒQJÐí4B©e“Þ¢µÞh{D™+¸GÖí¿©*F¸S()ÀBäwª„ï|"¹+ ÕFÅ#£3Ó¶ã5SÁѤWªÑÜ.›[¢DVF™yÀ¦ë{KG+é$–3zÜF¡FLÖ 6¼•x6¯@FX”%‚U<Ó/{²¶å "#­ãíË[õŸï€¸ 4s¾#ã‚Î÷•³oMoÔ¡«ºrªwoDtk«ÏGþ¾††ýñÑ"µFçé«*QÛ_¼õ’¬dßf<ÍQ„1ž‘}jò÷â^­(U ƒ™eΣ;Ð*Šï`—3/‡$ð#%WØÃL(¿8'^–ÃŽS›n¾ +0’¡ìÐs!+)ö¨J®4¥©Þîxs%ŠÜ\‰M¬ +lˆX¡À6öw±Â…ûÌYs¥àl¬*¯§#+fÝâSaf<¦À5õvÍ3ÜÛ²æ¹cÛ´Q +Ðùˆ°˜¯nÝTnc\½Ú·çÍj½ÞÄ~ÜꬔóZøéóÞ%Jò6…­ÇówÏ +úQa“'õ|}%"iz\›å ì 57Õíß#°P)Öá¸3á=jœ©Ga3MúÍ8…ZŒ¦c,[Ñ/_Ë´‚vÖ%SoxŒäõ8eÈðíÕ¸ùø¸f›Ñ›3urd´ò‹WªRýâù‹!4àQZl½Ì., æfn² ¦ä®­ÖFšr'A +¥¬yüâ6çOꆓ¥jˆ\Ž§Ûî—´î+Å'ýBª|ÍýÑa=©‹cð­ÓÍcqô5¢Ùmvnä&¥U[ñDTígT¡YP¤±œ;ÆëëºÊTæAÿç&1+sm:•ÂA“ªk0Dž‚âŒp‹÷¹»ïìîc9ú|Ý®¡¯2ˆ?¦)äÛçÑ¿‰ð9Ž<ÇQm˜÷(Q÷L³Ñ{Ñ-Шf{r -—ÇKãšìL öB^Ö¼xæ}ò'‘IƒªêwaÐJ%í ì¦î)j¦¥¨gôžÆe­µDÔÜ–v´(*‚ªbI_(i? ß㊩e빜(¶Dͨh‚F±Ò-٢뒉že½XþÜ|ð +v[¯#,ŽãQ[¿è¡í•XY¢ª>§¢®ðJ·¬f®yºxÔvãŠô¼(zCJŠ+8Ú-¥¬þuþk×oé!d‹Ñòñˆ­!`!4¯ |¦Šk_$$<¹ì ­f\Ð^ë¯Ãö"zÃÕu(ÃÀ q^©dw3– ÿ>h!~qt}~óF¿”ÏDénh17¶ÅJ°å¢y ‚ú¿4ëi‘˜RúgDV‘s«5E¢à›ó æCÝ5ª¥ÈãÙ—ü8@Q’Œd ÷5nýƒ·¨Õlý‡¼hˆ¼—8ÿAgvnK•sÖŠÉá=â5…Hܽðp;¿9Vú¦’MK¨s‚ +ÑÚ}v-r·Gõn¯g¶/€"ÔÌAª¼²ð7RMÌâYž·DWb%7ÔYr^³”©Òˆ IcÒŸòššzº0e÷ʈÒyÓ"ï•ól·²P FÈÛ[ËÂQò]ô>("|Wµó1¬ö?¦`÷¾í´µxC¨ñù‚2]f¾Y„ÖÏJêþ~aæù¬#i²QwÙMó3MsƬmЩ˜¡ùo‹IDGœéÚ äÐù®ÏˆŒ©-ãˆYH™ý›óÀK‰ãŸ“ym[y¿+`7e>êæ¢vPsóHqå)ql<­:¡w:Æ.µMÝÀÜ¡t¤šÐÚêH7v£–ö’Y + T\;êÇuJÇ60a¾Žh{+C£ëÞ}ß3&T aÂá?ÀKP*˜Ûb +d"^ƒu-ñ^—öÍyæAòemã{‰°Åüú3 wfóØN«ïJ%œµž‹wš¼ŽÞ@§{YÆgöS¾ûMià5£_E](Xà)±²îsÆð'{R¼˜]U'HÓ\,r™ñË)ç×®(äsø—! ÛôT^XªðsŃ®ÁYw„ÂUà³Ïϵ5™é{Ôšª4Yèl1Ô‚,1ΫÛ=ÔŽ‰PWß³ZNz?Èïëö(ñ(˜Nè½Z?§h³SK°eôÒ”PÕQÛ¦PN\}E©qD™¥ ma˜UUæWïVÆÅ àaî× "Í8°ÁÙ]¿é:…8áEÓ”„]ÓÝêý¼ÆÍYø&¿Hq4´«öa‚¾®" D´õ΂TÄ]ÚÑÚL—§¶®,Èê@×½|F<ˆ¦9eáë›óPê–/MB¬â85·öuÁA{Ø·°¡œ÷9Èïò|ˆ³kr±‚ ÑýS_q­YÁý ÌDÇp ]áp„i‡Ð’I¥ïç žSÝ „+D¾Þ-±½÷0”>êã6Õ«þñÛ°»?%ÈO¶ý?˸ÿWžìÀ£~ü’Rï?üõú‡óÇŸý›ÿý¿ýó+?þÝøÃþç?û÷þ_ÿêü›üûßýùÿúÝþ«¿ýûßýü‡ø?¿ûÃÿøÝüë¿ýãúÇ?üïøÕEï×™?ÿÿ…ÿǽ ƒiîýN‡Q fE¨¯¿¬­ÍD 4Z~ÇA¢8©Ó±‚ÑVÀ~ …µ.¬›}Ñþçæ1>µÖ¸‰ƒÛÌõ­ÌÀOSã %‹'àþ.iG|ToMþ4;ï'öÊ(› ˜€*°ÓI¶·w‰ÓT”KmÚý@²ïTdiÏ»vØqßI‹”=v­„Ä 3,ª™Büxe`Ô*ª½&Cô;ÔÒ¯›*å'ún’$©â–ן԰®M\WV;Ê0ý`ŽռپΜ=ŠÃ q†?uwEåÑR<¬«š÷±mMÞe¤ EM[rx÷®Ý£OÑÓ ”SRY}WîDy-Z’¢xˆÌT2ùURí­Â  E‹Iå /‹h+o+?Tõ«uo/.{:·âHÔ$J@à‡ú*+Iyö3“Õs#ÄSrJŧNq4°écÉÓÓó¢Èô@¨ +Ô±ªfs3§‹½âœö÷Ü4aHt)§\óåúC‰gÛ%"Kå’,ðóÔSi¥®Ã¶mìqó¯™l3D÷ŸTõWMinð}­Áýu –ëKÆô×Ážš/ÂÃ$â'±l‘ Œ¨÷®8=1>ê´>U¾9ˆ¥§>Þxô Àî¥VÙõÌÀKuÃVÓìù‰Ôê–ö(p;ÀýTÛŠ§¾Y:ËpaH[­ùÐßc&÷SÛr¬·ßy{°ìSíìI2ø;MHßçù!Z¸'Ö¸±Ÿ¶rYYCS€1Å·RïîùûHÆ_X©žªÓ>F7E¨BÉIŸ!ôhã#¬žCÑâGÕî¿ûƒW<ÆDZCÔ²E{*¹ÜÕãû!qXX~”㙉áyÅlD~Ú:ªí9ÐÛã•ecÚ¯='‰Ò§é»^t6W7t%ÖRp1|iuë§æ úà 6dÃÏdâ9·nê($µ?ðV;ÊÆ1Ъþ`y8ÂhÏŸò3Í$+NÀ%Æ>@ÚŬ‰%ÿÝqCÔä”@Û²‰œï¼µq–õ§‡˜ß‘~ªt¡¥Qåþü;gW¿9xj¨¦¸µ_KPL !/f-ÂØ¾ì…  ×z|j=š½Ô÷¶:l®hß/Oââö.?E,k½›Daeò¨¸œŒ¦´F¿)uíñ³„Ë<לÚ^×zŒª§ãjHDÏg +,¯žAÏ÷M«Æç{-(ùža§(´ †NlÓV­C#nE4轪‘¸• `][©¥ÀÍ\I ¢— ´¥-ÀŠºÑD¬L”+.‹’š³D€J#‚‚7µß‰X»LÁXê_çJÐ^¹‹+…y¶ ŽcÄåËøh=¼ç†Ð)e üÍ]Ü@––öÚZRÝ“2C^N[ •ý¶±‰¼œ.•‘ovr0º‘¬XoŸÈ å>$¸®‰²á©JìÚ^øùº?Ó.Ê·(Èpö|»¿k~°Ñ½}-ǧe‡Â–U-LUòuêÄT\UÄÕ’}ᵌtQ˜é1bû¸Г#€nÊä:ÁwˆûBVÈQRv=F7EðžÉºÃTíBxŒf°öëÿÆ;ðóóÆ4ÃvVr ‘ðR;’þ +Â;·£Ðµ`»ƒ¤`„¾hçQx7áy߸`('6ѱu/›p¡¹\{Ⱦ ˜ç^|‰¢o¡»ÖPÒdý°¡ô÷ZÏf÷<뀠/È“;B£¢5Ì(ë7 ë^éŒà•hÆp¥k÷–ŠÆrÛÆ}²ê*GGðÖcO]V‡h‘+¯)0›@&²ã›W1 –ʪ¼<ÑéW¾áŠbÆBì`×p¯4S *›–—×£}|”hüÒCáàIËd(Uć¬ ðw%ùõODéü- *5b‡ñç ®ÚT•HV=@g ¾õÎðwˆg~5Þ°GüÁBê”+ã´÷6”`>…/Îå¼æØSØóÔ9êéÚNÖ™±íA5é¹Ð†®?&^GŒIãt"2~q€Âú1Û³}ºç`Ý _Ê$T¿¬£P ÆJÊ"ŽbPçXTj+Ÿüiˆ”Ïùym‹jÐrÝ¢wpåëæÆÏ zým¨÷Æ}ˆ—RØ:þòȧÀ?VŠ“%­¬N 'öYÔGI¬Zðý´qÈßÛþPÄ»O¼š®ÑŽŸVµ˜M™}³OLúzÆa ¡¬•*”o#x4V´Ë³5µ»$]ë  -ï­Ó! M~ь͟úàÐèc +5w¿²5U¿H†E˜¼eâðt¯ƒwäO»ÍLoÐ…ç'5¢tŽ®“×Ô=ü›¾ŠçO +¯|d/¨¡/ +š³A_G¯R[8ÂDüuºÕÿõ2@í£ß£V‰üæ®ÙîZÚ›,´D ªaÐÓÁÉlÃkàΔ¤÷ƒXƒa½%7Fõ›'¤|Ì·¼Ü:|86·¶HXÅnª*íoHÅ~í’0Í(HÒV^©KŠÔ)¹£WL¥´êÖª×=fCÛðøÖ¸R ôJUá ØgÖõ6®Æš*pW<»Oð¹yÒ‡mŒfIa]v: h3MÛ)ù¡@Ðg²¦€Q¥a£ôxì9XHDÈk¶ž›?˜w¿‚|FÌcR›€Õ,¶·é±D¡³~û97Pë´®þmó<šÑ’1¦SÚ.ß…´ùCè/‰ÖµïK¡²kׯíGÑqM¹XëBA8XC­¦ÓP½üöjýdš£±®Ý^“7³ùÐ~yMÁë¦ÈÑEdøòÕ¬÷–ÝǺ"w›Œ­Ñã[oÌ8³ÜÊ‹ÅwèòG(`ËDQP}2BÛeù€Ôn…­ç‹ëX@ЩU£ê™ ’鯜‰p ‚ï_2¿™ 1XòxCŠ½q&‰§.\³úÓ¸Á”ICDö]C{É•°øa`t° ¼ÿFƒ1PŒ7Ç@>zE ø¿¯´îÄ%Ísý€~¦Wîhi€÷l`‡/V;¶3²81_p.‹ÒpL!¼‹6G¬B­¡ZÛ|®£J(µíõ’§Ol&ºÞgäfÒH½g<,5µ~_2-‹´žÈø7áÄIÕÂöI(MO´æѦ“0tªþ‘âuþ~gÝ"sóÏE§êcÚ“À\óf©Ò_g?â¬=…#QôÑýœ”WïBø®TØ׈†´îj2r}$—ò;ý’á.¥Ác¾Æ*é~«säõ2ENd:ÅâãtŽ +Ââƒv Ùb~a]m¾į"îrH–r`ðmFÏp¶Ã]‹(rY ñyUØË뛧‰‹Šƒ­·ÙŸç™$}‘ÕÀÒPXÉ éÙÚ‹™Ï]S8ßɉ;N0¨MÇ Pªª€=¶àlq‘,sóÒÐŒ§„°ÛFwM’É+ÒÅUEÄψ”hEѳ^q娟Q<Ñ#ÏœªÙˆM4¹7v¬GßD¼£HŸÑõë(Uâ¿:U:C ©µª#ƒxŠÐ”u©.±Ž\:•l´hÈ}ø‹B÷ûy–•Þ€n¨¨ëÚsQÛä{|ÐÒOŠÄÞ=|F Çð}Âs‹lÛ #®Æ`,Û ú:bPM%€¤e%¬KËø xFÛÉ66¨„ßœæR”ôRcñpÕ»UQ«XݱeN¸±Œ CÒæçÍØ’‰F1 uœQwR Å­ÙˆC)ëé;² +ºóâ]È.òÒˆå$¢[Uà %@”υ֒ž#F‚F oHÒOS‡¸_ìGÓzÝbl+•¨¹Ðz 8Ç<û'%jQsÉ•: m¢ðÒ­™P +‹!¨lÐ/´)ØË­ÿ]þh\ÙTÇ\okF÷È·½Îãt \öèlÛ>ƒ”³à ÷¥n“Wt¼Žñ]ÄsE0×~]?‚ÞÀF3É#ÓnkgüHÔìo»ÏûýxnÕÅ6˜Ý./aý8Þ‰¡4±ÙþÈÝ5døÒ¿˜iôŠÚÄ~$Q¨k +«Zë>ómoë; *H„ñ­1´5ñPg*¨Vï&&ŸÏêNóãÓC‘Á‰áô†K7å›?³ó©’51{q±!?È¿¨èoÈLù—LbqC[ÇÙCzÜ¢UGa÷ˆ4™½g¥‚ÊäÓ¢øßÌÅ21‚}$sÍ%‰ØE¾Bä<7-Â@YIìß$èÜ5ó8èß}|ä¡Ï+zEÌèÔá¶ö¯#’<´$ý3ªGRnàŽ{&£hi>¸Ö8k7î¡h`«’ ‘P·cð8‘Õåf`3ý’™9v~FïÑýÜÛŠDÿ“šžzÚ®‰2*Éc§|+„šK”Cïâ´Š‘|{]ÀùŸ¨áü:w Ü}ž15!,YkkŠ,èÑÖëy(Üõý¡jA©cE¼4žÄ»ñråñÇÉ™ã•I˜i‘ÍxŽ¬*Õ…²²õù&Ä u†µÜa{+}F•Ó ayÙ„áEBâiÙü?Y‹|v·^ûï¯ñ{ŽÐÕ@š=E貟آœ|)7ÔÃ’i³¥5¡3îS8Sf^Mõ°è+À |ÇõïÓ6É{ÄïŸ~LTjÙ¶J>ÎÓ0ïY)3¤ÝÜ’^…~fÿ.â»õï#HÊìt]šæÿCŸ›é¹Ïm”‰Óq×Ƭ®/;°æÒ®ûŒœÝ-ï­ñ”¹×6ÀEdmìo 5¬Që4ô>aSœ äØLt*InéŸX§µzǸŠéN2âi ˜£]£¡ž¾†ö æ.ÙP³boÀ¢ÒÎ뀭L-®NÜ¥Dº} +béDݶ[Øpn€€­¼d'­bÝ»NÄß|ïü›‚0–Oƒ­|µgÞ.âãs ?(LSÀçt- +5vë½xø”Žáµ®!Xå(všU1½PCÎÓu®}FAb®Â+Ë…“šŠ"J ÿÞ´ÓG2·÷ºwÏÚšîØô¬Š»·FÃáq¡ÎuÑ!o€îÚõ¬Ìå«Ú|Þõ ÅÒu™Œ,U*ÿ~ꤻNÕ†µá©ÿãõ(žîæŒS­°QÿúáŸü9Û÷‰À¹'³íJ‡åH—\غ: Zx/»¥·n.ÛåÁ Ó·ló<È‹Ö¤Úô Eµ¦¡õ´!‹‹àÀ;š¥M½Í —˨PBBé‰ÅåëÀ ³Ì©Mtzœž0N<ÐUü!?XlÏ™j:ÎM4Z +”#h§Š<úJo.Šd) Ò¨Cê]ãÝÑ÷´—ì°ž'$Ê(9M0=ò Ö„› ´Â‹ðÛ…H­Éæ|Rb¬h׬B”½¢¢D„‘5ŶƒÚÕuh«|ª8¸K’ðjX6‹š¼ÈëÅ9$Ÿq Zè½®4hšAŒÆª$ߧøñSï;É.ÿÝÕ“À°ÝÆ"¨Ðpw’Š™ëš7àsFD ¤÷Ž¼ 7à‚˜ßVNK™ôà(åTöv°5èH€ÇĈŒªd¾’“mü¬ú9ÅbÑèžÇª 6ØGÈ‚„ܪ¥æZu +Ïo‹.}Ûß…Ö{¥1µ·XÅ^Õ è:7?>ƒf rçB“µn]ȲÌVß‘²Å/Íz¥Ö 1k‚ØŸG`5ýò1qEÑæ¦G~„0ꖭ¯›¿8¸]íµ•ÑÍ]|w|)fY%Oå®Óí?¬< WÂàa+Hߌþ·Â$|ß÷¿/Åo‹'¨¦²ó`n©ÉóZ>9žŸç—7ÆûïFaõ&Þnãú8u³â-Rû¼^ÑN.ìƒY¥NJ¡*é¿EäÙÂóÄHãÖaû›ó8DxϘV[œÆ…2°Ž“ü}@Õm1d~x²ŸQ´à(–7±ëûÖäÇ´x¬Ù°™£”-LŒãEBs…Uè¬Y¨[ϯgrѸBkPѽꓰ7j5àÄv õ§óz ¸¨Iƒæxgt µ{îëˆYã cCiˆ2XQLµù6.<4ðɃªS*B˜fdÒ-çûVA&õL¡ŒÙ€ÈÛ÷KžäxHSDYæ¢-G¢ªqiO~Ú G‡RÒŽ%ÃîyVFÊë[äBö1w«Ÿy6ïÏÇy ®ÈŠüjÑóܺ]èt±üˆpômËýzÓߣ®çÍ5îŽ1öžùÂ÷V¡¯0uÚH €hI„zøý†0Ù¥À½”©ÿó4 ]X„AºhÏ€V ôÖõGîP¢u€å¥¦GßYB b†bÌH= uA.sˆa‘Òe!YR}Âñ3@qɶÅ%·?ÈG N ,=åvô×IÑDòÐÑé~`ÍóŒù¤ÑÞúèÏ¿ÀëÓÊ»ë0Jajhñl„Šb,AëÏW4……|¢<éRE|î$×#"®ØPüºRVWÚwT„ï©û6sòíyxj"„6-XÙáöÃ+±)…+žˆKúJ§¹¹_V.Òùu§Î—wÉÁ¨;p,ƒ OÜÀ m[éUQ¯˜`NÕç'}DòU’ËDèrê• ¢·®zòGßÖ›&ɵÅ?ü~gY•[ ÊâQ7N¡*…õ-ªÆVvÍůÚ«?-ùý§x# ;°6p-©oæݘUÞ×d0“G8Éøáj¼¼¦Œ/ ÔÛø< ²Wìؤ ¾ÀM€Ð~ˆO˵o^ÏÍ{\r ¢¦G ñ(ý ?ˆÒÛ #(TÎïsõ[‰²É.i¼q©ª+ÿ+"Êxî5QLÅs·T ’¦„ñþÂ'{ù¹¾®˜t9BCM¡i “N& +¼>g¼CZ™œ¢?(Ó²çüy ÝÐVÒ»4ÛókÔ°¬ø”^g0¾ÇÁ=]›‰I>÷NÙÛÂÈïϺñ¥%ˆÎÁ7Rç;•"¼’îëÑE½¢‹:Öݼí_ŸQâœæçÁ VÝw©é”!øÙº2º7—X¦û?7,úÖ…–ýa”}ð÷=Õý‡¾ Œ]›’w¡³dä×ã甬öÎQ³¹3zkf`&™þýïn + ŒÞ :Õ%: ªÅ’°¬Ÿùõ÷3ðغ²ÎsOܯƒ âႈ§¤¢³oÊÐË2'oõ³3ò§fÉB› þ&@AØêÏX“áܲ¸ò#Ø\û÷¢¼Løêšn 9$8ø§—Ó‘¬gö9r·žº›nŒ"ŸÚÜIŸeæyÕ_HYU ÜY¶©FtÖ/,]ë/@0jÜ.ã ®¿‰Zè½Æ% ç¡cßýÙí7"C2#ÞâJÒ#¤#¶FEó•Í’½/_²ƒWøA³˜+O`¤ÂiÐ̸/´¢)3° ŒRC~‰rè}P~8ç§hòÈaW ‹¦)Îo›zÏ€í‡t¥ÊPiâ,Ù‰“$–³ŽUgüvŒ6m"BE3pšÝ©YhLR˜>V¶> {ó¹oå6 +Ï=ÉþUþÅA â +j ê@/ºìë$~ ÅN,÷^ /»8r“ç@ÈŸžI’A{’Œþ5׋^GAùþ•ã 8]“·–ó.j—4GOB. žÑ‘¬2¥qF£gÔB XÄS ® gsÅ"… +§Qð<Ý ZŸ°÷lÓŽ9…Tƒ½³Xˆlg‹6ÒÃek¢$Ž ±dH iHŠåŠòâŵ,Ç“‹>aJV&·²ø+K&™äòðŽ² lË(? ãí¢?^+èÉ—prª&¿¸w1±'P"(‚Ê‚lo’e¡šw.8··¥(¥ÅQZ²å ¾ËŽGô›ð\ˆ®¹ë:¢m*˜Ý{I= 'v·XMwÈĤÚ%….6 €ên{YµEê]ªþù;ùýoømàí5›lœÔ#e™ùÏ98P`V¬T?DEq§#Í®ø+@š7-½;"‘Sx‹–fU*ˆö²ò,åpP{í[çøÛ¿RÙ¨(YæßH¨È–ÀÇq æÓ‹(£ÖävvÌÓMÿ?TMÈ­6¾iл¯£ê¯WìΦêhYÕD€’LÊÇjfÕˆ…ÄäÙ÷Hã²&!¼’Æ`Ý_†×5z¤îg %ÿû±¬T^ÏÙIŒÒî9­’wËFü×ÙnÅèJôÍöÁ n‚u=ÕR£ÄRÃz K7eØ(Å—Zy¡N‚ãsž>›Õ]-~Kl8ù8@iÒt¼¼ŠPÌT¢ïSÉk5Û=ÀBú!ÁzF—€D¹’eÞÙ“…Š°ŸGl"{/#»=O¥Sa}m'3§•ó4öXéš5´P×q¢ªP®Aaº÷A٪Ŗѩ¸ù¼m±ÛqÒv¦ 48h©—`›‚aÆX=(¯æ̆ ÆàQàñfKÇ,PPÔ~B\]8JuárDgGø@ær~~!ü-#6År€?¥)FÙž|ö´Ñ²Ã\FÀy|[»×oøÍÛµº})ÜŸž-tÔ«ç±E?Õ gÊÃÛ7LáÆî ù¾Q˜êQûº}]HOßÙbåv‘é‚í¢ U"(ÑÎO¢£ë \ÅÐ3ˆŸÊªZpAhïùqwÄÍ¥}=;ÙøOò'b£±¨ÂíH.L7É[£† ÿÍŠJwAômnQñ¸lœŽè÷‘âUE(vnº…°ÅÆ*w€Æppÿ°?`§¢`–÷¿k†yÅT}8€ãTuEpöUÌ`—´Â@uˆs0$ Ö[$;1Q/PO”·Üؽì¾gƒVÅ[¼ÚÉ¿0Ñ›v^q +S˜ý'LaØ@éÁûjˬb¾PTˆXQa§£CE‹­°ÝŽý¸4MjÐÁû ›QéFH•êkœ”ÿ½ÝEçqDÌ/U–ëÕ(”žoVZYŽ~€KYòÒÉŽäÁ=‡¼µúD¶Ä +)“)‹ ÕŒŒÁÞ¢8„¤CàêwÊaÚ Œ#O‘nvk 6Òá +×®‚¡ËÉÎõEÖÑ{¤¢ ³‡ž ¾å_H@ÚÝt ÀW;ÊÃW°¸#€ÉU2ÈØEëÖV˜Ìž<ûû€h¨Ú9ÛãþR·cgEÌ–Tá,ìsÿ{܃ÖT‚Ó4Ø£¾œÏᆳ*‘Ó‘½šQh¢^¢ù 9Î÷ˆ AFК¨-h1´Z„_þ”BTØ„²Åþr\(©9Ší¬N‹k¯OhÏ+4Qa–:Úïì”Ëèé9Õ+—Ö™®Lu±™‘o½~aKNý@ u†ÀAõ¯ç 9˸?Ža[)R[îé(ážžã˜1ºjïmºf—V,®3±ÒØEùò‰ö»í;þx>qo~^16€l„Xɸ/Ä*¯*¬¹cLIyŠÝÓJ‰“O~ÝGV…Ä™O‘Ežûð$¯šÏRR/R;ßYl-J úŠyó©2DX_EH»—(Beãt±&pÎ;˜|§a‹Ëû³GµfÝQÏUŽ©\A!˜‘w¹o ^Û’ öä|”TúLT[Fh¶©œWßo~ÁL¬¶‰@ݽÆ0BüEëì],7‰×Ñ8Ãì>*ªI`¦8„]I×ß³*:¬"û6ê¥.qÿÜ¢HwUñ‚VSL«kèÉÁu$áLºòÂ[Ò±J[nÏOPÇ®ú{£SÑ\>Ÿ"ÞP-±K|[¨udãÙE7Z¼ÄÐhJ{¢p‰Ö<Ê™íKpæ>Ò(….çM°ù¬’Aª™`7±·ƒÝßÞO)†Eì£VÝ•¡~<ˆM#ZBsW¡ b/¡B÷*!ô§µÔ¤.Ó|?ezø먲ä³íñYép_kÅe¢íw'l¾ƒÜò™–8]žà±¨ Y¨÷´óÑéd™…µ¸àfl™â!e¾z•N¨šG©ÚàÖ5i53ÀQ,O!¿Òý»c7ª½×Cj9òÂèA@ƒä1‚•Špõ)f5*0u?/NpÙëT Iõ'KgfκÅLßÀq¨·0Ž% +]§ælpO³9¶²r1òBg->j‹ò†I$ûKþÈž±õ¼ŸÑÆ©xÌl¹K%@ÐÉ žwêàûç-qÜîç[öóŠËÏ°@µÖu¶”# +¼lš"zºÀM,‹aüµáÅn¹<ñiô/§f L{ßSjHCPîý{¡‰ÒÞˆ¿?Òv°ÉÓϘrŸŸŠF‘9¸‡ê7ò]”~Ù¶»Æ9Ï{€ªM2 /!vÿ©õ˜Dè¡Õµøyº;jÀÖÒËóR›?§yÄ aï1Êæmo-wp°Á£éÂIÄÈ=²r|]h w¯˜‚+ J•DñµˆÀ  +©9Sç Jìl1ŒØ +¿cӜ¹›ŠZÎ…ÈQ x0>‰/y„Ñj-öRƆ/‚sñ}ö•ÉáÕŽ¡ê)X³ Yœîwm&ŠÜ@Å%]»ççîíc5—n a£îë’¬Lû‰Îh= 5®†É`*0ª›Ñò¹¾d@ÊÑt}sD¼™ÂQä"¹#@ܘ€+Êì)@aü÷^Èn1ºlì[ÖY²o¯€òºN ª‘EæB¶ ØÞJa%ª©Õ\ˆðŸõ@ÃÃaäú]ƒdLOÞž¹€Cdö › ¯:î =î<í+UFØçܸEìÑY\wWä5öOé¨Ã9S¿ÑÁüVœAÙHÖßS3"—D™î1©@'"²E°3f¬Q+t¸Ûí¿ñ<èUxˆq½NÒÍDʨI›G¤–ëv=÷/,ÑP^ƒqUsŠw .‚ª…íƲOù‘BD×›D°ëm2ˆ%€ƒŸtÊ¿äÏPv¾RîcPý¼LA’´ÁGT去νŒ‘ªû‘*\5è=‹é{‚ƒA©”¼•LOtüŠ·fé U‡k¼Ó\y)F$6D]ouR¯$ fL¤ñ§ÜÆŠ0ˆÂÊ´‡ÑGu0R0èn¼ÙçQµLܨº¿êçÊxE’µ( }Q͘•ð¤Šwo|²ê9# Êeûí«®> ´4_*-©?ã9]Ó¸yÔYØq!@trèõÞëôèìŸ +užÁyìý1 Ÿ©¾V8ËçÝaA¢U!Ÿsõc²L3¼G}¡¤š‚ŸÂ1—[6º2 ˜wrŠ¥"ßl?— +Y0·çêïã†Ì;®ÌÜ-* Ëzôñ§Þß #ö»³ÊòÜÇë:;x£\QÈiçáøÃñãƒjúaîq`r­Ò‰쇾±(®‘Š¸˜g4kI[l]ÁW'ýT©¤ + 7Þ¢·0†¾ŽÂ$TÒ\Ur+LÝ'/‰†x_êzT÷.èÞ$”¦ì¼ÎsAngXŽ‰GFNÓŲñM²…E€ã ²›ž¬Kfu/ñ n»?<Ýêœè'–™ vR© ãÐ/š·Ì6v¯ùSбh}¹>}D…í]q;À6œµXKàÇÅ{ÐóB¹JfjB—Î-R´OŠM.*ËñaíÚs“Dû7‡ê®B`;g´ìjê 2ÂyS.sÏ{U©Ù˜©óh'Ú´&k#—âN?Wó•¢f„z‹p/@“J(üçI¨!ŽÎÅi¸æÃÖB†Ð×’J0¢ngâzv¦šÑ ¼QEø‰ü‡xÇUs¥êj <¿çJQîÙ{¥à˜5¡‰²„r1”ê° P‚cÿê$ƒGàíyzÑÚS3g|j”(ÜÏ¢¾äÍ=71¢m;ûqyð\iá°tëtGct³ ”ŸöŠ¨åÑNˆŒ;âÝFî]ÖõG8%ÓºŸsÄ»V-`¸z"\"ZO±{\ÖI‘tß½÷UEÚû “aóQ^F—ÅÁœÄ:TE¶ VªëåÄyìfÕ•´õ4uÔ­XÇÚ͵˜§Æ~Hê Wp0¯ +·åÄcÛ‘zy ÙÌ…2WĪîžÓ¸‹¤¨ìV‰t –4`(šè`,öe%ÖˆGWöæÚ{»Wžì +ÜöÄ1Zúºzõ# +ƒñiCœAˆÉz‚ª=MjE©ÔžÝ»»ó¾RT­@;Ê(5C0sÚkUKç<ºæþ[]³èo]°ü#œ®â‘Ìò=º1©«Xh]답œ}׸b +ä E©+Ëvét‘ØV`®¯âÇ:xß~#kgÕË*íÒ¹¦ˆ-²Ló1£IÑì…[FSÖ©~ÓL¡î勞+ ÁºñUž±^¦\{Jaú±m.’áˆ.“¡«DÕ²½éj¶¹JÛÒ¶6úûpÁH?”ÌG¸‹%ܲMÙñöÈèB„âdxÎqK×Èô†qa‡ÖI¡ Z'#û…îÊ놣(ˆHí=RÁ;ª§î0t 6ìÇB¯Î½idÛb†ˆ…‰ç‹´­‘}²À+qrøˆâÇ—ë‚Þp̨ñÔˆsã +沟ßÎ,^{>B-£¾ÆJÏ‘¢Ý‹Yo³j«o²©í–M#Œ~ÅŒ­ÑF¦ö:˜µlTMàÝ'‹ðíA¯Š=íÓn•ƒ1a ÄÞÈI†ÕY÷u &½ðþçu" Ëóœñuq4H–ÇDji"uÊ[ƒª\óïZÂÒþ¯%ZÇ݃6#êC„.âf_êhìËÅ®îw @‘êŠ6âsЭa*Ç®®ˆ …Þ~¸³yîÖK¹_D>nÓբ쾯•ç‹)&#x§[yCðHn9èAº4w늹O—HzìSitνlѱ-ÔPš-øS›~ѶFŸŠïñšö³ùe.Œ;­­rQ˜Ž)í#&²Q9ÆDDZ½hüÝ¿ DéY|ÙÆqª's$èºí×£ž·Zõ= ˆ› ÜLgb…q²ƒ½Í;jÍûP9t€šj@ÃDkFð@<*˜äªÕ|M>Q¹e‹ÚÓg¦ÛÙs$œYö÷üQŽc¦º³äZóÔ (XßÍ– e’Û,S\ªØŠÚòÅ\]E“rSkV ¾p’&›óꧪlY°¢^×Ζ¾w[¼‚ÐRt²·dˆ› ¶(µd“ÜðŽ‰Ä¨E̺—’õDHÏ_ÇÀ…ð[Ù +Õ|xôÞÄò@Ò W¨û©Q3õ° ÛèQœhSΙªúëntê³nOg˪ÿ^úB×e_hT׊bÕ´¯9Ë66‚ýgç+õgT5®L´‚jÖ!ç5/h`ç3ÂUÁR¤€Š€~qªü?q €sWR•^»y=}%•p•¡£áæPt)LÁ$þßP‹öîáÅx6„GPØ×I»X#׊زMCvædõ³¦§­ +âRÔ/( 6Äh rƒœ}²È75ÜÛƒET»}ë]^ØJ!—³·Rÿ*òèo oúwÿªè_y¢#Îü§øŸï,1Å©P€+Sù¨³cÆ^Ž=\f›µ(]Dùx„›S&R©FQýpEç¹¥Š“}H½wã¦åÇ%«¿EwÖvÌÚ³>÷ê&ÁÚ$¨Z7Œ¨g³<º§?ÁÒõ@¬w"p}‘Í'ö +b" Xº#ŒK.:=Om‚h&¤,#â‰Ý½´§@;³Ò×·{Z˜?Ú×…l+€î¸U/Yÿ4h“›Z‚•¡`}MGAéUsoDŒlO‡;´À¯á­ +¬ã³„,Þs$I¢È1Mt·ÏìܶœÜPþ¢UÛ(C’ýâzù²Hïì ¾{Ùvúé ®[¬$j’ØoãÓCÒÂ-< å£v6c:_çB,ø<3Ê&ÑaÇ?¦SˆÜ·Ã³í%^/?ÜÉ[ÜËÇœy&= ˜%?VÕãŽN+f}­=IéA6ÓO×Xt„ë&m²`÷5è¨ÀÏ|ÿ>óáäºØ‡\xL(7Ï45¾.Fz¡ìž ä—L¨ïçåVàPîÃí tcR h¹•¬-žWópÚ§òÇg“‹Ù)½âjÛÌšytXeüßÏYXhúóÒPVHµd5`¹gÛý'^ ™€#&¹“ÇñMÔm™súiöy^š0 ×¿‰ð"=ŒÖó¹ÒGÔµ²LãkO3ÐÕ^œ@c ¥Ö„­4ñV·ÉΚëõ|ÈgA•x¡J³þè¯Jôp+×Ò!c‹XûEY6j:Uñ§±Ã“„úzS´ªz]¶EÂa ,„×tçÏU@X=l è¯ÂÐ_dl¶šíØŽ˜èÉþC«ãúù ¬æaczŸÛèÅÝó^+ÀÌÆøiÓ+ˆ!¥pq„,ÐrR\.ð6ë˜:#2TeGUNO±ZéŽÖó’Òóë0Ó®0Ó–ꛨûæ.Á‡fåtXO¤DtT¿àIᘈ +P5©'¢žˆþZ½êjüqÌÎÀcqCo3s[gèEžõ׈o‡äGDת @¿Ô‰Ãz¦+fö’³5&’¨-ðÙt?oÚ‡ÿoYŒ­a˜ªô,N—à¯Oe Ú%3…`×:RºÚ3Ý`Ìe$ Ä8Rw>|ëaÇfE*tΑW&‘ÆGe ¸ºS¡€û~1]Ë[Ï›5Aÿ)Lqo{yýÖUíºh¨‰-ÈÌ@OÇæuH2ã |ÉåSoJ”Àå—ëI`Ç—t)¿ Y…¸ü(%ÁÒ­iýþ;$üÿë†àþ‘êÏ¡¹b#]¾=›: Q:me.I©{ïW p·.)½û‰=‘Ôt£CÂr¿ŠzDuù²4’!BãUt›2ÝVÎH™”Æ ÷¥)ÿÙÔëϦ˜¥Cü¡¬²ÑµÀ®{Pdm:bå¦{@-h™Sb€«bµÃÝ‚^Ü;NÁoà•Ò“lK*ºè¬ß²¡Ýì˜&Æü‹Â4c’0ù³+¢ôiÿߘ‡Žàš±8å<íID2t*D4´¹’‚¬p.¯\ImòëîÝ–y°3{µíßFjd嘸ÌiªºŽë[ǧV° +‹â‚M.¾ú4 1úöN‹hG!-UËSÒ'îИQì´ø¶ðN3@ørðX¨•ÞQôÓä!Û¦sÜÑCÑ1~þt ³^NÑh«*zZP<ÝçŠF­Òå4¨]1´³Ö£NfSŸoÉçANڌ펼t;Vò/îÝòŽšwI%ÞÆgS":.·œ§ß1Ý8F/”ì’0¦¸XLì¯ó®\ ¥=ÎAYbMa3ÿ7RlsƒØ +hž31AèìÑU^Ýäc¦¶?í+Ã[«RÇ«;j²šÞc ¶¤Ý6´õyôéËøýoù5‚æàŸ® ¨šÈœí)¾ž]— nXï{HaáIy/AMéÚKÞ@§äHQ ¤H¥D¤ +âB”ð * öû ß)ú«Àþ¿èh Mbû2=OÔ(§›€Á<+Ý%Ânxl¼ëœãBÆk'X/¤œž¬°.ª²ËßtÇø³ a4K)³Ñ÷¯úY§† PhœW…8ª6ÌK€äü‘}Q?-Ïzw ‚"=i²ßñS¤ÊFµNëžÈ®¸‚zÁ(V¾)2üðM¯^ú±È ' +¢öI`¥î!]âââÌÐŽö0.ïH°™+Eùb¡ÎéÕõˆ(_%Û~j~”£)#ð‡Š†Ešn¨Æ]OÎÓn%@PyMDgæï\ z0´%~¶ºÒ`BEnâ«áñ8Oš^ªÕðAï'7ƒª+·\f6M¯ä(jOß#·Lz„ÇÕ»ŠÒWgÑhdNôÕºS»G0P§v¾çb(Í&Àùg1Û=¡j¾M€÷àa7!® Eç"ñHQ_Eâ‚ÉÙoOÔã3ÞvÉþ(íbüñ‡ÓÅPÐA’õM”Ê¢'ïd‡þï åÖb{ý•}dTÛˆõdž!µf¶I=ôìÜFýq®¨ÕV"σi®Bó˜Ë[ÈVð«©Q¨½¼´µ$+:/Z¿!Rê BPäÖu \`zÙ«*¦üðú³dŽn.Q8³†îÙb×óÚRvœ #.ÑÂÖÄ펗)Š+¦µ']¸].”Ä‚p¿s‹3áò¤•öl2doyæŠ_ÞÙw‡hwót^ï (hIBu*V¤yøZt“ß@[n sóm1÷Rcäõ¡@«þ×€LSÇݳïWÄ|üyÐ Ö›`RÓ©¹(?ß0o£+Ç·ý +9~FI" CŽÑš +xPš-_› O2Þ>£ƒ(!¤¿œçûèÚ«þ—ëaK|_¨IZ‡Ô›·¿Çë:p &€¨¶|Œ*ê§âUéŠÇ³þ¶·ô¨@ì• –̘®ÇÐý½$W>¨ô¨ªFŒôyüBÀNYÝÛƒRÈÄ®© sµsu +‡€+h_ú›6p¨²æ-$Œ˜-ž‚Þò üEï©ø-íQc?Xž1½È)›Ãô5ªhó9Ph3JzéZ9,>!U8N*=Cl»ïP5ŠžËàù:›©’ëuÛ—Û’@&šv™ÔD"ŠUº'VÖÑ G -:í.†'ˆEOÁãp‹5ŒhEá@¶‚ï“Äy.„ó£™' +Ž€f$r§Ü;”Vï”OX/¡¿ý-7FíˆnT=Ì~- „ZÜ~˱þJ8ì‡1Ö¨úÝÕÛ9JQ]7¥¥;±ê©š‹Ç¹ótÛœÅfa`ß‚»ß×UH@ x÷ßè1J!fK÷°Žòøyˆú@}§'ÖË+‡Q#‘RاKÓžXsWýH/Á¾M9_×KÚ_ 7]ÁáC=á7V)£{œJÇg'Š•’œ•&†J±d(¯LbO €°Ê9 0õJJP ™êYõð@T ¤Ñ´^í0r 0?)+ëÞRroÊ…è fï»bv–“‘… M Ù¢â}1}Ü +R/ó›àf÷¼ÿŽzK–À§æäAá¿`Ó¨Ãí­ÇÈè¦È5-9í‡<¥¬÷ÚC9{~E€ÆZ²d€òÑ`¸oô© ä •Éûx<óËž‰ŒŸâƒÑHVËÙ±­0úÑê©d%7<Ë»dÍŸà–¾¼~=8®èqó3uí‰raÃ’ãx½ä39ÿDöT…âS[Þ{V˜—2ì·ðVÆž>Æ€_y™·ã“3(âÌ‚ƒmå ˜Eñþá9½:Øü!Úq¥}EÍíkª÷µIþÓ(%Íg$Í÷h¢çñŠ< +(]ãþCw¤Y?œ·;,~=¨ŽD ˆ¯ëHV˜Iäz#gÿÌfFÌ÷„.2 ªtÖÎ!V}Gúè~ŽûkZÀ fþU<Ïùl¬à6)n]®c¦„€Ûþôô<${,W˜@]ŽÞCÆÓ¹^ï‹Ü'UóÕ@ß_#bÔ‹n‘ÈU‚økÉð'|Ë%8”gÙxÆp)½öÞã¤c¤ºþèÀ¨¦°ˆSuûyÊ'ÏøÚŸPUŸ$Æ:K-ËýCÅ€(þqÀŸˆÖ}ýæ¯ô£ÌHך{f„¿ÿ®x·†ÆØ1b>Ç'¹¡}Ÿ{”­cºÎÎKryoñôû8ðŽÝ¹‰’èúæÏ)k)úwIiç6-PöY?›L—ÈÛã?ùU{B¯Û1-Rô™;´¼s¿ +C`E—º¬ªžÁ´ÁsJ•U=à3aÛøÝÁHœ=gamWÖÀŽð/UÍaܽ:„ˆ#"EPY!xÊèâ㨠ótçAED×¼G×üÝ dŸJþJPEÑç׿j¹©rROè¸7%+œqä|ôGôjRÛîþõÙݹÔ›¬Ë{™7ÙÏi¯„{I\gØyà>Ž}?.é¼›@ý„¡»w%‹ÍÄÏ% ½‚¾Ï™¿6dpeÑA]À=8DôQqUýºa@BCUït¾¸a'#ê…¢$íâ¯TuàÚó€ïç:WŠôRUÅ›¢"®I¼­(!Å·²ŸÌ.Pn¢g\5?Ÿ˜¿bœm%z“gYâg›¤!GÚ~ŒèMêDS§©X|ƒþ­ý=€ •°)Ì“ ~Tú¬GúŒº¨Ýæ×_÷»®]ß(ñ¿Ã]„T¡>€É\ffû&ÜXà0Xa°¥_ýh‚ùx–$”ß3ËÈ¥÷eG0­ªh{ºŽ +L[ëF|·ûU –ê üzàoßah¬¼§}FAì ƒ?4EòqŠãäã¨.}F|7sDݶj˜‘:<ÂŽpùŽ¢?n\òã–>ZŸ”Æ„µÏÃxÄÿï§8{J¦59€Ðœ†í*m‰ŠÆƒ(qäÛ!öi%•’>l‘i·È¿Òù)ÀÙ^ÇW”„Ÿ‰ +{öžX$"{û’>üËÙ!gK¿VoåùæÏ÷ÔÖæá¦ÐÏPK-AmýXÁÞÜ»u¶Æa>2ÕSbo¯»‚QõD•7Ê:Á‚!uÎ#äæ,œˆy"t=¯M„Ž™>"pìÍ•Ž3êL1¯1ÁÀ˜êt4bšh¾y'zÛ¸™‰`{ÆüGŠü^GÕjjUzB·`í–ͳ¼~ë²»÷>ž ‹ÊŠ‹ˆ×!š†IãQµ„ìj”ùuL µ±é{t–”8%ÆêÏm*|Ês{ÄÜè%ëNk‚º†pÿçЮ/Þ¯w”r¨QþÑÒ§­FT&Ñó"ŠPbñ<,”û<—MËç`PÐl¿ãxÖ×ýu¥zÄÎéÔ÷Ðÿ“ýo„ó]„Š9¬–4•¼kxd®±H¼+üd,—kp +3òÖÿbú©1Åu}Øåí¡°¡Ý8Š<‡¯½êÞuï2•+ƒÙƒ46@vû‘5ÅS„a_qµjv?QŸû‰ÒÛB‰i¾åf£æ‰Ëpêk)ÃÈ 6®ÓH¸ul ,kF‰´1ùP¢é°Š™ `ëµúQ÷—Ð*÷iæFùÙ}ÅxZ•}ø5šY0 ²vﯴ {”ðª—R3ìí†4öra[}‰±LÚŠ¬ÑâNQ•×Ôž»¥H}Ÿ2tgI¬êùKqZØ·ŸdsÔh(ìï¨(•çM0^n‹ØþµÕ®q3¢ˆˆ +J |Âœƒ!Æ9ĈCUcÏïøçJ=ÌPÆKôU}-‹Ë-wt€ f-IÖ‚\¨Éì9_L8tNiʘ'Yߥû&|g6_4^“ã>ïêoTOä""š©›Œ;^Æj?  5¥…Ö@Ò^ ÞÔëÄúóü¼’;eÚj’æØu-ö¿/PïüUdvm #ºfsüø’ˆ þ”)_¯G<>úÙ•½UPæ¹=¿6¶ÐEòOÑM„"¾-ÝYÐË×þRÿCÑL² ŽÀQ…øòê|œÚÕ*¬ç5·CR¾DàAº‚ïÝ“P°ü{ÐÇõÈ*à-𴯿AÜDè2ÚÙÏ"Gpò>"ÿ$pUzÚËîü5 +¬ U\Éi)yÚS£)STE*G5µt[Å¢šF“Ôˆ‘ˆûlØ3RäÝ<ßœ‡1q‘"%¯%Gª]ÆÕ7¿8Æ~¤?B{¯(„¬Bª-XJ Ò¶sÅ4Ô]ü2(úåEÃñÐ…ê{?߈~³ÌAó  +©$¦:¶ú"Ówæ[!,dÛ!e½È;þÍ\fÇÃÎAÚ´ûtó’Ò~ë™àM€7£!#rƒàüŸHR)@\Áû¼N¼­f½u +ÓÃãLÒåMN³|]Z<š#V«!ÐË)¯å >#òà?õãÏ÷ld!žÊû”r6S⪤ ôh>"|³=o¶¿ò­QÀ×I”Â}ζÜè8ªhz~â q0€z“ï|áJ?b,H<Þã3*8G¿G[O<é /ZøDà$¬ÂyvN(öß´¬#‚VR«-T¯z4ÉtòHMüµ`ùÑWÌ(D~á²¢^A”¸[ e°ß.=Ž¢¯-Ǹ ݧpÈÖkÆ‚ ›™‚BhøÆG¤iÿ ×¹+™U¢½§ÿÊéHÑl0lNŽ-VËP¾4é®uÔô%Ã?¼žßÕt„)¬ -ûB¥:'Ñ›'“V£É¨0SÊÊõQý'¢êu]¯´ãpXïlÿÇNEõ‰KÇ  Là©—=€¯WÓ/³hþM~=ì®ö”a’íPÕܬÎÜ¥«Ϥ·åahH)¿Fdè?ÇÁ?˜»s$EHàYrlD¡ËØïoP[#/ÙC»\óåfÿõ¡žJóò’¨¹?¬9=>(Y6^ÃW*2pm¾÷؂еä,ÖæÓŸ`°úÓÑ[xÖ»2¬¤ÍFѾ,(9Á„¹•'Ææû½É*¶‘­_Ä`-ŸSGîãÏW‹¼wGõ‚*þNn­ö¾˜J¾ +´ÒüoÁ"÷á¸Ñ/Ý·z6A|9ÐÐÐ?Pý²÷†¾Îib‚ÃòxX«á-ìÍg›yŽVC:RQ=0}‹“ì±ü~07&IŒ~Tþ”¦ Çq3C½bò1GŒ*Šâ¢p‹!ìï)vÌqfÞ‘ªÞ±H¾î«|sšr4Z@÷2q ì* D”—3‡ØÉz1ÄQWôDñ2¿¼ŽNähÄíD'~M•ž?éÉ£ºuÔØ{Ы¿È½\ÁhaÔ-ç &± ïùs­ª›f€G˜Pô&ü5#ŠþFX¢‚;Ö }‹Ÿ+±YXÄDѤ{Ägþ¹Ëà_)( '½>éÞ²n.E™÷ðyÑòˆ¦@[»¸îëÕ߆ù?G›¾E@]±kÇ9Ô©U¾‹ 0”¨¼Ð«{o¿Dõ£ ÝûX>Ãg¥x¡’/Pð)¡(ª jă¦cÿŒHéa%Óƒ®¼Ñ¯Q΀2t ~°ò+J¸§˜C…»’b‰JÍs!_9_Ì<°œ@¼u¯DaEòwç~žzN·döün þ† 3UŸ‘`k¢A.ýë€9´ÈyÕ?ªqpA¢oj|L\Ð,ú]œji{a±’¨q‰J?·Gð¢pw˜q¬ðÊ­ÖW‡/qì©CUǦÛ÷Ë"ê9?¢u iCWm…bDõ -ÄîœÉkšƒ\é99ûo£Ì#hÝ#h=Náutw@²-vn\YVr4Õçoé‹ÝÏPSÒPêYÑ.¥žÎèý¶ÔÑø–U 'pßø"uR­Ÿ ˜œ+ÖÑèrHÖEð©(2BMØ;!Ø»ŽÔ˜ÒæúìWESj ÅŸ~=pà©ürŒÚ³™‹êB)‰<ÜÎÊ9€N™ ÏGœ„gû:ÈÊ‹} Eø†‡Z4¡@³ïȼâÑ‚ÁÜ/Èþ¤êö{7p‚x¤ŠW/ˆû5'™‚iü«Ðœ¿%þGîí¿Ê¿ý+OvaÛÿ˜`ëù4h~üÛÿîÇÿùüë+?þûÿñŸþéÿþñoÿ‡ÿ¿ýÃ?ÿó?þ·ÿò÷ÿþÿùûÿåþÓùûßÿé¿þÿOÿ×ßÿOÿñ?ýóÿüßþéÿý¯¹è÷ñ¿ÿãýÇøçü¿/ñ'7÷|ÝÁMøåÿß»žº{z™´ìjÚþùG´„°¥`芠@üÉß‹WÉ^†'-ÐÁ_Yg›rÓ egrþ{õ…ú†~Ãdvë[rrc¥¶#€ð>?›EíËM¯/-€j;w@|jñÅ|DA¡šˆN‰½év‹ô†¯fDß9°xÄ4·bÛt b?Öl`4ê½`Ê{-iÂPñnxœO÷d€Ø*§A«êD$§ ½Ô€@ë{› MÁù•¤¾cæ°”×"ˆwñÃå€.̸›{C4•»'âjú1 @`DQo¢çŸÏEÙ’ŠwG2y€æw"NO°Q"öoŠÎŒœˆaA=,@X 1U¸œ”ÀÛéP.¼¾‘­ÛÃQ‹Jõñ\*ÝÑ m€ä±6|p +Ü ëéåï_‡bh&ìR¹uŒØQq¥.Pž¡UáW3ÚýB. ûeQö+8&– HìÔ÷-ÌH«,¦-²3.€×¥D>ˆŒû{&‹&€:©éäù./‚xyÑs”\©è§>™²0ä}¥çuC ÕLª%Ó‰¹¥¦C‰âíLö·§çß÷fÜ¿‡âІœu#€}Z@¡ÿëüÐõˆR,˜(ÌD5„ñè8¶²,¹Ï½{¤löwqñÒ!z¸'è7j €8÷ÌÞØns(f¿ÿñ!8W4©ùJT•q W§f¶2çß=ko~‡€X7¾þ¦½-à–ø‚ÅQAbÓð}’ß¼ÞýúÖ»ýô Øûc€Uõd Âàðr@|êý¶VG=¯ë—¹ÿu´û›¿¢À9ìò³Ex|\,sûön]˜ÆuüÔØõžÊÐtÿBü¿ó)æìq|^†X+¢^½±á…Ã{é™qö˜ª—·r9´¿W„`Á$5©¾õ¯ I +EÄ™aOm‚àÏxšFÃ[[é’€:@ZkÀ~üW¦> + í#'þ÷B0]F˜.tùæw DéÀ(Ñ€aRÚŽ.Ò8MþÏGÄãþh2ë[Èÿ&Š `ï§)D)µ »\.d’½óþÑÙV ˜8±D0ŠåN;·ròØ›çÝöãÙÅ±Æ +<%Þƒ¼Ÿ%ÞõL=¶wöÛï–ˆ½'+D`Áy¦Éz¦É;çg +"¢”Ó¢cºe0EÕÒ(Î }…v왶ûtÚÆ Ñˆ{]FŒbÀ^i˜mÜåBxÖr¡qÀ,L™×uIá9(DÄöºúy6ðÞt¹às@ c:Clwx]Ê"„‡=‰œWom9… çfTšeª´ ¢ØMìoåa"ÀY²s¿¸ÿ\YÆ{¡0DÝÇÅN­:T/W1­‘_ANÏΚXšâÚü +(?,ƒÏY0Ùwú{AÍ3Ÿzá©ì0–3¦kÕÿåçÔET‹4í){ZãÜÛ8™†æoD8âYÌð"+]”μ €<¯aCIQºßÞâ˜_[)¹«¢\8\•ÆÎÓ–u9>M~@¾á¿ô…ü~>¨¹ íF¢=¼Sµ/<ÀVHúDí? ŠH”^/âryçå°AéÁàJÀ(I í ô•æ 9à@ ½Pö¦Hú+¾VD0 Þc䣢=Êæšw8¦*ï°ú¾Vv´9°†§d„DúMZûPÆÞ“3òÔ*)ìÄ}€Â +» Eüžû3 Àsëw“äØï~œG6â}±›ð~ÍíP¾ Ó¯åCwªÃz•K…°b0 <v£.)-Ž£8äµí%×ÒÆs†÷ƒ"Ò¡â(q[ r“Œ?xÂñVl‰o>‘Záà¯5Ã3¨Ê.š¡²>Óm ¤î‰Ðx]}ÇœQ"T‚ =»Hà^o2ötgŒg/Ëå–]íâî$ +'«=¶ö$ ‹Åò’µ§ÃÎg–‹å^P•²­`š" áuZàGÉr7ÐXì’ü,©ûbu„ø³bp¯Êß…CÅ7Ç¡!ÜlŸn¤ÔϘúŒZfÃeŸ´Çå~3öÙ=.@¤L´S“T¿]÷îêÊHêþ߃ôâÇ^r??ÏBŸ‹þ5íòâ}¥¡…†‚ùgÀÎÕ^.ÕDZ[)51£#5>»6†1u2¨Ã$è5[T’¶¨åœ?Wʺ÷{"ºÎÔ€v»*=Põ9¬‚¶™ã}®ásƒ†5Ï.x°ÞáÓôIBÜm¡¼XÒ¢Ká æ)÷ƒýû^#îDà€€)@"ª5‰…ŠtîT$ûí~t°ª¸t¾&ûŒxÒó”¹ßI2÷ö0,[ž0í÷ -ýñ]À±[ÓÌèMÕ>¢ž¡H©ÿ€ˆ7 ¥'#ÜGÅGÁë€áFñ-úù«D¤ˆBó]™€í›óu³Æs£pM­£GI›ê,ð«ÏˆuT½Éƒ¾ŠÃŸQ +t¡=ôªöyT„ï+û î×HåâÁµ»RKºÙ|Ünò+®<ªXuïü-n7\£âŸŸq@sc?t“jMc ýú1RªàˆoïQ`Õózö¾ô¨96a’{þ`[éV`õtÊ.‘n·Ýd†“Øèºê%¿¸ágNFÊî]:©»g\Zî@‰¢d~:d^Ï“¹2}eSR¯ln„Ò2šbœNx*е`“ßZí VìO¾":ïfLEžˆº°¡&Š'Qݺà۸4ŸÑKÉç¸=ðI”@ƒ^çßAž_Zò<_çGU®Xvï‚dœ»ÞaËæ2Ý»P5ÿºPÍ°Éñ(qV+W‘/´Yß_w–Z¨&[ùkem5‘d³ÄG¼nM]&M,„‘ËJ~òç3¡w²3Åß@ý¬®¤)ÍNÇ.êËcÖ’2ušX¬÷ l`„”áS¸Ú +dü˯#!é–²2O¢³ÑdŽÜk¼voVu“H¾œ”¨ÂVt°™[¹iM ŒÕóX¬e áa«5W‚ÒÖÝ{ïïcðˆkêG/š‡àBTÒ+M+.̺çÈùÉ<.!Wî“š–÷Ø* Ú9Ÿd† +^aÈ[O½¡«}zqhr›5÷‹jë<]ŒÑþ.?ê+RéÔ¬/&%8HXž0‹cÝ3 ;ûqÜ¡yÜÔU÷ŸÓdK†¦ endstream endobj 362 0 obj <>stream +<ùÎ2`hxû1‚5eüštÀI,K{¡¤{X¡Úá"³ïú×Ëpf7¸':¶³ˆ¼e3ïªîu À ².ýež¬°‹#Øw?A̘  T‹¥?/te1’js³¼r›}¿"ÿY—«ž;(÷:?ý%« x¶_?`¦ +ÐõäSJ" |=\”8Wäþ› ‰ÖÂsØGÿѼˆåçAüŒýNpn¹yû‰PØOäΦœ$¯†ÚÓÎ;•~³i½öá•0W»QÒ|y|wžK úÍ»QîÞ#VÈØ¢ÚN¦£5¬¦Ñêæn»ŸV ø©9gsž}ÔùQÏwAa•`Aé,9G·†dJ“®Pí΢è/€G@xPýºB›õ;é"‹¢|CºG—ãôKÄ;ØØê1Ø®7ßœà –Ï Wï,v \Ы\(%îuõ7R;ÊÛ“ š½÷¦º}®ôœyhQC/F È¡˜¿ó˜œÇœbˆ[Âc”±#ö[^FØ¥DóL574‘«ðèÖוìvŽx$ +ŽÆåÌéinŠ3”b¥+PÍD/äB棨TÕó“’5¡(~¿? 9Vÿž¤Ô/aJ¡Èç²Ê!‘©ÔÂmÄã2s17f¼íGu¾vs’_"r%$'êwNrfqm1ÄMª´ Ù):¨·oŽ¥â…Þ» Fµ ø@LÊûTƒŸ"Øæ"ós2‘ºW7Ô*À*Ð2¦š%Éÿׯ„Iº+&ñõ›Ó[cµÔcä†õq¿ŽÜþgD‘ñÈÈ…~‰j?Sò5aö†M”ëVî »˜N=£¦¢xóXö¸­m~F¼ßhêƒ(côÏ K¸d”~0ÕwÌG>ÿÙíæxˆ:ñ–9‚+åW¤@+Î$C,!CÃU~„,.BÔø{:-sý‘'ÖžXCÏ÷öÍy*5$»^N'+ë'+ã‰Õ'»×±×9p›$™×i‚sì+wÎÙrÐ=?ûä>ýsd9vĘΫïÌ€ýt p—¾âfÊ),¨±æ~­7„U¢ÊƒRP ‚AÛ=šaD€Í0"¥ÁÁÃ-?rŽe )#f RL˜à Wø˾”rÑ÷‚#px'%G9Ñ|@;ù/Çñ¡·¦<® œÈGDÖ´eˆÄf[ßžç9_Ô¾2`:4ú¨‹è¸É–à›ˆuòbµ×W.û§QHE)HˆyMSb”Ù]íá@;û* à gùMX¹î«£\p¡ N¡µûæ€íMäÀH UÛÀwõêi:¯jG4Ráª/z4’q”±U1¸±¾Î›ÕáÖmÜîÞ©ÀuÕá=/Ù$WV8Oõ÷Ç„Ó§÷æC@óI®vKõ…Þ|Òé5¹;á&K^?èÖu‘èü`ey€VXªw°‹2Øø&eÅjßa®W‚à×(ðd`ÞIÎdµ$ý`;öÂèíöˆëþ!¨<—¯_Ãwgk ßÅò¯¿²ƒÅf“H +ÂøÏI1×A‰ƒ[ÀÅœ± ­"¶Á¯u9||ËtÄYe)е…Oÿr Ø,ÑN±uÿ7Â3÷HÔ¯x[jBæìJ¢üDynôÞïýèÈŒ¢ò-;þEGõ#â»ò#Šú¦Ô–GÖ šžIǾt‰‰&r¾X0Ø̸Š¬b 9Áø{Ë°8§¥pÃÜݧÏJŸ³©èb‹šÄsamÍ/EJòC×¹MPÛÔˆrßñe°?þÛ¹`AT‡g +´"œìƒ*¢58Û×±œte +gM¤(úõWúDªy«Å¤‹2û£çØ»YаÂ6 Ÿ(+þ`>S{²†©¥eJ¶´HÙyhlUAÅŒ«Î™Ó1Þˆ–+°FØt¸¯cC*ž¡Bðƒ:)íÔI…’Á‹$+æ~ÌX"X Qº¥¹FeŸˆ aPûkˆusu69ˆ—Ìœ§ÙÎVc™óÀmð‰Ì+Sr¹ ­p%`ßàuÐÙx>gãi.KŽ}Ð…¯XCxÚ³‚¦Ú…–}ºž×i³á4ì¥w5×cïîíHзí9ˆ¤Û>xÛÃþ‹®Ú³.¨ ‰๧/F8IPü·Ž<ÝlÙ»#M”Ed× +À qF¢&«t"x7´Ïhj÷nË PÃ.=([ž) hºaÿ¼%à ´à´„“fŠæ–Úþ>¯”}Ptì ÷~½¬Q œ7š øª š¨Únç9c ¥Õwx]ʧQ¢®TÈø|ÑkU‹E€xA\Ïy)¡ýŽÜççñ.ub¡vy¿pÂ_ƒô»­lÓöîö鎠à9 îÀ#jPˆ]зkè呵:ýPD@#Bâ‚6}ü›ø¬ÓÿÃá{8’+U(›ö¯l1ºtb\(Ò¿®O–¨}ÀÉ鉊݄{*iDÙFÜQÍwOo•=Ÿ45ˆ@ñ‚(ê‰è龂0‡ [# ìW…zÁzx¾®ÔmÉòö[9÷Süs·8ø¡ëôf-´?†Žžî¼…a/*{×a68åíšJ°C¢ ‘ƒB5ru¼¤ÇÓ£„&¦å¶Š†:^Àûáa²×®”& pþ\ºŸë4þÞƒõö k¬ˆ"$”Ò‰XÖ‰úlÌŽŸï0­í;(ïfÓ¨ŠÔŽ*b^ï3vª¶;¹Ò,žçžÂqo¹LÞ5°}@®‚Š Ö²@H½oÀ,t1³Áþ4U‘jhpŸ-›Ø}šœ9XϤ3ýÍ{_r]ïÐ91ø¢0¾Ÿ~äd’¯<®¼–ým!­'3«ãfHØÝ/ÿžâüã¯3”oâ³d¿ÀgYÆóõÏp„wšÇ\ø<ÇúÛ¹¥0g™U´õÀ¢6 ¼`76„K‡H ûÁà×8¿*dÙ’žî`•ÿÁ+c9•?ëëBð@™TaóõT°éãÏúP,la‚z–y ç!ÌÊ0MGŠ­sý È´ðKÚöy¤ßÄRÙ÷Ù±}³œÔjÁŸ‚vÁ‹ðúˆb£hÎ:ýèç†ç¹áÓBD܇¢}ú=5îô‘vÜòMÄÎÌDtƒÌãã<`£„c£lA„†æ*-1ÄÙ™C›ã`^°)Ô‰‹ú÷ƒ]c¥»vO÷´¤C=}žÒ8¨zJã³Ñî€ÍÔyQQÈ·ô º‡*ÌE}±$ôƒpß[aCòZ±q>ööD-?‚>”õ4êrO°S’“®J´]ß&ãCS9^$)Ý "0ÅYpËz&6g´¹ÿ¼Rˤ.QÉäÞ¤B¤ô<œ–ˆAcŽý‰(÷…ö]I„{jîÅ…Š=ö¸|]\H?„hÏ»\dûysÚGÚÖ÷YËlÁ¬8ß‚»§-æ_#ž° A"”úÕàý“(ú |ãôA.9==+„L¬CUáå>Iï¨?ÃíøˆøÃIšîņaï… +e­ó çLañ)™X¦¥" ´uHûˆ@ؽ}¡þäDa¸¨™ +ÛRP‘œ­Å(þ{Ó~ʇ²gwÒ¿Æß}F|M*ÐV€fzãçAéA¶ŽR5÷)UY!¿™¥¦KÚßHYçq·<îþvÝünùmœ°¯…é»;¨ÖÀŽ³+ˆQŒèŽy.dvƒèe$ vI^w¢¯ eÚ>M8Š¼T‰ºÊ¹aæ"Pö· +|‘î)×MûqÀ9Ÿ O÷{+D<_H«þ®C´7 4JaD1AíG×O†»7éõêy%9MŠÀ|(¿F0žóT`è¼µ(”½h¤ãsEѦµšÝliT,n.²Ë ŸÅXŸ!û% îe’Ù»ù"dø׳À0}ت ÂQÃÀfꆌú}m>‡T‡ôn˜K;½=aa(ƒ%ìà¸gËÌ÷ž“„믰²AKwÄjYcy#¡H_™‡¹ÄŸ3â~»×³eÍh|D© í8*!lܘ2Çv·Àä$!üÓÝ>#„jÎ`´!ö0©ŸQU ^1n7;í`z¹ôÔLø»DÙãN;pçJQæ¹ kmƒÁßÿ<À@ï „#›NW™®Ác¿‡ô3å _·ÇÉcƒ[D$üó $:3*˜àN"í”  =L¢p¼öÌs„rh mà–ÐÕvÔ¨#Õ+ˆtÚÜ?"ÁÁmY–½s/ü5»É2rK{áºÞZªÕ !PõN>¡¬%½íoð•ðW?`t—Õ,r"Qñ|1{mJ¾9u—*úâÞsÍ[üÇìTØïvA9Kßèψ<«–Lñ™y€ßœgòŸ4šâý±ZsDb‡ +Ê!zvS³_Qô~(±Ñ± 3Ì¡ú»s;p4¨£’]:Ã,EØ»æX¤ö†œ/ú™éÁí¹`Ïgͬád»§ñn >¢°*#WAªÌ4 "Yv5ª­*ù•h§Zô¨ #‚ëéwïV FuR +»ßœgé¤k—Š“ÅäÓgÄÇòœäö×(´v$9`üT“$6‹Û´:œ¸Êaìüw–TR²u¸*0T'ܧ¼´…´ e;ØtA©±—]1ÐB¤šŒy©‰™¼<–æ®ï¥Å:ZgCz›6´üs5/®}àªI÷Å1 +µ“\Jd*" GA -ðp÷J!»Ÿw•=^þÎâ'r +Úw‘,·˜€¤% œ'% }Zí7±wúÏ飴”4tUá»øõo¯%©F— L#¡§Š®íígÐzr(9´··õEj°p½hI“»ï¤;C×@qk²;E~@€Vhí½þyÊÚ¥ï¤z¯~Ó‚x¦!°×£Ií¸Ã.›Ç)ëüÙQ}¼ðròþþï +J÷ÿ#î»\ô?!ÓÈñ;a3J£¡Ï–là|´ÿùD BOµH4iú¡-<ÍÍ9OUå w1"LânÑË +tÙâ5ÞR ºhŸyñ™êBFÇCKu ÚÆèÏPƒ¤f€Ê› öùâ–ýƒá—Üëa¥!²„nnÕ©öEa¾%†Æ™ANgXÑU.wÊ”óm.±a¥Å£TÊÌenrhöó\ÍßliÿèBa‰ê¡‰’WèÓÈy0á"¢Zwúúx Ó/æÈS¸€¹ÏDà+IúÞ¡¶1R齞4§:>HžôUàÅ«¸Ä,üLVÑêÒÐéjü~1ìíØðì¿?k?ØÛÉÄÅžK[Ø=ˆ¨ºß’µvJhŸy§¬P­;¥4(#,h¨½¸Gšä°üÓiÄ䜥a §RñP €ç:p£ýùWñd8Å7û¯å@¨U‰‘ػԋÉÚC›#J¶Ä`Rêyî¶A•:Hv‰$ó€»<`Øì –Ý‘€5Ö˜og§á Å鉌ÚΔí,//Hqê9Ó?¿Ðñ!BÐYnÐL{7­‡›]Ùe<Í+ΩKÓÃ+k4;Qªµ„ï§á ejQM7 ý)lQ.™É»]§Q.gz([Ûv6º^¢lbY¢2²GÓw.š×Á>]ee¨˜r¾2#`„);½7®H×À™¢Ñnõ²QÒÅ–Å~sè ÄÐi£dö»&ŠjØ8¿ó;ᳯÅU Ö܈­›hÇýƒÙùÊ3t”¹ûE@ ˆ‡¤YÜwbêòVVDöԉŽ2º£vë×A¢6@Ù +3Þ=ç+-¹7Bú!q¡©ë¼õ]þ‹ã¥1ÉÖH ƒq/®ŠàîYYÙ®¬·6÷O¿s'ˆÑxŠÁ8Aë:å¤/Œj[•È+´¨sû@#•Ò »_ +GŽÅc¼çOÚ[kP‚¼}‡ýö[;è>i•C}Žw¬ éùJo¿Ó•BÁ½ÏÂGó’÷ˆ/ßè]ò'C·áý ñym;—X'Ÿ…òàWž9w^YÔ¨U€³êôhBŒ½s¸4®:o ÿriŸU‚Žñósà Z[?›?Φ‚¡Óãü‰I·R;ÞúSÔ™L %v ãh ï<\duÏÔ^[èA,}(ºúmˉµ¥”Ùõžâ+ëEue?tZ]™h3u O*C’¨Aí ,æa*øÏs…!­ˆÉô†º:²Ó—Ó“E_•þq"j×3|ãOºdC´PÉuüûóõŒ¸Ÿ¤%Ž1çŸvfô}7̈8ZË`AgwoÀ“Ðï°A"¾¢„ëTåÄþÒÞÃ? Zù„%̓!gãÿÂè\ÒÎ/qû3àl™“Oî¼e¾¹ßÍöÃô͹è ê„…÷ó”ˆØeÞ{³‘’› ì¹óÇz¥ Ù”½xr̸„“Xݯtw’¢v’"cRžjòu“V±´’VÙ3á,*% u…Vûç¸7üVñ ÄóÉ_å«k1cÁåìkˆ/ +ý¤`ölgó?2µ7·atZˆ]”²!v= Çvh$ƒãËÁ€Vl—î¤2†$ ˜}JÌߨò•À¨‹`šÿ¯‘ˆ&”ðBY2i²Sl^_ël£\*ÞevŸ^$r¿9K`‘‚•¡Ö JrZrU³-À$y®­×:f :ÏÊI)èPžä.«›¡¨S„©hóùÜqÕ’’|MÂÿ#‡t°?„í¦ûûhî‘öµìÕh™wŸ³òmdb«TZ28ƒë ‰jI„$qb h_ +r»ç“ Ôl;:7ï4AÔ6êÖK«jD¬Ñó£ùpeqÁT5‚¦z+hÓ +íòB…&вSïÅ“lD•ªÇÝÅïãèZyvvž\Ÿ]šÓí­þp¶¿ÝZñÞ‡_~ýÑ?‹žDÚÚM ;˜ÖºoDØû$öËøýoø1 +1z¾{Û²ßK9Øù¥Ùô;¬ú` ¡JË¢"ÙYÓRãö1›zÀžGÎBBÕ0Ü=ÒÔ‡zjSúU-5ÊÒ»¦·fÎó°¬蔈«\A_)¡À­X鉿!è¤Í¿øÕAÌLŒ–e +QQœJ,¶TÀ'Ql +vÁžRš·@ŸŒŒúÇ!õ¿hÛ…]í4 +‰{64“B9ò¶ÌlãAʇ-'‘pÚÕöLo‹o­ÛšÀ‡'y+…Å÷‘í/rÍD1…$Š¶ïàî]4¤—Æšˆ¶ØóãÔÿÔzx +$V°c5ÈÊüy!Lâ.‘¹ÒšD•rí¹Ë$ÁOûÜ*‡ü!Žè‰x\ Û«2wwo?Hoé=g÷… йmZ~Sä›]–§±©¹OÓ F·<Á”›¤z}õ®îq*7dá—h9~ +U Ž/µg²÷éO1‡ÝO”õÿýd§†¥oÃýÊ×Ñ +¿+¿0š-áú6¤Biç'Â&ýE©x¬xØ×qD ÙÕ8bñ·wG»LL‡Ü…±Ù“©;ƒø2ɪkÉl?çó…ã½¥ï¿âd½ÉÛ @ß}¶“ôùåTàNÚîDŒö|]i +:Büœ }æ[6 +ˆ'ÊV (5z¯PK¼ã:C˜8*J7Ö‰À5ø}ý©½#_ò KHI‚ìÜ`œò™_#³ÿþ*ØGóqòKÄýä®ù)} €úÌa?Úópmzb]VɨåºnRd¯K[º)>WÉ,SuT’¥@Ãœa‹ö.ÃÞÖ¾W|ÃBo%ií ¼Êj öè£[™±ä:¥Pº…ó:òbì¼^x%]~HÅx3ª…´i=IÂa"û)±Á_Üõ~zÞæ b + øS_ôéQ '‰#–Û&g 3j +&vSÑ@Axå8‹ÒΦƒ8† +RI@M#±³<”#?‰!/äÛ£~ÙšÏd¤õZ.E©AÙ·ˆ +˜¥Tú +á©Iƒ=BÏîëüY-ÁX”¬*ø ÀîÙÉÍý`¨Ñ`âTˆ7Rp:Uâ¨ïÅv¤2^®£æ\Hß'@z¼Ìº>£x [Hý>ì|EÔ­H„»c¬ ZÊSfgT¸ØvÿáŸTKˆzjª¬ADUf¡\)õ¯½ 0°öÜþ”|ËÖs aÑå:“¯Àö®ÝoqÉvl¹Ú„óg´AžÌŸBx÷Èf‘O˃Ž"P+¶û9ý«–Ô >X"áÿq¿“ÁfßGÌ*¦ëI„%C©¦1ÃþkH6iÍ>³½ë ‰…àr8Š,à=ŽaôzÙF]‡Ðæg©¹ +8ˆrKò¸Æ!ÁW7½`l÷Àó„«Åð Oç—YÂå«ì"øÎÚŠ25æê8ùð¸TÁêˆ +Ý4íYÇ‘u Gu_‰‡jÄ¿Æ‘†Ê—#pß&Z4´ú¹ì“7xÙ[,Øà_›_kÍøÕ;ùµÑÿ{<>êý9™Ï?ÊG57í²2ÐcBÝèr³_ð˜k¤Ùò%dõ°;»‚8wÄ=Hö˜½Cs¼OÕŸî•Új«nWÁÐÀæ!í+/³®¨¡IœÅ™UyWÉCÓ¿&Ma!^L;Õ¨ðiS» OHÝ-è§ÝÆ1\÷ [>Š0U||{‘?[G&2 ‚À¹döW¤ãs€ DÔh„Ô#T!Œ§~]IÄ»>‘c¾âÆ/W%=2(²æþ´ûü´Îë^\hÜ-¹Ð•›}ÞÍ;Ù@oÃåΠl‘Ÿ†ÚA¡OuwŽ+ŠLLP]ÒÔ~uIh=óÕë.Vœ5÷~Ž48¼UŠUá0Eî»é=žvòŸÐå@25×›0k@¹×Ðö¥A9ÒdU¤WQ’|#ÉZËP„mÿ°†Þ öÐ’ßî}iÔ̤æjçS¹ƒÃQE™<³¿k!Q2ï(ÎmT— Wk›'Åßy•êŽ³¡ƒ²›—‚÷)¢áëh»O¶‚°Ô©:&«]'«UTí>}‘Šu÷<4ÙmF”'чÂïô9”Õ»j}§–Ge'Ï]‡Ïe_g\GϲŒd`ìáH,Dk[xü,>0¨= mF÷,Gª„‚כƷWœ ÃÝQ@U5óSì®"²–ÍLs (Y¤ØƒCÆ2îÖqAf¸5ǹ@·.-Ī VW %i•¾P‡€Åo°Q›Ä¡GbK18ŒRrEd‹åäÊŽÿ÷{hzTX§WüÚA²:å çêŸE‘ÈWj ¸q>þFRëû¤Ö‡-ŠßiX¨×»ñ›Š¶Q¡iQŒjá4.\zP«cvC4ŒÐ´“Yù†âR­ J +„/Ç1©¡\̙ӈ°§/'Y… `g5å©pðúKÃYz(ÃB™¬œª "ûö’}I¶j +ÂQsÝýOò½ÌÔ3máº$ÑÇÊ\O)T$šÂשµP³Kø`ÜDÁ´¿BðXF.(!ŒÒòýzcÐñ\èï`’¦»¾I7ÑIE†ïx$¹Z죎‡†šîh]©–ä§îWQ)é§! +îFsïÈ6~ÅòG•âDŒD´ë€ÌAÄו$«ìß.t• –ÎWíç•È¿dŒÈ6#-àØQd¸ €«FÞ:ŠÞì¤ú)ô„&›€‘›Å í½Î¨þ™b Lvå,Eæ3š|5صìŽVl0hSãGk„Ý¢¥KI$ö M¿ø÷³]Š ”p€¸•$?e÷q°ï°ÿWº/V–tv‚ +¨ÁØxº=MD!¨d¡IòEYºåëV†à0*ô }%O£R&§ÓC‹\BŽ•Àž$é:¿ÎkŽ·àOÕüU ^^[ª”ºÐÕ.ÌÎ%%qÀÈð“=åqNo?ÓðW±»kÖ¬¶3vÑÇ©‹¯SôN[‚ü+œör+ŽþeýÆÂÂW îuÝÚœEK¾„Þ–é•Ž¿ÎfÕT4c¹keüQ¤z«å)ó‰F­ÐfYöÈIÊé@³A‚]¥Ãï±ÎšäÉê)è™Ug‹ +Ý®Þ^±÷$}ÐÍ#üeÊ®4x5,îˆÖøÄvj¿ >20…³¨ê’’>Êz†âWJŠß+$Yz$äÇIÇ¥ø1Ñ/0u-Ba‚OZ ÊìrÇ(@ÆýŠZŸã~ï¥nk.Ðì‚{°(÷ÊšÛü+7³øÜö—Ðç?+¬0ØÏŸë°“Tì`w.Ú ‚ +o ¾{m ãh«"Ï`]‰úÛõÕyWè6 º¹c‘'ûUHè¥ÙÕBûÊ<‰R—†(½Ð¥©Âçñ¹=º‰ö-b^ ¦l$³ô…ßQSŸØÖ&/düï•®(õ—Xòˆ¾ +o`/#§‰'ê y6£Ü‹Pqä/Z(Oå˜1ýžÿ +n²úÊÉßsg‹ÎréÙŸ¾ú×£Ÿ.ó­ë¡u1ÙM OµÄ83 'ú<Õ”eØ™ìã”A´¯”#:fqAì!P{IÓ\g®jw ûlXÔµœu¿E Q“Ìy-ÚyRSäç;È•B%”#NF ÞŠß6Ê'(dPQ´*2¢Œœ_ô6ÕŸZRSw@X¿f +±Œü(e VpÔà¥%³ç¥ä ÙHÇ<ÖR’á\ˆÍ‘UN`;ß'› æ’ÕÃa"â€í`z¤¬w¡üÍ4*öTs¬ád³„»ô|Ú·Ü‚¾ïœ9cüBfF ž¡q÷R秇 ›µý¡1"/û§ AOYwªgÒvÞ,†‹#ÒDKÀ˜ U¸ýÒæ–½÷ïÏy€=Þv‚ôu!•´¿ÝuØá#ìp³S8ôV'ž1åðÇ“ŒE¼„ˆHŽ>ª''¦×>G9Bž ç×u!‘6%:]+yÖñn(†”’¹56Zç …|\Y¼—UøØK(ÑÒ Ï•ŽÍ]Ì¿(+!œdíF¯HYaty$)´Ö³ÌÉ?edíaðwÿ&\¸;'À“‡‡°ää™ØH¥å¯U>Y“è!GN*ßpx²Öë€m`/¹ñGwˆ~´J£Å„_¨ÇÏ&k ESqdß·omáÞ©ÆïzÞŠîS-cßQé]ù ”  zGýÂiŸÂƒ UÇÍõ>)¥[&4(×—´üŠå ¨=”õ˜¾U™TÙT§;Þ+¥2Jë“{ñÀêÈD/;8Œ¦Xã¿o’Æžê2Ä0Cû´dûãåo?Rë#+‰ y°{·e÷X'mŽ=&ÊÞSÁ<Æ–Êo\G~ã(jÄĪ€ z(!þΖVZ¢MMúfkæÄ ¬%¼[Ànl®@+ë¡îûµ“Ùs¨ÙñölfZLMö—ß°•}´ý¤­a®¦p_§ðoZ‡Öiq·±‡ŠëœÊä€ä÷Z¯<K Øþý©:8|+êiSe»ë,ûÂ÷xôô;“=õu‘Ttu?ÐÑŽsÊqÞ‹“òï‚nýèyì\è/zµ‡~9=X(¶H=O_§M¶ç—G¦ü_‚éý-Aÿî_±®ÿ«Oôa@Ÿÿÿó¼´¾û‘¡Á ·î"CW­‚¬>§¤Q,·þ#gÏùxÓ3’ˆ¶_Öz0G¶€ÆÙÁ° Wúf•óp_Ì[(ó• xZg20zìÞvDüvVsòÒ8¨?~±‚wTµº|«|i0Q·N5D¯ÞÜAà;CTB¼‰ ;ã9jΡ¬•ƒòrð¹z ;ªØ'¥8î$ŠŽõ¬¾°,òQŸi)*ƒm0BÂÂŽ ^”2q"®wÑ-ÇæqD´ÄEˆ¹JtÏý§%P”ÙŸÌ°cɉ±'#âã¦þ+v§Çù6YÜȦq41‘šñÇR/€:FåÄœ¨p©öéU…r„B$x™ª +ÎÒ¾SS¹¶åJÑ}c}Õ¼˜ÊœÄX&àò:B™´äÙÔ¸ŽæàŠmw­›`$ºf1 "pôCeü’ë÷“UFº2Â÷ +ÓÁzÜRz&"Ìa‘*¨:Å# ¯/Kõ@ÌŒRmâÁÈòSöYeª!"O¹’Wá3ÝývîEe;³MDzù]{éùímG_PpƒS̼¶—7ºôÈy¸2Q¬ +ÐS\»E€AØž @2ˆúá•ŽNÈB»¹<ßœÆú'y7\’@LbéUï"×õ×€ #¬HÃÂð½Ì¯Aݵž60H¦}––M¿_{Exû•«Š¯ê>÷瀧 š¨ÓTfRxÿJ;TSÚ BE¦µYC¾e¤ÏãRÙ)¯ˆŸ÷“ã ýMºešA‘X‡håÏÙOÀäaÖ ¨Îݜ٦u…¦„1ÂEï42ãÍMÕ¬Ìs½€6ÞëöŽf–k®aî¥ßÅóð;DX¿ëÝâ™ù€ú6ó:C”~ðžHÚ-Ää9Ð1hžæÔ ÷§ ˜k(õŸÚ£ÈgzɯQÈÚNŒ€Æ×ì\D““úâU^ƒÜá ŒŒaŠÑ§,+Y8##ê‰8 ¯ìùæà€ ­!2v°C·¤Ÿþ›ãߌÕϨt¬×`+ö+iÚð ºJŒƒŸ¡@q×x/R;„#v¿½B'd÷5‹܈=¼æ+ð¿”j:u5PË]¨¯20Jò/ ŠxÛz iSDuÎêMD¿"£E{"}ºnsOŠ½ïǤÒíy÷œ ­"j˜U5K¯ˆG%+™¼Þ¼ša€^Ž¥=agñ_G?÷\ý¯’Ö£èC>*i<2Œk¨›;­Z ã* ÖRâ¢ü|F¤l¢þ石1´dÕé„»™r8õ7œ×Ñ @vâàF$ ‘Ç´WŸE¯w ÿ€êº„?jÝðJï ó²N&¥ç ”Êp)û³ig•(óüu{‘Và™-tj„c¶&´~iký°JÙnd·ÒDS {ž;I!Ë&9—U·“ó49JŠÝl$(3àÈâ2·3˜ë./¡ÄQˆ*°^¨ø¸ }R†%#DôGNµ^0¤ïZ18®ÑÃÞÙ<È•®ðÚËÝ ¤=âÓeJm(JÕ0™=¨¦ @° ©ÑŽ˜í¬£á2݇ˤ1̲¿ j!d§;&é*ã8 +i`¿os/AeÅl|RÀ=&ˆú¤UÜߟ/ÃVKë|GÅê·†ˆÁYóø{h6ܵƬí%ê%«oßOÎùü–M%«öųpÿ ôøOÑ%ꪩ÷ºúrš¬LÏë“6"<[C]’/ +´–ˆ Cpǵ%ZGŽü©-” qE·)R"…åÚÍûÏ%„¬ª2»ö–{ #¥À6~%ñ));_É9ÀKzšÓnÝ™ªï¦5–çÇ_þd~ÿ~¦RPÔ½õ§—…É͸žt0›SO¸­V(6î6õ.?›lÔ}ðI5ø¤jûå啺kÚõBœînaðEýÖsÁ$Ì  k¡ÚÏ–í»¥Þ8Žú#@ç^",“ ÍÿÕ_-Ægò„_ïdH KbõDbÆqj¿ Œ¨0•(TJpÝAÒÚ>"²‰zÒ%¡²~F)?¥’/z¦þ‘jQʉÌF¥Hé™nI‹Úí‹! +y‹Á§Ü³gŽ‘ÄŒ4çŠq¤î‰p™¶4—‘òáËŸQ$Rõ”ˆöÇ~inÖ«¬óêPÛEPçÊiÜr¡$RNSÓ+KûŽoÍþ™¹Pp=ª½ò%wf&Úd*_ݯ"‚¼ã(ê(ÕER,¯ºlÞÞL¨RT_'Éå·o#•”ö×õsaµ ;°&kQž r«x*™¨3诿5cZfs_;àæ›zÇ”O)âרjŸò—p¹tŒ¾¥z¦PŸ »c¥¤tbXkægÄN£$¢5{¯4ÅÇyVsðKKî%šf#ülŒ€"ÃÊ_Hc¿Õó(8ßööëG ¬¾]+]fJ ’´gÙãë63R^V´hKèºQ ?ø¯Âº:‰`tò@êŽ^g¯É°ÒgÛí(Ìj²4± T…í”Á*¤?AݾöÌüºR$¸¨JÏCïFuŸ+Åž‘"òŒ@*íz~l°rˆº+g´Bô‚mÞצ»èøÌw’|žÿ»XÕž(l¦§¸sòS}UÒ˜gÿh÷kÄ!©.kÈ_}QוÉ_Ò­—ï$´¬ƒÉ£ˆOL/éÿåß3³½–Úû}J¤ûøkð´0>cnž0žoÌÅa `î—Vàg›ëž4;UÃf¦fOuþÿ¬½]«5Íužû öx} tU×G÷a"B08$ä$É‘0²†È2ŽbÈ¿ßu]wõ\ï3ç’µ ²A<ïê1û»«Fq€äÝ® 1=Û¢ ¨@GÐp-Ìëö4‹ö`ßÚƒW¸ÿyÔÑ ˆÁ(E©#Êßï/ò-©¤,Àýæ%ÝO짟ÜéÞä+4äËÆ{e¢ÐX±/wn iå,O¿Ù” )g–GdÎ¥´aê@¹2ÃËÞ#ÏY,ýÆ©¶v¨è$AaGt÷¶ñz'R- +ÕqÛJÀ¯‡ð+˜ u»@«Á½¡öH¯KÁ$âœñwˆh}±sý—ɽŒ E]ù9* \ €7»UzrÝ6˜ ù…NçŒSEÛÈåC«¶þ°)¼ŠÝÏ­°ØiÎËzN(32 tpî ÿŸˆŠCüs)Ñ_rÄR.5¤èÓÎá=Ù[Ó&.z›³O©V ð8&Ú£>Ò.²²·?îÜ6·p‹ éÊã8}Å)iO):û¾ÒnÙö…ú_&³íØõâÓ©®b_çb…6 GA¦'Ö¢õJË##Ú¹Eçjöîë@$k.®–lðÞÊkGúSVpèBÍ;ߦçH¿TäzwØó‹zS¹5Ø´ÜPC7”υ˨˜Ø½3ü&‹K€®ök–µškáÔZOƒú`vŠncéSÀðò“؆bH€´»i¡åE6[8v¶ÐÓŠc:Rµ«ŽLâ¼gʇûZeU”¡‘0y§4-½©RCÛ+é–)–Ç·›~%´WÖ‘aî Ú™f|q3d/ÉûxZû…ë¾gwZ0ÁŒƒo×WÒ< cͦ³Ù\ïÆø&bìlÄük—Ï5wÔý°\39FÜ®S…¡¦5™q¶¿kîpwSÆýgˆê_׋?ÉöOd}ZWèA‚òÌÂ:ãÒ¥ûN•©À€÷åÀˆ[ØÍZñŽ;WpµëŠõÎDjD½ü3$öé5¥1C.—±‹CªÈ{„§ÿaTø¥fgÌA©‚q/Zk#`,æ1!^SµGÝá}#øè0¸wˆÇ&·(·0 ÀfÍH²D¯œ~öU:s=m/"îò¥äc^†JNÛ»¡?nЀkÛðõ¢›ÆE;†C«¦1µ^¶_ºåý¤™ÈÜf"ϣЖƒWoÆX¨2†¬[ÀºIoâîã½`QÙ‡¤jw (WÔ£HŒ"8á:˜ïxCŠÈCi14ƒp‘¥X&Q·ø¾úÏ]éê&õ¶Á´î ùï"î€uÏ<†uQ’(ÎM¢è!>ªŠUÌ9úÎP@ ÖèTÜÊRœØ´ÏˆÜ4ÊMùµSâÿç~Žth8Ÿòí8ÏÇÅ0üM@Ýxéc<õ”÷Û„¼¬]9Y ØA÷Ë›»¢³&Ãb7­?eõVèØùñ|ŸzÃ!¯ xÆÇ~ÚˆÜ-Ê–õÌlÌöï"X?¹´nvûª¬Þ¡v·Èº»n ²•\å)œ÷ΩᲠ@ ¿%ÜÕc¢ŠÎõmS훈ܼDÒM¾Yïoöóh~âêÜžt«I·ÆÖµ eeAW(súƒàu=sð=³^ºãkÉO²64ÖbPC»Ø~+¼÷‘ ÕÇ–tÒ"וK¢\hsÔK@ý‚^¶œ•â‰°Z,èÑ„¶L-‘¡Jk=¯—gÝ”çv£­®åO}4s|Pªï¶sÆ( UÅÎ xäÕ8¡µ1ÛõÚd´Ð¡Þ†,Ô×YpoìJ}W<°¢ZHEwttYóDjžHßÊ‚)„Ð>Þ½[.4 ›)[KÛ…©c]?HM[©¦}ìœNš/”œI_JkßDÔº‘„×ñØ&«D]yei¿»Tlд*xóàA`‚Ï÷[éyhÅ\²áw”º´ëπˑ/‚3sÃ$÷%­Yn g-žÚ`7¢¢éÝž½~½‡`ׄøÆõ‡“šê ㈡@j­Ó”Õl6çVNDA>#~ûtñô×ì ÉÈoö3úCÜ]ß^É"a¤lÉÿøvn|Bg"Ùz»›äöQc U¤ç5ïSŒãœ,’¶éÅV†Fù…Ã|‹Ãü¬q¡UK›y¥>{l¯äØK†\¢Ýëóï7é£ËÏšÖä4o”‡ZQ¶~ê2m{Ù£ä‰"c½ß¡ú©è#âŸwýÈqû}#“øa*Gð@Œàz³bÃé}C¾˜÷¤ìãçøÒláû)«€#úE¨•}Fø{žâsùrÏ8Ñ\÷îóì©¢2iQ%LÒ =7\b0×êòÙ/V-–è|FyE¦Î)™e 2é +OÅQ¸àÌwMRúŸ V7kÇP•kù/‡Õ ¡ÒÆ]¢Mû¦ÆýQ{l‡nY£HÖoD."±©µ+%í¦dÖ½EwO•)ÀÆË|<°›ª£„Â1iµ¾Þ‚ýÞ^Û seÀkDØ–)¨Pû€rD‰ˆv'з'‰$wîeW;PáWõ±}íÿz4NÉ×YÔݸ ¤©ÜGÀ»ÐíP¾AÛÄÉ3jª]K_‹¯ïB=扦³shU—ƉæB ’î—*Ö[ˆÎü&b-‚³ß÷P™€¦8æ´Â’¢¿J\§V'H౦?{ÝëK§éóäÜeá* +ý3Jaj„û؈ì*,~³¤9ÈUëžà-æMT›nþ¡iÕ°#d€ 8„Y¶yŠ:Ìψ×"{—ºÒ…h³“ub¯Â½«ÉEµ~Ö¡UTA÷üŒÈb!ÂŽ:‰œ÷ýÍ~à…ÞŠXTµíZG"VX ¢…;ÁSÖOÍÈEÞ=%¾ŽÓò§3˜Û9¤cí'†o¾.óÚŒ †Œæ%„ǚȰ1r“ý8\®mAÏ~BÛÆšüŒî§:04³4ÂædÛQQÑ£ \GÛtþCØÓ ÙvGDš¾®WàåËòK Ðë›ý´-‹´–tEázQ½—•IK웈‡Îz⹸ŸÊGèþ¡pý°$Ò¶ääiÅ¥gÌ{¡• å¸$ýÒ m¨j±ÅxüHfÐל¨QìÛQÂîºÎ d'g§6©xh:ø#ÇÑΊ¦{éI¡•¡KQ^ÇÑAŽ',[CëüZ+?§#‚!o Iúþ)µ ³MÙáŽzùs ½Å—¢ 4ÂóÕ +¢Q +ž!Ñ9:ÎçžF wfª˜}F®R‹t½ëçs¤÷(^3£Ö;q~} âÃþBDŒcÐp2`v;ó3"Gª"‹µæV~Ûϱˆ}å` +üB¢pu8O (e“N;jäÅû’Üðf}/J`jDZ=¬¨zí¨5FýÍ>Ÿ;<1Z&‚y¦Ü‚¾‡užÂN‚‡Ô Vλ\ôsúh4Ê”ÚÆ/ÊE?E5ô,ÖnŠ2-—Uñ g™¡hÕ*¨ÎÌÂ×̬±ÎùRa¼Âôc¤%ÂÊ8Y°Âw~)¡\Ò²‹œUßYJ#•ÏY6µä÷¨5nÅŠaÍ›"<û›½+n£vŠà¨t%,¬{›u;çv1èÛÅàq›o[‹ÞÞ)£îIæœØ£Ç¢6ðC9í!kµû5jè÷#|¡Í7Aåp×7:Ç Ôa¸Nþ*w|Ûnü,@; åó†" +ÝZÖ^,5Ž†ÀåU6~ÝÊdÁ7š]ÒÝ}€P¶lóè`Rž%Ýh*O(‰ýÖ½n}k!L½E<(¦`7 á±ªy‚*¢—v%5æEZãΡôõÞ€aÐWëÃÝP×F&|æê €¡ sÜYºC@Ä]dôª1GYo +gtnG»[ygúu¾_o¬œžäÔî¢ÍÙ¤ðqÝ1›èM Åý¸õ5[¶xüË0»¿$¨OÎýŸäÝÿ+wvàvßI¬÷·þ1~üÕ¿ùñßÿÛŸ>Xùñoÿýþð¿~üÕ¿ûëÿò·üãïþé~ó×ÿû7ÿéoÿþ~óë?üãÿýÍþçoþÃßýýÿã?ýáÿücúý/þëïþñwûÇßýÝoÖ!~:¹ûuòÿ™ÿÅp5ÐÎœÌZ1d³T‹ïÀÔ»› å,®\‰ë]_&u™YUq³y£ª½^)$?D æߧÛ~d}¬Æ«ºó×·¢ëѸ?!€¹<=‚˜Õ=j­¹~¿£„9RM­÷øŒªê·¡0‹ç˜«#x®:"X„gUs«-“^Y6SnÞ¢°T{€ýGEpDø÷v´¦)–ô+gD¶¦ªC´Ž+í9÷:¡¢Õyè}mn“¾‹ù‹l{»F´ÛˆVŸÊYý%%«'BtéÚG+»ñrïýoi×õ'嵯•ª•µ´bÃeuaMœÊKáwŒ†Ø•8+‚5ùH„¨Œ"³  °ðJCfBÈU)rVËÍZ¾¢%²BÖ-D(äþ”½ªpµ°ñÓ/ûŠf¬ð +‰ú=©J?m\õYÔB”§fó„CWJƒh쯤°«ÛÈñ*ŽüFYéîÁ][] 9…bW€8™WZ½Þð¶/Kƒëi¬KQ¹P…¦sêôé +Tgí½žõߎòþ…’Ê\»`®®îinuäÓ.‹f‰ª”…ÕC†ü]=ê¤@l@ymýòÔoˆ°1:m¦ëµ`„Èücu¯%剞óÄ*‡ˆµºkÁΰN™šVå ˜õVÀ¡niÈØ»¿bÑÊ;k ˆç‚iWň–)´­Û$6Ì? +Ÿ82¬õÛ(ã|þÎ^‹5}¸•UõÀ×o4JXsNĘjQÓ+3ßG`žÅã^öî߃\¢.¡ßY»ACBWõ¢ÞQÖÅ0žEè@°À.¨ãsƒàÚ¬yÿÕ„a]ô>ÏxB0j Ôf N±4Ыø>êþ‚݈/âd¶¯ïZ„™jŸåçŒÑ’ã%íT‘Dñ öºuvQàŠÖöÑ_:QÐäÚ·|ØfJàºÑÝ ™(ë·)~ª5=™îDt8g¥²ö¨wŽÔEÎa‹Û"î6Óˆ@Ög¦r§ó‚Ö ešH˜E UÀ4þŒwCᡯ[}>Ïä#ꊼפ-Ò¹yUäübšú pæU[3 +FˆåjºŠÝäxê.¼Gx$¤ö°ŸXŸÏú›ýhtÁ’ƒR8Ã4®M§þë 3Êo"¾{?‚•®2ãcyw&›ƒË§‚|_àºù¾ë²óØx@^ Œ–Y&l pk´-,õ-n]mA²{„;ÉîA)þ³waóPƃD©±´s“ÀúuÓ°ˆrš²»À²NïÈr‡º ŸÀIJ%ÇÜxéHÞƒÓgzôñ„&B9ö5¾M­‰"L×°Ñ›€ˆ†8Õˆ2é[³ˆŒ'ªîÞxèÛk +p²ˆÜ +¨#ÄȼŽ´Ø×ÜqÛF¹•!?çg„ƒh‰õžèøn?ùkßͪcD¸ø0ÉXŽd,çxÜ8¯kVÖ¿Ý©…'ƒªå|Ú‡Qëmî†Ú§æzŠÜ;yIžþå$ìÉÆ##p–!Ng-â~L5pÒ@šm®pd°IãEµ¶ +˜ea,Y +ÿgÝÂ[¬\áå[¢"p‚œºQ\¼©è÷16ªŠm½"·^B÷9‡™ž@ë +pi03€ïnÎã°„gÄ}±V^‰êudÿJm"÷Q62ü„1Œã‹÷ö /ô|Ø,Ô­Ë­ò²òÿHy‹ý €õãqÚ#¨d»Ç¡äèªÀ4C¸‰e-*ù³œ~“g É}}cЂɤÕÌ*Ð2Cò½å gŽ‚Žï|Ïü¤ˆ­7`"ý¨ÛÝ‚§þ¨¬Á;ç+Þ©¹+ñÌÚ’uÛ¹½€ +ã·RíׯãG7¨äJjcodÐŒR“çö:Œ%ÂvØ6*ÒPu?0Mnì[ÀÈ1»¦{+¥mFœÒˆJq—š­á½ö<º_Èb˜›2g¬‰ªÚ­¢ÅDˆ[ŒÂ"j/kLý&àÞv—L8Oªöuhßê[Ýž0ÊH\t%…¡ÀK—/ +« ÚF®Á¸ˆ®xÈhu†ý{®]èÁÇ~(žXìÁKPè®Â>ßÇo"Цe>%› ðMl<Ú}롯¼ùG¥Ñ Äôløm£í¦wg-µ²È_¥f ïZÃÁ&"ë×{ÿ° sYáæçû5˜¹’ËyÉI +Eÿ»Eñ(áÎp]®­À$«w7ˆn´`$\OIn]rÕTú*åº;=Éôà¨Ë­h½ðY9­ <½MÒï#&é5l¢,]TôèËîj9Ÿ _ëHx²˜v…8‚s±¯Iƒ†ù8dRh,xàÐcÍ%¨ nîÆqÈ•¹ñ9J?·*µV“ºX‡Æßlï÷áGOê2 >Ì,ûív¯Yæ!„‡:~mËŒS@¦×2$/7̘kÄæ­3âPM_ÖpÃ.®Q?…?3µImheKg>>úϺY!¬Xïˆz“a¥u=ÉMTÝÔ +äÇŸO‚žˆFŠº¬·ðÔ[¦N×FrMÉ–PÒï°PŸ¸D-D|sb^$ý=(D.]´s¼ˆZ®[B¡ÐäiƒŸ5ÈeÄáK‰þÝ“®¨›®eJï9ïéŒ(g¥cÛ^"y*€¢ +®òH£e|v1BÄU’ U¡ öäÇÐûI®XNë˜Ú/>”tæ n ^àà‡âÅëþèÖ ’¸§äÖûJÖ ½Ôc›ª‚ºs†usê„ÃwÐHy5m§FyÌTÚu=À°¬«ëÈ8žŒMëA§³%A+æ¡êqnïÚ8:Æð-/°gļ´nçZu¯ 0ýc<Ü1šR“?F/רˆŸó±½‡Sb×Ê„XYö̱XE%‚u7z®¯tíR Úªé×q¶-Ü 5ÚÚ‘Ó쎶Mâm¦«žÊš ‘\€ˆ‹¾¸oǦÿáùœ’ȯ…e殡u//Þa; '3gÏaÏu¾ÄÏÑr:äFb°Õ%]¯Ò*¦W<¼…z@)Ðpü: ™ +1~‹ðHí’J¸ÒxÐç7ûiEá"»õžo%)ØÖd¿àŽInoñœïàD¶šD­óAƒ_YêÝ&°¾"ëHs/ +šiE»™*øb= JMY`ðƒúÙùMDzª4¥‹]ÿóTÿú}?¨&øüQ9›±¾AI$ó õâ3âg4ùózý£Góf¼ibª¬ý4bUGôéÀk_\ÿm?cˆ+Hž}DdQ}k +°îp%sùf?kå-ü0‚þ?£&òZ^”ï"èy•Šúí#½G Ób`0dÐ9íhÅÅ +Æ­Õºúñú\ÛgçgÄó¾ )”[6÷çýô¸T¬–ÿ‘ä°½Á}ð­÷à#YB¬ /`†€} +ȘDŽ¸f(b¡QEŠ/DɆŸˆC™Ê[é µUéñ¯_MÆiz_¼Ô¤…å¡£ÒíJÐÔÏl|ÆÁ3z,áŒ8vº_ÖDW04 ŒÊaFÊ’´ì+JæT¢Þ” ÀGÖ¸eP)Z‹Gìeœíù¢Ê¡V¾øŒŽyßt’.º¨žŸyJA|óó¦k„¦›£$Iÿ3Î+çwSÓUØ»+Qyöÿõ:‚Zé´¦6°1[ÉþU?#rϺ°2ºK­oéæ·ýÜé€èkp挛SIÕ¦ú› èOç0CÙGz‹R_W“dž¼ÞÖ‡¼s¤ðAP/pÐR™*¡?Z½/(‘ÔψçM‰à­Éòó~° Cûˆ,áÜ|þÙ%–]\¦ÆSë|šñWRfN /¢Q§sÍ-šèÞˆH@õ·Cû–ºÞ#rË.åS@¹êCù¹Ò”CUñWÛNÍúNÍîä©®`5O€Ê/Sí*7I[ô&røÞ ÖÊ=^ðêä¡»§Eù¸}jF(ÌJ",“¹ÃûÛû°¨Fë™sfèµD¡¥è¹`ÏÆÃGQàWx£‘úààîÆOú²ûè}«ÏnÑþÝN±n°ënçBŸ +ƒÊIÑ„P®ˆ2 i7ÅVEÓŒ,¦D£º-ψ$EÚ©©”X±ý K”Dtƒç)ô€V-–>Ëw—*É3úõJhŽBè÷^†ÇÌLúxGsØeUAaS ðœK™^l/‚¿ýƸÞ$q´3v`|ˆwÊZc§¯3ºÁþ®AàNaí&Oûp>Òn4Õ›¿û]}ëˆ!d·³¨v[¼c+ÂQAýØÂKçQ0iAáeE¼¤Þä¸2czå¡SµÃˆÇ:êZÌQÇžlT2Ù8{¾C;mÑ´Üo×GÔšåq°Ä»ÙÓfœÚ­g $ÒJÍÖµà8µYØ^/íà{é=¾~EËÑ•ÆUS +Æ°ˆ•5ˆaÌøC¸$šã´h±.=5Ö•v"‹y°Ÿ¢º©‹¶3zü¨“ð÷“' H¬\q ;6ú`eÄiÒ¯/H7P ³ú¢ j0¨£Ž]G*Cû‚äUÄÎÆnÍî°5%ƒ¾U²óžëᎧ1${¹QÙÄè¤ûó (¸­o¤h‘ÐCÛç短ü3J´p0M‚&”ãµË +èTÓèèQPe6cnND©FpLjÐÃRœC<2-©øôÚ†øßäðix9Q·3Œ.¼5«iP +XxWÖ?ä×[±ò™Û[åÙéb®,ßY°¼‡Nt„4¯óJ{ÜOçTpð–¾Ý]Õ`X·^JÀ#V”bzô•4ê=¹eßôøâÝ5FÏ)ƒ¿G5Šl´žÑN¸»oQ`´“Ô¢:ƒÐ™…×€2 +"Ò,Åh"o ‘ëÜïñLóîâ=É‘0¾½×€½#ô\iö'éàˆÊ9EÕuG2K­ï¬GöÜÎõ±?°¢ªhÊ"Kz³EŒNÉlGŒcm§D:Ñ•a¥}˜ã4ÌŒVÖR9ŽPÔ#›ç8"ø€riCéÙ>r?X°žò¦­²ó ÞC~¡ZÝ’,pá¶ê^Åþšµ® U¶üH¤µ—®Bɳh`1Õî'º¼s•>7Žš#ˆÄ8~®;=[íjŸkì:uQÁ»ý¹ÕP22ék#úED\B#¯3¨˜ +³%BxÇ:ƒò¬2:iñk¯ñ±¼8gõé‘ ®q¤®&N˜Vžå/Ü"•_cÉ©´Èö˜ªÖ° ¯C‘Šß#åUÇ؈ü,^¡ü ꂯ |¼äüýÚ<ëГ7•ŽÛ@5³?‚/gYð1Ò°¤cpXiŽ¶úñ÷”æ·øåÛ6æVyæ?óÙŸ?Ÿ¡ÍhþP“žm8YŠÅ¥çZhÛO©O·´¥]€x9 ­&WÄšB&Íê¼Jq¾Eäy …Õnd§ +ÜýøÜ×—Ãs?®QHPyÆÑ÷›´ÎøÏ?BŸQô;1P_£ÿˆE|0FÌ|xÞOÊýk%Ò)0IŠ>·…óõË»~­Êü¦óËeF +äˆ$ÿ¨îÁpúÛu#Tí­YLí’z_O§÷ÞiŒB¶m3¹UžÁ¢ŠÞ4—%¨KB´N#N\€ÿªoᣯ¯]2øxÈ¡DY¸£>%·HUŠëÕ_÷–!®¦xÕP™âv¡O±´šïiúQ¥ö‘‡ó–¶}îfx²õÍòb[Lõ{€/AÛ/ÁÆv}±8e{1åÑ]õtç>]èI£DâB©—ò×ö?÷œƒ:Ho¿Ý(8»/Ã|$$êu³jT÷T?$zŠ¡ˆ¤1˃RÚº§ –¬Ô×ÍÀ5EzÑ;¬» ®RŠLý›rOrç½ÑŸQôjׯKI¡¹@½]€8f5SY/_&G ÆVÏSî! ­ A›F±VAœü$ϾbŒˆpÚ)ჿ0ÒãJàô‹qo}XLúבœ&±†V¢uÒH¹Ð-}ã|xZ¹E‘Ê:Ê>c”þ™lnE8¥I’°Þ)ÔÍ@Zú¸g§ÄõIKàAX]ÏÜÇgˆ¬R¦ôgyàIÀH@jvcêЉCnö#€:w¥œã¼©ÀÛ[xŒÍ±8 +ÜÛŽ^aBòÊ*€DPÀzyèid±±¥>á‹^*ü½í¥‡e ´w]ïq?DÑ;DŠK?bÃJ L˜g©Œ•°¼Pî5’pÀàÖ¬iz£ÿÐÆöH×f@”좇,¯œÒ“"1ÞR‹3ÓÕT)9ÈÈéQ$;1 +ÝúÔ,I1›¶\ òQJœ‡ÖoA?Í -«‚7¿ÙO=]1Ü "‚sEØ)oýgß—ðÛ©à>ÒGÔ×Ä5x؉¨ËÊ·H'@ã‹$îDÉkRDÍ?•SnÚ6íT® ¾ZßÅÓ—iß[ckt°KÙ"å2¼(@ä¦íúëëЈBðZÏ(2ˆ7ƒ Þ¶ù1DW”S<²ØlÊ$¯ˆ‹P¬9œs|:ZŒ TÄSDSŠªãÝ6íÓ‹>Å|ŸÖûP4QÀGÆ +ùmÂUÏe|llªÎTáÞø@*?ómÚ‡@E«{ömG0ÓÚgD0“žPTæ·ûY97áµÎG™ÅóTEós¥°SöàfxÎddwyêʳ]ƒ[QÁ»Ý[ºí=êoöùØYlCf&úÀL  »u£ªœK™Ž9(ýÈ“møŶψ.^‹qkMLã¡~D•GrO¤mÖë¼Y¨Y«Vñ¦Z[Å°ˆøN0ÿßëÚ²B+T°–žå›ýP¼‹T­§ÏH=¦†¼ëàïSs*ÖïQk ­a¶r6 ´0B$`5‘Àþø1ö‚2cÉR”lv^BMN“ºFPÑ5‚ +h~! …Ë€#Dldâ¥Ç«ïÇsžëóu̶‘~€FÌ Q)8È!¨ŽñÚ€ÃÁƒ£ÜúJÓ‘çÓl—Š*´ÏµNP(€öÎ-"Î#Ú UP¤é»ýÒÁ4ÚøF¨ªJ:)£üXïô–÷?§K3SÇÀ(j|rÔníìç°ø® ßßD`ôÐÕ«_§¶‡ÿϨ#s׺\#² ˆrfÐé1Ô1¢¸Í¬ÈŽ¾mõ*Ñ?f­zÊîÞÅîúë\é÷­>AäUTœì¬­Û?‹ù{ÀòüAz—„þK¶ù¹Í:H¡ÒDî`%Ö owŸ²‘@D)q?Þ†ô¦§ r>i¥)lÛân¥Ëù …š}X¥ïY…üãĵÐG>caF Z§2Øbt0\£ƒDUÙuŒ¤‡› š¶©åe­YÏ;+Õª¤áÊ=A[›ÖQŒrcv‘×í3ØÞÿP…ÏÙ'¬Çsµ$ž°èh$oxÃ6ò¢šw"=¨@2+lΚÜ(ó–²ü¯ ‘˜oÎ0ÛÉ`µ¡U“ :Ü%â²·}d^¤’Þ>–¸ôäáWgM¤«Âë[mVٙ껽ҡ7¦ ¶ñTúÉ¿ [{Àõ;Ƥ‚XYã݆èâ'XпlŠÄÞ‚ce,b Qj0Âf Åª +_nP±x!ÎQ¥Z¹6?bZõIÙŒ:öFíÛ¨›Ô½¬0­G¸VlmGL~è¯L%95çw„n@àHŸÙ‹¥é PŽñ„»¶'ÜÑ·jÉoU'HJäQrȽvF  - ·C«ÚDY…’2‰†r£ä>¶è‰«êñ؆ #ªP¸<ŵ +€ÓGã> ÏÜ‘ú_QkŠAásÖ#56º©hè(gd£ZÊšµì,Çëe§¥ûë1w„O"ÿi=5 j¶f€_ïÉÀŒÓ;¬YÕlU½úkD®ÌkÕÞ‹‡÷‘íúØôžõA3ç?¢455@˜,¾ƒQˆÌÆ6FP+¶Œ|•ÍÍpxÃ{æH¸ Œ©Ôÿ Dîú êUŒR`aÌ- HŠHí ˜A„Î+¢ Ix«JΩ@!àzAPWÐÃNÕ[ãPnö MÀ +ÑA…—çØW´V¤ Œàžq’•4‰¨öùà³i!ƒ•_©ðÁ(ÎQ±ÒzŒ÷w¥Rùç›—Ý«šŸ°:·ç±AO;}Ž÷öÀ\åƦi‡ž«¨Î¢h5¸¡¬Fó@>µ›b:o$R¢xW¸—0âv{øìqKÀŒNâ pn%öxÊLý±|=$ÒwƒAM9¹²¦ô•ÌH~@l…w>äã.Ô¶ –cètŽÖŠdÔt +›L—„Ë)WÙÓÁ²|ʽi•ç¦Uò"±–§d?j¯Çæ4 ž*EíWÔFÜ_„vÌ -—½~õ×òÝ°(¹z®PÉÕä—[¼/@T °5¾Œh2²!{ùà•z¼vnЖñ¶éšyëÈTt*o‡¾Úª…ŸZÏ•v‘Ø’Ô`™KRsœ%õœ0ðš–¯åÁ}“ú´§åÔŸš}†¤O̦¦O.yûv¼BI‹ÙüÏ&jOÎ÷­¤p’ÿŸ’N›Õ.0UD/=”©H<Ú%7ìE/_ħ(qV}¾ö›]´ày +bî:nKغå­nz+ÉÇwq¶ ì¤À…³‘â× f²û#º¼ÂÁ÷Î-}#F9ÑD®çzlô´ŠÂ4kίeúëÈ8Ń/Füಗ@+öLn×ñ–¬’ãD6wX:‘׶~’ÈÆÙÄ>žWpÕ¡$€˜›ÁhªC„ +Q­‘èD'åVí Å%Й¨TŒ­¾ÛFÚr7 –,ÚÎtÇ×U™Öwë„5…RôN˜åw£ j‹ˆüFò¤Vodè+í“y•Ô|nö[»-1Uä5ª‹7QŠ¯JX^§Þñ³;C=¼.Æȹ„c+üÅÃŒÉiô!NþݜڥvÎÀÅú + ¯èîé™rǹ%AUÇPr*„æf¬ÚT¾ÙÇúïú]ï ‡Aû¯ÄšMÌûºe—®t:Û}û`Û h¢Ö³ +í Òµé…C¶Ë¢àÕÅTuËž[D™TŸŠtµ”l%cÍÖ’‚É¢³øíÐG‰‚ ±„ÒwëŠW¦È–QY¬†+Ú™ÄÖbÞŠ++ùà¤Þ¦ì×c¤ŽJS1‚9Ãåo/›R´^æ×qX¶Dq({ÙèAÒ$äWÇc!Ub!ÕÎœ€*Ïë—4šÁxIÄÓ¨ÂG0’Î]‘ßAä<íÚõú3õzsÐ+.Ôk/­ƒèçöÿÚn–Tý^í©~ïŠf[#^×EÛ‚û†Œžåe 9I·Ó¢m&.g—öXÕ¢d?Hß.º‚,âî¬Äö8õ¼;“âÞQ.¨Ñí\;‹zwÙj§Kè%Ý,‹o]Wº»†wGyŽ´îæ᜔ޖ#á‰hаF£\²þ¬s;gEçc&n Í @<åÞ+W ¶ÇJÞ&…J•]|’’fe“ 1ÕóöÖX=5ßj€+y”'¤¥¨F»Uùó«ùyß*· 1 +n Õæç ¼c¥ÎGÛ lnÙ:Í{oÖˆ´RI™¦âß³÷C k½Öj†6ÖÂx‡yE`çém•GÅQÛ¾,ò€ ”þ«è«TÓƒyø €e„ÒüŵnéwÚ.±ÈÑN( #jŠa…:ýÏyK-/6Û>=úmuåÀœõ fF4ÄX÷ÆWul4³8Ý ¢/†ˆ­|íWþ_%•éqgcI'¯¨’/'ŸÄ‘®Z,gdrNÙÝ:ë`¸·s@+ÎA}¤ì`³†„ÞãÏEÓ‘ìˆBÛ©¾ãú)²¯±OsÒÃ4 ¬ê´'‡RÿL«¹¥8á³lÌä¥Êúú9þ6Q2ÛjGgGï–¶wÛylO™´9×i†cOìK©r©d\ªÖXö÷"-;ùþ´nkÁÅQ¾”BG^p-¯@ÖQ7ÿIszI¥òB*û–§¶m<ïH·JË”Æz“‰«[Àzc¯#WŽ1$2’¯eþ…FmW¤`å4ʃë8bù‰OºšýU3ŠbÍ ·l"¹ànìš»¬ k–Œ›”U=hä©@—©§›%þŸW†gQ¢åIþ˜—€Tòx—_U@}`®¿ëØb°§UË–ýbõ„{ÕãUÃÉ·H®L‚‚ö2âµ+“_3ˆÊ#2epŠGßk®çϦ6ÉeMÅÄ„Dwݱ ¾K]kïk-ÌÒÜDªÐ @•.ÁñˆŽ³î$OZó’koçÚ+ÐuŒL3AHj\phõ87ÿ¨•õÛ}¤µ¥ÃG%%Q`l­ÉÑé¬ek +5)ŠŠ\›]­nàm„T4Ç#¤~mUk$CÉ_<×c“r{í3^QóÌ‘L®¹Dk{œ‹²äEBu` 2€8-‹q)šþ>Ω­,>í#’h§4•å©×ý ‚všõŠO…øø²Õ:Öî¡Ù¬o²ÙƒÅA=*jž >"ÞÈõàVc§<Ñ?ÖkÒT!jš •ðKIÕ!máË+®°çïs‹&À³ýÂnŽ’|“Uÿ:ÎÔÔÁfáó!%xõönšc$Ï÷öѳt™‰ˆ2’%©DhÕA†ô%¨#Ù¾—2 Ô©²lƒCÊ7~Þ©+Q¼VîÛV<J?¯Ó*8éa€À§‰ð¼>¢ÑÊâå»ÑrÒ„¬HÏÅm_µ¹¼­Q5;Ù´‚jüP +8Úˆ“<:ìPx’àXI¿ €Ù¥FÆZ@7ºC.©³ƒ¯ÇZ|·áçBè¾ôÝxDJ(¨²‚ g„ h±¨ˆÛ +Ž~èØ0X”ü‚'Lœ/JÝÑž\ŸšxË·¦ÿ–žöÝ@jó pž> +kc´ö¡_¹þêM8olÃH¥ç³ºB^_”’kíÞLéÉ·Â>ö;1}¹Š`‘rƽU}W¾›G¬îú*5àGÐnÏZ€— ú"³mNvC•÷2¯¸=½#'x½‹+ù?EU7Ù=ëE.ÛYd5¥Öì65‡ï¶KIŸfîQcQT ×~¾ P¶ \›ÖTXK½$Zãu€Ì•UÃÈ)àÑÖ+6\ÅuüàÀ³˜jYð‹i#o`4ðД +ÈxhH¼8ûHT”ôhÆfû#Š[r»€QÕ}nÂý°8%½×óuÄ:KŠS&gÔ·Žm¬Q·Ã·Q3¯¦^¤Xò8 +y üºé«‡›Ãø:Ç|Ì䔹Ÿ  ÅÓ`ïÿJSâ¤Ø¼KK6—pé‰UYÓ/sk{<*ÄÕî-"Æà6Ccêø3Þ¸®·g%÷¨ ­'3®ø„ËâçÑ‘õr +§ã+Õ(r€°Blä[MZ¤þ:˜4¢‘>ì=Ûk¶iÀÆa&_ú£tû£ÊË" +E³+*èJeqtm.Òë™ÁÏR¦ XËZñµò ”ŽçxSKOp½ÇŽ0ˆ¹¢’®¼JÀ·CÆ¥®”˜™Û¨âǺ[îïƒB›WcKŒéjf…s NˆÏ9_Gþ¢šEG@(ògüò¸Cbk€SÕÈl ¢Ã'BÀ…çž=sÇ äËî×z}¢ß}£²^âËäH÷²ê™.Žˆt[#YK˜ØŸ³ªU{>ZÜ-Ef—o3íù¸ìðö('=´Ygö8l©a ­hŒ‘•#—û +ÊžÀŒ¶Ýáaöxò¯c7TQTÕÛÅR + NJð”¯Éz «ÜÙ¾‡ù륀׋nÄ:ų®]×<\{^ÙcÏ£¨/Ô-5åà¶KS‹ûñþR²)Š^Ôm–vâPHÕì*‡a’‚DÔSfÒ@;zEmM”Vªçwõ®aϹ¨ô„WÖ½HK×ð)ñ›Þ| ¦3»¿D;›µmª„6Ù÷ô|;!·Jw3À¡nÃŽë#šê¨¢¡©ŽÒ+ÙK4G(ríùIíˆ~ë°sšÄƒLBE[}@ +ÁêØ"Ǩå!îÊQ÷È"ë¢,ÌXG÷²ûñXü 8ß„c¯´ç Æjû"Ru¿½Œo±ûÎú×æ]3Fò=1Öbõ„Û¼Ý^™#2NÉ•û§Ëvœ/¦³˜‚]»9Ê^3Þ!T3†n´ ÙEõÅ&뺾õ©® l<ÔÎrddi”Ø@2Óõ´¯#‘t1Òhœy• Q§Ã4:7ÚŠ²Åöˆ47‹žSÙÔÖ•Š&bäd1HzŽCÿ™’P¿z@cÇ•ÝyÌÈë¹j¢«Ð à¡*£Jd€òî¨^Ëf(+j  ²t -UX$AyæÚïg‡Ë7b i9pÁŠæ*Ë?…Иž¡ÂT´M튎鋅älj‰Òâ8$2.@‘Q.½7ï¿ b„âˆq±Ø¨Þ¥¸M?466šØUaJ~—Ç–ý¤¾ózŸbžÐç@‰“S&øQç³{;ë9=~—UOVg(ÖÜ Gá; +>^­ì¬ÙÑŒJ@’­´!ÿ…tw§ŠëLõâÚ1I‡Ð¶ÐÂãöÜ +´õøð Ø¹nÑ›ñ+jñ냥 Èj®EïKP)4~‡e4؈íéFÔë6QBþzWÙ¨7Kw#É‚¿rÝŠŠè"ü3Îü²@þŒ:Âúá|ÖFŠ¦¨ƒ¶64¯›â\Ø0µ€âì–«%#çt¼ëÙºhÓ‹ÅY ÿ´ÆH#ë ýdœD)/CYm¨Ž·I`, xRwoMúP¿ug³cÌjÏgÝ#‡¾"h+$Y]"ÈôŸ#Û?ADÑ_@5^ñ€½r*b¿m[&]¨¢!NŽs>ÔT‰'ÜX-*'Œ_‘y¿Çý› à¤@…SºÉµk[€fÒ8•tþ,ú +?‘®)#½¼#þ#·c%QÚšw‹ãšàÇt<ÊØ `Sc©vä-Öo!>±”zÇ6×4ÚwºFêù‡ë|ŽúgO5nûÊe©Í™¯šˆ¢?Ô87×{X–A€éÑ*5°]$bõ§¸j™AÒÎÐRÂ^qá•1˜´È}1ã·v¨Œ,CR¾°.Å´#pµ¢¢ŽiS>M$êHÊ6e"gÕvç2©¥‰3kÔÒ•)±ŸÎ½Ùï¤Ug>Ã*_€Z•ÙãÁ¶W…~ÓsÑ0|®Ç£“Àå¬Î>(_K±þr¸¶¬p¹ýÊZÙÏ[ð‘¡KxXaù–¾,8¨Ìlßî?LÿõÎÑ^x” Ew—s½GˆÞŠDô䪔+ H „&ãY{ï5;\kå‘â]AÊ}ákð( nuÂ¥u‡‚i×}}ègÊÖÁ #»ÉŠ²£þ·|# Í«,¨7m©Îmw+,³ ®¬=\™ +&V¨¤oÙMé k¨Àžži1ѲҜQGMjpQ9».Æxbß/Ú†\ôãÔÞ( ß4VŽ¶å·ž\—ÚpR¶órî{×Õé×µ7êкÀöäwòšQŠfTq«¶%Z­ûÀÎ,ҥ㊺í +3LíMNT@©ý!Š\ÁTéj^ì ŸŠ1¡¦ûív¬‘¦ÞQ2Iµβæ6=ƒ0jZr•¤ºÔîûÑtÝăBƒÛ)Tø ÝÆ$z^jS%÷¾‚¤ÙÝÕöݬ`Ô‰ºìÀ7ÏÍבD€>îxwó-ò61õä;îŒòЫºZŠ% ›¸v ég,=µ`@Ø`þ¾Éë^"‰ u6õ%ƒ.ì¨ÀGŸTΈ O“k ¹çæö_ˆ40ç¾Ø7Z.¡1ÊÆ2äȳD½ÛE_aˆ/áèouy ×í‘É`s[õ¹îÔIÖCZ¹êƒó?"](F·âü—ì…æÃu*?°Új1ãŠOe£OÔígæó¹6ƒkî#ͱUÖשN¹þ}ÕÖT2j¬¸#ˆOÖ²yò5îçúlÉÃJÔ“€Lï/}Ú‘ª:÷Û£1f¨â:âîZòP;t$âЪMKî8¨ç tÌJ„<^H´ÿAÄÑ:ÝJÔVB§ D‚-çIß|“Ýdnxw4ÍÊÙÌ×p©Ç ÊPç'uÉÅG¯»ôºXý4¯ äã£àm£$ÈY]SanÜÃ=ZO‡é¿]q¥àñ7• Y‘ Ù Yj0Z\±RŹ¼LRò}nƒ‡ ßj„Ú®!$/€;‡:¼ûE¿M:ˆ:.w\P¦Aä%<µ#ŠTXWny‰Ñfÿ*UôØ„R^¬µ.þ›ÿ',7+s4æ.·¡?âбWoù­brÉŠ½CØo•? |´ +>6°FG ܺPwÕm:U˜r$Wèr©ÏÇÓŠõ@ËŠ‰Ç˜/ô‘†½âBy\[¨ÝtAty°Â É e>"³3~$k8¯-ÚN·ÞrÔQÖjÑÝ[ûi©–œ0jôãªÿ8íᯠ$ 1M(Ä3Iá:‹VËV§UN˜…ñû8K¥zi!Õ ÞKï©[éj0·—³ óàñåÁ‚·h´Ž1P/E‰½¥äìQÓu?Î3ôÄè£S„ø³0¼¿$èïÿý“>÷ÿÊ}Èçÿ‹ÿÿ•»8§z«»ŒfŸÅQë³fóû”áÃ¥ï~òæ8 +¯›s¦b£NŸ÷¾iL¸ºè(ÿ¬Ö´»*.$fƒIG§tu‰†OÞ2˜Qt‡ìŠ*µ=8tçÌ\ïÍÑ@ºùÅò]Që)ª„ŪÚ(ÚDUýeˆhšúõªÜ7!l¦ŒT"¯mêÀ>ŽìCQ8Šå¡ÔË묌)•½(;‚Z)îGè˜:taZ)ª ã~f„2j´Þ5§!‚웈ã™iÛ6g¤eyÑn9’.E^±h”ß`ºËg>í¸þÉ/8 4—½·à -Ð|ªÿÆ-¢x¸o p‹:«9¦&U‘0R8ÏûÓeJÖTªQkã JË Ë$0 uSëÉÀEöJ À.W0VÙ–ðÉ^ßÚÍuƆÜÀ¾­ÎbçªÙk²"«çÞ9¤è:?·H¨ß:šgƒ¶&=#€wl•ÉòØœ½>5PYy… <ÏæÚyŽçßAr§ç ô›€f¡y>¶VÝeuNegað7~é°ã{”c=ø²¡³ËÖ„V‹X£¾4«eª4«LÔ§ KÂõa–P’ŒÓx²ð±«œÏ`Vh¿ub~>?&wÐód˜Ã|m<K Aèa× ¥Á€95AyaYƒÞ]Í‘lÈÙ<‘ƒ‡ zùúòèàè×xÕæVâoz{iaedwèŽ}®døý‘¹öåÚÖ•-€KÜ œ+¿^㦿Öp>~Ÿ²öi»¥?ñÖ*p$"˜a‡öÀ!ꉔm}çGË .Hþ/=Ê.êÊ­€umñŸZ“»‘åCÄå¥ui|¨“Q#Ž¶›u ¡H 3„VK¬¹ÄƒÁ‹p?©3ŠbÛZá:ÛwÚÆšÈPz¸îïQöº¡óƒD7_CøÂbz:‹³fq0ÀqÒQÔdò׎8wÄFü´ã¡÷mö n‹$“yši¤öìýsûw/êG|$ZrâT÷÷Tä¯R ËÖJaéªÊñ) Éšô?w3ÐrY2GÑ:3„€Qš”´Ê¨¸Q â6·ä¿-¤}¯ 0 +i…(RÝFw¶V¦íØ•»¡ÿËQeá`pgƒ}G„T!ŽXÎ}>$6Ò¹„óVÕpo:î*Z®í¥ÅUÑndž=¶ åV°à¬g–ˆl8Õã»ö›>ívƒÀ½µý:JU·ò ù¦@çó-O-z˜Ôï eAYx%VLnIõí3"å§ï?6ºü³.5™ìG˜oúígÞ)ŠcÚQð-™¸ŒÃAt´G`…(+”íÖDiÏÇÞι ù_®²qHœ¦ªÂ£èûçò¯1ÍÜÄ—e^\ƒ5vú%b/XÁ=“óÈÑ5•"í± Yš4‘RÛ­A8ã>RjÑ&ã8új`´fÕ ø¿ïÕGYË¢úB9¼BÅü¬Ä”EÈ:°¸ZFèŽ,¶-M£®2G²ÿò{É‘´Û |v<Ö«S³¢d'u„¯¤²iÛ=Yl+0ÌÞ¶`w”žë©Á3q®ˆÙê£#+¼~¥ÅÃ岇L±½ªóÞãcÎÝ(-–?S;ªÕ›»Kp“{ì +©(F¸x÷ËXÕâùJ’ŽÌÄMC´µ†:ëÃqâ"gA·o?E±¬¤ œÈ·ZCpUÌèÖv€5.ðoÂ¥6™J;ãEû“(¤¯ › 1Lpêý8™¡RSŒ‡´"z‰…Iƒ ʨ¤kNY»/¢¤–ˆJ® GÝ8/•uÆé’Pqó¡#@‚Ia÷àͶwè +“½¿G-Ü5ºÕÒÍ%õ*5"þÜçòë¿à'*¥DåÚõáà4 +â&¢‚Œz>NAý$Ä£6bpQýšD…ë’šÍc#Î ¸ÃDӕnÐK+ß­ó Ûƒ«8QQb›$~ ^L¯îGáâãÑ[Ç¥Ge>(+º³uÿ9!p¼Ú§mc¾Öó;føíE­M àtÆ)Í@™ŠŒîÞm Š»`üNô³-¨_‡µˆÔ•êîòÉÎov3P-g4GŠ±‡çkæ6~ˆšU3zT{à–wkWÔfQ”ú ˆ¶¬(u)\ÞÛJ×qâØ’ôD8Ãpbwü×£Ç ¢D0²]7~áfvĉ µþh)3³¢Ž#ûÉúJZñ>ü”1EIá¾õ[ ºèÝᚨw}µÁÒz¿Òü#,D¢3B <”ÙB@ °Yiuw€IL§GfQúôñKi¡§î„¯9•Bëm;¯DD&¨¬”u¨J¢ Íq0íÒIDz~ARÓT'†L¸_ˆ÷¨šž‚•GIÝHpÓ!ŽéTïR0ÏJ±Šhëq>­Ð*õ3â·»¢þÌz¹Z¾ûÙ LtPënB!¯½Ôá°3hÇ0채{äÆ>£ø$l.Œ¢ÉÕØm)n>.v‚%\Ï5RT[J#i%‹g2¤·ËƒA Hׯ).Ψ‹œ…Í~&·Úm´µj$åÑU½äÉÜiDîêămD?Qѽ1)c¿ŽréVj€òw7 +ÇûÉ2ߊhÚ1ò¢ ‚Ã;©\uóÆQí^éKdr¼å,Ó³°äƒŸ«âM´»Ò3\·Îê}â=÷þÖÖí=bN/«ÅÞgÔ±Ñ?k,ªûÞw+ãääØ”S…×Hìˆ)%åýïy‘·íuáþ9~ÿ5h‹Wg/[…fLû ë\äÞGëGœÚ03n¤ªÑø(ï4M§ÝHºJÞ)[Í戮a­>Þc`­c¹vv1=ðøó°¡½– 1¸ç 1"Ôýþ:Ð.OÙ¬ÜiÙl=œ¾¶3'0Ú$–—ú i+ °bûéܚϊQ=¹S#\ï”`Õœt ß]UX­Õ3í¡g +&·º…¤*£R‘`cYMEGÜööðÌê(-–Âi¼¬2]Ö4Øf“cͶvÔ `ƒ¿Z i5f‰€§F„®¥GÝó+yí-][2n"¾=]Qc_Ûº÷«bƒ]¤–Všgz(ú 8®g_ódÎP2½ÌÃXéŠÄ'=ŸéšàÒU.(åó¬êÜØ¿Þ?i}>'úK.’ ')è³¾ÒjoP·XÁ­Êt ¿¬´ìhö;™mù?*bh£´ŽwÑVa±åŒð²ëª¸®_E±¾a9±ÓÍÉ:ãë §ÙˆÆèédWq<Ý…é1¢Óã ‚Ú‹ÖuûÒ¡²ÎC0DE®ï*¼Q¤kÝFI>H¹À ܨkíÛ¯80q>NO9@QéÝ cÎ/V™ðÑ÷Ø®è*€­TážšûH> zÙD¸rS¹¶R;y+†£Ô:µšØ¤•øÌôZö‚"×V~w„×gK›šøþn/ð¤æ ²ÖFf$ ŒÇ™ÉQ7¹Ý#Ó*iÙµ]©z&Ê¡#ԴК§ÒF^DšÏ-“liOu‰ÏWÚÓö+VÄù ýŒ‚ÓITÝçèETǑƈ5ÓÁКɋ“霷×D@ŸNÛ÷*ÁìÔt\®¤‡Lh—@}$hØ`™Ðkò›¼)Kïc'$¦`‚ºQsG ¿lj>†I„©35˜Æ +HlȬZ–ùƒìp®,Oÿ°mA 6××o€¤¹ÆÆA/̆aƒ˜#r3q±B¸j—à÷e—ˆŸÎks¤Hììºfhj‡¨sF³ºû©×Iÿ¢°üfa ™Þ<ûOÁÝÔ™®J£_ÌÝRqò•ÙlÄØ÷ãö(x½oœÌXî#ýÓ;Æã0‰/M nñW—ÂO0ûî­A k£õ´¹DŒ•/e“3Tohä.!'Óeÿ¶Ì›b´j(:(P?ø ÀÙ­šx¿4¶û)Fó¹Í?’éÛFã”àhfU­ã[øP (Î~»k÷ð¼VSï}ßUrøL)vkÃwºr«3¯ôyFx$È2^C¶ñÝ~tÖ†H¢vÐÃ@ϧÅ0ü]DÑ(<º$1”¡éqÑ¿¿¯œ¯&é*ÀIÛÙe5p¿ŒŠtÛP‰aùöñ|¡Õ;ƒQÆ}³lwdÑáÊV2_¨U†béø6‚ETpJR¿j«wˆ›¨Þ; DR¬GèJåV™»ÔrÆ{¶°œ[Ó_ASntÖ} Þ÷‘{×?Â;ÄÌç~O˜´µõpµ# ¶Qç±(+qV¸ä¾âê±² V{>8²fº· å üJ8ÚFçÞPa%¶Â›?XU­¨¸>zœ>êkž9bN·¢„.xt×tÒWöÓ¼‚2ž*ïæJæN5Àœœ]´‘Å+ •õG†¨EÛÍ|ŸHÒ3‚Y´¦”ÖdL Xd=pbUG çCh˜šY¸:×%¢ó•¥Ð”D)ð#Jņ䖀Ž‡òÜJ­¼éî¦ïe·õµ½} +÷ŸQÚ6qVM‰š˜°L aQ3ž¢”Nwy]ÉmI°Ä;èbº^Úš}+JžÛõ¦º@¤þá ¿Ñ @Ælj HÄ£!˜š|O^ˆ2i ª—VM1õôÜGJözwÄÑ"¼›úþŠ*ä¡ÔýP®aADÃŒ=Yrÿ\n4R«êãoÌÇë.€žúÁÒÈÕ.EÜ…P',”žÉaÐíüŒÀ.‘Êð@[àYú|F]r2ÇÒ9¨¶›©e|gƒêþý½õŽ¯/ò/q³ï uf×_Y´cëu¢£ljÍš{6#­<§Èèbz³§l®_ïA¸ê¾p9 X˜Û²ìD»$Km¤¿Sqæ¬æ³wÇ©‰|üöiã ‹ûã, +Ï¿ï†ï>¼\:YÉÎ(wJŸßNQô=ÑÚC†q¨z÷©ÊÆ%LèjÛK¦Í yc›@l…h”_·ÂóÕ‚x«þ²¿wßq¬Zw•¡LÆÔžµr*N»ÈÒ°Tt”ݨ÷]¦?噹­ç]ùÞfq‹ö¯†8ïÔ¡ˆÞe$ÇɚÊZ½E«fž{Ýs jRÅEÞôhŒ£î#§¸ñâA‰z\ŽŽdWÕ¹ƒ(æD³¢OÅ ‰þ{GÌD‡2bîˆúD€ï'¢Ü/?¥ãË©[Þ‘â|lŠó¾Òêóö”ž‡¢Mü•/¡ÖæD f ©äa>·íN±&°u7[8§"?K¯Èsª›E T!…°35AZï+"ŽˆºZ#3ÊdA(x¿iS> "$Lô}>Ó 0ŸExEäyß%k”ǚʉö÷ L#R—Mª§,'QÂãÄ <Œ¨5”@Ú8‡x©íkM>÷~îØ¢WÁÃá‚ ýûuX>P1j1&&(?¾ÐŒìb>¸rªÖ˜ÜmàÐC‰yŠ¸£ÆŸs-ÒÇ)“ ÞqÕéj*þÀ >CÒeemé±q™$wuc›û±ßsjXá­Ë…·zËH]°`áclµÏjš_£_ö±n©œlô7_]š#F‘©Ö]{†|nÆ[$×›¯TÎ z\3©À qÉR1ÄâHÑ “%\fãÁ·Lò‚½ eFÎø­Néܶ;Öð¡»cºq«Ê +.ºdF*XAsƒ6 8ïЋöÁ£–ôiaZ´¢ZGÈpDhÞ ¢©ˆJ9–ˆZx8䘞ízûÀµÉÏy0¡m`7)¦òÿœ +7à5g~mµ¸ õ}eÿ»ÅçÍO‹KþF­ÄˆHB“<ü&6FÈXyÈ5˜ (—ÕÁô©X¼`Ú̘ãÞs¢ºûöt´€0Ž ŽÈ=¹ïõżªGtUŠÐÐÔ ¢©TxAš—?쑲+ˆ)ßð}W„h¢:\(«cë”Qòë0ÖPlO•G¶ê=ãMè‘€Cq$—¯ªf£qäv [E2@Žˆí-Çø2“ƒ îèzGHeA-äÜÑóhˆ*‘˜F¾Ìí¡/¬}S•ª‘msŸ¯ÆíÇ^`±2»Ñv=÷ƒÖf¨ ™w†›Î ÏβÖýù»{õ¨®IŒ:^¿AˆHºÁ9Ôлk4ì©}Dð-Þ âö# +D™dÔˆ˜Žž¶5Δ ¿P<"hÔ´Q‹a™{ÃȆÇOƵ¯ g]õ›Ÿó"̈ÍEk‰ÃÍkCO1 +a®ñ‹·g†735>„ë³A s6Yû¶”õ|)÷±PIW“îøÅMΟⲞb÷ÂŽ§ÅÄ«2BæÖŠúŽ]H…¨±÷ö@YÙhÆÂß„£dCª¿’Ï."µ*zX¤>ô°îy†¶.ný×xai®º¡.¬„(½G §Pœ +õît`J%- Ö j5¨9ÇÀÂúFcgéÚBZ¹\\NÅ\.â¢ÞßÔº?"j¡2B\9Q,6EnÌȼEN4*JEQ—4¸¬C•5^f¤Xµb¹¯°Ž1­Œ€ZŠ’×*N–7Ê’!fŒv1BÁÿƒ€YÜ Ò@}{_¶l˜åXÖœ[fà*sxûÚ¦H¨%ŒC7Ș۟k1UФí9=÷Dtä”<Èr ©‰N*=il~Žëë3¶ŸX¡±;‘Vµxiœj*iÝܶ|b¾ÑTuã=b-~óA¿o„ÃD¹Î1‡ÀsIÄ긘ϑ­@Ñþ¤x¡˜NÒ—÷ˆL!#VU%Âßìj…èøâ»"ÉŸ(Dšˆ\µ~ ÞbÞÔ²éìR¡¡î^A]vŽëp1¿]Åœ[É¢ˆ×øA½§©9Òû~Ö”gm™Þ½Üºb ¯à»€š‘]ä^w…ÿ¯ Cñ«—U] „Ǩ‰ämr¿ðt@T–à–°_“–Ê +ñ@º!òì¯ýÛu)»5Fõ‚´š!B4#¹§¶¬è6¨OÖ˃"ª7UÝP«ûû‘—{ £3ð“í  +Gº"¦Ç6V† §åcÃ,ûŒÁ$AÚ"!Å ¥*Ü%)À"ö=§*†%.ŒÕôq‡~䑬þÞõ‘Wjë7BM,³}³Ÿ^â±F´Cæ$~JB#קç7â˜~J ö8«Ÿ¯òÈtF“5‡5( ™Xs{Ü­ÁmlvlˆA ¹ÌË[ji„Ž’ÀXj æ } ÖÛ?žý„¦ÍÀxDÏ[Ñ:[w#qá@1²Ž¶Yûº¦P˜Û`jÌDy}A{ñQ—ó›ýÌ-}d‹ëÜÈÞK\í<ÊwwõÄ<ñAY½A¿JÑùe¥`HϹ TÔå’K®çÖ7˜–|¬†«†YŒÂ‚”5zx SúØQ‘»Æ£ÚßAwøô“„DÓÃ9И* kUR“B([¯é3Š5òó..»c •ŸÃa•477³‰Ô^[!:ïz +RÙЖ<,=Ä|l¸V_ëÿˆîÇÙþ3KJˆòA^ußÒèÜÚãßÐQÜÿ LòÂŽþÄ{eÐZ[à]„ø±2<Ê S´¬¿)¾>'âóõ‘Ö“õ!½ ûs?ç¦RÈÙÊ,Uî"VNÚæ—®„Ò|ZJU²Ía•Ö!¬¬Æ-ð…sJ ¿r iðí`SksÜß&Ö$¥ïQH©˜P;W5™´Ì«Û4T2:Ô¶aP¾5±g‰vÖmKз-Á6ˆƒdÇQÔÇ…üyÊ„t)-‡\í¡SÌ[‰Ý¡:á(®#?K-ïVø{å]*‰ßÐíú@çx:LüTßÊùÓŨ¸ó h(žœàqå^ñ};Áõü?‘Nb>çNòïëWÿ2gý½6r7°¡ÝÙÀZ À Üp´ÝAÉš#(ײþ¤Êõµr*Qn WÛüÚ +ò8nÐÖ ­ù éÅìî‚]J‰(uÅà ­?èzò®0gjÚ®Ú€ÖR¾jÚžmáD¶’Ðz±X º÷‡m¡J×V(B`§­]æ úAÔbëú~àkHÐ;| pÅŠô¤tQ‚…Wà+'b¦ÝàñÁÕé:™šîßeáOÔŒ™9&$’é†z//&ÐÚ¨Œ5Òq0âÿ ÓüWé-¡i7mùx8~ü}O›ÍõMDŠŠʃ$ÛõV)ºdÛ ËJnÊþÏŒÊû%ÍŸð´QåôjŒ{ÁO.…÷/e2sC^Û˜›ìÇsí”y,€ z NU“+OjïŸZ¿*ßlÔÔs ¾‹ $ˆ|ñÀÈvª_6°Ó—“(õ‚ºõ²ÓTBëòW¨ƒ©Ú̇µžáIõ'‰©ÍóT¨Ý{ƒp©tÐü +’-Ð$ÒJ…c&ä5ñÖ +ÃåMA:é$µ°R,ƒ« FVmÝ?Ʊ©)Zyýw& ,—û©M=Û.µ¢À×Ð)úA‡ŠUfW´H(œÿ( úð§|ßàW ”ñèŸ!Í–ö'óëß¾G`‡åG9öX4uz´ø™wáà˜¿ãÛ|å›-’×÷ $|{´5X=]6ýÕg„×V>¸†kJØÖò@èSƆŒ<çU³Æ+Ì×È…¤ñªI+[ZŠ‚7­Šàbø•† n@cc¨üårÇ-QEÙõ÷«oEÅÂûŽ?t»µ±šf€Æé»,ÔOÓ•‚S +ÿü;{1W¿Ù8¨†ÍX êDÇÿk:œ¶V™+PöÔ…$7S×;øÕz2{²“ÆÏd7œì¾Ÿ"Ÿ)¬îÂ:cå˜è_ÔMU¢ˆ¿Å`ï š|îÁì3dÙl»ËÂRl 91LpÉ·‹ï"K´‚(—F£ç3–èt‘?aõB—hý‘ÓaRfÄZèªð§›Ž~d··òtf¯Û¾H"JÙøÜ(ŒßÖdQnŠq’žƒ#U¼±8ÒšJ@Ráa‘|(Šß:xß ˜Ð^9ÐØÍøÒŸ4¿‡ôOÆÑ,g¾ë@îQôZ§È1Ù8}e’ðn×Ë~_˜âf„ŒÄ~ïûSC.7²s,f ((¬€:N9ë%S¡ ºäöMVâ±íóüßuw¦_÷b€ ¦ei^€ÌÄ,SVÀÆüBSê@¨Xõ¨¡$’YPµ)_‡kCPÅvE¼­$Ã;úfDT<”;lÈ`‹ãLL—æ̉UDqý¹rª[\Y›ìIþšªeê FK]‘+­nìöhbó¶ð«TÈØ-ž>mPá¤gCWÏkÖ=¿ª|7ÒX_kd<0#üÈdÕ ·_xÙ¸· #Í“Cª J²÷ÆÒšº¶±ëD®â^?ýtDd¸úÛUU >ŽÝÚÌ3}Û‡Þv îZõÄYöˆqi¼•°]ýú32uZ1¿ÆÖ¾ýÚ00ë-zé?¬·P2FBkÆÜLÊ<“m½ÿÝËÂ>rÎm`¯,¼ÁÕ¥½€’9¶_ŒÜ÷æõ·ª ·@ÐÒJˆ¢ë ¯×nØD¹RIÖ.r-*³%ÓScFž-ç»ùÉ<÷ON%ÃwöåÅBœ¶Ú-H£?»ëí¼·óZ[bͤ:g4ôG\S×´.r^÷FÕ4å}ú~Ô\¶¶tž5óË„Ò.ªÙ£dyÖñ‹¿ßÛ2ò÷Î+^ïAÔ[›s¸°¥kò6‘ +Ãzýjê*þÍ?AœMa¼o¼‘:³Ôâæ…õµáÚæ¶Lßc‹¿6Âß¡=pêËõãÖ1§n‹Ý¦oçÉÜäž%âPë‘Q_oB6~ÝIZ<½ˆàúã¦Z_¿yB7 ­nc[’eô`}FAsúV@º‘9IMŸax Ñ=Ò´%5ìÔäÏC&óÊ¥H•á,ñ­m±+ºaš ×à1‰ZSÃJµZÀv¾8ùh×aÀCØ8š vó »@f C8s"×4m·ä:nÏj:EîÒÒùñ@†v½IE£ ABuÍ:%~ô‡(¦fvè#k8Ë×nëk{3ňBÈc3ê±TÓ„æºÝÏ*ÍŠQÒgÒ°îÕr*$ËÊ9è´¦i=k†ýȪŒâa:í9%m+éGXnïH—gòü—§é'†êˆ-ÄL,ÓÉ9l)ì*a‘¦õ¤ÞetøÖ3•lj Ä¡OÇ‹¾7V«S·6Àº‡ï$F:åᣞ˜¥{¤QŽåDÁØu ÂM]ù‘&Ž44œ¬É‹‰è‚K«Å&ü»?~D1éÓJ£ˆ§ªU`æGŽT§çrùvƒŽª˜¶SÓbSœ.3hŸZ`¿wP,¾:'ÕeIŸCâ]t­¦}ü3P|Ô²»™ç±ý™)ŒB +‹Xý1¤éò-v[š°[‡û8€”&¥ïÖw¹˜/©Õw{˜•ƒX)0ÜÏ¥…e›iUÿÓVA4”ñK(\#ýEÉ2-©cË#®¹RÙΟïÌWÿ*¦Ab¬[ÿ²/c­èk÷ø¥û• ÑÏy¢§im`*ç6—˜JzN€[.äÀùq¢ïʃÏù³ºaŽ‡«¢éÃÕvæ‰Ù¹Žë8ÆÀjh" çsÅ ŽâœðÍçùz?o„Žv$ªpòȺ9L œAƒƒÿ±–¸®¨ÐPçcéª|á8¡ˆ!™õx]]ì‚#BûC‘9ª]+²HÒd);btûTrä¦Z‰~~G@èΪ\Bë!š.Zf·±™¨vd×9™E 1Lí~Ð`¿?#Rm3q@’\)°FoQ<Ó#Oek×ò… P7PìψŸ±¥ÏÛõSÌ©ÀJÓßSŠØ¨®5Š ¬Ì5 ¸t÷^o÷Z5rÛqQd}EÇî3Â+š:rÐ5Ck훨2pxÒXåè¢&mª«‘ +‚滚Dz˜Q·­øÞ£l00$âÝJÅš6e÷…,Ûµp6‹j¤è-u¤ÖúgÀó¾ ¾õ¾ÕÒ®ovs)T÷§>oN|·âjºV-î  ËÂ{v$P’Œã±í‰ˆ:œˆ&(Ú1g?|â+¸±í]s ’’Œl¡ÛÜM/†Á®h×ñ: +‚FðoÇ“;tºt‘@?2ÑhƒŸãÉ»¥ î+JU¢ï’ç3`bgxdLA¯Š5º68«®Á‹ó]ߤ×LëØ/ <¿|ï=ß{Ÿyï@W"ö¼ñ»(ž5òÊ<ëéAŠEXLÎï¦Æ‹7ò=å©h|F½!’ +f“GFßd'¬ÐIÄ”Ü[Ÿµ(•qFä–úáuCÜä®ßì‡l)-njN´9£ ÙN%õ#ânvH¦ ²GŸ·(µ7µGiP<ãÛ:wdó)3 ›OK}],Üo'Ÿϧjéêã×­ ʇÿþçŸ]UÅô™½žÊÛ{ÔŒ¯Š*ÉH²rܼ}3Žd}*²íƒ´€®½C ßyÈ}¢HKžŽ²PÞ÷sSøQlTõþÌÎúÎÎPvÑ+yã)ë\Ì ”ß hcl²Pf1Úãø¬ƒNSýhk~#7mXI ÕE‚€ Ê<6îö:ÒÕÉÅðâÚ’ÿYÔ85“úqE%ÿÚ¥«Á=ktH´kY\p?—‚>± œ=.aÓ³(Hýª½5ukÜ„/Ó ç +XØ©±±´’÷ É.¼œ +ÀÆ“òãç=ßMCJNº#¸BW@ÎüŒ¸d!ó²Ë¹~òÖŸ£”? †Îj @´Àü•Z6 ‰<•¾T&4Ö/M‘á«D4Š.ý|’þc§kk8d®70n¡†"¢¶+E×v š‰˜w6€AqŒH6ÜdÆ´dχ,n¼Õy7`³§Î´²ä8Zi+âP{EXm)z®<…»±N_=C"({¬ˆ—‰zwuÒãùÊã×ï¹P؆iÅ©ãý¡Oƒ8«R(·:å´mAÿ1¶:/mëÀùBiª6p¦Y¨BÙRŠ…O‘©<\*>½½£‡Ï&(moÔýµ…öÚ·çäÚ !ª˜¿—B4+Y’® O¡p¥œÜ JîL€¥(‡Ì‹gÙu#>fÓ#3x©¨¯¥|ˆb)í™SìjÂõðÛ§ŸK•bvüèÞö²î^>‡S}2|Ò§Èð雈o'Á÷(e¢íò­wi=ˆg)„{Š¾1mi‚æZÓZi:œa¥Ð1ÁÖËM£ó“îÖnzÐO{­œBPØ–sCFoý$¦øugDχõb÷VÂi­®• ²*Mf$«Ð¯ÜþÔcœ(ø¼¤!ÀÒŠmD@”E ž×[™ÇÈNQø)ú+ºõZ¸æ¼›‹¡[›ú5ÖßÌ"û/³¬t!k=Ú;ì5ØØa;ÆÞО ð¼°—ÏÞ8TL' @éPY­›ýˆïÅÿÇÞ»ö\“\çy¿€ÿáù€20¯ºÎÕùƆ)1dÇ‘=¤%")04ÿû¬ë^UûЫÍP q4&Lñ}ºvïÞÝÕUëpœw‡§¬mN c–‘Åá†R—ÓéPö"ïe¯³1‘Tôš“ýP¾œü[h2ÑúVŸXú½§Ó#% 1ƒ-Y¥;Õ%Ï5=?§È¯¾Ì)X›$õìÅI}q¦ŠÃÁA;lm¢{ž6 QÑœG{Š‹Ú9Ðkq,ÝPôA½k‰!ê%È¡“! +»¢¿~@Üv@¦tœNW„ë»@w«¸ŽÑ%­byðˆS*¶Î‰DÏþ. ›¬yË)#¼S,T©›ÚkŽž›ß;éxiæKÓ$¾%¦`‘l·˜b›¹›e¡,öò÷æ¾Qˆéæ ûÙaJ'hœ0ÏÆSs|"Õt ñu¢Û’ +‡<¡ÿ†½ë‰ò”WSQ-I¸5V溚ê~^¬íåÃäìIVœ™y°R L ‘–cÅFçì˜èïBIYºñ¯HoPÇ©êïT5¿R…Uf¡ŒtyJW:F^ŸÔT“äy• Õ£éß$x×2æ||SQ*¼øõ$}¼ËÃÔ¿µy2ðPë2/È!K*¢ïqáCúv¢¤;)þ„Ëo·Þ~ÁmeéBFÐÄx‹HĘ‚••»]PÔ”}[‘È°¬8Öòƒx|’…E¬SçQÅ ˜„RÕAþÜFLZ6‰€!dULµ–r)¬êKïT‹Q±\}Þ§ ²Zý›{ÝAŽ—–âØÐU€(…ÉÊWÌ{ÌÂØ' è‰FšüD›«Ý°^‡³Jå/:š±Gô8€•Ž~3°âU |S„*p¦¸Ôõâ{*-Hزf$2i`’ë%î)ñF tµ ” uÐI2︜E5Ù:L¯W]X q@q º¼r7Æ?ŽªÕ™€ ¿P5§€."!|¼û¨•ÓÜOp‰Ô·Ä ·ë®PAYOÕÃnΣù1µì †‚ØýšQ*¶UäfdÞâ^Í›IGáÒªÅåĺØÎã":ÇéĶ$b(o¨îv’Ö7¬¨©G®%sºÂó™L::ÁA§:L ÖVÇÁÐL`„°E§ÔÌ5¢2;áí뛲+Xº¼šŽŒR`£ËÇõHÁ}h"!F‰2±ÔðIJmšÄîÀÆò8ª¶>Њ„6_¤r—t鲞LSŽ ý’y«ë°@­œç„EHá@ôDmÛY¡AáOçñÄó`§-òd²2u-ˆ%L5îF¾¡yP×<Ø 0ŠT‹Hz¢'øMW<üŠÏ%S!R÷¤"a-%€@tÉ$ÖwðíB1©cµ©Vþpà.ìÁl’U^óªÍ‚Dm[LAÝ‚CqØ ‰éèûºð4¢Æ"éJՔżOh°ÆÒ,Òçjµ‡AÀ ì³(eR{Çm€ Mp +8Xj+#8ñÆE`„ ø.ÕÞ¾ŸøÛO[ït5Fðcw]Àr%ù‹±ñçâ#ôº$Ð,ÓG`l#pÖ^´×üþ&ß\éuµš¥Ž*Rõö+°jÚîPhY^e/}))©b³ ¦uÁx¯l¦ÂA›~’“F2ÂÞ;?Å`jQ_ nˆÑA1™Ó ÷ø¶O±òØÄ¡ôFŒZwœ/ív”‚‘eŒ#˜¢~[ðô\ùR%¹ÊV]®’Bùµ"ôöϹuSçÒMÍ`*:’(׿kJ¡„—F8†- +EƨUÓ5“™äB!ÐÀ¯ü’¦«ÃÿëBFƒ~ÌJ+Ka\–"ZF¦%“¡h‹D0¤…Ñt¶; ˼ð/Göä¢{Âe¢÷Ž¶C8 Äðp°Q8Øè\ Î(¶d'3Õ‘Çjý‚1yY9øqð”7,Èx¥« êb„ió:õkD 96PªSq&‚2›–8c›±dsE“ ÇÖOô¤˜–ˆÓ$\µcnè™pe³ÛÀÔe>Õ•îØ×´§è—õ= SB ù¶ýèغ^ógr »I!\§mT=-ã ×aï4¶^ ÿ\6ìný­J»@V—Å'€GÏ«q%ãöÏÊÁQ" árë*sm,ìϱùm…1è…™¤×þ…•mÓ¾nÒôcÁKànÙ}×ÞV±7¡ ¼ MÞµlXÞ×Wœÿ8ˆ6ûgw·¶PtÀ…ƒ2!›yÁI /æV»ü!u•Â-î€iÎS§cLèaBgSÝPa°ÊJaDØD¼ y…²€Ó¬WªÂ’âr†Â,ËnÜ25:˹‚Y¯Ž»ìñb…˜vœ8Úãt–R¤ž3ŃÁ GïçáXèIc,{sUÉ| Ï, íãºa[£z£à†ÍÎbGä覠MÍÏMCŒ< éª@–‡È^¨þÍjH`ÁB§¥+é4 +®#eK¥ØNGöðÐÖI(SCÓi/…û²>Â/ˆžŒÛ~™ª¸žEöÇ<*úJó²Óˆsô›–HÐÑ$?iuw+¨Ã„¤ûJÀ†ÎTKš¢y“ŒêäØ7â£ã ªý6¶d ¥Jß‹«ÛŸ2ëÚ¦ÉØW“ê»P·YYC£—ªÒÊ\Âw¹6êK¬ÎŠ£(‡¯ÜÂä¢ìL]{þÚNiœ’#Ø[Ì+úë5JàŽC É¹}¬»sóâ6À0Î#UR‘Úš +7Q–CV“²×Y—Í{£æ»bK¸+(°€¹+ }E¹ÒÀ¼¦/€Ü}ä×悯G]²ª~7Ä>ãp¬š ~¤YJ[lVƒ”¶´Cº7 Òšeï#;àI0ORÉ (×Û€Â/L²$Ñ „€î.tžc½|Q9UùDdY[˜Ô¢ù›c4Ð<%|ú~!4ÿ`ÝÊô‚!4àHœv¸Îý©Î¡EZ°î3V–üœÒ5a%&â”™êÖKZ´®á§‚äy)_‚š« ®†öEwï!‚ðü®§KÜz·—'!ebª)„TàÂ6‰ß¥áX9èK5"ÔÇá(TÇÖè€4šÑ.R«Ü–ý#û`v¥ýähhÒ +w«ú·»{zÙ]NT±–Eå5bè<‡Ì5yE?qf·U“á)ß´Ca+q’[9V”-LTøŒ:i}¨ÃNdã\$lþ l¿&OjRÛj)ŸÂM±¨g_Ž65¼«Þdà§Fr¡çTAˆ5¶Òîya¯ÂâºO ˜aQWj»›äÕ?.¤ìè¶Vi®brÓdŽ2À©Õ"Ä–HÞÅéÖ?µþîúÆ8™ o<·µö)"^@/uâúÞy˜âþ°øBž˜ †{MÜ©Ù¥$ô™ò:*Æe{i$m…:ˆ©ˆðEÍÕêØ0„ÇAjjL•9gÒ?Œ8nê Š¶/ØÍ£Ë/eó>£DùíLná`a3œQ¨}3Bž¶à˜4Hr9òs¸Æ1Ø{ ÉOÑúø¦Óg˜Du'´„KB[…Ô½TQ%?Á½:ÖošüÉÍh*P¢CЀ¢ž™²¢í§]…yFÓ›;€â™xÎVý/ø€ÚÆII¬IˆU=R(.ÂF`Ï ,ø]ª{õå&'¾¶ÄN‘–×–EáÀ&?y «lšÈq0KYPû*³ãF€†"¹Î‚FFLaçÇŒ|Zx·ñ„ »FÓ®o´íã'›Û,Ç^¬‚,íæÉÒ¸4åpü:ZÄܼ¨M-—uÌ´:‚q8õBØ(û¸¨e©ÿÒ3øõ&A/ûµJakv™ºº2£ð,=‹Fˆ`ˆY-Ì2¢eE¥N# ++ãȤ˜ 0 È3Š/ÒµSŒ]ˆ€ A°Ä–î„_ü=OF˜K“㪳ÓÂo1õx@Ъ~õ¡h…Ï]õ‘×&kËX/ +V÷ûq"õg›!nç6âXŽÓ×'±»gõÉÕýµ;Þh»3qG ù—ÑÏeñtŒ"‹'UÚ¤[”Ùþ)Å\Èm¢)I#æ ðÄ. g’GP–µ¦WGª8}UB›¢ }\ÄdùŠŽïŽÃvHw«ÏAžãò8v!¦aU å¶çîµ#ȇ‡~ ¦)·[ö”…ë%‡#:9)„ŠðMœŠ£4Ÿdosl4»…ßçLÛºäŽÅ£ÆlK©ÐœÞš> ²"ÍpDæXŸðPî3¢äeñx8ù¹"uȃ/:çÒÉQ7Œ²Þúpןå§uŠ[àgE4š³Zêxúhӧ궾.—S©g×X+ ;ài8¶{‚Âh"‚ —4‚$IKJÍlOáþ÷RûÒ^ìÞÂgågÝ›¸¨.T±bšä|ì<(}®X„Qn×¥LѾˆ~l‡ ÒcÙÞ>´j#eÆ#ÉÃÄÀêájgÌQBe,É=äLŠsr‚N‘ðDàak'#x1à5n7CQà9ÑícTK.«6”JÍlêzÁÞi€²ò´Ró¯´9•¥ï&µ3[Èμ#c˜Ü̼&…=WŬ@iìŽS¢áÖbâ€ûPv]Šäb›Ù¿óùæ|+9‹§×'ÐeÖ7 +‚¿öƒ ]Ó†»m‰¬Ç*¹ ÖtÛ7>kõ‹áJÅpïÔÔ,OÒV Çâ½ÍÁ¢ª!wõ†©LSìÀa)¬¾¦Ë›àt/6”ó¢BPcÈ¢-ÌŸDk‘ÔffïÍÓž™È¶Kñ¿É‘˜{i;LÇÕ?¸äAn¦”Y²âù>!ë‹wû ÄGLxxL(ÁÁÍ=M4Pu—¤ZHúu*ž.Ÿ€Í˜>@ðŸ§Ìù{mËð†c™©Öt”´l%ã +à ¹ëFr× 6uäç[7N™C×ÛDËgË Ov¿ê»¸…d5Ÿ¸48çÇìÎ U‰L6PLV€Ì«…m¤&$(žÐMªtRC§Ðh1À¡ÙmÝ‚qhÒ¾ÑBÝj¾tOøìT€ U´[¦¶¥¡Ò¹w,^kD0v§°ÃʯÞÝ© P-–†ºäî?„ãþsì\?¡Ñs– ”Àï/ðOAEX„ñJt—”-’%Ç[‡¸`þ&ÅO¾ÜúôÞùAë&´U´óƒ;lß%$H÷¬ç]²fý׈J[A2‘Žp«8 HTº–ì\TBš–7”ÃJúm´®²ž +à–¢Tä‰Î£VýÀèXi(wXm _ÜŸÝOGé=Tô/Ò=ÕM¥”1²j1bÍ¢gÎQ×O"„ÑÐ×A*¹, 6Vè`mwPåÀ\…íïd8ÈéÐЦ+Â#d¢=ÊS³S¥e÷‡ž+­*}TŠÞ` ÆþIò®êŸv*è\Æ©ú]ÎV¡N·H#Ì¢î¨Ôå;.‚ Ü½([Óó´¢BÑr÷FË•f­”°™8dúX—%Ó@—ÿTã‘9–ësÇÍÅ_²îÔ[wIFË¢ã…Ö ø‚s3^[øZïÑ€_U™„ÍÔw +é9I­áÜš",mK×¢™+â« >$î7Ý«ÚéÕ®xÀ'ѪrZÊ.®9ÓSñEz@ˆ˜Û[ÿA“߯JgF%Q’ÆqnÌ"’XZÅÁ‘Û*î@ªÓ“¥*‡?¤àijÆÅ5õÆ+¬l¶H’ä½,©×wº¤˜÷Óš@‚µ{ +‚Òb'ÿ¦ƒ…÷ÅJÁ¨¨VäoBºý’ÊG(„¾Š"æsªK¸L&gl…ü¹±–¨>"œ7ñ{ï2Ì*ëÒ>ÝxȘ»J&ÅYª^îT»|w'ƒlÅå2]Ç”ÌG¨ RrØž°W[ÃÀ¥/&öHÎÄ^ +A¬rÞ‚Jô”†è%±!$GF[YòNÌÐúë€Ô/ €•¿’%æuPÁŒ/:Úú‰•VÉ®vJƒÕ}¾ÅzÏ+›´çt(§lk×"á×$Ó î\¸z¨\‰ø°+¹âaüƒlÔX„ù–rTëD‚Y¼dúa4±x'ÄD \ ^9ã)Ѳ‹ÇV¿|bsémhÞëÀ”€*à&? …üÑ—¬6ç>=7?ج%—9%ÄÑŸ_黎DøЉmp@ +Úˆm>ä! ½ˆÝëA8œ-›"X?ÇÕK‚TxÛ V°¿B„Ã6F¥÷ ò,`§EOIKÒÍ°¸ÝeŸDÞËK +«EzДâPÓ‚öýŒ’ˆ,VL)s< 1`ñ–Fê$Þü%*?í*S\u¢Æu±¸ì.MDw~j(¬Ðo´1;Jnº”׳WÄd—Ò—¢|vÔ‰Óú'´¤Å&”_ +«}¡ì’`x˹ø }ºKöÔ2›ÐApƒ²ž÷_z¿ì^V7ä8âåèåÚÿµßL/[ÓÏ•e¹(kÈÑÈU‘)y4\¢ÏÕ¥gæ7êAö‡a ­®ZjWÞ 2³¹6õ{QùÂ)H'Ÿ–qjµ'h‚*Oq* ÂØWp_þ Š(M9xºS—Œ¶ÞAæVz±Ry÷ oàSºQ}ŸEK½`ôàXlÖt"÷É«¬”u8†Äœd`D‚=BiÇñý<çu:â–žãAPg^§\rºöŠôæ ´vnjÇ£–'&oxŸ¤—²^sn: ¶Ê{†B­=o»Åä…@ž dz½æÒ39¼^¡Ú;$²w›JI¬Ü)=\£ÑZ¡^Òj?1]&·sI|èv²ãp;a@!ã$!N°Öd-ˆúÚb)¬4ø{‘FFÝK*«ë3ÇNM‰á‚hØȾv[™ŸCnj¶„ (=ágò* 漓ðŇJÉN»RàáÕúµ@´¥usjy¦X·Ä''O³/=:ð¥v†„m^Â.0ÈPª|¸R¤oLe¸>“±_-ö$+GËO«$\'aãn*1y8&{j{|“`:!¾ÒÒ±¬®ì†2LO¹¼È%¡~i™t4ùm¯&õaô+깤.Ia ´. ^‹)µ:ûO•àæųÒ’¾IB<•ú+¿½¹Ž ÊLH¬¡ª=¯~νi“PkHršá€ê@?D-èžêt‰š„÷Åeb ìÓs µðÒÒ~¦ÓÊoÛáßfÇÔ<Ñ>K +#ÅHÐÒÊT._$,+©VY¯© öMç»V;ÖJHEB¦QØr.ÿªeþ£å +D‡îä µ»µh’v)\$ [ì+áªZ6ÒýtÆÅdu袰#.󮸩آ¸æš›%‘ËÁJ¯ ”4…¸Sôñ˜½ Ú QYÅõ,Ö1M ªÖüì\q·ƒ¦b=×G§‚½ö 1ÂÉÍwåp䘄{HÚ²27 ªÆ"K;}u"¨ ,;×£<ÚM_TØg=Yv5s©‘A°Z&ývW­,®¬ZŸè‡öìQcÏ—¸ìÐZê>S€À zLÍû¿ν_pAÌnLåŽUïôE ª¼?àÆèB ³’ *å¨'bÏâ%?¶y’:_,ààz¢sJþÆ×¢æa¤ò$Dy¨·ÐrWè†Ú1ýàÑä(žÈŠÑ鬇çê^×Øà•%ÙOX¤~ýd!ÔÄÖ<åØÄ:/ hÕº/°¹®@…pt(„åV6_=›—Q8ÊÄ‚ x,Æ[_:¼m+¢v|ŠŠJNû«2•D,T¾q »sXAµ ˜„U]‹zP’Ô~Áñ‹­8‡£¶%|ŠË]qVU\¥?ëÁÄÿkeÑ÷£X¦•fZçÙ°IáÀã (¾$Xe@yR¯{}~] ÕXk90O$˜ Ó#ð !±Í‰­_Áw{4.ЈF”F¤þøžŽæÍ2¯º" 6ý4ŸDÆT¬‹*Œ(€ªÚxÒD¥_¿¨ÈSJ‚C|D÷æ›ÒVZ_¶ä‚ˆsÙaBd‡U²Ë‚^Šµ!ë<ÁsN¡û9F¿KK†¢—å“Â5Ô%ÏuïÌY†™E£` 0Êñ¹`5hf±.¡m”Jíâ²'b÷Éæ_ð`4ÐwfÿˆÃ#ôžËd¼z+ùB›äû,P|êŽ ÷b®c{%ÉÉ’ 驠䢴oxQ˱ÚsÀ,f‰ÀH’£©ôIùD‹ÊÊzìXù:(€êÂÌ-‹šeO¬ ½?="‡\Èæí÷©SH%(íšüw¸c‡û| gr€é‡lg‰‰ˆ ®šŒ[ٕʲ®=h¶ ×)h¨Œ6d!©ÍÛøíQ °ži”¤åb|úÂÎâJúaWvž +Nf]ÐÖ‘¬`Ï‹×_Ð;à ÙÞ¬E0BJò®+D”_2kø’R]ű[6B yËkYFaõp¹"ïãªIÚÒòÒµG s )£\¥>T%Šôo¢¥ÇV%j„Û<ЦZªsB3rSfœ\¢‚cýU.Êïì¶9¾5R”ihg»•,ì/ð“H],‰ ½¨,»®“JhË›"å{øÃn&iYmt„~ö½åœ},­ù±Õ]}¤Pp9É–%#l£|€w}H.4¦4Ou€{Æ"Õ}~¥‚9Ðsºa>s¼Ù‡‰Rõ/Òrõhl0µŠŸ¡ì÷Y¨jEÏe%¶¹L” Ö•ÆX×L8ˆÈ¾÷Šr“ +1¬© tÛòr±ÞÝ …i`ñ‡ÿ i„•c¯=$È<\ʪÓ~°¯;fZ0 Jì_ hü%]…/ðõ„ø‚”6ÓÅ Jc×&ÁDú7 PÓ„Å!š£…†Ô3†—D¤ÀŠÊFã·eE]y7Rm+^Iµ90®Ë>É9ÓóR”oùMM%ŸŽgvTÝ"Ô–Ê2Ú®I/âv6¹ÔñS¼Ü­0S1²~ˆÅV×JQ›õ¤%‡ÑêN¡®fÝ´ÐR€ã:ìÑ3(j™/¾&ìðÖOÊNˆD³BÜΖ—3sÐGÙ€R¬ÃÏU7– ãÈR”Bqa ¤DÎnÊÝc»à mZñ%l§LK¾»`ÃT=z;YO +¨Ÿœ×IŸ¶úî/ðÅÖf"ñýºóý$·Í§h¿Ø;YŽ±r9=ý¡v¬šŽÿ!ë)m•HYØ&õ¯ró É’éqr–Õµ:UK’:–ž|—‚v¿Öˆ¦»-ã »ÛãA¨.K°7xéšõŠEÑI<ýIy§«˜á†~–„·éÅÄ—9ŽSoÌ’¶’ ÏuM²ÓÜíäö-Œjë p¦f;!ˆü;‹SÙÎ|ÅÌô²ù(ÚÿËÕµ–Çf(© Zmþž+§BC3H~Ât¾PJ`½IÌYÇ.E×^‘ö‰BŒÇSPÏ*}ñG,ÕЋì€<‡Ë¨¹I'»œ¾€HF³kËê^µQÀŽ*€3¤«\‘ËR2˜ÒznêÓËšZìñ{–(.-ºªêà[œIŠÓÿÃÔ ¹©*Lad·p;£€ã0*k9>ÖfUÉ.#˜ù†!ŸVtîþME[&8ýæß´Àõý‘!'(@³ìª»šb÷*AYqP‡c+Ó:ƒ³`Öi$Ö¨ºØ6åÈ7fWmØ¿iÀ³`”J=¶î©.ð v zÂØ0G«‹4 +,HAŸ‹àË8‚˜ÛˆL [µG²d+înw‹B°ÐGçpeT¢Á +]–°Ù­ w9„qÍ ¥\Š}NoŽW²˜j‹d7 +Ô¶ ê›õ@âE°+6<‰Pב‚…@´4P³ §§LÉÄÀìjO°loCÕT­$ûB”FâB”Öº€Qò“oÉÍÏâÉ$@€¢X­!dƒ ¼‡%Ói|À4ËÃX‚æ€pŽª‘*)®OÌu@ÏÇÆ]ãȞ³2ª"3† OuÄÛkÜÀIÊM&qØ žXô(mñT5JUU*8Ui*0”`øyNMaáæ÷®*?呦8b£ßSÛþ¦ºd%±nþM½dáûñÑvtúªd;Œ´ `%Ê%ú&‰Tax掃Α-\¹íòIrìR%¿DÀ À”ËêQªPÚTM¹› {¡pm=]©©I¢Í‰ÝRØŽñi.Ð/Hôܠ§œÀá0 I R˜5øÈ]O[¨Vî¨RÇ)œ´œ†«h\œ”ß$8k±Ø=]u9Š@I?’zç„©¤E퀆´zšÓäášÄù@·j3+ÂÖÎ…”á'N8°2šÕ(Ö.‚ y +ßpr$*eŒ`b«Ö·:¤á ω ½½Lí áÍ,pO)¦pÓXl0] UÙ ²&M¤îsS­Ç–2auo$IZÔû© Ÿ%È-’j,ù`+…"’¸êéjG¯Xh7Ùpg$ê--ëýƒ¨”µ/IÕú½–ƒsIÔÚnÖê® +÷RqšçÝIKM¸ƒùs›#þ +H°s0xÕ3V…ˆÝ¶‡ÒØéVt'e€¥ÆöÐÇ•PÂg›ÕýŽ´Ìؘ°á_<ÜTV#Ä™УÒã §‘ArG£céÈÒ÷©p–‹ÿ+Y> Ah!TqüAN3 =D~A %aX-Ö‡×Dã  !4Ð3Y(WÒEöÁæ”%ÇñvhÚ5$š$ ànà,p6¨^£jÁAÖQ&„…]§–[1ˆPaËú%wo®ü”±ZM“Y7™ l/øÇÒN¶:CTÖI´'ëÔLñZ*ÙÜ5uµùiÚ)•¨¹H⫪‘M£EcöA~Hl.f˜ínñ'”H.¹dƒ%=5JÐ VCÒë„2ÍÏ#.¼¦¥«YOçžØ`.“.ãrb\˜ª T¤\ýhªÔ¨xåZ<¢(ìt,©?!ó‚ð“ždYýíBÑk!ÏâYÓ$?WÕrAF=©¢µÄžtÌ¥Ž‹Úáô¾‡)˜°"TéôòäY—¤*~0V–…HØ|¨ðV·‰ŠPí`L–%&T½àâ‰rÅIÆÕFUË´‡ŒJúuÞ[¨£$6HpJóøê í!6†Ý3®^½¾!ËYq!Nåzà-åŠ ÕyJ6  ç¢?‚•º„zCÇ¡ÞÐ ¯†ó:TUc Y8_wG^°Ð¿Yé¯(@½HîJ Ç7g žn@¼†öP²‡´%c¯Z|E>'ác[“Sj“|¯]Š„U¢tè1VÜŒ©ÃWWy*2-âwTÔ +.Ú(JtT˜ +‘5N°T—ˆMR‘Å}°chBÉÑ»Úîà„Ó´|lä¯q.ç_wÌI©.TxâÒ=dzÚ,OÞBLH™c¯®ž“fsÿø~ôCbœþÕ§¢䉂3§ÿ'é?w™âÌ ³g“j-jÏã›e/÷¯× -…@&¨`_‰#à9ŒT‰Öt•t@û¹è^Éñ´RêNÜärˆ]è>ŽYåC‹k¬ç}Á¾¨m+ÊôN+:åì|JÂ[í¥».;¦Bxë¡ÙM8¸=Iè°çE\ÍÔÄ`t×C–H2P‘}Q…MZ¯š”Žä !ËœÆ+æûõ.I;/[uˆå±õxH/®ÃôQ¸OÓV}2UQÜu@ÒðÍ_FH^Òü”¹ Þ¡&úñÎMOßý ü—ØŒ•è0# +|LÈ÷nMïø¦¤Ú/Å·ÇêXu­7À¯ª¶XŸÂÍ0܆zô¾Œ±7C5ÌçÚYh²gä*Ör–÷¾Õ8³@1¹ŽÐˆ%qú‹7úͨ¬R'tNÛgÆzhîdR Ž›l¸ ¤DeG^ßG5mÓôx¥^hÙ?EÝ6Q©ÖÑWoø¥K@bAs‘+“ð@é 4âDf¾|*¹Ì06Ü‹‡Ü“Š1þ +tlÎòQ÷Ç“oÆŽg9iÝVy^àràØ.Ö§F70B+˜¤M¸¸›.#”ŽÙ0ëáÍâÄû›TͳQg^—Ñ`úÒþÊô )“1ÝàÂ<‡E#¨Ñê qí@Ù]»Ì&{¸/•C“ÀYó©² +ŒEÕn7R zs .Ȇ[–¯—à:ªŸ’ÏdšNe¶pCÜ>‘—>ȘáIã hï ñRí²µaDY#6ÎÖÈeL2OÚºI¦%BÒA#LçìñøÝ„ £NºU¼òÀ²Å†Î«…"R?ejª ‰è/MÓË}øfÍsòBûi‡Š½Ú „¤ B(<=ìK dr†­xå +QǹPe£G9–…Ž¢‡ÄdDgŠb2¼JŠb0ÈÒ9àÕê·û…i²ÀÕy}O€¥‹›6×UW­ú8†<>BhA¼ÄXFéóÌu Hƈyµ5¨Â„Εƒ©íDm©Sühq'æ?ùôMÙ ê-/ M sÙuŽ Ï=ŽðÚÏ¢»²¦ÛóÒ© …J9E<SUé¦ (dzW‘ƒ,x<°Ô-®'°Ÿh³åô¶‘…;ðÒ@cb„ºù1e/œu&bJÄðßxý£ñåé+¿x«€T^—«;R8¾ ÊZYÁ^"‚ítœàK”xrQ…atb¡8.5´“$FS’§0³ ÕÒvãBífÈ*æEl¥J>ßUë»ÄÕ{‘é¦äâ1ñ=ŠhƒR¾'R¥¤€²D"]Å(ãZä‚ÂJw:„¼cµ¾éx4J‡c»T‘m§«Rèõ&ìG‚Œ„ + o¿ÚçêôP¾Åäeg°!Äà:“¢¶Ú%KS.Œ"Ç©¢ÀãaŒ¾ È4JâC„kW„YDuû;nÕ4 +Tg¢Ûv™³9C\¼ËäÜT)wQ‰<«û¹he¨K‡˜ÚÝmvˆáòhïÂ#ê«Ìáþ-“Ê°$>$怿õáçñtÊž ·ÎéÀF º¾‘æ]-)hÚn tº¼½„œ"$Ý-ãA”Ó«¼>‰Öàxh÷®.l-À­z£K â)Ê«>»öò u—ßQäY±åów7ç¨ ²ýdƒÚvs`Ý9 E,ÔIºbàš¬žmú”óÙÔ•{·eùRU0TÒÅSÅðç,`€ìÉFn•?Ÿ‘E˜ApŽ&P'[ûaA! ú\ȇ‚OE¸_þB«ñÅ»XWY®(ôfL×uà@«2-ɸaèÀIp˜$ À²C~îÓId…ƒQiÊ:ÍôØBòMt¨~ÉTTæþe%•´ÍV9´AÀ—ñJB +ˆ€!±V•X¶SŽñbî™4<û„í§{œ=#i¡4ªn‰}:ˆî7 +¿çÿžŠ#uPZvvТ,¸ÈÄe\„òŠÆb‚AB?ÛRÓíâ¡|ãkŒøôzØÒMƒXëFî‡ßV…£¡8ƒÐÃ/àzàÛÝx€îCsJžö2 +~Ÿ3 1G“ úpÆ2r çä0ânÙŽ£:X…,VzÅó,å%`.®“ä:ðIAXÛr@Iî€"X{]ÌGeÃ=½˜€–ezŠ¬ƒ®.Ìß©¾Dɲœ°§6d'VÑÐÊ✠²¬2à>ÜrfòëËc7S…BÄœ¨r¤Ênˆ­ùzà+=ö„Þ1z7—=4–C§.,¨àÓâ'‘l€W!ñf©ºÄÓD€¬Ì-ê¡Û¡¸%¡¥=ˆäTEÓ¹N#Ä ½Œ±F Ñêþ¢±FÄiðcF¤óᶠ[gÄ=•ž.Jâ¥UgXÐc@‘«áÜXѨ˜ÔA“ú)$û{óA×táj1ã —Ú¿…´C\Vw+Q¼“#3)zÈ“æ¼×ŠDõÖÙæV…7?—½!*ƒr1,^È?V¬p‘Vmnbƒé~á´&¨hêß7늢ÓKEÒ¨¡ðvWÔ§©Æ(˜ý:%A‘OdD óœîê™Õ±<þät)PF´—/Pä{R¥ÈŽ Š|ôl‰öèf„\I ÏÓSÒ°¿ñÛ]@X0ä v± +gÙ +ˆáèŽ5+ZCïë[“»ˆ{ÍÖb1âÉb×»9ËšÛv‡\® V +¡#²È-ª›"»µÕJVHIG£Þ€€Ì|›2/Ôî\á™Q ­ó*­,.‹ŽãÀ¸6ÅÁĸÕ1YøŠT'+£8ƒc°X7T}q§ú\ħêz¦9Ö—Ô>ä9Zm°qCpâ½CN¼ ^²=U˜7œA‘KX´ÒêíŽêýpâ|±ÕRïi¯î+ˆ+ È*F$aœË)y(Ue}T -Í*pPJ +ˆ‰'¹ sÈbØe¤áW¥K:Û£k„ËP!IÍ+Ì9˜d$Vf#\` +^×±er›“COéû˾xªT+€nEÔ_ðôêÀÂ0€ÄÏyb*ã ¤â.í7š/'@M7P³f¿í³íQM£Økº°£,"îÙ]õ¦#yŠZoБ‚9Æ•0kOÀFéù=Ú誉0‡Ž=½M¾@Ìû‚’ý‡ #Èä§orœ³ âØ€¿eÿé"$>ÚE´»Óu,P±ªÃ…åé"»:wî A‹‹ÀXÇ +ÍòJAÂÕìo´Å¶°ðõ,Ðí^¢1TÖÖ¦X©¿ˆ|ëÞà–2É¥Çÿî ã²T +aU}|mA–©ËŽ¶¦ÙYÈ’E#JòÈkÛˆqŽ +‚A"}ÂÊ_¦wÓÀ… H +JÜESª\Sܽˆ5îEþÓˆî#&§˜Ôj–Xê åÇpžVœ]Ã,/-9KðAnÅïƱIô:Fbÿª­$óî+ë=¨N+#×!&†òÂÀqËÐÏ2Q{>¹ÎÀU]vݲñè¨Dû å€g[T¬·ão +aeê}ä!ÚéPѧ°KÐEȤ=Ñwð +¿€ zOW£’ø°°5=®W;MGÍvèÇ*ñâ2ÄC¦=©ÇªÃÝVYØ¡á!/¤fƒ—ÔÁs#ü¾¯H-|¼N„§Ø>D4«^ÖBPŒH¡çÚü¹îþ}”Ðö<Ô%#ƒ\×Yý:ûC“é'Žò:ïn Äâ Hm)±°qæG9DþˆÅú|pÈEuö*NS%«2†#² ,sáG@#$/ON9©#š "«ÐBæÐc2ŽÈÍ@]?ÐØE‚QÄG ÐSa‰¿H[ÅEe6$DŒ4›˜ÁÄ7ÄJa’LÉ…ñ[M¶·—m»,®4frÅ=÷?]h„#ùyrK.½>U€öÚþ.,µ´ú<¿6Ñ.~°˜Î‹'VûÁáÆÕ®âJ|àƒÑP2ešÿŒ(R½DúîÇ|¾Ì]é ZÚh³øâÌv"!d„IEÇZê>0’Œø| ]#í?³Ðè m+ *²ÿ(GGå ·9å#":£ MXsáSÿ\öXÁ Î^œÀýf +ø%ô›ÔÓŽÌœŸÓ˘‹jj’¼,ÑÞÝÑÆ ÛUxGoé@; x,r¶»‚~J< Š/’¹Áîp­c ¦£Q@ÆHwÚ"%ð&ë®#Ú'$ñ¾aßîE7ßG!˜Š_[_a¶šd¦u@žIú…*G’#HxñPbÉí&D…åõðëtvÓ‘ºäÒä¬W…”£¤ƒzšã—B œsœ›kw}´M=ÙóËòS…C +ù>r¯`rã‘Ö/ïø»’!¡ BÉ’’6Ÿ‚"L•ZInJQ¥µwØfv¬ÆµdE±«,=Žð)µ´ßÂÇA€–±ñ¤púØ5ûô:±|]† ÓØF&ÖáòÙYžžúð4ažePˆÎw¶…W]yÚ±ÜoØOç;S¡(N¡Ti˜5æ D·g.ûTΣð¨ñz,y?‰Qœr,è!—à >0bE—.Ã2›Š>\ÐÑ×€áìÁ?@HmE¼9MZÊ,€†Ið~8 ´JnFlšºŸÇ† _GA èR/1GDMÑP1'¾>Ü€ïTaDŒ[ûE§ß©,Š kíØH’L'Xºú®àì0IÜŽ’Mu• à’#`“Ög÷ó£ «Ԥĕp– Ä™ôø"·¡¥ß—ý¡¤– ÇÑ¥”¥!ÝÅ¢AŸE‚·x6èÛ2™Ù_@>&Hœ½’‡+6»ê¶œ'‡3v½"#@€+\kžSÅ8úÝ4h% m3½ìoºŽbviD yØ, +%‹)5ɲ´´ jZCˆ"AvX[á‡éQ|pTÂ(@a®•AqДË6ï`CŽ>‹ý&ÝÂo™M³ ‡ã˜.•çeQú_¬ë9ÝËsŠµù2<¡®ò%¥ÍÁC­(cŠôwD™(I6×j©q°Â™·%)ALi8ܽ"|iW.…ĬýWøHÂ5!î’oÕùEÅK¥¢Â±¹7á·“³¼,òÂŽg8}ûXØAuÎ0 +÷EÉ'$GWÝ@§Ë9,;¦Ùý½± @ò\ѺͶžÆÒ°nKÃzûÁKh;¡)ôôÄE­uHžîn4%ž‹Ž°¿Ôé$Õ%g)fvé]¬º\¿ª÷¹àZtEuþbUXR+gž‹i$8¡JGG,”¢ì–Ì¥¯$Xh•’1­ h­§$Ÿ®þÂ+—czž‡ ï"+H_E±ÇQ|… æOäq°iÓè¡ aïýq0)®ái-”BnŽ¹´'$äÔöÃkZ^†ÝUÆPh~7iº¦ =9tj?Åáü`qm?åÛþ‘';ð…m¯„Zý_Ò’ùøéŸ|üõÿòù—¥?ýú·¿ýûŸþìÏÿÍ/~ÿû_ýî7?ÿóÿëçù‹¿ûÍÏ¿ùí?ü—Ÿÿö?þüÏ~ùw¿ÿ׿ûíúÿÒûOüÕ¯þáW¿øý¯~ùsûŠ·‹;¿ë +.ãþì'ÿÝÏþ¼üüÏ~óËuvÿ÷׿úÛ¿ûÍúËÿñÓÿù7¿ùůõËõ—ü'?¹ù›§ÏÉHŸÙ¯1!Ñn®Ñ¼GBh™*À„°ÄÆâZÝÄŸé¿ÿú?ÿÄ9͛ΜÞoõÿ`ÿóÿ°?þgK?þòãýߎ_úÿJÑÜå®ß$|×w^Öå0jÿœ¿¸û¢ôàíùó½çsâ·¿ûù׿øöÿ´éùó÷wÿ+›ßþ>ðÀÿ‰ÿfÉkK-§™ØØ–’û‚¿ÇMýB WÒŸTl!Á”E%“H’‹·?1õ+*Y*æ¤:Ã\6šð”m)¶­È ¬ iu·ËñE+:°\2ÛVä +.gTÊÓ“–ב+Š©ˆJç…>$+5>«¼!U‘îÙ#_Ìu^ +îMB[S^  f‘<Þ:À]S]YM)eÙ'^%‹ž’EXçÞÀ£E%˜ÍD‹*K“]e )VËZÁÊâ0ôñïuš²L€v8®ý?‰xÊOïÚ߬µF­ 9àÃQúã‡Ø>-¯n/ʸ—ÐÆC+ªdÏ¿l[J€:›\w ¹dF…J±ÔÀ!ŠÜ‘·i&XþÖDÂF“jäÄ5@÷%'—-¶ëW“d ;•ÕöfñF¨K]¯%jvreµ}Ý“qàS$z(hþ³Èýd¬/jL<™²æ)ŸòîrÚ•­‡&u.ÉÜ{_NæÉþ˜ÎS¾—KhÎò‡"iðr˜+V*¹ÇžB‡ƒ£$™y{šän ½: äBÌ–Ø’‡bË‹t!¶å:ÄUønø†÷u% ‡–. ‡íŒjÓvùÎ =Õ3xyA}bÈ*Ç0ˆ>™‹ä9ÐU·h´p¡žÚÒ"’õ¡Lu•Ê7Õï>,ÞtkoJñ£ojˆR ÀöcéŽ2OP79§‹IÕ!ßTP4¾µ7Ì[Zv­âZ;…?4Š +rT´ŠÛC*iÂÚ–p¼I ·ðcqêtýð¦€ÉŽ²Z, psEi¾*eo úôË’œ–×h{%_V=hÊëÕ"ëC>Uá#ê ) í„*œV[šØxª[ ck +7ÐÕóT™À00·^³®¶IJ¼¨/éGá½`ˆŽæròêz +g3·y,Û{1„àÿ–”Õ'¥µUEn(5O*$I#|©çM;ëú¦Ã$9ùŒê-Í¥ºåx6—Å.}>,¸ ãÙŠ2râ,h ï×(µÚ23—4ðO›ÑÀôðK‹¦ØÝÞDb\(?]Òàa¼øn¯±Šï$'µ«ËA‘»NÉw#=³¶1`È©’ÁÃdôº³mUEÒkôæ2"1t½°u°ÿ²ÁW§÷XòØDÞ-ð_£ß­¡^•Þ`ìRºT角EuhÕ¦aufÉæ¼'ݪ"µDÝlgNë—È{Q²d¶Qq­‹¨ÚéÈE8-lw)Ö¤ŠÄìBïîǧ/$_ðõP|ôˆ=ÿöw¿øåßýê7¿·,ôO>¾"¢‘‰ˆÅŒ˜P² ÙŒ Uøá¾2Ô:K~%7¤÷àÈ5ùˆ=ÙÜøøúoí;¾ÊE¹ßp™h ÷¿¢D:З—š™:‡ §HJ~¥*'5.›¸u}|Eß‹ÊB£ýM»«n°KjðõQìúkÖ™¯ +ÜYÝìÑC±ëËXTfÄíaä|÷׬ ¿ 7h‹pTBíöóÇ׬5_%Uë€÷Û³÷ð+ºžBÉHÑ I¾ï÷ÛNAr‡‚ÓßÚfR‰«¸T|ýþ¶¯X·a ZÐèi¡†–…m$Ú#þzgŒÇÇ×_߇Öúúï?ýû{‹¢û»_ÚTò\ëúW¿úÅßÿå/,Eü¿mðÇO¿ùÙŸÿë5ÿÝüíï~í‡VLnáû/û~õóŸýùI’öoÿ_,f~ý%£Lž?j~ç`~ïÿÖd^÷㙜J-*­ôŠ®,#臲sã’Á"H“²ú²Õ.Ûl/jþÊöwª-äHD2'Mj²`ñ ´Ùn»-,I;ŠBÌ‚WeF×çBJ¸{–@T_ˆÃùÑÙ1ˆGŠ3ÙT‘a{·¯w´,VÐwáƒ21| +ëlaz_ÖÈ)GZY§öUùDÎã(ª~Aáñq:Á+ít)ÖŽÍ>U’€zŒYãÙo»+LÀ͇yŒÆ©þc—›o¦)ê•ÚºÜI„½Ú8¼ûÖÅTTLåüä¢m?Ýî¸ÀLÕ·„è3,y0¢Û€=(l· uI_ѽñý¹Co#³…L%…ºFvùnÿ‡±D$º ¸g߬žŒ…Ô……0‰Î_Þ/EzÖjQV¢ 1Pê/<'ÖNvÛzìâüƒîçŸ9è+~GÏ]#CxNùÅ®«àЈA¯ È»ºšçI)¿g]õØÜDWÁpÖ¾“† ¥Î¢Ä6¤…&…gÚ8f;:\4 GT ZKXÎvGÂv RiHÓnn8‹…ÄL¡3gíi–êd "Æ]ç²;øéÂ&¬Y8YSŽ¶¸þ‹ÜS% Ñ%2 +¾®¡4”¥þ Ä°µ +'7ì¨Î^¬`½qETŸìˆ1Rv$Pµ‹•b˜½ Ñ(Îò÷öø³n^¥Q ÄcÙüøÌôZÑÏ(ºÛÕæM<@õ0—}ºëAO{»t7ˆ¸±)}ü±:«¨V‘!cŒY€ÔP†Ä«yºœ¨ýºÙ¼- ¨I§åN¢èd¬îšçà­±eöšª.T¼ZÅÈ۲Ƽ£¬3—@bB®g¾ãKE=¶HÈžü_Ú¤]¸—†‡§þ!ZT]Ï¥ûßÒé –a0΃$ bèñÊè}Òì!Ý8 +ÅŒE ᎠÊfËrUÈk“ ó˜’W¯e=µRž=»*ì + AÑ?µÁ/¬×YE·'ÃK‚^FÂX•„1]0õ.¦1Ëq“M“DR+ž X(±TI~Aê›®<ªÉ§í;šŽý±WHn ßÌI²ccI<åÄI7”iÞO»²¬I™ _¼Ö>±–ó”Q]2^KíȺ"œÀÃÅÂð K8˜StµJU‚AùÙ†ûŽä–áL§›âü±.*yxÓiõ=†C‡Ä²Y(&4³Ÿþúç×K +÷i  ¨4â.™|ê—·¹\9pzþñ~»Ö{Õ€nÖþ¼É#;¶´ÁjL/ƒú/¼‘&Wã—ç× ê\„øéù¨)»)<&_¡¨Å¾Bþ‡¯sG'“»Npí9㸗™Šrƒöa§ÙsÔ¶ƒõû-¨!«|›Ù28G+Qòíñ>4ô(õ)¤YAQ®÷Å-|ÓІ‘JÉËK×X^áv|…¥‰ä¯ªª­§ˆ9ÝîÇËMŽu¸~Òë’Ð$!dÑ‚­ ýeõh¬ý(/ƒÖ8}­7­®Óp<Nå¹JéÊ«”’Ñ[8k…z¡öàÁ`f¯†|¿Œ=„ÕEÃýe '9¸܆2¹Ê# i¶¶WèFÝ›,¢w¿¬êС>Kgfo$¯rÅÀÞ €õÞ<øV”šÛé:ð²å´´¸z¶w#Wÿب(ß‹ d{„…Wå±µ5!²©§%Äoû`“øXVÑÂîY›g#$U½T‡¶¾Ûr¿@nñ¼¿íÑ ñùIÄÅwö¶£wüó†t= ŒÊ$D‚Ï;pð§9¼b Âh‚nöh£Á˜B¿¿ÃG„}…(Ð5TÀ]Øv¿G`Ó$lÙdöÓAŒïPȯzøUÛ+ö71åChñnJõlÙëìâ K+þ°¢3^xíxá´}éôtÑ ð4ó 62Š$š }öôˆÃ׈3Üq*5zê嘣i¥Ú‘-ëŽr/àÎS^Ã`.]¢O!Z¡3ϵìúÏ—P|ÿmÇí¼¥r`nBm?#}îN¶û2v•×ü€ 8˜Ý',¹žŸ‚³“¥õYâ3 iXtlñþIÓdµÌ"ÜiÓ#¢Û¡™D!¬ÆN•³†ÇkRh(¼&Xü8uýš´ƒÎGZÆmŸ89Ûm?áhìDN=m¢¨´û<{#u–&ú#ylrqé,Óæ¦k×ã© 3ð57µhÉÍÈQT‡ø°3Z>%…Ùœ­˜·IaL—(Í}þÜV ¿³nZ—R´­8¯¯4N*ƶ<o/o¹½mNë +¦Hû«@Ó•È—j­W(È‹€ °Lò/u€G–Zˆ]§[E +:%.È`??«¬ÞöpÎè½V@DÍÅ t¿À]6XŠ‡Ìéußí,Ó×n7Û[q†F)šÆìxHt?J:´>½S-²RyØ ºô¹ŽSüâgá¨mÚšÊ6—]nâdÈÝs2$E*š +ý-@lH5¼–µÂ§v1L2v‚6Y„…–ì.ŸqßØÍ^kmÜ/v]˜S#USÒ–>€ˆ¡¦×ÔáÝà¡Ó¤¾Öýê†ûw^=7 „j uI]>ÁŒÊGÌbrëéº8¶ì±ÛuPä³Ø°â¹œ–fE©ûIõLrUÈr4'&”$eJ<]• »ö‹„^]$’¤Œ^×Â.K™É·Û÷·¨«ùˆŠKæ±CóØ!‹ï¦Óɳ]]ªÛ‚% +·åì ųq¯[8H;ñk±Ù´ÓÓš‘°Î ÚMGÊ”<H›´¦@3üA—9a¿ù +à@„2›…µ¹š‡ªBƒÆ1¼*º¦é”²0½E3CLçDKLˆ•¾šP×gÑ>â7g¢héèAIgáú 0€-ËöØ:áNH‡ŒŒ”¢onhî_,^rÙ…Ñô7ŠÊ.OƒL’?ð,ñ‰òE¬1.;×›™ÁO*5i¯™qa…-?œ}8»IÐÛäL³7‡Ü{7·¥D†ò2Jdç¢Q…WCîð0åЊÞÔõíÊU v°‘rX‹/©lËyv+v0¾ìÅ] ¥J©|/tßl"ŠŸ‰îµšùÀÐ^`Òüœåo+8Šr”LyL.‹}?‰Â*Â1=¬}4'¾À‡(÷‹ ýv•a±•0pi£ƒ=x_©iŸ[¢‡^¨}K}"\Þ–yqÁ³ÔêQ~„‚NŒtnÜž#Å ®ßQIÀÛ®”h´à\ÏÓGØÏhV£ƒtÝð³ßÀ•Õ-ÒvP€ù¼S󧫯^6_‰V·ù°w,ǽt2¶ ¥/ýfáÞ¶~FI^ݾÆ÷Ç%fÀïc–ã ºõ"zÀûÛg× …›,„ÉŠf’ÿ!¾”®0ó +˜ Å…C­Ï.q/có³-“<ÜkF›žÂ¦Íc(17$mG¬áh¿Øè;îÓ[4Ɇi{¢_EõÇ1Dèè-“ó…x“ÂmÛaæ%JÕÚ§ó³Â+¿¸r…éÑIs‹ñ1?‘Z$?ñx¤ã×ðH©JŒO9ú™Ëx—7=öª"ëxz®;tÍ ä¦›ÅQCÛíü)E’9q…I ¹ÇŒ$ŒøvÅüo M<ÏJÀ¹2o`ŸºdìNOÇ[âö…mÍi ¬ù×d,Ž¸ær,†ljاǢzM“Ärðm9éýܤ˜pÊ䞪œ²G€yA»c8aé6å•tÔ—½CUwú’ý +O9©:8âòš<³<‰Ê„&ÂØÁç5÷–hs[¢ÍHF‡¬‚¨ «t*‡º$ý,ô"ñÙIKK%” h2ãhf­³×j`)P°€¥Né]‹@D±huÎeÎJò(œ¹H’ÄÂP[(GFØKá̵¾Âz¬èµ"LÔ5¶Õ“¬èZÐa+#ê±Uš®õ ‘ÁÓ[½‹ÂG"ö™Uªº”ËqŒªØŽÿú‚KµMž«MÒÙ#«> +u´Äô^GWŒ})óÙ@8¿yD”¯µÁ´´2ª€ÉýãQMÄÿQ;a‡8tWƒ”¢„έ5”0·X EO©‚Ãʱ”IÂJ×’©$]Ž&Á–ñZéz«¸ÂÙ‚œ +g++—¿Ôj¡%¹,“Ý Õ?®•^AC‹«†·´c¯K˜Ó × nèhøµÄì“Áb¦žÕµB­Ki~)Ráð:ú¥¾ Õâ=&'Êù#”ÆáhªÆu-¨+ŒÌ‚ áÅ^½·z< LG0¢PÉBt-å ¦Ïþ0¨¾\ò!%NhØÁ×G]쵉 Z¡¦Ö`ËýˆÝ’6æuU{*4.€þMý͇¼Jvi{ dÝÀGÕAÍÃ~͵cÂm—ìüµÏ‚K´f˜­Ï¥ìuûÚ¥?NÈR¹%Ü4…xKQ#Ý8‚è¨"Ÿ[€ Žºþàk[ +hÚ˜6Àîý»Y8U"9„´èÿõ‹.Í°ëib‡íz!qÄÝŠ£Â¹6ÿâͽö?yHj8î'“aRìfæíœÚÏË|\ÝÑë<Õ»—!ôcÃ+uí䆷24‚?y»ßÛÈ{mmç°¬„®õíòšÞa™ ýò°T®&ûÝ:óa¹=ý°âHÀíÎ× (bÖÖ w;_@\wиn¾xñºwG¸ÆuëH> Èmp&!œÙÈ”Ëmü@0! ø™ÁÐÍmü0;!ŽÜ(ŸyHP\¿¹í¸7ÀBØü]ÖÅÂï{ +‘{@KÝ%jòˆ€Ò +¹HyÝæ4#r£/ ùUq—§ÅQ—to£áB¢x…ÐÝæ›ïà»´ÌÞ÷q“7ߌº¤Ýl2ö Rñ6ï0Çx–+B2Ô "Òò®–áèÌ]ÿ ÎP: ÐÛL€†RN@Ÿ†rмúRD +H×Pƒ + ÙPÆ +ÛÛ*X€è†jZ÷†Š\ ßUöÆøZ àä]P æ»rdDïbf€P‡*è^ßQ¯híP‹ÝðîPÆ  ð»jp@”ïRò‚ŠÐÀþR¨÷P€ùPDxûÛb|€ë‡’~@úïn@àÜ6âǯM‰ÀL@l¸m^Dè¯Dnŵ59w-žN¬‹ZœåÅ{[—V‘~5Ñ1¿ºè¥»t›ˆªôˆaä½N7]«NN9l¡³eΣõk÷Ë—/„^Ѓè5‡Ú §A7®‡\á¾×QÔ@àZkŠí<Å®ÐfQF©±%h#W´Ê²NKæÚZô‡TܔĂ {ÆgèPöéÁúÄÎ]–kŸS‘͹T2³<‚[ì—Ö âöVž­VÛbÜ()&*Q¯ Úø©k7~óµG~ÀµÕüíím¸¶¬ãí¼¶¾…¿>ï›_ŸæµÝçÄ]Û>L­k÷?ÌÎ “ûnÞ Rˆ¯ÕßðÝ*®ûA“ÿ&xiÇÒáøúÿÉ=Im–?YCÒ÷¢¡¯ ´ž]Ä¿nÀ*{î‹x†^º" [ƒd™X,FC:=’÷5YRA™Ÿ´Áf¥e[€ñ»«äfÔÄd{¡ €Ô‰ž;éãî­½¢y®’0ú»çxT…uPUÃ¥XQV‘j èPK³œ²W¨QUTo«+i*ƒèîêÜÑÝíA:bI§ñâ!žfÁaª$ÙqO˜R1W[ϲºqŠ¶r¸…g¥FÒ'IõÑ0Zís8ð1ÊÒÜW‘Œ+* …Q~ÁT3Á¦b[î^ÞV7NUµj e>=­æöVÏòÄŽ&VåzÉ^¬ÊIš¢Bhª~ÒÏA@¨´Uª]ÀX´©zõ¨ArÀ•ü“›4¨—K4+\64¥ÓGgþ»r9Nºì–Ì×¹pGØäBÌKò‡8Ó7úáØäånTgôzpÁÒc:äwѸoŽsíÚ°·G¬éæÅ–q²«YègÇ]Ðä±I[ãypFÔ-¹^ÂR²»ÿ³{«%ÜûîîmYÍätʈ"ÜM0L£VN}ïî©0ª ŠhɉWS.ÕÞ·‚¸Î.aXd†ƒÙt?¯øä˜ù‚¡yŸm.@[–Ph S¬¢¨ZulÔÝ›ÒÆ2%G¹êTŠuyã-*Ú+´…•°ë[ûø©wï¼à€t8UÔT=è²v ¬æèºÓ.¿=—&žž { íáLJ¦}ö©ð… vP£T‘óýv]/‘8ŠvX£¦%Â.ѸˆGI ñ–G(n$€‘-Þù»"Ox~Dc¤Œ ‘sŽß§Âp¥F¶ð°ÜM%ðÕYÖ•øŒ­·mÿÜ£á¼lWQÅõ©þ>‰p«Dv´wp滾 ˜â!óæç½¼Kòì^Ã#y‰|Tf/o1žô +m‰c½÷óú»›RRPrY-N‰4ߤ˜(en÷u©z~è²Ê=¿æºHÞ^\Xl¯¿-,Øû¶ìeþõÆÍáúâs}Œq£º›qûΦ¸i^gdÜ|o«—a¿¾!¸¾bñGß½ª1,¹¾éŸG4ŸÅAßÞ†^w¢l1ôú.Ù5:§Ó dKô_ÿø²_æ‘mÓmNNp.‡­‚4t\zìÿZ`?xxû¯>‘…þ—y7šäiѽ­›QqUn‰(Úöö¶ÊØ )Yy T’wXÓ¸© ­Šáâ«lá“^¢½ô¶–JZµânÐl‚ã;”КGÄ2Be£%×0ŽKvmn_ŒíHÀÎ\0)îÚBµwìÙå·Àv# KTh%E¸·b­ÉžŽî+•l’TêÒQ$¡V¨ü;ÏŒQ•‹¯nPðà×Qn•i—Á ˇœlçâ‹Ø’AVùÐEÉS{¢~J²OtumQD*×vμ=A›*Ùµ +4g» <È0?(À_´Ž¥Øk!ˆ=SEX\Â5iøy¦³ &«Uc¿XY¸“êú&5ç)â!a6î&„¿iÜèC +ÀI^^'†w\p ìd‹âk—­8ú››áOµMS¢¤ƒ¿Õr Ÿ•Ë/*&a¢)•ÞiS¸¯41¤)|>B°,’¨>ægÿ´$rm¦ uWëíöóJ(uц;Xêï|;¾ùÉ÷Ðä´ûf?çø8Î&`!ãW¥qÁ°»ÞÉpxèÿ±ïá ó2\1¥;¥ÌÙš´'¶D2å{~*ð>º¶ žSù<0åìîZEËŽÍy q®Of¬¨Á¥œÂä ¯i?½œì÷¡d+ˆ,LZ|˜O ºlz»çó£Dðÿ*ÁL9mÃ+häŸòB‡Ó‚eXŒý_ëò…²8—n-˜zÞá@ó»fÉÿ9ï&÷÷+iÂÞ/ðUìÚ7ù¤ò]Ng9Ûé,–ÀØêîúývÐé?–ú¾½½¶UT×à­†˜Ä[ÁêZC-Ža^TB—€Aô ,ýÄ@Åw{Š”ùû”‚}Dä'h+a³ tbãI¢b endstream endobj 363 0 obj <>stream +ûŽ²›ø8e¸rw%ÕÊð‰`ÊO„ì¶'R¾1ªÎLàÔ³x#`Û °¾`è|ä-Wn}ˆ6<Ö^žMØ;s¨NU·ÞèC þ>õ±’§¿ðÝàÃ,²T]E.šŽ¦³h•´HS €>0“:Á WdjBA§¸Dò®ãØí!3«2 ¾ùøáO‚]·¤xIåPf?ùÇÊ4íÛŽñ¼ €o‰] +èÄáöÙ¶k‰ö!¢Í¨ï7Ÿö5uflÁÆÝ£CvE$Í$Ÿ®›Ç?e×g[`²¥LËí4B²$\³mJna:¢± ÌÚ5{"–H¢½iAKoM”›Má;,IJBƒÇ/¶4 +ªiÔðRÈõ¤E ¨%obw¸JÇP¹?€D:Óæí^’šUH,çÓ¥\DÂþŠkÅöø\áOxž$tör«!vzâ}"àO䧩$¦7ó)9 ÓÏóQ¾ Óè>ø%’ah4qj´©ùÉ@Åë]©<î£:diSqÞß°xžëK¯åú®ßÿ¦ëJïÍuµ ·÷ºVÝ>¥°æ…‡ÖÍ0_Âú{7ïâ:~¿7;Àå¸î»ýøGY}C"D§-`ý‚b8ØÇ)V÷ïñ¸GÚà(Ï3ú.P?V XsPž±ׂ[üî¬%ðWeÞέsõ1<Ê5#¯V ö9Aññ$’쿱Z _ÁC¾ ·Ø`µ€ù‰LÓ‹ ¸5Z-èR:)â¥{«»4we/òZ˜Ñj!ãøEñìê°ñìãÞXŸÙX«ÁО™.Ö +¹5'i\2€ažs±yØÒ½³B¶Mj1$ÙWtV Ö‰ËÞ{|ÆKtVÈDÉx3TÈv5ʇ Õ“{5Tà¼íœ—à7ú%XÂ)ð1fM°•î¸{¤ÇvœJtTà›ü<—R4TÈ(IéN‘Qq/ïœr[+Uø:¢±‚üM”8Öc…ï~õ¾W6÷Ý +ôß3·s‡Dcj…ñ¹0­„SHå Ъ¢Ô™—/@ÆfƒzwÉ_aRg'be„生 pímóÌ2À”Kt ;»¯l~Á ¢8‹eÌü~_×ð„Qf¯}‚`f¯‘ÔÍN9Û²¬´G ™º/L²:y, ¬¶ìL(ú}Ï_‡¬%jœ–Uå…"RÊJê~¨W¡{6Í* ¶óÃcÁVˆZSÍÿ2rºÆdáŸß„ î¤ïDZÔŸ&l4ÑßbBÚ‡StcÃ^µtL§Q¢›¸“éÝ„M–«ëàµgtcƒ©mÍeð‡«<\1Rܘ76Ñ&ñ&µÝò._ìŠ/ù9îíز¬ì9äÁ³G;¶Oï߶cãeôw@㸳c£.êe4*(*É^ìØ2ɽ‰®±?Ž~l ! œÏ:’w~lYª ;mKÅÚ8ø±e¡† “uP@¾ø±¤ô›b¹Ø¡åìÆMVbªòS¡Œ_ýØÄ»MTPm;O½G#5˜ÃÑPßÚ¯é[í¡""_Ü×´Š ¥³§®Ë½˜¯Ùe©×Œ/pÇÊùÖ| Ôqܘ¯ÀUxÍZKŽ3š¯ñV•‰mûªë¡ó5~$±`ºF•iªÌpN£éÚw¾;³àâ}z[nÃZtÛv3‡¥Zè¶@v£ÌŽ›ÑH Ýî%Eu»—,ý­ÛBVàÄÕÁfµ`»Y‚-¦e­%v]DÅ<ԹȔâî».¸ÕÞ{–öo«=pÆö±dÁáû<à¦Oâ‚Û†Ëú`B?”Ü5cô…¯7´wxîÀ•þ¨o³ ‰† 8r{;NÈ}ŭߧ×òÇÿ4,™Þ[- ~ðÃÿ6¾îÈ—FË¿ç³j£%x¥é¨{KmväóÞëLMÄÖl‹ºÈY`ÁƒåÀ–×cÖ.§(Œ¼Ú–yDßô—Y¿ ·F31šyÃar<ªÎlßÄ8öÿí\Ç»ÅYøÔV&pôõÄ‚—ãLÍiÖ«CŸ°u¾œíM©šÓyäoa +e[Ýš`@;aÕfû´8SxÞDD#øUE› »¾X›q§<È´ågƒ¥Ñ­ØŽ²•ÑIºù"ì­°®S|å8?ÔÀY«-ôkîCœòá«ì}¡þ†À›æ¸¶ÊÁºqó)•[<êõµ* §×Ï߶ŠßÍVïw¯3lhÈ$ê_KDÝÆKÀˆøÈ‚é§×¢+Ú=áßiµ+àÑÅØ©ËCço›ä1êæ|ZÁ s‚§Š1ÖXæd?c±¨«IòoÙ0Ã|eˆß|ÎTb—PÅá¶oçâÆ¢$ üîsÆ"9·a¸mâ29Ñ•SŸçÊ)Îl[Ø…E¯ä`‰xóRãèQ8 Å”,,ýJZ™á:›[¶dZÂZKMÊúæó¢ý–ˆ“^Åaúv‡a‘U.gÏïàÕÜ~2$C´„PXih¤½ºÐ° `"nŸ"¢{ŸˆRòØ&7„çÒ¨Ô™4'Î8\°¢t¤h[,7¾¹ì™¨Ê`|Ùïp»Fæ—u.Þ¼zxЪ,cÉMºEW‘ù³­€x-Ù:_}ƒ˜sȸ2çì^?ÝÍøJÉØWvôÀ·?Q^&ööxŽD#öÕÔÈ·5’r¶µö´B’ŽöèÀåùô9SÌO+’«µ÷ñÕqI]½F3ÓlÛ6M$†ˆ_‘Zl['õ0¦š¶Öï6gô­)w'¹ØR ìa%žÁ£žÎá2B~IÙ©’m oVUzÅ•ÙVEz¿ü­t6ýº¢4IÝ‹5Vêe†ZÎÔ^<´”P¨·X†…¹×rÝ +iòöéJZ¤¢*|¼¹{%5³ nsûAtÉ\íj¸Ú•M¥‡‡X8ðê<úe«›Ó]v¨6±±ÜÝŒ¦ê$ÈX"/34ú:^@bˆ.Ío/+0iȱC:½~þëŸß®ézðñK* .¶$_!ÖOgABS±R ˆï÷k½u–`Ñ“~ÞeÞe„‰»ÄŸnhâüÐ ¢þ”Êë“D NçBΈ"Ý~ú`Nt€Š%—¼¦ 46hÐØñ6ÅìdÈðq2â«çÄdÔÌÏL4¦ŸTå™.óÞåþ/ <àßÔÇK…$¥-j„±‰ïõMDãbÁ~kµkž·›Êì™×¥`À{Ý»€z%Ø»O_3!”Ó&º­çûúÄUñ䤓?k™EzÔ³‹ž¢$Š™¯F¼=¬®Û5Òb¢ò4—dKîo>”ìëª!lÏJ»çs‰‡¶›wL¿GÓLúÜþow×d§¶/ysàl_PzÞ»×ßùØåœÎ·ñ”€ácóL;J¦ƒ&?Ž½ÝBЦölk#ì7O1²4¦]ïá)–àŒˆWb»m}s;‘fcÝ#çÀ>C ]×xGì)Û¦&„J•ìŠaR—ºËïÏ 'gɽñb 7—| •`m e¿ã*‚s~{QmVäg$v=ð¶]>b<" ‚V7ÛˆŸÁ Í%/$Œ¬no!$4û§)÷Uë:]}ÊΟþý5¦½|D jpæ@àÎÙ#vF$¯cÖ~P2éïw.ÞÈ@÷2óù)v»ÐÑ/ýR´»Orõ²3’Ø´CÙD}Í•ƒžêxæG[·ï¿»†éOzÁ…z¤:Ü~úZ¬×Ô_ÉÐ4­¯'H‹OÌ£¹¤ŽAꨇ·33¢tôÜwæFHü5ͳJ³bg‚§” ÓÎÉ.µó4å¸Ü¥›õÖG’ +•à°´s%³S:Ëù-ñµœQßóp#Ñ?òÓø«ú"ðš{7JªDÛæk|‘ ÇóܲÉ1¾úšçÂzy w.ùQPÐ<o5[Â3Or(F¦éáÕ^QPoUfí ¢ã”iv-Å";âÒ׺Ëòô¡·û|Vsôf}w··+a•Ûå!ûzœ¡ Äº¨>é¹Ö>1Ú¢BÍÌ T\[-O[-›!X½»iñéi¢èF<«dó‰ô´ +–òªÜ¥ýiÛcÁhQ¯æΫ#p<ÊM³ž?ñÀJg”5Sþ$dÛEíxô=†mg«}¾`hµ¯âÎÇêqíq¸Ç¾s­Z·&zUí{yÕ´{<[gªõØ¢•?ÞhCµæ­Žßš7Að1Ñ®‚qžÞ >¦yÐ1\ïC”@\/Ͻ‚âzÓ‚ã~%ƒvãz}o•Zz‘{)ˆ +“kݸ¨\kL·ÜkQPÆ\ëÖ›š¦¯lQ‚s­€ÍεJÞÚ>­5È„î¥7š<­uúÞÛi-êAÌt¯þ[þtïwš©{3ÙB«{³¹*³Æ­êNàuïoAvíƒAYv´{ƒ º¶aKÚ¸qk‘Ö A但AÓ7"·ÚÀ!Ž Ã!ÚòÄÀéNÉ8„[A9DjAU9z·êÌ!N¼9Ï%Ä :Ñ7#nô¦oF]t«w@|sàFßêf‡(<Èo‡þ*ÜÜéÇQWño ò¥ÜJ™‡$'H¢‡ô(Ȫ¯œêN“}§_AÑ}çiAþ’ÛÝ0…¼p чlòÖ€)$£[%­A.å··bû;~(ô{¦%ýWZ}ï°rð§€çèÁp`åó÷vžúG¯¤]"> «œðæ®° +Á’a&®n*ÆÄ£ä±,$®¥‘«÷D¨«ÜZX„²L°ÂØ…œà¢±«>w«@l;v!)8~„òÓ­sH(^’]îÚž%¡6vëx²+jÁ0%Ôஞ+×ÞqK(þ]í_bÙðbs­8ÞZ΄zep¬ ¥Îmv +¤·9¡¼œvbÙöêÖGܹþÄQ× +Ê„v)Dßú…:v8M(Ž‡+ #nQî̵nïîµìÿÉSºt ùÒfˆÓãÚ¢¸Ÿf¡Ãq¬×ÖH˜î×ÆÊíKsmË„7ïÚЉ/ïµtÿî_ºI•ãÚ}ŠkÍ¥wu¿d];_aé M³°j†žÛÝ’:vaÁ;°ÖïáýVqm,†gw"×Û–wûZìz^·ÅØ/½î¨¡Ûúº ?Z³×=;4sÃvzÁ·ÁBh%‡P#t¡wpz×·!Mè|‡ˆ(4ÍWum´ß†]¡M‚¶ÐáßQ^ÀÜšTÁ#ƼbnÍŽ”!ºWDˆ‘ˆâ.ÂŒžðF ñ¯ØûTá +‰)Ç›Ä$e¸ÍqöÁk†tŽÄ$ë +›¹OÖ® ›˜ô]á:ßgÄ]ÞG]ó× Ì(¦ÀWÒ}*}Å8Åó\áQ1­0«ÛòÀ¥õ(*\q]±q……Ý×5®¨²X¹Òb‰åŠk{-Ð, \(ëÔ\, -¬ÝmA) ôB=ê +í …¬ˆ¼«ƒ€áµœ‰»þðŒwE»€†Ü•¾€Ÿ µÁ+úò¶²x…n†ºd}†’fÀŒÞDàtWQ725”[žõ¥F@°¡®ð³¡$à··å€Þ õèüÝì¾­{Ç_«æŸêìÕ|[¦ èPåÀêkƒ ´ïú d)BIe}—,ë¥O¡K`Ì­-v8XéÁPRò}$K`Ž7þ~óÆç(««(­‹56f2-FÄ-€ýÓÖ¿mëÀ™—¬JAÿ,±+¤@rÎÙ¯=åÚP"Žò{·qÓŽÚŸzU—–øAj»ßñ+9±]^“Fè›eÕµÎGƒ-“%–I¦yy÷3z ¾6íâ÷\û}˼³/zü¶ko1Þš;×¢p‹½ŸÍµûŸñ]ó4L•kë5̶ݫ “õµ±{ê» _’k×ø»Ù\ö +ñŸØ±b«(ßÈq.Oñ-fœ'/µ”£ÝØA¡Ä +¥­p7ÎÌǬ-9IoäÞ‚ÚPaD‡P•‚pžšäYlÑ›l£FVÈcoœ%8ÇxWã‡/Fñ®fñ‡Ü™‚ÄÎnêÕ äÓgòãL¾õÕHËi÷ÖÆèyðâ"ë“î?þ~ë^ôù§Oi,PüK€3n¾7Œ¸qú‰£®ŽAD>%kþ3ºÅk¼s/"Súû †»ó¬[/á“ÿíõn¢‚Llç»_‘åƒÎ… +®f–påv³Én!¥OL‰X:Ƽñ4¢4Î ¢î¾,oºsÁWÇ¡}úà*·/êÞ”ný”ëÁÇ¿î+×;õãÊ÷ÉÊw]%^…ÂÁíôÖ…í ¼: +ý#ŸòUàyúµ*¼8ô<þ¶ì|n7Š«ï‹mÐÍ’·?u½ïùäý¸B]&¥×žßÝ‚({)ÌÏ|®IÔCµÊÑÛãõÏ®¼} "®;çJ“‰¢N£òvÜØQªðžåD¹ŸÑ¼„Ò_Zˆê’ÒKrÅ:[UÅ»Z¨HØ:ªTÌ¥« ‹×ÂN¯…•ù‰KPüÁWW¾I’Ž §d[}5”ñ:'^6G’NÒ­1 ×£ó]}m(y¸2©}w7Þ8¨Ï þÒíѪþç±Ã(¼§O +h³‹EÔÅwÞ¸Å÷øÎ%ˆQ‚P7ëõÆ%ˆ; ¾JGE w×BÏó1¾5þ&h0RŽ©´wonÝMÖínú7‡Û ¸L·•Óú‰m£ÔbÓSÏ7¹°Rp½ 5Æ›y‚ ½?êKŸxé¬íf®–â +-Ýn VÆ0ßó”J3E$ —Û÷Æî¡TA1¾_;»lé;"³ß¼º×z»pÞ¡_—G/7 ‰œNÚr:©/kB#‚×ÔÒ‘v{õ÷øüS×ï»ú‰„«¾³%‰?þjooàÕ!%>ˆ»zK| WÇ–=®^/×itç~ìÕx&Ìæ«wMx)^­oÂ;µ}rÂÛxu؉/õSOX¶ÑOXW¶5PX–^ü„¢öøÐu=||MXNo¯2,Ëá×®ýq{®ûÀÛM ÛHx&a+ +5li·³#laŽ…í5Ìν;ßNî°¹‡—$áE ?öö… Kxï?u.Ñ··!Ø?«}† zoùIíƒ~´ú'e‘?¸‰/ü(ŸO‹«Æ%¡àÞ±î7ú-¥o|Ú™’-–ëЋ|¾ h³‚ ¬Ÿ¸9$© e·• Ï7‡¤ÒFŠNR’&f=j{÷jsÓz‰.퉃ý½oÛ°qZÈ[ÇæÆó€Ï¨¹akëðVÿ»sù®4µàÝX7Ä(ûź[‚Š¶i¸ûxY·½Œ!ÝÑpÕ5{±ÈÒíÏ(k—/r|{ÞÚÎ +@lRäóæ¦Ñý+Tð_o÷œš½’}ú°íæ‰IXŽ´jû¹yØm{¦ƒMn¡n' ¸Tšúv›j7Þ “œÜ†¥3¹§-z‘HÒ +_˜Þí»o>tÑ­_|Õ¾?àNC?Þˆ«¸—W9ÿø$îl⽸ Ä©p5(ˆ3êÎè NÌ«aBœÜWÓ…ønÜ™7„W,žæú’ÆK¹¾ë÷?i-ᎄµåzSÃÊtûlöÂmXÃìkëí, kt˜­qu¿ÎøÏ6‡]ßý¯aÙ]À¸në[’ï½Gž óÛ(‹ú-üºñÈin#,ÃÆ +'xäÁ™F +? +°øäÛ,oسíôÌŸç¼qË7iÚ[À^æ[ž2ÎQømwÕ«[^G¼%ߘäu°fôE-–!Ž&yýô_PÒ\»~ÁÕ-ºU,] +›Á-¯ z ÎóÎ-¯17@P‘iŸ¹åµýM mâ~ã–GBÀ>aOw Ýò:8òŽcAÎÜx}SpË#õ°üñäÖ ¨÷Á.ñK›½öM ÷Ê]:—`GK·u`[XÃ<`ò´EŠåÐ4£syK +©8ÝXèÁ²¢wbw_¸%}SðÐã<ÍÖ΃4{ôÐñJnÜávã¡w}{¾ùÉÿ;= éçÃ…ôs~·}b¡ÑJr®?ª;\íÞ¾ºÊÞ:èe(Ö'ÒL7Xè mK~/¹:Z¿õ—ûÇ¿Ðr^`µ_UûË`Í°K(ÅÒÙ[=uV±x±µý€'!߆?ú'ÊC/[ö:XÔ§ý{®z?ØO|xèr`Xè÷2\øéWÅ·,Ë^þôßZ¦ô›¿ýøé×_ÿìÛoÿÓ¯ÿê·¿ÿc}}ÿ~Ö ŸM¨ÿ†E`ÿù½š…âÖ¦­\ƒ8¥PÚ«’±üξiZFV­(è°#k^°j@˜b‡LhT,T¤JýìS#(]OLb(¡êàÀ»-Œh¶$ù +f!²¾ »X»´ÕÙ`×è›áiÛ7³ÆÛI—°¢±<žvÝÓ{²…"‹uÆ¢”ÎNŸØÎ’œ¹”‘G‰Â¨VNŒ§-Ä*©`ªt¡þ½Ncáî€c/‹nyÆúšS¢,öñwq;3´äÒêfÙeNùuh£¥ÚN—‘¯^Èš[ÒVÖA–»”÷m™Й#ŠbÿTÊþ=zPu­çÃn¸¿©}΂±­‘Õá ˜sÑåÀêMcGÛy¸”!{‚S¿`KX¸`ökÑ×ݱ\U2<€z^ ªÃw>nXv§¤GÁ¶Û,Û@¨¦èLwJßÙ/A¾²ØQ£P´>±Í"RÓNÜà›=5 ¦:SýõÓ+^ ÀXˆ(CS TùAJîš™PL´Ïßìx\…?Y–N~–UŠ_H±4‡ •Ú¢Ø™°ì[í»„*™+{FIˆýÜ®þöÞmÇ’%9Ó{‚ýy#€C rÂqÉÞ˜ +[Ô`DA{( +ÚM‚ÃnBo/û~s_™Ëͳ«º§H6©â¡Ñ+"üdgû¬8˜¼ý ¯¥nÐR`m^t`ìá‚a%t:j2Ö†s½!ëí.pö%Meº•€ghÏæÑ<‡FsCª?,UØÓÓd3gµÒ E/7hXM¦]t€P¼a:áÖHlÑ඗ÇD••’Ú,Ò™13ò)5½èÀ¢Dô"úo_d¦R›)âÊæSAb_Òµ'ADz¦¥ãåËÇcz.»S¹nÇü2kì¨[Wó´è!,G9Ïh£ËÇþ³`3U@x1mf‘UA<²HT"Gi°!Sd`…­Ø —ª .?[@yV'1£*¼MÀYØ& +òÝ•î+øÌ—º®s¦S¥ww¡–iHq^ÌS«‚î0#(z“Ivžì€&p,†é[.·ßM<Üø½;/Vx¢pv"ëŒO’ƒmhQB1áÂ’ëH䣦/ȶ=%™3}‰Ìu{˜ùøJq‡ >'6y‡„á¶ï/@‹˜›lÓj>¼™"ëñ|ùT¶ðÉ!ø¡G€ä)AÕ²7œ>öu’ÙºÚ‹£És^ûy¢,*ÄúE´·4*‘²hl|¯¬ÏÔWƒævyŸtsô×Ù¾w™bgûÒ©cóû¦R™ÑWs8†šêpL<ÐA®»llÐݤš-«‰¦v +ê  +´m`”yAˆ4+E[ɦªtèåIŸO ö+Õ®Ò×’4½é¢©ŠÃ +”3 g‰Öû¨†Æ%ídg´“ԂÉÂ,…ëÕߎ+9@¾ÚøPµb³¹*xPö‚£žäíõ.ÎÌ?ëËÊÁ¡¶í\E¶åŸÝ™AUƒ±MJÿ„iÀ/Ö®f.{E9ÅÏÊ7ÑJ¦·¢Ví‹:÷ÌYläü3݃¥XŽ³Ã®sS§7Ï%'8‡ÈÅìM`¥·î/0™•U«v*ã~Á2tcDØN̈å‚@r2Õ“<´-[×X18l¬6¾<á%ª’Ìy7;ýv½—HÓf&1_:X2öþä:ߌÉAXIû, ht“Muò©È’?þhÐQ§)á,IAc3NÍžÓ '„5ˆ¶<š?!qÅÕå1ñ/nhñïÚ[¦§u`Ìn„ui÷”Ë—¬&|Yö4ådm²^¢(ÞASŒm?¸”Ž‘5ÛÜDˆÎ|}Úé/ 'Oç~­tÐÆ¿ÚX +ýæ:wVíòB¢ë½<ÖÞÎ¥Äݲ7-åØþ¶ ExyÁ[AÙc¼êë(Ò³nîPéM–¨ï>b¸ËŽ«Tª8%…ÆIï‹›è€Æq?5: +9.ºLK0Ù¦¶­ õËXtÛûÚyÐ[â‡5MÓÊ5‡|ñ+–õ2ÐE èm¹I¤^ãbwÐË ÃšÓl\h2?…£hBËB,)ùj—N ûñ ½ºÝq€ÆzÊ×u¢m3Ó Ž±YØ£ ~H™ˆ´-ÒØnÊ‚,»«ˆNüß4©§¼X;«·ÞtÓÜO©žÙ¬yî2ˆé0Hí´T´LöQÚlj”•y€"‹þÔœ½‘xHbÕ ;¯îКÓÛ•>"Úv*ÓÌ8ôÒ«çɇV ©4Ô•Õãc+ +f áÐ0iâDçÐËl:§½È–²}iC1{»Ïµ=‰;1ûP½ ªÈÅ~ž„¦÷á„švÌâÌ V˜€è]˜Ž6XÓÏ'¡AÚ9M~rG›€˜¨pU/dBì”[Þ¸ÈÂJtz¿¬ÁÆö‘ÁV›yKYVŒ¢\PRp’ÍÃ'4e¢>âÇÆ[¥„U)Qdœv9€WS/Îä²9?„˜pѸÞD·e_ha…å)vÇå!¬ŒP¹ËP|—a—Ñíˆè¬R¥³Ôh’u¿ÔÚI“üÐ串â¶.ïerAÌy_Ý€MO/¯™·qèÒŒ½É…¦7¡®V}(…`l#›ƒª­(s=ÛIÆõÁøû[«Æö°¿nö0I˜=²ÒÍ##úh{°‘…ëb¬³×›éPDÝÁçWd©£¬ž* QÑ>¾_ƒ +6|M’Bò¥âÖ¾:¡2˜éi3ñø JZö±y×®˜`«Ãô<Žñ!Øt™˜ÚÇRCirÂ75ñ^JÝÈ—pC¸ÍN!‚4ºªö"„üt¹jóÈÅ[ÇK2ÒÌ")A%û€NM+`Ú[Í×éÏÇqu¯…P¸¼Ãÿ(ÔþêóÔC´Á;Ç+ÅeZÁ!»-/¤ÚZ< ØWDC i¢Á-ŠŠ `ºeˆÊ"°TŸ‹¢0A a8z(¤Úq)þU°=ÚüÙ.Å„×Iv5„«Œ'ÜqUG€µó}ñ®u˜ˆß‡ISªZ(Kˆ†•Ðœ€°ÌH3]l7ÏõMnË›c{݉Š,˜Á«!ÊA² ŸîØ )ÞµN í$­ìSðKœ^hé•°i8ÿŠÁl—iMœ„å& Bvk€wû`ˆ,žózÜíîj4qëùÔƒ¤Íæl0cë¬ÞÍ®æd§7à]ò¹?zÑÑô"SÄ›³Åêeb[ÄhZ_N§ÙcJÍRÐj{Æ/À +ÎP­ Ê +REþåpÒFÜ4PlD&VðÆöb«U‡Ž¹ÌÛ¢Ê+Š?ÅŠnpˆNX9£m"%\‡%Uë^’1ŒÞØÉñâ†Êh¥—¦v@B& Å, +è¶Dãe+2Þåºj"‚“$ /’ÉôÙmÆ7˜!ò(wZѾÁ¹>W¥Úø±I³L{‹Š¹ÁªiŽ1{\;ýÞ°Õlï]6á‡:£W3Áì=Óy‘M8PF«©Á7’ô$©û“¥QÏgŽÍeBÁ$ÓMbžìd»ƒ²ƒÍ'ÂG^턾·¾xNQðpV‰V\ÃqThð‚âµFKÕ!ÎJ °n-J’j-1rVU\‹eÚ¦³n³ll´n ÊA´xdèx;«qì;îôH"ÄݱŽÕÈn™8æE7ë¿ÑWS4ÇÖŠF?§”Ìžˆ9 ÑqðQŸ>jÀ^6^ì ç1ÕïBrTÍþEÓ¢ÿƒ µ!쉨[7ªMß‹äsÁ5NX#ú€ Ô fçý·xÇÎŒw­þ$áYJjHKqŸ‰ïÉv’ñì¦GËp»©¤&V·˜= 8â‡×£'þøÛêÇ#èè—"s#RÓ `(­&{}²·1¾•l_}PļyðB¹¡†èlØD7¨28ûTœýƒ(‰ùª£Ò Ji‡h ‘tvAlÑc˜¦A°Á¦±Š’›m¸‡¹QfÉ„Ç1lòÿkäI!{Ù]°zûSŒCà oÂ_dô‚%ܲŽp¬g²Ž›x2Š˜ƒb”™{ B“ÌÀÐ2MÄn¿B¥êd³¶w“èìn]mL'hÎpnIí!(OžÏó´~[™á|Ô»@Ît»m*€L¢op»öÈ$ðPŽ-à„$I8yõfÌÒv©ŒøOƒ ÇM•29ÝôAŠ æJ¡mþ…ɧ!aÍÞ(ݨ£c+Ýó&„Ð,:ÝØÚär×D’¶m2Ñqb;ãŤzNK—ܲVÈ_g‹ì_ÿÙä‘MÀ/‡ ü´#OA ‡¯°´*jêùL/ÌÅ36.ìÑÐsØTØw›õ‘‡Wà1£õ&µCÕf%CB +æÄcw} ÃH,¹”¨—›$öœœýˆ7Õ9€/Å\–±ÙÜØLIw×qG–‹ +ô+G)MÌ°Ãty‰ßrÞØv¨Ž£lÆtÝ"û¼’ûpÒìN×ÔajÍD7«ŽPõªài\!cÞö*$‚­P\eJ‘ô¸YDXëna$X³eil–uÓiÀê8/a ·I&&Ô=g‡KjÝ÷U~ìƒKåuŸ§Txz*!+JAÀíØo뼪©›A Ùë9ÈdçàŽÓã?Ë2„Z@€d®—)HÌ<JóÂQ°ééñSéw´q\*i˜]¥W¥¤Ã¶áÐ+ª6È=^ÐûFhVH‘ØA7aƒ³ïÅf%Š†ñ#h8ȼV¹]ѶúöWÓ™wþ•šmÒóù„鸭š£†l_•ŽtÅ.ì2Ê«2cƒíá,­><Ã'%XAÈæZ“ÓH£(àIVÔ¨¢4wŽ¨RÐÂUÝŠ‡Fš¦H ʼR‘pñò챫=`žš(XUD&fø‘Àš‘¡Õá&Š¶#¦b#aw_@ôd[M!³«db]Ôš§B[M*f>x›‡ÞeQ¹ Æf1‘Ìa"Žœ¢Oåܨ©kZp«!È™´ -œÉ$Wv5&mä¶ñ©ñX®ÒŠQŠ¿N´¢(7JݦÕq:i19ÝÔX­ázÐWAa6%mcS›,RÈãP€k”ëÓI¨÷ ì°ØVóž]IÆýÎ݆J7Ì­É…ñ¦Åݨ¼䘚À=Td½¸,è&4jû8Æ\Ê8U´cÚjú#«¥™7AÏÌc'¾/¬ +ªðbæ EÍÑ› w|žÏ“WŸ3J––:bZ;LÁ]ïþÐUósyódzZ ¼Ø!¸îÑæœÞxÇê;Û+rmUòÁ†Rw yc?¾ŠC[[åis‡4à §àÞÇ8­´üsZ ÉÇ’¨œ‹FËé³Üb`i%IzÉ:·5@‚t&ˆ Å&¤/!ÈÂ&¦ ´;±òb¨¦‚ÕzÑ‘sPr1Þ´†|*U“DäÖhQ5‡‘sO1êTi³akR—hBö±üOÑ« +ßS(U3ÐbŒ7I}Ú›­Y²rÖhZ%cWU*CIy 9“£¯’vÀ©É‘½ +~ª§&Á=_¼F¹Kš ¼t¬e´½)ßç"õTqˆVÖNüêa<Å:éëUU5D ×x)w˜Ast²wårSR'Õê¿­t%`‡\ê x !àJ£6Ší­ÄÖI6Ÿ@P¼ÊíPãkDÚ¶¨Bfnk¿½Ì`veÿK{&û‘Z–X¸iWš>0©×'o‰©WJì¥I¡Êyˆ÷qy›µ½\Ð" ñ}ÍûýˆY>%*-"—t»î’Ñn¿Æ"Y Þ{eŽjn‚ÑzT¾Ïnðr&— +xΚ!Ñ×Ñjî`­ÖD‹>…¢ÅV&ÉÚ’¯Áâ…ÞF-®—ó© rÊ,5ñ-ÓvÍÉåàCkzä‘?|Ê@!ü¹@èÌÌ÷šÅªåV2Rë&fÃj&¥Ë ü 3¾OªÙßÇÞ²å| y¹Ê3å®SÜc)¤÷ÌÀ²_h›xÏôÞ’¬à1#Ô×¼"3¯¢]óùôkz²b°Mºã%¹‰])ídŸs¨=gMÖž„j|©¦¥Ü›;Τ:jâ©Ï€Y¸kåšó­0‚²™ÏÓVž˜Úš:Æ¡£ô «˜>¹1¤%žÒØáSâ»!Å»ÂÔ¬©ù8½kŠÿiqT°®g(#˜;al÷ϨX›oT8„mê#¶»?ÔYÄS´Vj„“*>>8ÑÏ…#A0„Ú“ \B ËNF…R˜UÒ…jš ,G-ÎVÔ†Jž ²CÐö¡†è†HA¿„*¦ £B5ÔV×…ªª 3CeVP·¡Àk«¶C¡XPÿ¡Ølµ BÍÚÖ µoÁ ™esÁ +Uwï,©P³ì°P÷l¹P?¸µ Cb°-g c0KCd0oÜUR39Tdsû7úÐ¥²3˜í³(tú¡të'„‚Ôào„¢ÖೄÚØ­ïJkW*Tç/,Þ±óæâ]«W8 ƒC¹V)oýÒçBçà܆B鯸cç_oîZÜóPû<û¥‚|uèñ)k-{ˆUÄšøMÈÃëéC¸$â‡K(è߆nBc@ÅÞ‚5ŒôhMØE¡BgÃÌ +Í!š,¶Ñ´Ð¬¢r¡á#DöbãÈ.BúNB 1ô®¬!Êгt†Vš1 ]83Ô:{wvñÙµñgÄvg£Ð ÝEÛrhR +‘éµÏ)·C»Ô6HÚ®B°=4lÍ8}è÷Ú†ùC»XH„–³pkÛÄE|ÎÈ{„Ž¹2 wÛÔKhà ™›Ø¸ä|b/á.uÔ'|C±Ýè³%¥±bO3֢÷d±°¿´° «˜ ëxŸX·Ïȼ_sjf~½b£ÑìMr8¤åN2$ä6ìsèµÜf÷LB˜_aFÔS„2:‰ðËà`Àkš±C})óø@ê–l¥/Pbõ°ÀŤg¿œvÑìjzC)S™7÷)¨Jj§67f\OEÞ “<Ím²¶&cżM‚UÛìo|ΚEŽß²f¢ÃHÖ„öç턬‰ñ8¯k‚]=4a}ÖDýºÂkª?n“]É@ØmkåAØ°¡z!ìû÷Åáج¥ñä­Õ_îf ß‘Á? ‹éPj›©kêX³+5SãÝÊú°?`Œr@L}dâüljŸ7É~’uJ}B^íGxä*?À8xüAë1Üq'³¼Ýǯ.2 Ä ÛgE$`Ûãûv3´Lv0/‰þÄÇ_ÉÉŽBŒ¼m¾n½Ãí:¸ðœ8A÷8NйÒôʼnþ¾O·”ŠÈšÇˆ›/”i)…½@äÈD¹¹ËL’zoÂœ&RTBc¾ L¹Í]?U õ+~N þ!$ØÆ<Þþ°|øç–|ºë„Öd¸mØŒýÍ'Ü]wÀîÆMPç—9K÷¥Ž§õ#m÷Ù¹ÄF#žÑéJÞÃLwOúÆ笓¿åK‹ñùû.Þîâ\÷¢• dQiX4”f®’£™'„>›‡ VéC²QÔq’2jH1o"V»àE0ÜeÆÎkõ ÀA¤$JTÓä¯ç© +û¬üA1]|ŒKñøCÜǼSqüóWqê­´õïrö9»"z‘ˆ”ÿ¼ýã_·¢ÇŒ<ˆHÊÙÝqë§[ñºùù"qÞÞ¼þa|ïç:0ÝýJHÀéÚ3}Y«44ßHa\ík£ÔV\OT«´G·è“lO “¾ãƒiÿ.H—mJX§ùѶñå~!R<'€Îx—º'1;3æš"{¢ø¾O{¤Ô_!f’ŸIRº×Q(>fçA=¹ ­ +ù_Òc’¢i%“‹êÄwÅ¡B–YBÖÉ<¥ž`/§à}ç¢ÛÓUÔZ!&Î^¥«.Ê#“”© +Á‡’Õdñ¥;DrÀ›Y½û[ºj^i¾ÝIo ¯¬ý9Œu°49–ƒ *Á»¢éPaJYá$…):Žah+ìµ»ô¯õHit÷#yíIÚ<õƒ³ —ïHÛâ%_U§€ lSÓÂ= +ŸêPù©øëFÐÍg% -É©ÐA”T –@31X¯r¸q»U¿p4¯Àµy€FÜ,¹­:]‡±±˜î*oö“ªNµ+¤ +–§°Šš„õõù”úÁ×›Jº¬—ÞÆNƒ&3»×nfrA¥BœN p7ûÜÕT™I©ÜqñÛÿÚ¬;Ÿp‚KÜØ–”Îo¶O]+…Ã&¸˜»° Û5 þL¾62Å‘]Å:Õ»47ÇÜØt ЋH®‡ \:À¬<áÁ]ácx»3¬ªCAÀªžäŒ²Ì4/âãsÚFž€þ¦}O Nûx‡×Æ}ñ9á[)_Øg´®ÑŸ1‡r¶ñ‘f•ÓvíCY'BÆ;\TRñæ:Œs³»XPXFHiÆ“—rÛ°He“;@S§x;ì'pþ¼$ú[Ç8æ ɹӞLTZ=Za'ƒ™K§üAÚ7ÇA4Iàuɼ÷GyV8X*I8m»Ý6±e ?É~x%ü<È@ÊÎTÉåùh°çøÓ§¦ÆtÐWÿÄF!•C‹µœ$=ƒø¡Ï‰Â0{g?îúfj< ±ÍsA?e•§Ûi<†ù:sSl'>ª‘eý¢&Z·@Th»ãº£r]6sTÑÛðhÐðã@Ó`=Šq¨»#-•U"|…µóE«éóÖbû忱o³h{¥›ïH¾˜b£‰&ÈtÄwþ›ïü7_1t©‘ºŒ–™)ʮɉ=H’—¢*íÖæ{‘£‡€HËQŇ|) +¥næ0œj- S'ØRªNGJR‡Ãnÿ3"-æê8(z¦‹ƒvfø%ZH ÒeT…íhßÝ„  +%ÕY7Ö +„`mÞqŒG$RtÃ|9ð…ÁYÍF€{lÆcN '¨!êÝô\åRÉì VÏø •’IS| +‘@Á8º >íÈ÷ƬgÁö'ób‰U· L›ÈôqÜ +ÊÚ½(´¨úœê¡Ðµ×Ù/Ê`©Åf°Ù[Ä{›` K^» +ž•“'˜Çz{§¿Ý%÷1 (Ì^Wbï:DšÀëIS6ÅTÚ›Åd D€.U8”QQ„Lù¢$ XCʦ„d Œð ¡ÏËÍd>Ñ!¶›„9 Æ£8‡èµ x‡ž/Ç]‚Í2©6ùµôÀI·xƒÄ«FKªÑÚºî¥ÞŠ|ùöJ¾"ž·Š^>m7\ªSÕü˜B}LúEi¬×teE°ê­ÔÝyTRÉKeÁiú<ÏÎ~xÊ~üá+˜j¢·é¬Ð¦›0¤nA™S½ö8ãïd-‰lmW@. ÙÎ+ôéÛ®°¹¤$©qbpN”©Gž©ÅÀ¦>"Û :@Cþd™Ú_j‰í´J´§ByOWå¢Ó@õ@<]I¶Î¢œ¤P×–5±Áo‘9ͨ o ¤¥O]2I ~*xrÚZMnª:°w€r¡‘­[ffÃù5C"&ŠQEëHŸ&"Ì­U®Å=øþ‡Çô‰¾ÿ×næg¢G |Ƈz³ Íñú +¢€¯RRíƒ?ôÿ¯ßÊÿ¤mßÁ\´‘©‚Ñü›JT½zV•B\»«PÏkæ)ˆÄ(¿éIγ—;`ƒ©n®D¼‰!xa„)¢Iiƒ³ ”MM howÂq•"H¸W.ê¦Ï&šüî›I~zQ9ŽÌ—ãðK|£aê29™.ÿ<{Ž +e‰¯ºÓbã?%+£T"ßj5ÕE­2ôÄê…lwPk7™x …P¶°Ý樎Ñ'LuäH¿½ëÿÚzu74M‡žîû, Þ“ýbÒòîç‡/Õ͉½½0‰¸—GÑGüÑ÷Ã˵§çfØà7‘D=þfæLAÀ«æ¢³~°h0àfeôs·Ž¨µ¬Ôî·- ¤4ÈJS¢µâƒD» Õ|-ãà•ÍNäg4Ð! ËH¬?íæ&€?ö mØR»”Ðæ9§ó:' $®ßÙ,hÐt&:£ã˜¼'B©Ô¿çÁ÷+T¬Ø!91[&ÕL ÁôTÀõ—ÆüÎ^ùTDgÀ ¬0Þ#°CtÕä)”çMrf5¸ÙTi×Íf£v_Á»F5zÛlO{µÏ¼¡}>­Dq›÷¾OÏlˆ…q×ä§ +‡ìñóõXÆ7¯Ç{?‚UJÄ™X%MœÍUbmW%H¾°¸Ah†ý…oÜfQ†¯»u£–ÿ¡™9Ôz7§‡¹ÁrN>ØÝ@¨ÊʉĂhBD›ëæ˜m"8¦ÓIĉ@ ©Ü[´eŽèÎÔ™‘N`¿Jugo›¦dÙ÷†.żà¶Q+ÞFšÀàmd´i#›ÄÌmÈ<ôÑ,A™n'l÷Ÿ²!…tb裼ñr;³6öU!4 +üÌ?W(Ü_øÉQŠ¬ošM°¾+,)ÐFfoˆvây½ A ØnKCÒG#ÎÐD¹£^q åtÙ‰ÓÌhq`”™ìì‡SÇ‹õ§ÓG2*ùyŒÍ8ß›…ˆ" i´­§Úmùn@ýì1…e…¸†U]P´ˆ‡+Xи`ߌµIãE|³S,¨´ÁØcìÕ„kµ&äPÜX‚®8¨ +wËM'öhØ<¢ÎNÇÏ`³ÊOÀ‰¨ñVØôZrW™ös‘úvê. V9U_Æ ¤SF¡ðAëy™ß>^äÈtœ¢<ý9€õ%À|öEêÛ1#~.:l¦D6‡å«Ü°oFàF³…3¸]TœÒûŽÞLÇ ~À6Vè¿'² BÅ °z[rv=e¨«žéÍp˜.@Ía5Kƒ.˜D;bÿ_Øi”€Àí¢.Oÿ gnOà{8í‡êo8s¡ù÷a–¿íNÛ…:ôTW·o7Bu&@%n»TëøÃw·oBàFˆb…·b|e¯²H$«kñ¦!§ÊºoÇ©¸)™¾ËFe™S»ºtL/•40„ëa²#×FcE­ÃØ+ÞǨºÈR:;>̽êRm¼i +jãíIÕež„,ۣЌÝ7ªËLSÚsqqq§¹d3nZ|í¹Qu]äGToÒý¨Ýe[5$,¹« »xΑäuû îÂ~5µfÓâ˜DAwhEØY4ùß>Ð]÷ÈÀÞ7gúÚè®ûòÌÔ¬Q`fÕ]*¢×æ†ê.ïu—j(o¢À•=;¦£ÎRÏ ­}ôœÐY¶ÕY6²WH /&s¹:‹ÖF‚´¶åo;)ê¬/ß[½EŒl;>"F‹þú +õõÌ•inÉVÔ¶X-W7}Šåí5~i~i®‹ 0­Ð°†Òäìо¿Ëë "$ú‰ØÜAïwï¸ nίÐ]¿Çðrù@u}óáñº$Mù¯¨¸~ûFú·¬·Ì¨­˜þST…õ&» é·Ôò·ù£H€†åëpäÕ /'0m°ÓÔO»oVÑN¹^MÕº£“¼&7m&°Ã¼Ã\ Äþ1Re”Ó›-Ö¡º‹OSÌøæ”Tå,ÈÆ€“ѪgŽ©¢ƒ¨®*uËêl5íi÷9Q|BÚùè•Nú&“ÍL Í„/_˜Ð!;ÐDØ܈tíEIêç'˜ AEÀ7C…!afº7ú´ê²sÐÖ ŸýËÃÄY„(YÐü ïñ‚Ó™âÏGÔV€Ý‹_±˜Ô²~1éPÙ-+y‘”!ã I3;¨ÑöÜc‘Ø “0FÜÙi'Áw('«-Ç­¦v¶0Mí'?ÉM%εP—/jó¶dI­ÕúS'ªŒZ‘VI¢+F¢f TÑäì ú`ðO¥•˪`;í([ÏÀ3™¼;³ÅœUfXØWÔíz¸¶f‘þͪ’2¥AÝNGA! 1Õb»tŪ±5Yrd03(dô-W@ëÈGdª&«,9ñ>dêGO/Þ¡1J‘,Ò­/Šß@rn0½lM÷.©´t ÑìNÑ% õIaÖ´@V¹ºjŽÒ7I×Þ%àÁªÊ œáhq ô +/ž@îT·míøÛhÖ€õ³4Úÿp”'P +àwö¿Þ ÆE¡cRÌ”oö?&%tFÁþ‡mÏΈÙÈ(£Í¨Ï©¯2á â¹7ÿ›Ú?Ší¸@i üz7lYl“‹À/µÀ…Ýsí݈QãB©cêÑ ÈmÄùlÑiMn€Í“ÚŽ™áDä­©[Ï™DâI3Itx“6³½‰¤‡µošÍ6aÁ:OUæú;7€iTµ»Ið|¢•£b…±òýp2…òš!ó£µSwn@î ïA¬VìÓ ÈôUܪ£?³»ÏŽÀ—OØ7-œûRQö¬œƒÊž˜§'`ê +Ìy»”fùXr š¦aÀ +X@4õûɾ` äª,(Ðé @¤“â, ³÷JKg_¥Ûfu‹ðBÀ³¯¯yÈî2AyãÁ¿ɦ‚±QŠ °ÈS ]RøêÓè¢K²É"ÚüUc£ÓËœ±µûÍ8áz°Sô‹o8¶O5Ã7ÄFÅÏ¿e8ôÛõýVÅtÿÚßÿ]a*ÒuW3{ÒÖ0ƒÆkœI£«~duªR +ØT&DÓÁ`±290ˆ–Þö®@%ÅAYJ¡µà Ô\¼‘ ú×$Ãkñ@ƒç–ײqˆâ›¢às ÊœÑ6­R0ç¼wL¿våDÂ5EGà‹ó;*>nTZ"ïÐ6« &;_š¢#Ž¦À )›¿¯èO“ uÿiøS˜¥°g…ÚSt +Lܾ"\¦/@Ä[sAÁ8Jxç 0-:Âqƒ^·N•ÛjÊâ(d?³«:˜Ã[O@õÄаZÎKÀXš0êÊV€%U™øÁFUVðø­6¹W€*Í€iÅoRcë¯ lÖöê£ ôÀ-Y3Ø|-@±œr†¹Ï`#‡ +2À •5ƒ­ÍÊ\u*k7liÃÜ3µÏ`3&@a)YHbl'öƒJó,4JÆ vÁ¹9$ÀÒóK[Ïáèð 6C*›)ñqA”²Ieo%Ýÿ@*ûK¶üs.eõÉ“ÙPM÷š~¥¹à+rÙ¹š%m£§R·ä²û)^pz7±ÃfI5 ¿ÛëÈeóÕˆZØÑoíÚg²±®=•dKÄ~Á‘ÉÎ@áfÈ—/zZÖLö7Ü#“)Ñ lÐÕ~þÿšÿ}Ô¡|ÉjË»Mr"Qö±|3…G`¦…í-Æòñ!)–øRÃ|ˆå¬5!N²öË×sn02ùø  +æë[èï²oé +!­ÁüDWDO]1à´å'ÁXmˆÅ3Fòqø±S·ü3}ßÄíÃt~¸O€yÚQ3%iö¹ÜSp&x„.ò‚+L² jŬÀ=q +Wu\‹@÷<\:º7àÈî¸×G¿êpX±ªk?åÑÀø'~CáS0uÞ Úìç#„ŸJw UÚö3Qã5„¯*B•™l΃áa†ð7?_BøIÄ“”Ûõó@ÛÂßVd…>sE9¾˜m£Cøöͯ7=¥Z Übß±YÔøëÛ>–ŸÌ‚+è¼ÐË'hÐ/qJ'9%kL?a®«Ê7Cøfa6ÇH¾ÝëJå“_[#ù #÷· 8í#ù ç3SÝIø'—ɧþS1É/Ži·ÿñ¯¥Þ@]&#Èm¾!_oÛÁM97,ëˆH˜ÛÖðÒöÒùbÞžÛÕõÊ‹\“9®l&mÐc&LlT¶¸‰"BH8h‰ÐAböp•@³ ?Àë«:¨€‹Õ¦ÛdØ+ v6Ø;¢} +Õ9D¤m‡é¹vðýÐ$hl|„¢}Ší³Ín^f—÷ósŽÛ Km• Þ–è`ħcR—òJh’:ÍÔЃ–J€e‚£pÁ²AñeµhçÀX Ä5ývQßÓ)]ˉ?˜Û|ó:cGTG§íb†GºÔy4,ˆÉãñþ:Äp¶ÔˆÃµ; ”ÏxuŠ°ßÉ/šÍ ¨çÝ!¬ªGJj®×Õö2Ê®Ì:ñäßL]ËkÁ¡ô,(ŒØj.šm‘©ç9à¼6¡éD¢\`ò¢KÔ7¨4³³Šº+Ή³ò˜Êã®V˜J4¼¨ûʨªº(ÙM`d“0íOU¬8JDªj¸í"¿bŸ×@qf?{_c¦†=Qቀ´ªCäB™~bç¨Ï½ +öòü¼Ç `ø4mš‹% ;i|ÿqø½úLrøjÛŸr$ÎaJxB­Ç¯²m¡mÎÈmÕÌ"§ .ßI†ùœ§bÃtƒÙÆÃM#lÄ÷áÉ’v¡:<*N]°ÿq8x7­iyŒ\D‹iŸB-w ÊlØQÎô²Cƒ/#㱜Ïñ&ÇóVüÂ#çM³Z¦‹¸É3¶,µÍ(W;Õ¶ö&m¨w{Ô'g…»Å…gÂ.uUjÝUN©»¿À$&àÏ4Î)ÈÕäBŸp¶ÛL©´Ë6ƒRL¦z1ćû›Äô*h¯‹J«Ä::¾´»9@ý¼PÌ ÃïÝÌ¢tæ‚ãÕÛÝ—?x©Œ#~ÕÆÆ£`‹¡ú„Ç;2kFÀFŠ¹„»FdÓþ‘:þÿ]<™wÐüÇ +Ä;ŽŠÈÈÉNÁCÕ7‹öÕ,pŠ1¥ÖüwB&•s‚ÁWN¡µ ¡¤8q—?0ÖNv«pל½-ÞE„ä, ¯4¼h …}ÃÞTc3^ ƒü÷H6°}7+pøf#B P/oÒÄíh“‚M®¡èI«ˆÓàðòƒÆ)VqPEás%¿ë ˧^à›@â¼k ;AFÙÔCòqiR¡cRÍÃN²Ð†è¤Ñr6ŽQ ß´—Ì"¬zŽP:ÿªMÚ‡Mü¸0²Ü§P%ÆÀ$ée[å DÇ}üq‹$—>6oƒ±qÊ£¾ Y'&‹‡1ѯ‘]ÏÜ7?oB™R4™–¦Í›ÃÛw…qSS„™fŠN„LC¾I ’팇^ô°r¨+Ò˜äÞ{Þ¬¾}$Eì„H÷ü`ó´áÉN_]0%M‰œ¨_ÜaÖ®Ì9¸ÍOÞÕª¯L*Ï©ç ¦Œ(äDðÃïŠعmá®éõQ*â ¶yÃñF° +ü†É½÷;+(&œ¶L¤/mÜW[b'vê +•_sÇÆ•w—´…2(»…2¸õü€è’²•¤ðÌ&* œGÚRÁyl´/‡ç€‹Ì¹>&§ÃŒGÆrª°ÙDÔñQ8£ž‘%ƒæ¼ ‹Ø‡ú®'¯ŽXX+ôÁkÛ!‚j݇gøž#‚º‚ шîx”Rø«€ð;¥ÞÛðR™•jo±áÇ0Ÿ­ÂXš(…^C](NS ¿}1#ñÖ$ª’v +n×À› ƒ(µC JkˆÝ!G<ã pø¶–᎛LTÌ>ÆI™UÊÇí¼#ŠñH²‡ëDÌ6û¢6 ²}Y*z“ÅQ¡)‡,ª7™ !ÒK"4€ŠJÌ(ùÜQ_w¨<•NìjHÈ ƒ,\YP óšA#qHâU‚`lŒ1‘^dö“î8ð^È×¥yLÈå©ÔŸ3Í0‡.½P¿SÕ)P¡Ø,bŸµsiû‹³ñ’I ÏŠ¬«["L†jo³éà\ @ìÀcS8ͧÍL=) s+´ +´º¨Ä=ª${ÕöÞ¼è07¥‘mRì€QW[ÈÜ<ŒPldŒPܧSïQ9Y¥_úôæb@Ó‰&Õþ²F4-÷ƒˆî7ú^jm»šðDÚiT]µ¾f{“Ï +ÛÙÂç±shŸÓ]Â;hêÆ& O.·ˆ¹«$ÍÄ>EîÍëc”Ø„ûÔ³voø”æHôôg4 +ãÌQPÃP©ªrd™ózÔ3Dа²öa‚…^€Û +\¨¡§,ËÔoµnSUq¨Ù?¬4Õ¨7£.7‰…ÍŽa@YЃ§=iëuãuxSÓÝ/qïÂûAÀ„¸MÔ½9tþêL‚zhŠå$|._'¨±û%Fû~3hm>“åp¨é»Áý Ñ]¦5„©p_Gh'œxëYÀ˜•.ûÚVÞTTÙ*Úº¡F›¥=¼–gy'2±_ØS7¢f9jŠ9‰lÞ({MÏumCõg”d/äî Òè˜Ï«(èqØq²;“-Bl­lbÊÌÀÇ žt³H©ð ´jÞ¼”–¸—)™š6Ü óÌ£!ÖB „ž‡/ù¬;áæÉjê²yfÿýkç¤àüÚF8µã-zœ1žà×di«G7Þ“9€SÉû4 ¨èÚrÒíl«»½µá=™/˜ªPcªZÄ‚ „"ÉBªS@å«…«zÉÏW1v!—Õ3EnÆ1¿,±Õš³!ùÙ=ä»n¬B‘Ì–ëŦ¤PM]€iŸ^öl–ÒCiû”ªÃ¦ÒØÕº¥É[>*Å)Ó¼]­äÓÁ'£Í~\Msu&ò·&Sú,€Y-|äá(€ö €Å=q<0<€4 ¾ Õ˜Üzï +Ší#5Fçt^ª¤m÷•|—à$QˆÌAÆuO[#øXöj+´ÿrz„añÓ +~ã¥J.Ƚzô÷ÂîY.~ãæ9«‡ +¿ƒ"=B“h»;¨ëæ윯½‹î³ÉcËyº_¢?ïXÝròÚ…àéÎ-Ü{Z/É^ÓTñ…ÈP)AEøéÚÜ n +–j‡Ýe`@@«‘ÂœZv^BŒCíHÝ¡æIYÄ ·ˆ\B¯Koó)ÔB¥¶ ¹`4T½„p w¨]ÃìfŒÐö‘¨á ní=¢.Îci‚ q$…§¸:Bæi­yŠa«¢–«$½NwðWÜÕe*ȈQ4^¤s-h¦îI䌣MSé„D}ðvk@17\‚€\®Ú\vZÍ®xø¬O¡Dî’ÂâR¡&g G¢3$yˆG÷Ú‰z9$ê5ò!<*ö•Bð·Ó‰”^Bˆi¥z|U¾ò-k¤–;h&Ÿ®ÒS ·á/ظ)?Áâ2š¼(%ÄˉAgpzÕ•l¯<²%x͉TÅNd–¡¾ÀÅ_"÷ò9|ŽSsƒ‰Z8rǹ[‚ð3×¢•²5ïUz +ä«æ‘È +Ù|$õšÐlßZ“K’zÆ@Ÿò +<†FG„˜¹®fEŒ&ešbCMW…]% Ÿ H±äCØ´?1Õ¶Ñõr*ú¾ªî+¯™)½yPªÎü ‚‰E¤!¤_æø)ó£MKo¥½-¥·¬%]J….¾ZsN€äåBºJ`jš·ÃŠ~H{±tj:˜»ÍÏ^Ógh%ß/¤^„K²¤àÔ"v©'ˬž3yLEkÚ˜ð… Žfy]Ш¿Sý¤kV‘•¹µµèÒ-›ì$bÄ;ä3å¼3Ñz•èžHT""eÖ¼(t?WWöšÍš6wÐ<¢rëÒU!½Í¯†‡\ï£Åldˆ‹ :Ͷä³íoä•7t|üüÃöëæÃàÖBÊ{¡ê[‚¨ +52AÜ­•6[© +v¦Ð µ>AL‡š¡­¸_KV¥j—‚Þ™¥O[µ*§¦Ò EWA_†â­­Þ E`A‡B²`„z´÷&D(g H(‰ VL(­ÛZC³2/S¡¨/d¡&pkØ­¥…Ó( E‰Ñž\‹ƒ]úã®H2˜·¡Æ2˜É¿Ñwª.3Ù¡¢3ê¡2tgï‡Óà6„"Õèz¬5®[&ÔÊW(”Ùw*Öñîܲx×êÝ…‚áà †ºã­£ê—W‡5Ö@ùŽó¼¹kqÂg9wðßgøÞý_kÈ£ûÊÏC("”±oC¡~ Ä‚úU‰õø» L¨æ±Ð0ÃB¡Ÿ`UŠ} (ÆÐÎ0¢W¡büŠMk-6f¬¸Øàñ.ŒûCÖ(`ì1Y#‰¡Ee‘\]B\3´Ê„Øhh¹yZ ;!2º~f,wö +íÀ±Óh#Çn¥5»žv1íØ3µÆÆCÛU«‡ö­mx>´…è~ì$[3±#m—iˆÏY3±9nÍzÄ&»]ö$6ë­Y˜Øð·frbãà.#Tí À*ÕaKfI£(Üev‹Ù)ÙaZë*à¬mŽ‹-èw³õݪ_SeàB`{LˆR$Óm•$²t—¬ƒÃU´d&6Ì^k1éÇÜ“Kt»ó¼câ»{vÍ:N-ûÉÈȸ˜¼¬¬0¬V3~„£¾æ@eáœ$‹0*Bi%æR«h¶eN¨ùÈö¬ùØ–ºšÑ©Á#J·ËêƧ¬Éáø%k‚9ŽhMTÞÎÌšð~Ìëš.W_LX–5í—vÍÜ?vÆ.ñ7ÖZ@7çZƒ÷ø®–!ž•µ&"ž·µ¬âË ¿Œ‰^á«Hßœ)üxß/]hl¯8ÇÓ×z6~Ú[>ÓE<>1ÇæZ›ùö l;öTFÔìMï·/œ!Pª8V(þÐßïpiwCˆ}øH| è—ØäfÒe¬+{Ÿ§ _…EE‘­RI~ýþŒ‡§Üž1/¨jì©õ;L¥æ"A,l†³ÞáûzÌFøyœÈÛ 8]îΗÖãûn»ùo~ý $DU:k¢ JUóÓÌ¡r“xȾŠþIîû,‡‹T3ˆÊ•¯·?šÒl±ÈÐÑÜý¯Tï‚ŸH…ºÀ¾–÷Åž¿úóf>ßT‹ƒš£`j¤m`¦Ìœ„ªÇÒËýöýèžDÈ'™ö(`´ŸÛúJòÆ_¯ó¾àKÓÿùû¶ÝnÛcÊ šDýÐFüâó›}AYH>T˜°J¬}Vº\ãUîâH*,9OšÉ䥅·÷`©PžÖŽð¸H#\Y‰±ÒEQ¸‡Š©]]xÊ"Ì㇬ú h§VÖi O‰»èµ/®Ïwq¼ß׫̢ÑI1÷šÒåÀêñ®^Ä6C¾ +îíŠpÎâz—¨¹¿ýV1ý0æRžòæW‹˜Ú¼p½cýðÏ;EBÿU@M—Ê1Ÿ„j"D„§'Lðú6ó3ˆ|Ò˜eË?³2‹D?3¼~Â×à»P^ÂoWê!(ßÛÂæoÎ&roÎU” +ÓÇ!XB‚@€š±ºù–Ulï7ñµÚbiø44˜Œ•«xVâ ÃÕ¼xY™W¸lO594q +e0i‚Q^ê:Eºew$3bûÞÿç=i‰ìiT`'O"‚”’îõôÊ\›†öft¡†ð»‰Yáü>¬.Õ €ïèѱù«ªÖMT]ÕØëû:I™"Ìaj}>øl¨«‰(²&Þ¯uøôÐ*?qQÅRß& ú:XëÔ?˜ïæ”fÔT4ð¬ë¦ +êòÑ6Kß'®éõÂG;¨ÝÞ§ƒArÚlÄ6@ÒPœÜ°™[v&Šýš(v‡‚²ÔA:„Ž¶9\­{G3üq.Ã`w§ÚdÅ%zîDȳmdƒ¸S)økµÐ<ÄK®£4 ÿãŽÖ¬žûíO õ.œë¶<}oƃW Óî¼i}¶Nž½cåjÙ›è×)¥"ä¢Fš¼†Þ´.Í.*—¸PGÀ&ò~‰»¤€|KîÜzovX‚y0dìçn{8˜‹£òæy×䕤¦6®ë¸7'7¯ +©4‚Œ4y¤4Q§U»˜1IÂÙGì©ÚAd'àQµ¦v˜«¥©L)·4DA`jOÝM]ÛVvÒÒ†©=±?˜i-"eûz‡¿iånGÄ¢œLvH7#w{>–Ó4]We| q/ÕÞ*’wŠ·@âŽF#~Å°Ex-tîàqЧT+û{Ã羞Èø–|îO¤Ýôü4š}ûžÎÝnybs`e!73ò™„üzãsG>PÎhæ=Š,CåS’IÇ•ÏÝ4ð!l¾IyNí]Žë»—=èÜúí4~2÷ðRÍ· !Z§v|÷tîfM}ÝÈ›{…vñ€_BþÓD–©[z ~ñíFö s¾Ë•x¤DYÑ9 +¿™ûGÛâ€òåÛ8F¶Ôæ‹|Ä«æ‹|Ä«æ |Ä[Íøˆƒæ›|ÄAá >â­š „ÄAÍBâ©æ"ñNÍ=‰W5·-‰·Z.u©‰‡º ŒÄ;m‰Wm‰m ‰wÚ.¯Ú.¯º,2ï´]¤(^µ]¤(^µ]ä*Þi»ÈU¼j»ÈZ¼ê»/Äo¤ð¦u­˜§DæÒg1µåï(‹/ý«F6‹™ #½©;óë:!ü•°ø÷؃®˜ÊwµpOuGC}Bùf gmŠ7ÚN‰¨Ϧàe*È·í úÿÏf=^K§·¼mÏžM“B5‡ÿøà–hÓ6%–5y0oÈtAœ™À¯ZÀßókšèô°™Ê&cÏ ¿&#!Nn#é"üšjŠŠ‰o%¶v› ÅhxÉ¡“¿&.°™PÙ ½&I& Ä ÍN­Ó–^SB9ó½É»½f[)Žzoè5+TÍ´ù›_¥Öç-½fMãMÄûʹ¡×„VIËbtK»ºÇÛgK¯)ìÁ؉m%²k–QuNî1wÃÍ&­½N±EOG/{šMï]mÞ»J¯R Ù¤¯Ôɶnüîi6·‡ä«”ÏŽ…ðÓ#ÂþÎ’Ž ’O_íùiBê$€g´#„´}N b>zŠ? ל/ÌòY2­öxšêݽû»¾°›N3ƒ³á~5úWR`®x¾†`ó÷¡(/Ÿ6ËÐ>ÿ #êŤã±y};‡ëÃ}ò ƒ~o’Mr&Ä7+Êã wäˆĤé³ÃÜ3¸õ”×ÓÄÕí™›22Çȳ˜1PÑ*΢ _™üÅ/7…cd^ÕÌs˜ë"PHàFh4e‚“rªK¤¾TŽô~ 1!{ÁXlÊÍŽÐáÀ-©»Åy$ɽ[·ª!Ó+PT‰Û±jOÏÆ“7Û…H¶ jâ´¡áË‘h·ÑÝDïÃûÛ0†@~4¿i–ZUO¡Ó›‹öQËíÇhÆ=lƒž’ò!tø!Píí¥Ê¤»ì +½aãù£ ðû +á6+„íã;pŽBƒÃ ¯ ‘S0 +ÎÂý®p¡aÆRØ¢Ûÿ=})Ž2Üþ¶…X_ j=º›„Äþó¥-n—ò]õØË,€*~â/oóG9jóÂÃ~úq¸¤öhT9ÚfqÐ9„ì’:QÖ­`Ÿpj³à"Þì(3ïO®Ðª{mv&æ u&“è‰Ùlj~"° “s僊ðÍcÀÕÄ`7±xkDë§`ß1ÉåôøfDB2í²çù ¨0—ÇÕÚKœS3%lùh4«ùÄØkCGnãiÅ›¥ai)Œ(úN³dü¬»£‚«¥ç˜ÖØ ì²ŠQ +„™\wio»ìðJà6¨Nϲ¹Iä9QGNí}U¢r^4&úçÉ^ÏUøy8›áýáŒo??ˆŠ0 AÊ„© Òj»$Aê…¥ ’3l(ßí®)·Ã–Œ"ÝÖ_Ò3%F=ÅýÐa°ž‚)® ì.„»§aíÈá~Kë7)%ŸÂ;"½ŽÃêt‚·3K:kÌ,¬æ.Tç´g +ã4ä9TôSÕ“žËˆ@ÓA«©™‰‹Bl"”(æeœj (êÙ nÿòªERt;Ž† ÕPªÞÔ#ª€ üE‚#wm‚têMɅæ#ÐB[A#´ý 0=D.DAÙ­ )2ˆ3ÀH +G#©ƒ7XçK9á²0ËûrºÆ ^¡¯‡¾‰pÙg¤!¹\…;þ×x“€ +€Š„¿gáB|û‹ñ×.ÔzmPÔEKkò¿°´'KAÊ7Ó4ÐÀ®ëP .ÊþŽ);Gí%8GZ›NM Š¢‚³ëܧª;3ÃõR( Tl eU<¹|kÀ”}zw5%&Ì3¥µþ¦Ó—ÐLg²Æ9àTŒj`¸& JUs¯wàƒ£J~¯N_âÅ'ŠÖž +•šK…INXpŒéj^èÐ sQѹ=G +¾ªEYœµ °ÆD‘žSÆÁ pEÄg_::¿·ƒõºá3fè¦RÿG4|ؤÏÞ¨)L–ŠŽ +¢«ûf{ârÒßôiu²9`X æ‰ïÚ½/´âÑe¤sÑ}«ñˬ… q?£VÛ–×f‚†ýßñuø-¶™?B+ +ÜC7,Çcñ¬Äël¿œ¯môû ¯ß/`¿šI^`q)Ø¿ø篳+ "ÂO ¢RJñnÕ}*nVþÃËü¯æýêo^þè¿ø“ÏŸýóù»ü+îuç«C€»}ôoØý +ŒÝTY)ªÓs˶»€¨aVÏy%óá Ù‚nîóÙ†E©«IJõ@‚DVDG–´ ¶y½®/f—`BÚú8TW©¯™Ê—Dêûy +ài³7Ùʬ1yfz3‰ƒÖï»V20“´ÍiÒ*ðï§=\UàÙaÃA‰ù¢ð[BPM.¨øjÔ‘ûâSnÒ=Dq«®ƒ‚fÓ™`}™Z¦P4ðÄxż å:˜È’ÒÒ½UŠþô((Í$‰H<ù§4Àðè“Ć"ž§lIÊd Çó³`‚;„\W€Èšu5ÐU;’IôñÉë·MÏz¼"æ ìÿ ÒÙòÙŸêeõµÑ„Lø¹•jBê©mJ£! FCf—Qx9y–LUPï6Õ'´`µ‘×ðÏ'Dƒ7UYifœ5+ñs)°w Â±q +™¨.bðÜ f©ªøfªéþN*Ér2){ªSâÙ–IÂ]¥îrpÀLyìRÎ+ÒƒIk™Y×í+ß2E×£)¦÷TØP{ØqúPà@šâÔl\Ûd°R¡ÓÊ Pt_àªf`› ½Sî’@þ/Ît`%eYÜð‡XJµXâQ:|w‹3ÐNí":3„ïn&â¡yðô®OOq÷…L%îKš ÁŒ ª­ç ¾]²jn6¡ùW6½ †9M9ÖC`T9\T=ËsSÉn*áa‰uÖ,³c.Ç g:M×}°¹wÒoYá³#È ‰z®z&KPüY"½£›“à´|e¥Š•/!»$2ªúmZ‰èŠ‰PàÍœáœ0à®B¶¤\Â'Uf7±Yâ* +/•3DMG.›Ç@¾&ôÊ*•æ Û‰·¹¢V w˜ûg +Ò³÷„›ºC>ƒÐ9m)B‹nÛ*)µÞÐÏaiSO¾Lvld0ÙkyÛ ¶UØ BÒP¨e6§S’ LSx8ôÄ.ñ™Î? v¨x@Ú¶÷ÏŸLøPJuÕ©z]±þÁpTHzÝ»‡ùN•§Ã0fÁ0mu>v”ï;s*Nqè™P¥¥Ö$(0kâIºå0÷¡! ¶”éøMªNz@VÅìü˜†|¡¼üˆÛ‰%Èœ[*ŸÅÜ…´¹Š<ÛqP©ô‘ýѬu +)pÔºŸßý™*B'iœTÐ ñû|彴Χ‚4xׂd<+$_BÄ5y¦1#%¦ §æQTòÌ~QªªQ-.Ô²Ûé¶2—ÅyæLR'» t8;hcö5É+SŠä–øóv%G‹,ñʲàÕê¿°¶àÑ(¥n#ŸÞcɉö%g¡”ÏßG¼CŠÎÖ1ÀÜl[ÒÍ•Ù.oꟴu5!sà™Ô,z¢Ð‘©Bxº¨N9ô`Op¨»ïQÙE®J¾¾ê§<kú¨Q1Åî½Lâ(I‚g¿¯¦ü´àÀ]R¤Æ¬úƒü@¦/…PõŒÉ_U=_…w†«ÞÙ,¹g¼rþÀßpV”ì Yö¼ûU€Í+0lÅsÀNGw¢ß²/ {¶8¬á­º¹d¹2ÀýØ–©;¾¥ëNê¯!G»FXX0‘tØAi”‰g!\ê+v­Žh'Ïî"9XC¥BÒs›©ÈHÇ.å³:"û.0 l&˜(Ž— +ÈŸŽ[(¶ÜU?>¬¦*T8·}D z›Š¾e†eS:S¹WSI‚$ӇⳊ´Ró.{Æ(ˆw€%EÃñŽ¨ ä 9ZÒ|‡­ÅL&›•|yC—QÿŽ ô£ë¤êD¬©Uq`¡©Û/´}MŠãû”ÕÁˆ×)ë¸m<½›Yp¼u™CÁ‹†P½Ã`¨ïÂìjµ@²ŠLI$Œ—±KpÊßÂuÕÚûæ23í1{w€º(† +U”cO7±Úå=ðÔšc~ž »ÑSò[|醹W¡Ø1htsÝGC JÑ&¨¦¬€Ñ}Ždë§s êžÎ´LÇ90N“Fb‚â¤5§QoKŽgØÅx[÷Í$ZÓXÃWõ„-U¶¨>gá®u¼¬žº:álµîbiÑÂ6íé]û·Û™ÐÖ:wA7S " ϡ‘=2–]w¬ßîØŽ)Üæó .‡Î»%˜×ù­‡ŠwÌ:¬@ ´NCXuÚˆE–´®7˜ŸÊ„#Ôs«ÎuÄÈvŸAˆ®çÓ‡}ôÍ~5—Êï ’\ÓfÏ›j)oÞ_Á ÛN$¹ˆ.wmwkõÚ“åìš•ä3YNÿ‘€ø¡lx©ú¹™fù¬tœ Œ{IYvEEÜ* anw”¡J ª ‚/áuŸQ‚'ô71ù³VðÐÂ?hK¼Ü “+·ûÝÁXØTŒ…Ûªy@HaÛª´.X{rß«:¨ÅÕú½jJÅö¶èXÒV‚ÝÝhhõ“Ÿ%êwÅ+7öWÛ›ÄJïþÿdûž´O† nfëiou1nÌ(õ]w´ÀlÞÎzíí·{Р¬Æ_šN±Y1¶)ëÆ€4XPÛ&r¨ú‰†¨V hÝ•³M0hAË¥OÛD‡(糨Qb Ö«U-hèº1È5®skÌWM¦~´L°)¸vìí`‡^NÔ»ºyÔ%‰ƒ£×½w’Š’O—GùL¸ª¯‚7îØ:Sá®à‹ñ5xh4³07npžÄN xFŸm½Áì]–Á•´éyÂúdcù —6Üb46B¬´ŒØ$Ys(Š·ÍF6MÖù5£º +"©ÂÞ¡tö]üÕ¶Ù¥êÑ%h !첆{¥ãÏ}Œœk§ÔYBÌ@|¤k†¤éSlܶ‹cŸ" èè÷©~ô+ÆËItõ¾‹µ?~âáyÔßÍ|ŠçÓñÖâNÈÀd‹>ZsWÛnÍ> ÉOsRˆC©çšÅÐ:^m”nГj]TóZÝh£Æ[„)âñxÁB芹šU…út|«Ún€dÛ6Í ŠA ¡ Xt1ZÛ¦#.8G§lt>¿Š\€2Pê¸YçíËVæ`“K;ÉÖ` +Ĭ=zìÚ§ TFŠ5_[{šyÎç Î+kìE+”¶Ð”må$&4<ºÈÄÕf,L~°^Å«£rÚ>H0QsAÆô>„H5‰ÓÆý×`ˆ5Qg&Pyy¼º^ðÁ1”·ï2‘a9;ßrŽäšâ1ôkô bÒšPØœ.C(¹QB…õ1¿Âó‡¥)af]o«Â'at*f×±’wq}F©xx,=Mä(€bŸ´1Í}¢îr¤ÿ})yÿyœ‡ZœèCd2v «{ìÅ«:H¦*m!ãV¾š×*põo|œ„ îI>2‰=ež˜ràÿvž:çyƒûBí!äñ´yJ¡« lŒrXg€ÇAì0Î=|&h$ÍïO'"&SBÄ7Ä€ö2¥Šp Ôµa{ÒVã!‹D0#ŸY-wõ[dà»MñµŽ +†Çu2ÑVYüègµØ!A**8l¼ÀÇ›’È +Ê.ÕòÚìO1Í·دì[Ó<ûS¸ÓŽ›!¯1Ã’Í© ‚bœCsTÝb=?.a§–áop¤NuD·>gÆ–†Zë¡ÄD`Qiý¥\bøýSõ‰Ý Êþ6©"Y“lD-AÃÚæV;ðv*hö±i¼Â>N8 +C£Û(ìlâŠà°Ú¼N3 bÜpöØl«úÓ†ñ ­xÒÄqº¦ vìCÐûÀ>D z4_ ÀÀ¯£ç8´¸hqúõ"fõ—Õb\°‡™N³^´Ç!ý&Ù;S;éÉ®«ò›ØþBw:™Û˜‡¦Ž\HvÒ4-©¡  ð0‰:í½Õ0Õ<'øiZGó¿ãÄŒD/>úT5«‘îø<,Ë'c{óœÅ`giÁʺD±ECX¼ƒˆ¼p~ÍQïþNé%TAP<ž#x—ø‡Åm©p v:Xì³óÀ> îbßõRà‘££UåòaO9ïæ‚4I°.ìý{ïŠ ’B÷0½L'À¨ TÀ $R…FªK ´Æv×ô³Á‚Aê¶:éÀèwváÕEróX×'W4èÄ@Óu.† D®3PÐ)wµ†À!.)¬³^FÐÂdß+8°Qô2PÿÕ°ë;ֿnFGø£”çÍ<ã)"´! ¡ øp3 +#è7eHÄt’S±I,oÆxؤ ÈâºR‡v½Lð¹Lª¢ÝêDÕt/3ø` ÏÁ±þˆXUèê3M§üi òŒ0 #xs±Ô/3.Ví¿Ò8‰öÍöˆ¦/B f øŸ®EF P×=f¥‰|½Ü„ŠNˆ®}j®îGèå)rX™óxf¬‘•v¢)ûU>bD(yùÀh– „›ÖäG¬LÛ…ÊèÕ›)e•HÒ®QT½®f½ÎF3‚K0VÄ0JØæ Âô2ã¸À^5C&æ°£BX¶§Æ”côβ¯ +Öt\ÌÜ~†qÂ佡Ðð!Š-^© +kF;·X‚ápÂø–ȉ²Õ—P…È;¶è±âò¢˜Z`ÇU8’‡˜]¢ú•:dÞš`ž›v‘í'>`Í+€i5÷Èš•¨êËL¬¢òª1ÿDJ•Hñl¹7w.€‰ r@î~¼keÈÖûRØŸà~Tð†Ö¬O¥T¤E@÷ó93$Kö(>gÍ@Åo wlÇî +s³fÕâü®Ù¹§ÕYr{ai—¤àcO¬)Åý–ZS“qg®é͸»×4éþ”¬éÖxØÖ„m<°kâwÞ—üqk +:Jœ%“½•[k>|•~!¥äfHÍo¥nHñ¡ª¦˜Ÿ5ïUÂZˆʨaª(”@l5Y(¥Š0”cª:¶x…½êIVÊR¶æB(o ÆF(‘ vJ(µygÜ„B`…bŸ`L­5C[S,”C.”/0”ASòÇ]9U4Iײ¬`ÚþfÔÑ<•wEy-›&u¨/Ûâ¡N-˜ñ¡Ö-ºkÉÜÞ¥X+î‚g2jõ¢+3ÿ°õ„æƒ#µ–Fgl­LÜ;ukct×ɯ¸cëŸÆ»V?w©úŒ®òZ<ºw¹×"Ôøœµ5ºÿ¡ vEXëjc b­ÍqŒµÆwYk…c\%”‡ÐÌZµ¼ðŒšç5>ª¥cˆi[oS¡V;ĵÖrï‹Uã»xZ(:q¹P¸¾FôBýû6êèC81”àÏäZÀ¿ Z®]#ÎÚÖ¸hh>ØFUCCÊÎö‡Æ ÝÛ(pèÂ1äп1£Î¡ýcªÝ#!Ò:PB<ô±lcìñ9#0?ûfB?´Ýl¡{'äbÐ’yˆD»¼ENN4ÈaW–eÍhˆ˜Ò ‘­¼fNd‚i9I»b7lÒ.YP4PÃÈξ^“64Ó +¨ ³jL÷@—‚±]Š7}›+¬ÝÞEMÄ%fšdy‚µgKîUýk’ +‹Ëm\û¬á5Ã5 Ce¿Ö´L·˜<‡†)ÃؼZ­Âw̼e¾î˜«£êIP… …+§¶Kõ=~¾¦ã›×¬âã{×\äç÷£\3˜qŽÖè_ü0Š®ŸæzÍŸÆõZ3¯qÙw‰Û°{Ö´oØ€!aöñû4óz +ÖÔtþ å7-/Z +j55OóW€p!pÅ•¢ãy¾:àd,²©4•,lOmü1¤=B^€< ¿Ä÷ÞD|¼ÏTÚæ³×;ü¯£ω3wc &­„_\ïxËþ¨Ð¬B¦&¸Nl²¤&X~~ú£™Šã®ÁÍvKdš§x‰4}sG×OåÞ|ÅÏo¡MLT¼½9üaùÞÏ#ù|È%,÷ÃýSÖª;΋h,øßnµ¹=f4ÔÍl¯Î +Eû픂 ÕS^lž3æ.~Â3þùûÝíPX›(hßÊT›\3S]q®L|Q5};Q€®ðlÖ?á(ÈU +WÒòæíW.wúñ-ï¤ùãÃǵ·.ÊâÃyù.é>t‹p 1€ ôÏÛ?Þ¬ÍF@DŒE~gíåÜÝqë§[‘·ù¹‹ƒ·Ž¯ûüNƒ&žÈ¡æËL9ºöV1$¬põN˜ï²Çêmd´€Œ7ð‚P O 3¾ãƒ¹ý.Ó–½H\·ç§ôóˆN{À–J¤k#U +5ò@ÄFW‰—\(?O‚ %¼GU”¬2öæºfðÎ Å“ XRŤ@b©Îd£0"1²~Ë÷VÞ®WQÅì\ñÓ$a w¸ +u“@{QcT; ˜n&膸½iwˆ+P94E¹:ðL´E›ƒ a ¡4`aã é*0JÒòAõ|;gb®S¾p}Õ*In¥Ö¢P*ü\(\'xÒìÏÃÏ¢zY-T¢ús% @ +NHU:K(pKbVýM·ÕÒ'ÜU–3ZÙh ¹¸«—L¸jPq¶ô$ 6º’®d.gé‰ÿ2‹ik‹esê:6p)À(£Ý½:ƒª7-OIžÀé`êP‡¿ƒDDᎄóüÁxè8! +)ŸJ}2Êå1úÞ @º7Ó¨(~fÑvH\Pœg §fú°˜¥;çboÍ™×ýI# j"R÷~SÛ6;²PÓí§6‰iìÙç]/¡’BÉeâïƒÃQœNʱé0æ‰*f!âØwÐ;ZÎâ:¸í‰†+“”$E* Á’˜Ù¹ij‘ +ÂE±1*uj!ãï˜U]_xÎú)ÀÕAJuˆ×äŽÂxÖ·R Ÿ¯uZdIÓèaoÔH„¥âÜ­Ç.,×UXnÍÑog;Òù ¯?Ìàª(ö+κ Ag…-TßvgögPÃëÆž:|{,‚柧*Ø á@†!nv°a‚|ø²ô‘õyk¹ý³pÑÞ/.Ú[ètYgŸAÒVÛIÛÕÄ™¾øÎEû‹öküÖq.Zr¤Å)….5 j»è[͉&#c“ÁÇ„ƒ¶Z-‰dVÕI/Ø{8óéÕ¨+R?¡·~^€ :Å’r7°¼PÉ}û1!àH¿¥ì1ß¼‘´ ÄWlH‡w¢àŽœIÈ$ŠÅ:Ê  õ(ªT„k’kÅ<ÿ { QSógrðð ,€G¥ €*ÍSíäMxÖ@Ûžúßôœ>’ÑÐI‡"|Sˆtº˜ž¾0Ÿ“–œªßH‰ÑáäVý)•Éz™gΑ;†x€D«Š=RŸS«{v/ìWaÙŸÛ£5‡ýl)w@U¹fñwQçhwØ-ý-w¬&à£ñðAð¥j Î… +-° àÕ¬nÏa;6ÿbX¥TZ©7•1=fê ´ü + Âc§%šH瑪^U¦ù8¼šÞY’È·0‹Pç7ç`’2µ^ÿ1kK¢4säÙÙ¥&Ø¥´¿ûù@ìUiè0QhÕ±Önç)…ýj–·ûJA*ãæÈ63WÓæ9ÔÅÔ7ÙAVeZ¢¶¦‹M˜ôQ§ Ï•ŠD*ÔUB§¸ÔdºyAWPLñ"Vˆèâè ·´còbObQÚš€ŠFN](Âê\nã€Fhº¾h"3—é–$¡È€œ=45xI·Æq{‡T½}ša‹”Å*4àsr„m%‹Måe{@&ßÎ÷[E ÖµñNZÕ ´6•E~éüøæ‹#92éæÌ)¯¯ãbÂåk–Lj#¿Œ±Ju˜ dÔ"¸¢^C²K¥Š…™ø@ºƒí¢ür2ë‰2|OºTKèAme4ŒÃ‰¢,Ê”´wáÛ'Ð=$dTü•áÖ9½š¦+/v¦SÔIéHMõ5^DÚ¾¦Â. +×N‚\*!óÌ~sÈvORSHµ*° š×D ’`ü¯øAþ–êP€ŽVc›íQ˜¢:p¶ÒiZ„¹’ëÚN•Pö›iâu‡'Vì Õ“\»«ƒ„KUœ!ÐÎXÜæd 0¨O-¢ZTYJT2\(qÂh^ïðضUžßvŒjó(ÇtÒ 9ñÆíEYèØú”÷š²ÈÉÎ}zàøcþDg<çÇ”*þ°¹4wwøO`³E´%l{ø›Æü.B(¯ùòyúñ‡ÒX 9Ún&iì}©MǼÌÉvJ5™»WÎvzKë™R .€ v3ïÀÖµ£øÁ<7Ï oi’ÆÚ#@ö/TÊHÒ^T#%PGPõ_ó.;nT£˜;v‘2‡û•VµWÕÆ@ÙšŸ8c¡Œ‚5 OÎXû<[jì¾nd™"ÊJ ‡ý†¤öÔeÛõ0)põIû FV¡™lt$¢×IB&y6¶‰[úÎûÕ,zÿ |F¸òç êùUmÏ.±€WäÈ&ºêüE¸äuQÂÅkŽùOo Û®4j±d¹4^œ ˜ž¥z.Ô&Œdº ™Í-*a†¹õ¤`¥‚IͶ?aŽÀ“ò[˜èSé✫Š‹JªHyK-$§téÚÄ$Æ‘=GeùêDêþ/ãâltµíPVK¯U¸ =¢h³/|ÀRQëE”ás“³%9fw‘ÚrÀ‹ìÞ.¬eÕ¥ÆþgöÀN‚UÕmšh)Ÿ—1ß·ð0€âs:Únq¿bf(Ê:Œš:VÅ(S´H˜/Sž´ö¬4¡ìç2 å)õl›å«Çh7¯ +7½-xÍNÒG±ý~¿”ÓË…í§‡ý¾ƒ ôV5~;T%ö.v#©ÚÒ&n=ãÉËsN·ûÍ +§”û%~ËY½Í„¥ùáy7&o¼RV>ÍPæ:#bΤ*]ƒaRAv#$¥¶Â\×ÆŒûNm¥áõ3]ט@˜L‡ú=™ü¼M kÖsÕìu³ÝÎ:’&¢ôØnÏ»µÏ:SϤŒãFï}_3D¼U0uÔà^“Ðe=e_¯Ç2¾x=Þû áÆäJ˜Ã Ÿ¶k1Å[XÊ Ãnˆ‚u³©‚|{3ÊøuQWLÿêƒ0\zù¿ø»¿ûLÅþéþ«üÇ_þïþòOÿû_þ—_þý/ÿêùרÛ'õúF‚ôÁó–ûþÓÿÓŸüiùËÿô«¿OçßÿåŸýݯþó?üí¯þñoõ7Ÿ>ùå_üòoþöWïÿðßý=¹ü/¿ø‡_ÿ÷ÿk>çþì—ÿô2þñ’ÒøáxùûÿÿýŸ~øõSiÛ?ÿ_ûÇÿlÿåÿ¶KÿôR_þ——ÿãÿ<^þš;ÿËŸ*­ Ñ½˜SMÑõM<ìé2‚ˆ™þ‰ËvÜvWæOÚ?ñ§~¥ú_€«ƒ=Ž½qÒF{‡ºË@1=ÈÑr¥ KOäºYt.гzžÄQŽ×wq>l‡…ñ1*²ºðd\`[|Âf~­T•Ðõõª]§ñâb¨¼]þi¹ÜÇ•·ßÇ+Ÿþ·ç?Ò¢‘iyÕ¼¼¼ +öx8à–§tùñå¥Mlá¥óòúR´€9ìëÓ÷—ß²¾”†gÓpëKÇå奰ûš_žþÁåÇCþÛþÃŒÇtúesàçhMíÏ—ÝþYE»Å/ +„íóØšxÈ~™Ô­î%ßo¿xæ1±ö1XȺˆ_nzq\îZàJ¬´ñ€$`ÿÚ^ÇEBþ>.zËÆ€ðé(¶cM ûg ›DáiC(÷¥oÍd<Æ$bŸ^ÚèÌ2-f>9 =q.’_lݧ[­X÷ca#öËÄ_ü"µˆ\Êw—).©×»1m®ŒŸþ´â°ÉÌ0µjë«Þ]~z°ömý-—ß¿4>{¾ô˜§âù¥o—ß?]Íã%¼ôÝ姗†gO›»º5¼¼ôÝå÷O¯‘ôÑå÷/Ï/E‰[øýKß]~zzzgPo/¿i|ö|)¥äñóKß.¿º¬—øÒw—Ÿ^ž=ûò0Àß¿ôÝå÷OÇ%~XñÛËï_ŸýlÜÿŽ’hFøû“âðÿ.r¨AôIV±¾º q7m\úéý¥n7Ÿ—«ãñÃÍ¥ùÃ?IdßOë÷} Vc»Ä/Ú|ѵ|ˆÔ\ǵ9ò“ +ç&ûD |ô ëNHü×w9ý‰%AT»¼÷Gí0žÓo]îËÏã*­Rº™rqŸwR™w÷{+H%?mŸ ½Û„×yÕyýp-«h&ùÕ‹r ]ËÊ•rÍæycã©„úH1y¼öæÆt­ó»Ì\½ýk/ó¡ü*–мzÔµ3÷ñÔBÁæî;ôïO¯]}Vû0บKW/à‘t F†9ÿX\;²ééÂíGÿõÑ|TDeÉfúµk^{ûµí¡qMÐ2º˜r7fì#]SÕ—_ëãÚòá=?ôg_¥«¤ùÐc çmˆj?þøƒž/Ž…Ï88ãò ÿ˘¸ œ]¥„P'ô TznòB1‡~ŸÕd­k¥ø¥âª.•ïïý`îû¹šßqg“?¿ù ç‡6dýŒ`ÖÿìW {PWÓ}Í” ŒãCå•?W†_;|åÂ3“_e ŠpýoW¼fœÝ2ž›¯2v³ùGs\g§T†ø˜•Œåé¿ÎsVRÉ×üú¹M–/z|éùxÓqµÇ¾ïjpw uœc£97Abï÷Ýó›H€?vÊuŒ÷²3¶®Wyñ~îܽûÇ)KÈ´ ©q]ç”%·¸{NÎðývîÏzÍ{±¬ÇÕ«å!wîÓ¿<6_«[ð-»¨ïÎYMy,ŸéÍ4¯6Ê>tÍ>|L+èxãZOcóI‰»Z\¬ûÅ?Ÿ«wícŸ¥ŒßËÉõƒFLc÷E/]Þ5¾´¦6¥ÿ™ÇF³{ï;½Û¬~­Îß»·²ýþy€Úœêãnmn•{|íåHícS÷ó˜bö(óâò€!B(aªód³Ÿ~ö«¥Ö1µ¶Ç ˆ[¤1í,«ËÆ“©\žwÏÀ;Wó=Ï‚°t­WŸ2ÕŸT¿V¯«¼33~ô_ŸCÿvœóìwÞ¤Bý[Çî>ó™‚ aüúÎ×|7p_þÄÔÓ”w}|ÏòåcÎw×µ9~ö«K`lÍóö÷ƒL1žPsÿø›¸sn.ê‚üÎRúÜpmîÞýã\¿#]sÝç\¾£¶óY\6,ê8ݧÓe¾ÐVÿ$L¤sþ°¯±Ì¶ÔO‹ˆÄÒ¼ÖÒËæƒßi¿¾¦QTûlÿ*` ÇU¬0]³³2%08@~ø³_ë wÞ÷¼Óåg#¡y¯O\¾èq¬§´æ›î<5í~ïô‡õ˜¶CÝŽ~ÜWÇÛ³/§7_Þ¦ìY^<>&™>žI-«¹©&©Þ’j ó&4ª7Õ>¾1†DlíB:cRù÷4/é²yÙ½ûóœ¤+ó:²sʾ2¿I|CöÍ­k÷2;ºFû\­ 9ë•÷R]EÇn÷¦ŸËD°ÜßÕï4¿ Üí÷Ò>¨«¤þÚ>à!·Îû« +àð”[÷¸æ‘ç2÷nž‡TÊÙ/õ{žðiQQ\†È:Ž±!$w!xÍo¤°sŠ¬Þæf6{z¼ö8u4x¾ì>úqìîû±[/s0Çcö)K÷K¹,§†°âœ!aO:¨õO³½›Ëœ}‰Ã«Ç'Ýäæïûúy\=ææÏþŸ<«ë×®<>½Ð¦ÿããj¸ïœo§»a^kiªæùËåk» ·)o*V̘8§m{™sjÜÙ\öw£)ùM{Õ\§Ðû\Ì6g³Ü¿u'œ"ˆö× =u>äO½§*}þì‡êº6Œ, ¡ºèì†aö¥Ä–šŸîU®Ãú/s:%0?Nû‹uòö2´éÿÇÞ»-Gr[¢_€À‹Ì¤±”qÜ|"Á½e:º˜¨­‘ÎÌ D£IŒpá Ñ¤´¿þÄZî••YÝ]h±X”‰™qñððËr⓾pöé›á&äí¥ÝJÊýµ\:!PB‹rÂ6ZŽÅh¡O’sÅ«Ui]û;·¯û}r]ûv¦žøP’1ü˜Þ·è¨z½ÎØè’ HœïÙl©SP‘鈶{a*¼g@#2R+wÜpÖ÷íijóÐ6IŠ¼ê»0ñC”v(ŠÞÑËA®:}²M¿³¿ØÉþxßFí[:©è™Ò2’­Ikf‚nnðÖ:©7vãªJ’´èô….Eí½Ú˜·kz±Kª~DBCÑz¡UŠFì×Þñ4kÍPLr.ÎP«ÈEúÜ > Òj®]ʬÏø©>$Q7‹C8»‰&Ò2{¸a uiá‹16 %~-T •ŠÉ:ÕgÇÔ9î@º’X”6ªw¬HÙ[4eaì çuÑ’8.T£-ä+B¬]¾ùIÖû~jbª„ÔíÜÐ5ì¦ÖëÓ%‹ÞŽmgJ…©?¥+U,{ß5ì„äRƒú‘àÙ÷j"±z iH´7} xë¿ëGúØuqäê)ê{ònì#ò«o×Ò}£ê ²ÕD ÈV{‡â€²ïëm{áØ÷~rã{¥sïäP­ƒÍjŸêLŒý±qtÖ8ý•£ÌSª†lí»^>¸ ´èû;³ï¢Ìz”S—¬ãPlmJ×ÆØ[÷CщŸYSÿuˆý#¹úÕ\Ø+M=)ö÷8rìçúd …Фµ·ïäj¬îÆ÷[I±ôn¯ü[ÖíwÌØêIÂE„–Âr –kø.Ià{ǃœ[àdÝMqåóà™ùÇÔµn9vIMEMæm‘äs0¸œGǘL¿w”ô|peÇT¬¸ÏâpÓ—{èt“+Äî/â=ˆ"šqibíÒ‰¥š ,¦””éš;¢gˆ~iÝ;ïMZ¯,¶\ÒÎc|a/Äí2æ¾²ƒ‹MGèÞ³V&”jŽítR™'暣šP+\ÛOÑÞ™U+C‰«¢_G’¯~]%àb*'Z‘»aŒ¦áÒ=:A5RÒ°‰²F‹>[ Ïœšn4!Á”$U˜µ[bY~~eGT³«†äêÜŽ(6Ñ£v%g`æœn|B(bžWÎf G€ÌW^ìÆã>f=0y¹ÔéÆ7È›S[F^XQVÏ^EÍŽ ºW8Fék¾#¡˜}Rê0r@œÑµ§å;ezˆø– ¢ _+5Ênà°fùVÐT„Þ…æ­W*ÚðNùîxŠÝ‚üák¡–V+•z¦'Æ€çÞ)wÏV +"½h[}?¾tùÊ~‚ £4GÑ}[Er6ôGúýqfÖ*5.” ¾V* tR+mlÀ­F“hÌFZ1}¸9fA¤)-J\Ô#µ(ÐúNZW"²Zc–c¶¯ÐQKUQÚe¿ûx$t‡uKAù)u÷ƒê!vN…ÊB5¤Ùð?â ÒTö.ß)ß•KÅø!ZXuVè•jñ©PCöŠ!d½óŒ¤,îÃå+åSŽaS;®ÍC€{åƒEn™JZ̯ÇË7IkÛhƒjÿh=vÝTÄ7¾Ó}ªÇ-¿Ý·“D@hX[XÅnºE3æ¿NŸDÊ…Òz?iâœè;»OQà ¤Ó±D¿[~Z§iœÅ/Ó„»jº%ˆ;Ÿ„Ì+6%ùqèz„júnì>êj›ÞÕ‰MÏèÕ{æxT NúaðFÕÛ q,;.ªðÏ*ÊàÉk„ôƒV†¤´ZF½‘´¬ñ0PG?špµ³=¨ +ØòÓÒ%ÜÍ=íÿZˆ¾Xä±ÈKqŸS?ax.i ¨BtÓƒÚUx5wѺ»Y³„‚ŸÏ«CnN?ql’p¡G² £iò¤²°Á¢éÉÆê1(¦YS9x9_AUU£Š'´bÎä"î Ðôpå2ìm × ÆHz‚¶Bnú´Œ·´C'9ˆ ½VjqaBUši‹ÊvÞB£v¢­Co-þÒF{²PX,¿}bë±ÒžQ端G7èk‰y!‰ëƒÕEªYÖõ;}²Þ×ÍŸþ¾Ù‡{‡\%Œ.ÀÓºnpõƒ Wib àÂÜl‡6áìºnc4€"Òö¤Û5Ð…¿üvïSêÈ ”›°>Å•d Ag$C ”ÿûf)¼­OŠû$ô(?Þèò{¹¾ô'}ÐÖ±C$x+ö¦ž +– qL8eøõk¡ÆîI”  91;œ’ vÕh¥ÆmcV-Œ¶ŽÐÔÅä…ÿ6}Z&94#£T=¹³ùúI’QÉM )/C³ ˆ·ÖêOåƒÅ±?X²* Ù³› M@šÇÞz½C"Y®ÝSªñPE5Eî‰ø™%à fˆñþÏñ}ß«¨ÁP>/»áKèÁ{‹Ãؘj§ô¬k¡šE‡ë,ÔS ¨i%£…´‘¡ê8âjÆ;UÉîdãgŽî4òò<2µ`Іb§Ü ¸$F…A»Cçñ©PË`‡•/d +м×'Y†4‚Ñ…Õ'…ŠDv±ã#Õ]`Q¢r¨=Šê4áÜ'õIÐÜHÙÖ´ªÆc†µG2oz‡Y"ªLgç\5Þ›¦ì(¡§FbR€,œÐј×ñ‘f6‰ZO7·v‹f³ÃR¦Çß,:w|z¡ñÐ!­wÝ« >"ÌTm<*-W{.u +$ß­)/~üS¡2MHÙ›â‚ß±¥àÁ©ßIúN^Ð¥ï N4u…ï +-£¨\ÙÔì8(§2ÚÀÖ@‰k/O…(^n¾²R7ar±q0yô9¥xeôP5D…LŽ*f +¼ÔБRMïG‰ {£³% 5[‡*˜ê’K×9«„†Ú:¤‰‘“árµoK ”ß)=p-âh´¡:'4 žˆ zÏiQ¬¬šLã¤ïi >Ëët¼)Š•>ÿÈzhÚ˪+G…Œ‰M­m- hNÓN_ÏúlWêÅé.„Õ¡ÂMwáЃ‘ì!Nlvm-ž®õÀ½ÛñŒlyR+3óº`MÊãåùîš"ª>òòbßuj#=æ9 aè­Kí¢×N•:ä~®Ð» ÆÃïˆÎZ4[´NPŨؓYdƒd¹éÈ5ös?½„ µuc¹¤‡B" ÇßÙü*\nÕöœ‡¯‰­ã±ímTƒ“çC)½!3ƒ6m´põÅä™4_ì„ÅU‚‡´Ê³~9nYGý. >ŤG\±«^h§þ”MÎRKW~='̳ ”Š>xT[@†Žñ2¤›Óà®3•þ`ö‡b¾>§í}­‡þ»\!ysñIŸ#ì‡ý!P¯+æK š%Ƭ¹`16à"Ééö41Ròè„›QŽ£äîséÑ4×ý+‘H‰°åúo |¼Ò%›F¯KGgSÛÅ0Õ6m]]×G¯“S oD9]Á JˆÃÅ€½ãYáùîX9Ê &G5F1qËÊA£˜(PykžlóR%•yÈkÕÆQMh'¹V¶T!ùɶÕM¹#$˜T7ÑWQaH¨ƒ†Tù)q@#-‹ çœY˜ÈžóÝ÷Uä®>RÍu„ƒCž ÎV6‰`Í›ÆàŸ£í˜´­‚y‘œÛ=a<¡IË*Ñx¯¶NÛУà}ÐÎ/n`Òb¶©`R­}:Ú+¾;Uj1÷Z$ºž¬C’ šsF«ƒF0A}8TŽ3G8U!ÒšìbE×[örJæ¼7™ädඪ*_c°™¬Ý¢ž E•²2H]M0`¼:e"ºÆ§&nâŒV“Óæhc!´I[/‚Oç,ã®,{cæ: ]wÁĦ²ͣYñ°È¢,¦R±h›µ ^-y’®ddʦ ßŽ¦t‚³<ù¬O–d†VÛ´±o±Ñi« ð¦…C¤Ú¨©Ã-b¥tPcÍ~aâл 5q·­‚ÚI!'ã QX¥½~CïΊÕâ¨m}ˆê˜ÆcÞVA¹àÅ[%ýy¤^<±mIòj ೊA ^q]OÆnÆÉ™`åšTêf n/ìÉä¢=LçŠEÅ N7—µvØ—ÁdSáêPÕUœix2wॗNÿ6ÛúJ)u+qÛñ4öÍžhwfææhs1h`ƒì…ZÃ,N ZšÅ6Fgg•BqO$‚ +-«þÒ¨^‘Χ¸´=u†X.HÞ¢AÓ¾j„›ÛS6˜"ªÉHWõ(ÊÆE†ñ$´Ö¨mϪïÊç©<¹ +ë¢ùÚÏw'H ÒVÒE×­“¾ï9bËHk «“†ë ôÎ$ôhÐR_4CªFòÆnG‘TžÈ<ÓÛÁÚ:$§Ô®î‚:–uuÙã:VhjiaÁU¾û¢YS¢œ{e)ÞÛ)Dδ;5¾Ý^‚7}ǬD»šL'zQS¼ô« +´dênqêÑÇ+s÷ɈLòȫϦœÂ!-×êç,”û(¸XC7‚ˆ´ª|Ò…œ2œ(é÷²âQu(Fª`Xp÷¢ÏÝ”• Zù$àPïE£¦bòXanx£7Ÿt-úBsâÀÕà¯x4X™&Aù à"²o'0OŒ†]óA°9²å¤õpÙU[ßóÕ•+&ZëäͶâæ‘ï¨ )Ëd?TKé­7õoÑž,¦ÈDeJÙÆ”¶Š0k’j¨êñ÷Èóªvdx +JO=E»CŒ‚¶Ž½5Ï7¡ ]“•ŒÇVjƒzP \uT§À +цSì´wÝ^÷®ž}×á šZÚ`Æ‘$ámrì7*°IÑËP,Žæº®>Ô`a´ÕîôäfDÌT_Ð@«#êÜ´³ÓY3e—RE¢œ¹Ú¡(æ2â™jŠ"7,XãAƒÑÎi”‹±ÚIgÉ΄ý™Ú:Õ¤,=Ðg:£³¬Ggx3ù´Zê®tßÁ Ncédr¦¹‹¹„ÓûéT±a— «* bŽ ·ŽyáÖrš£Pu™¸Œ¢•†zјTéI[ÀÑdm=¬0/Z'¹÷­=™%âÊ`ð§H§î<ïÉVtø²uDApË“­£>ØmQµlPWºö(J`„Ô¤,0h-Ðj0é1H:º“‹’uì/=)ÛSŽÚ·ƒ@/ÙÞ´ªÁœ¶.w?ã ¾GJ7Ø—4UÐu¢­“Ä%\èqw,Šj<Ç°ªq:uCɧ Ñ—ŒÈØ&4‡Þ8˜"‚&—5ªÑ°ÚF É =´¸ +Yæ ‹®‡ˆˆ…–Åî‹Y5’å¼–×D‹y >h¤³I?¾Ã´Ç¾!¼jÚüL¤ªÄ¦/öœÚá,l­ƒxÊëÞ|Ïá*zaØ8©ÙÀ éM¢ßëT¨1[RÁ(ú©s‚oC¡˜sƒ"ˆ"È*#Ý ÉíÄK”¦¬àn3Ä• mTh¯¦ÈkkÀU‹¼b2; Tœn’»P,©S-€WG %Or%K 7§¡:^ ¯­‹³Tãšm!V°â! ÚŸw4k¹AÙÒ¼šg´h‰?LFî|å yêíÁÜSîTnoˆÐ꓆Àt(ˆ¾Æ¨Uª±íÐÿP{ÜRŽ–2UZÏ9eÏÖ:æÕ°{z~¤Õb¡êéL-^%&[°0IX5 |@ÍSËý,ŽZW ¨± JƒIÔ$TêË -Ò]GËx"¬U[[þï(þÝS¥Fƒ¡0ÿ^²·ì(…\㹞‹0˜*颺ò<”£ÉOËÀ-Z%´¸* B,užÿ…~–‹^ÁÙCøÖÞ!1¤!Çf‡`žàܽւ 5Ž—«{*Çmî'”Óµ¨ê|›®Ï»p>©'ˆzïôÉ:«‡D?´ ÐÓƒJFÜd®ß.¢¶ž +5+ωµR·¢×Ö÷LZÒ œ‘3veÍЬBDU‡ž›ƒS¥7kêLµUÜÒ +5 ¶ð7+̃Yâþ EUÚ8$›÷â»­P‹%¼*®Ö‹å¹T¢}ê¹/jí(ÆT&©î©9d)$Õ¶0»ú’¯ÖYŒY{þ nÍ ¯°2 ?†"Y¢™£¯và&³“W#TD¢ûìV u‹#¡&Ñê€ Qì Ã;Q¡™¸! [n{š¤*¯üŒ0AÐJ~Tm½?g>Œ²JôaêÃæõÚzø”'sOÛÿ Þ8Ú°ÍŠ{ŽºR+Ѻ§ÅaPÿIê7 +è ÔUN“eؾÀZGã ñØ{Kï’ qìXG¢üÕIdîð³WÓ +M¨ŠÖÕ&#æÀÅ“=5¶HÛêLSª¥YZÄX]wOEo©¹šXµ÷Ô'æ‚pJèÎ6òV¤[®Ÿ:šPûÑ å)×îè[ŠÄGx {Ú–‚×€¿X+c(êÊóaÜWìUAÛg}ÈLj)UR{I®d©v±L2f =fIÔñ«‚^xn•¬J›ðDZ—¾Y!Öw¦)),„;®z’¦“Åq’ƒ£™V|ÒŽ¤eÈ !c°¦Ál dºðÐ,wàkºuäƒî›„ÍmœªÙqqìr; ã(è¯zz¶ÆYåkUE÷ƒFäV:~eû‡s¶ƒ/ŠŸ,ÁWÛ÷a0‘å—UŠVr½zBé°}7Áè+H>ª@Žf Y¡UVrŠvPò“dZ¢ÕHKšP +ˆ½õ¸’ö’v›‚€+e~$J”âä ®Uˆ'fGV© F£òé +q¢(Æ·GóÅ ž0sÜòг´Ûªê·Sw³÷äÜ Ö“¥½¸ø@ëÅ_ŠqSøª%3&­‰¡ý;µVÓú4Ý-¡8Fî4*>DÄ‹²Æ8‰ÍŠcÏusvÚS ¥ž\ÕÆå­ ³Lv\ ÙSDt~³›$ùuߛܵl6î 2$É8ݱ˾çÁ÷ê9ô@ÖV%¨Ý(ÉÙ“Ý]’›ŽÓÓW£%ØfVUÖ-áÿ³‚Øq§ ž3)³BqO·,ü,'gs +H.g)þ-î@«ï€'5rk“ÑŸŒ¤)_‚‹i$+ 8J½5N&ûYâT¨uXQÔ2äŒu]‹æÚ‹ýŒ+hB{Îïhæ$®šîöIOo™6¡˜µ‹ŸY´´ÄŽod1bÌý;8Àeš½‹±Kд[[Û}£7ÔŠr(jc5é—Ѷ­I¶ŠmœØèÌÊI7TíŒÕ{D½gí gEèdcKÏg‚Êp*Ï®*jìP²áË!4Z ¶=§¦ o£ée)ådÄÕ2£™PZª…÷†ZhòZëÁrŠâß&Ž‹Fs«7º¡·®½òmýS¡æaìÜ¢þªßüØÍÚ£%·Qš£níKc?’Í¡)–,Y[Aí ‹‹åAÝ¥°"F?‹ÔyWV–ô¨‰ße•M9 +èà¨)RÙ[hHlT7X-QñQWÆ6ìèß[£±Ì¾F>ô¤®ƒ¢µØoÕ©àƒ‹ƒæ}é&–¢Yžx2­J~pÐÕOí[A‰ÎT^aµˆÑRQpý¤¼q˜€15>]‡ Šp”3¹êÞàsÎ÷c5VƒªyYÊcÇ+Šsǯ³|€ª–JUÒË>¡v^ .X'Õ°-cOpuEaëªj ;4(µÀl©†õÑwVu¤@"h1\ô^ øk’ºH1Øž¦Ed#dÎJãàmʉAbëÒS"ƒeðÁbèmb× +J‰XnV‰zBAa +-b¶µ®²Œ4ó¨d˜HwTüâIÀäâT-³ÍìDí“o«dšìðíÑ@.r{h†Ô°d‰êšÜZ^)®É }‚Ë ÉÖ_KhžÁ ú]«>Î`Ú´ŸD[ïïãÍA¤¹Îd,3¦6ÁXûìH‰XèðÎhz– •SDrê©Áë)mkÓ×gŠ}¶ùQœ#Œ”jó(¼ƒ|‰Ž…Êꀅ” Ì–%?ÆLÇ^3Olsò+©CGSÒ .o 5%›öV$j…F˜€šÇŒ^wàn;\uù!›P¡u¥¥ Cª¹Ðï±ËQÿ`3¹¢’‹ðsëwÏÁp·L²0TjPëÑǼ\ w +£ñâã9j3g§‰´Hk‡‚+›9- +°h={‹›TK`úÏ©PS²ã& ô¿Ñ\²ôÁ,8„2ô#$¬j á¦ÑÁ66ËYòIKÆÇüh’”856$uBQJ¬—A¼§|¡¢Nò(mäü)zöf@oìÓITý2ô*Áê·çšÖÛÓ„5á&¢´j®d¬Uk‘lÑQÉ4gNU߉∵“~UµB ë¹N2‹á§sízU°}œa²ÿie÷\U_—ýnN –ïéuQ”†Ü¤\¶tC-(”ËTnºïrÑâ”}oŸHëØ!üÕŽ?8Z‚É›¬>ò ~5ÅV&÷L +¬ŸÊ@Ú¹6’WsÖ‹$Asè¾—l£hÚ \I­ª°—F3 £_Ë© îŽu…#Ë!Ä{Ü(nµK¢ú0rÒˆû(9„×€X¯oËÕÎwfAjÛ•‚«YÌ9u-­—.M•ÖI†.Æ¢ÙÁAÊ­“6tžÔXaFöbê4‡åØ“Ì!2³~G°-’8>hÏë)æƒë®?_ºbh¼»•£ qyrÁ؇©ïœ¾%p­ªLñ0ÈQÁPÂþ ç•Hõ=âb5 þ ìp mo-‡îø‰TêNÐœl¹4»7 ïOwèÊòh¡•CO‘Â5>È^+H"×YSr³Eã&©¥pßf“ˆÿ­«ÐzëD¶°¤°O8„W¹ÓU –dܪcâÏKq’l÷+°ß’ˆ•Cw6êž'=*.VJ3£¹Ì-i¹øƒºÇs/p  q¸Ö£%ÙÕŒá…ï™»šc‡vÄé“ 0N£ ¨­]ÏÇ•XJ™¤FÐÑHšjÄÞIÓ‚\Æ®2-Áré9ߣ•eX¯Á€‚¼N•¹èÙ‡Û#u<pÖUƒÓ¬uâ8W›T²ÛåBP½ÕQê›'–EWe8´ÈÁâÛZÖË)†G´ðªeÊœà¢ô(”$Ê)j¾DÍ µùtRAOCÔ£Z=a-2¹f–¹ø´p¦Ùâ'DÕ]ÕœMAnàèèg‰z z4M`E7ŸHãz%˨(éÌM¥U5—_>±«‰L®§‚îu­ÄŽ6д2Ò,`•Zêh%Óç@-°£UHôV¿ßÐûQe"]²µ®Þ¨ž.=0h5ÆæÝ^±#´ÿïìµþa©‘]Ðzr´tñÙ·WÅæzÉŠ bH´S—(-¡%«Œ >Ùw‰Ü®zi´8Ê‚4YÖM%é6tGêØ …Æ™ZÞLd©jäqÚ˜nKeìùV‘ïNó‰=ÕKjT£u5Wž†3—_Öm +]ÛnoÊvw…s4ÁÛ +/µdEÇÍÃQŠär3€ŠB¿ОŸ°s“»TDÄ-;Ô‹W›ën“ŠW;ƒÈ  …žKÕšßa°¬«Ü¯c‚¼±aP]t`®×àA„Ê×êT,‡lÙ¡>£¾ß¹F [gT¼¦°Ð©“4c :æ¾d °t$¨Ç¥ =h=ø–„°B×­5 vÿ€fNìßq7.)mÖë+mj åQ¢ZÚT’9$Rë5 ÀÂc—¢ƒæwH`ß ô䤨µúÛ€ÞÞ¨ÕµßîµÁj‡Yúd•ÁJêÐ }6›)B CO!È#/• äNáÍØávéóâË}Å¥¦#9!¬öKV¦˜À ÝC«’ÂÉîIZ¾á´—Šë¬ì‹^å´6–ïé‚šgbUn€¶¶ô†ìzš›&hùI¢_eV”Þ-EÈòã–k$@ÚeNz念wj7£³ØžĘÍb);>TM¯ñ«šøÉpñ¾{mC*;ß3ØPd¹úÍ£1±Ö‹IShé’H,X¶¬–JfÚŽÅ%™_}ºñ º$¥¾Ò:Ñ×B5{L/óVȽù2s„"®'ZnQAº É¢ øÒh% £”Qr¼uOŸ¤Str·”4%ÿZ¨®WìЋ¦@“Ò‚`‘„ p +×KZ¹Ü3Ö5cótã‡úTYZ«Ö9Ñ© +Ý›§¡wWºÉ[)·* ¡Nʪ²ƒÕ]Õût½®àâÛ'v£¥ ò¼Ò¸V[>^~ªÔ±¨D«E-ŸåN­¤q–SLø –4Þ¼:+ýª‘Å”§•§GSƉ¿®èáŽú––_äÔÆÜ™©ì +HtÈ÷"Sâff5å"‘ºtËŽ÷ͳ·ê€b5xñÇ®¦i ¯ùÎjДŽì7­ÈËq ´^œ/ùwŒ†•UÓ$W‹Iw×B]@1öpíª8~¼,ÛËÂY,‡U¨ûZ©ý2¢öSµ¨m‡³å~‘í +0…{i¥ñêÖSz’—_Ñ2ÁCOdG 4½".iñ"1º«Óêo¨rdѵ*"(Ä`Ž¿"A3@5ä¤éÉÆ/õ㵚'4›Qê»»6Y‰q+ü¤,ÏéÆè‹k¯%âZ¨h‹’­BšÙàȧa'z(â +ðŽÖ’’8ñ,/¿}2»@ù_¼J™T\ï~èÖ.U~çmÊnX\ëž°Ãeì0Ïò]lÒ YzSòé:ÅUqX½b#qõ‚›öÍ?௵©’pÉÀŸy nAN¾ž˜Fyš9<ºèÇö—Fiò(á>îfó6ÎÅeÊU¯Qš®ÒìºC¨Ï‹û•3 (€öË´ ò×Ï›Éúÿ´¹ø?‡¨ä$L£»¸‚-xŽh¹Q2FÔ^¬qJ>=øJ®­n:šÆöRßôÄ£Ã83êÒdî¡ßmFPÇÝh› cK‹k¡CYë&ú=YúÝþ¨«À,„Lg—¯Qï©gý‘·VCßÜ¿úòâ‡Ë3tè*ö´Ñ£·8½~Ëè‰4„ßœ½}óæòìæ‹Žqì­ÙîÕörþÕSúí²½˜õrþ!{e×…Úíë×o.î?¼3žïîÿGøüöýte  Ïo¯nïþíÇï>hâLO ^mïmÔ§÷¶œ¼y{÷úìüâ«ó³‡Œj­Ñ^Hü cyóýÅùÞ~`=?ÏéÖ +:ÆÿöêìîäöæÍýÙÍöC[6|‚ðçCGùïÿøþöæâ#F¹jøœ,•f¶¶¤ÿÚzVþë)léô€¡ì¸[À=dYèx"Uä·—7÷§q™}_åÅWºcOUÇx~ÚÑË ;<ð Ýu½á£œ@Ž;ìö=7sé#Â)»"¶>-þþWÄd=ðèãã0¶ÈTØé@Ü.kØß~×ÿý)6ýö Û$<Á@Îî.ï¿»¾¸ß>ªýœÎ϶°R°ë'èÕåýÏ.?d`îÐíV`WN߇{’¼ñžh=wq÷ífòù©DÝj/xI>]?öÀ£=ðè]cr/ xô3J>9J[¯ÝŽûR°³vbur{{õÅÝÅÅm }‰ø*w¼ufôÝÙ«Ë·Ûï9{üÑ×öÕåÕÙö!îçg–¾(0ÜÖy.ÁàíWæÕö+³¥Pù4ªéíÝ÷ßÝ^Ý~»µDß³e/Ýž‘t{12íÅ¥nŸí³—i „¿üë‹Ùð[‡užÍŽXÒÉ3ØòϾûRk<€Ãv|÷Û+™Ï³ÚÁ7Û+ÑÏD¼½üZ[câÿåZ;ËŒÏådzÎU>€R™ì ‡å\vˆOv}yùæû«³ó‹ë‹›ûß}ÿüΤë³öª­ÃÉÏÁV퇋ÝÚÛŽš?m¯ñÙã»,(wý@ÞÚm÷l±‡Õn|šø 2¤·…øØE±÷€åØñ­²}>ís,²µeþläÀÖ#úr`Wvßë!‰^_^]=¼võËzuysq¶5»ìç¿»ÝG;i°»òåõÝíõöËć?«ck¸WÿüïÙý-8kõNÛ›­7ØÙùùÛë·FJL‡7iòøþtñI7Àm:§}‘ervsyýQö‰rTžwù»£º‡Íì˜wþâ`3Û蹸Dö°™wmOã +~‡íøîù7Ežïq3Ï7³}™»çœÙžŸËÙôœ3[k£Ï8órŽ¥l“Îœ¿8àÌö#ÚgöÀ™=pæA«õ3θŸ pæ‚r×äœÙ~DÏEÖÀ™,ÇŽo• œ9qÀ™íGô\äÀO"z>ðŸ,åCeÈ-å3.Vø@Ú~1žoU糟®OÛ‡S2òóß~ý%ëê|ý0ÿÒ ±nï.ÜKBž¼ܧ®PþT"ö#ÊóìåÛVò-ÿœåÛÖƒßË·½|ÛË·g%ßþý®öêÛ‹oXÓ½tÛK·½t£tÛ+o{鶗n{éöÒ¤Û4lôõÂã/LÈm=øŸ$‚û¬¢„ûMô€MT~ΛhëÁï7Ñ~M‡‡Ã´5P†ý§`'-]%ûݶÕDá/ÿqqõÇ«³~ý°|Ì&ï.®o?TSáyÕ¤¹¼yuñúòæƒW¢N±`ß_œÝù€DÿI‹Gß›ïQugÛ±=§š;[ŠË}š÷o_£f_£æg\£ÆmσQÛå±sè>óáKíßÃaûÿgíçößÏÚ?y>Ê“˜-.d³+ªÔ ¼yñq­ädž?àúøç’.ð1ýLÝy‰á›ñÙŽ ¹ Eí|bÿíõ÷·ošü‡·\?˜{"Ñpbc|~rak'âß?}|“oë|ÀÑ6ˆ{ +]{ël¿õÿþ;û| 6Hx +õîòþ»ë‹ûíÅòs:C?F@?³ô6Ðs:I?}9Êp”>ŸðÇG-í³!í³÷ñç}üù=݉øóÃj.îãÏûøó“ÆŸ_î/ûøór„ûøó>þ¼??Èûò’âÏT7öÃgR=÷1çÝõ·ì¶ isÞÇœ÷1çÇ ¼º|ýú훋“Û›vZÞlÏk‹vÎtÿ¼¸ººýqÛq^]~ûÝ}ûûÑ9Šn=Ìy³ÝäÈæÿêA÷¬µy|Íbk1ÿöîuS6¶õF»-wü<Ö½þÒN­ë_K¼wÁí]p»â‚{ÈA½÷Ãíýp{?Ü¿<´ë‡û¤wü>‰¹óÙ·w7Ÿ5íòâ³vÆ]~{ûÙ—·W÷ŸÝ]¼úìöîìæCqö½ƒî±¹°ní2=û¯Ëë·÷¸7q*3ìùGçƼõ˜.®Ú/rÐMZ<™÷çËKº5N¡Œ<1væK1SNU/z~ I¦ï¥Ùû2ž‰/ãÍ÷çM%¾{Œ”ˆG;nÍ„: öú.îòVÓÎþû?¾ovÖGŒrÕpïÌÙ;söÎœ½3gïÌÙ;söÎœ½3ç'8ÑàºgŽzvèÓÙ;svÛ–Ý;sþgÎSh/ —´Cž©¯ÔNz¾®©—Yšã#|»î¶y‘YÅé‹z"iµ/Ïñôå9¶/±ãå9¶_‘/ϱý@öå9^ü %;ŽzÜùSôêòþg—rïïÐýº¯pµ¯pµ?BwZ8ïúéùQ¦õs9A÷•­^Te«/뾪ÕGôãiûðbªZý¼J?}õÝÙ«ÛÞ÷½ÀÂ[×ñÚ.Ø-áy'ûo}è«HÕ).âOÀbÛdëkuùèNï•]j·¯_¿¹¸Çθ»xõ qýܤÀ8ÒŸ•ðñ‹û‚…]Yœ—}CúÞˆ{qFœ Ã/¶åÓï.‚`ê?¾£9n?¦/_=ð§O?þþó1µ\Úzñ¶×ŸB5~ÈH¶WŽŸB7^¹?~Öe¯_ û#ïÝÏÓðyÞîúRÜÛdïþxòs“{÷ÇÞý±Kr}ïþØ»?ž•ûãgdXÈ»?{hí%šp¯ïÎÎïÏ®~{¹=žZo¹Æö¥G×ù§C =6n÷xëj1ßœ½¹ø»‹ÿûöâæ|{õzÖêñ—jkÊs©btsûÕýåýùܨS_žþóåÕ²ß×Ú<¾Gyk4ùÍÛë?4 óÆ6m²Ãܸ/¦½¯¿´¯¿´Kçßë»Ûëí‘?¾_iû3z_MêýÃ{ÊjRûêKÏ®úÒp¼õ¥\÷·ÛÛ7·O0”OZGêI¼I®¦ôId$;o +þÛÛWßÞmú¿D;ðE^x±µ¶½¯J±àƒª<“[ûžw…}Ñ÷ d_aã_Tþ¿ÂÆ“Œð¥É´Ÿ¬^Èc{»¯š®'7ÒÿÛ7WgçÿìPH·ßŸ_Þÿóßà6~sÿÏ«íÝàúôãÁ1Ö—¶©4¨ç²§þŒøü¶ÔËtš>ŒÃ^ì)»û½| â +fž¼D‘÷Œï7ܺNŠ^ºyr{ÃËÛ··-æíñ~üîiÿWzüÑ’s2Êy³Çw¨?L\|u~ö¥o­Íãg»n­É¾½{}v~ñ°±­7Ú[ÁÿÂXx/ïs9¸zÝðË­ ãÂÖ{ñì¿.¯ß> lןôåÍ[×2º¸j¿<ȵ>iñèãâ‘ô”ÕŒ>‰ªõå%ÕˆÓ‡„¹?Qµ€/E.œêÑÿüt¿Ÿ…b´×ž‰öð÷? bkåÜæàÁVÖ²ádvb”«†{íi¯=}:íi¯<}2åé+ÝÊÏW{z™ƒ8…w]±x°õî¯Ós»Ìã{Õ·>ÿþ0ýd=ðèîj®ÿÀ“Ó¸'Èö+²ýÎÿûSlüíòª@Ó„'ÈÙÝåýw׸€á9£/‡üPOøΟ¢W—÷<»üº?B÷Gèc dÇÐíu?B·Èþ}Â#ô¡Ây×OÏ2­÷'èË9AŸ«z©äkFŸ\.>Ÿ%ùtýx~lñR+ ½À[æ_|mßm«ç<¿PþË/íôˆwû=ÑAóœk;]ŸµWm][ä9Èww8èÿ6ýÔ)Û™?mÏöø3‘»~ªý¦5}óaôÜs¶¯nï~·ÅÜEÁñÕ¨]åQSŸhC=ï’"G/¦¦ÈÑÖæÈ®y—Õ#éŸüDò๖ãx™PÀës/w#í~q +ŒìÏQNûÄbžÁòìúvrÛßÌþðé’mFùܶÔóÖÀ_Ü žûëë‹»ÿ¸¼Û/Ê®¬óýÙ7Û¯ñsðyúíìû_æÔ\kót…ÀÞÞœÿéù •ÇlÇåp8üy°Ûoöìöôìæ~.Âí‹'ªÐBôŸïÎnÞ¼Þþ¶‹Ýáþ—é9ù5n×UÓLhy~ÚŽèÜ{ÏÉú÷ùÕÕ,Í®LÉÇñìúýxÚ>|ÌV¿øü·nøúßo^õ«¬@J |ýûÛ›?¶W°Ó‘¿¸øöòfú‡ƒßÏwDùÓWÿ¼þæöêà—¿¹¸û¦§¿:?oÿÿëoV8¿þ³ýòÿ´þO#ýxwø?ÿ÷pø +OþéàÈû㱺xèÃñ0ºzx}€Ÿj;å´SŽÚ.ÖØH½Ý&Úªå »ò‡ƒáxµÔÃF_b3²K‰¥äêrhÿB£d—†’jÍ>Ž‰Ï Ë csðcm”q+ÞáRnÏŠOqܘ‡Ò> +ÿVb’gÿzvpäÒ1^tx”¹”6Ð#?ûTêáQ9Î>µ×ywìãàå)7¦Ã¿ñ©ÖÅrx”ŽãX†öƒs©í‡x\†:¸F©Ç) ”¡s<ŒCi¼ni/Ám;¹Úì·ï‚Ș¡mö¶[†BÞÐæ /w#÷i›qÁûmrÚ2’ÒÆà1¥&!¾±?Úxìßv0»ÓÞê±o@âàÚ“5&2eh õ ²1P")¸6YHÃj„¡IÀ&íƒ2þi2¨Éߤ[ÛpàðÆ8´nŽØVd¸Ð˜'6më¢ðkü=ÞÕ‘Ûpl{¯ÉÛ‹ämã¦Æ1x$L÷i“–ÞXÁö«§èumÛ\· ‡ï +IJ§÷2MbÂNi{i#&[s;yª½ÖGSM.qÓa"Æ6iü÷d¾}ÊM”a€Æp¾¶ÍÑf¼}ÔA>·…e×ÚxÛªà’¶›OQ•õÚ¸r„(lÕÒdiMnÜßva϶ýãKˆ‘cvcƒ–8Ä6ç.Üeí5äÅܶ¢œwNù¹/"Ë-æÊçÍVN<'¥òäÄVe!ÑIÉG(ÎWî•Ö¬‚c¥íÓ*ÓÞ¶")퀂LhŸ C*ò¹Y×w‚‹[GîÈ6˜1™§#)%OcȆ༦m8L'N #Y¸‰¡¶m! +ú2(_йpÚ4V¯r¬c±‚nï°"˜§oo<äe®ŒûkÌq“íX—Eö«½˜ãóƒ+±\­Ùš¶vm³fˆFðl‹Ð&¡äšíàŽÂiéëÈý1x‚©K6um:3Æ[tGsîÚ>"©cšt`6å»±±Ú¨DNàkZ 8¡ xO Ô”Æ\9è`_;‰œ0Ià êj“nœˆùÑgþ±})‰*šq$:Ì~æjç`¤Xl¬nÊéȯµ+ËuÕyžK úb;0Èíí•'qc¤ê\BÜ€©³A!ÂXjã-ª°‹ñíÄîkã«…ÓØØ ‡t›u°:Ýhµé¿‘³¢h!2¿Ѭió?Rh%rj›õŒmמjœ4Œ˜¢öò +ìÈ£šó¡¬TL6´](m>Ó;,€…™°0$æÖÆ9;ÉWã|b|›ýP¤™ð4,9•šK9Ò6˜îO2¼ZǶµÊ××çíITó#*mÜv9çÁ馅®ÑÄÁ6y úvcâF‰±x¡ð,æÂTWÛµõñ²0Mdo H‹´M0®Zo0E‘Ó5ïÃN(קfÑýSš–Õä0’3š| @'"7®ö&žXðÍŒ·ÀÁdïq¢Î|䡬låÈØ­QhìBKþáÈA˜ 4­Ê‰^ÑV°)«ÇM4íUÄRH…6WÒ‘øÞÌ7ùy¸œp-õ\¬,yX×JjKÔH²§¨ ·ï$FcšBkU#úb³ÄUŠí;ãØ£ØèY4kîÜ‘m<Œ¿f²ÓÜžmÛäÃÆ‹¢”{t¤½ Ç|c%mR!¸ÛlWÌÕH¨¼˜þ8m¾*žð¶e=ž¨ÇE5 L- 'ÿ„Ó—˶¾ˆxæpÐ×¹¦³I:Ùé½6 Þ³¡Îæb6[àI'ŒãhfcB!æ›Ø¥¡9 YߘýгCÖU ³ƒÇM¬&ØaT@­‚1/ËC“ºxO³))GÑ‘úJÚ´;!:šOwTãÂkîö06]\Õ™ª:ü>‘æc Pá,¶ÅÕ3+@Š8»@€ùB < b&Ñ<Û\SüP?åK#ôx¸ª÷œXüèècò£(©É ìpjG‹Jƒ;º~Húml)|²Bণõœ«î@ßU;S»±½ZËÀãT«IöWOм«mçàÄm¿ûZxòÊœµ¡@ÓÜKt¤Â­¡Í°á\À6-ÔLéFåY™Ù&‰¾×Q-ž†Å@ Nú!ˆ¦X ‘ òrT3}Lðÿ69™²£î(Æ!lµÒé/´åœj6€Ðm‡b½h<”mÌ'æá +.ë6ðà)ái¿>he%kDyQ³'å¬oíÏ4Y k9/PpqØœ¹®FöNóCsyŽÆ­×•nžù8vBÇ0a³ Û³\Ã5Kv„Än[^WpL®¡ó–“爘*ïš\Ž‚­JowMn8¤!éö­™bï9© ,?ïÅ“Øl‹˜䳸džÈÈzðäá”àø‚‹ ¹¸nûþ Ev c¤)®.Ë>ãÝ©†»ú]\cˆâ@í3Ã(…aE7û½)Ãc›<ÑdçÍ5¤í”¨“ÒÉO £ k ±ÙÛ–¸M3>é½èP‘œ-x xî(èK𳥈aD‰¡ÌBq?,‚u‹hÞ2â7ª{ ”äÑTxt ìj›ò á«×‹8Ë5I12RcÑX äôÀüí aNœí+töeóŽ êiËhw¢¹2¹œõ5Ð =Œm1¼ .Ïð^/h`„Em†[Œõü`1‹[ÌêùÁ†`ë2 »²=?0&‚’@ŇRÕ‹ß©ï|«áˆ#£5R;YHŠúÒµhÖ³p×"¶Œ˜QžxœC0 °0«M£i²óS8Ïí8r>¶Í2L3 +“°öš­ Qž}+aÓdJ†0i +Dú8Å×8«yÃa]_4FûªHô4‚/J`´­íq˜†ÎDH´1¹ _µ©Jä†#l£¡}kÁ’' Æ=ÝÌÜkÓöÃÁ2¹WÎCš\uq4~ tÞ'nÑäÍm óªÒ•‚UG$X\!~\Æ) f±Èy´rѤ¨ô´·H€-· _’ Ã\¬Õ“?m¥ }Aˆ À¹·‘c}=Ý]DJO7FS×#®?@Ò5tëó$WZû®Ë<À»ãrŒœU+´|—Ä¥“ÚÜÇ®B o†=CˆÂP žB{–PgT¾i“| ´ÅbBvAõˆCT™ÿ€y} sMè3iýÕ›²>„ýl’N¡ÕÒñGýªZ8t¨óÁò'hnGŒ¨'xð ÃÄ9Ê=†gD7 Ý ³ŽxÌ#¼rþÎÆð$ü¾ v#Hž¡*yÕ¨3 4’ +å‹H ¿¡«í ’-=¶þ¶¹k‰É õ,g;rƒP ©GCÅGð‡ªKUM~P–"D-¦q +ÃtI¢žÊ0ÛFL`B9n’Šä&.›¦O›µqNÄ„Mù§Ýo‚øfps”.G:z½z<ܵaNŸ¤Á¨0 +h™§CæQ†à‡Z§äËP΂´qrÒfhÛ|˜ã –˜ƒ“ Ëuúè‡ê2 ¬1þyØxZ^ Ïé¥mR,„Ù¦JBP8?ºduÇÐ=¡1¨Æ˜•|ÖÖ¦k t¥{a=Õ¢fü‰Ù›³ñéFfŸÏñ›P"s$ÉLÆ&äÆÞñý70oA“>Ü+Ðîð•ó’ÊÀË)‡èã»lŠY¤ô GñÃÁF´Å‘1?œË: ˆ É._Êô©&mœ?I .'…[ò¤%GÂz#C†y0ƒÆGq$ÈÑKüI¥ '4P ƒáè€Ãh÷Ü&cAAóP×ç(AÀP¡óeõÛ!¾ì`™üýöŒ{¶W‘¿Q5:<. + &9™Å”ÕáWÞÅ hŵèTÊ‹Ù½XFÒÚ_#¬¬#úÏá´æƒö¼¨TMŒI${tá XƒÂJÁñXl%ÂM^ŠºD¯èÍìˆö]g/U `IN ²NÂ’‹1¸2Ÿ™åì-æø ¨X,0ybÊKY¼èü`œkäk ;Ù›Ãg~8؈²Y qæp™•&i‰[Ú„nªª×4Z“û7µ‡LJõd„ìïê0m1+ߤ[A(!Ò[š±¡ûi¤'¾cI +ý¯D1Ã÷%s.¾fx1å 6âÓ;0šq¤¯ÜÓ»@æ‰LêhëImaB™‚9AÏÈàB¹xE§¤ÀŒl»=“YUåå-ÑÕ +c\ÀrÊv/Nì‹Évb>¼s9)SfK¾äŠïœlÄÏÍ1v h•o3üÕ¢5Ãq‰(B¨ûÑf0±`bË#Äë`f'Õwܾ€#ø)ÚYp|GuÂü…û²Mg¢Ÿ&Tupæ3zCÏ^Q5Òw3Žö‘þ  — 1¢èœ¸D×ùåÉp´Ò€‡« BF×mÆÅ€Ï@Á a#¤ÆŠø1Øú :Ae„eéð/ÙÛôˆ6R$Ú¯3ç^Ì«0õ¯Í´¤/Hÿð!Ê,dõøLÛ<øfrAwr„Xºã´Ûnç©;[/‘ŽëKºXô9cHvhÐ6aÍÊ‚mó1ƒI~l'?)bÌ¡}rê­£ÿðÀ„PúoѾ$g7Ž‡.(ýÐúJë' {uëBc=õ7'z{ŠìvèߌÍfó2ÂÈqFfUg³¦¶B O0Iœ], «í厇z& L•_®. ±ÌYdÆD²8 +õÓ÷ì„¡µÉ ¦) ¾”söœ-áÓrMë¦Ë©¨(»fº-©²òUÆùÃç”mZ½õõ]0ÀœIÈY‰æuU;‹¬·O7æøDŠÆ†qrœ!eýDtTI–ÇVá©L²,Î +ÌX„SÝ^؇‘ø#ÂR„ E_`ÓK¡h9ò&fŠ`£©E1 Ž¬Y‡“ºñ›è‹ 4±{¤õ¡ùšð< +*ÞáÜD#'©Ý4}ù{Ó×R]~.¹Yc˜Süqô3Y6Êža¸ÁûcÑTÛ@}ÈšQíq‰? ûØ¥n’ö´r¾ÜˆÏTácƾx±¹8äù4í‚ý<uýfð”C´ª‡tSÌn°³üîXA˜n¶QÑ6ŒŠ¸ýˆ÷(jrÛòUè ˆÐht+COÏW¡ìQ0ô +sÃkEql‘ÀÊÀãf]TˆãÉ"'P¬€ß ⛈rŠÓØ2Â@ƒõp1WO"€ÙƉ þu_È«˜´¨ÀX÷öuN|Sçd:‚¸¸Gq1bdœQô±ÅHÏ6ÍÆ|¾Ö§a`q"ŽÒUGä,Q+Ñc¤|ŽLñŒÇ‚ÑB˜|†öýá`‰^ oÆÙ>¢ö³DÕŽ*è ÀVÊ ªÀÐw±wªÄ9eZiÙ·“‹.”¶~6'Õ_¡Ø" 5F˜; ++j\¯@ôTuoâLñe^yhžãçˆèó6 +oó^Xz -gT ]] Ï6aƒçØá)ºød?žÏåO±Ý€þᤋGüºã@’Møf3KÆlZùúÍ×wÎ +®ÑÑoÂ\&bÒëœ[€ Ïæ0é9Šze}¤_åÞ-DÇ ^p 2)©t%„ÜGŒ`¨Ô$ÅŠLèà:XyfžÁÉ´ «“Ðÿñ_ðžšd²Ã㩲¹DÁPðTî€-À$F…$œÌn²çða0SŽ¶òE@$8~û„ª`è enŠ SÙÀç¤Þdžfs8Ÿäs‚ÁkÕðÇ^üµ'&cÑJ–3²Ο¬Î“Àù‡ƒz=ÃOŸ,&i'ðü‹Äe$õ`\¯$@‰FRHÿ¿Á1»6‡Œ-0e ÜÙyŠ«t47“A§ÖqÈpA² KŽ1‰¤ìÃ׈”šÁ p’ΠRs,Õ¸•Ô ^J·6fB§bdg1Q8‚ÿÛÛwø%9ciëšÝÔ$>kµäöCÓ|nïiZ:(imiÍ™ÞóÐÄ{›)Ð6+ó„×× ”ÕÞ¸În&íßY×)TúÇ‘œ\ÇCKh׺”Ÿ,Qç€é3¤0ZÍÐÄ ¼ñ“|ðªø߃ú¥£†«@P ä ͉F Äç:NH·®\À/ì v˜sübK̸m2k„ Cmm²ß‘JŒ4mÖу xá& â&¤âÍ8ƒ”lž-ÁisqoŒS :RÀa›Ä&søÒâ4‡Ýˆý#œ”40Mk•AèDHwÚ€”¡y=ÇÓ,17 dŽ ¡èKC‘QP4nз8_È +¶ZÂ/–Yˆ]³@ü¦`ýz ‘¾—E¸qcPr=t©þlÌB¯ŒPe;Dߨ²,£ví[Æö–ñ¿e”pJ\×Lò1γW§.ûȘ£Ë‹`Š4™\æ™õ  Û´­Áº|†Vx°Üúy ë5‹3lˆD¬»¤ÙHäcÒÕŽ¦«$U‘çU6™ù\çNÙ™ã ?ó nð®{³¸»FzÔ‹&ÁÒ“)P˜x––nV®¹zæ¾ ™·Ž¿eÂïzFð"[öü`îA¹:Øèe™ybîÈF/‚$H¥nÌOëNXóë~…¹ßaf{Cl/’`gI²ëf)N¡uÃunØÎ+hGëæ×Ì:›ç´ +Àîbb¦CÃ$G fÎîóƒ¥J>WÙ×õU*kYƒó¬ÂMy‡óÌÄyæâZj#³o¨<4†œ³ !” õ(ÙdQ³r^Ôr]?ò÷¿ýç”·Ï¿?¨ÁQˆ”f9·²|¸öš¶µM`DÚNÕ¶ÓÛ&jÔ$O•©6 +¬u-»ñíD3ot±˜ÄOåôÈ@òÎH€3¸Í3+Np¢F†‚‘þÅM—į€œñÝ&dÇ#˜›–ByLKÄ* +E5À\!9«U±íx›ö mÚSº=”Âä%î›SBÙ=à c³G¤"nÛ½E§ô´-eg I%b›dÉ©qq…Œh0°7§®5sót%‚ƒZÎ$UÿÀ4!eÚò6•òä@Ö­ÚR5$:`§°Y¼þXÒsI“QÔO’E0& mYY?]TD"¿'ó#9x”Ï™s P”øþ©ç8.Ûjrñd„<Àª8"q¡Ø°–®ÓAÞ'KС +Q×< ê,º y 6·6õ°Â Ïš +2u\f† âkàiË Á +eú³×¬ß9£ÂjýÏV^~uù«Ã¿þ­ ­° ¹¯Ô€˜‰Ÿ0`€+âÀÊMÎ)§™}æyÜm•ÓÌPhºé1OdbÄ[sj)UI‡“²“`Øu'àT{4^Ð~³2èjŒB͵ÐIÐj¼Ëgì- ßái,t…þô%ÐE_-¸‘pˆH:É:lï™HÒšìG¸Ù mŠ™Üá ¼â\„P RÖNۂ†˜p+œ„uåßmÓ;R½[›EšSçOÐô5Iã´Y5BP‹šY0£úPã +‡‹Z¢¾}(¨$°·tÂjVrDG†(Åò–#§ùà“i5séP'Yû‹fcâ¼òxj” +­ŠæÃ’!ý#´UNW!²$¢³Y¢®HY•âIÃþTÿé†ob{ú·ái³<;èÖ8AMþœög0™‘E £†0W”¤êÀª•={JØü[âkUò‹#>ê"Ó‹ÅX)ðØÒǼ„H C¢'„4i`¶×ž.¿„ÏÃPv’þEèΘ¥’š0Îú)iOÂç[]ÖÔB±û§4šnÚxõ\ÿÊéÆo³OÞ„Aô,¼œ*â ©ÉCÇ;<¿£dÎâ›Z;qB’0É´©=f_8ÝðÕSIŽIŠIkGvt’3Š0sê¶9<æ-À“4{næ5š+c×Úöçô§Ë¯jW nx»"Y2@YTŠúS(ÀªšKd¾B˜’‚N礡=åW³4ÿ¢vCjz›o‰¸µPáð+n±§œ¬¬/tSRTãa­¡{ÊjPâ© ºÝ6cJ¶Îi (Ê]PõxJ‘øĪ•=úîžK¾ïå¶ø¡iÿ2¾.úªÓ|ÒÓÕc^ójð~絜Ɗ”LšYÃþTÿé†oŠò8ªÖøŸ¿øúןßݯÝ[ó‹¯?kÿïϬî‘Û@6ÜòÛ««·×—7g÷¯ÿpwvóíÅÚU!ÝÊûûo qÃúµ!à +pÄH‹\§a´¦73‰ÿ…q"ôö*ýAà{ÌÏSÊѼéÑòý‚˜¢5_ý`V?Ò~¯’΢¤Å 6}æ‹oÚ|üò?op{Ë«ÃoïÎ^]^ÜÜFÿ+N̈9’Y²ñ-·¡üãš=Õt6^qášøó•' ÿ–‡!è·äÙ¦ee²b ì Ém÷µƒ6’üCí¯×Óoø¦À6õË~CÁ ÿàá ÉMv¶ qiÄ?e<üâ|ò1ÄÑJÓPË¢él­3ð±þDÃúb:,DOSÓ‡¦îøУúšÃ¸ +««²ÏÕë_ƒÓ–0´¦îCªç¦ç}̇rûP…¦Ýô¤ dÇìC?ÍjYœŠU¸¬ B'ÃjAAÿâ‹Ÿá®tÃ~W~ê]éÛ ³8Õ‚¤wñ“lÈvzÖž„f£5öv9Õzãüˆ-ýQßjÖs>¹ÙÔ‘¯)óoýT¬!Ÿ£N0ª]*¼,FÃpøë/no¯Úƒ¿uág÷÷w7ÿ~ƒ»Ýóöò•\4û‹¯íÜéCÿøþöîþÏz‡êÃ=¼»=¿xóæðOç÷g7ß^ÍüvEÌä’ÞúY)PŠäüf¹"k¿ûÑ®áHY +EyqûN(½ Y“—Jí4%œ2¤N9%eW¾5¥þÖéï쉬“ßõy¾oõº¢H_éÁ|.NßiUmgT í¸C`Ì3sYÎʲñ1G¤Ï ~zb6ÃÞ#TÝNÌÑqõ1œ˜¼s@ß4S݆<2E¤!â!‚÷ñ-(8‘) )Bƒ/y¦¶%P££0Hä=à?ͨæ–oP'¶QA‘l:£û¨…$VLEzíÓÜãÑZD´ùˆÈEûäG­Wˆò± ¨Cn¡/3…ê§[0L[ë(`,é<,±¶R§ö[ü#¶¸ÿ™ïñ˜ ·’i²‘¶ø)6ùOø­H'J™LÙÜŽùiæPö›u °îÔH“ãw¿ÿ>°ÿÌÊûâ»Í§íø«‰¸Ú‰m–±‘›\q[U3ÅÇ:ˆ-Áú>®™ÐÍØoº? +BĘQâmˆ<(7{¡öëõ1*QÞÒ“eóh!1¶Ü‚Q+/¶àFay´ØéüŽ¼Œ¾,‘)ÑÏýÍÑUêAÁ6%©†'Ó¿:¬™ÏÇŬN\ÖÞ±r⃾é‡IðÎ^¦-or~³¸üWgQ>G¯ýì$˜y}i•š†Ùt¦ªÉ©é4ƒoºf(.³dVë‚o +p¬+ Y–y‡Ó$C)ǽ–¦ì¥é~ã%ì“ýÐw±m‡uÂÑdƒMßqªîœ)9E•)½¥ 4°#ò˜Â‹õª l€)¥ R¦2Bo÷®^j#àd“!²|¼ýoS€·M~7ñ¢õWÃïkÁ÷õNçád>1¿‰‘¥Wè?çöíÜS3fú—0Ã×Ö.‹Å*NHG+Úb%çï:ú ]I;é3øÓå7ßÜÞlvÀ§ö„™‡ŸoöS ~Þá‡qæ©àŽæU%Ø}¹­ò& _ˆ]ÆJÄ R0¤¸$’k#³~á-/Y2IéòÀµ³‚‹Ò¾jB9f$‰&¦‘TüS—-k–±îYÐ2ä€[3sk¥RÃ9o0òØ#¡´—Ä&ؘ­פg‰Zd„£$ ½g wIèDó¥H±q÷ªÛˆª´s›Ž>OŒ?ol“\â!¡N™#¾‡)^R䇳V£d I’H:K|˜ìÎËæESâ톖fƸ|2JmN„¢Jxa™ €0m>Ë%[ZK:¬vš¦æâŠÂˆ’@‹H`ýœõc±¬NÐΚ¿‡S¥vð àÈ‚T+MÎ’rå‘ø§èSÅ¡ØÎ +­„”Ï64É`±'€ié”mgšUª.’ÙU‡0Gó8ã>¸K7îô_~~zú«Ã_uwyóíá/¿úîìû‹/®.n^ýîìþü»?ß~qñúöN7ó/¿ØødÛúßýöK}äÿ}ç#+¡ðKäíaÁ'Ï~ñÅççço¯ÿt{†îM¤‡;n»—¥ð5#ãš`~^ƒØHj R''¥ºY +oD™M¦Ö|_Ô7I^êêÊNB¥v&S£šÁȈq›k¹vSSÜÀUK)ãö$^˜™ñ–Æ`¢h\E¾ ¬Xwe¸*Á„R‚ߣ°ÔÖ€Zvè™›X}!3©Øó–Þ}ˆK XCÁ%Þ“JÜ*Jo ª?L@D1kL +«r°THÓ/Ú»ƒcÑ¥(à ‹!I^ „'HRÁ1ÅÚ+Q׊Ù¨kBe#5冥®¤à9fr5fœÔŠ^˜S’n#¢(ˆ‘ÝF|-Ó‡¬ÐË$"“ÏgÝE!H§„[ŠÚ›GVâç¶h{ßBÅ3BøHSX…±W’Žå–jþƒÜp!…ÉËr£KÕÜ L› ˆõÕàlj2!Øt\©iÊj ç´­ä¶ XÃȺiÃòÙ«\rAnÆä¦t«ÄT$§b[ƒ©Rƒ–@EyV²šñ˜¹¼ð<€Ø4¬ ÑŽ¡p÷°‚=ËÞ÷;R$*#ýÀÞY}QŠŽÉ¤3ƒÓó +Ĩ[ ²ñ˜Å P>‘·’½Ó¬@& +óÚæ; Rʱ ._¤0¢Ü—‘‘Œ™<“ÛYûµz(l*'W@ñPoò“׳SE9JIôÈâþ‡J¥4&&ê:äˆÊbW/E*Àˆr‰!wǶ.˜0üWN[7fŒ‚…}ži3]pª"jÅU¯ùf(nXFJïÓA|S¸y[IaC^X^l¶!pô’æ@ft<‘$‚×ßTI +DM%cÅöF©(¬µõ¹™õ"‚gp|IRﻩDQ÷Y5œj\žÌˆáÕäc”š¥d­ýŒü&(Êñ%DL*·‡“¢ ®+²+7!"1¨g¥ø&Œ;TœÉU8TªL¯–QËxņj"hä,µ[=°ŒíƒP¼P'A ¶¦GHÊ/~á½¾‰ù L8ÆÀR¹1BÛ ƒš5I•B2"J-ÓÍ•‡!l½Û„v‡¤³(,™ÊAã5+»íÒvÐÿÅ‘¢t6„2O’ì¥_®¶ƒÃ£¦IÅ‘0òB»W„WÊÉ™¦óñ:]Õ¢ëØK`=/½O/!™m´óœGãYàEæFl$(°£°# ­ Dݨü›µð Tì¥ü2ö<ïáÑÏÂñ(b¤¼˜šáoŽ–B/¡8£¨MÍ*c2Ñp—뇤ñ(¹¹pnÔ{Xp¶Ê‘ÌÊ×>È=,º_RR|b%Э xЋÉô‚¯&÷³è¬ƒx °…’ÇÅõUQjOe9ˆk•*ÛëlG^ô0ïD?F„ ¼$¦µr=«uÑðí¼TÌÕ5,²*JIo‡Z'¢½gV<—ô|ì‡a°kìXu(°¶¶¸Ç¥wPoReÕV?p¬±Á¬KÆ*>Smš¥_Q Ô3“Ÿ{+EŽá¬Úkû÷$W×ö 9‘Š2:¡ÀM5A[² ©²Ê½‘öAà"ä¥:, +Q‹0eÐ ="E¬ÒŠÊ诒£¯)³Üc©Õ„Ni#CqDž:X(^3Yõ~`êóc𽆪ð¢]j«¸‘p)t“$‡M >òŽ¨l;¥ß-×Ép®V9‰³¯¢qòó”™^Û±‰À,-ÎÛ¢xIËI&æøŽ×Õ9Ö”»A]•?(E]Ì@XgQì°u¾#3B´$1©X¸1ã(::%œƒñŠ•‰D=5¬I’E !«£ôjð¬ÈDî&Uø™T›„KèI‚,3ÍÂDR­ XùG.”ó¿HåKÔÏÃÜŽy©NÌšœ,½9’š’)®Ú‘iG(r[V‡q•«02ä¾=ç¨ß$HL!¯?õG(Æ­œêî^Nt)ÔFÄPoèþW½œ%™vd‘WáÞéØõ•Ø«0d‹ÝRäþK(+æØ r½Æ¥x5v‹–÷päùL¶\¶‹)¼ªƒjCæÂò±S\Nê¿¡™ +·¦ï¨Q +ô6¤Añì~ضc¥ô´Þ=H‡E¤Å‡àÏHÙB•Õö-ÙkÍZ‘“båÀ‡êÕÐ4ÑëqÌg)TRÄJ‡SC*ßi]qÍ‹G¹ü2bÄíp2ßü(ngÙËt3ËéoEÄÄ;/B€uzÅ=oŽf)MÒôºReê«ÜZG½®‹—ÑC/æ wtÑñqófÒì]ö"Ý“i³(ÿB}å­TŽUóhñÀÌ£¿D/}-«› WSè¥b>wY¬t“¸èJAZͱ— +áY” ±Ý¢Ô¬Ô&YIôqitnSÓ@ ´`iÏ‘ÒNnx磸f‘ø Îy^{iîús©‹ØÅY¯‘ð†™áXï2“+ø¡`tjÃ8G½¹å“H6MU¼òp7K¡eÙžMÏƈ¥RŽúäaöëeq Îûhçüç¯ï/îÔ§þùO䛇™Ú ݺ•oÞb›NS¢\F?’¬ç?ß]^þîìîïoÿ×/¼¼ÿîp<úêêòüâýj-X¨ümŸ‡ápäur½Éä×Sù•ÿ:=˜ý¸zèF‹•¨gX×@ª–±‹skÓä«”EiÛÖ±(ʳŠ-ØÅî«GÙXéô/ÿçŸÚÐßÜßq¶ÿ÷¯8¿ý¯ƒ#j¬Ks$7:W”Ž“€Å€Å:[ýÈzW( •ªñ6 1Iõ¸W¡®d¢GYêÅ#K¯mœfÎ-™ƒ’ë%…Ñ¥wõîØiÿäÖAöÏ>¿Þ)>0ù±÷¯}±º15… ÏÀÝŽ¯Ò4³¬…þ›ŽšÍhDJ%+<£ˆO”5áßÝA¯$ÖH:hß_ï˜ü¸ê`ŠíLo†?ægy³~`´%§™3"ÞC>TF™Q|¿=d®6ï^Þã ý£•!ý³Ï¯wŠL~ìýkšGäsÅ(µÓ(qÍÂÊÃÀ +É ·e…€‚Ë©°R[SÂ*¢œ,N¹wv0jƒë´ï¯÷*¸õWär¶ÒÌŠõûfF4…¨†ñ”ÖcœuL‘ìLÚA·Ú"aµE&½rãú½ƒ‘Eer׆ÌB¢Í–guñÆ`k¤°Åö{ãÉ$åÖF¦Ð–fF¾oú²màÞ9·Ú“•µŸz×Bã±öþ¦ø Ê9¢¾màñ› ¯òhªnÛÙ Y~ðK ïîM‘Þ¬öÂj+ô¤é½#n„Á×xÜ75…u@€ÄJÍfj[³zzÿ›žÖóÝØŒú¾~Té‡ë³ÒY¾Ý•µŸzGšEžÎó¦rÂ+0ÀÚE²U{¤PŸh#ü ÌËûz2JO|gקdÕ?®ýd}‘ZZ€€5ùÔliTÌ‚pjò64»aį ~þ8Ï2·x-Ðz»·À‰RL†îGNÁMœ^é“G-Ç?Œþ?ìèvØdGßHÎäcœŽ=…ûE®jO,È>û We(.Hô÷iO”ƒ6•BÛB¤wÄl-½xë (ô­v’6L,ƒ½þ1ò®¬QÖ kIóŸ½õúMy(5¯ýõjÂN‰IDP5•ÛŠ¸Tî]ŒŒH‚¨²\]µ4Ò¸š–Q ìv”oŠÕèíjtPµ†8 ^&a£=—Š<'EeJyÏž’l-Rñ³çXG˜Ÿ 8€%¥1*{ÓB¸ˆ‘J=H´õ@ÊC1R•yôv¡ ˆ¸$‚sm9èû! Å HªIù‰þnmÊkÂ@£/”›r:)Ã;Ð)lˆ‚¡iþW’ï§ñˆkU‹“1ÑÑ– WÈè…í¢<1m‰w"®HQ àÚ´s²OÍ +’b—"+k’¢x/í›—–>{ý‚)Iê*Yx·“´©ÜÍ=yÅa½|†ÀûÝ…"ŇeÆy;ˆ¬YN½ìG°%MäÊxlW—‘HH1ØN”òÜ :ì–•$n!2Zbm3ÖAÆ%±I‚ÙÙIó£Ÿ?Ç{9ñ6^Ÿ+;K'.X¦m°•Ž†Y‰!i‚2”>I¨W”òe6½Éöͨ+ãëJ ¹¬“I‘*5JS×ešTT¯âÇé“4êü²°œLyÖ·©™Õb¹Y,„ˆ•&ˆÄÁ+ÿÊO•å ”/Lí$ãBGVš>—tex‘†09‹î¯ !/e¹£ÏчRd489%Ñc«M•˜èÊÖ—‘ƒR>L>m󎽻qÔ=.,Å *óâºtÈcˆ«=âÚc¸¬ÄËGs—ÛŒDË4tÎÇ…æzÆ“ƒÅޢɷµCT‹K¼£‚·1Ô!ÂW]‰9ßíR;À=|n6â†ÌuŸßã]~veWWˆxÕΚrÁ{ KW.&?Ú3O¢=¸£O£E=‹—E½ž²AÁŽnRìJ‰DS‚ØW|I¶¹œ}8ù endstream endobj 364 0 obj <>stream +AòãJ RÞ–`·”RÆ"L’ ^¨s• ©K¨0®ˆÚ•ÈÐÙZcÁFðA¯œ¾ìJí]A‘ní²Iä“^Wjð}º%컶,º×¦‹Gàn'ÙØ)‰g+ÿÿ³÷¦ M-ËÂðûøq+Š"°º×, d ‚ à<`HDB‚IØÃýðüö·†žÖJ‚€(gŸãsïÝTzõP]]SWW±úû|/ßVBAAÒDÄ¢›0µä¤ð q‚eI÷ŒLR͵Œ¡ÍD `£#PÁò””Z.Ô§T¯'ÛNŠ\_’— Í5ëŒinjt£Ì ˆÔd¢h—ªµÞPñ#\Ê=ƒ»€î.UÝ@…, +ÕžFW‡ËstŽÐËÝÀž8Á5»2 é(ͨÉ{ó¢ qâ BähX«)­½ìžˆ€ãö²‡)à+§ìq +,ÕÚÍ11#p9ðÌ©Ô›:üZâplZ–¶64Ä]½E +áüÎÃáI˜„+Êó®€CýÝäu°Ìö)Á妀ËÄç÷žêäfy0Öòâ! 8º\—ƒ8ÃúÕå[VDâà¬$1(·âÆ©ä•ðÂΔXÒMµPvTS¬÷èJÌ€_Ž8b¥V`e²99®L8`/û1›üŽØçôÉ9õ ÀˆŸŒ°•ìêå@Z~V{Ù0š¨o!ÐoJQJnZ‘JåÞÅ`*º–S,â!a–XxŒÊ ÕŠµªgNénû5Îùgߢêyå‰Þ–/«ˆÏD`&c­y²¦¨ âð(Ï°ZŸ*ü0È£Qâñ§VÜF(€ hâ脈79…lgtCœ”LTã8R“ã²ðìRˆH&Zù }¤©>rÛéS$Ý– ·6î\”9;“`–ËD:3”åè¬Ë ‡X`¤¿Ôi\T46‹ò@…ž;[ð[wñ0Ǿ¹Â\ÃÚYX–Õ’†áëñ@ Diš÷Ò@æ^¤È"H38Î)‡Ñ‘‘塤#0Õ™f¤ž^ä@Ž¤µ@åÉÐ@‘ЂŠÅ‚¤ñ)ð›D„‘ŽÄ¼VMŽÊ,hpæTÏ¡„€jõ\¶ Ä"pÙ©z0ƒ4×Ç +bjz–&$•á ehvNõˆy±ZÖXƒ 4È£x©,ŠÙý‘Û +ª¼än+P™M¥ÒVdDH4L&B2)Òkå8Ñ$aj{7Ä‚ÌîÐÚûd¦‡µ$¼ ·Ÿë/»‹EPœdPâ‚\ëà 'âÇ%´Ú%“’tŒè–c~$ÙuV ·ßWñb‚Ò6{†vÐG™C@rÎe:亚™aÙ:'z²Ö¥ ¡UøcÎ!'äàÄ îtgÁfLw#ÌÜôviŽEøTŸ¥&gí.5Ñ{ebOèË-Ë 6£”`dC(ÒOD_.¢úIY†s¹¨ÕT™ËE­½‰¹\ ”ý i®„èY‚B«ø‘Ú0u·¨gˆ"O…¬ 2„%¹vl‰co©1Î#…6s·èmNÌÝ"Zâ#Ä÷‡ ænÑê»Å`60Ø õí[A„ÝÄaH©jçi­zHXîF:7‚ÌÝb ÞXÐ39=!¹¦æ‚Ö’‘sd|ž°½[¤ð3Alî Ce^i>2tãŸiĺ7åú +Õ zf­!èXáN].†*ä@úrÑÙËELõåb¨®QƒÔ\.†Jm¤£›šù²‹ +9¾ÒîCå8Eº5t¸ƒ{»È¥¼v-eaÜêÍÂȳ'º ëSèÞ|mAfªº]ÙÎœT$ê^W1F†å\Wqákšë*~,H«ÖJ(D2ÈÑUÕSÀÔ¹®òÈ GSg„I•ÙJƒèS|ô¯k\cR…’d>Ö"QkÍ’J÷ª™èÆÄ¡Þ'¡¾ÔRGkÍÄ8ƘrbW±fõBûJD$vBqpí²”˜²ƒ!ʯ)•¥ªAêK6¶5P/Œr"yíSÕ™r@ +Ð@…¾fÆ¡™ª GW¿âQÚ7Œ¥ä|æ•ÊìBÌN;@}_… T±†Xo=â&qmKµsT.p©´¾Ä¸É¥ôIæÂÊ3¼ÇÞXÑCw÷[|p㥙|~˜Î±ö‘º¹²â…´,}ee@î••jÿÕ D€ñ¡QõaƸõµ Íeí••°ÌM‚„.Rx£Ç ÙÍ÷U˜i5ÖØ$©I°W¿>yî‘7ª;+¼® ÉúbÈ·:½³Â‘j't Ë>é /ØV¤-IÍÌm;|Ý’ëÞó¨¾U+1vrúGKˆÔ8Ž^ªú2ÑJ—‹N+“C%¸íÃlˆÒOוªÅ¯¹µBw}¨ŽŠ‰‰4}ê[+äP”ZÊc&š¡Ï(R‹pé˜Òeé=6tlN‡£dNT  Ì‘ +øu¨ûqàsmEɲg;Pšº#»•o’Ö¥¯­4Ľ¶20­Úqœ›ÃšpDuÆ\M”r>¸* ˜™ýÃ+Vél©ú”ç˜Ù|ôKH™cÆ•k.PxA˜aïx«’Ä€Žò²"àÄ[ŽH18Ok+ rïm и…–OöÚJPÄmF$ê|ŽèT~>G¼j—2²Ù4JBÏçÜU⯬øç´95!&%6£NÐÓͬÒKm6;Á$Y5ÆÞ[Q>¬e „ÄU6ñY 0c‰éÒ(“a# mÓ©rAfY —©r,e³¢‹(eEÑéA¦úµu¾ñ¼( ç•3î矋.¢+Oôv\D:M”9J«V†ýØQZ18—ž@"ÈÞŽÆ”! +Aë ¥Ú™ËV®YŠ s0(5K²ŸÅ™ûÜÀKdîcŸüÉ4„‰ƒà$|qæˆút³LSÖG™ÒñÂô· ‡1X a µ„ Íe(0È3¼È翱óTÚ‘Øáló AÆ8–¦WO¦0lY6Mˆ•ë Œ!(<bX¹/dX~Ê:’\2âÃ@\!£Zi 8Bb+ñ\]¹ÆI#häÈkÕÎHI?&"Œ2"–Ã4Є<ÐkV÷ã„â¸2Cp|MŹ9#M, §Ä¢uéˆÊĆ ¾®LÈtFˆ¹®ä8 B¹½¯LÈ–ÙuâÜý£|¤ý +$ër{Ͼ?ÎÄqv#?Ê„Z¥‰"­¶‚u I¤4ÃPEîú‘£¶âÃã@µSŠf¨2ìÈS>!B¤>¥'йv‰¢aÛ;YhTßz48¿;;öuÑ"µöãèµjOÝag°‚/Ÿ½<þÐ á¥Ã@Šàqv$¤’ßjßã‘Œbµ¿Ö ÖÑ+®¡=zZquAÒñ"j &¾P=ðvHGõ“)ãì„Ÿ!y|ø‡™ƒoo¤È*ôà„ÎQS0¤k?¶§Å ‘r.'÷€‡øLõgœN)sÁÈh®ÈE€F +ë|Ûép(˜93Œ €q’fN•öLeöPÉ1÷Taåú¡íçd vò+ÿ! ùþãŠ8ÈHœŠ©VZ„ªè€•)éVð¸ # \ d¡ŠpÄæLZB;î:ΘgÅg¨Â«!ª(´Œ„6§Ç•Ð¡z’ù˜ïA%é.ÔÚn6S9J&—ÓÚxÛÉWœ[V—ÙÈzç„töóÔu¦9¤à«Ä!Á W@ÊaaúQѬ_'ÕÃøu,Äñë ¾­q:ó"F3(…¸jœGm¼Å»çŒðÉg ‚Øig ÏÏ}œ O?;„9CîTT;e_…7: s@™`dÔñÚ¦3Ô­e’ûÍÕœ½ÑϦ2;Bæ³òtdN³…‘#SÁRE¶D™Ú4åÜH9 yE4@B(šdÑÁÖª:kDüÆ#à«væ±[“HÎF½² ÐF&Y`¢ãŒÙ¿QhÙFÎ)¾‚³Ýº +2ß4fÔhü?™n‘âÚ¦¦™ÒÍÓ!¾†ô“Ì©÷•ˆÎœzŸ³œ»§ÞWÒ«N,È Å‚âg™ŠõS·ÌVøÊÇêl™v09ë[UŠ@æ@åhC?øt&˜ýL{3tb<ªZŽ¸@Š=Ï Ì*Efz”89¿ ©ø¾]­ŒDœÃ‰9¸³@sf•Þ„;‘èÞ|Í÷}Ç·K΃ÌΚ%ØýW¹Y³Å7⑘ÔR¢-§CôøI¥y%ßaÐô<ÃfcO¯‚ïs´ÃÛ¡'dpâ #×i;cÚ}pæ¦÷ /r|BjÄ%'gõ.IøäB…÷Îy ËË2ÒÍøl0v,–˜À4*Ø]3¾a ̸…õ\cs„ Çbî4ôÙp:‘4Ál\õ5rë¿`ª‹¡"1ÆGãÛ÷ö¾}o/ì#{çŸþm&¸òDo7ŒG†Ãa<2 +ã‘áP G„ñÈ0Æ#á0Žãɶóôܲa<2Æ#á0…ñÈpD óa<2̇ñ Ñ—Ú°ã‘6G€Ï‘áP G„ñÈp(ŒG†ù0²o÷Æ#á0…ñÈpDO¦]¤›ã¡8’0Æ#á0…ñÈpD ‡Âxd8Æ#Ãa<2 +ã‘áP G„ñdÚq ‡Âxd8"ŒG†Ca<2 +ã‘Áˆ0äÃxdã‘Áˆ0 …ñÈ`(ŒGƒ´C(Í6Óa<2 +ã‘n’Æ#ƒ¡0 …ñÈ`Dq´1!mCa<2ã‘ÁP †Âx4È~”äÚqq€Zߥ †Ãxd0Æ#ƒ| F„ñÈ`(ŒGCa<2ûnžÃxd0ƃªrl^ P Ž F„ñÈ`(ŒGCa<2Æ#ƒ¡0é…ñHDO¦ßüÊ`(Œ'³VÆ#ƒ¡0 …ñÈ`D óa<2 +ã‘áˆ0…ñÈp(ŒG†#Âxd8Æ#á0Žã‘áP ‡Âxd82Œ‡T)ŒphàŒµÅég m{ 9 +Èœ‹ò!GÔ)#2å2á(@`ÎQ@ô˜uhPÆQ GAæcvd†`GAn*lÛ;SÖgahØQ`ñät–sdp®ÎÞhGAfÙQ A®£@ÃÚY‘ë(Ð.¬Œ£ ŒµèÌ9 +d8ä(Ð^²Œ£9G +®œ£€qyGsŽ:YG‚†¤¿etª²Ž ªg‘2ì(ÈYö„CŽ6ë( éå´Œ¬£€›udâsŽÅÚQÙ +í(p¶L; +œÕŽ ršPŽäVCŽ‚\CÖûòÀ8s+m$rä–AŽ‚ÌjÙÁ‰ 9 +ÇYGíDÖQ ­’Œ£ ³³f yGAŽ ØQ!m*瀱RwóŽgXm´ÓôrŽ#ŬÀ¡'4ä(ppg;Ë; +2ûàÌ-ï(pvUZá;ì(È‘û´f›9 +4Ì8 +bÔ[@’¾ â T¡D-l#ˆHN‚öù˜|ìG®‹°m áSæÇL܆?’«%kÂæ¥6/“‹Æ5ÌoÏmpÅiÞV`•âÃìëÖir *¯A8Ëoœ¨%êKã4{¾v`:jR|~Ë ¾T ý%#fÚq¨u¦.›XG×ÓÐ*dè«î”Ó Tê=5Hõ±§ Bó%'‚ÕNeË"Ä: ´ýGõ‹D¬Û Ù›[ J-Š,P9 pd»ùÚiªœAÂwœújLøÆiÀUEdî¼Bµë3À½’a®çj¾q„*²eœä/µÏÀxu|ã3¹tŒÏ TáàTñNï _‚ +ßø ”FÜ›õ„ê2“X_kŒk_@¤˜ûŽÏÀÜ ¹í8 Á(bÑŠó0.ƒH³gßXÜ‘z¸ŽxS.’ŽËÀj—AdžzjŸAÄO>1ëºqDè%<—½ø7¾€H…î ™¹³NcƾãZHù´:>2.¼²åód|¡J"Èe¡ÔžÆÿèø BeQQ }&õëoã3ÕÛ{ŸA¨t(*§†YpT‚q A'‰Â瘧Æirm-F°‰&T!í3U)ùCã3p€Úg€})ì*—Ž¨æåšÑªR¡4.ƒp– ËCæJŽ¿™.‡BA¨ +ýPíL}lcƱã.еúèé™q®,æ¡4ŒHç^Ž» ÒGØmG•V©7å.ˆ”G,³N, ¡èC» ¢Ù„Ô,?¦,6dÝèkwA¤<#xö”» Ò¤ÏÜQ}êtkwA¤·Ã7n‡1Xw „œµN; @I˜áE†esÞ-Cž¡zö¨ ߸ B]Á7(͸ N#d&€Rßô3”#Ûwò2ª˜8_=Öƒ”ÍBC%¯Y<#škrºyæR~9žMR§ê®g“Ô±æšù8"•\¸Iê8«°È&©SÏëÝ$uFâØ×òäD[ ÉS§Jð:yê"]6ËÍS%JX±Ë‹Ö%§·‰ê8þ^ƒôúý¹yê¨î “½û&A3OȾ>á&;‹W´¾…È Æ®Mex;® +›fýâZ¸hÕ)áãä©‹<§á&ª‹„þÒÌ~(íÔÍW§8E6av38—w‘&P~’9$e€–ô´¹ï¨æøBŽÂÐÏ|dÙ v2gΔ¹)ÌPR2 ÷ÛÐ9*&€’ÌŠìé Q;î~m¤8”äPZ Vî4®J¡ÄgF±L3G +/#ýì‘ _w«Õ§^àç¹,FiŽ‡V¼e€±p6„RUQL‹U>+.B*âéJƒvß ¡´ 7„ÐM¥½¬À Y»Ë +Æ‹S»ï€en›(H/2Ƶs¤øôd“ÖñCVçcÌ(”,·ï(:3Ï-¤ª•hµ +6)\ÕƒÓÏg–ŸWf2”ÒªÉæbDª[[‡¤ÊâäP ªÚš<œ‹ªwèR *àŠbŒ?ÍBœ‹´#¦3{1bµ·Îäô­…TqÓΉ”*Wnæ4ë ”ÌqÖW(ñ*Tþ!çbDDŠÇ»#\üЙ²OÅÍ2 s@®†`Z‘0YmChuÆQJD 5£¼ðÃóŒ†ãiIà¾åpðÌiv€îÕßT’~ïx(…"Ý ïx²•!¨“+ˆ¤Ñ©·žl%û|Ç•mÜEÖ•mAŽ+Ûýì•ÓÀe”Ú€rVå”_Uå:“É™JêeTiU +6«‡¬¸^ëT;~2@©Ü0Ö—Íy3ƒ/µmèž|‰Õí²'_;‘iµÚ™mAŽ3ÛÍáKB¥¯[ov¢µãÌNR­œ«-“êºÖÙXÉUp HŸ© GøÈŸÊ^È™(`@/òÀXy3í¸ª"©;;%²«P¥kÝÅrÍ€ J,ÈA~Ä©»h#Ý›±áÌݪi2¿±f vû9ÍX† $Õï¢~U“cØt¨%šÔÚ#knFÐu ×Àª’P—Í1¹ ƒ¨0g;³è5cºÛ )Âl–UÌ– +vû™{³r—ŒÊhïE ËÉ2ÂÍÜ‹D)¾-Uf(½„ü󡔦÷Ÿ*«Eøô–‚ЈèlŒ®+|Ž‚O_C®tí´Ž´ïU$LA*´ŒýC³¡Âš r‘j€ûŽ©ÝÆ/*Ê°;ékÕ×î8ú¯ÕŽkºPeÈ24et +—¦|ƒì~«b+,ÙúúZØ¥nÔôM‘Í ÃÚ€=*boaÖÜ`›ë è"SA(dŽ LÊ}wcB…5gû8?CP¾Næœ%(~`o9¤oóuƒ‚ÂP©ã„bb gå!bÂg ý/êÕ0\0C”þL—!ÉE\¯N´C7Y†©7(Ë}Τïò[Ä—`˜uQà)‘ê¹5ð¸2¥ü6ÔÂå%(1¸§MEs$‡… ƒ²˜¶rHù3sË7‡ÕÊ6‡É¾z2¡h¤Q^„ús‘ýœ¯Up˜ÀÔy¤LD¢@HÕ#GQ`~u[/‹ÿ…úªÒu†Ç[ã%´.õuœGD¬æâb–_ {`wI¼<ÒÖcað9Ñl'âé„W£`;ËÆ(ÐnQŒ•ö©ýÆaÏâ³®yš+“ïÓ£ÌÏFÑ¢è3 +”a–¹bÒcrü縫"4­ËÿBuØ/üU +›ªFwèòݪ.ëî dB‰¨|·úÅ­ú ?ڂ޻ܑ©¦Xɶ20åÁ´G8˜†tŠ_8“ÈÌÁ™BfÙ äÇ3üØÑu]Û18Œ3:ª€N$» ï±’0ÌþK5¢ÕɘÐÚý¤yŒjÎò¹äÓÜù—¯ûô«Gç{Qš—Á€xdgÍG8V;Æ=*ŸiìRE¾è•š<:¨Æ<É1+Uç8±áí¶¢œæü§Z)ýÛ67ÿÒè_Êù>( £Vú«N‘d«ò…~½ùûo#½*3ñK1C>z´SgÉîFxv#àgâpü©ÙÿÓ‰,ù¨=ÇÒægèÁ½w¨Oµ„ –¾Ç.ñrÁæ.!øFwèpÄðØ CÆfîHŒ†þp!‘Å€C‰BtP–Ý_µ)FÒDUÁ(8ä¡Z"Áú~¨LòB–ºÌ&`sw§G| £;„2bxl¡33÷_ΨþgÉå×ò­ÿâ¼+s¶éÈÙÏìK–)bŠ”§Cˆ”0U¡V©ZÂÆpXk<tìðD^º_"ÆÜàJêª1=&ÝPf¨ÚSŸ¤/SE…CPW(úDŒc:ŒÌ‰(‘ ÃRz•8~¨¾ñ®0° +ßèÍ4GLB:õ‘¢ËT"ÂÔ7øÈA]=â¢Ø¡1:j‡öŒ¡‚‘çaHÛÚô1'oHñáˆ3>’YG:#†Èê˜38bQùGö‘x»óç÷ÉúÏØýçô Û^îÔÚÍÝ­pZë~¹ÉÊy«Ñì›VùÁ—ÿ>ëövØyø¿‰´0õ°ðöÍÄ$4ôö—; “þöK0ߎ‚L½†u{Xãió!lqþïí_ç&ªp4A¥¿éÞþ¬Ã?¾ˆî— +>y…º·;¨m«§I„7gì_öøõSbªe„*eR‚µUà<] ë¯Ê¨D©Ç#KõTˆo%âs˜h~´òÄ!L¤B_³ã6J¸².Y³2ÆËÁ(ÆR0vR*+/Â!ÙǨGÈÀúHf°,]>Gæò‡ÅôÆZP+™P5îY¾:ŽèÕ }†O¾0ÂRŸòYÍúj4Æ £öø©L¤*¹”±ÄÝ”ÓxA« !˜$]Fc¢.¼¯Œ°ú™Äo(AM±‡‰ ”P ÛÄ‚·â³ß¯H@žËY"‚ãõ`D ]ÿÇæHJ +ø’žîCã” »ò„¤ªÇ¸™tõ Lp*i/ýY/âhɉo’€ãJq3)_ZH­Ôƒl@ +å‰NÔsp@Oʯ¤"SDJeŸPE[L.‰xBÜ%”¾šSuj pÆà |tž`2Œ:ˆaôú !%¶J(ÇÈŒ—ÈŽýê•à€«%èèÔ™8a2,i¥´>@§øø¯6c|úDßáû|òã-}Èß!Ýcdl‹/}õ–˜¢ bO]€‘2y€ï£0P+ŽA¼'x÷„ª0ÌŠbS¡EÚà„ƒ£±¸¡DªHÖ?ÁžLNØü@>*,ÁÀ Øó@?t°ËÅÁéñ{œð;´@W‹±„QL½d§‡sFÒãÚÊHp1¾REPJ%¨1Å2öXª9# +dÁ~ÔÇ©<ˆá]‚ÀÌ"]qÿ/sñ‡˜ªd§°×ôŽ9 +Ué)üæùü-6’‰¤J|ƒXÇÀ68B%g‹RNg†¥Þé=1~šH!LfyŒ$¾•2 ÆdzêŠ#ð7&¤¿S‰”áóZ”ª!² *0ÞðF[0;•JâßÁì>Á ¤öH2(Dªù/õ ©.* $DÁ®X¯™f&Õ‹ÒÉä/B]á4.šZˆ¼2D¦ {ª,aÄå¶C¬l=à¢ð |sÐiˆ±!ôÏÆtJN0]1# ÇÂÐ7üˆ$ôðÑöÊ¡\X±ÑsqÐò¾pf‚@r §ßè#ÒB I ¸Œ‹£àá¢6è>À§ã@åò‡- |ŸÈ.N©Ê5&Åñ‰Ì|~Ó‰x ñUM¬ýEꕆ“G¿ù¦8˜=¢¼”Ô1¿«‰0X/CÏêàÉ 2Gœž(åä ®%aLéû=Uü-ÔoqÂå–B]pÝœyŸ¯rëK¬§‹†1?†¤T¦)?ÏHÅVϤìSp>MFfÆ0~ÑY ‚1ùG{ +Œ’SbR,ä…ýÒã5|L–@›¡>¾¡ À `&vŒ¯P¬ë=à£Ð3ËQÊø˜ÃC|­K’ E•!ȧ,Ð)¿õ 1bˆ¾c…*àPŠWÀ'F¬"Rªué1å"Þh`ä(ä¨X'1¡™Ÿ?q?œm*ðy»¨@ÕPÃXO8©A̯‰ð£˜ßPT$jP;P®Àì¸>]Jœ"¹¬:~’rf’ à88)Œa ">x GøЙ‡<I@=ýtˆPå’…Þ8n= åJ¶$û„yqH"ˆ7ÄaDlL$(£P'BDç‘U€\š2°…ºÄ2*P3ào?!…Èç‡mÀ6¨¶~G Õ°PÕÜŽ8 'p°s|>Mx†ð\‡‚Qˆ€õ† û@:„ñA›ñ @PPB#?òXôĤQ1o#F:[dHH¼1>ã‘B=E9D "bþKõ& !æ׋©l‘BÈ%bÌ@KÇ4æ²o1%Á $Ç,8ðÌÇx£† óÑ£ð¨ÚgÉcÊIËyœo> +(è‰ÃÌ c¦ + +²tF +"ΫLJU¼#Ûû¨†]ÈsÆAÊEu6Æ4›Tæ/`ú¤n}:IœE1{Ö}®Rƒ‘Ë…’ó3öéXâAMù9B¨Â(p’Î?òf.:‘pWqÀeXùpS²âÍiÁþ­wÇ|rŽ)”ÊøÀÑE©à©Ò7’SJ4@¼b;Ȩӄ¨  ðÇžbS~įœbÏVÂ[ÐgÀúŸ°²anvê5âÌ”‘Jô¡‡A™Œ¼¿ñ9! Ê`’g&JÑðY2Ì€•LuU±Eªo(«S¤ÞUúª vˆ•„¤]f¢ÈX‰¢º‚ãøè¢T£VʾW'V‘@9\‚ˆ!T +X€¢–ˆ…ÍP$¨`c¤Gøc~bçfò˜ƒ %O:¥`P,(§¦`Óe!§êI9E7V1é!3ң݈R‚ú”4Žž2D˜L“ò€ç¢Ñ‘ºñy¼¢7(“ÆÂMdužÔˆXªâ[(É|óQÊéž“à§xFQ»E‰¤¢OÊ-ŢċɦBÝžrU¡ý@¶nŒÁé1qÜRN`vŒXeÁOÔa$jJ'c~›˜¦|6!åÎ%>%è¼Å˜Ø§6d$ã©A–€Ë¡t9!Dœ…µvÒ¾‘H1ÍLVàc´õþ›Òä `D1„ƒ…„„àq6žõ‡´ä‡5qோåÀ11m æHÌf +½³$Q"Cê&8Q´v%«Y YJ6§²,•#Z*ÁŠF?¾{Î㞣6Î ‘Ú‘Á: Ácx7ǣłê¡a`’‰Ùj BÓWˆaÍ «¨-‡’JP8tüT‰n©é8*G@à±zzºªp¦ !ùMr[P•.@wdë€À,}<ª!=çöUjaP aß$©@=Bÿ0ºI› äà ep t¶isF§òjM\½ 8š˜ä} tºŽDªÞ —"#[zºÖ—àäR¨§Ì>¦nƒi+©Ò[ÈÆ0í%'Õ j[°$‡DWtÄziª¿¤l6”'Dª1´¡©p†1˜¬Lnâº%ð…¯Þ¹"Z@/Á @SI)z^ÇL<·5U%fý”ã£è×Ô"€Ob¶ +Þ€ ªaÊ\©X­& ñ(Íiu6U”c¬£G( µŠÛó–TkŠôøA[ ¨ÛDCHq¤2¬„áÓd.uΤ¤ õ[$$d%È<<¤äƒB–¼ç |–vèïò(ošZS™Üu\ˆ ¥̘G~@ùº +í>Ô”R•Jž€BvŠÈ÷9Ñ.0“êTŒ@lj`± à ‚¨úfѬz|œ*Áá«dú3ô(>fB¨$®D&HˉÒñ(S=cz7ÁGEräˆì¢J¾Ÿ°Ë‰ N$Éc¤ƒJ“›ÐÊðpsšrm +¥„ø“øµ1iÍПËìŠý‘"âlë¬ï©dpáaHTf9mg͈˜O6NRêGI¡.âí©ÒwËc›.嗠ĢFÂJ¬Ê9KƒNgYLÔ+{Tp8[Š.ÔùtqR¸i!ª\1U¹1 %y:¡£G }¥ü‹”Ø‘JMš*EÅ $NŽ`¶Ô8åªBô²% uZbŸÙcR.¹Ôû7cVÙpçŸü0©zz”Àé0ÐsCoi|º?c›H¨4}¸­DËhIú.Åq%»¸¸Eªõ?„¥d2ù(öjóqƒÞù-=Øb)±Áçi„òÐ% ãüFè +£áU£ýD®wrßà$Oy­é::HK76›D<‹>×ã“Ø#[–¨óÐR`§BÁžPUq"Å\‚Ô>ÎÑâƒ8Ujr*N‚¶KH¥_uÊ‘™äëåTó¾²ÍA!äʨÒc&lÇËRLŸ€ßÅê=!$¢*I*Úbž°š%:ˆ) +¯N9IŸPRB.”‚û*g šq 2SJ$ƒÊy½è¹}D™cÐíIYÁPÇ>o^ª*¿F˜’ü\‰âñhõSò'¼‚@j@g¡äs…ª:n¤ï€è*wQRwÉn7¾f±Ó’æghH3ÂãV\74Ž”jxh¯,Xø,Ñú橧ôjVÐ bŸJÄØïÇÒ"¥çÁ¾:yê±—ÇþJlÅ>b¼V"\<¼ð˜4ÑÄ vÑø!Yâ묤˜¶ÿöDlÕœn;+rtkIŠƒJEDþsÔË„ïÜ›ùx±ÂN ¶¾äËDÔ"é ùx×óõé5HwèëB É—ªFkŒ—P`¡huá|ñ>ˆœ›¨Í¨é v÷| ˽4t »ÍõT<Ũ‹¡y ¸‚“ 5eýi ;æ÷±è¾rGê ÕU§Ïù8È_)Ó%YˆW¥¨v$D’Ã>¹ ¨ØŒÇ^jfÑX: QçKGŸe¨q‡Zyä©ò×(*„`Gj¬rÀ2£ûx…žÍ}»*ï`ŠÅéREåæ#V‹þÔ€Ï8 ”àcLtKÉ e¶Š—ËÞÐãÅ ¦‘lD’_£à¤䓉éÊsRÀ?"JVLi˜Ñek£ƒ²WÿŠÐzà=t!Ž~;ß·HCE ˆØ×2RÅdTÈX½½sð'ßkãÅ%ùQ<Ž;ÀÓ@NÈ?ÈÙÎ0mpBn>Rúù½õÏ`º÷Œ¾n—t%ð-LUß’£|FßÞ¬û1úOÐo”â5zJiP€Ñ +zoc¼9ŒÈðôH‚Ð:Åd–U³:‘ w 0vŒÜ¼tqL*Ì7NX/ôÀ}&1ºtÖ7ô¡H6‡)CüM¹1³y84 1ˆ_y;D,ÅÑÅ‹41—Š•¦Ž\3ØP§E{:NøTâߥ€‹FªG–9ž2‰9`cqhÞ)£ï¯ÚÛþ%ß›!„ÚKÖ}(4A(· }M÷aÐëäàÎ"C…_È­€>´ˆdn¯ª¤—'PIÔÕ`B|]»HŽxûnU"5U&<&„B¼èƒ­ +ˆãJ¤h»~Ò ñ‚‚MÝ@ÕàÀ‹mr­éߥ€e£Ï A  謢%‰7lx5œÐ:ÙŽ‘ÊŒN :¹æ ¢ j Í-ì8R#õfÏg/)šøßE*‹¯Ž.Ÿ¾HäÓ OÃØ9Õ>Ï$BG=ì2ŽðLôþïFå”HlTÑ"èÙW…¶Pk0wkeƒR0ü TîlÉÊ•$…%D—LíJ̘@îm‰xdæòõ ¼–ü#³EQ¯?‡R/‚®ÏÐGÒ*1“r@èIÌF£[!f4INv:Øó(‰¨œ%zÑR’T Æk wîäbðx‡13yÒ€ÄL‘àŸ˜ÞG3‚ØæúF+اrn¢ý‰W E,¦Î†“†¨Áqˆ²Yw@yD‚›²åø* ‚v ù>ßß‘ÓÆ ™tPJy­#xö|ZõDÄåƒJÇ£Áë+ù/PõòÞOºtƈ.TèÝèÀ +ØcæSÙÀ˜þFö‘‹/ú{ÀðD6žR³ÁH§U“åŠz’£T™ù)/Ž$,P\„ôøf¾@ÝÿF~ð]r. Zô¡ 'ÿ¤@5Ÿbh}Rm|¼ …‰Ñ%:Ž‹ùÞàoÊŸO#À¢yœa—HRóSV[$½” 5FvøELîIÉ5ª‡~Âæ8tJ~ô™òò˘ꆠã%=uÄ®X2 +˜’Ð-‘®‡¦)J5²‰F/î‚YpcŒ€GÂ!Øë›2OtSZ k?|®sþ*íÅF÷ªO„„¡xË©(0ªˆ Š!Lܾr±%"ëÈø±ºÄ4‚Žé¬344²”-èYH(‚­J%C‘n’˜—;HÈY r Odg¸ZŒÎó1c$^ôSJã˜nïèÚŠ k–¡_”Óû vû¨÷ÓŸ ±€c?P;ôØ |6‹ÈEÇœ=¾Be.ÖœÍ{R×i=dT“+ˆ4¿@eÆ$ >àЃP©ÚêæY¥.’*F,ÚQ¢21b(ÙOèˆ u@‘Òêñî}ÖÆ=Š6Þo€<¤Äã’"ñ›T嵧|RÐeHØ-`Ìj~rãHÉ*ŽöŠá‰Å«;¼Å§—Cxƒ€×µ‘¢jr[†ä~2Z&…‡£èÚ±Ÿr=Ò¡K›2¥¬€J• Ne˜Ú麹ÐÙèSe—¤’F–g#å„2–i£]C.‹—ÎxåSÒgòÝÓÕUÂéµô;*€ 1òAü;a&#'Ž@eá0]dH2™¼L¤ÌDÀ] +Têæ*èÊ'*ä‡Àa_™Ø]dÕf -…$Åœ)^zìm¢l“(óc噥`J^¨ì32+!›AvàÇÖOš‰oqYy%ƒ/GN ¤ï­UAA)ò)+u¦¡ó.d÷4Þ\DÁ1Tµ!O]hå*s¾œ‘!Þž ‚ö +õîéY÷¼Ó(ôkgÍÂi·áÆAé…1fYÇ' X¯O¯#€"¼„_u€ÌCo +ª"ÿiáâÞ¯ +ñÏýÝÎv¯ÕÀêffLAäî/Îð—„ÙnŸÃ·¾Â +&¦ŠîA³Pê÷ ›µNí¨Ù+lõÍÞË+ðåZ»Ý:êÕÎŽ[uÕrV?W gƒÙÂÀÜpÛ‡…™‰©ìââ/²ýÂulýYk·=÷oJ]\оڮ t{ Õ­ìÔq¦]™º8®õêÝZ»0S¨6¡E³wñG•ÖŸ@¦uÑàn³Íî™n¢(dÔÎ…Õ&uÑkÎ=,ÌòFed¶ù†Éeh¶@0]ø¯W Â˜äÿãûü/Ð; +Q¡¸¶_êÁk7éËÖûýò.´ ÷qãûÕZçW­d ÅÀJ\zÄ?‘@ñ1‘•ÇO.ÿßEs½Öо¡VÔ7Pô.ÓÔ›Ö= Ö­2¥ÿUÿó“5kÅA 8¡]C‹ý8Îï.l÷öë½I:Å)`^å!Ö@Y˜+¼êtj§ÍF!š+èÿÙκæ +„3Q°ƒ„ù—Gg¬ ®‰Ð1óT,-79n­ "7Mä ‰ÊlíSNëYá49TÇ®ýÖø¿.à&wfïÅÝ!æÛkªe¿hþ¥[‚ìŸB ÿíì-ÛûÎÆÝ01¦ü lèîàŸv³?1÷¼Óý«C€*6Uìt Ͻ€ud®jÚŸMýë\Y)wÕV„íT[7`èCÖÍT“Ç 6Ͻnõ[  a‡Ã=ìjõ“+ôPªõ[ugÝNã¼5¸Ì§ó…¹íZo0¢—ÝA¯{Ò¼ü,$ýÐÞê©¡GW¥v;ÍzÔ¦þÈÍ œuÒ˜ì6J1^ 2ô_Ä2-7ÁØB4{u°òü4ˆˆ3£CÒf„£ ï¶óyãQ™·ÿ/.¼-Ž~3(1½¢y3èá›Alõ¶ëiæ …)gÎh©›?N¹ýàC¨»uxØoÒüÇ|¯>Xk·ÏI“ïöfkg ,ÎñšŠ¤§26€¼ ˇ¨×Cë½Öà:ÛkÞŠrsîz­Óo±í3¥ÿ‰dG±Œ³Ö®ýÃ>C /c7PÌ&7³…¹ (µ›`Ã]~ÿ.¤~ê̮͛F¿^ÅDŸî:ìlcæÿS©iá†paìÄWýæòŸÍÎV£AûCX má›Ç½±oL”€h§´ݧÑj‚¥™ðLgR+SôKøŒ¸Trlæpë4‚ÁñO¥‘ÿšÞC­¯ßüÿ{ÑÈDË-ñÉK"âJ'`bnùïfýç@?з£”À‚ˆÃÿEð\uñ¾£ºl®£»` Sè…±‡—þ‚òÊ„©/<™Ä î +|[¯h})Ðp“üQ3ðE±âK=¼¥˜þj1ý¡ö`4]Ïǧi£t#‡…Ÿ€ÕùoRŽ4·ú­ýlÁýÖ~‰C$Úà*¦—ô¡c‘YjA¿÷:½„?C•ø/Ô¥ø­L"ýߺôµuéRóÏ&l{÷pð¯Ò¦µCôÒtúÃÊÛì„ —ðKü ÿ^ž"ÓÀ§äèëØãÉ4 +“#k0ô Ón˜Å²)U=ÿ¿ÿû§°YëŸÜ”á¥oᬙu±?XŦ¦vçýÛйxqÊTÄK¬›ÂE¦ËËá#ÖèèöˆŒ@NÛ­Ó‡Ì>(0ækgpö_/NÃ@YŸŠ›•ªÿk¶rŽçü·hýw‰ÖßRó{öf@ò“}g2å˜|é‘þ$©¹×«uú‡ÝÞéÅ‚Â4›­µF §£Ë‰ƒã~½Önn´:.)´ ðê~µ†‰qòüK¯;¨ šû•æQ¯9ê£×ÙHÄtÎOËݳ77CÃœÝÓÖÿ53ýŸvÿ„·ýQ£¢l‡•Ò¨ž—w»Ù«Ã.õöZ÷¨éœµ:ÛÝ–i©fÓ¦Ðø÷„åQ ˜ü©ôóŸ°û6¶·verø'4î³Úm·jGÍý•Ýß—™ÿæËÌÛ?¤†ÅêgÇê)LæQqáù®‘ãÆdOÎúÂ÷ôÿ0,ƒÙ0 +ƒO…³ŸxÊÿÐ…ºlˆéÐü0ô|Iõm…—Q"¥ˆ¢  WišÈÔKÃÈ“ ¨T6Ì£ì¼ÅÅ‹€ß<ñy"½pÙaå²ðö7_üÍ¯Я‹ 8“ÿÖ«øϲ^ýËZ¯â¿Éz½E&½ ¬³Öèž÷~«®ÿrýŸìŸ×U~ÅHå×ñ+üZ÷g:°þ­¼.»·fE$æË–ËŒz˜4M½´ò8ÂRXèGŠA#!þÿI@‰F†²áPGö±ÀGʼn}t‘üVäoRFPµB¹Ûë4{ý‚,œýÔÇP#Õ°Ìnê‚>×é¯é{^Óÿë妈RLü+°I(=ÌLÁ?d”ÊŽæcÇôíXt(Ž±*–)Ä Îi¿¥ô’«¾Z¥M°y)ŒŽu÷Vè¿%ü¯;±üO8½í’9ßú_% éœ@—]ŽT‡ÜÁ™·õê`æ-ç›bJ4ìu’¨ +â¬ìØÍÿcõºõf¿¬j£6æ‡}ÝD/Ya¥×:Å?Q¥*“ÿò]ÆHªø–F©Ä2œÊ…:nÍÇÍÖÑñeN™iy»ÌX‚±‹¯YÁ†Är˜É÷Ö÷W«Aéw¾»<ÕðvWb9ÖÀc°ÁÑð½Õý}©3zÛ«šÀúšøÞzþ¹Ìzþ¹íõ˜k¸qË8è@YØh¶z-°)/³ªáoþ∻Ýó^½YBGík ¿n{ +§ÍA­ŠÒÎ#ýÁyÜm(ÏÄe¨ËiL|F21%öÒB­78èÖzB½Ûîö +²€áLK¶íi£3 +C?ßR\¡¥3þwÛœˆÈO|ÓT{nʵΟµþ® +­rŽoqMˆÂv¯Ùoöþlöš +ËÖ vÐj·¶\!³¬R»Ùlàñ|m÷@UîuÏŠ½f/h‡užs2XëÕb”GÏ·¡'×V53ü—«š‘7©j +益ƒÈëêš"oA׌ýÄSÚÝOÐ5AÕºB}™Â$Âà”ÍT¨-¹=eÓ"õ†•Í+šB›?¬l¬þzeÓÁæÍ*›W4„,6\Ù4è¼=eÓAë*›âŠ‘ƒÖk+›·¨l:ø¼QeS\Ñ&rðymeÓâó×+›7«lŠ+A¯­lZ<þzeÓÁã*›âŠÆƒÇë+›îÿÕʦƒÈU6Å­‘×V6-"PÙœ/˜¥ÐK÷ìºéi½I¸ÛœŸ6j£óÚQ³°Ý=;?{8r?tIÍz·Ý3=òR½pB„§‡iœµf ºT —³®JràG…gµF#×[­Ý²y‹Šk…âù [Ø©õÍž›(`mtÖ\g´Za¡ÐêwÛµA³p€Ù‘aW/l|ÒéÖOºçƒÂQ¯«‘â4M gµ³f¯Ðož·)õ®ÚGgjTdå¬Ökvêÿ@7­´ÖKønò› nŽŠ Lb}p¶¡ÕY“òP¬)ìWÛÝnoÅfÓðeŵ-Ùìßkº£ÓyØMÙnë¬V7“•‘ã&‹é5œ¹†Išˆ šÚ©bŽ ^b¢”xÇçw–txØo4kZn¶Ûåî¹ÎºâíÚ%rü4W»pºU'—³ð=“KÆe=QáÐœÖZgÐ*#¬õG…%$rNÃP’³L¢Ýf{µ6ΓiöÖ*}G*Œi¹Ñ­×ÚÈ+ܶnƒ=̼‡£*BÏ›õœ|7p8ªçí¶æG¯0Aøuxáž³pà·íV§Y4ÿäŽÛ˜–}ªs ð8 Uñ>œÍËóžÉÂ&[Ö.·¹¶ýÈm¢¢—ÁŸí'@¯0jñ‚+ÿÙ$²…ÅqìþL'ëþÙìazøþÅÔÛ­3Ð¥1yîß…^óø¨ú"¶9Ë2_ôH“ù“Þƒ¶jÙ¨SS1£õ힀²QíÂlvU%q››r.9 ¯öòÝŸõ®ÎÙLƒsgýY ÿ¥*¿ M[ÔI-RásíúǵF³×ì_جÑÕÅ9g +çf[„»­þ>›Í ØšŽ¹V½^Φ ËeTË£|Ë1ío@DƒŒŽqA;Ö0.nÙn]8$*t¤Ï¹V稆ݳz÷; úì85hœ_ÅnÌ}n“Žê¿×èõg›ÔþßiuxÞ©_@%ÜF>M)SO¾©u:Ú`ÕÍ¡Vœ|ü¦ÕOgOXŸz5»;[xÓ<(”»TÙâãÔî›­í ŽpMæº9cû0«òçÚ»j¶òJÆP?Ìáê§ÿœŒG­Ó°;8nöÆ·üÚ=˜=h Nkg#ŽÅˆS{tz2{€æE÷ðp–sž*åmlóS,ù‘o>jyÙÎÏûM° È”ÑûîÖ™ºÝÝ6VN•dÜqCãõíÙ÷H²Bãé;ô…Ÿ©öö\曳S»séI5.Ý9S²ùb$k>ë}—à±]m«#7Ú9—ãtl+7í´HƵjÖŽš³mл.×rÐUœ4Lƒ‹[öìðq8v–ÜÖ%Ð) 5°…ƒ +•(#½‹7{ÉpÅáê9¬oä"í‚®ºŽã‚Îr͆¹¶"/äÅ|¢Ñì·Ž:#¼˜ù†t:TÂá‹z¤†c¼°]­ÏŒêRg¿ºXHÕÛ=‡¹k¡¥;Xy¬[•ó ]«+ÞË‘­{Ùn¯Jqíb-‚ÒpÜíýŸñ”Žlu¦üüÑz4{!ñ©6)´ùVT4\÷õÝv^¼Æ~ý¬]ÿgmÆe¾’`YV5“YŽÉÀâÑ-–åEÈãï´©÷º(¶Ôä øk«sxÖJÍzN‘Âï ŠN—ƒZ/ïü¯QXñr‰ÆgIßkëȘK¨Av—hl¦q‰¶Î4F¢ÃÎ`¶Ñ¾˜åq›³Þa·s¿Ãf}°jµÁ8ƪ´ŽÔ‘ºF4O4f/´&;Í£šÍÊ=ÖÆÍ]ÔÌ›ìàÀ Eݸ`Ñ®o9´7bÛ­¿›íífï°© +sݱ¦¿¾3]«d'tÙ SáÞ vºÖQQhuÈŒüâ=ç*OŠk]ß]Ãc€‚hãõm>‘T†µqq¿±{l…w»}±ˆï±ôúMœ\ïâþú'­3±“K±FüBßš^a²ú“þ(zZï¬/CM´)[êâaoä5)£ï©¬|O;®ïi¨©îm×½ÆÈ^ á8Þ3þ׋E=ñE윪×Xr}+ß'7QXÞÞ½ðº87ÚVÎ —2v6bÎ"sÓŽÆcÊj;ŒxÜæ$ >÷"ße~¥™‹sFÆ +–AGîPë4ÔMúEwçüÑ L‡>¢ù²#ñl®:õý±®ÁOZ“v,Å\£ê³´Ö9)`QOçÍ•Zºïânym- +M<Øøcðlëþ‡é…7‹æko¯û÷·fJK½•Óã'G;ëÕ;§”[µÙþ½èÕêrt÷ÉÒ«•§›Á³'l.õÎëquYn&“"îz^¿òµrôØ»·4ÿyöÑÒÂã³þRÿ¹œ›˜\šß¸ÓÓÖ¥£Õ—K As·ÜZ|Z¯ÌÎ>8j£ñÆ‹+ÕÉ'ñû•Aåë§Rð~æqñ´»Ñ/®íŽ§ŸFwÏ«•àÞ›Ò×öƒ7“•Coý`dg÷âô0~ýòÃÇâ^yöõøAÝvO>--œT?-=éÏžNWOžW§V‡“„¬ê—ý­óÊá§7q©½Ô~ûä°t<(ÇïE_îWêbãÛÒ³o¸˜r¿üùèsþuÿ[e­±v§4“|½WܹÛá9¼­5Î'&Ó¯SÓõåzørª|ìÏ/'ýûÓ¥¿L/•¼ª–›çž¾^¿{<_¯×Nð_­éåÃcYxsµ¸×º÷åIëóz£Ôž|ö`¦7ýñ¼¸±{ÿÎÿáÒüú±?1Í¿þ´TìÔœN/nÎÏŧ[q<×?ô‹½úš˜>y"LõÊzÿ5 -~ÐŒßø^ãI«DwŸÆåîçé…×OäÁÝOÔíÓÎ$,èiôè.nɇèMô²ƒxzZ:yÍ(Ò|ÝØðħ»›•¹ÚÂýêé÷=%Â>S/ÔdbÒ;øc- O?­.¨-¼Y~ÎÍË—¿pgò\Ò}ëM?}ºüXVž-ª~Þ,.Ì7¾¾øL;i& ým•B5 +4*­› |²S‹;ب,¼Sªìª+Íþ³ z}­÷*_§+‡sÏ¿-×jî•¢ƒW/ÓíÉ·¯Š[åÒvåp·õméÛ§'G“¥àÝÞ>#ó}Ôx¿¼/¦_—‚·Å­jåë›ýrëk47x:yT->€À§_âx§Ñµã%»ßNŸ·6=¯V6ž3n4¢™öa÷g³/§Ÿ½®}ã=’ÚÒüÞàâÞúà|xi9Ì:xÐñ¶wGwµ 'g«<˜˜\~ߘ<’_žU¼êÇ%ŸH`áËBµÔñèñt©›~ÎïU³îÆê`ÊyvÜ?',ÁZ\<­o¬÷×QÌÂôÙü—êÔáóÙ¢·°÷^>¼÷y'’EGt¾“6«S÷Ï–£“åéÙª¥T8ïºÈav—H¡Kp¨NïÃÒî>,/÷ãùú«bü^¾ÉïÁöjûu¦ï?V–g¤£¶$=i>/OL÷6ÓÀaž¦•ÒÆ»“Q³¥–N»•wñ!šeéÉ•`s˜rÛÕ‡Ûíùj%|/§Ÿ®|™™˜´ë‚UÕ«Ë•0.E3[¯‰áÌŠÕ×iÐÊÜ—³G¥¯ƒÆi©ÝyÝ-]<Ÿ6œ-?î¾Õû3ñ‡âÎáñøìÓýR4½qÌÜòaåðÁj³í3\~ývU³pàù§=¿ºt´øü—ÊApü¦øj²Þ϶»_ÜÙûÐN¾¶gˆ£YA£Øß»ëÞBédê¬U]XSo¿ó¨æâd‰Ã¬…wp¾<µøí•4¹_’§ñL¯¹;ô{òez~­»~7,‰óö‡âî—õ²ú5-~YZX-ÏB“/¯ lÜ/‰÷çûÅÝó½ÀþJàt©WrŸwË=Ÿsïžm•;ñÎáÑ”8øüªèß¿soiº,VÊKø¯§ÞúŠXôšOž‰ÇSågöÔ~11i[ÿ,!+,Ó‡ôg´»!_⯋üµ Œ°wV\˜©ÆáLeGî¿?[Æ& Ôÿ¬LLšé•°Ñ Û‚ãe»X2“j¾˜§&8›mš’Yn‘&21IËäã¤âíÕÍ=„ÍSgvê""=åü ô§éoWb¾¦oæñwZÆ3‹JjNÓc,vŽæ_ì>Í( ¹­ƒÝÏnÔˆ­½ÆFä¶AÂß ²lß´ ÂS‹´*û'ufF^1‡¥‘kyúý-á)Ó¿Lu{uT_L/„rÂ"õ¬hl¤¥™£y52ä-1gˆ¹%Íq'JÄÃ-ú£—A“Çÿ¼Ïª~Š{¯ŸwªKš{ éJW`¤ÅÈÊ£ê—ÍwOªwÍ‚Ãu%ZÚÏÜÍ¥r²ôæî«£rëó¾tl(‘‚eñ¼=êáKGÙØ[?Ÿ×î(áè0£ WAY|_Yï=ú’3|pAÓ¤ý£øl­÷ íÝ]zý`ªTi´7>ML’ìÊϿߨÃ¥¹W•Õ©ÎÝâów»Ì¯µQïåêîÒÂLüGe}ún˜1öÀ®DÃÔÊpÔ“sr¸tpTi>ZÞ µê•îT«ûïó +Ö>ì}+n¯=ü8Z—Øò~zwþÒ-É” +ûkŠ?Cmž˜Ì)Î?Emž˜Ì)δ4e,ˆÕ•Zgå ë'ÕeFàn”JOtï{ññÝ&à$Š£øiqOÖZ<Ù®@ƒÝÚˆJÕ…å³Æ²š½Ø²º¬]…]Åñ«OÍ稿ìïε¶?”¼õåoe^µ/ÿøp±©x)CñÍWßèc¯&O”VxiËÂÅSåhæá3&—þ·nñù›·÷àøL³h›˜T{ð¹´‰èO<ñü Q-7NßÍ+ât&ÒܪT‹•¦9Ý;j7Ÿogì°¦œÊ!£Â™èJ©}°v‚5ðŸ¬Ë™©\· ËvË­~+ªÎ|æywm›Ì)“ž½ˤþÙ[?©­Ê/ó¶ñwïIã´í!_]'Æuѹڋ—柿ûcåìã;f÷Ÿ ö¿4¶»Òø +4¶Ø“K¯î¤|@ß›û°8u.›¥Õ½û‡üƒ!ö¤=Û[E”f­ÅêR¯wü*x²ùæõ2ï=›ÿ‚ÆØbÞaZ^S~ƒów@Øg¥ÕRp^òÄãW}cì~š¾ùás1>c~Ø ¾Í¶ŠÊ:ß{ÔôÖWÿ˜lÏNÏ<9ŽíÈ“£Æ¾é‘'&-Iæý+òýÉÝòñÇ;OÀ°ÛßÏô=û¼t²9 ßöa݃ϥ¹xÇþ@r->YiÞyù¢Ôn–D¹uçÃ`©¥—ïÞqq¹~òí>íFúµ—V«Ÿ—T‹/×€Á¯¼œb:—Qñ¾âÎo×Kd-Ïmî|,bÏ>9ù SLQè–êüf;»äé]! ûþùMÛÕîáeN¯;8½+'“M8/@Šµ—Ú>¨ÏÛùÑyà´¹R«|ü¡ ç<Úü2ÜmÖÊ÷é‘jòôÛ)yŽî,}{Voêm|p^LÎŽØÔ:؃cqô®r8{'d|.®÷úÞÚ‡•ãÙY~²^;˜Õ¾‹ µ7½ºø1`õD{äêÇŽòÖnev§1³ôäÅIËJ,KwìJ½¿ºó¶j|X~ÜÝý`{BéjøÇå£ãê XâS$»ógï—æ¼#øÏûÖÄä“Gó­åƒ½ûß²Š ”ÉÞý{»Õý»÷wªûÛź©FOþhòö/ýÝI«ÐßrXjÇ/§I¹}!õ†ÅQ´0µÔ›IÏ‹/UK3Ošg¹AŸˆä΋êÃwÛОDÃü°1½øüE£Ò8MÛ‘a}SS &î¿;Ù\2?=8ž;n~þ¢»h:¿Âþ­ÜSÙø¼ô­$æùw—gîÌFù¥eÚžüä°øüù…Ö¢osór¨I{w²ºgqûÃúÇåú³ûaåùÚÝt{òku©¿¾ñ•Ú3LEåÖ÷"u Ÿ,ÃJgó´¡î'掗^¾*á&Ÿ¹Ú¡êjzåSü¤X1\ý´–ÕQÕÆ'­RmùsU¾*¾œ3é(ÁjÓ©Êz»§;š]™\ù¸_쬼ªeýQÜÊ}"ºÙÆRzü±Ø†C\Ù,îì-}s5o5³9P^7ãóå¥'o¾µâ7¾ß,î»C$'Óo¥ðaô¾ØY}ô äþBµÞwˆåébä«n±¹V0éÏ*y¶6š@¢»ÀšGKQ÷ÞÛÒËÉn419Óy¼gÔ)0¢ö^?;YZxzö¢øjîùÂòÁƒ0×ä5‚‡}”†EÃŽ•«w«åâ§#øÏÌ~µ²½)GÒ¼ÔÙ[…C3œ?cWjîÜ^>T>‚F¾ ;‹§Çv£€'?ß+‡ƒmí=m¹}¿Y @Bìô—§E‡®rÿ™9Û/}^z}o0È×/x˳½þþ“³`dÂþdý4²¨f…]¡ã…WÜ|hUﯶ“°7ÿ†nb曟·GÐK„âkmbXΣûÕr)}„ªÚ Ð늽åÚ—Sî†Ö—Î+Gw?¾ãcª¾\.½§ë§9‚o¾’õÊúæ«·ÀKWg€¦?¬ÐñQ§’”8YUûâæ½2Ýòí—Êú¢/—?o|ªîufm· «•ýgd\‚ x²®î½ÀTp @uÿò4ú’·ºõ³ôE¸¿[ÒÙUsy¯§;GÙ³ø•Õ ø×±Q°‹wOŠþàQ¥¸ÓÙ¬,î/ŽSéÈïårýÍR’?c^ÿñ{r*ƒ"óàí( ýQݼ{P®¬ß9ôÆŒ¾?ßßÅÓWÁr1|v¸Qz¾’:–Ó'Õ%{µ/ãÿ!(2_¼åzí}³ÜZ|–À”žÏº—U3wNLãG Bôg*kk¨ÿÌ•Ú•æÂç?ŠÛ/ßÃy­¨´é +½´x +ÊÁ›{Ê”P÷ï‹»g‡ÕJ[”<¹Ø,9+u\áâûèé“­0gÀ;4æˆmèûñQqooò³É$+ù‡W_Ñê|tŽÖ"ÊÒ×êþàî¡Õ”ì¬]ùB£™~ü_¯ž¹+ ){»'€§à°ž'ÕÏÝéòÁLq¼==k¼…U­]y¡N±»zwçiº^ãþ…íöð +ttV&+ÍÙÜÈh½ê±Ó­7þ[¤äªÑÁs}{îèãQi“@¹ö•bL¿÷ÏÏšd¡xë`„‚‰çµz:•tKíÙåvéë×í¥ÊZãå ²>5Uų¿¶ôzo­V-¶kD4–÷×îè‘×ÏI‹DmüÅדì؃3Ö)«{mËp{wK Ï‚Ç°/ûõj¹~*\Ö»~>PZ-}a.-y-OúÞ“ŸΫ_>ËSPèŸù— _ò1&½“h%LŸ>» ž«ûíÙ¹Ì(Lœ•þ£øÉçé]Róäå?ìøÀ>‚Z1x𠛊7ÏçŠÉJÀ;{µÎÂJ»(?i4F}†:Ì»n-‰Ÿ•W‘”þd.ÿ±´ðÒÿ d³ð‡¹e¶Lø)ngñbê5Ð7š¡*Éb}iub’ZFO¿=Z¯Ô˵“Ê£Ö\ýÉæ´ÿNå}c¸kþdX“áI3ÁÛïã½/w{h¤ÁAúì}‰¾y}ÐÆË_—f&ìÉíûÓ‹ÅåFâ­ø¦r|:ú,9Þ‰_½hŸ(!ë(µgÄÞÊûdïUq§»ñûƒQÔY5ªß8Ì›Íé«E¾œT§Öj Ñ«íƾW³^ÔLªê]ôÕ>C/܃g÷‘=ƒ¬ŒjKòËÍʧ¯³¥\ódkåkÑp˜÷•jòôýè¾£çÇ¥—Å{Q)èw¢8Þ;k°%žóFÃÆ{ –ëç€:QœC“™®»û/?Þ©YJŠ[ϽÁ€šσAn<ÓË>Ú/¥Þƒb·x8åšíçóóåjå€Û#»8Xš¾5B{ü6/òŸF/AËܬ>˜˜\™¬®®$[kË¡^¿ë%·œíÏÇÝ÷ÂrÆìÈ)hu¾¯ÖäËDúT@¾,mˆFu9ÙYµ>—…Ç畃êÔÞ©øMkï`ÿ3ïìô@ŒwSp!‚ñh\ˆ£+ìjé{PÆ~iõ­1é–.=õJkHu»Pt\”mrÔ£_×`È%9û~á3T6ædþ5]šC{Vë4íËQ¶‘‡ wÿZm5ò/ø†šuLîkuò„ðí|— éü[×ÆÆW~Œ"ý“RŸ ½'iØÛíÝ&"C[LSÉ̈*c¾Þ±Zç‡-ýy¼²O÷—; Tt8 Pˆý˜>òÓ¿MÐÏ𦟆_âég¯æ¼¹éÍ™égÇÿ%ƒ…—O|óÃKó/úaÞ¶7(UÓ•“Õ»;‹µÊ¡÷î©ùUN/îDÇwú«‹wfæìLLÞ™~z²pçá‹÷éÇÇ-øéËáìéó'»wo¾­Ü™ñ6¥7·ønŠ†ï”¾ ú²¿ “«œ϶¾<õK‰ŸDï£Ó÷ 3_ª]º}´¿z«ûÍòÄd¯÷tñ øøìÅúÒó´ÿ4Y]x3[í¾^/÷>¾÷*ï«ïöª‹ÅźxTŒ;jÿÞËéòÌ£o#·\^Ь¿‘ÞŒ 5Úè÷zóý=äñš7ìò2ìÌúIuðZ~îžÜ÷÷¼e»í}òú Ðwr>ýtåÎ}Z8íKådv®Ÿ¬É·ù¯ðçJ¾~WÉú±÷ióãËу®ÄŸÃ'kŸgGúùÞ‹‰ÉÜ°vШõ|erô  w¦zÿ?woºG¯+Š>A¿c˜›rÍE$4S™! ¦/a†³ÏþsžýH²Ë–«\ÕÕtû¬{÷Ú+«‘]²-˲$Kö¿u7úN| Gü…iÓhkÜ4{71ûz®¢Ñè×äÑÄþš»ÑpêÛÌ3ÿ§{¤#ëak|t÷úï¶k¬ÞúÊÖóŠFãѱËÛ©ÅŠF÷¼õÞ|4RÚªnvcøÙñêpÇÙèƦ¿SI^ÿËÒÎjxñxÍžÓÏÀÉß–¶±Ù©ò¬Nî_ÿŠ)h4¼*±Ò¶·¢}71Qh4Š.¯M£†“e³‡·ß]îT4úü(NÖÇ„³ÑƒåªÝl“O¿¹ÇúløûÝ诳îFßw¦–nÆ.¶]Î<ý»°d…y±YizùËó·îFÃý}o=óÞ8Yÿ™Œ}¸ðߺm{ëß¿¯WŒ5?¿|Ý©jôÈÛ˜ýñÉÝè†÷bâl2Ù§F[ãEßÎ.«F÷ç& ^~.*ò®};_·ýòÌÛNæ6:]h´5~7²yv8šó¡ÙäºÈ¿Û_¿U4&ç§ëU®z¯§¿gÔ(òXa¬/ožÿ¹}óÁÙèǧAe£[gÏÕ(Jþðëœ÷ñpjÄ5Ö»‘­Í³7_¿LM:ý4qù³²Ñ¿Þÿ8¦F[ãå±~Ýð>m]?s7ºŒï®?¶ènôúÕˆ«QÉØì§Ã‘û +}ë}^ÞZu7úziíðàý÷ïÎF¿¿=IâþRëŸýèt½¢Ño©÷ýîÑ7®/ÞfiPhZ¡f_Î]VøvüãÌpE£û»ÞÚï‹WÎFÓ7s#ÃϿσfnŠ‹æá09TS…E3õeûhšõ'–&7í‘n{?¦^`£³¦Qh›´nr¡¿x[lôîêÉ¢jô~y¦0ÒáoG_§d£+ûâ¥-goïv—†¡h¶]–J/çh¬Ðhç¾$ +7GbÙè²Øš+ÂÙëðÜi‚±…Î5Š­¨fÇnoŽ/±Q¯Ðèí틳«œ_ ½‹þ,¨fyá}»@Þ‘«³;-¥?œÆv§6wŸ\-T–î¿ŸªJy›?ÆLiIòƒè]}Rñ5ÌÁ“™NÞ¯?gI¡4NÅáŽ*½;OK«2¾ü5²ç*—BñÝË…o•¥©?ýáGu鯣ƒ M±rù ÿÉ—“ÊÒí™Ëe¿ºôøí?OMibñèÛ‘“ãÍŠ¯ÓÍ'›Ïvïdé?7YáÛݙ߹rúY(Ql÷øÕÇKW¹”r«3ç·•¥ŸÇ燫K¿­.,æs”ÿ?HÆ*KÿÜ¿»Þ¬,=ÿ䯼7¥%Š]œ¯/T} ]zÿ4ª,}å‡K{Õ?¹<þ¸]õõ“á'/¿MW–®½x}|VYúÊ_Õ{1ìM-T”F›ÞÚât>æ…ɧ…Ò™÷w˪´Ó~V\•›;?6¦_˜òð.œý`[`ïtâkGÉŸã±ôZ™£ë‡‘=+Wwoä/KŽù÷chav†ç^eû`aþÙÁÚ[ž]ýÐÁ>“ý¦­7)#T{GVrÉw;âO,¾›SòìK[ &àÃW´6ÐÒa+bþuz9vìÞˆÖ‘'ÐÞ?Ït{£ó¿§@:¬Ý>ühÏYâöv¤5nš%K§¢Ñx휯îFÃý/•Â&òGô1>V²t*Å-浪ÑSÞhøldÖlúfå#kôôÉ“QÓ(iÿºÑ @^ÔþõH7þZNí“õÊ›µ¼èW6JÚE£`‚ö`…±Xcý^Ù(ø.¬nµÿÊF[ã¨ÿÿrõÙp»®Ñí‰ÊFI§0âÚ·šEbÇšÕ³,ož~©‰˜ê|9½hRïàáâ’¯ýŠšñèÍÁÚÞÛ®õ¢_Šï”´X1{¡¤eiéþ˜¼@ê|`š¥õ‡[%\ž||^Xñó¿GfÍ?Kד§;ÊXG™$íý¥ë©g×ã$¡XÊ6ÇþP/–²¯« ¸ŽÖ°åÀˆ&ÕüÒÚ¸úgöõ•j€T༭[B`p‹×²RÁûboeþ×Ùê¸þçט•Ïì­®¼Š­lðáþxµ¬|JÐåÝwx‚ +íÃtN©·+qÌFðî‰M@íqƒ{¯¢ñqúò³m%¹:už½,tŠwéðaX÷õ¬?±ì1?ZÁH$iùýae«ÑñÕyiÌ;Æ71\9¾Ö¸!ýS9ƒùü}é>£z|ÓŠÇ\#Dåu·†XÍçïŠøSZ ‰UƒLL¯üY¯DÕjÂî9±žÎõÄY†¯¬YrÖÆ·›Þ)ï¤;(7_'÷MyåKöªžî­jbÙ¢gª,zÖlÑTŠžV7Ö]ûöò–PwÙ"à)zÜ´;ÀË}rWôgvmVþ£h'.Ý´ÛŸ®ÛU«’üqΡíô>4¾‹Ñà‚åO;¯»‘zcë‰:Mpvdu¦0*¾‹±Q½k?Q³_£kÐʧ«žä’0À;`Îí1ÚêeÈùüÇÔèÜÞjÅ4Í^‹£±‰Mêƒôó?’0^õ.ýëjf1çÆÉäÔq#M‘¹PÝÙ)ÏtyÝ­7]wõ«öÊ£‘½‡Ú¹³»bÿù:Ã)Jl’qrmË5@1>¡øL¨qD—yƧ¥`™7Þ¾×]bý¢±Üo8{¶´q|W5ÈçÞÙýî|I +O‰±ø^Ï”§ät£›ÖTôœ§w­ñÊ©Îh*ƒ6óõ¾r[j÷2¿H“ÍÞµ'×Ú?Ý°å´ƒX¯¯ÌÆ[Ó¥ûåW•{åÛª?MT:èRµB—O]k¼Ù ‚•_«XtÓù­¼¯U,z˜¿w•ää>}¼lÐ/¼<® 2{Céµ_ù ¯B¶_oÄô2H®ÕõM±ÃÛRÌ–h=öëÚöÃüHnK†ëýÒÝ#µcëüú³é2«Zçí¯ìýî"ǘ‚í^•÷Ë#[•+{7c½Êò<¶4“›xd²mhƒ–xïÔÙyÒÔøкe‘:Kwz[îêFp+†›¡à\/ݧié®ÞÄ+tDqr¹+]¤@£ŽtËÇѤ˲g‘6òLÅõs]×_{°É塤d¤c{üïwÇmæІ_/‘ç×zh Â©šàpã äe½ÚÑï¥S£¶´Ä%÷ý~¼~[n*^ÐÔÞ¯ßD’·šÝÒºû3Šš¼ñõýy‰‡zŸÁT«÷bíû݆‘ V,œÓð©°É÷MùSXÕ«ù¯4{ÈJbõ¸Ä-bé%N_cÙù¼½À_áXì%î´º»{—€kGšyHê| ¯Š.Ùboê\²ÉÅ+[ƒ¯0Š»»dahl!©ÙïÙMò +ã^7uoh+©Ø•Wö¶ü÷ðÁO¢H•Òl5Ÿ«ÂžZá÷i×yé.^ááî~_~$Kw__ATøk:q#NfrÓò4\[ªôó‡¢* |PíLT2¹2TÞ*ªÒ½ÐNêcÔŸñúþ4õ/Ë»ÉD#˜-ÖÓF¸UЄ·^ÞOÕ­;Ÿo™ÍOï/ðF_o÷½ªµÆk8xÇÞô#–wÇ£V—9oF˜Ã.NúVa««Òe:…®Û>ÄuY9¶ÕáaÕßÀQ¬ 3T®:ÆÉèO»7ŸwyÌ{]»}*ÚøUù[]ÞViYTù[Y‹Ïm/Á¢i÷}HA³V¿åµã± ÉÞz£ÎÄ%ž~W Ä¢­Iƒ%—ɽàé}ßÓö¾™uBØÛ&jŸNv1;ikœ +SÈ C‡‚O[ãÝ—aƒí‘ùížÏø*”ŒåœnDJ¦~.ÜX»XQ¢ÝO•$ÚÝyñ¯DszH:@˜ç7ýJ´]#Ñrëµ ¾ˆ¬»Dk5àé]¢•|Oÿ ±ôöJxjNƒfW&õ)Î|kÜ¥ X›Ð§«: šºu”pöƒo 6OÉ7Ž°~`óý埉›j1ÔP3ëÀ:{ïZÌÒß›˜dŸ†{²­ñJ1ûÈè{1"[ڲģ À¬‚šª$Lw<ÍTûJ,ä‡<ý‡@– +ƒºpòÞ}èÞнì8M dƒðU˽Æ7ƒÚ…½t®ýž1éÑȇ»¾-¾½ÏM½YÌwQ¹~îI¿¯÷Á"²þõûo7®½°×] g­w—VyC<ýè÷ ‹Þ ¿‹‡~ï¢­× +rèê3è¬lÁ>rèê3èj²{Ê¡«Ï £lÁäÐÕ×S¹Õ}çЕ®•A§-‹>sèê3èHëžCg$×d˜½›XwiÛÕ1ðÕ™@…`—]rzzW»nO7Ë—Z/ØȆ]-j¿ç@SOï×U{·ïmêØßj1`ãÑtz:m±«+ª÷øÉ&É`gÎ>us¢Øg|uÈêã°š|}Ý2ç¯à¹*¯—æDŸï½K™]ß²™¥—ú·ß²>_®€¢Áùˆ½#;Püðþó£vuo60ö´Å×ÅØû¹ÙØØ«BPŒQ|ŠnW£À<ë[5š¥Ê=j†½îûK®Ò¸—¦¸uw2´j]Ò˜•Vaĕؾ.G®Õ%͹Á‘' ¨þÊ“VɼªÑ#îJ¡ü“«÷ HÎNFª8ùÏËÞÜ;SIjJ]äJR§:#êÞ¹›?B{ÙtÅ›õ^©ýyÙ›{§:Í®Aôh:ýêâihL‰jèÑ#cuÉÊG†Nù½¸?jºd/æî»X ¦]ªNïžW×¥âÝí‚ÿW¦$ Â#óŠ<2}F©&<ÖÀ#£Îøº¤çŒ-´Ÿôã‘aöþ«þ=2€`²&º£‡4´ÇxdJÙ‚ý{d0 ­à‘©ÊHí–Ÿöä‘qúù_5HÏi–œƒ)z°àºsr³ð=|ªÃ³•å-£,WÇ7Q–ƒåÝ™ÑÌPŸ“x½5˜Hœ—…÷ƒqêÀÐ&»Äô6IC{d;ß_0¬Ú»ÔCúX)tÁ™-Ø-¯®ç Ùr„*åÕ5Œ×«Í«c©£5™ÂÝ [µ—T±ÕËtËÚP½wå»HÖp£ËsÞ«““‡­ "¦®>®÷ûÇ“犂~w1è|¸>ò,zȇ«‹P\>z­û\ òá7‚V&q=>Ί†Ê¿™t>\ñÞQ™7è|¸®· $Î:±jjù¸|¸¢-Vu¬ƒ™lýgÖã7°˜H@eÇDVé–Mb"?]5Š‰ì¶öï΃~Õ€HôŸNxf»Í~C=¾úeíöù‹ÍxçUg¥}Òé¬Ìoá#¯óíhü¯Ýeåq²s±\Ygh%©¥¯Õygéû…wœ½ìd·§'o_sç3k4ú5=þløª*Ã.Üß«Kv;¯lÔ[ßY©Ë°ûûõQU£?jݘËX£Å\¬…™;æ…+&»…{¿¶õË„…°‘ɺd7Ñ.4j¿Ç÷ô¢*Ã.½‰¾U%»}­Ë:»¨Ï°»ÚÙ©lôÉöÙ¯ÓªFÏêßã{¿WÝèÚ›¯•ä» žV5úÞΰ£Y…«FM¿Ÿ/4¬÷ÔUOz¬šá·íFéw²žÚ:·c‡"ú&·’–Öþ7Ô:ßqƒ[[ƒ¹”c±ÎˆÞ=¹LjO÷բפú«›]bçb5êœÚ-ÎCZ¯ƒ|IÎõìˆãN•Þ¥º—äzòÂ}]m"Ù%’î„ì!®¯Û#rµq}Íù©Ë#r•ãs½ûÖí±‘¦ãëþÖ@c¢w{g¤p—ZïÇ5_/gÕU†»6}€®ÞÓû˜lºÇùazͦsÙù[œƒË¦kx?LŸÙt.Ÿ`q½ôŸMçÊ¥{læcu6Ëï'›Î"‹Z³ÎûÇúʦs¡êrCÈ#²é¹#÷˜Mç:§Ñ{åÀ²é\¹tÜÞL6+—®alOÙt._{þûà²é\³KžÞfÓ¹”;:˜l:W.]Åmó}dÓ•»ôk´ZSzl6K9m:›Î5Žh¨>³éŠ¨º¾)ü¨lº*Ýr°ÙtÍ)ÖO6]UñL|@Ùt¢XÏÙtµ9V˦sçV:›Î…Zp6ë´¤?€l:—x°­×AdÓu9P6]÷ýeÙt.bm|PÙtÝ2¹“MçÊ¥«|¯p΀Öûb=]ÝTýŠWù‘ÊâÛ Ÿnº,v‹øáUÕ[6¯®Ô¥{<åô{uÕÚE3:ÝÏLôD'C%+s2»* YÀJP¨Èz®êT¡KMEAƒ×å샩Gu )êéÙåº.¹)*%L jž]®™¸‹ÙѲxuW°ˆ‚±ìºËÃæõž9ýZVÝCw}?sÇî¹j¨’?æ™»Ê×å¶å*5|æ®*“«Y"]£‰úødóÐ]sµ±Ýä•™Z/U£gîºz‘0}?s§t˜ú‡îú~æNæ¾uyè®ÙáÑõÖî¹ +–wEý$6ϳØXˆÒõ–Q¹ËC[œjÊŸ•§<ø@]×ØÃnü‰ith<×zà%Ò5‰hnuÏ0¬âhœ_­Ô„öD†Ô©týÙœÜì…-hyt¢˜O„—¦VŽÙu7TÕyÏÆ»‹G&vYÑPÀT]âÁGCª®ñc£¡Y³ˆîúÀf’}É|ìÿ(_¬¹\Í~C<Þvuaa‘]± K)“Ý™)Ü=—5J¬­z¢˜Xûñ²œXûñrp·"²ÒÅL¼ru­%^C}vw>Ì}óqïšDÕöà^0„‚wÕGÄ×þnOUW¾áÃk}«K]TgxpK€ÄÓïkÕK¾]7ö«ºÛ ‘¡§Þ©Ç2üÔ ‘¡‘¾ïîòüʪ7î± Á}·º·îê2ûX†9£©îòR^×|¢f/åõ›O¤_ÊëÖ¼pgç%5ÉyÌ wuï½âw½¿p×ôVsÌ~ê?±öÛQsª¬×Ɖµ€¬»šÓj¢èÈ·é—X[Ì|Œúõa~ákg:to÷\žGÈÍÒˆg ùœ¯½f¶XW<Õ¹°…$¦V·k&è¹¼^òÛ]ILüTÄli.Lv™ˆ~­üÝ·†iLM’˜&Š–(ÛÅš¤1Ùƒìâ;5¨ô¼T¦1-L6ÊP¯5Ûõª\˜l²*›$1M,^•ïÇÚ•{=%1UEáÛ‰5;moŠážÊáíãVþ‚bè¸,qo_¾1ˆ×N»úV1´_z}‘®§ç‰”ÕúØþ஦ڧӷå¸îW^MÕ»s|×Ós]^Ê›dŽ+[Ÿ: £AÖs÷WèRã¬çºWå‰ØNÜíåªf¼m.‹ÎÊü«Ï«£g¯>¶ÆW—g>î,^ý˜‰á×Æ;Êî[ßû¶~êO,¬Ê‡\»Ìw¬~YIxo–?ðF­|¸ÖøÝè§ýwÜUe¿Ã¶Ðùº_‘7U„wûðcAØ2¹†çMWåþÅ£cɇ©ïUIxß*…±Œ¬_•cõ6F¿ìV6:ñò¸ý³ê¶)Ó¨Îä2~ù¬Q;5ín"ybÞ +YŽS“Ë_ÿºm#‹oÎYixGÅÜ?NÞ­gï*GÇÃ7«?Œ¶˜wXÓèÆX\ÝèÆûÑ/®FéÝ·tÓJ­,6ú¡îÁ­OÕ®­í®[‘]Ðì–Ïê_* óazqÞžýŠzAÇ«¬Çó+½Ó /`œY¸º_3Û$Œy?,*ú°Fy{ìͳSã'Ö]ðI j4E§AÖ+fÝ'ìÍ€{±˜ØÔÕCR’T­ªìrru§:MR›º'6Ý êerèvy™¥©'iµ>´ªrêJ/³@{ A«Ëº+Ü<ÙGVZ—MI¥V3~*zõ:>íµÆ¼»ž_Y¬èR)J«¸^½§(­Öx]§ªýĽu ¤euœ–êO)6¶ ®Ö¾-MÛ‡×kå7SgJî— ®¾îâƒ=XÔåûsÃÍn8lTë^nzÊs°ÖÏM´&'±teÜ£’ÝÜGÖ=Ýk}°öHϲ}‹&fö{ ­ë®¸Ê|±nVKù®¸Èí³ƒFG¬Gë=½wP-aÖf#­%ÌcO½eÂ]Õ:¦«îÏ×Y=+úù»»ìè=¾J¯JxS + 8Oo{×G*Þ~šXÜ«>ì-‘¬ôžEϲÕg2õ”c•õtÏëR!ÿåt£ÎwÜU·ºÄBõÕi5[µ=¦ž5Håä÷\Õ¤v{Ò¸Ëü™“÷Óî‰:™¡ð®±ó•™u{Eg¢˜][ƒ¬°«ôG±®¹<ÍiŸ\öI±®ïéôD±íð[²Rº°K;T{d`ÓÀÆ÷T%ˆ5ʬ‰NoØ4°ìOî% °i ¿á°÷,À¦9€äµ~t Å*5f1¶·,À¦9€H±ÇgzÖË ;Ǫ×,À¦9€ÚF~T`E—J9€ü¼Rö§<ªþåkþ"[?ò±±ü‹ò•¼ +ÿÊ£|Ý^dÌ£|2g¤^AéÿQ¾Ö¸C+ø£|å÷‘ÿGùªßGä£|Þ¯ìçQ>îUÀNÅ•R$ê–\ýª_ÿwC½ÄÝP ßõkt7Tßïú±¡ àn¨ªwýz‹Szì»~õ¯ú=ên(Ç»~õ^!·%Þû»~n櫽êïúuçäA¼ëW¢=W}¾ë×-“k@ÝÚWýZã=9t+ßõ«ZÁwñèwý¬Ž”^õ{Ä ÎwýêäŠP}Ì»~Îtĺ[Í›¦q¦¬ó.èG¼ëWó—>ïó]¿Ú®C»ØÞõ«w ÷tUÍ»~õ¦®3 +úïú¹Ò$ÍÒëï=¾ä¼÷ô®_=ôóâ]¿ú•Vc<ý$¾3Rû®ŸCÕd¯úñ×¼>Þõ+,CÏ~ÕϺ³«çôO¿ë×5ïu ïúÕ¿ê§9¹Ïwýê®YFj_ïú™ü-/î¹zô»~õ$ vïúÕ¬µ»ó`@ïñM×ciü_—çä¿Ç××»~‹sùôšÍQzׯ&ï¡k´íY¯ïúÕkòxãÁ Þõ«ÚÃ?•O—¿Õ@ÏO»-ÃnïúÕŸ7ÛQ×ϦvÑZ|ì{|½iT¿Ç7€å£_õëÿ=¾&ILÝno(Çjôö_1#µ¿wý,ÇvéU?™1Ôÿ»~:Uªî]Ѿßõ«Ws*x¬çwýê_õëûÖ&õ®_Ÿ¾¾†ïú5È{À»~õ¯úõüߣ²p Ò²ü®ßã“áÙ«~¬•¾Þõ+»’ù«~U·œõú®_}xWa{ô»~õNŸÒ£ßõsÌ {Õ¯þ^¸æïú=ÞoiS¬÷œ§ +»²wýŒàr½ê÷èèÁ»~µÁ^éï]¿zÅPZ¯ý¿ëWŸ«vä¾ßõ³Y|Õ¯¤5¾ÉÊ~ׯZ ’:L—›¬¾ë×H‡éû]?>“圚ìÚŠõé~ׯ~s¨ÍâXB¬Ü÷çÛ×:íí®;â£x¯Ô/y£ba¯hµk·>ð¾¶hqSÎcæôfiñšçÁKß”jjÒÚ!ÛW†0ÞüÛ³XÓóe‰nßaî›÷k¶•WÃ3_ߌyó{牬„ùTÃc¯—^¶½ÉËáù“¯×apûëÕúÒßïÙûÝéɹ‹©‰•›oó監7+ñð·£Ó~ýx>¥O7Ÿ¼~{ó!º9ßÿÚO¢vx’¾^ßkoµo‡£íðÇʇóÝåÓ¯ãÏ¿vÎÞD7Ÿ&Ïþûüb*\ý3²óaë|áï³½«³åÉË›Ãäfüöùeüføó›å‰±`r#ÿçëË—S¿F¿†WßÎÔÚ§ÜΥݟ†Û3χýŸ7ŸfžϽõ•­oýŸ“WÞFüäÃíílòö.ýüònô >¾‹Ž¿¾Ó™–;3‹ósŸÛÁv6’§½ý™¿½ûv…Íe£2·º,C¬üÒ§_¶³'ï¯Ñ ÖO@â{’?Äøô‹çÛ¯\Ä"bÀpïŸà¼cÕc¥ŒÆÉ£‰³w£Ÿ¾ì,]Þί?ñߣO‡O1“ôUž:ü0{¾Áxµ×—”“8üb}ggÜ;;%{ÿÝ•­²_– í]‡%Í+ˆ5–âñÎCk|õËúÙ= Ùù¾ña/~Ó9y>ÿìÕóù»Ó§ëÙèù»µÏσ ÛåÒÆßO{ß^lÆÃÇ€çÓMŽV>Ÿ9)fÎLeÜ?xþt{ø–Fõââ +9ùÅÖçχ3kŸö6ñ øÝß6&íÎJ³~)Ú¿%ËÉ›¿ŸÄh„Ã{)‰ç£öDþKL’ï(’\ŸÉ4eý‚Àyy .Î’„€]ìë-:íüÃõùõ5ÿu +h7½™ÕÙñ‡õÉÍ—/Åìį‹µ“ó›Ì›_J¦¨£Ø¹ïbº#žv~Í (XœaGcÅÖx^Ô™3EþÊ—Ýå¼à¥no‹·77öòWN˜7S 2®ÎÅÆöÈ‘†Í˜Ê­q±qÐ>ÑEs ÏÆõ‚‡°y¥Eo<Ì2º›ñ¶ú÷ñÁ¸Æý~FV9¾¨¡¿Ÿc>Ø1bií޷e¥“¸ƒxÞ{v+N6ßÒŸ +íÉ—¯BÍÕþúÌüëç”îÌR©?/œæî´óV¼©±Ôóÿ~_›Í¦€;¼OcÜÞÿ1©Žå$3”üGÊåç%³;ðÙqãÍ1z £˜½{6³ûô6^Ú ß¼H¾ŸNâª[ñ§¾|&VAŠíõõÄÿÆž=û±~8õuËŸz8=χþyÖL˜¿º¹ˆ<ý¹-I´úå¥À|Ì_=y©_w}õëáëÒ-ýµ©£CõËÿ!ŠH¢8üò1~ic¾Æ•øwÛ“_>|P¸Líinûâ³Þüxzú‡ +ˆb_B½*ÙX~|îàNóÃ>¬ümÿœ~ñîäŸíÕ­—à ч9íxÂ|æ$q%xy–÷çû< #xâï†ÔÛàÉÓ¯‘úµzô[× T½O/Š½yÛÙù¾¾úwääŇo­ñµã¹çŸq³~Fñljíß_Çr-Úªƒ}üÄ’›Z¬•åf¾‹¡ä,ÈÍ…ïo¯&×>¯¼~µ2õ÷/)ˆ7WÖ¼öNYnŸž ƒXKéŠÚ)}Æâ_ cï|FÎêÊÞÁ7>Òžÿ=òîI0–]Ü{ÞdûNÉ1–NþVø3œ¢4g±öí9þ¹0#`çXBAˆ6r>ñßà›O(R6Û¤×aèM›<(„ÐÓ°9_áSÚô³[“ûzƒyÞå|B·¦¥ ™íŒkج‚í +äé-àäÓfçÇù‹SKªn{^t¼…±%¸4\¹‰IOþùe4°E[ ýÛ ¥gmtf%Lî/e$ÖŸ•¿#—ëbsn] Ž{sùʶ÷Ó¤ÿñÓ·þÇuƨèN1»´½‡KÍ,~\;õNÇ/WQ»Xæo ++£x[J/µ¯NøS+ *Á“Àh?=äó);º™V°ó‘XÑÇ‘f×S‹îWxþB£¦»6ÌÃÙÏ?OãϬCA{ŽVSù®‰\„éw¢µªùÝh™3Oÿî¾AÉÿöl‹öí«7&ͼì _ͯIåîveÿk®×:x<öá0_¸Q|­-‚œbÃD±lX3¼ûúé4œ_2’šþpëö€œσëíð9 ÂyC‚Ö8ö眽!E×5H"¬Þ}ÔDøƉððõ­&AáÊ“Y “-"ÏßON*"-¿-¦#T£Ã|JêH 3ëé âþ•©-M„Íïí‘?» ø@ÆãI&NÞž+ø‚HÒR¿Õ^ÅòJ Ùoÿç{7+Õ! \Ñ¥éÜX݇£‘™ú>ÀXj{Á™áQà-(§Øã†[Ðù|ål˜>Ô¬J>áéI30 ESJLRVcyáü’Ð]7Ó…gØ' %júPCÊIÃØÞæêÇc¤˜ºlæÒ®9%oQ’çì¬è‰ßÿ^˜SïÑ<Æ¥îãx ¶„¶æ±G¡¨_ñxŒSâQ0¿åT¿òwŠO§EJ©]4†™Ðž†Á¼ +SU‚«1)íÈëßÞ+þÝbºßõ9MqŠ/+)Q߇ûJ˺^p¾| GLWæë¾9ö¡é¦\Y--§ý•}ñ’ýùý~á•ù3[èlɃ™³?gód¤rã]ás¯²/ÃséÜ°_fNڛóo§fÉ¿€Ç:Úñ´ÙÎòÛ1³ò)žâìþyJÚ¡öá0‹ïhìâ›ô€L,îG2ï­îC2çroå·;üS;à6'ô/0Öþâ! ^²)í]<øŸ•’vÚmÕMÓ‹¶6ǃåO×+¹ÙNjgâw^f÷Ú¹·5à ¶g´ÛpkÎò(Î$Ú³eÚûŒ=ýù"oï­0„Q.ŠÅiãÞQ°ªYåE VÌùÞ·ik¿ûóý,ïßïž܇Êmñn¨l¶BÊ9žÓ¤8)pòÛ[ä±woçqeÍB7cðç.¡–.‘ùw_활™Y}6Š¥3Ð@ìÓýmãºý‘·òh™[?zödáçåÄnk|ýÙZø§d¸oì½çÇïù¼¶ü3èãeÇ…Ñ…OÆ)uÁ˜ßtI§^¥ï³tcýùÄÙÎêËÓÅaåãØß9«ìp‡ßÔßÃÑÖ¸òí…zèGÐÊ}¦&ìøí9ä•u~¼;¯œ¼Ç{ž8Þ~9¿¾ +å:>ð—²—`uÎù¯ÓPzàgÕÔž ŒstDîåN¹ó¯^þëÀpÛ±?±¼½¤zs~°‚ƒáàPù%_ߌ뱜_Äd>+i>=;˜ú§ó+^»ò7M™ã…$žÁË™dÄvYàÍßïµu£'$ù‘Yî<±!žO¯#‘ÿ:5.à`|÷ÂRo¢ûOëÓSKO“Os#¯×¼Ϥ3ôÉá]â½Ú˜‰híÕZÜóFÉZ®÷öîÔëÁ?Ø»wÐu__7ÿ`ïÞA¤X¯þÁÞ½ƒ’“{ó’wðÿ,µÂ0Άâ(I†æ?<ü=»}{ûûçïË¡ÙÖÓÖü‹—Bì^ž^­ßžíœýïûÕ«“‡‹³Ëû¡…¡ù;/_¦ÑêÙÉÕéÙ1E?-.æTg_ó£lËÓHNò•Õ²óÍÑ‹G«ÿxûKl9HåzçfxöןM¹‘ŽÏž~žõ×?OnÞÍâŸûÒ #7Rç*I£éÑÕ³Û•‡µ™×ÛŸ §äÉ~ìlÀ¶°öyåìÃóù»ŸÏæ7_|ÙZý²þéãóù{ïŠöbuŽiE °ÓnÇá8è÷O†§ž½ ‡çæ'>á!ù*Œel5žzþ¡¯†gÞ¼Áñ½Á‚åá¹Áéðìë½Íáé›Ñ[t2¦\D<ñgÔR3[ÜúáaΊï®H¼¶ä¹±˜ýùs‰Ž ”œÄeG'ù²›§?1”qRýúsFJÁ”Ò&nðé˜ÍõçÂ$z§6çr§æ?Û¡BñÏîÂ}¾‚_zæÔ‘ËïŽ0Ífk¼òÓô­ÈãßÞö¿ P8žRþ=õ 5^¿F=&TcŒT@ÁÑ +¿§ ‚“+ÿžzcÑ +¿§°õròï©­q£ ÕƒúHü e¡jsPâúû:Ì>n{óŸÖ@(~ÞžzóeRÉ͇…w(RÁÀšZÞžúýwixîòý7”¥î cE³JNØâ¾u°h^slÚf~¼V•Žnæò€pÒ|è Ò PZÎ(iùîÉ—–j Ÿð'ž§iGiÙiÏ’êxörþ÷Þ}[*¿gfè¸äØÚ+W²9Ø[o¶ +³»ÐJrkD«”cßÕ(ÈUÒ@¤bÏÈv.õËèòŒî¼…)ä×kFŠg*OñÅÆîdªµ¥ +mü ‘bod8hlÏçW_=»òÏ[O +O&±7v}šøg†eóèðU› +ÂÜì¾Ò¨.&fèsÿÂSª2mí Ògï n-iÁÏñãÓt«tþx?y§Q¼+Ÿ?No´ÃÍù)çUþd“uP:÷š”NfûuP¯Œ­æ('­+ó XßÛt²«Ô²N'U}þ÷Òî)ÈhKÄç9ìÓMÛ¿ÿýörÆXËYj¾:û†N¿¸“€‹~Å(éÏú°púeœT½ûûÈY×è°îT\k"p¯I‘à 'ôÆNî?O¯{:-žj¯iÝ`lNA5ª¢ühJ`ñžÂ½ÕQð#‚—ÞÝ78U¡…ÎÓp™PÜÇqºL…” ¹±„‚skÖ’SFAQõ3 +ÜÏ—Q¬çaì.Ì÷t"]^•³Ùýt?<¦ôL¢XSJ å=×mGÏÞÁ׿"åÉ1böºã9‡Ñ”:)ÁÝÈÆæÊŒ~£5>ýõ6¸a5ÁÜŸR5×ÿˆÇcŠÏ'úä1PP&ûã±í°mTòX-Šƒç^¿<6ñk²?ó.§*x¬1Ší™Ù® ÂÐ l;HÚýÌØó‹ó.ÀcMQllt_®n*|¾Æ²Ï݇ƒ¶›”Ø +0é: `v]ñu}Ààåë…þfµÚzRÒþR7ŒÍ¹õÞÖF©ñ¶0ÁeZ›Ëü.}¸]šlGÞ ïۻϊãƒñþö€ãŸ33:´°®5}¸ÕLUèƒZûÅ^œŒ%s}qÄÉÜbÛ’IË.;GwœK¼²eiy²ür™ÿ¹ùö9ÿóýî ` Va)£¾>»=¹IV—§wÒÕ/ë/¶×_,ÞJÏUÄp˜½@=öÑŽi¯N³¨O² Š Í¢>ÉBfÒ,´ µ¿#LcKS+[ó¼èºóSSðÿ£ÓõÍ4‡íNê†7³¼àg¨‘ß`xZV(‹dx=Šäþ7B™ÞÚÿoPÀjCʾWÆ:°ý‚²Of_Sê× +r22ßûyòl€´¦BÏÇ{…¸ˆÐÌ3¹yx«£§æBäÐ9~Ä·üüi}¸¾Ú_ì€ýÒ¶üý©Íõg='Œ—€Æ÷Û‹æ|"º7v¶ñýKúóÅLJ±ßk_NßáÓŽ0üë8x®ÖÃþA —!Œ…%W,'šY>Ï© ‹÷”EŸçóóúÏ€ûø;©ŸEîúLç +xþð9пB‰`íéE,Ó#ÆÚø/úÈÿ‹§ ž\áèì…//X=Ëý’¢|,_âÜCÒ[†Œù§˜Ïò½m žL½E¢~÷èY°Ïè±ø.ô/Ÿ×|ý3DXèèÍûõŸö;«3éÔúúÚëOi¼³ã…ù³Mž éƒý"3&ú{÷ÊkÂ{óöîl=Â?Ø»w°”ÑÄ?Ø»w°5Þ›ðêÿ,µž¶Æñ¬òpíò”ŸS¶ÆÇòñìþá+D‡+g?_ný÷ÙmK Éÿxðü_?Š†’ ÿߊ¸}Üš\9:9ÿy{õpy:Ô¹ú{u;5´}Ù?œq{¿úûäþ÷ÕåÑí- hïõöîËÕ¡…!öÑ¡úèéÐ$tÏ;„ϠΞ–B—[ÞÐ øïÞAo^\ïUøïÛ–7äén‰¡ÉÏ¿~ߟMÑß{ç-Oõ>ûoøãüø ÿÞÐ롯߽¡SÀ¶÷¡•ùQ; £`(Š²v”ˆ`袅`A’„Yz>+Û.—%‘€2ßÏâÌOS?‚:g]ÃùOM‡ü$¨—dêÿ\½)Õqô*Œ5vÛsàúÇôq.l‡'òÂÄÙr%GDZjG·RWæBÎÈ5Å)T±³cºÐÑ¡¹ ‰u©á24¼é4òì»›Ö…®¦ãH#4èœ@††7-Dà<ζM©«ñ,Ô( B'ãá­û©"’«q]ènۚʹЇšB†Ö"D(ð«°‚ºÔÕáûzx £Ê0ý‚Ãk§±% ö6ˆ2XS@??Nbè#Ð2öS¬©HRø ~dq/ÊByž}_Ä @௱D"MÂã§>t3KSáy^äM~XÑÐÆb­,&¾q"È2è^b‰.á Õg&_ Ð! šÚBK\°C^™ÖÂÅ2idÐ9NeŠ]BÊ4ã ¦Ô!z âÚB·`båL2±Þ0Y Lì0 n¨%œ‰#†™ e²†apC-Äà\1Ô\h000 ‡jËVÀCÍDƒÕ`IšX וZ†wŒI>d&8˜IŽ¤lI¯Džïe©—a{ nµã$‹aUF [Â0 âÉR/q"$óD Ò-‹2"q®ñ@€x ?<->ü LÚa$|K|dqÊ|K|h CB4µ…–ø0`—øЭ¹Ä‡.d’ sâÃ;ŇnÆ)>t©CBĵ…nñÁʹø0½áâÃ@™ `ÜPK|083ÊÃà†ZâƒÁ-ñaP[âÀ™ `8ÜP[|°.> j.>xƒ%a5\Wj‰Þ1.>عø``&'8’ +pA|Diàƒj%išŠ†0 £´ á¥"‰ƒFj†*QŒÊH˜zÐsø2Ê‘R“@ ¯€~’€‚Ax¨Ã€*…‚)DPœež—xAÂkcPƒÈù0B'õA ‹`ʘÐõ¨,% Ð%W4šÚB[èh°Cè˜ÖÒÂrù¢Ñ9n¡£‹]BÇ4ã¦Ô%W4âÚ +¡cÊ™Ða½a"ƒA¹x1ÜP[è8: 3 ÊÅ‹Áà†ÚBÇÀ¹Ða¨¹Ä``.^ 7´ tL: 5Vƒe±Â®+µ…ë:|ÈL^p0—. I¸ t„‚Lña¶³( +}´w’8Š3ø¬ PT’Š8û(@C)Fõ¤ ë6ˆü,LÑJýF1¾Vþ»‚ÿj€ú m“‰²P0@‡1hj -cÀ.£[sÉ]ÈĉAç:eŒ)vÊÝŒSÆèR‡1ˆk Ý2†•sczÃeŒ2iÂ0¸¡–Œap.c f.c ”I†Á µd ƒ[2Æ ¶dŒ3iÂp¸¡¶Œa\ÆÔ\ÆðKRÄj¸®Ô’1¼c\Æ°!sÃÀL˜p$à¢]”^šÄ±ðc5èA‰}4‹ W(b¤NÅQˆž/„>#Ôœ ªØ6&OŒŠàÅE …jŒç‘Æq–f¤ œŠ²À£=: +0Õ@¯ñÔkâÌøb@9*ØM ã…iÙyb€.w‹FS[hûb4ØeLéÖ\Æ”.änÎ tûbt±Ó˜ÒÍ8)]êr·hĵ…¾SÎ)ÓnL(÷º n¨í‹1pnLÌܘ2Pîu1ÜPÛcà–1eP[Æ”s¯‹Áá†|1¦€S57¦xƒeo o¸®ÔöÅ°ŽqcŠ ™S Ì. IØ’9Û+òTlíò”ÎÄæ溜’yx<&ü”ËØ1Ù‹Ûûÿºº=ot:¦ëv?{€ÿL?àß×t.öŸánrçáH[ê0í±³•€iÁ”‚ÒBÛÏ84†MìK´=ñÿÄX¯mÞ0Á10QÝ&Pšy`¢´ ÔÚq @ÌoP´c0‚+€aÜβ8&XØöbl׆üxöA°4_S½ ¡ŽÂq‚Øè±€YÃÁŽæ“Vâþ(ð ,ñ`€qœ˜†´c)— üPâ)•möFŸmп›ŠezŸÐ¡Æ*# ‰—ÑLz˜q€ÁZ‘_Âø’Ö$¯ üà‡„5H£€°g¢ &ž~ÄK,‚íšA`HÀ }™Cü‘"mE(‚!̆H¨É(óg<Þ­Z I¤"d4 ÛtÆ3Hå, †:üÒLïʶÚJDZA–\a@ DD0Ê0”˜³ú* Oæ-üv üˆFƒ‹G¨a0 @¸ˆd=´zHæWA '& O%µÊ`>pbd G¨˜Óó‘Ð]/”œèþ3„cÂ…ûQ¡….¶ R*eÀm Q³ÎK˜'ñQ¾…ÉE¬ã¢¤!'Ôfª9¯½ˆ#Á(#31Tî߉½CAìk¾4ó†#ðßšM €—yrÞa;É°¨ãŠÅ!¤¡ëQêËe>¬;q‚3˜ Ô\7ØFâ³¾#«F v>Ä](à @¢H g}ØVÒ e2–@PŠD+¢Ð@àšôá!Âd1RÚ2©uŸ„°âI}„;0ª‰|ðòS XLÁ2Ƥ!¬=ÓÝÒ¨xïËãÌõ]Ö%¸%0Ž"M±A2xè×6>z ƒ©Æƒ1ÈLœnŽOpzMæ[Œ "• +†Ë@Øøs˜î—Feú^ »>¹wÚŠ‡&§†ö>›¨ÊÿPˆdPspSŽQQ pª H&,ª&qJ’© 9 ¸…”YR²ï,…]PD± q1ôþ b‚DjkM@ÄíVC:Ô6©i›Ê’Xõ«P‚6g5ŽÔGöAKv0É«v†®‘8=Ïk5,!>PÔªå©5‘’¥‡H‹û«aŒ1¬4²g0Cgó­–ÏÛ¶c.ÑÏât‰xCó//ï¥dèäê⚢Œï~]Ÿ ]\ž1‡È4©Øë¡ŠãŠô22QÀ …’Á@»a…j7ŒÃ‹Pè(H¨z‚õexå°m‚‘L¢ùåTUŽ‡¶/‡ÜFhàQ˜œ%¤aÊk Pω‰Ì•¥´7jX Â01™—iŠ^ êwÔ»9@êÚ1ÀY%”Ç,3AWY¡Ž.­eݹíƒIQ‘éªC#†ž“90`¼H*ç>5˜Ó³Du4ÒdÎé +Ò T’±†ŒB&3AúÈåy4—3ZJp#˜BYúj ?ˆÕS,Š%ªGŸ·¥Øì—G¥“¶¹ „s +ÿ{ÁA@ê%†ùÜXÐÓèáш=×Î#Zà¦R +B*“ ³D`Pî 0×Ì:ÓËÈf•“-:^‚¦‹œl‘!ÄK¨E”\¾ôF™ÜÊK¤WKø9³ÈÚ¸+™U;BÇš£DRD:€ì"ØÚa%¢#ˆäoh•$hê—z¥ +NZfº.Ð$9"lic BOH?HN3ì1n£HŸœ²°Œ}ôåêë…O‘YýåùÌËÌÜ£>âŠ-2íÙ°%@ÓA$EsèKïìFiæs|) +þHnh^úÁ§îc1Î%Ž4²múÒ| ƒº í °}ç„óá}è7p”°V‰*ÃÏÇ[ñ`EÁÉ84AôuZÌCåf›ÆZkH°0Æn¥1Š&a ÿ‹ú…QÆky°û Êã6‰[Ýè"~@ à‘g ýĪ)Î!©SV +Y`øú&`´!b°R +é]ó•1ãINR‰ŸJ ÐD'L¦v÷XÊßBgæfa/H惩yƒ€‚{i¦œ +†N%`-t,à–…U‰@z—¢dxF,('ºEf`èJ"úHHÑÈ2áËMAà¡™$„¡v€«z»%¿$ij@“°ÃZò¥ÑÅ –'Dº~Àj¬Vˆò­xÔhP?¾Ó0`q m‰˜Ò(â£)ŽùdpZ­ÛLêÜqDþY<} +|º¤d°5•`°”—9Ë+¦xøœ5(V¼áBQ[–A÷u+ï©©nÄPd(•b©ï‚½YõÌ(ÿ‘ àJAÒµ]!”»B€wÁöÞñh@(E]5µ à ‹¦ÈÈ3bÁ|ê ÂÒÌ€$ ŠÌ‡’o¶9‘Ÿ%¹|е’\ÑטÁp«.˜ .„°cK}Cõ=$.]ÈI „€ˆ¢Ac‰ÑäF=_·…’£'xû9L‘ É÷>„™ÑY¢AQ4Kê•DñB,tAOŸî¼™ö|€%FÐRÈ`DlHNM $î±Ðw¦‚tØT[µ$Á8&IV±H-{µMÎR†FßùÊy†^£€\zbæZÃÐÑEʹFDZ\pfS‹Ùæ‡Ödç0ÆÊ"^A:l²íZY ——ý™‘¾ƒ¾Í8W*UçA;É”®˜C84L+ǤéYà„Î`P)¥ýÀd‘†éÕ +ªh6¿½âº†­' 3K$Y )‘ &Hâû6B:X¤©%š…ÅZ\Ä·%“’‚ T0yÈ”¨z™%ws›'ÓK7G¤—wÞ“ E"EÀ¥•œ ISK2•fÕ&âÚrOB»'¥9Õ#1<‘¶Ä%ZPiÄZNY)¦LséRN.‰Ìà¼2$²jqédhmÄ•"Q&Š/³ºÐ˜ <Û b¹Ä°C©L/rÝ€š ³@R†~ÎEáy ’¦–0*¯#^[®9 ’•?Ï,Q–DèSãr˜Ìù8å£Ì!œ&¦)˜cÒĶye@’l·•)׿ + ûψÃÃ;Úªr¹f¤÷FžŒ§Ñ0Ôq20 + e‘Ôz"¦!Y(OH ,B­ÂW‡ˆxÞë%ä%ò•&FjHÊz²¡N¤³vŠÃA…DQ_ÃÄ8LƒbŸ#t€²«!'t.ax-°%Ò8eߣŽ¶´R‰6‘BS("CY~äŠÍHW"°> Óä"ïsL6© Ï%DAa¨„cûÆW§KÔ©0/°Ú`‹ú0÷Ø +è2 ‡À€âH¨ï,”ÇAÏRÚ‹Uø4¬- 8!>‰³(0u°÷™5°¸Ð³ÁÆ +|à‘ÙY~!ö ,t:KÃO06‹ìOúÓqŒÎù8ÇeSAà©ãÚŒæf)T=ÍaAŒ‡˜Rƒ}7Œñ!|ë£Ð+À@ÆêHYÑŠÒHSX}ˆ‘s8$X…®uÃägÆ0+4M“CHi<ò±Iå<”<à±*)†Lh*1ú3x%ÐßÐÜFÅDÄè¦ò$—¥iL®e\ë†J¥—¤±`|¤CÙ´b Œ n$iÈ °Y ^ ýýaBãD‚±U^ ¾S0`ž ½ëI£0bëÜ°ù[«½Ô]S;m§ N #HÞ”Cy)  `j¶‹S¢Ã»ÖL’~m‡~”ض ‹â´¹C¶¯ŒI ¯€ôº4 ä„Ly*«HbÉÿ©*ô­×#É2/1srA0!RŸÍªž„äGBþ‹$ õ}y>Ž!:^j‰%²ž¤F#²@r ÅyPQ UÄÉÐÁª!2VÁK|«ì÷i ˜€Ôp¾<ü30fÕ}Ë'H»#µœîÓdútˆ‹C€åºËqÆ’+hã0ÛÃúÿlÈð>HCɯ¡Bò‚ÁP½ñ—}äûER"7%tšÁ¾t€0;²@©:ËAˆÜCS4\ÐqIn™„A:Ô %IX¬ú @ ¿SñѰǽÔ$gØñ8•ó–t<¡’•ÉÐ4S $ŠƒIMÚƒ@å8R +ÉÒ0=rr»…¾dÈh¾tÀòYÙvÌÔ ²¼\B…Ì—vƒ'Ð\†?M¥†$BŠÑÐŒ6JÉ7åÅ2œEö †1ü¸\O%K%Ù€ÄIhÉ%¦ *"#c> ]†‰E¡RRÀ’K 1»'- B2’ ^I˜K­,ÈdpK¤QÁ1AlÕ + *Ή8,J™"k'ˆÉ†#óbI6¼†’zbmSÐ넲¿ÈÍĈ\œ +í4ÄÆ.š$#î‰iJ(?‡´¼Lås)Ê*Ž26 «….¬HbO1¾É‚P{Gu%”ÑA9}9µv ]Á@OŸM ÆbD¾=}˜]&-oS à0SªElÀòB×BȲèPx<Ñ´%2Ô#.º‰` Š $”¬~{>Q”`—b<ŠÖ™â&eàªæv<&£` ‹(*Èïµµ/bõêôS +ôÑÍ*ˆ¤D¡“ºvŒq 0 ûhÉ'*e²Ó2D‚5ÆQª("I‰"-Šeš¡vt¨•™O +_ÇùÔ™Zrªe‡ +3¯ƒ©°9µÃ¤`ºÐæ= c. +@õ C_ÖŠƒ(¡`ª2©h<¾ÚuB¨@£Ž0lÓ–Seû„€ £W`_ÕUB€ÖÊË…æ“ñ0ñ±Ób° G±ŒÞ!õä\¦a*ãŠ)QACþõËà VË—á}(@)ÔÈ0.Ð#¦Ò0ÔlÌ4H1—Õ4äsÊ0K#§£!2n,Ù„×JÀ<*:hpƒÁ¸‘ +©òRêX‡‚€¬Œ*¾EeÅ)¸@H*™˜f@°'PvF …!›æòÄë*Cg\‹‚"31f)SK—’)@,É$ô+_ε§Ö%Ad8Tj ÅԎЩ.×e”ÙŒSOõ½†Å”À‰ó‹±Èä0)ž +y,È ä„ú+?àµô¨0SIs¢=ÒÇQar+Æ´é…tAqÌäù0»² YK&–D¹éa’A˜\™ø2ÇÆätô¢þÓŠ00Ü/SJÍÆ`Í,QQ˜Ê„>Jm˜b®A/Î}Bû´ÝÜO˜ð§$_K{Œ. ½ êÄÕÀ0” +ÝJ€ ÄbÂG]¤‰”†ý/Ž¥ùIW’Hú„$Ò{Oôð#}"ÅOÿšÓLÂ"¹¨·[òËPy¼)%MP$°t:’MBs+YM™¨¬å·ªå“×…6ó|H ¼Jò+ dÊ:5Ð) v€±S”y(:#éóÐ~ÁaÕFyÙ ØCeÊ£b$?,HAR™aÃdb1O$0Jˆ`™g™_¨0‘šœCTŒæJ¨„1Óyù£ÄUù D‘á¤Å`hg2^b σL{Ihõ!‡)2Pïs˜¡!:äE‡0ÅZQG¥.X0™Ÿ!çÐt^Ïs>À7h©cæ{ÊG(Â2k¾ˆÍ·úÍ·‚tø|óZr¾9¦ ö ÄšoõÝ6åÕS8’Á%2™åDŒÅd6ߢO`Ø‹Á„ +[`ñœ‚X}P0>ßö—™_€H:ظ$µ¬ZQGåÏ8l›²^b­tË/Ñcñˆ!ƒåÄ2˜¬)äÜ0àÀ© À=ÃJ”/YL»µˆ ™”å{“I6,VÑÔq’[þR” DË77™ä1K(!,-VŠ xHoà"ɆÉì°XÚ¬ïQî²Ð2WCØÜh˜^±9&³ªóö¸<(Â☠5< ´i-rJYµˆš6$LK=°`2‚]Πä|xEF0òHÏ+“G6,³çZ‹=ך›õ\3”ϵU).à‘óÊÄ‘ “tESØí! ¢¯0 as­azµjLzEëö˜,(ÂØ\Û¦E@–•0%¥ö$5-H˜–¿ã0yHì§üKº¡ àã3CËie0ñ©±xá_~RÊÔ<&CÅ n±PC*‘Ș=TåSØ ðÀ.±¯0KØ·a¹Ë +¨èjCêƨ+_ÞÔ…þ‹$¡2Ÿ â##aÎ4 Œð0kã5Óc„zÜtŒ†xÒÓæ#ÍXä*µÂ™— 8£ÁC¹PgEc,_Þ¥R‰ÌnŽ}J÷+”m›2t ó“œ£¶ú¸@Ù:Ö‡.ìi ¯-Œy¹ÐÕF” ËQx1eÛÑ1í2r"³Ø#¼ Q×JÑ +!0Ó A0A K¼(ꈣPæÉÌby@)¾&»™Hû'Q–9 }ï2ËM»€•$„š’NË”âà £{a®Œ„:ÿÐrÍ^Μβ,¿F­È+˜ÞèáJÌÙJŠ<(s©Lóì6‡åœ7f¸¿´n:8]ùŠÙ exphé \N¡#_æÕ–Ê0€ÏG˺£ÔϤ 1Œ0Jý̘¾õSÆøQ\‚,ãB9žÊÔ•¡S9 +Sƒs›p +<§¨(Ã5J^ñ0wx˼xÝòápq>ôà ¹ÛG¡®%—GŒ ¤€/ÇG É…æa8ņªÌɾ–7’byÞ*¤Ï*Ud"Ô\sS5£úì]ÏùfðD²·ÐSé 1‹÷mÂGÐÇ×cKe ^]R*Á¨FÇW‰ŒEo«çq@†×ÞE6TY†ñ<~`Ê° +íÆÛÿR¢J×W¨¿eâMÄ“ÎuM!ï€Èò˜öÐôÁ¹JÌ¥W2¦ŠvÅˆÓ WwŠ­ùAj¯+ôLR»™o$nHÑz±—’¼Å#qŒÕ£Ý0ÃØÓu[2?ƒÊ¤ŽŠÈD, pu‰k­`$*ŸÎ2<-ÁÅ")·MA ^¤ÂÔµAÑ <“ñ¥¼Î†r€:P>lŸâV«ˆÅ¢dð5hL¢F6™€‘z }(Cp0@#B.ÆW³QmS§V¥<•ñcåò׺ûX’EId:a@¤–§xS˜I/-€(¢´øØDxW Þãu"÷ewBPø¡¡ !HÊ2$¿ïúHÈáCÒ-§Kð¾¡ÌWyÊèz¾Ü X™.©ˆdè˜s‰Å|å¯R¼4†‹Ð£€¡@)üŒ%©¬ÄÛ5ì¡Ãju ‹O˜œ½ RÖ|©lÛ”áùT³KxH +1½ +ãÌ`'iƒéß|&bl#(—È8N/7t|½Ï’€@¦•V«›ªßo\P4VŠQOe¾Iñv¿”öÀÉös -KùõxQX)ª\^€ƒ§²üúøÌ!r~ã½Y†çœøh ã й0x÷ŒP襔R¸•g4¥ŒFøÄZ¼…¯tKZÞÙòÐäm•’Ðí²l™gxîÂÏS<ðÎ|ÞûL즘¢Ádz‡×7Ò-iÂð"11<Ï<ã€O¼.Á³r¡ñÉ‘—á• +™%Žb©Û:V\F×e:×iFY^®uŠo†Œ™‹sàøoE†¦AàØZ ®/,o¬ÛDX-QÒÇ)g´LææÛ3Æb¼ŒÙÈs&Õ50Ö>ÅËž³0æcÌK|<èNMNZ¦ M²D•ü(­TtôŸV~à\‡˜“âMûß³BP^‘zµ5"JÔ±$A©Ž‹‚Ù?DJ<ót¬Ùª•n$BêQú>íI0 ï•‘N‘mi˜˜q§™V¬WÅR¼ë]]»†m §”Á+¨Ò ñD3_ºo( ôn ™U!'eqh!—E2V½¶Ì¡èä(kŠ€+ÛDÅ[¬ûŽÐêöÒÈUäÀŒÔ¦ü“Š²3)ãnPƒcA±Û(ÖÁŒPoú)Í)ùkJ"ù!Ä؇@N= +´tˆq,IC0\Ñ«GÝ8LÊDµÊÏ´ŒC¿e5£Ý0ÅkÌSrlm bàM-Ë¢ÉÒN”d\ê™’²i¦Ë²€Þ^0ý*µX’¦º¿œâýexC¨ã+Ê -æ%®~ê²Òèò–Ê©¤gnBšHYŒtÆtÊTËküÛG·n¥èßÒ'1ó2Ø aÏ༢Ж¹HwÒÁ{ÌW&9¾Šåçå–t‰‹ˆªL&o mUqßö;èÿ!> ¼íïJæÎad? ÍåÎa>Þ–oÜc‚ˆžrr¡yx±’`Va”©›ù †YRùuY9H€BÈÝ=ºtøáCªš÷t—J%3„R™¼ˆn…+•á-@tPê…)Q]î´4Hl›ÃÔøucšFeêvô‘AÝÉžB$ ŒáËבÜV0¯DÞ'(88/”»%(Bd('Ž"t0ÿ"J†O†UxïÆÌKR‘ Š² @bƒV); „1D¯´Ìh*wSh#:Á/užÍQfÊÕ…¥î”j°‘TÒW+Þ‹™àC”o»½M!F<êÐzâL&II’à9<Œ2ò\E¸–+ð±Ú8V9ž”à,oD­À®ãpd€+Æ\ZXüª8-cGµ# +Eæè­ž!ˆ UÙ5> r™¡D+î>Ð`U¯¬âÒ˜ª©,'"ý$•Šˆa!ß©ÀT½Ä“ðo&a.& É"¤EU)ÈA°¸9N9X#Vm¥aŠ0W<ìR¼*ø'ÒâÁ‰éÊa(âyO0¨yÎ÷{®R3vŽx*£)p÷„—–GᢧÜÄAþÐc ¾˜/¡Eë3Áwfa§¡é”KÀ"•™ê›ÁÏ|?Uuä 8^—–Xœ5¼64¿ª9L#IY_ÒebdošÈb 2Òñ6!Â9Ú£ÉKýÄËqÈ|«·:V°‚¬•Fì¬aÓÍÑN@Á8ÒXJ½ÅPT3â®ósRu5ùbjhþãýíïËŸC“ñjåïÙå黣û_ùST“[•UvþûúÌ~¯êš‚PAÌãÅ(9ƒLî…†¼0PÐm +ˆÔ²C™’b˜»¬#è–Ö=Ñ« &å ó è/j_Jðñ ö. 09 ¼2Îz¡ÃÉúÒ… ±þ®<ª‘¤«¡)QÆMѱ‘H<ý=ö xδPªQW ­IN +º\> %«ÐÎâ#¿ÂßaË[ ŽO¶F’YPâ`ÒŠïÉ:‚.£¦Æ}@n]ÅbL\O) *· …±Ï3½HS’ ë”kÀ~ŠV¦j$…F] à¡#•S#¼“È9ü“äJ£tÖ°h¥ñ}ÞÆGGÖô³X£<Öns"g¾ “„6ÀÜjå _ò=Ôö•r‡Ar“ 詬̓dÀ+¾ãÅ”¾’D²¸ˆ^6 Bᦠì|_ÖÁKJ5"ÜÖÒ¼‘¨Ø*‰—7äêŸé$ ËU¶0T!•&yûÁP™H®0ŒÙ@¹ƒV1Ž/¸Å©£…2¤N˜Á’@%@ZòtØ [ŒÑ +a]y1¾Y˜„~È“!¤é‰,550ÁÇ@“rr”Q¢C™Ü, %Í*ð1U(Ž¤–•%¬xjàÁlÈyH©Õ7éÑEM"Ì5’Âøœ5l¹ÚÁ£ß$òwOy)`íã£# ˜}D”Á¤ö“\mL€9µÑýw‡²J"UG¬éþ/‚òá»Eär\ȈH–‚Âf†,õ‡¬Bäuä…ÂÆ›’q+¼Z‰Ìû”¡‡Lø²8N‡ÊC SJì±Ôù4I\ØŒxðB Ü½bò誩þ?£‘È ÝÄjn¯âƒY¬2”}Ê +t¡öy«TåÖq Ì ÏJ(QaÀØjåÜx53‰èv’r)žÉEÂs!F5Ü@ð„Ê»$;• ~ *D ©¬_>”B'-¦¼a¬MwÖ%»ÈK%e«§veàÊ&^ÌÛz&í¯ïËÔÐdN¦X§qÇʘJAó#YîÉlg LÀƒR9#ŽxSu +š†³™ð Œ) Dû9LP*ÁP¹>÷¤‘Ä’¦…F(σŪXªÐE,ÆPôÔÏ¥«5Fg±¡QyBŠ1IwW¿¬âòÀj©/õô\ +áÔXñÒè8•ãÅÓ& å²gè­DªN"MÐ;¢X!)W@Ÿ è×U­Hó@„)mª +H£0Ìò­¯Ts1brŠU# Éå j?r‡G±“x+MŒ¯&ʺ-ÓUÃ"V¡|_=LERÙÃb…Ò»ÍFn4€ GCBîµ^,ª‰r‹”2)¨ðœ8ÓSá¤¹à…” Žb¤˜èå0Ó v¶0ßëc?aÞÅB1HfPÄrôa½O!ï9ú$7¼ ¡rÁö*rôÁ=8g1#Ž =¾|I§¥«{Vqypµ´Ï-ƒŒÌ˜@n«>ˆú\ž§Ê,ðÃ4•ÎEÐV"”+RE§&€íÀ" VW ¹“9[cÄ-"Í+d ŠYAÚ@¬8Å0CÀ/‹ƒ"z´¾Á +d±¯l»‡tv)3ƒøØ¢«˜‘ÈÕ憙_ÕC«8¦ãXo™]f€)x²™ŠH+HxélþÒoãÊ6ðÑó&ÓB ²™-†I£‘Ÿ#pÕ€}Cê€EìÊø!SŠA¤‡¾È‘Y…øP fËù‘3j{xŒ­†žª¬F»SÙ!”{™¶L>¦ra‘(®&0ÊKwÔ†Ó•Úÿoœ`ÀÑš;93í•)OM!ó89Ê”§ª„Òò~Ñõ&5涓U–•"›£Ã«Æ/´Ð™nàÕUa˜t¦÷®¢|ؤE–ÕGaa54%Á˜k#x5¦ob ì6Ã+Ö(¥’¦¹Zˆ H)S…ØS'|‘R =¼$*S0êTÍ{&_g…& +•5⪇‹Ý-Èàº.•Wê?˜dêRª•¦XÍFXÐý U”ZhõPMNÂZàCt9[ÈèR² ¬è!/vŒ¯vrÓ +D^êI}¬´; 93U\ØvðqQHœ…x^žzÂ…XžÀ s ™}S˼°$Ö9bǾÀ;UÚXøp\…š.Ä…ߦîN±ÂòpjhŒ“`ƒyŠðjZ.Ýàe&¥sNàê£ îè–c?d@L¡îˈcºÚ˼« +£I]EäË\Úb¡x"“Oü– +ñ1MÞÃÈ»àãÕK=û‘wUƒ0â <} …*Üv +Œ_ÇäùÀ¤ï±  .…h +•ÎYB[_R¾€» Å;.ñßÉA¡|~¯Óâ@Myg€¥aZägŸ»€lŠËÌmâ‰6PÞIÜ ãÊ°å¬Ùi fæm ¨˜ß4¡—JÍzûÿ[J/ÞÎéHi¢ 'É!¯ã@ï>bDŒÀ›`Z3z[‡Ž€è–C y5½†õ-Ÿݦ»OèjF7P`JMÉÛ.Àîç‹@šBq*/÷1ç: žÇ[\ñbv_½vL‡QJÁ3tƒ£†œP 'êQ¼V,/=•¼ïcH†wö§ä6b@¼â ¯²"ùV0£S5]uf-£[‚›¯ ^C©Ä˜âC¡8ž¯Îzýc}0Ð@N(ª5Âór^ Ä–—P 1î6Ÿ²/ƒ ºÑè7Ä–Ê€nâ×_$‘wÉÐSƦö_É BÉF‰TÅ3ÃŽkè…ŒØôÀ$S¡œÞ¨‰äµ¼„ +C†ðV4ºb€@°UÄ——®ÐXpƒÅ~j ΂§ž;ð†¿8ªrÎ 1·% +@_>¬Ç®.iCDÜÑsó!¦+x©úÊÅÁYïèPQ„ùÒ×ÈY^+&§$½0!/aQ¯&ù$ö3ôR&꥞DrÈ$ò½!ŒIa z¬îšäõûºd*Âç(_=MԤŘã!“سX¨ÅFòÕ À"+å80ß_¸Õ-ÒzÀ礩gøþŒÐ#=t«‡™ý±¯Æ™zô¦aL½¯Ai eSa 7>ù#"aSœ}Dyg râê¾®¯]é$<9(òE™ŠŸÕ‡u"âT¹þé¼Ùå Þ¡ ˆ‡I÷€ @Wê„tËŽÆx»Wª. Ïaø0%K°e-Á^á ÿ¬G9¨Ãûžåc‹V¦=C‚¯.†,—ÈÑ´£Šìq§å(ˇ¸í,ÌIRî‘&`™ú ³ ‘x—‘ šøøÐYŠÜ”â“àQ½Hzáu¦>TLd6#¨žq$\% ¿„ù7Ÿ´=a`¾¯Ë<¼ö†¾²K0¹D²†. Å5ß eD[¡+O¼Rô»o—ðAÛøð ß ®èŠ]ÌÇPEI: ñÞ³$Mò<94¨A¹ Ó<—0ÄØ!Xž¦F@7dÆ2ò o +‡ÂL(öµ Ñ–MñtJ –ã>!S×K<è oˆãË;W9Ýßù™=ŸhÄ¡B_èš‘ ‰PÙŽåq•Ê‹”)¢Ç72ðèE¡·ºV,´ÆÕ…êrfP Có8‡¬˜¤”ghPý¼¼ xM €þ‰$®Èƒ4­2¼œVm\D(‡‚/Ŧ1àzL­(ø¨ùäŠ2RToéuÖ“–«3©Žè“(Œ€Xö‚æ™aŒŽAXêO¹ŠKEM¨TD· <ÑÂ6¬•¡±* ŒÕŽð^!БSªZ2^PEë -ÕЃUh¾·Šñ]g&#u¢—#„Ø÷4z\°°S +Nqãªðqz|óÛÇ  +él)u}q˜HôåÁÙÅeâÐãíði³ +¼{Tûª¸4¸zÚÿOFMáJFœÙÐ@l¢û>Ã4•Ù4DïÒNË>ôòb%`æEîB|p%5èeVˆí,ŒL%|n4õBYIæ{« ©s@UbgSø4% XS¼³èAŠ£Ìt– å ¨êA™PôÈ´~ª³ÞU7½LQDºl‹UJƒí:'$5t)GF°‡ +]Ì”Æ kÁ9˜ۊa\0…ø&i^ óÅZ>•ÊA›3U^lD9¦cß”´ñ1·´Ö(ÇP,Çñ"’åI¹T'Ó,ËËOZÅ>â-îdµÁP¦«J‘\ :PQ7í #> c†§ÔÙr•âx»ÏŒœÀ¥rªjeRNƒ±OxΫ4@áà×Û_–‡RÒË0®7*ŠñîH +î@¯#"ÐÏTº<s)e…b1Þ#Ò}­Nô˜;ì¾ÇÐÛÝ£‡ÑÉç¥ÐÛƒÃbt±â"MPŽ€—¦]´‹Kì:¹î˜á”J½SPaëMÖˆ¹‚J!FGû¹ÒãUÔ¾UÐU",!•£JAŽÆ²×)>E’Ò]…’„’=Ó´Œ/Æ“8ÒP:¢éI€‡á …¥¹‚h¡XÌFï@àaUâ•»d—ðaTRÔh(x;y„×[‰\u 1ÿ.ˆ±ŸŸSWú!,/ŸUÂdžSc¥„4E4OÒº*`Wz°½T7%‡ ÔI 틯ÄYžh$…r/"÷U#©³°ˆb/³)wÖó¤$I4žò±ŠHXWʤs4•ÑA\À*•º\®Ru×YúJ< ¾ÕŽqe´a†@é$ïÔ…Œe "tK©Sp™b,“#¼ŒŽ!‹%è_Cùeöê^6%â$NÔNî¡Ìl¶ +ð­r T%ZòŠY tcêR’%ReÁÕ§lÅ3ø"6¼ /Wzìžð»ó5ÄüŸÌ6DëȪ°[¹•FH¹T™)¨† @¹QŽA»2ý‰$þ/uï²£Ù®¤‡=A½CÛƒ]æý2è; †s`H† iÉ6Ð%‚Ô€ÞÞü¾’ÁµÖŸUçtõ飽±7þüÈÅK ɸ”ù×- +-RÒSÙ¢8—öÜÆ^‘J¥{:ž¸†4÷²ø^*eÕpi`€k¹˜uP/]»%^)s-›Ïõm•}6í’xôë'T üŠ÷´1ÏüØQ ©À mcgóÓˆHNpØáL&Ør75ô¡<îÇ1õe:¼Ô6Øþ¿ªDÎyȗЗ˜™ðÎLE^rOG`ïË®¤¹¬•$3è”`yVº7Ó‡2hŸvÅO=}Èr¥Øµž†8„)ÄW-½§Ÿ=ý阈à ʼn 4æî ½8¹;Ò3C£fÔ”Y²ÃÍ*®PL)ÅÕW‰.õæÒCÁrN¦´Yoƒ^{èÓAÈ-”PüSÙL4´¬e³]Ù…´$á‚øaÙ—NÝÓ/4¹?èfšßí„ÉtôÀr(êðè>Ó"ô“Zx»%\à·}ít-†­¸y|´1³ý +ÔòöÔ‹[– ŽâùªZÃ$éSëîYгN­Wô5"g„¾¹Êo¦¢^E}g·H†"ÐX·ö>–BaSÇhƒ}Ö§‚¥gÅ#*ó§ÚØÖãþÔ¤Á ž´æùa—Y£¬ÝÞ¯Æ$8>=_²«—Ûùky‰ÖþÖ›pmùWÔü—½äJ·»ÜüÐÕ¯H¯¸åksšÒ™p»m²E>^jáš¼ŒcÖe§Wª›žUžªAâ^\«· ­‘·Fyn»K—„+1.EÞn¨îͺg¹uîKšO>”2À—¾ž“$@ë´¢Ý8#CÔYÔbðV¸K¯’Uøò6yo¤;“¡}ϵ¶|w&>$áfçEñå;Žåêj.ñÎ6±ÀQ~rËrú¡“YÚà?©J:[ÕÑÜ{²íì×c!–4 ´Žk¬!·xdA<›¤¥œ°"`È2-JÅ )ÁýÎY2¸ZÀSÊC5Ÿâ,!vg«‰ã€‚‹¯ Æ(×d¸Ân5=|]‹÷c¦æ¼lÙÕ¬êÒLOC$­âÞÑKò•PU48ÎâkÓc3‘\šmæÑÉ/ÇbÊç—È,·¨¡@ÛøÂøt$xñÛZ;™ß=ñÆ™®Å?2AíEÙåܳ0šQÑrž«zz[•m,F#Q´Òl7e¨ŠM¼ê¡øÇÓá2¹g¹uö§c2/†qO짷ɕþl(B÷Î뼶äÃmiªÐýýëôPÆ_²7¥Ë¥pñƒ·i*zQ{ðMõlšŒÕs© bÍu^”‹|xm‡*V-üÞ­3ýN˜‡JÚ]J­&ÓÙÊ{úÙï©?ß^ Å“Ê”m춡¹ o¯KðY{Þ1z +פ±O ÊÞŠ”“Séc—ÐăsŸI–ç_Š¼ï—æ\÷KG’-Š¸s¬3ÃÑ´3Évè5mO[¿&Ã×%F×ÔRTƒÞ‹ø¸;1(J„–$ä3ž“›W@NôOÅö‚þϽ~‰1Ð3}ÉܳÚ5Úî\À^¡ÔMÝWoB…ˆW†’ø•1+“¸^JÅKU7ÂûnÎsThßùt‡±)!,T‹˜2¢EÝÈ'Øs†¦eã@sìFÙ¥µ¸©„ÀUÞ@b(Ù3šN„–½ãN«VÍ69{H Æ;®«M>ã%;×r9ÖÂ9ÛcŠÑïL ƒÚbcÎèà¤:­ñP9£nH êÇ”mG¾Êó©@P°²“”…Æóû.æ=A‚›IATLýAÉ4 ‡”Λ|O³-]B{ÂïNÍE jãE‰£‰chhõ!ž„Ò3d&#Ä›@‚pOih Ñé3ü‚rfi\$ïJ»F~Œ0@¢{œSAdÞŸúmÔ€ŸJYtTijöƒ‰Õ…bñv•E(Š +Çœ%Î(›DXÞpì6˜¡Í«Ê@1F/{Ô%0ÖB±ôì””ÐC•ˆ ƒ¬©)¡¾«­i¬ß´E"²ØøÐè²›ìPà +QºÙ$Pë†Æ4wó>ybO&»±”âbN– À6 >Tn±›¿òñŶ)_ê¾H©2(1&¨ãôñÕ .cŸ‡,¯ÐI=¨3®Ø?P­ÉN¡îœj-8Î90Ôæ„îÁ§f )™úxäËbÒ%Ë*$9é¿hçÅhˆ/8¾˜ñSyP¸ðTè3 çÁLA<7ÈnÙE¨ ±vEØkÕ‘HªÒ5@‡ì.tfŠ ÁPéôyÎkÝá^mMTB¢ï˜¡lkò%†H•$Û¸['ˆ],iÌ&2–‚·<õÓΖ¯@^'sáüÙT8{Ò/'˜¾;º«1…%ÝÅqc)‚,Ü*NF±%§…=ĽƒYþ±ÂïYˆäòq}îèýšÝ:áo ÃA°'e‰cÆ$µå„o3“>f‹Ùí.–!ŽÓÉK‡¬K“h›-3ž7›˜ciMLKåu_1з{¹‹9ëÚý^¦™ír•õˆñ¦]b4#T"e»:äÂ.ž + ¸¿†ñaºÊÕ\[Ñ®UÌ׉pÚÐúIJèTgÈtä`8'ÐÉP ŠPr:²uÑÙgYl/[M$P&µ@ß“ÀZû‚8­¿|5G5ÇÜ¡+µ>0Òñ†¶tk@ÜðU1ÕÒKصÎQðLM®¶òÁ>Qw–Yšv­œ­Û¥í>Üúº#P¯¤É~0¹X»o†‘”¢¤1DXØØ#z´Ö«È5œ¦U{Ø3B–œÃ> 1Ú~(u'™Flpµu•º;tëøŸaôWr¥Ü4æyŒŠv°ò‚ؤ·J”¬€øS¤<Ð/PEÀär@° ºµ@Ämô‘Ì€Uv\Scñâ`5eBï¦ÍÓH´´©Ù߶*Væ¦iKÞ¿p¶ùãgßv‹7êý¢!OT7F™ª†iL1‹¹°ƒ¥ìX’q¥µØÄ@Ruk)=¤@É1ÞÊKgú +Ó4ˆÎuýʦÀybÝÜ˃B{MjssiÞq#½=][nSÎþžåúßÈ­8SlË_ÑWnÕÁ³”xÜþ#Á(¹Œ•0ýWýÐPå1õùJÅL½eï–éPiØàC2=%Wøvoi÷*¤WÎPã®"¯M)Ú³kj€5¼‹¤ÖY¸'×'K»h˜;¿ºwʤ^hr” Ãéç¸Ù–\RL¾¦± NxáÕGÞ‘ZIô*]уi£@¨¢H”lh¤¹{â#€¶v~•&}ÀÓ}™ið,ØbXìþš^À}àcéZhþN®)…ž-iÐÏoo׶›„{ŸaïÜÓún·â’pkþ´Ü: N¥:ÌBǶÔ@¼„÷'?*x.¬˜ÅTQ›rs&Âè:ð"÷‹,ð SB4YL5ÒÍÐZG\’™¾ºCU߇dh%Iò½x{«´Š¿5±À9¾óÏÖ÷NÉD:‹‡Mø±b2Øæ1¹ŒÉåÚ·¯‡à/©Ê0øo PÌæ<ØgžýŠë Áªv¾u÷ÀI˘ÇÐÐe&Z'ÁoóR[|Ì2†«¿ªGäÛÁ­›ÉÁŽ»„K:\IŽ£óË Vžè·º4 QÿÐLñÉÖ›fzê,²¸nÚq¡ØC=˜¹¡¨*¤´tœÒw —ô[O¿²Í‚Ëýì–Qh©ÔDŽ¸èF4Ï•cTœÂÌÓ㪖^¥ÃжÇEŠk%Ÿô]]˜J9‰Ž^ M®B.éc×èP÷›–-•àr)ÅeüòùíÖR˜øЖy̵§×ô+­x}•’©†Ü£¥½hæ5ýÚÍŸŒ†ŒØ`ι××샃ˆB°ÂRëœÂð¨eÔ¡ùî°‹õ Ī±'——ép¤4ªÙéG ÒÕ±¤0sàñr'Ú*á’^ ËÓZ{Uœödœ#v —6æÁø\q»—>f\2å¼Óï„z¨d,;¨·Wͼ¤ßºùÓјB"ü0-¦Ò|)¯'K‘È|Nα ºì*ÍÞ!†%î0Oi~\Ú ¸.½,¸?Ÿ0@^•kš‡B…o¯ +GÀ­1!·€x´ Þ$zÓ/=²iWzœ¥b‡M÷Ô¤kšíÊO¨¼E¸Õ쮫Înªtéqá€3²Ê‹Ý¡ÖðÜÇ)ræ °Š‹?¥3cÈ/jNÆê§Âr‚˜{Ã[¾–pI/0f/¯kÀ½B‰¹˜Î6Ž.!zoÝ5œ}dzõ¦*=Ô0DÊ:NÌýU/é·>~=!(&'ØŽ­£_Âöô¨®>¯ªyÙVB0‰P<›‡DEŒ6(—Ä%‚üþøRçú ¬^‰9 ë¬F/×D¸À Ó&â¡x¸tBð0]NlŸ¦0Rp‚R‹Œ§Þ]Óú\‹‡þEÃØSë.‰g¿¾&ü_ÒžŠÑÞ缩8¶ŸA¯"-×Kù±]õ}e\“ù2{zOÇÃQw¯jøäôØýõh ÒbBˆvÐ51ŒJÑiâ½lúŸò»ìKëøÎÖ¥´[×®‰WÊ\ÊεØ)|­{H?ºöÚsÅýN…ÿ˜¦|´ÄV<š,wˆ#ÞŒ°çZÊ`.8ÃîX¨Û§ì$K~‘Å#Xš)eÖ!犵HS^‚h‹ö”ˆ÷È’‹œ(|’¹I¤äžÜtÙpmŽ½SdøkÏŽÄ+QŽ‚‡ìâá +q'›FÝMw^S{žàh/®}v|Õ–NÁ’ËJmaˆ´b‰|ä­iõû8¨W1ƒ8K•#ÂØ é¨Boœ*¾»"®gžÒ=4¿ºËO¥Ã3ï؈¦"Û•$¸åF2_ûsM³”¸”:VJ9Ý–cv{.iG_¾ ï<ôÑÙ%ÙpëD¸[¹ì ‘÷ê3 I0†ß×;cueÄG~•<ȹ÷ÙÝU°¾âŒIÑôKz+ìõ5™Qpßt) ’ÞQ]Ú2$Nß®]°øµçGqô [‚™»÷4t`ŒW~,×t|É~¦]˜7CðKßé&-O~ˆ:]DBz¦ŠIåw×àj}fëwÈ×Tâ0ÅIgcÓ3nÄj“~˜D%ªvJgqîûàëÑOQýlKBØÚ#oÛK}è¼)~Lîqr¥³¿‡V©«+_Ñó_ðöªRq*çiÊËÛœçÅÄTÄéè~tK·÷IGÙ÷ëª +Oæ&¢¥ÝÒq—wãDz Å8#W>´¯¡å]Ã¥w·ô q*9/¢.m¼$Þ:øíeëñÁë9pÌÈS±yÊ/Ž9šN£!{Ñú K:ÓBž³ˆõª# ™zGLJïí)‹G`x泥±§z ÝÚê å /±äßžú{M»v>qÔ "™­áÚ̇,×ÎþddÄCI†R$ Å"$µÁˆJœAçùÊ'´xgêð~[Äèd`ÜØ Ë«ä–ç´ +ÿP…èêÔÚš­¢Â¡²^<$Cq+ù—UŒ#þ¶UÜ›‰˜l­j÷N^’¯„z¨‚™µ¼j&’!'¾èäOÇbÊÚvE2Ï(vڽܢŽÆ69¹s§,–u]+¹sF퉭ôg$!2-§5ýRƒPÓÛîÍĈô¶-t®=•3¸Ðê¡’“ÿéx…]Â%ýÖͯGcʃ©”y¸Ó,a=ðCŽšnqt(j‰Vå‚^e=${çû­pyíÇÁE߇Ùô!,›éK² P¼•š…^§¾+¥_8d€3˜KyC–Pzh´§œe±ˆÓ¥g¡NáU^Mø÷Ûžùø „³‘J›õ6ê ÌÖÆèH´ÅD¸Àòå¦ÁÁÏ–­¯0¸ßÃÝæ s!D}m;ˆÔtŽÆy¼©gN\&´;/³ž8`>ÎÓÁ­Äûóà›½<$Æ¿ñOð‰1¾—iÅMºxd´ØP7gb®c +¬7'XU[Ž#ÍZšÍ—5æž)mA¶Ö îæ­âl?ú¼ÄbÓ*å$Gÿ>¹‘r:…ät k•`¡ƒ L"¶íÂÌp®JGLӞν…¾Wli0u*´[¶ +Úí¿uó•Ü +–õõY.AʯÂeS$ÛÇ ¨C¶‡82Îü ~ËNÏ»8ÒŒ³ÍàrºÚŸ?- 8i¹€P]<ìGKsÖ÷™ið¶b w©9"¼½€µÌæ(Ø%H¬6¼… `£|E•N³ÕˆMÙg5H]¤ˆÖøÂäƒ`Íi€øe½å’sÛ·ÀƒN|µ¼ a÷çOଧüø Wtìó¯DšfðöÂH“] PM…@Æ»tï'òà†ñCîÒä<åÒ!`K-Uùþ8 ÈEHÆõç¥^G ÛÇ=@‹œ¢ÐÞtÑB0ÕvtjÁ"Žrxàg°lÛû+•ÖÂô˜½Ì•8/ÃpÛy§C§€µ,}ƒdÜQþ¤ €ØücÎÊÇÙ oŠž§UFvŒ ¼çðîü†ôf±ü™œ£‹|1;¸ —;ýt‚4Jã饵šmÇnøüB­–#&äî’å¾”Öàic –?„¡mHY'h§ãØ:`özLÇ…Ç×Ï \}y­\d3®–_7áM¤3îÅ?žúø¡›!βc±IeÂ÷ Ö". ¿ó¶ŠóÓg¹ë(=é.ÿÏÒ¸&4›²A¯M§°¹Ê¼ÞÒŬ«S»º I½·S0Žƒ"ùÄ,‹>(Ô¯*¹ ÷K;&8IR‹MW/P,Z$©ùš¯ÏMØ@)Þ›r€¼›#»û±'Åêìmžlþ´KÍp +Âtbº€v3´á·ùè´Bïvœù„„GiBj…NúKó>ä–³–¼êÀ§E¯·ðzy÷¶!;&»Ê.rFñ/ÆcB*tNH€!a‚vΟP,z·áÌ'+í„R¼ºAšp'YœÊ¬ÚÜlv…´· ²TÙà¢Þ*m“ø2OÞ³ 1„S…•ƒ•-p¯o(¨$ HpæêONv`ÊÈ€¥%0½ª£hb!WOFçø3ßÜdLiÑŸ|ìÀ”Ñ÷ ÷¦ xLó¸µ2,{BÇH-p-íUØf³NË<®Ø$ÈÜ–”ñ¬ŽžPÒ]n±±E¸K¾Z®P~hH¾4déîÞ«§·²™Ø*tó°R¶›¸˜ÎîÈbM»Ã†…mÂù„|GiÑŸìÀ„m FbˆÁ¬.دÆPíÜ™ ÜKz¶þšt†˜²¯=‡ÃÙ3ý€dE´WΙ¯–+”ýýËìOæ•sÉ­Ú>àí µ£§ ²Ùà¦Ü,m“÷œ¿‰wÝ´«øÞÅ]"ò´‹8 +¾ðz¶@JÚ•€rÏ*Àa!èJˆµ@R“k'<¿ñyöÄ¡‚¿ +LC:¨78d§/¹Tö«ž u/¤ÙZ`HÒjØTétAŸòñ¾mó5¸pB¾,gn º½Õ/£u¢fspãhIu%é~ZÔáåo‘³U`|päï2Ù“2„{ x¿—BSImCóõ4—zäÃÃÍX!¨ã 9øù©‚I_"£ÖuÔ•(uæÒãB>ejh+™\£ù½“tŒ—m{™á¶UD€kÏ/JRxðó8¢ã#è/ÉÉ É“ Ç{*#щD<ÛËC&Ü¿¢+ƒqŠuz«"?ƒvÊQcˆä9Aú œ}WŒ§[`¹e}Dmx9à·x7e眫R•½ÞŸzlUQne3ïT«n AÃP]V99EN€ØùPåøƒ\¹0B Û\ Ž};Q@´×<~i­ˆÊT8j>PëxíEVµgáðÙU½4¡¶Z6ô)r?î‡l6ˆà2×vœ½‚Ú¡‹óKQ>uÉ×rIv CÅ]—újíÎGÍ '(wyE^P”÷@í»‚:£²ô v +½ŠêŽî©q´Á ˜K»‚á ["ü¼ûĉåS×WßÔ}ØЧTŒˆÕG>>Þr94ÿvoÛUŠ: +!ì±ù! ^Ãì€a/‡FÇ^n|R¡@ ë’¤ƒbx)Ø«<-âcß³LBúq—*²Bˆ°$P‡$º }Ðw5…#_ó7Ë3Î’ŸŸ*ˆ€Ô¼aŒâ"ÐØÒ´! ÎÁ1®ÁÕÙ‰>æ1x“ÒÎrljÿÁ x+e~t_JÐú’®ò= á3•Ëlm¢qnóáòÌËKÜœc¥ÂÑ‹Þ"0JÕý‹LøCŠ£dDþR¥c²³žÜÏʸߵ<~ ýÎ/ç^æ'UT-”æ[Á"ˆ×ÞlYŠ´ï|씚**ÁüvAù8DA îÔÊ ÁëP_•€ðå“êl ¶Ò1y¯“ܪ7 I='[6¹Ïry|‘¤*Kòñ,¾‹›Ï'³ËQÞvm>† +ÐVBÊ@4p¢µAh"©RÆ›åkÐìQûóGoý"?Á/B|,>0ûí M¿cTP8›3?~ÂFÛЈœ†¸j·«€Ñ…“ d8©ì·€ËŒ¦#O]CiZÔ\C*”#ÜmxíNË—Nî—ñt¨âîÀ*ÏœQn”ûmqcY"-˜Âöº^•Úõ“#®»m¾„®ÛÒ6´+ÝØjÛ*l÷àÖÓ-˜îöÈ’' 03J6l` CTj\ó¹ör@† 3Ôk.)jÝnŒcÚÜät䃇VØ«°,lµl•µšíä?Q_)#Z]Øäaw‚ØÉUEo».èz± ßò0ÚU®÷Í×OXg\‰kúºAHö+„ÆI¦¼Ü?è]ªMÐӰ̦(6¤Ù6¡à=jˆp +R¥¸ˆ §ÏTr¢±ñ1Šv–Íô”(Œoÿ16iÜMÿÝwj">c– ëã'pÒÇÓÈýV=¥:e˜êd§þ!v-Œöµ@BÌ…û0Ñ‚PN»›ŒKÛà‡€CœyLó*.0>ˆÄç?6#ËüWhê2ôä|Avaž®Ë´Q(ɥ规T—Õ(©B ˆü¤Ç® EïMÝ‘1µŽ|MÙØ)x;–³F,¢‡€nÁ¶û} òÚ^ñ´^…½ÄP¢é£¶Xê^Gaë(M:'œÃÖ¢-20ĉªO3$%¨Bì[…Fp;òñZI¥BU÷1ë|¿7ã·ª!A'GE~WÔ(nb™©oTéì£P:‡ÏéYØ ¢ß”®¢ Mõ@”ïßEó)—"'Ö¦š8¼˜q©>&)-FÝ/J¢~vïØê†>ÅЪ•pæk4å“ +º¨~,Hû®Ÿ*Y|~Ú¼nÔÔö·µ*¤Ý×Ö­|¨,™ ¼ÞfOKzH…mßû7C“1þ©ä6û/´Ã¤ÈEÍãÖmƒ®Ê5võ®3ùæ@VeX/C¿‘Pq—Ö´PE*©Ù3–d’šKÌU‘ø¦ +±B˜»PÓ;A¾ùYGjzæ ñu>àˆ¨nŒÈ™õö ¸'Dß'ïß,ˆpcEUp<œ7Há‰B´Y:`ôÒ<šÛ|ˆë‘E`ë¤ÖF ;çd¢-bH¥ÁáØi²ÏïªVZrnRÝ›ÐtQi¾‚PoóÊ» µe|£pÖÅöö]Th¨qzlU0²vþ¥ÔŒjcEôpDÓ&©,Áá~˜FiQšþeDqª,]Äw5ȃªUó’¡2dÈ]RM¢VlïIM²å~¼¨në— ¤ÍŒ®wzbO¢[„yÆ+a…>¥¹ˆ{ä[Ýòc±íùxöõ7ë ÑD³d³NˆŽ/BDÚ¥Ö’h‘Ð, +Pð]Î +´TR…F%B\™ºœ´m‹®µQÝÇ^‹L¢,j]py1/ø{¢— o +Uaý æˆ=ZÖÍußä€EåZQSÒë§ „× +†R +\)µwéFFª³OŽä$X›“ˆq-ée;rP+ë-=Ó& ³îç\çßôã"`¨br‹ä&¬áx*EY;¡OâÅïg>p"Ûþê P± ëCÁ<âRRÒs¥öéÒóߪ{:d¨§¶Ž½èå)A5Uy7•YæÒÍT„ŠGÙ©¸A{Ù¾>q ƒnȇmÒ‘s6þ\—%³«gÎÝ#Ý{›Gp/äضAº+‡LÙÇOaòÏTEï½@µU Ýä/ ÌÒš+oP¡”÷Ç +…²¡Ïù©žL¾y cJ‹µnh6Å€²úñ1éø쇃•d>z» ¥Êç7 :1Ò5¥aÃ㎩¯élÊ'UæõAÓÛ L®E•”oùʪýÞ”T;]Ó5vo¯sÅ:¬a‡ Õô¿€ý2´sA?¶sA¡÷c.ùæ¦lJ“aWèœ ú)›E±Ç–Å®ˆ÷+1ú²saƒ;ùÐßliÿâ9-:›¢à1Γà-ʽ¼4%w“¯Ü¡Ú>µ ›’EwÈ~Œ»ßzövA–* ÜÔ[¥ckçÊïÖErØ=)ëmžfÀ¹ÊƒÄý†@o™a–§]Á25œKMT(†¨OO#Xú-_¹V,Ïœ-1 iáÁЗ7ËÒú¼(Ù,mAvÀ8ù.m±‚]«á#wp¥4Ãv_Oˆ41mÓî’/ÝJ«¶‚cxŽ–̱5ÝX3`wö:S¶NÒôÍÑ®`¿Ì„ɃÌL˜³ÞÌ„ÍÑÌL8ò•[aÅrÌs&l†o£Ïé(.Mö’4О œKÜ”6©us‘;hgÂåÛÞ¯ÐäR\ ÷|éVZµœ Õ¶$‰b‘ý8ÏËðÝÙY¢,po•vŽ¬)ÿ,úINÓ¿ý‡‹C‹ÅìòØ5âÛ´›@dš˜Fƒ›X5&Ÿð +f<–|‚S|znüxÊÄsI€«ìZÕW íwFEe Ðû<ãä möزó"¶%—q‹×ß?p¥‡PFÏ A4äqÈÀsçÈ/u¨ÿµ:ÜEQ«àE'œ™88™./N7.5tîøøö¯54å_©>'C‰3VEÐÒ0ÓGg¡—â< †Œ»A~ãü›à»5Cï鞉Çê4ÁáN±o™FŠŽÑí°¡@9¸’äòn€ÔxÈ‚«‡è|bár€¼f‚ KdËði>}Íãyq’J£Ú¥9—ßñÞó[–÷_Y™~m9üùkæZݯeÂûï˜Ö© IO½VÝ2Á”³ †šÐäQÜþ»– +%k¯tøñ¬c²f½Ãïc2çÒN1÷,ú8òHk'Oåài"0Ö4˜e ü(Ó¥ +½Í_àïOìáÕº‹5ÊŒæM12õ±¨VžŠ@!)ÂðV|ÃmåhI×GÎöÝ;²GxŒpî‚^eUÛh§1/‡?¼£º&´J–ºéccznâûÔ‘3ÀøVê¡õc»M†Ô¶Ï“W=ä0„§æ«O§•¼ ¥§¥.¥¹Ê&{¨YVFú¤31*صÁÁÕÿ×D1¸E{jâMò™*µ)›ç9Ò±ˆŸYpN}Õ@Ù°Ó#n˜ç^¦C«q*Ó6…hÇë£Tƒ¯„±ÝNÖÄTŸœ*KGΈ Þ&l°B‡˜üxƒc­¹P¶ßŒ­?Ä-OÒúNáש%ŒþǦÁÅ0 «k§‡‚ 5(œ«^ªA¡w,V€ƒ"¡œsþ ¡ÞzNê„YáÐ;g䊷< ~CVÞE÷{ªQí‚ìËÊÞ?„&y¨œ*F5k 6¸Ýξ—ü´óÃ䘸YÖEgÐ`p]ÂüZÑ™>Ω %vq[(u#Ìœ®ÒÕŸH7¡2£©÷OÕi:¿ýC¼¾ºxbEtÖUÉz¢ª3ÍÇfÙ~]‡¾3(N:w5÷P…gï®ÂÌ,ËÏhá¨!Ã+qmÖ ¯iñ²ò ŸÞ•õ#Ä峩ÇÞuk¯N9´ó}I¯Q³#îÁ nTDö¸ëërxê¾\Aó=#óÅkU¥»ì³Y¦„Gîº07.hý.6uPW Ýò’>ê$2üŠÔ¡×3gÓpbc¢xÑ‘Î~m§ð¤úþ8•hèûza©¹#,ó¹'ÂÝ?,òDžâ¥³Œ$»Qèùx endstream endobj 365 0 obj <>stream +h˜Âªú`H?¥hZ~øp”¸1[·Ag#W¦/÷^ýèÝ,etÛK“z28ßÁ5F/;šµÑ,îIm‰{TwÝvüËX6¾œ3eô*Ï7fë^¨iå*ÑôæÖïWJÓûXøx‹ûk:Õ¸² 6åP¡¼…MJÎæY…¾ÈèU‚c¢RG¨”èm‹c' +š-ßó@HñqР!Nƒ—<ðàk’–rÑ[Ç)k$ÅDKüà‡Œ8¸<¢(ÔæõNL¸…¨=hm0DTìñUiÊâ8åFNÿÏ£¤±}£Fös»}Cã´ÿÔûÿ1®yÿqð›A¤Á\{ž7¸0Ð)C|)\BƒÌÐ »È8MRÞ¾eú@&XT÷±ÖÔxê¾fYßpt´@§s^ó¦!°”É)2ÂjEKÞ¯¼æq,n7pNΈZÿ*ÓŸÌ×þ,Îwç*Ï™|©¡é'Çûâó‘R4Ž¦úëKøžåÊ +@¾k¦SùxÌteN×ß9ÜÏÙé_úr7ÀÒÇì/aƒDp‚Ü‚Ûf_¸QÃgzd{™ ÁŒ¡¸—WÞÇ8³ ÷ ÁXÛdñ\aZ +Vù5ïdqe03a05 ô?1%ÿ Öf‚k~DüI¦8$m“ AVÃóÌÔkë&ùU¶4­K<+K©'C#_Œ¡½ÎÁÖ q†¾*§!´œ­ +,­Æ¯²@¸¬½œUÝ2Á¹p¶Wá`Hø49~6ɦ”þ4I*^îòjç#â¥XZãîeO'­‡DmËÁÝ¥ó–PÅË,$L1Ú™L *ÔdÁ±÷¶5·,¸Kwñ˜ú·L¸Z"†-ÆNjgbrf ±W Oy/³|Ê‚N©–/Ë4î93È Îj²ÜÆŠ»Jw‹6ï¿2 çí}ðT{Ïîh[E˜OË‘ +V`gëJÏfXÄ%ö¹Š?ú÷ºÁØ«í ¸fâÍ#¶9Ë"`ãüŸ˜é‘ÙŒãZkÝý$ÓÅR‹¯Ç™ >øƒ#åí„5R/þ¬lGlÄílg–1-öFÇ¢ê¥uÓ2˜*·3T{7s&UCØzLËÔ¾,·¨õ¬j0=Ëq%¨»?æù8½ÍPYUƒ›ãj¹I‡¡m>Æ /í­¸#Ó8úYž{m2ê8ÓØ,xkîñuv!C“û²„@²› .û `¨sË2¯v̪®™°«u¶&ãLõž¾ÿa¼ð^Nƒ½B?¹ÛÈfi|Ë‚û_¼SÚ^Ý3]æòǯLøXÛß=+]l̯òA<¾Îôñ˜ Ô-?É^‹•Ú \ýÉj/Hö¸A—¦ç×Y8(p…ý“r™:[Ž=¤³Ý™åkú|^v“†#aüb€ |‰_.§†€ÖΟ‹!ÁL–owíÈ2~øsÂàRÁ +xxžÜôXµêŽL½Ë–ÑäêóÁ‘â8Ù9N›mÿ: ÚÓñnÝÓW匉ãÒÁ´nÒÖ=KÁ9«]¿g‚#²hå´ÆTN0«õg¯\³LÏc["Í“ÊCL:Wœ g9fîåXc`®zdÁcZó—½eÂËr8˜ ÕJ¾äk¸ÿ{ü×| acrÈ_•ƒ˜ÁŸüñ²¡óÁªŸbÁ8ˆ–s¬ÃD½–þ>d@Ç‘6Kº"ªˆ!!„÷#ËEФæEm¯³|>¬Ð§rà¦(œYZ¯ÁRù–þ²«¿VufÂû`Lõ,çl (ŽAµ Û+ì7*,ûóX0¦­=9 ’Š;$„}ðáË, -ŸUÝ2!èqCÐñí—\ðgÜt³ß-ŸêûÉ2[­ÇÕ2¸?JôÎd +PøÁÒø¢] Þ‰í/…p¶ø5—ÇÙ¤›fK.Û±¿gòPlùX©·L &Œ9¯Cô¸ ø¥LÇ{u¿”)ŒÁ åÌ“ßb—4Î,¥ÿ$Ó½:Žþز~šiüU~B‚[u¿’©Óº·þ$Lk?g’‹ñ‹M“ó1žKû¾¯Rå0F÷e9ðåy9¿„Xœm 5Ž.çŽ7O{ÿ•uf_!!ñsQ"ŒtûêX€·! œW0£uÝöï–å~ÁtÏ„0ŸU]Zsßön½zÚ>©€WÃW×fP¼«8}Ñš[–§^Ý3ݨsmÍ:VëÚì—ý‘à +·<î«M”zð[fwÈ6úñU–Ça½eÊMäìô¸¶æ¾Bn½zZiª‹îË;hƒe3÷Öܳ< ë5Ó:·ÖÜiüÓ±úSßçÿÄ×yq11„·?¬££WÿVkûTqÇ™–ºþDáâP½.à§uè©Sñ2%ªÞÅ^œÃƒÐ´Ï¦GÍ?|­ûÞ¦¹_pCÈ;h*$z£†òW¡ü+ŸºB9[ÒºØ ïVÿñŒ.á°áùsÐ ŽpÇùejeýW50ý|HUGgôº|O¥›¶€0[#­~çd:0øôÎê•m¡T+™Þ6 +E袾´¾IeÛ‰½?΢}1‹~:·vO ¼äD g¶ÑLk£ªNP鋳nLÐ.J®S¢ýesÂE&j1¢äÓ<~?fwQ‡-jŸT'•ˆá8AзëFíl¦—CÒÙ`à4êb£† 0iÁ¯—ÍÏ“¨ +…UL ˜*;ª ï©c1dïß,:'äljΩkê¹Mõ/–Ðï}3U^ú¥A œÒ’ài9ƒ½–òÔÒ1¡Vßc›èñ Mè‘ÆbÝlÐ;“Š8™^½´J¨z[¯Œ!±ÕÒ‰½½›è‡hºqBÚï <®ië1˜¶enôÚ‹3uõ{×w§Ókú‹3,Z#Z[‹~H÷p¬<” +»-X¤ÁœÄÁ¶²e õbp|`sx•Ç3Ó@¤¥d‡µÈM"ŒµÓ[ó[ 6€±(þÍßÆ6ø fª ʳü”6I)w„‚ð³6Ü›k±ÌĘªjÙ¥;xô¼'(š†ýÕ,RõÃNš8|u®ÏïlÌ£\ÉOEBúnZäÙhŽ‘ ªmm;p$?‹LÐ踌¾5å’ttà-)Cż ­:i žÈƎ݆H…Ä!z„ÑçÜ•¥7lvÙ(âçªÁÓNf¶•hç:¨6³Põ·”Ù»# ÑEñ©})·1•þ¶Õöàhå’܃jáÝ:Ò.T¹”šbï)í¹¤ýøšÖ: Œ.8ÚŒS#S§wPO’ú9M¤Óc/jôÞ¬½H)ˆ‘Ýc¹¨]¡ͬ —=-cdqɇñYüô_ÊÍß¹´µÜ³9xTí°ÍºuÁ¦\z~˜áùrð”{;®)÷.¼¢¨Á÷ @>ªG‹±Ü:w}†ÕõìÜÑ:oŠÛÊÏi- !8O;dt5Ç/²xúÈ í©ífè®÷F³$¿=%{øŒE³òcñCÄÏ-íâ¯M„†8¬·¢|íä%ùF¤kñƒ—uçK|ÑK_oÀY66G…¯A)Aç÷)Ƀýà$ÅjßJ ”¸„ C’0Íw’‡‘±oOÅ::ø(Fæ3Í¡ÿ¯6¿;:q&¸ }ýÑò§¦Ig¾¢©‘=ýÉ!HÊ ®µ‡À…“ØûºO5H„>è5¦™C¤­Ðb{™­&\\=Ö =Œ5Œ¯$žTú þ.áL/4Wx]C¦{Ù\l G=pì>ÕÔpô± +ƒ±-8©ôPC†«õàc{ÑÆKú½_ŽÃ_F¡Ã…’ðp#â˜æ'4Oc0 ©4q†tA¥Z /äÛ˜bp¥òT$<”ù9='ôT%y°¬Öâúp¥T8 é=Þ‹,Œ¸'),NÓ»\ŠšDš 1­¿¥ì~_ 0­Ÿ²ËÙŽ#éÒø/Hùz8ÿîŸAžLôø/]å~Úð~ëçá"ÀóûèSôó`˜C­L—sB`;º^d@;Ùn5€ë©™ôñIIäžJ/ÑÍÔKÙƒùBAv—}iL¿›J(÷¾ÝRÊÜÊFôŸRVÙ—–]R/½ú‚ê²ë Ù³VØ»§$2è 7P3¶6âtºŠÐM°™\GKøwȣȯ³àù&ßëPy?â|ÐL*ä§âþ%¾K.y–;ú¸_|ìš+Bæ¥eðuÒ5Pç­ggâ…(GÁ¾ Æ©->5êžh»ó’ÚKÐoxðsß5¹ºÑ˜‚n™É-ø!Ê^ ¹ÙU²ê‡Ä±cw¢»¬‚½{IÓdh\B)~}kŧBÏü©`:fƒšå,‚ 3… +ˆuu6êìÎ-Ñ’âV0<ê·îüc£ÎÄKw^Óx í}œÁç­T]a5§C€8søv%ö!\å%÷2¢4P]êRüô´ÛÒ™8v‘š`S±¾¾d(°Ñ–ê—ri9Í»£k«Ò8—¦¢ñÑ¡#å ÃQ ¼[¸’ý×t*7%š®oQú²©3t^ñžš—ü ŦÀ]EŽi +ßÊèWÜ#ŽX¨gIÚø„÷\$ÀœOl':˜ðà“\KökaÔØ~ÉÚ³v„³‚¸d.ÞL›Rm_BÇ™,ÇàŸ›bMëéö/ugkÞu>ì £æbšr%|ªØcoeîIó>ç(ñzOT©æŒ‹‡0¥›:΋~pÃx/^ÝÆV<š“¡¹ß²)×öãžj(ðP´½º4êHº÷æeu×]~ȤÃcZdÚâÎC%VVh“·B¶'ÞT„I_eQ)è¹+cí°JŽðf*TxÊâPÉk=þ±ÄˈnïW÷Æ:˜6§›Ç½¿·tèÀUƒÈU¦†{3oYîýrddô<ücV8&¯t¯š §"nŸR7GŸ"{š ö§p‹:Ï=®e^p<&Ãî÷ȯªPGuã ÐL¾žæQþž\vο¬¢ œZ?«¸5ŠtT9ÔÃÊ¥“×ä ¡ª€g»!þÿÜL&Ií_tògc±„æ¹™!Aµ+ÐQé•×é@ðžµq[µüå1u2§kÑW¦§M/òq¿0=ö{œ…Mª)Z(ç¢m»¨àH©h¶ËöI†ÅVlòP´en:a|¤ÞûôšÖK\Kð³'2…æ û¡œVòaQ{H²‡°‰3ø×éC†î3}•­/æiA½¦¡ácûîk›¾¤ËpÄ{¡ WèuÉi¶A†ZL¡g‡îé'1Ž²á›Î·\˵A—„{O^PxY†Ô4†%-CôK297•qBh±ÏÇùÇtËZ/^O.Ïeš|ãÀ—ô¿Ý.­;w‘K×.‰'YnÃñ@È;ùÚ®3ýÞ¯×TÇ°œzž?uÕ[àQ ÑDKèTÈ‘¸e…QßK´0.àf6¼…ÂÀŠX—ÀpðŠ¼ÜðØ° Ýj} ”ú)¾B­»P1 8¸÷º‚ÞöÐ<Æ[$8Dá!„##\ Mˆýèî]é`¿t„FÂïì™|p»VYC„­aµPDø©¤Ÿ.Nò¯ÀYCáB³;}—ƒ1C¼Ñ‡*C¤r)—ù½7é^Y6æK@D–Œeƒ‘ðêöÌæÌNüYŽýì;ŒHht*ã’“½š(Î6< ¢<šßHT%³â Çu b½I|bÚ|QÜîòˆ¥f: òöF·tw*\¼ó–’%¤ +‚~É'e‘mç@Dͱ÷Ã'!±\Zêþí©?¼C®„_ƒj }€<(;cWõ=É€Ö…Û‘Fè‰$NxH2 Vé 'ZGz,`¾=AZÅâAMäö“l5L)¡›¹1”“K¸˜²9û(§qÍlf»±Á.Ça1Ê× EhËê°@ºFñ.|*w¤?³=p—ÉŸÒpbà Ù¯ŽË׃D%§h10ÅAWùxÓg. 1æá1E;Z{NdPW|gƒ•J<ÙOO“#øÇ/L±˜÷‡Ð`L aòpÈoaêz°}š<§tnk:˜|…oÕ„’“Þ/(ÀÓƒN„â ³õ7[D¯¤ÖZtÍŒ™¥“>û|”m®Wû“î;Ì­8@]d6ƒ%h4ÈU§A=˜7ë$›ß…'=¬ãx·: ŽéÊ~ÐtËÄã@êEU—±VÛQ~uùä ¤éh‹¸v•YîÅÉ©Á"Ì^&q&šGOF—SÉ¿ÀÌn·?~³GÎh%\{Â5§UãžI¦Ð +·70 “^­ZßÞHE‡’,èô¯°Fx.Ã@Þ +¯ý»ÀH¤¬£¿r‰­l´n4‰«<Ë.IoßïÏcnöﱊG^.\slRcÆ¥rýι˞îðÛBéák±L‹ìª £¨p^OÞ¸óºåŽ¶´ÙZ7¸›·Š³ýxèó…M«”A¨¹VÂ!ÎY„Ë)Xȶjƒw`,²K3ºª=†ñðÍà üu¸!KŽ î®âL7®Ý}eÁ÷kFz8P ®4ÖhqbÌôŠsY$†ÐÄöè;j‘sühBÀ|þ6Hí¹\@<ì6ã¸*ñ¹óe +£aœ"Äó ½KÍÑAô#XËlŽ‚†~6¼…¤`ãîw`Ä"Ô#·ä AN7Æ$tì¾;‚c€SÁ’Aä "':¶nb–>‰4ù4T]_?`sœ>žïã7(Í0¡yC ( j rV(PÝÊôø‡U‚ßyÊ +jbÚš ¾ ODü×îoïÏÞ³×:1%rž?ô)G¤!XU›<ÅG–¨£k GM9¯N° (­/räé…Ǽ̆Þo¦ ©øK Ãä«c“À• ÿQrnPêD±ë`6:F3¥"þâT½¨‚Ý-«è|%ñu(VÎMê +EYáŒÄ‚‡Ûž7PfFÛÈÉP÷’D*w‰”þôt¿°Šà-—ÒÖ¼?5ñã7†(/ŒpQu§y ¥nž{ûS +-ØqË£Ùå:¾™#wæ+²°¹ÌÈσ*àÌc|Èó.Gþ ×Ä_®xäÏz¤(p`ëù1ou/cÎ;h +Þü7¤×L¸(´Ù°!@àF …÷D’îëuŒ`ÐUè<~3T$A™y ^U*òùÍ´lç*TÞØMT.bj s{¯]LT7áðH[yFZä5‹vƒ]Ük¸v¾5¨&^¤äuì?5l;ÎU.vÙ‚(bVàjþ“M™cŸ&Ÿä°Çü{äspú!}†O¹l„ “"¹1úI‰ÒÛß#bìƼABŒ×„e2f? ØEF!¡9 +ÒF+˜ +; ç¿œQúGisÆÙÞä ð3×efÀâî€2•6d“[ UoHX£V,‚ÊQÇ ¡½„OGS:Ï&îiêÞ’Ø/Æ™à5ÛØb­Y)N]pŠ{îk¸wq>½©ºã‡ÒÞõªN×Ð?̆÷Å>Ñ ã É= +®[zHd··,"¬ƒ’$òF®æ¢ôðˆÕbrA»9 GÌ]²Þ¾n°ÒhU†™“ò9á\ÙЧÌpx›mõIC%ýýSG^â<G»ŒÈõí¬Í’cOå NNÁ¸YhÂ;j’@ˆ„£TχQWxR’ðÒƒ<$Ö.nÓ┌ÆdiRË`i:í˸|ÈÇyõÁ˜!fè0ñÂX‰ÐE AÀí­Þ(ˆ«D-ŒBN\ µ7s8“‹\ ø1oÄp G‰CA^ìð¬O„B("vBjÆèBžRƒ€Åþ!WB5aZÑSân¸&.UX|âUGË">/H¯bߟù`ŒË/®Þ-ÈI7ùéiªQƒPO˶W×ÞþF9aÐK›¿ñvŒƒ"†bNÄÙË¿AP]àYðÍ|óéûXÕÎÇÇ×Ï |É„Ý"›sµýCΉd ?ngÎÝ¡ÿ¤7c&ú$µ1¬Ý V°HD­0ßoYw^¤¸ú(¬+ìÓw¹§8À M"ˆ5A8Îóc…²>õÓü5.ƒ.¥ÕljMÙ ¶÷"h7p+³®Néë†&MôöNADC#£X…Ñàþ®l¢÷KK&8‰ÂnLÐtö弡E”ž¯ùúäY*M9@4eíîÇž³·÷™²”)5Ã&^®#L×Ðn¦‚¶3ü6VèÝ΄3Ÿð(MH­ÐIiÞ‡\vîV6§×]x)‹r· =6ˆw.SrF)Ú«fö)tNI€!a‚vÖŸPÎz·áÌ'kí„J¼ºA‘›pGY\ÌG?†„ÓÓÑYEšLl“nµé{™$ï¿WÒ¨¸­>žœl›“!V'¤°ÍÉ‚Çî×ONv‚ÊɦŶ‚ÞÚÁ©´åP„B89ÀØã5ßÜfLiÑÖ:›mû¸hS(¦±ñ®c¸ö„ì`mp¯íYØæ«RË>nà$ÊÜ›”÷¬Îž‰b9Ù"Þ%ßÉÜÖP\›’oM™c»û±§Åêím¦lN¶KÝœìÄ”“íf.Þ³;³8Ôî´ád›8G>!áQZ´µžôßœÌw\dV³Ò<ß‚]Ž²a‚{q¯Â6سÏp²TN¶çóâ={֬Îö*:óÌm¯É˧ٶM̦¸ƒ#‡4ï³Vod©²ÁE½UÚ"ñužü&fvS·â#÷ŒÈûQ±)‡çV6Ha„wÑ‘îì²Ê'‡*@ã,¤/± „o½Ž¢ÃÞÒ_€8EצS›ÈÕqÂqóuþÉÐFØtyàÍç| ߌò”XÛlAŸòÊñæ½óêˆ"+>¥ +<ö藊ᙬh6Ää³”ºRtkP-âðZ¸ÈI‹>Z(Ö%Ê(;‚gB xÒ—BÓ`1š/ªY¿”|€2Bƒ¡fü±f??U0éó €»”eQi®®-äS¦Æø¯Ú\ wÈ$Oj¶›™±,ߟº~Q›Âs Oæ‘1Í¢œ”\“Aý +>U’ª€ÄUÞ9{õìLOÒÐ "ÀÇ8;M „ZÏ ™søâcx=‰Ï¥RE®’/ÅXêx¥^>üÓ}¥ÍĹÞfŸ ÉÏOôPÜê’Úq€:îýYk㬭P¨³½5™ܧ¥³¸ãŒô‹ë%Yú©‚EFVz_‚Ý¥°Û{iš™µh\ˆÇì¸Z‹Þr–!:ä…ik u`|÷XùCŠ£¤DnSÙ‰ÀpM†á,Ãߣ¿rÁúÄEáøðH{@i¾,ú•öfË£dоÇÂ=ú·.Ï ÚAªŠW^Ò0©•ƒú¯„÷ŸT½£Âñ™¿·yn "îeCQ›;A€p¤öFÈÃ3©-RñäyxöwîX>Jb^6n‡Hô¢»S hoÀ,q‘?¤‡ý ÐlWûóG +¢‹›šØÅ>O 3ß‚2Ð$¾‚Ôa8Û3¿~ÂFÛÐ_Ž…ð«ÝÓ±Àa{Nöc 0㑲^•|TF”¦%'t"¢œìnÃk÷]¾¯„t2@Z‡»`Gö¥ˆœåEŠßÐ7#<< ïÂöº^•Úõ›pH³V5¹õ£´™J¶Û6 3=¸ötk7íöÈ’' ¢È › Ж€)Ñ¿È™Ö,ÌQP2Eí±Û1c<ÍÅoóÁY!yÊ*l!¦Ê…í–Ͳvó/ü§ª4A<ª2«ÒTD¥i‚ØÏUPÔ3u½X0/Ë­Êå¿ùú ëÔà8°¦„†.ƒÝòu•· z—j© .`™MQll7mBÁc—¶‘§>,YÁBt‰J›‘É|B|ç­¢Àeó92ÁšÒàF`z£f:Ÿä¹ü3Ô\ß>`s|>ží·ª2eJ0"®ç Úò 2OÞ‘ùT=Û P‡l2Rp[Eù~HetÌáè¦ýO…ÆhÔâTÖÈ2÷šª½x›úÔMWD(ÓxF!(¢ýt‚ê²Ú+Õ4ÙK]5D`$± ©[’Ü™&ÒN6ç”x „gò˜eTj,ªê1äB¸Ö‚ªz@èBáÑ`ö5þ ïmöËç"4Ä8h‰àÎŽ¦<Õ§’’T!ö΋²µÍÇK&‹D#È ¬òý¡¿US)ênHYª¨ÁÜ!4S§î`Ê120KŠ×LIÅ&œ¶¤’‘hCŒµÖõ„H ÿ]Ô£r©²-5ÕÕ¥Xv*©û˜¤8Þ±rLH Ò›\Ù[2‡E‡uæƒÌîd«iŠ!ÒÞë§ +˜d~Ú¼nԜܶV…´ÿÚº/Ñyµ@žj}pÅ <¬ÂÆëý›!ÊXq©ˆ›øM<°´\ÔznÝ<Ì•9‡âXÁsÈv¾=Öq¿úÛÃðom%ºÞQ¶€û°S%ä(2ȲTb®¢«„uܥˈ>u•ä†t ³kš¤4=t¤Q è„À/Hß‘æ«^FÕr…hLðþÍ‚chÆ\’jq¥˜ä¬¿X¬vÿõÒ2]›3D•.tšTëœL±‰Aþ¨4D„"=·8ÌuG;ÊEýrAª’š.§™._ÂÞk¨D3Æ^njxœ~ÍÞƒ,Åœ +kw4š&V„†Nɪ™“Tjç@?Œ½ÑMZDÆ’ô¢Ú ®kWõ …բ䋡n¯ë”*µZŽlgaY¦¹_ ï:¿\ bœH>Ä›$Ô“èaŽ‘+ô)í…+…#ßêîÖ÷\<;û»õ“¨ãSÍÊ5ák8‡sn¸£’£šMÁE^ÐÓW‘j< +Å¢ [Ît`,º.ÈØÅj}œ |ËÑG¾FS#ü¼sûƒ‡oBUø~ °–ï—(!Ix]؃·óú2i@è'qÒZhëºjïÒ¹Ëm'ç‹ÇI¯îüˆPEZ2+wÝJªÊ•̲Ò?¾éÇM¶î †W£V×ð.‰ÙÀq+ÉoèSFxñzÍWÈ7ô*huO¡(ê7ú©‚¼’l%•l»uéüïÔN¢ÁOòRz¯Ó²—^7¨¦Œz¤ŠôÐu‚v2ŽßprNÆš{÷ýù#ˆóŒØ>l“lÎÕøãsx±Ó&ßT‹öÖÍ©žTxÃÜ%»«ANm¤‹_jÖ{yá~Iíˆ6¸gH÷ø ¨¦Ö–¢THoüø±BÉ@ŸóÓPnù|¹–»fS (#§ÆIeÙ$÷n¦·Rª°) tò˜aJÃ.‡‡S+ pÖ£)\Tñ4½½@)ohQ¥ä[¾)„¨÷{SP­!dtw?öX½½Íc±†ïjpûe.(hç‚~lç‚BïÇ\8óùr-M†]¡s.è§lJ•[´Zõv%Fo;&Æ”þf‹Êbqh«Tèl‡‚ÇD8?–®žPÊ÷ò„tg¾)¨÷‡O-ȦtÑ*²ãÞ·ˆ%ÉÄ6áVQçhÙYò›U” +ݳTÓ´¸ÖwƒŠY¨– €3Ö6!åf°LÍçRçƒðB‹;’KŠñíàf}ºæ+=]K³åÏ–S›½xt"Ê…®eÜ 2£µÁ½¸µ0ôJË=®Ø¤{°9ÏêçŠñäc‹n—|åµzoɪþ¹ŒëîÅýÕÕÛ,Ù|l¸ác°_fÁâ<{¬¿gác{ùd¥ÙòY`¸´? mK+ðÜíjÜ \k{—¶ÀªÔ0+ṽóSéé Åx/N(wæ+w¨Õ‡O-Ȧ€Í†x|Üçý÷îë‚,M¸i·J;ÇÕΓý$‘ÀAÏCg^Jx¥îᦧ”¹t¤oQ_ðÐ +Ó”?<Žž9]0œ)ƒð¹…GWò"5nÔÃw3«Ä–·d»E ãmG(æhЋ"Bë˜{iš[>_¡pÒ1ÝOí^ úþy¢ÃâÕž—£þG’ Îõ¡éeíéý‘W³¸¯#Æë71®£Ã=`ˆU3ï«m^îç>äŠ4Í°ÛE^¦ ÂLù°1"ÎM›–j–ðÐ#·â5ÔÈô÷qCáMâBãýý#zŒÝœŽHsD5&A]7gcsŠQ™èšŽ'º&îªÇLðÛúø‘"U-|ª“9Iô¬û ¿0%ˤó †å®‰£¼Ú«Q°õn°PÅÞÖUQ\§½@VÓðCJ Ñ Ï‹4LE +E;± ÏIY(ïQ%4v¦ ,¢OC[L‹Ê‘Ka͵̆ÁQMÓ‹+ƒ"Þ£WW9´J=Hw#òV4$„?̬N³  çΠϪ.¡àÑ£' Ýzž]Áå7ö)¢ §£Í9V˜‡o”ôeh±Ñ«¬f—mý{–û +”Øe@àyŒ×pGÕr4˜Ê:)åKÎ"wI¤¨w¼‡Ýý¦k¢¶­øOj\½®uè1",.ÊçËu+é£My¨«ls÷â'ŒX¡Çoø¢(À¾ñzˆºŽÅø¸i;ËçÒ+ô˜šž£êåÒütÓbÝR¸*6‹é5ý÷$°z/8zšJÑE“^ßûo°uJƒ|pÞ f’P—•9"·‚¢eZezµïꎤJÝЧXwPÉæËòžHÈ¥˜E/“úŠ5x`²6þTœ,œ¨6O·8YÔè‰ÊÕí®Žë„N‚Ø®\“ÁTû˜×?&'ôŒqcÍn6ÇG¸ áL?½OMӢѬJ«ÝrhÉÒ²þ`¦Õ&g 2»»ôdaÙ¦yËFux>žÆl;bÛIA¥~¨kIý8Q¸ÍW´5ñ@A¿BAW-uÖ¤¯pù´1Ò©‹å¶ÉI­`›®ÇËà[#¯Ê†ÂD0c¥¶ áíX{'ß²âœzšûKDUE”öžœnô ©ÚšÔ('Ú!¨I%P–FXĪ!É“ÁÆT”¶.GNXKçd™°Å +]Ûñã "òs(Û Æ{>Ó ØšàsÈëTçCï#éݹ‡Ìîgò«î©Be!¸4ëªAfÐ"„Š„–É:ªÎ$ñ1­=žê|8s‚eõÉèÈ’ æw{R ¥U~t!%ÃÞ?„(pô¥Œ*FUE¦-†ÙÐ"2çcÙÓÃæ ½Ý“ ‹ŽâÆÆ I%̯…#¸ç\»¸ “º¹ÁË2]ý­;Y<<Ë…ªÚœt"ÅÁgîxbE\©:äD±íû(´D´Ús²_W…UM„Ž?ð]u´U7QÄ"|ãáËeϳUŽ*BÑxòN¨Â9Ý‘1Š®5g?@X…Û©s*(FÕ)3‡6­)îÝ›¢-á ¢”B3/593t_® - ÓýÃ¥‹‡KÃv èè0ü_Ñúö0 +í,¯UÌzO®B½Grè"•ª<‰¡Ì¥™~mªðˆòþ8  Æ*~7N¶ +'«x·£ ß©.yªp]d¡-Ï´5QV7Ö°ª>˜Hò¢pls¦(G[âÆlÝ\Ú¾ÜzmÔw³”g"ƒ%+\œž4BÌÂ5Zk¾…$ÙhÆ“´³%îQÝuÛñOxmnçL½Š!†£DƒY’,t·r—hzsë÷+Ç7Ü¢ôø×f»Cýy‡Ð`ŽÂ=lƒxEfPú1„Å’`œÀœçîðîœù¢} x¤§ +ÊExžAúF¢>!¤W|qÚ[j«Ü$‡äó]âÒ1$|ŒI^ +cìE¦ä4ö±= ­‹Áb£ÅN¼k:sr³èÄTðÀø–Î7X¼˜" ÀñUµ]Ø„¼Q|®§ƒrc&Â;ëoÓƒäQ˜¿Óè#-jd¨Û‡ +2ŒR †fânzթبyÓ Ì‰ÓV‹å*û8J1(AƺAÉC&Š:" AÐDˆ,çjÚ'çWnéüw¼¡(¹“ô˜^ÐÑ„ Øû#‘Î+¬xè‘ò) 2¤¤ÙšMÐhVî~P]7B¶jƒRŠ©ß‹ÂiŒèAÔ쩶ר™¸4Ø©×éì›èwZ…¾/%4ä¥i!Gº8¿w}<è) Ýdâv!ÐC÷7°»yPRqcôE Å»¤DõðŽ‰M7¬y Ú`ÄR,6HƒM3·xäTMìËœE‘4Žóý’TàwFß©.(E]£h‹oBÎ]QŒ’­4ŠÌÅæyŠ Có’\È9aæüÑž„žÓrW(í!.ÝX!ÉÛªe#°ºH9ù7ã1ýZ_ + ’Ã\ ¸Y9°Áÿ°Y ­ªCùñ8Àóx{¤UžÉÇØÃ=Úà&—{ºÜ}LDÜz·yLM"ÉÑVßëÜŒ¢Ú)6(á¤^œÎgѺ^ûùA–û0)¸µ$Ø¥çñVN®¡Öˆ£›Ø8ŽÉC…¤œ Ë7pz&Q5Õ HM+Ê0vVOÝýʃùÊ—y‚Êh[¨kÖºÎN…PrN›19¹UµŒÞb¸ú®œÛ…6ô"Yâ8–4FXáQ šÐºŸXN²€é†¬OVß¡ÚF2\KeÒèÀcšª` +­n©A^OI2×/ó½AZ¹nɱKP ›j¹:Ý'„ÀVW%ÔýYü|‰Y›É‰cÌ“/ +©¨ck7"8ü ýºcíɳ3–‘1ÍùÝpÇ|`ó»ÄJ o¶,˜.b ä\3M7dtkjJzö >¢|ÖÍ nþ\80ªúÑ\ï@ƒ°/8æûÜ®t ÜË^N5;»BÔ \å«Ö,¦xt*51v·EŒþcO~Leíxˆû‹,ÁV¤Ñt:s`°7ucP8¾”§ i†Ôüºw^[Â#š:Oía[.[H C®Ô_쫳\fJxD¡Æµa] Ž^ñÔ6iâàd70^c9svÑŒ-f{“«“§5>=>Í#Y`æºR쀎¹‚õÒÚ9ç «t?ûBÌÛ²µ¡F!ܸ8ƒ©Úðb ùÈI#œ[?¥­–<$®¦ïzvï´XR¾m¬2Ð)‰qªe1Yý ƒ¥ŠÛtÚàhLÈqì¦À5Ô¶Y{R`ëPÉrçìÎd*ÞØjà.mwäÞåWæÿÆÿÊNÉ°qÌ)ØÑÇ%¦)YPKèØÌ +·(Ûæbp›j?–íÒ‡ÖEᑨ\>†nhlsçÖZgÒ¥·£1 {h¾)¤Š”xTg°Ùª×4ÀXbÓ-Q6ðrêJNŽP‚vÏ–Qþ +AÖ·…ÏB>)ÐÅ&;!tð”w`Nܨ>•â XÞ²ŠZ­kò>f[·±Ýó1Ý8–~Tg±Ù¬*u +ÿ8:8äèNÛEì:ôõŠDoE$ß ÑÿW±KÓJ”Eˆ§ßߧ”ócÄ$泑­%B{"¼YØCóM!Ï?é¬Î`³U¯i°Â?WúºæˆÀÁ8„NC7Uyq;Ì ®‰,»±*±ÀÌ×Ò¼&Z.Àº°µ7‹ÑmÄ(È~훸v–-sÖƒ¸A²íYØn¸ù8Óyh8ª!&3w5çÞë¿`ÜfË\“!$ï têÒ ¡n²ÊÕ˸K‰¶À2äH—fɸ‰z~Û˜ƒŒõXJÓ0”bêMß»ãÕÇjRߎ^˜oÕ‘ù¬ÍKNƒi«î4à‡ìâdw‡G ˜¿üàÁf‚VßäÀÛ“”`1*¼ð5–"7'tÙ. „ú„†ŽÇŠ{)¸`ÃëÍç7[¯ë£É¶oc»'ü: obyÔg°Ù®:} ¤ vqtÄË]¨öÉÐèÕƒ›U¼ðø•–ÄÒ”"mëQYÖ˜ %ô·ƒ©]ˆç×IÞ!us¼ÖXåð¶ÚGìíèˆ)¤âÞ¯Õl5ëN†¹+ùpr$>>é®Øä†Îh|“Íkt®Ÿ6%×íײzaîf\½ì† +y(ÁánÓ|‹Gä”ãܵ<ºBKŶfc»Ýæk#‹á¬Ç`«9½Þñ*c—ÎgQ0×-P´\\èÜ›TZP\ç¨ù­4+é +€·iÔB¢(Pß" ¤Ësœ•x‘¢¶ËñíhµùZÏõL[sïñ_†½ÓÁ’   +Óè“J½Oé®Õ0v%?_³O,¸yÁµx;Kùä5wõrèÀ£LÃ5À‰98,z,¾Áª\¬«á˜ Wmó6´úa¾ÕsGm¦­ºSá5ýÿî÷o¯q=á­$7¹rSM 5¹Îw½Ô’èÅö[l0Gpeôˆ›8ækÄÓ»SÍ@#ÚØÖXl¶Û~MW +í¬Ç`«=÷^“#âØÆŽ™#Íä±­g_çæ ŠôÊv`ðìW“ýZÄqH_†ÃµXÂÛaׯûXC*EÅŒUsªoG{öÐSJëÊ£>ƒ­v½¦ÃŽZ 4óÐç¿Ì>kµI ×J\çÐÖ¾Ùý QÞî,fZ˜œŽfµ„èlÞ `»b’mÀ¤!¹liIwpÔ[DR€ïÛÕdl Ž{80¸è«ºfcçj“Ž í“Ï“k2ë¿­cÎBÒ' ¤†eÁÃ¥³ºNö¼€K'*¶›’q9•ÊQòqXOöêqU\u=ñjËb vW^'õF§[±,à°–Ÿ%š-Ò”÷Õ‹b x‚¬þ'h¾ˆ„î`lîUÓW9_'ÂKˆÄù4`§•8‘ŒxM–_ ¨ÔðO΂K‰RÓ™“W“*±ÕÒæ¨ì‹îHßç„ž‘³[%gLý.R’™xjѲ}èý€¶ÄkÀ ¶˜¦´Í!vµ–—Ì{#gÖ·%S ÁLÍ]MÜ%îžÜ»¼dm“6yÉ Dx×â(<®óÔ¢j90K‘¶S Ö–¼GÖ´Ï̼Î÷Ël‰jÕüT²I³4Úèjõ.y÷îN‡Wš{¼w^>ý‡t]C“P +Þ‡á̳ A;Æ€cÍel¸‡,ž!R–ô! "Å!a*ò…–† ;6Ùù*½‰*v€RzƒU:<%• - +ªÿæßþí8fþ›¿EÀ¢¿û[_ú>Ë¢Ct~ö —ªŸ‡(8C¦ð8Ñ9š$<Öl”G« ZyÕôÿ3÷.»zíºšØøf³:ËÑýÒ8ÄA‚f«Aªíj–ëHN=ø‘”DihLïÂYås–½mJ?E]EIäÇ°Ó’DÜèÑD©4ÐÇ tãk ¦827Ʊ†É§ÉxŠ·œ +Dªö•éγÐŃ1ŽyeÎÝ!Ô^xtç}Œe8;TI5éHÂ&ïND¢Ð¹F!-Œ6éO1¬rδޞ%ˆtí.>jGj¿Ö,Äårøx–Ъ*ÔîÚ9N¾@lâùòÇ1@ûxva+Ùz¾1 Œ“çÏâœVO^Gt¾‡²9Bæ»ÃÆø"vœŸjõAG÷H§=*À/ª„î8[O®dÿV \îïìµ{){G2¬ +Ž4k+òûGq…#wÈoì {8açö§x‘Yu°?;w?çd(Jx3mÂÏýÞ¹¯ÆþzºâÙ Nà4NSA·&ë¹?cK©Ì†>ûäF¹Ìh¤XÞ*0ê餷Fô +Š>Š>ADrsIªÔg¤€ð-G®P.M\é¯q5òÔ3îK7=9ºŠ +9/)Ãukßp6‚º®£Q>žήþnNDi™+WD–U«tÜntD"ó’ý•¹ + ’¤UðLƒ˜Q…ï«ÈJÌù« +Ô.PÒ¯è­&}7diT‘ñ™£B…ëÏR¡^”Që}Tøùí!(œ#È´j†ÇÞÕk…m¸¼á`n›F€+@çüö*èQáÙÕ¯çDMxPÒÔŽAÇ=bˆ¡9dÄ kÏ»ZRMdÚ/UšÞAµØÊ{8»ÒŸ—FÔþóØ°F@QŸœVyVÀ¥vJݽ6Â>V18ÛÈ!(vBÚ#óläì** nÊV؆ëÒ¼ËcAØÒ‹ g…GW3'ÓVÄ7¥JghãÞŒ¥Øé×\ˆ@Õ˜ivb‰Äº^Š9ã}z°Ôn5œe ǃ)‘Võ¾Ñá‰8“]{a XæxmA{Gu¯&yBx!™uqØËE5‚rx´À™HûÞÂ.£Ã ¬Ö:9}D9ЦÇ(]ZŒéÕHÊx”?úøõ<ü«…Ÿ ¼§ËÜSO´ÔHXÅ¿¤ + S3ölá i`=Cmz²ä®¥¸ƒŒ½gÏï³ é0íæþbjq2ØJ‘Çæí…=®é‹n}lá ·² :,…4D?:ö(ÝFæÁøšÎHV²³tïÕoFýzóË-iê¢;lÆ–ÜôÌ"ˆžþ8%+î>G-Å|+Â-#óíÊ+3€œÒØñÎ`ú3[|F2ã.9{1 ì@óœB#þ³höúÁ±â}³å› ¶èÿeÕ¾äüc;iÌE6ÅB«zÍZ h3‡Ò¨”ÙW~Ü Àà¢t»—Ãf§m8å×Fôh@£Úb‘‘©¸ÁäQŽ¼·ÞÛ©²UŽ`°êܼyŠ‰  ±f½òzôôVå±G;ðìˆd ß%½”o=ýýœèÔUÎñ6Œ™ÎêžoODZ€N"Å[%»\¢““)YÚ 6o…¸_×{ر­§¦ÅxEÎæ1Ëö(¯€ûï‚ÐS ©Þ«ô0 ÞŽädnñÞ:u)ßÇäÁ^Ý;m/|öë}¼Õr¤³ô¬ 8%ô [¿â ¨mRÈ:ïÌžöûgg½wåä¦ýè´ë'((Dü< +ø‚½™»¤ƒ)Üšzxœ’8X®]þûxtáY¾wãˆw:…7¹Žrt¨×Þë˜Ññ_úÁ5ððGýß«ítÔàÄhIÈq‡ +¨:¯@ª¥%äÞŠ#pèb 7öÚ7:“ù.LöÍï·âú¬,ŸÖõ±á]àfÞ·KÞ%X“º£ovÞ¶®>eÛ€<¸ÒyÏǨ\y¶²gW¾éËû,$QõÑåq¡ žäRZÅ|¥×æne®‡¶ÂózÉ2¾Ü_!£+í'´Tå„ô¬ÀáÃÆôð•:@¦°^\«×»«„ÇGšß¹1lÝ: ·!9ï×QÁή}5æü5K#Ñ·ù95q&ѯÆl£½nU ##ikº¥·b1¶îì ×ïwE†ôfÇ™bM®¾²$UÅÓ*†(X£Îˆp‡’[È2ÁNުЖؿlJLºô&î£xïìWs¡Ó…ðÄZauD=:‡ä“RX‘,!2OW:;`‡˜Ë€Ipº{¯‚Ü´iÞ›Qážè»xD ݵZ‚úÅ@5–qÔ:ØÓ2mr‡¸üUbzNŒ­M<;zuk‚}[y“‹éöÒÉ/çbZêöóã +d4âZ²µ~S{:|Ñ‹ì‡vyT8µÓÙÈEj?ŠáqTÀ8†Í¯ÈhzÓÈCPÌGbckôäèªLXÙ*lÃukd×x:av¸Ž +gW7'ó&¸£p«ʼR„]Š%®)!›¡è^ȹÂ[a€^¹0Ö›_²óÆ1 _»\I< +eŽ^cèjæ—Ýéž¼ËÅÓ«ò>ztî£qáíùµ¥Ô«hGáÞ©/Fz¾¦T¨’±…íûŠéɯí‡'z'ÅúR’y>둉}2¶ªúQhÕüƒñs£xvn7nÝ*lÃrk%¡N»fò(Ü»÷>æÊsÄåH¼Dà…Îm&Y"[êÄæìvv6Ò¸¿н“'7‡Ù8e¾Sž!‡Ùpêé'¹570\›LÞ˜Nqånä,X–ŠÏ'nO>H~)ÃáÁ{Ý&Žk-ˆî8`ê6êéf¦œCbÇç˜o8sà&@·Œ}Ï-‘C.¸{9`m3÷Üû†×­Q8-æ£j‰“&(÷F\¨Ü;Ya¹%Ç¥ —› ýFÔœ†hê"–+m˜ÜÛ,PîëÈ\âp˜ñ_–[-É È°Üj¡†ÑÇúHTLË0÷F^Ülé0’í;Ù®[dƒŠÝ_È5í¸…‰“âÀ÷ftn‰ÄqyƒçØMDèžg(Î9G,éòÒ;¶Ç.± ºÙ<ÐL†{›wëÛÓÍêµfA¡1u³d¥ükt3­˜Ø‰ÌÍúK2@Yhn®M'çð™ô |¶¤è—"Õ€ªŸ˜l$E…$žuÙ¦/>ëÚg\w²"q3WÎilõ$ðiƒþ¹ËoªAGþkpïĉ½“' ÷u·xœY8€¸yÁÒ{§Nèí¬˜Ûr«8€„C[ú’¥ý4KÀ¬Éá7m•Á¶o’^"r\tÛl«9×Te›E€T=iCÜf,ù8–ŸïõUQ{'æ63(¹èXeÒ\Ù~„ët…1\X§¥’{>ê" £l©vâ„ÜÞÈs[¸À~Ûf Z‰]Ѷ¹ œ¹ð¯·-c0(ÎöNœÛ;Y‘µe`¼ßwS6[:°µ%‚¦»rÔEžª¼ÁksTN¬áÀ×f2.€íΠÑ:À6·¾eî+=Ý»-SáÚy :3ÊöNœ¿ÅÙÞ*Ò¶øÎcBm‹é}îæ¸YÜ°¶7âÛÞÉŠ¶-A;k±ëpùZθ§6Àm~«&Ãv|L ¹ÍÔu‡ÜÖGÞ!Ó3¢tIøø×BÛÞ‰n{#/¨j¾ mê›+ym§“;ybnK˜Œs‰[A·7²åqK{äÐÚClËäN^ÈÛ;y~.ÈM¬Y6– ¼¸6Ìeuýì5û£n”ä½-Øm‘7…wûº*ŽÐæÚ¼ÍN™tì«ÇÉÛú×°ÞˆËhÞ¨ +’½1]ºÃ`õ ŸU3MSÞ™ZâjßP—¨‹çêÓ¥ÿ[ Ï”mÂms¼Mòaƒ©7Ö àíŠg½Íà,ê@ÈÞ˜®‰¶Ò™E`w. `^èž9™.š›Eœ‚Ž«K—îÿcÐÛ¿ßp + ³ °Yç&M|1€¯eÇSˆ¿ECp "¸Niq‹ÎÂaï4¨Þ?±¬·¦ýz—iüæ÷¸9i{PÛ²Ä!Ô¥ÿzË•'¼ïÂÁæÍ€³¾ÿµP¯ÅyM¶ÑæZ?¤eÓle@þ1ØôNTTêÁ°¶m ¨ëM(C\Ø~ËÖ¶ qIõ |ÀiEvÉ~-®`I’ ¨kykí UYêæ÷*™„ÃO¨é¦Ôûï'zõÖ”â]ï2M¢•ßü>0~jÜÛ²Ä!Ô¥ÿÖïXq ‘*ôµlýÙmØײÑÈ‘ÃÐ<|­ðknTÓêN¸é8p©7 ÂÚ¶5°®7¡ qu`û=–C G[ –zHõ?û˜ +Ãtà`óåv*~ƒ½æ“wNú• ¢œ©d¬ˆ´Þó§ŠÊxÓ'‘a°wÃzoL°w±{ï‚e€9¬ +öI±nc0¯Œ9ý/ „-—ÕoHØr›¦XÄ• @¤åÒwà¦+ÖôNTPjûû~½55®7¡ qu`á_óm¨ï«-C\R=G`>lø„§b`˾ÓüÐk± XŠlˆ >Êüµ8±DÕW +4½‘zg0Á«÷ÆúzK°¯÷˜ßãÛ&°¶¶e‰SªËÌís¦ÿe°eçi²Q´kÙ¢8’}#iÃ0ÐýÐ+l³¢N[Ú@§Þ~¾€¬·¦æõ&”!®l €å™êÑØ"©ž#`µ¾“Ë—¿,¶ì?Š-ÐײM©´hužo²4Ë‘ô£ê¤HÕûÏ'¨õÞ’bbo)&ö.¼ù=Z›,ÑÕ–%ªL—¾ÿ1·ê0Ï6 [î–£`ó›ôȼ¸ÈͷËUM‰>}¥Úþ|ÂYkCÞbaïÚÞþׯ±jÚˆC¤gïÿ¬K3’Ɇ›ÉÎ;‹Í—åð&ØiÀ†“ ƒ-ÍWð|ö×B¡Þ‰óÅ0XÈÖ[[{j#Žl ðáä³1C\b=G@·Yäm ÃFTXlÞì³°ýŒlÂVAê©lDN\= ­'œ´8‡6·  ìD…ªÅkVQ­÷Æ{“jÑLìïü6Xì8¤ºÀz´ŽŠN;±åU7Ô¡™øZžÍ‚W¹§ +š`Òlm¤&ä +;½Ñ<õöû…d­mÕ {“j#Žl È„9-Vc‹¸¤zŽÀ´@E¦šTá°y«9gæµ85èåœ!~d¦(P,ib&ÔQ(ì¨ðÔ;ƒc½·¥ˆ×»T‹hz°~ÏvVÚ[24E¾öß:Æf¹}úËbaë-vÈ­z‡…¬ë†d(EiÀÒòÄ jt`PïDŪÞ,Të­±€½‰µG6Xj­Æ q‰õƒ#lì팱À±õ@7tìíè3‰ö˜´P¥·#Õ Þ‰ŠT½3˜ Ö{cŠ½‹µˆ¦ –Á:RÍÆ6âë6ê¸v†…j»mCÂvÛ° qmn *vÛªìNôÙý÷ŠR»µ¤h¶›H––«Ü€›¯pµbhKœgßuP"'æŽN˜l1Žr38ÇAL+½Ühª3â4[f9ˆm5À—w¢¢4o¿ŸxÎ[KŠü¼‹´ˆF|û{\â¦r4b,‡PÏþOsa,£… ½­Ù9vu›‘Y_ÂWÞ¾#…!*`óÎ`õmkLa w±Æàl]° ÖW3g#±nc »"°z=×—Å¢«¸ÈœüAl3_²Æ ŠnÍÞ‰cp6øzolŽËg \Jè+Ìj̧X—1˜ÖTªå@Ï–«M8U +{‰¸"žÜÜ Í–ÓÈß0¾kKÓïûõP¶¡Q¬@‹fd_?ÂÞƤ,Iž½þ[p´üìwÏ&à–x#÷ˆ†érê, >N|²Sâ'ˆ‘4.,êA$R¨ +Ô,­.†¤›úo‹X£°4d?(´VØjK•ÌÌìF†O˜…S²GÑÓ—G!ÚkW½åÆð™é*É(Q¹¹!Íþ}Zچњ¬ÇHO`2_bsÌÎBîðäkÜ5 ¹É±çžŽž›O8Í’ìÖß±âãÃêç§-Ä7 gÙ,Ñß‚ß”ìQ„Â¥TlüYøi +ðV¥Ñá'Ó ûL ûo¯48И·^¶BR¸vÄ$ðÙ F+œÂ¤¸ò¼Hôí@h1&êªhŒS ¡Žx*`£­zväíÒÿEÄØWùx pP¢ˆ¨8nWHLpp™¥ ðÔcä¢-BXXH«èÇ7Sh&zIG»Ñ¯~ûñÓí:c¥Þ «&z,™‚„€€Ë˜ëkQÎåȃ¨…snıÐg‹æ;{|K?ÞŽîãùÿíÿûøÿëôîãç?ÿúþù_þÛùøqxøøõÏÿÅĺ"¸ÓœÁ5G“Cî +á”å.…¾töѹª±ŸcÐUŠëäìo\h2ØoJcónV:A5«š00Yƒ~UÏ 8ŸÁöSغLÅ[!?ô“)Þ|ø±.ŽÂî釀.a}nÜ{¾_f\÷YB»;‘¡€£ò£è'F GÊœ¯¿ƒëý k8 œ{V̳h>Ìï¾ ËQˆè왦Qù!†zþœ£÷¹®k®LIc4»Msœ¸QRaãL[™¤Èx@VóøY“F î–Eܘ$åRÈýpÀÖ£¶sb”n-ì@Ì qr#œ.³'ÑF©‘Ž„Ÿ²²#^XFô5 k'ü¢{œK/šõŒI«÷U?ë×<|³9û#)L´b;>48·áâéÂR†XpqÃÓ'qð%ÐBy«bÏi>ZD¿F!àí«+kX>Q«þ›ÂÛTq¹H™Þ £¼ÔéX*‰¯é«J©™–¨g_:¾‚UƒÚÄÈ$G‹B¶GçÙz6ãÅù¦p<2oªfø~I!Cîžw;Æ1¡ï‘m}ŸøäqOè€ð(Â؆".ó·ß±¿(Jº‚ˆŽéàëi}D üî»ÜÈ"6€¾ïM1qFÃ@G²C8ô,à,ý(/y´3?Š¸G혗ßÁ]ž48¾ïêoÑÿw9ÎO#} v^qƒš/ÑÚ…Ué®,G//¿Ã9 dðI:€IwL7ËœFác|~µB¦Ãøª‚ÌãÕ(à_"¦vô£ðÓ’X©ÌÌVbA¬Â÷bk1êJéüá9b›Ï$2E1·7öÉœ;{TÇ5³³ºêŽË1Îàeëí(Bœ\jK e$…œfkþ®}˜6hÏ,üq‘ ¡=ÐLá´z€ywAÄ5Ð|ÀL8²4w/Êì~³«€YÞ`Ï%hÁÈo_èÛ—müãËñÄÚ“ù·Xÿ’B‡bý[ªvP²ÀK» p/=Üw'à‚Ô»FAøh„ÖŽ\™EÑ!¶(έ@ê(Ä +ê%-–¼¼:Õâo +o†Îdûe!ã±íÇH„©rF#ÏÁæy£]¹!ê >‡o…!H÷ ¶_"i +ÙGF!}()ÍÍs›š$f¹èÎì$gÁUçfćø«®F8¶c#¬ò3G_çâÙˆ +}*3Få_”Ö ,L;‡W.t–«„à®ÄÚ¿ö]­’Û™LË8RqfÔâ/p4uÑ“CÄ«~-8ŸºëÁ±Iüyim]…œ…ϾÖ.ò:óà¸:"j–G¸Š+óÔ¼³‚]ôviîéeìV³-¾Á÷²ˆ¦ ·Å‡0dFºý[ ÍÏÖfÑu8µpui42;} Ì[$À¿û,¥$+`_=gX ‚%ŒÒÁ¥sç(PÜœÈñZpq~€56lpT8 WWjÓ)-œÀBÛáaÝ{|«qŒÛµ¾q¢£Ë»´ˆA§cê=þÍüüɘ‘‚Ø~˜ÿËãpò¡s\†ZñlÛ¡†£i÷‚ÍÃ×½$ºÔ@w2b Rpå­œ?°ýµ *‡<;EYfÕt:Îr°åˆÍƾþÖ^Wil·v{´Ãaï#—×–m¹¥[ s]V#c‹Ù´°—?ûøå<ˆ†E”s†›*ë_\I2ªi¥üKÊ©‡o^ ç†@9Û +ð¸,ŒÝ~+å(¦žÃ…·öÈF]Ú¦ã)[9R¾—¾ð·…Òºóƶ”BÄY–‹eË[’aý8¸œŽò­W÷Ò1&Ê;„w¤¹"ýî’™Òg¯¾o`"5E“ŠÏ ûºCúùaÍáÖ]öµÜ© +)bnu$Zz-Ø€n¼µgüºKÕ¤±~[iF‚Ô6xçƒ7¢‚ª¼£ð6’á ×vÂûcïÖÑpü؇äÊQCô©¾fJÑ)à1Kiûøz¸§AX±i‹‘âHo„‰&ŠB—b‹\ˆdÍ5…g^2˜^xñ_”#cU{0×~ÑFò($3­EµÚ¶¢x Zí‚ÇY|q¹ìHJ)oÃ(\’a­²Kkô£g÷r3,·ðÐ^k“ì!÷ò­{¯cnmœ‡#Ä þTci¤„«Ì ²ÔŠvÆí ©Éœ¤¸Š•VôRŠ7¸,?YkÇ|¯¼Ô©˜LN ƒâð±—6ÜßÅQêÖ¸¤öÒi$NÁgH|\^ÚÇÞ)”ÒaRx5ãæˆ\y#2¦µð*™)½ôê}´ÿP€RÁ…%¯9¶³DfOW|>úÜRîÀkåÐy…¬ßõ ³áúÞçY4Yb¸§ÆÖ|^TØ¢zÇïLàp³×'KØÒMÏ4)"N[rEL=ÿvïÄYd:a›‘$6­°‰´myÓ?üTp5¼Úq:†÷€öMT 6"ƒ­ž­kRèàZØä€åöVŽX¼¦I†-ðøÁ1*Skn5)‡øq–KR£,x#ÙZ`*GŸ§.¥M<’AK;ÒT¦[÷îåk€ÌñLgÎø&ÙV~éÙ—£/›`Ƴ¯‡ßi(¾=‹%&¾&7¶¨ãáä6êDÆzõZ§Š]tw¸zy«‹2ŸúK;jÛ;×¢‘¥#O°ÖQë}«@*Og*Ù|Êh‡ä5 +üô¼Ÿ7§¤üP‰ç†yýèíµÆ6f6:M#(ߥÞËÍ(][@ÊI²Jú›Œ[ù³_Ïôÿ#9ónÃkRíCÑ·qðN>Ø.™¦¾)ÜtÐ@ÂÍÆkq Ýâ¯ìµÈSÕ´¿ª× +z¼YÅøh#Óâx²' “Ž§Èȇâ0S<~_•ôrö»¯Á¹²¯Ôã›x¶£“×ÜE3öÖôdµg›€¤0)ÊUŒ‹óPÐ3R å•ÎKš(„¿iØÝ/÷2N…sá« 8,r1évÎ6:O«0a§jAvxÃ3#)ouÚßu qðîïp9oš¦×Â17¾^\Þæ»8†þìÃãúowƒE ¡ºnÇó O5Z¯R«Æq;t+·÷K7X Ù5H³kzÞ`QyonXµ;s˜ªÑ¥ù&^G°Pª–ùÖ½k¹ k örêï(zôìËÑW{DlZ?1xñS¢ ÉëÉ:n6cœu?´Ù…ÿ¦ e–Ší½-¿õî‹ÑŸeª0vWâôõO­Üšº8kÀßQ׉¼/f%¶ôZß ŸJ¹¶ fœóÊêErÃ7RÍÒU®s4Ü<òÙ¢$ç••ªþKF§j[°}¼•ÛQº·PcXémù­_ÎÃ<©åØø¸ojxº ó'=»ìWx÷(=†·b<¸Ó]ÙsV[âä µ¾?÷…ýmkÙÄ{îN[ç®Åkp®ì#¯¢ö&ž-¾tßóQì‘®ëê?þïÿ_æ0©Çƒ´NjÆí¸MãÉ¿"üã:8|~û?Ä6uÒF-XÌ•j¤ÂàÉÓµm!LáBdŒæg)•Á§ pndí·ºï4øJç&“Šk  ¨}îTqù°íIg0ÒSÀAú!f¡.êçF¥/†ã ?u³n £â² œ nD(¤9Ø©@Ó÷bŒ2\ÓëÍCBª„ýMOR‰ÃÊMMj²GÀ€£' ,¤è»Œç$Ø´ËÃ<Çj—0b’˜}ÙU.:K„5|Þiˆ’iì˜i¨f8àÓ ©Q]Ä÷16¿¿RíÜ­UºQé¸Pܾ *–RÝŽ¡éûñÍRÇrüÜ©sá®vž ýýûù;B5—vÐø¯âßá÷Ÿ9ßÄõ[FC¹JªaßáÞ·>6FAð:& 5mit¨KÍöÛ–"žrŸ;¸¦¼†W»CCYIíÇÖ»¥Ë$ ¡Ùªì£ŸSê¶KS~|³Ô³Ÿ{éèöjî1J_Œþ Žÿwëò‰°=)Í€š`)RÛYž¦Xcל­(l°Ž$$Ñ#÷BM·"GËúÕ`ùSbi€RÐÂÌc×Çïl®ÏRÃ?Yu±¦”å.J†1ðVÿèÀV´wü`IªÖUM|ˆ²mxK6Ì*ÀÉbsâöÇÛbIÛ°>~ÍÐÇîV•Þ€ÿ<®·5šðøø^ãña–\ÑÎUZOuT‰¤¤É,½ÛÊWŒ +±ƒ5ÀëHþŒfîòË£[[Ù1*W€¦¯òe[?¾k™ ŸÑaUÎET©s8‡hqG`MS‡]ÍG_5àŽ¾Ûì/% –c9jW\£5 Û´–9+NÁ/|3®=½œ)NqàMǾöñè‚-9z¾3Lß1Lî"ÇQòèÂ눮›oDG’ªBþKÉ-AŸ_JœLˆ‘$kÏá“Ö +Yùè$4Þ¬‹ +>~Q…stÐ íÖŒvÓ§ûª€÷ĸó +zÈ›L'-~°w°¸ÓbŠXð¬JC'!ÝNÅA:Ùwœ“øðqÅô'¾tî«9ø“÷ä•!!XÀº”³”F¤ Ô¸$¢WªP‘ä®à‚u‚(ÿ*—ý/¥wmÖŠ@Ýöi¨qG+ÕüØ–ÖïŠýΚŸ›ùa£4 oÝäB,Ÿgçx.?ú”9ý¬mØ È5–etyÊU¢í²-}öé}¬E!n2ÃjÐÏ—!©dªÁ,°gÒªB{E +£ +Ca@¤ü^!îô—Fé7„q¨‘@e@í[…À8zÊ£]AdjŠØº¸‚bvl‚â¾Æ‡–£«Ï +Çp¡2·FÕI‚”WA +Ï®~='jZD€„¸µï8ähjˆXu}˜rÍe¦IN–*î&|fä ÀœQo%«ê­fe• Žg ÚÕ†Ë8ö†@:°¶Éá(6>©£öÖ<Ä2מ-2âH +Ÿ÷Åaï# ;Bãgù>J·hƒ‹È›ð&ã^þèã×ó0Í@:èàOô.}ßyfKF`“GSÞØ3Ù(f#‹Ž¦Ùߊ22ÙÂ+çÂVû–¿!¿hH U»y¤m¦óÄ…-<,ié›Ïˆã8gÙøÝÖ‰½h€“eÇ-mw7Q¶¢½_©µ=p-æ£SË ó[`S§Ù7'©+Ú^§.5ØÚ"yj{-§#š“UwiA{èüVµ0µzCaåp”Dè–÷ò÷RKÌÅ´°ËÈ‘9Í×ÕÂÞG.¯ÞHpŒÒ­@ß8úšßdÜË}üzþ”7(»$Ó÷ªbs½72C +ß;žEĺˆHf.Þu(ÇÉ—"4¦Èâòà‰+>ƨ|“ãÛ¨GßVR‘”£÷ødY€TûHoÏ’8Îèˆ2@f¥%ã”þ(±ý~0äÌœi›¶èþ‹¡ü£~ ]À¥«¼Ÿâf@LxN+rþé’Ü݆*9Øå ù CÎ_UÀãÛµ@:&Ã#g•Ó~Äy–†ï€Št£ôà $øà ïC8„qµžÆ¯¾¥ÇÈ»Éûì(=zõŨ«i‘K8µ‰aG”vI¾ “¸óÁ~–’5Og“¸Ž–°©é@濪‚Ìð­ägj ‚U +ûÁ©¹º¥p¶7æÛž¥F! OBÒóòS2Üvdè¼õÌ>egŒT~dçÆ«PBÛ×Ñž†>lÌÊ M®n†‘”“2ŠÉêBÐïµÈÌ[tç}&Å‹ž±öˆVwmZŒ4WäBéQè¡»ËWÆÈ5C¨ôîŒ +˜uuµwç(܇âÁ¸#áxtþ*Ô^xtç}Œ§Ñ^ 2U.²i ú¼zmd|…< ;Õµ·EÈþ^à›ùVñm.–|²×n‘rm"p:‘6XßÒQ±Âž/N¾œcfÜR%Nt3ŒsÛ!SrŒÃÎ^@ô½\ä8JКº|áhº>MAgƒ(cã÷0ä}Wû»Q:—p†`¸>²µ ‰Ýƒ 6/Æóâ¤ÂGxx£ ¢§©à“êÍ^bæÉŒFš3 ª­=[¯HKLæ’™l#ó¥ÔöucÚ‘îž_Ý.¢˜B#ýuÜþ­îŒhÖ ÉåA~^Ñæ3ìÊ÷ÎÛ¢gѸÏÙ8ž÷D×®ðÉà³…?î‰ð$œÚødZØéÆKp'ÃE±eÃ×öãYjFàÆÚ\BÙ¢go^FV,ÆÞ‹}ŸríåÏ~½úîÜø/¿ËØñxæKé?æ~8\w¬û!Óî‡êt¸ª ÑoÝóÇÍÿ°|ÜüËÇ¿Òÿ°¼Ÿþ‡|ç÷øCþ‡Qîwÿì¿þŸîÈý9üõÖäð?<†ãØÿðœEýWø"ëâé(´Óÿ©ÿC¡þ»ð?ü21Gÿ6-O®!ÿWåœ^Š»– ÝPyÙfµ3¡l9þ§#"Ñô¡FJ-*På‹.{ ÅÂyó…ŠÛ‘ÜõsL’óðA唊J“öÑMãd`§"¥nm€!OóAÆC.U`b2ƒy5CS‡uÎídkŽM”,MxÛixq‰êM¿¨±‘ÅCß98Ú|ŒõÊø°Æ fGÍ•Y} µD†è€Þ¥yÌ]ÍA8g¼n.ÚˆÅn,תYMüå«¥P²ësP5N@ÊnmEOqPLãËrÛ…—Ê i°RíqDÎ:ôI—ma‘ÛèÇu0Ž´ Ð Éà™WF…m$ºj üŒíÖ\ÔB‘.MJ ’7 +«Õ¡;¿ߨv¡Æ8u)ò‰¬1QíËÜxÝr–uÖñ4ZºH{ä4paƒ‡—,QøÕÿ¸ŽƒAÞE„Aì9•è»4@å>‹€uã¾p²xY*]àqŠd5Z4Žøe°¾×l¼ïMTòšÎ÷ѱ> +ë3LWIºH8ýTgöToF² Gf¶ÂÉ,yæ ‰×ZžHµM\Ü%cì¤1O2“üØëFÍö=²ßêgCk‚Ï¿^ÔÊ‘~Ì‘ŽÑ:½¸êжSlB‹M?:t<êVöPãŽëP.ýN H+c0.ÚARU€Y¨@JÏv·Ô^G$sÝj65LzÂ8‡0¶_§;ÐØXÛê04\W³¾Jü‰7vÝÛ^aš·ºòç¶&LM²x«l‘€Å®; 7=*àµX†iìÜ Ú…•/й]ñcþªsª5¸| F3™*IâfHMTÞ¦ ±O ¥1VòXÜÏÀÀÿãö&GjLã.·U§Iâý®‹³ÀÆš±bÓÎZ\RÞUkX¦ú9‘þ ƒP¼þzQ‘Á$÷̓1¥ß–Úýqq|¡jª÷O¹\ö™‹´SëÃb¨–NÄ=º³ÃË«ü“ÓoJ›d1:¨øTô‚;!“Õ*©}ìÚICn{Í +ßR5ƒXp›6Fm„3 þ¸® ÀTÒÜí~ùKîø¼O?Ð8wx•ì4cójàh€Ëqê +Ó¶Ñ*¸¤pµ–½&p`rØ9.ÚjÛR‡”†ãì괧 (• U¢·µ±ˆ +†Ó”UQdðgKÍʵ¨$Më€b9Ïùµò­•€Ë#Ÿ÷5íÊåÎy•YYuI=9›Þ=Æá _ƒÜþ÷àT ®…mZ¿‹q˜!Û"°)7¨ŸrR`«|QÅþwü‰ ‰²jo48®Å4O +BÅ-…&4Ä8Oª³\Åø¬ç1hÐ~XÙñS®ûè”ìÏù–ÏáÖe5ci"Ïo–8DÿÜ©£“¦™9ϱü÷±†½¬à~„}ÙÓ°µ¦k¼é1_ ùZÁqXìú’{ºáš[|°TØha:$n¿³Eopò‚f=øÆ×;ýS²:éœÃ±ig/¶¢½÷Ë‚LÔâ›<{ùÖ•·QÕÇCÇN¤E‹qK©ÍœØ/ÈÒ_GL(s4qLºâáÒz+¤•šFñd¬=âe8‹]ì¡hj£§9RÝýʘª::4¤Éx*^©ú‘jaïÎQx Åɸ㴗æÜíB…{w¾c„Ç"bHG¶yw +À`búàÔžS¦¾Wl,¼KºK ² +»CíHñ$Ý*tAPœÏâ‚tëT{òõxDªz>Åq@pLíãÙSrô|g÷c<½ŠsT0=yQã(ÄPš”ôjÎK.¸ZýpzÀLmgTàTg¡;?|h¨º+®8µÔ;{í^tÈøe*Ði!5}J~ÓYèoìéÄîç·‹x´>Ȇ¬ƒýÙ¹­ø28;{¶[pc¯âIq)åÞ¹¯Çþâ¹U¾ÁtâÒÊ +²P78´CcÕpÏÕhUæCŸ}r£Š^kJþªö’·Fô& +~³‘ˆÈM„¢ŠÂRoàîGÁÈáþl$ r²j…¼î_Œ [îFУ«¨ @)CŠc¸n`áFoª {‚ŽFùxVxvõë9¥Ù,›" º¬VÉj3ë—\­átœ´JDâoï ÒâÜP _UpÀ’yiDï6c 3qTÁµ&=yñ8* €†zŸG$Ø£‘ +ä‚Þó +œ; Èûž[3<ö®>+œÃUcä’m¤ñ›]SŸ€‹ g…GW3':q¸Žr¡ŽAGdºà) 6ØI¤iüàܲX½TibóÑ’å½BàÄ ý¥µÿœˆ€Vá„¿Ïm\åY?@Î9þÒÞ‡M¯FAñ“ƒË«‘£«¨hÐW…c¸n0bqlíUУ³«_ÏÉ´0i¥ +!ª˜Rj)Òf³TvB ³†i'&j£ÉÎ"ÜVôœ,µ[@-²…ãÙQwÃB4E´óÐÞÑÚ…¥$óPNÃB\âÐÊAâŠñ[Û‰½hïü…-Oð´v‘ö¢­#¯cj eùÙ+ü1 Nª¨°4Ú0逨‚Ëh2lWÑ;:Ú:zë¯p×@G†—F´‡±û<Œª„äŽdÔÅã¨@,éóÑFÚ¥ÜÈ»¾7rŠäî¡Öºx]E„Ý/)Žáº5‚û?Wcô¨ðìê×sò§"Ý +­Ñ‚ô6s§L=1 d>7ŒQ‚1N™y#ª •$)Ež˜‚¿³ÿ)î. §÷ÚÌCL#|ì, Hô’Ú {¼æ–¸BÖÝÄÓ¡“SÑngÇ¥ÛÈÄÄË íK‚V¯ù`ŽžÞª#öh§°‹cˆo’>ÊžþnNæ9¡‘ñ=,›ŽD|i†Ä¡Ø¥*qê¼›d²Å5êÍnÜß #àò`¬çß›7 jè˵ÿ(‡3â0¯¼ÉÚ‰5Ìà5‘+ V&/£NJ´ºÉ{ïԵ܎ÉÉ¢uø©ž¢…~}1ÞÓþï¡veð–Ú,ãÅ{>`+±ãñ¹j<Â^m=•“›ö£6Ü_ÝĉÂÇ£ŸLnæbé`êiÞÛðú8$A’$ŽÎgnå¶û;o$È>¼Êµ—£C}¾ßÆÔZ‘´9ˆ½‚w@7Pˆ¡€—#ŸØÇû/dû†!š4g‰ZñÎoÅdñf_k¸±×¾k§K a¾H¿ßŠ‘è­â8;LzÛ¶æZûvã»Kxhw¥®«×Õ§g™“+BM*\.òìe®|9Òÿ–—[‰M†:Üö4ÅE#÷{mlö®èQhïš,ãËeVBÊ)¼”™Ìõ¨@ÿ¤Ó§k3yÍـ烙kõz‘•<çK̺1ÝÚ !9ïwSÁŽ +Ï®½ùJˆ·ú¶sé²DBônÛ©ÝC{ݪÐiûÍtKoÅblÝÙ.®ß#Ȭ¶¼Ùq¦nÏÈð÷¡t5kÞÚ`:#bCFšÐ-·NުОîbÿª)5éÒ‹¸Ïâ­³_Î…Ân󳊇ÕÁ!]•3)¨I©,DåâP;+7dä­ÐSY_U‰ÐqùÞŒ: DÉ4ÇöÜ4òìY.¢ ã.ì±摶ÅÏóÖ.¦GŽ8šxtô,>êÖYZ(ob²Ÿrðoüj.¦¥n??®@FcÖx°‹ÚÓ¹X1t§v¹V°Úél䢵Åð8*`HÃæ×Fd4ýÖÈ!(æ#%o9º*f¥8†ëÖÈ®ñtÂìhž]ýzN¦YH‡·× <’ŸÎc¨‚&S|P˜’ÆÙâJx+¤ÉK7Æz œê|0fák×äÏg¡ÌÑ c ]Í~[6Ï.OO¬ƒ÷Þ££poÀœ‘ rm/<:õ>ÒóÈ„÷ˆq·unOÎ&d«µi߬ͧ"ñVÞ²€¬'W=.e\ ·aTY-½—í +~çzÛ!v™žÛÌÞŸGù1—àE½L7ùŽ²­_ïã|FÝidÎï¢rh»¤MŒ¾ÿˆ/ Œ°‰ªXd«Hž©L¤S6‡¾À­¹ +ƒ€1ë#2g’nZÔ+åwéWrFR½iƒŒ¥Í_È•*‰ºg"ëpïõեñ‘e¦ï¡ j9Âcj´4 Í¡o.îU|„“8uïDbÓØk.YšãšÌzŸq|Žùž3΋G¹Æ礜¼’9*ƒ;ùŒÇ>ĬÙoiah¡SØ›ÓVEöZeBÝw íÄÌqÑyèLræ+§‰û)¡êL$Æ"â¡^6¢ꤔâQŽW¡ëðp_RXt\xçÈ\‚u¢—‘¥6Øî×°,SR2Þ"‡ÉêF+Ħ"”ˆk¾a€ 2®ŸáM®19êñN¶Ë–˜D>S>È8É›aÑ`ÐËø"jÉšÜ;—88¸.‰”É&.^WmÏ3Zçˆ#\'fY8H÷3¶ËUÁ#ìg¯VQésAe ºá¾Xšªc2õÂQyÁó`*®ôðÅ->( éûFhŠÓ±À@«}Dëx_‚’“>Ï–°]ö!TM–¦6UH=ULÔ»®|RCŸ(­ØqX˜ä4ÃWHÃ8]¸SY ~q'‡b1ò¯ºÃqe¬4WÛIÄ­k3dguº>ﳸEíÌÂúAµ£ò ís£!6ĵ¦¡õ¢×1oÕ]SÅVZo!Yâ¸F8Ä^—þVÛàÚZÄ]ÎKÔÙ6×jÛUU×k^Ⴌ + _dÈú$êTÂÎÏ—|ìØC]÷6"wØéVW«Ù)*ǃʺöσ›äÒ-q„±øÚº¾÷”wím‰¸$Ö˜VCÎ]bX˜kèú=7  ²WZNúbôÛ„Û±ÄScÆï‘X˜ƒ-‘¬Âtqd¸¬ ˆ‚s豆É`L¡œë¦ãÓàû¶ØvÕ… Öè†>ãXîêü4ܘš :M‡…µÄ¶§@ª¶Ñ~îËÅTEÙU‰ØR­qþvRûM®ƒ CÚÚ‚\©B Z++$KÂÁxô¹ŸzËÇGÍqI‘Ö üü¶“ƒh&æjÖúøžËÜC¿™ª”>ÌÝSè%dÖ¬â(™zÚùóvð§’íp‰å¨‹£Î09ˆÝAC<¦~4‹JÓ@ƒ˜GðŽOSá\Éf5<®TäIŒaÄÏзå•J³\OòÆÐéO+ñ1©>„3<îdqÆÞ3ÉóS逪Ž»_g•/Í*xĘº[“ÒX\ÖùÑPo¾hžs9á<¥l LÃyÈzõÇ\³gèЛAR¢±“-5ÉŽ°1JôoÔ ÂzÛwYŽÔIm’ƒ©!Îö-uŠj™ÎN]ú¿…õ”ÝÈÕ°öD¶Z†1Žš¦`Æû|l‹ŠˆøûÇÆtβ•m-tÕxÔDŽêšž‹h›7Ô%éäiºôèý[0ä³ûÇ¢yدÍ'î>‡¥¹ÕF5&r“”j²Ïe™1C x͵¿×£vo}õ€nhðCÇÏé#DtìØQGK ‡ ç2Yâ”ß2¸‚lS¦-CœB=û¯—+´f»X «„Iêñ’ÈÝ}ø*¸^ß6z’ØHœänb“„ Wv@-+jw4F³Ðek3bâê‚eÐ4}oÌ—XÏ1PÃ$ +È„ízå„xÎÞØäð€°ü̶”"‡2vË@EK}Pa6n$št:»¿ŽxÄ3ʶ„ÈÒ>"\‡L‹hä· ¼üêј!N¡žý·O¸€xEØ!ì"# ×5^jðÇöiDŒ&™MC¤…ÚÅ T<¼3 °MýìÑw\üûobëæ5óÈÐÆG\#–!®.l º‚ôl(‹×ˆõƒ?ý¤ÚZ +ˆLý6|bpQ¿WÆí{rú¡-"¬SQ¸ƒ³‚L¾Ï8Ÿ&ÑEî± 2±zÍiƒ7¢Ü(±@,ê^?»``ewàÆü€ÍXÄ!Ös æÅ1àÏô»¦ÉOãƘí$!WßÔµ^0V"v$Ë`\G¹†1ÌÓHí†áK?8àŠ¤>ï€ÝÔXŒZ²‰µˆ¦ Ì §±ïÀ‡to̧X—1˜ðìÖo»”<6bìC·Å2@+øl²ƒfÒ\ †—KW¥•9éc'¯ ăA‚=YƦ8C ×x˜b1ñcïý=^SòG[†¸¤zÁÜb©_5oYº){^•iŒ"›¢D¸oÄ"í†nŠN­ lK¯›â "'‡/mg@Š‰±ÍtSÑD‰Ð7bâê‚e@Û ¼N÷Æ Ñˆõ«û‘žPδëØc–­)`D›ìbò4=âŒ_áô³È8ø±“p~ŒÇ¯ûhºÈŒv<£~ì˜ãÇ.¼e€å ‹Ô´eˆC¦gßÿ˜ƒu™'ä§×ÐtÃâk5ðnFèi~Iñ'Ë@ŸÚí¦zµ‰[ÚND ñÁ€w_½ÇÎ +!Çd±Y­T†6;`ˆ™RÓÔFB=GàÏ:8#IŠ\0{\:·é$ÓJ{Jæ×d·RulHzy9~̽×Ô²BŽ½¨gË$®'”ù{Ú æ%ÇjÈuváýØ2Ä%¼ý=‘{OGS–8…ºô^ý`2ºa)òF8ìZ‡%ÆÐ'lÄîÝj³¿W3„¶¬¹™8þ|-/7âç‡Rˆ!•2ÌÑ>é¤ÇjMìïqU%×O¦-C\R=G`=_7Ùº1ÉK†;Þî›ÜÝ/7òâI›jy§ +R#Ò/¹6´¶×§éAƒz¯©ï¿÷à=½vSÛF“MÝHµˆ¦–£m„½­E›2]ú?-ì?¢FùÅv ]®íCsPl½™3D„ï´`¨XY?hìãÇNŒ̸3íªšmáš+ûcHµˆ«æçy¾8›– 2¥øqë¿Ù`¿“¸ñ5÷at'—ÝÑ+8ú¤nˆ@ê“GÑÁ@¥ê30^kù؉8 øªÁ T8 „ihcˆê¬M-!–!ÎØß“©«e4Û²Ä%ÕcþÌ.»Ã Ÿ§¬€;ÔT‡ãŒ3ØøjÐ| q’ƒã”(ŒÓ~Ê +ø™x&Œcši §ÑDF,Kœ]° ÖÊ4fˆK¬ç¨ê_{"lq]vìˆ4 Mjwª¦{Úøù±'@&¿ï‰´xß›ŸÃûá؎Æ gѼ¬µí·fß›mXÚæÒouÑ,Èãĺ"&¹ŠQ#-q*7&×&AN 1Mm1¨‘Ï}&ØŸŸ=,1àŒS¤­š/ÓH1¶FùØŲÄÙË·¸ñh ´põé9‘ÔóCZ£`Ö¶!®ï`08>¤%…%BÜ\¦g¶1/Ÿ]¬94¶ –ÁúfÖØXâë9º2Ž?÷ÁA®neŒ$GWlD4Üó98 b¶<¥¨ZwçàX(óD>›ƒcŲÄÙË Ï7Ó˜!.±žc0­(}Ú”LGdø®Op¹R\{‘`ðÔ2€fy(ŸñaâÔæ÷KU¬––N™M’}þaI½ímXÚæÙoqÆ{àfÿžð?Š¬M[¶C¦ÛúúT#2qü"²xJ‹–LÄ*ÓY"®ØáG4øÈÑz­u~hþùÍa ±·ß'¨°ÅÉô|¡¦,"DÀ¹ºø B—”“iÎF+Æ*r&‚ÖeIUÜG¨›žLwú‚& BŽ)·½^Å /u1EAb3¤ØÄÓ‹:‰@\6Bãðo³ËøŽw@;x—ˆ]†g^²´J„+pïMzÄ÷…Á€Ëœcw–Æ‚Ex›…½f‚ÿhô<ïZu*÷$³6©Ãå1óceZŽÓ}•†ùJ~’x–缭‡;x,ø®Ót8ñ[`!/£°{æ>Àkƒ_W`˜bGNæËšHù;‹w:ü3‰Ÿ)zSk—‰›Ä ÐÏ^VV„Ÿ©º7²]…$&OËN$)¼k«ÿL̸ÅÊ\ÓÏÈX+×rú}p÷n}A +QåÑç5(Á-DÚih~‡x¿„–ÏhÜ["!ü¬Ê +©ìG‡WÁ¾^4ŒJFª$—¶šìë#¯C+@zvtÁ‡ŽgwBþu‚zë¬2ü4å»ÎÈÓ'+ŸL5êì‡i!Ïì*M’%‚-M÷|¯ ïÍȽtóFjx²‰úc%&¸%JµÀ£ùØÐŽ]ß•ÂzUƒ¿öFLë CŠTÊœÏReZ>/Sõ_çÇ¿J:®÷ƒ(ÞÌ0ÁP?-5!Ζ]E-)T;ÄòrAéiÑI‘h‘~JÓ½„´ÕKâ'€¥AÉßüy|˜ðˆÃIÑÌú[¼Ãm—;ƒ'ÈÖ*œ­‰WL^w´.R“7o"“Ð +œ–Yœêƒ$^Øç•?´·^Öv¹²Øð$Ç»h`‰P"¸Im5]Ýæi-±C”_O*ž·áÅ)SfRsŽÇMÃÁdGv"ÿ)‚s02h`M'O²~€{ ËÞEÑæ“–åîLFÁíë³ÈkɶŠq‡[yO]ÕàïÓºÕ<¥ e„ý%^¶S^ZûSúÏÏç "º¿Mìùaô=û×–¸ï s-˜zx€èLœ‰¥QzüÃAsWkYô9i²ýZhMéò¦.°C"¼ø5‘ú.ÊÔtžKã8~'ÚlQ=”¶´Sä"{[Ñ—ÕÿÓ(éq…µíÆ*Û;L@Þ(aìᵬàå[iÛøãR¯ºvªÅ +úpÖr{¡=op–† +êàL*¦R•2óør'™=nýôFDívë†Ã¶ÂCeÂm&”Ce¶(©¡ÐµÀ 4,ÍX£“ˆà`×1ù“áR³i«(zQ½hê5V÷–Û$ÙVq‰7ÙÙ~\ú¼là%•*Ž*N¯Fkà¹F”„Ãu„!mc1‰ é¿6ÂrûÎ'hæ- âœßÐä ÀÔÃ>ŒÜmü™Ñá ¤ð7fΩrÖÏÚ‡3z9tà°Â‡ XûÝãÇÀÚnXà˜óôŠ¨°¹1Hz»Ä7N¦¶˜ÛìJ!×C“$Ý×[¡!z®ó¹;Hb>&U>HÏ&•òó›‘lÖBßÙJn¼•ã&7•Ì¤è¼Þ6ŒA¶¥^emÏC¨ʧ£9¼ë£Ó`?î9]«ÞœT¾1ç‘<çž·BI§\m%èµüC:“¥Ëâ’‹Íü_¨—¤è_NìQú1™b2~T“— ÜðX‘üÿíyõšD&Ѷ »&;Z’Ç›¨^b¢«]ï&80óCRá>;ÚyA Ò™9›­­d½al*Ž#–”Å÷éç7K ’ˆ¹ñ[*•'Y|0$þ)NG®ÚzÞIFŹ%qÇ*ò€â[ÁYÊ$‡ä«[R–û¾Æ÷~ŽÕóH]Z×A4Y×B„Ö¯¹®†S{ÎAçÓ}Ó«k²ðª mËW]ÌÓo$6®ëòf5(:P‘¶£0ÉïzÞ)×Ô;×EÔË=æýðqçC./¸P駬šánë­NU‰OÿÏ·žþMJxš ˆÙEšµùyý’› @uÚoW3¼B8‹œÜìЗ™X95ñÀÄʘDÄéѦ(W8^“·ï4ïñ8ý)7ixžÁ +¢ÅÒ¤‘£®`:¬ÈUës!ù8.>RqfWàë€y‘F䟌–T‘ëŠA,‘cŠœûÉ”¯vµ6sDWÆ&[?‡ÿÒ»,±8”È·:uÔjY:‡¤{<¹4ºc™ 0Ôþ0‰~"EÕe>2âYÃ'¾êÈläùEÒ{˜©öµ"¼É€vn’õÒéO±0ÆGA(ݧ­SgçþfB`4Õ*ŠGf:¶å† íIü‡Ø©p kk'šÕ•´jWã¢]Û¯ïDÄzm|HdkNÙ·Ÿoà‘%ï5W‡tGäÜmEZcóú—%Â1 ›‘YTUY²=ŠhwøZ‹úäؽ“äKdbj‹¨$ŸÖ…ÔÆ>¼|:Lœ:zÕsõäV«iuˆ²ˆ^Eñ›»ºQØÃ1Ù¾NÒ½½Sbâ"ôÄdVœÜšF¡%z?$Ä1(ml’ šÎ$þ¦•4E>’­ñAjQ6"߇¹]ýXËböö±R–~Z\ÅW÷׃&ßS‰¶3üÛ¼uZI?ìJ8ê¹zr“¡VÒ>þ"Þ§\vòSÁ %é]0‘#‹dÂ$"i¬Xî“[1nKRIû’1¤}! ¢]õ;©æEúaÂ^O¾µÔÂå§ÍÈ÷)”Åå­ð!èÅvvPì˜LÚºÁjï±H~ü½†Fpp6M¶ˆóëHE‡÷ ¥bña›&Û‰ªÉ˜è³Õ= ¥Z¬†ê0²i2&úpÖ‹5žÜ¢muˆ­|üÑ’&O)›np†Ö¼)m¥lS5hëÃœÖç?[´ºãA#Â}XŠgöô YÍ6G¤–³ïAº‰’QÖÄÎ~˜51zû\&K-®Kí4UcKÌ©xVg¦zZ6jl ÎVO†pãm«ûø/5Ö#.1«ùÌ:?yû-.’Y“¸¾ìÉl}þké5¶U­Å<ÏZò;Éj¶v!ìõäCÛI¶Õý+5j¬Wdë+[?Ú¸ËZ½¤mT&qÞආøX'“&{xYño‘ïF»œC«ÆM"["Þ¨ ñ#“2®UABîÙ»&1#P<Œ9>\Þ‰8AÃùH­'2ʃ˜\¯D¤ÓÓw]Æ=dÄ{YágmˆI£áÇŒ}ÄÝcßÌIb‹8ÞÙzx¼ãz¸,“Ýd’»zë/•†²¢Õ†ØÕ9¤ëÅhŽß 9fah3u â¾$±ZÞ%žAb8暀×4IãAµ¸²ÕÃ[}%h‚ó[’BjéO•˜Æã‘pÇC +èn¹o&å§.$j6µÂwö"Àà•”²]àéäXøèüé,À<0áGœÛžO +A^™yás„qD2J@ŠÑGÎÞ¹3=© ƒVðÒåÓ°?82øN´‹Ž~Í°x'1"òè»ÒpÚ‰1ÛX>\èó/{`øK^Z`¯·îÚÇVÀ$¸ÜyÎàÓ.c‘ùMšG]^ÄfNEV +ßÎñþ˱ä“ăÑÍÖ6^ŸŠWâ³ü°µ"wÁˆ~h2`µ»€ +u +¾÷'/R0Â$ý”sgs¶õ$›¿c'¨~–àŸÇO•˜§‰I &æ;Fˆ?¿÷)ðªÇ©„’|ï¹ËÛñ$)–ö^‰: Ÿ·¹YžQ«ˆ“ˇèÃ"~Ä\ÚI¤[Íã&¡X_>u}N?$%ý”–9ŠÒÖÓg]þ*šÿ¸ˆwºEuÎiç×ý" Ṳakw¥É‹ ä;%5$ªçáƒ#³£°ÊË#~ìI‹Êº+N_›}VRŠ|e IóS’>ùã~«WèØ +xjÏI‚ + z¯¿ˆ¸Nb¯p+NH=4¤ÁyS\èÝÉ(жÒY²2xcàËæEJœÍqüT‰E1¥÷%ùm1êÅè¶h!MÑV¯0¨¦Ñ+À< +Y4vÓkDNvéëÒÆŸÂ %Ö5Uú%,jéq<:‡è7m?æ~UŠ’U9ò‘´ø”æKТ)ò¤aÅ×äœÝ ¥¦NLÙö²|Ȳ&†”'›Šg€ý´,‰³cNÛJˆ@¾ôI½:jy[¼ç7®O0ÞùŽ–í¸‘óð$ºÉ3¾“<¢,_œrÛ^À{¿ ~«ï°(Z–ÖI·¤Š°D†˜°D<«wRWÿðâÚªÌϯDx¡Á²ã½³KáN´¿Nr:‰ì¾`%2?¿Æ»“  +ª¨ÍÙGó‰ƒÔqÉkU&€=Ül½(– Hì‡(Ò¥ ¶‰|¶{Îñµ @¯Y­ )ôŽiÌ¢¶£Ü7÷4-FCÃZǃðb6¿ìըѸ¦åËpSωé0Üi5ºhS¶ÉÌôàìé´S<òÙóàÅËU +ÙM•¿û\{ÙHf&Mó +3wS3ÇnŒ}[ í ¯I0ݤ)Öd´d?zø¯óeÂ[&ûéó†­ú˱›BZ.è÷b‰±D9 U¹ø7¿¾Ñð"—wZÓ‡&uDü‚TÛTãRb’~H³ôW¯Ä2DQ˜ IˆÄflv¬+˜ÔX™Ó×ǸF“Äï‹^<·l='&XÉS[ÜèÅ(Æñ¤ÍÎÃ=K½“bGrüîBsóy›°¿Õƒ‰Ã¨‹ê9¨õŽÈ¡R)}„v¿‘pîi2O¡ Ø$~ +‘У¤PgŽšõHÝâðÒȲò•4|ú^M‘øˆ]F¬Œ’`±ýå r«†'Õ4 $/ELÒð)Ia¯Wå!šç ñ¶Žp³XeZ*<ø¹ûpð¢j[ rìB¤}÷r/žï@:÷>ça]Å­NœçaL‘yù™‡9ÕGP’Ž©’¸ +bë P-›Eê ´(Üæ› ¯“R„Å/³èÊ‘DØÌì§@=òm›ÀÄÅs}ÓC±©×8Ç®ÚFìœ Ý‚Ä>û[ \ ËžÔÔM±a9ÈÄVÛ1‰¯XyZ[ø€ Üî$±$Œ3¶zz;Ä-tq +™$í½þT‰Yb5„›×]:5ñž™­*Iû¯Ò­zÀ7r²’çû ß9U¸Uì÷ã›”ªP¸:2xPj¹h¼Ü¼wÐsN…ý†ç”™zs®ù-·\¦:*¹ÆAvòûPÅÉ’ÝA‚åéàòÔ„Tb®â¨DßrÖ öTG%¹!Ä@ÀáÛÐoÃÅS Ø.ðBŸD&5ßõ.ª–“Ä`p?¾Y"Ç+«ƒ”¢Eó¹"ñ1r.,ÊñmÜVÃq¥‰®í<\‹‚‡W'Kma…T>¯Î’÷:,zǧ +Wæ^IýrBÓïjÔ*ú¸=qì âŒq9Â:¸…ð÷âfÛµÕãÒC5"¬¨V…—‡Ú|R~,‚å ´ÆÚ‰öý%D=¸©W¬–åŽðv–i÷ú!2I=8­WqÛ!õr?HciuütKâO ¸µr—ˆœdâ¤D­6XI?E`yù®z³c^Ð#þó­··“‚7K7Ÿ+{Ëb§m#Ž0ÝõòP㦣ô$Ç=T·G!"*>¨í†@]DåDÆŽÑïc±¥6 =Žî»?Y¼¾_ô¶”cÉYºÌq&UÙÍ{\ ê€pÖàåŒÕz𖄃ýxŸ\D8)ñw„4\àÚ8ÊxkÖ^/r4çñêÁOSø()V&¬¢–A©ja.b–oýSˆ¥9ÑXA¯&Û>sµ®®ÝE¯¤£À3³ÜSéjIö!6fï”DFU ó§Jô|*fI—Ú«£ï§‹‡Üù"+ŒA¡HܸˆÉ(G+¬ ]KƒfWb&­BÙWâ$ÚË÷ñë gºåê}Šc+NÁ7bxcu£}tqú'!I‘—Úo¿, nc| _†òäWm„üeß•¤ÛüAÔHZõìÂD!É ë•Tò"ýÔŸ¦”õTXnÞ†(†¨> mªÁö‘@Mïø´·‹¤ƒÂËt²hü·pÃþ†wÓjaÈÚ´‹2ˆcTÆMoR1¤9*-õxŒ’ïOQ6¢ÆBÈìš~Œ%0;{.”¥˜Ö¤³O¬ì•±+A‰v%èíJPÒm%ìõôÞÐró†´¯ý)‹âÅßÇòó߃n‚=Fo)fLš>ZVYâm“JÚåPⶠŽ§'¿’/ü†Ù¾êe÷$ùþüéFdQ¢¸Ù'Æ(·;$ƒ¶n²Úfk[%³’ÏÙgÚ®Êq~ÝHñTÓP âìÅElÓe± §ç‚XY£}˜4u#ë(&å¸ë2&Æø¨çÓÉ­t£A‡(†¨!\/Á¬JÆóŸ‹›âž$;_“¸¾ne¶4ÀlÓªqŽ‰ßtÏìëAÊq×dsìŽz­ž$ž‰S”¨îç2·¦sÌÎ>VŠñŸ“ntÙAìÇJ˜Úg­„¹ê×J0ºl­„½žO'·ÒÝW‚ÑeH±íËÎÇ ¿)³E2Ka羸M5°Z5:äAÜÖÂþcéíNÊñÉOFo¯×êI’¹8~ºYÉšºý8Ž‹ðÕÛI²£2‰kô&·mζµò?ÏO‰1€ÇU»É1†}¨•Ï;§LóðB†„/µ;‰±úùm£fy§þ”ÛFÆFx£"fîëˆÖ*î5þª ‡X¿cx|ÕO¥Žó¶I/8O¸ÔÀÏéÖ/€œ46Á²8ìo5qÅI1YžäȸhYÖžžÝµËÅsd`Gð9Ö˃iœ:xñK:ʼn¸˜Z$Ð":M…1ççB TD@á¢ý”s]…Ó×VÏŠÜL/ì‹Ä)rû-º +q—ÓäÄÔ$O5þ²g"»³´Ÿ²ŠrDòS3 ƒµÊ²¤Šüe¶§EýÜ©ÔLwí¤†ïŠb„¼Ÿ¸H=Ûœ7\545÷:žmMÈTYɽ…ÿ¸ÉyúGeZâ¢ÛÇÄi \µl&8Je|˜üV ˆG ^J-«kǦ$Ëó»jÀ\‘ùÁBÍüÖ€vœ—³ ä.K‹öSnKç«3595Vk›’64ð2b[×ùÕ· +´ÍYy@‹m|tÔsÙþ2.‹v\œ$»äF¼™büªûÞFÌ¡ª€µR‹zgîkšã0cÜkó£[Ô‚óq`=Øé6P$g5þ¸LbÈOÖY©+t—oÁoûb8×mgY cÕ$Z¨²Wò±y'I ª?Ô4â- Ã4¶p#ˆ©|RRëV¼rñ4pøÊ;ÙJ8‰RÞiÈ>FrRûTôÄq­è±Ê߃őªAó[TI•†¿sã¤~ &q%wšhØ°æÓJ95'ã~Õ]D{ѱN`[ZS¤$þõ¢zöœÛL3x§ÇòûR»O.Ž/TM"ÀT²²,O ¨kž¡ðt{îÕœÔׇ7 '§ß”6ñÂ=¨ødx)3NÛØJEwÀe p»–©¼!Ö½¦—·fÈ‚³Ø´Ø´Îóãº:¬9 Wáд8ɤm½±“:€ Õ š¸hÆ6Th86Ç¥3VÛV»Ðo\ÝöQöá–,$†£¡™¶ uJ¹8šÞ<úmÂfÙP'<&EÜÙ¬’a¿;1tÙÓ÷ Ù1YTú´,â™ålæwÉgVBã£Ö¾fšnœM™£ER/Φwqxsßâ|Ó‡ ® ®Ák5”Ä$`F²½c h˜´?‚RkØ{Ì.áú jÈ{2œI“mAT}"£6Ò‰ŠJhwü±Ž›ŽÁ “8ÓHÛLή KîøOÿDÖÿùO¸wúßþ‰¾2ã>ìäî­pÓóz¨ŠŒd©úr€§»¨ráDÙÕGAMÙhIœóÇaE¨œü¹óê Æqb^Í ÑŽÏz.T퇕}G Ãg›Ÿ#Í–Û[Y$‘æÇ7CrnÄÙÁÕĈÇ8Ê{6ÖÙ,cŽ¶+ö?ÀS`ÇäçÐèh³XhË„?-Ùæ%%Pº£½Ðaµ€»ñ¤¹t4•Ž&Ò™iÌ°«ipä50ZxË5# Ó±H!ûæ;_R³z@ÅgQd¿Z,ÕX +)±= +Éd-}üÎáU™v„šÇxð…Óm õž‡gåÁ¡šeª£[ÑÞû¥¤uÆW{—ç(ߺò6ªøèÙT©Ä£: (È ïä:î(8#Iyä«*2ibæ!©ÿR +ç6ÒŽùÂ[{EŸx««ÜÅJ×_¥ü¢ϱ+ï‚ÃyòiòÞ%ŽR«€‡©jÂÚ^¥Ç˜Í%)¯G)B80„’/E€5w|ƒsòTOa:r³U…-8ÐtÅùÃY‚°l2Ç.,ñ:[4Z-=–Äñc—f¼Ýꯌô’Õï“!-‰0­“]Ž­èþ‹¡üƒi‚ù!ˆ”I—^ïs÷òOMÈCË®ö8O:SÊÚßTòKiÀ˜®¼Õ{°z²F9ß=õ>~}”V¼Ä4÷›Á4Šá½KF–HN1=zõ,ÝÆäÁnÝHWy—ì(=zõÅxËÆ—ðMfMôÑó%±äõ ½l2-ÏÉ­J™s's{â ç¿(§åYók#jÓ“Î!ûmV²¶+n0y”ã¡Éûb Í4ž,j¼¨iˆ <ÀøÆšõûÑÓ[•cÄÎv2çZ¡ðEÒgùÑÓßÍÉ´øq—†qT²­øzä—”;²“JŠÃ:ÉŽ˜j"ØÕ¶ä—R¼Ãã™þÂ[-~×qãÇÆßpCæqêÃ…n«Pხìñ‡ÉÂMzZ¾=¾ùì÷¾]+lƒóh·ÿ€¸Ëw”>»÷ÅØOë¿#xE¬N|ˆ¸ÛÏ#Øá&S‡^E!Œç +ôÞG@Dpˆ:¸iWpõ ö/ðèscL‘£{¹™ ƒ©&Áðõ8$qœÂ¨vùïãÑ…[¹íþÆŶÅø"×QŽõùè{SkPc#‹…œðæçŠßî/)wdGóqî–d‘¦¤Ÿ•ØÏѳ‹É¥ƱÖkíVbð4*Fl&­^ócSÈÐ×>­«ÞŶӢow±C ÎLç°,‡u=;ò,Zcðà˜ÙÁjåÄ”<åÿb\ÿ­.ɪîPÞε4¬V\¸µ±7ÌKGɸ ²ìÎK&xÕ‡¸é©è(¥3)Yy¸ÌîP†-{qãè·É‚WÄšòÔûKþ³dvú`g¯žrl¥Ï>ÜÇR-µ|+rßEµiËiŒ5ÜLÏ +Ž{~³ÖÒ[±šTWöÖ^[Û][Þ ¶U H"W_Ù#n»âIc¬ÙfD¤¯Ïnl;ÿqëä­ +í/î7M‰fšÚÅ}ïýj.dºpÀö àX•sW¸&µõ¼äIŸg[¡Q7¢lj]NH7ôUxªÅ Òg3êëK?‰¦™HŽ,Ï ·bh¸&ï7öH®™Ù¼].·1=Œ&=‹º4ßÔ +ýz3±‹±7bîüj.¦Ý=>=.$[w‹­õ‡*ÓIàûØÆ1³V…\K‡î9YŸjMÅ.æǶ}&]™¯¬eÔüÆÚÊ…AOlÛ>É””­tÈ…µUd:¦Ë[é³Oïc=m5:Ru//^Z'”qÿǾo©–YÜ`@Ú|B§Í;ø—BËÿd¬·³©ªFTÁkÌž>¤ÛB™‹Æ°škiË9äâÙ ¨?ß{tî£qá]øÓ¾¢…G§ÞGzjðN™J8ÕÜxÄ@Vz?íµ¡ mîã‘ôQ\‹Ý?¸êa&ãFK7Í{”mJûàzÑú‡L­ãèϳ|[ çF]¾½lï×ë8K"õGVô/Q¨9Nùÿ$iÐ(‰oXd\‡5ï&"WÈÜfbà4‚#Pf’>­9=àVL…w2 ¼#ð…:Þ`B?ÈŒL²Æ¸k˜L¨ptåÚêÔ‰ŽÀ—DO «h•š-M#erlïU‹dW1IÌÍNdȱ*Nò$ÏðuǢoÑ2sà¼øvk¸ §-r쑨€2ö!ò-1²xKÒx™Âî”[Múˆ‘ +DiÚˆèõhËL2R²ò©Q8EËœVMÃroQ#f°i«ËW©ëè:7Óné`­ ÷Ç}`.Q3Ñ—®m°'دa,–¤d…–L#ò‚[áжjÒÕ2»lddÕ€)¸\@‚»w²]µÉIæ½'™6¼nÇEÃf8e¼Ôff6áh¨DÆ;·ŒUÛóŒ›9G✉YNnßçv9xˆé8‰y}KG°%°màÆGhjÖÔ ’­P¸ªG»¦2Zñê͉EÀzG”ó˜hä lAÉiDÚŽ–"pÙ†TµXššM!û¼W%³QFdí÷Ò"6ᢈ¾–œF( Âfœ.Ý©-Kâê¦B~îò›ºmju¬ýÚNbÁY½q™d¯Ïû4n4³°ŽmDÐ [ë"~îÄ"÷•4_ô:ì ™/e]ÌòpcIÉÇ¥-R\íuIªæ’rm- â.ê%ˆ†lmºÕ¾«ª²Í* SWµ>Ëõ¹œýÅueùùȇŒ¡²ûL<ï±`5öP8ªÝá·Fò)$/4ÄTâ{Ûë"±¿lY n‰¸ÇÕ0SCΊUÅ\C×oºaWQ³Œì…È•jC†ä‡ï©ëUäd8VŽ´Ä,Øðd„G PJU¹Îu «‘Ý8ö5i³?ªâQ,·]Ñtd"ãœét×ççátnʈ Ŧセ‘}gT¶¹ïBfÁ¬ºHZ67¬–jÝió—ƒ@û|ošlrZsÓ„ïQ‹"p•Ú‰Íâ±ç^êM n7MRIoDñpõ#¼f’ƒè& 5ZK}¬ÿç·rDØ„’¦2ÙïmEç\%Ï„§!PÛ6þüÊœþüFÞs>dArú0„&sï 9IZ«?WjFž[Ý‘ÌH_Í^jx\© Ára±81§p¸Í¹TËQ'þi†"D%µþmñ¸RqGW€›FÝøHHHŸæÖ*ʃ‘îå#³Š1§£.®ìã`À⊰:»Èî}ÄÓœká¨á¨ŸCë®*ûcï×Ê¡Þ8SéF4†²¥’RÙ¦6L—Â0XÝ‚XÓVK½Á2˜¶¤rµÄ%€¡.Y SÓ«çl±5S:U&\Ãg’MÇT l5»oL¢žEEˆ`fÄ4ÃÔÌ´‘ά +ê ™nÇ +±ˆWC´Ãc¨SVÃÔôê9oa5ÿÂþAldÞßdÿH"Âxu»ÇÃ'“鯅ÕÖ|¤;±|W;rü^ܽËÁÓrP74<£”ãçY®Ǧ:Z‚¡ÓÛ)“%Nù-`ÈNeÚ2Ä)Ô³ÿêíÆWdü•xL­žp°#tù¢ˆÌ?Ÿ3ŽÓÛˆ™c‚e ÷BŒç ²äàýãÊÎÀ±sÊŒ£ÞQ÷·%–!®.l ’¤xØ3Ä%Ös tppRÉ<þzõç-jŸÉˆ –à[$A‘ó•!fÉ%l¨hYÔ¿çØäb†µ´¿úÔaÓR”<¼»L‹hä· š%{c†8…zößzþ‡ÿŸºwÙѬG®Åž Þ!‡m]Þ¼“ƒXiàÀp C@ôPÆ‘¿½¹VÉ ÷þ²úX¥_r ²2¹yg0Œ‹ÄÜý3¢ HpÞaZ‹ H€1šqØnFá–ùMîµ£mm»ÃerU}¸:õÛ pUâKë!6*Cˆä«®i–W¶° jÚ+#(‹w5ëa þè'ÐZ…{íûº³ûu¼ïÇæ¡ïZÞ»Øa‹Ø™›ôªåõrŸº1L‘ÿ®h/ ‰oZ`*ƒÖ HL³¢Øûí]0ÀiE¬«27S€s¬º¯©^%Ç5 ‚U\Uµvqzr€®ÈEb|?dÄAW3‘&x! mÙ ¸p±‹mŠ‚µ2ø†EØ5Û*ƒ­ð{ FÜOúÙë2àjÕ}æ3Ç•¾Ã9ošÏ¼VË& yx‘ð—RøæŸzÆŒï‡þISŠ•hÆó¶ƒqyÂþ=B²¨›c[Ußnn¼PÌF|ÛÛo¿Gü쎪 ¸u€y ÒA¨ìh“bUÝ Ø%.Ró°3ßÀKü›ô@¼”‚ûâô@ C0ˆ\÷œÒ§0ÄQü‚«üj–W¶/á¨kb¦Q÷°Z5ðÐ ŸÄçÝ8áL0ƳÊù58!‹;Îø|´M·üå¦ ÷Çp~®‡&˨ÉI̦½INXíÍ·`9È5•p4êÞû?J ^–ô’s©ÌëK¥ÀÙ ná·øêœòÌ»À«\Á Oè©4§4µŠ¾Ø^•®Öli€ ²Áï +»je°Ù~ó5×áóe«iµMðÇj,'€,mKuê¾Ôâ÷˜ø Mz¥xZ‡?fïák%(ÚÓU_e`óe} ×õCıª¹·[]ƒŽæp5}û>Iª½*®6Ýû®I§`Î1u⓾´Á‚Ñûy‚ÒÁì·mß+ÿqÕ:’‹{ׂQÞn¤ï3ë£F=3ua?Gå4f£&h;`¾O¢f«Ë‚³U#°Þ¯›l'|¥Lö£ÖA«ªúÇDä %ÄœôG &vÑþX¢" ĽÐm…ùÞa?«=I”"™†ªÇùj”W¶ï“x]ßëZàjÔ}&÷AGïBEùh;¸&’NDV‘0ß«d΀°°¹œ-@[–”bÔ• ~F'ˆ¨¨ÃØmïíÓkÔ¬ ‚®0ØѪš˜ïÛ|u6UŠáíqì~J…°k(êb…òk‘u7¤p` +ôYÝ€—HFMÚºA?ÇÃûâR0I|çȼ¾ZšÊ``Yªr £YØ>9t¯Ê€³Q·þÿ1g¬Uu¸ß¯ú<§nãx‘ Ø š+×õhpܯàL"…¸ß¯|§®½è&Œ š© !¦Yœ]0˜«ÔªÌ‚«Y÷1Pŵu(Àì5k¨NsAŸÀS˜ºŽª ÓCm|¾‰Ð¿¸¢ÛŽDÏûÇýÇyÓy•_YÚÏo×±·ê0Øj˽×:ˆg!·=x…F´~{I +3´ðLúÈnÀ:)Å(@´P…Œ2Xß<,zóQ@'UÕåÉ Êð6.ƒošeÁÙStö£ˆjWe…â¯f=ŒÁTr‹Hʹ‘Ö8˜µmÀµFÇF2­0 M‹½Û7SUò¾‘Ìà˜.˜ÌžYƒcÁÕ¬ûèyXëÈtzŽÂðèèb:@}c?††ÏŠòBÈ6Í;Á94¶ô,ÌÛø¬lm–K»}Þ挩ʀ«Qgÿ'E­œƒ¾à™¶„ƒ–@¦’…ÚÀp¹` Pª„r Žýo¿Ÿ„ÂÔ4)ŠiÒÂLë×Ç{Éð¯Z ¶šsï»h㯠 pSàûÕsIBb„ŠÊ8“ Áà ïæî`g=;yyË/äxð‚׿~ëq„*\5qAN¼•ý>~ÀöBBç½@#¢Ý‹VI¢gæNóN0¸ÎÀtÆ\œQ„²Q/ +9ùÌÙ!(¤^;tC¨:’Z’½êÌÔ˜/›¯ÿÕ aA"/I|ÔO'ØÇ) Òi¯£Ó^8ƒÝ‡ì>¼óp]€Áp“‡ãÓN9r€çÙÖªtˆË"ÓQXphÞb÷"diËYEï(÷-G³ê¿ô¢tÒ +ã,ÊŒQ£—eØÏ@‘÷®ˆŸzœ½yËç ô[ ‰söƒ µ÷‡18ôò2®o¥xC†_áÌŠ-à¾ÏHß:ÐàDᤔ¯*Š4è corÞè…v\YÒh´þ5k0CÆ:±ƒíÃê=Á&ò+ä¼ø„€f_é +\p06}c÷¨+È“{ŸÍÃzv³²¯ÎŸðËü%tî9Š(­dpÖ,ë£ÀYÇÀ)bE- câUCkˉë­/\…½Y­§ r¬PæįAÛú€ƒtÎ!Š=@¶CÚ\[¼™š »‰Èê^¢\\°¡×¶åupHeÛº&µXÅSMÐÏÚ—C,’1‹4Ãlw¯OÜY˜M7ùêÏê"ç£_€²Aà†)¦àÆX(3óñ0[SÇΤ4öåÁ•_Žx ׌¹gÀ¾† Îp{à¼'¤ËröÐRW…>¥ê~çk6ZÃ×72Ü>µïP¬C—®P"tfàPyÙfŽaÍŠÇZK†£ÖL†‹+ïbdyêÎ —¸Î4¡ÑÙ€i¬"´!ö*‚‡ÄŽ2ÌŽÁÕm¿m/Œ%6aâlNêÔrÏLBk°HÝžä…—Cªp²ÄÈF´éâð3¼œsœë@òOi9”a±<=íQ¤ßòq£Rn°u/yZ7`Òð|xµ7«æSÎ.líòUJ¶ù U;ù‰~P$\äKpû¸±OÂý!c[£_T[EhÑ|¶¤–º k=˜|}xë•ÅKú?!ÔKÚ‰ "Škmo¶´KB²Vª’£ÏµŒE!‡ÊrƵÂàz²S’ÌP£wƒÑÞ^^ ŠXÞ•õ Æv’¤‡M`4ærRù7–øf‡§|NQÏKp|ÐH@QPõí|þ^>${ˆ]pÇØh½³çŽ +Mh´ ƒ(TŒÁ(¦SIUŒ¢úŒ™ãn}ü®¨HˆLsj°Ix3µ.y·D[ lðs¹a0KÞ¶>~Àà *8‰`P?6›ÂF¨)üA8¨™¤Ë`æŒt#¨Ñµ+[’ÁoëûóÔÛs*WÌ'õ„âŒ?ŽH¨áVá‘<}eÈ0¥c00<®Ì ˜µZZQ.%+‚C'Õ6![çWÛfq¶ÝŒ°i•’Žü^µféÞK+WèVô²­Z`”iSš™ÉYí6çÐ?AHÒ-§›Ø'd+ ià(Îtãìî+Å·¿Må-!Ú,ÔgÑïÕì/  y_ȼ-•Œ+@9À"²î\pLF¹W­ÏŸÀê§óe›(Æ&AëTq-ØÑT:'‹§‚ ½KÅÞK­QnÏ“ífÀ`‚S’N»J¶p&÷žÃûb]n&}FøZaòÁ+Lar¿zRùÇBrŸcó&h‡zèˆxö4ú>ÇT}<ÍßÇ+!øÿ‡8}à{±<3<³Á¶èKnP ˜XG§†PVê‚ (UóA.Eû¬×Ò)<¸ãZÜ!úz‘a¢n0YrêªLèSnI +ç&^|á¥yzÉ4‚ËT9Ã&-ЖVÎ Ljmý³ e€{…[>˜¦­\Q4;°ËìgFT@”z²ócD ˜˜uìü¹‚ç_Þ‘æøš;æÁìì5_3ßšUCCy›üO“œp˜D/‡Rô*C˜š2Ñ0ìäÒ„ÿÐSô J—½—ÌøÃC0Ã&†b+ƒå›=!¶Ì'Hè +A¶ 5G-äà(QdY¼À8|HGZÿ`Ìì4iqI èŸ¥Ñ Vgò!¬hgÙ98á×-”Dùéó‡½Däai¾ Vš5öV«1.Ÿ2~÷[>Hi.sD±cCã³~“šä]ä·.¶¶Ÿhþ]±¾¡ ÛÆ,7Œ"A*iÏ©X ïƒx®AÇþT/Ô©b¯&!¶v EÀŸ%! +#ꇿ¢ãá5×ä‚1Z•¢P¤E2B8é@NL³G¤/@rË—ò) „!u ¢€`¼ø<“pÇë\ýÃßÉ|¤´ƒBÒš¤Ò˜IûG>nÁ0;8¡K暟0#Ü1¯zt)ìÛÖ±s>#³›ÚJ’ýV]指¡ ÇAŒH„5°sCÄ!‹ÙÙ¿÷mAN,ëÒ°?bM"¥~ØÖØŒ³ÝØ›vaki³ûñÔÃ=ñéåpÁ§¥sC^ `ɵ-Ó¬¨êÉKfï­%ŸbÀw€^šDûi€ +ù¶>¨äS¿¬)ݲywVª©t´dNZ“¨ˆ®^@$ñØœ]ЕÝ)ˆH¯$³08«¸ÈÝÏJó%§ñÖ’Ž1a7h:{@m!cLd{lÙÚ Xj-Ù@¶dÌìêÆZ³³·u²¨Ó*Utu¿n˜îžÕLmgøm²}VäÝ®ƒ#p+LZ¡}ô¥u"èÄ Ø€:PEÊ…˜+Ò·‰Ø5 X‰ƒi‘ýƒº Yv +ík û + ]î;ÔònWÀžM¶Øµòði3Íû$]~ØnDÑZ4}’Α›¥ÍÑ=Èo +±=yŒ;³Xv6Áµ±!IÀ½oí~ðÀiR&¥a;¨4,«_Cv2Åì–6QùÖ¤a}9ó…ZÏ‚­t´$Øæ±%~¦”ŠÕÓ Ù¹ZàÚÕZ–Ùú£JK7nàvbÙÕR1Ô$acäölÉß¡§–¤³%s^W7Ö’˜}½­’EÁV©‹‚í˜R°ÕÌItf_&eZ]6l Í–Op+,ØJ÷Ñ_¬óðÕWKÂpEÀ䛸 » +¸vö,líÿµò ÛA%ck-OʳVü¥¼£¹¶lÉß![é¾E ë7ÀÎnÝÀK‚ˆ°fg'desðfik„Uò›ÈØMÁŠï^i£Ñ©3ÅøÖGi5Ãã”}ʃZÎÙ|‚×ì%Ñ9´Ø è—Š]âU‹Åov¨Î]ïDst(Îr¹ÂÐB…(p +[ß<-(þ4^Q³~:ò51ø Ìçš©7-ýTÁ¨/€x«À˜âNÊJÂGäS×ìÊL.ˆøîf1ÙµQðT$7Á[çOU)šTö#QÓŸÜ]æ ËÎ’Ã~#ô („KŸ4Óu%v¦Emè3¸\üÇbyÚU×?÷´ƒPå}Wˆ\`tØÆR‡Ÿ6O&§ˆé j|½?uؾ¯¦&>‘Ý—¨z Àe2fžpË°±Çoæ™änA$ÆqÓÛ–žñ¢|*Ê°d„7* Q10 j,e0´¼á0^¹WùU\'°ð2‘O¹ôëb´Ù‚T{ö©D²ˆ1²~(„-”Ã(L÷ÌF†Ý‡;7ül¬É%;J£á…‚¼ j×ÔIøxš™¥µ’‘W÷ Ö"ü8À”ë &yáç°Rl‚åE9¥0W-»}JÍôNfò%}Ì宨îí¡y§JHyë$hÎΗ€ŒjogÞ:ªLméOÎâôPÅ–åø©™ÖS+òÚH¡àu )ˈ",UtN^YY æ«_¾ô3´ÉéWmÍ’Ï z}©[ ”’¨‹ƒÒ²'ªXimÁqjýUFøÈ ,£‘ÒWû`©}o!<å<>U0«SLtžR»ñË–™E+GL³ù?Lµ!+ðwä9¡¹oyÙ( ü"ÇRù$’š"hb¸9Þš}\äíÓNþÊU ¤O, +Á¡7(Ž' ƒ ¿“¦¬ðSâÈP𔣬HªJiøÆðB÷È5Ñt“kÞ!X¡·É] ‹hUƒþ ìò=—¹UyêÌ{ÈY¸Ÿ„E.W<þ-. ë‡&´)‘Ä?ÇÖa_aÏÅ¡Û×{o!8qâS€ ¦êHEÃ_€æœZŸ?‚R¿ib‹. M¯ ´_÷~=€TYØ[4?a¾»C EîRÌâ¸7û;#¼X:èÌfbÙòõõÖ.ÉGDi]Œ2ØG”kÝmzí¡‹'ÂÞè_el»¼M#›pYN™ø -†qa 7X³°µ¯g¥vÿ×$Âo›>õ|ØJ›Ðªta³m³°Õƒ[O—ºÚ#[žc€g.× €S,èˆbϧB ™ÖLŒÔë͵æn5ÆÌ1”jµ­…šÄÊÛ6SåÄVËFY«ùG'ÿ¥*L+æx…**L$!¸T¨‰:ý rF1ÛE„ýæë'¬QCiê¾u¢“ @4çƒ*Ez—jé¸LÀ<š¢´&§B‚ûPZGš ÂxæÐ{B¼àñe7‰¾–ÍõÆT¸ˆ4Þ-ð.ŶMÐtfâ.½ÀìxΟÀ1GO÷;u— Ñàô 6y¯¶IE\M­ÀŠ¸Üᶊ|æ‚Þš÷ üjèXÊáF?%ë庆¡¦‘d (4”‹ÈæƒÎ²n ¨ÓlP€‡Zýt€à#’)õ-¨T:k¬”Æj%1í½à·‘³¹A//.Éyë€(ˆýWÏÕ<ó«W%¼·{dšbÑ–©¹cˆÏ©˜293Ô‘Æ,)È%”eJ¨²MΪ ã$U…84M­m>J›¤Šª +Ab­ïOMù­ªJN§²Lä•ÕTn‚` ©¬PRÑßΣÍ";0š’²É×/lÞ ^I´!¢¨¼"·ÿ.úQ©89£jQM +˜ååÚáÙŽ¥­œÙH’î=.äSl°jÖ©Ù.1Odz! ÒÎë§ +”d|ZÚ±ÊìJÑÞkÛV6Ÿ8ýÒ¹,úAÙVÐæïý‡’¢îpµû2t mðgØìÎ1vÏ [ùÖLwšdZÏÉ_ºJ^üñ{/~QD!$Ä­P˜ +7É—C¿ÿƒè%¹¤ïõLê ˆ y”$Ú$ºK1^A•• èã Ñ©S%ŸÁ½ÿ° ­–UIªŸêµÈ͆¬÷ºØ…þYÚwAÛÀä+Ul[ê¶!б¿] WB8O>,ú‹¶”]示9¾¶¼å£wô²NªÓ¸*»ñ‚®ß»¨ØÐ]ÅvrUxaO¯À§¥•áÛ*'ÕÐIÊ\pÊVÑQšc­éDÇžd/åèÅhÚX5K¾à³×iwº ©²Q--nùœø*`i퀜Œ’~:Á¹Çà½Öe"ô) k lÇ€>¥½N„w+ßì—}êìïÖS‚g¹Ìƒ _§ø¯íÆšU®ÖSW³RŽÚ¦æ£€(Õ˜ä‚F,DIPÖ˹ì ÎÔ*ÈБlÕ [”¨ù…éôH({ãqŒX“yœâ}2;Ø0©4UOr˜¶B¯šDøwüñT¹@(*QäàÑÙ¥mΡZiTËô¸HzKLjµ'F„šÒ‚PdáÊA•"ܦ“ìõ©˜MÒ&XpSÛ‚Þèè|™3‡¸çú”9žtä#åP ÑìžB}mV??UÐñš.…ÅÖl·ŽÎÿV%˜ÞAÿ‚d¾‰hJµ ¨VzÓ‚Vš®§ Úå IÐy»fEñóãG—¼Ô¢r4hË9š¾x6JYGù*{ÎÕUSò—§xNök³ØµN +¾9ýê•^òÝþá X 9íOPíj3£{-P ØÚúX¡b Oý4æ[¶t+Kg‹Ðhˆe¶ÔkÞz[h[_'¤c†L¡?ššƒKi8âèlÕšé¶6îMà•ª[]=Ò4G¤µ3Ç÷€B¸7dÕBfvõbÍÿìëmƒˆ9å}‰g± 8Áv¬í:Ðí:PèÝ®ƒ=[º•¥§*¡}è—lH¢ò--rH·ÜÙe0Ákôöf +S³[§B{C´«àø6ßÒJk·Zetw(„û§ȆQ.²WQ˜¶]’ ®¡›¥mÓµ­’ß­¬[õèÂAÉ&8w7⯗<i\½Uoh†éóP~ΈÞa¨ 2)#I¡\wJF0Ö[>ßn¥©àyÒ²T;.˜íå´õ‚|¼Y½™±®ý=J3T`ÔjIÈ £RüFfo(ךÍÑÛóqŒÈ»{S6PÑevM?æX½=×Ê¢fkÚ 5;Àv¬…IÖZ˜ë~­CÍÖZØóùv+Mï“ž ö: +ÏlË‹ðé]í®\] œ»|•6iÁªÕ’¸­…ããx//ׇòª?óÉïw÷O7MÉrq·—!_½• ®Ñ›¥ms¶­•Í%áÂ3£Ú1bÌ%‚,ÜÈ+]T”«UŒ<Ú±òÓ%:ÚÄ4Ås 7ï‰ú«‰Ã¯ Š袰fÖã®wkÓ¢‰ñ6ßÔ‘ìD?,êZs(µ¾b‚Wh…Ë–á‰j–ððlê”qLœš·Q&ðgG¹%_Z µL%¸^|ÀÎœ”צ 9kŽ*í¡(Ö¢*½.b’Ô;Œ^è¨L^óv2 ¡pÑç8¤ 0]Û°(2è÷ºÆC¤å醺®ÃŽó*áµó·ÖäŽÂ +È‚j2Œî¯Åc°±Ì8PKòcGÇâÕ¬5þ°GÞÈj ‚À‘¶ï+!DqŒ½Ÿ8$Ýt•NÈ›*jŠ6„3ÄÆsÂBûUÍX[9 ÀøŒ:÷M +‡ÐÑÉ +QO4t-e ª¢¹Áiå òÎHˆÑ,D?³E6ødÚŒþ¡V™¹$p0¤á"'ˆÜ’¨qWuÜ(â5íÊW5˜*'SdsÒITÓn2ÖɆAQ»µP,J¥Gè´z¿SøNmí¤«å+']Ü°XâMz31<:ùaû²P¢Ç‰[šˆ& ®\‚,yhºfRÃ~쨓àÑ;ŠÍ&~õ<<0^QUË^é/|A-Œ£UäÀÜrf¼ÏzÝH…Ã÷vÞtÁX¤hfMí+CÚY¯b .›ïR?CEÜ bQýÐ%¥%(—^Ìê,€áFI\Þ¶¥ ú +ª˜E9ßËCÂÂTUôŠnϘĔÚdƒezØà· ŒÐ®ÎÆŃ_z/i>z/- 6'¾ Å?t_‚Iµ ¾¤ÿ™ô¬Om£ò‚ú, ½|¼Ð,¡¯e@‚kÛšíU§ÂÀHvug®¿ýìÌI¬‹,yʹŸœ—ÖB7[ª&yùR ©ÿAé7ö D+ÕXÖ¯ë @tM¿ÌO»>lN˜©ê’lªÌ8¡†8~|+ ž‚¹É×Ä7™T\Sv&AªSeAÃñàu!éß’3ÓïàaDz¨;ªÖä@›pãânÉ;¿-õû¦ØTiB‰'õ¸¿ ßÀ·œYeÐå„*º­£¨ÛæˆÂ7RØj•S‰Ø - „qÚ°,Š¹C9UPLê¥d*ÃÂÐ=£æ|4%<£^ÔVˆ"L® òFx€¶€^q©·ÊŠ80ÝfJxD[?¡Š ±EÆ•æ–T0¨¬%CSpk +i'*PX*—R$†<’fºy¼b{¾?®‚MÕ:Ådñ/ñ0¾ÍngÝ/F9E’ g»Ph$d~< 4”aV½ÑŠs$n›ñ°V ³Ut´q”gzrï³Ñz\R‚Áñ€UƒÛÉ|l úYÅÄl›˜àLʽÙâÖ„®ŠíÔ÷k°“¾ÞóQâÂlÕ5mœ%š®Ü:ýJÒ +`ûãoSŒ¤\ÞÁó")ø]—§VüO*Ì&(n´áÇÈ9x¿ iƒ4àÆûÂ^d:fXš§‰¸”+¯aù {¿^†ï·®£¡_Ÿ¡®ÚçPÇR¡†ç>èûÇjjâ•š&š(ªuïûá– £ó½¶Í74Ë{Ç ì+føÔÛ+À&Ç‹Bcaã¸Z”V]A"äÜ’ÔH®¦‡4^’(¡‡~q*ÉÎîJ¸}A§zv{‰ tþc>HÈ ¯H»Ò0æcýð›Ü'i66Í-éSîµ8ßáaÞµèÿo§~3©I\x•k|äØ6àðœö@ôÀlÂGÜ#±ÔN>}†àÆ÷qƒÐ÷¢Y…,Hokà㻲˜3 ½ÅÐß/iå…èÙ·Ä“?Œ}y¯%º%âµ(Yˆæšx§‡ Î×dçCæÜ=…½Äfñá3ôK¤¡UÃ;~Ó½SüÉ¥–¯§‡Åö´”(ñp\½XsìÓ‘NKúJî—Ã,|Š€žùûþHA<_æªÖFpcéÒ¤ü¼Uã‰nî¦*wKÚjß’XK¦IúÓg½ÖútÊͤ÷$*5X¡ÈîtÔ‘ú…Bš^ÒÖjÐåçûñ}4ÍÑçÚl½ø­ÄK|_àn™Ã6Õ3 Zn){a&ÂÝ^ÛÈ„ÐyØp5‰NÒã>3Ýž?«òä·:¤Тnˆ}²2ìæÓAû!‰´µï³u8‹mÓ<±ÑÜÜ‚=ÕÇò\y$˜ `bÞZ­)>Dëj…Ã4­‹ùU}3ÀhXzÒ]=h’?ÙH‘Âo”»´{ùžœo;×sR¢Ÿ›}÷ÏDß·ûjsЬxÚœ¯6µ‘âö²Äß–ˆ‹¦è/I¼.UX­±ØAi•ÖJÕ"¼–÷åùXjI"ä>}%–½Ëç™lR‚úÇ<T*§‰ø¿å¸ŠüP‘µß'>18³Øoá,ê¸<Â9%£ãB|Õ´aO¥a˜)¾}•H×çŒb¿K„©r_BGF"^”â<Õ9µxŠ4•³NSGr Ü%=~E á½èqŠ½ŠIy'ß&%£VÙë5ùváV13z:²ÊEV}_fª9±ð˜)m#j+éá66›¸O1{pÖv§“³•Oäµ8ÏPXŸ¹~åÃÝå^—¦<6q¤Ý{¦5= Ç«aœ÷ÅÕ!°_ª .š €Š@Í«Pi"σ9âÞ®¾Yô}­¶>¬¾âœønzøÎáµ·å{m3éq<5qujT²ú½Í/ üaHƒp¨©ÔØ 5cȨ~{ˆ®3Ù.8"9tŽ²W'}À”¹^¸q‚/B?ÜeJLÆž\…~ÉGòý.‹eÒoÛ©Ô7_çE/p[úû¿@uç?ü¥ýöwýg02tGM á£ÉÑ.ZÊ#%4ÇZ©òAÈñ6}>qDFÄcÜV’ÚcUðð#¥ºÆG¢‡^ý„Ö©8K¤û(õ~²·Ž¹’hÌomÞpÓË­$°MîzÙ„-Ù6ýqܱ•!4b¾…ê!v‘”ctqug:/n\îâ­d\šcTýý­MôŠáç)úvöé!}­dÄ®I}©¿jÓ–~ïÓó8Ëi‚G"j¦^½ûŒb ê—½LµjyʵHòTƯðUó¨²<,êJz#à5¾ß„^Ô£u¡³\+´*3FmLî‘Ï­ ‘YJz¨ +ÃÕsN¯¡hv´û ­'yT§Ýöø)Ç1nõPîÔ·ÁëÖ"‡¯ßõøóóùêâOÿíõößüÇÿüÿŸÿéûÓÄóÃßýãÿúŸþ—ÿñþÏÿÇÿßÉ#ÄŸþ‡—Yþ§ÿçÿ²/ò¢HëØNÇÐÖ€ðŠ 4EcàÁªFç„ÚÔ>ÿA’q>Kÿï1ƹ܋¥êy¤¦Ü õ-í—3z¥…ë¡XèQõ!ÜŠ5 ŠT‘ÌæKÓ&–šl↧‚ñ0ƒˆDÒ¢Ú/‹ëÓ•vïÊëÑÕ qžZ"ßK“ìÎYôu³zt AÝ`™Œk8–KÒY<˜ªN£ýs*¼šøüCÙÚ)žÚRwç•RºFúžÊin¤–½lœ4ч~9’d){µ kãê?ÊHßzõœ:ÆDËöÞ–ÝyüqùÇ–ÙÔ{¯¾o,0øÂpB^;ÝNˆ­ëqòMv-µqÀ‡ÎøN$‡šÏöÞÉ«t®„¤ìÅYƒö°ßòûgš£B!RrP£{O‡çÙ~©ÕÒ­Yƒ£†0ÙºÕF¼CH'5¼]dr oç =V€;qÈõe·ttšl’^o5ÜI¹¿’CË< Þw®jð~”I÷eéyÝŒ0[ò† ÆÖµ†Î½LvýÚQÏ’µgQ€5­ßûkP̦ bjÈIø§VÎB!K¾BÕä:X¿Ù(¬›¾GœfFª£KÏÉk4‹‡“ þUólòÞ¯ãly0ì™;ï¡u‰žz5*AȶxQz¹~ÅŠ’\„ CÀÜðœ +;îÞ’³èÁÖöó;IrgQúµA’ýÛž +ua\%ÕECë¤DR'8F_ÞÎKzָݳSH-ý—TeÓæˆ<– ÂÐj,-³©÷^}3Ú ó@—Y}øb·²~}”Ûå—$ºŒ«±&vJ#÷7š¢uN¹èeOÊÐ:m3iù)æZéª9Äæ¡E0¾3I¯ë¿^OE‚Wö¡ÿ‡BšÓ‡~dHÏþÃÕ˜éÄ™d:ÿT,ã^ûœš´%íy5¦¯'õïþ8BOù²PО©‘ ÁÉú½HX’Ú™3çåúB/'îgèû­úçÔ ÊÈNç½ìa’ˆ£Fêî—Ëõëð¶§"k¨I¾®u/›šRWßú¼d…$ØU? §éÐsêŽ[±ŒjñÏí1©}ùf”åt»xiìËãÊÔT@ä¸~ª79¦&{™Ty Ué%iž"<º«¹óå¯sD ùö¢å×á2Ò´¥“œ5ÏðhsÀVÊõ›‘0yÔÓ×ÃÈ‘aUëÜ”Oœ-…ƒÚ } 2.œgoslcv«£ü„T£´×­&×G­¿Ó„~Å” z£YÉýP +ë‚Éá,V«©ïI–Í»5o¦ÉkóÜÛѹÇä58ţǥ÷¢y6Y¾ò<+1ö†Át¸Ë9¨Q;òJ1ˆDå +“ßo¸'ñ¥¥³)©¤JýÅÔ~¥—©W¬¡=oek¡É•%¯ +^µáNg¤ö{J‚矎bṸ•Kû½ØýÑ(¼é»¤Ã~ /5£C©s8ˆSþ>éåÖ(“ðЗoFùßH, '9’§X*áAHŸœV²•=%ªüèVì.–‚™yÀÕÏûÑ–Øê¥lë*‘z½WÜK\mZxÄË*qõâ1qŒÀcÁCêt4ÆÀ·¼Så8”'Á-;PEÝÞ‹ÃÆý­<.uz1/š“Ù¢ªö‹äÔ`¼ñ\åG=`¦Z¯VJIo÷xgO©o¸døÅ­žÎ€Àà¼3Öp¶–!CÒ¶ä·{säΖäð]= æúºµGŽ‡ÿb~8‹ôÖ þï¤í±pC'6ƒ<èÍC/kåÁjp“„ðbrë|éë|õ«K~UÞÚ„M#O©ÄJf‹‡#“~hÞ\çŽ=ëÚ[q¬]7®q¶µÐ~KœÓAãÏ?ç°ãö\:àúàuké\ „ëu1?“û×ÍÊ pv‹ +'ùÓyÛ—žHïErž•X…îÄO%ñÊ;ñCŸá«]¾4ôOÌÛ"õÃxÇš´ÈƒúÉd4mgÚiÕC±“ÈÉøgÛÁ‘öÔ‰Wc:ÙÀN3ÞûGŽÞ9Nì`Ù¡ˆÜ·ÛȵCzyB 0|öñe:Â#ÇÚ §”W» j“Ð…x Eå%WºNÅзHg :†ªb’ßÕF–bk°}|J·£ô\CD\–+¼l£Mêã·ó0¯Wxݾ”'»ŸSŽágóå$ÃíjÐ.þUr…ËÁê‹çDœÎ*ñûJm“H½-þñ´°Í»8[ç“×à<ßoXEõEó¶ä{ç¾{LÏ {ÂUdv +¾¼Ácl pßoòˆ~–¨ ÒB„UÉÅ÷‰Ö¯{¡\X ñ5ö‹9Dr=ET{®>m¾Eî¬2U{2É—WŸZ ’S4rzÛþî/.7kÕÅßÆŸ#,z§ÎÁ—˜ö5x„¿§ª¡^(>?¤2^†øÜèÌìÛŽÁÁCÒð¥-ðˆ²PQLy³õ‰j†ëÐì]8 •[èdžB¥¿Æ:`á ú%Š`Fù,à Dx¹ÐÜÂ…æ³üŒÔ1hñäpA Þ'¦×"úßrVº½ ä}pã"SõãWÿï?,ŠøjQ}‚ðaþŠm¢¾Ùi¥C|õ…Š8.n:7÷ê`;–:³2üðNÔŒôMèIgU•{dûý#j'o-Ô Í}k›—ÚþQÌÒ±˜.²÷ òcGÇÒ5õÜ–ú7[èwXS.B¢Æêß©×K¢x›3K«}º× }GâõUí1ì [hé¹í\AtÝ‘%Àý"£ÜÓðÖ ‘é¡h‡ÀZä–XêÑ2r)¸y¾Ý»µ¥£r”Zð0ÞÉðS{Ž´­ßµLSöÝW4Fa‰‘¬žÚ˜KšÉ¹¢fqð“àî tuRÂYšvr,]4È:AX4äH‡BD’ª{¡ ¢K§Ž½%pÃÕï‚tp³µÝ&ìÞJ+â¶pÝšp$ÜÚþj¡[°¨Z ~¤tÞ¥S:^×8ÚðÄû‘<3Àž§ö Ò|BL©«òÑúeèTvnü±í£K¾´U Þf<Fz{Jv°ãÏQ“oÅ÷qî×ÎUüÙD¥‚ >Ñ'¹uòH¾ ÒY|¡Cø^4¯À…|5É{ç¾›ƒ?TÈèâbÏÖùy(@åZ5L4ü–AyEhh_[ÊÙL§ù}§5½LíSTžŠVCýXJŠƒr÷ã1šm*W–ž‹ÎTÍOR´¨doíÁ­júÑ'¤^ÍTlä¡è†aµ]K0ÛÔ[Ÿ¾k¡¨°Sõq™¹<”G3(’¨= x‰¨éý`ˆ~¤Ó;EÆûþ‹Ô€`0W{*[Ý.8 +±„zCrpm_¯TÏ cúu=ËvŒÃ‹ƒ‰©ê7cµ âçkZ_Û^ÝS혠ìãV6Ìuâ?¶Ì¦Þ{õÍx+›×™A¶=iÄUKmºÓj8 +WƒÕ‚æèœÑYDÏQ…7ë w¢×c:˜»ÐÂc ÚÚJ¿r¥ù¾À(~•°§g˜Â׋¾ ú`kØÛˆ敯ÕÆ£ wÝ”Lú>J5À×Eè·ºú¢Gú­ßÏÃdérm.(› Á¢VŸ‘K×û‘”K#]ÅË»BÖ÷+†{N 0¯(ñEáÚG¬ÍÁæ02z¿Ìê÷G¢ƒ;XWŸ ‡æc®V¹¹­a µPÇ·G—öÄcH΂ Ú•u$îÝù~¬-»€hQMzzNz%w÷OËdïJV=Û?Gˆk[ qd!sÕ/®¾Î€—•ÞÔ•h_}JxøÐ*ÌþGGØÎvªðº†Yè ¯­do(Ýu›e]e†âL+Îáz¨>¨B ŠÂsCÏ ·®þbNþ ýM¸Í†Òèæõ­S™6ÄÄSBD´4Æ<Á–ª¯2ù!ƒ|}H ª!b`}.C`ÍŒ+ûÌW4¢~O…Z–çÓçSñðM +;‘±¹Ø¸¾gW:ÔeãjúÖ±#õ™[ÙpÛۜ疩{¯~1ꨞ'|6#t­ŽB§ƒýV¨¯.œþΞyXÞ¸qsìWyRwUñg…1Åw¼ë¤õ©Ž$âÍÁ÷ȧŸ©½ëØ5R²¡#å)ûhÌ°j‹ãë£oGê12·² ©ÄYöѲ#õèÕ7£®Œ( }¯Šƒ"''r_®êî>ñ¶ÏH…#¦ú‡‚‡eÇwY*^®t¯Co>Ê96SÁ”\z98Kßk9å4Êí\û*Æ®>êÕúÞ2(‡á}{ê™M¼ ÊVpB”§^ëc£î‰¦;¯G{^ `§ì•ƒ+|’PãÌq-€<.Îtè ËÙ,Þñ¡§ÿ"50^éÊÖ^õƒÄ×Q7žâ®,b§{*±—v¥ç²]g¨à±d––õIé^ê•Eº÷êH=ÆäV6ž.µK¾·ìH=zõÍxÏkA)îR†Þ6Ë5„Utô›²ãéÛ¯FDf +4õUzím %‡£líUgÁ’²ÉТìùíž-aG¹ÑQ(o\CØt4(v²”ëtY¿zböîo¥Á‰z®a|´šp$ íIÂb¼MÃEBrñJÐàÒ /°ipõý%…KœÎFåéûM" NgK +~•[‘Ú‹{ß‘à0¨j7¶/öyxÃ|(÷¢§ 78yÛº_Í®ÙãÞ•‡ Ç@l¥Ä8ºfgïM;2˜^½ß+a‚IùªüÑ”DyÃZ;iÜ’÷Ü“† h+ñ@á»@„7w ú*nŽ²Ä[¡™>—e*·æ$h9Ö´ÊÝúqO5#ðP´•/Ú’n½y5²BÎCЯ~"†á³ +GŒ2+ý†>r@«Yb™î+¿ÏB†é¹ËŽ­¼ùئÞúôÍXOf.à]_MÍä'7—ÑБˆñ.9lì\†À§½Ì·ˆ©Ì ³x}‡Íí¬­ïÇy›Gö™A&%<”‹a£«åé¶Vq:¦Ç÷{¿îŽQÙŠÏô<ʽUgÊ­C/G{Þu"4¼5õ<‡ 5¯%Â*ÞO˜„¨GWmߤ_T໯ ÊŒâoäøH¿Qô£øóL8Z·)G׎Ä}Xn#Â^3_µkO¿÷ëõ¨cZþé—±9þ-Æ'Ŧ¿I‹±RÕg×bìÔb$zÓbôoÐbloZŒ§ÆÛÿ%:ŒîvÓ`¤ +â/½A–úbÞóQ}Q¿ý×Ö_T±ÒR^$°k.¦} þËÔÓ3ô/QX,ow…Åòö¤°XÞžËÛ¿…Åoîü“¸ƒþe°+‰[ø?D$‹êDíŠTµ7(—)‡ŒcÓHÎ&€]xàP}õ…&ñã¯ñr;Ís/QHàDtDÜEp†ZG¬€.ÚðºBÆ)ÎÅiSÜ»q!DŠ)â æJ%rbªBØ2"–â'–êéieƒ’¸Ãæœ`£Çã7±TvˆYg‡ï6Ð+†Ã0pýpIuî[•hTwº`®€]…ž¼{²—œGaaÛ¡ mN<· B‘†²cCwµ9¨jz0R½†ô¼ð +]u`A-M gç…©‘O:ò!h60˜ŽlkícŸ!9£1¸ªqïŠÈ?ÂpÃØ9F§5È+˜C<Š¬5 ˆê›˜# 1DGÛº½Rûd„ÈR(1Y_ v­Â®¤GËu3&JqCóNVn¾¢ÚóvÞn|²ºÆéÌrÑUŠ€Îïã`ã*øN­áѼ§fìß8œ$¦^¼,’7úyqY·2ýýb¬¢…Ñ™ :cÜsÒ9{”é*¢&PË”ugÄäó6Äž RJódœÑ“S"Ó*–³ês²|¤e¹ƒ© IVN‡àÂÙIWëUòŽõ…;,‰د~x83„Ò·š7ú€@Õ—s!Y09aS;vG“ Ë Špìǘ T'ëãq +WÈ“–Àèô"š`²èÇŽÂ;ÆUO”€Ë›º/,A m½tįRÁ>¥~ 7hsB!7ê@Âøëí©gЂ€øÚ±™¹£"=H~Ø&48‰½$K$'0 ËÃÅDêD!Â…€.J@ÂœiÔµÎE( +‘Ë›Tƒx?²¤§(л½3ËìÜ‘'Þȉ°¬—ßµÁÕ"k ï…‚N%ÊC°ûeÿâv•GݱxÁúEA¿vGƒXDÇ-\óëÎl¸àvÌKUÂݨ—¡T2°k(-DzF#c,{ÎL†Ô¨P`_;ŽàKO¢Ž^Wˆ†èèPg&ÙŠÜé^YBÞŽ ´§FK2?íº0;m/r\ÂûuÙ!/]¿(Âj’"¬òâ8ÄIŒ¬\Ù,WTÒ$ø3âo_M·íåZÚ1<Žaœh›”¾—¸ÖóXã·Í`cµF|Q4•Ç³Jn¼`ä¹{»#7ç*9F f²N(` +¶œÐ oº§âÙb oûŸ?6´O%â(Z¾ ò¾Pê/Sí9¹J| ñ!’d˜e€‹t¢¶„~_ºŸÕˆ.ý«ö¯’~‘ +çáµ(¶‹J³á¾m#K¬†ƒÄ Æ¡o~Ï™4p; +dÃÙlײV‚÷ÍêX–H^c6oT6BÃ%Ö}úá’s”“3 °A=ÍÞl‰‹^¬º-eI°ÌÞŽQyµ-5o%ÌÔmÐÙÊUâêͽߋû^iƒ˜¨h6d¡e“Ä +Y嵨BI.˜“…µJ†™X%¯ù5í3+!uz˜Ž5Ó±¾–ÜsÉ+ÍŽÑBg«WÉ«w÷qxåÿ¿$škÒ€‡†»ýâÆÝÏ.¼à ôC. žŠ•‹À#Á7¢%ú‹ç{\Í4lwƆykÕ@ ã’^Ï´A{ß?Бî…Ä“t}¡¨¼­Ç`Ò ÷mÿØÑÙËUÍûhþÿÁÜ­g™)•ì!˜—:ÌÝfÈ× ‰ ú —Ø´ôчø”Ô/ °4¿©¬Ž±:/„fßÙ$ÑM>-K®£\ÄŠð×âíéìX¿‹aÛÑ‹-iïýQ$"M:ìÅíÙÓ·®¼U°w^î'Ä;COï)u’<D§í9I0gï— Kæ®Sv÷"Ï[lî¡líUë«¢¬º¯~ÃÍ;àL…س¨νìŒòŒ ˜m™µ/n„S8zu¦cr+wÀN&_´ìH=zõÍxëû-lm«‡—­>¢`×Û¥ºDß衱'Áj¶¯(a¸bçtœ?ñ zM5Eièv$!d°;q]±fµô¶Í–†&ÝÖ(6ƒ·9mð­Ÿ{Q¸ívªöª[òjû‹‘3zCB^ÕWF'í¹ànñ@†b¤P~"ý÷‰½žð€¹Âòsrú‰ßŸ‹×ŽÁœ¥e“¡Óƨ:ö÷dú‚óÙ§âaëÅ òç‡æášÕOßCçŽäÛàœÅ7Üñ¾üÜ<$_W~Õ¹ïÆþÕ+òqrcr“&öEêh>2 Ý<œ‘÷MŸç¡Ò·7+;1Þ‚H#ú+}—Ácù½¨DQ•Y ®sþùE½Po ^ EnfÈ•àÎÚ ¶’£¡°œé·yÓУ«È ÎWF+ŽázªöŠ=q¦¡0l] =2Ü»úýœh<—Š§/w ÒÙïÛuZw}‰\­ßˆk4(Fwùeþ…0 f7/“¯Üoæþ©x•i"4—P?Úý¢·lÓödHÖƒKiš—íÅx7h-Mû²£yÑÛS­æ{Û¹‡ämp +¬•®¸ùj€­ësó¶ä{ç¾{å%ÊÌjÔ +\Šxbšæq`€2<Ϊγd'Yª0sÎåü:ÃÅw:µt¿U¢ŒÝMØ‘…±z=žÕ˜åžáJ#†¿¨ «W%GC©k䯴*9ºŠ ±øÊp ×S%ÁVõßÿØÐ#ý«ßÏÉdûe è©EFåa³ «2RX*Æ…d²ú…Ź§$ˆÌáEõ,R»U.qÐñ¶û½Áõ™¤CÄ¢VÒ{‘0ÆígÍp´¬<ßjN‹îñ­íÄ‘´uþ©ØYºžš´%íy5¦–A¡H:‰Œx"‚+ °up| b€ 9}¡Âi·“G«1÷j}•ŽGÎÞïðXƒö. À¨æ@lFÎŽtÿ3tnVk¨÷ l¿ÚVÃÑÆ ñ¯J)«„½H‡§±Õ‡}”žj@°ŽèôZõÐÆ=ýÖÇïçá²spÊßê•çÈ‹yU­]õöwµ– ×quþmX¸Aû¢ÏS~‘ +_pÊü\<·ðÌîú­iҸdžYÀ–êá3/ÖÅ㕶/ži£ÆÆ1ØÈ`3:/ꇅÛÙ±[ê62·²ŒÅY6hkÙ‘zôêûQÿCíßÕ$PÝ'lJ}QN…wG<0="ëÒÙîFúa÷á§$j7µ[‰Ÿò  E¢8Î{Àèg6©à9¦^%ˆ–³)Ñ4/QÔô3Óü{Òìõ­DØpÖœžb“Žæ¿Gå"gÞ]8ICÊr(‚ú8õÁ>/ˆâ´2! Lmã– +'õuºƒSï—•è s W]…à5‘B¥[º‡ôÀe[ ¶ÉLw?´hŠXîÍì7†Ð"ñG¯q]Žž>e9FìVOäã¯ZzK?zú«9™€ÚyƒàÆ© +1ç²|ëÉ<ÛJ2dp’¬:“à®}“ëæ=Z]a2s¶`eü{¨j2ƒû¶&ô§WiX—e÷f„â—iÛUÕµ–Qó^biÙ[§Óí˜ÜŠoˆ;®‡nMÛïýz=Þ“å‡÷K-O‰)×´A½ÅÈQx“ÜÀxPî,j¸'\H\óYšö£sVY‡¸ŸØµß~üÛ-¡Ÿ WªFZtê™çš¢ZÛ’ oþ¡,íä£ Oé¶û{Ù°¬¬É¿lמŽµù¬û4¦†q« ý4=´ y+ì±c¿$K¿ãä¦Y|ž6¢s n9 dÈË ©_“//*Ñ>«ÚÂBÄÓÉQ0e· j:ó’°ÕvÆ°mB\ÛĈ·t¸!¢TÛÃ{ê6D·²¢Z¸YöѲ#õÞ±ïgàßRžE)bXqç(à™ÞRÈÞB¤WÇbÅC·D+^²?ȯ"8)ª³Hg \›=4¦Ž +И–¯qO<ç1Í#cëÖ™¸ ÉYð.ŽzhØ‘áÞµ×c¾"v+ÆG]">¸kÚ.(GÇw3CÅÛÜÆÔÅWɆ=o¸»°¾¯°Q«iãðL2m ËËâ3,¦“Fžõ–Ý3Mì›Ìõ¡[¡Zn|Ê‚Ëå/ªf/¾jî-yïìws¡ÓàÅÎàÅR«7ˆožaòßtyåÑΙÞEF8 ½]…F4/³@ë¦ß4«Q=áÚ—¢©&ô IªjPvO¦§¤¢æh÷â]?ÓˆÓâæMlo¦C`Ž0ª¸uôL>ê© +* çüª™HjùØÉïæbòðvû1Cg'“šy== cwP—Ç –:•<@íG6e0¦—•Èhº­’£¡˜1ÝÙU™°¼e؆멒âé„ÙÑ82Ü»úýœL†1‚‡Üòøél’vú„5%ÞyuT‚‡õÒs¿J„´æ©`• ÇN4\X/M„·D™£cèJ"Ë2¹ ½]œžPFÙ{ŽÄ}4žÊFG®Xž›¶'z=Òó2Õ9ž0rœçžup%›ãǃFCbó"!nÚCÁz™JÙµ˜FCªÏÄÌŸ?gÃnÇÍÙ­{†cXž*é=êIÌ<÷î½ó{˜‚ÿú—†vWBà–ðög\±ü0ÌðiSÑÙW§`p•–/òbùÜÃÓD–9.?Á`‹’Ô%F¯aXÞúahƒ°.=À…Z”†E›7ft°¸cnÕ#E7\a›ûÖ¨âî2%' ªiN¿(”=/bý! @ùÔx€¥ïÍ8ÎÝfÆïSK˜¾VÒ1’÷1ßltæÐ9Ñ&W#yÖ#L£ v/Pœ£ïCÒP90ïµ ÚépÀ÷¼õ'Üì¬Æ¸ƒ‰ÆÏi˜èL8!"ž"†NFßD°ÒÁN0 šéоiÏÄÔ@ž÷U—>ýZ;ïΡy0ÖáÊ”:¨öö5øÌYu«T~wÃÞƒµÐ"H,g¢®¨9?Ø‘ç jäj“C ÐW¨]·`Y®«=À\VfTÔVÇ#l‹äîËU#x§œGW“hÐBú±œ[šÖ:ç@æ:52¹þœ‡ç4©à÷XžaµýX9IŒnØ›hA¥Ì½|ä…»éæµTQÅëqŽY±ìT‡Ô³3: ý6Õ†µŽs-+õ¥|V„ð¯MNÉ‚Êbù$¤aåEdp‚.ý–ëÂH¶sãî0á8W ÿËa§Pœƒ7˜˜¼4¨òcé—z‚:ÆeÂ:_ÏÓ¸ÙìÌÄB›+µÙèÜ  +ëcÜ:`˜j:ú*”ÖëÑ)çÖjªÂãèë‘Fm®j¡µÆî}0Üé‹h›qå÷Š’í5¹@+YÂÆôIßì}‰c\n>õ÷KÆXˆ°ƒTãäÚX±n„µès4׶‹ð+XpرxŽ¼¹oû2JUnA¬(µm5p‚¢ìDÚå›pÖà—Öy³¨ ƒ[(KécT¼‰%5†`€@Ã4Q´ ÝYù©Ü:`Iz5Šbðm!“‰Ü·‡ÕÓž5Cë×ï,ÀQjd-æñ® Dnb jÐãô eÓq¹šžódA«šßëç¾^LÞNך®±”œßN­ ô؈މ”(œ€.‰MÇŸ=RH§èêåw0C¿¹aØ3a/Š¥šÕ>¶À}»¶=°`$Œ÷”êÆn꧈zY‚‡Ù½|¾x§´—ϧú–ŽÝqÿ)ã¬è\È Â0S7ŽûLäªÇah?]Lß æDµE<Ãfz~XÐȳ +Ñ>Ñå„·2 3ugIa!“®[£m!Ï0â…eá3á¹_"8ípá-Dhü(@åD>ÊU•ÁÙ/=Ç&5­+ï\êÚEüQÙEWRj³<šeÀÕ…­X°ÊIj*3àjÖ} Ôôw9jVIT¾pÍ\Ä™zgW”+Öa'ZJ°hÓÄbž0xÇ‹¸çãû~BÂ5w³©+ÀºT.f«hz` ðPt{e­z«/w§ƒã†Û¢aÓ+W{èf»8ŒF£L§t´œý^[W’ðŠ¸ùªZyì(—ÇÍÀ+ª +ŒLeñÝxÝ]­2ØêÀö}ù™ŽŠ°Ddåš&Ý»ÿG¿³ÖšñI×P:â%E6É%>q¡£æä†k@¨µ‰he ÷,¯W“ ¡0ÂxXàhÐc H¸Êª<ÒTåEòå¶Y‘~¬ßö.ØâO1ÊÐÊÜðž±ÀѬûLirÌr#"p>Dp(É´ÃÅUU´W+è`Gh +Òb‘iðq+ä· £bÙ?¿šP­!¾&±rýï·­Q\`)Žçjí¬Ì€«U÷˜oWTâÞ—0TÖô0ìçBÐòð[ÑÙ¼r€—†à\ •˜&<±K´CzÛAh˜øpÁHæyŽÊÂO h›Eðmïý>@®–º 8[õ0ó<ÌB…±àá‡@ÚdAC~åĉŰnßÀÈhA¶=/e€ DWœ‡tˆq‘ë^|äô¹ÇᨬÏCë|Ó,ÎlßwòÖr:êZ iÕm,Åï´ûù†—¸HóDLà Ï,Røbƒ,6®6ãóÑ8Ýp>6š6 \Ãù¹ø?šª3£&G/ào{“¦ð¶7߀ËR”5º*³ 6ê¡÷˜2vW› ‘F/•D÷ IƒÓ«ÅS®`jÚnàU&Èôí=ÕkPõ*l;xÕ¸U”Ô‘aaUm« 6Úo¿+[ÝQÓŽ6ÝúÿÇjB7\a³Ø–êÔ™©Åƒ#ñ=¦ïD˜ŸXh%ÙÏÙ}¸5æVÜÁùŠb +è‡Á”o˜ºà .Uu¥8eÀÕ­l%Ž«2®fÝG@ÚNÅ\ôJJd ƒ ©ò@áÔï ƒäÊg@'ü[Ê…\Md¡ Û7°ûŠ(¢û„Âàf}Ìyp!³2ìê¨ ÇlÖMl0ú+G] [ºÀzÑn² í4NŽû*ò{ïD*/j·ÉÝ°I‚ôûaö¥>jdë;õÀ@ßKlû÷Ž½ujøF=}Õ7äÖ(®lÀ¶ÈLe \­ºÀdA8ûBIù”8XVÇ9BvPd? 7:—í÷Ú°¤|h¶gd HÚQüÍN_U£*È·Âà?F›hÚo>óéÙÔdÀ(î:ûoÕi‹<šüPúøªsˆ°/wS äRiµ º¶«ÚϵuJBaZÈd ·‚á»j|¿p^-gE0õ,UùÑ ®ÆoŸc)(c´ª2àjÓ½÷Ì)kµîW,ì07\Õá.#¾_Õ¸_± ¸nH£€ãŠ)ÄýŠ…z£ GPJw4S®,BˆL³,8»` X·)S™W³îc :ŠëH—AÙq(b—yQ}X'ÕŽSmp‹ *Wtû±Ø×—WWó}_4á<ñP*ó«I Cëóñí:ùV%[­¹w]Ǭ¢HU!í‚F5±Q¸Tá +‚\XwÐOr1 +PF-T¡¤ð ’øêaA¼Å|€&—'£6*ùL€i–gl‰n·Ê*ýYͺÁ²tÒ•$ åÜMkÌ7àÚ £€c7­VXíMù(ÀôÍV”ÚfÍÁ±]°¬³Ç‚«Y÷1Ð#±×”ì™Á0Ø›52œIºÔ! };Ç& ¥<>³EóNpŽ-À‹KC=geslL«,6;`¿óÆÔeÀÕªûL>*Q?±„û¶¤ƒ¦Pá.†._lÊH• Ò÷µµ-8h€ý~ SÓ$*¦I 3­_ÃBPf`Õb°Õœ{ßE3ïæFû×À“£í>甯N];Ý„—¬\:?â€ÆH\•ñð~ ¸1é Íñ‚¿µêá[_þô÷¹ÞþÃ_àKëš/7ò³—ŸßèÓ¡°X_€Î C£+ýÉ­}¸:†¸‰}eÒ<Î`aÌõ“>€ê^}Æ C™,`]¤äG÷6pªIª WdÎP¤¡ï-(±ˆ.J‡àA»Æ„ÐJÄgËÞf+ ÑöÒD `w,Ô9"êŽñË Âì Þö;à:Ðcvßñšh<'ý^È(ƒ +aÏEØYI‡Jn¨Ðoˆ,HÄvõûVD7MÆ‚r< +ìD¯¯¨ ê}"›Ç + 4ˆ¡Y‰2}H6W¤^ +ö'Äù›-A§Ø¢ØÔtƒ “÷§1Ø5ýÂU!>«…Á þjcÐÒÁ¢wµ!²ô„~Ãë2Tsø>ÿ¬hèéN+$'+« ’]}…Ú5—³×¹0{©¢©Ù‚ADbÈyy'|'–X :Ǫˆ3^òAåÖç5(ájÐêl˜¼Buœ/Á¢ãÖ p:ÑnÐ(•PàÆ $ܨ~b’Ö'4‹w©‡Vž&ˆÓŒÎv±BÉRÿº“>S‘¤N€èÀkG´%„«35áÞçÏ^ÕIg¶ [÷ƒ—ÛóŠÛåÈnÂôŽ!Ra òùDá&µHE—pŸf»7¨tǸQ…Õô™¬8héí6õíl F–J ¤gQ™›‡ùúǹÿWJ£¶i/ê¿0… ý°¨¼0†DœÖ†Î…Bé wÉNºÒ ô)U·œÂ–Ï‹ÖÁ|m|j஬טû©³¬“Ä48'Û¦Îá¹»5™æZIÍ Î."]QΫòtÞaÄ4 +Hǵì*8®Dt)¼I=‘Ú¥‚&÷£@Nê…¡DX*Õàöœ^ò³%´‹Ô…òõDáØZ¡R"N`Jú¯ ƒºÁóÓm5·Ï§4¼CYŸ>¹Ùoù¸á=:‹õáOº-æRe £sÎ.O<,á®°-cJ…[ÞòEÜ&-ùqxNNJãbÑ/ûÍ!cKÒý!cÀ—x*‰9ùö*šO¨>[R›9>ÍrXÙ®~ìT/_"ÊëAÍ$éB˜à%þÔMa PÁKTMÇ`Ö¢ €GG{#ã×1=Šã5ê`YŒn.¡j u ÝROm÷mYßÖÿÚ endstream endobj 366 0 obj <>stream +¬ ñVæò¾`)ËÞÁT¾tòÉ¸Ø O +À­tH ¡»¹ÓF×Ù'D•;òö•Ý\f“™+‹uzÙ‚ÍD1•Q§j¦-¾ÀÖQg>~a´TÇÎûäm`²~…·Ä­˜&¯O.\¶–­¯01ŠÏñ@ýØýá +HŸ¡»Bõ~§}N¨æ´å$ÇD¨‘)g Câ(PˆÆó8ùæwÐøƒtâv×'ÛÔ¢®ª<LWvl±¥ ¤öEØ.20«¶ä{÷B“$!…›¥MÈÖºÀÕ¼YÜÖ{Ÿ'3lZ¥d‚ÑDÖмÂVrí¾k½…¶±˜ –@ÂDÍÒÌ|Îj·™Çx¹}DWd +\Ž®ÎâL7Îî¾Ö¥›ztÄ…ñK; ˆ0 ¤çË`ý†C¦\Mº‚å¡è­êÓå‚6¿~À:ã¬^° 7­Þ‹7: `›ÉÔÂD.‚GлÔ‡EÀ’GS„øäF£ûöpV²8M ·‘€ZÀÑ~ï<þ‚À0ñ΂oò]zNVUA²Ü Ù¾ Ú€‚<ï¨Ï Ôõù8æéãaî>~CP°N¬”Y…"éQw–Kn2·0„½…ãI4Kù + U‰‡!iEnVûŒzè™qö†“ÔúØ`¨§LÆ>¦d O¹j…ò–ωS|”æáYÔB3T…ž Ôà!3dip8Äq'µ68x›òÐjL¾~ÆBÉ¥ Wc¨û]ƒÂ[Cqˆ\vÉ”¶ ¯çásqðßW²#|LÃ'½µóž<ƹ_ã©–õµX;rO†=˜æ’x?DzÛ#‰½Å)Ž9»×püãÜ.Ô Þ!mÍûS?„æýKרJS +b`&|ø 9–bA"9QW휵’ÄDc›]ÿ€‚†ÍW%¶7X§2rr2½Ôãz¹A +à1½MaxN.$^BWIf=ŸXe´ú…å¯høË̺@Bœ–a£P¥ÕB¾$U(fÀ«4=¼ª3{ ;$^vôÏÒè”bÙòA` –7ã0 yƒ’èe}þ° —E,ÍÂ۠pÚ[ÂíRt¥¥,ñõl¾*AçyÄŽq Á,®ßÉLøJÊ]rÇ‘1"ôQŸAˆ'¡Î´ƒ*i lQî¬cî–Ãû$ sбAÅ»w€M|ßà2 ‹‚1RÂm‘;O—B°¹ ´¾$WK;’á•Cr‚ôû\eš½¯czJ dÅå…|ÊaŒ“mõ©ˆý_Ÿ:ú›Èð`pGwAY>µ8£Ü¢\1׈Â%R"H(¸v+žêä&5AÄ„¸Ž§%tZ°.š±Y «q<›ˆHNng°ÁꌺÔÒ—’®iè ±u|ƒÈ…!BAvs@¬Ày()]ÁB!¸è¾Ä‚¥,òr§vΚǹ£5¶vVëô/ŒÕ±„‚ŒX’§Aj’î5,mÎo¿°ŽAœ€§CýQ¢¼$²ŠTåtàó±L\X€Jt2Ýä‚Wü‘Þ-è’ó‹3ÀŒÙç]¥µ¶^½ÿüœ‚Cœà kщŽ5?á• ÄÛ{P!i´˜Yf‡ÙZ ˺.ìÇdÖN*ž­±g»-HN0 Wë|N{ÎÕ™ÿMåQ"Ê¡6ZÝ~Y°d `×!/Vr\š`F!7)Ø¥B:iKx‚^šD½©R¨P ëc…Æ!ì쀵œÙêàíWauÐ@ï?NÐIK\ƒáÕ ªÊ“]Е*·è¤³0¸Ð¸ÀܯJA!Z;Z2À1&uœW_w„C2 1$²=ö|ƒZMH&âhÉ¢%sfW7Ö¢˜½­“EšV©¢BüuÃt÷¬f*h;“Ä6ÕvZ¡w³öl2€[a2Ð +í£/­ûi)~VO½JÊ:’ƒˆñ&d–ÁÁƒ7Y£0ò!l R¡}Aôq_4K~Cdg(ôn—Á‘¯ÔÒý¸j@á— Ü$ ±ÝÀ“²òó£³Úe‚kðFik„Uòþ›™Œ¤öo!›àÚ݈Ž‡Ð§‹ øOšäI Ù*!#›¥=„œ%PQ­X6:F0ÞóYª%P°•Ž–Û §–¤³%sfW7Ö¢˜½­“EÈV©‹í˜²ÕÌI{f_&Z]6tl ÍžÏR-;Іí èÄÀÚf®ö[¶{qAfLpííYØ¢kíB¶ƒJÈÖjž´g­ù*y'Gs mÙdŸí­tߤ†Á+tÎ[/2äX{_´É×Øiak|5ò›ÈØ¿0<Þñœ +Æ6á†ý%Ú4$û+Ô’2DÉ%E§ ö‹lážEüâÆÞ/1ƒc#wr‚¾ynS×x‘F±H¬d¾É£Ñ ·˜þ­l´§ÿJ¿ Oyd´›¯0¸¦AºÙä›t#ëøR1<êeÍ·ûv¤n#º^¹æèPåZ‡‘MÚ&Û…]Ɇ¢S–J;À §àŒÛ–ÉD¦Œ*JäcÓ‚ÔŘ~ª`ÔÇ@’Œ)\^±Ò~gõùÔµ H“ëBH£ÆÑëK0ÙµÑ!˜Ì¼?uþÐõr´2…Ø©=,¹œ\:®(<»,/®>D©/€„‘'¡‘>zÓâhé3Þç\ŒÏéì3h—Ô?AãNší£÷Šñr Œ^ìØDjñÛæƒ<Ù^PD¥µ÷§.oÄÐ ‡¨À¯êlœ˜`\I™uxÀ*OÿAlê'BVÿóž­ˆŸ@¢óÎÔdåQš_VíW³RGRá¹ÃXxá_åcàŠ“f PÙ‚>å’‹k[> ‡„¥\¢Yi!/®·ôSá"+6ÉG! ÙÍ0ˆq)ï»~6Øä«â +¥ñqÃBAÊÐî+¨óðñ49ÿ¸Þ²g$ g_Ó¯àǦ\Oª +<Ò`ÏSXa.6}ÍæœäS*®=“M/rcT÷öкS© a–`>ççK@ç‹3°NÉ\ÑS¼Dx ¨B Ç,Tuš·Vä©“’sáåK_ÉHhY=dK|+ RUÆã¶ù`Å5)è„@}tãS“ +E(ÃSä:^뛬;/¯¿è=tC 7í*,v0Šôž`¡(®ôS³8 eß].ÛùƒïÍÅ}Å¢iT5ù¢ø5d®Ÿ ¥Ž +ªJ.¡ -Çžä)ŽüIM‘Nq÷°H95 ñÖnþ˜ú•ÉIti”DÝ+ Åñúd0õÅiŠ‚ãn¯=G-BÔðÓö_ ‹µƒ—/ÊÔõÛ¸Çtåd!}×W¦N@ª|GUFéLSÜÖî¹Ä­ÞV¿2tÜ «ÓY¯yµ¼£¯…ŽÀ$qÛ J«õ vÐ0 ›ojÉÄ@áèÓ@åüt‚צzUPŽú4GÕúü„ØQX©Vp{øôLÐ| ¿}—[ z¬¼½EóóG&Í;BPäyq¬vƒwˆÑ:6BÔŽÈ惿E9ö¨D)ƒJ<Ë¿ŠÜ'o3lO]<çP×Ò’¿„hƒ)o³è¡™qr}XÐâ–°Ò± gakcÏJ-H ¢w“‹oq+kA«Ê…­–IQ¦õg/“ºÚ"žýOc–0a™ ¤Þò™îOì‚cª7S”™¶Ù;½x‰õ~Ë7—UØDL•[-e­æüw¢…à:Ô7Å¡z…*¯C *”x±¾ÂUo '±fŠøÂEÙ,Ñ⨲ùñ¦ð.Š]©¹WU2‚•œˆún„«@Bý"%SFïÏl í]ò)Fh!v[¶8îå0l¤>Ë‚´ïú鼿»<>­Þ a¦ù‚©Tí½¶meë½A¿tTF„.‡oÂFÑæñý‡’¢Î…µû2t j)«Ýà`Œ9&bÛÀ:_&ÛœçÞÊ “zNýÒ±BÍ*<鿈r(ôXRl¶PøT§µC£ËÎóÿƒ¨S¹¥ÏÞ£ÏþIx\†C]§¦·—ï¥PeÇëÚ ѽÅZ%Ÿýë½ÿ° ÀU· ª¿M¦˜Æn¬÷‚gÝ ý³´Úf6›õ>ÐÔÆñZT/YiOShŒ‰@1 +o†í+ŠA9· Õ(ò°Á¶ùŠxX›ÇNÏÒMnÂtßE3ˆ†RÛ™%^Èë °Äiff@x ËI‹’òœò‡U°T«ÖXcg:ÑÍ„K^á(¯a³%²Z +>{v—¢*RÔÆ°Ùúü¥à"3S ýt‚™¢è€(ÊUXj¼¥*ô)ÍíZ·å›Ýrâ‘ã¯O}ýÍÚU4a-v·~‰³è¯ÝVAäÛ1@z¹/b§¥ +›ö‚‹Wî à8`Ⱥ;&ˆµªýu©p)!wÞ$újà)¶"]oAt¯ ' ¡ÿ¡^s¶ô†½ÐÖ§ô±yg!ô}Õ€$q4âªÑÁ9TF]ÚÁÜ$’\ñ9b-Mˆ€ò‰–`´ò`Šò˜ L²Õ?~èÇA%àEL’á#+ ɵ©Rzgãâ‚>eŠ'ÑùH8T¶4{§¬ýüTAÇë½Ƨ„Õ­£ï¿U¹ÊQ!‘o­ ‹PÉ\ štê5 ºt²œhV#”×hBnãĬ~ü‚“¬AÍIGƒlÎÙô ¼(”Aà{‡z *9S’ÅËõmYôý¨ìßbÓý +o…ÔìÏ°8HOúT‹b€¡.P!×ÖÇQ< Oý4–{¾œÎÒ¢AFK&{¿ÂÐUEêÚ‹ÚigMÑöuA:&Ÿ?,ˆ·£¦¦ðR8¼­J3]Ç­!#Â> Ðôô€šÆˆ$wËÇñ= î-Ù@5ã™5½˜ó?ûz['‹2­)¿„4|ÝÁv¬í:Ðí:Pè}[{¾œÎÒ¢A¶u r4QX2¥Õ&öR”Ô„à6È®ƒ &x¯oo¶´$sM¥Šl Ql[ǧå^Zk÷âdä¶|i°ãJááS ²)N4¢ìÇ!‡­¯ 2c²À9v«´m¾¶uò»5¬® A},-3àØßаneP#!em}–”í`*ÛbueˆOS³ +C¢šøÃØIÀ\oùb½• M1 ÚŸe !aû‘† dî Ù ›àÚà£4CF­ 9Á1*ì‡!@£·TËAÎÆèíù8Æò½)¨êóM/ Ò ³Fgï+ÅP³9ë–ší`;–¢?s)¬U?—‚¥fs)ìùb½• Ý—‚¡g­2äñVúý¶''d—Âן¥-J0kµdä·¥p|œïP-åÝë•1Þ¡ïŸn —‚¨5Ã'Ì5„WÒ[­Q1à=SÚ6gÛZùmÊVíAËŠ=¿MÒü½Ü5< PÛ(¢&È›¼ïvVº•Î ê?¹D×~‡K¹çÄ£´n4å_ÿyá’AjRc{ƒÜ´G^•('ˆÆ"Ì|sM0ÀÛ,åÿp}!g­`'SeO\·ójê5ʉˆÄFYó¨ ƒ}ºŒyéhÙ-‰â},£êï‰&±ÂGKQë$bÑ'¬ñ"µûˆÁ#o(cbó–j4ž×sß°gÁ&xãó¦Lˆ²G‡t&[¢Úö&FR|Ý@4¬*@Wjn<¼(x‰™“¾FÐ’X@m]‘à3w¸€…Ë VæÈ›TÚm%½ÿ0‰f`ðÆúa38–êsb£¶‡5CãKì²¹À&r®Ç÷+q®à œ+}ÔhöÃm3½ÿ†§ŸR2‘îôý£†¦ãÛ["•ö©22§äGõ +SéŒL…ïžÇ +nC±‰Ð¨¬9o[ÚšbÒoáP+źŠýbiÞ÷*1B^!¢¶–Ú‡¨¦ÒwRƒãÇì×~“îCôÙ¹r¦´"™Öõ·¤¡è;Ý|ú®Á,☮ÕÕTA¥üÜø:l#‰Ëm3ÑÇÅ;÷8I/æv=ÃϹ‚¹b +«÷_24¼7UˆÏ›ßJö·Â‹ºŸ}¬¢àIG$·$ö#‹Ëç§ï ±Øú¾MPÅÛ„æë-ØGDá–®J+‘•d(&6Æû•&Q)L5ºI0Q_yIÔ“>×æÚ fAcÒÊt2t©áË«¼)žÂuTç‹z>Ôc§ÑqW­¨Þùíº`œÇBZHí*åá¬jÐFpyÕ8L1(|A^âfY F÷mâãB˜®~¼HLˆÇX~ˆ(CÒô]Õ™ +S“¨ƒB’ˆ¢jP?5´&o™¾Fç½(¯¾6«NïA`kk¥Ëû5|jnQqÕ€ëO}¸<„ ò¾ +í’=PO%æÿ—ºw×Õl×ÍŸ`½Ã O² º_‚¸€>Éà¨óJ]a¿ó£(‰ºŒñ/»ÚîeÀ»&Å_$% Š’x9›ÄÍ +”/?c¯¬ˆ–J«AOí»l!$x£³ÌÒ!¢gUCÚ4SlÏŽ8Í¥KÈÃõÛÛ9LkxBMâZǵÛÏlsÎ,pH_ußhªí3_;†qÚǼ|rȨǧ‹ÊƒÛn᮪Rd¼ü ^`¨ŒïÑ4o¢áèµe$¹ñXßoËcºÓ¸Še¥}7.9ÐþhüVÄ +.ͺ4zÃÕf“ó3Ц#L\æ…† 9NŽ&ñØt¹øëïÈR3dA3Ô¢lÇùvÇ\86ãÏåò»ymUŒÛe-á ˺¥Vµë³+ZAô1^ ¼W¶‚ønqò~¨iÉÐìWdi¢öeUdöáÀöìø˜Jó¾rx­9šØY I£Òíg¨d +7žË7š"‚1Û¶1`œRUmLÃT²O±ˆzZ¸®Í· VCÖ~5a‘þt˺K |Âii™ä—©MÈneG—ò1ôF$_¨«:JÍà¼}qï{îþ¡&Ží²·ßáeþS¡'ÎËÚ¿¸Ý”I¾e 86Úï6¾4B¼-ÃAKÌõmÛ÷Òvl¸ÂÍFíê}}Nš'ÿ15¤EZiq\hrr!Ýp²…äñ«ò¥Àû±{ý!7+k%·›;Èq‚„LôÕE².õG‘Ĩ Öº¹7EyZ?ÿÑèø `[°\5ëöu>}ÕÚ'™]Ê›ániK™]ÿnlaÇz£Ê¹SŠ»oK‰³£\u .‹çϹçä¨&¸~ø± È"•F¬žšÂì’—Vl~èï7ûftûÚç×íôˆ œ‰³è–¸À®½a +hòÔ˜¼p•`tûÖXàÒ[ÍÖmo„;FÛziÈ7&Å y¸(\õ-»Ô_ÅgØPâ;…&‚Va:¹:ß>¯Hmüáœi/‹ŒW-ü¹âum¢ )o;¹®ú¬·ÜÎaÒV=—«ÑŸPºèGáïªV‘®,^ͦ‚K:Ö]Hõ¦+‡£ñLˆ]ãiçA± Ñ+-b#“ÒÇH±ƒË˜êä:i@îCÙ¹ôÛºì¤ëËêœÞVŸóÅ`Ñ9yù…ØhºŽ¦4N™:‘!ö:2Ï.Þrüçü¼qIXáô·ÜÁ&Zj‰–ðr‹YäºWwN)´ f¾™2œ›n‚¤0ÓÏ¿&0;•'«Ã,;ßôõ$^ŠL ÷i¹gGÓO%ËÑȯ±Ýižòu'.ÑOVf“pÎd6Dü^€}(Å9`çhÿ·îo|•O;Í9Ž÷œìÞ÷$r|Ëï°b¹Ér •m~ ¤*“µ7à¶ÀÑ!gíLÜ!Œ£=e4„ƒÅ¯£ÉGp'&ßÁÑiÀ5°x nŒ 06¶ö•õ¥A‹»tVákGý=ñ±¶k îcˆ¯‘ߺsE>ñZ¦‹,Ç^ÓÀúq |ºmø%Üàþöl@ÍÝŠè¥+á‘Eµ©_°¾ÅM[ëZj’–²tÇÕ}Û£¿É¦×VX„0}71/Ñ&B"@  ³ñdþ>v2®p%pF£ÓQE%^\Ôün-ÖÑ´Fn¡þ‰H·cmsM©8*¿´»œü¾÷,’DRÒ¹Ó$‚ ÝfØÛáW›m?$‡£çÿ¯ ¯Ú O¸Pî­cñ­2]ÛÕh,=£"¹Â5£¯ÒK¼Ð¿aq<§fäŠ]¹¥yOœo¿á„¯Câ;Æ2n:¬[3Jª=qÛ0r)þIâóóëƳÏh&Ål›ž$ëƒX…iùÿš«.}ñM¿d©nÍêƒjdŒÞiF‡stû«¹ð’ìȈÁz;£Æ…þåhôp$®Þ\ºÅ§kð@¦ºU ‘Á›ý{üR‹Â¹DÝ8†áÒ1û¯z~WGÅGÕïl;Dyݦ4áBLçYÝ¢® )U òêÚg©âº¨Â©šÌÓÚ¹{ÔŒ +á)Œ«ç‚ŠA­ÑÐY9¤1aÙk®iFÓè’·vöŒN±7VÚgù½Éãâ­Ï{—˜mçéÐot; •¥B¤ÃB²=øV ±7)áoÝ"§ Â…¥¥iäiLŸ'õßþû-BNêBÇÈö¹$ùª"[‹­á0çiÜJ3KÈ47Öµ# Ç㙟Ԡ}jGfèìÌ‚D[²ëtÌôAzð_{;J«ø[¥ìØÍÐçÙ—’f­qÚRŠŠtïÞ>èè<³ñšígKûE²×Ñ…Ÿ›ª£>`f’–ò‰lËÚ6°i¸Ϥ4nO¨'ÁÉb½Ã[ã)ÑI?>ÑKÞFxp RF!ŽXì ÞVlôr«:͆@úǸ¯Ø9E":Ù¼¾Ni¯˘í4ÈŠ1ÑþÈåŽqJúi^†É§žXe£E`Y:qýD(WÙxg™¥/f¿ÅÛßS;ê1Pgw +"m¥a7¡oö •º;Ÿ{;ii2»:…pPÀYÀÕN!ÑZ\=Ûv +þk“ñÞ®FéJ!’Jr¥xãqi¿Èø:ã0@G„àÚHãŽ(ˆ¦/ã0€Wá ÖK¤cµ +²fyß.ßnípnH©-ă‚ÈI‡ÆR:ÈH{4 9ð¨vúØ|1IÚýAf5 ‹oí®ü8ðh¿v¯íj”®`E +ù‘Ç¥2Ò.8vÔOó íP<5&x]±/ª‰Á·‹ãû±€Ô„A:*|ÏtªŠr¡%±VˆÑdkñÚŠA îÒ·, wk§ Nv½^꨷ÒÚ‡Fl»¾î™‹àÀï´Ù®˜‚…2ôF"¼†@—V5×¾áNUùD²1¥NYÞFù_t{Õ%\¿ž¬cD.‚Ø_úf³¾,º5Ê5ÓÑíz{…c´'%/Íùkk¬Å ë¶÷ˆü_Èþ³ô8y©x| Yõ¨¤¸6ö¸u<.§Vf4xàeL›Rì& +2„ùæÇEû•ã³_Lĉc#éŒqVXÎÏbÄ=ÑцbÇñüþ”¤—øubÀ­!ÆPz/'¸ƒf:hg=A·‘WL^Ò×)ñƒìÈ^é4ó0ågn7Œ‹ÄæGfÑ!¢)>°Øóɦ®ú9.fQp°ìP!íüd®,1ÐË1ƒW:r¸ ¸Ìè8É °’™c9ãi ä{° «¾Ø•r!ЖÖ×®§=ÅmGWÌ}ŸI_‡Äw =nw:xÃJÈiõÈ-0By“øÃüŒ£€|¬ÍX¤µ=R­êOæÅEnäØЩrn¢¬¶NWå'L§ÖhÒªü ³Áé…©ô_0§»œÚÏrjž(]nÚ¯MF>㪫În§’k㟔€£í"Ä㘛09›#Û" #yŸ‹Ý&¤½37{ªnŸú*©ºžÚñ\XKºSÇ{<¦7Û•E¦{„ŠU9Ûe*º›KÜ)ðÐ9ß=Cc· '2-YSÐ2ÞÛç(Ý) ŽƒÎ1á‰GÝ~“ñuÆY+!t¨™dç6?_ÒÒ¾o@…=Û³Í÷Vܱ&¾ZºnM¥U·çöSIëÖSÉë¾oÛ„æìÜh´T×Ö1&×¾‘>½rÎÃgªõ"ÕËxcFîq±$Í%2á Ýõ?àj +—Vh˜ô0^ã&”¹¬¶UP‚a“Âø<$} ̶Ò[®Ø¡$òOJ©ÜµHnÅ7ȃ”é@m9²s4`ìWå郵YA¿5Ô·»z Æ,x]+PœÐl8=<@i)!Üh…²w£•Â˜²B°Ø}Å8¯5M℉¥F +eEÄA. +bKËÆ‹¯¥Á¸"ÛÏ¿4N‰A"Õ3WË«*Y%M‹ï@!è ×J^.0$©îy*'T G–˜« ŠDó1lÃ<{¸B—éKr…:—Õk‚« Óç©×΄õUöó/+ò{…ŽµÛɨ%~~"ÿ‘¥xçM>‚_©¢#y"â-'”}9¸’; !r«;×ÞCBu®ÁlJý ¥éŠ1Éêñ´õ…Ëf£L=Xdß< ž¦Pj/-;5 µX^mH1˜-©»Ë¿çä³Ó!õçýj.‰tH¯ ^j‰9Û¢âzTaü•æ¯ Y’ê¡?.«†í2ÀÃ_W_†½#~YP .4(d‰<¾D‚Á•ÓŽÓÒ¹MدæƒmS>ê"1„Íaƒ‘TQÊUN(.”8o÷XÛlä–ƒ¯ùÊ;A¿Ú¢ Ñh<ì<šÖä°¬Œ0ÞÕ~^‡bÍ´©åšãî9-—LAe„è[odÍr½I Bg烻'gã`¹`R³´Š¹1R›S‚ty‡ê5 gSÎN·A]«`;Fbä +ãO›jäz7(q~i?·œßÞ¦VÞ…a\äþçUzí]øÖŠ±©HÉÑk÷xVĵg…çâ1¥-.¼b4X`ß ãàÛ–qA̜ߗAœ$™£¨k’iKÖH1î¨ÎøÝ‚ñpÖ •Ó›"üqp!dæ&9¯`Ñ'¾µTÓ%ÕE ÂÚô$" å¢ÛÇfZE;‹(íЊ's•™ó®Œå¥†{èÃì[™(†rk£“£hÀØâ™SKÚ9a¿Z@·Áï‚YI$ãµöÕ°Ôeÿúk†ÒÙz´¬zØ9Æöh©ŠcxÇ›^‘Oò‡ k¡¥Æaù++š×*ñä†ë"‘) +ŠÛ¥6z4"Hð¡,QŽc—í Û翪ñíºŽkê( ‚lÓpp¬Ý/ ìU_d¬Ø¿ÛˆpFSRÞI$yŽAéÂ,ïÛç8v¹8žðcl šä²Â-sÿ±@áØɨT‡ôVªçun_iÆ×–@×sp­>‰m1%3YÈrómPgB%qt -ª üœbm„`¾ÕZ`HãZƦ M–ë }¯@ÏQnsVäRÀ»ƒÜ- cöSËÐßͶåÐnõs®ñ‡‡à 9ÝÖøûù—vQ¾WhZ‘ƒsŽíß bCdÚó,¼¾è×åK‰Í5dCv¬£Fó¦Ùý% ‰Í{Yì®ÿjv)§IOçD7 ¨´ß­Mˆî#ч«ïÞ/¢©Ûjrn×üXb,\ f—bkÒÒo]dCpÉ>ñ³¶/¢<*_vã0XÈ\ÍÒŒ9¡ôè64“Ùåמ¦æáÃ9§I?g{m4ÈŒB½tü«X oRo6¾ºTÇou#'†—øÀ£ã„D⡹ ñijaÊÁª¥í²s¼ˆs6ê¡8:†G¦s·2µ5®â¼Œ±Lmä4ÝÍ¿FÕ󪑷ÔHkz#*f&Ô4Àí™yÖ^Zæ ‘½C¥jÒ¤Fº”+þí-Ñ´ÕOu±÷ 3•‹{þúëdV—Ì8DXZÉ×é„hJ4ÏìlJ’§U!o‡;Zf>#-°*Îh™çI’—áý6”ŽØ!¬šV9áÈŽ·þÞŒjÀ>ZWnÝ‹xtB¨U#:¬”VáôlÎ?Ø¡æ©û„xšÄùùdy½iŸÖý!ÜÖ| ÎÞ=ç©Â“Ñ•=nŽ&ù»p¯cÿG½à4Ov„(GÜ~ÓˆG²%$1/ÑÙZè{¦·³O=ý…²‰×V®âQo]K iæܺFäw1¡µKÃl¥ÑBÚÏ­ië‚ô;¯»Ö|áÆÎ~“/-ZÛk]'¬äÖ5–%™Èƒ/SEd¹R­§LÏcÝ!‚áwAô‹ãÃdI† óp3=YkHC—¢ì(|û[} ñ Á™èê‘~YLM/êƒo ]DÔWëã@À&‹Ó #ä lü¥ÖŽð믃Q‡´J‘Æbö±ŠzEX† v¥1a!‚ëñâRzdtCØEý4'2qeiÓÜoè@ÉO÷t¸¬Ý€£í#Ô±WÅB6gÃ(ͤÊÕdŽ›¹¶#}`jÎ '…þ:`•5 ÖO!áŒq´#“XàTžw +N%cÁh ++%—œ‰“Â*#ÚQí\·ëQºQ`"}àqm?e|‡aò!oV[å”XÌr7øj±‘pE浬½4®g\ÃÑ¥ÈTè$—K´¾ #KûÝÒ„j?1—ré9?Mê^ÝЛìx¼“QU†™7„8š”ð·n‘SŒæôÂÒÚ´ò8¦ÊÞ@6i:x›67(‘lPR˜­Ñ2L=2ád…Ô²`äfð±{^yjÇ™äöW +"ŒfÁ@Â$ÚÕóìamwðÌrB¡œ,;¾­V‘ãÙåœg«Œh·Ys°ŽÒ[×Ö—'×öSÆ×yøS±p%#“pj5pô-ǸI¾4v§‚ãnßd" +ÚåÝÚ\ô©e(î­wXHõuï^|vl¦Ÿt ”-¸ölÅ'óí¡{¾™öÕ‹mÛ˜£ ¦·[œy\è¿Þ;Z—‘9úÆËqªñ³­u“ê}ÔÿhÌ_°™ ›jŒÕ¶è!®¸* ÙÕ>Šx/ ãm¼5¡D]®Gâ9G۸ؓ©êj탢›ð¬Dl]zDšž”fš$e&ƒ´O¡fÿlR=òÓ¸X#ºieÿi%Þ›vÄ@Ö +¼ñ¾MÚÇJÔN·ò‘HÌL$„—êz^d7¢ÓÖc; +Ù#Øý‰ˆØû1¢6å@âý^¤­“£Ý‘©emÒDH—Ív:Â!~æè9Ø„_ mE-‚©gNY%½¡l#vÐá„“Îù'NöMÒOs"Fe+ÀìmßUáÖBbûI€C.ì$Ñàr³0áBJ!?5šÈ&ìѱXÿŽöò"Í7ÿ®JTÏ×ÑžiÚ\lIŽ¾qA;ñ~lf¾à¸ìÄ6©Îµ<Ü÷&Ô¥}“£{h²Þû}ÝÆÚÚ¸Ëõ6Þ2!\ÃŽ¶ÎŒÔZã†Öô-ˆ0süblZÄ`ÎÔ E^›½C‘¥z—JoDÞG[˼+ÒÝíWPHïâme„?/­ÈÎî²)#pYw‹Z³5™’Ïë'd|¯Géšò¿· ¡÷îÔ¥Ò…ÝzÊpË‘ ––@» £iwP='…Ø1t˜™¥xgS,<57ãéÞ½²Éüü}ÁÇPâb—©f¤4ù±{øä(¹Çœ6Ò‹ .ÝÁÌHÜSÈ +ž©>j&Zxb÷h^„} Ì@íWæŒô´¤ÁPœDLD90asˆŠ¼G*JKÄ—ý‚¡®Œµ72¿$?ã“ Üïc36nÍ´Ð$úÔN[÷–>³ØCpíˆÕYÙ´\û©“8Ý›·º‘ }mSR$V6Ñ“bsòm.†åÝ?=n$·Š¥ÔC•É$ð%m)eSg—Ö©{ö®wµ&l'õcÝ +™IWÆk×mÔìÒµæ ƒØBê?Ö2µ)IKë[×J‘É\h‘uë)ÓóX ÚD‡09 NK…ò»ù‹#áMÍd¾ËŸl0UT¯öPÇ·ŽåR6ÙÊ=ñ\ÛµÁÑØæâ¡c XŽH¤1Í‘•/ž I’rH´5®£që»Ða+†|gmmÜ„zéñÈ‘-Rö¸]ÍÉ©†F.Øa‡Œ …½õI'˜ki”j^åH“ñü&­«æ]Û´ÒÞ{=µþÎÓ¾uìòÜÚõhÜ(xdœcåomÓr½ŒóUƱ&$œ,×VéžâÆ™l2"%”]„ +ë1ÌFÎi†RŒ¶B×¢¦.!¾$ø»C#òSÛgÔÂa àJæÃ[v‡Ò``ä¨IÆñð¦há]œ‹áB+M :­´n²$ìƒ_hð +&ˆØWL$qèOÊ‚iPl…gdï`åò-¨ÉƸß1Ò3®eŽ¢Es”°uÔH‡«3 ÉsPQæ-Òµï~^8W “°áêCZ0!‰e1q'é|^a8›:FU¢?{«Ô³åŒš(²ÌÓÐJPƒ.Î8acJŒ+&¢1àð X %-Ëä\(ÝQm”-ÂZŒQúŠÙInñc0Pp­’:–‘ +.5Á êÊçlâ*øh­¶…IÝvJy„êå +sÅ#`‡"w‚©N‡€Ô/WŽ`cšƒ4Úï}“ÔDýð:%ßbŽqP^°\c%|¨5!ï·T†Ãº m)TöGåÒËV¾fT¶”{3›0ÑÝ%Ö 19o·K±Míü^4­q â¨÷ ûd±¥u‚Ì-lÈ"žOÔ ‡¯-äl‹®hŒY3 [ÛŠé[”'KZH‹¬0ؾ㮷ÁZì6 -°äT웜ҪJ”¢¦åC¨¶¬ x0õeBe®¾¯38â_t[Äãoèy±‰”WÐïŠ8*Sv(}}Ùˆ¯mg^Tm12âαªî°a}»bFQI¾,äl¾ñ¹ÇÀ˜Â¾jîºØ4ó˜QXY±i ¬‘åÞŽ6YY¨•HÅÖ+5äȾýRY!sd/Õè‚øAºÐ—t‹6Ãå‡'˜ø ’nO &ªŸ‚¥ª5Œ”mH¹_.uhn1­Gö•Çü2gíÙœãû#5’»}d Œp›L"y»³Æ1Ïz»ÂrÖ¦>j¸¼G +k¶Õ*Ŷu-a4Þ)j …¿­“mØ´ÍP:ŠU¥õ¿ÛX„¶EAo…VÕJÒ{ +ÞhùkY +3Ñþeݦ¶X&¥[kè?îÐ@“œ%nµwd'ç+Äf. ¾a$2‰%TQ,$_ rAž0ck\aÈD,#9¡µ+{¾sKº/óãƒÐ0Ž!JÑÑ ¯‡0šn¤…þ¶ÜBOUÏ\uÖ•¥g™„ç銉ÚJA>+ŽßÕ0¨@h¥¼²w("5bÕÖn2}aŠï­j«T=>@QÜJÝŒËX6ëœqãU=`äÂn]ÊŸÙ‡£§­H)PêÅS›«Aä<Zw`‰ù²i¸ äàVL8 xÑ2Ìxc;XÑeŵ< —űD¿Â‘8ë½ówsŸlQŠjúÜVœˆÀ‹qM+XCiyÛ,ñ¡ÒãT“¶V.x5viÙ^9VOö›Ñ£‚)Ú +:¸œ=*i¹UÍàÑÖµ Iàp¥càËîøp„ä8_,0=&Z85¡s=ÏùUü©•à*’.¬+.oÝ|ëy¶i^&tr=zVÒãð?5b&’Ý”q<¢ót‹•7¿jaÄ ¨;B} m¶Á2ªØ¦Fß_>™PÔ~´¾) Í-]Óáª3í:apÔa?î;´×õqùyÄ)芌‚ CÜå€væ¿Wès™£qŽæß ’7Mc»n ¨ +Š ¹ÅCP)iÕ0É°àxÒ%¹ygïMW¹¦Å.è.%ع"ïÕtp¡Ã¾1µÿN7ñ£)ü¨Î.á:ŽLœÒåÊ +l"2BøÊf`iZߺL8˜d/¬¬M‹OcÙîéðîKßJ»A (H–j)=ë7 +K;WF-‘VàËñ{_j÷›·vÄ!_ý•‚VéXžÃãòƒÝ%&D¶x·œýZVè=ì`cŠ ÛÇÚ~·Ê£›¶ÁX»äp—}9YÙš´ïã+s€Ãj&ò¥Ê‰Äá ¸õéËaÔYj²ËìÙ€2³1û½7‘!!ûJr’YßÚÉî)¨Ê|táônÅ`å„Úà,ÏAÍ ïºaxí-Ñyµxs²°6œ¼?Œ¢r+ˆœ){g‹Ãa’>.\ØñB€Ûf_iG¸Ÿ/ô‘÷—XD!“>Fçgdn céJFdtˆ É¡§HÓÒÖÌÑŒl ð!kÍG÷ØK˜Ýï,&ìÔÆ–æp¹6Ÿƒ´u—„šjrwö¸™¾½á^çàOz ñI¢3Bó£c­‰4¦xÜK“ +Úl ÀǃNÿEPZ|tü MV~""v.Jc(Ï5ÕdžÇÒìã3x|ÎwœŠ”ßm)ÄÚãŸùwQ`ªæb®,\0š¼ áõ}Nš¾DhlhTƒ¢Fb=~7ÔÖ'¿}¢=Çœ± /•SUÜ[éøŒ º³o‘¥«Eï ¸Ô¯u+ûç§C²õm9›ÔAá¾5gH i]™!*‹TGë2&|…‚î›4©Åïœ-­‡Toã-3Bº7•±ËU<{ê‘1N$–ŒïÕx¤i‰ó{ÉÃ{ñ©ÝqÜgõW +bÄ°&ÙsR.ºÝžà~Ã1&w +¨‹óšÂÊc$µf’Q<®2¢ôiQµ¯£t£@Uñ©”'×öSÆ×y¡S›ö¥o8Ž·J±øhj‹Àþ†p×Ü+É°¢£–£îmñLÝ Ú{ùR© †þY~½¶Y¸CØríÇ:±vÅØÓ¾ÊAhK­xýo&'+´¸DHxbë¯:+µ¾þðÒX3.g +Û^tì²åÉ QEòND¥Êâv×W®JÒûØh¢È¾}!‚K‚ÄŽ;“ÈÊ(ª™Óæ“‘UTFÈVs± ×Y‹´Þ}ydtC8E}Ÿ“?7ƒIâ;–Ø)ƒxç¾ý:ÄáùñcÕ†­Îõø„­cä0BuÊþ-1K)¦ÞŠ‡¡³´ˆr´©q8zE&°Ôòuü,m›o£û'£c8= 3^âxçß¡ÃöóîÞS±u£Ë™Û%9@EÈ::8'—+…_íF9›0¨\ƒ+TñÚ[᫼é­[ßÈ!OŸðì{cŽ_Ñ$Ò÷”íh]FfïiEàƒf¯œí­«To£ÞöH‡§°Èõí’˜b«­¾Ê… ¢Ð"Îñ£µâ¦+(÷w¤öKïøo(d/räøFCž7Õ +kÃÈ`kDî ú¾bïáA£1q (3Rlœ!Å%Š3~Ý$[·AY;ÆC2œL¯LZœÇÑö>-2bzl {͈‘ñÈ}šíÈÓã``U“âÏ­(ÂV¢)—¾ÅÞ7Å»±o!³·I5Ž_/­Ž]ÕÄ{ߙӌcaÎhsÜá`|²p¶Ku´.c²÷$#4öõÎÙÞºJõ6ÞÃÞ§]Ñ™±K¢Ê©ÜXQ›qFÒ +¡­Ò9AÝÓQ÷±Ýá½+§½ïþ,CÛOI}û¶98ço×väò·|y´uŠz@¡ß8m >(‹5­%Y´økop4óÁ,¬ à=ºÏî”ÄÓV4pô,^zÜ'¤šüÝš *´40þ ¸ÝT·Ù·mni@QvcÚ>>;îQüV:ƒÏY.¼‚³KP +aÄ +­=š±Òâ¶ù`ne0¦Ô¥fþÚ¬ÄÖý"|Þ4ùÎÐÚ<åxÇå½\Úè${Xl»‰!•åÇz ë-E1‰¶‹œ³Uß-}_îšrF6ˆ`šÏ¼=ïšà iñ|ï>q‰£ È?8M¼aŸ‘õiçrk=…|™1š‰€²wžWXC8JÄáÂÜÍ30h5Õ’lsëuÓ8Sœ(Í„z £ ´IÇ¡l¨ïؾn(Å¿­Ð±W:p­òfnt'³ðÎÀÇ(›Ù!ïÑž k)ˆQ¶ð°±y¢¾ό$ÿˆðsäBõ\X"”‚z½òÞ8BÑÎaãDªÈïš|ê§(SÉ:}jöp–/|Ùw%!n9™6E‘ù¶ßœÍðÕ +ö‘Ž ¼8ØDBý’;‰CȽy¨‰„òž!™6ÑŒ(!?ÍÅ°¸õ'ÉH!Œ•‹^”ù˜Ñ#»ê¹"hõµ¹(H‘E‚Pê© y,"/w„HQ»ÙåjÅ*F7QÛ¤i.¶áºYµ LšS}l§¨ïs2LAÒᮦ$7ßêÉØN½3’“_ŒAú»º7RÙÖ½{¹ò%=mH@ÂŒaÞÚdùK¿@Wó°®x~rRýnrÛ¨¬Ý#É- +ÌŸ\m-§@O£=NJ¬*}·{¶=«]«Ë5ê¹9x’ÇR_Úm ¹^º—³R¤Ó`ïþTÝkû©ý×îýcånÛ~VÑÖÆmXöŽ‘ {Ò_[û!×˨¯A8$þ)ÐQnOÜnLŽ\pÒH·–륕K!q´mº´zƒØ"ë»×çÐ —L(§\ÎR:jBÝ”ðûI½“<_qØaº— üÖ@T×C™©(Dçœü 5½ˆúý†»4_í +Œ’—œƒab1 ãóqmñ¢ì¼<@ìÎŽØÿ²"¢ÐçÌ!Á4ÃØïŠaœîþç_ŠÜÅtRÁž Z[*ÄÓ8‚h Ä rËeœ (6ÇèŸ)  +fæþ4ž ð@\†wþüÔs6—§‚rÄœ™ÕBàP.ä0Œ†ÉÒboºíËð{…ö«èœ üùÃùï(ö´ú6rMæØF$ÏG1ä’é¡r-Æi‚[,Zæ(z”Új9¶×W e…|Ì£TêÃU¯'ùn8/`þôzðn.¸€Óæ„éÃÐqêKì}4§n–¥$ZjOó휆I°2Íl¨p™ç*<®„†EÖÀŒ‡¶0סXú®-}/eŸ–q<Ç| šg[pGšCá¾æ)–.rt½óQJ‡&>•L Îž‰‹ˆ ×d†"Ã`Ì­œˆÄÌ pDpÑ@Í ”§"ðE+8È!.@‰›‹Õ• —ã/ƒ 0F×5EÀXb9Þ·¡¹ÄÎÁ°œ²ÿwOÎ઀ɠ(’õ㯘 +èµ@¶VÉ @BzÜ\'ßüìEÛÕ˜â X¯[”C±^À™½k縈~¥C¨ìŒØ?VäÙÚÞ…©­$ÉeÝÖ8‚çöØ£çL[9ŸzŽ=|.qp"0ŒË?çl+'ߦH– =ü9µ/vàrä²Nz•ú‡©ýkIVü¹ j.VOÚËjž£yïŠ'H^ØAÈÃe¥3å\Ô@ Ÿóø`V\ܸŠTH']6 vYÔ“ºL†^S”•;ôªãÅà7Ý¢¸•Î¬®/ý\v`BjœÒÇe€e¾¾ïÓ¸ÆÐõÆÌeù$†ŽÝ˜&ô{ƒâ(0·Ä­2JK¼Õ•/i4±½©ø«q@–rÙpQ7 {é³U¦ºðz‰£kQks%Yö—¹åx2ÑJø,]”¬´Á™Ð×–¹ls+¾{—S©/8;¢Ï’aµm{€oö +ØÃÊW©R¸Ö´ ­Ã Â[²ï±Xq½ÊÕgÐkð}º«”^À O­Œ¡øSçû& «Œ2J.æ È7ô®Ì#HÚIŒ¢ÔÓ åý¬†Éxœ6TNBåú&ÝôÂè|.]©‘IXzt-Ƥ· ¯#eâ»f“q‘ªŽsgáx?Ÿµúk]0 +‘ý÷%ä¼ÂÆ/;!e-Ašê ±w=Â-ºñ §"ÙÂ3yìYÊžyËÊäÑiÑ­À„¸ÊjûMÞ»¦œZ°¡µ]5öõ~+[Œúùðµô2Õ„¢¨UŠG„A3œ'.Þ‘½õk¯8i+èäPuªÄ8E^‚êw¢Bän˜v¸Eá´kVßR»RÜjd&”V\-ιÕÜ©uà*§Ýp‘F#–­× Ô (èäuvª¤:Gà)žîÿáÿû£!tzo»§ +ÃRháÖ¦ +e©/L›|D6÷ö™õdC´œ4‚Á#ÿå€E<ñí¿§]%N{”{§Ÿ¾V®&p + Ÿ›ú\i)à`ê_ž¤$CË?D¨]„ýnSõÍ€@vu›$x?·²|+Ðp¶ƒÙÁ¯fšùê[¿ð‘2²  2×´@ÄÙrU¦„߉!g§l“-œ"èPÁWtó$¦ƒ­ËÈàÀY¯)'Ù­r],ë¡Y«ˆrNlJ•°HÕ@ú\Ó½a­å¾`0ÌÎF“žRÚ~Oš€LÍá¶Øiyx‚J¬úàj•ºƒ„Œùv#¦€ƒ«stämi»¿º4Œ†6ƨabƒ„~KaX ÌHdnõï;w¦YUˆîvüô¤€Å­];ÀKò¨<‰aÏÌQ\…« Sèß»V‡~!夌ýÂÔeþl²Ru¶«¾ÿ”ì~AΦ0ûROêÅtV@Rµ]Ëôä¼|RI +2o@siío@=¥"†ÿ¹—„ïl˜$×ÊAw€‹¡1+¸ +ØÙ:Ç`¤zCê`ù¬MI’0ÆJî g[zÝšhóôrÉÑ;ûŒTºf ¨Ñ¼ÀÈnr½ +Mÿ9+³ §ËIÊTαûµ0¥€Sî ŠjõHào7b +عºÀHHHD•Ï:!±aßKéŠÍ§ž‚†Sº¬@T +Qw ¬Õ ú*r=®¯ˆ÷Iç·8/p'ÔNÌ÷Úâš-~­èßãàÛîv-œ\C0vÄÜ®Jé“v¸³ìbm ùl‘­+—7`áx(ÝlˆV )˜ÖD?¨bÞj8ªP† ÷}}Cb¤Nn +:[ +8$п§ƒ+-œ\# S`Ó‰7´¥îØÁ0÷=‘NíGN©WÒ ¡ óãhÔ.Ìù(ØÇàkù–7dýyKe66ÄNɶëʀѭìëP ¡­QEL;S§ô¬fIç‹i7ظ;6¥u%;ÛÞú²=@<Ýêç¿Ú4~»>çדHDÃÖ’[e¹ÏòhÅ`6Sž¬¿¬tÀón£²;?ueûO–ñãiË⊺¥ÃEaîÖbtrÛÎWB ‰žÚåBÿ¹<»Ö\;Ø×$ ðp¼¹¨L"š®Óbé·ÂÔì, +ªK1œl# Ù…q¥]»YÈùüÄîà‹Õ¦8cQ3Œè Œ¸[:»Ãù8öý× +$ž[jqtЮ2 ÈÉéºÝщù–6oek•ºÜm¹Ö„M¦Î˜ f}û—hÏ#³¬]Cñ>›¥ ïJG~/Fª–vEm%wl‡A£çP×ßà Î=•›‘.< EvñÉÔì,x¸R‡Øv®n0Œ2»êçw^1:bð}ç`í·ƒF(%ýsá+‹á%íا]=®êÖ|eFž9¡„1/öÆàh÷ê×eÌEÌŠÎÌìŒë`D{1‚1 ]$ÿ3;ªv|8SQÒ1ŒãTn÷1Øôù6QŸppž†zÛq +5 £ëq +±OÁú­¸1ôó˜"†ãISAŠ­ T"èæÉISÀÉÖ9¢óçfàä¡uÛiþ¼kOžsRÀ¹Ÿõ¶ ÷Š&ØuCtxIòÛï‘cgß QÑŠñKK»ÎV?ž›Þ¤¢`“Sv©Š€,lÍFñ¡]ºtË,…Î… xaEùD5°ëŠÞØf„ÊJÑý/$ ÐAÁ¥­‹bS·Í1Ûr”®lM Aw€‹ßöØ ˆµ²D[ç +¡/¥Ö÷ÏiŽƒZá +8¿†ÞÁö9M.4üÆ´u dÓÄP‚פõsšƒ£EÐÌ/gŽN¶Î1Ý0µ<¢ëàðƒb»Äkà 'px¹o@ädËu€Ù ¦3‘wûØ,¸–š¼ßHwb}l4W6пÏãíFÑRÀÉÕ9Ä +¥½2+MÃw$òX?”JË€ßl 4-ù®i¦•ú´PtÀòû®,4¥®U4K¦¸Ÿ?&•äL=¨(Xgç&{óÕ=R~\’þŸ÷wdL (‰¾¾®|Ð$Π¸ü„}ÿ…rsí}}‘* åð/¤ë"«Iƒdoÿù×fÇ!¿­»³HÜóQ·n…œ=ñP$ÿaìhù©$9¿Ñˆ|þòKëðX«üäd6 ã ÓaCÂïØGbPœãuŽ5ü91Uÿ÷¿ÿ…Rƈþ*ÕeƒC6sJ¼Òf+ÜÎ01ª7äŒ:Õôý<Á#Wx˜ZÙ ‡¶2[‚ +·fÆA/¤<ö)Û² h·Eú­šZ?p~ +aA™¦ä Ÿ/*¹Â¨-ôݵY„O'¡æ9Ñ‚P% ¼66ÈȘépEgzþt®Ñ¼ž<'£µüškL +ˆ©2îÄÁx vÞ¢´t@¸ø‰c9)m¢¡ ‰a£·7­<¢ÛñàuÁ@u‘Bºiö/æ'#,gž–NáLÃ'{H‘… r±+·Çì2?ÿÆúÿ>qþÎúÿ¯#+­¿…“9K írÙçœê ‡«¨ Ôæ^Þ"‘^ÅàFëqÅÀ»¯…á^ܵI9¼R8›c<1~áŠ]Là‚²·^°eÑ·f8¥cX´ÄI /}Ÿ¿ùŸ7½Àë*&ƒühœ”ÖZv lh¤%o8Ù (“æ¿öo>;œ*k4Å8‡Vh~ñžN”¬¿o(@衪‘{\ÖøŽs›Çˆ#ä´®¯wœÿ¼û/êºCÜq<|Í4š¿}S„Œ†*rܾÜcÿþy÷^q=ò}Ã9´ÑÆíE£}ÒÿÝÁìUúj!,Њ„û´¬ºeÀ[±Ã Gp—FjŠ´ kù*ìýì$jŽö!j +ð"_œ°¿ç hѧ³!E["01p3ÅÓ[#Ü´QAjtûݺåä÷OÈÁ‰­» v–üç[cäòº_¨¨âS¤² ©9lãhF ™ô^j²èæI­ñ·±qÈ7‰èDÒÚKשݒ0æ†M(çëÙÄ‚d—»4R“m·¼%6©(¤·!‘FDçg#I«ƒ*KouøÕÖ6»gO²±ñ +Œv·¹Jæ aÖòÈÖÎÆíË®1´{®¾ ŠÈµ~jß‘¼ièuÁÕ‚ GÐ#IWah°à™¬ÁIGE°’Mš£‚µî¹„݇Æë'äñ~[ÊCcEÞî>’€ Õ +åT¦V„¨‰Ö"ªA¡§âñMD”Ð’¨û>>Švây´9º°rÈŒ¥íÙôCV£÷»5rÂc®qžG´´0ˆ-óûÄ'OW@šÎ£ c ÷p8Ý~P(Ž»¬1G=1¶7l#ݳu™Úy¹àÍ:mª axPBÉ.2D¾(âíªÆ9>º½T‚ŒõågtJ¢²[Wõ7šèë«Uz#} +ËÄ&”!M7­ »*Ë.äígx'©üAš&¾àúVÕ"”‘DÛ±¾ßÖÇ =8P*j­*íû»ñhC:Û¾gñ„@ˆ¹>uÎdö)‚0k±ªQ.c™GÓ‚Ž&~d!½þ ü×èY)Ô2xL_/¢ýÚ‚½?W †Ûd#ß–P2­\´ +­N½&ðqu´‚É`UVÏXmžê¨få= ŽãRŒ3i¨ÕÀ!UtôÂÎìøŽ°OÀð²í«]›@&ÅVˆîö;¤È·í­·DÎqÕúÙßµ¨=€) D4š2õ³0MSó: +Ѿ½qfZ˜paåšu—82a±¬W¹,Þ+ÃŽ.å+è(ÌX=„¡5õú}" ‚æíþB:œyn¿ã—i¥ÊœÕ +ëýâv†ùYãØ`¿Ûè::¡±žOe]_!iîÔðTG¶)µ›÷å9qGÀ1ÀÐx[H­É!5e™|HG­‘«dŽß•/EÃÆâ/+Á“Œà–v·tH¢ì-/Hæ¯?‹TšóÊ soBNðÉ\ûüG£ƒŽ F,r^¿Ï‡Ïú—ÚAQk³6çqVŸ=ÿnð­-!ë1aKÇ…,Rµ%Sî;R†ÏÛ]ÐÍ·Ôø4Z¼j‰|^W©´aù PÈè×Vm‘cï7˦÷úÖVùxŽ·¸6¶‹#6Q‡Ý;£ï6¦ã Áݼó?º}kD3².¶n{#}"¸µÒÓù¶ë¦33ùiÍ_u-"—­¿êhjâÊqÔ”qmÁbEÜâÀfru(²Ävßã‚ÔvîPcóºcaÏÁ£Ìe‰ÔLŒ|®ÉuUi£év +ëÕóK¿þþ:µ‹–ì\^µ+…QÚôò;°§sÁZoºr9Oé:µÛ˜<å<+Aš’åAFÊ £ô.ÊSØ<ùþ²‚ÜG´7²Çܲ{×—µ4x½­Á"A—ßÁH3Áߨ¦ëˆJ£JˆL¹×±yŠêñAö ›ˆ£ƒuïc¥³=üžSFfÝpœà ,ð†=JPB • hçlÂÛý…Çäþ-™ûqÈtŸ¹Ð‹AyÃÿõïÿ$3ãÿ“tø¿ý3»yýca.ÒÖÕŠ‹sTºw\E€›lˆôa¶&˾͸r«|ð–ÚåKê!Æ”ÓÚ.naÇØbêh*é°¾Ž¡~ØàÑ?:Eµæ̈#Ê|©º±¾4hq—Î`‘ÓçòÈÇÚ®%¸!7t¦"•ASÅy½à`G*7±Ñ𻵒ºŒ-7¬d^©¬^©-¢„`0×6Äx;×Ò‰-½Š(5©:M\~Ó"ãBHákoÇ[1­-i/{ïØÂb°HÚڻ׌qáà +óF8Ó]ç`\:δÞi›zäki¿Èõ8Ö<þ‚x àdTð‰¡³T»è₹¾Û§í‚~è[[Q%9óÒŽ7ù}ïYd"…ks§YB6ôno'½D'Ý~² GÏpdF  ÷¼ð„¤°ˆ%ìjk—éÚ®FCõLç”1IÚuò´µ2=Œóð $äC¨¤×àhÃggZPœ¬Mõï†C€jÕÙ«ã>C÷Ë™|à‡©ñŽKÖ(vbH¼e³Ý1ç«ÒK¼Ð!óÜ—M…(nQ­]ÖUâ;Æ2nW:|Ý”’yæÖ¼Iüa~þ`<-aÔîŽÎÅëè"ÔŠeKè³c§*´ +|ÃÀ×Yøæ´˜ôÔî¤vÚ•¾5äòdS ŒB_K6ºÝŽt xâßlÃi:'…•G0¢òÏìa•‘Û‘È]µëQºQˆ÷›ÅGEam?e|‡¶Û΢ó ÅUI5#Y_" ¡Íá\žY#Ñ"„G·3in}OÎØg +¾å½}‘êÞÚÇDúvné»r7ã֋T/ã-ö‡C­ëì».&%9 ©Ùí¼ÚjPñÞ+m8¦ax1÷HW7]E\X\Ú!"­'i/…sÄT¬ß›/ò`¢¶I…´j:ˆ*BÜœï•?›QæÐý©½½^—£ó.]IRSgD² ½˜lK“C.›É…åÖ/.Ÿ/Ò^†¹88ãqÊÖú^•s“ìÞ®†åJ!õ5´Ú7—öU¼§1×Ö +Í‘?oä–õ5â´Bê9Û‘Ö3´"®Yb¶1´æ,–#VÑc+*­@¯ì]‹`Þ—[37tâhÍîkmÅ õ½Õn]x¤$]Oó±3Vq%c]kOåk +­•J¹U ¼1"—¾a1eĶågKë!ÕÛhÿ³I‘!q‚gotx¬-¯/7³È÷Fc-±ÏžE9þˆÉô£ÏÞ?ãÑ4ºÄT þV@lŠ4V‡Pþö»¥É#¦·™Ý{—°²/3Ñwc'òUBCˆ¹¶¢»{“þÖ-'Qs÷ÂÒÒ´ +ò4¦0ü‘vÃÎJµŸ›áT ²Õæ¹Xp{B›M?u—꥞Þ6I¤&#ËË?µxO{s§ÀãW8Kß8¥Vdá—ü×ÞŽr±´¯µx+Y(°G™¡Ï³/%ÍÈ9”àÊtïÞ>hï¼ ¥‹­ºÏ…³µý”ì}ôÅ‚Äs0Ò:Åð+¨O>ðB¿Ùe[8GTl8¸Î ž'‹Å—ÖúŒK<Ö=ЫÇZÅKEò¾Ú«Ÿ¥´pŽäÍ'u:„ß1ö_­w;§ÈV—,’’‡>%›´WŒeÌá´6ž¹Ü0.’~˜—aÿ#1~¯b¤A¢Ð‰ã\m‰ÇzsÎÒ‹—3€a—ä‡v„¹ÀïúJA¤­ð +‚AZ¦%¥oÝ[»ƒS¸ëÂAÁ±óF§Çi€¬„–÷‚.ÚNÁm2ÞÛÕ(])TÚI²+õ‰Ç¥ý"ãë<Œ“A…k|i;-}·µ+úÒôñÊL¡6™I¥Õuads²>âS3gôk§É£{‘‘l—Vªv@FXnCƒÏlFÚMœ[³ß»ÇãY̽̅ëG‚É?ÁF'ìÙ¯M¸kóœk÷Èv‹{bO7C8ÚÇ.úaì§é ‘=(hÍß.iŒï‡ç3Šdbv=O~»Y ?8ßØC­sÄNýŠp‹›i +‚“-Î~-µX‹k;¼ê5‘WŒôÁ|¬Ô j›¦’ãÒ8ÇàÚoFfi”ÞØ™ð‹ Ïãú¯»ÛÂ%„mwm;w[>—Žw«‰±ÝÝÚõÍÓAá¸Û"ճРFÞﶨ½Ó­ÚµsdÉ ³àÚùÂ^¥Q!ëÎñ®íj€®ôµÕÆÞÖtHö:ú²£5›…Öw–ýômHVNÖ~±ýÀ±‘´É8£³,—g ¶ê\}¢£-ÇN•+Jds/ñëÄÀË>™¥÷rÒó*~Õîܲ{“ÂKú:%¾b.áôBGìÅT¹Ý1N‰?ÍO›EdÒÏ°c=ÊÐ|Á¹€>y›º"é;í™8X v(›v ‚íõ‚”:ˆy #§=„ øŽS¹äZ$‡>BÓg³ó«Þ蔨÷¡¯];ŽŠ[Tb‰±—Þå]`“øŽ¡ÇíN'Óš#sñ…ÛÌ«’Ž,˜Ÿq6˜ß+ã8R-È%zÑ•2;°$a]—]WÞÚµ:Ûú?4¥H‘Z»I‡¦Ä8˜`[ÿ‡²l#étÄ\„¥÷SU¶¹ªÂ}<´Ù¥ÿE¶YJZzÝ~“îeô‡EIV¨Ô(nÉÈ»`ì&%*¨ øDÊ:i/¼œ›+„Çv¤£ý-])ˆ£k×]Š`ºÿ¤˜¥³]æ¨;‚Ä¢óÝ2vÃrò(ó”5-ã½}ŽÒ©96GŸxÔí7_çaœÔH·'Úû¦†d@Þkû~UúÅT÷ԌĚdk]»—×ZmÆòï/j]7_öÝýukÑì]v'-ܵyεûÊ«¨<±§›/½ýZÅêðþÐå"Y|´nKY‚Ð +<©í銈8˜Gœoà Gò8yã Ju û#V{0F¹a œ.Y‚ñó¯a豄Žb/¢Ømî8ñëà‚±8 Ã2?ϱ;pÀ ˜’–¦ ’§nwŒ]fæeÅ9Æîû†sÌÀÆíe?­¿ï¸ÚJçJšç&¼ø¾:Ï·sŽ‹ÂDË{T>çøPÄýF¥9Â/õÀá¸*ÚËhò¡ùièÿᘖÚ+Åb±™¤±µ-R¸2àm + 'ñH̱¼Ñò§S$äuí­Úµ0¥t´u…†‚oܤŠ,9ÝYFdn¶d-•Œh¢Côÿ„ïópOS$Ûe¾K{ãp†$jSB 9œ8Œ˜WQN¼8d[!?_õp·=ƒ‰ïQ°/Õ+ÂnñŠ›i²¸ØÃá»&Ññ¯ä.1/;Žå¤‹>ÆÍÚ×Áì1y‡ÈÆÏ¿±ú¿Oœ¿³úÿë_ÈJëoá zn!89ÙxCáà}ú4[‚¸CÇy "Ð"Š±¶¨{ ¾ÇT¨<®ŸCvS ;Ãåãg°¢S ðC½öÂ/Å1b•”(±û. g~¦uóõù{ÿyÓ ¼¨2}v‰{ø{‡&qJÀàA“Qvé I˜û÷ )ô„—âpâC÷±ØÔTë¡­ñ™"ä/„¶¦6œË¶7…‡éË÷8ÿyö_Ts‡ +¹âXdñƒwˆõö=¹Væ…>ÑB*2]>ÚaÿòÙzXq ò}ÃÙõÐÎì©Ë>jÍ?¸ïpdÆefß·á¶Ïq#݆€—iQ`Ù?áBIÑt ÜŠûn‰`•Óv‹r?AaW§‰/{Cᘠmð`²YM¸¿Ã±0fµx2<-ƒÿ€Bk:”Ð%tú|†áıÿ¶‹YlÔ?óš-+Û1`Z‘ÔO|ÄÀÈDªfûÒ .çÉúž#öµ±¸a @ÁäºLÀŽB&éꎂN‰´²ýÛJø°žzȹ"÷nâ<^yc0GX¿pÇ!3{ñðºSË4Gd-¨¼ô *kÂD# ¼ëy±ó(Â}Ô¿|2œ2Ò(i$‚ºÄ¢ó=V¢ú.“+ÊHåÕ‘9îLé”3Úf +)+æ”Z¨¯–NéšÄ9¡†9‘M^tÚŠ^PH,WûÖ Š3g£8ÙÌ£#"ojZ¾ï™)¼Úê'A -T„ %†<´DÕ÷‹UTÝ(VU5Í-,¶‚»»úSk™ä •í“_?ð ),ªè 3õE_AVoúÊ"Õ½{ÓWÞmVk½mK¦1BÕ ½«óiF‹cáðòbª}7“çÎÅÉ/üòq´ qIal&!‹œr|Äøu~„—^P±.9”&Û·±`8Î2å: ^¿\Ñ+çÄpz§M„Éé­Ànf?Æ-V«MvÔ­V*£Õ´ñjÉ"éÔ BüÑâÁ•F=p§äX¾Rγ«6åöIEN•ê8ß\,ú0nMUô~7CËUõ×’ýÌvpŸËû“ó´5˜òÂ"Íôø¦¶QcÕ«íù:¬›ëáĈ|ÿµ~Œg±°qÒᯄóêÔÿÎi­¤þ +r{ègdzˆƒRKñ1?¦Εi;›ÞN œ¢ŽNѯrï”þ' îal¾ÆeYqmìçžSîukt(@ãß6XQ€I›û9j“Š‡%ĜʑL3æçßø”ÔqÈù€"èÏæ8>;¤-´ùÅdG®EÜ'ê3uYw•ã¼ÈÙQàjér®JãlŒ\6¯Mœë著˽ÜKqÖÁÜ 'ÆMœå“‘˸~šq/u¾or<ø¹yÛ Û­ÚæȪ./7Áwøe†åsÚ¹|»à·ÏÉ£"F}û(=|ÑÕÅß…“ ã&Ίr“•‘Û¸~˜OSþ@EeZ‡á­tppɲnw?–VÔ±8†¶`?pPl»f3Õžo é¹GŒBÅHç³:8ÏyÔº#Áþ +aö²c®$­F¾ÑAÆWÍBgçéRL(“ÛCâÆ:n:§Üê_¸=0‰?Í‘ãläôwÑ‹3„œ zº" né”é8s ã nL:aІç9L÷ÃÜc<Щ“§ï|ò‚7ÝÚprü:1÷  £žt8q +>‚jtVn9ó +ò§tœCâÆ:nw:(-–j|ávøHüa~di/¬´*áÖB£VçœU>Wƒ++»”!Û`îR¤`i %_1È슣ŽHø ;y)¾â¹uÜœìxM<¯Øœ:^ÓÙ¸E›¬úÐ6O„mÔ®Tê@ŸR|áuøÈûavF‹Cb:¿Ñ" Eõß– &2âŽ׳ t%#ýa’µI·£®‚}B –ÉðµÁ>PÁ­÷t¬8ÕóbëÄ}Èžbb•^Ò…r°x$¨“>@gc¶ ö[å½al£v¥ÃßÙ¤GnÃÚ‰?ÍÎŒ‡¡½ÈÁww C¿“RKH^Ž4²¿Š!eÕöK»óŒ €È´r¨ÖàŸ1Ș@.ùr'ûb+9™ +Ù-d^èNV dTñ~&øñÎh2;¯¨YGF’êd——1j‰c´ $Lή¼ÎìdÇ8å}Ÿ›¦´}âˆMÚ%›•‡¤$ŽV–I¥'3Hú†Ì4ÛqPqj±á`Mz¤ŸDâ»g ä ¦¥^èˆÐ5xd–è8K¦ãœpò »V0ÊI‡¸Ð®¥ÑY¹mEµ¢ÏçøŽ¡ÇMè·ÐÿüXž¹Ý0.˜ÙziáššóTîŽì×ÀYÈ«»›Á± 5gßXcÁ[à ÍøÄ1»7 Ri©>Ñ©™ûImE¨WÒp²ÿ:1ŘN']è ËsNht6n‘%· +Žý:%fŒÚ9 _ç¸Ýè¹GÇ:èœÜ†Q/tÎq3I¤l7³H_N{S•ÅÎë´dÇЮ”Ç5ã“ 3âþ ƒ³ø=Ñ©IW +¹ãTCÚgàœ¨È„BCŒÁo‘;.ä‹`Ônkn9 µ…Î×!ðaµ+•øƒNê3¯ÆEÞ³£ ¨€K’8Ô§EAf.‹{å(‰šŒÎûŽƒ\3¨0ÝpJ³?q\^0"*@Ä®È:"º…‹ùÄ¡µI;DÃñ_'FAXpìîB§Õ–HB'vKxáÏ´5Þv‰ 4 ClÔeÜ®t*\æMõÏÜn‰?ÌÏŸ1¡*ÑÖœ‘ñ;É +†¦:n/P('Yåm>t‡Jg.²ùÑëÞiŒ…ÆÕñ©µ‰ƒÏ01—x²=\}Æ€§'\žèðà¶êB â-_z _'}ìt Ž ƒ÷ÄNË]cª‘N•Û–ÿÆãF§_Amß1ô¸ÝéTNLlë3·ÆEâó#ö6|o¼åtu¨¸nY¿ÓñýÄd9ŠSp Ç" +qJ;1á©ÝÄg \¼E¼îÜéÈ‘ XŠ—Š· Áq_'FäÕ0ü83inÙ ¹=ç}Ú&ñc·:ÐGVò3·;ILß™`” ™sئ¹ìÇHâ:ÞŽq ÷²K‡qnBS;^%íì±á´ RJ Tâ @õ‰ŽHŽ˜A;qjñ(V>O_’à8Ó1ì…²è¾$\ÏMš[ø3%¤+­}ß%¾b,ãv¥±XË ·ÆEâó󯼱,œ<6mró }¼O°fÚÓÛ%àa½H܉\î+‘í§'Yn弯,\¨{˜õöB¦ –ª ™ÕúÅ´³&³I{ÅX†ìFf½¼ñz`ò¾ÏLÓÚÝÊ +ÅÔkÉØúu@ÐV0{ +*nøq#0íÊðŒ!Öée§N‰}Z/éëÄÀ]ÉTz/'DzÔÊT'³ÙÈ«­_<(#x|Å@ªS_Ȉ…žyÝ0.ò~˜™C„¯dƒJÍž°ðfZìzGŽ½P• +Ñóvè&>H¢R|zÁ@g–>Бc¯G­–Žƒäf¬íôu` ½‡i2t"Ä!sÃpýØ«¹å4#ð˜»Ë&ñCÛ—ùE>ÞGnc;™¥g‰?ÌÏ8ÉèÏ•±2Ör®ÅÉmå¦[dŽh#gœËÛÏ cUr‹2™’àä‹2ŸDäZ’^NeÚF×­t6mÊs„{BƹiÓ6‹U褋š»ÒÙ”e›Å¤GeŸIüa~†è#¯Ëè#Çr–©ˆˆª +‡óT‡á’Ä¥ëéxW^0 +¢³Éèy #Ö)…’ ¹ëÃð-Û1då±<§“næWÅFs+³˜5Uâ Æ6nW:„ÿüÌí†q‘øÓüŒ)XK¿T»îŠxÓBda8ûž‡w1Ú"Ò Føz¿é‰.×ÁáË‹‹Û¾KìçN³Ó¹lW;³çž· |ÅX†íF&óŠs/¼ny?ÌÎ[¡–EèBí±œyÄiÿȆË}ÿ×娭J›M©¨×õ›ðþšý†„œ()gOGYäxAA1ù‰Ô¯¿€„ÇÓ‰³›;ÝÏ‚0óN +è¹jR–íDÎ~.‚ï(— +vC*¹Î‹cu]õBú…¬·RmŒ5‹²™C§t? +’*DÿJ +©RѤ.,®4û¹~¢x’âGiûÊ2PVtþq®þà•WFt_Vu +Ũ•“âˆZè+RöJª„€Ÿ—oçDÁË )ãDzöƒ4 EõsAA\e¯¤ðŒ‚&ua™4 ò%~.‚ÅLÍ{À“‚aíaj í“êç@Ùÿ8WmßÆYµ¬@Á²\öm.e½Z6(NæG%ØÈí(HÃ˜Ì )Ÿ™n7$$¯žý\P2'^}%…бj5© Ë-SËÜ)/‚Ÿ(Ç2©ÅD û-®|gù†² þy®Ú” SrxB«Z¿ÃȬ1l:Ìx§ÈaYŸÅÁ7'½jÖµ cÇÛ—ËV÷s Ð&XRx%…šÜÓôiÖõÁ2¦Ð*ëñ"8£,&ï1€'©§ÁUyï,_PvÁ?ÎU·® WiZvŠjæ¦Ô¬kküJ®™àÓ¹àÆýÅäT^H5ëÚ£‚߶sù¸X×J„‡žy%S§.g³ ËH’ÓùüD9ð …«—°àv–o(»àçJ™bÖk\8w-%ãjb'T T[Lìýtz  “‹/¤ÚÄM%:.ò½˜Ø +òÓΣ甭dÒ†Åê;Y¦¥«Ú-/‚¥ÌI» à…>²U»,_PvÁ?ÎÕŸ1Å2ûÒqxÛÙ ¢»æ¢‰¨i±ª9x¿ù4Šœ¹bïgGAŠòR4¼!Bn=Nâpä¦&¼¡@ŸûøJ +‰Y­Ö;Ìò¶ÛÐ!³Ÿ‹à;ÊeOR™ì)ã_Y¾ ì‚œ«?èùGZ‰t}Úþ\œQÙïȈV¤ +×Ø©K}=Öíöà‚+r„¤RãŠBM;’Iê²ç†’9Áà+©„Ò¥z‘ÞXN?àÿ=¥º~¢x’‚f«û½ÒÂò eüó\±e-?í2ò‹žRÜ5ùÝÊÀ“ùr$3e·äVzŸo¤Ú‘ ÷‰›Íƒ?ÝÏž`WRq¿9©Ö,¤.,C­u¸¾£\ð$•~”Zí+Ë”]ðsÕd%Äý>©ñÖ#=÷íeC–#™ñûΊRŽmo#ÕŽdtšÙùÁ­„>’(œVǾ’Šœre;’™ÍÈÇ–ä–CÐ!ø‰r àA +¹ùCŽo,ßP6Á?ÏU?’çwã ‰¦Ö#é«GÈùårç_×íä@Á ÃïfŠ&Õ„/Ûa“Ìv¢SÂ(ÙÁí) /?«OÙ–iæ’—#Ù.ørà…TúaëzYv°|Aà%¾‘:pØïðjöËzu ¬ž×ŠLQËqX#A½ÃàJc½‚:PÐo­/¤d6sY£ª¾¯¸ pXûJ +)Ì¢¾¥º±ìɲQ»åMð å6€)ú +ëËÎò eüó\ýû]¿Ê>^«Ò¢¤eªïàøvq}tœÉtÝ÷;Ê Ê~×¹‘º_«:öa]l…†ÈÈÿBÊrRÉEáŸ,’Ïêˆwü@9ðBê¼3=X¾ ì‚œ«f–ÖrW©—黥‰<#/–öŽr17RwKÛÑY +e[_,m‡bÀùÕ¨'_ç)ùniã“Öw7ÁO”TŠû@ê4£w–o(›àŸçŠ§”Žr0!4š7˃Ÿ»³[‘8ºU #§«ÓKþ‚²ïR©vìN~5=‰ÖôrìÞQ\ýL)o¤\iNˆË±{g™\Ñ«ðü@9ðB +ÛJ‰¯,gÎù*øǹꇧýcnhÚÒ¾èŸ}¾.Z삲kÃÔ]ñî’Ý/Ƨ–7Rç8ßö +Ì—ò$¸í8Û”Þ´áIê¢U·)½ ‚œ«ni›0vù|ŸRTx@êcJÕ-®G¸ÊåC^PŒ‰ö}J‘o?„}%¢`©6£w”}J/¤xœóöø±³¼OéEðåÀ )ÚÙøÎò‰rþq®úyu.ß÷R¼Ô¹¼#­{)™£v=¤(N«¢;öR:ìÖººp[Êeߘ.¤Î îÂò¶QÞ?QŽ=߇×"_ ¹3(rÍZÑ%¡¸Y®fŽ)ž*¬l?™hR*ÜcX²ÔiBfvo‚Hê8Ã…Òr®(TFk éSÈ´|†ÎEŠÂŒõ¬±˜Û }3Íb«—£íØtÑ/ˆëÈÎX e-oƒ0‡)Ž°qš2T¡«(p„Öˆ‹mYTšä¯ ؘÇ:# B°ŒÒ¸eéEÚHÝâ.?ç¼üWÓ± ¸ÃEhDi4Zú!õmôðk$JŸc‹[hü>ß·Q¬Õdq’0æÚ‡Òa`ˆN±¥ èÄDDtŠ"jæ‚o +ä¹ÿxB±Ths#L,ãè´–È(çʪNÛÏrÀ@]æLô ÞÚÂÚ„È]Zeî¾_g¶×¹â Áyh³/-f ý^¡Ž–WÐP| ™‹á!×”ïú± mT¼|ÈûÅ|ÔTóŠ‰ËÔ$_Ÿ­ ú¾‘L5l‹C¢ã°ný®„¥q¬ÿ2žÈ8b–¸ç^ùq\·ñvò̓U¢Z¥A/Ð^¥µ/8ó1¬»–^G1Q‡ia,BÃâm|YŸšN¸´"—#‚%Hl“c–H‰Ç® ’M"&}ýc¥'~Ú µ¬õu…eZ»5¢‚¶ì%m%Vd²]¾–ó»š_^ö°´}·õJp03RiÔñCÓôE'm2ÀªqæD†®UWcwpiÑà{QuýkªŽŒ…–‘ö±ýzB1͆ËvÒAíG”öÚw楃+Ðÿ@ ØÈüe(Ò7¡¡£ä#jº¸ñ¤}…ºÌµ°whAö.Œ©›çÐ0¶ Bã„°GëËŠçqе“3´çGVŲmû€’Á·yW&F ¹ÎnÑKJ'#’ï,+¬±Ü­f»ƒ†y­€ü@æÀàìnèŠ JË„ÆÄ°þ‚ŒõlíÒ³‚)4´óªz"½ B·ô¯|ŠzÁ@ÁÅ8e …´™†[J¥Aj¤4ÁÍßàìpÌù…µJý9¦ºþê|éwBÔ0 ˜°©ºÒ¼Èÿ”6ÿ û‰ôßêÊZ‡ Èã—IQy„„r—Õ­H:ä7ûdu×Ï”ÄŨŸIýú‹‘8ÎZ!­ÁiW”5ÄíJj•»²¼†Ü]?P쪨½ì_Y>QÁ?ÍA.zbÒÐö¨ÖßלvEYCÜ®¤öP¹Ë[ÈÝMðåÀ )ö)w¯,Ÿ(‡àŸæj¸’àÅ”k!jÜ=*‘LêÎåI‡Ü +ß2Ö7â¼àÊä‘”Œ€!=£‘Öà´+Êâv%µ‡Ê]Y^C_Pö¼ÂGìËŒ²( Cðsõ£ò,j-Yn–½`ʳdÄã5oCÒ!w„R°Éä(ˆÃ|&…1F†¹°*²58튲Få]Ií¡rW–ר¼«àkTÞuORœyÛ閭ŠÊ»¢ì‚œ«¦ä‘"ÚýeQyŒdJN«®Ó!w„âÙ«øÁs}&Å£2c´+’N»¢¬!nWR{TÞ•å5äî*øeÀ=*`p/üdùD9ÿ4W2¥­<ôº¬QyÍȬyÛ·uÈ›ª3å%!P!=“ëÚÄuÇ]ƒÓ®(kˆÛ•Ô*weyÊ» +¾Få]ð$ò ¾³|A9ÿ4WúF®u§X£ò ÉzÖ½B‡Ü±©J†{þ€br)ϤDxR'uEÒÁiW”5ÄíJj•»²¼†Ü]¿ ìx!Å)|Ò+Ë'Ê!ø§¹R¦™m(3©p¨<¶4“]ŒÑ-äŽíÕítz¢D®JõLJF ®*q N»¢¬!n7RG¨Üå-*ï&ø•wÀ©ŠWÝðÊò‰rþi®þŒ)†\OX¹Ëþ·Gå!«1òÀ‡I‡ÜÑa…È«ù€â]¬Ï¤0¼x!ñaÙ$×à´+Êâv%µ‡Ê Ëë~¼†Ü]?PΙ$¿îÅkÈ];ßØåòòDÉ*VîFJŽdȸÚ<:8튲†¸ ©¼Ýœ¬¡rW–×»«à;ÊeORk¼Ú7–/(»àçjÉJX-Ô=*‘lð›%§CîÚùÆ,ç¤ÅíÛÞTÆç­XËÆN»¢¬!nWR{¨œ°ì7K.®GŽSð Ê>€R‘&°ÆW–O”CðOs5Žd5„ÛaÊã“IŠ^ïÇ[ÈŸo|]¶“%ýÀø=“áËzØÜ¢ò®(kˆÛÔ*wcy ¹» ~¢x!áªRëË”5*ïoÌ•¶ß>±wÊkÇ“dõÙBîø“ërYx¢À?Æ–gR}Vsc N»¢¬!nWR{¨Ü•å5äî*ørà…>²åœx²|¢‚š«QTÞíªîˆÊ“ÛÅê×e©Cînw”7”í®ó*»Ý™nÁiW”5ÄíFê•»±¼…ÜÝ?Pμ:ïL–/(»àçª)üÓz;" nç&p3[O”Ãü=áofôæPEYÝò¯¤v÷þ+Ëk˜ÀUð JªÆ~ u˜Ñ'Ë'Ê!ø§¹’)E"õ¢w©#*¯>sXÖø,>ÃÒ‚ªP¶]ê*ã3uòËAw‹dº¡lñP7RG\Õå->ë&ø‰r à…!³õË;Ïb–‚œ«qxÚ>æ#„릎ù:µØ eÓ†GÒM«ž’Šw‡ºêøË8Ÿ{ÅŸuÝqö)½hÓÔE«îSz¢‚œ«ai›°Ü_¦4àäì1_úX˜ñtw~È+Š1ù\dëá)áWf—lyù9PŽ)=I¡r7–)=?QŽ¼‘¢sµYøÁòr +þi®Æy÷‚ï{)^ê\}Ý(épiírH=Qœ:Ê3)9“Â\ö-傲oLRçwayß(/‚_Pö¼Šˆ"©¯,Ÿ(‡àŸæêÿkTÞ»g¡w7K`Üo€Jk(ÁjzìÇËåí0·BGl-Îá<ü‹ ŠLS[$÷lsÝ¢æÌ!_í>ÃæXDJì+KÀ‚â-†‘Œ û°¢"V +KäÜœ£+XbçH{svvÆsÄ»Oí2p=|® |ª½o®`áœÍK÷ÓˆS’:âô/¸ ‡VßÏÎAt+pDÑ­` £c`ð©û€³ã+s½Œïð†mSa\ÞpùóXBç–˜±s—Yƒç¬EŒu™!-zŽÀ8løއϷy‰ŸCa§Zdlt tÆÊQ6¿€ÕšMð…¿Aë½ñÝÈÑv³ϱ 1cò{D°¼¼ÌPÂç®Ã0GŠš3-Ó:´U œc02JÉZàÈ9xR RÍ:GºáŽkt†‰ëÔ¼árì]wÓç¨9FÄsö‰”°9«•¸9î"†PûV‡QXˆu¯ôƘ«[ì*FÿÀeÚŠ›9€QGÏ­°>·€{ü±š³]µЦ1KC³( ®h?Yù…¿=œas+xÄË]gò?†Ñ#B#×ÛÔàï [ÿŠ‚(ôRŸIýú‹‘àR¡‘¶€°ÊVv#u„§ÝXÞÂÜn‚(ç^Hy•ƒìååüÓ\ñ„øä´¹|‘p7OfõRÛÂÜM&“ÿ€2KHÝH±ð_¯W¤% 솲…•ÝHái7–·0·›àÊ9€R¤ÙíáQ¿²|¢‚š«6¥žl R·”#Ž#èmØS–07„“)õ,‡ˆ{_Ö€Ÿ#’Ѿ°¸5ma7”-¬ìFêO»°¼‡¹]?QμrPÏ»GýÂòeüã\M÷ „°çÍ!누˞,Q·z3oanÁqõ:P<ÌÁôLJF d³ùÞ/a7”-¬ìFêO»±¼…¹ý¿ÅÛÊ6¹±ß¯`îá;1øÄ“Vk×:ðÁ²YkaH09ÃxpÏÂvr÷©iÓªÍÓmüy`†¡T¯T%uK¥~ôSyŽÛNS8Æ}æG“¡"& ëøÛX}MŽú—𴟺n”JsÃ-B8/ü¨B!~9Ëç¦ÐÇ!èSVóT ç5eð4ÏdEÂyŽ+Îë@§)°ú ¶ á<ãøÛXõIl¤Q, ‡+ašü­Ncn¸Q`׋ÊyIvØp|PŽêµÂ<…•yMÎ3YanžãFÅv !áø²¡ G+“­Šqüm¬F(ñuWa劄Cù‚¹q¨$ËkTŽ-…”×Ôp^Ó¼ +óTVæ5eð4ÏdEÂyŽ+Îë@§)l±ÄïÎÖd«¢«]ÓR Îä*ŽƒÌ’ÔR¹q¨Äù"«‚”<„nH8¾»©DuXažŠÂʼ¦ žæ™¬07Ïq£b;Ði +K%–hL¶*Æñ·±ÚC1ÚêÝ[Ž#ÍzªŸÃæÆñªÜ|5*â +KÂÁ½$§D „y* ++sš²xšc²&áÇ5 çt ×^²v=™ì¨hÇ_Çê+‘p¸ ³•*Ö?CÂA)Õ¬@x¹ájÃRa9¨„+ÅÏM¡{9:Ù5æ©(¬ÌkÊàiÝd¹+ÌÍsÜQÑè4…¯âGy4ÙªÇßÆêk’p¸13§ª>Š(®â +Í+«`O`nP9êÙU*&Êö¹)îcE×ö ÌSQX™×”ÁÓ<“ææ9nTlÚ¦(X ]ݣɎŠqüm¬Füžô¡bKÂñΤɵXanØߤr=ÁrØ']Q2H†„ƒi*Ø@˜§¢°²ÞTV_Nžæ™¬07Ïq£b;Ði +¿É¶üh²U1Ž¿ÕÚ’Uyý–%á DÏÏæÆ—|–˜_TÔ²gI8ì·ð%I* ÌSQX™×”ÁӺɧŠäææ9î¨ètšÂÑÍÚM¶*Æñ·±Z[²óŠ*øR$\åc’ÆW˜ö7TœUðë +Ÿ›ÎW¹ÙÔ$œ§¢°2§)‹§9&kÌÍqܪ˜ôšJÈ8M¶*Š„{«-~§XŸ^1¡kH8(¡‰Cç +sÃ&;†G•„"çç¦Æ¦T…óTVæ5eð4Ïd…¹yŽ;*º¦ð’‰}¢5ÙªÇßÆêŸEÂ9Ÿê, ׿.ê‹8ææ|£´*æ[§%áœo¦óTVæ4eñ4Çd¹9Ž{*ª½¦Ì7Sk²U1Ž¿UŸðmôfOï;§>šï„­VÅ„¿öô¾FëC잊: +ï5eŽÔ{&«£ùžãFx=ŸÞwÂhc²£b«1¤Š"Ï Wy-®lÔLïaÏ㉬‚ŠZ¥, ‡=ujù‰rT4ƒä4eY&ÇdÍD9Ž[Ó^SXVDPoM†Šˬãoc5"mó2[lÊ™ÌxÙY̪˜ÙÐbSάjU\°õ˜ùà6)NrÔy>zRp—V'=Ô)8ö É#ûðµ¥ Ò˜ƒÖ¥šÎu ¿Sp»p;0*Ä“‚+1cÇ•eÚÞ^§iï^[Dí¬+ç%£ðì¨÷igÆÚPq:“àâزôí9'mºØ­†ÉFà„ðFà„x"p\kKò0,,ÈM¦¾ø¡?I¡,2kèâìuˆ‚qØüß8Ý) ++¸Èuÿßð·‚K‰FN«I¿\ÕÊ<Ýé7 +xs¦ ôÛ.Ý0·’œ« âíY·[<é.qd˜Åq¡]‹ƒg^€I‡Fàp8ç<ñ7¯¶óëé Î:Y™‰¿Aë1)„Ž¿• ŽC’n$¤è0”° ÑO¹ ÷oPºWAº°ão%ã¾JÇÒ­C'ý†ÒEÞ8ÑÖ:œN < ƒ1Jâ¶HÝD/PyQý&d7ý¶‹ýVp!RÉ"É$Oíܺ…ÞÛæSÙé·]¸ÑoB|ÓoÞ@.úMÞô[Áí¨—†ßvé;mÒ…¾¡ã¯ç³Õq6ö{—üÐÛ¿âú9ñ1³L¼´sož¥Š{+…ZM‡Y8Spel‘ˆB)œgxfôeî­ŠÛbšDÇäÞ ®-N>cro¨áºÆܹ7n«D°]õ¼ß„Á”‚_šŽ¢tî=›Ù{Þà›Oð µæf^Û¾Á*hã˜àwBÂŽ½q”þ.ìMoìmOì{åT¼g)È"Y¢2: c¥n£yôfÙû V~Ž˜€ÝÄÞJ¥ˆâXÙœ&ö†*( šfçÞÐØàï%…ì:CR0†x`6]ê·P&Ô¹·]¸qoB<¹·½ÖŽÁ‚”âY÷ÇÎs¦AîØ[a<ûšïqÇÞ„ðÆÞvñÂÞ¸[šâ?½wfÃÞ8÷lŽ *Ø2¹ÒÜ6«êØ’¶ž)©´âÉ–,ž¦T¥»pÅE½í²zÛ¤)Vj\¹ç>‰ïUu«ÂÞÄ[Ý]:A¶oB|WÄ·aåÞ¤a`¶Â´­ +_¼!oB<‘7ó½;³²(:ÐÌyë^*Ëœ†:ó{‘»i.ȃyó‡=fhôD#‹ðÑšZ†A¼O@wÒdB¸ÅÊ›x1j{­÷ì±Y°Ï4 JV3M+¤T­›p·`/k·Zo¿œ>¸Ãõݼ›~Cæè3Uï’e ¾“(²½6ñÕöJïÁÞ Ø pÉÛ C7öZÔº ÷þÙÄÓØ­ÒÛ+§þ1üÛ VúK ÿv2aÜÚÿvŽ5ž* ©Hù7V¢5VŸ£ß00WEÂdnSJsM–p›ë¸Q±è4UMòc²U1Ž¿’H²b9 ÿFJ¸-¹=ðo'0hÚRŸ/**W“Á·NàåH.•v ÌU‘0™Û”†Ò\“%Üæ:nTlš¦N$½& În²§¢«1¤å[|­ÜUÔüÛÉð|(üÛ ô[åG$žü{F>ðo®Š„Éܦ4”晬à6Ïq«b;Ði +‡·ãÿæªÇßÆjÚ8ª×z¨“Ç’£ +çu&e‡ÛN`ö9·þíäKBΟ›=pÕS!Ú10WEÂdnSJsM–p›ë¸Vq:Ð6ð’ÉûîµÉ¬"oÀÖŽ¿ŽÕWäßNܦrÈÛî ÿFJøzu©Ey‡ÛNÜ”ÏU½6åÏMaæ½°q zÝÆÿïõhÉ¿¹Mi(Í5Yòo®ã’s;ÐiŠR]KþÍU1Ž¿Oò¸\««\ $ÿ†ß¬p4BwÞá¶Ê´S6稜×õÈ¿|ãO¿ô) ÌU‘0™Û”æß\“%Üæ:nTljþíäÛ…$œkM¶*Úñ×±CŠo:í‘ã S§P’p[UƒÎ‰©T’ÉÕ¤ñ-ö¬…ðÀ¿¹*&s›ÒPšk²äß\Ç%ÿæv ÓT5GuÉVÅ8þ6V+º®ñzäßzY¢à¶ª}jZ©\| +øsS#ºÆo jåŠYF×ZEÂdnSJsM–p›ë¸Q±hšBô˜ÃÑLöT”ãïcµ…bñƒ¼[óo=Ò¬éëñªÜZ\µ(2D|‹ÝKrJT˜«"a2¯)¥y&+þÍs\ño^zMágÑðÀ¿¹*Æñ·±ú:¡Ø‰+-[ijG.ù7VJULs +n;q_a©º£B‘Ïù¹)¼¡µ :yàß\ “¹Mi(m˜,×c ·¹Ž;*º¦è92·"(“­Šqüm¬¾"ÿvâ2̬ÒkþíäË1/ ^I¸UŽšÚ£JÓðÈ¿¸r³¥K)혫"a2·) ¥¹&K¸ÍuܨØtš¢™M’úÖd«b«ì]¸FñzäßúΤén;ÜÆû›Tö¨’ ”¦ñ-V¢©ýsU$L6šªPšk²„Û\ÇŠí@§© ©0’×¹*Æñ·±Z[²zÔGþ•Ž#©ås‡Ûx£Mz*jÙ3øï·R¾ø7WEÂdnSJ&Ë _Âm®ãŽŠî@§©È6M¶*Úñ×±Z[²óÊ*ø’üïLrˆüïoN YZ•„]Èñ¹©á¼º,Jño®Š„ɼ¦ ”晬à6Ïq«b:ÐkŠçЇäu®ŠäßþŽ±Úã÷€Wì‘c¥ã üorèM}àßØò+œçç¦Æ¦T… +sU$Læ6¥¡4×d ·¹Ž;*ºMSØr`ž0ÙSQŽ¿Õ?‰ó>Õþm|]¿ +*¸ÍûFiUÌ·NƒoyßLæªH˜ÌkÊ@ižÉ +nó·*¦½¦Ì7Sk²U1Ž¿ÕËLôfÎì{§:ï…­VÅ„¿æȹF«£ë®Š<ï6¥Ò»&Ëù®ãF;©ã‘óÂhk²U1Ž¿URÚíí JÛî“Åfö-aE¾¨¨UÊà[¼§NMrY’òTyä5e&ÏdEByŽ[Ó^S´¬”ãÙë*ò&ëøÛX­Í“z™ ,åÍ?f¼ì,fUÌlhXoV5ž9¯$Ü9Þö³³VHÊ]qô:³¡Ó”UõZëøÛXH7 ‹UÞRœ°IçÿÆakŠ¥º«$œ²zäßX)%jÏ‚¬G©˜!µM(Í3Ù ©uÜSQè5U‘2áóT¬ãocµöÃô4?¤g?Q^òZc³Pâ÷¾z=ðo¬’‚¾FÁ¬¥ÍäÈ6KŠUÑ “Ó”]à“õBé8nTl:ME@!ü›«¢«(ÿv†#3 ò«#iÂGŸ¸‹Iúòü ©ç¦°pRº`·ó -Ðu׃x3¦:ÂÜm“R°›bàX\ÚH7¸î…VØ]}6!ìôã%uqíÔ™NïC£R<8j¬Ñ¦ëLªM‡Ï3µ¢Û&×»ýl‚c׎Zp°«¦½‹‚àH †ì>A=t±e‚“ÂÁIñ€àz­­Ž-à#Þ›ð‡þ8Sºx9òì^&DÜœÓ1„;¶éæ$:‡?!/æ±háH†(×ä„ûL”„Ò›x#qú6÷ +|éö¸‚R+Žpž0¾)8ו»iRpìÁYAÁ±·áTIàÜ>¸» ?Ó#ÚÇâXOAÁ‘ÞÎÐÖ㲄 %î†N¢>ÈIjârùp Ž„øt;‡p0p,½ûr p\A:²@ආæat2 ÷‚Èœp¨ïLDyë2{8“ïà”²À ñàHˆ‡¸ªù®ÄCpÂþ[7‚LŠ€Â€“âÀ¹cø§5{ì… €£ªÂ·e¹88!]Ü.wû•³ÀݺßǹË~èLîôÖ¼1ÎIÀ¹¦þ Þè4 Œm Ù›Ö!"1´‘SúîNuˆiÆQy™ì¨hÇ_ÇŠ?QNVÉŽ,ýV…9ˆOÛm]ëQ_TŽRËç¦Øyübô)úóTJæ5e4Ïd…¶yŽÛNSX¿Š>E/M¶*Æñ·±êCzŒâì¿)ú 4w¢ÍÆ®¤Ð6Pà¡èD3JÙ¤Å×BK¿Á³ÄOóTJæ5e4Çd¶9Ž§mSøÑQÞ’fLvT´ã¯cuÙ±N›e1†~£ÚqSS“JmpŸËQU……õøÜÔè«&}”~‡À<…’yM$Í3Y¡mžãFÅv Ó^²¬ÒK“³äŒãocõ5é7\ªBQðCJ6<9%èÍ4Ú†Û€ò©sbH•Ü4Ñ`é7(áò@µ|áÿ÷z´Š¢ß¼¦ ’晬è7ÏqE¿yè4…‡4ëPc§ß<íøëXõI¾àô®Ì#gè7\Ó’¦ÖÚ†kepøèEå¼Ú#¼uâŽrLˆ˜§¢P2¯)C¿y&+´ÍsܨØ4ô[¿Ì'&[ãøÛXP,Ðâu(”\Ñodfya¶BÛ8T ò²QA¶Õüo±g-D½n¢P2¯)ƒ¤y&+úÍs\Ño^Ú¦j0¹5´ÉŽŠvüu¬Vt]cSЕ¢ß8È,:Ï‚@Û8T5ÙX•Ê‰#åsS#ºÆýPjåÚ!0OE¡d^SIóLVh›ç¸Q±è4…S¸§Ö¥ÉVÅ8þ6V{(vÐóÚÄiC¿q¤)óYi´ãU¹;µ*øUþtmè7¸—䔨!0OE¡dNSIsLÖô›ã¸¦ßœtšº‚NDhLvT´ã¯cõ•è7ÜhÙª¾ÐGÑoP¶-•Ú†[ É©‡q¬.ÍÐ)ú I–VT& 0OE¡d^SIc“°§Ð6ÏqGEw Ón¯NÇ£ÉVÅ8þ6V_“~Ãu˜êH©¥ßp?f».É ´ *G•ÉuJÖDƒ¥ßøºÓ|(%y* +%óš2Hšg²BÛ<ÇŠí@§)Äm:;±2ÙªÇßÆjÄïØ?È䨆~ãI+*Êhö7:Ó¦VÉ8{Qš[²ó«†À<…’õ¦šúr¢4Ïd…¶yŽÛNSôê«|¦ÚdGE;þ:VkK¦HK¿Aé8ô½BmÃþæ*¥½¨Ø,¾Š~Ã~+i¶‚À<…’yM$­›,ƒ|…¶yŽ;*º¦*N§ÄG“­Šqüm¬Ö–켪 +¾ý†IIÅymÃþæ,G{TÁ×ü¹©á|•›MM¿y* +%sš²Hšc²FÛÇ­Šé@§)ÚOÐ^¸>™ì¨(úí}¬¶øw²ªSɆ~ƒV‘]I¡mØäÐ2ÖUp„IÒˆ†~ëwëêä óTJæ5e4Ïd…¶yŽ;*º¦ +€—G“­Šqüm¬þYô›ó©ÎÒo¼”% (´ÍùFiTì·NK¿9ßL5æ©(”ÌiÊ"iŽÉms·*¦½¦Ì7Sc²£¢«>áÛèÍžØwN}ß [­Š í‰}'ŒÖ×=uüÝkÊ£÷LVÇñ=Ç +:ð´sÂhk²U1Ž¿ÕÒˆ;t¥è7ì÷h‹(”Å{ØS$QöTÔ*eé7ì©“I‡%ˆ!GEsGNS–_rLÖ”ã¸U1è5…ó§A.YM +(Ž¿ÕÚ<©—Ù¢RÎücÆËÎbvHÍlhQ)gVµžÙ‰WqGÞoûÙY+å­8zHÙÐiÊΪzH­Šqüu¬F¤{†Ÿ´iÇ$δh´ a+MöEÞU.L$ö!“›§ë[ÚþT3Èz”ŠRÛ”EÒ“ÍZÇ=ÕNSD¦"Íi“­Šqüu¬Ö~8å¤Ö@½–šÕ[x\(ñ{ßeò'k•®ò¹©±¦ŬzIqTôÂä4e8Çd½P:ŽÛNSHÕ\¯G“­Šqüm¬þ±ô[®í[|CïëÄß2²SÔuººƒ[¹–uwáožãxú¿ é͹eÚatÄ÷)ãŒýG:¢–R´”æžwÒoÖã˜ù³:ýÆNä y8Ü’Ì ƒýGÅ«d\;5rý,þmnÇF…xòoùŠ8¥ÑT7Ú._‡j÷Ž[w| “Íè¼ÃùÅ1—t¨ u§³ŠÔplÜF-Ý »Ö4óaà„ðà„xp\k»oÀ”ãþz6 „ÌÄRQºü~ÌþíÜÞg;Fp,s:çÁã ÀåÆ_e%G»ðoCk3ý\'à2ÈÙ–æFgp»tcÝ2ðó+æñöÌR¹xÒë ×Þ%,ƯLq~ë¼ ‡kžùî <ƒ˜Ù(8¯#¶sí¹5d?™óÕ€à ‹Éé  #ÕÖÆd[ah±å]ˆÙ¿„¥•®€è:WŽzØ7ñÖ¡“ƒCé(@ìÜÞØ:«^@‰¶ ’‘cufZª¹Ýøáá„ìávñá2_T»ÀÜ>Y ýƒ¦‚v6Ý iAùÉ Ü.Ü@8!¾A8g·—Ý\Á ÚK"gß)ñ ÂíâE¡㯼×η±ãÇ”& +#®(ÓÆuËr›„Qgás +GF³oc ߶&™…‚êZ¥…+Ÿ5ëD=& +qï†Â¡†ëZI7; +Çm•"°7¶«ž÷Ë0„B]d^¼¼$»f[}Â…â‰Â¡R\Š6„…ƒ´Í´o…ã>HqÒU…ãN(ýI\,œÞ,Ü.ž,wK<$ßVp*¤ç.=0“5nº¸c­ò<‡ÌÉ3=ÛÂA•_€meø®wL iN™…C[qÌ]ki]¸aF-Cû³éžÝš.ì,Ü.ÜX8!ž,Ü^ëbá`AJu¦ˆ›,<ÛrRu½@[I4vNon/Cᣜ漷f£á8An® 14R³–œfU†ã,¬IÂvHÑKÏw‘Õ“˜Zmk8uß—ùJtNonoY¡îŽã­ü$^k«¨Ã—ÞD\¡ˆÄY|qB|×ùcDBRB¼Œq[¾x#â„xqÎuñžL°FQ3¸¥T«ÖÅ¿µçìH§Î+OÜDâ¼GâOûÃèš&’‘ ˜ßP1âñÄ/ˆ×p&Xá;ïâ¯íµÞSÈfÁ>݇- +Ó-L¦&kÝ„»»xY»Õºùeû`E)C&è3y³%ÕR3g&d{ÿl≯í•Þƒ½°? ƒgúÝ¥›0õÌw­»pïŸM<Ý*ݼ²=ðÿÅá¬îIÜqÑØ”£}øÍÇhçh—XB[ÇЮ/FHT¤ƒ‡F6Ó–¬o€ùû__þþýͯµŽ€‹ixDë"æœtö™›‹à‘shh]´}]õ­c¥3G}R}#Ì\É©¹MiÞÍ5Yrs®ãFÅv ÓTÖ÷ƒZ“­Šqüm¬ð¨av¦§ª“@­c¥#ç´.µnACMFw…|nŠ/Ø1+{vÂÌU‘œšÛ”æÝ\“%7ç:î¨è´Má·’"ò‡“ãøÛXõ!Í8Ÿ|©“@­‹Åi›ý5.‚/§Åð­C"<„®ñsSÃùZäýÃ’0sU$§æ6¥y7ÏdÅÍyŽ[ÛNS‰mÐ:WÅ8þ6Vëeó¾Qz*ê[§!üo¦Š0sU$§æ5ex7ÏdÅÍyŽ[Ó^S–¾4&[ãøÛX°ÌDoðNuÖß [Š Íiv/ŒV§â]y¶ÞmJŸÑwM–gý]Ç•Òdê"§)F“íøëXõ!¥™¶žh]ß}ÊëjdÅ{XÚŽ> u¬¢V)C†ñž(¤ÒŽ#y* +jòš2p”g²‚¬<Ç­Šé@¯),+õ­*6WבžšrÐ:ïe6–7ÿ˜ñ²³˜§¢fCƒy³ªñÌ™x%ÔäÎñ¶ŸµBBV‡Ô™ ¦ì¬ª‡ÔªXÇßÆjEÚ´yDë8àć;3^ûW\êçZÐ:V9h[ó<¤ÉdÚR„™§b†Ô6ex7Ïd3¤Öq«b:Ðk +—ÂÆǧЪXÇßÆjí‡3-yÏk)~©‹O :o.ñ+Á£ +êû+ôZJ›ÝÖÄ.»¤X³0Ù¦œΚ¬JÇqGEw m + VŠÎ>'MvT´ã¯cõÿÖ}…ó‰²_„ª=Ó‹zŸÈf1 miêdñÐ#!=ê( aô¤t‘xñÀù±qhýƒxz&)N¢ç¤¤Ts¸Tz:–Ò\,=v"¶"Žø’0}{\MñÅ“49È_Y‚—m$@gK;¾B .Ö¶”°p\´_¤¹¢=ã43cG ð„6Ú=»$Å·áa×ú¥‚}5Æ„‹¾ÊHI‹)íb³pˆ€,Q®|÷Í¿õßÉpoVZ<øâþŠ}>²%ePR‰‰xZR.9Ѭ[i:ÁºÈvÚ7U +Îè?ðÁ‹V|R øFðåàw–0ÜD|µÀ_ +.9i {hOÿËßÿšâ‘ÿ5Í"_~óëPÚˆ£¶£­œÃzýE´Ûÿ 3½}  +?hŒ|òôž•œœžþsÌòÚóÑ9Â4Š’~û.¡‚7ÙßÙš}ÏÒ¾hæ(hë§ü·ÁïLÉÃ#ø÷<œª²O§AZpJ2ÖV{Rð\k¹aÚ‹µž_¡‰’Ï<Ã5Ÿæ/ð ?ƒ/ù‰[5>½$¿5¯__WÇ‹pþõÚKÚÈï0ç©JôSh¿pòX)yFÝ­˒­Ç§Sðw¾¯Ï›¬ßQrͲÏ]æý4Ï©’'û·ßè’ù6|gKÖû#Û¶ïÝó»ýÛOçŽ/ÿéwþÛ—_þËïÂñ凟úŸÿçŸÿðå¯8ð姟ÿàŸ ++ÀÓbd~¼¹>ØÃé•)²<>ZüÝH‰KÃúÙç‚+%ÊÖWnlÒòÆñ×ÖZ£áyOyæ0ÞKê·X)¾h[úس\x1å¿5=0K¾CÉ…sÄY×uR PÚ‘tÛZ>¬å6D‰öô;«1ûHÙ`úöeüð<Íïý­›Öã…°þfÖ‡YôÂüˆe•ËiÒ¦mþÕVB{_êœìÔ‡O¬À{} +|êˆøy⋶\”U}dÍj­%ÂòýÇ×B†Æ .~–«cq¢Ð·4üÃŒm|AŠ©¥¸žê›o"n2KX—>)Ðæºâ|Sðž5üv3Up—Ꜿ‰²¹tØ{KUø†Ÿqã ±Œ)œ-ûbÝeªWT­×äÙ£Ëv?^úz HÅK5Z#¥Æù¤[Þqñ-òÇ(¤Ub°ÒÒ¼[;‚S‚õñÄQ=Uáp_.Ël óÂE{‡5Ëj<¶™ÓÖëz3n£ǨWšC…EȺ­\ØK”ç²Â‹VìœÇUb]øÔ£w +‚óà« i#ÑZÂÇ[zç¾íç÷úLC[pšBJFä>2Îi'ŠÚóœ4ñ#ÄAqÔG•ÐøÚÄ{Í 7O*:nÐv'ž’üÅ+ôø—’F±©3ô•fõÖDêŒJááñ÷ÖIUl:IWïQôF5×¼^L@s{ƒ¯™¡5Tš2i6Íe®¢ñgˆð™/".ß¼wÍU¤Ð3ŒG5¨<ãÃY+å'>ù-àI¨‰çš-àp:=µ[ ²ç‰hOü±Ü—Bî-”Þ‚´1ó×õv ã#Êië¼µ {ÉkÛ2a³±Ä½d¹õñqú|y!4¦Í✆(‚>OÞRѬ[{”ø‡»|«Ô\Ò9UHžãZbyR’{|h„}ŧõÚâœe"\ Ç]‡V U£Ñ~¾+\N#à­Sly(ð*© ¥à +ßCò]‡rÕ*¨îB#9%Ñ-a1‡?ªŒ«/c2Ž¦eškꘓüäŽþùh3¶k´Ñ?Fy£”öm(¿F|wÖRý„Ô+^ÅÃ?šbhíàÅ‚^òêþÓ­§¯irº¼z  ZÜ~µÕ»™„-í#6“vo2ç@Ì[éÞ^Õ_1Ô²oÖ^j=zèçÒäŒ=,¡‰…ÞâŒÕvd¯Ö€3ã¥o¼f̘é!‚’:m~˜²­L­œáé:ªc,“~|îו#Êü*·ë1??]œš6(«°Q<¿}j¡˜Ú9Rù¨p{çUƒ®~øTù±QÈ×BâWÛõ×Jç’Òõò5óÛ‘¶ +ðÊ5bcá(ý +Ä9.p¢ÖUhܲSãæú"ÑõX´(ˆÃ½~%ŒŸË·¶ÆùaQ†+3Nüõh7€Z5%îš©ó+Ü]ßp‚BŠzNvÕkø°ðÍù%õ”ÍVŠõ1†wß– 낪íSœðÁ)—þ‹ªã8ìÿÁ*U~»ó©Oÿ‰_”p–6å™»p}NJg^ý‚ÛòŠäw[¸}æÛ¯HdE«|Áî9CYÎÑø¢[7."Ïüáv…Ê45gÿø0ª—~ÙrÙ-^ â‘6PZï>÷úXó{T@Sàƒ¡xn°‰`R[DcqiÐÔ®Âs&N‰Ý!Ï“Jš>´³Åd›MÔôðC#õ'ܪðçÚ0Ú n;á[š5m3Æ8Îps…4þšò+és #‹fãìóÈôÑ+Lø'< ø •thæ*¿H¤¶í -°åVÂel…ɦ¾‡:®vÔOŸk¡4Lv~„;·ÿ¶˜Ö‹œÂÇ&hOtà:¶½ c&IàJä¹½QNêbÕQ^˜ö#ÿÒ㚉â3ofJ'ßÆbÙÛÉ:)!g!­º—3ŽÑàO±/ÌrÊqË·IK·`çÄáFé543'r/d>ï3Ëe ½+ƒhAÚˆ±heM¥}¬v d/y-ˆ™oŒÔ¹Õ Ë­ã°BAz¦pƒô­rÞ¿»Ó“Pï1¨%Š0¯®‹R5VYñøž¶ý%Ôevä_«fè!ŠûÈDS#úëlu›-<µl5 +Ol±èY1Åv´Á¼Nm‹[ü^]»š¥zk>îS¦ê/2ÎÇñC¿[LñWmÕ](¬È‘VÏúa:ÅfB—uëA&×é“*½ajÅÕT|ß¡o’(6}îé€ÎÓNßýæ›_üËïòû×?ÿá»ïÿÏùÕ¯¾ùÅ/þË÷üñ¿þåûÿñ§ÿòÍÿúýÿúñË÷þóÏûþo?þ•|ùã_~üëß~þË_þúßþßПLõ_üâ_ÿó¿}ó܇“É endstream endobj 351 0 obj <>stream +H‰œWK®ä6Ü×)tÇæ7InÝ6¼j ³˜<=‹¶{îLD&IIõTõº½Q1TÉT2”>ýãóöéËg¿ýôóçíæ]kq;^ÿúzûôë¿üöõ7¿ù-µì¼[è¿aûë·Ûn¿|ÁÔO–(X¢?˜ÿF€àohÏÄnA‡a R\©©m58ß%n÷ß5 WÜ—íÍ»,s¹7‚Öû]Yãû-»Ê„Å%–_¢Ž’+± „cqÜÊVqE–º ï·àŠˆï$3$ÅŠ:ÄÕðß[DÚº½EÔSP^pRú„¨µ$,Õu¶ƒ9 CqI¶o7þ‡Áì.†Jè]DõÐ’æ¼d@ïzØ\cû輜+VÐûßošÙ@u1–û£>!Èò{dçÓZÑiywìí?»Ô°;Ác—š“Þë¾KXÂúÙ€èÄg"éa{ÃCk`3Øùûm⎠Ó`ç}}‡±Â“ëá1Ï©ÕÜ«.O¨e¶f”‚[5ë¾=®´,˜Z‹‹‚Œk%Åá?Œ¾Í‘ËàÕDà\Ê OA©ÉµÈ;ç;írK¡»1áÿØì©Ê£iHW,Ž5 ]Ew+ÊaÒ:JšÁ8»ÚIäì ÁÜöJX\Q«­Æ²Àj5…h­aܹoÍ7<Ù™”ca-’Ëí&3”‚ÞU½UÆðn6PÙa>°Õ1ƒsQú^|{¿9] íê‚f +8Ò,•Û^·Á·„êÉ­(æ>úŠD—*‰Ð l]®l”yô7@íçØŠªê8Œ1’Vî‡!t½3'·^¿ZÅÊÿ6šÒ´ºÒU†^&Àº$-´Ó›êàEµö±4³¸Tæ?ä`áXé’@€¤ûÉí¥¨:°rÈɽ`®Ê EzcšÃûlêÀTVÕ±Ûq‚†|Ý" +vÇipHÝÙ u¤š´IôTÑš² И c³qÂ*•Ù4Š(Õ¼ù!“3¢o®ƒdC§Õ/2ØCÙyþ˜C¼Ǹë¤Ødt’|_ÿB̉-æ‰3º¸Á¢…¹‘>e†G¨ýÎfVŒ~‡±8Í "8ÕvÀ3{“å0¶ ûÓ,úš»î¹\É™ &‘\Œºk­±nZŽ¾Q$M(€\â…GÆ€S¢º}¯PÏÞytGŽÊ°©ñv; l©*¦NS»R›3ÞÀ¤¢m@ªîgœ“Ÿ3¾{ª1÷wv$ÑÝ&•Ç;Îs˜öD«¾õÆ@ËSMþ~™ÂñnÖjÄ0“ hO:W±øî8휥D#=½µ·%)›õg¼e êQÏ]|ï÷ Ûf¯ÁH ÚºóÌcÀ¯»kÓm<®—¢¤êÀªŸ +§70ÌѼVÞòŽ 2ÈÏO—VÏöj‘Œ]AvÕÊ™Á"Fò1wÅðnÓmly¯+G¯¥Stè~ör.½*#jÕ:|¬Ôfᥲåz×#ÅB`}ú::fóÿ´ÄüŠáÝ8&Ûز^—Ýa]0ü³ð>–ݲÛÂB8Êêxëì¸`ì +²«–Î 1’¹+ÆîÚt?úÎ;W¥‡†ã¼‚b¤yç©v,½ˆö§ˆž(Ùú¯(ê{é 9;TD}Žç·ÆY¯møœî…ø°ÄüŠ±» ´|id½,»ó€Ã» 9ž²ÔkuªÝŠ,@5¥—âì3è(Nù^möµI[©úÎø6Vå¥8Ó3qÖ£8ë q¦Å™âL/ʼnVlY ‘9jSˆ‚ZêLGu&X›AÔèP§¨ GÎ8€»6ÛwhöOGñxöx¹”f:I3™êÒ.ͪ*Û#Ö‚xÍ/¤Ù‡4óIšaŠRó^R¥â].Wž@øÂ(ñÌñܵ9ú3¿ï¨mqèß‹²ý°(‡ Õ"O\$ž\$^œž\.RzßK&ÀçŸ+îºð'½†sãhÇÇaÃÖ€,çÊó©Õrju[•óÝhÕîÃñ½ªÛ-{»óE»ã˜jon/ªž ±ªóË~—¿ÓïANý¾¦Hù¸ÓpÜÐáëš_uºDê~>iäXó$Ìù´ ~„WĆeûäÛf§Ì–:-/˜NU«ùòyÃÏÿw‰œ4 endstream endobj 350 0 obj <>stream +H‰¬WËŽ%5 Ý×Wäº&vÞ[Äj„ >à +†EÒ ÿ/qüHuêvݦ¨¥º9ÕNâ8>Ç®?} >}Œá»ï?†-î½sXŸß>o~ü%†Ïm1ÄzÞcä@ƒñKáÛoÛïÛŸ0õƒ-D¶ÐWXO›9çi™ôkøS׋²Á×@:¤ÀTöÔ9J}oTz¸}Q»/ÛS®{«ãgŒûžY&=å±·Ž¥÷ã‰Ò S·‰õ‡t¢½(´ç4Â\ç©ðÞ†Úß°÷?o´×z@Ûb.äÈ÷¹mŽíçyBsR—±³ß¼Ÿ7 +òwŠº'%—½æ´Æ!î-ÁEÿ¡}p9ï;ÈÂØ;ü½±=ŒäÙÄÐV šëˆ‡½µé6öçq†å +#Ž92ÎÀq/ŽÅõNêz§"~Pj …§ë­¶àï ¸­Ù†öÖ,|qŸ{ØÈÛêÓmlë^»^÷\c „4,”ÊÙuIùQ?R¡¥s3×óNY·H°Ícɳšë²‚XX@°¸xØÈÛâÓmlë^ºN´.¸cŠ{Îõœ0£©ë£©ë¥–¥½ä4]—ø›­ŽÝÒLì©ŽË[1 àKˆ‡¼>Ýö1»+Ç©#ÓsšémÄ|r¼âDúC’éè@KM3]¢^k-98ÛicOMtYÀ |m–-fcoÍÐƶì¥çÜG¦h¢7¢¼›£ã=Á˜úÿsTä%·JšèµB›ßâhÄѺrÔHÖ/9Ú_s´ßq´øtgçE¢gjšèeQN­'Žž(Z_(jTjÁ˜ò42¢:EW†ÖC›3´úlç¦.ûH\ -¸áqá>®ÚO íF?¾cèXš§É?1”/š}A^%KC+¦ CÇ])*z©ò,øEc«×!ä§^k1F +ÓÃÈžšãyhYʾŠãa ïØçÚؽVØqÊÍÞÊe»¸€ëqèRUøŸT%ŽUUØÁ•ª,¢Âå0ø+Ììñ.KQ)&*é@w¢BJþ¸­IÅ©ð'Õ‘US.%%-’’ªa)²xÙ2ßKJ}£ìWYx‘”¾JJ +ö=?‡Ýó¥\åË¿ 9¦ÎÝ;äçDäU›¡>Ç·-[2XvªÅ8¤“|lÁÌ.Y?ßz/¡á‰µuÞ6ÔE4Ç +àz“ÄR7êÞŠÜËcé*±É°ü‚RÌÂcËÑ%¶O¤}➤/ ÏZÈ1¸ƒcgR÷´B‰ïh©2`Ôî £Ñ*V•³Ÿ‚cãÛ†u…{ŠÚÎà<çîc„" +¡ aƒ81µy‚Óé®$•b¢PáF••ŽLÃÝÙç)j4 ¥‚ö“¤‘×|S<´À9ÑbÇö +㌔¦}Ð jvq«†"iù’ö‚ô’,8êŠ4D­e½·û£h)ÇQŠT‰¥¨€äôóáñ™§Eû˜Îò¦ró±0 ÷äáe‘+GJHr“µNC;5ùŒ+z8Ür ÉÍRâ "2IJCÞ3óâ›@týhçÄÛfY†…}‰ª´€9PR“ßðÌΤY6®Ñ[Ð$ R½É‚+ T³&Õ‡7ÛØ@Û£IrŸã(¥AóOÇâùõåH“×ðùUgÂ5ÏHuPRŒ…pl¢UµØµjùÅyhÜ%¢bò%‡Œ­š„Qä­Ø®Õ€p?ópÄJá1ù‹J–l†²4þ¢IQzÂ^µKÿŠ“'òO©,jÔ­?Ã-Ë Õ :4/;ч÷h$DÑ'8;‚'°ŽÅ;߀å I7)é’²4òzÓ·M®Wøð?ÃׄœÀÚ±ÊÁhHA]Nsx›Au¬ÝŒÊHÖ[³ÌÖ)Kl;Ù¬ +rá d|Öæ‚ V> GŽÀ#¸£ » +ï°"ØõË$©8DÚûç˜è2©èDHª,ï×çÅÖ}ææ/šj™ûE’DÝf*{g€zžòÿŽ¾A$Ó§ýã! +ëÛ¬ÊíÌÅÚNg/^´ÌþáÓÇ€Ÿ¿ÖÛ endstream endobj 349 0 obj <>stream +H‰´WKŽ7 Ý×)tÖˆÔ›‰‘Õ ²È‰7mžÜ)Rj©»ª<³ˆ H"‡¢^±øžª_~5/o¯Îüòë«Ùœ-Í<¾Ý^~ûÓ™¯ÿnÎ8ãK°Î¡Š4ƒyÿ{ûgûòF[_$H¢ÝcúžË´é/ó½ås|Àm ƒ·Hÿ Z_k0×o-îÛv Éæh}£u±yÓ%T› ¥¶>–ayë ˜ëÖí6AÛ(Ž6øjzžKD›+ÊݾÒÙ}}ÛÀ¦4L9¢'RKϹnjËt릀liäÙ¯T¼?60ü­ ©z®ZÙOup6{‚¨ØŠqXhÀ‰élOûÕ/ÇŽ 3JŽ€žG 7bÄ+Ûe­ãx†zµè©&à‚Åy^ A/øÔ½²–¤û ©È)çÆÌÓ£¦`§?K‚«ÃbýÝ5Ÿ5(‹hŠ«³¦ nÄÜ5e’<CÄËPºd(JEñÃzPhÌ÷F Xî|ßDd”]=ñ“žøC)¤o5&e@V¬ENÒÉ}?$¼IÂ]NÊ,'ÞȸȉÊRZäÄ«œ„IN©œâäÓÒ'8—“Ãû>Ì÷}øè}Ÿvïû<Ý÷¹ßúB±¢¡Ok¬1>J5}š¥î Ê|ßûAéIdã"'E7ë=ÿóÛžê]œà%̼Xxùt×ïðò쮇…—ᓼì‚BÅ.%¾žà¢'x€Ûá~Г8ë 5ÿ€:Õ”n’Rçe“«­‰~Ï­ØÃRs\jîØó\ólô9%h8ƒüu»¼–s5lÍÒçǪÃ\u€W½#ßm—ø©v9(9©ŠCL +ü¼äpTò¤z.%‡]àkÉAOïe?qp¬†»mî—‚/õ†t9¬2ƒö»}rZm×Öï_'øP2}3ðÃ"Ò¹›ï¡lm_ 7Z ¯/T¨$~ïâݤ›„À°Dª£Í¹íUOÔÍu‰HzF³žÓmÖO°©øá¹Ý=ž¯„Ð<”…?|ž=ü¡^â¼ëÙK;{x¦fl#¼g›;ðG„˜„­4U׿&ËB?|É-–f!%¢Pµ!Ú­týpÍš£(?Ôso÷:PIøgË°IK«|ó©çþHññ©“¼÷^˜¾çÙ3 +ì¦5‚Y£¶¼ØþòÔÒ—{ÝÔ–éÖMé Éã¦üòöjhúO€÷7Ÿó endstream endobj 348 0 obj <>stream +H‰lVIŽ$7 ¼ç+ò­µRW· ŸÆÀ? `ÕÌøÿ€#H)3««/U +-”DF„òõ·ýõË[ÜùõmßbPMûõ÷ç·íõ÷?ãþí¿-îqÏZBŒi—‘ð/ûÏ¿·¶ß¾`é«ô³1ç˜ÿB€Éíß-Ndà»XSvi#4©º‹ô nï6ï}{É=ÔÞѾ£­A[Eû%0â@ÜK9a +9cñvtÌÿûÙS$ÈhûŠõRRHEöµÏˆ³äröÜÏžT‘œÌž”‚ÖôYŒP£^W}ÒS‚Æ~í!W±(8\ YÙ)3€sµ ÒÖ"H"¨µ¥4ˆZ¶+L,Ì‹®‡Ë쨶q ÉvôâÞ8#‡:ò×P|û£ç¸N ÅB5¤ž¯IYkž{ŽäÆK[BR=0O&Gá&šå¼m{åï :/<Žî¦~½rN[ˆém8ÿA9œ  CC%ÀÖ)ïÙl Ü6¬?†ñÄýbk³¤ ´œEÔCG [X÷‚ VêÍî;êÑ#U\‘t^òß åè¼vˆPå;®™a‡1´a)(&‰kÐèa+†dÊîZö6‚ª;`+{¥ŸDxÔÔ+.ž»½}–äBí|ePˆþV° ]kZr †ð¢«FÆ'©ÍËy›Çk(º#²°³Ý¾‚+Lc€´7#†*LÍ0"ÝÊÌÆÁm¦uBÊö-¬÷‰ÝžœÄ¹©»¯ƒ’”Š$õá ~ :ë" T£îÐÙÆ‹Ø’q›ÚmL3Ëôù3‘ +ÈŽ»‚µ@z'|Wü%¾uðˆN@–±ðYºñAÌÚŽQè9‘@ƒÆái5£ŸPh'æÅ +sÿ3åâ†U¼jï|ªnY1].x͇QÔ~YNo¨—íæôØ]öÃÞ¢†Rð iöC¬ÆNp£X8ÖŠ=³üÜãazRc=thxàÝàGЙ}¤xø§`°ý +^6oãKÅéboа œÎÏŽá*ž-Êê:rYóì²ËÚù4V'ï;óa_j‹Íþ…0 Û‡;¾èw¾Î ¶píÅtkþ+Ö„¾ÏãÎÓ9±í[ýëö¿à0D[ endstream endobj 347 0 obj <>stream +H‰¬WËŽä6 ¼û+|0ZQOêšMÓ"rÈ4ò8ÌØäÿIÉ–ÝîžÙ$h [¥¦)Šb•è?~\?|úè×o¿û¸.Þ1‡uþþë÷åÃ?ûõ÷¿¿ú5rrÞ‡•ZÀ/­ýºü¶|ÿ ~0GdŽh•þ‚y°FÇ!òÊäŠ÷e½}V—Ÿ——€¥Öäx}]¦áËoß,?-_`5–Ë¿Lëÿ²þ©~üi¹T\Ž!®DÕQ8¬«ËµbüŠ1;.ã—Ø\ó ¾Oi‡ÁÅÖÛ²Môß×}&‘£V×áë%­cá±Ä´Ï¼î3!cçQfrÃÕ 5—=ÏO]Ì y¾Î3ÍÅLú‡'WΓ”¸ÄUSÿÁE9'A!4A%ê:&É‹FWqœ¹O$]Ø» +ÚŒàZä7ºÜℳK¶ü6³m'¹¤.¶‰ìBsRÆ3÷3[rý4&˜7,‘Ñvpõã¼-ÛÉ¿hua~¬àn(þŸ¦š i °eü–<•"Ãùy”C@bºFd³ p[ +Ψ ˆ”YУ¦lŒ¬H!/†ª«8ßâs g±± oØpËÍ€×’@LY"çžõ?œG•};¦Æ>}MzWšæ )SRórþV×’Ö?þ”hO˜ÈeI¶áo‚€‡ä«X{—ÔVhŒ•SÀ8ìˆm¥n‹yï°Á0ʨ֨°2$k ÆA;¸ë9lòîÄkÌ])m’h©¿HEBäQëË%-Vl # £´ŠÖjÑŠ¼›À.áL‹›[™a‰ I!ö/yrAÓOÜzz4)Ö@v|§­Düäœ%'¼ï¥¸Äªu6p™x€ˆ2å5»÷ÉU&©GŽ}ŒÃVj¹ …3õâ·ªÐꦤyi¨J$©©zÄ"É8ÞnKv1ð€Å5²k '*7Æ9ËÞXu¨¢Ú…ºÕŒzì‰çUˆd@TOmx'‚Ï´!ð0 Ã;aª[ÃÉM)„ l|Ó•m̪‰ØNn}ŒúÕ{ÁfâõþPâfT(ƒ¦½œ+ 9óZ[,޴и"©ÖÕ|¡žr¥W+e5Ýí›’ó({ÂüÊ¥£àjK«°¸ô1œ²‰œ œ½>Y¡VXužÊí©ÕmA&XãKMøgׄŒÁgÜi |óJ/â…R×ÒFáÚXÂ+8qCV€œtÂÂE%˜uH¹Óh €2K¸©)h^„*Ö<À­çµCáä˜ä´w„ôÖ`E ¸!9yµç ‘\W+{HBlˆ¨ê¨?ÊZ¹û×a ZÚ‚ÀÚ"•ƒzpGøËrG Ã)Íš#˪ʹèT^¬Êäè#t“Û0rÞþ—ƒ$¢‰Õà;$Ua”«dÿŒ5鯛ù âŠÙ¤Êû(¸Õ vk(DæýYS…}¥n|Â&­ûßNÄ#º¢œånDÁƒô|ëþŸ2•ôv ^ìXË玉ŒÉ¨øÖ¤ +‡ö³5ë.—¤3Ò1«E¸*šˆ¹tÍÜGB©ùŸé™ƒ³i•±ò®§V·Ÿ%¤í\/äÞ4vG[x{Û m¢=jü÷›*̳û“³ÃJÇ(öÏU-ýxBÊËn;‰^ön»¢.­Ûfë|<× +Ssï¶uBkï¶uFš6¹»/kéÖ±L‡·­ùÓÞOo ƒ²BÚ¼tÔW‘#Tla¼hAš›™Ö—¯9*•¨J.­„x x–½Ê‹Së)€¡ª_ +É5ª¶`Û0’ïÎÝ@µFEÌ øÍÄfíi÷ï a"¼—EQòBÔø¨LYV²^=ÒPu ì 8$_W[0íïP0²o£°tbÐ]÷G7snOÛØ쮇6š¸œÑqÎqWRÙÔHÓ†P°GàrùʼT¢±6ò]-nÖ)Rn øÍÆœèÓ6ìß—qãæ‘;—¾4¯‡°YÏ´²j¿·äwÄ1°‹…ÍšobKÃH¾“U”תkwnÀo66kûÞï‰ßËÀKs¡AJ¥Äk•}Ï%¬ƒ¾2ÅÒ&Äm”¸*‚ÙSî6¤w­ut¥­fÐ]÷G7™ «ÚØì.¯r÷Ê+ +J¼†z +œyðhâfý¸Éÿ›Uzáhï)òÒwIÍ0S3< fœ¨I33ã™yffîÌŒ™Áî”|BLôTRˆ +/ᤄµ×â51뙘<3 #ãç3ë™|`f˜¨É%%{ip&åTàϹ™vnê +l팾¸ù¼½ÅM¾äfèû÷%7AÒÇKg“·¸)tœ†6YKœZPÉiÔ8‘®Îè¶fdßštRu$ +kwnÀo62Ëýq›ßëСãEZF°“S¡“¬Ð•…:¡YVŒÿý/ÜÉÊP•ºöw³îO“]Txz6d\Þ˜ÀÍÚâYS÷}l.ü8‹JTYèFå *±½÷Æçw¨JtIÞÅÀÏšüYUâ|áûv¸ñã.+y¾ñó$+ñ±¬Äÿ$+C 9 Ÿ*|“•z•zwßçYVòÝ}R•2«J9© +ï÷ývë?C´'IÞüJÌåZTâAT¢éÅ–îqáÇ‹ ?^‰J™E¥<¹ðŸ‹J¿ðáÒSb¶ÄŒ31ß¼ï/™I_MÍ®(Œ—_ÛuÜá (áZP¿¼ÓÒsQyÔbáêŒHGšòu‡u™qzOÊéû!åñ-1¤7ÄP+Eãç¸o2çŒ]¥¼Ý>ÖÄÄŸÄÞ“ïð•ùžÒý,Ûð•÷³ ßìW"÷©þþÓÇ?ÿ0ÎÂY« endstream endobj 346 0 obj <>stream +H‰ìWIŽ$7 ¼×+ò•-JÔvu{àÓÀ0|ð^Óf ðïARYYUÌÝð¥›Q)Q\B$õòãëöòñ5mß}ÿº]Ò>FÞοü~yùáç´ýþ×%mi+C÷”ò&3ã¿l_~½üvùð[_\‘¸¢ÏX5Çú+ÿ²ýizÞÄDÙr{juƒÖš«noï¶ +w-m»¦½èÜd—¢mÀ¬x»”½i[gJMkÈyomb™#ÝÛ,¶Ì±Õ}¶Žï©Îß.8h´@°`pUÕ‹ê.’±âKÞ¥øµ,zÇ–\Û‚Ø#ƒ &zA*„¨ûº}ºà7mí ·}öI˜öœûJª€’ >N®m.cë8^æ¶dúJÕŽê®^¤©!#"•ž8©#.µËrâÎÁ7¤ù§sÂdî½dlìyÀÒ#cÐ4{§B¦Û'U&äÊòÞ“%m&ê\¸#Ë™n*ÎœO.fƒ©g8ÒD°³8Ê£õbÇ–Ü,6ÓM!aŠK݃+™Éoy›äÛWÊ>¡$AyH{=<“ iF¨Æ¤]sÌ‘ë½X‰\ðAöŽ@Qµ1¤ÈFñ4:C3‡beU„«ìlÊ{IeR<ç± "Ã]àpž£Ú®‚ô3¥‰P0—æ*~΄ÈsèÀŠDã2hà™¥/l„¬´k€‚ Ù¼ž—ÀˆœßA\4DeÚ/Øìò[í¨î¥0<g¸ŒÌ51{‰" ŸžTp‘<˜8¨ƒ“g®^ü†1öžh²Ê´×ÞœdÚ"øvÏú쀊:‹ÇÒ®€Ë, ÕÏ‘ PPæmv*»¯"XQÁ”Eeßy_áxXSfgÂ:Ð7rä í5Ö j`ò´òé—Ée³ Iwä,ûPÆ7}@•yrû^&݆×ÊZÐ29ÆÛ×QHi–—ßV<€Œhö½Ê\%G»§Ÿ°Bë›P!™^+g.Ål`U âIfáå…tÖŽl´&½mŒ4““â’<Ð@a¸ P Á,½ÞÑ€§2èj–fËÉÅt`7øßÊñW ^€(°2âi1ˆø ²}BŒ´QÊ—>@!Y‡&Í•xŒ~Ãk9XWnèêÕ`äKï+j>C+™ÙBŽÈà'^Î",YÉú¹}ók‘ìJ2bÙXþµº¨H/{J¼N,»ÅÔD¤O•aAÖQ„2$Õél=S­žpÓµYÅJK ›O¿ßmyÐwwØ2á(§NÚwžZÑ¢_c.@/g[tXx›0 ÄN¿óé¨ç_ïÖ?(»;éΈúGJK™(}É(=Ê©°%Þ´ÉV¼b𸡊’ŽM‰3C:I’¬Ñš†}Q¸ŸÖg <èRÃý øiA5fræl‰M«‰ƒÎ’ÚPα +xák삨†­ˆ-ع@¸ìe3Ò´|Œ¥0}¥1H¨{hê“4×süR oŽÄ.8-©!3­Ë®\Û%/Ùî|/Ö›ˆ5êÒhäŸ3zOªÖƲô?±«‰q˜s›eÁ°#a†ËoÞŽZÀh°%®)fÒ…Ñø»]ñªÖ«G¡kÚeú6t,HÕ“  2U7«_Ýڳͩ(ŠUl®‘i° +‡–oï"ñÄ⚎JQ™Ïi îÎ8'ñ¤Ûtžç4Â&6‡Õ.ÓUƒ•íV&ÎÍ,«Ê‹oVë ¥Rç(3ÍRú¼˜¥É‚d“}_Úôd»„~5±b/ÆÆU¹lu8´PCQŽébªsØI³iµ ª½8òæ®ÒÓ‚‰ÖÐ%~OÑl¦y ®ÀΉLijwP%&Ýêc5æ2¼¤¶Qnª_á?"‚¶5Ÿ;œÅy­F_¥„lcƒ¿È¬1Ù(+ÿà]½Íà r”æÆæ&άÒ9QLŒ$[)nŒ7¹ç³½ñµkói~X|¨—m>J $çf­vk¸—Z½—ÖÂgž± £°ãº®O¸9ÞN.sž°gˆE åÝ9–œ†u.dQ­EŽ¯(&|õ²Ê SgÏÁnQyÿYõå~áM¹!ô”J®aŽ`ÞEö‰' u‘s>x’ä®s3é¬Y°Áß%É’Ég*¸){çˆqI_’„i‰œ(xJºL—ºjEfæXS\†>+[Wj‰Ì)B‚å8q’ÜaeÓŠ#òᄛňœt’1©…¡€èbb]ŒùaÇãħ7l¶ÆqZNR±1QóDÿû„úðñuÿ‚æLK endstream endobj 343 0 obj <>stream +H‰ÜWËŽd9ÝçWÜ(·Ÿáð–ž«B,ø€ ‹ª‘fFâï9'"|ófe/lª|òÚáxœxø˾_¾}ÍÇï~øzÜrR­Çõï¯?ݾüþOùøéï·|ä£iO9×£¬Šÿåøõ/·¿Þ~ü†£_\PqA¿`÷޳ϼ]ýùøÙäe^ðËQlYŽ2ZÊ3·c¶4Ö’ãþaÛ>¨™ôã-'Y娩ö y©u=ZÒY6¸ßfªµm8ÓGIu._ŽTuÙ&€•êâ–VçQJÒZ(:uƒû­¦66Êi öáߪÔ5§‚s÷ÛßnBåxkPM¡iMw„Ú ¦5y +‹ª”ŒåJ£­ãýÆ°ø3.°« +qN}<Áš¡#\¹&pNC•ŸkîpLý(=•IcújÜo.? Ô×Ñø/¯¶¼ƒÓ÷[@ÜTšÂMyŒmÏ“­wÄþ×èMó¥:ç¡è!« %B N¯<¢ÜÅ'[0ç¢?OÜÓ¬k¢Ì—`k%ˆ¿\з÷2{Þ[*/™ûÈUÁ/R,ˆŸ,Ñœ&i5\q¡áLsN¬Þ÷ +"ë‰#ð\°îpælüeí%‚$ß:>÷ZíN£„³ƒd/  lYº€“0&Aø`Ô î7I òÂ-È€þ­$`É›$±à3Ð\§Ú ¯&é…œC……Ûì8,ÀÙp3ð_ eÙVÇH¡ÞÎH:ÄN˜#Ö÷¸ÚÔ^‡þeü-ê*†SÞ_c´Àh…§e‘\ã‰mFdcZ?œt®8³túÄÐÙÌÿ”ƒš°„&uXîG +Øšå ôæ á—Lk_wXOµ2Jf…©(–·@»PwŠ¦IoóزƒÒ—DF#¹Š¥*Jœ¦Åšú¡r*ŒÊÄÎÅX†1Ä“…@s>@¾•]Q , ºRzïkƒûvk`Äe@YˆÑ%'j©ˆ: £ {q¶“ rer2 `l“eêäI¾‹Nø¤¡¸øZihŽ~ÂXåÈšgNÔ¢ˆ¼ÌšÙ/yûæ—2ΩN>;Õ|ÿæÄCÒtiçW¤6ë-Ü#´4ÝÊøWZñMò +év#Yìþ„+DæÅJQ‡Ì>÷ƒpØã8«D{Ü»Ÿ!K­\d§V`:*)úZ³ª¨ìr,p$y|ót)ä ]‡‹ÏS¿S1k…5‚”‰>\ÏVÞ™=ù}‹ùè@ÌVËG»6-ë/<òF:5;+¦×XãĬŠè¥0ÚñQßà¾i3 ¢*Ýë9!Úˆlj3Û¬õŠÄÚFŠ&5 hFeù’ˆô]TÙêÈY\šu7£‚ìà›h¿±&·Fä¹?°}YùdCÉÐU[  /’ôÙ´)##ªº¬º ¸B%Œ\h±œ¹’캎‰4¾!/d¶>_Ã7͆C5Ô&ö9œdyíæ×£0+åĸºgs¨–°ÜxQª,&¬fCCìå"ksbê!e:ûÓ¢i 0¢è:b•…åÓâj?™ô2êåÈÀ/NK×t˜dÍÅ»v«„UÇå>5ºÙƒyrÚ{±µvÁoè³cÌ«Œ×_–=5ÞãÂdž|Ý ;½~綾ӫ崊±m´ç@~UŸu3|àðÍYÇ¿Å@‰õÝö³ÎOz9Tcíûjxƒ‡5dÐÉv'É´Žª9&B˜ ÞY«Õ^Y!Ö×[ìÚÌ1Õ6ÊŸVÖëíÍèují+|½owäŠù‰ÍyôôSË@ˆì²b5ØQh +=m µgËõpjàðjltÝÏSá×ëG½¬lÄN£†”­‡Ni®b ÓÉ™#”jçXÙ–ÄÖ®ûÖfCcÛ^ºÕqÊýò®®zI¥†a=£–üw(8ÿß)¸ž(¸þgœÿ:óú79˜×NÂu%áz"áß¾ø÷O‘?s¬ endstream endobj 52 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.001 192.0]/Type/Page>> endobj 53 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.001 192.001]/Type/Page>> endobj 54 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.001 192.001]/Type/Page>> endobj 55 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.001 192.0]/Type/Page>> endobj 56 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.001 192.001]/Type/Page>> endobj 371 0 obj <>stream +H‰œWËŽä8¼çWøJ­õºnÏ`OÅbó…éÙCõ=ûÿÀD’-g;³ª 86%Ó#H}ú×çíÓ—Ï~ûÇ/Ÿ·›wë†k +¼JÚþúãöéŸÿñÛÿ»ùÍo©‰ó>l¡Gýÿë÷Û×Û¯_0õ“-l¡ï°ž6sÎË2é·íÏVÁ/øŽ?.¶˜’Ë .„à]H¹l¯ßô½ßn/Þ•”¶ù\É}GÑÅaözðǼí÷ hÛ¦¯†¶-d®cÀï6vצÛx\õU_oÿ>¹Þ\(æ>»œK>¹^}ÜôO]or è²ds=¹(|‘Þ7@ÛÝÈ®ê:W0‹±ø˜»Ûð®Œé6¶u/]—誼Í{×k•³ë5ëKªúá±9jÓñ\s½k€–ÅL°xï¶5œÍçi ð» ïÆ1ÙƶêµÛÕ¥èóÖ‹KýÎë&º«ilg;JÎç=à­3àjl`Øš‘]ÕsÞ¥EÝÆâüncwÍаóÎ¥çQ"A4Íc‰áäz.ž\H'~EˆKª3Í \dÓá°3^›å‰èV¶±°¿ÛØ]Úzi¬zív)ƒfxi)_“3*9ËÞçf_¹7ËG©Ù?@Í„Ü.x‘§ÔL¨YWjÖ'ÔL?R³ßQ3-ÔLϨ U‰­UæxæšgfÆ3Ë‚H§›iåfRzµiD†n¥ãX3à‡ÅÁÌöfb2dëÁuÉñš™éÄÌd¤K3«2(o,ÌLÊ:93³­Ì샙rÁLë^z Þ¡[J­Ÿ t Žþ$âjK3Çcåô¾ 9;Œìª™2R¾–¹Ž¿ÛðnÓmlv×äôÎ÷nõ§… wš§¨*ÈŽ.4%š"«¦”©)ŒfÙƲ:k~è‰,zòÈeq¥¶®¼Œ)¦kA«ömGÔ€=Ú* Si–jÎÕ¾©Š¤URÒ»Õþ‘ëÝEØ–%Çx'(ò¤Ö—CPú*(}”²ÙuŠ ‹¯‚R† ÄEPâSA=Š {±šÂ‡e)õq•“Y­G©¿“´ŠI:‰‰,e^¦¤<@ H¡V‘üª¼eEˆ;2òïZR¶]ch;mNE>¾_ä“Í>jüƒ@Ï@‹ +ÿ‰ŽáDÇüÿÐñ\âg“pбý4‡‚´†6=¶k‰'‰—§×U@rx=L»kÏĪ]¤•­!Ͳ¯wžË)Öåë¶{ζh÷Gœ£¥jçx—#Þrï8¦ŽH?özfˆy¯3$ž2ä:Þò‘ Ùãý8Gòû- äCèƵÏéI¤û{YÝÏE¦¬>§™#§B–üO3»#· v×™ždvy/³Ë“Ì>{ý³Y |µ ¸´â¢—Eª!–AË!OZÐ}vÛèlµÕ 2Ư·ì:»"Eeä´/s,84±ìª®BÔŠó ç)ô”ïľSǯ74r9 ÑCÂY€­àFu†Áö®ª +%ÿvÃ'Ó/ƒ¨¤MÛ|) «ZWAÝ^ª–á7ÆXØàÝá€`µ@Œ† %dÅ+£Oó¨‚ol–à/`WV2·a[ñ>tÿ)Lðzã â„HŒÅ÷†1F\ +›/C|KÈOŽó[Nßùæe,ßIªyM5t^+K +Åj¾.ßÈtqÂú´ÉC*MŒRƒ ãc‹kùâ¾ÓwBq­ž`Ñ•PxÞX+“6z­Ó¡… áŒ4náÝ·Ĩ"…ÐƧ¾–ÞàÞ¿Íšæº£ä… ßê.x³v,ˆ•µÑòa÷¢BdRwÏæy`ŒÄuÏòÀ *ø$¶ÿµµ ˜è‰™c¡!’¿2­{U 'l3·±"‘èmEÂVò6ÖÀq+ñ øÀÆJìTÛÄ3Oá*;%¥šRˆHRVêßØûà–cü:Þm4oÚk mŒ\Nê/ÁÇÛûSñ%¬×x ñ”j’xÀy±À”C¹ñdo7OÄ©#ôJ·ž SÌ›~1¸=ÇITÄÂN¥EJc¬Y)†v¥\Í*-Y³Ÿ¤Åw«ª¼XPª‹Ô[$=–æ&¥ @çÚã`1ÊO1&Bè‚ð¤3>o:XúþŒ¹Ør&µÙGxe™±|`2 È—ÀžZš'©¨£îȯ3¸“NQŸ&LŸ1¶Bf¸ƒÍºœØ•äw¾¨Ë¦µ3!üˆP¯s,Š¦iNkaánùë–³yÈ +‹twÇ*¯ª­*{;Ã&;<µà8èPê|ZkwÔCŸq ƒŒ¢¨j7G¾ƒŒ¸˜j©õ=†¼–jB…r ØC<à´†VØ^ŽÉ”YÞeÖgH™•ã¡ž1.ÜQÇ“><å tw5½õ‘Qù”´•‰ZA›ïZ½'Y O„>úµÝGdÛ cn¦²ƒÌÃÁ8X5Õ’iÌya"ùªÓFdÖúä4ënÉõuÓ‡Cb-}¿‘X!¥ýu“íKi´{90Z +–'›jZàwXïžìï;½éìÅá åö¯_>oøû[€8‡£Ž endstream endobj 370 0 obj <>stream +H‰œWËŽ$7¼×Wä´FOJºzÖØÓ`±ðÁPðãà1`ûÿG’2³:«zl4­È¢(Šb•Ÿþ÷yûôå³ß¾ûÏçíæ]ŽuÃ3>sÚþüåöé¿?øí—¿n~ó[jÙy¶Ð£þÿó§ÛÏ·ï¿`ê'sÌÑ°†Í²#€ñÛï7ÌÆÿt +(k®ç»ÄíþU—ÃÓ•*Û‚˜¹ÜAë}‹®¬ñý–] 2aq á·u”\‰&‹ ðãV¶Š'¼ÔÐmx¿WD x'™&‰ùÀ´Z ~½E¸­Û[D<á'¥OˆXKÂV]o“0(‘³0—dûíÆß0x€ÝÅP ="<€úÖœ— ˆŸÁæšHƒè¬œ+âlñý¿ÇPu1Fû#>ÁÊðp)Χµ¡Óîî8Úÿ©!§RãV›“Þë~HØGì#øL$|‰U+ @3¿MÜõÀ°Ï‚¨ê;Œ=†D˜\'XYš±VC§ç]—ÆxH– %ÔšõÜ··>jqQàqm¥¸€Òðp>F.‡²j.e”OA¨Éµ˜ñFP!6¾“?-„ôÆ„ßc³Uµ¬$XÖn¹9Ö€t=ì(Ç XÖQÒ„È ÆÙÕÎBÎ^æÁW_h×cÔh+èÂ2 W}ˆ¦kÜyrÁËij<“–ÙD8G³$Â~»ÖNC,H^ÕWe ïceCÕyVluŒQŹhlüöþx:¶—º ÒJj§J EÉe‰—>9«!ÅÜGæQ$Ñ¥ÊRhàöØ/‰^' ýT1•"drc8­<CÈ;!è!@M XÊ×Æ'ržñe*QÓð +Ιg$`c’êy±ö±538$ß‹°p,õ’r Hzžv¼Ý)=°q¯®½p_Ê"Å1Íá}æt`ÒÈSQzŠ4øëfIX¡¼ÆhLƒDêÁ²ð\Mš#ŠªhLÙ¥‹Õmœ@)-mè} ‹RMzøþìDÕ Y‹¡SëW-Ø¢L<ÿ™D¼Y‰ñÐË<#}ýXô¨P.x€¤ï€È`n,žŠ(Ã#Ôtg+Z¿ÃØ›&P’ œj;ài£Ér˜n²°V3ëhêºûr¹Á~{æ!Ǩ§ÖæXÍêZRŽØz\Íšj‰j™mrß+¸³'Éíb$l*¼Ýš mU ÐThj׺æŒ7Rщ6`¥Þgœ×™‹ïšj…û•I·Yɼà´ÑÏ!Ú­øö*™cª‘ß/I8¾]7 Ú0 h+£Ø|×O;.=‚Î`Ï­J[’³þ·Œ @=êæyê‘=j¼' m›FQû²Ö€hêzÌÓ¿lì­M·ñx^r’•Ë=‹¤žB¯Þ +Íkè-ï,ËÅB§H+½ +$m—‘=5tz0‹á|Ì]6|›Çt›ßëÐ3:’¦A¢½œC¯Zµj>Öj3ðR™s}k€–b&>½ŽÙü=mñ¿lø6ŽÉ66¯×aãbhbɨð1ì–õX5Õ8ÏBC(+ã­3ãjl€¶ËÈž:=˜Åp>æ.{kÓýÈ;ß\…ºyÂ4ÖygS;†^DóSD;J¶ü+Šz3uÎ Q¡£~©ðÙ†ÎéYˆÛplÀ/{KCó—†×Ë°;{o”‘mCê5=UnE ÒKvöitd§|+9û‡ä¤®”ŽÌ°Â«TyIÎôŒœõHÎú‚œécr¦9ÓKr¢'ÕØ´ÊÃ"’ GnÊ‘P‹éÈΤkÓˆì%äððÃbçfûnB¿ /3÷hËÈ%7Ó‰›Éh—vnVåPÙ¶fÄg~ÁÍ>¸™OÜ “•ê÷²VjÅ–W–ˆoŒÏEž»fGÿñïj[E+WÐ÷Æ× Ùó>¿£~ ?ü²ñãVMC›Ýµªðvž¬åšåAU¢i@6UÉ ]¨JÜU%UE¦ªègÞ6Üê¬õû®(ù (ÏB³/[/åZPì*[ÚBŠ’·¥4]/Ы߇s¿oª"é()éÃ~ÿ$rhaÈzóåݱäIÉ/ú½ì’Ò’Ò’"›=‡¤äÍ,†ó1wÙ˜Ðì’_H +Õ°Fšƒœ5IzДôTSýÞØŸ·NýþAO–¿½ÛO=ɇ^Ÿ_ê Ž§húˆo,~v^éI>éI6©È»žÈ¶t†€¶Óæ¢Õ×£œÔ‡VŸlöÞéŸäzuz%hË5'Ó©Ó§ÄÉs§ÏÉö99e¹y¢"ñ¤"ñ2âô$âzT‘Òû3îÑ>,»ëÈŸ$Ò]»7œMê‘çS®å”ë¶"çíhÅîÃñfÕÎù–=ßù"ßqLµ»Û‹¨g‰XÔùe¾Ë¿É÷©BNù¾®‘òq¦!! ]òI̯2Ý?ªê~î4rŒy̹ۄC}„W•ÝAÔVã“ÊN/*[>Ê´¼¨ìtŠúÛªúû/Ÿ7üû[€œÏœW endstream endobj 369 0 obj <>stream +H‰¬WËŽ7 ¼ÏWô¬,J¢×lŒœŒ È!0Èã°`çÿI©Gê陬á`nUEQd±¤ýðóëöáÓ«ß~øñu»x—BÙðŒ$Ï·¯^>üô«ßþüçâ7¿Åšœ÷´Q úþúûåËÇO˜úÁ‘9úëa3æ¼L“~Ûþ¾À þ°À¼Ä9`fÇ!¤-“ œëvý¬Ëâé¸äíAe˜¹Ôª€ÚÚWàzI®R]Ä>*Eøf˜È8;‚ WÞ +žpS¨Ùðz!Ç9ð.'1‰’L+L0øëà¶l/eÄG.sÁrÄ~]«“0à ³0Ì.æíí"¿ap€ÍRè]@ô¤X·ê|N€ÞÅ€ªÈDÖy)ì µ1¾^à™[GÅ…È{¨}ŒTø+CXÀÃev>î[ZöwE•™ëT½kÜP'„‘ÅÓ¨vš8 ’à²O‚rC‰°hhꯗ›V ÛDí}¹ÃØ"EÑ5Z`’†’ y’b5+H•,7J(%iáŽ;rzd‡…ûá¶vø £·1r‰xG`—#Ôè*x…½†1¾J'Õز~ÕVU"'„ØDIrÓd‰"cÖj`G‰^‡Df0N®4arBø£îE »†5%Úb4ƒãî"k-a\¥n$É7<è•d¡Œf)ÛmÖg…Q!`N­¯¶²â<êëƒÃ‰•‚z2Þî«Ó2v +R2xF±-<#Öæ’¼‹f)á"ÂP²,¤¦‰—TÀ2–&T0Z÷›7iô2øo@º?Æ‚6ˆt2õ1œ +M.†ö&>œAMrØz·bçQz^âc ªÑiÏ By€êrN½ ùôÖvP#B¬mëÖ±‡ä0bœ«Ð%‚.RéëÅÊÛÀzì'àÛgÙA[bqŒcx9íXº³ª¨ð×ÌR`1éµiH-lª²Å¦¬2Qì#;˜‹Õm±Ie¶BÀÒVR"ßÛd%CðÕ5špÂmjÕ[U2//“ˆã˜T@ä ¹hû¬µ_ðìY%©o‡HaªÂž‚0¨gûmØÞalÍ“§•ŽÈß {T&åiºˆß3ëmåÉ—ã”d·-ICТÕ1Öš­õ'm[OV³C•Ã‰BÂy)ád¤=JƒìiGj¡ÚƒUe·Ù(@¶ZŒÿUu¦4åµÌxX'Ú@ˆ2}Ÿg¬ÎæuÆâ7E5Þ~–ŒDѶAä~%ÀqÓŽöøö ƒfŸj½ïwE˜¿.ögËJk·ïxM¢¢?û§ùÖó’¹PHSEÕQªÖ7r½AëyÙRÿ o=,ÇF•“üb¾^ÝÆ*†4—:z“[ÍøÜÏ‹î £¾À›ÁÛ€Ÿ:™[úô–¹Š>U™'J¬K£—¨ŒÕˆxG"´U‘*¶¸õïÄv7’gégYI†~·±¯6ÝÆýy*QroÅ•‡‚\X!ŽsèÕš­’ªÅ2¡¦‡™†^„zöÝ@·5#{j_ÉW³èÎûÜÝÆ+ÍÐÆæ÷¦—ÞÌgÖC,Û©íiÉqÐÅkY3§­±6ö´›ô +fÚ%¾[Ìƾš¡Ííiä DÏب½.ßУí==Ú6ëÔÿ¿GE^b ¬DÏÙ‡§=Zõhž{Ôš¬žöh½ïÑzèQîÓ{w>éQ]n¢Bt\ó¡GóÒ£K‹æ[‹Z+•­1 Ãȵ·èÜ¡yéÐÒ;4÷Ù½7Õí#qI*Dâj;ïÐjÊ;’¦ +‡mS‡¦a²t(ßwh8íÐÔöçY +:mªÚGkQå‰CÑ’Ïš{Ÿû9~jYÙ:R€˜îFö´»YÓc)u/:ô»| }®Í鹪 7kŠÚ›µ”4‘þCwÒÜDßÙë™Oß×*+P!¹äŠ›SY ‹¬„ÇG§UVx–9¸¼»Ýið::3þUݱ‰cìiIzX’ηØËœô²õ}šÑ’õ0ŸFðKÖCŸnuy.ŠÊüNÌiÎ:Ñû³>"_ÓÞùÂg|ùÖ”#`/ÿÖZàÏSNRž{‚î·”ÓiàkÊ©>e|¤ý˜ãä¤G>stream +H‰´WKŽ#7 Ýûº€Õ"õߦ3ȪYäF2O€éÜ’Uv•§;@Ѐ$²IŠ¢øžÊ/¿¾º—·×à~úùÕ‚OXxLѽ=½üò{p_ÿ9\lɇ:Êüþçé¯Ó—7r}Ñ@ ¾“õ°>çÅé÷7m†-ó6i,E6ûîhgú£-Zõ9`w€H6¡¹Ë7ÉáÛ霊ܕÖÍçÆNçÔ}k…öñ1䛈¾Cv—ÓTÈ\Å×4|êüuÎèglû ™b øÒÒÔ\ošˆ>æ$ŠRv5˜|¤3/^ [:ìªá¹ÓLš+%•Eb"åFŽóßÅ #5Oe§|©Ø"!E"[“!ûRŠN»fSÈî@'„45gˆ>e¶ štXäî±7Ž94·3åûcŸÙ÷V™áó¨™Ëš*ú”ùfÓ¼=“ìv/'“õú¯CÔæÐ8Aþ.Ô¿€[nÛ˜¢•"ïw`-uv`ÂdXÊ&¹ßD +Í:P2'ë@ÑðºKNèÆ.*]F%´Å >˜d\N&ët¢æ'AÖì©ÝcÀ\„î#Ö¸Ô#ø)m›ÀwÌSB_r“M(Ø£3½ +l;x¬ÒK-`ÄQ!LÕª»®mœçYSï>ô@»…ä±P˯©KŸóÄÍë!ÖEêGêráªWÁlÕHG¦Ѫ…7ßiÃÚbîºÖ¸»©ÇäÜ;!¹Â6qžL’EÌ0¥è1UMœÜ“l 'Ù¦>Œx,š8G` -W!LÖfs×µÆÝOœ˜éT¢Ä6™÷*™÷*™ç’§}¦Æ³Ì¹øj+k³T%oÖ&e?5W!LÖvs×}Ôn7o 6OäÎmžjì›ÄKŽN&¦@_¦DU)qôJ;-99ØvØð˜´Ë9€Xl´UÔFµj¨k »Ÿ9õH¨Uº<…þ€ö´;…éÿPj’Š‘Û<¦øŸíŸeŧ¬íâ³=â³Ýá3›»!óŸD-©»¨Í±¶{€– @7ø,7|*Žª3Mq)J Ÿ+<ËžÕàYÌÛ€)ax%ŠØ(Àx¶ <›bïàÙá‰?„'>gã^ÞÔÛ!ÓãÆð,HÈYÏr§<Ú' +ôáYì ¢î”[ÍŠGØté(žº¼HÉ¢È2LÖ¡ùêZƒî'MÈÄ‚ÌœË=§à€?SᘴË)xÄ)Ñ8…,œ‚&„isã”…Rð [ˆp™ú]—LFÉÊ(qJwŒ‚üèL0[å‰Í“…DVBÙ哸ðI<¤BÌt4BeÂÜîø¤‰NÇ Ÿ/• ŸDã“´ðIzÊ'„Kèq <ç“Ã?­~úèƒ_vüº<øu<ûLHßùÄ(Ä„æ-£t7&ÔŸ&í1ÊúàÇ]FIB"#ˆ:nø¤™³=ô?~î©Þ-Ä;` +L€ÿÌýÇ6ÀLŸ¦2 +׺µ û|‚>Áƒ´ãù$¯|ÂD>ç”lò7x —²vß ýúÙæž6%ÇMÉÓ-÷º–¼:;§­É®É§‘ü&q4w½•çl(½"™×ûªÃZu€W}d¾Û-ùSÝrPrŒ¾N4ï燣Š«ÏcÞZqØÍ{[q°¼—‚ªïÖ»Q¾=7ÕÞ>Ðâ°í’5å¸Û$OKMý½]’‡V}Ø Ò×xXß T|—O…+­›Ï×çD Ô +Ÿ'†|é¡d˜M!s_ÓdûÚ±XçŒÄe4Û>Cfî_ZœšëMù9H¢iJÐ~›Z^½5tg™»jx¦V*Ð\)©,êÄ_*RnMÝþ[”(ýê%eöØDB +D¦&Cö ¤6ͦÍõwƒiÎ}âN* ÿf™2}(týà3ÍíHùþÔÅgö½fø>stream +H‰lVËŽä6 ¼û+ü£©÷5“ §E°È!ÐH6‡žvóÿ@ªHÉvOh³d‰¢È*ʯ¼í¯_Þâþ˯oûCÖ¶ã? ÿsÚ~Û^ÿ3îßþÛâ÷ÔsˆQvjÏŸoÿl¿}ÁÒWw$îèfcÎ1ÿ…“ÿÚ¿oXÿÀƒNëUŠî"-ˆFÝoï¶ßûö’Z( AíwØ=ôZ`¿¤FðbÎ'ÔoÇÀ|ÞÏ‘,AFÝ—¯—¬A3"œû, ˆ%åsä~Žh ½'Ž¨†Ž¸?‘Jì×UŸŒäÐc»ŽŒŠØ‹(®†Ô9(¹O€¸jèÒÖ;x%(%©¢šì óbѵPcžÅ6ŽAmG!n5á™Bé‚ XaÛ#ÇqrÈæâ(A[º&e­y9’/¶íýÀŒLŽÂM4ËyÛ&öÊßt^¸Ÿh¿˜úõʹ^CL£ìé­ˆÿ "CF#èPá¡`kÍ{B6ë· ëWȸr¿J¹¤Äj³ˆZh(o Ë>BÏÎÒèÓ¾áÄ£,ˆª0ö>óþ4ž<Ä® +d’ÆP‡e!›VÕˆdÌF6ौ', +‡§V®PàeŠ³Ñl.ÕŒ³Ídœ 'Ë Ü6÷>áÀDå P)·36á4GÜD*’Sàožäᔋ¦±¦ðÖC­½\ú„ ƒ$zLBuƒÕ™¨W“EæCp†î²p ~U#l5Z> à pf ïF½ÖÄ>™²A¤€© +j>,CÙƒi*Í*øñ0¨q½†eår˜Š2kyÓ +EúøÚö*’–CëBb"Z·Qsh|½Aù”áô"S9HtÉ–¡…¬£ÞÑU@á„òé· I×¾` CpÐÆ@i#H»&¶£% –#eÜ‚t0szèÈ/½7ŠÊ; +–¥…M¿5VGФédbˆ§z÷¯•A§ a¹}³­ÝF›/p 2¦ÍûM-Z¢™ûsy”Ý™ü…lEÓ×r»†,ñ“sÊ¡- XefÞÄ6ª7/Û2Ü®ù©l­/¤¡ ’švN»7="PÀV¶ÂÖÑ®¦jqòÔìDd +}i·ø2õymІº».EC|-Ňóòvª»sØm†WQvGÎCØùËظ[£W˜rëÉè‚¥ÁˆÌUjyÛÊëÄ”R2”åDHoS'ñ@rÊ>BN²$ýÑ<vÙÅA)F_Œ¸ë±ªñ›®L4 õù¡·e#! +„sòÁ·ÄCyÇ¡K4²ŒuOc¡¯ÆÔëñ‚VògèÕ4üš'6kÈ +Sÿ3ãâ-+{Ñ1n|-Þ´b4<š\ðšNÊ˽9œÛÍé0ûìÕ{v%UÔIµ‹:p`%v~Û;j…ïªßüðÃÅa zRcF{hìü¸:$_¿‘âá…ÁöËüX2ß,NC¸‡†Màt~€ 0Uu}sYóàì²ËÚùì¬NÝwæCì orÙ¿d{€#¸ã;‚ýÎ×¹þãÑ®£˜.cÍŸ`ùšÐ÷y áŒÎYm_í_·ÿ‡NFB endstream endobj 47 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.002 192.0]/Type/Page>> endobj 48 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.002 192.001]/Type/Page>> endobj 49 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.001 192.001]/Type/Page>> endobj 50 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.001 192.0]/Type/Page>> endobj 51 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.001 192.001]/Type/Page>> endobj 376 0 obj <>stream +H‰¬WË®#7Ýû+êZ-R/j; «Æ`E>À˜I·Hæÿ9$¥*Ù.ûÞN¶ŽLI,ŠçõùŸ_¶Ï_¿Äí?|Ù.1dn¾éwNÛ¿^>ÿôsÜ~ýß%nqK’CŒ´Qgûýãß—ÿ\~üŠ¥Ÿ}#ò~‡õ´™k>-‹~Ùþ{Á.øà€ßñ£›û¶IJÛˆZ Žu»~³s¿]>¥2%Œß0–ÐRÇøSêA¸bï¥a[¶ëeŸ°ßfkÇL¦@%os¯O™+ôc¼âô1|»PH,ú s—Æ)×ËÀþó6¡;éÛDû\¾­È~€ÒÚ©e G±½s> â˜xÀ&ì·ØŒºÞÛ6÷òç¢mž31ö€/)3oÇ — ’t†9Há³ê¡à¼eÕÉLÛ:ÓC*dD †E'Iÿ3¿jªó?ì@¬ è­æÀÜÕd00i\Ì»jÌc"ÛÁ1°è3Š[ÕßJO . ‡¿Ïì“C¶-ö‰¸¥5(sÍãÌܸŒ)°ÈŽÕ3Ú/n q±šs†ýæß&ô¼ð}ÎsŽ£„Þú&ˆn-KÆÁ14"*68™Ó–Ì:ÁõRCÜÿBÀY‹È(#&šÆG-4Ü` „9¥`¯ÔŇW`‡aÒ' 1Ì8CE‘ž érró÷Å‚¬B’w µöE!(T² ÙV¬L@ -½B¶\Å4'„cdVµT­–xLlf¹-½®°&-)D4P-þ$}ÄÇœÑ\eòû»{–¤Ø ŠÏRq‡Ý´nŒB!ÙQB¦ÊVBÅ 94!MIIcŒ 7r;Âݱz#…Fú{fX‚S¶Ðtd&âÔM?RÕ¬­X®—âÊï°†Nø¯„Î ªŽKÑÇS"T¨ämrrl€‹³Í›Þ®Õ¬JÏ…ó…v*fåøÀ M3j× +wJ7Á)_ídCÛI +¥±Vw6_Y$Þï%+QSÝT%(æšdˆY´ôÝÍr-ƒ/j;(V!7ŠõZ7W^{X”7XÊL}Êþ&u ­[YmuŒ±©¸Ì)ÂÝÛÊVTM"¬WñØÉ\# ‹ù—»rÐ …ŽÁiaLP ÎE' dLëQÝÉDW­‡Iêþ/ÈÌšBiÖœTKL‰‘®ý{lAÛÍï Š<W« ÷¶Àa …(r¬uU8NÆwØÕõø;hBµ .Þƒ&°ÀÝo;þ3¦ ¡²XÖŽtÌg +ÙTTKÚz×Nˆ—¸#¸Ý{Àà-o–1F‹â™b¥§›šk¿ÑÁc¤”ZÿYÖÜl¶œ2O>ôÔóö›†ÃZ´™Èà¦êWi´»7°öм¥Îÿ¸«Â:»w…ÇxnåȹuáðÎSúôgéÀå•ÓM¢±Ü.b·—WÈÝÕÙÙHˉÙ:€©¾b¸‘~Œ°7‡æ5[ÍÄÝÄg}µÇ÷Ù«ƒh[ˆË¡È d—[†Ë´ZL‚Qp'Ð,ewÒgŽ›­˜Æ>üÛSY+®Œ­ÇÒÝÄ7÷Õ>v»sÇÑ%kUG?S +4qõ»‘ɇý€"B;‚¤v™ŽkÒy}1 ¶ÓF¿›û-Ö*$*sq·ñMlµÇ÷©ßxG‰\ô.E;¹¿Å.µ‰‰`ôè$©L¿«û-pjKÓH¿³§T´ W—VÝÜAÜm|Ö—ÇÑø辧žWí«Uóã­éƒ¯9Ξlo©öIŸ9ž­•(.ù +ÔT† YÑñÖ¦jó•GŽs î6:Ë›úØíNoZ„$YŽ7nwŽ‹L"-äl9寑ªÂuB{v}:å&¯Üä'ÜL 7i¥fº¡fY©Y5Ó &ûâÁÉÌ$m¢D3¼ò¶‘‹çÌl÷Ì”•™<œ gÔl7Ô”jòÂMyª)èÕá9î¤Þ%økn惛v‚x]·—˜Xv£÷¸)§Üä±ãø>å&io™Œ›Òõufq=¥8õ¶ ZòÌq"«mÅsŸ´»0[7òo :™<ñ66ww•±ÜǾï¹ërÐÓØ)¹Ò¬Ð”“…¶ UVœÿãÕ‡deªJÛFsìÖcå49DEQ¡'n÷À / àfë鉦xÁOýYÅO«¨$“…aToD%õªŠ|@UÀ4æÊϖ㽪¤µâÇ~SòÓ!+e-ùe‘•ô\VÒ_’ˆaR¾œUî2|—•v#+í¡à—UVÊCÁ¿S•ºªJ½S9 +þ^öŸÈaNÈo¼Õ²œŠJº•äz±‡{ütRðÓ™¨ÔUTê‹‚ÿZTFÁïbæü’˜ý 1ÓJÌwëý)3黩9EДÇÖÏýæAásAá?'(x¹£×¢ò¬ÅÂk›´º Þh[’óë4âô‘Óï7!Oï‰!½#†–)æ7ßû}ñþ$âiø÷eÊûásM‰\Ê¿Ó ¿ù#ñæïŒ÷îWц€ÇŽWˆóü~åõ‡£ýçrä1Ö?~ý²áçÿ ÏMU endstream endobj 375 0 obj <>stream +H‰ÄWÉŽE½÷WÔt9#3r»b,NBø€˜Ã ’„Äßó^DduuÏû€„Fê‰W•K,/–z÷ãûíÝÇ÷iûîû÷Û%íšû†ß"üÕ²}ùty÷ÃÏiûôç%mi+C÷”d“™íÿ—_/¿]>|ÄÖw~øAŸ±kŽõW,þeûã‚ÝøÃÁŸñ‡Ê–ÓØSÓ¹áØš«nO/vÝ 5*m»¦½à­ìR +A8bŸXàéRö¦mA\*4­!ç½µ‰eŽto³lØ2ÇV÷Ù:Þ§ºä§ .-4|Su@£º‹d¬øý’wiPFöE/Ø’k[{dÔD!H¥’uR·ç žik¯pÛgŸ„iÏÅ Jª€’ ^N®m­Â¹Œ­ãZ™Û’i+vTwí´"M ©´Ä.H~©}ÙpgßÂüÓ9`2÷^‹nsìy@Ñ#`8höÎórlŸ=ÕŒÃòÞ“Ål&ž¹pG3­Ô]ú#„}ÙPF8ÚŽ4áé,Žò¨@½Ø¥%ÛÊ2—"x¤Åâö`Hfä›l³‚9 ÚaHÙgƉ8<¤½ˆvɃ¿ÐdÚ“±dDz/PÖ\ 6ÈÞ™[ø3ÙžF§gæP¬¬Šd)û¨Ü\RY€Ïy,Ïpœ'L‡\{Æ3u㕪«x,„r‰ñÍàx€'Æm”¾°±±R­þ-Z37/á9O@$¼2íIZòS\í¨î¥sw¸ŒÈ51}‰ÂϯTEÚÀ4\ÔÁÈ3Ó¸e¾¤ëh1Ž‚Ú›SL[øÞ’¬ORJÅê-w%âÐf€4r A™ Ì\vgv_E$¾GcIAù‰l…塽¢~)Ô¤¾@Ûk¬#¸Ô8<@剭ys +»lº!莜†cJç#ÏÎâaݾ—I»a¶²´LŽÍÄ1¤m^d\~Zu2ÜÙ÷*síŠS·Ø„úØ,Gã›K1R%;y’Yv™‘.CÛ‘ÖDHÜFO3:)’ä +ÅQÎ@ƒYz½£S>WS4[H.†{,µÁÿVŽ—ȤÉ6W¡d¸Ó\îd (ú +ÑÑF)_ú…d^˜”å^ý†×r°®ÜÐÕ‹ÁºÈ—Þ#¯§'(`-«_¡Y™˜œÖ¾ ,Vi,xõ´H–’ôX6’¿UÑeb£ÀëIJ›¿ÁLx:ñVædå@(CR͆Ñ0Õ{ +ÃÓ¬`¥%ÌìQï¶<œww™ïJ·jêœ}á­ý9ð5†4r¶…@‡†·‘c@ìô”OG!àÓènýÃaw7ù¦PâÐî‘ÑR&JŠÊd!:¶ÄD›ìóˆ+¦Žª(éØ”80¤“$lºâbôöEá~Ú”1í I ùðyA5fràl‰=«‰ƒÎ’ÚZ^ÀˆUÀ³Àp_cD1lå@lÁÎÂ¥/›‘> ec,}€éÆ u¢ìÁ„Éú™ëÙ)&7NFb NMjÈt[«#\¹¶K^²ålÒˆXŸ.½,häŸ3zOªÖƲôŸÙÕ„-\8T±YŒ:j¸üäí¨d¯îÈ#äkŠq4 Ç!ËïªÖ¨G¡]7™v  òØIЕ˜ª›Õ®n­ÙTÄ*6ÓÈ´q¢šÒ-–Þyáƒ+ +VEóÂð¤¨ÊçðlÄUlÞfŒ»åÈòT¦5‡Õéªþ:í&ÎÍz++ÊómVkž¥òÌQÌaš ¤ô˜”6û8$“ìØèÒ¦Ú%´ªIûT oü(—m”¶ñ…ˆ O(Êù\ìèzRmj-AjŸ|FSii8ÁDëåö9Ò© =VÞl~¢ ¦€*‘ÒÔîºGP›r«4Ö“ËðNúBF·©r¼…ú£È’í0Æ9œeqZ¬ÇXÖ‡lƒŠYûa°‘#yHÝÉùCŽ²ÜØØÄ™U:‡‰‰iä/+Ãþ&÷|®7¾vm>ÉóÏe‹ÏÉ‘ƒQ«ÝÚrR«÷ÑZø}gl`³è캮²Þ!-r|5¹Ìaª›!Ž1w'Yr¶Ì«µÈñ…#ý—FÌCLsó*g±ÞYå8œ`±qx!ô“J®ÁmîË;Ǿ¢ICIäDš$¹kÚŒ9ËTð/’d±L,fà~T#BÐwŽp¢1—ü%K‡‘X* >"]&ƒK]Å"3t,*.à +Ÿ“­%5+º…HÐWNò‚;¬ÆŽˆÄNöõZŒÉI')“Zh +ˆ&Ö¶;Ž{zÃV^k|N J*6#Š‡(Û'Wöé%ïãÙKFLRÕé _䨴ŽP[+©Å°ö0ûfЫ6 §s(ž8zá‚[|0õ>Ç‚­ø…qŽñÅÍ–îo>2:¼NÔŽqÚýˆAg‰ù”ïÓi)»ŒÕjŽüo4ÜŽ• ~%»æh÷ 7YÍ»ºà¿W§Yr¡ZÀMìëÙ8­\»|¸/ØÕÛl©Ó¾u&\‰Aľ3ªê;Ô˜à¹W×€´üdk ÎÒu–ËëG®ïX$F¤u^¡!6vö<ÕÖw{ÃBåÒá.‡á®¨Ç–pÛéÝ8¹['·ã‹ÍX~? Õ–b:$Ó}¦M^—toÍ6ßœØø³D76¶éR®µíìŸW%k 7Ì)_#U>“JÿTóLªVýw´úßy¥_ãU>ÓE¿•WóÌ«b=0«˜ÕOÄêw¼êgZµ;Z};¯ôÛxõáãû ÿþ`´±MË endstream endobj 374 0 obj <>stream +H‰ÌW˪$¹Ý×Wä”ZÏPhëžÁ«Æ/ü…=^ܘ1 øï}ND(+««7ƒ1êêdJ¡xœx䧿|>>}ùœ?ýðù¸åÔë<ðÛ +{;~ýéöéÏËÇOÿ¾å#M{ʹeUûÿë?nÿ¼ýøG?¹ â‚~Áî½gŸ¹_ýýøù)øÃ¿à…Ž–²,=fKc-9_íZü&•~Üs’UŽšjÈK­ëђβÁã6S­}ÙÖ8Jªsùr¤ªË6¬TySj0¹”¤µPtê[Mm” sZ‚wø·*•Í©ààãö¯[‡T¨Ó5ÿŠS—„Þí¸×´&aQ•’±\i´u|Üø2·7\`Wbb½àš¡1|¹&pNC'_×Üá™>úQz*“ÖôÕ6xÜü‚€P_G㿼ÚFpN?nqSi +?åqš÷bëÁÿë5|Sà}9t ÎCŸÑC8V +„<^Ù—Û<îNžDsÑ'îi"°WS•òþ¦V¢øË än„=Š Á+•ÎDÌ]äªà‰‹á7†hNSEŽ¹à‰ gš ѱ‚Èz"„(×C°îðål|²öA‡~o¯{­v§QÂÙA²—Ùè™U  8 ógDöHÝàq“Ô ? ÜVÐo%É ¡$±Ø3Î\§àãÇM“ôBÎõy„…Ûì8,Àƒ“±6ÞT… ¹dY¶×1r¨[FÖ!ð±~ÄÝŽ ×@ô: (à·¨ëL^ùxÒ£UÖ!‹ì/t›°"Õúá¬ðµÀ›¥Ó!`†ÂÇ ecã ‰&, ›™ü;lÍzPP 4üpɼöu‡õTÛHÐ(£d–˜Š’y;“¢v=xãM4Mz ¤Ç–¥@¾$R‚ÉU,õ*ÒrZ˜kê‡ȹ0*;`)I§& |ÛË€®¨–†])½½~l¯FXÆ0)ºäD-Qç‚a”a/ÎvT®LÎÉŠ Õe™6y’¯ÆÄ‚R¦°Vc ¥Œ3Ž +G?A¨œù­Õ¢ˆò–ª÷KâÞ=ùçT'Ÿj ~AyI]Úù¹YC +‹–ƒ–¦›ïàJ«>0JÞ!Ýn$‹Ýßà +Ñ£yµRÖp˜)ó‰Ïý`Ǽœö*q^›_¡—Ú§èÔ +Œ@c@%E[kVI¸‚Jï˜-Xƒt\gÑ-S¿S0k…-r”i†›žŽg'ÌÅžüºÅlt f©e£]“–u¹“Mäø¹br]ß¼œz•x½lkpÖÙ ðWÞ»æ<Ü÷ð¾ŽrÂSËçl¡õˆ³V ò®—g/›¿‘ôzÏ«OýÞÚkC沸–ŒLÊõâåÌÔc‘ø¸±ë'–‘H 1 +]VàÙ²¦‰e³™-+*$(kŠëÛ²£nR‰ˆ[ìJE¬Vì+j9Û8¬Gù `ì©[];*ÍìsÀî¼vÍ;•e£Òò·±ùæ﵌‰ù Î{¸Hò«AP À/zëkÆVò „Yf0|-+`¼ 'NµF>»l˜­£G[Ò¼¼ÅõÖ6þ –ÆVÒ…> NGwE|ýð”ª!SŠÍeîQ·š{Ñ°Z/Ãl½œu2ÙÓG€@ °0÷C¸ bï¨Îªj™6W³®:gƃ„™™m†|šüâŽ7:¸c ö‚Êaú ÊîŒ;gc¶ÊLmÍi¶KD·U›Rš +‡n(i ¡.| ¯ÚìC§Ã +à ÿaPšVÞÀk°ëÌZ7>&±°¬Ùób‘˜FгÚb{›!Ê×ÔVlÌ!¢¶B + ó´Z]E^eî Ý©z¨JÃh—nñÅá ÷‘|ÐmúÝÖXº"£+g.² {mŽÊ¢œ™7Ä‹ºCiŒyªu¿ƒ§µÍÚ6€ Ò½ ¢X9W×ůH¬m¢hR²†f—ÿ HàE•!šyŽ˜äB\Ø£ w¿±,·FäÙ?Ñá-Hà}CÌ9” UµÊð"yAŸM22ÂªË + ê+TÂÀ5-;ÍÆ]ÇâïŒòh#ÖpM³ÑÀ µ}gYv&–µ‘y5K=ߢªds¨–^½BÎüO„ æW&§C_ÝûF”ÅöÃq+³Ìs¶¿NQ6{q2P~Ýæl$ÉlEc‚ÈV¼–¸'‹vc°Q¥1RÌOþ™RœÃœ„¡h (€¬¹ÅzXv±ûáË"Û¬Ž¡i7u¤ø²á‡‡¬îÊØìÎöá[]×eƒªšC×nÌ°™LÀ­Ê!SŸØJn|PªU,&Œ³Š}·ˆÅÆZÑkÇ@5…7|(¶Qr¹f……ÑÓ¿Xó6âå¼À² "³ƒ?Ã#0Æ wíV«ŽË~bt³ƒ¤G µvÁw4Ù1æUÆû“e3ãG\øܯ›‡UÃ;ór}§WËfõz4Úkù'±ª¹>pxwºñ·¯ðï¿ ¹tsJ endstream endobj 373 0 obj <>stream +H‰œWËŽ$7¼×Wä´F©×ug Ÿ†áÃ~@cí=ô°÷ÿ ¥¬Ìš¬žžEÕŠLJIQŒ õé—ÏÛ§/Ÿãö~ÞnqãßßÜ>ýü[Üþø¯=®!Ƽ¥‘ñ?mÿûöûí§/0ÿä““Oþ ÖËfÍy9Lú×öç-müÃþÂ?.ž¶,J¬ŸbHRêöúÕ¾ûõöCÙÖ¿j;Ê!®ñzÃpä²íÏ hÛ—‘ÿÂÐW …®uÄÝÆŸútÏ_ûÔï·_O®÷jê[Š%”RËÉõÆýóŸ¹ÞõŽr(ZÜu Yù!{»‘ÿšë\Á-æâsînç:§ûØ×½t]shŠ§ÃhMÏ®·biæGÌí€úr¼4ÆÜž: eu,>† gó½lsaq·áÓ<'ûØW½v»Éi5Èxpº«ª§™ÒŽ$IJǻÆÛŒL[7ò_sœOiѶ¹¸ƒ¸ÛøS7Œ3ê|réxVdH–å¹b G×Kµè”š˜*êÑ7„°H[YÎø”ª› §ð·{š¨Di› ;ˆ»?¥¡¯'sÕk·8™ŠexíR®É™œu$S~—›cù¯s³~”šãÔ”ÐU+¦¢ê»Ô”5ëG¨YÏÔ”o©9¨)jÊ{Ô„ªä>„I^¸æ™™ùÄÌz@¤ÓÎM9rSŒ^}‘¡“›Õè8×ÌÄiqgfÿ31§ö¾†–|MM9QSœur§f3 +•m‚5Åh§gjö#5Ǥ¦^PS纗ž'ˆwŽ^€¤sÒaÁ± ‰x­ËÊñÜø{î YÎN#ÿµL™)ßêZÇAÜmøTæt»Ý5;cˆcxýé)部ä%& º£ QÉwQÑ£¨Ô%*ŒfÝæ²6k=Ê3—5Ô6Ôx™%˵ ¨Wû¾#jÀm“…¥4Ï«}7‘£¤Èw«ý3×GÈ¥ò²–œEß)öEGEß*J]Š¢SDæâGE©SQòAQò»Š~ä•ÍX“ôaE9Ôú|Ô“U®g­P9ª‰œÔDu^—¦<Ñ@Mµm½!ûŸÔy=‰‰ºNè]L궋 m—Í–yñÙ÷*ÿ$ЫÊ#ÐjÊâc:ñ±ü?|<ùÕ&ÜùؘSBzgÃÞ¯$Ÿ$_z,OeÈu¼õ#²ÇûyŽ”ï7-$ã¥Ïò¡H?Éê~®2õ賬9UštÈônfä¦]g¶¼“Ùõ{™]ßÉì³×?šÕ Â×*®L½†õ ÕËdõW-è>Ûm´¶Ök&ã×[ ƒžB²ë+®M¬{ŽZhµ"‹Ê·ô6ǯ7tr%MÑCÂm€½àFu†Án̈aª +%ÿzÖé—C”Òn}¾V†ƒUm˜ n/Íêðc¬ðœ¬žˆÑ‘¡„qjlêCD|c·‡±’ùÛ†ï¡ý—¼ÀëØ!# ö =Í1âRÙ}9âWpçih ÓÚÌi£ßœZÁúüÀ€&µrÌ5ôÑJ‹¤êEßÖ鷺U;+¶yÈ¥…Qke춆^.`£qjèí«)Aåyc lÌÚ­P§žf„Ì™âgø°—ÊVyÇÚ[¡<ü·5 +%µI@6ìå]ñekYZžckµâB8¾l×]ÜÝÓƒ‰ž# #²>0…*¶Ä @»fº0u"0|‡ìo 7š» àœqAŠI£· T {-Û\.áøÂÇÆli}ᕨð_êŽÀIm.jm„° ]„cóƒG1Íñëü¶#ð¼[³Dö1’YÌ_‚Ž·oϧa'ƒº†ˆ×”O¹¦Â;΋G~æ +N$‡»ª N›±7ÂB(¹lsˤ}^$p@-ØßàÙ™>ZûcÍFÁp„¸£Â„bâR,ýI[lÜtåÅ£ÒB¦â"ë±4OI¡ÛÈ“Ä(@4û uIyÙ™Û›€Ö±¿c2v‚RHîÄú 'fN ‚Ù€8 a»j“hÞ½²‘ŒÊ£ ¼®àNL>e{+˜¾bì¥Ìñí 9q=H}5—]mW&Bú¡ÑÖX¡Ýòœ<¶ÒÂӊץMg³Lôp³Š&;Ùërôk¬øý©OÇÁ‡ÚÖ;ðÚú£‘ƈk¶h8dÕt»êðdÄÕeˬ1¶6W*Ô,À‘ò.kˆ…ŸåœL}Ð÷Üú ©³zi—DŒ+O4ðî†KYÀN×ÒÛ^9UObÍL¶¾°Çq!–9F¦|æ0æxløÙ>ÉXº ˆî ðz0¯VÝD†dšs^˜H±YÉô™u|sšõ°äñsˇ»Æzú~%±’Èþàe¶ ¤½£ÝË{SÁúäS] â.ǧ'û‡ÅN_:{qwÐsû§/Ÿ7üûŸÌy¡Ú endstream endobj 372 0 obj <>stream +H‰œWAŽ7 ¼Ï+ú+K"EI×8FNFä ç`°ó ERêQÏöÌÚÁ½ªŠ¢(VQýî×÷Û»ïãöÓÏï·KÜôïۧ˻_~Û§í51æ-õŒÿiûöçå¯Ë‡0ç““Oþ +kØìö/ +`üÇöÏ%múÇ_ñOJ L±n5…Ø%o×/¶ž¡TÙ^b`YàÞ´Þ·Ê>¾^8´T&,6ŒJ¶…’ Lt,!ÁŒ[Ù*žðRS÷áõ’Bqƒ°šP®ˆCB- _2ÜÖí%#ž‚ðRÒ'D¬…°ÕÐ[Æ$ JÖYJ Ù>_ô7 î`9U…. QßZˆÂ€ø l¡iòƒØ,æŠø{/c|½À/ÂqTC¦¢‘ç>ÆHD$X9Â.%DÚ7tØÝGûÛzH 9• +Ï-HïõvHØGîI=fM@Y‘èK¬Z“&C3½LÜq`–ƒ‚¨ê+Œ=&RH¡§¶BLÆ:µ:Š8½º4]¡–™ …je;·û­ÀmÄ)Õ²À㾕J#ÂùF]M„š#Fù„J¡eÆA…ø'¸ñ@Ho&üž›¯juà%¡e´\…Ö„t;ìˆóZÖYhBdcµk!sóàk,j×s¶h+è¢e–4\ó!–BÏØW=¹eâYžde6ÎÑ-a¿Ýy†X¼j¯Ê^ÇÊŽjˆ` VluŒQÅ\¬l|~}<ÛcÄ)¨´BíPi©¹4ñ²Š#„¯œµ€2÷‘yIT ¥ÐÀí±_%z p ìgPÅQ6Š(“ÓÃiÕq„¼wõ©‡5b_›îœ”ó«5 ¯àœõŒdlLxÐ0!¡Ñ™‡m'ÛÇÞ|¬Ñ!û´ +‹Ž¥iÁ'dêçÛƒñ;æ;Šn,A(«:Ò^gRVE•”Ny‚Ý-VH¯SÓ ‘v²Zù²’%IUU,&)k}¬îcÂ&­¶@ð“Òˆ@Ô8ˆr¬†›‰;Š¡«ÖïµàkjâõŸKÄ‹—˜z™q32Ñ÷‹ÊÏÓ¤Ç; ÈM‹§"Èt-Ûìb¥Ö¯0¶³ T†äSm žö8–eºË¾š[ßAW×›¯P˜u¿õŒs¶Ckslgæum?G|=]Í›jÉ'™j9u“û^Á[â‘Ü.NÂfÂÛ½ *ð­¢è»ÕµÎxA›è-”åý:ãèl]g.~ÓT¯Û/šJmñ2îèçí‰öøöƒJæ˜êä»$¬oÙÔ°`:ÐW:Fq ðU?í¸ôˆJSVMoíPÚBVÌöoÜ2‚õl›×SÏÚ£Æûn|½¹Q¶¾l5 –ºžyúqwëÓ}<ž§”ÔÊ…Ç”£Éz^hÑBo|C`]EÚ8M ¨ínäO ]=¸Åp>æî6ú–Çt»ßóÐY;‡êgDG9†^­$jµ8b® j3ðR5çöÖZŠ›@ùì>:fëï´ Çân£oó˜ìc÷z6.†Ú4Ô" +Dx »±«¥ç™ÒŽÐÊžñ†ƒÆÔv7ò§…®ÔÂ/ØÌÄÝÆßúô8ò®oÎBO ÝœTiPç]{ÚzËOk(ìù7”íf:ê\3TÄ„NõËìÜ@Ÿm蜅Ĵ ÇânãoÕÐýÑðzv×ÖYÒ'–zNO“[‘¼ÍÎ~ÆNù^rö7É©º‚;Y…W©ò”œt §|9åHNz›œ´“ž’=©BCµÊÓ"ÖɤÁMYjg'­ì$#X›FÊÑÁN1BæéÅA7n¶ïà&맇^®pŽ¸Œœr“Ü$§Ýs³¬ÜLÓHŸü„›}p“ÜL“•æ÷´VjÅVk%ã£äc‘s·ìØ?ýÄ[@m4‹<ëÍÍß;_ƒnäÏ«#ZÍW™~ÄÝ&Ž[µN÷±Û«ŠÞÎÉ[W–;UÉ®ìªÂ;:Q•|S^UE¦ªØgÞ6ÜÚ¬ý÷›¢ð¢(BµðÖÓz)çŠâwÙÒv¤"°gÛtaJ‚¥á§cÃo&#´j +½Ùð„1LœÅˆI©ð¦ð“†¿hJ_5¥¿Ö™šÂ›[ çcînãJsÓ”üDSTkÆÇ•²³’Ш䇢²4|§?oþ  ‘ÊK»Ÿ‚ÂK³ç§‚‚Ž#D7f|céwç™ ðAPص‚o‚"Û.4 +ÔvÚœôúºêI½ëõä³o­þA®÷Vñ¥q9'%Z=ý)­þ5)Û“rêr-ò@FòAFòiÄô âºÊHéý³\¤cÚíÎ#lhw!¶7œ vpŒœ¹–C®Û¹^öØcZ¯Ví˜o¹å›OòÇT¿¼=‰z–ˆGÍOó]þO¾rÈ÷y”·3 !Ïc¦ïÊôƒªnÇV#kÌ4käÐnÒRéYewµÕú ²éIeË[™–'•}ŒzV5/Qó«\øø~ÿÿ +‘š^ endstream endobj 42 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.002 192.0]/Type/Page>> endobj 43 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.002 192.001]/Type/Page>> endobj 44 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.002 192.0]/Type/Page>> endobj 45 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.002 192.001]/Type/Page>> endobj 46 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.002 192.001]/Type/Page>> endobj 381 0 obj <>stream +H‰¬WËŽ$5¼÷WÔŒ×ôëÊ°â´BˆЂå0‹´ËÿKD>\mWW7³Tå¨NÛétDØóáç×íçW¿ýðãëvñÿ}û|ùðÓ¯~ûü·|Hœ÷q =â¶o¿_þ¸|ü„ðÚ9h篈1£ÏËÔé·í¯KØø|Å‹,Ù嘶\Ì¥m×/2+ž.ײ½xGQŽzeÐzߢË- p½k!˜]ÚÐÊQZ Cg„p»¸ÐÛ†à–·Š'†©¡kóz .—¢À»B’bE"ÅÕðç%bXä‘PA~Á•ÜD²9a¹®·ˆNhäȽÐ,.•íí¿¡q€ÝŠлˆì'RÛšó…½K=5ÞW¤QÅ +zíë#çn¨º˜2盵Q +Ÿ¥x YœOû’–õ]±É¿ÌÛÔ¼Ã\IgìÖ»lFäDW<1*üsÖÀÅàÊ_/wl˜¬;ïëÆ +Cb˜\m†èŒyjQä±{œTãj¥‘T V’};.„„‘ Åo+É?¡õ6ZŽ@«À9”\F¦É5° +K£vÔ’!Ô6Fü›N*4PF0­C .Mç)*·³ìDafu,i@mrµ3Égc×+Ãì:æäl«’ ÛE +Èõj\yß‚/r&¡Ø@ØEd„åvùÖJÆ.0ý +uk_ufÕyh3¶Ñƒ) ûY1Þî7§¬”°`ð,¤¾ð,d‘–ÖÝø–~å:åW¤.…çR 2Õ"4ðÙÖË2¯ƒý +Xû»¡(ò`kcP¦ÉEÊÞyLÂ`ð’7Ó*VžXñ/£*MÒËØfÞ¢2@s¥‰0  ^U:$Û7#°´9;T_’íÒ˜/ |à­¾^T¬ÇÒ±rcû °€ÙÓh^GQ ³ŠÐ«c»ã ãudXÕyµRv–/± ­ÔSýÄêbvm',R¨Í aÖUª´yÓÉʆè›ëÅÏöI«/:+—ž_j/J2Þö<2'Ô¢ï?fÙ,O¨jà 6ˆRcúT¤y@Ví·{‡±4Õž"l8¡~7<â±3T¦îì +ù6™F {kžÆr™ˆWÛ‰÷8FÙ´6Ú²gÊkùI4¢óñlz¦æxâ1à¸Äb ªžX {ÕQYx€h°‰ëv=ðJ«ò¿‰ÏÔ.´æ/ Q–ŽÚ`žLßçë`ó ”FÈ¿ 4ž +”l@{žq¥B àº´¢,›ÊOƒZü,µ÷ÅN!ÐS¶5« pè¤O½¡u9”ÈF‘¦ßø[´¾ÚÖAÏMÒlTEš­Œ¼ä8ô/wù¾#µƒ©Ä{SI‹©`€ÉT¢3S™<%>tÔ=‰2›?°d÷”¬ž’vtð” ÚO›‹U§XŽý$62[Ê©£¤ÉQÒC3¤ ²¬ŽRžú{ÁÅnŽÒfGI›>G1g*‹£$sš…ž8 +{!5üSÊÂ,µ>w”‡‡>͇>½÷Ð/§‡~ý:Žþf˜©°§€R!,¥oãeFaèÌRæ3?Z +‰‹ŒA´ãb(Í:ÛYÿŽ–Pþƒ8Ã,ÎqÞø'â|vâ‡ÿ&Îá*|ÃåaN]%.®¸Jzä*´ºJž]…Ï-ïãwšüÂÆSÁ¿ ;qÌ–¢Ç¥èt˽ÎE¯›­Sƒ–ªÇ9yÉ/UÖ]÷å¹' +_$ó;/sÕCXª~G—¹ê#ó¸”Ýø’Ïøò½%GÂ>õ‘øó’¯<ÏÇÄÃIârq)á4ñµäÁŸ*>ÊþÀËqp>äyZ*þþ‚‡ÓzÏY§%ëw•ûã§× ¯¹×a endstream endobj 380 0 obj <>stream +H‰´WËŽ7 ¼ÏWè`´âC¯k6FN‹ÀÈ!0ˆ}Y°óÿ@(RÒ¨{»ÇÞÁ­&‡¢J«ÔûôÛ³{zyîç_žÝ%¸ö÷íóåé×߃ûü·:¨°T”Ü·?/Ÿ.^$üÉ&ƒM×þdòWÚD1Sô5»T}jso_4å—Ë5ùRÀaðÌÙ½N»x†$vðÕqõœH HžjqÌ>Š-³rŽ‚GÉ-¿G*Þ“Xe™×˧Ÿ./_eÍ~læºìæ÷—¬‡%:I ¿ŸdÙíFJöIr¢„„²l„“¯uµ+K{¿ +öR’,ã)Ä»‰¾Bt·Ët訞žkûÅr]#ú"8Ç:Ö>žž×»‡ÐSdõH–tèAö$[^f½õ€ÄÊfWO«ŒYÎBÆ, ¢º™â0›LD˜?'äTÅ)U¼RkµP2Il·å´UVÝ¡«ƒìxz®@žc‹šTXìêQErÏ}Oq¿íäc›{¯Ì˜óÖ3+–w©D¨Ón'ËóôºÕO÷vé¶ÿë0­9,Qï&ÔúxÄ$d’¦¦tÒ9åÙŒÜ;0·þ‘Eb½›$GQzªCG¶F7réÝXŬۨ„Õ!uÃ’Ýê Ü.ݶáu˜†O“¬5˜R´Ô@Î’œ/„ê 3-5E)øŠqZèS,šXÖ¦J®ûÍh±3¨=³öfh0ò˜fŒymº½÷çÜà +½6­’ðÀ“´ù +]{» ­a=P^¬q@×C6¿=Ö‚ìÙdA½Ñ“÷¹3¦ySŸnï–÷: c¤ÁköfØ—ÃÐAQP„iI·r6àrh¬ ¨ª©!±\GP{&Þ2´+G“|5ÂŒiÞاۻå=.]’brU²Pà òšyÍŠ<¦8-ò‘i oÅ·X}ï‘bOÅݼlŠgáf„Ó¼µO·u,î7Þ’PÛœ3Õ ðÉéÐdÏg€iIU^ z¦)²ëF‹1íÉÖå-ôÜÍ°V±óZ ½[ÚcäÒåŠv9‡ú‚Ö!huFÓÿƒ Òå ;–6'¦Çü,güL+?`åŸå-?ËŽŸ±OïÌ<ç§H ‰lK—c.{~¦ ?7ôLwz²ëF Åd$íô\Ù™6ìÌ©Ïî¼Ô´g²"_kÉéÀv– ;‹Qwì¬oÙ‰ße'>`gÏ#ÜMÀƒ´^cgB!Î +<ꑶgÿŠ3C>5S¿¤9õP£Ñ±-tÙSœ«^Hܳèk˜͇}®½[ÒcÐRäTP‰cÚK +ö7Eh5Ý:”<“ê’" IÁn„s—”EQðT ‰¤FBK®».™‚MPhZ;A%>¹nôX“‰ÍOª!«žÊ -rB§JHµrc%c,;9I®ûYo•„»œ”UNÈÙs#']–ÒFN¨Ë /rÂåDK£•vyÂc99½îy½îùG¯ûtxÝçåºÏãÒ?ÑÁˆ’DþÃÃ*_í[A©n ‹ ÔcAY¯{z (#‰MÜÈIé“û5ÿýË^ê]íx +/þ%/¯zØð’ßÉKhÅ.%±žàFOÐ0á7½ÅGzW=iB>Ö”nŠR»rõµ‰â;ojŽ›šÇ;ö¼Ö<»¾O ZÁ®ày€ßÇ>ÝŽå±j³(ò¼¯:¬UØTýM·¬UÈÛ%¾«]NJ.¢ \òm›ó8W‰Ip|[rèÀ—Š²ŸÈ¸\™xÒæ´)øìò h:쓇ÕþðòìdøG€W›¼= endstream endobj 379 0 obj <>stream +H‰lWÉŽd'¼¿¯àl©h–’«Û–O#«åƒ? äªGšñÿKŽÈä-½¨Eð€\# Ÿþ|O_žSøõ·ç°¥À¿Ÿß¶§?þJáÛ6QUbJ%äYð›Ã϶¯Ûï_°üÉ7gßœÿ°ù~¸°•XFmaHÔ1%Ü_íÈ×íÖbK=äkÏá±ãRâ¨8ÅQB± ”kÌ%‡ÚbÏüŠm]B‘˜g%„cãrXMÌ<¶¯¿l/ÛØ„óG 7Dñwø;…K£`>ÅÞë»úŒ½‚Ï.ÀÚA0ÓÍÌ­*âŒoðyf…˜DNXb­Ø¼ë÷qÎHŽ)ØϺ r×ÂnfAœOjËûÄãœ(-ª2]7¤QÛøl&#¥¹\w}2ƒbåëÄÄOµùDG‘fµÙÜv·:6y„µÖˆJ™DÝ“°pfV:-غM 3˜MŸ!+jl³_p‹‚òÜ·sæˆG¢´rhhF¹feßóqfOn:‡(êéV9j¶Ðªä}[Ø‹þØ¡·„ãL»ƒI/ŸG{L*²<àÞvFŒ[1Ððh;7 9í;¸ƒéøÔÙÃXz[c¤nsшC¾äÔÂŒ*‡Õ©k|Gä³Í…RÔ4kÏ 4JeÀÿn¨ }©1i›ddÝ!ˆ5-29‚ó{çxƒFÉóÎ96+]Š¥»ÀŒãQ¬dúþÚZÊ,K# Ÿ‘É¾àÄÂÊÒ\c.sD#¹#9MúÉ›(½zgÑJj(43¥±ÔçÔŠ á²k¶³J"íäyþdÄ NÇè³nfiý8@s÷>×Ù¯°ƒ10%‘¦ +}:Y‚Œ>/®Môf”<¬„ï£A‘+¢˜H‹öK4=ŠšìùìÒ@§ûi”@†V­kŒŠƒçû¯Ðmy‘áh ¶yËÍ,”geÿPÞÔt€û†”Ýaÿ >-ΉÂ&J +î•Æ´*‚`{I¢´A̧u›ÎÖŠôòx¦kÊ +öxïRÐÊ@I£ÉÂàNŸÆv$ 8yh…ùø¾Œ;‚Ö£1S›kŒ^®Å&Z y|,n?MP(îFyÓlâW‘¥~õœ€9̹y”z^¹7²Íî*f+š›œWç@[€B0´/„›ºsÚÇ8T§©ïj®ÒÂûÙXki¨Ã®Bx–Á¯b×ÚŸld•| vkÙA ò¶$_Bø=x û˜îu$Ö;Q9îH +”KüP#Øø&Îc>œƒÓž&9î+­ ’H5I>²;Š7ñDnZð}à’’”låã…nPc÷ÌÍÚ3>ÆÙ‹õ7èÛ™f–ézu\úApcvCk°}vƒÛÄ’v£H ñûiVÝÑ÷cÕ~|¡ Ûg– ©uúeO˜)'¦ÇhVfþ=f³+–xÍÞb\ûþ„áÃÌðù‚÷õŠ¦—íT‡v1·–¿Ã”Ùë鈖ÝÓ­…/ËbFVáq~3ª £Ä®Û’¸N­>PQ ƒº?Ùš×7"2<›sÑ̉èãÝâ½b·Ð´\ÎGÈt +¯9uýrÙóæ°‹•Ýò©¬Þ¹¯LG–qL ‰*6ÁÚ| Ã½…ñ âùV€tÈÂuöxw´²ÆûQŽÜÌ[NKퟋ—í3Ídª endstream endobj 378 0 obj <>stream +H‰¬WM¯#¹ ¼ûWô9ÀÓˆ¢>¨k&‹œAC~€‘ÝÞØÍÿR$¥nÙnû½Ù Ø*™’ØTUIýåo_·/ß¾ÆíÏùº]⦟ß~¹|ùë?âöË­ƒ%‡ÓF=á—¶ßþuùùòÓ7„ñÁäƒEôŒ™cÞ–AÿÜþs¡M?XàWüèädCXJ߈Z *²]¿Ûºß/oÜB&Fûm ;Úo܃¤Š¹C”vÀ„ؼ]/{‡ý6;z2*y›s½å’B_fÀ+VÍ÷ N2¡®PöY«\/{ïz’>—÷Šòý}-@ê!ro[ËAZïËó—PbÞ¨.UŸÃqJÈ0†–6,_ªþIŒêµK¨±(®¡rÙR$†±¶OVC‘d¥ýùOÈ'†„°2v-†Zùn—2ÆInOv©´c—¤ÒØ¥½9›Íiì’uŒß÷£GëÛÓ6ç²âsÝæ:cä¹ì=ïGO*AD;P,Éý¤ƒP¸x3æ¤;â¹ÍžŽêYT &­k'¡2!­„êþ/æ ¤¨M>$%”nŽ†H^,Ô°}<:ØVÆΔ½Ca«*,¥Ó‚KÈܬ(³gœr¡µ£„ê5™C:öÂÆ¥M JÛ±æEû¦ 4¶REax>ހΠŸgʼn=¤(¡7Ì,¨r ïŒþoœ¨˜J ‡Ä£¦e‚+ˆ÷¿Pö¤ ÇZFµ‰Õ¢µÐ°“5ú ë‚¹¸‹7¯xðŸ2AŒ†¦*!n‚Ú#³Ë¿/ØM„¡ú„UsyBȪ[-rÓÇFJÎhvÓB'•pþ€‰B1vDP*¯TÒÓ›d U¿Å +€à/ÆSå ®Ÿ|@xOf}ÍŒ%”GŽt Ø”—ë|Ž›g¼w3Ðä*eëÉiñ +•lF&›+©£5«,²þ¡¹.ƒcÕ M ?và9ip\z]a…°T6ˆ +h¡B²ú“ôQ K¦$òý»{Ö ÄÈ®Uáåa*6±›åV(Ôw+F’pb,‘CRN +6v"Ïaó’¦#…†œÆpÊV›j¢PÝl„«ÒR°É\/ÅO)‡5tÂ%à(¡j»¨c_®äÊü$RŽ °u6¹>òj(µãÉP$_hGÐb6ës Õ4S9N&ø^7!)o_meoÃâI«oƒÅv@8²J¼?nLV¥âÕªŠsaj_¢³Ù2£¥¶Õb¥QrÓX¯îc¶ƒÕªt™äw òoRÂÜí +ÐêhcRéæxŠ°÷6²µ“ˆ¨!V<¶Û´V$AXI,¿ÜU…º=Þ†¨%É‚b6óVÕÁÇô`ª›“×Ûš^ÅŽ;r¢]±Ó¸¨EâG/7º»x@¥¼2  ˆf›»nws×QÖUB¬löPÝ–œŠ;jc.ŒqðIKWY[àny¨³NúQ1âvmœ‹5³A¸UUªût~‰Jûá DQz„ˆv!Âêö¦Ñ(ÉtçY&ºê¹ÈR÷¡å¤ ê‰&@]yñ +I½ÄœlÕâßc«ùû~qÖ§ân£áÞ8¢aEŽ±n +ÇJ#ø»»ãA•;Õ\üºÌÛßvügBŸ²°)jœ{„ØÔTÕîq^¶÷¨;Š‹YL‰~;Ï2Ú¸ª8S áìé ázóè.àÑRE­ÿ,cn&[V™+vê¼ý®å°«Ú$2¤Ivký@{zëu_ïB6ÔåwSX{÷›FI£=§räËܦpdç”Þ_Á–»wÕ;›ÄpK)­Ü»ˆñ¹ˆ^#ƒ©ÀˆÔ¬h6ÒÄb T߀ÚÛãûìÍFôRX4ñd"¼e1rÕb¦ÛhJÌäyÃì,o‹u€ÐØg;{õŒÕ€1õº‡øä>ÚÛwž7.Èg3î0Åô±äÝÈÃ~  +¡ÁE»ÌÄõØÑ~}A5 ±3F¿ý®nh@™Ó8ˆ{ŒOb£½9¾OóÆë‰ø]ÏŒ~[ï&¶§MÌ÷¢W á2󮞷XÁh,Í ýÎΨhE®î¦:¹ƒ¸Çx¯ãªÓäIÅ«^¥«S¼Ù]b¥xr:&{uàÚ$}R<Û塸Ë+ÐP1dçŒ_fª^·ò xâ⣽ió@o{ÜiâMÏuNP¼¥t—¸ÈÔÑ¢ÍöÿѦü1mÂTRU®à@nçÊL«2Óeò¢LZ…É7Â,«0Ë&a&<ùB—زÛ8¨vGïAÄsY¶{YÊ*Ë4ƒ\gºl7º”]¦E˜òÔPw—Žý¨wä~­Ë|èÒ?Æí%–=è#]Ê©.Ó˜q|Ÿê’ô&YL—ÒqA^SÏÅ/KÅèM½-¨–<ùM¤[kýF¬ù·ÕœÌ‰Ò6&w÷í•1ÜÛ>ïyê0q\ M™¢ +¿µšò7Kh Z-ŵ?^tÒƒ¥LGiÛ¸ {ô9CC‘ÅPèIÚ=$ܼ!Ë&òÄNü¨çþì¬çÕOØaÕ?áþYC‘O +¢oˆPgãÒï,…×Ã>ö›ÓžO)ëi_OáçžÂÈSàƒÜÀph³ÊÁwSi7¦ÒÎú²šJy8ëï<¥®žRï†<–ú§o_7üüO€lgØ endstream endobj 377 0 obj <>stream +H‰ÄWÉŽE½÷WÔt9#3r»b,NBø€˜Ã ’„Äßó^DduuÏû€„FšŽW•K,/–z÷ãûíÝÇ÷iûîû÷Û%müûòéòÓöéO{P†î)åMfƯl_~½üvùðËßùfñÍŸ±kŽõW,þeûã"ÿpðgüðPÙr{jU7[3~Ÿ^ì:üßµ´íšö¢s“]J!hGì <]ÊÞ´-ˆK¥lµ†œ÷Ö&–9ҽͲaË[ÝgëxŸê’Ÿ.¸h´@Ð`ðMÕê.’±â÷KÞ¥AÙ½`K®mAì‘APM„ •JBÔ}HÝž/x¦­½ÂmŸ}¦=ç~†’* ¤ˆ—“k[«ðC.cë¸Væ¶dÚÊ£Õ];­HSC†G*-q„ R‡_j_6ÜÙ÷„0ÿt˜Ì½×2·9ö< è04{çy 9¶Ïž‰jÆayïÉb6Ï\¸#È™Vê.ý¾l(#í GšðtGyT ^ìÒ’m% EðH‹ÅíÁÌÈÃÂYÁPí0¤ì3ãDÒ^D»d‚AŠÿÐdÚ“±dDz/PÖ\ 6ÈÞá'žm< %²ò|à,æÑí{™´f++AËäØL¬CÚæEÆå§åP‡P Ã}¯2@ÁÑîñ'¬8u‹M¨Ír¤1¾¹Ó!U²Ó˜'™e—é2´ÙhM„Ämô4£“"Ix P\j%féõŽÌ5:]MÓl »ï±ôF´r¼D*Á +0Z€?ÍÇá¿AÚ€£¯=mœò¥PÈÖá•IY/à¦Ñox-íÊ ]½¬‹|é=ò‚z‚Ú²üz‘¥‰ÙY$€+¯žÉr’ËÆò· +£"¼ì¨ð:±ìæoPžN¼U†9YG9êT§³atLõ¦Âð4«Xi d3›TÀ»-çÝ]æ»Ò­œ:i_xkEƒ|©œ}!СámfÀ;=çÓQ ø´º[ÿpØÝM¾)”8´{¤´”‰Ú‡Ò0Y‰N•-1Ó&=âªý„*j:6%N é$ »®¸†Q¸Ÿ6eŒ;hRCþ|^P„œ¸[bÓjâ ³¦¶–0bð,0Ü×ØQ [9{°spéËn¤hÙK`z£3H¨{pÏdÍõì¿£G#±§&5dº€½Õ ®\Û%/Ùò¶iD¬Q—^4òÏÍ'UëcYú‚ÏlkÂ.œªØ- f 5\~ò~ÔÆDƒ-y,pM1‘.Ì‘ÈR¼ª5ëQhÚM¦i“j@ž< Ú Su³òÕ­=ÛŠšXÅæ™6RTÓ»åÃØ;G¼"qM 2(Ea>Gg#´b37ÃÜm4Ïs*a›Ãj¹tÕ`@öFgŽfý•EeŠ¹—‰X*ÏÅ\¦™@J_€yióC’ÉÞ‰/mz¬]B»šôX±ÏúÆrÙÆiaˆ8´ð„bÙ)vt=©6µ–`AµÏ >£©´4œ`¢õsû$éT†+o6@ÑâÔ‰Hijw c¨MºÕÇëËex3H}!cÜT9Þ‚a Rä@Évåβh-Öf,ñC¶©Á?Ǭ1ب*#ÈÝÉDŽÊÜØÛÄ™U:Š‰‰ä/«Äþ&÷|¶7¾vm>ÍóÏe—ÏɱƒQ«Ý:ÒR«·ÒZøgl`¿è캮²Þ!-r|9¹Ìy +œ!N1”w'Yr¶Ì«µÈñµ„ß¼,2#f"fºy•ãÆXï¬øFN°ØH¼ZJ%×à6÷åc_Ѥ¡*2 I’»¾Í˜³bAÿ*IËÄzîGA"}ç'sÉ_²„q‰Õ²Ì2\ð]àÅ"3t,*.à +Ÿ•­+5«»…CHÐWNò‚;¬ÌŽˆÄNö[ŒÉI')“Zh +ˆ.&ÖÅv>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.002 192.001]/Type/Page>> endobj 38 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.002 192.001]/Type/Page>> endobj 39 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.002 192.0]/Type/Page>> endobj 40 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.002 192.001]/Type/Page>> endobj 41 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.002 192.001]/Type/Page>> endobj 386 0 obj <>stream +H‰ÌWËŽ$·¼÷WÔ —Ïdòê•àÓÂtð4lù0+@2 @¯ˆÌduõô^È€1À4£ŠLæ#òQŸþñùøôås>þöÝçã–þýúÓíÓßÌÇOÿµM{ʹeUü–ã×Ýþ}ûþ ¶òÃÅÿ‚Ý{Ï>óv9ôÏãç[9ø‡ ~Á…Ž–òÌí˜-µä¸µkñ?©ôã-'Y娩v¼Ôº-é,Üo3ÕÚ7œi£¤:—/GªºlÀJuqK«ó(%i-ºÁýVSeÃœ–à~V¥²9¼ßþsë +utS¨ZSÇ%¡w;ÞjZ“‡°¨JÉX®4Ú:Þo| ‹?⻪çÔûzÂ5CørMàœ†N¾®¹Ã3}ô£ôT&­é«mp¿ù¡¾ŽÆŸ¼ÚFpNßoqSi +?åqš÷dëÁÿá¾)ð¾È¡úⱚP"âôÊ¿Üæñ6àåI4ýyâž&" ƒ5U)¯`k% €\з÷2{΂[*½/™ûÈUÁ)Ä–hN“¼š ®¸ðp¦9'Vï{‘õDˆˆ.Xw8s6>Y{‰¨Ã Ç·Ž×½V»Ó8áô ÛËlôÌ*Ф„ù3 Â{¤np¿Ijn­ +ø·’d°P’Xðh®Sòý¦Iz!éúD§V`ZJ)[³²H¾8°È”ÒxÇtÁ” ã:«n™úŠY+laƒQænz8ž½|0{òëÓј¥–ŽvLZÖ_xät"ÉϳëúæéÔ³Äëe[ƒ³Ð‡¿òÞ5çùàm ì('<µ|LZ8kå ïqyö´ùƒ¤ç{ž•xè÷Ò`R7sÈ…3ÕÅË™¹Ç*ñ~cß8,%‘bº¬À³e]Ëf-2[VTHP·^à~c_vÔM*Ñq‹]©¨ƒ€Õª}E1`‡õ¨?Œ]"uc+lG¡™}Øž×.z§²ìTZ^à¶16?Ãü­ž11 4ôÜ‚êÚ%?{d±Ùü¢'°.±fl%Ï@™eóÈ×Ù²ÆËpâTëä³Ë†ÙZzô%ÍË{\omãwbiì% QèâtÔqWÄ×wO©2¥Ødæ%°y«¹ «53L×ËY'“MmqÄ s/0„› VðŽò¬ª–is5ëáJ sf<8B˜™Ù¦È‡ÉOîx¡ó€;XË ›Aå8ýewF€-ƒÓ1{e¦‰¶æ<Û¥¢Ûª)M…c7”´ŽŒP¾…Wmø¡Óað„ÿ0)M+‰ +o`†‚µØvf­’TÍ × +=/#†qMK* ‘¶¢|Mmeî7ÔVH!ažV««È«Ì}¡;UUií’Ã-¾¸"<á>2ºM¿ÙKWd4§}’ùzíÊšœ™6ÄkšCiŒxªu¿ƒÿ¦uÍÚ6€ Ò½ž¢‹È&7óͯH¬m¤hR²†f—ß‘F$ð¢ÊÍ< GLr!.ìQ<ŽßX–[£r‹ìŸè‹ð$ð¾!æJ†®Úex‘¼ Ï¦MaÕeõ*aâš–‚fãÀ®cñ‰wÈ Fy´kø¦Ùh`ˆƒ†ÚÄ>‡³,;ËÚÈÜšù‰oQU²yTŠMKžº½0ñн‹}à Ÿ¯ž!‡þBó3ƒ§Ã¡Ïî}!ÊbûYΔ68Ü_§(¾8 Õ0‘dcIÆ`†6¬1Bª¯æË¢Ý8lli ‡ó”©g1g!G(MA×Üb=,¿Øÿ‰ðq‘m\ílëHòeãY啱! î溲ËFU5®Ý¨aC™€\•c¦>°ÕÜø¤¤[Å„Õlhˆ}¹ˆÇz2Ñ‹Ç@9…3|0¿À¯¹\³ÄÂæi‘ …ŸŒyrŽMÎÓ 2;ø#<[¬ñ±p×nE°ê¸<à7F7C0HN¥Ç[kü†&;ƼÊx}²lf| òu3ŒôÚÛúF/®–ÍËX†žüTýÉ«ê3n†¾9Ûø¿(%ÀÝ6¥Îwórª–¾‘Љ¹,ÿÅYHæáü˜Öà5ÇÄB#Ôûª•ã!k vp +^›6®ß†ùÃÊš=fóejiÜâë­€#×ÍOlÆ£©—½ïk „wY­l„¬3´Õj®xí9]8|CùóXx÷úv^OÖ²Ñé‡Øk‘²Uá Ò·’NGbŽP¨b][²o°~Ý·6'6Òí¥ÛçÜ#!ñ꬗„jØÙPÿGTÔ+Ÿ™ø ¢þi"ꕈÏ<ü³4Ô ç…†ó‰†óBCýÿ ¡>Ñð ÿrþåüþËç?0=4q endstream endobj 385 0 obj <>stream +H‰¤WÉ®7 ¼ÏW螬…Ú®q‚œŒ È!ðåðÀÉÿ©"¥^Æ=c;áyªnJMQ¬"õî§÷î݇÷Á}÷ý{w >õâ‚—ð[kvÿq{÷ã/ÁýñÏ-¸àrð2Ž„¿ÑýýÛí÷Û0õ-m¡O°^6kÎËaÒ¯î¯[tü‡|Â.]ÊÙ—žñ<s©îõ£~÷ãíþäìÖŸèkJ> xì^oŽÄ-Ìç´íˈ¿‘†¶-d­c l6öÔ¦Ûxþê§~¿ý|r½ûXcw1_J-'×÷Ï?êz—%_¤˜ëÙ'á‡ô¹ÚnFö«®s³˜‹Ï¹› ŸÊœnc[÷ÒuI¾IÂNCð£59»ÞŠ~¤©!µêËñÒs}j€–ÕL°øv4œÍ÷ÙÍ… „͆OÓœlc[õÚíæs +Ãêó¸óº‹žªFÇㆲe xÍMcÓÖŒìW=çS³˜‹Ï¹›=5Ã0ÃÎ'—ž'AŠÄ¡ižjŠ'×KÕð”™+báW„¸ä¶Òœ*Uœ§ð·[žˆE ÑÍ… „ÍÆžÒÐÖËsÕk·H‹¦xí¹\³3);ëȦü”œcEœõk¹9¾‚›Ù·ˆ}ã¿Aä)7óÿæfþœ›ãŽ›ùÀÍüŒ›•Ô$yášgjæ5ë‘O9ó‘œYùÕ—):ÉY•i­b L‹šý+¨‰9µw7¢RÒ55ó‰šÙX—ï©Y.¨™•vr¦f?RsLjÊ5e®{éy„z'V8P3÷q®@248ú'" õ-Ç“Š¡>75g§‘ýj¦ÌzÔêZÇ@Ølø4Ïé66»kvâDvöåNTÒ’•ÙÐ…¨¤]Tä(*u‰ +£YÝ\VgmïwA‘ƒ ?‹ôøRVs•©GŸWÂœ+M<äG|šÙ¹…i×™ŸdvýRf×'™O^kVG_«¸3õêSƒTC,£ÖâíEg»ÖV{Í(süz+~ÐE¨SZ#B]cÁµ‰uÏPó ¢V}èÅ 4”ïÜÛ¿ÞÐÉ•8DU·ö‚Žê ƒ?ö¡ª +%ÿxÖé—A”Ò®}¾T†ƒUm¨ º—¦uø1 +øŽVÄèÈPBŽ8be4jUðÝRgûã‡f s¶ ßCûŸã¯7~ -ˆÄˆ{È=Î1âRÙ}âWpçih ãÚÌi£ŸZÁúüÀ€&µrÌ5ôAKKŽÕŠ¾®ßIuñ"zVlóK £Ö ÊØmõ½\<ÀFà ßÛ V-R•ç5°1kSÐB{œšÎhEãÞí¥²Ev ‘ÏãX{+T€‡ÿ¶F¾Ä¶¡ì‡ý£¼ ¾¬-KËs¬­VXÇ—ô"‹»[z0Ñ#c$~Ö¦PÅ–xh;`¦g¦ŽA†ïý-âàx3@WÏïâ˜qy@†åFg¨öZÜ\÷­Ì ð…•Ø¹õ…WžÂý\7JJ3¥í"2»ÐÅ·¢÷©â¿ÎoÍ»öÈc#—³úK0£ñöùñ4ìdäB1ÆQ¥SªI¦‚3Ë”~šr¨7ô檀9m†^ù6 +S!§âtË`¥Å”ÃîµEjŸc¬Ù¨†w^I|+ª-E³Ÿ¬ÅÆUV^,*Í' +.’§d|n#MF£þÐäƒÒEá]gno:XÇöŽ¹Ø JaÊE–g8©,3V€ÌÄùÙT«Bób’”cYàuwbÒ))ʘ¾bl•Ìð›õ 9q(;ŒÝWuÙÄve"”mRÑ5͉@c­,ƒ«\WôœêË*=âÝÅ*¨ê$+ËÁn±Ù®O}8>Ô¶ÞÖÚ8@\“Fà £(*ÛÝS†ï #.¦Zj}¡¯µ™P¡dŽ˜v¸¬¡v–s²ÉÃþ-³>Cʬì/õŽˆqå‰z^Ý°ñ\ÐÓÕôÖWFäSÖ^&i[ØøÐÊS>òJR8öûˆ,¸ d,ÝD6Px;˜7«®"C2Í9/L¤Ð´bÚˆÌ:¾9ͺ[òø¹åÃ.±–¾I¬˜óöàev ¤ÝÑæåÞS°<ÙTÓ‚° ÄñéÉþn±Ó—Î^ìZnÿðá½ÃŸj¤¤- endstream endobj 384 0 obj <>stream +H‰¤WÛŽ,· |Ÿ¯èX](Jzͱ‘§ƒ ÈC>`8Çìü?à*Rêéžíµ,Ыê¡(Š,–Ô_þöuûòíkÜþòÃ×íCîu‹AbÆSµl¿ýtûò×Äí§ÿÞâ·Ò%Dü˜FÆÿ´ýö¯Û¿o?~ÃÔ/î(¹£_a ›ÝþÆÿÜ~¹¥pü+þÑ) Ö %êÖRˆCóvÿÙ–Ã3Ô¦ÛBR˜ ±åP÷ñý&¡§º` á÷šmTBÍ&kHðcl³á /- Þo)TUؽФä†84´š`ðŸ[†Û¶½eÄS^ +ZÇ‚ˆµl5ŒŽôqP3ga¨¡èöýÆß0x‚#äÔ#"<€TÆÖCTÄOÈ`Éj³Dâ£Îñý¿ÇQ ¹TFžÇ#±Àʈp©!–}C§ÝÝQÚ¿‹Ô‘SE5Z:F{ ûÀ"p˜¹ÿ4 +‘Ž´½aÑ–˜ &þ~[x ^–‚Š Ú;Œ-¦BXÂH'ˆÉX§5G1±TC;W`<7Š´&V¶çÀmD‘Z YáqßI © Fß×(hµ(W쩵„žùFA£ÐA`åÙÍ¿çî« œdu"[ [BºªU;’¼Yу"3Khƒ<t Àª{‹•v#g‹¶¡[ȲÄp͇Z +=cÜY¸uáÅÎb,[utK"ìwx›!$¯Ù«:‡÷¹²£" +‚{›cXªhfãûûò lOЖ +¢ÕÒODKÕz‹‰×m2® |òÀBÊ2fæA’J#:Z{î—}ÞV8`ó Hì([‡°‘ÓÃicE!ï(B²"@L XÖ®;/lyÆ'¢náUÔ™5Ò°1•Ù€ ÞxØvB°cîÍÇŒÙw@VŽµ“0EA±‚²¾ëì<šï¨ÜX‚2”Lq,kx_I˜}©(£ä:ü ·$lP^oiLƒDZeÉ|E³KEU-& )“«û¸`“Æmè}b±Fq6Ê™ 9Bõ’µ~'ƒ/ÊÌóŸkÄ›sŒU¯ pA*ÆþcµZ/xN€¤&ÖwBdP:ÙÓez†–nqµ¢õ;Œ½Åì +•!¹À¥õ^ö(èaºë¾š[?A—ׇ¯PE¸ß!,rÎVµ¾ÆV4'¶ýdMâëq5?Tk¾Éœ@晇܆æy$Éê]ØMy‡‚¾Uë€nJÓ†›3Þ@¤j}@¦Þgœ×Y‹?DÕ‰û33R¨n‹ÉóN€óª½Ðß~c fΩÞýqׄã[±jÚO°œMè+£xøî<¸ôhC 3E½÷µµ™íß¼eL Ù6ϪgÞ;æûa ¥¼¹ŸÉ9 –º‘eùqwëÓ}<Ÿ—=IæÂcÊÑIr ½E'Z´Ð»<º ý—J[FSHÚîFþ´ÐéÁ-¦ó9w·á[™Ó}ì~¯C0¿•~ +½%Z³8bnÔWàµ1çöÖ-ÕM }v³ù{Ù¦cq·áÛ<'ûؽ^‡‹a¤ìC1ƒcÔ]¬ª–i”3¥á<¨{Â;ê4Ðv7ò§EN´ðûµÈq·ñ·>=δóÍUä©ã4çá–iÇÈ«ZvªÚy"ž}CÙåÌOU“9ª—Ù¹Ÿ}ªœUBcÚ¦cq·ñ·4tez½Œzðdªj /¢íº9MlUwðyoŽ«ÞÔ?ÚšãÓÖ¤ªàJ‘ß×õW­YþïÖ,Ÿ·f9´fyÙš8‘w +’§'=d{;Sˆí´÷f9öf±öêˈ:{S­§ÏåEkŽÙšrjÍ´šÒü^r¥5\a…'>1j>“\†eÇþ­¼ Z/‹ä¹ÙWÒpòÛM›¤Fþ¼û¢q¾éòã î6q^ª9ÝÇnw-*¼œ?€¤‰>©Jv WÙÑ…ªä‡ªÈQUt©Š}åmÓ­ÍÚ(Š壕~ðôQëµ¢øM¶öQöl›.,©!8÷é|Üw“‘rÔ”òéqÿAèÃ$ÙOŸ’ªò-ùÎsªßÝ^D=âAËËt×ÿ%Ý'‚œÒ}M‘úy¢! Ebÿ æW‰Ÿ‘zœ=Ƽør>lÒé±Ú´·ö±Ë bëg™ÖÄ.§¨ÿ©üöuÿß›ó endstream endobj 383 0 obj <>stream +H‰¬WËŽä6 ¼÷WøF#QÔëšÉ"§Eähäq˜ °›ÿR$%·äv7f‘`·Ê¦(Šd•4¯?¿m¯ŸßüöÃoÛÅ;ªióŽ=á™sܾýyyýéW¿ýùÏÅo~‹•ÇÇпaûöûå˧ϘújŽ‚9ú +ëa3æ¼L“~Ûþ¾„Mþ°ÀWüˆsÀœ\"j[ŽR®Ûõ‹.‹§K%o/-ÃÌq«jk¹TÃ× »Ò€ÉEì£&ÒQ„ïgàÆØoÁnJh6¼^‚K9@XL"’]I]nËöB(#¾àrj"ر_×*ò(ƒD2 ÃìbÞÞ/ò ƒlŽ‚BÔÑO0ĺUç3zU©ƒË:¹`­ñõÏ©uTÅ$±Síc¤ÂGXÂ.³óqßÒ²¿+ªüË\§ê]K1#Œ,žF°”O6  —= Ê %¢%H6$õ×ËÀ Óm¢ö¾Üal1Dѵ°@LÆ:%òAŠÕÐ4XAªd¹ÑP ÖÂw"Íé±“ä˜ +Ýv’¾aô>FŽÑX¡ë8v ¡FWÑWØ+1Jí¸ÆŽ]ŠøNÕVÕF°žÆ%7M–(2NZ ìˆÃÒ×:vˆÌ`Ì®4édöIÀ¨{˜\#Òh‹µwY3h ëà*u ><Ú3j“ „2š¥ l·é»ŠPÊ ˜S^maÅy” Ö1F sÒöÔsñ~_œ–±Qd!¡ÍBlK›…¤Ü²´÷~‹ˆ¾H³õqÓ¼K&`KD#T4tß®ð¼Œö7 äg´°!R~‘CétÉŲÞÄ'ÃÄ$ÓÖÉŠG¡¼ÄÇ"DUÃK¨²T(P¡²ÜÉOo´ƒÛ¶ÞÀ:–è|Öƒç*í"‰J_/R^¡?ÒÿŽX#z¾}– ’ˆcÃëHjÇÂ"Ìj¨6 Pᯙ¥ÀbÒkÓ ‘ZY®²Å¦]e¢:º/´‡È†#6©- `¡U,¬Õ÷w¢C¾ºæ‘üÔ°`›¨úb«JêåÇ$âÅšLÊž@äŒ\´ýcÒb¡aðìY Rà‘B®Ò>aPÏöû°½ÃØš'“'‚JGä=*Ãyš.¢n‹™õŠ¶¦É—K̲ÛÆRc"-Zc­™õµ~RŽØz²šª‰N’ÎKl6#ëQ²g™…(«ªn³#P€ì´XÿW•™Ò´­eÆ Ú(éDHŸLï竳y±øMP­m¿HB¢HÛècÖó«‘w´Ç×±W½ìSû~W„ùíbp¶¬´Fq ð®­ƒhöŠŽ]æKÏ # rŸ2sEýÔ£ª–7¦zƒ¦è²¥þBõ¬oŠÌòÅ|½$dt«Ò\êè].5ãu?.ºƒŽú¦6uDð> Å§NfFŸ^2WÑo¡ +ÐÎXž—¨ «?hJ;’Ó§ê"RÅ·þÞ€ØîFò,ý(SŠ- ?üncomºûóT¡äÚ +y $÷Uhãz5®éõ”À¥2¡¦‡™†^¤õì½nkFöT^É[³èÎûÜÝÆk;š¡Íïyè¢Ò¢ hÇ ™KèãºmNaGrÆ bǺDSJ€-Ô¨É3[èâA,,!zàwy›út›ßÓÐE7+Éᇦâ¼6L+z+zÊiG8q9ŽÐ%ÿf«ãni&öÔÀå-ëm¥»6àwyÛút[ÇìÎG—Cºq'—N/ 7å9ðœô ËvA*!ìiÉq´‹×²æÄ[b;lìiçuÐ+˜i—ø`Ýb6öÖ mlnO#ohô\£6z¡T¾ƒ£í#m›1õÿç¨ÈKŒQûžõùšöÞ/é¬_¾7åØË?µøó”‡G)Ï=A÷[ÊÃiàkÊC|ÊøHû-ÇÁù°Ïã’ñ%áámÖ|ÏQÇ%ê¥ûÓç· ?ÿ +0´JØ› endstream endobj 382 0 obj <>stream +H‰´WËŽ7 ¼ÏWè`´"E½®Ù9‘C>`û2àÍÿ!EJ­žéÛ‡`î&‡¢J«¤}ùãÕ½|| î×ß^Ý%x¬ÉOù™stï_./¿ÿÜ—/Á+ùÀ?BC~ƒ{ÿûòùòá#}ÑD ‰¾9pRõ‰\n>‡ÜÛמGžÄ?`q· &_ +<Ø??ÿrùtùÆÁcÂຠøËý³NX‹O!’D^H¨ÛœWʾ왯2]•ï+5_kæ”>2Âi¢oÀ€/ÓÑߥ5OOM~Ñ\ׄ¾2¦1Ï°9‚Ï•¦ç¶y"úÈgɇ$yc–QàX^ìê‘wã7ø]TênŠi˜ŒMêóçìð’ªç +3^îˆn!gâX³!y‚^ŠÆ³&sôÙW4=Wˆž’DpM,vóتäžmMé~ÙÙ'»UfŒyôÌ +‡å›+Ú´egiîžY¶»o³uûoÃÔæÐ<¡ÿ½q뺀´œÐdë@¤èy'Žû¯ä2û¬ÿŠtO‘Úfr¨ÖÝÑßdý×=²B7rõõ¹1‹Zo£Z…l†& ̲ Þ.fëë6LÅד¬8¥¼‹3kFh>b‰K=‚/LÔñß0M }NµOÂ8b‹ÎüjHì ’géÔ3HŒC¸np,µ$ϬÈ%ƒDh=8¹aƈ7ÙpýÖ¼ÇÈY]ã$1Ðx+x+xÊiZ‘™8€Kñ5¶[¤†è³Ã/©öi¸aƈ·ÙpGãa·9aîmN%¶ðœ¢ë/@_¦ÅEÉqôJè{š93$vÄÈ“´Ë%Xn1´U4F½¨ßšö9·H¨Ø»œBû‚¶!hsJÓÿƒ Ü$«ty¤øœŸõŒŸy姬ò³>ò³Þñ3Ùpcæ9?YZ/3ÒæXê=?óŽŸ;zæžJ£âÌPAJR£çÊμcg1vfm¼ìiÏtEXY>à ;ëŽU©‡wìlìÄﲟ°³çlníø +#ìÌÈÄY§¾¥ò´ûœ|ÁÌvqsöMMJG1$té³78µ~ ‘eéŸaˆm¬~kÒcÐLLäb¦”ï%ûEäzc–ˆÀlï!)ø()èTXTR8Á")hF˜1›¤,Š‚§Z¯LKjw]2%© ÄiÝ +tâGg†ÅªLìNüØ5dÕ“C9‰‹œÄS%ÄÄJŤ$LõNNò“ã~Jx—„MNê*'Ñés''&Ky''Ñä„9¡§r´DåeÌð\NN{Z{úÑã>÷e9îË8ôO„0ò¿ü¶”ñZ¥ ÊzÜÇ'‚2’èÀœTlÇü÷{.w ñŽ—°ò`ǡ£þ€—ÏŽzØñ’~’—CP¸Øµ&8ÖÜé žàŽg¸ïô$­z"B>×”nÊÖ<É.<–ëšõŒjó [ô|®qzn›'ʉ@ÝÃYäâóè‘㩦uÔ£‡w-ñbW¼¹Y€ ü. *u7É%BMÆVõšh?gUËÊWY‘ÇÚ-äLk6$OÐKÑxÖdŽ>; þë`ž+DOÒìÀ5‘[¦Í—…¦—>ólkJ÷ËÎ>ÉØ­2cÌ£gV8,ß\ ¡Ù²³4wÏ,ÛÝ·‹Ùºý·ajshž°tà‡¯Ž_ÿ 0 â( endstream endobj 32 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.001 192.001]/Type/Page>> endobj 33 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.002 192.001]/Type/Page>> endobj 34 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.002 192.0]/Type/Page>> endobj 35 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.002 192.001]/Type/Page>> endobj 36 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.002 192.0]/Type/Page>> endobj 391 0 obj <>stream +H‰lVÍn7 ¼ïSè,‹úçµiÑSP=ô>´éÁôýÎÒîÚ øãh%Š"g(=ÿñ)<þ”Â/¿~ +GŠy¶bMÿ{/áÇ×ãù÷?Søúß‘B +eÖ˜ðQ4ãW¿Žß>cé³;wô³1çœÿD€É…o‡þÁñwüÐ)`×Ø¥ 2¢d,x¼Ú~¯ÇS±ûöŒ³#ÀðT4jRø©Ö æX +çÀú}¹FªDѶ¯§šc®ö>Ãb)õy¹Fr‹sŽägË?-ÍûªŸŒÔ8Ó¸h,MìC×c™”:@\=NûBÈб~`ã\CA.û«ÏOÈwæn „r)IÝf8PÜ%µ qV…³¢sÙœW›.”Œˆª1ò¹²þïz ž;¦™&ôÇ;„VÕrPM)ˆJësD­Æ|ý€EbcÚpà2ê +Ü£Hip6Z‚Í¥æ±smŒ³àduƒÇáÞTL,7‡Ôûó)VFÛ¯ò¥d6,NC¸†Ô&p:_Š¦Ó¢®î_nkÞ8»í²w¾z«“÷•ù{Þ-6ûSA!ÜœÁÏv<_ç }á>Šé¢{þÛׂ¾ÏÛ®èœ×ödÿrü/Àf°EŸ endstream endobj 390 0 obj <>stream +H‰¬WÍÎ#7¼û)úF#ê—¼î$Èi°Xä0v“Ã7 $ûþÀI©[¶Ûþ¾I¶J¦$6UURþç—íó×/qûÇ_¶K ‰ëC‰ ß­åí_/Ÿú9n¿þï·¸e.!âO’„_Úþø÷å?—¿bègŸˆ|¢ß=cæ˜OË _¶ÿ^hÓø?:9ÙÌ óS”bÛ®ßlÝo—O¹‡Bí7´9ô,hÊ85Ì"÷&ÄÖízÙ;ì·ÛØÑS(P-ÛœëSI!)ôe¼bõÑ|»Pȉ'ôæ,U®—ýçmBOÒ§‰ö¹¢|ÿZ P +ÀíIjï{¸ÕQ‰ls—r@Ô1§Që°_°M]ú6çòç¢m®31æ@.¹=oGOª9kOJ+õ„Šõ–Q'=%pìk„lƒÁ-–E;Iÿ3€¼Z`jó?Ì@IAÕ]-!%QÔ²=ÂÀ¤u±ìzh±ŒŽb ƒü¶¢÷(îMs¨’\Cñå÷žýqJ(·O\Cêy-Êóس7.m‚ yǾ©sã«œ3ì?o:/|žsÎ¥ÈAºÔQÞVÊ!3ÌA2p·é(å-£šu‚ë¥adB”<é‚œò6ª¢D¾8ê¡c[ ôA.“eao^ñÀRÅA4J §ª™ó¨úoì‡f’!¦„$á)½LÓ2Ý™R’Dr÷ Åø¯b”Lª– ›1ß ;ˆMŠ]£á«>†•KUÀØìŒÇ*\/>û€‚À¬O@£Y°†Ú„#]‡ìJ›ró;–¼BY„aÒ²x…F6a&›*©PH#X(ÆVÚÛwž8îÉ…‹ÞgjÅíkÍ»“Ù‡ý@"L;‚¥ +ÏÄõÒ~}¡1 ±3F¿»çÍvUÈTç4âã“ØhoŽïÓ¼ñ–ñ€¸»T½ÉÝäͶ©Í£W ÎuæÝ{‰÷®’×?ÊÍ‘Ÿ[©ë‘_[ÉÏm%ÿ%[æŽg‚8ß1|·•~c+ýáÀ¯«­Ô‡ÿÎUÚê*íÎUø8ð÷cÿ‰– šá¨eˆåÔTò©d÷‹½ÜóÀÏ'~>3•¶šJ{qà¿6•qà …XRy)Ly"̼ +óÝóþT™ôÝҎ¸”Ç.çy§CI熒þœ¡àå.½6•gW,¼¶)ùo´=óù ë´âô‘’ÓMî7%Ïï™!½c†ÆË;Ýç}[qyRñ¼Vüû˜òþÍð¹§DèyÚùEÚé#åNßYî¥Ú¯Š ÿŽg>§÷«¬?\ì?G‘ÇRÿøõˆŸÿ 0tM– endstream endobj 389 0 obj <>stream +H‰ÄWɪG¼ÏWôL«²*k»Z> c|ð ¶|Ð3HƒÿÞ™Y==ó„‘À`ÌËè®%—È¥ßüøv{óþmÚ¾ûþívI{uK»¦ŒßÖÊöùÃåÍ?§íß—´¥­ Ý^ÊÌø/Ûç_/¿]Þ½ÇÖ7~øAŸ°kŽõW,þeûã"ÿpð'ü㡲å4öÔªn8¶fü¿½Øuøݵ´íšö¢s“]J!hGì Ü.eoÚÄ¥RhÖ3¬™XæH÷6ˆ-sluŸ­ã}ªK¾]pÑh Áà›ªÕ]$cÅï—¼ üs•}`Ñ ¶äÚÄ5ÑDR©$D݇ÔíãÏ´µW¸í³OB„"÷3”T%5@¼œ\ÛZ…r[Ç ´2·%ÓVí¨îÚ;~ÓÔá‘JKá‚Ôá—Ú— öÝæŸÎ“¹÷Zæ6(E€á éç1äØ>{&ª‡å½'‹ÙLò|ì$óíâÑí{™´f++AËäØL¬CÚæEÆåÛr¨C(áξW™  àh÷øVœºÅ&ÔÇf9Òß\Šé*Ùi̲̓ˌtÚŽl´&Bâ6zšÑI‘$OÀö ˆ5êÒË‚Fþ9£ù¤j},K_ð#Ûš°‡ §*vË‚YGB —oÞZ@6[áŽÓXS.Ã;Aê ݦÊñ ëŽ"J¶Ãçp–Åi±cY² þ-fí‡ÁFIùÉCêN r”åÆÆ&άÒ9MLŒ#Ynô7¹çƒ½ñµkóQ~˜x.[|”èHÎŒZíÖ“Z½ÖÂu&\‰AÄ>4ªê;Ô˜à¹W×€´üdk ÎÒu–ËëG®ïX$F¤u^¡!6vö<ÕÖw{ÃBåÒá.‡á®¨Ç–pÛéÝ8yX'÷ã‹ÍX~? Õ–b:$Ó}¦M^—toÍ6ßØø³D76¶éRžýóªdâ†Oƒo"•þ©æ™TO¬úïhõ¿óJ¿‰Wúµ¼šg^=ë‰YíĬ~"VàU?Óª=Ðêëy¥_Ç«wïßnø÷¥8Mù endstream endobj 388 0 obj <>stream +H‰ÌWËŽ$·¼÷WÔ —Ïdòê•àÓÂtð4lù0+@2 @¯ˆÌduõô^†1@£ŠLæ#òQŸþñùøôås>þöÝçã–SÕqäÔsůH;~ýéöéï?æã§ÿÞò‘¦=e¼,«â9~ý×íß·ï¿àè'T\Ð/ؽ÷ì3o—Cÿ<~¾•ƒ¸àü£pÀÑRž¹³¥±–÷¯v-~“J?Þ Ô*GMµ ä¥ÖõhIgÙà~›©Ö¾áLk%Õ¹|9`ß²M+ÕÅ-­Î£”¤µPtê÷[Mm” sZ‚wø·*•Í©ààýöŸ[‡T¨Ó ›BÕš:. ½ÛñVÓš<„EUJÆr¥ÑÖñ~ãKXüØU…áèë × EàË5s:ùºæÏôÑÒS™´¦¯¶Áýæ„ú:ÿåÕ6‚{pú~ ˆ›JSø)Ó¼'[ïþ×ðM÷e:è¡ð!«MJ„@œ^Ùâ—ùlÀË“h.úóÄ=MDkªR^ÀÖJ4@¹ !Fá2{΂[*½/™ûÈUÁ)Ä–hN“¼š ®¸ðp¦9'Vï{‘õDˆˆ.Xw8s6>Y{‰¨Ã Ç·Ž×½V»Ó8áô ÛËlôÌ*Ф„ù3 Â{¤np¿Ijn­ +ø·’d°P’Xðh®Sòý¦Iz!éú˜%ðl/º¢Xt¥ô>öú¾Ýqäè’µTD †Qˆ½<ÛIp¹2;'k.T—eÚäI Š™ÂZ5”2Ò8*P¡rä—T‹"F äîý’ºož~Œ‚³ª“ÑN6F¿Ž ½¤.í|‹ìfÉ…dƒ–¦ÛïàK«?°J^!ýn,‹Ýp…èѼ^)Ë8ì”ùÀç~Ðc^N{8/‹ÍÏЋíCtjF 7 –¢³5«‹ä‹‹\A-wL¬—ïÚYvËÔo”ÌZa ;Œ2ÏpÓÃñlæƒÙØ“_·˜ŽÄ,µt´ë`Ò²Ã#o¤I~®˜]×7O§ž%^/Ûœ•68ü•÷®9Ïo{~aG9á©åc¼ÐzÄY+y—ˆË³§Í$=ßó¬ÄC¿—Ûº¹s)ª.^ÎÌ=V‰÷ÿxÀa)‰¼£Ðež-k›X6ë‘Ù²¢B‚²Î 4û÷³£nR‰ˆ[ìJE¬Vî+ª99¬Gý `ì©;*ÍìsÀþ¼vÑ;•e«Òò·±ùæo5‰ ¡¡éT×.ùÙƒ ‹ (à=u‰5c+yBÈ,3˜G¾Î–0^†§Z+Ÿ]6ÌÖÓ£1i^Þäzk¿KcmŒB‡ã¸+â뻧T ™Rl4sØÀÕÜ‹†ÕšÆë嬓ɦÆæ¸,̽Àn‚XÁ;ʳªZ¦ÍÕ¬‰+‚Ιñà aff#&?¹ã…Îî`ëa/¨§± èΰcp:f«Ì´ÐÖœg»ô@ôZµ1¥©Ñ p솎Öé·pª ?ô9Œžp&¥iQá ÌP06»Î¬uÃásëÊ +=-)€q=K*붢|Mmeî7ÔVÈ ašV+«H«Ì}¡;UUií’Ã-¾¸"<á>2ºM¿ÙKW$4§}rézmÊ’œ™5œ~ÄKzCiŒwªu¿ƒÿ¦5ÍÚ6€ Ò½œ¢‰X1W×ÅÚ®H¬m hR²‚f”–ß‘D¤ï¢ÆÍ,‹ èFLj!,ìP;ŽßX”[£ý¹EîOtE8 xßó %ó·Êp"iA—M›12¢ªËÊ ª+T¼5-7­Æ]ÇÒï òh#ÖpM³ÁÀÇ µ}'Yv"–µ‘y5ó 'Þ¢¦ds¨›•]=CÎü„üåWø%§CŸÝû“ÅæcÓ(‹¡J-[|½päºù‰Mytô²÷} „ð.+UCð>ò…Z+^zN×ßÆÆPþ<Þ½¾ÕëÉZ6:ý{"R¶*œBúVÒÁéhCÌ +µC,kKö Ö­ûÖæÄFº½t»ãœ{$$^õ’P Ózæò? â3DÔ?MD½ñ™‡–†z¡á¼Ðp>Ñp^h¨ÿ4üÀ¿œ„9¿ÿòùÀ¿?¢s endstream endobj 387 0 obj <>stream +H‰¤WËŽä6 ¼÷WøF#‰ÔëšÉ"§Aäd7‡™»ù ER²¥^wÏl‚Ü*›’iŠU¤}ÚŸŸüöÓÏOÛÅ;ŸÚæ]H¸Ä·o_.¿üî·/ÿ\üæ7ªì¼[hQÿ¿ýyù|ùôŒ™¶N°u¾ÂzØŒ9Ó¤?¶¿/X?¼à+þdñ°E"WbŽ[ðRÞ^Þô½o—ï2Ñ6þ‚Ëpt èb‹0{¹`Ø¢8ßï Û:ŒäÄÐV ëð»Ýµé6îW}ÕçËo‹ëՑ綟\J9-®7ýS×+(ºÄÉ\'Y^¤÷ ˆíndWu]V0‹¾xŸ»ÛÈ]îÓml랺ÎÑÕš`î½k¥ðêzIú’¢~øX&T‡ã©HÌõ®±Ìf]iͶFfËsÚúÂün#wcŸlc[õÜíâ8óÖ²£våteÝT 4v3„2|wmeëƺ­ÙU—»fÑïsw»k†¾G]îœ:Ùå–4É‘ìañl}a~·±»bhëQ_õÜëæbᬠž+¥snFåfÞÁûÔl35c§fþ(3Û˜‰ÍJð%øà‚g¾ËLúß̤ï™Ù®˜I3é3!*L‘$Ç“¬¹“bæ ›vjÒLMRvÕa$íÔÌÊÆ8V1à»ÅAÌúb‚!´­×8ÅsfÒÂL2ÒÑ53Ó 3IYÇ+3ëÌÌÖ™É'Ìä¾î©ç©­™jRmkýá¦ÁÑ¿€Dœ@©4r<ªê}As¶ÙU3¥§|Éc~·‘»Ô§ÛØìÎÙ -alŽ°³†ÀW¢‡¨,ðŽND%¢Â³¨ä!*ͼõeuÖþüžå–Ëì*.ÊË(Y~*(lžîH4`¶ÊÂPS±k±¯ª"4K +½[ìo¹Þ{4IàeN1^) +ß©õùP”6+J›%ovíŠÂ]Dúâ³¢ä®(qR”xWQ%|’^¬P¸R”xSQ¦Rg=Õº—ú+5é +§B?Ô„§2ÏCSnh`Jð¸¢Ü·ež1aÓ >Ä$o»ÈÛaó£UžlöQäoºyÄ™Uø:†…Žé¿Ðq­ñ£K8èX˜Ž]AjE—ë¹€ÄE@â©ÇtÃã2 Hjí^ÏÃîÜó¡†j—VdYòåÊs^b—X×Ýsé‹vßå„sôTuw>âÍ'ñŽ}jôm¯{‚˜ÓñIºÿrIt’Ì1ˆÀÈ3$‘´$§çì26 FEœ-ÈŸ,°æ´õ%*Úкi¸úXyM¥<ÒîSÞÉÅ„‚µ… ¤Ì3º%=L‘}üÒßm¨âc´Ñ@Û©Lꯀ×ï·§ + ƒc«â’jLr¾‘,SöiʱK^ØÛÌUqJ½Ò­%Ó¦Ÿ 2áV0 J€`À²Jiá\ûk‘ Cˆ{“UJRiIšýBZ|¸ªÊƒE¥¸(z‹¤ÇÒ}— €Î¨ë‚ /3&BèËA§^â`nû3ÉÅ* %I¹ ÅN*ËŒàƒdâ€| ÒQ«@˹+*ÇDwx€—ÜŽ…NQŸ¦„[!3Œ£ëFÊĦì0T—ÕeÓÚ‘‰~D¨•1fHEÕ4ka‘Ýòç…gcé—¥X‡«S•WÕ‰V•½aÉÎNµ8>ä2že9êHâ„6â5%Š¬ª]¨ð”ˆ³©–Z_c$r.&T(—€-Äkh…íeŸ,òÀÓ»Ìz…"³|<Ô"ÆYvÔɹ NiÝ]Mo}dTA>‘¶2Q+hõíD+£÷À(çAýÜí#²µ“1UÞA’³A?VU!SŸó ‰ä‹L ³æ'ˬ«%ç× ‰µô}b¢ý†è¦´/ ¤=ÐîeÇh)¤<ÙTÓ¿ Ä|w±¿ZlyÓêÅá åö§ç§ ÿ +0!Ç£ endstream endobj 27 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.001 192.001]/Type/Page>> endobj 28 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.001 192.0]/Type/Page>> endobj 29 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.001 192.001]/Type/Page>> endobj 30 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.001 192.001]/Type/Page>> endobj 31 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.001 192.0]/Type/Page>> endobj 396 0 obj <>stream +H‰¤WËŽ7 ¼ÏWô¬V)JºfcädAù€A;€ÿR$%M÷lï¬`^UEQd±¤~þùe{þø·~|Ù.1ÄÒ·RÁ#·¼}ýóòüÓ¯qûóŸKÜâFCŒiK=Ûÿ¯¿_þ¸|øˆ™Ïî'¹Ÿ/°†Í²Rã߶¿/˜?8þ‚êPJ(•êVSˆ]òvýlËá‰÷²=ÅÀ³À½)h½o9”5¾^8´$–@¿•l# +%˜èXB‚·²U<ᥦîÃë%…"â a5¡\‡„Z þºd¸­ÛSF<ᥠÈ×€ˆµ¶:uP²ÎÂPÉö颿ap{È©*Œˆpõ­…( ˆŸÁZÌHƒØ,æŠø{/c|½À/ÂqTC¦¢‘ç>ÆHD$X9Â.%DZ:ìîŠÒþ²/RCqR„ç¤÷z+öEà0ëþsÈŠ¤£ðX´jý-ñ×ËÄÝê…m¢ðpy±ÅD +)ôt€˜Œuju“–ªKÓ´Fž …je+ÛýNÀʵ„,ð¸vR~ÃèÓF+Lʃ=¡RhYßâc:0¬!»™ð{n¾ªÑÀ¡¬Np«›SÚ®bÕÀŽ8O ¬ÎB"3s¨]yÌQ̺×XÔ®çlÑVt‹²,i¸æC,…ž±®Z¸eâÉN2–M„:º¥"ì·ubAòª½*cx+;ªšc]±Õ1‰¹l|z]ž.èmp<+ÔGˆ^i`eî#ñàHT• =¶«m^'ÿhï38ì([ƒh§1†Óªq„´£Éj-^Y·6Ý8iÇ?¤4‹® ÊZ!™û͘Îèm‡xj;ó±Æ†Ô;P +KS¶'dÕôâö`Í}GsE·• ‹”Ui¯3¥kE•“Ny‚Ý-V”ÆûÓ VV¥½ SÉR¤Š*‡„Vq±º eÄV z¢=D•¬ôñ•æäØB‡Îƒ +]u~1Á×Ô´ë?ׇ'ç—–¼L€¸™èëÇb…Yð9MZÜ‘@nJŠ Ó=´l³+•Z¿ÂØZÌ®Nr Lµíð´GeXvÓ]Öjn}]Zo¾BaÖývÖçlEksl5sVÛOÖ!¾ž®æjÉ'™KÉM¥¾WtÎ-ñHnoÁfªÛýTà[5ú7S™ÚÖ:ã <*6ÑJ”ÝûýŒ£³ý:sñ› :o?kFH•mYï¨6Îr(öD+¾£éå˜ê­— ì߮ۅÚ0 è+£¸øê,í”ÕÍ*è­¨-dd¶ã†1ô§gÛ¼V=ëc¼W ¶mé39ÄR×3O?â²ñ·>ÝÇãyÚ’Út U†H¥Ã5‹Î³h‘7¾!4Ú/N…¶Œ& +ÔvùÓ"Wn1œ¹ËFßò˜îc÷{9r­ç ×»(ÇЫ1¢V‹#æºCm^ª¦ÜÞ:PKqŸ]EÇlý¶áØA\6ú6É>v¯çawHWSé` +4xvc«ª¥åLi!ee¼u=.ÍØÚ.#ZèêA-ünÍ<@\6þ֧Ǒw}szj8É+(¦4ïz¤íC/bù)bç {þ e»”šk†Š˜Î©|™è³ ™³ZHLÛpì .«†î†×Ó°»o¸¶(lj¥žw§©­ÈÚMô°9û4Ú7§|koöw{Se¥êiª ¯RåasÒÿnNz¿9iלô°9‘ê„‚(ËÓ ÖÙI£7e‡´¡VwÒ¾;ɬM#íÑÑb ™§qXÜz³}CoBþq$áZ ¾á*rÚšthMò®£ûÖ,ûÖLÓHŸü 5ûhM>´fšMi~O©Rq“c½g|^”|ä8wKŽý›wÔ¶8ž«}!uç¾]³•³ÃÈŸWÿ:´‹t•éÇA\6qܨÕÐÇnw.*71oNl@î4%»°k +/t¢)ù¦)¼×™šbxÛpk³Öï7=áž¼±]ÿýài½”s=ñ‹li Ý +oKhºÝž×iŸŽ§}3¡½¢Ð»§ý¡C +s+bmI©ð¢ðƒã^nŠÒ÷ŠÒ÷Š"›?‡¢ðæÃù˜»l\gnŠ’(ŠŠa½’£9+ }³¤ìŽ{o~Þ8÷wrB{9¡ƒœðî¨ç‡rJ³… Žë‚§zÂ=a— +¾é‰lKg¨í´99éë^NêÝIO>ûvп‘ëuÐG|&A[Λ’=}WSú×MÙ¾»)‡Œ Õ"o¨H>¨H> ˜Þ¸îU¤ô~ Y>ÿbZvç¿‘k(7Žv|6”†ú]ä|HµRÝVäz7Z±Ç´¿Wµcºå–n>IwSýæö êÉšæ»ü—|rÈ÷9EÊû™†‚pC†Ïc~”éþ©ûñ¤‘}Ì“0ÇÓ&íø‘’)¶7˜M˜-ïeZ0›Q«?||Ùðï_K¥› endstream endobj 395 0 obj <>stream +H‰¬WËŽ7 ¼ÏWèV+Q¢×lŒœŒ È!0HœÃ:€ÿR$¥uoÏx  t«z(Š"Y%íó¯/îùãKp?ýüâ.Áî.øÈxP#÷õÓåù—߃ûôï%¸àRË>„èb'}ýóò×åÃGÌ|6?Ñü|õ´™sž–I¸.ð‚?,ð/qXØ—R\‰ž¸4wý¬«âé¹÷|.°ò¹7­wGž[œàzɾ!öÙ'l£1é(y&†‰Œ‹pãÆ®â 75v^/Ñ3‚P|Éb’¨"â+Gü}!¸­î‰PA|Ñäm@Ë Ûõ 2`’YŸŠ{½Èo`÷"õˆ~15×|(0øÔ ¨B&ŠÎ˹b½ÏñõÏܪžKìÔÆ© V†°@€ËâCÚ¶´ÛßEþm-S‹>ÂAAEÍ2a#ÔÅIÈ—•Ž +aÍ*M ™¿^&î(˜î’S}ƒ±Ã˜&ßãb2Ö©ÅPˆR«ŽžÁ +R$K†’jÍZ·ãF²9;´]¦J·°ÇO½Î‘Ïh«‰ÐsH[öŒH“oè*l•æ…ö¹¥[Jøš-ªm`!mc–ÔtY¢Ê˜µØPŽHWSI"1g_»ôq,`V½ +d߉4ÚjMÇÃEÑZ¾¸JÙb(ÏæLÚb¡Šf)Ûíú­!F¤ý +áÕ6P¡(QlsŒάÍ'häâõmm:©( +£Ëbê».‹¬Ì²´vKˆ¾J‹µå®y—LÀ2Õ„>hhç±]ayÍo@¨ŸÑÁ†HÙ!4Žc §Ò%CÈzŸÎ %…Ü *vž„ð_j£ÊR¡2AƒØåÁÀˆ|#´("ØîFÿêX¢Cò Xb\š´KB»H¥¯)¯éE¬ =ß¡ÈÆ"” ‘HcšÃëLêÀB"Ìê¨6MÐ௛¥ÀjÂkÓ ZÙÜd‹]»Ê$uv_$iÆê6Nؤv¶€ˆ€…V©f­~x#9:ÚZà ö…ªO¶ª¤^^¦OÖdRvž‘gä¢o?² ƒçÈj”ˆæ&íSæl¿NÛ7[ dêD8!7<íQ™\–é" +|[̬P¤•_ž¡bØmÏRc"-Z›c­™õµþ¤±õd5;R™N’"”‘QÚ‚´'aÈ–v¤ $lªºÝN@²Õjhª3µk_ËŒ'ôëDH£,ß×{gë:sñ›¢Zß~–Œ$ѶÙÈYÏ”¬¥¼¡-¾ƒ +æ˜jä›$¬_wög»•öQÜ´¾–»•  a…0ï®A’õÔpZ£@¢õ"ô”‘¹cHñsCUÕ{F4-zâvƒ¦ó²ÏñAßz€Î/ŒÒgùÅ|=1Òìæ*†4Á:z•‹Îü<‘á` ±€iP›¼Nhñ©“•çgWAÈ"QEÂ0§¶c|MÚºúBGoHΡ¦Ž¥œ=¹ñÝ€ØnFò¬ãPSEJ=N?Âfc_mºÇóT«°M\®­É5òf¤Ó[*SuA]O5¼J ÚwÃÖŒì©ü’¯f1œ¹›Mж4C›ßóÈq¡(ríBr„vîBŸ·nã2Ç Éa[-t¹Pé]©!¶¥a$Ïb¡‹» çÂf#_yL·±ù= =ÊEBN00-ç²ï—^5ô^5t.¼!½9ÍÐ%ÿf«ãai&öÔÀåkÖkËpm l6òµé¶ŽÙ[‘ã¨k£×Žóxa=ÌŠÝ”jŒBZJší´¬…³@l§=íàŽz3 ߬[Ìƾš¡ÍíiäÔ— ¬4z%®ßAÑþŠvgDýÿ)*ê’«|Qþã ô£íGËÊQ#Y;åh{ËÑvà(éƒ8*§½ü/„Fǵ±8ZvÝQ´Ü(jTªn1¥idD]Zv ­ƒ¡eÌÜT·÷ÄÚ"wNˆ µ~Îжch3úÑ¡}ahž&ßb(24‡ãyÖ, -˜. 퇓ˆµ¨ò,ù¬¹eCèO-+#ˆéfdO»£u=•òð¢Ã°È7smlNÏUEn|µ+7[=(yaš ·ù¾¡SU¡o© +,ªBœ©Ê"*tWño+3[8tÉ&*l¢’6t•¨äOn€akR±;÷“êȪ)§’’IIwÕYÖ/k¦£¤”Çþ–q•…›¤´UR’³çNR†4•¤¤!)y‘”ü@RD !&af©õ±¤Ü=öózìç÷ûåôد˱_çáG K“c# §"4¥»ùZ4¥ŸkÊz꧚2ØÄ¢´1yœöï8ó¡‰é?°3®ìŒñÙ¹?óã±Se +†¼“¸9•ÚÉ +Ý <Ý <ïe…WY‘ƒ+ÚìNƒ¿ÃPè8#ÛíN-cÏ»¤Ó.é|‹½®I¯nìÓŒvY§5ø<ƒßeÆt«ËcQÔ~ÑÈ߈y\³ãû³>#ߧ}ô ŸõË÷¦GùOÖœòx/åe$èmà–òxø>åq¾d|¦ýŽ˜SQE<íó´Ëø.áñm÷ù^£N»¨ß•î_^ÿ 0Û¶Ú+ endstream endobj 394 0 obj <>stream +H‰´WËŽ7 ¼÷Wè`´E½®Ù9-#‡|À öemÀÎÿ)Š’F=Û=^È.Ðjr(ªD±J3O<›§—gg~ýíÙlκX³>âA…Ì÷ÏÛÓï:óùŸÍgBaëœ7¾R¿ÿ½}Ú>¼`æ“æñšøÇäod"ÌmMTMª6ÉÜë—–òËvI¶oÈYæl^§],ûÛÙj¸ZN†O6Ôb˜mô³r&n`ùnß°æ?6sYvó—ùºÉÎeÿìÏ„e÷)Ù&NÑx"ĸ²ì„“­™Úr.6y¿|) ëØàâÍ$[Qçë6mlžè-WùDs]"Ù"ˆû:ÃFò6žž×›' ‘›YÒ¡‡Øìy™õÖã‹Í®+ÆŒÃÀ˜*67‡8L`ÃDòóãdŽN”xQìf2!¶Û8Fö­«Æîh«{ìÐóô\|°%5©~±«%t +rÏmOñ~ÛÉF™{«Ì˜óÖ3+ì–wTÂÕiËÉò<½nõÓ½nÝÖã¦6‡æqíÿ +n}<¢q@WÒ9åÙLÜ;0Kÿ`ð{šGQz6G¹w`óȘÌÈÕvhÆ*j]G%´©š|$èV_àºu[‡×a*¾–d­Á‘,-åÀ1úTƒñ®Ú@9,å€:@Æàm¥8-²)–¶`Ìï~5$vÉ3·Vj$Â>stream +H‰lWIn$7¼×+x6ГLnWˆOCðÁhxƇÖ3þ?àˆLÖ¢º,V2·RO>‡§/Ï)üúÛsØRLu†¥â'~~Ûžþø+…oÿm)¤P†Æ”$ÈÌöüùÏöuûý ¾|r;âv°øøü°æ˜{©¡k=•p5“¯Û­ÆšZK“ðØqα—œbÏ¡ÌXû’%K(56á[|Ö4d2 !ëý4Öb£¯¿l/Ûì ç@nˆâïð}cÄŒ[SÆokå]mÆ–µ‘°èŒ tlÓl›[ˆO1¾Áç){ĤzÂKAøÛ1±žsF%f¤`·uS䮆}›až”*ûÄãœÈ5ŽÁtÝÆQûg3‚”J¾~õÉ Š%׉‰G±ùDG‘æa³è˜àVÃGçKšP¢Z+QΓ¨yf¥q§C·‰n[£0¶§Ï÷Æ%ÖÙ.¸FÕliÙgŽx4jÍ׉ŠfÔkVöo>ÎìÉMçåc‡t+5[hUò¾-ìEìÐ[ÂÍ$û»ƒI/Ÿg´˜ +ÈrC Ž¶3b܈ö QÌ®d 9m;¸ƒéxÕØÃXZ]c¤nsQ];ÞHªaÆ¡ÆÊk|Gä +á(Å‘z`íĈZð¿êB_JLP׌,;±¦¥CÁÒD¯fbÏ`ØãToШ2?`‘X­t)”îæQ¬dúþÚZ²;k% Ÿ‘éî›[_pbaai®±b.sÄM¤!9UÛÉ›(½zgÑrª(435 $•S+Âe‹˜­\•h4ò<*‚†“Ã1ú¬™@€Y£|œ@ Ò¼ÏÇlWØÀl¥‘¦*f*D´yvi¢3 K· +¾5f:¦eåL‹:Lõ|r@¦[PiT@c‡ ÔQÖÍ÷7¨]¦/£ÊâÂÑìrQKÍÌTçÁö¡ºWé÷ Ïc‡ ôC”5Ήº&* +Ž•Ê¬È»K• Z>­ÙÆ”°L d—æ™­¨*øîÀ{“"€šFKuÚ4²CG “F ÈÇ÷µ¹#H=ú1Õ¹Æhå’Ía¢•ÇÇòàð Ê¡ÉåM¯©ŸD–úÕr +â0çæPj²ro\›ÍEÌv,èmR~8êÔ>ÚB8¨'Ž`£´atL?"Õ\Û+•%A­Œ´–†Òí$„gze;ÝÐý$#«äc{äÄàXKî!\žQ-x û˜î5$Ö;qpÜ>ܨ–xP!Øø¦ÍPcÞœ‚Ón¦8î+­ ’HEˆTN„ìöì@<‘›ü;pi“lÿÁ» Ý Äî-(ÕÚ3>Æ Ù²õ7ØÛ˜f–ézr\úAq`vvCÅvg3ø–x g7JD'P?f;ºót,£oÁçÌî™ùÈi™~Ô +ÕÄÔ½ÊÄ¿ÇÌ·¸^©—ì-Æ¡ï^Ë Ï.¼¯çu\>§8ÔËvkù;L‘½ZG´lžf\yTf».:°÷ó1 ¥vØf^íFó‰P*Ä¡Sõqlˆ^oˆÈð¬NEÛNu¬1n-Þ*†pM[À异Lgð‘R×7—oÞ»ì²ï| +«7î+Ó!Ú Ô"QÄ&H{¢Ã½…q àù§Îÿt¨Âuö¸uԼƻ)G¾Í[NIí_‹—í…ÄfU endstream endobj 392 0 obj <>stream +H‰¬WËŽ$¹ ¼×WäÙ@kDêE]=^ø40 üïúÐc`×ÿ8HJ™Êê¬îž]£€L…Š’(’R~ùÛ×íË·¯qûó_¾n·bé[ Tð`áí·_n_þú¸ýòß[Üâ–$‡i£Îöþí_·Ÿo?}ÃÈ/>ù<¿ÂzÚÌ1/Ë nÿ¹aü°À¯xéä>máž6¢ˆcÝîßmÝï·—ÔB¦„ö+ÚZ‚ŸÛKêA¸bî¥a[¶ûmï°w³±£'v™·9×KæÀ +}™ïX}4_oË„¾Âœe ±Êý6°¿^'t'}šh¿;Â÷÷5ÜI¢­å ’Û²ÿJÌÕJÕ}8f†÷€14Þ°|©ú'¥€n©„u}Œª©lœ1¹a$ÃöÉj(ȵ†öç?ÁM½@Žè µ¦‡,eسГ,•vdI*,õèÈù€Râ‘%ëï×£GãÛy›sYðSÝæ:cø’rÙ{^.ˆ¥v X’ûE!pñ4æ¢qßfOGô¬?jÁ –­k'!mÁ­„êþ/æ VTŠ:X J“£¦’‡ 5¤/Žd+#3eïPØ*¦CºK§—S³ Ìž};9äBkG ŒÒ9b2‡¼éØ—6¡TÚŽÕ/Ú“6ÐH¥’ÂðÜÞ€^>ÏJŠ yàˆè2È+xײԕÿKDMTL% +à§-!¦e‚; +=î!ì¬ ÇZF±Ñb¾9j¡!“5úÀ낹RoÞ±ñ¥4Q M*JkAìÿ¾!+êHë>*çò„ U·Xä¦Û†KÊhrÓBGMjuB5úLò)Œ(©¼BRJCÁ[7AÈf«‚‹•Q¯»h9Op¿ùìv&ÝfÆZHŽt 託ëÜÈi“r†zÆ\‚8 d¤/:A¡’M˜È¦b•b²f¥EÖaÍyá5V­"Á)Io;°M5.½®°‚ X*D4P-þêÇÇœiLž¿‡½$üÚh Š{©Èa7Å­PHv%†b¬Cƒjb³’F Çó@È«7RhÁ+à +œ²…¦£2§n*’ªV¥èáêà~+~H9¬¡þ+¡wd5¢õØ(º= ¨RÔVÆàWT+Ôä˜@[¼iv¨ž`Tšx(œ/´#P1›ò9iš‘d¯à”·ï¶²·¡ð¤„•ÒG5lçƒ#‹ÄëÛ¼dø­ŒP• ‹k‘!fÑÊKt6«µ ¾h¨mu Xi„Ü(֫˘­—PÓJt™¥ï@Ùߤ„¸Û  ÕÑƤÒMð!÷6²U“«ÁUlÛUZ# ‹ù—»rPÓãmpZX'ÓnådLÏ¥ºyíz[ݫȸ#/@´+2 €*$^z·ñì*ˆL!ø@EÔÝÜ t»˜Ò8¸¸¨JzqA¶„ð6ö"PÜSaŒÓ Ž²Q˪º9¢Ê:ëŠUn—ÑƹXÙJ[ˆ[•¥š¨ëKẆóQr^U'Ú}/ÄíEõ¡)Ð"ÓÌ'™è®Çb’ºÿ .³VPgšqMã„WH*%&ĨV þ#¶˜¿îæG=« ÷¶Àa­×s9ƺ(+ ãìâzüjíT+àâ·å8°ô¶ã?# +ê)ÛùÊz÷ó•@6¦¨Øã´ JGÜ\ÌbLôËy–ÑÆMÅ ÅN‹nj®î-eÔúÏ2æ4Ù²Ê\ùS/ÛﻩÍ:5Éîo­hwo`½íëUȆ:ýã. +kï~Ñ(<Ús*G¾ÌÙ…Ã;¯è«±åÞL0ÿˆûJåtªó"VÚEôBüp5€âàfÁ@³‘&fë¦ú-äFúaŸ8ÍOl5wwïõÑÞÏ«oA࣎3øØåLÐb…V‹ p£h‘²;á3ÇÍÖLcŸFþôJÖóV ÆÔcènâ“ûho»Ýµã…Žlà6S +qõ»‘©‡½À¡AP»LÇõÒ~ýV5 ¶ÓFŸ~m· Ô ÌiÄÝÆ'±ÑÞÏK¿¡¿ª¼Y?ÏuÒÄrÚÄ$0zð’T¦ÛÕÝ‹·µ¥i¤Ïì-ÆÕ…U'wwïõáqÜzš< xí{÷oM÷½–8{9²}D¤Ú$}–x¶{DqÁW ¦2lÈN¿×T½yåQ✈»öòæ†Þv»KÇ›ž@zYG‰7nŽ‹L-ÜlÿnÊãfÓaò »~þ\R“Wjòj¦…š´23˜YVf–ÁÌ4˜É>xPòbâfQªUxå%l£¯‰Ù‰)+1y9?¯˜ÙNÌ”3y¡¦<•”Qá9©þ>7óÁM[AüT·/˜Xv£¸)—Üä1ãx^r“ôß›Òõ[fqÙ°»S±§ÞTKž5N¤Éµ~ÃÖüiA'SG"ÞÆäân£½2†{Ûç½v:^s2vJ®ô +4%Àd¡-h•çÿøîá7²2U¥mãfìÖcä49DEQ¡'nCÆõ»Ül==Ñ?ïSvà§UT’ÉÂ0ª'QIý³ª"ŸP•²~‘€Ÿ-ÇGUIëûéÄO‡¬”õÄ/‹¬¤ç²’þ¬@ ‘ QrVy¨ð]VÚIVڛ󾬲RÞœ÷ªRWU©ª"Çy¿ŸúOä×Ö š@–KQI'QI®{¸çŸ.üt%*u•úÎÿ¾¨Œ¿Sˆ™ó»ÄìOˆ™Vb~xÞ_2“~˜šCQ¤Š­_ûÍ'AákAáß'(ø´ã÷EåÙ Gg ‚ïÙ–äú†uqúLÈéäû)äé#1¤ÄÐ*ÅüæG¿ÏïO"žÖˆÿX¥||3|®)ðžøÞñ›?oþÁx/á~/Úp›¯Ëû=§?ìßW"oCýÓ·¯^ÿ`Q«m– endstream endobj 22 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.002 192.001]/Type/Page>> endobj 23 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.002 192.0]/Type/Page>> endobj 24 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.002 192.001]/Type/Page>> endobj 25 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.001 192.001]/Type/Page>> endobj 26 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.001 192.0]/Type/Page>> endobj 401 0 obj <>stream +H‰äWɮǼÏWôL±²*k»Š|" Ã}Àƒ-H”þ{GdfõôÌ#$ÁðÍ ð˜1]K.‘K}øëÇãçùøîûÇ-§ÜÖ‘“4ü)³¿ütûð—¿çã§_oùÈGšr–CV±ÿùÇퟷ>aç?Güœ¯X5çú;ÿxü|ÃnüÃÁ_ñ•£ä™r×uàØVšo_ì:üMZûqÏ©â«$©• O‘x»ÕÔµoˆK¥Â‚®-ä’z_XæHS_õÀ–5–VøNÃ]~»á¢ÙAƒÉUM'4jI¤`Å¿n%I‡2’&}Á–Òú†Ø#“ ešA•„¨i¹ŸoøM{‡{ZcÂýe\¡ä(¹âãâÚÞüPê<n •¥o™¶òhG-é yiÈðH£%ŽpAðKÛ†'ûÞæ¿]&+V°IšŒGÀpЃç1äؾF!j‡•4`—dž¹ñ@ ­Ô$ã¾b¨ ý +g&U‹8*ˆXN£Ú¥µØJ(Šà'­·C +#ß˱˜ª†Ô´ +²‡‡”ªè‰`—,0Hñš,ûen‘NÕ|Hׂ ’üij¤D1‚ç9è™5+›"YjšàRI5× HðRæ†ð wÁeÌF¹"öŒg¦ÿ´¥ºŠŸ…AŽ#°"S7z,Àã6ëØØØبÖÿ6­™›·Àðœ' ^YöKÞò[\í¨¥ZéŠ;\F亘¾DáÏïT‘EÚõX¸h€‘W¦MpË|I×3Ðb³ÓÛèN1íá{K²±H)«#´XÜ•ˆC_Xò,e‚2sÙeœ9|HùŒ%å'²–‡ +ôŠú¥PŒ 9è©Å:¢‰KÃT^ØZ§°Ë¦‚îÈi8ÓT:y>qóèŽTí†ÙÊJÐ 9¶2kÄæ ‹ŒËoÛ¡¡@;Gj²6@ÁÑáñ'l8õˆM¨Ýr¤3¾¥VÓ55˜'…e—é2´ÅhM„Äíô4£“#I^x På 4Xu´'Ôn•éIÍŠ€äb¸ÇÚz#z9¿"•`˜-ÀŸæã€ðß$mÀÑwˆž6NùÒ(dëôʤ¬pÓ¼—ƒvõî^ öE¾ôyA½@mYþ*½ÈÒÄì¬À‚•ç†wÏ‹l9I—Ú,ß*ŒŠð2³Qáu¡ä>jÂÕ™·Ê4/ë¬'B’æt6ŒŽ©ÞTŸn+olf“ +ø´å弧Ë|W~”S'íÞÚРßc*@'g_tjø˜0ÄNÏù|VþZ=­9ìé&ßJœÚ½RZêBíCUY¬D—Ê–™i‹qÅØñ@ •›2'†|‘„]W\ŒFÃÆ(ÜO› +Æ4©#~ÞP„œ¸{fÓêâ`°¦vdO#VÏÃ}]Õ°×±;·¾ìFú‚¶±ôæoti u&,ÐÒ®þË1ºq4KpjÒB¦ *z—#ܸvHÙ²ål¯Ö»ˆ5ê:Nhä_+šOnÖÇŠŒ ?³­ {¸pªb·¬˜u$ÔpùÍûQ ¶”¹Á=ÇDº1G"Kñ¦Ö¬g¥i:dÚ6unÈ£AŸTd©V¿†õg›RQ›Ø`#Ë`3Å{9­}òÄ;·|NPŠÒ| ÏôsfN"ÎÃfsÚç€JŒ*6K¦»Ú²/š9ttk°¬*KÌ¿E­ƒÖÆ3g5Ÿi!:6`bÚäl²obóK_l—Я=Ví½@ßøQ.Û<]g N-ÊyF9¤‹]BOªM­%hÐì½Q7•–†L´†.ÅA挚}«Šv˜º,DJsjœCmÔm>×Xc®Ó»Aîå–Êùøô#r"œ¶Çs‡«n^‹õËümlð÷˜µ 9ùo$É»8„ÈYš;››8³êàD±0’üf¥¸Ó!äž÷ÆסÝÇywÏeŸ/2ɹƒQkÃZòR›÷ÒVùÈ36°aŒvÝPÙß%žN.s¢° +gˆ3ELåÃI–‡}ój«r~D1ÁAÿ¡†fÌ™êæUÎs³ê;û+„5õÐSÙ +·¹/ŸûŽ&e‘s>h^•çI‰oXzÆŸ%ÙbÉ7*¨)y× oÉ^r„Q˜™”¨xGºLþÖ¶JEaàXR\† >*[Sê™Ä©œA‚ä¸q‘ÜaUv–@¤u¶l5g]$Lî¡( š˜XctØðàe{ö¶Ûâ¸,$¹Ú˜(ÙºT±WW™¼“/erzÕæ_\.Qe  ¬6²ŠaóÚw#lNçÐ:sòÂéÐ VªÖæX«‹Õ½°Ì1^ Å ™áûî£ÃûBÙ˜—ݯL–OyÑù=_–Â4N-j#ÿ7úíÀÊŒÃH¬5ûs¿ÍVîî.øß»3,»Ð,Ú&ŽýÛ¼¬Ü»|¸/ØþÔ»lmþfÔ ˜k5æ{g4Õ#v¨ÑÀr9®niù)!¿öœ¥û,—÷5Ž\ß±ŒHê| „^Ø™Çe­ö…5Ê¥Ó]Ã]ÐÎ-á¶Ë·yò´NÇW±ü~ªoÅœ.tHžûH›½è>ßúly81°ñg‹nllÓ­\ïÇÕ?ïªÕœ¨kÌ©rå”þ§Öÿ§ô8U®TÑ?Ë©õßpj\(5ž5þ„Ò?G¨>}<ðß´ËLA endstream endobj 400 0 obj <>stream +H‰´WËŠd7ÝçWèR­·B[·Í¬šaðÂÌØ‹jCÛ`ðßÏ9¡›÷V6ÆfÆTéÔ•Bñ8ñЇ~ >}Lá›o?†[Š©¯bîøU¤„_~¼}øÇ÷)üøë-…ª´˜RyýûË¿oÿ¹}÷ '?˜œlr¾`÷Þ³ÏÜO‡~?ß ?¸à þP8`¯1ÍT챯5Âã³^‹ßQF ÷ÇÊ¡ÄÒäÅÚ$Ô(3oð¸ÍXJÝpÆÕVȱÌåë«–n#Z±,îªe†œ£” ë” ·kß(Å5°V¡ºpÎ=n?ݤB¡ +íÊ–ØD6„æe…{‰kòE(Ë{]áíÆ©½à ÓÊ N±õ , :›k§ØeòsÁ!ÜÝq´ÅHß¶Ø œ·: dæ#SÿYƒøÎIq’YsÁ'&Î8qY‚h_Ad9b”JX78sÂ:˜º—: ŽàøÚ𹕢w*%Œä{]`ËÊÐœ„ù3„öŒ²Áã6b…|‡pËêYHî–ÜG|ëD*ô•8”rm—±p™Ê‡¿<·16ÜD…HÒÀ•¥[ #ƒZc4=ëø`v_?ìfPªÃk Úç ð;‹)Lè.y{ÐZ$~ c‘Zýµ‰œKʳŒrŽpenôh!³ª÷);4a-FÔÌ×ð5‹AnÕ@ů,©mÝ`<Õ6蹜X_ + +†g팂Úõàô‰ÄÙÔ¡[vˆAúžÄH­¬‰Û2äT7Í×Ô/Õã“1 æuÊ*AŒ!ž¬£3EàŸ;”E!Ð$hBñõÓÀcûÕ1Ó¡-ÄȪ112(F!¶ò¬'ÁäÂÜdÀÚ:–ª“&éªLÌ(e{Å×ÐJIc(Cst+yÒ\IQ² H ÃdÍl§¼½Û­ ƒ±ª‘ÐF6†¿ô üˆmÔã+R›õþÔ8ÍLÿ_jñQãÒïJ3ßýˆîÕŠ•°†ÃÌ1ŸøØ~ÀcÏã¬õy›ï¾B+µOÙ±fX΀JŠ¾Vµ*’04r¤¹cÂ` NÐsœÏS¾R0K1r„™†›žžg3oLÇíºÅ„40ÔTMH½&-m/\3ÒÕ ŠÎ“;ÓXg_3²C“?k£Ô¦‰dë¤iÓÇ0Úíä³0iKkM’–µ¹";~#Fl„E hÓRqElý°œÚ2GÖÉÌüI óV5*íg˜¯—qn(EWËbx“Ï1„« –ð†ú,²4ÓæªÚÅ…óÈœÎjfÒ)òiòÅ/dîpGg÷a7(§ŸÁ ìÆ°gp8FmAƒœ¾æ<Ûl†áÄK·q]EiÁWÎ:Ú“éÌ!NÕÙ‡>§0OÂ}”¦–D30BͶÛÎ,eÃnsËÊr,)†MkÖ!©ÓEٚʎ¹¿PÙA &)Ÿ3:«'îsÝ©zÝt4k³÷ä÷ƒyHA +tš|µ7æ&ÈfÎú¤rõÜ…%91itâSÀ¢ˆÞ«¥HÙßྩ]“/.”›•sB4-æ¢Ãúо;†¯u ¨£8dùL¨+¿#‡ÈÞE!šIg>Äš1 QArðAÔÃo¬ÈµÒþT=õ§òµ--þÃEÉ|ÔVG NÌ:&BœÎ A•¥Å¥up)诹‘ôº{üÛRf $×¾œSu60ÈQCtbŸÝh–Öî~Í‹ór··¤DY§%ËÜ–™whÈÖÀ‡¾oºÍWWÈ™ÿ‰À|E€`ãpéÕÁ/LYì=˨R;§ûó5²ö'}e`$IJ“Thy!EXõf–®$VºTRydΗSÄפñLÓQÕ‡(øšª¯»æû?^Iö^ŽžŽ$·×.iá}C2ÜÌ5e—¶Q:.MÉ¡SÙàœÏ1SžXK®¿(iGSŠðy’õ¡o—¡ÁÑF7tZU$Lh{\Tɺ—]C¬³™#Æ×Õ¾˜ô2ëváðóâ¸tNèT²þÇú]šÃ"ýô¾6š„rꃱÖzÂw´ÚÞçYÆë–ÎŽoûÆçŽtÞ K­†óòÚ“‹&¶tå[¯×6@†›v¼`ðn¼ãï¬ gݨä:¾ÉéTÉl#¡QtÙ“œÊ¤u7¦Líô’|p!„b ¶ô±¶`‡àµ dúm˜Þ­´ëëëÑJÖ¿ÅÖ[C¦›ØÜGwÏ{ßgGðÒºÕÙQpè#[ˆV;]®§k»o}£+sïž¿ÊùdÉ~ð½J’‘·*œHÚVÒÀáhEÌ +ÕC¬pkì´s·­Í•u{ivû9óˆK<;ë%©*æö„ªò?PñÏ2ñJÄ'eóPÎ<”+åÌC9óðJÃ+ ÿv® ×ÿ“„ƒï(ø ¬ëLÁº.´ièë$|ÇÁ?GÁu¦àºPð»OþüW€²®u‰ endstream endobj 399 0 obj <>stream +H‰¤WËŽä6 ¼÷WøF«õºfä´‚òƒ<³’ü?*R²å^wï$Á=*™’iŠU¤>üðqûðé£ß¾ùöãv ÿþúíöáûŸüöÛß7¿ù-5qÞÇ-ôˆÿxüËí×ÛwŸ`þÁ[ü'¬§Í\ó²,úyûãæÏ}ó.düÄù®?ñJ¾'l1%WcI[0I¹l¯ŸÕ…Ï·ïJJÛü\Á>E{„Ùë ù÷˜' m›Fü 4´h!s~·±Y[nãñ«¯úõöãÉõæ’—¾Ÿ]Î%Ÿ\¯ ÿ©ëM]–l®'…/Òy´ÝìW]çf16kwÎÊXncÛ÷Òu‰®µŒ@yïz­rv½f}IU?|¬ jÓñ\s5@Ëb&ÑÕÞíh¸šÏÓ666àwÎƱØƶëµÛÕIA zq©ßyÝDOU#ã aG ¸¼u\ [3²_õœ³´¨ÛØÜ€ßmlÖ ý;g.=âJÏAÓ<–N®ç¢áÉ%0WįqIu¦9”‹l:vfÀßfy"zŇmllÀï66KCÛ/]¯Ýî.V©šâ¥¥|ÍΨì,;ø:9ûJÎ8ÈYÞËÍþnâ´2Í}pÁ‹<åfúßÜL_r³ßq3-ÜLϸ Y‘„/G’gîy¦fŽ=ã~XÔlï &hBßzp]r¼¦f:Q3ëÒAͪÊÛ 5“ÒNÎÔl+5û ¦\PSƾ—ž¤¶f¨™Z?W éýˆ ¨-ÍG¶1o hÎ#ûÕLá,-ÊÜÇ€ßm8›Ær›Ý5;¡%’”œ-¹Ó”8@UAvt¡)ñÐY5¥LMa0Ë6¶ÕUûóCOdÑ“G‹kYGZÆÓµžˆUû¶#JÀlU…)4KµçjßTDÒª(é«Õþ‘ë݉•´,9Æ;A‘'ž‚ÒWAé« ”Í~‡ Èбù*(eJ\%>”„²°/«)¼[P–ZW9™åzÔú;1I«˜¤“˜ÈRçeJÊ Ì7Ôûþ ÌËIKÄdB-)Û®1´6ÿ¶Ê'[}ùžEþ'|Ìÿ…çÿ%Û¿æ£)HklÝÛµÃñ$ ñÒáôÀẠ+Hîýp™Àë=aÚ];þ ÔíJ27dYöõÎs9…ºœBÝvÏÙí¾,-U;‡»á–‹pDZԚ¶'^Ï1¯ãÓxçgñ–÷$È)Þ×)’¿ÞB=°QàszW¤$u;™²úœfŽœ +MXò#™~D%mÚåKa8XÔºêéöRµ ¿1ÆBý¾ÃÁjýXM'°3Ú4"øÆ^©ñj⺲’ùÛŠ÷%WQÆx½ñ;ìèS¡øÖ0ƈKaïeˆo áÉ£`X);>ô‹SËÍ…„µ£‘¬yÍ5´^+K +Åj¾îßxµ'¢gÅ&¹41J ¢Œ¯-¸Á\LàC}'D+UO°hJ(+¼í0qBŸq ƒŒ¢¨n7G¾ƒŒ¸˜l©õ=F"—jJ…šØC<à´†XØYŽÅÔYÞeÖgH•ã¡Þ1.<å ôt5½õ‘Qù”´™‰Ú6ß/Ä2zÏ&^ }ôk¿È¶AÆÜL@d™·ƒq³j*2$ÓXóÂDòUK¦È¬õÉiÕÝ–ë릇ÆZú~&±BJûÄËh:H{ ÝË£©`}²¥¦~ˆuöd·ÙéMg/-·¿ûôqÿ‡†£ä endstream endobj 398 0 obj <>stream +H‰œWËŽ$·¼÷WÔ —d’¼z%è´0 ü [>¬ÈþÀ™duUOMÏÊ †Q$“Ɉ ëË_¿n_¾}Û_~úºÝÒÆ¿ÿüzûòËßãöëoq‹[ébÌ[ÿñó?oÿºýü á_¼sòÎ 1{ü‚ÿ±ý~‹!Ö±Å*¹gÎñ¦âø˜Rk¨­ô­¥‡æíþ›ÍŒ'Þëöƒ(‚ŒNÐÇØr¨=-p¿Iè{ +¦é5[«„š+BØÖ0‚{Ýž¦¥áÍû-…ªê †”܈†Vþ}˶mo Uä—‚b]"ÙZ°ì0°ÀÈFÍì…¦†¢Û÷Cã ŽS#Diý&Ô¤‡¨CA={èÜŠ ÖO¤ac¬öýf#;h!—ÊÔó˜mT"9Âø#jˆe_ÑiywìóߎÛÔ±=)b›zÐ1Úc›°LÂüY€4 +‘ì&m‰Å`åï·…6Ìj€­íÆ +S!,a¤l‘DÊ­9Œ‰›5´s +î’×fæ‚WMlãž—ŽVtm5dE>ûRjÀoh}_­ ÖB`]ð§"×zæE¼­ÒËD(o.ø=wŸÕˆàœ ±ùŠÕ1°%Ô«Úv`E’ ¯³–Q´%´A&K¬kßa #g˶9ͳµ!ÔJè›àÎKQ^ô,F²…°I„å×RAíš½ª³y÷‰4V˜ö6Û °TãѬÅ÷÷›3â®à.xV!#ÏR5myÙ'á +²' , ,cÖÉ¡€)tH{.—:o‹ÿ(~É}¢l¡ÓlcÐÆýp„ª’‹[3cM±bå…’›Eé–]¦Ã¨ `]*S € åŒ.;˜QB®c.ÍÛL®Ôõ 9XÙÖNºÐ Ø~r{é ¦¬:ÄàQÑ×ôP2ͱ¬æ}ubª«Øí¼@ÇxÃ# ›[¯wƒEÚÎ +Ô¡Ðj±"ÑTÕr’ 0Îæí‚U³ è(÷(N™œÙct ½~'ƒOÊÊóŸ[Ä›sŒ»^@â‚RŒýGh¹°Âfóè'èîú+T®—< +9Û¦õ1Û¶g’|£Fh‘œ³)ù/5_XdN805›Ûñ< +ââÈ1vóÝᇠ/ÕÐÍhÚ0f³ÇˆT­£7È”Ãûcó`ÇyÖäKuâþÆŠšÛbò¼à<‡g/´ç·ß蘳««?îžp|+v@Íø Ö`úLç, ¾;NGe©ÐLKïýDm-Ffû7oÁ‚F¶Ås×3ïó=cû +â39 ÔJ7²¬qÄ=ÆßzwoÏç¥&):°*æÒé&†E'Z´Ì»<DùÅeÒfÙÑ’€±{?-sŽàsðÙwá[™Ý½íã^gŽZë èP}‰zN½#Z³x¨S¯jõ©jŠxý e»—Nš³BUÍçx|[œðÙ§ÏÙ^hLÛØAÜcü-}¼2G½L{ð|ÃÕ…/¢íZf·ª;ø\œãJœú£ÚŸj“¶ÒìÊ“xkúRœå$NýqêYœåsq–ƒ8ËKq¢Ôø4–§'ClKIS›z@Ô®ÎrTg1õDNuª 2¯QÄñÐfÿmÂþé(·èˆ«È¥4ËIšÅUWž¥YÒL+ˆOy!Í1¥)'i¦%J÷’* W9!U2>0j>s\†Çþ­ï» Z/‹ã¹Ù7ÒpîÛM›œAþ¼û¢Q¾éÇAÜcâ¼T³»·=îÚT2nb.N,@Ÿ<%»ˆ{ŠìèÂSòÃSäè)º<Å>ñ¶9¬õÚø‰ü䣌í Àž>j½ö¿ÈÖ¾#ZÀ^ls…e4îÏûiŸÎ§}7)GG)Ÿžö¤+̽6“eIUžE^÷GGGïE—£ÈæsðÙwqŸy8J~á(48[Ñòd)ùCK9÷.~Ù&8÷Ov2-*ûe'r8ê奀Ò¯ˆŽsÂK?‘“Ÿˆ[…<üD·Ýg»b.Núv´“ötÒïý8è?¨õ~ÐG|&Á[®EYN}ùS¢<ôïEÙÿ´(§ Ôª¸H>¹H¾L¸|p;ºHã‘2>ÿbÚã®ÿ Öpníø8ìØ圹œJ­§R÷=sÞöÜc:Þ«ú¹Üú(·\”;Ï®~s{‘õbˆg-/ë]ÿŸzŸrª÷5Eê畆ƒHG…¯s./*=>#õ8Ÿ4z̹,ŽœN›tàGzElXv,±Àìò‚ÙúY¥õ³ÏY/VË!kyW럿}Ýðï ƒNœ, endstream endobj 397 0 obj <>stream +H‰¬WËŽ$7¼çWè:G¤Þ×ö4X,öà(¬Ç‡öþ?ààCYÊ꬞6¼h K‘MIÅ2?ýësøôõs ÿøésØ(Èß߶OÿüO ßþ·ÅCêy‘ Æ/þýßí—íËW˜²Éd“‡õ´™s^–I?‡ß¶¸s/!îÿ‹{­I6û{ÊF˜Êž:ç@©ïJ·ïêÃ÷í%×½ÕŒñ+Æ}Ï,“^òØ[Ç6{‚ñDi„©ÛÄúC:Ñ^Úsa®óRxoC m‰oØ{Ž_7‚¿´-æBŽ|ŸÛæØ~^'4'u™¨7òß÷ ßã@£î‰G Äe¯9­qˆ{KpÑh\Ä{Ų0öN#o@l#y61´Ä‚æ:âacomºýyœa¹ÂˆcŽŒ3pÜ‹†cq½“ºÞ©ˆ”Ú‚Fáéz«-ø{nkFö„¡½5 _Üç6ò¶útۺ׮×=×Ø! ¥rv]D~ÔTè@içÜÌõ¼SÖ-RÅló˜Fò¬æº¬ ,n 6ò¶øtÛº—®í… .‰@®\Ï 3šº>šº^j9PÚKNÓu‰¿ÙêØ-ÍĞ긼 +¾´xØÈÛáÓm³»rœ:2=—¡™ÞFÌ'ÇkIAH2½a©i¦KÔk­%b;m쩉. ˜¯-À²Ålì­ÚØ–½ô|€ûÈMôÆB”st|„£#SÿÿyÉ­’&z­Ðâ÷8ÚŸq´®5’õKŽö·í->ÝÙùG‘èj*‰^†åÄÑzâ艢õNQ£R Ä”§‘Õ)º2´žÚœ¡Õg;7uÙgâmÁ Gˆ ÷qÍÐ~bh7úñCÇÂÐý=vNYAÜY–¹”>É +?q<=s<Ÿe¥¬²"…K¾«¦Ý¥óO +/ˆv@ºsϾçSÐùôr÷½­AoÁÏiF§¨óê|žÎŸ¢Î>Ýîå}QÔ|QÏ߈9­Q'úxÔ§çç°{¾”«|ù«!‡ÃÔ¹»ãž…¼z€Þ:n!§KÇÏ!'w|‰ø û1çšžæy:Eüpþ@šó9Þ«×éäõ‡Â BHéÛ•Ü9üñm=J•ÝiÅIÊú]$݃ŠB–FØZQZí‹ú߶lÉ`Ùy¨,ã¼6NòÝ3C¸oý’뽄†'ÖjhVuxÛP"ቜ"‹Ib)!uoE®è×±t•0eX~Ç ©ëá±%ËèæÒ! +/Ò¢…W­é<À±3)Œ{Zá†;º« µQèè9„•Ugå,uh öØø¶a]¡¡¢¶3¨Ϲû¡ˆÂmCØ ¢¨{Lmžàtº+u¥˜(T¸Qe¥#épwöåÃÆ;CY¡­%iä5õí pNtÛ±½Á8#%it‚ÆnÕ`$-eÒjÞ’EÇ}Á«–õâÏ¢eg)R1–Â\ ’߯s„Èr ¤ž~tíi:Ë›ÊÍdžܓ#Ä—Eº¸O9!ÙMÖF íÚä“®è}àDpËd7K¹7ˆÐ$i: +ô¡±˜7ßâ ­xÛ,Í°°/Q5„0Jpù‰gz&M³‰pÞŽ&Éê —1¬YÃêÛml ø*}Ž£” M@A‹×·—# _çXA¢ ÖD#ÕDÉ1Ʊ XÕÂת%ç¡q—Hˆ¢ÉWR¶jFÙb_»TBþÌÃ+C„Èäc,*Y²Êò ¢¥?ìU˱ô²8y"ÿ¬Ê"GÝz5ܲÜP Cô²³=yÆB4g5ÇQ$l,Þ!ø,I:KI—„t›¾mr½"ÿ+|MrX³ÊÁhHÁ€¢‰F§9¼Í :ÖÎFe$ž c½1»QÅÒјִc–¼—#Í*UÑZË>â,™;†©™-€à°6ã-ëí_õÐéÛ Ç¹‡–]%ôòcñbIvÿ°”Q’u|úߢâ*ŸK}„UôÍ!b˜»äOƒŸô=à¯Óú Æé"›B1¤8!„w<íq9¹.ÓEê}7³~€¢¯eY -~¶0Œ™õÚúð±Þše¶þKYbÛÉfU _h$ã7±"ðI8rÁÕhØUx‡UÁ®_)I•À!ÒÞ?ÍD7IE'Ú@Rey¿Î8/¶î37¿kªeîw Hu›©ì­ +zÊ:ü;‘LŸjô‡(¬o³V(·w0sh;½¸;h™ýåë瀟?‡Ýj endstream endobj 17 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.002 192.001]/Type/Page>> endobj 18 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.002 192.0]/Type/Page>> endobj 19 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.002 192.001]/Type/Page>> endobj 20 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.002 192.0]/Type/Page>> endobj 21 0 obj <>/Resources<>/Properties<>>>/TrimBox[0.0 0.0 384.002 192.001]/Type/Page>> endobj 406 0 obj <>stream +H‰´WKŽ#7 Ýûº€Õ"©ï6AV È"0’Þx˜Éý"%«ì*§;@f€’È&)ŠÅ÷X~ùõÕ½¼½÷ÓϯîNþÿx?½üò{p Žjô! ƒ†¼òŸÿ<ýuúòÆæ/ê êü­‡Íð9/N¸o§àCj.xHüÀŠrÖw>R·‘<ò?ˆžZ‹îòµ§ðõtŽÙ—yå}õÅé›/•Oñ”ê”Ȩîrr_ ;ª"Ô܈sNèKC=cÈ>{ì¯'ð9OQL²s.'“u¹Q“ìaBÿá:þv«ùRŒr#¡yÂBK‚/Ä)Ú¾ašúÌ'H`>›Øßô*ˆí4’gC 0⨦jÕ]÷öœwXSo‰k!zÌP6©Wè©W~ýœPY¤–p¤^rq¦WÁlÕHŸl¨Zµ°àæ;mD›Í]÷w7uŠžBÈ®5­À6séYz”`Jܲ±hæüÖb?¡q§¨À¶± #yfÍ\"ˆ…Öƒƒ«¦h“¹ë^ãîgÎm’ù£Pˆ›Ì[é™·Ò3O9M‰|Š42—ê«mß›¥šè³ç-Z1g¡UÓF´ÍÜõµÛÍ8ßÊ=&} wÍšxNäúÒç`J\•L£YB©9Eg‚ØyFms  [íµQ­ê^ÃîgÎmž€z›ÇÐ>ƒÐö„6§8ý?Êm½Ë)Òs|Ö#|æŸ +°º‹ÏúˆÏz‡Ïdî†Ìc|2µü‘»;Ñnð™7øÜÀ3ßà©0*Î1Åa¤ 5x®èÌtCg6oÃe{Ä+1rOVîÀtVEgœ’ +ïÐÙщ[tÆGtât¶ñÜË[1^Ê>!㓸ðI|Ê'œq¦,¸¤ ÏùäpÞÇuÞÇÎû¼;ïË2ï˘úL˜ecKéŽQšË2ïÛ>£¬óžvEçý¢Ž>©ælsþߧ=×»º&¬ÀøÀÜŸõ°fü$0£p±kM°O(¸!<È›>H(i%ajù-õ”TÀÉ$ÈW]i¾eþ¹µÍ=njŽ›š§[îe­yqvO5Z“\“#ùMâhîúZžÓao–žy¹¯:¬UøxÕGæ»í’>Õ.%gV ˆVòò¼äpTòlzL\K»‰oK–øRñQö‡À ½ßæ´)ø¦Þð.‡m£¬IÓnŸ<­vðXå.1‰evúñ¾\já¹8"Û„u&Åì[ÿt¸ò¾úT¥ýΑ©f¹…tyªpbB—¦èk龦Iöõc±Î ™ÛøÒvÎ…KÁçJSs½iHÆC욪Ÿ€ùhç;/^`[¾ìª‘•ŠOÀká¤RWGù¢P‘s«úÍhÎÊœÕC“q±»„‰mM†ä#ôRð,’¢uE?PH˜æ ä£4>pMä7Ì”™X›~šæv§tíì“øÞ*3|5³ÂaÙs%B&ë›oÏ${»—“ɺ\‡¨Í¡qÂÒ_Þ^/ÿ0D%¢G endstream endobj 405 0 obj <>stream +H‰lVKn%7 Ü÷)ú–E}Émœ «A0È"xH& ;À$÷REª?¶oñTú"YEõóo/ûó——¼ÿôó˾ÉÎ߿߶ç_Ïû·ÿ¶¼ç½jK9—]¬àËnm¿|Áöç8,qø;vcϹÿ‰›ÿØÿÙr*Ú÷œæs£ÒÉwø¢ø–†tÝEf’‚M7÷ý¶=Õ™úœ¿b¬IììOÕ’eƒ”[»`IµâðvN¬ÿ×k¦Iûaë©•Tšì‡ŸÃîRÛ5ózÍ”žT+gJIÚËfÄRÏz?õƒ™–4ÏûŒ¥ÚŲàr#Uå¤4]÷Iek° … ÷FTŠê!,,Ì‹ßn¦‘ûšèî¥q1C<,IMÝê ÷ÔÂý9s†ÓRsçDOeÖ{RŽ3ŸgÎäæÛX@=1o&gáZå|l Gå_¼;Ù°ö+ˆ˜»‚B6-x¨#å +'Š”ÄtÒ·E–3(2`U pR÷Š <6œ?—P…Â;ä1Ö‰B¹¹‹h¦‰’$¨ˆ4°UMcø@¬/%¸Rg0ŠB0ð¿7”h2)#7„ž™õ€P™yZš‹Wâ f²æEÂ’Ø',’:óˆXëlw(0Žªy…¡dßÊ~·­(–*bjxl4n4l¬¼¾¬aƒ ¦#] ,aGï"Œ +^Õ*¹“³Hž¢¥Ø­gº‰¬â¦ +µ,:áRsö" ‰׆7Šáý40aÌÙ®NÃŽŠ C¾‘æ)O¾¨y~Úqõâ}Œå­8iLŠ^± мû­Qê¢'ª ©î= T ¥©B>j]cÔÛå¥+¼vY‚Z „ä·4OŽç¥3HJÍå )/zÀ‘LgOf¨k΃ãÞ zwš <=S!PPrPd—Æ'¥€½§ê†âúÞbA‰T^šñ¸Êº¹ŒÐcüp×1Fà +V»­1hìÏE •Œ×ÏÅ)ìÓ­ìh"Eß1 iËÎ1¥='\ƒf˜o÷”‡¬¼»ÌlDóFQAìºó,@ýO=PIÓÚZŽ1Œjt?"ÀOÎÎ~’Ñ£–^xþzN õû5£ãýàºÖrÈÂË¡B42>Scc^o èÈÂÉñ@úwiئ–Ä›3Ú1@GS ˜x³ ðXi]2BKÖûBÈî,AbCnúç $¥"GnrÄ5ØYJwêš®1^ÉQœÛDÐî`šY¦ûÓqãCÃÛ1+øФwñ!‡¿Â÷=be,|µ=øHVç*ô\H c〴z£_PØN¼ƒ®ÌýGÌ”K4¬U{ñö—-+gÇ6å†ý|#çí8{C¿¹[Û?àè²×r2‹JÁ7dø !ðÁ}báÚhþÌò†Sè“ÚĆ ï†à½²[|&÷×øÙäc|½]á 2ßÀíü±PñQV÷•Û™wÆn^ÏWc ò¾1âßz‹Íñ…`Ðí ÎË_ìwq.@>ÛÂ}ÛÅŽý ¶ ?ï¯pÝ.ˆíßò_·ÿоHM endstream endobj 404 0 obj <>stream +H‰¬WËŽÜ6¼ë+t°4ßl^ã9AC>`Û‡u'ÿ¤úA‰Òjf×q°À‹j’­fWuëݯï×wÞûõÇŸÞ¯KXùïïOË»_~÷맿ú5QvÞãAòû÷ŸËÇåç0§‹ƒ.þŠ¥lË–ÅD+W½¯ëí‹ìôeyŠŽhÍŽÖçe~^>þ°ü¶|…Õ8EOëÓtìë_‹w‘Êê]Æ3ïjMìîtt®®¤˜Öš ñpvj®´†ñ3Æä¨bŸõ)u×}Ç9Îç¼ÃèRŠëmÙ&ì÷yŸÉÁ…ÞÖ±×SŽ.æ°ŽsÆð%å}æyŸ‰QH<¯fBwÅÓ¼êbômžé.• |€sÕá>0ø™øU…Ÿ°CH JÉŒbìŒj’W08.â]ÃÕf›Èr0®FNÔÆ­òor¥§ —õømf{ì²l±M[šƒ2Ö¼œÙ‚ë§q@ºÐ†õRÇŲë¼-†õçy@Í ÝÇËß ùÿÑ—Ž œ‘â1£G¨#BGø­eJCx‹]=R¤bWjrLׄ×nKŽõq ‘ðÈ3#RœÜ‹¢æàËÚ‘5Ø,uÒá AèÅ€—4O…߆ì&>/¸£Æ©ë<Þä ÞÝ·< hÖÇ% {àR÷œ6׳pÙÛÁ%^7a¿ B"V\œol 6‹-Ó'ç€ ¯•¸-º»ÁÃÄol˜qFƒ•">W*b;{‘ÃKê-N7I|îUé“l@Âì—BåÅ‘é8Jι, ŒhÊÅH·*ù[%K_Là-±™$<õ:Ú*ž²@¼?ò¹'áÔ-<â 'p z}§WIx9FÚߥºL¢:p%Ð iJkqÛg×(p>R²1.tOpo‘]¡Œš’Ý!K\:²Aê¢(©rF®×Àm).E°ºÀƒâzÇrÅÀ¸~7mjÈv¦ss‘!mB`ys”V¬ÚðHN8_†ÀÃ̬7 Â4!{­p§t¡œÒñMNÖ1‰NâuJ·1òWj…"‰ÄóËKÉ𛡚hèœaˆ™—Ü"ÞM-ƒ+j9È×`!zõZWÕb{Y%¹¥½f~£j(ºÖóÊ,®6Ʀ¤ÂÇw/+[áxXO¹¢JvkD ÿrgþiéà1øL‘ŒŒÁ7/”ñ,^HuIm$®ŽÙ½ŠW¤ ˆqÅMg\„ 3¾]lÇéFãTˆÝÍ]@—~ µ2ÀÍâjù9|Û;Bx[Ô$`Üœ²ê:h$µUÓ’º8ª:ò/ÉÜN6F‰¬QR›X[™¢|Qsݘ2¢b¿Â5Îy֯Ņ…‹Ÿ¨Ã“f_}¢n\!•í)¸9=†XqÞ``!FºrôÏX‚þ¼™Ÿ Ê~,*UÞ'ƽMЬ¹8Ò¾VUa?ÉŒOX¥uìX<’«’Á…k#,P ÷ÛögÂ$T–ê¹aó•<60¦ ã{çÞ(ÚÓÞµ+tr\†2êM‹fŠ ”Š.lÎHWÛˆ)5?™Ö6›N'ïzªyû…Ã!MÛHdk :H»£Í½½màÖQ—*ÿý¦ +óìÁþ´Ùᤣ»ƒç¬æ~½D&åežY/­oÈKíÀI;Om‡ÌÔb¸LÈo³\f¸‘ãjl{i›·Žc Þ¶†PúA© +ù„¼íbÈNá+¬n<¨Nê63­·O§ù3H¤êÑ¥Õ˜/ü®üC¼©öÀŠØd_ [à2*¶ +`šú0âÿÆy¦ +›+ð›‰ÎêjÛÿ a +ønK¬*ÁGQ§£2aX-Rz¸¡2ÀìŒê8$_[0µï*é¥0wl`[ÛÒÍD7×Õ:V»kÇ¡†×…&®tœ³ß-ˆlʤ†°†ã\|yž3QÛþßÔo’)…2¶Qà7ÝDVëÐþ_úÊÃ5EŸ›×ƒÛ$wÚH´ßkð Q*Ãíªn“ěۆaÄÿ³f”—×ÐVÛ\ßltV—{ë÷xßKÇkw±CJ9Å[ã÷žS!ê#ÅEÔV›’Ù©µÚÑÕ¾ªmmK7ž«êXí.o\{£dx‹íä7Ñ ÑDÍöÿP“¾š[᤟)üÍwÉÌ833Þafš˜fb¦1ËLÌbÄLF̨‹‘x‰–ª G‚×xÂf©xÍËvæ%ͼŒÃHéyEÌv &ˆ'fÒ]E)ž{ÜI=å÷cjæšri7#ßm¾lF¯Q“.©mGûIM0! ~35©óGÜä:nCzÆ")z›P-yäx|¹2¯ÀlÕHÿKЃˆcqµÍø͆gÉ–ëX÷½v2^¹c;)×pR•0@T¡MhV¥¿}ðŪ2D¥­öI Ö¶r˜ìšB“¦„;nCÅùƒ Ül=5åPîS¿WïÓ,*IdÁŒêATRkÁ§7¨Jr™?ÅÀÏ–ýYUÒ\ï}¿WðË\ðËÛd%}—¬@ qÉYé”᛬´ƒ¬´å¾Ì²R^”û“ªÔYUêIUh/÷[Ñ¿#‡èN2øÕT굨¤ƒ¨$Õ‹t®÷é¢Þ§+Q©³¨Ôõþ±¨X½ïèƒs̉Ùï3ÍÄ|µÜ_23|35MQß$¾õk¿ãAPâµ Äÿ&(ø¤ Eå^‡…Ò™P¥)9Д®¬Ëˆ‡·„<|?„<½&†á1”L¿ãÙïcÄûˆ§9âß–)¯7†÷5¾Ç;~§~Ç·Ä;~c¼§p?Š6<¦{éýÈé7û¿¥ÈËPÿüáýŠŸA.[Œ endstream endobj 403 0 obj <>stream +H‰ìWËŽ%I Ý߯Ȩl;ÂñÚÒŒXbÁ”`Xt# HHü=çØŽ¼y«z3[„Zªö¹?Žñå_/?•ãw¿ÿz<ôà¿þòøò‡?ËñË¿rÈQ§"åÐUð?>ÿõñ·ÇO?cù—ج±ùW¬ÆškýÿåøÇCNiëSþ”Yxǯ¸ŠçëQdžÒÛ Ziv¼÷‹ñ÷´Ú79«­CO­FÐ'´q‹àŽ³.š «µ rŒÙ7PHi–™ß·?BoŽ³éÚ {•õð6œzä&TÈN¦WÜÞwÕu`UMâiaáeB† mgqZ!o;=ÍàH&ÉW8 +4Xu´ðV:Ý\Óâ ¹î¹ôÿ{½>"•`ˆ-ÀŸîã„ðß$k@ÑOˆžvJÅÒPIÖ…ÉJ#žs<ñ^ÖÕ'z‹j°/Š¥¯ˆµÜ¡—Ìâ.‡gð“³j¹œß"-ÄS’+ÎòÕECxÙ#PâmaÙÓß &<-¼U§;Ùf½ʶ ³côLózÂMoÝ+–ll¾ýþ²åÃy/—m®r¤ýÎ[ZtbHó·Ø]&Ÿrgä¼\•àþëËú‡½Üô¢Ä¥]PZ²Q™'v_f!­ õPœç³Þª0ýû?‚iä‰ê<6  ¹I*Þ}]ÌîÃf©ÜO“ +¦ ´³#©~ÛÐœ.„Ä»°“u 0–›P6p¶U/1|ÚÙQ"{½ûr„pëËeж1—~€òƒn¡m¡¢Ó/VÕÒîþ“œè81©g=5i)ÓU; n\;´lÙ“¶WoÊ]Õ»wôŒX+’4ïmEdžßØêÔ‰Ía‹´bÒT#ä÷èQ=aN9ØRæo’ƒêƘ†ç}3oà³Ò4ºm›67äÑ‹ O*²Ì/jÃ{¶¯¨”M}ØÑå°)'’^.k_<ñ±Zk“kª2”ë{xö ŸEc.>²—µ•ð1.`ó {³¤@c‡–ÂA¤{×e©Yêþ-æmµ6ž9«ûÌ +Ö±³µvÝlòoê3M_ìÐÄ=VýAßÄQ!û˜]g"N2<¡gwõ£KêIµ©µ& š?CʦÒÒt‚‹Þå5Îm ÙÚ¢Z‡)Ðs!R&ý¥-šæøÛbÖñn]g´é9å–éõÄD¤z!¬ä\ÀUƒ×æôuPkÊ>KÄ3Íûƒ²ò$Ð[ô¼K®zÝÙñ4˜UÇŒ…9åß^Ÿ»?K4³~:_‡õñ§û‡ç²÷—DBGraÔÚðþ€¼´ ¶U¾ýœ ì"#_7lBV”|P…Ì!à œ#Ž9©à˜ ÛÚȽڪ^_QLPòé?ÔPña©î^Í!$¾yõåhþ +aM}"4šF®aÇb]¼ð¥eñŽ(nÈ„ï9`÷˜#¾-ÔyÛýƒËšC+/º¾Ëm)Lã»Åü!ðƒ†;°RŠ Y³¿6\ñzDZ֧·`˜„Ð<Ú.ŽýÛº­Ô”c†/Øÿ,Úlmñ´ ˜l5}4³#w˜Ó  +9¯n iù%!Áöœeû¬÷5BƒØ±l^ bÝ÷Dh†‰\–¸Ñúá_X¤BºÜ0ÝuíÚ’n»}[7 /ëôy|õ+î÷Ù!õ +ùr #’<¦\‰V`ûpï²åéÁÄNž-†¥¹Í¶f8ðîœO¥jN¯j¿‰PöBýBÙÿ>¡~úùëÿþ+À3ZP8 endstream endobj 402 0 obj <>stream +H‰¼WKŽ%·Ü¿SÔšÃo’Üz$x50 /|€[^ô ðíIÖ«šž•` ¼fT‘ÉüD~êÓ_>Ÿ¾|ŽÇŸ~ø|<ÒÁ¿_z|úóßâñÓ¿ñˆG5Ĉ3ëÿ¯ÿxüóñãlÿ䇓þ»÷?“·Ë¡¿??bˆm1¤†Ÿ<2ïúWòÜÐJˆ=Ö£—Ðæ´ãùUà7 «Ç[ 6Ó‘C®¢C©ã(aô´ÁóÑCÎeÃf;RÈ}ú²áÊ©M3äÉ-%÷#¥0r: s<9”¶Q Ó°ÿf¦®0çž=*„ÚñV Ú€¦9Tܱ ÔÎ9ÌÎSXäAÉXÎÐÊ<Þ|Ëœ`W6âj»Á¡#¼:;p mt¾ÎpînõH5¤Ncê,<.A¨?Zá¿8ËFðN? â¦TÜ[ÛöÜl}‚½F¯œoé !oã=„c£DÄéC ¢Øàd³OúóÄ5ô\ið”ùálÍD ñoԌԪ ` -ÃYpK¦ó !s¹*xbIAüÆ’CYŸpÅ…†=ôÞ±zß+ˆÌ'BŒ@yú™½ðÉÜK´…àøRñºæ¬;E gÉžp7l™ º€“0¿Cø`å ž ò„[Àj€þÍ``É›Sðh®£@߬&ry´„Lܦ à°ž œÙ†›©°€ÿY¦¶:F +ÕÊp®¤CàDÈ×Ïuµ#¨ÕºáŒ•´~Óp• —SÞ?ÆhNR¿6I®vc[‡QL«L]²®Á×o¦J‡€£€Â±aÎàà¤î†¥ä_9 5ëAªÅAÁO><¯}]a>õvÊH‘%&£f(q;À¨FÜ+#tº ¤Ç–¥… }ÚJ bdWR$ÈdÚZS?Ô€…œ‹-3³c’¥cÜY ¬1IàØžtE)PÔAéµÎ žÛ¯ #0 ÊB̘v¢’ gƒ0ê°Wg—3³³3Ðݦԉ„ŠÙ€¹c­¡•Xã(AsôÆ*®´¹“"§ÁË,šõ’¸o~)ƒà¤ª$´sÁ÷wÎ7ä4ënŠ‰c×Åû‘9Éòñþà@`/ˆÔ"PE®Ë + œj§XõÎÈöš!`°þqâXàù`¿N«¤ +Np:éN¶QÀ¬F‹;<|?. Þ±/¬’wdPõb6î¹Ëá©-{Øøm×Ö;Œßk&ƒCI,Q¸ÆâÝ œ¯Ÿëzíi­o‹û ˜˜LÌ/_G% L7s2euøÎ ÖaT«_ kÄéͯ‚Þ ¿›ÓqÐd"\_?=Ó6„LKšØÜŸšÃŠûPx¨ÉaèžÎDëlvlš1ÀÆ”\Â%ˆ…½¢j»P¯Ï¢æ>( G Y5]ÖÓ7w|˜#ÜÑØ“Ø#2ÇìW0(»2ì$š+ü¡®©5çÜju!¹ë2DƒÆrR£F¤‡s8U3}N7`΄û0@uUÊg`´êu6£žó†äTór6ˆžXaÚÆD),³:Ò—(_SYëû •52ÈøÑÂoÍð‘û–îT½l:4Ë·÷âˆå÷@<è´ñÝŽ™ê@>kü"'¬×ž9X©#³F“ Z&ËzF* ˆwcäýþëšœX&@Pª^å Ñ\T㇦xS?6[kÍÅò‚¬ª•å?H"ÒwReÕ‘¯dq*jy¢‚ìà—R;~c¡.…ˆeå~aëTO0dCɬ®e¡/’ôY×èÕ1U]P\¡æ0ô]bHIº®ÂžõyÁ ³ú®)š„8 Íñ½9Éâܱ®À¬´ãêåOKš >™š'ß×…¯ ñºvzýŽe~§gåô0±­•{ ¿²À>pøæ¬ãoHkýÔ>1ë|5.‡rZÀ7:?§C‚PÚßœ&]=uÄ5³ˆá½5«úz™¤$§à¹Ù#õ6Šß¬Ôïõ1éµjîK|½ïwäªù‰Í{ôõSϯ !ºS«±¢Ø$šªÅP™Ór¾»ðòìÚ躟§–o¯/Çõ`N^X{EK[Ž"œÕ\É…NG;d¦P®Î±¾MÛ—¨k׭І"Ý^ºá똻d ¼zëCF ò%åÿÃÄ~eâˆÿ3Þiø³pÞX8ÿ0ö ¿!áƒqþNÆycáï"á¼’pÞHøã—ÏþýW€=9y endstream endobj 5 0 obj <> endobj 6 0 obj <> endobj 132 0 obj <> endobj 133 0 obj <> endobj 202 0 obj <> endobj 203 0 obj <> endobj 272 0 obj <> endobj 273 0 obj <> endobj 287 0 obj [/View/Design] endobj 288 0 obj <>>> endobj 285 0 obj [/View/Design] endobj 286 0 obj <>>> endobj 217 0 obj [/View/Design] endobj 218 0 obj <>>> endobj 215 0 obj [/View/Design] endobj 216 0 obj <>>> endobj 147 0 obj [/View/Design] endobj 148 0 obj <>>> endobj 145 0 obj [/View/Design] endobj 146 0 obj <>>> endobj 77 0 obj [/View/Design] endobj 78 0 obj <>>> endobj 75 0 obj [/View/Design] endobj 76 0 obj <>>> endobj 342 0 obj [341 0 R 340 0 R] endobj 407 0 obj <> endobj xref 0 408 0000000004 65535 f +0000000016 00000 n +0000000286 00000 n +0000031384 00000 n +0000000007 00000 f +0000600444 00000 n +0000600523 00000 n +0000000065 00000 f +0000031491 00000 n +0000031584 00000 n +0000031677 00000 n +0000031771 00000 n +0000031865 00000 n +0000031959 00000 n +0000032053 00000 n +0000032147 00000 n +0000032241 00000 n +0000589467 00000 n +0000589845 00000 n +0000590182 00000 n +0000590560 00000 n +0000590897 00000 n +0000577443 00000 n +0000577788 00000 n +0000578125 00000 n +0000578470 00000 n +0000578815 00000 n +0000566703 00000 n +0000567049 00000 n +0000567387 00000 n +0000567733 00000 n +0000568079 00000 n +0000555240 00000 n +0000555586 00000 n +0000555932 00000 n +0000556270 00000 n +0000556616 00000 n +0000543714 00000 n +0000544060 00000 n +0000544406 00000 n +0000544744 00000 n +0000545090 00000 n +0000532673 00000 n +0000533042 00000 n +0000533421 00000 n +0000533790 00000 n +0000534169 00000 n +0000520359 00000 n +0000520728 00000 n +0000521107 00000 n +0000521486 00000 n +0000521855 00000 n +0000509687 00000 n +0000510056 00000 n +0000510435 00000 n +0000510814 00000 n +0000511183 00000 n +0000032356 00000 n +0000032735 00000 n +0000033104 00000 n +0000033483 00000 n +0000033852 00000 n +0000034231 00000 n +0000034610 00000 n +0000034979 00000 n +0000000066 00000 f +0000000067 00000 f +0000000068 00000 f +0000000069 00000 f +0000000070 00000 f +0000000071 00000 f +0000000072 00000 f +0000000073 00000 f +0000000074 00000 f +0000000079 00000 f +0000601888 00000 n +0000601919 00000 n +0000601772 00000 n +0000601803 00000 n +0000000080 00000 f +0000000081 00000 f +0000000082 00000 f +0000000083 00000 f +0000000084 00000 f +0000000085 00000 f +0000000086 00000 f +0000000087 00000 f +0000000088 00000 f +0000000089 00000 f +0000000090 00000 f +0000000091 00000 f +0000000092 00000 f +0000000093 00000 f +0000000094 00000 f +0000000095 00000 f +0000000096 00000 f +0000000097 00000 f +0000000098 00000 f +0000000099 00000 f +0000000100 00000 f +0000000101 00000 f +0000000102 00000 f +0000000103 00000 f +0000000104 00000 f +0000000105 00000 f +0000000106 00000 f +0000000107 00000 f +0000000108 00000 f +0000000109 00000 f +0000000110 00000 f +0000000111 00000 f +0000000112 00000 f +0000000113 00000 f +0000000114 00000 f +0000000115 00000 f +0000000116 00000 f +0000000117 00000 f +0000000118 00000 f +0000000119 00000 f +0000000120 00000 f +0000000121 00000 f +0000000122 00000 f +0000000123 00000 f +0000000124 00000 f +0000000125 00000 f +0000000126 00000 f +0000000127 00000 f +0000000128 00000 f +0000000129 00000 f +0000000130 00000 f +0000000131 00000 f +0000000134 00000 f +0000600593 00000 n +0000600676 00000 n +0000000135 00000 f +0000000136 00000 f +0000000137 00000 f +0000000138 00000 f +0000000139 00000 f +0000000140 00000 f +0000000141 00000 f +0000000142 00000 f +0000000143 00000 f +0000000144 00000 f +0000000149 00000 f +0000601654 00000 n +0000601686 00000 n +0000601536 00000 n +0000601568 00000 n +0000000150 00000 f +0000000151 00000 f +0000000152 00000 f +0000000153 00000 f +0000000154 00000 f +0000000155 00000 f +0000000156 00000 f +0000000157 00000 f +0000000158 00000 f +0000000159 00000 f +0000000160 00000 f +0000000161 00000 f +0000000162 00000 f +0000000163 00000 f +0000000164 00000 f +0000000165 00000 f +0000000166 00000 f +0000000167 00000 f +0000000168 00000 f +0000000169 00000 f +0000000170 00000 f +0000000171 00000 f +0000000172 00000 f +0000000173 00000 f +0000000174 00000 f +0000000175 00000 f +0000000176 00000 f +0000000177 00000 f +0000000178 00000 f +0000000179 00000 f +0000000180 00000 f +0000000181 00000 f +0000000182 00000 f +0000000183 00000 f +0000000184 00000 f +0000000185 00000 f +0000000186 00000 f +0000000187 00000 f +0000000188 00000 f +0000000189 00000 f +0000000190 00000 f +0000000191 00000 f +0000000192 00000 f +0000000193 00000 f +0000000194 00000 f +0000000195 00000 f +0000000196 00000 f +0000000197 00000 f +0000000198 00000 f +0000000199 00000 f +0000000200 00000 f +0000000201 00000 f +0000000204 00000 f +0000600750 00000 n +0000600833 00000 n +0000000205 00000 f +0000000206 00000 f +0000000207 00000 f +0000000208 00000 f +0000000209 00000 f +0000000210 00000 f +0000000211 00000 f +0000000212 00000 f +0000000213 00000 f +0000000214 00000 f +0000000219 00000 f +0000601418 00000 n +0000601450 00000 n +0000601300 00000 n +0000601332 00000 n +0000000220 00000 f +0000000221 00000 f +0000000222 00000 f +0000000223 00000 f +0000000224 00000 f +0000000225 00000 f +0000000226 00000 f +0000000227 00000 f +0000000228 00000 f +0000000229 00000 f +0000000230 00000 f +0000000231 00000 f +0000000232 00000 f +0000000233 00000 f +0000000234 00000 f +0000000235 00000 f +0000000236 00000 f +0000000237 00000 f +0000000238 00000 f +0000000239 00000 f +0000000240 00000 f +0000000241 00000 f +0000000242 00000 f +0000000243 00000 f +0000000244 00000 f +0000000245 00000 f +0000000246 00000 f +0000000247 00000 f +0000000248 00000 f +0000000249 00000 f +0000000250 00000 f +0000000251 00000 f +0000000252 00000 f +0000000253 00000 f +0000000254 00000 f +0000000255 00000 f +0000000256 00000 f +0000000257 00000 f +0000000258 00000 f +0000000259 00000 f +0000000260 00000 f +0000000261 00000 f +0000000262 00000 f +0000000263 00000 f +0000000264 00000 f +0000000265 00000 f +0000000266 00000 f +0000000267 00000 f +0000000268 00000 f +0000000269 00000 f +0000000270 00000 f +0000000271 00000 f +0000000000 00000 f +0000600907 00000 n +0000600990 00000 n +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000601182 00000 n +0000601214 00000 n +0000601064 00000 n +0000601096 00000 n +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000037318 00000 n +0000037401 00000 n +0000602004 00000 n +0000507489 00000 n +0000037825 00000 n +0000037711 00000 n +0000505403 00000 n +0000503207 00000 n +0000501940 00000 n +0000500559 00000 n +0000498533 00000 n +0000496575 00000 n +0000035358 00000 n +0000037593 00000 n +0000037625 00000 n +0000037475 00000 n +0000037507 00000 n +0000037901 00000 n +0000038217 00000 n +0000039812 00000 n +0000057001 00000 n +0000122590 00000 n +0000188179 00000 n +0000253768 00000 n +0000319357 00000 n +0000384946 00000 n +0000450535 00000 n +0000519085 00000 n +0000517512 00000 n +0000515499 00000 n +0000513538 00000 n +0000511562 00000 n +0000530718 00000 n +0000528764 00000 n +0000526558 00000 n +0000524420 00000 n +0000522234 00000 n +0000541590 00000 n +0000539339 00000 n +0000537993 00000 n +0000536570 00000 n +0000534548 00000 n +0000553627 00000 n +0000551601 00000 n +0000549640 00000 n +0000547672 00000 n +0000545469 00000 n +0000564741 00000 n +0000562536 00000 n +0000560404 00000 n +0000558228 00000 n +0000556954 00000 n +0000575180 00000 n +0000573824 00000 n +0000572393 00000 n +0000570368 00000 n +0000568417 00000 n +0000587429 00000 n +0000585463 00000 n +0000583502 00000 n +0000581268 00000 n +0000579152 00000 n +0000598212 00000 n +0000596122 00000 n +0000593912 00000 n +0000592629 00000 n +0000591242 00000 n +0000602039 00000 n +trailer <]>> startxref 602242 %%EOF \ No newline at end of file diff --git a/presto-docs/src/main/resources/logo/web/fb/black/Presto_FB_Lockups_BLACK_BG-25.svg b/presto-docs/src/main/resources/logo/web/fb/black/Presto_FB_Lockups_BLACK_BG-25.svg new file mode 100644 index 00000000..bf6ffbd6 --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/black/Presto_FB_Lockups_BLACK_BG-25.svg @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/black/Presto_FB_Lockups_BLACK_BG-26.svg b/presto-docs/src/main/resources/logo/web/fb/black/Presto_FB_Lockups_BLACK_BG-26.svg new file mode 100644 index 00000000..3005420c --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/black/Presto_FB_Lockups_BLACK_BG-26.svg @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/black/Presto_FB_Lockups_BLACK_BG-27.svg b/presto-docs/src/main/resources/logo/web/fb/black/Presto_FB_Lockups_BLACK_BG-27.svg new file mode 100644 index 00000000..ca87518e --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/black/Presto_FB_Lockups_BLACK_BG-27.svg @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/black/Presto_FB_Lockups_BLACK_BG-28.svg b/presto-docs/src/main/resources/logo/web/fb/black/Presto_FB_Lockups_BLACK_BG-28.svg new file mode 100644 index 00000000..32f3449e --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/black/Presto_FB_Lockups_BLACK_BG-28.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/black/Presto_FB_Lockups_BLACK_BG-29.svg b/presto-docs/src/main/resources/logo/web/fb/black/Presto_FB_Lockups_BLACK_BG-29.svg new file mode 100644 index 00000000..b6464139 --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/black/Presto_FB_Lockups_BLACK_BG-29.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/black/Presto_FB_Lockups_BLACK_BG-30.svg b/presto-docs/src/main/resources/logo/web/fb/black/Presto_FB_Lockups_BLACK_BG-30.svg new file mode 100644 index 00000000..c375dda3 --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/black/Presto_FB_Lockups_BLACK_BG-30.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/black/Presto_FB_Lockups_BLACK_BG-31.svg b/presto-docs/src/main/resources/logo/web/fb/black/Presto_FB_Lockups_BLACK_BG-31.svg new file mode 100644 index 00000000..09e5ba79 --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/black/Presto_FB_Lockups_BLACK_BG-31.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/black/Presto_FB_Lockups_BLACK_BG-32.svg b/presto-docs/src/main/resources/logo/web/fb/black/Presto_FB_Lockups_BLACK_BG-32.svg new file mode 100644 index 00000000..92d87341 --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/black/Presto_FB_Lockups_BLACK_BG-32.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/blue/Presto_FB_Lockups_FB_BLUE_BG-17.svg b/presto-docs/src/main/resources/logo/web/fb/blue/Presto_FB_Lockups_FB_BLUE_BG-17.svg new file mode 100644 index 00000000..58358b99 --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/blue/Presto_FB_Lockups_FB_BLUE_BG-17.svg @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/blue/Presto_FB_Lockups_FB_BLUE_BG-18.svg b/presto-docs/src/main/resources/logo/web/fb/blue/Presto_FB_Lockups_FB_BLUE_BG-18.svg new file mode 100644 index 00000000..a3bfabcd --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/blue/Presto_FB_Lockups_FB_BLUE_BG-18.svg @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/blue/Presto_FB_Lockups_FB_BLUE_BG-19.svg b/presto-docs/src/main/resources/logo/web/fb/blue/Presto_FB_Lockups_FB_BLUE_BG-19.svg new file mode 100644 index 00000000..59ad204b --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/blue/Presto_FB_Lockups_FB_BLUE_BG-19.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/blue/Presto_FB_Lockups_FB_BLUE_BG-20.svg b/presto-docs/src/main/resources/logo/web/fb/blue/Presto_FB_Lockups_FB_BLUE_BG-20.svg new file mode 100644 index 00000000..72b06f7c --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/blue/Presto_FB_Lockups_FB_BLUE_BG-20.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/blue/Presto_FB_Lockups_FB_BLUE_BG-21.svg b/presto-docs/src/main/resources/logo/web/fb/blue/Presto_FB_Lockups_FB_BLUE_BG-21.svg new file mode 100644 index 00000000..9920f4ee --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/blue/Presto_FB_Lockups_FB_BLUE_BG-21.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/blue/Presto_FB_Lockups_FB_BLUE_BG-22.svg b/presto-docs/src/main/resources/logo/web/fb/blue/Presto_FB_Lockups_FB_BLUE_BG-22.svg new file mode 100644 index 00000000..b733674a --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/blue/Presto_FB_Lockups_FB_BLUE_BG-22.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/blue/Presto_FB_Lockups_FB_BLUE_BG-23.svg b/presto-docs/src/main/resources/logo/web/fb/blue/Presto_FB_Lockups_FB_BLUE_BG-23.svg new file mode 100644 index 00000000..b622ff25 --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/blue/Presto_FB_Lockups_FB_BLUE_BG-23.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/blue/Presto_FB_Lockups_FB_BLUE_BG-24.svg b/presto-docs/src/main/resources/logo/web/fb/blue/Presto_FB_Lockups_FB_BLUE_BG-24.svg new file mode 100644 index 00000000..18e4b181 --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/blue/Presto_FB_Lockups_FB_BLUE_BG-24.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/dark-blue/Presto_FB_Lockups_DARKBLUE_BG-09.svg b/presto-docs/src/main/resources/logo/web/fb/dark-blue/Presto_FB_Lockups_DARKBLUE_BG-09.svg new file mode 100644 index 00000000..4f46cf27 --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/dark-blue/Presto_FB_Lockups_DARKBLUE_BG-09.svg @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/dark-blue/Presto_FB_Lockups_DARKBLUE_BG-10.svg b/presto-docs/src/main/resources/logo/web/fb/dark-blue/Presto_FB_Lockups_DARKBLUE_BG-10.svg new file mode 100644 index 00000000..077e29af --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/dark-blue/Presto_FB_Lockups_DARKBLUE_BG-10.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/dark-blue/Presto_FB_Lockups_DARKBLUE_BG-11.svg b/presto-docs/src/main/resources/logo/web/fb/dark-blue/Presto_FB_Lockups_DARKBLUE_BG-11.svg new file mode 100644 index 00000000..3a385c80 --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/dark-blue/Presto_FB_Lockups_DARKBLUE_BG-11.svg @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/dark-blue/Presto_FB_Lockups_DARKBLUE_BG-12.svg b/presto-docs/src/main/resources/logo/web/fb/dark-blue/Presto_FB_Lockups_DARKBLUE_BG-12.svg new file mode 100644 index 00000000..6acbc651 --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/dark-blue/Presto_FB_Lockups_DARKBLUE_BG-12.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/dark-blue/Presto_FB_Lockups_DARKBLUE_BG-13.svg b/presto-docs/src/main/resources/logo/web/fb/dark-blue/Presto_FB_Lockups_DARKBLUE_BG-13.svg new file mode 100644 index 00000000..cc1b905b --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/dark-blue/Presto_FB_Lockups_DARKBLUE_BG-13.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/dark-blue/Presto_FB_Lockups_DARKBLUE_BG-14.svg b/presto-docs/src/main/resources/logo/web/fb/dark-blue/Presto_FB_Lockups_DARKBLUE_BG-14.svg new file mode 100644 index 00000000..49dd283f --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/dark-blue/Presto_FB_Lockups_DARKBLUE_BG-14.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/dark-blue/Presto_FB_Lockups_DARKBLUE_BG-15.svg b/presto-docs/src/main/resources/logo/web/fb/dark-blue/Presto_FB_Lockups_DARKBLUE_BG-15.svg new file mode 100644 index 00000000..810c1168 --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/dark-blue/Presto_FB_Lockups_DARKBLUE_BG-15.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/dark-blue/Presto_FB_Lockups_DARKBLUE_BG-16.svg b/presto-docs/src/main/resources/logo/web/fb/dark-blue/Presto_FB_Lockups_DARKBLUE_BG-16.svg new file mode 100644 index 00000000..e2d15a73 --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/dark-blue/Presto_FB_Lockups_DARKBLUE_BG-16.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/dark-grey/Presto_FB_Lockups_DARKGREY_BG-33.svg b/presto-docs/src/main/resources/logo/web/fb/dark-grey/Presto_FB_Lockups_DARKGREY_BG-33.svg new file mode 100644 index 00000000..41b6a7c1 --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/dark-grey/Presto_FB_Lockups_DARKGREY_BG-33.svg @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/dark-grey/Presto_FB_Lockups_DARKGREY_BG-34.svg b/presto-docs/src/main/resources/logo/web/fb/dark-grey/Presto_FB_Lockups_DARKGREY_BG-34.svg new file mode 100644 index 00000000..a4c2b612 --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/dark-grey/Presto_FB_Lockups_DARKGREY_BG-34.svg @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/dark-grey/Presto_FB_Lockups_DARKGREY_BG-35.svg b/presto-docs/src/main/resources/logo/web/fb/dark-grey/Presto_FB_Lockups_DARKGREY_BG-35.svg new file mode 100644 index 00000000..c5f96803 --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/dark-grey/Presto_FB_Lockups_DARKGREY_BG-35.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/dark-grey/Presto_FB_Lockups_DARKGREY_BG-36.svg b/presto-docs/src/main/resources/logo/web/fb/dark-grey/Presto_FB_Lockups_DARKGREY_BG-36.svg new file mode 100644 index 00000000..13ae8623 --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/dark-grey/Presto_FB_Lockups_DARKGREY_BG-36.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/dark-grey/Presto_FB_Lockups_DARKGREY_BG-37.svg b/presto-docs/src/main/resources/logo/web/fb/dark-grey/Presto_FB_Lockups_DARKGREY_BG-37.svg new file mode 100644 index 00000000..848a2cfe --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/dark-grey/Presto_FB_Lockups_DARKGREY_BG-37.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/dark-grey/Presto_FB_Lockups_DARKGREY_BG-38.svg b/presto-docs/src/main/resources/logo/web/fb/dark-grey/Presto_FB_Lockups_DARKGREY_BG-38.svg new file mode 100644 index 00000000..9d7ae4ec --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/dark-grey/Presto_FB_Lockups_DARKGREY_BG-38.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/dark-grey/Presto_FB_Lockups_DARKGREY_BG-39.svg b/presto-docs/src/main/resources/logo/web/fb/dark-grey/Presto_FB_Lockups_DARKGREY_BG-39.svg new file mode 100644 index 00000000..4c28f1ee --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/dark-grey/Presto_FB_Lockups_DARKGREY_BG-39.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/dark-grey/Presto_FB_Lockups_DARKGREY_BG-40.svg b/presto-docs/src/main/resources/logo/web/fb/dark-grey/Presto_FB_Lockups_DARKGREY_BG-40.svg new file mode 100644 index 00000000..6aec4036 --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/dark-grey/Presto_FB_Lockups_DARKGREY_BG-40.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/light-grey/Presto_FB_Lockups_LIGHTGREY_BG-41.svg b/presto-docs/src/main/resources/logo/web/fb/light-grey/Presto_FB_Lockups_LIGHTGREY_BG-41.svg new file mode 100644 index 00000000..5120d49e --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/light-grey/Presto_FB_Lockups_LIGHTGREY_BG-41.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/light-grey/Presto_FB_Lockups_LIGHTGREY_BG-42.svg b/presto-docs/src/main/resources/logo/web/fb/light-grey/Presto_FB_Lockups_LIGHTGREY_BG-42.svg new file mode 100644 index 00000000..bdd30000 --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/light-grey/Presto_FB_Lockups_LIGHTGREY_BG-42.svg @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/light-grey/Presto_FB_Lockups_LIGHTGREY_BG-43.svg b/presto-docs/src/main/resources/logo/web/fb/light-grey/Presto_FB_Lockups_LIGHTGREY_BG-43.svg new file mode 100644 index 00000000..51a59886 --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/light-grey/Presto_FB_Lockups_LIGHTGREY_BG-43.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/light-grey/Presto_FB_Lockups_LIGHTGREY_BG-44.svg b/presto-docs/src/main/resources/logo/web/fb/light-grey/Presto_FB_Lockups_LIGHTGREY_BG-44.svg new file mode 100644 index 00000000..afb50593 --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/light-grey/Presto_FB_Lockups_LIGHTGREY_BG-44.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/light-grey/Presto_FB_Lockups_LIGHTGREY_BG-45.svg b/presto-docs/src/main/resources/logo/web/fb/light-grey/Presto_FB_Lockups_LIGHTGREY_BG-45.svg new file mode 100644 index 00000000..fc614e8b --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/light-grey/Presto_FB_Lockups_LIGHTGREY_BG-45.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/light-grey/Presto_FB_Lockups_LIGHTGREY_BG-46.svg b/presto-docs/src/main/resources/logo/web/fb/light-grey/Presto_FB_Lockups_LIGHTGREY_BG-46.svg new file mode 100644 index 00000000..4f2b6762 --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/light-grey/Presto_FB_Lockups_LIGHTGREY_BG-46.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/light-grey/Presto_FB_Lockups_LIGHTGREY_BG-47.svg b/presto-docs/src/main/resources/logo/web/fb/light-grey/Presto_FB_Lockups_LIGHTGREY_BG-47.svg new file mode 100644 index 00000000..92187f5d --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/light-grey/Presto_FB_Lockups_LIGHTGREY_BG-47.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/light-grey/Presto_FB_Lockups_LIGHTGREY_BG-48.svg b/presto-docs/src/main/resources/logo/web/fb/light-grey/Presto_FB_Lockups_LIGHTGREY_BG-48.svg new file mode 100644 index 00000000..4d49ea4f --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/light-grey/Presto_FB_Lockups_LIGHTGREY_BG-48.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/white/Presto_FB_Lockups-01.svg b/presto-docs/src/main/resources/logo/web/fb/white/Presto_FB_Lockups-01.svg new file mode 100644 index 00000000..a920c2e2 --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/white/Presto_FB_Lockups-01.svg @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/white/Presto_FB_Lockups-02.svg b/presto-docs/src/main/resources/logo/web/fb/white/Presto_FB_Lockups-02.svg new file mode 100644 index 00000000..58a2df53 --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/white/Presto_FB_Lockups-02.svg @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/white/Presto_FB_Lockups-03.svg b/presto-docs/src/main/resources/logo/web/fb/white/Presto_FB_Lockups-03.svg new file mode 100644 index 00000000..2bc586f8 --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/white/Presto_FB_Lockups-03.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/white/Presto_FB_Lockups-04.svg b/presto-docs/src/main/resources/logo/web/fb/white/Presto_FB_Lockups-04.svg new file mode 100644 index 00000000..594c84a1 --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/white/Presto_FB_Lockups-04.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/white/Presto_FB_Lockups-05.svg b/presto-docs/src/main/resources/logo/web/fb/white/Presto_FB_Lockups-05.svg new file mode 100644 index 00000000..747d893e --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/white/Presto_FB_Lockups-05.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/white/Presto_FB_Lockups-06.svg b/presto-docs/src/main/resources/logo/web/fb/white/Presto_FB_Lockups-06.svg new file mode 100644 index 00000000..ed31d519 --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/white/Presto_FB_Lockups-06.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/white/Presto_FB_Lockups-07.svg b/presto-docs/src/main/resources/logo/web/fb/white/Presto_FB_Lockups-07.svg new file mode 100644 index 00000000..435db555 --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/white/Presto_FB_Lockups-07.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/fb/white/Presto_FB_Lockups-08.svg b/presto-docs/src/main/resources/logo/web/fb/white/Presto_FB_Lockups-08.svg new file mode 100644 index 00000000..b56fc10a --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/fb/white/Presto_FB_Lockups-08.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/main/black/FB_Presto_Logo_BlackBG-01.svg b/presto-docs/src/main/resources/logo/web/main/black/FB_Presto_Logo_BlackBG-01.svg new file mode 100644 index 00000000..619a816d --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/main/black/FB_Presto_Logo_BlackBG-01.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/main/black/FB_Presto_Logo_BlackBG.ai b/presto-docs/src/main/resources/logo/web/main/black/FB_Presto_Logo_BlackBG.ai new file mode 100644 index 00000000..71b8a355 --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/main/black/FB_Presto_Logo_BlackBG.ai @@ -0,0 +1,2444 @@ +%PDF-1.5 %âãÏÓ +1 0 obj <>/OCGs[5 0 R]>>/Pages 3 0 R/Type/Catalog>> endobj 2 0 obj <>stream + + + + + application/pdf + + + FB_Presto_Logo_BlackBG + + + 2013-10-23T20:37:58-07:00 + 2013-10-23T20:37:58-07:00 + 2013-10-23T20:37:58-07:00 + Adobe Illustrator CC (Macintosh) + + + + 256 + 160 + JPEG + /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgAoAEAAwER AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A8qYq7FXYq7FXYq7FXYq7 FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq 7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7 FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYqqQwTzv6cMbSv14oCxoPYYqs IINDsR1GKrjDKIxKUYRsaK5B4k+x+jDSOIXTo4pJXWOJC8jbKigkk+wGIBOwSS6aGaGQxTI0ci/a RwVYVFdwcTEg0UA2swJdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdi rsVdirsVZF5E896/5H19Ne0F401BI3hVpkEicJBRvhOKqmk+QvOfmTRNZ80abYfWdK0nlNq10JYI xF8JkY+nI6yN8O/wKcVQ955z1e78oWPlSVYv0Xp87XMDKpEvNjITyblQj9+3bLpZ5HGIdA4UNDjj nlnF8chR7un6kZqOg+afy+13T7vUbaKO8FZ7aNnWVGCnieXpt7+OW8OTTTEiN2Om1mHVwPAbHI9F 0Wn+ZPzD8wX1/bxQLeFY3uEVjHGAqrEvHmXPRPHLRjy6zJKQA4vwG8yjhiAeTF7iB4J5IJP7yJmR 6biqmhzAlGjRbwbCngS7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq2AWIAFS dgB1JxVEX2malp7xx39pNaSSp6sSTxtGWQsV5qHAqvJSK+IxVDYq7FXYq7FUysfMnmLT9Ou9MsNU vLTTr8EX1lBPLFBOCOJEsaMEfbb4hirL9Z/K3TdO/KfSvPcfmOG5vdRmEMmgLEglhBaVeRkEzMf7 oH+6HXFUm0g6x568z6dpes6xKWmLxJeXJM/pKFZ9lZk6lf5syYcWaYjKTrs3h6PDKeOA26Da+i/X YtV8heatQ0nSNWaQwiJXvIVEfqB4klpx5SU4l6faycpT0+QiEviz0eYarDHJKNXe3xryZF5d/LDT db8nSeYLrUZEv51nmUgoIUMbMD6tVLGvGpoRmx03ZccuDxDI8Rs+XxRk1RhPhA2eZZonPdirsVdi rsVdirsVdirsVdirsVdirsVe5aV/zib5x1TRfL+t22rWCaXrNlHqF3czl4hZxSxRzIr7HmxEhG22 25GKqmu/84o63H5futZ8p+ZNP82LZKXntbLaUhRUrFweZXem/EkE9qnbFXmf5ZeQLzz75wtfLNpd x2U90krrcSqzIBDG0hBC778cVekwf84i/mEtnrN/qN3aWFnpf1r6uXLPLdLbFuMiIooiS8KqXYGh +zirEvyq/I/zP+YUN3qFtPb6VoGnki81i9JWJWVeTKgH2iqnk1SFA6npirLdV/5xY1OXRLrVPJfm rS/OBsQTc2li6+rsCSsfB50Z9vssy17b7Yq83/Lj8tfMv5geZBoOhpGs6RtNdXFwSkUESEKzuQGb 7TAAAE1xV623/OJEDzvp9r+YOjza0nwtpxADiQbFSFleQUO32PoxViH5QflFJ5k82ahHLr9hpsnl i/tlcTseN0wmkB+rk8CR/o/h+0MVe8/85C/kh/jzzpZav/ifTdF9DTYrT6retSRuE88nqD4h8J9W n0HFXy15b/LbzF5o86z+U/LipqF1DLMjXYJS3EML8GuHc14x9PfcAVJpir13/oUC4eVtNh886RJ5 iVatpNDyDAVINHaWnv6X0Yq8S84+TvMHk/zBc6Dr1sbXULUjktQyOjCqSRuNmRh0P0GhBGKpJirs Vdir1L87dM/J2G/0aH8rJWvEmWYaiitcyt6nJBCAJxXcFvs4qv8AyZsPy+E2rRecVtotRi4LBDqR ESKgB9TiJCq860rXcdu+bLs8YTfHV+bznb09WOHwOLh68PO2F65FKl/rK+XDdP5XS5kVHj9Uweny +DmenTpy3pmLkJBkIXwX8Hb6bJ6IDLQymI7rZ/55s/y0TyIJNK+p/WwIvqEkBT6y7Fhy9Snxt8Ne XPp883muhpRp7hw3tVc/j+1pwSy+JvddXj+c27J2KuxV2KuxV2KuxV2KuxV2KuxV2Kvpv/nIG8u4 v+cevyuto5nS3uLGx+sQqxCScNPjKc1GzcTuK4qxf/nDzWbyz/Ng6fHKy2up2M8dxDuVYwgSoxHQ FeJofcjviqcflNp1vpv/ADl3qdjbKFt4L7WFhQCgVCkxVR/qg0xVhH/ORvmjzHqH5p+YtLvdTuZ9 Msbsx2di8rmCJVVacIq8B86VxV7fH+XHmrX/APnFjyx5a8npEl1qRhvNTEsoiEkErS3D1fv+9Mfw +A9sVQP/ADj7+Q/5qeQvzDi1jVVtodHltp7e+WG4EhcMvKMcAN6SqpxVLfys85+U/JX/ADkZ59sN XuI9MstVvLqC0uZSEgjlW6MgR2PworBjQk0FKd8VY75z/wCcQPPVs1xqPlW/tfMWnSFpbdPU9K7Z GJYfb/cuaftCTfwxV41oFneWXnbTbO9hkt7y31KCK4glUpIkiTqGV1bcMD1rir2T/nNX/wAmnpX/ AGw7f/qMu8VTz/nGMLof5Q/mR5wtAF1a2t50t5qVZPqlm08dOtAZJAT8hXpir5sh1LUIdRTU4rmR dQjmFwl3yPqiYNzEnM78uW9cVfRv/OXiW+qeX/y+81tGEv8AU7JxcMAAWR44Z0U/6jSvT/WxV804 qrw2F9PBJcQ28ssEO80qIzIm1fiYCg+nJjHIiwDQQZAbJpda9p03lWz0aPSYYb+2mMsurrx9aVSZ D6bUQNQeoOrn7I+i+eeJwiHCBIH6up5+X6ejWMZEzK9u5mH5IecNO/Lj8xbPXfNGm3H1JreaFHEX 72IygATxrJx5UFVNDWjH5GieOUeYIbBIHkzD8xtL1r8/PP8Ad69+XOhySaZptpBZ3d5ctBa+tMrS MHPNxVijBQKk8VFabDIJb8ufmb5c8n+TpvKnmDTp7bXdJE9peaU0VVllLtyq9eNGr8RP0VGbnT67 HDFwkbj7Xj+0OxNRl1RyRPpJG98vx0eB5pnsHYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX2B+ZX5Y+b fPf5BflzH5at47u70zTLCeW0aRYndHsI1/dmQqhI8Cw9sVSn/nHP8nfM/kHXdS89efIk0LTdNsZY 4lnljZiXKl5W9NnCoiKRQ7kkUxVh/wCQmv8A+Iv+cnX17iUXVJ9Vu0jPVUmildV/2KkDFWC/n9/5 OXzZ/wAxzf8AEVxV7d5OS/8AzP8A+carbyz5Xv8A6t5w8rzKfqiTG3eQRtJ6YLArRZIJiAT8PNd6 dcVecad+SP8AzkrfXf1f6tqNqA3F57nUFjjWlN6+sSw3/YBxVh/lP8qfPfnjX9b0zS41udY0hXlv 1uZuDPIswiZBI/wlyxJ+IgUB3xV6/wDkN+Uf58+VfzC06e5tJ9H8vxyMdXV7qF7eWLiQU9GOV/UY n7JC7HfFWDfndrugXX/ORF9qVhLGdPt7+xW5uYzWMyWyRJO3w/ysjA06kYq9V/5yj/KD8wvOvnrS dY8raWdUsDpcVo0sc0CBZEuJpN/VdPhKzrRumKsd/wCcXdY017bzl+VOvTiyuNfimgtQzDeZontb qJSDxL8SrKAd+JxViVr/AM4o/nBJ5kTSp9NjhsfVCS6z68LW4irvKAH9Vtui8OXtiqf/APOXPmzR rjWPL/krR5Vmt/Kts0dy6EELNKsaLESNuUccIrToWp1GyrxHy55b1PzDqBsNOVGuBG0pDtwHFSAd z/rZkabTTzS4Y82vJlEBZRFn5k1rRdP1PQoWjW3vC0V4pUMagFDxbtkoanJijLGOUuaJYoyIl3KN z5Y1W38uWvmGRU/Rt5KYIWDVfmC4NV7f3TZGWlmMQyH6ZGvv/UkZQZGPUI291fzD511XT7KYxPdg fV7UACNaHf4j9GWzzZNVOMTV8gwjCOIE9Hon5V/nZr35MTaz5du9Ii1aGeVZnhFwYGin9MDkJBHK GVk41Xj265j5sMsczGXMNsJiQsPMvOfmrUPNnmnUvMeoKiXepzGaSOOvBAaBUWpJoqgDfKmSS4q7 FXYq7FXYq7FXYq7FXYq7FXYq7FXYq+ovzv1rWNG/JD8pb/SL6406+jsLMJdWsrwygHToqgOhU0Pf FXz95g/MPz35itVtNd1+/wBStFbkLa5uJJIuQ6NwJ4kjsaYqlWk6zq+j3yX+kX1xp19GGEd3aSvB KoYcWAkjKsKg0O+KqeoajqGo3s19qFzLeXtw3Oe6uHaWWRv5ndyWY/M4qqaTrOr6PepfaTfXGn3s dQl1ayvDKAeoDoVbFWRXv5u/mlfQNb3XmzVpIW2eP65MoYeDcWFR7HFWPaZres6Ve/X9Lv7mwvhW l1bTSQy7mp+NCrfjirIL/wDNv80NQtHs7zzXqs1rKOMsLXc3F1IoVajfED3BxViWKsqsPzV/MzT9 Oj02x806pbWMKenBBFdzIsaAUCpRvhA7AdMVY091dSXTXckzvdO5le4ZiZDITyLlyeXLlvXFWUP+ bn5ovZGyfzbqzWxXgUN7Oar/ACluXKn04qxMksSSak7knqTiqra3t5Zy+raTyW8tOPqROyNQ9qqQ clCcom4mkGIPNSZmdizEszGrMdySe5yJKVZ769e1Sze4ka0jPKO3LsY1Y13VK8QfiOTM5EcNmu5H CLvqshnmglWaCRopUNUkQlWB8QRuMjGRBsc1It09xcXEzTXErzTPu8kjFmPbcmpxlIyNncqAByU8 CXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYqqyXVzLGkckrvHHtGjMSqgCmwPTFVLFXYq7FXYq7FXY q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq 7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7 FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FWV/8qn/N P/qTdc/7ht5/1TxV3/Kp/wA0/wDqTdc/7ht5/wBU8Vd/yqf80/8AqTdc/wC4bef9U8Vd/wAqn/NP /qTdc/7ht5/1TxV3/Kp/zT/6k3XP+4bef9U8Vd/yqf8ANP8A6k3XP+4bef8AVPFXf8qn/NP/AKk3 XP8AuG3n/VPFXf8AKp/zT/6k3XP+4bef9U8Vd/yqf80/+pN1z/uG3n/VPFXf8qn/ADT/AOpN1z/u G3n/AFTxV3/Kp/zT/wCpN1z/ALht5/1TxV3/ACqf80/+pN1z/uG3n/VPFXf8qn/NP/qTdc/7ht5/ 1TxV3/Kp/wA0/wDqTdc/7ht5/wBU8Vd/yqf80/8AqTdc/wC4bef9U8Vd/wAqn/NP/qTdc/7ht5/1 TxV3/Kp/zT/6k3XP+4bef9U8Vd/yqf8ANP8A6k3XP+4bef8AVPFXf8qn/NP/AKk3XP8AuG3n/VPF Xf8AKp/zT/6k3XP+4bef9U8Vd/yqf80/+pN1z/uG3n/VPFXf8qn/ADT/AOpN1z/uG3n/AFTxV3/K p/zT/wCpN1z/ALht5/1TxV3/ACqf80/+pN1z/uG3n/VPFXf8qn/NP/qTdc/7ht5/1TxV3/Kp/wA0 /wDqTdc/7ht5/wBU8Vd/yqf80/8AqTdc/wC4bef9U8Vd/wAqn/NP/qTdc/7ht5/1TxV3/Kp/zT/6 k3XP+4bef9U8Vd/yqf8ANP8A6k3XP+4bef8AVPFXf8qn/NP/AKk3XP8AuG3n/VPFXf8AKp/zT/6k 3XP+4bef9U8Vf//Z + + + + uuid:994ed755-f22a-7b43-b3d2-eeea3390a93d + xmp.did:accba3ef-92bd-43ff-92a3-11107d352ef8 + uuid:5D20892493BFDB11914A8590D31508C8 + proof:pdf + + xmp.iid:ee634e12-ada0-44d2-be1a-5ae0e84d5b55 + xmp.did:ee634e12-ada0-44d2-be1a-5ae0e84d5b55 + uuid:5D20892493BFDB11914A8590D31508C8 + proof:pdf + + + + + saved + xmp.iid:a478fe57-ac17-43dd-a456-3029de78c422 + 2013-10-23T20:23:29-07:00 + Adobe Illustrator CC (Macintosh) + / + + + saved + xmp.iid:accba3ef-92bd-43ff-92a3-11107d352ef8 + 2013-10-23T20:37:56-07:00 + Adobe Illustrator CC (Macintosh) + / + + + + Print + Document + False + False + 1 + + 6.955882 + 4.317257 + Inches + + + + Cyan + Magenta + Yellow + Black + + + + + + Default Swatch Group + 0 + + + + R=56 G=227 B=255 + PROCESS + 100.000000 + RGB + 30 + 220 + 255 + + + R=0 G=0 B=0 + PROCESS + 100.000000 + RGB + 0 + 0 + 0 + + + R=88 G=144 B=255 + PROCESS + 100.000000 + RGB + 88 + 144 + 255 + + + R=255 G=255 B=255 + PROCESS + 100.000000 + RGB + 255 + 255 + 255 + + + R=15 G=38 B=72 + PROCESS + 100.000000 + RGB + 15 + 38 + 72 + + + + + + + Adobe PDF library 10.01 + + + + + + + + + + + + + + + + + + + + + + + + + endstream endobj 3 0 obj <> endobj 7 0 obj <>/Resources<>/ExtGState<>/Properties<>>>/Thumb 12 0 R/TrimBox[0.0 0.0 500.824 310.843]/Type/Page>> endobj 8 0 obj <>stream +H‰ÔWÛŽ7 }÷WÌX+’º¾f[ô)(Š>ôŒ^²Òü?ÐCRÏØ^'qŠ4ÅÞ¡†’(òœ#ÎÓÏÏËÓÛ縼ùáy9<=ÿ—ÓÇ%Úßòñôþðô†þüxÐchœ!üO²üýûáüšÓá¸ñømy Eÿl½xе09”L}!î!6ÊËéŶy9Pèµ.G +¹Ñ’IÃ’ð᥅–e§•™§M-þq蜧AbOê:^RàRL¬¹/\C”ºô ܦq:ä 5O“SZX£èŽ%P8ýuè¡EÄÒBÂJ/˜UújRh'À.]g3Vi˜…GDÐpêw¼-‰®„BÁ¡`SˆˆëlÇ<°RÔŸ‚$T#Â&Øžd ÿ~®mZ§ÃØbØœP%֣ĶZš(*êë66ØC%žÇÚù„êÿ²-hÖJµ*šõYÐc +‰‹®Ù9æwÁ2ŒîÆðu'ÿ5éÒQõXWu÷¸úø¨;ê3uoÆÝšvÑ8`E?—w¡3¤¬-ó:Y ôµÎÈõ~e»»†á®îã¿xÑP»ö;GsQø¨ùùrþ{+졤E¤\CN» %yßçyg`zJŧ…­_¸‡û°Ô}ºù¯uºÆðë+®^>lóýÑ·<Ç®ÖìN”è‡TâJG¹ºv‚²Gvõϸ„Ûh-š[Ùº';O5íÎn _wò_¾ŽºÇX¼¹›>>ªŽuñ|ä&€p§á j3‘|Aܶ#n»K\ÙWî·m‰Û¾Ž¸¼#zu¹Í]Þq—?‡»íßã.ßš“ª¶œ Aë¸ýïp—û}ò¶ y¹ßb/÷×é[·ôµfä>ñ‘ƒvZ{Ò„³Ö]Ö7 –ƒeRó>éƒóg0˜y/)Lú©vE]|„°ÄlÔM±Ô~¤âíÚYk×ÜJC’-BäWZÓ ‚‹¤ÀÔÿ›îÇ0ùPø4¤–)´Ì¯(¨ì/ßGâWÌXèT¾=fäѶm[ÌhGèt7ë嫳þнõZÇŒF³¥ò¿‚ûoŸüûG€å`áï endstream endobj 12 0 obj <>stream +8;Z\q9+HIp$jGO^E,SgJJQ36=J[pg&A#P4q79??"&t6VC0)uiDnHCY!#gDnKT=2]D +#RIcNN8L4\(IQe'r%bc5=(4Jaa"cKWe$XK4;nV8UZt1PQC\Vf`m4V.!H.[oipB,eitf`qNABo2Z~> endstream endobj 13 0 obj [/Indexed/DeviceRGB 255 14 0 R] endobj 14 0 obj <>stream +8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0 +b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup` +E1r!/,*0[*9.aFIR2&b-C#soRZ7Dl%MLY\.?d>Mn +6%Q2oYfNRF$$+ON<+]RUJmC0InDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j$XKrcYp0n+Xl_nU*O( +l[$6Nn+Z_Nq0]s7hs]`XX1nZ8&94a\~> endstream endobj 5 0 obj <> endobj 15 0 obj [/View/Design] endobj 16 0 obj <>>> endobj 11 0 obj <> endobj 10 0 obj [/ICCBased 17 0 R] endobj 17 0 obj <>stream +H‰œ–yTSwÇoÉž•°Ãc [€°5la‘QIBHØADED„ª•2ÖmtFOE.®c­Ö}êÒõ0êè8´׎8GNg¦Óïï÷9÷wïïÝß½÷ó '¥ªµÕ0 Ö ÏJŒÅb¤  + 2y­.-;!à’ÆK°ZÜ ü‹ž^i½"LÊÀ0ðÿ‰-×é @8(”µrœ;q®ª7èLöœy¥•&†Qëñq¶4±jž½ç|æ9ÚÄ +V³)gB£0ñiœWו8#©8wÕ©•õ8_Å٥ʨQãüÜ«QÊj@é&»A)/ÇÙgº>'K‚óÈtÕ;\ú” Ó¥$ÕºF½ZUnÀÜå˜(4TŒ%)ë«”ƒ0C&¯”阤Z£“i˜¿óœ8¦Úbx‘ƒE¡ÁÁBÑ;…ú¯›¿P¦ÞÎӓ̹žAü om?çW= +€x¯Íú·¶Ò-Œ¯Àòæ[›Ëû0ñ¾¾øÎ}ø¦y)7ta¾¾õõõ>j¥ÜÇTÐ7úŸ¿@ï¼ÏÇtÜ›ò`qÊ2™±Ê€™ê&¯®ª6ê±ZL®Ä„?â_øóyxg)Ë”z¥ÈçL­UáíÖ*ÔuµSkÿSeØO4?׸¸c¯¯Ø°.òò· åÒR´ ßÞô-•’2ð5ßáÞüÜÏ ú÷Sá>Ó£V­š‹“då`r£¾n~ÏôY &à+`œ;ÂA4ˆÉ 䀰ÈA9Ð=¨- t°lÃ`;»Á~pŒƒÁ ðGp| ®[`Lƒ‡`<¯ "A ˆ YA+äùCb(Š‡R¡,¨*T2B-Ð +¨ꇆ¡Ðnè÷ÐQètº}MA ï —0Óal»Á¾°ŽSàx ¬‚kà&¸^Á£ð>ø0|>_ƒ'á‡ð,ÂG!"F$H:Rˆ”!z¤éF‘Qd?r 9‹\A&‘GÈ ”ˆrQ ¢áhš‹ÊÑ´íE‡Ñ]èaô4zBgÐ×Á–àE#H ‹*B=¡‹0HØIøˆp†p0MxJ$ùD1„˜D, V›‰½Ä­ÄÄãÄKÄ»ÄY‰dEò"EÒI2’ÔEÚBÚGúŒt™4MzN¦‘Èþär!YKî ’÷?%_&ß#¿¢°(®”0J:EAi¤ôQÆ(Ç()Ó”WT6U@ æP+¨íÔ!ê~êêmêæD ¥eÒÔ´å´!ÚïhŸÓ¦h/èº']B/¢éëèÒÓ¿¢?a0nŒhF!ÃÀXÇØÍ8ÅøšñÜŒkæc&5S˜µ™˜6»lö˜Iaº2c˜K™MÌAæ!æEæ#…åÆ’°d¬VÖë(ëk–Íe‹Øél »—½‡}Ž}ŸCâ¸qâ9 +N'çÎ)Î].ÂuæJ¸rî +î÷ wšGä xR^¯‡÷[ÞoÆœchžgÞ`>bþ‰ù$á»ñ¥ü*~ÿ ÿ:ÿ¥…EŒ…ÒbÅ~‹ËÏ,m,£-•–Ý–,¯Y¾´Â¬â­*­6X[ݱF­=­3­ë­·YŸ±~dó ·‘ÛtÛ´¹i ÛzÚfÙ6Û~`{ÁvÖÎÞ.ÑNg·Åî”Ý#{¾}´}…ý€ý§ö¸‘j‡‡ÏþŠ™c1X6„Æfm“Ž;'_9 œr:œ8Ýq¦:‹ËœœO:ϸ8¸¤¹´¸ìu¹éJq»–»nv=ëúÌMà–ï¶ÊmÜí¾ÀR 4 ö +n»3Ü£ÜkÜGݯz=Ä•[=¾ô„=ƒ<Ë=GTB(É/ÙSòƒ,]6*›-•–¾W:#—È7Ë*¢ŠÊe¿ò^YDYÙ}U„j£êAyTù`ù#µD=¬þ¶"©b{ųÊôÊ+¬Ê¯: !kJ4Gµm¥ötµ}uCõ%—®K7YV³©fFŸ¢ßY Õ.©=bàá?SŒîÆ•Æ©ºÈº‘ºçõyõ‡Ø Ú† žkï5%4ý¦m–7Ÿlqlio™Z³lG+ÔZÚz²Í¹­³mzyâò]íÔöÊö?uøuôw|¿"űN»ÎåwW&®ÜÛe֥ﺱ*|ÕöÕèjõê‰5k¶¬yÝ­èþ¢Ç¯g°ç‡^yïkEk‡Öþ¸®lÝD_p߶õÄõÚõ×7DmØÕÏîoê¿»1mãál {àûMśΠnßLÝlÜ<9”úO¤[þ˜¸™$™™üšhšÕ›B›¯œœ‰œ÷dÒž@ž®ŸŸ‹Ÿú i Ø¡G¡¶¢&¢–££v£æ¤V¤Ç¥8¥©¦¦‹¦ý§n§à¨R¨Ä©7©©ªª««u«é¬\¬Ð­D­¸®-®¡¯¯‹°°u°ê±`±Ö²K²Â³8³®´%´œµµŠ¶¶y¶ð·h·à¸Y¸Ñ¹J¹Âº;ºµ».»§¼!¼›½½¾ +¾„¾ÿ¿z¿õÀpÀìÁgÁãÂ_ÂÛÃXÃÔÄQÄÎÅKÅÈÆFÆÃÇAÇ¿È=ȼÉ:ɹÊ8Ê·Ë6˶Ì5̵Í5͵Î6ζÏ7ϸÐ9кÑ<ѾÒ?ÒÁÓDÓÆÔIÔËÕNÕÑÖUÖØ×\×àØdØèÙlÙñÚvÚûÛ€ÜÜŠÝÝ–ÞÞ¢ß)߯à6à½áDáÌâSâÛãcãëäsäüå„æ æ–çç©è2è¼éFéÐê[êåëpëûì†ííœî(î´ï@ïÌðXðåñrñÿòŒóó§ô4ôÂõPõÞömöû÷Šøø¨ù8ùÇúWúçûwüü˜ý)ýºþKþÜÿmÿÿ ÷„óû endstream endobj 9 0 obj <> endobj 18 0 obj <> endobj 19 0 obj <>stream +%!PS-Adobe-3.0 %%Creator: Adobe Illustrator(R) 17.0 %%AI8_CreatorVersion: 17.0.0 %%For: (Brent Couchman) () %%Title: (FB_Presto_Logo_DarkBlueBG.svg) %%CreationDate: 10/23/13 8:37 PM %%Canvassize: 16383 %%BoundingBox: 0 -612 501 -301 %%HiResBoundingBox: 0 -612 500.823529411764 -301.157476056923 %%DocumentProcessColors: Cyan Magenta Yellow Black %AI5_FileFormat 13.0 %AI12_BuildNumber: 256 %AI3_ColorUsage: Color %AI7_ImageSettings: 0 %%RGBProcessColor: 0 0 0 (R=0 G=0 B=0) %%+ 0.059415001422167 0.150419995188713 0.282377988100052 (R=15 G=38 B=72) %%+ 1 1 1 (R=255 G=255 B=255) %%+ 0.117647059261799 0.862745106220245 1 (R=56 G=227 B=255) %%+ 0.345098048448563 0.564705908298492 1 (R=88 G=144 B=255) %%+ 0 0 0 ([Registration]) %AI3_Cropmarks: 0 -612 500.823529411764 -301.157476056923 %AI3_TemplateBox: 396.5 -306.5 396.5 -306.5 %AI3_TileBox: -127.588235294118 -744.578738028462 606.411764705881 -168.578738028462 %AI3_DocumentPreview: None %AI5_ArtSize: 14400 14400 %AI5_RulerUnits: 0 %AI9_ColorModel: 1 %AI5_ArtFlags: 0 0 0 1 0 0 1 0 0 %AI5_TargetResolution: 800 %AI5_NumLayers: 1 %AI9_OpenToView: -188.5 -218 2 1624 1073 18 0 0 46 64 0 0 0 1 1 0 1 1 0 1 %AI5_OpenViewLayers: 7 %%PageOrigin:90 -702 %AI7_GridSettings: 72 8 72 8 1 0 0.800000011920929 0.800000011920929 0.800000011920929 0.899999976158142 0.899999976158142 0.899999976158142 %AI9_Flatten: 1 %AI12_CMSettings: 00.MS %%EndComments endstream endobj 20 0 obj <>stream +%%BoundingBox: 0 -612 501 -301 %%HiResBoundingBox: 0 -612 500.823529411764 -301.157476056923 %AI7_Thumbnail: 128 80 8 %%BeginData: 4004 Hex Bytes %0000330000660000990000CC0033000033330033660033990033CC0033FF %0066000066330066660066990066CC0066FF009900009933009966009999 %0099CC0099FF00CC0000CC3300CC6600CC9900CCCC00CCFF00FF3300FF66 %00FF9900FFCC3300003300333300663300993300CC3300FF333300333333 %3333663333993333CC3333FF3366003366333366663366993366CC3366FF %3399003399333399663399993399CC3399FF33CC0033CC3333CC6633CC99 %33CCCC33CCFF33FF0033FF3333FF6633FF9933FFCC33FFFF660000660033 %6600666600996600CC6600FF6633006633336633666633996633CC6633FF %6666006666336666666666996666CC6666FF669900669933669966669999 %6699CC6699FF66CC0066CC3366CC6666CC9966CCCC66CCFF66FF0066FF33 %66FF6666FF9966FFCC66FFFF9900009900339900669900999900CC9900FF %9933009933339933669933999933CC9933FF996600996633996666996699 %9966CC9966FF9999009999339999669999999999CC9999FF99CC0099CC33 %99CC6699CC9999CCCC99CCFF99FF0099FF3399FF6699FF9999FFCC99FFFF %CC0000CC0033CC0066CC0099CC00CCCC00FFCC3300CC3333CC3366CC3399 %CC33CCCC33FFCC6600CC6633CC6666CC6699CC66CCCC66FFCC9900CC9933 %CC9966CC9999CC99CCCC99FFCCCC00CCCC33CCCC66CCCC99CCCCCCCCCCFF %CCFF00CCFF33CCFF66CCFF99CCFFCCCCFFFFFF0033FF0066FF0099FF00CC %FF3300FF3333FF3366FF3399FF33CCFF33FFFF6600FF6633FF6666FF6699 %FF66CCFF66FFFF9900FF9933FF9966FF9999FF99CCFF99FFFFCC00FFCC33 %FFCC66FFCC99FFCCCCFFCCFFFFFF33FFFF66FFFF99FFFFCC110000001100 %000011111111220000002200000022222222440000004400000044444444 %550000005500000055555555770000007700000077777777880000008800 %000088888888AA000000AA000000AAAAAAAABB000000BB000000BBBBBBBB %DD000000DD000000DDDDDDDDEE000000EE000000EEEEEEEE0000000000FF %00FF0000FFFFFF0000FF00FFFFFF00FFFFFF %524C45FDFCF8FDFCF8FDFCF8FDFCF8FDFCF8FDFCF8FDFCF8FDFCF8FDFCF8 %FDFCF8FDFCF8FDFCF8FDFCF8FDFCF8FD07F827FD05F805FD05F82700FD71 %F852FF27F8F8F8054335FD04F85485FD72F852FD05F82E05FD04F82828FD %75F827FD05F82700FD04F8270527FD70F852FF52FD04F83D3CFD04F8305B %00FD70F8277D27FD04F8360CFD04F82F3027F8F8F800FD70F827FD0BF827 %2805FD70F827FFA8FD04F83C43F8F8F8055B8530FD58F82727FD17F8A852 %FD04F83636FD04F85B5B54FD57F827FF7DFD20F8280CF8002FF8272927FD %54F852FF7DFD1AF8A8A8F8F8F8053D430CF8F8F8305B3000FD30F852A852 %7DFFA87DF8F8F827A85227A87DF8F852A8FFA87DFD04F852FFFFFF7D27F8 %A8A8FFA8A8A852F8F827A8FFFF7D27FD0FF8FFA8F8F8F805434336F8F8F8 %5B5B5BFD31F852FFFFFF7DA8FFA8F8F827FFA8FFFFA8F852FFFF52A8FFA8 %F8F852FFA87D7DFF27F87DA8FFFFA8A827F827FFFF7D7DFFA827FD0FF827 %F827F8F8052FF80506F8005406FD31F87DFF7DF8F8F8A8FF52F827FFFF7D %27F8F8FFFFF8F8F87DFF52F8A8FF27F8F8F827F8F827FF52FD04F8FFFF27 %F8F827FF7DFD0AF8272928F8F8F827FFFF52F8F8273D6736FD34F852FF27 %F8F8F827FF52F827FF7DF8F8F827FF7DFD04F8FF7DF8A8FF27FD06F827FF %7DF8F8F827FF7DFD04F8A8FFFD0BF85B28F8F8F852FFFF7DF8F80543433D %FD34F87DFF52F8F8F852FF7DF827FFA8F8F8F852FFA827522752FF7DF87D %FFFF7D52FD04F827FF52F8F8F852FF52FD04F8A8FF27FD0BF800F8F8F827 %7DA8F82727F80D3D0CFD34F852FF27F8F8F827FF7DF8F8FF7DF8F8F827FD %07FF7DF8F852A8FFFFFF27F8F852FF7DF8F8F852FF52FD04F87DFFFD07F8 %000605FD04F80528F8F8F827A8FF52FD37F87DFF52F8F8F852FF7DF827FF %A8F8F8F852FFA8275227522727FD04F8277DFFFFF8F827FF52F8F8F852FF %52FD04F8A8FF27FD06F8058528FD04F85B30F8F8F852FFFFFFFD37F852FF %27F8F8F852FF52F8F8FF7DF8F8F827FF7DFD0CF8A8FF27F852FF7DF8F8F8 %27FFA8FD04F8A8FFFD08F80005FD04F80005F8F8F827A8FF52FD37F87DFF %7DF8F8F8FFFF27F827FFA8FD04F8A8FF27F8F8F82727F85227F8F827A8FF %F8F827FF7DFD04F8A8FF52F8F852FF7DFD04F8050527FD04F80527FD04F8 %0028FD3BF852FFFFFFA8FFFF7DF8F827FFA8FD04F827A8FFA8A8FFFF27F8 %7DFFA8FFFFFF52F8F8F8A8FFA8FF27F8F8FFFFFFA8FFA8FD05F8065B00FD %04F85B28FD04F85430FD3BF87DFF5252A87D52F8F8F8275252FD05F82752 %A87D7D5227F82752A87D7D27FD04F8277DA87D27F8F8F87D7DA85227FD05 %F8272827F8F8F8272827FD04F80528FD3BF852FF27FD7DF87DFF52FD7DF8 %52FF27FD7DF8272727FDFCF8FDFCF8FDFCF8FDFCF8FDFCF8FDFCF8FDFCF8 %FDFCF8FDFCF8FDFCF8FDFCF8FDFCF8FDFCF8FD99F8527D527D527D527D52 %7D527D527D527D527D527D527D527D527D527D527D527D527D527D527D52 %7D527D527D527D527D527D527D527D527D527D527D527D527D527D527D52 %7D527D527D527D527D527D527D527D527D527D527D527D527D527D527D52 %7D527D527D527D527D527D527D527D527D527D527D527D527D527D527D %%EndData endstream endobj 21 0 obj <>stream +Ðñ¿=_VW›ø”/ÛMè¥cÂ|Z_z©ó¥mä¡ÑšÚ=à‘ª§™Ñh^a>G—Å1†WZLîQ’ˆCÛ xŠ* €¼€ž-± +=&!z #ZÖË´¨,²’¬¾L° ŒYÐ℆eAb–‚ S@hbÚHè³8„¬dB Ûb #AˆcYe¢3†°ÁÓë|ÏDòºÂл€¢4¾„‰‹†X¼Ežb €µ/cZ‘ž °xàõ–ÒÆOâ(D õˆÄ|Dã¬a†Ë ÂãX2¶Æ·”Jr.`H¤ìÓ A´¬ŒC-Â[+JL€²l)IEÕbEëC-×½¼7…¤ûˆCp9 w "0ØÓàÄÌ™\ çtœ0²ƒ£Í`éŒ[:ZÎó,¹òÚ‘0ˆ¸yb2(,yW¨†&!4ùCè…'Ö¬™›vÆ3 _Œ¶A}–u´´Ì[X³HäM 48‘b­yñU±G'¢Måñ€M¢zá,¡‚ûƾô‚WÆž˜¢Øb c‹à>»‡HÒùpöWâÑÅ£4©íÜ#¶ôN›³»€—ÑQ”§¡3À´V­gVš}i} +kXgjb œ&™½¥BÿXao"m˜ˆß¤˜·±‹ßDŒJ´otmd`åÓø ç5ãŽç‡lY�_l1&m‰V4Ž^Ÿpë@Üܯd&²ÐüJu…v —ðø b#ÄC¬ +qì˜<‘d%ý­ãˆe¦,œ”.ÃŽHxÀ  Â‘ðåýÂè¡ž±’¥\È÷QLà‹‰#Íú¾ÇÊ |Úž/*]îöÖ4N ü§ÄóL¨XÉ SD{²ÅV›HRÍE´ùa +"Zô§nÎ3-Öá._š¸¯™¹ÁS}4Dv]̘ȃȪSÌ3Å„¢ŸÙ’ßäu& +Á­(”ž¤p!E`1ýíѾáwßcÛŸŒVßÇ‚k‘µ.Á:àÝ¢g*¶š9X‚EH¶ŠÒp ‹£ŒˆŽUÄÌ‚5;‚ÄŠðL‹ª‡¿Yîfyñb0Á“h¯àËTbò+˜Íð{“àŽˆ^±*‘mÂŽº@Ö‰yýõ„RàáõšW~Â.@Ú3(ˆ´9ÄÙã—hØ +ì{x'‰4/fòÊ«8àÑÁÅ\qÃÄá:fGJq]r ³§£D âŽLÆ5p_¢cöQhüì¢ÜbW'¯•‚¯<À +°N È«Ù7fT¯`nì$%ÃîT­y ð¹8Š®XN%‡?ÜÉ… @‹°A>²PG¬ÁW| m  +ðøôK¤OªPˆXLheÏæßâ}Bˆú™3ñY,lY²¢IGMhKq12ͳ%II†o"ž{¥u“Õ$ØD‹× úˆfíV‰r‰ûÚˆöË{È&aå‘]âÌa+a/Á³ÀXi‹‰ZñJ­Eû ˜=óÌã54¸²L€iq(Ä©Þ •"Y¥PW9–X7°Ç,åNÄrÁã¡Ò’·ˆíì •¢G +;ÄAd#`°äÑ (%þ<’OûÓBç‘ò¬#‚¶Ãgs·[¤ð|8è³ xú‚ìÙˆxDFûþ†p—¢K@„Rª£*͵>TZÒŠ•Vµ>Ô"YŽVæâ™õ!‡DÐX‰a&ªÅû“R¾BA~1¢Èóx½À>4ûy7T•bGåøóBô +Zßg™Dtå“T#)E\&`¾ÖKSŽ‰iÃO²u S8,Ë2/+§øIŽÏ‚­„à'¤¯±@hŒWù²˜…-ÜÂ1¡á +œÔõ±=OY‡§ˆ|ÄOés¬IôâìÎ$Uˆb”äÑ»JX93TÂÝZküέܻ¡rô®ß+ÆyãÅœ'”õÁë4˜·„(æÎÛ(f'6‘RqR褘c~PÞÃ{¥ sKͶ1öpv…´ì3Ã4K{Õ D;±—q +xÙZbLi¬µVAÃRh±Ö2ü˜Q¾ƒÖc÷Àgo: sqrÑ{UÈ2³C*mø´”¯À1ÿbVC9󡪟iÎÏÇüßÁqé±K‡Ó‰^Û°/ uÍd’¤á ¨Öu™H¼y™PòBëˆCdWÁÙ¸5Géű°àÀªp¸\~ò9®£dMh=x'¯âl¹ÂW¨,£S»´!Ô¢_XVÃìí6œ_J0íÛ”6-:”òŒÄ…HÁ¡b@˜B9SÆJá *ögqp`;°iˆ»'ÓD »]<Ô¦ÒEFIaiÎœR\™òê•Ö¸Ž ´8Ǹa,ƒ´ájv°^ÇWü‚† B‰q Ÿ„ãæJl{VÇTÕî±/ê C”}U,ñŸ$MZQ@®DR2˜â¥]éŠmÌ66ù"õcn ¿ö­#›–ƒ<Ã,6i>8åÆÀ +>g¬P JfkqYˆ@/絤¤ãrÂf ³d8›EU?­¾r¶„CFÆyû kå­µX÷òÞ7°ÎñÛ@[ü!¶,y/H1²œÚæLsNCŒ$ÙÁ7À« cC°£˜pón¨EZN9u§”ÃX;ÅÎòÖØE‹!PøI¥ýìNòE)»å¬W|sœ;ã{¬}HJ@ÄLÞ"YñJ\ʈ$eh–ìü±R•TQtZcY \,¡k§gZ ~i5Y8$œñ Vĺ(Lð0½ØˆRSJiªµH|*¥Fӧ굫â‰>•Æ±Ùz5FÒµDÕCÄù3gwV|)7ÿÝP‹þRŠ1ݨ>TÎI*§-µHnÚ*m0ãDÊRzP}¨E.U‹|«bVÖLËÜ­b2Ë»¡–9/¥¼˜bòŒ¬J!!¨œ4Ô*µ(¶ª ÁtŒÐ;A4C"ÑP¸.S«1lz€¬Ù!‚°“Ôp\†]N‰Ð¬ËìˆØíÊÉ¿ðwÉš‹‹ÎKyB BìBZ«@qš= +Œ*k Ýcõ ð%ïàho `oU$P†Àt$ˆM¬òd+¬ž(GÂû“Ḓ$…¸#L·Žï{áG79HaƒË8P”úPËTµb:[)­I„Y!÷©œUÈ¡ƃ`uñb!wØT0«åΞÁÊfø§Fþ.½¹KlXÁ»XÇËS!-§aߌ cƒÇžC4컋¬Þ¨S»Þô„}D J‰Ó3/]K9d³ é*I,) [Ó´âb1g Ñú„F܈à«ïö'²‘%èÈž ,;yਖP2û< p¦I•ÒfññY7<6dcHnbÄóe|xs(tÑ!ã>'¹P·dIsF¢áVh³¡Îs–±…ý^˜ßÒÒ¦CòQ=âÏH+c…üTùÛfO¶a•#F’xHrž!b ÓêDÆå3ïJ©y…ô=¿ é‘Ô»ì‹ò;mÅ›„܃X“Ž8y0@†µaÖš°Ì&|‘Ù¬ÒuùŒ¯¸"+»‹4Eü Zž/á¤:›+¢L ©òˆ7Ó9 ³U5¦‹KýƒPœú'á‘b. °›l²`)—°ù&¬¦WÊŸ+äØÕ9ÇÎ؛ؔT9Þ +ÝÝ‹lÒs$™zœ>5ÃÂGƒDª˜ý~ÉDHJ` dK´³<$5¶É5¤6ñq;èO¬xº° +ëB#F2B€ †’æŒQ£;¶^«|¿R&\!§ìÝP1묔•VÌÇb³ ê7':ÉɹÐ:P ™Ð¢c,RXVÑc?ø‰u3öðDBðP¸9:Ï"¬ºb2´úkhÏ@±ƒ'à³ÐH<]L +–¬ôpÅr=äôà8£tyƒÙò* AK +x$dÀ³°nov9+Ái{K!‘Rñ +É–¥tÌÂvGΑšÍn¦Èr³#>¹Ã¦SÔôþÄÏG¬ž—¬Õîå÷·„E$aÌ2lOÇÖ°bTƒ«'µŠ9…Ì y‡¥ÌÄBò¢ìŸpXˆƒTXp IX6Ǔ愲ʼ 9ä'Â#VÓ6 ¸ûÿH5…®¥7±RœTƒLeV¤8êŒŒÓ ^q8¦ ßDe€8oén¬÷„E’o9<šžh$phœR'ç›ÙÖå¿Ie3qùíì†+Œ°4‡â<ÅÇ>q>BÍpˆAëª(«$X%GMb p‰9àL®2¥äGöªò#K”-“,-óqÖ½x A\<åâ2]¼Áƒ,5VîIñg±† t lK±ª¼ò·âCóHc`ÒJl Ç=” t‚çØDpiÈU¬3·Hâ,åy–2A‹Ù¢õ¦óôH„ÿ´M^ÃcES¤_$tâ±pñ„f"ÎÎ …+ Ë0FR'®‡@Ä6[Wà±=Z)­UW˜`)Ç•ðÎÀƒî˜±H"³è¼Øw’å¼ð¤¿ÉrøâÄNăˆ™ñj$¢€•fZjµÅõÊ/)b½â#àìmT[°¡sÄÆŒ/z$FŒ³’ Ÿ ª’y…X*ȹ ûn¨œÀ›Ëïm{±êN9;6±œ JkÌ ‚Õ^è¸ ŸX‚™²²lÍ“¨b' m!§#«³B™E¬#°À0M¯ +lðŽ$û"}ëÀ„ÑQ«,ãBr1K¹˜Ç\§Yh·ôŸÀ§]C*+Í*.eðÖ‡Zåøs€³YÂ3¥<ââZ¾ëÅ!§‡]|ÞGi¾‡qK'Þ× ¥´Úâþ÷·ˆÈ”„Ež>MÄÉ +ÇÙ”*%3ׇŠéÎÅlè|¶t)|1“oÄ9í’èq¾‘ïKÁh|Ì‚I‰Ã”œ~DÌ¢”cÌ9ù,äb–r!™ñƒ­æØÊE(üø/pÏšaÌ B° 9 }pJŸäq°6‡,b`ófŠwñGy‹YÀ@Ù!°†=èñH/‚#áLh.m0°î¼™¡ $Ü 8ìä¦Yœ6²í2«RX±â’Ö9¡˜»4bmÓî=VŽø´.¶( +EhƒYýZ÷H!OùÝP)‘9Ÿæ\Hƒž)-R7²ðÁô8³ÂX—Ä’Ž$Õ@6¯Grõ~m‘ÔUÌû*%†•’ÇêÄl*¤bã±b\þGe”ÏùÜ’‹‚SiR0[ø1‚ +¹L”…|§bBT‹œ)-y/l¾2ë¢9sÂ3âI˜)-Dìõó!U™EnMÑÂ*²‚ˆs…’¾V CzéÜ€€_¤¼ƒîBv‡û‰¡ßH±ˆ&*l§ø´*îªÊmêO”*òcvxã¤oœT\Ú¾Çöt—ÓÀëCå\ñéä…ä^ÜUH.¥—ÒˆëÈ7‡ºoÍ6` ˜¸©”¤YJã,d?r6\>C²”CYBg CãK$QÀܲ§Œ3{¡–€ÑoØ2ˆR~îɧ–r[%¶J7,§$’ÂêC-RÇÊéeÅ4Î\³±4 b€]@)~Ï.â[Š Hå$¥bâŒØ7‚IÆÆ•Ùöä²á,lÓ"×…åbFL9k¦”[#ùLìC5ŽDò`”g#ÔâJ)%Fð]åì‰r†E!B.‰…8z«X{>Èž”R´°eL1y´j¬tD9EíNc}¤†³á»r°®Ð+‡ýJ±ÁRô01’—ñ:kë¥å1r‰ƒÃ¥èˆÜRŒ C,ù( ßC‰8[¨Ë×°•í»£ëÅ ïW!pÐ"´÷1óMÂ%,'|ÈÄXý·èå[ +Ô¢‹µàÆc„/xúZøó¾)¦®„]ä‘=—Ê~)pˆ‹™²SƦFæ7EÏNÁ÷7^ù nþnék}¨è!9jéE)xZJîðF-Œ p¥ÔXÏòNXëy¿AѯP°­Á¶KçR çVó6'¤PÞ*-Z­EË ÚQÞ¶*˜^ ¿Î*?Ð]ìGSqYÅNõëúPY/jèyõ”„ÜY¿âYÀV§‹ç ‹ç sùÀ +„rR,ð£%ù–RÆbƼ~„ÜÝë›CPÞ¦Þ y¬Á‘ž!E&5Ìít4"ö 6®ÇÆù¤.pš‰Ç–½ ^…7FMŸˆá“—y•ƒP_ +›Kâ•RV€àôM ËÀ=ÍÇjààæà‘^œßb4âBÀ¡ñËœpåœoß&Ú†rBýGw’*²‘NTÄ£9…º»ª¬iò ]Ãækœ8a) SQSÓ5‡2¥Â+‘8qd}5ö¢sê““4 ŠM('þ"œŽa#Ø‹DšÚPš~ØÓ—µ&Cö­‘Ì Ø +ä(û +ÙlwK)W%²ƒž( ³zÎ.€nx¡däꪱe˜|E¥;9ÿ9IØÊպ㩋ÂÈi\ž’õ‘CtÌ­C^kd8‰_ŸµÅÛÖ\\\€;`WgÖBÍáB3Ø:Ú¾×óv(Ê}sÃ"EÆ“‚Ü€Ýs +3’㤜±š{±Ç[ÁÕšˆÛÐrðé\âŽ1)9ñ ” ¯ Îñ$v%EÆf}:^hO¤É9L·ªà[Ž‹rT0–—{N5h&Õ¢ž¦ÏŽ{‘o9{J +h®ªƒ ³[CTdyÊ°²gº3Ëê@í2 äFœ>¨0'^W– +ÝF`IbC±Qу¸¦³~°¶\jÍËGÖI¶‡(…MPhUäÌéUéók-Þ òöÙ• 7›;:MòÔñŸZz 3à*Ç O6!Æ*Í»Ü5AzÊ«ø.ñˆÑE1ã‹âô§#°L-ö·Ïõ|[ö2-ðPS‰3“¹Áýì[+¿ ¯‡Ù¬ä8gæ$¡)sB¦Z*%Ý•pïÆ*´§Å …±!¡²77¯KßRkùn“vÌ3â¹xÎHqú ëõÐøj™ëàt”£¯x§­B˜ID${«»Ì½¡Öâ­59ìblʉì@ÉY—D˜™²NœZæ2m3X’†š‰9#º “ܽéuöµò[íP|.š¡í4x(rêI”+EéU¨Sj5—€øYo—3s£»J7W©øF; ©bí‹ÿÂ88=Ê~‘Ô<>og!ÃÅ[‡ËÏ—l(ºÛ›_ÜO^ó+[ó±œM± ÒZ½fú%­ÇÕÍct#Ù­¼:ÝÙ=hŸU}&ÁÉ*¹O¿b2”"{Št6îá@JPEÇ,Iø·Ðó|û.¹–´¬“iIá‘0˜¨4W ï¶\G†Úãì;H›zvÄï°7Á)ßñË CHí‚á¬L‚O”T¦ë™—!‘€b¤"Ó`àqý™¦5–L ¬‡ôuÔúѽ ¥â{Èc¯Î¿ .\N13DyÄÕCÒó>äE!^„œ8Ò“B$q^ôóì–/›ÓËœñ¡B'ÃnAAŸžþR¥òTù±©RÓCqª! ɾÆB$=㞲ѽUèwHB# f” ’àQ÷’ÝM‰úË£ÏXÈs¸]úv:ÜÞ^Ðfæú¡ *o|ÔD¹£Ðá·ò:B'4^_űâj™a—XÂYjÝÏ5³íŽöÃÉiHͤw…ö*T¶õ(B©„Œÿõ ƒRÅ®‰éÄtº]ÂÒâHg¤ ™ tñM†¸„=E£Qц{YÏô.²ïý€ßõs¡†¼Žu‚"SM¹Â9’dÄ£fyƒâÿ!;¯€Ë)ý0 lec]‡™ì¯¹?†D´áÌÿ`iV<+\-¬ù#nb·GúWóer!g^à¡^ a~ü†áŸxÓðOqøGg6üSëQ\½™¡yÉLJ9[ ›£”³º‘æ~.þù“×–†žûÙ2jíµx_i¸4Çd‹€˘íâæü©×üôjrÙfRZÄ™òºÖ$¤È±j˜Eë\h¤p¬¬‹;C!R爧±"2áÈáìX¤E3@Ƭ’T¿’¯Þ+æd‚T›Ÿ¨òp*Onvneù2Ké·´Îœ/îÕíVoBß?õ+Û6yº¯Óé_Ùþ°¸²} =eêøäø§<,^edúää.¼£üµ³³ÆéñÜ1šµ/œìJçØ+Û®±mö¢oßœœžÝ·MQ;wÕ¬žÔoßVÖõ³ãW‡EO×±§†[z]"‚’®š‹t*`©HrÜ·>I¾>aáPUé9÷æŸB\Ø ,Ç• Î2ßÃ$#’™Kz±â`ɇ¼+–wA˜ãÌñÓiËÐd‚¬>ÿ@º‚¢‡…E|À¼ñ‚x£0Ç ä|ÇÌ’é…ª? +wüù^²ßd­è:ø9ù•/üÊ šÖµàN4½oÀ¿üë#ð/ç•šÞo­æ%×2n«&'#Ü/C„Ý-@5¾ßË)$»'x%LSD/Ê4+¬t´vP}€êeÆ„m-œ¬. ëáöXIø[°þ–Òz¸$h>àu>Tþåªà\C„A•äµöb)ðçG>Ɔ†c4?Óä +¾~øÆû q•sXÐëÀLBqà·´–Ø?ËJ*6žŠÊHÁÙŠh8ñ˜ðù8®²häèY&&ôØeéñ‘z H=^D墅$ddÏfO˜RÁ’Kê#ÇY3?±!…ìùßjŽˆœC÷=?ÿ‰÷Ö¬3¾x Žþ¤CkñüÌï–ÆI˜ #d.à.©qîö€B¹ìÓÒÏ™ +ÉüÞjòùŸ‹Ï­<²üºf~œ)nInÎüÜLÔ ÷3©ycv@…Ÿr3ÍþÖjs?ç‚!ÅÑüØÞÎüÔÆgâŽý–£›EhÑ— G)¹³pÔòËìÄÙ©ívÀ‚jYÐpænljBRrEE¦'½Êë/_ž·v%#æÒÅ£úS­=Ù¨ [ù¦|ÙÌý¸—Âa7éês·$€p ‡Fí6ÆœŠ3‹RLÅ'®Š€rJéŽh¤~sZ'š}zœt @UÃEU|Ô¼U¢ôˆÖž¾í¼Œd‚Pó)8®¬QçF}:Fz®@|>}ÂÅùô¶æ’쨂Šh8õ ÀzÄÔyfÑQ$µÆ«Š*8h4ên¤#q(Hó18î:*µ6<ƒJŠS`ùL´Ô¸ãU‹9²-§¢B^5N—¡W—~ b-Åß!— ÏÛs¹\ÄÝ‘©El(ö¸ §$ÊbÙt(½$mû¿Ii¶–ºêúÈâ•ì[pù8+ê‘«9®ä@=ðîãàj,µò-p6ÙVÐ匲ÆIyÛtʼn„œ|­mñ<ÔH ©Éq9®uˆÆ¶ó}íš3DRJ€6E÷°F>¯cûPkIéW§jµk•‘³ÓƒãW•«û;oÓ‡ãÝå³úþý“éÆÞÉ©%æ«Ó-¯$Òß¿3k/¹û£—4™ÂUtdžg®žžª×ÏÖOÎv0¼ ÷PU¢^îþb-ñy7nåK R.bn”è)éNÁÅ`ý±ÕGl ü2ZêÈ %¡9 WAmŸ„“ªh­¥S´=”ˆmë4 äϬë‚q~=ʦÆ8‘Ì­VP…2æ|{é:£Qð^*M¡ð{EóñEŸk…\…Csc"îß‹><\QHnäÍG;Pˆ +lÐK-àûhÞ€EáU\8‹Ô=z¶¯øØ3À@!j¸ ¤óH¢ y#(}ƒ¶À(ëÈ Q= ­…ÈæJÒã«Æ|E×”äœD¶)\d,Ñ¥—‡2ž%#~,15>$Ï¥f¹P0Ž¾ëÐR‘ïKCƒN|ôä„›Ï0YÍà](øÉYî8NÏgÄ ÀÕ8ô¢¤™Ékéy¤#…mm©(2¶|?ƒbô¼âÌ®€T´³|,0x "Ôó™“JIÂÆ™ó01}©¦„³·ÒiŪkôùZ:Ö`¹¥–tÌàr9@Š‚Ý¢K­ÀLæd$n¤¥= +(,±£óxÍ°ˆ*‰)ÝƉI¢ÔŽœDgóÄÁñÒ$AÕ;Æb>¯P#àæØ‘á Êhî[Î%õpj¸‚ ム»xÜ㇅XB0h"4CgE^X®LíK'[)vIC¤ò‰Û ¸ì +Žç\É•ä”°Y.ÐÁ1«D)ÁB+|)AEPˆ¦'ÒeÀç…{ˆ#@½Š6±-‰Î†éYÎ5Ô¹æº>áf"ÜÖ*F/*©ÏLK1H9o…fÛ¾0tÅåZÜÒc‡= +ù˜;ˆPª…(ÞÄÈ–@ÌóL,E9EOõ×Ý­”иˆž–:Ä "9‚cpÜ•ë=+®Ú*A}åJ9+il¯5ÏÅŒ½Àf<àÄæ6rR–‰s!@‰ž ›ó!\D%k[eXÛjxç©=F=aŒ8[ƒ¡ZJa£À‹C[  l&a6â¬F‡&øÂTvòDŒšz8È;|n! k´Âµp”0mÙ¯dÈíÎÄFF˜H>RèK_Û¡CãXÂ'öX¡@:!¿F +ÎßrÃRß6ÕÀÙø+º&¼¼¼f>Ó +'ØLû\óA°Qqz²”PÁvK÷Oš:I#¥BibÛã³X6[JÛG6'þé$£L€ ’Ž+U½¹|V­>iצҴt¨-_R¾4ƒ¢°ØȻȧu•e» „+%Ø::è Âu xdDñ …c~>Ü‘/íÕ?bêáŽ-Üæ%m &ágh;áâÃR…S‹hîôXÒCžu‚ÃÌÕ w¼edÔÊœçÊÜiŽzRÉ8B!)œ •ºÀÒi6D Ô‡f™L²0H[Ç¢E ++i|ÈBøg ¥ ƨ?3Š ÆêÊŒg¥40\ÙB¡¨JÄe#¹Ê¹–ªN@DiÔ‹J í  ÿi«‘׊cƨÄ×´ +”¯¬Šh Žk{$µ}}.ªh[È¡p )ÜÜ +fúa³¾¸à"Ù†8j&'K$9-Èßb97¯pðТ"=QjêÛ^2L̶÷ç—jpR#/H% +¬p7àPÃY‚—sýÅ@ŠÆ*›¥˜Ä²¼¶îôœŠXPÂYJ—k¤ûÓ ¡x¡°­pæó B9U̽θ}½á#âׄϸzX\Í2T¬*).Èìû†œ‚©—†¦ij[¬–ïˆPëëÎU^BÍXKX#‘B©\ ÚŠi&}“¥Ì?Ša¡Þ78W¤ànHš™|(P‡­.§c[@ E¥ˆ´k*®ƒŠv"|ƒá“Ù\§1bî‡dé€eqà»ý‰c&8.š+(È¥µ];[e‹>rE-eD›'”’^e\ÕFŠ¾“q/$ͼ°hI[µ’…3(•t>LÊÞGÊGÄK÷EýåƒÓŠkÎHQp# *rÀ0œJ|ô›¯¥"!R° =[5‹m@Ah|1›™‰ëõ¨¸j”öÍõeQÀ#H¬(ö™¦IO–y‘õÍ Ž‚Ü +/‰<Z¶ã{Ö¬1V)dDD§vs@¥0„Íb{y±Ý!Õ3¸&:#•‚Æ›ØÂ%D¥$ ÏÍ#À”Y’„ZÆ¥bEÀbˆ„„¸¹>ZÜEUÚ@;»Æ[M!°Õí$1:—Ø.²ç½'ÏYT`2š+¢ÉÚˆ6täÊd(ØšXü «¶ ¯”¸K¤ûhžû^±èçÖ)¨úgqÑD\,YÛz%gT‹CË —9áÓóbß¾Hjñ'R¾‚œJlß1ÈVÉÜøAûÒwÌÒ‹16…?æjd4aÏöâ´=-‰ï‡¢³zâ1 © Žk EDZ4™È£㢆y'ú±‰içÒÖ|ì¥â57«ˆDÃÃ`_KÁx»'0€…WÒÑB¡8˜hï!÷ü +6 Ïs[¹LŸÏÅð@âÑ&¨7&æÂ@\ HqQ*.úÄÔ±"²Ú„_¨…ŽŠÙš‹Ý°`'¶‰­öJïbãÝHÛû˜.dLdES`"l )À*«t¦ +Ø>ð¹ç“0y)à‹BÔ",tB– +‘X¥1Š;aÜ=•}M!?ŽâØ1ˆf†RÁ,u°QÜ\9F=*%®"ú¦Ó¢â‚‹®‰[‹ ‚Ä—ÊpFŽyÛòÇÜ1t”’¶S¥ÉŒp*ŽE‡:“_Ï’)®Ú„Oæ¢ê{ÔƱçâÞ‰Ô7¶ˆ%‰up,!7Klëb0(_K\ÖݵÝ8Ñ¥P$@Ë.ʳó¿ÚödÆ9`.y.ØëÃpŠ\ÇfÐ* ÙÈueô¥åsEÛζ‚ˆR½_#mÝÈVÀRŒó!û°¥¿l9e€›U±ÚòÆr4±STh¬ÿ†ÍT¸5uz®€:­àx¦~X¢XéÅ`Ûí²Ã"`‹Oû^"⧚QsœÞ%´†ðl¥„rÀCëµaJh¢GC☥–W$V:œR*Ö–×¼x¹ÜÌZ'œœo>·³Ð2»™Eú»ª›â&ÀUëÅ=ïÍR½‹ôº(–¥¥K+;èínpµOöÐË„¹£+»è}ñqsc6¤y\ì²înœ6‹ +i죹 £â2³lñÀÌc;/{éã¨Ùü¸¹„ZÆ0•Åƒ3LGìJ‰à ÕÒ2#%(I«T2׌Y›ä*‚â ÷9ÎÁ"Û¬i … îàZØ sk8¹áÄ5‹³âœçNÏÎ]_—F(Úc](b‰Ë®yŸ•Û»S:ñ‹|gAšFEõÎ-o„³ihªâ•‡»YÚy’žK19듇Ùo›£–0ïƒóS{gSëSŸú™|ó0SÉÐÛòͻئ²§†+ß”£Ÿ?Kaû§G•åÓ×o+O¯~sp¶_I†7ê§×rÁB{D«ÝÚ žWI¸›Žt÷ÊüY“?ù_µ¡Â×æEǶ^^d {\úGÊôFIˆ®‘w1†„ø$kPJ”¨Ò™ mÐý%Ö~è}õÉ:MýíÙ)¯ö³k¼¾¿f­ƒK· ³bI_íWßÃW±Îš_¹$$ªç@¥"ÜF40‰? ÙõNâYò€xR8í®C¤wA‰ Èj…Hñ=D—~ltUeÇ']vy|îõùAñ™¯éøè±J )̱-×ÂMâ+"Í,´oÈ Ge2QuÕ|PHŸ8(2ü½äǨíù– н??*¾ óµ9@ 4_ˆ.bI"Ý4¬ÚrVÀs8톼s”Ýhè +œw¦õûñí­úv|leÈøÜëóƒâ 2_Óñ‘æ£éU4áN" +Ù7Š,¬Ðó¸«A¿HŸ,¢fÛ¤Õ¡D0ê¯CÊýè;@_¥tïÏÊWù¯Íòv…ІE`§]$3‚"’†Á@hÄupµiÔ,ûÑ;@Õ$¿I"™Q©$ÿ5`ÀUØ Žw{!ŸÝôQ•‘ÆMæià”w@N©Hšp•‰(D—]è8œjÒGfDQî[:4ŸpŒžOŠ´×DœIúMÈU5©ºDÙáá—ò~|4‘Œ¦I MRH`²_Ò¨á¸&5…Ke‘å²™ˆ4cÍÞÒÓH1•1ÿÔ8b‡JW%Eùôí*Ê}KBy¨ ÏIå4\Ì‘Ö%"õ.‰XŸõ8ÐÆ™z@ž ú©‘$2"J—¤9ä¾¹±H¹I>¬T€|MŸõ6ŽÏªˆŒ,|“¾‘ŽfÅHÍ•‘si=é#©Sê­±{A1¦à+±ÓC{Aækbë¤q’ÌÇFAÑ$ì?p o¥¬ÀE NÅ#…û…ۦdžkéò˜µo;E(.@ìï(åhJäÛ[¥3…Ù»`èîÔÀmàaßj +²7î‘¿ 1ò¬ÐB؈r£pw²ùÏ£Õör-E˜û;°ÍySH`ì  ¨ØT}79VncÄ¥ÂtˆG$´€B©¸y§%Íei%ÀÃäb5¢ìp.‚(½8Â"æ¼T‘\'uxeI¹Ó¬¹½0‘.\Ç…÷ù’B à ˜• +Ò[#N70`¥ ¶õ +½ÈbYGíZêˆû¼–Ûöý0õ}‹Oìﶷr—LÀØÊ·äìP PP…w …ð¨©mŠ—ñNòóÙxDcñHɜرwôØ‘Ù Ú6à‰e3ܸ +¸?­»5†Æ½%ds—.>Í›ïeǦåNjûéê”táÝdo墙ÙëP?]Ë+Ü°"ÕúeÅC%c“& b/Ãw[j+ƒªëÜÉ@NÉÃ$®È Çæq,Ú¯‰š{Sñ4ÿÖÈ’É:áÒe¥ L3¤òK,ÂVØR®Í )·©Üý1¥2#,£‰Ò‚Š¼ÁNŽJ¤:·D”&ûjçoJ‡‡K^P˜Ü+îªÄùÖ2K’e͘r"9NÂáœ0 ‹ÃOfeì8¿±™¸í÷m†X¡œ‘žÃx%ã dw\îÒz:÷Z1ÇŸšæN¬É,üœõ–A§,(³&M`ºvîaÍNߙ݈tln»‚¤À"|I£ÏcSfîYlâÊÌžà +›|¬ ØR-ýçP?I§'aˆ~˜‰ ’bá>%Q@\ŠXFpHÙà¤DLFÂqÜRÇQ„a‹‡Õ‹}JS-ƘT9°j¾~ªd¾ÓE-¦ãvËcâ¼ñpdbˆA•Ó ³!ÄÀµiKÒbÀ ä”†Ñ €.–#Yµ€D.HÃGÚRÆ|r*çÆæì&qÞ»a8æ‰ô®vê5[(A’†³ 4’˜&½S Äwóä3ø€4É.4  'r×kmÓ:n‚’æ56œˆWD2•*ìÖ$ ': „€.œˆUódÕ¼Ô´8–¥û’$…ëB»¸.šX—%&•FѱYËf¹h"@‰ ÎE!Ôíx›ÑD¤ŠFò +Mt¶&@6šT¥Q ö hºýc阤áDt[±Kîâ„.Èç@|«‘´öüuÒM–_!Ñ>D',B7Š2û‰]ÁAÄ¡•<ÍSPœ (f€. ÈOcˆ á¼”q&žhèâ‰6˜Ë '4âIAÎ;ä/“ÆÝJ#¢1ÇîÒÑ—¸p"ÔO®ø§áD§¦”†öÄi81°ö@: ñA$€LSñcµ0M ìA,‘çHÁ°•à@)â‡qá:1½ñ´$µÆC»li41pÛ§ÑD˜Þ,ˆï— i41tÑÄ ¤«k\¼N¬ ^Ý8Ã{ç´>ídjr7Ö¹J£‰=UM@/ÕÈù ƒÒÀ4·¯f3$ãË€›ÑD>ò>Di”ÐXóÊÉpŒ?w]¹§Y_—±1óÜ\ td×ΆM²' 'f@Ípb +L\8ÑØÀi¤áDcÕF&Ý$¯ø¤Àñ­vo¬§ 'Ìp‡l<‘›Af¯ ¸“·€RÆÍÉÝ"Œ¼¦‹‰Ã,ð"G5{šï,ȼ@µñD’í¢Ái‹¢Ùø”0`0¬L|JÉb'™ø”äY;%)Êèªöð_’‰Oyìwã¡Ë‚ñùÐ ˆoÅ1·°©/LÛä‘ÜÍN$:­Ysw{;·Ã(µaÜ>){§“:NkÎ@ROXfý…xTäX½roŒ|+"âæ”åàÎG©Q¤C Ö‘©­¥ê@öN1¶ÐMrFyçDuµ +@Ip@ß1ccß, +UŽÃ·oy”s£¿ª/¼ÒzŒ³t§3@ (±¬!r›ÅǶ”ú²µÝ9­Ï[[­/NýâÚ ú8¡òRÞÓ QñÑöì½8bã%¹ør]Ò´´r×¥1*9CÈÓr1ª”Q5Î?ÆyH}h*¶óÌúÚ”ã²Í•j27gòr¥ »n|ü3¿ù¾Mìa­¦il²Ôd@5c½>»êÁm +ñÉ@ÙE‚ü¦NÐ R¡›nh¯S.5Ndƒ\Zmkä,IÇÌ›×áüZ˜qДkY ò“cï£ÄQN`…⬤ÔVF¤¤kždâTMP6P“SW¢rò©§Rœc›‰®‚EFtZ?_F¼v—r²9%$—;Âæ²7ÛR_yñ/…( +jBÄJlNàÚy¥#ÒÎlÎdäÕ˜f Ê#=2‰6#Í•s¦H¹ º3JÎTÁIA!Ѫ¸t‘‡r,‰"½I£°/çPåk¢4E®Ò*磫lÖ¦‹(ðSÏ‹õ±H%¹ÔÝ’ùø]tu<Ð\a(?Ì(­Nö£ŒÒŠt@€©—=Ô ‡F\ +  ë! e¯K£«ÒÈ ”0¸P +J-5®wå¸hÃV¸Ùg2¿"M|²{QŽD}%ó)sá ™˜#ø&(ÚÀ”Hš@ŽËp&åx‘/G|£ ϲ…F¢ g“$JcÝ„¹Ù³)L[–Oü£)}(Ñ&@RVî+‹9–ŸÈÉÀ.€Rñ‘B²BÆÒHå=@¢fvh(cÍÊ5) `*ÿH^ÛëR)éGŒ„aNÄJ¾‘¦9|~5{s̉[¹WHB%9c€iòŠÁây¹—”KI.\³é H®”Ä +^òf¼2f7Znk,Åe÷+<¤ çW`YWØ{ñåøQ.‘Hêùa.· +ýèÔVt0òf§›«ë‡µG{U4­©Ã Ïú ”2½•=®‹-7Ÿ&N~«ßôhHE›ìèÄ×ÅsíÄãæ꬙ÅídR6Ù[Ž¦eöÆ”Êí`(] fëéÈQsÆŽL K,Ú2f:ÓTª!€ìq@)eqRD‡X«–ÖùS€o¯K÷ˆ5 tNÕ«&È43“šÀØ%‹!§ÐŠ\P|•Ô·Í*ÈiÌ©ÑûrÞ\”¬m¢°d È‘¡ÌqþÑsTï[£z_êšg©Þ·Ò'ëNš tQ²À¡ä ff‰Ýá¶ÜVøÖÇšÙ2ç`Êl¬ßT¥”T'ú!Ô£Þ™óæð$õ¨:9’r²yîÅ¢Rä†Ç¥’‹ÓЖï7g«CÖ¤ ʬ]˜Ò¬Õ›°±{šïø¾Ÿñí²ó ·³éšûo«±æJ"9äѨ@ê—€\Z+ó@xüSIåx¥Ä0xx^Êf#ÏÍBâ9ÎáÁ§,(]“,0eäîan3ïlîCfln¿d(ð í,:efŸE Ÿ@`Qˆ;Y^–“n©Ï•ÌQ—DG4Ô¨ð‚0$u3"´dç q^C0cPD®!a ÉPÐÄzÙÙwC¦YNÀŽbÔEã7ØûÍöªyª>óÕïf%€ŽÚÝ,mÊY<Ú”²x´)eñhÓ"‹G›b6¥,mZdñä¯óÜØòY<Ú´ÈâѦ”Å£M)‹G›Y<Ú³x´)fñÀæKšY1’Å£›E\vŽ6¥,mZdñhSÊâѦ˜ÅÃæmæ¼gñhSÊâѦ”Å£M‹,žÜu¡]Üb§‘˜B6¥,mJY<Ú´ÈâѦ”Å£M)‹G›Y<Ú”²x´)eñhÓ"‹'wdñhSÊâѦEÏÿÏÞw®µ’# 7À=؃±éˆÎd0ØäèЃ“~¼×þ©JR·º3;3gf÷l8V+–J•TªRô /EŸðâQ´)^<ŠôâQ´ ¢MñâQ´ /E›ðâáEÜdû«q/E›ðâQĨ̋GÑ&¼xm‹GѦxñ¸v6ŠHˆ[Ú„¢Mzñ(Ú„¢Mxñð"ñ +Pzñ¸öOÏt©h“^<Š6áÅ£hA/E›âÅ£h^<Š6áÅ£øÊS/E›ðâIÙt  ŸŽ¢MñâQ´ /E›ðâQ´)^<Š6áÅ£¨^<Š:Å‹ÇW^ü*Ú„o­Ü‹GÑ&¼xm‹GѦxñ(zЋGÑ'¼x}Š¢Oxñ(ú„¢OñâQô /EŸðâQô)^<Š>áÅ£è^<Š>Õ‹%)E›b'à…IOÇcì·øWímÂN€Úœ,™°(˜xEv„±ßN€;á·p­Äg'ðí¬»„  €PÔNàC®) +M&îí°\gÇéì.óôŸÄ¢ ;;¯³ À·ÂÜ‚vaWùNÚ (AM\² NØ x™k'0An!’¢Ê˜mS2VK¶‘°H #LéÙP!Ú.Ñ |þ£y‚l›ñq  <µ­êô’@¢]×p {qhd/{FTÌ¿ŸÙàwNó{ùu`î=·î tš3 ™Õ@OQFu Y¬¥k4Ðy–Un4€øÓ( +¨ôE,-¢€¬ˆ·D_D_=ê週»&*£óipRWYwÌh 3ñ²Øü†Ø+rB¡¦»-iäWn4`º,”xF®ÿaÂ"ÙäõdZä]ÜzE¶"¯ `¨»©Üh ³A²* ø͘¬ºFšF”¹W^:[€g3€½Rô@5œ]V]£Îk`Q®Ñ +iKn3p­:ªk3Ði®&,rm:óÇw|_訬º6Rd´7Ïf ³ÈPh™ü˜CœÛ þ¾\lîÕXÿÁ!(2(k…y¸&ƒ“gÕÕ¸ önàÆLn‘"˜ „Bn20Ü—žÜf`ПfÝ5`E8¸&ïÞßµÌs_V|WÖ¶I¡/˜lzZ›ePäš àÆ–ž'×f ³¨4ÛSS¢›ÎT"Ì‚ÁÏ$üíÚ töôŠ\›Îd((dFð² N ®1Ú²¼"ñ©KŒP³?×h §XlÅ3èlŸ×f ³ŒQP¤N”¸6¡Û  /]f2€Ù¼D5š¥&T\“ž¢•dê”* ö ºÔʵè,³&ËäÇÖ¤0Ì<9J|f4¶ dï`†ÁC/È‚¹ÀàGX¬‡©U±7f.0˜EÌ·NÈÁðƒ› Œ”…bäc›Pä™ ÜB•› f³ÇÌ— UJYKMc§›› ¾ªkƒg. Ac­PüO,Jð —dÓ8[Ȇ$ë©EMV]sÎ.¨®¹ ÈJ}æyÓÈŠÏ’eÛT}þ“¶*bd.q*{%"{$ÔÌqMÙ£«DÓ$œb\9›>÷¥c‰ÖýAé¨äêkl H.‹AéhaÙ”Ž½®ƒÒ¹Ç{,ï ŽÅ^¡—ŽåÜâÒ+Äx³£—{² Ž»WúÔY\è¡B£ÈÈxdº2Yæ +î~0éTŒOÇ(…?@73—wGPúÅ_$ ”[è¡W÷å߇Ȇ®«„7\Dv52ûΔ{SèóŸ”u¶‘žÿ¤wT\ÿIT+ü§[w‘Z0÷s%EðŸt‹DÿI¯ wÖ‚ÿ¤ÌاO•MÅö)¸ŒTýG +|~Å­fM%M RYp,Öì=Ö=öæ+4ƒó<(e™¥ãl€¥œ÷³ ³vŠ\Å»*xPzE¢¡[èzPʺägx:•îüŒQ§Ù¨E +wÀS·]'HÉp•káHÑÓãRGß± +!êŸÌx¹÷Œ‚Ó÷ÚBaÉ=©‚ª¢èAãÍû–f|”Š'&»# +»µpAaAœŒQ›£‡p1‚ E œaŒkOóJ„‹·Ð»q;ó.FÜA½ÛarüÖBanÓ‰TXl\ßiæ(¾ã̯P|ìUfᇄ‹Ù`4^¼¡Ù…)«˜ÍÌ·0¡H”¼B.H¸yÒ†ÌÅA(‘5.©¸Â }wî“p$Πħ±ÔÜwš…Bñj„ÞT¢|/X(e†º¾BÊèTÁ’ÍAL‹ð=žzÏ’ÍxŸ*˜²]s‘gÊöŠS¶WhñW¯4 +œO¨Õ0dU@øei­}‘›1‡žO”f¹_ýr¸ ÑjmsïPafÏ–Mãd +_ạxòHgç?ù܈Œ«åÆl¯H0f{…îá³t&¯{Öl‹jÁ˜mÙ\8g[¦°ëZacšöÖ-âgJ à¼ñG„ò®F”š°B  MfÍôÆe)HÅÙ1à_ËU+.–& ðÄ+@çºôˆFî°xo®çÞ€˜¦7Ö]ƒ·ý4ʘ¡LØ5;ôQM ÐU†Ý9Gs•dn‘uoFÀtÀ×@E%™]6 È$¹ ä¼Î<ðºcŠÛÀ1ÂÝ,O p·T¦æ`Õw/â®\ÄWdôîE„B‘’ù˜›{/bØ𴌈Òd†Šd¡}^WÂùS©ðò,Á&LÃû<Øו•øáÀ €ñÞ<°Ò°AðÃcôà>™6ƒ¸Ÿôí²[ÆÁ€ªR º¼)…™伶Øßþ1©ÕE +Ø|Nv,ÍÜÇŠ*bU•=ß ¢ãm ¥S÷f6-ÃòÍ[eO}a} qÛ‘ ©ÌmdÔ>”ÒÔÄ"¨n!õý¢½±Ý•]»´Í0ÃÛI•‹¾ÞŽƒýší8Ç –ẇS®L!â”Ê}Ŷ̷ÂC[•_ ‹Ø ’>“á……¡Ò€wT¼oñ^™§nPòžð¬Rš.+„rCì‹£3¨ ÛG]ð}¥òXÎ~„¢ï«!B¼¢zẉ€BKfG#y´hPD&È´NŠˆüõlš C˜üŒ—:òEX/³ƒ7~‚É7ÈOU9_¤·/™–É:÷ Ò$ÆR%1éÍ„ƒ¿]l¡é$0.¸ÄUE·Là^©Ëa Cʦ=>Ä왎¥º‡Õãm< “ÈUödÀÏBA#° U©Ï…¿9½Va47±#"‚"t„d=R/ +ˆÆë%È¢<@Uñ:C¢[#Y¸.ÖÚ Âds!K KDïRàòˆX‰2[žNv :Ý Ù'¨òFCãfQð•–dŠP(ö»{Ê®!èšÄ©2Ú>% üì +Zè}&aF2M÷WˆyŒ†ÿueˆÆeù¿QVC?‡´ÐÉKÊ­k<_7KÄZ^ÀB,r]‰0_7û"¦ù&½ ÞeÚ¬Qµ!u%ÙJÍÍ.C{('ª!^B a¾9SðÍÀ?àø3†Ÿ9:Od;f2…™Ld–òHØu–vßcYºîÿ‹U¿ê¨Lpi?qêӟô\R·ºð—ÊûT…«G¡½š‡Tφ”Þ¬é6ÙŽÑ™ÍÔ±=_øJÝ0Ü©Æ}’ã®”cËsoQ-J¨N²•âß^u÷/^ ÿbl_{-41[éßuŠªUþ¯ Ã߯þþÛP¯H‰ø—Š‹>|´Ž°dq#$o#Èg¤p´©»ÿ?ú°=‡\æ Ð{/`«I*¶¬Ñ»q¨ "¯‹.P]D¤)ÉèN*øÐØ; £‹°À€‰>D@æß_¶)2Y³-–#$ « «ª:SÉC~ìr7ª‹;=¥1]@”)ÃCž¹sÿÛ Õÿ,ºü½të¿9ÿ)¶Û‘°/’»/~2ÆÉ`› nUÎLø°Ua5ÉÆP·Vs¢Pd°“ ½x¿ˆŒÁ×ecJuuŇÕkbkªb3,œ(™ò´&ò´1¦ÃåÙS%[“å. ™z;‘UµÐá®Pó¾é›¡q 6( +ñÈG /mÞDÖmÕ…G TdÐSš ÆNŒ¥ÓvHbXЬ7ßà*€e³oóJEö>­‰6uŒ)0ú>‚çsóÏ87ßA6þqJÿÐ)ý£2úlpÏD àD9A駌$½ y)xP§ì 9l²™G6¦ž%*ÙM¼ÉŽƒvÆŸ\ÈDÃiÄd*ˆ¸\8vSN?—K¦ò™g|L=ÒÖĦÏ8yrÜ”†SÎøTL¤2âÄɘ2DQgœÁ)‹ +6œvÚ§Âí»¨??NÖ?ódýýÚsü§ã?¨)ÎÜYø4ÉMg2ÓË…èózf0Ê·ê0~uðkhƒɶ 7Gš­K’LàáådYÇ:Ë6É$§5dš¸‚õ §Ú>©Ž­_HËP,s «¥êhä ºv¹Ýª;åzµÝê¾î Z#ç×xhTÓCëÝ‘¯²‡Ê¯}‡V$ýg{½6ÖÔK­_œvÉ4ú¨ük§ÖkºÕZÛùàÝÊBmÞ/­²7n5œ¡[+8xá—~o0ò†Ý$ÿ_°C±xèæz!J*JÏ…nƒŽ‰¿µç,™o—•Ä®ÈŒzƒYclj“-Îÿßü¼0v¡B¯ ˜ùÿ{ó+ùqHþx'Ex¿º”B rèn.@ÚfO!-nΨ}Y¢¯Ÿ,7Y†ÎB&Yz9Îã•0o•!Š=!e¶Æž +Ñ[IR¢R7Ñàh¹…&™H[SíaÑĺ¨ ¤. H‰²ccy™–P—l‰ú¨@Àd°‘$!+^>„ò'32ñµŒµ “q§èÕ±¯f°<ù‚8DPf«Ï*¥²Ñx7ðÚ£Oe –È%.ð¦ÇÓtƒ`0s"“ÄËhÔ÷•$?S  ¨¥É”`"fªAS¦[áÙ¯W¤¤<ç p²›: Ô1xýo#É +ÁKz¼5mªØåLz ›‰W¿„ˆ˜*¸—jJ2¨7ŽBßXõ+…ÍÄxi:Öb² P0L´ÅžƒðØô•–(6"F²·0¡-Ä–8ì, _Gªcrjpƃ|xnA2ð:0Éñõ )Ñ1°•…1†dßx¶[™gñ:_f¯< h²îš4-Š†9D-×G@GCüÈð®6Mxú„íàý<ù2á–^§íïÁ3ƒl‹ª¨ì-1z˜»Ÿ@e@r ÞG£–iönÁݧ¦³¼\d˜ó,â[Ô9r*€™.ðõ4²0ª; >×æfcÙs?tð– ƒããwÓ¢ïÐ4žtÌ„ F&öâŸÌP¦V„3á•*Ù˜",Æn*lÎP`hJÈkD°Ÿ§Ü uÙÅ23ƒ'…?àe.|01I¶Möß1:Ë<­É| •¶…*e …¾¨ƒc92 ÎfØ4œdzÇ÷ÄЄH"!ÝJÑ1,[–I Ýb!ÓÈøp–užp„ü†€4ä·­¦ð¼¸ªä³cknp£-Sr@D*~£c%ØœFÛ …é€õ&m#Ù*(˜Šek +m„ή®g¦°¥ á¿P ó÷Dâ©é@+u Úd¯tR7h¶m`Qð‚´0©Ó©¾:éÓL™x +4ÀéŠ2]¦¾0؆>"Ñ%xô½RW.HØ(‰0Ópé¾ÐÈšB38ðF€¦ƒKšè@³á¸0 +.t`#Ýk +¡Ó¦ÆBù“-ÓTÑδ1É5ÅQÍTú¦àªÃ«“ú3Ø+/(ÁÜÐè›olDÙ ŒK‰Ów58ëùð™8˜åˆ†'²ip®Ht£÷K,÷›Îßâ’šmIçùÖU™F`Ú§²Ðú +¤Ó…Nu ‰-±1KSp<<Ÿó­"4£O‘óéd¦ð!D¾è‡¨ ,À|1àù‡{J% 5 +A] ô‹×à1™EêL „0PÁñ• šLð ;øW’"“§{€G¡:D–Èñ&uwÔᵊ© ǰЫ ŠT mÓ·~:x a¼wHPE(£¤‰k (¦º”(æÜp` Àä0W'¡¤ÏŸh?4Ú”¦ÒíÂüd˜B |=ÉIÕLúš™ô}&A¤&bðU;šžÎFJ¡é4«:4±id  À80)ðaÓ zð á“Î$ ¹€ìé¿Æ@è,–,éúí’ƒ‡hi"[ä}ºEi±Ž"6˜ºdL¶€GL,NB­¨4F`Óy† TDÌ ¿U "•>l#dS;@rÄ@ ÓYÊmƒFá$”‡è9*=Mp†à\ëm^ˆ)ÒØ6èðŒO¤ 0 +@ DB#‰²%*JÛP€L„ç ¯ ÏxŸ¿BaPúc*ìM)Ð!¾ž‰YËtšAÀ„´xLMšõÍ„œ$00”0MÊ8  Äã p!GA×-JG5ðÂÃ^pŸZÙĘ´t ‰Æ[!4tºä73Ò1Å‚èd)Œ¤4®2 +u˜ðusè×Àv:…˜0`.ˆ³&„ÙÄ,ÅOìVÅó`™þQðκJ“ÔE¤ÙBÑøiªx,á Úô9”`‚QBI4rþ6ÓœíÊÔhVz¸1X Òf;äýæ»ã¶!|Žb(fñ!G¸‚Ä2߀K¢‰!ÑàÙBm[ˆmXB~SbdJ5è+'Sòò*A~-Ò§Få?U£Â’¡Ù±WƒF¦4X > ðd åÐF¥a€K<Ób‚†J9C’hɘVj˜:kƒQ ö®Reù°uH$¤x«áD+R@W`L4Ô U€pïÕ‘”(` Í Ê”‚”yÍ€%1BcÎÆ€¡Áœ»“‡6ZÒ1Ì-2eŒ©)SÕx! ÕcÓÝ X™(‡$ ]v ªbÐ8|Ê`@0MŒGì†çñ ßÈà3q,ØD*Σa*,÷p2•²yæáž,‰!€¦pFAº ¹¢ŠÂ-úÇ3Q§ÙcUþ€º® Îé&RØR2œ Ñ1L9š°7€ÔNÈ„ø6&NÐY 0v.Ò)Ï› ˆU¬ƒJ2œ °Ì@"‚A£°€ÔŽÒ7 )„Y"“•á1šrýar€1‚ßDCDh4ä;DÐRâ_< cB؈+`Y)Ô|gOd‹± î¨kÁDAÛU¨˜e’Ù¨s2ÍÒb1¢ÆXAé‡wÏÁ1`Ü1Hã4 `;¸ŽP$SŽ¼@h7õÕu—€)™= Tæø¥ƒ[³EÅ–ux¶ +J-=xüX†n…ã±.³šDÅS]âIÅeÒ,äßÈ¡&é"à6¼4 d–*UŸs«,´0`aÕ REøìÃ`nDiƒ[ÁEãѦ“FtÊ-€ÔD“w€"@Ž&9Ûž.À#Ùfo€J¡’­H<Õ—Lƒ(2{ʬBè62 n%&zÓ©2Œ{Iƒj`²-²@‹'tzÅMþbSµ!· Ûœ“:8aŒ ŒZ¦ f¤º*%¤…ÊÞ¹Xˆ’e` +p*6zÏ+H1-I‚mµY†YÕ¦ñõsl‘ „ht 5|€L@M¦L³ÕhÈ~5&Ù®¦l–?Ù„4z«h$Þ +¦š¢øøAF²¢a·–‚UŒC‘Q£B‚GÕU›e¾äG‹šëh¤‚Jl*Ú;èSA?¯’2dK–¨P"4³Y°~RIV1Î0ëæ¨ZÚÍ6 +ƒ‰HT|aQÏAA´¨P„hI•†îÒH¾ Lei‹1&9<[0Ú8 bÚ9äël(Ðû@R²Y(yú €ð¦tŸ:€ã@óËØ<#ÁcK¦l› 'c Kof¤Øãc›1•ÓOâ£x“Š2 âŠh¸l1 Ž2¦3Æwô¨(4²úâ ß·¨É‰*>4<‚#~X˜\ W‡›†)WA§`B²?>a7¬Øs)¹¢öHÙ ÑÖ©¼Ç‚ÁT ‡j -YŽëYIÙ¤'&©ðGI:Ïá-±Ô2ì–Du:›†¸$B,H$Tˆe1ç44iàé¢Q-öÊ-ÅNçñtaR°i:ˆ\&-1ɽa(ÑÒI:ÒéhºÊ„Ù¦舅&µ™ âGP ÁTS£!¯@—-ºÎë4=ÔAá’¦¡öM“Šl°sšŠv›=='˜@Ãa€åßÒ¨xFu"™…éƒmE\¥HÁv6Œ«P³MnasùÊlT™TàŠµjÓãFz§oé‰.fÓ$6ð< ¡‚:‹–ÑøF` +Ãá€Tƒþ„¦w4ßÀ‚–$´Zã,0t ”îêl +(hhYTi:>z¤š%È<¸²SºL-¡,ã„ ± dûhŒá˜6 €`rЕ(‡dòµ+N <m½4Ô¼Êts"ÒĨŠD‰°7\”Bøhg²÷„€RÀ‚¤‚.&Éžd b ƒBWǤŠ °1 á”2ÃTYÌP?LMaÀ´1 hõÂ; :í ŒfOŒ +LØTéæÙ,ñ«A!ÑÎe1Z?‚+À0*ôœ:KŽkð{‚tf;60¨»BÍnôšÅ›&Aiú pF–h-š6”H6æðàV ²`Y¥Œ´o:u_ÍÊx‚¨ÍßÆGbÔîG¹…σUvòØc/‰Ú+¡µõªàüàÁ%€DQìH4À.(?ÈKT§Sfr–KŽ,Ì=ÂíRôi,ä¹E‰Sµx¨«M{ÊZ´â%Ш¼*³Í¢6O¼ÅÁt[¢±PRb#–\ZÆ—{2R~4cIÈ€>s_BPË7.ªƒEƒùšðí†dO0}Ø`æ¶ nú€d¹pm†a:Óe{ +@¿’5*2ȃ T†K7804¾CÈ>`K¸y’E"NQ×Éæ€:ˆÈ×]äÌXÄœ”B2øÏAve™óxZO6xÂIx¾*³°8°M2[ÊrOËTEeR×sЧié*0à¨0h=ø€¶QÕfU¥Ù±¡‰Mïà µÌZônt¯«HŤÁþ1’†Dc‘‚„ 2ºL¼#éSøyVXf  +5•(<¬Ô;-Èæ4±‡ŒÏCüÉ4•\0 +%J>T±!l5ü–dÓw`F°íTÃ[KX("´Ÿƒ\&«Â½™ ++”‚’ƒŠ­ªÐËD"ñ ©p×bÒë7”k À¢°[HHªÂR´šp E44 ´.˜/Ü¡q¤6=™š{5z K/zqh‹ìv¯çà Â)Y Ô[+r2 Gµ©|¸Ó÷±`¾#èØ©³«N•ÆãЀ!ôJ/Ét¸*±ÃB4P¨aXE³&›‘¨•š’hH„£X/Nl–:‡Hå†Ä²_«ejH5Y 8" ™`>"´‚Oç ¶]wІÇâx©Âbó!©{ªFÏ8Ȃǘ`–R4›’U¸\6à†.!Œ†¡ÐWÁÀhP´É˜xå1)È+Æ0Ì`²1¹ÒÑ« ý2@{ {(ã…8ØíTÕšAm-)&#£Â*i`#?é½6\\¢E¢~p0à Úi´3l¡…)þüÞ³Ï@¼÷45zÝ®à•ˆFo1ÈTù-9ðg°ýÁͺj‚ýìF6\£Ûæd2Zˆï­ 7‡*žr¼S'GÑJÑGÕTœ°€ºjà;†f^¼8F€Ì×´¨\&ãk°™˜`BâQßÀ†¢Pu#ä‘ß "ë¡…Óµ<è+oaƒrq0ñ‚IÓ™LR* ‘¼ Ó‚>mZôTÂoðR€EÖI2K™1ÈÆ +ÀÑ$&¸SÛžÕ÷úWè½”`}…Ê>èš „0¶ ¶Æû0RòäÀÎA%_Ь64y¯Ï’¤ç@HÖØÕ …tL»€ŽpûfUD5–&-&B¸è#[¥!ÅU£e²Û`—AÙ.(¨ª«±p±¦5þ]‘ɲÁæŠ 8‹hxVA“„6¸¶ðFuGƒE£ž\÷ ‚ j¤„H8 nAdz© 7K*µ’¢£‰jÐ{#ƒEñUÁÐ¥b [:Mø©n +§Z¥31ÀPOöFÕ)Œà‡¨`ýCÚ Â"験€ˆfžU–h ¤™Ì]…TÙDÖ1O4Й9[¡Âf$'‹&-2iÐ+!bš·ðÀC5—^ïQæ×Òþ@lÕ³EÀg]á‹Àë3°Qê¸Jˆ¤¬!x,w£Á¬`R0)4ØÈLDŸN„é,ÁŠf#§²À_¬ƒhpGƒDw"s¡% ˜!",ø áUP#ŒA¬oЂUtÑAã&èŸpÅ ‹…ÐÙä¤h`Äl*;€Ü@ÆÑrTÐT•Þß¡Ñ ÇÔ)ê€8dÓµ*àÁcQ˧'žÈ&zù@ †ã†ß ðªŒÿË zIÝO¼t.èÜ`ÀÒ¨ÅLÅ´&þòá’/ªàb"y÷6œRw 2:®5W‹™ãâ(ô‹P$z3ˆ-@6‚ß@/4z×”‹ -ØPÀ?цGÍGZEnCÉÄðÆ…xoä7ÆÏGŠ¡AÒ<aQCócT[@=›"HŒ„ÛA Í“ +•¨A|±è—R{Lþ=«½“,Ä2^Í eãá[è¤Ú­¾:ƒÐÙ á âó¿…èÇ\µÝn½ªý·VÕ¬Õ¯‡ôP” +]¬OÖ‡’ 1y~ e5ÔÇ.Hƒ³ŸªíoÖ§}«bsêÛÕ¯OPõ¬KvêÍW/‡]¼Uõ^µJ†Š©á æ7Ê·~"ˆàVÂ.´‚Xí¤×çU†LëËé¶:´‹²Sí¬ÇC)ºÕ7|ý'#ÌÄ| J “¥¦þƒ0·ðªÒ¿ˆä2B™ƒç쀱¶ƒ-[5rðŸseRS†­§Àx.V»×Jfà Y‰ˆ‘ðPþ¡¬$úèB¦ÿŸ7×?4´êâ+L8¢B`_Æ)°Wú°Úã°D¿ÃUÚø?öß¿hPw­0¨Œ& Ðlp±Œƒ»Kööö ëý3ñp­ óÊMR¨„ÖC—Ýnµã4BÆzˆÿ—ÌvÁµB˜É!ïyìþ%á É 3æɈZ`r´ +©’ÓÚ`±ØÖ*Fµ¡Ž“× MYX®ásðgîÌ,ê «Fò ”wæÂEž^¬û‹)'aßئ’s/lè4`°¿ÿTPØô Ùèòè׶3\X?êö~îâ"¤Å2Ý¡Îë§dêDPYÏî'‡]Ï1±¯Øj¨A}‚­nˆV ¥q*µ±*kD ^¿j [DF‚'{(ªõßÑC¶:lÕ… ôºqkô•¦› ¡õRu0šÒKy4è}8_Ÿ…‚ÚgÖô( +»€½Ñ…Sïªi5ÇG)I¶¢S’u¼Ô ¢†ß¯jÌ»K4f  ˆÎ‹8‘âQâiXyx¸+*(1èAÈ÷þc†n2Óß*vÑ}K(Á[B¨uÓ «qš¡ÍÐB(&ÌD×Í.4$Âò?ÇŠÀnУÉ@{ÖlQç?£=kpÐnQÂï RÕ>"×éq!†Ð1_§¡c‚Ü¡Bä}R»Òý‘ÍußÒê´ëƒî°Eu¢ÿ€Ä!§Ã~»ú+ýŸ%gn œ²þœ- l@¶íÝîëû7÷±3oma§¿j… â:¼x=̘ÿ_Š€ ,\ýñrè~rºgî? áAèlöËß Œðí}c![#Hã‡HD–C4P‹Î4i{…ÿ›…çÅÙ¬dwßGÀiÂüKqä¿f‡e)Î¥ø?‡ÿ½`Ñ”?,߉N~¿ë,¬~qêc˜~À¶ÓDÀlêÿ bàÿ¸è"}Ct9þˆìñf ~¼m@.rmF6øl›ðTÛPl•0Ú=Ä¥‘4°€ƒd Ê¦¡™’/øàf†ˆÄø‡&Ó ¶%¼úTÙH&‡…D!ºè¿I8âÔê‡hôW3Nã‡lôI$á +Wöm:¾ØqA#ó°¬áÿt|Ñÿ +Qâ¿PjTäÂô4Ôÿ!KÿaY:ëüämï5Gÿ*iúï6‡~Oÿcáíwè s—ð·ØAÿ½4C¢OÌÁÖ'£Ã¤Ø†nÙàq.aà°ö'“XªJÇ¿ýökè¤:üø³/~7ç©YóíÁÌg%» п‡ÌEÇTE¸Úú³`áëòkð098zÍအñ‚N»Õ‰Sò3ïÝQÿ¿žêÓ<4z*þ\®ú¿Æ`óƒ^?T~«6z?ÿà°ÿ6ûO:ß@ÿÄK©®ƒçi¨H륪­éÔپƭí­õxH¸ÍìW[(@/NÁûÕútÚø}³¥ÿ^Òìmß°Þ0D—ø“3ùà0:¥òq¶=ÄpÕÚ݆¯Ý[oðè.÷oTP¢ò‚ïÃ࢈Ԉ*üÒ¯úœuš½ºrøbüL’ß‹´A„ûAÓÿ½4ýT!ú†)QCÕHÆ0üŠM áqŸý)D•Aµ;$D¥3Ÿ½¸Õfr¡£¯ñÆÃzµí·ºÔC^æ‹÷Ÿ‹Uˆ…§ è—AoT9ÏyçuàLktåo„Ô´;îäzý­îMæÜèuZ¿9¾þ;½ŸÈÀ¥ÑpÚ¨ ¶‘•â¨’·ä êd'z»â½il:ýV·Ôk¹5Ùlœf›ûÛi…7ÓÜ H[ŒøN0÷ÅY:æ„øÿ“‰ò5“Éßë¦fjäóƒ9~ƒ9ÊqLRïÕ$=‰·ðbñ{üA °Çñ?ÈÿaÜQýKŒ?X#ð5Hcɦ­k– ñvåYœR§Œ•%!t´©PEÒ¶þª›µ„ñaüËi X…5ýyùû=Ü,&yCA¼e|‘˜’mKýAP~”5Aùá2ûŠæ“PTH9 +E…¨IÿkôñUþRüù'ì¾Ä¶Ÿ›þ'7ªÿÁ…øå5f±×nU_ç½òËÌóeæ÷?¤.‰e¼5Ä^9ŠÑ"¤Ð=Ú®âÓKBz%Hö‚¼–Q7Cý¿ð|ÿ…• Š¬Ø’¦á³1ÉV ]1Mò­aà"Û&’­šeCL; dêóÅN~PÃ?“â³Å *V†n~PÄñ_è²÷ß®·Êÿ,½UýªÞ*ÿ7é­ß‘H—é¬6zãÁ¡õ_N¢ÿÉ– ùŠ½S…^Ážð÷Š·¥áêßún÷«; ù È2jA„j‡-A€k Ò”èëJ11P„¢ªy],I³`VÎÀÛýÃYhð­dA$XìˆV?•E`hÍP®7è:ƒaH õÿÒ®S¥0ßþ¬›ù@§¿ó~~0÷~þ_Ïî.¾ï;YAð¦ üŸæå_ªý°åþ…Tïž”VÇíÑ£@ìÊ­N¿í»ðo:9J„BH`ìØB·áEŽýføÙѶˆÄ†ð(ÕhX(x´É“ðýƒaÚ†¥èºbÉÍh˜’¢È6¤¶54 JÈJ MRlÝ& Ýàˈ݋ýãï›ß‚å ¥úd”W`DS1-ÝFïÈÕ%™6¤5Ô$“†VeQ¦t²#²i£ AcªJ䳆,/¡ØŶn„ö¶!Sbv›Lü±0ù…Žý§®úb["CHdIØ+¤c +$€±ÈÈ2&6†\f‘ŒdHìjY Á™R©ÐiÅ,È-C E! Y +™­Ð[éòùN¬8ðG÷¡?©C>XÈ¿̓Ÿ =µÂ*ÆÙ¶mU·e&dR%»e@_H!d¡‘ü"Ø(;’yCB;IÖ0Q*„ôvЋNmb? +'9_–%K$-&“aªE¦`*âä|å0³RÖEzŽÛ¿'Øò1AK‘†øßkN•©& ¦¾"ìœïŽ) ½Oœ[œÄ‰-KôMRÝü?2Ö Ww†C2Áú¨Ú}%ôu!vå€)´7huà'H]V·:"¤ül@*Aa…| TÃÐCìçÖè-d'1™ÇCѪÕz]òÇž3¨9ƒj<˜"ƒŽ>{ ðù^}Üqº£|uT…”$ü7f#ñqø}sr|Úk8S?n†b¿tÚ]ò9I”ÐA«6ñlë™Á ú÷tñ'ô/Ôª¿µÚÓ¥ux þþ¹ùJbËÝáóOÕÁpSˆ(Vý©ÚóºP>œQ4yZÍdèûõ/…N·×u¾˜v¯þá4¾^síû®«Öê6ÈDå/¬ GÙâ"¾½>±öŸ´ýÿ!$ä¹øÒú[è+ˆÿÍ]ý7ôŸ¾|Ô¡êwÆhX^}<õ:ß—’ýux¸1¬‚B<±¯¢ã_~.Ê÷3•ÿ†S:lþüæÆßù A˜ü—ï²*EHStEµ!?¹GkÖšßœÖëÛWN™[óûc¢©¥Dý·Ö÷s«QǾ¹Ñu P$v1n;‘\ð ±ä +',pÁ>a^¿ì¶êC8Öx£ºž\¥ÌÅ Öê‘qœÇ…Ó®ô.èpN¥Þ°‹Å¯ +ŸVR•dq%ê—»‡‰/Áë_5î§kš#Âù¸:®v_ÇÕW'TêõÇ}¶5ýàÏ™B Üéuð­6ÊP¦Ño¥˜4ì÷ØÉR Â~µÑôVm·<¹8sÊŒG½ÐEu8r"vÎ@õ@g¸Z~¬C­a¯]9ä08hi™[ù£Û«ôÆ#rÌz(BU;Ô¯öAhØêŒÛxûáçŠPÝÜúUBÍê¿’nZ R›/á›Ã;±±û§ÑwBYÀ˜ÝÀtÚÆBþÑ‘Ó%ÓÄ< + Xì…΃áOœá› e\Ž0š°¹^‹³ñ¨O 3¿Ëp¯Ý«UÛNܺ_ýtã¤d?á¾Vz}á‡òGæ TE‘9 ¯ßZõ·Ò ×lµ77µ‹~¾ªà@ âMEžÖa¡Ss_ë°”/BŸ +"aÔtËä§Djõ$~<û¹Øîõ{ WÓ0çÕͺ«ÿfÕ ÎC¼MžZï¬_­»“U Ež5Y éÂ\u˶ä9U½©cšSñ EiOœç7–IJQŸU5ç´Û9"¬q‚4³O¨èqEeö4÷{äôºû‚®§ÌXøô|èF¨éžÖjwÔ +BXN; +Y”‚…Ó0¡Dû¤Þ²ÓÞ¯Že “qù¡ÀfÔ<îÕ«m b]±B,;0*£P²–’R’ d‘ÃQ·Ûœ±›äëäÂ%aá„Þ¶[]'4"rƤ 2­æ]Mœ4 2Ú³9WáL†Žá2cðÌD:Tøeä +‚ÒŒ}$ؤ;‚•Ëà Mk[²-‡,Mûv›QÕeñl³§}Õr~&‹%Rͨڭ;œÍ;VÅjÝÉà=â7*#Hµg¬í;²¢x:µ¶KfÁk}bU±šK[Äöä +Yy.y%Ìø¤×pNÂcqû¾±sTodK„ýûòÖC¥vµëÐÃì\)¾ÜaBûYv«£›21À?g~+¤¹¿¿™ ”tÎ/ÅÖ`øù›”z¯Ë°†è¬ªl|&@×Å· ók;ˤ”ÿ`k±‡ÿpo½>¾¶¹^ý©Û„~K_Ÿ×O€D}ѹ€8§½²òO¢Í4(Î"÷}®ö~r}p?ÎoPo·úD–†Ë™_ˆNýJè(kazŠ²¯Å%ÙäOÔ¡‚H«^µø¤¾ò¸F„bÌæHõ¤HÆ0Cåq¥Í‘P‰úqÆ—Àùêºà}àæAç¼o~æ°î™ÂÖûÃ9+#¼6ŸS§-×Q„÷õ†oÕ†3p†s;#pÐvɲjs ¢¿–»9b­_ú)ßvxjf Ö`Ð?PË™Vó5XsF=7ˆQî“GæÔ£ÒÈüšíVî ü¡ì'j¨Ó*öúõÞ7* çì8VhŒý²j ÂaîÏ€¸½þïÑG=y–¶is4ÔÓ©²ñZÍq·>£hv¨9VÅæOÛT»]nŸðÄ؉Zô6{ö×;©Ž+.Ä.SåTèÚ©…r=ôÈzˆ•¯ÏJñÐOÊü‘núT=¤¡!N+(¼LôC)g½óëÇlÐ +{4XÞ¬šï½ZªÖuªý)GhÊ í|¤j ¶ôšÍ5à3¡pfõ¸ª«O[ž¿óñÐ!úªH|ßEâyàîµÁã¬Ägæ›o8j§´G$àNßÀ/hÆê{Çà+mú @¦v÷Ë“ê7¾Ü9Åd·ÅT2Þ|á¡m«wƒ¬'ÜšöÌZâu©lͪÕêT_TÛi~±æˆS/ÝÖæ×xÛúÌYÒº"‚Æ®´ªýʈ3˜¿ Ћ*NT@mv%hsºê Ö‘9ªMR ¨…ÖÍùt¢á [¯Ý)ÖÑ`E<5v{6¯G¬(ƹõªCJ¨¾t& Õ|&Uoâ>«—ˆö8g-PÓekíjÑfS™Z{ÐHõç©:_bŠM"`ðü(hZ«ÏîæáŽúšš‹|¬ÎO‚r°>ç}}³ÞOó×8¬÷Ûõ_gS%Z§Þ *•Á:#¢ RÖŒõ‘}jWû߆«7gîÈ°¾)ç`­!@óMÌÅê”N{ã+mÊàÅ› ¤ðžÅ’ ‰s›ŸÍáÇߨSôæÁX¥Oèk«Ûœ#ábµðþä[ƒ‚1§Vª³% +½|¡òHXÒ·ê +<æ b7/Tv§ñ…ºÂ4¦¢fw”j´ç“I™¹Ãën·ç3øA•ÁÐÉ æ÷7ühõ ‡í~|‰0B ~û;&Ë› §aÓa¯v@(Ù \ÂM9cו©X%­³h]ˆ­‰ª¼·²x9â¿l‚›u¸eÊø, ¢m {¢×»ëì•QV´Â|ÝäP¡Tž{ í,`ÜóO:›2g¿« +Tš )i¢î$àEÐ:s€09øTØËÁ.ƒ+õ]ÇS`ìñО¨ŽÞÏÏ»‘§ HQˆl„Þ:ÁFþ‘èl~ïP´Õ·Çúô¤ÕýhG„¤Œ=û(ò û‚§hÂ1Z϶xß™rîàÀÒólø¨íž-ß'¶®·W6«7k‡êòY2›ìuÞ6^»áÃbx-¶’kUSÃ%ãr¿`D6Ò—{;'ÚîÆñÃÊIz0®›Å‚rbEeM‹HÒ0ÿž]“–Ò›O©ÕôÖZ˜)ë ÑôæqxÀ+Ž²¯ûçÇé-Í)çZÛ;õ|*µò:1Ôqã–Œgæ‹Ñ óno”ÌjwɵL§w<Ì”Go‰#2.æµ¥ëì{{åz!šoJ‡µ©-™vÓ¼:¿ÈTr©«ÙƒŠõ6Ó[ÅÇôÆ0ÕIä×¢ãbl¯Ñ\ˆ"°Š/Ïgã|óñÚ̶Óí›föm”{3ïd8^–óuùø3½µ»rMû!Sæž^Ÿzä¯åÏüAã œMZïK™r2Ò¥s¸©6Æ Qû=–¨êúy,÷¦=one¢êr"{ºö’HçV.‹9g¼ºsuy۬׫ðW+Qh¿Ñ‘ei½jZK/­§ÃF¶Ý]IãÌqyùæOo¾© Qcóê1éÖW:‰í“Íu³ó°Ý2ÍõaSÍ êrâcCv{¬ç‡WlæŠc^«Rc£•[¯’ý•O¶cÉ5'Û6Kº‚Ûãh:w°¹.¬ÙúìËÁ½Ù1s½§ÄÖUã~C©E±Ûn”,hÇXÀ–Ü×Æyà´“ýˆI†šWcI~Œœä׫[ËÅpân£ðá {Á* Q©¶x á߉âûkëºpD«çÖ +/´3åV9 ¨{#%vv +kJ~÷u›õs½½µÙx?}Ât'Lú;ËêlR){èNàÑ›€Û¾€JŽ†ez8›FPçá®fÜïõL%ÿžÈ7×> ÕêÊRÖ¨]žoíçŸw2•·ú(SZªŸd*ŠJv?c>ÞFH›Æ]áæew삈b­MŸ>¼Î¬vj°Ï7춗o– „'é¶OìD6¯éAÏ Ñ³œ¸Êj7‡Åô`ðv©mœ\ïâYzk`Í[]Kd{öS”þ…‹pçp¢ ]-D7ÛãX1ß–³Ò¡¾AþÙ“Z´Ÿ£ÖKoVF‹™Êáh< ÊÀN +pç3CY…бÏT+„ÓøÂvŠ±å~<÷f\ÜjÒÖjÞ Ö$§´½éN„‚ÃÆñ^æùPFlÛJ¾á¤¥òïf•ž}º¡Vù³s”9{ÊžsÍCK’jb®Ñ¹Eâ9eö²mcûÚëÛoße÷+‘íÀ¢dÎY~ï#ê¡Î·€Â¨Rsãº79Û`½:ùkm´×_¬ÙvbS; @dó€(4¹Ö°eµLÞ—”xä`ß[Õ¦³±Ü!'ù"èu’Ø>¼;䃾>:¦’¯ÑãÕ½Ñs£“)¿æ×[Û±"í ¹²od*'½×ôUå Z,Xç7 Ñ­„TxpÁÑ/¬õN•â²±qCö|lêŸË|< í*%ÌË÷+Ç„ËVdËX]Î73+O>pQ,>ÇÃo¸ Bs»9Rz-ð•í>|ÿðq¡õ*a¢{½ÂZ¥¾M x~MöÍRbgÛPñk¦¼–îæZ­ó,- U*ù££ýdq« Çñ+¬åuµK«WNÒÑlûPJÁ^Ý{çÓ´v[«ZúÊ‘Lý®{)Å:òO#+É ýëðvÛ&Ýì²jZ,“_³„ZzmXÍ*kx{Íñ†ï䯸]Â6¼5Ëè?±~‘ÔÛ½ ½çi¨ £Èý‚8=¨$öƒ£tòÁ.„É»-?y6:%˜éGÁ©à2±GøÏ–ië W…£@ë ˆØ”'=ÞBØÀ(Øcì~Wh}8:Û"•.wÉ?÷”´òSšCqë\…çÝÊ)ïå)ã®Å·Qs¶öwlD`p-îF(á=,2QVý~‡ÃÉZÏûIñ1FöÍ×âî¥-_Ù\ý‹¯Š}’Qf!Æ·Ðâ.|,®ÊC•Iœ…Qf‹Â!ÁÁ!¬*O*↰æ~‘cò”eÒ6l<· ÿlĶ¸Üûß Ð÷ÅÅ¿«vÐ##Ä´£ÂàÍàˆHJׂ;ˆß¨¿†/ò›Äd²8ÖÆÝx¯ =†¯¹?„†ˆÓx*…pì¸qÆÿÂÁ2èJÉ™>êõÏ Wß³3ÝÌEóõˆˆ•GýÃÈT®ŽºÅ´,,D÷¢{ÏD,Ñc† ›ŒÏ£[ÅçQe5½)•Â‰ÂÕÍ>geÉ%Aœ¤‚ –$Ö[ˆ¢b@Å‚‚j&ÛDa9Ê+Dô‹ßÏÔ¦ÌÍ»ãbFýX:D=À¿ * Q«w(™…äZSËí~jâ(Ùg'sÑ-_fÊ£ƒN!q¼ómÓv´Ï•Š‹¸§|Ñî+Ÿ™³\¶2 “®:>À ¤0ÿJaÃܬ_^ḋËüá`õeZ}ÿñ st}Näd6³ËèÇÞðÌ0ÑïØȶýru‡Ô|Ýa¾æÒ× 3Ó¼|tŽòÍóay½Uº'(YøÌQy[Uïç+C3U¡…¨¨ y +K£öM欘¿~εÞõÍf'úJäÛU™ÀdçÅ4/=˜\Ò“˜[n?z]Lfý7iƪkØÉ¿&ã»t€suç…jg+¹·Ämn½µ³½ÁöÊ'Ÿ.òAÐ>UôaBaÄ=(í#øÉ jŒÇçr +™¥T,ïP<÷@i¿M4 Oþ÷KÿÂDíç(GÔH_žwìüÞ­Ù$êxA‘”=í$Øíy8þPR(Žø°/çùæebOrjżoè DMU^‹ålJ¸ž¶–?f+³øô¸)ºã%jw,¶û[kKë÷–Y?>Ìž—Òd-·S¦ào£™óÓòU1oð²÷–%óc\+IÇGµ¾§#mÊÝÙ—ÜÛCx#±³÷üÌ1ë1Ev0u”ý8IѾÔôÎìXZž²ÊvØûà¤F§-$•OÚwžµƒh|SÆþ³G.ÆÆæG·Ÿ:Oì^U?ù3½yt»HÔÇ·°¿ï³Ly¯ñNöe{ ¤/Ã6îîÀvl¬8€rMü@èØîÛpŒ›lŸ¢JFyª’.*ÃÂs_®¥7/WR‚‘LÑÛ‹›Íqé=S¹¬Þ’6ÚRñ!­’Ùì']ê4`,Dív±×œ…1²[³ÂO¯ØÙO¯*ZçŸ_I12Ëùšæ”¿yz…‰Àé}%˜\Œ—ڛż~§$vnW'{ü‚–/t{,œÞìñó“ìËŽq8Χ.ÉôÆéû‡Ë¢*Æb±›c¦A‚|Éþg¯•_¿ÙLsx®8ëoÎÓ ÀsÈ&·—ßfôz[6¯d;g;„‹ùS-{6Vï¨/†…ĪÑ8–‡w€JÆRäÓJo\¶6›ûµ×ôÆMQìŠRlª»Ûµ…(rì­µ[cûx7«)ňž>ª“ý»ÛÌ;ý³c'ŒÈ±2%T9Yô†7¯ÚÇÂèv— +ÏR÷1ß¬ä› ÑÂZïdœÑÓ™ûB½ÔŽÄ%f™|~‚j/šÃõÓ«Lå8 ÇÐNNN~+–$íq¦t±þ¸Ñz²šdǯWɾâ eGÛëcŽW‰€òpY|Þ|[ªì|vÈ&¿†ÓŸ»uÇû{5ÎXµþkJÞ¿Zced‹‰'«Ü%ó’}·RÌ9ÆG¦’¯ÊÙ¶±$eí³•Þ8;oㆠf‹ð~b»~sOMþ$sQINâFd˜kmŸ ÓÅhÕØ5nör§¹«]W:41·ZõûüAý¢”Õ®•ÑÄÆ?E„yHçÅØáî±½ÿP1!˜mbŠ Ú‘±y´”iæ_#š¹ÑOåˆ8ÔZõºB©:k:=@ºû­øRô°{«P[¿ëû%o:3«²¼T.>G–Ëd}‰‡ÂÓû¨Ž6ÈIxî +µìjl!ºq’Pï3•Êr؇,»n·ÇÌJÅÐâ˜aþóa‚ìGŠ¹Ì#Ó7„Žå×*ºk3¤³µ?2…JÆ´/^mí%Û!‹ÌJ3«#¨‚ªOv×*®x¶­-i÷#½µÓ¿&¼²Ý•–§tád?VF‡YívñrûA[ùÒJ™˜çë%–z€Q>N‰úQè‰çÎH÷€‚­“,WW#DE¸³½.6d+|ZŒß–F™£[¹! º}tÚÈ7:öšxgAf#TþìãÕ£¿”÷^Þt᤺·”à&N{ïù¦}ØÎ×V[Qûøe°UÌ´»‹h à Ù}¸ÊÛ8Í­?䎶֩¨¦œ—“Å‚ÜŒ72‹|C¯c„òWÖíbf'kDûzþ ÚŽNПv)N`óx´4•?:XÌjé?@[kãÒm1-žÉžwkÅðêšLTØKhc+ää¨/äîZB·êj,N•K³b>ñ»«tÔûÆ€°YŒt2öÛÃi¤˜/”‹Ñ¾FˆËµóšV³wšw™Dþò,¼Ã˘åH.Cþ¿ŸÞJ¦âSGJË{`,~ • +êä‹|d´Xö²ÖÈA`Iõ;Ürp“·3G)g•x³2µ yå°*óQ¦÷v¶*jNóN*¢=×øæ"¾k|¿Ç[Ø+%_-ï=ìE÷-‚/c¡òè¸øHv?½„¢ÞQ‚œl¤Oò©³·-kwŒµ^q5¾f1H‹U"ÝÆËÆMÇ®_±+8ãðR\©h‚ØUŠ›k‹ž?c"Ûî7šÅØÑž-n2âêùC>´3åZ¿IµEÅX|Ív­wARJï÷?ù©ôø Žr­nUÍÍó³R¦<¾T&Š>ÈtZC¶²2öQ|9‰Ú³%€³­ ±“Æqdúvs™Â²{ëõB­·ŸüF½Êòõc¡úr°˜í$–ÍqL>vOoÈN·„{û@g¹›X)0”æ3ÙÄýÕO"rœrŽŒäó}°rS\÷¬6ÎM‡hÚ‚NŽU:1‹PÐT¡}/¥óóQþ0+Â5è½7Ê´« +£åƒ0ùpL¤ñIáõxšËÄØA¤x·–9_}&²Þ‘Er|8I÷{[óÀÖB”zeãô©6.¾<)À1Õ½L¿QiÛÉ +äÎ9…CTú¸'Ìá&–o.÷#Æèh´]|n§Ö}£´žÖ^Én WͧDîƒÁ}Qã]•Z5£­¬ôlùúh=cí Gà6@ºƒêMz¿YÌ>~æ­§EoÃPæÑ‹nö ùxX76jgË_m mm¢\íïŸGÉbzÅ©ø/ã <(”ÂpÅ DXs:ùf‘ñÖööî¬ÊC¡VÉ\ç_õåV€z¹„‹Ó,—KS:Ŷ¤€{±Å+ÈÜÛýÖg1SJ÷7N†ãžß4hŸ²}‘¯çªùÕÖzÝÜR Ná®yPpÍ“X¥LN|¹¼©š±’©E°…ì!ÙBˆ•îÌÊËþz1?Îàµä9‘8V’ùb7sBøg%žýØÜJ2%Ðã+!påq¥á9nPjÈ:=_Ìêñâ:¡cü«@-Ù÷J„A™Áî2sÑ;~1v>ã¢Ê 0ήlÖ³Dß7”ŽgœE`ÞûÐ"Ûù\=Ì×nœ rÚvö÷nŸû/H#Ýã3òîæ0;á6!àÉbÄœïïÑg—è£Ï,N‰[G•óØði˸,5ÖÉI½\+FåÑ›Ï/Öò‡á¦$®€VîËÆÕg3ƒ,¡=67}»• Ž—ï^»ùfï1¦£©ökFê³X‡+{ãTÿ)st·¼Ý€•B}¼ødí«[];Ü,>¯]mÖâöS)ôlÛßÏKîõ­¸·Þ–oÊn}c/Óû®ƒwÇYþ±uo«=°qX½:ñÖïS£Ï‰ü{R\Ù‹÷;®ZD¤ë¥Tþ0ÑÅ‘Û£¥B½¶Ö/‚ke—•v—aãŸA^ÛòYºÍôæSrœ©äÌg¢u6ˆ†½ž¬fz§èMŽö¢Æ2›éÍÇˬÙ=¨ÈùÇ×G…èûåvzwFèE©AÎ_k‹,Ø'ŠŠË{Ç}Ï)^¶¯ ã°õjT’·wJ!œÌ®¿uÕ%ûõz@æºA<²r®¾_!ç%±ZÿÌKKÉ.²w=œ[•‰NÐY$âþem,§QYý¿×NoÒÐYÜꆡ<ÆX x6–«?9'ãö¨Õo;¿_*äº}.ž¼0'ßpÛ‹Ý ¹}Bü…]¬òæ Puà„FoNˆ½" Ä–†~~sº¡aõ'è¬Ú +…àC¨:„bšm¨”/º¡›R¡Ë!vIþõwökoê·I£^7ä4Zð‡¦Ý½Bš¤–o µÌmÚuF=è¢î„Zèƒ] µ«¿B˜¨j¿ßnÕi¤á¸þÓ;èæñ”× ­[m‡Ædv½¦7|kw? Jj>(aãÈrŸI×õA«?šˆa4­ryT%88hÌ­©a·ì ål÷kðÆ„š•A«ƒYævŠUy¼%‚ŽsûUh¿–Â&_1ðn…p8_­]™ûJÃWõÂ÷¤næÚJÕWç`îC^óÂyý"¼h ½Vw΋]^µXŽ®„ùv¿ÐóúÅÐ'Ùù¯eüÓõÞbÎÙ\6Œ|&•…ÖŠPê‘Ãó«àk‹tù#>|/’UIåç!y<èIàà@œ#|ÿä‘êß7A™}?i _Qf0µi;wQ2µÞOη)$Û¹?B"gÀùsõ¯38Õ/œ‰Ù1¢´¹†£1F¿cW³ßŠˆ2³¥'oÍÕôå(­¿ë`MˆnsYǼ¨–Ó‹ +C ˜*ÑÉz†r®:|_g_Õö«]Ç{KJuä‰@‹o½Ÿ÷[à›¾‰j]7–„§uÒ Á[úU¤ƒOnE{^Á1hxl ±öí7náp¸0¶K‡ÙçðíÎŽš_?Û·7‹Õè`;[nžDŽÕÍÓpxp-…—µý‹ôÆYzd·Š'¿Yg·ã74\üöÛoápü%¯®,†Ã±ÃÅ…¨¥Î~óþCF +éÁùLþXÀÏJÆ||m“?¢çäŸEk/›vöp>Ëoð]O_=~ÆÉ++ø³ðR?8„Ÿ›ø3«Z›»ðóŒV®ÜÕ7àgf¬sµOþˆ-Bß[«ôÞ$Å·áge!º·’ʬ‘¿W¡ÒâÅ^ôãT%­BëB®5¬¬ÃOœ¨žO]$‡ä¤Œ_‹9gð ?/ág²ð²óƒ?ªä[Ï*oµþĵØdf†Þñ¾sÍÇíߌ;£ÿV¨U¤ÅâV~œI©Ç»«jv=YÊT>‡Ñb!»ªXÇ µ¥{¼PÍÕ'S«~" ¿)ùõDÉ/ÝoH/WòÅo¿-÷—é Rg'Á÷c! +ÿâÃæ]6Ùæ­~ÜÑBáëÒ€€&²pNœ¦ä½ìíÝE梱»œ-¬G4˜O,õúù”Èï|l–{7«{Ùã%û±ÑXKÞ¶²*y½ž<ƒ~…å7–?Àuâu\Ü*œÆQ4îo$ +'Ç28„˜‰ÂqñÔP—Æõß~[ÿ8< +'vF“ÛØ7Ø3”×Æh!jvvRpëgîšÅ,Þ®¬5šÙ¹—/³9Ù<îŸjÒÚ8sti’|Od“öù=¼d\[ót[ͶõhÍ÷ŠÐ~„ã±x~HF>îeÊãð["[Pš{y„5n‡PóÙ““à–Ò½ü qø·p$.'Ñõz'¼Ø؇ó×ÁÙA.Þ=Á<Ù^ÅWÃñÓ›ÏðꆽŠ8ŽûJj°{„_‘b¬gI¿×Év"=Œ¿Zð) ¸^/.?n„#kùÓð’’¨…£&9ºËÛg:Áý• i/Ûm߇ã…r/¼z´™ +'γᵫ§«pò¡ðNÕ’ñðúkk;,uJçayh:a52Z +«ãK#¬/o†µÈSØ”Ÿa+}¢†í¢¶Þ¸ ?…7ëu²Cáíåk3¼³sxÞ½Ú~ §»r"œÝX͇s÷‘çpay°.–>Óáýhÿ!|pÖ„’ãLø¸}Ÿî&âáÒ²q>¯eZáòÁ‰¾”n.ÃWã÷ÅðMo­¾{Þ~#£<Ü\šá§óöCø¥¢¯…kWåp㦿n6vŽÃo½ê(ü±,í‡;·ŸáÞYj/üY¿þ Ö”ýÅðAu¼égO—³‘åÅØÛÍåb"·+-&‡Ñ—E餺¹¨Ê'…è¢þ¾q¸h®Ä77º‹;+Õ­Åôàös1÷vQ^,>i‹íÅã‡ý³Å³Æ‰¶xÑ>ï.^Ž®*‹·éç!Ó^\|.‡Ÿk2rsó@Yl]’:‘þãb¿dQF+—Fd±ºË•åµíå»ó½ÚrUji˯Ž~¿ÜÍ_§–ÇÉøõÊòð:¹’tÖoV´«ÇÔÊæùæÃJö¨§­œ•+¥‡Ý•ëV¬¿ò4~;Yi·‰•ÎñÁóÊè#½KèßÊÎv8–jÚ71Ã66bÛïÖ(V(nÞÄNUŒUºååØÃCµköcí¬dÄFÙ£ÅøJÖ©Ç×s‰RÜ<>Þ‰ï> Sñ½Ïü0^J}4â7‡¹ëøK/|oíTÈ(ñ϶¥¯FóŸ‰ÕÔò}dÕxÌ®îîk«ûê’³z¾Ü«®Þöœ§ÕZ»ú°úÑz¼_õžïñøëcB±zωÍýÅZ"ÿ"5§kùvâúäf¨.£‰÷+•-^˜ ѵØs$»¦N׶’½ûµÂ°ØZ;«#k·%u­þ¢Ö:µëäb·ò‘LFwIs#™IfN?¯“Gíf/y¥?¨É—‡ë“äGêâ-9~½“R‰ìýIÊX®µSé·>%ut»N]o-¥ªG—û©öa·½¾x¨n­'/.jëV#¢¯çÆûOëg#uýîòði ¢¾þYzªI±XfKR/#Ò®^-J‡£RDºzÍ_KµsU—ºd\9z;^ˆÊòi|]Þ.Çß䃪t"_öLE®É¹žÜÝ/])ÑÏVFQ¶WÊN¯ÐVŽrO·ÊÍjjO!r°©|GVÔx¾ÜU -þ¢æô—KµdfÕG{)­¾¿ZÚâõ™´ÕÖG¹„¶½e.k‡·‰Eífui¤9·KCm¨. õÄÇêH·¯ÍE}/—‹ê—ëgq½ž|•ôϵUÓX]Íî–ü²o³ëe£rQ~6jíXÛø´o—ÍÕÍ0íx£HøÙÞÓÑyµ£|˜AoÕ6€·œœ][[ûû}ë(—Õ¬»ÍS«u”~·#giÅVœ£’]¾êÛç…·-ûÅY"çÝÚZÛXmÝžolÄfíîmÜ:oýVe;¿Ý\:j÷6µÜ^q3Ÿ_nVòÇ›ëÕÍQUºÞ’b}+{om•Þê[Uë!¾õÙ¼{ÙNîÞæ¶wVñíÓê[}ûùjéh»¨«;k{Gƒí|íqçôhõ`çùî„ìþN¿Õ_ÞM&s»;§ƒÇݳþqi·Z\ËïúU;½~¶·žÎ˜êJº‹†ÓÎG?~züȨÕûV¦Ð¸}Í\¿^73ï+Õ·ìÊÖÇ{Ö>w²G}i}ÊîE²ýA=±Í¥NWÕ\F9ÞÉ•G£Ã\³¶w•_º7òæÙé0x–Jå¯ß2ùþËI¹°¾¼Ó*d­Õxáò!œ)´V;·Å•ë÷aqsýÍ.ž:—ÅÚÙh¸ÞˆoïéIëiï ~JFÙ{ŒÖOö>ñÁ¾dåöó¥ÁÇþÍ[ag¿³Ú~=X«ídâñ÷ƒËûzîàÝÞÆÇÊéáŽI–+o‡oWÎÎQìì~x´}÷tuD9›ÐËÖè8f n·Ÿ•ìBô¸¼’Y=~»º?‰Ëá«“V6wR¹l)'ïÙÍðiÂ|};Í…ûÓk=rzÚÙzÈŸ¥NŠ›gù'I9»_¯}>VJÊ[5R:Ø}—žGO£óðcs|n{‹çgv,º=o&ŒøEl½$]ìªïæÅ•µ¾{Ñ9½<,¯;Ë—å½õ‹Zùé,Þ¯„«²»U)mÄÎ*¯£fírµ~¹ÌÞm\Þ•Óç—ƒÊæÇ•þ´-_¼íœ\5— ï×ñlY[ˆ^g^›—×wVtñzÐÜ)Üûï7g‰ØæÍëÇIý6ñ²dÞæ/k·÷Šu®¶šwoÇ»w•µÞ]§?¾—›ÃÕû#õõé¾ñTÛyˆo×ùhëîá¡Ößy ?¦–¢›§µÇËóòñc¯Ò6Ÿ´ªy: ß5ŸZæêåsêò²ð|“ÍçúÍ[â%®Œ_r]ùãåé~T¯.Ôª»é«ëê]ºT®ŽJçµÍ‹R©vÕ¸*Õ«µ‹ºu:ºZˆÖËCù¾Þ;:¨6Œµ×÷Æy]4:W÷qG;”t§ôÿ{ûÎíÖuÑ'ð;ÄIÛqS/é‰kzï½x'{§ÛÉ™?óì e‰’HÛßœ½NVl! ‚h„Å‹µî[}þàÚù~ü³{zöûçõÏŠú¬*êúóîáÌíókÿ3û¢îÌ¿ìÕÞO^Þ^{½¿ÚUqþïþñÜåß-°>ÿ{ëÿ¯Jïÿ¾ž×_í™·§×“ƪùÚÿ÷uÿ¶ÐÜ·ÞÎåîûäÙûÒûÊÂáûûÒÚøÈìüG3/_}<Ö¤ÅϲYýùÜ訧Ÿ/K _Š´;õµwx{÷õYÊndrßÖÝ’þ}º~?Ù˪Õno¥z}Ò»5Ûýbîí»ƒöÞ~EãÌE´Šýé™+æ(·É÷Îzõíí°‹~~7H:oø‘^õ®·¯öùHÛæ>·~Så"½™Þ -€ÖCjüÝlçøH²…^_ÎõøH÷ä+mR™›õ‚…æ¢íçËÛRý¥pŸ?oò‘jÅ«Ò‚òÌ_)øw ×¦Ž¿Þ¶xk•Zk›+¤ÆÔôG¯¸(@z~#µþìzHÉ{­\´íìÂŒ¼q{ÄEÚî(GBò*KGw)ðâCÓ¿§§ÀÉW¿K[ˆ¶ÞÕ¹zù&©öb¥-iÍAº—Ïêúûí—‡ÔãdŠö¶wýòq$@ºro˜­i™‹ôfùî@„´“ÉM«…ù+þZ²×ý©—îé~½¸ô=ý¾ÅCZš›[ò¾øYivùbe—T;?—Z¶´ÃE:Ùz6§Þ•]ÒLNj]_·k5¦r¯ÛuÒ{©]¾;á#mK«ùnÁ<'H3¹ ¦ÊËÒóJ!@àåmÑ!oóêµåCz± m™‘ÎfrýÉN÷Ó8¸¯(€Öü +òïÖ厩1e¾>µnDHÒöìµM’œwÿZ׿çVþõv¸Hçóªéf÷P•xHQòk—éð¶8É[kr³Óݹ¼(¸HOòÏB¤‡/ûw)ø³¡µ^¶¥“ͯ>Ò-5wÜZYXä#ýÚ˜ä!™ŒhOnÛ“?_îJ§Ë› >Òí¥æíÍþõ5éõîë:AŠú%¼ÖçúSK€ôÊ’®ûo>Ò_ﻶ¥‚öv½ò!$p/wXÊ +žKÍ¿ï\¤ÖNe2»r]9h羃‡æ÷Ö¼u>¨ÅÀ¡)^lÝϤJ~©Ðñ¯tKº›[E¤e)`A´0ì¿ïÐ_ì‘ö?g¤?Ë¥ÀJ³W÷—EŠtí\^÷ Âr¯¼”,€¶–Jë²V@Zÿ ‰ÂΤA‘.Ë›•€ ,i;TÓ¨ÓsõM‚±8h§{½û‡D*öz«ÝÏÿndHûú¿9GÓ,ÏíWäüìÞeû¡{kø'Õ9ž¹ùœ>=ß—ßODO_¤Î]î×{’ü z3‚¿†=˜)Õóú×5O K¾=ržö_­Ð©4>^&ÏxÏ©PÜ[Ÿ»>µ”Ùƒ;ñÓ—û›¼K±ðóUeæâQøt«ô±¬ˆŸ>ìþ™÷ž(fLíN>>tmuf: Ç}úôOþÛüíqéïÀ8ý#OÎ…(vü°qøÁ{N¥\£ôÚ>=Í=Ô²â§W¹ÅÅ8Ïïr7æ´ðé¿Ÿ½¯Žðé뉲¶ï= Qìýµµt#úk˜Òþ¼.|º¡hKgbŠå?·D=“Y¿š>m®n?t…O7”åIYL±Õ¬2]œ<Õ;Rsqv°æ¹Â|àiéh¿¿ì<­W‚§²stמ]õžk}­|à÷ÀêÒSþ²îÈŸ»Ü>ýrÜÑÖ­NEÏÚg‡þæ“cÊÏ4z˜õleÃ>óßþ¨âw­l¹qPǧÄs½7*#|÷kÉ×›Tò‹{GžƒŸã³Ç–¦Ô<üáÆ;9èé0'¢¶m}äÁ=ûÑ:9øþ,¸ø¦jŠ &›½ß»jÅ'n{“™œ‡–x:¤Æú9—|¤Úù…)(‘rÀc×J<!RTy"¤O,Rí|d­µ³vÈ }š™™òëßEªÈ‹Ö¿»Òö›iñœx¯,Z!Rbý ‚?Öÿ‡Öâ[ëµ)¸¯‰‘¢õ/DšÉ¡ýÿÂ_ëB¶…t+/DJl +)ž}Z´)Ž|»ÚµèÉoÎFëOïIàn~ß?س/€4¦¾ošg»±pú‹ÃwŽ´Xƒ5_­:Ò2ttï +ïH&@³Ôúí9Âeæp%pâk§æÊÞ¥¯Âӑ㬣L¢þþÒWqá˱@†X²;ÓÿÈ,–ìöe×}1«žhrÐ/5sÎòö§ƒ€˜À®m óÅ-~Q @ô ÄÞZí¥Ûȹ?X‹Ù‰™íºÀ ÄÒf—{·±ìÄ”`ÊÇ{ðÅ ´¿³êP»ÝÇÌ +öfüt#n0aiCÏåÈdÈS¿—ěԫ½˜;¥Ûß<°îvYÉ/KL-$$iyý»¶GtüáLž:óœõå³ÂõerÞ +Éáöïp2~ÿ¦ÜõÍ:<Æ[!¯ÇÄJ¾Ÿ„?©g‘XƒÉ³kÿZ¡2IØ}@¬ùJ*Îòøʧ‘)gµ¯¾ÓSžKw0n.N™òÊÚ…½M÷Œ˜X~ÑS ‹ž›¦_ô¨BÑ“‰cÝæÕz% ;eg¨èáÓ6ŒC`Á|ÊÍ2ýáÐNþàÓî¼’ŠmÑ©$ñ8îÒÎ'Ó/ÕbdqêòÉÑv©Û›3Îmw"R`U¬cVÕÝ«Î8»£MÀrò™jA< ¼îÜC[÷²|~WœÉ5ÛTþ’ï§ó2ç’0’XK¿|–œÃp2 êð““ÆŠÐ¹å™ Ÿ»ûVÒs}ê@WÞOžýFîå”\>–Ëøã²Ä\R„Ø$c¡¹ÉÛN »¡ø6Ô D‡yÖçJÁ0oìî»SbæEÖòÓæÎl©ýÐ-rEêþ×BVXxKRÜÉë ˆA¼©c$âdFnú" _>Szå7hJˆƒ‰ŽLN`L#•7ƒ¦tÚQ{ŒÌ'=Ÿ¤ñuùØÌ8‚ñÅR)ÂÍ€%<Üy™Û/F/-žÏ7=åçê—!¢Ñ_›A½'ZP&ÁéÀ;~¥7ŒX>Î陘=OF˜Û˜ }& êD¶,P' èâôkËÒµ0ª/«òlþ® +¾K ÂSÇpræSMó¯ùì§íü·¢‰¯ a0Q¼•m¥ž…(ÞŠƒ¥8|| M&7ò%Ùµh•—I<Žß‘L7çNœŽ3ê ¤£¸Þ¤7Ê@&§'½Þsý}Î`¾ÂtJÔ?˜'ÖE¹à·8ë$Mu,7 u’|šÉÅÃêSª©ïøFærÎ&"%c~Î}û´XP¢õ_‹!‰Ö ^à%hÜI³ò=ªD;ö$ÚÀ{McÁ‹—h™„7H8ØðÍ=•í“Ï1H4Ø5žDK}öaœô-» ãŒ.Ñp”Ñï^É8·A嵂{‹SËäxÖ€O |FyÐìÕ!ßFÑÊþ|KðyB±qünÔ Ø~ù“ÿ‹¡„–YÎùîï0Ó|:1 ƒdS ÙLN(f‡Ì~ðæâKû<ña ص@R“HÂÄ“Ì´ŽBâ00Îè)dC¸yÇéž0¼Ì¹M ƒ#VMu!¬¯„Ö…_·•Qì{†Iï'ú#{|g§I£YLìB¨ OSÙ÷Ñ1Xltûþꛧ Ój1ܵô!­°ÃqF±ï™Q\]8¼#ãpì{Þ(®÷*'©6ŒÖ…DÂ\}¬ }ºð6xOL°À·cIGBùÃÍÃrt¥/yÊKÃÚ·½ˆ-aIYNpûvv6J¬:ç‡ÁF=Ý̼8g;:×:‚b‰ÌÝÁN"‘G{YMtÎ#nOë@Ï“ZDä*A:]`JBŽ ¹=±Ç+¨˜êÕJH-Õ«µD~eŤ¬= ÓóÄ™kf¦$#e‚ÃõÐ_¶- &¼»q£=Iêa0uz~sk ˜™ÎÄä âNÞVÅ`é`‚Ã~™|K9"3ˆß'šTBŽ.G#Ó£ëé³%}ûƒëY8{@ªåúÏÙÊÇþ-¾M²™­¬ª7X7×äUÐÑ7·Œ^CÇj±p] bhèºè +:"“ÇPC'DJ*è„Õ‚)kè¢+èØjÁQjè¢+è|Vß5tÑt¾jÁjè¢+è"ªSÕÐEWБjÁ1ÔÐEÃ9µÕ#×Ð…®¯‚Îõ,F¬¡‹® #öX| /!9¢Âl/ßâYÛâxq%P Ù%Á”¸‘ÞF|âöl²z©VÀG:¶´~9Á¤‘Þˆ_Û§Û:掯LØšNó³>võ׊º:¾¤¬û¯{DñßñE ‡•l}$ÖW9—x}ÈUø¼$'z-ý”•1‘«¨)ñJæp-©ŠæÉšf ÄËíIW@õ“*„æq“AÒ‡EAcòf°‘ãUç•©è¥e»ù3!Ó 'Þ1¦Ån¢k¿—Wì&öO£/iÙŒLñ¹¦qnH3PÍà«N6˜?KßØ<(ßq•ÉdÖ}+•ÓKN¥Èí}PÅ)”©JLIíÛvªÃx߈ŻËví±pè,.pøj}Â$ð·ÇܤÙ!n¬ÚB³KXÆXJÁ:°X³+qßo\|Š:¾‡¸ú—)_ÐJ\š&.‚‰6¥Ã9ðùÅ—)ѤVx%šâý‹ÍOœEðÔöG0¹û—´ŽÏêÅÕÄD1ƒ?;[…hÏ»ùU¦¤,žã“SÌ`‘b±e¢ÉéI´¡(æü*kgç¢S'XÏ©äJfˆb¾a7Ë=êü¸et½\`Á¯“~ÌâN¹‘ñÇ]äéî$pö\/ÆÙ{î$vöDs‡"îÕ(°Ïî[5’•Ê µC·R¼~˜4üc%nñA†LdH«ÒN\ˆí£jä21eÎ ®<É‚¢_y’ ¹Wvä\?”Ê_hü$ 9s3"âäëéÂ;W‰ZJ1r!©#®ˆúájó!ì±õ¤'Þ;ïB{ìßzºðŽ¸Ì.˜A44^b" ™€+!f”ß”|õÈ0)%Mø#bJþïÅ"è¤%Rtvz|Q\Ô”‚ïî¨â¿´$i™ ‘1K ,áéçŽ/¦dFÏ8Já¸û2öR¸áã–~ŠEûî)üÊJáØw‘b¸ÿH)'ªð(…ãÇÇúgçQ†!+ +Ø÷'G>½sU“¼ÞoÕ«1nQÒD/JOQL@ÆÁÄo¶KiÄ^2œör©F?Ox…õv^å<•ça‰/nÖÕ] _‡Tƒè(G=xç»Æ•yøœ= -.—v_Úo'÷·©§ßfËž^¹i-í5Wj?yü­£eóœ´ o\4{+«ãh£¾V}¬××j›Ø„àðk Žroþ);'-¯ê ½$§FéR\wfíÏí±ìå/v›ÜÝfƒÏ Rýe6·ýUØiçgQÅnO5!R©u´Ua—Ûý»}/Bz´]±¤ÁZ¬¹RŸ‰Â‹Ý´³—-·3a l²Uì&WHýýøæßEvÆÔÔ·ñ{%*v»Œª:{®°û<:"Ùê¾<‰v£ûñ퟉‘6w.ÛBòN¨ó·"¤ûþ +;²«p`U“ß>ŸK7σ£Q¤vµ•hD­¸GáÕ¹ep Ñ—´Ôú} *Ô¨Øq‚”[¿[ú kñÝíÍ|Ž§´'>C55÷±úN0%æ^,bRqÉ©qyÔ{g'9^ÛÎ;UD—¢:É¥ŠÂ]6§HÆTC’wB¦Èë‹k"™×—œŸbšÈ ×Çëû×l$éúâ{ $&z\Ÿ‘À»ÔFè—ü¼´»âFDÂtפ è¢#½ÃTÓ ‡I[MÇó½8ÇWM—ðý0#VÓñb‚Áó2z5¯–nØÊGq5/ÏÏ´¥šÎGçÌrß?6R5o¨˜7„ QM7¤FNYMÇ»§quåتéxµt¬¿?žj:^-]ÂÜžÕt¼Xû ûøªéx»K"½c­¦ã7lîèxªéxµt‚·ÍPMžÒË”ØR¶šŽgœfr㮦ãí'jÄjºàP±=…‡ª¦Ù–ã­¦KN±QªéCïÄÇTM7ÅRWÓEÖX­šŽ_[=îj:Þ€eÌÕt¼Û’@üªéxâÁn£š.æfdLÕtñúeÕtâkŸ¢^6Ã÷« MéoyÆݯNl]$£ÓO)ŸŠN•|™X“kX$d_‚ êY4©À”’Š‚ÝåüSCM )“JÕv9jJüD +¡„‰ SDÛeÈD-æ÷ˆ–å~À#R§í¯˜ÆæÑ‘9·[VT£»‘ÛÜ1ï¹Jh’ÓæNØ]n3Q­RÂ6w¢J®d…t‰$¢ó“½Fw£íUûü+I—™È(U¢6w±E$ÌÈmî&ºÑÝÈmîhí[L£»d—G_›cxÏ•º|,Gobò:‹Í±¥(}mz&÷е<°´ÅbRþÞò`ƒºØÜÃ8þÄ2:tž##ð‰ +é’d4gâ+ ÅI‰ë KDÚaŠ$2¤Ž0ôççäd¶óT>XO„/M®™÷n(Ñ}O{ï}ÈÂ._60UL>xâl(*6,q6 –,£;:±™È¾±T>Ž~‚#^îì~ÂqõvåÂdvŽã’QB•ìÜJáøZv2X¢ÂZQˆ`aíáG¸°öðc|oÄÁÆÒȘðÌ6‰0cu—”GŽp Œæt¸K§¼Øz¢dòF­'r;å~ #:Üùë’’Ô‡ Óá.ªß+ö¸Kßá.é[ͱúiôÂÚ«oÏÌy¯‰ ka°x3'“ÄС½é†+¬ V>ê£Æ°¾p›[î=Wdœ!äfè8ÎXê9·¥d¾Xì8âZØ@S&î5¤]^šúv^{+Š¢:†s…˜H×ô}KXÆ”¤ˆ)¿xôD-–¤ŒÉ¿È˜Ø©7”»/Â2¦¹B¢ +õH·Ý=•s…$§2IS~ñ#h|ëWž¥*beaïÄM›Î0lsõËsA=\Q\„×û½›“ý29P†'ÍŠjÿŒ©ió x-*»"…µL¶¾TáZ¥öÔűi~ý¡ú,êÃVôº•\÷t…Aê/Mëçͯ ¨r,–/ßxH39$p°çœ¯ ï>XûÇ’wsaO€Ô˜Êi;;/¬ˆ»@Úž6ÄHÛûS<¤¤ï›Õñ•V‘DõÜ<#m6[¾Ì.@;ÏËîoNæïìbÍ¿û8µ. áØúJénvn5Áˆ¥¹ÏŸ¦§&aÍçZÐèt/kœh_yÖ#âÄ®¢K˜6 DÕ‹ä6È×Å,>q2n†±ƒ…M±qIR8µJ8%ädñ¤êIJ›â ›úãêÌBº1Y’F’Ñ©U­ uf‡iRТªîož¡*-&C“R)“ŒŸ—^i×çF­±î.u—EÁ”BYZÁó’˜è©²´2¹¨I‰ãÄé¦ÒRœ§åÌ'”WÍ«¥Yÿåu3Ü3e8WÆ^Oðêë˜ìMs\/o<¯d“½á0A5Rdx9é-ÏMs”7Ñz5‰¡WÆ UìÆ¿²Nõ^ë›æ‘eÿ[4± +pÔ—ÐòÞ'¬‹óZÂp€øñ1ÒçŠõ¾•ªßX´Ææ#ß·‚fØ[oZp':ÇäÆ*¾}ofÁ8|ÈŽôãFU´ïPÁ«ÕKoz?åÏÄ—ƒé +ÉBý,FhË]É”ªÆÊNõžfJú—§vTì8Ö÷M‰IÕwn«™S›²ðu:A)'ûž«ˆRÀ¸–Æ1ûçݼ?µã u3C ¯1·ËLŠÁâºèäƒÕµƒ´Êh‹­åI¾HÿÍåˆ‹í§“Šb[Ú•`°P¹0Ï:t(6d`ÒÀÄï!ˆ%ªŒÈNOP˜´0ONS˜´}Ãaú*À¤5€$j=t U"<Ì`lº*À¤5€H±á«=zFË UÚ*À¤5€®f-ÿÁ¦|¡¨Â¤)_\G¶ñ4å£5#ÑÊèMù29ŽU8ö¦|áþÈÿ‰¦|âþÈãlÊ—¨å(MùبNÊNÊ!Q\i°¸«ßèï†ÚÇ»¡öõKôn¨‘ûú1Kû¡D}ýÒå) Û×/º«ßPï†âôõ‹Ž +ñ=ñô}ýøÌùn¨!úúÅsò8úúEg†¸‘«ûúÅUr) ÙÕ/“KÐöõ‹^Z v1t_?ßDB]ý†èÂíë½ ^†ê0}ý¸åˆQo5ON˜Ä•²ÜwAÑ×/úÍ_îøˆ}ý"]h±1ôõ‹ §zUD_¿hW—›=D_?^™¤wôFëÇ7†š÷T}ý¢GÁ8ÿ8úúE_¨d3Já{°"uؾ~S“éêÇvgFèë8†’¿«Ÿï]©ËS$·¯_lÝëXúúEwõs9yľ~Ñ ×LEêH}ý¼ú-/Þs5t_¿h v }ý"ÎZÿUS?¾ÙèQ÷ã‹i'—¸ßH}ýÜQ¸Ç'm5G¨¯_DÝCl¶m7m_¿hKßx0Ž¾~"~¾I®~+9?¸IŒ;†q}ý¢ï›ýYÃ÷õóS;è-Û/]’†¸ßŽÛÕoô~|IŠ˜âÞÞÎÕH׊/X‘:Z_?_`;ÔÕV Þ×Ï-•Šê+:r_¿h3GÀc©ûúEwõù­MN_¿c} ûú%¨{C_¿è®~©ûñ U…–á¾~ÃÃ3]ý,#õõ ‡’Ù®~¢·œ¥íëÞÐbC÷õ‹Nt +Ä”†îëÇÙ¦«_ô{á’÷õ>né§Xúš'_9B_?Opñºú =èëÉLÑúúE†Ô{½¯_tA¬£‘Gîëç_d°«_ÈKü&+_?±Dm˜˜7Y%ìë—Ȇ¹¯»“á œˆêZÁùä÷õ‹V‘Uœc+ˆ¥z¿ÆQQ6Úî1?ã#ø^©úFÅ€®„oÅ¡ÝèÄû@Ù¢›<æÝÞ,-~±uð46å *ø4dõÓ#ŒTÛí.=w|žèVkß”þKÔÊF¶t¹3-ÕÎ^M +„õTÙéí¥õªTøÈÖ/¿4µ÷²ÑZz»¶÷g •÷b~í{Mê<ï¬N}ÿ®Ù«û'~»[ÉéÖ|gf{÷û@ÿ~=¿ÌäL½ª=ZÛ­³êfµ—Õ·ÚÚÝÚÁëñòÓá¶qúrÔÝÑ¿O +Ý?Ó§«E­ñoòè`óuîmáì³»\øø¾5¿s½•c'{º³œŸV m#÷çr}½øû2u©}^u眳Oj;—ŽO²ÕÒÍJVyþ>)-¨ù©µ¶¹&µþ±Ùœ=Ek«Ã2ÄW_:±uk¯>î¡Cí¶€Ä~’¿wrnvuekƒG,B XîÏ î‹:®•T4îóݽ©“‹£¥é^­5£hÏ¿SoOÙ'¬$݇fË_Úæ«mšÄìjëè('MwŸˆ¿¿÷é7Ù¿Ç–¶WgŠæ¼x+³4k<:,¹úo&׸huÏHÍúuûàÌØ©?M®Ô~6Vjý§ù–=õº×<]Qo€lKí·“³«ÕŽ‘}€qN¾ÃÒö™¹ôjb)ãùÍÊüV¶GVµúþ‰œ¼ºyzz[jžœuð7XðÞ[‹vËÔ­_ÒÏ{Äs’j?ÌF¸ý¡’¸¦Wóƒßä‰EÌøhÎ’ª_8ë_ðq±L$ÌT´Øe¾¨WتµšÊ¶Ãv¤R£œûm:ëër9ÿòÞ||ý¶¥Ú’Y$ÅÉ]˳uy¾þ÷¾&ÃÅóà~º¾˜É Õ+Þ#eíâxyð`ÝÅ·Éâ«L¯¿ ³S‘ñù*··&ïÝïJp&'·oªî£ +3NûkNÂïjŽ}¤ïHXeô.wŒ-?*t쇛œ;ö~‰‚<|Éh¡ïW˜ì´’_*`ºÕ~•=ug_Âä°žüØÙ%a/.eg¯Î[¥Úöá« +OÊä©R4æžHª,RqÚ’”·Ã\³l;¤“iÖß¿+8×r”Bñ#'ä#Ö(³sÆóç'QbF”kSý…Òñ|ÏX:ÖvVÍ맞º5¥xq˜%¬‚;¿TÜ¿S§îZ·ÅËM¥øûô:XúiÙÛ0¥ÑYDž>­R5.ÖeTäÓJãqWw~{;Vœß~/oÛRiïoß”g‡Ðé·+8âEë5>•Û·-‰þõíï3ö]ñÌå¶ …™ÍÝüÓ?ò€PìBsOå³–»Ól5Í<¦)¬½UŸgW÷ÿl56׳‡ž q/sênp`†‰9x7‰kjîw½;˜Ïu,CQŽ52[ufþRw~kÜÿuáTîä}58›ÝúÑu«ñ6ù¸zpt•Ïäš••STÖ $ŸÍ9û?_=‹~ÓÁ <ã“›®X ËÍCÉ›s×»Ÿ…æéÚöÆZñ­‡/)0:kM©z–›Á¶Ãy²pkyEmѽãñï,ãìµDwuíì¦/Õ^'«µ¿“{3ê´ýþ#I…jß‘c -I"8‰·ÂG­HÊœåæÕ +~œ+É 9–P¢<Øø+ø›)*±ë0õ¦J"(„0ÒЩ bJ‰³ûø`³€¹¯ßXg!}Ü 6ts–Êr=÷à~Wv¾;–‘§77Ì“NýîuõÉ'U·$IØÄØ”Yi¸öm;ùye 8ÿ$ª—~µêØYíz™~GµE{‚ ±þ­½M~´äN¥%ãìØå”mû³Äþcoøök3êÁpŠ§¥ý:œ°Tiñ°ù$=å>h],>ΰæ éu½°yêZ¥~ô yö&ùo“!`€»\p€ß‡½o0’¬_ç}Ù™ eƒ1¡‰E™]¸æÓÚÁnÕè (ö›%³³”˜Zÿr锼dÄžòæÃz? $XQK¾Þáwh5™Îç•é!E^×@‰ÐèºD¸b‰ð{¹ë’ ðÊ“2ÈÉ>"¨+û…‚C„ûåÝpb:ÁAše·$Šne=ébA¸­¸é¡s]üwœ€h>ebs÷Õ!ò+€´t{µ‹ø O_)Aç­<ïóY)jR+º4›€Ås¸Ÿl—¢çk‰œË C-ƒ¨ ņ[¨ ×šp7¼9DœJv7†áé‚·°ßI)QðH)^ÆòÜ뀼ëf6І½àQ"b¤,xŒ-u‡H1çe3~È"}‹}qÎÑš»ñç×Íñà†æ1VêÇc ª. 5Dô‰OÄc,%†â1AKΦ̉2È`ú>¥›’âÁáIY »á àñXâ!Š,Géà 8*ÅQåo‘•¾ƒ9PŠ%Ý¢·ÑL˜·,Ž*‹ìvúHI­‹$Ëð64Õ2˜¨BQ$¸“2‘FÎæWûÎJÿÆ?Äì¨çs–ä]8|)¤Dôè!”–Q³`ùrŽ˜%\9 X¼&âè¡Ù¤\)––³ÊÚ¹¼Î|¼þ™Ûð>ªÓsõMæãòñÖ–û±ät–Ⱦ01ö’'u?Ò$m÷#½•|¤/ï£o^%o^”“Ktfäʨû¯[#î3ëa¾²a_d+Vå"[z¬v²åÝb™Ä=ðºÉ ˆuªƒ‹‚-Ë> :JwV,b·ºÑ¥Æ½Ÿ~¿¢±™üâ¹>ˆÜ·¶2ˆ¢^õñ£ìäÝß +ˆ¥õû†×CøR“õÄ1%¡LD?Ö«Ug¢ÎÖ¼0º|òµ6pǵ‚ä,úâ–eûÇ ;n–ØxëVÉ hn²Íò醆6=|×êôüóê߮쑅Dz1xÒ^œõ¾eã¶àï30n;xž?œhàų«\÷ÿýƒ™|¾ËÂǽ8ûî·N@e¯Eè²»FܼA²pC-à±Ý€ìÖðÄ—Á÷4|<&ÃÎÒPMmïRu#¦¥ÒÒÔÂZ—A<çó-/¨|7ÀrT6.*ÚýÂÌÜóGþ¸µÐÔþüO¼IÛgÓ|Ù$^>ÿ– ý¨gXoPò.NÂÉdØ↵o[íÖJ¾{ÔXZÌ:q˜ó3yÀ6GlP²øv;åÄ°Îï5—÷€åÇv¶îa·B® €Çháá¸æ¢Î$ùak½¿]ÊNÌìáFY²×Á3®=Ü«ƒßž42„³É¯dÄ +¹Ä‡gƒ°áë¥4ȶ­½Þxœ÷ ä—·–œù¼>©Ìƒ›¬zëÄN·¿sÎZ^ß âàªk–jÍwoŠê/FkwíÍš²|õÈähbž]÷ƒf•²¤ÚÏYÕEúè0ÍÏ$·å•üv/~{ò‚Ôäm°ÂŸZh>úÏIk¶¸4ožT&·[wÛ9›lgnû¦´Ñ.aÜùÚpNå™4Eüy^“ì‹ÃüÏE0‹ÃüÏE0 ';1Ìÿ\3“ób˜ÿ¹&Y‹Ã$Ìÿ]ʘ¦­O˜–¥OÔ~ߺ½ÝÞßç¿åÌ|¦¶º.ËÇOŸ­^·{ÔýïŸÆçãï{÷ãgbn¢¶zX__·ôF÷ñó©;Q¦ÞteFÅ™¬ôd¯Û}ÑPÈ_kü±Û¯©ƒÅûÆé|‰‰•R«êëè;[~ù×ÉäP©æ²åÛ§ËlYif ~?žÓPUªÜK_"f§ÝÞÚo³´½u¸Á 1"û˜É5O׺+µþóB­³z±Ù¸h®Ô~¤O¢• Ä—ÙÀÜÈs.ðÁ2ø™Év´l¥–?Á‹|‘Ùé†UË–‹++øíF¶´?¹ƒëÛÁËÙʪú”-oŸu²³ßS= „Z¬ˆ˜QJÎu§ìZ··VÜû$â5Cï¶åòóó¹Òpä$;rË28v5òÓ- ÎoÿºÄ<(:ö@þÛÛtJÎǹFÐ:•AàõÏ–æ ñçxîgp†×%ïf”•à)®Y½KVF ¯Y½BS!©¡@¯¨‡5’ +$ephS!©¡É‰tz>B§ûÔyljG¼¬;¥yàˆÈTBzóyÓéÍXKj!½yÀœ—ÄBzó “ÑÙ*ƒË 9‘rpÄÿìu vÀY¶|rÓ¡xz–-î\¹ù;·‡"œ­âòQ¶ø÷m)[ùØ¿BYj£^˜f.x‰ƒE7lñœŠ:84«$€7ë]¸~9@÷ß•ARŠVð’RÜ<’<± PZ–i¹7óÎJKG…ç•üJöšl;JËzµLühåúwa½ö÷ì§JMŽ¿å¹¹R9ÖÜàăﵳØ}|p XÌž'Z©»vŽ@@®ËD*ÎŒÄì^©åàsÀ$Ïza¾ö\yc›äÃ÷Û¢™rû¸`¹Ö‹c +µŸ5¤ØMy‹m¥ÖØXø”`œ]‰ +OFb·²ð£ÄT¹)¶^þ,sOûR;X<âæ&Ú½Ò”¹æîÞOîBoÔ—¯°WœÖfèŽô§Ðw‡Ø ߑζ«Zgõ}pû9h+å»Ì­l3—¹û\õ]æSÙÜÚtc0ç6x­Æ ·ÎÚ2¹}v’-œcœNŒõÚߥã&1Ñ–Ÿ¾;ù|wÞ«ýÝý(yI „å|†¾s?Ç^R&¸¡cE½¤\T÷´Ä~v/)7t^ -}L’]RFÝÔÊ_.nXx˜C‚ –0ÿ ^­¯T7µÁ{Z7²uIÇ«¸Dp:™ˆdK`ñ‡¤¤;×ÕC\V/Ïíý$¸§uÒ¹7ö´èy„+Z®I“-rchÒp.ÙhÚPxÒ,k”eâ‚Á‘"K½Œã¹Zª[óð©,Û?³£ð4ºÒ¥ÀÅ’R=ïJÜ2îv¼ÔË7Ùb xäòW]â.#!)1½S&ô'Ûµ’Ûf$“›½ì©ß $¸ûE²õO.q’8|~›‘ÇÀ@)ŒÆc[ZÕ@Èc‘CܬH£òXþ¥0IE%b«TŽev²?ݘÕQvüùÅo౤C´·&ã+'Å ¾Ä²?‡›*Ÿ”8’»``Æžø¨9`‚õ×Üh»Vm4)‰~‰ZF§ÒJw6Bs0¶d/n¨³ÑY>PbæÐ[ªÉŒFîh?Õãfˆ‡›Üh:àá¹TrÓ£f1‡/YÌT98g?8‹Çi³2GäSŸ¸4Tí%T.µO‰s]`‚ÑÍ›¹ÇãÛí~8³×ŠrÛy*Ÿ›¾hx3¸‰“JÝGuùx’zÓƒèÒTž¼œ‚ÜÐ*÷~ÀñºI<æJ?^÷»$—'Þ‡AÅô캽ìDæ?êÏ<8–Ý[Þͪ/¸2ûã\W"¬¡rlLõ Âjz…0¦za5ˆ÷zÖ1Ôƒ”DÕ ™ÜëA„Õ ™ÜëA„Õ ¸–±Õƒ”DÕ °–ñÕƒ«A0¢8¶za5œJ·ÝUÝË#‡É×f¨ÌrÒö‡ó‡»ÝYJÅüâá¬S·Öú}#r +Ã’#§N›÷ƒXï$“¢@Cƒà³’< /í¢¦4*—%R¹æ†+¤*•¯aíë™sÚ›Eçårz«x•†û}]„áñv~pEOR5Pʃb*æw$sdV&/­”i` +Ï¥õ¹®¤W¡UVØ«äDó‚w-ÀÎuåâìàžä)ª&@ŽU¨£òîk®FdbqÂŽ]¢UÈ5‹7¶w×2¨ˆìÿïRf>“ÃÛÊÛæÇ{S™Éåà›ÃîÏïè·kÝç¿[÷ÿÓíeä úO‚øÓ´'dÅšPt>èøíÖC¦@`'äâÄÖG&w[[íý4þ>þüýü¸ïýÏÄ~u¶½u¼Þ˜˜› °·;?Q€ÙH· Šx=z 3¼ÍH«ðÿÙòUüÿ >5àÿÝŒT5uI‘lK²TÍ6${Bª¦mX0Å’5ÍÒÈ7’¢È¶¥©–¡iø-Ɇ&)¶n[ŠjÐÕH…ƒEi¢ ÿ¯-JE²¾³×Œ41x|ö?ða~ù_ýׄ,MlO\^KO8¹øQ1deâ=£KR†Õ[“eÓÐè÷[œïUI®Êº©™†¤¶¢$ú–Žñ‡ãþÍþâ¯H +ÉÙ üKÀ­h“ŸkøÓ]ˆ,+UM×t˶MÝ6쉊fËUÕ45YÒlI“m˜»,YUUÓ [’LÛ´5üRÕ5Ë´U[· \†fÙUS6-Ä¥jè²eÝUC2Īʪa: æÄc& ¤ÃȦêŽcÀzm£*ɲlKª ®Ú 4]®*¶¡¹è¶\5àIÑ`âºH©ÂÖ°kÓõ*þH­Ú¶®Y¶Ü@ +ÀV\ $£bh"Y²_ˆTÕ%ÝöhÛazؘ²Á)ZU3%2LËG1D“,`fC×È”íª¥êÞº8 &žEe·# d©U ³óŠZU M±lS’T2åsÄ3Ù#0± ç—Ç$@Ùòhü¨Á®Y$›ªa¨$±™ ‘%³j> µ­Zt` 6Ü£µŒDSʶ5‹mØ{ÕÇ×Áq8Áª +üÇçbÚÀ ªÉP:€G^S|t€À—(¬Ü1` `#, ¶H¥Ff· †¶dÖ%¯P5+ÁFºb ‘…¦l#ت‹Œ,Œ‡N5tÌÇPªŠn¤QEk&oÝ‘8 'ऑ@¡¤zú¢Í0tŸ Á7`_1ÇPa‡ ¬ø§ÐL½ +çBãG’ùŠ1LÀÐ8HQØJؘ’IöOfxb€T4Ã@A^ÞJÂðo½.‡Sd±gKµq}¦b¨&h‹ dVeË”X1‚MC‰e}P0_ ¬SÔ’TÒÀ ±Vƒ³`ÐM¹ p@Õ@y€7§U—ɨ}‹Qc±:4ž>>eö?ü¡[¦®‚UÆãðw®’@횘FÄh )`ŸK8 &À(†s(ÁÜa$˜šèBƒÃŸë*=ÜÀ:Ë›¶ ÆXv bÁ*tĈg…Ù;D›“ÓH¥*”±­y#á”C‘qÊ ç*‘àLX ˆrÄ¿©kªnšÇŒ"ƒ½ +ž„˜@ªpÇ‘Qk¡*RᣩqÌ,ÐËTMªIt–ÅŒüŽ³3žH°ñdKñ­ +Ä¥«Aêœ8UW=¾F*£á»¥™7ÔR}»f³Ž»”2’b«Œí†€£`ú ¶ ¨KØK÷õ&î%xwY„, +*ÀçW„dRøÅ@Ê€>ãy ¶Êr„­ÌÑå +˜—h‰Y0eP + +uN@žk>L(¨A%™Š gZáØ}[d/m𘰩Eõ=¸Û ƒÂ„á´Âiiš< ˆjŒ'‚gË>òŽfhp¹ 1`;[*0O2³ !ØK#Š{ÀU¸W¾S˜ +$þ Ã~ƒ^…uƒ•Gœ|À±ºÆàIÇ°b 1ÕÌdž½"³±‰fÆ( +°°¨LÙÎ0ˆ]Õm˳ºUÈ2A=Ú*3eP °½°™ ó•+þbŨ'wòÀ™–bš{Oe%˜ž¨™ –§RÂ2kdÈÔ4ÃC +ö+1î 0)m‹;/°Í5`bëS`å(Äì†3ûŠ o(B| +*MqÓÏ‚á@È ™é"¢ œ +ÃTY£\Wµ(b·FR˜ÀÁ•F“« X@ÑP&€¯h¥MBA •®bFq°ÁWžxŒmÈ–F–Ud ”R:ùlI€`CP…¹ÂE*3ñHî `+K<-‰|H¬Ãs’xIEŠÞ&h<`B ·¥qDZ@‰£ÛŠ$ÍÛP Žáð}Ly ÖAØyK¯'9_ž·B$<.ºrŠç*Àa?@–$@ .®!ñŽ‘¢£ÄóÌ°'Tê™ãLr@¡$˜œØôÞ8áÙ„t]xU‰{ÛªŠ¤ñâc° UIò‡ô‚“ ƒp +'4™…ãwjã-? qS‰‰h ­ƒ¯ÁSàq!÷ˆZ8 +|ãúí&„³~†Z\?VžMè|„WÅ9gpÊM Ø_Ç8£ÆÇ€¤ÖÁy°y³á€„W +Q'<›ã÷ÊQÇ$^ö”1& +ʼn³SÊ÷n3x Ätp6Øfà~`HF×MFáDƒýUØܶ$ -+Ì*ày&¢-ªÙ +½ +«­½(kZà&@…­ÔÀ6U©ÂŸáŠÀÖ.d=Pã’pT58® , ¬ÌÂ*†p¨žù  æ€.H[ÐÏ&E¥!†W1Ær™9Z2Þ1(ž¡ÎùÙŠÆ€„P¡Õ­j³ œ)ƒ³!›®‹jOp ƒ +Nƒ ‡ÅRuŒJ•7eHpáñ{…¬¥šàW+`˃S ùX)§H»¥àhG +v¯ŠN, +(N„É„¸À›)§h +Ä@;Uë×$ú=ˆ +›î#ÞC¨’M€àÃdoR@@ˆv¬-¹žŠÁE%WmÍb}NΔe ˳ªÐ ˆÎ‚„ ÈAìrD¡S™@X’Ê[wüV‘µÀ’ЀQ;ß;–&)ìŽZ`‘Ø®_I,;°-Ó³ìT¼Å¥ƒ_¬‚üV¹ @AÃHÄ#áø *ºv0¡`¬q"ãTôD‹¡aÖ +Ï£P ž!ëžr¦¬ÊhÒÙÌ8¡…SUˆ€•âCZݶe‚‘TÞŒ „ä@ ­;~«¨V_ýÊhv°\KÁ=jŽhUpÕ=¶Õ]¦ N°  Íñ½-‰‰ Á\ª8¬YQÑo4LÞÚèúMÕ=×Á2“Mv˜¹ªl3ò0„‰¸uLð1…& |”PR‡–M@T/zÄ!AeÌ¢4¼ . ðCˆœ±"SKq×í…òìî*7½¡°Zœ¨þôþ~„e(&F -¸9Ô2Ð Ì #`¨H'ò@jD"êÈ«1W\Àèkƒ¼µP²ƒ>C ð¤%&Ü ªÝI ¯œH/ ‚w†– & ˜IäÒ)ˆ +˜5h5 9 +å¸Ç{:sµÃC%9a>T¡)cp• 1qN@4ƒA‚‹ˆ”ɽ•>;RY•LL'S–XT6uë ˆªs»WD€'‰|‚{¼MDuc¡Ï^³ƒ~­_ §JÕ¾ï"ŒøÄ»&okz ŒÔ´*q±aÍ ~*÷>ƒUª†7Ã@8˜-p-QûÆˉÃѪèÔE¢RÀ®”Yãž3eŒŽ²®gá! *‹]f€às–``p§Ì .<~¯È–‚Mhâ•ž +G›œI„ƒI¯¡ö»…£‚eбð’ã î–‚1 ªØ@c~è\¼`GcØiI Œ*\<¦ä€ìë[×%Ç4m˘WC®¢‘È5&â*zö;¢±OP‘¸I•UéÔ~‡)ËtÊDEÀá‚xžgá„ pÈAƹ 6,Æ´@ĦÌeD# +I± gü€{5°ß5}P5MÖ›U2¼­ ö»ª€”•(Í]cKøà& 1Œ²ƒ‚·i`äÁ3U*\¼QÕý—pÀ›6æ`›‚ªc¢oÊ­sa΂Äî•kîÁYªÂ0¬ +dC“‚=ª`ÌZ:âÁ@³Àñ‘ ›ñ+À$–t( kîcFFYx#¡³äÌACWÄÖqi%G 0h,•1« ˆ,QbžJÏæá Ò1gPµXöñOW'yT à Ö©VÂQ4lHf^…al0¼Ý0mÞt)ˆD@L’M@ͤµ%Ø£ÿ+C/a*'øEU° `˜óHm|• ªhS€Ñ@ t%'ôçÁóÀŒÁëlŒÙô‚9 a~ñØ–hø!€ ¶A"zL ³¨¨±¢(:#)9 Jo8¨`35‰½~qf¬Ñ“в„\ax8¼n +ÂU~C!ôÓ >* Å`f±fÀoÊÄ4ˆ&é¼…Ço•˜¹ÖÆíEÀ±Y¦ &¯Ž…sÔYÆQÐÒ ’l™(À„eN§"Ó¬áa³u.ؽ`ÅhÖ%É¢ATHc´ŸÁFy*i«.™y Þy«‘¨Ðâ×|LÊ™2z2c¿rN@XT„€% 1ux¨À×? ø^U¸SFÅ  š»pVÆï±À =CVÈM‘†V¤êå'P—ÍÆ °ÙÑÔC ´FeÏ2GÿÔfÉ›&¡!…`N šQ ~Av‚£D2(&Ÿ’Q%šKêl9™šD`ú0ÊŠ‚†º¡P§P™:5ÐÀ£×}¨‚3VÑoXo,´ì0Hˆ|Lz:xÎâpñfÌ ­;v§(,“¶l›fc¡ Ɉ8ê°Á²Qy[6KG[Ô`Ø?Š‰e5.Z½¾Tþ *ê°‘+‹À*µˆ,Ib?bPɱ« 0 ú *¤² J€d‘Ü]Æ„£LY£S&v†~rá,A×>³'8$¨dŸ kìrË– ’\š2Q6°Åú˜ öŠ:lhbòLFjaàõxÀa#ù6à·(ñzÑ|Vdÿ…‹†×>`‚¢Æ«WÅT +KÖiT¢bFÐCUlaÚ€J³ÐÑÂÖM†ˆD£ ªc âyô9lTèÙþNxÊðgà2‘…ð ˆÅútFeJ@YvQùÜ}= €t±Å›2qc™Æ`á »Wžuo’ h\—mÒ(¢‰¶$ͦ^›f¡ˆó"81߆÷ˆ¹´–WºÖ +Ä@ë¯tÙIý¢¨Ø{ \ž$aàL]“ˆMœ¡/F@ÀZ' 4mNõ§9STè!*ÓÉõ…çlà“3eü3ƒIãã,œ€01V9¨°0ÃwCS¦ 0¼$–yëŽßªÿ#ÿ8c»W¿Nd÷ÿèÖ—†&i¼P¦’ALšO‘§æ„8‰R†ñÅ×i ¤8 ”§> ‚6‘í\U•ƒÊí4pK`Ô`Ü6!£¾bÜ“0"R¢ùXx qe.Ý/Vh„DX$$ +ƒ¨x2—,Lgìë0„еaD„ʶêCš0Ù-6‰&¼ìО‡ÉÇC’¹! ÉÜð²c7jpó"éRèÜXž—G ù@EÙ,™½S²@"h˜«SÑu.ˆ ®øÙX$¡*&Íœ‚SãXŠi)îÕ¾à€ yT% +©þð焧L¶‹Í/<´é„€2òà)¹Ò €ÊÀ‚P%®ßF% 8Ú?Ü…Çïu·ÑÅuÙàNå¨Qð:A5 Ð ·,¤$ÁÇļ2ô]eE—¹ N’10•d’­àèQLwGo>fsðô $¨G9¨ÂÚ3å "å,<¨H Ñ™D|LºRU°–G¹¡Ð¢Ÿ ¥ ¡e¡uÇoîh(ÇÒ3¹½ß¶Öè«-šOä•J&—Û»îõîÿ¾u{™çþýÿëNÜ||þÜÿt¿àÉÄs¯Ûÿùìu'ú/Ÿÿ…ßÀŸ Às¹æn+óÿs$ì¾ endstream endobj 22 0 obj <>stream +%AI12_CompressedDataxœì½í’$¹u%øþ±?dFÎ.SŽo€»³f™®Q¤f%“µ•ºKdÍtWµUWS«}ú=ç\À?""«²Y½S=œ +tfWàp¸¸Ÿç^üÕÿò›ßýâþ«7ÿòòán>LõW§·/_¼{óö—}{øÕ×_ÿÝ»·üêg¿ýùÁ•»îU¿è ÿóË·ß½zóú—ªRå#¯þÙñíË×ï§7ßùÇo^¼þùág?GÕï_½ûú%*_üæíËïÞ½ùâ×oþðæ‹ó‹·ÿíøõ÷/s÷ÝŸþðó1 +t{~ñíÝü×>üµ ‡úËP¿ù[6xñúO/¾ûîÕÿËêjÀwÇ7ß¿þêÕë?ßü?¿<̇_dçiv‡_„Ù¡ú?½úíËïn·™ïªÉ·è\ÉQܹTbÉsÊͳóó›/¿ÿô›·o¾|ùÝw§7_¿yûÝ/§ñúð·/þ€š‡zùõ×oþípüúÅ—ÿmÂKJ_<¾úú%ÞÇ7/Þ\àÛ¹ÿ•ó_¿õõW÷ý7ÿòoʧ̯Ãêò¾C_è–ÿæ×å‹_}ƒo~÷òÝ;Œ7äþíß·Ãàƒ°üì·ÿq>ü ~ŽÿqæKü_˜„‡Âºè½Ëßà¯èZkÉÕZðNç;‡/¥ÕêæyNžý¸„ŽBEOÅ[WNU>±Ž¿ü=n¤7Wp;Ÿ]i ßÔìKLnÎÞÏ>&»:e^ìËþâÓÜêkŒ5eŽ)Ygsõ­ÆæíâZq±‹qw±=ú?ÿöå^i¡bÕü—Ÿ÷7úöÍ·ß`m}÷ƒ¦šWþþå7ß~µ§eZ¾KlÉÿmÿèm1Çj÷ çË]ªKÿõð‹ã]*µ„:û³?d\Ç»ÂÇE¹îšX§ër{ù§W/ÿí—‡¿{óú¥­©û·ï~gë>Æy¶ßVóÛï¿~ùö^¿zgëäþWÍÕß¾ùêå×h¿\ÿøõ ­%·þ¶¿ñö/ßa³¼ùúûwÚÛuÜ‹ö×/þý%W¾³üý·/_ÿþÍÖ%Å—ãñ蘲ì#¶n üÉ[Ä|À7uëoë›=±Ÿqƒ‚ùý Öþß¿}õ‡W¯Ù0…eö¶)þæí«¯Ö=Qü¡Ú/=ņËsÍÏÍ·ç~Óô)Ù¥Šíòœoìàm¾{÷òu%Øߧ¿Ýì×ùîo‡'yxýÕéÍ7œÒïHª°Z_cýæV·ü[5¸üûoí­èï/0û¿yûê5ûœþN5õ‹ß|ý=ªþæí›ï¿ýÕë}3ýÌ(ö~ù%È2ÈW‡¿ÿ—ÿŠ?@{EC¿ûâKt€¿—6w/^}ûó÷v÷›¯_¼~ñö ïq¥}ùëWBÍ è`—¡fýîÞ_þ+ˆâz­}ûðúO/¿~óíËõû囯¿:üß/Þ~ûá®1o7=èÏñÿg<-VÞ·x9z5¹xò÷4ØT=ãF/ÞýŒâå믾[ú¶?÷oÔ¾ûp¿û’kðíáøöûïþxøý›7_/Ýî«–Þû×ú–í÷ø.xý÷¯í]ß©7¸¼ˆÚOî.hýôPùSîýôâë¯_ýáí‹oÿøêË[7¸Q¿ÜÉê~ÈÂú÷oþåÍׯ¾ûf]O›o~óâí»W_~ýòwÿþÝ»—ß<{r_½‘{b¿·ÍïþíÅ»/ÿøëWÿòöÅÛW/ß»û8ÿúêõWXû¿ûþÕ»—ë zóÍ·”5¿ûã‹o_ê1ÞýñQ-·t˜¾éß÷_üâ=TßåÃñõ¦þoÞ¾øê +êxýúÅ7 øè_Üϧ[_’ Ž_Mÿ<ýÓ© d”„OáäUÊ|šÇ”3ÊéÈϽJC©(%£¤cD (~:ú£;ÎÇùþåáþ|B±ëøi(¥¨d”„QŠGq(óýÜÛC;·óÔN(¼¥®ç­(¥e•ÔÒôâApåŒ+NhÂÊÔBsõ±>ÔnWk®±úêÊC9cÔ­”’J(®ÌùODO%CÜÌàc:§ã„qA M1…äâc|À#6Ȧ9¦¢‹sx'xçg÷€—yïÚäŠCOÎ;7?Îx»ð\gˆ€3z›ÑR2S·Ÿñ/¿üöËïÐì7;úÍ΢~'ýÎþ‡ÇD)(U¥¡Üëgü>.¿1ë('ýŒßçþ/ý5mªÏËï³~?à72ãQâqWNï)ç÷”‡Q¦õŸ½<>]Òüžâ¬Lã»â·Es’3–GÅ"¹ÇR9å3Í#Þ¦+‹(b)e,¨ŠeuÅuÂ{(xÏK.`á%,¿‚EØ°Xç ëò“¹ë4bµBó¾Ç2?aÕ?´GL‡ÃšØ »¤`Ï4ìŸ#vÓûê“ä°×^l:æ ;±b“Ücž°[Ž˜‡ðvöw±]Âëx¥]41I›¸ô.š6ë±wt`g³ˆ; š2vš~TnQîóÓñÄÏåå‘‹âìPü9 p6Ó¯ïŒWt®gîo< îÂKøy˜ÎçǬ£ì›ÿ€]¦™Nxçx¯õÅñ-É×=n\ûÃd<ò¥Ù6²­Ò÷TÿèX°ïãÑfS‡ÙÍ\±wÙ´Uj¡¡y¿ÞKûC<žîí-VÌHÀž~\o¥}¸»Ù¸]ˆ4ºžÝÐçJƒe]ÙtbB×Çúg %ªDMÝ"ÈK¤ÏçÒ>sµ$µ*&VeˆZ{‘Ö’“ÖÇÀEϲ|‰oZ>ûNJÚ!C)>´zô3 ÊÊŸvÇï&]WÙÌaƒ`Öp]¡MÙ;üj9:’ÃÛ¬ «Ðº³04{©×žçÏÅô®÷Ìþ# ëƒÿw¾¯=+øUÎx­)àÔ3¡:Ïè-¥ +ΓŠîYç„Ø^Õ$ù®Î1”*›‰/5‚Ÿb›ÙKܽz÷Þñ¸;²ÆÍ{øéŒI墨=‚7ŒW¬ëR©óP1Ä’uw(æ ³­žÆ‡4wvµ^ä·÷r¼*lè¿É›˜óu@óúz~ÃÁ›9žArȃÏç›2â¼]ëm÷ïQd+ü«àIËRöÌ5ògZþØ–°)œ.þÅ>û¿ÜÔÿ?¯bH/úëaý[BÊZNK1Heê¢Q[¾j»Rw¥lJÞ”Í_~íe¸üX KYØÉàaëC­¢áÔÿ¼ø#ôš"Žó±‚ÏRV‡èA¹RŠdav¯éã‡}4_R›©8›êœÅµ¤qÐt$ú$¾ö ÌÒ¤M—¦6M}š¦¡Òµê6Ért”%élêµlS±½LOQ†¨ÜUí*cÓ}W¸©£Iéê3­\CN‹þ\ýÙ´çÓО»2îdZ ]%—þ,åêó$ýù¸èÏÒž¥?›öìí9-Ús]´ç¡?s¥Ø2wS_ÓaYµyY˜mYz§em=.ëÇ/ Ä–C±™Ÿú4Û”žûìÙ|‰Á>‚»91ÞÒœA¢+¦ã^\úŒôZê@ÌuX\þ^v!Úxe–ñ`’d… +q÷{Âc?`,³ä 褦@?ß"3}Ø$se‘Éï³ÉLÏ1Ê|Ø&#ˉ¬&“&Ý&Ü&Û&Ú&ùاWSÛgÕfÔfÓfÒfÑf0N}òªæÍæÌæk±jt©§Ë=éç¡ÿ>ëç,ÖiâYè"iî%bÐl"Fˆa–ߣì½Y"íº³ŒS0Š & *pÞP… ±ÐªãêãÆ-6qyvúe\9ß±¯eëU¾(Æ*üÎV•6öª±çl×­û®[®lï­6ž÷-Ò mSÕ^Ú¦ÜoÊqSN›r¶²i:QªW’;µ¦™<êÇÊ©Ïô¹¯‚DZ2´Nl`Ú)cŸp—ph‡,û#akì7Æv[ôMa3ß.Jí³¼e%§ÝºØp |ú7Ãt~»ÜšïÐMæ±Ì×’Õ6Igî%$Ì]ÐŒŸ$’µZÌ‹V W ××Qgg/7Ÿ»äÜö²³Ô¬!=ßK~.]‚¹¯Œû«ÒžQêeÙY7 wðø×M“àû[ÉF3·æ¹ÖjKÞÚ. E‚E;h8*²þ%,š+'ú2‡ «Hv!:Ÿ+¦+ÒjV/,ègg3Œi±µ msmÉÆTý·×j¬ù‰Kï +û2Íz-ô-ï9‚À8¡Vèµâ˜t'J@X¥ÅÉòª ­Êe0ZÜç°G3aÝ´ mM²í{¬~?©QÙš‚,KMاxý‚†|bö°¹#®çý[IÁÍøcƲYÚ<ôË9£çgh7Q|²4Æ2ËFÉí¦ÎÝ4üýDÆóq–¿xÓòŽþË_×¢œè¥QÉ,»ÆðÒñGºH+›“Ñ.ŠÉ” {1vB6âLæ²»à‰;»;oÄÈUŒ²êP˜âä(%RN²šTirå,)[žºlùÐÝæÛ I–ˆ˜Üö¥ šÍ„ÍIò¦Iœ’€gÉ&y’­“±“µgÙ¾ª|“5ì(6«?K)„˜2‰çÏ2¡Éê:$D»yQ?Å>RuJëÅÜäÇ^NV¦r^ÊVé|Ü—:_– ïÔ2·[>9~·¥{?(‹'ÉãQ2¹yB|÷†tˆ¤t+ÀnéîiSŽåþ²˜¢ó¸(µ§>¯÷]᩺Mî3eñšg'÷£f{Ì÷I–›ûIÓÞôØE“Ÿe湡³h)\ë-[ÍeÚ©/«¤MšÀf}Ñ<˜ö+“•Ö…нÀ XF“L[eA˜Æ,-˜ùp6x³Ž9븳ǎ=ø3”éØos\?÷KiK©KÙÎKÞZíÒdÿS‰7JxªŒ}¼0V ;ç°}Þ/³Ÿw¦œ­U{ZåþsºQŽO”ûm™ö.+üƒek-Ù˜ ÍHøÃM„^.iû·I¶ÓM#áÖLx¿3^ + d%ñ´ÁWmÑUic4l‹…ªÛ¦ºÓÃ/ ªQx¢Ñl G¹ÑèL{Û`†œãäQò­n¥ñ­<>$rA».O’ÉéÌ£<~/ïy@÷yÚ,Âû\±ƒ£Þ£øG˜ðÝ€¸B®Üú´H¯+,ò|ñ{ÅBÙö<]! –ßçîq\çÞõy¹œÓ2ºþE—%ŽKÃûî€Æ*TíÿU†“lî)!ò£>ÿƒt¨gßáâsÌpX³œÙçŽp /» oöÔÝÙrhŸäÐnÚYh†­K›(†{ˆ‡K;hÃÑ©ý0|ÚÓ3œÚuãÖ&.fî@—S_U‚w‡ ìTû ?Ÿn1ô÷¹Æh~áK/l|ïm$'Fš±{ýœª›!7™ö‹xŒý²ðõq®Ë!c×aÊã%xº¬î|K'JÝy Âê"ðíŽõ ¢ÿÓAï 9„6ØnJDjð%Ï-˜£M~¤’j Î%ÌëlA„“C\-XqÙB ¶8{¬à§äî®Až:äx—yÇåM|ò‘Øûà såNªXå|³)2QBNÞµZšn0‹,ADôö ^: ‚(H,nÛ æ~ë{Kº*íÂ.†…›l^ÎOjXçæÊ7Ý\™÷°qs9À“=iú´š 9ƒÝ/lÈËÀ_e“7Dœ;ïçßSgº÷ƒ¶Fšèi›]8,¹+ÝG§Ž¡hņt\Èta‘hÝ"1ì—Öˆãæ„m³ ²jƒ4ÑÑcÁ¶¶ˆºØ"Žkô—-vmØ$¶V ¿·#¿Xâ½v‰§YÑ4Ç-šæµ¹"5/‘™{(æ5³Û¼¦+üå颜/ÊõgB™ž@aî˜(;ˆætÒüóKÒ§ÇÛ±‘vù íp•WOwî²ÞQâœIèAÈUB? ¿¶Cê=sf?N’*C÷c¯þëó¹…BÞBÊ$¶  'ZÄ:Ý0Ù|Ð×=ts˜ïþå,!¨I "1§°ïèÚT3aIµ[Ä Tû°€i©®'¼²â­ù9Š²œ7:Ö@Ï&³uN‹¢uß-ž'í´³ô?SD‡Ý”US]‡½þ¾[A­Hî¨+ÃO–²ÆçMÙ:hÚ¶luô5`uû¹¢ +ŽUHà¦n¸Uü3Ë.Úyz_(ô3J^ŠùqÒ4þñc•¿Ð¯õÛÕ‰~[¯]ÕÆpá_€Ó/1þ]úï"V]³Þø^ó²:ÓÓ…=]ü«,õÛ”e=7~‡Iÿô›Ç xà°èÆëÿã“ÿß}3-ïä:¤-\(Ýn¹Õ'lÿ!Ü:»»h²-îêVãûÍp¦‹‹öc½Uâ{Jäç6O7ÛÞÇ{“Ü>/eú³ÆôžòÚ኿ èœ`sÆæ9ÈxŒÌk,•ç —žå{¬ ³±GâÑ |ßãðhÙeTó3ô<Dh iKüÝ\ÌÝø±š>d÷èVé™f½ác5}¬ÎŒný˜ºÄL kÜfíž|e.2cÑ“æ¢ig/ÚFr.V#½÷.·>,Æ£sçï&,HƘäqT¸g/I'ŸäˆýLr¢Œ²~ü¦t£Ûd!¢*µÇýÃU¹æÝ[àýiÚüñ$f«G¡>§ì€aWäûþqå/²ÃÖÔ‰Àýc…\;x€¶LOT<Ýv£Lþ¥ÿu¸ ˜{q*›\]-ïBSf ½%ô‰ÄB´NŽŒÿšïf†ñ´CŽ11qÖا‰’ò®’¹lŽµ½ÇæÓáøÝdÙ£óÁ…C\¼û/G«»R.©.¿þ8wO½éî©Ÿ£š>G5}ŽjúÕô9ªésTÓ稦ÏQMŸ£š>G5}ŽjúÕô9ªésTÓó?ÿƒtø9ªésTÓ稦‹¨¦m4Ó6¢I$»Ç1•ž0o´„w2õ䕧‘õp—ìÏøLwùÕçÌÞ_)T‹sb ˜âÚS\Ü+[ æ®m2Ð{†Ý ŠŠšÌ‰š4g*mù9ê§ç@­üÐéݶŒ#MÌuz¢÷‘î}”Ôs¬¥lJí¿ùÿ:é#°¥öŸûž(þ¾›êWó«™ïOý_§žJþ´ ²C>ħ÷ vëgZÎYó¬Î+§MY2XŠíA9ëÁ:yZŽäYßèÈø¼†«¬1iæRØpÿååµnn=)—·ñ­²¸14Q˹3­C–s?qÆõ³fNt8SEÓlÅ¥ŽË¡!!$kâ‘!&,”ÎfrÒa1dD¡ó@nOöƒ½Õ!'7ž—Ï7«Î& +:Ã嬳?†1^ù»¬ºëº–õ“—2€‰26ɾkên€'»Æ.¿ù¢lp•Ç]€-¢2/PJ +]œÛÆ1\•=ØÙíÊö¥ÅQ0m¼ÇB{ŽÛcçø˜nø=®]«KÃ_Ö†Á@)¹íP‰º;Xâ¸,ñ°,á»e|=V¢.öña!hÓýzœDÜ%±#1ìäûCn`¶òéöIý|ˆgpq® X +„2`]'ì¨=e°"W¢ñ´J?g°Õ~¤ß.±SDøž++wñîø»‘¼ÜîÅmúäO9I Ùy°bªÙÉ9ïÇŒ×3“É0ëµ³ÌßÛcÚb?Øpß9zºŠp¯ßH¸»Øú„cø(þïož*ìÝ. ¾{݆ß~ëµ?^¸ìÍjC¼:ì[·/’<Ž­º¿þac9\O"„àÚF¸âS¯ïÇO;êŠ;ÝâLW@é%ht]šÃí¶ŠÇÍÉÅÆ(Ûr‚±£(0 +¨Í´ˆ5KÜË‚§Yùø|YöÒT/’³¦ý¡DlÁu9Ý(ÇË2]õCŠ±›`žWvVPÃ!#uî'”%´úÐEª¸(Ç4á¢H¬ /„þ{?ĉö;‹Ë—³ó"Lè2½gc‚:â¹@ö°œAW$X(•CXä Z,4iœCf¦„kD„ß0# #½N„nÂRŠÒ±s†µrËñ+°5Œ÷˹sã¤ÅÜÏž³b—ÎýðÃ%ïÀpÊ ¿ÞÎÓº—l$4ÚžU¸µêÜ !¼mOúðióG{¹LȶðÇ,Ùø¸Xi-cV]L‡f6L¾î ‡¾‡à·ž"«C,9#þ~DDo€ï®Û,×íÈŒuê†W· ¾·ã]dr@rÁDr¼ï&×m”íqijÙyWi&ÓF2¹ßyµæµ¸T“VÀÝeD‰Vû´—²!@«tùÙk?7Ê´œ¨u«|"ÿØü½Üdv¡lÃâÙ/œ­Ùã‡gÿQVùyÛ¸íÜvÈ­EôŒÌÏOÿ¼&¾zücL8\¸€Ké8uÙÜ0I;LîÜc¥|Oò0й¡›ºB0”© +S‡ëÈîê 8.9ÖÄ+ fMÞ·=ÃëwZ´¿+aW®ƒfÒ¢aNµi-ÏÿÔË2]õCŠhÊ&͆¹[ÞwØØÓÇ-§9MË&:#ÖcÇÆÁcus˜Ýö(»íAvÚ±[•cU:µc9Èn{”Ýö0»Ýqv“T‘õL»ópðuÅd ?Ž· ]¥JKJÄqT¼wW§~æÝ87~œ?NÀ;õsðFY•Ám.Èö8-GÍo‹¿Y®UÔË‚ý:]Æîý°bÆüm¼ÐuÐe0Î¥ü·•ïí@ÂK1p5SmÍW"á ‰°Ë„Ó Ñð†xx[0¼%N?\\Êý­2-ÿ¼â=7Ë?Ó{kÿ š0}A)[‚<ÌÐÒ»Žn:õsÓΩ›Û!~lyß«¼îçîo¹ A÷¨´7ñ”/ ½„Á_ +`áC¡tÁö˜ð6Œè¦?Éz‹ÊYd×€j(`ŸÔŠqeüZÿ”ƒ0_ÇŒ«°©¡êä¬cºJž=º gò¾Ê_€¿è}ÁŽ¡ZÈ×Ö…-qßî¼2Žû"MŸù|¸ƒS9ˆÕ³ñ ¡·PšOo-³ÙCÈiƒ‡üÔš×ie3™²#Í®6s<×b=ç¡<„~ç5át†«1æ‹6¼›÷Wǃýt†ôQ–žnYzð-:^<=ObCÆ¿V\ÄÖMb4F†íA2Á0b‘¤”›ÛùÔÏ/¹qhèös’µ?JT ®Œ²?ºøúÀâܽ;uÒH×ûŽ}Ï᯺xs¤üz¨|îA`ëáò^8EÙ!ó{ˆwÙRp)Vj½švè-%ÌÐxRšƒÎ3Äfr + J±ã:8åò¼Œùœw‡ÎÅÕÕ\òÝ :¿9ùî¿ç]íì¿9„”öËQÄ1Bñ‡ ƒ•“ÜÌŽp/OÇj…´îf lmG0Ò›º%¼¢¨{wê&gm9 Ú<ö§ÄGíᛇüů¶3ýEþ?G÷š1L›8Ø5 +v@ôÿúŒ#¢§‰c×®ÛŸsDôô¾xŸOº5ÜnÓ°åÕ¥[û+ÝÆ‘ç“?¢GGé=Ú_üÜQsN¦%b4/ÎÝÚç¤Y„hŸ›õP_;Öyê;\¼Þb>'¡°sð,r™Zäf{^Dæ|Aû·ïè2SolͷļŒÃÜÆ`ŽøË5ŸÝ¼TÐå´‰¹¬ó>Îrÿ×ý2ã÷ý¿@9¬§û„ãi|*¢qÏHÏÈ´sŒXƽcdbÜúDÆAÜe9Š[i…Fö”YèvîuƒŸMï˼þœ¨ÀíáÝ—1ûˆÀÝ5- U¶Ñ€ï;•yxy&3ˆÑ4`ïWÉUv©U.òª ¯÷Ü_úšOå8-^ëº íþ¨çÅÕÝïâêöQuû˜º%Ïí­xº}4Ý>’n‰¡ÛÐõйi9·˜±rû¹]PÜE>Ó&ämÔ¶‹L»Ü ÔOÕ* à^ ü4÷ÿ8©à¦ß^Éî†\0ð0¦õ´\°“ +ŒÃ¼_*H[‰`fÇÜ9Ê +¡J Æ|¤È6wÇÜÇf£ vÄÆâV|9_ ¶çò¸ä{[.ç.r ŒLqÞg{ÝçrmdSÛ<§åÇøþïÿ±øÌŽËœËôøÌ>Ö~OeÃô{ŒeÏõ(»éé8þóׇxKÙñ–5Þ|m¾ÄšÛئ%Ò|gþgùñè–‡Þ;·y’x0üG¬TZ€†á î™y}k " ³YNÊÅy 27lSôˆrl1 é ÊõÉFðq´Ëߤ]žV‰5ÃÏ|iñY²>ch‡”¯¥¶L06Ú†ÉöÌð].êR‘YG7±qÞÚ~IF¦®éÑy§WÕfë™ñ+»Ëoe/ö›¿ÃZ¿ÓO¶67m«-8²mDDHépMHº\H©póä’:ô¢vL§Ÿº¸‘ÿN]¢Y³å­;RBŠ¢ñl/®ùòRO©iI5¹ñNÛ<]Žûÿ?°×žá©“öIbŸ8x¡—´–i 2D1ã×-_â“jéâ@€\…ïOÛø} {_YÙ>1•±_cÀÆ‚¾t›ïÂ᧑ŸŠzú…5Ê¡îýBí@ÄѺ$ŸV#¥  +ÖHt,&J.¢…äÊ*”®l&þ)9¨Õ;ðô­uêSŽ¢[«Z5Aàh5+Äèd¨Õ{ïMr¶/沪Œ¤^›u—;qX´Ÿù­‹ÀåõþÙßá‹°5V}²1 ‹]ÄJ µAÈM’usžg_ÁWð%§ƒ Œ%‚¯} +ö–1NÁÕ€üSÃìÀ‚JÙÙë>ÑÆ*ˆaž $@ü_ÖOtŠ×&û?x®¬¤%Qy£Å¦4—-¢Ø¯ SàKÛÇÂð9.’ñÑ-´ûÜ’C~*ãù(¾Ó‘>çõûœ×ïs^¿Ïyý>çõûœ×ïs^¿Ïyý>çõûœ×ïs^¿Ïyý>çõûœ×ïùŸÿA:üœ×ïs^¿Ïyý>çõûœ×ïs^¿Ïyý>çõûœ×ïs^¿Ïyý~L]Êw}}R\ßÃçÌ~Ÿ3û}‚Ì~é&*4¹]¼×&a܇ÊÃSeËí&ýo—¹Îo?ë±Íq)éÂe5œUËy¿[ç”駎w9wÌË0™óÖK¸‹ýÙ¼KѦ%sÄ6wÄCÏá¯2G¬Ýûž2bM!Ò´‹`¡0x\dÁGI5#‚%õ–5†e‹ZðBSwÅ^B†¶ylh²`´üìRžS¦Å½¹-Û ôõœÍ‘…ð¾‹á«·ó´.¢iqy>råtGç¼Y6ë2Ù.uaÔ%”Å‚Y6VÃuA<.0¨uA„îÑK¢-vÄ]‘©'Ù¦I–Äã’9d]AJA?΋_ÿžªE©‡]HÓ­°¦±,:ˆÌæóÇølöâÔÿv%nÊzÊrÙ,•Üa¥ç´u€9ŸzúÉûÅÁ½º¸ÍÁÝiÇn®M7ãÇü×±Ël… •jWº_¼Ï§¡±Ì-•¸Å>o¢5úŸvéaö³:’5ÍéÙ”¢4§a“OdMtœö!jW›<÷Y¸Œ>£ÿ3›:?E‡ÎeÕu­­ö”DÉêX?Ý™43ÞóËÓŸ§iºánºíOZóy”q¸P»qúá¦ÄÝ¡Ôm±"v âtã`êyã£ßæó»_"¨.]õ«ïtå­¯]ZXl[ô°ÛšØ¢‹Mï^{"ÊÍt1›ÏÇg—gÍôtÓ©øüÙ½šÛi;¹»™ÝÌí³f¶Þ>püãfs;=‡—˜Å¡a{\ÔÜN}j·á‹×s/çVó¹s¿¿ø'ËFØ>èc¾µ .ç}9wå(»m¼ÎóÓs<æW3<]=>}œüSs;À5}f§Ý¦µœÃfri5yjÇž¶ñ¨Ó…ñä:(õ¦õä"g “BBWk^4™êhˆÐbç%>Åô73¦@z” +†ŠR þB?u¹!©·*wñ*ûjÜh÷ЋŸŠüiŒGºµ£ð–èi ¸ØnTãŒ+ÐE‹JE›‡fHWšChȸÊdS7ðð$£Éu*ZÞ¼„»9„²Å(Â1|œÖ|3žýZóµä ÙO74äéÇ;ç¥r<ÝP††*žÖo«Æ[¨³¿ÈªX-§Ã&¡âÐŽÝðœ7Pgaœ'!,{jÄUtÞêÇ]ºÖw15]Ež:êx¯#߯Ùd_ó€—MB¾S;?G—~–þ¼U›§Ö\ohͧ®5_éÌ[…y«FMïј˲TÚNcKdU˜7êò´Y${¹, å^IW¯±7—ö­i‡¾9÷”¬¦5û®52´æÚs€ ÎÃVmž´XVk$Yƒ¯dE¿‚U–EÓí*¾†}aáÚ*ÓÓö¨†+]züÿ†&ý”=-zôã…mŽÐµèU.B}ïÕèûuÞ'Mú™.”®Gû%Xì:Ëê:×ô<Ð…Ðç:h®“%U.MÚfú¡Ï´ÓL¯ÙT³|c–w#]êÔI‚eåÍ‹ÙÌæ¸Ïôâ2µY‹ëtÍÚ¹Îò§RBÿ¢;ü°†íwÞÌ­Š]‰¤¿­c-[ž˜M8QćçÙ¢xÁómüƒÚ÷¥j6ÝÐÍ>¤wÇ÷áw¦+¹ýY:÷ÓÂû´I\²jf7ó–ìø'U³iÁÈ?­wï´³­æ½äwùe.=¡WÚw÷„~÷ýys¿³¹ÜÖÇ?¨‹_)â}¾§÷hâ‹ž¶ÁjíqZ7f{Úèj·4ñ¾ÖCÁo)lË|O7b".çûR––[ŠÛÆï½×Ýžãû¾šñçhîÔÔ/Õôé½:ú{4ô§ÔóéF¤‚2RáéY_æ|ºÜâ7füÖ|ß°©ÙlOWÛ;ÿ0ËË9©@—é½Zúm»Ú“(‡½>ϳ˜w*EhNšom f°èßTÔGÜUß8.†¼–…“NÕ딚ñÊ·n×9?¡ÑÿdFô‰tz&ŸX§_ÆðQ:}½¥ÒWjôé"Öøöç9rÎE™nˆAO%ùx‚ì}˜±'f-®Fø̦7ËÕè/ù£Ë¶Ã§Àyv‡^yø£•Ïþ€ 7‡5†%‡XÙä{ˆÕ !|‘Ý´X|u$8N{Ô¨øŽeÉSR`kñ lÖŽ"kØöh‘‡ø0]ÅÅÝY"÷ ^dcí]Ì-¶1‹?ê¶ái='dw4ˆaüwô˜çýiãŒÃåÐŽéêTŽ5|tÄxÞ”'Cÿi˘66/U²ÄîKÌiü㢻LßÅOµÃ5;Äø<\•[ïøt³, ú§M®þmâÜ[åC'†)Ä}ºudâ®\f«ù@™6áòS.’Çýˆå9ÆR¦Öüs‡?é·Ù^·9]ng›ý@6—žÊå"ãìUv×ݱ¯©çs –­]kºiÈÜš2·ÆëýaCÇ‹Ó_Ë0e_þ:ŽÝþuóä¯ËS¿î§«£_oøõ¼3¿dY›n»w£Ì·Ë lÓ$ïÖQ±·ËÅѱÓå?°\46Ý<}ì#Ê_p‡cßr×6ìØ„ýê”iõˆUÊ «A¹U¹3ïñ¶¹!ÃÙi#ž´‹6žïòžh<ž´½Ìb<ëdÞÛÉb6­}¨Û²rzV˜Ûj^Àfî.ž6b§«ÐÄ50ñá*0ñ241õ{kt¢›6!Š=ŒI²§%Xñh¢oΡŽ“0Ç™½ï)ŽOXÊ +ØÙÂé·'îîsËî¼S8_•Ûg >èî×2mÿ¸(íÏ)ÓÅ7õøR¦ïâ§ÚáêÙ˜{q*?øK;*Æ”x&uóEɸ¹¸#ôä´aB ku¡äà¡JGÊSIp÷XB¦?½pDè£|Ãò)qünrŠ0͇¾–³œnÔŒöw9¡"»;v!Þe—âîºÛ-ö×ôš|>øù.øVn\Ùb¹¾”‹Ì­—_œS#Üôj„Ý¡Ÿàêÿ½ï {JGq—üÜ)~¹Þ,G¯!¦ª@Iç—‡žòemöÐ3 zb ‘ð'/icw3è‚yä‚ù‘Smôh›k³ñÇA`tÌp}æ!UÏLnqèÛƒ|z¦®q|ï8¸7õãzWÑj Ú¦ôp'KY3W€{fñ×åÂcÛæ±µr¦ó¯® ,ŸÀ!vˆfñÀZ‚hДu:Ä]À4·ÑêùÉìÜؤ…ÎØÕKû)GaÑíÉÍ¥¥Â°j7Ûo@¼Ìú*]Ž—ëlÔOœ²punßÇ{¾Há‹/þîÍëß¼}õúÝ«×øÅ/6´k[1ýÝ·¬ Vó›ïÞ½|ûzCÔú7‚6¾9xWyº¨§çvüƒ«ì„>Q˜O 5Y‹Ùçæ|8€#Y çRh9*IÁ½~ÿã¿áß럮ç-øÇןÿþù_ñå¿çþöðÏÿe>|e—üÿ»Åå=ß ÕGrøõ­VWOôë[w|^«›w|Ÿ¿¾ûîüêËw¯Þ¼~ñöß¿;úëã›7_~vÿ«þº¿xøêÕ»7o¿8¾øò¿aÚ¾øý«¯_~ñÛ—_¾ûùáÃÿ;~úûûöÿ^«1ä9TÈß¡ÌL5Á½4C6p1bhÁÍrùc1…Fî/Éž+/ºê#å~°§ìþWùÈÃUw¿dcàÒ®‡|!Ĺ0™=(@q.—vhù.àŸšVbx¬œ™F©doÍ +õÌŠˆw© +l0]1EV ÁáKõÖîðvSåI+dÂ9—s6ñ°×–Ñ5LA:8ŸîDè5x*ô‚mwbå+øió3Z„p× 5µ Á[×n§få¢ÂÆßp¹¿qZKw‰Üáè†2Ü™S‹¤á¶Ä4ðw<Ç¡Q?)cÍ°_2ZAòVÂ[^¦<ç–-x\¹cnÑšT4¬8«Œ~©ÙZ`¯7‰´Ë}çmJÉÖ¦ò)´ LêËŠÔx°ˆ€]© ¤ ´éXnz'hŠËaî„ï¥`s“úÐrƒVýBýÈà’xpôïð<¸dŸTƒÞöߨ/¦èF ¬t&Ã:ÆRìSsÕ*s÷cI€Ê'Pÿ'/€sáÕyŒ û˜5Ê\x?XdT7}äú >”zÝÂVæÍæ//Þè‡äœã™+ ¤âÍl.·ï¨C÷…škGÀ]°¦ + 3¶ +Ô†eãÀ½”¹V°—°²ÈTòh€]:¹ãFŠÆq@$«XƒÇ^1ÛºÁ<`‚ñ6<Ö68 $N0]ÜàOzV´rI•Qö<4t­ïAîy5TJ¯ Ê…ò*~ŽÃåi.hÁ¤njá¹g.é]—hE.V®µ¨VÚ'h2ÑÔ7êyò™µ`R¦–ÕT(±AöÞ†MÔ© LHß¹´[AVnÑ8˜ç!Øà`XùNËK‡;”r¸3NŽÍ@ µƒK5cp×5í¯[èNÞög(õV?ØVw<„š8Ó™Y\¡˜$e6* vD”¹³`V‚x³²$03 +6)ÛU|2V²I«;<)@±»•¬xfǕã°Wð:Ié#ØMÁ*ŒG‘@HfÞéÌN!MÛ¥·i¨Ôzƒ@ѯ(²°3bÈ NÝxhôlPg¾H¶Ð’C JlÁ™@ ¼2mÞÈQRà ”ôa|{xx’IΤ| o!SrÁ40ÍYdÞ¸¢¦- á*S¤åÌhœúž‚È#&ƒs*ÝÁCèƒàj›•쟕x¬ÄJˆ|˜^Åň +È|ØYžñwŽyó;Ç)AãÌ÷‚JÐUk<‚XiÉö=X,¨-Þ¬úà’ê¤ÌQÌ®˜|W(_ì‚(c¥c›Px¼£íåc2¹ï¯Û/ê<Ø:!ôPö$Òö«fš‰ýL)Þñ´Iè Œ„ð^±Ç¨nàu:– Ô#>ߥ2e,6\Õ¸¢^ øut]d•3…¢ÆÈÉõ¤`‡ŒM\É‹ oCHÈÔðâüRaê:XqJ¸® ]8ÄEÀØWÒÐ×¥èPx¨Ô<%Drt…»âSã°Ii¨•XwqðèÈL+ª„<¨«“¨€ÔãXQZÐ÷Pùùg[Ä}'#‚Ï'¤¤ôª“B®ƒÞÁ*Y!O·ÕS`L€‹Ùô+ãæÎ4‹Ìû;MOO;Pv”fA{¬”kHo¾Þª ÷<ê²k@$ëo‘Ô@@Ð#®¥ 1‘Æ| vq·Ö, œ– Ä·¶½B]܆7F’qHÂS ›$ªC<íÆû¡¹*«$MÏ´#™šEZM®OZi²¬Ý‘èÏæh†d³T|9L·4‹cŽÑ°-•´øБÁ²tÖæJP¯¯+¶ä÷ª2cæ¡á*hØôÉ@ëÐ¥Ch8ƒ®…¹£HN½€Fâ0T*úÍL ²‹¶Êh¤XQ¨ž@ŠÂ┧‘F°ÜL᮹©Ò‚H•ÒµÇŠP’~º?Ài(õFIšq&Ï ì9¾ÿ“TÄÙH)pæÜôk(l<'h{£hMoeHj¢tKR\u˜¢5¦ÖÓ¬#-mÊüÕ¬Žš¿*¨×²|î$É*¢UСŠî†fe ¸©qõp  rT’T!ÿ˜ 8*4u¬pIj2Vc…£¼‚@V`a˜hÑ°~A8âÌ' pÐ\zP#^³*âEDšØE¼æ&ç!÷]"ú0-¨‘R ï“S‘Õ Íôšà=•.,±U-£2èê ôíQ´fàrˆ°A-hyb <1 ÷³9Ñ/;Úè'Ët½ ¤[ABR+G!€& ­tB®¯;§~|¡ŸŒË-ÙHÁ®ty{DP]Ê—»LDJRÔø(Tkù¦ƒ¹·4DàVhZ¬½b&x‰hk!aÈIÈÔy‰^««”pkŒSÛ:ãûV„9Ú 7Ñh ½P‚aÕq7í@÷Ì‚³ÙÜj Ó7k†(s†ZîN¶ªDœ¬3›FïiLµ§dÅ]M«ô\[â¤ø`@l5§¤Mµ· IgÚ—û3Çe²-GCT$q1‘n.¶ šS‰0¤§x&„ï”þŸl ht@²J4 ãÂ6"’`Žëª‘-þ¸@ `ëšY¢ï†s1÷T +Õ.¯µgvZj9aùÞvô…|²\ŠADòôFÑÛY)O\VØ´Æ>­cŸ]µ¢vÐø,3dŠ}T¥ +$²çL)¬;¥ÈX ±ˆr–¢»ÑâËîø— ´dúJÓ~ø‚ˆLÀ›Åý’^«ü«zÕKé_•-ü $+*1†©Kv-ÊL%–Œ–ï¡H/¦…¼˜},¥ë +2FºÑñ^r +âu+O ¡³šÝŠ&×ÀòB$o24nR ÀI R÷¼xTЪj§ •­[¶rrl¡¨ŠÜ1‰‡%ȱ¾^µ` ´  [-@‰;…2ณñ°Dâo­ðî"[ÑÓª~’N¶eh‚ ,¹Có08P(»u.Ž…zZP¦É„’ç.h¤f®·J» +y_tš&ð¾@ÔVˆl~x1ŽÆVº½«ÏÄAª©`]µÈŽ ωÍÏtÕ +º÷(‘ZTkD5é@" št‘xw’\ì\ŠV4‰9Ê —-¾ì¢Œ¯DA¢w´Œ^õƒU›i‹i\/dá¥ÜÑj†!@¡æºÅ TâË ½ ÒSâh¡¼î‡†ÏJ„yà­5NÇÔ%‚Ý6ô¡2†w‰t2ÙëŽöºÝpEË™šÔŠ:¢¹Hì;¥´k±ÀÃ…ú´S ¯5ÏIæ(ŠÎT Š”lA³âr##»Ãî^dÑÉÄO¡~H\ØÂJÄÌLy,"ËÕ€Q§U8•f˜š¹à° ß1^|3v#ºkxª¦”ÔH9“ØÎ@/ÔKšH Q".›V°9Qκnáú[i.rÛ.[ÑÄ ‚„Æ âƒÉè„°Ò(K2ÍPø+šÈÛ=gômf"=/ز‹"2µAôWTÁe/û±ÉùĤï,Ò™@)7@!!ŽCHXBx†NëbGD`“TŽ’Â1“ÙÓQ¤óTSA–IÔa¢–”Ü€ß2@Æo'Ì-÷“Ý HŒ¢G êZ#ñ“¸L« ­. ÷q’ìë3™9 ’ŒËæ7"ag4'byÑŠhqÉó3!Î BBo­$oÜu‹Ö‘mžYÀ†‡þ²¾…8ƒt§,\"Q·ØÁÞ`ÛA ÃFø±ž‰Ÿ ⣵p ^‚Ù) ç’©½R°*Èf¥ƒjl5`ïÅŽç6´Ó<Óëk-L·V¦JÃ?+ zÕÜ­žj¥µQ¼æmƱŸ…U?Œ;_jZdå@˜£álzž‚â²õý^- +Ð1h¦w6<6ÏìD+ú• zcO݈Ưz­‰þMs¼ÙK²ã_nTÒ¶A¸6`{êK˜TÌ!‘oÄdQ¬mh×-ì]E“ùZQPì­~rX°0xo¦JšyŠ™,i„¤$ bDÕØÄÒÎÑ•ñ9uŒ•©š®[ýº‡ØR´ª=4@®(¯TA Ozÿ„놢Ì-MÙKäTPïHK¯[Pþ'µ-Z¬f›]«J¸S¡ñ3Êï}G«¦Ž˜’ÛHP<5»Hn0“&¾ëCÕ"bœf?%½î§öЗ¢3I)ЖCëNäñ¦ùF‹Kþl¢ìe+3á7³l¯&%FTAíŪ˜¤ZFˆ—§!‚sÒM¸ûl7âëA hÜ ¤õÐÔ¡>Ί£ù§î¶ƒ–OŸ3DNOeß »Ðèãjt¤Ê|MsõÌ™+ñßÐÌô£ÒΈ¢«¯ÍÍØÀs*·Æåµ´øˆ ¯$S»G£TsÙ‚ÚHê:ÐU%…:è §•|¬ÉHDSÉñŠñeFL‰¤\Ø®d eaM4i °VémÁ¢VÞr¢°âÌÆý$öe€cþÃ?LKLÖŠtÛ(ÊzNÔWad‰‹ü‰»™hÙ¼îNÿÂl—„Û¢Ö*£xÑ›f„'œ¦(&Í©…PÚ©1—¥m á4éc¡±H-fzZZ$P¡«N¥;Ç)¦q6óSDV íç3‰<ä*òC‘¶šQ±k´¾]tƒ:#„¼'¨ÇÓÊÅ÷´:‹¡9rÕɯ¢V¢Q™"ž:|ëš­ž¡(R9‹Ü%xd +. ò+ý> ØW‹PÖûJGhEL¹µ²šÞl¸± sAÏ­)·ŽØú³éÊcTz!°ÉÓpñC¼ñ-r¯b‰xÅh€„áJUXÐÏ<\ÀŠãI™ÂÞwjŠXHYc½g…-J†‚z/×2:è =S’šA$èî ꈘiµö¤ž™€]ѹ’ó^ªÖÎ ¦ù~ +Šƒõ‘íNQ™f÷( ¹}š†b Ø¨o)ž`U`Ü ´2È!K’Ô\03ì&rûäÅÏ'œj&‘¢P !‚Gf¡GTA]ƒŒÆ gp–Á³Ð$Êð¶Xç!•©• ö¨ê™‰b—ËDä&·ºµ £d°šê¡äêS¯'f—ÝÏÃýGeÌ4-,2J\ 06f¤50ï2,‚¤;‹NtƒlAø +Oü&XA›{,C²ópñFáÐ9êDÝF:x>EbùidvhtŸzS°ñz“¼ò)t\d3‚v¡có¤öþ‚²¹ŠA,"Í°ž2(Är¦À¦å›€ð—uv õôþ*pG$û¨ã“[8Ê»Êý±}¸\;>œvG¢ÑN…Œ‡þv½jBüÁ)õV¢Þ¹þ(¸…¢5&–‘ŒŠ-¡?„‚D¸c*ÙŠ,Ž­2írlþWØ*›û»vÀ‘£0c h3cƒœ8êÖ £˜É[y£gÝÈ §4ÝøÑÑÌJu­ÿIÝ·:»Ó]ˆ!sñ ážh€é.½!)ì‚hQ¶Xœ&Ì<øc´5ɽå$Љ%ÒîIt’@€21þ‰QwT$‰{—drÆËc¬…¢m¨°Ç2¨† åõr§G)ÿ<dˆ<Ü«h÷Ì4œ …AIq'¬uÎ#±¥™Nø¬p<îĈž4ë^v'¦w'OcßÄ¢x‰¬xqëä8m¼S¥[ ´;£EâJJZy™^$èÜJqà WaÌ ('* †S–ÕÜJ^)l@Ògbè£L{¹§ÒIÚ‰EÈ]¦µ§Ï“ƃ—bubþš™% vitë‚ú ×ëg"‘cªhNG,o&¢7𤡳`‚ÜørB³•â¶¨PéYÌÚ-,¡ÐŽ¾‰é³-HÖ"(–¦§âÉj†ÞÚ]ô˜Xêaà¯|3a$‹cÖ>–&I’¨5,€µRÔ@Fv‚Ëß:[ØÅe†™ÍÅ 6ÓŽˆpæZÌ”“øÖégåŠ \çæ³£Óp;ÓÊChm³(ŠƒN3Úk]3gIEóI´Q¤§½hÞÄr `"3ED5óÑBŠÍfF›[Qt4·’v¨IPÚqŠ#ò„†䎺(E—‰ÔÆLNhãºÌ$ÆJ©Š§zaÌ&Zx¡ß?$¾ AðFš†Ÿý‚ù`Âþ燿þÝ»·¯^ÿáð³ãñþË/¿ÿæ·oÞ½`Û]~‹Õ3˜“¼Â,è< ‘Ú„á­c’ˆ\å¤b€Ôë´Þ;ðS±—úç#¦hxdjˆyÐ>×An:ƒÛ««¶Œ\fãâUáDêœ9ÿ +ÎDDM®i×ù[ ·¯R—O†›IƒXQ²x\€s5ÙšË Û Ä¢Y¦šÞ¶N´ô™öR1RnoxAЀYxA ›ƒ Û„ªÙ|ÉЄè^V²¨9tñVádœñ&0>íî’Ñ,´ÑSÊj œß€|±z¾`Õa•h™*!q<”áÇ´ö¨‚ðT@7-¬XñàÔ»‹z!×RcYø +s'~Oç†uB"{—Ð@USž¬IÖ ™|ÖÓ‘.iTJ=Íýô¡1Áð•O¬«Ó¸„i“)6 +ßäÏ ü:)µð?¨\ßôVR™ˆ)DØJQddL›l;o`%eoªmƒ´Ý$ž›ñ'ÖF«Ð\çd>ghê"47âÄZ·¥JVsÁ2³[Në‡Ø+öCm”-<ù35fÆC„$É¡8¥Ü`ÌöbCÊй’Ô្²ž &ŠEï>M0tÂÓµ.çå\*ä±ÌV"@y%ñn„jç*`NðtÃI°TÕ ìù2tpœUbáÁmž¶+èIöŽÆJÉx´!¶%ý¶b´žµ¢‰‹¨ò¬°¾$ø[€‡µPwâ»­N-È7Ø¢ÔÅtÙ°…ŸólG}G­¨„³T[ÉœÉw8`&%F‰¢¨¾U¦!Ø ä®œÉdWl" wBý%„„ç)J‚ÑD$† +þCc6šÀ¬iÃnèÕc7Sˆšõæd£„B#èâ¼ñ¹[ú<Ùtdd3GCL#ÒU .³½RÔ-r@И»ï´¨•@¥OaQ–ÊîI™M$FŽ0šî⺡UE~‡î‘aöš¢~3Ëlš³€(L@ñ²PÆuâ¯ÌÇIþJlÓâ³ Y–°Èx-óÉyKCmÍTp¸ÎãGœC)f!'ÑÀA™j5a3Ñw¥F>¬¸4ø=Ýbt‘²Qž“iPÞË<¢ƒì)J77ÝäÜŒ´>­ßä¢È’so,òXQâŸM‚Œ4ª(ßÓ€{ûFÙZ³`3Œ`°|4ðFù‹-e…@9[Bw:¦Blòñ™ Ý5ëõ¨Ù) dÞºKyþI‚‹=Ÿ‘£Ûž;ùÀ±Ó'qb,[K¡ºÁpßbå-*Y.ZxIF4Õd†ð \§W<åH)bÆðÜF8`f¾k^ôs3ȇ¨Ü#7¢W´¸ÌÆÌXàQðGæÃÔxþ0aèžX&åÔA†·ËEæ˜v„ð0²>®:åzao˜eO¦ínÙÁ³-\Th0`¥)h ý2a%ñ~¬¨„„ð…+Šš0tföbˆæDý‚²“uº=­|ÍD2†‰Qi„WtölÑœ9™X]Õó|X¦ù1•€À6H‚Ñè8÷ÝF-Á‰Ô#)=„w4F2E—S¨"YLÁãÞI½L‰®àƒ@OÛ©EÄvÌ£*#ïiXP¥<)¬°´R‘‘ÏÜÑÈ +êËJyâú—^Ÿ—î a¥`qJoÓA—¾)ÑÙÌD¬h6º™æ- .î±Z¸\šdf:ŠH¬ ÊP×cEcÂ7È{”‘Ä#F†´z±fÒà¢ÿ +é¯[땲 ‚±>¡…œ(3@ý¥Ñ[™h€ÄØDákI–ä » #RŽ•Ê©ÑˆÃR€3Sy]¥à_TÌ”ËYQ9C¬„ˆµL©N஼vW„m¡”½*iÍb…¢€éo6Œ‹ìUaxÐNSÅ’˜eÃhºÀ€Sé+SeÎhôœ3#¶*smL 5éÝu!׶PªÝ´}xóË0†TZÉiѤÒϼŒ°ÎôV¬á7SZD¶/èÑ–ÅTfD&UŇ* { Њ2ë&ƳS#0ª².cBÒ“ ¤H&æÜ‘ðdgLnPyĽìw´,xLù³Qá-‘…œ<8áUéóe`¦=b¹è'ÃîÆzXsjÐ…K(ÔmÚKñ&ïzœœV Ö’àpLZšé­ +k\“ÀTq[#K rû¨¬y 8’Ç—)mêAépª!3ùf¶èJ”+YO'h®gyà¤>t´£÷qe9Ìܬ¼¾gÀ´gF~Ä%nœ.ÀlÐwÅ8ú.Éu +M$ ÁM¨ù;%KŒ–H‡Â1Õe¦Áj]Áe%“ù±’µ¥’Ô$,âÑg“)s–ìšØ¼5ô‡ÌbTqzH%³DR" 'ÈÉ’Eˆîâ`Â4h)Ôƒ1G³>Y/UFÌk*”·E4a°B)&©. õj!À¬ –AáÏÍ‚yGwŠ€üJé0´ù€”íù=@U%—ažÐfænŒ×Wf´Áà³p·œh².&Ð#Ü›6â´I¶fËlHÛÓ9‚Ë+3ˆc¼Žú ÚSw¦ÑÑ2(ú&(m†’@zï,G ©(*’â án<¬v¼¯l¬;ArGƒ?È1$ipÄFýE\$ÑïLéÁ÷d ô~yyíãÀ{¨’xÇ™¾Ê¨4I +žüIšÎìS¬°|BÅw3déåÕ8Ò莎,VB¦ôª¤•ôÀ5Em5U(UƒUø¨ +Nf½c0ò¦7®0ôFÓ$ÝѲe´ §›FpÅîåž’QB2'šp0)ÅT«»Ôam4bgfCmÂB0ÿŸx(“x +#–,‚™0ÉÊL˜‰ˆô ·Ï+•™ˆÁd +™‚Ò”àË™D‡Ä-£¤)$%³ÁͳŒš9õLe–h´D% ¤¢Øª´ŸrÂM ÍŒê«$2Åò +¯eÂ\f9ÖÐQ¸‹P?ŒJÑ@O8“*Í¡ƒ²:š +ĉ¦Zi)4îe)FJ¼Ò*ð}×c˜œÆðcáx©f¦¡ÇŽ!S´¥H á,a#ö¸¬eæ kÔô +’[FI_2“µ%Œ˜'ÄÓSHðˆ´%c*Žxí™ÔÅ‚"“fHÈ`Päsízõ#&·º›ê‹2AÂ,œH•ê¯˜O"ò­(±»¹²hd%Ñ hÍ(B#_°¼¹#å%=7¤h3£- ‰ r&CæÚÓ>Uö$&ZQºáà K“é5VPTíÄŠš-· Ì´ðbJ•lψSêá`Ѳé8bṳ̈H¸¥ÝÉ´B„ @ù9tÌFáT‰×™3ŽnBÊNûƒ4ƒÙÑ€ÊÜ­ŒL +É|SE‘~rü³ÒðZ!Ò¥\hV¤kJöìù—F1U-7&¹Òr›m7>3)GM:\˜ßÇ|–ò4¡ßê|%,€kRšA}þý²…“JxI¶‘=du®xO‰s%£,æjVžy7ÙãáIé$ÈËIWçž©ka¨“Q‹á>-Yô£æ±ÈHTÖ"ö³…½*$… Ü—Ûý[ä¬'Ö/.7 )Lw6×f™{˜¥³¬‰~´H ‡S ¾ªÅ\3‚Ry5¥â³4¨³aÀãrPª—5·TÜ2EMú!ÉdLO$j¦|ÌE'± +‹ŽÞâ# Ïã•ÈP:&ñÚ!„ŒÉé™ç ဖU-ñ,`m›„͇&ceJ_eíÎÖ ¸¤ÏôIQ%ôV –&ôùìÄ5xþ² ì.œ‡l„1ŠvÌ ”6@Ê,”ª£rr6Zë¸1¢ÆD-}Žk#¥o)Œ€ÀÚ’B®ÀíhnÆ*ó#ó¢…ÅwÉ:)O@¥Ã"( &G›i|©‰#1!×Où"u1Á©T±òXŠ*3Øšl…L™dù­‹™»‚¡CðŒÂôS +ãmšØ%Š£PP¨³’¢‘ÛRK™o kd+ +i„ p®P)·+H9Y¡5IY¹2SâÌÜÑ*D˜™B­,½¢™Ð§`÷”dM‡ú¥b˜´Œ±Nyæh8Q¸5p²}DP!û¬1Z~ª&°tšÂ-… íô4F‚fæ³”_„( —ÄìÊXdµ+y+Bˆ$œü±5¦ÎPFR%‰$…xç™V…¹e„d£Á‚8 +â°˜|PíLËHÖ0wƒn¢ é•|2ƒ&G-Lz]„a`­$#ú©ЄÉ’Ò+‰Ï*å„Æ^VÁö]lÚä-lî i(ÉàÅHwêX„-É4IÜ]nÖ´:ºÛ(¤2½bç 1`s˜÷H"9åø6$DÌ‚+$"áüNÚ¡ wxœ¹C¦„¡¦É<+`)/õ 9*ó"/µ¸lbXå˜sã9Šûl¤ãxØ*ʃ¥æ–˜Ó2h9’ßÍlAÕFoŒ®Vj <G- 7&æžç ®Vu¨ŽW+O¿#ïÏ Ô‚Œ“-f¢òЂ'u°¦ºØšas³Ò.Í„›Ab #¬ó€U'ó2f¬PfH¡ …A„Ê9¦”x˜:BôZ2»†²6%¥6£¢ñ°þÊ ‰…KÌEéY Ö”ð±gîK=·ÊÁì ‚”Ò梬®‡Â‘,*8õò|œåN8îÔŒÎU&)&e'¼Pn{Z†ª^"]„k3ª’f|:Åf€Æ*DXB—÷yŠË‘k«T !xƒÐ•²4°ŸÈd6–Ò§ ÈܨP’˜ /3Z~^a³ŒVbŽ¸¸œ”ŒNçVEBØ”ÅR “Æ"Å{Q¹QP9MÈ|’$k]‘Nùü)“É 13›ñ@¼Z¾¦Òr,ÓÁ$X‡P¼Qs*žIÞ ¾´="c OÇlí¬È8*;°ÃðU³¾šBÏ,ꙋÕVTø¸(SOÕ +&—{l¨w–.U(pbôª)Æn•·×-ÝË]H{°"˜ÊDÇ UÔ”4P2ï«Æb„„OÞïÔGPÔa-·æ˜\½“p. eÔFIS,燎 ¢38Åëú¹'­Öž–F´”[òÊYͤü„?d…¦^ªŒ7«Vì0á9ŒÙ¶#ÏUª¿ª€Ø$#º‘½¢0~'L2·‘g‚îB"yÇþ„Àn ¬÷v PÖ!èx¦š¿6ã +3ÇfEr*¦œ7dð3W¾Ï²Ñ­“t[!oâ'cH…_Z}dzQHkÇ—ªsíª›b­HÅØŠ@ñ_Oúá­f—YbV6¾žl;[»:êصFqËò +$„’¶¡§Ü>u&C°¾´éD;:•8š—D <8ëJ1&ÏäQ €eò(%5-樥fFìEN:E¦Äž•+~&L˜Øš ˜§™–TH<3«’Ô”cÚD‡j‡}PáchCàéª42˜nH©"ñWU‰t[Wz˜¬¬x›8D±””« ÕP”‰ïiÑ° ›¥Çª(µQu=\‹§%zå8/#– _Y‰þ)êÊ-BÎÅ€O¥fgBfª¥oš þ©«sŠ‘‘׉‰½!†‰ §ÝœB‚Ò”œQ•¦µ:F·xfÚ‚”~»/ :÷Ôr¡S –‹P)UÙ&Äo³ÃÐz#GÆr¡_±'hïèÞ.ú Í¿ÊÌ®D¹UóS+Ñ}S<8sa*‘µ4Ÿ:"Sž ˜B3ÏJyªhÊ¡§JWEs•< Rù1‘0Š­ü1:Úœ{xÖo+NEç^œ*©¨ß”ø± :{‹ Ãâô?ÌÚd'³ße#…uˆˆ‘\:kA%Xb<‰Wz©ïR>ín®.܉݈ãå™Iù1É3u욶–‡ÆíÊàHAAlz2R/Ô] ŠÅ# ·ÄU²§Ž‹VÉÞhTVdˆT"2jîI­éŒ¦% Y +&˜Y¬å©æàˆ`Õ }p¬ÂEÁÞUY‚•Ž_)!›þ^,ŸL{BW£H•è'Ú¶g4´h(ø‡®tÎB¹¤.É›ÂR¡î˜±2¤òI[/’¹Šò›c.9®uÀå÷ÌÂÐs7^Õ¹ÎDÀ”,0y;,„’-…<å(%lÙ[RØþ½ú"f™HƒÄc3ø²k˜ðT‘âÑ@R”ÑhpàÖ¤~¢3à„߬cP2ñ+Å[|%âfÅLT/ªU§\¥ÅdöİǪŠ”ÆJÙRñ|v+eKL<çRç*DñsU(<»d³Œ¡‚ùÏØÛH¤—˜ˆ5ì'ÆjÓl‘8¥Ûr°u""¥]Ê!WßÏæ#·Þ.*©È  |t&}<ÆC唸êT²™ÞŸmãØB5Úú]–‚ BÂóµé–Áþ•ãôªb»0®* øR¾džÔ™jiXŠ¼Ù´†qe„sUÚ¸¶{Ð W´ÊÎÌŸ(G·Q†¦¼óD8VÆÐÒFT¸¬ì×,²ƒ•$„¬lDt¼°‚nqú…a4+¿ODYÒ²5'ëœWg…0w&/}dÉ Ìï•È¹ÙÅü;ôDƒÜ^y=ÏiO$}´py à¬H¨ t²Á©X d1ÁK…™U,¸oiL©Z:}¢qÓh&“ò#W…-ÎLMflŒ¾©¢Ë»˜úAÖRÈZÑ +ÊVŒ@Q?rBòÜB3>xBL²8ia˜ïeÈï<‚ðÁí‡ÅáÉ+ "·ӱφÑPÆfŽk/37à—’- \ͳv -§¬SÎŒv”ó¤€m¥Cb•Ð•ž›¸EUГÀ +šö™8Öà“®'T$ƒR0ŒeÝ™ Ÿ ,³²}§Ù’*û nŠ»ê‡í(³¦ÒÍeò´¬ŠÖ3 È7γ®xƲ{Ç;©Ö ÑÑl•¡‘YùØù`ü‰uVhö±ÃEÈaÍ‹ÞâzÜœ dÅÇŒÜÐ:>• þ?ÖÎme–$=ÏWà{X‡²Ú±ÌC»1F cã[GÍÐ3¥äñ€ïÞñ<_DÖ꪿5Z#†YQ™õç&âÛ¼›06QšU:Á:W#eè!Ø{Pªæè¶ÂˆD·‹–¬û|붨Kµl’QvAz´N“©ÃgA•A‚{ˆÆ,ÖçpÓýüRó4 +qN¬âÀÄmÛ‡°ßçÛ§ÄLä²”ËZã~û•‡îÇßðù“ký£è5ag]A&[Þy?âà#%˜ÿ h¼•Iù;:ë:2#÷²!uC ÐQo¦ìèŠ ¬™ÍƒR`¡,¯~ÖõðklÂP…NãÃ.í{.Ô€ŒÇ@À)Þlû«ôÂ9Áxz&ÛÚOâÈs”>nlAy>/0/¶-ºîBSÒL4$­!RÛ[“„Ê̈pöŽÎÇêËç,ÂÔ&•–û;ƒn,:ªÒÀ‘eÂV 5· 6½¢q£®ée’ ]uðšQÀ¾{`½Ö,®Ç×vi×»Gu|Íhš™$ù:«"aÌ ß„íŸ¨‚xÉ4rEfs¾;¢8Qp%TUÙº8 XzZÉ»-]¬›'¢3ÏŒuâS;.±jPm/’6ÙŸÈ”'õyƒP Tɨ(Š&P¹¬…¿Kã!¥bx…ž-ÚG£¿¶$Š>°G°ÇS`/º¨Ó!ëX~ù˜ü™»B…±0–ùÎÇ ñÖpʼ£Ë笾;âëyl¡åHˆ»À3¸›²Y?ö¦íæUß•!Iz›ñó.Ê3¼çŽùÇÙx«Aã·#9@—¸&I|ƒ•íŽ?f!R©™'½g÷ñS!çéÐx»R–9ÙPB9ÒA½ŸÊˆegÌs¦D<Ç·µVH+rªrA1ÃRþšÑ¶ Û-f+ÊË qUión7fd…Ô9ºP‚bbá9€ 57Á¬Ç¬®múéÁ4° 91:y½°K#077îIúuNpíºÅ¾ ,0´6R0s³YÑ•ôÒ«9»®OÓ°écF ÇZû÷)оςµ@Çà<¾/}·jã5#m•Ç¹$,sÔ´}û<¶cܶng\øþm:¤V ЗSù–=îþb€^S£Ù°^ÈG†çc–^R±¦xI‰í3 +ï¢ð¯úÕí\q8–ý(¡–œÚ è¥ÂôV'0Ñqzk'7„2ߺ·QÉH‰E3^9zkB ÞøæÞÐ|$WóFø…„£6af,šš‚Š‹%øÒ/¢$³.ëÈá0ï– !‘ûÇψ†¦ð^y¦j–¸[à™jw+ê§zR{škÑÉ» sB" •õRßí%Ög±¿!JÙºÑó €‚˜‹µx`3ye†qvЊ¼®;3Ç>~VÌt<ýkW|V%'¿æFÒ…ÛpÚe{©Ž3”ý^OU(;§mu¾¢¥£à/wõ +ù-Ái…óÒŒ$Àˆèk±WŽmxBqj="ú-ó PùPÁV¸ˆe|Q¿QŽ[ñºŽÁõ[O±Pt$ Çuˆà ^æ·57O+í¹iÞ6¿**·º§ÁÌbœ!Ò¿#ó˜Âf +šþš•F€!Ýé¤K5†dE¸|Šë2 “„ðø{+Þ0£Õ­èRâóeš¶mzFÏ=ÂÁ[Ÿ].7W$[«à2£6d”“<Éå#´ÝïsSš`ʘuz ?è½*Õ«å­ëpÃ^‚+oaëûS— °£j{¾²Eýc`CSãZC.@…ú²êçq× oü1Ö +ŠÀÇÑä-ÁM°ð}£µ³v1ì¯ Êa W^ñ7•îgÀßøî„sI¦È CyÞᜬ(Ü4Îð#fPa +Ï£þñ>¨ß²`dñtA8³µµân_”.ŒøoðnÊ.ίa˜¶DÛ`Ï8šÒå»’Øø¾9©]1I:ÂaÓ³z, + c3‚Ž—Rï3 lã‰zŒÐ¾˜”¶¿ÊxÜ¿²å©ÓÚÒf(ìÖ†±e°›ñÙîƒ|Ì8MPwëŠý¿šE³nJÔT@³ˆ¶ ïmqiï +ÉR6_C.ìö­žƒÌ÷CƒÜ(ž5kÒ°Q–VågèÆdòŒw’^>„ß¡3e°8/Ûk±ÍÛ´)«Å;{å‡ÚкÉPÙqhâ³Þ·$Uò +P˜ShM;C,=•™jåñøâïåŸRwôƒFç9)2ùƒ•Úd…rzÙzk*4@äð™ ÝÑ+WWn^ÓQ?gÄ]ia×!$òFn1kÄÝœ.fOüPzÄÕM+ÉÏý‡íÏ¡ÙÊ>ÓÇ,ÚÀÜ] ‘e€×ÐÛB‹Bj<‚Elâm2ƒ}}›J!Í=Ú猸h(Å7•X긿QD¥søØßVkÕn ­-¥zÓ5úó84ÿ‚V·^ä*ê¬,lŸ¾Üß'é{ÉÚŒÅöE½@•ð`Š® €"è¾½fmLrCó&ÿQ{èÿãnQìÆ»ÒYª„»"J +# Pv›R•Ö¢XýVzb<²| +з”$±Qº¾3:%ŒNÉ$‘ˆ§ÉsQ Ä¼S>füe‡\¶ßa™Q(ÚÌ!!w“РpåJÍUV\4Ñ“¸öPé€yãTcÒìg7›IcîÃXêï3f$,Ä1c€¼˜AÇŒqδ§n!s³“ìÄ)ˆNN?ß>Šk¡ANèµ¾Iþ3øã…1N[ zR¾ã¶Ü9ˆ bõP¼¬t®œˆê-ÛbòÓºrÜó ¸jl'GõÉ°h(GvýHzbQzò!Ýst爀¡ìú`±ý§ ^ZzewñH4b !Š¢xõàC­YÎNá0‘£tG[xnP¦t¿);‡ +Ö¤4ár$ìz—u™oYã[cª :ä<0_LÐÝcÐy£ÉQí»£i=¶>Q–ºÍy!ÑãºÃ†‰ŠÔë½\ï’‡8>Æ$ìÒ‚¡8ÿQÍW‹²îF¹9 ^ær8~®PiâÉ|">¾Î“·5[§Ívè˜]4èôûs†ÿ9ÁÛ Î3â‰üY¬­¢ÄHüë«Æ©ôIª™;›¾7œb CÛžA!7r¢|óM`ù­¤Šä“ “ õÀËÄ¿g/G0¤lÀDÕ®ØV^)LëíGwMÑí틲êûŒ›¤}¾°=xh:X“ª]]Gp4.BB„–#¥%á"+®"²º—,ämA´6û 7$¸ÅŠ¬..ˆTñ%_ŽxÛ©¥÷êd§"F¬¿¥Í†Òô-,æ”é-n‘_àQB`¹ó`ª”ÖÊ„½…‘%Òìnâítý\ú¤\¬HsÁ÷¡Vç`oc ÿUo„ÑEðÜ!§ÒÏ–W6í´[(:ŸÇ*•BܪTk *À@<…ë7 +QíïŸß±¹vñ/š#=Z]ß1¨.ê.¤•Í-|\ëÄ!ëxÞ»0ÀÅ ;¿¾µVhµ=&ÒUN‡è.ð@Ô„½&'1¼‘ã˜~–}W·>ñ£_F¯:ÅÀåNקd…íåØF3h$ +åüÔõÊ›½ÒuðhJå5­K@žöõâLSÇ^òûh“7/ÃíèakÞJ*eV0šÊ3¼>ãO‰C•аy¨Œ±õ í&«¯ç`÷–‚m—«¨¿dª3ÑYHSQ¾pz¾•¯Ð,¦Û(zÄ>+27*“ûံ¤·ec”¡´ëÈ1aØ©2yØð$–”^$ ¡69wW¶HôŠø/ˆZ-lÑEF@^_Òui±†)“(zçp˜c+‚gŠ0µ¨ +ýúVÛ2+kC6<‰ÀéßÚæ!ÅáÞkH ÊE †]AéDwº7ÍSì/C}ùÎcß‚Ÿ>¯Érä„ZQ0Tüü|J¤l¦ä€ò~ƒtÅPÅ(sÓCv ò‰¤Þ©§Ð'ÒƒašOè‚ CjUN‰›euÿíjÐ}`ÞtŒ.>¯\íPº^qTܹÎê-‚êÙ”9ê3‡+±·ëJ{û[iSOúÚ[…+‚¶¨ºr¨R§I×؇AãÖ5”»éQ¿™ƒ|ó7E0È!iñ’C®E°|uó1€ÈçñÎ{ÌplEÌô Üfý˜Ö¡÷™å.OÄ7`xzÂF!, k?Z·¾YB—~*”²‚=dDT‹‚7ðH ®÷2hÁìX’ +#°¿F”lžªÂÀì3¤^(ZÛJ8~ËýnàF†Ü9u +6•)JÌÚ‰VÞõkUG­ýéÚºA£³¢w(Š¸ë…^5¹Ý‚Qv~©¯©/Ƀ­ê˜^_Ò,×. +©ÒÁúÇð'Å­p¢Lw°ÎUÁ‚ÀPoÈGÈkG²Å ¹€¡˜-‹B`L¡Šf Í쪣)È4tÚlß +¬ KÜq€4¡yY­ÌÚxx|p Hc®Iø—YÓTdîçÊN¾9£ÝÎX‘y¨¹åË1#QŽ‹cP]Çhy×Eï}ü­‹† †-ÿuë¶g@€Zµ-ˆCaªÔ ™1 àkey1£5îDœ · ýWHTùP¥ õ* Ê‚!f%=S[ëMÝ¢@…ÀŸ¾ÒN¼BpÍÞ§[ê.¥ªpÝù|Ð=®y[S£>´6õµP‡àë +[º2XöûVÄpÒÁ•×2 +•Òt,Me;ž¯ES \à,ñJ|îk%3ŽŽˆ +~yjöj#®¨ß-)å9¼K,j#*OõkûÅ܈½í〿l0Ѷ[8Ý[ÛÚ<»üôAÖ„ò „@ûðá/¬µ4Ö Ö²`­t¡:·¾ uã9F€h¦}J!¢î†uõ|ÎQ³e?þ’¢ÚÏókë5tAF«H––û—3øû¹¹b÷á?&ÍíyïOOCh¸J¯T:õ]òCÝwšqbÞ<Á +€„ ~|ëŽm~ýѳh¯ˆëÉÍž†ä·;,Ö„ÒeañÑgÃÖÝÊÌe}³8“Á"^ñõðÃ\Wü +ÊXg@ú jà04+!âÊ4déžóTóÖ’D¥œPÙîQˆð˜Q1èÉjy…ΙÑáztôHÚr˜q¦.ªE6I(¸Ô¨z®„l[ÿEÑÕ rë@n#hr[~ÀçŒËƒç—šÜ>Óû,кƒN=²c¬ywÙ€Õ¬R¹–KbéÖ%¼0ñ2¸©¼/Ÿá™Œ×fr ¿„Ïã(Ü ´œ +k4ÒöZ¾R<Ò ð}ÆWñÇ$_Ùv°›IØv@ôªü-Ù)¨ìH¹ÂT*& õ•~ô-©–Õ3 H ývUÚº]ý ظa–\,ºÿÅ«°ñßå¶øÅ ª¨|^´vm<£«0ðÄÍxcô`FU–¶ñè¼’ÞCÑBÅ6æÓš5ì¼^iwó[ “kfz€ìc=¤‰™ †ˆ'J9{ˆ¼IwŸe›Ù¦ØÊ4Š§ž % ªàå–c6bO×´œWI[Ù;œQƒÔ:?g¸„æ°—ÃtŒ¯ŽSê\&Æ'zC¯4‰÷’`%E°‚¹XÄ*›®æ«8c4-ˆpúˆ/‘S3m˘ˆ5îW#©I¥µžöøú󪲟°M柛i^ <Ç ‚‡ÐªOf6q¬_5áwqq0Ê@dƒvèò Â>ŽÏ–WC´VízÊõðÞ¼aˆ®Ç— GôcõXüѨLXRCds×Ý‹fÝÁ;Ô Â¾…pË7bÝ´ @%‡*}Ų(âJŒÝØŠR˾û+X !ÍÏ (í€^6²è=¥èÿ}ôYˆ§ìmp°Õø›”àgHÒ㊖‰·>5kÙÈx-k0¼*›s¿_ßÙa5ç‚rˆ®~ (L±™Tƒrû:œfÅkó«+ôÊ¡ SÖ~°‚|Tkð+ùqD imQßT“€( ¯ jÚ“Ï¡Èõ¨Ú+h°ÃóhÂpÖR A?«ö ‰õøÿ€ н’opÎJ4Q(wxŽAääžwwt™4y-ˆ‹æ(ñ³hb&ê”dÒ‚®HøX>Ö‚‚^W4 S°ÜHÖ²î($³nésÑt!Ò‚ÄÄ€8P²BäVÊ#iË=@!ÂŒG4©y>öE =ùƒkçi±™|¹è®P*È`­²Ë@ôî}›9çíÔ’É/z‰jNÁ¾Cü èBÄz)ÊOø÷A&ÆدAy/×K‚™Á°(Z$>®Êt¦¯´=sÐãZÛ0úÕ˜ÔTÝtE‚®¼ÅzÀ@qËÖë’Â7ô7´ +Œ0 ÝP^–’8Rˆ7’ÀÚŠîÖÊ«bܸKnøÇ("Œé‡Ûo®Gà’iä7wP/7G) +×ð9;‚•rM™}²k¿©ßy;¡ ŸB=A{QšOÍҼΡ#× ç`K`2¥½ŽæNÛè1ès·ì¬«^ßG`{¨™ª€•^÷.h…pó†^Ä6×ÐÊB…rz}†L÷Ǩ¯‡Dø6¦`&-†K²›F¨E 0H™Í_±•OqŸ(™·žÀÈέæÍõ‹ã ¯‹^eVç7Ð6ê4Ìó’JÖ8­†"þOpbØN‹À¬eú~ ‘ «\4ƒÝ‚¸JVߺàF<Ùþ{«ÄœúÅŒhópϳ¢ga¤þqÈâÞÇŽ¿¥Ûz'| äÜy~1¨¼æYHµh¶vnn»{?á{Û¶^@\Q>9ýoýÑŒ¸!Žó-°Ö0E°VÏ%ªòc §0؋ý^¤© ï0ä lœT“²Eùú œ›?LÞHñF~}«‡¤-QRs;m^ÒÓèÜ´t3_i±ÍnÇ ¯*i' äÌtÇà}á[ÝÏîo³RÊVDy‚åae@Ú%ûÎ8HU .…‡2 “P0ãUÈOV•Ÿ›°yVÁóó=þ$œØQµ¬ÁÑKç0HsØce”ÿ©¦ºújŒx5¨Ÿ¸ta{Mn +ÂjEÕê쟘ñÖùÇà†“T¥t97ïæåÒû­žµõ J¤g þÆ.lb0‹ôwߺ£P™¼6þF?΃Ï%{Dp $LU|7¯„þìIɤ0ú÷@Ñ-ÏÀyd#GnÎÏ·ðÚAã0³)a`°vÏO‰ Ëñ-|T}›G +<º·¹‹“ý†ÿ½ò +,æV~Š 9ñ—^ÁN$>æmû[chksãÇÂsÃíó[ØÚm’·ÁWéìJEدvx ÒÆÖ2 ž ú0áZ·/¨òžìëë™âüâŽg*íjˆ!kp…¬œ{!Â"3áœ#Y¿0ùµß VYß@;¢¾ÌhéŠ7B£Dø„*ï+§„1‘P—Á÷®'pºEÏPU"g¡:pbƒÊR Øî2z} ƒƒˆ†—)ås¡>¨ ? ©÷Á‰tQÄ&rÀN 9y´±2nè«¢p‡ùì/Žý™Ž ‚Úc ‡{ +Fj¼MIUðLjï'T™›KÀÜuQKŠ„×c gÇÁ!çC×µ +$ðßÎuB2EjX‹¤BMúËË~‡†@ÞÒ +,‚$ÖW@`>r¸]þ¼Î_ 6íG"˜X;­±Ë Ö¢o6…ý¥!,òy‹ÑQ…FñüûïÇ4dÃlA¯ôߣçj¸¸rq‚]j¦ +ÝöÅìóoƒ¨ E†Å". K +¯Ü·k—* N~ÁÚ@ 7]ß•%¬GÑT¤EØö| A–H9ÀœÉ{Á} übÙ}DíEÏo7´<×½u]m‹j ü xêÒQw:UÓ<*Øm@qT§-UQàE‹A™ËX‰d[ªÛ芃ًìlȳ" SðMl[ëWì9iõó¿˜ ÖXrxë3´Ð0f]/ÓET=R„d¡âYp‹AÛ!‰ÔµÄêؽÇ(½;`ƒôDŽvÚ¸CDô>­hŒUé!RDn—œk{®~Ë‹½J Éf¹À1­„ŒÝÌ’‡[/üº~¶3(­ƒa‰æ¼ržØQóqåh‰r)áÛÞc t®‡²£;Çhs;E5•ËãÆù”º £M5ñLãû‚øe>³Pïd|i1Þ%ª" ¥òÄ›ðÎhߧž·Sü6µÏ†L=ãh®; àÿ½Äÿy4:"Ô±à aó3(—nX˜àþ®µ §l¥˜–­b˜Z² ËVÁÞ&¹A.Á?îR‹)ëýÎ3ll.à¡@GF° ½v辡wdfTnÞ»#ŒsËÌhÍÛHjˆt¬‘夅†Ž?]Ì‚ÊaEÅÛ‚5éöL¹ûãµ+(cF…Ô»r åŽ3Á8GÖe!”¼êz¹l3²Í’<²\\w,­5S.¬AOß#i×(áˆ4É2V(T=…Ý:=ˆ<š•gŠ3šªÎÎÐk~uÑôFÙž&<Ò¤÷suÌ +/Löë#.”÷æ¦9ŽëóÃ7ØYëµ1i[½k_'~þZ‰¯èæmæâ +­@Í.»òÆ]ž!uv%Âf½ 6Œ¸ŒÒÜ^i`#ÖPÌØ'âÁ™cV¿]E0­+Ðòy¯•;€{Ü$ÐrÕhBöGÀ/ ŽŽ:α:wV¥K¢ÅLÈ&ă3¡ŠÔ8o΄îx¨ÁÄ_`nÄu½Ç¦Eæ´ÈñºÓF ëzêáSB… °ÀˆI4À¡‘^W} $ðK; QŽnõ”Š¾^îblGë áµõãP`‚ŽQèâóºîG¹+txÎgl/p„òi·øOˆ"E­–iOî­0Æ [þ®¿=ÍoTÐ/ß6„fp¯tZ¡È].j…¬?”é ùf¾ÍˆÛ0Tz¸×sC'è‹ã\[…ÛYµÌÌÛÆa4Ðÿ3ä{Õma·Eh¾˜•Ã´ÆÙehë„ŠRÍ¢`r[f>â)˜IS¾íZ“³Ð*ì*4£.¡Bó˜„Ì°aŠº—8f€@i ½cf(V"’vBX’Ù½Nä^^ôï™ñs¨“’aËsÆe2}É¢a®1P¤00sņ¢èUÝäK€R{=6ÖŠ© .2.”"E¨û„ÃÔ/WÛUÚK¯Äg nÀ[|ò|‹Ý“.z] ;€ÓüñyÜÖ¶oëóš½MÊ;ØÂA°š‘ñ›æþMˆ3”¢KÆùtäâ¡\.ºPtvßgü¼[þV?yþb’í_ YÁ”0ÿ’š¥}ïm[Õ&ìÿ`>&.4e)ÓÝ!L§wÚ̦6^¢*Öûç@hkÕ­­uRÂYtTSs” jê¶Pœ¿Âîþ§þ·ô”\Ywzú(ÈSøü®ýKÅ.f)Ä)ihzÔ±µ©Z%Û³å‚ۻ̶Ýd†\Bf„è*xéu&w/\«”y¨ NáQSÇã@,c‚|™œÄ Ó­ð"Ñ)[ñüꛦYî  ¬GŽ©¢Þ#T*úîRXæ[1P2¤w§èfAÖ¯}1ƒ˜dˆÐZËK:mê÷Y8¨VÒõÔqÁTÀù +ñpX¬¶ë=é«Í_7Å÷?ïF…Âj¤]¾8ΰplã + CË(:CìA£ôsF:¨ÅÏ1µqÊÖÆ¡”¢ Ž½Ë\‚ª¶Ö7 £p’i¸T ¬Áï3ž…œ-|¢ªÔûq ׂû+,_=´>î(RŠ/t&£\Q÷ånq¹¥³4›¬¢QÛÖÞ¸Ò%g¥,,9¤ÒÙÅgž3nˆ‚¼œa(Ù!H»Ã,WÜSmŸ=tA×s>çþÅ /n!°5´“ÈEÚQBb†f¯²îXšF–c=u®Mœé‚WÇqj˜)"ÙE´î$èDºj#fw,ä±Þ'ä}UîÜú–û˜¤¦Wœ¼¸®9vhžàÚFyÊ_£/7íhÚ2[‘ãÕJý˜)Àª3©òv”¢•#ð÷q‚ƒÞ' »¤§p ,ËÉf©ƒ`ãÃЭÈIP½Ô{IöħÕ÷Aÿ6%,x,zÀ¾Íþ«ˆê!â–÷)N’ÁÈ×_M/¹Ë@™Ê;õ–Š±Ì–¼3³cU0倛„4géí¾?gD³|nÌï½kåûqÐä ’G-™Õ>ŸSP¢÷ÅŒ{ÃÙV(ÖÚiË¿ÏÞ«d.LÏ´ÁˆP&ð´]ERŠºü›"¶ËdW1#?(‰*U‘?جÑÁ°¡·†éקU}…ó–>•žh÷ÆŒHªA¦øY6@¢¢Tž©iªæ˜¿sn|=A(ýQ~ +1|u×ÖŒ"NŠ¹ÄÝMÆݹkCA5R_œãÚHÛ4å”F¢—XèBÀ_Z!f¬¦îZjÔ¼øbå¶ú`{Þ“Ôä"•;ã/'bå“¿¾íëDÂâßgÄ fzëu¡ ÷ÕqÐÜê­Á†Z9ŽóHÄÕ¨r\‡‹2#þG7Õäe3® ª€$ÛïÏY·9SKº»3Îïì²ÅJþ`¦‹ÜouŒ¡²²Š~Π—Â:KÑñ>`›÷YpΈSóÒƒ}Ú‘Ä»>…°Ø·dß°¿”Ê"Ã3N­oÅýÚÊ›ôR,ÍNi Hr_×aÅ|Ÿñ¾7G•ô}Ž€j몄ña3 Ì5v)=•X)ʬÈ߀ƒˆÐ˜¤+EØ^F;¢¥L]’b®Jþ•¥ˆö! +lͼW$˜@ÑJf§8Óml©¼Öa¨¦Í„ C7ܧgl‰ xRó'©„¤[EOq®Õúµ1†£ðŠ à2% 4¿›t*=Sªi;ƒU”~ }–õšT ËÉà5Y«m —§Ìõ0Þ}ÿ8Jú3* ‰,ð‹Ia8ðïª+] 1‡°¸+iÈÏì¹}µ·ÁÒj jÃŽ/¯Ó&ë5ƒå°6Ó ¸oá–7zEö Rƹ@ ç$‚I¥O¥ {™íWñ.#©ð;†Œ·ï©mu«øäýUrœÄØ‚kÞ@ë¼{üLÒç`{v[Z”¦žXѸ4• Ð$@¹ªŒñ3tÍÇЈmóñÀoÓî„Ó™«ac°Åͱøîv ð\ Ñ&|žÃ‚©ÅÇ»mŠÛÊÙUùQ- +IE\Ï®úê §-aåÒ4\?øX˜ÚúrRø— ³²¬}]=C0wÄ*¬RŸš g\¦®`üeŽÐðR’,‡ážcCƒ}š®˜5©¹ãÆnƒ_ÀDHkT)i9Ô%™ÑÑgATñÔJ §Æ™Ô¥¡ð4ž‘ ½^Mµ•J.BšŸãL4v™Ñäí‚fω$Þ5÷ `ŽÁSK%%eÊñl‡9žÅõV%¨Ç\šXÉW0r}\¡xñQÀû/Â@Ã5bßgÍ(›È(Åc-.´ŸÞ·ÈÏE’Øf–ß «x~¨GYîÖ”<(c~e=[’d=Šw2h}ÓðÁÀåÑMu y_++¥Z@éÞøt%ðä œ‰ÛÆ™(g}ƒx«Äm€O»OÞ>‡ÄâŠ*3^HUP¤wü£ršvíR•®O,Ê€èq±Qäµ@€}…'ârQ"C ’פ˜¦ÞaFþã†ÈQË"gY¦$„hÙç Ê&?šã4ù%d¤ý’@üÒm23INV<Úe9›äP(#L@á2bÖqœµsô %Ôvc†‹؈Óøw®'½hhp#µ„6É)Öq~̈ÃðàÒðÑ –Ñå¼rÑ™An¦Ðœ¯¥ýFhÈÆÒrñœr5j¼7ë7u‰øCešS• ›/ð²q\“–´N‹ó „1àzrè4m4QèqßpXCÿšå¬4QJ#š¯²TÙ€5ßSò3™Æ4˜fÓ$y`Pe¥FÎöz"þŠa+‘2¼#ïšU™¹—=Rp¨˜NgÜâ_ ÓNÀ÷[ØPožà9Q‚ëF¯ bÖ°pøÇ`âö°‚ú$}óQ߈öDÕ ˆ#Iâ œñîq¼Pm#×(Ø'O×ZÒË›èßGÚgö(dªEc?±¥P>Øg¯øÖÄ'aì.+×Àü“]`n)3d¡óA^…©AË%¦2ð¿‚5®¨Nà£IɃˆ@€m„þ™@íÈ‘j´‰×²‚”˜7¾î V†XEZ½èã_„˜,‹k{ZOõƒÓàxnº`ãMJ£¬ÿdU7þSþ‹¬.²îßžçó&»žž¢ûŒ©Ât{îE2 +¾D…6‡'trølÇä" +è‹Qq¹&$¹FòžkD{k›ü•ëÇßð¦ø õæµÊ4è3q`I*ùQ4ùÇ= Ñ\J4ÅÒsÖ ÜYDGiœu=‹ ÇA?Ó€<ª<Âשò°ÎÙ;­ž…ky#eïIÙmÔYh0+óð;ƒ»™1„Ʀª÷è¡~áO¡åÚðáUDº‡cÞÐó©Ý­qƒ½ σf׆~ »‰Í{”õ0‚‡ 9U+ŒQfp%ZAï$Šë¡×õàé–ÿŒûCìa…3ŠÊµÈç×7'K».§!N[)lUúK„wkƒ;Z/ 4¬YÃ_Á,jY€ÅßJœ¶fð_Îgʤò“3\`™!IºEW¦o@ôs"2&‘m3«Ú^Æ£ÆïMG•=‡*;ö ľ[À¡´˜qk|‰´ÖCî‚å.k1t÷¸=¡Ýwí‚u‚µ[‹¾ +ë™ oç"ª^!¯>®§?ëlE¯5€ù°,p=ª=fyU›¨q[´,tdHÑ* +Ä ¤²~D7x)¹:+ +LÈR Û¦^Â<ì,„ŠÍ:uö,zwK,¯™Î•T¤FpÀ„ÖÈQÚ¢¡ÀΣ†gWÓ>†ÖMŸT“Ih|HÕÊÜbôxúà`Ýõ3`ÓÖõÎ"og’—•Xïä)Òƒ¼XQ¶(‘1­BEÕŠÈ*—Æ2`Õ8oÏ ŠY3ZëÖkö¨h9Dç§ûwßjZ²9Ƭ®1À…¼¯_Ÿ°ÂÌÔv.Ê%#ª¡Áx ³Ub  ;?‡O‚ƒ +;fŽÌQÝÓv ÉCƒA7*Wûק¶OÊâ¤mÿUTT×_Õó¡c!!¡™iŽ_'7‰GÁ^2)–®j·Àv¿‡vÚ +tå&­¥'—§ ½‘0®Ž!Ï@ ‰<yŸ[ÿëñK’EWÑ#›zÙ®¯r“Âa¤}w\)/ë¿!ª§'‚öxBår¯'ô²²ˆš¨•Ž¬r’h›;µŒî•¥F 5¥Ó DÒ4Ø€=$ïµÐT‰>m“tØ(2WrÛÃ8ð¢z9Ù½V—yv÷ `û­,:¡?²u & +ض„C“‰†xô¢Í‡b7°·±7Ûõ3QTñOÕáØðzF/ô +2PÍ\Œh³5,[ù½×[òô@à…,ËdŃG™‚.^ƒ7—‘ÑàÿËõ€6‹<É£²v¡û2ÙÞ²±S®@Xn3B!l½èk(Áµìj_®¿ZüRœ(>¬¿¿župìNkEáC{QzÞA•]œg·¦¦ \;CµmÞàºîEù-2´xîâ3r|«„*öËj´QÞlnñ%|ZÖŒŸK9DåKÖ@&bÏÑ•#i”¿Èûqb]BAt é2¥­»u"Â!PSõªg?é6tžCРŒÊQbÀU§l}!_¨²­ÙœÞÿXó<šsaû›âùF;ÏPOä¯(WŠ|Q-¸Ýxî\àdhÛëÎ!°II@·ÛõÂN™ýy;úÑ/˜Ê»ƒ|Êiª*’ƒ•™C€™Ÿj•›þÄŽ©OeÙ\ë}Å÷ +p³úWèXª+Ž¹Ù7Ù2):B: ±ñw Á}ks*„P†E‚>òò”*ÆÆR#¥à›._Ýǹ‚°ÄLu~‚~³$›c+1UÝCe9ýˆ(½v©ü›ud8Ãl3|9¸-Úê±–ËÄ+pÝ;¡Öþ@ÊYæ}æ( +»E¢w½wIšÚm5sXDÙµn³’Ölf$h£ã“ƃ®îƒ».¬e#ãR(êN”øs¸H}»úiFÆã`ëÔ,ÊN³ Ì/{°¨xC¹ƒbEq@Rî9\ºôÔÌ1o¡É~C`U +)n|~ß ¿=Cfºœ: +˜@i¹½¦Zš{ b†ÿ\w±JQÑ›çD +Š©(ê£EØ¢zFÈ˲Þ,É4Y K¾ ™òJ¯ÓÃ’Ö` rª’žë•v³¦é‹%ã”÷--Óª£´ŒÜßBñÉ +¼È‰u=HÃA¨ + ¶KÈÉto|ê¡õ¶ÖN€X}hô3uƒ ew5KçmZ­A´=¤rÊ€²|kƒ¹ïy”áq€Ýÿ¯JÕðz=šœ0®ô§ ’„uJ@`SUëyò"4[oQŽ ºŽ(RÌçÈYÎ-8¨J=a¤‚ár½^UòTj{DÃ4…Br›úz… Š×]¡éÔDP¢&/8B³Š¢æˆÒbe2·GZ´ô‰iñQšì[s,´CÁ‘­$/€µDjê öŽÞzñ-¿!ÿ<&WóÍÁé%Ÿ*ݲâF~OÕBÇæõèêؼáV&E ?p-­š˜ðÇÓ¾½€$ZßÖç;u)°Âó‹œB^ÃsAa†æ˜ÕâÒ˜¥>$df® ~˜¨ëvÉNFÁò\ò ¸X·F+¸P‰žm&/DVÏç#€èaE[ÄÔ?ˆ]b0h¹NvÊFg¥DT ÍXTrœtÂrÔD‚×-›’ •Ê,‰D|û8À”ótÏࡪÞâr³ +±>Gò âc«æ†nc…òAgÇŽ|¸…Føq‚ˆ"ø‹¿œ¥]+³ê>ÌØæ™à„{†ho + מA¬ÙQ{ˆÍ™Wûf]ZË•¡šN™a“.DføcB5ؽ̺ÅkÈŒ¿•ÏŸÑ"ÿìôäsàݳf=êòN#<…NÒœà][ª¹×¶´§œ$uŽu„2à šœÆ~˜FH.s§4nD®~ò¯®:÷ÆŽB|Ý@&ó$vݨZ¡¶ $Š… àqoX†³$  $³³Þ<°Ò»=N ~Kîaš‚/‹ô?ý^Ê—Ãd¬tŽk±~…YC-Ð+¼ÍñóÄJÅ]k|[Ü0Ò7xØõßrãz…Ÿe±×áÒÁ¢°6¥Pà +1Ð(•=;²ñ‰¦$Y¹ÌbŠ ø&`ë=aSÐY¼9]Y›~¶r-t ø\Yša¥Hh®@oR1Üvæ$ˆ†\(Q‡Wôêë¹(¡^(=%ã$t\ Ž±i!] J…½Xjä®8íŒéýgÊ¿’CHªR>Q·£Jd;á  Ì+¶­+ +ÈÏpy,s +B1ä–“‰©o})¤„¤äuéT)ø¾/ùûÌØö± ¯w’üÐÐDÎ×VY§Ÿx:þ¼Š3h–AJV~Ë|dû¾ÌðcÍI×Ç‚ÆÇC÷¨µ]<¢$P/i该§ ¼£p68iWÞý6•;pÔ`›7øj*YCï‡âEîò>¥ãU7£µSŸr4³ r0üóµNÑQ ¤¶Ç 1œµ5D,Ø–ZOU–êÔC“‘’[Ï{µf<¼šºù‚kmF'ÎÇêăŒHösŠÀüQ–²b2Û¶8þ¢#!Ê› Û׎&!•Õ#+JŠ÷û»Ü™¬º(O">çT¦ˆ¨%t¬gºë"JƵA¿]O`¦‚†Å•Dù +=GÈ­ }L:=áhÊ!3Îò'_(V©–â)Ç)–Z(¨õP©®Ãì +5œœë© Œ3Ùªë9"´Vöñ¦~Úz«›¦‡7Â5ˆ‘Àà7Æ ú‚¥GŒüL|E*˜Ýè óÔP'ÌGA¶«õÀß@· -O€v“Ž\JÙxüìYÔsŠqݲI„É|£ÐÝIÛJàjnâ=0ÒœQñ„žx(®÷&†4Õxé´ø¤5O'–»S8°) +½á©¶ÂÖ¦þÝ»ø:Å7¹Šgc`04êÖž!¡å\ Å •ŠK'XªOÛ$uÒ;œ¿rü ÁJ'í9Ql$×1>›1™CÀB1@Ze3e#ìº$O3Aˆ`Ñ|“.©žO1…»ó¾½o,1dº#|+SNå΋V1·Õ¤Ñb ‰Ÿ‚…Š†( »¦ð!Ž3mõ¸R7©^;+^5ÏâÊ S vº– fàÁšï!8¬)0Ü4Ý3pS8ôÎHŒ+iÉv‚TÒ3@Ô!WöVLE^ˆ‹&R‚ÄÑ¿õGLí%Dd¨³²1]›MÅ©º`°IžÐX¦] ‚ûªùB&zs¸$)ÄKoý|䉔£0ˆ«Ñõõñ¶)+—™&ö7[°G7…-™w‘j¤Ü‚Að§_1ª¨ª?(«ˆÚW7cqAà ß[6Æ:ݳ-æ0 +ÁÞŒ›EÉiÌ‹+j¹mhߨ2ÀÊ​3°¦ù-àÊU·Ð˜DP½&IØÈŽUv瘼i…ö2À¢6ŸŒŠm“pkä5´h’IHí¢®¢] yº-0¯öÚéÝ ¤iºkÂ{¤“‘hpg/…{¿W 81ÅtzÈPÓ)ÐçJ»‚[uÔCFlusª[Â3KËØ€2“¤:ja\BÿSlhš•Às ñ-ø>(¦¨Í´ueˆ‰%&ÂäSXµuÈDËrÄqLçh­©Œ ±¹Õݳ"ùþNGLˆv©xÑ ¯V„€%>²D ãSªûü!£)žˆsoðÖ+ ‚©rב¢LÑÅçÑB©œYÍvÞ>w¨k†ÖúûZL¨]0á¾ö£«JÖ]«2©â‰©Fú;«`…V¤ ­˜¥Ó€3Ø9AÆJxÁâth¿"8EE,å“R ‰e¶Á+ÈôŵLºôæ% Ø.#i­Npß= +ª/u•ÉBÔ*ˆhK¹zS1OÚ.Š/^[‘š§ иÑõÖ4ו¾‰mšvÛ¬oV7Ï™„h%Û;,s†QLGë[µM Žpð6[±Fº@²l z¨Ù °æûQ +pòŠ6‘+“ý(!°­mA4è½ásrÙ›3ìà­VåÌÍO¥©4`¾§6VþÑŠï/ÛÛJÉÑC ›B†ÒT +ó0 \ä52ÄkÖ +ǪDIןp‘‰òúÂW;°Ö¼àdC 1Á!T·yKÝ1:ø-”òõ‡Qâ +¶x±ƒg!Q ˜ã[¶¦±µA‰ëIéÇ–60Eçý»¶Øª±A=K¸’)ài€ön@Û)õüÞâ ÚªÛ%ˆÖ6šé#ò3ħ“|Ù³Ž¬;W÷™ö/PÌ>fHDC“þ»3 i¡ìa©—Y[Г‚`iùaüΚ%ßÚeñQÏQ•u¸ÞÛ’)ÒaÈ;HªmÛÚ´±iIJ÷âmœíèôpráÎjW,éÏ ë,–5+pqÑ”°Co>  5½mØ·£Ï@uùlú£ 7ñöÁÍĈÜz–±™Ì w{*@4pºÚaøúÅ6AšŸF—Æ’‹Õ$uZ¹¾F‰Y<ËÌB±àïþM:¬wÑT!ÿU󯹾…°bRQGñeµ´-dDºîûƒšïÏuì`@ä›G(d„ú ïÍ[{Þª? o)ú±ä#ǃ<>®/ëO +!SžtìT¨M±Ä¥J Ê!¼©íDx¦+‹ZÞ.ëÿOœÛnµúâW=Ùi‰Î&¾´„S‘è¬)€³!ý'ò’[Ogä¢ôL $ˆ›ôÚÃ&+"lMÕY‹­®p–ÇéÒ¹…¬ pb»6ö•9î6V5&¢&„Õm_¬qêßï$NþKAЕfHÙ¨àž×´ÎŒ´À_¨r³z¢k@:O}m`£SÀô¯oe8J)ñ.}ËâN.#Üfª ùHýÈÅèíDÙq ‘N춻[mI2mESq¸¥tw«ý(ž «üã\ZÖX¨»!V„>` j¤áȤap4è „ò["]þݯz.þ+ôafÿÉþç+[AùåV´ás~9^뢸P{AîÍY·õÔ÷Y26\º.XŽN¸f=Üùü2-;®§Ê–¯gËŸ +J%9^,2ÄÄæ‹e› Ñb»6è‘·áÇ;6ª4Ÿ` 7ZƒááÁ˜ÀL¡B íáù¹¿‘üÆÓ«ó\T«‘Èe¡¸jmðƒµ¾Ö¨ŒDŸ<ÇáDÏ®løOÕ³mg‘ ò%Åi@bšÈK®PIyWVl~a Ý –.ãö#”=îÈRXâ=ËŒ†,+¬úôÓé’¥{(º-J‚õôPF+ÌÃ]W¸d¥,XZg”~ OÅ£p’^eÞwç¾ì«¢Ãùýzª»ìm¥Ô1qɳãc,îü\õéû2xb Ãò‘›¾ÃA¢R˜ +×>ØA>§­˜¯êùœ¦iæüZƒîp_t1âꬾú´U©Ñà^KZò·]®}d1‰ÖÓpÛÉÜQ)Ô‚û|É¢Ä*dŠ» 0áãóõ(ä­µø1–cÛhyêjAÿ[´!,¿¢¢DÏÀtÙKãóø]IóEÚ0V²ÎwP(-SuÃ6Å™QXr—´&‡÷H‘-låIôº`q‚l¡)æNçZG6µ¤R0@hÎÀZ?x`f R/’˜&K+ÑiSöâç+âV¿³®7ë)¡“¸’øæÑŽî]£–HÁpÁºí· Úh)ê5¬(r¸ÎÁOÿø<}çaÿ>Hš’\C ž§Í&µÚŽAñm¯Eß“k\1°%ßšì…磆‘e¢uxœƒN5*7¿x*Þ;H\,u4jÅ% Qq᱘Þ/¥¬Dù”Íai%,.pøÅPÂ¥ ÑÞ S»+™¨Š¶“¥à/~©ÊçT³‘°žŒihÅ]A;Š™üŸ„Âݦ{û´hž³„»"c£ÐùNauÔ¬5‰:IJpb€`|jÜvžÉiîáȳH° V‘§K¶ ŠÕÙ Â0:4ï.Ñ5Ï@$êAØ;Ÿ;›¸O6†x̪œbÆ— D +)-ö- +ì[éè¨3ËzÍEÛ'fáûÍ,òXcq̓ÌÆ9Õ¼½´}þ¥'sÐêì°Z#¤\”5(²ñ+°V=»µ”¢Kð0dûì#‡-§æ<ªDµ(ËÔ™e} +ŠcT;p%ÍIQÒ‹èQõ(¨ù¢½mÄÅál)® ‚g6ß!Õ|WCXp¥iTgo‹"(™#v®SÎBSy¸uýq½²Óp, i|#Ž‚º³BÜíÓgØbÊøsrS0=àsnC26´[8\¿Ç|Ç²Þ +“;A“•:·†sÅî;¨Þ#j¨ +ô^Kõ=”uêÔy>Øx\ E逈±õ-~4›y@6ðy t8UH¥ºœ;m¦Q„Ti·/¢¸”$Öç©ÄÑ0oðZóüvéßájç6«Y«a:13õ6Sk5“çêFÿkÜ¿á³-ÆÖÒEƒe]u5”lfçýxßô׈hBÓY7xI»£g®Ë FÞenð@Ýà´˜’© +Ê<…ê\è>—Ww4fÉq^p¡Ó×°_û€×´ðè(¦ª$Wt\ÊþªnŸëãi‰lêõ˜¬í1hpÔ6nœ;Ùvhµ¨x™4•¤dXåi\e·¬DPØûÇŒÈsËîkä$lãã0€>%Å¢\Ôƒt85u»‘à´B\žá‹Ü§¨;ö—ƒÉV¢6¯Oðp­E¿)À¿íIÓ‘`ÅZWvågÂ;!˜î42Yr5ß½=N¾Ï™¢¡êj^àùÀ‘…qCÄÅk»×wšã^3`ÒÄ h”\¯HB›ïÌǦǒAù—ˆ1_›³OÐså0HJX N`*›¶gt£†Ô- é¿A&Kò¾Bx¶ÏζñìF–ul ä h‹«èšC±dñ¤Nj°|„ŸZ…Ëû ›ViÖrùX®|Ìê»ç]Á«•ð QšE$B˜öÚŽÇ!Stù¨§¬K¿¹|ÎøyW]%y§¤„êæÇq6j=šô­ý-6 OD¹`IBÞ`a;ÜáYš5Û¤µì¾Kà\|º+â‘‚ …¥M6ŠPyV’5bV1ωfˆAŸ ¯mø'Ò£rX5I‚´R¿r?ž\„ø[×2(Ù˜¤xÕe"à|(]A±… +à<7ïtß4ˆâ犨Ç*Èš,fŒxÎD±¬’¯²Yf¹{‚´É-Õ½Ä+N‚RdÙ¹ C]„•¯ïfĺruÛ‚*[Ð ¼¿šQÃT–V¿Ïzð>  ý]ø¾ôÝú pËþik2²Ä°´áž¼Ïñ6WÃ\¥ˆ|ÿ¶ÑROUŒ]‰©ÏûHF7a½„ÎÇ, ù¨ùbž÷†¿Û[Ùà$¬zæÈð|“ý¨üÜW½ŽÒªka†¡Xi°þ¼Ù ˆE4T%›Ñ¨ ]Ë÷ÇÁ>ˆe‹|G}øîÖ +E@ç™ðqC|6“mð^(.c«a8œ…sÝ[!‘žfPéŽúK¡ª‡¼{.äBà?øE”ÙŽöÒ3-B³ÆÝ‚Êd]ÄMhÓ ½\«NÞMîÛU"…µíÊšïöÒ׳œc4ˆ‰€Á B`*j_Ñ÷3à|1ãf#MeK²ÖŽŒF?«=:^íi€ W¨—¢HÎɯ¹Aráœva¾aÆ6c@}îÚ€ÃlÝD!±µ²”"Ô”h†€p‹·L‰ÈtÄ®¤SäHÑe‡Á…"Ç_‰~Ëä¼½´ÿ}’g§JP6À:H‚¸‡¡ sî — C…`u·Ò$µm_âÖ×jß6gÛöÖ@åV¦T…4xœÈë0#0üÐ:¶{ ³¤Ü·m¿ì,v;fi~hþ +e]±['êåLj1—-ÄÎ ™øÁ<ñóåSZ··9»_Ž€ðÖ —±ù¸'*T ¥?’œIdiw Õã0*å¢H{òÆ”­5·°P®.bk¤{CÐæÚ¦Æ5Ó¡±^ùzX…# )0R+È`TkG›Öž\¤WØ?£:¹ßÛ @áB“÷5bGì“jìÃÌêIî?c„3}Ò¤pnÙ½Y>=˜‡¶~ ŒAɱӥ”„3C (eWôÂTž9À^Ï™ ü0«ìß㪅ê$ÜEg¬ž,­±yb²›ÔôoëË@ô +åž;]¬T綫¦kCë"m×C+…á^"œaS^¯rN2?f”LàŽç¦ÌL:K?…mp/ô5QËLk>êú&ŒYI £iwëƒø8:(m³ç‘D¿žoPG2_U뿃%º¯gE%åË–”ŸÇ¦'‡¦!ØS° …¥Ì+ݬUá°j«ÀŒÌŽ;Úó¹?ñÝ´æùR™QÔnt±Xz)ü4µ1AÚŽ=cì÷±ç8RïƒÚ"‹50 25a{f€øÂ>œ™¾‹ñëåîæ.Ñ&Ø3åWú{WÄÚr¦³Ú³T%añ K—iSÉâ<”,–ëó>¡QÀ7¤¨Ç²ìsNÚ²ù +Üqóú–“®Ò|Œ™Ôøàª0aDDqw=>fœ‡f*#¿.ßüj9áò•ØO§˜;R/ü×5M‡4.@£Š<–¬í1÷Ù0©•wä +!Õ\•ãÜ$ªI…‘œ4* 4ûÀ+…Níé¡ÚIT•È¡˜³Âm~»š9HokÉsÌ'_±'uñxxG×YÌ3¹†­äíUjzU «F‡M:_02zAä0… ÏÑÍ4€¡@½>gÄ fkTôp®ÝŸ×ž5âA@»&Ëæ'V©`këW3úÛJGŠs¦YÐC¹»¥f N vÏ”­¸4n B2q!™A§¾%îb¯WïÏž (;1ÀSm|u|Ûõ*"»à*c *(ô>ÃîF˜ÂîgúcA-0b0½† •vÊÁ°“—]´ÆFTÊúG3†äìsÆyM‹×†š÷}q$H¤¹`H’c;P¹Tlùr¹‚ûF‡Áµ±xή* +Œh>£˜b}HÎ\ª b”’O >R³Þ\£¶z†r™Èk±(P Šø¸ˆ”\[Y7¡áð¡3ÊžÑǃˆ[[èc–<àˆ¹NC]Ku@VJ8S¯_Íhfë”ÉO°ó1I(~€‘FÀLÔ'0ˆõ´àÿ¡¹Pÿk +³å\­Ë:ƒ:  ‰†kéG?.vw†i>H>ðÎ`«aÆÑ’‡Îª_îë9A¨m®ð®Î{³vZ'ûLš®¿¼Ä™¬Ú¢ÇÇ>îIÙ÷äÚ3TOmèÆæ‰þ +¶NPÜýƚש¼êã™#ò§â¢Ð>²¬–•Yõ ê}ÎØ ¤õÌ©ÍÿóΟ~9Km°Ú`ˆ{fèÅ!Ãó•=Rê÷×*ô>½\dž‚Qß“¦=Æ&[äXé.(ÿTVÖ¯4ŸFrFÂ!%Sš¹Kª¨‰ÍÍ$[k˜B:×^NÖó& œ­³Âpj}ï·š ÈæA¼z‡­$Ï ?ŸÞ–ºísèíüy˜û:¬9°Ü=ÒTËGhÀn|Ìørƒü˜µ6åÅÑšFº¥Üý¡| ë^‹¶Øÿ°LG³. äϼé}SðzþÎß³…¿g—µM%'NìVq!{R00—”„Ÿ…• øa‘~‘º´ Õä!*ŽOßj$J/s;‰²ˆ€çFQì1Æ‚ÀÇŒ¿ì‘K÷Ç×5e†¹«r ¬k1²~ÒMÀrbÃU‹Kk}‘ñ—”eŠèÊIeîIÜn&¹c9 ?²{Ϙ1cmn{ÆÜ3Ê™AÇŒqδ݊׬néFb’€¸VŽ(«¥ &nÛCðÁ@Е¡•øC˜˜ñ`‰ÛVuÆm …Œ°ô±ypc†žŽõ‡.us€Ö›ÖiHãœî@/*9†·B9eÁ#ÓI ñäDZÞàLxE+ÀxEC1¬å”£‰Ï{8Zw!Hk]¥*X6 Û‹TVéØRÍk-¼gHȯ „X×D/ÀãàÇ ,ƒ÷q¨cÍ)gÔ¿æ ó‡úã3×å„TË}µÕbã‰ùj†ªƒ(Žkï}EGg_>*öó14.aV•Vø\e;@M)Z>LÙÙ Vg°(ªœÑ{óÙ2ê +Û•mñÛwa0Ûð•Õ]fíºS +¢hwø²'äj•è¢ð=óÀü÷‹ÐĤL²:=ØÓâÇÎZêma9ˆ9!¡Zec˘2¼àIñ‚Ãz¶ Ælìé–~€Í™ÃZ]pH‘*ÑCTÁh• ©sjŽ]f-Ÿ,\X‹¶ ì\Žv_¡³É ¸p´\s¾iÿ ¶‹ØÐû@ùÒñ¯¦Éå ôAˆ)ØfÔ9/¢ð‘y&2ÙÐú6¸ÂSYÑ–¼[>8:8ijÍ£¥ž(|~»¿Ó¾ùø–d/|_äa±ì:c™4"&‡zI{ƒwÆ.†Šý|îQ•ÉÒÏCïùšB‹•û½6{Ð –-ßg¨ìrdßßQÍ ¹ˆ_‚ƒ>°$’•p»É3þ}¼ xqÓ&¯ þšÌš +õ 9ÿ>&ªÏÝœ"ÎÕ„¡?ƒ½Ôø!hÇžŽ¢ß“¾»(~rÓ9¦ÈºRmH)Ô1ü¥iŒeKâàøcæ#ê4þQ{±Ä÷»9ÀÁAJ8hÙœâºXx_V<¶~ý´]ktîKšT%*…7¨kÅFÜû@\›÷HâãëØãh⎃O[ŠŠÜZQ`I~Îðâ?'x„GÉ{?«²EÙ¤ÊÑʶOxŽýü:Y5™ŽØ +ÄVÔx…ßË¿Áw]³À¾ä(•cp÷F¤>¯4b/G¤oðΊ!š¢,ÂÉ©f¶[O>ú ì*¸T§rQ^}ŸqsæùÂùè~éàTŽ«kã”E¨ÕùèIzçs^™²êıxÂBˆ7¥amef¿„,„jhÝ7ièÖrõ]Cxìt[·Vç"1ïdƒ^,»g„.Gi†D*q ½Ôç¹´ËTàHf¾$ÙV€ÃÛ9(s1oú1€,Ø‘'xöON'¸™ŠfýÞõHlÑ'Ðïö Ä29¢h˜ƒ£úúeOjFè-P)+ù‡~xÿüŽîôŽ1xd«ÞlÉ¿.¿#ÐìàQÔ/¸é·=Ïk®µëò6œo­Zº® ÙÉbäX–¦¯—äKÙégÙWuëÚsý2PÀ¶kLr”É[ÛË5Ÿ£X*Ï»UA>*=¦´€9™ UgÐÁ€ò´YL¢H=¨5û‰¸÷ጯ4 PÂ4 Ž®ÉíòƒÄåÏgü-q¨Aƒ·@5ÅUy0b_·Øä¾üvµÎÝjà\Ï@Ü _Ê+m…äø| ¦Ž&sëfM¢”êN¡hoGRHxÄ”=Ü‘/¤áƒÈ™ë7®Iúl¬W—B~k(ßïÈ/)Ë$»æõúÖ&ò¦ÙÂÍ\ Ä­®=Á—Äå¾${̃éò£žëë[sK9àÂÌ—@'âööyÛ¤£8ØÛX ­@ gÁ¢[?p Ý›Ê ¶lý´Ž> lòï=6.Ö ò@-@’€ÉZíê‘Ät`Ê–¦ãòfƒÃÉFôžmƒFþ ôþ:^ùä—’Ë¥ƒcÂQJÇ ´†6U¹Q¢`àÙ®ºòrŸã¼ô˜V¼]¡ Zžákš×íõD`={àÊÔ]Ï@ü}ˆ™”¥úõ­ºy&$’ëj`÷%íLZ \Ÿ\C1V/°Oƒ—0§‹ðÆAÊf·Ïu»ôf˜<ôaë N^àÓçÀú‹ë8 ÛÛ`†D+bæÚöJyè¡A†O£«¶øĵj_`ÂÓ)ÎÌZò’•ˆJN­À‚*@)Ö§ÚŒôïä~ß[Ûz²wWŒ²ýxÀþfKLcQ(ª„vë‘;›[¨¢bU +ÙÐ3,5¬M¦Û4n³þ"B‰Éæ¡Éª ½VÅNk:¶nÊ+.'††¨ ©âÓä\lÝúU@²1ùûz—’u+¢ëK3ˆtqÕ(©à”H|r`é!ÞÝ‚ Ò¼ù8XƒèÃvª:×ë[¨Ö_r'b³ûù·ò=t›¦,Ê#P¸YþYÃñ‚¤§¡Oköe •ÑÑ+®S—Edì°MÑÒŠšì£ ¶eüîPÕXýPÍ*\ØËJw°{_Ÿ“ƒ‡)©¡ 6± üuìËo‰´‘/ø«œÁåÁ~Œý{R ÿã÷ëŒoóo¿ýÏÿñë'Ëßþýüãÿ÷·¿ùûß~÷ç?ÿáŸÿ駿ý??ý—ßýÃ?ýôãÿôÿ~úãÿúé?ýþþüŸÿùÿ÷OqÒ¯¿ñßÿð§?üîÏøýOë¿øq÷ó >¨ÿ•ÿ–~qM[Ôx€Šî+hká?½^*«„Ù.Úp](Ȉï¸ü7 ™žÎ:Êeç…#?ÿBšo­e»ªœ°Ý~E¨œ+†EDyPR»ŒÏ*öhç3hÂô˱ÔåŒ&l&/æ'<)ä ÔU’¿C©·&4‘sxˆºê¡]³~w6×¼¯QJ¼¬7æ^÷ëw_?ð¦¢C»‰.Ìú£hw (ËJ“†â™ßh»À©)g–]!hО_—Qù[_¢˜Û ®¬J…ÐüwÆÑ•ôc`löJ  à • ¨ù ¡H-ùX©w! Ú‰Ójº}GÐÔcW°‡\rµ·Â¾AŸ5¹Ãóªóë¾ —IжtÛüT!Çmn¬qè@ *Ë5ó¦òÚk‰‡ui©„p °™üHFOyƒªÝظMòQÆÜÚa£w?·½>3t¸çöº=¼>¿>½Ä £”›ÊD|«o0‰ÝMžÍ×YZ‰_¶ó/Ír{h˜#®‹‡ÐaU9L~ar+ÛXÎ?cá't„º¼aàgæ Š{ýû‚±C@¯ÇxÍí¾ÎN¥yß +tàK„Mö¦yP»Þ?ŸÀ;ûÅ Ö<£……6>‘š’ëö:5_šÀ¡éøXþ¹•­5)î7h~¨!÷Pˆˆ×*#×€ì3È­„˜„qr @?H5’裨pN 9”Ú€9ÑxÐYþ¦½Áä@ƒ[s0Ô1ÚµdÈa  IW‰×çWDµ$2í¤^{Œ@EˆN{ƒÎvGémhЃïzÑ3ÎÄ?IB÷û€¯0H£Ô?“XÂ0ü¼d$óÑE +HP–Ô ÑVvúk+ro}´?*êðÂâ‘Ø®½" ›eñç(ëä— ¹~·À +§=*}\@ï¨{Ð.¦a[ΪsoÁ׉-¡1ÇÆŠð arvtÅ!j/uØ.m% +Þ{€Þö H•’s‡©‰ +Í|!obU&m»C¼V– Ôi6ÊûîéÍÓFÒ†w›`²‚¢qà +Z6ü=aÝ0Fˆ +ø8 n;´kB×·åL›èåˆe§øŽ+»ª£äbek”"zLÓÿ¬.jqø²VuJ¤½Nɪî—+^Ts/aÚBgÙÚ2·ügZ$ÞÞ¡I„tÑØùK€N–ˆKºÇ¼CP··mUî +Ãg…ÉNÜ@›•J ¥Ý1Aö.ȼìA3ã"¢Õ:Ün¾iú”EÇŠÐí؈Å£ W­.›{ ¾TñÓàñû¶zµ¸ßÂ1‚*Ö]«uþ%uàÐ6LAr›v5¸ƒrh×foÌßÍgÚ¯ÐD Û逮¥‡ú·š ÷ :‡ l·ÚCó!>%•Àù' â°ëxp“ +báÝdî¬TQKz š@¡Ò{€útc0 s šÚzËî+~êõ­<}3™Õ#_Ê©Zl‹ËyÝÂôilwk³‡tu\Îé%øñ¼2äÃx*õü8çŠRÏxK°M¢×ï©noöü–sÃ5 +lèœ0€@‡ƒqÊÀh{kbZ®þT† +Z×1ÚãÁXXJ À B"» ÿà:ùÇ„…*+’»V?^)ÜÄÞ—´†!êOú/J¶N¬uŸ·ZŠŠ=Åg@ÕÌæ`Œ«nENˆ{,nx³ÊÎZá•B½Z‘2RIÏZò­€ÚhB—Ò÷úÌ5KÜÕ¥5˜ð(*^œ…þ5)4 ¨£ƒŠúùñDøèSíÇü´p0ņÌ‘ÄA‡´¤£¢ùç߈ +–]ˆ~>¸¦LüX“5_ ¹ÊrK´qþíOÃåeÎç3œß(ÇçË×ëßk ÝHðó9üôSÅü‰h§f3`ÝTÐ?ÁPV³ñYó"!8sTˆä;üRcä]jû™ÅÒÓ0z­ÿ¬êzôW×—y]W~ +XgœÕìOÑqxþ݃Øíwög˜øªá€r½¡ü$Ê SϹ׿}Ö Gµëùìæ¦ÐÖΡÍxþmvÓ0Îgž…„½€ ˆï¦~j +XüÀúä‹­äÊ° ç% çƳu.ÉMÁ'…MTw†CODrW™â(¢-˜OâÕ{YÙ³æ¢!µ°-Lm‹_°={8© ˆh–õÔ÷&¾IQgUăFÐتÄ0Þ’¬¨nò¨çãCÎ`£^5Ö çï˜Â2°OB/V½ì€¤•×ˆÀg… e„ì ÞDÿòVþ,^«, ”P'k z„‡9s|kÚÄQÀ 4 vÖÅõIWœ‹eé‚2†3õ?–Æ‹Ü—+í‰^à~ –ÇV¨ñ×ø"]0—hntp4Y§ÑNa÷#XÎáÊô<—ôŽ+Üý ¨/=–üG‡9ì%[ü°5Õ-¤¡#Öõ”A +H¬ÆEXl¢0”./›ß¢þ(x µGË%KÝȪûYTTå,‘–Ó„ŠM¤J+µgˤ?Ÿ*zÔg¬X]µ|ç?!KùO:¤h¢íÄ%éaç3¸®²ƒ´¡ÉlõuKª¸ k€T~Y‘ú>úÀ‘Ór[ÇI¯{‰î#*%”1xÝŒùËæÑ@žÅ:tzß@ƒœÔö} Æ8 -ê&òÀˆkÈ:Ó;fÁPë°ãäë¯/føÞ\x¶Q°^wÈ áã8`ƒRèêÖ6q†Øj e(D°^òƒÝ|vÁ™ Q9;’· E4 b ¶þ"µÿ¨¼Ó:š¼Z@]WÀ•‘E?ÞgD~¤þ+ˆÕ/¨‘ú6‹›”âî!XÚ‘Ö…Šú9pp4çaøÅ Äk0oK0G–¬‚j0.!>(D„ðŸ&⨿îöû5 êÚÚcWX}]Ѻ¾ ðœOŽù1xES]|}ØFð˜ä0ãpì|(¬·šé|~nþ «èœ ]¯/]Ê b—Sµ0î4‚HœðÑaÁéÔS»ÍJ[aq¸o*ƒ‰e~DïÌ^—5°òsKq¢k²f¬d-UX6}¶[ )M›0î`PÓ…‹Æ,‰ xÆË~‰='ÖãÅ7(~ùÇm‹?©¡ÙÙ%ÖÒ è‘<æRûžß°žc~5î#|5z¼e>ŸÇmÕ˜:aäkËB—{ú'õ‡þ¨Øì÷1¸{Íæ¥Éõ%v-ìîÔM"¤q?ñ'Èp;aßåõ­[›pÏ×=·Î‚5²sÅÞc¢;žUNb×ÐÛ¨6‡)òȬ´÷ùyr‰jŸ¹<M{\ÿ[lÜç§Äxå>ÙöÛ ª»R/ƾ·ò‚¿­ÌïŠà( ä¢]Õ¼ŸøópŠgA^jç[ú©i1èÅž ±HO¬h÷¥³ÿƒÒˆ^‘P±›7ƒt' +þWdݽйnúgö ›‡Gš‡Þµ^õßCl(<×-iè„1`êç7ÆtàñÓd¢w …äŽ×…o×H?Ús×úmTø‰ÛZÑØ +åùwì@?·ªºEŽûšÜãqƒ Ë‘’ n˜ûvÄû ËêA@Žpà—¤UFü(°ñ¸¸Ž­ó4$¿Ø·nj=w vè×6Œv†SfÅZNo9ç°‚Uº_ÁIŠõˆAY>)R;††ÆHÈî©©6ú¾bü9@¡)ì¦HlâpÈVs¯{X÷øîòŠiTr[Œß ꯯r¸Ûý¼ƒÎ_ 6="€È+%HhÈp=Ýn­qe¬¬«¿žçcôÀ5ƒxFu´#H#ÔôwGøÒ‚ ï ýþ òçØòoÅÜÖ«’±Ô‚9yë2÷¬Ë w‘k'Õ™î?Ùo2êë»lÛ’ $J&ˆÖ>ßÂÅ—x©˜¢`¿Xbߡ©BÊÆÛi·¢3ÎTS¦‡ºõi¨ªÌ_O>EŸ$j'PzÀBMh©Mk5ÀHùeôFCYB?},Ü]G w"=£Ó-4S*¡¥¬˜?•cßr¡ÏçÖ®q-`åÁca§þ~'(UTuíàC@´ãm“»(ÿ·=‘øh=IñÑ¿h¢:öªã¿Ûþ÷nŒ ½ÖÞ¢Æz@Š ¹ÿž(]{ù­®*ø¿ãñŸ‰ÍÆ×d¼k»þY)…Û\q£Ü[è¹]7xV|ž!÷fXN»iŽïj MN>³L€&î€rn êiÀÝ)™>A¸Ú†HBp^ÿ{Ãw(uãœ1vhMd‘½õ¬´{ ¾!Ù¦{ßÚšùÍu;¦áò€- +Êk'!ºÃèÏGúüûÎj²:±?šð°˜ØT­þnÑ2VÜZŒÃM冿Eí%cÅ6/õ{vAÂeF´° ‘á™ó¸2Õû_ïrXÑã6-Üõ† °sÝ®k¯q™A)Ùj¸w ym#[Z…IkÝ#•kVå,3J°F£þ+þ‚”ç' ÞÀz‡Õãš‚‚3H™A{5NtÛÖ»FÚÄfuÊ”>t‹òçäpŽ8Ž~9çâÍDŒÎȨ-öx½f X‡–<°i‚ÏS«š*øŽíGh#ú§ò9³Ý/²·HY"©n˜˜ÓÏ3dåcuýšK{Võ0z43Ãä~ÍHà|ågk;MK¸$‰|Çäù)408ö¾Îª:…­¥`Sâ‘Á +˜¶€)Ök^^«¶ôG’^’/k$k(§` ×Ívk¦VjìT‚¨{ÓkHñ_ï¿€VD} $€•«=Êv=ƒaßÛäN•à +açI+’®Ë«>þ(ú´@3ƵnôºèÏ·ªØzxà—¹îÁé ´.ZÑ´,²;v÷YàóeMpÔUÉ(ÐãüÕ*,N¨¢¼`cY¨WNE=þsFÜ.¶$^'jæe|qo;¯NÂÚ¨¥‡åI> ˆ&ê¶{Ú‚_ÌÊ!ò?½ž=,•’‹K2Ÿ$(–»>¢Rµ..Ƽ±è ?G³³vUÑ„!é·TöÃÓVëtL¶dfDÛ Ñ‘‹—-UDk +<8ZäÜ|T¿Æ>S ¥Ï™Ü×,¸þ(e´ìM ¡#ú®ib¢á½5ó6)!EgÂЈˆ&é%ž]¹âs"3op)Tüà[šÈjüÜ5öÜjyw­‰Å‡ÔÂæ4î(Ï@Ü·­ýùf[‚é‹â^œÇð1w¶í;û¼‘ﳈ¯!‡M®n„ü¬?ëÞT8%L,K¥DŽ+6Âçå}ÂÏ»ekÅ×èÃÐÚc'¤µ§!{ UFw8aPó[ Å@eû{ ZmÑDrèÏNÞì6ꔼ¡°ÔûÇç¡XS·bÍN°>&!\Ûw¯¯DS‘HÅ>å½  B×î3u¢!f¡ÎLœpìˆ!õú0¯ÈJz`ÝDq0ø +¤eåàÙÖÇîy= *§oÏF*ÆZ>v«é}«¹ /•^gŠ=z%`–Sm šrÛœ3dÈDâRv´í:É’’Ò÷ÎØÊÑÔ©öóNüAHº®”].°ÓàJIú.÷[)#ì ˜Ã㦠˜Õ¾˜1È8Ä¡”|Ü?gÍí Œ†£$0ª­D|]¢ ¢¼!!Yu™ÏŸwèS¦Äh’ÞŸ³VækÈV5ˆTVÞ÷Hÿô|¤°DÙ—‡+n*6Ú7؆~G÷¹_ÑÏý¼œ¾ÏxV„ê\ÿâ0ÐЮ¨»jJÝ1`i{êòµ¯_‹ëw ~˜ÔfwR%˜gV8Ü®U,ç€ÙÚi_3ÂÿË-Œ´ÐÁ‰§8Gc$mlÍhѬ©žËíž4®Ø“xÖg ½õäιð Sœq …£B¨#¦v›áv1*KkÈâhsAâDèr÷g c1xÍ4Èáïf0›ƒæût’¯}‚k»¿š±•Œ4{ÈgñŸ¥N`iÑ¥òŽÊÁ„Bv1¦k‹1 ž÷¼>>÷‘B[#Ï1tزÁE!mGéÜÊ;as@)v2‡à²ˆÅ¾~FxF‡Äg"IÛ(»tÁ•‰aÀ%sÛwkͽGM ·€‘ÊØ@M%bþŽ'o(yük´Ïó«]äAhj´¨ï·â5poÐ +€Ú^¾ŸAð|Š=ã½lûæ¶ñ’\h½~) *‹ÒiÃíÆ7þƒ9ð“Ûÿ8Q8Ô¨]'Ï~Ô,qÕËþ‰f’ÂE»ÆßU6½)\Eñ¦)ëBÃyLCÖ@h`®å–¶¸<ùRå{ÖÞÌpD ´":…"FÞ:½ê6®M³³¾ª¾íÞ°?KÜ„æ„Š&bȨh"‹bê›åR[:ù>85¬ölØã„UI·gÞié»­´UŽ¶ùÇàßÅ©Ö> çÝÔDRD[Ê+¶ Ú£jø  R¿ÀuŠuC¦G~ê^5DDvû?bÛ>ÄNtNn›gÿZ…­Ðñ×1þã8íŠn-uãdYº¼ñ†ßÊøbÆû.¥¯÷Yè(¢È+}EÀÒŒKêVHÑ9ƒ÷h¥”eÄaiTFCImKÅ +”­±·èê®hsåý\÷2lÈa%Ucƒ@yQ®”Cµ É›™VxÐ/zz†2ì?€1EPサ@õ’IƒZwõŠæ³ Ùµ×±÷$ÁÚ¿›,ëoA¡A›mQ©‹<ö¬ÞL”õµ©fƒ ߸Ž†Í|2hl8µ ìõ1!êª)ÒäµÎd{©ïGÑGŠ;‘ŒŒ-ù¨tÀ>gÐÃìãèè¼ Î n+!E¨F’‰g;ò(Ùôþ)ö·’4ꛘÿã†%@#QÖ’úPÛ&ThA6rÌõUìº`©ñý*á ¤|ǧßð=‘!‡Z#Ï#ÁBW]V#N„ÂòÆp¶Ý¶Ðqh#ecsÙ¢ Z…ˆ#¾PC¤–Âr]^xh[B>Öu7=RØÊ)ãù ýêni"~ÊÔ[Æv=;¶à&óæ¯ÅÉÜ@/ØPÆ+ñxË‚`L»¼ÁMQ‰É§Ëg™aÍ*%ž± pv!m¿Þ¥í)-šQ1×ÇÔÀÛèéŒaöe–„*þŸ¯óÔ;$ò×ú%)ÐÊ”â÷ÚqrµxŽ3ý4¹`@Æß!¨s%m£yg‹ú6<×(©®Eè³ÄhØ­­maá^çŽ>Á&¹i +8Ø—ª+8 èxã±Ü@ ¯0wܱ çJDhü¯7MýõËR¤kìnt²/ŠÎ%¡8«&^8¹ŒCGï +˜¶¢§»öfäb¦2^¤ßèRJvèù^’—õ ¬ßÐBs5zkÜŠ;.Ýú#¢‡ûèÓf ¬ô1º³¨ûu"¡Íw(¸30í0¢/WbàöÙ¢÷w:{òØ|ë+lqöð¦ =ã8Ö)°]Ôj×Sù1V‹<QA3é*a¬€Œ¶ç9*ãÂŒpüR[mGÎzȬ[ÜàÇ?nÚ²L×^Ñ›@Ž·ÞƦ¼¬’€íg輧}—à>Ø jê÷ÛÚÂE¹] •+ ×[W÷ÿ³ö6»²,ÉyåèÎPÒ äÿî1쾺ѓG¢È4U„Ä&Ðo/_ËÜsŸ›¹‹\HÖÙa‘áánnöýÐF¡búéRðûj½sp9ݿʶáÌÜȘ¢¿ØÍDÏ +¸I«B3dЇ-MáV4`BM1cTÐ.ýdQ§{Y•„ÇÏ+Ìáè´ÎBÚèz,v˜:Àlidù›é,°®áÌ4/›±±¶yPŸuu£mÙ¢ò7ï0”jˆè5‘)Ë4çÊÉQ5öç¹cî8Œ’yÄÖLéj"ÊcDµAñ7ê‚}ç+kžASa%¾­¾ÎÈÖŒ»f +þŽ06UÓ;3ø^ªíÉ<¢üÐd¤r¯Žg7`gˆž”’QC¨TGQ$ç¬)™±¬jÍÚ~Š_,SÐë~TuøÞ÷¹í#*Éþá°€Œ<Úž‘[²6V(HwZúYÌÈž^Äz0»@}v8EM²ýtZÑÿTÜ•–E ºÄ •r91éÐ#%…ƹ¢®Äa4ð;´ +¢ ^²¨p“ÿžð°û@)¿CÅÆM +ø½ŒCô/~‚ý²…÷[ŽØb= ò^O›¯ë}égœÚp?>ŽnÖ ·Ü?î£0€£“pØüEÔÿ$ÑYÒÔÑ€Þ“(‚².† ?Qoï~“”ûÞóľ‚Fü,>±ý*y`ÝÞp†ÄÆxÅ óÎJöºžP ò5Ö[±‘ð¿³YÚÒ,Ë«Lí˜-AÆ(’^ù÷ªŠîUÅ3±k9Z#ŠÄOe)#/Z„Ç/èÙ ›©q†®ÓzXß·ëíï™ Ò€ƒ\_Nù“µBº¨õ(jÁñ¯MoÈ·k‡|ù’çäo-ë‰mùº£†æ-(@¨¶ÑXníKâ à ¬íu^Ûtœµ,ºsÌÚacw\¦‘,ØÿB$´'øG”vë#’-bz:UœÒ,œý©Lp\šÔL?Å¡©5/éÐ*yïÇÎîÎwQQ¬ñEö?÷ñøoJVü›²y+¡v>2çR ¥Q”ö|ÕêЪ†×•²ÛxР0“Ÿ‘zbÕ™Tk ©+ð¾IÚ?¼`ä,â`hêR0Ù¯LPðÅÂrKE„ñÄÂ>QËß;õ¦È4ºší ÚÒê÷ö}Z"W@ØÂ’Ö·Wz¤›G9TËkjp‚7òj¾)z)QAQøw„Êd àAɽJ0ZŠ Ž h‰?{ÙeÑje|q\½ ­_f¡/€’·5(zË5 P¤ÆAƒ +GÃK§¦cFg qø·Ì¿xé2Æ–d©(\¿ØÃmK5äñ«]L/‚ÒÁ²ÜpcÖzÆ—òdâ•m=PCMÞ~üþY€˜¦1u½TKz.ÌDæ÷~b¡ —´=n—š;ªÏÙg€e³|ÿÆÚDŽ%á5e}IÜ7å룠™ f·ÙÏ,L qã•bÔ©+zø(Ð{€òÍ̽4ôró‰üd°;.×!çò aYq#P[âþ¸êë øÊGO°kET°/<Œlœ‚Â.Ôcwž¸_“–¿º©œ•D[Œ`Qi ¡ý|Ü(|í—Ò2E<¬·4H#kIG´öüW9§ã¥´ôZ°¿¿xOàÄ—h7Ìw÷WOšû¦ƒzÇPUÓ…ôëX‹É|Iækæ‰È O”>µYr·÷­„Z÷^õlhM8W[¡—+Ë®¨ñ\n&GiXÂÂåM7gú#êÌÌäj‘î¯Ñ €¿Áfi›Æ+ +Rë¬JuŸ‹©ÿ6~ù›Ä¦…Ò—+W‘€Ãœ­+vAüû%®¹‘’k‹ðDSÉSú¬ÿ.?ÔC¼R AQ„Ð{µpÒäQŒÐ&ÂOœiKêZÔDþÁþR÷hä{ýÁ­]ò†Ð2©×n-ó»C^ aÈဤY£x‘ðÌb€ué½AP©jŠ·PO»ÅO—Š‚‰ëMi°?[‘}ýxÂ/—ðÆ œB@¨ÞW³)öâ2hád°‘f¡§ztkD4Ç»ô¢Z}F} ðó¦™C@lrHR´>öƒ}RˆÈº°€¾L¨[ Â."‰E›Ö]µ,XŠŠ·×ÛOƒurGˆc,QªbçBÔ.0ôuSáq„ºòÑLÂÎÊhJ‡'Ҍȇ‡± ØfçñãIJÀ"÷7°-tp0 ÂÍrÒ S $C©b¾‰iüÔ½}}Ž„X€tÊžêÖ!Î|¥V‰mK¾43„ýù»øÃKeÚŸ\ +Ø¡5-ÚHíëüvÔ8¸ˆ¥YGÙÛÞçœÿëÜqF’=Dh DR@b¼þò¶âµÎAƲ<=äž±ÆÚt’ÂTlFa§çt[D¸Lå0/.A{ZaDÃL!¹zq]}š”>y¯Ë)ðW2âPl·È]y­¸ ú ’é4z07ÂW;"šD0O +ó/œ7øP:ú¶×bÙ31+¾Ä™qˆ39(ÀÒ.†‚ijM¤X+øvÈÀèi>‰Æ)žãÖRrçdKãç‚T­ÌãR&;ï0'pD ›5{6o±–ÄwaFèÞm̈¸Ûm¦KE;Â;£ÃÄ<åþM9"übà¾h¬NNÏY d™ÖªkËðA[Æê0Môr/Éç d$#³™ìæ(ËÕeE!ã3õHF'™n1?²a¶pƒø~¸Õ´/!‰ìû› *äneèQ²¢NÓ§ýuÑ¢9vÒè­¦Õûõ•u4pª)/7ºµW£iÏúœ±…7] +ƒ+¶ü_lsãeá§F3H!÷¡9… +ÎÔ/%›·Ø¡þ äòµüE‡›(g»4âÁLDÛg´dJ§}\`D„ãpj¼ž¨.làV{\)À¦UÎ{¡ÀêHÝÓèTØÔ÷‘¼±”^$"ès‡D„_¶¯)üˆRÁ™qq% +Nè¹*­SQƒ>¬~Åbè}ï«BéDO!p¬;[gûöÕ˜‰Äª8täQlMá¬5Þ x¡Ò펀 Ü^Eå¹Wa‚iî|Ϋ øž=E׳LN„)gF)#Tz‹Hv5õ äOkk¬„MÒǘ10,_®É踊ŽX Š~”--ùCn›âÝ^7$5JÓ¸“£î¯Ð?H°V‹Œ _…,þy ¿‚-ú³ü;PÁ.´ätA " áâFtQ™åw`0ÿ´a/1øvÝÎ#ŧ»-‡ŒÄ€öà·RgâôKÕ¬L„íÎ8\i?·ÎknxF„nmu'-é'àšJJ`\z\©Ð¡'™ê¥±‚¹R='aýxž^„Þ¡¾TÌÍ÷ýÎ)dþBõpÐÊÉ>ø”¸”²X~.å +,Ym&Ãè%«iÅêg¹JIEûÃÐ#ðíõjLuºZ〄>ÿž¾|@?Öc\D?eV„ðyãÑ¢KP]¹j¸Ì`vŸ×Ÿ +àXlGkV™¾w£VóqàWOÿý` +CR +ŠNÓ^ŸE}©¢Â¾é50rúÅØ8#É|½Ä™…=ÅÛzîÇþMŒ +q¼ýÿâ§Fp¦àŸ‚¬OáÖ8ÍCÝ¥88Ä¡øwþ*ì¨Ū;¸ÍžÌY Dÿ<_aÏã~h•pðÂÒþ]×µ5‚¡Vt~ZI` !³ämvk™ïÃvïHјn3¿Ä.ùPdšö¨?}*¥³OGÄ’öz>Õ8šÊKîÏþ¡OÈbiGÅz•.¾™¨.±A•Xõ{`#é¿7¤­¸WÌ…Žb¸bŽKfúWWÞ5Ãø+]î·%?§<¸§6ñàW?¸ÈS™¡qÇ^ ºïà²[Toû›nÖ”KèX‘›6Ðt…øJât×N™j +¦_ÀÅ<€) ŸjÞ‰ƒcãtJŸÁòHžN‘Ë{ºhÆc¡FYö‰¢@>ZÏ,ÜrŽ£€¨€jÅú.«—,4ÛßÃÒ,Ïý2@-Qö£¢¯¶¬2 rÀè}ã®jRRó—s…¼(7Oá¢4$¢*›÷ï 6BÖ–2À<¦›r0£´ù ™m“ÔêW©Ê«M¤`EzÓlµ“3ûø‘yÑ!ÍÌ?tYQ†ü TÿNÑ¡¢]æΞ{¸?^‰ÍÄ 7¶ÐûBáêVÆÅEd\ýBŒ,$¼h(ËX»£±ó`A™PXW¡ Q*üåB'¬ZØ8bZø¾YÝëA©Âµ € Ipz§Yý1/øAŸ–·‡ùŸ³Ø› §HMÞuVd ŠúìØŸ,/ø¾*†n„‹¼ù0m\Áœ‡|i´#ˆÕû•=k¢hÀx ²~uOÔ˜µTzWå,á)–p/ÐÏ›ÀÊrpY´øFH ­pò®'šLûk¾)Ι˜çXǶ³Fa¤çëÈA¸»óÓƒí]?Õ.—¤…•D™/RžåvÚÀ¶ž›N"–Âf`¨ÏZÐ]™ ‰*ŠŠÙcðŠŠÑ×r×ÿ 0n…ñ±›/9pðAÃéØ^³Œô²gèö_v7Ï{\Šèv{E}žã»¬šn9jº³¼týMõÓÅ{Q^)BA¨ìP»%5Å )G_>_~ÓUÚ¿æ ŒhY•Vƒ;6âÀ8Âä‡ð}þ¤ï xYÑ^ònÄ÷Z²&ÓÜcÜìZ‡nµìsà¤rB[èãÕâÁžî!„ꑤºÂlìØ1­ç›ˆF¥Ùµ³^‹’o‚ÒQöUà(îShkëLÒ}¢晄iúÛóàæ<óà¯.…]£•_Õ½£uòéŠäb³,pPåµ²B&PKj>«ù!1…K-›QíŸò¾4Þµ ÉšFôlV;–ñ”Bÿßûì´Txî1ÂçuçE­SLj§ ÛÕýOz#š¢Æ+‘©P ~ÙÒÔµ¡>Lº+Vü«>hjã*ÐosmÚ¹Rƒ^Ê@ƒÙýé@ÿÃQÙÞ_íè̤á®UnÍ ÀlÄ|*攉ÖÓç#[Ýúͧðÿ<»€}Ö*:¼Â¶/—ƒý¾ÄB9\â³ ¯òáN'ªFí2é‚kÒ‚C•w´Y@÷èfûE IÙؘa{ ADcùæ8¹²h6hW^?ü”1-ôY%”ƒý®ã6ÚTB¶L]€›=ó/–ü4Dó¾ðžÖ~w0Mž³h¶‹føÆöã®Î'žWÿÙÙƒmPÂ:mõ‚fŠFÃN5P¹…ò‡JŸéu endstream endobj 23 0 obj <>stream +²å¸97—(ÅO5¿4_ë5Kšöv”ª—½oO†j„½¸yn\‡- ¯±ÙMdfRbC\®cÎå8ë~œ^>özäŠáaAßYùd£ðu ög/<¯ãuŒ9›mj6ýî®{Û·+ íB=â%ûÆ®’] k¾¯sUGÊºÞ +:¬B ˜Ð&‰D?,ÈÒ­B ÛÙG‰ûñK]‰6¹û +oÕÑ®¿,PêʼrH°SÀ ðú´ó7™ÉH 7Â%Ö©€h©ºpw²|" ¹;ÅÝ? —»ƒÕ/48hÂé'ÌÕ‡LôÄNa‹ôRùÐó§xª’üUáØÃhoìñ©&5!k0¬óh~â€ýnÔxxD\çÒì=ص)²K8‹„×}–½L„½®v&xêfˆXº·,Œu÷6M±yüB­L=Öe¬l³»RóÛddZš¤ê/‰ˆOÉJ&8ñçeJ3‘ KªùœºF²ÒÕw¿Ò°ŽðÞ9+> …{°®äOéu±ÇVΟÈtæzk“o×x26.¶‘`0O’9À“PÜ>=!öÔ)E¾¾4´“}z¥(CòúTØø@:t”yšÕo¿¦_¯‡úó±zYqIŒ`i9-ÅÎE0÷hë£'Ø×ýÓ áÞ¥¢8ªõc«º0T– +µb7û+á‚ø“ËòŽÞ?2Žc;è9ÿM·Êñÿ„ï"ža÷›8CŽŠJ6ß~ZÕÏì½SØ)zèÈïͺ/ÚòåÝŒ#\ûŒˆÛv2‹£H +¾ŽÂ/.¨Êt± PIžÏÇ=ïûAñiœ¯ÕâkPcÑž‰ÒG Á•—ã6 +‹Ò÷ÂEãu0:ƒMFRûø-€ñÔCeJþxd?³ß#­7Ñ“£"¶t³*!Ñ+¨@'Œ¤™3ßËb×1ýB2;y|¨©Ya‡sIŠÃwËTëtúœýgö†¯³õã-À: Ïmè:…dìEYɾ¢o((pÿ]ç”°Ä{2'ú!× ŽÜ°½—Bx„–² ¬2Žº ™} RãDÖ§_APçæÅN¸[4ðaN|6ƒÑd·Å0ŽÚ¾t{o†ÇŸ·ß½NŸš&~(¦90B*ÞB¨Vý<п!€„ïkHeÉ{‘Öƒ°ù^^©>‡döø ËÏx…©e7 Õ®2^ÀjS9ŽÚ:…„L01Lé[Óm7áÜšƒvD.Žá~$ÍëÔÞ`¶ß o€¹S½èluDëT|)¤hä0žð›”Ïo%¨t¯_¢ÖïWï•‹xc3¯k”Ä0Ä|?ZˆÏŠå†â÷ €“àèBöª –y,!¹nîßãMŽ16OK?Ð!Ä„üoεž0Ι䩶J”–x´,¼N8ÿæ._Êó7½euëËG +Ñ}?É/Ÿq^9ðSˆlXM¯”zro'—Û®ÝGöãÀ  ßi×Ï¿Üï\À³ðauÉT”¿A4y¾lf&Ïú;ÚJ +Ek©Â¤Ø)q ÓEëᣑúênžO ùBo`ñ•ç¬öAh³K"  åKÙ¸õ +µz»às÷Žø׿O£ÔÍÈù#sYôûóú·ªK—Ñ'—sw^™”Ñž—†Š©®ÈL¹¨nhÞÒá}©- +Íõvß7nÈNø‘S+ÇwŽÝ)(ŒÄ’¢Éò^2z@h¿ïÿžMxÙC‘AôWž,a„Ô¦ù_þžjêûï~ü×ÿû/_,ÿøÿûŸÿüÿþø·ÿÛþ?ÿîŸÿùþÇÿãþŸü/÷ßþûùó?ýÿüóÿóÇÿø÷ÿíŸÿÓÿøóÿ÷OqÑï?ñýÃ?ýÃßýó?üý÷%~õåž×7ø :ýüߣ%Ýû'…Ì™Œ=¶p-ܳé›BV»qÝ}6ÿ×{§¹š°æ)Ñmg™•¿Ô¯áŸÁ§`Ü"±¬½àú–^ÕXFÑñ¯…D4 ~ã{P"¡¡1uu\$ßQDðÝô}¤ VTÛ'_oGDWO˜Ÿ¾_ºb’{^d¿Ú'=ƒGvoec¸Ó–“= Ô¡¦w¤aÑwÍút¯±òýÕ¤Ξ±TŠÕ±Õ¢Ã$A”W ix¤[ÕbÕ"@n¼¢Ë%pD)>§‡ADyÎéÇŸšLòÈÓ&hñ¡ý1DG‰B¿#œÊšŽ'SH(³cDÀk$bd¡• ýâÙBá}߆ÓLÀ…¬ì8é¸?Þ»T[kpÛi¾#kÚ«ââ±#•ˆÊ:-·=Q<ôiœ8€ ¶Šh®%«d`µ4=göTBî´ŸtUŒì+ÝT$‡_¥L¼Çv͇MMÉe$z…ÈI’ }Þ†…ßXaDOc,Äo’A9¸…·Îž·öØÌÅ`ˆ ïMY-=Úãû•˜é ˆˆÚ›ÊBólDá¯óŒs`¡óÄÇ¡à€pà4@ˆT#Ä^î'%µô^ÂQªï%Ž+›éw}xPÜ@'`P›÷ÀŠÙSŒ8û»Díä•]@ª¦WÄyÁ¤dZ+‰ÀÎfTûtlš÷e4³‹)YU ³]£ÜÚ›oáÞN6V×g¨àÐÏB$¡ŒÊ¢NUp'Zñ|F~? óp^äo¢ZØ¢’eÚ©à~¨ùŽø2Âu9ï÷š‚E…Màsy;?€fbùæShÛ0Úö\]ót¯B‡aÑ¿CÉ7<¡µÎæyª¸¨Ò½CDq›¢êK”¨ˆ,=¬ÆyÀOv¤¢º¶ÂX(/¿KŠBÇLTvD]äC”„$êJ‰ ßjÆnt§´PËqÝ6ùXÓã½bgÎÔÇM!…¼ü´Ûç¾ì|È ¨S’¯(ËSŽ¤@ô¤Zž¦¸…åÀªÞ9êÓ¤ n_¿‰èჼGr¹RäŸQèŒáz 0ŸâOhšý?¨™4yM«Š'4†Ï~ƒ-o1ÆJØÓî‡0éš|œÅlŠ‘õ„YZ£g(‡¯ß Ý{0ÁÚã½¹$<à¨;¡ì05Æü„‚/$:¢¨Ç"Q{¹8.«•Îp‹5G<%‹ƒ:ÓÈ;j +ù0S­ròXo€Ý©ö‚pHŽªÉfÇ¢¨ÇÇ)ßeZ+F¨ZE ³’Úôb[DÐ(5›""Øž»9*´£2Ò6%òF­Ì<¹2fÑ…ég3b¬n¦DìÝa|—½m²š5ë!T8€ò4Pj±Šé³Z~Žú{|%ʱ˜ãk†K;Nc{…õ9’Âëï^i ždCLSý›ó4exöÚüp§wBCîÏ:º³YJ^:š¥$3ÜBĨZÄÅLÚÃf;8ï‘í ¾N¿Uä \93&¦ 2&t†~;óº)xPã͇Föˆçë€ýúíWŸšs‹L …&™éšÊšSÆa¯À‹w£ŠÁ œ{uAŒ’·g¨y ou²õ¨;ag1òØþ?Û VñXGµ}oX„±,mc +WGDDÆDyþ%E@ôzÖ)ݵ5ŽDQÅ2J)ص²¢ÊØÈu®”QS‰š€Dì—ˈý.Ž%¡Æ»¢Uïþ.épîiÁyZ\¥÷ἂœ¤þ¤¤Lúc"$Å*Â* ·4ÝFÀN S f)µ3ó„`3ø.qÞt…:w`.¤<Úïñ¾¡ ÐÂîf6Ui\•uø-I4ÍŽP¡«•Í¬j½»N6öϾê^¥—úõ9úãeOš/1r¤ðÃŒ`ı3#x ¨½p¯œ°PVM÷BMý¾•9lLWdõK½m¨ÿà(7‘‘ä ¾Éçþ[¯È/ç•jÈ4°ÝZ|¡ 0õ " ‡ó'ª%P(UCû*IÖ7çáÒLô³_Åü ËpÀPé¤[rx?ÖÕƒñ>?¡ƒ§Lh#H\ZS9z=•zRjîsÞÓ"’Ba$¼Ñ]EØt÷bfPÜ—çPØvë(WñîaS3 (˜°óºÐZ¿¡ƒÅ¾}¾è °Lé|*ˆQ,Bñ ñÿ'"(hÁskJçR›Ø)&&^©Á‹Û³/ÐÞ°ã©œÇõÇUˆUÞ<1$¶Ã3<‘ÅœÓ!à#¢èžÁƒÄ²û^é#ê …2ºÀó<DG¸5’3‹ì¨¬ž:ziVAvÚ†®0blå3"ª)4¤Ôp õܾ9*‹=áJùB»ö‚ü×g€N_ȶ¿=]Õo¢Ð‡# åÄê¾¢«¤ä쇺o%Âê¨>4Ð ØΚ½ûFÞ–Oµ4K· œ2‘鎃=hÉh‹:-áq£òûH©öž‡j”¶UðöétTÖcÑl(î_±Ø^VöÌýe¿¶µÛk +„MY¯Š§ðfA/ÑÿÈQccJÓß:ô£Ý“Ì‘E'ŠâÅŽBÕ$„Õ\™ž`TâÈ“÷R±–ÑâpÒýpï"`¾4¬V¨”QTÒÍ¡]sÙáûpp‚Ùk,Q©—ˆ +Ká²3¼½$bgGûÙ.Ž‚q¦º²´°'Brm>Èc"ÆÇ‹m=<–ÑÂý¼ðÈúŠ©V#ØŽä8Gr¶í¡ÓO…pÖûS€µ®*|8ùÎÆL•w]oñ> bÂÑ!+GìO§ßLˆîn¤kc°ÀîVIGvùpÍå[‚¾'uê4‘XøÚbšXb“‰Ñ¾â1;öñ‰[=(pib=ÿ£«è³§É½Úë M„<ŸqíLÊìJwШéú ++Îè=Ý=0>¦ìeŸ¸P8§IYrGŒØ–¨¸Xƒæ¬¿¤å¹Ҹy–{l`ŽÀ'=?6Û5î_s's +í2Wò ßq>‘!ªÝ‚!æ•7튷%¾JRíȳhÞL{l>r"üŒ•™tÇì.™hpTÆžR×µ¥0/o Êܧ;©šY>ì ”Ó¹@yŒ`UŸ B$ÂÒ„·G MZX‚¬O]—pX5è$h)O—Õ +QO–¯§Á†œ[ÕF<±Ò2EÀœ€9Ž´-ª’kû×uR¾.ê5\ÔçŒOïg䧡¸bõø X©¿ƒÿ6 ²SñȽ†””9ì“ëÈú^+Þ8ð÷cZ¡èÛÌKôýl±·ÿ&BÉÆùz¿>³ÁÓzFÔ~ÐT&$Q) wª„0_éGÇÍWc'A"<Þ#¼Ž»#TËö··õyž¤$¦´£ _aÂC„€Ú[S#…_6àã3Vù®+ÞöÏ1â<û$ Oε!)ªR )KÛÙ"îw›ž‚}n VÎà'‘`‘¾’\±6ólóm>N¨/žýÒéªàÐÆÚV©“–ùMP¦Ec~ÿž>¿ׯ£ð2Þ/?²U½B7¨¨ï‚’M(u¤s€x (å‚Œû~ÜË í=pÞU¨÷“€Ôk`~jˆªßA©"¾ÆGEGƒ\Ÿ×Ãë#Š6ÃômJÁD¦9´ÿ7vk“Qm°1‘_º>#îPl¾ôKàÿ<fY;y Üåí(/Ø[ìñµ”øÜÙ Æ@©›ü[ÓÖTÞ‹uÙ<Ï™‚¿lTc°‚"æ]6—›‚{Ûrd°™ƒÙ’a«ùà¢D€—$C”וlyÒ­Û(ÛN«3Í" +t/ÜXšB†5\Bv„äD˜Ž¢SÏoŠ¤É>ÞýMX1ö÷€˜€ó %Á=ŸXùèôb`LÞL‡#‰ä¾{žÁ¶oÕy×MH~×a»R¿ù0y[}öB-Òs6ô´*h.}qŒŠöë}™¯ŸA_©ìŒ­¿‚iW)d–˜˜# ÙïÝðWâÉ´(f1¿÷"x*¸NÂP¯§ )Ä +‰fñE›+Ç‘×ý&bFïˆÑ¼ä#ª’hù¥$̉/ûÈy,Zý(í”ndÚìi»#ù\¸{¸V¶¯ï÷ÕŒÚ`Ú¼½Q®ÝSt¦\›ÐUæ«ôoþÌžj¿7î©Z½%Î÷(tê}& ñáâàÛ9UW¡q!Õ8ß¿t`O»–Z–¸Þ"âŽuâ ¸«Î[ç©Ó:“·^ 'ë'ãŽÕ'ö¬ PÆ +mtx ]‘ô!<%#’›}”Ÿkñãà#[~)õøÌ”ï̸5‡IÌwôTÓÔx-2Ì2¢0mqRŽ¹ŸêŠgA´‡ãø?x˜Ì’:É v Š…8ÔöJòŽxOËU—8ŒB³ÒÝ$HM!*ˆI¬ÿ{D,bËö™Ì~«¾=Ïsަ̌VÃ]{)i=µ1øŒXùÐXÕ6­¯ìõ-ªM¨ÚôW$«¢ÿ»Ø»‚E¡¼ÄÛ®۟´Vr¾úø£w€’÷ø›çr×åv37cOKÇ!ù:Pe =vDSÔO[ùêè?üû‘h¼ßÍÜëë¼±$vL]ÚRTßÊ$¿ˆóâÔÁ••/[3ÊCˆ£pwýnÞ„±Fôœúq6%I·$0ø’9ÿ#·Šý#8¶vsìœRTÑ{^*R±õù&³ª7íæ›g~Dåpƒé„NìÀh‰ÑÑÆ¡["¬û§à6–_^x§h à‚rõëSUµ \’Õ`¾–j¨(î ÜaáãyéVYf-h†vmT9ÎEb@JWvj+8„b˺šd½s1n5xok­} Ä¶§µ_’.^­ã3âO·¥FOsÂÉ›ÏgåuúÑ{ƒµ×—“Š}/ªêê…D|³6~检A{áÞ0Ÿ¥ç¾Ójl¿÷B 7UCÅÁÈlp´±>ßÃFKT´JЮ=ZŽVO€„DG¥0Þ`D÷ö\?¥Ö¦| µ„L÷_![ÜÄiÏà%ó£¡úJ]p±k5C–^¨,íåðTiè\ìYeÿ…JH*85{ŨJ` >ÛëÀ¿XDJ1‡³(šÚ÷S:0!0²ï¬{OHˆì‰pv¢G ¼ +[³,ZÌÕ­qEŒƒ*ºzP=ç°(‹ +eD°ý­wRï^oD‹ ¯F$-ÜuÞ+±u'jg ¬Æ%N«üç©žè•Ûe„#f¿kMo„íº%¹=ʳ¬4ÌÊ-ííyv9½FpD탨M%ˆ;áßp7Áûâ[Ž[.šWÔu`]dhµUƒè3 …€ßJ—-UÏÚ1ÒXˆko7éy¥Óe£Ìù­À›¯Q3ÿªÞ/ +š¬Ê;+tÇßɈ¨êú­ÕrS6ÂY‚º¿ÉŠùÎ ¶ €ä{~ë h•aÕK” +iƒ£Áï„­–é€î©Åãáœ$*ÆuÊÕk†Ò]ŸÈßã:0aúÒªnÉÖègX#Ï“ÝÏ`µkHsN_]©åT½í>Û©ûí§OÆË'*œÊ|LRÀˆª4vQ¾ºJ&VŠ¡¼ºlm, V ¡>*b²¶äï+Ì.\ÒGlÇ=ð°³Âè $'¾ÎhĈ6GA–PWä¬û± S‡Ní“Om§õ‡#Ý +ÔˆÚÙ¡áÐtUc[ø¨)%DÆY‘Õ€2êÞ4Å +…žvD3p“ÊÙe qG5ªóD ЂøD'ƒ¨éDdV#î•oZ§μÅæPW€ÊÛ7ÛëJÝv¬ù|ŸìÇÝÛ`€B~³o¦.ò~‚aò£Ý"°÷zýœwªÍÂ}ØW‘_®T<´DÏÙ™J?_ªZ¥W¬!M›=v¥iÄ£9': +§úD¨Ò8ÈF›ú³x¢ +‹½1.èuïDÛÊ­§€ÆìˆLsگ£У<¯%A;16ˆ¡@PŒ´(òÂ.˜ Â'L†_Â×>Э/r a%·[¯G`¢¸˜~‰Ž'ûNWá[;‹Ý«e¢ùÓløÉu/ló"kïA¹I`;‘’ÌñD!qÇá~(yßa¹T½Ÿï;Ï÷ÝùþDdç{qÊ´è‰íì‘ öÁqú3âOgÛeBí§ÿq`Q€ø&Ôz"dõ;¤†6ó Žl‰L@™ˆcDŒÇú€'À¥î<0åÚˆš–thÎSï-êáí»ˆ@³Á£yxðŸQtÈ÷ šB ÿgÏnŠ¬Û™}±TBþø'´;CKJ4rÕß3 +Ý\»+Ôj«7«ˆ7 +÷¿ÕˆÈ€ð¼=_¡é }%¨{Oˤå ø>‚œpħdQ·b‡¬¯ X=Ùò¥OMŠOóÈ3ÍB0÷Ú.8¢R¦Äíœn,“… Œ”8uª=Æ1…ûèSsúød'Ê_Õçh™%°‡9´R H›ø1Ÿ#`ßegSWYå3ªÈ÷.RdýšÙme¿¾%˜O€‰y*Ô{ùAˆpðƒö“‹ˆüÂÔQû‚cÅ€Yç ÜZˆ%îGŒdô'J,ðŽ@>€¨0zˆ‚ÜÅži½.40ëýž-hj`3²ðú¤T¡D6·ŽÈ,P¥@øDxïUcÆ*ÞJy†JÿEž"–%§” 4¥â=£ÐÐøÑÐÜå]ê¿Ö@:û›J¥õ…=ýõÁâUÈ7„½'M4úx¦•=Ê~¦³¦€7FÄÒ!ò3"îU?B2Ú«É·ç‡ô7‹*Ð|?UYó"yðÐ;»Øl ˜>×ÅW¿ÜÁ~ànª¤õϨ¿9_nÆŽZ‡\wZÁy¶×ÒQ‘µ÷â¼ÌؘÐÉÉû…E¡ê›ˆ¤½dB7ø*Aý* +=f»spŽIö¨5@Â;§•V™X Ø;"ÞÑ˘Uq÷gñ(™Òµýæ<ë°G'ü\!jMèp¥‚ðñ¾0GVû…ŠÌ†¥ Éa³¤M“Ã)«IIŤWìö¼(€3=2Œ"—­-™4-Út¶‘FEñÌBEq2€‡D¡MªBj}Þ¤•j +†;ô +tìôãHU1…œÔôÙq \ú@*ÂFD@ ð>?Š³¨£@a ©ÿž™ÿötµ‘b_hÄuê‰ôÓ`?ÒͳÒ!{‘×¹„2ÌN È@p£ñÚIµò÷¿G¥EAC[àDŸ>6iäÓ¤+fó:H¢½ÉG>#Xyu©Û§?dûÏ +ö÷WU–Ù'ÓÆ"±ÁWŠ¼tj8biÄ$«ž–@×\`¢Ì#€ÎKJ!Ð}þêN]‡?nCZÑX@ÙÀåo#þ.6P¢ÿO¬wYè¿¢›+4Ë둹Èö{çåÇd§ÓA!|˜%{Î0%¢ºïŠóPÚ§B¹†O(ÏŽoœÜºl`Á º#êÝ7<óÀ[Hé`p‹ ô¿À0ÐGwç8 77ì3„Ü}ò¡–£Å EµYÇX¬:Ç…x@// R»fF9£íMã^ô»§“L)ˆ€ùév%H" +)ûy®„6glhÓוêc3½È¤B¹q\pˆ¨']ˇ:ýbˆ5}Z™öO¡Ös.€žB-{4¥´ó>ìц<9IcƒÀ©“îµÀã/‹¶°² a²”VׇvŒAÃìÅB`ÅþüYÿÙn“·Œ¬„£=ˆ(º£<¸ç?P‡R?¸kG¶‹Z“Á¨²l5Ä%OŸ –‡ˆ «'¢Ýô¼×Áí?Æ_…í¯ˆ"ÜÀP¦rÇl2©&…¦û6ãGÆ´=˜–ÜSt¡Â~œr÷Û#!| v;¨§R€åÀþÅ|i$Y¿úÇr¿9Hg#‹\Š¯eÉ ,?“AD°¬MTŒ¦G’[ú‰KÌnf%î6¶eÓº]mÇõ/ƒFBÎ xšàX¦8ªN¤@¦LŸ½ë þí~¥æÝ«Ó'‡öJŸœëîÅ26h çJK°e}BÂõ[ñD•€„œ£x§´ï ËvÉ€2XŠ¹T†+Hoºxi¿Åö½'n¯”ÑÐUÌÒ›í`K‡b?„ (td{è ’y¶÷7 +ÍZLzù™…ùe:l½ÓÐåFØ*Éhß¿NÔþxˆ»_¤¤iÒª Ny»x­¦t“Eñüà”ŠHB]×ÁÒdŸ@ñxLñP ¶U²_¾—A"Û®Ð}¥ŠÙ¥!ÉÆèÀãíÙÙbE„œLU¹&¢¼tÀmq +ÊÜDŸI*Âz]èqæj{ûózÒŒ ²#ªP†ýE;Kk¨óäMö“·‰œ@p)éÆ]´¶Zá–l÷«Å;ªƒ8ê©Y«ÅŽÜú¹ˆï.]V[ðŠ÷ïA×AÞã=¿CM«‚Bû»¦Emêd3ÅzF¡€UÕ‹9«è€¤¨Wâ¡q%VË`(®¾ol×Y˜ÊÉèµìsàn"Ùgù [¡·ò´F8·%3d1+(ÇûNI×ön˜ÝH‡hJÉ¡^(Š!\±l>ó.ÊÉtÈ÷Ô——"¸kÒ˜£}TLËA€É"˜™çDînŸ§Û]öø‰’­²è0ˆ’:AÔ8œ¾q`(iI‰ˆÂZˆÕ:!V(Ç…ü©;¾_awèAd%Ãs¯ƒ(Cógxž€›,esN…j~¢2Å­'‚ÇÍHeb!®ߎ ¸/9úÃ>+Qé¼Ety?j +*½ÉOqÂóËJDý ·˜É&÷·\é¸ÿíqóÀ"pBxr®8œèJN5û‹½9Qo„Í MM”\§Í0ÜÎr¯KB˜á–SdDídDÆDuŠuúdL®«äTT½<‹* lýWìQþõìí&‚¿‡U|ÐÞµšMW2òV¸,gàçâ6Ö¡›d¬‡»ÓE~ˆ ¡ƒÿŽ|4Rer<ÐõV~FÐ&À,ç‹Ê¤ˆ(þ³Rô(qÊúæk”¡ÔÌs@§³ééS˜ +-MÛg$Ö¬p-÷’÷J±îÇ[#ŸFdCaž+*—ùj„Ù&·4®$Ô‚ —šŽ@gìD¥AkapXN’ÀÕµ`¨QáLT=™)H9s 7t¬:w©œðK临ŒAàMÜRtÐñº[²®`>Ú®9’iq$Œ¯'hSKò#Y,ˤµ +¹ÝóÉÉ +Ìl;dú-‚XùIxÚñ¾>Œ`$BÃE"|]Òál¼®´7pFQo Ší§Ã¸õ¸ÒùžäÀ•™¿dg”ÁñÊmà3é÷Ê_ëDÚò`ûC†1@-¡iË€J~S6™M–ç¡¡é^¯ÒÄ+鉜–d~·sÅL`©³Àî H³•4€/ŠÏ$ïÓö ›ƒF&;¬• Rnè’ÖÚl:yž=ˆgÞÿUùãþ©£½xø;iRñ䉢¿iîƒò +–|—€Ýîñ>g !Õz£f® 1ô'2ÊGGVÖ²ÇÄÌ]Yº<|8f²õ* ‘” ±£ô~ËÙNú0¨j=èî\ú?‰òßR»‰„ã.Û¾ÈûëÑØ„FÒ „‡‚œ¬spŽã1‚¿ÃÄËêDZ|f‡þý–LšÔ€Ä]„iòžåˆXÇeì[àƒŸŸ9"Ї%‚å?΋Á)üs‰N墖«rþhTØž¢s`W•DÚΣ=rVþÒÅá7sšßsSÃÞž —é"t˜ŸSGRhìŒÅ +'³ŽJB+šmè†mL +ëÐS­x?¢~YF?ûp¤wTaⱞ¤ Rl8—P”ÓKdrGáɆµšJD1@ž»"– ýXyù®©È1O-ŠˆØC’'”O…I¹B"óvDT å º_Ûƒ/Å•Hª‰`$ÆwáUÜß8….çcQÕ±aæÇK‰–άRqo²ze®ØÐqrZÁ‹ÓoßùIÓŽ}ˆœ¯l¤^RÛGÄGj§ƒjÆéØ¢.·7¼T^†Z«òÈøÁšW͇:|sRÏÐW%ˆ³ƒM+À}BÂ"“Íø—Ú‘b_;Ê= QJ6Š¨¡š¤4ÅC“)êÁV6˜é‰8:‹:º<2"ÚKøu×dUâˆâm&êœf…÷\i6óÒ!”ð…3}"B;©`y”…vDÉåÒr¨{=AR 6m{UcãBŠmyi¿¬!p@ƒŠr=´(]´4H•P'xA%I±Ñ¸¼‡/ɾhõ‰c‡}oJ,dàË{?Zؤ‰ïó]¯vDK +p„ôžÆþ¼‘üz¶Î@4Ç&Üs(§û`ÐÕ¬ÌwUf&€$èð(u>ï#*« J‚PBPÅ‘“·ÚGílv]Ñé³3ßÓÎCŽ‡v* /D¯Ø†QŽ0Wôô–-@Öw ¨5¨,¶°^rÄeGXUb$ßmP +1¤•fsɨØQÞ±/Ò~èrÀˆ|†ÍÌãlN%òòÒE›Pœxžpô)äGŒó*‹tß]Å  H甪$Dhs›¸YÃZÓC XÆ&ÉNýI@YÅ)þ¤4ÅuÀ”ÚfP‘Aè4š®Œk„ú˜"”£ëUi) ©[M–‡½§Õ€E²úxPzôxeu‡ýƒ$Ëàúƒ@»yi àœ¨c«¥ç2”xiwƒ,l³® îÆãæ¥7A®ÁÄ9˜ø7 ñä§8f#Œ³V¹¢&IŸ”y³Fi" Æžd”ªžšµ Ì•©!ÍwŒ÷˜òG‹¯àþ d׸´š=jíL<=ç[S²Íä÷öŽégyˆ`“¥@—‚Ñ&J´¨ô¹q"££§¿ÂH¼>qtR'òl·£ÃRAêWíp…žé˜æÒ’ad±à?HÆå»ÓÚ%¯QŽ2ŽfD¹jÙ`a[˜{¡Ou€ê +­› +Õ„+¥q¨?Ó™ež/K;ÒèÓ>B´ÿût‚øþñôÊ&‚7­­"ˆâ~À n’NLŽû–M“¨’£3× Õ;Š{ì*唨õõ%( äÝõˆ|‚™Â|€Z¡ª>ØÖù²e" é Òå›òµõ ›ãZMODc=ìGuñ¨¦º?¡ŠEo Ð÷çl]!P4º£{Ž*/x ë¸/‘º÷]{Áå‚õ¼Ø}™f{Òd)PÛVü¹~eºêÑÝFYV–@+è‹‘8hlÁ{V0µ©7‘ßU&¾p1EëÔžªuØXçñLÀ£ƒ%¹ÝdÇG F&†8>jºFH«ŽžC˜žˆÐ·è‚rPÉëF”ʲ•˜˜—÷ÉÖ FöÎYæ}$ç'=dp²$€J[ ²4/@¾¼øîz;ô»}ënbžÆóaç"CdÏQ04ÓlÄ8œ¨¿«„u46¸ É™ÙàòŽÑ…«ˆ9¤(MÚ=›=pLÕ‚ö¹iªk"‡dÆ]x4(µøhAc7HåQsæÈ9º#ýË)ÊåõFjG^ogçTLY­¹•:æ¡æØ’•µýÑþ|Â궊y‡ ¹Ÿ†~áo£Xw|ísžHoD•gå{ʉ©²¿¦€a"¨U‘ž¯+i¾ò½fDŒR]¸…@‰¨ªUÒêïrX"EyF|YVä×…(ÛÓ&›ÂPÑ\q1åð­k ×@áàÌ_À¨)g<£O´ô')!-´9Ùç<£©j½Â¦©«é‹~‰ )Žò­—WÚ{€ +±ËE‘†þ„`š„.©E1ˆÌfø¦AÉÔÝ `aV‡Q#;†Š›¼#€¤À´Å0C(¡?š,›„üçë¼fyTP­bS—àÕ¸±YèÍ {… ¤L²²'2¨Àyœ Øh®3¯]±ç"™áô 4.34ù6BìÀ’?±à춗ûqàÅõÓ¼ÂG +-ù—½Ê5ÒÈA£ù¥Ž;φn†“ùâK'MQ¦ÎA÷ ZŒZáÍrì±Âé.Èud§3lÀ;Ó•F¦óG‡¤N–<«¦û…´D?oàlY¼yX~[V^—yW œëø6 +óâÛ°PdÞæt<Ø„[’ž´ Ó•ÃâM`›?n>Ž +?ó|Á·( &ð”M"V?µ^„àî…˜û×I5 +€¬ã ÁAà¤NAƒb"Ü΢*ÒÛ‰ðÏJ* vô§Æ +q¿€¦¸Àü5 ¬ñFäàÒˆì~ÑݶDó\]z(€Ü¼u  {ÉYi -)–¥àñ®QÆ€ýfžD)HC”š—Òã´‹Ìèö”¼Ó²eDô÷Š‡nR¼0‚p=ì‰ ¸¬@¢RˆógxŒ‚[TbáŒA#Þ BÄ,ñU(툄|®Ô§‰ˆÅ˜}žƒT<â÷”¾$¹3…dý–ëtXt`0¶©Ó7ªé ŸÁ›Š"ûY•Y + \)Ò üÐ\(ä%¥-hð"܈€Y8ˆ<Ø+±»ºcðK¨eá$mÏe¹Â¼˜fÁLmG*o¥[æÏZÛp¥àŠÖ½¹€L†ïåz ߺ)!û›¬O ˜âY&4R‡ç=饹Fz툗pøE«G÷xN-tHTÜóZ#XGt¤…ö}±¯_ÐèŒUóþ^WEaî òQ³@;àn§Ç<²ú¡70L¬À^pQÞŸoîˆvÃqÞ+´vÇ}4#ˆòš4÷x±w†ÂKÂæfª;dçd!.D‰R¾2ƒ*œ¨(IJΛŽŒèk¥ØǬêÂF´ =[<×½l.e`Œh'"Ÿ_º\Uk滳«€ªÌºU˜y°;¨´´#|F8ÙÓdÖ)FèÑdDda!YBDˆ‹>1ÿAët>s‡º<¸€X_›#ä¹@пj#vÖ`0“iõJ'Á’æŠâWY{£ ü}Q¢Âéb:-ž Þ"•Í)®"ù÷¯ŸéÜ–XÜ8´å©ç4]Ä-/äé/Šî‘“²ëGCùå¬T5h'“‡0b–ä}VËSÀOU9Å_ìòž^;%eÞEÀƒËŸ.ç“Å +ŒÂ„¡ÅÀ²¼±¬‰`JìÊ-z´@²‡©Ã%¸ÔE¡Ž“uw„Ì¥ÅÓªz*ðQ^Ùî– ] ZŒ‚Žu#¨ÒjD/]³ŽÙûƒ(D¬ò©WÖ\À2ž’7-)søµª/ÝòPÍŒâjˆêgq©¬8KÎÙœb3XMa}e— É}°úÚkœH3ýµÜTpÖ³q¦ %]¦cW¦9‹EªjõR‚xEûëtÁÄ´Yã¢æê`‘L—$E;oÊì¡*РĈ8_Éü¢:Y_ÅOÈar£HÈQ顤¦ + +€&;õ°F1£*Fìæz°—m!²r„R_éw³ Qá'Àd¤+,È”…‘¤XT=ó‘ûfîX±Œ£_1¥¢IÀ^S:¾äjƒ°;®{ T/ýV°²âcO‡±¤Oì2÷ö fØÐùTHöiöÔ,ølŸWåŒ%Ì~µ¿¾”o¾KøŒ+löк¶ ? €H.±4´Xûn*iì()á*3ÙƒH¼3–…"µð7ÍIJË-¼Ù*£¤˜Ý˜šaïT^PÀ:eŠÐ^!þ±Ø°àlÕ{¨*»EU±Û<ßßf;®{¢ô¢h¶<4%ÊRx%Î!µz•€ÍÖÂ¯Ñ +½·ÆØïX‹jŽ—¿ú@ÑäÌtTŒÚ/,Cmj‡fK8H†a¼bfŠ¶6Wz‚Š0t”™€fÉ]Dº_ +I ž%–Un +C^R-P•w+(¥Ç90ÞtV‘;`§é~!-·Èq+¨ûyAæ—°ì)™/¨Ö ­™ÚÇJRBÕ¨Q"]OKªª|ÄL×Sá›5çëJþ¦}%Dwµø¦Z;o“÷TµË¢L¶d•ÐÔƒ¶ÐVqåC>KRSÛÎ'(ö_΀ ÇþJ2×…i¿ôÂÌ­„pž†Y¨m©ÁYŠ¦¬2¦¨tÁÞm\*«_Xžè»b~Q™”¥¨„È·Ë8%0i²ì¾B„ƒž ê¼ÝÚ*²Y‹°R¢LG6½Ê ‘ÃÁ íT4˜„†Ç‡ëZÎ%ò2m%gJËŠ¬Æëw4%«¤`aû¬W\ðÐb/¨âçÍÛ-žìeyIûQÍÇyûPê}ej J‘Á/+ö§¦È&–›×+…_B +_ÆT^ab™Åy„¾ûÈ¡Ìhx@:ÛìéšDœo0öDP™%"]ýJ„4Rÿ¢GTˆñKÖ9¹S|O^6`3”‚Xñ6 îòš.ÙŽS)až75%鉪WŒŒi§†Ü‚¸>¥†ƒF6Ù[ mjJܹËÚÇ>CrFFÓaO:E ¯&, à,«°ù’Ø]ÉÒvL v&å$÷‡ xær‡r ý2,UÑUäíKS´?(ÖA¸Èp hRÚѧ0‡Éîð43¢˜áˆÚiÄßü›`À•8æ;XïäÚ°¡Ž¦'³#DÑi!3ŽôË÷Gk‚ýþ€;<â[þN³X-9àw´p*tä½Y1gX!?¯°Ÿm»&žf¤tÆêèúX’0ªíNe{õó +$È*mEGÖyŸW–Ý”µÅ1Xà#¥t»„êäx©È¯ðdó¨lr£µ¬®ä¾å5H²±ÛM˜1 SJ “4Ü–†ÏÙ¾ Omß+6Éag·0Ùj“jTG뤜$}í߶ß>©5ÖV£ß®-è¯{@Ú8Ñ ÍoïË7õ%)áŽÀX´ð@ï+%{_¼’ð5;BV娶Oþ·g7+Ñ~&Ëe~_Bc½"!Ç^àh«[dûöÄë>„=‚]̬Œ šÃ }¿¤Ù ÙÔÞMš¬¹,!²-‚W ê:DG¶Ü8µ.­Ÿ¡1]ƒ‡Ý÷LWD\DùÍëØseç­]:Ë> +šÇLöLÞÈKu«zü@<; RŽÑÞ¾Îá € ·rô<^v¦DDe,FOs”R©}œ'G{ ò <ñßDèýžxÀÿœêÿê}øÍDzÿùÎù]:_y”kžžQR±ŸCJ9,¥ó9' Ìy>ƒ$ë9!Õ’Œ ÎàW{¤”Ùüçì@Øž½[×"+»«˜-Š Ìêþ„uÌæCÕ…½éY!ED;ÑÓŒ—¹ˆ!¿¼/èKâÁ‡g˜(Ž=$Bž¼tzµÄ&’b´ïŒÐîQìáá9jœ#úÄ™™§½®d“aGåñb½)ÀCF>¥Í'–Eõ9–Ì•X\#”^›$e%dŽÃ³¢»êæcì8B«´…¹Sèrl@MåîÝM³£[fEA­''bpKDR<ë8úÀ$îžæ%SH(w…¥’JJÔÐ퉽{R!¡ „O³x–vviÑt>Wê]ûä=¹ ᦠ[§NP:wˆ¤ÔYf/eŽ’= vbìÚîb7agt ÂÁ–£9(Û—\¿“µµÅ·}ƒLð½C†XêQfÌD¨–²#``"ºÖ&“)n¼.PF\ ~ +ºHí!O’N "g&•‚ßEøä<òûf·+æ>X‡ýÝdv +Á‰­±…&7G—a¯îa°××ì™Xú•ÝÞ‚;öá•Vñò åûü|skŸ$Þ}È€…Æ€œò°‡pY2 +Ü˼Q a­olÈÈV³3ƒQAø(²]¡ö¯:Lªºö÷€§@RözÔé&3'ÜO¡¯®VJ;øSÜ­¬jíç„8%"د¾Q×´‰ƒˆo«Ó´ ¸^`j¸[%>Ά¢ã4½$`Z€²˜çŒmZ¬¶""ÀÅŠÁ×/°¾‡0Ì<_££x·£À¤ïóŒhžïààQd0ðÏFX¼í âBð¸Ðíáu +ºk;`Úy•ƒTˆ8Í©Av4K» (æÏ wgLJÔWôä3ª!yp^l]P²Ùt² ‹ºT¨Žùõ)HîJ±’ôÍ|"ê‰8<˜~¨v.ßl³÷÷R~ T«m¶f-£ñÝhýŒ*`õÐé$z?:þ­X¯tVžðÄ'®,¯¼OÓŠ”‡+v¢‰ +‹‘˜JsÓžå‰Z (*n@­w±Rƒ.0j’;ï×Z¶);ù£%12«)èéÑ©âQÅBòž®­ÜÕÞû6q%@j”­‡Y–î½{¾†âæÇYÍÁý§æ4&eÊØ„€´ŽŽ YZ:WIcœVÄž7»kMé“üMß)<‹khåWm A °ÑžGj·‡žÇgDÔ ƒ ÿùq®hÑ +Zuö¶N=|23µ°µXª$”I#aÃDS®. QªÂÐ#E¯(Š$žÇ®¿ ±uR)鳦?~;#ûW3Ï*Oæùëèõb¬Á3_hj;þª‹u A&ÉgM» {n8b»2žI 1„Åz°­‘ŸË"LtŒÎfBÝjyÔáp|x‡Ño§0‡e`r/ý[†þ(X±€ÓKZ ’nxÀ˜Áï—,©™Š3™E‹+iÚA¹o]´kÏqƒvö§˜žâãŠBéš{Ðy’RÛì,vRØQÇ]Ä®a7zÑA»ÜH溆0ÎÓ\Ƥ"# ØÔÖBMa„•P@½ :– ñÉ>ýØêŠÖáK™³Z[çM‚=(jTw5ê~ŸÇsüGÙþ.Sª¥7CÙÝ;…Z ¼Kõ€öLÈ«¿ýÒüò;¾¨RPÔÁ-ÚÑ É }É6I€|C­{ðSD–LP›b—šÖãFàµؤØ$wä*¾¾S?éªõ¢›è$¶/ìµ {D¿RJ]bœ( OÏ£>L½ªmÁB¯>`XhŒ·óQ@4Ÿ^ÜÀ‹‚ÂÐPy‚ ¯ï7j0 ­Û$†$0IQ\4ÎÚ¯šüÖ$¯í#"öQOtJG0Y?£Ø 5EŽ<ÂC#÷ ¯Y-iž6ZÑX8è{’«í’j¡M¹T6…ý…;l¨¥Á4—Ø$U½éÀŒ"ëd9 ÈU“ŽÄoåB¿ç æ ™ú >UÔ¡ª—‡‚ÃÕÇÀ‘µ9˜÷ØZ…žK£õ£âp£Kkµ»ÑehZ²–;a£:Ãûª‡‚ò¤#·ƒWN(“V×9ÛôÙ¯ï†pLdÂ^H}Û'5>ÌèêÑš+qŽU[}]ˆ<ͽÂj‘2ì½ÀàŽdkê~.Â<ðÙ|G(öÖ»zo?±Ûd/Q_Š  uR(`ÜB%ÎPÆ ¯·¯¦äÎŽ@Ö¶)š£`Tî´l'lÕ#× œÅ"ô÷L`çÍ ÿ…"%ÍPüä +éä +ÚPÀnpRIßF,»3Åírûk˜cí(DZ"J{˜!+$ÔȆýôÓ˜n±Ä¦|#̘’"GFØ¿ÈO`F_WBŠ¨v¾³Q­=‡kÝ•eêÁ²x„ù–“¿=G(Š¿Gg¿ +ˆ[öØ©9Šñ,fdhZèòlDí“¥Ø÷ )ä=Á«ÈEö@§rQóD=—cD½*(í Ç>Kæ"=BA-3šÔìãÏN pÔ%¼ÖýÎA ;¡æfIA%aAC(ðKð„{ÔÈÔo§¿g~9.¦Zù„—ˆÚ0b²“}Às¤–\ ÄÖà8ÐwÅõ]ØÞãÓgùŒðûx¾GáAI’‰31·ºÈc}HÙ"FDèy©Ú)Þ’@Vy9„Ò¤ÛF„´¹ÇšE ,#œïÝfˆ)w6·çeÄ-×(ÓÍåpC5¨­+ÊüÎ{=C¿Nò8·o"r®TIæïÂï£t™á "…¸'Ž½óÒV`ßäÂRêª iøœ.ÞGÄ[¡ˆÁÛõ]Ýì©–es6ÒÀ  ÅÊdµCö—ÏMŸ^ÉsTû¦\Óµ|py”`h;ªÃ®ªKOœÆ7]ۻ쟗­pLÍ•øH0ǥߞ»[zi…¦ˆ(‚¢¬#Ò:V¸ב¢Cj`DÄh¦ÌÍïÈé$Hû›”+h&¾-쀆ӹÎ硲JH>šd¬0ØB»§x‘Çy‘GýŒˆGÒÂÁ Œu¨+­ƒÉ$Š„}Ç&ë@?:¿(í%2ï>µzøˆ(Õ;suÀ>£!àѦL"¥ðèÕ‡F侑¡ dª"%Ò~™Ç±Žÿˆˆ{FÆÝ,~V%®?Ï“¢=Ã÷ð!—˜åùÉ$e¶ŸùAHe΋¼øuªS4¹ö½óZ^›|<×ÁöfzEëTsàm‘³2-†šÞ^´œ<ß#îû Ÿr z,í›ó M&»Ž÷ Æz¡¢àÂö]„û§.7«ä¯²êŒNÚa1£„øؤv2et9:„¦»æŠ{ÂãjÖ”ùÇ{Ïm¹7/ ¤ + šÏó\™OˆÑíæ[-E¾ÅZüÌ HA”8ˆr:ƒ€Š»Ê¼D¹_ªaeÉ¿'LÔÕt¦á@¼÷ÖJó²§Ó»–Œ’7S·n!wÓL”wZ~OœÇ|_(pó4¤­”Œ} ù'~AHG<Ç7 V³›ÁŒxEs #´ï³(‚Csj(DÊ2‘)t‚Ρ{NñMxì1PNÀ*¯,¥ªEÍÏ£^ñ¥®ƒ©eAò1+è­Lj´l]¿‰ÈO…¬}1ùŸQMïM¾“Ǥ…4 +i¡2öÕ‚”ôþ!¡ ‹Ÿ¨™ Yê¥ÃYbNç ¾qÅn¤?Š9ñ¨Ú°@@Ç ÔV®Ò ,¬ÇÝåÁ6ëåG×3¯¼O¤ÜÔu‡þ°ž«V#ÖñDJ<‘~T£Rà蜦¡{ ÎKMJìf3Jˆ»ï‘{ƒ½1Zº¡FoÁáV¼ˆP±­y·=Q +‡öUöÐ>bÓ3áƒOÿŠÍóÜ{äæÁf[òàO”j6¼ÆCxšÂž%Giýv|FGˆ + +Ûõ3½ÛK–×/gvcØc>aÀíIgº¾V„Î÷úº7vá$¥Ö~t3£ï­ÙÖ÷ˆ?Ý&žžšàªÔ5}?Ïè—°Ûªz¡)Mdm‰\ã=àÛåñ-ˆÕ¤j.ÜAJQV(a‰ËàIª,£°šƒ¼×wŽ&ìЊ)óÃ~9MŒÓZÐ&@nTöTÖ¢þ§ðÝ%5­§1C>P“Í8´&»1 šGC<ô¿X{›]é‘ì<÷ +Î=|Cù ÊŒ?9´ †!@†3±=j-àV r[€ïþÄó¼ÁÜU™[* ènt£js%™I#V¬õþ¤2Ó·ý‚z7¥v]×øƒ;ýA*úˆø§]Bræ~?84²¼pebÒÚª¿ì{PýÅN5lÊ{—7l¨`wúè&•Ç&i&¹"jŽ'ŠRN“£¼Ïc¹ì„¼#æŽ(sGÌDP›JiÕ÷Sv#a  +õæÍ|>Â|–¾¡º7µÛ‘š¿Ã#Eþºó+e &è5g¼Î/‚s®-û¼ó`n™¯W°°4Ü/ ¢ÇOC…µK‘rÃÖ“§>AžÛERì±·Æ5Üããð•6DŽ6­E×ÍI?çðg3x¬V¯ÁSc¡°{HPxÅ'µÓ5¤êv&*¤º­G,¡&rA •°%n;µMì1¹ç<‡h€´æ±W}WÜÓ"†ÇT#¬èR1d¾ P匜€­gc0X¿·…´Q–ª§€‹€-¹SÕ%ƒ©jW€w&=4$yKß¼>Úµ%ГÝmßzêÍŽôÓÖÁéK«XIEwÉT{ #ˆ„§uq6Àø>#p¶fsŒÐÍÓ¢1Ê+Uøj@Yx^Ã4§C)ºìšõü¨ýÜȵ‰VÀO]ÒÔ ETiù‰+æ5g #ò8Çõ½âîàen³’qÉiºÑqHAÏí¾\“x•U¥®unÜéúÚÇ#w_éò§U Ž¨:ã+ twõK؈ä<c‰@±Ó/»²Z‘mœBdEh_gn‹¾â7Ôδ"ž›Ç ˜¬äôÑDA…—2èˆP‰È®²6lüçã¤u‡™rÿ´„±ªôm3ð‹9€Š{KOpö¯P …SmJ;7 @ÛÜôò²ˆRn”–ÌFD¼"¢K¤?µÊ2BAôü¦Üì؉1óMÀ¬ãj=ס‚BŒ„h¢¦ìÕškÏuJôZNö®U- (ð·ö7±²5ä«n«–Uy=‰2ëöNZàÚ¾1ûÉ©ï1EP©nòÌîxÊ+CQQ‹"Âg`hQøÚòn "ÚÓ²ý8WË„4$ö£Ö•9¶Öõ9wÛf–6ÇóçÔògO€_HCßA£H'ö2S¢“ú Ú Å7|y?"Ôƒ™Ú"œ”ã= +ÁLŠMº&Þ~õu `ÊG´Ž°²‘ ý„N{œ9ð²¤g¶r¸Àü>æ7g(Àá=9“•5¼]_®”¢Ö¤Š”ÒךáÒ(:¶öMŽ ^˜1¶w÷kñô@L§ºÆg´ëŽ_ÜæâŸ'=Õ zΧIJ_"„óõQ*oš³•‰=Y‹Ì°N,ÙOé›4ÙVªŸ°!›èç }"B +Î="Z›ÜvsÇx}]WòŠÝ­_fsE þò. sÐ*GX©áÈCYçsm­Ñ÷ykÞ)ßǦqkßO€JCQálä?}DøGžãxêQ°ß.°êóí[ßõŽáƒ´W„‘¨âÐÚÑ˺Qü}ÁhØWFέÿþ%’J¾TêgÞê”®pö.LÊÔ†$@Š‹ªJ” êb­ïqªÌµy,(Gu‘ýØ)˜«ÖÑ¿)xDŒ+ÖC«Èr¢º«(T Á¥ÃšÈ¡bôŒ¢õ#0à2EØŸ®Ÿ°.›-Ì\O4dŽã‘W[¿è©™^Û +s¥Â‰¥Ž)*É•Ù(ØÁc"BHN#o«@O=$yéÛ·~ÿzÔNI á\Œ–kI5§ªAñŠ¿gª¸ö…cŒ1d·¨«JSqƒë™ Þ±ÃavÓ¡6C—º}ÚúœöI€”ö½Æ¢Šïƒ>êÇï¥5©1­«Ä”澈}î¡>rMÆ2tŠ!7ª[l`(YûE¤ÅÑŠ} RQÊûœú]P™o+âRNùóÅÈ;3ÔšyQ„·¨qmšûfaHn…óq>ès6`§YÞ9ÔÒ™S•—÷ˆ×‚=bPva>γÖ=E¦ ´ž-,:„㺠œ­„Üî•Xs»×CÛ@)jæ µ]©ù&’}HÎ[¢6AÚ>Ôõ)ö‰PãŒ7ˆå57•t_fZdiØiq÷ÊiBDèåB!ð»ÄF°^/T©3¿ÄvÇ°ÄÿX‡ûB“ §È«ñƒó€Î~$»Ûæ%þêcÕ[€ÂÉý ÒÌù´é˜W²»CdžQ7rË8ªÉ‘¢#\wm˜‡›X%ÛgD¶ ÛÁEÓù›ó@â¼U³¢3x”B×6ÂêÐxb¨þš +8Š¹é¤˜÷ jÆJ„l‡:¨3ð[àQè/c&8ï͸A¼žJMÛÁ'Gh\þ¦{ÈÚ&×ð¦U]¶Ö[sŒg[}¦£Ç/G³Ê(ÉsΦ ”g=â€ñbL?[y:Z`ù +ºRhf¨Ù~y­É«æщў+)ùø$#@ADEU[ÂcÛ~µ¿^çv¨Âìüsz(RbM‘S:ÃúU“Žpç ŠôlÏ=&î©÷w¸»žjâˆH>(Ò·¨±½XA Å=hqê32Nʹ½xÉ ;wȱö‘+.«PÃ#!üvžcó鯪óÛ‚"+(–RÊf ¢¤+Ïè~ž/¬ßGþ¢µFTÓóaEQá$ +–À_íïs‡5fRÛmö¥ 3`*Qò¡=‘ÈŽ ¼èRÆg–S¼Jë¿(ý*êBàbUVªúC]&–i¦Z%M«Ë³PdlLÁ˜—ók‹$ó¹v@ˆPˆrSV勳ÄP#P<R¬;‚F§T>Ú”–ߣJ‰+CG}BÅÈ+f{WlGeuU@©Ù¶<ã‹ôvÌmi0bi05Ñ­ŠuMm$dÞ‹-£Öt`*‘*6j‰‚±o)¥Ž(ùßv…¯"ER)Uqå\¼@æ@‚ŠFȾ–5L¸åtŠI ©§Ð¾eÿņPóèµbÓ± 7Ÿ¼íTÀqz¸´=¨¯5jÈä +éáªì»Ü£†™ªìë†ßº×­w-ì©·ˆÔ,[†Þ{zÀ‡êÉFäR"W€Ãs9B»Ø·À¡œ÷9È‚OEvíN«^¹¿êÔ2œ" ¨ü"ÂÒBÐhÖ‚ ÅÒ®lç +¤Í-d|*|îŠD硲æÔŸî]™2´¦V×1ØÀˆy÷çÄùÉÁÿgyøÿÊ“XÞ_íý‡¿Yÿpþø‹óã¿ý×þbåÇ¿ý÷üãÿüñÿî/ÿË_ÿéOûÿ»¿ü_¿ûOýwÿ»Ÿÿøÿçwü¿ûówúÿøÇÿý¹è÷Ÿøÿþöþö¯ÿô·ó»u‰_}¹ûõ >äþ3ÿ +æ¯óÀ9ýè¾^ùuÓÏŠ ÕäïœF¨î8 ø[ïyg \½~üO=ü5d$“4þÒ¾þMªì‘•éWþúV¤Ò¿j÷jNŠ¨;Ú– +¶L¥. kuV÷úoQJ‰°­]€}JÃ7¨Wx”ÃsF3bìL•©Džðð±Å¨¥ÂaªÚ’˜Þ{: ¤¸ßÙ•ìšrë2´FÎÝOý¥.øŠSFZ šÂÓNŠ£—«…}RÙ2~·Ë9g´g`Ëàï½ÞªSW +w|y î CIQ9¯X~ pVU×PµuZïízwFlLÚÆVrð¼õ4wÉ’ì&Nh½ÿg }ÀÕsØHôÍtª¶ÐdÅ­’»±Û®êœ¾NÉ,AÍĉê9~¥Àö鯮ÐÇ4. :"q¶(2èV”ï±ñî’Ð~ ›‰WPã/É_Z?†¬_ÍÜ¡³"wï·IÅê~‰ûùy¿Sr Xšje⎊¨ôÑúÃŽÂ;XÂ9"YGGšÞ°Oè_§AòÒˆ-ŸG·Ç°\üÝ”oЖÛI \åÓûF”z+¤¹söíi?·§};‚T{E\Ò1ÆOw¾¾¶’œÿÚ½Pæ×¢}%; 5°ÊN™œ™üwFjEâî—`ÊJ¿GdƒRTl§rßœ>ÙÐðòûšå!uƒiV›QbÃãåÖŒ˜üd!©I²~Ò⥩(5zD´$²â´=´×€,H*Âyd ÕÂ;mCŠÝþ…Ž¿€â`*é°gKI”©Ö)Ögÿ²#´QñAkq{ÄÎ’ãZiø+Xi >*ØÁNªUHïŠ:m=ÓuB ³€Úa­ôE ümvwkÇëFjŠ®±èc Çõd®¾Ò‚ÆVèÕ«ù9bñÛèµÒþ£- ñ 4v?Ÿ8¿Úmr´¯ôU7@¾«$jš…$Ø©®Í-$_œú ›ºòUyÈ”ψü¦²²BW¤Þ>σ{=“-Í&OS’t­svDæ>º=âãØø)¨¥.\•28nЫÊNÃŽ6³6³0<Ó¬õ¬ÎÉ9m\ +ˆÆAoª'gÇÞã-gÑ‚¯nEýs-:t¸ +w¯+Ü2ö¶x!cí¹Š…ÂÅÛ_Ýnˆ§’_žþÉ‹x"¨lq_¹R“]´ÖK*Š!Ð<9±ÜÝ™T«{`?W’…§0•MSâŸrñÔ€}& +<Üe±C3Èo"ä¯ã6ÛèÉÙ>£î(ƒ/žû À&ÕÔ²7Ënß7]µ¢äú¶?"2kµ°‚ðÏqsžóPîãÖZP)+²û&øø&ÀV'¯'b›4ðM$=º~”P˜±–sÚXI·€j4˜ ãgÌ ¿¡„ãàEΊúó>¿L…Öw¡¦Y†Á´Œ pFLKØT‰‹ø(gØ¡ûɵí3}6#i©Öªê0l jy:yQ£•ª“oLš“iÅ!qNyýŽã3ì⢀Æé·&ã•Wz«Å:õe·· +OewsdwÀ3˜œ”´S + mpÝQMqèû›«ÒS{í=$І¾Ó×­*DÁò:…F šãã~"œè*Ò1Èx3ÃýªÂOÖ›Û_§§8ÏÇíñ(wõT£bü›]#ôΈ·x¾[NAgéC4k<0Õ®1QsçQ¶t×ëÑM–Rß•&'¥½ƒÅˆ‹ÈÑÿ˹гò RJto"•’ú¸Ã Éš@]èX(T\ÖÜ°µ ¥1]íú°ÎLZ#]~­ÌÇ¥ê¡Úfó½ ¼ÙüÖÑ^\™c;BÌùâ„þµžõÚ`j­z'Âuû ïWæή„ˆ«$S³¦9ñãêJTÎïÎ1Òª¨ñ¡Ô3ßàÆ2’ˆ~*jŒr}5Beû9ÑC¢ +MD)ã¡s³÷@ãªKŠb£HüaŸ òÉ—®«”ªG‹;ÕJlÜ/ø W·MÎÙ¯½…ù\àÜdä;{2µ’$é{ª)Q‹àG†`÷ˆµiÝÐÎc¤e<©•"aÚNí©•D…Ž®Qé'ýí¤utXÕµ¶m1^²ìšöàhÅ¢=Àëñu™mO+eˆWW8áÆŒ4+ÖÈõÑæÉïsƒ€ˆŽó5¦¢ ½þÌûdFOeÃ7—ÔŒ&¬è.î¥Ç€§goÿMPè:_Oâ×QxÂ!¡eHÍx#ÐÅü¢eêÉ—5,$KÜìSÇo^§_ orÐÑÚ7ç£â{f2åÜÖH¶Ø(dT@nÝhßqŠîo{ÑJêFœD»ßs+'4Ê!5UÙ Vmðô Ž§œ?Ì/›óØShÛò‘Þ*;ý¢@­& +ŸçAƧ¿^‹1c‡³îÛí¦áégÄ;¾ü^¿ŽR_I«X@Uwl–u¤Æ|æˆÈ @Své¬ }ûÃeIã×Ù\ßñ +Xï¢2ŒgðßeÈ#Õ6ˆF¡äf»å#€v°–ìCöоÎ{”`-_¨5ó)JwÑØŠ±=§×c-ÔS;$Å.ZûŒxFÛi5…šËY¾9ψs,½ 5<»kÉ9¥%o…|¶¼?¼[<Õ(f! ŒZ*\b†Ð¶a~!‚÷Û6Xâä +1›ò`×’§Šo[s|&jTÒŸ Ùý,Zg]‰’CÒP¨ž±6D¸s'"šŒç8 &¤lMËþIIœZ4r¥"‡o½•ïôÌ(î‰)Rú‡^÷x—Ãáˆu£B¹eîÁ¦±4o»†SoyHúýñᮄö›gI‚+èõÎHüüŒ8bÀ +Jëzfœ÷ 7Ü¡%A aÌKW¦æ¤"~%j1ëG¬öªïÇs¯†˜2šK}lç_ãNÿãðžç›v5ÒKÿ6b¦ØÉF^Ïä= +¥]¡–k„Þg¾ímñl%}b—ú®Þ4Õ«®T«7Y’úñ¼š)¢\Ç7ça„#ç]È4Ø&@½ùæÏ쫪ì/l4ž:ç{Ô±­–PH/–ïÚ~CVÎÊ Žˆ¡x¹RÁgöXH™ë-"·ìŠ– +&ïÏóhG­À8±¾Ó±±Ó1~ªÎæçvP`ÊÇ“ZTIJ÷–OÍÁ [˜ã¯f|ê#c4€¬ÚÏF¸A×±ìs¤]KËá~]I¢­•GRÈÌ)¨X_ÇqeÁ¸¡·qÜÊ`GP舣<4jÏ)Y Ú§ðws$a£~Ï4žÉÚ¬“À®ºáâ4~Óª)&-Ø`C»]×óh]v;?#’mÙ§¤3”¿9šI=/”þªÂäîæâïW‰k­½RØ·¨ÙÉÑö™T˜Ò +µ-wR#Îwü0çü’éo½¶?Ö`Î{D¨ø½ÉÚN,C%¿âèqyžxl™#,’Cö;Eµ›Ìx(ÕÚ^çu1àS”‘²²òV¡ÔåÓ¼¾^xúõfJCCï¨ý(üé< +ôÖ‰ Ø²"^ªoäçVô +»òЩح(L]§lúèl¹ºmš‹¥Dnxª{ñÂÄZùÀjçJŸQ%^–O†30z$¡$Ë/4$›­1p\x¼†í9pß¡ü‹OÙºbï°v +ÔùZn§Jút”Œb´RhJüøTX×d†Ú™äyhª‹úêa-ø*)«ŒÇNlT…ý˜”™k~ÐNü'h I|É•"Ü–rß~ÿ4Ó…°qý›Ó¬]²­i‹¾ŽšŽ›5™>"¾[?‚QaÛC_¡Ìm#E5-ýèuèoÝ h/‚˜"6´AÆâ¦hé6zïŽù4x%Š„ªvK¸c½Š ¦Òi‡„”‚J—öF!ÔvCMM›)ûšXN‰¸g;Vw‰æ”ãooWõz`†Réƒ,²ïs¿ü“%¤–¹›9­}cÏAÆoçrÛYg¶C@Y'=…sƒ ô²W©W½ˆƒÑää´žšÌÀèÎÇ-’ ¾+;"64wkODß÷v >GÎçJj¥´(ØǬ¦ñ¸×þ©Ý9OrEÔ¾s6Y·MË{D=k3¡Qcß²”¹—gîiôߧQá—5è7ZÔ:%ânÑæÇB†¿7ž0 ±rÅ2ìØ4ÙgÞÙgZñW0\îŒBQÒºS>jD;@2/ü˜c€`ƒ­«¨ð­ +žë‚GÿEé>t¯+•`¾ L9ðúëfQÒ_SSOÀ©_™f„F8M@ι‚^›”AÖý½%VipT Á#QÊ©®¨sÓ䘰9>àãpŸŠß{™¸&¬—öIP1‹v'qO¯£Hˆ¢my‡‘˜e£a‰©Á 8~ ‰«zgÄÝ×Ü +Š@r+vH@øÀãä³*6”kË~2-ÒƒkþPâ««3Ý`Q”liÂ_E1fx^Ëa€”¦ÿŒ¸ÂêMpo~çgæ5ÐTÖDN…@'cRUª3]Ììì×Í’t†û/¼ÀÊ!baÔ=†gú~0­ážÌ…èåh峓1¼­É‚z0°gFê¨8·ºHñkM35øÍ*ß ? Ãn¡”(9¶µ«¤‹ÁqE9Z¡*z&b¤ë:¤©›‡X<¦rQ¨ëgÔÍ' +u +¢âHé·Ù³¹!¥µk·di®ò Æ!Ÿ ò°þm–uNȵ9í±ùÊ@®@U"ÒQ»ê>"¤J°Ü¾†â#­Ï5nX±Â GÑQ£$o6ÈHúüV˜÷•p šç>ˆ:NåöÖ”@IÊ*]ÚÜH¡ faèjÅàJί@;¶ÒŸÜ¯å_š»ê´!² ÑÖýs!xk\H[¨;4+¿5/Œ€œJÕqÿÞ¤òr$š¬ +ÔÀ{¹cñ–@—fŸ;}ò˜¼}7¡ÖÔõö="æ=mû8Ï6ÅïB+m}Ädæg€C g”×ëûÄþЄumÆØßvîoËèŽcw Å€{K önŸ¿ß¸x Ceþ&H4”xl¨QBg„½ ‚r:æÒ +a¾¥+>ÌN+Yž›}jŠò¢aXw\±º»ßE(—Û¶\îv<úŒÂñglÌŠ\%¶ƒ¢¼îúÝËT¶Ì—óÉ%äèÏC!­ ^£TÙGð㈶ÚîðZu¬ZW|0”°¦¯U,ˆˆVlS»÷u%×IÜ$•gƒŽEY_oσÚÃœäwÉÙ9y¥[3¼á"K㓈c^¼)¤Úí‹+Jb>64¨ÝÍ©á¯Ì ±h7RÛuÊû›ˆz…YÖ½©û}Fa¬ȠÓÉåd«3 %AM’W&`­|F±V96ÍÞ#~¿“%È›»úÍyÖƺ'HÍ~„T-`—Ô÷DÀû ‡ðv¬x‹ìc]C‰¹ðÁ%£c&1jdÈ°õ-T톌fz-ºû[ôûþÞin</ýq}žEsës몙LÉuɘH}œÁz¯=í8烫zªqN¼w¥M •p~(cÉ{"»¤È×ôåEz°\a—ˆòBÒ5eøµÙ¼í¢Í]s?,®u:Ö¥l±r©]LJ HaôØCav±Yº^×Ñ«€ á×ìá¨È(-Õ*•aa±!–LDüEæ-Ì'К's·œÃEAr×O­ÿàƒÛ7‰VýSȲ3ÚSæyÆ£$^k{†qá7+Ãx»ƒŒ¯óca¤ëÚNàjÂÎW=r1°1â 9ÀL^Øîܼoγ^3pÁkÓžßO•ŽwÊ:›×9ÍÅ>Ðò¾ íÚø6HãþŒú«ý}ÒL\¹¤ÓßÓø]7àˆ£2mýØjNg4~Tí>YÚïï"Z€GX=õò€„ߣÆ!G´·@W¢/+¹c ©q} +ײ`Ch‚Ò}!gð­´-)M¡îƒ‰^:¸eóF«…ôi í8mÐ^ëÅ+ßD¼/Ê)T¿G¡%)—Rn&…nögà wÀTã;U“ì4·(Äß:¨òéZžõŽØsC#‡¢ÞB¡!̘,ÈMX¹Bpg¼?Ö„p*u¨Ž“ »ˆÉcaSÁ.!7è&k•ˆº‡"ß°á§ò[*©¬%Kêß=ì8¸$YôFѺŜ…j°(T72Ù„ü)šÁ|âÛõù÷tifª»ÇY{ãýÖ"v¤]0üûˆÝB#„£ñÔ#1§2¸–’t—°Eo&_ðÀKDèõÖ+ìÝz]î3Ú>Zɬ=Oجïõó©/Ûs|AzmM‰îäÄyõþñÛ(¿(Ãÿtwéç¿ä™·íÛA>‰Fç!¶ß¸ÌˆñþaG þ!*vgÃ!¯qOæ.`мRqúúpA¥x›E3ñ„šsXÿ¤¦÷tc˜WÌGi"·x™mÿ…þ*98kšG}Ò†Ž[c‰Ð|ú&•SŠGÝ\)X°Ýˆ=,iÔÖ§u"[ÉÐAo_ŸC°ÇøÒErwÊÝgwª`ùŠ`ÛßC=ÿmMêþõøºŠ™Du‘ÎD…38‹–D}è K„â;|•3N‹ƒŽðú;J4ùŠþ˜Ý»+JîÃÖ{†!…× ÙÈ:I[o¸¡©f¡d’’—HQõäöGÙ hsQèÛ Ä .( ^ÜŠHé*U¬é9~U» :ÜÌ°(È ÅÓºoqW<öA[#_;9+»£8ÁÛÐO\?í.1f²æ–`›ªOþ]œ˜ +·S4el«Å@àíÇmò FæsÚM|Z->5cj¦½~Ýà¼Á“U WÅ.^'oåÚJеö·ÃôÝH#llçð0Ãåï€;u%fÒ8h/é©U‚D] Y.#¨Óoët¡Vqî8ô³ä:(íÙ÷y^e rTÜb”LAóù–+±_"¢ØvHëÙl•9‡`¾+´˜~]¯ Ý0X yc2¥n ³¥¡ƒÒ:§å!Ô àn’p˜\?¦$ ·Œ<Àøu» +-´P¤è¯'MS$ã”—y¥~¢`} tøÈ䤹¡ÁÐuíG5t] Ê^¢ÛöéY3ЩF#Tv¥­',;®ÄCãJ$*¹QÃÕ¨â¨;Ù¢ž>‚²Êzö/t¹Øã)0]é+<]¡×%Ôi*É•ó€û`6?gÈHí*ò"–ÝìÕnÙÓíyjwn6Õ^n?hаâbÝáÀ¨iQ‘‘7“·ÿŸ¬Ñö®ŸCE¤Tx] rs@ŒFøÅuÎå§]tåÎ\ÉߺÞýñÐ'ðÆ5Ys¨$e¿ó8~/gÎ@ɥѽÖëT͸óDðh¡ïÓÍ[Ð%ØèébzqËáúêÐ5ä¥Àù™u”2n¬~í‰7µ‚uÍ׺Ÿé/ãMázCáb¹ÃߪØV´\q¾VºVW E1ü Õk«1…:纠¶ÍGæÕ'+*/óžž¼G—zíò]Ô¹”š;¹ç½â{UØÝ_ÙŠüËYÚ“ð}«ä„TçZwÿo•œ6],ªÞõ ¯¹óÔ?e\;3EO_@íïÒŠ„þs³‰ÖSYSņB0´pH[ã&ª1Þ/Zß NêÿTö9Hñ %ó³Iå©{(ß‘ÓPT&Åç÷@ ¨ÇÇFLCè@ÞjÖs¥,@À„ï6£u乯¤ãÚ)ó] ÀÁ{í’Fs¥ëáÏY>Ásq7Ïd›ý\S‡ƒd®‡$  +DµFSû_6é‡ÂËŠ_/U´wå®ô•Œçž:zKs|Ý[Ûé/ùá–häsrY"MçWQêF|ÆçVX‹âÅ–íl•Ü|nâ?hAõöÏ,¼ü$e7ôŸ"T”³f5Ç’I¼õp¼°dº"Â…©1Ê•àÂäkh£Ë.Utº—·_T_C"‚YY¶Ô÷¶û¥|PóêxªŒ¹²ìùàä:" ªˆ ÙrÊ,‘SnJ´T* +£‡Þ«ˆP?0;m?~ûýøùÏøN + ºÃìЋ{oˆ J_S3úÃŽ:å‚S—õ·®£ï®BìA*E›×#ÏœðŠ˜J^‚eÿVHnvçºé9náï÷•R3*<Ú`häEnó°cèUZÕm¶ÊFu·®}!ë +Þƒi´&".c‰Q›˜§:qàh›23¯æ¬€l¼u„C…òT }!럛¬ÿHxÔ?ÝÇD-j }œj²µh¯ax%+ºó¦íñÝ@1_=’¯^Ç#`äûˆÖŠ:ýDQ˃ø-ZÐei—ØO¡ÀËZ‘†½daQ¶!.>wýº[¶.`3_G1O¬aúÈ…êñøH•øHÕ’_b2ÑQ:d›-ʈRÆzWù¢NC/…áê˹Ñgk««õ-Õz²P`p´,ѲcÀ¶d¥îG¨þ¸#ƒóHI³¯énø +Zn_Ûqnpžõe9{^v¶ 3¼ïÖ°?P=íÝ(j¹=+µ]©Ñ°B¹íŠ's¾}•(árU2ÊzÊdJæÁN³:*ó~®Dî â>ãFLó‹âH Ö.ˆ!noc÷Yç¶Ï*3öY3ЪsˆˆY™ù½÷­ÌcÈÑçB‡Hà|4,›$ ÔÂJ„í­ƒ6ýÀw„½w ylKò“R÷ÌL]½¯¯¾ç}«ÙÎ"Ÿ(%@*/MóãÂ:(5ö½»¶¦±2;†ÇkŠ`ò´†| '€“…_UVû’0sa DÀXnö×&17’«öý³H(q¶gŽZ?^5P¦ª§kê¤.¤ÿ‡}.”ÖÆfïˆÑ£™È7¢™x²2ö+à/„Ãƃ”ŠfÓûÆ«Þ‹#¼Í¨ÎcwÚë&Õá@½7ŒŠº&½Òˆ+⓾q^Ù~ë÷Æ Ž>ØÊ‚?ÛÈUèeJ«t&â:xcºLŽéëJV‡7+ÒÛÌþžÓÒùˆGˆ 6EÓ!W";bIóÑ$bfÅ~Ðvý3]枺вᒗÚê·l˳­qÄ,{Û ®cÇè(_“4ÏN@‹Ö’ëïúšC ¢zè¡øâ×cb}jÕki0Ö$?ìm,u³SË9Z¤­V_§00%dЬ0Q¶#Æš2ÈC°´ÁÆl‰1¥$ p¡‚ôν\oû)!‘Q“'5@¡*5ΑSÂý»ðÃÇëÑØdFÍŠ™AÁÍGæJ¬ëÚþÖ/©”ó”[x”=ã1õ´²„ý[ÓªD¾³nQ˵Œ¶—€) +>¦4[ÿ•b-µÏ‹ßAu1~ઠ™¹ѤÎþÊ+Öò9T±z†ò“èyÁ%ÉþåŒæϹ§A¥À·Ëª)Q +ºäÌE“?!¾0â9csU;J㰟免¸)ð~ª˜M!™V2Öa ÙOªf}«(L#€å¾Õù:'u”D…upžÒD/‘õDE‘²yÔTàΆHÂAg\ñô²K¨“ÕGûÜCÕÎõJ0‰å^QL~Fܶk™žçØRä'¬ƒ­•n¸`;· +Æ¥2D?žY¥4½eÑ'׉F¥Lï&)%hâtj ¨Wì)„ų“Vœ·9uàÇ6·ºpÄÜ£Ñ8Щ°ù0Ã~!Üí†Á?T¤XnÕÊ/û×è»:ØÂ'EAJñ:`‘WWÙÆœT–aÂŒq«‰’’làŽ(«¢&b€¤Ï¶ÍGØ2€2Býø‰ d]Çþ~‡@ßÑZŸÎ\¾¾¥ÕT6n!Š°ü§/Á¶é¨|÷¬ç(¹m`‰@µ ¸Ú]O¢O1|ˆà[kßrµöíÀëÁ›ºHì‰Wf«D”@tÊŸìˆÖpU$l­(ãu;hâ_á•«I*Ö·Å•/ËŠüº…z +CÃÞ?èÖ+ç(Žœž[§[½'›Ûb;×uØ]G„Ý)¾Kf6³|ØÙ܃éÜf*8RÛÔ¡FTüÒ£ãÞ€vx| ¨ã ƒ­Ê4ÁiDLVØ4¢¶ ;à'÷!JÞí‰Dx1*„ ^D¶v÷&ü³±ìt&HüÈ.pÓ`Ý$`<¯3fŠF¬^-ùT \*›d‹*‚{mQgŒf“w¾5i“ÆÀÏìt—™×^¸IÉXÓèÕBVÛ‘Ñ[éÜØÇØù‰ñf}¹ ' :æ‡-…„œ1z#õñÈ(aÅfzhªyòwÑ®ž"öÊOשýÕ”‹áçµSÜ5¦î8YÛ½qÉr×[@ÁG Š¹âåʦùBb,RÞŸq¤¸ƒÖß½)&VÖ‹Mé:¤ØðhX+*•šó;8(õ^ÆÞyzÛ;…\Æ+H·±éz0ÑéöªÖà +À?I8|=¦yøz´ 蛥gQˆès~ö1×N0ØÛÞtAã`Qõšr ÎX]‹=Ø#ü3MIþ¼&ȺŽÓ¦`È{Jã@¨`Wë¶Ù5N¾¥mr¥±Ö°š.ß¹^`?ÒÐ'c¡¾[hÅ*!¯éý•q¥® e5U+綘\¿}=¼ÓóDdo¨cO6Åwj+@ ô@W!” xQüè=F_¿¹y^@C@Ãá"²@Ó̬u­^ÁD U´ÁÉ…žÚf ´6llÇ ÝYµÇßoÖÿFI†%Mä>õ*?Ë®ÔqDwö~èá´L™ÆèäñA<‰—C… +À½(/Vì»ÅßÁäU6Òdp3EõH]Q¿¢‚Ký +5TŸ®‡dº¥î®ã©èk*é•Âûºº7Ô¬VÐ)$Ec$MQÖªµ)Þ§u™õÎÏ­B›Ö ó5Bçp#B‚=zÉÔa£¸0¤zúxê²rqaŠñ£Ø¾Óœ}Esv>BœìŠ‚—FˆR?|1áÜi/°­˜HXE °ñ[•èÏ:Ä îÞL[¢¡;±Îý˜PgÊ[¸r“fÇ1Þ%ª$ìH7F£ðªT‰èó,è:E„Xý¬Œ‚0úØÑ÷ôÉ6¥Í¢,t £©5ÃÌ—ýÖè°v<Ÿ«Ü¯Ëd!a»rµMî>7¹»#¯ð3ÔѺ’Ey„!É(ÜûBh¯Áö{_¨G`C Áx¶™+}:~Žtö“lÂï¥&K™ˆš,ó¬²ÝxŽôú3#& -6îMš Åu§¸è¾¸­ÍOÛo¹+Hô¶é‚Êf«¿väD Hq5ˆ.PGa´™‹½ô+È{ wó%866ýÑ'³Mt¥M$€ç¼S?@1œ9[Iºí –QƒC8€ŽÀØäbóóËflÃh˜CTõyH±UsFHÊOœ²@ÖTqj[Ér/Ó`'æOa‹Kv7—BŒBCû{£¿Ø"|0›žöÓÔ½yp‰^zä&y¥Ðâz*&Ï­rq4]z eHÁåúTU©­²°MdØ™¨±ñ›À¾ý³tDÁ¬•M±æJwèC6]¼C&í=ä +Þ`ÍiSyn&4ª&meDÊ“)l­fÔUóz¢ÁîESp¨&ºM¸z²Ü¶~>:®›sPŽ½%;·üiH óåÚrÁ^>l!ô€¥š*¾› -sÈ |yy]IÔçºR¼8ïT×VnRLlc^Ð$ΪÆñ "Huv`ÖÞã-4•-Ü?Ì0Èîø)çÆ– ‹#—ðØJšX¨ˆ…oNeRñ)ñAÅgÆŽ8¼+œíySo‰Ò%ŠÞG^¼HF/4éöÄW¶9àúRÇH°VE!פ݂þÌ.—,‘+䤩~þ–UÉ–mÅ"‰ò½æ\ôT¹è‡mÈÌÂJ(x³LfTšbÁг”ØChì{iPl“`¿è¾çàD¸#¤çÓë:Ž¨g´­ú§MQÅüò87,‚HPßÿ}ïa¥V´îm|Ý›¡^ŒéD¬Idbø÷2[üÔÖ÷|dܼ½ƒ™±©A—¦E©ª§c˜4PoéQÚ³©]d…‘´Âaäw­â¢Ç qªúÜB¨ëû´ ߥ7C#V¨T™Øn«JÖ÷jo±þK¼D¶±¡’åÞ Û~ÍöÇ£Á¾E\לl¹h]ã7B·_­odü!ÓW†ñÈËÝfH4Ÿ^¬ 7Ûdj&Ôšîg¼™"æz Í]ç™:›ÕÇ·ìR Vêg³|<†œg4µ$¦N]ìoIŠþ±²íDÍ­ŠM‰îº/ÐXÍ1K¦Xéã²tuƒ¹‘åÑÆß6¡k6Lhd¤ï—mºÓéëp©Ðo$úüö óóŸñ%•R¢j-;=pœHB® ¿eO5ð–ع¾cÛh!Rb‚z”ª©r]2SàylÔQ êHÄ>H[UÒYøŽè¸” +à ÕSÌëÃT .é½tzbq?mì´Ò%uEX>+ú³uôÊq^‘€ºuà^=RXízsó˜@6RùÒg²"ÜCý¥îN^šRµ‡¬º¦ 9j©-ÕÝ-‡x¯Ó ¥JŒ#,߀ƒ*]BŠ”ÊX›ú3\Œeh†IÂKµ²¦zpÐa¨Ñ‹»ÖEÁ*ÆdÎÇ–¡'Â%þ°a™+EŠ‡—žº.¼qD”W·î!òÁíÄQ_¦ù*Øøp‡µ)†=OWv‡Îl<Ì0å´5rµlÚÅí †s¿Œ­Ff%ú5pi?d…„ÝšjŽúZ=bÿàÄÛ­ý&%jµº:t™ÊFÓ´À`R½ÖTK­öóÊ®ÆÇ¢g÷ t¤låßÃØ•bUX¯¾9.ÞÂ)à–§•óµ z”ÙaÚ§jœ¦¦Ôep¨á<ÇËñ·†n†X 6yÖ[É”SX µ¹?FÂ+©™±üD½Zê|¹ÙEì+±=C@¿°¹Ü€ß ªPÚAƒÅ%íÜZÏ7ÿ¹|g-®!5ôF&ÀõVV×¾(­”êY‰öÔ£¦MëôJÕOÁÏÛ’áÚÛ´K «³~§Í²üq>‚²,´J°æ4Û¥Aðhÿ0椦¯O)ª«ÿÚ@á+§—'ºáÏéÇ ö­¦U{ÇUfØCš»Q¶ö’./~ hü°öÖùÖ"#õПoZcÄú 8$Èjœ‚M^k>L¡Ój5óŠ0üdèϹ=b‡£|ÏÊFªÁvŠWN‹›xOI÷²X0,S·ñˆ³ á|m«Ú«ZgA¤Wì7™ñöU¹µ7HG)úF‰FWÛ©À—y,ûáèŠ6W:(ØÛEتôŠ8#s%æJâwêv¢!BA‡[ˆx\{tüµ=®:¢¯™TqïPîTµöý×û0]¿bˆ\í©ÏjÖÕsÞðMgÊ?·‘Ä•VTí)¸ŸI,CèârÛsm…öŽ(m¼©U* „§©]®hØË )6î‹9,ð»íµmK›Šø~q/¤f 5=7W$T·„hñ݆9Q8«97vë)¡&c”²óì ¯úmÇÒ»SƒšÞsÏ"«âGº{e“ž¯t§íV­ÍÚו sU÷÷É´EûÞäi,ÑE+u/^âÝîö(•)]ÄßÓ½_iïxÎ_ +Þt$5DÚâø¶2àqI°xÍt6]Ïxe?¯J:$é¡£5wÔ¬›?Duñ8· GŠúKgo*y³\³yáúÐù÷tûÖÁÁSóúúŒ N6Â7ˆß…ØÆn³nݵ󌇖9uäÜóËQ7/²Û3Q³ñN•ÉFK¡Y=¤ÓØH¶0Å%5°¿|ðëØ~œ«Jz*f1SV±–Eî ²qîˆûq<{抷ƒBŸ¥1P£¿bK¨Á8b‹\v°kO‹¢ÊâÊN Ú=Oı#žrr7CÃŽQ«ƒ&½%tK^ü:4p‚5*°ø(#]¨ª~æx•~¤ãÇŒãǹ„ÎM^£«‰Š^HœIé3m,¶{vÏÈšæ¬Û7¿‹¢s-§‘ j¾wfw†õLÉû1]ÄÊ·ûꄵîÈã66ðN†ÿ …ÎØeœqì÷öÿ¹’©øgêé|0´™Ý.à8TÿWÝ6m;Ý»s>*a!¯ˆµ]™‰`KF„ÐD:®Žåsheë•‚;?sýêäk"xò€fv4µo(¨.±*`tB½Qu*¾Ðƒà1_Øxï_Eä3^cëôÓž¨yí(ùßw= ~€ñEÙ3’{1Ο5P\Ñ1®×gȨ¶-*RÚ“Òb¤qíe eÚÇÊɘÀJºÝŸ^’ \WÀ +Tœ§·mýHžvÎã4Ïo~hñ¿Žx»p§P¿É•Þ¢ª-B~Òz ÔpÛ]å7«›»¤ƒ8J”ˆö‘Þ4$.t{‹xÞÐX·!gvßßœgÞ‘®­’u²b(R„°á&ªÄ˜æªå«°z‡¯‰Ô½3JÄÄæÚH\OÙ|ì²y¶6juò¾è„ãqŠâO³£öMDî]ú³\ÝÛ>Îóˆv ­}ìŒK§‰µtµèv€²^ª¡/U‰ŸGAÀ{|9·¸gjÛw’T“Ä­£kÔö +.m°‡p©#ut?ñòŒÇLjÇÇñZhŽøÑu¤Ïð…s!q¨mô†ÌV†\ôÎ/0-ïÂps¥„̉Äb¦1Rù>‹*Â0jšSÌÚ1@Ëñ‘F¯p)Ÿ…'С’ê}‰é2¢îˆ«¾ZÀ’U}D©Öô’;uªÍ­ðWYa5Õøˆ(¡¢1ðàî?£z6ܵ¦nÒÉ Dïω°‡Ež—_‚8¿ävÉDäTjueg/]Í‘yƒz½UÛ‘‘Lj Àø­£ÛµbCRc h°ÔñÈ*[+a*–Ø /½Â;ãÉ_TåJÖuÑ_L½}×7|(׎°$²ƒòäò1Ùq@)£œÑé¼>þ[áR²C ,£±J.žiT–뛈1‚D@k>»Ÿ(¥@G¤@Ë.ÃW˜5ÐÕÏï xj[ùíŽÃ—ûÈîZ-q‚¬Âð"ŸQ$R©… #›îܬ¤ñ t#Š˜ÞìK*×Ï{Ö±ðиþÐà΂dr·=6Q’i¾ž§ßqg»óðû§‹wZÚK8¿9Í}=|\¨#ÛYjLlg™%?#¾]#ߣXPb2@GÅa÷!”JÙ¨T8ê¶  Û¤6¶ÅkŒ¢ùy73Òb@¼œ¥Î-uøJ çaÏ#{¾ë;¼~ +Ž¬•þzáó1äa©æ;FÙ¦(]·ö _{jo¼ûpW¡›QšMÖõÈ_R‡>"þiW’œ½?>ÞΔUÐv¸ƒ™w/îÇ(\߀FiŒ³’LÜ›þ³?NJFµµ^Ì+ªiex—ŽVÒI,gô¸B˜¬Alx+ñl^€*Œ°(K«y¦_ödmËDFZÇÛ—9¶ê?ßq +hæ|G*Æï+gßšÞ¨CW#tåTïÞˆèÖVŸü} ûã£EjÎ ÒWU¢¶'¾xê%1Xɾ)Ìxš=¢b<9"ûÔäïŽZQ*ª@3ËœGw UßÁ.g^IàGJ®°‡™P~qN¼,‡§6Ý|`$+BÙ¡çBVRìQ•\iJS½ÝñæJ¹¹›XرBmìïb… ÷™³æJÁÙ*XU^OGV̺ŧÂ:ÌxLkêíšg2þ¸·eÍrƶi£ óa1 0_ݺ©ÜƸzµo?ΛÕz!½‰ý¸ÕY/(çµðÓç½K”äm +[çïžô£Â&Nêùú JDÒô6¸6ËØjnªÛ¿G`¡R¬ÃqfÂ{Ô8SÂfšô› p +µM+ÆX¶¢^¾–i!í¬K¦Þ:ðÉëqÊáÛ«qóñqÍ6£7gêäÈhå®T¥ûÅóCh†A£´Øz™]X@2ÌÍÜdLÉ][­4åN‚JYóøÅmÎŸÔ 'KÕ¹O·)Ü/iÝWŠOú…Tùšû£/ÂzRÿÆà[§›Ç.âèkD³Û,ìÜÈMJ«¶â‰¨Ú9ΨB³ Hc8wŒ××u•©Ì ‚þÏMbV>æÚt*…ƒ&U×`ˆ<Åá+ïrwßÙÝÇrôùº]C_eLSÈ·Ï£ásyŽ£<Ú0ïQ¢î™0&f£÷þ¢[ Q;Ìöä@[.— Æ5Ù=˜ì%„¼¬yñÍûåO"“UÕï< •JÚØMÝ#RÔL)JQÏþè= ÊZk‰ ¨¹ ,íhQTUÅ’¾.P Ò~¾?"ÆS!ÊÖr9Ql;‰šQÑb¥[2²EÖ% <Ëz±ü¹ùàì¶^GXÇ#¢¶~ÑCÛ+±²DU}O=D]á•nYÍ\ótñ4¨í:ÆéyQô4†”Wp´[JYüëü×®ß4ÒCÈ£åã;ZCÀBTK‘dz/ùq€¢$ÉîkÜ<úoQªÙ4úyÑy/qþƒÎ&ìÜ–(*ç¬ “Ã{Äk +‘¸{ááv~s(¬ôM%›–Pç¢9´ûìZ6änêÝ^Ïl_E¨™ƒTyeáo¤š˜ųJ>®í=p™¹™NÖp¸>Õ$¶ +ë¯ÁS€'†Ü:³ Ádø*²ÎÆ ¾sIê «‚ƒ{Mkâ!>@[©6ªÜÐ Gúë ±­ÀkëS·Á⇭†<ßø.è±tçŽX{\Ÿ¹]cãe'uðó»ó´M>¤¨£¡C @ŠD XCé›l:ãÀ~¸0|}‹ž:õÁC(ˆ(J›DÑ_ÿ«ý}îÌþæ±Vß•J8k=ï4y½N÷²ŒÏì7¦|÷›ÒÀ/jF¿ŠºP°ÀSbeÝçŒáOö¤x 0»ªN¦¹Xä2ã—Sί]QÈçð/!C(¶é©¼°Tá æ& Š]ƒ³î…«ÀgŸŸkk2Ó÷¨5Ui²ÐÙb¨YbœW·{¨¡®¾gµ<œô~ß×íQ0âQ0Ð-zµ~NÑf§–`Ëè¥)¡†+£¶M¡œ¸úŠRãˆ2?JÚÂ0«ªÌ¯Þ­Œ‹ÀÃܯADšq`ƒ³»~Óu +q‹¦) º¦» Ôûy!š5²ðM~‘âhhWíÃ}!\Eˆië)¨ˆ»´£µ˜.Om]YÕ®{ùŒxMr ÊÂ×7ç¡Ô-_š„XÅqjní낃ö°oaC9ïs5Þåù 1f×äbA¢û§¾âZ³‚ûA™‰ŽáºÂáÓ¡%“JßÏ<§º:;'Vˆ|½[b{;îa(}ÔÇ?lªWýã·awNŸlû–qÿ¯<ÙGýø%¥Þø›õç¿ø7?þÛýç/V~üÛÿÇ?þÏñïþò¿üõŸþô·ÿø÷¿ûËÿõ»ÿô×÷÷¿ûùÿð~÷Çÿñ»ÿð7÷§ÿøüßÿð«‹Þ¯3þÿ3ÿ/'횢Y3fOëE^7ó¬êŠ¸q39°v:›ð×Ë W$Le àu–Ë.-g~ý¯!ŸZkÜÄÁí@æúVf`‚§©ñ… ‹ÅpÈAÚŸÕ[S£¿ÍÎû‰½2Êf&  +ìt’íí]â4åR›v?ì;YÚó.€vÜ7BÒ"e]+!1È ‹j¦?^µŠj¯Éýεôë¦Jù‰¾›$Iª¸€¥Àõ'5¬kWÀ•ÕŽòL?˜ã@5o¶¯3gâpƒCœáOÝ]Qyt ëªæý@¬C[“w)HQÓ–œÞ½k·ÁèSô4å”TVß•;Q^‹–¤(Þ"³Å#•L~•T{«ðhÑbRùÂË"Úo凪~µîíÅeOçV‰šD üP_e%)Ï~f²zn„xJ.B©øÔ)΂6},yzzÞB™U:ÖCõÁlnæt±WœÓ>âž’&L‰.åT‚k¾\(ñl[¢Dd‰£\’~`žz*­Ôuøö=nþ5“m†èþ“ªþª);¯õvÞ_j‰°>°ôgLì©ù²!µÍ^êÎ{[6W´ïÈ'ñ q{ —Ÿ"–µÞÆMH¢Î°2y T\N FSHZ£ßºöøYÂežkÎm¯k=FÕÓqGµ ¤ŠF¢ç3–€WH Ï çû¦Uãó½”|Ï°SZPC'¶i+‚Ö¡·"ô^ÕHÜÊ…°®­ÔRàf^Înk-©A0i °¢.F4+åJ£Ë¢$‚æ, Òˆ àMDíw"Ö.S0–ú×¹´Wîâ +Eažm‚ãqù2~Zï¹!tJs7¥¥½¶–T÷¤Ì—ÓVCe`¿mìG"/'„Ke䛌n$+ÖÛ'rB¹ ®kb§lxj£»‡¶—þF¾°îÏ´‹òÀ- +2œ=ÂîoÀšŸlto_Ë£ÇñiYÇ¡°eUg SU‡|]„ú1Wqµd_x-#CDEf@z †Ø>.ôä [G‚2¹N0Äâžr””]ÑM| g²î0U»£¬ýÆzÆ¿ñüü¼1MÁð•H$¼ÔŽ¤¿‚ðÎvºlwŒÐí< +ïà&<ã åÄÆ#:¶îe3.4‚koÀÉ 47öâK} ݵ†’&ë‡ ¥¿×z6»çY}AžÜ­aFY¿IX÷J7`¯D3†+]»·T4–Ûv00~è“UçxT9ØPt­M]V‡h‘+¯)0›@&®4´ŽñM„«KeUÞžèô+ßpE1c!v°k¸Wš©•MËËëÑ>>J4~é¡pð¤e2”*âCÖø»‹‹üzŽ'¢ô þN/˜¿ š@±ÃƒøÀsWmªJ$« ³„ ßzgø;Ä3¿‹ oØ#þ‹`!uÊ•qÚ{J°FŸÂg€r^sì)ìyêõtm'ëÌØö šô\hCׯŠ#ƤñN:‘¿8@aý:Å>Ýs°nÐ/eª_ÖQ¨c¥eG1¨s,*µ•Ïþ4DÊçü<ˆ‹¶E5h¹nÑ;8ƒòusãç½þ6Ô{ã>ÄK©GlyäSà+Å‚É’VV'†û,ê£Ç$V-ø~Ú8äïm(âÝ'^M×è@ÇO«Z̦̾‹Ù'&}=ã°†PÖJ•NÊ·<Š+ÚåÙšÚ]’®uŽЖ÷ÖéÐ&¿hÆæO}phô1…š» ‰_Ùšª_$Ã"LÞ2qxº×Á;ò§Ýf¦7èÂó“Q:G×ÉkêþÍ_Åó'…W>²ÔÐ…ÍÙ ŠŽ¯¿£W©‰-á "þ:]‡êÿz öÑïQ«D~s×lw­ íÇMZ¢Õ0èéàd¶áµ +pgJÒûA¬Á°Þ’£‚úÍR>æŽ[^n>›[[$¬b7Uˆö7¤b¿vI˜f$i+¯Ô‚%Eê”ÜÑ+¦RZukÕ볡mx|k\)z¥ªpì‹3ëÆz×NcM¸+€Ý'øÜ<éÃ6F³¤°.;P´™¦í”üP h3YSÀ‰¨ŠÒ°Qz #æ1©MÀ‰ê‰Û‰ÛôX¢ÐY¿áÝœ›?¨uZWÿ¶yž‹FÍŠÀhɈÓ)m—ïBÚü!ô— €DëÚŽ‚‹÷¥PÙµë×ö£è‡¸¦\¬u¡ ¬¡VÓi¨Þ?~{µ~2aÍÑXH×n¯É›ƒÙ|h¿¼¦àuSäè¢2|ùjÖÎ{Ëîc]‘»MÆÖèñ­7fœYnåÅâ;tù#°e¢(¨>!m€²|@ê ·BƒÎÖóÅu, èÔªQõLPÉôWÎD¸Á÷/™ßL‰,y¼!ÅÞ8“ÄS®YýiÜ`ʤ!"û®!€½äJXü00º‰@Ø…Þ#ÈÁˆ(Æ›c ½"PüßWZwâ’æ¹~@?Ó«w;,#Fõâ Y/V;¶3²81_p.‹ÒpL!¼‹6G¬BYœÏuT ¥¶½^òô‰ÍD×ûŒÜL©÷Œ‡¥¦ÖïK¦¥c‘Öÿæ/œ8É ZØ" ¥é©ƒ6àÏ<Út†NÕ?R¼Îßï¬[dnþ¹èT}L{¸‚kÞ,UúëìGœµ§p$Š>ºŸ“òê]ß• +ûÑÖ]MF®äR~Ç¡_ò1Ü£4xÌ×C%ÝouŽ¼^¦ È‰L§XÜaœÎQA8C|Ð4[Ìo"¬«Í׃øõAÄ]ÉR¬¾±ÍèÎbX kåA. 4>¯ +{ùq}á4qQÑ`°õâ6ûó<“¤/²úX*+$=[{1ó¹k +ç{Ã"9qÇ µé˜JU°Çœ-.’en^:šñ”vÛèî±I2yEº¸ªˆø‘²­(zÖ+®õ3Š'zä™S5±‰&÷ÆŽõ¨ó›ˆwé3º~¥¢JüW§Jg4µVudOš².Õ%Ö‘K§Òƒ ¹RBèþb?ϲÒûÐÍu]»c® j›|:@ú)C‘Ø»‡Ï(á¾Oxn‘m{aÄÕŒeT_G ª©¡¬„uiŸÏh;Ùæ¯Ñ•ð›Ó\ŠÒƒ^j,®z·*j«; ÌɃ®É2$m~ÞŒ-™h“QÇu‡ ZÜš8”².¾#« ;/Þ…ì"/XN"ú±URDù\h-Yì;b$h” ñ†$íð4uˆûÅ~4­×-ƶR‰š ­·€s¬Á³RÒ¨5—\©Ú& +/Ýš ¥°â€ÊýB›‚½ÜúßåÆ•MuÌõ¶fÔq|Ûëü8žAÊiζí3H‰p00 r_ê6yEÇëßE1W£qí×õ#è lT0“<2í&1±vÆDÍþÖ¸û¼ßçV]lƒÙíòÖsàJ›íÜ]CÖˆ/ýÛˆ™I¯¨MìGò…º¦°ª5°î3ßö¶¾Ó ‚DßC[u¦‚ú`õ>`bòð¼¡î4?>=œ>Lk¸tS¾ù3;Ÿ*É#E•TÝÞ£Žm‚r±°tî1Çž~º¨¨÷ÜÙ¥(#ªÝAýŒÈ}*È&t0l8s’‘Cñ¯šÇÎÈÆÎÈD_ìHOíá™}|­q$ÎY”VÜœO +_ì©liu?¡ˆ»&ïªÐ¯Ç{ìsPヱòƒü‹Šþֈ̔‰`À$7´uœ=¤Ç-ZuvøA“Ù{Y*¨L>} ŠÿÍ\,“#Ø'A2wÐ\P’ˆ]ä+@ÎÃqÓ"ü”•Äþý@Ò‰Î]3ÿƒþÝÇGÞúL± WÄŒNnaÿ:"ÉCKòðÐ?£z$åî¸g2:€–æƒk³v㊶*É u;IP]n6Ó/™™cçgôÑÏ ±­Hô?©é©§íš(¡’…3`aæÕT{¾¼Àw\ÿ>m“¼GüþéçÀD¥–m«äã< óž•2CÊÑ]Á-éUègöï"¾[ÿ>‚T1à¡ÀNו¡ið?ô¹™žûÜF™ø71wmÌêú²k.íÚ¸ÏȉÑÝòÞÑO™{m\DÖÆþ–PõNCï6Å JŽÍDG¡’ä–þ‰…pZ«pŒ«˜Ná$#ž¶€9Ú5êékhŸ`î’}5+¶ñ,*í¼ØÊÔâêÄ]J¤ûק –NDÑm»… çØÀKvÒÚ Ö½ëDüÍ÷ο)øcù4ØÊW{æí">>ús€ÂT1|N×¢0Qc·Þ‹wOéîPë‚U€b§YÅÓ 5ä<]çÚgÔ$öá*¼²\8©©(¢zñïM8}$s{¯{÷\¡­éþ‡MÏ¡¸{ûat1ê\Òøè^¡]ÏÊ\¾ªÍè]R,]™ùÀÈR¥òï§N@º{qàTm¸Qžú?^âáén>À8Õ +õ¯þùÈŸ£±}Ÿœ{2Û®tXNtÉ…­«РÕ÷²{QŠqëæ²}P ‚1}Ë6σ¼hMªM¿PäXkZA²¸¼£YÚÔÛœÀp¹Œ + %$”žX\¾\:ËœÚD§×ÁIá ãÄ]ÅòƒÅöœ©¦ãÜD£¥PA9‚vªÈ£¯ôæ¢H–*:¤nÑ5Þ}O‹qéÁîáëyBb¡lˆ’ÓÓ#jM¸™°@+¼¿]ˆÔšlÎ'%ÆŠvÍ*DÙÛ!*JDYSl;¨]]‡¶Ê§Šƒ»$ ¯†e³¨È‹¼^œCòÙ· .ÜëJƒ¦Äh¬Jò}Š?õ¾Ã‘ìòß]= Ûml!‚ + w'©˜¹®yþÐ9gDBzïÈ»p.ˆù `å´”IŽRNeo[ƒŽä8pL< Œ¸ȨJ&ì+Ù1ØÆϪŸƒQl îy¬ +bƒ}Ôˆ,HÈ­Zj®U§ðüø¶èÒ·ý]h½WS{‹UìU ‚®sóã3hJ!w.4YëÖ…,ËÜaø)ûPüÒ¬Wj½³&ˆýy&PÓ/WmnzäG£nÙ*üºù‹€ÛÕ^[ÝÜEÁ×ÁqÇ—¢a–UòDQþà:Ýþ㡹[Aúfô¿&áû¾Gø})~XÎqEV$àW‹žçÖíB§‹àG„£ o[î×›þuõ8o®qwŒ±¿ð̾· +…|mÔ€)¨ÓFDkL"Ô{Àï7„É.î¥LýŸ§éÂ" ÒE{´¥·®?jt‡¢­,/5=úÎJ3c®@ê­ r™C ‹”. ɒꌟŠK¶-.¹ýA>‚hpaé)·£¿NŠ&’‡ŽÀH÷k~˜g,È'~ðÖGø^ŸVÞ­X‡Q +SC‹g#Ttc Z¾¢),ä}àI—*âs'¸æqņ‚àו²ºÒÞ¸£"|OÝ·™“oÏ3ÀS!´ñhÁÊ®·^‰M)üX)ðD\ÒW:ÍÍýº°r‘ί;uŽ¸¼KîÀ­ÒMAK\xâm³ØJ¯ú›ˆz…Äsª>?é# +”¯’\&B—S¯L½uÕ“?¢ø¶Þ4I®¥(öøyà÷;˪ÔØJPÿˆºq +U)¬oQ5¶²k.~Ð^ýiÉï?•ÀÙµkI}0ïƬò¾þ ƒ™<ÂIÆWã-à5etxi ÞÆçIE¸b×À&ñ¶h„†ôC|Z®}óznÞã’K5=‚hˆGéýA”~ØA¡r–xŸ«ßH”ýKFpIãsHU]ø_QÆs¯‰b*ž»í¤b4%„ˆ÷>ÙËÏõuŤËj +M+X˜t2Qàõ9ãzÐÊäýA™–=çÏcèþƒ¶’Þ¥Ùž_£†emħô:ƒñ=îéÚLLò¹pÊÞF~Ö÷(õ(At¾‘:ß©á•t_.ê]Ô±îæm‡üú<àˆç4?^°ê®¸ HM§ ÁÏ&øЕyÔ½¹Ä2%Øÿ¹aÑ·.´ì£ìƒ¿ï©î?ô]`,èÚ„”¼ -˜åà #¿?¦4` ´wŽšÍŘÑ#X33ÉôïwSxaô~lЩ.ÑQ-–„eý̯¿ŸÇÖ•už{â~÷D<%%}S€^–9y«˜íœ?5CHÚ\ð7òÂVÆš ç–Å•ÁæÚ¿åeúÃP×ä9ÛôqpÉ!iüÃÁ?Ÿœˆd=³Ï‘»õðÔÝtcùÔæNú,3Ïã¨þBʪåβM5¢³~aéZqm“öÀíò7žàú›¨ÕˆÞk\zî:öÝŸÝ~#2$3âí ®$=B:bkT4_9Ð,ÙûòÅsWWÊH²ïˆ*A0}ü«\j-Й2íòZAëU :áR€hƒŠ¬g€Ê_Z &:%íìþupÑâÒ4íúq]†ˆ²l°¸Ù>?²c²„l`Ó0Æq~sž~PBóâ“M]y‹õsá~F¼/©>¾Gas,ZóbžÒMGÚ*Ôswèè(l'9lêâQžÙv€¤ +;‹Î*MùLW)Š®1V¶ €ðç¶Þ Û~7"'T\aø¸£ÄKž")¨Á§Ùo +ƒc +^ÉH“©4Ç~zŠé1ƒ§®}J•ß×ÞU¾Óß=HrrªÄ4q%Ê^øÖÐzÃP2úÚð"¼6GÒÚÐà½éQy6bâµ.wÎψÔÃlÅR‹£ø8Ç< |ÒÝ1Ží—»nÜ¥«ö{šÝt2i5?$Þ a ¢\ +»vu«Á²Í€ +.nBïÀ¸£›ê¬¿¾*mŽm­ŒÅ`‹~½…©²UeÐ*ÕöfóˆD«A -^Í0*´ª~¾õ½äiþ‚Ü(Ùð—¬ÂÝuÆï.¨´M‡¾Ý'¸€?ì(ñ'¤×­ü—*ûw +Û Á”ßpNœ!´&GÞ¸&<®®= U¯ºe °©xRXÌ€% ZÛ™?^‹[Z(¹é‰Í*GŠ{ÖhZ·¼ò«¾Û©·`7Zñ›ÇÁåže³¹{¦­µy90Û¶ƒˆÍ«¨ìŸx +ìŸMgdq6Êç÷ö˜s‡u|] ‚Q­nÙÁ#ü YÌ•'0Rá4h +þÿ¬½Ýª.K–žwº‡u(ë`;#ã7í1‚66>±uÔ4¥6,µÛ ¾{Çó¼‘ߪZsvEa«÷Ê13óËŒŒ1Æû“q_hESf`¥†üåÐû üpÎOÑä‘î@MSœ9Þ6õžÛ1éJ•¡ ÒÄ!Y²'I,g«ÎøímÚ D„Šfàx²fHlFhc¼@»&ÊpÌuEÊžÒ¶Ÿ"má> Z%ÎÌÚµ+¢ýstÕ¸—úÜ‚>О†šÅˆ=‡  ‚÷ø±šëJ‚ÕplÀaÇ`éã=!œREõG¢,u›9!U<d܃³OÞ°€ÊPÿœ·¼iŽQÀG¨È–«ÇžGìIîÏrFwM"¦ªÁÏ8PO1W´×Ë4¢\éÍ^ÄRâ –rž4[›KLÖ| }Ú^¿”óƒ…ôf„_#K6dtDP¤ÊÁoz÷Ê^°c‘â¢+/4fZ{tˆ|±x‘õÚ"àãNE"Ë”€ðŸ^iWöU¯¦F×æšXE*`Z«´T§°ƒ)ÛB„ +‹Q/Ö©dÿë|(Pö2.Э˜ÇÜìîZ+î-Ô•7Ð`ûtZúQ™¹Êë#¬z =•~œ_? øÊx߇öoWCa©Íª +ZÛÏ\¦Ô-Ñ¥ŸwXðsçŽ$¡§GÉæĪŸ èJJv©Ì½BõeÕ¾j®ÄöM @QbtEü’¹0_”dÎýbd†ÉŠ_Ÿ+=^l¿¦Ï!Ó7%”@Q­QXÛQo¹æ'Ñ;¼´s|’(Pþ¡ˆ‚ `øXtz^x-­ÀrÄ€&V ×âÎ;`a@ƒ>Û[dòöïé7ÕßØ+}J¾  Ÿæe‹"È«–>²ZQñÆŸ@iÊâ;„æ•hBs%ÉÁ©7:°ž×1 f {Ecšál?ñÁQ\z+ëu胥.XpLÙÕÔgPä +^f¯I÷XQ÷¹ðE,g6}hV«oâç ² ‹¢1Ôvì¿J:]…®¥% ’Y˜ôRï¶ K×ÉÚÝ…IZ5J‡"ù«” cWÂsF`FD´®lÞyìUW(MS‹¨þÑßB„ía˨ø¾÷ƒ‡bC¦·ªŽ°Pµš÷Õ¯õr¹ä¡CÍj9…](àdã­Ch¤9TÉÉ·B“K6Ä¡ÉŸßM'KêˆñgV'è\"R£L>JE1=¬ óƒ +Œã +ï 'ð4X8YdS…ˆmü4r#<–ŒäšIrT‰†ì ¢ •É׋ ùML3S¡Ûw×GS£;5 I +ÓÇÊVãÏ'ao>÷­ÜFâ¹'Ù¿Ê¿8¨A\Á@ 2°Aè¥C—}Äo¡Ø©ƒåÞËäeGnòùÀ3I2hB’Ñ¿æzÑë((Á¿ràq‡G ‰oR¡©ÂZKÄs J¡u'Ât³p¿%*Áî¬@v°©àð¼ÜR?WzÖѽѢzó<`Çs?áÓ5yk9ï¢vIAsô$äJàÉ*Sg4zF-t€ÅA<µàêÐp1÷P,R¨pŠÏÓÝ õ {qÏ6í˜SH5Ø;‹…Èv¶h#=\¶&Jâ8ÀK†’†¤X®(/ ^\Ër<¹èV der[!‹¿²d’I^!ï(ÛÀF±Œò“0Þ.úƒàµ‚ž| '§jò‹{s{%‚"(¡,hÁö&ùPªyç‚s{ÛQŠRZ¥([Þ0à»ìx4A¿IÏ…èÚ‘»®#Ú¦‚Ù½—ÔrbwÛÕtˆŒ!@Lª]R¨áábӨU[¤.0Ñ¥êÿ—¿“ßÿ†ßæÞ^³ÉÆéA=R–™ÿœƒfÅJõCTwZ1Ò슿¤yÓÒØ»#9…·hiV¥R1h/+ÏRµ×¾uŽ¿ý+•M€Š’%`þ„ŠÜi üw×`>½ˆ2jMng×Á<ÝôÿCÕ„Üjã› @°û:ªþzÅîlª–XM(É´ |¬fVXHLž}4.kÂ+ a Öýex]£wÁAê^pFÁPòße¥òzÎNúcvÏi•¼[6â?g»£+Ñ7Û/4¸ ÖõTKK ë ,,Ýx”a _jå…v: ŽÏ {følVwµø-±áäã@Y ¤IÓñò*B1S‰¾O%¯ÕTl÷ é‡ë]åH–ygOZh*Â~±‰ìi¼Œìö<•N…õµeœÌœVÎÓTØc¥kÖÐB]ljªB¹…éÞe«[F§Fàæó¶Å +lÇIÛa˜6Ðà ¥^‚m +J„ cõX ¼šW0f€4;€GÇ›-³@AQûýIquá(Õ…È à™ËYøù…ð·ŒPØËü”¦e{òÙÓFËsçñmí^¿á7o×êö¥pz¶ÐQ¯žÇýT/tž=(oß0…#¸/äûFI`ªGíëö¹ž¾³Å2Êí"ÓÛEA«DP + ¢ŸDG׸Š7 g?•Uµà‚ÐÞóãîˆ):šK5úzv²ñŸä 2NÄFcQ…Û‘>\˜n’·F Aÿ›•î‚þèÛÜ¢âqÙ8Ñï#Å«ŠPìÜt +a‹Uîà4àþyäÏOOïM8m¹bN)Þ ûmƬz ø¡ªÓÎŽÜBÇYÀç9M‘ûz-q6:ýÃñÛñU-1f£µ?s¾=mŸo¬, Û3HX!é7‘„ ÂÔLº_%‘Þ æ?õˆÐÜ'²¾/ýŒS/¯åüÎEÛË ¦D1Ž-ÉBûÓùÌí+ô¢µ``ahW³Žfæmµ¦½PÅ[Òðšc• €êÉf Â^Ì»ŒÆ͵b<6l¬aØßÇ% +_1hÔ˜1œÏ©?ßy Z84Ít´8{yÐÇã‰ËÅ6ó-Á‰ÿ†’H™f”v´ÊTOÖ”éð$Ö °D¿ °?`§¢`–÷¿k†yÅT}8€ãTuEpöUÌ`—´Â@uˆs0$ Ö[$;1Q/PO”·Üؽì¾gƒVÅ[¼ÚÉ¿0Ñ›v^q +S˜ý'LaØ@éÁûjˬb>(*D¬¨°ÓÑ¡¢ÅVØnÇ~Ü š&5èàý…ͨt#¤Ju5NÊÿÞ‹ƒî¢ƒó8"æG•åzõ +%€ç›•V–£àR–¼t²#ypÏ!oíŸ>‘-±BÊdÊbB5#c°·(!é¸úr˜6(ãÄCÇS¤›ÝÚ‚t¸ƒÂµk‡`èr²s}‘uô©hÂì¡ç‚où H»›îâcøj§ñByø +w0¹ÊB†»hÝÚê“Ù“g U;g{Ü_êv쬈ْ*œ…}î{КJbš{Ô—ó9ÜpV%Rc:²W3 +MÔK4¿¡"‡Âù$ˆÀZµ-†V‹ðËŸRˆ +›P¶Ø_îƒ %5G±€Õiqíõ íy…&*Ì2@Gûr==§zårØ2Ó•©.63ò­WÀ/lÉ©ˆ´Î8¨þõ`á"g÷—cØVŠÔ–{:J¸§çÀ8fŒ®Úû@[g‡®Ù¥‹ëL¬4vQ¾|¢}ãnûŽ?žOÜ›ŸWŒ !Vr î ±Ê+… +kîSRžb÷´Râä“_÷‘U!qæSd‘ç><É«æÁ³”Ô‹ÔÀÎw[‹¨¾bÞ|ª ÖWÒÀî%ŠPÙ8]¬ œó&ßiØâòþìQ­YwÔs•c@*WPf$Å]îÛ‚×ö£¤@‚=9%•þÕ–šm*'ÀÕ÷›_0kĆm"Pwo‡1Œ¿FÑ:{ ÆMâãu4ΰ» +†j˜)aWÒõ÷,€Š«È¾Í°j©KÜ?·(Ò]gU¼ ÕÓêzrpI8“®¼pÄ–t¬Ò–ÛóÔ±k…þÞhçT4—Ïg†ˆ·TKìßjÙxvÑ/1ôšÒž(\b†5rfûÎÜG¥Ðå¼ 6ŸU2H5ì&öv°ûÛû)ÅÃP£ˆ}Ôª»2Ô±iDË¡@hî*Dì%Tè^%„ƒþ´V€úƒÔešï§LbU–|¶=>+îk ¸L´ýî„Íw›çlW®¿ÞmÏž0”Ätœ¸ÎV@X°»b 9™ VöðhΈÞk¾ 6 j`Fkz'Ñ^ï/ÿ7 (”æã'€Áª˜*– H lm°]Îú^íÑ*lˆH](kÿ€9NãÆ1ª¸Š§-ìfdÛͺ‡œnÁE”Ôxù %}Z(\ù«X_ ~G¡b¥àCxöã=[;ÿ”LYu:¹!Q c²ôÇsÀ§‹Â<õq ! õžv>:,³°·|Ãì-óA<¤ÌW ¢²Ó Uó(Uܺ&­f8Šå)äWºwì¦âCµ—âzè@ !G^=bŬF¦îçEãÀé.{ +4©þdéÌÌY·˜é8õƱD¡KøtÀü‚ îi6ÇVV.F^èãÌ¢ÅGmQÞ0édÉÙ3¶ž÷3Ú8¹‘-wc©:yÁóN|ÿ¼%ŽÛý|Ë~^qù٨ֺΖrD—MSDO¸‰eÑ Œ¿6œ¢Ø-—'>¢ þåÔ „iï;`J iªÃ½/4QÚûñ÷GÚ®¶#yZàóCîóSÑH8ÒÀ"÷P]àò@¾‹Ò/Ûv×8çyPµ¢I$â%Äî?µ“=´¢?/@WbGØZzy^jóç44ì=f"PÙ¼í­å6x4]8‰¹GVŽÏ…rðŠ)¸‚РTIaQ‹ ªš3uÞÑ ÄÎÈý¡ð‹06Í)l»©¨å\ˆL‚ã“øò˜@­fÐb/el¨ñ"8çßg_™^íª^b€5ËÅ)á~×f¢È T\Òµ{xîÞ>V3qé–6ê¾Îc„ìQL +ùûÞƒŒ‘ ¬9?çUôFA‘ª àªq3 [®j¿hã7šêN@LU;]ê¨&Âbs¶þl`°O}ÀQ% ¨¤\'2lŸ*^U6èØB®úÉi¹‡r[Ñ¿h*ûX²óí´ +KÖˆ8—¥2ì©Uä~IV¸²¥dQW Êx¥­UÿmjÐ.Á¡w“’€zTD%I@”lò(M×øÒZcF¥`O‘ t:¾®ÝªüþÿžƒTØÀÎì<­YG@ÍŽ™ýÆøe|‡ºÂG’•i?ÑÍ g¡ÆÕ0LFu3Z>×G¤M×7GÄ ™)E.’;Äm ¸¢ÌžÆï…ì£Ëƾe%ûñð +(¯ë´ Yd.d Šýè­„Vò šZMÁ…ÿY4<F®ß5HÆôäí™ 8D@fŸP± ú¨ãÞÐéÎÓ¾Re„ àpÎ[ÄÅuwE^cÿ”Žº1œ3õÌoÅ”dý=5#rI”é“ +Ôq""[;#iöÈõ¸B§»Ýþû‡çA¯‚ÄCŒèu’n&RFMÚ|8"µ\·ëi¼a‰Ö€òŒ«šS¼KpT-ôh0–}Ê"ºÞ$‚]o“A,Œø¤Sþ%†Š€Ã} ªŸ—)H’6øˆ +²Ü±à`×¹—1Ru?R…«½g1}Op0(ƒ’·’鉎_ñÖ,}¡êpwš+/ňƨë­NjÂã•ÔŒ‰4þ”ÛXQX™ö°"ú ¢FB +Ý·#û<ªÖƒ‰U÷Wý\B¯H²…¡/ªù³ž4@ñ.ðOV=gD¹l¿}Õ5À„–æK¥%õg¼1§k7ú ë".$ˆN½Þ{ýS¡Î38 ?#ä3Õ× +gùü¢;,H´*äs®~L–if‚÷¨/”TSðS8†ãrËFWfóNN±Tä›íçR! æöüCý}ÜyÇ•™»E¥aY>þÔû;aÄ~wVYžûx]go”+ +9í<¿q8~|PB¿ Ìý!ÎÃL®R:‚ýÐ7¶¢Å5RÑóŒf-i‹­+øꤟ*•T!¡àÆ[ôÆPÀ×Q˜„Jú‚«Jn…é¡û¤ñÖïá®G…pïB¾1àMBiÊÎë<äv†å˜xdä4],ß$[X8ž »éɺ„`V÷òŸà¶ûÃӭΉ~Òh™Ù`'õ +:Mð¢yËlc÷š?å‹Ö—ëÓGôQØÞ·lÃY‹µ~\¼=/Ę«d†¡&tÁŠ»¥hŸ›\T–ãÃÚµç&‰öoÕ]…ÀvÎhØÕÔAd„ó¦\æž;öª8R³1SçÑN´?hMÖF.Å~®æ+EÍõá^,&€&•PøÏ“PC?ŠÒp͇­…  Ï’J0¢ngâzv¦šÑ ¼QEø‰ü‡xÇUs¥êj <¿çJQîÙ{¥à˜5¡‰²„r1”ê° P‚cÿê$ƒGàíyzÑÚS3g|j”(ÜÏ¢¾äÍ=71¢m;ûqyð\iá°tëtGct³ ”ŸöŠ¨åÑNˆŒ;âÝFî]ÖõG8%ÓºŸsÄ»V-`¸z"\"ZO±{\ÖI‘tß½÷UEÚû “aóQ^F—ÅÁœÄ:TE¶ VªëåÄ\èfÕ•´õ4uÔ­XÇÚ͵˜§Æ~Hê Wp0¯ +·åÄcÛ‘zy ÙÌ…2WĪîžÓ¸‹¤¨ìV‰t –4`(šè`,ö±’@kD€#ƒ+ûaSí½Ý+Ïvn {âÎÆ-}]½ú…Áø´!Î Äd½?AÕž&µ¢TjÏîÝÝy_)ªV å”š‹!˜9Oœ™°ÝÊ£kî¿Õ5K€þÖkÀ?Âé*É,ßs¡“ºŠ…Æѵ¾XËÙw+ÆÃV]+ÖO#ŠªÊTÈK'àbuÓ®ò~Ë&%p¥Æ¶½Ú“P*°ý{¦2ZS¸n2æó¨@ZÔ™º²lÇAN‰mæú*~¬ƒ÷í7B ±vV½¬Ò.]‘kŠØ"Ë43ú—ýÀ^¸e4eê7Íê~ñNú\IÖ¯òŒõÒ0åÚS +Óms‘œGt™ ]%ª–íMW³­ÈõPÚ–¶°ÑßÏ€ Fú¡d>Â]Ô(á–mêȈ·G¶@"'ÃsŽ[ºF¦7Œ û8´N +mÐ:Ù/tW^7EADj +ÞQ=u„¡±a?zuîM#Û3¤È@,L<_¤mì“^‰“ëÀGD?¾\ô†cF§F¬˜“W0—ýüvdñØ£ðjõ5VzŽí^Ìz;˜U[}“Mm·lÑ`ô+flî42µÏÐÁ¬e£j27è>9X„€oêxUìiŸv«Œ %öFN2¬Îº¯c0é…÷8¯]®˜çŒÏÄÑ Y©¥‰Ôu(o ªrÍ¿k Kû¿–h_tÚŒ¨ºˆ›A|ÔÑØ—‹]Ýï€ +"!Õ-mÄç [5ÂTŽ/\]A ½ýp1f;òÜ­–r¿ˆ|ܦ«EÙ}_+ÏSLF6ðN·ò†à‘ÜrЃthîÖsŸ.#ôا Òèœÿ:zÙ¢c[¨¡4[ð§6ý¢+l Œ>ßã5ígóË\wZ[å¢0S,ÚGL>d¢rŒ‰Žc{Ñø»‰Ò³ø²ãTOæHÐuÛ=®G=o/´(ê{7¸™ÎÄ +ãd{›wÔš÷  rè5Õ€†‰ÖŒàxT0ÉU«ùš|¢.r˵§ÎL·³çH8³ìïù£ÇLugɵæ©P°¾›-AÊ$·Y¦:¸T±µå‹¹ºŠ&å¦Ö¬|á$M6çÕOUÙ<²`E½®-}w[¼‚ÐRt²·dˆ› ¶(µd“ÜðŽ‰Ä¨E̺—’õDHÏß>Ú@.~/ÛA¡šÞ›XHä±ã +c?5Š`¦–a]"ŠmÊ9ÓAUÝN}¶ÑíélYõßK_èºì JâZQ,£šÖâ5'`ÙÆF ÿáì|¥þŒªÆ•‰VPÍ:äÜ¢æ ì|F¸*XŠt@PÐ N•ÿ'®ápîñJªÒk7¯§¯¤®2t² Š®2…)˜ÄÿjÑÞ=<°¯ƒÂ†ð +û:ikäZ[¶iÈÎœ¬~Öô´UA\Šã¥Á† TNb³Ov àù†£†{{°(ƒj·o½Ë [)äröVê_Eý-áMÿî_õý+Oôň3ÿ+þï;KLq*àÊT>Bê¬Á˜±—cOW†ÙfG-J×_¢|<ÂÍ)©T£¨~¸¢óÜRÅɾ¤Þ»qÓòã’Uˆß¢;‰‰ +ë;fm„YŸ{u“àGmT­FÔ³YÝÓŸ`éz Ö;¸>d󉇽‚˜ÈV£îã’‹NÏS› š )K`ƈxbw/í)$Ðάôõížæö¹mзêá%ëŸmrSK°2¬ï£é((¡jíépGC€¶ø5¼Uu|–Å{Ž$)C9"¦‰îö™[À6‚“Ê_´jeH²_\/_é½!Àw/ÛN?Ôu‹•DMòûm|uã´p O‚FùĨ͘Ç×¹ >ÏŒ²ItXÇñéÔ"÷íðl{‰×Ëwò÷ò1gžIÏfÉUõ¸£ÓŠYŸµ')=Èfúé‹ŽpݤMìÞ£ø™ïßg^"œ\û å晦Æça¤Êî¹@~É„ú~^n^å>Üž@7&‚–[ÉúØây5§}*|6¹˜Ò+¾¡¶Í¬™w@‡UÆÿýœ……¦?/ Åa…TKV–Ûq¶Ýáš 8b’;yßDÝ–9§ŸfŸç¥Ù ézý›Ÿ!ÒÃh=Ÿ+}‰ºV–i|íi¦ºÚ‹h¬¡Ôš°•&Þê6#ÙYs½žÏüpT‰ª4ëþªD_·r-=2¶ˆµ_”e£¦S; hŒ¾½Ó"ÚQHKÁò”ô‰;4f;-¾-¼Æ ¾<j¥wý4ùDȶéwôPtŒŸ?謗S4ÀªŠžOwƹ¢Q«t9 jW í¬õ¨ƒ“ÙÔç[òy“6c»#/ÝŽ•ü‹{w£¼£æ]R‰·ñÙ”†ˆÎŸË-çéwL7ŽÑ %ûD„$Œ).û+Ǽ+hisP–XSØÌÿFŠ cnc[!ãÍs&&"=ºÊ«›|ÌÔö§}exkUêxuGMVÓ{ŒÁ–´Û†¶>þ/}¿ÿ-¿FpØôïüÓµU™³=Å׳ëÄ ë}),< "ï%¨i¢#]{É蔼)ª©”‚TA\ˆòO>A%Áþo?á;EØÿ÷-¡Il_¦ç‰åtp"˜g¥»DCØ Ï€ws\Èxíë…”Ó“ÖEUvù›î–A#Œf)e6úþõB?ëÔp +óªG•À†y œ_#²/ê§åYï®Að¯Q¤'Mö;~ŠTÙ¨vÀiÝÙwÀCP/ÅÊ7E†¾éÕëO?™áDááCÔ> ¬Ô=¤K\\œÚÑ&Âåý 6s¥(_,Ô9#Ý¡ºåS²í§æG9šâ0Qq¨hX¤¹á†jaÜõä<íV•×DthþðΕ A[âg«+ &Tá&¾ó¤™á¥Z ô~r3¨ºrKÀefÓôJŽ¢öô=rˤGx\½«(}u @æD_í¡;µ{ujç{.†ò×lœ³mѪæÛxvâ +Rdp.TõU$.˜œýöD=>ãm—ìÒ.Æ/8] ô!YßD©l zòNvèY(·ÛëOvô9Ȩ¶ ëÉŠßÒ5öƒåÓ‹¼‘²9L_£Š6Ÿ…6£¤—Þ©•ÃâR…ã¤Ù3ĶûU£è¹ ž¯³™*¹NQQ·}Ü–2Ñ´Ȥ&Q¨¨Ò=±°Ž6X8jhÑiw1+¸MŠ[—ë˜)!à¶?==ÉËfP—£wÇñt®×û"÷I€Ä@Õ|5Ð÷×…X õ¢[$r• þZ2üÉßò…EINå™F6ž1\J¯½÷8$é©®?:pª),âTÝ~¤òÉ3¾ö'TÕ'É„±Î’EËrãP1 Š9àODë¾~óWúQf¤kÍ=3ÂßW¼[Ccì1Ÿcˆ“ÜЂ€¾Ï=ÊÖ± +]g玥 +¹<Š·xú}9ðŽÝ¹‰’èúæÏ)k)úwIiç6-PöY¿7™.‘·ÇòS{B¯Û1-Rô™;´¼s¿ +C`E—º¬ªžÁ´ÁsJ•U=à3aÛøÝÁHœ=gamWÖÀŽð/UÍaܽ:„ˆ#"EPY!xÊèâ¿qÔ€ùºó "¢kÞ£kþO%%¨¢èóë_µÜT9©'tÜÇ›’Î8r >ú#z5©m÷ÿúìná\jMÖå½Ì›Àìç´W½$®3ìë‘>£.j·„ùõWÁý®k×7Jüïð@!Uc`¨`2—™Ù¾ 78 VléW?š`>ž% å÷Ì2ré}ÙL+…*Úž®c„ÓÖºßí~ˆ¥z¿øÃÛwkïi_£ ö…Aˆ‹"ù8ÅqòqT—¾F|7s‰ºmÕ0#ux„áòE+ ~ܸäÇ- |´>)Œ kŸ‡ñˆß5ÿßOqö”Lk8r¡9 ÛUÚQâÈ·CìÓJ*%}Ø"Òn‘¥$òS€'²½Ž¯( ? +öì=±HDöö%;|9ð/g‡œ-ýZ½•ç›?ßS#X›‡›B?C-AP´µõ`{sïÖÙ‡ùÈTO‰½½î +FÕUÞ(ë †Ô9›K°p"æ‰Ðõ¼4V8JdúˆÀ±7W:vΨ{0HļBÄCncªÓÑx ˆi¢ùæèm;àf&6‚íó)ò{U«©Ué Ý‚µCX6Ïòú­ËîÞûx2,*(."^‡h&GÕ²c¨9Pæ×15ÔƦïÑYR┫?·©ð)Ïís£—¬O8­ êÂýŸC»¼x¿ÞuPÊ FùDKŸ¶Q=šDÏoˆ(J@QˆÅó°Pîó\6-ŸƒAA³ýŽãY_÷çKõˆÓ©ï¡ÿ'ûÞ#æ»sX-i*yÖ8.ð: È\c‘x+VøÉ6X.×àfä­ÿÅôScŠëú°ËÛCaC»qy_{Õ3¼ëÞe*W³il€ìö#kŠ§þâjÕ: í~4¢¾Fì'Jo %¦ù–›š'J,é¯U¤#'ظN#áÖ±5°¬=%`ÐÆäC‰¦ÃRh(f.€­×êGÝ_B«Ü§™åg÷ãiUöá×hfÁ$ÈÚ½¿Ò.ì}@PÀª^JÍ<°·ÒØË…mixô%Æ2Qh+²F‹;EU^S{î–"õ}ÊHDÐ%±ªç/Åiaß~’ÍQ£¡°¿£¢Tž7Áx¹-bû×V»vÄ͈""*T(5ð s†ç#MTŒ=¿#àŸ+õ0CA/ÑWõµ,.·ÜÑ&˜µ$Y ra &³ç|1áÐ9¥)cžd}—î›pðÙ|h ½'Ç}ÞÕߨž(ÈED4 R;7w¼ŒÕ~@jJ ­ ¤½@½©×ˆõçùy%wÊ´Õ$Í=°ëZ. í_ Þù)ªÈìÚ@FtÍæøñ%Aý)!S>¯G<>úÙ•½UPæ¹=¿6¶ÐEòOÑM„"¾-ÝYÐË×þRÿCÑL² ŽÀQ…øòê|9´;«/TXÏkn+†¤|‰Àƒtß»'¡`ù÷ ë‘UÀ[àiŸ¿AÜDè2ÚÙÏ"Gpò¾D0þIàªô´—ÝùkXª ¸’ÓRò´§FS¦¨ŠTŽj +PÀ[  ³ÑÕ!Ç`t]EO>"*Ò™͈w>"Š¶ûŽ²j@”þ[-WΪb">Ê!…«Šó:÷bA¼[E~©ÿ÷¹R‘kó½Ç„®ý dÄ°ö0ŸþäƒÕŸŽÞ³ޕa%m6Šö`AÉ¡&ÌÈ­<1¦0·ØïMV±lý"kÑø‘1u$à¾üùj‘÷î¨^PÅÀßÉ­ÕÞSÉWVšÿ-Xä>7ú¥ûVÏ&ˆ/šàª@öÞÐ× MLpXk5¼…½ùl3ÏÑjHG*ª¦oqƒ=Ö‚ßæÆ$‰±ÓÊŸÒô80®bf¨WL>æˆQEQ\n1„ý=ÅŽ9NÀLÀ;RÕ;éÂ×}•oNSŽF â^æ".‚]eá/ååÌ!v²^ ñ—¨+z¢x™_^G§aR4âv"ƒ¿¦JÏŒŸôäQÝ:jì=èÕ_d‰^®`´0ꈖs“Ø„÷ü¹VÕM3À#L(zþ€E#,QÁë…¾ÅOƒ•ØÇ,,â ¢hÒ=â3ÿÜ¿eð¯“^_éÞ²n.E™÷ðyÑòˆ¦@[»¸îëÕ߆ù?G›¾E@]±kÇ9Ô©U¾‹ 0”¨¼Ð«{o¿Dõ£ ÝûX>Ãg¥x¡’/Pð)¡(ª jă¦cÿ‘ÒÃJ¦=\y£_£:+œeþèü`åW”pO1‡ +w%Å•šçB¾r¾˜y`9x%ê^‰ÂŠäïÎý<1ôœnÉìùÝü fª>#ÀÖDƒ]úç€9´ÈyÕ?ªqpA¢oj|L\Ð,ú]œji{a±’¨q‰J?·Gð¢pw˜q¬ðÊ­ÖW‡/qì©CUǦÛ÷Ë"ê9¿D!ë@Ò†®Ú +ňêZˆÝ8“×4¹Òsrö=ÞF™GкGÐzÂëèî€d[ìܸ²¬,ä6hªÏßÒ»Ÿ ¦¤¡Ô³¢]J=+œ)Ðûm©£ñ-«Nà¾ñEê¤Z?098W¬£Ñ嬋àR;Pd„š°wB°w©%0#¤ÍõÙ¯Š¦ÔŠ?ýzàÀ1R+øåµg3=Õ…Ry¸•s2ž8 Ïö9ÈÊ‹} Eø†‡Z4¡@³ïȼâÑ‚ÁÜ/Èþ¤êö{7p‚x¤ŠW/ˆû5'™‚iü«Ðœ¿%þGîí¿Ê¿ý+OvaÛÿ˜`ëÿñiÐüø·ÿÝÿóÿø×/V~ü÷ÿã?ýÓÿýãßþÿþû‡þçüoÿåïÿýÿó÷ÿË?ü§ÿò÷¿ÿÓýÿþþŸþ¯¿ÿŸþãúçÿù¿ýÓÿû_sÑïÿâÿÇÿúÿðÏÿøÿ~_âOnîùÜÁšðÿÊÿïö¤<ÉX.Í ‰5:÷M±¸=ų¥ð{&9ÙùÀà÷*öõŒ¨ÂìŽ Ñô-ÍVÎüù/>!þ +õ ý†ÉìÖ·ääÆJ;lGá}~469ŠÚ—›^_0ZÕvî€øÔâ‹ùE÷FD§ÄÞt »EzÃW3¢ïØÓ^pK|ÁbŠ¨ ±iø>Éo^ï~}ë]È~ zŠìý‹1Àªz²aðø9 >õ~[«£ž×õËÜÿÎ:ÚýÍ_QàvùÙ"<>.–¹}{·.Lã:~jìzOeèº!þßù”sö8>/C¬Q¯ÞˆØðÂŽá½ôÌ8{LÕË[¹‚Úß+B0‰`’šTßúçB’BqfØDg› ø3ž¦ÑðÖVº$`§Ö°ÿ•©‡BÄBûȉÿ½L—¦ ]¾†ù]CQú° +Å_4`˜”ö££‹…4N“ÿó%âq4Èõ-äÅ°÷Ó¢”Z†Ý .2ÉÞyÿèl+LœX"8܇6^é_"2ÌÉÞ?¢vò®oÎcJÅè¶Gö-4M*>¾½ï± ×R܋¯”þNËí×ìÝÞó@ê-Õ¢ºâsH¦PPJ93x“–E4·3»ð}<…–êÍ$á̵UY>jž€KEUº)¿HÖŽÄ[#¹ªmhkÏwÀAŒàL̵}—ðÁ ?sƒ¸£ +Hu¢øRˆBCÐó^Ãì¹s#M â)‡„´xÌ;·‚ ÈØžo·bKŒ?Õ¬b{%jHšbóÍv¾Q Š%«yƒ'Z@\NÑ7š2“%á¤ô%Â+-‘Û”@ê|sž¦´ÃÈÚ¿|oC[WåÂ<åJžRÖÅœjžr™ëܯ,QÌãQíb'AÉØùN¯q ¹<ƒó‡rØ t€`ƒp%`”$Ðv‡úÊ@sÐœp „^( {S$ý_+"ï1òQÑåaSÍ;S•wX}_+;ÚXÃS2B"ý&­}(cïI™?yê•vâ>@a…Ý„"~Ïý5 Àsëw“äØï~9lÄûb7áýšÛ¡|A§_ʇ +îT‡õ*3– +aÅ`x ìF]RZGpÈkÛK®¥ç ïE¤CÅQâ¶@ä& ñ„ã­ØÞ|"µÂÁ_k†gP•]4Ce}¦ÛH 0Ü¡ñºúŽ9?£D¨zv‘À½ÞdìéÎÏ^–Ë-#ºÚÅÝINV{líI‹å%#jO‡Ï-˽ *e[Á4D@Ãë´À “å n ±Ø%ùYR÷ÅêñgÅà^•¿ ‡ŠoŽCC¸Ù>ÝH©Ÿ1õ5j˜ —|Ò—ûÍØg÷¸‘2ÑNMRUüvÝ»«+#©û—€ü ?ö’ûùõ,ô¹è_Ó./Þ‡PZh(˜ عÚË¥úrìVJÍGÌhÇH ‚ÏÅ®aL ê0 zÍ•¤€-j9§ÄÏ•ò‚‡îýžˆ®35 Ýn‚JT}« mæxŸkøÜ aͳ ž¬wø4}’w[(/V£´èR8ƒù_Êý`ÿ¾×†;8 PhCDµ&±P‘Ί$c¿ÝVU—Î7ÀdŸ/`Bzž€2÷!IæÞ†e˦ý~¡¥?¾ 8vkš½©Ú—¨g(Rê? âÁ CiãÉH÷QñQð:`¸Qü@Ë„~¾Æê_"RD¡ù®L@ŠöÍy€ºYã¹Q¸¦ÖÑ£¤MuøÕ׈uT½Éƒ>Åá¯Q +t¡=ôªöyT„ï+û î×HåâÁµ»RKºÙ|Ünò+®<ªXuïü-n7\£âŸŸq@sc?t“jMc ýúe¤TÁÞÞ£6Àªçõì}éQslÂ$÷üÁ¶Ò­Àêé”],"Ýn)ºÉ '±ÑuÕK~qÃÏœŒ”Ý'ºtþRwϸ´ÜEÉýtȼž'+reúʦ¤^ÙÜ¥e4Å84ðTþ kÁ&¿µÚA¬ØŸ|!EtÞ͘Š<uaCM-N¢ºu‡A·qi>£—’Îq{à“(½Î¿ƒ<¿´äy>çGU®Xvï‚dœ»ÞaËæ2Ý»P5ÿºPÍ°Éñ(qV+W‘/´Yß_w–Z¨&[ùkem5‘d³ÄG¼nM]&M,„‘ËJ~òç3¡w²3Åß@ý¬®¤)ÍNÇ.ëÔƒ´–”©ÓÄb½g`#¤$ ŸÂ½ÐV ã?X~i I·”•y&sä^ãµ{³ªã˜Dòíä¤D¶¢ƒÍÜÊMkb`Ô¨žÇb-k#X­¹”¶îÞ{ƒG\S?’h| Ð<¢’^iZqaÖ=GÎOæ9p ¹rŸì0ˆ¨Ð´¼ÇVÑÎù$3Tð +CÞzê ]íÓ‹›@ÓÛ¬¸_T[÷àébŒöw1øiT_‘J§f}1)ÁAÂò„Yë6˜YØÙãÍ㦮ºÿœ&[24åÉw–Cà ܬ)ã×´ NbYÚ %Ýà +Õ™}×׸^†3»Á=ѱEä•(›yWu¯ki”ué/ðd…XÁ¾›ˆø bÆm Z,ýy¡+‹‘T› ˜å•ÛìûùçÈò¸\õÜA¹×ù!è/Y dHÀ³ýü€™*@דO)‰<0ðõ8pQâ\‘ûo&$ZkÏaýkDðr –_âgìw‚sËÍØO„‚Ä~ê w~4 endstream endobj 24 0 obj <>stream +å$y5ÔžvFر¨ôã˜Mëµ/^ sµ%ÍÇwç¹´ oмåî=b…Œ-ªí”a:ZPÃj­nî¶ûù`Å€Ÿšs6çÙ'Aõ|V ”Î’ÓptkH¦4é +uÑîÌcô‰÷OB›híDz.©§_"üI• >>,™í›Ó€vÍÛ_ʶîue'r"XAþÝßDƒu÷¨ ô®?‰©Í›CïIµ1H+Ä%šÍ@QiËÒ²¢Lzh©Ã°}˜3à…®è{ïg·W7Èu_Nc©±S¬‘Ú¥ #áêÈñ  Æñ%«á<¹/QôÀ# <¨þ]¡ÇÍ Œút‘EQ¾!Ý£ÀË‹q ú%âllõl ×›oÎðËg«w;P.èU.”÷ºú ©?åmƒIÍÞ{SÝ>WzÎ<´¨¡£äPÌßyLÎãN1Ä-á1ÊØû-/#ìR¢ù¦šˆšÈUxtës%»#ž‰‚£q9szš›â ¥XéJT3À ¹ù(*Uõü¤dM(ŠßïOBŽ•Á¿')õK˜R(²Ç¹¬²AHdD*µpñ¸Ì\ÏzÇÛ~Tçk7'ù%"WBr¢~÷ç$g×CܤJ š¢ƒzû&àX*^èའòרˆIyŸjðS»À\d~N&R÷ê†ZXZÆT³$ùÿá•0IwÅD#¾~s`k¬–:cŒÜ°>îבÛÿ…Fd<2r¡_¢ZÀÏ”|M˜½áG¥Çº•;è.æ£SϨ©(Þ<–=nk›_#Þo4õA”1ú× K¸d”~0ÕwÌG¾þ³Û+Ìñuã-sþWʯIVœI†XBA€9ªüY,\„¨ñ ötZæú5"O¬+<±†žïí›óT +jH:v½œNVÖOVÆ«Ov¯c¯sà6I2¯Óç ØWå {~öÉ}úç`$vĘΫïÌ€ýt p—¾âfÊ),¨±æ~Ö›GÂ*QåA)( Á ížÍ0"®i íÐ@öGEýGαŒ€!eÄìï/AJ€ ¼á +Ù—R.ú^pÄïa ä('šh'ÿå8>ôÖÁƒÇu¡ìô%"kÚ²Db³¿­oÏóœ/j_0}ÔEtÜdKðMÄ:y±Úë“ËþiRQ +Òb^Ó”e¶EW{8Ðξ +8èY~V®ûê(ÜFè‚Shí¾9`{90R'HÕ6ð]½zšÎ«ÚT¸ê‹ždelU $n¬Ïy³:ܺÛÝ;¸®:¼ç%›äÊ +'âi ¾ñþ˜púôÞ|h>ÉÕn©¾°Ñ›¯S:½&w'ÜdÉëݺ£.ì£,Ð +K5ðv±@ß ¬Xí!ÌõJüž Ì;0É™¬¶€¤lçÁ^ؽÝÑ`Ý?•çò9ð¾;[cøÞ(–þÊ› L")ã?'Å\U$ns^Ä~,´^ˆØ¿Öå<ò} ð-Óg•¥@c`Ô>ýË5x`³DG8ÅÖýÙž¹GÚxì¤~ øÃÛR2gWå— Êët£÷öxïGGf•oÙñ/:ª_"¾[ ¿DQß”ÚòÈzDÓ!àÑ!éØ—.1ÑDÎ ›·”Uì`¢!'8¯cyç´n˜»û4àYés6]lQ“x.좭yâ¥HI~è:÷£éj›QîÛ"¾ öÇ;¢l#ˆêðLV„“½bPE´gûøËIW¦pÖDŠ¢Ÿ¿Ò'RÍ[-&]”Ù=ÇÞÍ‚€¶iøDYñó™zØ“5L--S²¥EÊÎCc«š*fDXuÎœžˆñF´DX5¦Ã}ã‚Pñ$‚ÔI±h§N*”Ü^$YÁ0÷{dÆÁjˆÒ-Í5*ûD ƒÚ_;@¬›«³ÉA¼dæ<Ív¶ËœnƒOd^y˜’Ë=m…(û¯ƒFÈÆó9Os @…ìƒ.|ÅÂÓž´0Õ.ü°ˆè+Ðõ¼N› ‡ a/½«¸{woG‚¾mÏA$ÝöÁÛ–ö_tÕžuAMÄÏ=x1ÂI‚â¿u,àéfCSäçI\+7ĉš¬Ò‰àÝÐ>£A¨Ý»-w0üA »ô ly¦h4 é†Eüó– /Ђ{Ð>Nš)š[jûû¼RöAѱ€Þûõ²F-pÞh‚à«6hv¢j»çŒa$–Vßáu)ŸF‰ºR!ãóE¯UX,BâAqp<ç¥@†ö;rŸ_w© µËû…þ¤ßme›¶w·OwÏYp×Qƒ‚@삾]C/¬Õé‡"i´éãßÄgþß+À‘\©jDÙ´eƒˆñЀ¤ãB þu}²D­G ôDÅn‹=•4¢l#î¨æ»§·Êž‰OšD xAõDôt_AÆ­‘v+‰B½`=<Ÿ+u[²¼ýVÎýÿÜ-~hã:½Y íO†¡£§;oaØ‹ÊÞu؆ Ny»¦’ì(Cä P\/éñô(¡‰i¹í¢¡Žð~8d˜ìµ+¥Iœ?—îÁç: ¿÷`½=ÈÚÇ+¢HcàI¥t"–u¢~³#äç{LkûÊ»Ù4ªb µ£Š˜×ûŒªíN®4‹ç¹§pÜ[.“÷F l«à"ˆµ,Rï0K]Ìl°?MU¤ÜgË&vŸf'gÖséLóÞ—\×;tÎA ¾(Œï§ß9™ä++¯e[HëÉÌê¸A€v÷Ë¿§8Äøëß å›ø,Ù/ðY–ñ|þŽðNó˜ ŸçàXßc;·æ "³Š£XÔfìïƆpé „b?˜‡mjÚýþ‘F.²×£ÅŽp´ïÎY9_)"ùàD¬½f#Ó÷MÒ[\z>~~?‰ªZÊ^~þ7ýˆ>’Ü/]–_]3+¹Ró_Èà3¸™€ªkeþ|êûq…ºAthxá̳žFl’ö=âÀÐ0oºô¥ Ö:ùxw¦óv{Ç›Çàœ3ïÜ k˜6Á¼‹/ðkœß²lIOw°Êÿ`€•±œÊŸõ¹ [؇ žeÃyÈc ³2LÓ‘bë\¿dZø%mûz¤ßÄRÙ÷Ù±}³œÔjÁ_#í ‚áõ%Š¢9ë@ö£Ÿžç†O qZˆöé÷Ô¸ÓGrØ=pË78X8[0Ñ 2/ç%e "44wPi‰!æÈöÈÚÿó‚M¡N\|пì’+ݵƒxº§%êéó”ÆAÕSŸý›ˆvl¦®È‹‚ø…|K?ª{ ÂìPÔKB?÷}±6)!¯çcoOÔò#èCYO£.÷;!9éªDÛõm2>4•ãE’²Ñ Sœ·¬gbsF[‘ûÏ+e±ì@êõLîM*DJÏÃi‰4æØŸˆr_hß•D¸§æ^\¨ˆÐS`Ë×Å…ôCˆð¼ËE¶Ÿ7©}¤=lyŸµÌÌŠó-è°{Úbþ5â  B©ŸïŸDÑá§rÉéèèY!dbª +/§ðIzGýnÇ—ˆ?œ¤é^lö^¨PÖúrôœ),>E#Ë´T¤¶éa_"votŸ€( 5Sa[ +*’³£µ8Å?poÚOùPöìNú×ø»¯ŸIÚ +ÐLoüËyPz­£ÔCÍ}JUVÈof©éRö7RÖyÜ-»¿]÷¿[>@§Çìka:äµ°ãì +b#ºcž ™Ý úG ƒ]’×A£ès¡LÛ§ G‘— +"QW97Ì|CÊþV/rÀ=%àºi?8çäé~o…ˆçƒ´êïú0D{SÐ@£FÔ~týd¸{“^¯.W’Ó¤̇òkã9O†Î»Qû…²t|®(Ú´V³€-MÃ*ƒÅÍEvô³Øë3db¿dصL2{7_„ ÿz¦[T8J`ø€ØLÝ1PC¿¯ÍçJøÞ si¡·', `°„}÷l¹‚ùÞs’pýV6héŽX @"k,Ï`$é+ó0—øs¦BÜo÷z¶¬­€(•¡G%„ SæØî˜\€$„ºÛ׈¡š3mˆ=Lêרª¯·›v0½\zj&ü]¢l‚q§¸s¥/Qæ¹ kmƒÁßÿ<À@ï „#›NW™®Ác¿‡ô3僯Ûãd ‰±ÎÁ€­"þy‡Lp§‘‡vÊ †&Q8^{æ9¿F94Ð6ðFKèj;jÔ‘êD: mî‘àà¶,ËÞ¹—þšÝd9‡%‹½p]o-ÕjШz'‚P Ö’Þƒö7øJø«0ºËÇj9‘¨x¾˜¿½6%ߌºKý qï¹æ-~cv*ìw»¿ œˆ¥oô׈<«–Lñ™y€ßœgòŸ4šâý±ZsDb‡ +Ê!zvS³_Qô~(±Ñ± 3Ì¡ú»s;p4¨£’]:Ã,EØ»æX¤ö†œ/ú™éÁí¹`ÏךYÃÉv/NãÝ@}‰ÂªŒ\©2Ó$ˆd}ØÕ¨¶ªäW¢jÑ £‚Ž@ +®§ßE¼[5yÔI)ì~sžuh¤“¬=^*N“BL_#¾,ÏInBkG’ÆO5Ib³¸M«Ã‰«ÁÎgI%%[‡«CuÂ}ÊK[¨A›P¶Ã1ŒI”{Ù!-DªÉ˜—š˜É[ÀciÞéú^Z¬£Eq6¤·iC;À?WóâÚ®štOqP£P;Ée Dö "²ÑpÚw¯²ûiqWÙsàåï,~"§ -àqÉr‹ HZÂyR‚Ч5Ñ~{÷§ÿœ>JKICW¾‹_ÿöøZ’jt™À4zªèÚÞ~úBO%‡öö¶¾D‘,\/ZÒäî;éÎÐu'PÜš¬ÁN‘ šD{`²vé;©Þ«Å´ žiìõhR;î°ËæqŠÅzvT/¼œ¼ƒ¿€ÿ{‚Òýÿˆû.ýOHç42@üNØŒÒhhÀ³%Û8í>QBƒÐS-Mš~h OssGÎSU9ƒÅ]Œ“¸[ô2B‚]¶x·TC§.Úg^|æºÑñÐÅ’C£DÝ‚¶1ú3Ô © rÇ悽F¾¸eÿ`ø%÷ºFXiˆ,¡›[uª}Q€o‰¡qfÓdt•Ë€2å|›KlXiñ(•2s™›šý<×Aó7[Ú?ºEX¢ºph¢äú4rL¸ˆ¨Ö]†¾>èô‹9òÔ®`îóÈ|%Iß!Ô!Fê ½·Ñ“æTÇ¡ ÐÉ“¾ +¼Xx÷˜…ŸÉ*Z]:]ß2†½žý÷gí{;™¸Øsi »U÷[²ÖN í3u§”e„ ­÷H“üá–:˜œ³4 äTê#ªð\n´ÿ1ÿ*ž §øfÿµˆµ*1{—z1â@{hsDÉ–LJ=ÏÝ6¨RçÉ.‘dpW‚ ›Á²;r°æÂóMàŒâ4¤8=‘QÛ™²åå)N='búç:>DÚ#Ë š)Ðb/ð¦Uâp³+Û£Œ§yÅ9uizxaf'Jµ–ðý4|¡L-ªé´?…-ÊÀ%3y·ë4ÊåLeëoÃîÏF׫SÔM,KbCF6ãhºàÎEó:ا« ¡ SÎ×Cfl€P#Åa§÷Æé8sB4Ò­ž@6JºØ²¢Øa}:mô1Ì~ׄC‘B çw~'|böµ¸ +äQÚ±uí¸0;_y†Ž2w¿ñ4Ë€ûNL]^ÀÃÊŠÈþ‚ QØQFwÔný:¨CtÀf(#[aÆ»á|¥%÷FH?$.40u·¾Ë°ˆ0^“l]ô ¼0÷⪡^àž••íʪqksÿô;w‚§Œ´ SNú`TÛʨD^¡•à@Û©”NØýR8‚t,>ã=ÒÞZƒäí›8ì·ßÚA÷I«ês¼Ã`IÏWzû® +†ì}>š—¼G¼xùFï’?º ïÏkÛ¡¸Ä:ù,Ä¿ò̹óÊ¢F­œU§×@bìÃ¥qÕykø÷Kû¬útŒŸŸoÐÚBøÙü p6 çOLº•ÚñÖŸ¢nÌd)±[Gcxçá"«{¦öÚBbéCÑÕo[N¬-¥Ì®÷_Y/ª+û¡ÓêÈD›©kx +P’D jmD`1SÁ¿xž+ iELF 7ÔÕ‘¾œž,úªôQ«¸žáÒ%¢…J®ãïܧ˜¯gÄý$-qŒ9ÿ´3£ï»a– @ÄÉÐZÎ ú8»{kDž„~‡ öý%\§*'ö—öþ1ØèÑÊ'¤(i 9ÿ/ŒÎ%íáü·? Ζ)1ùäÎ[æ›ûÝl@1Lßœ‹¢NXx?O‰È]æ½7)¹ Êž;¬W +šMÙ;€'‡ÁŒK8‰ÕýJw')j')2&å©&_7iK+i•=΢RÒPWhµ>{sÁoŸ@|0ŸüU¾Ú°3lQÁ¸†ø¢ÐO +&ñ`ßiÁöq6ÿ#S{sF' …ØE)b×Ãpl‡FA28Þù° hÅvéÎA*cHŠÙ§$Áü*_ Œº¦yðÿ‰hB /”%&;ÅæõµÎ6Êu Bà]f÷éE"÷›³y!XjÝ d!§%×Y5ÛL’çÚz­cªó¬œ”‚åIî°ºj:E˜Š6ŸÏ÷Q-)É×$ü?rèAûCØnÚ¸¿æi_Ë^–y÷9+ßF&¶ÚI¥%ƒ3¸ž¨–DH'–€¶ñ¥ ·{¾´‚šmG'àæ&ˆÚAÝziUˆ€5z~4®,.˜ªFÐô@om:"B¡±]þBè¡ÐäZvêá=¢8b’¨Rõ¸»ø}]+ÏÎΓ볫 @sº õÃÎö·[+Þûð+Ão ?úàgÑ“H[» ©a§ÓZ÷{Ÿ$ðÏ~¿ÿ ?F!FÃwb[ö{);¿t"›~‡UŒ!T©¢XFcBd û#kZBjÜ>fSXØóȹCH衆»GšúPOmJ¿ª¥FYCzâ`×”ãA€ÃÌyÀr–"q•+è+%¸+=ñ7=€´ù‡_ÄÌĨaY¦Å©TÁbK|UÀ¦`ìÉ!¥y ôÉÈ©Rÿ+°]ØÕN£¸gC1)”#oËÌ8¤|Ør’ §]mÏùÖX°øÖº­ |x’·RX|Ùþ"÷×LSH¢¨ñaûîÞECzh¬i€h‹=?NýOý¨‡§@bÕ;Vƒ ©ÌŸÂ$îÙ™ !­IT!מ»L<ð´Ï  rÈ∞ˆÇº½*ãpw÷öƒô–ÞsVq_ÈÛÖ©å7E¾Ùey›šû4 jtËL9°IکקwuSé¼!{ ¿DËñS¨jp|©=“½O¿xŠ¹€8|ì~¢¬ÿï';5,}îWö¸–ˆVpø]ù „Ñl ×·!J;牲FÅâ)ýÄ&brc%lž=±“2°+ú´¬ê°ˆ¼YZ_ÓÂIÑR=Ð"¥ç) ÉÜ*ɽÐÚŸO°QÊË-ºÏNe› Šô…þaàcÂpõl + wXól¯y6)âµûœ›ô¥ò A%8±¡^øi8fkT^¸ƒíl»U ¹4ÎöÏE€Àb§¨ÂžÜ}NÀJ.ïü»æKïù/ƒ{Íb,º;.ÖlÚm‰ˆ‡v œ–g;×åûåºóü0R ñíµíŸ(´cÿÞ£!½°…üCuLR²ß(x  Ëc‰?àrm{ï)¿Çí‡%¯þXn¹Ð°®Ö™@eZUrÅ<6l`¼¬6·òó´2ÕwNCC&}"[]; C—ݪëŠwÍ1æäàUì}]lÞ›-|*h)ܯ¼Âù!«kå+rƒìˆ_#Õw¸8Ï«bDÕnvèï^IŠ[*ECËU²EAó:oâyU‘(ž7ûåÑ$±eÅZ'Õ›‹E)MŒB[k¦üT0|ðYñ°¯=âˆ$@²«%p6ÄâoïŽv™˜+¸ c³'Swñe’U!>Ö’Ù~Îçƒã½¥ï¿âd½ÉÛ @ß}¶“ôùåTàNÚîDŒö|®4!~N†>ó-Ä“e+€”½W¨%Þq!L¥kŒDàšü¾þÔÞ‘/ù„%¤$Avn0Nù̯‘Ùl‡£y€8ù%â~r×ü”¾@}æ°Ÿíy¸6=±®«dÔr]7© +²WÈ¥-HÝŸ«dž*½Aqv(Ã6æKÚk£!úñP\ÇD ªM'^ŠiTÖ€ÇÝ8ÜÓý@ö’]2»@ HØëºHÎèØ^Q\]è¥_!S ”Ñ55te½ÚögMŸ)‹:*IÈR aΰE{—a oëƒïß°Ð[‰DZ{¯²ˆ}ºÇèVf¬¹N)”nἎ¼;¯^I—R1ÞŒj!íÁAZO’p˜È~Jlðw½ŸÞ°§9ƒ˜þÔýpjèIâˆå¶ÉY(ÁŒš‚‰ÝT4ÐA^9΢´³é@À Ž!€‚…TPÓHì,åÈOEbÈ ùö¨ßC¶æ3i½–KQjPö-¢f)•¾Bx*@Ò`гûœ?«%‹’U¿Ø=#;¹¹ 5L<ƒ +òF +N§J¼•â½ØŽT@ÆËUbÔœ éûH—Y××(ÈãÒF¿;_Q'u+áîë‚–ò”Ù.¶Ý8À§Õ¢žš*kQ•Y(WJýk/(̬=·?%ß²õÃÜBØDt¹ÎäÃ+°=k÷[\²Ý›F.‚6áüm'ó§Þ} ²YäÓÁò £ÔŠí~Nÿª%5¨–HøÜïd°ÙwdzŠ„éz’aÉPªiÌ°ÿ’FZ³ÏlïzCb!¸Ž" 8Ac½^¶Q×!ô£ùYj®ƒ¢Ü’<®q@°ÁÕM/Ûýð<áj1<èÓùe–pù*»¾³¶¢LàG¹:N><.ÕA°:"…B7Mû@ÖÃqdÈQÝWâ¡ñ¯q¤¡òåܷɃև m…~.ûäÁÍÄDö 6¸Ägók­¿z'ÿ 6úaÏ!ÇG½?'óùGù¨æ¦]VÆãzL¨Rnö¹Fš-ÏQBVk±³+ˆsG܃dÙ;4ÇûTýé^©­¶êàvü lÒ¾ò2늚ÄYœY•w•<4ýkÒTâÅT°S +Ÿ6µ ò„ÔÝ‚~ +ÙmÃu²å£SÕ1ÉÇç±·ù³udÒ)Ó !œKfE:>Ø@DFH=BÂxêçJ"Þõ ˆ ó7~¹€(é‘Ay”5÷§Ýç§õp^÷âBãlÉ…®ÜìónÞÉz‹†(we‹ü4Ô +}ª»³p\Qd²`‚ê’¦ö«K²@{왯^w±â¬¹÷s¤tÀá­‚T¬ +‡)2pßåHïéô´“Gø„.w’Ѩ¹Þ„Yʽ†¶åH“U‘\EIð$k-C¶ýÃxƒÚCK:|»÷¥Q3“š«OuæGeòÌþ®…D!ȼ£8·Q]‚\­mžçUª;"Ìj„fÈn^ +vÞ§ˆ†;¬£í>Ù +ÂR§ê˜¬v¬VQµûôE*FÔÝóÐd·QžDD +¿ÓçPVïªõqœZ”<t>—}q=@Èz0’E° „# °­máñ³øÀ ötr4´Iݳ© +^o ß^ap2 wGUUÕÌO±»ŠÈZ63ÍQ€¢d‘bZ ˸[Ç™áÖxçݺ´«~€X]i$”¤UúBs¼ À:Dm‡‰-Åà0JHÉ‘-–“+;þßÿMì è Pa^ñkÉꔃœ«H|E"_©5àÆùøI­ï“Z¶(~§a¡^ïÆo*ÚF…¦E1ª…Ó ¸p\ètB¬nŒÙ Ñ0B_Ð:LfåŠKµ2t* (¾Ǥ†r1gN#ž¾œ@dBX,€Õ”§ÂÁë, gé¡ e²rª.0ˆTìÛKö%Ùª)G5Îuô? È÷2SÏ´ ‡ê’D+s=¥P‘Lh +_§ÖBÍ,áƒqÓþ +ÁS`¹ „0VHË÷cèAŒAÆs¡¿ƒIšîú&EÜPD'¾Hà‘äj±:jº£Et¥Z’Ÿº_E¥8¤Ÿ†(¸ͽ#Ûø]ËQTŠ1Ñ®72Ÿ+IVÙ¿]è*@,¯ÚÏ+‘É:‘mFZÀ±£Èp;@,W¼u½ÙIõSè M„ 6#7‹Ú{Qý+2ŘìÊYŠÌg4ùj°kÙ­Ø`ЦÆÖ»EK—’H*ìA@š~ñïg»@0(1àq+I~Êîã`ßaÿ-®$t_¬,;èìPƒ±ñt{šˆBPÉB“äCYºåëV†à0*ô }%O£R&§ÓC‹\BŽ•Àž$é:?ç5Ç[ð§jþ*/¯-UJ]èj +—?fg’’8`døÉž‚ò8§·€iø«Ø݃5kVÛ»‡hƒãÔÅ׋)z §­ +AþÎF{¹Gÿ²~caá+a÷ºnm΢%_BoËôÀJǃ_g3‡j*š±Üµ2þ(R½ÕòùÄ +£Vh³,{ä$åt Ù Á®ÒáwƒXgMòdõ”ô̪3‚E…nWo¯Ø{’>èæþ2eWš¼ wDk|Åvj¿ >20…³¨ê’’>Êz†âWJŠß+$Yz$äÇIÇ¥ø1Ñ/0u-Ba‚OZ ÊìrÇ(@ÆýŠZŸã~ï¥nk.Ðì‚{°(÷ÊšÛü+7³øÜö—Ðç?+¬0ØÏŸë°“Tì`w.Ú ‚ +o ¾{m ãh«"Ï`]‰úÛõÕyWè6 º¹c‘'ûUHè¥ÙÕBûdžD©KC”^èÒTáóx€ÜžGÝDû1/PS6’ÙúÂ﨩Oì?ë‰@“—2þ÷JW”úK,yD_…7°—‘ÓÄu€„<›QîE¨8ò-”§ Èr̘~Ï… 7Y}åäï¹³ÇEg¹ôìO_ýëÑO—ùÖÆõк˜ì¦†§Zbœ™ˆ„}€jÊ2ìLöqÊ NZƒŠWʳ8‰Œ ö¨½¤i®3Wµ»„}6,êZκߢ…¨Iæ¼í<)‚)òóäJ¡Œ’?Ê' £ÇoÅo›å2¨(ZÑFÎ/z‚›êO-)©; ¬_3…Ø@F~”2+8HjpÒ’ÙóRrÐì¤c k)Ép.ÄæȈ*§°ï“ÍsÉê‡á0ñ ÀÀv0=RÖ;Š€Pþf{ª9Ö€p2ŽYÂÎ]z>í[nAßwÎœ1~!3£Ïи{©óÓÃ…ÍÚþБ—ýÓ„ §¬;Õ3 i;oÃÅi¢%`Ì„*Ü~isËÞû÷ç<Ào;Aú\ÈEeíowvø;ÜìνՉgL9üñ$c/!"’£êɉ°éµÏQŽ'Äùu]D¤M‰N×Jžu¼ÛŠ!¥dnÖ9C!WïeA'>öJ´tÂs¥#ÏKþÐH ¬ÈöTdQ`& ^’ØÅx– +¡÷3éèºkú]J ´M„³bÕ™jÖÈTÉœ£À§àŸªˆŠ?U¦9Óª7…*>ðûwˆýäÈïJ˜žƒË_ H %>Zp)0¥#Ô"¿ò`É:¢:q’ïÎ*ó­Þ41ùdUzuP„vE€ÊÀÎ]”¯àÕŽV™úì:’ÄCw\<õ£µRöÍRxïçâÎxÁßfžòª+Â^ n¡ë¸ Ê["«S¸±…ÙD•OryK“m¼•Kwî%àZÉÙóÎá+B¸sr;˜e '0—8PǸ^vÐx÷FªéJɺYsMSvWôûŸJ…*Uf@Ñ•»ƒŽ.ù£¤—Ö?˜ŽÙdH¡jÏÃUqfý%怢8˜)‘GWÅ¡ªXÞÐ.XbÁrªqÙrt{-…3ZFÛc“íEÚzki Rp¦oÔo¨‹¨ej[1!Áù#üŠÎ¿Ü)8Žà$ôìƒî¦¾¢ÜøØSbÖõ=ͺ« ƺw %ûîYvšŸp°¨c뱡\¬“90žßÏ€ “cDŠ_À6W›ÍöC  +&¸!Fº(bœD°¼*Yît‘­FˆüŒè—H ÕŸªNV{Ù¦û¥÷>\È©„iÇ6,‚SH‹,•zI±ú³žÔK&.sŸ·2£3µ=€õ€Â$64ß®‹ùT'¯˜[êÝóçûkðPãxØý3¤ëÑh¿‡~ö‚çE¯5dúŠd‹ÈéïMÀ8Ôzÿ½dŠB¾„;jÙ.13*‹ïùm÷8¸'IÕAU°Çô¹¬ŽLô²ƒÃhŠ5þ;ñ&iì©~!C|3ä±OKF°?nPþöÓ(µ>²’˜±»w[vO€uÒæØc¢è=ÜÉaléÁ¡üÆuä7Ž‚ HL¬ú™ ‡râÿáli¥%ÚÔ¤o¶ÆaNÜÀZÂ˱%ìÆæ +´²ê¾_;˜@0‡š]oÏf¦ÅÔdù]@[ÙGÛOÚZæj +÷uúðÿv¡uhw{¨¸þÁ©\@HŽq¿¡…ñʃ°Šíߟ* ƒÃ·¢ž6U¶»Î²ÿ |GO¿ƒ1ÙS_ùGEçQ÷í8§ç½8)ÿ~ èÖžÇÎ…þ¢·Q{è—Óƒ…"l‹ÔóôuÚd{~ydÊÿ%˜Þßøïþëú¿úD_ èó¿âÿ¾³‚—Öw?24äÖ]Ä`èªÕB°‚Õç”4ŠåÖ_bäì9czF1ÃöËZæÈÐ8[ ö/áJ߬`~î‹y e¾²¢OëL¦CýáÃÛŽˆßÎjNC^õÇ+xGU«Ë·Ê—FñuëTCðêÁ¾3DÕ(Ä›º3ž£æÊÊQ9(/Ÿû¡×°£Š}RŠãN¢èX?Áê Ëb!õ9–¢"1Ø#$,ìêE‰ 'âzÝrlGD‹@\„˜«D÷ÜZE™ýÉÜ;–œ({2">nê¿bwzœo“ÅlGc©,õ¨cTN̉ +—j_½ªPŽPˆ/SUÁYÚwjJ#ö\‰â/ºo¬¯šS¡sƒ“Ë\^G( “Öƒ<›×ѼB±í®uŒD×ì &!@Ž~¨Œ_rý~r£ÊCWFø^aú1X[JÏd@„9,RU§x$áõe©³ˆ™Qªm@<¹@~Ê>«Lu"Dä)W²âÊ |¦»ßν¡lg¶éX6¿k/=¿½íè +npŠ™×öòF—9W&ŠU`ŠË¢s·0ÛHQ¿Fx¥£²Ðn.Ï7§±þIÞ —$“XzÕ»Èuý5`ÂÈ+Ò°0|/ókPw­§ ’iŸ¥¥CÓï×^^Á~媢ëºÄý9àiAƒ&ê4•™Þ¿ÒNÕ”ö‚P‘imÖoéó¸TöcÊ+âçýä8H“n™fP$ÖÀ!Zùsö0y˜5ˆªEÄ°c7g¶i]¡éDaŒpÑ;ÌxsS5+óÜF/ ÷:D£½£Ù£åZ£k˜{éwñ<üÖoÁz·x¦@~  ¾Í¼Î¥¼'’v 1yt …§95ÈýéÀæJý×í€Qä3½¿ä×(dm'F@ckv.¢ÉI}ñ*¯AîpFÆ°@ÅèS–•¬œ‘õDHP‡Wö|sp@Ђ€Ö»Ø¡ÛFÒ¿OÿÍñoÆêרt¬×`+ö+iÚð ºJŒƒŸ¡@q×x/R;„#v¿½B'd÷5‹܈=¼æ+ð¿”j:u5PË]¨¯20Jò/ ŠxÛz iSDuÎêMD¿"£E{"}ºnsOŠ½ïǤÒíy÷œ ­"j˜U5K¯ˆG%+™¼Þ¼ša€^Ž¥=agñ_G?÷\ý¯’Ö£èC>*i<2Œk¨›;­Z ã* ÖRâ¢ü|HÙ0Dý¯ÎÆÐ’U§sîfÊáÔ{Üp^GƒÙ‰ƒ‘€DÓ^}½Þ%üª;èþLh¨uÃ+½ÿÌË:™”ž7PR(Ãý¥|ìϦU¢Ìó×qìE>Xg¶ÐQ¨ŒÙšÐú¥­õÃ*e»‘ÝJMu, ìyî$…,›ä\VÝNÎÓä()v³‘ Ì€#‹ËÜÎ`®»¼@†G!ªÀz¡âã2ôIn”ŒÒ9ÕzÁ¾hÅàp¸RD{g#tð Wº^Àk/vƒ’öˆcL?”)µ¡(UÃdöHX šj,RÀvH$L¦F;b¶³Ž†Ët.“Æ0Ëþ2¨…«Œ?â(¤ý¾Íý½•³ñI÷˜ ê“Vq>†­–ÖùŽ ŠÕo ƒ+²æñ÷Ð:m¸kY#Ú+JÔKVß¾Ÿ&œóù-›:KVíópÿ ôøOÑ%ꪩ÷ºúrš¬LÏë“6"<[C]’/ +´–ˆ Cpǵ%ZGŽü©-” qE·)R"…åÚÍûÏ%„¬ª2»ö–{ #¥À6~%ñ));_É9ÀKzšÓnÝ™ªï¦5–çÇ_þd~ÿ~¦RPÔ½õ§—…É͸žt0›SO¸­V(6î6õ.?›lÔ}ðI5ø¤jûå啺kÚõBœînaðEýÖsÁ$Ì  k¡ÚÏ–í»¥Þ8Žú#@ç^",“ Íÿ鯖ƒ ã3yŠ/Šw2¤†%±z "1ã8µ_FT˜J*%¸î im_"²‰zÒ%¡²~R~J%^ôLý)("Õ"¢”™J‘(Ò3Ý’µÛ!BòƒO¹gÏ#/ˆ-&hÎãHÝá2?li.#"å×?£H¤ê)íýÒܬWYçÔ¡¶‹ Î•Ó¸åBI¤œ¦,"¦W–ößšý3s¡àzTzåKîÌL´ÉT¾º_E"yÇQÔQª‹¤X^u;ؼ½™P¥¨¾N’ Êo ÞF*)í¯ëçÂjv`MÖ¢<äVñ:T2QgÐ_kÆ´Ìæ¾v¿D€›oêSJ<¥ˆ_£f¨}Ê_ÂåÒ!0tPø–ê™B}&ìŽu”’Ò‰a­™_#þp%­Ù{ )¾œç`5¿´ä^¢i6ÂÏöÀ(2¬ü…4ö[=ÿçÛÞÃ~ý¨Õ·k¥ËL äQ’ö,{|ÝfFÊ«ÓŠm }B7ªáÿ)¬«“öA'ˆ¤îèuöš +}¶ю¬&KÛ@UØ^@¬BŠAðÔíhÏÌÏ•"ÁEUzÊx7j¬û\)öŒ‘gRi×ócƒ•CÔ]9£¢ló~¸6ÝEÇg¾“äóDø¿±‹Uí‰ÂfzŠ;'?ÕW%yövq¿F’겆üqèûuxP)@‘ü%ÝzùNBË:˜ ñ7ŠøÄôR‘þ_þ=3Ûk©½ß§Dº/ ‚ÆgÌÍÆóÍ€¹8,Ìýh~bsÀ“f§jØÌôÂì©N¼Ç4¦gG4èîù}<Í¢=Øöà +÷?¯::á1˜¥(uDùû×Á€|K*ip¿yI÷ûé7wzù + ùrðÁÁ^™è4VìËÕ£!­œeõ›MÙrf‰qDÖ\ÚH¦”++ °ì3óL‘ÅÒoì‘jk‡Z€NvDwk0¯O"¥Ñ¢Pýp¬üz ¿‚¹p`5¸7Ü=ÒëR0‰¨3þ­/vrþ#¹q¡ ¨•?G%Á‹ áð&`·JOþÿ¬½]«5Íužû öx} tU×G÷a"B08$ä$É‘0²†È2ŽbÈ¿ßu]wõ\ï3ç’µ ²A¼Ïê1û»«Fq¬Û³!¿Ð©ãœqªh¹|hÕöÂ6…W±û¹;ÍyYÏ eF”νáÿQqˆ.%úK®“XêÀ¥†}Ú9¼g {kÚÄEosö)õªÇD{ÔGÚEVööÇ=ãÛæn±!]y§¯8%­"â)EgßWÚ-Û¾PßáËd¶»^|:ÕUì ã\¬Ð„á(ÈôäÃZ´^‰`ydD;·è\Í>À}ˆdÍÃÕ’ ÞÛ@yíH_cÊê]¨yçÛôé—Š\ïns~QÏBc*·›–jè†ò¹p[£w†ßdq ÐÕÞaͲVsM œúQëiPÌîOÑm,} +^>ðbÛA v7-´¼Èf ÇÎzZqLGªvÕ‘‰@œ÷Lùpß S«,¢Š24&/£à”¦¥7UCjh{¥3Ý2ÅòøvÓ¯„'m‹jŒq>H@£îb |xÎ'ƒàÊ!2lØT;ÓŒ n’ì%ÙaOk¿ÐbÝ÷ìN F"XƒƒqðíúÊBšga£q¬£Ùt6›ëÝßDŒ˜íòY¢æŽº¶‘Kc&LjÛuª0Ô´¦ 30ÎöwÍînʸÿìQ]âëzñ'Òþ‰¬Oë +=ð!HpCžYxBg\ºtß©2ð¾q »Y+Þqç +®v]±Þ™H¨—¦€Äþ#½¦4fÈå2 VbqHyðô?Œ +ߣ´ÃìŒ9(U0îáEkmŒÅ|"F"¤Ãkªö¨;¼oÆ·âñØäåجI–è•Ó/¾Jg®§íEÄ]¾”|ÌËPÉi{7ôÇ pm¾^ÔcÓø¡hÇpè]ðqÕ4¦ÖËöK·¼Ÿƒ4™ÛLäyÚrðêÍ UÆ5c X7iâMÜ}¼÷€çÅŠ,ÆÙÚýM½l9;7UsV¼óÌÚìB“ynº¿”îõ†˜ªÎ±yTwïç/8ê¹Õ 7¬&bwAHgǙϖ–€rœwé†Ë^øOdÙàô5=®U"Üb 4Î]ÐÔ,*ûTíåŠz‰Q'\óoHøOy(-†fÎ"²ÔË$êß7Bÿ¹+]ݤÞ6˜Ö$ÿ]Ä°®à™Ç°î#JŹI=ÄG5B Š9Gß +ÄŠ[YŠ»‘ö‘›Fù£)¿vJüÿÜÏ‘ 'âS¾çù¸†¿ ¨/}Œ§žòb›—à+'«;èÞbysWtÖdXì& õ§¬ÞÊ;ß#žïSo8äÏøØO‘»EÙ²ž™-b€y"Ðþ]ë§ —ÖÍn_•Õ;ÔÎãYw×-A¶2ƒ«<…óžÂ95\¨sá·„»zLTѹ¾mª}‘›W‚HºÉ7ëýÍ~ÍO\Û“nµ#éÖغ¤¬,è +eN¼¢®g¾gÖKw|-ù÷$kCc-5´{í·Â{ÙP}lI'-r]Q¹Ä3äY4¥ÝÝŠšBF9ºë9œªfö#\Nõã ”Ì2¦Ž2 ú÷¶P‚Ü, s½0ÄŠ:…W÷} vÏma<Å@Ðy +†•ìyL bCì_ØÛjÎ /mRå÷T°e/ŸQj;˜WV”‹šÞ +…­åàÍÂõ3¸ý‘—`é#Ê…6g@½Ô/èeËY)‘«Å‚=AhËÔª´ÖóziqÖ-@yn9ÚêZþÔG3Ç¥ún›1gŒ²PUìÜ€GnPZ³]¯ýGF êmÈB}÷Æ®Ô'qÅ+ª…TtW@G—5O¤æ‰ô­,˜BíãÝ °…àB²™²µ´]˜:ÖõƒÔ´•jÚÇÎ餡ùBÉ™ô¥´öMD­Ixm²úGÔ•W–öÛ¹+ð‘AÅM«‚7&ø|¿u𑞇VÌ%~G©K»þ ¸ù"837Lr_ÒšåözÖâ© v#*šÞí)Ñë×{vMøo\8©©Þ1Ž +¤Ö:MYÍfsnåDä3â·OOÍÞŒüf?£?ÄÝõí•,b@Ê‘ü€oçÆ÷ t6 ’­·»In5ö¸pQEz>Pó¾1Å8ÎÉ"i›^leh”_8Ì·8ÌÏZµ´™WêñÇöJŽ½dÈu!Ú½®1ÿîq“>ºü¬iMNóFy¨eë§.Ó¶—=J^8 ¢1fÑûJ ¿‘Š>"þy×·ß7Âcµ˜2m¥²ŸûŽ˜‰èí9ÐL„˜X#Ȥ‰(÷Ë•é8B€tä?›ÿ|…Êtä×Û¤ù陿³4^¿´Ôa„.Åe~Ñ¢·£âÊËÂEuf¾‚„%T¥AË¿Ø‚QdS¯DžùG8¶M6š¤t0 y€ò¨ƒ£Bõ,†ÐíÕHz:¦²ë›^O¥ùîXª^ïus7öp•^jʦ(þ¢vBÍÖ#ÆP“ôzE c`§Bö¬`‰Û=û©BNG ‰¯¿Ø¯ÖÛÃú…ÛâÀI|ß7:š]䯜¿fàh-«ËƒWÄ%2Oí¶Þ1úò§b*Yi²î?6øPxИLèi:B›µÐ’ØÕ²ïúÌk>Ô£q÷D‘쉥å´DFúÁÑ.Áö]Ä:©Ú+9¼ŸîŒQ «¸ØÜ[sFUõÜT¸È³#Î\ð +þqÍ£Š8€üd$™ˆ¶ )^( U¤Ì~Sw=!žÁ´xÂ7~|oŽíØCË +ØŽ¹4ãÙ6ÙDF&šZVhš SVãȘlÞâ•îáŠê¶ª‰:º£bqõ¾â¾4"Ê•ZPMÒ´ðÈ@^„µ±’+©ý‘?˜Û+€Ó–¾NCî<ó€4^ d%ˆà È-\~ÊkGT"¢%òÑæFQ=µƒëØ‹P“ÑÆ:% ¹_¼½ðF»¶¶yá\ƒÍ‘`§¬Éœº)k2#bMÒãŠRt”æÌFé_˜ž b +0釆YnW”=Ü{nÿE•DlÁv(°6™.ž£œ®³mhÁ=M™«¤²R?fjÚT ]µ*¨ßqÚ°à<)hM²„œf¶k ãË…LŠÌÄlÖ»vGcýA0ÏV¶‘›k¼ á!Vo,ékd>6‘¡_Òéj´Ý,`½šµ{ÛÚ´¹‡.–‡¬ ;¬P @º¸»©]d‘‡Ÿ?§€?£w‚éa(èù Eú±—™ºœœïU€6ê7ø¬«n`°íÍ÷¨znÌ5:Er_j:Õ4ß$­N)uÔôa‹"—熑 /gzP2¾*¦8ó›ŸkÕÙ\+F…élÐ*±¡§…<êøÅ»3à™:'wf“ëðþ0Œ³æ­+Üø»ÑdØóà_7¹ø§xŒhxírm¥PÀdxÛk±™7³·ÃÊF“þÆó਻Z£ÿÑd¨$ïªjZDµéN„̈œÛQ^Hä¾ðY^§ëܲN«¨È”¤ßÐææ3‰¦rÄ 1‹ 6œÞ7ä‹yOÊ>~Ž/;Ÿ²úø12¡_„ZÙg„O±ç)>0— !÷ŒÍuï>Ïž**“UÂ$=ÑÐsÃ%s­.ŸýbEÑbi~Àg”Wd꜒Y–"“®ðôX… Î|×$µ¡ÿ¹Á`u#°Öy U¹6œûrXÝ*mÜ%Ú´ojܵÇvèFàј5ŠdýFä"›Z»RÒnJfÝ[t÷øP™l¼ÌÇK°©:J(ó‘Vëë-Øïíµ0W¼F„m™‚úµ(GDð˜ˆhw}ûw’Hrç^vµ~UÛ×þ¯Gã”|p‘EÝëÑ@šÊ}¼ Ýåô±=@<1И<£¦Úµôµðú.Ôcžh:;‡Vxiœh."é®q©b½…èÌo"fÑ"8û}ß• hŠcN{ ,)ú«Äuju‚Ôkú³×-1°¾tš>ï™AÎ]®¢Ð?£ö¡Fˆ°È®Ââ7(@šƒ\µ~á ÞbÞDµéæšæP ;BØ€s@˜e›§¨¡ƒÁüŒx ²w©Ëƒ'þØDVУy£3Ĺà¯@º€—YHñfÓ±½-x2-D¢f6€ìhGmPšÀGîñ Ú ¡u`¿f&}êÞKK_"^ +\U»ávĈ¡t£Ž¦4JiªBë/Ç‘°GW²ng1Õëk*ä¢F ó|XÒßå\¨†h2Ú‰tÛ–ÊÌy§p¿úͨ¤Þñâ–$H¥’5«—¹»â#]qQˆ6;Y'ö*Ü»šGzâ53j½à×">ì/DÄ8 çqÐ)f·#Ð8?#r¤º!²Xknåà·ý›ØW¦Àï!$ +W‡ó´€R6é´£F^¼/yÀ oÖGð¢¦FÔ©Õʪ׎ZcÔßìó¹Ã#¡e"˜gÊ-è{Xç)|á$xH`å¼ËE? F£ü@©mü¢\ôSTCÏbí¦(ÓrYr–šV­‚êÌ,üxÍÌëœ/Æ+L?FZP"l Œ“+üpç—Ê%-»ÈYõ­‘¥4RùœeSK~ZãV¬Ö¼)³_±Ù»â6j' ŽêAWš±·Y·snƒ¾] ·ù¶e°èí2ê.‘dΉ=z,jA?”Ó²P»_£Öˆ~?ÂÚ\pðWw}£s¼@&ëä¯rÇ·íÆÏ´“P>Ü ÕxmdÂg®¾° +:Ç¥;D PÑEF¯s”õ¦pFçv´»•w¦?pQçûõÆÊéINí.ÚœM@ +׳‰ÞÄPÜÛX_³e‹÷È¿ ³ûK‚úäÜÿIÞý¿rgn÷ý—ÄzÿãïÖŒõo~ü÷ÿö§V~üÛÿ‡?ü¯õïþú¿üíÿø»ú‡ßüõÿþÍúÛ¿ÿ‡ßüúÿøó‡ÿù›ÿðwÿÇÿøOø?ÿ˜ƒ~ÿ‹ÿú»üÝßþñw÷›uˆŸNî~Á‡<ÀæCZÃôu˜¦ÿ-Ð*]­•âútã!*£„Èìh¬¼Ò¤rUŠœÕr³–¯h‰¬u  +¹?e¯*\-lüô˾¢+¼B¢~OgªÒOAWE}µå©™À<áÐ•Ò û+)ìêvrü£Š#¿QVº{@p×VWBNa§Ø Eæ•V¯µm…ÆËÒàzëRT.T¡éœ:}º†ÕY{¯gAý·£¼¡¤r×.˜««{š›Fù´Ë¢B¢jeaõ€…!WO€z)P^[¿<õ[ "lŒN›ézíß!2ÿXÝëCIy¢ç<±Ê!b­îZ°3¬S¦¦U9f½p¨ÛD2öî¯Øc´€rÅÎZâ¹`ÚU1¢e +më¶ɇ ó‚Á'Ž ký6Ê8Ÿ¿³×bMneU=ðõÖœÓ1¦ZÄôÊÌÇ÷˜gñx¤—½û÷ —è‡KhÄwÖnÐÐUg½¨w”u1Œg:,° êøÜฃ6kÞu'aX½r‰3„Œ€µYƒS, ô*¾¿¾¤ÇµÕø2 Nfû +ð®E˜©öY~NªSr¼¢¤ê/’(žAÀ^·Îî#Ê`\ÑÚ>úë@' +:\û–ÛL \7º»!%bý6ÅOµ¦'µù;ÎYG©¬}#ê#u‘sØⶈ»Í4"µÃ™©Üé¼ 5H™&fH0?#ÄÝPxèëVŸÏ3ùˆº"ï5i‹tn^9£˜¦~œyÁÖŒ‚b¹š®b79žº ï ©=ì'Öçóþf?]°ä Î0kÓ©ÿzÃŒò›ˆïÞã @¥ë†ÌøXÞÀÉæàòÄ©à@Fà߸n¾¯ÃºìL2–#Ë97Î+Æš•õÆowjDáÉ j9‡öaÔzG›»¡6AÄ©¹žâ÷NžF’§9 {²ñÈœeˆÓY‹x„S œ4æE›+lÒxQ­­‚fYKÖÂÊÿÇY·ð+Dxù–¨œ §no*:AÇ=DŒªb[¯È­—ÐýEÎa¦gк\Ì à»›ó8,áq_¬•W¢zÙ¿R›È}” ?a ãøâ½=ƒÀ =6 uërkãFD§¼l„ü?RÞbd`ýxœ6Ä*Ùîq(ùº*0ÍnbY‹Jþ,§ßäYCr_ߘ´`2i5³ +´ÌP‡|o9dBÆ™c„ ã;ß³?ébë ˜H?êv·`ç©?*kðÎù +€wjîJ<³¶dÝvn/ Âø­Tûõ«ÇøѪ¹’ÚØ4#ÏŸË"»Ü9*zÝ ¼´X ÝŒÉÜ$TQý-_ÀY–¨Ó¼‰ê×HM“÷CI‘ˆÀo‰ð¥£mxæJÈ«Ç3ÙøöÊêx?‘òV¡´S’¾¤~ÉrX÷›Š3VtË¥DÖ¨_c4B%ûÏ}"¿Þ_Ô‰8!Óú¤ UÔçn%WF†PÔïwŽÁX•ß” Rëïº^ãJÁRChékÔ?°Ïƒš¹¼j®U#¦šŒ/î1q‹#'"ÛëçÍZÞŠŠÄ + j³´dHAßñÄÑÔk[§­ˆneýk%ÉÊ~—8ª‰$·kaÆGk#2Çu¡|¦?ªÊ11 TTÈâë««pí˜RhÄbë-"ë“"6q}—"ô7û9Oe”ôO´•HÑ.;¶e}΃n6MÞ¦)) Þù|\ˆÒJ”¢p¢ð¼…(²P;ã;¢¨¡8B ¨|‹+[‹™"˜èØ° ¶YJGxê¤SA¹¦°ü‰Èúe ôÆÎ4Q  X€uÉèÄ^fmÅ´7ÇA£Ý +Þ¬`ÎvÚx®0"`¸®<¿èðÛO?EÄ?õ±¯¸Yöµ¸¡k¯ö`Öp®`òíá©%AR¸m¹­„|e^ëûŒµïij¦/ áØš«Ïo¶›™M»F”‘÷¼÷@¼ny‰«GÔ¯•ö´ø•"-¡?ßÊ>öH—OÔEpa{ ÈõÄ•Ù¹êLü¾ëiÉ­÷ªhåv™nqJæg@}Ùûû¶ªš¢·X/¶¢¯ +0Å%t·{y£ÛÇÂäLoÖVž÷3†„4Q·& D¨ØhÀ™}pW$ŠÝn‡KcpßÀûß-„j©És{Æa;l ÷ÃVi¨º˜&7ö-`dŒ˜]Ó½•Ò6#NiD¥ˆ¸KÍÖð^{ž@Ý/d1 ÌM™3ÖŒƒD U íVÑb¢Ä-Faµ—5¦~po»K&œ'UûˆºG4Œoõ­nOe$.º’ÂPà¥ËG…Um#×`\DW¼Gd´:Ãþ=WŠ.ôàc?O,öà%¨ tWaŸo€ã7hÓ2Ÿ’Mø& +6í¾õÐWÞü£Òèbz6|¶ÑvÓˆ»³–ZYä¯R³…w­á`‘õë½X й¬póóýÌ\ÉåÀ¼ä$…¢ŒÿÝ‹¢xŠpg¸.×V`’Õ»‚D7Z0’ ®§‡Ç$·.¹j*}•rÝždzpÔåÖ@´^ø¬œÖžÞ&é÷“ô6Q–.*zôewµœO†¯u$”tæ n ^àà‡âÅëþèÖ ’¸§äÖûJÖ ½Ôc›ª‚ºs†usê„ÃwÐHy5m§FyÌTÚu=À°¬«ëÈ8žŒMëA§³%A+æ¡êqnïÚ8:Æð-/°gļ´nçZu¯ 0ýc<Ü1šR“?F/רˆŸó±½‡Sb×Ê„XYö̱XE%‚u7z®¯tíR Úªé×q¶-Ü 5ÚÚ‘Ó쎶Mâm¦«žÊš ‘\€ˆ‹¾¸oǦÿáùœ’ȯ…e殡u//Þa; '3gÏaÏu¾ÄÏÑr:äFb°Õ%]¯Ò*¦W<¼…z@)Ðpü: ™ +1~‹ðHí’J¸ÒxÐç7ûiEá"»õžo%)ØÖd¿àŽInoñœïàD¶šD­óAƒ_YêÝ&°¾"ëHs/ +šiE»™*øb= JMY`ðƒúÙùMDzª4¥‹]ÿóTÿú}?¨&øüQ9›±¾AI$ó õâ3âg4ùózý£Góf¼ibª¬ý4bUGôéÀk_\ÿm?cˆ+Hž}DdQ}k +°îp%sùf?kå-ü0‚þ?£&òZ^”ï"èy•Šúí#½G Ób`0dÐ9íhÅÅ +Æ­Õºúñú\ÛgçgÄó¾ )”[6÷çýô¸T¬–ÿ‘ä°½Á}ð­÷à#YB¬ /`†€} +ȘDŽ¸f(b¡QEŠ/DɆŸˆC™Ê[é µUéñ¯_MÆiz_¼Ô¤…å¡£ÒíJÐÔÏl|ÆÁ3z,áŒ8vº_ÖDW04 ŒÊaFÊ’´ì+JæT¢Þ” ÀGÖ¸eP)Z‹Gìeœíù¢Ê¡V¾ø÷õ¼o:I]TÏψ<¥ ¾¿ùyÓ5BÓÍQ’¤Çÿç•ó»€©é*ìÝ•¨<ûÿˆzž8ÍZcpº2B'A-ôZSؘ­dÿªŸ¹g]XÝ¥Ö·tóÛ~ît@ô58sÆÍ©¤jSýMPôƒ§s˜¡ì#½E©¯«I2O^oëCÞ9RøŠ ¨ +8h©L‰Ð­Þ”HêgÄó¦DˆFðÖdùy?X†¡á]D–pn>ÿìK‚..Sã©u¾GÍø+)‹Ž3'Qƒ¨Ó¹æMtoD$ úÛ¡}K]ï¹e—ò) \õ¡üÜiÊ¡ªøŠ«m§f}§fwòTW°š§?@å—©v•›¤-z9|ï…?kå/xuòÐÝÓ¢|Ü>5#f%–ÉÜáýí}XT£õÌ93ôZ¢ÐRt„Î\0ÈgãᎣ(ð+¼QŒH}ppwã'}Ù}ô¾Õg·hÿÝN±n°ënçBŸ +ƒÊIÑ„P®ˆ2 i7ÅVEÓŒ,¦D£º-ψ$EÚ©©”X±ý K”Dtƒç)ô€V-–>Ëw—*É3úõJhŽBè÷^†ÇÌLúxGsØeUAaS ðœK™^l/‚¿ýƸÞ$q´3v`|ˆwÊZc§¯3ºÁþ®AàNaí&Oûp>Òn4Õ›¿û]}ëˆ!d·³¨v[¼c+ÂQAýØÂKçQ0iAáeE¼¤Þä¸2czå¡SµÃˆÇ:êZÌQÇžlT2Ù8{¾C;mÑ´Üo×GÔšåq°Ä»ÙÓfœÚ­g $ÒJÍÖµà8µYØ^/íà{é=¾~EËÑ•ÆUS +Æ°ˆ•5ˆaÌøC¸$šã´h±.=5Ö•v"¶@Ufj1H;p Ób8HŠYG@—:y©…ÕL]n¼ŸŠÁT@þù¹Õ2ñweG´1žˆ¶#îíÀjˆˆ1^R…¦b»"“À“ÔÚ¶¦èæ“„‚Þ¶YNºÑúMDé–åÎ(³Ü±f¼·eŽ´’4>Úõ( +‚7 +¼T$¤Ög1öST7uÑvFuþ~ò„‰•+.aÇF¬Œ8Múõé*aV_D uÔ±ë¨Seh_¼ŠØÙحٶ¦„bзJvÞs=Üñ´#†d/7Ê!›t>·õ-zhûüೕF‰N¦IÐ +ùÚ¿å†tªitô(¨2›17'¢T#¸cDh€a)Î!ˆ™ƒT|zmCüorø4<Šœ¨ÛFÞšÕ4(,¼+ëòë­XùLí­òìt1WŠï,XÞC':BšWƒy¥=ÈNî§s*8xK ßî®j0 ¬[/%`È+J1=úJõžÜ²ïz|ñî£ç”ÁߣE6ZÏh'ÜÝ·(°NÚIjQÁ@èÌÂk@‰„i–b4‘·„†Èuî÷x¦ùwñäHßÞë@ÀÞz®4û‚“tpD圢êº#™¥ÖwÖ£{nçúØØQU4e‘%=ƒÙ"F§d¶#Ʊ¶S"‰èÊ°Ò>ÌqfF+k©G(ê‘‚Ís|@¹´¡ôlŠ?¹,XOyÓVÙùï!¿P­nI¸p[u¯bwÝNÍZWÐ*[þ$ÒÚKW¡äÆY4°˜j÷Ý Þ¹JŸ GÍ‘FDb?×­vµÏ5öFº¨àÝþÜj(™ôµý"".¡‘×TL€Ù!¼cAyV™F´øµWøX^œ³ˆúôHP×8RW'L+Ïòî?‘ʯ±äTZd{LUk؅סHÅï‘òªclD~¯P~uÁ×H>^òþ~mžuèɃ›JÇm šÙƒÁ³,øiXÒ18¬4G[ýø{Jó[üòms«<óŸùìÏŸÏÐf4¨IÏ6œ,ÅâÒs-´í§Ô§[ÚÒŒ.@¼†V“+bM!“fu^¥8ß"ò¼†Âj7²Ó…îÇ~|îëËṟ@×($(‚<ãèûMZgüç!€Ï(úŽ¨¯Ñ¿Ä¿">#f><ï'åþµé˜$EŸÛÂùúå]¿VHe~Óùå²?£Gr D ’Tw„`8ýíºªöÖ,¦ ˆvI½¯§Ó{ï4F!Û¶™Ü*Ï`QEošˆËÔ%!Z§§ .ÀŠUƒ·ðÑ××.|<äP¢,ÜQŸ’[¤*Eõ꯿{ËWS¼j¨Lq»Ð§ØZÍ÷4ý¨ ÈRûŒÈÃyKÛ>÷3<Ù‰úf÷TTy±- ¦ú=À— í—`c»>‚X‚²½˜òè®zºsŸ.ô¤Q¢Gq¡ÔKùë@ûŸ{ ÎA¤·ˆßnœÝa¾ uŠ‚ºY5ª{*=ÅPDÒ˜åA)mÝŠ‹SKVêëfà€ƒš"½èÖ]W)E¦þM„ +¹§ +¹óÞè‡Ï(zµë×¥¤‰P‹ƒ\ ^€.@³š©¬—¯“#c«Žç)÷V M£X« N~’g_1FD8í”ðA„_éq%púŠˆˆÅ¸·>,¦ýëHN“XC+Q‚:i¤\è–¾q><­Ü¢H¡ƒ41.§c?("^:²õ¯v}¨(&YͶÅHSØŠþš%û.Ì31ÃÆl¦}qwI™ç åûjìþ…R´‘bJX#n”œfÄ*ÁÚ¯3BµŒ! +|DüvçJŠŽŸ®Cê7û8!OA@ý¢©FäZQW'»÷„¿Û ’ðÓ¶HYÖHY +Õ‚dÞtÝX7¸¡¿Aß8×ç98±zèÇ8ññM ­@œ>Uy{ߪ0ÓÃ_¼ù¡]_eXœÂÏzgÜâáÞç¾Ý-·ûñ 'ªAÜäQ—Gç ÝUh.술@KÍJõ«0?œë>’‰Í=pÓ4Bã¿IÂv})#ön½©—ŸaEeŸ1JÿL6·"œÒ$IXï”@êf -}Ü3ˆÓâú¤%𠬮gn€Àã3DV +)Sú³¼ð$`$ 5»1õÀ èÄ!7ûÀ »Ò‚ ÎqÞ‚Tàí- +¼ÆæØ‹îmG¯0!ye@"(`½<ô´?²ØØRŸðE/þÞöÒÃ2Ú»®wˆ¸¢è"E€¥±á¥&̳TÆJ +XÞ (÷ŽI8`pkÖ´½Ñhc{¤ë3 JvÑC–WNéI‰˜o©Å„™éjª”däôÆ(’…n}j–¤˜M[®ù¨ +%HÎCk‰·ˆ Ÿæ†–UÁ›ß짞®n€Á¹¢À ì”·þ3‚ïKøíÔ pé#êŽkâ<ì ƒÄÔCMÄ [ú“HâÞI”¼&EôÑüS9妽ñ`ÓNå¸à«õ½±„ù(±žklv)[¤\†ˆÜ´]`}Q^kàÙEñfÔÛ–ã ?†èŠrŠ§B–›M™äcÊ‚5§‚sŽOB‹”*P€xŠ¨`JQu¼Û¦Ó¢}zÑg ˜âÓzŠF +¸£ñÈt%¿@¸ê¹ŒMÕ™*ÜHåg¾Mû¨huϾíˆfZûŒÈfÒÊÊüv?+ç ¼Öù(³x~€ªh~®vÊÜ Ï™Œì.ÏC]yö±kp+*x·{K·½GýÍ>;‹í`ÈÌÄ@˜‰¡t·nT•s)Ó1¥Õº~±í3¢‹×bÜZÓx(€QeFÇ‘Üi›õ:o–jÖªU¼©ÖV1ì"¾Ìÿ÷º¶¬Ð +Õ¬¥gùf?oÅ"Uëé3R©!ï:ø{ÄÇÔœŠõ{ÔÇBkXã„­\ÄÍíŒ XÍA$°?~Œ½`€LàX2¤%›—PÓ…Ó¤®Gt Z _há2àQ„xéñêû±Æ„ÁœçúüD³m¤ 3HT +rªc¼6`Àpðà(·¾Òtäù4Û¥¢ +ís­ + ½€s‹H€óˆ6Eiún¿t°6¾ªªR€NÊ(?Öû½åýÏéÒÌÔ10Šßµ[{#ûyF,¾ë·Ç7=tõêשíáÿ3êˆÆ\ĵ.WÇãˆ,“´Az uŒ(n3+ò‡£oE[½JôY«ž²»w±»>Æ:×Eú}«Oy';këöãÏbþp <ÿ_Þ%¡ÿ’m~n³ÒA¨4Q‡;X‰5ÈÆÛݧl$QJ`Ü·a'½éiƒœOZi +Û6…¸[é2B>BEB¡f@é{–G¡ÿ8q-ô‘ÏX˜Ñ‚Ö© ¶$ ×è QUv]ãéá&ˆ¦mjyYkÖóÎJµ*i¸r@@ÐÖ¦u£@㘆@äõãC;Ö§ôsÐU¨Üæü>€’²ÜbJšì/öO¹•¨&ì™(iƒ*Fg?Âgâ"¥\ešÑ™ÈéÃ\Z§Ï ¶÷?Ô@ás¶Ç ëñÆ\g-‰',:ÉÞ°¼¨æHªGÌ +›s§æ7ʼ¥,ÿ+hd æ›3 Æv2Xmh@Õ$ˆw‰¸ìm™©¤·%.=yøÕYéj‡ðúV›Uv¦ún¯t¨Äicƒm<•~òï‚ÆÖP^ˆ1© VÖx·!z…ø ô/›"±·àX‹XC”Œ°Y#h±ªÂ—Tì^H†sT©Vn Í˜V}R6£Ž½Qû6ê&5@/+Lë®[ÛÓŸú+SINÍù¡8Ògöb©FºEC”c<á®í wô­Z2Å[DÕ R y”r¯Q‚FCKÂíЪöQD–A¡¤L¢¡œÁ(¹-zbÇê€z<¶!èˆ*.Oq­àt@ãѸÃsw¤þWFÔšbPøœõH …n*:ÊÙ¨–ò…f­;ËñzÙ)GéþzÌá“ÈZOMƒ…­à×{20ãôëFV5[U¯þ‘+óZ5„÷âá}`$F;†¾6½g}ÐÌÅù(FM &‹¯À D"3†±ÔŠ-#_es3ÞÁðž9nc*õÿ¤»>ˆz£XsK"R;f¡³ÄŠèBRÞª…sªÐD¸^ÔtGÆ°ÓCõÖ8”Û„=C°BtPaçå9ö­•CàÁ#i£¸gœd%M"ª}>ølZÈ`åW*|0ŠsT¬´ãý]©Ô@þùæeE÷ªæ'¬A§ÎCàíylÐÓNŸã½=0×D¹…±iÚÇ¡çb½rÕ•FÃPV£y ŸÚM17)Q¼+ÜK q»=|ö¸¥ƒ`F'ñ8·{åÞ´ÊsÓ*y‘XËS²µ×csO€¢ö+j#î/B;f†–Ë^¿úkùnX”Ü=W¨äjòË-Þ *Ø_F´Ù½|ðJ=^;7hËxÛtͼõd*:•·C_ mÕÂÏ@­çJ»HlIj°Ì%©9Î’zNxMË×ò`¾I}ÚŒÓrêOÍ>CÒ'fSÓ'—¼};^¡¤Ålþgµ'çûVÒ 8ÉÿOI§Íj˜*¢—ÊÔ$m‚‡’ö¢—/âS”8«>_ûÍ.Zð<1w·%lÝòV7=‚•äã»8ÛvÒàÂÙHñë3Ùý‘@]^áà{燾£‰h¢×s=6zZEaš5ç×2ýõõ5Íxð¥Óˆ\ö¨cÅžÉí2Þ’UrœÈæK'’âÚÖOÙ8›ØÇó +®:”s3MuˆP!ª5è¤Üª= ¡¸:•Š±ÕwÛH[î¦Á’EÛ™îøº³*Óún°¦PŠÞ ³ünÔ@m±‘ßHžTÃêì}¥£}2¯’šÏÍþok·%¦Š¼¢Fuñf#jCñU ËëÔ;þ@bvG`¨‡×Å9—0cl…?°x˜19^"Ä 2À¿›S»ÔθX_ôÝ==SNà8·$¨ê˜+ë'uF‡m?a![N¹]:<µ¾1˜Ö(ʹº¬1!öŽXÊŸý4~ýü):u¿›¸âªƒÊ×R¿£pö 4eíQ}l4+àtKoˆ @„]‹†F‘€ýà z©#o@Ìl +IÍî]Ÿ‘ë†öý¢Ÿ¦VTà÷7£ +÷ï¢*jõ ÊUóT&Ô e¿I2VÄÊJN’PÃÜŒU»‚Ê7ûXÿ®ÑõÞp´ÿJ¬ÙÄ<±¯[vé€AG ³ÝG°¶Ý€&jÝ8«Ð>Ð ]›^8d», +^]LU·ì¹E”Iõ©HWKÉV2Öl=!)˜ ):‹ß­q”(øK(}·®x%aŠühUÅj¸¢Il-æ­(°²’ÏNê=aÊ~=Fê¨4#˜3ŒPþö²)E@ëe~‡eAˆ²—$MB~u<R%RíÌ ¨ò¼~I£9Œ—D<*|ă!éÜùÝ´AÎÓ®]¯?S¯7½âB½öÒ:ˆ~nÿ¯ífIÕïÕžê÷®h¶5âu]´-¸Oa¨ÁèY^Ö“tû8-ÚV`ârviU-Jöƒäñí¢+È"îÎJlSÏ»ã1)îå‚úÝ峨w—­–qJá°ôˆ^ÒͲøÖu¥;°kxw”çHënpÎIémÙ8žˆ k4Êõ'ëÏ:·sVp0fâ–ÐÜÄSî½r•`{¬4ìiR¨TÙÅ'ù!iV6É@S=ooeÐS#𡸒—AyBZŠj´P•?¿šŸ÷­r»£°áÒYm~ÎÀ;Vê|´½Àæ–­Ó¼÷fØH+•”i*þ={?Ä`±†Ñk­fhc-Œ÷˜p˜WFpžþÑöQyTµíË"˜@韱Š¸J5=˜‡ +XF(}ÀÿQ\ë–~§íÛ‰œíÄÂ0¢¦V¨Óÿœ·Ôòb³áÓ£ßVWÌYÏ`fDCŒuo|UÇFS1‹ÓÝ™ úbˆØÊ×~åÿUR™wf1–t‚€áñŠ*ùròIéªÅrpF&è”Ý­³†{;´éÔGÊ6kHè=þ\4ÉŽ(´ê;®‘"ûúû4'=LÓÀªN{r(õÏ´š[ŠC>ËÆL^ª¬¯Ÿão%³­vtvôni{Gà±Çö”I›sæ`8ö4̺” +!—JÉ¥jõ`/Ò²“ïOë¶\å @)tä×ò +ô`x#ñŸ47 7‘T*/©²oyjÛÆó~t«´üGi¬7™¸º¬7ö:råC"#ÙñZæ_hÔvE +VN£Ü9¸Ž#–Ÿø”¡«ÙQ5£(Öœp›Á&’ îÆ®¹ËÚ°fɸIYÕÓ€Fž +dq™zºYâÿyex%Zžäy H%O€wùUÔæŠñ»Ž-{ZµlÙ/VO¸W=^5ܘ|‹äÊ$(h/#^»2ù5ƒ¨<"S§Hqô½æzþljó—\ÖTLLHt×»€á»Ôµö¾ÖÂ,ÍM¤ +ÝTy€àRè8ëNò¤5/¹6ðvÞ¡-±]×xÀÈT0„¤Æ‡VsóZPY¿ÝGZ+P:|TRÆÖšÎZ¶¦P£‘¢¨ÈµÙÕêÞFHÅAsã5ÏÉäšK´¶Ç¹(KÞX$Tf ˆÓز—b¨éïãœÚÊâÓ>")vJSYžzÝ"hרIQ¯øTˆ/[­cíNêÍú&›=XÔã©¢æÉàó!â\nõ7vÊCýc½&M¢¦ R ¿”‘TÒ¾ÌÓVVÏ«•ô€Zä.°0‹&’"PôY p…=Ÿ[4ižÍèvs”䛬ú×q¦¦¾°6 ŸÙ(Á›¨·wÓì#y¾·ž¥ËLD”‘,I%B«2¤/AÉöí¸”i N•eR¾ñóN]‰âÕ°r߶â¹Pú!xVÁI>M„/àõV/GØ¥ˆ–“¦ dEzŽ(n³øªÍåmªÙɦTã‡úPðÀÑFœäÑa‡ÂÇJúmÌ.52ÖºÉÐrImœ|=Öâ» ?B÷… ïFÀ#RBA•\8#dA‹EEäØVpôCdžÁ¢|„à ÂZê%ѯd®”¨FN¶^±á*®ã§žÅTË‚_L8¢ßšøhJd<4$^œ}¤F*Jz4c³ýÅ-¹]À¨ê>7á~Xœ’Þk„ù:b%Å)“3ê[Ç6Ö¨ÛáÛ¨™WS/R,y…f rÊÜŽOЄâi°÷¥)qRlÞ¥%›K¸tƒÄª¬é‰—¹µ=âj÷cp›¡1uüo\×Û³’{TÖ“W|ÂeñóèÈz9…Sñ•j9@X¡@6ò­&-â‰ÒêmÒˆFú°÷l¯Ù¦‡™|ébÐíKŒ*/‹(Í®¨ +•Åqе¹H¯g?K]˜~4`-kÅ×Ê/P:žãM-=5Âõ; æŠJºò*ß—ºnPbfn£Š'RñWDÙn'5ŠØãÑ껲¨\ß|‰ ×Ø‚O`ÍÔ¶„çÞùØâb­€ÏA=ºHû1Ÿ\çjqÁ†ýZùj*WŽ”•×Éh†Æ~C‹«<›xý™Ë“˱îgÁ¾EG£$fhvè½6½Ô±OñŠ1£zß„·×{iáäm­ŸžÏ‰€[Ùݤ•Læ'LIç&ߣLnbiÊÛü´/…Z¯àØÌè`FQuŠݤ°–`<šû’¶Ê<§Ò= +–𛨹D­Ô‹>\1=7b˜o0˜G”'º'jð9€\bLW3+œ[pB|Îù:’öÕ,:êD‘÷8ã—Ç[œªFfK°í>.<Çðì™Û8!_t¿Öëkøî«•õ +¯X&GºUÏtqD¤ÛÉZÂÄþœ%èP­jÜóÑân)2»|›iÏÇe‡·G9é¡Í:³ÇaK mE›`Œ¬¹ÜWxPöf´íŸ³Ç“»¡Š¢ªÞ.–RhprP‚§|MÖ]åÎ~ô58È_/¼^t» Ö)žuíºæäÚóÊ{E}¡n©A(·]šZÜ÷—’MQô¢n³´÷€jDªfW 8„“$¢ž2#ÚÑ+ +lk¢ì„° +T=¿«w x|ÎE¥'¼²î@Zº†O‰ßôæc0…œÙýu ÚÙ¨mS%¼°É¾§ç{Ø ¹Uº›uv\×ÑTG Mu”^É^¢9B‘kÏHjGô[‡Ó$d*ÚêRVÇ9F-ñÇPŽºG\eaÆ:ºÝÇâÁù&{¥=g0VÛ‘ªûí•`7R“îLÀuzû²pô\úxÆb¤Í)œOñà&çþ"âi>z§6Ê· +€OÕ{À<@…Aî +(;S’r¿îÖÌPM…ç»í¤È¦M-^r§šÚn`œlAô7 +,ÞOÏÀÆÛe“5µHÉÇ­n°âÀÇC­>#4¸^JúV¢ÓNMEiê˜h¥;ân|á¶îá?4:Ȫí<ˆEª3 # BŒ¼ÛCQv€g”ãç^au × …´§Äõm³žð0„açÖ—6b>ø"ñ:’ Š~׎Ð!–ñ¡}‰¤‹‘FãÌ ¨‰:¦Ñ¹ÑV”-¶‡D¤¹é\ôœÊ¦~´®T4#'‹AÒsúÏ”„úÕ;®ì¦ÈcF^ÏU]…f TU"”wGõZÞ0CYQµ} kh©Â" rWÌÈË0w°Ð~o<;\¾mLË V4WYþ)„Æô E¦ + mjWtL_,$?NL”DZ ‘qŠŒré½yÿ5h#?@Œ‹ÅFõ.Åmú¡±±ÑÄ®âSò«€¸<¶ì'õ×ûó„8zH$˜D˜2Á:ŸÝÛXÏéñ»¬z²:C±æ9 +ßØQðñâÉ*2ÀWl|¼Y^_.ÀI€„'¿™SøH˜ŸÁ£9R¯ †ðc3n멺'ïzÐÂY£+Ë»+^°e×ÇU²WWlÞ‡>®wóÔËÁ/@¤“0Wp¾eùG*Z¨,’mÑ_?[$vÄÉ“½öiegÍȈfT’l¥ ù/´€ »;U\gªŸxÐŽI:„¶íˆ€·çV ­Ç‡ÅÎu“pˆÞŒ_QCˆ_ïœ(]@.Ps-z_‚J¡ñ;,£Á~DlO7¢^·‰ò×» +ÈF½ÑØXºIü•ëV PD—áŸqöàÏògÔÖç³6R4E´5°¡yÝ熩g·\…,9§ãm\ÏÖE›> X,Îù§Í0FY_è'ã$JyÊjCuĸMêcÀ“º{kÒ‡ú­;›cV{>ë9ôA[!ÈêA¦ÿéˆØþ zÀ Šþr¨ðʈ/è•SûmÛ2èB qrœó¡¦J<áÆÂhQ9aüŠÌø=îßd'*˜ÒM®]Û4k”Æ©¤ÛðgÑWxø‰tMÉèåñ¹+(‰ÒÖ¼[´×?¦ ãPÀN›23Hµ#où°~ ñ‰¥Ô‹8¶¹¦Ñ¾kÔ5RÏ?\çs¤Ð?{ªpÛW.KmlÎ|ÕDý¡Æ¹¹Þò LÖP©í"«ï8ÅUË ’v†–öŠ ¯ˆ1À¤Eî‹¿µCed’’ðå€u)®ð¤«uL›òi"QÏ@R¶)9«È¨¶›ø8—I}(MœY£”®„L‰ýtîÍ~'­:óVùÔŠ¨Ì¶½*ô›ž‹†ás=.guöAùZŠõ—“Àµe…ËíWÖÊ~¶Ø‚ˆ ]Âà +Ë—°ôeÁAÍ`fûvÿaú¯wŽöÂs L(º»œ›è=BôV$ò 'W \i@Ò°h 4ÏÚ{¨ÙáZ»(çÑ¢¶ƒ?^SâàTpëJ†…ó^WW]'o_Ä¸Þ yœTr*ø)‚rG•¤ÀÕeî#m‰Ýzn‰‘>’˜¢ö{çŠÔ‹«&?]µìF=µî—‰Íæú2Á5y/t<¹¹t ö[6Â*)@¼ÈP×hÉ;©D§@Òò°A=ÎÓa´RG†ÌÂ| +Û?´ó5*õµ–3T7(´‹ øˆˆf“f‰Á›> üÆλÙE•ð~‹EoZ‘¼2*é”CŸžm(Ðt QC^‰ö‹M½ç$rKºëwVäˆÂ¿XÕ#[¦× ­9š}ª ÔxV‰–˜q°y‚6/­I¶¾«¸€t¸™È™-'àrTô.ýËÁ),ÎrK5"!þ(D× Þ§K#(`@¹/| ÅÀÔ­N¸´îP0íº79-OX?@2Œì&+ÊŽúßVðŒ€6¯²X Þ´a¤:·Ý­h°Ì.¸°öpe*˜X¡’¾e7¥ƒ¬¡{z¦=ÆDËJsFq4u¨YÀEäìºã‰}¿hrÑSx£,|ÓX9Ú–ßVxr]jKÀIqØÎ˹ï]W§_×ÞD¨CëÛ“ßÉhF)šEPÅ­Ú–hµî@N 8O°lH—Ž+Jè¶+@Ì0µ79APP¥ö‡(rspP¥«y±'|*Æ„šî·CرFšzGÉ$Õ8ËšÛô ¨!hÉU’êR¸ïGÓu + nop¤Páƒt“èy©!L•ÜOø +’dwWÛw³‚Q'ê²ß<7_Gz@ø¸ãÝ9Ì·ÊÛÄÔ“ï¸3ÊC¯êj)–,lâÚ5¤Ÿ±ô@Ö‚aƒùû&k¬{‰$‚ÔqØÔ— º°£ÿ}R9#‚>>L®1äž›Û!Ò|Àœûb @Þdh¹„Æ(Ëw Ïõn}…!¾„£¿Õ5æ%\·G&€ÍmÕçºS'YiåªÎÿˆt¡Ýˆ{ð_²šשüÀj«ÅŒ+>•>Q·Ÿ™ÏoäÚp ¬a¸4ÇVY_§:åú÷ T[Sɨ±âŽ >YËæÉ׸Ÿë³%K(QO2½¿ôiGj¨êÜoƘ¡Šëˆ?ºkÉCíБˆCW¨6-¹ã ž3Ð1+òx!A<ÒþsGëLt+Q[ z$ü ¶œ'}ó Lv¹áÝÑ4+d3_Ã¥ƒ(CSœÔ%O½îÒëbõÓ¼6‚·’ duM…¹q÷h=¦ÿvÅ•‚ÇßT.dE$dO€dAªÁhqÅJçò2I=È÷¹ &|«j»†¼ìê@ðîý6é Ã\ÜÇÐö4ˆ¼„§vD‘ +ëÊ-/‘ Ú,à_¥Š›PÊ‹µÖÅóÿ„åfeŽÆÜå6ôGú"ö +âM"ÿ±UL.™B±wûí ²à„ï€VÁÇÖ(â¨[ꮺM§ +SŽä +=P.õùxZ±hY1ñó…>Ò°W\(k µ£±›.ˆ.‹á,ìok,šÑ¢´pk0{ìu’×#¢‡^ëÿzlŽÁŠÜd­$+]’Dš“r$*Ê¥F±5«¶$Óõ'ä—¨=³DÀ¼„üB²æuš¬D»LT¨òSUly®Öd¶®ë-„`-µ>Y»*[¡~rCè§ßuö»¦J¢ÇˆyQ…0v„‚!\æØðÿ±²R+ø5춰#*è•0o$®±—BQÏ uËš•rÊÁV4´ÌGdvÆd çµEÛéÖ[Ž#ÊZ-º{k?-Õ’F~¼Sõ§=ü´ƒ$!¦I¥ã‘ø`æ#)\gÑjÙêñ´³Ê ³0~ß'``©T/í/¤zÁ{é=u+ÝA æör¶a<¾#ÌJ’qïB>vc•ãÌ*àí·NÌÏçgÀäúaž,s˜ `I€á#=ìú€¡40§À &(",kл«9’ 9{€'rðA/_¿Bý¯ÚÜJüMo/-¬L‚ìݱϕ ¿?2·Ñ¾\Ûº²p郛€så×kÜô×îÁÇïSÖ>m·ôâ‡#ÞZŽD3ŒàÐ8D=‘²­ïüh9ÔÉÿ¥GÙE]¹°®-þSkbr7²|ˆ¸¼´±.ƒu2jÄÑv³Ž!©a†Ðjc‰5—x0xî'uFQlc[+\gûnCÛXJ×ý=Ê^7t~èækˆ_@LOgqöÀ,®8N:ŠšL^ãÚçŽØˆŸVc<ô¾Í~Ámуäb2O3Ôž½nÿîEýˆ‚€DëQ@NœêþžŠüUŠ aÙZ),]U9>¥!Y“þçnZ.kBæ(Zg†0J“’V7jTÜæ–ü·¢…´ïµF!­EªÛ¨àÎÖÊ´»r7ô9ª,¬îl°ïˆ*Ä˹χÄF:—pÞªîMÇ]E˵½´¸*ÚíذǶá¡Ü +œõÌ‘ §r¼`×~Ó§Ýn¸·¶_GI£êV¾!ßè|¾å©E“ú´,( ¯ÄŠÉ-©¾}F¤üòýÇF—Ö¥&“ýȳâM¿ýÌ;EqL; +¾%—q8ˆŽö¬e…ò¢Ýš(-âùØÛ9·!ŸâËU6‰ÓTUx½cÿ\þõ/¦™›øò¯£Ì‹k°ÆN¡Dì%+ø¯gr>9º¦R¤=va K“&Rj»5ç`üÏGJ-ÚdgXW ŒÖ¬ÿ÷½ú(kYTB(‡W¨˜Ÿ•˜²Y–WËÝÀ€%#¶¥iÔ5 PæHöÿQ~/9’v”ÏŽÃzµ`jV”ìD£Ž€ð•tB6m»'‹m†ÙÛìŽÒs=5x&!Î1[}täo…×À¯´x¸\ö)¶WuÞ{|̹¥ÅògÊbGµzs7c Nbr]!Åï~«Z<_IÒ‘™¸iˆ¶ÖPçr}8N\ä,èöí§ –•„ùVk®ŠÝÚ°Æ¥þâM¸Ô&Sigü¡h…ô•bs!† N½'3TjŠòVDï#¢0ipáQ™•tÍi k÷E”TÂQɵá¨û祲Î8]2*nžc tH0),ð¼Ùö]Ca²w€#ð÷¨…»F·Z:£¹¤@¥†BÄŸû\~ýüD¥”¨\»>œFAÜDRQÏÇ)¨Ÿ„bÔF .#ªßB“¨p]RS yläÑäw˜ˆ¢`ºòQÑ ziå»u~a{p'*J,`“ÄÁ‹éÕý(\|ÙùÍnªåŒæH1öð|ÍܦÂQ³Š`FjÜòníŠÚìƒ!ŠÒÁAÑ–¥® …Ë{[é:N[’žgøNìŽÿzôx€A´‘FÖ¡ëÆ/ÜÌŽ8‘¡Öí!efVÔqd?Y_I+ÞG‚Ÿ²"¦È#)Ü·ckAý" ;\õ®¯6XZOø×Cš„…HtFh‡2[(6+m£îÐ iƒéôÈ,JŸ¾#~)-ôÔð5§Rh½m畈È•Õ€²UI´9îƒS¦]:éXÖo#HjšêÄ ÷ ñ5CÓS°ò(© n:Ä1ê] +æY)Vm=ΧÕZ¥~FüvwBÔŸ9P/WË÷c?„‰ÎjÝM(äµ7š"ví†=–pÜØgŸ„Í…Q4¹»-ÅÍÇÅN0£„ë¹æBŠŠcK c$­dñLƃôvy0éú5ÅÅu‘³£°ÙÏäV»¶V¤c“w¢À eé=bì„ÄìQP7jî(á—MÍÇ0‰ u¦ÓX¡‰­™UË2Εåïé¶-ˆÔæúú 4—ÃØ8ˆã…Ù0lðsDn&.V(Wírô¾ìñ“ÃymŽi‚]× MíuîÁhVw!µã:é¿B–ßÀ,¬!Ó[€gÿá)ø±›:ÓUiô‹¡[J#N¾2›;â~üϯ÷“KBÃ}¤zÇx&ñ¥)Á-þêRø æbßý¡5tm´ž6—ˆ±ò¥Ìcr†ê \£À%Äãdºìß–ySìV EêŸ8»Uã—Æv?Åhþ1·ù‡B2}Ûhœͬªu|  +ÅÙowíÞž×jê ï› ªBŸ)ÅnmøN—g(µ­Ò÷㾈pó» EjÝéóŒso$ÿ;ë‰øSPÒ‚1öÆÍÕ¨º15ô°¤« ƒfFêV¾¯»ÚÓ–øi”V†P »:C%dä±Ö+32qÖ~Xî‘w™x)sgHP2;Al€5É}@i°¤©áb‡Üù”YF7Ö¼Y¾n •¦ çõ‘ÌX!–mOÔ¼Eòͳ•ÛîG_LÔ`Ò:ÈŒ’ŸwÀ¸Âco¹(½lÎ/›N£ÀhqÔè;EcX/ÕGnu&ð•>ïÏYÒ+pÈ6¾ÛÎÚðIÔ®zèù´†¿‹"eƒG—ä#†24=.ú÷÷•óÕ$]8i;»¬³æÃî—Q‘n*1,ß>#ž/´zg0ʸïoöƒíŽ,:\ÙJæ µÊP,ßF°ˆ +N‰BêWmõqÕ{”HŠõ]@©Ü*s—ZÎxÏ–skú+!`ÊîϺÔû>#rïZàGx炘ùÜÏ£à “¶¶¾®v$áÂ6ê<6e%Î +—ÜW\=V–ÁêqÏGÖL÷6¡m¼aqœEáù÷Ýð݇—ëB'+ÙåãNéá3âÛ ò#Š¾'Z{Èð"Uïþ"•BÙ¸„ ]m{ÉÁÔ£$olˆ­ò CøCx¾ZXp`Õ_ö÷î;ŽUë®2”IØڳ¶SNÅiY–ŠŽ²õã¾ Ëô§<3·õ¼+ßÛ¬¢3nÑþÕç:ôñÏ»ŒäØýñó59PSY«·hÕÌs¯{DMª¸È›>qÔ}ä7^<(QËÑ‘ìÊ :wÅ‚hV´ã©˜!ÑáPFÌQŸðýD”ûå§ta9uË;RœMq¾ÂW:B}ÞžÒóP´‰¿³òe ÔÚœÄ,!•<Ìç¶Ý)Ö¶îf çTäg òyNu³(*¤Pv¦&Hë}EÄQWkdF™ åâ7mÊgA„„‰¾Ïgšæ³¯ˆ<ï»dòXS9Ñþ„iDêñ²Iõ”åä/Jøqœ˜‡µ†Hç/µ"°!b­ÉçÞÏ[ô*`8\°¡ÿb¿Ë*b#F@-ÆÄäåÇ·š‘]ÌW®@Õš“»mz("1OwÔøs®Eú8e2Á;®:]MÅÄgRºl ¬-=¶ .“ä®nls?ö{N +¼U`¹ðVo©ë¶,|Œ­öYMókôË>#Ö-•“þæ«KsÄÃÁ(2ÕºkÏðƒÏÍx‹äº`óuƒÊ¹Ak&˜€!.Y*†¸S)Ôb²„Ël<ø–ÀI^°¤ÌÈß õÁ)ÛvÂ>´`wL7nUYÁE—ÌH«1hnЦçzÑ>xÔ’>"-ì@‹VÔRëŽÍ›A4QB)ÇQ ‡Ó³]o¸6ùÙÀ"&´ ì&ÅTþŸ³@á¼æ̯­¤¾¯ì?B‚`·ø¼ùiqÉߨ•‰@h’Ç#ƒ_ÃÄÆ+¹“å²#˜¾1õ+L›sÜ{NTwÀžŽVÆĹ'÷½¾˜Wõˆ®JºƒºA4•*/HóòG‚=Rv1å¾ïŠÒMT‡ eul2J~ÆŠí©’ãÈV½g¼ =p(ŽäòUÕ ``4ŽÜna«HÈq±]£å_&bòaÄ]ïÉ ,¨‚œ;z Qå"0RÓÈ—¹=ô…µoªR5²mîóÕ¸ýØ ,Vf7Ú®ç~ÐÚ $óÎpÓy¡ÀàÙ¹QÖº?w¯ Õ5)QÇë7I78#‡zw€=µ¾’ÛAüáÁ~D(€Œ³ÃÑӶƙ’áŠGš6j±3,soÙðøÉøöuá쯫~ós^„±¹h-QBb¸ymè)F!Ì5~ñöÌðf¦Æ‡p}6ˆanÂ&k_À–²þƒ/å>*éjÒ¿¸ÉùS¼CÖSì^Øñ´˜xUFÈÜZQß± ©5öÞ(+ÍXø›pÔ‚lHõW’'èp›|Õà5ÕÅüdÓ+ê÷Õ;Šý•gy®óKe,ë¦Zl’±¶QdbÝs†VÊ0P +¨½7›Oïrsß3³Ÿ¯±F„u»:hÕŸ(o¢RtëËþáSìyŠóòÄ‘¢¹ÞçÙEÄ¢VE‹Ô‡Ö=ÏÐÖ­–æªêÂJˆ"Ñ{”p*…À©PàN¦TÒÂp`íÑ f Pƒšs ,¬o4Öq‘®-¤•KÀŵáTÌå".êýM­û#¢ö +!#ôÀ•ÅbSäÆ܈Ì[äD£b dQuI@ƒË:TYãe&±AŠU+–û +ëÓʨu¡(y­âdy£L!bÆh#ü?˜ÅÝ Ô·÷eˆéPŽe͹eF®2‡·¯ýgŠ„ZÂ8tƒŒ¹ý¹SMÚžÓyODGNÉ€,·šè¤Ò“Ææ績>cû‰»iU{—Æ©¦’ÖÍmË'æÝIU7Þ#fÑâ7ôûF8L”ës8¼1—D¬Ž‹ùÙ +”-áOŠŠ Àá$}yÈ2bUU"LðÍ~Ðñ¡VˆŽÏ!î±+’ü¹B¤‰ÈUë¸à-æM-›Îþ!êîÔE`ç¸ó»ÑU̹•,úˆx´QÑ{šš#½ïgMyÖ–éÝËÍa¡+–ð +¾ ¨ÙEîuWøÿzÑè°0¿zYÕÕ@xl€šHÞ&÷ O4ÀAe n û59a© ¡?¤"ÏþÚ¿]—²[cQ¿!H«"D3’{j˪n€ødm±<(¢zQÕ å°º¿y¹÷‘ð0:?Ùªp¤+bz¼aceÈpZ>6̲oÁxL¤-R|RªÂ}P’,bßsªbXâÂXMwèç±AÉ +àï]Ÿy¥¶~#ÔÄ2Û7ûé%ÞkD;dNâ÷¨(„0r}z~#Ž¹á§Ô`³úéñ*/€Lg4YÃqXƒÂ‰5·ÇÝÜÆfdžÔ˼¼¥–Fè( Œ¥–`ÎÐ×`½ýãÙOhÚ ŒGô¼}¡³uG1#ëh›µ¯k +å¹ ¦ÆLЙдu9¿ÙÏÜÒG¶¸Îì½ÄÕΣ|ðpWOÌ”Õ[ô›¡ýp0œ[V +†ôœ›@EñP.¹äzn}ƒiÉÇj¸j˜Å(ü(AY£‡2¥%¹k<ªýðtçá€O?IH4=ü‘©À°V%5)„²õš>£Q#?ïâ²;Rù9VIss3›Híµ¢ó®§ • mÉÃbÑCÌdž[aõµþè~œè?³¤„(ôèU÷-έ=ÎñMÅýÊ$/ìèOA¼W­5À±ÞEˆ+ÃÓ¨ :EËúëâ‹às".0_Ÿ¹a=YÒ ò·?÷snº!…œ­ÌREÐá.bå¤m~éJ(ͧÅsç¦Vñ[!êœx‘P'ª^;jåè³ÏçÎàOËà}{ô@âÏ&ˆ×O÷ÅÕâ®ý@Ùå0y‚eûªýÕ¬`z“Jt)’u(|Ð3‚:Ë"ZÝ\ ÏKRË3î£T%ÛVÙaÂÊjÜr_8§”ð+׆ߎ6µ6ÇíðmbMRú…”Š µsåP“I˼ºMC%@ð Cmå[{–hgݶ}Ûlƒx0øAvE}\ÈŸ§LH—ÒrÈÕ:ż•ØªŽâ:ò³Ôòn…¿WÞ¥’øýÝ®tŽ ÃÄ@õ­œ?ÝYŒŠû0ïЀ†âÉ Wnáßg°\ÿÇÿÉs´7hA‹L§Ñ%Ñ U7àm«¢­oÖr{˜FSýA ë· ªõ\Ç|D<¦7º•àoöCu[Š´nQ& ’¯ È {ÕÏùÀ4žÌñõj*5;E4:¥ˆhéÐ%þ¨,0:é\zc8ì ´ó—tQÞûõFÊ)çÕd è˜Å + +÷TH®aˆÝoX×sàÇŸÇØý%}²ëÿ$Ãþ_¹³{úþK +½ÿñwë?Æ¿ú7?þûûÓ+?þí¿ÿÃþ׿úwý_þöüÝ?ýÃoþúÿæ?ýíßÿÃo~ý‡ü¿¿ùÃÿüÍø»¿ÿãü§?üŸÌA¿ÿÅýÝ?þîoÿø»¿ûÍ:ÄO'w¿ÎàCà?ó¿²ÕÐŒ^_e`¤T¥›µrÇP¨`qÂmô<l_/y$¡ñ¬¿t”²× s)8ùËùõ/ cøÕš Õn=¨×_ßÊLGETAÉâ(Oû÷Ùˆ¢ØçƉì.EgJWªIh/çŒTz<~õ(²&¾K\¨¢k:ÉŸû5äuÈÊ+& ?ð™A;â>Nb>çNòïëWÿ2gý½6"ìøƒ íÎÖ +lᆣíJÖ„A¹–õ'U®¯•S¡ˆrk¸šØæ×VÇqƒ°nhÍ_H/fwìR +èHD©+mýÌ@דw…)8SÓvд–àòUÓÆðl{'Þ°•„Ö‹ÅZн?„l Uº¶Bê;mè2gÐϨ€¢[×÷_C‚Þák€+V¤'¥‹|¸(¼_93í×®N×yÈÔtÿ. ¢fÌÌ1!‘L7„ÄÐ{y1ÖFe¬‘ŽÓ€ÿ™æ¿J l M»iËÇÃÀñãï{Úl®o"RTDP4 Ù®·JÑ%ÛfXVrSö?3*ï—4ÂÓF•Ó«mx0î?m¸Þ¿”ÉÌ ymcnV°εSæ±6è8TM®P<©½nŠ/ú›Z€zÔw”Q‚/ùÀNõËvúr¥^P·^všJ¨s]þ +u0U›ù°Ö3<©ã$1uáyž +µ{o.ÕƒšAA²šAZ©pÌdƒ¼&ÞZa¸¼)H'ݤVŠepÔȪ­û‡ÀÀ865E+¯¿ó®À„år?µ©gÛ¥Vø:E?èP±ÊìJƒ‚©…óTþ”ïüŠ2ýsã!¤ÙÒþDc~ÝÁãÛ·âì°ü(Ç‹æ£N?ó.Àów|›¯|³Eòú”„ïc¶ ²§Ë¦¿úŒðÚÁªÑ×pM »ÓZˆ}ÊØ‘çÜ¢ªàqÖx…ù¡4^5 ieKK±Qpã¦U±\ ¿Ò0Á hl •¢\î¸%ª(»þ~õ­¨XX`ßQà‡n·6ÖCÓ Ð8}7£‘…ºâiºRpJáŸgo"æê7Õ°+DèøMB‡ÓÖJ8s…ÂÊžºäfêZc¿ZOfOvÒø™ì†“Ý÷Sä“"…Õ]Xg¬ý‹º©Jñ×¢Lãï4@ƒÏ=˜}†,›mwYXŠ­! † .ùvñ]d‰VåÒhô|FÀ.òÇ!¬^è­?r:LÊŒXË#]þtóÑ1ÀìöV^ƒÎìuÁID)Ÿ["…ñÛš,jÀM1@Òsp¤Š7GZC H*<,’bEñ[ï;Ú+»_ºâ“æ÷ðþÉ8šåÌ—@cÈ=ŠþcCë9&硯LÞízÙï SÜŒ‘Øï}jÈåFvŽÅ %…PÇ)g½Äb*”A@—ܾ¡ÉJÜ ¶}žâ»îÎôëÞ@ pÁ´, À ™˜eÊ +ؘ_hJÝ+ 5”¤C2 ª6åë0`mªØ®ˆ·•dxGߌˆŠ‡r‡ lqœ‰éÒœ9‘¢Š(®?WNuc‹+k“} É_“CµLkc1Ö=¦¼*$ë]Öìñg>_?ß ºo¸¹Ž·éT¥êV¼í:} ¿£Ú¡Üʼ"åP·;æuâàüÃýøz³p’ÄY1²6¤HSq¤bZ2à%T#®G|›(ššoY4ÕöqßîG´ºÖÑÄrð®$Ïn”&!GÒžïR€s©Âc°pCãÑDH1k´Ö%Hè-õ[Ô0»çŠ€ï´‘cuïßD8…5paåsã@ÆßÒxÿUz™GEýÇ é75åþ0›ÌÇhi +r¥ÕÝMlÞ~• +Û ÅÓÀç¯ *œôlèêyͺçW•ïFëkŒ§f¤ƒ™¬ºáVâ /÷6a¤yrèODIöÞXZS7Ã6v}c€ÈUÜ맟’ˆ W?`»ªjÁDZ[»“y¦oûÐÛÄ}@«ž8Ë1.·¶«_F†¢N+æ×ØÚ·_f½E/ý‡õJÆHh͘ûI™g²­÷¿{YØGÎù± ì•…7¸º´P2Çöñk’‘ûÞ¼þV•ãúBZ Qôc=áõÚ ›(W*ÉÚE®eeö dzjÌÈóå\c7?™çþÉ©bcøξ¼XˆÓV»iôgw=¢÷v^këQ¬y‚T猒þh,†ëšvÁEÎëÞ¨šÆ ¼ïQßšËÁÖö€Î³f~™PÚE5{”¬"Ï:~ñ÷{[æAþÞyÅkã="ˆz+£ss¶tMÞ&RaA¯_M]Å¿Ùà'ˆ³)L˜÷7R'b–ºBܼpࣾ6\ÛÜ–é{l`ñ×Fø;´N}¹~Ü:æÔm±Û´àí<™›Ü³Dj=2êëMÈƯ³;I‹§×àƒ\ÜTëë7Oè&¡ÕmlK²¬‘¬¯Ó(hNßà +H72'©é3 ¯!º€ çC:€¶¤†šüyÈd^¹©2œ%¾µ-vE7Lä<&QkjXé V ØÎ'í: xGÄnôa—ÁÁL agN䚦í–\ç1ÀÃíñYM'¢È]ºQ:?ÈЮ7©ht!H¨®Y§$Âþ°EÀÔÌ}d gùÚm}­co¦QÙÝÐplF=–jšÐ\·û¹@E¢Y1JúLÖà¢ZN…dùCù/Öt"­gÍð@£Y•Q¼#L§} §¤m%ýËíéòLžÿò4ý$ÀP± …˜‰%b:9‡,…]%ì"ÒÁ´ž4 Ó»Œßz¦’Míoá‘8ô)ãxÑ·ãFÀŠ`rêÖ†X7âðÄH§<|ÔC£t4ʱœ(»®@¸©+?ÁÄц†“5y1]piµ8À„÷ǯ‘(&}Zi” ñTµ +ÌüÈ‘êô\.ßnбCÓvêaZlŠÓeí³S ì÷ŠÅ×C示,ésH¼‹®Õ´ŠZv×"ó<¶?3…±CHa«?†4]¾ÅÀnKv âpÒ¤ôÝún#ó%µún³r+å†ûy¢´°l3m¢êÚJ ˆ†ò1b …k¤¿(Y¦%uly¤Ó5W*ÛùóùjâãßAÅ4HŒuëà_öe¬}í_¢tŸ Ò£!ú9Oô4  LåÜæSIÏ pË…8?®SôAyð9V7ÌñpU4}¸ÚÎ<1;×qÇ8@ M„á|®ÄQœ>#¢ù<_ÏáçpÀÑŽDN>âY7‡©3hpð?Ö×ê|,]o"'1¤1³¯«ëý CpDh(2GµkEIš,eGŒn?€JŽÜT+ÑÏïÝY•K¨Óáb=äB³ÑEKÃì66ÕŽìš '³$†©Ýì÷gDª `&H’+Ö¨à-Šgz䩳líZ^ 0ªñŠýñ3¶ôy»~Š9Xiú[`JÕµFÑ •¹—îÞ”ùWÄ¥ƒéÁúŠŽÝg„W4uä k†ÖÚ7Qeàð¤±ÊÑEMÚTW#Íw4‰ô0£nÿZñ½G Ø`6`HÄ»•Š5mÊî Y¶k5àl-ÔH)Ð[êH­õÏ€ç}|!ë}«¥]ßìæR¨ïO}ÞœønÅÕt­ZÜA–; “ìH $Çc?Ú3u8MP´cÎ~øÄW4p#bÛ»æ>$%Ù B·¹›Þƒ]Ñ®ãu *àߎ &wèt/è"€~d¢Ñÿ>Ç’wKÜW”<ªDß%"ÎgÀÄÎðȘ‚^ktmpV]ƒ绾I¯™Ö±_@x~ùÞ{¾÷:?òÞ®DìxãwQkQ*ãþŒÈ-ôÃ놸É]¿Ù +ØRZÜÔœhsFA²JêGÄÝì LAd>oQjoj*Ò xÆ·5t&îÈæSf@6Ÿ–úºX¸ÿÞN>žOÕÒÕǯ;Z”1ÿ7üÏ?»ªŠé3{=•·÷¨_U’‘då¸yûfÉúT +dÛ…i]{3†@¾ó‘ûD‘–(<e¡¼ïç¦ð£0بêý™õ¡ì¢WòÆS<Ö¹˜(¿ÐÆØe¡Ìb´ÇñY¦úÑÖü +FnÚ° “@"ª‹”ylÜíu¤«“‹áŵ5$3þ³:©qj&õ%âŠJþµKWƒ{×èh×>$²¸à~.}b8{\¦…gQúU{kêÖ¸ _¦/Î'°°Scci%ï’]x9€'åÇÏ{¾›†”œt1:Fp…0®€œùqÉBæe—sýä­?G)@ Õ€hù+µl@y*}/¨ +Lh¬ +^š"ÃW‰h(.8]úù$ýÇN×ÖpÈ\o`ÜBÿ 5DDmW‹®í41ïlƒâ‘l¸ÉŒiÉžYÜ(x+ªónÀfOie3È!p´ÒVÄ¡<öŠ°ÚRô\y +wc¾z†DPöX/(õîê¤Çó•Ç¯ßs¡° ÓŠSÇ)úCŸqV¥Pnÿ&tÊiÛ‚þclÿu^ÚÖó…ÒTmàL³P…²¤ Ÿ"Sy¸T|þþz{GŸMPÚÞ¨û;k íµoÏɵ5BT?0.…hV²$]žBáJ9¹&”.Ü™KQ™ϲ êF|.̦GfðRQ_KùÅ&RÚ3§ØÔ„ë#à·O?–*ÅìøѽíeÝ- ½|§údø¤O!àÓ7ßN‚ïQÊDÛå[ïÒz? +$ÎR÷}cÚ6Ò͵¦µÒt8ÃJ¡c‚­—›Fç'Ý­Ýô 5ž:÷Z8… °-ç†ŒÞ úIL ðëΈžëÅ î­„ÓZ]+)@dUšÌHV¡_¹ý ¨Ç8Q ðyIC€¥Ûˆ€(‹<¯ ¶2‘œ¢ðSõWuëµ>pÍy7C·6õk¬¿™Dö_*fYéBÖþzþ6´wØk°%°;Ãv6Œ½¡=àya/3ž½q¨˜N@Ò¡²Z7;ûß‹ðîð”]“X1kdqÄCu9w‡²ë^æºu#.$U€Ä#½²Ê—ÿM&­o÷‰Õï½CTbS²¥;ë’÷~=?§Èo_æÖ¦¤ÞúpÊØœ)A±`8Ø(±cMtOÀÓ=<Ú[.ê`Ãhg°tÓìc +Nzv·–ÜÐ)dȄ¬˜¯#”à^4¥cwv^®‚î’[\Çè’V±\@ÎdÅ) ‘+BÀa9VltN:éô»…’²tã¯H¯QÇÉêïd5¿ R…Yf¡ŒtyJW:F^ŸÔTƒäy•Õ£îß$x×2æ¼¾‰)È(^üz‚>^匀¡ëßÚ<x¨u‡Nä¡GÑ÷¸ð&};QÒÂå·[ +o?á¶Ò„t¡ #¨c¼Å +$bLÂÊÊÝ.(jʾ-IdØ¿GVkùF<>FÉB‡"Ö©ó¨@ˆâLB©ê n#:­F›DÀ¢*¦ZK¹Võ¥gªÅ¨„X.ƒƒ>ïSYÎþM½î Ç Cq¬é€*@”Âdå+æ½@faìë (z¢‘&?Ñâj7¬WÇá¬Rù‹v„fì]°ÒÑoV< +„×A1E¨GŠKUñ(¾'Ó‚„- `Fâ &¹^àžo¤m€®„`¡ +:IæËYTÓ‘­C÷zÕÂÝ$Ç Ë+wbü÷Q9;ôªæÐE$„ƒwµršû.‘ú–x#ᶎðç +”øT=ìæ<š]˾`è ˆÝ¯¥b[EnF@æMîÕ<™´û(\Zµ¸œXÛy\Dç8ØD å ÕÝÒú†Õõȵdv@x<“N'ÂBÇ$8hWGƒ‰ÁÚªó8¸€ Œ¶è”š¹Fdf2¼u|StK—GÓ‘QŠl”cù¸)8 M$Ä!Q:–>I©M3‚Ø]ØÃ@žGÕÖZ‘ÀúõE*w©Aל*ëÉ4åÚ/‘×9»>DÐÌyNX„DOÔ¶l#üé,Ï~ì´EžŒ@Vº®±„®Æ]‹7#4ò˜¤³"Õ"’îè ~Ó7¿âsÈTˆÔÝ©HXÄEK ]2‰õ­¾(&u,°6ÕÊ¿¸ {0†$ÄE•×¼*`s„ QÁSÐF÷àP6H¢;ú><…(‚±HºR5e1ï¬ûéN&éN¶s´Ú·AÀ ì³(eR{Çm€ Mp +8Xj+#8ñÆE`„ ø.ÕÞ:ŸxÛO[ït5FðcwÀrù‹±ñçä#ôºÐ,ÝG`l#pÖ^´×üü&ß\éuµš¥Ž*Röö+°jÚîPhY^e/}))Jr•%»\%… ò/jEèíŸ}ê¦ö¡›ÁTT$QÖ¿kJ¡„Úv [:‹ ŒQ«¦k&3/È…B ù%MW‡ÿçŒý•":V–¸ ,E´Œ LK$CÐÞ‰ I £è,l5v–yá_ŽèÉE!ö„ËD%îm‡í€ÃÃÁFÛÁBçZpF!°%;©Ž\¨õ ÆØäeåpàëà)oXñJWA äÁ<Óæu(ê׈r L T¥ãLd-q Æ6cÈæŠ&AŽ­ŸèI1-§I¸jÇ8]Ó3á&Êf·€©‹|ª*ݱ®iOÑ/ê{ +¦„:@òmûÑ1 t5# æÏä@v“B6¸NÛ¨jÆ®Ã^il=@þ9MØÝø[–v¬.‹OžWáJÚퟕƒ£D$Ârë*½ m,ìϱùm…1è…™¤ç¼üµ•mÓ¾nÒôcÁ ànÙ}×hÞV±7  < M^µlXÞWGœD›ý³º[Û(:à›2!›yÁI /æV»ü!T•Â-î€i°'wǘÐÃ8„Φº¡Â`M”•¶Û&âeÈu^ÈNc°ž<^É +K’Ë +³,»q ÈÔèLçf½:î²Çƒ +`ÚqàhÓ]XR’zN/½Ÿ‡c¡;±èÍ94V%óÕ<³L´ó„mµìqŒ‚6;‹‘£ë‚6?7 1ò,\¤«Y"{¢ +øW£!I€)Ž–®¤Ó( ¸Ž”],•b; ÙÃC['  L M¤½ìËjÛxAüðdÜöË PÅzÙó¨è+mŒÃN·#ÎQoFX"AG“ü¤äÙ­ØF® $ÝW6´pºZÒÍ‹dT;wÀ¾Pí·é|”![(Uúš\Ýþ”Y×4MNÀ¾ŠTß…ºµp0ÉʽTÅVæ>暨/±:Ÿ(Ž¢>s ƒ‹²3uíùk;¥qJŽ`o1¯èïÆ(;* Y€&çô±*ìÎÅ‹ÛÃ88xTIEj+*ÜhDXYMÊ^g6ï…šïˆ-á® Àæ®4ôåJóš¾roô_W˜ ¾uɬú]ûŒÃ1j.ø‘F)ýM±Y RÚRéÞ0Hk–½ì< €'Át>UH%ƒ¢\o¿0È’¤iDMª»ÐyŽõôEéTåóLq(jéRõó¨µÆãc>éRôS;Æ{ƒfêŸKÂ~Ê8?¶Ý§XL'. +–=67AZãjTÛâ—‡©HIH+KnlÐ! õ&"ËؤÍß¼  ®à)ÁàÓ÷ ¡ùëV¤ ¡@â´ÃuîOu-Ò‚uñ°²äç”® +1§ÌŒP·Ò‚ ¥p ?Ä ÏKùÔ\p5´/ª{w„çw>]âÖ»½< )SM!¤6Iü. ÇÊA_ªh¡ >G!;¶F¤ÑŒv‘Zå¶ìѳë(íÏ G£@“f¸[Ù¿ÝÝ{Ðóˆîr¢Šˆ°,*ŽMç9d®Ì,ú‰=º­š Où¦º[‰“4ØÊ6¢lù`¢Â`ÔIëMv"ç"d#ð`û5yR‘ÚPKùNŠ=@=ûr´©á]Õ"?5’=§ B¬°•VÏ k×}JÀ¬“ˆ²¸šPÓÝ$Žþq"Å`G·í0Ks“›"s”N-'!¶lDð.¦H°þ©õW×7ÆÉ}ã>­½°Oñz©«çðþÈÃ,÷G€ÅòÄd0ÜkâNÍ.%i¤Ï”×Q1NÓK#h+ÔALE„Hj®fdž!<RS#¨`ªÌÙƒFøaÄ tS_€P´}Än®.¿@–ÅûXŒå·2¹…ƒ…ÍpjD¢öÍyÚ‚cÐ ÈåÈÏáÇ`ï$7€E~òVÙ4‘ã`”² (öYfÇ… ErŒ˜ÂÎ+ù´ðnív¦Yß(ÓÇO6·QŽ½XYÚÍ“¥q/hÊáøu´.ˆ¹yPêZ.ó(˜iuãpê…°Qö)pQËRÿ¥gðëE‚^ös–ÂV¯2ÿtueF*àXz&Á(³Z˜eDËþŠJF$WÄ‘I0`@’g_¤j§h³‚`ˆ-Ý ¿ ø{ž:06–&ÇU7f§?„ßbêñ€ •ý êCÑ +ï³ê#¯MÖ–6^¬ê÷ãDêÏ6CÜÎmÄ1¦"NbwÏê³ûkW¼Ñf§ãŽ@ó/¢ŸËâéhIOª´I·(²ý)$RŠ#¸,# ÛD1R’FÌà‰]Î$W†µ¦WG²8}eB‹¢ }\ÄdùŠ¶·Ça3¤»Õç  Ï‚ñ£<ŽBˆiXÖBù†í¹{íòᡨiÊí–=EázÉáˆNÎA +!€"|g†â(Í'ÙÛÍná÷9Äô.9šcñ¨qÛÒA*4'€·¢Oƒl…HÓ‘Ù†Æ'<€ûŒHyY<N~®ˆG òàõEg:9ê†QÖ®ú³ü´Nq ü¬ˆFsVKOmúTÝvÂ×år*Õâèki`Ç<5ÇvwPED°æ’†MD iA©™í)ÜÿšrÚ‹Õ›BàŒü¬zÕ…,VL‘œ¥Ï‹0Êí¯º”.ÚÑíDzb,ÛÛ‡VíN™ñHòp1p„|¸ÚsT£P r9ƒ¢Ãœ “ä| 8FØÚÉ^L8AÛ d†ÀxvtûU‚˪u¥R3ëº^°w ¬ü­Tük­ANeè»Ií̲3ÎÈ&73¯HaÏU13P»ã”h¸µ˜8à>]—"¸ØÅf¶À¾"Ÿý„o%b"gòôúºÌúFAðw~¤kšÂp·-‘õX¥"·ÁênûƧ`í¡~Ñ\©îššiâIÊ(áØA¼·9˜Tµ"äÎÞ0•iŠ8,…Õ×Tyœîņ²`TjLY”yâ“h-’Úôè½yÚ3Ù¶c(þù#sm‡î¸ò—!È”2ST\#ß'd}Ñã.oäC€øˆ  %88¹§ªRø²ƒT I¿NEÀÝå°™“Âþc—™ ÏeÞp,²c UšŽ’–­d\4wÝîºÁ¦Žü|`ëæÀ)ócèz“hyà,q@€à©ÑîW}·¨æ—çâ|ëÕ¹¡*‘É&ŠÉèCÀyµ°Ô„źI•Njè-ø 4»­[0í@˜7:B¨Í—ê Ÿ +¡Švôі ÐT:÷®‚Åk…ÆÎãvXùÙ»;ªÅÒP—ÜýpÜŽ™ë4úa΄8Ðáàý%"þ)¨‹0^‰î’2E²äxëÌÀ¤XàÉ—[ÿ’ž;?h=À„6£’v~pGíÛ¢„éžõ¼JÖ ²ÿ‘i+H&Òn§“‰J׋JHSâ„rX@I¿ÖUÔSÜ’”ê<ÑyÔªom# åî«à+û³Ûãéè!½‡ ’þú"݃–ÝTJ#«#Ö,qƨyü$B]}¤’Ó°°ac…VfåPÌUØþN†ƒœ mº"WÜ\ü%«N½u—d´,*^hŠ/H07㵯õ øU•IØL}§ž“ÔΩ)’ÀÒ¶t-š>‚ ¾šàCâ~ݽªÝ™^íŠ >‰V•ÛÐRîptÍŠ/ÒBÄÜÞú7š4ø~e:3*‰’4¶sb‘ÄÒ*ŽÜVqRž,e9ü!_ ž.®¨7žae³E’$ÏeI½¾Ó%żŸVÌÕËPÔ;ù7,¼/F +FE°"#Òí—T>B!ô¥@P$1ŸCÂe2™À8c*ä÷‰µDõἎß{}“¡ `VQ—öîÆûSÆÜY2)ÎRõro§Úå»;dI.—é:¦d>Bex”ÚÃ–ð„½ +ÔØ +.u0±[p&öPâ`–óvôP¢§4¬@/‰ !92ÚÊ’wb>€Ö¤~I<¨ü™,1Žƒºf|ÒéÔîÔ÷H¬4Kv…´S¬îó-Ö{Ù¤=§Cá8e[» ¿™Þ`pçÂÕMåJć]É äds Æ"Ì·wµN$˜ÅK¦F£‹wBLbÁ•à•ÓÒ%ºxlöË'6—Þ†æ½t ¨nòRÈouèÀªasγÑsóƒÁZr™SBýù5œºëH„7ؤ Øæ%íEì^ÂálÙiÀú¹8®^¤ÂÛV`°‚ý%"¶1*½gr;-zJZ’n†Å…ì.û$ò&X^RXMÒƒ¦„ú蜰ïG”ä@d±bJ™ãAˆ‹74P'ñæ‡(‘PùiPéâªC5®ŠÅew©#º+ðSAa€~¡ ŒÙ±PrÝ¥„¸½"&»”:壣NœÖß¡% 6¡üZPX­eÀ[ÎÅ7`ø è{äY²§–Y„‚õ ¼ÿRøe‡ð²º!ÇY/G/×þ¯ýfzÙš~®,CÈ=@YMŽF®Š|HÉ£Èàx®,=3¿P°¿9 mýsÔR«òN™ÅµÙ¨ÿØ‹ÊvA:ù´ŒûP«=AdyŠSÆ>ƒûò7QDiÊ©À[Ð¥ˆ˜ +¸d,°õ +27Ó‹•Ê ¼yŸÒªó,Zê£Çb³¦¹w^e¥¬Í0$æ$+#ìJ;Žïç9Ó·Ô¸EqÆq:À%§k¯è@-ž@k禆p 1jybò&÷ z)kâ5×麓`³¼g(ÔÚó¶ ðWL^ä™@¦Çk.=“Ã몽C {·©ÄÊíÒsÁ5­ê%©öóÑarۇćn';·2NâkMÖ‚¨¯Í(–ÂJƒ¿i¤å¹¤²z°>sìÔ”h.ˆ€†ìk§•ùÙä¦fKˆ°€Â>p¯’à`Î; _¼©”ì´Û.^ݦ_ D[Z7§–gŠuƒA|rbñ4ëÐ# ÿñjgHØÆ!Œá€ ¥Ê‡ËE/ÔĬì#IúÆT†ó#IûÕbO²r´ü´JÂu6~°q€àV¡ƒ‡c²§¶Ç׉ºâ3-ËêÒlX ³Áô”Ë‹ÜYê—–™AG“ßö(`RF¿"ŸCê’f°@ó áµèR«³ÿd n~P<+ý é›ÄS©¿òۋ븠̄Äú¡ÚóòáÜ“6µ† §¨¾ôCtЄî©N¨Hx_\f!Ò<ÝчP /-íg:­üÆSvNb½¦-ô¡Ï³þýçÌ{rtqLÍí³¤0RŒíîkÛ!­LåòI²’:a•õšZcßt±kµc­„T$d…-çðÈZæß’pP®@tè~A»{R‹&h—ÂE’°Å¾®ªe#ÕO'a\LV›. +;‚æ2’-ŠcNpЩ¹Q¹Ìôš@ISXà€;e@Ùk: QìPÏbSÑ4¡jÍ£ÇTÖ- \³¤åéúsrÿùó:—ˆÓ¼­ à°óáß`ç’»ë¹>:ìý°·‰Nn>û+‡#ÇÄ$ÜCªP†•¼iÐ5YÚé«A]`Ù1éj7}PaŸõdØÕô¡FÁ¢i™töÛ™\µ2¹f°j}¢Ú³GD=_â²Mk©ûLƒ4è15ïÿP8÷B~Â1º1•;VY¼S%(óþ€£ ‘$ÌJ~$¨”£žˆ=“—üØæIê|±€ƒë‰Î)ù_‹Š‡lÊ“å¡ÞBËM\¡CjÇôƒG#y"+BF¥³J³{]cƒ—†d?a‘úAô“…P[ó”cë¼€ Yë¾ÀæºÂÑ¡–[Ù|öl^Fá( .€,à1ouè𖩈Zñ)J*8í/ËT±PùÆu<&ìÎýaT Õn$`Vu-"èAARtøÇO¶âŽÚ–ð).tAÄYUq•þ¬ ;pÿ¯‘Eßb™fTèaœgÀ&…×P| °Òò¤^÷üøº6ª°V'r`žH +0 @¦Gà4Bb›5Z¿‚®öh\ (õúžŠæÍ2¯º" Öý4ŸDÆT¬‹*ŒH€ªÚxÒD¥_¿(ÉSJ‚C|D÷æ³ÒfZ_¶ä‚ˆsÙaBd‡U²‹‚^Šµ!ë<ÁsN¡û9F¿KK†¢—å“Â5Ô%yîÌQ†™I£` 0Êñ¹`5hF±.¡m¤Líâ²b÷Îæ_p1hŒ‹Û£Äáúbaße2ž½•Š|¡MðÆ}(>TÇ…{1æ6=ƒ‚ädI šô¿TPrÑNÚ7¼¨éí9`=íÀH’£®ôIùDÙ••õرòUPÙ…™%.š5ËžXz¿{D!8‘ÍÛïS§JP˜54ùïpÇ÷ù(ÎdÓÙÎ% +\5·²+¥a]{Ðli®SPP',,ÈBR›·ñÓ£ `”[–¨nó@›rÊÎ Èut™qr‰ +ŽõW¹(#¼3ÛæøÖHQ¦ íV²°¿ÀCvb u±$6Òô¢²ìºN*¡-oŠ”ïià7»™¤e¹HÐúÙz1Ê9ûXZóc³»úH¡àr-1JFØFùïú\htižê÷ŒDªóüJ/s çØtÃ|æx³¥ì_,¤%$äìÐØ`j%?Cšï³PÕŠ ŸËJls™(¬+%°®‘p‘}ïÅ"bXÿRÀ!ëÙ½P˜V(Añ’FX9öÚC‚ŒÍ¥¬*ígûªaºÓ@¡Ä^ñŠÆ_Ð¥Qø_/@ˆ/Ha2P ¢ÙfmL¤“5µAXl¢9ZhH=£yID +¬¨l~[T„P•w#Õ6â•‹ãªìნ3=/Eù–ßTTò©xf'AÕ-B@m‰ ,¢íô >`g“@>ûÐÅË +3É#,ë‡Xly¼¡µYOJp­náòhÖu -8ÎÍÁ5‚¢–éqóâkÀoü¤è„H4+Äí,q¸13×}¤ (Å:üuc™0¶(E)BJUÎQ”»k»à mšñ%,§LK¾º`CW=z:Yw +¨ïœ×IŸ¶úÎ/ðÅÖf"ñýªóý$·Í§h¿Ø;™Ž6r9=ý¡v¬šŽÿ!ê!L•HYØõ¯bñ É’îqr–Ùµ*UK’܆ž|•‚v¿Öˆ¢»-ã »Ûí"T§¡NX ¼tÍzÅ¢è$žþƒ¤¼SUÌpà ?KÂ[ôbâË‹Ç)„7æ@A[I„ç:&Ùéîrr{“Fµu8S³DþÉ)ƒlg¾bFzÙ|íÿáêšÓµJ*Å‚V›¿çÈÅ©ÐÐL’Ÿ0/”X¯DsÖ1…KѵG¤}¢ãñÔ3K_üŠ¥ +z‘gsÙ57éd§ÓÉhVmYÕ«6 +ØQp†t–+rJ]ºCM½{YS‹=~ÏRÅ¥EW•üo‹Ó!Iqú?`˜ +âä/7U….ŒìngpFE-ÇÇxÀ¬*Ñec¡#_0äóBÒŠŽÕ¿)i˧_ü›¸¾^ r‚4Ë®ºú .&qÍ”çu8¶2­38 FFb¡‹-]^Œðx£WÕ†ý›< F©ÔcëžšáÀ`— ×!Œ s4»H£À‚ôé°¾,€Ó!ˆ¹˜É$À°QËq K¶âêv·( }t6WF%Ì€Ðe Ý +y—C×8PÊ©¡Øç$ðâx%‹©°Hv£@m’ ¾Y7$¸Q”á»`; Ï0ù@ÁB Z¨QÓS¦db`vµ'˜¦·Ç¡jªV’}!J#q!JÃòH`í3‹^Šz§ŸÅ“I€E±ZCÈxKºÓø€i¦ËX‚æ€pŽª‘*)®OÌu@ÏÇÄ]㈞³2ª"Óš OUÄÛsÜÀIÊE&qØ ž,ºI”¶‚xÊ¥ª*œª´@˜ JÐü<§ƒ¦°pó{—•‡ŸòHÓ±ÑïÉe~S²’Øÿ&‹^¢ðýx„h;:}U²FHZ°å}“Dª0<óÇAçÈ®Xfù$8v)“_"``ÊEõ(UH(­«¦„ÜM½Ð¸6Ÿ®ÔT$ÑæƃD Šn© LÇøÐè¤anÐ]Nàp˜‰†¤)Ì|䪧-T+wT©cNZNÍU4'¥ë›Çb-»§ª.G(é[Pïœ0€´¨ÐЀfOsŠ<\ƒ8è6BmfEÀÚ>Ò üÄ VF³ÅÚÁC$/CákNŽD¥Œ¬RlÕú–›4 œáÙ1¡·W€©=!¼‘®â)ÅnËà ¦«¡*DÖ¤ˆÔ}NªuëÂÒA&Ìî¤"IC‹z>ô³¹ER%l¥P$Bg=]íè ä&îŒdãO½¥i¼Е²Ö#ÉZ¿Çrp‰ZÛÍJØUá^2Nó¼;a¨ W0nsÄßA v/{ƪp±Ûr)nÅAwRXjl7}\ %|¶žÝïHËŒ€ «þÅÍMe5Bœ¹=*\_ 8ì‚;CG–¾O†³œüïXÉòip@ !‹“à—ršáù„aµX^ƒÔ„Ð@Ïd \IÙ‹S–ÄÛ¡iW (’,‚;8€³ÀÙ zªYG™vZnÅ B…-ê—8ܼ¹òWPÆj5uBdÝd6°¹àCH[8Ùì QY'ÑžÌ]3Åkatªz‘k $m?M[#¥5I|U5²i4hÌ>ȉÍÅ ³Ýí×?sB‰iá’K6XbÑ]£ b5t!½J(Sü<âÂkZºšuwî‰ æ2é`1.'ÆQ€9 š@VAÊÕ€¦Jý€ŠWÌÉ#ŠÄNÇ’Šð2/?éI¦ÑßN4¹ò,ž5MòsT DÔ“2ZKìIG긨va ñgà{˜B KB•v/OžyHªRàwceYˆ„õK…7»uHT„"hc²,1¡ìO”3N2®6ªZ¦í484(`TÒW¨‹ðnÜ*x@%Á°A‚óPšÇWi±1ìžqõêõ5YΊ q*×o)W©ÎS²°X8¯ýüËÔ%Ô:õ†y5œ×¦ªkÈÀùº;ò€…þÕHEªIrWj88¾9‚\pð¼pâ5 °‡ú =4  y{ÕÐâKò9çhÓšœR›„àk®R$Ì¥C1ãfL>»ÊS’h¿#£þpÑFQ¢¢Â”ˆ¬q +€¥2¸Dl’êˆ îƒCJ~ŒÞÕv'œ¦åc#s8ÿºcNy Â«—î9uÐfyðb@Ê{uõœ4›ëÛ»ð£Ÿãô¯Þ5ý‘'Úœ9ý?Aÿ¹óÈgE83˜ÜPkQ{ß,{¹7i)2A{$BŒ€ç0R%ZSURÐíçv z%ÇÓH©3q“È!v¡û8F•-®A°ž÷ûB ¶%)KÐ;­蔳ó) oµ— +ìºè˜ +á­O„f'á¼áö$qLðI£Øé…À¤Î“²`4¶vv‡fTa)BU*­SˆG K­¸8"Ò” ÂÅ^ Þ]ߥ(Þ}û£¦&s9b4•kí>Ä¡ï(€úI]¯úOªžJƒUäÁ„æØg&VòðÐÈ¥?ÑáíÊží%%{¶E:Ae‹ô +g #>…à°[y K¢=%‡ì%*5Õ÷˾˜½'yìÁV—xÖ¢íÛÛÅýÝq~=è°ÇA\ÔÄ`tçC–H2P‘}R…MZ¯š”Žà &ËœÂ+âûõ.I;[uˆéÚz<¤סû(ܧi+ˆ>²(î: iøâ¯?#$/i¾Ë\ï ‰Ð }}sÓÅwÿÿ%6c%:̈ò½[Ó;~£(©öKñí1»Vë ðëKÕëSØ¢†[SÞ—1öf¨†ñ; -CöŒ˜ÅZŽò>À·g(&ë}X§¿x­ÞŒŠ*uBç´}¦‡æN& êa»Á†‹ °-!G”¬›QEÛ4=^©ZöGQµMTªuôÕ $~éÁXÐœäÊäüPúØQ§éOŸ +.³Ì£4÷â!÷¤bŒÿ‡›³|Ôýñä›1ãYBZ·ƒYž¸86¤Šõ©ä ŒÐ +&©@Nî¦Ë¥c6̺F8D3¹ñü&UólÔÇe”˜>U¿"=hÊdILA7ø° OçaAÑj´úB\;f×.²ÉîKåÐ$pÖ|*cRµÛ”ƒÞȲá–åã%XGÕSò¹€LÀ©ÈÎbˆÛ'òÒ3Pe£G9¦Ž¢‡ÄdDeŠbÒ¼JŠb0ÈÒ9àÕê·ó…)²ÀÕy}O€¥‹›6×UWÍú8†<>BhA¼ÄXFéóôq Iƈy55¨Â„Î •ƒ®íDm©Sühq +'æ?ñôMÙ ê-/ M sÙuŽÏ}ᵟAwe% ·ç!¥S +•rŠx(¦ªÒM+: PŽ.f¯"Xðx&`ÉS\O`?ÑfÓém# wàÿ¤ÆÄuó-bŠ^8«. LĈá?xýÑøòGúÊÞ*`•×å*䎎o‚²–F°ÈŸ`;'øež\da˜(€KÅ#í ‰ÑdÅ)Ì,ECµ´Ý¸P»²ŠqÃ[F©ÒFÏwÕú*qõšdº)¹xL|$Ú ”ï Ï‚-ù&I³ +Øåß$¡r!¤Êü&Ð Ë…X9£GNK6…XÖŸ€¼ Þ½viW—Ä4nŒÀ±ÁF´<öMÁV@`´Žzy­bµQšÀÝÍIéÇ +)‚¨¤Ä î—?Q´Í²“sY;ÁÙRrHÕ€r9ªäx Õ(Û#>—¼1»ŸVv@™[Ë·Ž”ø”L_£'ÑLî(…ôPjjn&O“aÊÞZÈ!Ù 7d9ÆVËiô› òÜú1ˆ baHÕ;Ëšgªqþ¬ö¸Šg”¥m¦Ó0¶ÁIjµ [ñŵp«"¡‚ýXƒM6Öh:Mî"ïLf«Òiö „o˜õ]äÛH%„{/>û)ßEÿä—þÈZãBgÑB!‹„ôF@Ö0ßmFáæÉ( cŒ*òÕá<¼$*¨Å8¬(9¬Hþ2€H¥‹ÔÆé0‡&i©€x|ˆŠ@À#¡Â /`bÌGª”P†H¤«E\‹\PXéN…wŒs ÖÁ7W£´9¶KÙrŠ°*…^o¾ÈH¨°ðö«mqŽNå[LÞ1QvÒ@ Öž•ÑîLQšrÛ(rœ, +<Æ蛀L£$ÞD¸vE˜AT·¿ãVM£@u&ê°e–9‹3ÄÅ» ÎM•r•È3»Ÿ‹V†<•"˜ôbQµ“¦[¡™Šõ’Íš¬•~<ž@É'H©x› ø*pXÂLÁ +ˆpuTV”²·‰òp!X_ñ—U_¤®/ç%A™hÛeBI£Ð-ærBõoR)­²{$§G«áÕœ\Ô9¸!J³ªF@bD mút`IœçÀØôhÓ4¾ºÔI.”|«c¦ƒË"&xûxôS¦GyÊpXÞ.È,+~Ž(ƾumaCÍA¬Vû3·»DƒÈ‘§Q†E6Nš·,tÃw ¨¦”·£?ŸMzs‰>i¹£÷@Pä-Ð\r)ú'uœ§“ÁƒPÝœ`@êÿ-”wá½+*T œ7 èB´všØü]n»X8Ÿ_,… ·Î m½©‹q¸bŠÝ+jqCNwì¦YbZ ‡Žõ“ºßÁpj‡u:Ç1 I¡¶*ƾ*‚eåÑA½àÿñ”»/bÕA¬+yÈ«W¯ûÛ4¡s;Ð-rˆ/‹>UªŸ¨•Ò'mrxóÛVó áÜ%Ÿq2ÔK|¥ç8+ Pg“äI²ó@€¢™QxÒA¡€ÉœÏv”{.I¶ +ЄldüS½1‘د ²º08§è’ Т@g+¢œÄLyE†Þ²qœ((ˆ˜§wRÊ=”l§§ð´æåWt!­ë1Ĺ3êBÂ:&êp~Œðñ'våóCNñ/D@dÇSAôyœî23yöb8p†K‚¢«ÃAz:H+¡¯9±ƒ¬ItoFôè {ºÏÆ=¯ƒ¤nÞ†º9J }jŠã*ŒçQýñ4_—¿GÛš–é×c2cNÁ¯‚EÔÎfKlusØδYõS¤,aoªX@¥1Ï1+•®áLÔú ú»Ð¸9Žô +ÝuѽÐJOŽe¢Âµ©Õ¦"y8Š„>e8^g©øµ«Á[“¿5¾zu±k´“\ÈIâÓ á倞2=òq.ƒÄ{'û= b ºöQ}8¬)\Äpž¹Jÿ>׿/¢´5_2Êز€—ÙûC¹A|ñõ€~#Â÷éæSÞLAÆѬ4TI¯¿“§$=êȢ瑪nè~Øöúþò¡àSî—¿Ðh|qÁ.Ö•†+ +=G Ýu8P²LK"n:pÉ°ìÁïótYá ÅGTš¢NÓ=¶|ê†_2•>EI%m³Q-ðe¼â`ņF¬•%Ö„ƒm—c¼Ø°ã@$ ÏÞ!Bûé»CÏHZ(ª[bŸÞD÷ …ßóFNÅ‘:(-;;hQ\dâ2.ByE c1A#¡ïe(ƒéö4ñP>ó5F|z=lé¦A¬u£÷Co«ÂQSœAèá°ør6 ûÁœ’§]FÁïs&!æh’AoÎXF®!✼¸[¶÷Q¬B+=ãyâ0×Irø$‹ ¬m9 w@¬=棲ážL@Ó0=EÖAWæoW_"EYNØSk²ËhhEqNŽYVpmn Ù#ùõÌ?á1Š›©B!bNT¹NRe7ÄV€¼øÃH=¡wŒÞÍÇe¥E“Á© *ø´øI$àUH¼Yª.ñ4 3s‹zèô@HnI¨Aa"9UÑ´ÓqC/£ÍG”<¿¨qü˜á¼Üd댸‡ ÒÝEI¼´ê ºq Hr5ìÓ=±+ú“*hR?…d¿`o^tM®3z©ý[H;Äeu'±Å;82“¢‡<`Î{­HTo­OePxó}Ø¢2(Ãä…LñcÅ +i`Ôæ:6˜îNk‚Š† þu²®(:=U$] +ouE}šjŒ‚Ù¯óPùDF:Ï鮞QËsàON—eDyúEN°'UŠ¬¸ ÈGÏ–hanFÈ•ú<=%}û_0Ý„Cb«p–Í€ŽêXS±¢åÑôž±¾ù±‹¸Wl-#ž,v¼›=¹mwÈåÊ`õ@ :"ŠÜ¢º)ò°S[-E…”t4òÝÈÌ·.óò«vç +ÏŒiGi `qtÆ•.&Æ­ŽÉÂW$;ÑXŃ€Åºy úèƒ;Uû >e¿Ð¸0ͱ:¤ö!×ÈÑ‚hƒ‚£ïrà=õ’íÉÂ¼á Š\ •fowPï‡ûÇ舭–zOkv_A\a@V1"ãtXNÉC!¨Jã£iñhF€ƒRR@L<Èm˜Ë@Ã.#4ÿ¸*]ÒÙnU#\† +Ij^aÎÁ$“ 9°2áSðºŽ)“[œ +̸KßçöÅ]¥Zt3¢þ‚§g.†$®xÎCèS%wi¿Ñ|9zLºšÅ0ûm×èeŽ*Å^SЭ€eqîj¬7)ÈS´ÐäxƒŠ<Ì1®„Y{6 +ïÑ.@WM„9tìémòbþÛ¤è?F?}‘ãœ] øÇü5(ûï!ñÐN¢Ý®cŠUn.„,OÙÝйsOZ\Æ:îPh–W +®nd£-6……׳@·Cx‰ÆPX›b¦þ"ò­{ƒ[Ê$—ÿ»ƒŽÓPUH„Uùú Ú&‚,S—meL£³%‹¾HÁ#7®mj î£pT éVþÔ½›.ARPâ.š’åšâîEô¨q/’ðŸFTq™œbR«Yb©ƒ”·ó”äìf‰¸xaȹX‚rkðj{M¢ç1ûWm ™w_Y—è @ýpZ¹11”Ž[.€~”‰ÚãyÈu66È:è²ã˜ èn¤‰GG%ÚGH(<Û bM¼Sƒ,S­ã ÑN‡êŒ>…]‚.B&í¾ƒWø\Ð |º•Ä‡m„Ũáº^í45Û¦«ÄˆK™ö¤ ¨w[ea‡†‡¼š ^RÏyá÷}DjÛÇëáDxŠíMD³ìe-UÀˆì#ô\‹?×Ù¿ß´=uÉÈ Çuf¿Î:ÄÐdú‰#„¼Î«±¸p¡ %6ÎX÷Q‘ÿb±>r‘C„½ŠÓTIƲ ‚ሠ(KøÐÁË“]NªÀˆú€„È*4‘9Ô½F¹ˆÅ@]_hl¢FÁ(â£Pè©Æ°Ä_¤­â"Ž2"FšMÌ`âb¥0A¦äÂø &ÛÛÓ´W=¸âžûΟ.4ˆü<±—ÞŸª@{í€ 5— –ZZ~œ_›h?XLg‚Å“G«ýàpcŠŠjWr%>ðÁŒ((™2ÍF$©^"ýGw€£?^æªô -m´Q|qf ;‘2¤¢ãF-uhAF|¾„Ž¿‘öŸ€¿Yhô†–‘%Ùƒ¿%‰££ršÛœòÑ„&¬YGøÔ?‡}V0(Dnç/NàÎ~Óü’GúÍêi +GzŒÎé2fQM ’—%ú» :Úxc+â± +ïèí1èbo®%B®ÃvWÐOÙOƒâ‹dnp§;Bë¨îh1Ò¶H ¼É¸ëˆö I<…oØ·«FÑÍ÷Q¦â×VÂF˜­"™ig’¾@¡ÊdÅÇ^F\Š@  ¹Ýàƒ¨°œb ~În:B•\šœõ²rTƒ4BxPOsüR(sŽó²¹&q×7AÛÔ“=? ?5PØ8¤ï#÷ +&w?ÐÂøåõ‚¿û Ú ”,)ió)!ÂT©•ä¦YZ{‡mfÇh\KV»ÊT÷>¥†öÛöqРelg<)\€>vÍ>½ÎI,_—!ˆ4¶‘‰u¸|t–§§>•ó(<*¼CÞOb§ zˆépƒŒXÑ¥‹°ÌÄ&†¢´Õ1 ù{ðÉ¡-M¢ˆ7§ C™Ð0 ¾Ã»`VÉ͈I“C÷ó˜°áuÄ€*õbsDÔ s"àëÍ øNFĸµ_túŠ¢˜°Ö¶‰$‰tr€e¡«ï +ΓÉíè ÙTgÙ9B6i}V??º°AMJ\ g¹@œ ×¹m -ý:ì µl8Ž.% é* *Æe'x‹gƒ¾Ý!“™ùäc‚ÄÙ+y¸b³«nËy²9c×+2¸Âµæ9UŒ£Þ@ƒV’Ð6ÓÓü¦u³K£ ÉÃf`X)Yt©I¦¡¥PÓjBù²Ã\ö^pèåÁGÕh(̵2(.ÃêrÙæ¬ÉÑg°ßdb£ûâO8ã-3iömàpÓå£b£,Jÿõ¸žÓ½<©XR›/ª*_RêÑ<ÔŠ€âÐF¡HG”‰’äas-קú3œyûPD—FÃÝ3—våRHŒÚ…$\âþ’|ËÎ/J^*ŽÍ½¿œåe‘v\‹ª(¡nÂ_¬êËAjå,Ðs1'”éèˆEƒR”Ý’>ô• ÍR2¦­õ”äÓz`à/¼B°Óó<\xYAêø*Š]Dñ&š?‘ë`ѦÙÐ!BÂ2Þ.ú%â`R\ÃÓZ(-„ÜsiOH4†N©í3‡×”8 »³Œ¡Ðü.ÒtMA{rèÔ¾‹Ãù)Á>âھ˷ý‘';ð…-Ï„Zý¯hɼýüOÞþò?¾ÿeáíŸ~÷Ýß½ýü—¿úó/¾ÿþëßûù¯þáó?ûâ›o?ÿ컿ÿ§Ï¿û›Ïÿô«o¾ÿ·¿ÿî?ÿ½éý'~óõßýÅ÷_õ¹}ÅËŽ‚ãí¿ùú‹—ò•ßýþóÿù›ß}þç_ÿþ˯¿ýþóÿíëòóæ·_üêÛïoFó¾øë¿ûÚ/ü×ß}ùøLyï3þû¯ÿðÍ×ÿhù»x÷ç±ñÝßn·åßüþ»oÇO¤î9LXè÷Þù‹oì²þã7_}ÿ·×ÿøØÿåëo~û·ßìŠ~ýõß|¿^Ò»ƒ¹™vg¾ùú~Ä}ùÍwÿø·óOö?ýòWéó?ýö«ñIÿ÷§_ÿö›oÇ_þõÛÏÿý·ß~ñ»¯¿zy‹ò³›¿y•¼UÂ*|`¨‡—RA…% Ü4¼/ ¬Xþié‡æú/õßù?sêùd‡×7âµÿùÙÿÑòõ·?{ûOÿçñö•ð7 +º—KX¿I0¼^¶ÏmÔü9¿¾û¢?zðöüßþàWy<ÍO¿øòÿ¶UħÖo¾þòû®¼ýü· +ÛƒÙ‡†d}Ãu»l¿åL€0¡¶ÕóRÊعû éÈt~5 Æ?¢6V¨ŒÔM”ؼå. ·Sèä¶cZÄà>ãdºÛéø ô4ˆ’dÞ.[ºÎäÒ‹÷‹: ¨\8õ§³,<Õ8¨Ñ<*u^ú"Ezh]–ÏåG_œÏžÞ°:ú²Uª ÓØñfáåÄ+x]\²Tð$ì®vgrÔ´âh¤I”ë€8«}œjçWâ go²V¹»3Å r£@M¬.z¬¾*À, RÖ[y’“¯J]â„ÀpŽ§3ÙÊAežU6ùxµHÎÑn…ö–xÄh¯ÃCâ" NW†t9Ö’j*C¬ë‚wTµ¦À¾ f h´·²m’Ò˜ªC¡S°<ˆ¼­¸ê¿šÓ‰úf¥Ë-‹/ ~i~-!ªM2‹ƒ’„=ﲂFøRÏ›væñM"€ÒN•ÅŠ+ªË˜®'dÚ±8qp!¶›±ETD-Ñ$@†±P·e¦%Pº6£ASb×mP)–½‰¤"0³ªN€-yÄ^cõHÈ!ßrU3Š^DîRYG!hlc eP½¥ÐáÔÛz@¶U%)äÑBP½D¤öúãÁþ‘t–åøE«VöþKxÔ¢…#‰ÒÔRÔŒ«M•Y@WN¯Fººi¬NMú¹ž¦[•$ +öžíÌaüYdJ=Î6" +ãyð‰+M™=‡ÁO ^¡Ä«øƒ]hãÝ}{÷…ä >ýkÅGWìùÛßñÕ7–U¼¥ò'oŸà}%¯‹ñ +e²AG÷Íí(I+–üD¦7($Be,ð&Ðä²¹ñöéoí;>‰I)zs5oËÊ>¡’ÝzåšÒ¦¶‡Á(~¢b4¥H›˜ªÙg¢Ü®©Û¸<5Ôö¢Û.HÜœoÉ:óI‚âŒðž=z˜Jv}'шF§=,‚œ]dËø€‚a£{Éú‘k8/Çøö)kÍ'AEUXöÃì=ü„æ´ÀLÞB9ñý¶Ó‘ÓM!âÉoGÓ)˜fÌD>ý Û'x Aæj =ÃÉÛH,´GüéLì·O?½­ñüï·_üïß}oQôw¿ÿʦ’']tn(Óû³/,“ÿlðÛÏ?ûå¯þ혀ñ7ßýþw~hÄä¾õÝ_ýù/u’Kÿ»ïÿÉböÇ×/‰ð4_ó;þó{þ·&ó¸‚D½ÂH¯QižãƒÌ+;7f&,‚`ÐîÇÝÕµíE-‚ÉÙ#¡(FŽDdñV©Fƒ% a€½ží¶Ú´£(ÄLXZQ v5a%DéxÕ ío•ƒx$9áP…3¶wûz5ãØ¿v5}àM·Oá0„{O­Ã¡$×9e,‡Û: +Ô¨®Iâq^§ +ÖNQÌ­¸pÀ¨_±| +Ô› m‡·ÝÎ…@P€ Žbþ¶þc‡ér¤wí:ºïÜ)¹€Ú¼IZE(ULƒãä¢m?&Æ ñ>°„„:b@5ÄŠ"žÛn?N÷R(£Éæûs……H,f /ÐWêq4÷ìòÝ¥ÿ"äzBѾY­3 © aë +Y¿ÉŽ[¨Eõº+üM}¨:†? —xT8pÙm,ãi°cHšŽ¤˜ƒ¾âWd÷u1`'Š°ÁüâJs\¨u4Ú`í«‹Ÿ]t‹ó¤ãRA²y•ð.á lßI_ŒÀÎÒ@'»âî ÄB“Ä3­èP³®í†q-Hº¡ÿg»#a;H¶Ð$=8NלlDbôÓ Îö4SvÎV1²rÙ`Rüt(–æ¨:©§éX\ÿA&·Rî¨Ò‚Y„ŠéP§¨ÙZ…ážNFÜ É4É?DéQ¹¢™}H€“@Õ.VÂnö6D# ÌßËõg=Ü8*ØÒñÇYûúL÷š‘9IºÛÙæÍ~€"/ Y?ÝzÐÓÞ*y"nÜd¯?f'å,®&œ™ÖÓõè6eH¼š§«¾Ú¯;P‚Ý–í­ƒ0L$¶ƒÈ9:g®º4=°8Blyò†¬ r2g°K²-·¬1ŽÇ˜üÄVR*ѼÊ؇Q6Oò ÿ—„l<Éü‘x{-爠‘ýoIÀTË׃H)yeô>iön Æ°ÉàùpÇ…8´e9+äµIyLÁ› rˆáZ©¢ŸUv@»ÈƒĩRÃufñöíÉð’ Ù¤—‘0V•{¼}› ŠÎr\ä¦eÑïÔŠ'¤‚9YJPdgÅ<¸5ÎXÅч¹ö +©b!´9It3‰§ SiZ3Íëé˜d–õ&ÅT¦ÇÚ'ryìò³¢™Ék©õ]ô-x¸•|w¶"\Vea^¥*¡ÕülÍí8Q3°/|Cݾð™Ã="!¡9Õ´óû%¨M‚ïLÌ5Ø]5ó£|ÀƒÒò ®¶MR´OßÙçRXm´ÚdJ§ää [Ž5Ýšk‡ºr ÈŠ.ñHZ¢ÃMѬèÝ&Uæì(ˆŒ=)ÀocG“'ìAª«Ê7öྃ;äE*yºXQ\,à%QÜçûMD¯ð@µ O)#j}Hñ„Ž1Z³URŒ§nh«å™3¢ • 4X¤Uôï°ÆæYIU¯|#"´ï¶Ü/vð¢[M¯¨)»ÁC§I½ÖýêšÛ8W^=÷øY‡j uI]>ÁŒÊGÌbrëîòEàêì±ÛuPä³Ø0‰†- <%’a”8TÅPO)ÎIA¢X?ºxDu‰ ½º(YIÀ>…]Î?o·ïoQUó±ŠÏc‡â±C8ë#Þ §sœ«º:Y·çnËYZgã–ÄPCPàâ×â†j§§5#ý£Fµ6•DypkiMføƒ.³;WÅ~ó)²Áx6 sqÑU…]tMÃ)ÃjA¯á¥Š ˆæщä›+u4¡ÖG±CÞöoŽD5¨ Û%9ŒõàÓ›†Oï1uŽ¶;!¹|02ô¾¹¡±~°x¹É âÙ_)*[ž*:™$ö÷õy}¢|ѹˎùffð“~šMRÍcf¬3,ѺᇳG÷rz™œA˜Chp°ïæ¶ãÈF0îl·íÕÉ#tYé" ¶¾]1Kh«Œðö—Tîò<»³¸¿ìÉÍ#%×J©|.tßl"À]èÂòε5€f{Á)Ìo|Ìò—HTR†8&ËâEßO‘¿0¶ßÖ>š“Gƒ¯ ­¤ûE~;Êm±•~ ¨vù׃=x]©iŸ[¢‡¬«}K~ \^–yQö£ˆîêQ¾m;É(XGØ7(™G¾’€—]Énúx—'q“亟ѬF®jÝÑY´ßÀ•å©¥´í ð'x§àOtÉ]6_i‹7wc±w,î{7 rÜQµ üÙÀ+¼lýŒ’ +¾}Äü·%fÀ–¥§¼ÇtëÅÇž9¥èÖ …›,„Ɉf‚Œà'¥ª0s …˜€0Æ¡ÖŒgKÅËX¼ÅlË$w ÁhÓSØ´y sé&‚ƒKïˆ5[ÐÏ!0ã>½El˜¶Wò ú9PTߊŠ,69ßoR¸-3Ì\¢T­}:?+¼"ü%À•,„œJªËó©Eò+_Ãk §n(1>åè52—?2oúØ«ˆú"÷>îКÈô8ŠJˆßù¶¥AÒÂ7°ñºg$Ûˆ/GÌÿ’Ðìç)8—Dæ à,àã@•Úàé)c{IœÀ¾°­9{„5MÆök.ÇbȦ†[s¸Õ5 Ò4Â^ç¤÷s“bBý“É àÊÛtâ+Íye·É(¯¤£¾ìʺÓKö+(çgWDù\ CÒ$ ˜\ß®j"6Ú +ü®»¤Œ*%ô tn¤¨[ óp'œ­è)ñvÈS–2Iÿj-™Jyç(ÒÕiÏ•®—Š+Ô:8ÄPë¢rù¥V {ÌÕ³ìf¨þ±Vz M.î^ÂŒ½–:1§AUÜÐ!m÷µÄì“Á…|ºžÕZ¡Ö¥¿‰¥x}©oƒCµxɉ€C|ÛJãPiUãZ ê +#£ hv†ÔÔV”éF„DYˆÖR¾™`úìêËÚ],q‚½Çü!KAìOM­Æ–û¶wHÚ˜×Yí©­qô—hèo„ßåU²¥íÞxÁö27jökÖŽ ·]îkŸ3oÍ0[ŸSšëöÚ¥QÈR™ZÜ4…xKQyˆ}+БÅ÷>§NÄ>jýÁk[ +«nÚ˜6Àîý¹w³0E +6Ðÿã-Í°õ4{‡m½}ÄÝÚGm7fmþí7wí¾óÔpœOvkLn“b63oçÔl|.óqtG×y¼uTï^†­»½Rk'w{+·Fð;o÷ky® [Ûy[V¶®õíò´5½·enë—oKåh²ß­³[c~[®·žþ¶âo€ÛcC¬ÐŽE[׎`¸ÛùvĺƒîЉuóÝ€Ï{÷×X·þ é±…Pä6 Ùp&[83‘)3Ú`,·ñÓ‚Ùâ° ?3"¸ tsÿm˜-Žœ(Ÿyn -pýìP4ãÞ †´…ÍÐe-(¦-üž¸§-rßÐRw ÀµÚòˆ ¥µå"Èë6§Ù0b[n´Á˶üjq—§í£–to¢á¶Dq…ÐÝ曯à»-iÝ0{?dÄMÞ|3jI»7°á–±/HÅÛ¼ƒ9îgY’[ bGZÞÕ29ëˆs+lÐÛÌ!ÝJ9út+]àÕ§"Ò†tÝjPHv+cmÛÛ*ØÑݪi¸w«Èí á»ÊÞ†1^ëƒ8y7Hó]9rDÏbæ¡Þª x}[D]ÑÚ[-v»·2î +¿«oˆòYJ^!è[z°?•°7ÔûVßó[}ÃÛßã7¸þVÒßþ³°ñn› ûÇצÄÆLرá¶A²ñ"¶þÊέX[3;Gã®ÅS‰uõ³ yokiéWó«“^º¥ÛDT¥G #7éuºéZUrÊf -s­¯Ý/ _>zA¢×¼5Ð= ºv=ä +÷¸Šð :üÐZÃÞÎS éBze¤¼·mäˆVYÖiɬ­EHɽc,˜°g|nÊÚ=Xï¸n"Ÿ³ö9ÙœCÌ4ÊʹìýÒ¦ÂäCLùÒ£Õj[ŒûÙ£˜E%ê¹A»jíïîß¼öˆ·°¶š¿¼½ kËz¿kë[èñõ±xß|}šk»}Ÿwmûmj­ÝÿmvNàÀ6¹ŸáÛ»1A +ûkµâ>N¡âºžöD¼ªÑ%vp£1¹øV’%aµWO––H0RìÜd‘-x;OÙ?¾DËö0,Ƀ^]Ã7¢u&“Úe„~7:U'jóPƒ»ótyFèèAq jk£BÇøi®>£og÷+[±.¶ïA0 ³×J3ß\ï:Âçèú»·ól÷ŽX:<ægE´—wnýã9ýöglŒ³59X3q•´õ§!(²Êª‹g±ï·º°‰›zÙ¾>‰Ži›ìþw?]…š“¼ÍŒ»ïÝF,×ÿå,½Œ’´¸\9 ®@*Mv;Ñš°EÒ(š7€·û5Ú-ƒAò(­YKDjZ¢úq7çYïå~-{&_¾>Ø_^,š"IX + ö$-¶Lü7®«¶Ð +ùÖ F{¤ÈÞ‰jvyg¢B·Oä³0ƒ$}%ßLy°]Çd{i Ò7é Ð3‹¯°ýÓËû¹_Áújï¿än‰ØïÈvží®®ËÕósXß²uþ¡Vòx¹–ƒ Xœ8«Œ®çéæ¥ÒÎ'nߥǧ–iõøžu¾«ûòöÕgƒ˜µÏôn}·½òX_MèþžÍO­·èúžwîÝ| (†DL`¢ŽR^7™ØG•åϳ]U4h$Šø£jMBðd‘ƒ³PiWFo3»†Ÿ6ÅŠuÙL8ê:ó’Æ :çøÍÁZ¤,‹n2fM¬a¨´SHìÃÍ3e—Þ‰ê(K #Þ_Œ²ˆßGüQ’2~¬ÉÑn—Ýb€ ÀÚS½b£³E ÁQðT¤Èí!Rìsß82/™ëAžw…:™!i²¶GYIB ‰‘ʨ> ¬r+5{Zï_C '¸àòðjO°S©Õ†Fª´†xì®™Œ‡'Öž©¡\sŽV:pM‚”éOïéÖ ¨B²2xM¥Î^âHXÐùÆ…ð'êÚÅ¡[U+ÆÚèÜÌgYÆ øؤ}4‘ÔíD˜—Hl?žlº%ë% q¦û_ЫWfKÀ7ìîNPÙ“É…¥Ð’Àß_ÁZPþ!e½{*Œ*B×Xàá ÂòPíípúu>p Ͷ/ڒ̦ûyÅ Ë4ÝÈu~–>”­Š½è@^Ö)üFš*%{»ÿîM)mØ!#Ær*|ZÞ¸BÕ•Š!•Î@r·¾µ×O½{ç…p¡h¯<])βv 䀑Ó.¿<–"ê‰Ð(ÕÓ뀣-ÞûÔö…À{!úŽ±‰Q6®—p‘&LÃÐÙ.!‡°ôAÒïoû-óˆöQ `}Ê~çïò–íù±÷fœY"N£{h‘g5„ÚÒÝT2eš‡ÃÑxÛæϵäÍB»Š„ §Oõ×I|Åb§b‰'Øú2`Ç…r‘Ÿwy—äÞe{ àUŸ–©?½Å¸a+,°%ŽõÞÏëï>>.A{ñ²Zœ’‡½‰£›¨Äu©z|hYå_³.’··-¶ëoÛìy[æ2ÿ|÷Ía}û³>Æ}£º›û†·Î¦}Ó\gä¾ùÞ&äÛ&¾¾[°¾bû¾{U÷°d}ÓßhÞ‹ƒXZ$½r ©ÉOÿög÷:C=ýÉ~ ’E/,›©o³%ú‡×?~Ò¤0m9mÍHj:8˜¦ÃVAj”®¦ó$oó¯ÞÑaýoBãæåFß Þì7úKÚ ¢`˜Öà]ŽO™ôbĤm’ÊIQ@‰Žg2øoØ w„Â=2zì\ª°Íu[B$’W¡p¸+Ž¹ƒKo©í¨H W NMÁa+àí‹ÑùÏ;D'&\;Ò‰oì+Ø¡É*+12ô‘—C‚ kíÜ(øá¼ÔF@& G­WEmæg—ƒë{#«ãבJƒ¯±ËQKÓCÖ‘} m]‚´ž„À©f£É¦­@?§÷Š4¢­ˆÚ†§ _‘Ö‹u‚°f—éjã à¤6´ .óüäU·;e|oÏ¢a‹j·µ²fI_ªèf¿X9—ÃNóø&´Ñú)9É&)Hö«"¬¦0ÜèCZŽAæ9'S\𽎼âzŽ°^š_Çä•Ö„òâ0>:Feníƒ2f\뤷ØmjÜ»Yê絓ӕ”ØèÄ2ú§%vx`‡Ý„•îbµŸwh¹6ãdPq};>ûÙPW³ûf?çx;Qª¦ác‘Ò'ׂRhVâ›ïÕ–õÿXî1aø*6¢¸@ÒX+EP¨¹`=>êU+%à^¦òɧßPòû£AÚ‡¤ÚødÄû„ÁÉÙ©¾4*ÔöÓÓùc¿MBú£¸A}Àøô´Xæ·«ª=> +ÜCßOãÓÖùTÙ„ÿ9?Ð65~ ;#ðÕÈ´Âjÿ~!_Øð‹¦ÕAGòÿÊj›%ÿ?V»›Ü?dçᆉpZìgëO”Š»­ Ú +i1Z±¿Ð± ¡U±ë‚ŸÚ/m¶5¿Ó¡ECÁu•ím¶­#»º28hÂ/u›£iMµp +ì`RÝPÒ Ä’'€C ôjPÅÑž*µÍÀ¾¥˜yàN]]w¨:ø葯Òèš1¯ã':F*I‰žìIô¤ô Úc æíô'É«A­Æ†qƒŠ 1ª endstream endobj 25 0 obj <>stream +×ä™öBŒ/h:¡·äh•b"„|òâ¶Õö*×ä©d®ð&TýD¼œ)ª^tj>Ì¢›imçQë¡2J.}E«¦\bßÒÎÃÍå – ØêÉÅ/g9Ãn J–ìÍÇìòØ•ìW”ue… õÎï8FÂeßv´ÇMVE,4AÛnŸmÖo‚P·üzóéÐPeÄ—§Ý=:õ¢ßåÜ<þ.¿¬„·»ÝÄòÎ4‚Œ ơض%»®m:¢ž +QÀjÔs"¦H|·h /•ã›uuîNÜÁ›ô²Ö/¶lÙDóŠÀúÂ{6t0'eo»ÊJÐ/>»8†úå^’¡dµ-õÑ¥,"à?õâ;zŽph{žä5ör« pzuúu"`â§Éäg7ó)8À×½óªbmÓP&irBÒûÔ>hIñ“滊ØFtŸ[öfôY¿¾aûyÖ—t¿–õ]¿ÿMëJ±ß›uµÙnïºVÝ>¥mÍÛö¶nnóe[ïæݾŽ¯ó÷fXÞuߘ=—å!òAX¡#PœíÁ‰¯w§¨M|î‘7™óܵ)£n"Úˆ®S¥°×‚]”¢ïD´¥¶~fÞö°‹höêc˜kF®"Úö9,Q›nc¾Ñ& IMþ »ÆMDY{¹'¬ò.¢­Kž„ô÷"Úvin‹œ¤¢Ýwíˆå5¤U;;¢ÆÞÚ½d6?ws~¦ý-o¢Ù±‡ß®ZÙ(Ï9Ù<,á^3;È#+µ˜’ll×̦ö<ßÞ{Œ~Ó®™Ýãö"•íj”˜Új•Êæ¼ýœ—`xW¶øK°2l8À¡ßkes÷H—í®-üs=×ÎF g¸GÔ)û¦h‚vj÷e÷~ôøφu‚õŽ2=`ž]6­±ü¤>±ùÚ›%fý‡}]Å ®€½öêÈ¡†—˜,;mYV$ñìHùr YžÔ³mÊxý¢Õô]ÉâBØ;o÷¡ž7IÞOõãðcv2#° ¼Ô³m…È9äøßGŽ÷ÓÈgÿËÛëÀŠñýÈC‹úÃ^ê Ê*n?ÏJ´úìàoX º¦CK»ÏŽX1áÕ^GisNx}÷Ùê¢mÍ¥ñ‡Õg†•°¥5[û“ÏŽ1˜ƒ‚Ç+ñÆh‡ —1mÆøl÷F;Q^ÒœZÈYw£wïß4Úa õwн3Ú¡Nêe5***Ñ.F;‘ä¦à O~×ê´Ú hñ ¥ B¾sÚ‰âã‚â²-oÑÍi' +3qŒ¬ƒ‚òâ´hÊoŠåbËÙÓŽLbTKä§B\vĨ +TTm;µî9pÂx‚¸¼tùµÜ9íh–ÊÖ¦E¢¯»ót1ÆOyÙâê2¬vx¾™ð®hÉ·û“k8…Ýa¨œèÛ«±D*{6 £VaouˆŒô­öÐNQÌ_`6BÖ©¡êr[»,µ\1æ¬x©ÞÚê€n?Ž[¸ c­dQ*w[Þì²±m_y<´ÍV‡_ü³Ó¡êŒÉ+UgØD»ÎG_™Ypñ>½-·a-ºmÃÀ†À,gë¾@c ¦ËŽQ¿Ùº/ÜKŠìv/Y0ëK÷…¬À)IÍpgõÂc°[šœÓÞ…ÉæP'#RŠ»ïÂ`ÝxïÙôcK{FÁ<èi‚ù®Y¹ta¬³èHs¡öÒ…Ñ2¬_Ù-‚ê{†ó«5ZcËÞƒ¨ôC,ê´'q߃ 3†-@Ç ’®=g¢åÁDô¥ƒWµÌcñìŒ9Ü÷`ÄÑ7ÙOÒ£Yz0šþp +&ŒgØ{0|‘’ +‹r¢îØÞƒqf4÷Å–Iôªf†TPþâ&ãÙ¯ ¯·¿œNã}†©GÙvP»ö Üä>NLN(~m=˜¿ÿìÌëDÿŒ· ŽÑ.8÷v±¤Ââó€u؉ n0ãƒe8rWû =È߶Œïòû6›°Ó3zAöË×·þÞËÿi–.­—Ñ…ÿé_wÄ¥ñò߉§Í?·ñ²¹Ø„îPg{KOÏï]lÐ$¡ÉiÕ–]ñ2 +3‡UyÄ ¼_b™ŠÂÈ«m™G>ëEY“õ›Ðyªoá†d1ö•íÒëdû&ƱÿÏ\~5¯Ù>5µA }=±àå8Ã¥&Êzuè¶Î§³¼hr:ü-,@{fê– h'ÌÚlæ5 +oØ“ˆáz=룲aç'Óî”™¶Ü¡Y=ÔWÛQ¶¢2Úaü=I¶*¬«_D&‹—Î+kµ…~Å&Ã% K±ÊÞêop<_ÔdµP–„ËjKƒÖâQ¯¯e‘³.6<Ûþ)~[½_]lH8t¡¨ y\/i +â# ¦.6Ðé=:´;zÂì»SáUÀ£‹±S§KÁ×}ç[pßùþ0±‘½»¦}`ÂXâMìÜ¡I^ªÂìyòZªÃGS¿—,Þ6Ó©xÌFé›mVt©¦FrÐÿ ZózQVæh€H + cÇ©ÇLüà +ºÎ¶àLg…8X{â¨Àv'MÝ.ò‰ ì©M=•f•×&|èTs%hZd‰‚?éTóã4/- +èñ¡aÍÊ¡{ËŒ8àV9ìíÀ³vö~ГJ˜æèñP_ͼªóïÈ™(äkh†¦ o=uÒEta¦ò7dcÞ©)>ÿý¬'>ÿ6ÅÇ•&Ü\›èL¹r.ï†ßáß=‹œsëü ciÆ5h;œŠf-û<è͹–ºËIübÛPФx`—ž CLi;’éC¶ìG4{‹ºŠÄœ†Ð;3Ì'QŽöÅÁF%v©f0ј~6"dgXæ½ 9]/ êÔÜ ¡äë¥BlÌ5ÂØ@ˆ÷ü&ÂÞ_?ßZíšçõvSyƒDò¼ôäx®`Ð%»`wãáX#€rÚ@·õ|]Ÿ¸*žœÛµ–aӜχO¢$Š™Ï_¼=¬®ÓÌb¢ô° cK®/cìëª!L72»çc‰o‡¶›W‡3¿G—}nÿ·û¦±SÛ—¼x«•hxÎÝ«Igé¼v9§ýeG<%MumžaFÉtФ´>·[X©Ôžmm¤“ýâC–¦Â´k+]n1ê„è¶Ûæ¯˜Ñ Ñt=rpä#ÄÐuµ×pÄž²mjB¨d™ü&Tñö‡Ôk}=1Jȇ£¹mØS¨y•âWœ³ðÛ‹j³">"±õÀKض¼b<" ‚V7ÛˆÁ  ~´1[gò« Œ¸Å»î«Öuºú±Æ÷ÿþÓ®¯HD šëHA½ºbgä*6¼%“úpÇä ÍÂÑŸÑAa· +-ýØ­¢ût f/;#vJ;ôÊ&`'k®ôTÛ#ÿ8Êx¼‰|ÿÕF/xÐ .ÔЕêpûék±^S½’# iZ_OïØ‚r1ˆX‚ÔQoffDé(õÎÌ4:øsšg”fÅÌOiT…™1’]jço¨q¹K7=êÍW’ +µêHf»4ãKâK™‹ï¹¼]Hôø°tɾ<çÞ…’*ÑÇ4piD¾¿Üh-kÐÖ³c-p.Ì=‡»m¥á¯‚‚æy{©9Øy’³@Ѳ0M— oF³½T=òf¹^%äd)ÓÌZŠEvÄ¥Ïu—9äá0l÷ù¨æè7ôüê[lWÂ*7ËCö#ô8·‚ë¢ú¤ äZyÇB… +53s“áÚrz¦à6O¾ÿâ“Â7†‡= +` Y”$Í;¢*Ð2XÊU“EûÓ4>á¢^Íß œ%¥r›Í M³ßq7 ðáú.X£üIȶEëæxÁ´]ægÉ<ßfU2¯âΡäºöEöçúÁw~$ãÖì.$ó^®jE׸õmwñÇ»ŒŒ¹p«Ð4æͦïtM´UjŸ§wS×4ߪÆû°‹[—ç^k¼i›´Ö|%7U®ñúÞjz]J`s)صÃƺq+=6Ö˜M¸l®E›æÙX·^tÒ|eÛÅÕÆ +x©±UòÖÐc¬¨›Ü\zwûŽ±Nß»vŒE}“©›«ÿ¶›;ÄÞÜL¦„ÞÜlVͽ}«º“î›ûÛ¦ü7öÁM3pî™·’ƒsƒÝ ·-yS=Ü·ö'ÑÄ- ؤ×PbSkÜ‘[ÕÇ-ŽÙÔ#·h +O^ÓFånmZ—[¤¶éenÞ­îæ'Þœg 17Л7J¢7£EÒßXÂè[EÔ- +ß„U·~•dÝÜ)»î£V…Ø™olâ²[–r+R»%9›Øí–m‚¹#§ºSÛéצÕ;ó´MÝwÉín­5¶¼pJ oÙ䭵ƖŒNã‘´nBÈ#¿½•Qž¹ð¥½ì™ò.Ö<Òê{­ç‘ƒ?¢=Gߤ¤G>/Dí©ÿî‚1K›ö('¼èf‚Ã&¶= «N÷UŸ“ù¾JC|-¬ªâ[]åVœ|+Ël"ç³³é£ÏªÏºú(m‚ì³´i¹oå§[Mø­xµiËÏr×T£ßjc·Zö³¢¶Iáo5¸UM-àÝIòoÅ¿UØ/.†kÅñÖL`«Wn^[©sÚlÒ[¼ºy(ìeÛÕ‡aqçç°ZðVPÞAhK!úÖ¡b«co§ÙŠãÛ•l#nÑ6j»3kÝ~¿»kÙÿ§´t ®‡¼´ö鱶(î§ÙÖáX'ëÚÙ¦ûÚX¹}iÖ¶Ìöæ­ ýå]ûA÷ïþÒMºVŽµû´¯5Kïê~ÉZ;_ÛÒ·5ͶUsë¹Ý-¹[Çn[°·f߶ÖÏáýV±6·gv"ǵ·-ïöµ½ë¹n‹{¿tÝQ·nëó6|µf×={kænÛýÖ ¾ ¶Vòjl]èœl½ëÛfë|oÑÖ4!ÔÚh¿ »¶6ý´mþåm¸€[‹ UpŘ+áÖÆbƒ2ìØbä DqaoŒ-<ßÀ{ˆ¿b?îS…:²§l²')óÀmŽ3®ÒŠ{Ù“¬6sŸ¬­ ›=é[á:?dÄ]Þ¹Zó×f´§À+Hé>•^1NûyVxÔžÖo0«ÛòÀŠÒºŠ ++®k¯G¬°°ûºÆŠ*Ûë#+ m/±¬¸¶çÍÀÀme 5·—†Ö´!ô¶zÔ +íÛ +Y;B𮶠×rÚ†Lœõ· ÏxW´Ûгҷá'·ÚàŠ¾¼­,®ÐÍ­.¹>·’憽+ˆn€ÓYEÈԭܺáYŸj´v«ënøÙ­$¼Áoo Êzw«GoÀßYÁÞà·uïýãkÕ|â“·:û†j¾-Óo è­Ê¿«×Áоë/¥4%™õ]ê¤KŸB?–À˜›oüÒXéÁPRâ}$JgŽ7ÎMýÆÁ"ª«(­‹yoÌDZŒˆ[û§­ÛÖ3/Y•„ê÷™ö®ÉF8gk¼ö”µ¡Då!+Æ=í¦5>õª––øAj»ßXVð+9±]^‘ÆÖ7‹ªkWƒ-’–I¦yzuª¸¯M»ý{Ö~ßu™wÆ×o[{‹û­¹ó£Øn±·3÷g³v?÷g|×<ݦÊÚzÝfÛìÕn“õ¹±»NõÙÞ_’µküq6—½R°P©F½Ã;šSÜhÙ³%õåÆæ ð0ƒžW•xCÔN$³q¯6OI$±î#ǧy;O2a´ E¾N»ò}ÔNoÍâú£½NÑýËèþ~«pÿþCî ö²g»©«Á»ÏäñEŽy·ƒ‹?Ä^rg‰íï·¾ïÚ]×)õZñ7ß»¸ñpØG­^ìs)j>\}÷“دñΗ‚ˆ·u‰ŽHúwç·p¿„wnü—/ÏøU9_(0v‘¶Õ¤ã}3í$­Â;vL”ÖoÜ*HsÛyc.QÝzàE\ë¼zIÌÓoî0ó¢îÍeÆOY^?~]EÖ;µÎóuN´ ló=Ns}¶H–àö½±{(¡;\:Á:î¯]¶êNÞr¯7¯îúCoWÎÛôëb«éf!‘†}öùi ‚;¯ŽqNµ¢gåö÷?µ~ߪ¿]õàüþãWáúý®Ú÷ûƒ¸K!öºjñÏ©°ªø¯ÓèÎ `û±«¥À6›WW‚í¥x65Øީ逰½«wÂþRßy0l‹Ã´pØÖ•iú°-KONÛ¢v}h]¯¯Ù–ÓÛ«Ü–åí׎ýº=ë>ðrS·md{&ÛV´=ÖmK»Ûָͱm{ÝfçÜo'÷¶¹o/É l/Úöco_Ø-`ÙÞûwc%0byùÿÚ¢RÍ(„wÊÿÃâÇCü Ès3ï[CÓ¶[xÑ–¸š{ÉòW¨¤¥:å)TKÀ•S{ad—Fe;ó‘ßÑé-¤h/rø›PsP>vgi°¹¼*DK³hÙ@qðFž[-œZ§ 5¨½ÏÈh +=}£fÍgT¶²%¦yçU›lGê]zïoD¹÷`óI”› KRY¶vÓ_»ûx·=Ù‹Äz¼]@vu¬š,@¢Ã|û3ÒØì’,m÷€†‚öa›ñ¼¹iÔu“ÎþË=§,¥T +{¹yb’ "2ËZ…ovFµ˜émÑOï¨rG´kì6ÕnT¹Õ&<¹ CAlN[”Àr$¼ºQÞ|hQ$Þ¿xU5ÞÀ:ò~#V•åí^®BÍû“¸|ޟ袽O…UzzŸQwÖûÄ\¥°÷ɽÊiïïÆ,÷öŠí§Y_ÒýRÖwýþ'b»#ÛÚ²ÞÔmeº}6saÛí¶&n³c[[ogÙ¶Fo³u_Ý×ÿÞæ0‹Zÿ5ĸw¿—b-⤨}ï†ä’Ë6Ê‚_‹BnÜZ‚4]m„%š˜lnH°á9Žjò ¦à´EjöÄ,¾8iÆÞo|‘€8tÚ1¶ach»û")`æèÆ´[}‘*´üxc‡TAPú·Ø†¬p·Cª§ÿ‚p¡Æ}›/@l’mÄúÒi›/RiÞÂK€Øú/RanÐ3¢0QÞóE*ó› +ª“õƉ¸˜}žnCq÷Eª +ZÔ1rãõM›/¸¥Q'·¦AªÜŒ‘5³Ùkß&#Ý#¡È *(U[¦Gçf’Zp²T’fÂî‘DºA¡Ãr# +/7fIàç)ÛÝWGZß´¹%qžbkçAtwwK¤^:ÝÜárã–´¾=Ÿýì¿Ì-IÉñp‰dd:…Ì*ï˜%uYúÚí +°ÙìÏ«±Ï'«ÀÜ­W’;7€ ª°6³$ÞlÉ/å$”- +¾uúã_h©€©Oð8ÕOÊ€²`KºøZÁbŒäx/;—Ò$Opêl Ì~m#ê¸;–K¢7ƒ»Cƒ}«rtåã¶E÷ÀH Om»2„‚D„‚h¥ýä ˆÐ81 +µÛC"5íÔÁŸ?›S“`ª2áŸ?=âêl‰ˆ˜1Uƒ–¢ò¬™ xXûüÍŽÇUø“eéä×aF¢ø…Ksº-Š• ˾UaÕ&¹: CJ]T-@ΊâÐÈv`-;¼uApœh³ƒ &Ø##J¨`¥#ц»øÄ’XDAY«éÿËÞ»îø’\W~OÐïP_ Œ裌kf~ÛcìöXвà äVDcš(Š‚ÞÞû·vDVUì(žCNK¢Æ.üŸ¬Ì¸îû^Kö¨[ x†—óÏ¡Q¡Öêå£ÚIªÕmåï¬"é$Zj´"%j/.j{ÉX›N¸5Û4È{å1QˆB;:ÅRYtc*fäSDtQ[Oñ¼Ï”úš…i·ÔVŠðªùÀ¤Ím$]g=¦€P;5‰vÍ/³@y±ÝødžÝ!JgkÄÙ‚iÌRβ™EVÞÅ&QO„褼֦LŠÙ^ +ïM±.U<\~·i«NOC½_›­ð²°Mä»+ëUð™/õÓåL rïî>BШäCx´ª¦l3r;L5¦TìDtð¡±G£ýú·årûÝÄÃOÑ»3¾`…'ÊËLàÔü$98†v%.,!ÿô`Z4…<ø@¶ã)Éœô%Ü{ºùUÇkæã'Rû6ùœ8äxí%FÓ¸¹É¶¬æ#€h)Ÿï—OeûS‚àkè; Ü`¶ÂÖ`ßQqÈRW9 +s^ûyžƒP!ä ÜÁË4Ê/²J£m~ŸEœ©œ§çò ˆ„èœ(Ç)v÷*%È +5Ð\aÏ>™}5õ;ÔT§%”{`âÞ@=´m°I5ÛÖ‚¶ˆPäh# +ü©mÞ iVŠŽ’-;õ†]É“>RÌí ×þj–½ÐöD¹<—NZ Œ¦Ñ®¡qɾØí¤É´á ƒHIbµÍ?Ä»=á[Ú¨šì8\¤ûÀQORdöygæŸuÜ¢¹ýàRCˆm[S« |Ng/.)ý¾‹¬U»ÊôíK”d¿G(ßD“€ªzŠšð.*3w±‘úÎt +’ b§³úUz£â¿Ú\ +•ŸfóvvíòB¢Ÿ±<{o÷Rb>¨›fAl;„¢2»@$§Ö«8ï€OéY7O¨‚€öÔw1K[MͲ²ŸÐydþšë‰‡$VÍð.ê í9Uûyt~"¢í„ 2ÍŒC/ðµ ´bHå ¡®¬êmÛQºñ Ç€sF{:‡.5Ó98íE¶”K›ŠÙÛ}îíI܉©tϪj=ìϳ‚ÐtW‚W9¶Yœ™^d½ ­Ë&kúù$4H£ŽÉOžhê ®$~&ÄN±Ý]+ìdA§÷ëu¬7ltÁb°™·”eÅ(ʵuù4F¸Íf}Nð›o•VÁ@‘qÚå^M]f8{ËÖüP/ìEKzÝ–}wh‚ï·Ø—‡°2Bå.CñAKƒ]F7ï"*UH•ž!S E¥ýRÓí0’ãÊŠÛº¼—ÉåÚ}uß=]ZfÞ,‰Kö%ÛÚ„§W5Pê¡8F¶UGQæz¶›Œëƒñ¿ö·vã7Ýìa‘0{d¥›GFôÑÎ`# ×ÅEdŸ7Ó¡”áWä|©£¬jyMQÑ>F€¯A!>‹– +I!ùÒhwk_5î™nÔt‰Ž“xüÙ çؼkWLðazšÕã¯6A—‰©s,5”&ÛoS{Ö¥tÑ|¹­‚µæ6ä!°Š«êœ!Bè­L`z¶Ž»ìj¨ô˜OxâªÞ3 +Û1:>âSë4¡‡½“¦wP¦3 +U-a™‘fºäZçþ&·åͱ=‰îÇDE€ÔÕå`„¡„'vSŠO­Kû.I+ +¾q‰Ë á°6 ç_1˜í6­‰“°Ý„Ah­k ðæ|Ðò›Å`{B|¸=]ö<½Ÿú´9œ ÎS=Õ»9Õ @vz£q?Ÿû;¡M2E¼¹[ì^&¶EŒÝåvší0–Ô!­¶wüæ¢à uúP£¬ UäïQ'mÄM£_Ôfdbol/¶ c¤ïé2o‹b§(þ+ºA˜8mbåŒ"´‰"Žp–T­{QLHHÆ0zc'ǽÔ*O IRšÚU ™¤Öw³¨#ÛjÍ—£È|O”몉N’€¾H&ÓA´c<0CÌO?¨šÝjEƒ³¸­JµÝ`‹f'˜ÖÄsƒ/Ícθšwú½a«ÙÙ»lÁõ¼­f‚Ù{Îý|²à€T¬¦c$éüE÷'6&K£¬ îr +&™Îhúóä$Û””h>>ò¢ç$Ü×½õÅ{²(  QJ´âŽ£Bƒä}5Z‚ì9pNP[okQ’Tk‰™³«B_,Ó6u[u£uKP +­‹ CÇÛYc?q§G¡d½ˆu¬Fv£EÝÎ.‚¼ÑXÿµF\9BÐ[+ýÜR0âæ4DÇÁg}ú¬iãßx\° ”œÇT7¾ ÉQµq:È@‹þ‚ÔV„°': nÝ(Øráæij#Èñ8aè&P'˜Sôßâ;?0>µú“„g)©!,Å|R$¾'ÛIÆsš6-Ó›;"•ÔÄêsGüð÷è‰?ÿ¶úñ:ºeÈ܈®.ÄØ *ŒÉ^ŸœÃmLQ£’mÔµ¼›÷‘jˆÿMtƒ*ƒ±O•ÁÙ?ˆ’˜¯:* Ú©”vˆ¶I×€"hŽ¦i@§sèÀ"¡äfîiJÝ*~RwÃF!ÿ¿Fž²—-ј´`±Ä84Üò!üEfÁCÇKä¬Q=n]j+÷A®A /)V;q£Éã=¶ÇäÇì hÿ5ˆ\‘ßp±öAÅ6|÷Š$« 6YµãÔ•X¢˜Ü¾J°ö‡µì£¡2> ÄCŒ©6à!Ø)aîáZŽpl¥²lŠ Šˆ 6ÑÜJs'ïÀ?úÀ nYGØs3YÇM<EÌ‹Á§ÈŠÌ½¡If`h™&â´_!„RÏA[Œ‚»Itv7‰®¶¦“ 4g ž¤.†”'ÏçyZ¿­Ìp>ê]=ÞçnÑ6@&ÑÀ lQ{2 ¼”«ÃK -º! AN^}§¯´]*#¾ÅÓ èqS¥,N7}b…µ‡,b›añ©Ë_³7J7êêØN÷¼I!4‹n7¶6¹Ü5‘äÃ>Åã êÖåŤzN[—ܲVÈ_w‹ì_ÙÓä‘--ý‡Mü´+OA —¯œ*xQêùL/¹Â?fl\xA^·¥°q›õ‘‡W@‡7£õ&µCÕf%CB +æÄã|w}ôüK,¹”¨—›$öžœýŠ7‡K8¦Ã\–±ÙÜØLšIw×qG–‹ +\@Õ«‚§q‡yvÛ§¶Cq—)YDÒcàfQœ¬§…™`Í”¤qXÖCG¤«ã8¼„-Ü +£:Š7¹XBÖs_ÕèÆ9¸T^÷Ý” +ooO%dEé#؆ûm½€Wu ›Í¢mî±àÃ:Od™ž{ü^!Ô‚•p½LAbæ9 iŽ‚-OÄO¥íÏæq©¤a¾v•^•’;†ð4Z¥'è}#4+tœ ›°ÁÙ÷b³EÃøè4-«Ü®haÛ}ûWÓ™wþ•šmÒóù„鸭š£ƒl_•Ž]tÅ.¨‰¨ Ê«²bÇû,­>žá;%Xi™ÏæZ“ÓH£(à­`ãPEiîQ¥ …«šöÍ4M‘”y¥";áþâåÙkW{À<+4Q°&ª êÍð#5#C«-ÂCm/FLÅFÂî¾_"ȶšBfWÉÄr6ôB[M*V¦_1ØË¢rŒÃb"™ËD9E ʹQS×´àVC;i Z¸“I®ìjLÚÌíâSã±\9¥£hEQn”ºM«ëtÒbrº©±ZÃõ ¯‚ÂlJ(ÚƦ6Y¤Ç¡×(× ¦9’PßAØa±­æ=§’0ŒC6)º L•n˜ œA8¤¿…;ïÜ*43®© ÜCEÖ‹ËRølµÁ1§àúÜtN2'ÓVÓY=(­¼ zV;ñ%xaU T+_(jŽÞ\xâ»áù¼ó +ã{†CÉÖRGLk‡)¸ëÍ?tÕü\ÞüñÞ ­^ì\÷hs No|bõíô œV¹¶g×ßùàC©;0Z±‚_…­­rÈ´yBšá‚€[pïãÜV:ß¹­…äcITîE£åÀôYn1°´’$½@Û›ÎÛ A:Ć< ¸è—dá SPXy1TSAá»èÈ9(¹_ZC>•ªI"rk´¨šC Š‚ȹ§uª´Ùp4©K4!ûlÿ»èU…É#§h1 +Æ—¤>/ºOO‡hZ%cWU*CIy 9“£Ÿ$ì‚S“#{dð)»´C¯i;¢ +y˜¹­óö2ƒÙ•ó/í™ìÔz°ÄÂM |¢é³‘zÍqó–˜z¥BšÄ™‡x—·uQÛËá…óÓ/ñ}­ûýÄ,ß%*-"—t§î’ÑnE²|÷Ê\ÕÜ„ôT¾Ínðr&— +xÏš!ÑèhµáT°Wk¢EC¡èA±•IŸ³äk°8À¶Y Eý%ä|*xk2KM|Ë´]sG2A¹øÿ™yò‡ï2P6.°×2ë½f±j¹U„ŒTź‰Ù°Ú@ é²<ÃŒo“jöïãlÙ:Ëùòr•Ì”»N±Ê¤Þ3ËþB‡ØìÀ{¦÷–ä`i¡¾æYyí^tUŸ1=Y1Ø&‘å’ÜÄ®”v²ájÏY¤µ'áU^ªi)÷æ‰3©Žš¸Dê3`žZg¹æ|+\oæó´'¦¶¦Žqè(=Ã*¦OnLiIA‡÷„4vJ|b7¥øTXš55—wMñ¿Û•¬ûÊæI˜ÅÛó3*Âáá؆úˆíéuñ­•á&†Šnôû‘ BíI.¡†e'£B)Ì*éB5M–£g+jC%OÙ¡h +ûPCôFC„ +¤ _BSÐQ¡j«ëBUUЙ¡2+¨ÛPàµUÛ¡P,¨ÿPl¶Z¡fmkˆ„Ú·`Ð̲¹` …ª»7–T¨Ù vX¨û ¶\¨ÜÚ„¡1Ø–³„1˜¥¡2˜·ßì*)ƒ™*2ƒ¹ýk t©ì fû, +†~($Ýú ¡ 5ø¡¨5ø,¡6vëû„ÒÚÕ… +Õ¹Á ‹Oì¼¹øÔêÎBãàP®UÊ[¿ô}¡spnC¡ô<±ó¯7O-îy¨ýžýRA¾„:ôø–µ–=Ä*bMü&äáõô!\ +ñCÈ%ôoC7¡1 „€boÁFzZvQ¨ÐÙ°³BsD‡…&‹m4-4k„¨\hø‘½Ø8²‹†¾“h ½+kˆ2´Àl#¡•&DLCÎ µÎÞ]|vmü±ÝÙ(´Æ‚CwÑ6¤š”Bdzís +ÁíÐ.µ ’‡¶«l [3Nú½¶aþÐ.’¡å,$BãÚ6qß3ò¡c.¤LBçÝ6õøBæ&6.9ŸØK¸Kõ ßPì4zÆlIAi®ØÓ̵èò-Y,ì/m,˜E×*&Ã:Þ'Ö-À32ïלš™_Ÿ°Ñhù&9Òr'r6z-·Ù=“æWؤ5Å¡ŒN"ü28˜ðšfìšÉ¼Ez…%[éT„ØC=,ô>1éÙ/'Ô2»šÞÐ'e*óæ>…ØhSíÔæÆŒë©ÈDa¡¹MÖÖd¬ÀÅ!`P°j›ýïY³Èq,k&:ÌdMh·]51×uM°«‡&ìÏš¨_wxMõÇc²+§m­<6T/„sÿ¶ø!\›µt"Þ¼µúâóÝÀLd%èp„š…gR4¯+ž õãÔIéÚ?`ƒq.LjfÂÛlj{‰Å2Ê(#­lS× Ž¨j£ZûŸд/f;^T–“÷üÕuŠi»@wÅäPÐÔ¶ìNíëÃðLØÎcUôˆ¯¿’³5…ÐpÛŒn}ÂOë:¹ðž¸@÷8EðÓÑ óÙ…~Ýž¿þjÔõ›€S'h]ÜæÂ<.ð²Ãnž2TïMPË +&Ìò—âÞ<Ñõ§ +«}ÁŸ‹ ïÑìÊ ž/‡XþÝ B½{ê„áÍn¬íSÆÚbw×°´ðD¾óe¦ñ}©¿e¤-ºG42ÏôÄ"+Qn3Ô<Åß³.bËç6ã»÷›—ëþ"‘f)ªßpƒú³õœ4¨Á1ÌMäê«^Ïi–Fæú$.Þ>8³v˜Å rAëôYxÊ$ú§êaÎw0ÞWŸÎSe6¬üAÅP|ßÙ8‚q¹ãÞˆ„8ÿùWqé´]õõV-ÇŽ>¢`?lÿñΟ¶ÍŽ“¬è£ÁØ=qëO·—ióçËùzýòúc¼ßí.¿ ¨O˜ûf$WóÖúK8ûf÷ȥŒ¾62@-@qsJ{:ÁÞÝÄð–°¨a,û¼6DZ°39-ýhóä‡áõ{,%ÕqƧ£8qCMP79«â#¼ˆ¸€ä¡hvÁ«31ÀŸé^¸"-ÔÓ •rP"iG´BÙ’ôš$±d«—E>=º•¢ ºÀˆÂ˜~*š +Õ"5œ;«ÓÞ®:­ +‹ZöÂ35™8cUTá8O3ÿ)_zBàö‡øB½¡Q¾§Yó€¿Éî47wô”æ4ÖÉÒ·Sâø£EË¡ºfáåŸ}O Q’ mL7%Çs*gƒ4ÓƒNBŸÊº²PhJ X†$fXCR‹çfvîMØFQ»ÂÉÑI°½Äƒ@v† ò@0õˆá<‰„•€Fó˜“$Dǵ„“ $ÍŸ™Œ/’ð˜ÌAz­8KY¶ÓŽÛm [8ç»+Ù/îœ4-Yª":ŸžQ®?íêÓI­¢/Al¢“t]PžD?ˆJ÷©u°oö㮯öÛ¼g„q(«<ÝN$Hã1ý(Èו›z`»ðQ,û5Ñz¢BÛ ¨ד•ër˜£ŠÞzüAà Lƒõ*Æ©î®t´TV‰ðÖÎg­&¤Ó¿4³Œâ—•nž!LàXU_!ÓÿÆlð›2©WK\±IÓΊ“Êea)Oœ/aãÑ8g–6ÑV:Jq'*»Knšƒ (œ¤©ÖÒÐÐÜkБg:óB¡8θý¿áNB‰Ê ‰÷ªî|o¿>˜nq ñR/^…ÒeãíuÜ›<û’†á¤Í'ŽñŠÔÄ9üÍð[/<ë~V"Õ`Žg؅튒#¾›Þ«¨81ZZ=©—8j8ø¿˜Œd(xùj uœ<˜ÿǹ…¿ çŠfõª €9™Ï‚{\]qÚy5Iáñd +½¼§¨Ž<’ð„Ú§ÙùÃd©ªc²Ù›ý`˜ =I^…Dg050'è•úz§S±'5ôÜÇhr%Å>^ +P¢nûuþš¬ç)bözP3i† ©—P÷Ñ%US(!p‘\ Š¸0I„ñTTxáŠYŠ'¢ÓN“ÐC\?¤Y ÕÃ0>Ôºv9‚ô\IUf稊´Å|"6YµÌPï²jd é[‚‘Ê j+ÙäÛk2Šˆk*êè´Óp©âHëczäYô‹"'ÏÎgzÄQ’âà6%yÔÄÉ‹žèäožÇÙ?¼eß|õÐ5Q¥~ˆÚN¸¡Mh P‰P]rþ¸ãodÍ%m ó t.`;ØɃSšÖ“öhz¿íÆÅ™H8,›©óÁ®$Óù®å×èåOPvÃßAI†¤§¨íªAq h@»éh¤¾ÜöYZ”\Ùƒ‹ DõoþAï»f&¹nvü×v³nS{¢$RêÚ )¾ÉÏ¡æiʧ#…£7bÚòü‚)Ýäs±%h‚¨6'®\«,ê{ ?ÿ7Ïé볨î×Ù&YÀáEÚ¥7“Èü/€|þ"%õÑ9ø=Àqþò£ü/LÇCYœûH‘jSÌ̯„«'(±²§ +•Zf¥ÑcB„ +`ƒžäCz" Ó´\³¨3°¬HG"\,RcMp;*m€¶º/ŠG¤¸h¢¾rQŸd6éÔä~Þ´¾ð§5˜ŠŒ¿Wâ%ùxÇdÜÙņ+7¤€lw›ÿ¡`QI°|«ÈTUhÔ®Òí¤1 +³è ŠbBnðRL€ñsüåíWG_Z.r¥ßÞÏym›ŽCܶ¯å‰­àDØ?P&TÞüùá[@Ý1¯Lb&äUtˆ}4Þc8{ööãÜL›NUÒ?&¢¨´Ü¬œ) HîT6~Ö6FøÓ¬Ž~îöµ£f$ Úýz„¢õÞŸ +‰©Óh?§lN"Fk³ŒÜÑ»ÓÜÝĤ@Ûjß¼çt„èC÷ü«u,°émY¢ç-ÎÉ«]†Oí |½_\ B.Ò.É©ÀѲ¨fz€ù ¶¡×¸5æ~‰ëø õgúÍa‡q¢” Ö»& ¿?$gVë‚=Aý]Ý6ª2ÃjÔi¦×Ãöî¬ö™,±áS$yïûT>·ày(y¸&óH¸dÏŸ¯×2~y½Þû¬R"®Ä*iâj®k»+Aò…Í B3œ(|ã1‹2|=­=°œøÕÈLýS÷8w*¤ W’`ÎÕöMú€ñÃ5!~+zð[ÙZ7Gãƒ#5ì¢Ä$B…|"¤q¬^–ÎŒvâÛUª<{CÅh~Þ0|)Ó‘‡š'1rQÞ#Û Hq¯(¶8$¦¦i×?ˆ@j¯t;#­ÿ)r@tP"!î%å@ñ¨S‡ÂÐ[IÂ0ëü,p°º@[nÄTkœ2b¬ñ +þ=„`Ù[ÝœYW_‚ J¶Ç '/TH‹ .QÈ¢ÉOá +ýb73³Zìf‡fÄeF&;ûáܸâsèTM^c+Îx¶Œèà…´PÕSл¶} '{MaFÙ«áoaéàPUçáÕác¥Ë€~ì›±7i|¨€\sŠßŽg{}š¨¥öè#¬)Ñ +v…«è—½[vÐ!ÑcÂûÐD¦} ÊSj¹]§ðK˜x(®hF¯Â“Ô’»Î&›K”ÓPˆ[©nT'åT 7h‘¿Á0aÛe~üø÷dÓK„òô÷Ô€i88©YÄŠø½èðÔiØ\–/rË~4jÊh›ç¢–ˆ¶§7Ä5ºõ#Pvê®CTYæTØùDÁ [W %yO\ƒuíf¬YÔ7'MúoýÁN ,Ô<ö&JI“›]½§æã÷'J&;w¸Ð™ö»ÌÐ Ú'»Æ°™R³Róüx3TÍ)\©µŠæ¾ÕÖûoÔÉIÁ¯ù¿2]} $Ë6X×¾@wýÓËåÕõ£OÏ%iÊAÅõ›Ò¿f½eþ@=h²AðŸ"¡\+I^è\¥¶h¼ÍEpg^‡cêe×èíoƒ’vM¹²jW*˜Äj—Ó.)Z3ÀN2À“‘:£tÖlŽ -n]Lib×Ä7'ƒ^•Ã ;Ct«žIJ¤ŽªÛ»*RñÌí­ö)­äàDö„šW$¯ˆ+Ðgbr¿R2I +Î>¦\Ó” íô.« Õá#Ï“œVuW ¥3üLMXøQ'8½WY`æ*c'ÉrÐãÓ¼žrØ.]AŽ=Ó‰‘àkJ–u‚ ”H|1]íYÄtù˜ ¨ä¤x‡·:õ +úÃ)õ×õa[·Ê‡ ¹|ÖÅ·úÛ´íQW»íQ3E:4[‹>¡qO¼gÑu4S'bÿÓÛ˜M5^éÝãPªJ;ŽöxZÁ.\Óú ãØU`À{ 3Uâ'6É}9#Øu-½ªU– å• _*^r ,tQY²'¶7lãf¬ !È5ÞÅz!Ï%¨—¦Šþççy8síýI€¶"Z"ÛçL]E ©-MiÇ`­#–Õ³dÚÓžs +à*ì5jЗ4&$òM„Uwä3WíGJŸ}ý$÷MŒneÇ’åÏlµó/³ª¯•@jìVýמj&£eÈ™?¨,¼n(GܺŸ¥ÞÓ|”U…h¦t^ƒÏþô²³Ø[±¯“ùX  Š{±__ô5[7rÃÍnE…íÎ\r¶§{ºt‚òhôÝ™ëdwÊÞ¤DÚI”án‡xM¿lnIFñCZíNZü«xNO"m®ð~¤¹QÀdS#’hE·¯8}‹¨—D÷PûÜÉø=Ðc?VJퟛ(ÚÔÍ‹úUóq;¢¸IÎÓ#–‡©Ó£·*B¥™²³©gµã9u¨Yû¨Uª/º …ÛAAF¡ÂŸ?)N¯n÷ö†x9Ÿ%[[7 í" + sÿ.^¶½ÇU¯uFt €´ð,Ê-⭜Ԅ p;ˆÇªËPA©Í:§é˜n­pþ䉀ÑTa¥ ]!YND³èà;H‚Ð8—!IwúÏõ[|8«M÷TŸm&W <®/Ÿ]áI¢*®=hö·`ìM“=·Ëxœ3ëIò_Ž”Ý8œFUá bìáiY‚uîåîÀ¶sæ¸ñ¿˜‰w¦Ù±Tïv±VSAÏå +õmÕËå«=Õ®Á:q‹¼¢¸ÇØÇ-0ÖZ[ÑÉ¡½(q8¹óük¡&`˜(´#Ì,C÷ GC«ÞÖ1mßAGðÕ¿e'/’2ä`{)¾ys}€?¸Ak±=Ý»Xt¦ÒÒ-¬š;E—€Æ… ØÓ Ùê¨5Fc"’®½KÀ‹;U ”E9wÅâh€ +/ž@îT»mí˜yèY€Ï³4Úÿ°Ï&úU mÇwö¿¾K÷r¡MLœc¯ö?&¥¸™Wû%»#f#“†ËÑü‡Ô–z+Ž@]{󟹩 ¢ÀÜ^?€R˜“n°u²xÄ?€¿ÔNϵw „jD ¥©G7 ·ç³M§/º¶Nj1d…°.·n@¦|;g‰'=Ñ àK:Ìö%J’kßy¯KïµÌõ7n˨¢o“ 5D7 +GÅ7µù~Ü€L½¸VÈühÔ;˜JPæ{çt2í·ÊÉÏìîÁ{Gàó7ìG-¤û\mò¬¤¤ÜÞ˜§'`ê +4a·r0P‚ÀªKÀ-´neê÷ëTJ[B0I@øyõ€H‡#kÔÝbÍÎ"¿6çë”Õ4ÁE½¾äsXd&‚nXÏO\Lf¥Ö„Ò\ZÆßÕÔ%€¯>}."ŒCäë_87JÍŽanf†=¾À Š·Ý¢Ÿüˆs³Ï¡K8¨ø9åzœÚNˆúþXÅuÿÒàÿ=ºB›R¤ âú’¶®€4^óL]õ#«+P•RÀ¦2!‚œ®›•iP®ô¶w(>UYJ¡µà Ô\¼Ÿ b¿$ÃkñÀùÄ´Íç–ײqך¢h"}’68BTŠ Ó™÷Ž€‰ãO]9‘„pMÑøìúNG  +©›•–È{Gµ*ð°ÎHStˆ1¶Š2úûŠ~€ÒÈ`Ðp†ß9À” UPÈo=E§ÀÄí'„Ëô¢ÔZP@ŽÞù,G‹€zŽ¡§S嶚òÀs ³É쪚äÖÐA=14ì –sã𖌺²Õ#`KÕH%æ—Q•<†£ÝÂ&wZïÅ#ठ+)E*<ôÕ#¨4 +©f¤§—ÞyÂÎa£ï‹Úµ¶ñîîY7êðzã`Ä‘HDù«në¬êVØÆ#³Ì•fY¥¡$xdˆýez hïp"UÌ¥ÃT£G,–nVGàó—b:!/¶m›ÒV¥垉^ºïÖ”¶: Ô‰WÖý}J[E0f2Ó0³Misɨ² ™l•0–öŽ(‰ÖL¶ÚÜæ{׶Úü8c‘òZb[ˆ6ìˆ6§špß'°]Bð±Ûö¶ lز.•ú-™Î˜Àfœ ÂŃH?¬ lŠ +O]’Î5Éû6 l “GM`³··ð¼l¢nÉšÁf´@rË™æ>ƒ*ÈP&$TÖ ¶Z+kÕ©¬Ýd°™¤MG@J¬Ô>ƒÍœ€û£d! ï}d°² ’´³Ð/3ØçæGHÏ,l½‡«Ã{Q ©l–Ä/Äþ&•½•tÿ ©ìÏÙòïsÙ(«¯=™ ™ÅÑt¯éWì˜/Èeæo2–ÔEAç-¹ì~ŠñõåkZ7;Ù^t-¿ÕçÈe3jØXl03®™ë]3ÙXמÊF²%b¿ÃäÈdà `÷øczç5“ý#MîÉdCꢲaàôúùÿ kþwMPÿ„ò,Î8ú&9‘(ûX¾™ÎjË7ÓƒÂöcù „êžhSßxˆåÈ 5%NÞË×{nÐÏüˆ„¿ æk,ô{ÙXºBHk0?ÑÑSW 8íCù $ óbñŒ‘üt¹º à'8‡û&n–óÃÀ}¦Í®šípOÁ™Pº`©¯¸÷z2É.H³²÷TÄ)\Õq-¶pÜóréèÞÀ`ºcà^ƒ$~Õa'aW×~Ê£5€ùOƒ§`J”ÇÔf?Ÿ~*ÝÑñè^ÏD×¾ªU6f²9ìîÂßüùÂO¢÷ôy•j áo+²BŸµ¢œ _ÌŽQ!|ó§›ÓáCn1„oþ–YÔ˜‰Û>–ŸÌ‚¼ÐË'hÐ/±…&9%kL?a®«Ê7Cufa6ÇH¾=ë8 Jå“_[#ù #÷· ì1í#ù ç3SÝIø'—ɧþS1ÉÏ^Ži·ÿû¿—BxÅ-ŸGXï7p6z;nʹaYGDÂܶ†—vp–ÎóöÜ®®‡ÀNä:˜Ì!pe+iÛ™=ì¤WÛÜD!ðê´D f‹ 1g¸J Ù°ÄU +TÀ7hËm2ìÐÙv78;ôÒ Õ9D¤í„é½ö ÀÌT'Ýhl|„¢ ÅN„Ùf7³Ÿûý{ŽÛ Km— Þ–€þÅ”`R—òJ0`³6gªB4+ʽüp®Â~:Å—Õv  œ7b5PôÛE}O§Dt=.‡tg]ìðUÌëŒQã˜^´‹béRçè‡`aŽ×úêPþØaP#WÔž,yâÕ)RÀy'¿x$Q<ˆTتz¤¤f{ÍPm/£ìÊ ðâ.à›M5Ú\>àlÁF€œâ€‡à¢ù™z™‚/aê‘N$ÊuÌ%ºœÍp–ÙYEÝ÷ÄùLåñT+,¥ÙI‚‡ZUU%» ôSòÍ´Õy8K•àòÁƒµÓ`;r‹WX1‹§ÐÞ/}l*ôD²@7¨‘ eú‰“£¾÷*¬¿SÀÂ'€»Í´i.N%ÃFøo†ß«a’ÀWËØþ”#qSÀ1~U~pÚ®™EN, øsó9Oņ鳃‡+šF؈ñáÉ’rÝ:<*n]dóq8,+­iyŒ\DxfC¡–;À e6ì*gzYÈ¡„žÆu ŽXÎçø’#µ*~á‘Àó¦Y-Ó‚E\ƒäG–Úf”«ÝjÛ{‡6Õ»=5ÂÉù~n±™°K]•Z7†@•Sêî/° ‡I ˜Ñ€MÚ¹Ð'l¼¶R*í²Ã “©^ ñáþ&qø áê¢Ò*±„Ž{,ínˆ7/sÂÝx·£ÜŽI}|òö÷å<€TÆ +ÕÆÆ£`ü¾û«ú‚Ç'2{FÀFŠ¹„§FdÓþCêøÿwñdÞAó;Ÿ8f("#$;U Ó,ÚT±€OÄ”Z} ™ÀëÝu  p=„Ö2Tab;\þ¹v²[œ·æ¼<ñ)"$g pu€Âåaªž5ÉlF’ ’î5Ól d©9 ù|¢~Ð.š]«¬L5ð¡‹1B]8?öùÿzÂu9Ä,÷`lB&?ì8^ÖÈ6ÈÓ‡×ÎlÆŒz´#ñÆ®À9èº,‡Óßä&…€†ÒU‡»!nYؓцÓÃ6\q›5%„éR§ÈÓE ÉÄ(njbÉ­qJbBø ½§;F¾ÙϘkö'¬Ã=0yŽˆ£úäøš½1 ¥hm—T:¸ýUOp·<®XœÃJãKï7 p„÷)Ö2¬ˆˆ86 ƒ(ਮê $³¸°¡@›7Á~+ûÍÉ‹IöLü´Dôñh2yKt$•‹7˜W#²=Mòš•™EÎ, +E\ G¸SÑÓÛ@%dƒœÍªÊËÝÜ,bôШÕØ£²žXl@r!Ãr5ZsPVÓû6MÚ>MŒˆ(àq¦"h£˜ô=Æ-ãT⧛1JÄ–‹‚ +™ž9óöiâ{ö¥ z”… èyj&Ì=†ZpÜ%žÆ0CLr¢ _I%Þ'èeD®ÔªN'ŒßÔ³‡'± É« ¸dpÝ8)ÝÓ]S[õ<{ëE>P¹[ÄRðÿº³¯i©O¡þ&pÅÉ@%È}è[5+¡Âñ"öÜ#@¶’7' A©'ÔŸ„…ðõ/g³Ñ!ΤÐí`Þ:5­ê  `]k¼··ZÓ;íS$õ»Nó‘\>úÜo§N¢$]Þ„J†þ`l«"ÃæÞA<èDDz:’Ç¡©.µìŸã€›QБ‹Ÿk8|ꨂ;ñ” ¸Ú±a.Ñüm®ï«7bZµÙ¦04aÙ3‰p£¸ïÓå¶|@1IpáM5ÃáÅ0Èdƒ^w³‡6" +õò%=AÜŽ6)x‚Šž´Š`»/?8àa >×Yò›²|ê~€£Í»¶°ó„”-=ðí—‚Õ<ì$ mˆN-gãòMgÉ,ª÷x¥ø¯hÒ9lb>kÿ>…*1þx=’^vTJt|ÓÇ?®³b“äÒÇæm06NyT‚Ù3ëÄdñ0&ú5² ò™ûæÏ›P§M¦¥ióåðÄvàá©0ojŠ0ÃJщiÈ7©"A²]ñЋv®}E“Ü{ϛݷARÄNˆŒËOË£Uú€L5ma‘Ó© rá ƒ6… ÀwM¶½j×múÒ`s±H¦¢H!K3Ã/ÞMØmU·»Ã©YHz2òƒx Á¨-Y% ­|5} ˜¨&öÙ¼ŽS­í«€Sxˆê4â¦çU¢ôÂ2¢qèf@ 6²Öì·gWAMT \#rDQØ“„$“nvÊeöJCÓää%ð(Ñ¢«ò±3!˜µ[Pb´Ÿ-ê‹1‚-g. Â{%È·M»ÞVVС. ŠÁ "¹ôp°…T*QwúN·ú< ^•*B;¸÷½± ò5ë¹TžsGÛ"›˜¶C`OP½¯'¢‰Bµ -†4f¡¬–15ÚŸâ‡Gk‰IÓØÀ¤ÚÙZ]¼‡Ö Þïb´Þ„²®˜`#ÙGè¡}ZǪðÙ«ÕX²SàRg±a4:Ëpºí, –`·‚K +Ú “[°˜¿~Øꈶ*Œ•hFçSÀ¨àÂûXá¿Ö8MÀH, §aºÑy!CXƾ1õ}†ç˜aé{—;åÄpONúêz€A(1hJäô@ý⾃°veÎ/~çýP- úʤòœºqžaʈ¿?ü®ø;·-<5½>J%PÄ6oØ»‚Ãvá€Á0¹÷~gÅ„Û–‰ô¥ûJÉ`•^k…J‹/ybãJ‡§‚KNç’B”ÝÂùÜz~@aFÙJRxfî#m©à>6Ú—Ã{€æ^ÑÆ#c9UØl"êø(œQÏÈ’5Þ…El ~êÉ«#–À +}ð:vˆ Z÷á¾çˆ . H4¢;%Ä…~Ç* üN©÷6¼T`Ö¤F¥ÚWlú1LÅ°U @¥Ðk¨ Å M¶â—À’ï"f$ÞšDU²ÛNÁíxt¥£v©Am ±;äˆg„‘Þö1À2Üq“‰ŠÙÇX")³Jù¸ÝrD1I–‡*,,Ah|ÎD,kœ¹±q-FC©cצQ›\S‰ñÔ …,Ù§’Å·ËR»LÈ-„uUÕÝwH'!±††ÑAªÓ²±P؇˜ÑAv°ÐTDUJŒP«z …Ü)ü«1ÊíFÑéF‘ß}´èé +±ÓTàiXqj\!6:/Dšw‘ýr¤‘m,b„Þ¯Ú0®¢:}I2®Ã¿ 'ÁjºÍUÄ׬9VðR‡US­‡´ ö÷9\‡ b¶ÙµaíËRÑ›,Ž +M¹d¹Pu¸É ˜]¡TTbFÉ׎úºCå©tbÛSÔLW! W6T“ƼfÒHÒøE• GcLE¤fŒé8fÀÏPB% ´Yg‹\žJý)0Ó +3qˆp õ;Uò´"^A›Ñ¸—v¾¸t’º>+²®n‰0 ÞD©ÓQò.P vá1sq7[™zRæVhVhuQ‰{TIöªí½yÑanJ#ۢأ®¶¹yŒÐ[ÄËô‡jå +¨\7œÅU…ô€N´0©ö—=¢i™¸P ¿Öx©ýµãjÂi§YuÕúšíMv>+lgŸÇî¡ §»„)vÑÔM@ž\nkWIš}Š Ö›-Ö×(± «]fÞ0”æ€ìôg4 +ã”ÌQPÔñ¶"CŽ,+c^z†Vö>,°Ð ð`[åî/ô–e›ú­Ömª*5û‡¦õfÖå&±°91L( zð´7k½¼Žoj¡Ûã%ž]è/˜·…º7w€ÎÿCI~M±Ü¤“á2:AÝ/ñ2ÚøÍ< µùLr”Ã¥¦ï÷ƒDw™ÖH¤Â}a_pÚ¥÷ƬtÙ×°­'ÄvT7möšZ;J{¼–÷òNœ$â5°§nD/tZÔs9¼Qöšžë:†êÏ(È^h{)@%¤Ñ1ŸW5PÐãÄdw&-Bl­lbÊÌÀçït«H©°x¹Óð楴ĪIÉ`Ñ´ág ±bp ô<¾ä{Ý EMVS—­3ç7è_ ¢q~í ‡Úñ=ÎOðk²´ÕÓ÷ÎÀ©‚¾wTtHm9÷LŽö¶ÕÝ^ÛðÞ™/˜ªObªZÄ‚ „"ÉBªSÀå«…«zÉÏW1v!—Õ3EnÆ1›¿,±Õš³)ùÝ=ä»n¬B‘Ì–ëŦ¤PM]€kŸ^öÞ,¥‡ÒÎ)U‡M¥±«uK9’·|TŠS¦y»ZɧƒOF›ó¸šæê.LäoM¦ôY³Zøý¯\}`qO  ƒoh|-&·^Á»‚‡rN!œÐy©’¶ÓWNð]‚“D!2'Õ=màcÙ0ÔVÓ½G?­à7^ªä‚ãªG/<ážåâ7nÞ³z¨ð=(Ò#4‰¶{‚ºnîPÁùÚû°¬?9¡ÈÓýýéøÄê–“ÏÐ)Ow©àÞÓ¢xIöšþ Š/D€J)ªS¤ÈñuCP°T;$'ûZæÔB4óbjGê=OÊ"†H¸¸Edú\z¼Íw¡Ê(uÍ£¡ê%„kxBíf7c„Æ°D Œ™€üpk×èuqK‰#)¼‹;¡#džfÖ¦¶*j¹J‚Ñëäqçyxþb|PCÜI1ŠÆ‡t¯ÍÔ=‰‚q¶KžNHÔ‡o·ô„Mr\kŸ«—ÝV³+Ÿõ](‘§¤°ø©P“³†#Ñ’¼ Ä£ûŠaM‡D½õùŒK!øÛéDJ/!ÄŠ´R=¾*_Ë©å šÉ§«ô.Ð[Š‰pŠìÜ”„`qM^”âåÄ 38½jJþçé”-Ákn¤"(pPËP_àâ3‘{ù>|ŽSsC¢ú Ñ.Ax®™kÑJÙš÷*½ ä«æ‘È +Ù|$õšÐjßÚ“K’zÆ@ßåx Ž1s\ÍŠLÊ4Å<†š® Nú™€K>„CÛñÓYí]/!§¢ñUu_y­ÈLÉè˃PsæoLl" !ý0Çï2?:´ôVÚ×RzÍaPÒ¥Tè⛡5ç„H^.¤«f ¦y», è‡´[§¦€¹Ûöš>C+ùy!õ"\’%§±K=Yfõ´˜Éc)ZÓÁ„?d(î5#ȇè‚Fýê']³ŠìÌ­£E—nÙd'#Þ!Ÿ)ç‰Ö{ìD÷Ô@¢)³æEÁeºº²×Ö´yÂná©rëÒU!½Í¯† ‡\ïÓb62ÄEÐ fÛòÙö7òÊÏÃ#_?ÿa;ºùarkž<.Кo߯oh‚ û4R q‹×ŠýQY+‰[‹â¡]‹ ö‡-¦ˆ—h­Çxî_¨æØ^ßP²^þµ°$ˆP òVè„ú– ªBLwk¥ÍVj†‚)tC­OÓ¡fh+î×Ò£Ui„Ú¥ wféÓVm…Ê©©ôBÑUЗ¡xk«wCXÐß¡,Ø¡í­ ÊÙ‚Jâ‚Jë¶ÖÐ¬Ì ÆT(ê Y¨ Üvkiá4 +CQb´'×âÆ`—~³+’ æm¨± fò¯5NÕe#;TtC=T†îìýP`܆P¤]µÆuë„ZÙà +…2ÛàNÅ:Þ[ŸZ½»P0ÄPw¼u4Cýòê°ÆèÏ?±sž7O-Nø,çþû,ß»ÿk ytÿCùyE„2ömH#”ï¡‘XP?¢*±” Õü!¶fX(ôì¢J±/ÅÚFô*4Clƒ_±©b ¢ÅÆŒ5<Þ„ñbÈŒ=&k$1´¨l#’K£Kˆk†V™ -7oC«¡c'DfC×ÏŒåÎ^¡]8v­qäØ­´Æ¢c×Ó.¦{¦ÖØxh» +aõоµ χ6°Ýdk† v¤í2 ñ=kÆ"6Ç­YØd·ËžÄf½5 þÖLNlÜe„ê ½X¥z"lÉ,iÖ…»`Žn1;%;L{]œµÍqq¢ýn¶¾[õkª \èl QŠ$bº­’ä@–î’upºŠ–ÌĆÙk-&ýX{r‰nwžwLb—cÏ®YÇi¢e¿“—µƒ†ÕjÆpÔר,œ“dF%@(­Ä\jUÍŽÌ 5Ùž5ÛRW3:5xDévYÝø–59G²&˜ãŒÖDõwÛ•YÞϺ®érõÅ„mYÓîqk×Ìýs2v‰ÿx°Ö‚x8ׄxÆwµ ñ®¬5ñ¾­eŸoøeN8ñ¦M¸ÐÏ]±… ‚«õHü0”–\…‹0tâÕæQšÕäó²:•43ËÛÌ Gp¦|úδü‚_ŸðK~:‡éƒ¹_è#¦¨Y2£Â¾'Þå¨0$¨-UD?ÉÝíð–ÛÅ …±ŽÃ4I.âͲÀf:ë¾c5ŸDž¼½nAˆ ÀÍ|n?^7ñ¯¿ÕýôÃÄ‚T(iM³ÊÍ|vÔŠø8®ŠõœIaÍé*‡_ SåÊ×ë?ˆÐØÉÇ~7bwû¿Ru^õÈ‚vZ¾x?êïFèêýCµ8D™…&4ÚTÈŒø>!ö+½Ü¯ãGÒ$üd²¢<¡Gñû¶ãJéÅ¿^×-ŒàsËÿÝûÝ:8^&a@ãHIl.Ž QrßùPöu=#YuÈ HtÙÿë)ÃKÊžŸ'4èûCŠá¯FUS¿Þá‚°’Àd*ñº +¥â…g( ÙU …·,W7d½ýqB;!².KxK\ØEŠ}v–Ë·žPšOë ¥\šŸêELÔઘæiE‚O³‚˜cÍýõhÑ_´ZÊ»ë·ù«åPn>¸>±ü»Ø ·¢ |"9ãøaï®P"DŒ÷Öêµ¾ÎÀl¢4]SÏO3Ø»ûÿ<®ð:„ÏîÁ¼‚ÀšèÊD?aÙé9S¡DhF™½÷_ñJ“…´…ª r@ƒÍ\ª"0¾qŠj¤¡¦©™5×Wñ@ÛAжy=ž’ Ð3(D`§rSLoå©ëV¨Õ<Ê¢í‰d +ªïMZÑ8i;€¥:«N#“^§“¯ŽÚbV*À¢zB´è“: zAìB¨âçò¡+äç§Øs­™¶ÚÌ´½²/±‰,^êKÅ ½y]!ÌÐÐázÜL:snÁJR6K¬rãÍ`Tò`[JòËA¾oG«UÛ{UÞ[+ÞDt«g@Ø_ñ9Àþ[|΀«?ñ9?¿˜âéoñ9ž~Àç\ñôö”»`ü<ÑXϺ¡Ü¥œÅ&}§gi5Rî‚|âIl°ãÚSî&1=%¤})w9°ðšðÁšiº®Ç@Ç[ª·.T$ïoŽ烖ʊ=‡ðZˆy鬦â¼VÎ÷†™w½‘ß|õc2ó¾£_¥z»Ñ¶Õ÷ļöÈ;^^š;W>Y3#ßÓÉ^¯Ì¼È +S°j/¯vAK2é¸2óš>„²4Ék©¢È‘*w÷±‡˜÷ £Änã×à¾ÀEoçê¢~ÇÌ‹hyKÌkÖÔÎÌyy+ZL¹•%6‘eê–*ÑŸüx3{hyÕ©%>Ÿ^VPŠ“–÷£cñ{ÞÿãPqEÞ»æ‹Ì’«æ‹Ì’«æ Ì’[͘%ƒæ›Ì’Aá fÉ­š Ô’AÍjÉ©æ"£äNÍ=Ô’«š[©%ƒ– Ô’[-¨%ƒº‹$“CÝnɶ‹Ü’«¶‹Ü’‹¶‹Ô’;m©%Wm©%W]9&wÚ.’M®Ú.’M®Ú.²Nî´]d\µ]äŸ\õÝç/â¤ð¦y±é_63çždŽôªî̯ëD®WêÉß}bñ$5ŒjÆ›êŽÒnBùѸj>:ÿÝh;å_vŒi +¨ô@ßv.rÝ0¦Ù—N—`%Èœ7Œi&ÿ„gÿ)©-eZ5”z³·V>ýl1 0¥…f¾·Li&z"i¦²ÉØsÔÆLÛLº¨"SšÊ[Õn×ûœ¤ê+U=ÿš^rŒÀ”ÆÀp¸!J#·¢‰˜¡Ù)èØ¥I(gÆ›¼+¥ÕA–’20w÷†(­BºIæùUjbÛ¥™|ð/ï+ç†( ¢?åêŠYÐ-íjš”†-QšP Ôøi7¶•È“VFý egÇ< ïÓhÒr²ªs{Ù¦yRó.$ªÎa¶³Ó¦ÜøÝ%¦m/É)ŸŸÔ×O„ý%½!<þ–´¯'µ—nÈ{j¯N Š%ºÃ> I›ÌòY2E¯_CÆMÓYš³üÖì¦ÓÌàÄ¿3í^¹ôÔzÅó%Ti¿Ã E^öŽ+­ íóO0CšöM:.li×çp}xN~tÐïN—f‡k¾™…Ÿi¡µóˆh3[¹B{À!ˆôV:b’¿ÕÄc¿:Ý]&ŠÙA™=ïbÆAEË8?LDö·ü‹Cšmê§H@ªLû0WFp_4’ÓâaÌFBUÑ%ºF +(z¿Õ ›½nª žÌf[xÕ#ALŠ ñHž\±÷aUµÚx!†Ê;VîéIi²ÃfËÙ–Mü‘£"ä òÍ6»›ˆã}xçƘ^æGÍŠ£ê™dº®ÐFªæ¸½¸Õ³ÒѸ‹mQEƒn£3Pè£sܽ>ŽIA¡ê¼4x úRÙfY¤ N ¥çÈ·vÝ3;DRéî,8÷›ü}œ†­È'‰ð¡·/ÅQŽÛ¿íc#ÖÏ‚GŒG€.'A±¾´ÇíR¿«uYߤáOBþåuý(Fl^ÿPh­Ý/?˜Ô µØGÛl#.:ˆ^R±õzl§ .ãýÁ‰2sïðd MX×ædbÞî7ó†²ïÍ¡æOÔFmr¯|P»y ˆið&&oÍh +ö‹\.ÊnfäíåÓN{¿äö {yœ­½Ä5E¨µÔ|b|„½¡×ª1Fš,fÅCØZêŠÆiö‚Œ¡õtTSôÓ"´¬†S†Ç¡_3ÁîÒ^OÙá4RS0!†åp“ØsöœÚÛâ<åÀh÷Ht®Ì›½Þ«ðçán†ï‡;¾~a‚” K¤ÕvK‚Ô [$g8Q¿9]Sn‡#Eþz¬?§9fŠŒúŠûÑiðÙ«˜ðº.„»§eíÊe¬ ´q4zàó)$ Òí¦F»… ¸È,ë¬9³±Z»‘Ú(¬Ó ÷PíKU·a.#&;MDfŽ4. +¹ *¼˜K”q²M$z¢g3À}(‚ZF%ÑOÜŽÓÍæ(HEŒzEUKªH@1ä²MPÐ7KÙ%¹qxÔŽJ¨ëb!hq³ÿA@ˆnmÑFPWu+ˆŠ â0“Bké‘ÇLê`°óÒkNPÊÍ¿ÈŸùßjœíÅË‚o"^6Ì‚4$·«ðcǽï’vQðŽhíõï, ˆoÿP#Û…G¬Šº¨ckMþ¶öd+H%dJÆè Õv t¢EÙà±dç(AÁB{Ó©1AQTÕNåWfÈ^ +-c‘ +§º¡¬Š'›o8POûÜ„y¦ÂÔ¿túš)MY3§í™Â©Cåë×Ä·¯jÛòÞJòè­ìÕé½EÑÛS¡Ss±0Ñ Ž9]Í 0ýª½¶÷HÁW5Ÿ m¼6A˜(Ò{ʸ²ˆÒæsWçwv¸^C9 c†r*epDÇ·œÔôóX”ï™’«Ïý‚c°(ÛUûzu ²9v+ÁŽëÄ{íÙ½ vA:?.”ÔMEtkCɲm¯­­˜¿åçðcì0MPÝùÄÔM ïH©ÅØi¯¿œ‰—cô»L¯ßL϶ÚÖ‡l,¶ÅOþi¦Ççìª>ŽïŽÒŠ/p³þÝ×Åakʼüá6Ÿèçýòï~ò“?úÿáOñ«¿äY÷¾8$¸;GÿŠÝ±ÀÅJÕ•¢MRªñ Œ™r :²¤±>hqèkL¬’¤dmÒa)õS¦&‘ +?FÞàÁì u2kLž™ÞLBV§»ñ®•ŒÌ¤ãqœ +°ïi/W1tv@XúGþ(ü-!)UJÀ¤‚"ÆPn‡H<ï}«Îƒº^Ó™ ¸˜Z¦pŽï4bøÅ| å:8f’ÒÒ½UŠþô¨(=‰È<ù¨4`ŽhÆ"¾§ìIÊdÇû³×!’;„IThíÍZuµOU»’IÄÀɢMÏT¶2fö߃ÃfÀ>©§™lPõêrÁèŽ/#dÄJu!eŶ¤¾Ð…†£ ­Ë(¼œÅ ¦*_[jrÔ¦Ésøð ¶2tó¦fË€>I£Pã¬Y‰ K>¨ªlœBfª r¤ ÷« +ŸYj:=“J´œ&ÄÞêdGvd’õ(‰»ö)S.oˆ#ÆwÄ`R[fÖuûηLíñè ©ÄAU6ÔvœJ£wSÜšƒk‡ ¾tZàøÀ) 6Ï ìr[5»; LçâÜæQRÖeÐ|Ûºª6K ‡Ÿn±AÙí{W6tƒ3îFa`">š°I00w_È\⾤ÙÎÌ¢Üzÿ–Ï%û@pµfšáqfÓË ÓR@Ñ”s=DíBÕSuŠk²>7ݦKDx‚f˜s9",ËiºöèƒÂ͸“n»âØm9@FI¤BÕ3“X‚bF½|$§å++U|K Ù%‘qPÜnËJ„WS‚ekæ çìPÐwf徨jT0»‰ÃŸP‘HxÜ!jdPŠ¶@5eŒîs$_‡<…reVõtM*:Î^—4¶Åö +ômËñ »¸ «#ú˜Dkškx⪞À¥JÂ6Õ×,<µÎ—ÝSs#ìO­Ö],màØ¡=½yýv;¢·Zç)¨#âf +Tá=4VD³ë‰u,á‰íœÂSam0¿@éî|[‚y]ßz¨˜Ç¬Ã +ˆäGû4„U§›V4ë~ƒævP°L8I= +é}KÛ]GŒlÏT·z?íÈGßœWs©ü "É5mÎ<‰ZLyóþ +NØöêp#ÉEt¹k»+X«×¢,w׬$_É*Øá$Þe#@@ÂÓŸ›i–Ϻ‘AÇ)8Páû»—´‘e7@0TÈ­’^ÉvGªtÀ  "øë%xB“?kéöþ ¬Bâåf˜\¹Ýèæ¡b.DØVÍö¥¨xV¥uÁÇû^ÕA«èUS‚é%ŸEÇ’¶ âFC«­ú,Q¿+^¹1 '¾ÚÞœ Vz\çwÖ±ï øüÎÁÍl=íí .ˆ­¥Þ£ëŽ˜­ÛY¯½ýv€ûÕøKÓ)6+ÆeÝf DÕDÙúhˆj·A÷ÔSipƒDÚ•MtˆLx1‹‘#&pWºZÕý¬ƒ\ó:·Æ<@¤dJa¾É›‚K`×Þ®ñ*Ü匫[‘G’ÐÕ{Ý{'©(ùô¸48òÈgú U¼¡ðÄÖ™ +O_ŒÑàE ÑÌÂܸqµ;º}¶õ³w]WÒ–Cæعgÿ3l\ÚðPðŒaþ$1p&šowN:ØÌ•ÙòÕÜ:é¹@r$òæ=bp$¿àxvðLJŒ7äú‰2¥òàƒcÜ(OòJ®sû 9]‡®H½?ˆ¡ b“D¬)ˆkŠÉÞÅÖ‡²X¢9àFKå"Š‘@ +%Ê<1eùõcKxô0a†h”ɬìØÞ›`Ö5xÖ˜Ù&E¬(kð,ƒ¬”å6çÞn}"w7ÇXÜë£ãÎy*<ËÄil„Xi%±I²æOn#š°Ç»u¾ÄCͨnà=†Hª hrµcv©št ÚBõwc¬á^éøs#ÁÔÉ–3Héš!iúíâا` ûý>ÕŸ~ÅxùÕärnbíÏŸxxõ7òÞÅóé€ëx&dà(D­9è±ÏY!ô6û€&JËI!¥ŸkC'èøä #(¡BÃúX_!5ÿ‘0³®×]aHØDË£ùuì$Ù\ô¥ãü˱õ4•c wzÒÖ4ωºÍ‘þ÷¥äýwã>Ôâî¢ °K Ñs¯êÈ€¦ëÍão›£|5¯-8Tðêc|n«ƒLÂÅŸ7„¬³ÝçƒNÂyß@5W»Ey¼mÞR€È)£œ>Gð: »Ç½©D¢ÙâýÝ͇bÔñ qÛ¼L©"œuqØ™´Ýxd‘¨CàZ2«ånâb øs|·)Þ Mö4¿¸.C&Ú.«±ý¬–»q!$HÅ.;!øxSòaAÙÁ¥Ú^[ý)¦k×ÄÆšæÝŸÂöÜ -ŽÐ§MmÅóš£êy°ˆA¾q ;µ ÿûÝTGtïsglk¨¹%&hòJ+0åÃO¼-øSö?ÀIŸ*’ý1iÁAL"ÍŽÖ·Ú3€×аSAsŽMãÎqÂQ&£$W‡ÕÖuš3à†Á¦P¯ó4tOš:N×4Áî€Wâx%hIæ àæøuô çš­ n¿>$*”þ²Rœ‘ ^sÂE4ì1fHÿÉE¶ÁîÔc'½³ëªü&Ž§ðΦÅ6ö¡©Cú¦4MË;9\ËauÚ{«aªuN0´Žæ Æ-‰Þ|ô©jQ5ÝË >àA4@D°ç>« +q¦ •=#Ìv<Þ46lõËŒ‹Uû?iœD;g{¢iàPCÙ ¦k‘ƒäÄuÙ@iF"ßDï;¡¢lò†k‚€Z«û ½¼‹V–À<žkd§BÄþ*'1"”|Æ|`4KèÜÍkòGÈH;…ÊèÕ—)e—HÒ®QT}®f}Îf3‚K0VØ逦Ûá Âô2ã¸À`5C&æ°£BX¶·,ücô β° +ÖD+¬Ü~TÝ佸>Ðð!Š-Æ +0|;»X‚á ýû‘ȉ²Õ—P• »¶è±âò¤˜ZàÄUØ/‡˜]¢ú”Ò¬sÓ)¢¹çŒy0®æY³U}š‰]T^5æ?€L©²)ž-÷æ À0±AèÃÝO­³ Ù`` +ç +þКõ©” +8Òx>g†dÉÅ÷¬¨8–ðÄvNá©°6kV-®ïš{·;Kn/lí’|ÎÄšRÜ©55OæšÞŒ§{M“îoÉšn—mMØÆ »&~÷÷}ÉG±±¦ £ÄY2Ù[¹µæÃWéRêAn†ÔüVê†Ú¡:`ŠùYSðV%¬…Q¡Œ† ŠB ÄV“…RŠ C9FС¡ªc«gQHÐÛ¡ždÕø¡,ek.„ò–`l„™`§„R›7ÆM(Ô Q(ö ÆÔZ3´5ÅBéQ0äBùR0CT0%¿Ù•SE“t-Ë +¦í¯GÍ»ò®h ¯%bÓ¤õe[C<Ô©3>ÔºEW`-™Û»kÅ]ðLF­^teæ?l=¡ùÁ‘ZK +£3¶V&À1:‡kä<±õOãS«Ÿ»T}FWy-Ý»Ükj|ÏZÈÝÿP»‹"¬uµ1±ÖæÆ8ÆZ㻇¬µÂ1®ÊChf­ZÞFxFÍó +ÕÒ1Ä4Š­·©P«âZk¹wˆÅªñ]<-‡¸\(\_#z¡þ} uô!œJðgr-àß-×.€ç ík\44l£ª¡‡!egûCã†î‰m8ta„rèߘQçÐþ± U‡î‘é(!HúX¶1öøž˜Ÿ}3!‚Ún¶ €Ð½ò±hÉ<ÄF¢]Þ"'§ô°+˲æ?4ELi¦ÈQ^3'2Á´¤]±6i—,hÈ%áÚå\¯Išé TÐY5¦{` ÁX5ÄŒ›¾Í‰‹nï¢&â3M²<ÁÞ³-÷ªþ5I…Åå6®kxÍpMÃPÙ¯5- ÿ-ÂÏ¡aÊ07o…V«ð3oY¯;æê¨zt!ÈáÊ©íR}ÏŸ¯©Áøå5«øŒwÍE~÷v–k3®Ñšý‹¯FÑõ»µ^ó§q¿ÖÌkÜö]â6œž5í`H‡sü6ͼނ55ïÑšÓþ|cóX9e§Pß5WÜ~µæcx&(?çàÏÌð£D6YXE{À¬]'=Ê*žCU•.êñžЬM¹‰¢ÞL©Ïü+°¨3bÊЩ4KY¸‹]èÀ&É•©ßÖøÇP¶p ýþ¿ ’}©¹iåÒ6Ã^Ÿðã»Î:¼'®Ü=N¸’tÐ}v^÷M œ4È옞h&rbvŒ~x÷¦Lµ[CYíÖ1¿àùé扮?•1û~ “ŽØQ"çÿúåðËx¿¡§÷OSQAò†Ÿ]0ï1ê‰Þb¡?8Qf䚊¨›ØƒÍ AËü„ …WB­ iöÍ{ÆÚÅ!|°âß½ÛZ¨ÚÝÞ °ˆ«t–íïræ7'Ä9.í¨¡íXÿÜ:áÞ­G$¬’>8]¯å·ìyý¸ŽÏXÞÜÝgàã·×.¢áÃuYÏõr(ú%ÀôÃöo†´Ù|˜Ѷ71”^ÎÝ·þt{À7î›ÿúÁñŸÇè¾{së@NäG²ùR™ŽœõÐ XuÑf—l.Ÿê–, ^ ì–pÂ[ÂÊ…q|°¶óªèy£üA\¿6go_À€ðSPÓ,ž¡J8¤äBEeÊ"ãi=Èö¤¿@›¹¦O{ˆäá†ÅDÝ¿ÌDnàÕÉäãöõ[æ¤BѽŠ ag³‰‚! F³ÃB¥‡TK€jýÍ Þ%ú0jðÂÁž ”ÂÂrÜ:ˆ#túÁ)¦ï1Ñ~˜$eðиT1SÚN‡Ž¢>á”ygþØÏp:pO=:P‹¢™hÛq¨{8¥Ùr‚é@AžºÄf­@& +´ ÂD©KS³‘Dg©”²9¼ ”ìJD‰@ÊpÀ5ÑO™#6ØfðÄ=®þU`ÚåüK€MCõ¼U8ÙìÈx;)tœèî G +9Ü­YÞ’<&ÙÍäSiMGK½=‘°?˜EÔxõÐ-© +Æ£\v2ï-«z3,£WH$dÿvˆÅQob§þа™¥;›–9DÎÙ¹žè·È¹àdÝûCUÆ›YÈõvûS[Ä4ÎìûS±¼ž]ýZ>¸ÅSn 1oT15ˆ­Úì;´ÜÅurÛ Qvò®yÁ`~¦j&ìÞ4Uýá"wäs-qã³Pá3ïY‡¼+‡ ûï8!,uà¾ûz­Ë"sÚe{y#íV”¤Þ¹Û§÷UðDÍÆn'Ï9µô T{ˆ%¼…³¤¤. ñ:G¿L‘Ä 0„-ˆX…#Ý“²_ÁÁl.Ž +É<•'/˜òV'À¶ðæÔ—p3Aò¥4<ÜëÞ†9a~¯ù@¸*øÓAûÕK0ÀþªªÛ.¯É¡M’—Âd±D™?&Swñ=«È C ’w;£ ·ç‚LIVpUû +gÝР³Â‘ªo{²‚ + ç3¨áõ`O¾½AóÏ[l†p!÷;Ø0A>|ÞúÈŒBBý3± Þ;–Á[€KÎAÄ$é³í÷²-œé‹cüýb$Ú]œ,âR+Y!KORN”‹{h†\ – <œj.!F®žH;T»¯F†X!ÞÄsgçäŠÂßON +>Úc‚ùH-J¾`µxc 8(MtŸ¹ÔAéÑT IäYu$Žl/QihvØ‚¼ÿ ÓDSOrØ ¬­ÁGqÂg[ÏBQ MȤ€=Ú™ús½§´DaÄt ?©Š„.Ϭçä$:î’,>œ¶¤_¢1g3ûa¤Ö= ¤4!s¥íRSGMmvvÌ6¡„Ø4÷ìlüqh<•+e1³P±Âyºrͨ Pü9°É¿ƒ¦]!|´ |‚“ÛÞóJóâ"}©Œå1 ÷ µåOÀ[3NZ¢l•À|½Ì¡:$å ŽÃóR0 +’¸…>£÷8Gˆi³¢FîÑ"UE6#cÂ)5!À)¥yìÍŸìÅA\®p,„’·Œ”ÛhÄÉ» äë¾Sо Œ²yµ­Œ]ˮܡÂòP bÂ.²j YÒ.ʵBoGÅ &J÷UHIÔg|á–gú²è“U:³x:“¦ÙHkš­ ‚æé芘QRvÇÔª4îÈŽR=hmt«ÒÒFý>íæ) jP»ê›.àÜšÇíµîõöe¦­Yê?‹/beåäX© +û›¤Ïö‚L;¾É±Š§ëà4Mj*pùÜ%øæÍuQa!e´GrŽåḊQ†F×Å„Ë j 1qˆÐg'Å}Ó}K T½}`»B(3D<Ø›…Y¶€óÒ¥¯LA2£‚J#9H-ÏA•LR“¿7†"£È'v!'ú´%d”ÆÏ°&œží +õžé)F:Æ+RS¦Ô˺Txã`QHІ$dž™-¾«2ªC¨;¢Vëš(& óá¹[äo©êä¸vØž£*ú8J§Y©4;”䨥v«Kºl¥4;¯^©M"ÝËï³MŒ©‘¼djæºT5Jê´²MÒ¢}rµäD tìŠÎýö-˜ñï\¢ñÖ6˜¡ +ŒÞpKSl'Ž¥lN ‘z‰©Œ +¢þX¸¹W £—Ô $ä·Ôp@ŠPÝeµ»:HxŹŸìNªj¶5 +ƒJ£"-H()¥â¯B²[q}Â'`ÇV;1ª² ¨‹•æ|$äDŽµeõ±bâR¨eÊ"'‡¬õ彉õQå\Sª¸fÉßÝÜ@…Š°u¨}oZ,»¨><£÷ùûôÍWÿt€rPO:ÀûRÁµÙ/“ÇŽº÷*œÇî–Ö3+¤@ìCQ!ÌfF±í«ýoa—ÛmWVoÒÚ+Àh.¢GÒ^ä•ýã¨ú/ù¢”ÅÏ„pM ÁH˜ZYNÈøò;6@È@ÀΓ ІgBµÄ—ÍŒ’:êNAÚ¥‰®špÙq=L +\}Òþ33ÓÂSÓ2èu;ûö-eN³qKÿÆøÅüHÿ Lx´ç žíU!Ã.¾Ž—ä=ëúÕ™)ðL2ÞBF¢ŠÔˆ=EbÃéžà¢ÙFw̺T2Ìÿ›•¤àÎceu[æFüóçe¬71<Žç{ÔŒRVXœSùƒá–þ*P¨_›YS¡¤P]êjëÿuñøX¥¼x¿îdº•¢ˆ§m¶¯£‘°*êòºá5;ð}e”ûóRN/³?=DÝÎ]AgªÎ²ªÿg;U…ìi¾aÕå=§ûf•S¤÷ÇrVÇå1áy$ªø✼¤^©È4#z늓2 “²ôƒ„E¥gWÜ_Q jÝ3ö;é{Šþê:\÷øï‡:S<•øþ˜@Ë©÷4êë渡Åão”‡§ç¸½?­}&M]“0Œ½÷}}aGQ]uM¨þõ–=½^Ëøáõzï'0„C˜+a ƒ|ÚîÅoa+ƒ@ §! +ÖÍ¡ +ò9œÍ(ã×óýY]1ý­Âréåò‹_üWS¹ü'ù«_}ÿËŸÿôÿî§úýß~ÿ—¿úþ¯P¿ïÔí+½Åï«/øǦtß¼î?üÕÏ~õ‹_ê•¿úþôÏþño¿÷·¶Ã-ƒÍóÿãÏ~øéŸ|ÿËïL…ÿôý~(ýßÎÿúËÿÇÌ„o~ñ·ÿøÓoñÝ›¿ã=ø?úû_ýâ?ÿí_~÷³ŸÿõOÿü·þ‹ÿø#zVñÍÃÿó/ñc¾ÿO~ùý¯öý?Øðÿëß}î•ö‹¿ýéÏ~þÓÿé—¿øùØ`ÃRË„:K>úŸýÌ–èÿøÙ_ýêoÆ:2á9 ÓoþÃÿøýÏþúo~õ¹á}ûýùÕ2¾f—mË~öýß½nØgéOñ·=•ÿá«ÿáþ¸üô?üü¯Æ_òŸÿù§ÿé?ÿ“_þì翲Mûúkÿù'ßÿµ óÍ?|õŸþ–¹ü_~òË¿ÿ»¿™ïyÌÊþ_/dÿýþÃWoÿ5íEûáí?ü/öü¿öÓ?¼Ô—ÿíåÿú¿—¿âÙ?ýêkêë>©Hœÿõƒý`&"ÞzŸ¿}ûþ·^;†ß¾ùÛÝoão®Áüï_ôÅÄ=©Aú_­)@ dÃù¥ ®OL“Ë2N){¦áí< ÝŸÞ„–1O_”0ycÁê‡wö+æ«OAP&]eùÏLíñ_k1ïO?æ‘%'SçsþjDTúÏö{wÀ7÷”ñ”lǼt‡ŽÏAŸ5ž¼çЈؼž¡ë£Û8ÛŽ±Ã(xv;‚oùƒ;0Íuü¹…Zq +!œ8ë5Ÿ&h?¾ZÒêþÔßœ“ïß©’í×Æ0hY{.dMyì)5ÑiþÜYú6ƒ±Ì4éÎ{çê 5ê+t»vðgË#ïÌŸª}œpsíæ+’šÖt#‰ioö:âåƒsÄ5µ©Eàþv>}ßéÍa?Öù +àH?šÈsÏÚ\{ŠžCtQwšÇ¾ŸÇ”ÓGy~]Þ1¤ŽzxꔵÆÏ¥Ö±ÔÇ:J ¤±¨í,¯?/ï˜/kq>/oå‡ñ3Y3ÿ9à?öêëW ¨Öñ£¸_˜oÆ Î¡ØQ‚m>{ãÎûÇñ/…^£)Ó‘žÜùš# q¼5õ4%¥-ív +ó”7ÿ CóÃø¹`iŒƒ{ÞcôŒ—à¡ÿ†¡ñìݬö'ÏcŠ +ûMÚÚ=D}>‡Ý|Óx?ÅJ:kÀwÏš]阿#{ÁYæÈ7ç<Ý ¨_s%¨ñ2¯•)©U}âÊ°´y‘Âç¾}Ý»»½~õ;=ãϧ©ÕÏж¾þüþ%¯2ï¼Ï±Û ¨<2ï?‚‘=@™§ûÙëâºßì÷#¦gZd\¦ bÀ8*徦ì×-©ÎGÞõ6O|îC eÔ8¬²À·Ã½¢÷ýØÖË3¯ãÙª5Æo¹¬÷+ƒäPçý:Îgd™UÙºéÍâfút·˜#C|>ÖŽéóçc^z›rªb2ÍuPô8Ù÷\HšìƳ(Ÿñã3¯’ßèÄšë44(¨[Üæê–û3Gäæ‡k*¿ó‘\ÐXoÇÿªïÇ`’Å5" Ã*Ícƒ‹Ýµ9‡ãÈSÉÃæÃלþ?ßÓê»Ç•‡´· U}’Aßà›gÉM]Ìã?G÷1¢æΨ ZÊ8 *×?zï€~,¯KffÚœ®öüñ±Âï×ñ~ŸZz|ôØB¹œm^Š»ýæÃÐröٜڌ§ü†kÙžŸñöçÚÔy×Ú~óÔ¨®ð t##Ês-¯ácR7æNjÁ¶rdžl÷#ìÇãÎóI4º ¶’4žeAü·»ŸS¹|æZ?" M‘õöÇN;ÿØû¼~Ô:?/hÃß<ÐFpU?L§ŠÈüÁ4zÍ ;k}þ^äÕ¦!ÝèJ9GøP¯ñã9ì°“Ûý:ƒæ؇ TülÁḥêá’2B\úñê׫pZöàÛù3©<ýüÜ*Ô@>ýGê°_¶{;70Ó<"Š%ü0~®Þä#߶MÝJýw{ÔÒø í¯Csè”6Â1ÌT5>{9Î9—-å¹—mª°T®çÇ(¤ÉyÄcnùuXï'ñí#âÎÒ½´GÄ™»1ž?ûð&*ÕZçbp•Ç’ã”ÔW›¿QK¦ŸËˆ•Ù_©3MßµÿHCÇ4;ÎüL$=VÃýêP;Ît~< ?snùzÁu>1—{(óV"EÎùÖ½Rû4É5n/½yÑÒt{>’òÏ`kNvº›üvÏ ôûyð¾_—°ßÏkŸ¸Å»Ÿ“ýç«p¤Ýµ½¾·çG>ëíÏ÷q?ûu>öÉ}¾¾àQ·i ¾õÖ-™“}>Ô¯öf]æ[S¨¼¹"gz]Šy§ÔÏÏÊökÞ†tçÏŒ`üVÏ×ῆòÎú™õ{}öÈéy¶•Í®l6öCÑ‘Ÿ”{ÊàãœâDg{{gßÄq†]q™qÓ^Ý€©Ô ‘>± ¶É˽Û¢C5Ò,á<ýtXñ>Lù_£é겡S¤œ§€ûDK!‡&¿.Á'‚à'¢B& À$:Z_&„?ÁúÜQàÈÚä`5ˆ4 бÿ¡°‘-¼ø„N´*çËM°2+]ª–HÔ Ö `ùéhÑÌ #–¾ˆEhIÃÕðO’Áïc Sg«DÔ˜"á +¼.‚03‰ L£“ž  +ü›«Î¶Äx–ïbÂQ^äÄÚùì'øßÆðÖ 1ˆùÝüÛ8Ùp¤å-N´yOE$:Üaȉ>jE¢%Œœì *.Ÿå£=*`ßg& f†Ýˆ r€a›fØÖ¦à®-F¢*U¿àÄ'*:À{û3C²4*„¥Î.g˨}fäˆÆ'©Ãd!j'÷†(Å£‚‚>ÞÏý¸üÙ!I¹âm„dðYºFªÛ‘~9Âqè ÄäŸA±©SàtíÄÀf}”mb“ï˜ËMa1ò&¯ŸX=ö–Í|}ï‰l«ú™ˆ„U%S®ƒÐ‚X¦t9€ð=q"£gm!`>™AYnÐÈcùûz‡¤é)f4M•¢¼9L†þE$œ6I“N-E˜£L·Í1?zýz^ªLá})i^ŠØMtqJÎQ›Y(xK"fªšÜMfÐà×Lг„ ÝÏ_øtë©™21‹ÓäÔŒµ8ÒuZݦ4¬ûûÛkݤgåQÆŽªZû`ØòQìR f%“Wø¥HŸýÁ¨x¬ /ȺH¢À ¾oú Qõí!&¿‰šCDUX#XÕèûbTa‰ ¢ïEC¢ÀåŸN€ vüs.°dL„““È>£!Ñgv^íçÛ¼H4 €2 àGf8Y‘ø©ZHô£5Hó³;8bÈŸÎKˆ#¬µõ=|wm&À±ß×À Gðª)ÍE=ŠÉþö ™ãó*6ðf²ûôu8Ø; °‰¡CÚ‘ݽ ÓË&QŽtÙÒ|À»Æùd°ÔÁ™ žþw×—<DZüùw!~Ãەჺåƒq$*6)N…²¥°ƒ°TÕ÷§wÙVHùi¶Ï-HÔ4ñ,¯,õ‰Ž¢ëA7Èíæ×8•Ì˜¹± ×ì!.¥‰)äÂÝ@o`YšW¦E;í¼N´uL?Ðÿ9ƒI &ÉðúÿÉ.Zs«í'(ñ^wÊánŠ,ü½¸ NYŸz4–„y‡5ÐÖÖ/a¼IÐú Ñ¡¬/£ùºjb_ðªv"JiÎ# ਜ2°uEŒŠý1hg…Qð7ýú ,ácf)Ápø{…Ô”ÂtZAá£æº‹ß !È–eÒé’Þ8SM…_t-VE„­3¶ÞAâÓ™—ŒÁYò´qŸloÀf~yŸO¶(~ïŽy¬èÖê}Õ?6&i/î<´…H²ðŽá>ГºJ;n +%D ‰¦Pþ\Ä—™Ìö¶¬ ·Ç>Ñ¿Ë¢U´(æ¢òô^ñ(i)ÅwŒûDâuqT8ƒ†åaÑ‘mò'Ú" ¿Á¤y™Â‰‡ÍÊU":Š¥Éq5ùa”!#eL,‡_>Ĩ”Gô{ƒ‚úGùQEÞM#y¦ª4¨é—}pÓ@®¬A®R{é2«VXÚ>‘ÒûØZÏZú†àÃz"ñ¨¥Ñ1UÔ2è«x–  ‹r¶^Bl,C'{° "½T^»ª¤~,J”†§…@BF¦8vç°r)ã)Êo¦Q&¶B,§Yò÷&„†0U-…òšð„ù^†$7¥°Ã/zñÊá÷gØ*⊎FÖ˜j}…—Šým›R8’TEzŒL´ÖLb/œM‡²ïpeýû5'ŒÞ¯I‰J.{|P£!Š }l’Róu¥‰BÒÚ°Ë}š&ÌÞúÏ¿Ù  Ÿ5¤Õ)^>b’<[[ZØE¡gMÕÏê„öøUƒâI~דå¶_âžõŽ ›"oIhû·H±¡ùð˜·G”HXgºWóMüÔù¿·R&“òÁôïð~g…ŒtË+cQ@Ö¡ÄŒ*)ò0¯CBL%µìЂ¡ˆaD´1aíeäBF"X´r}jò{K(d¢S´×4dŽoEfˆgUÝxÃò½uØ­F8ómŸ‹ýßë¤j0Dê,‘ g\ø=9îˆg×±tE@1l&©KÑgùÏ46$2ö_”“Õä 幃>À¢XCìuJ£ª$é™Ì66uªÛ#NŒ*Í{CH9 ê><ã}dUCƒ ò{óŸöGð»ùrŽýH>o)¤“Áëª0u*æã(Y—ÃâÕH±ùQ\E›T+eà퇺FKš¶&´$Ö…[/~´%#‰&ao[¥ØkI—É3“ %ÛÂÇ…­0íöÒè@9ài::¦õ @ RTµø0Ühyð BnX„oÔ7EÒ^ûæû6Uu½\k7 zTÈ%ÃÛ¶’\§\FU#½cÓ ä¾—9müÊwʤÊ<ìªnöæîuŸ¨›±5d~"vÒ²iÇ¡Çùå$tœÉm£Ê%Ð-ʧ@²Ff ?dâe¢® þæ"q­±m{=Ï‹ôu¶nÈgmÂM†`Zƒ÷)&¦µ(ød‘ÁÂd|<øYS\ªú¢Öà‚LL‹'І€+NAT$¦Ö|` ¾XãJ‡6ò/ ¸÷Ó€ ”C” +I÷ƺêÎñk(( +¢ÊL*ãA¢BVœ¨ý\ɘØeŠßÛr†²ÄW)¢Ü†Ò€PZAsã,k>ÇcR Šlã µ–„QÒ®äKÂÝ\ùD¼UUhV +ì’‰quù{ª +`²˜ Ã"ßTL°T’ZܱK˜"†IW†!Q+Ÿ•ï÷2Öù„Ìlþ:ùBüõSi²¢èumÖá·Údp—‘\[›-ª1ø­Jl&r:°µŠiRò&#wvœ3©<›ê‰ ]"ô_ ¬3•TDü^E•žµü¤1ó†Êf3|WŒ¡ÊúEË÷ƒû Å +ÁàV7¬h²¾ UVãašœ1MX¾ÌÑIè(¢À §eÙ¤¶þ[“ò ì}™°ÎLÀ5ÅæéÉÆ6;¾³•û74C®­ªÐë¨ÒÕàiBbSÄstFq<2ßùRUöbLQh¶HÂe²*lý£*7@S“µû(=…µÀ݈À‘Q„áÛõ´??„§{[Ô¥ºHžGLD°á‰p´.:n©.ÂÊŽbRF˜#sümynt‡ ¿Â}φ ò²oñ$¿ºÄ'kŒÐE@›$ùÁDIL\QÞ™dÓèxe"¡6îêë²aQÉ +0ˆŠÌBÕp°$O/Ìí°(,ƒ7L'yU²O\/ߟȄúm#hJð³d°W¦eØŽ/zù×RyŒ-Ëèqi +:Lò7*Ï©¦Ò‘e XV YÈ*U×;T¶ãl$ +¦àÏ_Ž;2!LUˆm‰¬u-,¿¸˜Â4flpJÉÈš|Ô’¥¢Bþ.,NÏR&®ªi2„nQöš*"˜>)k] f±¬ùà9`–!0•š›~aŠO4Ö­5„üC ®ÈÌ„5> ®S- +7P‚;rbÂà3xN«ÌW”ÚUÛ(™à¿êÙ¢do¾ë¡Ì-©õxƒ¡Ì5:‘´i¿œªeÊÒg`QДBçA7}—â.]ÖÚêRã`}ˆBë°Öd–tË›~€ÛGtºB­¨jÏ°e6&Š*r³™Òœ ô›)+²ÚÛ«´|¢J¼†\ró}‘Þ‹DÃ&Š4Qù»|~FQ©dÌê +ñóºò [°)”M²a¶R† ¿øÙV¶-Ë!±»­´ó×_Kù¨¦)×Ûòs|¢*•6*”nÕ ¤?~¸ ꔿÆñSò¿ŠµN¢çRÑ/hþ.RYºH=C¢,¨àOJ¿Œ½®`ÕhÂ/Ãù[£>IÏZ²™œK¦¬#°¤Ñé_ I3È%’ȸâYSžsƒX«%Kíd)ù/§6;Œ˜†ÂæºÒÌòð‚(ó]yý¹Âȹ„L¯‘/m/¨‹ f“§8Y$†åÒà³²Ü?_gR¼·Ð¢úæ¨kWœ¡Q!:•Ócïj…’ò4C'Ç#Y2ÖF˜iû&ÿ2ÕÇ3蔊Œ +áMÞìP =¼°]z9×mVD/zIee!ÅâðA’lß”¥ßë6•0èŠJÏbsiñ"ùoÃw‹’Mz¯14GœLMǸO[ÂU¼H’m>BsüÒ$]ÿõº²|è‚Îk“4^ƒ í´lY£ÉL²`ŸÜ~–BÉS¸^2» Ɇ%4¦x!Ž„±ôÅx[€FË` ”a©ÂââR‚…%s‹¿L“ÂLœG\\<šÌd¶dÙ”eÈî¶àD~ìãë@kó$­“ö°±¹I2Ù¦¨OôðWPTZ¦ Ø ,‘/·¤ñ-šùDÚYÓwõ$yÜoCÍ%~™ÂƺÝ?´BY;O뇞/zG4€.qÁG¼5G!4¤‹ÀŸ-÷^ˆß[²xûc‹g)²bR¸ÁZ7]BYe¬£ŸØT–Ô½J‘ÒuÖºµŽ“=mû½jhÁ>ªºŽà[Ì&-%cÚ´¨˜L܃úŸ*†È¢¦SW%Õ”Á_ +3#n$t.ÈÈ·¹7RæHË|JÇÆD¸Ñæ©Ó4YÓÇ‚6†Cd‹r $Ûу‚T­‹ø¤­JubKǨ­RhŒ?I|l­ª ⊾#Èk¯ŽÌäѤúƳmšaÒ»,[¦­»ùØ~š ?¬¾Q¹¥ˆ8¯Í!*Ó¢Ÿ­oTdsK¶,°åѶ©O æîè:UĈ6_6z¶¨6Žjms£LÔúÏ’ž³± Ìq¢.oŽ4¸0ÇIJjÿ€IRҌӤwÎæ´œE¶)‹ÖÚ×6ý'CØL` múØH •É<Û’I½¨Õõ ,‹´ª!üƒ6ªByh)ÕÉ’"ÌRB¼=V!Ë_V…Ø T{a¯³>PÿSÀÇvd^²å +úÄuå8ÏOô‰¦¨˜Fæ a;²“˜-«Ìmæ§Ûúë%¢nŽ²¡ìyrÀQ6,#GÙ(‚!"÷Ç&±23:%Ü ‚\Éݤh¾ÓÏQe¾6Ê@S2ÌjÕ(™œêO}£e€&|œH”—lCwŒ G‡#ks‘,ËÑxØ—ÈŽCÀ“Ê4ñ*)TñêjA¤¬;%+™s#2®ù§É1eTÑ‚o?!Ñ”ÕðY鎵œ©Çƒƒ§ÅøxÁíBÕµÛÈâŠP|™BÁG¬ÖRIPPÔqd“ˆu·‡ÉH'n9 ;$K[¯Y g¥¥BÇ ¯æÑMÍÌ`ëJ›êÉ +V²5‘¦Â4JñÀ/P‰æý‘¾0,Ê…!˜F*,!ª^àE¾W +t2l%¤ IgÊ)ø¬ð‹ Ó¥1 ÷ê™M@£ ¶ßdBþÞ$5Â{©tÙQÖZÏw=2“ˆ©ÎE´ E¢Bry<ÂLYénKC/f•6”)œ8Ì‘˜ ˆ +²ÌÙh²º®±Ý¨>°ý¦^D6,ù:¡9¦!Pï ‘ÎϘ¬3{^¦a5âQj‰W¢‘†WdñƒÍèØpCÎ…2]7LœÓÚG©8T(A_±½‘*OhmCrN1^@F/B“Žxzݦ†{«9É¢lBÅOA¢­ÓiVT‘XÄøm²O­¯uñY‡‘½'š#ñ'¥LâV»@!w¤HïâDC}— Õõ¨ª²À‘ m¸ï¢#È"CŒo¢uHf*“JñGÖ°4’“¯c2ÜËÃÅþ³†JQ³¢,nÙª :†"Õ›AÛ³vØ¢SÖU¶Ÿ‚DÄ$¾tJMe¾ M&ŒI*õØQ}Ç#ãëéP[mã÷”gq ‹4[*Û¿‹Ñ'ZI4ƒ¬fæ‡d;±Î¦îç²!Ñ]~LCNºd˜ºcÒ³L‘ZÛ4Ñv$gú?f”]ÀA)gMžÐLq¼Z’UÊ æ¾ áYädÙžBí!Ñ–HWñûèò'm +¢hÔ  (×]Øta"Õ–Ç“¯• J°nø°pÞý^þsÄ°ú†÷Iä qmXˆÆlüIÙgF£ÔK0Kd“‹ ƒà÷§/ý V`¬DÛì©æÀ$„çÉþ¶xÖ‘‡V”;ÿR¥'>+ñ‚ˆÃðg 2ŒUq\¨Ê#@1`$3Ñ-…¾fØ-\%;F·ý:XMõc*þ³~h›¥¯í(]LðQÛ¤ ™æˆÌA|Xm’òŒ·+§ ¤À´œègèm¦àtUòýÔ¿Ü•ê„%†+£Û:å³oÉšJélªn8âYQ–¯*"ië‚É#ü÷²T@bVÅš¬h-ÄTéâIÇ àøZK¤Z2±àªQâiÁ 6Éi̳ñ‘2æó4,C²¢`JS鲩ˬJ¿´‰ëRÓïÆyÙ©säîC²¢Qä_Tl0Õ¢V‹ø¬ßùŒÏ@&Añ@# ª+qºšx™¾‘Rá·fFš`ðL2á%àGÑdåÚJ[’§f1:¤ºJI‰Ü%@h²\ÁF¿oG&óz0™„Š£Rf’¡ˆÓàHÓˆX'ŠS}eG²JÄ0y…i‰g…Îך›²Öüã²pv¦ óÜ:’¬´âŒ9$AmY±‡÷lÑïEµ./Ï–㓤y‰‚KÆdῶ\ƒÈµÕ…SɽÈÓ´ Rù¶Œ3ßxõ‰Î:·Ór¡d~[.ž ¼ÖŒBþà6hšäS‚®H`^R)lÃáýq=B㊫®ÛU*;qÔ5b*ƒŸ'[¿·äd…IŽDE–WðH”?€A ç¥h„… `%‡²¶,[€r1Ê D¢êP¢¸”«zlÊ×¤Ö +œ&‡Rž˜ÉÄÏu¶€ç”ù¿×ÉAËkÊ|íðŠ"¾”§ˆ4¼_U°¼AšN™Ã¶o`â ë8Q‡PÄ 'EŸŠÓN4zÅ.™7Å „ÜHªä3`”ŸÄ½q>‘ò?-Bb +¹ZxŽ¢ä7•êÐ9Y#äk[Fä«Sˆ´38ÌÖ)ןù*9¦®p_&UÉ€¼Q Ù)Ì‘k%L󫛉(+©GJ^R@ŠB=Š¸¢Ð×ö³hµ(Ýí'‡ÖÕÏFdX #3Ü,òF£ gR梥9d~©2{Ÿ6&?_èí“™YË£bT…²á„pàv¡LJ•éFÜZ$½J•ü8™ã j›6VdêðóJù” ›ÙSr15ÒÃ/`Ræ!Îü ¤ÿ$ÚÞò dá*,H^LK)Ë¢"€_IyãÆÚwÈA©Š»Ž ƒ!¾YžâÒŽ,*‰\̦¦Ò<©ž8ÌÞh3«Ò:‚lš¤¿,Q]Âlº¤Eš³¤FR7Ú2[âÕïqë?+¢BújŒd1ÏIô/šú›+®~`–ïIæƒR²3ýŸùÍvÖKÊ"Ó7?pÑ0ƒ (¿ ãùH²Fžê¿˜ìñÉ G{™äoÃå2å""ÍK^ÉYÅ ò ù¹ç °n#ª ˜±Q·kËü{fH0§ÒÉG¢)”.ž`Y\Fhš@• ¿³ÉoRÒ¢]êñÄ Ùk ^Ô©a-‰ƒ²À%"©UîCÒIRqûšÈª¶^tº„É6Æ<}ÜßK ó?I[2?OŽit ¯?VÖ®)Õ¤EU`è0“ÙÌ”´DM#ô[Î5]‚ZJ‡F§Þˆ4 ©ëÓdPÙÞ.«‡QËðÂ#êУш¢+%ßéñS7€6Õò3UâCÙ ˆvÞ¬Lǯµñª„OT$ÓR<oA÷Í~N¤ #Sdû~Û½L$&ñ† +~ò®€*›/(”ÞÊûHÀ#“0™BfÕˆg×å"Õ½¦¦–àÈÙQò4ŸÕ¾©š¬ªùp^åu—Ϧ s¹ë“eYŠ&+Õ5Ý¡²m}w‡i­'Fî(G&¶ã X•äÀg¾LŠDÙ¹ž‹;Ú' +¾ã—Ø°/o1Ðùü ¤ têTG-éïY˜Œ¸#* c¬]¹Ž,ÇòMYþJU{Xï¡P³ S¦áëõš7D‹3¼‘^Ö#òXÇg'F˜O“ÅJ¢cŽ «² ]ЇDqKó/Tð‰¢¤ݹ¤* š8Prçóo[/Un‹VB´pºt`RÊ¡!¯5âÞGY3* + tÐ +­»¢Èîò0wuê4ÀG3(KíÎÕ½r HÚÝqhO&÷”0…0tl2Ê>¤#›¥;Òó|Qj–®kä¬Ú(ËX7#Kk̵k]Ó°vOòÀ‘O´tjVa gÖ)¨d¼˜–¶.Ëp4Ù Nµè]üNaèh4«¾`Ýþ£‘éÓ •‘Õ†‡²íˆ¸†W˜Z”I΋8¨Ÿ™!:q!Qvá4µ_ú.¿³ºQKÈKEt¡[GÆ×wx¬VŸÑck¾1cÈ$ƒ{"Ë›Úàßhtq9&+ZëëÈשox»øŽøýú®jwûô]Ôxܢ漙!]ĉdÑ­Rõk4:D–ÑJG”ÏØ™lèCðC‘¼¼›y4ÈÓ¢­“4~qQV»áiÿt¸þÓ/ØN$º³˜ðoA¯éœTŸ_–^¿{s;8{ûöêõsŸXºº¹{þ92}þüìþêr‡Sw€¼£FÁfóàÿïùýôêŽÂÿoñ-üG þå Þï¨ÊNwgu¤ì\⣣/’¶Í lógóV¥XIvDÕô«J×ä·dìù„¦GgsˆÏ×<‡wŽ¾à÷¤˜ tƒj,0ñÂ{ ;ËP Å´L,p~O§@ܶЛÂïQ±LÓ ñËzÑeÇÌ´´ŸMüÑæ+À‘ا í9¬÷Î3³ðÀ nÁR<ÙA÷&¬ˆ±ÓÅÙâM–ºÆ?›ÉûwúÚ6¶YuŒMrç‹1,0,1šº‰=¼AŒ"–Fk‹1 r,êþÅ9i:ˆ >††#ÌÒ „-,PÒ1‡7üÝœ&Λ€B„ó†î¼›À Xl^²Ž…{±Þ0kƒüŒOUÙéÿ¿=Ù³OxNÌ›» m¬‹q|™Ã©¼]¤~Èf¶ÿúÇDþ}±¤h=ûá x=»BDz¿ úˆ+’>|ÖòoÏûpÔ5ñƒl’ål×£n~Ö‡ pñ4xÇw‘‹ÿ9‹Äк°»9šþô‹_–\²]~aÉ•{÷ÿ ÑÂ(IM÷ñ™Íÿ)¾¢çd@/ºO6ð2 gBþpߟͫ{Œ€ø £€âµÔ'ðßÌïzbófÂÈs|…—²E2Fƒ?»h6ö‘„üüP ™ k[–Ö‡C` +ã¸ë×}M‚3iV6o¯~8uGÜŒ¹þÌò>™„õ¢l +©ÿ/wïì‹MY…¨¾lC§ù›§ùY œlc„‰ ñùVl9MßšâDwçBì¾ ã?7:Ÿ‰q-L¯ä¬Ÿ@Dž­ÁŸuÄÉ㓽ÌDßÓçfûÉâp+÷“Oº Óúè“0d1ãÃOB܃ÖÛGŸ„Õ XÚøÑ'aH ­ÿIèÈ'2O£þ࣬×Ô؇õé|ÿ/˃ÿÇhJÓ>ÄSêŽ÷òÿ¨â½ \GçÞQ'Zé¨m7þ¢óÑ_˜xO‰O]óYâƈ­6æ¨âá{Í“Úݳ«Œÿ¯üÓGgoà«¿€…‚¹Ö¿HW®¾¾»¸ÕK;‹Þ+Ä!Éÿwùú£ ݪŠ…¿½ý?ÿ†~Aÿëìè0;Õæˆ} ò¹UÎÞževЭ¥~±hþWº7³v2×µ*s»¶´ËsÝ¥ s³vê´ÎÍÊVÇ­¦áͼÞU¶uê4S…a,Ó‰±–RjÞÄð¦éL'SåÛGåÑuapÅÚvëܨŸf»wñL7Î’,aVÌÒ8Û9)ïrÃ'¬{m4“Þ(l7Kƒ³\çH-ŽóëòèYmþÆjœªÅ…ݾÔÊÉì j5µÒª;ãòôYº¼ØQJñl_q'0ùˆQWrÃBç¼>™\[Í£h¦­¸£T±gÖ&!§fTg•ù‹Úê›æÙoò“JyË÷£™Nuú¼}ú}uõ¶2Õ<ü.7x¢Õ#™ŽU[éåy2ךµ â¥XCÍwìÊĬ̪ïÒÜy„u£¬Ï ôÊ&f”ç¬~h”ª·l/ßW§¯•Ò*̺ûz-hÔ㙎׿ʷO£v=jÖ²õã|ûÜ*ĬNX«¥2Ý\ý°{ðÞ¨äú¬zR«Û±\Ìj'X/Éz‰ÌÀ®ŸZ3£z¬W÷´ZØl%3%?Vóc§zC%Y_w1§ÐÊ°bÉì0κq§“kœZçJaœÌ FóQ,÷8–ÛK{Á´§f»fqdÁZÍ°VN³Ö^"û0lî)nÄjÀÏcNOw—ZašÊÃzvÁ¬$2ݽD.f6‚ju/é¦2³|¨»vå8Ó<Ï6Ofgß4æwQ³œÎ³­ o|—ëžEÌzDWô5–ëT)-Ãv7l4ìÊAyxëu/ÔL¯2¼±ëñ\/l7Òù!kžgZ–7)uCšT=»²pÏ2;£~n6.ŒÊÊ©î8jTÔâ°8¼m}íMŸçú°Ç±lÓôéûž¿Oéü ÐÒ>ùmuñ­Y;IäGÀà §wZQ»µ›°Gª7ƒ¿Jæ'ÉÜ8¨×öÒ%˜vÌæŸZµEª0RK‹Bÿ¶yøýø⧓WJ—ú±l3×½¨,ßv/W]}c¶/ÍÆ©S;ºyý×å³?&ÜqØjO‡Wlÿd6/Íæyª0†•LfzJnƒõÊv¢µÚIÖØIä÷’Åd¶¯—°Újõ Yœ¦Š³Ta–ö–jyçE+˜õÓ(ëÂcQ»µ[a£¶—*Æí¦^¦²¨UkÕ¸ÑLÀ_i5`$5;î/^·f/€¯v£Ù`º´Ï?³VM•VNãBõŽ”Â2׸)´®Cz-ɺéÜÐ(-Òøü¹S?óOSùÁƒ¨ý«œ™©¹«bë2•í=Š°R[ÇÑ̃ ¾ޱʇvõH϶cY˜F:ÛßO»A­ +sú{J#ju€KSîÌj_¥ ÈVŠ[]X„DAÉ«“WõÙÛêä «Ÿ´¦/~üóÿ|öúŸþn;žï]/Ó>ûÎn†Í?Dƒ¸Ó Õ}­2›pŠýËBçÌ(ŽœâôâÅŸ‹ƒ‹ÝØQ\Ó5WßÕ¾1¼‰V膔’’íz½KÍ[¤ŠÓDafÔÏÜÁÓÆòM®u–b-·sÚ:|S_õ…Y?‚ýeíËòäÙáÓ?”F·Ja`—¥ñswúʬŸ%³cT°DJ~g­T> –€Ç@¸iÞ2•Ÿ›åcµ87K‡Ja6«Za`U§‰|/íMËÓ—Ó'ÿº¸ÿw£ÛŸ’^߬L‹ƒËüè:;º1šgåù»ñåï:‡_/¯j­^EYøÇÜ‚ìÊönÔʑѸÒpØI¦~œÎ €{÷ÕʾZUÝ9ügD¯=Ž°@º¤ÆÀÉ«¹ïtâÅEÚ;ÖʧÅÉk¥2ßS=Xç|ÿIØjí)•GñbØlÂÎƬz‚µALÁ?“™nÜl“î^¼°ŸtA,$¬¶Û:ËÔwây`* oE2;±Üv<±Zª·Èuïœæ k\yýçn÷IÌîZ¥H6©ÜÄ(k¥£|ïIcù^+ÍD¬dÄ h£´¢¦‰gwÌ#¤¸i§cx‡Š{w »g•OòõÙê|/]ÜIWz[+ÎàcaCÓå“T~¨åûélo+Ê"ZEÍXõ0Ü%«–à„^ü [ü÷;‰Lû¸6»K‚Üs—‰ü$]œõ*Hþ(_­4K¹ãxDÊÈk?ùІª;q*‡™ÖeÚïéÕ QÓ‹“bç¼4¼NæNóP«,ôÚ¡â-ÌÊa*7ŒµúäÉðòo|SÞFWf}•í^†·ðO4¯;ë,^»ªÐ*Jn’k_é¥Õ…Ã8 ™ˆÓŠú³jA³u†Ní<ß½c ÐÚ‡éÂØ(ÍAa±Æ*Ó>uš'…Á]yñ¶0y¦T•Ù‹Êôi¦sê.¬ÎQóèÛ£ç^ýN+ÏCv3dÖà»@¢>Nä÷µJ±{Û9þ©±ü¦Ð»ñn¸3; ZaA­º«”BzD"Ó׊SøRÐzñÂ$å­ÔÒ‰Ó¸©Ì¿$Èök“¹þ]@¯ï¦Ë{éªá-´â9*ÛUò3i\“ÆN4Õj­L¹»‰B*Ó׋³°ÞØMzûéJX«ƒy´öÓ^<Ówê'!»Ô›!½6@'Â"§²PˆÀ6a§ «¸s«~RD:2žS i%`ì˜Ó~HØm=?V²ƒÇ‘LP©’tF°Œp`AôÅì¾SZÖ‡7åÞ9°ô^ºÔµ1›ªwéÜW¦¯ÜÁMÔF¹·Ÿ,ì&rCÆV„™ÅÉpõ6ß½N—‚ø]¹˜]ùlWAQæzO€ž*Εâ4ß:ÏTçq Q[+/Ö¥;¼¯ŒîXý@ÉõJ‹ñåoSùqÌi%2=³ºtê§ ð@õW±wÓX¾L¹£­´›ÈÂàGÙƱ<¸Ø™=ÿ—¿ü¯Wßý%á …žÙ<ÉtÙ^¶¾îc7á+àˆÅžê°Ömaøº0|•ï=Ëvoàìdʳbÿ:Ìú ¬#N_)Î +Ý›\ï–u®³—×oþýøü»Ó0ÊËòäEçä‡ÎÙO³‹£ÛúèÉïþùð7»FT­â­”ÒA¶y^l_°Ú!¢½Ûm–çpˆ¢NþÄ,/}Nkqú bøœ^°VqPë1{ÊNÝöe¶uô0‘ÛKUâl’é<+_úÏXó"]œ¢€­ ¤ +»ñÌ~*2ª†;wÊ+§| d†Q³1 Y‚i7f5A÷í¥JÛ 7¨ÖƒJ X+át#Vs7]ØŠg¾ÜMoÇst%¢ƒí»¦r0U€dÝÇÉÜžV‚éÓæ;·víD÷–°ìp¢-ÃâG­È™­hf/áî%¼˜Ñѳӈ +¨šf½íû*¨? +g@mé aÀXn'YLØsºaµ +š(]˜ß²Ö%°ŠU9Ò €±ûÛQça@›ð«á®â>N¹“%»¼šÿ¼Ÿ™9̲†â.s'̓o[Ëo\eJƒîì®±xaÖõòRÐ^™zÝsÀœ“ï"V'ß<ÌÔb¬#X€üëÀ“ëËW¤ãVËÞÀ.[ ”¼…7zÚ\½™]ÿôßþÇåóߦ¼‘Ó:Éöo³ý;0Xû¼0|bÔŽ¿ ŠÙÐ)îàЗS?ÏŸ¼pûwÇ·X<ùc˜ ¢l¨—WÙæi¾s‘ƒÐ¹¬OŸŽ¿)ö.ùYYš`ÕùÛöÉùÁMuvÿÍþKãèÕVÚdÀiê\b‰ +œ  žÀ¿ï+¥4ë°Æ kƒœ7AÚ¸K£|À6b?]ÜOBj`ƒU9±Ù>xŸmÃ.Ÿ¤r Þ§qPO ©+Ç6 îâ2Á†ÛÉÂ㨽—Ì…T/f5 wš0P9dåU®~–`Q Pew7Y +ju`0)û©Ò~º”p: CF}7™ß +Yq«HU¶¢…„ ˜ÕÏà°M@¯‚ØW‹Ó¨Ó9›åØÀfðsÝÚåƒÞ*å H¡T æ3;ºöS^(]-ù(lÚŽ‚‘ñŸ0‡¨Ùþ1ݨ'·›ÊõÞkÞ\/ÁÚ.•ü0¤”Wa’Z~”Êt¶’8ƒzinWæ œ÷R4 AH§°ÙÖMûè§ÁåÜÁ§<ÏWg‡×¿_~êϪÚõÃ|÷¬6¹ë½Ë÷/~Û§ùÖ1`¼úôeeò¦2y[¼pǯôê +Z¦´è¿ Ùu@ãFé wòãôæ÷‹ûžÝÿœƒ±9¹úMyþ*’%Ý™^YUf¯«oÁXËv®aJ¹ö)hö¨ÓÑg–—•é‹ÆòëÙõGssµ|¬Š(ÏÚ¡Ó&¼-ô®õòhOXXÖ8دºÓêìåìî¥ùKwò´}ü.Uïk5Õû%²§—#v Ы€Å–×N¢È¼Y}òTõ¦;©è&Í;)äÔN“foVˆZ=å~é¥C›3@ø0•Ý~/ì¤Ja» ¯€ÿŒÛƒ„3ÙWjûjÏN<0Àìµé΋ÍóöüM¦~±º!Ð}vO+ε PkÊnÈßM@i&8ú…WD´š’…ÔFDkD P‹`ƒ‘8 ã3}»v –8¡Ð¹tªˆBcvËðæ…άa2ÓßO¹!µ’túûÉpÈ1uûI$aÒé4ÛŠå@ù†´ +üÙK‚,-Á‹ÒÙuZv˜«ï©åÇQö8dm‡31«ê8Û8µ»“vaÑ’™ÖVÄø‡­8 X€pì2ÈÏ“lë<Û>WKSPñZnP¬O¿õÆ· õ +½£~¤”F€–‹Ý•]…i·ÑW0{¥&px3Ê«d~´z©ÂÔ©WGOs½ó€YѼ±U;(ô.Yë„uΕê*UšWf/OÿTž¿óÊhœêÕÃ|ÿÆ›¼Ìï³Ý;0·áù(ëw‡Ä3ÝlçÊ¿Îôž©õs½~x;`ÔSùkžh¥)Ë€uA¦ó3oðBs'£šm`Á¹¥¼P*`ó‚ÅêõŸ4o£™NÄi&r½D®`ï¶Så]¥*[=¬îp #lŒoT–`t‡ÓÕpºœr:°;[a+¬U o•ï=϶îœÊ±QXðgÆ<ŽeFì¯BFÄaw@/,A¶ì¥]°gÀ4Æ€5ÏÔÀ ›ˆÙÃÁ©[0É\?_Y&ÖWu'ž–KgA®ŽRYÀ ý4äëG1£úånj7ž ¤AâÒžšå  p í«Bç:æ´A‡f#Q)LA㫃 V+ð +J[ûaÈ ·›pv?ņ('S 'ÛAÅÝŠewÁÄHº ÜB`ÀÚ-°Ž‹w/ÿëÇñì?ì*;±¬ž›-·R™:N&åFÍúãˆþ[¡Ý°b Œ_8&0Ãd® ¦VPóÀÒqZh{Ë|ç,–Á#“ë\ªÕžVLåÛfi”käÚ'Õù«|ÿ6(—xax>Y•ÃêôUmö"Ó< šµ=ÕM‡iwÉ´Á" pQûäçØîU²8¬Lî˳—ÈEÙžÓ<¯¯~è^þ!;¾W+K ªÞ,åƒN#Ó¹v§ïò“wYàœÊ*–éEì6HKoü4`Àö@) ¹”r¶—öÔü¸Ð½鴓̱æiõàëâø9œÌ%ø¿‘ïœçú7!Öyœ,÷‚ôƒÓke–jq{ˆ>üXñ6Ìí„ÕÔs8°ã°; "Âf –4¬ÃÑ+ùi‚L{Í"ðvÚ1»PJvR0˜óÀT[±ü^ªÈ5Ý8¶R¦‡Vyº8¤–ÁðßO“N3jV÷™ly\lŸ€¦¨µ}¥º+ÐÒrã°VþÕn\aËFÁ´7š»ÀJ)¢WÊQ±ÿ,l5Fí}µd•æ sF¦2½Ça¶/Õê?î%GÍ€âÁ«æ†¡´·vàPÒe¥Í¸Ô»Á»È? Û_4”ŸvSsÇ™Æá.úúÚNeR ÃÊã`r+¤Fõ*«¬\¬W åöa@+ÃÄ¢Fñž;¨n*Ó°J“ QýŸµ0=ø—\ë̬Dr¾{¬–&qÖ²§•ƒf@Zëðû\÷Z+L+³§aV‡uä¹/†”zÒî$3%?pªKü§;™]ÿÚª-F2»©â.l„Ó¶jGÝãïðxí‹›oÿ›m5T=%Û×A‚¹ÓL}¬Uèž—/®¾ù+ëží*å°ÙÐð–åñ‹å³¿tÏVJˆšJ³ç±|?Â:ü¬ÆIqø´}òëÊÁÛæèöõÿ±<òeÈ ©5@¼`Ç+ºwfõÔðìûX~0€X÷€ÌÝîµÛ¿Mj&O~«×F_î'àÓîD¨]?XÞý~þìO¬{QjŸ=ûmÐ*ÿj?õ0¨ëÅ1ÌôK¥ + Ž‰^c°ó ¨nÇœ`ºäv1p»RŽª”F×(+¶b€ÄšZažk\©Ö4ÀQ„¿ +è_í«B&è\ ÝÖšåÃD¶˶vR FQžD´ +X[ !á¤'í–×>nL¯wT—»Å†`.í L•cZÍ,ŒKs`#?ÈÆ`ÐÑA¥„Š _Ñ¿Ó½ñ£ˆ ºŽƒ’pLpp”Yèå ø—-ØÊD~?Q€‘÷Så}¥8íôÅŸöuo?æèN{?™{v@{¢>5º©ü¤1áõÏw†fW¬|3a”…íýtÙð¦…ö˜íÀ “Ð4VJ€07Î7½I¶±‘¾›(\Ñ +}XXЛFa\=ToVæ äÓ™v’5áÌFì¦Råëv΀y‚vÕ(‚Æ<2Ks°Ytoª†é|otðüùÏÿ©8¼ ‚™Ÿ€!Shž{Ã'­£¯­&ÊÐæVûhÇ,›åikñ˜- †pqj5Ž.¿ÿã÷¿O¯¾ÛŠçl°.ºÐw•’U½t‡/ ½'ÕÕ7ÉÊQÒ[— Ð÷cäúð^@é€J£»°]{u@ +™Àºõx¦¡–ÆJi¢º£ùáë?ÿ·ÿ[,ÓxÔ”l»6¹ÑŠ£€ZÞŠ¸§tR]Z]“Ê´v=íí¾E}תö®ÞüÓ‰åºbÙT¶†m:3Hg‡ þÂz½6º~öí¿Í®ür_{P÷âN +æÇ6QK0•¦ +CÀu€£BF,µÝ˜œ²;ÅæY¥e×æ;Šääà €±ZÙÒʇҥ@²v"X‹ ÑŠ…Û:Y„_±ͽD¾(j·‚Zu_ñ„uÛy­ Õÿ(ò8¨FÔ’^\™¥ÒðÌN”E•RL×°µe)8°Çy»”ÊOáH>ÛÛ Ð¿e-?TÀÏ@$îÄ‘÷@/§ŠJ®£d»¹Úª=•í_«¥Y2ÛKâÂÖà‹Òù>0•S[å»çNŒµ£taTÝUÏòÌ(ÍìÚæ²›W†Wfý`W¯ï)5œ`΃¶¥¹¯yº7qjKXç\ÿ:éNôÊ"ÊZ»Àêʼn7¼ëž~×8ý¡0}ž®,Cˆ»ãZiO+é•%u½|è´nRÞª<~Uݸ +åD¦³j°na½1Áúë€ÐK¢æW!e+jª0ÏÉ‹ÆÁëÆ즳|H» \²585Æ£ˆ¶Ÿ.ÂR;7¬qnÕŽc¬ –8˜?µ˜ä!T§–ÑI}úºuúý®Žˆ  »1w¢öNÌÈ4ŠÃk»~u:q«JƒpÈl…,0a#à\Gì^Œ ¬ÊiyüNñæ;‰|X+ª‚ +0íNÔ雵óÚÁ7 `Ó“™^0ín…@/çSz5ªV´l¿9¾ ª°­$ ë4ChÖ5€·#NWs§pšÔlÿÁ^j/šM[í¸Yý¸ Ð1žÝO‚Œ­ÆàÆYÒ¨äk RÀlÌlG³ð`’ Ƚ4(§ö0¨íÄ2Û14œ£ HY/W? ©Õ`Òs«ÇBÀÉHßnµÁ`|vö’Å]ô¬ÂU“ÎÀòV€U¢F+˜.ÀZý¨º¬ãVLQÐ5ËLý0îvíæZ‡`‚¬Näljü\)ÕfßtÏ~[]½WŠc³4ÑuÀÃ#9èdz#ÐÚ{)WË‚±s±ëNuž­dšçµÅ»ÒäeÚ›m§=½º2ª+‡p®ó“¯Ë‡?¦¯á¯â™âÎÕ2|õ0`zxìòªwüSaðT).¢`D«%XR˜ÛVØFwvb×ÎíÚYš;ˆàc¹ ^VŠC³¾L¹Ce×2›‚0Œ™xÓY6ýˆUÃÐiœg{wª·L`¤¬­‘YAW|Xa¦·rš—Zq¡gp^@ŸF”Â~ÐKr ¸Ëéž„•ÑK ѪÞîin7–ÝŽdöR¥ˆÓK—Fù8ž™Ä2½tĪÇq»zÖ + 8˜½`nkÞ2Îz†;w»·€ú±Œb·ÓF-œ,FÕªS>Nå&©Ì0fµva\jaªVõ¨Ô»u[çd!®5‚I÷·“÷0ü  Œíü `€²Ûâ!Ë4ëï&½Ø)÷AÐÛ$ TJ=¢7꣛l}§Os: Q>&Üå€CÂhäe{ôúaÐz´öâù¯‚æ?îiÉ"z=¨V` ö“Eô9ä§ðuaW1*GÔ†áÎBZô,(20„òQ©—Î`¬¼h-îS…:Þá‡Íoü¶µú1×['½>½w`JHÝ’]^ä[gùÎ]9¿—ò˜·lÏ_sZ¨¯ƒ7œ{O¼Ñó(JÎj£}µ8Û,Ó€ZßIº*˜®zùQHÃ?`Ô÷AÀ¦K°tqôçŒ@WÕúƒ}¬r6 ÃCz;dt2ÍëÊø­7¸·Ê™ê‘’ï?ŽÙ`ŸîkÝh…S™;×1½š¶jÌ›n‡ÍGûÚ~¬ÀʘG¦°Û˜Ù°‘d½=ô€í…yÒîk™ °Óß=ŽþýÃÈ^,Œñ«]í¶Ò;1ÀxÁ$¶Ÿ®$3ÃíDñ« £æ©ì(•©ùEu°ó8¶Ü>O9ݯƃ} 5ª76Ðó -3hƇ!;¬TJƒpôv“E0?á ›í§ÁXýXe +»r;™l']€`À.a5R¥Ç‘lH©djgîðIЀ‡+FùÀ,¯`˜§ â™í´ ouzoU—£žÌöã 5TŒG¥ —U‹Í‹\ûl'é$X]õFVc¥×1T”iœ¸½ëlï6YÀJ:×[ÕÝta/]ˆ•Ì­Æ¹Z?Mf&gç?ßÿ!`Õ÷Õ +z²ƒBë\/šêìñ/êΟ@«MÕ;Ê6/tw¦{0Ïãxvhºó§ïþzûþ/1ÖÞŽ·“`‰×|Fkx«çz ½b—¦{š÷UÔÜJ¬Êaiøäj¡³›NXt¾g¢v?ŸÒÎI¢_ÈK;°ú·âù€V9ƒŸ­f*ÇZ~1:€ÃÓN7ÌiHUŒâ<…6f;l´w”zÔôæo³•ñ~ÒIgZûªÒŠ»qk+¢ÃqVœVιõƒ‡-€_)…R€¦r¨¶#Îv,,‘mœÆQ„ZI³c–¶·qa–µúÛ©»úN ‘ùW»z0Q +¢)Å4øg%áôYã2‘õf<3 ›í=¥¾ªì«5`ÐåÞ¨-°à‹@™îÅ3`Z> +Áx6f4Ðé—.…Õª‚•í$è÷ªží©ÙlÐV¢ö#,Ëv +V»° ÓV« ÜR™ˆšÛ +³½„‹:,b _L›Øž™Þ£({vD²°.†LT`ÇXUÝ…U?Ù×ÁX°S¬‘´ëZ±€mŸâÁ>­OîíÆê«8‹:Ýlë4Û8ù¶Úpœ£V7îôAÎ+NÍ*öAV„ÌV*?cÍËBï¶ú£·xµ£4­g[ç`k?Š8_…͇12j`ÿ‚¡Ò›J~ppBĤˀ֔¸:{¾¸ýytñ-˜« +°ëÑr`G÷˜7³óCVçë«-X¥c½ˆ‰qaàŸ|ë´Ð™ŸD¬:ã`~F /Îê° Uóµ³úø…QY”ך¿¶«p®+pØè€ÆH[+LÀœ1½q”uÀlÔ+‡Ý•âÔ.’£2ðŸ»REÛ©üv<³èQk°Ú «Ÿ‚QûV=™pÒ…¡Q9ðÆwÅþµV<°k§ å¾ +hÛ1'Í:§ U€ í6àáXf8¤SlÆ;«À²(…¹S=±Ýš$FN(€ +Pû‰â^¢HWáh€1Þ[ÜÏ®ý€[dËqÌêlÇò §öRµ}¥s€c[[ÑÜN$»ÎìD³¤û8œybd)n4²µp,@î$fÓ¹ún 0cXêÚ~Ú}eÁt1ÏP¸—¶<ðjLovf¯ ·÷[Á/GÂ0½Xv/^Ü‹»pœJÀØh_nEv"6˜A  gÕÜd;kœT1þ ’ÛS1·*Àm‡­H&Œ«æÞRj ¤Ÿ‘&ôêƒ= SË€½­æwÙ=»~˜iŸš•%À†t~¨•XûœµN“pf[Ç…áu®ÐjðT HݛÌF©5<ûð« ŒÓ +;Í ùÜþ6×>7˾ÌT—nç¸Ø;ͷϵòÊ®®ìÊ2ß:Q +]°«“gNíÌ4³´ØÓ«»:`æÁêö¬~té\wG-á©Që»o”–ÙÖ1À6öZù h¶%¼­„HÞ†A¼9Ho°}iÉZgJiú«ñ0¤ƒm¢äÇfùÀ(¯w©y+c!­ t€ y'—¿›\þ>×¾3+' —àà„À@S½„ƒøvÂM°®‚Ö`éƒC4‚cVK`Ž%2ãlã²Øº³¤: +v} `ÁÆ(Ãj˜åy±{žï]~̓ÉL2m7š[Ý„3Ž9ã´‹©&«Ó@‚yƒöÀŠŒà…ÀªbC85jv­,cš»Ï%¬.`à­Hðp€46ÜI –ù»Á/·b;¦¢RîÁ£¨ ¸.¹Ò¬\ÐÀlù`­ ²Ò뱊:ðÉOŒeÁÞ[˜¿ +r‰TþÍH‰¿{œøÕ–ò^­ÅØ'h-aÚÞLËu-ñ"ho¶ás’¹I®{U[¼,ŸxÃëÊü™Y?´›§™îÐ+³çõÕ»æñwJù0Yœ¦‹“x¦Zmî,žê8¹½8È7³vœQ]æ:çfíP1ìo® +ëšÕ–n÷¬4ºsg¬qj––€Ÿ ÝËåÕoG§ß{)´Å† ‹wši]H[Ì©®Xë(Â'íÁÙ«<í΀çsý›lïZg¶÷x5>NãH+/¢€sܹ7~Þ:|¯×€ÕC< ro;]n+/Û»­ÇpxS…D¦“ë]faÂÕÃ}­ ²1]˜Zµãlû,89ÛQ ÖMÔl$YW+Î [øö$æ\uÖÎ5à¤iî^Z…n»Ïv.°f=¬UöÒåëƒUë´/r½«ÁÅûÊò¬¡ÊìY¦{iK ¯[G_oW;ú&ӹм‰šm×çÏœÖH ³Hcžï]û—Në¨4•_zµƒÙî9°¼«²xÙ;ý¶}ô¦4}f6NŒÊaeô€=k€ü9=ÍvÎ +½óáÙ·™Îi¢0Ò*‡vû:;xVÝgz·Zõô]¾{Ñ8x©–&Õå›úá·vû"íÍÖ¹ŒZ?Ê÷/SÞ(íŽâù¾VžÂcùÞ™7~R]¼.M_¯:ÕcEÖ,–(Þ$UÀh„¬˜‡Š; šõdnc}áøQµ#Ó©¥9èëkF­ªS]‚õ† §Ná«+oÝÙ‹Âø¾0|Ú]½¼x÷¯ÞìYÀiÁÆÖ«-߆w¸ÚsLZn€lLzQ»j–åñem~Ó>~5¸ú¡yô6Ó¿4jpRnc`{s«Š*Û¨¬_%sÖz?¼ú©wù}ïüøË•íà ÒkNçÄ›>ñ¦O—¿î‚ÍrÇ7»¡[bʈëêì%HÚîéûÙ“ßßþú?n.Œ¯['0çW“ÛŸÞüËêí¿ ®¿¯Ìž´–OÎÞüþüÝŸÏ~cÖ²íÓÂè67¸t§O*Ó'ùÎÉèèîÍTwêoŠÃÛ|ÿ¦uônyÿ‡ñÝÏå‹ÅÝ̓gñhÕYyò¤wú~póÓàög§sž*NjÓ§jeP feÒX>¯ÌŸ .8ùú/7¿ùÏÝ«‡çïïü·ÊôY"7÷¦¯Ëóוlâ÷«Wÿ®qôV^î¥]™–;òçÓwµÕëÖÉ7«Wÿ2~ò;àíöêùáí÷Àr, 襃|÷¶¶zß½úíìÙŸKÓûÑÙ7V}]_ˆèÿ¼¼ÿ—ûßþ÷ßüùyöý_ïúÃ¥éíðúûÁÍõ£W£ëïοþëíÏÿ¹qüÆj®ÌÚ<Ó:¨-žU/æÿIïáåÆy¥}þ;;bnvFΩ +…*•P +…œsèœÙÝl6›¡EREeYql%K–4–£,ÙʶeYŽãñ|ßÎì9{v/´çàè­&Põ¾÷>Ïïª.ªë‘Æ&˜h¬Þim깤žŽ9¥2møŠ‹®Ô[²GÚX°Uaü‡ Âh숭]sGlì°©^fb‡M4ñ@Î“í¹¢]gd‚ŒMúós±Î¶¯°åB7jeÔDšŠààdlÂ_Z Ö¶L C”s*Ìc¢$LÈq™^ º*VV£õSÙŠÃ3 BôÔ©è$”‡TÞÕw¸Ì’™ÉêHÐ@py:6Áåf]Ñ6¬9ÃMÐT,é˜$"BV‰H›Ï-À +GÛ;vo^k焨±x`ŒîžHvwí`m=:±ç/.´W.€¨~jk…ÅKõµkÍõkåÞ*\Áý!?gbR€ý˃ÍÔäùhsK,/g§ö¡ñeVÅ“%£©¼šš:“œÚ57¦vn%;›°È§dõ¤ ê˜Ì\¨½í+­x’ÓÑÖŽÒ)©l.#l|Pc·2Q©¼\\¹»¸|erëÖ¥ÇßrJ \šH÷®ú +«F:gf‹Ž`ËÌÕDtÄâ1Ñq3VHG«lr‚Ï-úK« ¼ZwLÊõxþ2>9 %H©’™Ø£¢=WlÖWÙ¢Sóàìå«:*1b ±>YM«§BõÓ‰î~¸¾Þ;ukþüc¨¿äM÷r½ ¥ÅƒâÂ…dw§°x¥²~³0yöê#ßËKF*ÂÅ›ÁÊœH¼s:3s®°|5ÑÞ¬ÍioßBùTfb£²rE¬m±nbj¿²q[êœs‡J|ºád5„ˆúËÎX—Í-0ÙÙ`}=Ô3cªÀ·ÑÒêÔ©ûùµvr±œËÙâ¹Tï|qùnkTd +2ì  +!‚JÎgf¯¥¦¼ÅuoeÛ_`Pa6V“ÛØqa +£‰äLréž]¬àÞT¢¶ìKO©a<ÜaóËdªï¼žÔä°…5º .Éè Ø‹èÈ&[¸µlž»d"X&Q•Ê3þâŸU)·WzîmX•Ãç‰5 +³wÃ#?{ +Íõ’ÕÅ›·Ÿ½öôLt47³W^¹‘ž½ý›Ÿ¿RX¸JF'6ön_~ä*Qл0:bFª®åfÏuNÝïlW¦¶ï<ùZ°¾5o¬K•å`}£µvcåâ“{Osé¥Rk½Ö;‹z³±ïÅd´+–SSç§îLž}">±×]:W›ÙFøì“ì:CuW¸!dgZ›·›;z³s,ЂX23q‡¿`Š°bWRíLxÓ ‰‰Óz `؆Mœ½r†îH=ÕÜ<Ïsù©›ˆO„[{by5¿p:ÍZé$Ÿ†N×á~>Þò½êÒAsýzffkV6›®mîÝxåâ¾T+=±“êžu„šÈxnÞ—_ ãOb‚‰µdVZ{¬ú K •ù‹+§'NO.žë®\À|iøçRej>?{~aÿýûž“ò³s»o? e,¡'c€®ÅòéX÷R¼{ž‰4'Ϭî^÷F‹F——ªxd’LÌò™…ÒÒ½lzNƒKd¤É— þ ± ØC§ç`ÝÄüìéOMn^Qؽ Ÿ&*¨m •uWdXð•—wî=¸÷9ZªÙùœP^âK+Be3;wpH bî7ç5ë +”™x7R[ONŸ—šgØì’ÙË·¶ÝRNŽ8Q>ê/ÌÅêë©ÖZ²¹X˜ÚÚ4àN1®l„«§Äâ +¸˜3\‰V–÷îom^×;¥Pu,€ÏÏ'¦Îùkdbʪ¯íÞ³}í1W¨xBç$ &9éŽ6¹Ütrr7P^lôv_|ãç™î¶SÈ6zg-®Ý¬¯Ý˜Þ} ¸t“ë§om]|Ü x^4T]”–õµô̹hç4.U–vn4ÎZ<‰@i1ØØðdf¹Ìl´±Õܸê]aâS•þ•c©£ +H*f¾lò@ˆk +óbª/ÏÜA§T¢ ákŽ@…Oͤ&Ïy^ƧÎQZÙ˜3T1ñù‹0ŒJˆPrËrÔ«Á|ÞD› UÓÝSùÞ_˜7óE™Š–N_ÆÌ +ÓÅù ñÎ.•šÁÅ(˜¤É“ —=ñ®÷áþ´;\£ k"-:Räç\R¥6¹UïíÚ½™Ds­0w.7³Ÿ>S˜ÞI7—=¡ÊõûžzýÚkWÆm^#C¸œ7» ”NùŠëT|ÊìŽî]zè±çÞ +e;V&mí†Z{ÑÎ~ºw-;ƒÁS•Þ9©´8b¤ÇLHˆR”|)3yfÿWÁ»ÙX=RžWØx³'Tcá²À¡òÊôæÍÎòeŸIÔÅž‰ëÝq„+ѱÉ@iM*®hq‘“Ê|¬mp‡`GÀCÑ)Lj!žŒ'ÜPÛ=# aC¾L';¹ÝÞ¸!ÖVŒžÔI½{PGè4, Ùe#ÖÙ*-Lœ¾M%»ˆ'éGÝxßÄö•åë¡úF¨²olPцÒá“Ù8W¸i ÃF2Äe¦¡ Àss3gÒ“§ˆPÉàò²Ñ²›–j«±În¨¹í·Mîˆ7ZY€,™š4íL\o÷ÑR%ÞÙ +77è$8È)ð;÷¦'ÜÑ*@p+abIïŽÛ8­]XÞ¾EÆڇǭz#U6øh«ÒÛ!T~iV6®ÁE·¿-Ï'jK&¦'À€B»ú²¡¿°àMMù““ÞpÖ*ÛÞ +Ö7‰p ²ª‰É*íH4¸¿V™¿â Õ”vÁÆç¡F²³›èìÀ#Õ> );™Z;÷€Õ€CòW7©Ô4íÔíÊ☛3“±­ƒ‡*Ó[ãý@¤c³ñSGX9Â9Aû—G¼j_qYªm:¤Ú˜Ù£Å%ŒKq‘òSXÄ›· È2ZTFôN=æ¡C9&QcMÀˆED¨!äfÙXË%¤…™æÊ%>7ø²ˆ/‡‹e2Üv;Ljª¶ Š3X˜‹67£ÍH{Ë(«])•„̤ÚÁ‰ÉzïôÉÓ…›;N©¦#$8$eH(fî„Ì&Ó(cc :ÑrÅšlzRK…¬žXvê,ì¦Kù23‰Î^jr7\]rK+³ÐC§ÌÄœTY‡´8”hª½ilǺèì•è¨ìW .¯ÒÑ)#“…=²@6!¥Dc%ÞÚÙy•Ýë‰v¡Ôu¸h +&¡ ÝæI;ew°ªDøHe½²x]È-®dÚ§¸ÔŒÂ.°þüâö >Z6¹Ù̬XÝJNzWéø´…/꩘KÈ­lß-&J…ùÊì…Dã”[IÎ÷Å;ùÎiè.Új®ÞJOðÉ)5ÈPáóp¦ 2>>¬sXÈ0—ì2±N¦³S^¸lç ã€Ü\ìʽÏ0² +¬l +÷q?pï|º»_™»¬03R²UY¸ˆøòJ”Q#´Ñ)ªûú Ž  ¨,Lcî<88„qÂ*1¡Õ(6_vˆ“jç×Nh5ö€;Ø>&3¨Ðq ä ìr‚MN.^âò“2«u‰:WH…Ü¡¶4 +‹3B µÇeæ¨Ì„Þ–[<wÈÁÇmTÀŸn»# Ç9ªwêì>§77(3ÑôU"Ú–V¢µu/à·;lp†âµìäY-0QaW¨˜‡{ÓV"4d.ÙAùŒ…I˜¨„Õ“cÓÑêFnrãsJ›Çˆ<áºÂJkPºž‰u*ÉH×Îfuˆ`¥"°eÆþÛS9w¸“lïVæ¯ç®+«2›gH‹)Ín™Ù©±óà€gü¹9¢íöeÉ@a@é°Ð :Ññ¦»_ÞBÆ-TeR [ª-¬î? ²s‡UãZ»Ù!Pb™ 5tvÿˆw0±—¿ÿ^}rcPaÑ!,ƒg˜ó$çÅܪ˜[ê‹P¢<$/Wl@‰À‚Rip¿ÍwF*L,œ+S2Œ41¯¹£»;’€ƒÔZY_´M…û^¦wK°æJÄc  ‡ Îv_mzýr¨2ïŠV-Þ´žÝ¢ÓŸ–ÊsDCúRà5ábÏ%åUïI½ÓÆçÜÀá¹%>1©DYBªÊë…™³©‰SRqÆ,¨L®9;»{7æK–Û,ž‚Cj“áŽSè¿ý+G|€¬b~ +“ÃéE«!õ¥KùÞ~¢µNGªÞhem÷úÜî­a 㘿€ç‰OblV‹ø4°‰„hr‰p‚*ÌëÖ‘@Ãl‹åS¡ÚŽŠ 2|B=1t+Ê£lÊΤP:¡µyG £J³ÁîA˜¬b¾<Ð…ñâBúRꘉô秅Òš´°qMÔK‰…xmÝB…Uè`#í6|à2åU.ÖíߣÆL®P0?wB‰ žmDëpl&:!·0r aA’‚û¡=“ÝýÜÌùtgWÌ/ZÙœ @åؘ¨ÌD¨m4üòZiéjª»ïIÌ@1ŸPaZB°ò ³{Ód &$gÄÔ¬“Ë î ‰WØŽ™äZ»Úä²’Lï£#Üåͬl]÷‡Š'GÍVWºå²vO®ÿÉÒ°žRz¥Sˆ'5jp(­Ð³.^w–ÓS+RmRªvSÙæÚ™‰­sù…-"˜ðÄ›Ðl¤e£"Ãz\e£,NŸÓ—`Â%6^q3®p6oºüP}u_,uÌ\Øê‹s‰r¢³À'd(ü¾7œi{"¥aµÍFI*4n¥%ÈøíÓdjFO…q_D,v¨D™Š¬BTéòšù H§{ bsÆÀ”ä V°hVêè¨FfrZ€Ù%ÛÛ˜/l"(;'‘Ñ‚…q1æŽäP!îI”‹SKµå-2YÒ¸|d¨NG;FgtÜHÒÑ+L¤˜™ö&râ»ðL(.ºã]4P1Ðqðe%ò©2挣n¹ƒµù‘úš™JŒhˆãÃ:½…²»$;2`>……3SjÌ焬œ„¤Æa „ÞÖåÖÚ9*^<®·Úh7PPlÂèŽ3ž7«1C0:ƒÃÇWw0˜·ì’šÞtNà¾LoëJ{eO*w´4hÀT¸Ç)#bVf§­L@*ÍAXFø ”´Ú.˜¨L²u¶8s‰ +׆¡6l4ÊF½©6D‡#ü—˜‡¾×Â6VÒ-v6_äâ“C +û¿‘}ó¤ÖFE=ÁªBOP1Êõ´Ó[ˆ•–‚¹„É ©2«Þáµ{Sß3Ü5¦?*·Ø²w÷ƒWzæ•·ß}ä¹W.Þzpùü=voRi¡&— R™‹M¸|1Ùb¥œ?^O×çg6/Ô&gVv/n\¸qæê}/¼öýß}ù÷ßÿéï_üéo|úùýO~'’›8&³;ŒD”‹N³Ëc:jDe×Z)‚K"tRk÷ëPÞJJ’’µ¹õs[—î[>wóÜ­Gî}üùÅ+…é½dkçSÇÇ‘¯Õân±Xš^8µ±sáÜÁÕ‡{òÕ׿ÿþŸ}ò»?üêÃO>üø·ÿùÿüôó/_ûþO~æåéÍ‹ÑÆš“ôˆà“J¬7&…“…J{acÿÔÙ«{—oÞ~ä©WßúÉ«?úÅÓ¯ýà‘ç_yãíŸÿò£Ï_|ãÇoÿû/þþÿ|鿼óÌëKû÷ŒEŠ½ÜÄZ¤2MÓát±33{úܹ[·ïü™çžyéÕ·Þùù‡¿ýãßûè•ÿåÛï~ôåŸÿþÿ?ÿÈ?ÿòG¿üÍÕ‡ŸÍv׸XÓ‚³>)‘.6&Wá1·¾½õž‡ž~áù×Þxü;ß}øÅï>÷ú÷úÞ‡¿ùè“/¾øâÿ_ÿýáo¿xäÙW¶.ÞÊM¸¸œLò‘N¨´¬wˆãâ¹òÄüúé wß¾ùÈÓ7¿õüÝ=÷Ò[?ýÉ{ÿì×ýùoûóßÿëן~ñéïÿø7Þém^8Äù<l:ø¬'ÑöD«¹zob~ciûÜÕû¼ïñ{ô…ï¾ðúÛ?}ÿ“÷>ùý/?úÝo>ûüOùëý¯ÿ†nýɯ>{î•KùéQƒkPeå3¸Õo¡¢¤˜g›3«;—ïäÊC}ë;¯ýô7Ÿ¼ûñoßøÉ»/ÿðûùÇ¿ÿÃ/?øøúËÿüÏÿ|ú»ÏŸyåÍ­ 7™BùåۘήEXˆ%‰âT­»²|êÂÁ͇o>üÔKoþèç¿ùäG¿xÿõw~ñÛ?þåoÿù_ÿîó>ýô?þó?ñá§×x"×Y ——È`MÑŸÏf’£)átå™|s®ÐêÍoŸ»rçÑ{éõw?øä“Ï¿üî¿ÿüWýö¿þ÷ù·<ûÚ÷o>üĹkwØPã +vœ¯Ö§gæ7NmtéêëwzàÍ·ÞüÃþðå_þòѧŸ½÷ë÷^|õ¥s—¯L.¬³5.Þ48+‹ºÜíå¼b:SXZݺ|÷w½õàcOþÛ‹?ùåû?þÅû¯¾õ£þøg_þå?àt^xõ{/¾öæî•û[ {éæ¢Û—"åH¡ÌT‰Bkr©·¸º¸²|åÒ¥ï}ï­ïýà‡?üñ;~úÙýlÇ»|úÙo÷÷üÇg_|ñæO~zõÎÖÂ&¯:…á¹=d®Úš^Zß=XÝ9»º¹sùúW^ó×|øÁ§¿çýßüñOßÿèƒß}þù'Ÿ}úÙgøñG/½þý³Wn'ë³B´F*ûRS62‚‘Ÿ˜Èä* ËË÷ßyèu8ŽŸüòùï¾ùó÷~óå_þö·üó×øå—_üêƒ^|ùåç^~uáôe*T4¹‚ Vn±ÀÅjÞX¹Ò™kNέnï=ôøSϾøòs¯¼þíï~ïçïýúïÿü_¿ûò¯ï~ðñúã?}ù姟ñÒ›oß~ø©L}I‹ c:縆Pšhê‰H–{±\samëöcO=õҫϼöÖÏÞÿðË¿þíË¿ýÇÏÞÿàÃO?ýòÏþü_¼ûë÷?þä£wßûÕÇŸØ»|+®â\Xn²iQJÌx‚Y¸Ø]»~ï#¯½ùï?{ö‹Ï>ÿüË¿üõãßýáןüî¯û<Ï;?ÿéÏÞýÅ»ï¿ÿÀãO¯¿ÕÙ¼Š ¹rTeö¨nƒ”k+Æ›½Í½ƒë·~þµ·^|ýÍï|÷ŸýòWÿøç?ûÅŸ~öÞo~õÞ¯?øè£g^zy÷•é…-.R‹UV=|$Wl¦ ÕX2S€5ߘš˜[š=¸zîÖ¯žß=w&–MP^ÆŠ;µVBPcjtLÈ´¨¥_$ÏKJ£9¿²6·ºÒ™îœÚ]»~óÂ=÷]ß¿|eÿÚ­õs—J“3¬wúÒžXÇéÏ«MN­Ùi²“V»Ëéö¬n^œYÙ Åâùtbyivo÷ú=wßÿÈíþðï}ðÛ÷?úø“Ï>~êÅç·ÎïW&'™@„ +¤­]QF±XÙ8¿¹#U›LkÍfk¶7µ³³ñôÓ¾ýã·?üøÓÏ~ÿ»·òöw_õå—^|ê‰oß{myu#W›bÁÑÌ.³Ó¯F¹“jÈ5ȨS$ÉÇ¢á¥ù…{¯_{õå—_}óÍW_}á—¿|çÏþãsßþ· çvV—ç +ÕZ4_6bî1­ÍæöÃÃààõÞêäQ‚òI±L¡>3³pîÌé~àégžüÞ¯þúƒ÷?ÿâó¿üõÏïýêÇO>ùèþÁ^ª5 .ƒÝ 0<¦% (OŒšNŽ[Ì('ˆY¿˜lt¦º³sk§Ïœ¹ppñòÕ«×®çÅç~øö endstream endobj 26 0 obj <>stream +^~õ•ž¿õÊËß¹ïæÍõÍp¢hqyV§ÚF™pJ‡‚é+¦ÌˆÛ/Åæ—6/]¿ýÔ3/<üè·.]¹ñè£Oü䧿øÁ~pßÍKßÏc>°{vo~q¡TmÓÍìÄ®u9´|Ym¡äÔ`v†c¹ÅŵËW®~çÕ×€•{òÉ~ðùçž{çgï=ýì·/_¾»7¿œÈ•ì.âŒÒÊŽkðq•YcÂõV§ÙNÒb:ªçë3é+ŪLgw)­.¹‰€}4;™?t||Tn1ØX³Ã«³RVŒCìœÍËúBi×™Ùªµ ¨‹AHÎ!¤ä(7¨AåÂDô¸oXSkl&”ÑšB9…8°·â)–·Úl‡"¡x&Ukw·ö&–}‘¨ÑAŽj‘AuT),ŒÂDêˆqApQ&˜C©€ ÷`”àõ=¼Ï+øCÑ°‹Gs¥rw–¥@0TªTÓ©KyŒV—eM¸€Ò6Ö²2±¯ ¨dF’ò.6Éñá\ºˆ%$¿ ú…ÞÜb¹œM&ÃåzUŒæà ítÀFÅ ¨oT…+,C2ãÉ1ÝÀ˜qDëÒ ðœ1’Ï$²Ý`$¦._[ßØL%"ÕR)“͵õv½ç´fû‘“c†¶#ª£ƒøø¶'>B(oŠR$ˆe Ú§3Z$/†ro4š(TjSµú¤›¢¤PŒå‚V;#ÓÇ”öþ(+ïôåI¡ÀYÜ:‹ËE}‘24µÓ“LÕÖ —ËS{n.¬3èyŽ…b4ë3!„\@B>ÕÞ#ƒía•ãÿøÆÆÊ› IirÃ.c¸Çç Cñ@0Šf)Êc·Ùq‡›æâŒæy>\Uôo^`¹hƒð—¾qTy×€ìĘ$Tkq[ɈƒKEsÝÙí[rÄñeâ—09ƒfWHeó )ƒW#žCƒŠ»N*å6âAÝa&Pvòµ¸Cý™$:lX‹h‚“Ôˆ`§âFL8©4 ++e@X“U™ ™0ÓQÄ—&ļ[H«Ì˜ÚŒAì”Õí#|I­ƒ5:M®ð …íkò#£z`]-êíßPCø,¤8ntŽéqµ•Q#rƒ}\o7;|nob2BEíž„ã)oÄáËj5BFŒxÀÆfèè”…J(P•…R[ÈA™ypT'W˜Ædº±þ÷9â¬Æ)¿£m¸_ab‡µø(̈åؼmÂ%µ™QØdzרž50JÔ?nöè0Áâ +„³­¹Õ³b832¦Â!Óu:EÆÂ3 id“#80fþ? 5š~Ю1­Siõ¨Ž*ßmêìÞ“2(rs4ÄsÎjç4&—Æ@Tš«á?< ‚§2Pi3WB!WúKFwHnó(Q~XƒƒÉº¥ò«Rq%9±OE&•ˆ%#©¦ÎÆ~ãèø`¢…‡«¥É³©æi³;~dP?8jˆ$;Ì×~P…ëA`R(2³B|R®Æ5&Ä£6Q£j‡ÆÂjÁØ¿Ô­?d’·˜ŽéŽkŽËt2½õ™É¨KyÓ“VwTŒµƒÙ)ŒO¡lJNôG†â’ñ~ýøø]ƒª~ ©q«#€1 Ì“²¸B±Ü\´¼vdÔxlT'3V&á +6 ±Šr)-Âád ]žµ±‰ããÆ2àҬAX_fžŽO:¥ +,¦?V¤i¥Å %7ªw)m~-r5©°¥wH'ä•É%FkZÄ3ªÃU6¨: &àÞ,á-~uKRfpø ð`aÅ&ÆâŠ0Áª… 7¨,œ[¬‘RÅJÅUvÉBgÆLž¥]yÇtØ×Ëï:¡7‚©l‡ßêëí^…‰’™o èË¥…Ó •M‚ÿZɸG5„Öî…Ôߟ¤§p ö ‡ØhCHwtïñqÓÉ1“ Gd¶ÃÇ•‡‡ GÇÑA%®±zùPW‡ˆr+<†”è‘Aƒ +õʬœÑ¡#]&ÞSZy„”èPÅLÇfv\é8>¨?1f6ÛýÉÂâ’FÀW%„ËËm‚ÌÌÉL¬™N¹C"PW£‚‰ðCH‰7w¼éy“;e3nb™HO²ØÞÕ£Â]'T&wÄF%p6ÉyøÉ ùæQ…\…4'wÌÎЀÊaqGuDÐJ'ÝRSÌ­à|iXáECù“ãæ#z­…¶‘QGÿrÓš;ØƸüá!“å\ØÊ…)ÌÃ:×WS@ Î`•‹O¤Ú[þÜ4ÂF1_å2d¨áµ,ž¬“d6ÿq¹}@Ž@‹õïñ¶X‰°ÉÖa"ÁgbåȘy@nQ#¬‘ »"-3—1Ój”£ÅÎÂÌ_€†²y ¸`Àû#& ªeF·ÆL̯íWgN),.-Â@y£LÆF§Þá¯Ã_TØ# ðŽ i‡T¨ÎÞÏSÅ“ì‘¡®Ù‘R]:T>®0÷-ØLp¿ à|ÎJ%4¨Áý‰Â¬•Ž ið™öW”ˆ¨°úØXG…rw i56ÖâƵ˜\> ›®%@u*ÙŸ€mfa xH…Geæ½KçNP¡V 7ƒr鱯ƌ¨Qþ¯É²QÑQ]ÿ¦‡'‡ñ¯;t\gJJm_Ò þq=$Ç M`tŽÂј‘·0£F· +ñœ1=Õ;‚•é3ñÚ‚ŽðÛè8J§m®(âŽÙÜq•ÍÿêøˆNc†NtèÜý;xÄ—]©/ß©m+l>¹™Â¸$¨Áˆž18¢¸¯DITtûòNoÁì Ê ”Ö.« °ULJ ƒc–£ð_9jLŠðefÆ¥FtĘÞ)7RP–ß8®:1jÕ¢‚Î.üË]Ã'Ft&;´3¨vŸÔÐ'T®oŽ˜ïµ»õ–*‹Ž¨íCFÒê-;c3\aM¬¬SÉIÌ›-×——Ï= %CÃ&÷°‰V;ÂgóÖÈè܈…MÐX¹20¡(°ôg¼ôG^㾜Éå³²a³'¡§¢6_—Š&:jã2®H‡Ï.áþ²Ñ„ÒÕ~”Žº…œKÌ\¢ÚáSÙ8‹+8Пߢ=4¢ù³wvz³6²ÿö5x +Hô˜Æ>¨Bä¨_ëJX¹²ÃWv +E§X6")¥*mjgt†ÌîT,Ϭ LìظiPa5`+šœC=yÓ¨SXÚº.$[G†µÃ +«ÆLëí>‡7ïàój é‹&wðȘá¸Ü:¬Å€WMd̬épñ$ê ç‰`¥?”æ˜ÜÓ@Hg˜–*Þø„ÂÈ ™-˜abw ¿z˜ ›uXˆ 6¼Ñ–ƒO9…œÂæ9Âè’[3wx ‘êfiáÂ@÷1Ùæj´²®³û‡Ô¬ù°‡£²þA¹ùè°ˆEÈÌy3s:WtÄèµùP±¬og§÷q¡0nòØج9Â$d§ÂäUõgB[¶Ñh2£ LP‡ðr9¤Ä‡4N¾Öü%Ÿú×£²}1áIÃêJ~u·xhØ!Ów¨QuÿƒE¯ñÙý@1Ù&Ã¥ã°P˜ £¢FOŠNM[}y#%å©å+K—׺ƒgÀ%U¸Ø”76åOõœáΕu…ªím›ÿæIíÚ¡Cû÷W2¡Vqæ ZÝb•ÅPTV>åÍÍ*+Ru-Ô8jÆC-ñG*÷>ô|±· -©±1*„Ó`"Êåùä.TeFJz‡úseý2a&SwÊÊä¨`›Š´äúø˜–é«;zÔÑ䊚\”M¾|¸¸€ó)hO&ÒÒâ¢ã«K¬±@EGÅ娠°ñ AÃÚþõð +«õdÙhÛNÅtŒ +d4¨§? È%A]ëÝ ÅfwD‹ FwØÊ&Q>£Æ„½sXƒëT C™®Ç§Îé˜äqª´rZÄ«søà%6Vfõب8BÆÀ¦È×´‡F̓jBmóÙè᫸¥:.@k­,œcØƥΠÊÆ É¬+–äF'ƒ{|âëÇ•'Æ-jv 0ivNÈ,2ƒcÐ5—ãKOùK‹x¤[‰zs& áP͈K'Æm#*B‹,®8ÎWœ¾†Ò*iÜf\‚òF™Ô°QcÇFLý›Î¬>+™a¢bf pHm‡¨ÒŸÅj†UDÿÞdTÄ…ŸèÂa8=1Rˆ ©MÃZt T×Ðc~Ä“4áq+‹ +yWâ"šrö/ÿ›IwöÒ“ç½ù9;“O²mvKFܧÃøq }Lm·‘‘deófŽ ôv?, ‰ˆ¸¼¹py‰KN„²“+gï·úÒ¨?i®%º»ÉÉ3Áúº¿¼jã³V&R¬/|絟,ž»-·1̇óYB¨é¥`qÓî-‘b9ZYÓ8¤“JÜäJ`þ†;ÚóåOÅ;Ùôœõφ*ZP{33dp]!B(8¥²Í“¦"íDi~ïÊãåÙ‹23dŸ›š&3Ž`›ŒN{âs@& +¿©ÆD=À¸¬Æ*Èõ̘Þ:,&:Noj{XCX]A°@‡¿ì’jÑêšX\Ò;%¹Ù ¡6<×(Á*‰õm‹P²ûK€—'•ö]òd¨Ž %.1-¤z"xhXlÜ<¢sžÙÆôHÆì|å +F2øŠ¸T·ûËzW~ÀèŽ9·XäB•J{UqãF…Ä„P ÐärÛÑ=Î¥L|TcVZw„—Kv¬|šŠv¾€x2L¨n£ýIV¨d b:LÒ؃r³oÌà19Á2ŠRz|€\‡òcpxãVÚ¹ÿÅ)#Q;H!¯°°ÿrhäè^®§ldòá/€’ð€7Ú ÅÔá!ù°Ú¦²±º¾“2zg²’'1k¡cm6ÕÅ‚Ug¤éŠµ¹L5.*åÅ+7ž)Ìž9:fTZ(]ÿºAö‘ +5È`cXG(ô¸Ù–™Ùþ7tXiÄ÷¤ºbqQÈÏ[( à=Ñ^·yÓv!ƒ+t¼,.¥[[•ÞþÌö}66¬¶Sl¨*$gý‰ÍЃ+ÈÆ;|nvÌê9.GùD/Ù=Ÿš:Çeç­ž¬ÆU`¢Ã› æ ©lñ$m\ó¾VÜ‘šÝ—%c5>ÝL6—øL׈óéÆjqñjtbõ×0±Ž‰µQ³§ØX;sýé@¦;¨Á1¡f$ fwÞŸZ”ŠÛ¾4v(Å-•ez'8Έ8wº2{°rþñHýÞÄÂ~eêôsp‰`e1ÜÞŽtv sW›ë+§A¸¬¤„û³ +ÀÌc¢bf&3fæ¢GõN€½ãÀ3ˆO…ø•ýa=™°K ¾¼Ag—tTR‡ûÜRÁ!äml +aS!çÕ¤Ò®ëíB,QáZ²¾4¿{kíÂCõ¥‹6>‘­Î½þ„?=”,oÌí=¶wÿëùÕÛDdJOFµJ%¤ÜÂHÿºDçÁ"eÄ*Ò‘[Ø#c–1½Û€î4¸d cD¨ +(ƒ†TSõùùd°b"ƒZ‡ÇäI=i(­pyavïAfrØ@hþÄÄ9°Qg°>ÖŸ„LjQ>˜ìd››`‹\Bø¬ÏؼywtÒï +ÅÙ|ïìÆÝÏd¦Î‘c`+t¬ë 6ÍL2Ô 9>ÒïåÎ%ÿõ„rÔH9Ã].³êwÉðÄ°rœÛJ…Õ(§Ãý“§nl^jbëVaîì)°ÀqEÌé»_ äæ5õxˆ·ôxJNÌÍ+á®A½ÊB†³]'—8zRù¯w (V_…ͯy kB~ÅìLhQ‘kzT<2 GOŒÛOÈ1¹‰¾’›èÊüÄ°÷˜‰í=„€ys@#x¤­@½˜#BU›0ÑI"زš¨·h&$Œ Ù\%ä" ¥ ðè«ê£z…13q×°ú¸Â*·ò¡©o&ºg¤Ê‚GÌl{°6¿¯À8!Û«¯Ü,-\6w‹½ƒüì%"Ô8¡ÂèPÝîIêì\Ÿ¥a3[pZÁòºÃŸ?"¬²›œAÀÀ]#ˆ'Ÿ3RI\ìßiãÓØžXeã8KµV3S»©ÉÓ¡êBcáBiæ,)UØh#ÑÚJ¶63§Üñ %1Cƒ0Þp#˜PbÇeæ-‘ w°å +¶¾ÊI®03VwJ ˆO÷²½‹p +¤?;»~áú£/¹¥1#eÀE§ÔŠÖ·z§ï¬_{6=sJwÜBòÉï¨Pߘ‰>"³éÜz<N¤uHŠþ›i­Ã? ¶+ŸÃ_óå¦Oß{þÁWW¯¿À–‡Œ¬Ïè]a•Ý?j¢ DØâL¾2î+C1ÀÓ2‰žÍ›=‚ú_uä +ºƒUƒ hŠ/Nmå:ëBz’MõÝÂ@"ÈèDzú²Î•Ñ3t¬ÿŽâèÿ?]ÁæPGǬZ»/R\ÐØØoþúá9¬_pǦìþª“;åHÀD¦dè¡cŠq=-7qr31Ê@„äýöƒÒ"â †ÒFgÌNÁ[X¥’óÎð„·¸Šð¥¹‹À®f6 ½5 ?‘H™R™=[”&Vi¤K¿9¤?4d€ÄÊký‰ë#†“JtâD¼Yg°B†JÞX%U_ƒ0ŽyâåÙsåŃX{ƒŒT¬LXïjýéd”X4ã~…Þ ÉÝœ‹;±æ~qön}ø3ÈKŒ ×Á4Gn¹•Ñg°EE'P_¢D¶½–®¯Xœ~“Ý.Ì4W¯Ïî?¼pîÁÝ{¾om;}ÅÓ»÷Üû4&¤µ¸_ëŽ"BÅ—]Jwö«sW®ä7‡MG@CÜq*6A†!W† »GT8À6d1ÝPcîP + ”-Tb#Á§CÅÙÂÒ%+—UZ½ÑÒZªyVÊ.T{ûµþ5À¹qÅø³îŒ¶Ë*4€õóà„Í“‡¾Ô7á~h‰QƒÐÑ›îùóó¹©ÓKçl­^C„j¬ž¹Çì””6ž +5ý™¬˜[Â|õ>¢u dPð}˜³ªA,,Å'¶mOÊàð ‰Æ¸Ñ5jpAiMï=ÖÞ~0¿xw|rßìÉÞ5d6aÁŽG,žøá1Ӡέ@ab@‘ÁÒ©`yûè˜åÈI¥L‡Yúï·˜† 䈑† +7³y*:t +¯8ª²éPvPÙ7¡#B6ãTj‹—/=¡„3bã:»…ê ”Üáº;ÚaÒótvÙÄ$ÍÎ`4?‹0a-êq@á2N¡Ôé@mfî:©<:¬=4¤PØ¡ëûßh¤Õ¯°Ðá—ëqŸv‡ªÞì”/ß‹4OÑñ ”Ïz¢%3éµoEœO¯QR•NjPQi"Q·dÀý£`%‡±I•„^ÐÙ“F˜Ä ™šbˆ'XðÆ%7óŸd{T°ltx-¸— eÉ0tJ'9µU˜Ý”#¹ÙîòA¨8m¦Ã\¼#UÖ1Àæô¬XZsGgtMA—Ppp©a-1ÙPëìÃ" dÂÂdMý|á;4ªþúI¹ÚƲá–79®lÕÖîq+”?7¹z•IN‚ŒIõW7w T’¶£µÕã*ü¤Ã=‡·0¢wÝ5j84j3’Ÿa"m=.jl­XÉ°ÜD)ôì¼@Š3­;J+ ›nB˜¤ÊÌŽjœÿŒF5×ý+Ø!¡Ó)&Ú!¤*i¢¾‚Ý_L·Oa çt$ÝXI´6à'&¥Àü +”Ç…’'>ã˯Ž ;ÜRḴq€EB¬½w [F5D¶2_ë®÷ÇÄ)âuˆ¡´ižU?2jñHå`º9•p /@…ª\¼Eø2/dšþ»+*›ÏÁg0_ÖÆÄ<ñŽ¿´æ+¬¦ºÄâ*m*1¤ïts 8´®?„Ðì“é!¡8†Tv¨X`ûo|õ½ ýaõ¬Í7b’  +R±ÑÛ5Ñax©©Óóž˜?x*5{àJLÁ ±B²3½Ùìs‰%¹•–™©¯±dÄo¨lÄx¹£„¤Û›‚Àbz$âã&ïq%Æ4¦wŽh°c£†þ¨miì¿C[uðýÏ4/扥š‹•ÞfkåBçÔÝÝk½{ÊsçÅêš +Ç Óý7NÙˆÒFᾬ+PG¹²KꉳHÖi­¼¾ÿÕiQƒ#¤2Ó«gLK8˜$‘ +¹|)à ¯e¦Ž@™U’ÕÅ•3÷9Å,lß—ûß8Sô%¦©@eÒãʮċs£üРnTGªQ‘ÊÐv. ¤ÁKÙÖÚ¨V÷?XñgÒݽÜÌÙîÆ=Bvz?ÛXšX¹4ªsk¬œõÂ*kq­±Óè¦h¹çà“Pi*h7«‡”½Å¥Dï ½v¯/Ñ™œ?óÚÛ¿á¢a½á*L|.\^Ù¸ôäÊÁSD°}xÄjr>½Öð•D@sù\b›wÝþ"Àù°„Ÿ5¸‡ Lé¨A-#7òt¨áð¥Ç „èQ¨%%!7¹õ.(x7„P ÆUÜ_âPy‘ËNzs½`}“ÉÌá(d›Ýµ{ÉPmÄ@ÉÌ^-²Ó)H¬j+†¨0Ó`úƒjÇ †Õ»µVŸÆì1Û}|(Ÿm/±‘j¢ +qïr 4+•¸ühÉ8/§Ï·ï68ƒjTnv[Éî+"lÒéë_kíä“á줙ð÷ßTGÄS²óàž™Q# '8ª#F4èqpÞAÖŸ„o÷[٘ßu +ÉêƒO¿ñì÷~Uš=ã2ÞÌŸ™ÉLœŽUWJ­SÍéÓ”˜¦Å,(¹ü%2Ø°R™1={tÄzx¨ÿ ”–LïT8Ž jåÖ%äú¬åÛ[ùóCB‰zuΚàR)PX’Š«d°ÁªÐZ•æU6Nn"M ÞDÌCéN4;ybÄxrÜjN b:»oHéס<Æ$ØpMef ˜ßIÁ—ã“]wÿšº,æ-:ØD¾¹Zš8ÜŒldÈ„‹J#©G<€¾z”óÆjˆ;00ª7 œÉ)9|9W¸NÅÚð<&‡7˜(oîÝ\·{+Áâv¨´Å†šbb‚µÍtò¸ÌîâRÑì îáaݘžÑ9â„¢¾p_uéB¬žÛ >ÅrJ£ë_Ë¿>bÐr‹ßJ¦¹ø„‘ i ŸÜBÝ5 ‚Ç … áR¾Â„>\¬xóË\zá²bÿÎ躂àÜÑjp·{fró¾Õ‹On]{~fç”K`ž„•Žë°ôÑI•ýè°þä¸Ya¢‡4„•Žé\ÃZ—ÊD‚+péÖòòÙÛŹÓ*‚ÇÁ KÑúi!¿Ø¿Î6Ú„Wï4`â&8B»×–çɣƫ§Ò­ÓN©9¬%ve3_´Ò ´Â°‰‡Ç¨‘¼„eñ”]ƒ*›ÊJqÑZké|wír´¶ØZØëí\w‡ò(åm:Ro/]ÈNì\! ÊþøÄÂY)݆4¤C½*įAG¥zÄ«µ°'eæÁþ8>,×À¸é¤Üâ𤼠ééi!ÕÅùÄäò•tsÃ@JX°iŸIM]€“¢Cµæüùxy2»ŠûRSBbÊéýJôĉqÓ×Ë:€Ì€Tœ·õß·G½À™£z—Ü@õßÒ»ŽÉLZL@èpB¤´ÄËPK'å¨Í¢„ì¨Ú~|Ø 6Ój àaÁ'å¶!¹e\¿ Žé0ð‘ã +pÂé‰LÐÉÇP&ÆÇZ;Wž˜Ù¼ØfvÅo<]Ê/¡TÔ„±¨Û?ntƒëYÜÜ“w°9OdÂ쎎ª1Âòó@8ÿrTÁlNadd¶A2¤Áä}©dŽ†Õ(€Šñ@¢æ²q)#5‘”ÏAë++T0ÛÛºV_¼¯®ä'¶ª‹ó½}_nÎÎ%ƒÙ®;“›\;?npwŒ(m‡è`“´TKÖÖ5p¦2³ÚB!L‡”,÷„Xc\ºù„ñ |šÖØä”7¿˜êžK67:+¯ýà½ÞÖuÈŒ†Ä|OOAJ¥¢SORic12Ô™» ÅdžU'ÇÔ2-2Öÿv€íÏ||ßè b|ŽŽ·Íå\÷T¬4³åÎεGèhÕÆ¥ý¹e!³hœn®ÞWš½\˜Ú§ÂM1Öš?u÷Çä(©%$}žX† ¶±¡Bý£FÊ ž!£ç° P; gñÉY6>Mpé…õ«ùΚ3cýq.ñö^yþZoûÎÚõçãí­T}ii¦Ã52PFÙ¤©ÿUìI„NA¯ iP¥Í£ÅD—Ф¤ ø‡4knõ:˜ô +;„¥Šó¹ÆúÿÇÑ{?¹qey¾ÿÄÓ=IÑËÃ{ ‘i4Hƒ„÷ÞªPÞËUô¦èD‘r”m#Ó’¦Õ;Ó=ênµŸÙîžÝÙíµ/âíÛx¿¾ E E&óæ9ßóý ï½çÔÝì¡ìppbàOtH,ù„‚‹Œ•ºÛÕÞ% “²8½ÄheBô𢴲—Mz‰H"ßK–æF-Ј 2{8&ƒBiö°#ÿ˜ÙÏk%Q7Ø1„R ½¹½“Ùý§\fÁ,ØðèÔÒñ'?ú•žìŒY àîüt‚*„Ô@ŪqÐÔ%Å\aæ€ÕÇ-0§‘IhÒŠÙ½œ +±BÒ[}¤.LB¢ë¢sÌF:!Áä`ä èR¼utòpQšŸOã‘:—íËå­ØïÎn?|ë³\gàpºµ“>²³l|Ú/V'‚äÂí'Ÿ¡Jé/Î º0 qÈÙ@ØQF/UfÖÔ"/Ÿ˜ƒ¥L0Ù Ó™ÞF´±èá¢BfJ¯oJ-h4ÞóóYZ«âb‰v’µeBŽ‹é*ÀƒO² ’ —GÝÜ°‚Ø !×­xÄIŇÌK£îQkÆ42Ò‚‚Y,˜ê¯Ü(t6(­ÊÌÄZ»¹þQ{íÞòÁÓÒâU>;]Ÿ½üÖ¾&£-3¦Àb“ŠÀX2Ñéoºrw݃—jl z¦|³-°ñ›A'œA•FBÅ fì¢õÄØàîó MvìTwŸTËD(Qéléù¹d¾½QšÞÁ¤œ +ºÈˆŠ€˜°9fgÉš}a«W²8h,Ð;?ájOJ™a+rÞŒZ+;{M­mÒÑ–•ˆXd¯:ª‡Ncô9ï vqÇ4aÇŒLÚñ ž±úB£& @ôÍÊ5&Å,ÎòÉVuåfwó¤·ù`vçÑÜîÃrkÁ‡Kv¿QQ‡Ÿ7{=›ÁŽkùAôíçZ³kû…Þš áµêjcýÁìÞó™íG•ùÃB/”î†cõgßù›+Ï>›ðK\K¶v›+'±ê^¬z™‰v~Ðh8^Å̈:5†œG†mœÒ;î¢m îøù;6x“Ë á_çV¥<ì¤Æœ¤‡Š¨uà€©HMÉ÷c•yð_F¯B´«-gº;©Ö†”UËKji‘T+áXuy÷v{åÀ†Iß"•¥ÁV6z–*ƒN£fØ WwïE ³c”0+¢Ûñ$x€ÀÖrÓ—o>/Mm,‚¸›™Ë«ùþñö­÷žý°¾þØͤs•¹Õ½ûH0b,R\Íõo—ÆšÇl¬ ÝÚ>|¯¬™ð +k ؉蠰–bí=W š«-UzÛà•*KET®h…yP8í]1Õ†0’í‘áây~vP9› h-.:eòñ£6< cÍAM‡˜„Mø…” ¥+ϤLqöpñøÍDwW)Îcá¼W Z8³|øêÉ‹/›‹Ç*ÁD¦`.gôp&'>˜á0æ}eØsaƦFlØŸ{eÔ=lD&m”Õ¯øCE;È8ÌÈeði€ãÝÝÞþó¥änz¨h²±ÑÛ<Ù¾õ^}vÿÒí·k+×ÂåÙÂâq¼Ä—×C©ÙÞÊíë/ª G€Ú¸Ät´¾¡–V±¾=2Â/YÎŒº<„êÂÃ@!ÂMF`1‡… |nFÊMÏïÜݾó¶ïFê Ùþþ`{¢òZ¢u¥8{G«¬[1ÉŽ n+ƒp¢ô&“˜!ôi:1È •´À⨓ðÐ1O0k„EgÜ$´ÔÀg2 +ÜŽŠÇü\”O÷rÓW*‹×‹ñ‹éBoXX£J…ùúʵk/fö^“J‹t¬ê¢D„ÓZ‹G¤’q".B| Ì% @$–Ÿæ£5«_0¹ƒF÷ 9‹V,°,¦z±Ê"¨#§†lž@¡”H¥«-ÕWnÈåe¡Î/îß}ú!§æ<5Û½’ê\ÑË\¬?梇̰ ÷ŽžfëkçMf7¢ôŽ\\Mv£K0­_¹ùÚÎñ€ð¿H¤²|wëÎw–n|È×,ˆâÆÄ·¿óÃþÚñ˜›{eÌ7áÑÑ!»âÎNø¬¾`¾½©×Ö/˜P $80.ñéÙ`rç“{·ß¸öø»|¬eC$|ðS|O/¯ô6n_ºûA¡ó%ˆM“r}ÒÍœ±ÿå™ñSlÀ ‚ò1 R‡ÿê¼Åâa”doÜJ M@ nÚ1}ÐrÔ-ÀLÚ…†a.NêõXc3ÙÞƒ„2-çsÝÝ|ÿ ²p¸¾rãµïgº\º»zôÖÞ£ô.¿êÅíç?j¬Þ§"xnnþÒãP²5⢌H”ÕçNØ 'ÌËz¥œ(ip“@p¹¨Õ×cÝÝúÊ­òÒ¯v׎ós‡J}]®o°éYT­ f5Gë r±™ž) »BI›#Vwl|yÖœlQk|nÞLÃJ Vë^¾„Je(”‡¸´ f›õÓQ/­¹è°V^inÊÏ]OvvÕ +з’˜¨OïÞe§Å\¿¾ùPiìâ€VÄ:µ|Õ…Æ¥°ÌD;H0ÃFêvD1¸h¥…ô:pJuÞ~vÌkòQ>­¬Ñ‘¦’&l¤­;P~Üî7ºq@ò{wßÙ¿ó¢µrCËõiµdr\HÔ©A:ƒÜNø˜X¼²/-„SÝQvê¢mÔFØ‘0°mž€ÍCk>BJ{Œ’:7á`”J}õæôþ«å…«R~ÁËå?pÑ{ÏßoÌ^ó%ü\ÆŠ¨ãNÀžœÙËzðp²2OØÊ8éä|pÔµ€Ö DÚ~¾ F+7½¸r÷Ê¥ñe›óÓIÌŸÿ¦‹4 ›Ÿo™t±ƒÆ6ÒGň žôQ:¨JñMkuàÊ.X`£—qˆpARNó‰fqf¯±|ƒÖ*ä`MÇ73îü&7ãÀË&ÙXËÊF7À4ÊàM:èqnñ2&DŠ7Ö+×sÓ{|fÖͦÆ+åÅXÓMhŸñb~:ê5?7ú8Ï`é2bÆ­^þÌE×és¶Sç-#&Ì…ˆT@¸8ðü/½b2؉3òW,§G],ب=0aÅ°PÜ=¤"åçÂ…eœ•#ÀÕ—r½KÑæv09²b¬¦•ú¨ Áœ@k9"œSŠË‘Ú`=ƒ—1L…+©p~6Âà{IÝà¤@Œw"åÀCª„œr³Éæf}þpzyo÷æ“æâ^¦³5wð¬³õdåèíþ΃Dm™‹ùX=U™[¿òM´ÅìlyáZwã€Hu9”j“z1’i_½ûÖÎí7¬¸Lö©hWÌ-Íì½ÑÛ{#ZYnL­úåÏ÷NÞ ¦Úzu ØíÒÂþîýwßøÁ/>üÛûêG_m^yõƒïÿÍÝ7 æ…ü<›šÕ*[µ¥{+×>lo=ó %"”*ÁOO|ü¤7è¦u£SsùÙ[|¦ï¡´B{™²´Ö¦§Ñ:0“¸\Ív÷¯¼‰JIVËqÑ*"W2ÝÃÂÜí`fÅIf€ó§c=Bk= %&]„j«Ùþ¦9‹4l#ý\:V]Ç„œ |¡leñvjê2k"uLÎ[‘Á.4vœ7ûH/Fù&åðpŽŽ”Q!J +ì×úÍhy#.Û`qÒ°j"•m.=¥–Ìh؇ `’Nô"ÕK¸Ò€ƒiœá\ÌKéŒZ6¸Ø!#Ñ16RC…" T`#-~ñÌ°D©‡Ðœ˜ à„G-áÁÌ@'‘ðÏa3znº`! ä Ds¸x0^ÕWDD”lB²É%¦%c57'çûHP Éyi•‰¶¸ô,`Càs.+ÅzáäÔ(`'åD+ÄM:©scÎÓ­"œp å\˜ Š"f³í­9@¾û­eTŒññJ}ñHÎuõBWÉv>å%UZ©°j•5|LT+Î&+%ïçsNT:R³¼^Ô =>Ù0ø8½²Ìg¦ƒ‰neö0×^WãÅj½wóäy}a›ÕòµÙKÛwÞ<|òÞµgß¿úôÃÇï,=l-nß{º~ô´Ö?ظþvyùnu夻qR›?Ò««ZG«×_û¤¶tüÊ$4dAý¡T@kÈÕKéÞU8” „b÷ž~È*E£O°Z(Û×ÛÝ­'³‡/J+÷Ù¸|çþó2Ým7›$" ¯Pò‰µ|ïháê»É©'¡ìß|-Ý\q2«Ç\!;óqùLûXtF­T:[Í•[ PãÍuL)Ù:*‘j¬µá—òF/Ũ9à'1! ®…—¼l‚‰ÖKóמý0?³Ò<ïs8nlat±À¢ØÀ94ÖQ!‹‡áTœN¢s…IÎú‚9˜º Ä™R[&¯O´¤l“Š B&Üô`¡/ºà`ÖÐGm$ `ÏNøÏàa > ÀmÄ}jØ5XÇa€OC&T¡cí`¦Ï¦ºt´IjuƒWdŸQǘ3 !B:œ›W+ryWË1©¤jL´l†˜P¬oïéõDë2Âç,à,ìø¨ÉÏË1R28ðscö³£ö‹F°°ŸNûÈÊ$3ÍuÁ…T(^6×èD“s`LâÍK™t"@ ¡¤W•òæ`Ï+\E„‹6üBȬ Q`.Éê5…\Y?领ͰŸÒÌj‰;ñ°Ù‰æ£™&ÁG¹p2[›)N/gZ3j¾MëE6ÕKýds¾¶´`¨ÚÛܺó¢¶zŒ$+çaZs"œÃ‹Gb¥¹Í›Ñê`Õ ÞVR}>Þ!ÕºƒP¬„ +‹[‡nB……J(½h^ên?ê_~Ü\»-¤§É ~ëÞ³¯~ûïû—îù„¼\ÙÐÚW +ëO/?üÁÜîƒTc×3ßþhîÒÉ„'xÁ„S±¥¶“›9^½ö¡R˜—cõ[>ؽÿÁˆ‹÷ñF˜R¹³~²qçõû…r éêâÝ·¯­˜¼A“Ÿ€œlíÍì=»üàãòüM.>½síÍHaáÔˆë•׈•tâ1˜Ëê……ÆÒŸÞ?~ôÞg?CCŸX¥SóT¼Ï¥gößZ¾ù½xcω(Wî¾sxò®˜jƒ1?gôƒÌ2yCn"†Iõ³“øYÙ¼¹íU³—ûÖ+“ß:3ñç/^r™`àyìpx¿|gó¤VC„à\G`ÐÌáR²<¸ŒlpІð^&ÆÄ;ri%?µÛٸå{NLÐó³¡TÏ„–Y»Øa+ap1­R|Üæ! V/lиsÒQ›4–ø¥\tÂ॥ çgR3ûÓ{¯6·îgçoŠ…µ1'a‡F-Ú€•Å5@ú#6Êâ ¡ÁŒVX€‚¦ÊÉ8$3Äš`iÄÁ˜aÑ …‰ÐBTv«—U ó å)!‹PQ‹7äAÅp¬ªfÚ#Ù|>JAø)g/Ooßi,…bu9Õä“m÷Àê””FiÝðH@ò“ò„#pÞ™¿¹…a÷‡Ü˜àDD `!U( ›ýÀèåNx†¬Ì¸_óð%¹´.çƒáÔ›ßùâÖ“÷ÆLèé!Ǹæ_­îæ–ïƒ1Ú1ŒVåDÍI@4|ŒÎ'[Àû姶µ%/£“\¤3·›¨Ì}A,á’ŸKÁlÔæ LX|£‡Í hÎêtYÝ~„‹é­Kô`ÏÔÜè®ÍoÝ3S­$dføÌ¢“ʘ<ŒŸÁ¨É›¤S BÇØT(Ú\€"cŒ£C©\y¾1ÕŒJBv²Åœ°ƒc¦äHnjÜFšôQ'†_pq?Øv”ããõx}MÌ.°Ñn®sYLM;P1__N”çLnð  ì¸nðE\D6 VÁ!MZÜœ ©¥Q p’œa@a! ${©¸ÕN˜8“œIcmA¯CT*“—õfOσú Áƒñ\„Hx³2}8?fôLXI¾ ú<¢bÁMÅ ®€–ïI¹®b J!å,ŒSbšW2`ÄhG .ª¥6/nq#. pL‡sŸ§ÆÁà €ãµ@ƒ©\À€g^†b”R~Þ®Ú$•²Žú˜ÁŠHˆÔ1.ΆÓ>”Ÿ´ÂFàÕ±ð™‹ös#® “B8É+‘R"ß>pìĎƼÁœ›Ž;qÅG†ü&Yl†´ÜéQ÷`]X|:7}X]¹[Z86»Y7"@DØ 3n˜q"À°}”Œ‹)>^ãc5˜Õ>ʈ1ˆÍnÆOêà|AƹpqÌ ¹ý¬ÃC¸}ɽ~£8ˆùTÓv\¶Á!NˆÏ-]^ß¿GË)ªrf6ÝÜÓ‹BuxI³'e÷².T°ûÄy bD¸Š²i + Éõõ££‡ïÛpÑ裘hó…œÀô¢àÁOXáQ#üÞ¤°x9+,½ÁImöq(ï£ÎÇJ³‘Ò¼”éóɧwÐPÖˆ¼VéUW.\“SJnÍËÕ@h™\Üð„ÓhóRœ‚s‘—ΛN_´Ù^2égó."f…€ŤÔL£âÀ"@u-~ÉŽkÀa´ÃüÅ1—ÅÛ;½ç'ÜÀ8 pœ »ÑàȤ+(åÔxÇ‹/çð ^B‡dñÀ +ñò˜ÕTvSQZó¢!« 1Û|0𾩢Ñl³9Ü4ä h>*bƒùqŒîˆÑorQ§Î›‡'¼Nˆ7Úvp½ÞéaŒ6Üác‹S›´˜¶¸?ñàšW R9xqÜyaÄ2nôùahÜ94á±zB$,Dò¼š¥CI70ø—g&O Ù^v\,ƒõY`"¨ t£Ü°ÑóÒ3LSJ9 LªE)¥1+btb&Ý/3Û}N4hh”§JSj¦ÅªyÊ›\ȨÙepjâFCZ¢™lož3ù‡&CcÖ‹ãV¯ŸÀ.¤$Z¢Dˆ<$ÁE +Õ™Xºþp‚aJÔ‚Z¸ a£3`õÐd(Ɇs.44aõíˆÙ…ÛAô0cfÌå%›.NÅŠ? *IËF7 Þè‚X/_°]œðÙ¼ƒS;sÑ8æd¬há‹´ÚFÛ¬V²û(Nˆ5fwrpq¹,%:±ÒB(Ú¶!Ò¤÷S +̨ƒ™u^` `^ÚågŒNdxÂ6arzü´Ý°¸ À€q¥Âe<”‘`ö² p0jhÂWùä2¥ÔmÀ3¸(£0Ú £V³‹‰¸ž‘3«˜ SC¦q£§=½¹¼sÇ‹‡ÇÍɆÚ]ÉêNˆuøƒ ½0jwÁB+`(€>»|”¦`ŒõbœÍã›0˜\Œ1 Z°`fƒûÔ0t¤ÑŽ¿tz|Üà7BCmgÏ™ÎÙÆŒ y9$ Ò¡8ð-Z<·ºsÃO©c&Øl'ªXÜŒÍMû±°ÓËLz„HMŠu¾—Ínúâ„÷Û§ #&Ìî ãyùì¸ã1V7Ø°I+2b„Î8@pº!§£^D²:(¥“Rå•1Ç_›üËWÆN ™Av0R&yAÒÖ._ j¹QjtÓNL„˜8Ê{¨ø(0!ucëÆãïüÝ$ÄýÙKCg†ŒV`#¬")’•Öw¯õÖ®š|Ô… פƒ˜°ávÜGˆ(#’Áp8’Ñó}"”HÕC R¥E7„¡C‡bvˆ´ºa›°¨—! +œ¸ßh6BˆDm9áF&œ>«!C)JjX¼‚ÁÍž›p‚S¶&ðÇýŒfÐêí|& +‹§Ãi­¼hÇ$«—±¸)P Àÿ¥ø'gG­þWFmcVPh88±ûØ1‹‰MišÎÍ`õŒ™½@ÏCÑš^^øæ—ÒOGÒõe)ÙŠd +\V SLôÓ:%¼¸ +†]U#ɺÕI'HÁ‹p^$!¬Åé›´8åhr}ïÈSC­#€¯ÏÎ]°…AI”{T&'ë„ÂVl±º†ÇLçGL&›KàùTBϤã¨ùå6—¡IPÂ,ÆÃã—³8¨s-/}éåQƒ… ‚Œ‰ÛœŒ$5%G²¬”!íðÐ €×žp¿üÊÄ+ç#cv£ÆhÝqã“®¡Qû˜2Ù^LucºÑAŸ½hyåü$JøpéÜ°exÂ3iÅý9$eü¨d´†F¼V9ÈŽaû°Ñgsñ¤&Ì„ P¥4WŠÍšŸ¡m~$¤Æ“…šMú™ Åív{ÜBXMb¢òòùü¨Ýâj•$=N‚#s™øÔ‚O;¡a³Ïâe!BÀ9!Qô¤®'´ÙÙ©wäêý öóãÖó£&gHŠEžaY‡ËÍóœ®+ +5šÇ­6Šz4-˜J«›ëskÛ+õ¥©x)ÎËAZœdxÐí×à™NsB(Ì3’#)3bD/°=c“.ð±L1UïPRTKÕ JrÁ”ËO›èÙ۷ΌΈ]÷€¡3X¼LHË–;Í™9:È5y]ÏJ w²H0â¡äDiŠQ2T8ƒc6Äæ xÑ cHFD©° b¼˜ˆr)”VI*HX4" R˜dØ`(ˆ">õḷRILõ[Tsx)»wC¬lnÆ⤜>B%§‡´X-6«Ñj5sWLEºÅØr¯p¸Ý¹¼Ù{øèÖÒJ_K>ítø­f#ŠØÅ /—`Ú5½7]Ê–ôhR(#;KõãýÅ…¥f>¯æ29¢³"`vúÔÙÑÑq³ÃááHJ¢1¶Dx¨ßLf ZP¢X‘ *2A•Ëf…é†vrcãþÝÝ›G‹·®¯¦òÉ cæ ãv‡òÄ0lˆÁi_=î·Õ¼ KP5Ã/O¥Nn®ƒÇg/Ž÷wï|ý‹OßyïþÜlNÑÈKAttÐ<V2Õ•bAmÔµJ2ÌÓzDÅ)Æ`uXmöÇæ ™J»2³0çéÛÉZËù&lÞ¡1`€^(µécRçF§OŸõC^MQ²ÙL<qyì×` :XÈg.픚­ªöWæÕd¡&7Bð:ÌEøh"y¯×Àà¡kz*•SÂb\aã"¼ÒÏo­õåD!Î/uR·WÝÞzñôÊ“{—Ž7:²Q=.¯Ý™m„Ê@“Œ…!˜Ài®æ¤rVêÖ´í~ìÖfñùÍùŸ]úîÓKüÕ¿ùùžÝÛY_™.•r,ù<ˆÝä÷“F*š~oB +æcRR¦›u¦•lWS 3£ƒõË;ýW—ŸÞß{x÷úîÖn©PR#*M‡}hÄbv#è•‹öQ|æì䙳ã«Õk·ØÊ|¹˜–x†#ÑL*–N'UE×Ô„ÛGœ:7Ödõ{6vð¨1§x7»ÊÝKµã­ú­ýî{Ï?zçÆ;OÖß»×ûÃîÿóo>ü7?|ãÉÁò|G×u%'³aquÆÛŒÁ͸s&_žUÖ —Vòàqe»qëróÉõ™§·¦ß;™ÿÃOÞø÷¿ýîgï\ßÈÄ°Ñl3º`<ˆà¬×ã%}ö² mÔ™Ýr)÷þƒ¥7oO?¿Ù~ÿáÜ/¿¸÷Ï¿ýî/?»ù›Ï®ýóׯÿïÿñÛ¿ýääé­ÅËëµXL÷ø(§›‚UCù;S•7¦"í42•'Ê bº®÷[ñ¹vì`­úö£íßÿößüê×?zÿ½‡÷o®¯®L‰7Lv¿ bÏŽZ^:sÁf˜(·Æz£!ŸDš$ÊZM‡º ¤Æú”öìþæk÷Onm}çÍãŸ|ùÁãG739­3=Õ˜Û鬟PJÁi5…>…Š +ºÙ–ïíÖom•¶ÚüÃÚ/¾|úõ—Ï_Ü]üà~ÿ~òü_ÿÝçúýG?ýÞ•?ýêùO?¹¶=ç.‡Õ>ipúü`@ Ìk‘I[7MÜXMýõ»û_ÿðɧï^}ëN÷ãWçÿî{WþøÓç?ûøÚO?¼ü§ß¼þO?¹ýîê­íÂÊt.ª‰±X,(ÆBáT @*¬§ñÏ晣Åäk×fì–O.¿xÿêo¿zñ§úÛßÿò»¿ùòäþñýÿïÿù§?þ꣓½Ú§¯oýòËÇožlFd~Òè9?ê4:p4rØ­»Éï4°ˆ5wÒýùÔÞbn½«îÎFït>ÿîƒ÷ž\zïÙµ§njjÄæ#=K"HR4ãØr™º>/¿w£ý×o|þÖþÏ?¾÷~öâÿýÓÏÿ‹Àüñoþéwßûè­ãå^6"s’å(¡AÜ›—Ñé4|©EÍÊ·ÓŸ=[ýù§÷¾|ÿÊ'¯o~õÝ+ÿñ«gÿóß~ï¿üîýßýèÞ?|yó7_Üz|<—ŽIŠ&Ä`q¹€Ÿ‰)‰ï%ÈGkꛇ™wŽKŸ¿¶ð÷?8ú¿矾~ýÇß9øÝ×þåçOþé''?ý`çgnýê“ý½³}e-—KéG. òÀýVq¾›-‹Wæ#ß9™þ»/úÚâǯ­üá§oüúˇýâòß¼½õÿãÇÿç¿ý/¿yç_Þþo¿ç‡ï.¶"×tadÔhóx<O’­t°,»J¢u¿CßY‹ÝZ‹>:(½ÿháËww>y¾üóOoý×üü~üì_½þ¯øä§?¸¿1_nu§ô\ËðUOk¡j4°\ÄÁÛß¿ÙúåGWÿþ‹[?|{ë³77úñÍÿò»~õù/_l}ýñÑþõÛ¿ûôÚ§÷kï_Ïo¶BI…ÔȨåô©§Ñ(lͨw§Á]×OöÊ/îÌýü÷ÿå×/þÃÏ^ûÇŸ¼ú_=ýú££¯¿³û·oÌ¿8.îM‡+Q\ °Ï`$8«¹j¥Þo¦›Iª›@ö:ô;·Ú_¼µó“ïþø;—õÅüÙkÿîç¯ÿúó[¿þîοûÑÿü‹Ç¿ÿÑÍO_í½y¼¿SÊÆ%»‹–a" 'äSJNÃ+:ºÝ ßXVN6ô燅OŸ¯üþoO~ýùõ_~rü¿þãÿë?ýíg¯o|öö•{××Q„pzaÀ^ ¡Ýe ™É»ñáNù“§k_¾Øÿêƒ+ÿú›þÏûå?}õÚß½¿÷î½å¥éœ"PõÀ +&CRRt‘&cAßT&x©—¹¾V¸¿•þÑëÿðÃüé>^ûð^¯üì»W>~¾yëRu£_,ä +A9EòI€H€ÅEM¤#‘(‡Öt|££oOEg£o×üÞÞ¯>?ùòÝÃÏ^ßzÿþÊÓýö“ʵåÌ\#œOëAX\8ÁeEµÀ™¸"D"Á£½\èÖFé“'‹?yoç·_Üüû=üã_û—_½ÿ‹Onüò{{úúé¿þòõß~ç󇥓ít'ËÁ^»ÑæãåB4Ñb9• ù0V}Sšëh:ôd¯ðìJõ{–þë¿ýàO¿{ï?yôÏ¿zó_ÿðÑÏpç£gë/î/L·!1ŒPŠ§³uŽ¦• >_‹Ö£H7æݨR·–SO;'»«ùwotýé­Ÿ|çÓ§ëß¹ÛvعÜKLey‰‚œP8™¡ á„×.a¶ŠŠ-Ôôõ^nµ•:˜/ÜÛ)¿v¥ûüÚÜkÇs[Me»ÞljSëÀÑQ8NÏFq6Æ +).IÇI…OˆÔl)º=•½Ô‰<;l|òêÂo>9ø—_<ûéw¾2û½»Ý'ëÑ»sÊÉjv6ä;âóÀ FZ^B›´Àn/†¸í"æÌŠÈ\!¸Õ ï¬èw²Þíýñ'Oþ÷Ÿþú_~ýæï¾|üý'{»ýb=¦× ñNˆt9ìË"¨HgUªžΖ”~fo>{{³òÝ«=Û}~söx­ÜòDøŒÓ˜ÁnöÐJ¢®¬ÈñMK †å£òæüÔþÖ\-Ž_êÊv›¾výxkê`¡Þ»ÒŽVR’*Ò"/ÀÅêÛÕG'p>…RZ¢*Šjò…,!ó ™Ë¨L¿¢ïÌ–ê—ÊkÝL+o¦ãÕ¸¦Š Š FŒ.bÂAÃTÜ‹„,§Ýbñ»\<¨¥S ­ü¥éøõ¥øûwg¾|qéû–~üÁñß;úáóÍÏŸ,ÿÞü‹«ýU¢Ý^—§x«/"^\1Ø ¯OEb¥¸®‘ŽŠê[«K—§´£¹è篭ÿ—¿ÿÞú݇_zç‹·¯<>ꬖzõ¨ ñ´˜äõDE JvBEKÅL)¢"‘՘ȄÙfZ«Ç¥NœÞí%¯­w¶{¹²B¦¥  2Aƒ6âgG|C€™¢˜A\Nòi’˜Œê™h4§«QžÔX"ÆÓ•q{–Ë嚀ÐÝ~Æ ¼ŸdZ g`­ˆÓ…Òd0ËMuW¦ê½¼šÉ…¯,òÁËÓ™vr>/.¤^&<•Q3"c6L:ì>åò8|vØþW§Ç†GÍn›3DzˆÉ«Á8çkÆé~.¸?­?;ê½q}îîVk½UPá¡p†ÏÌ¥ºG0x ŒR$-pA¢¸`PÈ粩˜ׂq••BPˆfyÁOrvœ±ù‡ ^›Ÿ'„”ÑFì˜ÅËŒÜËËÄ’ŸË!3,@±DRä’`msølNÌæ&Ì.bÔ‚ ›' gåÂÌ%h)SA>êñ‘ MXV®Vg›Í…r¹'‡5€^ ùpÖâBNOŽ9p;óó•€TfÃE˜†† ã&?^¦ BTä¨"å“ÊT-¾3WXjF¶g²—æýJªž WZZSlv÷顉IÇ`mûy“屢–!3æcÀÏ¥Š‹ñT“$Ó2Â1Ö·\O\Ý~pcÿÒrº\TyÇ„ÀlÄ©vLw3”ÞN¶’õ½hrš¤ÂÝîÂ¥ý»>ˆšœp¢XH’±X!•®èZš +ðv+êòGƽg/:‡˜ÖŒ^Éàæ¨J³ÇíðÚðˆÁ7f†€3·z SCHÍ 3£Ýé HZ\Æè,ì'c¤R Æ»Oð[gáEÂ0!‡”"%U!\ñA$ˆ´B¶ä2ÛüÑ‹|P½¸B(?n„Î Ùœ¨â§cfìÏ¿=ü­—G/Ž{<þ Ï«‚õùpŸ¦ FâxÂëæ`Oð„À……`Ü`€FL0­5õÆn(Þå¢Ýlw'/ÛQÆK©ŒV”Våü¼VÜ(Ë„"Åæì°Í{fÌ1b§pIÌ0Ñ*|l’¥‹SÛ¸ZñðNz°(ÃMÅM>Ñ„ý˜8iñ6ƒLô:.ýlÆŽ(6T¶@"þۣ͆NzÔÉ»8w ʨåP´Æ*ùp¼ŠqQðRÌ9qÕMFQ±HH%R.r±&«W >FH9G f†%+6ûeW êÄ#çDˆV))ƒ‡â”\’!V*9?Bc$lj:É'† ¶Iä€%Rnr™U'µã¯Œ¹^±¹X•!Õ¦ÑËŽXÑË`O~˜Œ¸v»a–cÙ¦/ ÚÁòd„M;°ˆÓŒPèÔÈ`ûk*¤‚VX…ù’RLÞ  ùDWLö†¨ÑÉø¨!æH­fÃ"¯`…eB,ƒ‘™p2Ãæ€Éêå 6Ú (u'¿hcŒV’bu*Èó…`fN,-Ç›ÁtßNÆ&=œ/ f›[(›_Œu¥Ì¢ZXã³ËÝ©Iäôdò  ýÅ©±ó>BiˆåM¥¼ÎÆzF7fÈaq±6æÛgmg' • "SbfÕ{FHwóç è¸C(Íéç_€3q~Ø1iÅݘÌJy’‹ŒÛ'Mn¯Ÿó ƒûY77æ`&®ÔI iÖà̈lÃ(˜§£m!3P«.*Š·õò +.æ(¹ìrX¤%×. åPn‰µ11›Ÿ9ÆÂU Ìò ëÀና˺ƒYØ–©ýÎú'½`A Þ?”·¢ª‹IzC9¦•R¾µÐƒ½Ä库+xBULî ÅŽGL~qÄAŒ90+ ˆy\éúØ‚ÑI›]4%•±hÅTpüáÒ +¸F>±æ«6äˆ ó9k6XBC³°É¶‹Šù…‚›Ë{ijÌGëFuÑ™}AÁÄ2Ê#<œ$ÄåP±Äè1'kE5G åfòö@ + VÙœ5Àç'ý´:¸e9î"¾ WÈ»…¢'T ô.—ÝTÔAh µG<Â$,Û™ —[%b}.»Ì¥–^™@‡-$ÌabÞ‡xÜÇÌ~ˆ³›Œ[üá!æaS\lŠ‹NYõ¢ŸðˆXE•6«Tl>îü¸÷Ûç­Ãh ÙÍ:pÝè­ˆlö…ܸ2fðYËÆ:t|šˆõðhÏÃå^õûå,•ÃéÞòå'ñæª;Ù¸\ßTdÇ`;÷Hc0Ù&;ͧ»“HhÈMOøe¿P·¯Ä§¯«]Ï`ÛX W¨xŸJÌyø²Nƒñg@eçéxׇ ÐÚ|a›M¯ ò` 2;¡ƒÀ”†PL†Kl¢«”×ôê–V^³srfvzó>ªV|á’ÚÚ+.ß«o<(¯ÜÓš—’ÝCJ®ª‰Nký– ;è-*1.oÇ»Wc«áòšƒ¬9%Ô:"V<Á,ˆOToaJµ:}ÐÛ~‚Éå3&Ø€*|v)Ò8 — ±i $Í kÈÄ¥ÌgösCNfÌÍ[ý’‹Ð½lÆŠG¹Ä4Ÿ™uÒª- Úé ,·À`*@mR}k jö Åî%6Úç{ÑN:Ù”—/ù„:éÁbˆ˜Õ˲j™Ž4Q¡h…% z­#d—Â…•p~iÜÁr# Maá&ø.ƒäsÚŒê|zÁÇfΛ`{ â—J¸ÞÁ"S~©2 ƒF¨±) + NÐÅe™ä,7ÈøŒ_iQÐ:bvqØÂ’·ãq,àó™Ø”ÔL·¡í=Òˆ…ìÜ @6Åü²_¬Ùˆ >î ‚¼º÷ÒÙÉoŸ3½<˜°x@m3±iD,[ÉI'ƒ MòL3㡃t (-p¡£Õm½²ÁÅÆ`©À¦zvFG¥<¦5H„jMÊö“í-:Z‡B˜VsZ@&¼¡¬!´r;“„Ë¡Ô´ ƒá%ô›šKL77Ÿeçn[©ð!z¢Û^¸j$"ÀN Q[$£}¹¶+7/‘:«æ¥|ß/—­B“«D¤)–ùLß+¤¹d»ð¬uéd°mTyLι¹È÷L}­½~碓ÄÂ%½~)Ú>JÎ\´÷ðXÏÎfùt¿¶r7RÝ0x%13ßZ»[Z¸–ëw·ŸPɾ“Iú¾éZ ñ `d iÕÅûÅ…±Þm¡¼í¦RN2tÕªg­ä„+qZŸ£ã |nU.¯Ú ‘PËBaÍ,ñÙDï(¿p{°«O|&Ù>D@`¨X¸€GÚL¢Ç¥ç1}f,v EÛµù#›0xyWL´£Tw‰è öðƒ/-­óñ©ag'SfDwÑ&µ*lázߌÅåôôâî,Ÿ5áì,.Hhy«yéMTªn\yõÅÇ_é¥ÅsVz’1­í_ϯ=JÎÞ¤“³“¤—€::¸³“ˆƒNXq?Zu7”^žp„Ì^”32ûvµÒÃVZtzEm|{Èy~ÂcC$3¢˜ýš ؉¸RÍxœIÌIŵ 77:Ø}Kv’1“ +ë.*顾PQZèt¼u¥¸rÏ/f…TTºXcU¸0OEë­‚k•D÷r(ÓÇ”Š?\ ¦õæ¡Þ:äRs^¾0æçít”It½¡4$1­ƒé}Pž¾šîîÝ&{@Çz–jØ4“™%µJsápzçoƒ*ÞËDÛÉ©ƒüê “]ä­Fçá[_$»Æ€æ7´ú^zúF~î–^_wRÚEtÁ´‡K[IÕ-dŒÕ…¹Ûõ'±ö¦5q¹šêÂrÕɦ‰Øt´sÌ.ã‘®[ +¦gνãTÈ´¤\—It,dÊ,“‘Á|ûÞƽé­ûç@ÚºXT¬e»7ª«OØüŽOª#¡ìÕGË+&¿ŒJ•Úòí+?ºòê'Çdbƈ3ZãÁëŸs±Îc ¤¨øB¸ºŸ™½ÓÙ|-Ö:€‚©Òôn¤87ä Œ˜ +É-¾´[[}5ß?.ö¥îa³ZeÝ«&X!´V >ƒ¨] ~‘Úniöð ­6åò6¡Oá‘&¡5¼l +½[[wÞòè$|z‘ÏÌÓ‰$Õ¼Á‚ƒL¢b5Ù=ÏŸ5Q“‰ILñù¥²m^Ž×J©é™âìѨƒ>k@!pÕR«X¤K€êÆWÜ\é¼…–ãíÝãçœRþ³—Æ.üÀžÓ+éþ­þÁÛ ÖLºC[×Þ%¦ °dDŸÜ±Qµ)”]MNݜݯ³ý*£V–6nd:ëX8Ïè5>=“hn¦»—ÓÓReb•%ð=à‚"J ¤6(‚ °‚4aS³‘ÆÄçÜ\ +;µºïÝNÍßÖºˆR–b­ÞÖ1?c£469£T6ËwVŸ-_{·0w,¤zÅÎvnj‡Ö›Ji%=}U©lÅ[•Û³¯ÉÅ^®µQ›¿I'¦AÜzøœ“‰ƒ/ò‹u.½ˆ†«”V.ö÷0µj!"ÖHÍã@±õ©tÿzuýˆI6ÙJkµµ;ÍWÕöŸÜœÄ"&T¥£ ½¹1î'zÙ'UµL*­Ñ6ðœç͘‡I’ZW‹ˆÞDbsTj)”[só6\¶cb´¼HEªˆâ3½DÿŠ\YQJËZuC+®Ø èa(»\ån07/–6ÉÔ2p‘ú›ìMxƒÉæffæ*•˜a3 @Åó… +Rfnfë>,ä|\ª±tmûÞw¦Ÿ—ïÄ:‡\¼ €zqëÎþý °ì cBj&3}”ž9NÍ&¦¯Ú‰ãçÙ®B\"Ñ;,/Ÿî${׳ó'p¸yÑÆÁ¡œ•ƒéY.»@§gýÊ`p@EŽâœ‘´¡:È,ˆ/Ÿ5øΛá³FÌN$©Pš¬Öšt±/Ù€'9åã+.6ç '}€§´é­'áLßèídD‚‹×íbR°X¦çµÊÈe,”îôwÔ´3 Ój5ZÝj¯t·¥¦ö¸ÔÎ0Ìö¯NxX;¥‘ѦVÙI÷nFš{Z}G*.“J5R^Â&§Ãµ±¶I€ôKzi£ºú€MO;h=Ù¹¬•WK3{Kûê«wBéiJ.5göKý=TÌk…ùåÃçóo´6ÕÖÄ:—i5½±wùòc¡š1Í/•z Ä@¸²M§ ¡¬åúÛw^™0Šrq•I/rùµØÔÕÙ«/ê«'bjvnçQié†_-…JËDjR§¨Ô²TÚw®¢j3Û7Þ,ÏV;’qPöJUX©ù‚¹hy#ÕØž^½Zž»âkÈ -uÛR’Š¥--ÞÈ&­Ë…Y­¾!•–b­KB~81`~ÔˆÒe ®!r)\YךûZëJª7Ò< c=P€6ŽŸÍïïWæjÐ`·¸k^–ƒ‰p°þ`ª¾xÜ\<–2@íÛd´Í§¦£å……ÝÇS«·L~‰Öå¹£Úú}®°2Xð®wp¥±²÷HÏõ¬¾à`Åkk+R]ã“=6>«Ýq¿vÑ „KvLÉõ¯öß*n>–»J}[®n!joØΓáj¹·ò«Ÿuá²Ù¯ø˜0lˆXµÂÚ+ãÅ/áR͆ëçLƒOÁ¤*µØä2€SPôh»™¤#7Ãa03Ø@ÞËYÍÌPÖró\¢ ‡‹6TÄù4°R|fF)/j lq•ˆO½2á&„V²³Ÿè\ ĺî`z xr.kï’J &3Óûåå;`ü›ë¯¦ú×ÐHÍNj¹™/,FéUDšÓP óZy •‹Jb5)Y‰–g¤ü,Èn8\&Âi-?ÃÅší8"À[¬¤b!e2Þe…ܲ”›ç3].Ñraíè­©½çÁü’_RªèuR*´ç¸Ùt¬{5Ò:ËÛJû˜L.ƒ(ûÿÙ{¯ÉÒ,?ì]Ð<Ôrvº»ªÒ†·7âzï½ ï½ÉH﫲|UwWwuO÷ÌìöìÎ g¹$—¹ !¬ÊAôèEÿtnHPzÒƒ%ˆü::+#2âÆgÎù™7¾C9ÍÁÙ‹Ó9xñ(ÏPÖ$öéÇßzó—Þøim|==|ýÏÿÍÿøÓ?û÷©¨=68´Æ¯–¯þxùã?L^üìꋯþêïþáím^ç„ߺb›7\ózpõ—Gßü}óèCUªû£Kwxùiwß!ˆUÊËѦèôõ?¹þò¯ë³›ÃÛ_ºý 1ZËõÊÚËq!0 (Ïþñ·fûØ€Ûà‚‹Ëï7w?…³—Œ3Üiwõ"_²ÎX=¿©},߇V„zU¨ÕÏÁc¦ªJž²9©ôŸ«Áò=çÏ)£7:wüö/€Mv*jžõÙ`U?ø¦6}oÍÆ&‡´Ô§·¤6È`DÑbdÉÐÍÅ—ý£•Úa{ñ¼I$4‹”WdêIÌ)± ¥qL_“ú³Ò<€¼+ y˜y±v¨t.p{Š¨Êì¨õE÷¤Ú¢¾~ÌŸ{£;Ðc‹_³MQüÙSstUBÊ»t D+À=³sáïÀªãrmxòÕàìC´xnô/…æj÷ gÍ_ËãFŸ2ûJc£56ém{uçŽ.³¬ËÛ=tJ‘ìuÖ¼LûèkX!\±þ d¤Ú:“‡yÖTê“pu7<ÿvýâwÓ§?åXa,«>Ÿ]Œ}«ï!ƒÓ¯F_ Ï¿$ºS½u +X½W‘ ‚ÉÓó·…㯿àÁýªâô.»—ÂäŠÙý“ÎâfrøüÙ—¿mL.¹möžÚƒfç +lXÊ_{ýëÕí'/ÿ¢ª´«jß™½kœý…·üÊè]™ƒ[Ú[‚a?õÛÞúØDéÒî¼³ùpüæŸÎo~½o-ž‚‹øÛý?-.¾«ù¶~o«Þ>ý6œ½ µÞáÅ×ÿð¿üïßþ“ÿ»ÄJûÒŸ½kW_½qúçvsóôí_?ÿî_±þ5º¸;ç£#³{-5ÎhwEèÃhp¶ºýÈxÌœ¡ú8ÏƧ­Ä`ÖÙ¼Ú¼ø ®6Eo¢×¼Œâ×7?°ö4K€˜WÍÚÁÑÓßpÖd;'&+&¨µñù¯Ë÷`ýp}tÏþÉ¿úï_~øý“¹•ŠòvֵɫÆäð¦h÷Íþ%¸$˜ULª›#Λ ÍSÜ]òѦ¶ø2˜¾ fÏÁð‚‡uWZ‡bí€t&„=á¢Àíå‹¿Òûç{¨Â¹3@6\DËŒ;Áµ.atyoÔ"õ.ª´Áê¶oú‡›«` +Jߟ½’ꀑ²Áó+ŒÛ_ß~ý7“Ëoh£ ð¶wtfꮪõ*ªñôôëÎü9`£ÖÚØã»öá׋»ßF«/‹b'‹Ù~ûxvñ5d1gϤÆ1˜50†Áøéèìc8º!äÆéí÷‹ãןvrvq½>kþì/Ö/þš2Ç€~%:`µ£ös¸‹pQŽò0½§5¬ÖAcxÂÆß&3µÖy´úáj¹ªš*ðUÊ®YEXÚûã+£ æ+)®¯t1¸øØ1£wYdý +ëÇÛ¹+ «wþ§îõNœ€t¼úAjlU ©9aö1³/¶p{Pžõ¦ÑìZÖô ξq§Ï¬Ñµ5¾ +Oµþ9fôÀþ›Ãë I´–§jûœ 6˜1*É-ÈÐÁáëhþ¬¢uhà}¥G™#`d@{̈wŒ7{ó§¿.‹uèØøìC{ó%È ³sNžA<—ņÌ´Ú2ÞŽOé(Íc¥{&±ª6I³EêM¯sìt²¸œFE°™»%U´ÎøŽµ†‚Z;<}½¹ù€‹õ'l¸¤¼9°•Ö8’‚ež²´pŽ/­î¡×‡nŸVôIžWÞî­ßYýó®UÅ€T;:䜅־ÂíY–tPŸïNy+C‰€\1Z'‹ëŸf׿U»O·JZ†ô8•ÆŒ"í(—PZ .¬Æa8¹Ó$ƒI}rÉZmL +r0ÇjóØݘ½KóŠ4ç·íƒ·°šr딫§™.÷@iÍLjÊîxtþ„+¼´iŽâý¾(çìéw‹£¢ÕRÂq4>ïl^No¿ï¾Ë’>¸E`CÁœHöd+ͧ*&ç/‚Ù‹Áé·Vç´Â…)DÃÕn8½3;g“ÄnŽA: 7³wÝ:üÞš~iÁŒªR ¸=Þ’f/F¡`…YÓ’:€ðƒe™±£éÓhñÈ4˜½j½W›¤õVUŽ€Ýº§ß .~°ú7°ÐÎàRˆb0îo^ϾAµfEmTõ&ðûå×ûüÇ¿ŸÞýVÞ–ùLh¸z‹ê=€bDêv¹ƒëë÷˜]~íÏç±0›¹ tÀñÛ‡ß oÿ +D‹hwûëgnG„1„ âƒ•Ñº„Ápí•DÖ™ˆÑKÓ9-« ã`J³»ze5·2è“t%©`À«ê³§½ËÊ(¨{ +-‰éRãDë]›£ êªðrÂɃí9xMÎÊœ]‘Â<_#Í©7ziönaY¸ ¹æأݲ’§̘ +µÓÆâ}ÿðÛæòõvA¤”öàð Äv7ò”[``I¥uAû+Êæ1uqúUgq‡Q™‡(jå)?…’?óûç骴W`þ´ÿ3¹_–Z):.ôŽ®£ÑM™ §×?€ÚŒÖo­þ5ïÌé&Ë"#G^{ 1_f<ÆŠwþñGÏbLÃ"evgsó¡9>áôšMËr‡ ¼Ñ3)8 õá~žK#JcrÓ?z³¨YÌJ”Õí¬¡ënþu•¯å1M¿í5&Äf¶jÈÑ<æZ‘Z„Þì‚Tz‚d„Ö9W»7\ëšHgZB€ \m—(Ѽ¿ÒûOÝù—Z÷²(ziBÆAE7’7â‰Z߀wÇÏg‡—?rÑ&Gí×fϺGïŽ_ýtúæ·ÁüŽ ½Ó/gw¿¶&O÷0 ˆ>œ¼Vê .äæÑëN/¿¯ˆ X«YÛ|l}r‹ pcT•›¬ÙƒwD—ëjýØŸ¼˜]ÿzxòQo#R=QU9wÎÂÍŸr‚7ÏúP¥›#ÝŠXƒØ.Ю3¸íÿ²{þ±ù«`õuÕœ‚µq{ÇãóöàŠ€nžpáFk_ÀËãêrJ½9:}õÝßÒö8I8`ÃoIÙoüŸ½žITTBíÍ3Á?üµÑ¹Ÿ¾WQŸ¤V·Mc½qE¬Wù:.·å`IY#ð§IÜBXw}ý¡6½*pžÞ:n¬ß{ãgzëN¢æã¹[àeo²“¥i£NŸ/~xùÛÿaýúVïb7G£”>:zãôN!2*ó”"M%£´ˆoÈãLrT þ8ËBÐÂ_½­¸]Zqd¯#ù# >˜œ$"¾Wýó­rѵ+{ã<©¦ª"¡¶ó„hCäè:û‰U›ŒVO•˜ý “,Ȭª)+Þ+£~bî@‚X-ñ5>XBvBÊpÁJ®“—áâÖ;Ï1þVžGø¡u{@9#½sæŒnÅÚš4ÚŒ+a!yJ4_Ù‰?£ùýèæ7c”¨Ê¤ìšáÀªµúLªÍÁAŒÎ¿¿úþßt¯¿gš‡¨Þ×ëkÀS „hÝ?ùøüû{ûñß ®þ*‹ÛUÆu:Gjë˜4{RýD¨5×߸ƒ;Lí”Hݬ¯Œæ‘èOãÊ#j/Q1s´/Ô:Çá-L»GoÜñ5"7Ù0m ¶.jëþ ìL[Fǯÿø³‡tt t.èh#Ô6Ó«ï:¯i³ëu»Ç_këÝ[ VÙ_ªõƒÎòÅìüÆìrö0š¾ñŒoõö9®€ÚöË +„.sà„>¼©Ý¿á¥V? §Cx£D«,fVÅ&eÏôÞm°xÛ\IÛÀRRïŒÏ¿£ªv){ž¡k0¢ãC„pfw/G+Þ(쟥7¼&íQ +7Rˆ¨yC»¾ÎÇ_š Íö™¬9{ŠJ­2ßØ+É{EA¯ðîƒïK“¦,Á>ŒŽ|~ÒÆ·9>J“>eN‹Œ[ tÚhuÞ~õwýËŸ„úa*þr¢I+>c¶X·¯µ­Ñ 8)¡~˜1j”â¢Í˜m¹¾RkF`ÕáµØ»#e´(½Q‚˜Ó§7'/~:|õcôªÄ5­Ã;Ê¢rÓ>5Ú— a¼é¹q^â%Ò2¢9oµe¦Ì¹b°B¸&mŽ¬á5ä2iô§ßYßÑV¿ÌG¼¿”krßÖ¿a 뮣Öê…Ò8ȳžÒ>\ýxðúñ'’§ß Á˳‘·6ß¿ú뫯þéâùoýÙ]…óÃÎáé‹ß´Ï +Œ“%í,aClë#Ñš0J+SáÝÖfxü²•2'€E6âÜe|¡Eû¤ Â…ÝåëÞÑ×róT¨Æ([°Ž¼ Zâ ÐëMë/!¤ñ8ÙE6€I8^o +f“3éS­u +î©*ÔywV ¼'9®È„_OU4\jIþŠµçD¼§t´…ÊtÕ.(pa•¯Ã›Â,AšøƒKoxSâ›ÿx»B«ÌA@j)Ì-ð¢ÐuÑY½²[ÇexéárE'uR%y¿×…Wøz’>K‘ûe¡Hê`•æimþæâý¿8û +„ÐÍþm†´šÔÚR +æBã„o]Èðzç?Åõ†8ÛhÌôæP‹ŒŽ|{uœÆépýÜ"x¤^§18¸Éåáôy–ÒUS#¥y¤µŽ­à/ûãîæMÿê±s†ÛcDl’ƈÖ¨B9ýS{ü:n¼d×Yk ·/h{×´GÎè•3y'ÖÎi£Ÿ(³_¤*ï‹á&‹»Òg­±Þ8ý™ÝÞXÍ5à‡¼?yøS±~ìM^×WäÖÌm†Ð3¨xúâ§æò $«÷ b‘»´=‰¦Ï«‡kÆâÕàüãðøMgýJ¨­ R´„Ý:jMoõÚ2ˆEÚÍ®w–¬[Yn§(Zuwý¢Ä8©x+_×Ó‹f×?IÑQ²¢Óz÷üÕoggß”…:eM€7íÞµ=¸ÑZ'ñÕˆl¶Âé ¢4ŸÅaÆDÜó(©Î)53­o¿Ó;'9®ö¤¤âæ¸,ue`ïr¸•È1V´lÏ^B<§ªÆ£$¹•e± ࡵ[«Bd÷Îk«·½¬;#´AŽò8ˆÏ>ë¬rDÀY}ðÔ[¾QÀCÉm„ö3=Q”%sR\$ +Ì/vÊ­(µWf}„óØ¢^ž @oĨî êó»þñ»þÉûÁÙ×Îè +T"e äÚ„=ÜZë÷‹g¿^þ¦qô è\­1N·ªÖ”æ‰5z-´žêý7ƒï†G­ÎyŽ6 µ.EsÂhãj´ª´´Æfóüwg_ýkà”dU)’†àöIùp¥npgV[}Ý=ýñV¨X®ïüþIQ¨å¹(ÏÔPô¼sð¡¹ùŽ²æ0çeôÛ\ªŸGË2~™¯Wäv0¹›=ý©v@j‹ÞŒ´Æ°^e± ¹ÀØ1þBe7YAŒ™ý›Áñכ׿'ÕNYM¢¨§}TbÀhûRíмۼúÛÕóß3þ"…Š $ÛË»öò&†,}|íÊ;göž´¦ j¶ l\?¥¶N”¤Ý‚9‚Ågo‚ùÆžV…0QÊ´%¹ƒ¥Ux•BÁ] +ÞšµÇ¨Xý"ÔáO„ Ò«›¡bä ND´¡Ek nj³Wöð¶*¶*ŒKðžâõU¯‹ò–`õšãÛh|Í^Ñ!jŒŸäÄtY ãKªê‰’˜Fj}­~¤·Î0©½W”?ßÇy«ßY<¥´ÆNžÿ"Ŧ©ˆ©Ù«ï›'ÆA•£öºøÜâÏw°Ý¢’ |Ôœ ­[»w—ÇÌÇ Lõ&«‹ïŸ$©Ÿo#ðÒš›ƒ› +AcJ2ºj}i4j4½.‡¥x+­šÛ9 §ÏÔÆš2:ðg +v€ˆu@up ˜¯ÚŠ0»R´6ºWRóÒŸsÁ†4KñéÁ¦Ü8²ÏÀPCô‚aÃ%ïNýÑ_;HP^5H­ëôΔh o[G™ð€òˆÖKÓnæÁ™`æ0‰™b°‘Œ»"´!hãìW@ŸÔq¥ ê"]‘J zMoœKá‘m s³¤GƒŠÔN.aÏhÿ@m]‚ƒ+‰Íaìi!Îêðz‡6lxýwÆwIÂ|œe2¸ Ë]•Û¸Þ¢ í­€Ð;ë·Áä¹\_R ìmÀ—eèp¯¢h_«;cJo'ÏŽÂô±ÞZkß$q7OûŒ„Òí~áãݪ˜ù¶¬ÀAR¤ªfUî€ãÈÑn¢"ÆXmž××߀f(0ÞV–ÉUÔƒ³¯ó*ëæq³Èx˜áB·t¢9}¾ºþãßý{Ùé¦Q#YŒ|xŠÊ1 æQ)[ÌÖFpGYÌ(ÑAºj€LÅ}Rªg¡€i”1aƒ.8Dµé~QLùlE”¬hòÏd²b˜ÝøŠ,wü\j\ìU_ì’ ÙxslÔ3¨î´O´Öc/‹Ll» ›½%z·wÆxóÑ/’±OÁ¥š.Íö©7¸ÔR0•Â!"úqù¼ŠÊC4b8§­e´xo$† ¹y‚É=Bí‰áÚì_Mn~3»û5º+r><±"ÔbS&¶ÊÊ )ÞÑkx©Ö×vçÔyÌ#Ú€K5|©to€³J°@ö”ûtQhQˆ™dÕDø¶Ú8¥­áV +KXÁf ‡µ¨2Lá~† 0µ/„›²Ô%&ÔNhÃ…G.ú,Ï}žgR¸ÆYÃ*ç‚o47 rÔîW;Fô~’0@qaJ p¬*6Œö…?}ã Ÿ-oþÒÞBHÈVϨ/’q‘Êb›¶çNÿvrþ£T?/²õý’\¤œaí—AÀ79Mƒto_‚Â/PÎNŽÍâÀ8¦µÁ–¥&fŒ@Ã+õ3»wK˜ã'%8¬! +JX©šÝË$î€4õA½Î(b®ª‚È¢Z¢($KJ ×-oÐœöç—©2¿_âzjG®go·(ÿb§T¦m7Ö¨'»YŽR;¸Ò…$¥ô>Dד$ñÅN™’j|\M/Jcvžð +„“G  ¿*™`'/q樻~*,aÂÝídG‡6§ðs§ „Ýãïÿ^û䋵•Óp}Ê$OheÖy ´KÈMˆjèÄ¥6ã}z¾ÍEo +’Àl8ýk¥qŸraÀ¨T+1v6#­ Š¨Ä…¤ +Zî0œ=o¬^…³§b´ˆ7a3:9Ò‚Ø.m AÄ‚7‘뇄#måiUÚõƒoìÉ;sôVëÅÕ½ *š4º@â¸Øp/ŒîHµÖ[ÆÆf¨ +å(þ$O:jíÄhß2îPTïÜÄqev­Æ!ˆ–ŠÜ£¼MIæÜì\YíË"aÓb¸ºxÏ:#L´Hxqåz£}6³Äy{eAòçΧs’l˜ÖËÄ>ˆ+Ãxå0ɸ’`Jè#€Ó"Wß))Ô!´‘ÙºƒåH#ÜÍŒHMÜ=ÐIãú¢æ$œ½v‡7„Òzúáo6wAX³ª2 ìi^hìcVŽö =38€¡©¶Î7ÞIJJû@µ@a[ž·&6„„­­Æ‰U?bŒ„úNûTÚ^ßÎ`{Yf¿¬UÕ!ôLHOÒÌ`ý +øt+'îd °ŠO†€ò,³ž]›½ÿñ_­®>T¤FU‘Þ’pfU5¾Àh)ŸRNÿD¹0êà7!ÞöJÊ“œTzZç7'VXàè.Ì*©4°²„ .Øè\F³×)DËa6ÜP¾¥„GÌe*¹•Du1˜#\ðç»Õ$¢W„†è-”pÉY£lg¤ý’ÉX³’Pg¼šà ³¦i&€P‰OG[c!XõsöBk^bÚ°ÌE˜\§­mêó·VÿiEê¹vŽi"B7Qµ²¡Œ_‘ê„Þÿ €XÇu º ¢Î>j@„fÞ¥"¦«qÐ +ëbRÄùcµ}ÜX¾õ&/@Û RÖ;œÑ®/º¯Ù«úCæ´ÚªpˆRÑêÑ€ZHkµªT%`ä9„J…õRe‰Õ;Vc#9cRms Ø´½’ººš'OÛ¬Õƒ¼ÐšGL\ËlÊ™±‡jcSëàÇÅXÀ¿4ú7BtB;+àˆåƒ=`,€®v–\p Ɉªm©~$?{R´Úç¬=žÚ/‹ ãÁéw?ôŽ¾­È­"ëyƒ+HŠ2ç*HTµ4f@Ì N?ÌŸÿ•Ü<µ^}ñTb²´›!=·¢Bk]­¾Lò•qq(s +îxa¯¢íø +Jg7Kƒº{XÕF¤½Ô:„ѵ¶“g!¹à…ɲÂ{´³!´ *DÃèÝ,µŸŠ¤÷(IHR&£çÖðùáÛöü7ÿ]0zõ(É0«€Û;9èx¯_fOêÃÚôecù†s§ D~%‡E¦¶Ú **J RŽV7?~÷7ÿs\•Ø. -˜Z½g¼·âøT­£†ÊíŠØâPSÇ .>§Á¹ðø°XÚi2f‘j—rí¥õr„ÉÂ;#)X‚ŠK" +¨¸2’ÒÇÔ^ >ßE¶³ÌVžßE´jA´3~fX;ƒ[\jePS­:¸ÔF‘rÓõÑ~ùq¢ +aƒK=QÈ8oöjñêóëë/ÕæEªê~¶G L,´'©é¿è­ÓÆüúX2n~ñK£qÚ¬À·Øöm÷âÇ»_ýýW¿ýû›÷¿ËÒ^Œ˜ÚÃÕ>ç-J|4³ˆÁšó¸ÑOãö~ÕN™ +mÒ¤@œdh·¢¶‹Ÿœ~‘‹OÇq(4(}@êÝëƒôŤ†Ußt–¯ 3”Oê½a‚4•e,hk r0äë„ÜÄ(ƒlSZ¸1©-ßE«·°ÄnÎ+Pf‰?­(±A‘­a +˜…#Ñ_ÿ>ÎÑ»šT¨•ø×G`!ÍîÓ`öŒ(YwÒóf=®4¡wp{Dû+!:b½EŽqó§ró¸¹ù2\¿'yEd«ÃŸ0­‰ª ²²Økǵõ·õõ·[Y*Q–Q©‰ðqùBÐ]ˆÔe‚#TéÕ´ÿbMØ<¦¦ÊB%þlqH#Dj–“£yY 8gàO^½§z\ørJYðÑ%±&3¯ž%-ÌZë\¬­Ak•˜(YÖ¶óLUý ¸ùùv¹D»¢7ãìYžmì"ú.¢î ôG”I‰ A‡PƘóÖ´5ƒuÙ+‰»ùvždèt\LÙË€JÿTæŒRGJt’¥|>XJµ 0;˜ÓGy~»hTägÌæÚl®vó0&,ënIÙ)i¨Öå‚9¬õæ`ð5h9­}¥7Ž%wL‹µ4"…6Ä6(ê$nïV4¸eHWòÎà©=~Á·Î+ú$ £_'} \íŒ iw ¼ ÑX#iÜú´ãh“n•[°š9ðÁ‚E‡$—«]ÊEˆL"V¶ÍÅûÕ«Þ<ÿª à E6@¥  · ÛA|­ï ®¶©‚…õK8`¨½1©)L +Á™Ô¸Õ;·U¥»_ AB‚G“Wîà98í}ÖY‚aá¬ÁvIãMïüÙ[Df™,ªà3|ªÖVkW¥š,&ç߯ïþŠ æÖKÓçM8•¡¼" €Ö7zwÖèµX;Ë3á“,™DD„dâ+x¹&¤-ån0cbO´‡)DÆ¥˜ýÁñ[ñ’t7eú©——µ%Ö-Ð`gt9\5¾j~hm> r„.è¢$‡ÓvŽM–á9¡Ì>LUäí ½_ °«B틶“€)xwÎ_fÙàIßcHÚ%ÆJÇ—ô({%ùQŠ-ÒPäz‘ôq©C[ñ•ñgˆ¨“£ë€~fïFú8ËaJ×Þ9£—Rí¨H; o· +Ì>"Pr+_ѾØò¸ë ®×Ï ç7U()þèQ«5ƒ†çÃÃpñ®{ôCIìT•P©ôÓ”·‹(»e“šµñÝàä—£«ß«IÜÝ)‚ tìV’Z}ÈG§¥ZçLH2HŸ’ +aY•:rtR –2QÕ}ö9ý'¼úô]@*©„ÚQ£µÝ¹B)° ðÝþð¹Bª6Š ˆÀ¾Ö9Áœ)îÎŒÁÓÁÍï¬Ñ«,È¢°_äJ”%9s£yIš.ÜÀX`q% ² t~ª +³ªC¯¬Þ¥­¡'˜:¨Ê})XSF?ÏÕŠb›uWõõ‡hù%U•¸cRk‰þØjD‹;¾vÌ_wN¾íœþ’ñ7¹)nï*œ½Á¬EYÐÖ„g¤@šxÉ +_æ`õêë¯çÏÿ(¶.b¶›¬5a¬eµŒÞ‘P?#Œ%ªÄgHJ\´W’`,yÜ$äšÎ@°Iµcù¬5cIªblåØ,f"l-ƒ÷˜@¼¿dÌ(íG)òÛ%NOò™°ÀF\°öFOëó×¼;MUø©Y͵Ý:ýlð{›‘#0¹r°w§È“úð_©ŸÆe—u‘m¥QOòÁe§1ëQ’Ì“.„GIìýeˆ`¯¨Í²õt|íJ¸ļ=>…i-ÇØcÈGðJIÌ›Oè{ù`†tÚë×õå- ªz§ªŽ=j§U1¤´V +×’¨,Ä—ûˆ–¬ÔNW*|€j Înξ–š‡ QµH¸š!1ZÑÎ,ÇÀàñ¥_µCµsź‹<ã‚mDxOðFr8àœ.ˆ=Ê]vO~­÷î ·¨ÔÎbçÏQ­cýª“ÙõóïÌæ«5q!y_[½ÅÌ $TŽ®ÍáüƒÆêÙ=I£Z¶j”™:'d+nÏ3L ’e·$Z[¯¯ +œO;KÂZñÁqÿøG\í¡b+QV)Á š¤Nüõ1¾IKÒZ Á!ÉgûX‘isê}¯bVÔ7iÎÞù³÷ K¥™G)èÌa ò|£zØž+õs{Ríx>É2@4Àª`ýrcµ3<1XÑA™ò³X¼iÂ×qu(ë{%K@„4cwCiæ)÷q†˜¼Ü9e *B-‡Û^ç‘Û;¸žæ‚düµî&jÌHca´.Aì‘Z‡H0)ÌÛ.ê &¾ðFï]V´.(OÒÓ΢6·"×Gwy®¹Zy6,ÄDU¹=dƒ3¾vÍú‡pœ2ç“*`iÏh_¯þÒß1Á*Oû{¢,%7R¸rżŠäÖQ1ÞB}R}er\°‹YÚëñ—ª0µùy’x’e!+l=þ¨Q¨g¸f åÖfŸd„ofp7ƒyU©U•›ðrÜ›ý»Š6Ú*+)HêƱֹ?¨ÔV¢xâJÎÀjÒ΄ñæ ÜÝ*ó²j@–gHcUôÖáàôýòÙ¯ÌþEÓ@EäGö^ÿ2þShjS«{_£^;Ú) ?ßÊeP2G59¾ÎFçæè]¸Ž+p‘z?G:ÒTŽ0!H@Ôe {;Ç?N3à¸Á°ðÞ<¾\üÁõ§KÄ}Ôšx\Rai „”Ú¡,á­)k”ãÂßÙÅÃݪ‡ió`öÖéi·H9zçt|ñËÃWãŸÁ2måÅ-@?®.{ƒ¶ggþ]ëì7„³­¾›—s ŸCÎþWžþÅV»½§^ÿFŽ ŸÀ80”ÙO¡f +sA*`@ÙW—ßþ7zç)ã/Ó «ùz–­„¢Ž1}¹Sæ[Zý€¶:LJŠ£n•ëŠÞQký%,ö ÃhnfW¿*ËmTïWônš«)ƒlã‚ÂèÇU¡¹7§ÑâcãàG½{‹ @6±Ñ؈Ñ"I…ÛU¯$ôäèʾ£³’’¬øK‘“7FÿyYëíÖ~U#åv–´Ÿä˜­SfC½}c^1Þ*‰s\öáö$Çg©€ò–öøåøú÷­ÓùÆYŠ‚ ”¥—@Œ­¬¸_¬•„–Ҽܼùƒ6"ì¡;|j ž áaI¨í¢ú.j0æX«™<ËB ×vïÆê?+Šýø< ÿ ô."5ŠB­À7À2 J—P:±BÔ" ñÆñæp7¨àâKî¤"7k$5ÎÝé³›%ý휸[3¨™,K9\+àúN†ÞÎrW“ƒ1Ü€ûÛ-rfãÄüNN`ìUïò7JçŠs7¤>‰çÄŸÖ‡ÇEJýÅ~¥ªO™ðܼi¬ÉõI¬¬žþŠ4:{ˆúó¹[1øðÔ²×õŽ?ÖêàtÐ `¤’4 ycùõèò/K\ã³m4…èN¸5†°” ˜“ck„³wE¨Ýîx“—›×ÿ¢uø½Ò¾Þ«úÿÇ&müÿwþ¿j¹oía ÷­= ä¾µ‡Ü·ö0ûÖrßÚÃ@î[{È}k¹oía ÷­= ä¾µ‡Ü·ö0ûÖrßÚÃ@î[{È}k¹oía ÷­= ä¾µ‡Ü·ö0ûÖrßÚÃ@î[{È}k¹oía ÷­= ä¾µ‡Ü·ö0ûÖrßÚÃ@î[{È}k¹oía ÷­= ä¾µ‡Ü·ö0ûÖrßÚÃ@î[{È}k¹oía ÷­= ä¾µ‡Ü·ö0ûÖrßÚÃ@î[{È}k¹oía ÷­= ä¾µñÿöŸIó¶þ3iÿåþ¾z2W&W“ŸµŸí‹&:‚ûÑäòjqñ3ïgû£¢xq¥Ì®NO&϶(x¨T@ª[Åh19ÚJýé™[ð¤-ÿâ`upÖf“£Ez+O¥áÿŸ5âãñq¥‹ëËu0¹‚—œüéAi¯ù¿=œjœœLŽó­OnÁÃ[‚¦VÚáÿöíÏ®áŸòVéÓígpÇ‚_6ðÐíV¹´ånõ¥­yüÔègy'Ñ-œ ЭãøN+àØŸî;ÿéýʧÿøüÿçýOÏ?w>ÿï]ŽßûÓ|iG êO¿~Íÿ5y©tÜ?ýgEeqs0[Dº´Õö~ÖÛÊš¾OÝùOzW +/c[ƒ-¤T†Á`ñÖÿo_ÿù?¼î?üKlU §eüS§«[ŸŽñ³ýýOýŒÚÂ*$RþYÛü/*b#KÚyÆÅ”®up¹UšE®…jcš¢Jájªõ-z ÚF¿À…ÊÉqõ D¬WÅF‘r(µÉÚ9ZrÁ‚´g¸5­êcÚ=ÈRî^‘ÙË“9TA¥íŒøð€ 7¤»¬šÃ¼%qS +&Œ3(ó5ÖYÊёֺČq™oãö‘»y:Hc&"µ1}HC¹qT”Û[%).Ô,Ô¡ó©ª^bBΙê­36Xbæ MÙ%!*ðªÕ„VU›JëTë=5'oÙúiIngX?M9jãÄ?W{WJëÜìß1ÁÑû)ÊÁ´^EnåÕöKb4ʬƒ+uTiî•Ex/Dh¥H7MzY&¨(èXUn‘z¿*·ËbÇîܪ‹’ÔK’înEÛ¯êYÊýkÓ¸žF5Z²ö“»ÌI"Zr½ïv/áKŒOªýDY}’a2˜#½<éå¨×ǘ1©ªÃŠÒßA´$jå© ÄÖÊlPp¨<éW„v†p÷f,O‡YÒÍcŒ8kZâjy*Ü«š_d˜Gf§(îÅ2í¢|„Ábf‘‹¤µ“£?O¢;%!…ðò áU„Â5 +t˜¬è° +¨ÚÍQîNŽÉ Æ~YÝÉ *@å~EèâÊ2§´9jNž­ƒ4*ÙmÍÄÚãNR¨ž*Á[øˆÓ5.I$î&«®tåp-º³2å)á +×»YÆKâF‘ IsJY3L¬Kn?ˆûe«¡QÎAUŸ¢Æ¬ªôâ +ÞB-]UÊ|ȇkcp#6NÖq˜á`™‡·No‹\PdÎßØ£wjûªrl”‰ëdÚYÂJãF+u”Å&ü)ÏÖóLm¿¢íãâ{ÂŒ‹ií•¥6ç¯ÍþóÚìõèüÛ¢ägh“qgJçÊ¿W{OQ{ŽcB¬.~Ý9újIÌàƒÃpñÁ¾FÍ9jN \ f2Oy%&€ƒªÃÅu/Ib+Çîäù¸¬Ô†Ù.«Ý<ß(ðÍ×,Š²ÜL~*„êã4׌Mãv·’Õ¸Y7+|X 4¦'5[5sð'Dƒ@*Ó5¿}a5O!®¶Óô~QÚβ’dQóER0feqPâ:Œ±â¬e¢¢åI·È„U©1Pä`ø-BŸˆÁa >K㞨BdRÚ@p¼5/ÐÞ)2QR³X¼…ùgû•í Y&Lîãê ÂFO24t£Hû»Ea‘a2„¿S2Ò˜QZš˜½(ra +‘²˜± “ãJt¨ÖÏõæ•Z¿$õ‘Õ8}õñ_O.¾þGO²¬·ŒË*NîpkœDOId w¿ªÆ›a£&¤ïÏ9gRå#‚oÌN?òÁ,[%šfïNë>­Šu„s%éSA¤9"¶ |#Ç5«úDÎ%cM +¤%8c«É×–U½êX_ÒžËõ£þáWR´.q.·¥Ú‰Ð8GõIž®RÁ•Ø KZ[v¯,AŒ¸!b§À¶PyXæ[¨ošžDU„ 0µ‘c½¢ØgÍíã_Fë×yÑG•ÌÙhIG«ª9‘[×µù{§ÓY¾¶zç鸖N kÀ.Ú[••AÕX ñaë”>Œ‹¦¥Ý²²[VËB\$UѥȽ¢TæjÉ{˜¹K8Y¾]‡ˆ<æë%¥µSažY“Ĭ’òE–O¢&¬lÓs¤ 0?ó”›EÍý¼°“åvóÀB³kBiý­, A?NQ[æI–NaVYl3îa®Hc!ú'‚»Éà.&µã"rl­ÀÔ«ò‘¬·1:·ˆÔú,…måY€A`œªÔˆ(€ngñp÷³=ô³½j¢$ §*öKB7K9ÜÃä«+hµµSä·Šê^ÅFø& ´( +lˆ°~‘ö§É¢”™ˆTð„ªÐ!•¾:{Kü_oå({¨5!Àò€{B'ÇÖ‹|c¿¢ò§?Í"5 B-‹Dk<ܼp6, u".T2/ +­Šº_ÕâŠCÎT +—q¹ ³(íŠÖ/‰mTé˜0]Õôú&œ¿k+µ±â¢ª÷hwÆ…køYæšNûœ fÛeaQJL±©[ ë ÔHVèÓöQ3M„„6eÝÒÖî¹ZUja‘F²Ç„ïš/·¯¸úQIi+ÍS¥qH9c!˜aÎÀ<œü*\¼GäV7¨ãD}”cw…w×ÎðµÑy3GqeT`¼ØGÔí’”ˆ«[‡9ÊGøŒX/ËÕ b¯,c¥´žƒÈÅå¯Oÿ`¯¢oå¢ZÛH¼+¼§Ý[œ)Æsbl¥é4¢¥u¿ lç¸åWøf²blçÅÝ¢’Dt€‘Ïö±Ý¢˜¥|B%p{¿b&*f² +œ“0,ÄE¥º6I†¿–„¦ÏÕÒãÀ#´"A`gâ!‡Û¶V¢ƒG)j¿¤ìå…/öñ<Á4BÂôepŸ:z¸’½)„ôNQÞGŒ½²’BͲإœc¥q.«4ãÞnžÛÎ1ÕÇ)åëaïŠu—E©»‹Éàà3®ö(oøV‰o°Ö”R[Y €ÈFäaÍ…ðX‰H½[b<É™Õæï +l-CX9ÊCÕN\ ô€Ú‡ä⽕Ñ9+Ñ㢣áàÚ˜ËÁÐiž|÷«»¸ûUN rœ‡š#Ê…`;³º7îð)nÄ Ų„Wº¤µæ .É2{E%Uu!\ãòÔH2÷QžÙA$è-ë¬qmT;0íÑ –aòÓ˜8ó8Míä„œ˜©:º‘*ƒR‹¤÷$Cþb¿òE’ÚŠ+Á3q]Lj=ÂM–U`¢"W‡¸%­9„ +¦ *hlÿIšø|¯’EáUávIxTå%\î5§ov‹€™!hÀv%¡Ã8³ûÌê<qEIÛ<0Ú§¨Ö¯Èˆv¥!ºSÐœÎè.…9¬Ù§ôn†tà(}1àŒ^ésÒYÌì2 $¶ÅèÐì]6—¯_|û/;'ï +bDX#Ú_ÓþXÒžrᦪ A¿ep8E@}ú”…—Þ:®¿jo>$É M†¹G›cÖ™QÎ\oœDç¼7ϱªtP%.Ý©¶®ìÑ 6X©Íã§_ý­18\AU!›œ9(v@Tˆd@€,Àï»%©H:q©k +8ÚªÜa ±[äw \¢¬ƒlÀ”!À¦Ý½+ºçyˆ“ðވ˕S+CT7ßÉ‘á“<÷(ïä™DYÌ`FUh° ”>)÷}’#À¨ªÒÝÎKûˆ²[v‹RŽpU};Ï>N`YÌÞ+(ÓÜVNÍ@ê¸AØìUÔ¸*/ßHô•{à5 Ìàå¡ËÝDÅØ/Éû€B úŸA]»1Q”%¿HbAO2€Bpäø'ô!š?¨ÐzüuñAÞ#b«"ÁÜvJl˜(É«ÐI„ +”ó8ÏAV¤Ö§êåîN‹-!€$߀µ ­•=xÌ¿‚ !·XµÙ_¾¬ÍŸýaZ×û¬;ÑêÞàšõç ?x{ÌZqý7½q¦Ô/•úœ +µóŠÚ@£¤¶7¼Jà:¨ñªÔõF¯«/ÛÇß4¿&ƒ)˜Íúâ­Ü:O1Q^hV”žÒ<7zÏÀ¬ÑκÄØ㸺#áô¡rGiœ›æò+äi¦U–‡%Pr‹Ðú„A¸æ¼eEî{ÂÄ’ÆdYh¨Í³æÁ©u&Ôíáu¯í"ZYhBøÙ©È)ܵ@j£¸d4jmåxRlêõòØØ*pÀMˆØ"´qõ¶a&áAD/ Ÿ£ŠÔÇ? 3PøU*Ü~”å¶ +R·á-ànrD}·×峕e@ƃøµF…oNíÖ%¥OS˜›îÃ=„o!\Tk·Aäoç9 ÍÜ'õ o‘B´%ÊF +1ÒU E°Æ`ëÉø9>® !ÁaÚ!8gN¨± +ÍàVUlqÎæ0Où»!QVò„¿›— *Ç@ÔíæE@Â<áš=Î0@¾ DÛN°T‚7*Ò€ˆ:„m¸S–¥ÉG ìI’Ê`Ð1m v·ŠLZž²§ªö8[Š+D5!pðsD[SÚž–¥P<¼>¨Ÿ‰µ5°ç­ªú $E –y·‡«Ðm;>WÐ×gJ=Qæ%KöóåtE%•ž¸AÙ2'Ûý¸²+ +èªÇzO¨í•…e`R}¿ª¿ÃðÓ˜݃_k‚jÝtUfÝaYªgId‘÷QDšÕθK„k(ÍÃ$©Ã„'«qÙØdUEøPmIцsÆZï¬jô+J7'y®™¢ü< Þà¹=~Q˜Ò]ÎwY®–¤<`Iµy©õ®0µ :ô ÂÚEô­’ – –C«Ÿ€ÐåÏÚ£$nm好² +†(Gxjtlw¯1½—CuopIãm„ÛFä]Tƒƒ”„z}ö6Z[µ¦`¯ÔÆa‘R(–²"´lpyÆèIŽJ–ù2ã=Jca±r $> \½g씨íeq'‹¯»u·²¶KídE†LÔ“, ¡˜ªj_ÄîÉ%µ)ˆ„ÿ“¥÷`nä:ÓFÄ[“È!A9§F£sB7€ÐhäœæœÉÉA“¤Ii”³´J^ÉZ+Y¶œ´–£,ÙʶeYŽëõ~ß·{«nÝûbî­B±†„sÎû¼OèÓݘð hb/qÁÈY)øéÀ":ëÂd)^×y¨…ÞГ;¬w„Ã:¯›ˆ›=★²úƒG6!ƒ\}œì bã<Ϩ•ük,”ÁÿØ„ŽYq“FK@¾Û7ê8xë‹ŽA£ÈP}åø#&,²oÔ6jDÁ%‚½µ2 …gâa47Í÷Ž™'Œ.·?àÆd‹XÐ"<’‹)å%+¦Ø±¸Ý3¸¡Á fñÈ`œç°· +&ÏÿÿMæ^.3øIªÉ榛Oï×ú›‰ÃPoÐÍÂÅu0 þv w'7æߧ÷‡èQ3`kñE-h äoÂ!ðJ³·zg²¹õ•Qû~mÄà5ÃØ m$A3|šñø:ðQãN’Úa½`ö„±ÂF>5l¥@1'÷k (ˆ[Bé”N"<ÁŽE±¨çA0N#¶ +ivKÙ>HöÀC›š0·Ð9Ò"(H“R¸h0S­çà˜kĈÌtiÌÎZ©}™¤¤põ?гim´ƒÈ¹è,°4ˆuÀ¶Ö¶“ è&Ù7bÑ¡wÐà|òXG:jŽåôФÄäd1(b£oH‡Âl’s°ñ¡ïY ¡¼üþ1û°Þ7¤g)"û…ò¸3Q$W<04 k}ƒOw!0˜ðŽ˜ˆÃƒ#«0#Î亩xS³0#@+¢sƒÖC:–4EAkBuŸ7xÁ»†ýR pµ‹±”•.ðÉ•peË-[‰¸‹Nuè¼À0ØÈ[ b@Pí3iG!ìäµÁË¥P>ë«|z‘Vg,TrÈB9¸œ“ËB_cê“ßÁsð__ÔJ¦l Ì:¦qQ`x0æ1`·¹þyÄ–LëÆl,”`ÔD Ž9` ˜Ý„ˆ‹Ð˜­-à$“ãvt„ ‚°—)БŽÅ¯À&-¥ûf\x‡Š-*¾ å¶ü‘.”:]Hô½0$;°.íaÒ˜TÁB-›“?b¦*LÍŒy¶„ÉX/Ö8Æ¥ÍdÒ¡ô½R}ÌÎk]&³†«ó& ŲSiB™¢S ECC%û`­i@#;gB£¡òE¿gB7ª…ì溓,(dü²›ÍÙÚiH¬±Á!;®fò'õžàˆÑ¯µ0~.g'#ÀoZ'ç V¼Ò@¯Çœ·‚³Ü¥”)Ý€99Ó`70b#R !™jl°‰´Atu0Æíü5NaÖBÃÒÇsÐÊ1›°oÔ †TÆ8|Üw†|b“/PѾ›Éú¸‚‹Ô{ ŸŽÚY©Úñ¨—M¡¦ÞÁYÜ8 +(ãþqÏ„•¥£ÓÐz‡MÄOhd€Ù¨Â2è#b:ø6i$bòE‡L$؈På1X 3}P‹Ž[Y_!cÝ1'<™u2Y“3B0·à ¾! —KôÝ\FëLhÄ€ JáÑÊ€ÅE؈–4Î1ªwý,ô„³Îp`‡L ïœ•m¨ :dÄ!?² ™aµñÃ0läföEj†È¡ dÄH4± ,ŸÑÆ ÙÓ'Ð!û G˜ð! 1î*Èq`VmdÚ-”FAëÔXpkV<Î%§Òí¥¶ +qÕ +@dð!v?äh¡’,†àqLÈ‚E°2zDÖºû€L*ãbrÿßÁ¨Q e'“f1êà`À q ˜ +VŒ +Uééaaδ‰`ü†”ܦS„Z ¢™™:lÀ¿2lýê°eDï7¸Cv˜“1yDñ±é!#²Ì:b@p°VZke´à´ÍÄ„ |—ó+‡ ÔÛªøø<Éq ƒÐ)?Ÿº¥Îƒp +qpbP¾ÁI5z ÔøWéö¶DÝþ°Ó/›¨‚$¢àÁÚA#¶_ïuô¾Á¡˜[Û=M,Ü0¢ +H Ä +0ÿÿxP Rkr 7¤ókÌ”Æ U&ö9 ûl„bõG!êB×€i4ù0Õ²´ˆ8p#tÊ€„ËîPopwr3Ñ@¤ã·ŽN[àåxlÄÉô;7ø +w`Ø„CÒgc­@f–Qû&LÕºã?µNÊ€P PUŒ¯ñi'›uñÃfJJÍy8èkš]382ÂHÐŽ«g\T\‡„ 6:Ø,ñ¸•0#Qï—…e±â)/WòéA$qrС`*@ FĈ‘ÐX8h ãrºŸlnÞæv˸ÞÒc S#f~Ô*è½€XéÎ?¬EMø†u¨ÆDœðG4&Úà  | –Û48›ŽÜ¯qÖƒGc KÍZȃ:dÌBh >0êûG,àâ ËVõ1”œs’ò?ûÊAí OŽˆ í¬±²`® £}åvXëh06Ø@Gmþ8p;¨€UߧõØçVine‡CZßÄ`5  )ìçDcF·oÄ>8µ àí–çwyd÷Ë.6¶Á‚Åìt V©l‚ž•Šx¬éÔ5v 1À$*Án”ô>¼Êï#MxE-8yþ׬º˜Áö¥Ë¡"!—±`ÕÎä<\ÎÃf0©dÅÃ9µçåóÓ\tzÄÁv€gŽæÚÇ!kò,þð°tMÐz"N:ƒJE°m`ìíLvÌ<`¤)pòx*ìmoOg©b¥_wîw@6±bq“u29+™±S9à±q;ûÊ«Ö¨õ£þ`ÇÅ–€— qÆ! Ù(£wÀ‡Œ¤ [¬iðÒfšH¶š°ÑÇŒ¾8¨Rb°ú€ØÔ a€ÁÀj¸˜®brý6‘@œvX‡Üa£7®÷Æ-äàT­;¤·ƒQûÇ<û5n²[ï@CªBbÐ564Š²½5øî0xàCZü°Ñ6v’ªFïû‡}c_9¤Ö"¶(ËP}]йÀ×™†Æín¿ îl6˜|H+gå;V:/Ly°Ÿ¨G!ï܃óW—nM*1—,ñ_=d=€·ñz$vë­48L@ ‡JÚýa€%O;È›A˜ŽÉ¯úà >=CÇ»T¬É¦z.!ï˾pþÎ&§„Ü¢X\³2y‘°ªÁÄ1HmdÒà9Xè\Ùüæ Ç9¹Œ?TuñypÅP_«‰ð2\¡•Ž7PAeÿŒ‡ë™ÆžR^'äšyÅbÀEV2á“j`’!‹y¹"Æ|,ô>¤r ™Ìû#-TnZAã\ÁQ+<ÈÇ(Ø™´|™¢âSR~Ù!dêã·6€÷†,  R*÷ÇœÒAh^3nô…ür…sùQ»ÜhÁn¾ˆ+ãà“Q¨ÒÎ0!a;‘ÂCm˜»ipÎUØŠý蔂Ì:èÜ„7lÀˆŸz$ +V𣱠Î1€ft9™Œì‰è} ñÂa£ßAÄ͇~uÜ-CÓaá&¸V­+†dÌÁ ÙC?@ˉ'm~Õ -C¥4Fÿ@2\€Ð5ØZ"ÒV< I¸Nc" N~XçUÕ»BºÁ N vTö’q*TÒX°q+‰pi.1 ¸uB)4f¦lì:)˜˜ƒl5>Ç +º¸’É`ÊXtHçѯzö¹56V?8{Vv³Y&Þç“}!ÙGCu#¬ƒK˜°³#F‹D Õzƒ5¿ÜˆÖ–ÙLÒ›ìùÂu@"Ö” +K±ö¾°â Õì”jCƒBªç•*À.œF +“kD¤î• +´ ++ŸÆåŸFÃU€|›ž‘Ë«ÁÂ<è¹%'›g•I0öHø§5EC\®Æ*«¾PÙˆ+v6ï 6ÑhWú>¹mç + wX¸ÈÎØh•ËÌ ùUO°f¡R^©ê  +,R7SŠ…T XÄÎ$ài˜\¡â].=G'¦«^®èPVRï‹Z© 0•Æw ZIuÌ%˜ü1=LŠ/À{z…¢“N^ëQçæ¼\øÜBÄaâ€/_àÔIZiƒ'ts9¯X4‘‰1DGà “”Ú#”¶I†Òà…·M8…‚3PÂc-:1é‹.ˆWu0q3µ’qXC2ÖñŠ†ƒÍXp\¢ \‹W²AšðJô_€Øëb³°ò:c%’žˆ>ׄ—؈¤O(±‘6„¸:?´@Îà ƒ‚CZD…o‰ù).Õ¡ÕZjr‹Mö!ÑPÅ({ź“ƒŒS•z@m;¸Œ‹úåàgØÂÂ,`: jf¤ ÚàáéXÙ‚…=¬ŠJP‚MD§°p þâæ‡-rë„|Ï @ !?ƒF›Lj*›Åbu5•™<ª,ûãí@~A,Àtˆx7\Ys}@—OLGÊ3ñ暘›6cŠƒNŽsËT¤f'ccH(<Ä€=íøå&ð|:­v—‡p)À§™PL†VºÉÖŽÚÚñ«@5ÐÝÐ5v΀ˆN*N+­HsMioçgOtv®FO« ¡²‹/Nx@€Š¾`•W'Ó͹ºNÄ'Att^ð™»?â²àŽ< ‚’˜‘+Kbn†ŽU ˆJ†›Àö–Á)jy&Ñã³S~ÀÃ`w[u²ß²„ÒÄâ]2Ñç³³±ÆºÚÙ¢âUJ)‹ù>Ÿ›ârSb êØ–Šs$`)Ró…Ë6:ás¾ÐÀ’ ÙÙhy­¶p¾¶tN./2Éi á +›hsÉn¸º.”¶âÓtrÊNÅü"4]XŽŽ·@Ž¹Ü‚7Òò„›áú›˜¤b Se˜5›] “Óx¼Ç&ù™Úâ9*ÙÓx%(¨Ÿ™ÇcÁj‡ªƒ“–¥p£ —uÎEG™xOµ‚ÅÙhcC,,ø"u'ÒÖC¦Rnn ÙN6þÊä"bi`#Éä-²P}°°klËõu¹ºX.4T„rðYo¨D%ºTb2ZßtA±ÈxKë Ø ð–ƒS&€®¹ä 0m¸¼œìmoÞ¥´wðxS*Á˜gÕöNvþLnáL´¹Î&»R¦[™?Z]<•îíºø,,ãJÛ­“‰.›èb¡’R˜wPI™ c-"ÖÆ"-©°˜é‹wv™étgCÌö ~PÕ$£våòr´µmïxCU3¡ò‰I«jl¸‹U™)6ÕÖ7JKZ»7­Xu¹¿u'›èý)*1ǤæØ$q=7{{ °bg2#8ÓM*T´*/ò¹9©´’›=ïlsSùöºlùࢨƒÎbá6Ÿ[7ö’½Ót¢¯TVÜBGÈEÈÕLÿLïþ•ÓOöÖ/ö·/òÓt¢k®G[BaVi®U—.¶wnŠón1çâS>)˧{\z:5y4;u\*ÎOíÝ9î–Kn)Ï%;Rn*ÖÚâËëR}›ÌÎR©>üj£"n^ Ä’˜:­ÎœOŸV§N‡Ê«ÕùÓ¡â4“¨‡k«|nËÎò‹ñÆz~îT¬¹ ð¢Óûƒv6MD)-Pð@~>Þ>’êD‚àr¬Z·QaDLRJ]®®&&÷Ôî^®¢¶p ð„Ò¾e}1·ðHvŽ¥û§åê¶/Xw€Aå¥ü¼\_ãs³LªÇe¦gµí +–pp’ÍÎF꛰¹ÙÓd´á$#^Vu a00^¡,fæK g33ÇR½£¹ùsñÖæìî p8hÃDïHsëJÿÈ]ÓGïê¬Þ.fºL¼©4Ö‘`ìß`»<5]^¼”›>©vvjK ñMþ0.‹rsÉÎ^yéöÒÒÙüô±¥Ó÷–æŽÃ";¸¤?\Ô«ëéÙS±ön¸´œ›9må’6L e:x¨0î ýÁ\²³ÓÚ½»µsmñä½Wžþ—œb’ó•Õë±æžWªûB-:5ã“[v6§CÞ@Îè ‚J¹ÉPi>Rߊ·÷€xB>Y_}ä¥w…E“ $»Õùsbn•Ï¯Åº'¥ò(ûÜÎu—XÔy$jà¬S“'Òý3Å… ™þÑÕ÷n\zŠˆ·£•Õúêí­Ë­Í;J §›[׺Go6Ï_âÛjgÛ+fåÂtª{&R˜;S]¹Øܹ^œ=Þ[¿}öÔ½D¤\?Öݽ¦öŽ³ù…â҅s…t;R™¢5«ñ—_Õ7ƒµµTÿhzú„X˜*ÌnÇ:ljRÈNûär 7,ÌJù¹ÚòÕxsÏHá±²\ž'bJim”åÚf¶:7}JÊuB¹.£¶ÄÂœÒÚŒÖ×ÓÝÝÒÌ‘¥“÷ô¶.[’Ñ®8{ª±r¡0s2Ý;®´öhµßš¿½µv‡ŠDÙéS©É#±ÖvcíòäƵWŸ)Íž´²>7(®ì¡¸üšT9–›½DG.#b(N©- ùéH}E,ÌÊå…ì䓘ƒ.ÀǪ‹™Þn¼±Æ© TÌ‚çÏñé~¬¶on±f¬¶šœÜ V–¼rKÈÏæ¦wÕæ*-«Ý­@vR©-”NÁâ‡J³ÙþÎùû^Tê«°åóÝ…¥ ‰É#‘ê +©L‚¿Íµ÷–N<”h¬ØIIHÔó=˜ËùÖæÅòê¥ÖÎÝÖÄìdØQ!B,mT×î*/]Ž¶ŽF»Ç¢ÍPq.Ý\ËO®›±‹@˜¢@hò`råÊ*©v™h¹ØÛ‰U–ìt†ÉÌ…;ò@yÃåE-*齂‡Ozy•…Zäæ6al™™³©é3 —Áì,N°8™ì¬Ä[ë‘Ú2°Jgvï±߆õ±Ñ±p~ª¹v7<k×x¡újirëæƒ_»ëùï!R®¾r®³{Oeí +ôocãZsóz 7ì܃WŸxM,ö5nžR #V’“GêkçNÜW˜;Õ]:õð³o¤ú»€ÂÔÑdw'Õ?6säžÝ;ŸY<÷¼\ÙnÏí­ž'¢5Zhq · 4wÊK—¦N<¼xþ™Âü¹…틽•Sx¤ U –¸tŸÏL)µ•™ãNŸ~4Z[[PÛ¾`Ž7I¥+æàKv®­lçϸy°‚!E\¾â2SB¶_ž>~é¾K§‘P±°x93sNíì56¯B§0ù p†~©),C§»˜x¤0£4V'·/O½Q]¹€«=¨Vé?wÏ× ¹+ÏTæO—ÎÓéih¼¾klJ…¹pq>˜Ÿ1ù%7Ô"5onQt7îܽüteþÌâÖÅ…Ý;¨X^žìnæk—6/ø5€ x w aܵÚ9“_¸RX¸ÌN/nݾwöF4×òòq&9ÉdŵHu³½}¨²î`’ìt¤µ øgÕØ©²ë¦6ÖÎÜóÜâñk2 +ü‰ˆåDï¤Ò=ÊgçÁ«³±ÎÎéû/ßÿ¢”ì‘‘ºÒÙŽ´w•îñÚú5°CV sÔ<âÀC|¢,,d{GKË—’Ó·‡jÛ>>ߘ9%$ëfœ#"¹xs=ß?Zž9RšÞj.·?îa8µšéËLžP[» b\¦›ënîœ{hæø 7—LOî€DÅ¥‹ñÞ±@qIH÷œ½ïÔ]OñéÖ¨‹cSÁÒ¢›–ë˥ų‰ÎÖÔêÙ—¿õÓêÂ)N©Mo]^=ÿdëÈÍþ‘{–Ï>Òھɪýcgî=yçÓ nà7àCÓ“GíLïHeåbnî “ìnŸ¾gzó<.&Ú[©©cáêš\]ËMœ>ö`yõZ°°Ôœ9V¶ààÀàJ×é aq3‰æ†Zž*tÖ=BŠK¶EH¸©èFÊ+åÅÛÁy‚½,,]¡ô‡ò\º‹DTÑI\™Tê;f"ê bÑâl =YY8ÑX=inø"-$PÎ5·ÏÜxO5ÍåÖÆ…¹³by…Q›l¢å –p5ÑÚ œLŒ‰W„LO„®ÉÎHÙ~¢±Î'»½Å“ýÕ³d´Zœ>Ò\¿X_¹P[¾½¹|º2½Nwo<ðÜ›ï|0{äš‹zy\®Gk›JûD¬uT,,ù„ܹ+=õâwÒµ9°˜›9›ž9—›»PY½«¶q›†?–»«“í-W2 aHˆÉ>0ùvuñö ¼ÚÊ÷³ ñ…«àjP¹¾"ÝÙ]>~snç*©{›‰Ö**¸….·¥üb¢}$ÙÚu2ªœìDò³!  årKTrWÙ);Öy(<”ŽUçj‹§fÝ£öv½áò˜[w±nNF¤:Jm3Þ=–Ÿ;ÙÞ¼<æA±´€‡‹ÙAÔ­ƒöÍŸz¤»s#Ý?–î‰¹)+3a2Ÿ™ö2Þ@Z®. @së+·WO°é¶‡†r¥¾œìíåçΦ§OÑ™YDÈFs= È©CÙi2Xp“1)Ù-ÌÌL“J  '@ï0¹­Ì ¹)BàÖ¦Ô¶[H1ÙI*;§î ägýn&EFëàsPð9ÁªKȃÄG +ýüä– f:›ý½{ wK[¥ËÙù³L¦Ÿëî¹ö*—µ·®ì\ô¡ô¤ÊNzáz¸²¡C$$N·7 CA¥ÊKçëk—gwïÙ¾‘éW[\´²qêfiz×à‚–ƒ<%äVÅâj²wR,.`¡ÌÚÑËéÚ""ä(VuM*®°©i©°’ì‹äfº«T•·Lš?Tp0ªoå:ÅÞ6Ì»Y ´ƒŒAßAvConFËKñÒb43kU›=™êg33U‘`ÍJ& Ñ0ñ^wã—îYI‹4¸ôTiîlqî4<ʳg e'ªKG.>âç0¤øäq±¼ †¿wâÁîÖeµ¾î äO^~¬»|Ò8¤))¿†Ë?U#2ã248¼m£BzV ¤´VåÒ@Ô€=¹º.f!`Ý|ÆŒ†Q!MG +˜˜ˆWf…ì“«À¡Ü´’ŸZÙ»2¹q ¦¼D¼ºÔX8Û\¹”hï ¹y:9é2°ž©ú*‹ˆ6ícP‘úÒ……ã÷ϹIÆŠùÚôú©»ÍlÒ)4T”VbÕ€ÙÅ‚%5zX2œ‡ ÒÞºV?ñjñ䣭•ËñÒÜüæµ…Û‰X¢d@3µÓj 1C«&D0ú¿˜â ¹P\vJ'!3EC»ÅêN6ô[蟊Ap—ÈHÆ©ws.2ÆEëã&ä°Î1`‰Ülª½›ë‚ý2.]è«-žw2 DÌðé)°yL´‚3V\‚€,—æˆH ±è×CÅåÜä±úâ*R·ba/“gú¿ä dèú`~\e »@†j.\ñ‹Y(™wpxª.dæJ³g»×[ë×RÝ=žpRVŸ`òq2 +q&^_g…X-hj¬4*¥â\´²@Çh €ŠE"Xv ¡vosïÂ#6R>8n3:I­ˆj'”žr‘q¡ƒùW¿û^ñظuá¡PÞa=\ÚPë{j}{p³+äÅç5V˜ÊÁıpVªÁds~ó¼Ú]2Q$˜” +=!×%U¨Héô‡b¹Y13Ð2·„5·âa›>r&cUX´å£WÓÝ >7‰F+n)ãT.^IvÖÙ¨` V­É´VùdÃFGÇÜ© àÃëۑ⢕±Én¢s´¹r¾<"ÙZáRM¬O¯­½›Š™14ܤ“³Ì§ ÿšñXVµ±C)E BsÑ|vjLHûJcõBq樔ŒæºGÎÞX?{¯ÆÅz*Þƒ.,R¡š9 ˆ¬Šð*LÐFEéTOLÑ©Yµs"Ý;íó:/HTÀ>Ἲ•ˆ¡2,RщEÇõ½Õç!Ãx0HMR±¸ +e”ô#¤Tˆ7–•ö¦T^DCM"*ªÍBï(*fÆmÄ„ÌFEHMÅ@e:{r~ap ŒBøtª±>jÅñp>˜›ÊM…±!RÑŒÍH‚ (…‰C{–.ÔW.UæΪ-¨n!€,˜3!¬“à×ÚÛ×Ë ÂÅó¨r²Š?RÄ"y2Z $zJiE-¯qrRd0?bÁF ˆÙIÚÞÏ&MnAc&t†VwOÞˆ§[czŸŸOC÷r ×;_ljÂÁÂz&++„TÆÃe½‡¶ú¡gr¡_˜Û©,í&{‹ÉÉ…òÜÚô‘ÛçO^llždó €ÅpaZ7›ÁĬÖÍØ0åb\¬Ì´C….—ªò™Z¢1sâêcý½ j{Î'gü±‚\ìç6ƒ­yÏfª³ál[kÇ01‰Ši£_JBÆŸ=(¯¸Å ˪­9±Ø M¿’³òQ_$%+•ÕMuzÅ#§¼b’KMzaÑüâ°ÞaB8° v¥ÙST,ƒ°")'¹&R5/dë„R;­¥íÞÎÉ@©íàct_ÊÍy¹œÑ+ M8ëÜ$ V—£%09{¦´¶„‘èz¤è²GT#åÏ ÁL‡°X1Û?â‹:;¢u¹Q‘ä“d í¡bT2øD;ã k—!)€pxXeõäÕ™#ÅBkÄé1I”Ÿ÷ +ÙÃï¨Ñ§µS.Zñr)­ƒ¾uÅAŠvøät´²ÊÍ3±êêÉk³»ç’¯”÷P6&Ìe;¸Z3‘’?˜H¶×!,ã‘*@ÚN*ˆX-Íœo­\3=-`“ˆP.Zž…h@+u6ÞäÕôu¦¿™™Ú åÛJe&^[K4¶äÂâ„…üÊiÿ˜sáÔ¤ÅÍÒXFt^³[â¢Í|{;UßÄƒÕ ­1ùÝt”Œ–÷< îa3Š…jã˜RÞ$¥ŠÉ# ‰Íeë½¹ã+{çŽ]ºçêÃÏÜ÷ìןyí[oýð§~ò»?ýõïŸ~ñ×ïþäà 7Ÿ©Ì¥”úàuq©LqªX›*·jÓ…Å“GÎ_½þÈ3—zæÌýOì\¾yâúƒxæΛO½òÆÛ/~ëí…ݳË{³%*Zðrq!Ù¤Ûx8#Æó…ÎlykqûØî¹K·ßóÀ]?óà×^=uß“G¯=ré¡çŸ|ù›¯~ïo½óþë?x÷á¯}snï</º˜ˆÅÇùh9šnÅ‹].–Œ—ê­…ÙÝ3½­ãµù•îúnûÔê©;xêÅŸ½ÿÑG¿þÝ7¾÷ãÓ×n-ì)ž­Þø˜„ZœÍ·7}\B礙p¶¹°[žÝLw–”Ælª»´ròúÙ»¿úð³/}óû—~öüýÿrñáç…LË‚ÉvØìæÁBpц™=Vóñj$ßV«“ùV¿2½Ö_;½}îž{Ÿ|駿öw~ñÞÇ¿}íû?=qõÁþêé\s™ —À.Úü!ÀƒYJ.Ù±°É+"lJ)-¦÷âµY¥6µ{áÆ#ÿúÚÏÿÛ¥¿úÈ o>qîîG¯=öÂko¿ûÄ‹¯Ýyï£;—î#£%+*Z $ÉŽœŸçcMµ4JÖã…~¥¿±rüŽÞâÊîÙ;ÝqÏí×øúßýÍ—ýíþúÅþòÁ§Ÿ?ôì7²õùÃ&lÔL{Ùœœ[JÕv .Qg#~‘•K¸Tr’qñ’¸˜N–zëG/ž¼òÀÎÅ›ï}âþ§_Ú:}­¹|®4sœ‰”GŒømÃNFP[“KË›'Ž¾ãâåë=õìëo~÷ý>ûä7¿ûŇŸ|øñ¯ÿóoÿùéç_¾ñÝ=þ«ËÇïÌM±SI7®Ä’íP4ŸÌ”šÝÙÍcNœ¿~îêÍŸxîõïüèõüìù7¾÷ÄK¯}ëíŸþü£Ï_þÖßþ÷Ÿýõ?þþÊ÷~þð on_¸ÌX¶µZŸ?’í.K©J¦Òš[Y;sñâ½>ôô /¾ðÊëßyç§þú÷ßï£×þýço¿ûÑ—üëÿõÿ?þå~þ«ë­¶pDÎ÷0Q&K+­©Å­=x¬=uáú}=ÿõ—ÞøÖÓßøæã/óÅ7¿ûã÷>üÕGŸ|ñÅÿûÿü÷‡¿þ≯½vòÎÒõy^®“R$;—nï¸iÕƒÃJ¡Þ™ß8z掻¼ùÄó7ÿ奻Ÿzñ•ïüøGï}ü“_~ôÇ¿üåý¯_~úŧ¿ýý7¾õÎêñ»À2‘F(5MGjáâl87Yï¯ÎoÛ>uñú>ðô¿>ùõo~ýÍ·üþ'ï}òÛŸô›_}öùþôçÿú_ÿ Ýú£_|öâk?L6–õ~ÜFs<‚ b. 62µé•½ÓWzâÚcOýË7Þøñ¯>y÷ã_ëGï¾úýw>üõçÿöw?ÿàãßÿáOÿó?ÿóéo>áµ·NÞq3QŸك\¤A,)¶–z »;'î¸|óñ›?÷Ê[?øé¯>ùÁÏÞóŸýú÷úËþ×Ç¿ùü£O?ýÿüÏŸ}øé]ÿò›ÿþÓ_|ôëÿúßÿýå_þöµ7¾{óñg.Þõp(=IÉM’‰Lö—W6Ž8uáÒ•+×ï¹ñðc¼õ·~÷»ß}ù§?}ôégïýò½—_åâÕk‹›»©ZO.L{è„Ã"x…¢rT­T›Û{'¯Þýð=?yï£O=û¯/ÿèçïÿðgï¿þ|ÿ‡?ùòOÿÓùúëß~ù·Î^{hfó\ezKˆ••l'ÛœMU'ÅæÌâöêÖÞÖîε+W¾ýíï|û{ßÿþßùðÓϾøóß ï~ðég¿þÍ_ÿöŸ}ñÅ[?úñõ‡žÙ<)LrJ™æ…p¢TŸœYÞ>zöòÞéó{ÇO_½qÏko¾õË>üàÓß¾óþÇoýðÇïôÁo>ÿü“Ï>ýì³?üø£WÞüîùk–úk(«¢l"˜ìÄÊKX K1µX­w7wvzø±7a?úùKß|ë§ïýêË?ýå/ûû/?þðË/¿øżüê«/¾úú晫bº…ð) +AmÊù^4ßéέO/®ï:÷ØÓÏ}íåW_|íÍûæ·úÞ/ÿú÷ÿõ›/ÿüîÿðÇ?ü×_~úù¯¼õöƒ?Wío;)ÅàâŒÖŠH." +Qê¬æëÓ›GN>øÔsϽòú o|ç'ïøåŸÿòå_þã'ïð᧟~ùÇ?~þû/ÞýåûòÑ»ïýâ᧟9wõÞDe’‘3f„48 Q­†Sµx¦µ°väÆýO¼ñÖ¿ÿä½ÞþÉÏ>ûüó/ÿôçó»_~ò›?ÿå/ð>ïüôÇ?y÷gï¾ÿþ#O?äÒ½sǯ3J}ÔLØ|a»Wð`³÷S¡Öôêñs—o<øøKo|çå7ßúÆ7¿õ“Ÿÿâoÿû¯¿øÃOÞûÕ/Þûå}ôÂ+¯ž½ãÚòæI9ÛËw÷‘l½5]iNæKÕÉyˆQSKóëÛk—¯_¼÷á{î¼~éìÅÛóµ¢ úÎégݸh°;nr~BRbÙb¡ÑjOu§¦7v¬ïíÎ-Ï8{äÆÍ;î{àÆ…«×.ÜuïÑ‹WÚ‹+¡d‹UÂù9.Þ°#œÓÇ!dÀOòœÞ;~çÊîét¾Ð¨w¶×Î]8{ã¾»zâÁïÿ{ï}ðë÷?úø“Ï>~îå—N^ºÐ]\ &²b¢â‡èJÝTˆ &v]:~ážro±ØêMOϬ­.>}ìùçŸ|û‡oøñ§Ÿýö7oÿèío¾ùú«¯¼üÜ3>xÿ];{Çê½¥(š÷qq;!Ù!×àz'eóÐ@$ŸËlolÞã®×_}õõ·Þzýõ¯ÿüçïüñ¿ñßþõŽ‹§÷vÖ›“½\£ã¥ƒÄ8<£—¶øEBÈÍÇš<¬OÊá± +«6¥bóQvÊCŠ~!ÆÆJN:¢÷rŸµ`_Õ˜‡ônðºN":¸ †¡Õèå nÆî—tvÜì!nÒGÇ„hb2.æÈpÑCEÄh–«‡´æ‘q§ 㬗I`¡ª”[BÅ’ÆBØPÑŽÆM¾q½ËlA &—Áìr!LH©0b¥$Œ‰[ÖÉLÃèÐÃø a’vŸ¤³`&7¯w‹zOÐJľ°‹RP>‘©Í¬ïW3UÁÆ@¦éäTŒ’õð Eè”ÆàûçƒÚ½×GÇ» NÎêÛðŒÎM»Èè˜ ­D}]‚x¯ûIÙðÛÞÃÙø! ÞÊ#V|r›€\o{…´ [‰ˆÖÁ€È +É)1·¨4ö’­ÝÒü1;?nÅQ"-O»°Ð¾aãøàŽaIl/ž/OŸñ …¡q÷¸Þ“-Íy¨ø ÷¸2º`•€ÒNTה¢ÙÎhƶ#¢ÞN;ÐW¼ƒSÝ7™”2³‡u¾aƒë°Ñ1briLn'ór¤\ŽVýBNÍϦjKT¤L„Ên:áaÓ¨TÜ2”IZñèm#Æ㶄쌟NPÁ".£|:__ÏuŽ 齇õ.“—õ‹|jšU' ¹ìÄe&¨tÖ°PqÄè5yÆ­>ŠU7¤Â"—ìÂbÆó-)Q±¢@Nïæ­XÜI¥i¥—lžtÓÉQ3jCx5×sâa½‹·a€:¥0Ñmݺ¤‚*óÐ1ÔVÌ‚Q>LM¢ÁÌa£Ç†Ê‚Ú $»~±`#“¨T5 a•tSQƒ‹ºmÄ|`Ôª3z‹ 6 ¥ã~!ã&£D4{ƒû4înEe‘°aIø霔ªw°N2 +©p'= =>¸±p:”›R*s.::bDÆ ƨ:vhÄzhÂ3l$Æ­ŒÃ¤\¸ª1ûá1a%†Æ]5ùe/Ÿ•² ÁªÕÁI)ÝõIy‹/d´Ò#ãîQƒÏGÆKÍmœOjt «I\n˜1Åä“MHÈ'•…ô›èÛ aãR +Ó§£• D(lŒHhĀ¥ÖìY7¡µ!B‹L¨Tð—q3¾Øb¶áÓ‹§}\Zc£Q!çbS~©$$§Õú.ik-´ˆŒ}£:·•°@ŽœnÚR³”Ü848™–3~9sØâÓºø[wmr©I¹0_ž=¯/ã¡«r5žÒ3h¸f¥’&,>b&5fZlp¿õ³„˸(•Ô8µ;dðįy>;ã“«:Ÿd'Bj®5·y;oi ¡°°‡Q<Ìà€j“WpøØ#&WNXPÞ‰ÞD°ŠI:Úfã}ø•3ád€74ᜰ.r§èx7\Z ¤|\6Y^Ò‹o Á¾€‡‰#L‚‰ÔýbÑAÄp&^l®ù¥ü„ƒÑ˜üP_;‘°âªÅ åçl„|`ÂéÀB(§”ÙMCSAÑ,°..–wÀö…`=LÚ†+Ã&ŸÎÍ»„¢˜žIÔW¹b¸u›;¡Âÿ"|sz×à¢:\Ç…ÛŽ˜a¦ä,i;ð¸Ñ%M˜)H””…ñ@82xF4¨÷ +6<êáòn6ç¦SÝåÛ ½MǤ!U0>‡ yL(Ø°¼jDçrø yKÜ`‡ÉÆj»ý‡²½S,fö‰”\6йƒ:ÇÄÚb²N¶„Xƒ‹6}\Ê䤪µ°m#Zϸ†ŸfØXJ´à…&_˜’Ë:kpsf¯°Ü7bÕû„â"•: Õ¹R·3nÆҨ߯óÐc n«;—»[w ÙÉ oÀípù¹yDíK‹T´Öéïì\|ÄHkA‹Hv:ãá +T´È­ëÐp‚Ã/š@Pd\*RÜãepËk&VGø˜?”ñ…‹n1‡ÅêL²…H9L®òÙ¹Hm›‰w¼| + ë`ã„””:¯Ö=¼j§c6LFù”fpÿçAè ÄÔ™‹Ö°Ààð5h +P´ÁAŽÛp3wòE¿Ü¡cNiqjaÕH¶.}ÜŠÙ1ÙË¥}BÐ%§:+»—‰`þ°·ø=TÂ/'׉p 7KpÊöÉJifHëÔZüŸä&ct´AGv4ƒäÔ"¤† ž³_ë¤À¯"¼ê¹û›—ØTwpSšÃvPO›D¹Œ”ìF óopÂàC©ÌÐzo=|µ>•¦¦¢¹:R攺 B ðòf4è èh3;y¼½yB÷kÓ{¹îQŸ°³°æZ;£ò³ñq³oXëÇ¢T×£ÕuŸÓyÃz,F¨ÝTÿTmù£4H ÕaÍñ`Ñäá,¯· î=muH‹ë\:mò†A@A]xÄì LX™ Ô7³q…”¿2lI$Ÿòó¥[×w«µÂäfŒšuvÂ>ØXŒZñï‚EQK³L{ŠR\bÎ.Kåe¬ásDgiçÚö•§BÊÃ%ødWÎ/EóKñò*—™²ÑŸžœ=ÅFûÇœvÚE ®¯ ¦gZ+—s“Ç”|wëô=*¤­¯$º»ÉÉ#é©é™3Lz³ñl÷þÇ^j­ž…–t`A.;(•‘Ò:£Lš¼¢ˆN î+GE_ ìÊþ`]LÍŠÙ3*Ü°[WôØiásŸ%B6ÖÈ´6™HÚ3˜q2*}ëk*Ñu‰3¡X°PÖ98ÞâáZ(7KŠyZÊ‹‰ªƒnÄ'WF·Tì²NJñ +¨DDªvJѹ9­ƒé´à¸˜'‚Å@¦_Xºè +–Fl„Õ/;ñ¨‹ŽÁGX°ÉÆÄȃLk,øW5΃z߸µc1Lª³±®ì3 +´FË‚9f0¹âåR6,âe“$x]õÿ%齞IÏtÏbÇHª.KÂ{ ½AZ 3Þ{G½+šb‘E–÷®«º»ºKí­¼4š‘Æh¤‘f´’ÎÍÄÎÙÐîÅ97»±·û¢7ÑÁF£Ì/ß÷y~OšïkZ|<Í7î¿f”ü7.Ú.ͨv 0`ã—ÌA³—#åšžtø§\¸ T—Š{È.xùÔ,"ãÑš_ÎÙh],.³£ÛÿVK 7JKwÔÚ!gõÂB¸0à>Zs“ÊlP¼à P>]hï‘jùÜ”×CÄ`ýL:¤VS­Ha1YYÚ½ù +ÑJx¬”žÛÏO K×ÞA¬µ‡*DJ7z[?úëÞ¾ýŽ•¼¤F+&ÚŽ–vŒÆ!¡6y½•iï;©Ä¸ö‡òd¬ÏeÖ´ÚÕÜÂ=¹´á u©Èɶ Ô> MzY_(ÉDël¢…†KBz>ßܼñèãÖú=s²OU.®ðùUʘç3+á܉À'¤î¡ãd¤âD¢dò„@‡õü«–¡¶§œ 2À ¨X+”èf:ûzcÇÃ&,B ðÜP¼ £¤÷ŽƒÑ&k^ŽÛˆi'áÀ>Ù#£ÍH~%Z\ó2ÆSž ³i7{ÉŒš<H©Ô ¥†Gê>>i :Ñ#b-O(ŸðqY6ÞæôF$ÙnÏïyÈÈجÏ” 1aB4ù’=?í¡#%JÊÍ8‰)‚qqÞHaQJBfSêX¸,%{¨˜Íd…'¼LÖM&œ„a h&oØÏ‚e4¥ðr7®˜`óf Úy´0GÑÇ$§­YƒòŸ½1}~Òcñ(Ÿ‡|ÇÄê ä^:®fú¢^<;i™r vTvœTò°)ÈJáüjvp]ÌÎËÅ!itØô\(;)¯1É9 ‹vkûÑó/ëë×Ï›|¶ àÝ7¨Àq’}ÞèO¹«‡„Ræ€AÇ*^À,첩l +(@Ñ3`ï"ð ¦Ù±˜m4EgÊÃç‰D_i]+;n¡à¦5.Q§¢5T.br‘ŠV¹d7ÑÜR=¡€¶ØýÂ4ÄI4ƒï„¤†©c6,)Ú´ú(./å–"Å5T©#áj(¹éŸÆëûdÏMû=Lêì€Vë>P9É6Ö Å€Z²fý +ƒ»„<žš›q³öÆÔ”Ÿ B¶u1–€L„˘X ä +JϺÁ£Å' ¬‹Ž¶¼®×·bõ­öÒõ›Ï¾’Ò=,R4:W”ò&kô+ó§õáD{ YQTÈ ‚„²J aTNvŠÓŠF}ÍÏ%aÁ‰øtOεÆ6Ÿ]ôz{áðôÅ÷,X˜ŽVcõt÷ ÞØNõ®$»á⪓ÔBÑJ8Ó›„Ð礀)µ¡•·zû/ŠË·FSàºéŸ´Ú”s’n&AÅ{±æaÿò %7Î âu/¿dG¼¬.e纗ï¾üjïÁÇ•Õ›D´’È4ÖŸ„R‹.¨O«®Åª¤>§×¶ëK‡!½lE”D몜[CÄ2ë@.°Œf«“2Í­|oÊEŸ™pÏú%àÑ&´êh ˆdË×àw­¤,g{•ÕëÍí;Å¥£ôÜ•HyVp1ùáwqxÿ]7£ŽÛq9¿žœ¿•èŸ†Œù)¬Xën/ï?ûæ¸ÏŽFicí^Í­ß/n<’ë—­LRŒ×_ù÷+û&¨PEÊ 3ó7–OÞ/.Þ±#çÁÎ¥$ÔÀy :îbLAWG·KA:ƒ˜`ó2«#BB«]HÎç—ow®¼Ó?ú6xÔÏ“êàHLL^a4Å%ÄD%”fiù>¤uˆÀ¶€¼J‡óN,2ëåÇfQÝ=:ê–Æ£gÌh€ÔݘŠ@¬ò^Ò0¹Á+ÃÈÂNÂO+“NÊ”á {¹®””âJº{þ†T˜êëÃHe«84m4®“ö“ +«„bAds@™ñÈSNÞ%€€ùA0¹ §šN2lÇ¥YD +Ê…T{¯¶~¯¶ýˆ²µÁ~¦{ÙÅéh¤¨TÖÄü®5“­½Âü©œ_J!•j¬²¡ä†ààb~‹µtŠQ’ÓoÌøQ èÅ+ÑêŽVß•‹«>M*E£µI'êRcô¦êz;›§/öï~»·sUò•ÎÆͧŸÆJ‹@YFëÊÆn¼úYmï&½ìáÓ.ò‰êÖôè¾ÌMé`‘æ`â¿^°ås¦ ÉÃyièÎI'¼|–Iö@Áâ-ÐN±·¹yí9o´ý¼á¢ÂþpÁ.Ai¥Z[ë7^ÇÊKS^ÆIÅò‹·ÁFY£gÍ„Ì»pÅ(,TæÁ½tS*¨RFÕ—YârÃhc½¶vóʳ/ËË×ÏYH°1;d¹€T€ uÉBOz¹JG +qÉ6ãØÔ0RÞãRC>µ8å‚Ç!BÊGÜtléêóçŸ/½¨o< kV2~Ñ:š +æäÙâÕM@MS‚’Ó«›V,zfÂcò©ÊäÏÛþâÌÔ˜•B´¶\ÛWëûÑÚn€Í»p×»\?7æ›Å/Í—,¤Å/_YütemqØÛäfFËCDIµ +4B§ç­¸JF«L²ã—ó~±À">‡«“ ù$ŠÛ ¿0)ȸX‡×hºªÑ„c"@o¾¾¥—ÏL9.Z ¢PÑfºw˜^O´·Âzùèöëîæ-+‰VÖz»o6·žfæNk÷kë˜dÿ’“="\p‘K³©€\gã£u@Åjç@„í„Ÿ5w} žJÕ'h}ôt$ª”°ÃÙy\ÎÑ‘lq°W^>-.$;[ý­»ÍÕ›|¢-gúùÁQapX^¼ÊåmLz: "©©¾QÙ³‘Í©Öùô"g BÆ€ÒÚãvÚ. % ŽP¤”Ö*k÷`øXeýàîÓÌÅ›&Ÿà¥u61ÈôŽÖNÞ=xòUiõ”îlW +!·`Ç5“_¸ô[“ž7&½Xe£;šq}Ú;nç ^1¦VX£Í'›j¶]ìíC'ùÖúíÖöýìü>ÝF¤”‡3œÔhv2Aoè˜ÕÃBrËzãZvîVcý™g4ù3ÈKVJõÀ4g|œ‘\Lš5Bf×%*óû¥Þnù‰pª¾:·÷týÖû[·_Ÿ¾üanpÌj“Ó·î¿õ-¹è˜‹Ë`ѶVÙ)-Üêl<ö… +ßšòŸ árBv‘OA®LBwOÛi€mÈbº¡Æ¸dO +·‚Bb#£”’õúÎ$R±!j¦¹_œ»™¨luÖnuG÷Wgý‚«Ü}õ}>3ÑBÙñ89ʃ‹h¸}¨ï§cÐ3^ÐQ-­Åj›Õå“;¯{O°hSJö÷®¿ ° ªɹXy V¯îZoÌNO»(Œ7˜2‚ybÇ¢z}'·x lƒ…‹^J‹æû³¾ÐŒ7¥µrã£ùã×µíg¹¥[påÌdÀOÛ7>†sgMþ 7gÅá(d"æU£u|Þ<7n3»Éàè|‹ÊËOûD¨ð€\2«@§ð‹3vÔ˶Ñtn&”ËT¼ÝÝ~°ýàSì‘œsQ(T6ÞäR=.³ •6ÅÊe¿T°F¦¶ŽI)¦ ƒDÊl´ÔéÀ/š¼gÆmç§\oLºÆ¬týhõ@Ÿèª`!³xhJ)qÉŽZYÖjk鹫bnW**R¦àÕ ñV§•|LHt”Ì’×m~ç^:6V‚EH¹€ +è7'¥&å/™hvˆ! ž`Á“Î% J-^YŒ–Rƒ´*%+| +:e¡°|T_¿•nm§«ëÃË÷“•€˜Šäí°¹´®7÷¹Ìª›‚Ðd„¢u*Rœr1³ ]ì°(Îç!ÜaRÅ?ÊÚ3ŽoŒ[¨,§ja#Õ>êî¿äŒ¶«.í=– +K ³>ÞñõØPP2ó™îÞE;=n!épRëӞЙï3^“'•²”ž÷к”LáS¿`Ò#).®Aر!2lip“ +ö€<ãd!þŸ›öÎ8)”7f=ÌèvHèbQÊ,0‰ŸžÃµ:k”毒 çbºÔßÍ®À;^©h%cV\¡£ÍpnU«íÍx!S\¢ÉoÈ9€mFïš|ê™©àŒ“©´7»ÃƒÑ4qVÌŠ©”ÞŽ6wÓs' êçf‚áDË(ÍA%\Ä‹ ÉN$7`´2ä ³stvÅŽj”R&µ +*eù…Xs_«ï‡wõÆž™³‘aHߥ¹#àXкÑ$„Íì„BMÚ ¨X`ûo~½.ÃØh²z å|d¥h¢Ñ_;õ‹)xÅå“Í»ŸnÞÿ¼¸~?”_†’£……•Ã¹µÛ!½iADs@ø:'|dTM¶|¤bñ’B´À©E,~ G&7ëW/ÚH0&“‡v’f¼£©¶½¼ot†¶C)£ËgNJ%ÃÙâÜv{íp°{wáê³á•'k×^¶6îè}¯ÊÕWF'Nå´ h­Š÷ðH+”Xð1Yf7ìv!Šg´tZÆK%íщ„M.†’ +4D6!ÒŠnJ…ß +ˆ y*Þ“íBg{÷úÛ¬^‡ùòhÅ™†–_â}\*Íz5ÕÎ56fœôî7ïÀu&Ú‚Ž " ÙhVû3.zÊ1º°«l•†7ª«7‡W^F+[Ðû•þÎâîƒ7çD"\…QšuÑ.T"Ä4tS¦µF)¨4;´fâ-µ±“_»?¿ÿ––_XÚ¼þ׿ú·HfaÊÆ"m)·‘jí^yðÙîýÏcþì4â§âŒR…^køZ" ¹´>/¦†\¬p>å áÀ§f¼Ü$„)·0á’,>ELö)­dò2¢g l”„ÅÏyBPð„РX¢ã:֌䒭íHeI­®½C©¼Á¤út¼^î÷ßâ“Ýi¯`¨.2IˆEH¬DC´D0ý 5ádf<œ Ñœp€Ð”d­2¿#§;ùĽ‡ñæz¢µ©máñ¾Ï)‰ÆÊöÁö3/kŒ9pK€Cø,­50¹Àj£{­Y¥ª,˜Øè¤:ÅÂMB÷,ÏøDØÁ73í¤ ÇÁy'ìäh&|"†ÈY*VaãõBgëõ?ÿêoÿKsý:-«åe¥¼Z^<Évv›ƒ«s+'‚^õ +o†bMÞè#BÙä‘ÏO#g'GWè ´ÌnÂJ›òpMXiäú¸dtkóGýÍ;“^Ɔ«n6é`ât¢¯ï${¼Ñ…`Uìe››v4bñó~Po& /æÉÒB¦²tiÚ7>‹€„¬›Ð&mè¼WH)/§ºö€ä%c($­ª†Ü螺 +©6(9_›Ûk.^… ã(ŸôÓºÍÇ{°0 ¯¨Ù.ÆÅÇf<^<âg”V ¥zBv¾ÇO©F¾uxãà:¡¶Æq²y$'çôü¢’ˆ…‹f")f*Ë€¸g§Ü&ä¦rLl±·õvgç%£w.YF)ÊñªÍú‹‹–oL{Çœ¼%CøR$·èã“.F³…3cvx]²¢X¤¨Õ7 ôÑz[­]Ž”6°HE=ݳ2.ÓIî¯/¾½wï³£'ß[½öÉ“á<"æÜdúhÜNœŸòŒÏ¬~qÒÉ bxÒšr…ì~\ÉKGh=_\¾|óÆƉQh0ÇúN¦w­mî³ÍÌÁ¯&4r~ØBBEÁòÂU`Ô\çjipÂ&æ¦\¼ˆâr™TˆJò+ðšñÉ€—0Œ ž_hÂŽÚ!’évî ÷fºÛƒ­kמrÉ.g"ùy1ݛ߹[Y¼æ %¸ÄÇr‹[7¥yHCn\µc1'÷R)\Ì{0Õ”Ç͉Ñt|~®±Yÿ¸%H…‹*Hzi%ZÒJ~éò£ÒÜ/Ÿ Vzþzqù.씘ìÎmÞɵ¶!³£BN+.Góˬúµ x˜K³þ¿nA'-ÁY'|@7¹Ið‘‹V p‚ gÉ`•,.e•ìàÚ£OWŸ¶BYFm€§'j;¸ñ“2ÎÅf}¸^KðÐá%WÃéÅ—™qL8©5 œ?;oà6gõIcftŠM:IËH*¥‹3Þ) âÂÂh ¹ÐHÑ'fü|WªÐzF{W0*kGOzÛ÷rÝÚâQgû^mí–VÝ "£2äâU‹?ä%”Yo¼cÚ†)’ b¢Rê ¯/?|cÜyæ¢ +#J‹Ù!kÁ¡SËÖ×/?€¼ì“òA! +i?©I‰*¦'m(1꬇žu3v¿4eÅ@ÄœˆìFçì.T ¤”Å‚JK×—æv ªÜR«›¹ÞP*äcûþˆ`ý²,Àšuq."NÅÚ˜˜½4„pca»_p®Q&¤fvâSVtÌÀBqBL}óüì7ÎMÿåÙé³cÖq+ê@¼R„M*´Ö¢Ùþ¬甼 ãJ‰1ºraY­m‡· sWwÿúÿºvô2#¤!½¶¦”–!¥ +™å`¸`Ce’O.lÜuÑñ Söq“ÃìÂL£ÕmP 1Ve”ø¾5H¥*æù¹ËÕáÕlsõÖ£w¯=ù@ÌtÐH)V½-oåû's{o7×Ö—o ©9=;ؼú˜Že-8ïbžÑ|beÞ0zߎÇf|B28©Oø“¾ðY3>æ  g)…u9·ÂDJ[k ûl¼*çGÓ¹äæo´6Ÿ¬¿»ÿô{¹ù£bogçÆ ¹°"¦º|¼…Ëÿh)ö&–‡ ×&¸ »H=‹ð½TÂcŽ¨”Tš´“ggü™: ¦l7¤_CxW󨔆±r1)6ÑímÞo¯åêkÙÆR@HPJ&’lYGk DlHÔì“A c’ÅÆfµpvÂãò"lñI +Ó¨Zós™ÆÂa{x: !KN5D£Éj£5¼x£‰Hy„MæªÃ|cmƉMÛ1GPÒ:¥#(M[q“WŒZ<×µzh’OÔ†ûk'ÏWO_É¥`¸æfÒ‹[w~øóߦòó& t‡ 9Vm±Ñ¥µm£E=(N«Ô–¯+™î¬“€rš¶`íAd‘Ô<Š¹P• ãìÏ¡º&}&7çÃT»WôbQ$”Šfçn?ÿtÖÇúyWŠL²+—WôæŽQ_YX=|ñáO*ó‡‹sWËK7Ôòª”]µ¶y„œª×½ý*Þø‹ógÇÍ'¬„Éa§ÄT£µ¼{ãE(QùBµ +-…ó=½¶Tî§{›A9­–SÝ+l¼†4šâJY0ÚŒÖPÒóùÎ6«gµbbŽ:ɨ›Ñgò”‡Ç¤«w]LÒÇg'¼â™™ÀŒ_rЗœÃÂe:\XÙ¹_›ßçF¤´œ™;®¬Üì>ݾþª±yK)/uW¯}øãßpé9'´­XŠé¥¯W垇t€Wè]­™òõ´À¶¯g5ûÂ(_$#õq=é$À¡ÆFWŸGËd·° §\¢ÉFr­ù+)ˆür¾:Øo,]¥£+ösI¨ŠV°iòˆdhÌ…D^ÁOD!è™qP{.Zšr‘c†5æÊ«w!=çb“NR‡ŽP²m_(r6 Ï‘ìp4‹;3*$³‡±x³›†w\hdÆNCèÀ¾~rÍOÇ8­ÌÅÊJ~®½ó`áàùðàÍÕ«/׎_4ç6P&êÁ5ŒO{qÅ­Ùär^ШP*’ê§à‡ŒÆ¬ƒð#AB·8i?®‚|Ù=ô¤Á„¢’]…Ôdµ‹üÜdÀæÍ)ÈåùDŸV«J²í£5ˆ<þP 8_«.·6n­?_9~«¾óÀÃ'@r›ëwÈx+ ¹èr˜\"e°ƒZs¸oÅåož7™” Z}²Ù²úCNøöR{åê}$’±RQ¥ºÁgúZe9¿|ëâ±:«öOž÷î;qÕKÅq¹¢äWÚ+7›÷¢Åòrmá½Tœ¿öç笓ÜŠÊNÊPs‹Åþ±˜^t`Êòþ)Ó?3ãwR*á ¥!©A&ÒJÃ\suiûFwó¦ƒVi½)ÌÉ™6¯d»Û›§oí>üÕªñú¦ÑÙGÂ÷ûP ;i@¥%ªFmÆÉçL~ Žcjþë…VÓv4vÉÆ^­c.Ÿu›|´ŸK¹¡Z¿œì¦ºWÝ´aä矿þÑö­×`ÐBª¥T–äÒPÌ @·Ý¤æ@eN«—§D¸:þ5·[½<Ä„³S~?“¢nÍÜ|ò>:9 =:Ñs…rl¢Ÿhî;-g{½•ã“G—ç­AÅG$FkT1I”]ïsy0>kP˜…çá/ZH“?ì “Rv%Û>ˆåû­Å+ ‡Ï6_qçÛË×^ŠÙW‰p>Ó¾\^¼†ðIN¯„>stream +qG€MÀL´îã â.:åçSzy çÎÏú Ó-¨êa ˆüP½ ýÙÊÜêîim¸ë&£}¹·÷æêÉëå×­õµ•“Hq!–é¾÷ÕßÝ|ï'f<êeŒüÜqçy¦}’i_Ó 6\4Ëvµ4íáÏšÈs³ä”[¶aFP,ÇJËG^ßxþ©‰Nûx ¡º¹4­7ó½½âÜIfpÓ.û]o¬ƒV{ùÔèq{>ë 4'+oŽ–óó„~ï>R¢5.Öq"*Áƒ÷¤XtãìÂFk6T:?í=7áüÆYóù I“ %çY½•¬­szW+L²OÅ»LrÀç–-ˆDÇÊzu:"d4’ÝÝÊúîÞ“ÎþóúÎÓxÿ˜Ë.èùáþ髵£˜ZÂÕZrtWÉ.“Z£b‹£åÝíÔS`vt¥€·á&oÈAD=Üh潶­-e;kËGÏ–Ÿ_¾óîÜîƒTo×[^.M|}ÖCªßè~3Jk×jË·c• L)Ïú7ø®L{h3äM¹DÅzbv#VÙaãÍ)oòqA> ¡fÆË@ä“xu%ÓZ‡Š©6&$2íÒÂÕÂÜ~´¼šhn%›\¢Ë´· v®»é(Ä·dkk4•MªOD[£•F„ _>~š®­š¼"X˜‹Ly˜<¼ °ÊÒµ¯‹‹0¹ •VµæåêÊÇŸß~ïoº{oÄb¥µvùäÎB%ë—++š›/2ý;RfȃGo¼•míž7ãà°î‡MŒµ¶•œøCéJg«5<„Où ­Sz˨­ƒqäÇZa€0Yr±ú˜¹8‹R¹TsrzÑŽ*3n&¤×3ý‘§cb•r¸ZpÓª¯gšë¡h©¾zcóι…ãx}ŽU}LRg¶o¼óüãŸõ7ïùœ˜\$äŠ-(Û}Ìèra*8na™Åi7ýggMfS6Òâæ]xÔ=Ð#DLÔ›ðm³ ÇÃÓ×[÷¿¨n<òé|oxðüðágÝÕÓ£GuvîÆš«µÍ;Ù•ÛJs/RXî<>¼÷q{ã6¤69·”îî';¡ÌŠ'”›°oL8ÏÏøƒlÂÏĬAˆBl€KZ…ŽÕ”Êr´²´~õÉáãÔêB²»Q^9MOÔÜÍÍݬ¯>6Z{.:ê¡G—•¡œøT_Ì-³©%!·Nê½ ŸwÚŒ +™`¸lƒ„%” ZÁ ñ™KC$÷âi\N+ÅaeéfkóPÔ?£kÃC@Ø ˜ˆÖÖ»;wï~¼|òn´±)dÚ~^#ecnó6/y9ŒÕ1¥ +CAÈ9,”ÌT—”tÇ…«ö@Ø-Îâ$âNB× +ÃLk|äì„;JBÀaã .^Ët¶º;÷õ涗M¬ož>yõ¥œ¨C‰òÂÍÂüÍTs_ά˜ü„ƒðÚÉíWåîî¥Ñ"³j@(ð©y½~9¿p#Ý;"„ÔÍï^½ó&Dx'…$ÒÚ~råñW[÷¿Tê»N2 µ¾ú›•Ý;¦€|Á„š!½¬–wðpí¢u¡áêà ÕÙ·SNLõÒqht¬¡WÃù%FÉŸÈž²‘‚L,ßZFÙÊfñÉ(DÝ ýPr€+5-ݺÿòã›O>¦äBË6×oÍ­haÒ—IϸX;ÂW掠k.™Q?äµEÈE4dxHý ¯ÅE5ûÛßþâ&Íè¹ ÏŒ3äc“¼1§åWcÙÉGÛËû¬VpŒ¦çjSÉÅÎu·JÃãÊÒqqþ@¯-K©V¡wyáêÓùý;•á>ˆ§V^f“}D.(©vcñ7F·²DF.¯ÞhlÝ-¯žê­úúõîæ[ï^{ó pÃBo÷øé·_ý`ýú«êêmµ¼Bj£{;½oÅ´/ë¡.ÚUd¢-1Õ#”2kÙ0Vë.22íÁ€Í˜ø×+Ö „Òv, ᫵rÊ'z±[l¯õu1»Œ)MTªàBÞK(c_¯" %áÆø_,~i´p€›Cù¬ÔáM”O+aRê[0º@eãN†ˆP̉±>6ÌéE%ׯ/Ÿô¶ï F+(ÀFŽžéøúŽ;Ü}x¥¼”™óRº-1·"¯0ëfœˆh'£ÙÞ^oç^eéD)­¤‚ÉËKѪ–éXƒVJÁPJR¢ƒ‹Y*GÓÆ…(ç'ýç.¹ÏŽ9§í´ŸÔ‚”JÊY`þ3ìV;í ¿5î<7ãwÒ3žÙEÓ‘l0¤¹x´º«m3’žênU†Géþa8¿Š–µLÇh¬PªAȪ`TØX%^ßNv®@Ö³"2Ô0k…¢µ1 6e#` #\Êêã¡ÆÆf}ˆÁ‚\‚Õ«je5ß?è®ßXÚ>9~ðvó¤4eíú{óWÞÞ¹ýÑÊÕ7sm9YW2ÝBkmïæ )7ÐÊ«Í» û¡’ííHaÀ¥êÉÒàÖ“¯>zßÅ$Âù>½ U¶–OÞž¼Ÿnm÷÷~ô³_Ÿ<ÿ \¤Ú[€ÛÓãgŸ¾ÿãþòïÿë;ßÿåÁÍw¾øÞß=ùàÇjm]­®K…U£u¥³õtçî—ƒ+ïájƒ0øÅ%+ªXp@HyùŒPX«®>TJ+AÞ¨ ¶Yµ,½pq1”îL2z»¼p²yó*š—ŒŠœn“z«´p£¶ö(\Úñq% !3d¾-(òZÞÏà­4öõâ,Ñ)7‡ËÅL{V+vBE#åÖæ£Ââ5>3Jvi½ê"G³ÐxÅr£”­0±ŠlRj ¬üÚ{n®ƒ02±¦›Ð,žÜ$Útc +z>ÑpP1;ÁÂy!7L¶˜x)ÃÈ„O‰‰¦Õ/MØhLÈHÉ¥Ö!¥F:qíüTª4È>Z‡pá%c3NDxtg Oĸ$ü딃ºdÁƬ‹úBÉÑŠ9r6œífº;^6™ÈÔâå¥æÔ|_Î-BJ +gæ•5½ºB†!Õ’DHˆé9¹¸ +Ù8‡”ËÑÌ0–_œìïã}¤êÂd‹¿dò›tAˆð1:RñÓ:˜©•Ëƒ+k|O_äæ¶)-£d[ÝÍÛze!U[ˆ—çI¥€p !Þ’m%ÓCÅ´Q_ÍõvBñ*®T|8Á'ÊJªžª •|ÏŠÊ©Ö¶RZ +çZ«7*ƒ½D¶Þî<ÝÝ8”Œjgõèðñ7Þþìî{ß»õêËëo}²uûÅÜæ᧯ön¿ê¬\ß¿÷QsûI{çùÂþóÎúíTûrPHÅÒí{ïþ°³uç‚›pRx¤2zzû¨8¼EDò¡Hæé«/¥x݆ªNÖˆ”WR½Ã…+o¯Þø¸±ó 0fÿÚãg¯¿_Z8 Hy6ÙCÔªuªÃÛ·>Í/^÷±ñÓïû»Ó>bµÉñ°T®–×ÑÅD«5¥¿ó +5Ûߣã O(EEk¡d;3·G«6„àIZ-±ð0QDʉéncýîõ÷þ¦º| +mÎÎκFGØü Šö¡·G©e&’‹°;¹ù›b~ W¼4dê&ˆ3Ÿ˜³#*D%7-éh*ÄÆ¡'ýD¸ì¥fÜ$€Ã‹f|ÌJL9™)nÓ³SþÑsVâÜ,f§âBf.­H…!Ý献Ñ$-‹Š “—ö…¢¤ZŒUÖ­}½¹Ç$š!-/tÄtÓ‰‘Ì\vp’ê^ÍÍ]#•ŠöÂÃÌØqE¯iɆÕË\2y.Îx&mda\(¢\Žó¥þ^PL2j!’í¦û»Bn@ë“lÿ““ j@ªxýr¼y0šóŠI€‰Èé®–AfÝdœóRª£Pèuèz‹ŸŸr8o8‚¼ÙÇø˜ÂhR$‘LWÓ¥>«¤åX¾ÜY®/m—æ–ÕªK…žÞXÉ÷×;['†ÚÃ+?î\~#)éUB0|¤ìE˜d¦±vð ÝÞ€¬N â…%;Ï%º^6î +’BXݼr#À&µ)näúG ‡/W®½Õß}¤—¸pêáÓ÷~ùûÿ¶rôU«zkßÜ¬í½ºöâÇkÇozJªôÖGß_;zn†Çí ŸYŽw®V–ï\¾ûe¼¶®gº_~qüì‹i¿4‹*6hÌhs~ïùþã/wŸ}?RÙ(¶7Ÿ|ô7ÙÎŽ Ûqr~îdùä½koþ ¹þ@Î.]½ûA²¶qvÚaÚ?íâ|L†Ë©ÚFoë~H)žÞyùÙOþ‰Š”P­-ÖùìŠ\Ü\>ýpûÁw³½¿ùä“Ï?Õ +óK6:ËŽDl†Žv/Z˜ +¹sðàôî;DþÆË7Λÿü ë™ ¿æÙð1`¿êü>­U9£CªȹÞÐh1#R.@È +2Ñp²æ&ÂnRAÄŒ˜×;ÕÅãùýÇrqè£ÕTu5RÚ©ÑcV³~iÊÅZý")$x%ë²VB†"V76ë¡}BÚ/å£Gü +~!gE1^Ó«Ë…åÓ¥“wúWž•×hµ]“õ`¢˜¨»e’þ´›w#T¸dÔ6°pÉK'ôüíD"AJ‹eÚ‰Òu“Eù8©äød½¾zméðqoëv$ÓÕ }%?Œ°0ÅG‹” + +Šâœnö†Æ¬˜ãëK< U©„Â9’K`!Ý £B–L,Òq)/À­x¦]ïm;›Xdt5³¾r37w-Í…b…šÇÒŠžC4ÄIMËùVÁ¥ŒŸ‰ºPÑàX)£å˜DÏT¢åu½¾%§ûÍ•ÓþîýL é¹RgóðQ^BB©xe%Z^’3=V/ƒ”݃«ìh^¯ä¸•°#>É.GKB¼CJiQIê õ…]ø€ÖÚU»¹áíüÂõxu ”3!.m]îÝùÑiXN­Ì:©YpÓhåáóOŸ¼ú†Èg§ƒ.q7‚JCoìéÕÍp¬ðÁW?}øög&;unÂ;ëþíãÊö3¨›‡¦…„žëè(ˆ*¦”ü°_uñ0×ÙBÄ''ç׎s­5vQ6ÖÀå!¥ÝHÈìDg¬^· ²ËçwpRÎ¤æŽ„Ñ Þó¸R%x½·°»~å‘VZ  µ´¬”6}|ÉqV …Óv/ãðqA:Ž±)Z*DÒsË °`\FT+B¤Pi®÷Öo9¨¨—Õ}¬îDÂf7;Úf^OVgÝ,á >e£|T ƒ|!gq) …ÙÑrJV²ÝlwW+oHé…Êü5­°ä¥´jw;×\³$ȃnBõ0)+šô³åÖ†M²8²šŽ$3N IÙ:Ja'¦#|Ö…Ç€„Å™TP1Î ÔTã“TvDÂGwOóP8«i%Èh§38PeÂþuÀ¡“-hv’7?D†Fëã «Ó±ó“žKÓ~³ÅÈÃ)ñd#W] +»•ôP$\ YG¹óõ~Ĩœ› Œž Ë.U–n´wž46î8R€T16æ#Ä!úH¶0ÊëŒVP²%Ó!¤”¨¤E-ƒ±š# â\ +ö:ÎÏh&À%o  4'†œ yã4¥Ð%´¢‡ÑÝDDV³k[×öNŸ +zZU/­û§Zq3È&¼çð°SDòSª½˜Œ°:&æØX›’Š~,Qó{{·o¿øÜÍh6TðÒšøz)x)f1cÀ÷,nÖ‰È.B³!a‹_p 2%eaãQ!d”Lc5ÙX–V”üPNÍS‘²—Ô£Iµ¡®üL\Ë/Æ+»ˆÜÒ²ûå)³ÏæFx9ÎÈÉ3cös“n“;„py\ªúÙŒ ‹A‹Ép@ÅK'AuxÔÃÆ ŒB™4ù^ÆÙÙ†Œ™$>69¡c*dŒ Ç8zprÖ7>휵¡(©NÌú&ÌAW0$cj²ª$ÊB$ bÿò¼åì„û”wrô,nrl810@ÉS¶à™q.òñf¦0ÇÇ&ióÑã–À™K&‡õQa'&PB¬ÐXL”æ¤DÕO)v?9ãð[½,ƃÄTÄÈõóƒƒKv|Ââ›0¹&g]ÎÒ¬‰çHASÓ%RM[ƒ+'kíåL±ø`˜r°Q…$î¡c6Âæ ¹‚ÉK±ŠŸŠ˜]¸ÍC:üŒ0(š´×Ôx¹X_ÌÔçq6Žç¡—m þG?&AA¾1îž4£nd´kç'm&Ÿè¢’¤R½pz  ÊËj¦·zÒË€âBz3š›Ï46"雌Z¼ ÎÇ 11º³€½ˆàÇE›œ2»Ív_<Á3‚påcM&R„Jp ‡˜è‚æJ[Éoóñ®˜ÁÏÛ<¬ÍMŽÏ¸~r½¨—H)5mÇÎNØgmÁÁÒÁöÕÇ›u`v7åñóœ”òa’­nj|Æã'dRˆÃP€>ûQ%x‚–ZvQ³Õî¢-BµÐá=ß;¹$ø‰I×4äë‹ÖKã.PŠK€ÝCQÙ}’‹¹¼„ÓåŸ2ÙǦív·_U”B.U*feðüæÆeÂæ7y§fƒ~TvzùK“Î3çfμ1cu²l¸D‹Y·/D³j4QГe)Z‚’ö¨`­)sà æ c¶i“Çæ$h!åÇäY‹bÆc²avO¡:eó +'Æ,\$‡2ÑKSÎ)sÐâbð‰–p*js…&¦——uÇ”gʆº ž¼Ù$Yžb)^¤òÙx½ßÁEÁ“‘D6_ëDÓy\ ;@0 ÆùZO˵ޘpŒÍxœ>P«t4š +ÁNÈ\¥”]ÜØP³ÅY6å@ˆ„±*#«±d<•O¥rÆêêâý'oVº+ãfÏجklÆN2"Ç󚦈’äõEN¥â!ž²9f]n;E #\(&öÖvwº[‹ÙFVѪúhdx´Ú¯‡Nd5SDUŽèI’ÓE­ Õ Øc²øá'2¥z¡;ÏGÓF¡ƒñQ?ÁûqÁî¥.N»¿qÑj ÊÁPf|6Cgu"bÄ(7çûËkBX™±#~R‘¢Ð;e2œ òz®±(ÆK|¬D…3f7é†*ŒÒ"'jóc"Bk”\ „LJy–N'£j4ƉR8¦H”¢P†AZ­ÜâÊ̓*Ò^„÷ø˜&!¤êˆNïCeŒŠú‚œÓåt»l.—C–åz!¹PÏlk7ç¯ _¼|¸µ³¢Ç¢(ö ¾ó‡üä7ÿü£O>{¶¶Z‰\Hâ1!=Z<—ˆ³\,Š×k‰^'×iåcŠJ&^´º¼.·'"KÕZ©5h-o¬?~õQ¾3çÄP³™0<€^…øD —f¼çÎ]Ä1ĈÇËåR6—ô=^ÿhY!\«–ŽN¯7ús‘Dbeg=‘Ïy0Ê Y%EÈI%ÝÆ8A!&¯”‘**ñ˜–KYØY©^Ùöš¹ZVÙš/<º³óòÑ•_Ý|ûéÑûóóM#™ús¸IŒÒA“IZ"0BŔЮD›åèBÇ8\É<<¨¿~°þå{Gßyuôï¿ýéï~ýã÷ž^ÝÛYj4*’(ûƒ¤ÇrÇàœAsñ ”&Žä¢áj&š×…~-±<—´ ˽Û×÷®]]¹kûÕ³“Oî_9nÔ‰dBX¯×31ítxÙi+vaÒ3c%Î_´œ¿hžšqº\ˆÇéY*­+Íz1ªˆ2G• +™b1Ÿˆ§ŒD.€²g/™¡aí.<èu „W¡l•8r°rÔ¹s¥ûðtá³÷n|ÿ“ûŸ¼½÷ÙÓá¿ýüÙþîË¿ýÁ‹÷ß¾¾½>ŸJ¥0Š³Øœë,é³¥D¤Ÿ!úYßr•¸¶š¸¾W;Ú©ÂëæaïáµþÛ÷–_=\úìùú¿ýâýÿöûïüäÓë÷öK™asØM6?Á„IFB‚‡zš:¶ß‡Êó£Êçon}ðhéõƒÁç/Öþå§Oÿó÷ßù—Ÿ<øÝOîþço¾ýÿ÷ßÿýŸ¿z¸ym¯“ɤ‚(ï p XéD¤š“–ÛúþbrP$«l3Ç.uS+sÙµAæúnû£—‡üýßþöþù矽xö`ïòÎ"Hô¬Íj÷ànLº8ãxå«îüâg_¼õòA©bÌ/-öÖ®Îï=çã5ŸË ¡q«Ç©ƒþô¸ûðJãÊ@yq½óÏ?{õ›Ÿ½þøÉæÏVþõ¯ÿÏÿå¯þôÇïÿê»7ÿôÛ׿úáÝÃÕ¬ò{]‹Õ‡â0 8uνPdï_.üõ§§¿ù›·ôé­/üàõüîÍÿÕëúÁÝ_}yíO¿ûöüâѧÛk;K•´¡e2™°–‰Ä +¡—‚Í$¾Zooæß½»üæqóùµúO?¿õû_~ü§ÿøû?þËw~÷³çÿãß?ÿÿ¯ÿø÷ß~ÿùIçGß¾ò/?{ëƒçI]±Ø‚c3>›—¡B¯ÇôØqŸU"]abv¾:]/œlVöÇ«ég×çÿê;o~ööÑgïÝ}õæ#‘t£\“8’Ì«¡~–Þnò÷ÖõÏîþúÃëõáé¯ðôý§ÿŸ?ýúÿüŒÀ¿ÿÝ‹?ýá»ßÿðÎö°œÔå'izš¥©0ƒTuj©HÍ ·Wõ·‹?yïò¯ôôgŸßüá·~ù›ÿÛ/ßûÿõ»ÿÇ>ÿÃÏŸþëÏüî§ߺ³VL†9ž±:ý~à‰ˆVˆ*Ã÷r7ñÁÒ'wõîÆùñíÿþÇOþã7ßþ‡¯®ÿá§wÿ÷_¿ý¿xþ«/®þÓ—W~ûÃÓŸrxs·R)¤œÐG~ +sõõ^fµ©Ý\O~õ|鿼ö£w7ðîοýêÿ#齟%;ÏûÎ?ÂeK$ÌÌ™›;ç>¡OιOèœs¼·osä;9`€$DH"%J )QeI«,J²×’¼µ»²eÙ[åýaË¿ïÓPU×0Óáœó>Ï÷ûùžð¾ßþÅÏ¿ú{Ÿ?ø÷ß½óÿþãþ×ÿóÿô7¿ú÷?÷ü§_ýýï=9žæp4t}e5C„Öy~ZS{vªkFmŠ¯Î‹ïœ>~ÜýÞÇG?ÿµ‹óäÏòÎÿÇŸþÇ?úìþä[ÿò÷?þÓßùàÖao:ÛÊ7§ RϹùš§ +ÜI‡ïíé_ýðÙøÙ;¿ÿÝ;¿ûÛú£·ÿÛß}ÿ¯úêçŸßù‹=ÿ¯¿øîßýäÍŸ|0üÞ[­ÛS­âˆ+¬¬F.½±’ m.6) cåÙaþǽÏ_üùï|ðO¿øüÿü³oüãíÿäëñÃçñ›÷ÿðÛ‡Ÿ¿è<ÜÉö Œ£†p’EpÙ¬ÛôG{“Ú¤"ÌÊäÃMñWßÙøÙ¯\üñžüÑo>ø럽üÇ?ûÆþóoýâ§ïüâÿù^þ׿üä?ýÁÛ?ùÚöwW>¸è6JV<Å“RNRŒVÕizL?OÝe_ž8ÞÊóIû'ß<ýOøá/~úÖ_ýøÅÿü¿~ö?ÿËþî·nýîwŸ¾ÿÖMŠd“(—…ì%Ñ91ÝóÈÝ{ÓüêEïÇ_?ÿùçþäûOÿåo¾ÿ¿þÇ_ýòÿí{íý“;MÇà(’BI6+šU1Œ¼)òEÛª«÷¶ëo·?¸Sûƒoßü¿ÿÑ?üé÷ã“óßxû÷¿uúg?xú£oÞ~çÞàÖ^§Ýl«v•×+‘ ‹ÍÅ-×r¹‚B óÌ­Íüݭ“ý¯¼ýѯ?üëŸ~øó_{ò»ßºó½N¿þhãÓ‹þ›'õƒq¶Uô`#)†U¦Ûç©äƒ-ëÔvS{çV÷ÇŸÿñ¯_üíÏÞþðÕø£oüÓ_ï/üò¯~ëá?ÿÅ×ÿ対õ·¿}ñÓ¯v?¼[Ûl(Æ0ÝnÊSYquŽle鎉my©ç;Ú§ÛŸ=üÖÇ7þûÿþýþ»_ÿû?þøÿþëïüËßÿðÏçÕ?»ùùG;Ó²ffIÁÁ˜l­1RDÑQ™ÃaaT gEôÖ@xç¤úõ'›Þ¿æon7ϦÕLJí÷/zßx:ûæ›ßxqpgâÜgoO¼­6ô:À00?§¹(UEÍÕJ劣—Ma¿[¸»Õ¸·™ûìÉøÇ_;ú›?þ§¿üìOðü·?Üÿ­÷fŸÞ,¼wà|xÖØoª:'± 57EYÏ!Ò(M¦ã&l˜äA[½3_æ?¹hüÆ{ÛÿðÇŸþÿü{ÿô‹ïüÝÏ?ùíOÞßëŒE€Þ0®'q>•ˆã©¸Æ’Sl¸Â¨¬îw‹½úÃÃÆ»·û?øè쇟ÝÿæÛû/Î{' FBkxòF­j—¦¢hI4Ý*Ø··Ý9–˜{3û£û“ßøÆ[/îl=>jÃgO7 +ýªåš¢©çD3Ùébb™Ñk´VõÊÓt9ÓXºlëe[©»Ò^?±Û~|côà¨w>«O›¥I­4(y®)Q4j2˜b} ‘J(©E"Éx$’I¥t–ÖªGÓÖ½Ò[7Jß{o÷çŸßûíoüÑ÷_üѯ?ÿýoÞþé§'¿ýþáçÏÆv]KL£©8#èQLùÊ88‹"L5Wì–òŸè»ØùÈz°å=?(üô7ÿÛø­ÿòw¿ñ?yõ³ï>ýäùÞã³îö¨`XºhVôür¸`'qA­N½›Ó„²É6<©¬sõ¬<©y£’µYïoWÞ¼¹yw»Ùsøš¥ºª*±Žs8¿dV°Ed¦!ÉT’Ç1Ï2+…|½PhæÝ‚Î{2[Ôź«˜ ¢ˆ#+Íæz:#% 2r®>…ž…€Œ’É%òj¥ØÜšn¶[Ž¶ÛÌ>=jµÔ;õ[•Ã–yÔ¶¶ëÙ­º[7¥ÈêrÀŸˆc:¥´Dva9þ•KkË«át,©±l^“Z®ZR°IIÜkªvòŸ=ßþö[ïݙޜ¶]ŽepFËÖõúAuöœ +ÏxJàECQ APTÕh5Õ¢]òÔ’+[§i¢¬^‰3ÊJ,³@c5ª˜ä­Ä骰’§Î¯åÕ‹•6–JØ×€(V®˜ºÃ³ÇŠ±KÒ±4N±«r9D&ÙB¶ql·oJY´j$oá„ êã¡MdÙ ö'“£^oÛÎz½ õl#’"/-û×Lœ-fô>gõäl‡`­ÅåÀòj(CÂÛ<Ã(˜†]p¬VÅÙ–.Ú7&¹»»{ã½~uTÉÊ^Ísbñô¥EŸ?1¶ýZ{m)²¦1 ñAµs\ªNx–5$¢f %•(ÊØɨúá³»½|tïdo§×qu…¡I' 9— Ý8í¬§%!¿Q™>®Œ*;¼ÍŽî=zÿ/IÑše•‹ÅvµÖÏ{5ÓãQ*…¨+ëèÂRr9HG /ˆZ´ž¤\V-GÂH:Æ“ÄJ[ ã@æQDJИÉ{(!‚ñd2 I ´”Gèl†/òÎP-͈ú¥…` Æ¢d–`mÍéÖg ç¡ÒÚn*Ïêù\¾£«2BÅ)}=ˆ_^Œ%)'#”×Âô¿ûòò—^_]ZGŒªë®a0ŒÁPBd%KÑY4­ˆÊfTŽ5”¬¡–|%DˆÞ$?¾¯•fJaÖ˜]¨¥^œ’PÁ•¼A®{f·1ÑIS²¤å:“ýåzy-±ÈìP*ïJ…mÊhcr…Õj­»ŒÛ_Aô¤8(#-”B˜™&³ÚôG°ËËaèD”Í3F'#×㤣ìn2_L¸½šW“ÊzJIsÉíi…¡ì´²¥­à ¼ÙL2nš/Pf‡µº¼ÝQŠ9?`õ"k5\1LXQ2ÎØ)®drDIR.º‚Ug´’`·­Ê¦Q©N3CŠ4¯(fž×Ë‹˜?†'‹·'Jý,)VVãÌ•µÔë+ñµ”œê¼; ¢òJ”Z‰Ìçä'ø„ëtš%³Ø˜`œ#ç'“r-Açâ´ĵ7VæÓ_'(‹³Ú^ûŒÐ»AÜ ¡jŒ4õò̬l/©`R„k6yo£sÔˆ6köàÈø’Òr˜Ã¤F¾wK.Ì8g”äKK1)å9/hˆü„ÞVëf÷¤8¾­Ööâ|Ñ(ç6&w(¹ +?­gVýØmŸë“¥´ñ†Ÿ¼äÃCØü¡¡_zcíšc±Ù»íônÊÅí`Z¿¼˜ˆ¤d_LúòBlÁ‡§„²Û2ëçr~;ˆÛëiýj€ZÒ¤à%3ú•E8¾kË ”IÓ¶lµx¥¸²÷‡ÒhFAÈùõ¬@ZYKH>D‹0ùé~éuŸR?D´©•vn-M΂L6)—â0¾ÕmèŽùtî¹ñüf›ÆŽ^›ùIm1-ú2vÆ”6ž–vÞrÇ÷‘ù´±&•í ¥=¡|€è½¸Xƒã/A5ÅÒ,Hd¸Éyzû®\;¥ìùdq6@;ãë„pÊvåòÌéçw¼Þ¹Ù8°ëû;·? Ü>–íºÓ‡“÷G·>ê¾ïMîUfO{à–7§7ß ÒÙ„X„ÒʇÙÞÝÒìYqóY¶wžæÏœ²îˆ4ûˆÚ€ú¤òSÚ voßý”¶{—CD€rôÆÜøI¶÷7'1®†®á‹¸R ãJ8£,&¥µ´ÍX)6Êõ(SPÊ;z}?)º1΋užÂÁt@mª{Q®ÆŒÎìž\˜Âþ.Åù¤\Eõ.fŒèÜ6av@Ä¢¨,»=17¡ŒN”°@ô8oÓhÜȶO³­ë …µÇœ·Eg'ð[!:·†ÛW#b˜Êëµ#L®_ q.—±ºL~“Îme¬¾7æ ¡·b v0¥4¤ÊaùÒnÆ™©<ëmšã唥gJÐ,ðýRq ¡ó1Ê ³ b­Døù̽¸²i¶N2æp-Æ®D˜õ¤ +}º÷Ú‚ÿËWC¯ûÈ]âÜ ©¸Cš½õ(ïOJŒmÒŠÐ^˜ÉÅØ<´çLa  ƒ»ùþ9BJ.V[®nÇ¥”Çzy:Þ»øê¯ü¬2»ä¼LvìÖv^¶ÞÉn&o ZC”Z”wÓF=Èå)oÜ>xw|óÓâÆCÚ›0ö :{B؃¤\c‹;…Í'jã„ÉÍŒæ µ¶{9ˆ®#”QŸZÍ™TÞŒðÕ”Úãsóûí·o½¿s烫ж)™2‡ÙËÁÙ§rë³F¤ÖxöÑ +½ÓPƦ¬þðäݧŸüðé×~<¾ø„/ïƸ’ä?úÖO•âæõ˜ãªBé(;xTßµyûÅéc\­vwîç:‹ 6H»¸=Õ»÷‡g_kí½èì=g"7¼þÍᆇõ¦\i—tg ~¹áýîþ È/¢;±{wÙü“›°Þ•[´ÑÞ¾õÎW¿ò˜äËzíX¯ŠåmÜ¢j;ÁW(sP™=†¿_ ~Ä’Ê[zëÈéß,Lî­ñF·¶ÛÙ¾š£V=£s3ÜM崙h—6î¿ø¦âôþíkk×À3µvZÛ{gïñwÁküiíΛßÕÊ[ +’foÆØBŒ-‚7i³ÊÖÛû~}óî×$·ãÖËúæM:Û’òC½¶[žÜ®ÍÔv[ýS&Û.öoÀïÀ€’NZLŒÚD®îçƸÞL+U0;wð°´ýnõð]oö˜tzVqº}ç#³µ<¹²ëôoO^>ùìäÍ_k¼0ªÛͻͭ 1?qº§µgNÿNq|g|úîþãoØíæôÖððm±¼u‹èͤT‚ʘ#¥vLe‚×ëì=¤ÝA„ÍͳFõÅÎoÕöÞÜüjR®ìÝóáù«ÉÅ×ܧ˜=ñÓ¹劅q~rk=ÃsùfõIw6Ìêž³… `Îka‘*¼7bÜ™ŸÅ¡zCkž˜ÍÃcÇi³Ð;rÒ¨êõíòÞS»êtO¼Á-¯sg=ÐC­q¤ÍGy¦6Íîm¾zä]È•mªV&·ë»Ï„ò®\ÞƒÅô¶U?ؽóa41¥:¾ñæÝ÷sçÉ7;'¯Š›O”Ò õñW>øa€°|ѨîÖwž×v_TwŸ”wžÅ¸òÅ‹oŽ÷§WÊåí'½“;G¯*Ûo5?$²“¥˜BhÍe«µ}¥q$Öö3Îüà€#Q\ ò1*…ë½…v-L,é8[!­çLdoêOɯ/Æ€ ¡§0½Ÿ’›˜Úñc§¼;Ÿfë{ÁŒç P Q:Ô’ª„ÙSk‡^ÿô2­Õ6÷.ÜöN’³EwPÜÙ8ÿpvçãêÖC¥ºÅf› {Ï|ˆ<¾0ñúµí·s“‡ÞèÂêœðÎ ×»"Wv²Ã sx›…ö1»ùî­ÁÙGrm'!æ+›¼ÞYw÷áGÎ^iµÁîNvu÷RfËkž<ùæáãoOo<¼ùQqóèÖn=üàäÁ'1Ö Ó^Æêqù)Ô@¶W¬áFÏkîÝ}õ¹QßP´;gRíXi·ží?û|tö¡YÝ?¸ø¸{ãeÆíjݶzŒ»[BõÄêÞ/m>£Ü cÖï¾üNoÿÅüiG¾„=ÔÎS›…Þ­êøîÎÙ³ÞÁSÄ’óe©7BÐ’B”¶{üö\6żÝÞ÷F·¬îâôžÑ:øq‡P¥'Æ#ín¶Ó›<ò¦O«{ïå&Åâ6ЭŸ>öë\ÍØíœ'²íµ¼ ›Q«£ã“ãVÔ~ƒ/lèÕBïèèþ'[gï„2–è{χ7?PÚ§óÞó›Œ3>}øq¾¹ÅÔù¯Ó;¹Á¹^Ù–K»„;[ÏxK •Ëvã´ÓÜ{¶ýäW:·?±Æ÷Ñ]{p‡t·—ã:Ÿô¶Qz;š‘SŒÎ8˜Ô`#ÍA”ð®¬ã‘ŒÅXÓ¿â˜C[Sʚʕ§`ú´ÓR%Á•ÂD6£ÖçÈ£J„õµ.@Xk*å‘íÄ(“Ñk€Rz}×é°î Øß9cK[W|(@8”VeóQyóWœ¥ÕÚ0¹RknÜç.©Vê;z'¯àøOn~<¾õµêÞ›Tnç½æîcT¤ñ„ü€´AsÆnûÐëÝ ì"xFqhUú…Þ®ÕÚ‡î&²=6[óZ»Jq²gH£ ‰òN„·ùÒ†Ö86š'VóP¯ÏbŒ%Úíó翲õð›jë†Ñºá‚ªäG¼ÕÞ8| +–kÅÙ³Üô¹Ù»ël¼à+'PEkiÅ-o?ø(ß?~݇¦¥ú<§ïk­Zm?[Ûi O?ýö^~ò?• ³EÌJµ“Îɇ[O¿W?þâêñw¾þ½?(N× 'ãmcî.îî”·ßÝûŽ;ºˆR¶^ÝR+[_Ìî[XMK5ÚØ Nw?عý®ÝÜî=VK3ÒêÑö0Â×qœȳ4¾/æƼʳŒÙno=¾4›7P¥I¨B÷تmaJ«YÃ{öè®Ñ9v”ÈÚí#ȘËQÆ—–q½Ã”Œî£s ×[i¡Xݼ9>Üd!Âú03ºvÿ^¶qdÕöDg°žÖ°{)®¼W Š®……µ”™äÊnûviô”Ésí#šˆpi-€ÚKq%ˆ9Œ35§)¾ +À̸}è»aA‡#Of‡L~–a6Ÿó¬Ýög4*Û¶{§FëH«ågÏ1g ½¹/V·ý„™V .õ « º'ægzí¢z‚ÎV&wÊVûH(mn?&—’JÍjÝ—O¥´Xbœç œÆ^®{¨V·Ö05#õê4)X´V¼d™Üè.,av1½ Éz”3ôa"c×Íîaeó~ïøUcÿå:¦„QI²[ÍóÜJÍçˆ ~yz§:»SÙ¼ ŒDª Þ›‚V_‹PÐF}óü} µ²þy=Ê(Å*—ãt„²äÒ$ßÞ­n¿íÔ·ÂtN,îËåc1¿ 1"CZïi¥îÞ“É7£L.Ê–”æMgãM­sG(n‹å=Dë@`ß-nyí}HëÇíÙèP×;çöx>­znzßl§¸âpv÷{¿÷·÷?ø>(v7˜Ü–Þ¼™?´»gJiSvûçï=ü Ó[1¡P[k$v(gQ»I¾b•7º{P­›1¾æÃ槭H£™œ Ž_$X—Ôê|¶¯•a/ž÷wŸ`rc- 0ÏŠÙþhÿ.Õ¯¬“Kh­¶ùÌéÜ‚è—àëFaãƒÏ~ûÆÅ{—–S—}D€® J/[?qêÇà›¤\K[’à¨Æ)[Ìp­E¸Ó„ÚÉXƒlû¶Ñ83šGx!ø3ÞÌöSJ=)×q«ºÝ9~‡/m^‹1¸Ú%Á̶Õ9FÕz‚+$…BF«ºýsÊjƒL)•³{–Ÿ>n¾Ú¾ÿòÖ£„P ͆VÛZO)1ÒMòÀN¤Ô@µ6‘íÆ8[/ö»ãÃÍ„Pñá¶?“¦‚d øŠê-øòí›o³N¨˜ñ&\i*œP«¹ÎÉèæû&‡Je(ZBmB×[Í=µ¶™/½çrN/Æ8¾”HšM½8®NäüðZ˜»æ×#)Ve_ÖÆ8Kt»n÷°wð¤}ðÌêÎJkò¹ ¢¤Ä’^?hï½9:ÿ”)‰ÂRT2Ë›zm'He¯ÇØ¥ëÇlT©~¼ŽªQÒjÌî"‚¥øBŒÉAÔõÚg¥á·{‘0”ôæ e@FB¤ï j¾¶³w÷£úÖ=DÈ|Aì­nB˜:ŒrÅ@UkLïæ[G œ7k‡¹áÝöáÛV÷v€Ì¯Åe=7nÎîBãr“rÆÖ µýêƳº›¤éÞ£öøô‹™œÕŸ‡œÕ:x³wünZ¬úãÊ([ZO¨aÜZOkq¾È¹#Éë;• 6šLä¼M«{7Œg×£ì²?M«¤àe̦w“R‘jzm[ÈÀÜ¡æë+ÍʳgÇ„âVÓ#˜>ŸÎq¤âæ¿n^qú´è¸ý„r‹Q0©VR,ÅÅ™%är|VkXÍZL­‚ô•7î©©º#Õ¶Íö>WÚŒ EˆÿbeÇ@õ(gÊæ6qcªAÚƒ-O­ÖA„Ë#àûL1-VÁ‘AíãÂ|Æx±8kí?‘6lXmã"7¸ ˜!æ7ÍúÔsˆth£Ée;óéø˜<㎙„Ä(ë¦D/Å»Z~¬Fk z%FB̼dblŠV©bR…`³Ãéé`÷>¦×1³“ÖZàVœ3¢ŒŽ/-qfݬmI…¡V‚ÍžFøºTž+önJ¥Íå%›÷#&®´¹ÜvBn®¥èóÖ+HÊ—W0ÀÁ›´w^6wÞf û—ƒÜjJÃõîJ\ š?­&èBr†fý‘4êv} “rqÆŒÐvR¬±îX¯îŠÅ-óa¸­½\ÿF“ö¦¸=^A³ º\Ĺ“8åÒj­ºyà +J‰Õù|_iecÿa{tLJcÖ¬Úf~p£±÷(?¼¹–Ò!-‚b’ë—W2Ë×ÛFó¸<½/å§Ü\s ¶`6ÅüÆKÉ«ëhÕ`ƒá%w¼á#©q[ªA(¨F)ƒP‹µ’‹s2ºq©dËP~¤Ñ ¡²ÕØ·Úg`¦Fó”Èö#|ÑOº«Iá½(m»¦˳'RiZ)oV›4j¥Áieã^Œs#¬å]ð÷­»=ýNãðm®²ÊBÍîyŒ/‚‡©h—ZÞÙ¹õ~së®ZÙlÍÁ¬À-($àŒÙ‡ïÏ ïUöÞh!åB©w G¨\M +(ªŒÑ¼-ØY®kASê¤Õg`hòÓ›_E•8ãº'’;¼¼»´Y‰³À£l9.7"| †e«0B[Šó”3áŠ;bõ . +O*>ˆ=ý#«¾Âåeú2Ù”ØЪ7Äâ ëbB„^SÁäêÕãCŒ¸Ð ²S§}«4¼ïvN¯øÉ4“+Ï ¶W‚/­ú1\’ñfˆÞM« _œmOïäÛ‡a +e Š<_Z_Ž ”ÞÔK›+Qêšý×ùŸÓó].…(o™/ôcVw¬ên7;O€6­Þ¹TÚAç3sªK!¥--׃š¡*ÍgþÑ«sM Í@Z äü`÷­Mp>KYǾV= Œ>ÂW®ûð•0ãÔwK£³ëav-.-†Ø+k”®ZÞSJ;ÑLÖçøùÓ^µ$é®EÚjÍ=EoD(/É@» •ÀžàÁå7ÙÂ.îí æ(¥4B„ ”`sÁ´†òåŒÞåKûjë6WØ +ÚJ’NE»mJ«f”:k€ÀÕÚAyãAeë)n ÖÓ{kÙæAats|òrzö¶Ñ:Dvqz»yø\ªï_‹s`ôfý”±f´;CÔŠÓÆÖ£éÀ(H¥­ìàAntp 7û ¡¥]L,Â/Æ5IÛ¬=ÖëÇÍç•ÉÞ‡){1Êâj ƒ—Þå„ .–@ú@bLa=¥FÈ,Ô¶Q•ò^qóqaóAië™Ñ½mÔ⸶ù@.o'¡†Ý n¸Ü >>_]Ž±ÝêôäáLj\[J*ÃQ­“–ëZíXoÞŸYŒ°I¶$¸„Þ'ôžßœ~-Â^ZA1~>m¦Õ"¤ÍØ :G´T…|º”˜ÚÛ¹È6¶ý¸Æ{c§wK«ðÞ þz)&¾±œºêÏÐZ}a A„¼Ùد̞Üxû‡½Ó÷¥âìê:KóÕÑ™RœBe‚TúÒ + @$ÃäÃô|B¥¾ž6ÁÁßXàhá_Q>Ǩ„Qh-OéU0>88Kaú+×¢¿|9´æP¶@k5_Š]Ž’I6çKC‰:´Ù§­~ŒÈ_÷S뢜½D¯¯¢K~“ÀC¹@ZšÏ•aOäò!@ Àj0“ÍèNhÜèÒöبß0Û7¹âæ:ª_öe™l’+ r9­Tùü†RÝ#³½”óÇñ`ORc5!Wæç×hޫÕÅ(¢UÑ,KÙg7©l DuóÑö£ov¡î0Æ—x»Gƒx’aõJ“G¾»÷àóòö;k 9ŠªJ~Äzã”X†Ú£ì ‘Ýp{÷ÔòaœÍS¼hwwDêùÊ#lq1"®#:aòãJe/c4 +£3µ¶¦]P¶8Wf½Y¶w¡7!Îäx«:>}ü=.W«Oägˆ5 ²ƒÆöÃ|ÿ Z~Xß×æ {Œ²Þaí~¾sÜܼ‡Š\®Xc€O£¶Çç6|¬ízˆÌÎ:$‡•K»¥ÃÙc`bê•]Æê®ÅÅ(é¦å&_Ü3Úçnï6"—AKS|¾¶y´º1¶–[«Hö(ˆêP!¸X¸¶Ž0ZÕ,M§£UvRru9!,‡IN«ÈvÏ7hÎs”ÑÃåFŒòBçZ¾ x»ŸQ[‹!.Îâ|UçëŽÅÙÂ|â÷@Rñ:VuJëßÌçú£æŠæB¨HªËQ~Á¹ÍööÃ$Sx} …7¬FÁ³ªi&ïOˆ×üÄk‹1_B#f’/^žÏω‡1`&­tP½ˆÁôrßJJ$ŒćêÄç%WÛ[ÏX+)=-6¨êOóˆàú7§w¾^ÚzIØÃåùÉ"Âè¨èaj‰ó†Ru’aO€Q!›f”$)£bŽ¶»ŒÓ4‚ÀÈVvÈâ)!7JóN”0æžÞØ¿ž¼/TO‚¸ƒrùŒR‰fŒvµÊ¾Û†Ñg´³Ì8Á”$X­Œ”[ ¡!\%nw±*Uv —SB‰PJùÞ!"•B+£wè,”Üýìð>€%Œ;kT½î1ãô}˜Æä6ËÛOû§ïϯHNF2N©µçu|˜E˜cop|òîö¯¶ÞÖ›‡\7óÃéñ ¯}àG•µ”¼–”¡¶ygDJu”ñV#ÕTÆЭi±jÀ,\íÌo´Èm +†q³Ð9-ŽîÒî”È“Bu%!Á8fd`‰3° Lk8½ÛPÒ‰y³;Ì€ +YôãÞ%D§±ÏySHOQÂΨMZ»´ŽP3œ±—#\‚ò(½‹É­ä|Nil+FçW¢"x7× \36ü(%h½¼¥Uvƒ÷ß]‰h…p(Hn9®ú3N€p€.òÝÙ‡àS)-AÂ@tT~9H_Ì×…dì%Äøòrêzˆ¤xˆŒ;ͶÎf·¾¶q@èC±´·š’@šØl‡2Z„3Éx3:YïAyóå|½!\œ&ïv@µRÖx1“»SgZéÝ„ô&´o#J \}ë©Ù8ZKs‹Q,ÎZŒ;â¼Q’ó@üi½Vœ•¶Ÿù„\ “nJ¨fŒ^Œ2¡ƒ”ÒT®Á†A‚,¹–à1©ÌçfˆÜ˜¯9(W•ê‰R¿If7¡´Â^[Ž„3:iÖª?¥cRw†¤Þ”sÉí„3fFoÞ jƒ´ÇZýÔî^ÐÞ6ÛÕ$¿#§Ç/ÝÎ1&Ë—üd>L¹n5P©˜àòNû¤¼ù 2>Ë÷NˆlÏO9À²7ò{|¶³&ˆºžT\U›KQáò¾ %§WèQey>•N˜½ÆìIsç%e–"<Â6OÞnnÜ vZªƒoÊŹ¼Ëy“ùÝaZÌÌÆn˜q/ÈuÌŠ u +tOï§)g²¢Qíí=äó“u<{)È&ÄZˆÊ‡™²\9\OH‹ë¨durÍPÏËQáõ¥Ôå5,Læ@ü¡´®È(aÉÅÍl÷ªS›I®¼žÖŸ%Lé®' \ªóå}­sÆ@†¢saD_ð‹šëÙòlÑ~i!ôÆbaËi¶Âô0®/†!}˜¼1Wu¥l·K㛥ɭòÆ]¥º ”˜Êt¶`/¯w«}ð^eë…3zÄœ`³¨Rˆ²YÆHÕSÂÛçKgùþÃÊè”ß\GÄ$kSV+)ä¬ ,c<Î Ž^mÜùxÊR” ¤B-“fÌ.[ÞM(Íl÷naúÕº12[éê¥I€Èúpˇfc $èV¾á¦¥óüÖ¢ì Èhk¨ÊØ:gÔ›û/#¨©5SR Æ+DºÐ ¨\'çT–¢$À˜XÚ-ïNßK)Ý…»ã€^”Ü(ˆBÐÖ©ìP­ßœ|Ü=zÕÛË1H2×9Ìu⤩ÁÐÏï]¹©4o¥¤PÍ?6_?%Û[ RWýôH|~æp×h] r#J˜‹!"„H”Zö§¹HF‰Q ?&¡v­‡ÉµiƒÈz;LØðOIЫ°šžSVžzõÊ*BJU£¼›mžÈ•½(éEP5™Ñ­Äj…XF"¤¢[Û³j;Vó˜°†1¡vi\ ©ù-UöbMKq%ÎñÞFœÊ] Ð_¹žÈH¥|{?Í9 ¾ÌkËØJÚB³r÷‘;y 8}Œ¶r½ÒðÒâ/-į˜Å¤[„·'}qñÅ8«Õ»³G—–Ò¿t% oHI-±¼ÉXÐÅqÆN ÖîN›µj¤–OÐfp>•VVÍo˜Ö饅<¼A)Á–ä ª©…ð•í&Åeõ„Â6ånBûã*Ä78?=èÒÎH*@ †ê…ÀHšŒÚЫۙl1­-Æ„WPŠŒÕ1*{ÞèjöÓZ;ÌW5ÇA©ÇÅÊR\$íÌPµ›ä*ÀÆëˆq=|b'èb%BqPõ,ïlR戲йk) Ê*·”T“rÑû¬· .HºËIa!€@ áR„7Éçc€™CØ~¥v¸”ßXCW" w”Î%ø"a ­ †žïõ#Úî%©,Ä1D®B.[EÌkÞèœ=wŽ3Å>ߪ8ïÇ,Lëq¹Ý¥„êCthÆ$ã’ri%œ™ÏVMÀ‘ÏF4ÅrTŒÒyH눺¡“Bu7íÞ=`?ª]^C×#lã®SÙˆbª/!P-NX „׊?à|«»óá×@+…•˜°— 3æ4FÏMУ֢„è µº‚ˆ±“¢P¡”¢ìµ0ási¡Ž}ÜƸÆõ¹ȬEHJÊ“y1µÄÂüŽ,µvD9³kQåKWSÀl±&d‡«1^ÉM8o†Ê:]ÐM(ï‘Z]-n`B~>ù ©R󜒠²¬ÙsS­¼Å9}ÊhPf%Lêóåó",*TiH³…Hù´àe´*i¶iw§‹I¶Hš=±´]ß}Ñ<|%U¸o€AŒÙy(#½SišÏèUÙbížœŸ-@Kµ0WF RUn0…]ð¬ ÜÀ¿¸©Xjf)*†39Ö™"Råòr|Ùje-©`r;ÆT–újʈ³%„¨"‘ ú7G«¸õeþºœàp©Åõ$¡ î ‡-lãÙq˜/-% ®8ãŽEIGÈÍôÆ™V9èì¾¥Uö $h©(Øí¥y=L…È"·”Ò^}ó)eo0ûz¤•õ¤t=ïâztÏmáûÓÊÂ:¶–@Æã\ò`ˆrãBž±7äâ^R¬] +Â×2P¢@ÂŒ= [K ЈäËvie  Èõ( <°ãÄR &xI+{åi©µµÊ\ózlžÎŽW“ÚÕý¥…`‘Õ9£N®®ái6Ÿ` +Фi¾Õui)ùÚB(Me3óÕô¬•¸ìKjþ¤â‹ `ÑŒ@…‹ÕBï4Æ8kIþ÷Šº#ˆ øsÁϘ…ñ£÷¾§å&¯-¦/¯s ¾ dâKr!L<ÛMÒ.T5lÔUšuçóô*%Öj‘Z@ôúJi‡q&óS.¨£²ATö#"xD’Ëq3ÅË Íæ‘Ó=1›û¤ÕžOÂ&ä×SÔ¶?¥ B ² m¡<’ì©çC´“³û÷äúM±zÎç«3$ùPtJ(€‰'HG- …CÊšPÙ ¦uæÆ®¦#i…Ñë¾”Âf'BnUûðÍ ¢|~w^WbAr†-º˜ÖAºš[b~[Êm’2BšÝÙ-L©2!ú0©ÍW®rCˆ™A\»"(½¥|qNŠ-ÎWç9ÁþÒæJœN0€$_9 àöBHXŽ)I®*z3Òè\4JÈÀÌaÊMÀù•-Ì'ź٢ ’\=F—I}€ò……µôuHi¯/%A$Ó &Õ#©r4<ÿäèÅoÕ“×—P\ò'ä…õ Øñ5ÿü6û_É6n83\m,†¹êðmöhözL^L[ +(eÔÝ}úð£ßñAª"sAÂK + ©xÑz`_¬Ö‘ѹée ©1ô‚ŸŸÓÀÕ:d|,hí•”ÎBe·èìæJR®^OiÀTÀE”9{EÄp>и·’BQ«íeLøþ¼Ɇ03IÛIÊùŠe4L­0Þ`~÷o~„SIÚA)“DðÂÔüÚ^Û— ³0nÓ +)Wq¡°ž¤VCk1Ÿikèön¥¥ÊzZY +³(›#„¼/ƮǸíRV_®qÅí,ÍYÖ€Ký˜’’ʶœC¹cJÖv­£jÆl«µ±¸“›ÐtF3Ú²›?É­D)Rïèõ³´\OEÜì^M(_º&eR©®E™¥y~”ßlÜNWcƒröøü^”)\B¤ Á­ú‰Z>ÊC?Ð^”\*_ ’ü­Æ¡Þ<³•5Ô„ˆJ€øTöÙl7ŒÉQ*Kíúæ£Þá;˜ÑZÅ´Dµ:®wWÓZ€+ ÅC©zJf7|¨yi-µ&à Df~/îBÛ¦ÕA\¨ì‘re9L'(Â~y|®·o¤ÔAˆ€í䃨ŠIå ¦úˆ3œ"£vÌÖ5̸äÏ,@0LÉATZ™ßÒÃ\ Ò¯/cÄˆÏ È\¤ô•G¤ùókˆ1e±AýÄâ.ìékxœ)h•C¥zƒÊŽˆ|{Ù^iÚóE¸×®ÅתVÞé=5[»Q,i~é ‘³Ç`ÍÀðsh¶oFO‚d>ÊTã •Li%­] 3WCtœr³µÃòäquûm£{±”P#h?b‚»)/ÂW2Öª”ËoAè$ƒöY²P–Q*O[›€j0”‹QÜçZ˜^ùW½úâYP*ÀÔ$›g­žœŸ¡ø1r·^9¢MhU'€–¸ü$®4jS(ï—w_IÕ“5Àq=€Ó¥´w+%ÖqsûƒKéÀ6àüå(U¶J*n‘V¶$Ζ£t‰2zi¡äó2‡©]»waungÌQ”)B:Nq©×¤\ßjf²}£ušŸÜÏO£ú$:E-n›Í³¸ÔQeDª§!1Ð&ÚR$ÂaôûvïnëèCÒ›ÌÝŠt1©ŽJÅ´ä Åao$…NŒ™Ÿ! âÖµ ûâKˆI:KšM6*;ÈǤ&&Ô—#Âåul-.†±ìj¼Ï€ÆËÈèTli¿¾œúÒ• ÎÏOòPÓY¸ÑÓªûvë4£6–#Š“ÜžìM¿|-âñv ‡®Æé"à"hïB “âk ÿŒ=/»¬ô˜·Ó(m){%.½¾”ò¥T( Yû[M×Ì¥åtÑ0ÞKr¹0“O›#gô¸¾óR«J Z‰2Z…ÑmðMÌë à’úˆr6À –’TBœtõÂÔªlYõC8¶‹Q¾y%¡y] +àËi)Ì–èÜŽR;¢óhâ°¿8Ø`¼‡¾0 ‚¬ö¢l þ{!Ì,ŸnÌŸ_(lq¥=¡´‡+ Èò þpœapµ“ÑûÍq¥†Ëહ5DÂô–Õ¾QÝ~Ò9û$ãL¯FxÀ‰å0II„ó|)i9Ƃװތ÷¶)cpÕ‡^÷cóÓƒ)¢"—ý¸šÆ8¤¸Þšßç@çÒJ↙"¤-Ìgô.Â!‡ËE¾05;Gƒ—̓·ÄÚQˆ.¯£Ù´œÓ3n‚¯æd¾²§;‰ð¥`›Œà:ku…ùýº“¨X IMÎÝ Í>ø)"—rƒóó$I^ËI-%¶ œ†p;ˆhW×Óa\Qòc!7¥UXO™« (N‡±`”‡ó¥pH‡sÆ„5Œ Õê,@Óh€‚¯ø’Ò|¹<+Aå×8w +Áv9ÁCÛºCFký›×Öá@1v_¨î"5Ö¡2—ÒËQ —þ´êK„>$´A’­²æüúÈÈbˆ‰àY к•„bÊ`.aÔŒ£F ²7? ¾f¶Ïy{˜æ+Iº%³rnXœ’(ê*¢H/!7WPëZT\C-j!| 'B¨ñ9õR|7œ)®¥-ø‰ËA"Âæc|%D{ë„“jrí´°ý¾Ò¹“ªÐP é~<+¸›vû‚pvb|c ³Wæ÷®8aÂœ/1/7ÁOá0Ë¡r ú²ÒR\€˜ŸäËryþð5àjJÉõNíÎn”£|>Ê‚ŽíÙi”4Óœ·œà–b41¿=@¸æ–"ÂjL^‰ +ÑÌüòAŒsp9WÞ¸K¹CÂ[%]]M*¤ÕE”æ:j€ƒÏoýÊÙü6¦¶}¨ +±1œÑ­J›e\)ì¥ÕNaòœ/ÂÖƨÜZœÃõVŒË­cº?c²ùIsçieã¡èö1ÎMà}¶{ëÐPëHb®÷îM±0Y‰qkQ!„ÚPœÐ­ ¹µŠf¡Y®‰—ãí®×¥“”ºc\?1I°Åé-†Ø4`0P•Ÿ?>–qSB'%õcEòåëñÅ0ˆU ÷k1–µÖ ±ySoÞ,y}}}ûøÿÙ{IÒ,OìN°ĈêJÒµ437­µ™››k­µ{¸‡–"E¤•U•U]U]ÝÕr{¦w°;ƒÙÙ]4ÉHbI€Àà…þ1ä³ìݽÌeÄ×^Ñ™žîæßûÞ{?afá˜/ÚîÆ¥*z83Ñ+‡PöŒÑð\ò@4Àª`ýbPcå<Xq”ÍGIïK0©BÝjùz”† +©ñvÏûÝ ½g³+`ðVÎNX»›’Ë1*“kbZcƒ²Âb!èýZw°ÇŒ=µëÇ ö³ÉC%ØÝ™[OZÆ»ñÆj§Ì(O&3àÜiyâ}¹Õ¿Œ‹5?áÄ…bÂûBƒ®Õa†Bá@*Ÿ +ùm8*æ°´m7N'ßf—|açò¾”‰@U°ävˆr@®8ÝÇ¥ék­¾“ô¾B}‚ƒ¾Jwcba“£\^©x¿TEµ»Az5*@¦„Šw©Q®DÄ2i÷´úé V&Õ"T6BæpµŽk5x;eÒË”Ù_Cõ4uu×lžÔËs%x’UÝ®SÛæÜ!Ÿ›¨ìÎ+P²<ÂØ~B·êÛÝý糋éÎQ€4AEÄhWËMscï:¦\3«ûNëÔ»G½¼³ÈŸ®Å"„ŦûÐ51©"”ÓýgÅ-o.ÆêÄ÷~˜Ñi(uQ:³“VÂ<8n0,RnâÝ.ÈòÝÓ·4(~" 4±‚(!½¼­fðѬӉŘÔܤŠ›xŽ4'…ñS·}˜ä²IÖµšûƒ£/¶ÿ:Û»€4­Å•5@?±¢¾”ÍeƯÿ®~ð íNA«oÆU>=*ô® gÿ«Ÿ„?Û SJ«}þ«Þé7ZióÆu`Ó‘‘Y +¤ Pöòøíß[Ís>? ƒ®–*Q¡œë˜1 ­ ô*ÕÍÊ‚sšR BƉ,.¶”ÜN}ëW˜úiÛ®-Ç'P­AX”Õ +‹e embaAÛoWh±H¥G¥é»êâ+«õ“« RBAqvu©”¦A¶¸Žç¹­•N²½çJé`ÑC¤ãýRäðÚî\¡fÛO;~Üd´F”ɬÆøµ +E«q–é?æsó e¯ÄÄ—‡ÇjLŠ²67Ë  NQßÿJª„XX@•ÖÛÌ^‹*~ïfõ"×õÚñòú÷ÕÅ»ˆP¢3½lïÜé^ÉÅmD.oÖ&aóéYÞNòx¢r2»™ö™Ó¹H*ï<Œô¬¦V“r9!UÁ2z‹Ö›žÂŒ¤P’½/¾ð¾üþÀ pñ<ˆ¥ì0¥Ui§¯V³£ëtçA”ɯǔ̈́!ÒATQf‚²6"ÜzTÄIJVX(Å%¸¿Í¤µ‘®nƒ˜ßˆÉ|fÞ>þFožˆÙ%c ½5É*½Ý$k|æOáÖˆ/ÚÝëêÖba Qß ÒéÊb~þ±›>Ìø4Àl¦l©¸ jÙðöõö®›•%p:h0+ Q»Œ=©Î^õ¿EÄêu"„YPN”3€²¿à¤Ê´«wB­å憖O~_ß~¯7N}xþÿù'2ÿOàÿ«qÈM·Ü´qÈM·Ü´qÈM·Ü´qÈM·Ü´qÈM·Ü´qÈM·Ü´qÈM·Ü´qÈM·Ü´qÈM·Ü´qÈM·Ü´qÈM·Ü´qÈM·Ü´qÈM·Ü´qÈM·Ü´qÈM·Ü´qÈM·Ü´qÈM·Ü´qÈM·Ü´qÈM·Ü´qÈM·Ü´qÈM·Ü´qÈM·Ü´qÈM·Ü´qÈM·Ü´qÈM·Ü´qÈMƒÿãŸÈÈ­ýÿ¥ßoìMôáÉð“†ý‰_I}ø{ix|2=ú$÷‰¿ŸTŽNôÅød±¿7<ºXcá)$ákÉÒt¸³úÓ+×àEkù£Å|±O–ÇÃix-/åà¿O5t ùø¿ÆÅ'qŠÂ©J 8‰"ÍàkÓ a(Š¤):E®íz/¢S‰Ea$†¢Nÿãíx/">>ÇÀqÁàEÔÇw¡) +¥qÿÓ‹þÑÇýg½è}ÜÖ'õOö>)~¬…Âkú'‡ÿ).Ô‹ëãÒ™‹)û§?ªSXŒÿ°ŽðˆÝú$©OÏãiÉR×¹OÚÞ›½¥üÓµÿmŠLPäZw CÐ5!½÷oýç½Ã›Ñ|×üz-³D©Æ×¼÷"Ÿøýçèåž]#S †~ÒHÿ)¥e2q>KêuÊlRZ=%×’b0´3"ô&–ÿ´å·’›r΀¶; ±aÝãm¢)\©&Y—5jB¦«•fbaÊdÆ”3­—]DÙ¬/ÉûâLŒÐ µÌ¹}©¸à‹K&;ÃÓ½¸R +Riµ0äÝ.*•w¦•vÌú1iP©Ae&˜ÖŠs…0™ÆÔiõh»§Uw’Zc Q½ýšå +L>„[_Ý‘U? +32Ý ³D.%¤aV´‰5½¾o¶ÏÓçBeÑ!f]£º—\í½~˜î\ò…%fuB¬Kší”Vóy?aú%ÁبàRz…Ðk>TÏÂäzˆÉ†™\”/¤ô&L ×êŒÕÁµª43ÍFõQÛA&»™2ý¸e]%?2ƒ0e… “³zBfDj­é13Áfy«“mÃ'"|ž1:ÔXð2crq&c ”5 í!nôRzg3ƒ„g ˆPF…2mtáPq&Ÿ’:ëÃ4X±8WŒ2Ù(íòv_tFˆXŽ³Ež¾áïGø¤âO*(—%¤ kH¦ƒ˜–dœw7Hl rˆ´áí:—’›˜XMpÅ`Ê‚,F+Æf7b|„°ý¨±—lÐ:)¹Eé=6=âÒýÚðÜ®/„–Êœ3VÊ >; VÈc2,×Q›A*ÄmJoiÅ-%;FÙœ^œSV+Êç‚”ŠLzÄ:cR©¨ÙNSü¨By›¢î°î·F„=Æõ¶·‘·\ã:*¥â–Ý=Sª{|òØ‹ˆ¦êöƒo‹£I± +b~™é?3„Ù ¥ˆ·]f&J;aÊSÞ†¨RƒŠ •8_ö§Ì¤·_„N{{™„XBÕ†˜ßJw®Êã'ý÷I5áÒ|v¬7O²“çFûœÈL{@›ÝùÑ×Í×1¹$m©°]œ¾vzOˆô„HbV2Îæ¾ÿ¸MŒèmɸˆµ˜°—¼=aÕ¬6j´âR5!Õb-©4Q­ü¸sa ÂŒ·ul˜Ê„)'ˆ{;‘E©tJ*&87LZÄâéüfB!¡\9ß8rjûPWëaΟT×£Âý ÀŒ8T‘Ú¦í1ªt±ÉÛsÑ™RfœÉ&ù"®6¡’"„_§­¡RØN…;aê'*“5»²;•œI‚ËÝ 1Ĉ’Þ7™ßñ§Ö# ʸ¤Ö¡ŒnJ(­F8˜F’Ëo&e?¦Á +Dèüb‡Iª4!×ÈÌ4)C˜%íuX„˜ˆpE£rhÕNŒÊ1cõêþãw3 {[êäÂ`—›£z·§˜wØ +kõ¼½S“ê&ªo¢*{›ƒ„RæýãKª¨X†Jö‘éMÚJ¤ÒôT9BôúªÀ: ùet6ý^T +iÈl„´bL` +~ÆÙl”HûãòFTÜŒË 12#;CÖì¬E(*x~%Ä®EøÕ("TiðÙž3öTÉïÉÙe„Ê’jÃÛKN('ø +®õ0µ+ä–vó¦Öï„ȵ¸0Œƒ« €8€˜vÊÞñw|x‘“´‹+DnEéBŒÊ‘Z_°ú€œQßHJkI×Ê`R ‚…„&µ~B(bB>ÉåVÂLÓQ¾Ä]x.7½£B‡Ž¯!žc3=³¶ Ü“›1¡’”ªþ”Èþ¸˜ZKÈ娷§XIq½ås`CT®ÐÞ~%“¤\ßH~Üô6rGjqæíš‘î`z#ev¥Aè_ ã¦UY'•òܨÎÅÒ”°Ú\v,·à'Ì+×ÜÆ¡X¯£²Ó¾Âg¦)µ…ÊÐŒ•a‡h'ôGš~"¦‹´9² ÆÖî$Å2®Ö°»ÍftÚûò|­q"Vv½¡×öõê6ëä˜t»éîEwïCqúÓê* Lˆ õ~LØÄt)»åöžØÍso_°tßÛ •¯Âü˜±Ž¨o“ëbŒÍcR"Ö‹Š•„ÒFÕ>mÏõú(˜· ö>Ÿ_øRÖzRÛH¸ÒÀ¼/‡ÏĹ,"Tg’ÞšØka.Œ™!Ìð'äõ˜˜`ó)©LÙëqe3©1 `䎟ÜL*Q6O[ý•ñ§ÒT:ˆ'Â"ôÞÞR-(› Eä:iÕҽ£ͦBaGhê!FeRBá +÷C¬Ñ}qùžŸŠÓ%XFhX€¾•§Õ¦Uœk¹”ôFRóc¶ÕCDUZ¬»«WåÂSFˆ’Ï-áù„TG¤ªàŒX£%ˆ2˜Ö¤‰\ÜÕK Æj!|NuÇåɳ„PŽÐNŒÍFÓÛ ô€Ñæ’rs»yK+I9ÆÁÁ»œ=d– 3@·¶÷ù‡¿^~ˆ)…˜˜#Ò}6 Åvà´Î²½sÊö6n€‹Ò9Tn1ΖX<‹‡Bn‡ËΡwX­&ågA&d¢óˆT³s>·Å¸3»v0;þª<º Ð6®5µÊ¾Û蟸ÃÇRiË*-Ÿ¿ù+¥1_Ç  ZDi#j‹K¤Ì˜1;žJI™nÂÛ”³¦óðˆ/yºUªÞ á>ÂÉÅAkI jE¨B‚«Ê™ çtïÆø„e*¬»#÷Åü“'¥T ( +H/!®GÙÍ„À \®ÓZ›ÖZ[ N·YüI9B¦û6êjLö£–1¡´bt6D¦×“âJ”ýt=¹å}I=”ÍB¹z»TÓ É²÷ãü¦Âô hw‹2û)¥ Ë j?L:€3+av#&oÄ”j d$™Üj„ùÌŸºd¶¼à€o{Lj=:D `¢¤Xºeœ ” +©wS"hìüj˜¾ëKE xWq‘ï'äûq•ÒÚµÑõf0³š°"7yw™n]8Í W¬ZÈÖvcŸ0;)­‰€h׫JvšÓí_†HWHwX«a\8 ÊßC ¸ýÇVó„t”t”â²L() ¥´n×fO¾ýëæÞ³„R¢>—ßâò ° Lf$—¸Ùý@¡\๰õE[#¾·×¢·õ²±|d +a¦˜ÒÚ\z ¸cÁXÕ½Rï\ÊMbBЛ„îíàiÔO2ý‡BanÔvÏ_þÖî®$@8t“;ň +• e ðçMDM2®·c‹3œ'mä&®u@Ø@"6“ÒfB  ÈRïlfZ¼ÝãÔI‚x¯z»–Së= +T·ÔŒ1ÅÕ¸x?LmÄùªDH—«ˆ½ÃhmÞƘ`TŒU™]«~Ì‚HÙL¨›I5F»ÜZ +2Jf| }%,®ÅdÐ Œ5„”/ex›óJÕ0íÂœ ­ ^Ê Þž’«”Ö +¤l?¢ù…*Ì?B¸ º6J ©KÞ ’A«@!8²÷æ&ÒP?„Üz’ó[ >òSê)Ö¶‰Å¢A­Â$1¡”`Ý•¸=˜Rë71Ïn$DÏHJUÈçÌ3Ý'…ÉK¹°¤µº`Ô:³GåÉÐiv(«#d‡fe‘ëž +ù È)3o8«z WŽõʉTؗˇ)£ €Æª\ï$@Y Æqµ•ë?®Î_4vßÔv_1…˜ÍÊô©V? ñ¥¸\Kém½vh·/À¬qî ¦ÄgÞ&t ÐšzußnžÕf/A‡ù:ªõPZ6;´ E¸%æf)­ì ËØ=ý¨\5jµÅkµ~ W¶3½Ó„TÞÄLT®AùÙHi!ʵÀ˜}oçhÂY‹IŒR³*Û¨R]KˆÀM˜Ò¢ÍAœÈ­ÃJ“˜• A?—Rj‡ú(Ì@áãPTTæ~T\K¨A*R…]ÙD¼-„@ù¬Eyñ ~ ׄ\—Ò£Lý˜µF!2î£r˜TÇĨ֕‘¿4cÕ/|D3¾@íf‡q E°Æ`+Aï5yÊìAƒÃ²C%ˆî„6<¡\©‹îÖ0Îæ7rÕãt~3®BUŽ¨ÛŒ+€„q:h¶á|˜8`© +”äJ ê0®È›½ T»fîÈÕ ! cÎîí®%eX´8묄ð¿X‰"ÞFQ5èJüìsΈˌPµ +ñÉê–Jy XOÌÍq«‹¨%PËR¶M0íŒw® vˆŠh^¤‘ÖŽ U?™KˆUÚê¥m>7ò:¦”I³%æ&ŒÓgÜb´j]¯4¶ßjõ}°W¸=H!?W*Bq—Ë.ÀnÃëÃŒ Õe³œ;•ËGlnµF)k zÛ‡[ ¡Ä¤û˜Z³ Z€4)Ô”Â>&WB¸Á9}œ\A´¢·Àó‚cUòËtã$̺!:ãs1>ÈÝjB[Gt`(Î蘥E Ú0DC:àø¸ÞÓLÁ¤– ]ÈÎJ b:®´…Üç,h½‡‹ Ÿ×ÅȘûönˆú,€‡pð( ¼ÐlÙHÊàg¡À@aÀš³VÌHpC„Æ6z$L’Ï z3N;Ÿùе(%—äWK tB>É«ÁO×ëQΗÄ¥]%´ \13ÝY„·‚fö¶‹«ÀøPêàƒ¨ ´¢îrë19FåLÑÃÉàdÆÈ+n,F\p €¥pÇ…Æþâàýý(÷ëÈZ„Kñ Ô²ÓJ°–7™„&¬û¡Ô_¬Öƒ8À˜ß€·ï¤ç³`µü˜NàTtFŠ;Üa„õZ†w'¨ÑØĤ„!Ôo·øLߨ +ùl —¨X=K¸Þ1ª‡fmŸM÷ý„¹ÊI©˜”K!6Žè¢LÿZ…†ÍNãRQ¯ìjµ¯Š¸Yí‡ÙÉK®¼ëmËxgJÙOÛ¬;“«§B唃ÊÑÛ6¢2€–JyÛ‡ƒÀV¼ýÑÀ.ñ`!kIÊbvè´ç™ôÀhIå=èì’ õo îˆÏÏŒ{?.BõúA÷ÁZj•* P¼ø=pñ>’v;F¦S|ú2Ùˆ&,i0­]F„jŒ.¦Ýsžð¦3*ý§méÖ +Øy(ª•ˆ°‘>2]9^‰ÍyKkP ŒÿfBŠÓé0alÄXN+K™>0µ57c%"ÐÂørÓ~²E—”«a°öxz +QC)ƒÖ»R~'H¦ï†©MT%Õ:`Ž·(›»dÖ¢¢5þr#v?Lø /ÊIe5HCSø’ê¡MYÍÍxWcÂÝ õ©óð“Jcr™µ;ëÞ¹¾ ­W¨x?ˆÜ÷ÇWh8e0z[Ü`ì(*j™Ž·Á+èjyzO.ûP9ÁÚ¤Zñãð;„&m˜üw†„Ù +ãší¡j%ʸР˜æ',iNçŠÏÎ0±ª×¶ƒŒ ĽÝcƒ¸IE£º£––¢;0Û¸ÝIé-*=Œ‹µ›âu¯2ƒ‡I¹JêMÐEàì(wËA6,iÔŽÍö i´@§1ùi€v61k ÑÀ²A:ÌÊ-PþB¦¤œµ¸êC 0D1:g”v3­SÒjÇ+×=æ +ƒuL\Ç´M„ƒ r¥2~ZÚz‹;#°WFu;)B +R™’ëpyv5ÆQ ås÷Ãä +$+ÆCãÊ%xÐ{öÂÆ8ø(J¹Q +xÍÞL›Iµ JmS<`¡V£”b7 +îyî)˘# AÚŽr.Ø^H¢çAÈ! +üL ùpJ'„œSž„i r!]Ö€˜ÈlÏ@®…iR*'¨´?© l)JåâLŠà  /“SJ1ÒAól"2Ô¿/i@EIôÿzœó#bœ‚ÙJàïîl¦îÜï˜0Jv'ç¯þòw6ÑÍ*äý&¢úpìa¡}TèìÞ÷'‚1‚ämRÈ%Y@E¨–t”rRB±48C„&”1¾%!™¤dm9Ó幕ˆ§þÆæ´Ñô~Ê•Úìi6î†Øõ„´‰ 3¤ÙÍö®@ð(™ñü⧫¨àG„˧Á¼ÖÛPZbvTlîOÏ¿f²ÃuD 6x@\ijåýæ·ìèQ=Õ¤Öö"B>ĸ üH»/·3ýGzë$]Ú:zü3­¾ü4@P/ø8(E1» ŒTqx* ƒAH€»e.ggr~LJŠ•ÖòYÊ,}ºƒ“r ”²ZÍÅ‹úÎ[&;V3ƒîÎ3?©ýd3qןJIe˜ ð‹ž_aA›¤$0ƒî?º¡ýI1þÑCµ#T”®–f!T•(±4&Öy{ +V Ü4ÈQ„?ñ¥>ÛDïà.`7µ„Ö‰qÙç¬%€F=< a:¸-@Hèô8å(™ž]­¡òÇÓbE°Kàݼ ¯Z3 ±¬º#(\(° 18Ú¨‘y‘-öNSJù^ˆîƒv@¸ +'ÐT ç@GbN°ð‡HeLØŒ‰päÍ„¶‰è Óûo7SÊf„NÑ™Í8?H{z|ŠgBÅ®ï+ùÑZÇ(Ò1\»¤6“®TÅÌl;ýq“\Ï#*(LРó ¥ÂÙm€ôõ˜r*ó°°À›¸X¶JÛ ê ½ Ÿd3q& =¢ÒˆTç+»C(?eà0f—PëàYRJ‹I!Wjíí]ÿB*Îý`ó¹1=RŠK§{F¦=Ìñ¶Ît×ЪNcŠ-.z{’v¯5¹zýý¿ªN/W¢<¥€ÖõN¡¯#*iLäâ˜[íó¸ÞÓEÉ™¡o€Æàóð¹ ÒA;©¥E2ï‡i@!B­kEYUˈZAåR½sô‹°ö?†p³2Ǥ’ÕV¢âÇ3¥­äm2 \“`31ÊJr¹uзß9Fnzüê·>{/Â%¸<Û$[HrE ¿`Ê2K³‹ok³ÇŸnbw}èF”NÀÜ mc8Á„P†úLˆEÐu £xœÚz„†JHP®”êù)eÖ×pònˆñƒ§ÖYµ.Ù=x&äíâØó>§ 4ˆÈ ÒËƘ»¶ÂÚ|3.øB ©úâøDp‹Àh’Ý`‘iˆºï'6b"D¦?fl"Ê`Š’kŠ3¬þó{¡û~4„ª)©M¨-@ixÍZ˜ #ª¬k\ 3 t`îcm« ¡ +-y'H­Æ€5L("àĹ@âZÔ+à àå„„ð.Âey³©rùªÖâ\.î-¬ %…<m¶…숶À¬u“bI+-ŒÆ®ÕpµF™-L²«ëÅ)aµÖSÖbr‚¶ÒÜÄ””R¡Í&¬3ŸŸÅåJJo„gJ]ª(ÅEvpiŠÕ½¤Þ w®gÆ1uSSz„zJëÐÎ<¡´µò¡VÚqÀµ›‰&¬[0å„p.@_J½&> +a…yVöíÖ‘]›»Í¥/)¹p&t ~/„m&%X +É3öˆ4{& Nì•â/¡Ò&8£¾U=rWë)OqACÁa×BÄZ˜Z‹Ð6Ý•Š3Êê…i7Jš$€» ÁB" ¯CT.ÂH} •O¥¾‚˜ +T +â0m7Lç sd¶Î éq6çOÊ+àe!‘2¨ŽqùtyáG!NK§ž­³¡¶Ct“«ÐM(—¿³‘ØsI2%¼=‘WA:F¹Í8`¬&2q\L€°ìj˜ƒÈ$Ü»ðQÜKBCÑæ]?¶aW#žq29ÞPÃWd£w/_ ±Þ§“0Œ÷‚ôF\Z÷άBDFœ.J´JwüI̓ÒʇIàzpÇŽ¬(p;a­N”íšå@Àê˜PŽ uDíšµóìð™Ñ~€HeB­t„ijdäG r%`í„Œq`v:!Ê¢:g¶ØôÈlœª•ƒ¤R[M*)£mÀCèk¡r¦u®ÅêüS”- rÕ ê¢P@ðPZ;×{"¶©ª°¤0·• åâæ*”9¢Ìaòã "ö£×©HXÍ„\D=ìj&ù*€a„ðš1ɰɇHŒ!m¸ÜUš1ïJ1¸­¡{§ò "pa„Ò¦ÓLj¤¤ô ði7c ^â+P]´ zV&¥6¼K´¨²æå”_p«!v#¡†è\RjâZ/ÊV"l%¥v£¥,àX+â`{ÁncJ3Êäp¹.g·B õ#,Be’¸ŒKaÔ µ^‚¯$Øb„tÖ¼‚‘"P±ŠJ Ò誹-Ùùâb³ýqù/Wãv7xLJF;ßñá@v+/Y&™üz\‰€×HÈwüxbø+”²­Òœ³ÚÐ}í¢‚|M¸á¡B‚0r3S:ºë'ïùɨð™ŸøË $Y(eùQR°—¼sB¢ ¦¸$_B ¡6.ט< DF˜Öºj~‘äK0Fk8Ý„˜óN¼ÃÓs¥|â´óù%¤:ݪîÒ0% PW¥´†à wNéù …Qš™úÄ3P•‹;ÅéK£qškàþ¤Ò.íLü˜"l­y)VŽ“Z ’…) ©´§ÖO SR‘sû˜w}måò>̈spˆŒÕz‚$‚Þ‹s¥0]w#îœ$@êm[LÇZôNÙã8_‹P™Jj¼ÑÆä<à[7ØÌv<¾öãsn©”öÂrqïj`•êà Á™úPk-.£`]SÚ½Æ߇[›°I–.êÏ)WúQëÎ& ®„ `x • à.›žé奰Kj-Öè"Bþ~„º‰é)¹‚‰Z¯Kî,’2’¤É(ÕÕ qoÛŒˆŒæÝGV J À6B8 6âLnÃ;{šÇÅzœÊclÊéÏî‡ÿünh#ÂBaüdû‹•äZ4žw”ØfR³ÅÕ˜ô™ŸFÅz‚+%¸*4Œ"ÈÎT¬–%èìg>üÎ&…NÙSH Œ-F¡f¼ ‚ˆ®ö¡õÖãØOhd(³Í$˜eàGÈÔÛTšÉÇÙÂj\y²ì‡ÕH¨÷C\ÑYs(—~^¬ãZ‹ÐÚ10æIñ^”]MÊ`xê.i4C¸çòQÆc¨0˜GD‰Ëè)=æ3õ8c,T)‘v;ey—ŠX»/çf\n+.z—T’|>HëIq#)ÄŠsEÒ¡Ö ÎV†£'½Ý—>ÒÚDuïÌWQJ5ån|Œ([ßó£iTéréqJ®¥˜g/Ê ¹¾}úõÖƒ&³•VãàÄ-\ȃÀpŠ[)>Ké”ZÝÀ”ÏÂÄJB$õŽZÜ\󋕦5(ÑúJ” ¦òI¡J*'îR’´ ®%*ø0æ ŸÓ;¬ÞÄFwA‡'é¬/΃5ó%t\ª'<™ â™5Ä +“…\ý„ÓË›q:É:›¨À¤õ(¹JA;#´ÃË%ÙjÝõa>üˆH€šâAP­†èÕ%ÁÙƒ¨¡dœp)±I)m€ Rl†Pã/WwÖSkO™¶žòÇT¿ jƒŸzŒÎ3ö$Æ–ü©t”-‰Ìb­&ôMÔ„2vÐr  -p +éF”kyχû£\·½“~I5ˆˆw‡ƒ¾~7R\år •˜þ–e5«-®Ã´QÀ-ÁjÖbÌJÙˆÉGƒ#&@òY`mà=Ùܽ0s|D\\MJÂTàã@¬¢rƒ´ú›)0 T‚±ã”…Iyl›/ă?µ*»”Ýþ,Ê„é,ç 8» ø$3ÐÎa2¥ó€óm’R°"@8 ¡Æ¤'bn+?x¬4×0¦çŒÀkß ÑŸ‰»&€›àÁhRiD¨A®‚Nᾤj ËFm¯±u]_€]E $˜èŒc”%±,XíXD‹0¹á]†úœ˜î1VûO'£6“ +&×|*lÔý Ô v +VLq‡vc-.Å3Ñ4¿µ˜âµ‰§!ž5K(ëQñÓ5ä'kÉ%] ¢Óšq*Æ4Vo¬Æ˜»~d#ÊxÒƒˆB´(í„DAw៮D}qžŒ`Á•X³F2ԵΛõìì™SøˆûA/}ÞM5ØøOVÂw×(Q’Ïâ|.΢`$™’˜ß w#üfÊŽ°Þ©˜—{fBvãJ@%`+@üÿùýPmœ°¢)c5ÌûŠ/Y–îøSÐ}¨TBøX]èq¾¢LVˆI{jD­G™¬·ì´Y ¼AÝyàWHÏN'áíbq7Wúq‹]AÚkqœ¾^œÛÍC­²*!Ò3ö3„+QÆ‚\« +æÐ*ïãzs“0׊S?¢ èkšÝçf$ƒ‰°3„R3.ØÆ”ÞéŽHU ]0’>Ü à6üu@Bx\M«Qv Ô#f3fŸ±`j AT‘ ‹¸ÞRÊ )?äeå>óa«:ɸ4˜S@(B*z8–@)ÁÀ¼3F –ë´Ñ§ä†gIp:DÄfLÚˆI¾¤­f<×Ø­Í}âÖ+Ër„tW#ðÔFÂÜD¬ 묄ùµ·d×Âœ/.ß²÷Œ/®Fq›3ÛP± ¹ãÞÝtò]_j= „1,µ¹™”ï‡RòEYêw7’ âÀËC­FRi·v„˹¿Xñz?„éE¸¨´•¡}ˆâ +<Ú§+¡µÖÀï]@çP¾ ØÎØCjãwBüêÝ[åûèVBlÐ[ °{ï‚júá\1–2îl`Þ­ePÞ¤ãÝßEå(«Ãf„ÞÙŠ˜Úb2#ÆÄ¡gžXœñù‰3áH’Rêðð®F¡¦×ûð.Žãét”<_€å3#Bó._²FSv{Rn dF˜Ö¦Œ6¥7§ˆYp‹Fe‡6;`Óµ±‘2ÖS ™ í­—ŒÕŠÓv’Ï®¡ª×5¨¢ò¸ÚäœÈ6ö˜Öò™{1e%¦€’§à JÐm¯6gˆ¨ÕŸð»xD(Z ×ÚˆÜÄ”6àXÓàyÀ¼•ÉóÊäŸYzp ' Ub´gÀWcrŒÉ"ÞÖhéMT‚¶ +¢*ر[æì‰äLÀVª{ÀîA,˜q( VƒÐêRv$ä&Ÿ…°üP„€iëa.Jfct9B—“²w«Iˆt# ˜r×OÝõ‘@d ÀU1Eè”+pz3‚É›Q>FfA¯„8ÐÃ1ºhŒË_„ý³;þOW"k!õH9Ù'Ôn˜]_ `$Ÿu2D>¸OY¥,ïŠU˜†½ë‰ü~”ôî_\úT~”ø³û±Ÿ¬ ÷¡àQ3Â?Þ Õ… H)5ŒÏBYBá…<¿™pâ|…ÏNÍÆZ^*Å™^ß!¬•°Ù)<¯×ö¬öiºw‰h¸TMJ•(›Çá¹6¹å‹):7|#,ðq¸ÑäÝav@C~ù,ÖŒ1›rv¨–´=dì¡6A?‹ÙIsú¬4¸’rã„çÅŠ€Eˆ\e1ˆdðb´ÑfœnÀ'©@ïƒ+OÊ5¨y>?çr38ŽÈl" ðLÚîbZ# :G®+å=§ó eµ Ô/(î­&5¨Fpy\n×;÷¡ybŒuùÜ„ƒ M, ؘ«¤Ùã2Ãèd.P î&LØq&‹IuÑÝ‚ØãÞ=WY„Éð6tJ“[)µ¤³Q¡ö3Â@êCýøPïhv¨.\k&aÂT>ÂÅ[ë1>%•^gc|%@æ é„ì TkˆpAøSÆZÌ»½å¡´p±†òZF©ûb¼G„EÕå]Z’ˆX'Xç‹KQÜ\ ÓÀª {Kdáì\Ž–ËŠÛ÷%…"3FènC|¼¡³ !G½«N%!ÝoåÃæ!ƒ„ÑÇ´NB +«av#¸JÝñ“>TxwÏæH½¥•wÍÚ®UÛåÜI Ö°‚˜¾‘ÔBL\-ó¹iaü@oî‚Òk;lvB@IgN÷¬¸õÜìž³îS*(—±ê;´3” tPu!7–òÚéªXù†˜›­}.;‚òƒÏÒ¹ÁE¦{¬Vw»ë½´ ž±úSΊ¹QqxÁºƒ˜XÂô•™q…±´Ëæ¶0£ |'dÇvëU+FóØê\P™qR©ÓΈ„BµºB~’PJI¹ò˜V…— ¹¡R^#µºµJ=ƒ¬Z„- JʇÛÒ{ˆÈ?aÅùb„Ɇ{A™]8&mõpµ|aÒaÒ &àyR*CàP´Ù5*Ûji 4!i´ét/.WýL.ÀÀkJeG*m¡Z}LP ‡­âV·ûbq®V·Ét0x+)­œ ˆ\†5”‹ :=„i¤ôfR,JŒ1n”°“´ƒ‚› zÍì‚í%ô¬|Jm"R-HåCð¹TÞ‚J5Öêëù-0q÷Â<´”\”ʃƒ[䬮^ž§;{F}¡VÆõíÇzm}”+Qö€NOp7†úYKꄤ–`Áe¨QÊT‹ƒ¤¥ô +çt)«>:Z*ì Ù9H·w¡ºØt#?8(Ï.Óíý„PJ© ï °Ñ³¬äǘ\ô§À¡˜` ì¥Â‚ÏÍàÓÕÊÒûõ£õ™J)­©––µùue~MeF5ÐÝàÐ}˜eÒ¸RVKóüì²´õ¤søzqý³‚÷²!㳤€€zlfdV¶³ëÜèJ*oé„iЙ6Æç)«ꈲ +úéÖAnx–n¨ÅQ”q€)¸ì Ð>éÝ¢ÖѪ;fk‡zð®nWpì[K*Í„òR®îš­Ãâôª²x¬”GJiîìší=£½—îC·œÞ‘ µ”³ÙªV©t›u=Ifµ ƒËñÉWã³/sƒS­¶Ãå§\v¨W·ŒÚ2;º²úË‹wjmSŠ|š®(§–ç@ÇFû„ÎÏ©ì,;¹Ö«ÛJxjQë­¹¶/–wÅâv¶}0>ýR©íøh¬g6ÅâÂ[mwäÝ´ìôãb.L„ZÐʳ>Ïô Ó‡éî ›Ÿà&tÊVŒ°R' ²q½ ú*Θtß“‘rí#XyPŸé>(NŸä&W¹Ñ9<`¹8·”2[´ÛWªK¥º]˜<‚ê‚dÉåyˆ²1 ´¥wËÀµQ;¤ÍÔ–/¶ý¢´u-–gNæ|XÙºnÞ>ù¼0»ÒkK§¹¿¾mì<%Ì—ˆ¥-¾0‘«K½ºÜ~©{œRj¨\•‹s©¸%äçN÷´¹û²¼¸¶›ûÅÃtk'Ê«Ö´Ê27xP˜?)l]Óî(!UÌê6ªW|¨H軹§×—…ÉÃþÙ‡ùÓ³ÓÇÅуÝÇßêÕ_WªGZýH¯A¯Ú‡_ØÝsLkn$eÀLR.)…‘;85ÛGNÿ¼}øyyùj;ÓÞël]Ñ Ë½_ *¤Ô–Ý2Û²Ógµwju·4<'­6:ÊjDäF×ÍÝÏwŸýîüݿܹúz÷É7vg_­ngW…ùC«{Xš]ŽÎ¾ÞºþÑî“é6aÖY§e6vŒÆ~}ûEkï•Ó;Þ{öËÖÁ+2×'ŽQ[8í½âü±9¸r&OäÖ¡Rß…¿¢J>Hjð^–êÞ»ÊÁûòþ»ÊÞ;wp1:~çööµê$;¾0Û'FëØ§W£·ÅÙ#(ºŸÁô*4‘RšƒÛãòÖóúÎ&Ê¡­W&¨’eÒ5¥4É.ªÛÏ*ËgíÝ×ã“/™tŽ •€·Zu7Ý>…ò¨-^6vßåFOØÌ8@Ø€ÀòNç87¹4Û‡Z}ÇhîÎH•-"Ó+ЀÛzë0?y+Ü>|'¦¸œ§õJŠË‚€¡­AºyÜ?yßþ²<tøô{p*íµauçùìñ÷»Ï±ÿâ‹‹/ÒÍ¥Vž•¦WLfòÏ»\^ßœ~ÓÞSY\Ï>@ãÇù,—íŠÚí£ÚâÙàì‹þÙûÎþ˳w¿é½‚EN5>;€ªËŒ®‡o‹[O³ýíƒwˆQC…´Û\ˆn7’ùL»¶¸ž?ýåüú‡Ó7¿ùþïþQÛÓjÇËŸgÏhgºsµ~Àææ˜ÞsYÊnÇØ P¡ÓÞvûÇùÉãòÖ3^ÜêÔ&ø¯ÿ½Ý=õáº][ŽŽ¿L·/ÌÎeqùÆ<f?ºþ9‘î…)Gñ”Õi}ûuc÷óÞɇæ׿yøÍßJå­ÂðbrñÓ­ÇßÍý´ònöø‡å‹_ÏN¿úù¿ø_*‹'tº•ëî×—Ï!îÑç£ó¯g×?ï¾Ú¹úâðío¤ü`türùô‡ÊÎ+½sÒ;û°|ùûÚÑ×Vc+?ÜS«ã”^‘Ê £sâNeÆ—õÝý×éî^÷ðIqñP¯o[­}67°Û{™î¡Ó9?øYyöŒ´ëbqKÅ¡RšlBȹñ£Öî»öþ[§½pÛK­2OwJóG…ÉUcù´ðüìͯvh)ƸÞáÛéù‡îÁ›ÆΫÒü™ZÙ1¿ü)ª¡D¡$ZûoëÛÏ‹ó'ÓËï¶þðúgß?|ƒèM³}l÷Ρ졌Υ3|Ù>üFÍO 0#é@\i|fuöó“ót÷078im?ÓªÛÀbÐPðÅÑisçiyziT¦\ºlØ;þÒlìÇçåÙC©8+Ž/jÛ×™á›[ÃöþÓÊìB. *ËÇvk»4>œ¼…Åwû‡­Ýë¯~ûÇÒäÖaúà«ÅÓ»gªÛÏó£s¹´ ú¶½õììõ_U§ç˜ìXÕIgbùjþèëÁÅ7óë_‚YK·ÎÀÃn¢˜ˆtÿáèòƒ³ï +ó…åËÂô‘Û;jÌ.;ÛW Á y0S +MDÎinx!W–ZaÐÛ¹.Ï0µ©5Üéµ=ð˜7;8 qN„¶(³F›rÑ>Ø„¹5Þ×÷?ºÌ´Ž`q2½íÚâ¼<¿Êª,ŸýÍÿwXT-f;{³Ë_Âczùž;¹èo?þõïÿí/þõÿÆ8íÉù—‹§¿^~ý;}øÃìÑÏíöñË/ÿ³ñߧ{»>ÒTJÐçµíç“˯^ÿ¶{ôvyöö¯ÿÕÿXß} +5ÐÝ{Q[^×w_<ÿÕÓoÿþôË>Ù:x±sñ•T«‹íöIiv=8ûfïõ_Ÿ~õ÷Ýã/Ož|½sþVÌ k™þ‰ÑØ5›{¥ñùÁ«ßï¿ûç…ñ• j¡²Åfºjy&—æ°b)³½ÂðQïøsÒ)Ø´šÄsÀ+£¹gµvû¯¾ùí§gï·×=ý®yðeeñlúègÐ)Zç!(CÞéç» Ó ­œï”¦ÛO¾ÛñãèüƒXÙáÝñpçÕ—¿úo¤\·88¿œ|¥6öu€ñÉÃâô‘Ó=ÊöŽ3ƒ8ï‹úvyö€bùðÛ§ßýÝðøóÓÇ_Ÿ<ý©RÂÛkËÇPóÓËo}øÇöÇÚôòøêý·¿ÿ·P6 %H»fÔueñyçäûîÉ7™Öþéã/ž½ÿ±ÐžÓfY«mk­S»w™=Úzò;wx•Òjvk??õ¯W {œá¬[ezùù¯þáôÕI¹øɤÕ7¥å ³u Z]/.®ßýî»ßýÑ©íÈùIiñ$¿õ´´|5¾úä`n›çS¢kV™îIkçEÿÁ7µý/ÜñÖìLÞZµIB4¤|»<»êì¾<ïï?ž½µ 4£2j._6·_WæOŌ沽|týå_¼ú‘4jík €üôaïìëòÎK»wf5vŸ¿ÿíÛ_ü­Ù˜o†^ÝËôO­ö~nò ú¾ºx¼wñþ¿ýŸÿÏÑÉ[£4ÞüÝÅWÿrþü×»ÏõàýæO~­Wv_~þ›7ßþ°è øÐÆö‹êÖusçùðüëöÑçZmùäݯö}Åe{Õ­Çõ½—ÙÑentÙÞ{³ÿò÷ƒ‹2ݳ¥wçØ`-)‚'•–l~ÁdÁÄTg+ƒ½î⊲êFm+ ·¾£V—ùÁùàô Pž /»g_QònÇh,™üÔÇ•BRM,m—&× ©RŠ…Þ¡ÝØž¼ž^|™Ÿ=dósÆ´gO>ÿñߘõiuö`þð§Ý£÷éÁ¹V™éÕ9•é3ÙQu~ížàZQ+­æNº¦uà´v«Ó+³¶Ü9}³{ñ^.ŒzûÏgW_OÎ?Œ|1{ðn¸m,ügÿð?ýûÿûðù1¡@Û17)Œ•¶^ç/ÒÝ3Öjùýßüíÿ]c|Ägzíƒ÷ƒ/ÛG†¿?ü•Þ€'Ë‹¯k[ôe²àk»€äOF§_|øÃÿÜívv[‹‡I!ÏfG j¸ÜtEcñôÁ«_]ÿLÍz;ªó Æí’VWÌm9ÓêÖóÚü)®UrµE¾sHY Èp¨Ñ>Sjbv”mîar6L)¢Û(ŽŽÆ§o_þª²ó”Îü¤ tÒ(A#ØõEiü¨¼|Ù9z³õè»ãϟÙ^˳ºà¾ã·X^ÿØØ}ÙX^w÷^¦Û{ˆZŒ 9³¹OÙMÚnäF  €s'ç_ O_ë-Ê,¸íEiò ¶ó¬sô¾±ÿVm2V«ÐÞX/žÚmíË™.)Ú²{ô¦¹ÿÒ郼¾rÝÂðØjïI%0n[Je‹´ê1!‡Ë¥ë·¿±;‡+1žÔêra:‡“V(>ßÝíl?Ž ™æâÑî³ßtO¾ÍôOϾk¿×š»íåõóþË B³Ãû¥ÒŽ3¼nœü<;|fÆn4¶‚‡‚L ξš\~wøô7O~l._•¦ÂðáÛ_÷÷ŸFù ÈQ@9ðSVû"Ý»¨í¼I÷N·yùâ»Æø”±ÚJ VõÒéëõ}§{^[¾Ì·–*`*?Š4Þí¦´ŠUž·{;O¸L‡Ô€)¹}Þayö¨08+÷O Í=X«ñá›úî+½y^•ÉŒ¹ +ŽF+ï,þ`4v¹$ä§Fc¯ô¾wôƒÃÏÁeWGgÏ¿þoVaJåíWéÁü;¯¿|ü]erÅÚ7ßýÍòÁ›˜gHëNçRÌý¬ÄD7!æ €AÏðæÅ€º8¿®í¼Rk;Q6‹k5%7ȵABIò±0JKðœ m[«.äA*Y§1Éôv2½}7`‹ôÆ^irévÌÒ°7;ßú}~ò@,ŽÅâD«,ìæ¡Q?Ê ® !MPœõÙU{ÿU{ÿeëðP]`fÍ®m•F§˜š«ôw/>ÿëÓÏÿ¦¹ÿΨíz ¦¦Œ‡Âæ6ãBœÔ¥LÇíì9½³³ïOñtƒÏvÆg_A6…Ü 8:ï}98}ßÜ~bÕf|¦åœ`ʈ3?Êq Jtpø¶µ÷¶sòSg|•î¡r֬Λ‹gNûŒÎŒ!Gx»ÖÛ{Ú=x—ó¨\ȶO Ô ­"”fLi ]Èêªo#b¾µ|±|üciòäÊèðunpž”Knyúøí¯òíEˆ±ÜÑeeûMÿì»ÙÅÏî.?'Ó³4yúöוÞ¡Tj³‡ËËŸöö^—&Aç»Gӣϡ#ríƒýg¿ž}—ï_ص=»±/æ§iTY¾"TÎnæú'™ÎÑèèÝâÑÏäü,’;×ùáwÿ&S] R‰wZy®•A÷>ž|X^ý,Éfjýƒå£oÅâ‘2˜èÐFó.)µN+U”Ëì]} f]D)yw=bçjeÏ?ÙÄSrÕª®ÇY*Ÿ (OÐRn¤»ýÓÇß禧qÕÅÍ +a6P¥j5-À@»BDÎhPCíåFWéÌi6\–³j¾+¤«åá¡ÕÚÓ»îèÐÃmï—:{çϾß~ø%—©Óvµ<:›ž¼ŸSÝzfµÕÚ6i5a=ë“ H–T˜V·^BF&gN^ýîðù¯åb¯3Þ¿zûËêì<.¼K‡jsGùþyqt Hð!ÓO©…¥ËÙx­Ç?ŒŽ?{uúæŸÏÏ¿+÷Ž};>ùB*NÀjLˆL­ØÕ)3Š3VŒµøtݨNaæVïî ž¬æž +íVœàzà·»û¶Æ!Û—óC˜g„4¹h&8³Ny(Ñ>¬o=mï¼(€ü¶š”Ñè~…kU&Ý4{@ ó´ÂPÌ4уœëIù—é1韸½íí—“ÓJ~‚YZ«f›»IÞII9èúLçT¥Ý:‘Ý1!–øt RF{§§&Vó¨ø~ùðçó«êËgq!Ä„µâ¬‘’óÀ€`gÊ“«rïÐ*Žíê̇¨œÓszG…á‰Zœrv—K÷¤Ì Ź[;ž}ø*çîÐ.³j)]Y¸=B.‡1MÍtþ»ÿõÿÚ=}Hr„èº8ÂU¶ÿ°2yV™<ñ¾,‘òà¼ÌŽaa©RZYÈvÕÒ(S›?úª²<‹+6“©9Ý«½”+‘LçÝbû0Ýô¸Œ´j°æˆ˜¥ô*à!€³\Á¢=xñ³Æò¡ÙÞæ +CÒiÒVÅ(k‹+½ +*cÀ5Íù…Y›¢jÁOB~bŸ<É÷NÉÕkËêâÅìü«ÁñëÚüܨÏP53Ù¿¼|ÿK¥Ø[I\v¦Öíæ‘QòNÿ&Ä"HÖÊôZ)õ’Œj:­½g BvŸ|?½øÐ;xá´¶ íåó÷?^½ÿÐ)»«”w@àe»§Š;ÆÅb +’¨W³¢JA­ïŠÕ=µ~XY¼nì¼£Ò0­ÛÕ!È')ÛIB·JyÉÈ™äôp¡ˆP„¥ä¬˜éØõm¥8uˆ­4‡~—eìòôAië‘38åÜ.˜¦”THÿ¿$½‡—ÛÖ¹öûO|ç¤Xu4•½“Q €@ A`ï½ ‡Nï3ÒhF#4êŲä"Ë5®±ã¸ÛqšOœ8±c'îIÇ©'çœï»çܵîºwSw-®Y#ŠC‚ïû>ÏØûÝr9Õ<êäbÃF|ÄÌFΣŒÊÔÖýÉ^ Œ‰„ÜQ¥´pÄ€a¾¤71ž? +Ž âÓ:§W±¬ ¤P!ž™Þ™âìÙ\÷¤\ZF„¢ƒÈA½ -D›Püx­ºr%Û;ãKÏ‚`>b$-´„ˆiTLnJ™Y9;Ïøó˜G!¼É=: †t¹:¢µyuø˜™ròk[×BÑÊ +FÜQ}¸¿@øŠý;_´2b¦ÁxFr³8ŸÅ|Y•Ýe@@Άý©Vª»š›^‹4§"^¶;ßÞ85¹µWZÚ¢“@Ó¾T›n6>rñQeD9'd‚io¬*¤êŒ’wÇ +áÒÄñKµÖÏÈÕ.ì!Á”?]Kw—¼•I-^ˆå;¾xuÔ„¢\ÄÉE5ŒßÙa³³6.Fãr¥Ë¥k\ªŒH ƒ;‹ +ŸÎåæ–äö¬Ý¯8¸£4`Ðîʬ…`-€Øe:Ûd0Ñá°‰²S)9é‰q)åK×*Ó+ÍÕ-6S5»ƒl´Å'º&¡qðG,‡Çlzˆ•ó3 09)3öLª,{R=<\·ó) ËvN³52×àK@ƒéxkæÒcfz`Ôjsr„;B°Q;Ô;y5Ì™È X;3H‡–æ¶.Mlìq©Ê€ U¡¼¸ ä¤Ã?¬vÑÀ£&Òê’Œ2jvÝYqà%5w¤ÈÍ ‰I*˜ŸÛºÜYÛÔz>2l'”‰×0¹ %xÄŽT,cb„´‰ .Ÿ™8]™½ÈÅš£ 6Pl K*Ò¡²[.¼Žµ–bãKB²*å&B…ùpiÙŸšÑß8¨½kÈ‚r ŸÒÐÛèƒú1‡ÎÆ3r²º¢—0o~ÄèÔ"6W€dïRÛ÷©m‡tNÔW’KǤìÁç´vÞ®0D¼Øì.nήï;{ýÒ­'n>ù½'^yó­ŸýòãÏþø×üçç_ýãGï~|æž'r“GI©Ø_¢nv£ŒK§ ãÙj¯Ð^¬õ§V·6N_ºòàgxbçÞGV/ÜsüÊýgî{âü=½ôÚÛϽùvoíäÌú^¼6MR&䉔ØhóŸP2Uë´f–§VŽ­íž=uý¾«?qÿ³/oß|ôèåÏ>ðô£/¼þòßyó_ýéû·ž}½»~š¥­”¨‡ØåD+¡t FB™b¥·ØYÛi.o&gë k­•í¹íó÷=öܯ>üä“ßýñÅÿâÄ•[•Þº”n ÎàÌN¦Ârº“¬.ÁLxÌâ¢|ñro-ÛYŠÖ¦¥RG©OÏn]9y÷×n=ùüë?¹pëÉÓ÷~gïÖÓžXEúMˆOgs ÁJÀìÁì–ÅdUÎ7’•V®=ßš?±²{ýÆ£Ïß÷ø³?|ç×|ú‡W~òËã—îoÍH”g_ØE#"h!óÆIÆ„ú´¢)ÓKµ×C…ŽT_;síÁï¾rßÓß?{ß×|fïžGvï¾}ù¡g^yûýGž{åüÛ«goŒÁÉé!7ʆÙHÍŸœtËrfBˆC©V®µ8»y®95»vòü±s×O]¹ï{¯ýè÷_ÿãþÇWþûGŸùÀ“/Æ‹“‡µèËA'ü‰i¥°ª¶rcF‚p´?ƒñ ²â"ÂF0.É4Žîm]¼ouïž½ÜûøóË'.—gv3›”˜Ð`ß:d¡þüêƒO»k±Ú +«4õýþlĈ'ÙÅ+¢RN×fKí…òÄÜâöÞå[~÷¥7Þÿè³Ï¾üúõûå¯?ùÝýŸÿþúïÿ|öµÝóð{Wo Ñé/”ØhÍÌ.;¾}æìÅ‹W®_»õЃoýð­?þñ_ÿõ¯Ÿ|þÅ¿ùà…W_Ú»tyjiM)4ý©¶Ý6#î–(OÀsùòÊúÖ¥»o]¿õèÛ=ùÝ~þÞ‡?ûÕ‡¯þð§?ùÙ»_ÿõßÁ×ùÞ«?xáµ·N^~`bi7×^ö³R¼/w”|#œ.OL­Ì-¯/¯­^¾xñ?øá~ü“Ÿüì?ÿâ«¿ýœŽ÷?úü‹ßýþÿü÷/¾úê­ŸÿâÊ­[K›bªÁHY:ôø™bcbfåèÉ ë'N¯ož¸tíú+o¼õ›>þèó?¼óá§oýì~òÑï¿üò³/>ÿâ‹O?þô“—ÞøÑéË÷gZóNZvÒao¤ÌN£lœdÃA9/Ö—VW¸õÐà8~þÞó¯¿õË~ûõ_ÿþ÷þço>ýø믿úõG½ðòËϽüêÒÎ%.ZÜ +(V¹ìO6ÉZ½»ÐžZXßÞ}èñ§ž}áåç^yãû¯ÿà—üæÿù¿ÿõßÞÿèÓŸýâgþúëÏ¿üꥷ޾ÿá§ò­ )©­ŒÆL ÞŠ@‰ÈÔæ’ÅöÒÆÖý=õÔK¯>óÚßýðã¯ÿö÷¯ÿþïï~øÑÇŸþõ_þò埾zÿ7~úÙ'ïðë[?±{éF8× ü1D¨-8'ç}J!«ôæ7®ÝûÈkoýÛ»|ôö»¿úâË/¿þëß>ýýóÙïÿö÷¿ƒ÷yç—¿x÷ý_½ÿá‡>þôÆÙÝÍ+”T<¢Ã°ÏäðØQVgÆR¨´ç6w/\»ÿáç_ûá o¼õâëo¾ûÞ¯ÿùŸÿù»¯þüî¿ýõ¿ùè“Ožyéå“ç.Ï,mùãÍd}Ý'Æ‹•v®ÜHfòI€Q‹ãÓ“ +ó®ìݸuýü•³'÷N% i.àE(Æ‚Ð6ŒS›pµ ÓZpç¥`<*UªãõñöâÚÆÂúZw¦{üäƵ{ÎݼïÚ™K—Ï\½qtïbujVˆ¤˜`Ηì2¡’ b,0,B¸o}óüìÚ‰h2UÊ¥WWæwÏœ¼vóî¹ÿ'?ùñýîÃO>ýì‹OŸzáù­³gêSSÞpœ 瀮¸×F +´7¼vììæ™ëÙæTºÒl·'æç¦Oœ8öôÓ¾ý³·?þôó/þðû·þöëo¼úòK/<õÄíûコº~¬Øœ€¢Án˜ ™pÿ p ¦²F»‹eÅd"¶²¸tﵫ¯¾üò«o½õê«ß{ï½wþò—?=÷ýïžÛ;±¾ºPn4¥šƒô¨-(ê ‡Ý%Ú\"ˆ8Í#É|¹5;»´wjçöÃ>ýÌ“?xóÕß|ôá—_}ù׿ýåƒ_ÿìÉ'=sa7[.Ø1·3¬¶Ðû GTÐÆ ã~I.„äÌxwº7¿°±sêÔ¹ ç/]¹rõÚ‹/<÷“·úò«¯|ïùçžyú;¯¼üâ}÷\ß<ºKWœî€aL(QAœ*¹® gaÌŠ$W6/^»ÿ©g¾÷ð£ß¹xùú£>ñó_üêÇ?ýñ}÷\|ü›Ý~ðäéÝÅå¥j££äÚ…É-!Öº3ÚtÙäätfÜ3±dqyyãÒå+/¾úðJ=ùäí‡o?ÿÜsï¼ûÁÓÏ~ÿÒ¥»çWÓÅ*áöœ1 ‚ÆLiŒ°¢l,/çÂÙV©5;>³†p‚ #­„Û€¸u Î#ÌHZµ@£Ò9í¨»V„CH?FøQøX!Íùä”ÂÌ0bqâ¸Û‹±~—”Õáþa3®³Ó¶QÁQ~PmÔ˜Q÷Z ÚŽqŒ”Þ›àDN¥i*¦òÙf§»¼µ;¹´Œ'.VeÁ†ÍˆÊFê^=Ä«¬´ÆNÓþ„W)â\¢|$'BŠO ¤P4‹$S‰bµÖ›÷Ë‘°­Ö¹lVà|Ämň’p>.$'oò›ƒF­ƒå¥’[ÈøÅX1WN'Ó‘$‡¤¹…åZ­ÉÄj­†œ(‚ƒ$ø0Ê%íxPeÄGõέcHmT;Æ,n3Þ3ÉŠùt¡§Ä‹©DîÂ¥«GmfÓñFµš/'Æ[V5‘*Z`âà À¨=8h<4l¿h,ý•øæYQNGâÙp²@óA«aXQŽ}D"]®7§›­)ÇE¢IÁ¯ „Wk¥ˆ~+Dd‚%6TÄ8 ,«Óíæ”`¼’šñe²Íñ¥Kµé]?fµÛD¿&y!a´Î†(¢¥R¶³Ë*Q£ë}{ÄŒˆ01@p–IÊ *J4VbÑDã|JP.ïOy¥œ?\c }ñ‚àOŒÓ¡ê·ö j¨í „Zœ„»üÙD±7¿}C‡ùÞâÞéÏ#1 +ìŽQ߈³¢ óíÖï2Ö8†u¨óម7\cļ‹K8=Ñ~O+9jÁÌ.ÉBFL˜Dp)) `Ã¥G8;&@„`„i­†ùÌÑrÉ#åŒ0i‚Ià ì‡x‚t0cq‰*¹cGôè7uU6àu-x ¿ †:YYã`Ô6Ê„ðc&Lg'46v=<ÀdŒK¾´¹@Üå“Œê†-f§cã*Œ +y>1íä2ƒzÜèäLNvX «¬:=¤ÖZÕ:«¢)Gq!'É£TH £jT˜1ça5ø ¢"&˜Ó£Z›[eãTv¯i`Ÿ•”œîp¬0±°~ZŽåÇÔF +0NFFI¿ +¼ÃˆyPë„\Ê þ×ý£*ì +Ú¥¶0ÄgÄDp< ò}‰¶• i!.\ x€ç¡"BøÍÛl§ëíuŒ4‚·²s9Ø_ÅW†ªOT‡ú ¸8j¦€Èz"ã\bJ*­G*k™É3\|rØ€9q6žm[QáÛ‡4ÃýŽ>^nT§NgÛ;°'upØ6¬²Ç3];Ú7b6’(°+UÃùy)5¥3QƒÃfó™ Ner™‚“ý©ný&“|¬sx >¤¶Ö˜´ÖA­Í‚a6Aø³ÜâIÈÉŽR˜&Å,.dm®°Ž:ùt¿e(1`o hö û!d¢W˜ô¦I_ÖéŽ&‹ ‰ÚÆA•ã°ÊªuЈ7íVÚ´ÜÀýY æ§Øp®6 +éãˆÖ>l€Í˜Ì/ò©)&RƒJVøpÎàô€SÙÜ4d!£.©)oÙ\‘#:§rˉ¦ó©¬Ô°Qg&%*P •;K*HPÊì® Þó¦æ ˆˆ±>Z‡ù¤4×À°íˆ†‰P¦¼‚¹#ƒc@W#˜¿¤C%-ì×BÌg=Ñ.n™p ¢CRRíÜ"äI°Ñ@€b}™Jç¤ —ö1Bž8Ê¥)!ã•Jà™av×!½Îˆµ§NÀLtÐèrzVZAøŒ'Ò–‹k”XÕ»ÆÀ áâ>2f³8y”M¸úÓM›¥CúKF 3îwùcˆ?vXZÝwº€–¥áOMf;[¡â &$È`÷çÙè¸':áô dD‹†tÄ )Ö_ã?êDèÄĬ¤L‹F®TÃ:§ lÌŸ€ýù1˜7á‚œ¨t—N‘¡Ê H(Ôg§$;Õo1¢Zëð˜azqãLcö¸Þé¶`^Þ¸7ò9W J‡ZàŸ..æ‹”Aౌq+Ñç)W¨îË̱ÑÌÄ#Ù­ èá¾ì +AT˜‹—6ãAŒ +¥ËóŸ1SƒZœ_6`² +É®÷ﱘQÁÉH ©³¹@R“n¡AÕŸL¿6,€q¶SQ#&ÒÂc6·Õ“æ¢áâ,îÏ©ï´1á2ø_ÈE¹„ÊÚ_tãò16õ­êý:ðMÙHÇ%VÍXHcåGt$  ’ƒãp¤v°§Wåð±€IÚè„Í¥ÔgN¥šKV:„ò)œÏ¡îæI¢ž” ‚¿³ša‰î1«§ß`‡Š k­ÕâÍm=ÔÁéÏ€j0fóÚ] *Xå"u_¤â –˜@f­³ò¨¶q`Ô>¬v?u¨Æ|¸þP ûHvÌJ«mŒÎÁ°üö€ñˆ +±à’•þeßè‘1+DøÛ6y†Ìü£û®1xŸ +ê6·z¡¾|î ‰q°H Æ$gýå ¹~”ËL‘B­µºº÷ …ŽBžQˆ7¹bv&EšlbaÌ)‚š`FüG´@PüŸÆù´³ßã¥ßòš +!wb°/mãh°HE*Ÿ@ýyw¼+V¨PÍáV@èšéÎ';ƒK.Á†kÓ«—W.>nñ(v&ìŽÔýÉé@r:”cb݃FîŽ6:Ût t×eÄä²âýõ•ÞèDeöB¢qLJÖ—O\A…ˆÙ@q6\_‹46¢ãÇ£;Ttââ¡xýÞ‡ž¯Ì)iF½FÌo&eÜ_3 ”ÔÐ:8é÷• 9Ù4Ìfíž,â-rJ‡‹Oèœü€Ú º³¢Çä’!wrÇq!GK±Ê%fAzzãJvÝ™bM†ëV.¥Ã%=*‚4jéχ×#ÜW‚Kºø$Λq_¿A;âJcó€R {âRrxbˆÁż‰”Æl̨™<¤Sa\÷¦ÙX+5½gõfŒ¸ñ[°€Õ¡G-âC¹Æ&Lê±oZö«àamBƒ(_¤ƒuO¤EI 5*ˆ¾c õçŒbDE!€×•«:K±Êî…Û´˜þÖ€áˆÆiBE ÇÀLÂLøˆÖ©µ{H! ²Æ鎀ƒ æ¦CÕe*>N%(BÀ G›*rDƒŽi vºS”Xg‚ãD1{`*Â÷fGõؘ‰<<õ!A„Í{“r~˜ÀP¥ß‹•Ftm2.SRUL÷Àa0¾$+ÅGLШWƒªë +ÛÈæËØÙ˜p© )%óÙi¦?ýo6×ÝÍM ”!)gº¾LöDTÐJŠ'ØD lw¦>aíìãñÖqpx“KgêÓ;#&ÒåO+õåXg;Þ=Y^¸Ò>ú°Rß… a#T¨`ç€1óA\öæÕ°\´ÊƳ7ü 4b!C¿EgÌƦ‰È¸X;ÆV¬\ÆJ=‘²K*¡B².©è‰6#Õ.Ö²"¨-Fˆ8‰úÁà›©aA$ @mpê4䎕÷¦¦üÙ9T,#¾¢;ÚMŒŸ —7ɃltÄ3*Pv0À*Ç1¡À„* ŠbÉÐïúåêsiöX[eeþeÿè¨Ò8Ûj,´_ã3„P@Üqh4¯2“À¯‹ö¼,——Bå¥úÔ©Ów?ë·0Vió‹Œ2^èœ,÷v#õ1Ä7¨GQ. *(¡ŒX‰Ð'£ËÌ*å9Èß(o é^°²Ì&'9¹\ïnž¼ñ¼óQR1T^‰7†+˱ֱhó¨/;k&ƒn©àK´Fô™]À»•`~©µq#;}¦ß×JÁlÔ@Gì ™´ÒW¸ªnŽ¯ÞS_j"\™·³á#FÄÎÈÞd»¹zþäÍg×/>^˜=MH…H¢2·yÕk Xhàú‚ŹPq”Ûri¹<µé–ózDŒÔŽ ©9„Ï3¡à]¿[7Q]J·6F-Ô¾a«ò?àVêD°Øß Ùô ð¹zR’­Âì©êòÙìÔV¼}ÌŸŸÇ¥ÎG}îÇ›nYéÀÒóÑΙÈøI·Òµx€W,5—§7îþöÈJ”2!5§æ/d. åU=åÃåÛÏüpfãâ00T¨èMõÝ釳“guNÿ! çÞ(ˆC:tÈB«"èO—t0Á`§FF¸8€V.Ù¶·ÓÓ{c÷o=4ÄÏþ¸8±ÅG'Ôv®ßâà  Äjnú u€À˜~•ò¥Í˜_cgÓ ²ÕÝ_:j^—ÿ£EaR¶b`—¶“ŠÚ +´Ò6%Ž˜]j§NºÝÁÅœ˜‰7WÁï€ +cãÛrcÓ_XÊvNšÖo‡k¦ Rd‚Y3!êA ‹*›0jfMXøàùAÁ¤…Œ«šIŸ÷j¯SÈÄêë¥ùó¥åË„7YšØH4W-õgÅŸžÂƒÕhm=Ó9)¤'K!Åb¨° ¦z@Áùt ÕÍTŒEIˆïW9€E .,HÅ•`yMÈÎÂlœ³Jm‘Š”Md–«\¬™i­,ž¼±qî¡ÖÊyTL §¯=ÊM—¥ÔŽ-ì>¶ûÀ¥õûéø´[hœKGŠKcýy#˜Õ%‰Ô:}ÿ¹xWçªj›ÇN)ÀÝ™©ˆMÒѨ`á¨!lkqñÄuV©C¬bqù _ÆáËЊՖæwo‡òS£vÚì +¥'÷€Œ2JKÝï„ÌZpQÉt íM ‹v*‚‰TÌ£’'1åIõ¤Ê|iîô±»ŸÉOŸ:¨#¬ðÉ£´ao0Ô5ÖÏå"åÏ|ãˆAåà˜XÏŸ_÷ÄzllrÔ8΃p1î·R¡©ã×7¯=5¹u£¼p‘ÏÌéÉð€¾ß +fçîÂÅE`5mT”Mب9¹¸¨Ç¤}Ã6£“zŒ?}hÈð}£ƒz¬ ¥@yC*­ÁLڂˬܴáòÁAÓ ?¢!ŽèH$¥ƒ¼ +K“À{«!NÕßB"EàF¨xGH©HG†ø ­Lá6¨Àt„d£¨;lüBÇ#à|<úíªú Çx`zÓå%9?¹oÔ4 Gtˆè’ªñÖfºw*R_òÉù­½ÛÍÅ3zÒ/æZk÷T—®%Ú'+sJóéèø#ÉG[„/c%ü}/ÍÄ`¡Ì„'”ÚQW¨ta#1 +° Àî:@ñ‹.CÉýÕ‘¨˜Û—ìàBŠò'³ëùé“Ù©hci|é\uö4© ‰ñôÄVfb3?yÜ“š4Ðñ1$ˆ7W +‹ƒr@‹‘2Ÿô(ne¬)=ìEAý­ŽÜŠGiØÝÀM‰•é­b÷¨”›²sX¨åô—A‰`“¹™KVwzÌæå“ý+Šªÿ¿»4Ò‡Ôˆ…Æ+KfT¸ëàè·ŒéÀˆeOrš5,”rXÇè°0ÄfµøþÃz×A~ìe§£ºþ‚}%\]Æ%š›t0a Fà+Êë\f‘‰M*똘7À}»¼+,ä@Ïhí¬ÖÎaÀ$<ð¥wØöر +J³ßq}Ì>dÀG^Ñ +(0JVÉz¶µ`œô¥jó{µå ÉÎ16^G¼1›G1»úÝÉ8¹S!½äîV¦åʉdûLeþn[¿ù3(/Io¬DSåð诅Ž3Ê—˜Äƒ€…ÎF®µædBá‹•gÛë×æÏ<¼´wûäÍï§&¶™`eçä½î}š”r*dñ$0©,¬äºg WîÌ]£ÐAPC<).9ÉÆWFAv)`¶‹è1扶8àBÃ5'—ØH‹¹he¾¼rñ H QÝȶOG +K¹3Íþà¢⼡¹¾Ç&::—“}œD}%wÀêCT¤0*;¬c 7*-§wVÎÞžX¿ŠIUot|ýÔM˜‰P‘‹¶Cù9P`åâ +l ©1‹ c&öͼ1b’\^IMnoƒù²vWPJkn•Ý Bkf÷±ÎöíÒòÝ©©3°¯°o†Hey÷§/u@ [=zœ…$p‘Jõ¸RÛ>¤v2h­¤³½µ³cD8,”¸Ä,p§àUFÔŠ Æ~» +u +yW¸Þ\¾¸|ñ øFBÊJH P™pÕky]on‘/¬BÞ Ì(‰Ò<æYpŸ 0ˆ?ÏHeàú˜`a@mß7d84jÙ?bÔ ëû»:x“3€[è +él”KÌy¢@a:Xš‹·ó©I\,€AJTa6àdÞÊ”˜/ã" 11eÆeÄ➈ +©€”`~RÈ \ä‚•“ÞæMÑ" Ù†€â $xÄìÖÁ")–Â…9N©9\'ðF l dJ73½Už?¯-Ç‹ó½Õ ÑÊ ÌÇü©n¤~”¶97/W7<‰Y« @“â–Ê.vÔBkZ˜¾yÀ$œM¸Ã¼¨ÏÁý*Ó·†t&TbÌB¬¾ÕܸéQê\¨8µ~Å›™@ã`Mww`\FLtÍõ#5¤#)_Ù(ÙÜûTöý*»ÚÁ’bÞïØ(ÙŒrb¢Ž°1Ä€Ó#D%RœŸ8`Ç€ÀÁæ&ŽcÞŒTfàÿÁ1»ÊìBYEc£û3Ø¡óYo¢KGl¼ËD¨’ë'A=çã¹ñµôÄ1ðŒÝ›Õ“!=.RRÕ—š –ÖUvÂ.O¤éO/)`6–i¹©vö:UfºP_löŽöÛÄé1=pÉu©ºo~PåôEjJ®="á0°xa.Úð§&è`ð…ÖÜ¿ºbDƒ.1O ¨7éKuCÕ`y=Û;'WÖ¹DÛ@ú}çÚ[ÀÇ‚Z×oBµ6@(®#"xûoßÙ—a°ß¬^@Ý)A)EŠTÆçNB| ^vzgñÜ‹žÊÎ_p§§Á R¦;³ÙžÛsËUÂkaîG¤ˆÖ¤¨³“œ”ñ²X àé” + H Lj3f&«ìýVÛvÖÑ¿BÛp‰ýÛgfW€ô%³íåúÜæÄÚ¹îñ»{ǮθY[8+76ì\,Užé_8┣‚w¸…ûkîH×A'a¶âÀ$[-ˆhëo–°»¢F˜7#>µ…vy3@6.êf­®ø,˜†!í +×øh=ÓX^;u#—ÁÀöu¹¿ãL%˜žáÂã¸7§±sX=UYP™©ýÃV••5á2-Õ@Fþp‚R-Ll¨,Ô¨©c%TXÊõv‹³§{ÇnJ…%û…ñ•Éµ‹*«ÇŒømxŒ’ÆBYP/ÁÇA6%js.1"ÍÒ ñÑáZ ²’ž»ÐÙ¸7˜îN-žzííßúÝQ›ó×½©…XmíØÅ'×.j¢€…:¥²{FLY¹a‹Wçùè¸+˜SÛiÑ* „„òØÜ à=B|Ž +7¨PÕŸîFkËþÂT 8§´6½ù:6N…ËùñÍÞƽl´9fç´pÀBF > ˆÕ„ˆ@õ0DØä6Ó*›Ç‚Í°&‚b´Tè¬ñFºpïR¸:©-ùKKxxÜÁ¦ÄHefùìÄòÝvF4á:؃°I*XÁ„ ìϵfÄL¬0Ó¡þEu\Â|UBê™W9xðUVzÌì9”wØHö;á!DHºB&\Î4–n?ýæ³?øuuþ”KÊòÓb~6?¹“l¬U'Ž·gv89ÇË6\u‡ª¬2ŽpyµM84†éß¡¡¥µy†õ®ƒ£ŽÖa2f\ö*ÍRgk|ñìˆ6à+5Ña*R —W"•uVi°*O¬'«‹FÔ¯ƒXTo: +Ì£¹n¢0udÌ1¤A`ฤ•Ž\ Î[q‘ô¦…XÓ{íd¤,Š™ž§?§®@*.!]j¯W'‡©0ÊF!J68XæÖ׆ûÉ&æ ªlvÜ1W°èŽµ¸d¼ä +(éÚæî `׉@]©lG«[B´-§'Ådæ3ZÂíÏ& +ÓÀ⵪m^«+E‡&[K÷5VnÒr㈎ Å¬.îo è¾5f4³:gasþÔ¤ƒZè ÎÉí4‚Ç=Šù³Áò€>J®J«þÜæ/Èý•Ñ-=í÷$q`w{§¦6ï[?ÿäÖÕçgO<ˆûÓ¤/ð)+y4d$Ú†4°âGÌ4Â'%±ºG-n#ÄU²S~JNç&VWOß_YØ1Ò"ı¼’híH¥åþ<ÛD|z¿¡7#$(<_xÔTãxnb‡‰´G-¬ƒp!OŠ„•V…DðP9`/Á0‚â©s¸‡¨áü‰æÄÊÙÞÆ¥DsybiwîÄ5O´„ ºÃÇ[•s…ÉvwÔŒ{ÙPjrét$×4dÅF,dÆÃvW çÓ6,`q +CZx¸ßŽÃ5¨†tN—/%=7#e{”˜žZ½œk³³R©Å;§²ÓçÀ—â£ÍöâÙTm0;Ê¥‚Ùi)=Íî}D}ó€VouƒÐÚÉýÃú#´ÝŸ©²¹uv®uÈæ>¬…,¤„ñiàâÕ¯R±4¤ÃQ&ÊI•‰µ›`ÞñÀx8©ðÑ95fðYm%Ž è1`'_‚ö*Œ˜Ä½I19qâò³›×mƒÝI:Pš)­à\"ÜÒ8<@õœž80<”¯äŠ¾ø$ìI¨L$í‹”p8ÿrHÀÈœÞáÔ¢ÃzlÄLêú¥Ò; ²šp`T,˜ H.ÔŸuð ˆãb¤žR_ã”ÂÜÖÕÖòùTc­4¹ÕX>_š;,.þŒRèyÂE䶢ÆîÚ1f@.Ù%døH ø±Ló¨|S-lrr˜7®.Îœ¸wåÂw +“Û'/>|òú3Þtâ’ŸuriJªùÓs‰ö^~þš7;C +©lëX´±Î'Ú[¬ýn·¢ð‘.Ä$¾=l¹kÈrHOÀbÏx ÚþŽ¨^›‰”œbÁ9ÛYÚºrãÑï/Ÿ¾ª-ݹ ZìmÜÝ^¿ÂDŠV:àMtJ“ÛÉÚ’/ÖPòŒ?®±àN*‚òi#"ŒY`56¯ Z fâà¼2h•f^™À|e˜M‹Ñjwé””n!Þˆ?×­­\ê¿ 0ÖÙ¥£mì‹ezë{·mžÐ ÓC,D›+b'#j“ÛŽY®Õ;Þ[½´ȼo@ÃéŽóɪS D-Yž›\½xÙ +Š˜7íäâ(‡È 7RAùøˆ%Ü}ª±Q+m„¼£z 13"XQßÀ¨Ñ‚r„7¦³»A¤ÅËSí€*gÅÅTë¨T(Ëú´@ÌnÅc!®Pã“G4NVÌg„8Kÿe&ƒZ3>ªGÕ0æ|ìÛ‡4ß:8öÍcõCzÔ„øX1 )S›“’ãîÓ̇‹9Zi +™é@i9ÛÛË´u'×^ûñs[×3’KsbnP*—˜vú2T Ùhwᜅ +5©MZ ¦îïnƒ#Á‡Š´˜ºï`R,ò©‰t{µØ;ž¬Îž¹|ëÄÕGøDõçBÅU)¿”ßi¯ßW¿Tž>ÃÅÚrrbñø*”ÔᬅŽØúýÄò¬2AËãF<¤rp0`pRvøF¾Z|Ðäœ%fæ…Ô íÏ-½Rên0á¢î·sIuvk‹Wç¶om\{>ÕÙʶVVvo™>ÖdÃ5\È@ý­Ø3ŸväÚˆ7 > )»¥6™€owEÔ`Ì‘€Ë›1’T0™6* DÙÔŠÜ1áM¬‚¼&JñDÛ˜/Où23+J V©øsÓ‰övafobíÚò©*‹gÄüTsöÄ£/½ã‰·MT˜–(© Œ%Ÿº³+wÐ 0x™Ö±þž)wÚîtÕ:|(›%ýå!5b&€>‚ëß}îo“];Ó=é‰TªÖ9È/¤‹•©ã”TÐc>ÈQálªm< Y² ’ÙÎA„@oP‹ƒjï‘r£rÐD3J;?{.Ò8ÊÅÛ&j&eb²îpGœ\ +PŸýÉ^¿‹;Ý$­eDg£µV +szbñ¼”¡/—º>þb¶sâ_êGL¸Ì.%šÌŽoóñI&No\õ&Æ÷©Cf—ɱ¹ã€Ôs½Tuvjy·¹xÚD(9ïÏ´…D’ÍåÅ“÷®]z + ÃåE¥±ø +WØÔÇ1’ +ˆ´HqA)-0á ÂÀyŒuîl´7¢¡#æHsá€ÆªvPö¦zòj´¹k·RŠ’î\¿ýâò™Û@ ¹XM,L ¹Ÿ˜uÛJM¨à –ó' _qèŽo×ÛY€ F!ˆNG]ë=}õ1¼qLr+-*Ò²¸SLdûÖé_Öâ’VÒííñ•ë‰úN¢~‚w ¸h4”lÐܘ= &jÈQ«`À'Ÿ妷.ÞÞ½þ„ƒ–ƬŽX=qJ®¦[ëÙöNbâ´Ã—wв\™µÚÎÆúËíÙ¤‰šÉP¾wº¿ŸÍm£#N> 31  + $@º“‘"…®… éa-WbÍüä©É+…³ý—äîùûŸã|kiÌƾé/¹U;Ó9ýÀpRþ,âQ†L¸ÖÉSÁ+BªÄúmyî‹–¦v}úÕÎò™ñõËd.efϽwêص™cWãÕduþÕŸþæʃ/8È '•<¡† ,Ðñ–7TuãÀ(ÂH%ê=4f?8lþÖí¡aI tG;Œ\‹–æ=rèè¸+ܤ£ljZ‡x©P^.NƒŒp+•hs­0¶¹~µ±q½¼r-<¾íIvåtoãäs[7°@”¢ýY%ktlΚìoïntVÚþzÈ€«ín!Ù<ýÎ3riV*M%sÓ[wOo__={«½v1ÖÚÀåšÝ'î\ƒµ‘£?ß̬f'N”¦÷B…LÌk Î +tÇl”ð¦s…Z|r!TXaÂÕQ«vxœl@ÊNd£pq&Q›?ùXã"‰Ær®{<ÓÞò³‘êR¤²è‰ÔB‰úòö剕SVJø­-õ[ÙÄÆ ©ÖßiÔD80ßêöµxiVmç„YȘNƒl¥0uââíÊäQ€E˜ñæfƒÕÕâÌÙÍKOí=øzsý^˜Ïjs«;w“¾$ˆ±hyµ0s¹ºx#1~Ö›èdÃý›»÷&kk‡´ø0PX«ÛÆÄûÂZZJLì@îx¡±Tëm‚W:Ù!•]rM)ÍáHMl3]`£ùž'T4Ò¬OåÞ¼[i ñI#*ª¬´[.'ÆûšŽñ)Ô›Â+àÃåDuÞ-åʳ»‹gIu·Ãåy*TtÐaŒ‹œYÞ½ÿúãoŒ/žu²)>:IƒS0:èþ 5rxÔ9¤£}‰É1+õ/Ô‡Uð¨ÔYY ÆýeÈ"ÄËUðn€“ÝíÞÉÛKž..\t²ñtk£wôúæ¥'›³'·.?ÖX9ªÎ–Ï&göÄêº?3Û[¹²yþñú 6!5onD*+îÄŒÍ6û‡Í‡T“‰@tHï(ÄÀž(,P¡’X˜– +Sóǯn^y,PìF› ù™“ýöDÕµTûtyöŠR[·P’êßVáÄÆÆùÔ4›âRó¤Ür²i3T9'—púò@X\ö€AËh>{âÉm$ x +âb¶W˜:][<\ˆ:˜-õ6…uò©4ß\¹²vîñé[Re‘KÔ!6H +J{qÏÎÙ=A2TÆÄ" +BHaîh¢8%Æ<`„}¸¿9‹™› 9˜é%j‹@G [î(&\ñ„K‰ÆRså‚\]¶3‘ùÅ“WxFˆœîH¾{:Ó9«n‰5Ä ›ˆîì=o®éo2€¹ ëÈåÕtw7ÞÚ"¸Øé‹·ŽŸ½ ¼—‰Ô–¯»òìÒ…gÄòš™ ÃTð±g_ŸY;«†…ÃjT û¹øt ¿‚ûJZÔ‚úŠGcõ!£ËŒìTªˆÙY_zŠÓ;—>wïwÅDÛJJtÿR|/V]ém\Þºútif+˜7ë‘›:˜?8fûæ!Í!+pƒà­P(uè®A³ÙɇÓ=…Öb@7mT¬¿å( ø,ä +BÒk&ZGÓ;X ÊÉÅBw»8sª¶°»~òú…[ÏçºB¶»º÷èÎÍ—z'zqùö›­Õ»Ùh+Y˜›ßº×ŸnA¬ 2’Ëœ«µ1B”cµ +pþþ´ö€š@Ëe¥¹žèn7W.U—öød½»v¶8·n®ËÍ ovÖiôg5Ç›@¹¼¹žÑ-Cþ´Õ›¬·ŠU-¡8¼92Ò óN_–WˆH+.©Šù‹˜5bÀl{q.Žp +Ä…”êÊøÆÍÖћŹóéÎv¤ê[%˜jNmßíÏO 3Í£7­ãîdÇ­Ô±N.Ÿ±S~Ã.-óñéËy£MÖCÊ*þX8¥»mjĈú\b:^[ã¢ã&LÒZ=xÓî56ÜÓ€äw®~çä•ÇÛ+” ©a7DúNõw Óãƒò‰dm9YYeº*+u`Ī²262l›Óƒæ䔑ÒåÎÑÚùp­¹zqêäýÕ…3RqŠý‹BüÚí§Z³'ÔNP`\ÈYȈÆØS0!^'J×æ¹>[te£n¸•qwtKÁxíÂÍÇO_}Ü%dœžduîrvòŒR]’2]@v:®²0F„-´·@ÖÑ¢à”@²¨[±‘òþa»Î⪎/?ôôF´èÁa›Êìv0QViÓ³¡äÉJõé &˜1õÛsÕ]ÑI˜ò¥šK¹Þvaj;Û9*—¦½±Z¦µÚ=~­³q¶ÐÛÅ3˜Ÿf¢ãˆcõÊä&®ô§²øû~÷*áÆL„õ‚ß]BRmuZA¸¨/5ÑX¾¸|þÉÉã7ʳ;“ó›?úé{»w?Æö›_ÍÄ'vBåÕXssbãúú…ï´V/ç&ŽîÝ÷œ;Ùջ¾ü||âDjòÜô©ïLï>V]½R?úÜïßzî§&*¢†Å1ȧFüV.íŽ÷BÅ¥lcåÔÅû§Ž_ÉÏîV–ÎågOʵ…òü©æâÙ•3·NÜó4PÃLkmûÚÓ{¼0êâì^ ?Cûs;iy\UvÆFE,”ª"-ÕøX‹ód¨fÀd*P¶þ1¼¾s²BUØ7b>_µ™“l´"g›Ùú¬Ržç“Ó˜XE½œKÛ qðÎ.Ò $¬¸þDyûX=(›t2xec@•0oÄ7§4+2„‡Ý!3Æ8ŸGΊ©ñòôNkù§Ôœ8ÈþšŽ;3îp#Ì;ðzÓÞDÛî’ 0À4VûuvNc¥Ío$¥dk½µr¾0µ#æfaoFmg½R1˜‡…sNwçrÞHç“Tpö—c&Ú‚ˆ‡F ƒG¬ÍcF +"ƒNW€’Àóï;lÔÛ˜1y×ù  +2S*›[k¡(Òé–ž°Tœ •–i¯v¨¹TèmÅÇ7}éI·”&JeÆP!À)&T——£c€õôˆb˜ ÕÜRiP‡ð#ž˜ÞÁ‚Ô89pz"ÿIïÙ%Ç•¥çþˆ»$ÍôÐÂʧ÷‘a3¼Í0é½7•Y••å åQUð ‚h@4MÏ64Ýä4[3ÝÓÞΨ»G#iFjì]ëêjݯ÷dk­ø@2*"ÎÙûÝÏ[‘gJ/+¥•lw¿½vu¸uz|çQwã´Ð?X½ò¸ðhûÆ;££3­-!Q•Sí\cu÷ÚK|fA-®Ô×o öîH4·¤ÜmW……ë/|ýèù·¼¤)fGLr –6—OßZ:}+ÙØê,î~þÕOO¾-æìæ&ÀíÚúåãï¿õŸü×ÿæÕO~´íÕ¾ýW/¼ý¥²¦”×øÜŠÕ8hmÞß¾õñÂÁã¨R£¤\ ~~è€åyH ³v€I±¹ÕòÊ]¹0Š0Vea‹RŠ¬Õó‹±dÀ$©7‹ƒÓkoãZ–·JB²‰éÂàjeõy±°¤ €üÙÔeuŽQ³!ÊµÕ Çÿ´9‹6飣B>ÕÜ%”’ U`©ØØx>·x¤z±D›ÐË^lÜ…ÆOÊn˜†˜8.g­DÆKl¢Ž+Pp€_»w’õ5 Œd¼îCÕy̪‰V÷!2˜zƬ¹ñ¸ •1Ëf–ÍK¤ÑAÅ<ɧH!16gÖ!~ÂI lŠO´p¥ +\*ÀHOT}f2 ¢4BYABæ"€Å§=áñ7ƒB'ÀÿNºñ³óÈyå@´`,1Þ1GH‹évª½ fªbp%£d»Bf¸$1Õ7K«zy„‰fL±ØD bM.Ùò+ÀÎÁ„¢–ZŠg§÷2ALñ"Â|9;|ú‚˜ˆ '¤RˆÐAÁÔbqá`8ßË/ez[¸š’ÓöÆ ½4°+£ØÇäD›¬ÑàͦœêÀ\Òª®d:Û1£•KAT:‹1‹²]µ+Kr¶ã€»±%†bfÐX¹ZZØ5ÓÕf{éÎÃ7Ú뇼Un­\:¼÷öÕGÜzüíë¯}|å•÷6o¼ÔÛ8¼yÿµÝ¯µFWöž{§¾õBsûá`ïakí†Ý¼aíx²ùÜ럵6o>;Lx𨔋Y½y)¿t•²1)uÿµy£ê„eIÅ‘Ý96*kzª}÷åŽ|4âgaÙ S«÷wîÝûxçÁ'Ri=ßÜxáï¥[Û.HtE`³½ÓåÓÇ'/~Z_»#¤‡G·ÞNTÖŸš +=;šòÒA2… +E»²ÞÙ¼“ó—o¾üÁw‚KXm²¹5&=òË—¿¾uç[éÎi3®½ðÞՇ﫹0ægQY.H +S)BkŸ™''ØöþË·^uCŸ?;ÿçÏÌýË'OL„\(`žu?ìWîïj™¶Z˜R>7of„ 9`²"¤&&*>Tôa2Ä¥¸t_¯m—û{÷„üRPìòŠ”[ráãeV³!~ÒK9BÆšŒœöE(‡Âb’ÇÌú‰ › ñY÷x‰_.Äf˽¼œ[¾<<}µ{ð ¸vG­ìÌ)?ÂqfÕP–´€ÓŸò1žˆ„‹«²Žˆ…aêÙ>¸%7»Pm*À¹QÕHÀ‰°J Tv/Ä›•5òŒRʤ’"¸O5ÍÂÂé3>f LÎ0‰juådxx¯³yCJµõ\WÎ.„ÇXh3Zgí0&c1-JësØ9âþÓ+ T +JSÄÄ F›HLwED˜Mb>LÆÃ1=ÊÛà¸e¤šÕÎF¾µHã·™ÕѵLo_+ôbñ\LIŠñ¤¬'AAœ sÜ!dcj)ʧB¤æ…9o˜¦ø”šfg."kÅ5½º)$»õÑåîÎíTwŠé™BkãðŽUB1Û(´âPHu(½¤,Lêþ¨Bûz%Î;PgD +3Y)½¬ÖY£…ñINN䪃ê`|@mì(µÌÒìàŠQ^Êp)军W–voî(ȨfƆ¥•Ò¬ŸÕT+Ý}øþ ¯}ä„„§¦"^n6jEäš^ÛÕËb<÷ö7¾¼ûèƒþôD`ÖÏø7›Ç¥­ Bœ~‚`M=Ó + ˜³ål°_yñ0ÓÚ„8›ýÕãLcÕ ‹>T£âµ¨Cù¤ŠÍyàiGÀDb¬à †¼á(&¤ìÞ%vÜÁ»•Ë(£w;kÏ«…ŘUS +Ëra#È\.J©11é +î ! „² >'%{À—ÂÐ)N)±R®T_ë¬]wãZ€Òƒ”îÄ95¾gFO”g}0ÂóÑI'ÄãðB:Ê'@`oHª?.Èévº½£×ùä Ô?Qsî–Û[™úª+Ì?èC?i;àDˆ*ÆÔ&¸¥yOXP’’Y›ö’c&ybÒÞh07&“ÌeÅÔ‚b·&€ÊñÑñ·§óàQJ‰r„TZÌà†å ן !Ï8#slÞG‚ a±ñ>¸Z 3iG(f•—´ÒÀƒpcÐzÓŒš—BLL8ý8%$­|Ç‘ž0"€/ÐÙx‰’sàqÀ88 ^2þ* ð“Ä1¼3f *ûà«öi£³I˜¯ˆDh›Ò|<ãò¼=?ˆV'âÏ\ðŸ +͹`‹‘´l$j™òDø˜Ø˜OAb)̦ƒ¤ÓR4Æe«]É*==¯ KKëÍíjë7Ýa>Œ)¢\å‚6ftRÍÉé–œj¡¼ÍÉINM!”êsQÚÏ 2.Dª3n$å* 4'BQ”`„Vå\Uó~R÷¡’ ¤W7Ov/ßgõHU½°’ï^VóÊ @´Û‡òC|Wü0@ˆÒ.CÅ›8Ÿ!’¤dwwoÜxéC©:a6@¨>X +èÅÁ!ÏyÑi'xoÞGy Á‹ªNHœ±nXÀù4¸y˜5"¤œª­$jkZa$g—»KŦÊV]²› ®B¤¡fÒ$´@h¹BÂä\ÐéƒÁ …Äç\O_ðÍøbòå•ò"qàÅ´Ü"&樈P]OTó“c Œ~T¾0òH?ðÎNèÜ\I²ƒˆx§æC¢V2Óýª !U Ê·ä‰Ð€?•t}Æ*JJÀ»™8kA¸ä anŒöÍUnŸ/Žà³`&áCåY@wÊu…˜§Î¹'ç  ";ý1?˜/LF8§ À|uqŸUóžeÒŠB[ /ÌÏOyf0Œ)³Á‰¹ˆ7"E°¸’(Ëf‘•²a4ÌàŸ=3ÿÔ„ïÙÉÀ…ñ2Ø茥DØÀ0.L:#Oœwãbž1ê"˜\1j3^Ì$ÎχŸ8;ãöÃA\ô ,ÎÆsµE³ÐãÍr—]!lÚr(„µÀã’•éföϺ¢óÁ‰ï…Y/¥JŒ ƪJ²€)IG„¦„D¥¹œÊwÀÁ0eZ¢UNÜOÄ'œ¨3óFXZÊòñR—æ¼Q§s‡H?¨€nÆM„¢ªbóÕÅTµ¥DÑÈ‚\v†ypbáA@>yÞwaöAãG{æ‚s&Èyñ&WY³#&x«æ‡AIuVŽcz@q1½®eú©Úº”\ðaÚ|€Œ2Ê™ãoÖA 9€‹ˆ E9g›œó͹‚‘(ëÄ<á0®L¼NJy nˆ…ƒ3Û@s0¹)g·£íÌbœ~ÊéÃÎO{Ý! +$ðõœ^Àx{Ê…<5ášuF†û[G÷ 2>ëF\>ÜbhÞ"|æ>üü´?„ +k€¡ú‚eP‚‡Áç®PF D !fQ>3~OíCG;ýäOÏÎ:¢³Ndâ‚ïÌY×¹ ߌ$¯€ÅLVJn±Ò¥‹G·£Œ9ãBÝ~¨Š'ÌùÂl”ˆ!aj>¢$ZZª€%ˆÔÝaöÂôµ§S.ÂKà~ž<3 2ÁÛ1ïŦœÈÙ©Î0"lÂ4o0†36­5ž üÅÙù?{væ© 7ÈNË¢´¬hÖÎÉ-Ñ*Mûpg˜ *Â¥Q©aÒÓ8$sïàö+ßø›yDøOL<3áô†¨ŸàÕ”’ÈѼ¶{|kiçº fÎÏ…æÔœœó“0¥âœJ‹ñx¢`—G”T@h3BSgÕ0BàÇJ)?B{è/ˆ!„¤Úu„uºÝTŒRIFÏ…±¹ ì c´”c´ŽRaþì\<²üþt”³üÀ´FU?$B¿ …'B²ñ¼Ußðšâ??ã„`˜ ¢^·Çüª—2ÜBË^ÖŠ5;™UêÕÄÑfûæåõÍn¹l– + =aó*ðììSg¦§gÝ@D %XÔ“‘Q7[¬X¢Æð*/:ÅÆ S(•aÇzx{ïÁ ÇwnlÜ}îb®œ=?ã>?ëDH OÇK©°ˆÊÂíR|´i–[Cšyk1÷ðÎ.8¾ûîÍßþÍ{?ûùçï}ð`u¥dXtŒg69Þ<5(:ž´jÅì´2­F6.³vÂ$Îá x}~IàË•Bc¡±¼¾vïµw²­žç|ÐÄ € W1ÆìÂ\îìtàé§ÏDÈ2Œb±Î$B 4FŠ+åÂ¥ËWjÝždš£í53›ñ#¸+ŒQ² + 9ÙDh‚Â12Û²s¹’WÓŸVÑíQù`g©SÏTÒòf?÷üÍí—Ÿ?x÷µkî_ºyu¯ß¯[ 3‚üÄíÃ\šŒ<Š l ÍÛl³¤Õ‹Ú eŽRw÷«oÜYûøñ¥o¾vé¿üò×?ýÎãûG»ÛÃZ­ÄsB(‚ùC@îÈ(m´¡…2šXNiYíVÌå^v¡™[_îܸ²{r4º}}뵧/½ðÜñÁq­R3&ËR€bÊãPSäÙ þiúÌ™ùgÎÌMN{¼^ÈïñsžÔåz5¯Éœ@ã…\*ŸÏš†m™™0L=uv$¬Ë|,qgÉ€öÆ —Z7Úw/>x|õ“÷n¿÷h÷ƒûK¿ÿþƒüõÇÿúÓ—Þztek­oÛ6‚ÓóNÛ1‹6uSh7\.£'+æ•ÝÊ¥í28®vîžt=·üÚÝá×~ÿ÷þÝo¾ùÝ÷¯<·WHÅQ§Û5ã ¡¤ˆ‘<hØ_ב½6w¼$?¼TúðÅÍ·Ÿ¾qgá×VñåýüÍ7ñÝ;¿þî­üÙ›ÿë¿ÿæ¯?{øÚÝ“ÝV*eG`&¦`%M©œá—›úÞbb!-–©z†¶íQ/½ºº²Ó|çåÃßýæ_ÿòWßÿðƒ—ÜÙ½¸½$zÖépù£>„?3íyâ™ó>ǜƄ-JJ°F»4ÆÛÌKƒ†RcwÑzü`ÿõ—.?¼{ð·oþð«^yùN¡dõ‡‹Õ£þîCƨ½.)R5ðýýþqûîAí`A~éJëç_½ö³¯Þx÷…Œþî‡oüó¿ýâ¿ûäÇߺöÇ_¾ñãÏn®¤åX(àõÏ;‚p B@ö òÔ틹¿|ÿòϾ÷èó÷¯ýÞàÓW×þæ[×þðã7~òé­|òÇ_¿ù?|þý{Í»‡•ía)i©©TJTSR<‹Ñ©'¢+eîÆFöõ[Ë/מT¿üðúo~ôîÿá¯÷‹oþú«‡ÿãþÿÏ?üá—Ÿ<½ñŸ~õÎo?¿õùƒÖ‡Ï•÷{RÖ`c35íyú©© Ó©Ç|Ý$tÔ®¯ÙOëïÞ[ýéwüÓ¯Þý÷?yýïøêßÿ赟}rãgß8þë·ÖÞ½Y=ÆIÒQŽÄ8 Åãf©Ùhºùn–d°Ó>ûÞÝ…/¿~ôÃo^ýÁ7N~ùåí¿ÿÉëÿö§oþê‹»¿úæÑ¿ýþíÿôóW~÷ý;Ÿ¿ºôö•ìƒ£Z1­ùC@bŒKp‚RÎ%‹lØøá ~{Ëx¸g¿qµòùÛ¿û뇿úâ¹_|vóþ‡/ÿçüëï¾¹÷Ýw®ÝnǨ ‹ïÅ 6\·°å"uÜW_:ªöÚÎWï^þÑG×þù×ýïÿú‹øÑëóáéû÷·6‡%C‰áA9^ÍJZVQl•¥S"¼X/-žÛ©<8Èÿ­Ý¿ûÞ‹øñG¿²óñý¥ï½¹ý“o^ûôý»—š{£j¥Tõ-gE^l\QÌL>‘H +xË&÷úöábòêJòë7Û?øàô—_<üêý«ß}óàÃÛ¯]^xtÔ¸µUXíÄËY€”'DRBQ51’KJR¡22¾T’îîÕ>{´ñÃŽ~óå¿ýþKøÁëÿôËþÙí_|ëô?{íŸñæo¾}ôÅKµ‡‡ù~Q@!¿ÓËz%™éñ‚)Ç°rœ¨ªð¢º1”V_k~ëåÍÿòo>úão?øý_þÇ_¾ýÏ¿ÿä§ß¹÷ÉãÝw¬{IcŒ“ñ|±-°¬!’k­d;‰ RÐ^“¹»•{íjÿáqçöÅòû·¿úüîO?½÷ùk»ßxaôøjÿd)³X”5¡HÁ@2S!)ȯ¾†I¬·ìÝ¥ÒÅ^îÊZåþQýõkƒ7n­¾~sõ kvâû]k±rC’ `þ˜˜$ù¯ä1‘Og²†œQ™•Zòp±x©Ÿx|µóÙ«ë¿þìÊ?ýüñ¿yãÛW¾õÂàÑnò…UãáÅâJI”1?GÁ=ˆ²æ=h"°°_%‚E[­ˆöÞ¶ýÊQñã–þðÃGÿëùO¿zû·_½òíG§Ç£j»˜ÐëFä B‡~$ä—(,©²E“igÄ•šq4*œ®Ÿßo|óÅ‹Ÿ<>~ãÎÊÍzÈ;=®‡ßaÌB¾±­§{,«qQNêûk‹—V[iòÒ@ñ¸ûñëÏݺùƒn|ïý/m}ûþÚ»×;——M C!?ÉÈ^XBb ˆ4~ +Š¹Dª–¶-:Ð0á¶v²hÝXM~ñúîþÛoýÇß~ü³Ïï}ùεWnŒ®\¬-µ“Š&³jV¶;“@=ˆ0 «U µ„ÄdTªhq9VˆóݼÕNký4{¼”½µÛ?\*Õ :¯‰¦(rƒ 1‡Ÿžt’g¦à‰9à™’DÃBA-MÍ&íB2Y²Í¤L[<•’Ù‚)¨dDF#/”J]àÐÃQ.Ø Ê' +=³À`:½X0„³´˜M•Û‹í¥²!-—â×Ö+ëeñdXØ[È®•ÕõŠ¶Tˆ/Ì‚Êy¦'ó?,ãB9€ÆÏLúÿâé™ÉiwØ”(Ê–¸²)¦¸›fG%ñòÐ~|cé­çV_8èíö*fŒ"RŠäÂjnpe’ÀžàAq†fATFE¥\*æRzÚÓ&¯I1IbyY‰Ò‚Ÿ¦|Ñ ä‹Ê”’ƒ9kÊOx â,qü.¯ÊVàP@çbE`Å2YU6hŠ‹Q¬/û‚„/L¹CÔ´›taA*/nè•MTÈ°Z£5eD9i&<¯7›+Ýîz½¾¤Ç-`½@Êñ¢'„==9? ýT**7bZWQJ›˜tLN»¢ø˜¥(IUÑ“†V΋­ôÑje³›8\.^Z팹v6ÞÌXyËðùÃOOÌÍÆkÛϹà'.x&ÜÌ!^ÍU7Ò¹.MQ +‡æu&-¢)Þjç^?|ñöåK[£a½jÊI`CùD€0ý„1æ{!Û»’mŸ&³Cš‰ë—.¿#Ìü\'$MˤR•\¾a[y&&û½x("NÍBg.'„µœæËAܤČÇ  rÀ3n¹7Â…P˜F[Ê9œþ`x@ÚbÎŽñ(¢–˜8"⟟q:|„ÅQJ—Œ*£5Ò€DZ¥X ¹ýqÙNØUY4PˆŒ¢"‚˳Nä™ _7¢LfÆMü˯Mþù“Óf#‘¨(˦¢ endstream endobj 28 0 obj <>stream +&a˜„!”¥8M)(, ‘ŠŠ1J⊘v8)ÊZ]»s,¥BrP‰éºç Æä¬f¢vQ/¯Á¬ÆyNJT»+“>è™™À”ŸÁâ-.³Ì%—p¥óYJÊWI³1‘ƒìxQF˜I»`5ŒÅ£„:tƒL„(›TªQ¾àÇ ®{•üSÃíé ;fCB8–ä̺”lñF9žnB|€VKAÒ ÓI\­RZÖ«BªËÛMJNQZ)K¹QÍ‹ÅÝQ=KÉ„#"qaMF+RšÑ+Z¶¯¤Ú¢QŠb,A ‚jÓrfÂá›÷!T£õ®P¸d³Ó~òٙГSþ™b +´ÙuBü”ŸòŒ{ò£t˜ëpå95UìÂ1Õ‡—'c|>@$ü„åD¤§¦Æí¯¸Ó*Vå"*לˆá‚D¦Ê™š]štâÎ 3 J-ÑVËG$âEuJ­ƒ‘™ r“îÌíúŸÄŒvN_ðqN/Íð6#%åGåŠXXUk[©Î¾˜ùéÔ|D€cf±{€ó9pi15Ð +feG.n]+OÍcOÏ!.x¼hè_=5sn¦ŒŽZß7ê»|jÉ–Ÿ™xBüœûÚß™9$Äd˜Ä¢ZØáí%'¢Ï†å³|ÖK`ŒŒÊÏNƒ3wn20ï%ÄÎkeZHMÍúç]a(*D°ñû,GX˜ psÉCÚ.Ìüó'ç„ÂZDÊcRz¸y£¸xàSˆZ +óYR/Óf]LöJK'ó˜èD„¨”ã2½ ŸçúciÒZÈuO¶o½g6·ŸõÔŠ¥V¥ò™X‚„¢UçÆËÒ âOoÇü¤¤³”9àÒ#ÒhÓÒ¢Vܘî# D,³É¥0Œ™Í“”Ò v}›TKŒ^*%"ÑÓ[—”úžTZçR „Z,/ß$âM*Súx;-&ÕGã P ‹E`Ëâåþî½ ›<ïÁ•Ê^Ü qYH*ùS4jåÞaÌêŒ{‰ëí P‰HMB_r †ŸL¸¢êT€š ^DŒ©eÒÀ|ÅdÝ!–Ñj˜Zõ&¸ÿxm̬¶`µéAŽé¨\¹æC5\*¸£ã&Û!&U*a¡<QÏ8˜µæ‚ qâLj•Ê˜ '‹%\­qv&È{q+Ë…¹²?–A¤ñ*›3ôÜ|”5ǯ,gC´!¥Vª©ÂØ=R¯‡™d€²@jOE”yT÷s¡t‘J„â–Û|vŸôШX"Ô² È´M¹£6ç0öDã."Âç„Ô¢\ô`æ9Qµ‰ ¼ÑðÁ¹Yèk缓h ;ÌHÛ ©^LwÃR˜4fQ k pùTŸM©Ô™\Š¥g§£Q½ƒjõx~iëäQº{ÑIƃ|Úæ7·²cÜÎ=ÑÙ¦8”óƒyLš³sQ=ª4Ó ×ÒÃçÌÎqdÜ6VÅã &=b2«¹îgó`ü9GÅ56=p¢q¢Æ¬¹rÈç·q}Ü‚ÌOÙà£ã£ "ãñŸõ»y`ÕwÔâª^Xî?Àͯ™½ÓêÖýöÞ‹õíûV÷Rvp•Ñ›f¦ßÛ½ë$â6B‹É¬Åë‡éÁõTÿz¼¾`ÆkN)³©ˆXñ‰Û=Âh6‡W–zýêÀ ¹¸™è\ׯ j×˺AÖÐ)DȹÁ&‚ÜLXöFµeC|ÁK&…ÌP.¬YÓ3ýlÕ{`0  6¹‘7–tÃJup‰OöÀó^ðÓA>É5Xi‰%T­óBë¥çƒ©€4){ËM&|” Ò-fôÀD'›‡vcBˆO¡Z…Ï-ù9×Ê„Õ¡Aš-­8Ê.°Éö,"V+j¤¢#–õ + ?WŽÅëRnèÀâ`x)»ÏçV3‹7»û‹«Ï{™ à;3XX¿î¤s'ðd„¯ÒÉ‘Þ:Ö»'T¢Í›e­<Šê5Êjƒ%ô&•èª•-¹0‚”¼]]yÜ»ôpÜ6ª¾KgWÃB ä{¡½³°{ïB&â5»})¹p#»ü\bá”L-ùù¢œµ¶_H4÷¦Öz;/ÔÖo•F7‡˜ì(Èeá?íZ‹È `t iÍÕõSKÏ+õÃ0“ Ò  Û¸Õ<ã¥çB""TX{•M¯Ë¥‹zý¢ŸR)³®Tvø•^É,Ý(¯??îê“^Î.\‰%@`˜D¼B&¸Ì’_#ìåyt¼ØMJ.´ÖnÀ|ÆÉ ®¸dßhSÉe,9ŒÈ OÖvåôâdHðÓ97f‡Ø—Û’*¤=ri=?Ü8~Ñ…êg\äy?„ Z?è^zך{×^}÷ÓÙµ³^vÑ k!9z®¼órvå›]™G4£º Ôi: œ™ÇlÆKƒø±šÇR~k. ¹!”7"N¸£@»ÎxÙɈ‚=6¿mv®|m"xn.âÃ47f¸£–Oø©´1ÝdšË¬jÕ¹°0=î¾¥éTˆË+•Ý“°X*aF/–¦{תÛ÷£jQÉõA¥Kuö@PÅ+kL²³¤ÕÈ N¤Âˆ0ÑxCÌoØÝ«vïª[…äÊLTö³I.3€¤<¢T «OØ£ˆÒ^^ÏŽ€n‹Ù% c1{ÕZ±Ô+¬ÐV£»~uxô"›^U œË%²‹WÊrÅ 9Ó댎^úú—ÙÁž3fEã«}šÞ.¯ÞµÛ»Aƺ ˜y/m†•‚3fãV§²ú|g÷Qjá”°º¤ÞÌ ®¢z3Èç©Ô0Ù¿*·ÈÄ@)mŠùågœÐlW +=­4à2} ‰u:1þ¾ýÒÞýáÁƒ³ mC<®¶ŠƒÛÍ‹øò¬µ1©xýÅO“õmWTǵFkëùk¯|ríÕÏ:G¯Ð™e_,ÍYßüBHõÏû8_,Ǥ×ãÍË…•{ýý×S½+ˆ˜« ÕÕ‰å$LDïɵãÖÅWË£›ÕÑ ÊhGø¢ÕØu¡¦ 5(«K/cæ¨_¢u\[¹ ü kvõú!e/’‰.eu ¾L(•¥½»÷Þò¤3r~C.¬±™%DkAb%@gqµ™\??ãbæ#—Y”ËëFc7Ù½4ˆ×˘ùåêÊé{Æ#`Ör‰Ä€ÕMn„…Ú9«§Žo¾!õñÄÌyGà™˜ßÎ¼jÍ|X:¸õŽ”Yt š3`½ï£’>*j“T¼˜]¼³rùƒþá«œÙØÜ»]èïñ2g·äür¦»Ÿœä‡W´Æ6¯¤›à:`B1£RAPXAšð¹•Dç‘Ka!ŠÙ¸1­¶C%sžs.K[mÒ¬bvK­2¹M©´¥–Ö|¤î'Ôd}ƒI41%'–2£kzcÛ¨mYÍ=«ºí§, ‡Rq]Ïò@,­©µ}:·È!Ñ>â³Ks˜íî–¯3™e>³ +PÒ>ëŠ9`ƒÐz¸Öã³[Àœ‚¢Œv˜Ëbi7Š…qyHðPVD,0À¬•Ö„ÌW}¸JÊy€RraÙ¨oQf 88`õ"•^|vB+Û¿œé_¥a1?˜\È—Ži£†‰ÙÂðr}ëÿîî˽Ws£[x¢å§­ÒòHHc1vÓætÌÊšUßÄõj„±”TKË6’õe­¼²שxÞ*/ ©î?‰)pŠ—6<´N§¤â†RÚÒJkraà#5V¯ìÜøúâébyS)oš@Uì6­UÖ®óùÔàz¢wC­ 7é숢™°`f6N^´OÎAa®0öéc©¼)åWâùa±µýè­Oo¿òÍy<á¦R°Òâò[Õ­‡‹×>,l<vuãàîk~?ÕÜžE¨µ›Ëˆ9Ì,=×¾ô¶Ù>ò⺜[³‹êî›°æò„ÖÔ:>î?¯—–[£+bz€iuBoy¨Ô,¢‚ÊÈ3Ý9f™AT­T/7×n«¥MH(¡b1YÛÐò‹°åÖµÖ%½}¨Tw@ õ ºë•uà1'½ä\˜Gä*™^Uj'Ju‘Ëa&•ëïvvnjrÆCÍÁ2¬ÔôÆ¥xq]ËX£9`"”¢G¡XfÚ/€(:çffBj0–1+ûéö52ÞJTÖ-D¨éKH¿àœ°A=¥¸¢s˜I³òÎ…jÀȃ‘Çâ-Òø¢›²Ã¬Mé•ù¨„Ç+z}[)¯K¹5Àcöàl4¨"—VØÜÒ<ª†`—¨VºÇÚ9¿¬z€ˆg»™…#­²Î¤Q³áãÓA!¯•7A•0é0›&fÌhÅQ¢¶&æg`1ʧä\/Èh„”d¬&ð2‰ö!XT­Ár `$e-àFkfI½ ÖÖ²ýãúƽâÊíYXpC§—KÓ±oÅÇ="0¥‘éäÙþ!`$L,ÒVhõ9²@)¬ôwî³CP¿ÀÏ{I!5€øÔ¤ŸðàŸîÚ•åBk}uÿŽQXt 6µÂg6X{ Ø`Âr]Jk£«ÝÍ[^2á¥ÒBi×X¸%U˜Ô›E¤*0ìý­;©ú°ÿn2Ëvó¨sñ¥òòµL{Ϫ¬ñò›ŸU§^¦ Wwôθ­z¢w¬–6B±Tkpøá_þæøÁG@±ˆB&åÒn¢sª×. +é>o6Wvž_?} Ëe“ ˆå¨Öf“CÜXˆˆµ Õ2 µÑ $ülÉGççàñŸ­0¥d7·š7”‰I:Þ2à)n4–¯Â|q&`žbãöÊM„+<;‹]ð°€ÖòýëFuX¿]P’ {óè…§'CÏÌ¡"êñ–QØuãÓlz¸$0ª~\gí6"•Q³«Q­¯ì+Å‹Ji^àÇÁ¼“V ‹7BB!È­t»ºq—N÷ÏùHD,%ÕŠVÝ€ÄB – 2ɨ”3;¸V2%d‡jí¢Ý»R\»·tüzfñr€IbjQÊ/ΆfiÀNÕ W„¤ +¯ùbºœjzuQLvÑç£qÀTÀY|…ä2øåK»w(£¨˜´º±ôD8*æÕ­öî}™€¸ ZT,¬×J#1¿äo½gÆŒº4æB,¦–äT§ÐÞâíÖ9w쬛ž‰(A6Çù¥t8¦±fͬ­ÕW¯VV¯kÕµq…’Jtba.")±i¹°ZÝjï<"SkN4yÁË©™¾œ:ñøyuÁCÍÃ:ÄWÙô*Wؘ…D/¦ÇA= +ÑI™Vת\L·NÌÚ°„2-—¶p½ dÄ…iàóH´óÃÑá‹…ÅK&ä ØÞ\˜©5o,åÁP勽C»¼´1f5ùüZ¢uXY»£Õö˜=ãçåD§48YŒð%Ü賌¡’_É-œ¨¹å aôF—+í?ur´ |VyõV}ãù0›êçŒ(p,QéÙ€èF´Ù°ä§S1³ÍY #Û…Ç«ÉؘÕ×j‡n$>ë¥&ç£Þ°ˆ1VT­Ãr-È"\^Î/1 0øx¥AfpØ1&µè€e,Û¹“—êÿŸÛKõ®:.]Åæ„©rMûÙ4–høŒÔY©¨•VÐÂbH_fá’X\årC.¿¤VVb龟IûÏf‡óDuÜèQ‰>¢4ýLÎIX C3­m­¼ê‰ÙP÷ÉT˜ÍŠ ÔÞÏŒ;Ƴ©Ayå† ÓÁåŽÍ}€¬ÝW « ž]˜A(¥X¼:nÇGÚ¤Ù!“ À$z)3ÄZ!Ú”ìŽlψ)læY'é£2 h…üÌeQ*Þêm7—pÁrV«a© ªUÌhãJu.ÌÅÔ‚š_ä’-) n»ç¡ sàÊ©ú.—îOb^L Qö|DE„J,±àK3!ô¹w8åg¦# ˆ\a¬nex»4¼C%WžqƦC"צüŒ#"͇ŠiºàŒ–ZX‹‘T +zaæ~RõzÍSfGÎ-³©E æT1Ë£DcÌ&aõ½3ÅD +pQÌìúq“ó¹þWp"`¡›÷û + +§•öÆY¤š×ò}»¹Y]¶[»3!¸EP Q¶€ó…g¦¢“‘+Ji#Ó;æìžQ'ݱ•T‹k¬½ðÔ…àÙYÈ Ià†ÁÁ¦†Vë2WÜçòÀ伸‚Š©¨˜ ±©± +)5?WtR~˜RuA¼V\Ñ*A1UJÛh¼á¡Só˜9d#´å%4PÝ’½ÓÌà*—^-dQ­‚)ùts;»pÉ3=”á¥MPß_^¿övqíN,;rE`BÕÚŽN)vãI ]bf8Ü»_Z<³ýòÌŠD8$ª6ÀïO´.eGw´`|2]_SmˆÏ™,ª¨Rc¬Eð°@¸Î91X(`ZƒSc÷\”= ~ÒLÖ¶8³õÌ´ïé)Ï”ŸÜKeü|ÑC§Á´B¤¡%»€Ð.øiÜèÆRC6· Î N +sÀö4ֵ‚ á=¸:‡Ø¢”ÛdS#0­äšJŸ;ë"ç"ŠŸ)¢ñžQÙK·ŽÍêö³óX˜LdZAlO˜¹°8+ J’Ö "×ÂbqÎOUzveÍj®(ˆ"k.,Oú\.Ééþ”?7ýŸþÏáñ#§]¸5oô£æ†ZnÙ…¨ÅáU@›Z}‡K¡qgNñ‚ ƒMJÔAÌ» âÆäÜêX0ÕfPÞn.™ù.BÇq­è"lDiH¹U\iDèìù9dÊM…åtûây75ãç&\Ô³3(]13ÒCo4>çÑãÕ^ù fÎxB+kŠ\ôàVNí©Ê8@FÄì>•\F¬aDm‡„¢ U¨„3,At&*×èôŠXÞ%˜4$€¢Í +.å¢BÒ›€ÀÅüjfá$»x Ñš³a`{óñÒj²½ÛٺݻxG)¯AJ%ÕÛ/­Ýà ++çü1PèÕÂ6©ZfD)Õ+.^ö`˜.½ož$Ú—n!j#À伄 ³)pE* Ò;ra£4¼‘ížÐfÇë^ +Ë08äPNàÁÙÌ*> >29=XÄö|D2£TÿJ²’^¼®Ô½lX1ÕÉ÷OøÌRÄ°ÙEÔf,1§w—#u3×Û:}9Âç/`Ã!©æ R~C.í:3á¡‚Tš1P¹ÊuÆŸ~ÎC==Áô¸m,å=˜îê"A(Õ0—þôB€sÃb}x/.Í#muŒúž”_¥­øñûÔdèì|” +gf"ÆV‹+ÙÁÕÍ;ŸÔ·ïs©ÁÙÙˆ/LçÚ…TD&ʹ°@ +H†LL¸‰qC¡0VAjA þ¢¤˜Œ!Ù¸œ… Î7ñç¼öŒkÚƒ¨$!åçBÔ¤ R‰¹0QƒP„Öð¡öùy¦L(¦O:¡óÓÐ…yæ@ 9ÂܸW†Þå3k¬:£ñ¨RÙ RQj„ÞQ +›je7–êÏBò3sQw4Œ%!>r´½ äFX¼bó~ÄéG‚¸Dj%à+íñ;šrË7=LnÂK„‘U3\<ÓKx¼ D®yéò[ÉáeÈlùè4­× ž˜‚jõt÷dýò;£“w3Kwg¼»MY›±‡ë]4¾`Ö/‰™5?e;C4«×³ÉÅñÎ#TjÂÃÎFdToÛ!;Š*Ådû¢˜º (›?–¡¬A¼~$—€IÐZ®³ý<¨ï~>Ѩ=ˆhM4Þ,.Úí›”ìV²sª6á`–å*¥7ìêF© b“ŸÕŠ>•üˆNôt”¶ó.„£Vg#@5@øà¢|z9*Tcz0À\D•³Ë¤V›ñ³^Ì ó%:5R*;f}?Âg€–†h;ß¿„i5• óåéH<‘’A„ lòÜl„”rjºËU); ñ¹É3éÆbR–×ësãEs*›XÀ•:Â}¸åŠçœÄ9Jë¨XžpÅüdÒOg!q¼ï˜ŸJŽ¿;‚‚UÕrCZÿ׸×>Vé‰p —k0_Ž{J« lù{ÊË‚Z0¨3 \£:¸(%&rfQÊ.;£æ¿|Ö@Ë…€€ŒMúÅù¨á@ @vm‹·:.pVH +I7 :Üžtçã}`AŽ€÷Dõ åk“¡ó.Ô¢ $Í^¼|q°÷ꡇlz4â€4Qñ*®”Q£µ„ ¼ÞI¦{¼ßÂ3F‰6«@µBZg"š8­ï÷áF¥­G„)qQ¦fdI;_..¿¦´¤œ~ŒU×ê_?û‹“—ã nKl¨†‹§þöäÉŸÒÖH‰P7[Ë—­õ«`z—^À¸ÍÙmÿøãMH»®„èÈîu¯gdxÃfo{ùúÏÂù]ÝP¾ØÜÑÎœñÖ­Í»ºëZ<8Ÿ|ƒx.sÑ^IÎÔtÆžAüZ9Òæ¬Akùtxñ=¢WoŸHÁº®t‹"às¥'u©gÄGáúMçü;Ê1j·Ê†EÒušGÃõ³"¡}•¥÷‹Šê¯Ië]ÆèØ¢eCï7RUOÖ£Ów«Ç?¬î~\?ùe²}JT¢µ;¼Øã6½üñìí?lžÿÕøö× p‚èµdÁùCoro¿µ¦oÂÕwó«?ÛÜþ:ž?­«MÉ9ƒS)š‰þ,Á{Ó`|}ýþïŸüô¯QSJœGÊ‘Õ^¡’šý ýJLN†¿\Üÿ…Ö¹àíáæò]wuGZÆ1hhCÞƒƒ>_ý<¹þ3%>ÅœÓ:øíÔ=G«i]Ú±î¬wôîäÍß°°ÎÌîœÈñëEÛä‚Ö:²Ó?¨\”80Ö\½Z?þåõ·ÿ 'YÚ/ñè%™ÝRŒv×Þ´~¸þðÏ.ÞÿƒÖ=+ó6HrvþnvþV°û,}zíÊÉÉr| ª9$ôtÿ”áe‘rr„…Ò#‡¯z§?k­cÎêi‹Vc§½&”€5Þþô­ö¹Õ¹Ô[;ÞAdìîcð+© ôZT•”‚:ë;»»=¬ªv¼í­_ O>´6¯9{ÊjmÉìx•ßYðflÅËÉîõ`÷rpòµ5¸á£ÝAݮЖœ^R5*R4MVÁè6œ>œYžtD3^ÍÏÞ(Á8Û0•õŠ2ІOZ>¹ûu4¾"xw0»\Ý|·øeVÈ‘^QêòÍSkúºµ|ךûEÁï]<ûóƒ’òå!ƒÈñisýŠ5ÈbÁ)ÑÂGã3°³;sÑíSéGi Ûó'ýã·þøR‰æx@²Á®{U‡ëÔ`¾†Rsá .£Å gòéo´aC&TzxpâŽoãõ[jD/ £Ý?7ÛÇÝí sxUT:E>’ƒE²|â Î{›×ÓÛ_ký+¥sÆËŠÚ&1É‘ÐÜ”„¦Ý»vÇÏ´ö…lÀÆuµW`Á'#Ñ›€.*¬CPõa8~êôoÁ52·&wÔhÍ:³’Ô–Z'j÷ÊŸ>‡ƒ£ìIYŠ²¤Š2â%„W +çjïZïß ÿÉî]Ijî×´ªØÄrsîL —ÖàZí\  Ï/¿ï½wG—’3„S[[ø²ªÚϳ!¡vƒÑ ª³à-³ #í•ú@ï\³W%±ÝP»HFɛحU…1ÓO«¶0ó3«wC„¤(sMÎÃqÔÕv‘u¥hçOžŽ.ÿÌ@hLM«³þÕ“_Ž7O8½Ý›¤Ö¬hõq«Šj„§/ÿñ_þon²¨ðQEˆ‘ŒfÿžwÓ"Øàg5§×V{["JíU¸˜ˆÒ¢•ìŒjŒEé½+£wÃÇÒ.‘fµx&P”KlÔ\¤Wdµwïñ³<—|•“Álfs oª|˜Ìî‚é3­uNj©íB6iáÔîµ—Oôhž~ø‰Ý%åÔ§ˆÎÐïŸ7g÷õó`|åôŽþ†±»éöy¬¯E Ý?Uã¹MÍÎÖ“;Á]JþÒî_6W/Ž^ýÕÉ»¿·ïH£‹`Yk˜š2{J{kHSú‰^›çþè²5¿-ÀÈ ñŽ Ö*,Õæoñ +5‹ÂµŽõE3%®É˜3|¯Æ›LY(ºÕÞÔ¤DoñÞ¦,v«rOðWVÿšv– 1kx§v¯þmÕ/Óf´ÒzþÜ>®Jé~•¥hµÕNõ.W3.z $©®]%éQ–Vœ¡™î¦7¨­†Ô!¤¤ÁG(œ9 µ^¶áÍíâò[Þפ&~<$sµyŒ¯YÂë/ÿù?ü¯ÙÝ£¢’©b¸™4¤€ÖÛÀc”]É ªÑ1Ä•âOÒÏéMVþàÔî šÓ«dõÒߥ‡\´Ì;CJkj5B +f "ÊèË>Xî¦ò~|ñ¡òÆœ¥ÂÍërŒØ&äD6€XxwtƒðŒtÚP;¼7]ýiëè‡æöû`™îÎ … P´-PÄE{Ü^-Þ9ƒ;gx§wÎÓŒ­*¬’xÝ£†œøûhöZk_á•¡¢áüUWÍE<¾´°îRé\SîVjž6ç/âÙsRj©vÿâÙz²2©Ý©“î\Ín`3)£“§-§{š|<&‰`‚e5õAñºýŠàŠàX +·SÒeé¨Ì'R°mNŸÙ½ó<ÐHl™g"FàyE óL(5ú'߶7¯$oúæçzýîŸHñ ç­¥Öqĸ®vžUbØô§O´vú!–œÚE©E ËTM3>bõ>BH°†j¸‹ÇwñèV‹Öõ,a|ÜÚ><¬ +ùšV Îß ‡0$QéT´õåÔÓLÝÎ.JXCˆmÍÁ‰®‘ã$Fj§Hù‡U_ u`'g~ÿ‚7™šZ¤"ãƒÊ´æ†³& ©K«Áòv{ý-¾@Çzç1 4G ½OÙÓš1اœ,e×Ħ™œ$ËgNÿLp‡ êêÖxWö(^ýÓï[›¯•øX‹wr¸*‹ñ£ŠrHX„ÚC`Á‚Ðâ$쬚ËÍžìp«j,å”ùÂÀI‰žàBŽ¶Vûé¯àPÔx¯^³{%ÞqÞ2ˆ,ƒPÀåi‡"³µµ»§6Îì+^úiœÝÕ3P4’¢Ìø .Q(%¸ªxcÄ;ÔDüVk®áÚ0«½ÓÃå[„esöf!=ÜÝ\À¿W¥&ëNZ«×áâ•oáÄëZ÷€2ZOŽv0Úu1BÕ•a]·ÏK|+K@Âöò±-Q@+|ËÜ…‹·þôE4G­<Í3Ô •®*·²¬«%Vï:Z¿œÝ¦C@ž´Þi O~üí¿ºxñ3댹p+wÎ¥ä„óÓk¢é=©tœ¬žÙ˜n‰® øèØ›¾n®¾YÁâäúGVë"a+s©SÕFÉî›íÓ߸ý³ƒ'™}ÞèWø~ñ–§¼ƒºC[Ë`þ +ÊŸ«Û¬Ž½À¬ÊÞ«&µá‚£ùóÁÉ·e&¨ -ÜxsêõoÌ´Òn•øÐî2Fï9®Ä„¬5¶;g^ÿ܈·”Ü;¬:ª©Å'”5Ò:WR|Š›W´B%=ï¬Þ9J¿Ñ: &Ï…`CÁ©ñÚ\N¿WoXgE³º6a¬E‘‹kP­Ë:#)\ÁÿBð¡Ex}Ð5¸ Q×Ú¼Ai`¬ waí +—nÊêmÁÝ?{<>ÿ¾sô5؆wFj87¢Ùh÷lqõ-*;®¡nïTõ§¬‘Jíx©B1‚¾ 9«Ç9¨È§Vï”iGçñøÚIv²?3lÁF²ÀÕK0OCméñyLnµt/³³ÖÚøãkÚÁÛ)À­^Yƒ;5¹@ ”.ì!„‘W'çFï1’‘÷gÎèª$'t@BãÙS½u„:U m`<œþâæçåí¯XwJêÎú’‚6: ‚"T„1³¾ÿùôýß¹“[1XŽÎ~%æ ¦¶«râ–%=5X£óÊG_™nÕ<†;@]ȳA–0Y½'{ó\MÝÁrÁVnógR´­e:’ O,ÑžÙ¹R“k)8âݵݽÖÂE¶¦)wöJDR˜lßÇ›÷7ßÿó÷õ¿÷¶öJ!Ä„ØÊÖM”ã<‘^f/‡›áñ7ãóïŒöq‘ ¶7?¹ý+RøVQ(åöâÕoÿìŸþ ¸*{FYS):Ž—oÍÎ% +ÇÇÝ:†¼;cí©™€¦##=¦a´àñ±XHíŠÜCeq†ÏÝáÓŠÔW×å˜ +\äô¯Q^Õæ +œ'Œ-;»×f¯?o¨CZïKîHr/Þìèí7½N¯þ¿†êícÉkNߊÆj4eœôÜMw÷¦µxÆJIìÖÖˆuÉÍVéorÓÜL.TâM]IJŒ¯ù3+š7x¿Î¢;qW­íû`ù’uÇ¥t“帔Ð9^³þœãLC*ýÉ-´«®µÍþY{÷¶¹|)5Otp£fçÞ‚ +çØÝóîÑwJëHŒ–Fÿ"'&_JjÙɶÆy%R‡ÇûBùûÇßDS8e[°{—ÍÚ= ¥ÍÛhíôdÜ雿¼üæï“4êŽOžüÚé¥öÐ?ÆB‹þFöVJ°¬K’ÅL¶NïWb{½xöÛw¿û7?ýí¿yõãß×Ô #æ/EetÎ(sföWvïÒ螉Ѫ"¶ +\«ŒbjÍäæ’‚8©ªmÖŸ‘>i¤‡ˆÓ8´ÆJ¸–áw¾‚3ŽG×óóªҕÃeYjDo­Dgj|â!†æHr§P ØæMÅèhxþÃàâ{,1Ò5:„Ò$´ôl¥÷H}(x0 ·v÷õw¿®æUöƼ= Ì¾na!›‹7½“Ÿ0¢eg+ŠÖÍQºÓD8[[µ{a nõÎY]KŠ¬etÝÉãÉõŸô/”“SÖƒ²ð+!˜ðþ…Œ¶ÇöðñðòW£Ë_A.25¥H»¼3aÌtûBpã,´Þ-ï-au¡ö +|‘Ђ_¦-6=·¸‘¢-ãLÁrîà”¶{F²î}-ß„éÆ—ÇJ<‡¦ì¡Ó;鬞ÖäÀLŸÚÃK°¥ JtpØЪ¼êOˆÍ‡‡4¥¶íΉÑ:ièãæ?GZ(è{UdBé}pˆíŒÎ¥Ÿ`]ò”k¤²sPU+éfÊ*(ýã6gŠ¿õw5¥köÎá5*;Ìé^Ã<$#ÖÝ­msrÙœ\ä˜ÆŠ&–5GyY*àƒ…Ñ;5{—áä ´õ,Ì^„ãÇN{§Úà +ã’Ö ± ¢.‰­àV•ÛN÷,Y¿ií¾6§OÙð¨‹A§×4ä.€Á>Ñz7jû ºŒh¤­1r¤"Æ?qt¢¢¶ºS¬f]‚>ÄXt$\¶è/€Ê$"SJÉvröãŇÿaòôw¼·Æ‹zwÆ2 œ¢Û½ôZ߯á5 ú•j¨·àÌo.¸«÷Ä¿ç¯9oQ`2|pô¡½~oönоғs#^R6Œÿàø]÷ä{ÆßÔ´>,ªñÙ¼ñ‡ŒÞ✡Ý;;zúç—ïþNïVõNEΑѽ¨* +¬¢å»xû­=|ÒÐú5¹ÄØŒ9€È¤Wð¤­Ò¾¢#ÀžÝÚ”WtÆ0ûëÇßwϾ‘Û×´…~†”ÖÖã5¥· v&tû“«Ÿ¦7?O¯fÜ9@\TbÒp:¬ë%é[½Æì—Y÷°ª ÍYÃGE!Û°P)Ìöyÿô›šÞ; Ì,Œ¡Ü¢´¸’^Òãå)w¯¬“jOH иNÊ]Ñ™«qzåCz‘Oêêê×\¾ÂH÷k†à-:›wÉögxKª ø6ChÆRÜiƒ å…šØî¬_^¾ÿmÿô¡$¥§ÎQƒÑc”f0¼Ù¿éŸý°¸ý ÊžsÞV€Tz«ŠÒÉ1^Žvg2ܽ[ßýfûâo{?—Äv–„p µêF9S6ܘƒ{Di0Ó$Cúd)aÉ9sw𨆥,r!ªOžq+¿×«ë¥¦JþÜ\¶æÏPP} ßÝݼwûHÕ1©WÁüNHŽÅöI´~³~õ÷ñöC øAZÒ ”ØIN£És¹ydô¯1,®Ó=¶óËf5D¯âås{p‰žþšsWNïR‰V cHÚ3½}1ºüypþ'fÿ–ó–pÇr0µ»»xv58{g¯z§ßÎï~5¿ÿÖ½†ä"SÚËý“ï„øŒvÖj|¤ÀyH“N‰5i«5ºüåéû´§Ï iµ²'z|¤ÅK%žFË[kôDŠÎy/=BBƒ<å`, ±)¹C»`s†ùz|¢GGe6ÊÔõšÐdôaU@íë!1Q2Ìî¹Ö<iï•å¯)#LòZŸÐFï²³}3:ýÖl—Y“ƒxrÙšÞ?È ØÛ2 ÇÝ +î¸íÍ’¦î ÿÞè>Ýv9¹$õi…ï8¸ìŠï•ä†ÜFxPöå¯*õò¤wPV8µ£‡S)˜1Þ\éߎosôòo:›÷VrŠTrz§‹Û?AÝ„` ¨’Ý[güµ $ňÁžt÷ƒÍóÁÑ;Ìm‘kã•+bÅë€4ÊJÌø+wö2Ù½wçOàhŒ×@ùÆ÷0uxA ‚ܾäü¾Ï2^‰H£—þýÂây°z­^É1¼<ÄóŒU0Úçf÷ +ÞÜHvF UuVSc½{:8ûfûâ/οûçæø>džÀ‰2c;ñB ¦ 9.ó>j?}N_8½ë\C+zzxPN` ÔÖš0zÐ4oüÄ×OÓëÜ™’œÁ¾¹ÃÆ[Âmé½Çf÷B j43ZËpqß?þöoNÞþus÷žv×um¨Dàœ Á›ˆá‘Õ¿KwöœÜ±áª¢¶mÖèúƒ‹(½^÷Žk1¤ø$˜<±ûW¨§jk5»þ&=N"uq+K¹y +sJ#Jíäê +c$Éüq4»wÇOXgQ—ûUÁ9öXå›t+{Œ[ƒ!Ú’Ú8KBÓ\ÀnRœn÷c cDþÑû`rc[C¤íäø×9ýƒGuL”7ºŠ6oíÉK5>®#2KJ™‹Fë˜PÚ ¥guo¬Îµäoý~zþ2R¤=Ö‚: u1¡½5Š £õ­ÇÃ;éadÔµþÙ÷áèF 7’»àìakv³ºþ@J1µªvH{*¶N*Ú Ï5kÚ ¡ Ôp Èa­¶RÐ¥^0沦 ðÊbý9nhwZ·ú|¼kí¾]¼øï“óŸÄh‹„‚¤Æ0š<ýl_òáqMUÒkWÆŒÕO·˜o žbÁrZk‡|„W* l¾®[ëô¯A€U9™]~;:môÖ\8ç|èØkxÏÙ}%˜–Å Ä»Vzy@T`‚UùV…‹83=}Àc£5[?ù¥3¹òþ +J]­J‰=¸P““ºÖCO/ýÞøózû¬¡µa³cu¶nm$ ÀžÒ>_Üýe¸|‡Þòά&F÷”fu½K˜}~wòò·›'Öœ\éÁD´ÀûáÅ÷Bó UW‡°9F÷j|ñCsqWáƒÑÚÁ‰l[§UmˆdÉQ–ÌÂÑatÕä\Š/ÌÞãÕãßBLDÉÛÓ"í+À`P“3Oÿ|ÌœÈѹ_Z½Ƀ‚Pd\µ¹½çÙ&ë¯;§ß4O~èžü,Ù«h{eåæKŠÖ sL‡[§Þè)Â^öWσš†Bƒª +ëWGŒ ŸxR±R{´Ò­ é‡0æHô×E:Ì^MB„L´h“þí†7i(íýª +™…ÞZíS%Z³Ö°.¶:ó§Œ;ËŠaÅè•Ò?ëžðщEÓç€=9˜kˆ„h]:‡dX 0é…7áò9,@žrk§&gÃÓô£ÈÃí»†1)ðqCïé 8wŠê½'æð¥Þ½ÁëÐFWö¡¥Ëhöb÷â¯[»wZvó¬… Xò¨,ÆÀ•xýapö³;½%Ó†ðÀ'øª¹®½cÕÔ®=Jÿ¨Jð'KÒAMG²}”žj´FUc(DwúDˆw„˘“ªØ® +ΙrîO£]sõŽ ¶Ú+#©Çƒù+øAoxaw¡'m'YÇ“59Ò:§E±¡à¼Z`yUŽ +¼NoÖ÷?ž¿ý]sõ¬( ˆº”¸³ÎêyzÓšãûxñ2½F}x›¥¬/3õ**Í-²¦nŽôÁÓæö‡þeº—®êr²WQêRA¨«I­Ãº¹_Ñà¸aXÌÎiz¹ Úë®_~¼¤Á.ðm”‰}ÊÇÒ „¼áÛ;Ç[+ñ¶nôëæ<'ös\GN{'ß'˧¤Ú&•$œßïžýææÃ?moÞb™2 ;õ3Fvï +¢‘g#µuòäç;}òWRrVÏ5­yÜÛ¼GÎþw¿¨|•X{±|ó›—å®ÐOT̃Ò\•ùfYh„RöÓó_ýÏáüÖ=¯€«ÍQMÖ”ñwBxŠÜ¡Íi0ºRãyUpJXq¾Í »s;½üµwV¢hr}òâw´;ãÃ.*ÆEØfô®¤h•î +môÅæñàì×ã«ß†‹×Œ5±zÏNvÑøÚœ•”þ!ס¬¥;xÑÞühžd)¯,ÄéE}­ÞÓÁ² Å.ÝYMnÔµL]£õ~8{ÕÚ~Ð:%1Ú¯„ÚÅí nÖ”žÒ9oí¾Ù½ü‡éýoÍñ“²‚ t$oYd¢LÍ.¤«(kêMž_÷/ÆW¿®ê©µioÞÄë÷Vÿ†²†9>Ìñ‘ÖÜÃRKÅ“¶¦b°n-_Å«·¤½JØu \0Θ´†„9†eཅäÍS+Äø¤>°Ò¾H?ü?.^,µXw,Å[gü´}ü]sõº&wëvŽ°«|³D;u1 Ä0[Ukc ÝÞ•Ý¿†ûË‘b£9¾Ìgë–ÖºX>ÿ+oþÂh_ËáQ:'ÝãÑæ1©ø_X.<ÖúO£õwãËß½kŒúQIjŽ®.ÞüNŽæyÆÿ²(çØÈì?-ûé¾Þéá`tš6Àìå¬åèt|þËí󿦌ñƒC¾Ì„'1Þ!ì÷ŠjQHêúPŠ1{/$Q“ÎÑ7×ßþ‹éÍŸ{³—y®ûýWÒvÿwàÿ«öy ŸZûäSkŸò©µÏùÔÚç|jíó@>µöy ŸZûäSkŸò©µÏùÔÚç|jíó@>µöy ŸZûäSkŸò©µÏùÔÚç|jíó@>µöy ŸZûäSkŸò©µÏùÔÚç|jíó@>µöy ŸZûäSkŸò©µÏùÔÚç|jíó@>µöy ŸZûäSkŸò©µÏùÔÚç|jíó@>µöy ŸZû„_ÞÙ««“³Aèdf/–¸³ñ1@~ÿ?}œ\žÿýÔó™u†¡hŒDHŸ~ù_ô„´?ÿþIÿþÿR†Eiñcw¹LúDê‹BácÓpV2VŽþbÖüoX{\“[ ­-xS1˜‹î”µ&¤1åƒóÞ‚1†¿ßÅÜñNŠV„ѯ*IUN÷aìgI%Qü‰ÞZ»ƒs£w&·NÄø˜ wjûª¦´ó¤–oÈuÞã¡šlÍþ•Ö¿–Ûç\sÓ°%±éôŽ´dM›C=9w·Áô¹íhs&¶NwÑP{¡É83!ÜHÑÆß’î,C9éÔÖ/s!¥õä8œ>Ñ{çBs]QZ”5 ÌŒŠRÀùoz,ß4¾×G÷”;«êÝŠ’øã»Ö|áMŸ6Wï´Þ5®ÊJ"KÖ6´n +”MÈ­'¢7â½Iž¶ñ^Œ5-ËíŠÜ©i=Ö›£cœ;•ÃçÎh{Þš¿öÇÏ(gY’Û96(paMIìî™ÞÚUÄ°Âj¸Ñ[Ç‚»¨ +I‰ ¥­…«öâ9Þ‘Òº²¿*ÒþAU« +­ºÜiȺÒÃqþ†õVY&(ñqCéQúÖ‡’¿ÆK5ä.kͪR;ϸ˜±†Ú¯Éíš”hÑÖˆ)cØPúy®ù¨ªíUµ,iH›VÛ¼90‡B³Ä¸¤gëêߥ¬²áéU©ÃZsÆj¿Ä†XÞ_Ô•v¶®Uù¨@ûÙ†E(=Þ]±ÖBô6JóXmn'Go¢éU…wI}¨Æ'öðJk•ù°Lá-ºŒ…éÚQμ$¶K\$z ·i·Oh¥ãõ/ÄpQÓ:%1"õ¾Üç̤áO¥ðÈîÝzïAEüE‘Cd*ÁÚJÎÌø”P;Êr‘òkBúáì +ìaU¦åDpW¢¿fõÁAUE7Hµ›#­ãbªR7KE!A”ÖDh‘F¿Ì85!:Ä$Ô Jíû£§áä…?z.‡Ûx|ÿá×ÿúèÙ/ÿà ¦wÎÓ #Þ‰ñ®ÄG“¨W“ÚÎO?æ›o"Ìî©‘qæ@2Ç'÷¿6{'E1ÊPoMšËwÁâ g£]¤œ[=2öŒ0ÇucÂ…GVï&š?×â#BŽ­d¯ž›Ãs.œñáë+·NÝÑíêæ'gpI=Ñ9Ã;kü”êJ…)¢ô^MŽ3„ž§Äıç„>åÝ mNy'ý8øï3FOðÇu½CÚcwüd|ý³Ç¿\~Û°»¼76{§úà\\pÍ#wúrxúc²z5?ÿ6^>­¤»õ¬Þ%´Kí\ÐÞš‹Î˜ôeGJ¸I·ƒ%íåhŸ¶ÒýNÊl°W–ó¤CCDr^h椤fÎH{ø;sôŒò¦YÚÆ<ëÝë’g)ïQÍ,ñM¬lUër 2…¯ ¥]ã›…†•­¹†Y¨ -+>R‚U¦¦#¨pÿ~YÉTµƒšZbÚžií+©y!Ggv÷Îj_WŶàÌÒíñô!¡8wÃ8k½sÍ_3ÎôAYÈ4tÈ *çÌ qt» +ÅÛòüƒF +ŒH9Åtßî~]é2æ#EÕ«#Â^ÒÎVŠ.¼é{@=ÝØû^ë^åÙðt³¤ÏÙ3&ý¼ûVCmSú:C¦se*j… ÊŒ_ ¬ÃºA(]Öœ”Øè°açH¯Ä„‘!GÚ5¥+…Û¢Ø*°Í"Û,q¨‰˜„ ‘n—µ@Ø”¤~KYS!<ªCÒÓÀ“‚"ã °«R‚x¨‹-VRjo¯¬(/ß°Ć4À4"a!}U±+9ó°ávŽÒYÒ-0QžöÊ|“¶JòØ?µz1Õ½\Ã8¬k{En¿,ó樿|¡·ÏIgQHÇ¥UEú,ú+J­sû sJ™c=>VüiM€µw.ŧVÿ±7¸’Ã¥uœädxú¡«R\W:¼?O÷{ø+$—Ù¹ˆæOk°OZu/¾V£#²Ü;‡&“»?ûÝÿtöîwu»W7:|s«´lOâÅ«öæ¥{Q ÅjR‡¶r|iôŸý§zçVm_ wwbvÏKrź,u)sb´/´Î¥œœG“'çÏ;<~W”"λ£ûdûurômrôÁ\†ƒëÿô_Ú³‹CÎG©¥ì%å,Ôæ±Ù:‘ƒUJ)l€åæÓ}F©‹[U€¼t)d«9~Pæò†Ói€µÌYž«bPÇVëT×ëZ–ðjòHInÍþ½Ñ½•›'¤9A‚(°4yÂ8¬)9B/r>gM%w)¹ JéWø¸ÌE¨,Òª +MÔ¾,áÔ­¨¡U—Úe¡yHû5åËCò ¦åI¯Ì¢Œ¶®éÆÛ¬½×вŒƒî!hõäR ¶¬=Ç´#£A˘üŠCgö+J¶neëv•KXu\¦@>)wªòWöQIAÙJ÷: VÓ+ ÀžÔ.Ñ>*iŒ·r|ŠP¼5k€±»éaž­ñxVÿ²ök¯áˆîrrü]Ž„föÁ Ðve͵亹xÏß®§×ž\E³{>X±îœ´{c»} æL¶ïÊB¢7WJ¸¨Ê ^Aù‡'ˆdû!œ?HׄØî_`•ˆ’=³7ÍåóÉù·_ÿêœßý@Ø)ÞªÝKµ{Ë ·Žþ5lÀo¢ª˜ ¦X½+Зk ütS±«ÍåO³ëŸKr¯"÷Yw©6wzr¢ ’Óp|7ؼ1;§u½Ç{sÞK7%õ§/ZÛ¯õÞ…?yü槭Ÿî“6ˆC6%§ v(*" +PSzø>G9¤œ¤›ÐÄÇÐyjcÍ9w°ÁBäH3GE:6Þ²ÙZ¼N÷ªo˜ˆBƒ¼ÓXP©½ê6çu¹Ð0ö*b¶¡i»*Dœ5ÖÞJv—ZxT—{Шºªl6œ"À )9ÂÉ‘N]J E.óî^a†§³ÖXtE6*Pn*D8è•O]9Â.’.ªä£’ :¨B…ðÊéWô¡Â7?¼5Cy²º—„ÖÞ3ö”u0·sJï)±ŠN2ú€P’ý†déÇ}ÙÛYÂH-!DÒc-Ôø¢µþ¶wú“Õ»–Ü©îOVçß Oߣü ÁJ Wzû(]uÖ/õî)ðÃlíô8ÝÙ.?ñFÏ½Ñ ³wo Ÿ²þ‚¦8³ÎæEQ A㜳èl?Œ/þdöøO')÷Ža6Gg߻ӧemа&¬·ô&O£å[˜559G—´Ö.Ý·RjCúxwîï£ù«ÉùOòŠ6¥Ý Šp§R°’"á¥Ñ9gݪ'&VŽ6À~Úû“'“«ŸéktÓÚ¼$ÌaŽ hk‚ðƒɲnYŒA r°M7ÃæãLÝ”íI8º¡íq†0P›{’‚]ƒïb&q'øyÀ:+ñ#˜ð9•ØÚ«Â)‰-¼~¬‰½º4ÊQé®H ŸLMÆ~°Ö¼55›Ç­és%<. í"jŸØaÌ)cL@­„Øä6 ÍúGúÅ[”™€ÒE:*3Q…CY„5†I•ÒÇtÅ`ƒÇ´#ŒäTòS +­Š1gOä sØPº9Â*Ò^Cêæ¢:¨Ë5l(aCê@Íö«Šo‘ñpË6 ¥ÞˆT@£öµ`“¥Ý½Š¼WJJUˆQŽÕhƒ²›!-LZC‰÷ËÜíרtï« rAt¡Ÿ[5>V[Ç´3F‰g´ž®‡»·öðUÏè\pášr e³½}t»•+˜<¥’W¹Ë†>.ÂKáÆÜhã<ï1öPFçTŽ·rrLùK™z“'³›_¹Ó{Ø+.Ú±þJï^Ø£'zÿ±Ú¾‚ÝÆã+r‚èB„Ô”¶šœYÃgJç–Ùð¼çBBÈÍ-ãŒa–ÁºRRŸØ½{Æ•9_·œ5¢Üå-àyáXíîusö¢¢$e©Y×:u­‹Öî€p)JõWÁ઎4,KX¼>çÍaºK¤_"]BJ°:û%¡Äxœ½Ô;wj|%yΘåÓ,V1{UåaYüªÈ•9x„YMB]˜C[²¤?‹H€5@``Εp 3Râ“2Ã8HþF@'µ®îÍRüUžÎÔT„©BW„ +Nè’rO×UÎÿò8¬©yŠÒóîÀ€‚k´ÎŒä¼*µPCÁÌé¸Æ¡X¢=¼Di¿,>,ò¹ÃºU»„ÜOu’€N¶ +”µ_Ua1Ä­+ÆpǽÙýÕ“?ß«©tHeª*«LÐr¼ ”0í aUøp¯ÌþÑ~ñ°ÄAÖ`~‹éVšvCkÃjNrjÄÇvrª'GU%M-9¥ýYŽ1 ½Å;-Zh­­?}ªwÏØ(.5cžoÎ[ùã§Áä^in |¥-Òì“Ö ¬´àààÀE­íw¶}Ö0ûÞè±;y’F‘Ú‘šÇáòëöéOêðqº®œm ìaAŠ”äÜ¿ÔG/UDŽ·¬*²Ø‚ZÚÛ<À¶Ó-ß`—4XÈI–´i}h´Ï¡N™†&7wþâ•9¼CvаKâ?Ò“c­{Q”“½†è…ú!û0W¼3£ÍÖÄ‹?‚‹ÏSXDØíºÐdµò+ŽÕD”øSZb‘ÚCJ×¥>4m¯¢¦à-µªbó÷;íeÁ; Ú¯êYÂüXé†ex%¥“:,w†Z\¤]ÿa6¤f…÷³uEu‡fk‹J§ƒåïWM€£ KŒû‹Ã%'‚5®ÀÚsÍCå”Y_òÖf÷¶$4VÄíΚ“nqªtöJr¦fhÿ³õ½ +Ÿ§l^ZëIû $!)ò¤K§j3t:çÞƒºþ°$~™gRý›Œ5T¢Õaz¬¯%y£"m앨½Bc¿HWX_ö–tCŽj´á¶Véžµ<Ô5LyÏæi‹P"Á8õﺇo´øˆÎÕÛÚÕä ’eÜÒâÕ{­}ÎcorS’CLx‰K7Ä-q>cöýñ­3¸6’]°|ÂE+Ö[ˆÍ£†1)+Ýoý¾µûš´Æ‚7ÁÙ‰ÉYÍ–”ª¤?y,_þœ&wÏŠRœc å²a9‚Ñ@ 䯷¶%1Î4œ<íÃÕ¥Ž?xÜZ¼Âe;ëçjowȇŒ›ã¼eF'ß.ÅÅÇ°Wþø†4{eàa"°”¬5mÁåEÛƒºR¢MZëìU„},V]CâCå ¼e)¥®&¨G51©‰¨kQŽðs$°v&:³ë↉:¨éÅ2@ +¥î©-Ç€„’ÕÔ¶‹˜:A€eã+«w+¬ÇëxxZ‘B¬EUHRYCaÚU®… ÌT$Áb³@Ú”2¨‰†Ü¥õ>|òšÌÚý"ëyr”…øÏ“>ª&$àÿÆZ Œ†ˆÞšðwrìÞÇ-œQ¡QVrúæ—ÿª¡wäè\]%ïs”‹R˜ç"ØÃÞòYoõx¯@”ê¼ E‚Þ!¨"¢¥YcVïv¯(}ÀèCFë×,G+Kê¤Y­ È3S3‹TØÿÝí’?O¿Z£Éù7B0{XV ó !µ„`ÝÞ¼ðØ­“‹·ÿä€Ö ´M©] +–n¿DhíãþüþìÍ_Êí£CÊ-ñ< gÏÝáýüöwíãï('¥&grWÕ»e9ø ÑÖìß´¶ßx‹ÍÁå³çN¯¿,òE:ñÂÇ!öïo Pý£÷U} HÀ̭݃ö¹Õ½@%ŒÑâú6|™«cŒ¤5‚„Šáb~õ'ÓÛ_Éí§µ[ßþPÜ_䈇–5‡è ê‹×½BÁBš°&Ì`ò @T¥i4>úbD;e"¢ÏÐÖÇÃb}Ø%x·to­2o äáÁé=Œ!'¨ÑÊI Yúíþæ%k•EÔ>¤¥ŽN`*à8Š7:z4Ã7ûXʺž«xåáæ(œ¶»ÿUŽµsU‰•Z¹†¶W’P=Ózʵ }Mïíîq¦Ä1¢'èÍ:ç>*‰9Òåì±Ñ:‚m—>îû›ZcÊa‚Áù¼=R£%$ý°n!ŒÑÅÄ¢nrÆ0Ü€êyo +‘'•VCn"gËb“2p¾Vr„à)ˆ>g¢b®yg +ÏÂÚcÊè“zg°¸»ûîÌþE6_íÁÈÍc»¯_ ÍTsÒ’[ë ïòî8žÝ#ØFº}£m§ïþÛÿe|ön¿¦‰6X7=„~H9‚jõŸkù¦á­RߌOQг` ­‹÷¥ƒœÁUI ö*Tˆ÷ÀºaM‰hgH9#ÚLWÏ~ý7ÿ¶ªD + ¥¶‚Ñcò´»_3>)¹ƒtß\ÔBiÕÅT;‡àÛ´ÞÅ~çìù/ÿYUk?ªª„Ú…±%•©öQþJl Îoßþõäü×9æažÎÖ$}CÚÖM8AB"> £®G¹&œÚaUB$bb6¼î™L3”Š |X–ó¨ B¬:SÅ™šÑ`ônÔ?CõÜ8eéeB Ë&«hr 0FšçzžÐ‹¤“o˜ð‰p‹¨hf4³â´(iYÜ+ðÙºUĸÀø9Ê~PbEkbÇ'Ðê?|TÞ+ÐeÚaÍ%ï, ÒxL¦"W(§ëZ*2!ƒ;cÛ!ô1RòAI<¨£þºŒÞ§àÄÕ$1SK8‹ºL˜”–Pj[ –­éSµ{N;“†Úi¤`D¤ÞEPIÁRoK!ÌÚš4îàÊŸÝqî„s&b°`l»©×?ãÃÅ!f©Ê ;j‹¢™clÖIÁó¬uÏÖˆõf9>D¨›#»ÕÞ½‹v_ã;Ò›È]’šqÆÉ2ëÍꬻ’â Â^ºÃ§îà1àªÈ¹u¥UÌ[‰Ë<Ü_˜gGþ«"µ_áiôst-žE“‹d~'-5@ÖpÊLŽ41fr!GÇB°©Êm8qØŸžB•8£m8~ïÞ²)q!¡ð²™2Ÿ©ˆ™ª„…Pšk³.†›Š”Ô„ HB”ý¢ˆ…@^—ÅNUî Þξ¤ìi¦®—¥ + TâÐí¤"uùà8X¼`ÑJ§@ZûEÔe`ý +í1j·9¼*ÐX‹¸—•šÅÔÖEˆí²Ôf¬1²‰V»²D¶¢’B«Æ§Û<kj®õ«HÒšÜà<=€¤À6*7`—žøA÷H$”<,0™ªrPMsB*w´pW¤ýBöüÍ£""°‘)+é» -ÆG%)Û0Ó#«‘ßz‚½«T¸¸@ºù:B«[PëáŽã<¬(jMrª„«švmkñ +Z]ׇu}J9ë`ò¦}ôƒ¿|M™CÞ™@:*R„F~d€nM jg ‹QavVe1”ü©,”æq0{錞ö䀴YÉùKè!òZ½rWßãgøUMéQÖ”v1ê~ž·<¢»ìl¾5z7”9«ÀDÓ¦}Û/‰é!nu$ÇbpD~<@„Á~ôà.eöùpNX}:Õ®9©!†U>MFRØtË‚c(EÇj状çõôL1ÜÖ€÷ÒCy\o/¥æ)cÎXs‚|A=-SF®ziì#º¤<‰™aYzŠ–¶3éšj‡Uõ ¬d §,uHsι›š2ª*#ÖYËþ¦&†¨#˜+ 8l/ì6cÏkr‡³¦Vû² Ô¯*”Ø"¹ Ô0+´/¹BJ¿*Ä™4`,HmŒis&øk§siÅÇù†Qc¢BÃúãƒÆ<,=È3Ð(TçyÅnÿã)KRî6ì*¼a=(ˆð&yÊÏSa™ÂÁ….‘}Œ”@QO`Âê†)1¨ÈóÖàÙÂð¨ dkúWþ³ ¬Ì†ÚÃäfzÌAct%Âeæ ·LGœ5)2ê, +Œ°ä®î© ÐÙųDŽÑI¼ã‰Í {ø"^~к×Xzdz8~,¡K T×Ý™éÉ…è­ùY–íykú¤xf m«Û?ûÉŸ½$¬ ÜŸ9x,ŧ&(ó‘;gŒž“î‹ÅØ3spçL_ Y³¯&[&=¿v^S»yÆo¨=8D9Ü¢aÞÊéß@WîUUlJ.Btº_“@±Kê#6"§‘²I)ë߯éyÆCŸáñUo¥xFŸ•¹NJí|Cƒ5ËgN‰Ôc¶J\+C…¡×™¾P½a®!‘Jœ£í"cÖ„ý2‹t¦¤X³V¸x˜gò~Ê) ) @uP–ªBBvµTB…ŸˆÆ\´— Á˜—iÿˆ‡l¦š’ùW‡l¡îRqª ¾zu©+G§ueP`›5¥_â[Y*< ¼ PÜÎÊœF„bš­)°–ò\¡¦V¹(=èG:%Ú§Ò+¼ƒê»ÏªZí`öëü#¦å€Àl‡è6íCÜ¥©ÉÔåý’œ­[i†#æ|!¬ ï©tUäð ã€4‹| +Tðq€UÚš á6ÇÂ,ˆ„5Ä1»¶ÜÇñð§áè±-¿ªÉ©­Æ;5ZCÿKB é\Ú5© §¤@0»ÐŠ"úDnžËîîƒ={ša€¦¡Ãk?*K_•ø‡U¹Èð¿0ÚE¶Ié$à8¡ÌåI´FCr7»ünpòv•BHÈp£ÁGwd{"ê}Ùêár“@¹U¹SæÓóˆ=ÞÍ.0*GÚŒ5!´."6j¯„`— T˜1;9Šf÷™†Y‡fÒM€_¦nçA›\k‘Z3Â>¬_f¨_dÈlU« ƒÑ¹ó†Ø¬0®âÍêòÕ­Éi°^‰rÊ”[if‰wq_î×ò wV™WJ°‚‘,’®ìLµ`ú±:§æo±WJ—/½¨¦ª£‚À±_yxH!D­Íi†Ü«ÁHÊ£u²W×VµU•ôPÌÇÓ=çzû¬®PJ`+ÿ¸WF©mðaõ*Zž°óVÙ|P`‘}´9 ´¬.²ÐØЀj˜¬²ÜLiÄ™Öäv:íR*YoÐ]*n BZüxtšÄÓ~– Pú¹PWQ¦aÀé{ý‹hþÔ=n裲ÂŒÃ~–9»&‡X TU=8 +‡÷œ7ÏñA†°ãé3ÑG^{Hö|zd €3ÒbŒì o+rÛÈz+ ;eŽQva$ó\Pä"üx LÈã¡Ô” 葉ä`+‡;˜¬c‰¶™HÒèsÞÂ^™ÝsÆ\ˆÁ*÷Už9¨J¤œH0§P¡ØW•¸H™„܃y—ý¦…2¦’¿­YjI8 +¨@ÈÕÍlÝÌ“>Rf¼3{<9ÿæ+Àm–êTuÔ©,䨰*!bãýŠ–)«û%%SQó k¯¤<*Êù†Sã"5X"bÜôj:ëaž=¬‚‘Ƙê GZ{¹@šùšP˜%AqðòˆÕ*ÛL&Ï8«óGû…/÷Ê%t¯ªfkf¶f!󔸂Gûr¿œ)‹°…ôºJkCh»Õ ãÊZ–N¯­Êôûe¥”ÎìÞ(=!EP?Ní×YÿA–I/-Cx qz}—ØÕÒÚñÞØ@ê}ÆYÈ­c9Þ5³ñÆèŸkÝÓ<à†%aí)néÙ(:HsÏRð:qIj–AòZ¿ÕZǼ›ž¾Tü¹•lÌÎNo3îRô—¢7×ã-e´áýÑ­¬`Óxg–eýCÌÜ[^þ$‡‹†‘Z;C;iÖÐaYìrÎ\7À6€=ã. +|ëQÝÞ¯Û y/bO¡Þ4ØÞ™ËñåŒQäÿo’ÞÃËëNü#Þ¾±-Š©Ù¹‘s¨B¡ªPU@% +…PÈ9§Î97»É&Ù$Å$Š¤$*‡Q…±’eKËÙ’­l[–å8ž™÷vfÏÙ³ûƒöœ>M]¸áû}uï…²‰Î2ãq®ê<1¦ pxì(¯\Ø” [®@ÕL¤— p! é±q[?€÷ŒÛ9]ÿk¼´ÆE$AY é½ÇÆ!']@ýˆÀê}bïS,„q£aÆ#(—s󅯸!òÓŽŽ8Ç,ܸ-4j i=ý¥&ÃvÔ †}ã„õ²¯®€ Bª²¡jôNÑIÄF žãc®q øÎa'øáq›llòÈ£Žÿñõÿpçè‘a»¾/Ê<̾ٛ1ƒ¯›8‹…jd´ç ·«62eîS–2êuX ˜jÀDZhˆ‡:|ÂLM¸‚£v8¼ß)_®i£R&oôzÔÎŒXH>×¢!è8`ÀæK’rÝ+UÀZHÕƤ&<áv~ÐT0¹J=9 +!¤Á— ›¨¤‰N#Á²7\·0)³ˆW6â! "êªEí_-È·ö'¨H%:N±„GZ´Úu ˆ˜±ûã)/„ò2*#BÎ+dÔæ&" ^ÊÍå …˜ ls‰y›ÓÓ‘Ú* MãQ¾hpåON2©W±*&cÂDƒGÔ ’3## B©{BUWéwG,Ûü)+“ÚtK <>CªÓtbÖ«´=\Nmî’ñ©!§`¥ÓT|†Rg½‘n™)U…Æ]<”Ä8³û¢|zZ*.±©i>= œ<ê–`œl: f‡ [ü}GgôM¸%ðÀ6z4d!bN&KEºˆX²RTR©Ûý "8Øœ‹ËAáxä: —™c“D´adGŒÔ¸KrÐðú¢GnÀO „Wú"­X}‹ÍκB:1Å$¡;Sh¨Æegµ èr0Q!Ó •µ­qKFo´ÿ 0ƒ°Œ yƒ'x Å1`ŠU_€w÷ʵþö2øÔ ’y¥šR^“ËkÖ@¨ªú€³3&,ä•ÊBiAª¬&º'«kWÄþ˲v6cö¥†¬ @)G ç“ëÑÒŸ[DCuøLÚà¬TÜ‘•*H3ñŸaÔŽ7˜³ûA)œ\ Ø^Û_¢–Àà _¼å<ôïnË&â[•JîPÍnúâÝ`qQ®®`¡&e˜DÓ§¶HµÅ¤a+þTÏXò.£÷†­Œê`û–ŒŠwÅÌB~ê\~æ,Ÿ™Æ•†S(:¹,®JË-Ré•Põ”Wi° ‹¢弡2È1©NÙ„²•+q…5"\Ç‚ Sè5Ÿò(m$ÔD‚uNíä§ÏbJcÀ應ÕóÅ&‘`µ?Úl®¿hÙŸnœ@ø+iöŠx¨à‹”©®X\b’S¡`òA¥TF!c Ù—l¡‚¿šp‰v&Ý·‘å+²êS} 9,®ò…E>7.'›‚ +2úâ66…kX¸.–]0YžPyØJPð–ý%@פÒ¦å2³Jm«²|·TYCB%ÚÜ•+kñÉÓêÔi±´H(5¬–ÜÊMïEëf_ÜÈ RÅ%<᮹ٴ”œ4bŠÞöËh°âÊþät¬¹ª®Ñ±v´ºÄÄc.PU—k|fV,¯Š•5›Ó ²/\×ò€12k‘šXXJÏ”ׯsÅ•`n¶¹rnŒ»"X¸‡Gz„“¸¨vÏÐÉ9;¦õgZ<&æØÌ´OíùÓsj÷t¨¶ ب­DeѶ¼¿H4zãn®âSg¹â†Ò8å 7¥ìœ…R!ÐY)(‚Ï­Åš§›÷Íz¼±x¾¹zN´½áJ°´(–—¨dW*-äfÎWÖ®Ó©I £š}‡?î‹6Èh;Rߊ·vü©ÉÖÆ=ñÎŽ…O[ü R©úÕV°¼âË,ú «žx‹4áŸzL²àð·@,áÖ)¹³jŸ’[§ØÌ|nò›jãá—Ÿ÷©Sd|’NL‡Š‹‰Þ^°´ ð¢u DŠ“Ê àtb2TÙŒ4víp*!ôggL*ð¹ùp}C®m¨Í“ù©³v& W@¥*ð-n2ê4ÀC©nG›§øܪ#4ÓÀ òþÄ$_Xð©]<Ò cmàT®˜iD†¬ñ®PX†V»§wí±ïÈÕUç“íHm:’ìÎÍ/­]Kuw‹gº{·P!“›Ü®­_•;Db*5sPÛ¾­ôÎSÑŠmyÃy#!£¡*™˜b ËüB¤¹mŸd’­dw5X]""u*ÞvðZm’]¢—Ÿ½*mXèÌð™I4˜Å¤"ð0Ð&t™Ï/Ç›§Ôöž_­²j —ËL²'•—ÅÂb´¶žîlÎìÞh¬\¶ôˆy˜¸Tw¯8wììF;RyÃ+7Ë“gÊ õX +ˆ·÷"õÍ`yµ¸p©¾tõ䕧ÒÝ]ó©“tj`µ@&üÙmµ{Á+ÍF˜Pœ”Ÿ¡m¡0Ç$»|f*^ßÀÃuP1¨|07k¬‡Š ¤\t2qPÃÔäY_´ÌÏ…JKh°ÌÏ+õµ@vÆÆ—©DWm¯Ë¥y˜‘k+t¼.å§2S{0ølºo®»÷©0ãPœ=W]¿žœ9×7…ÜœGªƒ¿U+3'ç ?.$Зsååó™ù åµ{ ¬1ñÈ°Çõ(„&½”[¸;3sI,o‰µm±¸Ì¦zÑÒB¢¾¨q³ãnÂB““3Ígç=r 3©ÆZ0;cðÆðX-®Ñ™¾òr™éa§ÔFY}ŠÍ'0ê$Ð&´-ÖÙ´Oƒ\â=œ@ª®TçBåE!? ¬Rín<òÂ;0>zoK´J ÷À£¸p€ÇæÓõ•›·Ÿ¿ûÙïÙýjaîluýFvá2Ôoqéjiù­NnŸ½}å±×˜TsÀâÃ$¨ˆ9¥¾YX8ß;yo²·W›Ù{ðé7"ÍuÀ@²µ¥ÔÖ"ÍíÎæõ»žš>û,Ÿ]­t¶óçP1ï•ûZL«SRi-3s¡uòÁésO%'ÏN­žoÌí!Bf-ž"£M_¬%åç:;·Û§ó‹,¸¹â$½¡’G*È}i™³Ë©ÉÓXÁƒIœ¾"c-*ÞÌ´w.ÜûBqæ”M%§/Å:gåêFqù +T +žXgèò§…ä,Tº ÉŽTœ¯¯^jo]ÏÍ rÃÅæ³³7^Bùd0ÓÉNžÊLóFÛÐxa)X\ö'{\j2èL¸ü˜‹H=TZ¢¨-ݵ~éÉìäéé•óSë±`þ\©­æ‹ –:¸ÿ¥¸0¹¸×íç6à%,t¸k¹z:1u99u!oO¯œÙØ¿.ªe›/„+u<>M§„Üreõ>6»hÄ:ÞÊ«€B®‚íñgaÜäâÂéÏLï\ÕzDàO;“ 7v¥Ú–/> ^V×NÝwé¾üJÃ#¤êªPY—j;ùÅ«`‡t@æ>PsÁˆ°¾p5œŠ7¶Ò³”ö6¿êð%Š=J)hÔPi1ÑÜÊt6Óí•ÒÌ.¸ýA+NʹXm;V?)—×AÅÈXM­-¯} ³sÝB*ÑúH€P\JÍœ5¶éÔ mnîß»w÷¾hù¸™$­@zšRÛ|a6=½®®´æ÷_þöOsS{¤”o¯\š?÷xyófsóÆìþCåÕ›„ÜÜ>}k÷®'AÝÀoÀ›Fë[áÊZ¬±™;¯öNãJmõÔöò9'— +WV"­m.·ÀçÔÖn{ûvfþj 9Së¯ËÑ"àÀà¡RÍ!T털N¸´$gZÉꢕŠJ…„ixÃ5!3—™>Îìeræ<¥‹MÑš](8¥aTA¤ºTXÓ ¢ Š©.­g§NçÏ +¥%‡P¶Óµ´zúús¾H1\š-/]Löö™Ì.—ˆpÙHÛ¹\¸¼Æ%§Lxe©Xƒª‰wüñf¸¸èSjéÝæü¾GÌ¥Ú›¥Å󅹃üì™Òì©l{‹Ö®ßÿÌ›ï~Ðݼ:îmtá b~Yªœ –·˜äŒƒRÏ^~ä‰ÞŽæ{®@JíìG;gÕÞAvþîüÒ " +Ofjóç•ÊʈÍ?fç !*M`òÕÜô™ƒ‡^ífÍxuIë\\“σ¯ˆV×gwnöÖ®x…\ª±.ÏÛÙ¤…J"|ÅŸ˜W6•òº —y¥*$ºV* +3Jª3˜ÒA¸k<܈CØh0×ËOïu·oÈu—9a¡Í„…” èHUÊ/‡jÛ‰ÞneùÒäéÛLz +áRñ~Ô-€öMî=T[»mnGkkÉÖ6£¶tÞà„›÷ÅÚV:f££|n`š[˜;“>ID+VŸÈªU©0«46½ýh{ÏëÚ©¸¨6€ @¦fãmO iñýJ-ÙÛµ·ýiP“ wn>)f')µ…JÜ*˜\±P‘q7oòHk{·èD÷Îq—xÄø'øœ@ÎL%@â…d3Q_™pbÕåæÆ­äÔ]ôJqæR|r5ÕÚÚæÕgœ|fø«ýMRß]‹N]ã²K#v¿ŽF+K¡`¦23ç + —ºë·:«×cµ©¸BŠÙ¥½›éöú˜+vXò¥Î3©y¥±Ë¤¦ÜllaëR4?m§TL‚Q]ð§æˆHÛŸœSjÛ‚Ú©ÍÈ*¿2i.6iÄe*TV«K©Æª3° @Q£'uÙ a¨´,ffBéi1Ö‚±Êww#Í"Ö¬jäuž0$<Ô¨-]%£ Gr E2ÚJ÷öS½SðÈtOCÊçf6Ï?äò…¡I¡ú“™Ãß8y»¶rI.,:èÄî¥Gj³»ãý@ñ'â§<Ž°„'ûW@Äu°¼¦4v¼JcÌÁ™pã3|¼:dÆ´®"ÝR 2…“…ŒVÇõ iÁ8´H5©6؈ED´%ØDÇ'eS¥¹öúe¡0‹óH°€ËU:Ö%#½@fÐÓàŒ”ÕöŽÚÞŽwwÝáªÁ§ÐJEÊM¼¼œnΟ~púô#±ö)Ri˜ š¡Ì ÅÁŸpOX4`-ªãK´Ùì´‰‰º¸D~æ̦›Ïss©ÞÙÌô~¬¾J)%W :æôÉ {à„Þ5d¢™î^¼µ—˜ºèÏ/2©žÞÃùÂåXuïÎØy˜#'dZIµÖ“í  ÷ˆœ:P7ã²[*Ù¥$t7—%ÃU*R×!B¼¶U[¹.VÀ®äº'ùÌœÖ#±¡âÊÞ A­Û)6· ×wÓ3—Jó×üÉY§P¶0 ŸTXß»)§:fLVJKµ…‹©ÖI©° ”>?˜ì{§¡"xµÓÞ¸•¹$¤çi¥EGÛˆP„ž†Á• Éa³×IÇøôT ÑËõNU—¯x„Ò8Xn>qõ¾ç᪕\l•ñøÞ¥ìÔAmñŠÖPÒÚò]H°¨CÄo#eCÿV`ÐêØ°°Þh-^‡0®GX&õW=c U¯Ü:a ÿñ¸Éè S‘îÑ Ç€wÀy‚÷@ùÁ¦§W.óÅé /kòÉf_T…©h—¤#Ã:ÄIF¨{|n‘‰CÀLY|1“sRQ¯t3áP¶KÅ[x´Éææ=Xµ-%Zs—ëKgˆ‡r3Å©ýÒÜ…peƒR'½JÝBÅ`<#…y˜,T,†+Û0#…™ƒ©ûº›7=ÁT"ß^Ü»'\š›@Äþ­CoÄÉæ„ô\0· иDw môŠãVÂÃ% ƒTV®æ&OC¼šÞ}¸gÂÃv&拶@ Àæáb Ätˆ2Ÿî¡BÎHÙ™”‹+°©Yµ¾]˜>À„‚ÎÍÙð0kj]~#ÊCÕSà*éø”‡Í›ÉÅÄaÊlý§ +T¬—îî×–®•¯FjnnÈ„éÔ„ƒ4zP@ˆ3¡Âb(Õ¥‚y:\Ðyþ”?Õ³SÞ`ÑI'L + dŒN¶ÒXÞ8xHïá êÇM‡Wbä*m™=¡î $^ýî{ÍéíA­ÓŒ°l®°È¥—ä†\Xí¡CH^¾Ä€¦2â!7—ôJ¹€Rš\>'×f&0ÚPüÉ¥Ö<2ÌH +ir±AµËÄúZf¡sÂY‰0ð!³'˜ƒA›Ýº­-ùÔºSÌZü1%“¡¬R]$ t0Z+Ïû”¢Þ+ž°n¡@/¬ +©iÊJ-\Ý*ÍËLžTÊsd¤¤÷ +í……ý{°`êNÛÉ•¼J—ŽõH©ÿñ¯ ‚e•‹k˜”ÒÚ½¤˜ˆ·6À„4W/çR-¼.ªµÍýë‹û·Ì„•Nb¡<.9±y4Â$²Ý'Cõ˜è4‘pËéÊÕ“ÑÆ)+“±t8 ö åZ¨VT@ÙŒ'Aý)“[µŽêV‡t¤Ž‹à.tˆˆKe¨GH©cv:Tœ•*ËþÌ´“MBh2¢"#—’-'Ô£C0Y*Ò +‚ÊT7øÄT Œ³û¢‘ââq‚p‰€ÚR[[Ð6»?¥q4v‚(Ay¦§ +s²½}¹¸âb Z4 ÈqÔ ;apûáÈk•Õk™©.5`>®ÇL„äRn!á³t¸!¥çäÌÉç*â $ŽiÝÇÆì“Ç`÷¹eÂB hÐ#îsë»×CÑò‰Q‡Ë…êCù¼‡+ôï|‘!#ã©dçPá2£V¯Î5æ“Ído-;³®4¦•úT¦·ÐÞ<3¹{¾¸¼K$@S\²Í€›wÜL|Ø‚ëÝŒ“ ’ÁT Va“52’óÅòábçä•Gšr¥çàc®`’OUS½å@yr…׋±\—‹W† n7£8™è¸Ë¯@Æ3s&†ãr¹Ç¤ªL²ä’TOt*›_–ÛsV>bc2R·Á ¹˜#£Æ ; ÖÄ.ÝÝÂ1;Áxx…VKNVÆå/ R’KUË3«µ]:]1ú‚t´éW{6R·ù™ŽŽX´vZÎÍŠi09I£G{&•W¨ä®YýIÐe+# ™*Š£”Æ˺ƒ©xsÓÁ¤FŒÄ±a³ÅÉx|Š‡ŽZ± Öés0,HBÖNOCRá°Òüî•Îæy&Y>fqºý¸ Ä¤Š³w 0³W²‘‘a£÷«L¬ú”¶˜gÕI<˜›ß½Ú]?«T§l~eЊéqŽŒW9?áñ»a¥²ariƒG²3¹tç\yî2k 6Ü~”UÅL¢W*¡’O.B]ǚ˱Ö2›¨HÙN(¿.®ðÉé!­çO|ã„Éͨ\¤®µwhØ4?)–•ÕHa ä†ôÞ —Å+zÄÌ7ƬwŒYŽhœn®(·¥Ì²ÇŸ°ú©p™Õx¡Ñ[Ú™Û8»}áÆ•Ÿº÷é—žzíÛoýð§~òû?ÿí?>ýâoßýɇ7ŸÊNnaR¡¿EÝès“‘Xª•Ê·2•©|{©:µ4½¶»yîʵ‡žºðÀS§ï{líÒÍ“×nÜÿÔ]7Ÿxåw^øö;Sëû³çãÕLLÚÈ¥éhábL(‘¬v›³+Ó«Ûëg/œ¹qÿÝ>uûùW÷î}|ëêCxöñ—¿õê÷Þýö»ï¿þƒ_<øü·zçˆPÊŒ Zéðòb´JÕÈ JÊSKÝõÓ•üä\mq½¹º7¿w×ýO¼ð³÷?úè7¿ÿæ÷~|êÚƒå© )ÕÔÙ5:–SÝDeÙA†GL^œ‹—¦Ö3ÝåhuF*v#µ™¹Ýkû÷þÝkßÿéÉ+·›ó§ÔÒ¬‡Kƒ]Ô»Ø ;âŸ6¸¹ c'"Rz*ÙÞå»R¾µ~pý¡~íþgÿåÂý^yè¹ó7;{ÏÃWyîµw~ñØ ¯Ýuëáµ ÷zÄ´ÎÉhí>7¦•*Ÿ˜ôKrºÃ*…P²™m.Íí\lLÏ­ïßµ}ñÆ™k÷¿ôÆwûåß~÷Ç¿}ñÇ¿~ðéç<ýÍxaòè„û¸Æk#T^‰ä×ÆÌ̈Þcr1ŸFüi“'dF­ LTI7·Îï^¾íüÍó·»ïÉWN]-ÍžMwvp!slùÚNÉåúÌìòÉíSÏ_ºöÈO¿þæwßÿà³O~ûû_~øɇÿæßÿþïŸ~þåßýѣϽ:»s—ÚÚ4`Š‘‚J…J,]ªu—·Nž»vöÊÍÛ=óúÛ?zý?{öï=öâkß~ç§?ÿèó—¿ýÃwþõgû·ÿxå{?ð¹7Wî3/Ï&7ãµY$Ë–{s §ÏŸ¿uû'Ÿ{á¹W^ûÝŸ~ø›?|ÿ½^ûן¿ó‹¾üÓßþïÿçÿý#€üó/ðó__{ôùüÔ&Ÿha:q6¨¤²åÖôÊ<·ö®Ýûȳ/½øÆ·Ÿüæ·}ù[/¼ùÝ¿÷á¯?úä‹/¾øßÿ×}ø›/{þµÝ»î&}|ÁC§…x/ZY³xe«;ÀIÉBuriëôÅ{nß|ìÙ›ÿôâ=O¼ðÊÛ?þÑ{ÿäWýé¯ýÓßþóWŸ~ñéïþðÍo¿;¿s7˜C\(²‘¶WÈs©.§Ö ÍùÉ¥íÕ½ó×îøþ'ÿùñ—¾õÒ›ïüøýOÞûäw?ÿè·¿þìó?þù/ÿù¿þ ªõG¿üì…×~¨gG­¾A½˜sÌJÁɨ´\ŒåÛs§®<ðØÕGžø§o¾ñã_ò‹óíýâÕï¿ûáo>ÿøw¿ÿùÿáþïÿþïOûùs¯½µ{ñf8×B˜Æê3{L ±$UžiL­¯¼xéæ£7}æ•·~ðÓ_òƒŸ½ÿæ»?ûÍþü×ÿÏûùGŸ~úoÿþï?ûðÓ»zªÐ[UWéHCÛ?ŸÍ3¤A1ZÁý!RJUçŠíÅRg~iïüÕÿçWÞüÅŸ|òù—ßúןþò£ßüçÿþ¯/ÿú÷çßøîÍGŸ:÷ƒl´Žñ%.Ô›³sKÛ'÷.\¾|íÆõyè­·ßúýïÿåŸÿüѧŸ½÷«÷^~ý•óW®N/¯Gò >Ù¶zÃF‹ú$œyQÎæJ«»Wîyðƃßzø‰§ÿùåýüýþìý×ßþÁ÷ø“/ÿüoЗ^ÿÎËo¼µõÎòÙl{… +f¤x5^êFrõpªÔ™^_ÙXY_»zùòw¾óöw¾÷ýïÿðÝ?ýì‹¿ü¦ã|úÙo~û·¿ÿÛg_|ñÖ~|íÁ;Ë;B²NJBLP\8]¨wfW·ö/mœ:·±sêÊõ¯½ùÖ¯>øðƒO÷îû¿õÿÿÑ¿ýüóO>ûô³Ï>þðã^yó»ç®ÞN7œ„ì$Â¥Ì̸é8F‡ƒr*W¨-¯­=ðà#oB;~ôó¿õÖOßûõ—þë_ÿþ¿úøÃ/¿üâ—|ðò«¯¾ðêë˧¯0Ѳݲ¢äŸhˆ‰j­·Øž^ÜØ;ûÈ“Ï<ÿò«/¼öæ¿|ë;?}ïWûÿõÛ/ÿò‹>þáøÇ/¿üôó/^yëÛ>“k®š0iÌLŽ ÝoFE ˆtu>Qh/oîÞ~â™g^yý¹7ÞþÉû~ù—¿~ù×ûÉû|øé§_þéOŸÿá‹_üêý?ùèïýòÁ'Ÿ:{åV8[Çù˜Æî3¡Œœã"ùP¬<µ°yý¾ÇÞxë_òÞïüägŸ}þù—þËÇ¿ýý¯>ùí_þúW¸Î»?ýñO~ñ³_¼ÿþCO>»yáVoç.ŽkP½ƒ3Ø(«›ÖÆ–Ûó;g/]¿ýè‹o¼ýò›o}ó[ßþÉÏù÷ÿøß|ñÇŸ¼÷ë_¾÷«>úè¹W^Ý¿xuvy—7µ NˆÊíl©žHçê“£–Z3“‹« —®¿õà»®]Ø?&‘O1bÀ…“&aA˜1:f@&L¨ õKÁx*Y,WZµV{i}sqc½7Û;¹¿yýæÅ{ï¿~påêÁÝ·¶Î_®LϱJ’ f¹D  vÒä íÚåñ‘·±s×Üú©h"Y̦ÖVÎì_¿÷ž»ýýïï½~óþGòÙÇϼüâî…ƒÚôt gÂYDW4`ÁX"^ß¾°sp#Ó˜N•ívga~æÔ©ígŸ}ü¾óáÇŸ~ö»ß¾ó£w¾õæ믾òò3O=|û¾»×6¶ Íás!ÊŸ0@®AFM˜Þê¥i!¡ÆV—–ï»~÷믾úú[o½þúK?ÿù»úÓ^ø—¾xþÔÆÚb©ÞP‹UF™Ün*«W°x) T¹Rsnnùü™Ó?úгÏ=ýo¿þ«Þÿü‹Ïÿü—?½÷Ë>ýôã—ÎfJy+â³zD0Ãc&âЀîø¨ýĸÓò’œÉéVofjaqóô™3/ÝuåÚµ»¯óå¾ÿÎ^}ýµ—^|á¹gÿéµW¿yÿÍ;[;±TÙéµ.ÒàfìxõG#Ù+gRK«;—¯ß~æ¹—}üŸ._½ñøãOýèÇ?ûÞ¾wÿÍËO>pï?´îìÒÊr¥ÞdÛùÉ]6Öüj9´tÙàd4FÔê c‰ÂÊÊæ•«×¾ùúà•žxúé‡}øÅ^x÷'ï=ûü¿\¹rÏüÒZªPñøxˆ3:;nÄÇõ£·¸H‡‡öËÙp¦Ylεf×] k@0³Ç§sù4væÑAJüбñQÓêf^Ñìb\xx·|,Œf99i¶#F‡ËäDQ_¡y¯”Ñ ü ÕX ;¶àÁa zxL?ntÛÑ€ÉNX†”’à½=ŒÀ°‚Ëí&<&s™F··²{vry-Wm^zÔ„ ]£Lë híþQ31n%^ D +(¶ãÆHb( AQ +EÕ˜’Hª…Juj—•p$Z©Õ³™ Ëp6—ÏŒ²v\Býq6Ñqÿ8 Ÿ°Ñ~©ècÓ¼+dK©DJ IrHš_\©Vóét¬Ú¬Ëjéñ‡ÝLŠGõè°Ö94a;1f³˜|F®™ …\*?‰’jöÒ•»·¶w2©x½RÉå V³Û¬¨É‚Éá9|b "À°Ö}x@dпŒ›ú;ñ"̈AN)ñL8‘'üA³ÍEÒ‚-p¢ª¦JµÆL£9M1ŒM°|Äå L˜‰£:Oÿ(—@‹t¨€0 ,”Ùéó1‘`¼ +EMréLc³µ|¥:s–âcf«Eà…h4ágƒv„ÐX@!3ݳt¤;¬÷þ_2º¡èìÌ2†sÁ`$M†#±¨šgÎãöà^ÊÏ'R–…X]Ûß¼Àòj‹U¾~DwÇÀÄñ1+P¨ÉI¹è¸—Ϩ…©…½[„ƒx‹’Ÿ#a'#_Tïæ†tˆÕ#îРöŽº£ã¶AÛŠp( „«¤ó2ª“ŠöÏ$1cÃ&Äè•L˜b@$“´aÒ cÜæÕº+ÂÚ=¬ÞALX ‡_E‚YB.RRVïÀ  ”Õø¨ L›¼Â¨´ûbǵîе€×5¡bC tÒò¸³à—Ä€h¬žq‹Çá Rbb2¨.eÅFŒ{9ùÎaͱA“ÑÉ!t܆‡ÝlίÎ8™ô€Õ;ƒ“œp Žš5ZûØ„yLc6ÛqVÊâLȉùÝxHkg‡Mø0̈ó輑ߎ+‡Dëž°øF-̨5 CCãÎŒIN_8–ï,nœ“c¹‘1=™¤“”Ý? +W2L8íÞÈÀ˜ãÿ<4|lÔæð†€»ÆL¤ÎÅéÚÈçÔ¶Ù#ž˜°3ár¸°è‡x*¸<¼Ñî3Z‰Z{!BwèáRV&ëà+(äÊPÅFE5nN‡ +ÃFD–RZŒ:-7”òzzò€‰Oê'JÇ3m³›ýú‘ñÁþ‰œ_®W¦ÏeÚ§Tòð epÔO÷¬XèŽ!Ë ·ÁJ´T ç¤ä´Æ€ íg°3£¯ÑÉšÉÖ_êÖ?dÒëq37›0LXLhÐA«>#f§]”*'º‘ü &dP6cñ†­DÔéOõ Å"~íØøƒú>„ ¸ËÆ)ŒË8}ÑDaQ­nµ5OØW å‹´ ¹ŽòÂãt8[]p³©cã¶ãÖAÈ°ÁÜ’?9M*5ÌP¢ìguN + 7jñéÜ!õJ ¥´kñ*Ç5N½Ý'« šñA½PgÄ$\Ìbù«-P™ÕàÁüˆií§/ˆÔØÑq«ÞÉSrƒVj.&©÷(NnÌÎ è<L3c_;¦¹ã¸ndÜ\4¦w;½!³xD­ÑØ_°›@tNÞˆ†õn~ºè¤ “G„É#Bê§õöŽ²jKÊöÌ^ñظýĘÝË#î;éî²Gu¸Ñ% +Ñ)3"h\ðÒ¡‡Í=*N¸x›/îO’ó:—€ÐŠ?ZsøZ;®ó´s8<¡tiñ)# « +Â5niÂÁOØY‡?CE{D¸i@%;‚’lŸ³Kv* °·³ÇÆì4—.w÷-¨tÇq½Š»™ΦRžÔ ß8¢Õè‘öô)Ð{”j&".šRÚra*ÃZï *œw±˜œ~7­zûËMT¤‹ñÅ;‡ìF”÷ò1;ªu ›}_Z"#u>9™éî† +³«bÁÊçèh‹Švœ\^‡)îÐ1g@ƒ@‰õ÷ø;]DÌNÆ̘LyR®s hœ„µÑ1_¼ãàs#¿eeµÜ[>ƒ…ÊPPnΊKV¼Ä zÂFÄÒæA}î¤Öé3!€7ȹýY¯X!BMø§—‰qJ €wxÈ4¤GÍž~žò†j\zžŽN9ȸ’™òG«Ç´Ž¾;h+²ãa\(¸˜” "x(UZpùCF|`Âók@Ã:DÖº‚l¢§Gù;†LF7ë$¥q¦±x¡¨OÀ¤›`]„I÷OÀv°0ÎV<ªG¤#Ž‹ÏL¥˜h'\˜CùìØWÇŒPþ×uÔÜßtãå +üÚc‡Ži §´Òõ +#7û‡4¤ ̇ö@8³ÑãÎÀ¨Ò#¢•LXÕâÔfÏ$Ëf"äö'QÖíS*ᦒzwþê؈Ùè€Jô˜©þ;x<˜_o®=oìiÝAƒÁø4°Áˆ%`õªx°Â(5N)SÁ")–ddÂʘ<ò°€­?6lsŸØØ.ÃN88ŒÏŒ˜‰1 ©±1˯Óu™PÉì‘þçÃÇGÌvngÐ@0úë}ßqÜ1êu›_»T[¹xØà²Ñ.±J&æøÒ¦\ÛbÒÓ˜˜¯6×ÖÎ?d¢£ÃvjØî7xcV2‰‰ Z]q +À F|…Gü)ÔŸröÏxéy v_ÐÅÆ\ʨî`WÊv¿êæs¾xOȯ⡪Íè‰êW)©à“ VŸlðõnÞé‹ ôÏo1±}Xƒ:“bÞM÷?¾MŠ3zõˆ ™|)_õ«¤T&媅x\ú Împó62ê €.>R[¿„GÇíƒZ— »àäÊíLrÌB ¤´º{]Jw›†µ.£Ãoñ½bÑ+ NIÊe;93à “^±¯ïT–/#¨¾@¾½¡Ö¶ÌžÐ€16àÐ*Ô8Ž [À±H¹E1·hö©#6nÔDåZ¤¹—Ÿ=À¥Ò¸s³s$š°’Z»oTß?{ÊêÎaç‘óˆÑ;aã@@A͈ ±ÒC:|ÈHõu–.û„Ì?™8Þ'µ\¾ôWû»åCÃv˜ˆ qÇq͈5ôo,Š:$è ÕÀ¢Èé.«ƒÂ$3£Ú¸Œ?3ë +mŒJ‡«3kWW/?i¢"V2ìSj|bFLÌ„2ód¬wXïE}Ñzw‹ß8a2xÍhe Ú)Ï]RëÛR¢¶rê€Ê%dÄÂ\¸¶®Ô7£­“ÑÎi<Ú±3ñP¼vß#/–ç÷¡$î€ᘌòE!½ˆKõ c@Å¡þ¹²!'rÐ+•q +L¤ËÄ;§ÿؘ–ý«=¯l÷©v_e³D°+/ãBÊ3ï˜pÙûÕk,\33I *iÝPа©¿^ëQ.Ϫ]“ðúL8gD¹þA>p5n¡€ŠTÜ„I6*æbÓ¨3`Òˆ…6bÇA:µÂ$Ð@ŠŽ5“3çÍô1=ªsñ&D4{ƒðZ7;áâÜL¡ ÓZäL‡FƒÂàºý"X£”&.Ai”],ô1ææ³62¢w 6Bñ€×•+Ó‘³—&„Ô׎éŽ; nä̤ƒ ŸpNX)ŒMAÕ8} +4&˜ UVðx ¦ vpÂц WŽ»Gô„ ;}I\¨‘Á–Î%)®¼Ñ@fX‹Œ°£#öþ¦3WÐEçꤜ[8dð@TéŸÅê–†õDo2*ãREHMA3H.AKñ!ƒ}Ø„ŽëzÃ,„pi+w±¨T´³I.û33dùß\¶w6;}A,.zØ„œîq鮃RlxÐŒ ãNÿQƒÇMÇÓµ L̶Zë $Gža ¡Â@¼|ºç²ŒÚC„ÂåѦ۟êŸd…*V"aÆ£'¢qǬœÉ(+ÙYÐ0äfTƒæ»(çþsdlDtÄ॥¢ÖÉþÏC#G†, ã¦SïˆP ˜ÜŠ‡Eµå—3wi† n½›5÷•4`!c•¸Ô\¢sÆŸè²™),R'ãm_¢Ëçæ‰hÜE­ºrõÆs¥…3GÆl:'cî¯`™h‹Ž´†Í„Ö‚;|± Ûÿ†—á’\fJ.¯HÅ%'£€yOu·ÜbÖ#åðHÍŸìEÊ«ÙÎnmþ`nï~73x6Z—ÒsÞþ‰Íaà«/Â&{BaaÌÅÓ Bj>=u!3sžÏ/¹¸¼Ñ§j1Ù+¢¥%B©:¹´›Oc"Ø×ox‚y:Ѳít{UÈMÙp!ÛÚ(¯\S'ÐP“›˜ÜupåÖæ™ëφsSƒF“6ºä Š¡ÌŠRÞ f¡Ù*¸J©NXHPœ3øÜÙÚÂ¥õ OÆ›'¡y“˵™ÓCÌ˧"µ•Xw/ÞÛ/-^ko=©ârÑ +Ê[0fœI8¹1‡.zÔB‚Ù;~ ê‘®DgÌB§nö10àuÝý–—äÒr¨´\›>sîžçñ&Âg"õm!·DFZùî~iê¬RÛqqZ·›I€A +%…"BNz/ÌDJóv* +ý%¢ãM65,¯Ð‰IF.Õz;û·^Ô .B¥Õxc+\^‰5·£-.3gÄ‚>)Ï©Í!}F/`¯Xæ–››·23ý#p͸ƒŽê<Áa=`ÄÌ„â 7C•ÖÚ-!Ùå’pyÁJ‡ë]VR$Úµ»öï}~ãò“ù¹s)¯¨åù»}±ú1®/X˜1¹-WJÓ;>9§u Jõ$›œwùsd¨¹@Ó?­. V–SÍÍa~Ç yÜ?à‹Ô<ÁBÿ;€ ’Íœ‚÷Õb,›hæçÎTV.d¦wãím>·€JyÔ}ü…ïí\zÐLˆ'ô(›Zˆv”Ö¾/Ò6Qà‹•™Í{¾~¦wKx¤#5N&.e¯²¥5-õ‡K?÷öìæåA0Tn!œR»ggN?š™¼ qòG@ÎQÀÀû„‰s +¨Ø_.é b‚ÎJ¸HÙÅÄ!´2‰^´½—š9_ß¾ÝÚ}4ðshÈQèìú£1+Ó?ââ‰Kô•ìÌ%Hëu?øUœK~ÜJƒwËf_먼4*Ý þgÂíÀd3"º V0)+3ƒVrÈÂF†ŒÞ1' “n¥TÈ +™Ùxc ~‡TkíÉõ>¿œéžNë‡kÄí˜@3F q±aÔÂi¢€ÿÏ„I°i6V1bœ Œ»N6«mî*®\õÅΦÚX3Q²›Ïùyj V¢ÕtwŸMM‚KÁ„B(¿($§@Áý©ªñ¤ÄÆÚÀ¢@Ð ç¥Âj°´Îfæt2‘ê®” X+L¬‘n®.íßÚ¼øHsõ.·Ê×Ï]*”—©n/ž}âìo7nñ 4(“R +Ë#ýu#ˆÙ+ƒDN89ˆÿL¼§q²‡ÇœcÊŠGÀÝqÅJ'ˆh,\©gšKK§nБšŽ˜¼œKÛ¸,@+V]^8ûp(7=l%ŒÞPjò<È(iŽõOB¦M¨I÷òíE+® BÞ-äÜb‘R§©ä”T^(Ο۾ç¹Ü̙à dÅŸ˜"#mG ê¸é×rçÓÿp\7jcÈØŸÛ bStlrØ9Žr11Ê›ñÐôÉ;ן™Ü½UZ¼ìOÏk±ð1mÿ(˜Ó÷¼.,Õ´àQ¬cÁc9¹°¤E¤;-z'ËO‘|êÈ Ý?Ü1< õº‚5¶¸)–6¥âºƒL™P™–T><`G{Žk0¥± *‹“›à½ÇìÌhÿë!$L,€Áã]-*bRˆÖílÊîO‘Ž'ÜFŲƒP0:êö…u_ˆdÔ_‚Gÿ¸ªþc~0½©Ò²œ›¼cØpLëÒ¸¯T‰7wRSg”Ú2'çvÏ?ÜX:Ðb¼”Ÿo®ß¬,_WÛûåùKÅ…ËD´u\ù£M—6{ø¾—&c¶D†;‘ê–7T< $¬÷ØÉØ°»6 O¡`cÒ¸Üßé²`°¹De“8ŸÈt6r3û™éÓÑúrkùbeî­ÔXµ•êì¦;;¹É“TrRGÄGP 1ÖŠä—tر Kt|’Št|‘Ž7X;¡ÇµŽ€‹Š¤AqÀ Ùùüü]`  t(¿°uñúã¯Páʘ±â2©tÔæîüé·î~>;w ;t‡IöôhpÌî?<á2S<Jdò*Úþ‡Y“74`ðh‘ 7Ô–gOßwáá×7®¿Ä—Ö†l¬GÈY|1½'4jg¬DÌI¦ˆ`VE‡F¬pÙ@jÞÍÇ„ þWù"T¤nõ›Ê3»…Þ–”f3óH¨éäK@´:™½bö¥F,¢ÿ‰âèÿº‚;8 'ŽŒ¹Lž`¼¼lt³ß8<üµ;G40B‰JÌxBu9ª!5HØNg&ÐCGµã¿ÆÎk<Ä(+Õô7ìG•„ŒD³“62 b]KLz‰ŒMŠå DÈé}»ÞÕÁf¡v“ðÌ„•ž°2z±Eggu6?øÒo Y Y!±²‘FÿÄõë : ñŠˆ bžŒÔèhELÔ2ÍMã—¬.œ¯®\Jt·éx͈Y¨ˆÑÛ?Œ‘Ë<¤µÜ}‘¹|*Ñ>(/Ücéþ ô’Äš š£6Jã +˜ˆ8é0ê$,C”Èw7³Íu'²{¸Xi®½q}áàÑåóïßû/ÉÎ,ŸÞ¿ïÒ}ÏbRÖ„‡L”ŠHµ`~5Û;¨/^³ùÒ߶¡’Lb’ŽA®ŒBuèq0ÛÅ tƨh“®:™$ÄFBÈFË ¥ÕË.>¯s‰je3Ó>§ä—ëóþภ„òx‰V»Ç4^=ÆúypÒÍ¡îÀêÛñ”‰Q+ ÖQÌ·ŠK…™Ó«îlÜH•@´µqæ^©èÜm‡ró@°ra 6ôøˆÉ‹Ð"ôÍü˜KHri59¹Þá2VoPJµÆm¾Q« 5{ö‰îÞÃÅ•{’Ó.ÇÃŽEVÎ>æä’wŽÙÍ”…YH€‹ŒTNFª{GÆœ‡Oè&̘³ÿy‹}ØJØü€p[dÔ9p§ðŽ£z·euýã&ÌDÔÉæ¼áZcåòÊå§tÐ#6iöHT2\¡bMJí²Kþüš=vµ¸€b&”óBás¤T×éÀà`Yï8¡;2l:4dÐz êûßhóœ¢ l¡7¤±à^!KEëb~&Xœ·Oú““¨Á ©-:iˆ·2.¤áeŒRÔi#*ëì4J)V<4 +R‚ð›v3i¨³'Œ²H u|ÂÅ1È$xÈèÓ8L(†óóL¤jóŠN\ Dót *¥—žÙ--Ä«+ñÂÂÔÚ¥hyÖáñÉžRÛÂÀ6gäÊ&¥Î™½š">©äå3Ã&b²¡‰ì›DBé„;$·÷óEðШák'47ËÆ:bz1VÛmlÞKEjL¨0½q-ž·Ñ†¯6w LZP»jcã˜?¡Áp®äK#ߣÖC£Ö1 ¹@¼kÁe£›Ôš‹Ži쌜žGÐáPq®s +ÂŽÎÅ‚ƒÍvN"´ÞÁŽIˆÿ‡G¬£F¯›ŽŒ[ˆþ +vHèþL@íJŽ·Ñ`É*g»'1às<ÛZOu¶ák £ÅBZTÀ¥ +—œ 7F­„½”ÒàS‹lÌÆ +!7ÆlâÃÎQ#‘¯-5¦¶úÇÄi-"zåšTY·O«urJ5’mŽ‚Å 3Ñ:ŸìÁä‹ cÿÓ½;èrX0ï$¸d/TÙ –62Såò£¶ué;ÛÞ \×?„Ðœ°@Bñé=€Xðö_ÿê{ú‡Õ³n_Ò†)n<")åÖü¾Ýƒ€—™9½tñ©¥KÏd.ùR3ðF¬”îÍî´çÏûäŠÆåŸp0_%bņIb´jÃc¤4%f °ØÁ=ÿGïù$Éq¦yþ7‚$кt¥Ö™¡µÊ™‘‘Zk]••¥U—V­µD7n¨†A$8Cr@Ô3Cr†»;³Ë™•gv{k÷õ¬/_Ñ›[>ž®ÌnœJ +Ì“Z‘‰´Q¹Î=/•†Ù…“ìrBŠ{°uZÒCÄl~Á'œ!fI€l|ŒÑr.B¿å€aÈ‘ºkd›kçÑztì .vœ©j™y>ÒAÅü¤‡WãtuyÜA>?ìwqvT§Bu˜\NCŠÖŠS[ãNrÔ>X Wóý ¥…‹ý‡¡â*Èýbg}fãƸ‹u@²UA/M:I',bBdS²¾H(Yi6nPŠÔÕêzfñúôÖ‹Z¦7»rþû?ýƒœìºƒˆÜÓËñúÆη7®¿CE§A>"B)%  4|# ¹4FŸâ}6\æ|ÔN êÔ¸‡0å⇢٫±¡å'<€èqV„„ÙǺð,€Ð€'#M2\“3½X}M.Ϊ¥Åh{W,,Sñ©:»ý­¹XkÌÛüªaB«R@A´øPô‡íÄ°ƒw³NHsøƒ~LSbåâôº”hfš÷nFjKF}U.¯¢‘Ž—K+Fu~íÊÔÚ==cGÍ~âR¤VE¤,­ žµ¦•l¼8ë§Âƒ›êh Ö0TϸW wQcä8¨¼Ã6|°>†¤.Ò‘J¶¹úôݾÿ7_[:O„ +jaN),fŽRÍÚÔ^wþˆ×ó‚^ä"5&\㢈/L¸¥cб‘Á-“›¶ÇG½Ç†ÃfhÌ +¸>"F[åéýÎÊ•eEU³SÒ¨E*ëFu“‹¶XU¦6Sµ,›}œ¨7óX¾—,ΞóMB~àø” ÓF¬ÐyªàbFŠ·l~у‡a@ +ZIÉöÙÁ3uE\­R¦ÜݬÍì÷“˜‹ùHÝêåÜHX_7*«©ÂFÎŒ»=¨ì£ B+1ñ6Ÿšßã#Ôh¦¾{á°ë˜ÚˆVbµ})ÖÕ33JjÚ/dO™0FÎ%‹sÀâuM¸E‘¦Â3íÕGÍõ‡”ÞÚ¼ööþï.>Aå Ì@BÚ…G@ Ù°£î¡I¿Å'Œ8(HHK<âbFŒÍǪä!eRÏä§Îž½øRuùÈF)$(Ž•õdû(T^ã^ ØKÐ@<Í^fØÛ ^N¶¦Ö¯ô·n&[kS«ï²±2*%åÌ´hO¯_-Îz˜˜¹pzfõ¢‘Ÿ4äBUv G…ŒQiÈä,ÇçÝufÒ7dÁœ +$=?ÊõI%3{öV¾»ãá T·c§F=v¿`÷ ÀxÈÈ1&à ú„ uä”v‚&)1J+)TL)©©Ã[o-ìÞ¶ÍϤ(µ +jºQ^Gù¤—P6<éeAÕ ° `xÈ`™JÁÄŒŸMŽÛq*S£eàpþâ„€(s¯xÆ[nH¥xjÜ3jGQq"A@4 ¹`9ç’>.*%zÑÆ-.îßi¯]K77Ê3û͵kåÅKZi“³ÑbŸ”Ì>ƃ)“ÔŽ1+ tBÊ +Fø±lkÛZjòÛ<"¦#µ•ùÃׯ¿Yœ98wãÕs÷ß3=Ÿ‚„\€Ï¡ºœYLv/–y\JçÚ;±æ¦ìlq V»Ul¨!=üö°ó;CÎ̯Ô1ðŠX4 vD11kTV|Ô‰Jznzuÿöƒ×?Y»ø0\_ýf´Ôߺ×ݼM%¥ŠÉéòÌAª¾Œ7£…)ZNL:ÑiÀBÆIc.ØÅI·h…4¦ûé¸îdž#&Èì¢hµ(D§`ÅÏe”X­·z>”iC¢!ç{õõ›½½‡ ãÓ¨X×ìƳýÍËOÝlxÈŽX|(ˆnÂðàÆ„ñÀ'çÛý½þÙ›Ï9ž;e`BªO†ëàR‚¢–ª,Îœ½xÙDLÌøÌ'|¸&UXHŒXaŒxÔI79é¢l>qÔ‚s@’ žµ9aãf"-Q™í®T¹¤–VÒí Tµëë“L 8˜I'ëÄ"D¸©Ó“.$hóñÎÁ¥áÇ5“µÀg&üÁ„ø·OL~ëøØ_;vÆ2díPSrà”²õÅPª3iGY%cE‚¨’§¢-);§–×rýËÙîNofãû?ù‡Åý»€ éåE%?(•O΂Y+,á\¬·|ÕIFNŽÚ†&ì&'21ØÝFB—(% ê¾—ŽâJIHOeºgKý½TmáÒ­—ï¼&$›°œ—Ά +«™ÎQwóQméfeîïê©©•½Ûd8eF9'e¸ë‰¸è¥wlhxÜËûƒãú°78â 3¡gìà,%»$¥ç)9¿º}»ÜÛ¢#%)3XÎ%=}¡¾rgñàå­»ßMOïçÚëëHÙy!Þâ"uTÊú[±g!ìȵj…ƒN\gB]Þ˜€ï!Œ ÐçJˆù~lÜL¦›L¢lÕ4¾1á-TÍÀbô•“ŠÓF«½r½Ö?LWSÕY?oJRŽÕ-ƒ=Pd+2y%{@C¨X®ºRêlvÛœ šþ$ŽhUX-ûØdµ·Ûèï%Å«B´Fkƒ=¼¸h 3K—ú™ê⸳!ö€ uP(íqÌ‚NØQ%ZŽ¤[7‰sF¹¿µxtáÜc)¿–]TbfõÊ'?üU<3=᤻Cù4­ÖéP›ÐÖÁ¦«Ësç•dkÒp3#f'é†$/"‹jbÂNXÅ…ìùK]#Þ ëET›Gð !ˆ‰‡RÝË÷ßšôÒ>.Š*9*Ö’ +ózm=Z™ï-ì>xý³âô6Àá\w¯0{A-,ˆ©YTk˜‚Uõò­GŸ‘ê_>6d8lÁ&\ vBˆWës0FȬ±P>˜iëåÙ|+Ñ^ H 5?oíБfÐhª*>Ú ´ª’˜Î4×h=¥ås,pȇ\”>î—FÝ"æi½å¤b^.5ìž÷ûD;ec]$X ƒÙùõëåé-.Z•ósÉîAqþòÔÆݵó«+—”Âlkáðõï}Í&ºv2‚ie2TÆRHÌ~³+÷4 `ð²íÁž)ß, lýfEP“7s9\® ÙÉê#ˆ±Áèó`›ì0ØÙÞ9Ö¨Ñrº>½È/eJS[ÕÙ=2T´ AQÁhE›n¬;¡ÃÃû°½3&¨=Ê:ñ3vŠŽv Wæ6Ÿè:é˜×AF(©†—1| Ïrª?XÅ’ÉM1»)“‹¯8ayÜFè@¾™¹æ#ìV`Ã%Óm¬ßèmßïo¿°°÷pñàA­» S!7ª!\ƒ*öÀ`Ϧ›öbâr¼?­NÚ1w@`ºÙAúPÈ—ÍMŽ˜!„Ï)©@=Z;eFø­Áäl†3:¤ZRb /©äñ1aàóµÒ\}ùÒüÁýùƒ+ë7Üœ$·¶tÔýR’r>>Hy\å \ëoYPéÛ'&Lv ‡,^Éäa,>ƾ=ߘ߻ÉI RJË\²£ç2sGáÎ.®Ðjvëè~óºU=D•ŠJf¾1qjåZ(7O^.÷ž<û47}ø—Ç-#vÔK"ª¦gr!1cG”¹­;b²óܸwÈAØ ÃÍ$©&ÒòýtmavíBkå¢TI½ g»R²AEŠ©ÖÚʹ7n¾k¥He%ÚÜ‚‚E'ñ‚êC6< +"Í(-GËËVD:>áCÀuŒO³Ñj‡O[éÓƒ}Ì¥c“® /é£#bº¯VÎÆZ»ñÖž‹ŒF3Ó÷Ÿ~ºvé)(Ð|¼®g¥|_HNÝváš–X­R˜:‡KCßøv‹‡˜plÔ磒ÀQק·/ÞyÜ 1Ñ6i´Lš6:Fmsz÷‘”j·çŽn=+LïZŠ df ö¨¢b0;ïs8Pø,~œ›;eÆ'|A;Só©Æv8Ó©Ïìôvï­\yuùÊ+s‡…Ô”U±`&Ù8[˜9„¸«—ý|ÔbeŒ;Ù kvóf'텃ɼ¨×¾uÜtfÜg÷ñV7es³¶€:á‘P17»uTÿÿë˜õŒ>™1:Rªï$bß9ã1û9'¡8Ict¬ +¬{sónnþ*ï‘šR˜OtK"¿÷ͳ@KáÒ:H|k »ë÷Z<¼Ã/ŽÛ‰çÛW¹G옋МTØ„Ê㞉VØdÍ'¡Ò\míF¬µ•hœÍMï§Û›l¤ü³{0¥H(E?›vbp#]Í–Z+{W^bÍç‡ì~ÚP¡ +¨>v,â$ã>.®fá`úĤdºVÝT ?ˆ^ ý©bwaã\¹¿á•hãl{ó……£§s»ëKÊóGr®N¶ž¼ÿ·Ÿ|fBC*šétÖï'GÉÆ¡èYQÐh8Õ¤Ôü˜›;6ŸÄG]’‰„B8?·ãé…ûoy©Ð˜—3cª‹Mz-ÓÞÌu’S½Á‚—ÒõêÐjL·çRvLsàáBÿâ`;?7㦌€óÓq'¢ + J0€ô2Š='¶øƒ”^·¶ +3çg¶n7—¯ .ÕÔ…k/}÷˯ íÕ1·€˸\&Ôš‡Š›20œ¤œƒØè5R+ƒ¾ÂBÕø`YžG±ÊòÜòÑëï~1½v©³ykúðqnùfváZgûÅÙ»ó;wͳ©ÚÒ_ýþö“½¸Æ‡Êl¸é€TŒu¼-†k@7ŽBt¨l…ÅcžãÃŽo3ötR)&6MëõXy‰Õ+¨Z¤b"Ò¢bS\zÎ ‰d¸ —æ@F0Ñj¬µQ\ºÒÚ¼Óܺ_Y¿é°©žžéo{¼¸ÿQó¨ZŽ ž*Ù â‹Dxf°½»89៌PCVtÂÃر›¬<£—BåÙTsqnÿÞÜÁý³W^în܈··P½îaØ7÷`ݸjöž7#´Znê°Ë§—p½à2L÷Ò>¬€°ø¼Ÿ–5|fÉÝ8 x•J®_œ½X_¹\ˆJË•û»ÀÂ#T^j­ßÞ¸úlîèåPu…O6|œ†KÑîÊe6’÷°® J t&¥&–,Í*‰¦Umþ Õ?ØœÅE˜®eûÉú +¨#dž]&‡ŽTÙH9Ù\m­_×kkÚXZ9wçñ{’Q 0F¡w1;}1^Û’’ó>~ØŽù0íèòãBkãô`“YÕÏg¹ø´^9›é]H´÷1>~ñÆË{W^ï@C€Dêkwvn¿¿zý=¥²áÀ#~R{ãýÌo\™ðK''`“_æsja –O™`',MmÇ››C6¨2 W•ÜB03K)™£[¯^}ñ%Ùuá!jp+¾¯­÷·níßy·<Sªˆ˜cõ–Ù/sÿõ‰ÉcC.àÁWÁ¨Ôáïœq8B$ÓŸtrÃ&ÔM7l9êW1!ç#˜”bã­d{;3u„¨5^/{¥ùóõå ›çî_ù»ùÞ–”ë½üúÑÃïõ_õâÖÓ¶ÏÞãbíTqqiÿE9ÓóqVD£Õ2!ÆçšÜ´Sôx½ +œ¿œ±øY  ”^‰¶6“½ƒÖúÍÚêe!Õèm\)-^ˆ´6õÖ–˜[ Œæà©æD T.1ß·1ºOθÄ< ­±çRj&,êó¸ÑTŠK`‹T1£)U"TCä"ål0Û"Ê' >êãÃÑÚzgëa{ûaiñZfúÀ¨}«jéÖìÁ=¹0«ç[Û"í=&5ÍDë€XgÖ.yHyÒËDÊkBbæÅXËG,>æ¢r¼œÒwθOM@68H(™D}ƒuìHÈäbÕDËC(“nÔê§ÉÝyóÜígÝõëÑâÀ)j“r0uãúóó“¨uÖ^y÷Ç#&øø°{ÜÁxéíj™…pj +çB¹-ZËÚËs5ˆØŒŸ ¦[«ùþAqö 7½­—çÄx=Û>ÛÛ»;½u¥Øßâ©æèX’²J¼QÙå"ÕÁ£,òÀÏ£, ~ÌŽ9aüMH© hâcÁôTsíÆÚµ·göTŽf–vüÕo/Ü{ƒ,~5Ÿ˜: +WÎÆ[»S[÷7¯¿Ù>{+?µ}ùчLªg!"ÁÂRbê0=suîü›sÞ¨½]ìløåï^þð+;iLø•1_p’]|†IôÃ¥Õ\sýü—f÷n.TW¯ÎéõåÊÒùÖÊ•õK/¾ð.¨†ÙöÆÁÝw/?þxéüãÒÂeµ0kƒg;)½cA´qí& 'ªH…êB¼)<\·":©Vœ¸<æF€7£"ß\¬pÍÏ$lHÀW}þ«ê¹V®±­, ©9D©Ábå3L9óÍ.Ò $\¨>bö‰ƒ\,Ì¥¼¸^„¹8¨Jˆ˜ñÍG[À• 90+$ø™°¡½tÕsJºS™;j¯]ç£õNr0§ã›'îP›_ðà3b²ë!t«`gñËf?é¢`ÃC©öf{ýZqöHÉ/øÅ섇C%-ÙñÓQRɘ$ÊçE£‰ +)+,Sð1;å„”#¾ã§]ÇÎ8Æl¤×„ŠK)àùŸ;i³¸é1;þ!Çñq߃w3&'IÊ©£ØH¨´.¯Q¢v¨µZìï':»ÁÌ *hÉf´:O¨QLRùh‘#•µXs°ž’@ sá:*Ÿ1#£V 4P0ÄÆ-^ÄØ™I/ bP¬Aë%µ¸él·–.Ì®ÜxÔY9ÊOï,ž2½óhýòó{/¤›kR¬¢$[ÙúâæÅbzJ+,Ô–¯ö¶nƒˆ5Öäì¯ÄòS—î¼¾wëU'e3ó\¢§WçŽ^í½š¨¯µg6?ýòçG÷_ f§âU`·«Ëçî½õê÷~ñÞþñ¥¾Ú¾øÒ»ßýÛ;¯}O-/©¥%1»­ï4Wï®_}ojç ªVi9[Ÿ›µÀŠ +úù¸‡KòÙÅÒÂM%?à¢å©5Z-ðÑv07Ã$ZÀLRz£Ð;Z¹øʈѢ”hàz=ß»P^¼̯{ÙV#Ô<( °_›7µ% ŒT¸æÂ4³›qjª¹\zΨډ° “‘`†O÷c}*ÒÆ‚9JLRRââ‚Q³øÄa+‰ðI1Ö$Ô + T`#¨vbÔ¢4@G½¤àƒ‡Ç$áÁ“^acàŸ£vâ´rÐ$äebƒs¤T0ÕJ¶Ö=tÌH–#…)BM«™Ž”ž”LNÅE½4 Fò±"ÄB¢+埃K…P²ÎÌŒö÷r^\u"’ÙËžðqˆðbaR.úH\+¦vùž{î®ZRIÕ[+—õb/^îE +Ó¸’…XƒÔE£¡$Û°ˆVÒíu&RB•¢—•.Ê%^‰—ûJ¦m¥x}MÉÏÓ½úÂ…âÔ¦‘ª4Zý÷Ÿ¶–wÅh©¹°¿{ûµ Þ¾úä»—¿wþÅ7W/?è®ì^¹ûxóòãæüù­koÔÖî4Öï÷¶î7—.Çg|<œh\{ù“æê•“fdØA r–‰¶õÆ~® “3Œœ¼ûø=1R±ÂªƒŽÊ…ùx{··óhá³êú=`c¶oß{úQ¾·ë3t¬ ©UXk–ú——/½•™9ï¥#çn¼œëlŒy€Õ>ÙM'a©”Ÿ:,º`ÔëÓ;õ› PSM2Ru3q"Tfbdw •¬'Eà'I5®…› +AbZH´ªKWÏ?ùAiîHó`j˜ÃIç`á«OÅÚÐÞ$Ô%§ÃÙ)ÐœôôE!³‹0u ˆ3gtm +AIwC…>ª€1ùù!`¡G|X°àcâã.0‡§Lè 6ê F¸ùúó8,ØñIÄFDøäT0?/f{|¢ÃF[Hµ,ÒË„p5..õ-½¶I5FËD²M!Q³#‚œì¦¦Žâ­½t÷WŠÐ +75nC½¬ÅªuzÂ}jÜ=bÅ £|fÓ„Éw6BŒR³rª•èlðé)R/‚>Iuö)föâ@ pµ©œÔ¶k^Q("R¢ª ³.<‚I1ÞÔ€Pèõf7jÇP.jp&/å¥Â¥‰²K”ù­$¤p¦Ðœ«Ì®å»sFiŠWÄl[¯Îg:KÍÕ#CþöÎígͳ7@OŠz ã£^\ò@T,Y]ܾ‘h,V Ƨ"Ùy%5Í-qp>¨®ì\ðÓ¦ÖåÜrº³ßÛ}8øbgã–š›eƒñ›wŸ|õÛÿ0¿VKz}+:u±¼ùøðÁ÷^ȶ—•xþÅ7>ZÜ¿o +‡l—œ‹4÷ŠsWÎ^}/R^Ò“­›ß=¸÷î˜Oœ„+HÌPmzóþÖí÷6î}$—s•;oü Õ\·AA*@Ît掞¾ðqm醔šÝ»úZ¬¼|lÌwrÌ7æd½T“ +ñòr{õ:£äÎ]yøög?#ä<¬5øì—š—r+sç^_»ñaª}äÅ#ï¼yáþ[Zv +ôùi+ +2ËÉ~:I†Z§ÌÔ°_ß¾qîêKvHúÖIó·N˜þòyËsÃ><ϲ ïWšÞ"µmâjp®‡lf„KYY*Œ•]XÐ…+RÓzu½4s0½u[Êõ½¤/-ÈÙ¾L³šô‰£NÚâpÞà””+@[œÎÈ2é&½|Â'fìƒ)~YŸ¶@¼)륹ìܹ٣—:;÷ +K7´òÆ„—v#‚`T\ÀÊRQ@úc.Ή`>Z^F‚yiè™ipJvD´a¡1`Ç4;"áÕ"¨ìNH4ÊK å9µ€s $-œlù)DÐ'\0ÌEp%ÍÅ*•…ÃÙÝÛíÕËr²¥g;JfÊ?°…q.”#ø¸Wp&„²ºÉÃœ± öo†0ܨì'U/®`‚iœ5F·‚0Ÿ"ƒ9?ö3:*ÆÁìV$Ù¨´WrÍDŒfVæ/¦»Û¡|— g5 '= +¢É/ŒØIRÊ0Z“>*䄧Ÿ¥Å¤–éQFÛPB…%½²*%:µùsëÉÎ&Äèé|se÷F´0 1ñHq>T˜•’mZ/)óSºUéÁº^±! f È~.#§æBùe>ÒÄÅ„ Ä²•^¥·Þ Õ7ÔêFº9Ó;)-åG„$„ ³«çû›×ïVP¦š܆eÕ⤃˜Õ4T¼yÿ­;ßµBÒ±±À°S˜D£¥ªW7õÒJ0œ}íý/n>z{ÂFöLºy`þÆAq툫›$yCO7ýdˆ,Ä•Lx¿ÒÌnº¹ + qVŠM/¤ë‹V8èÂBt¸ŠJYLL¸ Æä€Ç-—axÉéõ9ý(.%ãÝ}~°‚÷4ª”0No÷6–vniù&ZUósJ~ÅËåm¥5&˜°y(»— „Ž“bVNt—`Aؤ y9[¬-µ—.Ù‰‡Ö½´î€‚&=8gNg&]4áa3:j%¼D|!¥P1°!¥¥Ý„¤¤Z©Ö†VX½âô¡–õZ©µ–®-Úü"àA¦º©¸Žùè£5À)™~IMÈFuÜœ¤dP˜ì@tˆK9Ñ0pÂÂÀ™a!LN©ñÂÅ€¡²A":xz +x¥Õ©( auàì°2lûpHeÂ09p³‹?„3ƒ} ­ìçR-õCÅž.Âê"˜â´œÉ3Á˜ÕMÐR"šk» ÊáÇ}$ài% šúÁâá€ãu ƒG¹€¯x #gxPÅ…®Úf#5˜OÀÂ`F$ÂÆI)%†s0¡˜Øи'¼:>1â>=æ3Ù`g(V‰ÄªéÒ<ˆðc·àn" ‹~>å¥"0+£Œ©tähñø¸0/,5[œ½ÐX¿S]¾b÷‹~\Eè°ü˜àÅa ÂœNiY%ÕT’MLŒ JBÐ’­ÙýÊÆA{AÆù(mÂŽøQÑ ý0É +AÅHNBXMɶ0-ç¦t&KjjqõpóÜ]^Ï‚TÕó ¹Î9-·  ÄÚÝ8h”}„ê†"A´Ži:Ü Äœ‘e5³¹yùòƒw\”f…y©¹`Ù L/ÅäÄÆ­ð{fí€$'¦Y¡ ÙÇÛa‰Sàäa> ”du!V] +åç•L_ŠOrÁƒkJ´&Ç ®|TDËÌDŠÔ¡eóI£&¯ÕqR„’bϱqM¸ˆÍ bÉG'H°X(;ƒsÀ¨xÈP]rSQ`Œ0º1edÂçðPnÀÎVèŒÉ ‰—ŽŽƒÈ°ŸŽ™}ÁPÑHMû0ÍIT…è(8%G€þSMÕ&œ(PQJìf|"d§·»` xßlÅjw¹<þ!CLæb.L™t‘ÀèŽYQ›;vÆ>j‚¼ˆbu3np½pŬ.Ê‹•™m^Ë9|4ÊÅT4@E6 +rpdÒ;4昴Â0®Oz‡Mg@àa5VRŒ/güXÀà_Ÿ0võŒ ¦Á¢ŒFú iÔxnÈNs\¤&Ûå"Õ 'nõ’Cfÿs§'ìnØKOðáluÆÈwE£ä#›·û,á@-ˆø 9šîd¦¶OÛÐa³wxÂ92é„Pš¤%9’ÆyMMäq5a °´+7æ’¹6øà º)Ý FK€ÄÝdxØŠY½Œ3À³rF }„lr¢V7n÷QnP„ô¡š)ä*3ÉÊ4Jƒ‘ Èe«_ô!"Èç‡\#&Ø švbÄ:áœD W*¼Ñ&¦ÄhÕ s’šl/0zpqŒ^ ¥§“Õe91åÂCf…rL0OÖAÀ@ +À.z Þ‡ +V/>jr™lÞÊ»ŒÃπƕ ×(9"Á‰ pF h®4”Ìi¹€gðqV7muáCãN»‰¸^Ðó¸³!džm“ÖÀÔìöÚÞmˆ +OÚ›‹pû8VŒ{Ñ 14îöaÎG@W}öÁŒq)B¤ä +À&‹Í€1RÑB3˜˜ŒS;@×±V7õÜñÉI :iE†G\§NÛÎ »&¬ y%œ1x9|K4U<»wåŒ fw³@U~ÁåçQ2ì…¤1s@5CÉi,C”n÷ó#&èÛÇ-c6Ò Ëà|ž?5 “ +)Æ-.ÒìÄǬÈé1N?"Q|ÂCN/Cpq6T?9áùÎió_Ÿœ86lÙ!„2«¨¡èÆáÕ`´8î"¬~ÞKjˆÂäR€KÃ![;×_|ÿï̈ôÏ Ÿ¶:}4#ÆD-©Æ²¬Ú<¸Ú߸dƒ¹!“Ïì¡M.Êä¦`Z# †Ã±|¼4OËy„5ô U ^ó#$A ¼œt#¬Ó¹¼8BÊZ¼†p á¨Õn§Z%\8kòã&/ìô㬜åBm¤Züâi“4Ù=x€?… +Q7€VTsã@"Tð +Š碵7rB‚ÃÏþ/§¤%½0îDOŽ»&œ ÐHsÃâ„gId£Ñ8h›Å˜°C@ÏåD3^[þæN ‹ò±\k-”™Šd +\V SRCù8§•!ÊÝn$±LËéÀ¤Ó¬ +á„\txa³Ã«'2›G—7<â|}ÊrzÈ †` PîAPÙ¼¢ ;=˜Ãé°³Ù\>UQ²éx>—’@ͯM~6ƒæšðŒN|°äðp§GÏîùq‹ƒ¦ƒyRH¹¼ I«!#«Ç +b(BÚàA<¯5jò?ÒtòŒulÂmu`$÷!Ò¤Ù7<îž°"67‘†ŸŒ[=ü©ÇÉ3fVNÃTèô¨cÔ0;)”ÑåP%BV'3<9=ì ;FÝ£VØåâÉ™ìœæšà"“ŠT:MTà](.©L¹JdP!èðûý¿62嶖®??l?3îvxZ%B¡8!±Å|jfyYMå&½È¨v@"B«”¤†c‘x&OGf®ßy¡Øš2¹ÏL:ÏŒÛpJ`9NÓA=>¿¢Hñx„á«}Òé²D  fsÆöæâÆîzku&UM)zWU/)ìökAA¦ó’*‡A•d=†³º @ôÛ3aöŸHæ+ÙÖ4JD³M„ ù0·ò6qjÌõ­Sk@ +0É¡Éè:‹äh¡6Ý™[äƒ +PqäÃ1r§€cNOWg„Hž ç‰`ÒäÂ]"‚0)°‚Fpa"@¤FHY‚7X.ÈÑd"RCaVƒrÀa‚€) +ª×Ó3ó]’ªHz Îí¥üˆáªË/8¼œ–"ä °§Ãå´:vI’*ÙX¯’\ë—/ìNn÷<¼¹º>¯‡C0tC¨}8ôÿÕs£Ç‡l 4Äbñ|$‰Óš¬*²,ŠA¯s!æx‚¡ ¦h#†‹Ê¡‚ÃOŸµ=w|ôØIóИ{x°Æ‘oÜL;ã}î´ëĈgÒ†[¤Õpz H2çG?DIrÜ힟Àè%$&íÐñ!ó„‚aÞëAv+»µ \L SÍx¶Z¨ÆµV‰í­¶®œ[Y^í”JF1ÓcqQÌÎ;5>>i÷xË…x’Ç1™ïd +åh0ĉšŒè4ÏD ©PPgÛÑû×·îÝ9¸qyåæµ³ÙRfhÂ>4éö( O‚ Ê¥òˆÆíbx~*Ý(©ñÒÈ+k3Ùû76ÁñÙ³+¿û»7¿þŧo¾}oq¡‰²ŒÈ!|b°y.¡Ùp"©”v3ݬg +'Xœ§Ë-Kb©œ¯OÕç–—n?~#Óì:Ø䂆'€yzÅpF²§Ç=ÇŸB(‰ +ùT:æ ¸=¾A7Ò|°\ÊïŸ;_íteØ__22i7BØü8­Ä1)¦$«@Ÿ¡pÄ£ñl¶ k©ˆ˜Ò°õùÒÎF¿]K—SÊêtöÖ•õ‡·vž=¾øèîþ• [ÓÓµhÌø ·±»p„Ð&㤈!Ï`¹8ß(†j…P¯ÝOÞÜ®<½±ôÞ“ýïÿñW_üæçß{rwos}¶Z-Š‚ä àn; +e£$ €ÐD¡t(XJ†2:ß)sÝÌT#»<×¾|~ópoþú¥µÇ÷Žܹv°sP-W˜Áó´ÇãsØ=ô˜99â·`'N™Oœ2Ž;œNÈíp 4‘ЕZ%R‰%òÙd.—1"ñ¨‘öÃô±Ó&°6'ð¸xÌ£ÖbÚîEîì7¯ì´nžë½ýäÂGo^óÑæÛwûøá½ùÍ{óñƒW_[šŽÇãÁš­»e÷ZãÔIb”w®„.ç7Ëûë%p\Ümß<ì<º6÷øæìÛ÷—þð“WÿÃo?øì­ó׶òÉ0fµÛ&¬>Œ +â” v×td«%ô•ûûÅw^X}íÖìÓSï¹·ýòƒs÷oî¼ÿÚ•Ÿ|ùî‹oä‹ÑéÙ™öâÞôæ}.Rö:m2G¤!¶§ô»­›;Õ)åÁùæ/¾|üõ—OŸÝYy÷Þü?üäé¿ÿóçþýG?ýðâŸõô§Ÿ\Ý]H)ŒÏãt›-^‚Cg]½}ýlöûoûú>}ëÒë·{¿´ôw^üãOŸþìã«?}ïðÏ¿yåO?¹õÖíÆÍÝòúl1Õ’ÉdPKÊá,ð1P‹¡ %áòJæå«s/ÔîV¾xçÒo¿zöç?ýè÷¿üà7_ÞÿŸ|çÿûþôÇ_}tÿ¨ùé+;¿üòÅ×îoÇtÅl œ÷Z=ÁÈ·3ඡ^‹ˆ;ƒØätŽ9·”=Z)nöŒƒ…ĽóÓŸðÂÛöß~rõñ 7¢F̳Ddq<£2¹Vã®-éo_Ÿúþëç?ýÜÏ?¾ûöìÿýóÏÿ‹wAüñoüùw~ôú•µ~!¦K +jz‚&‰ •tb6‡íwùË ú‹»¹Ïžœýù§w¿|çâ'¯lõÁÅÿôÕ“ÿùþÛïÞùÝïþ×7~óÅͯ,æbA–c ±8|>à'd-Rúiöá†ñÚ…ü›WªŸ¿¼ü÷ß»ü?~ÿ柾~åÇïŸÿÝWÿõçþô“û?}wïgïíüê“s?|s÷âF±˜;@ùH$€Íw+KíäBM»¸{ÿþìß½wøéË+¿¼þ‡Ÿ¾úë/|ÿÙáß¾±óÿÓÇÿç¿ý¯¿yó_Þúo¿óï\XéÆÈ646nuRaÙn.XÓ}UÍynš¿½‘¼¹‘xx¾úÎÃå/ßÚûäéÚÏ?½ù_ÿéóøñ“?~õÊ¿ÿá“Ÿ~ïÞÖR­Û›‰»\‰ñ\Tn$˜µ +>þÎî/?ºô÷_ÜüÁ;Ÿ½¶ýÓoüÛïÞýÕç·¿|¶óõÇ—ÿ˯ßøݧW?½×|çZi»+g"so¯ZH…Ü>Xb\ˆ ’ZÊFŠQª'v{áëk‘û[ñ§ÊŸ>]ÿýîÿúók¿üäÊÿúO_ü¯ÿü£Ï^Ùúì‹w¯m8í…˜0`/ŒñþZŸ+ÐÓÚƒ½Ú'7¾|vî«w/þûoÞý?ÿí—úêå¿{çè­»k«³ÅˆÊ8ÀQËÈ¡ŒªÆ5žMá™|p¿Ÿ¿¶Q¾·“ûá«›ÿðƒþøÓwß{qã½»ý¼²þ³.~ütûæ~ck¾R.–ƒz–U2‘‹ *Š‘ÎÅb ‰hÆ©­éøîLâÂBâõ+­¿}ô«ÏïùÖ…Ï^ÙyçÞúãsSöêW×ò‹íp)¬íðQ´TÐŒ:C ©ˆšPé´Bô‹òÍ­ê'V~òöÞo¿¸ñ÷?|ðÇ¿ü¯¿zçŸ\ÿå‡Gþúñ¿ÿò•ß~wïóÕû»¹é‚„An« Vôr"Ý%CaðR˜¬hðLÔwyV~tT~r±ñáÃÕÿúïþùwoÿá'ÿåW¯ýû>úù÷nôdóÙ½åÙnZÖÂ8©p®Ð’x>¤–š‰Vï%¡­ws-ûøÂôýƒöõ³¥·®÷~ýéÍŸ|ûÓÇ›ïß™raú°Ÿž)(!¡)!Tàd†Ç<BÑ;Dºê¹ÜŒoö‹g»ÙóKå»{µ—/öž^]|ùÊâN'²Ûow¢3eëÀÑqÅÏÏ”˜Õ¬ŒåRéLDIkÜB5±;SØŸŽ=¹Ðþä¥åß|rþ_ñä§\þîý…ïôm&î,FîŸ-,ƒ +îÆaN0Ö…è¨Ùù!÷»5Ò[ÐðÅrp§Íß^¿¸WxïNÿ?yô¿ÿüýýõk¿ûòÅï>::˜¯´ +I`zíˆâEXŸÇøÜ2'4¾`p­tp¡Ù›Ï-nm×?xáìGOžÞX¸²Qëy¢a«Ã6aqÛ|$=•«¯ë©.χ’,%ôí¥™s;‹ÍµßÓ_8è¼÷òµ+;3ç—Ëà³ëS‰z6dh¼¦¨q¢anÀ|šRr¤œ¦šf0,ÓdZWÒº”7„ùz|o®|~µu¸\Ûèå»ÅT'—j¤¢†&$jÜê£MãR.;^·Ãú| +Í4sÙåni6um5õι/Ÿí÷áêß½òã·/ÿàéöçÖ¾{wéÙ¥ö¹9#Äû!Ÿ›â',#L ¢"7 ¨l,YMÅ£¬§nÀ­ÐáLôòbâó—7ÿíï?üÏ¿{ïëOoñÆÅ/ÏŸ?[í·jHᵌo#\ át/Âq|¨’¯Æd.­Ñ…¨V˜|Xì䢭Th:Åô3W7§wûÅZ„Í…‚F0(Ђ07;j¥NÁÃ&ÀL ÅqŸ—EàhHË$âùD¢7 +é¤Âç I£ +ˆˆR±Ø„îG/ð¨ËwAÎÀ´:q¯àÙ`&Yœé­Ï´ú¥ˆ”ÃÙ‚qA%€Y&¢¨7 Îr­Ö×ÃQ€^ •pÁáÚ'<”›N¢J ÕÄp£Cã–ÑqŠƒ·EU5¡©z"*e"3ÍÔÞbyµÛ+ì/¶çëÙV&ÜHGsшËí?>l2{sÛÏØàçFÃv@/f++©l‡¥iUÀr:— +bI^keï_Ú}áú¹ýµùÙZÅP$ŠÄŽ‰1i¸ÉȤ_àâS™îùLë(‘™e¹p¯·¼îŒpf“— åP(L–³¹z<šãÅí$|àØ$tjÄ;j%XÔ +…,~ÅKt0í°üÈíÅÆ,ð„ÎÜ|HÌ(ÎF!L°XÝ^/`@ÖáãH! Ã(›d#Í`ªg ¿uÊjqÑÆh]ŽT¸P¡"0‚H+ª>»;¬ÄcñŠŒ`…bA„P&­È‰a——ˆ \zÂNþå·G¿õüøÈd €ÅPµ S0„ñ´’òKX H£A†V¥°LY,Ș ã£xû@Nõ¤D¯ÐÛ ¦jnB€8Cˆ6bÕ³zi æ#~BäX¥³0ê‚NLxÆÜn +é9!Ñ'Ô2,fh9W™Ù¥ŒúX@ñòƒI~.eƒ5?FIÍì€OŒÚA&BtœR+¨˜wã¡;úfÁíq/?î•&}’ŸIFMN4ÅH)œjR¼ÕŠ^Êð³ B«Ð¡*«W¤dGŒ7h%I‡Š&iÇBNëfRTt*Û9\¿ú¦ÑX?é -X”I.Ê¥*Ö‡¤‚ÓLƒiéò›Ñ17õ²Úè ©y*Òb# K Xµãº‹Œ ÁŸ˜Ró³ŒÑðq 95¯­SZ‘Ók¨Z$c]½¹¯Ö¶äⲜ"µBiî +n80…ÖÛiqÉi,\÷J° Û2snzó¶—O 9 $£rÉI>!ÉEi#ÕRw—‰¶k‰ë-¯TÈ Rï[ˆ›ŠÙPmÌCOxH'd´éÁbÙêåí>ž Uq­â$ pþáê:¸F°Ö„µ† 9®cJäš rÞŽÙöqIT-û¥Òh@;e!a>n p#6Ä]TŒÔj˜\ÂÐ9D*ZUˆOOxE'õ0Y¿Pr3iD̲9eÁΘQÞ YNúX+„Ô’_­ä2ïRzÍÏ% –ÕˆpKÍséÅ€Rsó9ÐÿȣŸêY±°јè”RÞsë„>X‚ÌMÇÁ ‘¶‹ŽØ…WÅt/RÛˆ7v¢µ ­°¨çf·ïFWîQeínkë…ÚúÝhg?Ó»Àé #=Ýݼi%Ã> B‹K/…k»©Þ¥äô¥pmÃà æœÒF ×ê`Ä'ï’‘Fcö|÷©×NØ0 Q +«±ö…pí<¢u\LƲ†M"RÖŽHvTö +~ʼn†|tóN*!¥g•ü‚—7\Œáæó˜Þj“w2 ;¬Vzûb¢ Ú;âf½bRª°Ú"c}L«sB¢hÔøX‡P+N,D‰N«…Õpy=\ZôH´Þf¢3d¸~ËFÆ&ý´ƒ·q%· ‹ù36ÌÍÄÐP•ŠO“±4T7#ê`#Ô䌇Ï ôI!³ˆém65‡FºV"NG§µÂʨ„¥â¦R YÀ÷ É™¨#dÜEDùDÏ9ØÁʽˆ +dS+­¡ZsÂE9¨IoäнçN™¿}Úö¼ ·‘)Ƙ’³¸V›t²f¯@© MJ2j§b.:Ò‰tÁ…N4vãõ >1‰…Êb¶ïâD¨DFÛ,B£*Ìg¦vøDk‘ÉhÓj›†ä‚…É8¥º[(1ášœµàaнt|ZÌ.¦g®t¶Ÿo9¹4ð!ñtojù’•Ž™€ ±Â&æõæÞ9¤c-Ñ(…Jó¨^¥£-¢¤Þ c­¼¦äç!5'e¦æÏ?éîß,UÛüÿÙ{¯'9Î,_ì]!>(VW»Ã!‰F»ê.ï+½÷>³L–÷¾ª½7è†! 3äc¸cÖÎîÆjºÒî …¤½éE/ú¤“XÝû "îƒ"Ôq£¿)b€î2y¾ïœŸÉÊüŽÜ¾ÀŒÔ{oùdÿé!Dæ‹“Úò‹úþ×íÓŸW÷ßã¼Þ·»g‹Û_VçÏ„çö.÷žürrõ³ÁÙ7‡/~¯´Ï­M~èZKÙ0%À´ùõ¯ÇW?6Ž¿w¦/0¥ƒÈUÀmΟ¯fåjRÆH­]¨Í+{ð¸4}œ]±2uFOôÞØØÕ§yÚÞÿJªBbTøâH¨îk­c£{É×NãLp³›Uß_\~Mê­aC^iõƒòü•X?eë'¸=KÅúä©Ý<ÚB¼ÜI³5Tíikô\¨¥ùf©{rýêÇSZM y€‹>ßýâœ7öîÿð/ÿkmr½–U£T‰÷÷ëg?>ùmûü;µ}§¼òø S¸`¬ÆÙ‚ÚÊHMÈþÊêÞÄ +Všp€bÒ”¹™¦»V³êî0å=µ{[ÙùêãMd=†çX/Í–Ó´Ÿãªy±™ *i¡©µ.¼ñ“f„ƒÝ·JˆÜ@µ®3zŠ*m\m‘Ö€-ïIõ“æÞ»ñí¯h·ït€é;Ï ©Š£K¥¾”ü™àÏZ‡¯­Þ_žÑřٽ®í¾­í½5:„=ŠÐv^­k­CÂêRΘ÷øÚî,`Âû'ﻇ/·Íö1à˜TÛg¼…Ô8Ñzç²?Û½z{òòGµ¹,¯Õêûí£¯†£õ¯íÖÞÎÙË?ÿ»ÿ©}ø,)ùtqÇ_¾éž|;¼øEmùQü:³‹ݬ\Áœ^RªqþÎèâû§¿oì¿áý]¡4ï¾eJsDúÁ[³#TÁ#³{º’$¢8çôö¼Á¡Ö:ÈÈÔœÊÕàzûãg¿:yþë5([TçÜEÿðÛùãßëפ·d­þûÿ¥>½MÑ%Λ-n¾÷»~÷‡·óòwrë4'55çÇ¿ùÆÁFNËI¥yUœÙ;ÿáàó¿lì}E™ÉÉ«êøb³ &ù +UÚ³'¯ÿ0<ûf|öµX^âzߟ=M1•Sý=©yÊVýª‹W“óoÀ¿¨•ÝÒô…X;ª»¢¿CèCÞ?ûÅóþà‘[v÷Úî]ª­cÊ[æ¨ ·9wÞ>ü +~¾šR⸧µŽìáUyö´¾ûEoV©tOÇç_‡ êj‚£`Õ:ùê¡ìfÏ0c²žQKÍýWßüµQžþ›O" ä™Ù½ížýâì«¿®‰cÖóŸý½Õ:J0^’-“¥ƒœXω à&«ÿ¸}ôÝù—ÿõÁ‹?h•Ù£gßöžòÅ¡V[ØÝÓÖîçÝÃ×Ý“¯¼Ù­P5fàs`AÙòJHˆÊDïœWw^Rö3:@v•ù›æñ÷ËïýïØòÔkì?ÿÑžæ_oŸ–gŸïÜüpûö¯n~ö£‹oœÎñøàÅàè¥ZÛ-On»'ï˳çç;·ßŸõ—¥ññ`ïÙâò;µuy‹ÛDkÂÑîÒè^sŹâOÇgoøÊ<#V¯Ñ¹±kGݳŸÏŸþrRoŸ9“'‹'?ì¾üCeÿYÚóÕWQë;µÝgQZ–jSÒ›±•80oòD¬ïƒæ\Oó¸Ö–ý¥P³µ]¶q¡tYƒwp™JyÞ­O¯•êœu:vï¸uö®4»-Onüù3|›}ÀC«e«|h.ÝÉçrç”CuùRodz½ûyïô½Ò:Õ[§@€x¤5òz§ÏÍ8Òèì<úÙ‹_ý·'oÿz|óCãà­Ñ<C}ýü‡/ýÏ ¦TNç´wòu÷ô›ÎéÛÖÉûœÔzùÍ_ +ÊhµŽßNo~3¾ú¡}üóþåo˜ân(g0Ö Ã•Ìî¹Ñ¿R»çt9˜`PkI9ÇÕ ²({ºš ×ÓÌj’Ï‹mÖ[Jå]Ýß‹£ú§›9ЄPS¤=CõiŽã$ø)ÿäù³$íæå:dB–¯‚êFµãNÍî¥?{µÌ[݃³—•Ñ "•Ôʼ>¾ÿä7‡ÏÛ9zctŽÄâŒaÿì} ×óŠ/×wýÙËîñwÕÝ7þò¥7¾‘ËóêôdˆÞ>).^º‹ÏE(wR›<›?þQïžÔZûàµ?}<9}óèËß.ÿ`uO”Òd÷ôËÉÙÎú£Ë›·}ùÕßî}þÛÅÓ¯ÕJ÷Ù›_ß¼þ]N¬¤yŸö¦Rmr 8{¡v®(gêÎ^üðNï„biüXë^Ã'£÷çïÿaùø7nçüâåo'¾¥+kr#v®©Ê‘Ò¹ñ&¯šï¹Ê®àö^|ûÇéù7ÁÝŽrw¦„7gÊ ÒÔ§Ï:;/N¿Ÿ^¼ÃÝ´¥ÞOAI* @ÚÉõwlªµÒèÜ_>ó&{_8ÃPb ~* ÈÒ›Œà³¥IqöÔßýÒß{×9ûeu÷+µq ô웿º|Úoâ*h°;¾mì¾.ŽnÌÖ(XÚì,¯¿Ù½þÆëÚïËõ}»sRŸ^]½úÝÑã_¤hOõw¦_/žþÚÝ7¼×„òÎí›ßÖÇYÒ îxÝ{^?±ÛÇzó”©Fi?T0¥â$Ï—gïßþÝøóßy;¯ÊË¥ùs¶r¼•·åâ|zü%g²´Ž +¥4]&µ6Ögÿa”ÊОà-rBm-%%È2ïíqÞžÞ¾s +¤FÓÚ©™fŠ´Ù 6'ŒŒèãfO³6¸4Z‡Lqœã\Áî‚”²{§åéXY€ƒãAö‹Í£‡1D8¤VûàËÖÁ{©qˆ™Ýhr£;Ø%—'¬Ùî|9½ùæ÷éowžý¡sö3®ºÈËþàô+ÂIã+µ9[ÌÙ©Œ.ýé#®4Æßi,¼ö¬>=õ†çPÝLq*»þðÔhì†òëŒà%Y¹œ‘Krsßê_;ƒopi÷s‚§–FO¾þ»£7m9ÃG@•ÚRöFû—o0½Û8|_ÝûÚ¾(ï#·o ‹"˜Qií_¿þ±6»þ4F`Z/ðé;¯¬á#«{^ìžô·¿ÿÛùöwÿ6ÎUÓbƒtZ÷f|ó›£wÿÔ»þ ØÕëç¿ø‹ú_óÛ(S¦ýc²rJUNZÇ?_~ñÇÊòe–+Ù#³}ôawß6ˆULëòÞ¼èôâ×'Ÿ_œ.ξ2›‡¬7åK‹ŒØˆR.0 (ÏæÎ+µº£À£uH»£ÑÑ—óËoÝÁ#Â0f¿>¹öºG¤Ñ•:WÞâ‹Òò…3~šaJY¦X]ÇÜÊ +1L§ì±Ð¼p&¯ñ3ÊbJ£sðtçÉÏ€MV3bŒ´IgRš}Qì_yÝ3µ<\tJý3Tj…ódÑzZ‰ ."µ*£Ï›ËwBqQ]ùPDL%Y ¢ÊI²,”÷œþ-*w@0 •Ô]ŠñÀÈÃ̳ŅP;,èý´XÃÔšXÅi‹+ŽJÓ[gxeu.AÕ¿&Ëóã؃sµsg\Ì»4c¼ àžZ;´»—`Õ |±½û¼µÿÒ])Í#¦2ËéMÄèzÃGÀò¥‰©M¡<—Êórÿ¬:¹4;GÒ¤õ†ÝÙC·êŠ?/S]¾€eÜ i@FŠþ>W^ÄHU(õÜÉeûàÕôú‡þù·QÒHšVN^¾• öˆ`Ykïyçðyûàh$ÖìËþ`õz†ƒ*pzçO~ +ÇlŸÁ7²‚Ñ8$ôÆVžÏpžÞÜ­N{‹«‹Ï¿+÷ŽÒ|Umœë­kµv 6,fO­æÉäìíe…jVlƒ§åýŸYãçJãXmáÖ ûÁÍwé5Øÿ´PÇÍamþrçñŸOßµ–ÏüÑ9¸ˆßþÍ¿¾É*={ü¤´l«^Ý{å®Q©±8|ñOÿóÿþê×ÿ v’r„ê‘=xZÝySš<6šze~þäû«7EÚÃœR/˜CÚ[ªõ®¼›Dn{­ýÉÙkÂêåÕANîÆÈà´ë jó›ùõ7±ÂZ=¹8³ZÅ׳ӷ¤Þ æEµ8[žCi½‡Q6”QA­uÞ—ÇÏÀúäžSßÿõ_ý÷^þòÁºc|7¦ÅÞM¹w ¼ÉêMµy. f5Ï•ÔÚ’²†Le¯`Žio^}îô;ƒ+0¼àÇaÝÁg¨ÑCôåÍ·Ç׿›ë92€$¤;òÆׄÙ+HuD©ÓV§2{Ây#€)£}âN×ö¾ê_þpüê/[G_”:ëö­îQ5rl‘A;­OX#¦8ÉI%»1Jãå”vŒ*Åé"h*pÖ _ {o~üô;±<U,ø»Ró2œ1;ÕñÍòé¯2B•ÐZ´Œ9€ª÷gf÷8´Þ«HåiN(ÇP•uvc§·¼Ñk‹õ´´––#¸ƒ¨àW,‘’§V&•Éåôâíèâ½7¾ ÊÈÕýn jÓî]ŒÎ~¶|ò{¡q™dꡬæ¶ìîI’+näÄPFŒ“%B«Í ­w%Ì,ëõ_D>BåzN¨‚ÕõG›‹×•ÉK°„¡in¸Ò`$Åzðü aÖº'g/~ì}+U€/°½0S—Y©‘á@Puû{/jÃ+ÀFÉŸëÝËêâÅèò;oòy‚­Eòº]ݾ€*¦ôWÞ³ÆÐéžwö_»S„/ï}9Ú¹ý°“³Ykೆ?›^©]@¿$îR‹›Ñ‚™¦¼(fåå†TYjþ¬ÜÞ%ƒ»ÉTÉ?ð&/ÒT1š·ât3YŧÝ)iO­‡k]»{¬TAÀ<ç‚þJ‡­Ã÷`Ç”ÆQ‚´3¤lç.”µÆÁ¿^cï]¤ãñ[®<ßÌI µ™W›luYÐ[IàY«ï .@Ð’f ¯µÿ…Ù¿Ð:'Z÷ØK̓¼Òû¯¶Oâ8Ñ”+ï‰ÕÊ™ç•N’÷¡B[‹[ox‘‘j8ð¾ÐÀÔ02 }^ vŒW‡Ãó¯Sl ¬»ÿ²:ÿd†Z;p{Ï)¶Ì;©8¶ãjBeG¨ïƒIÌŠTõQ¹bÕvŒú2Rà·s,Ø̵¤[´F÷’ÔÚŒX\ìÝÎO_p‘vtǘ5¶’ÊKÎÇ0Mr{n÷H«/¬&ö^FîÅppåÕÆô©Ö<Ø*HYÖAÅZw)c$U ú ‚ ¨Ïg?€S^ ã@" Wwtòíàä;±~¾’”¨EÙ“í¼’À­8f"‚êB+/ÜÞ¥ éôJ½#R«æ7×µ+VvìΩÚ80Ï0NexV=Õäý=ª´³M |t‘TÙÍsÞìv^‚p…‚BÕN°ßf쟿-¯YÍÜ®×=¨ÍõϾ¬-žFPÜ"°!£ö8½·²MoeTÊ9ƒëÖÞ+­¶—¡Ü­´TënÿR­íBÖ¢Dš°à€á¡6NüÅ—Zÿs­ ¦ “åÆlÐfU +9“¼ÖOŠ-H?Ö§ÝëŸ{£Ç@¦Îà–)Î2r#ÎVˆŠË~–÷€Ýê{oZ‡oµæ),´Ñ:b¼ët›óÛöþ9©’ËY¹ü~ôâ·WïþØ¿üNjŸ¥hL¨;y’“Åi®Øe¶NNžýjpôÂl aÖOP$èÚÁûW_´Ï~¢…ÕëÍé…ÙXzQÚT´3Qü#€k=É’Fõf,Mm/%Ö„‘*õÉVY¬„s¶3Ûy xVlåõ~Fn²BÙ«ï‚B åe®¼+5NÔÎ5ˆº,¼1b`{fW^o?EéÎÑETí[Gjã –u³ B­™@zg-%Äp'¯ô™â^yô¬¹xUß>Œ³˜Pm-Cn‡ J 3ã¤,)ø‡¸=ÁÌ~,/Žöž×F—iÆKÑE~ ³·r +gìæÁv–[ÿºÿ3„ÜLqþ4úq;'^ç4E¹ý“· 6½é­yB;sš¡KðžUBΧ‹Ð‚ìÎE€ ¬›ÀF¯ÍO_Vº»”\ä¼~Š¯QÎÌê\pÎ —Û1j;-”{§Íåã´Ék›)ña„Ô5[gFó$KcyIîöê"l%’UxopŠÝÏp>"×» ”€žà!ÕÄú)åŸàî5ú)Æ*ˆÕ$fr‹¶'róÜ~.Õ¬µðPÑ•guh£'–æ ÀÍîEkÿuûèåÍ£ØÞnqpQ_>ݹùvïñwÎð’pF½Ï—_k½óõ¼Dïön…¨…C¾2B´{ý£/3lVAk篫Ë/@nQt²|…Tð‰9ÆDø’XÚ±{׃“¯Û»¯åÊNš+mfEʒ𰀜àÁÕÖ@à@N¨GQ3Ã!·ã¸i´Î_Õ^7Þ;“YµÖÆlìt^ë­cr¸²K¹s©z/ºË ¥JgïæÍoq½B °á„5ÆôžÕ½¶Ï€g63""6•Ê>cÏ{ªÔNÀ§¯gÄÛ)Û¦‘V7Ö²t©ÀWygŒi𧡂–&ÍéÉËbÿ8NY²¿Sž>³º²?åÔ϶е8Í[½ÕŽ+5·Þ>|û軞ÞþJk®Eñ&w–Æd&@e 3A¤1 d„êfš6ä1zQÌÿ,BBÒÂo ¹*˜u\0x«ÆÙ >˜œPšÿézöÏVRá´DˆuÞêÆPq+Ë"b5†AŠ–ywÆ{³SÛˆs¤X!¤ÒV’Ø¡8OjÀ¡RÓ‚½2J»zëD ˆÕ$]¤1T'” åLøÒŽÓ{䎞Jƒ(a¯Äè4]D¤:¡·0£#×öÎ[œ¢J5ž§’y +á,Á€¯¬ßÑü²súMFélfy”7U·¥»RiÀ‡à :_ù·õ“/‰Ê"'7åÒ”ðdÆ›6w__}ù÷g¯ÿ¡uü‹HAϦQ[Šþª¶ ÷¸Ò.SܯL¿0[—y±–Deµ4Q*KÖîGÄÆfFâ6SZÖv^í3Úé×—ÍîIš¯²å¥–è§/íØ™ªìuvn¿~ÏëmÜ›1µCÜ›3ÅyÿøMmv‹«u«¶¨ï<Ö–ëg¬²=K³Úøzpð¡Ö)½íõ¯A|:Ý3¹zP;@m)RHqÇQpŒ +>TožÒÆX*퀈á®Ý>¼I$¯fÙ +¦äÆ™3zR™~Žë-ÀRT®u¾`½IN¬cú0Œ!¢$aC†Pj}=Š VÇmî*å±Õ>AõÎVAÙJ³’ÕÖKÓXpÓœ«V÷9gJéýç§èòz’_O0riF›ÃÍ””êy¹M˜Aß±¼X6~O †?ö:'ZìõLj—–R„—@Ì­¬¼'+ƒ“ÑñD¨"à á,pVjñ‚ºg>ÙÌÅ +zw¹±ìÏI¥IÄ fŒ { +Œà3ø¾mTeœ1؇΀ϷR÷,J{Û¨©ýaÆ1WüúìéÞó¿h}Ë”[Á͉*.Ø„ê“fSòZçœSÚ L(EL0V'Ô*_šå)H#0Œbû„m‘RªKL.g'àôþéîõ·‹›_)›$U&¤m´3¬›ã+Vû\©†±úùòA’.'QMñ†´VÝL)ÊdIšªàjGkŸ@-£J“1šµé%®5S´GÛc¾)÷ª¸xÂÖ]t:þäZ(Ïb¤%TZÇïf·¿ +¾‘Ü{Ã8#ð8Íá™?¾ˆ‘ãîøóW;7ß?ÿóÑÕwöà2CÙnm±wý?ºˆFÕ#ˆ¹-——¬Ö#?œ¡MÞÞy ÕŠ©=@ƒéQæ8¸Ð¢zR0M¹õñmcù‚¯ì1Å¢t¶ ¬#­ƒ–x 4AZýòôsHéBPìåé@†lÆ)Z®0j…RÊåþ¹äï{Ê2%ÚÄ1ëA”Jnš.me¤çsö„Ô‡H°§´ ´•ãkÛY¸ N¹HWº +³eb·Ž¬öi’®üW3 ´R$¤´•7ãt9Á”A]Ô&7º¿“‚W¡V¯§AÑqµ­$¿‘úÀB@ືГ@e°Be¯8||øìûÏAýFmž…Q  I,Ž9gÈ”wiÿ¯×{Ý:ø6è7DéJy WÆ€Z¨·³IW7r†QÞkOŸ‚ûH3*—p£ ®wôÎí_E0i3KæEO¨,%‰H>€?owëóÇÍã·lm¿ wÓlU:´3Íq.TÑÜÓ»Wp`à†ÀKF +2©µäê!®÷ƒžƒzÇèܽ§lñWš›)ò“­Lš¶Yw)˜qÔ&µ®\^°ö@¯ÎµÊp M»´=yC˜}¶´cõnK“—¼ sFäpŽÝ»þ¶2¾&dåfœ­¥ù:®÷¼þ¡5 +R­<ºi¼nï<®Mo˜â4ΕAKèþÒïŸÉÅñfšMàf1 +R‡0¡¬²¡V¬VžÖ§×IÂØ +¶±²wÚ?|;8ù–ó–¡ŒŒËõƒ›ïû_¤˜¦õ€7õƉÞ:•üÝàj„4¯V—nÿ4-T$Ø(é啸gÏ0®D EÕéLÏÞȵÝ(U| j7ÅÕÒBKo_F Úf”мquðòy+«|BW"dš­øCj­%Ø,ãéƒâä d/i©Åì͈Ï&iL¢ˆCi=¹un à¡øj·Ãy3Ásj¯Ø:ÜŒ?YM}¶‰áb )ÒNSöflQ#F: 7T7Z¥áesçis÷Ykÿ…Ñ9•ˆ)-¾8aúltñËöÑ7ååkP,  +b‘0êY±(TvµÎ-ãŸËÍǵٛöòµV;ˆâ*"–8oˆ(Õ‚X-‘|©<Ÿ_ý°ÿüo€SBY!*ŒÙ&¥Ý‰Ø:-ƒâäE}ï-aMrl±=½´›» ¦£¼QÌ à ‡µÙËÊü ¦ aÎS$è·!WÚ!ì]ÊðU§w98ÿ6v€«²ÖÕº°^)¶µ@è=6¸¡²ʲ ÆÔæikçÅüö—¨1YM‰¡œêŨ.“m›+.ÌÞÓùÍo'W¿$ìÑVŽ%Y_VÇyÖµ`éƒkWžƒg¨ÖUó0NýSŠÓÍ$·g FòÁ™ÃSgø’ÐûYÆÝL1)\ãÌV“2´‘ã\Æ3Ö”Ô»9¶ ÃÚ£4S‚_!*H¯z TÕÚeíÎÃ0Îj§uZÜèí³,ëg¡-ÁjŠV=GkŒÖ¨tϼî‰7¸f¼ENé>ˆ²Û) .©*m&À4TjJ¥¥ìïç¹êz‚ÿéFÖšµÑ9&•Wcô'[ä6æÅ}}òee÷µRžÅs¼W6OÀ-þéj~-!l"vN2þ™Þ¸ŒåÕÏ6ó¢Õ›~ù „ýéÃ4<Õ†jë4C{PÅy¡„)u±4VÊ#Ñë²V­À»É`+­¢YÛwûbyŠ)5x‚ÑÛJ³%@up˜¯âQëœ7UêÇ\åÊŸ2Á†T’ÁéÁ +_^j­ 0Ô½`YwL›}»sLg›˜µ™SP©n4öoì´ÏüåkÂaÖ(-5¶q3ó`ôòj;”WYgΗ s‚HmÐÆQÜÙÈ€>)„ +¨‹í —¤Õ‹rù€s—œ7‡Ê ®´2\5„˜ˆ>Àí™èƒK²•-DYMàB”ÖàEäîÌIwÇot/CˆúY„TXî,_-È Æ›ãÖ½6}âô®øÒáŠ`Çp½¾,Œ»ë9ŽÛRi윫1*8ª¼'=ÒšJÕÓPÁŒá6#"TX½¹¦ƒÝª˜ù*ãLÀAQleÕ,_ÇÅÍÍ (]±rPš~š!NX+"šgû/Êíý,iÆ +j‚°òŒW`\xlÇ1œ’‡““ßüÅ¿åúvNÙÎkPŒ´»—ãŒå¸H–Qý9cv"y%‰;ÛYdˆ(Bi¢\)’fây Sz¤3£œENêo$ØP‚ŽdXN«&ÿx eµ\‘ev¯¸òázÖøÉ +šV»JqÎÉFuWò }œ ÛÕDÈ>kõÌÆ>©Ô‚ÍOX;>¥ÀEw¬V÷¬Ö‘TžqNŸsÛiÖÚçeDBiƒ¤aÝ!®Õ0ŧ­ëŽøÊnžo bƒu§jó¸wúÍàò­s™ lx,b†)¦ŒõSB  )ØÑ«}$–¦zmÔù¼ÖMK-,Uû‘P?ÎJÂé}êÃE} E!gBY5MWÅò®µW¶ò[q’1ÛÄ õQNhoì0êäÅ&ãÎS\”SÜÅí9å.Ô÷qŒúiŒØ*H”ÖÎR6ÂØJe"G¬SÅ´Ü ! +(®¼àŽeÙ²R=´û­öÅøôçVû R‚×JiÊ°i.ÅVq}h4Ïzï¸ÒA‚,m$ùfDm#¾BÙS¤{õ~3V£d¤ Œç¥*øÁWÉ+ÐðBi_oœ!j÷AÞV€%,”jý(T0@±r«Ô< H6šADrÒf‚ %…dAÖ¬–ßÚk¶RôF‚ Nè‰5¾¸F¬µÿ“Õd +×Í@£î®E(L¬„:)&7!»„OVSW¤ƒnzÞv^!V1b9è/K{ ÂYq”Ú©OosB9‚¨ðχq¨Ž®öáÏÕ¸àÖw¾üå?YÕÝO6±•¨T[ Lbˆ”"MÇ@»_¬†ƒ¼ÂÄJ°O¯Ñ½!kõA¨þÌhžåÝà” a€ÎqÅ$¡Çq8‘ª ˆ’”‹Š åîàª<¹qç¬7 +6aSjQTƒÜŽ£®´AÄ‚7áK HD‚Hýnå„jiö…Þ{ªvžH ;"×AE£JH¼À–ÍÖµR¿ä¼]®¸KZã`ÃØ0–Á ÁîÅPC,î*Õ3ÂœÁ;ŠÊµÓ ¯ÔºV^€hÉð Ìš'ù¢ÕÚ±V=J :κ“Ãg¤ÑÉ„Û Ä +:×+ÕØÌ$e­§ÎÎIB²å¥F8ðAT +âåÝí<_@ô¹pš J«)e+g RGõYg¼Ò¨ ƒfNs•‚z ¶]×Ó2¢öÜÁ­Ù>EÿüåóËŸ!Ú +´½cÊy-Š[Pžá€¡*úû„lb™Åm Z °•0Mk½ éB +å™".wµò®VZJ R}5N}hm/? ç×#ÄFJÊŠm8B0@"æ<Ø&ZÓàÓ•(»çÂby d›ê p¹õgk¹Ÿn iÒKàÖfR|¦áÏ8î±ÆHt'9Ú[‰à›)n3-‚*#Ôv–©Ä;…[^cٙ߂€ßHi¤µ"!A—b¤›dýå}–äV“l¤ ÒÆÀhrî(Ïc8 +Éñ¨ày¹Ã'zûÓú„ÖEåæVAûd{g⸉‘—êqB „뀵Á>ª¶I£ 0a­&¹­œ ’'Uò<ÀQ‡1‡ #ùâ  +®u´æ‰TÝônVð¡Ê(À­§¸8¢Ðz‡µ‡lYÚÅ„`7N»y*Šb+-B€¸Ü¢DÀUimJë'Âo µ® fÕ>“–jõÌBpº[­ƒ#j†¯èÍ3¹~ŠjpâQÂ~¤b„ƒ*]0ÚÑ‚uÅŠQt{-”ÓW〠²ÙØA•èvNg½]¹~!úÇJ ¸r¸žæ€é¨¾šá cÂ8s¥uR]'C@y¦HK/ž½û«ÉñË WÎÊÔ#Æ +×(þ^³1±l463`ÐórJ_ðÏÔæ5H#FªæÏ2„ » `ŽXa¢dtu¾âÝуP¡Ýånçdð›oëIáA”K1 ©v +È¿e3$ptf*@XĬԎ¼ÁíVZŠæuxäh_p—Ì)̹Êɬ3LSΟ­eCi9ÔYk$¸c endstream endobj 29 0 obj <>stream +Jë$Qça˜ÛHª„6H2%š!Úy­¿M8*Áéh­Ë8c ~JI•£¼ÔNQ^ž/áZKðæ¥á­yžáš ª%*i¦¾™Õ"¡„áJˆÜÿ €XïêtdÞ>jH3•|J†ÝÎ @3¤™ç<ÊîŠÕòø‰Õ»m“ãJ¸\£”j©{XŸÝ³gå ï qÑÏPˆRVkà€’‹KÅ,ãd9`ä!¤J†´¶R)×´òœ3º¨X¥@°I-0zItu4O ×I­u!U–DÐËlÊ™ÐÛbyžbKàÇÙ@À?Rš§Œ·‹àˆ8fƒ=`Œƒ®6Æ”³Ř«\iBó ¨UH½<µ‘bAƃӯ/^6–¯2¼Ÿ -«u E‘¢,P›Yi;¯@δö^¯~ÁW–©Q=•§½n†Q Àm5!àR]*·?øÊ 9”Úw¼°ž‘Vãt†tP¡¶ÁAÝ=ÌJTKµCD©ƒZ[‘P\ðÂPJ ­nÌ©—ã[¬='äújÛˆ1 Ôú4„Hb&+­}µxò»«oþ;§sóiˆˆçµxA_Ò@Çëñà2{TnûÊãÇ”ÙßLKÅsÞ%ˆâFNßļ*e99}÷æÇÿ!®Š­&QúZザ¦@ºus|5Ãú´jjj!Nç4(³ J{u€Y¸â_<ØFlÐÕQÔMºˆsç@¯¸ÚjúÁ²®?ØÍ9=äÊ;°Ð± +MLjDŠ…6:œ3J  âRHB3/6â„óÓµôñ£×ÒÒfNM€h'ì8Ì°Ü5ZgÎçT±¸‡ÈàRË ÌÜΈŸn¤>ÛÌBÚ¸¤(Tœ5¸Ýü¦vð®4ý\¬neÍב4­ÏBX5_d¯<|,z DnËCÅ ¿RÊ» Íâ´OVÏê‡ï.ßÿñùw<}öC·2`ÄÄFAlRÖ(I—@3‹ÞŒu¦”=*(Í킾‘Õ·€L™*ªvR O¸™«‰N?A§ˆƒh9Þ¦X‡2ZvïViœËAãË>¦ÕÀG'Ù"ç ¬æAÕ@0Kþ[œ‚ÖJ^(%=Œáœì/¨?}˜Jâ&k (}#Ëkiy--®% ôOÃ8(“$é‚Á”.eMqm벞d×bì<ãÛA3e+ *ýC›3LìÞn³igÌçÀì`N?ÑJ†ïPzG­LÕÊd-Óèi²®%…Õ¤”“ê”3¤©\Ù,¾-'Uåògvq¶¸æLru¨ ¯e$x„Q“³GFë\ï^ÓþAFîmƒÅH×ÄPUÜ'œnN`!€—!SLjd» }Øq´‚·ò>¬f|Ð`Ñ¡ˆÀeÄ:Håd&(ÛÊèÙäæ÷•ƒ÷9¡o’ WF!dÛ ®õ½¦Šó,XP¿ˆ†Ð;ÏUóB-Ï·gŸ+ŸÉµ³¬PßÈ€„@¡À½ÞÙº¢EœÑÞ$1Jk=L²`ü½þ¥=x’Û‹Êø´ÏÅâ$MêY®È:£ÞÁ—ÓË_Î0LZÛ¸FY=Êž„1+ 5•Æ¥Ö¹e‹û1Â}ACi6M{2Á¼TÊ3çy¥bÕÛ[i¾À•Áì·vžØ£G¨9O1pœr’0I­•$Í8vFæÝIeöÜ_¼ôç/Ó| „.è¢P:H§‡Q2”‚縌3HÓîV†Æ7â $v–)~²™_1À´9v‡"¤ó N¯‚1Dõ$¡m—ôëIþÓ-2;ù€€Ê ר]àj¸\ù|‡˜3¢x ÐOmœB¤ŸE¨¼P·Ú—FçW\&pôíJœØH3ïÇ2Ò'ëùHÁ´Z'Ó«wîð4«%_!J¥ fÐð´»pGOëË·I¶–:y€J¡¹Ykia-Åç¹J±{ÙÚýªsü3y*˜« ð|wÝ’œŸ‘Û´·Y*ÕŽÀt€$ƒòYMŠ–Y®Æ{ Õ`)7³2°ÏzšßþW¼úp¯ ÈTD¬‰ÞT¯¡ÄÉ2øn»}Å»Pªå"°)ÕvóF¿`”Öyëô­sù‘`6TÓ8c¨TŽPµG¹sˆ—³Ç Û@çoeaVe8*­qÄzS8’¼ØÊòMΙbJ3Fl•4'¥éKoü9í.³BÜ1*ù¬ÝÕª3otIgÎð¶¶ûª¶÷aÏr¡RÌƱ;xœ×F)®…k= œ‘eb…2tŠ‚ÕŸ•¦/†W¿aýC€­Ø +©õ­i¾ÒX2¥}Dç„à I’òÖ“Ä+¨_dÝ6®¸"ŸÔ¤ÒÛÊ(+Q2’WÓd1œîs 02h{L¨PÚŸn¡?y˜¤äà$O‚pã¤G9S«s^ÞÒf+CÇQI«Luïãõ<€?ØÛ-9|'Ï7@.ö®&hTîþ ¥½ í²1MþvÎ⬸ìí¼öi¡&¤G’mý…g=!<ز¸EÊ>"UÓB s—ååW½“o­öc ¡”8gX_~¼ €¹P,i/¹ò>pAÑ òlÅ®ïyí#¯w s»™5á· ׃µ…ii±ÉWOŒî_ÛG“‡x) ñL¼!€lN³bþ¾šB9)A9Áý õ#©y¦4Ï(£^ÀæV2Ç´=oN]JV­Fp´‡ÞèQçøíøñïèòÞZF9±•f9­ŽK~ Õ¶r"pèÊþ1çÌ×bÄFœ N¢X\oÅ)0M(ï³Èõap_ÅŒØ7¾¸H p[¤³CÛ\®ãJ•Òr}Ï_/¾\ü\í^¥øV”(b +èœI^¨äãî=+»¹¹M€Øf3”-z%¸^w7«öÒ ´TÙgÝð)®7«óGÁyĆÇb¡êÌiŠ*%qk-Š¥)èí(Õ=¾¼ŸáêQÔ  9Ë‚«¼Zá°e©¼Ãx‹¼ÒIåÕ`⇑Û1D ÚýPE 2¿w%UöÀØnd(ÛJÿR°†òI&J(Í”ö[9Áµ~23„me”Þcf s{ÁXsDìˆnðýÀÈfJÈPEP€uÛ#%´€\Ò„›'œx)8 ¼æŽžÈ¥&·¾že‹zuÑœß$ 5Œ[ Ö/èƒmÂ[Ϫ‹.·@äd3¨ )*OÒt#‚yð+I&#Örr;ÅûQÆÍi]½{[?þ•1~^P:PPéqª¨TJ£—Lù$'÷#di;¸v¥œfܠż>>…i-Gè]¨GðJ¡¼6‘[z+¸ù`5ªÓÛÒøŒrZY¹–ÇΙâ^–u1Éß*H¡Ï—(i)”QÂ9};«déà냜T¦ôjkÿWY€ ̉M@ÀÕ0b°Þ7QÂ.ý*.ÄÚ1iŽb„ ¶1M[ŒÕáÝeÔAìa渾ûµÜ¸„£ÍqÕH^¢ìaNªFI;N»bmwpò®½ÿF­ÌH©R`<÷ÅÉ“¼Úƒ‚ŠâE°9”=+OžªõÝíœÉ*)¢É ÕZЇa¢Ų–dP©*—&qÊÆ1¢Mhg§¹óÀ¤ 6r¬¿™1Á š¸Zpû]A•1ªMgIòñF~3ÍãjÔûzF͈-køH<µÏ@–|ºM|º…}€ùB”VŒ.'AëC¡tiŠMψX¬_r¬¸‚'+ÜIav$l¦K±µ™’×ãB ©J;¸wC¨Ä0ó³00 x˘CLie˜b´ [µƒ4_]-ÈÛ” +në®ä”ªŒÿÄ*ÕÈ¥µ•·&äp&¸ðFne¤:(OTïâƨ8 ¶"—;—1ª²‘Ób¤64ð²¼GH:ûtñ„´ð>)ÊFEÀÒ†R=îÿ\ï^Î$†Ûë"Å%W¶ +È­uã^òþ2l !€>É‚¾R[QÊYK3ÜfKÁMUy±òÓò BB fÈRðU#S +SżÒæýý¼Ö}çÓt%\0Ãy+ËùY¾//(]µy™‘:+)a Šº¼#ÕNÁ +Å kž˜œÑÒ* ÜèÖp³`®$Áyé5 Ë貑dÑÚ{6¾x¯67ó¨ˆ(bðÖÈjßc2©¼§ÕO‚kÔ‹ËÕ$ó§+ÑpNÆÔTM”.‘ÞÚyêNƒ\¨ÜŒ¢Æ§ÛXQ!I@ÔEýa”þl›Ç †…¶†Á傸c·N>\ÒÀnäL ‰Ï’", ¤P\ðÎ>Ó:QÊÒµµ‚»–µòÒÐ<1 ÜL`†\Ûë~µ¸ùÑl_À2­ÄØ@?ªÄ:3õŒ‚ëƒý—ÿàïƒ#Ðêk1ŽPûNû +jö¿ø³íŸ¬æ3l½qþ›öÉ7¼7ƒãÆyÀÔæVNÝÊ› ò +@Ùó£Wÿ(×Î {¼ ºš.EÈbœñÓb7/¡vR´/•f¸V 繬xÎÌRuÖZúÓÏqg´(Je>8~Ÿâ«9¹™‘ëÛTHdåÌ¥t…¦Ü‚Ú÷F¯Ë³wrý,Í”AeH‡5ºJyÎz£æ>ÌZI¦Á{Çfûëí¯&…­¼ÜÙ{¬4¯RRcÑ6²ÊW#¨þ J¬D‰éÊÕS½sCX“PAù,JÅq¢ts0k¬wuO~éï½£Ëû[L ‡Í´²a7‚‹Õ½$ã •£ùã?”g¯Ã¤‡èm³}®µ®w‘dŠk9y-§jW*.Dž)Æ/H-½qª5/l38CÀ¹žæÊ ¦§Ë`rBjJ‹ Òc‚/‚ÍàŸ›i\<bÉìeø2¢u¸òÙ¬6Ï"¨ý0Ê®ÅÙpN ¥¸hAŠäÕ0þ0B¥©"ïÌXwîo-AAn¨åˆùÕ(Cè“ÆÑ7Bí˜2ç¨Ü æÄî—Ú; LüÉF&+÷ ÷@i=.O¿¢œ9DýIQK³Éù{T©­§Å?ÝD×2 +íî€Zƒ¾ÞÁ7ÂRiœÚÌgq.ɵPeX¿èýd³I$¿R@l<ŸD +hA ™ÔÊÎÿã“ +™x.—Ìä2É\2YYOBàgYEB6ŸÏ“²^…¢©š,þ_žôû¸åÌ1-?š~ä´û‘ûº²µ½Rõ?:øq¥‚¸>L4[Ž°ý+7‚Éø¿æ^±Ë%„Ñél0òdn¥j}ÔXù0‘Á;Ä>ü Íý‡ ìJk%L­¤’ùàåÓÿ¨¿ü÷/ú÷ÿ¬dàS…‡›]ùðml|8Â`展|M§>ªªÿY†-GP=F˜yÁ/Hµïg˜J‚òsRÑú9¡ž¦ŠÿÚð›µF¸ÖE”fœrØFƒi¶”eË ÌÀÄ +©·xoL9#T´~Vîâæ,‚™ë b=†FsBŽ+âF‡vg„;GÍqVmÇX/TP9§G­]$1ï-%ÿ(¯tStµ Ó|=†;Ûy5ÍUórQÚ|y™à«+I.èÖÌ”àà·²r’p)£/ûû¤3Ϋ­mLO2^œ¶rRi‘²bEð÷¤Æ¹Ú{B–ö’|5LÚÛ˜!–wõî•Ø8üµyI8ó´ÜÜÂŒ¼ÔÈð~Œ°7rÒF’£JŠ4 +B)'TÖS,|Všñ·Psµ"„“jp`YÞGåf–¯¦Øš^;ˇI®B͵Œ´‘•#˜ÁÚ#Rïnä휄ËmRïçùz8o„ÒR3 ¹iÖà“„ŠÍÍ”ø L„ózµb¨Åœ‚ÜÍ+½¬ØÎÍÕ´Êi1ÌI’ÅYDļU µ3L5Œ˜ëif,†»ÔŒ ¡t(­Ÿ¤Š1Ì]ϪŸ„‰OÃÄj‚ÝH°)ÜÌÑ^æ0¯†Ò|ÕV£øOC¹Õ$³•WàåaÄÊ0µ4UŽãn(#Ã*äÄz3W£D8§l¤ÄÕÇœßÌ0õ‚ÐÆÔ>®v*½sÅŸmçøYĵ[œfo+'o%á#ì4ÓÕMrµPÁ e•‚PçÝ)kR˜%¸“‚\V¨ $HUû˜6ȳ%Îln¦Ù[Z¢.1c–•û9eAo¦¸R´K»S¥uÊ–w Ö±¦`™Ê‹³Ÿ»ý³å$H‡²çzç©X½ÈI(é…ƒf™zѶ Êv!hבb+ð«YŠÅŒ´š:ð…5èH$U㔗⪔=U›WÅÁmçàU‚³Ã¸J˜¡vlŸ‰óœ>Ì)]DjM¿®-_F™b(¯ÐνÔÚ·9u˜Sûqª3ì$áÀ›hCÍ/Qb%J®Æè #,W…ÙN‰õ]ŽÓ•8UI°µ_ }蔓»ÛhÐ8v» o´P6èC)¨ÚãÆv^¥ÅHV¯Ò$R +/ÚÕC­²yõpßHp#ä§!t3-Æ ‹¸¢ Rl+IÕeBiãÍŒCÍáf¹ä@‚‚ð}Dî±Î"N:oþl3 ™‰I-ÆÑÚ0Ž[Ÿl¡›I1’ö1ÿx#ó0Œ¦P#Ï7 b+CzÂ8F·×ÌFš‡#öjRÙÎ¥q¦’×G ÊÝJs‘¼ò&!J%qW,È•c±t„Ê­¼wóúoz‡/þäA„´ÆAoÅÞeAë†rʇ"r"ˆ¹‘ƒ±s*”m)£—¥=„.ö^ÓÎ`³ ¬$™SQ—Rý<Ë–Ò”¹™ä>tE¦Ùjœ.G©JVî1ÎB©Z/ŽjŒÑÕšGtqœ•«9¹ë‹êC¾´l.žsÞ4I9¾Êw™òANîÅð" LQ’t"¨¶'×Sä€[š­ÅI?Ç·S´Ÿã‚ÓC91M9y±%­[æËûåùÛêÎWÞô6ÆÚ9¡L;CÒãÞ$«öxÿ¤8|f4Okã[­q°4Ôqg +Ø…[“”ÐÊ*£tð¶%LnSÜZJXK‰)&h ²•‘>ÝB×\Š*B&¯çÕ5ĈÐÕÛNó]ºt˜üÕ óLÚóP^[M +ŸDèPN…• çå(ªLÁŸ1ÌŒäÔ³¡Öb ÀB4¯3Z“š+’ +~þÙ¶&Dð­¼–b«„9CÔ ªŒX{—1çá‚™çªA'9²'JY¾æZ¤5WjgiÎÿx+¿#q²\  ; ˆW0?^Ï}¼žÝL2 ÄȲÍ$S N´`åù)w+pÑ_MÐ+ q=£§é + šà;qÒM“v·>ÛF·ÒBŠðP±OÈ25ThrP¡ƒÇ°ÄÿåJÓÛRe,¸ÇÔ¢d)A—72" ÿö‡yHs•8SŒÅÊpõÅXÚÌ)[ˆúËK9uq©Oš3TÖn&¨b–ó°P¥é]D ¶Îç«ÇTi™ªBeO(/0£Ë8ƒ¼ÑR[­Ý÷îèYš÷7 êfN‚¸Q?’ki6§FûV©]ÁÔNÐ/Ãl¤Å‡In3hqíF1;M—!R`½UŠ³×A”‰à_ˆ=°÷{¶ž‘&øÕ„˜e«é`kx=†›I²8“æDYÙÆ·ÓÒVZ܈3£T³3t%”QÆص„JË#oä×l³¹³YÐ72êfF eaÚñ ³TÒ&„èðÛ$ãçå :@zxˆ´™æ ±Ãˆù-貘ÄO·°¤°c>Ù(Ħ + /\°®&»ÞêCJ¯&ø´²ž¶rjŠ­cÆŽP>`œÉv!À½µõ0J|º™ýl ÍÑ%·qLšãWßâ"Âð¹ 6( k?Ó~’.“ZýH€HOó5D2îŽàÍP¹ž$,·Oãd1ŒhQÌʉµ 5è± ÅE[¥¶g¼ÏL‡7oáJXvÆ€Fe÷Íû¿]¾²N”²rj3!Ùöµú©Ù>/(AÛ(±b¥˜:ªM)÷rHk‰›¨Œ¯Ðö8„Ú@Ö[ˆ¤+”9!¬)jŒ•Êþøè]±¹‰(Y¾Æ—öŒÎµÑ»5z7´7•½ù³/þ‚­NfE Ú$ÛHru\íÓú•šJÉH°Ü¹ %§·Øðç^ì$T+]þx+»ž„p¬h-ººž’Ã'Ž—}ˆk­ŸF‰Õ¸AK˜±¤Ý=Ê^¢ê AW AQÀҬǩ‡l-NnfÅ,ã#|áëIÌÝÎi[Y˜e#Á„óêÿÉÒ{0¹q_i¿bƒ,‘œˆ™AΩÑÎ 4R#çLΙf1IIQ¢²¬¸J^ÉZ+Y¶œ´–£,ÙʶeYŽëõ¾ïݽU·î=Í÷V¡¦†C ðç<ÏïénLƒ÷éa=9a&Ì<”–ÞT;ÅQ>¤óÜ2jÖa*«¶¡\•{T»É‚G ؘ•†áAÑz¥¶‹/Ù¨ ,;t4Ð2,¾ÆéÒxÆô䘞ÒÚ%šP[8Ö"·NØOyÀ¶”ÛÁ j•›;öÜÁ) NdÂãP·ˆ¿¥âd 6;<¬qRÙtø­è¨™ïöA¶ñPÓÆdÁ=aa_°ßB&¸äl²s‚NÍ’ñé@qÉHÄÆ­¼…LBùA³1j—háKÊ}£þ=PI!>m¡#F¼ÉJeA…Ü|ÙàÂJ­‚Ñ ü,Ûè¼ë&˜áÛ¡¨\#:|ÄHO¹ððO+¢wÇÇÍÊ „€|Ft`<À쵃Lb5ZðUµ38 Þç +Y‰”Oµ]€üQ¦©¿I¿ðj+oÆäI‹Omõiì`‹!$Ƨ”ç„]|–*—ênN¡P­Ëo§R¸Ô€54xÂãFrÒÂÜáq U:P7n @ î¨Ùó´²ð3€–ÒðF&Tq¨³¢QŒ/ŽY˜#äȤsxÊ£uúÁŽQ_lwÄD¢<þ!µýŸ†tfå6QIèúYBýU4PµÐ °x+!„B¬¼FÅÚàzx¨e +fZZ&‚9Ã(Ç +’s<Íë4brobÂ2â ·Pääi,TU9X+sòYgyåö®PWAá=2¦²FÏIÇ'ìø;L_ãôÁðàÌ_qðYñ‹:®C$h1+3áÒüù ,Ø´â 69=…°àSvåÞ±SvÎJD¹D–»¸Tæs³v_ÞÆf]bÅ€'Õž°¯°(ošÈ„“ÍA²sI ›ò„À%¹äŸ[trYà4$ܘtûǭˆ™ÈÛÁÇg´€ü½Ò”Ë?b U‘Þâä~ »ärz‡*, ‘ò¨µ2ã^ÄLÆãµ=¹}Êî¯B¼âÓ&"¢vxP°•62€”ç+ ë=S‚…ŽhœC°Yz TΈïùÆÌ=*é\’ξæ7rã&ÀÚ´‹NOÙxÀB ë¼PŠj;RpXIOA„¯$L¹}:T‚Ø ›¨$A93_mÞ°ÆÆ:¼!¬®q °Z§¤È“3¨µ G4n'3ºÄ eöÈ:WÈ€„-Þ(äè Ðd´±À<ãfê_eâ@ tN øÔ€N˜qƒ FK@¾»mÜväæÝŽÁA£H©¾züQƒ7|Û¸e\%Þ›°B•Ýñ0’›äûG&ŒSz‡ó9½!“TªEÔ¹ü6oT./›½²Õ³bQ¶#0fòš<>2PòÑ“fÁàúÿogîæ2ÊW2žln9ùô!µgÔHŒÂF¸N¾,nðPZkíŽa‹wÂB™Ñ° L¹ÏzJ V£™Acõ"¬Œš™)‡2 Ê0±A¦w!XÝ5Ó +5ÑÉ­7¬F$?§¯DD§¥-6»(Êíùí+Lª{ˤcÒÂñBŽƒRăWŠV6´Þ8`0€¤{ s2Ø$Ã-pR'Ïv÷m¼|˸æh"ã ¡.!›éKõN!Á(zûNæ+ãÆC6ƒ‘€¿°á´‰€0(Ý6aÖº'L¸áf.†j7PQ,-7ÕP!-˜hÅS˜¯Q Ò4à(áWT¶[Ç-‡'à# \àn€µ&¯GƒZÔ?bUôDme!mBB§\~*Pô%š#òæa±(Ä%ÈnÊí®ŒÖÊ;ð-U¡<ìÞˆ‘À£'Ì´bdÊ[£Å%;¬v÷A;˜Ñ8€0àp”y}iøf¶Rï×ãðÊãFfÜ̧•§ÆmÔ¸ÖmsÆ Ø‘)7¸§â§ö Ñ÷¥T¸:2e·ºX§WÔÛ™ÃS®qc§x ±Ý}ó¹J46Ó@˜ÀÀù*Žúr é£zp*ÄŠ‡aaÁ7íxL§êl +DÞä zVíÍ„ É—”*P<.ÎN€ct +2‹J˜ñ¨É’³33»wÑÖÄ|4A«T´ë/,;EEs”› + +#ÆÁ$üé›Wîtèô³õw=›h¬é0¬«B5ÓN®NFgñP—Ë­Ø‚Á%üu0ô1` , ï ”ìDË)Dãr°Àº‚Îã³Ð13·r*?úòÓZï¶ « ðñ–•UfH‡ßïKöŒ¡ÍÇ ^•Ñ;i¢Ur"¤Ep4—&ýyÐ"e¦jב ǘ‡i\þ +7n¦n›²¹È$å¯Vÿãaõ‘ ‹ÚBÛˆœƒÎ‚JÃsF4ˆÆLO@trk#ºYÛ´Ñ›€–¼mÊ5¬ÿe¬Þ¨’8IÑ)<¾l$̘dFƒŸ ¤æÐpÓB' hÈ ,,32yÃPTn>ç VÝ„µ‚ —¹Ã¥gìLÒN']|ÖJÙ¥ØhÃ!dGm˜™å„8n ¦9n¥lTÜÍg`±pÓ@ÆmlZƒøG¡Ô‰8íËë¾ò&ž˜1±™I'Ȥ„q+=f¥ml@ÝÆäÝþ–‘Ê1±9Fî\MÚ½' uò°nS6¿ÚéO‚¨²Ñ‡5Ž['ÍC‡Æø²ó¾dKÊtU&Ìå¡kì‡ÕÖqKAH-ÄWuòE-„$ñGe! 7O¡ºyHF%!1ï/oŒÚ₆‚—Q;F4®­6Â#ˆhÓ%5nIçä'M ž¡I'@Øèkµ+¤E"N¶ÌÄ–ÌTjDï²Ò`U°ASv¶¤q‡|•Ï®‚À¦<¡ 94 ¾ì5Ú8…µ¢a1Ö™°À^ø ð²nqR‰u>¨mµ;h%ÐM4|Û˜qLƒšœC¹#ò0 £7€ÆrZhRb°³^$b£gXƒÂ0É®œøˆƒî™ ¡Üü¡ ëˆÖ3¬U‚³„ aByÒÂM(’+ž„ +4Œ¨=Ê»;O¹Ç ĨrdfÄÜ'•VÑØý&F¥‡Ò +kœàõŽý*ˆ¢à5RÝ#äun`× æÏC­Ö{czoÊLøäj°²ÏåVÌDÌA'A:4nÔ0`äMëP\{ÌHZQ;yµKps)”ÏzÄ*Ÿ^¢ã³&*9l¢l\ÎÎå@¡¯½ñe&¿‹'æá¿tžˆ™LY˜uTå x\L.TÜÁ#Óf"­m¡aIalCS.å7wñU_1Ý<@“½™Á3u#µ(Ú•1a C­CiF +`V;Y†n_ u,TF¯œ)†´%;XåPÌR˜ƒÊ¹Åº•HÛˆ$ô ø©ÚŒë^ CP]n xVÆF§•S´jDÙSlT‹«=cFZ홈Œ)ê;™œ´²à³`d„ÝLwL˜ @˜´?Ý7â!åÀ;ü¢Ø¢b‹þÜ6îÂÖC§ ‰¾†dÕ¥]LÚë¯x¥–‹Íä)„ÊR³ÏV‚ íE\zÉH&!ýrßí¯OXyµÃÇdÖñø‚‰ÉÂfY©4!ÏЩE(EE¥’U9¿ÖÔ¡a••3 HˆˆP{‚MÞ3 ²ÆQCvs‚ÜùM(dü²“ÍØZiH¬QåW3`I­+0¦ÇÔ&ãrV2 ú¦¶sž@ÅíWüzÂ~38‡º”<£Q”“3(gÃ"Ù’©Ê"ŒH DWsxÒ +Á_eÆA`M4,N9ž#ƒWNX„ÛÆíPÀÊl@Ã'mI»ä›ll‘ŠôLÖÃÌÞð­ ò鸕µ‘q+q³)BjjmœÉÉ#TbxÊqxÜ:®ÅF¹Ž¢”ˆ­ÖáØ0 ¡1åèhØŽ§ ®°Õ‡rú‡#š<¤Óz 0¾2jý§!ÓˆO9ƒ $6nb žè°ž¸uÂmÁSFT6¢I‹7ÍE;‹P±L jtoUÙo·B¡jl>3±yÓV œñФkÊÌÒ‘´Þ¨€ø e6n‚° þȃ™*·”FÂOdØ@@€]ž€Õ0ÒGÔ褙õð2ڰÓY;“u09=s~Xç6‘x¹DßÉeÔvÁ€†uˆâPfaó„XÕƒ[Jvúr6A9Uäñ•ÈP µ ¸rJÅ„…§œÜ¨ 3€X4êôU-BÙà‰Wª;ÅþÊ)Œ[XåÈÁýU4%ÝœQ05£ ¶ˆª€Š5™´Q0΢:ÈÔôÒÅöÊ-ÖÃHâ‚ÝÀðGÛ6,¤·±.:1f¥nÕ8†Œ¸“ÍÓÑiÐU<ÜѺD7%šÒ!€@WØäiCå”ãB”É-AêÒyUVÆ eó¶hõ¦Õv 8Üäª D3•‘µ)£’1SöÀˆYÐ8#¡Ô"ÊÆÆ n“Ç?n¡&­Ä¨Î9¤¶A;›Ý~Œ”I!{HeUð›éI#Ð@5¬vk1( ÔWÖ)ê48$žqQ9 'žQ[¸6Þ6jÑ*d~ë¨mBOO(Bk­ð•Õ»Ãˆ¯®÷È6Qç‰N9cfaØÈŽ[x(p&ÔÛ‚¤33Óy ZVÙ't¨ÖîSú™è) gV®p`‡ àïœ YÐlЇüË2l„ÕÆGaØÄÍ艀Ԍ葡)dLO* ‰ØÈ'@´1Böô„kÛ Gða1éP€ +rÀª…L;…Ò¸ ‚ˈø .ÁJ„ØÆožˆ‡|*Äû._îV¢qQõ@ÿ§œhg3¨s‡AçÍnÞI„A+&~£7‰ˆu<Ô—·©ô܈ÐT@ýUÈÚ‡Õî[§‡´È¤‡ü A{Ò&š½IhÀaàµ]eb€ÖÌxŒKΤۻrm âªJ ‡X1ÈÑ!„Jº¼Qy…Ü,‚™Ñ"!µC9/ õãõ—q±ˆ¹ÿs0jÜDYɤ CåCŒ:25à†¸J+FI_z0b ô ™ÀoDO©€6í"ì…͌Ԩ¿eÄü•Ó˜Ó9%+ÌŽÉ\¢ÆÊxØô°94aÓ!JØØ)3­63j m#1eî²ß2¤S¼ðC­Up ²‡ÏCœ41ÂøÔMwVÂ)¼Å‘)eû”‹j´^pC ð¯ iš¡DXÐŽ… HDA‘ñ@íˆÞ{H‹Û|Zr(ææ鞦7ØУ2X Ä +€ÿ<¢«58Ö`*#¥2Â.·MØ û,„lÆ"u¡k ˜ P !Kˆ +Ð)T–Ý­H€7Ð"n„tòæÑiü:³óÃ`ývÁ \áôpHúl´åËÌ1ñ¾ÁW;ã?ÕvJ‡°àª^¾"Äv63îàGŒ”?5ïâ ¯Yhv•rd ÂHÀŠÇ!Î8¨˜‘ 6ÚØ< »™H€íBTÙùI»þ9 +`$@‡ÞagèÑêCø"”!ÔÀ>NY(€Lhµ³Y*Ö!ÂM+‘uñeP¹[UÖa­Û„Hn§ *P„®ð°Ö#Oš #ðŽpYX3žrs%™V"‰ƒ¨ƒ×czBeâ 5 Œ‡ÒýdsëV€[¥,cZ§4¬õ‚Oùq³ uCÅú‡4؈šòŒhP•<2å9<‰¨ ´ÎîCùT, ·A¹šŽ<¤²j‘Œa©ùqyDƒL˜•Î ~hÌYjUk¥ä¼ ýÓÐÄ-GÔS0<-:¦#Æt$´³ÊÌ\AF»eH=¢vA4˜PN £,ÚŽø*€j ã·©±1‹rm•êfvR{¦”Õ€¸WNHYxP?;ÕÛ¸ÛƬʥePÞN¿r}—+äòž@ÙÁfLÞ¨•Î"*â/ gýE<ÚÄÂu••‡X (‰JÁC9eá•Þ‡ßòÀëø§Ü¢H‹ÀÿbªƒQN_z¸ )‰PÙ¨Z™œ‹Ë¹ØŒ×_2ãAH‹\¼çæóÓtzÌÆÚ€™#¹ö"d nŸ ŽXh¥k,‚Ú¶ÓÔ_l°·2Ù Gà°žÒS@ò.x*êm¶§3ˆ¿b¦_™´š´A61{c&kgrf2c¥r c“V~øί×aŽƒ-.AãLB@³Pz·À‡õ¤ š•¬i`i#M$C[MYhˆczO õÕ b¨º"ìŠôÀB‡Â``5LŠV½¡ú­/D~(BдQ ªsõî˜Ö3‘Ê¥&j§¤µ‚‚Q‡&\‡TN0²›¯@MBªB¢Ð54‚²­•×azgxHëÝPc;Wi=ÿpÛÄ-CÚ5bQL9»ï  paxÒêÄB@w€ÙùV²² Ê+¦¬œOÔ¢÷uNåúUÐ¥›“ +ÃcÂá•ø‡#ú¯ ™@Á[x-½yV*ÐE%­XÊ +O­äÍLǀű`ƒOÏÒ±.m²©žCȻIJ'Ø€Ÿ³É!·$×ÍLÞ@$LD\ç CâPR™ÔaQ Ò¾9Èqv.ƒIUŸ*†ýÅ‚`XM„ÏÁ +-wܾ +â+;è ð3¬gûryƒÕŒJ‹‚™É„Ç_H†,æærˆ¿0 âc¢ ÷!•›È$Ô<n¡¡¦<Î7àñ >n_Áʤ5À9dŠŠÍøó+6! ¥>yó„èÞ°‰j„”‡†úvÿh^#®÷HX¨ŽÂ€¹ü¸Um4á '_D•Iàd4R éFãð •HáRænP®¹ +š‘æƒN)XɬÎM¹ƒ:¯ ñS‹Dõ¡~Tåhv¨.;“1Á€]a­,^Õc6"fTÊ8¨Çâ“Î47ØjU;$’ 7¢WNdë0(-;ž´`q7´ •Ré1Å2~(B¨.åÔ‘6ãIHâ u*¡³ó#7¸ªÖ!i”%ì ìhÈMÆ(©¤2y'Í$Â¥¹Ä4ÀÍ J¡1 0erÖIöŠ9ÈVÀ9ž0ì ƒ+Y™¼¦ì kMËm`B'—s‹E™˜@B“¼`’Š÷¹maR£"ÀÜð² »P°ûJx´E'¦bÑÁƒðÆmL̈GÌd ÖŒvÜb†ac3&\JÔ#’Îá3¹ýHn?½Æ ö:Ø,¬¼Î˜‰ä”+¬†÷uàW,DÒ#”ØpBÜa ­%§sÁÁ!-¢BµÄü —êÐñZjz›Mö!ÑëPÙå+»ÅºƒŒSåº/Þ¶q­7‚…jP?#&fÓS3z eÐ:OGË&oÐÅÆQÁ%” ÇAG‘o°?qr‰Qܼ ߥlPCÈÏ¢‘&“šñåæ¼Ñ:)#þ,)×"õpk‹ˆµðp•—sý£x¤a¥“ PÞ`FHÅA ªX¤†I•Hi)ÕÝJ3’I¨F>=€ºòÄâ²;ØFmX¶S+1â2¨r©›œ&c#T¦i¹ýE—XÙôÊ=&»Ìå–|ù:9 ƒÕ\ÿ$—]œBÃ._IÈ. ¹:5O%fBÎBÅôXÚÁà "|:TZ’›Rq)TZMÖzeØTªÁd&!qب„ÚéWˆÎƼ2°¨…ˆ9Ù *V„Ôi""*—œFüy#öHU,X…Æ!ãÓ°PÁòª˜_`Ó=›˜± zLöøàù 2Þƒ¯F< +ÏäS3™écRe‹µ}ùE±ÓY$bÝ`eEÌõ¡º;ƒA=(g·ãvâ[–›ÞX—Lôùì\´±ïlS±*%—Å|ŸÏÍp¹±ûØöçI¨¥pÍ,[è„KÌy$É„ì\¤¼^[<_[>*/1Én Á +›hsÉn°º!”¶cÓtrÆJE1š.*GÇZ`Ç\nÑn¹‚Í`}—MLSQð©2ÌšÍ.’ÉëãÑé`n¶¶tŽJöTn?l¸ŸYÀ£eµ¥ªrѲ¿ÚhÀCç #L¬Î§Zâ\¤±)=ẇNik!S)'§X¶Í_°"–Œ$“7ÅJ‘ú@a%ÚØ Õ7BÕUxÀr¡R:ÈÆgÝR‰Jt©Ät¤¾Õ›EÆZj—ÏJ[*—L€\sÉYPÚ`y%Ù=ÖÞº[nïⱦ¿cž‹·w³ gr‹g"Í 6Ùõgº•…cÕ¥SéÞžƒÏ¢2.·±HLtÙD×+•ä‚JZÈmѶ7Üò–2ýƒXg×—¤;›b¶§ÃÀU“L¼*¯DZ;‘ö®[ª‰8Ÿ˜¶°q•w°q_f†Mu#õÍÒò…ÖÞõ`c;Z]éoßÉ&zz,E%æ™Ô<›„MÜÈÍÝî+¬Z™Ì˜‰Ít’2©Jå%>7ï/­ææÎĺG¡¶¹™|{à X®|€(b£³Þ`›Ï­ûÉÞi:Ñ—+«N!Î%ä@"BÕÝLÿLÿÕÓOö6.öw.ùò:ÑŽ67"­M¡0'7׫ËÛ»×}ŧ˜sð)?˧{\zš>–9î/.Ììß“=î •œþ<—ìøs3ÑÖ6_Þð×wÈì•êÃ?-TxÊÉÀï‚°$fNÇgÏƧã3§¥òZuá´T0‰z°¶Æç¹ì‚/¿kläçOE›[P tZ,`eÐD”Ü÷åbí£©ÞI$äcãu DÄ$%×CÕµÄô~¼»ŸëŸ¨-žCļ!w@o™D_Ì-Ay$;éþéPuǨM:| àòþüB¨¾Îçæ˜TË @gˆxÛ(áqhÀi6;®oÁ +çæN“‘† »Ù¸ À¸…²˜Y(-žÍ̤zÇr çb­­¹½»@À]¢Ò†‰ÞÑæö]ý£wŽÝÝY»]Ìt™XSnl 2àŸrº<5(/]Ê NÆ;»µå Ðø,ˆK ¢¾Ü|²³_^¾½´|6?8X>}_iþ8,²KbÁ2T] º‘ž;mïK+¹ÙÓf.iñŠR¦ƒK…I‰rÉÎnkïžÖîÕ¥“÷Ýõôw¸ä “\¨¬]‹6÷ÝþºGjÑ©YO¨ees4èòåôžX¡?7-•ÂõíX{„×.ä“õµG_zÇWXRÙY_²[]8'æÖøüz´{Ò_ÞgŸß½æ‹—ŸRÈj)5}"Ý?S\¼é[;qß楧ˆX;RY«¯ÝÑÞ¾ÜÚº£´xº¹}µ{ìFséüµ'¾ïì¸Ål¨0HuÂD +ógª«›»×ŠsÇ{·Ïº—« ݽ«ñÞq6¿X\¾Ð=x(9QH·Ã•:Q³±q"Öáò‹R}+P[Oõ¥'ÄÂLan'ÚÙdSÓBvà •}¹™@aΟŸ¯­\‰5÷¾-‡Ê D´BÉ ÐaM˜r¨¶•íŸÎ Nùs)×eâ-±0/·¶"õtw¯4{tùä½½íË –d¤Wœ;ÕX½P˜=™î—[ût¼ßZ¸½µ~‡…ŠB‰BId§RÓG£­ÆúåéÍ«'®Z]Êôöbu.Þ@Å,¸aqáŸîGk«±æ&mFkkÉéÝ@eÙj ù¹Ü`/Þ\##åxwÛ—–k‹åÅS°øRi.Ûß=ÿ‹r} Ö¡±r¾³w½°|!1}4\]%åiàÛ\{ùÄÉƪ•ô ‰z¾s9ßÚºX^»ÔÚ½š˜]† ;n! Dˆ¥ÍêúÝååË‘Ö±H÷ ÒØ’Šóéæz~zÃè•ôÞ0„) +Œ&³ª¬‘ñ.){»Ñʲ•Î0™y©±ë++Î,/©Q¿Ö-¸ø¤›³°¹M[följpì2‡Å §“ÕXk#\[UéÌí?öâÛ°>:ÌÏ4×ïGcý*žT_+Moßxèkw?ÿ=ÄŸ«¯žëìÝ[Y¿ ú·±yµ¹uÍ—[88÷Е'^‹}•“§dèˆÕäôÑúúÅù÷æOu—O=òì©þÔ@aæX²»›ê̽wïÎg–Î=ªì´gõÖΑW¼Ø—[”›»ååK3'Y:ÿLaáÜâÎÅÞê)<\†] ”¹tŸÏÌȵÕÙã N5RÛ€âmO @Çš¤Ü‚³ñ%+WŒT¶Š gœ< `FÈÀ&®‚^q™!Û/Ž_ºÿÅÆòiD*–.gfÏÅ;û­+Ð)L~Èó—Â…èt fåÆÚôÎåÁ±ëÕÕ x¼‡IµJïø¹{¿N„ +Ñòleátyñ<° ãõÍhcË_˜ùYæwÂ^¤¦cÍŠîæ{—Ÿ®,œYÚ¾¸¸w­À¯'»ÛPóõK[½ðà‹ÉÆúÂÆÙ;ú” °„Ó—‡°tïœÉ/ÞUX¼È–¶oß?{=’k¹ù“œf²K¾âz¸ºÕÞy@ªlؘ¤/;·v þÙx°Ç_Ù€u‹7ÖÏÜûÜÒñ«&2ú‰ˆåDï¤Ü=Æg€ÕÙhg÷ô—xÑŸì‘áºÜÙ ·÷äîñÚÆUÀ!3ˆ9n¶áŸè +‹ÙÞ±ÒÊ¥äàv©¶ãáóÙSB²nÄ9"œ‹57òýcåÙ£¥Ávsù$Ðþ¤‹áâÕL÷ 3}"ÞÚã2Ý\wk÷Üóǯ;¹dzz, ÜØ,._Œõ|Åe!Ý?zöþSw?ŧ[ãŽMÌJKBnª¯”–Î&:Û3kg_þÖO«‹§8¹6ؾ¼vþÉÖÑý£÷®œ}´µsƒ÷ÎÜwòΧÁÝ€7àMÓÓÇíÝLïheõbnþ “ìw°u ííÔÌA°ºª®çfN*¯] –»Ê•cå$€GÈ]O¸ƒ!ÄÍ&š›ñòL¡³áR\²-BÂMõèD7\^-/Ýä xYX¾F‰Iy.ÝE *«‰$.OËõ]#±QÑHqΗž®,žh¬ 77=áâ+çš;g®¿À§‰æJkóŽÂüY±¼ÊÄ›l¢å +”`5ÑÚ íL”‰U„LO„®ÉÎú³ýDcƒOv{K'ûkgÉHµ88ÚܸX_½P[¹½¹rº2Ø ¦»×|îÍw>˜;zUï¸}y_~nH9™©ç À9ªCȃŇ ýüô¶ÁÈt¶úû÷ï ”¶Ë—³ g™L?×Ý=zõ94TVßüd‡ò¡¹ç¯ì¦¯+›ÄøÒéö&d(Ø©òòùúúå¹½ûfw®gºÇåÆ6©lžºQìé°à(¨ä)!·&×’½“bqÑ+eÖ]N×–!GÉ°ªëþâ*›ø «ÉîA87Û]»‡PyÒ0©`câB¬•ël{;h ïdÁ€Ò62 +}Ù€0ÖÜŠ”—c¥¥HfÖª6w2Õ?Îff!«"š™L@¢ab½îæU.Ý3“²7ÜàÒ3¥ù³ÅùÓð(Ï”¨.½ø(Æ'`H±éãby€¿wâ¡îöåx}ÃãËŸ¼üXwå¤^ ¤)~AüŒëqɈ‡8A%ð†ñH„:ÚÚMöŽÓɞδ3I*Te;SÊ„ðHÃ+w!S d´i&ÑÕ89'ô§ëb/PÞ@,bÓ3r}]ÊÏòr¥Ø\ìÝ®¯àÑ­3ñŽ/3Ç¥æå ¨FØ&(ÎTs#78ždçNz+Ÿô%ÛruÉJ‡â¥þÚ™G–Î<–œæ’=›„!A(C!¡xBã¯Áɼ”Ÿñgùü@ª,ÙÅ4Ì×–ÏÃnzCåhuµ8®¼t63½#$›X ­CýS6΀&,Ø”-ÏÊÎœÊ/Þá¯mˆÅy ä­Lgߟ[vj°G(d_²8³W˜=0a  æ¡ÔLÜ+7¹ ݬp‰Žš6ãál÷Xwûº\ß\©Î•WM¤,ÅÛ§î ç:jDªëñé“¥åË͵kþ +n9ÅRY¤£ ÔW@Å"(ÛP©ÝÛڿ𨅠™´èí¤‡–ÅxGJÏ8ȘÆÊÐü«ß}¯¿t0iB¸$åá6‚¥Íx}?^ßQþX„™Còâó*3 JecbÞ`–«dsaë|¼»l |H é/ô„\—ŒÃŽavLŠææÄŒâeN! knƃ.6zâLF«°h+Ç®¤»›|nTœþŒ[ˆs±J²³Á& @|Ñ2xM¦µÆ':2áä¼áº^ß —Ì„Ä&»‰Î±æêùò‰dk•K5-t >X_?{-½h°I'ç|™yNVÿñ( k¼±KÉEBs‘|vf ¤¿sWcíBqö˜?;Éuž½¾qö>•ƒuù +T¬€,,QRÍŽGm°‰láã0A ¡S}<1C§æâéÞi—˜×¸Y_¢øDó&èV"LHe2P&üE»72©uiÍÄy_jšŠ6€.Ìx„‘[ÐRuˆ/ÖX‘Û[þò* 4ÙˆˆozÇP13i!¦¬!5—éì‡ò‹ÊG`¬§Sq3ŽóÜLnæŒ ñhÀˆø ,8@R˜´giñB}õReþl¼±Iu‘€Êñr„µzýð äµöεòâ…`qŠyÜBÙY ½á<©ø=¹´/¯s¡*.¤È@~ÌäÓ!F;iExŒMœ‚ÊHhl ©î¼K·&´ŒOC÷¡¬+g¾ØÔ”…õLVV –µ.ÚŒAÏ&B…~a~·²¼—ì-%§Ëó냣·/œ¼ØØ:ÉæÁ‹ÁÂ@šÍÎzŬÚÉX¼"ÊE¹h1iK….—ªò™Z¢1{âÊcýý ñö¼'”Á¢…P±Sœß +´ zéáçŸ|ù›¯~ïo½óþë?x÷‘¯}s~ÿ<+:˜°ÉÃyèP$ÝŠ»\4+Õ[‹›s{gzÛÇk «Ý½þΩµSw>øÔ‹?{ÿ£~ý»o|ïǧ¯=ÒZÜ—‹}3"šÝ’ {˜D¼8—ooy¸„ÆN3Álsq¯<·•î,˹Twyõäµ³÷<~å‘g_úæ÷/?òìùþåâ#Ï ™–ɲbA£“„à" 2{´æáãá|;^ηú•Ázýôι{ï{ò¥ŸþÚwÞùÅ{ÿöµïÿôÄ•‡úk§sÍ2X\´`’ñá,*Y½Aƒ[DØ”\Z, öcµ9¹6³wáú£ÿúÚƒÏÿÛ¥¿òè oÚŒ—f¥d=VèWú›«Çïè-­î½óàŽ{o¿öà×ßøîo¾üëoÿð×/þð—>ýüág¿‘­/Œ¼ãFÚÍæB¹åTmWç5ÒŽ‰l¨„ûKv2æ ˜/‰‹éd©·qìâÉ»ܽxãâ}O<ðôKÛ§¯6WΕf3áò˜¿uÄÎñÖôòÊÖ‰ƒÓw\¼|í±§ž}ýÍï¾ÿÁgŸüæw¿øð“?þõþí??ýüË7¾û£Ç_xuåø¹™£V*éÄåh²-EòÉL©ÙÛ:¸pâüµsWn<ôÄs¯çG¯ÿàgÏ¿ñ½'^zí[oÿôç}þò·~øö¿ÿì¯ÿñ÷W¾÷óG^xsçÂýcÙÖZ}áh¶»âOU2•Öüêú™‹ï{èá§_xñ…W^ÿÎ;?ýð׿ÿþ{½öï?ûݾüã_ÿïÿçÿýùç_þà翺öø×j‹GCù>&ÊHÑd±ÒšYÚÞ‡ÇƱS®ÝÿØó_éo=ýo>þò7_|ó»?~ïÃ_}ôÉ_|ñ¿ÿ¯ÿþð×_<ñµ×NÞù`º¾À‡ê¤¯ÎΧۻN:îò‚r¡ÞYØÿßþü_ÿë¿¡[ô‹Ï^|í‡ÉÆŠÖÅOZHPNK°âaTÌùâLm°ºúÊÃO\}ì©ùÆ?þÕ'ï~üëoýèÝW¿ÿ·¿þüãßþîç|üû?üéþç>ýÍç/¼öÖÉ;n$ª3¸3º¼:iÇ%ˆ%ÅÖroqo÷Ä—o<~ãñç^yë?ýÕ'?øÙûo¾ó³_ÿþOùÏÿúø7Ÿôé§ÿñŸÿù³?½ûÑgêó{™ÎŽ/Õ3)Ÿœ2”/ÉøSáT³ØYm 6š³k›§.^}äÉ}åÍw?øä“Ï¿üæ¿ÿôýú¿þ÷ù—¿}íïÞxü™‹w?"¥§©P“dÂÓý•Õ̓§.\ºë®k÷^ä±GßúÎ[¿ûÝï¾üÓŸ>úô³÷~ùÞ˯¿rñÊÕ¥­½T­* \t†I/3B$‰WªÍý“WîyäÞGž¼ï«O=û¯/ÿèçïÿðgï¿þ|ÿ‡?ùòOÿÓùúëß~ù·Î^}xvë\e°-DËr¶“mÎ¥ªÓ‰bsvigm{{o÷ê]w}ûÛßùö÷¾ÿý¾ó᧟}ñç¿Áv¼ûÁ§Ÿýú7ýÛ|öÅoýèÇ×ydvëx¸0ÍÉe6’‚‰R}zveçØÙËû§Ïï?}åú½¯½ùÖ/?øðƒOûÎû¿õÿÿÑ¿ùüóO>ûô³Ï>þðã^yó»ç¯>Tꯣled'Z^öú²”/«õîÖîîÃ<ö&ŒãG?é›oýô½_}ù§¿üåoÿåÇ~ùå¿øàƒ—_}õÅW_ß:sEL·>b%Ä›¡|/’ïtç7Kû§Î=öôs_{ùÕ_{óß¾ù퟾÷Ë¿þýýæË?¿ûÁÇ?üñÿðå—Ÿ~þÅ+o½ýÐãÏUû;vJÖ98½5#~‰(uÖòõÁÖÑ“=õÜs¯¼þÂßùÉû~ùç¿|ù—ÿøÉû|øé§_þñŸÿþ‹wùþÇŸ|ôî{¿xäégÎ]¹/Q™fB#Bê섯SµX¦µ¸~ôúO¼ñÖ¿ÿä½ÞþÉÏ>ûüó/ÿôçó»_~ò›?ÿå/ð:ïüôÇ?y÷gï¾ÿþ£O?ôÒ}óǯ1r}ÜHXûø¹—_:yéBwi)ÈŠ‰ +Ñ•8)‰ $ö.¿po¹·TlõƒÙõµåÓ§žþÉ·øö‡úÙoóöÞþ曯¿úÊËÏ=óÕ‡¸{wÿ Þ[–ÀÑ<¼‡‹Y‰Ð„r ®µSíó…ó¹ÌÎæÖ×ï~ýÕW_ë­×_ÿúÏþÎÿøûÿí_ï¸xzw£9ÝË5:nJÐÙ½^!vÒaŒ ¬Mæ«ÍþêêÖÅÛÏ|õñGŸáÙoëõ_~ðþç_|þ§?ÿñ½_üðÙgŸ¼pù\¹YsἋŒ ëìì•y\‹LèQ’ãµX¼43¿¼¸¾qôÌí·ßqùÎ+×®Ý}ý/¿øý·ðêë¯}ý¥_xþ_^{õÞ¸÷ø±ã™b å#&Œ³zE„‰þtª2/ÅË\ˆ%ó›;ÇïºþÐs/|ýñ'ÿ宫÷>ùä3?úñϾ÷ƒï=x㮧¾ÿ©¯>zöü¹Íí­öô\ª2¨-œ”2ý›—CÓàËVT4Ú—‡ËäëÛÛG¯\½ö×ßVzêÙg¿úøW_zñÅw~òÞó_û·+WîYÛÜ-ÖÛ$‚8cÆ$½Ñ[<6„qbœ‡ôùã•D¹ßè¯Î¬ìa¢dÅ)É›1Þˆ°°N68™#cz­uy%q`"F…p2ä%c¥hºŒnó`v” øî ÑrÙH„&m„ÑÅ"lÂÉDÕNbXgÑÛ¼°#¬ 9¹ìMŠaQ +c^/Ë2élºP-÷ææ·Ož[ØÚfsnÚ§µã“6Lë¤LhÀ„øµVïbÙP.ªba‚”(Gb©`8‘cé\&™/äêíÎâz(žL¤Òíît¥\–Ä ã„„02áÏJùY,ÿŠÊbpûürƒ—J¡p¦^ióÅdLŽÇäµíN§V*e:ýéx®ƒ$ý ¯˜wQ­…P›Ð)ƒ{BçPéÜ;oÃá5ó¾pµX[Leë…\åò•»/³ÓívµVŸéÏõÛ¹BÝî!‡'tÔ&ï°Ê22iƒoôvå“ø8›#åp¼˜Ì–ùë:Üç ÇÓõ`$—+6»½å^IÅd:/…R08ØQ3©ü),ÌE¾XÁYÊób*ší@SsÁR¹wtfëJgùœÊ8\Îp(œNçýRÁY£‡*båFyîœ/5§¶ÐÿxÛ” {ؤ`—)&¦RéB"•Içj¢$½$C þP! WB‰F83mR>¼ …r3l¬}Ûˆù°Ê0®s„ÚQóeéP9W_\?uŸB¼%*T@¸”‡O[¼Á)3î"ÃV"NJ…Új×~~õ0íh‘PìaoëImõË×" Žh¡º¥SW# ~Ú›”’½tc?YÞòع… B$ˆì0Á¸"’±éT·i“I%¿vs½á ßtgÜá9w$DZ¨T¤ôšYßÂÄ¢]ZË6·i£Fjµ“Žr¹¸R™¶ e3>Âü`ÆueÁ? ¡‹1iZ­Ð‰Z\È•ZÅþùuGì¦#ìŽq˜Z²«œ=$õZˆÐY)]ïïãZeÆ›uG|hЬơRÞâ3ÌT©«¤ë¾¸!çˆ><¢sLr”éÜ‹0™YOÜvq"Ž0»àÇ!ê‚t’5›œÙ}¿¤‚†Re,<¸¾0b^D 5;Œ«ù›®¨?®‹öHÊ 0¹ì§2q¥áDs>*B›Î0ýÁŒçʬoÙƒZäôãq&…‰ùezÙS>™q¾¸$Ó~<ÿbR9DÛŽ ¢L þi'=/³0m,œÓŠ+Éúz˜1g\ȼÁY{Ù_›ñ][ŒÞp‘ >6ˆ™Fn3LØs ^‹>òúB‚ÁOšnL ¥°©–÷|˜AH%7@•’Õ\>ff!2ëDQ*UíœBfnt5Cèmžt£ºÑP¥&æÖ¹ô8@&.R^}`Ö± aãB´'"%ªÝµ‡2yeÖˆ\®°ZUM¶áñá ¯ÇO¬n=@ùÜœŸ‰‹Å0—Å”ª˜Yµ[§¬Ñ[ò2Ë0h¤1ïBg—#¡¸‚KEf:Ýt$f×h½}m ’:£ç1=Ó‹.……÷]@;|v¨—7jk÷R­B+ÒV‹ÔRnEÌM≦θñÔŒ‡šóbÓ5þKqŒË#|>LÛœÑäíÁu':ç‰-&å…ÂÕ˨ 5»Ø]?zD§ºsPx"Ê&£ì´ÅDµ;&Qîðüép÷®7.„›T¸RgÌ—Ã2r>‘é@à]_ -úÉ05å)&5HT÷¤Ü&Ê2µM%ןñ¢S F¥(›BØ4k´0¹$-‚MU:û˜RZ ²sn ®o€LûÛ‹YZiÝOêWCA\‹óIWˆöDHêy¸è!ª.!W§°Q Æ9ÊæüDò†]Ža±"ç&éÖ.©×ïÛŒHþ/"äp¹èOÝ0‰!•?¸æ¼:ã3•2kŒÑ )WXYôÐ@´R€ã8rÆ$W\uÄD?aFùR„+F˜ì`çQytæR¸R&•:. ±„‹e?nÁ_Í,‡ƒ(d¢°§ vØ‚Õ<ßúº0ºïÅ-*ÓzªÁrD2EÖêÉ™A"Ó­6ovP>ëŽÊ!Ê^òC`ûg–¢ Îø ø×C@5VÒ]øC7š õÚr˜sFxOL†°üùŒÖ…Èd˜JþìÊÒìr¡tp; q>¨Ìú…—Ñ+ÔmïÖËÁñ‹ëj1&afŸ/íês{p[®nÑf³?¾uëÙ·!)·„ˆKˆ`òQ¾L›#©x°7 &1}Ö ‚¢J…T*ñi—iËkÖj!‚…iy4Q‰ÈEÜj±™.¢q½!Öæ ›êÇ„,„nK‘JQL¶»ìcùq=.dç¦ý[BW—#P¾@¬Ay³‰KÓÛ× )P¢AjÁOxÈTH¨`zŸ±ú|²ËÛ}„³B\ú‚àzŒÏ¡b ¢KÏöwO_’jé¦ YðbQ:ÉP“[d¢Èeg„#ùäɽ·ÉêäúRhÉ‹Q%BYŒÙfŒv ®ÁAòv³×Ѷ¢Á¯"RI̎¬M$*ã£ç\v0mJs3êå2q>¯dfyÃShœ¶µte)öþ…^]BÃtNÍ®˜Å cÔødË‹'f(b‚'®¢R™1;…áEïè¡Bö©ÍÕ³âàv˜J-8ó¥ G…q©zc)Ž%Ù80a¡¸K8p‹´ÙñýæÎS6Ùq! \kÁ˜jÅ彈àðO{BZ][ŠßX/w, +"& OTZô±‹AJßäð•`Ôþú†{vZL )µ‚ Õ÷ë»í«K\w„»2ëYéƒEÓGXTjÅ®®IùÞ  ËÅX¢¦Ôv0«“‹Rº¿}ë““W?„Äl”O ™^Ú6KÛ©ÚŸ_¿îgH!7\»Ï™íçC‹&LN×Wª¹Iw÷eqx'Y?xA…5³µ›œf†ç¹•»¹É%›› r!U|ñË?u÷BJqÕOèAÚ&õ¶Q=`“CwLæâ´¯l*.UP©k˜Ú’³kraâ‰+3Îò~EO€±¡ˆR«sV;ß=b¤§Z˜„X›y?ÅšNÂrÙC&½¸%h)4ïÅL2ÑÔŠk”\b”’œnÉÄ´A¸rED(ŨXÑɘ˜Ç´*i4tr9Â/éYN/AÈ%R­HùqyûYX­ÎøI¦‡3ÌXð^\sc \.R dzÎKüÍ\èª]pÜ•g Ä̘MBjt1 Î1ëõŸõãFŒËPàuíž'&±RöñËï8£òÁŒoÖàÈ1˜I”OϺãî¨HkÈš¸ƒ±êÛ©Þ1[XKIš-œpnc3³.|ÙÏ…ˆt\(³Æ€·V|˜½Q6áMªµ%/± o.#ÓEg˜…I µ¸a7öÁ.(@•i/V<¹äç¦k“I›MöŒÊ&Ÿ(IÉÂbY +‘N¨ºL:B§ˆD5*å]˜F&ÛˆVö±¶RÛæ§Óÿvëëë[ÏÍö¥•ìêz¢º†Š™k…iÃWn(\*Tg´Ù¸¾P)@„+f+ß?Ñ«¹æÖ铯1«N¦ê…ÕóÊæÃêÖ£ìøvª†ML-tÇGÿðÏÿíøÙW\Òk4¹ä Y?Év/(³'Ùýâà<Èdæ},"TèÔŠXܳÚwËëiõƒkGhCË BPíQu1ÊÇ„—ìð™>ž¨Ë…µJïðñ'?ô÷?r£À>-­¶#Uv™ìšTÜI”À™xq~3@Û6MëÍ –ôDTgD€:lWÖy³±½ä0! Ò–€Iõ…̨8<·»'>ãAE€¸ ¹Bº£dïÇ“=*Õ{9ƒT€4¤Ü˜NöôÊN²¶å²W—"7]èr˜ŸuãΈ ¥ˆ6:”Ñ&õNLªV—ÍŒ©T?"¤áw ÄÄŸˆvWÏ kgZŸsÅ‚qˆ‰+P“g=øå«×µìRK>ŒÓPxõê:fÔåâ:atˆDCÍq¥2ídEf¢\)Lg‚TÖƒZÎháA2º™úèò0i8áð\é<ݘ£ãrËFJ¶½qígW—o,F<—*Àw\ª•<ʦÍâŠb×®-z–¸×ÂS%U#|X)QÙ-M)¥5­¶Ig‡|aU(­é=.· +îbÐ?þäÝOýG7œ1_\Oç påÜŠ”]Y +sÞ‹ +y7ªMwèÀ"QNÔ6íîq²}—3`Þ+k·q³N%lv ”׳ݓúäÞ`ïéîý/q- d-7LVw™iÇæ4Ô™¨ÕÊëFk߉%f<¤QÙ«n>¯m?Ó›‡X¢Š^ÚfÌV®sÈeúñD׫´ öu F”Õ”J#£¾Z]=1›1Ö¨¯œu_7ž’©mi{ä@Ý•óGoL76‚,Ť*¶SµãL÷¾U‡Ã.‚K3}w„ÅYƒÏÝì¿<}þCa|oãèé`ûr1@3z%;8ίÝ/¬?ì¼^½ý«ìà +&eØT3*ƒ1K r UNÔíˆð`öfÀÏ–ŸHù¦-:ó©BeVŒþ¥y–«aÖ3&ÙƵ¡Õ˜dKÌ2½9?ŽPÔ?"/Nâ: ~H0ç|$H +@í‹{c"d@åÕò–^ÛÖh ¹õâÊÃtçHöú2á +Ï ¬Ù‰ñ`• „ÖäS](AP1 –|Ó®_ P7T®PPØó«Ž0ÿ³«KKlÞ¶u…8ªQ‰¡T)­‰ W4Zqi0àuñé‘wìÎQªs4ØzôäÓ?ª…1¡×²Ã;FãÏ®4×v6gçËXb΋ãr *”PÞhC"LÃÉψV-ÛÙCÄœ/(‘Tk•M«{,•6d»3X¿xøÙŸæÇ“lv’Ý-↓|¢uny¹œ’î|÷ÓÚ9µ† +7Ôòfqíñöå¯jÏ=qýȹšƒ¸áÁçCœ3nætºÐ`‚/Êa¼É€V¹´ž[½_Ù~6¼óÕʽ_‚Aü\]D[“{JnâŒÊÓ—€'˜I½úöK u@`ª€_e• ¡»¢ø1n‡…éÒÑ(xi2y üGi;L˜`…\‰ÒYg´2 )„5ƒŒ3®ÁEŠÒ¨µÂèü T˜_¹o/ôæQmíÔ´i;Ü ‹ÐoÕ‚”áÁ47j8"ÚRP +ð?àù¡`rZUË÷‚tÂOª.LkÕüଽÿQûøJ-µ'çÅÑ­hãzÍhî)•-ÒêåúgÕµ‡Ze\ +m´RÍ£¼ +®TÖ‰Ô Èæi(JZáª#@/Ý9|øÙù‹_ŽO>ÂJsxðäíïRõ pYÙþƒÇß?þú_Ûg_q…íˆTXq¤\É´Ž–§óFˆ0cƒDºã À¹°î‰k×qgDŒ²YpwA6•J\n ,݇2¬¼“²Dʆ˜’¨Æu­|ÿhÿñw©ÆÖR” 2©ÊÆ3Q>;vN;!K!ÒÈV×›« ‹Q6CMÜhàf[,n‰åÍdw¿½÷äΧ?5¶]÷Ð +Ji“Ï®¢jjÖÃ.Os¹ÅêÕ¿žõ9b2ŸßÔgb~SÊo,…€ãDLÎH=̦¶î¾»xû‡{Ÿu^)Õ=/žñN[Á\~úwéÖ!XÍ›Sò“›‡³[‡^"ye!âKùæ&¯WnÌûþúÊÒœ—Á¬Ö>7;çÉö)ÊWB¤-Ù£i_Ÿ ̹ÈY5ë¡=ˆþʃ¨•ísðÞNDvL·‡HÒf Ü[Xó’&lq¹!¢U¥Êe'Tz•4»(—¡¥.¤}À/\T:𚶫š6SÀôV:GvcãÊR`Æ‹y0ƒIö +ã‹Êæ£Ìà(a7î=ûntøÔKëÉæÞøô½£·ÅՇݽ—íýW\neÖO+¹1•¨†)}ê¥ù<ªuøô$ۿͤÚסû)„Ï‚m»ƒâi´br•µ§«#q£;QZ#µ2«—j“³ÆöÃÚÖenx´rô¢·ûDÊ ´âJer¯:¹hlÜË>®°ŒB‚¨f~%Û<œóÑ3n‚6;RaCÌN„성ó~Ö‹ª˜X€ÅSdÔ÷š{y€SRÍýÛ/Þþæ/bºçŒÉQÖæ3“âøÞÞå7·ßü±¾ûB×—ŒêD.¯ûIˉ(×ÝÔbXŒ°YP¢“ñNoÔCLj.@y ‹I¬ÖÑÎåÏ¿û§³·Ö;·ce4"BÞO¥ˆåòq¾ÂY}ÖêC]]ŽÂǪ•=\k»Ð@Ðt«#!+f‡QÜ”Ñݾ×Z¿¬oiµ="5Žë(Rq£¾óqX¨,GT¥4½£èøÿ»+àÖœŸ»áÄB”UèqíÃëK\[öÀ±´M¥†!6{ÓÃ{ˆ4"ÕæÜäÕ›^WDñ ºÕ£¢\Î3]°ŸM÷Ž >›«oÄø4ˆœ‚Ù9“«‡|~ÃìžFÇNí"xWT«Cî@LÂ;î¨äŽÊ~4ØâC4_L_úábäêbˆUËŽ¦×—£ó>r ðŠËf“Ϥ\Ï, jãs€q:Qîï?ë¿,­Ý‘ +LÍGÄl™v'“í.ʦ¼È]ÈnÛÝ¥Õ§ÝýO#ÓæÏP^Jj~ ¢éˆ‰L q>;‘‹¤Õ”h®×ǧq>…P‰|gwõìíþÓ_=ûîáç_žÜç­îåÃ/^~ñ#¬‡ØTH,ÉÕ<©¯?¼Ž Õ—ëPCIJ\ÚòÀ•9Èîe? fX  bLÌep¡é~\.6rF=×ÝÂô¦3‹½óÚê“Lóh¸÷t4Ür!²šj¾øúÏRqmÆÃøÉ4=åÁ <ц¼«°)H0Ž(ÖѬï¥Ú‡­íË“çßMÎÞÉžš[9{ô9Êg|¸!çVS=(°v넶Æs~v9ÄR6@S3ïÄüDÒ7!µ(c%++®˜àˆ +Z;¿_»ÿ]ûøÓòÖS4Ѽ²ˆ"töøñ¯ã‰ò5'²½$\…¸Èlïn¶ÿ†3~}ÞçÓñéýd)*-LjpTkËÅ]p§ð?&µß´ÝD˜Ëŵ“ŒŽ_¿úÎH+‡©$*Ÿî‰ù±X\Wë‡Jó¢VQ>[lïj>D&`½Á';àú€¨6ãŒ^™÷ÝX +]] Íy)Èúéî1%71°…LÊa£.æ†fsÛjïVï*å Òhš`Š=T2ãà­ÍUø5934Š[AÒö!)f¢lÊRBè´VÅå*äB˜JÓjP+³n ’0Š'HðbPð m´ÓÍ=9Û1fœ5Õ\SÊC¦¬W·ïuöŸúÇ…Öþæ­—¹îªäõòzfp›Û\ß·{çbq7Ì4e…d‡ÑkK!Îlâ§æH’RàŽP›È”/¬«ŽÀóž®iù‰Y=ÈîÎ?³9ÕÚ:{­V· ¸bRàýâB®ŵâèlÆÏÎ{h6ÑaÌÎrD¸âˆ^uD1‰6ja-ÂÚA\6ŠLÊ{ÙN2|lJqcò`LJià`ë“»„Zõ£š#Èþ__Ž:‚ .e]n:ƒ]©©Åu.3” +«¤Õ¡RÝúÚ]ê¹R¨¯œV&wà¨ZóÒ)/i°É^¢¼kµÏQaFÌŒôÊV³qÌÙ#g̼²w¹æàp´y{Ú&ÎKx “±ÉÞiaõªúuG<‘égë«s 7Áâ¥åÜP/O8«|áNï®øq‹1´ÕÄÕR¢¼žê[³Úæ »{&W}t軾z|,ÔºiBÔrG€P˜E? Þþçï÷e˜›6«×p¡£38›Mfº+{%€WÛ¾<|ñ»Ã—¨í¿*ÛðEZ²º¾s±º÷L°{Lq£ò{"ÎÄ褙ëÇhÃ¥ådU4k,¸G®ìBÌ ÂäŒðËAú¦#:mµ•bÓ;´CƘ>> 2&(ÕV{“Óëw?ݼófïÁçýƒçöð<*çËéS­àÃeÖj +é1©÷…ÌzŒ+a“`’Ã!̈L·N+F™œU‚XÂâµÊ²É9Áª…¾ UÀ0T˜t_É ªÃãÓG_òvvªËÓgºVeGN¯jÝ•Íü Ü=pÙ« aGX +6—ìCFPzœ†–í5'玻˜>XI5ê›[»O6ï|žlAî7WN6N_9ÂbÓ#¤ £ä +±!\¥”dS±¿ÇUˆ4?¤–àÒ}³{RÙ{¹vþ…UYß:|ôÏÿóÿ£×—" B¨åƒ|ÿôΫߟ¾ü—]»¶Œ!Lš3Z ïK$—%ØkJ~SLuÁœ/X°p SŽ¨¸0–Bª'f(¹ƪ;£@´âÁG@Hx1"@À‹¡q¥Î¦‡lª§WÖsýc½¹e¶ö²ã µqÀåWØt§±r±yþ…”-Ge7j†è¥Ô€X˜‚èEý…³ä1„YA4R–‘k7×N´Â°2Üû8ÝÛÏôôö™^‰Ie#ÓÝ9~>9þ4Êgç¤1©ÄZ]B«òÖt®5oTóÍ-”KMoª“I"Ñ£ Pφ#¦À :ÂÜrå]ðÓÓNøT +ÓJLªÉ§;ÕáÑw?þÛÿÇÿ£·ÿˆI6ÌƶÑØml\–†§½ÉÝÕKÙ®+vSJ÷„TOÊ®`rÃÑn,cקOè ´ÜqÁË\_Š][-x°ep}ZÍŽÚk÷VŸ/F9i†ù\€K³™^ºs’éžIÙ€UgrVêúq݃HTo./ó\}½ØÜš]ŽÍ»0|‚\ +SÖ¢:& Z­hù‘U£t +R°ZFuSœÎ©kÒf—Ñ*íÕ³ÞÆ]p”MãRam_LŠ °¾R7K#BLÏ9"QRGø cµ„üX.­Áç Œ™­ô/v2Ùîý\ïž–[µ+Fi Uª3nJÐkÅæ6XÜkKagD 3e.µ1>úrxò9gg=gÔ´tËþzÆóÁrt.(yâ)Lªë嘔 q–'._™óÃkÖ‹zÍêô±öÀlßÒë„Þ´§+£Ç^N‹ÃØÝÍG[_ž}ôû{oþ´ûà[R¯Ð‰ +¦”ÃtòhÞOÝXŠÌ»P/¢,9L)%^ K!ÁH JQVgíJ}rëÖ“¯º—~Î`A;'Åñe²}©¯Þ‰J:Û/¬=ªm¿€“Rr£ÕÃçåþ10;.—­Úv²²Í›ï B„›u!sÍí î(}uÁ;ë§÷íI|¦#"x¢òôîPD¸éFBt’P*à +½5Û‡Xš÷8Ÿ““MG€šYŠP%€(`<âlzÞƒ/zâ® ü‚í Ó #3^ìŸ(rj–7J¤Z2J“Ÿün÷âØ6T(qf4=Ó>!å"Bk¤˜rÅDP½¸XÃÃ&ÚŒÖJ6P±èÐ\"gfÛàp~vÃà2ç©sn|ÁK,iÏ´Tª3ŽèR€£"@4\¸^‹)ED*F R/;8•³Í½{oÆÇ•‡§í{ÃãÚ{O­Ö¥W³ÍM1Ýò B”2\Q´cهǛѪJ¦~¬:º„3u£¸L¨åtïpçÁ'/ÛܸÿðÕ¯¾ûI­¬#r Sjq¹Â&ûze¯¸ú¬±ÿV­íÐZ¹6¾“ž)ÅUÀ–ð´Û­á'3Jfá‹?_}8ºá¥P£OÁ;jÓ=ÝU¥Ôj¦s—³!R³kkG÷^ö›¿?~òyªôþ1hkóüÓÕ³×|¦æLµ¸ÖÞ¸_ê%òÃlcÂëWˆŒ³\©ø1m9̃]tETf…)å pݯÍݘ'ÌñfSÉNˆD•*F®·~ô(YcjF¯¯÷O>^¿û9 a~í1—[õ ‰|uóìÙw15 ¼ˆ‚a2Q:ã QÜ’ôúxóî歯ίÌx 0âBA)m²©>\JµRgoãÖ+àå01µ— ¸\@hKÍtq¥°èÃ)aêQ]Öæüˆºä% ˆ1-Œ'f–ü!\¦Ô¼'*@¤:[«'€*OÍÖay|*åãøåÔÁ"š$ (¸BbˆJ3©¡”f]qH0‘ð#rhúŒ2ƒÒ–;H.yñ9'JiJÉÿü†ëƒëËsmùÚœwÞ‹°„dÔàªý½diÅ E£â#¤Qç²#­ºm¶k›Ïª«wÖ7Nÿù¿üŸ{÷Þ3 Ùí=£¾ ”*·ã‰ª×h)·~ð"Ħo.ùçwˆpNw·ÁÁH(©g”A÷c|–6ZJyRY½ÕÚ¼[êí>ýä›o~­‡¸^Oµn%G••ËÕ³/{ûw¶ŸÊùU»49¼ûšM•<¤â2‘i?±†”pöŠŸL9b2 + NÛ ±Äb,qÍMÎà,£º¯•w8½~tûu{ýœO·´Ê´KyíqÿðÍÞýoÎßþ©¼v¯6>9yü™VÝQò#)Ý'µ*2ÝŠ½J(u°Ck‹AÒ‡'B´-$WåÌ~”É8aÌ1“Që‹~úš“aK Ê~PC6óÞ„H³‚««—ç3£ñáËÞæƒrg¯ÔÝBå cõ\ß;ÝE÷aIwL Ä-‚ËÕº‡­•Û×"¸¡î©?ÉV7ÛˆXì®_ 6ïAÈÒò]%Ûã­é^R¶‡©ŒÏ•[›•îž#H,û‰@\‹³6e ®.{Ig€4²ítyä°´”iožï]¾Û}øµV?ˆ'Úa®°qôüïÿíËWÖœ!Ü)—y³Ï'ÇŒ5ðM7õ`D«ÙÞ~dG® á´ì!BŒMŸ>O·É€Á®®?3=^/÷×îäùµJkrÞݺË&›^"ˆ9ˆ +Ájl:# +lO…°d0*#T@oÎMBµ“õ¥=àøìjc÷Efx[.¬†ø\¶!#ŒÒ &dârŒ Ôg½´9íâÎMÉá Œx"œ;ÌÂ;!\wøY€âýÊ5„M‰VCL5ŒÊêàäÕúíw›·±{÷ó½ûŸõVp.!-B*DI#ŸîÙËQ¨QB^ϯäዲ]W€ŠÄõ8e{‚,BšP¾üvуrÍ(íõÄykÆC^_D}QÅQãbEʬ°fËÈ b¬ȃ)ðùVk»ðtçþ»û_tN^E¤ ”ÜÞþs:ÝGµ*¦Õ¹LhuZ9h÷6Ͻ¤öóNw€ áIoLsG/"áÓ냻/1½èe’Fë@*®XÍíÊöejå‚Lux³z~ùnóìe4£LšÔšFeg°ódrøQ²¶Ã/·×¿ýájkþêºw1@zq-ÈdÍòFmå¾RØÆöùµ¸rÅ›2& +@jÀDV}³ÜÛÝ:~<:|`MÖnèÕU­8àÒÍÒèøðá§ÿ·ZéÎavxŽ%š!&õa2~: ‘–idÛ>B»îD¸Žùµ÷­üxjÖÇÏN÷1×®¹Â΋ðiµ¼ivnåFùÑÝ0›ÍVÖÞ}÷ÇO¿–ó}£¹¥Õ7•âêv˜¶¸&ZÆä!•hÍ¿÷íÞ¨˜pm A¸"8êþÚí'o¾'§7Ç’BvÌfÆ!¡ÌgV2½³µ‹/µÒx¼sÿò“k޸ᇌ£2Ó=ª¸.NŸ÷ãŸ7.»€à"ÒŒ‡v"‰›SK;¥ÁíTe¥¿qgýâÓÃç¿:xþËíŸ+¥‰—4©D¥8¸ÕØx€I9Ñn£r´á2Žè Šžˆì ñ1ÓMðÏ‘é–&c4Q±vbz#ݬ¶F‡wŸ¥ä†Wç(Ÿ¸dÔ'@¥Cl‘òvc O”o¸béÜŒpY@~ˆ^¨ý¥æêîéÃöæi˜6²ƒ[ã³_ì^~·}ñyÿq{çR¯­§Š£oÿøŸ|ûn2岕Õû+'ËâàRX÷‘Ðhª4äÌúrDºæ¤¯»è¥°æ#²q¥‘ªoß{õÝãw¿‹qÉå˜ä¡Ì°X`í^e|V[½,NžÄgÛÝ}¨ÕQ)?]n/•”¤SÍ'Óíü"B„ËÄ•ÊçC„ +  ç“™æzˆNyÑgwó£óÆÆ£ó×ÃçÓ—féñG_ýé_ÿkc|´Q¨D›ÖیًryO\ÃÉê5LÌÎHw\a­6Œ•ìæ§my¾Ìu¶.óã?­?]9ûdíÁ×µƒ«»­ÜþbëÎÛ;o +Ã[¥Þþ?ýûÿýúÛ¿‹Ñ–œl‹©a3) t|¬¦zP7®-a|²íÃÕËÑë Á®¹o,Ä$C\IÈ­ñv?×Þíi6¹Ü +“q¹‰TÞö`*›jØ­mÈ!ÛÍN›ûÏGgo†çï:'oÓ+÷ÅÒº]Ù<øõ޽ϳNšíÜtVÉ)—ßcRÓíÝýÌM'êš>)àæ}¤3*¨dDœvž±Û»ÉöVi¸·}ïÓíûïn=ÿfõôU~|NÚý¨X Þ߃Ц'6oÆX½ÚäA{ûYªy@ "‡AwHc9º7µ:“+¥ƒTó„O÷–b’3&Æ¥@#ÊJ¹aºµSìïÿJ~@È™âð¸¾~·ºzžlìfzG™î¡˜é§ŠƒãûŸLN…Ù$à[®4me“_¡’ýéN£*F$nÝ[hï:£ +HXˆÎG¸ +¼ °³Í­¯¾ënÜ,"´ªZßµz·Z;Ï/>þóoÿetöªÔšý½[—ŸÒ‰ÄX®s«¹óIïð³âÊsµ¸(Bê¿(õOo¸ÉPØ°á Sam'—ˆPhú›ð›q©H%;ŒÝ϶÷A8Ê“ûVu a®±)¦:s~nÆEL©\mÙU­°áÇ G˜ìNqeªé„RÆÕ2iV쩤;ÅÞ¾¬wv>ÿuyý~º³Ï¦Z1.MÈyÀ™ãÇ_½ûá_WŸÇ¥²’Û ´¦/®ùcÜt†ƒ»¹Ÿ÷p‰âÆr˜ýÙ5çMºä£=a)D¦I½¡RŠÝƒO,­ßß|øÝÑË[¯âR¡2>ß¼ýîâãßvÞûäûáÉ‹To·}ø¼´óÌèéÕÝÍ“×ý08xÔ¦•· +£óL÷D(îD„ò‚ºº¼á@â|áRÞ8 Š9Êj²©¶ÑÜN6·öï¾¹xý½ÙZÏ;§í‰z§åÕ'Ý×ÙþYˆMFØéce')¿¢”·ùü–\Þ§íq\ª)Ëããr1žhø€°ä:* UÝ€Ïb³øðÍ×?i™f\È4ÖŸTמä{çZqÇ‰È  +¡¬Ëg_7F§³ÓMfMT®Jù5»s«²þ¸0¾GÉù'¯¾¹ûü€ðA2 $Ò?~sçõ^þdtNƒte­ïÿø/;§Ï¨vÓ‰»Q].l›2Ñžqã!<ÑšÜÎÏæýL0£lMuÚn¢²Å•ËO~õâ‹ÿ`WÃt’›ÞŠßÌ÷N6Ï?¹÷æÇöÎ3ÊèjM´GT¹¾ù›®kóapƒðQ¸Júp.Œ+éʦ+$-¸ ÐÍ›Ÿn9Šš”RC˜¥•Äü¨8¾]™\fO¶[Íõû­GýƒÇgß½üæOõõs­¶~ëÙo.?ÿËæƒ_‚^|òÝ¿o}*åÆ¥æÞþ½/ôÊê2"ù‹7ÛŒšÎuGøeØù~œ¿^ñ¢"ÔÎîdGgÅõû£“{GÏ”Ò`ýôykïqztfÎÕÚ.“Ng5F \j}Ó/؈^ «u kp7lôÜT6¦ÖéÌÐhîÇ5*Ý¥2#Ìè2É¡·­æ'Àl«¤\Àä,"§²½“•óÏÇ·?oí}TY»ŸéC}ëZåÑÖýOõÆ–ÕÜÝþ,=¾+”Ö„lˆuãøi”Õ]1!Ý>V +kt¢®æF:íEd\Êêù8¥ç"3NÌ'£RèŸÊ¹•‘t‡E³0Š2†+BúPHþòÍo¾þaõäe¶¹#gº~T@htjº—tEx\)–úÇ¥îAªºî³×ÃŽ0¡S`ÛâB-.gq>Yél*éê¬;ª¤û£[¯¶~Õ;xšl`Zkzs@+¼ýîãÝÎ8XžÔê!:ãŠ{jLs©J_ž²•ÏÓp õPÈ®¹ i´­Bÿåç?½ÿöÇg_ÿÝþ£¯[»ÏÌÆmMçvröŠ—°Q>ÂfBlª"—ì+ù1e4èTßGج٠Ñúr„oÆ¥ß_¬T +~"ðÕßy(åºvmTìf;ûJi›0z¸Ú$åJ”2æÞï" !& ø¢N7‹¸TŠÑ6¼‰KyP%B-B|ËÙ¸²ù åÃTH >Æ'D»f”W:Û—ãã—r¶—á §k:Þϸ#ý¨àU+jq5ÊØ>0Mò¢º'*»Â\Süt²4>Ÿ|Ôܺ4껨ZuF%5Ù²Š+(Ÿez\(’r]Í I¥äõøté½àB˜qc¹>¾6\ö³mÅ“ÖJàù¯Üô{#ür€þp>xÝÌYGDp‡XV/Å;.¦“­½Tû˜SíØ¡ÑQsó^aå"QÙ’ «8Ìvw3Ki¦œmò©fºsœÞÖóbÄ°”ê Éöœ‡XòQp +@Á˜˜÷Æ$ˆ±9W ˆä .fx»e6w++·Gû·Ž/ï¿úråð²¾vgïÑ·kw¾Ò-ÒhÆDPº¬”iùN¾½iTÆ^\Ë÷úV¢¼Þß}ÜœœeJÁhóÕ»ïFj¶5ܽwñú׿üý‹oÿôôëŸ}ñÛ£gŸ­^<ûõÙ³¯‡;Î?ú¾wüfpònýüÝpÿY~p+.çS…ÁGßüýðèùM±dH½*dÇöà^mó)¥W½øöëŸÔtLJ›A>«7vòã‹õ;_î>þ¡{ò)ؘó¯?ýîÏõõ T­ð¹1fvqkØÚ|vðôw•G1>ýðÕ7µ•Óå˜XíDô_ĵV}ò,º’é÷×| ZZ9cÓ݈g’m!7(®ž“É–“”Lü$kÖàZD¸$¦–•Â¨»ÿâÑ·ÿÒÚ~iž(­9t…¦#|ˆ +% ç0>ç—SÕ œNyí‰RÙÅÍ( L݃â,eVý˜ È`”W“M6Ùq£ò +X˜”k¸Xf”J}å,®ä8³ª—F…•S¹ûúÁgÙ»ÿ‹êøÀÈ׿øþÏ{÷Þ¹ã‰y?'·ÓûÍíç·^ü”nïÛÅÑÇŸÿxÿÓ—Õ…>HÌdoíìÝùëŸN?ý³Þ<¨ ß|ÿ/¥á‰KøI ¹²z¹}ùíƒ_ü]oÿ•VÚºûâ×¹öÁµeäæ2²c\‘ÒùöÁøè¥`Ô>ÿü÷ÿø¿0z·ru_*íhµÃ퇿9~õ·¥ñeŒN?yóÛÇï~gU'0æ³>2Ëé(_d“£·à¥On¿zø⫦}pÓóÁ ÷_]õ^Y@üxžƒ•ï×Z;g­–˜Òf87*L73¢µ*@VœK&rí0•Ó¦•ÒšÝ=imÜ_;­Õ6c¬™oíêÕM?3]fåBÔ¥ïEZÎHF)ç½!Œto˜pEؘ\@ÔJ`ºÄ¯ŠÈe/&+é¶ÝÚ®n?ܺüjåΧýWVûÔã#„¢d:a°²\H9,ã:“¨gÛD¢e3ve )@¨~*¹U” t Ùl‚²‡05ÓÞ‡”—Ì-‚˜g¬Tq©OÅv†q\JÓFYÊu:»¶.^žéÅ‘]]1*tj óR²ÆÈy”6h!IŠ¶;*Ìy‰ÀûGRGY3F[P„D™3„`ûã \.±‰Ê¥PÁ&Õ<¼Àn¥‹ƒÎø°6<$ôéÓÌÎΓòêíd}UHU³H »‚èF•ÅËjÁj’já’!\ ¡"¯­Ê:—»ãF²±owŽ´ÂJoçáÊéËâÊ&ØåúððâU¶±… ùts'ÙØÒŠcÞn@)C9;Bšü´¯WnÞKùâ:*UôÒv²~ §‡´ZPŒ\µ³ÞY?…_°ú§f÷´¼ù¬²þ(ÝÚƒÊO(EŒR¶Žmž½ßVPS-MoÊfÓd\ ¦ÉæÇï~÷æë}˜vm9¾R\d6ntíî™Ý:L¤ª¿þã?}üåï~æúBÔ‘Áüg÷›ÇŸB„ø",+gìòe“P4p%oTVÁûµ6.ÊÃ#LÉ‹Znmï~¹¿çÃa*ɧº¤V¥ÔBÜAÜá†c„ k¡BIZ+æWïÉÓÞk¤Ñ¢${¼~ºç«¾!d»f}Û¨Ƥº?®¼%$ +þ(ˆ‰q6MðyV­ê…Uà2B,*fSÖ«ÍÞþxÿi€IFy;ÆÛA,áóÓc–ì\sÃæ„<ä’‰1)øB+‘jسÊF3J£ÒèÔj¨…õæÚ«ºe¬Öè¸ÜÛó£*ð`˜2#\Þ‹ç¾!X8$OÕÌ‚žé:‚à$5ï”Âô acR)D¦À +SgÒÄ•J¢81ó#BÊ¡òc*9=ž‡!y3ÎqÎ"D\Ž²V×c`zxîåðà÷ˆi!Êòa "pQKp𸜎sF±»›ëî'ë;FeS˯1z#J[F¶§çW—¶*éæ)¦ !´üˆ¶äŽù˜¤¥9-weÎ}1ì ˜X!ÕÂCD +X,YÝ 50*Q6U7H&#\Œ1Æe,:‘`”‹;û°97 +†$Æçã06…2‰e’H63¥5„²¢˜%MŒÏÂ!ã"øO³Ôs†H¨¢œì–aä,Æè!„„q +¼oµã „ÃQ4Îè˜Å¥\˜2\aŒî²ô#Òµ¹À’‹†/"DàzÑF,®øÂ\W;·e«DxRÊŹlœKbrpÑ›_º|8N› ®Ø‚;Šëq:eæZF¦!ë”J þÍ Ïµ…ðÍ¥èât,é R|" ˆ2Ú’/~e>À$jRº—S]•Ò]gˆöÅØyzeÖˆà1&$dFNU»™úªši!ŒáGhGñFyB-H£Œž-¯T&·gýä‚'¶à -ºBɳ¼¦§Ë´l™…:m¼q‘×ríÁv±6†b0Låa"Û°©å‹ ¡¸,ê5ÕDÝ"}:€pPÀ¸â °i™éF­³Q쬑|"‘®@.ûPþ!TÈ«óáE7Ʀ§vcÑçŒ)!&G93N&j¶Á%Í,Žwï v\œ`÷’åµb÷@/LÂtÒåH)M)™éÌ: ¤v1ŠÉ©øbô’;ìöÇ⤉ AT€ã*¥zœ^ƒH`*‡’AÍ¡Q9–Ò£0xDòEx_˜žw„‰\¯ØuZÍ/û‰k ~—/>Ùº}|÷5Æ¥\Âf"ˆ$ªù¡FqÅfæ„Òh9 CõÁ%œ’(VÅX-ÇÝ^?Ç)VhaJ-OŸSaèD_„»rÝåò’.±°ž™õÏ-„>H^2²^ß’-5oÝ}IJ§Ÿ +DD¨*AT £2ɦb˜¶ì‰›¹a²¸ÅuŒ³¨¼èÆ~~Ý»ìg#¸ÇsuÆ…³«æ½aÖ¢—}Äìr‚%4N.`t2)/&û7Ñg=sÓym!Ù¡$+”h˜Éì郉lÓf|¨c-B)Qz+.•`8ôÌù—_üñòÚÏ®,ÜXð…^PsªU4sUQMžÝ±yúÔKónÄåÝaÎápÞbKL¤R¹z¾µÃëuBÌÄùiª2²…,Ã+²^Œb¥Â1š`u+ß#$8qÒðoæ +aZt£´;†‡PZÔ«RrÄL/ªÎºcpÊ‘éþ©d#­¤¡¡D˜ð™0Á8'ÿ$½ù—#çyßûGÜ“Ø’IÎLÏLï}G¡Pû¾  +…*ö}o ^ÐÛt÷tOwϾ‘3Ã!g¸‰›(Q"%QI‹²©Ä’%KÖjG’í›Ø‰âä&¹çÜÜœûë} œS‡çpKÕû>Ï÷ûù¨÷M–¬Ön€Ô}ˆàp`ðWNÍKFeÞ?7ï_ðÑH“ÄÄo Î$•)Z– ×æôE<è¹’éÚ­?|ÒÂÆùt©·§V@1@¦`ZA¦¢¤çmN«#” Ãnf;éBÏÆ@Òi6à‚Ë(.zC1‡7dd +‡7îE1nzÆ7ùzÊyþ¢†`M°{(*wH ¡I_óú³ î sn·?œPÕbÞ.—rx~kÆeÚ潸œ]Œ†c’7ÈŸñ>sfþ™gç^š–ˤó‡’NèfÑHWD½ %ŒòPÀZ³K‘gÏ-»àš[¸¼ÉÛaTZt„§ç .Ô`ÒŒ¶+ÈOÍxÏ]p°J>Féçg½³KQ‡Š3†¢—ã„îò1Ósˆ/ÈNºc60ëŠù# žÜ’'ŠÓAœ@r©F¿xWÌ\¡ÞÕ3…¸ {#‘H4’Hš…ú²–o?;í¹0ð†@­2ºn3p[-çVwv¹ÒbõļˆˆÒ JJ$Ó)»`ÛykssõÁK¯T{ã‹K ‹¾ ónœXŽÓ4UÅ`8¢ª’m§Žpy}~7AD-K.–Ì+‡[—Oö{—VsÍœjÈ|""%áÉn¿Î8t:/%”¤*$$ÅHã¬!h¨^ÀžGÞ"[n{CNÏXÅ.ÊéaŒ Çyw˜šóaÊéŠJQ&{q1 +Cçô"‚bUZÃþÆ/«  ón$Œ«¢½SÁåt”3òÍU!Uæ’eBÎ.ùq”A9F +¬ \2Œ +©R‘àM–“9šÌ¤õ„ždQVdDŒ¢v;¿:¨"D¸@ˆŠ "‚'üÁâB1 %ôP”õú¼~ŸËçóH’Ô(¦GìÞZýöÉðú•µW_{til$õ%¼'“³‹‘?zföÌE·ÓK‚†8áh˜$qZSª¢ˆ¢Š`>bŽ' +‰Ç(šÅá,E¯x#ôÔ¬û™3³Ïs\œ LOÖ8 +Ï;ˆç.„ž9ï?;\tã.éòD}A$•-EP&‚P’b‚ÀüF§)!³èAÎ\t,¸XŒã>‹Àš«æ…•®½¶Þ¬4íL!Ñj¤O/õîßÜݹԯÕÌj9m¤mQƒÌÎ?75?¿è £Ëé<ÉcÞ´ŠŽû…JÝ’uNÔD9eÐ<“2¥J%±¾l=}pôä¥kïí>zá X+\\ð\\ £È“ ˆŠ@%xTãc½jr¼’ïÔ¶ŽvÊêÞjñéÃC8¾ûþý_ÿÕ×~ú7Ÿ~íƒ'[›Õ”Å2"‡ò™Éæ¹XŠf“;Õ¨›ËÝ|·]Hª¼6)Npú‚>@‘ÄZ½Ü^ioìl?~ó«…îÀ‹Æ–üÈôÀèÙý˜PúÙwþò»ÏÿÓOßýŸÿíWùÉÓ7í^?ìf³v4Æ…",VÆTjyq£c­¦WJøjnåéõž=ä¶V²·.w¾úÚÉo~õo~þ‹ïøÁ«O쯂D/ºœî@ÜŠSóÞgÎ^ô;—t.b‰HF‰é¬[ç|’2j[Ї«ÖÛO®¼õêͧŽ¿õÞý~þ×_{X®ZÃõÕå­ÓááS.UùÜ +K h#E\Y1^¾Ö{tÜ<^Q_½Õý›Ïßüéçï¼ÿÒî7žŒÿî‡ïüË?|öûß|çÇß¾óûŸ¿óãOž?ÙÌ©L8è 8œ¡X%¯ÁúG%úÁAñÏ¿~ó§ñƧ_¿û•Ç£¿´ýWß¾ó»¿ó×?ÿão^ÿý/ßýǾøõÇG'õýõjÆÒ²Ù¬¬e•d‘aØ”m¥ã›5áÞná­ç7^¹Özz½ñ½ïþêGïÿþÿò7?ûè—Ÿ?ýï¿ûðÿûþñw?ÿÎÓÝOß=þÙ篿÷ôJÚP®è…ù+HŒ ø¢w<äqŸŒ-KÌÍíâÝêáȼ¶™yrkøÙG¯|ðÆÕÞ~þÍWZfÚc£¨Èâx!Áôsä^‹{aÛøàÁÊŸåÖg_¹ù“_þ÷ýþÿûûŸüæo¾#ð»ûêïýíï|åþÞZ%mH +jF†& ™Bj±^®ø{›Æë'¥ï¾}ð“O_þüÃ;Ÿ¼{åGÝù?zû¿ÿ»oÿç_øëï¿üwŸ?üå÷½~«”–YŽqzÃaà E+êêZž}í²ùÞíò×î7?{kçoÿìÞûÍ×þñ§ïþà[·~ý½çÿù'oüãŸþø§ýÍãŸróû_;¹s¹Z-Ú^è£0‰F±ñ ±½œÝliw¶ÓßzºþWß¼þé[»¿µÿÛùŸ¿úçï_ÿ·_=þ¿ÿþãÿõýôŸùµß~þâýÍ×þâÃÛ»ƒ4Š¸/ÎÍ»üÑh”TYvP’[F¸©ùnùÇ—³.g^»ÕüðµÏ¿~úÉ;{?ùôÑùûÏþîoÿîGïþËo?ùñŸ=9Ún F«vuÄÕ´i—,¥“aö<ýǃŸ}çîß~ïÑ_|õø»ï]ùñÇÿó¯¿ñóÏþþñO?¾÷Ÿ~ñÕ_úü§Oº¾P»2P +)ž¡¹¹yï™çæB.—ÁøûätYº»m?½ÑzÿñÖOþìÉ?ÿâýÿ×oýý¿ô÷?zó§ß¹÷Óo]ûË/o¿¿qc=ÙÎP)ÃbQFÐ1&™4«voÜ/õ Ü(ßò_{´ò½¯œþð£Û?øÖõŸïÁßÿõ[ÿð“wñÙ£_|túßðŸþæõß|ÿá§_Z{ïVáÉi³’ÓaÒ‚”¨SU‹jÛÄÉ(ù`/õôÈ~çvýÓwöó—OñÙ ?ûäþÿøßûÿñ/¿ûîÑw¿zçå œ!L²—@¦ùHËÂ7*ôµ¡öêië“7/þþÍ}ãοüòÿë¿þìôÖ_}xãë/ï]Z¯¦ QLµ‚¢ [ãÙ¬[-ËW×Ê/\®?9.}ÿˇ÷¯üîÇßøæë—¿ùòÚ_¼»ÿ×Ýùø+®vŽÆzµ.EV-@D‚,6q3_J§3ѵ©£¡}²š¹½™ùÊýÞ>¸ñóÏž~þõÛß}÷øÃ'ûoÞ\yã´ýü^yk9Y+zÐÞ0EKÍl3”K%2 :¯kUåÑQó“7vøÁ鯾÷ðo¿ÿêï~ðÖ?ÿüÿùäÁϾ}ã÷?}ó_~öî¯þôô³W›OOJÊ„!—?¦õL~ J¦Êàµ$ÙÐb«VøÞºòÆúÛw:ß~íÒùwßøý¯?øí_û§Ÿ¿÷/¿ýÎOþìñwÞ>|ÿÉÎú ¯hIœKŨd©Ò“x>%SÛÝL/ƒ²ÈQ‡{´W|óöðéµåµ¯?ýâÓG?ùøñ§o~ë¥ñÛ·‡××ò«UçPšP"$3=ÄPŠF:éo›äN×>\« Š·¶ë/Ÿ¶Þº3zçù­·îo÷S'ËÉ+}kµ½DÇQÌÏÈJÌŠ‰¢$§K¹|!¥æ5n³™9Y­\¦ß¾½üÉ—v~ùÉ­þ›·üѽ?}ºùí—Fof^ÚJ==¨lVeà±89=@hËáÅ"‰GªhøV]>^æïÛ¯ŸV¾ùÒÚï~øÆÿüýŸÿó/Þûõç¯ÿé7®½J ×ƒª!” h8 ÐxFã+&×ËË›ÍÔé¸|c»òâ•öG¯|çíkï<ܼ¹Õy¢c.¯{ÁðDùT~¥ÔÞ7rž×’¬eŒ+Û«7·º9êêÈxåZÿ›o½pÿxõÖNž»¿’iuSã551)_<éÇÍŸ§Ô©­|GÓL)4™7Ô¼!•MaܶO7ê·.õ®ï´.ʃj®_Êur–©  w…é¥ q9W¼ÞPÀë‡Ã*ÍtKÅAíêzî…K¹_Úøüý«úÚ¥|ãþ>¸÷ï\ùì½?}yûý»Ë77L áÅ©¾˜‚2i„J94¥Šél3g[l°mÆ.÷ôë«Ö½­Ìgoþç¿ýöüõ7úéãï}õÎë÷Æ·šk½LBWy­ ÚË(—F9#„r¯7ÊÍ´Âå5ºb y•)'Å~ÉêåôaŽ¿¶Vxþpx²Vm¥Ø’.›²,Њ2Î;뢦æbÓK™2dÇÃ!YºVÈØåL¦j›•µD:«òeSÒ¨¨ŠES¢T­ö!¡GâBØ .¦ËèY˜. +<+²ÕÕÑþjo­–R6ªÉ;;õš|}½|´RØ®i;u}­œ\-›eMðÎÏ:Á@L%¤ZKNÍþäÌÂì¼'â)4m+BÍ”sR¬ŸãÇUùæºýö½µ/¿°õÒñàpP7šB)%YVË[ÅÑ=ŒË@<àÁŽå’œà8I–µj¥˜5r–œ3E]a…ÕDœ•”4çO;\¥Ř`ÍH/"Ñ‚%O¾Ë+g õX8hL¢X¾ ©)–š÷cþéО0=ïÅgÝxˆÎ$+»Fý&åy½„³:Šq²š‰ÆXhQ4:Í~§ÕZ3’D/èA5Yñ†ñ3³Ž…  ³qµÍè-1ÙÀh}zÖ9;ïŽãð0+‘Èh #“Òk…Ôj7wºU¿ÔOŸlT®n-ÛÅ^!ÙÉ[%+åDÎL/9‚“{Û/¸cÏÌx§=dL€!Þ*6vsÅ>KÓ +\NƲbl¯W|z÷ä•7¯î×[ S•(¢8&¦ƒ¤ S‹³W +ƒ[…ÞLaå’£ÑÎÕ›/ÅPα"HE×óÙl½XjÛV‰cÔ€Gå¹Edj&4ë"}˜åBtgD &-罞h$ˆBØœ3¶àAÌ}Q!ŒAcZ8k!˜àtB!È€¬7Ì‘‚%“q6˦ºrnäŒÊ_˜r9ý4‚'1ÚPR Nï T*†²PiõJ3ì $U;m7T9…!T“QB]t¡g§ý!"çò ò_qö ÏÎÏ,F£qYUÍ„–‰Å¨‚ñ´ K*D$,*Óq™¡R2!çœNtÎñVß^¾¦äFRfTʹV€Î¬Nºy`Ô¶c|*Bˆ‚’nô7gýÈÙ…à\€Ã“]!¿!dÖˆD=&h¥ÔX=¡Ìö\T ñ“›2"\ÎÓ"x2Njoìì¬:¡m*ш‹åžò†Õ¨?,¸=âçCÒbXŠ0Ál)™®˜ª%sRÊÀX­¢Ì›!´­7Y£!eû¢Ý¡Õ,­WƒLÖƒé><é‰a&¢ÒΨ"(orz™RrœQ× ÃD¶'§ªqœ'YIÒlVÍO;ý?ÄtÖèKåƒ_˜PçÂÏÎÂb˜+³fß…ˆs>bÎ;Y“cÓ®#L´l¥c4?>¹=KA2 -ª<77Yþ:HèŒ^·ê˜Út¡)7"ûqMÍ´ÂÚ¬‹p…„—¦µ*kuýdÚ‰$|˜Ak-™¥0ëabBÅn‰™“ê…ØÜŒ_pùXN´9%‘SëryKkîe—¯È¥q€Í:¢RŒ1+ýcB,Â[ËÙ‘^Þ5ë—ÕÊÞL$ñœ?³„ºc“›†þè¹… K1:µ¬µ®¤Z‡bvÍQÏN½aqÉ/|qÊ?µ„†¹<—^ÕÊ—E{Í…‹õ¼“Xô‘8g…âê¹i8Kfƒ! Q¯±Rvn1àpG¸Å'ßg9#ÒBPXŠ*^Êvãæž]’ÊÛQ¥„+¹õK÷*«'A1‹jÕˆX Œk¶äÌ ºvÝË.TŠ+E!?‰xn€ÉQÖJ±}ÿù¯™ýs^Ò‰YLvK©Sé5Dªx1mir[zŠü÷cÊ +±Ú ¹1•ê±)ÒŠ3–ðà†ŸL¡rϬ$ÊëŒÙ s%·b·ö)­Ê­x¢J¦F÷j¢u¤Tw„ì +©Uj÷ÉdÇ‹©´1ÙN‹Ë±d;$U"rElY½9<|â3½„QâJÍG˜a¡€(U?iÊ©fmpÂXË“µÄ^HªG•i¬9ÑT€J»ãÚ\^’>Tf´•Åĺ+Ä{Â<§7q­á#M8ÿdsæ(¦ucZÇEŽ˜Z…^óc:¡”=ñÉ"Ûa.OÔ#Rm6ªM9Éo»¢ÜŒõÄd?•&µ¦Ôp§€JUBk +öp!$ú+È#B-ÀäQer—Í”»àˆóæä+ËÅ0ëŠÉH¢I4¢J³”ÑŠp™ mAkÏEÌe©z@gÇReO*^:·DÌzYL®’ZÍ%ƒT΋e=qÄ9Âæ¼ñä´›ŒŠE)»*eV½¸9㥖¢ªuˆÔŠ˜jûcÒ…Eä‹|³N 4Ѓ”íB4nxbJ„J-8ã kP¸bvÈçÖéì•Y‹JÕsóñ¸±Œé­dimïú¹þ‹J†Ä\æ·¸Ý1YÎ=½<ù±Me]-¸2á—âF<ÑÉ­ÜÉ­¿`._‹N–Õˆd›Ë¹üVTmøŒ¿}TÙæs#–t¢c­¨õ±´O“%È´ '@¦–ýtʪD²)æG©Öe»slµ.k•-£¼¹~å a¶cɦ9¸ÑØ{¹wôJkÿe«µ0ºÍ3?>r‘É Ÿ…ÒâòÛÉÖInt7;¼›l]r“{Ni³‡kí¨\ú$ì™êtÖo­¼A­³nÌI¤ÔÊ¥ôòídëªõýLÁ]ÃfQ©èA%O\š Õ×ôˆe•‘òëjy3Ä›~Æ ðeÌÀ`¦@mŠc“ñÄÑU13€ë °!±ˆ¨ÍX¢G¦×0­"æCDÑlñé>‘hø0D±†‰Ê¥d}?Y»´”hc™±VÉdÞËM¦Pã¼—÷¶ZÚ‰‰å n,À¤ãz“²‡dz5®·hb²jv5È—@áÃRE(laÆ2›Ûˆ§.¦­¡VÙ BYª*ͯ/dW£à#¤í',>3rEõ9/;Y¹M€ljµ½¸Ö]ðÓs^j1$Cßî=3åøây÷³K¸›Ì1æŠ]ǵ֢u„*mRó’–‡JûiÚI `¢3»}!,f1½.ׂMè5ÒZf¡Í®^VŽùLoUH«/`óˆRq2ŸÔ5&ÙRŠëN< ÃKÛC±¸•_½ß¿òveëE—±ó£•».:½8Ad¢bƒÍŒî5£N÷D³¦×Æq£I[=(QÒèÐé¾VßSËc$Q’ ++ã[o®>,Õ:d [© +ý^î]^9|<bÉdÓî]ͬÜ+l¼^¹Ae×bE-»û/¥;GND×ÊÛƒË/5wž¯ŽïNÞà +ãPˆýa×ZT…‚1@Ó:»O;¯d×^L´N"\1ĦA· «3åc—Â2*Õy{‹Ïí¨Õ£u 5Úl%ê—ÅòÛ̯ݫí¼8YÕ'·QX¹Å¤¡0L2Y§Ò+B~M*m“ö†›Üì¦dVºÛ÷bbÞ‰¨PWBf˜ê\£3xf=ª¶±d¦y¨æVgÃR€-zp;Ì—…âžR?¦ì±‡Ì¥õÝk¯¸1cÊM] ˆ \Hhë¸õ=BïÝùÒûÿÈnîž÷ñ‹¨AZ+™ñ µË¯6ò…Mª§—@æƒÒ”òy/“ƒú±:×”ÒÞRPñ °*O{â ]S>~6šÀR¾´o.ßúâtèÂRÔë<å‰[~" sNÔôP9!¿¥7./E¤ùÉê[FˆÍ†…R¢~æ +Q>SªxjÀdÖsƒ;ý—ãZ%Q‚Óe— ¨’õm.Óc¬6eµó£ëJyL¦Úñd[.íÚýÛöà¶TÜBÔúB\ ð!?B”šhÖ´ÇÑD¼²~·4:Ý– k cŒ½‚é]&».”7Y«Ýß¹½~ú +Ÿ[ƒç +™•Âê­ÚÁS¡²«æËãÓW¿ò½ÂèÈÅXñä²Õ»QZPÛzd÷Cœ5N.E¥’5#‰²‹± k¹¾õâòáÙ•¤Õ§ŒNqt3:!±Dg×3ÃÛreJÕKri㬠YŒ‰ò@¯Ž„üÐËÃr‹MO~o¿vôòúñ“óжa‘к•ÑƒÎÁbí4¦÷p¥r÷•3­}wÜ ôvwïÅ;¯çΗ>Y>}Íoø™œ`-¿òîgRvxÑ/ø™"—ÛIvn–7¯¼•ÜBåbsýZº±5¤]¤‰µy­{ð¥Úø~c|Nõ¢bÅjº1Ó¥hkÀä6psê—î^knÞ‡ü›}£uBÛ«TºO[ˈX#õµ£GÇ¿òbójiW-oóù5Tï"r=È­SÝ‚ŸrsŽ¨.äWÕÚNª}˜é_]âõqfi£±yo>ÈO9 f­x@¦G4¸›ÚŽHÍ ^ÞÈ­\»ÿŽ”jý«g.:ã€gri¿4~4¾õUðGD9~þ«J~Õ‰é.<3†~:㧳àMJå °úpóæÓ/ fûÒуòðLÖ»«–6òý+¥ÑõÒú-½½O%ëÙö%x˜P<Õ„Öc…6‹›éåST­F¤"˜Ù¹‘[{±¸ý¢5º…§Zzv°vüŠVÛðs–XØHµ¯,ï=Þ¿ýöÞó_¯oÝO×Óêê)o÷SÍýÒúÝTû8»|¼¼ÿâæ­·ŒÆZupÔÝ~Èçסn£j5$äàâZO*íÉgµã¤ÙñÒéIÖ(nS ØöjiüBçð5¨I±0N4/w/?îŸ~É\¹3ú2í&L>³l÷ã,c·bz7‡pbzó2Yæ¼à!£Bµz”ÙÀí>žÝ⊗”êžVÝöSF€Ô2­].ÝÁEµ¼–ß1Úû©æžÕ9²ûÚ=T*;Êd–Gru[k^a‹{@éÞ©XX[BäBÿJyã.—ßó` x1¥®—·6ŽŸ`‰jL*._zþäåo­ß~§±÷8;¼-åF¨wß|ò'fÙl¢¸Q^¿WÚ¸_ܸ_¿ëgò§÷ßYÞ¼¦LTÊç×n·öž6vÖ^¨l?Å’ý¿„)U/aÈ¥M©²Ã—6ã©Éà€#Qœw±~†ÎBÕÖ”3vÁƒM¹È]Àõ“ê‹ÖÀŸöBOÅÔvX¬Æä†#yÊZ?~#Y»âZ€Í@%øÈ4PwX(bZK.m[í#èeR) ǧf}=ļÙÉtŽW.?¿V\½!Wéd‚ae|w)*8‹Íô­öiiíaºÃêê=6ÕI·.A…ˆ…õd÷Të^¡¡}´¦Ý<ê¼"–Öƒ¼]^·ZÍ—n¾Ö;x¬”Ö9£Ù߸Ùß ´šUßÞ»ýÎö­/®¼Ö=|%;¼Î›¥£Oö®¿î§MiÅõc ’í¾¸ƒ&ZVu|òøýDy @Ñh¥]©v9»zwóîû½ƒ§Zqsëôµæ¥q³©4÷èâ.j®rÅ=½y-7¼K˜}J+Ÿ»„D„ò$§/_Sj—”Òf²´^éî¿ñå¼þ‘ƒH{èl,ÑJ{½§«w>,ï>…¸º{üèÍ¿Ÿíì/b©¸µ37Ps=¿öBïê{fïÔGjqU.¬þaußÀjD(‘zÇu:y²~åE£ºÑß’s#\o‘F×KgQ œÈ3·|O/späGq­^_½ÙÙ~ U/!R“+™æ®^ZI%¦¸£w¯½“Dã2x¨3|XÒ¨ï@ÆœõQKUTn+Ѽžh¡j-Âe‹ÃÃåËσ›Ly饘K4öÕdeG/ùTg1ÈEé„Q‡™ü|@‚*ºàáÂZˆÉ›õ+¹Þ*ÙM×w,h"ÌtF'bÌ$W,E¥‰Ê~˜-0SfúÎéäaäñd—²GA±â¡íoÓFÝWˆdÝhí'j;JqxÌÝ‹¥:N,¡V7ùâšÓ"Ä¥6¦7A÷x{¤–¶!ªÉd¡œ_9Õë;\n3Û~1’Jzí¸|ËEø•ê0©Nª2N7·åâêBLŽ‹Yµ8q:©d8«Y&Ý;Å´fL­FÒÖ +‘ê.ÅxÊ(kÍíÂðZk÷qeóÁbLò ‚`Ôªë×'¹•˜¬'ÚùÁqqt\ž#ár…µ Õ¼tA¢¼9¼ü2Ž\Xÿ‚¼è£¤ì³³ÒKèb®o×7ÊÝ­+SåU™æ³›b~—·× æ@dˆ¨-%·Þßî_zÞG¥}tNª¦VžWÇ\vÏ£Jûpïa¶µ ñßCe¢rÍîœ.¼ZÛ¸“ïYõMH¯½ûI}tÃÇ•ÕÆecy²¬zzpM«î†™lwtòáŸÿêÚ“o€b»Ð•^U«‡éåFó@Ê E³³yùÅoÇÔšŸËåZ\ïñ™u"µ•›!¶ çWšãëˆRðU?[ZŠM>¶ÂU»³×Ù½¤M\)³É¶’‡«¸×Þ¸+ !€yšO¶{›÷Q¡|nŸñò@k¥áÝTã¢_-'2+OÞþÓK§/™ Ÿ]Âœd!*µ’å½Ty|s|nRŒj€0x»‡*5ÌåF\ï$ëW•ƒDu/äq˜wÊêâÉvX*‡Ä2ª·A·»ØÜð‚ŸBå*(IL«ë]D.™LˆËÄ•¢Ù¾Lèu)©°®5ìÁ­ÊöãµkoåWo¹ ®U”ÒêbXòãfˆvj„„ +¢Ô±dÓÏj¶M%T r…%ÔpÄ“ÀT¬_µ/¾vøN5Š)«ÏäÖ¡Â1¹˜nìõ_öRiDÈCÑbrº^¯ŽåÒšw²õžÉ¤Z~*µæq­ªf—˽=Ñî^ð0ç=ìB4â‹"È/mÄ7›fs»µu»¾uWolOJ©²é•¥¨Ræsjy«>~¾wù *»íÂ23>AËÕÒº‹H^ôÓ3^Ú3±Á綄òî""ûp½2ºˆ~f3~* Qתäº×Íæ)DB/•S«{„Ñqã:<Þ‹Èvi}|òJyõj”Kƒ|Aì-!Lmû˜¬— *U'vm´‘±:bi;Ý=©o?Ô›Wœ¸½Õôrut]ŒŠU"µ a ‚a¢´Y\¹®7Bdj0¾Y_ÞÿÃJÎrµ!gÕ¶žoí¾áK ~®h"Æä:·”=¨¾Ql–1{‚ÕNú±ÉÝd‡§{A1ïŸU*zu €6&Aúò+WåÊ–P\JkZ}“É \â?_XwD¡‰ZDj@§‡h¢àŠ.Ò‚Íw÷õÚ–—±£àûT6ÂÁ‘AíÜdÅx>;ªmÞsãœXiå4ݹ˜ÁÛC­¼õìÆSd¢Ê$“åø(›2—©Ì +„Dm†y+ÌšŠ½,ez ArÎCÌ<ï¢ütŠV*mÇ„F'»ƒýÎÆ5®˜ZŽiˆR·bR="ÑXŠŒVÖJ«B¦«äà´^¶¼…TžÎ¶…Üp6ÈøðD˜¶Q •êLz-(VÂôyô’òÙù(˜à +gõëëªëéÌæY3VPµ9àœQÅ‘C”t!¤ºZy›‘L”òjLH(ÍK!¾D›ËjqƒÏ®‚˜{±„Y§Û—a6Ik€ËsH2Hf‹³ LR.‡§®ðD`¡0_œ¬÷‘V6oÔ{»¸`QZI/ íÎ¥Êø¦Ý=\«Á 1¾Lˆå³sñY/ªõDu7?¸&Ø/ªÍz˜ Ñ*Û¼½òÜLèü"âA8a8øìºÕ½)T®%E‘Àäl\.„ùìD…Í€PqÑy(?<Ñp#¢^ÙÔë`¦‰ê>–l{Ù¬7çC|”µ|¤î–ÜÈn ¹ ˜h)¿ŠéuÖ_=ymçÎ{•í‡LaìŽ' „jÍË~6 Rì!2 ]r~}ýèåêê‰\Ö&`Vq¢:pH\kÃ맻W ãG-¸˜Éµ¶äl‹!®EO49k.„ë‚ Ie\oS05öÀMÛóˆ ÌLsO0»gçýgæ¼s¸ÎÄŠ—ÍÁ´"TJÏôÐf,‘ê3Ùu¾¸ P烧‡¤%ˆ=í½¼âFE/¡-Å“a¾¢/ñÙ1Lët‡^“ÁÄây7µM¸ +–¤êG¹î5³±ÎG¨t¾{µ=ä–"²#–—¤¬QTmFäÊR€®Žíú¶ÓÝq¨"k)¢Îú9B­ª¹áœ¸à@þ÷úÏ‘É%çÜ„5lô£×õâ†Õ*ë·6õÖe!·ŽLVæ”gÜ8BêJº5ïFD˜¬ü£·&š€k·‰vgãÔ,õQ6Iè7i£‰¶RÜ"í([¸¸„Îy¨Ty#×;¸è¡´›>·€AéÊù±”[÷Å“K†ÜíU +áæ‚#õÚÄSÔŠ—°Bl´ Z ì èÆÒ™ ÔZj½°TqcHPN»" +Âæãj“Ímʵ+LfÕ‰+s!2mÖ ¥—Ê´Ñ—K[ù•ë…Õ;¨ÞYŒ@ì-%«[™ÞáòÞƒÁÁÃDmIÔ³ƒ+Õí{ByóB€£×Êû”´0"Í¢’TVozñÌ‚[Mv®§{W·P­äŠ>ÒŒñYxG?&‡Hƒ6–Õònuý^¡5—=„1í£Q¹ƒC­‚rBçó[ } ~*³–½xjÛ•¥ü8;¼•^Ï­ÞM4O||¢œ]. ¯‹ùµÔ°ÙGµ“ÁÓ'»ËQ†YìÝx-*–fBÄpDiDIJRÚU«Gà3Ó^:Dç8sSÛ˜ÚâìuÈé¼ô™9$ÆN–M‹)%/nøâFL“‰FD(B> +ž˜ÜZ?MVÖ¨ÂZË©Ö‘RÚb­üóŒŸn6|Þ'•òÔB4ÊÙZe³0º}éáwZû/ ÙÑùŨ?Â{Rv• R¹‘@Ò0 *=í!' òHåňþÜB ŠþŠ°iJÎD)‰TlB-‚ñÁàÌxÈ?¹àûã³îyƒÐR)-…éY¢ÓK(Ñ©µI½íÇì‹"F›c̺‹óÈŒƒŒ à¡Œ3"LÖÊ0úb~ `ÕOÆ èNh4Ñ$åDù’V?d²ÃED=»÷Ä“!&ƒˆùˆTdí©8Æ“­0—vPW  +¥W!WÚ“ïh^*nÜ÷rÅi&e^Ë ÉcT‰d Dqxsíæ—3ë7³ëgs¬Ñ"A<ñ¦·rýë;7¿:¾þ~~íÑBPô!²d÷hk9Ìç¡ö£%WÌÖU9¿ mW˜å&göpµ2Ùy„ÎN{ùŨŠ={ùºTÇ•Lï@.­{H”-Àäik”lªUˆ3iV/.ï¿þ Q½Ù£¨ÞÁ’ÊÚ »½å3ŠÝÍ,ƒk³™1³¬6h£m7v«Ã«ŸAÅ‚^ÙøL”Ælzd‹`mÝ”§5!`:>¼©˜ÛˆK ÆXXŠjjaƒÒ› Þ‡›±Êfljúe³u%*æAKì]^Åõ¦ŸÎDÄÚ|4 WäBT¨”Ï\XŒRJQËõ¹TC)¬‡Åâl›õàŒRÖÒä¦9O¯‰*Vü„厧.¸È NŒ5Úq¹6ífT&Ày²ïX€ÎL~w†$«¡ס´þÉZÄDñ<ŒÑ!yÖÇN9bfu½¾v#DežAàó>ð¬b„²Aþ‚{fÚ¿=Q-ÄfÏNÖçD=1 `&"5µˆÁôrß\˜Ç ˆÅeŸLi¼×çÂj„¯8Ùa£œ•iŽßÌ­>ÀŒîìäæD>J©oÅäcu…â$)Ìè#\2BI!\Dø4i4©T Ð#]Xdz«¤¸t/¦|Xbâé•þîƒîÞË\qÏ…¦ÆŽK/®ùIS)lréU`¥r@¦†®xÊ8½ÒÓnÄÊx¢éAÍ(_ +ëÐËa.‡I9»µrî¸WdJîZ²{ ÀæN­æ.•j/Å*=̯Ýiï¿<ùFrpKÔ!ãäjc«±µÓ1mÙê\[Þ{qíøÕúÎCµºíEUÍîvï[õ-"-„Å…µÍ¦z¸PF(kÞ—­Naùº5—A œ1•“Z¤W=¨–iìg{'¤9À’ÝWœ +0qXâl"¦TR­+PÒÁI³§œ±TÈ´³&Æ›(—JU6kéɇq¹êˆ(gQ'¢yâƬ— ¡6cb-4YSZÛò“öœ/p Ú”kÜ€7…Q‚6Qó«JaÃ7ÿõ9/€–…‚df²#žrb)  »¹'ZËnxVX ’aϺȋÎÉ>°Ð#páÞ¸1M|q6|Ñ9Ã,Ä@Ê$k££/­=åsãù°ÒD'D¢†¥úqkDÚõ®ç‡&û ¡"—ª²fT+¬/OÇÓý’”Z‡><˜f¨T‚W^½£Uv"Ì´/ uÊì1V/ÄX þ¤ZÊtrk·q{%(–<¸æŠñDËOhÐARn –vàÄ A–\²1!ϦGQ±2ÙsP,JÅ=©|ˆ'‡Q.7íŽ=3ëõÄU\ë,eGX %6ÕÅÕª˜îf tÀ×âjð‘+¸±¬”÷æ)i­ÁØ·Øy?>Ø}`6vc`²lÎÛ2Ëze ²AÆNÕ÷òÃë…廵‡%[",!Z=«2f“iîŒÊ‹!)ȹ:ããÎ. SN\Hµ2­]"ÍN–±R1­UÝ®®? ôÞŒ—²™áÞÃêÊU7fD„2ø¦˜]óŒÕŸüÁCòéžVÙðPæ'¾Ó\™ÝSÛÂ@©$Ÿ(¶Æ7X»¿ˆ&ϸè _r¶‡Ê‹…íÅ 0½ˆz#]½õ<ëãž Ÿ]ˆyð4ˆ?”Öy'îÃt1;L6/CõÆäjˆÉ/FÔé Àg.&5C T(³ùM¥q@A†"Óž¨:ïe§$Á—“ùÑ´ù”û¹éH”ÎGè¬;¦zPuÚ±(»KoLT]ʵíÜòa®”_9‘Šk@‰.O&[öpX­£úÖK…Õû©Þu à„ D¤ŒNRf_(îcÖ&›;°Û7 +½ë‚=\Œò!Ú ôZˆKiXÂOYLªÓÙy¼rü.xÊŒr†9LΓƵ&ßJÕdó$3¸(M?ž,´¶Õ\߉%—P} Iú)HÐ5»}jvnD„Œ¹;üV#ŒÈh ˆêŽ^2(oW7x!i\©†…Ì—7¡±ŒOn¨ÌÌøp€1>·‘_>éì¿–šSnzÆϽHéž  ­É®\>ìì½ÖÜy Që³~H2ÝØN7¶¸¦ÀÔO~»r(UÂB¨æœ#6Ù?%ÙšvçôH`òÉáF¢vŠˆ¦M»1wT ä¼#Âxã’ŸýÑ0¹)­˜Xò㈠®Ö=˜ +ñ€^™ùÈ„‚”|W‹ç棸PLä7’Õ=±0öá–‘Cq…Rr´’ñÇLÈš¥±^Z׫»˜Þõs¥3‹øœ O~ReL»0д0“cŒk­ˆô'ù'ƒq!g×7#Ljj)þÌll.¢#ɱyÓì_çRm‡ŸÔÓ­\÷2¤Å?š +œwRÓ!ÕÏ×0k,f·—üsÓZ)7G7ÏÌDþèœj|~Ãס‹”á2´ÑàRuZ/áŠ$5×d)­¤l¯h•-:ÕŠp6<@ÊÁæ=¸ª©ð•l†ø ¡·¸Ìa¡ýQbˆéš|2d³˜Þ‰*M0t»u9QÞ!VˆHB‹ŠEÈeóQí‚—uDUÆè‚;¨ìÔ:9«ëˆé1¥Å¤7f‚òRT…f Q&.ææ<ñÉjÕŒ|K4!ASÌúxiCâXŒÊÓ^2Ä•hsh´®38åì²è¥Û+'©ÂŠ/&/y'¢0=ˆipÌ9"Q”­5ן¾ù)eæüÜ\@€fŒk?91Á%?±àÃx«ƒÉÅ…çŠ&æ|`@Â儱àÁ&•c‰6šèú™ÊE'>ãŒ/xqB°É¿8žñr|fò‹,¹´C¤F|Ò·Ùâ|‰Kvçý¬”î3ÖNd» ›Ö•²œ]‰qödñ\u†'9%H$i­Á§J~•Iµ‰D…Ð +\lŸç¥®Hƒkµ¨`G8+®q­Nšý™ ÑY\kñ¹µòÆýêöc¡¸íDUxL¢KNBn¹© +‘ªp‰Êl€g¹`‚Ä +ú‡õ‹BÍÌøxOä‹—™ìdw†›Šs0ñ ž’ó»\f›ÐûD²S“cç#ÞˆD©å¥°D'û\zŒÈmxePQÖÞ˜ÔŸR]€/™(Y ñ5Þ^Ò«Îŵæè(&™¢j7¤Lv®çÒ]ˆ™.T¹àƵ&ýá3I(¶“Ÿä Ô ×Kjs2HTBläÔ‰SnnÖ/…˜"oðDã QPföf°ç‚ìâËZu_.l„(kóô•Îöó!¡ê£ò!±²„¥.„Ũí91äik‘'‹Xú¢*X-XØÙùx\({c”PKFÙ’ê FáòPêSô[Û³çæ‹nÆGà !  ‰‘Ä™9$ßÚ?=»ˆ_p`aK°×«Q6ÿÇçýr1ä‰éΨ2í¢ÏÍÇá¿Ž¨ŽKuZkúãúÙ…è´›˜öÐ@e_ðaæRHuG=Û+vöà/º…˜² àŒK1Í…[ ¨þœ‹˜rá A>.U¥ìˆÐê2¹u%üd˜ÒÁ¼´Úe±°*ˆP +³¹Ù ðÌ\äœsDP&ã@„ Hà ˆ6Q©æ 1©Ç<¢L¹ˆY? â$¤$ÈQ“k€‘d² %*…Ü:“D„’²@¡Ë@( ]pŽ‹¸Zƒbóŵ5YSÍ€¢¡)f=4\`”Í‚Q† U T('Â_>© F5Q;b³[P–|zaaòq7Ÿü>⽤)æÆlf#,!‰/"꺄$Â\ ‚öbƒ>ZŒ$àv{Æ/N9@X9»æ²` s~×ûlf‹¶Ö8¼²vÁC€:Ó͇Å)/‰HM,ÑáòëéÞäà OwL“Õ£;o7×N½DÊÇÃJ#$U}ôä7œ5pFÔ’rÃi/8Ù=¢û¹ +eùÜ. Ædª#/¢BÃΘ‡”yÄJ—ŠÃ[¤V?3ã Å5?ªÍùYÈ›Po\Ô™EÂe{”ÿü"îGg`TÔ †µ’!söª^ÝŸõ0‹Ü¢´³;’Üšñ³x¢æA|Þ7ãa½X +Wê”Ö@…¢+œ87O\tñˆPua¢´CB Ž€P™CP*“£…–h€õ£b1WLÁêÒˆ +yJïµËBnÓKäœhz1=XfÚ',€„"ª—0Blò/>h¼>Ð5pTXØkð`¦ÞÅ‹Ïù&€zcr€ÐQµD§—SËJyØÆOQÖF¹´QeÚûàì>6ŠA&jQÚò¢@).d£ Œe’>,á#À‘kP*Þ˜2ë&b¬-¤:„T +Ói€ÉCÐsaÀÕY`ž¥¨²ÐŒÙC&{™Uœ±@§:nÜ€<ŽOþ—ÛÀô~Tj‚G8"*ÄCFpµÔ@ËÐŒ~:Mí™°ô¯Î8A…ô0&–Á§.ºqÀxHú™îi¶wÍKZΘ¢ä× )ܨT0ícæÔL~pZÛyDš½ “5ê‡@‰Ž¸¾•çà +ˆÛ”“Š2ÆhÌý!WN6‡â+À.x™)GÜK„)ûüBèâ¡)†ÅcB\hmj)ÍOœqSq¥•:!¦ì'ó¸ÚAØÌÔBäâæ +Ï΄@$# &Å¡°Ó½üúÎýo'Š{ÏÎ Ž€àŠS‹q°ã ŽÉÏìÃl!Y¹”j reÚûǤÖv"É‹~q:¢;  ”^sãÎWþl RžvaVˆ«Ù­¸ÒãøÃnI?™öâV\šZ†^p “Ï4P¹ & Z{.œg!’«dr8R«à +0p¡uÀ^£|8hÜê!.«”Æq ^ß^Š&Ý1-D!BùòÇ•˜\ ¬Îä׿ö„4&WBd +!4ŒKE9ËCL¾»QK›bfäAuWDÂÅ"ÊeCäÔ¼{Á9Â<©wÍÖQD(,F¤ÐiŒ³—üô¢Ÿ ’&¡·Åâ“]÷’©™É&Ë +p©#&……¼—Îçæ2H%mö@»9®ÕåÒŸ]ñUh:H£q¥ÙÍbæ|®6ÔòAD,¹,ª5Ï¥/\ ¸B".|ÔŒ3yÞ”_«\â,HÊ@nš^AÊöâ ¬* òä˸Úæ½Ö¥ÇÒ¤ê*Õ•ëDb‰Ô2Lt.„©\„É.†8h–¸T$  ¸ç†B¢r:ë@rÞsn9»?ïa¦ý¼ Q0ÂlIʃ„5ïçéä ÄBJM9#òœ—~ö¢û¹i”MÈB‰BÇ)Õ½úÞS{xÇh]¡ÍѬOþâ…™€Ös3WXa­Aªv@ëÝ[—‡Ž«nq©>°™#nÅÒãÌèÎöÝ÷Ž¾·qôx!ªx!ˆÑÙ C•º+n3ÓúÿÏÞ›7·•-yb_ÀüÃá?ÆÝÓï=Ô¢*mï9÷ž»Å."EjE ÈEª¶y[o/z<=Ýîè‡í˜‰Ïß$;™ç\ÜUôJÀÞ«*0q—³äžy2¦Öö'VÑâÓkÑÝ+ÁÝ«$L'&w¶‰¥ž\[ö现¥?21ðprstáY²ð¸zk•TßpzséþWö~GHx}t5Yxr5¾CªQ4ûlt±1¶´Gê!fxû~<ó€8†"µmöA´¸³±÷×ë¯þH[LäæO¬TGïTÇ­ðn­ÜÚgÉX8œZÝ'ùûë›cŸUÇ’ÙM3µîݾ-l“ yçñ·kõ¿£}éM}zm4„ܼN ¢»Ûc«¯&×o­4nŽ×¾ð''V_Îlm}õ·÷öÿ&©íú³ÄÙîÓOáü–™Û$A¦¦6§6Ž6öÿáþþ?»¨ÜýB͘é-}í IïÒÓÇ×Íì2u‰ÛÿÕóEõÖp8wUMúˆ->·õôÒåfÖwÕÔÚDíÙêÎïŸ|»€Æ—/G—‘íMmL¯ÕWž~}#Y"…yþÁ×Sû¤kyãë_ªùO†Ç¯›9’þÕèοýDycËS+õ‰»õá[›Ÿé…ÏôÜg#“$Ðu}Œ4ïÖ=ÒCF_L¬ì-Õi_>÷¦>ÛùÍõ±kh¦¼r´tns6:·=»~|ctõöÚÞôÆW$ÙÉ8ýÕðíOFý™í‰»Ûw¶öïl½úl˜–q„&mëgÞì§Þ¼™<±¶{{maë ñ"’פËÍ?<[Ø<š^~16µqMÏŒL>$Ü&úËèîgþ<ýÿz²<½Ú¨=ûöî‹wûÁ×þÂÎ521r†“UR&6ÞŒ¯½[~EAr™°QMn\‹–¸âèÖÉÖ™´›7câK´éDDdeGsIU!ÌŒ¡Ùn5þæÕïþ¯­¯ÿÙÌ>£‡ŒÜZ3Ó› !ÀÂör}ÿÝÄÆW™0¤ýÆ52¨‰{‡ÓÃÙGáÌóɵ7Ó›ß,<ú&˜}|Å'"!_ßùÝò³n¯½®NÒþôVm –‰¥gŸxSdø¯¿ü~µþG=÷üÆø=2Q'‰ù<ÿvn㕾u7˜Þ˜Zkì|ýö¿ÿßn­í^¿µrmlibegbõÕõÑ•âóOŸ|¿´ýû©7Ãã÷~s#ùROéÛëÄdÁ;±Ed;ºüU¸¸CÊÞÔÝçWõL4½IÆþ³£?®6~›,¥&iœ Þøò­¥gÞ­åê™3 3÷^mü݃×zðÕŸôÌ#RtI/úR>¹yëKE×Ü›\«ëÛ÷®ú3Ÿ\»R$Ä&7þê‹ðÓáI’·—÷îíþöÆ­µßToJ†ar×_º†”žÙϽ™_]½52¶Bm’º>’¬FÓÆ–ù€¢©Ý»OÜïΓw4Ó_ߘg¯<ÿ¾¶ýÛéѱé·•êø=9:ó`ØŸÿ«ÏÃÑòʳóýþéÞî»`‘DBç$çï‘h&þö½×÷ýøð?zS‚ÙíXåìÓk£+ŸéÙÏÔL8½µñâûgÇÿ¸}öŸ×^ýéËhùÓ²#fªc÷HºyÓü…ç·×OKç½%£ƒT2"ŸO½9BË`úÑÌúפªÑV~,ôù\Ï\~Åg=ˆS‘šÏ=š[ß¿ûè”JõÖ&ÙݫϘ¹G¤º92NJàÓùGÇaíe´\_|öí³wÿûÒöïnú12yedÂ]š®í.n½MîìLÜûŠæB›;½ºGjéùWZÕÕÒ“·Sëû4’pîY0ótzmtñéðÄÆÈÔÃ[˯îïÿi}ïooß; fŸuœÌ?˜Z}±ôð`½ñý탵Ýß?:þ‡G'ÿ8¾ú±\¢”å'g÷ê—júÙØÒÎ(YF³D&+_ú·ÕíþÁýý¿ßýá¿L=8%&i5µukig|éÉèÒƒÅ'‡“÷ßÄ‹{fobýsošæ2݉g6¦îÕIa›Þ8"%ÿÖRýÖâÎU±róÖðŽ¾µq=$Ù·F„I"ãöêÞø:iÚ¿ºšüÅ'ÞÄœ<#ã÷ª·Ö'ÖöW¶¿½¿ûûÛË/¯ú·«ÉüÒÖþÝ'ùyHÌŸÌÛ«¤äÌl‡3OH]$ÞûéÈídáñÿÙû'h»\Û¹õàšY™^yMVöµpéW_&ÃÉ2¡‡7õ„Äßõxíó‘Ùß\ ÆVn-<ˆçêÙG£÷7ÿqçü?­<ÿa²¶K¤4½¶ûøðoInüB¬€¤äêáôæ’_ÆK„ áÔÖêã“õço×w¾§µý"X¦'_‹VHxýfdâêè’ž{:óð¼ö⇙GoÈ¢ i¾$>H_£¥£ÆÄ—÷ƒ¹ôýS=û¥™™XÃù…ÇoçŸ~³øô›‰ÚK²å‰ùÓ:Ó.L,ïÝ^= Û|¢öbâ.IÕ‡7Æ–n­î®7~»}ö÷þðßnož|æ/:qUOM/=›0œ,]5s$kæœ.<8›^ûê³áñ+Õ[p&52 Æî>«N¬O›Ý|3µNêú.òfŽÖd¾Íl¼Ö³OÈÚºµvt{õÕØÂã±Å‡wŸ,<>¹·÷ÃÞwÿ©þÝ¿Þyñƒšyvs|ct‘ôœWáìV´°3yï=·Žý…§×ÆIÙžò'VçÖ_-"_÷8¸³£IAZªÏo½™ºw@òtìîÓ‡_ý~’x•þ5^Iîì’qª&î{c+ŸÝÕµÚ££Å‡'3›oüéÇ7“{×#BÎÍÙuÚå×h…3µ9¿y4¹þ:\Üßüt„xÚ )?“ χã%´û™ØX"Ìßùa~ë„ Û«Ñ‘íÖËïgWvÿ§¿ºI 5{ÿ`ñùwS[çcK/of~9z5X$%pâîËêèòðèÚäêëÉ•¯â¹í¹{ˆùBÍú¤u¯»ÕÔì3.zü^8¾fÈv˜‡™äÚ½Æî¿]xÏ<¦6î>|ýô«ßÄKÄQ¯­ŒL=ˆîÖ¯¯ܹ1¾><¾>¶ðŒ”r9„´Ÿ,¼Ò·ŸÜ]§WT¼Iî‘Yx®fÜœ¼g–^Ü}ñûÇgÿgmïï¢Åm"(béՉŭ¯ï7þ4¹yn^Þ¸uÿrW6õä=´˜¿['yJËHºÜøÝDd+}.’™/<»û ‡¯I¼žÔîÿþþÞ7kÏ‚…GÁñ±o'7N‚©{£ó®Fó_š™I¤,^Ñó_ú‹×ÍÝkÁbpá3¿9q÷á³7?½õšB3÷”8 ñÕëqmjýÕX­~s|$8R¿6^Ï=:»µÜ_&³Qß^™\Ùž¹÷l¢ö˜”½Ñå½ÇÇÿë“ïi´fúáp~bu×Ì?¼ykµzûÞÜ£ãúù?=ó¿ÜÙ:¸5¿M®“z¿ñêá"¨›cdæL¬l¾úë;¯™ùÁ¢¿OÈIÔÝݽ>¾AÄò™7™Ì?\¸ÿª:±:VÛ‹—^Ý^;zzôOÄL¢¹'fêÁjn”Ô`Òš¦áøØí­dq/YÚŸ\{MHò—WÂ/ôÌØmÒÞ?÷ïøsÏVv{§þ׫õ¿!µäW×ÆuuŒÄ_ñâ³áÛ›éÃwwgïMhŸÌ=%æù›ã$hHª’éw“plã )<`VckjtõFˆ¢úöýhîÙjáóêì˜0dk|ñ9ÎnÌn .ÿúú±Yâ·“Ë»£‹ÏüÉ›ÑÝ•G_뙇ŸF ×&־ıî-³XO‹Þ’²—Ì?'LX|v5\ùddázH‰7 OÞúóIóL56vQŠ|aûûቭ+fiøÖ½* +¬3h„·ÖÞÜÞ8¿µúšž£&V“9â¥Ož½8û×»/¾_{5<¶ú¹?G¡¦É$_¼-‘º²ôìwë?Í<8AiˆYÒOÒ¯î<»9±ö™ž¼1¶:u‡ªÂ¹­ûeü›·ˆ}b·î#Ô8yÿúÄF¸ø|æÁ›péÅoª3úöÖõhùz¸L?f¶èöhñŧßûóÛ5{•ˆzóhþÑ;²g7^M­?Yž®=[Úz=VÛ_Ùý"Z®xdyÝ%VCjùõdñŠ™]xðúÙÉßì}÷Ïwžž~Γq3®Í¬4Vž¾Esrk~ódéñ9rÔ7?õ&ÿMåæu³0zg›¨ææíû·Ö¿¾³ý×÷öÑ+Yxz3©ýêÚèÍø! )u7⻟ܼýëkãdq“Ár{eé‚ck«ÏÎ9¥aêŠY&1ñkoŽ¶†PhvãõÌÚ½ztiûæĽ›·}Ýû,X çw×ê¬=ùzdlyd´¶ðèäÅé?¾þÝ]~þmSexªBÜoâþÔÚ1Ïýű»õ7ú¿¼ù—¸Ö ]ý³áéñ;/מÿ@4û?þÏ×þâÓПzüäÛÿòüü_fÖhœ$qhFï<½jî\ —IU‰•ýÝÛø};¾ºwôêÛ÷oÜÚ¨N>Ðs/Â…]¢uûÁüýƒ±¥G×Ãé/iÇÍr0ñxjåðÁþߎ­5®Ä‹‹[_ÕÏþYÍ<4 Oý…Ç×&6H(“Ú6±v/>EWè‰{Ñ—ë¿yðO ¿Ñ“›¤ù·Ö¦j/7¿šZo|9zï“`Å›|2³~¶üüo¦Öß|êÍ^ —p(rç‹OPóO®ÄKW‚ùdæáäîonŽWnŽ«[÷¾»»ý»ñ•W_F‹¿¾9Q[¥ÿÿææí£k£+{w_üöÅùÿñàäŸno¾¹:J 8Ï>ùB/VnL]A²úº7ù`vëíWøï›ÿþú­õøîóåçß.=ûaòÞkorã3³ð™Y¿ób~ãõÈ8˜§š|Í?»ûäÝÒÓïF¦žÂs›ùÀÂc=½92¹Q½½I&ƒ™}Ï>‚)¤çFn­O¢ðŠ?П_è9²âÇIYZÞñg6ã¥íéͯ—_þáÎÓon$«ŸÜœú¬:uÝÜùRMߌæ«Ñ§×Ç>¹1¡'6fÖ¦î}EÖßg#„w6_“2ÿéÍÉñ»¯ž¼ý—ÙGgË_% ;X“Õ—÷ŸŒÎýÅ?Xx9~ïëÅgØÜÿljµ¯hÖõe|çþÁ«oÿ9Y|ô¹žû7_$Ÿù‹·ï‘¶<‡¾ÞˆÏßÿŠd:é¤Àüº:íM?Kw7÷þ~ûí¿z›ù‰¹ª¢¥„ö¿úbì‹°vóÖF¼D«wÏ=¾ÕVv~ûÕïÿûƒ×ÿaöáùçÁêÿwI>/º=€Ÿë3˜H¯}éµÏ`"½öL¤×>ƒ‰ôÚg0‘^û &ÒkŸÁDzí3˜H¯}éµÏ`"½öL¤×>ƒ‰ôÚg0‘^û &ÒkŸÁDzí3˜H¯}éµÏ`"½öL¤×>ƒ‰ôÚg0‘^û &ÒkŸÁDzí3˜H¯}éµÏ`"½öL¤×>ƒ‰ôÚg0‘^û &ÒkŸÁDzí3˜H¯}éµÏ`"½öL¤×>ƒ‰ôÚg0‘^û &ÒkŸÁDzí3˜H¯}éµÏ`"½öL¤×>ƒ‰ôÚg0‘^û &ÒkŸÁDzí3˜H¯}éµÏ`"½öyñÿ^’ÏJå’|þ‡+WæŽwgwÎv†.]™ºc¶éïõ·gÓ¡•¡+Û#S§g³õ³ƒ“ãÓï*£òª:¨Œ¬7v+WåÊ +]TY==xupLÀúÎaãZå&]:Fÿ yUñø¿Ž¢ ªú:Pq¬é?ºÅ‘_5ÆóïOû•#\dâªëPÇAàÅaù¢C\,H’8Ž‚0 袀ïJ%^ÉE¥×µuQéuûC†Ž‡î %•«×* mf¾ÑºÅX·éÓó·ûk;g´$Çébn¯œ¯Ÿ¿ðtƒ–*ûÃÐÊü¢<ùickaþà{0’~¥µy¸\[9ÙmàknS~ô‡±ÊÕoé§aÖéÁËó³Æ[ì íëéNáŠúþÁáîiã¿ëÊÈã³æoø×Ùwox_¯*Ï»r­2²y|P'ð=÷øUþÒw;‡çrí~ãàÕþÙO_}¼sÄÓXÜå7{yNßìží·=%{õ…ÏèäåWúÙôÉùñ. oúäÛv§·ÇøF—ž½m{’¹{.|ªíoÝ{!3£o{zßµ=ïº0©;ÛS‡oöw¶U»ó9Ø¥+ß3\s³÷ +›<;?}y~Ø8®7Ú]¹µÍuï¹h­z¦Ýù¼ÜyÛ˜?m|}N‹Ð>ÆîºðbßÚáiãíùaûâÍ]~ás:>Ù8;8«¿Gj5çõ–¯¾pØh_äî¹ðêv§v|~´Z?Ûy×Á̲·´Ã~düê'Æßšûe„rã~›ì¤9îŸØƒ ÞœƒãvwçäMãtçìä´í½iÞpá(·qr~Zo,œî¼Ù?¨·-éÞ³YwÜãÜïàø=T—›Œî"ñÌœ½9y{pÖí|Œq°–ÜîFf{•± Ø‹s˜€p`¶c¿dpït‡ôÆÕ“ƒ·—Ìl۲؀…© lÀ6öyÍÀØ€p`lÀ?Ç^ +.¡ ØÁœúÃÖ—Å4êd&=oM7Þ57öwvO¾ùeGÉD€²­xYÄgÛjèÛ³ÝÙÆ»ƒ ¨;{Ó…Oîåáù{¤×Ï`uICXØ9ûö`çxú½sìE»m´ÛmŸÏïvƒÑ·?‘öÙün7ø|'´ÒëLídoïmãìý”Ñ¿Ô¿Ê3ì?º?„®ŒìÂúÉáÉéè7ûï5q²è»Ãö½öê ǽ¶NÞžŸîíÔ6µÝYån0‰?c.oß4ê«çï¡¡þ󜶭 cþç‡;§3'ÇoÏvŽÛŸZùÆ.„?;åÜ·oNŽ0Ëæýd©ýIŸvéû¶WåûnØÒ¦ƒ©ô¸[@u²-:º¤Š¬ŸÕ:q™}_ecÃRlÍêý§]ΰC‡‚¶×õ†ruwèí=ê7séÂ)½ÂÚ–¯ßãŠÈì.½ø<Œ¶'ò6;ÕËöëö©þu7ˆ¾ý‰øíOÄïÂDvNÎögíGµûI~^Ê°}ÇJA¯KÐóµƒ÷˜ÚÞôŠôíÜ“Ü1áui?—§¯XÉþS‰:%µK¼%oƒÄ£AâÑÍI]¶Ä£_Ðá“aÓöÞõ¸/µÊêù«™““ÃéÓFãû¶£¡—1¿JUÛ>}º³{pÞ>͹Ë/|owwÚq÷ŸYz©’áÚžH¿ƒÛß™Ýöw¦M¦òqTÓ“Ó7û'‡'¯Úæè½c¶ ¸[q·KÃÓ.ÝÑÀöOû xÚ(á—&ÿõÒ|Ûa¾¡øÎôÉ÷kúîe­Ð†õ8õ{í+™ýYíàeûJtŸ°·Ë_ë íœø?»ÖAÏ"c¿H¦~®Úðž,• uvæ¢[‡-.Pê€Lz>ÏãeÛXÖ/ò§ýuÌÄ.:ÒvÐÄgpBz¹ öÑ‹l¯ƒíèqRiÿÐöŒ>èêÛë(“hïàð°“äµÃ.lëáÁqc§íl2ØëË'íçÑfnè]þ²wzrÔþ6ñŪ£mØœWúú~›,ÜÕ§íqÛ¶S¯Ÿ¿?S";½Ì-¯á¿GτЮýÐ\}áó9m° Ùövíë`³Ò.>Måð›ïÚÞ.’Êg;§Iq¹¾w#g'íÛË'ݘHÛ›³Û~7¹¶K–ÉÎñÁQ¬ì#QéïòwÃñ m¦Çì¼ú¥K›iFýâ¤Íü˜hëŽ+¸ ëqê¿ü"냼™¾Ë›i¿Ì]¿%δý"›ú9q¦mm´?g.Xê€Lz>q¦~égÚŸÑ qf83Hœéh·~‰3ê—’8Ó£ìu|égÚŸQ¿hâ}8ÓÁvô8©\êÄ™ú¥KœiFýÂ~æQÿ¤ÿt°•ò.me+ì !m°ý[Õ£öâ㣻c¸4%#§îlÏr]íÎüK—,‰µ}wá XÆÓõRp»By·Xì”çð·¶ø[øKæomO~ÀßümÀßúŠ¿Í` ¾]*öÖÀž¸Û€» ¸s·ò6ànî6àn—»eÃFÛÇ/“k{ò?K·¯¢„"ꀈ¢_2µ=ù ˆ({pØ«˜¶eÚëäfî¸p•ìÔm‹XáÚÁ·ÃµÃï¶;;yÉøàiãèä}5ú«&ÍÁñncïàø½-Q³¹`o;g³ôÏÜqáó{ûUwÚ[?ÕÜi“]jÔüôô5j5j~Á5jTû8øµ].z25¦½Š2ôo¯BÿŒÑwúïýPùèçQºb¶t\ȦWT©KØyñb­ä‹Nî }|¿èdN¿ÐnŽ\ÆðMGxÖãLîCQ=°ÿäèÍÉ[2ƒWÏßø~6×%Ö0ãæØ|¡m'âë÷Ä{2ûK/Þäk{"ïq´e'¢º¡k·=‘öIÿu7(¿ý‰¼',;¿&êéÁÙþQã¬}¶ÜO2ôCtÈÒm ~’¤¿å%¥ýþø ­í›4¤Á)ÄAüyþ‰‰öDü¹³š‹ƒøó þÜÕøóåíù2ˆ?—g8ˆ?âσøsGÞ—Ëfuhíu¤zbνëoémÒ æ<ˆ9bÎØ=ØÛ;Û˜99&iyÜ>®•î»p¤û®qxxòM»ó<|s/±±Ð+›s¹;¤Œ¸KgÄ)ß»Ò.žî7:É`J/¿xGsÐþœ¾9Øí áÏ^}ñý—cj+Óö浯wC5îd&í+ÇÝЛî_tÙëKèþîþ4|úÛý_÷Gû¸?úÀBî7.0p ܽÄ×îû£¯Ü¿ ÃFÜÙNIk—Ñ„Û;Ý©Ÿí®œ´ŸO-7·¹ÇîM<¯úÇ˺è¼ÝjÛÕb^î¼mÌŸ6¾>o×ÛW¯ w]üVµíCé—*FÇ'ggõ÷¸Q³¾\}ÿà°ƒÓï¹{.Þ£Üv6ùñùÑ*q˜wL-{Kcã ˜ö þÒ þR/É¿½Ó“£ö‘/¾x¿Rû2zPMꧧ×ÍjRƒêK}W}É«¶Ý”ëì¤}ûæ¤ Sù¨u¤ºâM긚ÒGñ$Ý??}y~Hœ¹]—³LêxÛñSþ¥K®à86pÃþ¤}} ³Ð:˜Ó ­Çœåío]oç µ?žÏ@k*+Ûï©p¹Cgm*mýÀðªÞ%÷ûcßžÿ~óü·àõg;þ»¤ÊlÀ^³;a+p`¬À÷#ÊÀ +X+ðrYmÛ —Ñ +¼¬‰l^µíŠ3°0µøÑfØ6RÌÀ80fàÀ üp“É\B3°ƒ9 ÌÀ3‡Õe1;™IÏ›‚NNv_î´/ý/£x)^´­mªRô†ØQõ€>éÚ×ß6Fj"ƒ +¦òwñ6º2ÃËÆÓ~¶z!íí>$]O:Ò¾<Ü©¿«èäÍNýàì»ÑÜÆoϾ;lß n¯¾øDpÌõ²UG“êšš"öI]N§igvi¥lï70ºüÄ[®˜9sY^÷7l»NŠmº9srÌÍÛÛ·-Š÷]8â}³ßÁ±ÿCÛ~¸ Ι™eñ¶‹w¨wÆ.6ê;(}¹{.þ´kÛšìùéÞN½ÑÙÜò7 ¬à?c.öåíÁÕi»áË[FùmÓâÎ÷Gç„íÒë/|{ök5éŽ\ë™;.|^,’ºYÍ裨Z³¬FÔ: s¤j³ÂjVô÷Ÿî÷‹PŒÚCŸhЗ¸_4ˆ¶•s·[Yå»p²£ÓYÎ}ûæä¸ñ³lÞ8ОÚÓÇÓžÊÓGSž6,)÷¯öt9 …{]±¸˜dëÞߧ~kæÑÇ^õ¶…âë÷„é3ûK{Ws}ýž+³Q]˜Hû;Ò>å¿îá·?‘÷TÊNÄïÂDvNÎö4`è'9z)ó;õ„÷¼=<8[Û9xŸ:¡zéqÚ¾.Ðã"´ý‰ DhEh§Ì¹×¥ç™Ö zy$hÿ¸ªM%{X3úè|±¶äã£ÿÐâ²V@º„]æ/}mßv«çô_(ÿò—vºÀÞ~]4ý\Ûéh‡Õvm‘~àïªâÙÿµú–BÚ2k!Ýå}À"{]ª-Эoߟ=×϶àáÉérØ‹ŒãªQ—º,Ê…ìAõwI‘áKSSd¸ms¤×‹Š\ˆËê‚ç´zñGƒ»ÄúµÇåLìXŸ»¼„ÔûqÄ)0³ûQNûÄbú`{zœTûÙûQÏnYÛÇ(û¤ú[¿t +8÷p¯q:pÚ ^”^Ùç³—íïq?øº©_ +s›îr¢ +ÛC8}ÿtçøí^ûÝ.zû/§çäCÔ¸^WM?ð@K?øMdj=¢s<'ù âl¼©ÃÃØš^Y’ÃÙA茣»cøVW¦î(o{îx7med{åäxÁõ˜†<Ýxupœýahå ?#Ÿ6¾;zyr8tu¡qú’äéµ!¯2Eÿ<üfè|¨™§óð;úc‰¾|E o*Ae¹òä™WÙÅ•ëCÃZW“XíW½DÅ•£!|‹Ã$…ÔRÈ0}QA(½¯¬yç1euÈ«z:ðT&‰êX¯Ea¤“$P~+Ï'HH_•ò$‰ 4†_M´2aê(0^À´ O%¡Ånó3x~Ry¸34¬L5Rô}8¬8Šh^ÃÚ«jÅ•á¨jCÖªJÃÒr•JLå_úaT6Õ ‰<ú¢’jÅô%¨F^ì)‚ÄUc¼/Žâ¤2Co «J›Ê°_MBÓ3•_õ½ $ô#L#=$I ”ïÓbÓË|­|a)Ñ6‡ç@ÓóhÍðp•0YÒ¶¨¨N‹C»ÆšƒÆRD„ÐÌ1´¡}¥{4ˆCÁòpè©dOŽ®Œ#<%ò¶0IÂà ¦ðŸÓ"á˜[iR]ð¿$> endobj xref 0 31 0000000000 65535 f +0000000016 00000 n +0000000144 00000 n +0000017538 00000 n +0000000000 00000 f +0000020735 00000 n +0000614405 00000 n +0000017589 00000 n +0000017986 00000 n +0000023717 00000 n +0000021034 00000 n +0000020921 00000 n +0000019855 00000 n +0000020174 00000 n +0000020222 00000 n +0000020805 00000 n +0000020836 00000 n +0000021069 00000 n +0000023790 00000 n +0000024142 00000 n +0000025591 00000 n +0000029802 00000 n +0000089701 00000 n +0000155289 00000 n +0000220877 00000 n +0000286465 00000 n +0000352053 00000 n +0000417641 00000 n +0000483229 00000 n +0000548817 00000 n +0000614428 00000 n +trailer <<9EAE669C663042FC9291A478915E56EB>]>> startxref 614628 %%EOF \ No newline at end of file diff --git a/presto-docs/src/main/resources/logo/web/main/blue/FB_Presto_Logo_DarkBlueBG.ai b/presto-docs/src/main/resources/logo/web/main/blue/FB_Presto_Logo_DarkBlueBG.ai new file mode 100644 index 00000000..ade2693a --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/main/blue/FB_Presto_Logo_DarkBlueBG.ai @@ -0,0 +1,2411 @@ +%PDF-1.5 %âãÏÓ +1 0 obj <>/OCGs[5 0 R]>>/Pages 3 0 R/Type/Catalog>> endobj 2 0 obj <>stream + + + + + application/pdf + + + FB_Presto_Logo_DarkBlueBG + + + 2013-10-23T20:37:30-07:00 + 2013-10-23T20:37:30-07:00 + 2013-10-23T20:37:30-07:00 + Adobe Illustrator CC (Macintosh) + + + + 256 + 160 + JPEG + /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgAoAEAAwER AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A4Hm3cF2KuxV2KuxV2Kux V2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV 2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2 KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2K uxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2Ku xV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2Kux V2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV 2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxVfFDNM/CJGkfrxQFj9wxVa QQaHrirZikCCQoRGdg9DQn54osXTkR5HCIpd22VVFSfoGEC0ukikicpIhRx1VgQRXfocSCOarcCu xV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2Kp75M85635P1t da0Vo0vkjeIGVBIvGQUb4TkZwEhRTGVKmmeS/Nmv6Rq3mOwsvrGm6Zyl1O5EsKensZCeDurttv8A CpxMwDSiJO6GufNOp3Plm08uSCP9H2UpnhIUiTkxkPxNWlP3rdsuOQmPD0cOGjhHMcwvikK8un6k TfaN5g8matZXF9BGlyKzQIWEikL8O/Bv45McWKQPVGn1eLUxPAbAclnrfnLWLu7hjhFyVRplBKIA qiMU5Fj+z45YIT1EyRVt9jGKSGaJ4pXif7cbFWp4qaHMUijTaCswK7FXYq7FXYq7FXYq7FXYq7FX Yq7FXYq7FXYq7FXYq7FXYq7FXYq2ASQAKk7ADFVe80+/sXRL22ltXkX1I1mRoyyEleShgKiqkV9s ANqh8KuxV2KuxVH2ev67Y2Nzp9lqV1a2F6KXlpDNJHDMCONJY1YK+23xDAYgptlOq/lxp9j+Wem+ c016K4u7+URPoixqJIgWkXkXErMf7r/fY65AZLlVMjHa0n006n5u1+x0/VNTkJlLRpdTky+mApfY Fl68fHMmNzkAS67LwaXFKcIDboNrXaxDf+UPMN7punai0hiEatdRqE5h41k+zV+henXJcUsUiIll pMw1GITMau9vjScaL5EsdV8tPrFxeut3KssgIK+mpRm/vKip+zU7jMzDoozx8ZO+7OeYxlVMFzWO S7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq9i03/AJxm816jpOh6vb6nZJpurWiX1zcTFoxaxyRJKobr zYhyNtttyMxzqACQ2jEVTWf+cadXTRLnVfLOv2PmZbRS01tabSEKKkR8XlVmpvxJBPap2xGoF0RS nF3PPfy98k3XnXzTbeXrW5S0muUldZ5QWUelGZDULvvxy2c+EWwjGzTP4f8AnF/zyLXVb2/ubWyt NO+s+gWLPJcrb8uLogFESTjVeTA79Mq/MRZ+EWMflt+T3mLzzFdX0E0Gm6JYki71S7JEYYDkyoB9 oqpq24AHU5PJlEfexjAlk+pf8436jLpFzqPlLzJpvmk2YJuLWzdfU2FSqcHmUv8A5JIr232yA1G+ 4pkcfduwHyF+X/mDzvr40XR0RZlRpbmeclY4YlIUs5AZvtMAABWuWzmIiywjEkvTz/zjDC8z2Nt5 40qXVk+FrAgBw42KkCR3FD/kfRlP5jyZ+F5sW/K38r5Nf8zXqSa1ZafJ5dvbdXEzGlyRK4PoE8Cf 7jw7jJ5MlDlzYwhZe0fnn+T/APjPzbaap/iKw0n0bCO1+rXbUkbjNM/Mbj4T6lPozHw5eEVTbkhZ fOOgeQNe8x+bZvLOgqt9cwyyo1yCVgEUL8DO7GvFOn3gDfMuUwBZaREk0HqH/QrU7StYRecdLfXV WraZQ8gw3INHMlPf0/oyn8z5bM/C83kPmryrrnlbXLjRdatzb31uRUVDI6MKq6MNmVh0P0Heoy+M hIWGsikoySHYq7FXo/5vad+VcV5pMX5cSm6WVZRfKrXEh58kEQAmFd/i+zlOIy34mc66LPyusvJg l1KLzOsEd9HxEMV+RGqoK86Byo516138O+Z+m4DfE8921LUjh8Hirrw87Yrq8breaoNDNw/l9J3V HT1DDw5fDyPTp05b0yuRIsRvhdpp8h4IjJXiEfFl3mu28jr5TD6f9W+sAJ9TeIr6zGorzp8R+GvL l0+ebPVRweF6avp3oxmfFu83zTuW7FXYq7FXYq7FXYq7FXYq7FXYq7FX0L+eF1cx/kZ+XNvHK6QT 2dn68SsQr8LGMryA2PE7iuYuEeuTdk+kMd/5xX1W6tfzMNjHIwttRs5kni3KsYgJUY+68TQ+5HfJ akelGI7pr+WdhBYf85Q6hZ24CwQ3mqiJAKBVKSkKPkDTBkN4vksfrYf+fXmLXr78x9d0671C4m0+ zuilrZvIxhjVVFOMdeI+dMnhiBEFjkJt7AnkLzLrX/ON/l7y/wCVljW4vzFdagJJRGHhkaSdqt3/ AHhj28PllHGBkJLbwkxoIP8AI/8AJj8yPJXnqLVNSFvFpUkE0F4sU4csGXlGOIG9JFU4c2WMo0EY 4EFL/wAt/Nnlnyl+fPnWy1OZNPtNSuriG2uJCEhSVbgyBHY7IGqaE7DphyRMoCliQJFIvNn/ADi1 5yt2nv8Ay3e2+vWDlpIF9T07pkJqPtfunNO4k38MlHUjrsxOI9HlGi2l3Z+b9PtbyJ4LqC/hjnhl Uq6OsyhlZTuCD1y8nZrHN6t/zlx/5MjTf+2PB/1FXOUaX6fi2Zuacf8AOPIXR/yu8/earYAanbwT JBLSpX6ratMlPAF5N/lgz7yATj2BLwGLUL6K/TUI55FvklE63PI+oJQ3LnyO/LlvXMmml7z/AM5R LBqOieR/MpQJe6hZuJ2AoSjRwzIv+xaR6fPMbTbEhuy9C+fsymlWjtLuWJ5ooZJIov7yRVJVe/xE CgyQgSLARYR1xq1lLoFtpiafHHdwSGSTURx9SRSXPBvhDU+MftdstllicYjw7jqxETxXbLPye816 f5D892us+YbCc2hgliRhH+8iMgAEyK/GtBVTQ9CflmLnxSMa5NuOYBtlXnzTtX/Ovztda15D0d30 +wtYbS5urhobb1ZVZ2DnmwqSrBQKk8VFadMqgRjjUizl6jso6L580Tyv5Zl8t61Yy2+sab61tdac Y9pJOTVq32aNX4iform1xamAhTyWu7Hz5NSZg+kkb9346PG8wnqHYq7FXYq7FXYq7FXYq7FXYq7F XYq7FX1N+YH5eeZvOf5KeQ4/L8KXVzp+n2U0ls0ixu6PZRr8BfihI8CwzChkEZm3IlEmIpLPyH/K rzD5K1nUPOXnONNG0/T7OVIxNJGzHnQvI3ps4Cqikb7knbJZsgkKCMcCNyxX8ldb/Tv/ADkO+tcS i6jNqV0iHqqyxyMq/Qppk8wrHTGBuTDfzt/8mv5m/wCYxv8AiK5Zh+kMZ8y9f8qLe/mJ/wA4/wBv 5e8u3v1fzT5clU/VkmMDSBGfgCwIoskMpAJ+HmN6Zjy9GSzyLYPVGhzYFYflD/zkDeXPofV7+3Ab i8098qRrSm9fVJYb/sg5acuNhwSYt5Z/LXzn5w1vV9O06NbjVdLV5L1biXizusoiKB32LliT8RA2 O+TlkEQGIiS9R/Jf8sPzp8t+ebCa4tptK0RJCdUV7iJoJIuJBX0o5G5sT9k02O+U5skDHzbIQkCw /wDN7WdFuPz1vNQspENlBe2a3Fwm6F4FiWZtv5WQg/LLMQPAxmfU9I/5yM/K3zx5u85aZqvlzTTq NkdOjtWkSWFAsiTyyb+o6bFZloemU4MkYiizyQJOyRf846arYNb+a/y11qYWc2txSw2wLDeUxPb3 EakGhfjxK060OSzjlIdEYzzDGLb/AJxp/NOTX102bT0is/V4yar60RgEdd5AA3qHbovGuT/MRq2P hFO/+coPM2lT6ronlHSpVlg8t27JcOhBAlkCIsRI25RxxCtP5qdRtHTxNEnqnKejx7RdEvtZvDaW QUzBDJR24jipAO/05nYcMshqPNonMRFlWttb1TS7O+0mIoIbktHcgryNQOJoclHNKAMByPNBgCQV ObQtQh0W31lwv1K5kMURDfFyBYbr/wA8zgOCQgJ/wlRMXXVFXWo6z5o1CztZTG1wB6NuAAgod9z9 GTlknnkAeaBEQBLPfy0/N/WPypfVNEuNLi1OGeRZWiExhaObgByEgSUMpWlRx+nMPU6U8VHmG7Fl 2sPPfNnmS+8zeY9Q16+VUutQmMzxx14L2VVrU0VQBhjGhSCbNpTkkOxV2KuxV2KuxV2KuxV2KuxV 2KuxV2Kvov8AN/V9V0n8n/yxvdLvJ7G8SytQlxbSNFIAbCOoDIQaHMTEAZytvmfSHiOt+evOeu2y 2us63e39sp5CC4nkePkOh4E8ajMkQA5BpMiUt0zVdU0q8S90u8nsLyMEJc20jwyqGFGAdCrCoNDv hIB5oBU76+vb+7lvL64lu7uduU1xO7SSO3izsSxPzwgUqppmq6ppV2t5pl5PY3abJcW0jxSAHrR0 KnAQDzUFPbv80PzHvITBceZtTeFvtJ9amAI8DxYVHzyIxx7mXEe9ItP1fVtNu/runXs9leCtLm3l eKXfc/GhDZIgFiCnl7+Z35i3tq9rdeZdSlt5Bxkia6l4sp2Iajbg+ByIxxHRlxHvYxk2LJLL8yvz BsbCOws/MepW9nEvpwwx3UqqiDYKlG+EDtTIHHE9GXEUge5uHuGuXldrlnMjTsxLlyeRcsd+Vd65 NiyJ/wAz/wAxntDZv5m1NrcrwKG7m3XwryrT6ch4ce5lxHvYySSSSak7knJsVS3ubm2k9S3leGSl OcbFTQ9qimSjIjkaQQCpszMxZiSxNSTuSTkUqjXV01uts0ztboeSQliUB33C1oOpyXEaq9kUFsUs sUiyRO0cimqupKsD7EYASNwkh00000jSzO0kjfadyWY9tycSSTZUBZgV2KuxV2KuxV2KuxV2KuxV 2KuxV2KuxV2KqklxcSIsckrvGmyIzEhabbA9MVU8VdirsVdirsVdirsVdirsVdirsVdirsVdirsV dirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVd irsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdi rsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdir sVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirs VdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsV dirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVZL/AMqz/Mj/AKlTWP8AuH3X/VPIeJHv DLhPc7/lWf5kf9SprH/cPuv+qePiR7wvCe53/Ks/zI/6lTWP+4fdf9U8fEj3heE9zv8AlWf5kf8A Uqax/wBw+6/6p4+JHvC8J7nf8qz/ADI/6lTWP+4fdf8AVPHxI94XhPc7/lWf5kf9SprH/cPuv+qe PiR7wvCe53/Ks/zI/wCpU1j/ALh91/1Tx8SPeF4T3O/5Vn+ZH/Uqax/3D7r/AKp4+JHvC8J7nf8A Ks/zI/6lTWP+4fdf9U8fEj3heE9zv+VZ/mR/1Kmsf9w+6/6p4+JHvC8J7nf8qz/Mj/qVNY/7h91/ 1Tx8SPeF4T3O/wCVZ/mR/wBSprH/AHD7r/qnj4ke8Lwnud/yrP8AMj/qVNY/7h91/wBU8fEj3heE 9zv+VZ/mR/1Kmsf9w+6/6p4+JHvC8J7nf8qz/Mj/AKlTWP8AuH3X/VPHxI94XhPc7/lWf5kf9Spr H/cPuv8Aqnj4ke8Lwnud/wAqz/Mj/qVNY/7h91/1Tx8SPeF4T3O/5Vn+ZH/Uqax/3D7r/qnj4ke8 Lwnud/yrP8yP+pU1j/uH3X/VPHxI94XhPc7/AJVn+ZH/AFKmsf8AcPuv+qePiR7wvCe53/Ks/wAy P+pU1j/uH3X/AFTx8SPeF4T3O/5Vn+ZH/Uqax/3D7r/qnj4ke8Lwnud/yrP8yP8AqVNY/wC4fdf9 U8fEj3heE9zv+VZ/mR/1Kmsf9w+6/wCqePiR7wvCe53/ACrP8yP+pU1j/uH3X/VPHxI94XhPc7/l Wf5kf9SprH/cPuv+qePiR7wvCe53/Ks/zI/6lTWP+4fdf9U8fEj3heE9zv8AlWf5kf8AUqax/wBw +6/6p4+JHvC8J7nf8qz/ADI/6lTWP+4fdf8AVPHxI94XhPc7/lWf5kf9SprH/cPuv+qePiR7wvCe 53/Ks/zI/wCpU1j/ALh91/1Tx8SPeF4T3O/5Vn+ZH/Uqax/3D7r/AKp4+JHvC8J7n//Z + + + + uuid:6ed7a846-b6fa-4b4e-8d9b-436989612252 + xmp.did:3d6f9230-e529-4e84-b801-8ef151ad4d97 + uuid:5D20892493BFDB11914A8590D31508C8 + proof:pdf + + xmp.iid:a478fe57-ac17-43dd-a456-3029de78c422 + xmp.did:a478fe57-ac17-43dd-a456-3029de78c422 + uuid:5D20892493BFDB11914A8590D31508C8 + proof:pdf + + + + + saved + xmp.iid:a478fe57-ac17-43dd-a456-3029de78c422 + 2013-10-23T20:23:29-07:00 + Adobe Illustrator CC (Macintosh) + / + + + saved + xmp.iid:3d6f9230-e529-4e84-b801-8ef151ad4d97 + 2013-10-23T20:37:29-07:00 + Adobe Illustrator CC (Macintosh) + / + + + + Document + Print + False + False + 1 + + 6.955882 + 4.317257 + Inches + + + + Cyan + Magenta + Yellow + Black + + + + + + Default Swatch Group + 0 + + + + R=56 G=227 B=255 + PROCESS + 100.000000 + RGB + 30 + 220 + 255 + + + R=0 G=0 B=0 + PROCESS + 100.000000 + RGB + 0 + 0 + 0 + + + R=88 G=144 B=255 + PROCESS + 100.000000 + RGB + 88 + 144 + 255 + + + R=255 G=255 B=255 + PROCESS + 100.000000 + RGB + 255 + 255 + 255 + + + R=15 G=38 B=72 + PROCESS + 100.000000 + RGB + 15 + 38 + 72 + + + + + + + Adobe PDF library 10.01 + + + + + + + + + + + + + + + + + + + + + + + + + endstream endobj 3 0 obj <> endobj 7 0 obj <>/Resources<>/ExtGState<>/Properties<>>>/Thumb 12 0 R/TrimBox[0.0 0.0 500.824 310.843]/Type/Page>> endobj 8 0 obj <>stream +H‰ÔWËŽ7 ¼ÏWèF+’z^½ r2‚ ‡|À ƒ×€ãÿR$¥žžÙÙ±½ Ì6Õ”D‘U%öÃÏááíc +o~x ‡‡Ç_S8} )¦2ðK?Ü9„§÷‡‡ŸðöχR()ÅÎ9á–ð÷ï‡?ðj ,‡ãÎã·ðþ@Aÿl½xе08ÖB#˜:¶==Ù6OŠ£µp¤X:…I:–„‡{‘eœTca^6õHøÇqpYqL#«ë|I‘k ˜ØpZn1I # +÷eœ%J+ËäÈ”kÈN`‰TNFì ±ô˜±ÒfÕ±™ûÀ °ËÐYÇ‚U:fátœúÝok¦çB±âP°)&Äu¶SÌX­êOQ2ª’alO„cÆ?¤Ÿ[_Öé0·˜6gT‰õ(©o–&ŠªúºÍ6§Øˆ×±.Ž|BõÙ´h µÙUÍú*è1ÇÌU×ìˆó‡`Fž±4vf-jŽD„U·‘X[µD î#ß)±H6[bLÎ6džܥ8²¥‘RƒàPCë’R›)›Aa¨7«ëÕ©*Þ2&÷Á;˜¶Ø:öÐRø#á˜ùlv@ƒ%æÈ9x ¬#ŸÓ,QKZ&(ØCCú›mnpQäTl†ø±ˆž³¢2M]"´ÚYê²°f`ÊöÙnÈ Ÿ³ÀÉ……‹z+ f³¼hð#¶Z —U¡¢òyIV¬Ukê]ÓÙªQª)b2"ªhª¸ù 7æ÷ô_ƒŽª‡„¹ƒióñ-ÜÑŸÝïµ¥U»^ÑÿoÁ«¼¬Å€¼,UM²ÅTvu¶»1}ÝÉMºtT=¶UÝ=m>>êŽúÌsÝ›q÷®]4ØÐÏ•‹ÐRÖÃú‡NF6½Gk+r½_Ùî®i¸«ûø¯^5Ô¡½»¯âÎÉ\$>j~¾œÿÞ +;'(i©´XòEÐ’½ïó΀¼30 =¥âÓÂÖ¯ÜÓ}Zê¾Üü׺ ]czÌõ§•6/¶ùþè[žcW ëv'Ê ôC*q¥£\C;A¹Dqõ/¸„Ûd-š[ź';O3í™ÎnL_wò_¾ŽºÇ\¼M¹[>>ªŽ-øN>r@¸Óð ƒuÈ™H¹"n¿ n¿K\ÙWî·ï‰Û¿Ž¸¼#zu¹Í]¾à.wû¿Ç]¾:4'7m9A‚>pûßá.ûäí;òò¸Å^/Ó·íékÍÈ}þâ#í´ö¤gmYß1X.,‹š÷ L÷\>ƒÁ4É{MaÒOµgÔÅGK*FÝœj»ÀTC¼ýC;kíš[yJ²…'¬6î†ú–åä¿Ößê +ê±–qïÉ¢åç¯| }nsí›…@ã;Jb£oíµ|/ÊSwÊóü!šµ¥lÌúsOy.[†úZåiŸVž¶SžvOy œ¢_6è•ñ©Úè¶òä åÉ.*|­<ü*å‘›ÊÓ?­<ývÃè[×—(Ï9õÏz‡ü\~ê+å§ß“(g×OUŽ=_"?Ïþ6ús¿¶¢iIª2Ü%rûj"·×ù…Â4ˆà"92ÿ¦û1L¾*|šRË{áT./ßGâ7ÌXèT¿=fäµm'Ú¶TÐÎÐénÖëWgýU÷ÖK3Ížëÿ +î?¾} ø÷ίä endstream endobj 12 0 obj <>stream +8;Z\t0b2)D$q997bBTh0R<"$U5!TYVY>\q$$*$=GYW;:d,[jB7B$%WBkRSuOKJVI!>(dlQCJXW!j%SSY@=QUG<< +]=crITA-P!j9U1cjm@AcgE%2Efi+2Tk8U,dNHMtU[F]j+8%d^]!+7`8`r~> endstream endobj 13 0 obj [/Indexed/DeviceRGB 255 14 0 R] endobj 14 0 obj <>stream +8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0 +b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup` +E1r!/,*0[*9.aFIR2&b-C#soRZ7Dl%MLY\.?d>Mn +6%Q2oYfNRF$$+ON<+]RUJmC0InDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j$XKrcYp0n+Xl_nU*O( +l[$6Nn+Z_Nq0]s7hs]`XX1nZ8&94a\~> endstream endobj 5 0 obj <> endobj 15 0 obj [/View/Design] endobj 16 0 obj <>>> endobj 11 0 obj <> endobj 10 0 obj [/ICCBased 17 0 R] endobj 17 0 obj <>stream +H‰œ–yTSwÇoÉž•°Ãc [€°5la‘QIBHØADED„ª•2ÖmtFOE.®c­Ö}êÒõ0êè8´׎8GNg¦Óïï÷9÷wïïÝß½÷ó '¥ªµÕ0 Ö ÏJŒÅb¤  + 2y­.-;!à’ÆK°ZÜ ü‹ž^i½"LÊÀ0ðÿ‰-×é @8(”µrœ;q®ª7èLöœy¥•&†Qëñq¶4±jž½ç|æ9ÚÄ +V³)gB£0ñiœWו8#©8wÕ©•õ8_Å٥ʨQãüÜ«QÊj@é&»A)/ÇÙgº>'K‚óÈtÕ;\ú” Ó¥$ÕºF½ZUnÀÜå˜(4TŒ%)ë«”ƒ0C&¯”阤Z£“i˜¿óœ8¦Úbx‘ƒE¡ÁÁBÑ;…ú¯›¿P¦ÞÎӓ̹žAü om?çW= +€x¯Íú·¶Ò-Œ¯Àòæ[›Ëû0ñ¾¾øÎ}ø¦y)7ta¾¾õõõ>j¥ÜÇTÐ7úŸ¿@ï¼ÏÇtÜ›ò`qÊ2™±Ê€™ê&¯®ª6ê±ZL®Ä„?â_øóyxg)Ë”z¥ÈçL­UáíÖ*ÔuµSkÿSeØO4?׸¸c¯¯Ø°.òò· åÒR´ ßÞô-•’2ð5ßáÞüÜÏ ú÷Sá>Ó£V­š‹“då`r£¾n~ÏôY &à+`œ;ÂA4ˆÉ 䀰ÈA9Ð=¨- t°lÃ`;»Á~pŒƒÁ ðGp| ®[`Lƒ‡`<¯ "A ˆ YA+äùCb(Š‡R¡,¨*T2B-Ð +¨ꇆ¡Ðnè÷ÐQètº}MA ï —0Óal»Á¾°ŽSàx ¬‚kà&¸^Á£ð>ø0|>_ƒ'á‡ð,ÂG!"F$H:Rˆ”!z¤éF‘Qd?r 9‹\A&‘GÈ ”ˆrQ ¢áhš‹ÊÑ´íE‡Ñ]èaô4zBgÐ×Á–àE#H ‹*B=¡‹0HØIøˆp†p0MxJ$ùD1„˜D, V›‰½Ä­ÄÄãÄKÄ»ÄY‰dEò"EÒI2’ÔEÚBÚGúŒt™4MzN¦‘Èþär!YKî ’÷?%_&ß#¿¢°(®”0J:EAi¤ôQÆ(Ç()Ó”WT6U@ æP+¨íÔ!ê~êêmêæD ¥eÒÔ´å´!ÚïhŸÓ¦h/èº']B/¢éëèÒÓ¿¢?a0nŒhF!ÃÀXÇØÍ8ÅøšñÜŒkæc&5S˜µ™˜6»lö˜Iaº2c˜K™MÌAæ!æEæ#…åÆ’°d¬VÖë(ëk–Íe‹Øél »—½‡}Ž}ŸCâ¸qâ9 +N'çÎ)Î].ÂuæJ¸rî +î÷ wšGä xR^¯‡÷[ÞoÆœchžgÞ`>bþ‰ù$á»ñ¥ü*~ÿ ÿ:ÿ¥…EŒ…ÒbÅ~‹ËÏ,m,£-•–Ý–,¯Y¾´Â¬â­*­6X[ݱF­=­3­ë­·YŸ±~dó ·‘ÛtÛ´¹i ÛzÚfÙ6Û~`{ÁvÖÎÞ.ÑNg·Åî”Ý#{¾}´}…ý€ý§ö¸‘j‡‡ÏþŠ™c1X6„Æfm“Ž;'_9 œr:œ8Ýq¦:‹ËœœO:ϸ8¸¤¹´¸ìu¹éJq»–»nv=ëúÌMà–ï¶ÊmÜí¾ÀR 4 ö +n»3Ü£ÜkÜGݯz=Ä•[=¾ô„=ƒ<Ë=GTB(É/ÙSòƒ,]6*›-•–¾W:#—È7Ë*¢ŠÊe¿ò^YDYÙ}U„j£êAyTù`ù#µD=¬þ¶"©b{ųÊôÊ+¬Ê¯: !kJ4Gµm¥ötµ}uCõ%—®K7YV³©fFŸ¢ßY Õ.©=bàá?SŒîÆ•Æ©ºÈº‘ºçõyõ‡Ø Ú† žkï5%4ý¦m–7Ÿlqlio™Z³lG+ÔZÚz²Í¹­³mzyâò]íÔöÊö?uøuôw|¿"űN»ÎåwW&®ÜÛe֥ﺱ*|ÕöÕèjõê‰5k¶¬yÝ­èþ¢Ç¯g°ç‡^yïkEk‡Öþ¸®lÝD_p߶õÄõÚõ×7DmØÕÏîoê¿»1mãál {àûMśΠnßLÝlÜ<9”úO¤[þ˜¸™$™™üšhšÕ›B›¯œœ‰œ÷dÒž@ž®ŸŸ‹Ÿú i Ø¡G¡¶¢&¢–££v£æ¤V¤Ç¥8¥©¦¦‹¦ý§n§à¨R¨Ä©7©©ªª««u«é¬\¬Ð­D­¸®-®¡¯¯‹°°u°ê±`±Ö²K²Â³8³®´%´œµµŠ¶¶y¶ð·h·à¸Y¸Ñ¹J¹Âº;ºµ».»§¼!¼›½½¾ +¾„¾ÿ¿z¿õÀpÀìÁgÁãÂ_ÂÛÃXÃÔÄQÄÎÅKÅÈÆFÆÃÇAÇ¿È=ȼÉ:ɹÊ8Ê·Ë6˶Ì5̵Í5͵Î6ζÏ7ϸÐ9кÑ<ѾÒ?ÒÁÓDÓÆÔIÔËÕNÕÑÖUÖØ×\×àØdØèÙlÙñÚvÚûÛ€ÜÜŠÝÝ–ÞÞ¢ß)߯à6à½áDáÌâSâÛãcãëäsäüå„æ æ–çç©è2è¼éFéÐê[êåëpëûì†ííœî(î´ï@ïÌðXðåñrñÿòŒóó§ô4ôÂõPõÞömöû÷Šøø¨ù8ùÇúWúçûwüü˜ý)ýºþKþÜÿmÿÿ ÷„óû endstream endobj 9 0 obj <> endobj 18 0 obj <> endobj 19 0 obj <>stream +%!PS-Adobe-3.0 %%Creator: Adobe Illustrator(R) 17.0 %%AI8_CreatorVersion: 17.0.0 %%For: (Brent Couchman) () %%Title: (FB_Presto_Logo_BlackBG.ai) %%CreationDate: 10/23/13 8:37 PM %%Canvassize: 16383 %%BoundingBox: 0 -612 501 -301 %%HiResBoundingBox: 0 -612 500.823529411764 -301.157476056923 %%DocumentProcessColors: Cyan Magenta Yellow Black %AI5_FileFormat 13.0 %AI12_BuildNumber: 256 %AI3_ColorUsage: Color %AI7_ImageSettings: 0 %%RGBProcessColor: 0 0 0 (R=0 G=0 B=0) %%+ 0.059415001422167 0.150419995188713 0.282377988100052 (R=15 G=38 B=72) %%+ 1 1 1 (R=255 G=255 B=255) %%+ 0.117647059261799 0.862745106220245 1 (R=56 G=227 B=255) %%+ 0.345098048448563 0.564705908298492 1 (R=88 G=144 B=255) %%+ 0 0 0 ([Registration]) %AI3_Cropmarks: 0 -612 500.823529411764 -301.157476056923 %AI3_TemplateBox: 396.5 -306.5 396.5 -306.5 %AI3_TileBox: -127.588235294118 -744.578738028462 606.411764705881 -168.578738028462 %AI3_DocumentPreview: None %AI5_ArtSize: 14400 14400 %AI5_RulerUnits: 0 %AI9_ColorModel: 1 %AI5_ArtFlags: 0 0 0 1 0 0 1 0 0 %AI5_TargetResolution: 800 %AI5_NumLayers: 1 %AI9_OpenToView: -188.5 -218 2 1624 1073 18 0 0 46 64 0 0 0 1 1 0 1 1 0 1 %AI5_OpenViewLayers: 7 %%PageOrigin:90 -702 %AI7_GridSettings: 72 8 72 8 1 0 0.800000011920929 0.800000011920929 0.800000011920929 0.899999976158142 0.899999976158142 0.899999976158142 %AI9_Flatten: 1 %AI12_CMSettings: 00.MS %%EndComments endstream endobj 20 0 obj <>stream +%%BoundingBox: 0 -612 501 -301 %%HiResBoundingBox: 0 -612 500.823529411764 -301.157476056923 %AI7_Thumbnail: 128 80 8 %%BeginData: 22744 Hex Bytes %0000330000660000990000CC0033000033330033660033990033CC0033FF %0066000066330066660066990066CC0066FF009900009933009966009999 %0099CC0099FF00CC0000CC3300CC6600CC9900CCCC00CCFF00FF3300FF66 %00FF9900FFCC3300003300333300663300993300CC3300FF333300333333 %3333663333993333CC3333FF3366003366333366663366993366CC3366FF %3399003399333399663399993399CC3399FF33CC0033CC3333CC6633CC99 %33CCCC33CCFF33FF0033FF3333FF6633FF9933FFCC33FFFF660000660033 %6600666600996600CC6600FF6633006633336633666633996633CC6633FF %6666006666336666666666996666CC6666FF669900669933669966669999 %6699CC6699FF66CC0066CC3366CC6666CC9966CCCC66CCFF66FF0066FF33 %66FF6666FF9966FFCC66FFFF9900009900339900669900999900CC9900FF %9933009933339933669933999933CC9933FF996600996633996666996699 %9966CC9966FF9999009999339999669999999999CC9999FF99CC0099CC33 %99CC6699CC9999CCCC99CCFF99FF0099FF3399FF6699FF9999FFCC99FFFF %CC0000CC0033CC0066CC0099CC00CCCC00FFCC3300CC3333CC3366CC3399 %CC33CCCC33FFCC6600CC6633CC6666CC6699CC66CCCC66FFCC9900CC9933 %CC9966CC9999CC99CCCC99FFCCCC00CCCC33CCCC66CCCC99CCCCCCCCCCFF %CCFF00CCFF33CCFF66CCFF99CCFFCCCCFFFFFF0033FF0066FF0099FF00CC %FF3300FF3333FF3366FF3399FF33CCFF33FFFF6600FF6633FF6666FF6699 %FF66CCFF66FFFF9900FF9933FF9966FF9999FF99CCFF99FFFFCC00FFCC33 %FFCC66FFCC99FFCCCCFFCCFFFFFF33FFFF66FFFF99FFFFCC110000001100 %000011111111220000002200000022222222440000004400000044444444 %550000005500000055555555770000007700000077777777880000008800 %000088888888AA000000AA000000AAAAAAAABB000000BB000000BBBBBBBB %DD000000DD000000DDDDDDDDEE000000EE000000EEEEEEEE0000000000FF %00FF0000FFFFFF0000FF00FFFFFF00FFFFFF %524C45280528052805280528052805280528052805280528052805280528 %052805280528052805280528052805280528052805280528052805280528 %052805280528052805280528052805280528052805280528052805280528 %052805280528052805280528052805280528052805280528052805280528 %052805280528052805280500280005002800050028000500280005002800 %050028000500280005002800050028000500280005002800050028000500 %280005002800050028000500280005002800050028000500280005002800 %050028000500280005002800050028000500280005002800050028000500 %280005002800050028000500280005002800052800280528002805280028 %052800280528002805280028052800280528002805280028052800280528 %002805280028052800280528002805280028052800280528002805280028 %052800280528002805280028052800280528002805280028052800280528 %002805280028052800280528002805280028052800280528002805000500 %280005002800050028000500280005002800050028000500280005002800 %050028000500280005002800050028000500280005002800050028000500 %280005002800050028000500280005002800050028000500280005002800 %050028000500280005002800050028000500280005002800050028000500 %280005002828052805280528052805280528052805280528052805280528 %052805280528052805280528052805280528052805280528052805280528 %052805280528052805280528052805280528052805280528052805280528 %052805280528052805280528052805280528052805280528052805280528 %052805280528052805280528050005002800050028000500280005002800 %050028000500280005002800050028000500280005002800050028000500 %280005002800050028000500280005002800050028000500280005002800 %050028000500280005002800050028000500280005002800050028000500 %280005002800050028000500280005002800050028280528002805280028 %052800280528002805280028052800280528002805280028052800280528 %002805280028052800280528002805280028052800280528002805280028 %052800280528002805280028052800280528002805280028052800280528 %002805280028052800280528002805280028052800280528002805280000 %280005002800050028000500280005002800050028000500280005002800 %050028000500280005002800050028000500280005002800050028000500 %280005002800050028000500280005002800050028000500280005002800 %050028000500280005002800050028000500280005002800050028000500 %280005002800052805280528052805280528052805280528052805280528 %052805280528052805280528052805280528052805280528052805280528 %052805280528052805280528052805280528052805280528052805280528 %052805280528052805280528052805280528052805280528052805280528 %052805280528052805280528052805002800050028000500280005002800 %050028000500280005002800050028000500280005002800050028000500 %280005002800050028000500280005002800050028000500280005002800 %050028000500280005002800050028000500280005002800050028000500 %280005002800050028000500280005002800050028000528002805280028 %052800280528002805280028052800280528002805280028052800280528 %002805280028052800280528002805280028052800280528002805280028 %052800280528002805280028052800280528002805280028052800280528 %002805280028052800280528002805280028052800280528002805280028 %050005002800050028000500280005002800050028000500280005002800 %050028000500280005002800050028000500280005002800050028000500 %280005002800050028000500280005002800050028000500280005002800 %050028000500280005002800050028000500280005002800050028000500 %280005002800050028280528052805280528052805280528052805280528 %052805280528052805280528052805280528052805280528052805280528 %052805280528052805280528052805280528052805280528052805280528 %052805280528052805280528052805280528052805280528052805280528 %052805280528052805280528052805280500050028000500280005002800 %050028000500280005002800050028000500280005002800050028000500 %280005002800050028000500280005002800050028000500280005002800 %050028000500280005002800050028000500280005002800050028000500 %280005002800050028000500280005002800050028000500282805280028 %052800280528002805280028052800280528002805280028052800280528 %002805280028052800280528002805280028052800280528002805280028 %052800280528002805280028052800280528002805280028052800280528 %002805280028052800280528002805280028052800280528002805280028 %052800002800050028000500280005002800050028000500280005002800 %050028000500280005002800050028000500280005002800050028000500 %280005002800050028000500280005002800050028000500280005002800 %050028000500280005002800050028000500280005002800050028000500 %280005002800050028000528052805280528052805280528052805280528 %052805280528052805280528052805280528052805280528052805280528 %052805280528052805280528052805280528052805280528052805280528 %052805280528052805280528052805280528052805280528052805280528 %052805280528052805280528052805280528050028000500280005002800 %050028000500280005002800050028000500280005002800050028000500 %280005002800050028000500280005002800050028000500280005002800 %050028000500280005002800050028000500280005002800050028000500 %280005002800050028000500280005002800050028000500280005280028 %052800280528002805280028052800280528002805280028052800280528 %002805280028052800280528002805280028052800280528002805280028 %052800280528002805280028052800280528002805280028052800280528 %002805280028052800280528002805280028052800280528002805280028 %052800280500050028000500280005002800050028000500280005002800 %050028000500280005002800050028000500280005002800050028000500 %280005002800050028000500280005002800050028000500280005002800 %050028000500280005002800050028000500280005002800050028000500 %280005002800050028000500282805280528052805280528052805280528 %052805280528052805280528052805280528052805280528052805280528 %052805280528052805280528052805280528052805280528052805280528 %052805280528052805280528052805280528052805280528052805280528 %052805280528052805280528052805280528052805000500280005002800 %050028000500280005002800050028000500280005002800050028000500 %280005002800050028000500280005002800050028000500280005002800 %050028000500280005002800050028000500280005002800050028000500 %280005002800050028000500280005002800050028000500280005002828 %052800280528002805280028052800280528002805280028052800280528 %002805280028052800280528002805280028052800280528002805280028 %052800280528002805280028052800280528002805280028052800280528 %002805280028052800280528002805280028052800280528002805280028 %052800280528000028000500280005002800050028000500280005002800 %050028000500280005002800050028000500280005002800050028000500 %280005002800050028000500280005002800050028000500280005002800 %050028000500280005002800050028000500280005002800050028000500 %280005002800050028000500280005280528052805280528052805280528 %052805280528052805280528052805280528052805280528052805280528 %052805280528052805280528052805280528052805280528052805280528 %052805280528052805280528052805280528052805280528052805280528 %052805280528052805280528052805280528052805280500280005002800 %050028000500280005002800050028000500280005002800050028000500 %280005002800050028000500280005002800050028000500280005002800 %050028000500280005002800050028000500280005002800050028000500 %280005002800050028000500280005002800050028000500280005002800 %052800280528002805280028052800280528002805280028052800280528 %002805280028052800280528002805280028052800280528002805280028 %052800280528002805280028052800280528052800280028052800280528 %002805270028052800280528002805280028052800280528002805280028 %052800280528002805000500280005002800050028000500280005002800 %050028000500280005002800050028000500280005002800050028000500 %280005002800050028000500280005002800050028000500280005002E05 %050028002805050005002806050028000500280005002800050028000500 %280005002800050028000500280005002828052805280528052805280528 %052805280528052805280528052805280528052805280528052805280528 %052805280528052805280528052805280528052805280528052805280528 %052805280053FF5200280528433600280528545B05280528052805280528 %052805280528052805280528052805280528052805280528050005002800 %050028000500280005002800050028000500280005002800050028000500 %280005002800050028000500280005002800050028000500280005002800 %05002800050028000500280005055305050028052F06270005002F2F0600 %050005002800050028000500280005002800050028000500280005002800 %050028280528002805280028052800280528002805280028052800280528 %002805280028052800280528002805280028052800280528002805280028 %052800280528002805280028052800280528002805280028052805280027 %052E06280027052828280528002805280028052800280528002805280028 %052800280528002805280000280005002800050028000500280005002800 %050028000500280005002800050028000500280005002800050028000500 %280005002800050028000500280005002800050028000500280005002800 %050053FF59002800063D3C00050005305B06050005002800050028000500 %280005002800050028000500280005002800052805280528052805280528 %052805280528052805280528052805280528052805280528052805280528 %052805280528052805280528052805280528052805280528052805280528 %0528052805280528052828842828052805362F2800280554302F00270028 %052805280528052805280528052805280528052805280528052805002800 %050028000500280005002800050028000500280005002800050028000500 %280005002800050028000500280005002800050028000500280005002800 %050028000500280005002800050028000500060005002805050027000605 %28000500292F280028000500280005002800050028000500280005002800 %050028000528002805280028052800280528002805280028052800280528 %002805280028052800280528002805280028052800280528002805280028 %05280028050500280528002805280028052800280528002805280028002E %FFA8002800283C43052805285B8530280028052800280528002805280028 %052800280528002805280028050005002800050028000500280005002800 %050028000500280005002800050028000500280005002800050028000500 %280005002800050028000500282E2E002800050028000500280005002800 %0500280005000527A853050005003C36060005005B5B5400050028000500 %280005002800050028000500280005002800050028280528052805280528 %052805280528052805280528052805280528052800280528052805280528 %052805280528002805280528052800280528050652FF7D05052805280528 %002805280528052805280528052805280028052805280528002F2F28062F %06282F2F0528052805280528052805280528052805280528052805280500 %050028000500280005002800050028000500280005002800050005050500 %0500050006000500280005000505050005000500050028000500050053FF %7D0006000500050028000500050028000500280005002800050028A8A800 %0500283D430D000028305B30280005002800050028000500280005002800 %050028000500282805280028052800280528002805280028052800280528 %0059A8537EFFA9840528052EA85928A87D280053A8FFFF840528052853FF %FFFF842E05A8A8FFA9A8A853000528A8FFFF7E2E00280028052800280528 %0028052805FFA9280005064343360027005B5B5B06280528002805280028 %052800280528002805280028052800002800050028000500280005002800 %05002800050028000059FFFFFF7DA8FFA8000528FFA8FFFFA80053FFFF53 %A8FFA8000053FFA8847EFF2E057EA9FFFFA8A828002EFFFFA87EFFA82800 %050028000500280005000500052728002828050C3500280C060654280500 %280005002800050028000500280005002800050028000528052805280528 %05280528052805280528052805280528007EFF7E000600A8FF53002EFFFF %7D520528FFFF0505007EFF7D00A9FF280006052805052EFF7D05002805FF %FF52000552FF8406052805280528052805282F2F00280053FFFF53000028 %3D4336280528052805280528052805280528052805280528052805280528 %0500280005002800050028000500280005002800050028000059FF280000 %0552FF590028FF7E05000528FF7D00000027FF7D05A8FF28000005002800 %52FF7D00050052FF7E00280005A8FF05050028000500280005005B2F0500 %0553FFFF7E000006433D3D00270005002800050028000500280005002800 %050028000500280005280028052800280528002805280028052800280528 %0028007DFF5300280053FFA8002EFFA800280053FFA82E532E7DFFA8007D %FFFF7D592705000652FF7D05050553FF7D05002800A8FF2E002800280528 %002705280528052700287EA8052828280D3D2F2800280528002805280028 %052800280528002805280028052800280500050028000500280005002800 %050028000500280005000059FF2805000528FF7D0027FF8405000552FD07 %FF7D000059A8FFFFFF2E050053FF7D00280059FF7D000500057DFF280500 %0500280028282800050028282900050028A8FF5300000500050028000500 %280005002800050028000500280005002800050028280528052805280528 %0528052805280528052805280528007EFF5300280053FFA80052FFA80028 %0053FFA82E535253522E05280028287EFFFF050652FF7D05050553FF7D05 %052800A8FF5205280528052728852F270528055B3028050553FFFFFF0028 %052805280528052805280528052805280528052805280528052805280500 %050028000500280005002800050028000500280005000559FF2805000052 %FF530028FFA805000528FF7DFD06002800050000F8A8FF2E0053FF7D0006 %002EFFA800050028A8FF0006000500050028062800050028062800270028 %A8FF53050028000500280005002800050028000500280005002800050028 %000500282805280028052800280528002805280028052800280528007EFF %7E052828FFFF52002EFFA805280028A8FF2E2805282E28005328280528A8 %FF28052EFF7E05052800A9FF5905287DFF7D050028052828280527002828 %2800280528062F0528052805280028052800280528002805280028052800 %280528002805280028052800002800050028000500280005002800050028 %00050028000053FFFFFFA8FFFF7D000528FFA8050005002EFFFFA8A9FFFF %2E007EFFA8FFFFFF53050028A8FFA8FF2E0028FFFFFFAFFFA82800050005 %295B06050005005B2F050005005430280005002800050028000500280005 %002800050028000500280005002800050028000528052805280528052805 %28052805280528052805280528007EFF7D59A87E53002805287D7D052805 %2800287DA884845328052E59A884A85228052800537EA87D520505277E84 %A853280528052805282928052805282F2800280528282F05280528052805 %280528052805280528052805280528052805280528052805280528050028 %0005002800050028000500280005002800050028000059FF280000050005 %002800000028000500280000000500000028000000050000002800050005 %000000280005000500000028000500280005002800050005000500280005 %002800050028000500280005002800050028000500280005002800050028 %000500280005280028052800280528002805280028052800280528002800 %7EFF53002800280528002805280028052800280528002805280028052800 %280528002805280028052800280528002805280028052800280528002805 %280028052800280528002805280028052800280528002805280028052800 %280528002805280028052800280500050028000500280005002800050028 %000500280005000059FF2805000500280005002800050028000500280005 %002800050028000500280005002800050028000500280005002800050028 %000500280005002800050028000500280005002800050028000500280005 %002800050028000500280005002800050028000500282805280528052805 %280528052805280528052805280528052828280528052805280528052805 %280528052805280528052805280528052805280528052805280528052805 %280528052805280528052805280528052805280528052805280528052805 %280528052805280528052805280528052805280528052805280528052805 %000500280005002800050028000500280005002800050028000500280005 %002800050028000500280005002800050028000500280005002800050028 %000500280005002800050028000500280005002800050028000500280005 %002800050028000500280005002800050028000500280005002800050028 %000500280005002828052800280528002805280028052800280528002805 %280028052800280528002805280028052800280528002805280028052800 %280528002805280028052800280528002805280028052800280528002805 %280028052800280528002805280028052800280528002805280028052800 %280528002805280028052800280528000028000500280005002800050028 %000500280005002800050028000500280005002800050028000500280005 %002800050028000500280005002800050028000500280005002800050028 %000500280005002800050028000500280005002800050028000500280005 %002800050028000500280005002800050028000500280005280528052805 %280528052805280528052805280528052805280528052805280528052805 %280528052805280528052805280528052805280528052805280528052805 %280528052805280528052805280528052805280528052805280528052805 %280528052805280528052805280528052805280528052805280528052805 %280500280005002800050028000500280005002800050028000500280005 %002800050028000500280005002800050028000500280005002800050028 %000500280005002800050028000500280005002800050028000500280005 %002800050028000500280005002800050028000500280005002800050028 %000500280005002800052800280528002805280028052800280528002805 %280028052800280528002805280028052800280528002805280028052800 %280528002805280028052800280528002805280028052800280528002805 %280028052800280528002805280028052800280528002805280028052800 %280528002805280028052800280528002805000500280005002800050028 %000500280005002800050028000500280005002800050028000500280005 %002800050028000500280005002800050028000500280005002800050028 %000500280005002800050028000500280005002800050028000500280005 %002800050028000500280005002800050028000500280005002828052805 %280528052805280528052805280528052805280528052805280528052805 %280528052805280528052805280528052805280528052805280528052805 %280528052805280528052805280528052805280528052805280528052805 %280528052805280528052805280528052805280528052805280528052805 %280528050005002800050028000500280005002800050028000500280005 %002800050028000500280005002800050028000500280005002800050028 %000500280005002800050028000500280005002800050028000500280005 %002800050028000500280005002800050028000500280005002800050028 %000500280005002800050028280528002805280028052800280528002805 %280028052800280528002805280028052800280528002805280028052800 %280528002805280028052800280528002805280028052800280528002805 %280028052800280528002805280028052800280528002805280028052800 %280528002805280028052800280528002805280000280005002800050028 %000500280005002800050028000500280005002800050028000500280005 %002800050028000500280005002800050028000500280005002800050028 %000500280005002800050028000500280005002800050028000500280005 %002800050028000500280005002800050028000500280005002800052805 %280528052805280528052805280528052805280528052805280528052805 %280528052805280528052805280528052805280528052805280528052805 %280528052805280528052805280528052805280528052805280528052805 %280528052805280528052805280528052805280528052805280528052805 %280528052805002800050028000500280005002800050028000500280005 %002800050028000500280005002800050028000500280005002800050028 %000500280005002800050028000500280005002800050028000500280005 %002800050028000500280005002800050028000500280005002800050028 %000500280005002800050028000528002805280028052800280528002805 %280028052800280528002805280028052800280528002805280028052800 %280528002805280028052800280528002805280028052800280528002805 %280028052800280528002805280028052800280528002805280028052800 %280528002805280028052800280528002805280028050005002800050028 %000500280005002800050028000500280005002800050028000500280005 %002800050028000500280005002800050028000500280005002800050028 %000500280005002800050028000500280005002800050028000500280005 %002800050028000500280005002800050028000500280005002800050028 %280528052805280528052805280528052805280528052805280528052805 %280528052805280528052805280528052805280528052805280528052805 %280528052805280528052805280528052805280528052805280528052805 %280528052805280528052805280528052805280528052805280528052805 %280528052805280500050028000500280005002800050028000500280005 %002800050028000500280005002800050028000500280005002800050028 %000500280005002800050028000500280005002800050028000500280005 %002800050028000500280005002800050028000500280005002800050028 %000500280005002800050028000500282805280028052800280528002805 %280028052800280528002805280028052800280528002805280028052800 %280528002805280028052800280528002805280028052800280528002805 %280028052800280528002805280028052800280528002805280028052800 %280528002805280028052800280528002805280028052800002800050028 %000500280005002800050028000500280005002800050028000500280005 %002800050028000500280005002800050028000500280005002800050028 %000500280005002800050028000500280005002800050028000500280005 %002800050028000500280005002800050028000500280005002800050028 %000528052805280528052805280528052805280528052805280528052805 %280528052805280528052805280528052805280528052805280528052805 %280528052805280528052805280528052805280528052805280528052805 %280528052805280528052805280528052805280528052805280528052805 %280528052805280528050028000500280005002800050028000500280005 %002800050028000500280005002800050028000500280005002800050028 %000500280005002800050028000500280005002800050028000500280005 %002800050028000500280005002800050028000500280005002800050028 %000500280005002800050028000500280005280028052800280528002805 %280028052800280528002805280028052800280528002805280028052800 %280528002805280028052800280528002805280028052800280528002805 %280028052800280528002805280028052800280528002805280028052800 %280528002805280028052800280528002805280028052800280500050028 %000500280005002800050028000500280005002800050028000500280005 %002800050028000500280005002800050028000500280005002800050028 %000500280005002800050028000500280005002800050028000500280005 %002800050028000500280005002800050028000500280005002800050028 %000500282805280528052805280528052805280528052805280528052805 %280528052805280528052805280528052805280528052805280528052805 %280528052805280528052805280528052805280528052805280528052805 %280528052805280528052805280528052805280528052805280528052805 %280528052805280528052805000500280005002800050028000500280005 %002800050028000500280005002800050028000500280005002800050028 %000500280005002800050028000500280005002800050028000500280005 %002800050028000500280005002800050028000500280005002800050028 %000500280005002800050028000500280005002805000500050005000500 %050005000500050005000500050005000500050005000500050005000500 %050005000500050005000500050005000500050005000500050005000500 %050005000500050005000500050005000500050005000500050005000500 %05000500050005000500050005000500050005000500050005000500537D %597D537D597D537D597D537D597D537D597D537D597D537D597D537D597D %537D597D537D597D537D597D537D597D537D597D537D597D537D597D537D %597D537D597D537D597D537D597D537D597D537D597D537D597D537D597D %537D597D537D597D537D597D537D597D537D597D537D597D537D597D537D %597D537D597D %%EndData endstream endobj 21 0 obj <>stream +„ħ|ÙnB/æÓúÒK˜/m#ÖÄÐî'ˆT=Í|4°ˆFó +ð9º,Žˆ10Œ¼Òbrº@DšØfÀSTIäÌðŒh‰Uè1 ÑcÑB°^¦Ee‘•d…ðe‚aÌ‚„'4,ó³BÓ¾@BŸÅ!d%Ý BDË*1„„ ž^ç{&’׆ޥñ%L\4ô(Àâ…,òƒh¬x!Њô…ů·”ž0~G! +­G$æ#g 3ôXNÇ"±5¾¥T’sC"eŸN¢eejÞúXQb$eKI*ª+Zj¹îå½)ì ÝG”‚˹iÄžÿ#fÎä9§ã„‘mKgÜÒÑr†˜odɕ׎ˆ„AÄÍ“@aÉ»B54 ¡yÈB/l<±fÍÜ„´»0æ!CYÂtH†(Á bž'z%q*^0èD‡ü#½ÉˆÎB˜),¶\" 0‹ó¼Ài‘ ‹*º/ÒXuè•J³D1PìˆÕ3Ðb ½',?VÊ`¤9Ñ*· ¹`.1¡뚥ùu´h6qÄ‹F¸aJk \Á “ZðZùh 2ŸGl\Ðj'Ì £!­qš¢«"Ò ,=Üc½2a‘ʳ!­›ª–J(Z=ó#ŠyI{/é÷E# ÎƒäGC²ðˆ4­µÉm‚-ЄDžbÉL‚¨Ç£Š<ÚŠ5Zû8–·ç×­+ó0+WLRaHaID/‚‰¢ñhÏ!5˜P– Ai°å‰Uìó}´?Z6†Ô=Vp¡ß ++5°yh4X¢€—«8†nè¼-W¢4Úx±N0'/aD ^íåúYÐrs[¡@ M +¨üó­Ve‡5tè„ïRŒÇt“o|ÃöŒtA@,–ŒÔ%méUÃìžã›ˆ-cg¢ÓÛ41ÇJyM€¤¬~‚†aOCÑ?² Ú ±ŠJï1,‰Éü(‰ck*³bGö°UdzO’ K9…— 5á{4L02œ"ˆ\¦Æ0 ®L¨'º²Æ@èYÙd\Ù[bpeZíëÄútS,àóRÐzŸB0A˜‘㊸YÝ%d‰NË?ÃËÿ¤±ïåé`¬~È{ZX¤™®°æÜ …3&V˜yam€JÐD±i‹å'žÊÖ˜,9¡vEýЮ9£60Ê°C\º>"вè¡O,Ï!ÃŽ™¤‚p8\‹¬u Ö'è=Sy´ÕÌÉÀÚˆh,B²U”†KXeDt¬"Î`¬Ù$V„gZT=üÍr¯0Ë‹ƒ äD«@x_¦“_Ál†ß›wDôŠU‰ lvÔ²NÌ;èo¬$”‡¨×ü»òvÒžAA¤Í!ÖÈ¿DÆ$¨`XaßÃ;I¤y‘¸0ÃW^Å.æŠó&hÔ1;RŠóè’c˜=%bGpd2®û³è@ãgå»:y­|åV€ujp@^ýtȾáØ0£‚xsc')æp§jÍkh€ÏÅQtÅr*9üáN.Z„ ò‘…:b ¾âKhãxíT€‡À§  _zìŒ }R…BÄbB+«x6ÿïBÐÏœyŒ—xÌbaË’M:jB[Š+ˆ‘iþ›E(IJ2|ñÜ+­›,¨&Á&ú[¼^ÐG4k·J”ÃHÜ×F´_ÞC6i +ìg[ { žÆJ[LÔŠWj-ÚOÀì™ï`¯™€ éÀ•eL#ˆC!Nõn¨É*…ºÊá°Äº=f,w"– •–¼Elgo¨•8bPpØ!"¹Û€ý †H@)ñç‘|ŠØŸ:”g´>›ƒ¸Ý"…çÃAG˜ÅÓ,`Ï.@ìÄ#2âØð7„;¸]"”"P…Ui®õ¡Òz”V¬´ªõ¡‘Èr´2Ϭ9$‚ÀJ 3Q-ÞŸ”òm +ò‹@žÇkìö¡¹ØÏ»¡Bp¨;*Ç—˜Ÿ¢PÐ2ø>Ë$¢+Ÿ¤I)â2ó°^šrLL~­c˜ÂaY–yY9ÅOr|l%?!}Bc¼ÊGÅ,lᎠ5Wआ¨ˆíyèÈ8{ÓY˜‹“‹Þ«B–á©KŒYcVC‹WFìHö;?®Š¡e“Æ>Ä:˜'Ö K´0°xC²IÞ„B6BiA.^ÙÔP¬bYmdx4$+Ëèä±7ƒF'+°~ŽÒdÁÝ KRƒÖÊ.ø<(R±‹_Âo å| ›m ª sÀÙÀ“F6–'NJ¦(\#ʽ0àÿƪ!.*·0‡®`w9:Œ¨rÝHs Š•Mžˆ¹‰ð›G*I!àD{ ­]&ö2@¬À`;IÆúÌ*<",¬«c"nÁœAV[ˆt8ôe$ˆ Yûå{|TÀ ŠD¾Ë€‰9’&Ï(aN€÷H¹g£Ý½Ìš†‰ 9`‡«¶Ž ·©‹Ñ‰ù±ùg³  V!Ën ¿ˆ%o†6æË=ÊÆa”ÜSŠQãëÅ|9H?Ób»jÝ¢å8« ”£±Åˆm)®[g·ƒPJäB…´ZüÀôØ_j‰†ý 6úC¸3ª‘’JúÅ°Ç^m-Øg5§Šb‹˜\k‰ïÅe~7Ô*³¢˜}Ñ"¡U¶C)%âÝ»c`Á&‘røK ÑqÙy]BŽ*àáÌŠØi“¤ì)…-’Þ µLY(§52ê²Ï›'ö¥£({•%Æ´ˆ¹×»bƒÉæ²heŒ„ÅÆásÄ+èð(H8ëOK(È2^PŸµ.ðžRñ8hlç¥|Žù³Ê™…PýLëp~>æÿŽK]:œ†HôÚ†}a¨k&“$ AÕ°®ËDâ%ÈË„’ZG"» +ÖÈÆ­9âH/Ž…V…Ãåò“Ïq%«hBëÁ‹8y`˾Be½˜Ú¥ á ýÀ²fo·áüR‚iߦ´iÑ¡”g$†œ(DªðÂÊ™2V +U±8‹ƒÛMCĨØ=™&jðØíÊà¡6•.2JrG•DKsæ|âÊ”W¯´Æul Å9Æ c¤ W³#€õ:ö¸Âà4JŒù$7WbÛ³° +8Þ ªv}Q_¢ì«b‰ÿ$iÒŠr%’’Áoœø+íJWl3`¶±É©›p[øµoÙ´àf±IóyÄ)7îTð9c… T2³X‹ËB¼z¡8¯%%…—&0{%ÃÙØ,¨úl ð•³%22ÎÛ‰X+¯h}¨Åº—÷¦¸uŽßÚâ±eÉ{AŠ‘åÔ6gšsb$ɾ^M‚Å„›wC-ÒrÊ©;¥ÆêØ)v–·Æ.Z ÂO*ígw’/JÙ-G`½â›ãÜßcíCR"f²ðqÈŠˆWâ:PF$)C³dçà•ª¤Âˆ¢ÓËZàb ]ó8=ÓñK«ÉÂ!áŒ_°"ÖEa‚‡‘èÅF”šRJS}¨EâS)5ª˜>U瀬]Oô©4ŽÍÖ«1’®%ª"ÞÈ¿˜áp8»;°êÌàK¹ùï†Z$ð—Rü‹éFõ¡rNR9m©ErÓÞPiƒ'²PÆÒƒêC-r©Zä[³²fZæn“YÞ µÌy)åÅ“gdU + A夡V©E±Um¦c„Þ ¢!‰†’Àu™X‰`ÓdͶ„€¤†ã2ìrJ„f]fGÄnWNþ…¿KÖ\\Ìp^ÊZbÒZ}zȈÓìQ`T øXí«€/x( xG+x[ x«" €2¦#AlÒhd%'[aõDy8†ØŸ Ç­$)ÄÙ€`ºu|ŸØ ?ºyÌA +\ƦԇZ¦ªÓÙJiM"Ì +¹Oåô¨B•0«+ˆ ¹Ã¦‚Y-—pö V6Ã?5òwé àÈ]bà +ÞíÀ:^¶˜ +i9 ûfüX<ö¢aß]dõFÚð¦'ì“àˆ€$H (PJœžy|éZÊ!›eHWIbIáHØš¦‹8Öç 4â&@¬_}·?‘,AGöeÙÉGµ„’ÙçI €3íHªÄx0‹§ˆÏºá±!Cr#ž/ã˘Cá ‹Ù÷9éÌ…º%Kš3‚ ·B› u˜³Œ-ì—ðÂü––6½ˆ’êFZ ,ä§Êß6{´ «1’ÄC’ó [ ˜V'2.ŸyWJÍ+¤ïÉø]Hψ¤†0ðØíd_”ßi+Þ$äàÄštÄɃ2¬ ³Ö„e6჈ìÈf%®ËgŒxÅYqØíX¤)â_Ðò| '•ÐAØ\eJH•G¼™ÈY˜•¨ª1]l\ê„„âÔ? s…Ýd“K¹„…Ì7a5…ì¸Rþ\!Ç®Î9vÆ&0ØĦ¤ÊñVèî^d“ž#ÉÔãô©î<$RÅì÷ÓH&BRk [¢Ýxœå!9¨±M®!µ‰ÛAbÅÓ…UXo1’Lˆ0”4g´ˆݱõZåû•2á +9eYg¥¬´b>›]P¿9ÑINÎ…ÖBñÈ„c‘²ŠûñÀO¬›Ù°‡'‚‡ÂÍ1ØÐyaÕ%“¡Õ_C{’ˆ< Ÿý€FâébR°d¥‡+–ë!§·Ç‘¥ËÌ–W ŠXRÀ#!ž…u{³ËY NÛ[ +‰”’ˆWH¶,¥c¶°;rŽÔlv3E–›ñÉ6¢¦÷'æx>bõ¼d­v/¿¿%(" c–a{:¶†£\=©¥PÌ)dîXÈ;,e&’eÿ„{Är@¤Â‚I²9žì0‡ ”UæÍ!‡8±B˜†°YÀ¥ØÿGª)t-Ÿ‰•â¤d*³"ÅQ?`dœfðŠÃ1Mø&*#Äy+Hwc•¨¸',’|ËááÐôD#C#à”:9ß̶.ÿM*›‰Ëog7\a„¥9ç)>8ö‰óÒ@h†C ZWEY%™À*92hk€KÌgr•)%?²W½YÊ l™di™³îÅcââ)—éâ d©±rOŠ?‹5„¡khd[ŠU ä•¿šG“Vbsh8î¡L <Ç&B€KëD®b¹Eg)ϳ” ZÌ­7ÿ ‡DÒ $ø§mò+š"ý"¡…‹'4qvN(\Y†1’êíRÉXiVq)ƒ·>Ô*Ç·˜œÍž)å×ò]7(9=¼èâó>Jó=Œ[:ñ¾†\(¥Õî÷¯¸¿E @† $,*ö•ði"NþS8ΦT)™¹>TLw.fCç³¥ëHá‹™|#Îi—D@ó|_ +>@ëàcLJ¦äô#b¥cÎ Ìg!³” yÌŒl5ÇV.BáÇ{Ö cn‚}hÈ™XèÛ€xPú$ƒµ9$`›w0Sܸ‹?Ê[ÌêÄÈ5ìAGz¹gBpiƒuçÍ e ‰àNÀa'7Íâ´‘m—Y•ÂŠ—´Î }À¬Ø¥k›vï±rħu±EQ(BÌîкG +yÊï†J‰Ìù4çBôLi‘º‘…¦Ç™ƺ$Žt$©²y=’«‡ð Äh‹¤®bÞW)1¬”°”#Ø*‘°Uºa9%±Vj‘:VN/+¦ q暥YìJñƒ|vßRL@*')gľL26®Ì¶'Ç ga›¹.l,3bÊY3¥ÜÉgbϪq$’£<¡WJ)1‚ï*gO”3, +rIŒ(ÄÑ[ÅÚóq@ö¤”¢…-cŠùÈ£uPcí #rÈ)²h‡pë#å0œ ß•ƒuå€^9ìWŠ –¢‡ùˆ‘¼Œ×Y[/-‘ƒL.EGä–b¥bÉGaøšHÄÙÒ@]¾†­lß]/x¿ +ƒ¡…¼™oþh,a‘8áC&Æê¿E÷(ßRð ]¬7#|ÁÓט÷M1u%ì"ì¹TöK D|\Ì”2652ç¸)zv +¾¸ñÊgpó‡tKXëCEÉáPK/JÁÓRrÇ€7jaœ+¥Æzw +ÀZÏû Š~…‚m ¶]:—Z8·š·9!…òViÑj-ZNÐŽò¶UÁô*høuVùîb?šŠË*lt²¨_ׇÊxQCÏ«§¬ äÎú϶:-XV7ÿŽôâü“ µˆ_Öà„+ç|û6Ñ6äê?º“T‘t¢"Í)Ô•ØUeM“gè6_kìäDÀ Ki˜Šjœš®9”Ñ(^‰À‰#뻨±SŸœ¤aP€hB9ñát sÁ^$ÒÔ†ÒôÓÀž¾¬5²odÅV GÙPÈf»[J¹*‘ôD `˜ÕCpvtÃÃ%#WWåˆ-Ãä+*ÝÉùÌIÂV®ÖO]FNãò”¬¢cnòZ#ÃIüú¬õ(Þ¶æââÊÜ»¢8³jšÁÖ‘Ðöåy²i&B`÷8`•[ Üh"NlÖÊ"^ðО愿˜5^>Ú%é[Ë^^ìPè³½©íÉÝ"¢ÂdÝâJ»CaåêµÊö¦/顉Ow‘{z­üB…â2¦|2˜ã7L;âósþÁZó*ç`ÜŠ0å0,½3½Ì½ V~§°.G«8¼É…iØÙ®¤¨Š’ ãZó:X ±%9åŽËå`Q"VEz¯»,}E­Åkk\®Iü6¾¥AIJi%¤SK¯Šrç[õ8 ‘àDó.wŸRwñ]ò~-íà•fk˜£é¢¯*{>´Ö¼LÛC2x¾Ò¶$Fd7s7¦W¥Ï¯µx§(‰Õ7‡®lLžåú°\Ù£Òkš=2¤»E‹~wÏŽwλ•ÕÓãW\ë‹Ôæ[ýñ.ÊË·Á@¢ ÿ‹žíá`¤7ó¡XüƉÀéQö‹¤æñy; .Þ:\~¾dC ÐÝÞüâ~òš_ÙšålŠ•Ðê5Ó/i=®n£ÉnåÕéÎîAãø¬èk¼0 ÖHVÉý{ú“¡|ÙS¤³qR‚*:fI¿…žçÛwɵ¤e…œLK:„ÁD}¤¹jx·å:2ÔeßAÚlг#~‡½ NùŽ_Bj ge|¢¤2]ϼ ±Ðˆ4£ ™ëÏ4­éì´db`=¤¯{¤îèÐLj>èm(íÇ0ØC3xuþmpárŠ™!Ê#®’ž÷!/ +ñ"äÄ‘ž"‰£ð¢Ÿg·|Ùœ˜^–àŒo ­:v +úôô/*•7 ÊM•šŠS Iö5~‚$é'ð$Fè­B¿C‚I0£Á4/ˆºGìFhJÔ_}ÆBžÃíÒç°#Ðáöö‚6ƒ0×Qyã£&Êý…v>üש€:¡ñú*ŽhTãÈ »ÄÎRÓ0è~®™hÇp´NNCj&½+ü°W¡²¨GJ%dü—¨”*vˆHL'¦Óí–G"8#mÈ\À ‹o2Ä%ìé(Òˆ6|Ø»Èz¦w‘}ïü˜¨ø®Ÿ 5äu¬™jÊΑ$#5Ëÿ)Øy\Né‡a`+›@ ë:ÌdÍý1l$¢ w`þK³âYájaÍq»=Ò¿š/“ 9óõŠ9óã7 ÿÄ›†rˆÃ?:³áŸZâêÍ ÍKfRÊ٠ܥœÕ4÷sñÏŸ¼¶4ôÜÏ–Qk¯Å“øJÃ¥¡8&[ÌXÆl7ÿãO½æ§GøS“˾°0“Ò"Δ׵&!EŽ…TÃ,ZçB#Í€ce]Ü:‘:G<ýA˜À GgoÄ: -‚˜2f•¤ú•|õ^1'¤ÚüD•‡SyrÓ°s+Ë—YJ¿¥Ýpæ|q®l·šxúþ©_Ùþ°ÉÓ}NÿÊö‡-À•ícè)SÇ'Ç?åañ*#Ó''‡táå¯íœ5NçŽÑ¬}áü`W:Ç^Ùvm³}ûæäôì¾mŠÚ¹«fíô¤Þxû¶²Þ¨Ÿí¿:,zj¼Ž=5Ü҃肔tÕ\¤3PKE’ã¾íôIò-ðq ‡ªJ×ȹç0ÿâÂ^ài¢4$­/!ß|ÈÌPùšáâEµ¹¨ÅËr#n5äüÄK—Ì ñ5PrMNb®©ñ5xHbBœó.]ÁÃ5…gä‡Ûò’ÂÚ”®)¯L«KJ/ÊŽµÅPóÓ-¯Èûñ¥ö£Æx{¶¸GZ’JB^ÍóÓÆ ^]€ê0*NbÄᥲŠ–ö 4h˜_*JÑ#b;9â3F¸ +‘É‚ÊïqÚ8ŽÅ4s·éüeèÔKö¯‡s+ØNZË+ü†a(Õ‚$a/†¶N’ðgšXÑYÆ¥{‘¦ÐÄbÒÊ‘ñú!o ƒDlóÀða9®Xp–ùD ‘Ì\Ò‹ýK>ä]±¼ Âg®àˆŸþH[†&dÍðùÒ=,Ì(âÞ8àÄõ€9!ç;f–LG(TýQ¸ãÏ÷²ýû&³hE×ÁÏɯ|áWnд®w¢Éè}þ5à_9¯Ôô~k5/¹–q[59á&xê$Äènªñþ\nL!Ù= h|Àû(aš"zQ~ Y)`¥£µë|€êT¿(3&l3hádõpIX·Çú£HʸxœÀß‚õ·”ÖÃ%Aó¯ó¡ò'(Wç" ª$¯µK??ò1¦04£ù™&Wðõ«À7®Øgˆ« œÃúƒ^f∿ð µÄþYVR±ñTTF +Î~TDÉDŽÏÇq•E#G'È21¡Ç.KìÐk@ +ìñ"º(%($!#{6{|”â–\R9Κù‰ )dÌÿVsDäºïùy¸øûO¼·fñÅKpô'Z‹çg~·6LÂl!swIs·|ÊeŸ–~Î P8Hæ÷V“Ïÿ\|xvhå‘å×5óãLqKrsæçf¢¹ŸyHͳ*ü”›iö·V›˜û±ðÐ8 )ŽæÇövæ§6>7pì·5Ø,z@‹¾l8JáÈ…£–_f'ÎNm·TË‚†›0wc P’z”+*2=éU^?xùò丵+1—.ÕŸjíÉFÙÊ7• àËfîÇí¸»IWϘ»%„ù84j·1æ¤PœY”bÊ(>pU ”£PJ/pŒ@#õ›Ó:ÑìÓãÜØ ª*.ªâ£æ­¥G´œðômç d$„šOÁqe:7êÓ1Òsâóé.þȧ·5—dG}TDé—Ö#¦žÈã0‹Ž"©5†\UTÁA£ Tw#‰CAšÁq×Q©µáTêTœËg¢¥Æ¯ZÈ‘m9 òªqº ½ê¸ôKk)þ¹}ÞžËå"¾èŽH-jdkD±Çm8%Q˦Cé%iÛ'øMJ³µ,ÐU×G¯dß2€ËÇy\Q7ˆ\Íq%‚ìwWc©•oÙh„³É¶‚F(g”5NÊÛ¦+&H$ääkm‹ç¡FMMŽËq­C0æ°ïkל!’R´ñ(º‡5òy ؇€ZKJ¿:U«]«Œlœ¿ª\ÝØßyÓ˜>lï.ïœÕ÷ïŸL7öNN-1_ny%‘þþY{Éݽ¤É®â ;6b d à—ÑRG^( ÍI¸újû$œTEk-¢í™p Dl[ I ÷xf]ŒóëQ65†hĉdnµ‚*”1çÛKׂ÷Ri +…ß+š/ú\‹(ä*šqÿ^ôááŠBÊp#o>ÚBThdƒ^jŸØGó, +רâÂY¤îѳ}ÅÇžù +QÀµå =˜'@Òù#ÈAé´FYG>‰êIèl-„DF0Wz”X5æ(º¦$ç$²Má"cɈ.¸<”ñ,ñc‰©ñ!y.5Ë…‚qô]‡–Š|_Štâ£''Ü|†É‚hïBÁOÎrÇqz>#Å8à®Æ¡%Í$H^KÏ#•)lhKE‘±å#ˆø£ç…§`v¤¢åcyÄ¡þ˜ÏœTJ:6ÆÈœ‡‰éK5%œ½•NÓ(VX£Ï×ÒɈ°È-µ¤c—ËR(Ø Ðì] +lf2Œ #q#-EèQ @a‰åÇk†ETN´Hé6î¨HL¥vä$:ã8˜'ŽÇ& ªÞ1óéx…z7ÇŽ ×UFsßr.©‡SãÀì\ØÅã?,ÄB‚A  :+òÂrej_:ÙJ±K²@"•OÜÁeWp/ ØC,êUì°‰mùKt>0LÏr®¡Î5×¥ð 7á¶V1zQI}f’XŠÉ@Êy+4Ûö…¡+~,×â–;ìQ°¨ÈÇÜA„R-Dñ&F¶Š`žeb)Ê)zª¯¸în¥„vÀEô´Ô‘ ^É9ƒã&¨\ïYqÕ¾P ê+WÊYIc{­y¾(fì6ã'n4·‘“²Lœ JôLØœá"*YÛ*ÃÚVÃ38Oí1ê cÄÙz ÕR^ÚÝH`3 ³¹`5:4Á–Ѐ¤²“'bœÐÔÃ1@îØás YX£®…à„ùhËöx%Cnw&6‚0ÂDŠØð‘B_úrØçÀ>±Ç +Ò ù5ÂPpþ––ú¶©ÎƇ\Ñ5áåå5ó™NX9Á¨8`Úçš‚ŠÓ“¥„ +¶[º×xÒÔIq(J³ØŸÅ²ÙRÚ>²8ñO'eLt\©êÍå³êlõIƒ(¸p0•¦¥Cmù’ò¥m„ÅFÞE>­«,Ûi$\)ÁÖÑA®ëXÀ; #Š_(óóáˆ|i¯žøSwlá6/i[0 ?Ë8@Û –*œ²è\ä@s§ßÀ’ò¬nd®n¸ã-#£Vöà4Ëd’…AÚ:-RpàXIãCêÄ?c( \˜0Fý©˜Q5PW†d<+¥áÊ +EU".ÉUεTu"J£^TºHh_°`ø¯H[¼V¬3F%¾ U |eUD[p\Û#Ù¨íësQEÛB…cHáæ]±0ƒÐ›õÅÉ6ÄQ39 ÈȨX"ÉiAîø˹y…ƒ‡é‰RSßö’ab¶½ß8¿Tƒ“éxA*Q`…»/€Îj¼ì|ht˜ë/Rä)ŠBÛýG$ù ¿,·d/¨+Å/á’f¬*r]JîÁALÐ8+ÕmŠ,WXQ¨ö‘0VÙ,Å$–åµu‡¤ç¼ØPÄ‚ÈRº\#ÝŸ^Å ……l…3ŸOÊ©bîuÆíë ù¿n$|ÆÕÃâj–¡bUIqA6`Ø7äLE¸44MSÛbµ|G„ZÏXw®òjÆZ©ˆJåÚHÐVL3é›,eþQ õ¾ÁÀ¹"wCÒÌäC)€:lu9ÛZ(ª(E¤]+¸PqT´á ŸÌæ:s?$K,‹ßíO3ÁqÑ\AA.­íÚÙ*[ô‘+j)#Ú<¡”ô*ãª6RôŒ{Á iæ…EK¢Øª•,œA ¬¤kôaRö>R>"ÆXš¸/ê/œV\sFŠ‚aP‘3†áTâ£ßüx- ‘‚ÍHèÙ*¨Yl +Bã‹ÙÌL\¯GÅU£´°o®/‹AbE±Ï4Mz²ôÈ‹”¨ou|Ää~TxIäÙÐ +´ß³f±J!#": °›* „!lÛË‹í©žÁ5Ñ©4ÞÄ.!*%ýx¦h¦Ì’$Ô2.“àÐ(C$$ÜÀÍõÑâ.ªÒÚé|Ü5Þj +­n'‰‘Ði¼Äv‘58ï8y΢“Ñ\MÖFl$(°‰ #W&CÁÖÄâoXµex¥Ä]"Ý@óÜ÷ŠE?·NAÕ?‹‹&âbÉÚÖÐÃ(¡8£ +\Z†H¸Ì ŸžûöER‹?‘òÌàTbûŽA¶ŠHæÆÚ—¾c–^Œ±)ü1W#£ {¶§íiI|?ÕHÈHmxtl ¤Xc(‚8Ž¥ÉDí5Ì;ÑMœàH;—¶æc(¯¹YE$ûZ +ÆÛ=,¼*Ž +ÅÁD{¹ç‡T°=xžëÜÊeú|.†׈6A½11âAŠ‹RqÑ'. ÎˆéÕ&øB-tTÌÖ\ì†;±•HÄphµWzïFÚÞÇt!c"+ʘA`k˜H¡VY¥3UÀöÏ=Ÿ„ÉK'X¢aÉ j´TˆÄ*QÜ àî©ìk +¹øqÇŽéD43” +f©ƒâæÊ1êQ)qÑ7\tMÜ‚XÜH$¾T†3rÌÛ–?枈¡£”´*M&`„Sq,’8Ô±hœüzæ™ÚÞç‹MôàÎÜ‘ÛDqqeŽì‚·(k.i-±ÊW[-V£]àsâ4¯¢Øay¼cdk1bRq‰|BÆDt Júø÷PÝSü ÖS¥`#Š&†Ö‰Abƒ½,+B>Üb¬ÂÏu'Œ`bä§çè¹ËWò“òvØ(ŸKåI/]‘ÿ‘ÔFÁYˆ`óR®Ÿ+Ts!ê„ñ”LqÕ&|2Uߣ¦0Ž¥à8÷N¤Ž¸±E,I¬ƒ“` ¹Xb[ƒAùZªà²î®íƉ.… +@ ZvQžÿÕ¶'3ΛpÉsÁ^†Sä:6ƒVaÈF®+£/-Ÿ+Úv¶D”ú;èýikìF¶–bœÙ‡-ýeƒÈ)ܬŠÕ†7–» ‰¢Bcý7l¦Â­©ÓsÌÐiýÅ3õÃÅJ/Ûn—[|Ú÷Ï8ÕŒšãô.¡5„g+%”ZϨ SB=Ç|(µ¼"±ÒáÔR±¶ô¸æÅ#ÈåndÆÔ:áä|󉸅–ÙÍ,ÒßUÝï¼0®Z/îyçh–ê]¤×E±,},]ZÙAowƒ«}²‡^&Ì]ÙEï‹›ã°!Íãb—½pwã´YTHc}Ì]—™e‹fûÛyØKGÍæÇÍ%ÔÒ0†©,¸œa:bWJ­––¡(AIZ¥’¹fÌÚ$W½ÏqiìÜfM-lp×ÂN˜[ÃÉ ï| ®Yœ çé²Ëãs¯ÏŠ/È|MÇGoŒUbHaŽm¹Î~l_if¡í|Cf8Š(“шª¨æƒBúÄA‘áï%?>@mȇ°d€îýùQñ™¯Íš¥ùBtK鶠aЖ³žÃiG0äý˜£ÌèFCWà¼3­ßooÕ·ãc+CÆç^Ÿ_ùšŽ4M¯ +¤i wQȾQda…žÇ}X úEúd,0Û&­%‚QRîGØú* {~T¾Êm·“(„6,â;í"™¤‘4 ~B#†¬ƒ«M£fÙÐت&‰øMÉŒJ%ù¯é®Ânp¼Û ù즪Œ4nB0O3§¼ú›pÒHEÒ„«LD!¸üøèBGÀéàT“>2#ŠrßÒ¡ù„cô|R|Ș࠽&"àlLÒoB®B¨IÕ%Ê68¿”÷㣉d4MZh’B:“ý’D%0øÇ5©)\*‹ì(ÏÍD¤köþ“žFŠy¨„Œø§ÆË8Tº*)ʧoWQî[:²ÈCyN*§ábŽ´.©tIÄú¬Ç6ÎÔòÑO$‘‘èyTº$Íè$÷ÍEÊMò1`¥äkú¬·q|VE´`dá›ôõˆt4+Fj®ŒœûHëIIRoÝ Š1_‰Ú 2_['“d>¾0z Š&aÿ}+e.Jp*)Ü/Ü6=6\K—Ǭ}Û)@±pb@(GS"ßÞ*)ÈÞuCw§¶nûVS½Ñp߈üuHˆ‘g…ÂF”…»“Í­¶ï”‹h)ÂÜßmΛBco`@Ŧê»É±r#.¦C 8"  õˆ +HÅÍ;(i.K(v ï«e—€sÜ@ÙèÅÉ0祊ä:©Ã+KÊf-Èí…‰tá:.¼Ï¯`÷`À¬TÞqº€+õ±­PèEË:j×R@Øçµ Üv°ï‡A¨ïàØX|b·½•»dƾP†¸%g‡‚*¼)„oDMmS¼Œw’ŸÏÆ#‹GJæÄFˆ½Ó ÇŽÌ^Ð.°O,›á®ÀMPÀýiÝ­)04î(éÀ ›»pñi†Ø|/;6-wêPÛ7HW€¤ô  ï¦ {+ÍÌ^‡úéZ^ᦀ…´©Ö/+*›4ù`{ x¾ÛRÃXT]çNrJ&á9J”~Iv£[hA↠GKŒGŒ±'ó’ ‚Ù™‚RÌtñ:îL§qy¡,»p¾«Åà»\Î@’È/A¼(]¤h›tóÃÜò¸wvgtÜdC*´‹É,U5[UÊÓ¤I,~œt‘»¾\{U–<´O³f6v‹+²äBˆØiNâÐÓå[Ì~,^r059,TŒJÙëŒÝî<%HÎ]jrSEÈË"‰´¨UìC‰dlp2 b­½Õ »²íÃ|áƒRaS^8âMÒብqA)žga˜øAž;„‰4iÆr—¡»—–—†)ßæH´ /Å|2§ìuÌKyg"÷4?pü-'Dm7ä +æé(A/¶7øŒ‰TÇš»W‘òŸ„>N"„æÚ,åÎgPæ ,Sˆ{Ñå´ îƒ¥ÚEæ«kÉÖ¨ãvG# +¬dŒ´ìêQ"4±±C ätJSùÊE‘:áÑNš•nä».ÝÌd'HÒ^XéŠ2 ”EùIh‡pì,w³$Gð+|ËyEüòPât(hda‡ìX‡y^QR€˜$å•)L;iÍ]³äYˆº áunðý30rª+b€H.#³ú0r<–ˤ9 ›;KÚ0Ž;`Xq†‘Ê€¥mÚÒ;MP6¥R×—÷rz¹€XväDb’ÙèèId î¶PäöŠ… ÆO…‚žôcåvÁ>‹—Aædü&È!WÒ9TeçwîÖ +Iþùœ-ãPqS)‹Ç……vÁå G†'¡PeXä]äúg‰É)a¹í³‚+KLAU:öž;ÇçypÀm¢J@ÎpÌrud.QŽõÛè[^Dáš’$]ò¦¸É‚R©”Zá…‡Y±ä$^š8¡œÑMÑþ8+19:’«X(;ƒ¦LN)'+“ÉØËß,6FìK‹‚z å'§Eb&gu€û”¦Ÿ×^j©*J´¨ŒÒ©R°,¤‰6Í·fås.Ljʽқºf¥*œUÖ8ÛÙ¤z[ækuQ×ìx Ýò¾ÅØÔ)e:rª¦¨†( IˆòRÞês<eä&÷C°)_CN (,A2J` +D즒Ç„ó/e£4uÙÁq_dqž9@:vÚ·±u ‡æaö:G6:{¥€pº&û +Έ.Œ$¨J'å̈k+fæ•© é7¡»Ó=Ì­eÀMÓóKØdóÌÖr:!» ÞÈo‚²ÒÛÁó°0 +š¼•UšGë•qV¼dvå€Â®XsÈq4–¢Da“i² `âD°pÈж(€2¢µ ´¾ '2 *'™„ï¤S/‚œBŒ•"a®vpâFÉ‚‘;P½°$ ´³—^>E ú¤æŸèóqE96cÑvxMœÐÜ›Š§áø·FîLÖ —&(³(M`ºxœ!•_bqx¶Â–rmn˜hL¹Måî)(•aM”Tä vÚpìP"½Ð¸E ¢4ÙW;S:<4Xò‚Â$à^qW%Îט°Y’,(kn¤À”ÉqÞç„IX†|2+coÄùÍÌÀm¿o3ÄråŒôîÀ+% »ãr”Öӹ׊9ÎøÔ4‡pbMfá笷 :eA™5iÓµsk.púÎìF¤csÛ$áK}›2sÏbŸPföoXØäcÁ–j!è?‡ú!H +ð8µ8 CôÃÔHd ð)‰âRÄ2¢€CÊ'%b²0Žã–:Ž" [<¬^ìSšj1ƤÊUð5ðS óÕ˜.j1´[³燃$C ªœ ˜ !®M[’†n / 4„ˆÞh°àt±ɪ$rA>Ò–‚œ0æ“Sùë<76g7‰óÞ Ã1O¤§ÈpµS¯ÙB ’4Œ˜¥‘Ä 00éJ ¾›'ŸÁ¤Nt¡!m8‘»^Ëh›Öq”4—¨ ´áD¼"’¨Té`·> i8ÑY táD¬š'«æ¥¦…ı(Ý—$)\ÚÅuÑÄÀº,1©4šˆŽÍZ6ËEJdp.š¡nÇÛŒ&"U4’W¸h¢³5²ÑÄ *±AÓíK§À$ '¢ÛŠ]r'tA>â[¤µç¯“n²ü +‰ö!:aºPDÙOì"­äi. ˜‚âL@1tE~Clï䥌3ñD{Ð@O´Á\Ù8¡‡H +rÞ$™4îP9vÎ茾 Ä…¡~rŧ8 ':5 4œè´· Nɵ?Òiˆ"dšŠ«ý€Ùhb``‰J"±ŽLm-U²wŠ±í€nêl”3Ê;'ª«…PJ€ +ÈøŽûfQ¨rl¾}Ë£œ3ýU}á•Öcœ…¤;º@‰e ‘Û,>¶Í Ô—­íÎh}ÞÚj}qê×NÐǹ•—òžfˆŠ¶gïÅ/ɽÀ—£è2¦}¤•».QÉBž–‹Q¥ lŒª tþ1nÌ @êCS±gÖצ—mƨT“¹9C—+ÙuããŸùÍ÷mbk5Mc“¥&kªëõÙUÞhƒTˆO²È.ä7u‚f +ÝtC{r©q"ûäÒºh[3 gI:fÞ¼çY +OãÓÀòV?}«Sbšƒ“d?žBh§ ÕoÜTí±Sº²‹"…d +ËÇ%«KÀTûH7Äê§qÖ•êÄo¦‚ÞXRIóCB‡Ÿ.L•e0*61O˜h?‘[Âc.ü“Ç÷(Åã”*$%GQ5r$ÈyÐìÍA†TÒ8—sÈÓv`5õŒì¶¾Iž—‹S9H6N•Âœj'™mÖ„7ZËj¢\å!KPŸ¹Ìíbª:³¥öVcnóá—кÀŒƒ¦\Ë•˜{G%Žr2+eE ¥¶2"%]ó$§j‚²š˜º•“OÍ8•âÛœHt,2¢Óúù2â5°»”“Í)é$¹Ü>0—½Ù–úÊ‹)DQP"VbsêÖÌ+‘vfs&{$¯Æ4Ué‘I„´ñi®œ3EÊM`ÐQr¦’N +z‰VÅ¥‹<”cIéM…}9‡*_«¥)r•V9]e ´6]DŸz^¬E*É¥î–Ì×À¨ãvÇEä +CùaFiuÊ°e”V¤‚L½ |è f84âšPeXI({]]•FÞ¥„ÁÅ€RPj©q½³(ÀE¶ÂÍ>û“ùi⃔݋r$ês(™‡ìH™ ÉÄÁ7AÆЦ DÒ”r\†3ü(Ç‹|9âex–-4e8›$98Pjë&ÌÍžMaÚ²|âMèC‰v0@x’²r_YȱüDnÌHv!”Š’2è”Ö@*ï5³CCkV®I™HSùGòÚ^—JI?b$ s"Vò0Íqàó«Ù›cNÜʽBšx(™Èû0L“W¤ÏË¥¸¤ \2H +táʘMg@Òp¥$Vð’7ã•1»Ñr[c).»\á!9¿˺ÂÞ‹/Çr‰DRÏÈs¹UèÀ@§¶¢ƒ€‘7;ÍÐØ\]?̨­8jØ묢ilMyÖg ”É€ì­|è¹p]lq¸ù4q²ð[ý¦GC*ÚdG'¾.žChç ~7WçÉãvnUpÖÙ+®Ü^RrÊNfGLÕ-~;½^¿0²ûÛtÈ%Àx Š«i’žS\³ ñ": C>ctgPoõã*ctÊÏ¡<ŽzD&G8m£UªàÁ1R³ ¢nóæ&µ¤¯H¤zS–À ’§íóR§S"\0L5× (»M`hW]¢Eã`§sŽ‘0Š“U9ÏTn­ËRMiû¥cfbP.û8ºíôaÍ,n'“²ÉÞr4-³7î Tn£@é1[OGŽš›0vdZXbÑ–1Ó™¦R ©d¯ˆ2H)‹“":ÄZµ´ÆÈŸz|{]z¼G¬I sª^5A¦™™ÔÆ.±Xü 9…Vlä‚⫤¾mVA–HcNöØÿ“£ðæ¢dmÓ…%K@Ž eˆó~œ£zߊèÕûR×ÖÌ–9Sfcý¦*Å ”  +8Ô¡ÎõÎœ÷0‡'©GÕÉ‘,“Ís/•"7<.•\œ†¶|¿9[ª¨°&MPfíšÀ”f­Þ„ˆÝÓ|Ç÷ýŒo—¹M§ÐÜ[5PÙÈ!FR¿äÒZ™ÂãŸJ*Ç+%†ÁÃóR6ynÏqï >eAéšd)#ws œygs2csû… COh§€dÑ)3û,Jøì‹BܹÌò²œtK}6¨dŽº$:ò á F…„!©›¡%;_ˆó‚ƒò r M†‚&ÖËθ2Írvì;è¤.¿yÀÞo°WÍSõ™¯~7+t<ÐîfñhSÎâѦ”Å£M)‹G›Y<Ú³x´)eñhÓ"‹'çÆ–ÏâѦE6¥,mJY<Ú´ÈâѦ˜Å£M1‹6_ÒÌŠ‘,Ý, +à²s´)eñhÓ"‹G›R6Å,6o3ç 8‹G›R6¥,mZdñä® íâ³x8IJx´)eñhSÊâѦE6¥,mJY<Ú´ÈâѦ”Å£M)‹G›Y<¹ë$‹G›R6-²x´)eñhSÊâÑÁÿÏÞw®µ’# 7À=؃±éˆÎd0ØäèЃ“~¼×þ©JR·º3;3gf÷l8V+–J•TªšâÅ£hA/E zñ(Ú/E›ðâQ´ /^ÄíA¶¿÷âQ´ /EŒ +À¼xm‹GÑ&¼xmŠkg£ˆ„¸¥Mxñ(Ú¤¢Mxñ(Ú„/rß°õ¨kÿôL—Š6éÅ£h^<ŠôâQ´)^<Š6áÅ£h^<Šÿ¡<õâQ´ /”M÷Áúé(Ú/E›ðâQ´ /E›âÅ£h^<Š:áÅ£¨S¼x|õèů¢MxñøÖʽxm‹GÑ&¼xmŠ¢½x}‹Gѧxñ(ú„¢Oxñ(ú/EŸðâQô /EŸâÅ£è^<Š>áÅ£èS½xP’R´)v^˜ôTq<Æ~;pÕ^Ñ&ì¨ÍÁ’ ;ÂcDø: Ú mŠ +vÄG¿€ùì¼Ðg'ð5¦vßÔN˜ +Uí…)sý_X˜P4i'ðà$t°ø`ÎíÂÞp;o©€‰v^Öö—!ÚˆvnÁòÙ ‚…&g;¢OØ ¸‘Ìg'€Â€WÀN€v¸  +v<~;MØ P~óÛ ðTùí¼¨îʤ XHy>a'Àaývœ^ÐN€ËðÛ p±~;(baÀN €˜Û |[Áí–q;°±ÜNÀ‹;'˜¨Õ„ P‘Ê}ÁBÓ63iDŸ°–vßj©þW4a'@ûí¸~;×J|vßκKÚ Eí>äášr ÐdânÐN Ëuvœ^ÀNàr1OÿðI,š°°ó: Ú |û Ì-h'vUñ˜ï¤ €Ô$À%Û`á„€—¹vä" )ªŒÙ0%#aµd ‹”0Â$‘ž ¢íýÑÀç?:‘'ȶÐÀSKÑ* N/ $Úu ²‡FöâбgDÅüû™ ~ç4¿—_æÞƒpëžÑ@§9S°Y ôÕhT×hB‘ÅZºF‘gYåFˆ?¢€J_ÄÒ"zÈŠxKôEôÕ£žØ»k¢2:Ÿ!u•uÇŒ:ï!‹Íoˆ½"×h jºÛ’F~åF¦ËB‰g4àú&,’M^O¦EÞÅ­Wd{ ò +™ÑV€º›Ê:‹$«‚рߌɪk4 iDi‘{女x6Ø+ET£ÁÙeÕ5è̱å  ¶ä6ת£º6æjÂ"×f 3opLqÇ÷…Þʪk3 E¶A{ól:{€ …–Éï€9Ĺ-ÀàïËUÁfà^ ‰õhð‚r ƒ²V˜‡k208yV]Û`ïÖnÌdà)‚É@(ä&Ã}éÉm}ñ aÖ]“æQ„ƒk2ðîý][€Á<÷eÅwem›ú‚iÁ¦§U°yP6E®Énléyrm:‹Hó@±=5%úQ°èL%Â,üLòÇß®Í@gOï¡ÈµèL†‚Bf4/ ê”à¡-Ë+rŸºÄõ8ûszŠeÁV<£ÎöYqm:ËEêD‰k3 +¹ÍúbÐe&‘ÍKT£YjBÅ5è)ZIö N©’`/¢K=¡\{Î2û`²L~lM +cÁ\À“óA¡ÄgFcË@ö&a<ô‚,˜ ~„Åz˜Z{cæƒYÄ|ë„Œ ?¸¹ÀHY(6@¾1¦± Ež¹À-T¹¹À`–8{Ì\`p R¥Ô‘µÔ4vº¹¹ÀàÛ¡ºf0xæ4Ö +õÀÿÄ¢ÏpI6³…lHr±žZÔdÕ5è<á‚êš ‚¬Ôg.И7¬øü'Y¶MÕç?)Q`«B F槲W"²gAòGÁ×”=ºJ4MÂ)Æ•³éÃqP:–hÝ”ŽJ®¾ÆŠä²”Ž†–ýAéØëz1(Ëq¼Çò^‘àXìºqéXÎ]!.Áód‰qé ‹1+jòÂu)ŒÒ{é¨û=/âëgø'Æ¥ÃD3íÅ' œx +…ÔÖ'‹ÁÍ F=Û‚!iaçÊ&S¼SwMóì^‰¨áòB/0c>B\:C’è4ÄÀt†Ì[º¯˜Ä'†—ã—7>JI7„z®û=cbž ¥Ì°Cp¡”9ŒDJú2GãÑB²%Ì-](½BîBIÕEY\(©Ï¿¬ø\(eNi=JÉ£p®5]eÅg÷±ùvßfGQô ¤qÜdt:ÎK„ètŠÍö_wM®L F§c¹×ňr4Í°,FÓ$[(r ç B=“m£×Î*ûãÓ¹BŒ7;z¹'‹ê¸{¥/@Å….*4ŠŒˆG¦+“e®ðáî“NÅøtŒRøÔq3ƒpygp¥ïQüEJ¹…êqu_@PNñ}ˆlèº@xÃEd÷XP#³ïL¹7…>ÿIYgéùOzGÅõŸDµÂºu©s?WRÿI·HôŸô +¹pÇa-øOÊŒ}ú„QÙTlß‘‚ËHÕ¤ÀçWÜjÖTÒÔ •ÇbÍÐcÝco¾B“18σR–Y +1ÎXÊy?»Ð1k§ÈU\°«‚¥W$zº…®¥¬K~†§SéÎÏušZd pl1uÛu‚” W¹Ž==þ uô«Ð¢þÉŒ—{Ï(8}¯-–Ñ“*¨J!Š4Þ¼oùAaÆç@©xb²{1¢°[[ÄIÀµ9z#˜àPÄ@ÀƸö4¯D¸q ½‹·3ïbÄÔ»&Ço-æ6-œH…ÅÆõf~â;Îü +ÅÇ^e~H¸‘ FãÅ‹šíP˜²ŠÙÌ| ŠD Á+ä‚„Û™'mÈ\œ„Yã’Š+¼Ðwç> Gâœ@|K½Á}§Y(¯FèM%Ê÷‚…Rf¨ë+¤ŒN,ÙLäÁ”±ߣá©÷,ÙŒ÷©‚)Û5y¦l¯H0e{…õJ£Àù„Z CV„_–ÖÚ¹sèùDi–ûÕ/‡»P­Ö67üø +f†ñlÙ4N¦pð®Š'_tvþ“ÏȸZnÌöŠc¶Wè>KgòºgͶ8¡ŒÙ–Í…s¶e +»®6V¡ioÝ"~¦´NÀD(ïjD   +Ôð@°ÐdÖLo\–‚TœãþU°\µâbi’H¼"t^¡Khä.Ü‹÷æêpî݈iJpcÝ5xÛO£ŒùJÁ„]¸CÕ +]eØís4wPIæY÷fL| TT’Ùe³€Lb‘ ±AÎë̯;¦¸ #ÜÍòÄwKejV}÷"îÊE|pEFï^D()™¹¹÷"† Oˈ(Mf¨HÚçuE1 œ/Qñ1•š/ÉlÂ4 °ÏƒÍC×á5ŠiJ˜[gPb^¡$M¼ŠõÒxÁùùexH@p9vÏø™üžñ]ÿØl¿—c%%çx%vif¼¡Ve·ÔÕû@–sKi†þ¦F¹ ’%‰¦ŒvUú¤\4ƒ§H4y±(ƒìÜÂCvZ*Ù®»£lÑü²’áº'R[±ª‹é9RžÑYii,ƒ´ÞIÌÕªØLmT1%«[ÆåH´Ž¹¥î»JY¶·T=8 }k)¡#{‘”ƒ²¢pÈØësýQY‰¼PïÍ+ K?ïóí“úXQ]¤0ÍçdDzÑlÀ}¬¨(VUÙó½`!:Þ +Y:uoF`Ó2,ß¼UöÔÖÚ·ýQ "ÊÜFFíC)AM,êRß/ÚÛ]ÙµKÛ 3¼T¹èëí8دَs¼`yÇ|8åÊ"N©ÜYlË|+<´Uùµ°ˆÝ éS0^X* xGÅ+ñï•yêÕ ï Ï*¥é²@(7ľ¸1:ƒš°}Ô߇P*åìG(ú¾"Ä+ª®›(´Ta†q1’G‹åAd‚L뤈È/pQφ¡ À1„ÉÏxi #_„õò8;xãà'˜|ƒü´Q¥‘óEz ð’i™¬s"Mb,U“ÞÑL8ñÛÅšNã‚K\UtËÎᕺ:¤lÚãCÌžàXª{X=ÞÆÃ0‰,PeOü,4Û²P•ú\ø›ÓkFs;b "(BGHÖ#õ¢€h¼^‚,úÈÔQ¯3$º5’…ëb­Í L6²ô²Dô..ø•(³Õè9ádÇ ÓÑ š}‚Ø)o44n_iI¦…b¿k°§ì‚®Iœ*£íSÂÀÏ® …Þgf$Ót?q…˜ÇhøPW†h\–ÿÅa5ôsH ,°¤ÜºÆóu³D¬å,Ä"וóu³/bšoòÑËà]¦ýÈ:URW’­ÔÜ|à2d°‡r¢â(´&ᛃ0ß üŽ?cø™£óD¶3`&S˜ÉD&`)ÏQ„]giø=–¥ëþ¿X%ü«ŽÊ—6ð§1¬:ýIÏ%uAq« ©¼OU¸zÚË¡‰qHõQ`H¹áÍša“íí‘ÙLM Ðó…¯Ô ãÀjÜ'9îJÙ9¶<÷öÕ¢„êô'[)þíUwÿâ•ð/fÀöµ×Bã°•þ]§H¡Zåÿ +2üýêï¿ õŠ”ˆ‰ ¸èÃGëK7Bò6‚|F +G›ºûßYð£ÛsÈe.Ñ=°÷ö±š¤a˽‡ +"òºèÕEDšÒ˜Œ.àá”á¡‚ݹ2ºø 1<˜èCDdþýe›"“‘5ÛbI0Bz°š€°ªª3•<äÇ.w º¸ÓS“ÑD™2¯g£|«ãW¿†6H‘l›ps¤Ùº$É^^N–%p¬³lS‘LrZC¦‰+X¿pªí“êhÐú…´ Å2²ZªŽFΠk—Û­ºS®WÛ­îëÞ Õ8r~‡ÖH5=´~Ðù*‹p¨üÚwhEÒ¶×kcM½ÔúÅi—œAÓ©Ê¿vj½v¡[­µÞ­,ÔæýÒ*{ãVúµ‚ƒ~é÷#oØMòÿ;‹‡n®¢¤¢ô\è6è˜ø[{Î’ùvYIìŠÌ¨7‘5vœ8Ùâ ùÿÍÏ c×*ôº€™¿ñ¿7¿’‡äwR„÷K¡ûG)Ô ‡îæ¤möÒ2àæŒÚ—%úúÉr“eè,d’©™ã<^ óV9¢ØãRfk쩽•$%*u Ž–[h’‰ä±55ÜM¬‹š@J1árÐ0Á”(;6¦‘—i uÉ–¨ºLI²òáå³a@(2#ßXËXK±0wŠ^øj›Á“/ˆCe¶Šñ¬R*‡q¯=úTÆ`‰\ráoÊq~7-úMãIÇLÈ`db/þéÁœõhje@8^©B‘¨!Â2aì¦Âæ †¦„¼FûÉqÊ-ÀP—],!33xÂQø^æ“dÛd¯ñ³¡³ÌSКÌÇPi[¨BPÆPXà€:8¶‘ã!³àl†MÙA¦w|O Mˆ$Ò­òe™´Ð-2ŒgYç GÈoHC~Û +`ºÏk«ê@.0;¶Fá7Ú2%D¤Rà7:FPò€MÀi°ÝPh‘XoÒ6’­b‚©x`Q¶¦ÐFèì +éšqf +{QjÚþ :OpO$.œš´R¢MöJgQ uƒfÛÖ!±éo H “:êà;¡“>Í”‰WºB ®(!Óeê ƒmè#]‚G?Ð+uå‚„’3 7î L )4ƒ“oôh:¸¤i€4‹Ž £ÀáB6Ò½¦:mj,”?Ù2MUíL“\CPÑL¥o:®:¼ª1y ?ƒ½ò‚ÜÉ ¾ùÆFÔ™ÝÀ¸”Ø1}Wc€³žŸÙ!€“YŽhx"›7 슀D71z¿Är¿éü-.)¡Ù–tžo]•i }* ­¯@:]èT—øѳ4ÇÃói0ß*B31ú9Ÿn@f +Bè‹~ˆÚ0ÀÌž¸§„PÒP£ÔòxA¿øx “Y¤ÎÄ@_ ¢ÉÔ@±ƒ%)2yºxªCd9ŒoRwG^«˜ +r ½Ê HÅ Ð6}맃ÇÆ{‡U„B1ZAšhà±F€baªK‰b.À Lsu"Jjðü‰öC£Mi*Ý.ÌO†)ÔÀדœTͤ¯‰ ‘Iß`T@j"v_Õ±£éél¤šN³ªC›F&Ñ ÀŒ“6Í  +ð>éLš (Àžþk<„ÎbÉ’Þ¨ß.9xø€–&²EÞ§[”ë!iƒ©HÆd xÈDÀ"é$Ô +€Jc6gxBEÄ ò[µP RéÃ6B60µ´!G Ä0¥Ü6hNByˆž£ÒÓgε.Ñ&à…(‘"½aƒ>ÉøDšQ± €J$4’(ë1Q¢¢´ ÈDxn òšðŒGÑðù+ð!$¥?¦ÂÞd‘â뙘µ QA§Lˆ@‹ÇÔ¤YßLÈIcA Ó¤ŒÚ@<rtÝ¢tT/<ì÷Y¡•MŒIK’h¼ÒHC§k@p3#S\ HN–ÂHšAã*£P‡ ïP7‡~ La§Sˆ ãæ‚8kB˜MÌò§QüÄnU<–é…שׂ4I PDš-Ÿ¦ŠÇªMŸ“A &%”D#çh3Í9aÑ®Lfa¥‡ƒÅ m¶CÞo¾;nÂç(†brt+H,ó ¸$šž‘ Ô¶…؆%Dà7%F¦Tƒ¾r2%/¯ä×"}jTþS5*,š{5hdJƒúàÃOZmTx°„Á3-&h¨”3$‰–ŒiU¡†©³6ÕÉ`ï*U–[‡DBŠ·ND°"qÆQÁDC@ Rð^I™BÆpÑ ª P@(H‰× X#4æl ø(úɹ;yˆA`£%à Ð"SƘš2U€ÒP=6 Ñ Š•‰rHR‘Ðe×À *ç ÓÄ8àptÀnxÏð N0Ç‚M¤â<Š¦Âro'S)›7lîÉ’hh +g¤[P+ª(Ü¢ß)p<u*í1Vè¨ëšàœn"u€-%ÃÉÃd‘C  {# Há„LˆocâÔµ°cç"’ñ¼™XÅ:¨$é’ËÁ t "4 + Hí(}’B˜%2Y£i 'Ðß&#°!øM4$@‰Fã1A~°Cý %ùeÀc±À0&„­¸–‘âAMÁwöD±ËàŽºL´]…ŠY ™:'Ó,-#ZaŒ”~x÷ƃ4N"¶ëE2åøÇ „vS?pÐXÀQ]w ˜B‘Ù“@eŽ_:¸5[T,iY‡g« ÔÒƒÇeèV8ë2‹ IT<Õ%žTŒP&ÍBþ|j`’.nÃKBf©ÂQÕñ9·ÊB CfVÝ P„€À> æF”&0ø°…\4mZ1iD§ÜHM4y(ähB#°-àé<’mö6¨*ÙŠÄS}É48"³§Ì*„n#âVb¢7*ø—4¨&Û" t°xBGp WÜä/6Ur ²Í 1©ƒS¡ÆÈÀ¨eÊ`†Aª+¡RBZ¨ì+€…ˆñ!Y¦ §b£÷¼‚Ó’$ØV›e˜UmX?Ç™ÐIˆVA·PÃÈÔdÊ4Q1[†ìWc’àjÊfù“MH£‡ @±ŠÖAâ­`ª)Z€d$+vk)HPeÀ85*„ÁÓdšéŒIVHáo‘‘#“ࢠ+$Z´F4î ¦Rnö. 㦱5åÐ\Gó°—¨™í#x$P]µYæK~´¨¹ŽFŠ ¨DÀ¦¢½ƒ>$ðCñ*)C¶d‰ +å ‚@3›ë'•dã ³>aŽª…‘¡Ýló àÑá0˜ˆDÅõD‹ +Eˆ–Qiè.à€ä ÊT–¶crÀ³£"æ¡C¾Î†½$%›…’§À oŠ@÷i 84¿ŒÍC1<¶dʶÉp2–°ôfFŠ=>¶ãPY0ý$>Š7©!³ ®ˆ&€Ë“á(c:c|7AŠBã ¨ï ž`ð}‹šœ¨âCÃ)h1⇅ɵpep¸i˜rt +&„ ûSàÖqÚ=—’+j” mÊ{,œA•p(¡Ùb‘帞•”Mz²a’ +”¤óÞK½!ÃnIT§³iˆK"Ä‚DB…XsNC“ž.eÑb¯ìAÀ¡ÑR¼átO&›¦ƒÈeÒ“Üë†-¤#Ž¦«Lø—mj@€ŽXhR› *îpÅÑL55ò +D!|Ù¢ë<,±JÙC.iªjß4©È;§©h‡±ÙÓs‚ 4Xnð-Š÷gT'’Y˜>ØVÄePŠlgø +5Ñä6—ÿ ÌF•I®¨Q«6=n¤wú–žèb6MbÏÓ*h¡³ho¦0H5èOhzGó Üñ(hIB«5>ÀCJé®Î¦€‚†–E•¦ãS GªY‚̃K!;¥ËÔÊ2NØ BF±ÆhŽi3±¨&']‰rH&_»â”À3ÑÖKCÍ«L7'!MŒªH”{ãÁE)„O€v&{O ,H*èb’ìI–` Æ0(tu ÉñHªrN)#0L•ÅlõÃÔLÉ€0V/¼ªCÐÞÀÈ1`öĨ`À„M•nžÍ¿í\£ñ õcð'¸‚lc¡BϹ¡³ä¸¿ Hga¶cƒº+ÔìF¯Y¼i”¦ÏÐgd‰Ö¢iC‰Äacnµ –UÊAû¦S·ñÕ¬Œ'ˆÚüm|$Fí~”[Øø…Ÿg…eÆ` PS‰ÂÃÚA]°Ó‚lN{Èøì1ÄŸLSÉ£P¢äC%ÂVÃoI6=qfÛN9¼µDÁ…"Bû9Èe²*Ü›©p±B)(91¨Øª +½L)š +w-&½~C¹ +, +;°u„¤*,E« —PDCÓ@ë‚ùÂ}7AšaÓ“©¹W£—°ô¢‡¶hÀn÷z*œbÅ@½%°"'“pT›Ê7€kÐ1} æ;‚î€:»êTi< ²A¯”ñ’L‡«R;,D…†U4`²‰Z©)‰†ÔP@8Š%ÑùâÄÁf©sØTnH,û5° +Y¦†T“Å€# æ#B+øäpž`Ûeqmx,Ž—*,6’Z°§jôŒ“,xŒ f)E³)Y…ËenèábÂHh +} L€@›Œ‰W>“‚üa`°b à &“+½šÐ/´º‡2^ˆƒÝNU=   ÔÖ’Ò1a22* ¬’Ö9ò“ÞkÃÅ%ÚQ$êw§3œ }F;ƒ°ÁšQè‘âÏï=û ÄÑÁ{OS£×í +^‰hôƒL•ß’Ûܬ«&ØOÀndÃ5ºa@ £…øÞšpsh â)!Á;ur­}TMÅ ¨«¾chæÅ‹cÈ|M‹Êe2>°›‰ &$õ l( +U‡1Bù±!²Z8-P Áˆ¾ò0(/8И4 É$u ÂéÀk2-èÓ¦EO%ü/X4`=$C°”)#l,¬-@b‚;e°àY}¯…Þ›A ÖW¨ìƒ® Jc bk¼#õ!Oì,TòÍ +`C3ðú,Izn„d] ZH—Á´ è·ÿ`VETciÑb‚ „‹>²UR\0Z&» v” á‚‚ªºËÁÛhZãß™,l> ‚³ˆ†g4I¸aƒ«a oÔQw4X„`0JàÉu*Ø‘À FJˆ„êtl0‹‘ +r³¤R+):š¨½72X_ ]*¶°5 Ó„Ÿê¦pªU: õdoTÂîqˆ*Ö?¤Ý Ü!’Ž€: ˆhéYe‰¶@jÉÜUH•Môhó„A™³*,aFr"°è`Ò"“½"& y[¥Ž«„HÊ‚Çr7Ì +&“Bƒ]€ÌDôyàD˜Î¬h6r* ü5À:ˆw41Ht‡!2ZÒŠi"‚Ÿ^5ÉÄú-XE4n‚þ W +°XMN€ÆA̦²ØÈ dÜ-GeQ¸MUéýÝpL¢ˆC6]«<µ|zâ‰l¢—”`8nø +¯Êø¿ ¢—¤ÑýÄKgðèÀ ,ZÌTLhâo þ! ù¢ + &’p/`Ã)u× S ãªQs¹ÐQa‘ù1.Ž‚P@¿E¢7ƒØd#ø ôB£w}@¹Ò‚ ,AðaxÔ|ô¡UQ´Qá6”L /Ña\ˆ÷F~cü|¤$Í£võ04?FµÔ³)"€ÄH¸´0Ñ<©P‰ÄCÕ¢¦j0´v°™ÈhåWL̆àôØ5Å¢R@1 ÌÊz šW“u*C‚Ò » ë”qƒ€„ÌÁ6¨Õצ4QÆ›Ò^ûÁs “ƯâVl0¯ªˆHàZ·¬óÄÈD0$S@s„ÊL,2c‘u ô±º!hd<(³&q2Jd6ZÀ²`¡h9J=Ý$^ÜíÀBcHP<œÁjÁ;O…ˆ‘pÑ!M¼½Ãk+ªÈxjØEix™šý5ûñ§…d@£¾ JTÔTª¡= +9µøÊ,r1§ä Þ£¸ŽëA¥MA(ùi,2&Jðu=Й¨Ínþdaè"…ùˆé27”°HŒà +„úâtîPĤz¸»›µkî7?ÄÀã +zB›ÅµÇxR¤ +ð Ù¢fA |®@òÓ˜GQ¨ˆÃ­bpbáênññåÜ Àu­Á°Í–:šŸ\)ÝÃõÈÜ°oÓ|¤—69 !˜'"•2•KÔ: `æc£Š™]¬|H96 (ãMBãp×€ÊÂ¥3ÜA©Xå™`÷xueÑðZü€uæÀDJL ƒðÛ¢DÄJL(¦…ƒp‘:òd´2¡0™­C˜ê‚ÌUÆ+#†½t}w©#+WKp)ôA’I£QÆ+µ6a´Iàù&³Ì¢32%Igúª @•‹€Ì9PMÏNê‰ÞâRá¸é`¿·fe ‘~X¶0Œw:5OÃÍ%¡s ¦iHbZÁr4^ÎToÉs‚–Bõ^§ßw¡á[µï„:½†è=£`ÆežLh’VpBzU|A0B²è«ÂóÀš¢È?Í]\ú»ÜÅuøù|Ú떭.™¤ÅèD.~X8íË~)µÇäß³Ú;YÁB,ÓèÕœPv0¾…NªÝê«3 Î >ÿ[ˆ~ÌUÛíÖë ÚkÕYÍ +YýzHõG©Ð ÀúdÝx(¹ó7ç·ðWVC}ì‚48û©Úþf}Úw°*v1§~±]ñúUϺd§Þ|õrØÅ[uPïUÛ¡d¨èÎ`~£|ë'‚n%ì¢A+ˆÕNz}^…aÈ´¾œn«C»(;ÕÎz<”¢[Mp÷Ñ2ÂLÌ— ™0ùW +aê?s ÿ¨*ý‹H!#”9xÎÈk;Øò¸U#ÿ9W&5õgØz +Œçbµûw­dΕˆ ?EáÊJ¢.dúÿysýCC«.¾âÀD€#,*öeœ{õñ§ë¡=Kô;\¥ÿcÿý‹u× +ƒÊ8`ÒÍûWÀ8¸»t`oo¿°Þ?§Ñ +2¯Üq!…Jh=tÙíV;N#d¬‡øÉl×\ë!„™òžÉî_ž±ü:cžŒ¨&G«Ú!90M  ‹m­bTëêá89q Ò”5€åþ7wæÎÌ¢¾°j$¿@yg.\Qäéź¿˜röm*9÷†NûûO…M¿.~m;Ã…õ£nïç.þ BZ,Óíê¼~J¦N•õ à~rø×õûŠ­6Ô'(Üê†hZ§R«²Fêõ«Ö°Ed$èp²‡ò¨Zÿø=d«ÃV]˜@¯Û·F_iº¹Z/U£)½”GƒÞ‡óõY(ø¡}6` I¢°KØ]8õ¨ð‘Vcp Ñx”’d+šÑ8%YÇ{AÍ j(QñMðªÆ¼»Dc¶À¡Á€è¼ˆ)%ž†•‡‡»x¡òˆòƒ„|ï?fè&3ý-¡aÝ·„¼%„Z7 ²§Ú -„bÂÌAtÝ áBC",ÿs¬ì=š ´gÍæÐÅqþ3Ú³íö%üÞ Uí!rÒÈaóu:&È*4AÞ'µ+­ÑÙ\÷ )­N»>è[T'Šñ? ÈA r:ì·«¿ÒŸñè!PyæÊ)ëÏÙÂÀdÛÑí¾¾sq;óÖFvú«V*®Ã[€×ÃŒùÿ¥Ø¸ð'ÁÂÕ/‡Ná'§{ÖhàþÄÎf¿ü ÂßÞ7²5‚´1ÎqˆDÔh9DµèL“¶ÇQø¿Yx^œÍ +@vçðÝqœ&Ì¿GþkvX–â\Šÿówøß MùÁòèäñ»NÀÂzá§>†9àl;M ɦþ¿ þ‹.Ò7D—³ñèÈ.oâÇÛä"WÑfdƒÏ¶ Oµ ÅV £ÝC\I 8Hªlš)éð‚nfˆHŒh2ý"±a[‚À«O•drXh@¢‹þ›„#N­~ˆF5ã4~ÈF?D@®peߦã‹42[ÀþOÇý¯%þ ¥FEþ!LOCý²ô–¥³ÎOÙö^sô¯’¦ÿnsè—ñô?Þ~‡ž0w ‹ôßK3t ðÄl}2:üHŠmè– 7àk2‰¥ªTqüÛo¿†NªÃ?Kñâwsžš5ßÌ|VB± ñð{È\tqLU„«­? ¾.¿“ƒ£×Ž/è´[8%è0óÞõÿëÙ©®1ÍC£§âÏåªÿk 6?èõCå·j£÷óûoã°ÿ¤óýGôH¼”êú0x‘†Š´^ªÚšNý§ákÜŠÑÞúpP‡„ÛÌ~µ5€ôðâ¼_­O§ß7[úï%ÍÞö ëícAt‰?9ƒ‘ã¡S*gÛãA\W­ÝmøÚ½õ¿AîrÿFuð%*/¨ñ> î Š@¸¡Â/ý*¡ÏY§Ù8¡+g0l+Æ?À$ù½HûD¸4ýßKÓÿG¢o˜5Td ïØÔ ÷Ù‘BTT»CBT:óÙ‹[m&w:úoqa<¬WÛÎq«K=äeN±xÿ¹X…XèqÚ€~ôFÕ‘óœw^δFWþFHM»ãN®×oÑêîÐdÎ^§õ›ãë¿Óû‰ \ § +jY)Ž*IqKΠNvq¢·+ޛƦÓouK½–[“ÍÆi¶ ±¿Vx3mÀýÀ€´Åˆïs_œ¥cþ@ˆño0™(_3™ü½nj¦öH>?˜ã7˜£Ç$õø^M‚ГxÛ /°ÇÔð{üÿƒìñÆÕ¿Ä(ðƒ5_ƒ´1–lÚºfÉoWžÅ)uÊ(!\YBG› +U$m믺YûAÆ¿œ¶€UXÓ—¿ßÃÍb’7ÄÉ[Ɖ)Ù¶ÔåAùW”.³ß h> E…”£@PTˆšôïñ¸F_å/ÅŸÂîKl+ð¹ér£ú\ˆÿW^c{íVõÕyÞ+ÿ¸Ìü7_f~ÿCê’XÆ[Cì•£-B +Ý£í(.0½$¤W‚d/!ˆÁ auã1Ôÿ Ï÷?P˜Q Ò ÈŠ-i>“lÅÐÓÔ!ß.²mR Ùª¡Y6Ä´C@¦>_ìé5ü3©!>[¼ beèæEüAÿ….{ÿíz«üÏÒ[Õ¯ê­ò“Þú‰t‰Îj£7üZÿå$úŸl™ÿ Ø;Uèì ¯xûW®þ­ïv¿º³À€,£D¨†pظ¶ M‰¹®E(º¡ê×Å’4 f5á ¼ÍÑ?œ…†ßJD‚ÅŽøaÕøSY†Ö åzƒ®3†”Pÿ/}à:U +óMáϺ™tú;ïçsïçÿõìîâû¾ó˜o +Âÿ a^þ¥JÑ[î_HõîIiuÜ= +Ä®ÜêôÛ.±›±ÿ¦“Cq D(ôH€ÆŽ-t^äØo†Ÿ-m‹HlR†…R€G[< ß?¦mXŠ®+–¬Ñ܈†))ŠlCj[CÓ „¬ÄÐ$ÅÖmÂÐ ¾ŒØ½Ðÿ1Žð¾ùm!X¾Pª/@FyF4ÓÒmô.€\]’iCZCM2ihUeJ';"›6Ê4¦ªd@>kÈòŠ]lëFho2%f·ÉÔÙÈ “_èØêª/¶%2„DĽB:¦¡@‹Œ,cbcÈefÉH†Ä®–e°œ)• +MVÌ‚Ü2R’¥ÙØ +]±e‘þ!ŸïÄŠ_ptú“:䀅ü;Ñ<ø ÚS+¬iœmÛVu[V1˜aB&U²[¤ñ…BÉ/ò‡²#™7$´“d¥BHo½èdÑ&ö£p’óeY²$AÒb2 æ Zd +¦"NÎW3+e]¤ç¸ý{‚-´iˆÿ½æT™jÂ`ê )ÂÞÈùî˜ÚûĹÅIü‘ÈѲD?Ñ$ÐÍÿ#c zug8$¬ªÝWB_bWØ‘B{ƒV~‚ÔÕiu«#BÊϤVÈÇÐIuð1 =Ä~nÞBv“y<Ä­Z­×%ì9ƒš3¨Æƒ)2èè³WÁŸïÕǧ;ÊWGUHIÂc6Çß7'ǧ½†3õãf(öK§Ý%Ÿ“D ´jãÏö±ž ªOBÿB­ú[«Ý8]Z‡Ç`à_៑›¯$¶Ü>ÿT 7…X€bÕŸªí1¯ åÃõ@“§ÕØL†¾_ÿRèt{]ç €i÷êNã+á5×¾ïºj­nƒLTþÂÚr”Ñ).âÛëkÿIÛÿBBž ‰/­¿‰€¾‚øßÜÕÓAßøéËGª~gŒ†åÕÇÃQ¯ó})Ù_‡‡Ã*($ÀóÈû*:þåç¢ pÿ1Sùo8¥ÃæÏÿ`nüÁ„Éù.«2Q„4EWTò“»q´f­ùÍi½¾}唹5¿/1&šZJ´Ñk}?·uì›Ëc¿ïê¤o-ç—/Êl¤!+ßZɯ_Yɯß{%î­Ô¬eÔz#";ÍÑÙ EÔǯ¬j²Í?@@âWîu' 6…ï.Võ½§ÐqFÕ‘‰þÓyØÿá<" f„ø +v •‘`*šb¹ÉEíPu0ªõªƒF¨Þk÷!%¾=Ý/ÖpýÒÐuUŸ]Sþ5…ñ¿Y·æÂD6TKu«r#M®Úý©:,3?#áøfd9T8Cgð“ª8¿ŒB…FkT­µÚ­‘—¨•’¬lÛqp<¯¼=àÏrrƒ^?3pªôºrR¼ 'wà [ 3åyz¬iÚzÈ’-î±5ÓtÃfä]—Ebã¶3ÉŸK®p‡láAæõËn«N0„c7ªëÉUÊ\Ì`­Çy\8íJï‚ÎçTê [°Xüªði%UIW¢~¹ 9p˜øB¼þUã~º¦9"œû¡ãj÷u\}uB¥^Üg[Øþœ‰ ÔÀí‘îPßj£ %ñaýV*€IÃ~,Õà ìW@oÕvË“‹3¡ÌxÔ ]T‡#g bç Tt†«åÇ:ÔöÚÕ‘Cƒƒ––¹•?º½úGo<"ǬÇ"TµCýjß„†­Î¸·~®5ÐÍ­_%Ô¬þ+é¦Õ µù¾I1¼»?q­q'táŒÙ LA§m,ä9]2M<Á£€Å^è<þľ¹PÆå£ ›ëµ8ú:óÛ¸± ÷Ú½Zµ}áôÇí¡ûÕO7.@Jöák¥×¾q(dJÕY™Ãðú­U+ zÍVÛqsS»èç« +Î"ÞTäi:5§ñµKù"ôI  FM·L~pJ¤VßAâɳŸ‹í^o°ç‘pU1 s^ݬ»úoV½à<ÄÛä©õÎúÕº;YÅPäY“š.ÌU·lKžSÕ›*0¦9¿0Q”öÄy~cI, ÅñYUsN»#Â'H3û„ŠWTfOs¿GA¯»/èzÊŒ…Oχn„šîi­vG­!„Õá´£E)X8 J´Oê-;íýêˆP2gp +\aFÍã^½ÚZ!Ö+TÀ²£2 +%k))% B9Åq»Íé °I¾N.\Nèm»ÕuB#"gL +"ÓjÑÕ„ÁI*£}0›óqÎdè.ó8ÏÜI¤C…_F® (ÍØGB€ÝIÊA±#X¹ Þд¶%ÛrÈÒ´o·U]Ï8{ÚW-çg²X"ÕŒªÝºÃ мcU¬Ö Þ#~£2‚$P{öQÀÚ¾#+Š§Sk»t`¬±Ö×( Võ«¹´EÜiO®•ç2‘WrÀŒOz Wà$<·ï;GeànkøF¶DØ¿/o=tPjW»=ìÁ.À•âË]Ö)´Ÿa·:º !üsæ·Bšûû› BùA·áüRl †ß˜_°IÙ©÷º kˆ@ÀªÊÆ—at]ÜyË0¿¶³LJù¶{ø÷Öëãk›ëÕŸºMè·ôøyýHÔ ˆsÚ; +ÿä ÚLƒâ,rßçhï'gÐ÷ƒáüõv«Odi¸œù…èÔ¯„Ž²¦§(ûZ P’MþD*ˆ´ê‘QáU‹Oê+kDØ(öÈl.€TO +‰d 3T÷QÚ  •¨g| œ¯® ÞwntÎûægŽëž)l½?L‘³2Âkó9uÚrEØ q?PoøVm8g8·3m—,; 6*úk¹›#Öú¥Ÿòm‡§fj ýµœi5_ƒ5gÔ#pƒå>ydN=*̯Ùnõç ÂÊ~¢†:­b¯_ï}£ÂpÎŽc…ÆØ/«* æþ ˆÛëÿ}4Гgi›6—Ac0L9]*ߨÕwës0ŠÖa‡šcUlþô°MµÛåö OŒ¨Eo³gop½“ê¸âBì2UN…®Z(×C¬‡Xùú¬ôý¤ÌŸé¦OõÎÙÛCêâ´‚ÂËD?”rÖ;¿~Ì­P±GƒåͪùÞ«¥j­Q§ÚŸr„¦œð×ÎGªjK¯ÙLQ> +gVZ°ú´åù;¢o ŠÄ÷]t!žî^<þÈJ|f¾‰ñ†£vªA{DÎáô ü‚f¬¾w ¾Ò¦ßdjw¿<©~ãËSLv[L%ãýÁ7ê°ÑÙ¶z7XÁzÂM¡iϬ%^—ÊÖ¬Z­NõÕIµækŽ8õÒmm~Í7¼©Ïœ%­+"hŒàÑH¡Ú¯¡ü€9ƒù›½ø¨âä@õÔfW6§«ž`™ÓY Ú$µ€ZhÝœO'ΰõÚb VÄÓQc·gózÄŠaœ[¯:¤„êKgZÍgRõö@ î³jpI€hsÖ5]Q¶Ö®Öm6•©µToqžªó%¨Ø$Ï‚Ø©µúìþ`.ਯ©¹ÈÇêü4!(káspÞ×7ëý4Ãz¿]ÿu6U¢uêÝ R¬3"š eÍXÙ§vµÿm8°zsæŽ ë›rÖò4ßÄ\¬Né´w0¾Ò† ü^ܱ™@ +ïY,™x0·ùiÑ~ü:õAoŽŒUú„¾¶ºÍ9.VïO¾5(sjÕAШ:[¢ðØË*„%}«®Àc¾ yÓøBew_¨+LcÚ)jvG©F{>É£uúƒf¯;ÞAµ!Ñ–¹r9Cõ ´Se!‘ƒT ˜Ü`~ÃVŸpØîÇ—#´àw±¿c²¼Ép6öj„’ÍÀ%Ü”3vQ™zñUr`ÑÊ1‹Ö…hÑš¨Ê{+‹—#þË&¸Y‡[¦ŒÏÒ ÚÆ°'z½»Î^eE+Ì·ÑMJå¹—ÐÑÎÆ=ÿ”¡³)sö»ª@¥Ù’&êN^„­3“ƒO…½ì2¸Rßu<Æí ‰êèýü¼yÚ‚…ÑÁFè­lä‰Îæ÷E[}{¬?@OZÝöpDHÊسÒ!º!xŠ&£õl‹÷)ç,=ïÀÁ†ÚîÙò}bëz{e³z³v¨.Ÿ%³éÁ^çmãµ>,†×b+¹V55\2.÷ Fd#}¹·s¢ín?¬œ¤ãºY,('VTÖ´ˆ$ óïù×5i)½ù”ZMo­õ‡éᑲ¾Mo‡¼Òá(ûº~œÞÒœr®µ½SϧR+¯C7nÉxf¾Ý0ïöFù÷Ǭv—\ËtzÇÃÌAyô–Ø1"ãb^[ºÎ¾·W®¢ù¦tX›ÚÙ’i7Í«óû‡L%—ºš=¨Xoã1½õQ|Lo SD~-:.ÆöÍ…(«øò|6Î7¯Íl;ݾÙhfßF¹7óNöãe9_—?Ó[»+×´2åaîéõ©GþZþÌ4ÂÙ¤õ¾”)'#]:‡›jc¼µßc‰z¡®ŸÇroÚóæV&ª.'²§k/‰tn岘sÆ«;W‡‘·Íz½úµ…æñY–Ö«æ µô²Ñz:ldÛÑÝ•ä ñ0Η—?aþñôæ᛺56¯Ó™n}¥“Ø>Ù\7;Û-Ó\6ÕÌ ~ '>6d·ÇzþpxEÀf®8æµ*56Z¹õ*Ù_ùd;–\s²m³Ô¡+¸=Ž¦s[‘ëš­ɾÜ‘3×{Jl]5î7”Zä»ÝéFÉ‚vŒÕlɽqmœwN;Ù¸‘d¨yÕ8–äÇÈI~½ºµ\ 'î0Šž°¬²•j‹þØ)n±¿¶® G´zn­ðB;Sn•‚º7Rbg§°¦äw_·Y?×Û[›÷Ó'ÜIw¤¿³¬ÎF!•²‡î½ È±í ¨ähX¦‡³ùguÞîjÆñ^ÏTòï‰|sýè³P­®,eÚåùÖ~þy'Sy«2¥¥úI¦¢¨d÷3æãm„´iÜn^vÇ.ˆ(ÖúÐôéÃëÌj§û|Ãn{ùf¹Ð@x’n«ñÄNdóšîô¼-<ˉ«¬vsXLo—ÚÆÉõ.Ù¼ÕµD¶g?Aé_¸w'º±ÐÕBt3±=Žóm9+êäŸ=©EûÙ1j½ôfe´˜©ŽÆ“  ì¤w¾ñ7ƒ0”UûLµ2A8/l§[îÇsoÆÅ]¡&m­æÁ`MrJÛ›îD(8\`ïežeĶ­äKNêQ*ðnVéÙ§j•?;G™³§ìI1×<´$ù¨Ö(æ[$žSö`/Û6¶¯½¾Ññö]v¿ÙÌa!Jfáœå÷>¢ê| (Œ*57®{“³ Ö«“¿ÖF{ýÅšm'6µÓD6ˆB“k [PËä}I‰Gö½Um:Ër’/â€^'‰íûC>èë¡c*ù=^Ý=7:™òËa~½µ+Òš+ûF¦rÒ{M_UªÅ‚u~³ÝJH…ýÂZïT).7dÏÇv¡þñ¹ÌÀÒ®R¼|¿rLÈãöJæ¢Ý_ëW÷ ù[ÑÌ‹·ÕTæè¶Ü]ˆ +$œNWÌå³ÜñgEö±ŒÕå|3³òäãÅâs<ü†Ë 4·ë#¥×_ÙîÃ÷§Z¯&º×+¬UêÛŠç×ä`ß,%v¶ ¿fÊkén®Õ:oÀÒÂP¥’?:ÚO· +z¿ÂZ^W»´zå$Ͷ¥ìÕ½w>Mk·µª¥¯ÉÔﺗÒQ¬S ÿ4²Ò‘ÜÈпo·mò×Í)«¦Å2ù5K¨¥×†Õ¬²†G±×oøNþŠÛ%lÃ[ó°ŒþëI½Ý ò×{žVÊ0ŠÜ/ˆÓƒJb?8J'ìB˜¼Ûâð£‘g³¡S‚É‘^pœ +.{„Ÿñl™–±ÎpU8 +´‚ˆMyrÐã-„ Œ‚=Æîw…Ö‡£³-Rér—üsŸAI+?¥9·ÎPxÞ­œò^ž2îZ|5gkÇF¶×ân„ÞãÀ"eÕïw8œ|à õ¼Ÿ_s`dßp-î^ú×ò•-ÁпøªØWñ'eb| -îÂÇ⪂Ÿ ªFÀxpLqž!§0‘YJÅòÅsï”öÛD³ðä¿ô/LÔþpŽrdADñåyÇÎïÝšM¢ŽIÙÓN‚Ýž‡ã%…âXûržo^&ö$§VÌ+ñFž@ÔTåµXΦ„ëikùcÖ¹2‹O«‘b¡;~Q¢vÇb»¿µ¶´~o™õãÃìy)MÖrÛ9e +þø6š9?-_óñÿà!{oY2?Ƶ’t|Të{:rÐæ Ü}}ɽ=„7;{ÏϳSdSGÙ“íKMïÌn€¥å)û¡l‡½®!@ÚhtÚARù }çY;ˆÆ7eì?{dàbll~tû©óÄîUõ“P1Ó›G·‹D}| ûû>Ë”÷ïd_¶Jú2lãàlÇÆŠ(×Ä„Ží¾ ǸÉö9!ªd”§*é¢2,<÷åZzór%%ɽ½¸Ù—Þ3•Ëê-`£-Ò*™Í~Ò¥ÎAÆBÔn{ÍY#»5+üôŠ}ñôª¢Eqþù•#³œ¯iNù›§W˜œÞW‚ÉÅx©½YÌëwJbçöqu²Ç/hùB·ÇÂéÍ?¿!<ɾì‡ã|ꢑLoœ¾¸,ªb,»9f$È—ìöZùõ›Í4‡çŠ³þæ<½<‡lr{ùmFA¯·u`óJ¶s¶C¸˜Oð8Õ²gcõŽ€úbXH¬Mcyx¨d,E>­ôÆõgk³¹_{MoÜÅ®(Ŧº»][ˆ"ÇÞZ»5ö±ow³šRŒèé£:Ù¿»Í¼Ó?;Fqˆk(SòG•“Eoxóª}\!Œnw©ð,uóÍJ¾¹-¬õNÆ=¹/ÔKíx@\b–É×è!¨ö¢9\?½ÊTŽÓp íäää·béAÒgJë­'«Ivüz•ì‹ ÞPv´½N0æx•(—ÅçÍ·õà ÊÎg‡lòk8ý¹[w¼°WãŒU뿦äý«5VF¶˜Èc•¥Ïd1Ö³ãOríø@ñºpù+Lù4™mw¤&Á—;cgãì6½Ù·åÉ¥±z°/¤¦i÷VgVÂ*Fís²Ê]2/Ùw+Åœc|d*ùªœm[KRÖ>[é³ó6n˜@a&°ï'¶ë7÷äÐäO2•ôç$nD†¹ÖöÉ0½QŒV]ãf/wš»Úu¥Cƒñs«U¿ÏÔ/JYíZMlüS$L˜‡t^ŒîÛûÅ‚Ù&¦¢›GK™fþ5ò¨™ýTŽˆC­U¯+”ú ³¦Ó¤»ßŠ/EŸ»· +µõ»¾_ò¦3³*ËKåâsd¹LÖ—x(<½êhƒœ„çÞ¨PˮƢ' õ>S©,‡}Ȳ{àv{̬T -Ž)æ?f È~¤˜Ë<1}pCèX~­¢»6C:[û#S¨dLûâÕÖ^²²È¬4³ +0‚*¨Jñdw­âŠgÛ*Ñ’v?Ò[;ýkÂ+Û]iyJNöcet˜Õn/÷·´•/­”‰y¾^b©åãà”¨…žxîŒt¸á!Ø:Éru5BT„;ÛëbC¶Â§Åømi”9º• ÛG§|£c¯‰wdf1BåÏ>^=úK‰pïåMNª{{A nâ´÷žoÚ‡í|mµµ_[ÅL»»ˆæо݇«¼ÓÜúCþèhkŠjÊy9Y,ÈÍXq#s±È7ô:F(eÝØ.fv²F´¯çªíèýi—â6@KSù£ƒÅ¡–þ´µ6.ÝÓÒè™ìy·V ¯®ÉD…½„6¶BNŽúBŽá®%t«®ÆâT¹4+æ¿»JGý·o ›ÅØA'c¿=œFŠùÒI¹ík„¸\;¯i5{§yg‘‰Aä/OÀÂ;¼ŒYŽä2äÿûé­d*>u¨´¼ÆâR© Nž±ÈGF‹e/ ±a퉖4\»Ã 7y;s”òqV‰7+S»W 2ezog«¢æ4ï¤"Úso.â»Æ÷{¼€½RòÕòÞÃ^tïÑ"øò1*Ž‹d÷ÓK(Šà%ÈÉFú$Ÿ:{Û°vÇXëWãkƒ´(Q%Òm¼lÜtìú»‚3/Å•Š&ˆ]¥¸¹¶è)ð8&²í~£YŒíÙâ&#®ž?DàC;S®õ›T[TŒÅ×lgÑz$¥ô~ÿ“ŸJ¿à(×êVÕÜ“MÜ_ý$"÷Á)çÈH>ß+7ÅåqßÉjã,Ñtˆ¦-èäX¥³MÚÙ÷÷R:Ð8åc±"\ƒÐ{£L»ÚAѨ0Z>ó‘ÇDŸ^§I°LŒD€wk™óÕgÂ!ëY$LJã‘tp¿·8l-D@©W6NŸjãâË“ÒSÝËô;•¶¬@þáœS8D¥{Ânbùær?bŒŽFÛÅçvjÝ7Jëií•ìÆpÕÜxJ”á>(Ü5ÞU‰@ U3ÚÊJÏ–¯Ö3ÖÞpnD ;¨Þ¤‡ñ›ÅüÑáãgÞÙzZô6 eþ°èfŸ‡uc£v¶üÕÖÐÖ&ÊÕþ~ñyô˜,¦WœŠÿ2ÎÀƒB) W J„5§“oomoïΪ<j•ÌuþU_n¨—K¸8Ír¹4¥SlK +¸ Q¼‚̽Ýo}3¥tãd8îùMƒ†ñ)Ûùz®ú‘_m­×Í-µàîš×<‰UÊäÄ—‹À›ª+™Z[È’-„Xéά¼ì¯óã ^Kž‰c%™/v3'„VâÙÍ­$P=¾WWžã¥†¬ÓóŬ/®:ÖØÈ¿ +Ô’}¯D”ì.3½ãcç3þ!ªÌãìÊÖqaí1Kô}CéxÆYá½-±ÏÕÃ|íƹ §mgïö¹ÿ‚4Ò=>3 ïn³nž,FŒÁùþ.}v ‰>úÌ┸uT9Ÿ¶ŒËRcœÔ˵bTŽ½ùüb-nJâ +hå¾l\}63ÈŠÑcs#з[™àXqùîµ›oöh:šj¿f¤>ku¸²7NõŸ2GwqËÛ}pX)ÔÇ‹OÖÙ¾Z±ÕµÃÍâóÚÕF`-n?•B/À¶ýý¼ä^ߊ+pëmù¦ìvÐ7ö2½Ïá:xwœå[·ñ¶Ú#‡Õ«oý>5úœÈ¿'Å•½hq¿ãªEDAº^Jå]¹=Z*Ôkk ð"¸VvYigq6þäµ-Ÿ¥ûÑLo>%Ç™JÎ|&ZgƒhØëÉj¦—qŠÞäh/j,³™Þ|¼ÌšÝƒŠœ|}Tˆ¾_n§×yg„^”äüµ¶È‚}¢h¡¸¼wÜ÷Ü‘RáeûÊ0[¯F%y{§ÂÉìú[·Q]²_¯d®ûÄ#+çZáûr^«õϼ´”ì"{×ùU™èE"î_ÖöÀr•ÕÿÛqýàð& õÁ­nÊcŒµ€gc¹ú“s2nZý¶“ñû¥B® ÑçRìË sò ·½Ø½Û'ôÇ_ØÅ*oÎÐ UNhôæ„Ø+²ÐÐAlùaèç7§V‚Ϊ]1¡PÞ9„ªC(¦Ù†Jù¢º)ºb—ä_g¿öÆ¡~›4êuCN£_phÚÝ+¤IjùZ ‘Áܦ]‡@`Ôƒ.êN¨…>ØÕP»ú+„‰ªöûíV†AŽëo0½ƒn_@yÝÐѺÕvhLf×kz÷†¡q÷ ¤æƒ6Ž,÷™t]´ú£‰FÓ*—GU‚ƒƒÆÜšvËžPÎv¿oL¨Y´:˜U`n§X•Ç["è8·_…öK`)L`òïV‡óÕÚ•¹¯4|U/|Oêf®­T}uæ>Tá5/œ×/‹Òkuç¼ØåU‹ÕáèÚ©Aø—o÷=0¯_ }’ÿZÆ?]ï-æœÍ¥aÃÈgrPYh- ¥9<¿ +¾Æ3çuÒëöêoƒ^ÇÉ“³BÃoOø8ÏÛ.öÌY8åJ +rÕ> kØræE#øz؈o<5à a)t~=ýêÛ`ŒZôy¥×vÖ3®à©½v¾ˆÞôá24ñW)ºñ-Ô©õXÊŒ¾ûiæ{ƒê¯µº_{ùìGCÈ:&¼‚œ·&Oˆç 8Ÿì@L›å¼"ìªx*æX9•·q§Ö­¶Ús^bñaÄ­­øŸ@Φ3t£*BLïàÀÍ +òÜãí…TÁˆ*oüm¾¸ð[h$l+‹öu*’©zµêè¸ú«3˜O×Ú )ÞA·NˆO`ƒµ/½ßƒƒßŒ—6o…Óè¬0âÔÀ*3ðþ‹¢¸Msø†: ?v´„Ó´š-þLlê³H—?âÃ÷"Y5‘T~î’÷ǃžÄ9Â÷O©þ}SÄ‘ùÐ÷“Æ ðeS›¶så Sëýä|›B²û#$rÖ œ?Wÿ:ƒSý™˜#J› `8saô;v5û­ˆ(3[zòÖŒPM_ŽÒú»Ö„è6—uÌ‹j9í°¨0‚©ý—|¡g(窓Á÷uvðUm¿Úu¼·¤TGž´øÖûy¿Õ¾é›¨ÖucIxZ'¼¥ïQE:øäVÔ±'჆ÇÆkß~㇠c»t˜}ßîì¨ùõ³}{³X¶³åæIäXÝ< ‡×RxYÛ¿Hoœ¥Gv«xòð›uv;~CÃÅo¿ýÇ_òáðêÊb8;\\ˆZúqáì7ï?d”°’œÈä¥ü¬dÌÇ×6ù#zNþY´ö²igç³üßõôÕãgœü±²‚? /õƒCø¹‰?³ªµ¹ ?ÏhåÊ]}~ÖñgÆ:WûäØ"ô½•±JïMòW|~V¢{+©Ìù{*-^ìE?NUò×Ú)´.äZÃÊ:üĉêùÔErHþHÊøµ˜sŸðó~& /;Ÿ1ø£Jþ±õ¬òVàO\‹Mffèï{1×|Üþ͸3úo…ZEZ,nåÇ™”z¼»ªfד¥Lås-²« +uì±P[Ú¸Ç ÕüÑX}B!0U°úà'2‘ð›’?PO”üÒý†ôr%_üöÛriž° uv|?¢ð/n1lÞe“mÞêÇ-¾. h"‹çÄ3’Ö¤ø,så艴xÝŠسgòåà›­‰Ø’=/ïZêÃiJÞËÞÞ]d.»ËÙÂzDƒùÄR¯ŸO‰üÎÇva¹w³º—}0^²µôçm++ ’×ëÁ3èWX~cù\'^ÇÅ­âÁ™aÕáAãþF¢pr,ƒCˆ™(O ui\ÿí·õãpbg40¹ípƒ=CymŒ¢fça'·~fá®YÌâíÊúQ3¡™{ù20›“̓áþY¡&­3G·‘&ÙÉ÷D6iŸßÃKƸ5O·Õl[Ö|¯íG8‹ç‡däã^¦<¿%²¥¹—‘IxÐXãvø5Ÿ=9Ù n)ÝË߇ Gâr2Y¯w‹ý÷p8­œäÂáÝÃÌs‘íU|õq1?½ù ¯nØ«ˆã¸¯¤;±±Gø)Æz–ô›ql'ÒÃø«Ÿ’€ÛáåðâòãF8²–? /)‰Z8j’£»¼}¦Ü_Ùö±Ýö}8^(÷«G›©pâ|1^»zº +' +ïáT-¯¿¶¶ÃR§t–‡¦V#£¥°:¾4ÂúòÖaØX‹<…Mùy¶Ò'jØ.jûáËðSx³^';Þ^¾6Ã;;‡gáÝ«í×pº+'ÂÙÕ|8wy–Kábé3ÞöÂgýHø(9΄«Ñçðén".-áóZ¦.œháKéæ2|5~_ ßôÖ +á»çí72ÊÃÍ¥~:o?„_*úZ¸vuQ7núKáfcç8üÖ«ŽÂËÒ~¸³qûî¥öŸõëÏðhMÙ_ TÇ‹‘~ödq9Y^Œ½Ý\.&r»Òbr}Y”Nª›‹ª|ÒYˆ.êÖéJlqs£û°¸³RÝZLn?soåÅâÓ‘¶xðxÐ^<~Ø?[¬dzÚÊÁY¹±RzØÙ]¹nÅú+Oã·“•¦q›Xé<¯Œ>Ò»„þ­ìl‡c©¦}3lc#¶ýnb…âæMì„PÅX¥[^Ž=¿>&«÷œØÜ_¬%ò/R3qº–o'®On‰êâ0šx?±R‰Ñâ…¹]‹=G²kJñàtm+Ù»_+ ‹­µ³ú0²vûXR×ê/ja­Sû¸N.v+Édt'‘47’™dæôó:yÔnö’Wúƒš|y¸>I~¤.Þ’ã×;)•ÈÞŸ¤ŒåZ;•~ë“QRGW±ëÔÕñÖRªzt¹ŸjvÛ닇êÖzò⢶n5"úzn¼ÿ´~¶1R×ï.ŸÖ "ê럥§š‹e¶$õ2ò!íêÕ¢t8*E¤«×üµT;Wu©KÆ•£±ã…¨,ŸÆ×åírüM>¨J'òeÏTäšœëÉÝýÒ•ýlee{%¡ìô +må(÷t«Ü¬¦ö"›ÊçqdEçË]ÕÐâ/jN¹TKföP}´—Òêûñ«¥-^ŸI Qm}”KhÛ[æ²vx›XÔnV—Fšs»4Ô†êÒPO|¬ŽtûÚ\Ô÷r¹¨~¹~×ëÉWIÿ\[5ÕÕì®aÉ/ûF1»^6*åg£ÖŽµOûvÙ\}Ð ÓŽ7Š„Ÿí=ݘW;ʇÙôVÍaxËÉÙµµµ¿ß·ŽrYͺÛÙ<µZGéw;r–VlÅ9*ÙÙ嫾}^xÛ²_œ%rÞ­­µÕÖíùÆFq1LhÖîÞÆ­óÖßhU¶ó ÑÍ¥£voSËí7óù•áf%ÿx¼Ù¸>\ÙU¥ë-)Ñ·Ò¹÷ÖVé­~°Uµâ[ŸÍ»—íäîmn{gµß>­¾Õ·Ÿ¯–Ž¶û‡ºº³¶w4ØÙÎ×wNVvžïNÈîïô[ýåÝd2÷±»s:xÜ=ë—v«Åµüî _µÓëg{ë錩®¤Ë±h8Ýè|ô3á§ÇŒZ½oe +Û×Ìõëu3ó¾R}Ë®l}¼gíóq'{Ô—Ù§ì^$ÛÔ Ñ\êtUÍe”ã\y4:Ì5k{Wù¥›q#ožó‡g©Tþñú-“ï¿œ” ëË;­BÖZ.™Bkµs[\¹~7×ßìâ©óqY¬†{áøöžž´žöâ§d”½Çhýdï3ìKöQn?_|ìß¼vö;«í׃µÊÑÎA&?¸¼¯çÞí½Áa|¬œî8‘Äa¹òöxøvåìÅÎî‡GÛwOWGD‘³ ½lŽcÖàöxûYÉ.DË+™Õã·«û÷“¸¾:Ùies'•Ë–ròžÝ Ÿ&Ì×·ÓŒQ¸?½Ö#§§­‡üYꤸy–’”³ûÅñÚÙgác¥¤¼U#¥ƒÝ‡qéyô4:?6ÇçV±·x~fÇ¢ Ñófˆ_ÄÖKÒÅ®ún^\Yë»ÓËÃòº³|YÞ[¿¨•ŸÎâýJxñ!Q±*»[•ÒFì¬ò:jÖ.WëW‘ËìýÑÆå]9}~9¨l~\éOÛòÕÉÛÎÉUs¹ð~Ï–µ…èuæµyy}gE¯ͱÿø~s–ˆmÞ¼~œÔo/Kæmþñ²vûx¯Xwáj«y·ñv¼{WYÛèÝu +ñã{¹9\½?R_ŸîOµ‡øv=ü‹¶îjýÇðcjy!ú¸yºQ{¼§./ Ï1Ù|®ß¼%^âÚÁø%ו?^žîGõêÒAí¡º›¾º®Þ¥Kåêø t^Û¼(•jW«Rm°Z»¨[§£«…h½<”ïë½£ƒjÃX{}oœ×¥A£suw´CIwJÛwY§Û¼øÿ½}çz*½Îèp! B›^RHé½÷ÂJÖJ‡ä|gÿ9×~${˜ñÌØS€ýí÷ÙëI@±lYV³dýQ;ßvOÏ~ÿ¼þYVŸUE]Þ=œ¹}~íf_Ôýù—½ÚûÉËÛk¯÷W»*ÎÿÝ?ž»üû±³Öç?cïqýßáUéýß×óúâ«=óöôzÒ\1_ûÿ¾îßÖö­·ó_¹û>yö^_^8|¿QZù‚ÿXËËW5iñ³lV>7:êéçËC}áK‘v§¾öoï¾>KÙLîÛº«ëߧë÷“½¬Zíö–«×'½ÛY³Ý/æþؾ;hïõ+`.¢UìOÏ\1‡@¹M¦¸wÖ+oo‡]ôó—óáËèy·çº¿ÿ8h_r|™fð]†ØO¸~u©®ß™¥¥“‡šT+mWJK/?*þ¤h ûsªûžûùb^]:úYmþ±Û¯©ƒÅûæé¼î~«”Œ—lQí,f+µüX¦¥úëXñv¶üò¾ºûVöïÜa¶¼}ÖÌV¤mEª-žz=Û(îk}¥¿ “k¾jK»wuuÕR-ðFß/È{¤hÑûVêÜv™\ÄïÃJùkgcyÓî×­ÎÂiµõy¡¬õ®.¤æEëü¨µ¸²ø(Ï®˜uz¿Ô¨Ì¾-C´\º ð~íiÌ, mõ{½ùþ )¯K5í.ÛYßjýœ(7Ÿ`{?ÍÈó®7lïZê/ÀØÖo©ÞÎÎ…“}i¾Vk}«­Yßóÿà×öüõyÓôªw½}µÏGÚ6oô¹õ›*éÍôX ´Rãïf;ÇGº-ôúr®ÇGº'_i“Êܬ‡¬m?_Þ®ê/…ûüù©V¼*-(Ïü•‚ϲvêøëm‹·V©µº¹,@jLMôŠ‹¤ç7RëÏΡ‡”¼µä¢mgfäÛ#.ÒvG9’W¹¨Ý¤À‹kþ==N¾ú­o!ÚbxW çêå›\¤Úgˆ•¶¤Ué^>@ªëï·_R“)ÚÛÞõËÇ‘éò½a¶¦e.Ò›¥»ÒN&7­æ¯øk]È^÷§^º|¤ûbý{ú}‹‡´4ÿ6W÷¾øYivéby—T;?—Z¶´ÃE:Ùz6§Þ•]ÒLNj]_·k5¦r¯Û Ò{©]¾;á#mK+ùnÁ<'H3¹ ¦ÊKÒóJ!@à¥mÑ!ïÚÕkˇôbAÚ2+2" Íäú“î§qp_Q­ùäß­Ë‹{RcÊ|}j݈6¥íÙk› %yØþµ®Ï-ÿëíp‘ÎçU!ÒÍî¡*ñ¢ä×.+Òámq’·Öþäf§»syQ,p‘žä?ž…H_öïRð±Bk½lK'›_ |¤[j¼°ÈGúµ1ÉC +2ўܶ'¾Ü•N—6›|¤ÛõµÛ›ýëk.ÒëÝ×u‚õKx­ÿÎõ§–é•%]÷ß*|¤;ÿ¾ÞwmK ,íízåCHà^î°” =?–Öþ¾op‘Z;•Éìòuä ûšß[óÖAú ‡¦x±u?K*ùz¡ã_é–t7;·‚HËRÀ‚haØß¡¿Ø "íÎ,:H–J•f¯î/‹éê¹¼î„å^ÿ¸ž,€¶–Jë²V@Úø ‰ÂΤA‘.É›•€ ,i;TÓ¨ÓsM‚±8h§{½û‡D*öz+ÝÏÿndHûú¿9GÓ,ÍíWäüìÞeû¡{kø'Õ9ž¹ùœ~{¾/¿Ÿˆ¾}‘:w¹_ïÛäÑÛœü5ìÁL©1˜×¿®øÖ°äÛ#çÛþ«:•ÆÇËäï{*÷Ö箄ßZÊìÁøÛ—û›¼K±ð÷+ÊÌÅ£ðÛ­ÒÇ’"þöa÷ϼ÷m€bÆÔîäãCGð×Vg¦³pܧßþÉÛ¿=.ý§äɹÅŽ6?xßS)×,½ö„ßžæjYñ·W͹ÅÅ8ßßånÌiá·ÿ~ö¾:Âo_O”Õ}ïÛÅÞ_[õÑ_Ôöçuá·ŠV?S,÷øñp¸%úë™ìÌúÕ¬ðÛµ•í‡®ðÛ eiRSl%«Lçßêimqv°æ¹Â|àÛÒÑ~Éù¶Q]žÊÎÑ]{vÅû^ëkå¿Öžò— GþÜåÞðÛ/ÇmÝêTô¬~öwèO>9¦üL£‡ÙÈV6ìsð0ÿá?Uü¬•-7øÏ)ñß\ïÊßýÁê@òõ&•üâ^Å‘çàçøì±ú”š‡?Üx'g=æDÔ¶­<ø±g¿ Z'gߟßTíïâC¤ÓäZï÷®Zñ‰ÛÞd&ç¡%žŽ©1…~Î%©v~!D +J䟰ÇصOGˆUÞ£é‹T;™Akí¬2HŸff¦<¤Äúw‘ªò¢õï®´ýæCZ<'Þ+‹ÖGàEEˆ”Xÿ¤à‚õã!…µøÖz-D +îkb¤hý ‘frhÿ¿ð׺­F!ÝÊ ‘›ÂCŠg߇mŠ#ß®vízò“³ÅÆÅÓ{¸›ß÷öì ©ï›µ³ÝX8ýÅá;GZ¬Âš¯Vi:ºw…w¤Î ©·~{Žp™9\œøÚß©¹²÷Oý«ðtä8ë(“¨¿_ÿ*.|¹#Èu»3ýÌ¢n·/› ¸î׳ê‰&}}-çüSÞþtx€Àµ-a>°¸Å/ +ˆ>Ø[­½t›9÷ŸÖbvbf».p±´ÙåÞm,91%˜òñ|0ƒíïì€:ÔnwÄ1³‚½?݈LXÚÐs9ò2ä©ßKâMêÕ^LŠÒíoXw»¬ä—$&Ž’ƒ´¼þ]ÝŒ#:þãLž:óœõå³ÂõerÞ +É?ÂìßádüþM¹ë›uxŒ·B4^#ˆ•|ÿ> RÏ"!±"“gWÿµ„Ce’°û€Xó•Tœåñ•O#SÎj_}§§<—î`Ü\8œ<2å•Õ {#šî1±ü¢§=7k~Ñ£ +EO&Žu×®Ö{,Ý)û8CEŸv7khÃ8̧¼V¦ÿ8´“?ø´;¯d…b[t*I<Ž»´óÉôKcµYœºtr´GêöæŒs›ÀH³X«Å˜Uu÷ª3Îî‡Äè`9ùLµ ž„Þw­{ Y>¿+ÎÀäΚ‚m*É÷Óù™óII¬¥_>K‹Îa8™uøƒÉIã EèÎÜò̆ÏÝ}+鹋>u +ï'Ï~#÷rJ.Ëeüç²Ä\R„Ø$cam“·@1vCñØP/æ XŸ+ü±»ïN‰™YËO›;³zû¡/Zä²Ôý9®…¬°ð–xßv)¼%Oí8+,©èyµú™œP1%°=`°f.„j)“K³¿m¤I'½õÄ;ûOm¿œækûÓS¼SúYÚêÊÝg>IL:˜’Ø l]&—lÁË4,âl~ß~ä" ‹û×Þ{÷†¢œ<Â`‡ æ§2Ñ`~…’v^ƒ^g°óh'&Í"Y«ndŠÝöÆH1¿DK9¯/æÎì…ןzHëØwÿóéðœA‘AËUÐÊêÙK6FŽ16ÿTþ,MŽíT®ž}O§s”é}lh';xe²åÑ=ñôÔ9ŸœIê|¸¶e:õ¾«–cÃ|ð +Üó¿Mõ~´‹˜ˆÃÉá©ÄHD Ø–ÃÑ$æØ3¡>rI ¢ž;º¾Lá“ÓKI’‘›Û£\÷ªL@~ZGž_K@ÔK0aÃpÅ‡à€¬G›™\šIM )üÒÜõO.Z-'ëžHêï‹×—O@òL2¢ûl…øxFÐ’÷b}ÿÖñRïtLÕü‘×®ûmO2ørḎÀ'ÞQ“ò§scE¬äG\èö/ ˆ•òˆûˆåqòê´ýZóð \‹ÿˆs½îøèpíd²IT,a#’ Î&*$ˆ¼oø-xS’…¥1ÉÙýÔa’ ÌKØNÞp½¤àT6~ýjyˆððÁ·BD2UB»•|¯:U÷É䢢tïx¹{>RÜÉë ˆA¼©c$âdFnú" _>Szù7hJˆƒ‰ŽLN`L#•7ƒ¦tÚQ{ŒÌ'=Ÿ¤ñuéØÌ8‚ñÅR)ÂÍ€%<Üy™Û/F/-žÏ7=åçê—!¢Ñ_›A½'ZP&ÁéÀ;~¥7ŒX:Î陘=OF˜Û˜ }& êD¶,P' èâôkËÒµ0ª/«òlþ® +>K ÂSÇpræSMó¯ùì§íü·¢‰¯ a0Q¼•m¥ž…(ÞŠƒ¥8|| M&7ò%Ùµh•—I<Žß‘L7çNœŽ3ê ¤£¸Þ¤7Ê@&§'½Þsý}Î`¾ÂtJÔ?˜'ÖE¹à·8$Mu,7 ’|šÉÅÃêSª©ïøFærÎ&"%c~Î}û´XP¢õ_‹!‰Ö ^à%hÜI³ü=ªD;ö$ÚÀ{McÁ‹—h™„7H8ØðÍ=•í“Ï1H4Ø5žDK}öaœô-» ãŒ.Ñp”Ñï^É8·AåÕ‚{‹SËäxÖ€O |FyÐìÕ!ßFÑÊþ|KðyB±qülÔ Ø~ù“ÿ‹¡„–YÎùîï0Ó|:1 ƒdS ÙLN(f‡Ì~ðæâKû<ña ص@R“HÂÄ“Ì´ŽBâ00Îè)dC¸yÇéž0¼Ì¹M ƒ#VMu!¬¯„Ö…_§•Qì{†Iï'ú#{|g§I£YLìB¨ OSÙ÷Ñ1Xltûþꛧ Ój1ܵô!­°ÃqF±ï™Q\]8¼#ãpì{Þ(®÷*'©6ŒÖ…DÂ\}¬ }ºð6xOL°À§cIGBùÃÍÃrt¥/yÊKÃÚ·½ˆ-aIYNpûvv6J¬:ç‡ÁF=Ý̼8g;:×:‚b‰ÌÝÁN"‘G{YMtÎ#nO@Ï“ZDä*A:]`JBŽ ¹=±Ç+¨˜ÕJH-5ªµD~eŤ¬ž= ÓóÄ™kf¦$#e‚ÃõÐ_¶- &¼»q£=Iêa0uz~sk ˜™ÎÄä âNÞVÅ`é`‚Ã~™|K9"3ˆß'šTBŽ.G#Ó£ë鳺¾ýÁõ,œ= Õrýçlåcÿ_8\ËVVÔ¬›[ãUÐÑ—[F¯¡cµX¸‚.P14t ]t‘Éc¨¡"%tÂjÁ”5tÑtlµà(5tÑt>«o„ºè +:_µà5tÑtÕ‚©jè¢+èHµàjè¢áœÚê‘kèB×WAçz#ÖÐEWÐ{,¾†Î—Qa¶—oñ¬mq¼¸(ì’`JÜHo3>q{6Y½T+à# Û Z¿œà@ÒHïeÓ¯íÓmsÇ× &l M§ùY»úkE]_HR Öý×=‰ ¢øïø¢‹ÎÃJ¶>ë‹«œK¼¾@ä*|^’½–~J‚ÊǘÈUÔ”x%s¸–TEs‰dÍZ ÄËíIW@õ“*„æq“AÒ‡EAcò2ØÈñªóÊTôÒ2 ‹Ýü™iƒ„“aÏGÓb7ѵßKŠ+vû§ÑŒ´\‹Lñ¹¦qnÈZ šÁW)œl0 +–¾±yP¾ã*“ɬûV*§—œJ‘Ûû ŠS(S•˜’Ú·í„U‡ñ¾=‹w—íÚcáÐY\àðÕú +„Iào¹I³CÜXµ…f—° +Œ±”‚u`±fWâ:¾ß¸øu|qõ/S¾ •¸4M\mJ‡sàó‹/S¢I-óJ4Åû›Ÿ8‹à©í`r÷/iŸÕ‹«‰‰bv:6¶ +Ñžwó!ªLI3X<Ç'§˜?À:"ÅbËD“/Ò“hCQÌøUVÏÎ+þD§N ±žSÉ•Ì– Å|Ãn–{ÔøqËèz¹À‚^'ý™3Är#ã?w‘§»“ÀÙs=¾g﹓ØÙ ÌQbˆ¸§Q`ŸÝW5’•Ê µC·R¼~˜4üc%nñA†LdH«ÒN\ˆí£jä21eÎ ®<É‚¢Ÿ<É„Ü«;r®Jå/4œ¹qò¿õtᎫD-¥˜¹ÔWDýpµùöØzÒïw¡=öo=]xG\fÌ šN/1‘†LÀ•³@ʈŒoJ¾zd˜”’&ü1%ÿaŽ×btÒ’N):;=¾(.jJÁ·;ªø/-IGDfƒDdFÌRKx:ADƹã‹)Ï™ž«ÎŒ‘aüýÑ#20@!"»#EÚ0™Pµàè,C DdD©qõyZªˆ 7ο‘ <'Yq–èÁ‹çädé{تCòË›ž±,ÎOb,«KÇ¥©Ì]“øµ9žLÜ—¹ýüx‚:°´¹BLNo’2´!ØYý‚dâèRŠò±Pê·Z0®®.uÒl8C•ÔÕ%Ì׋¬«cJG#*…ム›‘T1§—±-#SõöÂo‘âg Ý æ]”˜4žz8Ä2Žœºèz¸ôï SÇË‚Þ{w=Üu)êá¢2TÇW‡QëO`‚z8΋ Â"®áëá|ÙPƒ¿É».øî(­ˆw=\ì+c©‡óÝX%Hµ®.苉®u°’môÊzTpcˉ„¡ü9‘"Û2INäÉg¢œÈ¸³ßUG5$Abôjt2N9n÷Ž“¤ŽD8 +µÇÈ8c¨ä +e2DäÀÇK4,®ÄÂùÎL/AÆsþ{6”ñœÿŽÙˆÀ!F{ÎNF9†^ñUõÂ`‘ÏÊðä˜ðž%=†Ö8Ò;¿Èe´ñ¼LÆIw|õÈ0ÎX +!›µdZ,vñ“À‚×´DiÖá'c_CóG¥¯5ËáŸE?ìëP»©òä° "ÌšE U‘z?y› ˆ4aEêýäC’AL©ßX*R¯¾ÇS‘ŠãŒ£"ëÅF¯HÅQÆQ‘Šã${:àóòúÈ?šš:ɨXqvß ÏF>†R8¾w)ÉçÌèG)w_Æ^ +7|ÜÒO±hß=…_9B)û)†û¯”Âq¢ +ÿ…R8~|,¡ve²¢€}?9ªðéE˜«šäEx¿=֨ƸEI½p(=QD1A¿l—Ò† =2œör©F?Ox…õ:/ˆržÊó°Ä7ëꮄÏá•Ã :ÊQÞùÅ®qe>gH‹ËúîKûíäþ¶9õô»Ö²§—oZGõ½µåÚO$ëhÉ<'mÛk½å•Žq´ÑX­>6«µMlBpø5PG¹7ÿ”ˆ“¿‹Wu†^’S£t)®;³öçöXöò»Í?în³Ág©þ2›[È~Š*ì´ó³¨b·§š©Ô:Zª°Ëíþݾ!½‹@Ú®Ø Ò`-Ö\©ÏDá‚ÅnÚÙË–Û™0P6Yˆ*v“«¤þ~|óï¢ +;cjêÛø½»]FU½GWØ} ‘Îlu_žDH»ÑýøöÏÄH×v.ÛBòN¨ó·"¤ûþ +;²«p`U“Ÿ>ŸK7σ£Q¤vµ•hD­¸GáÕ¹ep Ñ—Toý¾jTì8AÊ­ß‚-}еøîˆöf>ÇSÚŸ¡ŒšˆûX}'˜s/1©¸äÔ¸<꽎³“¯íçM•Ñ¥¨Nr©¢p—ÍÄ)’1ÕäMÈy}qMä"óú’óSL9áúx}ßâš$]_|¯ÄDë3xKm„þqÉÏK»+nD$LwMÚ€.:Ò;L5Ýpq˜´ÕtŠ«éx±x~¦ý(Õt>²8g–ûþØHÕt¼¡b^¢šnHœ²šŽwOãêʱUÓñjéX<Õt¼Zº„¹=)ªéx±öAöñUÓñv—DzÇZMÇ3nØÜÑñTÓñjé¯ÍPMžÒË”ØR¶šŽgœfr㮦ãí'jÄjºàP±=…‡ª¦Ù–ã­¦KN±QªéCïÄÇTM7ÅRWÓEÖX­šŽ_[=îj:Þ€eÌÕt¼Û’@üªéxâÁn£š.æfdLÕtñúeÕt}ÇvñµOQ/›áûÕ…¦ôƒ·<ãîW'¶.’Ñ駔OE'J¾LH¬ÉŒ5,²€¯@APõ,šT`JIEA‚îrþ‹©¡¦„ƒI¥j»5%~"…PÂDÐ)¢í²@d¢ó{DKòF?à©ÓöWLcóèÈœÛ-+ªÑÝÈmî˜w®šäô¹v—ÛLT«”°Í¨’+Y!]¢‰èüd¯ÑÝh{Õ>ÿJÒe&2J•¨Í]lD 3r›;dž‰nt7r›;ZûÓè.ÙåÑ×æÞ¹R—ŽåèML^g±9¶¥¯MÏ亖–¶XLÊŸÂ[lP›{ÇŸXF‡Îsd>Q!]’ŒæL|…¡8‰#q}!`‰H;L‘D†Ô†þüœœ¬Ã`žÊë‰ðÑTášyoC‰î{Ú{ïCvù²¡€©bòÁgCÁP±ùc‰³¡`°dÝщÍDö¥òqô«ì(ñ¸³û ÇIÔÛ•7 +“Ùu8ŽHF U²s+…ãkÙÉ`‰ +kE= ‚…µ‡áÂÚÃñ½ +ˆƒ¥‘1á1˜maÆê.!)x¤Œí÷•C=¾ÒÅJ¼pu¤'.Ê¡îö_ÇR™â¾|œÞ’µõ;®†ðÅžøŠ8ñÙ?NÕ¨Ztû†×F6'è(QY)ÆÃ+tœQ»UÓQ‡÷Âaº\Õã… ©nx‹%Î1Ù¤Š‡„v™Ø±ó“¯ogé€E꫇ËäúS'ç{l¨Ê߇m®qy.¨‡+Š‹ðz¿ws²_&Êð¤YQíŸ15m¯EExWB¤°–ÉÖ—*\«Ôžº8"ͯ?TŸE}ØŠR·’Ë#ðž®0Hý¥iý¼9ãõTUŽÅÂÒåi&‡öœó•áÝkÿXòn.ì S9m§yçÅ`ƒq·HÛÓ†i{ꂇ”ô}³:¾ÒÊ Òƒ¨>‚›'b¤kkÇ-_f ÆïËîONæïìbÍ¿û8µ! áØúJénvn%Áˆ¥¹ÏŸ5OMšϵ Ñé^Ö8Ñ¿òlDĉ]E—0mˆªÉm¯‹Y|âdÝ c/ ›b#$â’¤pj•pJÈÉâI5’”6Å6õÇÕ™…tc:³$$5£S«„[êÌÓ¤ EUÝ^ž¡*-&C“R)“ŒŸ—^i×çF­±î.u—EÁ”BYZÁó’˜è©²´2¹¨I‰ãÄé¦ÒRœ§åÌ'”WkWõYÿåõZ¸gÊp®Œ½žàéë˜ìÍÚ¸o<¯d“½p˜ )2¼œô–çfm”—h½šÄГqC»ñ¯¬S½k}³6ddÙÿŠ&VŽú-ï­8a½Xœ×~+.ÂâÇÇüI#œ+ÖûVª~b Ó›|ß +J˜ao½iÁè“«øöu¾™ãüñ!;ÒOUѾC ¯V/½="èý”_<_¦+$ õ³¡-[t%Sª+;Õ;Ì”õ/Oí¨Øq¬5ªïÜV3§6e)àët‚RNö«ˆRÀ¸–Æ1ûçݼ?µã u3C ¯1·ËLŠÁâºèäƒÕµƒ´Êh‹­åI¾HÿÍåˆ‹í§“Šb[Ú•`°P¹0Ï:t(6d`ÒÀÄïˆ +ÄÕFd§'¨LZŽ'§©LZȾp˜¾ +0i ‰Z]èc•3˜›® +0i Rlø*@žÑrÃ_c•¶ +0i  ë#U(˜R¨½¯¤ó ¯jô¦|É;²Ò”Y˱)_(ªð_iÊבm6ª€“2„“rHW,îê7úÛPãx*a_¿DoCÜ×YÚÞ†õõK—§4l_¿è®~C½ Åéëâ{âéûúñ™/òm¨!úúÅsò8úúEg†¸‘«ûúÅUr) ÙÕ/“KÐöõ‹^Z v1t_?ßDB]ý†èÂíë½ ^†ê0}ý¸åˆQ¯š''LâJYî[ÐCôõ‹~ù˽±¯_¤¢k-6†¾~ÑaáT/PEôõ‹vu¹YÐCôõã•IzGo´~|c¨yOÕ×/zŒó£¯_ô…J&ñ8£¾+R‡íëÇ15™®~lwi„¾~c(ù»úùÞìJ]ž"¹}ýbë^ÇÒ×/º«ŸËÉ#öõ‹N¸f*RGêëçÕoñx1ðÎÕÐ}ý¢ H,Ø1ôõ‹8kýWuLýøf£GIÜ/¦\â~|#õõsG៴ա¾~u±Ù¶Ý´}ý¢-y|ñ`}ýD:ü$|“8\ýVs~p“w ãúúEß7û³†ïëç§vÐ[¶_º$ q?¾1·«ßèýø’1ŽÞÎÕH׊/X‘:Z_?_`;ÔÕV Þ×Ï-•Šê+:r_¿h3GÀc©ûúEwõùÕ&§¯ßˆ±¾„}ýÔ½Ž¡¯_tW¿Ôýø†ªÂ HËp_¿á‹á™®~ –‘úú…CÉlW?Ñ+giûúE§w´ØÐ}ý¢1¥¡ûúqö…éêý.\ò¾~ÃÇ-ýK_ó$ð+Gèëç .^W¿¡³}ý"9‚é2Z_¿hÃz¯£÷õ‹.ˆu4òÈ}ýü‹ võ Ùc‰_²ò÷õAÔ†‰yÉ*a_¿D6ÌÈ}ýØ _àDT× +Î'¿¯_´rˆ¬â[A,Õû5Žz¸ˆ²ÑvùÁw¥^è‹Š] ŸŠC»Ñ‰÷²E7 xÌ»½©/~±uð46å *ø4dõÓ#ŒTÛí.=w|žèVkß”þKÔÊF¶t¹3-ÕÎ^M +„õTÙéíúzU*|dk—_šÚ{ÙhÕß®íýãÙB彘_ý^•:Ï;+Sß¿«FöêþI…Ÿî–sº5ß™ÙÞý>п_Ï/39S¯jÖv묺Yíeõ­¶v·zðz¼ôt¸mœ¾uwôï“B÷ÏôéJQkþ›<:Ø|{[8ûì.>¾oÍï\oùÃØÉžî,å§ÕBÛÈý¹\_/þ¾L]jŸWÝ9çì“ÚÎúñÑáI¶ZºYÎ*Ïß'¥5¿,µV7W¥ÖŸÇ ©mÌôzwv¡×·N×ûS7ÆC_¸Üs+-J‹µÊiUݲ'eoÿj½þÕ'6›³§hmuX†øêKç/¶ní•Çý/t¨ÝØOò÷NÎÍ®,omðˆEˆËý™Á}Q§³Âµ’ŠÆÂ}¾»7urqTŸþèÕZ3Šöü;õö”}ÂJÒAqhö·ü¥í`¾Úö©IÌ®´ŽŽrÒt÷‰øû{Ÿ~“ý+p|`i{ ¦h΋·2K±Æ£CÝÈ5~3¹æE«{Fh6®ÛgÆNãir¹ö³°±\ë?Í·ì©×½µÓeõÈöQo¿œ]­tŒìŒsò=–¶Ï,È¥WKÏo–ç·²=²ª•÷Oää•ÍÓÓÛÒÚÉY‚ï½U±h·LÝúº~Þ#ž“Tû)`6Âí•Ä5½šü$Hì(b~À¯æ,©ú³þ¿.–‰„™Êƒ»ìÁêà[µÖš²mÁ°©Ô,ç~[…Îúº\ο¼¯=¾~ÛR­nÉDqr×òlCžoü½¯ÉðÅb‰ùâ~º±˜É ¾jT¼¯”Õ‹ã¥Áë.¾M_ezýe@˜"ˆŒÏW¹½5yï~Vò€39¹}S}t¿ª0ã´¿æ$ü¬æØGúŽ„UFïrÇØRðW…Žýp“sÇÞ/Q‡/-ôý +ƒVòõ¦[íW)УÑÀqö%LëÉ]ò«3ìãÅ¥ììÕy«TÛ>|UáÛ£2ùV)sO¤GÕ©8mIÊÛan­l;¤“iÖß¿+8×r”Bñ#'ä#Ö(³sÆóç'QbF”kSý…Òñ|Ϩk;+æõSOݪR¼8ÌVAŠ_*îÆß©Ó w­Ûâå¦Rü}z,ý´ìm˜Òì,"OŸV)‰šë2*òi¥ù¸«;?½+ÎO¿—7Žm©¬ïoŸ”g‡Ðé·Ë8âEë5>•Û·-‰þõíï3ö]ñÌå¶ …™ÍÝüÓ?ò¡Ø…æžÊf-w§ÙjšyLS8X}«>Ï®ì=þÙjn®g=Aâ^æ4ÜàÀ sðnWÕÜïzw0ŸëY†:£kd¶êÌü¥îüÔ¼ÿë©ÜÉûJp6»£ëVómòqåàè*ŸÉ­=T–OQY/|6çìÿ|ô,úMÿ%ðŒOnºb-,7Z %g@nÎ]ï~ÖNW·7V‹o=|¤À謮IÕ£°Ü ¶Γ…ƒX³ÈµE÷ŽÄ¿³Œ³×ÝÕÕ³›¾T{¬ÖþNîͨÓöû$ª}GŽ´$‰à$Þ +¿jERæ,¯]-ã¯s%4G!úȃ¿‚¿9A‘Ò©»Soª$B# š ¦Ô‘8»_l0÷õë,¤»Á†nÎRRnäÜÏÊÎgÇ2òô&ð†yÒiܽ®<ù¤ê–$雸›2+ W¿ b'?ï¢ìçŸDµàÒ¯V;«Ý(ÓϨ¶h¯S$Ö¿Õ·É–Ü©´d€»³t€²m–ØìmßþcmF=Nñ´´_‡–*-®=IO¹&Z‹3¬ùBz]/lžºÖEGi=hž½I>ÃFÆÛdà.à÷aïŒ$ë×yï";s¡l0&4±(³‹ç(7~Ðò<ö½0±ÕÇ÷ ö¼!²3úg+8€º~é p8è)ì8Å[Tz9 +¼u›WŠï«&Á`´g ù¼è|vÿ=ë|ö:Y‚ý »ìúä£û'Þ¿U“·6¼ÆÙ˧³÷Àø¥|Q­Ó~kb ÂÜ>Ñ®©yíY™¥ù·ã”ü»ÝIüêÜÿôFÁÛ—³ìgmw½ÕóË]æàÃôÁ0ŸÖv«F`@±ß,¡˜¥ÄÔú—3H§ìà‘{Ê›ëý€`Y-ùz‡ß H Õ<dr8ŸW¦‡y®¡Ù?t‰pÅá÷r×%AàÉ“2ÈÉ>"¨Ëû…‚C„û¥Ýpb:ÁAše·$Šne=ébA¸µ¸é¡s]üwœ€h>ebs÷Õ!ò+€´t{µ‹ø OŸ” óVž÷ù¬5©­Ï&àFñî'Û¥è9ÀZ"gÁ2ÃPË *h@±á–*èµ&Ü o§’Ýaxºàí¬Å7DRJ±”ŠÇ<:y,9›2'^È ƒè{J!6%ŃÓ²@vÃÀã±ÄCYŽÒÁ7@&pTŠ£Êß"+}s KºEo7¢™*0&nYUþÙíô‘’ZI–ámhªe0Q…¢Hp%&e"œÍ7¯ö!”þˆÙQÏç,É»pøRH‰è9$ÐC(-£fÁòå01K¸r@±xMÄÑC³I¹R,-g•ÕsyùõúgnÃûUžkl2¿.om¹¿–ü‘ÎÙ&Æ^râ¤î¯4IÛý•ÞJ ~¥/ïWß¼JÞ¼('—èÌÈ•Q÷_·FÜgÖ- }eþÈV¬ÊE¶ôXídË»Å2‰{àu“ëT[†}t”îþ,[Änu£K7Œ/z?ý~Ec3ùÅs} ¸%nmeE½êã¯n`°“w* –Öï^á£&ê‰cJB™.ˆþÚ¨V‰:XóÂêÒÉ×êÀ× +n³è‹[–í7ì¸Ybã­[%7 ¹É4Ë7¦Úôð]«ÓóÏ+|»²GéÅàI{qÖû”Û‚¿Ï|qÛÁàùÉ^<»*ÑÀuÿß/|q “_ï²ðëÞ2œ}wˆ[' ²×"ôÆÙ]%nÞ Y¸¡ðØn@vkxâË€à{~=&ÃÎÒPMmïRu#¦¥R}jaŠF­K€  +žóù–T¾`¹*•fí~afîù#ÜZXÓþüO¼IÛgÓ|Ù$^>ÿ– ýhŒgXoPò'ád2lqÃÚ·­vk9ß=j®?-f8Ìù™<`›#6(Y|»rbXç÷šK‚{Àòc;[÷°[!WÀc4‚ðp\sÑg’ü°µ^„Ÿ.e'föp£ÔíuðŒk÷êà§' álòë±B.ñá׳AØðõRdÛÖ^o<Î{PòK[ug>¯O*óÅMV½ub§Ûß9g-¯ïqpÕUKµæ»7Å?£µ»úfMY¾zdr41O€.Èû‡f•²¤ÚÏYÕEúè0ÍÏ$·ååüt/~zò‚ÔäµÆXáÏ?-4ýç¤5[¬Ï›'•ÉíÖÝvΦÛ™Û¾)m´Kw¾6œSy&MžÁ$ûâÄ0ÿ{LÀâÆ0ÿ{LÂÉN ó¿ÁÌä¼æ/‚IÖâÄ0IóÿÕ3¦ië¦e鵃߷no·÷÷ùïÇD93Ÿ©­¬ËòñÇÓg«×íuÿïOóóñ÷½ûñ317Q[9l¬¯[z³ûøùÔ(Óoº2£âLÖ z²×í¾h( ä¯6ÿØí×ÎÔÁâ}ót^gb¥Ôªú:úΖ_þu29Tª¹lùöé2[VZ‡ÙB§_Æ_Ïi¨ˆ*Uî¥/‘F³SÍnoõw­´½u¸Á 1"û˜É­®v–kýç…Zgåb³yÑ:9\®ýHŸD+;ˆ/³¹‘ç\àƒeð3“-.ìhÙJ-‚ù "³ÓM«–-——ñÓlir×·ƒ_,e++êS¶¼}ÖÉÎ~Oõ0j±"bF)9מ²kÝÞXqï“ˆ× ½Û–ËÏÏur¥áÈI¸‰ý´•ò]æV¶™ËÜ‚}®ú.s©lnuº9‚s¼Zc[gm™Ü>;ÉαN'ÆzíoýxÈhuÂçƒÏN¾Ÿ÷jw?J^a9Ÿ¡ïÜϱ—” nèXF/)Á=-±ŸÝKÊÀ HK“$ÅD—”Q7µò—K„–$æà‚%ÌÆÿÁ«õ•ê¦6xOëFv£.)£âx—R'‘lI,þ”tçºzˆËꥹ½Ÿ÷´Nú#÷Æž=påOË5i²EBn AÎ%›M +Aše² R\08R„b©—q•eûgvžFWºÀ¡XRJ ç]‰[ÆýÂŽ7€zù&[l\þjHÜe$$%¦wÊd€þd»³ZrÛŒdr³—=õ›w¿è@¶þÉ%NC‡Ïïoó#ò(…ÑxlK«zy,rˆ›eiTË¿Fã1é£(à±ÄCl•Ê± Â@áç±³:Ên€?¿Xã <–tˆöÖdüqåà¤Ø¢Á—XöñçpSå“ç@’`bw ÌØ5L°þšm7Ъ&%Ñ/QËèTZéÎFhÆ–ì%À u6:KJÌzõšÌhäŽöS=^`†x¸É¦žK%7ý1jsø’ÅL˜ƒsöƒ³xœ6+#qÄce±ê‘’‘–1šˆÂhp¸paiù¸´¾ÄþÚÙ]fÝ?^q’ u™¦~xÔÈ\­JöQk.•nÏš¿÷í­Æõ)­¡}Ù <]ùÔ'. U{ •õö)q® L0zífnÁñøv»Näìµ¢ÜvžÊÄç¦ Oc7qR©û¨.ORoz]šÊ“Ç)Èý­qﯛÄ3`®ô×ë~—ÄãòÄ»qâ0¨˜ž]·—h àÏ|áú3_Ëî-ïfÕÜ™ý‡q®‡«VƒP96¦za5½BS=ˆ°Ä{žu õ %Q5H&7Æza5H&7Æza5®elõ %Q5¬e|õ ÂjŒ(Ž­DX §Ò­AwU÷òÈaòµ*³œƒ´ýáüánw–R1¿x8ëÔ­µ~߈œÂ°„äÈ©ÓµûA¬Œw’IQ ¡AðYIž…—vQSš•Ë©\sÊR•JŠ×°öõÌ9íÍ¢ó¸œÞªžÒp_Ð×¥AoçWô$U¥<(¦BanpG2G–Q`er}¹LƒTxÖ×k互^…VYa®’Í Þµ8ו‹³ƒ{Zcx¤¨š9V¡"ŒÊ»¯¹‘]ˆÅ ;v‰V!×,ÞØÞ]Ë "²ÿÿê™ùLo+o×>žØ›ÊL.Ÿv~¿@¿]í>ÿýغÿO·—‘'èü‡ÿšö„¬XŠ®Ã/:~ºõ)Ø ¹8±õ‘ÉÝÖVz?Í¿???î{ÿ™˜Ãζ·Ž×›sö`ç' +0é á«"^Þ o3ÒÄ +üÿìùÊüÔ„ÿïÂ/RÕ2Ý4eÛ¶UÝ–UøDS ÓÐTÉÖ$C±FS-Ù´à7øÁ6`šUI·5Y—$YSÙ0áøMÃQtÙ²L2Žb)ªiÚ–%K’¤+…ƒEYŸh/ªÖÄꢩÉòÏ^3’C ˜ßà— øá|ô?²4±=qy-M<áÜàŸŠ!+ï@\…Áu&!Ã\éç[œÏUI†™™šiHºa+*I¢Oé2¿ðßì/’ )%9û„ÿÁ +p—ÚäßUü×]„,+UM×t˶MÝ6쉊fËUX¿&Kš¤yË’UU5Í°%É´M[ãÁU]³L[µuËÀ%h–]5üˆ JÕ2Û’¢¨†d ˆU•UÃt@̉ÇLö§jªî8¬Õ6ª’,˶¤ÂΪHÓåªbš‹n ÐÉUCQáwEƒ‰ë ¥ +Û®M׫ø> µ +\¤ÛLÞ0dR€¶â!CÉ’3DªêÀ“`;l@ÛS6x @#E«j¦@†©àb9ã(&€h’eæ꙲]µTÝ[Ä”ª@•ÝŽ0¥V`ÌÎ+jU54ŲMIRÉ”CÌÏdÀÀ2o“IdË£5ò£»flª†¡"Äf6D–̪ ø€Ö¶jÑq€!$ØpÖ2ME(ÛÖ,@¶aïU_ÇQàôª*ð;Š$£ œ š ¥ƒxÜ5ÅGçˆ|©+–7Ì„l„%Á©”AF©¾õÀé´a=pu‹òHŽ²¤˜ +ò …7P– +'$¤aq€˜ £Ž2°—©ëÞ†6’låìúì1•Z°÷0CŒð?ÝÈÜ ‹d°²Î'L @€™€1éì øÅc7ͨ2-DÑm:;ERdo2­ +L+ÓhbóÃ_³›²ñœ{îU£j* +FÕªêI8bFC1’ÁVvGk‡‘ Ô•ÍÙ?ñd €5S¡‚F­ê@ †vÕÀ£ò΃Ë'®u©š©ã1ƒµ¨”15Õ¤‘d˼=8PÀ2{–LN‚éíË;Ò@ÃÁç-‹r È!ogdë*A)é¦s&-Yö@LÅ2ƒÇ–ýµ¤ªá—ë¡q,Ž ÐÀÂ; ð‰®˜,Ff· †¶dÖ%¯P5 æˆáŠ5Dš²VŽBÌn8ƒ°¯èÐðÆQQð€"4À§ Ò7Mñ,„ B™." +ÂÀ©0L•5Êp¥Q["qk$… y\ `4¹ +Ò5 eøŠPÚ$ÔPé*f4|dà‰Ç؆l adYEÖ@)¥ó—ÏÁ–6U˜»!| P¤2ÿP€$àþȶ²ÄӒȇÄú7 0'‰—ÄQ¤èm‚Æ&r[w ”8ºm @âQpмýõêÐÇ”j„·ôF’óåy[ ÄAƒá¢Û(§x®Fð0Ì ’Œ‰wŒ%>˜ï`þ€=¡R‡Ì4g’%q€Àä”À¦÷Æ Ï&¤ë«âèLÜkÜVÝP$ƒm¨J’?¤œL„³¨0Pˆ8¡É„(¿Sƒðoù­ˆ›JLD=h| žê ¹@tÐzÄIPà×o7y œõs€0Ôâú°ºðlBç#¼*Î9ƒSnJÀþ:Æ5î8l $µ΃͛ $¼ª0Pˆ:áÙ„h¿WŽê<&ñú³§Œ1Q(Nœ’Pþà²j (1$œ ¶^l`HF×MFáDƒýUØܶ$ -+ÝÀ« ¤‰„èA‹j¶B¯Àjk/Êš¸ Pa+5° AUªðg¸"°õ€ YTŸ$B Žk‹F+dz°ŠáG ªg>¨9  Òô³IQ©@H†áUŒ±ƒ\fŽ–Œw Šg¨ó@`~¶¢1 !Thu«šÁlgÊàlȦë¢Úœ…‡ABä ‚Ó Ãa±TÝ£RåM™\xü^!k©&øÕ +Øò @ÁÔB>V@Ê)’Än)8Zà‘‚Ý«¢‹J…a2!.ðæ@ÊéšÂ1ÐNFUÁú5‰~¢ÂÅÃf€ûˆ÷ªd øÄ0ÙÅ›Tâ„€kK®§bpQÉU[³XŸ“3eÃò†Á¬*´p¢³ arP{c \QG„EhÆB&–¤òÖ¿UdG-°$4àAÔÀ÷Že§I +»£X$¶ëWËìAËô,;oq`éà« ¿U.PÐ0RñH8>ˆŠ®L(GkœÈ8=ÅbhµÂ÷Q¨À€ÏuO9SVe4élfœÐÂ)‹*D@‚Jñ¡­nÛ2ÁH*oÆBr †Ö¿UT«¯|e4 ;X®¥à5G´*¸êžÛjƒ.S'XІæøÞ`÷zÂ\ª8¬YQÑo4LÞÚèúMÕ=×Á2“Mv˜¹ªl3ò0„‰¸uLð1…& |”PR‡–M@T/zÄ!AeÌ¢4¼ . ðCˆœ±"SKq×í…òìî*7ó¡°Rœ¨þôþ~$E¡J² +Ѐ…™¶MÓ'LŒ*@Zps¨e ˜#FÀ2P‘N†eÔ‰DÔ5Wc®¸€;Ñ×yk¡d}†@àIKL¸AU5º“^9‘^ï -L0“È¥S0 j 60@¯KAA¢÷x’@g®vx¨0ùÃ4|¨BSÆà*bâ,œ€h‚ (“{ *}v¤²*™$˜N¦,±¨lêÖUç.?)þéê$Š¡`xÁz ÕŠC8Š† ©ÀÌ«0Œ †·¢¦Í›.‘ˆI² È¢™´¶{ô¿eè%L忨 +v¬s©Í£ƒO¢²±Am +0ú„®ä„^àTÁ«è7H¬7Zv$D>& =<ç +q¸x3怄Ö»S‡ –ŽI[¶M³±Ð†dDuØ`Ù¨¼-›Ž¥£-j0ìÅ”mô~lEÓ¸ hõúRùƒ¨¨ÃF®H@,š6M€A [b?bPɱ« 0 ú *¤² J€d‘Ü]Æ„£LY£S&v†~rá,A×>³'8$¨dŸ kìrË– ’\š2Q6°Åú˜ öŠ:lhbòLFjaàõxÀa#ù6à·(ñzÑ|Vdÿ…‹†×>`‚¢Æ«WÅT +KÖiT¢bFÐCUlaÚ€J³ÐÑÂÖM†ˆD£ ªc âyô9lTèÙþNxÊðgà2‘…ð ˆÅútFeJ@YvQùÜ}= €t±Å›2qc™Æ`á »Wžuo’ h\—mÒ(¢‰¶$ͦ^›f¡ˆó"81߆÷ˆ¹´–WºÖ +Ä@ë¯tÙIý¢¨Ø{ \ž$aàL]“ˆMœ¡/F@ÀZ' 4mNõ§9STè!*ÓÉõ…ïÙÀ'gÊøg“ÆÇY8ab¬<rPaa†ï†¦LA`6xI,óÖ¿Uÿ;FþqÆv¯~ÈîÿÒ­/ MÒx¡L%ƒ˜4Ÿ"!NÍ q¥ + ㋯Ó@)Hq(%N}m"Û ¸ª*•Û5hà–À¨Á¸mBF}Ÿ'aD¤:Eó)°ð„AxȾ‹Øð² S$Â!XÁà2°­ArèȔݒ{Âq"»2wÙ±E” +±ej:+¡|yäÔ¤µ¨I+ÑäuÛïFSÃXs c“ öµâØךÂÁ40åmÚé䎜/7•¢bx‘ ãpPaMˆì ã„g GÁkå,A,&¦DÉG-yGÊq0¡™®9–¼äΘ ðQcß±äÉunhÝñ;E6\E0UaY– ÿr¾4Ó©GTA«D—F”¥-£Äd˜Ü]`et€Ñk²¹ f09ˆŠºõµd3ž01›IQçì}æn0Œ +SË-ËAM ÈØ¢ÎÂ)cH„ ÈA%cy ž.¬ÖÝ)3VÁ ?¡ù´Á…ÇïÕÀ9 efyÑ'®Ì¥ûÅ + ƒ„DaOæ’…éŒ}†0º6ŒˆPÙV}ˆB&»Å&Ñ„—Úó0ùx¨B27ă!™^vìF n^$] +Ëóò¨!¨H#›%³wJH s•`*ºÎ1Áµ?‹$TÅä ¢™Spj`K1-Å=¢:Ã$ªD¡"Õþüð”Év±™á…‡6PFC<%WPXªÄõÛ¨ÇAû‡»ðø½¢î6²¸.©5 +^'¨ä–…”$ø˜˜W†¾«¬è2ÄI2¦’L²=Šéîè ÂÇlž>á€õ(UX»q¦T¤œ…)! :“H@ƒIWª +Öò¨ 7ZôÔ£D" ´ì/´îø­Â åXz&·÷ÓÖ*}õbí㉼MQ©dr¹½ûçîQïþï[·—yîßÿŸîÄýÇÇçÏýO÷ ¾™xîuû?Ÿ½îDÿåóðø“x.·¶ÛÊüï5ˆn endstream endobj 22 0 obj <>stream +%AI12_CompressedDataxœì½í’¹•%øþ±?ÚLš]e;¾íΚeDdöhMÝ-“Ô³3Ö6VÆ®bKœ©"ËX¬îí}ú=ç\À?""ɤ¨Öh¨Ìbàp¸¸Ÿç^üÕÿò›ßýâþ›7ÿôòán>LõW§·/_¼{óö—}{øÕ·ßþøû·üêg¿ýùÁ•»îU¿ê ÿãË·?¼zóú—ªRå#¯þÙñíË×ï§7?~ýÇï^¼þùág?GÕï_½ûö%*_ýæíËÞ½ùê×oþðæ«ã·/¾þoÇ¿¹{ñêçcèòüâÚºù¯}økõ—¡~ó·lðâõ¿¼øá‡Wÿ/«s¨ßßüøú›W¯ÿp|óÿüò0~‘?¤Ù~f‡êÿðê·/¸Ýf¾«>$ߢs%G]pçR‰%Ï)7ÏÎÏo¾þñ;<ÎoÞ¾ùúå?œÞ|ûæí¿<œþíÅëÃß¾øj^þóËo¿}ó¯=Ê„”¾z|õíK¼‹ï^¼;¸À7sÿ+ç¿:þøêÛoþîÇïþé%Þ’O™_‡¯Ôå?ü€¾Ð-ÿͯËW¿úßüîå»w5nÈ·ûÛ¿9n‡ÁaùÙoÿý|øüÿýÌ—ø¿0 …tÑ{— ¾Á_ѵ֒«µàÎw_J«ÕÍóœ<ûq …ŠžŠ·®œ +ª|bù{ÜHo®àv>»Ò¾©Ù—˜Üœ½Ÿ}LvuʼؗýÅ!¦¹Õ9ÖkÊS²Îæê[ÍÛŵâbãîb{ôüíË?¼Ò"Ūù/?ïoôí›ï¿{ñö¿ýðQSÍ+ÿò»ï¿ÅÚÓ2 -ß%¶äÿ¶ô¶˜cµû…óå.Õ¥ÿzøE‰ñ.•ZB}Ù2®‹ã]áã¢\wM¬Óu¹½ü—W/ÿõ—‡¿{óú¥­©û·ï~gë>Æy¶ßVóÛ¿}ùö^¿zgëäþWÍÕß¾ùæå·h¿\ÿøí ­%·þ¶¿ñö/ßa³¼ùöÇwÚ×uÜ‹ö×/þí%W¾³üý÷/_ÿþÍÔ%Å—ãñ蘲ì#¶n üÉ[Ä|À7uëoë›=±Ÿqƒ‚ùý Öþß¿}õ‡W¯Ù0…eö¶)þæí«oÖ=Qü¡Ú/=ņËsÍÏÍ·ç~Óô)Ù¥Šíòœoìàm¾{÷òu%Øߧ¿Ýì×ùîo‡'yxýÍéÍwœÒHª°Z_cûæV·ü[5¸üÇïí­èï¯0û¿yûê5ûœþN5õ«ß|û#ªþæ훿ÿÕë~3ý̨õ|ù5H2È7‡¿ÿ§ÿŠ?@wEC¿ûâkt€¿—6 ·ßÿü½ÝýæÛ¯_¼=è{\i_þúÕ¿ æt°ËP³~÷áNÏ/ÿDq½Ö¾}xý//¿}óýËõû囯¿9üß/Þ~ÿá®1o7=èÏñÿg<-VÞ÷x9z5¹xò÷4ØT=ãF/ÞýŒâåëo~Xú¶?÷oÔ¾ûp¿ûškðíáøöÇþxøý›7ß.Ýî«–Þû×ú–í÷ø.xý÷¯í]ß©7¸¼ˆÚOî.hýôPùSîýôâÛo_ýáí‹ïÿøêë[7¸Q¿ÜÉê>faýÛwÿôæÛW?|·®§Í7¿yñöÝ«¯¿}ù»ûáÝËïž=¹‡‡o^È=±ßÛæwÿúâÝ×üõ«zûâí«—ïÝ}œ€~õú¬ýßýøêÝËõ½ùî{Êš‡ßýñÅ÷/õïþø¨–¿[:L_ôo‰û/~ñªïòáøzSÿ7o_|ó + Âô?¼~ýâ;Pü?ô¯îçÓ­/É…Ço¦œþéTP2JB‰§pò*e>ÍÇÇãÊåtäç^¥¡T”‚’QÒ1¢?ýÑçã|ÿˆòp¾?¡Øuü4”ŠRT2JB‰(Å£8”ù~ní¡Ûyj'ÞR׊óV”Ò²Jjiú?ñ ¸rÆ'4aej¡¹úXê ·«5×X}uå¡œ1êVJI%Wæü€§?¢§’!nf ð1Óq¸ †¦˜Brñ1>àdÓS ÑÅ9<„¹AíÈ!…\˜ý^ÞÑ7ÓÙ'¼ó³{À˼wmrÅ¡'ç›ç¼] x®3DÀ½Íh©™)€ÛÏø—_~ûåwè?ö›ýfgQ¿“~ç ÿÃc¢”ªÒPîõ3~—ߘu”“~Æïsÿ—þš6Õçå÷Y¿ð‡™ñ(ñ¸+§÷”ó{ÊÃ(ÓúÏ^Ÿ.i~OqV¦ñ]ñÛ¢9ÉË£b‘Üc©œò‹æoÓE±”2TŲºÇâ:a‰=”G¼g‡%°ð–_Á"lXŠG,Èó„uùˆI€Ü‰u±Z¡yaaßc™Ÿ°êÚ#¦ÃaM쌄]R°göÏ»éŒ}õˆIrØk/6ó„X±Iî±GOØ­ÇGLŒÃNx» û»Ø.áu¼Ò® š˜¤M\zM›õØ;:‹°³YDM;ÍF?*·(÷ùéxâçŒò€òÈEqv(þP8›éŒ×wÆ+:×3÷7žwá%üÜÝlÜ.Äš]Ïnès¥Á‚²®l:1¡ëcý#†’@U¢Ç¦nä%R‰çó@iŸ¹ŽZ’Z«2Ä­½HëÉIëcà‹¢gY¾Ä7­?Ÿ}'%í¡Z½ úeå«Ï;ˆã“®«læ°A0k¸.ƒÐ¦ì~µ ÉámVUhÝYš½ÔkÏóçbz×{f‡‘„õÁÿ;ßמü*g¼Öð?ê™Pgô–RçIE÷¬s‚ l¯ê ’|WçJ•ÍÄ—ÁŠŠO±Íì%î^½{ïxÜYãæ=ütƤwT]ŠÁÆ+Öu©Ô€y¨bɺ;ó„YƒVOcÆCšŽ;»Ú/òÛ{9^6tßäM Ìù: y}=?áàÍÏ‹ 9äÁ犈óMqÞ ‰®õŽ¶û÷( ²þUð¤e){æù3-lKØNbŸý_nêÿŸ×1¤ýõ°þ-!e-§¥˜ ¤2uѨ-_µ]©»R6%oÊæ¯ ¿ö2H\~¬„¥,ìdð°õ¡VÑpê^|ŒzMÇùXÁg)«Cô Ü )E²0H;Š×ôñ ºd|”`^K‘„L“¨/~ÒLÎù…ì›ó‘²;vm‘àœ!:ƒEd×èÇŽM•")ß'Šé”¦Kâ'Nªƒä+GA­ s+Ž]ÌnàÒEÂvŠü„èQøšQ(zSø>AihÁ'¶ÏaÍ—Ôf*Φ:gq-i4I…>‰¯=h ³4iÓ¥©MSŸ¦i¨t­ºM²eI:›z-ÛTl/ÓS”!*wU»ÊØtßnêhRº‡úL+×ПӢ?×E6íù4´ç®Œ;™ÖBWÉ¥?Ky†ú.ú³´géϦ=ûE{N‹ö\íyèÏ\)¶ÌÝÔ×tXVm^f[–ÞiY[Ëúñ˱åPlæ§>Í6¥ç>{6_b°ànNŒ7‚4gèŠé¸—>c#=‚–:óE—¿—]ˆ6žG™e<˜ă$Y¡‚GÜãýžðØË,ù:©)ÐÏ·ÈL6É\Ydòûl2ÓsŒ2¶ÉÈr"«É¤I· ·É¶‰¶I>öéÕÔöYµµÙ´™´Y´ŒSŸ¼ªy³9³ùZ¬]êérÏFúyè¿Ïú9‹uš8DºˆDš{‰4›ˆ$b˜å÷({o–ˆA»î,ãÔŒb† @ƒ +œ7TaC,´ê¸ú¸qK§M\ž~WÎ7dìkÙz•°/Š± +¿³U¥½jì9Ûuë¾ë–+Û{«ç}‹ôFCÛTµ—¶)÷›rܔӦœ­lg‚N”ê•$ÀE­i&ú±rê3}î«àq¬ ­˜vÊØ'Ü%Ü#Ú!ËþHØû±Ý}SØÌ·‹Rû,oYÉi·.6ÜŸþÍ0ß.·æ;t“yìóµdu† FÒ$„{ sÅ4ã'‰äG­…Öó¢ÕÂUÃõÃuÔßYÅÙËÍç.9·½ì,5kHÏ÷’ŸK— Cî+ãþª´g”zYvÇ $Ã<þuÓ$øþV²ÑÌ­y®µÚ’÷¶ h‘`ÑÎŽŠ¬ ‹fÆʉ¾Ì!È*’]ˆÎçŠéŠ´šÕ ‹úÙÙ cZlmBÛd[²1UÇíµk~bãһ¾L³‡^ }Ë{Ž 0Nh‡Õz­8¦݉Viq²<‚jC«rŒ÷9ìÑLX@7-H[“,Fû«ßOjT¶¦ äRö)^¿ a Ÿ˜=lîˆëyÿVRp3þ˜1‚l–6ýrÎè¹ÅY#ÚMŸ,±Ì²QrD»©s7 ?‘ñ|šå/Þ´üÅŸ£ÿÅò×µ('ziT2Ë®1¼†tü‘.ÒÊæd´‹b2eÃ^Œ8“9‚ì.xâÎßîÅÁÎ1r$£¬:&‡89J‰”“l‡&Uš\9$KÊ–§.[>tw ùöB’e"&·}é‚f3as’¼içƒ$àYr§IždëdìdíY¶¯*ßd ;ŠÍŸÄêÏR +!¦Lâù³Lh²:ÎIÑn^ÔO±TÒz17ù±—“•©œ—²U:÷¥Î—åÂ;µÌí–OŽŸ°ðÇ-+ ­&Îiq,ŸºXI‡KyïV>o<ÆÃc¼j' Åég˜zÍÞF‹Í°!¯¢L^¬Í´ÐmmÔ&¦ÞO}RÎ9-RΩ˵§.é¹×t . wéxÈÉý3ɲ´Œ­¹ŠíßY¶§¤>Š™­ìY±Xóô„„7¾;>UnÏè¼ØœÆÿý2»[i(,?a"SŸôýįBÒåÿÇ’H›e1 i,‘?0ùX(ecJßšÖ[ÿ{û¯ºþž.*¶ÖùÕn¿ý}ùÿ‹o§Í[ƒÿŠŒØ~wÜ}·ýv©™v î{GÇ}£[¥#'$Øž!ø%W)=Twh'vØ/²Ÿ¡÷ {q‘Å8v›ñ°ŸÍn<-j–lÌ|,¿6m g‘ï‡;½#‚È ô($ÐɬÊSí@^ZÄŠý9wìÏÀýægEü,xŸö8Éþl +âŠ÷i]U,ë#óÍ<~çäxì¦6ªh´¨`d“,Öæõ°OéÞÊâIòx”Lnžß½!Ý#")ÝJ'°[º{Ú”ãE¹¿,¦è<.Jí©Ïë}Wxªn“û GÙ_¼æÙɽð¨Ùó}’åæ~Ò´7=vÑäg™ynè,Z +×zËVsA™vê˪Ài“f0‡Ùc_4¦ýÊd¥u¡t/0–Ñ$ÓVYf†1K æG>œ Þ¬cÎ:îì±cÏþ e:öÛ×ÏýRÚRêR¶ó’w…V»4ÙÿTâž*cF/ŒÃÎ9lŸ÷Ë,™³]3F¦×i±Ç+í°Ý¶4ü—«"<>«¡x5 /VåÕܼš¡WãôV†XÙiSë÷Ô×Ö([£ùÞœ¾5´Ïç)gkÕž–E¹ÿœn”ãå~[¦ýŸË +ÿ`ÙZK6fB3~¼‰Ð‹À%mÿ6Érºi$Üš ïwfÂKC¡¬d ž6øª-º*mŒ†m±PuÛTwzøTµ1 +O´#šmá(7irÌsœ<ªQ¾Õ­4¾•Ç‡Dî (Ðc×òI29y”Çïåá=èÞ"IãСº~/üL!/tÔTž@¿‡¬}–‹‚2…_Äé"Áù(ÍG›EØcŸË"vpÁ{ÿž£÷@È•[Ÿéu…Ež/~¯XÈ!Ûž§+¤ÁÃòûÜ=Ž+áÜ»Þ"/÷‚óqZF׿è²ÄqixßÐCÂX…ªý¿Êpr‚Í=%D~Òçõì;#\|Ž r–3ûÜôe—áÍžº;ûQí“ÚM{ Í°uiÅp/ñpim8:µ†O{z†S»nÜÚÄÅÌèrê+¢JðîPcGÌ|¤H\‹m§`{k_ n{¹mꂽض +m«ÈfâZÖ6rÚ*¡I6›6 •ÇS9w©ë¾ÏœÍ] ÔvÌÊõ2q:ÍÖ¿È>]¬Ë;ëíVü¹€nˆ@Óµ ôèZü¹}¦ÎãV‘g+êlEœP³‘]ü2·ÎHÅÄ)Þ²ºÅ%=¼Ï{óãÎ/èw®c󦽫x¸‰[wßoœÃ—náãE¹_Ä¥õ_mêmed£ÖÒÌýPµèì?È 1k}¹"¢Ö.u®f¾@®o®s¬v®z½E.ðµ¥O7ë7½ô£\y÷š þœõsÒσ87~0ƒýg"#¿ü1b[eõ$óÇc·tвQdŸ—â‘?2@Ù†±ì >‹5²äiq·n­ÃÕšv®V2~ªêà FcwÛýÖÓ:ÝrµÞâO;ZÂÎÅ’6¦ÃÚÕå“ñÈó{¬‡f;”å° i2\߀“‘Ðþ^þ8Éel&l¤åër¨acëÚû‚öV‹K;Å¢ão¥„ÁÒÏ‹ð»!vÃÖ¿8Ž²bN J2÷‚µ`Å&TZ]È$[d?òj?o°wgñ¦“HŠÀ«²8æI¬*i,H<ò¬Îµ ˆ5ðx÷â_­ƒ²Šl¬©s2Ãå¹Irßç=ˆpžzÄÁÆp¿³tàÙë#Åx‡oÏ0~AnÑÕúÖŇjäçÓ-†þ>×Íï!Üaé…Oà½äÄH3v¯ŸSu3dâ&Ó~ñ’±¿S¾>Î5p9dìÚ Ly¼O—Õ=€oéD©;TX]¾Ý²¾Aô¾1è4ç±€ÐÛM‰R ¾ä¹s´ÉTR­Á¹„y-è€prˆ«+.[ˆÁgü4‚ÜÝ5ÈS‡ï2︼‰Ï>{¼a®ÜI«œo6E&JÈÉ»VKÓ f‘%ˆˆÞ¾ÁK'AI‚åÏm;!ÂÜo}oIW¥]xÂÅ°p“ÍËùI ëÓÜ\ù¦›+à6n.'x¡'M?ƒV“!g°¢û… yø«lòÆ‚È‚sç½à¼â»qêL÷~0 ÐÖ(P=Mb³ ‡%w¥ûèÔÑ!ô­ØŽ ™.,­[$†=âÒ±bÜœ°mD6Pm€&Ú"z,ØÖQ[Äqþ‚à²Å® ›ÄÖ*!á÷väKÜ ×.ñ4+šæ¸EÓ\¢6W¤æ%2sż`v›×t…¿<]”óE¹þì@(Ó(ÌóeÑœ.PšzéBúôx;6òO.¡®òáêéÎ]Ö;Jœ3 =ȹJè§á×aHÝ£gÎìÇIReè~ìÕ}ÞÀ!·PÈ@H™ÄäD‹X§&›Úaø²‡nóÝ¿œ%5 D¤#æö]›j&,©v‹˜j0-Õõ$€WV¼5?GQ–óFÇèÙd¶ÎiQ´î»Åó¤v–þgŠè0¢›²jªë°×ßw+¨iÀu¥qø ÂRÖâ¼)[MÛ–­Ž¾¬n?WTÁ ¸· + ÜÔ-·ŠfÙE;Oï …~FÉK1?NšÆ?þ\å/´Ãkývu¢ßÖkWµ1\øÇ Ä´ÃKŒ—þ{„ˆ•E׬7~†×¼¬ÎôtácOÿ*KýöeFÏ €ßaÒ?ýæq8,ºñúÿøäÿwßLË;¹i J·[n5À Û¿F÷…Îî.šl‹»ºÕø~3œéâ¢ýXo•øžÒù¹ÍßÓÍv„·ÇñÞ¤7€ÏK™þ¤1½§ü…v¸â/:'Øœ±y2#ó$KFå9È¥gù+èlì‘x4ß÷8‘XˆÖÉ‘ñ_óÝÌ0žVbÈ1&&κût#QRÞµ±@2—ͱ¶÷Ø|¾1˜,{t>¸pˆ‹·qÿåhuWÊE8Õåןæî©7Ý=õKTÓ—¨¦/QM_¢š¾D5}‰júÕô%ªéKTÓ—¨¦/QM_¢š¾D5}‰jzþç¿D5}‰júÕtÕ´fÚF4‰d÷8¦Òæmƒ–ðN¦ž¼ò4²î2ý ŸéÓ.¿úœ9Âû+…jqN¬S\»#`Š‹{òq äÁܵMzÏ°{AñQQ’9Q“æl@…GÔzXÄŒ%À‘„Í3¸pé˧-?Gýôˆ'-º³–ᣉãGËÔõüˆãgükÿ÷h㦾¦Ï:»á$ìQ¿õ¯5«nb±ÎÝêgXO³Ã ´ç[4R–±Æ“Á±œÜÔeŠÒƒ’®#²‚¢“r7ç»èðØ£²¼Œx›ü—SË:à¬M"Ìm|Ö*@»±î¡#Ê--¦‰…NÞa†³´ƒ[`(õœ0¯[TqcNYÑlK¬Ô0ª®Ánèº7©8í÷28í6ØKÓÉ¢½d'µZ%‡%…Žµ§Äe²±²U({¾CØ7ábM˜Jî}—Ÿ‚0¯÷}{Axëóü|Ðn l»™Žòƒå>ì)ØØŸŒL›6²åéà´§ÓÎé2,m J£éIŒÑŠ0ZÃÑòU’x×ʼnvš:Žè~­©âãr èaAö\'Œ—EtêòJÏžÚ³¦Ëëq{{ØĽ]b:n¡:žŠ|{à‘A§D¾}dìÛ´ˆ}üž+÷M=Cû¢ß_HH]Hw-}Þˆº>„ÏPWÓfÙÿêÆHì}L¡‰‘ÑœŽÒðÎSvsÄ -îÈ#…±•°xuƒˆÍ’6ÜŒ­s[¶³‘˜™Ñm’0:íâê.Ãå{ê~RÃú4k»éam?×&íÖÅXyZÊy1cwíd1ÖíÓÊÞmË8ÒÄ\G¡'zéÞGI='ÀZʦÔþ›ÿ¯“þ1[jÿ¹ï‰âﻩ~5¿šùþÔÿuê©äOÛÀ!;äáC|zÏ`·~¦å ™5?Àêü±rÚ”õ ƒU Ø”³¬“§åHžõŽŒÏk¸Ê“f^Á!… ÷_^YëæÖ“ryß*‹Cµœ;Ó:d9÷g\?kæ4@‡8U4ÍV\ê¸bB²&bÂBé iÖi!'CFúá0äöd?Ø[ýhrrãày9ñ q³êl¢ 3\Î:ûcè5œ»Ëª»®kY?y)˜8 c컦îx²kìò›?,(Êsw«,å}_·u‘§¶˜JCUÆŽv¶9qÓ \åÃWyÜeØ"*ó¥¤ÐŹÝaÃUكݮlQZÓÆÛ±ñx, ´ç¸=vŽé†ßãÚÕ±º4ü`m ”’Û•¨»ƒ%ŽËÁËÁ¾[Æ×c%êbò‡ö8ݯÇIÄÍQë1ÃN¾?$àÆ1f+ŸnŸÐχxöQç +‚¥@!æÑubÁŽÚS+r%Úo¡A«ôs[íGúír;E„ï¹²rùÁËè^ܦOþœƒ”+¦šœÓù~Ìx=3™ ³^;Ëü½=¦-öƒ ÷£§«÷zñ„»ˆ­Ï8†Oâÿþæ©ÂÞíê»×møí·^ûã…ËÞ¬6$Á«Ã¾uû"Éã8Ѫûë6–Ãõ$âA®m„+>õAðú~ñ´C ®¸Ó-Ît”^‚F÷Р9Ün«ˆpÜœ\lŒ²-'[1Š2p£€ÚL‹X³Ä½,xš•Ï—e/Mõ"9kÚJ´Á\—Ór¼,ÓõWSŒåØó¼²³‚"Á©s?¡,é Õ‡.jTÅEy<¦ Ebyù#ôßû!N$°ßY\ö¸œíœaB'é=ÔÏý²‡å º"ÁB©Â"_Ъ`¡Iã23%ŒX#"ü†aîu"t.R”Ž3¬•[Ž_y耭aD¸_Î'-æ~öœ»tî‡.y†SnøõöpžÖ½d#¡Ñö¬Â­Uçfám{Ò‡OÛ˜?ÙËeB¶…?fÉÆÇÅJk³êb:4³aòug8ô=¿õYbÉyñ÷ã "z|wÝf¹nGf¬S7¼ºMð½ï"“ë4’ &ÒÐ{üã}7¹vh£l#žÍλJC0™6’ÉýæÈ«5¯Å¥š´î.#J´Ú§…¼” Zµ ËÏ^û¹Q¦åD­[åùçÀæ?ör“Ù…² ‹g¿lp¶fžýGYåçl;à¶pÛ!·Ñ3N0?o<ýór˜øêñ=2iàpMà.¥ãÔesÃ$=ì0¹s•ò=ÉÃ@ç„nê +ÁPB¤*L®; »«'à¸äDX¬€˜5yßö S¬ßiÑ^ü®„]¹šI7Š†9mÔ¦µ<ÿS/ËtýÕÇÑ”Mš s·¼ï°±§sš–M>tG:­ÇŽƒÇêæ0»íQvÛƒì´c·*Ǫt,jÇrÝö(»íav»ãì&©"ë™vçáàëŠÉ8~oºJ•–”ˆã¨x;î®NýÌ»qnü8;~œ€wêçà²*ƒÛ\íqZŽšß³\«¨—ûuºŒÝû¸bÆüm¼ÐuÐe0Î¥ü·•ïí@ÂK1p5SmÍW"á ‰°Ë„Ó Ñð†xx[0¼%N/.åþV™–^ñž›åƒŸé½µM˜>‰ ”-Afhé]G7 ú¹içÔÍí?H ¶À‚¼ïU^÷s÷·Œ\† {TÚ›xÊ—GÐ^Âà/°ðÑP(]ð1ÐóÞ†Ýô'ÙCoQ9«“ìP ì³Z1î Œ_+ðŸsæë˜q65TœuLWɳGäLÞWù ð½/Ø!T ùÚº¢%îÛWÆq_¤é3‚wPb*±z6>ã ôJó©ñí¢e6{9mðŸZó:­lÆ Sv¤³ÙÕfŽ'ðZ¬Gâ<”‡Ðï¼&œÎp5Æ|цwóþêx°ŸÎ>ÉÒÒ-K¾EÇ‹§çIlÈø׊‹ØºIŒÆÈ°=HÆ F,’”rs;Ÿú™à¥S#7 Ý~nC²öG‰ +Ä5‚QöG_X¼‚»w§Nézßñ¯ï9üUoŽ”_•Ï=l=\Þ '£è1;d~Oñ.[ +.ÅJ­WÓ½¥„OJsÐy†ØL®a…A)v\@§\ž—1Ÿóîй¸ºšK¾›Aç7'ßý÷¼«ý7‡rÃþq9Š8F(þt°r’›ÙîåéX­ÖÝl­íâFzS·„WuïNÝä¬ G A›Çþ|ƒø¤=|ó¿¸óÕv¦ÿ¡hØ÷çè^ó †i»FÁîˆ^ã_ŸqDôt#1bìÚuûSŽˆžÞwïsâI÷±†û Âm¶¼ºtk¥Û8Òã¼bòGôèˆ ]¢Gû‹Ÿ;jÎi´DŒæŹ[ûœ4‹ís³êkÇ:C}‡‹×[Ìç$vîžE.S‹ÜlÏ‹Èœ/hÿö]Fcê­yⶑ˜—q˜Û̹泗 +ºœ61—uÞÇYîÿº_¦`ü¾¿ñ—(‡õtpüؘƧ"wñŒôŒL;×ȈeÜ;FÖ(Æ­OdÄ]–£¸•Vhd`ÿ¨ÌB·s¯ülz_æõçDnフ ÜGöè®iI¨²|ß©ÌÛÀË3™AŒ¦{¿J®²K­r‘Wex½çþÒ×|*ÇiñZ×Møh÷G=/®î~W·ªÛÇÔ-ynoÅÓí£éö‘tK Ý.€®‡ÎM»È¹}Ä܈•ÛÈí‚â.Âàð™6!oû ¶]dÚm4àN ~ªþsa÷jàç¹ÿ§I7}¸øöJ.p7ä‚q€‡1­§å‚T`æýRAÚJû3;æÎQVUZ0æû#E¶¹;æ>6]°#6Æ·âËùb°=—Ç%ßÛr9w‘k`d’ˆó>Ûë>—k#›Úæ‘8-?Æ÷¯xÿŸ‹Ïì¸Ì¹LÇeö¹°ö£x*Ö ßc,{®÷È@ÙuHOÇñŸÇ¸>Ä[ÊŽ·¬ñæûhó%ÖÜÆ6-‘æû8ó'8ËŸnyè½sË‘'‰ÃÄJå¡hÎàž™Ñ·Æ Ò0›å¤\œ— sÃ6E(Çšž \ŸmŸF»üMÚåi•X3üìÀ—™%ë3†vHùZjËÁc£m˜lÏ ßå¢.™utkç­í—„a`ášçqzUm¶ž¿²»üVöb¿ù;ܨõ;ýdksqÓ¶Ú‚#ÛFDd€äׄ´¡Ë…” +7O.ið¡C/jÇtú©K€ùïÔ%š5[Þº#%¤(Ïöâš//õ”š–T“ï´ÍóÑå¸ÿÿ{íž:ÉaŸ$ö‰ƒzIk™– C3~Ýò%>©–.ÄqÈUøþ´ß·°÷••íSû5l,x0áK·ù.~ù©¨§_X£zàÞ/4üÐ +D­ ÙAòi5R +ª`DÇâa¢ä"ZH®¬BéÊf⟒ƒZ½OßZ§>ç(ºµª…PŽV³BˆN†Z°wðÞ$×aûb!ë¡ÊHêµYw¹‡Eû™ßº\^ïŸý¾[cÕgðØE¬ÔP„Ü$Y7çyö|ßXr:ÂXò!øÚ§`oã\ È?e0Ì,¨”½î3 a¬‚æ¹@ÄÿeýD§xm²ÿƒçÊJZ•g0ZlJs Ð"ŠýŠ:¾´}, Ÿã"ÝB»Ï-9ä§2žOâû7éK^¿/yý¾äõû’×ïK^¿/yý¾äõû’×ïK^¿/yý¾äõû’×ïK^¿/yýžÿù¤Ã/yý¾äõû’×ïK^¿/yý¾äõû’×ïK^¿/yý¾äõû’×ïσ©Kù® ¯ÏŠëcø’ÙïKf¿ÏÙ/ÝD…&·‹÷Ú$ŒûPyxªl¹Ý¤ÿí2×ùíg=¶9.%]¸¬†³j9ïwëœ2½âÔñ.çŽyæ"sÞz w±!›wI#Ú´dŽØæŽxè™#üUæˆÕ¡{ßSF¬É"dBšv,‹,ø(©fD°¤Á²Æ°lÑB ^hê®ØKÈÐ6ïÀ€ m€CŒ–Ÿ]ÊsÊ´¸7·e”¾ž³9²Þw1|õvžÖE4-.ÏG®œîèœ7Ëf]&Ûå±.Œº„²X0ËÆj¸.ˆÇµ.ˆÐ=úcI´ÅŽ¸Ë#2õD"Û4"éÂ’x\2‡¬ !H)HâÇyñëßS5 (õ° iºÖ4–E‘Ù|þ9>›½8õ„]‰›²ž²\6K%wXEéy'm`Χž~ò~qp¯.nspwÚ±›kÓÍø1ÿuì2[aB¥Úã•îïóiAh,sK%n±Ç›h¾Ç§]z˜ý¬ŽdAMsz6Ä¥(ÍiØäYÓ§}ˆÚÕ&Ï}V.£Ïèÿ̦ÎÏÑá‡sYu]k«=åQ²:ÖO·C&ÍŒ÷ðìòôgãišn¸›nû“Ö|egA.ÔnGœ>Þ”¸;”º-VÄnAœnL=o|ôÛ|~÷KÕ¥«~cõ®¼õµë`C ë‘m‹v[[t±icàÝëbOD¹™.fóùøìò¬™žn:Ÿ?»Ws;m'w7³›¹}ÖÌÖÛŽÚÌbn§gâ0â³84ì¡c¯‘‹šÛ©Oí6|ñznãåÜj>w.ã÷ÿdÙ»Ó}Ì·–Áå¼o#ç®e·×y~zŽÇüj†§‹ Ç§“jn¸¦Ïì´Û´–€sØL.­&OíØÓ6uº0ž\¥Þ´ž\äŒaRHèjÍ‹&S Zì ÄÇ ˜þàf†ÂHRÁPQ +Ô_è§.· $õöCå.^e_ízñSqƒ?ñH·vÞ=­ÛjœqºhQ©hsâÐ éJs W™lêžd4¹NEË›—p7‡P¶åÏ8†OÓšo¦XÀ·¡5PK¾P‘ýtCCþ~¼q^*ÇÓ eh¨BáiÝø¶j¼…:û‹¬ŠÕr:l*íØíÏyuÆyÂⱧF\Eç­~Ü¡k ySÓUä©£Ž÷:òðšM&ð5xÙ$ä;µóstégéÏ[µyÚhÍõ†Ö|êZó•Î¼U˜·jÔô¹,K¥í4æ±DV…y£.O›E²×˜Ë²Pî•tõ{si?Ñ +™vè›sOÉjZ³ïZóX Ck®=ÈÀáâ®úÆqù0äµ(œtª^§ÔŒW®Ø¸u»Îù þ'3¢Ï¤Ó3ñhüÌ:ý2†OÒéë-•¾R£O±Æ·?Ï‘s.ÊtC z*ÉdzdïÃŒ= 4»hq5Âg6½Y®FùÈŸ\¶>Îû“;üÓÊÈÀ?[ùÒáG\¸9¬1,9ÄÊ&w؃@¬^á‹ì¦À:à«#Àq²Ø£ž@Åw,ëHž2[‹µ`³vYöG‹<ćé*Ö(îι_ð"k‡ìbn±YüQ· Oë9!»£A ã¿;¤Ç<ïOûg.‡vLW§r¬á£Û Æó¦<úO[Æ´±yù«n”5 v_úcNãåùéü/Ž#˜žyjÁSåÏ}vÖÿT®ûÖòzd9Û¹cyö©—ÒOƒÃÆÍ]Í?kcÆß¹×V|¤j’5z$Þx蜸€6N‹y),–Ãc·"ì>/Ñ{4'?®˜4$WMúi Ät:”)-¦ã{¯ÔOqYÎo ’µû©-Órp¤Õb'´x…l¬á˜Y¯¨-ÇåÀ•å˜•žé¦~Êr4Šâ5“b6så(ÝËkV…~^ÞrËÐzzÌî´D ®¾Œ°”¸)[ xGöó­€ØÛÑ´ïû\…ìÞ.ÏwÜoÊ…-áIˆã³Ëôé]üT;\³CŒÏÃU¹õŽO7Ë’ Úäêß&νU>tb˜Bܧ[G&îÊe¶š”i.ÿ)å"yÜŸ±<§Ãø1eú¸æ_:üIw¸ÍöºÍér;Ûì²¹ôT.g¯²»îŽ}M=ŸK¸°líìZÓMCæÖ”¹5^ï:^œþZ†)ûúð×qüëîô¯›']žúu?]ýzëįçù%ËÚtã0ص¸e¾]e›ž y·ŽŠ½].ŽŽ.¿øÈruÐØtóô±O(ÁŽ}Ë]Û°cö«S¦Õ#V)3¬åVåμÇÛæ† g§xÒ,Úx¾È{¢ñxÒö2‹ñ¬“yo'‹}Ø@¶ö¡nÈÊéYan« x5›¹¸xÚˆMœ®B×Àć«ÀÄËÐÄÔì­Ñ‰nÚ„(>öt2&Éž–`Å£Qˆ¾9‡:NÂgvö¾§8>a)+`g §ßž¸»Ï-»ófLá|UnŸøt^ c¸_Ë´ý㢴?¥L_ÜÔã?¦LŸÞÅOµÃÕ³1÷âT>úK;*Æ”x&uóEɸ¹¸#ôä´aB ku¡äà¡JGÊSIp÷XB¦?½pDè£|Ãò9qüarŠ0͇¾–³œnÔŒöw9¡"»;v!Þe—âîºÛ-ö×ôš|>øù.øVn\Ùb¹¾”‹Ì­—_šS#Üôj„Ý¡Ÿàêÿ½ï {JGq—üÜ)~¹Þ,G¯!¦ª@Iç—‡žòemöÐ3 zb ‘ð'/icw3è‚yä‚ù3§ÚèÑ6×fãOƒÀè˜áúÌCªž™ +Üâзù>ôL]ãøÞqpoêÇõ®&¢Õ@´MéáN–²þf®÷Ìâ¯Ë…Ƕ5Ìck)ä +&Lç_ \X>CìÍâµÑ )ëtˆ»€in£+Ôó“Ù¹±I ±«—ösŽÂ¢Û“›KK…aÕn¶=ÞÞ …y™õUº +/×Ù¨Ÿ8eáêܾÿŽ÷|‘Â_ýݛ׿yûêõ»W¯ÿð‹_lh׶bú»ïY¬æ7/Þ½{ùöõ†¨õo6m|sð®òtQOÏ;íøWØ}¢0 žj²³ÏÍùpG²Î¥ÐrT’‚{ýþOÿŠÿý¨ºž·à?ý›þü¿ðÏÿŠ/ÿ<çð·‡ü/óá»ä·øßÕ(.ïqø­>8’ïoµºz¢_ߺãóZݼãküüõýÛwçW_¿{õæõ‹·ÿvø¥ØÑ_ß¼ùöð³û_õ×ýÕÃ7¯Þ½yûÕñÅ×ÿ ÓöÕï_}ûò«ß¾üúÝÏÿ.øßñÓßß¿³—ø÷Z!Ï¡Bþefª ‹C n–Ë‹)4rIö\yÑU)÷ƒÍ8e÷¿ÊG®r¸û%—v=ü§Bœ “Ùƒçri‡–ïþ©Ya%†ÇÊ™iô—JVðÖ¬QϬˆx—ªÀÓSd¾Voío7Už´âA&œów©1g{mé]äƒóé®Aô^ƒ§B/Øvw V¾‚Ÿ6?£Ew RSË<°uíÁó4ÙÂV954Šw¾*‹`Øl›$°Sµpà.l1C±s¬ê"“¢žËºÇ ’}gïqÑ â5:È®¼—o-re·äØÖÌŒ7]mÎZĨ>BYÕ ¼ÃÔâU·­pÆòÆh0õìdÔñ1ÅAD? Ïàù~ÝŒ7ÅáNsºóOÐJöxxÉõŽÚ&„i?i*”yœRAÔp' £ù¦Ûó€Þïæ@‹rÈ] lP7™gð웑ØLE ¶éuƒPÑ_©Ø©!)iˆ‹ 2_åŽsX8ý4-<ôÛàµ&,VyÒ€«<ÿ+BR›-ÁZT¬<&››6¢nÌF'õ¨‘÷Uø hû÷5G] ZߧBŸ7‡+A @ã)¼a°n´Š<0‚­0ì qîý@ÀW?ÅÙ(SÀSrião >öu³ÅÜ»OwôV·9A „L‘šëb®.VÐÁ‚šÁ¦±À~°òcÁ>@¿¿ 3o Uko|¯^SÔ&lXÓ1pÐýÌÏ‚…òM|ê\§ž^¿›-ЕçäÎœ_ëþºQ¹Ã½HU@±‰766W©Ë=µ»’'SÀYásjV.*lü —ûWᡵÔx—ÈŽnˆ Ã9µh@nÑA¬AÇsõ“2Ö [ñ%£4,k)<°UáejÁsnÙ‚Ç•;潡IEC!ÀŠ³Êè—š­öz“H»Ü'pÎѦ”lm*O‘BÀ¤^°¬ø@‹¨Ø•€ªA +J›Žå¦w‚ ¸læNøø^ +67©-7hÐ/Ô .‰G?ñσAöI5èÍaoñÍúbŠn´ÀJg1¬c,Å>5W­2w?–¨|õ?pò8^ç1Àh²Y£Ì…÷ƒEFuÓG®ŸàC©×-l•aNЬaðòâ~HÞ!Á9ž¹È@*ÞÈæRqkñŽ:t_¨¹FqÜkª0c«@@mXfÐ9ÜK™k{ +‹L%Ø5¡“;n¤hD²Š5xìõ³­Ì&oÃcmƒ³@âÓÅ þEÏŠV.©2Êž‚†®õ=È=¯†JéÕD¹P^…ÂÏñc¸<Í-˜ÔM-<÷ŒÂ%½ëï­È%ÐʵÕJû­@&šúñFB=O>³ @ÊTÀ²€ +%6ÈÞÛP ‰:u é;’V`+ÈÊ-ó< +ßiy`ép‡RwÆɱÈ‚¡vp©fìîšÂ£¦ýu Ý©BâÀ;Àþ ¥ÞêÛꎇPg:3++“$¢Ì&¢@¥Á’ˆ2wÌJoV–fFÁ&e»ŠOÆ +R6‰Bxu‡'(v÷Ï’Ïì¸rxö +^')}»)˜ç죜¸j¥™š®x%™ ïeŠ¢F¦¬¾ìš§Tærn¶yH@‘!]š¡ÏänPÚ`3ªc\ê2#jY—Ê .F‰ÌO-j(º:qâÐ +¢c ®|µh5ð|ÙT ˆ†Ø<7/‡~§¬•ÆVs"…F+öËVLu` h…¼uÍl-<•Ëˆ ˜ÕB7Ùe²al€'÷v£æD²!<£ ÄL^ §(m=ÐìIȨx'ܽ"U£8‰Ñc{£EÀæ!ÊIÑUÆ,ªÈ3Ö®¨„ùÄjÁ"we•$C ’LÀ@7õn¦ “9ÒÄn ÷E Jîœ4¼nï17‹$0ae÷Ç 9h¸+ä¡J)•ÔA°RŽx.µàÖG…ñ(’ɬÀ;Ù)¤i»â–#­•Zo(úEv¢Öa&C ´À©ž êÌÉZrhAÉ‚-8h—B¦Í9Jj¸„’~#ŒrO2É™£‚oá-dJ.˜¦9‹Ì×@Ô´$\eŠ´œùí‘SßSyÄd0bN¥;x}\m¡2‚ý³•X ‘ÓÀ«¸Q™¯±"«‚Â3þα3o~ç8%hœù^P ºjgB+Í"Ù¾‹µÅ›UêæX¾¯Š¦ÉÇ’zÄÇ㛢T¢ŒÅ†«×@Ô+á¿®Àƒ®‹ì¢‚c¦°ÃAÔ9¹ž챉+yôm ™ú^œ_*L]+N‰Gé‚ .WyаÏÀ€è'Éè÷º’pd¶Gëî¢sÉ\d¤š@¸h«KˆžÂ+·Â)¾±+ôÉÐ Ö>m}(¯iû¡ÏÌÅ;¦&Ìx+-ÓðÕȾÀßôÊÔ]Ä Àx*u2;Œ8`eãjðJ +št W²¸»Lå?âYeb¡&¼Ÿ,å$õ†J*­Ìþ¨Óq•È6¨=*ÀKÈ °›9mô{ ¢0ó%n¡:Óâ£FmäÏ +VÁJų2¦]ƈeEKßsëe¶¿‡¶Éï(}°18zÖ-ôH¬ Ý­bëžs½Â9U€ú¢;š\§=2d}< +¸BÖÒ„…O¹–‹×вE¢T|4†r“™ L»7ƒ4Ëd)¢ñxبäv‰Ö2®‡@‹!vo‚,î!}@/„²„>élMeâžå‡pX±PaV:jdë¥j$3Éc’š&Ý%UyLÒo°ª2_X¶'穵w—;G ´©âØ3] ³nM±N:o[šØr¢ËÓúÀl 1êeƩ߀ á9é]À£ƒsò¹$ T`·@ó 7ÆzÁ»]Äøgô)êÈéÙ‹oª ÉÊ C€ lTdU¤£vÙ“u©`‹Rìw 0³Üª€šÎ +p¿¢ +(ŠVAƒôèµuMW•³ˆZDkÌ7ÆÆàç¢z©wH¬`jˆƒ”¶uêd\…ì +ÝJËN² ;Šï Š[&ê+ÛUE™¥HŒ÷µ«ƒè½Eh×Ñ·ÐÀB…I.Èt=ˆ’˜óÝ‚™_·Ð è2¤óÊ×A>pÝÏŒKë-tPZeuÅ. 9R/UIK< %<§£õª9[”$5¼ׂ˜;úÛ©•U¦*!íâîL÷*„YrÐsÎô¼!ò<˜Œ¢$hF.×-ôDrG&iŽ7º‰ATL±=þÕŽ®áëŠB€¯ËZØUb"%—ñêÆÒe¸£Úà›ô„B€2/ß«7ÈA êÞƒ2z®ÍLÙÌΘÅJ»=Éz½Q‘hõõ£»ËJ¼cÎí">“j$rM2Îb0*ÆÌÓ^À™§â¸¹ +ì3ë4íVø$ ·L Œ +j¸´^ãÆÓ¡§4V*+µOi3Æ2¡ÂÀ T¡ÍBÝ +Ò,*‚Ó +hæ»È PåV‡BJ·ßèNZï”ZVÎÉé*Ÿ´‹÷ZÏ +ð2UHDÈ4¤ÛèÄ!­;¼!¯³Fg²^m>—ÉøñÊynê4+šh¡6FªWXwž)§7Él©5PŒ’v"ë×9ÝÔe%å>«#}¡ª;ûDfš"ÅÑB<ƒ0+ +ùu¯Pwž²%'”nÕ°^䳌’ày\²ÆIó®*ÀÞhÆé½]TÏz¡QÝM¤šy(]Þkãã`0¶xTŒ%kÚ;äHH£ŽfÐG3 +n­xÙtý-¥GÚLÁ½¨$)Ô‚šg›ä芆=‹µ‚!c@8ÄÉ.ÚRaŠ¾¹™˜K=c–«Õ×DÊ‚›ñZСšÉЙéJ%mQhÃf£'J›ê¨;°‚^Â*;íoýu†fB¿e‹± ¨ÆŽÞ*=“TðA“I7:žœ‹cÅL•R­íŠª +O‘¡’Æظ*¹×‰b$ wýÅú ÈW gSMåŽ-hSŒwÜN ‚Ðq±G…‘È*“'9ƒ›6Wµ¾Íá@gÁ×´ ‡¸ûJúºT +£Ó•š§„HŽ®pC|j6) µë.™iE•uU¢sz+J ú*?ÿl‹¸¢ï$`DðùD€””^uRÈuÐ;X%+¤àé¶z +Œ©p1›~eÜÜ™f‘y§ àéiÊŽÒ,h•r éÍ×[ä>ƒG]Vb È’dý-’èzĵ$&Ò˜áÑ.îÖše€ÓrøÖ¶×A¨ ’ÛðÆHò1Iø rªa“DuˆG¢ýÁx?t We•¤é™v$S³H«ÉõI«3M–µ› ýÙMÂl–Š¯‡é–fqÌ1¶¥’:2 ¸C–ÎÚ\ êõuÅ–ü^UfÌ<4"\ ›>ÈÀ`=ºtˆ gе0wÉ©ÐH†JEÿ ™©@˜AvÑ6P+ +ÕHQXœò4Ò–›)Ü•"7UZ©Rºö˜ƒ@JÒO÷8 ¥Þ(I3Îä”=Ç÷ÿ"q6ÒC +œ97ý +Ï ÚÞ(ZÓF’š(Ý’W¦è@©uÇ4ëHK›25«£æ¯ +구€Ÿ;I²Šh´G¨¢»¡Ynj\=¨•$UÈÿfN£ +M+\’šŒäXá(O[wÐÌ„ÇF_ÑU‘Ö*VdÙ ÐÆ;@‹ü>pí¤ d™æñ1¶ JåË d@ò‹GºÉÔ WqâlÌÝ: +‚ >XˆžÌ[›”C}G;ÿ&?¤ ·©"S ¥ñ‰‡Æ'jûôŸ§nÔc¥ž`Ï%I|,Öè+0LV4ÂÔÜ"èÎãsqçÇ€ >S†Ø‹>#¹/E<#ctzb1Ü*œF‡md¸ Øè¿4#Ƨnoñ…vPðyê´•VzzDc# 1š°«‹©´—ßC?ÈœJ³éíëÇÊÓ+²§üΩ3ˆ èh oW¤ +­q«ŽîƒPM¶@= [ñ4Ò¨DÌ„FMÞ)#-#t«yÖt'죙•T£Qò%f6ôH“%¯ºZ4£«• ¹[¶@ʼn¬à;Ù|Ñ$1©‘|Ø™Z¨ælèâP(Ó«…0ï1Wµ€23Ëé›ášüï ê±-wJ2€sgEׇãt¹äGTDqL¼!0y—¯ ЇX˜E&Z4¬_Ž8ó 4—Tàˆ—Ã¬Šx‘&v¯¹ÉyÈ}—Å>L j¤TÃûdÆTd5h3½&xO¥ KlU˨ º:½D{­¸"lP ZžØOLÃýlÎC´ÀËŽ6úÉ2]oéÄVÔÊQ ICkëëNÅ©_è'ãrKöR°+@Þ”C—òå.S ¤’5> +ÕZ¾é`îm'M ¸šk¯˜ ^"ÚOœð¨W6Ïl2YóE-_4ñ0p@ÊŠx|'‹ãøÛ ^³ K›Ì…ÈÍfNûÜŸ¢E„Òç0Wý¾½ÎÍTK1¥žŠ›iC ù¢bFŽÇ™½ÇÕ3÷,δ‘Ñ.Q™’ÑU ›¢Ä@Ìñƒoõ3Ë•¥™JœOHr2u^¢×êªeÜ£ÅãÔ¶Îø¾aGŽöÈM4ÚB/”`XuÜM;Ð}'³àl6·èôÍš! +Åœ¡‡»“­*§ë̦Ñ{“@í)YDq×@Ó*=×Ö€8)>DÍ)iSí-HÒÙ¤öå>ÆÌqY lËÑI\L¤›‹-¨æT" é)ž‰á;¥ÿ'[Ѐ¬ ȸ°ˆ$˜ãºjdK ?.(غf–è»áœEÌ=•‚DµËkí™–ZNX¾·}!Ÿ,bQÁc‡<½QôvVÊ—6­±OëØgW­¨4>ËÌ™bUé£Ƀì9SJkÆN)2C,¢†¥èn´øº;þe-™¾Òt£¾ "ðfq¿¤×*ÿª^5ÄRúWeË?ÉJ‡JŒaê’]‹r#S‰%£å{(Ò‹i!/fK麂Œ‘nt¼—œ†‚xÝÊ“ðBè¬f7„â„É5p ¼É› ›TpRB‚„Ô=/´ªZÄ)ƒAekÁ–­œ[hª"wDLâaÉ0b쇯W-˜-èV P"GÀN¡ 8îd<,‘ø[+¼»ÈVô´ªŸ¤“mš (KîÐ< ÊîD‹c¡^ ”i2¡ä¹ ©™ë­Ò®BÞ¦ ¼/u§"›^Œ£±•nïê3qj*XW-²cÂsb³@cÆ3]µ‚nÀ=J¤ÕQM:ˆ&]$Þ$»—¢MbŽ2Èe‹¯»(ã+ÑFè-£Wý`ÕfÚb× Yx)w´šaAè¹n1Üâu]Ž†J¤´T‘äC<8ÁgÃvÈœ§7‹^f’(•„ø²ÅB/ˆ´Å”8Z(¯û¡á³’axkÓ1uI D· }¨Œ¡Ä]"Löº£½n7\Ñr¦&µ¢NÇF…h.;ÆN©íZl ðp¡>íÔÂkÍóF’9Š¢3Õ‚"%[Ь¸ÜÈÈî°»Yt21ÄS¨¶pD…13SK…Èr5`ÔiN¥¦f.xìÂwŒߌ݈îÞ‡ª‡)%5RÎ$¶3ÐËõ’&RB”ˆË¦lN”³®[¸þVš‹ÜÅv£ËVô1ƒ ¡ñ‚xÅ`2:!¬4ʤL3þŠ&òvÏ}›™HÏ‹¶ì¢ˆLmýUpÙ‹Ã~lD²F>±é;‹t&AÊ PHEˆã–ž¡ÓºØØ$•£¤°@Ìdö´Fé|UãTeu˜…¨%%7à·LÐ…ñÛ sËýd7£èƒºÖHü$.ÓjC«K#ÁÂ}œäðÀëºÇLfNÅ‚‚$ã²…ùÍ£HÁ͉X^´"Z\òüLȇ3ˆÐ›D+ÉwÝ¢ud›g°á¡¿lE€o¡ÎàÝ) —HÔ-v°7Øvè°~¬g¢€Çg‚øh-Ü‚—`vJÂùdj¯¬ +²Yé [ Ø{±ã¹ í4ÏôúZ Ó­•©ÒðÏ +€^5w«§ZimT¯y›1AìgáAÕã—Z‡Y9fÇh8›…g ¸l}@¿W‹Bgçô@L Ú‡é Í3;ÑŠ~eƒÞØS7¢ñ«^+Å@¢Óo¶À’ìø—•´-…C®ÁXÆžú&sHä1Y@kAFÚu {WÑd¾V{«ŸÜ, ^㛩’fžb&‹G!)I‚Q56±´ste|NcAeª¦ëV¿îÃ!¶­j +Ê+Eè“Þ?ạ(sKÓpæ9Ô;ÒÒë”ÿIm‹€«ÙfתîThüÄŒrã{ßѪA€éƒ#&†ä6OÍ.’Ì$‚‰oãºÅPµˆ§ÙO G¯û©=ô¥èLRÊ´åкy¼i¾Ñâ’?›({Ù +ÄLøÍ,Û«I‰QUP{±*&©–âåiˆ O‘ÀÛøzÐÚ7(i=4u¨³âhþswÛA˧Ï"§§²ï„]hôq5:Re¾¦¿¹zæÌ•øohfúQigDÑÕ×fŽflà‰9•[ãòZZ| HÄÈ×’©ÝÆ£Qª¹lAm$u誒Bô„ÓJ>Öd$¢©äxÅxˆ2#&ŠDR.l× ²„²0 +‡&š´X«ô¶`Q«Fo9QXqfã~û2À1ÿî¦%¦Ž@kEºme='ê«0²‚ÄEþÆÄÝL´l^w§a¶KÂmÑk•Q<èM3ÂNS“æÔB(íÔ˜ËÒ6pšô±ÐX¤3=--¨ÐU§ÒãÓ8‚ù)¢+†öó™DrùÆ€¡H[ͨØ5Zß®ºABÞÔãiåâ{ZÅйêäWQ+ѨLO¾F‹uÍVÏP©œEî<2ù•~ì«E(ë}¥#´"¦ÜZYˆMo6ÜØ„¹ çÖ”[Gl +ýÙt层@*=ˆØäˆi¸ø!Þø¹W±D¼b4@Âp¥*,èg.`ÅqHƒ‚¤Láï;5aE,$„¬1ˆÞ3‚Â%CA½—kt†ž)IÍ twuÄ Ì´Z{RÏLÀ®èÀ\Éù@¯ Ukg†GÓ|?ÅÁúÈv§¨L3{”ЊŠÜ>MC1PlÔ·O°*0nPZä¥Ij.˜v¹}òâçN5“ÎHQ¨wSÐÁ#³Ð#ª ®Á +FcÐ38ËàYhex[¬óÊÔJ{T +õL‡D±Ëe "r“[ÝZQ2XMõáPrõ©×³Ëîçáþ£2fš%.P˜F3Ò€˜w™FAÒE'ºA¶ |…'~¬ Í=ˆ!Ùy¸x£‚pèõ ¢€n# ¼Ÿ"±ü42;4ºO½)Øx½I^ù:.²A»Ð±yR{AÙ\Å ‘fXOb9Ó@`ÓòM@øË:»…Àzz¸£ +’}T€ñÉ-å]å~Ø>\®N»‚#ÑhŒ?§ÂÆC»^5!þŒ`Œ”z+Qï\ÜBÑ“FËHFÅ–ÐBA¢ +Ü1•lEÇV™v9¶ÿ+l•Íý];àÈQ˜±´™±A +N uk†QÌä­¼QŽ³nä†Sšn|èhf¥:ŠVŠÿ¤î[݈é.ĹxЀpO4Àt—Þ€vA´([,Nfü1ÚšäÞrè‰Äi÷$:I @ƒÿĨ; ª?áŠÄ=ÈK29ãå1ÖBÑ6TX cTÃ…rz¹Ó£”‰2Dž îU´{fNР¤¸Ö:ç‘ØÒL'|V¸ ž wbDOšu/»Ó»“§±obQ¼DV¼8H‚uòNœ6Þ©Ò-ŠÚÑ"q%%­¼L/tn¥8ð„«0攓N•Ã)ËjîF%/„6 é31ô‚ш ¦½ÜSé$m‡Ä"ä.ÓÚÓçÉ ãÁK±:1ÍÌ’»4ºuAý„ëõ3‘È1 U4'€#–7ÑxÒÐY0An|9¡ÙJq[T¨ô,fí–PhGßÄôÙ€$kKSÀSñd5Coí.zL,õ0ðW¾™0’Å1kK“$IÔ–@ÀZ)j #;Áå‰o-ìâ2ÃÌæbƒ„N›iGD8s-fÊI|ëô³r E®ss„Ù‚Ñi¸iå!´¶Ù ÅA§íµ®™3Š¤¢ù¿$Ú(ÒÓ^4ob9?0‘™Æ"¢š‚ùh!Åf3£Í­(:š€[I;Ô$(í8EŒù BC‰ rG]”Œ¢ËDjc&'´q]fc¥TE‰S½0f-¼Ðï߆ x#MÃÏ~ÁÀ|0aÿóÃ_ÿîÝÛW¯ÿpøÙñxÿõ×?~÷Û7ï^°í.?ƒÅêÌÎI^atHmÂðÖŽ1ID®rR1@êuZïøŽ)‹ØËýóS4<25Äk‚éH—4*¥žæ~úÐ`øÊ'ÖÕéϸ„i“)6 +ßäÏ ü:)µð?¨\ßõVR™ˆ)DØJQddL›l;o`%eoªmƒ´Ý$ž›ñ'ÖF«Ð\çd>ghê"47âÄZ·¥JVsÁ2³[Në‡Ø+öCm”-<ù35fÆC„$É¡8¥Ü`ÌöbCÊй’Ô្²ž &ŠEï>M0tÂÓµ.çå\*ä±ÌV"@y%ñn„jç*`NðtÃI°TÕ ìù2tpœUbáÁmž¶+èIöŽÆJÉx´!¶%ý¶b´žµ¢‰‹¨ò¬°¾$ø[€‡µPwâ»­N-È7Ø¢ÔÅtÙ°…ŸólG}G­¨„³T[ÉœÉw8`&%F‰¢¨¾U¦!Ø ä®œÉdWl" wBý%„„ç)J‚ÑD$† +þCc6šÀ¬iÃnèÕc7Sˆšõæd£„B#èâ¼ñ¹[ú<Ùtdd3GCL#ÒU .³½RÔ-r@И»ï´¨•@¥OaQ–ÊîI™M$FŽ0šî⺡UE~‡î‘aöš¢~3Ëlš³€(L@ñ²PÆuâ¯ÌÇIþJlÓâ³ Y–°Èx-óÉyKCmÍTp¸ÎãGœC)f!'ÑÀA™j5a3Ñw¥F>¬¸4ø=Ýbt‘²Qž“iPÞË<¢ƒì)J77ÝäÜŒ´>­ßä¢È’so,òXQâŸM‚Œ4ª(ßÓ€{ûFÙZ³`3Œ`°|4ðFù‹-e…@9[Bw:¦Blòñ™ Ý5ëõ¨Ù) dÞºKyþI‚‹=Ÿ‘£Ûž;ùÀ±Ó'qb,[K¡ºÁpßbå-*Y.ZxIF4Õd†ð \§W<åH)bÆðÜF8`f¾k^ôs3ȇ¨Ü#7¢W´¸ÌÆÌXàQðGæÃÔxþ0aèžX&åÔA†·ËEæ˜v„ð0²>®:åzao˜eO¦ínÙÁ³-\Th0`¥)h ý2a%ñ~¬¨„„ð…+Šš0tföbˆæDý‚²“uº=­|ÍD2†‰Qi„WtölÑœ9™X]Õó|X¦ùs*;l£Ñqî»Z‚©GRzïhŒdŠ.§/Ý@ÃJÁâ”Þ¦‚.+|S¢³;™‰XÑlt3Í[\Ücµq¹4ÉÌt‘XA•¡®ÇŠÆ„o÷(#ˆGŒ iõbͤÁE1þÒ_·Ö+e c}8B 9Qf€ ûK£ ¶2щ±%$ˆÂ×’,ÉvF¤+•S£‡¥g¦<òºJÁ¿¨˜)—³¢r†X! k™RÀ]yí®ÛB9({UҚŠ+EÓßlØ«Â2 𠦊$1;ˆÑ,t§ÒW(¦ÊœÑè9gFlUæÚ$ ™jÒ»êB®m¡T»iûðæ—aþ& ©´’Ó¢I¥Ÿy#aé­XÃo¦´ˆl_У-‹©ÌˆLªŠ;U"@ö eÖMŒg§F`T d]Æ8 „¤'4H‘L̹#áÉΘܠòˆ{Ù îh5Xð˜òg£Â[" 9yp&«Òç ÊÀL{ÄrÑO†Ýõ°æÔ  —P ¨Û´—âMÞõ89­@¬%Áᘴ4Ó[Ö¸&¨â¶F–äöQYóp$/SÚÔƒÒáTCf(òÍlÑ•(W²žNÐ\ÏòÀH}è.h%F îã:Ër˜¹Yy|Ï€h+ÎŒüˆKÜ8]€Ù ï <Š-pô]’ëšH‚›PówJ–-‘…cªËLƒÕº‚ËJ&óc%!jK%¨Ix†„Ã4 +¿äzg…Ô0Ì e{…‚„–'¹ðA•ÌÉÃx}å +Pw²S€« Ê–7•æ¨î㋦êW¤g9×íM¬×8W‰»#Ü*2†CÈ'|o©&” bø²5ÜÈsA‰h +¾K‚Úy¹+©n¯$Ó)÷¡ÒÜÍ©œif¨BŽx×á³ †É\leô@M¦wbvÚÉ9ç@„-Ø$“¸ žAc(²ÁÊ5Jñ†)0¥I.ÍXEjˆk<©â¡ñ¤LJÈÜmNJ&R&@šÓ`š¨‡Ð³6 ÖCËŒbÿš€%Æ1-±¨ŠÓácgØ,îÀB`˜s«°2JA+³¡¿›¬ÊäùœÍ®Ü”J4iKjŠ}XÄ£Ï&Sæ,Ù5±ykè9™Å0¨âôJf‰¤D@6"N“%‹ ÜÅÁ -„iÐ< R¨cŽ +f}²^ª˜×T(o!‹hÂ`…RLR]êÕB€YA,+‚Ÿ›óŽî!ø•Òahò)Ûó{ +€ªJ.Ã< #ÌÌݯ¯ÌhƒÁgán9Ñd]L G¸7m&Äi“lÍ–Ù¶¦s—WfÇxõA´§îL££e +PôMPÚ %ôÞYŽRQT$ÅA0ÂÝx Yíx_Ù8Xw * ‚äŽcHÒàˆú‹¸H¢ß™ÒƒïÉ@éýòòÚÇ÷P%ñŽ3}•Q9h’&<ù“4Ù§Xaù„ŠïfÈþÒË«q¤ÑY¬„LéUI;*+èkŠÚjªPª«ðQœÌzÇ`äMo\aè¦Iº3¢)dËhAO7àŠÝË=%£„dN4á4`RŠ©Vw©ÃÚhÄÎ̆ڄ…`þ?ñP&ñF,Y03a2’•™0!éAnŸW*;2ƒÉ2'¥)Á—3‰‰[4FI!SHJfƒ›g5sê™Ê,Ñh‰J4HE±Ui?å„›@›ÕWIdŠå^Ë„¹Ì,r +¬¡£þp/&¡~•¢ž +p&&UšCeu4ˆM ´ ÒRhÜËR”x¥U>àû®Ç09á'ÆÂñRÍLCC¦hK' +ÂYÂFìq)XËÌAרé $·Œ’¾d&kJ13Nˆ§§àiK&ÆTñÚ3¨)ŠE&Ì +À ÈçÚõ:êGL8n#t7Õe‚,„Y83(x‚®,„„ZLÔʳÅK)P$Ö–ÊlEI•YZM›•(E:P¼ì)v-í#”*ZcìR$:˜ùlDPN’é¦j ½ÙªºÞ*ôn²5ˆóhÀC¶Ñ aÛwÙÂylA¥X©7âz#òiºþ‰ßb+Ʋ• ,1vx±\zjÁ¼€lHÓØ Ù$NÖ ÛXcRh¤$„ô`Ø€€Ò“3…7b˘E%¦„èµD‹ÀŒºl!‹tUcoièhÉ«ŒanLÌ=Ï\­2êP¯Vž~GÞŸ¨'[ÌDå¡Oê` Lu±;5Ãæf¥]š 7ƒÄ@FXç«NæeÌX¡ÌB +ƒ•sL)ñ0u„èµdv emJJmFEãa1ü•A +—˜ŠÒ³¬)ácÏÜ—zn-”ƒÙ)¥ÍEY;]…#YTpêåù8Ë4pÜ©)«LRLÊNxÿ Üö´ U½Dº*×fU%)Ì øtŠÍ'Tˆ°„.ïó!!—)"×V©Bð¡+ei`?‘Él,¥ N#@‘¹Q¡$? 0^f´ü¼Âf­Äq-p9)έŠ„°)‹¥&EŠ÷¢r£ ršù$IÖ"º"òùS&“bf6ãxµ|L¥åX¦ƒI°¡x£æT<“¼A.|i{DÆ@žŽ!ÙÚY‘qTv`‡á;ª g}5…žYÔ3«3¬¨ð'4qQ¦žªL.÷ØPï,]ªPàÄèUSþŒÝ*o¯[º—»ö`E +0•‰Ž-ª¨)i dÞW;ŒÅ Ÿ¼ß© 0¨ÃZ(nÍ1¹þz'á\Ê.¨’¦XÞO…%@EgpŠ×õsO Z­=-!h)·ä•³8š!Hù È +M¼To V­ØaÂs;²mGž#ªTU±IFt#{Ea üN˜do#ÏÝ„Dò.†1ü ÝXïí ¬1:CÐñ*L5~mÆfŽÍŠäTL9oÈàg®|Ÿe ¢['é,¶BÞÄ'2NÆ: +¿´úÈô¢ÖŽ/U+æÚU7ÅZ‘Š±â¿ž,ôÃ[Ì0.³Ä¬l|=Ùv¶rëí¢’Šl‘ðÀÀGgÒÁc.O>XY¹Ý˜Ž}6Œ†260s„\{™¹i¿”Äh¹åjžµ[(h9erf´£œ'l+ó¨|„¾¨ôÜÄ-ª‚žVдÏıŸt=¡"”‚a,ûóèÎ\øL`™•í;Í–ÄPÙhpSÜU?lG™5•n.“§eU´žA¾qžuÅ3”Ý;ÞIµfxˆŽf« ÌÊÇ.ÈãO쨳B³.Bk^ô~h׫àæ!+>fä†Öñ©LÐ`›(5«Â p¯H•!YÂÞÿµ³[•&ÇÒóø¾Ã±ÊúWÄ¡]30ÆÆ'öMu<Ó͸Ýà»·žgI‘_eîšf ºš¢vJ¡–ÖzJÕ}º­0"Ñí¢dcó>ߺM*ÆT-…ͨF»ˆ ½?Ú‚§Ù©ÃgA•F‚{ˆÆ$,ÖçpÓýüRó4 +qN¬bÃÄmÛ‡°ßçÛQâN䲔˚ã~û•A÷ão8¾Åäšÿ(zMXYWÉ’wÞC|¤ä‘ó? ·²#)Ge]Czä^6Ä nˆ:*àÍ”]19³yP +L”åUÃϺ¾~ B¸ªÐ)|Xå¯}7À…Z ñh8›m•Z8'OÍd[[£àIyŽÔÇ-(ãóóbؤëN4%ÍDCÒ"¹½ÕI¨üGgï¨|Œ ¾|ö"LmRiy¾3èFÁ¢#H Y&lRsË`Ó+7êš^& ÒU¯ ì»Ökõ‚àêq|mÇ‘v½{dÇW ™A’¯³*Fö›°ýïU/¹Í\‡ÙœïŽ(N$\ •GU¶.CB£žV²ÄnSëáĉ¨ÌÓcøäŽKÌdÛ‹d§Mö'òåI>EžÀ ÔUr*Š¤ T.óFáïÒ¤d ¯Ð³Eûhô×’DÒÖàöx +ìEu:dË/“Ÿ¹3T `¹ßùè!¾ÓN™÷qtùìÕwE|gÀZŽ„¸ <ƒK±)‹õc/Ú.~‘õ];$ÉCo=~ÞIÙ`†÷Ü10ÿâ8o5ˆ s\‹1’ Tùhk’Ä7˜Ùáø£"•šyR{v?rF‡ÆÛ庰ÌɆÊ‘Šè5øTF L;cž3µ bßæZ!­È©ÊaESù«GÛ‚l·˜Y¬(/;Ä]¥Ì»Ý˜‘Rçè:@ ’‰…qjn‚Y^]ÛõÓI‚i`rbTòz=`—F`nnÕ“ ôëœàںž!L0”6Rpçf±¢)*é­WsvÝŸ¦aÓGŽµÖ7î“ }ïkŠÿÀy|ßúnÖ.Æ«GÚ*rIXæ¨iûöyLlǸm=Î&¸ðýÛTHÍ /§ò-kÜýEµ¦F±a½ ÏG/½þ¤bMñ’Ûg$ÞEÿà^õªÛ¹âp,ûQ +B,9µ; ÐK…)è£N`0¢â +ôÖJne¾ª·‘ÉH‰E3^9zk‚B ÞøæÞÐ|$WóFø…„£6af,šœ‚Š‹%øÒ/"%³nëÈá0ï’ !‘ûÇψ†¢ðžy¦j¦¸[à™jw+ê§z¶ö4פ“wæ„Ñ´Ð& wÈB„§¸Ì(…å$Oò£G9Áe÷û<”&˜2z݇èz@¯Jõêcyë:Ü°—`ƒ•7€€°õý©Ó‡ØQµ‹=_Ù¢þ1 0ƒ¡©q­! B}Ù +õó¸ë„·þëA…Eàã(ò–à&˜ø¾ÑÚY«ö×å0È„+¯ø›L÷Óà5¾;áœFD’IrÃPžƒw8'3 +7…3¼F£Çˆd˜Âó㨼7ê·,Y<]Î,m­¸Û¥ ãþ¼›²“ó+@n[¢l°{Í éò]Il|ßìԮ褌 á°‰éY=±AÇK©ˆ÷6ŠñD=Fh_tJ[‡_e<ž_ÙòÔi-i3ö sÃØ2ØÍxˆA¶ë =ΰ ên]±ÿW½(ÖM‰š +hÑì»@[\Ú»B²”Í× »}+‚ç óýÐ 7Šgõšl”¥Uùº1 ™<㤖áwèL,ÎËòßš,Bó¶mÊlñÞ½r¡´nv¨¬8€ 4ñYï[’*y…(Ì)´¦í!–žÌL µò¾ø{ùSêŽ~Ðè<"'"eB&0S»Y!^¶Þš +M9|&(w´ÃÊÕ•›×tÔÏñTZØu@ Á…¼‘[ôñtç#‚‹ÙJ͸ºi%ùÙ£ÿ°ý94[ÙgúèE˜§‹!² ðz[hQ¨SG°ˆM¼Mf°¯o·RHsöÙ#nJñM%–:î/“¢À…ø”{LâP +ª"DodÈë;(dè÷NÌ€PBÐŽé5œ­ÂN½!PÕ˜fB@)œÇBc'#ç4>{œ—uN^ÒŠÊíçqp*–3“Äb Ì ˆÙòe¶G‚ ØÉ‚mÛ0={]1«(HÉ B@È5(5ve@Ñê@+â"V`à!G@ÖP0þG¸y9ð$7|ç¨zÅh'–j)b©¬ú®Æ7”³É/8<uJÍØ °^âïIÁÅÚi(hÉà¿U£G˜¥,šÁj¡t¥~vzW£—›0óŽãËsöRâ8Íë'\Ý«È ®È_I­¹ÙÄ÷öP«yê×U’r¢% +D§ÚjÚª#È¥[ȽccØ+ÊÉ_ƒµV¡¸;”ÝagxêÖ{°(+zç½d 3T ²b˜ª†¬Ý ŠÀ_õ¡xÄìžö³Š + Y@%;›ª2ñ³¢¸jþ¤á~$L´5„¬E >º\%¦tuSa…Ài»¬ˆç„d‡P¨ù¤ôÓªÝGzÒ:=Vôz=ÇiŽÄ5ìUMï5„Qö‰"*Ãa›­U» ƒðx %Hß*Cf9èqHüUQö–âôÉ7Ø=2—<ß+b~ôU뇠m:yMçLx_ô¸Ä(­gFôPGß{©$ÖCI캆1fH¢¡®të›ýš‡En})f/yòÿ%7u÷šzï´¨ÕÏPúˆcôŠ­tHyT%¦Û™¹“©ˆÍÍ4C¶“å×j·úcU°9ËfE'g®+…oäQW_°iD«qm¸“LuÏÍŽ>”=J$Ñ‘1A¸{ì]óÔ䯨-®Ä¬ŸZÌ#'Þ7v•×Ž(W”Ç{BÚÃRTŽÒÐ6öøV“Z:´v ¡+)Dœ„†éYŽ,‡Më`C‹ÊU^i^,‡TÁ˜ad¬Ò‰§'ý3‘ôìBÏÝ´¾nÂz†ž"ÚÕ§ê=“à­«¨æ!6 ¹_@îïr>¾%¬!¯'Ygz׿‡½¤ÎtÆëL¾gT.&þtçãHfÏ`ïc— Ëø +õL­8³^Á›ÒV¦€}ÊŸ=°üÈõäö߯;°$Ï-Ñ^ú‰Â–¿¤]κÓþ}\Ñ¡òBVLóՙ紶Å&ô÷ßÇŽõy–Sì¹j1ÔÞ§†¤ä¾µƒ°›Ä˜ëE7–¾»)™O”¥nsžBHÔ¸îð†¡@¢"õz/×»ä!€6 »”`HÎð7ªùj‘BÖÝ(7Ä«S\ÇÏ*çBœ×™Á‰qõ“’*?¦»¶iÙL+”“H›uuL¯Pûhˆ‘ùD||‘·5[§Åvè˜]4èôû³‡7ÿ9Á[#Î3≼¬ÖVQb$þõÀUãTú$Ù̽›¾7œb CÛžF!7r"}óM`ù­¤Šä“ “ õÀËÄ¿g/G0¤lÀDÕ®Øf^ILëíGwMÑíí‹´ê{›Mû|a{ðд±&U»ºŽàh\„„-FJKÂE0V\Ddu .Y ämA´û 7$¸ÅŠ¬n. ˆTñ%_ŽxÛ©¥÷êd'#F¬¿¥Í†Òô-,æ”é-n‘+ð(!°Ü˜*¥õ£²áCoad 4»šø8?×PŸ¤Ëi.øÞ Ôêì­ ô¿ê0"ºž;äTúYòʦvEçó˜%#Sh€[•jF¢ˆ§pýFX!ªýýó;·S.þEãQs¤F«ë;ÕEÝÅ€´²¸…k8d= Ï{¸dç×·Ö ­¶ÇDZ¢Êé݈œ°÷äl oä8¦ŸeßÕ-€Oüè—Ñ«NÑp¹Òõ©Yay9¶Ñ4ÉF…B9?u½òf¯tD<šRyMë§}½8Ók*ãØK^cmòæe¸=l À[@I%Í +FSy†÷Ïgü”8TÙ ‹‡Ê[Òj±ú‡»0l¹\EýµAfP†ˆÎBšŠœð…ëÐó­|…f1ÕF™Ð#Ö X‘¼,¸Q™Üw”%},£ ¥]GŽ ÃN•±@ÈÆgcIêEÂj“sye‹D¯ˆÿ‚¨ÕÂ]dtáõ%]§‘¦k‘2‰©w79¶",x¦S‹ªÐ¯oµ-ó°vmȆ28ý{CÛ<¤8Ü{c iA¹hÁ°+(ˆâN÷¦yª€Ýàe£/ßÃy¬[péóŠF‘ì Gî@¨ÅCÅÏϧDÊæ–PþÃo ‘ªª%`nz¨SnA@ž ‘Ô;õúDz0LXó }C°aH­Ê)0q3°l£îß®ÝgæMÇèâ£ñ +ÉÕ~¥ëGÅû¬Þ"¨žÝ@š£> q¸k»Þ8¡´·¿•6õ¤¯µU¸"h‹ª+G€: uŠdql4·®F ÜMúÍ¬à›¿Y(‚€Á’/{È5‰–/¢n>ù<Þyˆ™¾ÛÌßÓ:ôÞ ³Ü%à‰ø OOØ(#„ia­GëãÖ7KèÒO…TV°g¢Ù¢à <Áë½ Z0;¦¤ÂÈì¯%‹§ª0p'û ©×#ŠÖ¶’Žßr¿¸‘!wNÄ…ÝÊ” %fn„D«ïºZÕQkª¶.ÐhŬèŠ"îz¡WÍÞ‹ê Á(+?$ò׌ +•—RS/OF[õã¼ELnjìkvVqËýE«dØÚÊñ4¸A”b‘Zí4¢£‡D/+¸|þV܇z€Žr¤˜O#¿— +ˆ=dÕ‘$ æî›*;ºÍ­×eÝU”±ÒbÎ +? +”ëP1d~a‡þãÆ° ¼†å +Ñ«ðv¯“=“Zº½%l·gγ]Á¿ù-Á6²•Iø¯GkÛ e€e¢­™¡…§õÚÆc!ŒŒcƒhL’a-‰ø˜€µ\{:phŠ€HA Oêë/õ%bó F«:¦×—4˵Š„Bj…ô@°>À±ü‰F1Gkœ(ÓFÌsUp‡` 0ÔÛö#6äµ¢Ùâ¿…½€¡˜-‹B`L¡Šf Í쪣)È4tÚlß +¬ KÜq€4!y™­ÌÚxx|p Hc®Nø—™ÓTdîçÚ|³G»í±"óPsË+–£G"Ç #ºŽÑò΋Þûø[  KþëÑ!mOƒ&µj[=†ÂTÉAÒcÀ×Êô0¢Gk݉8AoAû¯þÈò¡JAéU +@”Cv˜•í™ÚZoê*þô•râ‚kÖ>]Rw*½í„ëÎçƒêqÍÛšõ¡µ¨¯‰:_WØÒ•Á +´ß·"†“ê0®¸ÆQ¨¤¦cj*Ûñ|Mšåg‰Wê(àÃp_+1ð4½°’>Ãýo°±*]G;ö¢¹ï¦”ŸI·£¡Š€tBÍ7:axH'~ÂÞ%ªÍð5ù|Ì8:"*øå©Ù«¸¢~·¤”çðN±¨¨<Õ¯ís#ö¶þ²ÁDÛnáTo-k3v¹ôAV‡ò „@ûðá/¬µ4Ö Ö²`­t¡:·¾ uã9F€h¦}R!¢î†uõ|ÎQ³i?~IQíçùεõº £@‹Ud—–û—=øý<\±Îûðæö¼‡÷§§!4\¥ÇׄN~ׂüP÷bœ˜„÷O° !¨ߺc™_?zíq=¹yÃÓüv‡ÅšPº,,>êìqغK™¹¬£Wg2XÄ+¾~˜ëŽ_!Bó ¨C¯ C³Â ÎLC–î9OÕÑ1o= ITÊy•í…ƒž¡–çQ蜮GG¤}!‡gê¢Zd“„‚K¬çÚmë¿HÚ¢ÄÞ:ۀܖðÙãrÁø%'·ÏôÞ ´î RìsÞ]6`5«T®åÒ€Xºu /L¼ n2/ÂËß{x&£ÁµØÜÂ/áó8 +7-§Â´½–¯$4|ïñÕ0þèà+[v‘`¶½*¿CKvA +*;Rn …0•ŠC}¥}Kj€eõLÒÂB¿]•¶‡nW?6n˜%“îñ.lüw¹M~Ѩ**Ÿ­@Ï(ç* h_bÖ¢»â2Gä€ÆWt'Ô|ÍÝÍ4`ÇE-݈—¾ýÑ<¡®(Ùiˆ BìN…ÏšÊë[Ìy(õƒL%Í[rH¬ñ³^2\Ìõ"Yß{±§N<†oßã5ŽÖbB&‡°ÊØzßqr®*B®‹Årw&T]6"–ÏT§CŒc…=ß \rtŽ-Àzñ(¨q9âó5#ðy&½Tq ÝP¾‚®˜Ò›ÄüxåQ*@|uL.k#§hNgÃÓL0®Ÿ®ÃÎA§VYpG?ГiÔιº‡çU¥¸Cú¼†“ÄGÊSÙŘÏFÁ›ëÌe$Â×v³iED50Ö„"õZj~âêÂ(‰u©à¶ý|«¶˜~›TÀ¢4þñyrgÇúeS~7£ D6x`‡Þ!ÚIì#àø,y5DkÕ®']ï͆èz| rD9VÅFe”"›;ï^4Óè6Þ¡nö-4€[¾ë¦M*94TÑè+–EWbìÆV”ZöÓ_Ái~<ƒ¢´zÙÈZ ÷”¢þ÷ÙÐCf!FÙ[ã`>ªñ›”ಠ$éqEÉ‚·>5kÚȆx-k0¼*‹s¿_ßÙa6ç‚rˆ®~4(L±™Trû:œfÅkñ«+ôÊ¡ SÖz°‚|dkð+ùqD im‘ßT“€( ¯ jÊ“Ï“!Èý¨Ú+h°Ã óhÂpÖT A?«ö‰5ü@è^›opÎJ4‘(wxŽAääžwwT™itóZÍ‘âgÒ.ÄLä)ÙI ºbÃÇä‘RÐ5° +oj³ÄUÇ „+¯Fïå6æc¤ûøX +x]iÐ0 LýÁr#5JX˼£Ìz¤WôEÓ…H  â@mÈ +‘“X)¤UL÷…S0Ѥ:äùXY,ÔX䮕§Åbòå:v¢G¸^@© ƒµÊ*Ñ»÷m朷SKf¿xQKTs +öâH@"ÔKQ~¿21Æ~ Ê{¹^Ì4†EÑ + ñitV¦2}¥í™ƒ×Z†iЯÆMMÕ]0AW$èÊ»Pl£ 1m½n)|s@C«À¨ÓêÑõàeI‰ã …x#XKÑÝ\yUŒw©À ÿE„1õ0‚£cûÍý\2…üæ +êíæ(E¡ó¾"gE0S®)s¢Av­7õ;o'táS¨§Ñh#R󩙚×9t„àš ì9hÀ’˜Li¯£…¹Ó6z úÜ-;«Å¬×÷ÑØj¦*`¥×³ Z!ܼáЋØæZY¨P@¯ÏéþhõõßÚÀô̤ÄpIvÓµˆ&)³yõ[9ˆûDɼõðF~Tn5o®_}]ô*³:¿¶Q§a®˜—­d­Óºa(âÿt'†í”H<ÀZ¦îú ºÊM3Ø-ˆ«dõ­ nÄ“å¯ñ X›˜µ æÔ/zD™‡gž= #õã@÷9vü-•ØÖ;á£á çÎhøE£ò +˜g!Õ¢5ÚZ¹yì®ý„ïmÛzqEùä4Dô¿õG3â†p8ηÀZÃAÀZ=—¨Ê†žÂ`/÷Þx¦’€¾ÃTƒ°p’MÊ&åëÓpþpóÆoä×·zHÚb%5·Sæe{ÚÝ‚›–næk[l±ÛÃÑȫʶ‡öÌTÇmà}á[ÝÏîo³RÒVDy‚åae@Úe÷qª\ +¥&¡`Æ«FV•Ÿ›°yVÁóó=~Nì(ŠZÖàèÆ¥s¤9ì±2ÊÿdS}5F¼ä‚OCܺ€°½:7aµ¢‰ŽjyöÏÌøëü£qÃIªRºœ›wórê‰uŠA@©g-}ƒéiˆßØ…- f’þî[w$*“÷Æ‹Ðhô£aà<øܲ·F×@ÂTÕÑØïæõ‚PŸ=[2)Œþ(ºåi8C66ðÈÀ­Àùù^;hf% Ö +òù)d9¾…ïªo3¤À£û˜»8ÙoøŸQ+¯Àbná§ÈÓ¿ô +¦pbãã¾mk mmnüX¸a.¸}~ [»±Mò6ø*U©ûÕ¯ACÚØZÔsA&\ëö UÞ“u})Î/^àx¦R®†²WøÇ̹'",2Î9’õ ±¿öÄ*ëhGÔ—-UñFh”ÿ€På}ç”0Æ!ê2øÞ5§Kô U%ö,dNC,PY +ËBF¯oapñBÀð2©|nÔG¹á‡!õÞ8‘.ŠØD؉!'C+ãÖ¾* +w˜Ï^q¬ÏTü£Ô =ÜûP0RãmJª‚7à¦ö~B•¹¹ô]7µ¤Øðz äì88ä|èºfríÜ'4!Sl­â‡µØT¨IyÛïÐÈ[ZIõ˜Ï†n—?ï€óMû‘&ÖJkÀ@ìò‚µè›MbEi‹¼Zža1:ªÐ(žÿ=à˜†l˜-蕾ã{ô\ ×^œ`—œ)‰B—}1û\àc¤Øa1I£Ë”Â+÷íÚé‡J“+X ä¦ë»´„ù(ŠŠä£Ûž/a È 2˜3û^@p ¿˜vßQ{ÑóÆ %ϵD@¯D]WÛ¢šÃÁÿ(žºtÔ½ªiž ì¶  8 ªÓ–ª(ð"€Å Ìe¬Äf[²Û芃ًÝÙgÅ&L=LÀ#±- ¬«*Øs(ÒêçqƒXcÊáý­OÓÚ€†1ëz™.¢ê‘"|d*ž·x´M’غ–˜Bm»w©w,ðƒžÈÑÐNCÛ wˆˆÞ§±*5D’ÈíR€s-ÏÕoy³WCi!Ù,×Â8¦•±Û™²ãpëÅaȯû‡a;Ò:h¦`Î+ç‰5Wž–(—î°í=›Î5(;ºsLDZSôXS¹<œã ÔmªÉˆgß'”À/ó™9€z'ãK‹ñ.Q»P2O¼9_ጀö}òy{Kƒß¦öÙ©gÍy‡ü¿Wƒø?FE„<$l~ÆeàÒ “ Üµ&ôô‚­$Ó²Y ·–¬È2¤U°·InSð;Õ¢FÊz¿ó › x(Бlh¯úƒoèGÙ™›·FàîãÜ23ZóñÒ…"kd9)¡¡ãO³ ²CXQñ¶`NGº=“î¾Àxí ʘ‘¡õ®h¹ãL0ΑuGY%¯º^.ËŒ,³l™®®ßSkͤ kÐÅ·ÀHÚ9Jx"M²Œ$ +U@a·N"°f噢‡ÀŒ¦ª³=ô†_]t'½Q¶§4éýœF½Â‹Ã‹É~}Äí€ò^ÃÜ4Çq?|ƒ•µ^“ÖxÔ;÷¥qâò×L|E5o3WhÂhvÙ•7î*Ö É³+6ìµaÄe”æöN1§€bÆ>gŽÝXývÁ´Ìd@Ëç½Vîî5r@ËU£lÙGW@uœcun¯J•D‹™Mˆ3¡ŠÔ8o΄îx¨ÁÄ/poÄu½Ç¦Eæ´ÈñzÒF ë~êáSB… 0ÁˆIt€C#µ®újHà—*v@£ÝêÓ(}½ÜÅØŽÒÂkëâP`‚ŽQ¨âóºî¿#Ý:<ç3–8Bù”[ü¢HÑ_«eÊ“{)Œ6èÖ„¿ë¡§ù àåÛ‚Ð î•N+$¹ËE®ù‡” 5!ßÌ·ñ†J÷7T‚¾8εÅQxœUË̼mìFýÿÑC¾WÝv[„æ‹^9LK`œ]Ö‡¶Aè¡h ÕL +ÖH·hæ#ž‚™åÛÎ5Ù  Âz¡B3ê*4IhA ¦¨{‰3¡”¶Ð;¦‡a%"i;„%™ÕûçD®åEÿž—Cž Kž=.7Ó—,zàE +3;l(Šþ‡QÕÍ~ Aj¯ac¡8Hep¹!ãF)òP„ºO8L qðrµ¥½ôJ|â¼Å'Ï·X=©¢×5A±È1ÍŸÇcmû±>¯Ù[§¼ƒ-«;2®iîkBœ¡Ì]2Χ"ŸåvQ…¢²ûÞãç]ò7û©Èó,ÿIÈ +¦„ù—Ô,í{o˪æ0aÿ#ð1q¡(KšîÞa*½Ób6¹ñY±Þ?B[«nm­³%üèEE…05Gº°` ¦` Åù+ì>áêKM Á•p§§Ž‚<…ãw­_*vÑK!NICÓàþˆ­MÕ*Ùš(ÜXe¶í&=äÒ#D§PÁK¯3¹záZ¥ÌCqªš:b=ð äÈ<àü#z¸Ý +/‚Ò¹ϯ¾išåÊÊpLð¡RÑw•Â4ߊ’{@jwŠndýÚ=ˆI†­5½¤S¦~ï…j%]O'Lœ¯¿‡Ål»Þƒ¾ºÑüuQ|ïñóŽ`T(¬FÚå‹ã Ç®À0„°Œ¢3ÄJ?{¤ƒZülS§lmR)Êà¨Ñ[°Ì ¸¡jk~2 +'™‚KÁÀüÞã™(ÀÙÂ'ªŠA½r-¸¿ÂôÕC 1áãŽB!©øbòHg2Òußî·ûXšÑ«Aã`ØdÚ¶öÆ-*9=He9°Dä°•Îö(ŽyÎd¸! +ò²‡¡d‡ už½œqO¶}öÐ]ã|Î}Å /n!°5´“Ø‹´#…DÍ^dÝ3°4 Œ,Ç8zêÜ›8Ó¯ŽãÔ0SD²ŠhÜIЉtÕF +ÌîXÈc½wÈû®Ü¹õ-öÑIM¯8yq]sìÐØž·Æ$5¹dc&ÄθÆËI‚XùãäÕ·ÝcHXü{8ÁŒ@o½.Tá¾:š[¤5øÃP+Çq‰¸YŽëpQfÄ_àh⡺yÙŒ‡kƒ* Éöû³×ßíëqÏÔ’îÁ® ‚ó»»l²ƒ”? ˜é¤÷[c¨¬Ì¢Ÿ=¨¥0Ï’t¼Ø潜3âAƼô`D§v$ñ®O!,ÖmÙ7ìÅ/¥²Øa'×7„â~måMz)¦f§4€$¹¯û0„b¾÷x_›#KúÞ G@µuUÂŽø°æ«”žJÌeVäoÀADèLÒ™"l/#ˆQR&/I2W%ÿÊTDùŸ[3¯É‰:´‚ÙIÎt [ê¯9AªÛfB†¡îS36ˆÄ<©ù“TBÒ­¢§8w‡lýZÃQxEp™’š¿ßE:•žÉ Õ´ŽÁ* R¿…:ËzM*†åˆdðš¬Ù6ÐËSæ €wß?Ž”þŒ CbøE§0øwÕµ] 1‡°¸+iÈϬ¹}µ·ÆÒj jÊ/¯Ó"ë5ƒå°Ó ¸oá–7zEö Rƹ@ ç$‚ÙJ+žJö2/Ú¯â] FRáw ;oßSÛêVñ%Èú«ä8c®y­ó®ñÓIŸƒqìMXm)Qr˜zbEãÒT&@“åª2 +ÄÏÐ5KC#R´ÍÇ¿M»Ne®†Á7Çâ»[Às +D›ðQ‡%S‹Ã»mŠÛÚ³«ò£Z’Š¸ž]õUN[ÂÊ©i8ð±0µõå¤ð/;ÌÊb°Öuõ ÁÜ«d°BJ}j4ìñÈ!¶ð…¥Wks÷ +y¶¸LLPè}Ý ‚BÑšbÝú ë¹Êå ´^{¯¶ý¼¢¬Ñ¼¢è×JÀ¸Ø^ó{±œXO™·ÊŸU# jØpO··3R].ˆÕIzDŒE˜Ö‚J¼»LkèDN`çzâú–¡NÑ‘s°{õåO—JãÖaêNÐ.ò–ÐÄÚ‚µÛWO„9$s|‹'‡ ;Õð+oHQUæÀÏ¡ÃÀÐPèR”\Ù/S{#x'«Ô¥úÍxÔÄôâÎàØæ×Õ¥,JNØ@=†I '‚߰ຎ›Nf¯´ƒ©²F׌¯›÷Iè÷’Ö±ÇT‡’™Œƒâ"<ÔºÛÑãðÇfBèe ²gÙ”."?é(0Kž°ña + +ƒ`,²H»µM êOFÖáà¨f°¶’2Ÿ¬‹{“µŽ1€ÒPæµ(³.»³o¶Ô ²,¼#[øb Ÿ8·¤¬Mrü‚Ûbpxˆ”5õˆÆ\q'­$ר)—õ„{Z–¯Â¼(g<²‡z˜PÑŒ8zhè þN$H™°QÜgÂ^/qW<Å‚QýÃâ"&¼kCˆtñtp¹uã/s„‚GÚd9 ÷ ìÓtE¯IÎ7v ܘø&BZ£JIË¡.IŽ> Ò Š§Vr85Τæ(…§ðŒ eìõ:hª­T +t¶ù9ÎDa—MÞ.hFðœHâ]s÷`Ì1µd2@Q’¦Ïr˜c,®·*A=æÖÄL¾‚‘‹èã +ÀÛˆÆØ¿S` ׈uŸ9£l"£5¹P|jß"?WtIb›»üNXÅøE e¹[Sò Œù•5¶:)ÉzïdÐúnSptÂ7–G#4Õ-ä}­])ÙR÷ƧkÏ”3ñØ8é¬oo•¸ ðiwä àsH,®¨2Sé…TEúx7Ì;2§içŽ UéúĤ H€ËI^$€˜ÑWx".%2” yMŠÛÔ;ÌÈÜ9r¹@äLË”„-Ëã¼RYäGsœ"¿äŒ´_ˆ_ºEf:ÉÉJ‚G»½Lg³9ÊP¸Ì…˜ug­}C µÇ˜á¢6â4þÎ5ÒëV7òXSh“L‘bçbF&€—†v0.ç•›Nö&a +ÍéñZÚo„†lL-ã”»Qã½Y×Ôq$â‡Ê4'+6_àeã¸nZÒ:-ÎLÆ€käPiÚh¢Ðã¾á°†þ5%ÊY5i¢”F_e©²k¾§äg2i0Í"¦IòÀ ÊJŽ œíõDüÃV"exGÞ9%²2;6r-5z"¤à8P1;•Î{‹1L;ßoaC½9x‚çD ®½ˆYÃÂáwx€‰Øà +ê“í›C}#ÚY7 Žlø‰3pÆ#¸ÇñBµ\#aŸt<]sI·/o¢¿mŸ»G!S- +û‰É(€òÁ>ÓxÅ·&> cWélX¹Øq Ì?Yæ–2C:äU˜Ú´\b*ÿ+XãŠì>š¤<¨@ˆÈØFèŸ ÔŽ=R2ñšVóÁ×ÄÊ«H«}Üá‹“aq-OkT?8ýŽç¦ 6Þän#Àùª4Êú'«º)ðŸô_4`u‘uÿöl<(Þ£$¨‡<ɹ¨ZaŒ2 €+Ñ +zÿ Q\½®oO7ýgÜb+œQT®Å~~-ps2UA°êrâ´µ…­*ÐC‰ðn-pG«ò„†Õkxô"—Xø­ô7Ài«ÿ²G8S&•ŸìáKIÒ-ª2}¢Ÿ±Ã¡»mzUËË8`Ô¸ÞtTÙs¨²cHC¬»J‹·Æ—Hk=ä.x@Pî²CwçÐÚ}×NX×HX°µ¨Û¡°ž úv."ëòêãzê3¼ÊfôZؘË×£ºÑc¦Wµ¹a@Û¤e¡Ò CŠúPQ  ™õ#ºÁKÉÝYQ`B@–Ý6õæae!TlÖÁȳgÑ;¸[by Èp&¨¤"5z„ƒ ”FŽÒv5<»šö1”nú$›Ì†Æ׉­Z™[ŒO¬[ ~lÚºÞYäíÜäe%Ö;y’ô Ç/V”-Jì˜ÖJ¡¢jEd•[c°jœ·{Ŭ­õë5kTŠl¢óSý»o5-Y£W×àBÞׯ‹OXafj{/Ê-#ª¡A{ ³Ub  ;?‡O‚ƒ ++fŽ£º§í’‡‚nd®öÕ§¶OÊ“â¤mÿ*2ªëWõ|èXHHhfš£Àß×ïä!1¬õ “bªáªV ,§q=”ÓU¡+0iM 8¹m“tØH2Wö–!†qàEõr*þ²3j­Nó +:íêÀö[Xt(BdëL°- ‡&ñ¨?D™Ån`oc/¶ë2QTñ§êpìðzF/z¨æ .F”Ù–­\ïµÁ– ¼e™Ìxð(SÐÅ+`0âæR"2ü¿\h³È“<ú kº/7ÛB6ö–k¦ÀŒ[/úš#Jp-»Ú—ëW‹?PŠŇõûë™Ç®´V>„°¥çmTYÑÅ yv`jºÀµ3T Ùöç ®{Èð,rÈo±C‹1pÇ@Êñ­ªHØ/{¨ÑFz³¹Ä—ðiY=òÜñ%7&ÞE‚Qã’“ˆP¤zɺ®‘lZñK‡ç|Åïù«Ío¹` l ºîöçÊ‹6lZ¿l½l<Á;ìAå/ÙÊj{óˆg‘õ…ɽ' ×ðD ¶¾„¶mÄ +¼?ZÈܱ ¾Pe[³Ù8}þ1çy4ûÂ÷šb|£‹g¨§?òŽW¤ƒ+I¾ÈÜ.¼w.p2”íuçؤ$ Ëíza§Ìˆþ¼ýèLå†ÝA>å4UɉÁÊÌ!ÀÌ¥šå¦~†1$„cªÁÂS™6×|_ñ½ÜìÐ@Ÿã +Ku%Â17û&›&EG¨‘§!6þ´àÜ·6§òHe˜$(á#/Oªbl,5R +¾éâðÕ yœ+KÜIà¡Î%è7Ëfsl%¦ª{h#-#§Â1©×Ã.•³Ž g˜e†/·E;PÝ Ö4p¹ñ +\÷Þk åLóŽ9Ò‡Ân‘è]ï]’¦v›Í&‘C¶pÍÛL€lk63’´Q€ñIc «ûઠkÙÈ8…Šº%~7©oWB?c›Ñ„ñØØ:9‹²·Y4æ‚=XT¼!ÝA²¢Ø )÷.]zjf‡hŒ·ÐÍ~C`U +)n|~ß ¿=Cfºœ< +˜@iy¼nµ4÷@Å ÿ¹îd•"£6ω-(¦: ¨a‹ìQ /Ëz³$Ód0Lù‚dÈs(½N KZƒ)È©>JzîWÚÅš¦C.–Œ?Þ7µL©ŽÔ2r | Å'3ð"'ÖýH 9@¡*€Ø.!'Ó}ð©‡ÖÛjX+bõ¡ÑOCä .”ÝÕ,yœch´hµÑö´‘Ì) Êò­ æ¾çQ†ÇVÿ ¿*UÃëõhr¸ҟ2HrÖe(MU­çÉŠÐl½E9.è:¢H0Ÿ#g9·à *]ÔT„9° —ëõª²O%·G4LQ($·I ¯W¢xݹŠNM%jò‚#4«(jŽ!-f!sy¤D@IŸX©É¾5ÇB;ÙÚäá°¦HM]xàÃÞÁÑ[/¾é×"äŸar5ßœ^òÉP aWÜØß“µÐ±y ]›7ÜjäÈᮥU7&üØá¶oO ‰’÷õùÞºXáùENa_ø 1CqÌjqjÌR’23WP?LÔu;ˆÀdoFÁò\ò ¸Y·F+¸P‰žmn^ˆ¬žÏ!€èaE[Ä­»Ä`ÐrÝ) pœµ%"kèŽE%ÇI%,GN$xвI ’©Ìò‘؈`?˜ržîÙÑ”lV.w1Hʼn|°õ™°¨èƒ,^Žœ®¬ˆM?[{-t ø\Yša¥Hh®@oR1Üræ$ˆ†\(Q‡Wôêk\”P/”ž’q:®ŠÀÇØŽ´.‰Æ¥Â^,5ö®8íŒéýç–m!©JùDÝŽ,‘å„/€Z0¯X¶®H"?Ãí1ͬÉ[N&¦¾õ¥’’;Ô¥S¥àû¾äïÓcÛwÆ4¼ÞIö‡ž€"r¾¶Ê:õÄSñçUœq8@³4’²ò[îGÐH¶îK?Öœt}Œ!h| +²]­~Õ2´<BXM:r)eã ð³gRÏ)^ÄõÈ&' +8ðBOT'-+W ¨¹‰÷ÀHsFÆz⡸ޛJÐTã¥Óâ“Ò<•XžV@LáÀ¦Hô†§Ú +[›úwW¬âë$ßä*ž…ÆШ[m“ÔIípþÊñƒ[H´çD±|° ¸Žñá،ɊBÐ*›)a×%yšB‹à›tIö|Š)Ü•÷íõxc‰ˆ€ Öá[™r*÷±hs;ì ¨&ÕˆKH\ +*>’ €ìšÂ‡8δÕãJݤzí¬x Ô<‹;ƒ4N1ØéZ.x˜Cj¾‡8à0g¤ÀpSüuÍÀMáÐ;G u2¬D¤%[ RIKÌQ‡\Ù[1y!NšHGüÖ1µ—‘¡ÎÚéÚìVœ¬ ›ìÃÁË´kApßA5_(ÐDow‚$…xÉâ­ËG®AH9 +ƒ¸šÝ‘_Qo›²r›)b³{tSX’yç©AJÀ-/qú£Šªúƒ²Š¨½qw3ŒÐð½ec¬Ó=Ëb£ìÍxX¤œÆ ±¸¢–Û†ö­†ŠÐ) Ì,689kšÏÑ®\u NÕ«Sq#)Ù±Êê‚7ÍF¡G‡}AXÔæ3ƒ‘±mÒn£†MÒ" é/¢]ÔõB´ 2£Ûóza¯½½{¡4Mw®CCø¯bT2Åžì¥pï7ðª'&™N j: ê\igp«ÎzÈÃÀˆ¥nNuK³”<€ (3ÉVG-ŒKèŠ%ÍAw%ð(|˾Š©j3m]b"¤F‰‰0ùVm2Q²q·s”ÖÔ +Æ‹X½Õݳ"ùþNGLˆr©xÑ ¯ V„€%>²D ãS²ûüÑOĹ7xë•ÁT¹ëHQ¦¨â3´P*§W³\…·Ïêš!€µ~_‹¡ :Ü×¾cTUÙuWÀªô@ªxbª‘¾ÃÎ*Xá_…)h+zé4`VN±^°xÚ¯ˆN‘F Kù¤TCb™eðJ2}EEqM“*½y (¶ÓHZ³Üw„†êK]e²ÐÇŸµ +âÚRC®ÞÆBÌ“²‹â‹×Vä#çi´nt½5Íy¥ob‚¦Ý2ë›ÕÍs&!Z‰ÉöËœaÓ‘ÇúV-Sƒc<‚MÀÒ_A¬±] Y¶=ÔìXóÇ=”œ¼¢MäÊd?JlkY zoøœ\öf+x«‡YFysóS)* ˜ïé…•´âûËò¶RrÔÈÁ¦¡t+…y˜.ö52ÄkÖ +DzDIןp‘‰ôúÃW;°Ö¼àì†@b‚C¨nó‘ºb&tð[(åë£Ämñb!ÏD(¢0Ç%¶l¦±µA‰ëÙÒ-mà÷ïÚb«ÆAô,áJ¦>‚§Ø«e§Ôósx“ƒj«no”p ZËLh¦ØŸ!>äËžyd=¹ºÏ´¯@1ûè! MúïÎ$¤…´‡©^zmAOv€¥å‡q5K¾µ,J⣞#,*?êp=¼=:¶%S¤Ãw"TÛ¶µ2hcÓ‰3dïÅÛ8 ÚÑé3àäÆÙ¯X¶ŒæY,kVàâ¤)a‡Ú|@AkzÛ]`ߎ>Õå³è‚bÜÄÛs7#pëY6Æf2'ÜíÉQÀéj‡á7êŠe ‚4—F•Æ”‹Õ$µZ¹¾F‰^Œez¡Xðwÿ&Hæ»(ª°ÿUó¯9¿…°bRQGñeµ´-dÄvÝ÷5 ÞŸëØÁ€ÈwM  ôÞ›)–ö¼T~ÞRÔcÙNðø¸¾¬ŸB¦ŒtìTÈM±Ä¥J Ê!¼©íDx¦+“ZÞ.ëÈ'Îm·Z}ñ«ƒ¬´Dg_Z©Øè¬.€³!ý'ò’[Oeä"õL $ˆ‹ôZÃ&3"lMÕ™‹Í®p–á€téÜBVP8±À] ûÚ9î2 V5&"'„Õm_¬qêßïMœü—‚ +ŲQÁ9<¯)i¿PåfôD×€tžüÚÀF§€é5^ßÊp20”Râ]ú–Å\*F¸ÌT3@ó‘ú‘‹ÑÛ‰´ã@#ØmW·Ú^1%j³É´MÆá–ÒÝÍö£x‚®òpiZch î†Xú€1$¨‘†#“†ÁQ ÿ5Êo‰tùw¿ê¹ø¯<Ї™aü“ýç+[AùåV´á÷ür¼Ö Dr7 ö‚Ü›½nó©ï½dl8u]°ípÍ z¸&òùåN´ì¸ž,[¾ž%|*(]”äx±TlÈ›/–e2D‹­Ú ľ}?Þ±Q¥ø[¸Q +¸h Àfê bhÈoÌýä7žZç"[D.óÉUs›€ÌõµFf$êä9'zv5`fÃ}²žm;‹d·()N‚ÓD^r…JÊ»2cs…%t'˜ºŒÛPö¸c—ÂïYfd™aÕ§ žN•ä(ÝCÑm‘²¬§‡2ZaîºÂ%+eÁÒ: ô}*†ÀIj•y?û²®Šç÷ó©î²·™RÛ<Æ%ÏŽ±¸ósÕ§ïËà‰†p ?ÊGnú @‰Ja*\ûh`ùœ<´b¾ªçsš¦™ókºÃA~ýèbÄÔY}õ;h«R£À½¦´äµ]Î}ìb¥§Óಓ#¸#S¨÷ù’I)2ˆUÈO`ÂÇçk(ä­µøÑ–cÙhyêjAý[´!,¿_QQ¢g`º¬¥ñy\WÒ|‘2Œ™¬óJËTÝ°Mqf$–\%ÍÉßá}Rd [Ù„Fµ.Xœ [(ŠÙ€Ó¹Ö‘M-i4šÓ°æ˜äK`E£$¦ÉÒÚè4Œ){ñóq+Šß™×›ù”ÐI\›øæÑŽî]#–Ø‚á‚u[o´ÑRäk˜QäpœƒŸþñyúÎÃþ½‘$4)¹†0~1*Þ;H.–: µâ‰’…ȸ0,¦ÏK)+Q>esXZ ‹† ~1”p*h”7Ø©Ý•¨Š¶“©à/~©ÊçT³‘°žÓЊ» +‚¶3û‰ªM÷öi%Ð<³„»"m£PùNauÔÌ5‰:IJp¢`|jÜvÆä´Fwpd,ì‚UdtÉD‘‚¤:+AF‡æÝ# æiˆzöÎgÆÎnÜ' ÃÃöSÆÏÉMÁô€s̹ ÉXÐná@pýóÓz+üM®MVêÜλì rx¨¡*Ð{M5Ô÷PÖ=¨S·ÈóÁnÄã-JDŒ­oqÑ,æ]ØÀçÕÐáT!!”èrîm3…"¤J»u¹Å%%±>O%Ž†y{€×šç´KÿW;·YÍš ›ˆÐ‰™É·¹å0W3W—0ú_p?þ†c[Œ­©‹Ë,ªêj(YÌÎ{xßÔ׈hBÓ^7xI»£f®Ë FÞenð@Ýà´˜’© +Ê<…ê\¨>—Wu4zÉq^p¡S×°~_ûn€×´ðè(nUÙ\Qq)û«º}®Œ§$²m¨×0YËcÐàÈmÜ8w²ìPjQñ2h 2IÉú°ÊÓ¸2ÊnYAaï=bŸ[v]#'a‡ô))墬  Ã©©ÛMˆ§âò _ä>EÝ1±¿ Ül%róúךô›üÛž4 V¬uµaW~&¼‚éN!“)WóÝÛ3áäûœ)²ª®öàõžY7D\¼–{}§9@áÕ&Mô€ÆAÊõª$´øŽÁÁ|ìazL¤‰óµ9û=WS¤„âneÓöŒn争!ýW ÈdIÞWÏöÙY6žÕÈ´ŽemqÝ@sÈ –,žÔI –ðsB«p`yï!pÓ,Íš.Ë•^}×¼+xµ~!J³ˆDÓ^Ëñ8dŠ.õ¤u©7—Ï?שׂ$ï´‚”PÝü8ÎR­¡IÝÚk± hûDÔ˜k–$ä &¶Ãþè Y³MJË.á;Îͧº")ZXÚd£•÷(%)ñW#V`Öóœh†hô¹ðÚ†"=*‡U“$H3õkïÇÈÕIˆßºžAÉÆ|$Å«F(çC™è:@’…(Tç¹y§û¦A ?WD=fAVˆ`ÑcÄ8ŲJ¾Êf™åî Ò&·T×ï8ŒH"ËÎ ê"¬ýú.F¬;W·-¨±ÍÀû«5Le)`õûÌï½ !PÐØ…ï[ßÍÏ7ퟶ&ã`—–6<“÷Ïcos5ÌUŠ¨Á÷oý(õTÅP•˜úø¼7€ÔiTÖ ùHè|ôÒœ/æpoøÝ>Ê aÕ3@†çØìGåç¾êu”ÖP] 3 ÅJƒõçÃN@,¢ *ÙŒBmèZ¾ë ¦-òùáC¸[3gÂÇ ñÙÌ.hƒ÷Bq[ Ãá,œëÞ +‰Ô$0ƒJwä_ +Yý;ìà]s!?øñÀ/"õÈr´§žiš9îT&ëº n@ƒ=½ÐË5ëä]ä¾%RXÛ®]óÝ^úz¦óqŒ110˜@LEí+ú¾¢œ/zÜ,¤©lIÖڑшãgµGÇ«< á +õRÉ9ù57H.ü€ÓNÌ7ÌØf4¨Ï]ûp˜Í›($¶f–ò@„šÍnñ–É$;q†kÓ) r¤¨²ÃàB‘㯇D¿åæ¼½´ÿ}’1S%(`$AÜÃÐ9φۅ¡B°º[i’Ú¶/qëk¶o›³myk Šr+SªBˆ£‚Ò6{Iôëùy$÷«jýw°C÷á5VTPR¾lIùylzrq`‚í0 ‹ÀÀ‘X*Á<0Ó]ÁZ«¶ +Œ0ÁȬ¸£=Ÿ{‰ï¦5Ï—ÊŒ¤v£ŠÅÔK⧩ Òvìc÷¸=Ç‘òxoÔY¬±€¹`© ‹Ø¨0‹ÄçöáÌôŒ_/wwïe‚ÝãP~¥¿wE¬MgÚ«]ÑKU&ß°t™V0•,ÎCÉb¹>ï |CŠz,Ë>û¤-›¯À¯o9é*ÍǘIž¡ +FDTwÕã£Ç4SùuûæW½(Ì —¯Ä~:ÅܱõÂ]ÓtH“á4š¡(Àc) ÀÚsŸ Ó‘ZyÇ^!¤š«r£qœ;‚D5©°1’“F†bx¥Ð©Ý =@Û!"‰* 9Ô³W¸íÀoWs émõ`ó=ÄÉWì B]<ïè:‹y&ç°µ‚œ„ µJMo ÊdÕè°Iç F&P#ˆ¦Ô9Ú¡Ù‚0¨×g8Á cŠεËàóÚ½F<]‘÷ (×dÙüÄ*lmýªGÿa[iàHqÎôÑ z(O·Ôl ÄâY€²—Æ TH&.$3èÔ·Ä]ìõêýÙÃ3e‡ xª¯Žƒo»^EaW œÅa T…Þ{X½Ó(SØ=¦?zÔ2„ƒé5\¨´kT†•¼ì¤ 46¢Ræ?Š1lÙœ}ö8¯iñÞó¾ï/Žƒ‰4 Ir,*—€Š-_ö`¯ äÁºÑapm,ž½†³Š#šÀ(&Y’3—*ˆ¥dÏ‘É›Ž<ÕÖzÉŽ¸‘û4ÔµTdm gêõ«-Ðl4ù v>:iŘi<PÁD}ƒ˜O þ/š ù¿¦àŽú:kRÎõÑÙê1­Ó¨ +âh¸–~ôàbw{¸ÍɾÀ,5ô8zBòЙõË}='µ ÂÞÕyoÖnCëdŸ)BÓõËKœÉ¬-zLpìã™”ýL®ÝCõÔ†NÀal^±Ñ_Áv Š§ß˜ó:™W}z|¹@~ôZ‹‰òâhM#ÝRîþP¾„u¯I[ìX¦À£Y7PògÞô¾)x=çïÙÂ߳ˌڦ’§FV+Œ¸=)˜KJÂÏ‚ÄÊü°H¿H]Ú„êNrǧï5©—¹D™DÀs£¨ö˜…ñƒ ðÑã/;GäÔýñuM™°aîª(ëÇ\Œ¬Ÿt°\€Øp•ÃâRÆßš_düå#e™"º²S™»›NcîØÃìÞ=fôX‹Ûî1wrzÇÑcœ3m·âÕ«›º‘€˜$ ®™#Òj)ˆ‰Ûö|0$tehå#~=,q۪θ-¤–ƒž 6nÌÐÓ±þÐ¥nÐzÓ¼#iœ³ÃãE%ÇðV('-xd:)!ž=‘–78^Q +0žEÑP k9éhâóŽÖ]ÒšWÉ +–ÍEÂö"Õ£U:¶Tóš ïòë!Ö5Ñ ð8¸ÃÑËà}òXsÊ5ǯyÃüá€þøÌy9!Õ2B@_­AµØ1_õPÕcP‚%Ãq­½¯¨àèìËçBÅ~>†Æ%ÌªÒ +Ÿ«l¨)Eˇ);Äê E•3zo>[F]!b»²-~ûN fk¾²ºË¬UwJ!Cí_ö„\­]$¾g˜ÿ~Ñš˜”If§{šBüØ^+P½M,1'$T«L`lYS†!)^pøQÏöÔ˜=ÝÒ°9sX««)R%zˆ*­4uîBͱʬ铉 Ë`ÑvËQîË#t6iNƒV‚«bÎ7íŸÁvz°ô—/íÿj +‘\Î@„ˆ„mFóò +Ù‘1‘Ù ­oƒ+<™mùØ×cËG'M- ZZÁ3ÇwÝ}Ý]B8 2­¤q!,’„=ÜjTqÆÆŠ mëñ¥&‹Tð·p"Q¬ûáFÐÂDø‘Þ°‘ Åìᦢ¿eg„9ÛB©âˆ8?i TÐyïçÆž;&B}ÝÞY-At–¯3Tyòš) \ÛüÆO>¿Ýßiß||K²¾/ò°˜ö ±L“C½¤¼Á;cCEÈ~>÷¨ÀÊdéç!÷|Í ¡ÅÊý^›½hËN€–ï=Tv9²ïï¨f‰„\Ä•à`†,‰d%ÜnòŒ¿7/nÚãuÃ_yRS¡!!çïc¢ú<Í)â\M +ñ3ØK Ù@;~x8Š~Oúî¦øIÈMçtŠ"ëNµ!¥PÇð;”¦1–-qˆƒã£U˜ÈÓø7¢öb‰#î v³‚‚2”pв9â¼Xx_V<¶®~Z®Î5*÷%M2€•ÂTƒµb!Èî½!îÍ{$ññuìq4qG„ÁÑ–"#·fX’Ÿ=¼ùÏ ÞáQòžÅeu@¶(›T9ZÙò ãØϯ³«f§#¶15žFá7ÄrÀo0Ç]÷,°/9RåXÜ=¤ÉÏ+ØË‘é¼³bˆ¦(‹pr²™íÖ“ú« +.Õ©Ü_¤Wß{Üœy¾p>º_Ú8•ãêÚx#ejuD>z’ÀyFÀœ×NYHu âX ‚°âMgªDX[ÀB™Ù/! ¡Z÷Mšº5]}×;ÕVà­Õ¾HÌÛÙ WËËQ@š!‘ +F¨@/õ—V™ +IÀÌ—$Û*pø8i.0FàM?;òïmÀþÙÓɃ0nf E±~¯zllÑ'Ðïö4Ä49"i˜ƒ£úúiOrFè-)+ù‡~xÿüŽîÔŽÑxd«Þ,É¿.¿#ÐìàQÔ/¸©·= Ïk®µëò6œo­Zª® ÙÙÅȱ,M1^oÉ – +²Óϲ¯êÖµç)úe €),׸ÉQ&o-/×|Žbª<ïRûQé1¥ÌÉPõp4(O›Å$ŠÔƒZ³GĽ‡d|¥ i(pTMnÈ—?>Ÿñ[âP=‚ojŠ=ªò`ľ;±È}ùíj»ÔÀ¹ž†x„¾”׶’ãó-˜:šÌ­!ÌœD*Õ•BÑÞŽ>¤ðˆ){¸!"_HÁ‘3ço\“ôÙX¯.‰üÖ6P¾ß±¿$-ýØ5w¨×·þ3‘?0Ínæ%nu­°è ¾$.¿ð%ñسa H߆z®¯oÍ-å€ 3_ˆ?ÚÛçm“Žâ`om5´%œ‹n]à "@º7•lÙº´Ž> lòï=.V£ò@-@’€›µÚÕ#ˆiÔ-9܎Ș 4;Ñ{x– +ù3Ðûëxå“_Ê^.ŽR:¡5¤°©ÊÏv•øéÊË}|ŽóÒcZñÖv…‚j=xR„¯)^·;Ôõì†+“w= ñû62)Sõë[uóLØH®»Ý—´3i-p}V

Õf¤'÷ûÞØÚÖ“½s¸b”íÇö7›b;ˆêDQ%´[ÜÙÜB«RÈ&€ža©am2•Ø¦p›õJÌnš¬ÊÐk&Pì´ö§b뢼ârbh؈Ú*>Íž«‚­[W$C‘¿ÿ7¡w)Y·"ú·¾4ƒAW’ +N‰O,=Ä»[DBš7? kuØNVçz}‹Õú%w"6»Ÿ¿•ï¡Ú4eQÂÝÈôÏŽ$5 }Š˜«°/ƒh¨ŒŽ^q¼D("c‡…hŠ–8PÔde°-ãw‡ªÆöC5«pa/k»ƒÝûúœ=x˜’º`‹À_Ǿü–Hù‚¿ÊüW,áÇØ¿'ú¿_ÿ1¾ýÍ¿ýö?ÿǯŸ,û÷ÿñüßßþæ?üíûÝŸÿü‡þ§Ÿþöÿüô_~÷ÿôÓüÓÿûéÿë§ÿôûøóþç?þß?ÅI¿þÆÿßþð»?ÿá÷?­Süââîç +>¨ÿ•K¿¸¦%ê<@E÷´µðŸ^¯™UÂ,m8/dDw\þ‚LMgå²ò‘Ÿ¿¤æ[kÚ_Á®*',·_*çŠaE”­Ý?Æg{´ó4aêåXj‚rF6³/îO)ìÈ«$‡R?nMh"çðuÖC»f]wv¯y ^#•x™o̽î×ï¾~àME‡vU˜õ£hhw4(ËJ‘†ä™ßh;Á[SÎ -»BР<¿n5¢ò·¾DÑ·A]Y • ¡ûßGWÒ†±!Øk]O¨AÎ%EjÙ¿•z’ œ8ͦ[wM=v{È%W{+ì›ôY;<¯:°î›pI‘mK A·ÍOrÜæƇ” ²\3*¯½–xX°ía+!l&IëIoµ·É~”6—vØèÝÏMA¯ÏÇ î¹½®E¯Ïï‚O-qÆÁÂ(å&3ßêLbu“Ñ‚¢ù:K+qe{ÿ¥Yn-÷ˆëæ!´FX•G“_˜ÜÊ6–ógL¼1B÷G¨Ë+~`Þ ¹×¿oh!;ôzŒWÛÜîë¬Tš÷ý @¾@¸Ñdošgµëý³!ð ¼³_4bÍ3ZXhã©)¹n S𥠚Žå;·²µ&Å} óÍ5ä +ñZeä}9°•“0îQè©F}•Î 4‡ôa  B0‡ =åoÚ‹\@ 4¸9·C£]S†–št•x}~ETËF¦­×n#@¢SÞ ²ÝQzô ä»^ôŒs'ñO’ÐýÞà+ Ò(õÏÆ$–0 ?/É|ôÅ H Ê’úc"ÚÊ®CíoEîM öçCE^X<Ûµg$a³Lþeü² ×ÃïXá´F¥Ï‚èyÊÅlË™uî-ø:q¢%” æØX)gE§QÜ¢öR‡­rPV"Ὠm¯‚T)9w˜š¨ÐÌò&Ve¶mwˆ×Êr: ÃÀByßu"£mlÚçÊŸŸs4¡,å‹FL˜ú ÝÞ\Ò³þlrŸlfZÑv¤]±†Á^x_çNx CfP4n˜Aˆÿ£'¬ÆQ‡ê¶C»¦!t}[δ‰^ŽXv’︲«:Ê^¬lRD)úŸÙE-_¶ÀªNiƒ”×IYÕýrÅ‹êÞKضPàY¶¶Ì-ÿ™‰†·wh!]4öþƒ)@'SÄ%ÝcÞ!¨ÛÛ¶*w†á†3Ãä'Hn`›•J¥ÝÑAö.ȼìA3ã&¢Õ:Ün¾):Ê"cEèvlD‹b€ÀQ†³Ö —Í5_ª¸4x\ßV¯÷›B8FPÅzjµŽÀ¿¤ú†)HaÓ®wPÎíÚ¬yÝ|¦Mð +M² +èšzÈÛ¡YpÏ sè°Áv¨=4áSR œ"{°Ž×i‡Ð© .ÐMæÎÚ*jID(TzPŸî`t¦"a®AS[oÙ}Å¥nPßÚ§o&3°zäK9U‹e1py ¯[˜>íníîa ]·ó_z ~<¯ ûa<•Æ¿ι¢Ô3Þ$l“(Àu=ÕåÍ:ƒßr87\áÀ†Î dp8§4Œ¶—&Éåêo@f¨ eq£=ÀÂR¢nÙ]ø÷Éo<> LTY‘Ü5ûñ*Há&ö¾´ 5 Q +ÔQ²ub­û4¸ŒPRTì)>ª.`6c\u+’pBÜcrÛUvÖ +‡ÈêÕŠ”‘Jzæ’oÜÐÆØ@Cº¤¾×gÎYâ>È.(¨Æ„GQñâÜ(ôÈI¡Y@TäÐÏYà£OµóSÂÁ2D6:¤%}Í?#*Xv"úùLàš2ñcuÖ|â*Ó-ÑÆùÛKÃåeÎç3œßHÇçË×ë﵄n$øùˆ=üôSÅü‰h§f3`ÝTÐ? ÁPV³ñYó&!8}Tˆä;\©1òÎ5‡ýÌdé‹i½æŽ‹?«ºýUãõe^÷•KëŒs šý)*Ïß=ˆÝ~g†‰¯þ(×Ê%‘zνþv¬Aj×óÙÍC¡¬C›ñüm‚vÓ0Îgž…„½€ ˆï¦~j +XüÀüä‹­äÊ0¡ç- æÆØ:·ä&á‹“Â&ª;Ã!‚'"¹«Lq” +ÑÌ'ñê½Ìì™sÑZئ¶Å/Xž=€ˆÔD4Ózê{ß$€¨³*âA!hlUbïIVT7êùøÓØÈW5ÁÂù;¦°4¬À“ЋY/Û iå5"ðYaBa#{‚wÑ¿¼•?‹×*K¥ÔÉj@‚žáa6Àßš6q0MƒuséŠs1-]PÆpf"ÿÇÔØb’ûr¦=Ñ ÜÁôØ +9^à?@¤ æÅŽ§Éz—õÓmô’ñ0¹ð4+âÜî4YH<#2È1÷&º21×Óû6/F¥Vݾ(S58‡ zSÖj_í>†’Fæi´SXýØÆ ,çpezžKzÇîÀ^@C„êK)ÿÁÑa{ÉV?,Muièˆu=e«qQÖ#›( ¥ËËæ·¨? +h­ÆQrÉR7²ê~fU9S¤é4¡b©ÒJîÙ4éÏ'K‡^ù3ÖcgAMßù'd)ÿ¤BŠ&ØN\’v>ë.ÛHš­¾nI—aÌ/3RßG2rZ ò8éõ,Ñ}D¥„4#ÑŘ_Žx4Cy>&ëÐé}o r¶¶ïm0Æm‘‡¼0‘F\CÖ™Ú1†Z'€'®C@¼¾èá{sáÙFÂz=!wÇ”BT·¶‰3ÄVK(C!‚õ +°?ØÅg'œ •³#9p»¡ƒ¤C ÄÖ/RûÌ;¥£ É«Ôu\éPôã½GìÔ±ºâ5RßzñR<=K;ÒºPQ?Žæ †_4J¼ó°q$`ÙU À-Ä…ˆþÓàe".€ú;án¿_ðÈ®­5v…Õרë‹Ïùì1?¯(ª‹¯Û†I1Çʇ€Âz«Y˜Îççáv´ëõ¥K¹AìrªÆB'|t˜p:ùÔn±ÒRX®Ç›JcbšQ;³×e5¬ý¹©8Ñ5Y3Vv-UX6}¶i[)E›0î QÓ…‹Â,ðŒ—õkN4¬áÅ7H~ùÇm‹ŸÔÐÀì¬kjôÈ>æRûžkX㘫ÆÝr„¯FW£Ìçóx¬º`S'Œ|5r»QˆãvORyèφŠ ɾ¸Æ]kv_šœ_bÕÂî®AÝ$B÷Ó?‘@†Ç ø.¯oÝÚ€{¸î¹uüh¨‘;öÞˆÕñ¬r ¸†ÞFµ±q˜"Ü•öï>?#—¨öéËàhÚãúo±qŸŸã•ûì¶ßUÝ•z1ö³•ümí<ð®ŽJ.ÚUÍûiˆŸ‡S<ò’ë<ßÒçHM‹A=(Ö\ˆEzbE¹/õ‡”BôŠ„ŠÕü¸il'qâ!áqEÖÝ ë¦VaÍ°xx¤y¨]‘ëUÿ=ĆÂ#p5P’¦J nýüƘ6ÜÂ#¾sšLÔ®¡ÜñÚ¡ð­ñZéGyîZ×F†Ÿ¸­­Qž¿cjüܪê{ÜWçà B‚,GR&¸a~4ì{ØïL«;9Ây€_²­2zä¢ÀÆãâ:¶ÎÓx8\±+1nÝäzî@íÐÐ1®mí ¦ÌŠ5ÞrÎa«u¿‚“ó²|Rlí<«!!»§¦ÚèûŽñs€B“ØM±±‰Ã![ͳîaÝSà»Ë+nl ’[büfP}ÕÃÝîçtþ¢±é9D^ÛX‚„† ×SíÖWÆÊj¸ú«á£®,ÀÓª£A1 ¦¿;—L˜x_è7ðƒØ?Ç’+æ¶^•Œ¥ÌØ71/#pϼÜp¹ö¦:SýâýfG}}·Û6e ” ¢µÏ·pñ%d^*f£‡(ØGÃ/¦Ø÷F¨pª²ðvJÅ­èÃŒ3Õ”é¡n=Iò†*ó׳Ÿ¢N¹(½`¡&´ä¦5Œ`¤ü²`š£!­N¢Ÿ:î®#;±=#ÓM4“*¡¤¬˜?™c ßr¡Ïçæ®q-`æÁcaoýýNPª¨êZÁ‡€hÆÛ"wQþo{"ñÑIñÑ¿èFuìYÇ¿Ûþ{ÆÀ…^kmQc= ņܿgJ×Z~«« +>Çïx¼Ágb³1Åu3Þµˆ]VR¡Ä6WœÅ(÷znÕ ž߀gȳ¦ÓnÊ€ã»ÈE‘“ÏL I‡» œ[ÃzÚpwÒ@n„ \mC$!8¯¿ÅÞðRÝ8gŒ]„A'ZY$Bo=kåˆoH¶éÞ·–f®¹nÇ4\ð£EAy­$ÄCwý9¤ÏßwP“Ù‰ý°Ð„‡ÅĦjÍðw‹’±âÖbn27ø-r·(+¶y©ß³ +.3¢…Mˆ cÎãBÈTï½ËaEÛx”pׂÀÎu; ¬µÆiý¥d©áÞ%䵌li:­y­\3+gšQ‚5íÔ_ñ$=?¡ðÖ;¬WìÁ6”WãD·e½k¤M, W'M éC·(/'‡sÄqôËq8'o:btFC@6&Ø@m±Çë5Å:´äM|žZÕTÁpl?B ä?•Ï™íÖx‘µ½@ÊšIípÃüÃœ~î˜!+«ë×$XÚ½ª‡Ñ£™nîWÎW>p6·Ó´äK’ØÏâ €<—B‰cïk¯ªSØš +6õ'† VÀ” L1_óòšµ  Éö’ý²¶A²†r +ÖÐx=l—fr¥ÆN%ˆº7µ†ÿõÎq”"ê«!¨ÜíQŽ°ëiÄûÞ w¨‡¸ÈP;OJ‘T $X^õið¢¨ÓÍ×zÐë¦?ߪbëá_îuNÿ£ uÑŠ¦e‘ݱ‚¼÷Ÿ/kª€£®JFçW«°8¡Šò€e¢^{*òñŸ=âq±$ñ:‘3/ã‹ãøØyuÒÖF.=,ŸØäS€øè!a¢n»§-èðE¯"ÿÓûÙÃR)9¹$÷“År×GdªÖÍŘ7&äç(v¶S®*š0$ý–ªÀ~xÚjŽÉ’L({!:’cò²¤ŠhMG‰œ‡ê×Øg*aàc¡ô9“Ëàê%×ëRFɾQ´:B¢ïJ±ML¼·æcÞ&%lÑé04"¢Hz‰gW®øœÈ7¸2~ð-ÝÈrü<5ÖÜjzw͉ÅAjbsw”§!žÀÛÒþ| ³-ÁôEq/Îãøhˆ'Ûö“}ÞÈ÷^Ä×Ã&÷G7B.kÆeÝ› +§„‰‰c©”ÈñqÇFø¼¼wøy—lÍâÃâýqJ{¬„”öÔ a/Á¡Êè' r~kC1PÙþÞR[T‘º„À³’7»…:%oH,õþñy(ÖÔ­X³7X®í»ÖW¢¨H¤bòÞ¡kõ™:ѳg&N8vÄzÌ+²’X7Q ¾iY9xV†õ±k^¨ +ÇéÛ³‘Œ±–Ýlzßj.ÈK¥×™bÍ#_ ؤåTÛ‚¢Üv‡gA2€ø€”m»Nò†¤¤ô½=¶r4yª=Þ‰?IײÊv\)€¾ÓýfÊÆ{Bæð¸)fµ/z vâPJ>nŸ½ævPFÃQ ÕR"¾.‘Q^‚ „¬ºÌgÃÏ;ô)Sb4IïÏ^kçsÈV5ˆTÖ¾ïÕþéùHa‰²…%.=WÜT,´o° õŽî¸_Ñã~ÞNß{<3BHu®qhhWä]5¥î°´½ˆáGUùÚ÷¯Åý;?tj³Û©ÌÓ+n×,–sÀl­´¯áŠåFZèàÄ(ΑEç‰I4«Gªçv»&+Ö$Æúì¡¡·Fîœû‚;Å™*‘8J „:ba·nW #³´æˆì!Žö07$N„¾!OÖ0ƒ×Lþn³9(¾@'éñÚ'¸¶û«[ÉH³‡|&ÿ÷^ +àô–]*ïÈL(d×cº¶“à™qÏëãs‡Úy~´¡‹À’ . +i;RçfÞ¡›J±7s.‹XìÛág„gû€øàL$ie—.¸² 1 ¸dnûn ¢yö¨)á0²@ ¨©DÌß±âä eßÿí³ÁýUŠ*òG£45ZHT÷€[ñj¸7h@mOßO#x>Åñ^–}sÛxIî ´^¿$•Eé”áváÿÁøÉíœHjÔ®“g?j–¸êe¢;Iኢ]ãï,>î¢xÓ”u¡áË]kkO¬ý÷»È²~ +=Úl‹J]䱿`ö¾`¢¬¯uH5øÆu4læ“Acée¯‘WM±M^óL¶–ú~}¤xɈÁØ2œ‹JGì³5Ì>ŽŽÎ[ã â¶R„jl2ñlG~%»‚Þ?ÉþV2‚F}óÜ°h$ÊZ’jÛ„ +-ÈÆsE}».Xj¼E¿ +d8H)$ßñ)ä7|OdÈ¡ÖÈx$XèªËjĉPXÞζ˶:ãq¤ìol.[$T«qÄjˆÔ’øA®ËmKÈǺ¯â¦G +[9e<´_Ý%MÄB™zËØ®±cIn2oþšœÜèÊx%†·,–Á´ÓÜõ@˜|ª|¦V¯XâÑgÒöë=PÚžÔ¢{ 2æú˜Ø`=í1Ü}¹KB•GÿÏ×yêùkÀ‡~I +´2飸^+#0N®ã8³°QOc# Èø‚:צ mô/ïlQ߆q’êš4ŒZ0KŒ‚ÝšÐÚîUqîÈál²×`›N#Ö¥ê (:Þx,7è+Ìw,Á¹$ÿë ES]YŠí«•ì‹¤sI(Ϊ‰NîãPÇÑ»¦íðOÔt×ÚŒ\ÌTÆ‹-Å7ªT:B¾—ÍËëZh®FmGqÇ­[?"j¸>m–‘À\@£Û‹Œ°_'jQ|·„; Ó +#úr%nǵ¿SÙ“ÇŽà[_;Àg oÚ3ŽcžÛE­ví1•c¶È3“®Æ +Èh{ž£2.ÌÇ/µÕv䬇ÌzÄ ~lðã¦%Ëtí½ ´áxëmlÊË + Ø~†Î{Êw  ¦~ñ°¥-\DÛñR ±özëîRF¡bêéRðûjýÿ¬½Í®,Kr^ùz‡3”4(ù¿{ »/A…nô¤ÅQ(²M!± ôÛË×2÷ÜçfîâE—’uvXFdFx¸››}?w.§ûWÙ6œ™Sô²™èY7iUh†€ ú°E )|ŠL¨ )fŒ +Ú¥£Ÿ,êt/«’ðøy…9Ö¹â`@H]ÅS˜-,3Ö5œ™æe36Ö6곎£n´-[ÀBTþæ&‚R ½f#2e™&cã\@99Êá¯&¢Âþ© ¤ÉQ‰B„Ç;¢5þÍ,ìæs¿‡{èáîsÂ6˜:U5¤¸ÿñ,ú¸NñòpznxGÌË‘Iš(°ƒ§æ'–ta¡3ÜwÂöå`f€¨QQÇä3ànO1)¬¸ù2ƒRþR¨Æ¤÷ýçà +R¼jª¡ãÃ׉nyòIÙóñ)>¬A‘¾E'€š ™E_÷öý +³ÔC0’4*õ+Øôãô~I²ìfB΄`磯'M•r‚¥T˜ï6“\§ý& —õXG.—‚rä®'ûaå©Â³Œ’Ål×HÌ%È’€×~¡qeëÍjz·—P‘ÙZÂ{À””ÆéãeP·Ž³Ñ¤"!\!r×X6~½ñlqÈÉ>«¡@?îâdmÔ=èÒho3"¯ýv€ýò;Že`OMæ³÷¢Ê0QÊøÇ88¦5[=¸ GILRd¾…[‚Iµ¥”¦Ô͹ê©(V¬K]ˆRF–MD§¬é§d%£*Ã’:±Eëâ&P/ð€ØèÌ[„)PĤ¸F¸ìÁ^ £uLŸb!)’Ž˜¦ž Pw KX` +‚V†wr®Üd{=cÒŠ_ü)Eþí æp49H݇R2Š‡Îr¦‹íø|1-óî9Ãùw­7pLãE¥Éˆ$s°"øÅK( ïêGÈ6‡-(‚Ã7ì8Ñz…­q„ýq*BÄ)Þ»ÔQV]C“B€\mŸ¥¨å6»¨!ø¹×¶u ñO9…,<ë©YâQA•>H“3ü‹qñC™ +õ›}`ž¬$ôio¤{#N•a•Rÿ “;úÖŽ£¬¾“”dz¦L].$ÌU°·×4[˜œ$Ñq7¿Ò#ÍáªQx¬²–ø«ÅÞ=¦h;“â3ß U0xñ +tyk Œzª`%öÓ}ŠÎû í¯e’)À­‰q¨Ë䳧LÖºFß5¤Ê,–Md û‰@tªãBÒ¨Ôe{ú®ÕïÂ85bá"ôÕ»ÁH˜ÞMñ©€¹P*ðÈò<öš§šPóìظ{dâ6R°qgë–œ®÷Kt'V”åD‰´#Ó.«xÆ7Ç©;q¼_tpñ¢Ž¹¾BÝüRÈjTÈòå§àZÙT#—ÀzÏ+;ÝÑg•ê(Šäœ5%3–U­YÛOñ‹e +zݪßò>·}D… Ù?ƒ'CÛ3rKÖÆ +éNK?‹ÙÓ‹Xf¨Ï§¨I¶ŸN+úŸŠ»Ò²hA—´R.'&z¤¤Ð8BÔ•8ŒÆ~á‘VAÄK6‚®bòß6`?(áw¨Ø8£I¿—qˆþÅÁ¯±Rð _¶ð~Ë[¬‡AÞëiÓàu½/ýŒSîÇÇÑ-ÐzÁá–ûGÀ}pt›¿ˆÚÁâŸ$Ú"Kš:ú`Ð{EPÖÅ°á'êíÝo’rÿÂ{žØWЈŸÅ'¶_å"¬ÛÎدtÞYÉ^×* A¾æÁúo+6þwö K[šeyµ‚©³%ÈEÒ+ÿ^Uѽªx&v-GkD‘ø©,eäEë€ðøâ=tÓ!5ÒÐuZÏëûv½ý=TpëË)²–C¨CµE-8¾àµ©â ùví/_òœü­e=±-¿CwÔмÕ6Ë­}IQ‚ŸLvÇå:ä\~!,+njKÜÿW`=A_ùè v­ˆ +ö…‡‘SPøÁ…zìÎ÷kÒòW7•³Òáh‹,* 4´Ÿ€ƒ…¯ýRZ¦ˆ‡õ–id-éˆV Â^‚ÿ*çT`£”–^ ö÷ï œøí†ùîþêIsßtP甆jº~Ýk1™/É|íÂ<ä‰Rç6‹§éS-‰w{BMˆtópuÚå[÷cxW'ÑÌô+w9Ïýåð7ð°vþáûR@Íò¢e‹uu©d< ä;ŸI+(ÍFZeè~z½â¼°ÎGîö¾•P âÞ«ž ­ çj+ôreÙa5žëÀÍä( KX¸¼éæLD™™\-Òý5º0ã7Øì1mÓ˜`EAjU©îs1õßÆ/“Ø´Púrå* p˜S uÅ.ˆ¿äÂ57òOrmžh*yJŸõßå‡zˆW@j!(jƒ°z¯Nš<ŠÚDø‰3mI]‹šHÀ?Ø_ê|¯?8 µKÞZ&õÚ­e~×bÈ«! 94k/žY °.½7ˆ*UMñêi·XãéRQ0q½)-ög+²O£OxãåÞxSÈÕûj6Å^\­#œ 6Ò,ôTnˆæx—€^´@«Ï¨~Þ4sˆM®)@ʃÖÇ~°O +YЗIuË‚AØE$±hÓ:  «–KQñözûi°NîqŒ%JUìòAˆÚņ¾n*<ŽPW>šI8ÂYMéðD€ùð0¶Ûì<~2I XäþÖ …ÎT Y.@Zƒt +„dh#UÌ·#1Ÿº·¯Ï‘ NÙSÝ:Ä™o£4Ã*±mÉW€f†°?x©Lû“K;#´¦E©}ŸbÂŽ±4ë({Ûûœó?b=€;Έ@²‡HŠˆ+T^ B´Ä½Ð€°jTvm,ÏC=¾ËÞ1©Ø)JSª¨­ðò”YLôõ +å'a]ç°sµs½¥çJ¬ÛØŸz98qH‡YGø’úþiÀ•ëFbWÒ5›ˆ?ˆH!­ºï3Â?«Jöëˆës_L¯¿›r£}BtrA•zÄŽ:¸$hÒòáÄ+âa" N}¸ÃÙ©žåû2ÕÏW} ™–t—ü±JT³\ÑöÈž'ä`¢D@”–1kø¾^ò@WW)÷ü¤ªü‰†®n”Œœè +PÛŽ•È~ø PŽZ5ÎPï -„ïè=¿W7qØÈf«4ð –2&Þ™(HCi©×:=ÈÙãBuçó‘;á]T‘îEãyNë6À[P¦Ó5E*Ñ¢¦±¤¾Xñ˜¡¬­ùÕmÆ]ù  ® T˨„VŠ+Ùê†j<¤­‘ܘ1ãŸÑDÓ0—R¥ÑNFÄóôùzàTP"~æa•b@J©p i|›Ø³[4[Ç…×eüŽ»ê iDÒbôe‘‘é&º;9xßG@Œ×_ÞV¼Ö™#ÈX–§'‚Ü3ÖX›®@R˜ŠÍ(ìôœn‹—©æÅ%hO+¬“h˜)$W/®K£¯S“Ò§"ïu9þjBFŠí¹+¯Õ—"A_xA2F/æFøjGDó€æICaþ…óJGßöº@¬#{&fÅ—83q&¸SÚÅP0ÍC­‰kßx =-À'Ñ8ÅsÜZJãœliü¢Âá\ª•y\ÊdçâæŽd³fÏæ-Ö’ø.ÌÝ»w»Ít©hGxgt˜˜§Á¿)G„_¬"ÜÕÉé9 „,ÓZum>hËX¦éãÂ^î%ù<Œdd6“Ý%c¹º¬(dr¦Éè$Ó-æG6Ìnß·šö%ä ‘}“A…Ü£ =J@Ôiú´².Z4ÇNý Õ´c¿¾Ò£Ž¦ÑN5ååF·öj4í™@Ÿ3¶ð¦KapÅ–ÿ‹mÎb¼,üÔèÁb)ä>4§PÁ™Zâ¥dóÛ#ÔŸ\¾–?¢èpål—F<˜‰hûŒ–Lé´Ï€ ŒˆpN׳Õ… Üj+Ø´*Ãy/x@©¢{`½ƒ +›ºá>’7–Ò‹D}ðËö5…Q*x¢!3.®DÁ =W¥u*jЇկX ½³À¡ï}U(è).€Õcçqëlß¾ó ‘X‡Ž<Š­)œµ&À»/Tº=ÐaÛ«¨<÷à*"L0ÍÏy5cß³§èz–ɉ0åÌ(b„JoÉ®¦>üi£s•°Iú3†åË5YWÑkAѲ¥%ÈmS¼ÛÃ놤FéqwrÔýú郠Öj‘´à«Å?/áW°ÅBö€b*Ø…–Ü.D4\܈.*³ü æŸ6ì%ß®Ûy¤øt·å‘xÐÜàVêLœ~©Ú¢€•‰°}Âç+íçÖ¹cÍ ¯àÀˆÐ­­î¤%ý\SI ŒK+: ô$S=°4VP Wªç$¬±ÏÓ‹Ð;Ô—Šçh' ±È°®ôeƒ*ì ±£Ë +–怘äROZ„:]‰—pÁxfÀ"³< ·ŽÍñœZþ:*ä#ìZCψ%ѾJ „N1K‡’3øÙoæ¯+‰— k%»°0ÈûÔµó5ùÃü•É€¤#•ÛmoÑmb…ÚK5ÿ +_òÀÈÖAâ6zTïºÔ–6Ú.V–ú8àØ)Kê´¢&â–ù!SÔud)-^mxé °Vั7 8Äv¤)d½Üc«ìéwK¬Y‰_Ð-4É£ö°×—uÉþˆ‹ˆ0C&‡àmô…Í5Ÿ© gc:®¡€«"9ÞÐm…ìM¼Y;Ý°8<È>ðúq^ôç«íµK¡Zv®qœ"ùB©n€³94Öð}PëV¯€»ÀXt÷€I‡ãÂt%ò@Uç3ÎiÓ»I£Ä°ZˆÇüÔʹ@\xÏã +ž!é_áŸ.`?|Gõq¤ŒÓ‘C£ Þ`ÝÕø»Î@Ãð RÜDàÆWÔølhÞ4_ó Ì8QÂÆ0v¯Ó+ì®óBa‡U«>ßÆ>ij:â½ ’MLîà6˜Kè”ïôªÖ·w) k{Ü0Ø$Ì”Š¿$Ö`+ÝîŠåÊï¦òN¥ç•˜-–àPå:¹PE²|`Õ+Xnc˜FA×L€?Í¥m§zY!o~HÔzF}jNÀ1&3Ž!ˆü7ÿ&ðÓê´aŽ ü…l‘P¸¨Ñ Åê{œ§ibhÞ‘] ¨õö{‚LáìuæÊ©”u£ÝP³Ÿ‘EW-㤂™o5€ +U@UF0|l¦ŒöͶà0N±E=¿Ü@ÐdühelaZ¨Çù‚$Ò—Á¾Z^ìÁ)sF4l*óY_ t +èýÚÍc{%XÃ-eCºY±éL5{ì“Ø÷&ú{|–HÅÁKÑT*ÚY>¼µex˜U*a á¸ÓÄ^çÕL¡ãÈUËŠ '¦ WM+J¼ëŽÓÂêoÏÖO<ú¨ÊœP\Õ=‘‹CZª¦j +Á²ª0P +í…õªD´’£!2Ã&OSÀê!ܨÒþJ¶ä]ɾ8ˆ­¥ŽËú‘O»fÖh¾å­EJM u* |·±ymøh½rnÑwxo¾ÿžýÿÍlþÊ}¸ÄIJÿùίEH5r»@ª«™w îD¸KžjÞϵ,¥¾G‰™;™hÜþ €Z,ûÀˆ’Bdö”Ú.×X‰á$ý$ÏHŸ§;´…†yI5Eæ9ÚY’ciŽ:{±3[_›_Ü•–[{nâõæõ@³. fè&„šÐ]pºÊN·©å¥ÌºÑÙÂ+ôK-ü½*†ÌÔš9N7bã„»nt…dø!$„Šü‹½šNŬê¢çv³ó»¾ZÛÆD[bo “sÏh&2…ÖXÂ÷fêQ +† +'mK`…½‡¥—g+â”Að#íloR]hØ +Ž‹¶×#ìØ]óD˜{L±T>Ð#üÕì)&à9 ù’Ý`yÒX2vvŠªàÉ ›PáP“iHÎøˆeý!–[<[|7TÈ/芒ˆês™˜œq¹òï÷M-ørÓíiêÀ¤Eé[…Ð9Kî£àÜ ´Ñ]=<‹ÂáøÒŒ¯ƒÅÊP‡qCùã,kmòq0Çü_ 0cì^@•$Íž¢:‹×ΰà!œ¿ÇW£ë»ós[[¼÷39¥¸À:@c˜úØ„¹íë«ÀË1(|ûiµ~i‰y€t’¼v˜ÐÜ瀹ù>°ß9…Ì_¨Z9ÙŸ—R«ÀÏ¥\%«Íd½d5­Xýâ,W)©h8z¾½žB©NWkÐçßÓ—èÇÁzÌ€‹è§ÌŠ>oüêé¿ì@aHJA±ÓiÚ+0â“£¨/U4PØ7½FN¿Øg$™¯×8³°§xûAïÀýØ¿©€qB!Ž·ÿ_üÔÎüS5à)Üz§y¨»‡8ÿ®Ñ_…Õ£Xu ·Ù“9kèŸç+ìyÜ­Žà^8@šÂ¿ëº¶F0ÔŠÎO+)â/Œ!c–¼Ín-ó}Øî©#Ómæ×Ø%ŠLÓõ§O¥töéˆXÒ^ϧGSyÉýÙ?ô Y,í¨X¯ÒÅ7Õ%Ö ¨ë¡~¯l$ý7à†´÷ŠY¢ PÀQ WÌqÉLÿêÊû¯f¥ËÝá¶äç”÷Á&ü*ðy*34îØ‹A·â\v‹êm£qÓÍšr +rÓš®_Iœî:À)SMÁô ¸˜0eáSÍ;qplœNé3XÉÓ)ryOÍx,Ô(Ë>QÈGë™…[ŽÂq¢@­XŸÀeõ²ƒf;â{Xšå¹_h %Ê~TôñÕ–UT½oÜõ@MJjþr.°åæ)œGíèt0Ñý£A¡'V9ÞàB‰+P_bUR¼¸AÓÿ}Prw·Õ$'y>Å×Ö^6…Ü°Ot2ÒÕ}Bó«ètMK‘0t¨³¼Òb-äÉ0„õÀKf&Iÿ‰¬%kóÍPpwÀúËœ…*Ûadýò{b]ªùÞ¦ÔRÄŽO>ã¸0YQKÈ{øÜW¯£’ûDs\[ ²Àp9=Ðh‘ÖŒÀþ(±8õ. É\¾Úði‰Êš‚X0è2µu]fûÀæˆ*½Ô +}èç™]ðûéœ#)1%êÛeŸAùgE0ÊZ÷œMy…á<àƒR§bL­­xGƒ™¤÷="ÒÓ~ZÝíz¼EÉq–ƒ¶–ÈWF0ùà6”:—,eÜžã±Öï_ºÏ2¾ˆj‡•©[4„JìDø’¶«»E/ËÄÌŠÓA,]{dÖà\*HuóyU‡ú©.Pø¢%ÁS¢$3o0M# Þ+‰œô'‹)bFô¥Ãj³ηå®@Þœ/!ý“@§içÿ0dÉj¤x³!•N…ÜƼð˜’ÁްΣ<½¼éVh îïUFÿZh•±þ4 GlñQˆzG9îÔéV¥X“öN[Òé9 ÂÐùžh®þüë`?ì=݃¿V]\ò®Ca[ëZ)ÿøŒ[›Ýo¦Ó·ˆ?Š©ôIäÉw|’‡˜ÂŸÖ¤ÇJHJÎ@ßîòÛïAìt¾¢l_â«qIVûøPð1ÍÊ&J½¨æçÓ[vgó^ …¨6©jç­r(ò±¤H·ª¦Óö¨uH…y» ðÎí -3»Üö¢rÇJãu!4çˆBâ¨>ôˆ‚eéyÄ^`×,Û—ˆq„Ù…6L'Æ¥— Ǽ/^ÖÁ팻7?œÐ2Úå€æ¨yGãl<Ý$n¹‚~û–·ñ|QÃ×fSî{ýÄ›=÷iž{gŠ{V%±ÑÑ}®x¿²£=[×ÿˆ©ê¸Ë Æ-4éã444ÝÖw6üÃN‚Ò?€»5:t®øÑG”†DTeóþ"ÁFÈÚR˜ÇtS@f”6T"³mr˜Rýj` Uyµ‰¬èBoš­vrf?2o#:¤™ù‡.+Ê„êß):ÔÀB´ËÜÙs÷Ç+±™˜áÆz_(\Ýʸ¸ˆŒ«_ˆ‘…„ rkw`4v,( +ë*$J…¿\è„•@ ûaÇRL ß7«{b=(U¸v4 Nï4«Ÿçvó%>h8Ûk–‘þCö,ÝþËîàæyKñÝÃ`¯¨Ïs|—UÓ-GMw–—®¿©~ºx/Ê+E(•j·¤¦$åèËçËoºJû÷oØ„- ¢ÒjpÇFG˜ü¾ÏŸô/+ÚKÞø^KÖdš{¬€›]ëÐÍ£–}œt@Nh ½b¼Z<ØóÁ=„P=’TW’;¦õ|Ѩ4»vÖkQòMP:ʾ +Å} +mÍaIºOXÔ<“0M;pÜœgüÕÁ¥°k´ò«ºw´îC>]‘\l–Ϊ¼vÂBVȪqIÍg5?ä ¦p©e3ªýSÞ—Æ»4YÓ¨ƒž­ÓjÇ2žR¨âÿ{Ÿ– +ÏÁ=æ@ø¼î\£¨uêñta»º¿âIoDSÔx%2ªÔ/[šú¯6Ô‡‚IwÅŠuÀMmü@%ºÃÁám®M;WjÐKh0»?è8*Ûû«ù¯ƒ4ܵʭ™˜˜òAåÀœò"Ñzú<àod«[¿ùþŸg°ÏZE‡WØVãår°ß—X(çKÜcôU>ÜéDÕh¢AÆ ]°sMZpè±òqàà‚6 èÝlߣ(!)›3l$¨ƒh,ß'W¶ÍíÊë烟2¦…>«„r°ßuÜF›ª@È–©kp³gþÅÒ‚Ÿ†hÞ÷ÞÒÚÉsÍ–bÑ ßØ~ÒÕùÄÓáêŸc ;{° JX§Í ^ÐLÑh¸cЩJØ#·PþPé3½ÎB¶— ç endstream endobj 23 0 obj <>stream +æ¥ø©æw‚æk½fIsâáÁÞŽRõ²÷íÉP°7ÏëС%á56¢‰ÌLJl¨‘ËuÌy œgcÝÓËÇÁ^\1<,Hâ;+Ÿl¾´Àþì…ç•c¼Ž1§Q`³MͦßÝuoûv…¡]¨GB¼dßØU²‹aÍ÷u.â  +áhXQ×[A‡UˆG¨hz• õ±0æn”1»uŸuOúÊS>•(íq:¤vÞ¡¬P0oX·qwŒîÝaUΈÉµÛäýG)ÇN‡k3ôTKkýmýéD?ê\…Ïa¶Ýƒj¡ôÐBYËrb˜ô®?‹¥`ÐïäkÅû«~;Ej©L¦å;ï>ú¸:†ƒ²yìEÌÈöæ)Qªž±M›#œ·Ñ·Q yhƒ?é…€ûïGÐX|·ºÕxÀøìN´·‚I<|=Te9Ó:|”©÷Ò{ÄwÓöglN8“{’,:0–#û¹£zèå/À=Ð-¬©žC \¢R>¼¤³À1üÉÓ©O§Gp=rtÔà» ˜xE éd‡³Õf¿‚ŠHf”CF$ ~™ìîbo™Žeg3BcyD½(§æÿrö¾±[_{ÕÎÏ7ʘ¶‹zj ‘Y¢-$( ÉÈb¢‘Æ z’Lµ½øä«~UÃ4Ç |ƒØx4æ9”Q牘¡´ˆóDX»h¡ÛcĸW:Fu;J1YÉRI²íëo)6;¢Ú5øÙùõÌ´ü“Phˆ¡]ýøB:Î#øo%ŒZ,¢lÞÉJ{ŽlïTC—ðRB‘år•S¯œÓû|Mm÷¨uu*=?8V3á[w#‡ ±êð"fs¨+QûÙ)]KýJ¦…¤¥8(€T%¸Pö<ð4E;{vÜ{š6òž*xäãIf}]`±ƒÕµ‡X.•@>þHú.B…n`+­ÆŒ¦¸ª¼PMP³·…ضÇ)4ŒvËCOå±ýBgNñm Ã{2mîä¹íõ n,^Ô.ÎÈ:õ)©eÕ(Š¢Üv…V*¾½q,Rw?"à­HÝb:záÚê‘F$ +ƒ4ëfÁ(”P§5 B@^Ft‹:Ëþ¸:pû§fto +á{ÔgÓÚ$‘è‡YºUôb;û(q?~©Ñ¢+ÑAÂ&w_á­:Úõ—J]9W v +^Ÿvþ&3Y ôF¸Ä:-UNàîáB6ƒO$w§¸û'áñrw°úÅ€M¸“ ýà„¹úñ‰žøÑ)l1Cƒ^*zþOU’¿*û`8íã̓=>Õ¤&d †uÍO°ßˆë\š½»6¥Sv g‘ðºÏ²³£—‰°×ÕÎOÝìK÷–…±îÞ¦ )6_¨•©ÇºŒ•mvWj~{ €ŒLK“Tý%ññ)Y)Ð'þ¼Li&’aI5ŸS×HVº³úîñ÷@ÖÃÞ;§ qÅg á¯ðaÖ•ü)½.öØÊù3™Î\omòíàzOÆÆÅ6ÌæI2xŠÛç¡'Äžú#¥È×—†v²O¯eH^Ÿ +ûH‡Ž2O³úíï×ôëõP>6CÏ!ë1.‰ñ,-Ç¢¥Ø¹âm}ôûºúƒÁ Ü»TGµ~lõB†ÊR¡VìfŸ`%\°rYÞÑûGƱq,@`=¢ã¿éV9þŸð]Ä3ì~gÈQQÉæÛO«Z♽w +;Eù½Y÷E{@¾£¼›q„kŸqÛNfññqIÁ×QøÅU¹ƒ.*Éóáã¸ç}?(>0óµZ|­j,Ú3Qú¨!¸òrÜFaQú~B¸h¼`@g°IÂhBj¿0žz¨L)ÀŒàgö»b¤uà zrTÄ–nV%$zèà„‘4sæ{Yì#â:¦_ˆBf§352+ìpîC Iqøn™jNŸ³ßãÌÃÞðu¶~¼X'áã¹ Á@§Ì½(+¹ÓWô î¿ëœR–xOæD?äšÁ‘¶÷RhïÐR„UÆQW$³TjœÈúô+Hêܼ¸Â w‹>ŒgÉË—¾qþ]§&1•ÄŸ€!Gg¡ŸL¹ÖÐrègñc)ù é@© q FÙs”€QÕ–p{>雎©4Ä’œúüûëÜmŒýêà•ë¢•$÷ì;‡ D¡wG†s„ çxýýõf骖 S¶¯Á²Pb`ß›’Äæ‰PÉáûŠŠ@."B¹97UB†ß™4eÚ®T!¼‡.IÍ 8‹KßžÏ)¡Â¢^×—ƒlXq¶ak‡çǺ–7âãÓ~ua¨.›j8í¸Nõž®œÓeé* óã¶kg´¸Ùþ!Z¤|ó÷?!ÆÒŠ$ǃ{zyù¦[«W ×ê¢ A%u÷· vÄ#<ê57xµRD•œbgYXÏ¡w–2H"T#¡dÄ nC%è5&je¦Qêdš~½ŠÜ¥¯ë.²#@Þ¼âosèTGԒĤ‡"¼ÛljÏf0šì¶ÆQÛׂnïÍðòïóö»×éï3àOÓÄåÃ4FHÅ[ÕªŸÚá7ðý` ©,y/Òz6ßË‹ ÕçÌaù¯0µì¦¡ÚUÆ Xm*ÇQ[§)&æ)}kºí!ü‚[sÐŽèÂÅ1ÜO€¤yÂ{Ìö;á 0wªƒ­ŽhŠ/…œÆ3~“òùÍ¡õq€îõKÔúýà +á½rol¦àáu’†8€ïG믱ñY±|ÃpCüàp]È~CuÁ2EÂa"$×Íý{¼É1Ææié:ă˜ÿ͹ÖÆ9“<ÕVB‰Ò–…7Ð çßÜåëAyþ¦·¬n}ùàH!ºï'ùå3Î+~ +‘ «é•2‚COîí$#àrÛµûÀ~xâô;íúù÷ƒû x>¬.™Šò!ˆ&À—ÍŒ ÀäBG[I¡(p- U˜;%®aºh=|”2²SBÝMÂóé‘!_Hã ,¾òœÕ>mv Cd´|)Û·^¡v@o|îÞñ¡ÿú÷i”º9¢b¤q.‹~^ÿVõcé2úärîÎë “2ÚÓàÒP1Õ™)Õ Í[:¼/µÅ@¡¹Þîûfà ٠ rjåøÎÑ Û#…‘XR4YÞKFí÷ýñß³ /{è/2ˆþÊ“%ŒúÏ!ÿËßSMýñoÿÝÿúÿå‹åÿáÿóŸÿßÿöûÏÿçßýó?ÿÃÿøïüÏÿóÿåïþÛÿã/þ§ÿÿþþøÿþ¿ýóúþÿþ).úý'þ¯ø§ø»þ‡¿ÿã¾Ä¯¾ÜóúD§ÿƒÿ»s´£{ßã¤9“q£Ç®…{v#}BÈj× ²»Ïæÿzcï4WÖ<%ºí,³ò—úõ/ü3øŒ[$–µ\ßÒ«ËÃ(C"þõ£H‚†Áo|ÂJ$44¦®Ž‹äû#Š¾›¾ÄŠjûàäkàíˆèê óÓ÷KWLrÏ«“lã÷@›â¤gðÈî­l wÚ`²”:ÔôŽ4,ú®YŸîõ V¾ß š4ÃÙ#–J±:¶Zt˜$ò +$ t«Z¬ZÈíO˜W4#p¹$Žˆ Åçà0ˆ(Ï9=àãøS“I^yÚ$->´?†è(@èw„SYÓñd + evŒxD¬“,´¤_<[(¼ïÛpš ¸p€•ÝÃ'÷Ç{—Š`k n;ÍwdM`U\<öb¤QY§å¶'Š‡>pÁÖA͵d•¬¬–¦çÌžJÈö󀮊‘}åÑ£›Šä°â«”‰÷Ø®ù°©)¹¬ƒD¯ ‰@r¡Ï»Ó°ð+L€è)bŒ…øMò"(·ðÖÙóÖ;€¹ äã½)«¥G{|¿3Q{SYˆbž(܃ ðµsžq,tžø8"œ‘j„ØËý¤¤–Þ @!Jõ=¢Ä²ceÓ!ý®OŠ›Hã jó~X1;bŠg·“¨¼²‹HµÓôŠ8/˜”Lk%qÃXÂ9ÐŒjŸŽMó¾ ‚¦`v1%«*a¶k”[{ó-ÜÛÉƪñú úYˆ¤#”QYÔ© +î¤C+žÏˆÂïçt΋üMT [T² @;Ü5ß_F¸.çý^S°¨° |.oâÐL,ß| +mFÛ~ƒ«kžîUè0,úw(ù¦À'´ÖÙü!oBUºwˆ(n3@T}‰‘¥‡Õ8ø "ÀŽTT×V ååwIÑâOèø‘©‚ÊŽ¨ë€|ˆ’D½R)Q"á[ÍøÂî”j9Ž£Û&kz¼WìÌ™ú¸) —Ÿv{âÜ—9uJòeyÊ‘€žTËÓ·°XÕ;G}š”Áíë7=|÷H.WŠü3 +1\oæSœàá C³¿ó5“!²iUñ„ÆðÙo£å- ÆX {Úý&]“³˜M1²ž0KËcô åðu໡{&ØC{¼7—„uç ”ƠƘ?Pð…DGõX$ +"b/ÇeµÒn±æˆ§dqPg¹qGM!fªUNë °;Õž QÉQ5ÙìXõø8å»Ì@kÅU«¨ aVR›^l‹¥F`SDÂs7g@…vTFÚ¦RÞ¨•™Ç!WÆ,Z¢p "ýlFŒÕÀÔ€ˆ½;Œï²·MV³f=äƒ +PžªQ-V1}V‹ÂÃñAb D9s|ÍpiÇil£°#GRxý=Â+-Á“lˆiªsž¦ Ï^›îôNhÈýYG÷b–"KÉK‡B³”d¦“ÛAˆU‹¸˜I{Ølç=²Ô׉්œ+gÆÄ”AÆ„ÎÐog^7j\£ùÐÈñ¼b= °_¿ýêSsn‘©¡ÐD"3]SYsjØ0ìxÑánT1„s¯.ˆQòö 5á­N¶þu§#ì,FÛÿ‡c{Á*ë¨¶ï ‹c#–¥mLáªàˆˆÈÀ˜ˆ Ï ¢Â¿$b¢ˆ^Ï:¥»¶†Ã‘(ªXF)»£VVT¹Î•2j*Aˆýr±ßűD"ÔxW´êÝß%]Î=-8¯C‹«ô>œW“ÔŸ””IL„¤XEX%ᖃƠÛØ©ajÀ,¥vfžÌbßÃå Λ®p@çÌ…”Gû=Þ74’@ØÝ̦*«²¿%‰¦Ù*Ô cµ²™U­w×ÉÆþÙWÝ«ôR¿>G¼ìi@3‚ç,™9~0ùýùfzÀkYF‰ @è¢hÑëôj|MòQ¾Y +!W\pÙç%F΃T~˜Œ8"væcÏaU£îu€ʪé^¨ ¡ß·2G‚‰ãŠ¬~©· õœå&2’Ü#Ã÷#ùÜëùå¼R ™¶[‹/Ôƒ¦¾DäpþñDµ +¥ªcˆ'H­7ᜇÑI®ÂÙsT_àeQÛA†þ)Çk¯ë¶V8˜yfy¤ÖÁC£X@v ̨i%°—( ã1ì¡ß»‡ì†óÊ3íYÄïžöÀÑÄMÍ°¶g8¾±+|1zy´Þ¡ÄFÅŒí„Ê=ÖÀÞ"¼@Öy,.ç7ç‘TÛ‰\('Ró-ŽÊz,š %Ãý+ÛËÊ^‚¹¿ ì×¢v{-¡S°)ëUñ4Þ,è%ú9jlìCéoú[‡~´{’9²èDQ¼ØQ¨š„°š+sÃŒJyò^*Ö²3ZNºî]Ì—†Õ +•2ŠJºù"´ëo.{#|N0{%*õQa)\v†·7‚Dììh¿#ûÃÅQ0ÎTW–öDèCÎà¢ÃyÌA¤Óøx‘¢­‡Çòã#:B¸Ÿ¾Y_1"ÕjÛœçHζ=tú©Îz +ð£ÖU…'ß²Ó˜©ò®³kâ-Þ'AL8:dåˆýiãô› ÑÝtm ØÝ*éÈ.®¹|KÐ÷¤N& ?C[LKl21ÚW\ &`Ç>>‘`«.M¬çt}ö4¹W{½¡‰ç³#® A™]éZ5]_aÀÝ£§»ÆÇ”½ì +gà4)KîˆÛkÐüƒõ—´\8W7Ïr Ìø¤çÇf{ ÆâkîdN¡]æ +#P¾á;Î'2Dµ[0ļò¦]ñ¶Ä—@IªyÍ›i€Í§1@N„Ÿ±2"“î˜Â% ŽÊ8ÐS꺶æå TYƒûãtÇ"U3ˇ=r:(¬*ã3Aè€DXšðö(¡I Kò©ë«-åé²Z!êÉÒáõ4Øa«Úˆ'VZ¦(؃0Ç‘¶EµCrmÿºNÊ×E½†‹úœñéýŒü4ôW¬_ +õwðßTv*Yã£×²2‡}rY¿ÑkÅÞã~L+}›y‰¾Ÿ-öößD(Ù8_Oâ×av"xZψښʀd *ôN•¦à+ýè ùjì$H„Ç{„×qw„jÙþÖà¶>Ï“´‘Ä”v4á+LxˆP{kj¤ðË||Æ*ßuÅÛÃ^ã9AœgŸô ¹2$EUª!ri;[ÄýnbÓS°OÀ ´ÂÊü$²,ÒW’+öáOÂfž‚-c¾ÍÇiõų_:]ÚXÛ*uÒ2¿‰Ê´hÌïßÓç×àúu~B¦ÓûåG¶ªWèõ]PÒ  ¥Ž4poc¥\qß{´½Î{â± +õ~z ÌO Aõ;(u`CÄ×ø@¢áhëózx}DÑf˜¾M)˜È4‡ößáfÃnm2êQ¢ 6 &òK×gÄjƒÍ—~ ¼óŸçÁ,k'”»¼â{‹=¾–Ÿ;Û#Á(u“kšÂšÊ{±.›ç9SÐò—j VPļËFørSao[Ž 6s0[r"lU"_\”ð’D`ˆòº’-OºuûãÅ`Ûiu¦yCDî…KSÈ°†KÈŽÐœÓQTcêùM‘4ÙÇ»¿ ë/Æþ^p>¡$¸ç+^ ŒÉÁ›ép2‘ÜwÏ3Øö­:ïº É¯â:lWê7&/c«Ï^¨EzΆžVÍ¥ï"ŽQÑ~½/óõ3á+•±àW0íÊ …Ìs$!û½þJ<™Å,¦ã·ã^O×Iêõã4…X!Â,#¾hså8òºßDÌè1š÷‘|DU 2¿”„9ñe¹ E«¥ÒL›=mw$Ÿ ÷c×Êöõ=⾚Q¬C›·÷ ʵ{ŠÎ”kºÊ|•þÍŸÙSí÷Æ=U«·Äù…N½/Ã$>œ@|;§¢ê*4.²çû—ìi×R+À×[Dܱ.CwÕyëã<•bZgòÖkádbýdbܱúÄžÊX¡­“¯¡+’>„§¤qDÒc³òs-~|bË/¥Ÿ™òÙ·æ0‰ùŽž‚jš:¯Eæ‘YF¦-NÊ1÷S]ñ,ˆöpÿçSƒ'(-q†e¤ #®Ij×ç‚YR'¹#ÁÎ#A±‡Ú^IÞïi9 ê‡QhVº›©)D‘#‰õˆElÙþ!“ÙoÕ·çyÎÛ”™Ñj¸k/%­§6Ÿ+«Ú¦õ•½¾Eµ£ U›þŠdUô{W°(”·‚x[Øuû“öÂJÎWôPòó\îºÜnæ†bì‰"cé8¤"_ª ¤ÇŽhŠúi+_=ý‡?÷»™{}7–„ÂŽ©K[Šê[™äq^œ:¸²òekFyqî®ßÍ›P Öˆ â‚žS?Φ$é–_2ÇãäV±ÇÖÃnŽSŠJ zÏKE*¶>ßD pVUã¦Ý|ǫ́n0}€Ð‰-2Ñ!:zØ0tKDƒuÿÜÆòëÀkï\P®~}ªªv A²Ì×rC uEÂý„;,r8NFüéál»L¨ýôá?Î, +ß„ZO„¬~‡ÔÐfÄ‘-‘ (ñcŒˆ€òXŸð¸Ô¦\QÓ’mÃyêá½E=¼}1hö x4þ3Šù¾AØBÓ@ôàìÙMÑu;³/¶‚ +ABÂÿ„vghI‰ƒF®ú{F!£[௑kw…ZmõâfâFáþ·™gªJT5ÂéL»¹úu¥X-ÑîªõJ–£F0Ã.j â5˜e?ÄîÉ+±«æ»°E3b…Èpº&û¶4cÕ_©(#BçØÙUvS3£ ¿PÃ,Md˜>wÞ¸]å÷ˆ"/ˆóž +ïOúˆj9D¯"±Yθ4òý>„ß7• +ùUgnóGÄŸNÖ„®;¡=E~sžb/‰åO©ʪŽeí)ç7;½8•ÍoŽåÀàbñ#d )&¨ONÌ2"íÖ›·ùÉʤŸ²,ﷀׄceAÈ€ƒð~˜ià9—Lù_’"ÚžiÙŠAëaØ4bŠ{Ýâ^×Ûe‡þ6õSmì› š ¢±îucG•Zäàh\œðAx!©ÕRW"d.ß ÅŒ}ºnÀE­æ@2yæ"Hû§Þ§N=“€É|"Šó Äi¢‡Ë)ÚI·~—UœwP +CR!Ud³ð#*‰½=‡8 +‚z[×½ªµo"P'õ®PÅ=ÅŸQœDG§;QÒ­Æ^Ö/-¸>”6÷{U3Š zdÀ–xˆQלböûˆŽOýæ<0è"ï1…®(|Úþ-k°E…vßE&h‰7—šÙÙ-Óž·ç+4½¡¯õ`ïái™´|ßG€ø”,êVìõ« [¾ô©Iñiy¦YæþQÛ%GTÊ”¸Ó er 0°‘§NµÇ8¦p}jN_ ŸìDùë ú-³ö0‚V +i?æ3`ì»ìlê*«|FùÞEŠÌ£_S û¡­ì×·ó 01O…z/?~Ð~r‘_˜:j_p¬0ë”;C ±Äýˆ‘€ŒÞáD‰ÞÈGõÆAQ»Ø3­×…¦£f½ß³M lF^Ÿ”*”ÈæÖ™ªŸïÑ㽊bÌXÅA©"ÏPé¿ÈSIJÄ㔄¦T¼gTÀ?úš»¼«Bý÷ÏHgA©´¾°§¿>˜C\£ +ù†°÷¤‰FÏ´²GÙÏtÖðƈX:D~FĽ +áGHF{5ùö<ãþfQšï'¢*+`^$zg› Óçºøê—;ØÜM•´þõ7çëÀÍØQëëN+8ÏÀöZ:*²¢ö^œ—:9y¿°(T}Qƒ´—Lè_%¨_E¡Ç¬bwÎÀ1ɵHxç´Ò*+{G$Â;z¡êo"îþ "%Sº¶ßœgöè„Ÿë"D­ ®T>"ÞæÈjߣC‘Ù°T !9l–´ir8e5‰2  ‚ôŠÞp¦G†Qä²5 %“¦E›Î¶"ÒÈ£(þ€Y¨H N@ð(´IU¨Q ¢Ï›´RMAØp‡^áŽ~©J@#¦“š>;„KHEØHƒH@£ÞçGq¶u(l!õß3óßž®6Rì ¸N=‘~ìGº¹`V:d/òz —P†Ù n4A;é¢Vþþ÷h£´(hhkœèãÓÇ&|štÅl^I´7ùÈg+¯.uûô‡lÿ¤QÁþþªÊ’#ûdºÀX$6øj@‘—N R,˜dÕÓèš«LÔyÐyI)ºÏ_Ý©ëðÇmH+ (»¸ümÄßÅJôÿ‰õ. ýWts…fyà12Ù~ï¼ü˜ìôCb:h „³dϦDT÷ÝAqJû4C(×!ã åÙÑã“›@— ,¸CwD½û†gx ) nä‚þúHà®ñäæÆ‚}ƃ»ïC>Ôr4°¤¨6ë‹Uç¸èå…Aj×Ì(g´½iÜ‹~÷4b’)å¯0?Ý®¢ID!Å`?Ï•ÐæŒ múºR}læ y¢‚T!7΃ Õá¤kùð@§_̱¦O 2Óþ)ÔzÎГB¨e”vÞ‡=Ú''iì`ø uÒ½XbüåqÑV4L–ÒêúÐŽQ h˜½X¬ØŸ?ë?Ûmò–ñ€ƒp´Ew”÷¼óêPêwí¡ÀaQk2Uö€­†¸äé3bÁò01aõD´›ž÷:£ýÇø«²ýQ„£ƒøÊTî8M&Õ¤ÐtßfüȘ¶Ó’{Š.TØSÎáã~{$Ä/`ÁÀnõT +°Ø¿˜/$ëWÿXî7éld‘Kñµ,9åg2ˆ–Õ¢‰ŠÂôHrK?`‰¹ÀͬÄÝƶlZ·«í¸þeÐHÈ9o @3ËÁôGÕ‰ÈT€é³wÁ¿Ý¯Ô¼{uúäÐ^é“sݽXÆ dà\)` ö¯¬OH¸~b+ž¨0€sï”öa™Â.ù¢P†k@1—ÊpéM/í·Ø¾÷Äí•2ºŠYz³léPì‡0e€ŽlA2o¶óþàF¡Y‹I/?³0ߣL‡- wºÜ[%íû÷‰Úÿ±b÷‹t€4MZ5Á)o¯Õ´€n² (þƒœRI¨ë:Cšì3(ï€)ªÁ¶JB@öË·â2HdÛº¯4@!»4$Ù¸x¼={#[ãB좈“©*×äB”—N¸-NA™›ˆà3IEX¯ =ŽÀ\m¯s_b^OšÑ‚A¶bDBC•Ê°¿hgi 5`ž¼iÃ~òv#‘.%ݸ‹ÖV+Ü’í~µxGuG=5kµØ‘{C?ñÝ¥Ëj ^ñþ½"è:È{¼çw¨)pUPh×´¨Mì`¦XÏ(T°ªz1gõJ<4®Äjù ÅÕ÷­âï: SC9½–}|£ÀM$û,_t+´ãVžÖç¶$b†,fåxß) âÚÞ ³éM)9Ô E1„+ö€ÍgÞE9™ùžúòRwMs´o€Ši90R3óœÈÝíó4`»Ë?QR£UQR'ˆ‡Ó÷# %íq" )QX ±Z‡à1Ä +å¸?uÇ÷+ì=ˆ¬d¸`®óuehþ Ïp€¥lÎi PÍOT¦¸õDð¸©L,DØõÛ÷â%G¿sØg%*·ˆî"ïGM!C¥79à)Nx~Y‰¨ô3"Ùäþ–+÷¿=nþXNOÎ'ƒÓ½C©¢Ó©f±7g#êm‚°™¡©‰’ë´†ÛYî5bé@3ÜòrŠŒ¨ŒÈ˜¨N±NŸŒÉu•œŠª—gQ%­ÿŠ=Ê¿ž½ÝDð÷°ŠÚ{ V³éJFÞ +—å ü\ÜÆ:t“Œõ0`wú¯È±!tðÒƒFªL.€ºÞÊÏÚ˜å|Q™ÅVŠ%RCßÃ|!Ôàšyèt6=Ý£qŠãÏ“@¡¥iûŒÄš®å^ò^)VÃýx«`äÓˆlÈ"Ìs¥@Eâ2_ Ûä–Æ•„ZpôRÓèŒÝ¨4h-ŒËI²¸ºl5Š œ‰ª§"3)g.ᆎUGà.•~‰—–1¼‰[Š:^wK֬§BÛ5G2-Ž„ñõmjI~$‹e”¢6A!·{>9Y™mG"€L¿Eë/? O;Þ—Gí~Qñ£ys‰(N D‚‚zœ&ô’f¨¾™p¢ÖŒ hšÇöUž»ü6™ÕÑ×; ãõütTùdÝDhÕ¨°\(;-ëÅ{?žb”ã¾VJK]6 w>òÖ½–…ÄPK$HõÇo¿¿üŽ¯$%)€/”¤0kwKþ°¤²£”ô'ŠR?Q¹2¢äáðòU$èžg 'éy@ ïÄcŒæA„3¥-i ‚ +´ËÛ~N-‰ÉÛ³)ƒ³:3ø|î•öý)ÿEçkÒÉÎÈ T‘šÜԃІÂ߆Y MÔ –ðÀ³ÍÊ!ªô‚/¡µG †"î)²÷¤¡ç­‰ +)>—ω€oƒ‘Ÿd¾<ìŠ>Êó‘7ž=7~›ôÁÁ1¶´ë\33pì·»Òoƒ• öÛIÕ™ñ#ý$X0¡V8ck̼ÆÖr°*åÉÑiþ ^——ëÎóòҞ;‡º`Hªèë°íA³L{]çÀë¢é¦BV²›b·QÜØ„9Tü¨ö]fçNC)Çí‡Õ’Є¢Ò‚&ŽUâiÄJ ÀHzkÕ©x™³¨¶¬6ߎÓÊ \<Òègˆ¿Ìû,H­1@Õ„­LÍ•–…¬DQö¶GÌU´½HÙí§ð7¦ø^nﶦKN¯`ðð! â~€:âU¤úþhÅS3"GÄ:íÕÑòN‘·(íEº`]ð…èqóK”«…´PEeÙGÒZ›M'ϳG ñ¬Àûߣ*¢3#Ü?u´'m@*ž"Qô7Í}P^Á²ƒï°Û=Þç 4¤ZoÔÌ•!†þDFùèÈÊZö˜˜¹+K—‡ÇL¶þQ%c!’²!v”Þo9ÛI@­ÝKà'Qþ[j7‘PbÜeÛy=›ÐHzðP“uŽÎqÂŽˆ +$¢|A÷k{ð¥¸I5ŒÄø.¼Šû§Ðå|,ª!6Ìüx© ÑÒ™U*îMV¯ìÀ£# N@+xqbúí;?iÚ±‘ó•4ÐKjûˆø(£SítPÍ8[Ôåö†—ÊËPkU?8PóªùPÇ‚oNêÚâªQbv°i¸@(@Xd²ÿR;RìkG¹$JÉF5#T“”¦xh2¥A=ØÊ3=GGbQG7‚GFD{ ¡îšŒ¢JQ¼ÍDÓ¬ðž+Íf^:„¾p¦¯BDh',"‚²ÐŽ(¹\:@u¯'èAêĦm¯jl\H±-/íׂ5 (b pPQ®‡ö¥‹–©ê/¨$ð!)6—÷ð ùÂ- >qì°¯òM‰… |yï'C ›4à}þ¢ëµÑŽhIŽ0‚ÞÓØŸ7Rƒ@ϖˆæØ„{åtß ºš•ù®ÊÌ€¥Îç}DeTIJHª8²`òÖCû¨Í®+:=cÖbæ{ÚyÈñÐÎCô…èÛ°#Ê択޲åÈúŽµ•ÅVÖK€¸ì«JŒä» êA!†¡òÑl.;ÊÃ;öEÚ/]‘Ï°™yœÍ©D^^ºhŠÏŽ>…üˆq^e‘ ˜`”éœR•„­bn×#kXkzˆAËØ$Ùé¯? («8Åÿ‚”¦¸˜RÛ *2FÓ•qÍ‚PS„rt½*-…!u«©Àò°÷ô±Z°ÈAÖAJ¯¬î°d\Ÿbh7/ œublU£ôñ\†/Íán…‚ÍãaÖõÄÝxܼô&È5˜ø#ÿF¸!Þ#ƒüÔÇl„qÖ*÷BÔ$é“2oÖ(MÄÄØ“ŒRÕS³„¹2µ ä ùŽñSþhñÜŸì—V³G­‰§ç|kJ¶™üÞÞ1ýŒ ïl²èR0ÚD‰•>7NdtTââôW‰·Â2Ž.B +ãD¾€ívtX*Hý +¢®Ð3ÓüBZ2Œ,¼àɸ£|wZ»ä"ÊñBÆÑŒ(W-,l Óaï#ô©P]¡uS¡šp¥4õgº1³Ìó…biG}ÚGˆöŸN0ß ž^ÙDð¦µBQCÜ8Á@Ò‰Éqß²iUrtæä zGq]¥œµ¾ž£T€¼»‘O0S˜P+TÕÛ:_X6¢L4T: |S¾¶¾ács\«ià‰h¬‡ý¨.ÕT÷'T±èm Ú@âþœ­+ŠFwtÏQåa÷%R÷¾ c/¸\°ž»/ÓlïQš,jÛŠ?×Ï LW=ºÛ(kCÃʈcå}1-xÏC +¦6õ&ò»ÊÄW.¦hÚSµ[ëÜ!ž xt°$×¢Ûƒìø¨ÁÈÄÇGM·Ã©ñqÕÑsÓú]P®*™'H%"B—bÏDý´Èˆ’í¤9@¨~ÅŒÕy²w +ø z"Ô@ûÑ>òU ¯¢„QW\(Å—]wïŽ,¼ðx¥™¢0TGð;dwA+fb«òåŪOxN#5&½Â-é¸âÙÙr³Õ£çÉï“2ÇVZ0eÐ÷p?Z{S«Ë}ÿ„¹Ñ›C.ÁlK!'Gé±—öäˆ.+í²ß%ùÁþ(yéHÑÁÚi( óžIrd%P",LJ¯BV4ŸŽu N3ÏÞ¹u½ë!QH1'ÔÈ[hê‚°£ÜlQÀê£ +ö¿H>d@±–ªÝ,§ˆÆë¯DF? KGsóu!íÉšÙL*¸T¿[h°?YŸ1¨~öF”f„ƒ gjUÿ@ÿtÞÇéè×hàL×ìÍhâH,Ö0e¸;Rìàô •ËH¤m¢ò$Xìžõ„þmh‚+˜»LéAOUmÒT·ƒTÏFFÖÎÃ}‡é £¦€ ½´%¼Jo´•ŸsnAZ|U?(,@÷S}„Á+·&æyל|’®Ü©ÕÛ\¦A8Û;a°mPZpxÐ[GMÁuïX*+?DßuÈØCâI'E·Fµ¡®hViM ìò\PE+Êñ{>íõ0Am¼s‡€ +Q3ÈÌÐùƆDç£Ø=AÅg݈RY¶óò¾3Ù:ÁˆbÂÞ9˼äü¤‡ N–D€pCiËD–æÈ—ßÝ@o‡~·oÂMÌÓøc>ìüQdˆì9 +F€fš‡õw•ð¢ŽÆ—!93\Þ1ºp1‡¥I»g³Ž©ZÐ> íAuMä̸+¥-È`ì©Ü#jÎÜ9Gw¤9@¹< ^ÃHíÈëí윊)K£õ!·RÇ<#ÔÜ[²²¶³?ÚŸOXÝV1ï4÷ÂÐÃ/üm+àŽ‚¯}Î3"鈠ò¬|O92Uö×0Lµ*"Òóu%íáÑW¡×Œ¨€Qª ·Ãò¡(UµJZýñ]K¤¨#OÀˆ/ËŠüºe{ÚdS8*š+N#¦¾u ä(<œù 5åŒgô‰–þ$%¤£…6'ûœg4U­WØ4u5}Ño"Qc!ÅÑC~ õòŠB{Pá#v¹(ÒПL“°Ñ%µ(‘Ù ß4ˆ ™º;,Ìê0jdgÀPq“w$˜¶f%ôGe“¢ã|×, +ªUlêò¼706«½ a¯0ä”I–CöD8s;Íuæµ+6ã\$"3œ”Æe†&ß&B¨À‚Xò'œÝör?¼¸~šWøHX¡%ÿ²W¹F9h´1?°Ôq§óÙÐÍp2_|é¤)ÊÔ9(ðžáBC‹Q+¼YŽ=V¸à€ ݹŽìt†mxgºÒÈtþèÔÉ’gÕa¿ð€–èç œ-ë7ËoËÊë’"ïj³sßÀFažB|ê‘Ì{Ø”Ž’pëQÒ“tºrX¼ lóÇÍÇQágž/Øá…Áž IÄê§Ö‹ܽsÿ:©Fu¼!8hœƒ4À)hPLä€ÛYTEz;þYI…ÁŽþÔX!îиƒ¿„5Þ’\‘Ý/ºÛ–hž«K›·tO#9 -¡%Ų<Þ2Ê°ßÌ“(iˆRóAaœv‘!BÝž’wZ¶Œˆþñ^ñЭCCŠfB®'€=1—HT +qþ¬Qp‹J,œ1hÄTˆ˜%¾ +寑Ï5‚ú4±3 Ïsª‚GüžÒ—$w¦¬ÿÀrÝã‹N Æ6u:@ãF5ô3xSQd?«2K!"+Eú”š …¼¤´ ^„0 ‘'{%v×Aw þq µ,œ¤í¹ì!·S˜Ó,˜©íHå­tËüYk®\Ѻ7IÂðÝ \ä[7%d“õ©S<Ë„Fêðü¢' ½47ÐH/£ñî¿hõèÏ©… ƒ*ƒ{~Bk눎´Ð¾/öõ ý€±jÞßëª(¬À=A>jhçÜ-ðô˜GV?ô†ÉØ .ÊûóMÀÑn8Î{…Öî¸fQ^“æ/öÎPxIØÜLu‡ìœ,Äe€(QÊWfP……XÖ™cÓ‘}­û˜U]؈–¡g‹çº—Í¥ ŒíD¤àóK—«jÍ|wvõP•Y· +3öq•–Öa„Ã'{šÌ:Å=šŒˆ,,$KˆqÑ'æ_#hxU€ÎgcÎàP—ëks„<ˆúWmÄÎ f2­^é$X2Â\Qü*ko„¿/JT8]L§Ås¡#û@¤²9Å•B$ÿþõ3;ˇ¶<õœ¦‹¸å…"ýEÑ=ÒcRvýh(¿œ•ªídòFÌ’¼Ïjy +ø©*§øKƒ]žÀsÀk§¤Ì[¢xpùÓå|²XQ˜0´X–7–5L‰]¹…¢@VHö0u¸—º(Ôq²îŽ¹´xZUO>Ê+›Âô‹AëQбnUZè¥kÖà1{…ˆU^ õÊš XÆSò¦%e¿Võ¥[ª™Q\ Qý,.•gÉY"›S¬q«©"¬¯ âò!!¹±V_{i¦¿–[¢€ +Îzö"Î4¡¤ËtÌáÊ4g±à@U £^J¯h.˜€6k\Ô\, ’é’¤hçM™½#"Ô£@„ç+™_T'ë«ø 9Ln 9ê"=”ÔTAáÐd§Ö(fTňÝ\ï]±ö²-DVŽPê+ýn6!*ü˜Œt…™²0’€‹ªg>rßÌÝ+–qôË ¦Tt# Á«`JÇ—\mvÇuoê¥?À +VV|ìé0–ô‰]æÞžÁ û:Ÿ +É>ÍžšŸí󊣜±ƒÙ¯ö×W€òÁãÍ—Â` ¿ƒq…ÍZ×váÉ%–ÆkßM%%%\e&{‰wƲP¤þ¦Y#Iéay£…7»@e”³S3ìŠÀ +X§LÚK#Ä?œ­zUE`·¨*v›çû»ÂlGØõbO”^Í–‡Æ DY +¯¤Â9¤ÖC¯°Ù:Bá5:P¡÷ÖÃØûk@ͱâòW¢(šœ™ŽŠQû…e¨MíÐŒ` 'É"¬ƒWÌLÑÖæJOP†Ž2Ð,¹‹H÷K!éAسÄR¢ÊmBa¨ÀKꡪòn¥ô8ÆûÎ*rçì”#Ý/¤¥â9nu?/Èü–=%óÕ:¡@#SûXIJ¨5J$¢ëiIUU€ƒ˜ézÊ |³æ|]Éß´¯„讟ÃTkçmòžªvY”É–¬šzÐVÚ*®<`ÈgIjj»ÓùÅþËpÁáØ¿áQIæº0í—^˜¹•ÎÓ µ-58KÑ”UÆ•.Øû¡Keõ‹@Ë}W̯"*“²•ùv§&M–ÝWˆpÐsA·[ûBE6kVJ”IàȦ×C™!r8´=€ê“Ððøp]ËY Dþ@¦Í¡äLiY‘ÕxýŽ¦d•t,lŸõŠK ZìUü y»Å“½,/i?ªù8oJ½¯L T)2øeÅþÔ4ÙÄróz¥ðKHá˘Ê+L,³8ÐÂw9” Hg›!]“ˆó Æž*³D¤«ÿO‰F*âBôˆ +1~ɺ 'wŠïÉËl†R°+>À¦‚Ô]^Ó%Ûq*%Ìóদ$=QõŠ‘1íÔ[קÔpÐÈ&{‹¡MM‰;w™BûñXÃgÈ@ÎÈh#ìI§ä5Ã$€e\ƒåb6_R»+YÚŽ Ä΢üƒäþÏ\îp@.‚¡_†¥*ºŠ¼}iŠ–àÅ:íAŠC; úáæ0ÙžfF3Q;ø› ¸'À|‡CëüB6ÔÑâÃôdv„(:M dÆ‘~ùþhM°ß0`‡Gç¤9Ïgd='ä¡Z’Ô<àj”2›ÿñœ=Û³wëZdew³ÅCуÙ@ÝŸ°±ŽÙÀ|¨º°7=+¤ˆh'zšñ271ä—÷}I<øð ű‡Dh“—NÏ£–ØDRŒÖ!àÚÀ= +’=¼ Ò’¹R‹k„Òk“¤¬„Ìqxã`VtWÝ|ŒGh¶0w +]î½—¨©Ü½»ivtˬ(¨õäD,n‰HŠgG˜$Ð=ãÀÓ¼’#p + å®°TRI‰º=±wO*#!ðiÏÒÎ.-šÎçJ½kŸ¼'—  ôaëÔ ŠBçQ€”:Ëì¥ÌQ²ÄNLƒ]Û]ì&쌮ÁAø Ør4eû’ëw²¶v¢Øù°o ¾wÈK= 1ʌـÕRvÄ LD×Úd2Å×BcÊÁˆ ÄOAC©}"Äã©S²Â‰Aä̤RÐ⻟œGžcßìvÅÜëР¿›ÌNÁ 8±5¶Ðäæãè2ìÕý öšàšƒ=k#B¿²Û[pÇ>"¼Ò*^~¡|ŸŸoNcí“Ä;£°ò ÐS>öÎ Kd…{™· j!¬õÙjvf0*E¶+ÔþÕA‡IUB×þð´HÊ^:Ýdæ„û)ôÕÕJiŠ»•U­ýœ§ä£BûñàÕ7êš6qñ `uš6× L w«ÄÇÙPt‚F¸—L P✱Më€ÕVD¸¸C1øúÖ÷†™çktïv˜ô}žÍó<"Š &þÙ‹·]`A\Þº=¼NA—bmÇL;¯rPcƒ +§95ÈŽféqÅüy€áîŒI‰úŠž|F•#$΋­ J–#›®StQ— +Õ1_ >É])V’¾™OD=‡ÓÕîãàÀå›möþ^ÊjµÍÖ¬eôo"¾­ŸQl :DïGÇ¿ë•ÎÊ3‚øÕå•÷iZ±‚òpÅN4‘àA!c±2SinÚ³Ù§»C]Ñ:|é/sVkë¼I°EꮆgÌ +5ó0õ@´€•ÍvØ·’ 4U]—œ¾ý}0YÐU•]«4½, wàlÝÕYÀ¸ö¨æO¨ÃºsW'r¿pEãNG„‚`yeJ£—ˆjÙ!4ƸRT±äª_ìæ‡û¹‘rÛã||)¼‡GjD ¦LÑl[DÒGÝïóxŽÿ(ÛßeJµ´âf(»{§A „Wb©О yõ·_š_~ÇU +Š:¸E;:!9¡/Ù& oè uO"~ã`ŠÈ’ jSìRÓzܼv›T›¤à.€\Å×wê']µ^tÄö…ýÑ dèWJ©KŒ%áéyÔ‡©WµM"XèÕ ñv> +ˆàÓ‹{xQP*OpáõýF d£u›Ä&) +À€‹ÆyBûU“ߺƒäµ}DÄ>ê‰Né&ëg»¡¦ˆz{XR|Òf3ä5*U¢Gð2Ž5lZÑW;‹Ã²8ÞÃÙ!JÈ)}¾™Ãú̹by{"\ç“š{q¥PñA6h†‘Š§Dä[æÅnÂí:\É8ͤ©šš6]üÖ‰O~eA™vhYÔóRÇWŸÌæTÁ™âò•#‚PŽ¬`£º„*i±œê¨?ø•œ´é9´+”ãóA«E F”—R(T¦\þÜÒ85C\:ý +å,·{_4.YW,. ±ÏÌ£Ž=í&ZU'»ý^ÖfW‰ îã&Ðõ åu´LXi\–þé^ÊÉÆq|ùÁY˜1•rë²/K ^ !UŸíÄ鳂ìíeV($jÅÇ‘Gxhä>áU#«%ÍÓF+ Ç}Orµ]R-´)—ʦ°¿p‡ µ480‚曤ª7]˜Q$c,ô¹jÒ‘ø­\è÷Ü!Á\¢!³SħŠ:ÔBõò°@p¸ú8²–#Gó[«Ðs)`´~TNxtIcÍ£Öb7² MK¶Ñòý³p'#lTgx_õPPžtävðÊ eÒê:c›~ ûõýâ‘ãÏŽ‰LØ ©oû¤£Æ‡]=Zs%αj«¯ ‘§¹WX-A†½Ü‘lMýÂÏE˜>›ïÅ^Àz÷@ïí'v›ì%êK¤ ¡cC +…Œ[¨ÄÊôõöÕ”ÜÙÈÚ6EsŒÊ–íá„­zä:ó¢X„þž ,à¼ô¿P¤¤*€Ÿ\!\R +Ø N*éÛˆ…@xw¦¸]®b s¬…HKD)c3d…€Ù0 Ÿ~Ó-–Ø”o„SRäÈûù ÌèëJ¨@ÕÎ÷aÖ"ªµçp­»²L=Xp8×rò·çEñ÷èìW!qËû/5G1žÅŒ ÍA ]ž¨=`²û¾àƒ!…¼ x¹ÈÈãTÎ"jž¨çrŒ(0¢WÁ¢ôØgiÀ\¤G(¨eæ@³‘š}üÙ Žº„׺ß9(aç#ÔÜ,)è¡$,h~ žp™€úí”ã·óÌ/ÇÅTKà ŸðâQF Av²ïxŽÔ’ „Øú®¸¾³ Û{|ú,Ÿ~ÿoÂ÷(!è± I2q&æ¶Qy¬)»@Ĉ=/µQ»3ÅÛAÈ*Ï!‡Pš”cÛÈ6÷X@³¨e„á½Û,1åÎæö¼Œ¸ååqº¹n¨µAbE™ŸÃy¯gÂá÷À @çöMDîÑ•*É|øUxøu”"3D¤—ãıw^Ú +ì›\˜CŠC]5!mŸÓÅûˆ¸c+1x»¾‹¢›=Õ lÎF8´X™¬vÈþò¹éÓK"yjß”kº–. mGuøáÀ@ué‰Óø⦠Kc{—ý󲎩¹ÿ æ¸ôñÛs·q‹B/­p¢ÂEP”uDú@Çj—ã:Rtˆ@ ŒˆÍ”¹ù9i“rÍÄ·…Ðp:×ùä³f½• C–­ë79ð©µ/&ÿ3ªé½É7 bò˜´†A!0-TƾúC’Þ?$dã5“!K½t8KÌéÔ7®ØôãG1'Uè„À*CÀU„…õØ¡»zƒ(¨ãòQ*±ÙUÖ†ÿ|œ´î°³1S®ñ_€Ö€0V•¾Í`~1Pqoé Îòª¡pªMiçäh››^BQÊ­ÀÒ’Ù¨‘(€WDt‰ô§VYF(È‚žß”›;#f¾ ø‘u\M¢ç:TPˆ‘MÔ”½zCsí¹N‰^ËÉÞµª¥åþÖþ&V¶& “|ÕÀmÕ²*¯'#QfÝÞI \Û7f?9õ=¦*ÕMžÙOyeˆ *jADø - +_[Þ­AD{Z¶çãj™†Ä~Ôº!À6Òºþ"çnÛÌÒæxþœZþŒà ð ièû#héÄ^fJtR¿A;¡ø†/ïG„z`0S[D‚“r¼G!˜I1¢©Q×$ÂÛ¯¾LCùˆÖV62ᯟÐiÏ3^–ôÌV˜ßÇüæã 8¼£'g²²†·ëëÀ•RÔšT‘Rú@3\EÇÖ¾ ÁqÁ 3Æöî~­³3žˆéT×øŒvÝñ‹Û\ü“â⤧šAÏùt˜PöK„p@¢>JåMs¶ò"±'kño€Öé€%û)}“&ÛJõ6dý<¡ODHÁ¹GÄ£Qk“Û`ë +B^±»õkÃl®Ä_Þ…aÎZå+5y(‹à|®­5ú~ oÍ;åûããØ4níû ÐAi(*œ|᧟ãÈsO=ð# +ö»ÓV}¾cë»Þ1|öŠ0U¼Z;zY7Š¿/ ûÊÈù¢õß?£DRÉ—JýÌ[€ÒÎÞ…I™Ú0HqQUéTÝ@¬õ=N•¹6Åà¨.²;sÕ:ú7ïˆqÅzˆbõYNTw…Š!¸tX9´BŒžQ´~D\¦àÓõÖe³…™ë‰†Ìq<òjë=5Ók[a®Tø"±Ô1E%¹2;"xLD(ÉiämõèI¢‡$c#ýaûÖ¯ó_Ú)‰!œ‹Ñòq-©æTõ#(^ñ÷L×¾pŒ1†ìuUiJ#np=³Ô;v8 Ãîb:ÔfèR·O[ŸÓ> ²À¾×XT‘ã=aÐGýøý ´&5¦u•˜2ÂܱÏ=ÔG®ÉX†N1äFu‹ %k?²ˆ´8Z±¯Aêá#JyŸS¿ *ómE\Ê) yg†Z3ï#Šðõ!®Msß, É­p>Î}Îì4Ë;‡Z:sªòòñšC°C Ê.ÌÇyÖº§È”Ö³…E'pAw3£•Û½kn÷zhû(Eͤ¶+5£ÓD@²ÉyKÔ&HÛ‡º>ÅÞ jœà£¼æ¦’îËL‹Œ ;-î^9Mˆˆ]¢\(¾b—ØÖë…*uæ—Øî–øë°s_h2áyu#~`pÐ9 ÀdwÛ¼Ä_`¬z P8¹Ÿ¢¡Qš#Ÿ6óJvwˆÌ3êFnG•#9’Bt„ë® óp«cûŒÈ¶!`;X£h:sHœ·jVtfïRèÚ&CXO Õ¿CSG17óDÍX‰íPu~ <ÊgLñ&püHˆ†´öè€è±@B QP,é›kBcúç R ‹{=®£EþSUz[¨[¬÷¹©(ïµÆÅ+äþC æíH…›²-ÔóQV¨{#V5:ùæ<} $ï36¾÷\KÍý›ˆòp[›|žÊgÔýeÌç½×!ˆ×S©éc;øâäËßtYÛäÞ´ªËÖzkŽñl«ÏtôøåhV%"ybÎyÀ´ò¬G#^Œég OG+,_AW +Í 5Û/¯5yÕ<:1Úse#%Ÿd(ˆ¨È£jKxlÛ¯öW òÜUø‘NEJ¬)rJgX¿jr#Â1îœA‘ží¹§ÑÄ=5ãþ.w×SMÜÉEú5¶w+´¸-N}FÆI9·W/ùtçŽ9Ö>#r¥Àejx$„ßÎsl>"ýUu~[RdÅRJÙ T”tåÝÏó…õûÈ_” Öˆjz>¬(*œDÁø«ý}î°ÆLj»Í¾tL%J>´ç Ù‘]ÊøŒÀ²cÊ‚Wiý¥£_E]\¬ó ÊJU¨£ËDÀ2ÍT«¤iuyŠŒ)ór~m‘dþ1× +Qnʪ|q–j䊇BŠuGPÃèô‘ÊçB›Òò{T)qeè¨O¨yÅlïŠí¨Ì’¢® +(5Û–g|‘ÞŽ¹- F, æ£&ºU±®©„Ì{±e´ÓšL%REÀF-1@0ö-¥Ô%ÿÛ®ðU¤H*å¯*®œ‹ÈÃ<HPÑÙײ† ·œN1‰!õÚ·ì¿Øj½Vl:6áæ“· +ø!N—¶õµ¦B Y€‚\a"5\•}—{Ô03@•}Ýð[÷ºõ®…=õñ€š‚å`kÃÐ{BøC=Ùˆ\Jä +px Ghû8”ó>Yð©È®ÝiÕ+ãWZ†s@d•_DXZèÍZ´XÚ•­ó\á‚´¹…ŒOÀ€Ï]‘èœ"TÖœúSÀ½+S†ÖÔêã:ño"ïþœ8?9øÿ,ÿ_y²ËûñK¢½ÿð7ëÎño~ü·ÿúÏ_¬üø·ÿþüŸ?þâßýåùë?ýéoÿñï÷—ÿëwÿé¯ÿîï÷óÿáÿüîÿãwÿáoþîOÿñÿø¿ÿ!ýþÿßßþÃßþõŸþöo~·.ñ«/w¿¾Á‡\ÀæÿCaÃüu8§ÿ=Ð×+¿núY‘¡úüÓÕ'Aë=ïL”«×ÿi ‡¿†Œd’Æ_Ú׿©B•=²2ý*Ð_ߊ@úWíA@ÍIuAÛRÁ‚©4Àea­Îê^ÿ-J)¶µ+°¯Ai`øõ +o‚rxÀhFŒ©2•ÂÞ#¶µT8LU[óÃ{Oç ÷;» 2]SÎÀa]†Öȹû  ¿Ô_1€‡íŸ¨¸‡‹ ¢"FôÛˆ•ˆE•¶Lü279+ç û²Î-™=˜{Ÿ?¯üI‘íªÈZÔ1u[ºí q:ÃÜ¿võjîê@j"z÷T_rÑ# Ô ŸÓP@»Jų®ØŸ³#’¤ëªøúÝŸXÌjÀÿ(Ž®¬0Ú±‚-`^»o§­¯.T!Ès50ZËa%a±Pi=Õq^õ »Þ3DAŽ`âJ´–*Ò +[p¸*ª­.?õîè7‚&¤GŠ¬Áň^SãÆ8{é–󎳅„WàäÎ}‡Uxì½Q(…UC¢*èù6¡û<:Öˆ øñ{æÀÐÓÕ™†›AoÀŒbúE•L¼)âZºQªIšÓ°†ã 4œ†‡p4íÜßÀ.º”wàú{ò5…Å&š¡€Qƒ HÑÎSìÔ8+h_?(À>¼”ÔS‘4=ÛówÎZ,ïl¥U|}c.ZþCLc(-Pßr¯¾‰À“G÷j«EcŸÿ#ª§–N»q;•gðºÑN"¿ÁZ× é!ASÏÐHòý€`c 4þãSw†õ«g–YŒ…ȵPç¥ÚXî˜ +ð-²uÚùø·zâ8•ëÑ=íÛ·9§5´¸ž¶Sæ?~…ð—ïg”ÂœœŠÜÉñºRÃ[‰ê÷Øzb›;Ñik@"J@«Wnf|ÜÓJ¬"Ù®þ©ìË ‘t¨êôH½éF`ÁÍb‰Ï8,Ó„N¢!Åøü.b¸Oc¯ýêóHÞ£È$O A”­ÞÔé);ˆ•t³LW¡%6y†`u“áIyy ð:sÄ‚Bƒ Þû÷³˜É¯)nP +gŽÝA›¦ R˜Ìý=â»AüEͬDV‰Uð3rzðzbV Åœß1ƒòFwIPÑÑžˆ®DãϾ$-~æzÉ q^ô¡î°uÖˆFgü‡žkž»ØÍü“·!œRÑâÆÀ¬ì åªi*ðŽÂîg}½Ã­Ž(Å¡W¦*ûÕ=¶$TWÆ(»Õ&{÷㢇Vê2µx$‚”]âØ5Œ€óMDÄîíX³"Ö¤xí+Q‹! +…ô’å v&Ë ²†ö/ïÚ­‡Ë:2cUˆC /Ê@x¶ùá$Zâ_¸Êy~wJéÁ»®C-t—! Õ’¯¨Ý“tå0åÑ‚ô÷;1 +Ø #½NÒžq…Ñ϶ ¿< äÒ¤NjH mþv +öäâQhîà˜ «Š;Ôìša`3V-®‚ fSoVXö3´/óÔZgÞ¹`r/«‚ÎZôðÂmºÁÖs*ªd4}Sÿñ:½HÀœ‹°&¶†w‰‡ þ£gæͶçÍ#çGx¨=Œ}¥; kon Os‹EãmÏãC¾}©D€" ¢]aj@ü¿mè%`öSÄ âí^èÝ8j løZÂZËgÊH DSxÚIqôr5¢°O +![Æïv9§óŒ¶ó lü½×[uêê@áŽ/¯Ôt()ª3çËΪ꺪¶®@ë½]ïΈIÛØJž·žæ.Y’ÝÄ ­÷ÿl¡¸z®‰¾™N5Ú¬¸Ur7vÛUÓ×é1™%¨™8Qý"ǯxØV"ýÕú8‚Æ%‚AG„ ÎEÝŠò=6Þ½CÚd3ñ +jü%ùK@ëÇõ«™;tVäîý¶#©XÝ/ño¿#?ïwJnKS­LÜQ£S•!ºQØQxË@8G$ëèHÓ;öãiõë4H^±åóèö–‹¿›ò Úr;©á ‚«|ºs߈Ro…4wξ=íçö´ocGj¯ˆK:ÆøéÎ××V’ó_»ÊüZ´¯dg¡VÙ©#Ó30“ÿÎH­¨@Ü=â’à¢ÌAYé÷ˆlPJ€ŠíT.ð›óÀ'ú^~_³<¤n0Íj3Jlx¼Üš“ŸÌ#$5IÖOZ¼¢4¥f@ƒ¨“6 DVœ¶‡öIE8¬¡Zx§mH±Û¿³Ðñ÷±PL%öl)‰2Õ:åÃúñì_Ö¡c$€6*>h-nØYr\+ + ­ÁG»"Ø)Cµ +é]Q§­gÚ NhaP;¬•¾ˆ”¿ÍîníxÝHMÑ5}` ô¸£žÌõÁ@ZÐØ +½z5?G,~½VÚ´4”Æîç3çW»-PŽö•>¢êÈw•äOM³°‘;Õµ™c¢…ä‹SdSW¾*#¹€ò‘ßCVVèŠÔÛçyp¯g²¥ÙäiJ’®uÎŽÈÜG@·çQ|?µÔ’«RÇ ÚcUÙiØÂÑfÖf†gšµžÕ99§KQÂ8¨àMõäìØ{¼å,ZðÕ­¨®E‡îWáîu…[ÆÞO#d¬=W±PØ¡xÛá«Û ñTòËóÀ?¹±s/C• "î+Wj²‹ÖcIE1š''–»;“juLàçJ²ð¦²iJüS.ž°¢ÏD‡»,v`hùM„üuÜ`=9ÛgÔecðÅs?ؤú¡ƒZöfÙí;𦫖Cô‘\?À6âGDf­Vpþ9îoÎsÊ}ÜZ *¥sEv¿ÃßØêäõD `“¾‰‚¤G× +2ÖR`N+éV‚PsaüŒä7”p¼ÈÀYQÞç—©Ðú.tÃ4Ë0Xƒ–qΈi ›*qål;t?¹¶}¦Ïf$-õBÀºQõ@†-A-O'¯#j4â£Ru2âIs2­8$Î)¯ßq|†]\tÐ8ýÖd¼Ò!ðJ@oµC§¾ìö–BÀCá©ìnŽìnxóbƒ“’vJ¡ ®;ª)=`sUzj¯½‡ÀÐwúºU…(X^G Ð¨As|ÜO„]E:fof¸_UøÉzsûëôçù¸½3¾å®^ƒj4@Œ“ k„ÞñÏ7p«Ã)è,}èf¦Ú5&jî<ÊÖ€îz=ºÉRê»Ò䤴w¢Øbq9ú9zv"PžA +C‰î @ä±R²Cwx#Y¨ ý …ŠËz‚ö±v¡  ¦«]Ö™Ik¤Ë¯•ù¸ô@=TÛÀl¾77›ßº"Ú‹+sl§Sˆy#_\£‚пֳ^L­UïD¸nôÝáÊÜÙ•q•djVÀ4'~\]‰ÊùÝY#FZ5>”zæÜXFÑOEQ®¯F¨l 'ºsHT¡‰(eùÒu•Rõh±q§Z‰û?áê¶ÉÙ"[൷0Ÿ œ›ìƒ|gO¦VRƒd8u/P5%j±üÈì±6­ÚyŒ´Œ'µR$LÛÃ)£=µ’(¢ÐÑ5*ýÄ¡¿ý4°Žn«ºÖ¶-ÆK–]Àœ­Ø¡R´x=¾.³íi¥ ñ +à +â'ܘ‘fŹ>Ú<ùrÎbÑÑc¾ÆTt ×ŸyŸÌè©løæ’šQÄÝŽôðôìí¿‰ +]çëIüú +O8$´ ©oºxCƒ_´L@ý1ùÒ †…d‰û€}*àø-ÂëôK‚áM:Zûæ<`ôQ|/ÀLf œ‚Û)Ã;…Œ +È­í;NÑým/ZI݈“h÷{nå„F9¤F¢*Ī ž4Âñá´‚ó‡ùes{ +m[Þ#Ò[e§_¨ÕDáóEé.[1¶çôz¬…zj‡¤ØEkŸÏh;­¦Ps9Ë7çq®€¥w¢¡Æ‚g—c-9§4à ä­Ï–÷à‡Wc‹§Å,$”QK…KÌÚ6Ì/Dð~ÛK@œ¼Q!fS¾ìZ²àTñmkîƒÏÂDJús!»ŸEë¬+QrH +ÕÃó öÀ†wîDD“ñ<$„”­iÙ?)‰S‹¦S®Täpà­·òžÅ=1ÅCJãÐë/àr8±nT(·Ì=Ø4–æm×pê- iC¿?>Üõ‘Ð~ó,IÐc½Þ‰ŸŸG XAi]ÏŒóô†;´$h!ŒyéÊÔœTd`âÁ¯D-fýˆ•À^õýxîÕSFs©­ãü«sÜéÞó|Óîâ±FzéßFÌô;ÙÈ뙼G¡´+ÔrÐûÌ·½-ž­¤OìRßÕ›¦zÕ•Šbõ† KR?#žW3åAÔ‚ëøæ<Œpä¼ ™Û¨7ßü™}U•ý…ÆSç|:¶Õ +éÅò]ÛÃoÈÊYÄ ô/W + øÌë)s½Eä–]ÑRÂDâýyí¨§1Öw:6v:ÆOÕÙüÜêLùxR‹*IéÞò©9t ÓaüàÕÌCƒO}dŒFUûÙ7è2–}Ž´ki9ܯ+I´µòH +™9Kãë8®¢,7ô6Ž[ì +q”‡Fí %«AûþN`Žä#ltÀã™Æ3Y›uØõñC7\œÆoZ5Ťelh·ëz­Ë®`çgD²"û”t†Òà7çÁ@3©g ó…Ò@U˜ÜÝ\ü=â*Q#Ðb­µW +ûÕ#;90Ú>“ +SZ¡¶åNjÄùŽ_æœ_2ý­×öÇÌy¿7YÛ‰eh¢„âW=.Ï3â-säƒE`È~§¨v“¥ZÛë¼.|Šò#RVVÞ*”º|š×× O¿ÞLihèµ…?GÞ:[VÄKõüÜj€^aW:»…©ë”M-wB·Ms±”È Ou/Þ#€ Q˜X+Xí\é3ªÄËràÉpfFo€$”dù…†d£c³•"Ž Ï×°=î;t‚ñ)[WìÖN:0_Ëíô@IŸŽ€’QlƒV +M©‚Ÿ +ëšÌPû1“<MuQ_=¬_%e•ñ؉ª°“2sͺ鳂ÿm$‰/¹R„ÛRî[ÀïŸfš 6.£sšµK¶5m‘ƒÂ×QÓñbÓ¢&ÓGÄwkâG"*l{è+”¹m¤¨æ¯¥½ÝCâ­;íEãCäÁ†6ÈX<Âm#ÝFïÝ1Ÿf¯D‘PÕn w¬WÄT: òRPéÒÞ(„Ún¨© b3e_Ë)÷lÇê.Ñœrüííª^ÌPŠ"}bEö}îײ„Ô2w3§µ¯cì9Èøáí\n;ëÌv(뤧pndƒ^Öã*õªq0šœ‚œöÁS“Ýù¸E2ÁweGĆæní‰è;âÞnÀçˆ`Ãù\I­”û˜Õ4÷Ú?µ;çñI®ˆÚ·qÎ&ë¶iyˆ"£gm`F"4jì[–2WâòÌ=þû4*Üá²&ýF‹Z§DÜ-ÚüXÈð÷Æ$V®X†Û&ûÌ;ûL+þ +†ËQ(JZwʧShH†â…s l°u¾UÁs]ðè¿(݇îu¥Ì·)^Ý,Júkjê 8õ+ӌЧ È9WÐk“2Ⱥ¿×£Ä* Ž +4¸s$J9Õunš6ÇçüoîSñ{/ׄõÒ! *fÑî$îéu Q´-ò0³ì q4,158ǯ!qUïàŒ¸ûš[AHnÅ xœÜcVņrmÙO¦EzpÍJ|uÕb¦,Š’-Mø«(Æ Ïk9 ƒÒôŸWX  îÍïüŒÂ¼šÊš¨À©0‚èdLªJu¦Ë™ýºY’®ÂÁpÿ…X9Dl#ŒºÇðLßæ¢U Ü“¹½­|Öb2 †·5YPãÌÈ‚@çV)~`­i¦¿Yåä'aØ-”Å c"Ƕv•t18®(G+TEÏDŒt]‡” uSã‹ÇT®# +uýŒºòD¡NAT)ý6Û`67¤´ví–,ÍU>Á8äTÖߣͲΠ¹6§=6_HèJd@:jWÝG„T –Û×PØTdÁ_•« B{PÍÞâú ?ƒˆñPC‰²`G]ÊGŽD“Ux/w,>ÀèÒìs§O“¢ï&ÔÚ‚ºÞ¾GäѼ§mç™#áxøUh¥­¯‚˜Ìü pô òz}ß‚Øš°®MÃûÛÎýmùÃq쎡‚ øpo©ÁÞí3â÷/a¨Ì߉† Õ J⌰DPNÇ\Z!0Ì·tŇÙ)p%Ëóc³oAMQ^4 뮂+Bw÷»årÛ–ËÝŽGŸQ8þŒ™B‘«ÄvP”×@¿{™Ê–ùr>™ 1 „ýyÈ#ä£t#Âk”*û~ÑCÛ^@«®“5P늆–#Ðô¡ŠÑŠmj÷¾®ä:‰›¤òlбH"«óëíyP b؃“ü.ù!;'¯t«s†7\Äbi|qÌë7…T{£}qEIÌdžµ»™#5ü•™!íFjûb£NyQ¯2˺7uÿ¢Ï(L˜t:¹œluf¡$¨Iòʬ•Ï(Ö*Ǧ Ù{Äïw²¤ysR¿9ÏÚC÷I Ù‚ªEìúxáÞŽ•o‘}Œ‡hWXÇ ÈÆøæEö±¿ÜÀø1˜ß^3 |ðv|í³ +#K‡™oDçœ}Ö u¯W—!ˆÂWÛ÷ºç^?þáDAV&JÖ>Qhe)"(Î!§S)FtÇ;W2§¹!è]F˜²ê·ú<W»¹W;ÞŠ¾Ë…¥©[èyü)w+Å)?ôy³žjÄt\RéœAš´n9GßèhîM®¤ðq)iŠÌ,™,Ð9S[\܃¦—ß´ñb‘ò= ^û®@ÍÙ’UAJñ<¼äÆœ{ pX8Ý’ÐW$¡Åvœ°çÕkûŒÈ^cK~29\*ý½Ÿg„aŒð“î1©dE’|ƒZG¹¥“ üRŸ}2†R`òÖæúF[æ& Üwàˆ}£¼Éúú¹ÝÒO]R¦ä%,Ãõœ“štÊ[™»ŧk(1>X dt̤#F ¶¾…ªÝ‚€ÑL¯Ew¿c‹~ßÁ;Í'ã¥?®Ï³hn]bn]5“)¹.“©€3Xﵧç|pUïQ5Ή÷®´ ´ÎÒe,yOd—ùš¾¼HÏ–+ìáQ^Hº¦L¿ö"›÷ ½@´¹kî‡ÅµNǺ”-V.µ‹I)) €þ{(Ì.6K×ë:z4üš=5¥¥Z¥2,,6Ä’‰ˆ¿È¼%ƒùdZódî–sX¢(Hîïú©õ|pû&Ѫ +BvF{Ê|À"Ïx”Ä+cmÏ0.ü&beOb÷qñ5c~,,‚t]Û \MØ™àj¢G.6F$çý3"˜É Û›÷ÍyÖk.xíñqÚóû ¡ÒÑâNYgó:§¹ØZÞ¤]ß©bÜŸQµ¿Oš‰+—tú{¿ëqT¦­[Í錃ƪÝ'Kûý]D ð«§^ð{Ô8"äˆöèJôe%w $5®OáZlMPº/ä ¾•¶%¥)Ô}0"ÑKçC·lÞhµ>m¡§ Úk½x囈÷E9…ê÷(´$å2PÊͤÐÍþlœá¸ƒƒj|§j’}‚æ%‚ø[U>]˳ Þ{.`häPÔ{@(4„¹Q¢"“¹ +WÎáŒ÷ÇšN¥Ãqct×1y,l*Øe äÝdí!°ñB÷Pä6üT~K%•µdÉAýﻇ’ä#‹žÀ(Z·˜Ó¢P Ö…ê†C&€ E3˜O|»>ÿž.ÍLã`÷ø]ò6ko¼ßZÀbÀŽ´ † [h„ƒPx >‚z$æT×R’îr¶¨òÍä x‰½Þz…½[¯Ë}FÛG+™µç)c›õ½`>õa{Ž/H¯-¢)Ñœ8¯Þ?~å÷àeøÿ‚î.ýü—<ó¶};È!Ñè<Äö—¹1Þ?ì(Á?DÅîìq8ä5îÉÜ šW*N?C.¨oÓ£h&#žPsëŸÔôžn óŠù(Mä/³ ò¿Ð_%çgMó¨OúÑÐqk,š¯CߤrJñ¨›+ ¶±‡cƒ%Úú´Nd+:èíësöŸCºHîN¹ûìN,_l{às¨ç¿­IÝ¿_@1“¨.Ò™¨pgÑ’¨}a‰P|‡¯rÆiqÐ^G‰&?@Ñÿ³{wEÉ}ØZ`Ï0¤°ó4Y'ibëí74Õ,”LRò)ªžÜþ({m. +}4ƒxÁÄ‹»Q)]¥Š5=Çï±jD‡›¢xcºS÷­3îŠÇ>hkäëo'gew'xú‰ë§Ý%ÆL€@ÖÜ,cSõÉ¿‹SávŠ¦ €mµ¼ý˜«5ëWOW:ÄÁlTs×Á\ùqeç«Ú^ ßÅ)ŽGUÀvù ÂùÕ+ö4ýñqž”¯eyi=Áµ_ë;b*3k´„âŸ2r<Ô¶#”ÿ]x’)ñµd\ôr@Û³'ѹ9‚ì¦\Él%kè€Ô¨GÓrÃIˆë˜¡Û®7i>›szⲤW~Q²ŽìÈŸŽlV­/yˆý@&‘Í÷¯DïPÓ>C¬Â‰3×—Ã’ŠÿLæ4ꎉÙNYÌâÁªh}O“Z®äêêìÅϧM~ÁÈ|N»‰Oë¡Å§fLʹׯ›Ü7Xc²ªáªØÅëdà­ÜB[ ºÖþv˜¢i„íþf¸üp§®ÄLzí%=µJðƒ¨‹!ËebúMb.Ô*Á‡~–\¥=û>Ï« D@nƒŠ[Œ’)h>ßr%öKDôÛ®€ãi=;‚­2çÌw…Ó¯ëu¡;†‹!oL¦ñ£Ôa¶4tPZgà´ü „:üñÀM“‹à甤áö€±‘ç¿n7pA¡…–Šýõ¤iJ‚Dcœò2¯”ÀOì $‚™œ474º®ý¨†®kAÙKtÛ>=kæ:Õh$Ê®´õ„eÇ•xh\‰D%7j¸º`Uu' [ÔÓGPÖ#@YÏþ….·{<¦ë1}…§+ôº„:M%¹rpÌæç Ù©]EÞAIJ›=°Ú-{º=OíÀͦzÃË­ñ V\, ;ø!-*2òfòöÿ“§i¡="ÞG€5ÚÞõs¨H€”ª¯ DnΈѿ¸Î¹ü´‹®Ü™+ù[×»?úÞ¸¦ k•¤ì÷aÇïåÌy(¹4º7Âzýªwž-ô}ºyëºû=]LB/n9|B_º†¼8?³ŽRÆÕ¯=ñ¦Vð¡®ùZ÷3ý%c¼)\o(\,wø[ÛŠ–ë!Î×Ê@X×ꪡÁ#†ÿ´zÍb5¦PCç\Ô¶ùȼúdEååqcÞÓ“÷è²Q¯]~¢‹º3#—Rs'÷¼W|¯ +»û+[‘9K{¾o•œê\ëîÿ­’Ó¦³‹Eûô57cžú' Œkg†£èé h¸õ]Z‘Ðn6Ñz*kª¸ÃÐBȆikÜD50æÁûEëÔIýŸÊ>)~¡äa~6©<0uå;aŠÊ¤øüÄõø؈iÈAÍz®”å˜ãÝf´Ž<÷•‚t\;e¾ 8x¯]Òh®t=ü9Ë'x.îÆàƒl³Ÿ+`êp€ÌõÔ B¨Ö¨sjÿ‹Â&]ãPøoYñ륊ö®Ü•¾’ñãÜSGoiŽ¯{k;Ýá… ?Ü|Ž@.K¤éü*ªãQ݈ÏøÜŠkQ¼Ø²½“­’›ÏMü-¨Þþ™…—Ÿ¤ì†þS¤€ŠòoÖ¬òxC2)·Ž–LWD¸05æA¹\˜| mtÙ¥Š®S÷òö‹êkHDp"+Ë–úÞv¿”¯j^O•1W–=œ\G„Aq![N’%rÊMÉ‚–JEáaôÐ{êf§íÇo¿?ÿßID·c˜zqï qAékjFØQ§Rpê²þÖuôÝUˆ=H¥hóázâ™ó^SÉK°Lâß +ÉÍî\7=ÇÂ-üý¾RjF…GŒ\¢È rv ÝP@¢J«ºíÁVÙ¨îÖµ/#dCÁ{0ÖÂDÄÀe,1jóT'ÜmÓBfæÕœp‚·Žp¨Pž +´/dýs“õ ¯±‘ZÃâ§û˜¨E ¤SM¶ ±í5 ¯dEwÞ´=þ±(æ«GòÕëxŒ|‘ÃZQ§ß‚(jy¿E ºl íû #x9P+²Â°— "Ì!Ê6ÄŇã®_bËÖlæë(æ‰5L¹P=©©ZòKL&:J‡l³EQÊXOâ*_Ôiè¥0ÜC}97úlícõaµ¾¥ZO + Ž–%Z¶q Ø–¬ÔýâÕwdp)iö5Ý _AËíkb;Πγ¾¬1gÏËζd†÷½Ñãö窧½E-·g¥¶+5V(·]ñdη¯%¼ÀAΣJFYO™L‚â#BIÀëÜöYeÆ>kZu1+3¿÷¾•y 9ºñ\èÐ \‚/†e“„ZX‰°½uЦøŽ°÷Ž!mI~Rê’¹‘©«÷õÕ÷¼o5ÛYä¥Hå¥i~\X¥Æ¾w×Ö4VfÇãxMQLžÖäp²ð«Êj_²fNc#,„Ë  ÓþúÂ$æÆArÕ¾I%ÎöÌQëÇ«Ê4@õtMÔ…ôÿ°Ï…ÒÚØì1z4ùF4OVÆ~ü…pØx2CÑlzßxÕ{qd ·ÕyìN›cݤ:¨÷†QQפ·ÃCqE|Ò7Î+ÛoýÞÔÑ[Yðg  +½à£Li•ÎD\oL—‚É1}]ÉêðÆ`EzÙßscÚC:ñè‘Áæ¢hš äªSdG,i>šD̬ØÚŽC¢¦ËÜSÀB6\òR[ý–mÙ#`¶5Ž˜eo›áÑuìåk’æÙ hÑZrý]_sÈA´@=ô_üzL¬O­zc- ƚ䇽¥nöÁbj#0G‹´Õê름 š&ªÀvÄXSyöƒV#ؘ-1Æ£”„.TÞ¹÷‘ëm?%$R"ªbò¤(T¥Æ9’cJ¸ Ÿ@c~øx=›Ì¨Y13(¸ùÈ\‰u]Ûß:ã%•ržr ²g<¦žV–°ÿckZ•ÈwÖ-*`¹–ÀöÒ0…AÁÇ”fë¿R¬¥Öãyñ;¨.Æ\õ$“#· šÔÙ_yÅZ>‡j#VÏP~=/¸$ùÁ¿œÑü9÷4¨˜âvY5%JA—<ð¹hò'ÄR@¦”>î‡|tݨ҈Lˆ©òèG÷ãvë2‘ Ÿ›\¯*¢;ÂTÎ’¢Ô&q –÷\(ßFmQ³,?úLÎ}¡|š9ÎJPþ>÷‹Œ%N™Ûæ —Ä ^„™+ýˆF™yW”áü9còuÉm;6ÏL3s ªaLl3Ì%7N$tðtóHÁ£õ\Î+È‘h¸ÓÉhk*ŠÏêDvÅü¤Ý3ƒ +:´EÂ*̹tÔîvû–¬¢±ª­Å¼1'Ê5˶ÊÌØ7¡!Ëq¥À+žÔضeÚÚÝ‘%`ºüBªÌ-|Rm¤<¸Æ±S̸77/£ù¸ ö²~¸’fl.¹cê{´3KäÏlÃñ·ÞžA|eK¹^ûý®{ë;`Õ9·p'Ó®CĈZz¶¾° áÕòë[»`Í£~í{>Ö-³¨Ü®S4Î6\¥FŸ?sù¦Ôån=D‹5—âlí—5™ëÁ&>ûb-"ÇJZiy×pæŒ2‡4ž±ëü\µ”HYEJ;|_MIç#ã]¶T˜Io÷厕$ ê§Õ÷ ú) +^“ u&Ö˜Q÷}A2Kí\ríâA6µ]Ì\¢ú Š;`xlåûÚ®¯$@Ä;CÑìuý±F í~ÉÏŠCûÞXx'Žr…-Ñö9aìž +3»gõhâ®3”%‹=Ø¡è%°öÓŒ1>RkÍn…•N”-1;Aÿkĵ—ˆä±;ªêF3[ ¢Š3áz›$täÈšÊóˆÿÀÔÀOB®L×$ëLrij=ðÉynÕØ‹)Ëú–ÔØ c±´ûÜá«DsnEÄ% ñY7Çι.˜ÂQKž2Ï|]Èf4™l1±»ˆtx•®;îOw®Y§¡ öŽ /]û™yZå(à‡\ÿº]צ)Ã[†tiW÷ûxú ¤3°¡qzbj¸%Rê€vRg“M +“2ïµæ佊HžWï[_f0 +ýl¹P Ja³‰‰Êšq§|ùXóP.aFA ˜ô®̾ÀšfDc£€Õ¶Ddo”_‚çà-nÿJÆœT«ŒãÊ6M(å5i¤ +›]¾‰ 2ÿLì7]ìZœN}[²m¼7•PM¼uÒ&LJ]wJÝ®M(½B(m­s}ŸÌ-ÔØàÓó„› ;ïÜÏ Õ¤5p[ µ!¡ã|fヾ¹å ëëË­}±-çÎ5]¯ š¯.S%´-N¼øî¨dÈ«šõB˜Ñ³ŽÍþ±m¤$)·wÕuÎ+µ9…wðK>SDÉ»=‘/F…pÁ‹ÈÖîÞ„6–Ήٮcz¬›DìçuÆìBшµÓ«%Ÿ +„Ke“lQEp¯-êŒq  a’âη&mÒø¹€=€î2óÚ 7)k½z@Èj{#2zË#à;?1Þì±/wá$AÇü°¥ð‘°@ƒ3Fo¤>%¬ØLM5Oþ.ÚÁSÄ^Ùâéº1u²¿šr1ü¼vŠ¢ÆÔ'k»1¢7.Y®àz (ø(A1W¼\Â4_HŒEÊû3ŽwÐú»7ÅÄÊz±)]‡ kE¥Rs~¥ÞËØ;Oo{§Ëxé66]&:ݾãQµÂzXø' ‡¯Ç4_¶!}³ô,ê}ÎÏ>æÚ †{ÛÛƒ.h,ª³^SÎÔ+£k±[b„¦)ÉŸ×Y×qÚ4 ù`Oiìj]Á2»fÀÉ·´M®4ÖVÓebã;× ìGúÄb,Ôw ­X%ä5½¿2N¢Ô•¡¬¦jåÜ“ë·¯‡wzžˆì uìâÉf£øRm¨¾è*$€²!/Š½Çèë77/«Bhh8\Dh‚™µ®Á+˜$¡Š68¹Ð³@Û „ֆ혡;«öXãûÍúß(É𢤉ܧ^ág9Õ:ŽèÎÞ=œ–)Ó¼#¾#ˆÇ"ñr¨P¸åÅŠ}·xã;˜¼ÊFš îb¦¨©+êWTp©_¡&êÓõL·ÔÝu<}M%½RxŸCWC—ã†šÕ +:…¤hŒ¤)ÊZµ6Åû´.³Þ¹ã¹UHbÓša¾FènbDH°G/™:l†TOO@Vî#. C‘ ~Ûwš³ ¯hÎÎGˆ“]‘CðÒQê‡/&œ;í¶ +°(6~«ýY˜ÁÝ›iK4t'öù“êLy WnÒì8Æ»äAµ“„éÆh^•*}ž]§ˆ«ŸU‚ƒQF;úž>Ù¦´Y”….¡a4µf˜ù²ßöïÏ.b‚çs•ûu™,$lW®¶ÉÝç&w·`ä~†:Z·@ò¯( $å{_íU"Ø~ï õì`ˆ'hžÑÙ¢ùo;óØ{êªÏäí Lj%ÃËÅ$ß„ÊêÊr ©Ù©q_hKéÖ]‘`G +°óØwf(4BÖÎ~Î3\,â^)sW9&`3\=4õÚ#ì 2°€ë"3eçÁ;­,Ë +é‡çæŠ`7ªF‡$ì .UCógý3éÁ^-–œ6.û]|'«¶°Ì&˲ե‡ØÍN¸9E•Œ U2&H¯'“ŠÃ²ãGŠúÏ¡>+ö¼ì¹,‰;\¿­{á•Ähž)¶ª]Óu +¿Ötæ liÆ&ÑE¿\Ÿ¯5^RÃÊ.$Qwâ u|z¸èÙò +nx”Èf‹ý7Ëü×R[b)8ÛÞ†˜†ÕmÍpnT2*TGËy®8žOÖúçt¡Y¬Ÿ猖4½]=6ƒYYS„¸rá©°ÄÚIIÀþFæëÚ„ûü]Ÿ^¶‡Yÿ¹›Y¢78ÿ84pEô”¾vrîªÛàÍn!SðÞاa"Ï6s¥OÇ/À‘Î~’Mø½Ôd)Q“ežU¶Ïу^ŸcfÄd£¡ÅFÀ½I´¸îÝ·µùiû­ w)ƒÞ6]PùÑlõ׎|ƒ¨) Îâ¡Ñ%êÈ"Œ6s±—~yôn¾„ÇƦ?úd¶‰®´‰ðœwêà(†3çq+I·Ô2 jppÛ\l~~ÙŒm sˆª>)6 jÎ Aù ‚SÈš*Nm+YîeúìÄüI lqÉîæRˆQhh_`oô[C„fÓÓ~ú‘ºw#.ÑKÜ$¯TZ|COÅä¹u@.Ž¦Ko´ )¸\ÿ‚* ê1µU¶‰ ;56^cXÃÁ·_b–Ž(˜µ²)Ö\éÝ`H¦‹w(Рý¡‡\Á¬9m*OÃÍ„FÕ¤­ŒHy2…­ÕŒºª3`^R4ؽh +ÕD· WO–ÛÀÖÏGÇusʱ·dç–?m d¾\[.XcÀˇ-„°TSÅwó¡e9o"/¯+‰ú\WŠçêâÚÊMŠ‰mlëšÄRÕ8¾A䱩ÎN£ÌúÏ{¼…¦±Râ±…û‡Ù?åÜØtqä[I ±ðÍ©L*>%>¨øŒç‡% oó@xP…«ù0GMSÂta«W˜áKxù[QãŽ5 ”.s„Y:ö•w[ãûx +:Âû×$pÖ@Ûü’µpûl]Éœµz¼·bLyZR°Ÿ·ßqðÌ ¬Å€x%í+¹°‚2FqK‚ÿµj“Ml˜ѽ7g ?ž7¸ž” ¾ÜmŠ×./wÄŸó[SÿŒpÚiR±°*AÁ,ƒöÃú£:¿…þ!/ß1…Fð{²")Á7[*PÑò€òóó^µË% +1;£ÌD­§÷WÿO¸mähÉ]:pKcê‹Â+ºá2ø·­îŠ`û‡pÞ + +¾@º ¬¿÷Ú¼£Ã;ҠàÛA[cd·Þ d +¤ej…ˆ–»ôcãØ+51¬+çª8E‘¢gHÕJö5öÐÇØñ‡r…³=oê-QºDÑûÈ‹ÉÈ¢`ã…&ÝžøÊ6\ÿQê Öª(äš´[ПÙå’%r%€œÔ!ÕÏß²*Ù²­X$Q¾×\‚‹ž*ý° ™9CX o–ÉŒJS, Z`–{­€}/ j€mÒìÝ÷\œw¤‚ô|z]ÇõŒ¶Uÿ´)ª˜_ç†EÐ êû¿ï=¬TÊֽ¯{s"ôË1€5颀L ÿ>Bfë‘ŸÚúž’Œ›·w03¶##èÒ´(Uõt “ê-=J{6µ‹¬0’V8Œü®5P\ô˜!NUŸ[Õc}¿ã‘ö`á»4ãÆbhÄ +•*ÛmUÉú^í-Ò‰—hÃ26T²Á»aÛ¯Ùþx4Ø·ˆëš“-­kœ‡.Ÿk"1ªÐAÝP¬ÿÈßrž’^¬@½¾~÷çÄûý¿ÿ¬­ý¿òD~ñùoñ¿ß9·KÔ[CÞ9HÑHfÁX¬é}mV·q!L=g"ª„Bߢäá9 _1&#u˜›9=Ã0ÔÙõ€ }vk:[•îñ ‹ž@R%•{ +´óž¹À$H1¯s°-ˆBŠìÑÑ•–Ná¥{±¢š5äNOæ2ªFÅ\tzž&všöþý”rF×w¤ +ymÎqäi +æþº’-ìúÜ +ÂúIà÷z…/ò +<èò€Va¼Ìã!¶»UH"t´)ñ:ÍVnÛ1Äf2î¡Úª©MKðfn÷Ï[çía mºû^&~P6çõgßÙ%ŠÛð ôI8ÞŠ’RHTîí½»I!ûÐS‰r +f v³s%j½à"_%( ÍÓ…ªFyF(› ͯ ’NÒ;ÚÛÔ,ÎëŸ,ä]ºów–ƒ˜xé œ”‹n=Ìs@“™)€ÒU"Æ!Þ åv­R˜ÕÎ×ùÈ ÊýÀ1÷÷_£\’9ïÞiVÄ0Á;©ô|Q‘sKkLœŸÕ–fÌÒµ%-7ëdÖÙ)“&ÝÑVXÀuª¤Kí"ƒ‘ÒZ »X<-¡™^Ÿ¦ %Ù8÷`ÞOc…³Í¸*s$p 'Ÿ0«AŽà²º±JATóDñ¯Ô«#`ÔÅüÒÕ6ìißu…ËÙ –„þ:oæu’0æçSÜ#Cò9xápÊÖcBL’OÚºã–+”g×ö<†h èpÔ +ôv +d†GUóéAéûl–Úˆôjð§ìÆÌp:¸ð€4ðõý©áñš‹ÇÒ³· (¢¾~Çmkœö‰¨2’€ì#.ÚÚ9ë1É… 2p¡§G× do¤Äïs#ªÁœ”8Oꌸ7WóCîmëßà\Αԡ7Ú훨ºUß™ Ö¥ ‡y0 "GrÓÀ*á` Ü6µ“Ìœˆ¶#6·¥W=†>Žu˜VC×WPÝÀáØEC<ù3âÛ¡úUA¼êt³ö0ÿ¡ŒöªHäîàj…]§àKÑiÛü¼_=îA·Õ¤™;×ô^xÑš{<%€æVù·¤µ^ïëÜУ ýSÐ>ÆVµÚ¢'¥u#N^”¼Õ%?•ºç‰/Ö·Íñ¼IúÏ{VªUn§ÏâÖåÇé!±QsÃC™/Ö¦—‘-ˆ2:ÉyÛC}ÚçFCèÖà«õŒ?¤cúÊ0y™§®<,êZ‚¨ŒÑ”L.äLä½G¤ÒýÇA7€V¦¦Iž)y«ÆßãA¡Ž¶  ™h%.õVц]ö>Ö ác+ªÚk‡»2$V™8dNŠÇ#ÍþH%ü‹Ø¿˜inÞË¿Ž-/¢Á*;„+Û`«†èÕðK¨RÚ3ßQvÙ›œ÷¤L­_ûšR_òqYGK#ƒó5;Ê ˜¦$æ­y8ˆàU'fJ.1c²>@ï¨çÜCG«I\»3v$ •³ì[py­¾€¥‰Ðac@á}¬WœfEIÒ6êÈÇÕrB3m;& le ¦¥Ýƒ‹ࣩ3š Uì©@ÓÕ5™IŠ¾Ð@:9N$Œ—ÛQz¼~ãB¦Få5.àä&wŒVÑiÛ· ú¥§z¾voGã®ýæ.WtÇ5ßhƉ¼¶qb‡žÚð%ßJÕ»3Ö§Û ‰æÓ‹5áf›ìQÍ„ZÓýŒ7SÄ\/¡¹ë¸=‚8êË4_î°V"…°çéÊîЙ‡¦œ¶F®–M»¸Äpî—±ÕȬD¿î 퇬á[SÍQ_«Gìüx»µß¤dB­VWǃ.SÙhš @ª×šj©Õ~^ÙÕ8àXTãì>¡‘Ž”Í¡ü{»R¬ +ëÕ7Ç…Â[8Üò´r>¢6A2›à1LûTÁÔ”º Ž§(§˜Å;p*õ3â÷»¢ìÌš5»‚LŸçÙØËú•ïBº˜”6Ö³ã(³¾öö>¢ nÛ\¸|¯½õÚisê5ó4%R@xíÍ´Æ‚qÚiaÛø\hFÐ&üy'ê:›¨—ΪhDºhë©ó0t:U#ٷΈÜÕ |æŒj¢J{ÜÈöU¢‚Ö¤xÜÃú‰šÇ¾Ò8Œ/¢e*?üÜè7êè`áC/Jø£„¸ÇsÙ£õdw˜–…ŸpËÒÝÚ-Ãuëv•5"÷ù´þˆØTÓê:ôÌïQçþ4ê¹÷ø&gpáBËK»†b-ŸÏ@ÞæÖ…ûsÖo> ØÁÒ•pÙËN¡]¯m) Å}©û}F±§”IÛQh|”5=Srä1£æ*m§<Úõ‘ œçx9þÖÐÍ ´QÀ#Ïz+yƒ²`Š‹¡6÷Çh@x%53–Ÿ¨WK/7»ˆ}%¶gè6—ðTÕJ;h°¸¤[ëùæ?—ï¬Â5¤†¾ÓȸÞÊêÚ¥•R=+ÑžzÔ´±i^©ú)øy[²£ #\{›v‰au–Àï´Y–?ÎGP–ƒ%€V Öœf»4¾bífØ”Ôôõ)EuUà_(|åôòD7üâ9ý¸"Á^¸Õ´jï¸Ê {Hs7ÊÖ^ÒåÅÖÞ:ß:Pd¤úó­QkŒxX‡YC°Ék͇é tZ­fÞA†¿‘ ý9·Gìp”ïYÙHµ1ØŽ@ñÊiq`ï)é^ †eê6q$œ¯mUÛbUë,ˆôŠýfX03Þ¾*·öé(Eß(Ãèj;õÂø2e?]ÑæJ{»[•^gd`®¤Â\IüNÝN4D(¨óp kŽ¿¶ÇUGôõ “ +2î½ +ÀªÖ¾ÿz¿¦ëW ‘«=õBÍ: ¢zÎ[¾éLùç6’¸Òʪ=÷3É€‚e]\n{®­ÐÞ¥7µêO…ð4• Ë {¹!ÅÆ}1‡~·½¶miSß/î’Ôl¡¦ç抄ê–-¾Û0'ªógµ3çÆn=%ÔdŒRvž=áB¿íXzwjPÓ{îYdUüHw¯lÒó•î´ÝŠ¢µYûºtN¢êþ>™¶èqß›<%:¢h¥îÅK¼ÛÝ¥2¥‹ø{º÷+íÏùëOÁ›Ž¤†(@[_ÃV<. ¯™Î¦ë¯ìgâUIG€$=tT£æŽšuó‡¨.ò–áHQéìM%o`¶‘kö /\:ÿžîaß:8xj^_Ÿ‘ÁÉFãñ»ÛØmÖ­»vžñÐÒ §nƒœ{~Ù#êæEv{¦B#j6Þ ¡2Ùhi 4«çtɦ¸¤aà—ï~ýÁÓ`UIO¥Ó,fêÃ*Ö²È}áA¶#Îq?ŽgÏ\ñvPè³4jôWl 5‡Cl‘Ë£cíiÑATY\Ù©A»ç‰8vÄS®QîfhØ1ª`uФW¢„ƒÎãqÉ ‚_‡N°FVe¤ UÕϯÒï‚tü˜qü8÷ƒÐ9£Ékt5QÑ ‰3)}¦M€ÅvÏî#âYSÃœuûæwQt®eâ4DÍ÷Îìΰé"y?¦‹Xùv_p£ÖyüÑÆÞÉ𿳡Й¢›£Œ3ŽýÞá?W2ÿL=†6³Û‡êÿê¯Û¦m§{7£b΂À§C%,䱶+3lɈšHÇÕ±|­l½Rpç'b®_|MOÐÌŽ¦ö Õ%VŒ€N¨÷1ªNÅz<æ ïý«ˆ\`Ækl~šÃ5¯%ÿû£Ôð#¾({Frï!Æù3¢Š+:æÑõúŒÕ6C¢EEJ{²QZŒ4®¢¬¡¬SûX9™XI·û3 A’ë +Rêóô¶­ÉÓ®ÀyœæùÍ-þ×aîê7¹Ò[TµEÈOZo.cû¢«üfus—tçQ‰Ñ>Ò›†Ä…îooÏë6äÌîû›óÌ;ÒµU²NV EÊ€öï"ÜD•Ó\µ|Vïð5‘ºwF‰˜Ø\‰ë)›]6ÏÖF­N@ÞpPÁå¯ ö.u¤.ƒî'^žñøñø8^ Í?ºŽôïž  ¢aÎ#$µ¾ÑÙÊ‹Þù¦å]n®ƒ°“9ñ‘XìÑ4F*ßgQEFMsŠyX3h9Þ!Òèõ.å³ð:TR½/1]FÔqÕW¢"@ X ª(Õ’^r§Nµ¹þ*k"¬¦%0T4ÜýgTφ»ÖÔM:¹èý™"ö°ÈóòKç—Ü.™h‚œJ­®ì쥫92¯sP¯·j;2ò‘Á¿ut»VlHjl –"ù@eK`%ŒG,Å(Pd2ÛGÅ{á¥Wxg<ù+€ª\ɺ.ú‹©·ïú†åÚ–DVbPž&;(e”3:—ÃÇ+¼QJv”e4BÉÅÂ3Êr}1F°ƒhÍg÷ó¥èˆhÙeø +³ºúùOíb+¿³ÝqørÙ]«%NU^ä3ŠD*u¢cáqdÓ]€›•4>nDÓ›}IåúyOÃ:~×ÜYLî¶Ç&J2Í×3àô;.ãlw>~ÿtñNKû` ç7§¹¯‡ •cd;K‰í,³ägÄ·kä{ JìQè¨8ì>„R)uƒÊãGÝvbt›Ôƶx1P4?ïfFZ ˆ—³Ò¹¥_©á<ìyÄÃ`Ïw}‡×OÁ‘µÒ_¯1|>†<,ÕcÇ(Û¥ëÖ~ákOíwî*t3J3°Éú£ùKêÐGÄ?íJ’³÷ÇÇÛ™² +Úw0óî}ñÅý…ëÐ(q6C’‰{Ó¿cöÇIɨ¶£öÏ ƒyE5­ §j†_ë½#æŽÐ$äØDƒ¦‡ÏŽ —kìOŸêYcxN#5ž'ÈͲ4t._N+i·$5§÷c½¸Y"ôÆdc|Y5 Ú,ˆÌl#ÍûqY,Þˆ“졆Âz#eÖàaÄC_ƒ,fˆ¸Y#2Š›õõœÿQú¦KùlŠt÷¨òö˜Ó"ˆÈ2ÛUë5vªjùçîïA—>ûVz³„ŠÆßvnzæb8ÿbÆgâO³¢†¡PP±ZlvöyÂf?MnèH©ë +K>çuf>P;c¤¸pEmJo‘o"Ôs"1 UëyÅÌÀ¥êJeµz*¹g1çqõ³˜@V½3ÿ¼µ´€!hÇÓ–Fc²c Ê2Éð¶Ñ¼B´u†ãlj ~œ2RW€=…‚)ó¹å>­[Nº¿‹X_„Þ1J«óÕª9âß`”t»PjÙ¤·h­7ÚQæ +î‘u»†ÃoªŠîJ +°ù*E á;Ÿ†Hî +CµQñHÄßèÌ´í¸CÍTp4é•jG4·Ëæ–°(‘•Qf`g°éúÞÇãÒÑJ:‰åŒ·Q¨“5ˆ o%žÍ+P…e‰`U ÏôËž¬m9ƒÈHëxû2ÇVýç; nCÍœïHŸ ó}åì[Óuèj„ƒ®œêÝÝÀê󑿯¡ac|´H­ÑyAúªJÔöÄïC½$+Ù7…O³G¡@Œg GdŸšü½¸W+JEÈ`f™sãè´Šâ;ØåÌË! üHÉö0Ê/Ή—å°ãÔ¦›¯ŒdE(;ô\ÈJŠ=ª’+Miª·;Þ\‰"7Wb«"V(°ý]¬pá>sÖ\)8[«ÊëéÈŠY·øTX‡)pM½]óLÆ÷¶¬y@®ÃØ6m”t~ ",æ«[7•ÛW¯öíÇy`³Z/¤7±·:ëå¼~ú¼w‰’¼Maëñüݳ‚~TؤÀI=_ŸA‰HÚÞ×fy{BÍMuû÷,TŠu8nãÃLxgêQØL“~³N¡£iÅËV4ÂË×2-„ uÉÔ[#y=N2|{5n>>®ÙfôæL­üâÀ•ªTc¿xþbÍ0h”[/³ H†¹™›ì‚)¹k«µ‘¦ÜIB)k¿¸Íù“ºád©"—ãé6…û%­ûJñI¿*_sôEXOêâß|ëtóØE}hv›…¹IiÕV<U;ÇUhiìçŽñúº®2•9AÐÿ¹IÌÊÇ\›N¥pФê ‘ç 8#ÜbÅãý@îî;»ûããX΀>_·kè« âi +ùöyôo"|Ž#Ïq”Gæ=JÔ=ÆÄlôÞ_t 4j‡ÙžhËåqà’Á¸&»Sƒ½„—5/^ yߣüIdÒ ªúG´RI;»©{DŠš)E)êÙ½§±AYk-‘57¥-ŠŠ ªXÒ× +DÚÏÂ÷Gĸb*DÙúA.'Šm'Q3*š Q¬tKA¶(ºd¢gY/–?7¼‚ÝÖë‹ãxDÔÖ/zh{%V–¨ªÏ㩇¨+¼Ò-«™kž.žu ]Ǹ"=/ŠžÆ’â +ŽvK)k€ÿÚõ›FzÙb´|«ÄHšlÔ]vÓüLÓœ1kt*fh~ÇÛbÑgº6è#¹t¾ë3"cjË8bRfÿæ<°CÁRâøçd^ÛVEÞï +ØM™º¹¨TÅÜ‚Ž*c (:fn)Ósn>«"˜Bm+ùA÷™=€Ýp£ªžYsmƳ¥¾ÓÒCI>£Ä$¯½»~%'c'HÏÃY_¢36î- ¼sÆ@4¥+¨J!˜!Uûe–ËÕ(´ÒGÉǵ½.37ÓÉšקº‚ĶCaý5xÊðÄ[g¶‚!˜ _E¶ÑÙ˜ÁwÎ I}aõCpp¯iM<ÄGh+ÕF•záH4¶xm}ê6Xü°Õçß=–‚îÜk`ë3"·kl¼ì¤~~wž¶É‡u4thH‘k(}“M'Bãq؆ï£oÑS§>øaEi“(ú뵿ÏÙß<¶Óê»RÉg`­çâ&¯£7Ðé^–ñýÆ”ï~SøEÍèWQ +xJ¬¬ûœ1üÉž¯fWÕ Ò4‹\fürÊùµ+ +ùþ%dEÃ6=•–*|ÁÜDñ kpÖ¡pøìósmMfúµ¦*M:[ µ KŒóêvµ#@"ÔÕ÷¬–‡“Þòûº= +F< +棺E¯ÖÏ)ÚìÔl½4%ÔpeÔÁ¶)”×C_QjQæGéB[fU•ùÕ»•qñx˜û5ˆH3lpv×oºN!NxÑ4%á@×t7z?¯1D³F¾É/R íãª}˜ /„k ˆQ m½³ q—v´6Óå©­+ ²:Ðu/Ÿ¢)@ŽAYøúæ<”ºåK“«8NÍ­}@pÐö-l(ç}²Æ»<ŸA "Æì\¬ HtÿÔW\kVp?(3Ñ1CW8aÚ!´dRéûyƒçT÷BaçÄ +‘¯wKloÇ= ¥úø‡Mõªü6ìîÏ ò“mÿÏ2îÿ•';𨿤Ôû³þáüñÿæÇû¯ÿüÅÊûïÿøÇÿùã/þÝ_þ—¿þÓŸþöÿþwù¿~÷Ÿþúïþþw?ÿñþÏïþø?~÷þæïþôÿñÿû~uÑûuæÂÿæÿå¤]S4Ë`¦Óìi½ÈëfžU]±7n&ÒNg~âzyኄ©ó¢l¡¼ÎrÙ¥å̯ã5äSk›8¸Á\ßÊ Lð45¾P¢a±xBî9H;âó zkjô· Ùy?±WFÙLÀTN²½½Kœ¦¢\jÓî’}§"K{Þ°ÃŽûFHZ¤ì±k%$™aQÍâÇ+£VQí5r ß9 –~ÝT)?Ñw“$I°¸þ¤†umâ +¸²ÚQ€és¨æÍöuæìQnpˆ3ü©»+*”âa]Õ¼ˆuhkò.#)jÚ’ÀÀ»wí6}Šž¡œ’Êê»r'ÊkÑ’ÅûCd¶x¤’ɯ’jo^-ZL*_xYD[ã­üPÕ¯Ö½½¸ìéÜŠ#Q“(ꫬ$åÙÏLVÏOÉE(Ÿ:ÅYÐÀ¦%OOÏ[ˆ"Ó¡*PÇz¨>˜ÍÍœ.öŠsÚ@ÜsCÒ„I Ñ¥œJpÍ—ë%žmK”ˆ,q”K²ÀÌSO¥•ºض±ÇÍ¿f²ÍÝRÕ_5¥¹qÀ÷µÞÎûë@-Ö–þŒé¯ƒ=5_6„‡IÄObØ"AQï]q:zc|Ô‰ú›ƒXzêãGÙì^J`•]Ï ¼T7l5ÍžŸH- n9l‹·ÜOµ­xꛥ³ ׆´Õšíñ`ð=fr?µ-Ǻqû·Ë>ÕΞ$ƒ¿ÓTô}žòÐÉa ¡…{bKñi+—•54S|+õîž¿dü…•ê©:íctcP„*”œôB6>Âê9} ~XPíþø»ï0xÅc|;D-[´§’Ë]=¾G€…åG9ž™èž—QÌF䧭£Úž]±=^Y6¦ýÚs’(}š¾ëEgSp%qCWb%n×V·~jΠÞ``C6üL&žs릎BRûoµ£lÜ­ê–‡ÃA Œöü)?ÓL²â\bì¤]ÀšXòß7DM.@ ´-›(Àù@ Ì[G`Y +qˆùé§JZ:UîÏ¿s6qqõ›ƒ§†:ÑùgŠ[KÐñµÅ´òbÖ"ŒíË^¸‚±p­ɧ֣ÙKÝyo«ÃæŠöýù$>!n¯áòSIJÖÛ¸ IÔV&ŠËiÁh +Ikô›¢Q×?K¸ÌsÍ9 -àu­Ç¨z:îñ¨6TÑHô|¦Àð +iáô|ß´j|¾×‚’ávŠB jèÄ6mEÐ:4âVDƒÞ«‰[¹Öµ•Z +ÜÌëÀÙm­%5&mVÔň&be¢\itY”DÐœ%T¼‰¨ýNÄÚe +ÆRÿ:W‚öÊ]\¡(̳m@p#._À@ëà=7„N)càoîâ²´´×Ö’êž”òrÚj¨ ì·ýHäå„p©Œ|³“ƒÑdÅzûDN(÷!ÁuMì” OmTb÷ÐörÀßÈÖý™vQ¸EA†³çCØý XóÓ€îíkyô8>-ë8¶¬êlaªê¯‹Pÿ#¦âª"®–ì ¯e¤sˆ¨ˆÂ HÁÛÇ%€žtë@P&× †¸C¼Ø²BŽ’²ë1º)€ôLÖ¦jÂc4ƒµßXÏø7ÞŸŸ7¦)>°³’‰„—Ú‘ôWÞùÃŽBׂí’‚ú¢GáÜ„çq|ã‚¡œØxDÇÖ½lÀ…æBpí 8¹¡ƒæÆ^|‰¢o¡»ÖPÒdý°¡ô÷ZÏf÷<뀠/È“;B£¢5Ì(ë7 ë^éŒà•hÆp¥k÷–ŠÆrÛÆ}²ê*{ŠN µ©Ëê°-rå5fÈÄ•†Ö1¾‰pb©¬ÊÛÁ~å®(†`,Äv ÷J3µ ²iyy=ÚÇG‰Æ/=ž´L†RE|ȺwQb‘_ÏñD”žÀßéó÷±¡"P(vxxâªMU‰dÕt–°á[ï ‡xæWcqá {Ä,¤N¹2N{oC ÖèSøâ PÎkŽ=…=O£ž®ídÛT“ž mèúcâUqĘ4ÞI'ò!ãÈ!¬C§Ø§{Ö ú¥LBõË: +µ`¬4 ,â(uŽE¥¶òyÀŸ†HùœŸqѶ¨-×-zgP¾`nüÜ ×߆zo܇x)õˆ­ã/| +üc¥X0YÒÊêÄpbŸE}ô˜ÄªßO‡ü½íE¼ûÄ«éúèøiU‹Ù”Ùw1ûĤ¯ç`ÖÊZ©ÒIù6‚GñAcE»<[S»KҵαÚòÞ:ÚäÍØü©>¦Psw!ñ+[Sõë€dX„É[&O÷:xGþ´ÛÌô]x~R#Jgàè:yMÝÿ9à«xþ¤ðÊÇAö‚ú¢p 9DÑñõwô*5±…#¼AÄ_¡ëPý_/Ô>ú=j•Èoîší®µ¡ý¸ÉBK” =œÌ6¼VîLIz?ˆ5Ö[rcTP¿yBÊÇÜqË˭Çcsk‹„U즪Ñþ†Tì×. ÓŒ‚$må•:@°¤H’;zÅTJ«n­zÝc6´ oÍ+Å@¯T.À€ÝaqfÝXoãÚi¬©wÅ°ûŸ›'}ØÆh–ÖÅa¡Š6Ó´’ +íq&k +8UQ6JÇ™ƒ…¤Aä€ ¹fë¹ùƒy÷+ȇaÄ<&µ 8Q=Àb;q›K:ë7¼›sóµNëêß6ÏsѨY-c:¥íò]H›?„þrh]ÛQpñ¾*»výÚ~ý×”‹µ.„ƒ5Ôj: ÕûÇo¯ÖO&¬9 éÚí5ys0›í—×¼nŠ]ô@†/_ÍÚyoÙ½q¬+r·ÉØ=¾õÆŒ3Ë­¼X|‡.„¶LÕ'#¤± P–H½áVhÐÙz¾¸ŽZ5ªž *™þÊ™— øþ%ó› 2ƒ%7¤Øg’xêÂ5«?L™4Ddß5°—\ ‹æF7»Áûo9Åxs ä£WŠÿûJëN\Ò<×ègzõàn‡eÄè£^¼!ëÅjÇvbÆB'æ ÎeQŽ)„wÑæˆU(+ó¹Ž*¡Ô¶×Kž>±™èzŸ‘›I#õžñ°ÔÔú}É´t,Òz"ãßü…'T ÛC$¡4=uÐü™G›NÂЩúGŠ×ùûu‹ÌÍ?ªiOWpÍ›¥Jýˆ³öŽDÑG÷sR^½ á»Ra_#Òº«ÉÈõ‘\Êï8ôK>†»`”ùc¨¤û­Î‘×Ë9‘é‹;ŒÓ9*gˆÚf‹ùM„uµùz¿>ˆ¸Ë!YÊuÀ7¶=ÃÙB t-¢<ÈeÆçUa/?®o"œ&.* ¶^Üfžg’ôEV_KC`%ƒ¤gk/f>wMá|oX$'î8Á 63@©ªöØ‚³ÅE²ÌÍKG@3žÂnÝ=6I&¯HW?#Rv EÏzÅ•£~FñD¢P×VµÖ}æÛÞÖwTã[chkâ¡ÎTP¬ÞLL>ž7Ôæǧ‡"ƒÇi —nÊ7fçS%y¤¨’ªÛ{Ô±­SB.–Î=æØÓOõ>;»eDµ;hCࣟ¹OÙ„† gàoÎC2r(þuBóØÙؙ諑½"‰ãI"°=<³¯5ŽÄÙ!‹ÒŠ›óIá‹=•-Í£î'q×ä½Cúõx}j7ÓsŸÛ(ÿ&¦ã®Y]_v`Í¥]÷91º[Þ;Zã)s¯m€‹ÈÚØßjX£Öiè}¦8Aɱ™è(T’ÜÒ?±NkõŽqÓ)œdÄÓ0G»FC=} íÌ]² fÅ6"Þ€E¥×[™Z\¸K‰tÿúÄÒ‰(ºm·°áÜ[xÉNZĺwˆ¿ùÞù7`,Ÿ[ùjϼ]ÄÇç@P˜*¦€ÏéZ&jìÖ{ñ.ð)Ã=j]C°ÊPì4«8bz¡†œ§ë\ûŒƒÄ>\…W– '5E”@/þ½i§dnïuïž+´5Ýÿ°éY#wo?Œ.†ÃãBë¢CßÝ+´ëY™ËWµù½ëAŠ¥ë 3YªTþýÔ Hw/œª 7jÃSÿÇëQ<<ÝͧZa£þõÃ?ùs4¶ïsOfÛ•Ë).¹°uu´:ð^v/J1nÝ\¶ʃA0¦oÙæy­IµéŠ<kMCë1hCÁw4K›z›.—Q!¡„„Ò‹Ë×+@g™S›èô:8);ã´"Ð…{]iÐ4ƒUI¾Oñã§Þw8’]þ»«'a»-DP¡áî$3×5oÀ:猈@HïynÀ1¿¬œ–2éÁQÊ©ìí`kБŽ‰„7UÉ„}%;&ÛøYõs0Š Ä¢Ñ=UAl°‘ ¹UK͵êžß]ú¶¿ ­÷Jcjo±Š½ªAÐun~|Í@)äÎ…&kݺe™;¬¿#eŠ_šõJ­bÖ±?Àjúåc⊢ÍMüaÔ-[…_7qp»Úk+£›»(ø:8îøR4̲Jž(Ê\§Û !WÂàa+Hߌþ·Â$|ß÷¿/Åo‹'¨¦²ó`n©ÉóZ>9žŸç—7ÆûïFaõ&Þnãú8u³â-Rû¼^ÑN.ìƒY¥NJ¡*é¿EäÙÂóÄHãÖaû›ó8DxϘV[œÆ…2°Ž“ü}@Õm1d~x²ŸQ´à(–7±ëûÖäÇ´x¬Ù°™£”-LŒãEBs…Uè¬Y¨[ϯgrѸBkPѽꓰ7j5àÄv õ§óz ¸¨Iƒæxgt µ{îëˆYã cCiˆ2XQLµù6.<4ðɃªS*B˜fdÒ-çûVA&õL¡ŒÙ€ÈÛ÷KžäxHSDYæ¢-G¢ªqiO~Ú G‡RÒŽ%ÃîyVFÊë[äBö1w«Ÿy6ïÏÇy ®ÈŠüjÑóܺ]èt±üˆpômËýzÓߣ®çÍ5îŽ1öžùÂ÷V¡¯0uÚH €hI„zøý†0Ù¥À½”©ÿó4 ]X„AºhÏ€V ôÖõGîP¢u€å¥¦GßYB b†bÌH= uA.sˆa‘Òe!YR}Âñ3@qɶÅ%·?ÈG N ,=åvô×IÑDòÐÑé~`ÍóŒù¤ÑÞúèÏ¿ÀëÓÊ»ë0Jajhñl„Šb,AëÏW4……|¢<éRE|î$×#"®ØPüºRVWÚwT„ï©û6sòíyxj"„6-XÙáöÃ+±)…+žˆKúJ§¹¹_V.Òùu§Î—wɸUº)h)ƒ OÜÀ m[éUQ¯˜`NÕç'}DòU’ËDèrê• ¢·®zòGßÖ›&ɵÅ?ü~gY•[ ÊâQ7N¡*…õ-ªÆVvÍůÚ«?-ùý§x# ;°6p-©oæݘUÞ×d0“G8Éøáj¼¼¦Œ/ ÔÛø< ²Wìؤ ¾ÀM€Ð~ˆO˵o^ÏÍ{\r ¢¦G ñ(ý ?ˆÒÛ #(TÎïsõ[‰²É.i¼q©ª+ÿ+"Êxî5QLÅs·T ’¦„ñþÂ'{ù¹¾®˜t9BCM¡i “N& +¼>g¼CZ™œ¢?(Ó²çüy ÝÐVÒ»4ÛókÔ°¬ø”^g0¾ÇÁ=]›‰I>÷NÙÛÂÈïϺñ¥%ˆÎÁ7Rç;•"¼’îëÑE½¢‹:Öݼí_ŸQâœæçÁ VÝw©é”!øÙº2º7—X¦û?7,úÖ…–ýa”}ð÷=Õý‡¾ Œ]›’w¡³dä×ã甬öÎQ³¹3zkf`&™þýïn + ŒÞ :Õ%: ªÅ’°¬Ÿùõ÷3ðغ²ÎsOܯƒ âႈ§¤¢³oÊÐË2'oõ³3ò§fÉB› þ&@AØêÏX“áܲ¸ò#Ø\û÷¢¼Løêšn 9$8ø§—Ó‘¬gö9r·žº›nŒ"ŸÚÜIŸeæyÕ_HYU ÜY¶©FtÖ/,]ë/ mÒ¸]þÆ\µÑ{K@ÏBǾû³ÛoD†dF¼Ä•¤GHGlŠæ+š%{_¾xîêJIöQ%¦ƒ•K­:S¦]¾B+h½ +T'\ +mP‘õ Pùë@ ÄD§¤Ý¿Ž#Z\š¦]?®+ÐðÑA– 67ÛçGvìC–MBlÆ8ÎoÎÓ¯Jh^b²©k o±~.¼Âψ÷å#ÕÇ÷(lŽEK#c^ÌSºéHÛB…zò…í$‡M]<Ê3[Ãt@`gÑyB…¢)ƒé*EÑ5ÆʶþÜÖ;aÛïFä„Š+ w”xÉS$5ø4ûMapLÁ+i2•æØOO1 fð”ÀµOé¢òûÚ»Êwúï»INN•˜&®DÙ ßZo +RF_^’×FàHZz¼7=*ÒFL¼ÖåÎù‘zø‘­8@jqçÑᘇOº;Ʊýr×»tÕ~@³›N&­æ‡ÄûÔ# A”Ka×®n5X¶PÁÅMèwtSõ×W¥Í±­•±lѯ·0U¶ª Z¥ÚÀÞl‘c5¨¡Å«FVÕo· —<Í_%þ’UX¢»Îø]CÀ•¶éзûð‡%žã„ôº•âReßáNa$˜òΉ3„¶‚ÂäÈׄÇÕµ¤êU·,6O +‹ DKc;³ñÇkqKK%7=±YåHqÏMë–×AÞaÕw;õìF«"~óØ 8£Ü³Œ#b6wÏ´µ6/fÛ6cq£c•ýOý²³éŒL#ÎFùüÞsî°Ž¯+AÐ ªÕ-;x„4‹¹òF*œMÁŒûB+š2›À(5ä—(‡þÿ³öv«º,YzÞèÖ¡¬ƒíŒŒß<´7ÆÚØøÄÖQÓ”Ú °ÔBn7øîÏóF~«jÍÙU…­Þ+ÇÌÌ/32bÄïO”Îù)š8ŠKoec½}°ÔËŽ€)»šá¬ +\Á«Óì5é+ê>¾ˆå̦ÍjõMüT–aq@4†ÚŽý÷BI§«Ðµ´T2 “^êÝ6aé:Y»»0I«FéP"µ€’aìÊÁBxÎÌÁˆˆÖ•Í ½ê +¥ijÕ?ú[ˆ°"lß÷~ðPlÈôöCÕªV3À¢ñ¾úµ^.—a/îÙ¦s +©{g±ÙÎm¤‡ËÖDIbÉ@ÒËåÄ‹kYŽ'} +”¬Ln+dñW–L2É+äáeØ(–Q~ÆÛE¼VГ/áäTM~qïb. bO DP% ”-ØÞ$ÊB5ï\pno;JQJ‹£´eË|—&è7)à¹];r×uDÛT0»÷’z@Nìn;°šî‘1ˆIµK +5<\lÕÝö²j‹Ô&ºTýÿòwòûßðÛÀÛk6Ù8=¨GÊ2óŸsp À¬X©~ˆŠâN+Fš]ñW€4oZ{wD"§ð-ͪT*&íeåYÊá öÚ·Îñ·¥² PQ²Ì¿‘P‘;-ÿŽã̧QF­Éíì:˜§›þ¨š[m|Ó v_GÕ_¯ØMÕ#⫉%™6”Õ̪ ‰É³ï‘ÆeMBx%!ŒÁº¿ ¯kô.8HÝ Î(Jþû±¬T^ÏÙIŒÒî9­’wËFüçl·bt%úfûà…7Áºžj©Qb©a=…¥›2l”âK­¼ÐN'Áñ9aÏ ŸÍꮿ%6œ| ‹4i:^^E(f*Ñ÷©äµšŠíž`!ý`=£K@¢\É2ïìI BEØÏ#6‘=—‘Ýž§Ò©°¾¶Œ“™ÓÊyš +{¬tÍZ¨ë8QU(× 0Ýû lÕbËèÔˆÜ|Þ¶Xí8i; Ó´ÔK°MA‰0c¬ ”Wó +fà cð(ðx³¥c((j¿? !®.¥ºð9¢³#€| s9 ?¿þ– +›b9@ƒŸÒ£lO>{ÚhÙa.#à<¾­Ýë7üæíZݾîOÏ:êÕóØ¢Ÿê…γåáí¦pc÷…|ß(‰Lõ¨}Ý>ÒÓw¶XF¹]dº`»(h•JD´ó“èèzWñô ⧲ª\Ú{~Ü1EGs©F_ÏN6þ“¼AƉØh,ªp;ÒÇ€ ÓMòÖ¨!è³¢Ò]Ð}›[T<.§#ú}¤xUŠ›®@!lq£±Ê 1œÜ?òùéé½ §-WÌ)Å»Aàc¿Í˜U?TuÚÙ‘[èx" ø<§)r_¯¥3ÎF§8~;¾ª%Æl£ögη§íó­ƒõE"a{é+$ý¦  ’T˜šI÷«$ÒÔü§šûd@Á÷¥Ÿqjã嵜߹h{yÁ”(Æq %@hb:?¹}…^Ô¢Œ , íjÖÑ̼­Ö´ªxK^3r¬2P=Ù,RØë!‚Ùb—Ѹ¹VŒÇ†5ìûû¸Dá«"3&Àó9õ'0ã;oA ‡¦™Žg/úx½‡}\=hsÞéÆxêŒZ ³.qL¼!¹_ç;b +2§fÛ•*„¯ûüàë·^~ Á´Ì6:¿È ±¬§UFòBèJ}QÞìÎÞ/Àö[3»äë_WÙ—}3äPÐöìTÌòþ÷qÍ0a x€ªp¼ó€ª®ξŠì’V¨qæ„Áz ’d'&êÅê‰rà–»—Ý÷lЪx‹W;ù÷&zÓÎ+îCa +³ÿ„) (=x¿Bm™U àÀE…ˆv::T´Ø +ÛíØO€›AÓ¤¼¿°•n„T©.°ÆIùß{qÐ]tpGÄü¨²\¯@¡$ð|³ÒÊrô\Ê’—Nv$î9ä­ýÓ'²%VH™LYL¨¦`d öÅ!$W¿SÓexèxŠt³[[°‘wP¸ví ]Nv®/²ŽÞ#M˜=ô\ð-ÿ iwÓ]|,_í4^(_ÀâŽ&Wy@È c­[{X5b2{òìjçlûKÝŽ1[R…³°ÏýïqZS RLÓ@``úr>‡ΪDjLGöjF¡‰z‰æ7TäP8ß#‚Ak¢¶ ÅÐj~ùS +QaÊûË}p¡¤æ(¶°:-®½>¡=¯ÐD…Yèh¿³S.£§çT¯\[fº2ÕÅfF¾õ +ø…-9õ€ÖÕ¿ž,\ä,ãþr ÛJ‘ÚrOG ÷ôÇŒÑU{hëìÐ5»´bq‰•Æ.Ê—O´oÜmßñGÀó‰{óóŠ±d#ÄJÄ}!Vy¥PaÍcJÊSìžVJœ|òë>²*$Î|Š,ò܇'yÕ@SÚ…KÌ°æQÎlÁ™ûH£ºœ7Áæ³J©f‚ÝÄÞv{?¥xj±ZuW†úñ 6h9Í]…‚ˆ½„ +Ý«„pÐŸÖ +PºLóý”éáO¬£Ê’϶Çg¥Ã}­—‰¶ß°ùróœíÊõ×»íÙ†’˜Ž×Ùªè vW $'“ÁÊÍñÂ{Í—ÁfA ÌhMï$ÚëýÅãÿ&…Ò|ü0XSÅÉ!­ ¶ËYß«=šâB… © eí0§Âi¼Ó8FWñ´…½ÀŒl»¹SòÓ­#¸ˆ’/¿¡¤O«Å‚+ë Ôï(T¬Ô<àbÏ~¼gk§ñŸ’)«NçO 7ä" +tìO–þ˜N.v}­×Ñka¦€Î…–Ô…«P m!»½æG)˜ IDAvXàˆ©½¨WPc¤b—zéعä!‹ïß)ÕÕåIË]å3§ùÈgXâtQx‚Ç¢>4d¡ÞÓÎG§“eÖâ6‚o˜ý±e>ˆ‡”ùêQTTv:¡j¥jƒ[פÕÌG±<…üJ÷ïŽÝT|¨öR\¨!äÈ £A ’ÇV*ÂMÔ§˜Õ¨ÀÔý¼h8]@Àe¯S&ÕŸ,™9ë3}Ç¡ÞÂ8–(t Ÿ˜_°Á=ÍæØÊÊÅÈ }œY´ø¨-Ê&=ì/ù#{ÆÖó~F§â17²ån,•A'/xÞ©ƒïŸ·Äq»ŸoÙÏ+.?{ÀÕZ×ÙRŽ(ð²iŠèé7±,„ñ׆S»åòÄG¤Ñ¿œš0í}L©! Au¸÷ï…&J{_ þþHÛÀv$O âÜvxºÕ9ÑO-3ì¤RAÇ¡ ^4o™mì^ó§< cÑúr}úˆ> +Û»âv€m8k±–À‹÷ ç…8s•Ì0Ô„.Xq·í“b“‹Êr|X»öÜ$ÑþÍ¡º«Øέ»š:ˆŒpÞ”ËÜsÇ^Gj6fê<šÃ‰vã­ÉÚÈ¥¸ÓÏÕ|¥¨¡Þ"Ü‹ÅФ +ÿyjˆ£óA±C®ù°µ¡ôYR‰†CÔíL\Ï.ÃT3z7 +¡È_#‚ÿï¸j®T]-ç÷\é êÃ={¯³&Ô#Q–P.†Rý¶Jpì_dð¼=Ooâ1Z@C;bjæŒO…ûYÔ—¼¹ç&F´mg¿ Ñ#.#ž+-–aîbŒnvòÓ^µ<#Ú ‘qG¼ÛȽ˺þ§dÚB÷sŽxת WO„KDë)vË:)’.à»÷²ªH{t2Lbþ!ÊËè2¢8x€“X‡ªÈvÁJu½œ˜ ݬ º‚6°ž¦¢ŽºëX»¹óÔØI½ä +æUá¶<€xl;R/$»€Ù¡P&°àŠXÕÝsw‘´•Ý*‘®‘ÁÃ’ EŒÅ>Vhpdpe?lª½·{å9À®À­aOÜÙ£¥¯«W?¢0Ÿ6Ä„˜¬÷'¨ÚÓ¤V”JíÙ=°»;ï+EÕ +´£Róq13ç‰3¶[ytÍý·ºf Ðߺ` øG8]Å#™å{.tcRW±Ð8ºÖk9û®qÅxتkÅúiDQõQ™ +yé\¬nÚUÞoÙ¤®ÔØV¢W@{êBö¯¿bÏTFk +×MfÂ|ÈA‹:SW–í8Òé"±­À\_Åuð¾ýF4ÖΪ—UÚ¥+rM[d™æcFÿ’¢Ø ·Œ¦¬Sý¦™BÝ/ÞIŸ+ ÁºñUž±^¦\{Jaú±m.’áˆ.“¡«DÕ²½éj¶¹JÛÒ¶6úûpÁH?”ÌG¸‹%ܲMÙñöÈèB„âdxÎqK×Èô†qa‡ÖI¡ Z'#û…îÊ놣(ˆHí=RÁ;ª§î0t 6ìÇB¯Î½idÛb†ˆ…‰ç‹´­‘}²À+qrøˆâÇ—ë‚Þp̨ñÔˆsã +沟ßÎ,^{>B-£¾ÆJÏ‘¢Ý‹Yo³j«o²©í–M#Œ~ÅŒ­ÑF¦ö:˜µlTMàÝ'‹ðíA¯Š=íÓn•ƒ1a ÄÞÈI†ÕY÷u &½ðþçu" Ëóœñ¹€8$Ëc"µ4‘ºå­AU®ùw-aiÿ×­ã‹îA›õ!Bq3ˆ:ûr±«ûÝPA$¤º…¢øt«F˜Êñ…«+"h¡·.ÆlGž»õÀRî‘Ûtµ(»ïkåùbŠÉˆÀÞéVÞ<’[z.ÍݺbîÓe’û”Aó_G/[tl 5”f þÔ¦_t…-ѧâ{¼¦ýl~™ ãNk«\¦cŠEû‚ɇ¬CTŽ1Ñql/÷oB#Qz_¶qœêÉ ºn»Çõ¨çí…E}OC#âf7Ó™Xaœì`oóŽZó~T ¦Ð0Ñš< +&¹j5_“OÔEnÙ¢ö4À™évö g–ý=”㘩î,¹Ö<õ +Öw³%H™ä6ËT—*¶¢¶|1W@ѤÜÔšU‚/œ¤Éæ¼ú©*›G¬¨×µ³¥àn‹WºBŠN¶ã– qÄ¥–l’Þ1‘µˆY÷R²žé¹ãÛGÈÅïe;(TóáÑ{ËIƒ 剾qæÅÿ}g‰)N…¢\™ÊG(@53örì‰àÊ0Ûì¨EéúK”G¸9e"•jÕÏWtž[ª8Ù÷€Ô{7nZ~\² +ñ[t'1ñ@a`Ǭ0ës¯nü¨M‚ªuÈz6Ë£{ú,]Äz'ׇl>ñ°WYÀjÔa\rÑéyjD3!e ÌOìî¥=…Ú™•¾¾ÝÓÂüÑ>²­ºãV=¼dýÓ Mnj V†‚õ}4¥#Tͽ1²=îhп†·*°ŽÏ²xÏ‘$eˆ"GÄ4ÑÝ>³s ØFprCù‹Vm£ Iö‹ëåË"½³7øîeÛ駃ºn±’¨I~`¿¯n’náIÐ(Ÿµ³Óáø:bÁç™Q6‰ë8þ1º@侞m/ñzùáNÞâ^>æÌ3éYÀ,ù±ªwtZ1ë³ö$¥ÙL?]cÑ®›´É‚Ý{Ô £?óýûÌK„“ëbrá1¡Ü<ÓÔø\ ŒôBÙ=È/™PßÏË­À  Ü‡ÛèƤ@Ðr+Y[<¯æá´OåÏ&³SzÅ7Ô¶™5óè°Êø¿Ÿ³°Ðô祡8¬jÉjÀr;ζûO"¼@3GLr'㛨Û2çôÓìó¼4;a @¯á3Dz­çs¥/Q×Ê2¯=Ít@W{q5”Z¶ÒÄ[Ýf$;k®×ó9΂*ñB•fýÑ_•è+àV®¥'BƱö‹²lÔtªâOc‡' õõ¦h5Tõºl‹„ÃX¯éΟ«€°zØÐ_…¡¿ÈØl5Û±1Ñ;“;+ü‡VÇõóXÍÃÆô>·Ñ‹»ç¼V€™ñ= Ó¦WCJàâ-X å¤¸\àmÖ1uFdþ>¨$ÊŽªœžcµÒ­ç%¥ç×a¦]a¦,Õ7Q÷Ì]‚ÍÊé°žH‰è¨~Á“Â1 j&ROD=ýµzÕÕøË1;Å ½IÌÌm¡ûÖ_#¾’_¢ ºVmú¥NÖk4]1³—œõ¨1ÉDmϦûyÓî<üËbl ëÀT¥g)pº}*kÐ.™)»Ö‘ÒÕžéÖ8ð£`.#!Æ‘ºóá[;†D0+R¡³pŽ¼2‰4>*KÀÕú Ü÷‹éZÞzÞ¬ úOaŠ{ÛËë·®j×@CM„hAfz*86¯s@’_àK.S¢.¿\O;¾¤Kù]È*ÄåG) –nM›è÷ßy áÿ_7÷¯©þš+6ÒåÛó°©³¥ÓVæ’”º÷Žq•wë’Ò»ŸØIM7:$,÷«¨GT—/K#! Â!DQð8^E·)Óm匔I9` r_šòŸM½þlŠùW:ÄÊ*] ìºE6!Ц#VnºÔ‚–9%¸*V;Ü] èŽcàü^)=ɶ¤¢‹^ÐÉú-ÚÍŽYahÀ?¦“„ÉŸ]¥OûÿƵ‚UøXlrñЧÑ·wZD; +i©"Xž’>q‡ÆŒb§Å·…w˜—ƒÇB­ôŽ¢Ÿ&ŸÙ6㎊Žñó§õrŠFXUÑÓ‚âéÎ8W4j•.§A튡µup2›ú|K>rÒflwä¥Û±’qïn”wÔ¼K*ñ6>›ÒÑùs¹å<ýŽéÆ1z¡dŸˆ„1ÅÅbbå˜wå-íqÊk +›ùßH±aÌ rL`+d\ yÎÄD¡³GWyu“™Úþ´¯ o­J¯î¨Éjz1Ø’vÛÐÖçÑÿ¥/ã÷¿å×›þ=€º6 j"s¶§øzv]‚¸a½ï!……'Aä½5Mt¤k/y’W E5"•A*ˆ Qþ À'¨$Øÿí'|§è¯ûÿ> £%4‰íËô$ë›(• DOÞÉýï åÖb{ýÉŽ>Õ¶b=™gH­™mROý0+÷†Qœ+jµ€•Èó @š«Ð<¦Áò²üjjT'joom-ÉŠNÇ‹Öoˆ”úB€¹u(˜^vÁªŠ)?¼þ,™£›Kά¡{öDØõ<‚¶”Ý'HĈK´°5q»ãeŠ"ÅŠiíIn— %± \ÇïÜâL¸þzÐ Ö›`RÓ©¹(?ß0o£+Ç·ý +9~’D@†£5ð 5[¾6žd¼}FQBH9Ï÷+еWýëaK|_¨IZ‡Ô›·¿Çë:p &€¨¶|TÔOÅ«ÒgýmoéQØ+A,™1]¡ûz%H®"| +PéQUéóø…€²º· ¥‰]Sæjçê;Wоô7màPeÍ[H50[<½åø‹Þ'Rñ[Ú£Æ~°ÕÛ9JQ]7¥¥;±ê©š‹Ç¹ótÛœÅfa`ß‚»ß×UH@ x÷ß臥³%ûXGyüŒn©—ù€Mp³ûÞG½%Ë?àÇSsò ð_°iÔáöÖ¿cdtSäš–œöCž‚RÖûŸvÆPΞŸÐXK– P~ : ÷>0„œ¡2yo€g~ì™Èø)>ˆdµœÛ +£­žJVró¼ËAÖüÉnéã}ôëÁqE›Ÿ ¨kO” –Çë%ŸÉù'²§*ŸÚò޳¼”9`¿…·2öô1üÚÈ˼œœA`l+Å, ˆ÷ÏéÕÁæÑŽ+í›,jnXS½Ï&ùO£”4Ÿ‘4ߣ‰žKdÄ+ò4¬ tûÝ‘fýzà¼ÝédñëAu$j08@äx]@°ÂL"×s9ûg0ƒ0b¾'t‘aPÝ ³v±ê;Ò¯p@÷sÜ_Ón0ó¯â‘xÎgc·Iqër3%Üö§§ç!Ùc¹Âìàêrôî2žÎõz_ä> ¨š¯úþ¡ D ^t‹D®Ä_K†?9à[¾°( À) <ÓÈÆ3†Kéµ÷‡$#ÕõGî@5…EœªÛσT>yÆ×þ„ªú$™0ÖY²hYîo*Dñ/ü‰hÝ×oþJ?ÊŒtí¡¹gFøûïŠ7pkhŒ#æs q’ZÐ÷¹GÙ:V¡ëìܱT!—GñO¿/Þ±{#"7Q]ßü9e-EÿŽ#)íܦÊ>ë×ã&Ó%òöøO~JcOèu;¦EŠ>â1sç–wîWa¬èR—UÕ3˜6xÎ`C©²ê¯|&l¿;‰³ç,¬íʺØQƒþ¥ª9Œ»W‡qD¤j +O]ü7Žº0@wTDtÍ{tÍßÝÐ@ö©ä¯U}~ý«–›*'õ„ŽûxS²Â9GŽÁGD¯&µíá_ŸÝ-œK­±Éº¼—y˜ýœöJ¸—Äu†îsàxÑ÷ãò÷å wó¨Ÿ0t÷®d±™øy ´WÐ÷9“àç`CYÔ܃CT@OWÕ¯v$4TõNç‹v2â ^(JÒ.þJU =Ïø~ s¥H/UU¼)ú!âzÄÛŠrQ|+û‰qÀìå&zÖÈEQóó‰ù+ÆÙV¢7y–%~¶Irôø¨íÀˆþѤN4ušŠÅ7èÿÐÚߘY Û™Â<Éàç Òg=ÒgÔEí–0¿þ*¸ßuíúF‰ÿè"ä j õLæ23Û7áÆ¢‡Á +ƒ-ýêGÌdz$¡üžYF.½/;‚i¥PEÛÓuŒP`ÚZ7â»Ý¯±Toà×xûCc à=íkľ0ñCcQ$§8N>ŽêÒ׈ïfî/Q·­f¤€°#\¾£h…Á—ü¸¥Ö'%€1aíó0ñ»¦ãÿû)Ξ’i G 4§a»J[¢¢ñ JùvÈ€}ZI¥¤[d@Ú-ò¯´ƒD~ +ðDv ×ñ%ág¢BÁž½'‰ÈÞ¾d‡/þåì³¥_«·ò|óç{jkópSèg¨%HŠ– ¶~¬`oîÝ:[ã0™ê)±·×]Á¨z¢Êe`Á:çrs NÄ<ºž×ƒƒ& +G‰L8öæJÇÎu&‰˜Wˆ˜`È `Lu:1M4ß¼½mÜÌÄF°=cþ#E~¯£j5µ*=¡[°vËæY^¿uÙÝ{O†EåÅEÄëMäñ¨ZBv 5Êü:¦†ÚØô=:KJœcõç6>å¹=bnô’õ §5A]C¸ÿshW€ï×»J¹Ô(ÿhéÓV#ªG“èù E ( +±xÊ}žË¦ås0(h¶ßq<ëëþ\`©±s:õ=ôÿd¿ÀaÄ|¡b«%M%/ÀÇ^g™k,oÅ +?ÙËåœÂŒ¼õ¿˜~jLq]vy{(lh7Ž"Ïák¯z†wÝ»LåÊ`ö Ý~dMñaØW\­Z¤ÝFÔ׈ýDém¡Ä4ßr³QóD‰e8õµŠ”Àaä×i$Ü:¶–µ£D Ú˜|(ÑtX + ÅÌ°õZý¨ûKh•û4s£üì¾b<­Ê>üÍ,˜Y»÷WÚ…½Jø@ÕK©™övC{¹°- ¾ÄX& +mEÖhq§¨ÊkjÏÝR¤¾O‰º³$Võü¥8-ìÛO²9j4öwT”Êó&/·ElÿÚj׎¸QDD… +¥>aÎÁãbÄ¡‰ªƒ±çwüs¥f(Hã%úª¾–Åå–;:À³$kA. Ôdöœ/&:§4eÌ“¬ïÒ}¾3› ¤×ãä¸Ï»úÕ¹ˆˆ&Ajç&㎗±ÚhCMi¡5ƒ´¨7õ:@±þŽ @™ÀS/{ŸWÓ/³hþM~=ì®ö”a’íPÕܬÎÜ¥«Ϥ·åahH)¿Fdè?ÇÁ?˜»s$EHàYrlD¡ËØïoP[#/ÙC»\óåfÿõE=•æå%QsXsz|P²l¼†y>Dg„þð™%´¾ ð·oN³—ÃhÝì¬C{\ÀPû†¦‘Q~š´^ÀžÏ]f@gäŸ+AŸFÑÛOJ™ÂjxKb€t6º:#䌮«èÉGDE:³£ñÎGDÑvßQV ˆÒË¢åÊyBuBBLÄG9ä ¢pµ‚C±`^ç^,ˆw«È/õÿ>W*2pm¾÷؂еä,ÖæÓŸ`°úÓÑ[xÖ»2¬¤ÍFѾ,(9Á„¹•'Ææû½É*¶‘­_Ä`-_#2¦ŽÜ—?_-òÞÕ ªø;¹µÚûb*ù*ÐJó¿‹Ü‡ãF¿tßêÙñå@CCü@õÈÞú:¤‰ Ëãa­†·°7Ÿmæ9Z éHEõÀô-Nb°ÇZðûÁܘ$1vúQùSš‚ÆUÌ õŠÉÇ1ª(Š‹Â-†°¿§Ø1Ç ˜ xGªzÇ"]øº¯òÍiÊÑhAÜË<@Ä%@°«,ü%¢¼œ9ÄNÖ‹!þuEOï óËëHà4LŠFÜNdpâ×Té™ñ“ž<ª[G½½ú‹,ÑËŒFÑrb›ðž?תºix„ Eo"À_0¢èo„%*¸a½Ð·øi°û˜…EüqAMºG|æŸû· þ•‚rÒë+Ý[ÖÍ¥(ó>/ZñÃhk×}½úÛ0ÿgàh3À·¨+ví8‡:µÊw4†•zu/ðí—¨~¤{Ëgxà¬/Tò +Þ"%E´SxÐtì_#RzXÉô ‡+oôk”@g…3 Ì]¬üŠî)æPᮤX¢Ró\ÈWÎ3,'¯DÝ+QX‘üݹŸ'†žÓ-™=¿[ƒ¿aÂLÕgäØšhP Kÿ°â"‡9¯úGµ".HôM‰‹šA¿+‚S--`OÂ",V5.QIãçö¾Qî3Ž^9£ÕÂúêð%Ž=u¨êØtûâ~YD=ç—(dÈAÚÐU[¡Q=C ±;gòš¦À WzNξÇÛ(óZ÷ZÏ£SxÝl‹W–•…ÜMõù[úb÷sÔ”4”zV´K©g…3z¿-u4¾eà Ü7¾HTë'&çŠu4º’uüAjŠŒPöNö®#µf„´¹>ûUÑ”Ú#Cñ§_8Fj¿£öl¦Ç¢ºPJ"·³r SfÃó'áÙ>Yy±¯¡¡ß°âP‹€!höy€WÁžÉ¢I NjzÅFy¾Ë‹ ^^ô%W*ú©O¦lŒƒy_é¹EÝPÅBE5“êDÉtbn©éP¢x;“½ÆíiÆù÷½÷¯Á¡x 4„!gÝ`߀PèÿœºQŠ‚™¨†0¾ÇVö‘%÷¹w”­Ãà .^Ú!äB¯÷ýFmçžÙ’ÂmÅì÷_#>çŠ&5_‰Ê£2ä +àÔÌVæü»gíÍïÐëÆçoÚ+Ðn‰/XL$6 ßG ùÍëݯo½ Ù¯AO±€½1XUOÖ Þÿ ħÞokuôÑóº~9ûßYG»¿ù+ +œÃ.?[„ÇÇÅ2·oïÖ…i\ÇO]ïà© =@÷/Äÿ;Ÿ²Q`ÁÇçeˆµ"êáÕ^Ø1¼—žg©zy+WCû{E&LR“ê[ÿ\HR("Î {‚èlÆÓ4ÞÚJ—ìÔÒZöã¿2õñPˆXh9ñ¿‚é2Ât¡Ë×0¿kh! JvD¡ø‹ “Ò~tt±Æiò¾D<î&¹¾…üo¢˜ö~šB”RË°{ÁåB&Ù;ïm‚‰K总ñÐ&À+ýKD†™"ÙûGÔNÞõÍyL©]ÃöȾ…¦IÅçÀw£÷=6áZŠÛ`QxàÕ€Òßi¹ýš½Û{èA½ ZTWÜcÉ4 +J)gÆoÒ²è€ævf¾§ÐR½™$œ¹¶*ËGÍp©H£J÷ åÉ‘8pk$Wµí"mM"ðù8è‚ñœ‰¹vÀ ï>ôçaÎ`wT©N_ +QhzžÀk˜=wNb² D<å°“öyçV0a ÛóíVl‰Ñ৺‘UlO DíISl¾ÙÎ7 +D±d5oðD ˆË)úFSf²$œ”¾Dx¥%r›2HoÎÓÀ”vYû—ïmhëª\˜§\ÉSʺ˜SÍS.sû•¥1Šyœ§ÇÕ|EMò$üV!y¨öš31%›3äL>÷z“ð0‹ÛÁ¿,ò†ëuŽI•_Q0S5F5yÌTæ‡çRŸ@IP3üCö±—ñä£Xîä±s+'½yþÑÑm?ž]k¬ÀPâí1ÀûYâíQÏÔc{g°ýn‰Ø{²Bœgš¬gš¼s~¦ "J9-:¦[#QT-â¼ÐWhÇži»O§mœ¸×eÄ(앆YÐÆ].„g-lÁ”y]—žƒBDl¯«ŸgïM— >2¦3Äv€Ð¥,BxØ“8ÀyõÖ–Sx‘pnF¥Yö¨J "ŠÝÄþV&Ü™%;÷‹ûÏ•e¼ +CÔ}\ìÔªCõrÓ*ÐùÔèô쬉թ)®Í¯€òÃ2øœ“}§¿Ô<ó‰ žÊÞc9cºVíñïáQ~^@MPDµHƒÑž"±§0ν݈s‘ihþF„#N‘ÅÜ/°ÒEéÌ ÑÈó6”¥ûõ’üÚJÉXåÂáª4vž¶¬ËñÁhò³øò ÿ¥/ä÷óAí¬Èehß0íáªuxá¶BÒ'ª]ì$(» ßéõ"!—çqp^àP”lP®DŒ’Úî@_hšS”Ð %aoŠ¤¿âkE"à=&@>*Ú£ýkÚåÅûJC ó¯;W{¹T_ŽÝJ©ùˆí©Að¹Øµ1Œ©“A&A¯Ù¢’°E-ç”ø¹R@ðнßÑu¦´Û@PéªÏa´Íïs Ÿ4¬yvÁÓ€õŸ¦Oân åÅj”] +g0ÿK¹ìß÷Áp' +Maˆ¨Ö$*Ò¹S‘dì·ûÑÁªJàÒù˜ì3âLHÏPæ~#$ÉÜÛðly´ß/´ôÇwÇnM3£7Uûõ EJýD<¸a(m<éà>*> +^ 7Šh™ÐÏ×XýKDŠ(4ß•ÉHѾ9P7k<7 +×Ô:z”´©Î¿ú±Žª7yЧ8ü5 +C.´‡^Õ>Šð}eÔý©\<¸vWjI7›ÛM~Å•çB«®Sà¿Å â†kTüó3hnì'€nR ¡i ´_¿Œ”*8bÀÛ{ÔXõ¼ž½/=jŽM˜äž?ØVºX=²K ƒE¤Û-E7™á$6º®zÉ/nø™“‘²ûD—Î_êî—–;Ðc¢(Y Ÿ™×ódE®L_Ù”Ô+›¡´Œ¦§“žÊt-Øä·V;ˆû“/¤ˆÎ»S‘'¢.l¨‰¢ÅIT·î0è6.ÍgôR2Â9n|%Р×ùwç—–<ÏçüH£ÊõËî]ŒsÀ;Œ`Ù\¦{·ªæ_ª ¹69ž%Îjåª ò…6ëûKàÎR Õd+ b­¬­&’l–øˆ×­³©«Â¤‰…0rYÉOþ|&ônBv¦ø¨ŸÕ•Ô ¥ÙéØezÖ’2ušX¬÷ l`„”áS¸Ú +dü˯#!é–²2O¢³ÑdŽÜk¼voVu“H¾œ”¨ÂVt°™[¹iM ŒÕóX¬e áa«5W‚ÒÖÝ{ïïcðˆkêG/š‡àBTÒ+M+.̺çÈùÉ<.!Wî“š–÷Ø* Ú9Ÿd† +^aÈ[O½¡«}zqhr›5÷‹jë<]ŒÑþ.?ê+RéÔ¬/&%8HXž0‹cÝ3 ;ûqÜ¡yÜÔU÷ŸÓdK†¦<ùÎ2`hxû1‚5eüštÀI,K{¡¤{X¡Úá"³ïú×Ëpf7¸':¶³ˆ¼e3ïªîu À ².ýež¬°‹#Øw?A̘  T‹¥?/te1’js³¼r›}¿"ÿY—«ž;(÷:?ý%« x¶Ÿ0Ó€@èzò)%‘¾.Jœ+rÿÍ„Dk á9ì£h^ÄòëAüŒýNpn¹yû‰PØOäΦœ$¯†ÚÓ endstream endobj 24 0 obj <>stream +Î;•~³i½ö%Â+a®v£¤ùòøî<—ô šw£Ü½G¬±Eµ2LG jXM£ÕÍÝv?¬ðSsÎæ<û$¨ó£žï‚Â*Á‚ÒYr:Žn É”&]¡.ÚyŒ>ñþIh­ýXÖ%õôK„?©²ÁÇ'%³}sЮyûKÙÖ½®ìDN+È¿û› s°î„ÞÁõ'1£ysè=©6i…¸D³(*mYz@V”I-5`¸¶sæo¼Ð}ïýìöê¹îËi`,5vŠ5R»4d$\9þï"4Á8¾d5œ'÷%Šþx„Õ? +ô¸yQ¿“.²(Ê7¤{xy1.A¿D¼ƒ­ƒ­ázóÍy¾`ù rõÎbʽʅRâ^W£!µ#ð§¼m0 ¢Ù{oªÛçJÏ™‡5ôb”€Šù;ÉyüÀ)†¸%ˆ2Fÿd ·‘ŒÒ¦úŽùÈ×v{…9¢Nc¼eÎ_£àJùU )Њ3‰ÃK 0G•!‹…‹Ð5>ÁžNË\¿Fä‰u…'ÖÐó½}sžJA IÇ®—ÓÉÊúÉÊxbõÉîuìuÜ&IæušàûÊÁs¶tÏÏ>¹OÿŒÄŽÓyõñ°Ÿ®îÒw@ÜL9…5ÖÜÏzóHX%ª<(%r!¤±Ýó FÄ5m¡Èþ表ÿÈ9–0¤Œ˜ýý%H 0a‚7\á/ûRÊEß Ž8Àá ”åDóíä¿LJޚ"xð¸.”¾DdM[v€Hlö·õíyžóEí+¦C£ºˆŽ›l ¾‰X‡ /V{}rÙ?B*JA:@Ìkš£Ì¶èjÚÙW=ËoÂÊu_å‚Û]p +­Ý7lo"Fê©Ú¾«WOÓyU;¢‘ +W}Ñs ‘Œ£Œ­ŠÄõ9oV‡[·q»{§×U‡÷¼d“\YáD< Ô7ÞNŸÞ›Í'¹Ú-Õ6zóuJ§×äî„›,yý [wÔE¢óƒ}”åZa©ÞÁ.È`ã›”«}#„¹^ ‚_£À“y&9“Õôƒí<Ø ; ·Û#¬û‡ ò\>>Ãwgk ßÅòÏ_ÙÁb3I$aü礘렊ÄÁ-à‚bÎ‹Ø…Ö Ûà׺œG¾¾e:⬲h ŒÚ§¹l–è§Øºÿ/Â3÷HÔ¯x[jBæìJ¢üDynôÞïýèÈŒ¢ò-;þEGõKÄw ä—(ê›R[Y/€h:S{²†©¥eJ¶´HÙyhlUAÅŒ«Î™Ó1Þˆ–+°FØt¸¯cC*ž¡Bðƒ:)íÔI…’Á‹$+æ~ÌX"X Qº¥¹FeŸˆ aPûkˆusu69ˆ—Ìœ§ÙÎVc™óÀmð‰Ì+Sr¹ ­p%`ßàuÐÙx>gãi.¨}Ð…¯XCxÚ³‚¦Ú…–}ºž×i³á4ì¥w5×cïîíHзí9ˆ¤Û>xÛÃþ‹®Ú³.¨ ‰๧/F8IPü·Ž<Ýlh +ƒü\À¢2‰kà†8#Q“U:¼Úg4µ{·å†?(€a—”-Ï4Ý°ˆÞ’áZpÚGÂI3EsKmŸWÊ>(:öÐ{¿^Ö¨ÎÍ@|ÕÍNTm·óœ1Œ„ÀÒê;¼.åÓ(QW*d|¾èµ*‹Å"@H<ˆ ®Žç¼ÈÐ~Gîóëñ.ub¡vy¿pÂ_ƒô»­lÓöîö鎠à9 îÀ#jPˆ]зkè呵:ýPD@#Bâ‚6}ü›ø¬ÓÿÃá{8’+U(›ö¯l1ºtb\(Ò¿®O–¨õ¨á‘ž¨ØMx±§’F”mÄÕ|÷ôVÙ ñISƒ/ˆ€¢žˆžî+È#pØ°5ÒÀ®q%Q¨¬‡çs¥nK–·ßʹŸâŸ»ÅÁm\§7k¡ýÉ0t4ðtç- {QÙ»Û°Á)o×T’€E`ˆª‘«ã%=ž%41-·=P4ÔñÞ‡ “½v¥4i€óçÒ=ø\§!ð÷¬·Yû8`Ei ü ‰ ”NIJNÔ¯`cv„ü|ï€imßAy7›FU ¤vTózŸ±SµÝÉ•fñ<÷Ž{ËeòÞ¨írË2žÏ?ÃÞisáóë{lç–ÂœAdVÑbÔ‹Ú,ð‚ýÝØ."Pìó°­SM»ß_#òÂÈEöz´ØŽöÝy"+ç+E$|€H˜µ×ldú¾‰@z‹K/ÐÇÏÏ¡ñ'QUKÙËÏÿ¦ÑG’û¥ËÒã«kf%Wjþ |7Põc­ ÀŸO}?¢P7 ˆ /œyÖÓˆ RÒ¾'@@æ @—¾tÁZ'"ïÎtÞnïxóœsæ»a Ó&˜wñ~óûà¡B–- àéîVùL°2–Sù³>²€ʤ +›¯§:€MÖ‡ba ûÔ³Ìc8y dV†i:Rlë×€L ¿¤m_Oƒô›øC*û>;¶o–“zC-øk„ƒ Að"¼¾D±Q4gÈ~ôsÃóÜði!"îC Ñ>ýžwúH»nù&â g f"ºAæñå<`£„c£lA„†æ*-1ÄÙ™C›ã`^°)Ô‰‹ú÷ƒ]c¥»vO÷´¤C=}žÒ8¨zJã³Ñî€ÍÔyQ_¢oé@uT˜ŠúbIèá¾/¶Â† %äµbã|ìí‰Z~}(ëiÔåž`§"$']•h»¾MƇ¦r¼HR6ºD`Š³à–õLlÎh "rÿy¥,–H]¢’ɽI…Héy8-ƒÆûQî í»’÷ÔÜ‹ z +ìqùº¸~Ñžw¹Èöóæ µ´‡-ï³–Ù‚Yq¾vO[Ì¿FIï¨?Ãíøñ‡“4Ý‹ ÃÞ ÊZ_΃ž3…ŧhdb™–Š4ÐÖ!=ìKÂîíƒNø“c…á¢f*lKAErv´G øîMû)ÊžÝIÿ÷5â3©@[šé9J²u”z¨¹O©Ê +ùÍ,5]*ÐþFÊ:»åq÷·ëÞàwËhãô¸€}-L‡ÜÝ@µ–vœ]AŒbDwÌs!³Dÿ(#a°Kòº#h}.”iû4á(òRA$ê*熙oˆ@Ùß*ðE¸§\7íÇç|‚<Ýï­ñ|Vý]†ho +h”ˆb‚Ú®Ÿ woÒëÕòJršùP~`<ç©ÀÐy7j_¢Pö¢‘ŽÏE›Öjv°¥ixPe°¸¹È.ƒ~`}†Lì—€ »–Ifïæ‹á_ÏÃôa«‚ +G ›©2 +`è÷µùR Ò»a.í ôö„…¡ –°€ãž-W0ß{N®¿ÂÊ-Ý«Hd匄"}eæÎTˆûí^Ï–5£ð¥2´ã¨„°!pcÊÛÝ“ „ðOwû Ts£ ±‡IýUÕàãv³Ó¦—KOÍ„¿‹@”M0î´w®ô%êÑ<d­-b0XâûŸè”°bdÓé*Ó5xì÷~&¢|ðu{<¬!ñ1Ö9°õ@DÂ?Ï`@¢ó0£‚ îÔ!òÐN™€ÁÐÃ$ +ÇkÏ3ð›óŒCþ“FS¼?0VkŽHìPA9DÏnjö+ŠÞ%6:t†9ô¯QwnŽu”C²Kg˜Å¡{×K‚ÔÞóE?3=¸=÷ìùA3k8ÙîÅi¼¨/QX•‘« Ufš„‘¬»ÕV•üJ´S-zaTÐHÁõô»ˆw«#:)…ÝoγtÒ€µÇKÅÉbò@ˆékÄ—å9Éí¯QhíHrÀø©&Il·iu8q•Ã"Øùï,©¤dëpU`¨N¸Oyi 5hÊv8†1é‚Rc/» c …H5óR3y x,Í;]ßK‹u´(Άô6mhøçj^\ûÀU“î)Šcj'¹ ”ÈTD6Ž‚@[àáî•Bv?-î*{¼üÅOä´ î"Yn1IK@8OJú´&ÚobïþôŸÓGi)ièªÂwñëß_KR.˜FBO]ÛÛ¯@_èÉ¡äÐÞÞÖ—(Rƒ…ëEKšÜ}'ݺîŠ[“5Ø)ò"´B“hìýóÏSÖ.}'Õ{õ£˜Ä3 ½MjÇvÙ€¯$é{#„Ú"ÄH¤÷6zÒœê8ú yÒW ¯â>³ð3YE«KC§«ñûAÆ°·cóÿþ¬ý`o'{.ma÷ ¢ê~KÖÚ)¡}æ²Bµî”Ò Œ° ¡µài’?\ÀòO§“s–†œJ}ÄC5žëÀö?æ_Å“áßì¿– V%FbïR/&ChmŽ(ÙƒI©ç¹ÛUê Ù%’ÌîJð€a³3XvGÖ\Xc¾ œQœ†ƒ§'2j;S¶³¼¼ Å©çDLÿüBLJA{d¹A3ZìÞ´Jnve{”ñ4¯8§.M¯ ¬ÑìD©Ö¾Ÿ†/”©E5Ý€ö§°E¸d&ïvF¹œé¡lýmcØýÙèzuŠ:°‰e‰BlÈÈfMܹh^ût•!”¡bÊùzÈŒ€ j¤8ìôÞ¸"]gNˆFCºÕÈFI[Vû!Ì¡/C§>’Ùïšp(R¨aãüÎï„O̾Wi•C}Žw¬ éùJo¿Ó•BÁ½ÏÂGó’÷ˆ/ßè]ò'C·áý ñym;—X'Ÿ…òàWž9w^YÔ¨U€³êôhBŒ½s¸4®:o ÿriŸU‚Žñósà Z[?›?Φ‚¡Óãü‰I·R;ÞúSÔ™L %v ãh ï<\duÏÔ^[èA,}(ºúmˉµ¥”Ùõžâ+ëEue?tZ]™h3u O*C’¨Aí ,æa*øÏs…!­ˆÉô†º:²Ó—Ó“E_•þq"j×3|ãOºdC´PÉuüûóõŒ¸Ÿ¤%Ž1çŸvfô}7̈8ZË`AgwoÀ“Ðï°A"¾¢„ëTåÄþÒÞÃ? Zù„%̓!gãÿ…ѹ¤=œ_âög ÁÙ2%&ŸÜyË|s¿›í(†é›sÑAÔ ïç)9°Ë¼÷f#%7AÙsçõJA³){ðä0˜q '±º_éî$Eí$EƤ<Õäë&­bi%­²gÂYTJ@ê +­öÏ'po.ø­âˆæ“¿ÊW;ÖbÆ‚-Ê!Ø×_úIÁ$ì;-Ø>ÎædjonÃè´»(eCìzŽíÐ(HÇ;–ƒ­Ø.Ý9He I@1û”$˜¿Qå+QÁ4þ_#M(á…²dÒd§Ø¼¾ÖÙF¹T¼Ëì>½Hä~s–À"/+C­”,ä´ä:«f[€Iò\[¯uÌ@už•“RС<É]V7C-P§SÑæó¹ã>ª%%ùš„ÿG=è`ÛM÷÷ÑÜ#íkÙ«Ñ2ï>gåÛÈÄV;©´dp×Õ’IâÄÐ6¾ävÏW‚VP³íèܼÓQÛ ¨[/­ª°FÏæÕÅSÕšè­ MGD(4¶Ë_=š\@ËN=¼GGL²Uªw¿£kåÙÙyr}vhN·´~øÃÙþvkÅ{~eø ôGü,zik45ì4bZë¾aï“þÙ/ã÷¿áÇ(ÄèqøîQlË~/å`ç—NdÓï°êƒ1„*UˈbLˆ ddMKHÛÇlê {9w =TÃp÷HSê©MéWµÔ(kHOìšrÜ"`˜98@ÎòB S"®r}¥„·b¥'þ† 6ÿ𫃘™5,Ë¢¢8•*Xl©€O ¢ +Øì‚=9¤4o>!õCê%ж »Úi÷lh"&…räm™Ùǃ”[N2"á´«í™ ß ßZ·5OòV +‹ï#Û_äþš‰b +I5>lßÁÝ»hH/5 m±çÇ©ÿ©õðH¬aÇj!•ùóB˜Ä]";s!¤5‰*#äÚs—I‚žö¹TùCÑñ¸@·WeîîÞ~ÞÒ{Î*î  sÛ:µü¦È7»,OcSsŸ¦Any‚)6I;õúô®îq*7dá—h9~ +U Ž/µg²÷éO1‡ÝO”õÿýd§†¥oÃýÊ×Ñ +¿+¿0š-áú6¤Biç¿œG †™PB׶յb÷'<_@~5ùÈ+¶fh°„z ÎyÚ¾YÄ0ˆ‡I uCðyí•õßß6WT®®£rU£õIþ˜AÀ:Œ:€J,ÛÎ8yD±±X"  伇;ѽ}æÀÌcîÁ?!x/ ä˜F¥"Þõ:”$%>¹ù[îkP8(ú¢ÉÄ Ë<5ùEMö¤RÈ6t”Ú­±öƒh€ïW`ê ÀèïêW]˜‰48R$¥¨VÒu ŸÇ?W †WŽ¨Æý4 +*‰¢»K””UO”³lH‚À&_‡†­ž`ÄF¨ÿ(ÕÜ´ËÊx|@ u£CÊÍ~à1×H³å9JÈêa-vvqîˆ{ì1{‡æxŸª?Ý+µÕV=Ü®‚? ÍCÚW^f]QC“8‹3«ò®’‡¦Mš +ÂB¼˜ +vªQáÓ¦vAžº[ÐO!»c¸îA¶|aª: ùø<ö6"¶ŽL:e$sÉì¯HÇ爨ѩG¨BOý\IÄ»>‘c¾âÆ/W%=2(²æþ´ûü´Îë^\hÜ-¹Ð•›}ÞÍ;Ù@oÃåΠl‘Ÿ†ÚA¡OuwŽ+ŠLLP]ÒÔ~uIh=óÕë.Vœ5÷~Ž48¼UŠUá0Eî»é=žvòŸÐå@25×›0k@¹×ÐöÑ i²*Ò‚«(ɾ‘d­e(¶XCoP{hI‡o÷¾4jfRsµó©ÎÜÁᨢLžÙßµ(™wç6ªK«µÍ“âï¼JuG„YÐÁ ÙÍKÁÎûÑp‡u´Ý'[AXêT“Õ®“Õ*ªvŸ¾Hňº{šì6#Ê“ˆèCáwúœÊê]µ>ŽSË£‚²“‚ç®Ã粯3®£gYF2°vp¤¢µ-<~ÔžNŽ†6 £{–#UBÁëMcáÛ+ N†áî¨ + ªšù)vWYËf¦9 +P”,RìA‹!cwë¸ 3Üã\ [—bÕ«+„’´J_¨ÃÀbŽ7X‡¨MâÐ#±¥F )¹"²ÅrreÇÿû¿‰½´½*¬Ó+~í Yrsu‰Ï¢Hä+µÜ8#©õ}RëÃÅï4,ÔëÝøMEۨд(Fµpš®ƒ =€N¨€Õ1»!Fè Z‡É¬|Cq©V†N%—ã˜ÔP.æÌiDØÓ—ˆ¬B‹°³šòT8xý‚¥á,=”a¡LVNÕ‘Š}{ɾ$[5á¨Æ¹î‚þ'ƒù^fꙶáP]’èce®§*’ MáëÔZ¨Y€%|0n¢`Ú_!x +,#”Æ +iù~ =ˆ1èÀx.ôw0IÓ]ߤˆŠè¤"Ãw <’\-öQÇCCMw´ˆ®TKòS÷«¨‡ôÓw£¹wd¿‹bù#ŠJq"F"ÚõF@æ âs%É*û· ]%ˆ¥óUûy%ò/Y#²ÍH 8vnˆà㪑·Ž¢7;©~ +=¡‰0Á&`äfñB{¯3ªE¦X“]9K‘ùŒ&_ v-»£ ÚÔøÑa·héRI=HÓ/þýl—bÀ% n%ÉOÙ}ì;ì¿Å•„î‹•e j06žnOQ*Yh’|(K·|ÝÊF…ž¡¯äãiT +ÀätÚbh‘Kèϱ؃D"@çç¼æx þTÍ_âåµ¥J© ]MáòÇì¬QRŒ ?ÙSPçôö0 »±{°fÍj;c÷mpœºøz1E/á´U!È¿ÁÙh/·âè_Öo,,|%,Ðà^×­ÍY´äKèm™XéxðëlæPME3–»VÆEª·Zžâ1ŸXaÔ +m–eœ¤œ4$ØU:¼ñnë¬Iž¬žžYuF°¨Ðíêí{OÒÝ<Â_¦ìJóWÃbáŽh¯¸ÑNí—ÁG¦pU]RÒGYÏPüàJIñ{…$K„ü8é¸?&ú¦®E(Lð©B T™]îȸ_QërÜïã½ÔÃmÍš]på^Ys›åfŸÛþúüg…ûùSbv€ŠìÎE;APámÁw¯­dmUäì¡+Q»¾:ïŠ]À&A÷ w,òd¿ + ½4»ZèbŸÌ“(uiˆÒ‹]š*|Ûóˆ ›hß"æjÊF2@_ø5õ‰ýg=hòAÆÿ^éŠR‰%è«ðö2ršx¢g3ʽGþ¢%€ò4YŽÓïù¯°á&«¯œü=wö¸è,—žýé«=úé2ßÚ¸Z“ÝÔðTKŒ3‘p¢ÏPMY†É>NÔIkPñJ9¢c'‘ĵ—4Íuæªv—°Ï†E]ËY÷[´5ɜע'E0E~¾ƒ\)”QòG9â¤aô˜à­øm¡|‚BE«"#ÀÈùEOP`Sý©%¥1u„õk¦ÈÈRbI .PZ2{^JŽš}€tlÁc-%Î…Ø9Qåt¶ó}²™`.Yý0&"~Ø¡GjÀzGÊßL£bO5ÇNÆ1KعKϧ}Ë-èûΙ3æÁÁ/df”àw/u~z¸°YÛ#ò²šô”u§z&!mçÍb¸8"M´Œ™P…Û/mnÙ{ÿþü‘Øãm'HŸ ¹¨ì ýí®Ãa‡›Â9 ·:ñŒ)‡?žd,â%DDrôQ=96½ö9Êòd€8¿® ˆ´)ÑéZɳŽw@1¤”Ì­1°Ñ:g(äãÊ⽬"èÄÇ^B‰–Nx®täyÉ©ÙžŠì! +ÌdÁK»ÏR¡ ôã~&]wM¿£Kiv£‰pV¬À SÍ™*™søüSQñ§Ê4gZUâæ¡Pŧ~ÿ±Ÿù] “ÁÓbpYãà«)¡ÄG .¦tä€ZäW,YGT'NòÝYCåq>¢Õ›&&Ÿ¬J¯ŠÐ®Pع±‹ò¼ÚÑ*SŸ]G’x¨ãᎋ§^`´Vê¯Ñ¾Y +ïý\Ü/øÛÌS^uEØ Ô-t·AyKdu +7¶0›¨rài¢B.oi²w£réνdB«Q#9{Þ9|…CwNn³ äæªâ×ËïÞH5])@7k®iÊîŠ~â3B©P¥Ê h º2`wpÂÑe8w”4á’ÃúÓ1› )TÍãyØ¢*ά¿ÄPô3%²ãèª8TËÚ…K,XN5.[N€n¯¢¥pFËh{l²ý¢H[o-­áAªÎôú uµÌ@m+&$8„_Ñù—;ÇÑœ„ž}ÐÝÔW”{J̺¾§Yw5ÁX÷n¡dŸÎ`WÈHXs›Y†Áî»C‚/&JvØ‹¼¿xýˆœ¥s´NçˆÔºŽ”Æ'§ÉƲ£؃²¡@ÚRfšÊ¹E>k,è{S@uÆzÔ„F;ú&ª0¾”:L#J LŠV—挾âhŠX­¬³€áNYýE (¨{…e-\;eô@ÑÂV¹ÑuD1 uÛŠ©çÙÆØû+k‰(îk;b¬?ÙÊ)Sq%Ë®ˆ»[뚣 »à¹9P òŸ^é #aHᦹw©ó´ÇNé=8úáªUñ4a_1+Ü‘ýtƒÕÛ­*Ÿ<÷HÁêv¥I M-It©<Ï 6Y ëlÐjÄT©<âaBS¾*Å|®äO¢¯y¤ü)6ROÚþ£ßÇ=ËNóuì/p=Ö ”‹u2ÆóûparìHñ Øæjó Ù~TÁ7ÄÈBEŒ“–W%ËÎ ò£Õ‘Ÿý ¤z¢ãSÕÉj/Ût¿ôÒ‡ 9•0íà؆Ec +i‘¥ÒB/)VÖ“zÉÄeîóVfôc£¶°P˜Ä†æÛu1‚jãäsKÝ£»`þ| j|Ûã†t=í÷ÐÏ^ð¼èµ†L_‘l9ý½ ‡Zï¿×€LQÈ—pG-Û%fFeñ=¿íþ÷$©:¨ +ö˜žÇ åžïî{ÅÒùfGLu"Z}ï`g·íD¸%™x•Ï•„É!RϨ£¤Rj€ç)…:;ÿý ú X‰·f}ÿ£rOñ¬$uv ™Të¬fŒ*“1íÔ(/ˆîã£p´!TÓ/•yÕ©äÁ½Ä$æ—_J‰SªÉÆ lFIÐeA¯Av1}¡! +{MésºvÜ!v*:PÕ”œ­Ñ5$‹ÍHG`v7¬YÑWd·°W6Øü¢¬„p’µ½"e…Ñ呤HÐZÏ2$ÿ”‘µ‡Áßý›páîœOþÂ’“_db#•–¿VùdL¢;„9©|?ÂáÉZ¯¶½äÆÝ!úÑ*~¡?v>›¬MÅ‘}ß¾µ„{§¼ëy+¸OµdŒQ|G¥wå €R6€êõ §} +‚T7×?Vø¤”n™Ð \iùËAP{(ë1}«2©²©Nw¼WJ%d”Ö'÷âÕ‘‰^vpM±Æ'Þ$=Õ/dˆ`†<öiÉöÇ Êß~¥ÖGV3"ò`÷nËî °NÚ{L”½§‚;y Œ-=8”߸ŽüÆQÔ‰‰U?!ôPBü?œ-­´D›šôÍÖ8̉XKx9¶$€ÝØ\VÖCÝ÷k'³æ0P³ âíÙÌ´˜šì/¿ (`+ûhûI[+Â\Má¾Náß.´­Óânc×?8• ÈÉ1î7´0^y–@±ýûStpøVÔÓ¦Êv×Yö„ïñèéw0&{êë"ÿ¨è<ê~ £ç”ã¼'åßÝúÑóعÐ_ô6jýrz°P„m‘zž¾N›lÏ/Lù¿Óû[‚ÿÝ¿b]ÿWŸè‹}þWüßwVðÒúîG†ƒÜº‹ ]µZV°úœ’F±ÜúKŒœ=çãqLÏH"fØ~YëÁÙg Ãþ%\é›UÌoÀ}1o¡ÌWV€âiÉÀtè±?|xÛñÛYÍiÈKã þøaï¨juùVùÒ(>`¢njˆ^½#¸ƒÀw†¨…xAwÆsÔœCY9*ååàs?ôvT±OJqÜIë'X}aY,ä£>ÒRT$Û`„„…A½(dâD\ï¢[ŽÍãˆhˆ‹s•èžûOK (³?™;`Ç’…cOFÄÇMýWìNóm²¸‘Mãhb,"5ã¥^uŒÊ‰9QáRí«WÊ +‘àeª*8KûNMiäbØ–+QüE÷õUób*#tnpc™€ËëdÒzgSã:šƒW(¶Ýµn‚‘èšÄ$ˆÀÑ•ñK®ßON`TaèÊß+L?ëqKé™ ˆ0‡Eª ê$¼¾,uÖ13Jµ ˆ#ÈOÙg•©N„ˆ<åJV\„Ït÷Û¹!”íÌ6Ëæwí¥ç··}AÁ N1óÚ^ÞèÒ#çáÊD±*@LqYtîa{‚É êׯttBÚÍåùæ4Ö?É»á’bK¯z¹®¿LÙ`E†ïe~ ê®õ´A2í³´thúýÚ+Â+د\UtxU÷‘¸?<-¨`ÐD¦2“ÂûWÚé šÒ^*2­Íò-#}—Ê~LyEü¼ŸéoÒ-Ó ŠD À8D+Î~&³@µˆvìæÌ6­+4ˆ Œ.z§‘onªfežÛè´ñ^‡h´w4{´\kt s/ý.ž§€ß!Âú-XïÏÈÔ·™×¢ôƒ÷DÒn!&ÏŽA£ð4§¹?¸Á\C©ÿz 0Š|¦÷—ü…¬íÄhìqÍÎE49©/^å5ÈÎÀȨ}ʲ’õ32¢žˆ êðÊžoZÐ"cw;tÛHú÷é¿9þÍXýõ€ŽUãlÅ~%0MDW‰qð3(îúïEªc‡p$Ãî·Wã„ì¾£f±‚±ƒ×|eCþ—RMÇ£®¦j¹ µàu@FIþ%Ao[4mŠ¨ÎyXµ‰èWd´h¯q@¤O×mîI±÷ý˜´Sº=ïžc¡UD ³ª aéµñ¨d%“×ÛƒW3 ÐË‘²´'#ì,þëègàž«ŸãUÒÁz}ÈG%G†q us§•BkÁaü@…ÁZJ\”Ÿ¯)†¨ÿõÏÙZ²êtÂÝL9œzÎëh ;qp#ÈcÚ«Ï¢×»„@u]Ÿ µnx¥÷?yY'“ÒóJ +e¸¿”ýÙ´³J”yþ:Ž½È+ðÌ: +µ‚1[“Z¿´µ~X¥l7²[i¢©Ž„=Ϥe“œËªÛÉyš%Ån6”pdq™ÛÌu—ÈPâ(DX/T|\†>)Ã’¢@ú#§Z/Ò÷­W +€èaïl„äJ× xíåÀnPÒqŒé‡2¥6¥j˜Ì TS @ +؉„‰ÀÔhGÌvÖÑp™îÃeÒfÙ_µ²Ó“t•ñG…4°ß·¹¿— ²b6>)àD}Ò*îïÏÇ°ÕÒ:ßBq£ú­!bpEÖ<þZ§ w­1k@{E‰zÉêÛ÷Ó„s>¿eSgɪ}xî¿àÿ)ú£D]5õ^W_N“•éy}ÒF„gk¨KòEÖ±täBëÈ‘?µÅƒò±!®è6åAJä °<@»yÿ¹¢D‚UUf×Þra¤ØƯ$þ1%eç+9xIOsÚ­;SÕãýQÃôƒ ÆòüøËŸÌïÃÏT +ŠZ¸·þtâr¢0y£×ófsê ·Õ*£À ÅÆݦÞågsƒº>©ŸTm‚£¼¼RwM»^ˆÓÝ- ¾ ¢ß:bÎã ˜„™Á£a#Tû9À²}·ÔÇQèáÜK„e¤ù?ýÕra|&OXñEñN†Ô°$V/A$f§ö ˆ +S‰B¥×$­íKD6QOº¤#TÖ¯QÊO©äÁ‹ž©?E¤ZD”r"³Q)Ez¦[Ò¢vû"DˆBÞbð)÷ì™cä1£Å͹b©{"\æ‡-ÍeD¤|øòg‰T=%¢ý±_š›õ*ë<‚:ÔvÔ¹r·\(‰”Á”EÄôÊÒ¾ã[³f.\êC¯|É™‰6™ÊW÷«H„ ï8Š:Ju‘Ë«n›·7ªÕ×I²Aù­ÁÛH%¥ýuý\X-ìÉZ”g‚Üj ^‡J&jà úëo͘–ÙÜ׎Cà—póM½cJ‰§ñkÔ µOùK¸\:F‚ +ßR=S¨Ï„ݱŽRR:1¬5ókÄN£$¢5{¯4Å—ó¬æà—–ÜK4ÍFøÙE†•¿Æ~«ç_¢à|Û{د5°úv­t™)èä1€Ô½Î^“a¥Ï¶#ÚQ˜Õdib¨ +Û (ƒUH1~‚º}í™ù¹R$¸¨JÏCïFuŸ+Åž‘"òŒ@*íz~l°rˆº+g´Bô‚mÞצ»èøÌw’|žÿ7v±ª=QØLOqçä§úª¤1ÏþÑ.î׈CR]Ö?}_¢®*(’¿¤[/ßIhY“!þFŸ˜^*ÒÿË¿gf{-µ÷û”H÷å¯ÁCXÐÂøŒ¹yBÀx¾90‡%€¹­À¯Ql®xÒìT ›™^˜=Õ ’÷¸‚Æôìˆ& Aý1¿§Y´ûÑ\áþçUG'\ ³¥Ž(ÿ:oI%íbî7/é~b?ýæNÏ!_¡!_>8Ø+€ÆŠ}¹z4¤•³¬~³)RÎ,1ŽÈšKéÀÔre…–}fž)²Xú=RmíP ÐI‚ÂŽèîc &âõI¤4Zª_㎕€_/áW0îã¬÷†»Gz] +&uÆß!¢õÅÎAÎ_b$÷1.µòç¨$xq!0ÞìVéÉýØ`6ä/tê¨3NíÿgííZ­i®óÜ_°ÿÃsè쥫º>º‚Á!!'IŽ„‘0D–qCþý®ëº«çzŸ9—¬m â}VÙß]5jŒûc#—­Ú^øæð*v?·Âb§9/ë9¡ÌÈ€ÒÁ¹7üb *ñÏ¥DÉuK¸Ô¢O;‡÷ doM›¸èmÎ>¥^X5Àã˜húH»ÈÊÞþ¸güqÛÜÂ-6¤+ãô§¤UD<¥èìûJ»eÛê;|™Ì¶c׋O§ºŠ}aœ‹Ú€0™ž|X‹Ö+,Œhç«Ù¸¯‘¬¹`¸Z²Á{(¯ékLYýÁ¡ 5ï|›ž#ýR‘ëÝ cÎ/êYhLåÖ`ÓrC ÝP>.£bbkôÎð›,.ºÚ;¬YÖj® „S?j= êƒÙý)º¥OÃË^Lb"ˆ!Ò–ÙláØÙBO+ŽéHÕ®:2ˆóž)îtj•ETQ†FÂäeœÒ´ô¦jH m¯t¦[¦Xßnú•ð¤íqQí¢ƒ±"Î hÔ],¯ÏùdÐ\Y#D† »‚jgšñÄÍ@’½$;ìãiíZ¬ûžÝiÁHkp0¾]_YHó,l4Žu4›Îfs½㛈±³ó¯]>KÔÜQ÷Ã6riÌäq»N†šÖdÆÙþ®¹ÃÝM÷Ÿ@ ªK|]/þ$CÚ?‘õi]¡> nÈ3 OèŒK—î;U¦Þ—#na7kÅ;î\ÁÕ®+Ö;©õòÏØ¤×”Æ ¹\ÄJ,©"ïžþ‡Qá{”v˜1¥ +Æ=¼h­€±˜OÄH„txMÕu‡÷à£ÃxàVÜ!›Ü" Ü€›5#ɽrúEØWéÌõ´½ˆ¸Ë—’y*9mï†þ¸A#®mÃ׋zl?í½ >"®šÆÔzÙ~é–÷sf"s›‰#rÓ(4å×N‰ÿŸû9Ò¡áD|Ê·ã<Ãð7uã¥ñÔSÞClò²btåd5`Ý[,oîŠÎš ‹Ý´þ”Õ[ cç{Äó}ê ‡¼‚âûi#r·([Ö3³E 0OÚ¿‹`ýäÒºÙí«²z‡ÚyÜ"ëîº%ÈVfp•§pÞS8§†Ë‚u.ü–pW‰*:×·Mµo"róJI7ùf½¿ÙÏ£ù‰«s{Ò­v$Ý[×"€”•]¡Ìé‚WÔõÌÁ÷Ìz鎯%ÿždmh¬Å †v/°ýVxï#ª-é¤E®+*—x†<‹f¢´»[QSÈ(Gw=‡SÕÌ~„Ë©~¼’YÆ4ÀQD?âÞJ›¥a®æ‘XQ§ðŠâ¾OÄî¹-Œ§:OÁ°’= Tlˆà+;p[Í9ä% í"Pª¼àž +¶ìå3JmóÊŠòcQÓ[¡°µ¼Y¸~F7£³?òì#}D¹Ðæ ¨—€ú½l9+Å!aµX0У'm™Z "C•Öz^/-κ(Ïí G[]ËŸúhæø Tßm3æŒQªŠðÈ ªqBkc¶ëµÿÈh¡C½ Y¨¯³àÞØ•ú$®x`EµŠî +èè²æ‰Ô<‘¾•S¡}¼{¶\h@6S¶–¶ SǺ~š¶RMûØ944_(9“¾”Ö¾‰¨u# ¯ã±MVÿˆºòÊÒ~;w>2¨Ø iUðæÁƒÀŸï·>ÒóЊ¹dÃï(uiן—#_gæ†IîKZ³Ü@ÏZ<µÁnDEÓ»=%zýzÁ® ?ðë'5Õ2ÆCÔZ§)«Ùlέœˆ‚|Füöéâé¯Ù’‘ßìgô‡¸»¾½’E,ÂHÙ ’ÿðíÜø„ÎD²õv7Éí£Æ.ªHÏjÞ7¦Ç9Y$mÓ‹­ ò ‡ù‡ùYãB«–6óJ}8öØ^ɱ— ¹.D»×5æß=nÒG—Ÿ5­ÉiÞ(µ¢lýÔeÚö²GÉ D4Æ,z¿C ô7RÑGÄ?ïú‘ãöûFx¬S¦§-ìšgeБd…ŒYdMAŽÁ Eõ¸øt&hö'ˆ2Az#ΧT6ðsß3½=š‰k™4å~¹2GPC‚ŽüçcóŸ¯P™Žüz›T#?=ów–Æë—–:ŒÐ¥¸Ì/Zôv´@ÚÜ(ª§vpÛ`j2ÚX§„!÷‹·Þh×Öö#/œk°9#ì”5™S7E`MfD¬éàQz\QŠ®À€ÒœÙ(ý Ó³AL&ýÐ0Ë튲 ‡{Ïí¿H£’ˆ­1ØÖ&ÓÅs”Óu² -¸§)s•TVêÇLM› +´«V5à;Nœ'­I–Ó Òv ”c|¹I‘™˜Íz×îhÌ ?æÙÊ6òosw!<Äê%}ÌÇ&2ôK:]¶›¬W³öc/p[›6÷ÐÅòd‡ªHCw×#µ‹,òðóçðgôN0= =?A¢H?ö2S—“ó]£ +ÐFý&Ÿ•bÕ Ö£½ùUϹF§HîKM§šæ›¤Ò)¥Žš>lñCäòÜ0²áåLJÆWÅg~ós­:›kŨ0 Zå/6ôT G¿xwf4SçÄãÎlrÞ†qÖ¼²u…7š [cüë&ÿ-¯]î±­ +˜ o{-#¶"Sâföö`XÙh²Âßx¼uWkô?š •ä]5@M‹¨6݉y‘s;Ê ‰¼Ñ>Ëët[Öi•™’”à[ÚÜ|&ñÂTŽàÁô 0fĆÓû†|1ïIÙÇÏñ¥ÙÂ÷SV??F&ô‹P+ûŒð)ö<Åæò$äžq¢¹îÝçÙSEeÒ¢J˜¤'zn¸Ä`®Õå³_¬(Z,-ÐøŒòŠLS2Ë@dÒž‹£pÁ™ïš¤6ô?7¬nÖ:¡*׆s_«B¥»D›öMû#¢öØÝ(<³F‘¬ßˆ\DbSkWJÚMɬ{‹îžÿ*S€—ùx` 6UG …c>Òj}½û½½¶æʀ׈°-SP? öåˆíN oÿNIîÜË®v Â¯êcûÚÿõhœ’®1²¨»q=HS¹€w¡Û¡|ƒ>¶ˆ'“gÔT»–¾^ß…zÌMgçЪ /Í…@$Ý5.U¬·ùMÄ,Zg¿ï¡2MqÌi„%E•¸N­NÁcMöº%Ö—NÓç= 3ȹËÂUúg”Â>Ôö±ÙUXüfHs«Ö/<Á[Ì›¨6ÝüCÓªaGÈp³ló5t0"˜Ÿ¯Dö.uyðÄûÈ +zt"ot†8üàHð2 )Þl:¶7¢O¦…HÔÌÆí¨ JøÈâÃý"~A"´®ì×̤OÝ{‰`éKÄK‹ j7ÜŽ1”n4ÃÑ”F)MuChýå8öèJÖmà,¦z}M…\ÔdžKú»œ ÕÐMF›"‘n;°Âr@Ù€9ïîW¿Ù•Ô;0^Ü’©T²fõ2wW|¤+. +Ñf'ëÄ^…{W“Šjý<6¬C«¨‚îù‘ÅB„u9ïû›ýÀ ½±:¨jÛµŽD(¬°D w*‚!¦¬žš‘‹¼{J|§&äOg0·sHÇÚ=N ß|]æµA ÍK5‘ac(ä&ûq¸:]Û‚žý„¶5ùÝOu`hfi„Íɶ£¢¢G¸Ž¶éü‡°§²íŽˆ4}]¯À Ê—å— ×7ûi[i-éŠÂõ¢z/+“–Ø7õÄsq?•(ÐýCáúaI¤mÉ)ÈÓŠKϘ÷B+ÊpI ú¤ÚPÕb‹ñø‘Ì¡¯9Q£Ø·£„! Üu7œÈNÎNmRñÐtðGŽ£L÷Ò“B+C—¢¼Ž£ƒNX¶†Öù9´V~NG26CÞ’ôýSjAf›²Ãõòçz‹/EAi„ç«D£wxb$´LóL¹}ë<…/œ©¬œw¹èçôÑh”(µ_”‹~ŠjèY¬ÝeZ.«âAÎ2C3ЪUP™…¯™Ycó¥Âx…éÇH J„ ”q²`…îüRB¹¤e9«¾5²”F*Ÿ³ljÉïQkÜŠÚ7Exö+6{WÜFíÁQ=èJXX3ö6ëvÎíbз‹Áã6߶ ½½SFÝ%’Ì9±GEm"(à‡rÚCÖj÷kÔÑïGøB› n‚þÊá®otŽ¨Ã$püUîø¶ÝøY€vʇg Eºµ¬½X +j Ê«lüº•9È‚o4»¤»!û¡lÙ4æÑÁ¤£ªúm(Ìâ9&Àê~míøÐÁ"Ü8«š[m™ôʲ™r‹pð…Åh¤Úì?*ê€#¿·‹ 5M±¤_9“ ²5U¢Ýèt\i‡Ì¹× ­ÎCïks›ô­XÌ_dÛkÜ5¢ÝF´úTÖÈê/)Y=¢K×>ZÙ—{ïK»®?)¯}­T­¬¥.« kâT^š¿c4Ä®DÀX¬ÉG"De”™=M€õ€W2ƒB®J‘³ZnÖò-‘²n!B!÷§ìU…«…Ÿ~ÙW4c…WHÔïéLUú)h㪨Ϣ¢<5˜'ºRDc%…]ÝÎ@ŽTqä7ÊJwîÚêJÈ)ì»ġȼÒê5 ¶­ÐxY\Oc]ŠÊ…*4S§O×P  :kïõ,¨ÿv”÷/”TîàÚsuuOsÓ¨#ŸvY4CHT ,¬°0äïê PÏ bÊkë—§~ D„Ñi3]¯ý;#D¦à«{}()Oôœ'V9D¬Õ] v†uÊÔ´*gÀ¬·u›HCÆÞý{ŒP®ØYk@<L»*F´L¡mݶ ù°aþQ0øÄ‘a­ßFçówöZ¬éí¬ª¾~£Qšsº ÆT‹‚˜^™ùø>ó,/€ô²wÿäýp øÎÚ ºê¬õŽ².†ñ,B"€vAŸ<wÐfÍû¯î$ ë¢W.qƃ‚Pc 6kpŠ¥!€^Å÷×—ô¸¶º_ÄÉl_Þµ3Õ>ËÏi@uJŽW”´SýEÅ3ØëÖÙ}DL€+ZÛGèDA’kßòa›)ëFw7d¢D¬ß¦ø©Öô¤6'¢Ã9ë(•µoD½s¤.r[Üq·™F²v83•;´)Ó„@Â,©¦ñg„¸ +}Ýêóy&QWä½&m‘ÎÍ«"çoÓÔo€3¯"ØšQ0B,WÓUì&ÇSwá=Â#!µ‡ýÄú|xÖßìG£ –”¦qm:õ_o˜Q~ñÝ{ü¨tÝ˸3Ù\ž8Èüàû×Í÷uX—ÇÀòÊ X(`´Ì2ak€X£ma¹¨oqëj ’Ý#ÜIvJñŸ½ ›‡2$J¥˜Ö¯›†E”ӔݖupzG–;Ôù|&–-9æÆKGòœÆ8Ó» 'ˆ'4ʱ¯ñmjíH„aº†ÅˆÞD4lÄ©F”IßjœEdŒZïhs7Ô&ˆ85×S¼àÞÉÓHòô/'aO6³ q:kðcª“Ò¼hs…#ƒM/ªµU°À, cÉZXùÿ8ëÞbå‚/ßäÔââME'踇ˆ±QUlë¹õº¿È9ÌôŒZW€Kƒ™|ws‡%<#òJT¯#ûWj¹²‘á'Œa_¼·gx¡çÃf¡n]nm܈蔗ÿGÊ[ €ìO¬Ó†A%Û=%?@W¦ÂM,kQÉŸåô›ö7˾7tíÕ ÃÎL¾=<µ$H +·-·•¯Ìk}Ÿ±ö=MíÏô4[sõùÍv3³i׈2òž÷>¢ˆ×-"qµâˆúïµÒž¿R¤%ôç[Ù§Óéò‰º.lo¹ž8°2;W‰ß÷‚c=-¹õ^­Ü.Ó-ΣCÉü È¢/{ßVUSôëÅVô5@¦¸„îvo otûX˜œéÍÚÊó~Æ€f#êÖ„ 8³îŠD±Ûíp©s îxÿ»¥ƒPí#5yn¯ÃX"l‡ma£â~Ø* U÷ÓäƾŒŒ³kº·RÚfÄ)è wé ÙÞkϨû…,&¹)sÆšq(¡j¡Ý*ZL4€¸Å(,¢ö²ÆÔoîmwÉ„ó¤jQ÷ˆ†ñ­¾Õí £ŒÄEWR +¼tù(ðB ° +¢m䌋èŠ÷ˆŒVgØ¿çJÑ…|ì‡â‰Å¼µî*ìó pü&mZæSò ± ßDÁƣݷúÊ›T½@Lφ¯Ñ6ÚnqwÖR+‹üUj¶ð®5l"²~½÷+:—n~¾_€™+¹˜—œ¤P”ñ¿{QBî ×åÚ +L²z÷q@pƒèF F2Áõôð˜äÖe WM¥¯R®»Ó“LŽºÜˆÖ Ÿ•Óº ÀÓÛ$ý>b’^Á&ÊÒEE¾ì®–óñÉ𵎄'ë©aWˆ#8ûš4h‘C† …Æ‚=Ö\òšàæn‡\™Ÿ£ôs«rPk5©‹åqhüÍö~?Žpô¤.âÃ̲ßn÷šeBèqH¡ã׶Ì8dz-CòrÀŒ¹FlÞ:#Õôð5l98ìâõSø3S;ÔÆV¶tæã£ÿ¬›ŠõŽ¨7VZדÜDÕM­@~üù$èY€h¤¨Èz O½EaêX¬Fœ&ÙŠ@úê—¨…ˆoNÌ‹¤Ÿ£…È¥‹vŽQËuK(Úƒ¶÷pJìZ™+Ëž9«¨D°î&BÏõ•®] +D[5ý:Îv£…¤F[;ršÝѶI¼ÍtÕs@Ys!’ËâqÑ÷íØô8ô#ŸSùµ°ÌÜ5´®ãåÅ;"l§ádæ ò!ì¹Î׃øy#ZN‡ÜH¬±¶º¤¢ëUZÅôŠ‡·P(Ž_4S!Æo©]R Wzúüf?­(\Äb·Þ3ðÍ¡ $ÛšìÜ1Éí-žóœèÂV“¨u>hàï+K½ÛÖWdiî…aE3­h7S_¬'A©)Ë ~P?;¿‰HO•¦t±ëžê_¿ïÕŸ?*g3Ö7(É€d¾¡^|FüŒ&^¯Ÿbôh¾ãÑŒ7ML•µŸF¬êˆž xí‹«à¿íg qɳˆ,ªoMÖ®d.ßìg­œ %€FÐãgÔD^Ë‹ò]à"¯RQ¿}¤÷(aZÌ † :§­¸ØCÁ¸µZW?^ŸëbûìüŒxÞ·a!…rË&ãþ¼Ÿ—ŠÁò?’¶7£¾õ|$Kˆ•áÌ°Oó˜× E,4ªHÑcã…(Ù0ãq(Sy+¡¶*=þõ«É8Mï‹7‚š´°úúÚ%ƒ‡J”…;êSr‹TÅ¡(°^ýõwoâjŠW •)núû@«ùž¦uYjŸy8oiÛç~`æ';Qß잊*/¶%ÁT¿ø´ýll×G‹CP¶SÝUOwîÓ…ž4Jô(.”z)hÿsoÁ9¨ƒôñÛ‚³û¢1Ì7AB¢NQP7«FuO%ðC¢§ŠH³<(¥­[ñaq +bÉJ}Ý pPS¤½Ãº â*… ÈÔ¿‰P!÷T!wÞýðE¯výº”4jq Ô ÐˆcV3•õòaralÕñ<åÒ +´ikÄÉOòì+ƈ§>ˆð #=®DN_±÷Ö‡ÅT Éikh%*PP'” ÝÒ7·§•[É#tP€&&Âåt áBã%@ÄKG¶ÞáÕ®/%Â$«Ù¶i +[Ñ_³äaß…y&fؘʹo"î.)á´|_ÝŸ¢BŠ¶ ²BL kÄ’ÓŒX%Ð XûuF¨–1Dƒˆßî\IÑñÓuHýf?'ä)¨¨_t Õˆ\+êêd÷ðw{A~Ú)Ë)K¡ZÌ›®ë· ô7èçú<''V/ý'Þ#^£ ¤ˆÓ§*oïûAæbúbøë‘7?´ë« ‹SøYïŒ[<ÜûÜ·»åv?¾áD5ˆ›<êò蜡»ÊÍ…h‰ Y©~æ‡ÀcÝG2±¹nšFhü7IØ®¯#eÄÞ­7õÒà3¬¨£ì3FéŸÉæV„Sš$ ëHÝ ¤¥{qz@\Ÿ´„ÕõÌ x|†ÈJ!eJ–÷žŒ¤f7¦x8äf?x¡sWZ0Á9Î[ +¼½EÀØ{€£À½íè&$¯¬H¬—‡žöG@[ê¾è¥ÂßÛ^zXÆ@{×õ÷@½C¤è°ô#6Ü¡”À„y–ÊXIË;åÞQ#  nÍšV 7úmlt}`DÉ.zÈòÊ)=)"óá-µx€03]M•’ƒŒœÞE²£Ð­OÍ’³iË• U¡Éyh-ñôÓÜв*xó›ýÔÓà ° "8W¸òÖFð} ¿šî#}DÝqM\ƒ‡}aX€úo¨‰8aKIÜ;‰’פˆ>š*§Ü´7lÚ©\|µ¾7–0%Öa­ÑÁ.e‹”Ë𢑛¶ë¬¯C# +Ák <[ È Þ ‚zÛräÇ]QNñTÈ`³)“¼"b,BY°æTpÎñé@h1‚R +OL)ªŽwÛtZ´O/ú óA|ZïCÑDw4ƒ®ä·W=—ñ±±©:S…{ãC©üÌ·i­îÙ·qÀLkŸ9ÀLzxBQ™ßîgåÜ„×:eÏPÍÏ•ÂNÙƒ›á9“‘Ýåy¨+Ï>v nEïvoé¶÷¨¿Ùçcg± ™™è31€îÖªr.e:æ ô£ZwÃ/¶}FtñZŒ[kbð#ªÌè8’{"m³^çÍò@ÍZµŠ7ÕÚ*†ý@Äw‚ùÿ^×–Z¡úƒµô,ßì‡â­X¤j=}Fê15ä]ø˜šS±~ZãXh kœ°•‹8°Y Ý€"«9ˆöDZ K†”¢d³#ðjºpšÔ5‚∮T@@ ô -\!*`#ƒ/=^}?Ö˜0˜ó\ŸŸ¨c¶ô4b‰JÁAAuŒ× åÖWšŽ<Ÿf»TT }®u‚B´pn pÑf ¨‚"Mßí—v  ÑÆ7BUU +ÐIåÇz ·¼ÿ9]š™:FQã› £vkod?Ï(€Åw]øöø&£‡®^ý:µ=üFј‹¸Öåêx‘e@3ƒ6H¡ŽÅmfEþpô­h«W‰þ1kÕSv÷.v×'ðÀXçºH¿oõ "¯¢âdgmÝ~üYÌß”çÿ Ò»$ô_²ÍÏmÖA:•&êp+±Ùx»û”"J Œûñ6ì¤7=móI+MaÛ¦w+]FÈG¨H(ÔìÃ(}Ïò(4à'®…>ò 3ZÐ:•Á£ƒ„á$ªÊ®+`¼ =ÜÑ´M-/kÍzÞY©V% WîñÚÚ´Žb”hÓ°ˆ¼~|hçÁú”~ú£ +•Û<ƒßPR–[LI³‚ýÅþ)·Õ„=%mPÅèìCX"ÐàñL¼@¤”«L3:9}˜Këô™Áöþ‡(|Îö8a=Þ˜ë¬%ñ„EG#yö‘Õ¼éAõ¨’YasîÔ<à¦@™·”å Ä|s†ÁØN« ¨šÑá.—½í#ó"•ôö±Ä¥'¿:k"]í^ßj³ÊÎTßí••¸1mŒ`°§ÒOþ]ÐØÚÊ 1&ÀÊï6D¯?Á‚þeS$ö+ckˆRƒ6k-VUørƒŠýÀ ÉpŽ*ÕÊ ´ùÓªOÊfÔ±7jßFݤèe…i=µbk;búóC% `*É©9¿#tGúÌ^,ÕH·hè€rŒ'ܵ=ᎾUK¦x‹¨:A +T"’@îµ3JÐhhI¸ZÕ> ŠÈ2(””I4”3%÷±EOìXPÇ6Q…Âå)®Uœh<÷A`x®àŽÔÿʈZS +Ÿ³©±¡ÐMECG9#ÕRž Ð¬`g9^/;å(Ý_¹#|ùOë©iP£°5üzOfœÞaÝȪf«êÕ_#re^«†ð^<¼ŒÄhÇЗÀ¦÷¬š¹8ÿ Ñ¨©Âdñ”ˆBdÆ0¶1‚Z±eä«ln†Ã;Þ3GÂM`L¥þ¿` ‚t×Q¯b” cnI@"P$@jÁ "t–X]HŠÀ[U¢pN5š× ‚º‚îÈvz¨Þ‡r›°ghVˆ*ì¼<Ǿ¢µrîr@m»`9†Nàh-à HFÍ“æ´$`EáÊ2¸Nܱ+`Öº%‚üÉ~>„Àw£t~tÙ®Îð0 *ÂFH„^ Á¶©”‡zs#òB×.æC§°ÉtI¸œr•=,‹À§Ü›VynZ%/kyJö£özlNÃà©PÔ~EmÄýEhÇÌÐrÙëW-ß ‹²‘û¡ç +•\M~¹ÅûD[ãˈ!#²—^©Çkçmo›®™·>€LE§òvè«¡ ¡Zø¨õ\i‰-I –¹$5ÇYRÏ ¯é`ùZ¬Ñ7©O›qZNý©ÙgHúÄljúä’·oÇ+”´˜Íÿl¢öä|ßJ:ç` ùÿ)é´YíSEôÒC™ú€Ä£MðPrÃ^ôòA|ŠgÕçk¿ÙE랧 æ®ã¶„­[Þê¦G°’||gÛÀNz\8)~Ý`&»?¨Ë+|ïÜâÐ7b”#Mtàz®ÇFO«(L³æü:P¦¿¾¾¦¾tñƒË^u¬Ø3¹]CÆ[²JŽÙÜaéDR\ÛúI"gûx^ÁU‡’bn£©*DµF¢”[µ4—@g¢R1¶úniËÝ4X²h;Ó_wVeZß­ÖJÑ;a–ß‚¨-V òÉ“jX½‘ ¯t´OæURó¹Ùÿmí¶ÄT‘WÔ¨.ÞlDm(¾*ayzÇHÌî õðº#çfŒ­ð3&§ÑK„8Aøwsj—Ú9ë+€¾¢»§gÊ ç–Uó`eý¤Îè0 í',dË)·K‡§Ö7ÓE9×B—5æQ"#ÄÞKù³ŸÆ¯ÿ‚_#E'°îwW\uÃaPùšBêïwΔ¦¬=ªfÜ‚né ±ˆ°kÑÐ(°D/uä ˆ™M!©Ù½ë3rÝо_ôÓÔŠ +üþfTáþ]TE­¾A¹jžÊ„š¡ì7IÆŠXù@É©@j˜›±jWPùfëßõ!ºÞƒö_‰5›˜'öuË.]0èt¶ûöÁ¶ÐD­gÚ¤kÓ ‡l—EÁ«‹©ê–=·ˆ2©>éj)ÙJÆš­'$“!EgñÛ¡5ŽAc ¥ïÖ¯$L‘-#¢ +²X W´3‰­Å¼VVò9ÀI½'LÙ¯ÇH•¦bs†Êß^6¥h½Ì¯ã°l#ˆâQö²Ñƒ¤IȯŽÇBªÄBª9Už×/i4'‚ñ’ˆ§Q…8`0$»"¿ƒ6ÈyÚµëõgêõæ W\¨×^Z1ÐÏíÿµÝ,©ú½ÚSýÞͶF¼®‹¶÷) 5=ËËr’n§EÛ +L\Î.í±ªEÉ~<¾]tYÄÝY‰íqêyw<&Ž£\P¢Û¹võî²Õ2N)–ÑKºYߺ®tv ïŽòiÝÍ.Â9)½-GÂÑ aF¹þdýYçvΊÎÆLÜš€xʽW®l•†=M +•*»ø$?$ÍÊ&bªçí­± zj¾#ÔWò2(OHKQvªòçWóó¾Un2b6Ü@:«ÍÏxÇJ¶Øܲuš÷Þ¬i¥’2MÅ¿gï‡,Ö0z­Õ m¬…ñ3óŠÀÎÓ?Ú>*Š£¶}Yä(ý3VÑW©¦ó0ðAË¥ø?ŠkÝÒï´]b;‘3¢8PFÔà +uúŸó–Z^l¶3|zôÛêÊ9ëÌŒhˆ±î¯êØh*fqº;D_ [ùÚ¯ü¿J*ÓãÎ,Æ’N0<^Q%_N>‰#]µXÎÈä²»uÖÁpoç€V ƒúHÙÁf ½ÇŸ‹¦#Ù…¶S}Çõ1Rd_cŸæ¤‡iXÕiO¥þ™VsKqÂgÙ˜ÉK•õõsüm¢d¶ÕŽÎŽÞ-mï<¶óØž2is®Ó Çž†Y—R!äRÉ ¹T­±ìïEZvòýiÝÖ"‚‹£|(…Ž¼àZ^¬£o$þ“æô&’Jå… Uö-OmÛxÞ/n•–ÿ(õ&W·€õÆ^G®cHd$;^Ëü Ú®HÁÊi”;×qÄòŸ2t5û#ªfÅšn3ØDrÁÝØ5wYÖ,7)«z:ÐÈS,.SO7Kü?¯ Ï¢DË“ü1/©ä ð.¿ª€úÀ\1~×±Å`O«–-ûÅê ÷ªÇ«†“o‘\™íeÄkW&¿f•GdÊà)Ž¾×\ÏŸMmþ’ËšŠ‰ ‰îºc0|—ºÖÞ×Z˜¥¹‰T¡€*\*‚ãgÝIž´æ%×ÞÎ;´%V ë™ +f‚Ô¸àÐêqnþQ *ë·ûHkJ‡JJ¢ÀØZ“£ÓYËÖj4R¹6»ZÝÀÛ©8hŽGHýÚªÖH†>’!¾ +x®Ç&åöÚg¼¢æ™#™\s‰Öö8eÉ‹„êÀ dq[ãR,5ý}œS[Y|ÚG$%ÐNi*ËS¯ûAí5)êŸ +ññe«u¬Ý)B²Yßd³‹ƒz UÔ<|>D¼‘ëÁ­þÆNy(¢¬×¤©BÔ4A*á—2’ªCÚ—yÚÊêyµ’~P‹ÜfÑDR„Š>+®°çïs‹&À³ýÂnŽ’|“Uÿ:ÎÔÔÁfáó!%xõönšc$Ï÷öѳt™‰ˆ2’%©DhÕA†ô%¨#Ù¾—2 Ô©²lƒCÊ7~Þ©+Q¼VîÛV<J?¯Ó*8éa€À§‰ð¼>¢ÑÊâå»ÑrÒ„¬HÏÅm_µ¹¼­Q5;Ù´‚jüP +8Úˆ“<:ìPx’àXI¿ €Ù¥FÆZ@7ºC.©³ƒ¯ÇZ|·áçBè¾ôÝxDJ(¨²‚ g„ h±¨ˆÛ +Ž~èØ0X”ü‚'Lœ/JÝÑž\ŸšxË·¦ÿ–žöÝ@jó pž> +kc´ö¡_¹þêM8olÃH¥ç³ºB^_”’kíÞLéÉ·Â>ö;1}¹Š`‘rƽU}W¾›G¬îú*5àGÐnÏZ€— ú"³mNvC•÷2¯¸=½#'x½‹+ù?EU7Ù=ëE.ÛYd5¥Öì65‡ï¶KIŸfîQcQT ×~¾ P¶ \›ÖTXK½$Zãu€Ì•UÃÈ)àÑÖ+6\ÅuüàÀ³˜jYð‹i#Gô[@M©€Œ‡†Ä‹³ÔHEIfl¶?¢¸%· UÝç&Ü‹SÒ{0_G¬³¤8erF}ëØÆu;|5ójêEŠ%£ʯ›¾z¸‰1Œ¯sÌÇŒAN™Ûñ šP< öþ¯4%NŠÍ»´ds —nX•5=ñ2·¶Ç£B\íÞ"b n34¦Ž?ãëz{Vr +Òz2ãŠO¸,~Y/§p*0¾R"+ÈF¾Õ¤EKŒéjf…s NˆÏ9_Gþ¢šEG@(ògüò¸Cbk€SÕÈl ¢Ã'BÀ…çž=sÇ äËî×z}¢ß}£²^âËäH÷²ê™.Žˆt[#YK˜ØŸ³ªU{>ZÜ-Ef—o3íù¸ìðö('=´Ygö8l©a ­hŒ‘•#—û +ÊžÀŒ¶Ýáaöxò¯c7TQTÕÛÅR + NJð”¯Éz «ÜÙ¾‡ù륀׋nÄ:ų®]×<\{^ÙcÏ£¨/Ô-5åà¶KS‹ûñþR²)Š^Ôm–vâPHÕì*‡a’‚DÔSfÒ@;zEmM”Vªçwõ®aϹ¨ô„WÖ½HK×ð)ñ›Þ| ¦3»¿D;›µmª„6Ù÷ô|;!·Jw3À¡nÃŽë#šê¨¢¡©ŽÒ+ÙK4G(ríùIíˆ~ë°sšÄƒLBE[}@ +ÁêØ"Ǩå!þÊQ÷È"ë¢,ÌXG÷²ûñXü 8ß„c¯´ç Æjû"Ru¿½Œo±ûÎú×æ]3Fò=1Öbõ„Û¼Ý^™#2NÉ•û§Ëvœ/¦³˜‚]»9Ê^3Þ!T3†n´ ÙEõÅ&뺾õ©® l<ÔÎrddi”Ø@2Óõ´¯#‘t1Òhœy• Q§Ã4:7ÚŠ²Åöˆ47‹žSÙÔÖ•Š&bäd1HzŽCÿ™’P¿z@cÇ•ÝyÌÈë¹j¢«Ð à¡*£Jd€òî¨^Ëf(+j  ²t -UX$AyæÚïg‡Ë7b i9pÁŠæ*Ë?…Иž¡ÂT´M튎鋅älj‰Òâ8$2.@‘Q.½7ï¿ b„âˆq±Ø¨Þ¥¸M?466šØUaJ~—Ç–ý¤¾ózŸbžÐç@‰“S&øQç³{;ë9=~—UOVg(ÖÜ Gá; +>^­ì¬ÙÑŒJ@’­´!ÿ…tw§ŠëLõâÚ1I‡Ð¶ÐÂãöÜ +´õøð Ø¹nÑ›ñ+jñ냥 Èj®EïKP)4~‡e4؈íéFÔë6QBþzWÙ¨7Kw#É‚¿rÝŠŠè"ü3Îü²@þŒ:Âúá|ÖFŠ¦¨ƒ¶64¯›â\Ø0µ€âì–«%#çt¼ëÙºhÓ‹ÅY ÿ´ÆH#ë ýdœD)/CYm¨Ž·I`, xRwoMúP¿ug³cÌjÏgÝ#‡¾"h+$Y]"ÈôŸ#Û?ADÑ_@5^ñ€½r*b¿m[&]¨¢!NŽs>ÔT‰'ÜX-*'Œ_‘y¿Çý› à¤@…SºÉµk[€fÒ8•tþ,ú +?‘®)#½¼#þ#·c%QÚšw‹ãšàÇt<ÊØ `Sc©vä-Öo!>±”zÇ6×4ÚwºFêù‡ë|ŽúgO5nûÊe©Í™¯šˆ¢?Ô87×{X–A€éÑ*5°]$bõ§¸j™AÒÎÐRÂ^qá•1˜´È}1ã·v¨Œ,CR¾°.Å´#pµ¢¢ŽiS>M$êHÊ6e"gÕvç2©¥‰3kÔÒ•)±ŸÎ½Ùï¤Ug>Ã*_€Z•ÙãÁ¶W…~ÓsÑ0|®Ç£“Àå¬Î>(_K±þr¸¶¬p¹ýÊZÙÏ[ð‘¡KxXaù–¾,8¨Ìlßî?LÿõÎÑ^x” Ew—s½GˆÞŠDô䪔+ H „&ãY{ï5;\kå‘ât×5*¹ Û†& &Ù¯cBùϲHI\ùëê¥Õt©!1@2øëH²æÎq§Ûé 凙 I1Ö õÍáGf×^Vt(¨¨ƒÓT¾”¤<ô±V#@üQ‘§S«ì°*ønìÕ0Ôg±©÷üƒDnIW`àΊRø«zdËôº¡5G³O„Ï*Ñ3Ö!OÐæâ¥Õã ÉÖw—"ã0ƒ79³å\ŽŠÞ¥9x"…ÅYn©F$Ä…èÔûti (÷…¯Á£€ºÕ —Ö +¦]÷†#§å ëàH†‘ÝdEÙQÿÛ +¾€ÐæU Ô›6ŒT綻 –ÙWÖ®L+TÒ·ì¦t5T`OϴǘhYiΨ#Ž¦5 ¸(ƒœ]c<±ïmC.úqêo”…o+GÛòÛ +O®Km 8)Ûy9÷½ëêôëÚ›ˆuh]`{ò;ùÍ(E³ˆª¸UÛ­Ö}àÈ ç – éÒqE Ývˆ¦ö&' +* ÔþE®`ªt5/ö„OŘPÓýv;ÖHSï(™¤ZgYs›žA5-¹JR]ê÷ýhºnâA¡Áí Ž*|nc=/5„©’û _AÒƒìîjûnV0êD]và›çæëH"@w¼;‡ù–@y›˜zòwFyèU]-Å’…M\»†ô3–ÈZ0 l0ßdu/‘D:›ú’AvTà¿£O*gDÐÇ'‚É5†Üssû/Dš˜s_lÈ› -—ÐecòäY¢Þí¢¯0Ä—pô·ºÆ¼„ëöÈd°¹­ú\wê$ë!­\õÁù‘.£[qþKöBóá:•Xmµ˜qŧ²Ñ'êö3óù\€Á‚5Œ÷‘æØ*ëëT§\ÿ¾jk*5VÜÄ'kÙ<ù÷s}¶äa %êÉ@¦÷—>íH UûíÑ3TqñGw-y¨:qè +Õ¦%wÔs:f%B/$ˆGÚŽ âh‰n%j+¡S„?"Á–ó¤o>Éî27¼;šfe€læk¸Ôce¨sŠ“ºä≣×]z]¬~š×òñQð¶Qd¬®©07îá­§Ãôß®¸Rðø›Ê…¬È„€ì ,H5-®X©â\Þ@&©ù>·ÁÄo5Bm×’@ƒCÞý¢ß&D`˜ëƒâÚž‘—ðÔŽ(Ra]¹å%D›ü«TÑcJy±ÖºøoþŸ°Ü¬Ìј»Ü†þˆC_Ä^A¼Iä?¶ŠÉ%S(öa¿Tü€ðÐ*øØÀE5pëBÝU·éTaÊ‘\¡Ê¥>O+Ö-+&c¾ÐGöŠ åqm¡v4vÓÑåñ`1’…ƒýmeC3Z”n f½NB@òzáQôÐ+pý_ï­Ó1XQ€›¬•d¥K’È@sRŽDE¹Ô(¶fÕv‚dºþ„üµg–˜—_HöÂ|£Nó€•h—é€ +U~ªŠ-ïÃÕšÌÖu½Å¬¥Ö'kWecë!ÔOn½±ñô»ŽÃ~×TIô1/ªÆŽP0„Ëþ?öBVÊaE¿†ÝvD½æäÀ5öR(깡nY³RN9øÀ +ƒ&ƒ–ùˆÌÎø‘¬A⼶h;ÝzËQcDY«Ewoí§¥Zr¨Ñwªþã´‡¿‚v$Ä4i t<Ì|$…ë,Z-[=žvV9aÆï;à ,•ê¥ý…T/x/½§n¥;¨ÁÜ^Î6̃Ǘ NÜ¢Ñ:Æ@½%öj”’³»JFM?Ôý8ÏУNâÏÂðþ’ ¿ÿ÷OúÜÿ+wôa Ÿÿ/þÿwVîâœê­î2š}[DY¬ÏšuÌïwP†K ?”¾û9È›ã(¼nΙŠ:iü}Þû¦1i àꢢü³ZÓ˜ &9œÒÕ%>þyËP`DѲ+ªÔöàÐ3s½7GéæËwE­§¨«j£hSUõ—!¢iê׫rßt†°™2R‰¼¶©û8²Eá(”‡R/¯³2¦tTö¢ìja¤¸¡W`êÐ… h¥¨&Œû™ʨÑzל†²o"Žg¦mÛœ‘–åE»åHºymÄ¢Q~c€é.Ÿù´ãú'¿à€<Ò\öÞ‚s€¶@ó©þ·ˆâá¾&À-ê¬æ˜šTEÂHá<ïO—)YS©F­5Žƒ(-/,“À$ÔM­' ;xÙ+1»\ÁXe XÂ${ s|j7×rû¶:‹G¨f¯ÉŠ¬ž{ç¢ëüßv ¡~ëhž ÚšôŒÞu²U&ËcsöúÔ@eå½:f&ð<›kç9žÉž/Ðoš…æùØZu—Õ9•Qd„Áßø¥ÃŽï=PŽõàˆÎf,[Z¹ðb.brøЬv–©Ò¬2Q_œ‚. ×g„YBI2Nã]ÈÂÇn¬rBb<ƒYü¡ýÖ‰ùùü ˜ÜA?Ì“e`ó´ñ, 0|¡‡]0”æÔåQ„e zw5G²!gðD2èåëWÈ£ƒ£_ãU›[‰¿éí¥…•IÝ¡;ö¹’á÷Gæ6Ú—k[W¶.}p3p®üz›þZÃ=øø}ÊÚ§í–^üpÄ[«À‘ˆ`†Ú‡¨'R¶õ-‚º ù¿ô(»¨+·ÖµÅjMLîF–——6Ö¥ñap NF8ÚnÖ1„"5ÌZm,±æ/Âý¤Î(Šmlk…ëlßmhk"CéếGÙë†ÎÝ| à ˆéé,ΘÅÀÇIGQ“Ék\;âÜñÓjŒ‡Þ·Ù/¸-z`\Læi¦‘Ú³÷Ïíß½¨Qð‘h= +ȉSÝßS‘¿J4,[+…¥«*ǧ4$kÒÿÜÍ@ËeMÈEëÌFiRÒ*£âFm€ŠÛÜ’ÿV´ö½6‚À(¤¢HuÜÙZ™¶cWî†þ/G•…ƒuÀ öR…8b9÷ùØHçÎ[Uý鸫h¹¶—WE»öØ6<”[Á‚³žY"²áTCŽìÚoú´Û ÷Ööë(iTÝÊ7ä›Ï·<µèaR¿ƒ–eá•X1¹%շψ”ŸB¾ÿØèòϺÔd²y`V¼é·Ÿy§(ŽiGÁ·dâ2ÑÑ¢¬P^´[¥E<{;ç6äS|¹ÊÆ!qšª +¢wìŸË¿þÅ4s_þu”yq ÖØé#”ˆ½`ÿõLÎ"G×TŠ´Ç. diÒDJm·áŒÿùH©E›Œã ëªÑšUƒàÿ¾We-‹êCåð +ó³S!ëÀRàj¡8°dDض4ºÊÉþ?Êï%GÒnƒòÙñ`X¯LÍŠ’hÔ¾’NȦm÷d±­À0{Û‚ÝQz®§ÏD Ĺ"f«Žü­ðø•—Ë2ÅöªÎ{9w£´XþLYì¨Voîf,ÁILî±+¤¢áâÝ/cU‹ç+I:27 ÑÖê<"@®lj‹œݾýIJ’‚p"ßj ÁU1£[ÛÖ¸TÀ_¼ —Úd*íŒ?íO¢¾2@l.Ä0Á©÷ãd†JM1BÒŠè}$B& .<*s ’®9 dí¾ˆ’JX"*¹6uà¼TÖ§KF@ÅÍs „Ž &…Þƒ7ÛÞ¡k(Löpþµp×èVKg4—Ô¨ÔPˆøsŸË¯ÿ‚Ÿ¨”•kׇƒÓ(ˆ›ˆB +2êù8õ“CŒÚˆ!ÀeDõ[h®Kj +4<:ƒ<âQLW¾#*ºA/­|·Î/l®âDE‰l’ø1x1½º…‹Go –•ù ¬èÎÖýSä„8ÀñjŸ¶ùZÏï˜á·µ6€Ó§4e*2º{·(î‚ñ;Ñ϶ ~RÔ>"RWª»Ê';¿ÙÍ@µœÑ)Æž¯™ÛTø!jVÌèQí[Þ­]Q›} DQ:8è/ Ú²¢Ô¤pyo+]ljcKÒá ?À‰Ýñ_0ˆ6ÁÈ:tÝø…›Ù'2Ôú£=¤ÌÌŠ:Žì'ë+iÅûHðSVÄy$…ûÖcl-è¢_t‡k¢ÞõÕKë ÿzHó°‰Î-ðPf %Àf¥mÔÝ$m0™EéÓwÄ/¥…žº¾æT +­·í¼™ ²PÖ¡*‰‚6ǽspŠÂ´K'ËúmIMS2á~!Þ£fhz +V%u#ÁM‡8¦S½KÁ<+Å*¢­Çù´Z@«ÔψßîNˆú3êåjù~ìgƒ0Ñ@­» …¼öRS„ÃΠðÇî‘ûŒâ“°¹0Š&Wc·¥¸ù¸Ø f”p=×\HQql)aŒ¤•,žÉxÞ. ]¿¦¸8£.rv6û™Üj·ÑÖª‘”GTõ’'s§¹«¶ýDE÷ƤŒý:È¥[©>jÈßÝ(ïs$Ë|+¢i_@ÄÈ‹&ï¤rÕÍGµ{¥/ý‘qtÊñ–³+F ôäNp½S‚UT pÒE0|wU`µVÏt´‡ž)h˜Üê’ªŒJE‚e5qÛÛÃK0«£´X +§ñ²ÊtYÓ`›MŽ5ÛÚQƒ€ þj-¤Õ˜%žº–uK̯䵷tmɸ‰ørôtE}mëܯŠ v‘ZZehžé¡è/พ}Í“9o@ÉDô2 `¥+Ÿô|¦k‚KW¹ ”ϳªkpo`ÿzÿ¤õýùœè/¹H‚œ¤ ÏúJ«½AÝr`·*Ó1ü²ÒZ °£!Øïd¶uZäGü¨ˆu Ò:ÞE[…Å–3Â7Ê®«âºV|Åú~„åÄv L7'댯ƒžf#£§“]Åiðt?¦WĈL'j/ZKÔíKC„Ê:Á¹¾«ðF‘®Qt%ù å€p£®µo¿àÀÄù8=IäE¥w7Œ9¿XeÂoDßc»J «¶R…{jî#ù€êeáÈMåÚJí@4æ­ŽbPë<Öjb“VFàk0ÓOhÚ Š\[ùÝ^Ÿ-mjâû»½À“š/ÈZ™‘€2g&GQÜävL«¤e×v¤ê™(‡ŽPÓBkžJyi>·L²¥=Ô$>_iOÛ¯Xçƒô3 +N'QuŸ£QG#ÖLOCk&/rL¦spÞ^}:mß«³PÓq¹’2¡]õ‘<¢aƒeB¯ÉglòN¤,½GŒ˜‚= +êFÍ%ü²©ù&¤ÎÔ`+T ± ³jYæ²Ã¹²ü=ýöq€Ú\_¿’ærq¼0† ~`ŽÈÍÄÅ +Eàª]ƒÞ—]"~r8¯Í‘"M°³ëša ©¢Î=Íêî#¤v\'ýWˆÂò˜…5dz ðì?<?vSgº*~1#tKiÄÉWf³cGÜÿÙ£àõ¾q2cIh¸ôOïÃ$¾4%¸Å_] +?Á\ì»?´®ÖÓæ1V¾”yLÎP½¡‘k¸„xœL—ýÛ2oŠ=Ъ¡è @ýà3g·jâqüÒØî§Í?æ6ÿPH¦oS‚£™UµŽoáC 8ûí®Ý{ÀóZM½sô}TUÈá3¥Ø­ ßéò ¥¶Â¢Uú~Üñn~7¡H­;}þ‚qî ’äg= +êCZ0ÆÞ¸¹šµB7¦fƒ–tdÐÌHÁÊ÷Õ`·Q{Ú?ÒÊŠaWg¨„Œ¼"Öze&B&ÎÚË="ò.ó/eî Jæc"ˆ °&¹( –45\ìû1Ÿ2ËèñÆš7Ë×­ ¤Òtá¼>#r€ë1IJM≚W¢H¾y¶r{Àý苉ÌAZ¹€Qò3âWxÌã-÷¥—ÍÙãeÓÃiøí#Ž}§(` ë¥úȭξÒçýá‘ Ë@zÙÆwûÑY^ ‰Ú@=ŸÃðwAD£lðè’|ÄP†¦ÇEÿþ¾r¾š¤«'mg—uÖ|XÀý2*ÒmC%†åÛgÄó…Vï F÷ýÍ~°Ý‘E‡+[É|¡VŠ¥ãÛQÁ)QHýª­Þ!n¢zï€I±¡ (•[eîRËïÙÂrnM!L¹ÑýY÷zßgDî] üï\3Ÿûy"þy—‘»?~¾&j*kõ­šyîuϨIyÓ 1ŽºœâÆ‹%êq9:’]T碘CÍŠv<3$úï1!ʈ¹#꾟ˆr¿ü”Ž#,§nyGŠó±)ÎWøJG¨ÏÛSzŠ6ñwV¾ „Z›˜%¤’‡ùܶ;ÅšÀÖÝlᜊü,A¾"Ï©n%P…JÀÎÔi½¯ˆ8"êjÌ(“¡àQü¦Mù,ˆ0Ñ÷ùL3À|á‘ç}—¬Qk*'Ú߃0H=^6©ž²œüE ?Ž3ð0¢ÖPiãâ¥V¶#D¬5ùÜû¹c‹^ ‡ 6ô_ì×aù@ElĨŘ˜< üø6B3²‹ùàʨZs`r· ƒBE$æ)âŽεH§L&xÇU§«©øƒø AJ— ”µ¥ÇÄe’ÜÕmîÇ~Ï©a…· +,Þê-#uýÁv‚…±Õ>«i~~Ùgĺ¥r²Ñß|uiŽx8E¦Zwí~ð¹o‘\l¾nP97èqͤ0Ä%KÅwŠ#EƒZL–p™ß80É ö‚”9ã´>8¥sÛî@XÇìŽéÆ­*+¸è’©`5Í Ú4à¼C/ÚZÒG¤…hÑŠ:@j!áy3ˆ¦"J(åX"jáácz¶ëí×&?XäÁ„¶Ý¤˜Êÿs(܀לùµÕâ‚Ô÷•ýGHìŸ7?-.ùµ#"Mòxdðk˜Ø!cå!×`2 \VcÓ7¦~`ò‚i3cŽ{ωêîØÓÑ +Â8‚8"÷ä¾×óªÑU)BCwP7ˆ¦Râi^þH°GÊ® ¦|Ã÷]Bz ‰êp¡¬Ž­SFɯÃXC±=UrÙª÷Œ7¡GÅ‘\¾ªš ŒÆ‘Û-lÉ9N ¶k´ãËDL> ‚¸C¢ë!”µ@sGÏ£!ª\Fj`ù2·‡¾°öMUªF¶Í}¾·{ÅÊìFÛõÜZ›¡‚dÞn:/<;7ÊZ÷çïîÔ£º&2êxý!"égäPCï®Ñ°§öÁ·@rx3ˆ?<Ø(eQ#bv8zÚÖ8S2üBñˆ QÓF-v†eî #?y×¾.œýuÕo~΋0#6­%JH 7¯ =Å(„¹Æ/ÞžÞÌÔø®Ï1ÌMØdí ØRÖð¥ÜÇB%]Mºã79ŠwÈzŠÝ ;ž¯Ê™[+ê;v!¢ÆÞÛee£ ŽZ ©þJòn“¯¼¦ºx€ŸŒ`z%Bý¾zG±ß¡ò,¯Óu~©ŒbýÏ”@‹Mr#Ö6ŠL¬;pÎÐJÆ*Aµ÷fóé}Cnî{föñó5Ö¨€°nWí úåMTŠn}Ùß#|Š=Oqc^>ƒ ƒ8R4Àû<»ˆXÔªèa‘úÐúçÚº@¸õ¯ñÂÒ\uC]X Q$zN 8êÜéÀ”JZ¬=Ô jPsŽ…õÆ:Î"Òµ…´r ¸¸6œŠ¹\ÄE½¿©uDÔC!d„¸r¢XlŠÜ˜‘y‹œhT ”,Š¢. hpY‡*k¼Ì$6H±jÅr_acZµ.%¯Uœ,o”)$CÌíb„‚ÿ³¸¤úö¾lÙ0ʱ¬9·Ì(ÀUæðöµÿL‘PK‡n1·?×bª IÛsz ï‰èÈ)yåRTzÒØü××gl?±Bcw"­j/ðÒ8ÕTÒº¹mùÄ|£;©êÆ{Ä,Züæƒ~߇‰rc€7æ’ˆÕq1Ÿ#[2¢%üIñB18œ¤/ï™BF¬ªJ„ ¾Ù:>Ô +Ññ9Ä=vE’?7Pˆ4¹jý¼Å¼©eÓÙ?¤BCݽ‚ºì×áb~7ºŠ9·’E¯ñƒ6*zOSs¤÷ý¬)ÏÚ2½{¹9,tÅ^Áw5#»È½î +ÿ_/†âW/«º PÉÛä~áé€8¨,Á-a¿&',•!â€tCäÙ_û·ëRvkŒ ê7i5C„hFrOmY5ÐmPŸ¬-–ETo ªº¡V÷÷#/÷>Fgà'ÛAŽtEL7l¬ NËdžYö-/‚I‚´EBŠ/@JU¸JR€Eì{NU K\«éâý<6È#Yü½ë3"¯ÔÖo„šXfûf?½Äch‡ÌIü•…F®OÏoÄ17ü”ìqV?=^åéŒ&k8kP2±æö¸[ƒÛØì؃r™—·ÔÒ%±ÔÌú¬·<û M›ñˆž·¢/t¶î(FâÂbdm³öuM¡<0·ÁÔ˜‰:óú‚öâ£.ç7û™[úÈ×¹‘½—¸Úy”ïîê‰y⃲z ‚~3”¢†sËJÁžs¨¨#Ê%—\Ï­o0-ùX W ³…#(kôð@¦ô±£"wGµ¾ƒî<ðé' ‰¦‡?r 1UÖª¤&…P¶^Óg!jäç]\vÇ@*?‡Ã*innf©½¶BtÞõ¤²¡-yX,zˆùØp+¬¾Öÿݳýg–”僽꾥ѹµÇ9¾  3¢¸ÿA™ä…ý)ˆ÷Ê µ8¶À»ñcex•A§hYR||NÄæë3"7¬'ëCzAþöç~ÎM7¤³•Yª:ÜE¬œ´Í/] ¥ù´xîÜÁ*~+D]‚/êDÕkG­ýoöùÜüÉaü£o>HüyÂñú龸ZÜu¢Ÿ(»œ&O°l_u¢Ÿ¢‚LoR‰.B²…zFCPgYD«› ôyIjyÆ}”ªd›Ã*;¬CXY[à ç”~åÒ0àÛÀ¦Ö渾M¬IJߣR1¡v®j2i™W·i¨dt¨mà|kbÏí¬Û– o[‚m?ÈŽ£¨ ùó” éRZ¹ÚC§˜·»CuÂQ\G~–ZÞ­ð÷Ê»T¿¡ÛõÎñt˜ø¨¾•ó§;‹QqæÐP<9ÁãÊ-¼âû v‚ëÿø"yŽö-h‘é4º$º¡êü mU´õÍz#PnÓhª?(aýTµž+à˜ˆÂàF·üÍ~¨nK‘Ö-"ÊdAòµ™Á`¯ú9˜Æ³‘9’¢^íC¥f§ˆF§-ºÄÁ•…F'Ko¬‡}vò’.Ê{¿ÞH9弚‚,³XAáž +IÃ5 q ûñ ëzüøó»¿$¢OvýŸdØÿ+wv`OßI¡÷?þnýÇøñWÿæÇÿoú`åÇ¿ý÷øÃÿúñWÿî¯ÿËßþñ¿û§øÍ_ÿïßü§¿ýûøͯÿðÿ÷7øŸ¿ù÷÷üÿô‡ÿó9è÷¿ø¯¿ûÇßýí÷w¿Y‡øéäî×|ügþW¶šÑë«ìŒ´‚ªt³V. â +Œ"N¸žçíë%$4žõ—ŽRöza '9¿þ…a ¿Z“¡Ú­õúë[ù‰ã¨ˆª#(Yåáiÿ>QûÜ8‘Ý¥èLéJ5©íåü¡‚‘JǯEÖÄw‰ UtM'ùsÿ¡†¼Y¡cÅô>"hGÜÇIÌçÜIþ}ý*à?@欿×F„°¡ÝÙÀZ À Üp´ÝAÉš#(ײþ¤Êõµr*Qn WÛüÚ +ò8nÐÖ ­ù éÅìî‚]J‰(uÅà ­?èzò®0gjÚ®Ú€ÖR¾jÚžmáD¶’Ðz±X º÷‡m¡J×V(B`§­]æ úAÔbëú~àkHÐ;| pÅŠô¤tQ‚…Wà+'b¦ÝàñÁÕé:™šîßeáOÔŒ™9&$’é†z//&ÐÚ¨Œ5Òq0âÿ ÓüWé-¡i7mùx8~ü}O›ÍõMDŠŠʃ$ÛõV)ºdÛ ËJnÊþgFåý’æOxÚ¨rzµ ƽৠ—Âû—2™¹!¯mÌÍ +öãÀ¹vÊ<À=§ƒªÉŠ'µ÷Ï @ñE³Q PÏú.‚’ JðÅ#Ø©~ÙÀN_N¢Ô êÖËNS u®Ë_¡¦j3Öz†'Õ#`œ$¦n84ÏS¡vï Â¥zÐAó#(H¶@“ H+Ž™l×Ä[+ —7é¤;ÔÂJ± ®‚YµuÿǦ¦håõwÞ˜0°\î§6õl»ÔŠ_C§è*V™]iPÐ"u¢pþ£€êßò}ƒ_1PÆ£n<„4[ÚŸh̯;x|ûV–åØcÑ|ÔéÑâgÞ…€cþŽoó•o¶H^߃’ð}ìÑÖ`AötÙôWŸ^;X5úà®)awZË O2òœ[T<ί0_##’Æ«$­li)6 +nÜ´*6‚‹áW&¸¡òC”Ë·De×߯¾ ì; +üÐíÖÆzhš§ïf4²PW:ø‘ÝÞÊkЙ½n#ø"‰(eãsKd 0~[“E ¸)ÆHzŽTñÆâHkb(I…‡EòQ  (~ëà}'`B{å@c7ãKW|Òü>"Ð?G³œùh¬¹GÑlh"Çdã<ô•I»]/û}aŠ›R 0û½ïO ¹ÜÈα˜¢" °ê8嬗XL…2 è’Û7"4Y‰ĶÏóo@|×Ý™~݈.˜–¥x2³LYó M©¡bÔ£†’tHfAÕ¦|¬ AÛñ¶’ ïè›QñPî°!ƒ-Ž31]š3'RTÅõçÊ©nlqem²$ùkr¨–©3ð`m,ƺǔ÷B…dý¡kÚ=þÌ'ðëç{A÷ 7×Ñà6ªTÝŠ·]§oáïwT;”[™W¤êvǼNœ¸_oN’8ë#Fv†4i*ŽTLK¼„jÄõˆoEsCó- ƒ¦:žà îÛý(€6ÀB׺#šX^ØäÙí‘Ò$äHÚó] +pî#Ux nhÜ"š)fÖZã  ²¥~‹Æc÷\ðÖ#r¬îý›§°.¬|nÈø[ï¿J/ó¨h£ÿ$=⦦Üf“ù- tE®´º±Û£‰ÍÛ¯RA c´xøüµA…“ž ]=¯Y÷üªòÝHc}­‘ñÀŒtð#“U7ÜJ|áeãÞ&Œ4Oý©‚(ÉÞKkêfØÆ®o ¹Š{ýôÓA‘áêlWU-ø8vkw2ÏômzÛ¸hÕgÙ#Æ¥ñVÂvõëÏÈPÔiÅü[ûökcÀÀ¬·è¥ÿ°ÞBÉ ­s?0)óL¶õþw/ ûÈ9?¶½²ðW—öJæØ>~m@2rß›×ߪ2pÜA_H+!Š~¬'¼^»aåJ%Y»Èµ¬Ì”LOyx¶œkìæ'óÜ?9•@l ßÙ— qÚj· þì®G´óÞÎkm=Š5OêœÑ@ÒÅp]Ó ¸ÈyÝUÓ”÷=êûQs9ØÚÐyÖÌ/J»¨f’UäYÇ/þ~oË<Èß;¯xm¼GQoetnÎá–®ÉÛD* #èõ«©«ø7üq6… ó¾ñFêDÌRWˆ›|Ô׆k›Û2} ,þÚ‡öÀ©/×[Çœº-v›¼'s“{–ˆC­GF}½ Ùøuv'iñô| ‚ë›j}ýæ Ý$´ºmI–5ÒƒõuÍé{XéFæ$5}†á5Dô|HЖ԰S“?™Ì+—"U†³Ä·¶Å®è†i‚\ƒÇ$jM +}ÔjÛùâä£]‡aãh‚Ø̓>ìr!8˜ $ á̉\Ó´Ý’ë<x¸=>«éD¹K7JçÇÚõ&. Õ5ë”DøÑ6¢˜šÙ¡¬á,_»­¯uìÍ# +!»ŽÍ¨ÇRMšëv?¨H4+FIŸIú\TË©,(ÿå ÓšN¤õ¬hô#«2Šw„é´ä”´­¤a¹½#]žÉó_ž¦Ÿª#´3±DL'ç°‚¥°«„]DB:˜Ö“tz—Ñá[ÏT²©ý-<‡>e/úvÜX¬BNÝÚPëF¾“锇zbbh”î‘F9–c×57uåG"˜x8ÒÐp²&/&¢ .­˜ðïþø5ŤO+T žªV™9RžËåÛ :v¨bÚN=L‹MqºÌ }vjýÞA±øzèœT—%}‰wѵšöñÏ@ñQËîZdžÇög¦0v),bõǦ˷ØmiÂnAîãRš”¾[ßmäb¾¤VßíaVb¥<Âp?O”–m¦MTýO[IÑP>ÆC,¡pô%Ë´¤Ž-tºæJe;¾3_Müaü;¨˜‰±nü˾Œµ¢¯Ý#àK”îTz4D?牞¦´©œÛ\b*é9n¹çÇuŠ¾#(>çÏê†9®Š¦WÛ™'fç:®ã¨¡‰0œÏƒ8ŠsÂgD4Ÿçë9ü¼8Ú‘¨ÂÉG< ëæ05p þÇZ⺢BC¥«"ðM„ã„"†4fÖãuuý±tŽíEæ¨v­È"I“¥ìˆÑíPÉ‘›j%úù¡;«r u:\¬‡\h6ºhi˜ÝÆf¢Ú‘]ädÄ0µûAƒýþŒHµÌÄIr¥À¼EñLo×O1§+M L)b£ºÖ(4°2×€âÒÝ›2ÿŠ¸t0=X_ѱûŒðŠ¦ŽtÍÐZû&ª ž4V9º¨I›êj¤‚ ù.‚&‘fÔí_+¾÷(Ì ‰x·R±¦MÙ}!Ëv­œÍ¢…)zK©µþð¼oƒ/d½oµ´ë›Ý\ +Õãý©Ï›ß­¸Ú®U‹;(Ãr§aÒƒ €$ãxìG{F"¢'âc£ ŠvÌÙŸøŠ€nDl{×ÜÇ‚¤$#Dè6wÓ{b°+Úu¼D…‚ üÛ±Áäî]$ÐL@4Úàßç8@òniƒûŠ’G•è»ä@@Äù ˜ØSЫb€® Ϊkðâ|×7é5Ó:ö Ï/ß{Ï÷^çg@Þ;Еˆ}oü.Šg¼2Ïzzgbц󻀩ñâ|Oy*ŸQoˆ¤‚Ùä‘Ñ7Ù +t1%÷Ög-JeÜŸ¹e€~xÝ7¹ë7û¡A[J‹›šmÎ(H¶SIýˆ¸›$)ˆìÑç-JíMíQEÏø¶ÆƒÎÄÙ|Ê ÈæÓR_ ÷ßÛÃÉgÀó©ZºúøuGë‚2ÆáÿÿùgAU1}f¯§òö5ã«¢J2’¬7oߌ#YŸJ€l»ð - koƨÀwÞ#rŸ(Ò…§£,”÷ýÜ~U½?³³¾³3”]ôJÞxŠÇ:óãå÷Ú[ ,”YŒö8>ë ÓT?Úš_ÁÈMD`HDu‘ `ƒ2€»½Žtur1¼¸¶†dÆV'5Nͤ¾D\QÉ¿véjpOãíÚ‡DÜÏ¥ O,gKØ´ð,ÊR¿joMÝ7áËôEÂù„‚vjl,­ä}C² /§°ñ¤üøyÏwÓ’“.FÇ®Æ3?#.Yȼìr®Ÿ¼õç(娡³Z-0¥– H"O¥ïU õAÁÀKSdø* Ň¢K?Ÿ¤ÿØéÚ™ë Œ[蟡†ˆ¨íJcѵ]ƒf"æ `PÜ#’ 7™1-Ùó!‹oEuÞ Øì©3­l9€VÚŠ8”Ç^V[Šž+ÏCán¬ÓWÏÊ+â%E¢Þ]ôx¾òøõ{.¶aÚBqê8EèÓ ÎªÊí߀N9m[PÀŒíߣÎKÛ:p¾Pšª œiªPöƒ”báSd*—ŠÏß_oïèá³ JÛugm¡½öí9¹¶CˆêæïÃ¥ÍJ–¤ëÂS(\)'w„҅;`)Ê!óâY6A݈υÙôÈ ^*êk)¢ØDJ{æû€šp}üöéçÀR¥˜?º·½¬»¤—ÏáTŸ Ÿô)r|ú&âÛIð=J™h»|ë]ZâGÄÙA +អoLÛFš ¹Ö´VšgX)tL°õrÓèü¤»µ›´ÆSç^맶åÜ€ÑD?‰i~ÝÑóa½Ľ•pZ«k%¨€¬J“É*ô+·õ'Š>/i°´beQƒçµÁVæ12‚S~J þÊC n½Ö®9ïæbèÖ¦~õ7óÈþKÅ,+]ÈÚ_Ï߆ö{ ¶vgØΆ±7´g±ú½wè‘ +CÌ`J¶tg]òÞ¯âçùíËÜÂÚ”Ô[N›3%( %v¬±‰î xÚ"¢¢‡G{ËElí –nš}LÁIÏîÖ’;:… ™P˜óudƒÜkƒ¦tìÎÎ+ÂõCÐ]Òb‹ë]Ò*ÖƒGN©l,$FÍ·€n²)-ò–—Fx·,TÕM×gŽž[î:^¾ùjšþì½kÓ-Çyž÷ øÞ/©¢\…Íés¿ˆì0¡-ÛR¹R(€($" ¢`*ú÷y®ûéžµV÷¼Ü€ +qÙf™â~§×¬Y3=ÝÏá>€Ä·Ä,’í]l3w³L”Åžþ^Ü7 +1Ý8a?ó Lé¦ñYxjŽB¤š4¾Nt[Aáá'ôß°w=Qžòj*ª%·ÆÌ\WSÝÏ‹õ£½|¸€œ5ÈŠS "V +„€ 4Âr¬ØèœtÒéw %eéÆ^‘^£Ž“ÕßÉj~¤ +³ÌBèò”&®tŒ¼>©¨(É ò*'ªGÝ¿Ið®dÌy}SQ*¼øõ}¼ÊC׿µy2ðPë2È!C*¢ïqáMúv¢¤;)þ„Ëo·Þ~Âm¥ éBFPÇx‹HĘ„••»]PÔ”}[’È°¬8 +Öòx|Œ’…E¬SçQÅ ˜„RÕAþÜFtZ6‰€!DULµ–r)¬êKÏT‹Q ±\}Þ§ ²œý›{ÝAŽ†âXÓU€(…ÉÊWÌ{ÌÂØ×PôD#M~¢ÅÕnX¯ŽÃY¥òíÍØ#º`¥£ß ¬x¯ƒbŠPŽ—ª âQ|O¦ [ÀŒÄA: Lr½À=%ÞHÛ]-%ÀBt’Ì;–³¨¦#[‡îõª…ºHŽA—WîÄøï£rv& è/TÍ) ‹Hï>jå4÷\"õ-ñFÂmáÏ*(+ð©zØÍy4?º–}ÁÐA»_3JŶŠÜŒ€Ì›Ü«y2i÷Q¸´jq9±.¶ó¸ˆÎq:±-ˆÊª»¤õ +ªë‘kÉì.€ðx&N„…ŽIpЮŽƒµUçqp4!lÑ)5sÈÌdxëø¦è +–.¦#£Ø(Çòq=Rp@šHˆB¢t,5|’R›f±»°‡1€<Žª­´"!€õë‹TîRƒ®9UÖ“iÊ´_"¯sv}6ˆ ™óœ°)ˆž¨m;*4ØFøÓY"žý<Øi‹<¬t] b ]»oFhä1&HgEªE$ÝÑü¦+n~Åç©©»S‘°ˆ‹–@ ºdë[|9PLêX`mª•; pö`6 Iˆ‹*¯yUÀæA¢6‚-¦ î Á¡8lDwô}x Qc‘t¥jÊbÞ4X÷ÒLÒlçhµoƒ€ØgQʤöŽÛAšà<p°ÔVFpâ‹ÀAð]ª½u>ñ¶Ÿ¶ÞéjŒ"àÇî:å +òcâÏÉGèu  YºÀ.ØFà¬.¼h¯'$øùM¾¹Òëj 5KU¤ìíW`Õ´ܡв¼Ê,^ú&RRx´_aD׎/Æ@ô£xÞîTÕî¬ç'-Àˆ¦,3GEqÀ q`ƒ†“,FùfñvÑ™‰?ëÕ}eóe(v”ö“Ö_±F伋¬/² R¥Âô­s÷¤¹ørY±É¢§ª +¼:…¸Ë.«&É5 ‰Ù癯Ïî¼þä:pÑuàØìewŽ”#¶ +àÎnÔ!)6bX²»º¸– ]ˆrsôº[:Ë+WäªÅVÄÊ¥û—ýþM]Ù#Ô +€y0qñ4‚b%Rìxq3¢#—e#ŠÏâà­8ÎaQªFÈ «y2]ߤõ¸.>©b³ †qÁx¯l¦ÂA›~“F2ÂÞ;?EcjQ nˆÑA1™Ó4÷ø¶O±òØÄ¡ôFŒZuœ/ív”‚‘eÜG0Eý¶àé9ò¥}”ä*Kv¹J +ä_ÔŠÐÛ?ûÔMíC75‚©¨H¢¬×”B /´í¶(tA£VM×Lf^ …@?òKš®ÿÏ ú1*Et¬,…q XŠh˜–H† ½ A“FÑYØjì,,ó¿Ñ“‹Bì —‰JÜ;ÚÛ%†‡ƒ¶ƒ…εàŒB`Kv2R¹P댱ÉËÊáÀ×ÁSÞ° ã•®‚ȃy ¦ÍêPÔ¯1ä@˜@©J=Æ™È,ZâŒmÆÍM‚[?Ñ“bZ"N“pÕŽqº¦gÂM”ÍnSùTUºc\Óž¢_Ô÷L u€äÛö£cèjF@ÌŸÉì&…lp¶QÕ0Œ7\‡½ÒØz>€üsš°»ñ·,íX]Ÿ=¯Â•´Û?+G‰H$„å Ö%TzÚ6$XØ-žÿbóÛ +cÐ 32IÏy?økÿ*Û¦}ݤéÇ‚Àݲú®Ñ¼­bo@Ay@š¼jÙ°¼¯Ž8ÿ:ˆ6ûgu·¶7PtÀ…7eB6ò‚“$^Ì­v!ùC¨*…[ÜÓ`;OîŽ1¡‡qMuC…Áš(+m#¶MÄËë(¼œÆ`=y¼’–$—3fYvã©Ñ™ÎÌzuÜe+À´ãÀѧ»°¤$õœ. ^8z?ÇBwcÑ›sh¬Jæ«yf™hç ÛjÙã7lv;"G×m*~nbäX¸HW²:: ÚoÓù(C¶Pªô5¹ºý)³®išœ€}©¾ uká`’•54z©Š!­Ì%| Ì5Q_bu>QE9|æegêÚó×vJã”ÁÞb^ÑßQwT²MÎécUØ‹·†qq𨒊ÔVT¸Ñˆ$°²š”½ÎâaeÉÏ)]Vb"N™¡n=¤AKá~*ˆAž—ò%¨¹<àjh_T÷î"Ïï|ºÄ­w{yR&¦šBH.l’ø]Ž•ƒ¾TÑ(BA}ŽBvlH£í"µÊmÙ?¢f×QÚŸAŽF&Íp·²»»÷ çÝåDaYT#šÎsÈ\™'Xô{t[5žòM3t¶'i°•mDÙòÁD…À¨“Ö›:ìD6ÎEÉFà2Àökò¤"µ –ò)œ{€zöåhSûªE~j$'zN„Xa+­žÖ,,®û”€Y&eq5¡¦»IýãDŠÁŽnÛa–æ*&7Eæ( œZNBlÙˆà]L‘.`ýS믮oŒ“ úÆ}Z{aŸ"âôRW!Îàý‘‡Y î‹/ä‰É`¸×Äš]JÒHŸ)¯£bœ¦—FÐV¨ƒ˜ŠÔ\ÍŽ Cx¤¦FPÁT™³ðÈ覾¡hû‰Ý\]~,‹÷±%Êoer  ›áÔˆDí›ò´Ç$ AË‘ŸÃ5ŽÁÞInxŠæë›NŸaYÔÐ. elBõRE–ü÷ê¿©ó'7£Q¨@ˆAŠz +dÊŠ6ŸvæMglîŠGâ9[õ?àj'%±"!rTõH¡¸=/°àCv©îÕ‹œøÊ;EZ^[…o|‹üä-¬²i"ÇÁ(eAQì³ÌŽ Šä: 0…W0òiáÝÚ2ìM³¾Q¦Ÿln£{± +²´›'Kã^ДÃñëh]só Ôµ\æQ0ÓêÆáÔ a£ì7R࢖¥þJÏà׋½ìæ,…­^eþéêÊŒ:TÀ;°ôL ‚Q fµ0ˈ–ý•:H<®ˆ#“`&À€$Ï(¾H?ÔNÑf!.Á[º~94ð÷NÇæ_trÔ £¬7>\õgùiâøY欖:ž>Úô©ºí„¯ËåTªÅÑ5ÖÒÀŽxjŽíî 0Šˆ`Í% › ‰@Ò‚R3ÛS¸ÿ5å:´«7…0ÀùYõ&.ª Y¬˜"9;JŸ#$a”Û_u)]´/¢Û!ˆôÄX¶·­Ú2ã‘äá bàùpµ3æ¨F¡2ärE‡18A'Éø@"pŒ°µ“¼˜ pƒ·È !)ðìèö1ª—UëJ¥fÖu½`ï4@YùZ©ø×ZƒœÊÐw“Ú™-dgœ‘1Lnf^‘ž«bf 4vÇ)Ñpk1qÀ}(º.Ep± ŠÍl}E>û ßJÄDÎäéõ t™õ‚àïü H×4…án["ë±JEnƒÕÝöOÁÚCý¢¹R1Ü;55ÓÄ“”Q±ƒxos0©jEȽa*Ó;pX +«¯©ò&8Ý‹ eÁ8¨Ô˜²(óÄ'ÑZ$µéÑ{ó´g:²mÇPü/òG$æÚÝq5ä.9B)e¦¨¸F¾OÈú¢Ç]ÞȇñJpprO T¤ðe©’~Š€»Ë'`3&…üÇ.3AþžË0¼áXdÇ@ª„5%-[ɸ8hîºÜuƒMùùÀÖÍSæÇÐõ&ÑòÀYâ€ÁS£Ý¯ú.n!QÍ'. ÎÅùÖ«sCU"“M“ч€ ója© Š't“*ÔÐ)4Z ðAhv[·`Ú0ot„P7š/Õ>; Cí†i£-A¡©tî]‹× +ŒÇ)ì°ò³ww2(T‹¥¡.¹ûá¸ÿ3×hôÃœ%%p ÃÁûKDüSPa¼Ý%eŠdÉñÖ!.˜?€I±À“/·þ$=w~Ðz€ mF%íüàŽÛ·E Ò=ëy•¬dÿ1"ÓVL¤#Ü2N' •®%:•¦Ä å°€’~­«¨§¸%)Õy¢ó¨Uß$0ÚFÊÝ#VÀW ÷g·ÇÓÑCz$ýõEº-»©”2FV 2F¬Y4âŒQ#òøI„0ºú:H%§aaÃÆ +¬ÌÊ¡˜«°ý 9ÚtEx„Lá£[:rjvªt¢ìþÐs¥U¥JÑlA›? HÞUýÓJË8UŸ ËY’c"Ôéi„YTUƒº|ÅE”»esxœVT(ZàÞh9 Òì•26‡Ló°djèòŸ€j¼3ÒÛp}®¸¹øKVzë.ÉhYT¼Ð +_`nÆk+_ë=ð«*“°™úN!='©5œSS$€¥méZ4}A|5Á‡Äýº{U»3½Ú|­*·¡¥Üàéš3<_¤„ˆ¹½õo4iðýÊtfT%ilçÄ,"‰¥U¹­â¤:=YÊrøC +¾@<+\\Qo<ÃÊf‹$IžË’z}§KŠy?­$˜«—¡ ¨!-vòo:Xx_ŒŒŠ*`EþF ¤Û/©|„BèK Hb>‡<„Ëd2qÆTÈïk‰ê#Ây¿÷ú&CAÁ¬¢.íÝ÷§Œ¹³dRœ¥êåÞNµËww2È’\.ÓuLÉ|„Êð(µ!‡-á {¨± \ê`b·àLì¡ÄÁ,çí&è¡DOiX^Brd´•%ïÄ|­?Hý’xPù3YbuÌø¤Ó©Ý©ï‘Xi–ì +i§4XÝç[¬÷8²I{N‡Âqʶv-~ 2½ÁàÎ…«›Ê•ˆ»’+È?Èæ@E˜o!îjH0‹—L?ŒF#Ä‚+Á+§=¤3JtñØì—Ol.½ Í{èPÜä¤ßêÐUÃæœg£çæ ‚µä2§„80úók8uבo: ± HA±ÍKڋؽ„ÃÙ²)Ò€õsq\½$H…·­À`ûKD8lcTz ÏävZô”´$Ý ‹ Ù]öIäM°¼¤°š¤M)õÑ-8aß(ÉÈbÅ”2ǃoh NâÍQ"¡òÓ ÒÅU‡ !j\‹ËîRGtWূ +ýB³c¡äºK 9p=zELv)u(ÊGG8­¿CKlBùµ °ZÊ.&€·œ‹oÀðAÐ÷ȳdO-³7(êxÿ¥ðËáeuCŽ³ ^Ž^®ý_ûÍô²5ý\Y†{€²š\ù’G‘Á%:ð\Xzf~¡$`sÚú稥Vå 3‹k³Qÿ±•/ì‚tòi÷¡V{‚&Èò§*Œ}÷åo¢ˆÒ”S· K1pÉX`ëdn¦+•xò>¥UçY´Ô FŽÅfM%rï¼ÊJY›#`HÌIVFØ#”vßÏs§#n©q?ŠâŒãt€KN×^ÑZ<ÖÎM ábÔòÄäMïôRÖÄk®Óu'ÁfyÏP¨µçm௘¼È3L×\z&‡×+T{‡$@önS)ˆ•Û¥ç‚k4Z+ÔK +Rí'æ£Ã䶉ÝNvn' (dœ$Ä Öš¬Q_›#P,…•/ÒHËsIeõ`}æØ©)Ñ\ Ù×N+ó³ÉMÍ–! `…|à ^%ÁÁœw¾xS)Ùi·] +<¼ºM¿ˆ¶´nN-ÏëƒøäÄâiÖ¡G@þã1ÔΰCÃ&J•—'Š^¨‰XÙG’ô© çG’"ö«Åždåhùi•„ë$lü`ãÁ­B%ÇdOm¯t'ÄgZ:–Õ¥Ù°@fƒé)—¹³Ô/-3ƒŽ&¿íQÀ¤>Œ~E>‡Ô%)Ì`æ!AÃkÑ¥VgÿÉÜü xVúAÒ7 ˆ§Rå·×qA™ ‰5ôCµçå¹'!mj AN3P}è‡è  ÝS.P3ð¾¸ÌB ¤yº£¡^ZÚÏtZù§ì 8 œÄzM[èCŸg;üûÏ™÷äè☚'ÚgIa¤ ÚÝ׶CZ™Êå“„e%uÂ*ë5µÆ¾éÒÕnú Â>ëÉ°«éC ‚EÓ2éì·3¹jerÍ`ÕúD?´gˆ{¾Äe›ÖR÷™iÐcjÞÿ¡pî…ü„ btc*w¬²x§JPæý7F"I˜•üHP)G={&/ù±Í“Ôùb×Sò7¾;Ø •'!ÊC½…–›¸B‡0ÔŽé& GòDV„ŒJg•8‰Œ©X0T‘U5´ñ¤9ˆJ¿~Q’§”)†"øˆîÍg#¤Í´¾lÉç²Ã„È«d½kCÖy‚çœB÷rŒ$~—– E/Ë'…k†)K.óÜ™£ 3“FÁ`”ãsÁjÐ.Œb]BÛH™Ú#Ä!e+Äî Ì¿àb4зGÿˆÃ#ôžËd<{+ùB›àû(P|¨Ž ÷bÌmzÉÉ’4驠䢴oxQÓ1ÚsÀ,zÚ‘$G]é“ò‰²++ë9°cä« ² 3K\4 j–=±*ô~÷ˆBp"›·ß§N!• 0khòßáŽîó.PœÉ¦²%&"J¸j2neWJúö ÙÒ\§  2NXX…¤6oã§G-À x¦P’–‹ñé ;‹+é绲ódp2肶Žt `{ž¼þ‚ÞNÈöf ‚:P’w!¢ü’YÃ;”ìú(ŽÝ²ZÈKË2 +«‡ËyÇWMÒ–‡®=J˜SHå*õ¡2Q¤}(=¶,Q#Üæ6唑ëè2ãäë¯rQFxg¶Íñ­‘¢LA;Û­da‡ìÄ@êbIl¤éEeÙuTB[Þ)ßÓÀov3IËr‘ #ô³+ôb”sö±´æÇfwõ‘B9Àå [b”Œ°òÞõ!¹ÐèÒ<ÕîˆTçù•^æ@ϱé†ùÌñf&JÙ¿XHKHÈÙ ±ÁÔJ~†4ßg¡ªA>—•Øæ2Q&XWK`]#á "ûÞ+ŠE*Ä°þ¥.€C.Ö³{¡0 ¬ þP‚â$°r쵇›KYUÚÏöUÂt ¦B‰½â ¿ K£ð¾^€_Âd¡D ²ÍÚ$˜Hÿ&jjƒ°ØDs´ÐzFó’ˆXQÙ(ü¶¨¡*ïFªmÄ+!ÆUÙÃ9gz^Šò-¿©¨äSñÌN‚ª[„€ÚAYDÛ5è@|ÀÎ&7€|8ö¡‹—;f’#FXÖ±ØòxC)j³ž”à ZÝÂ)äѬëZ +pœ›ƒ=jE-ÓãæÅ×€ÞøIÑ ‘hVˆÛYâpcf®úHPŠuø9êÆ2alQŠR(.4„”ªœ7¢(w×vÁÿÚ4ãKXÜ7YO™–|uÁ†®zôt²îPß9¯“>mõ!>œ_à;‹­ÍDâûUçûIn›OÑ~±w2m0=ärz:#úCíX5ÿCÔ9B˜*‘²° ê_ÅâA(’%Ýã*ä,³k/Tª–,$¹ =ù* í.~-¬Ew[Æv·ÛE¨NC°xéšõŠEÑI<ýIy§ª˜á†~–„·èÅÄ—9ŽSoÌ‚¶’ÏuL²ÓÜåäö&-Œjë p¦f;!ˆü;“SÙÎ|ÅŒô²ù(ÚÿÃÕ5§k3”TŠ­6Ï‘‹S¡¡™ +$?a:_(%°^‰$æ¬c +—¢kHûD! Æã)¨g–¾øKô"+ Ïæ²jnÒÉN§/ ’ѬڲªWm°£ +à é,Wä4” ºt‡›z÷²¦{üž¥ŠK‹®*;øߧC’âôÀ0(ÄÉ_nª +]Ù)ÜÎ(à8ŒŠZŽñ€YU¢ËÆBG¾`Èç#„¤«SÒ– N¿ø7 p}½"äh–]uõA]Lâš%(+ÎêpleZgpŒ:Ä5B[º¼áñF¯ª û75xŒR©ÇÖ=5Ã>€Á.A¯Cæhv‘F)èÓa|Y§Cs1“I€a£–ã("@–lÅÕínQúèl®ŒJ4˜¡Ë6ºò.‡0®q ”SC±ÏIàÅñJS `‘ìFÚ&$A}³nH]©©H¢Í‰ÝR˜Žñ¡Ð/HÃ0Ü »œÀá0 I R˜5øÈUO[¨Vî¨RÇ.œ´œš«h,NJ×7 ŽÅZ,vOU]Ž"PÒ· Þ9a*iQ; ¡Ížæy¸q>Ðm„ÚÌŠ0€µ} ¥Aø‰¬Œf5Šµƒ‡ H^†Âל‰J#X¤Øªõ-7i@8ócBo¯S{Bx# \ÅSŠ)Ü4–ÁLWCU6ˆ¬I©ûœTëÖ…¥ƒL˜ÝIE’†õ|*èg r‹¤K>ØJ¡H„$ÎzºÚÑ3ÉM6ÜÉÆŸzKÓxÿ *e­F’µ~å൶›•<°«Â½dœæywÂP®`þÜ房ƒì ^öŒUáb·åR;ÝŠƒî¤ °ÔØnú¸Jøl=»ß‘–V#ü‹››Êj„8s zT¸¾@pÙw4:†Ž,}Ÿ g9ùß±’åÓà4€B'Á/ä4Â%ò )Ãj±>¼&© ¡žÉ@¹’.²§,9.ˆ·CÓ® AP$Y(wp2g³AõU ²Ž2!,ì:µÜŠA„ +[Ô/q¸ xs寠ŒÕjê„ȺÉl`sÁ?†¶p²Ù¢²N¢=™»fŠ×ÂèTô"×@HÚ~š¶FJ%j.’øªjdÓhИ}›‹f»Û¯æ„ ÒÂ%—l°Ä¢»F ÄjèBz•P¦øyÄ…×´t5ëîÜÌeÒÀb\NŒ£s@5¬‚”«M•ú¯˜“G‰Ž%á'd^~Ò“L£¿h8r-äYÔqQ;ìþÂ@ãÏÀ÷0…–„*í^ž<óT¥ÀïÆʲ ë— +ovë ¨EÐÆdYbBÙ .ž(gœd\mTµLÛiphPÀ¨¤¯PáݸUð€:J‚aƒç¡4¯ÒbcØ=ãêÕëk²œâT®ÞR®R§d3`±p^!ú#ø—©K¨7tê 5òj8¯MU5Öóuwä ý«‘þŠT“ä®Ôpp|s¹ààyáÄk`õA {h@ò8öª¡Å—äs"ÎѦ59¥6 Á×\¥H˜%J‡cÆ͘:|v•§$Ð$~GFý!ᢢDE…)YãKep‰Ø$ÕÜ;†&”ü½«íN8MËÇFþçpþuÇœò@…W!.Ýs<ê Íòà-Ä€”9öêê9i6×·wáG?%Æé_½k ú#O´9sú‚þsç‘)Î :‹pf0¹¡Ö¢ö<¾Yörÿn ÒRd‚ +ö:H„Ïa¤J´¦ª¤ ÚÏí@õJŽ§=Rgâ&CìB÷qŒ*Z\ƒ`=ï ö…@mKR– wZ1Ð)gçSÞj/ØuÑ1Â[ŸÍNÂyÃíIâ˜à)’F±Ó3 +I'eÁhlíìͨÂR„ªTZ§–ZqqD¤#(„‹½¼»¾#JQ¼ûöGMMærÄh*×Ú}ˆCßQõ“º^õŸT=;•«ȃ /̱Ï"L¬ +äá¡‘K¢ÃÛ•=ÛKJöl‹8t;ƒ ÊéÎ@F| +Áa·ò—D{JÙKTjªî—}1{OòØ'‚+¬.ð¬EÛ··*Šû»ãüzÐaƒ¸©‰Áè·,‘d "û¤ +›´^5;)ÿÁ!@M–9…WÞ)kÅ÷/ê]’v¶ê+ÒµõxH/®C÷Q¸OÓV}2dQÜu@ÒðÅ_FH^Ò|—¹ Þ¡&úú禊ïþþKlÆJt˜ >&ä{·¦wüFQRí—âÛcv ¬<Öà×—ª-Ö§°E# ·¦½/cìÍP ã9vZ†ì1‹µå}€o5Î,PLÖú±$NñZ½Uê„ÎiûLÍLÔÃv3‚ `[BŽ4(Y7£Š¶iz¼R/´ì¢j›¨Tëè«HüÒƒ% ± 9É•Éø ô±£NÓŸ>\f˜GiîÅCîIÅÿ:6gù¨ûãÉ7cƳ„´n³Âk?ƒîÊJnÏCJ§2*åñPLU¥›V0t ]Ì^E°àñLÀ’§¸žÀ~¢Í¦ÓÛFîÀÿH‰êæ[ĽpV]˜ˆ)Ã,ðú£ñåô•¼UÀ *¯ËUÈ)ße-`/?Áv:Nð%Ê +<¹ÈÂ0 :1P—ŠGÚA£!ÈŠS˜YŠ†ji»q¡v3dã ‡ ¶ŒR¥ŸïªõUâê5ÉtSrñ˜øI´A)ßž[,òM’f°Ë¿IBå6BH•ùM A– ±rFŽœ–l +±¬?y¼{íÒ®.3ˆiÜcƒhy웂­€ Àhõ(òZÅ4j£4»›“ÒSQIˆÜ/9~¢h›e'ç²(2v‚³¥äªårTÉñªQ2¶G"|.ycv?­ ì€2·–o)ñ)™¾.FO¢;™ÜQ +é¡:ÔÔÜLž&Ã&”½´C²nÈrŒ­–Óè7ä¹õ bA#Īw–5!ÎTãüYíqÏ(KÛLÿ¦alƒ“ÔjA¶â‹káVEB#û°'šl8¬5ÐtšÜEÞ™ÌV¥ÓìAß0ë»È·‘J#>ö^|öS¾‹þÉ/ü#´Æ…΢…B é-Œ€¬a¾ÛŒÂÍ“Q@ÆUä«Ãyx7HTP‹qXQrX‘üe‘J©Óa>MÒRÿñø €8€GB…A_ÀÄ +˜T))  ‘HW1Š¸¹ °Ò +!ïç@­ƒo:®Fisl—*²åaU +½Þ„} ‘PaáíWÛâÊ·˜¼c¢ì 6¤1€¬#<)*£Ý™¢4å¶Qä8Yx<ŒÑ7™FI¼‰píŠ0ƒ¨nÇ­šFêLÔaË,sgˆ‹wœ›*å.*‘gv?­ yè3B»»ÀQ#\þí±äâRØcD~ö€9Ü¿¥S–ćÄð·>ü<žNÙ3aãÖy ؈F×W# "Òü s «%c# Ó „N—·—S„¤;e<ˆrj–×Ç!ÑíÞå­³¸dotIDe„^Ë8éÕIE“Ô`[DíL<"Çæè*Ïà`—e®XG¯ùâ®{Ž1÷œBÞ>bÐíº*´ÓFhu ¼«ä¸ù…b4ܳ&ù±<1Ò³pû- ±ú¾ðµjø~{DB6n§¾ ŠS¡(UÕޣι+‹-XÙ)~u3 +X’æþk’sÀ€zTËþ¾©ãÙ*ž.†(ZHÔg¤C]ª«Pó Mû&¡m€Ÿ Upóñà ¸("вº™ ÕGÑ+ÉCW.öQÝ%²Ÿó²¸—ÕEŸ +¦öaÊ·À¬‚@T/ —WuQR©·Æ‰^à³Y“…è†S¬-ÙÑf¥Ò¹ZmÕš&Ø/QÒ)x©|x*E0éÅ¢j'M·B2ë%›5Y *ýx<€’ORñ6ðUà° :„™‚àê¨*¬(d1n#äáB°¾â/«¾(H]7^ÎK‚2ѶÊ„8’F¡[Ìå„êߤRZe÷HNVë9¸¨spC”fU€8ĈÚôèÀ’8Ï7° èѦi(|+t©“\(ùVÇL—ELðöñ é§Lò”á°¼];XVüQŒ|ëÚ†šƒX­öfnw‰5#O£ ‹lþœ4oYè†ïPM)o)F~>›2ôæ}ÒrGï$ È[ ¹äRôO ë8O'ƒ¡º9-À€Ô'þ[(ïÂ{WT¨8o<Ð…hí"4±ù»Üv'°p:?¿X +?nAÚzSãpÅ»WÔ↜îØM³<(Ä´@!:ë'u¿ƒáÔ+êtŽc’BmU<Œ}UËÊ;¢ƒzÁ)þâ)w_ĪƒYWò8 W¯^÷·iBç8(v [ä/^}ª T?Q;+¥.NÚäðæ·­æ#@¹K>!â e¨ –øJÏ&pV@ Î&É“dÿæ:E3£ð¤ƒB’8Ÿí(!ö\’l(&  #ØÈø§zc"±_#@duap NÑ)80$A !DÎ WD9ˆ™.òŠ ½eã8QP 1Oï,¤8”{(;ÙNOá3 hÍË®è2BZÖcˆsgÔ…„uL8Ôáü2áãOì:Ê燜â_:‰€ÈŽ§‚è ò8ÝefòìÅpà —EW †ƒô4tV7B_s +b)X“èÞŒèÑ&ötŸ{^Iݼ us”úÔÇUÏ£ ûâi¾.¶5-Ó¯ÇdÆœ‚^% ‹¨3Ì–Øê æ°?h³ê§HYÂÞT%°€JcžcV*]5™¨ôôw¡pséºë¢{¡•žËD…kS«MDòp |Êp:½ÎRñkWƒ·&k(|õê,b!0Öh'¹“ħAÃË=ezäâ$\‰÷Nö{ Ä„uí¢.ú8p:XS¸ˆáNeKâȸ¥”$il-µ½'˜vøº°êØé<;“]’Þ%d,;ï¤f€']hHq‚Z@C%”®0_úÛÝ“äãæ ëc_͇𥳐Í×ä8à_Ò6·t…Áןäj†ÙåRÞtjÝ62˜c’¤áêæÅÕÍg6¤6EðO%EåPaš>E£Ïåwyfì@ù»¨áüÝÍ92ˆl?Y£¶]\Xwèc’KõE’®Ø¸&«gé>åü@ôyäÞeXþm©À*€*éâ®bøã0@öd#7ÊŸƒHŒ"Ì 8G¨“­ý° À}.äCÁ§"Ü/¡Ñøâ‚]¬+ WzŽ@3ºë:p d™–DÜ0tà$8 ’`Ùƒ!ßçé$²ÂAŠ¨4E¦{l!ù&:Ô ¿d**}þŠ4’JÚf£Z àËx% ÄÀŠ X+K¬ Û.Çx±aÇHž½C„öÓ=v‡ž‘´PU·Ä>½‰î +¿çÿœŠ#uPZvvТ,¸ÈÄe\„òŠ +Æb‚FBßËPÓíiâ¡|ækŒøôzØÒMƒXëFî‡ßV…£¦8ƒÐÃ/`=ðål<@÷!ƒ9%O»Œ‚ßçLBÌÑ$ƒÞœ±Œ\CÄ9yq·lï£*X…(VzÆó,Ä!`.®“ä*ðIAXÛr@ î€"X{ÌGeÃ5<™€¦azŠ¬ƒ®.Ìß®¾DŠ²œ°§Öd'–ÑЊâœ²¬2àÚܲGòë™Ãc7S…BÄœ¨r¤Ênˆ­y=ð‡‘{Bï½›ËK‹&ƒSTðiñ“H6À«x³T]âi"@fæõÐéÜ’PƒÂDrª¢i§â†^F#š(y~Q#â4ø1#Ây¹-ÈÖqA¥»‹’xiÕtãäjا{b#Vô*&UФ~ +É~ÁÞ¼èš.\-fôRû·vˆËêNb%"Šwpd&Ey2Àœ÷Z‘¨Þ:[ŸÊ ðæû°7DeP.†É ™âÇŠ.ÒÀ¨Íul0Ý/œÖ Aýëd]QtzªHº5ÞêŠú4Õ³_ç¡$(ò‰ŒtžÓ]=£:–çÀŸœ.ʈòôŠœ`OªYqA‘ž-ÑÂÝŒ+ ôyzJúö7¾`º  †Ä,Vá,›1Õ±¦bEË£é=c}+òcq¯ØZ,Fâ29ŤV³ÄR)?nç)ÉÙ5ÌqñÂs±äÖ>àÕ8öšDÏc$ö¯Ú +@2ï¾².Ñ€úá´2r=bb( ·\ý(µÇóëlluÐeÇ1AÑÝHŽJ´Px¶AÅšx;þ¦Y¦ZÇA¢Õ} +»]„LÚ}¯ð ¸ øt5*‰Û‹QÃu½Úi*j¶M?V‰/—&2íI=Pî¶Ê y!5¼¤žó>ÂïûˆÔ¶×ÉðÛ›ˆfÙËZª€ÙGè¹®³¿ +h{ê’‘AŽëÌ~uˆ¡ÉôGyW7bqáBJ,lœ±î£"ÿÄb}>8ä";‡{§©’ŒeÃP–>ð# ‚—'»œTõ ‘Uh"s¨{r‹€º¾ÐØE‚QÄG ÐSa‰¿H[ÅEe6$DŒ4›˜ÁÄ7ÄJa‚LÉ…ñM¶·§i;,®4zpÅ=÷?]h„-øyb .½>U€öÚþj.,µ´ü8¿6Ñ*~°˜Î‹'VûÁáÆÕ®äJ|àƒQP2ešÿŒHR½DúîG¼ÌUé ZÚh£øâÌv"!d„IEÇZê<ЂŒø| #í?³Ðè -# J²KGGå 4·9å#":£ MX³Žð©û +¬`PˆÜÎ^œÀý¦ ø%ô›ÔÓŽôÓeÌ¢š$/Kô…wAu´ñÆVÄcÞÑÛc:ÐÅÞ\K„\‡í® Ÿ²ŸÅÉÜàNw8„Ö1PÝÑ( c¤;m‘x“q×í’x +ß°oW¢›ï£Lů­„0[E2Ó: Ï$}B•#ÈŠ$¼Œ¸@r»ÁQa9Å@=ü:Ýt„*¹49ëe!å¨i„ð žæø¥PççesMâ®o‚¶©'{~~j °qH!ßGîLî~ …ñËë÷A2$´A(YRÒæS0B„©R+ÉM)²´öÛ̎Ѹ–¬(v•©î#|J í·íã ?@ËØÎxR¸}ìš}z“X¾.Cil#ëpùè,OO}xš0Ï"(Dç;Û«ª<íî7l§ƒó© »P *€L€Šó¢ÛÓ‡}*çQxTx=†¼ŸÄ(N9ôÓá±¢Ka™‰M E.h«c@óöà/’C[šDoN†2  a|‡vÁ­’›“&‡îç1aÃë(ˆUêÅ"戨)*æDÀ×›ð*Œˆqk¿èô;E1a­mIéäËBWßœ& ’ÛÑA²©Î²r„lÒú¬~~ta5‚š”¸Îr8®/rÛZúuØ +@jÙp]JÒU,TŒËNðÏ}»C&3ó Èlj³WòpÅfWÝ–ódsÆ®Wdp…kÍsªG½­$¡m¦§ùMë(f—FA’‡ÍÀ°R²èR“LCK;¡¦Õ„(òd‡¹ì#¼àÐ=ʃŽªÑ6 +P˜keP\†Ôå²Í;…X“£Ï`¿ÉÄF÷ÅŸpÆ[fÒìÛÀá8¦ËGÅ>FY”þëq=§{y6R±¤6_„'TU¾¤Ô£9x¨Å¡B‘þŽ(%ÉÃæZ®Oõ!f8óö¡ %ˆ.‡»g„/íÊ¥µÿ +I¸&Äý%ù–_”¼T**›{~;8ËË"/ìxƈÓG°m;¨Î¹Â=FQ2Ä ÉÑe7Щr‹ŽivolP„ý{ûù/õç_|ÿý׿ÿöó_ýÃçöÅ7ß~þÙwÿOŸ÷7ŸÿéWß|ÿoÿÝþ{ÿÒûOüæë¿ÿú‹ï¿þêsûŠ—‹;?zÇÛ/~óõ/'ä+¿ûýçÿó7¿ûüÏ¿þý—_ûýçÿÛ×ÿäçÍo¿øÕ·ßߌæÿ|ñ×÷µ_ø¯¿ûòñ™òÞgþü÷_ÿᛯÿÑ>òwÿðîÏcÿ⻿ÿÜnË¿ùýwߎŸHÝs˜°:Ðï½òßØeýÇo¾úþo¯þñ±ÿË×ßüöo¿ÿØýúë¿ù~½¤ws3íÎ|óõ?üˆûò›ïþñnçŸþìúå¯ÒçúíWã“þïO¿þí7ߎ¿üë·Ÿÿûo¿ýâw_õ6þòÿäg7ó*x?ª„UøÀP).¥‚ +K@¹ix_@X±üÓÒÍõ_ê¿ÿòæÔóÉ:¯oÄÿjÿóÿ²?þ£åëoööŸþÏãí+ÿàot/—°~“`x½mŸÛ¨ùs~}÷Eôàíù¿ýÁ¯òxšŸ~ñåÿm«ˆO­ß|ýå÷]ÿxû?øo¶-³= Éú†-êvÙ~Ë™3`Bm«/楔±s÷AÒ‘éüj*@ŒDm¬P©›(±yË%\@n§ÐÉmÇ´ˆÁ}ÆÉ+åB +Ö aèùZH—ÙAd¼+ß^¸›tÄÆN¤#¨·ÿ Ó¤áÕTp-r‰&hd¤eH‰¾½{×þjlQôÓ5 ›Æ #?ÄÂ)Ü“u{0 ˆn+’^)zšlÑC{[dŽœHù#b)0¿%r\Tœ8½M¡\”"®<²¥;æº/1¸º èj¿š Ÿ{øò! t{,zŠÈ‘êz-Ÿ¶«y® ¨ãž´;)±x!]œI&5m|Q!þ’wn첓¯®zž‰ÀÄæÊÙµÖa8Ü1ÉoÊ>Yz€–æ%)økÄ3Sp?æ:Ã&eÓÛÓ7Í¨È +‚ø°9íÈ ˜ÐTJOù2Ì¡¸ +Â{×äö»dK¨jcŒºéUö€MGe' ï*à\|seì+r‘<ÀTó€|×+tòäP)ïcU\ŠÊ¬o–¸;“V'ƒG™ ˜ û±4±™'ˆÐœÝ5¿r“½-`'À +;%º$RrI¤Ä +5œ¡? +É]<•Ûâ@LÖT¤£œø±ªºL°`0G0€"˜î"ˆH9C\\Zwƒ”ã6ù.%á"'á,îSB}ʾ8Ÿ=½auô+d«T¦±ãÍÂˉWðº¸d©àIØ]íÎä<¨iÅ1ÐH“(×pV9ú8ÕίÄAÎÞd­rwgŠ @äF €šX]ôX}U$€Y¤†-·ò$&_•"ºÄ áOg²•9‚Ê<«lòñj‘œ£Ý +í-ñˆÑ^‡;†ÄEœ® ér¬%ÕT†X×ï¨jM} @ÍÐho-dÛ$¥1U‡B§`yy[qÕ5§õÍK—;['^@ýÒüZBT;›d% {Þ)dð¥ž7íÌã›Dÿ¤*‹WT—1]NÈ´cqâàBl7c+Š¨>ˆZ¢I€ c¡"nËL +J tmFƒ¦Ä®/ Ú R,{IE`fU)œ[ò‰½Æê‘C¾åªf½ˆÜ¥²ŽBÐØÆ@Ë zK¡©·ô€l«JRÈ£…¡z‰Híõǃý7"è,,Ëñ‹8V­ìü—ð¨E G¥©¥¨7W›*³€®œ^tuÓXšôs:=M·*Ií=Û™Ãø%²È”zœmDÆóàWš:2{‚Ÿ¼B‰Wñ»ÐÆ»ûöî É|ú׊®Øó·¿ÿâ«o,«xKåOÞ>ÁûJ^/3âÊ.d3‚Žî›ÛÿP’V,ù‰LoPH„ÊXàM ÉesãíÓßÚw|“RôæjÞ–•}B%»!õ,Ê5¥M5l!ƒQþüDÅhJ‘6 0U³ÏD¹]S·qyj$¨íE·)\¸9ßþ’uæ“Åá={ô0•ìú"N¢N{X9ÿºÈ–ñÃF÷’õ7"+Öq^ŽñíSÖšO‚Šª°0ì‡Ù{ø Íi™$¼…râúm§#§›Bēߎ¦S0͘‰|úþ¶Oð‚ÌÕ@ + {†#’#¶‘X høÓ™ØoŸ~zZÿâùßo¿øß¿ûÞ¢èï~ÿ•M%OºþèÜP¦÷g_X&ÿÿØà·ŸöË_ýÛ1ÿâo¾ûýïüЈÉ-|ÿ껿þúó_þê$—þwßÿ“Åì¯_ÿài¾æwü æ÷üoMæq?5‰z…‘^1¢Ò<Ç™WvnÌLXÁ" )0Ü»«kÛ‹Z“³GBQŒ‰Èâ­RK@Â{ =Ûmµ…%hGQˆ™°´¢ì2j(ÃJˆÒñªÚß*;ñHr¡ +glïöõjƱ5ìjúÀ›nŸÂa÷žZ‡CI®sÊ8X·u¨Q]9’ Åâ¼N'¬.¢˜[qá"€Q!¾bù ¨79Úo» ¡AÅ>ümýÇÓåHïÚ ttß¹ Rr! µy“´ŠPª0˜ÇÉEÛ~:MŒAã}` uÄ €jˆD<·Ý~œî¥PF“Í÷ç + ‘XÌ^ ¯ÔãhîÙå»K#þEÈõ„0¢}³ZgR' Ö²~)’·P‹êuWø›úPu .=ð¨pà²ÛXÆÓ`Ç4 I30}ůÈîëbÀNaƒùÅ•æ¸Pëh´ÁÚV?»èçIÇ¥‚:eó +*à]Âؾ“¾¥NvÅÝ@ˆ…&‰gZÑ¡f;:\Û ãZtCÿÏvGÂvl¡Izpœ®9ÙˆÄ觜íi¦ìœ­$bdå²Á¤øéP,'ÌQuR9NÓ5°¸þƒLn¥ÜQ¥ ² %Ò¡NQ³µ +Ã=Œ¸’i’ˆÒ£2rE3û'ª]¬„Ýìm ˆF˜¿—ëÏz¸qT°¥ã³öõ™î74#s’t·³Í›ýE^²~ºõ §½Uò(DܸÉ^ÌNþÊY\M83­§ëÐ'lÊx5OW}µ_w .„ º-ÛZa˜Hl‘stÎ\uiz`q„Øòä Y*ädÎ`—d[nYc1ù3&ˆ­¤T¢y•±£lžä7@þ/ Ù*x’+ø#'ð&öZÏA#ûß’€© –¯3‘$ "RòÊè}Òì!݆ a“ÁóᎠqhËrVÈk“ ò˜‚7äõRE?«*ì +€v‘…‰S'¤†ëÌâíÛ“á%A²I/#a¬*÷xû6A!œå¸ÈMˢߩOH9s²”? ÈΊxqkœ5°Š£síRÅBh9r’èfO¦Ò´fš×Ó1É,ëMŠ3¨LµOäòØågE3“×R;ê»è[ðp+ øîlE¸¬Ê +¼JUB«ùÙšÛq¢f`_ø†º}á3‡{D CBs:ªiç÷KP›ß™4˜5j°»j>$æGù.€/¥å\m›¤hŸ¾!³Ï¥°Úhÿ´É”NÉÉA¶ (jº+4×uå@]â‘<´D‡›¢YÑ»MªÌÙQ{ S€ßÆŽ&OØ‚TW•oì;À}wÈ‹Tòt±4¢¸XÀK¢¸Ï÷šˆ^à€jAžR&FÔúâ c2´f«¤ OÝÐVË3=fDA+hx†>ŠŒb»8«æá³¬Ç ½Ðí·ÉK%+sÆÄwÞ~lÁ2Q0¨%ƒ„(I=ÒH?\[^öOóh¿½E¢]ül¬IôÓÀHÌo…zøûüÙ(4K—Ü2kfŸ‚½Á×QxìYAÀRà÷äý³i˼ô«-âæfzÄ-¸³;Óé¦8Œ‹ +Þ”C’Š×pX«xu Å:÷Ó¯~¾¤íàü!…"L¢BÜ%/VýòÒ‡y +†< t^o×x¯ +Û\7¹E‡ȧáéaPÿ…ÞSd>ýôü +@s5äûå¿"H5RûOk¨¨ƒÌÁ±àäU (蕹Bå”pýlÍ?äîr­ê°<‡îA”ÐÜ +H^e^‚ 8ø¹yð­j¡‰TuàiË)aP*mïÆUàÚ¨(ß‹³e{„…WéÚÚŠ€óÔÓ‚â—}°H#.ªèß!aͳ’ª^ øFDhßm¹_ìx^`¶_öèBF‹ì¢¤µã;{™Ñ;6‡Mr PF §¨]î8øÓl^±Ö¡{´Q ¶a³P‘8@+„(Ð5d0x¸P$v¿+°)Ò-òdªûg(äWÝüªí{‰›˜‡²‹´x7„|[ö:»†KÁyŒ?ŒèŒãA^xá¡Ÿcº‚Š€Ø*XO¶~E‚…Œ"ˆ "\±ãvà9âÜÎ8•=õr<ì´RÍÈ–uG¹¨t8DÏa0—.mðAhEЙçÀZ¶þó)Ÿ›q;o©Œ²‹ÀõHŸû†ápÇeŽ]å9?à¤áf÷ ç´Ç§ VEY2àiùÈB +©¿¾“Ä9b³BÚpå@t;4“($©™©RaÖðxíB …狧®_‘ÄÓy¥eÜöŽá¶Ýö*ÍLäÔ3ÑÆ!&Q¹Ï¹7Ñ)Ò +¸’Ç"³ze™67ÝbësœÏ¹©EKîð=ü”™Ñò) I ›2p¤À¼M +cª´ƒîóç2bø™uÓºlp×mÅÍ`¤éP‡ñ™¶åBzÉímsWÐ¥­0ê4-\ ‰ {‚¼x²àÿ¤ôÿTwC]”ŒÙë,RÐ)qÝxñQÕ`õ¶‡0 @Öé¹"5>²íhØ:β‰ˆÚ¡ŒhKЬ³øng™¾v»^^Š34J‘žfÇCIý*éÐúôNµ8eé*±Tɨ§hàÂQ™6ÂЛm.³ÜÄÉp%àd(¿^*š +ý-@,(j<—µ¶OÍb˜Ô…@³ ÉßY>ã¾±›=×Ú¸_ì:³0§Fª¦¤-}àE·š^QS„wƒ‡N“z­ úÕ5·q®¼zîñ+²Õ@ê’º|‚•˜ÅäÖÝå‹ÀÕÙc·ë Èg±a>& ZxJ$<$Â(q:©Š¡žRœ?’‚D±~tñˆê,zuQ²’€} »œ:ßn?Þߢªæ#b;ŸÇÅc‡p +ÖG¼Nç8Wuu²n Î5Ü–³´ÏÆ-‰¡† ÀůŠÕNOkFúGjl* ˆòàÖÒšÍð]fw®ŠýæSd‚ñþlæâ¢+ª +5º<蚆S†Õ‚^ÃKÍ£É7!VêhB­b‡¼í߉jP@¶Krë/À§7 ŸÞcêmwBrù`d$è}sCcý`ñr“2ij¿RT¶< :Tt2Iþì/îëóúDù¢r—óÍÌà'%ü4›¤šÇÌXgX¢uÃgŽîåô29ƒ0‡Ðà`ßÍm Æ!`Ü9ØnÛ«!“Gè²2ÒEl}»b–ÐVáí/©Üåyv+fpÙ“›GJ®•Rù\è¾ÙD€»Ð…åk jÍö‚S˜ßø˜å/+¨¤2þ qL–Å‹¾Ÿ4"!al¿­}4'_!A[I÷‹ ýv•Ûb+ý@Píò¯{ðºRÓ>·DYWû–ü@¸¼,ó¢ìGÝÕ£|Ûv:1’#.P°Ž°o0P2|%/»’Ýôð.Oâ&=Èu?£Y\Õº ¢³h¿+ËSKiÛAáOðNÁŸè.’»l¾ÒoîÆbïXÜ÷n@丣j +ø³WxÙú%|ûˆùoKÌ€-KOy3èÖ‹=sJÑ­A +7Y“Í3ÀOJUaæ +%0aŒC­Ï–8Š—±x‹Ù–Iî‚Ѧ§°ióæÒM—Þk¶ ŸC4`Æ}z‹:Ù0m¯äAôs ¨þ8¾Ylr¾-Þ¤p[f˜¹D©Zût~VxEøK€+3X9•T1–=>æ'R‹ä'W:¾†×@NÝPb|ÊÑkd.dÞô-°W%õE0î}Ü¡5/éq• ¾ómK)‚<¤3„o`ãuÏH¶_Ž˜ÿ%¡ÙÏ3R p.‰ÌÀXÀÇ*µÁÓSÆö’8}a[sökþšŒí#Ö\ŽÅM ·æp-ªk*¤i„½ÎIïç&Å„ú'“Àÿ”=¶èÄ'$VšóÊn“Q^IG}Ù;”u§—ìWxÊvH|×kòÌò$ÆÒmŸkî-mí2´µQöÞ²vF¢‚úÕ©jIúYèŵ´'†äÍV2 ÉŒ µÎ®ÕÀRî‰äxJ^j-VÅ­>ÜÙ‡‡öVêÀ7IáÌZ ÒtC#ý5…3k}…õXе"cO C¶Õ“¬h-è0‚•ƒù˜bZk=Hœ=O^êGˆ¥à°‰¹VU*³–Ÿ¤ª,L(>ƒw½U¯DÊ.£õRïâ€ð‘h²F•ª–r#Ž–û"Å0¾`©¶É·Há¼EÕ‡¶Bí…†~@Í­*Æ^Ê|6P ÎÏ®ˆò¹6†¤I0¹¾]ÕDl:µVø]w5HUJèèÜHQ·æáN8[ÑSâí§,e’þÕZ2•òÎQ¤«Óž+]/W¨upˆ¡ÖEåòK­ö˜«gÙÍPýc­ô +š\ܽ„{-ubNƒª6¸¡CÚîk‰Ù'ƒ ùt=«µB­K)~)Kñ:úR߇jñ“‡ø¶•Æ¡ÒªÆµÔFFAÐìþ ©©­(ÓŒ‰²­¥|2ÁôÙÕ—µ »Xâ{%ŽùC–&‚ØŸšZ-÷mï>´1¯³ÚS[ãè/ÑÐß¿Ë«dKÛ½ñ‚íenÔ<ì׬n»ÜÖ> fÞša¶>§4×íµK#£¥2µ¸i +ñ–"'¢6òûV #‹ï}Nˆ}Ôúƒ×¶VÝ´1m€Ýûsïfa(Š2 +l ÿÇ/ZšaëiöÛz!ûˆ»´ÚnÌÚüÛoîÚ;|ç!©á8ŸìÖ˜Ü&ÅlfÞΩÙø\æã莮óxë¨Þ½ [?v{¥ÖNîöVnàwÞî×6ò\¶¶ó¶¬l]ëÛåikzoËÜÖ/ß–ÊÑd¿[g·Æü¶\o=ýmÅß ·;dž(X7 ‹0¶®Áp·óíˆuÝ¡ëæ»/ž÷î®±nýÒc 6 Èm²áL¶pf"Sf´ÁXn㧠³Åa~fDpèæ6þÛ0;[9Q>3òÜ A[àúÙ hƽ i ›ÿ ËZPL[ø=qO[価¥î€ jµåJkËE6×mN³aĶÜhƒ—mùÕ>â.OÛG-éÞDÃm‰â +¡»Í7_Áw[Òºaö~Ȉ›¼ùfÔ’vo`Ã-c_Š·yÿsÜϲ"$·ÄŽ´¼«e8:sÖ?6çV:Ù0 ·%˜ Bº•r6ôéVºÀ«OE¤ éºÕ 6ìVÆÚ0¶·U° ¢»UÓ6pïV‘ÛAÂw•½ c¼Ö7pò,(næ»r䈞ÅÌ B½UA'ðú¶ˆº¢µ·Zì„woeÜ ~W Þå³”¼Bз"ô`*ao¨÷­¾æ·"ú†·¿-Æopý­¤¿!ýg7`ãÜ6ö¯M‰™°566bÃmƒdãElý•[±¶fvŽÆ]‹§ë"êg9@òÞÖÒ*Ò¯&:æW'½tK·‰¨JFnÒëtÓµªä”Í:[ægís*²9‡˜i”•sÙû¥M…5ȇ˜ò¥G«Õ¶÷³G1‹JÔsƒvÿÔÚßÝ¿yío?`m5y{Ö–õ~;×Ö·Ðãëcñ¾ùú4×vû>'îÚöÛÔZ»ÿÛìœÀmr?à ¶wc‚ö×jÅ7|œBÅu;=í‰xU£Kìà&Fc>rñ­$KÂj¯ž,-`¤Ø¹É"[ðvž*²|ˆ–íaX’½º†oDëL&µËýntªNÔæ¡wçéòŒÐ%Ѓâ"ÔÖ +F…ŽñÓ\} FßÎî#V¶b]l߃`@f¯•8f¾¹Þu„ÏÑõwoçÙî±txÌÏŠh/ïÜúÇsúíÏØg3jr°f>â*i!<êOCPd•U'6Îbßou)`7õ*²}}Ó6Ùýï~º +5'x›wß»X®ÿËYöL¾|}†1¿¼X4E’°ìIZl™øo$\Wm¡9ò­ŒöH‘½ÕìòÎD…nŸÈgaIú:J¾-˜ò`»Ž?ÈöÒ@¤oÒ g_a;û§—÷s¿‚õÕÞÉݱߑí<Û]]—«çç°¾eëüC¬äñr-°8q,V\ÏÓÍK¤OܾKO-Óêñ=ë|W÷åí«Ï1kŸéÜún{å±¾š0Ðý=›ŸZoÑõ=ïÜ»ùP ‰ ™ÀD¥¼n2±*ÊŸg»ªhÐHñGÕ +š$„àÉ"g¡$Ò®ŒÞfv ?mŠë²™pÕuæ%tχq›ƒ´HY6ÝdÌ4šXÃ,Pi§Ø‡›fÊ.!¼'Õ1P–@F¼¿e-¾ø£$eüX“£Ý.»ÅA€µ§zÅFf%Š‚£à©H‘ÛC¤Øç.¾ÿpd^2׃<ï +u2CÒdm$²’„@#•Q}X/äVjö´Þ¿ 8†@NpÁåáÕž`§R« ŒTi ñØ]3O¬=SC¹æ­t :áš)ÓŸÞÓ­P…deð +šJ½Ä‘° ó áOÔµ‹C·ªVŒµÑ¹™Ï²ŒAð±#Hûh"©Û‰0/‘Ø~<ÙtKÖKâL÷¿ W¯Ì–€oØÝ ²'“ K¡%¿ÝMY¿‚µ üCÊz÷TU„®±ÀÄå¡ÚÛá"ôë|àšm_´%™M÷óŠ/@–/hº‘ëü,}([{Ѽ¬Sø4UJövÿÝ›RÚ°CFŒåTø´¼q…ª+C*än}k¯Ÿz÷Î áBÑ^yºRœeí@+È#§]~y,;EÔ¡Q +ª§×G[¼÷©í €÷Bôc£l\/á"M˜2†¡+²ÿ\Ba郤ßßö[æí£Àú”ýÎßå-ÛócïÍ8³D4:œF÷:Ð:"Ïjµ¥»©d0Ê4‡£ñ¶ÍŸkÉ›…v ANŸê¯“øŠÅ>NÅO°õeÀŽ å"?ïò.É-¼ËöÁ«>-S~z‹qÃVX`K뽟×ß}|\‚öâeµ8%{G#þ6Q‰ëRõøвÊ=¾f]$o/n[l×߶-Øó¶Ìeþùî›Ãúö f}ŒûFu7 ö oMû¦¹ÎÈ}ó½MÈ·M|}?¶8`}Åö}÷ªîaÉú¦¿Ѽ±´HzåR“ŸþíÏîu†zú“1$ü@%!Š^X6SßfKô¯ü¤IaÚrÚš!$Ôtp0M‡­‚Ô(]MçIÞæ_½£Ãúß„ÆÍ˾¼Ùoô—´)DÁ0¬Á»Ÿ2 èňIÛ$•“¢€Ïd(ðß°/î…{dôع:Ua›ë¶„H$¯Bá&p Vs—ÞRÛQ)(®œš‚ÃVÀ Ú£óŸwˆNL¸v¤ ÞØW°C9’UVbdè "/‡#ÖÚ¹QðÃx©€LŽ$Z¯ŠÚ2Ì;#Î0.9Ö÷FVǯ#•_c—£–¦‡¬#û@þÚº&i= SÍF“M[~ +NïiD[ =´ #N¾"­1ëaÍ.ÓÔÆ@&ÀImh/\æù)ȪnwÊøÞžEÃÕnkeÌ’¾TÑÍ~±r.‡æñMh£õSr’MRìWEXMa ¸Ñ‡´ƒÌsN¦¸à z-xÅõa½4¿&Ž'È+­)åÅa|*uŒÊÜÚe̸ÖIo±ÛÔ¸%v³Ô!Ïk'§+)±ÑˆeôOKìðÀ» +*ÝÅj?ï Ñ„s-lÆÉ â>úv|ö³ ®f÷Í~Îñv¢TMÃÇ"¥O®¥Ð¬Ä!7ß ª(,ëÿ±ÜcÂð TlDq:¥±VŠ P'> rÁz|*Õ;ªVJÀ½Lå“O¿¡ä÷F‚´-IµñɈ÷+ƒ“³S}iT¨í§§óÇ~š„ôGqƒú€ñéi±†MoWU{|¸/†¾Ÿ0Ƨ­ó©² ÿs~ mjü@vFà«‘i=„Õþ+üB¾°áM7ªƒŽäþ ”Õ>6Kþ ­v7¹ÈÎà á´ØÏÖŸ(w[,´Òb´b¡bB«b×?+´9^Úlk~§C‹†‚ë*ÛÛl[GvuepЄ_ê6GÓšjáØÁ¤º¡¤Aˆ%O‡èÕ Š£=Uj›}K1/òÀ>ººîPuðÑ#_¥Ñ5c^ÇOtŒT’=Ù+’èIéAµÇ@ÌÛ%èO’WƒZ ã):@9b:T¯É3í…_ endstream endobj 25 0 obj <>stream +Ðt>BoÉÑ*ÅÛ†-ß<¡nùõæÓ¡¡Êˆ/O»{têE¿ 2ʹyü]~Y ow»‰åiŒC±mKv]ÛtD=¢€,Ô¨çD&L‘ønÑ^*Ç7êêܸƒ7ée­_lÙ$²‰æõ„ölè`NÊÞv#”• _|vq õ˽$CÉj Zê£KYDÀ†kÅwôáÐö<ÉkìåVàôêôëDÀ ÄO“ÉÏnæSp€®{çUÅÚ¦% L:Ó䄤÷©}Ð’â'Ìw?°è>·ìÍè ²~}Ãöó¬/é~-ë»~ÿ›Ö•b¿7ëj³ÝÞu­º}JÛš·=ìmÝÜæ˶þÞÍ»}_çïÍ°¼ë¾1{.?ÊCä8‚ '±BG 8Ûƒ_ïNQ›øÜ#o2ç¹+jSFÝD´]§Ja!¯»(E߉h3Jmý̼íaÑ&ìÕÇ0 ÖŒ\E´ísY¢6Ý$Æ|#¢Mþ@“šüAv›ˆ6²ör-NYå]D[—…= éîE´íÒÜ9IE»ï"ÚËjH«vvD½µ{Él~&îæüLû[ÞD³c)¿]µ²#P0žs²yX½fvGVj1%ÙØ®™Míx¾½÷ý¦]3;6ºÇíE*;ÚÕ(?0-´Õ*•Íy-ú=8/Áð®„mñ—`eØp€C¿×Êæî‘.ÛyÐìN»V6ßäç–v©ìˆFˆî÷òN3;–±R% Mm—Ì–r½ÒbíÔîÊîýèñ!ž ëëezÀ<»lZc øI}bó ´75JÌúûºŠ \{íÔC /1YvÚ²¬4HâÙ‘ò'ä²<©gÛ2”ñúE«éþº’Å…°wÞîC=; n<’¼ŸêÇá;ÆìdF`Ay©gÛ +‘sÈñ¿六Ïþ—·×ãû‘‡õ‡½Ô”UÜ~ž•hõÙÁß°tL‡–vŸ±b«½Ž*Òæœ ñúî³ÕEÛ›Kã«Ï +aK $j¶ö'Ÿb0WâÑ.cÚŒ1ðÙîv¢¼¤9´³îF;ïÞ¿i´Ãê,î {g´CÔËjTTT¢]Œv"ÉMÁAžü®Õi´ÐâJA… |ç´ÅÇÅe[*Þ¢›ÓNfâYåÅiДßËÅ–³;§™Ä¨–ÈO… ¸:íˆQ¨¨ÚvjÝ-rà„ñqyéòk¹sÚÑ,•­M‹D_wçébŒŸò²ÅÕeXíð|3;á]Ñ’o÷'×p +»ÃP9Ñ·WcˆTölF­ÂÞëé[í¡¢˜-¾:Àl„¬SCÕå.¶:vYj¹bÌYñR½µÕÝ~7¶:pÆZÉ¢Tî¶:¼‡YecÛ¾òxh›­¿øÿf§CÕ“WªÎ°‰v;¾3³àâ}z[nÃZtÛ† YÎÖ}Æ@M—7¢~³u_¸—Ùí^²`Ö—î YS’›áÎê…Ç` ¶849§½ #’Í¡NF¤wß…Á.ºñÞ³éÇ–ö.Œ‚yÐÓó]³réÂXgÑ‘:æBí¥ £eX¿²[Õ÷& çWk´Æ–½Q!è‡XÔiOâ¾f [€ŽA$]{0ÎD˃‰6èK¯j™ÇâÙs¸ïÁˆ¢o²Ÿ¤G³ô`4ý!áLÏ°÷`ø"%åDݱ½ãÌhî‹-“èUÍ © üÅ LƳ_=^o9!Æû S²í> ví=¸;È}œ˜œPüÚz05þÙ=˜Ö‰þ!o£]pîíbI…Åçë°Ü6`ÆÊpä®öA{&6¾mßå÷m6!a§gô‚ì—,®#ný!½—ÿÓ,7\Z/£ ÿÓÿ6¾îˆKãå¿O›nães± Ý¡Îö–ž8žß»Ø( IB“Óª-»âef«òˆAy¿Ä2…‘WÛ2|Ö‹²&ë7¡óTß$†C Éb8ì+Û¥×ÉöMŒcÿŸ¹üj^³}jjƒ8úzbÁËq†KM”õêÐ'lOgyÑ åtù[X€öÌÔ-%ÐN˜µÙ>ÌkÞ°'ÂõzÖGeÃÎO¦5Ü)2m¹C³z¨¯*¶£lEe´Ãø{’lUXW)¾ˆL/WÖj ýŠ;L†K–b•½/Ôßàx¾¨Éj¡, —Õ–­Å£^_Ë"g?\lxþ¶ýSü.¶z¿ºØpèB Pÿò¸6^ÒÄGL?\l Ó{thwô„Ùw§Â«€Gc§N—‚¯ûηà¾óýab#{wLúÀ„±Ä›Ø¹C“¼T…Ùóäµ4T‡=-¦~/Y¼m¦Sñ˜Ò36Û¬èRMä ÿAµ. +æõ¢¬Ì5БÆŽS™øÁtmÁ™ +Î +q°ö"ÄQíNšº!\ä 8ØS?šz*Í*;¯MþøЩæJд ÈÒ©æÇi^ZÐãCÚ•C÷–qÀ­rØÛgíìý '•0ÍÑ㡾šyUçß‘3QÈ×Ð M/Þz ê¤%ŠèÂLåoÈƼSS"|þûYO|þmŠ+M>¸¹6!Йrå\ß ¾Ã¿{9çÖùÆÒ4>ŒkÐv8ÍZöyЛs-u–“øŶ¡ Iñ$À.= †˜Òv$Ó‡l;Ùhöu‰9 ¡wf˜O¢(틃JìRyÈxÖ©)Oܘ”Ä¡þ_lxB$ç¶#47Äòõºrêó\9ìÚ)x“(é•l,/*ùð™< +»Â¥­ÏÒ¯¤•®³¹¤%¬µ4ФÌ/ +þÚ/l‰8éU¡OÝYåröü^Íé@2DKî|AýæÙ_€m{XûÝÃÒ†@”’Ç´/ <+4SgÒœ¸<¸`Eébi[ Ÿ¾¹ì™¨Ê:v+p»°J¶Û¥ÎÅ‹ ZƒaÙ !À§(f1¦É¯%[ç³#s>æœÝë‡o _)"¹}eEéu:OÄaOlç4bŸí*|[#)g[+“ ‰ëàUŒÂOìÅü´"¹Z{Ÿ½4ÔÑkÔ#ͶiÀAbˆ¬ ‰!¡Å4ìP£«¹akÝùj`Cÿ›rw’‹iÀ"fâ܇é;„5”í‘*ÙòbB¢W\)mU¤÷ùDgÓßáhImÎNXc¥Kca¨åLåÉE õ/ß2,l[†ŸÊ–&O– •AüxÀÑíÅ·%¨™÷ °›K¡8ã:&ÍuLl*]î0ÛgO™í _¶º9Õ%r)É}kÜ“½Oöz›ú:^@<‚.Ío/+haM +Œ~þõÏ/×´¼~‰Å8gäbSðbüt$Ô¡šøz¿Æ[W-¯héás’£#9Y%ƒõð¹õ…Nõ§žŸ$*?:BéæÓs¢T,¹ä1]P °A]À6ˆ—)f'C`‰“_=&&£z|Ì`¢1ýlDÈΰÌ{rº^Ô©5¸ACÉ×K…ؘ-j„±ïùM„½ ¾~¾µÚ5Ïëí¦ò‰äy)èÉð\7À. KvÁîÆñFå´nëùº>qU<9) ·k-æ9ŸŸEI3Ÿ-¾x{X]§˜ÅDéaÆ–\_ÆØ×UC˜ndvÏÇßm7¯g~.;4úÜþo÷Mc§¶/yñV+Ððœ»W“ÎÒyír +NûËŽxJšêÚ<ÃŒ’é Ii}n·°R©=ÛÚH'ûÅ-†,M…i×VºÜbÔ Ñ+l·Í/^1'¢¢ézäà ÈGˆ¡ëj¯áˆ=eÛÔ„PÉ2ù1L¨âí©×úzb”/FsÛ°§P ò*Å3®"8gá·ÕfE|Dbë—°m=xÅxD;¬n¶?‚Aühc¶ÎäWq‹v/ÜW­ëtõcïÿý9¦]^‘0ˆ4ב.‚zuÅÎÈUlxJ&õ5àŽÉ(š…£?>¢ƒÂnZú)°ZE÷é@Ì^vFì”vè•MÀNÖ\9詶Gþq”ñxùþ«Œ^ð \¨¡+ÕáöÓ×b½¦þz%G@Ó´¾ž -Þ±åb±©£ÞÌ̈ÒQê™i uðç4Ï(ÍŠ™ žÒ¨ +3c$»ÔÎßP âr—nzÔ›¯$j0Ô‘Ìv)hÆ—Ä—2ßsy»èñaé’}xν %U¢iàÒ>ˆ|¹ÑZÖ ¡­gÇZà\˜{wÛJÃ%^ÍóöRs°%<ò$g¢eaš.ÞŒ6f{©z4äÍr½J$ÈÉR¦™µ‹ìˆKŸë.sÈÃaØ6îóQÍÑoèùÕ·Ø®„Un–‡ìGèqn%ÖEõIȵòŽ… +jfæ&-µåô0LÁmž|ÿÅ'…o {À@7²(I ›wDU d°”«&‹ö§i|ÃE½š;¿8KJå6›šf5¾ãnàÃõ]°Fù“m‹ÖÍñ‚i»†MÏ’y¾Íªd^ÅCÉuí‹ìÏõƒïüHÆ­Ù]Hæ½\ÕŠ®'pë92Ûî4âw7sáV¡iÌ›Mßéšh«4Ô>Oï$¦®i¾)T÷a·/Ͻ6ÖxÓ6i­ùJnª\ãõ½Õôº>4”ÀæR°k‡uãVzl¬1›pÙ\‹6ͳ±n½è¤ùʶ‹«ðRc«ä­¡ÇXQ7¸¹ôîöc¾wí‹ú&S7Wÿ)l7wˆ;5¼¹™L ½¹Ù¬š{ûVu'Ý7÷·Mùo샛fàÜ3o%ç»)n[ò¦z¸oíO¢‰[@°I-®¡Ä¦Ö¸"·ª[³©Gn!О¼§;Ê-ÜÚ´.·HmÓËܽ[ÝÍ-N¼9Ïbn + 7#n”DoF-Š¤3 ¾9°„Ñ·Š¨[¾ «nü*ɺ¸SvÝG­ +±3ßØÄe·,åV¤vKr6±Û-=ÚsGNu§¶;Ó¯M«wæi›ºï’ÛÝZklyá”Þ²É[k-Ç#iÝ„G~{+£âÎÏaµþà­ ¼ƒÐ–Bô­CÅVÇÞN³Ç·+ÙFÜþ¢mÔvgÖºý~wײÿ;Oié\yi3ìÓcmQÜO³­Ã±NÖµ5²M÷µ±rûÒ¬m™íÍ[:ûË»öƒîßý¥›t­k÷i_k–ÞÕý’µv¾¶¥okšm«æÖs»[r·ŽÝ¶`o;m­Ÿ-Âû­bm,n;ÎìDŽjo[Þík{×sÝ÷~麣nÝÖçmøjÍ®{öÖÌݶû­|,l­ä-ÔغÐ38Ùz×·!ÍÖùÞ"¢­i>B¨µÑ~vmmú-hÛ:ü3ÊÛp·6ªàŠ1W,­ÅeØݱÅȈâ.ÂÞ[x¾7öÅ~ܧ ++tdO9ØdORæÛg\3¤÷²'Y+læ>Y[A7{Ò·Âu~Ȉ»¼sµæ¯ ÌhOWÒ}*½bœöó¬ð¨=­ß`V·å¥uV\×^Xaa÷uU¶×GV@Ú^bYqmÏšÛÊ:jn/ ¬ÝmAiCèmõ¨Ú·²v„à]l®å´ ™8ëožñ®h·¡!g¥oÃOnµÁ}y[Y\¡›[]r}n%Í 3zWݧ³Š:‘©[¹uó>Õh7ìV×Ýð³[IxƒßÞ”7ôîVÞ€¿³‚½Á…oëÞûÇתùÄ'ouö Õ|[¦ß@Ñ[•V¯ ‚ }×_ KiJ2ë»ÔI—>…~,1?6ßø¥+°Òƒ= ¤ÄûþH”Î,oœ›úƒETWQ<[óÞ˜‰´·öO[ÿ¶­g^²* Õï3í]!’pÎÖxí)kC‰8ÊCVŒ{ÚM;j|êU-=,ñƒÔv¿±¬àWrb»¼"-Œ­oU×:¯[$- +,“LóôêTq ^›vû÷¬ý¾ë2ïŒ)®ß¶ö÷[sçG±ÝbogîÏfí~îÏø®yºM•µõºÍ¶Ù«Ý&ëscwê³ ¼¿$k×øãl.{5¤`¡R-z‡w<4§¸Ñ³gKêË!ÌA4àa9<¯*ñ<†¨(Hfã^mž’HbÝGŽO òvždÂhA‹|våû¨Þ&šÅõG{¢û‡ÑýýVáþý‡Üì7d;ÏvSW3‚wŸÉãŠþ2ón *ˆ½åÎÛßo})Þÿ´»®Sê ´âo¾wqãá°Z½ ØçRÔ|¸úî'±_ã/oëôïÎ3ná~ ïÜø/_žð«r¾:Q`"ì"m«;IÇûfÚIZ=„wì&˜(­ß¸Uæ¶óÆ\¢ºõÀ‹¸Ö5xõ’˜§ßÜaæEݛˌŸ²¼~üºŠ¬wjçëœxòŠØN‡mLׇíÀ³WÄù”?óÇéÇxò^¸þ6Œn¦˜[;ìïÚ“!ÄÍŸŸZïÃõ=ïÜ 9©&ÔøêA%Ä«´±ûcFi¬6¥ß­–-wRúª­ö .÷šðdÛ<gpå)«¤<ѷ窶ÍÇzî²ôd³a€·(‹ò¨‹0Ù;$bÃ*Ž/­"R%^ÜÂU_ßÓ»ÓÓ»ÔßñØðª÷Ï7I¥ 0€<öV«OÝq)8(Oå{Ë®Gç[ ˆâ]lϾ;·×•ÔѵL=¨¤uçžÀ(ŒòNrjä†6óÅùªí»GïîÛ°Oß;ÿF©+‚¢æÿî€J…5Û¥„óîZ(ƒá¦‚fÍ€Wí¿ d·Ä2‹›[ƒrö»éß¼Ý^ðº-÷åw !¥ª±žz¼yÈ Ð"×[dï¾Ïä(g“2½ã +¡³–›¹š’‹XZ}hAØæ{$œæúl5,Áí{c÷PBw¸t‚uÜ_;»lÕ*¼å^o^Ýõ‡Þ®œ·é×ÅVÓÍB" û24ìóÓw^ãœ*jEÏÊíïjý¾U)~»ê;ÁùýǯÂõû \µï÷q—BìtÕâŸSaUñ_§ÑÀöcWKm6¯®ÛKñlj°½SÓa{Wï„ý¥¾ó`؇iá°­+Óôa[–žœ"¶Eíúк^_³-§·W¹-ËÛ¯+úu{Ö}àå¦nÛÈöL¶­h{¬Û–v;;¶­q›cÛöºÍι;ßNîmsß^’-@Ø^´íÇÞ¾°[À²½÷ïÆ:K`Äòòÿµ1D¥þšQî”ÿ‡1Ä3†øçfÞ·†¦m·ð¢-q5÷’å¯PIKuÊS¨– €+§ö,ÂÈ.Êvæ#¿£Ó$ZHÑ^äð7¡æ |.ìÏÒ`=ryUˆ–gѲâà<·Z8µNAjPzŸ‘ÑzúF͚ϨleKLó&Ϋ&6ÙŽÔ»ôÞ߈rïÁæ“(7–¤²lí¦¿v÷ñ4n{²‰õx»€ìêX5Y€D‡ùög¤±Ù%YÚ<î íÃ6)âysÓ¨ë&ß 3ý—{NYJ©öróÄ$Dd–µ +ß<ìŒj1Ó!Û¢ŸÞQå Žh×Ø9lªÝ¨r«Mxr†‚Øœ¶(!6(äHxu£¼ùТH¼ñªj¼ÿ€;uäýF¬*ËÛ½\…š÷'q'ø¼?ÑE7zŸ +«ôô>£î$¬÷‰¹Jaï“{•ÓÞß;YîíÛO³¾¤û¥¬ïúýO ÄvG¶µe½©ÛÊtûlæ¶=ÚmMÜfǶ¶ÞβmÞf뾺¯3þ½Íaµþkˆqï~/ÄZÄ%HQûÞ É%—m”¿…ܸ!µiºÚK419ØÜ`Ã!rÕäLÁi‹Ôì‰Y|qÓŒ½ßø"qè´clÃÆÐv÷ERÀÌ92Ði·ú"UhùñÆ©‚" ôo± Yán‡TOÿ)àB=Œû6_$€Ø$Ûˆõ'¤Ó6_¤Ò¼…—±õ;_¤ÂÜ gDa¢¼ç‹Tæ7T'ë/q1û„=݆(âî‹TAV´¨cäÆë›6_$"pK£NnMƒT¹#!kf³×¾ LFº1FB‘TPª¶LÎÍ $µàd©$Í„Ý#‰tƒB‡åF^nÌ’ÀÏS0¶»¯Ž´¾isKâ<ÅÖ΃èîî–$H½tº¹ÃåÆ-i}{>ûÙ™[’$’ãáÉÈt +™UÞ1Kê²ôµÛ`³Ù5žWcŸOV¹[¯$wnT`mfI¼Ù’_>ÊI([|ë$ôÇ¿ÐR?SŸàqx²fØ%¤dYÝ­[’º(ˆ÷ÛÚ~€€E´úÇÿD¹%É­‘E½#½\7·¤Ÿì'^nIvéö + ôIiÿü“ä[–e3¿øw–9}ûÛ·Ÿúé/¿üò?ÿî7ß}ÿc}}ÿa¢ÛïM¨ÿ†åýþåU¶‹…âÔ—º­\8%Qá…ñ4¾iZF +!)è°#j^°jýìÂýv¬)ŽZMè(£žA·#ÿO%1Q¸gaD±%ÉW0 ‘õÑe8Ø¥eËKYß Ͼ™5ÞFˆ”ΊÆòxÚuw÷dˆŠ >‹ ; L1;KpLzD Ý êƒZ9qÖ´+YH¤r¸m¨Rüø:…» t³½D,º Nëk êÐÛGïâÔÜ.ÁEs±B±Ëìr¬¨‚R¶ ËF;?ˆßÀÇ’smeÌP²!ËãÐacãôýÙ¿ ±·‚ÒGë¹Í£¢£AWHšD}ªŸ”eÀ–t9ðµ‚ÅÉñ^v.¥I8žàÔ/Ø.˜ýÚFÔqw,—Dow‡ûVåèÊÇm‹î‘@ŸÚve‰ÑJ8ú%ÈA¡qbj·'†(DjÚ©ƒ+>6§&ÁTeÂ?zÄ ÔÙ07 +bª#,EåY3ð°öù›«ð'ËÒɯÌDñ )–æt![+–}«ÂªMru†”º¨Z€œÅ¡‘íÀZvx=ê‚à8ÑfL°G +F”PÁJG¢ wñ‰'$±ˆ‚²VSÅ£%v›ç™CùÙ{×_’ëÊï úê‹Ñ}”qÍÌb{ì‘Ý A¶`„Üê‘hL“EQÐÛ{ÿ֎ȪŠÅsÈiIÔX¢xé¬ügfÜö}¯E…ZCª?–j'©V·™¿³Š¤“h©eЊ”¨½¸¨í%cm:áÖHlÑ ï•ÇD! +íèKeÑ Œ¡˜‘OÑEm=Å?ð>Sêk¦R›)«æ“6·/éÚ“àÉbYS¬ûòùã1=ˆ„Ú©I´c~™µ*È‹­Æ'ó´è)P‚8ƒX³ ÎLc¦ +p¦Í,²*ð.‰z"D'åµ6dRÌöPxoŠÝp©âáò³H[uzêýÚl…—…m¢ ß]Y¯‚Ï|©Ÿ.gj{w÷Ò€F¥ /À£U5e›‘Ûaª1¥b;¢ƒ8í×ß–Ëíw7>EïÎø‚ž(/3GTPã“ä`ÚF”PL¸°„üÓƒiÑòàÙ¶§$sæ£/áÞÓͯ:^3?‘Ú·ÁçÄ&ïÀkß(1šÆÍM¶i5DëLùÜx¾|*[˜‚ü_Cߑᳶ{zˆC–2¸rÈQ˜óÚÏð„ +!oà®^¦Q~‘Umãû$(âLå8=—w`@$DçD9†L±³W)AîT¨æ +{öÉè«©ß¡¦:-¡œôê.¤mMªÙ²´µ@<€"GQàOm ôNH³R´•lÚ©7„èJžô9bng¸.ðW3í…¶'Êå9¬€tÒJa4v KöÅÎh'M¦íì@J«-þ!Þí ßÒƇªÉŽÍUAú°õ$Ef¯wqfþYwÀ-šÛ5„ض4µºÀgwfðràâ‘Ò?á»ÀZµ«LßÞDIFñs„òM4 ¨ª§¨ 1s©ïÌF7ÁÃ`©âì°ëÄÉÑ麘¤Ø·Rfo·uɬ¬S‰ç þˆ#ÂvbFÜ(%Æ `b©ž¤cmٺƊÁacµñåÙ8\•k½H?Ùé·»èªá@š63‰ùÒA °÷'×ùf¨2*2ço[6L6x§"Kþx±¥ÛÙ¶ù7%œ%)hYc ²Ùsú WƒŠÑ–G[ô|¸º|"&þÅ -þ]{Ëô´LM˜çî)—/™ÙŒæ˲°§)'›h“õEñÊmûÁ’qŒäÑæ&Btæ£Ð(y*v:«_¥7*þÕÆR¨ü4›·³j—‡ýŒåY{;—{ôAÝ4 bûÛ&•Ù"9µ^EÀy|rHϺ¹C´Ï ¾û8ˆá.;®R©b ÎU©,n¢¶uÇýÔ蜊²ë+I0Ù¦¶­ ¨ÿXtÛûÚy—ᇵÃѤèº|ˆÛ±¬/0Oèm+*eEosÈM"õª}»ƒ†'NÖœfãB“ù)%[|ZÊ0ÉW¸ÔØÚoˆt펜=lÚÚÁ“C hÛùq‚PivãhÒ^&¶ )´,š²„úÄîj Ca‡?´7®7Ë$@;«·ÞtÓ¶I¡–Ù¬SèVÏ·éÐåì´T´LöQÚlj”•y€ü„Î#óÔ\‡H<$±j†wQwhÍ©ÚÏ£óm;•ifz逯íäC+‰Têʪ޶¥Ÿp 8g´ç sèR3ƒÓ^dKÙ¾´¡˜½ÝçÚžÄ +@÷¼ ªÖÃ~ž„¦»ê”¸Ê±ÍâÌô"›€è]h]6XÓÏ'¡AuL~rG›Pg¨p%ñ3!vŠíîXa% :½_‡¨s`½aûÈ`£ ƒÍ¼¥,+FQ.¨Å¨»È§©0Âm6ês‚7Øx«”° +ŠŒÓ.ðjê’0ÃÙƒ\6ç‡za/ZòЛè¶ì«Ck|<Åî¸<„•*wŠZì2ê¸AxQ©Bªô ™-Â(í—švh€ù“WVÜÖå½L.(×î«û°ééÒ2óv`I\°7Ù¢ÐÎ <½ª¥ŠmdsPµe®g;ɸ>?ðakÕØ~ÐÑÍ& ³GVºydDm6²p]\Döz3Š@Ùù<âŠì"u”U-¯!*ÚÇàkPÈ…Ï¢©BRH¾4ÁÝÚW{¦5]¢ã$A6È>6ïÚ´¼Ð¥jñhEe_ Ö›âk(6€é–!*‹`ð|.ŠÂd T;ƒáè¡jÇ¥øWÁãeóg»wò6Ÿ]v5TzŒ'ÜqUï…íñ®u˜ÐÃÞ‡ISŠ;(Sˆ†•ª–€°ÌH3]r­s}“ÛòæØžD÷c¢" @êjˆr0 +§„;vCŠw­Sû.I+û|ã§Âa%lοb0ÛeZ'a¹ ƒÐZ×àÍþ å7‹Áö„øp»»íyz>õ!i³9œ§º«w³«ùÙéÆý|îÏ„^t4½Èñæl±z™Ø1Z t—Ói¶Ã˜Rs„´Úžñ ˜‹‚3ÔéC²‚T‘?G9œ´7~Q‘‰¼±½Ø‚Œ‘¾§Ë¼-Š¢øS¬èaâ´•3ŠÐ&Š8ÂuXRµîE1!!ÃèoôR«<&IijW$d’ZßÍR Žl«A4^¶"ã=Q®«&"8Iú"™LeÐf|ãb~úAÕìV+Ú78‹ÛªTÐ 6i¶ƒiMlQ17øÒÌ1f«ùx§ß¶ší½Ë&üPÏÛj&˜½çÜÏ'HÅjjð$½€¿è~ÇÆdi”µÁ]nBÁ$ÓMbžìd»ƒ²ƒÍ'ÂG^ôœ„ûº·¾xNä$J‰V\ÃqThð‚¼¯FKÕ!ÎJ`ëm-J’j-1rVU(â‹eÚ¦³n³ `´n ÊA¡udèx;«qì;îôH"”¬±ŽÕÈn´¨;ÐÙE7ë¿ÖWAŽ4ÇÖŠF?§”ÌE§¸9 ÑqðQŸ>jÚø7^ìåç1ÕïBrTmœ2Тÿƒ µ!쉨[7 +¶\¸9ñìrÜÆøjT²}õA-ïæ9G䆢ÀÝ Êà@ìSepö¢$æ«ŽJƒv*¥¢-DÒu š#„iÐél:°H(¹Ù†{šR· +‡ŸÔÝÆ°QÈÿ¯‘'…ìeKt&íXL1 g€¼A‘YðÐñ9kTÛD—ÚÌ}kPÃKŠÕNÜ(FòxŽ­1ù1{Ú"WdÁ÷\¬}P± ß}†"ɪAƒMVí8u$–(f#·o»Ò¬ý°–}4´QƧÏ#ñcª xVJ˜{¸–#[©l›ƒ"ˆ"b‚M4·ÒÜ Á;ð‡^°ƒÛAÖöÜLÖqOFó`ð)²"s¯Ah’Z¦‰ØíWa£Á³FÁ£ànÝM¢«íƒé$ÍÙ¨'©‹!åÉóyž–Ào+3œzW÷y€[´MIô-p[ÔžLåèðÐB‹nHB„“WßiÅ+m—ʈOñ4zÜT)“ÓM¤˜@aî!‹Øæ_˜|êò×ìÒ::¶Ò=o@Í¢Ó­M.wM$ùgŸâquNërŽbÒ=§¥KnY+䯳Eö¯ÀìiòÈ&‡–þÃ~Ú‘§ Ã× N¼(õ|¦—\á^P€×m*ì»ÍúÀÈÃÀ+ Ã›Ñz“Ú¡j³’!!sⱿ;‰>zþ%–\JÔËM{NÎ~Ä›Ã%Óa.ËØlnl&¤»k„¸#K‡E® ž18*š²Š;s€N1ífºõ&(ö×›´ÝìM*h‡êÏí@SÉBê‹ÅA'ÛâPºäÈé RuŽŠ}‡q‰„^Š×êéwÀºQÛnßÓ]t‘%TQáÛ¨TLP¶Å&:M‚‚äw¨¦ÑUA|ŽRš˜aîÐò¿å:½¿ë0íIKKÓu‹âå»í”܇û“fwº¦Sk&ºYu¼€ªWOã + òì¶W!l…â*S²ˆ¤ÇÀÍ¢8Yw #Áš=(#Hc³¬›ŽHVÇqx [ظFu or=°„¬û¾ªÑ}p©¼î»)ÞžžJÈŠÒG° ;öÛz¯ê6:EÛœcÁ‡uîÈ2=çø½< C¨+áz™‚ÄÌs@Ò.›žþˆŸJÛŸãRIÃ|ì*½*%¶ ái´0JO^ÐûFhVè.ØA7aƒ³ïÅf%Š†ñ#ÐhZV¹]ѶúöWÓ™wþ•šmÒóù„鸭š£ƒl_•ŽtÅ.¨‰¨ Ê«2cÇû,­>žá;%Xi™ÏæZ“ÓH£(à­`ãPEiîQ¥ …«šö4M‘”y¥";áþâåÙcW{À<+4Q°&ª êÍð#5#C«-ÂMm/FLÅFÂî¾_"ȶšBfWÉÄr6ôB[M*f¦_1ØË¢rŒÍb"™ÃD9E ʹQS×´àVC3iZ8“I®ìjLÚÈmâSã±\9¥£hEQn”ºM«ãtÒbrº©±ZÃõ ¯‚ÂlJ(ÚƦ6Y¤Ç¡×(× ¦9’PïAØa±­æ=»’0ŒC6)º •n˜ œA8¤¿…;ïÜ*43Ž© ÜCEÖ‹ËRølµcLÁõ¹éœdL¦­¦?²zPšyôÌ•ªI"rk´¨šC Š‚ȹ§uª´Ù°5©K4!û,ÿ»èU…É#§h1 +Æ›¤>/ºOO‡hZ%cWU*CIy 9“£Ÿ$ì€S“#{dgÉ×`q€/l£ŠúKÈùTðÖd–šø–i»æŽd‚rð!þ3=òäße þ m\`¯eæ{ÍbÕr«©Šu³aµÒe*x†ß&ÕìïcoÙ?å| y¹Ê3å®S¬2)¤÷ÌÀ²_h›xÏôÞ’¬ m"Ô×¼"3¯¢Ý‹®ê3¦'+Û$²\’›Ø•ÒNö9‡ÚsÖiíIx•—jZʽ¹ãLª£&.‘ú ˜…»ÖQ®9ß +×›ù%Þ±R¼+LÍššÓ»¦øß-ŽÊÖõ es'Ìâƒíþ aó +‡°mC}Äv÷‡:‹xŠÖJpCÅÇ'ú}áH ¡ö$—Pò“Q¡f•t¡š&ËQ‹³µ¡’'ˆìP4…}¨!z£!BRÐ/¡Š)è¨P µÕu¡ª*èÌP™Ôm(ðÚªíP(Ô(6[-ˆP³¶5DBí[0hfÙ\°…BÕÝK*Ôì;,Ôý[.ÔnmÂP‡lËYÂÌÒPÌÛov•”ÁL™ÁÜþµ>t©ì fû, +†~($Ýú ¡ 5ø¡¨5ø,¡6vëû„ÒÚÕ… +Õ¹Á ‹wì¼¹x×êÎBãàP®UÊ[¿ô}¡spnC¡ôܱó¯7w-îy¨ýžýRA¾„:ôø”µ–=Ä*bMü&äáõô!\ +ñCÈ%ôoC7¡1 „€boÁFzZvQ¨ÐÙ°³BsD‡…&‹m4-4k„¨\hø‘½Ø8²‹†¾“h ½+kˆ2´Àl#¡•&DLCÎ µÎÞ]|vmü±ÝÙ(´Æ‚CwÑ6¤š”Bdzís +ÁíÐ.µ ’‡¶«l [3Nú½¶aþÐ.’¡å,$BãÚ6qŸ3ò¡c.¤LBçÝ6õøBæ&6.9ŸØK¸Kõ ßPl7zÆlIAi¬ØÓŒµèð-Y,ì/-,˜EÇ*&Ã:Þ'Ö-À32ïלš™_Ÿ°Ñhù&9Òr'rö9ôZn³{&!̯° H#jŠ)BDøep0à5ÍØ!5“y Šõ +K¶Ò¨±‡zXè}bÒ³_N¨ev5½¡OÊTæÍ} +±Ñ†Ú©Í×S‘7ˆÂBs›¬­ÉX‹CÀ `Õ6ûŸ³f‘ã·¬™è0’5¡ýÝvBÖÄxœ×5Á®š°>k¢~]á5Õ·É®d 춵ò lØP½öýÛâ‡plÖÒ‰xòÖê‹Ïw3• ÃjžIAм®¸/ÔCŒS'¥kÀc_˜ÔÌ„·÷'Ši”QFZÙ¦®QÕF´ö?а/f;^T–“÷üê:Å´] »bp(hj[v»öõfx&l屪zÄÇ_ÉÙŽBh¸m¾n½Ãwë:¸ðœ8A÷ØEðÓÑ óÙ‰~]ž¿þjÔõ›€S'h]ÜæÂ8.ð²Ãnî2TïMPË6 +&Ìò—âÞÜÑõS…Õ¾àçbÆ{4û‡2ˆçÍáˇ7ƒPïî:ax³kë”±¶ø„»ëXZ¸£ ßù2Óø¾Ôß²~¤MºmG42ÏôÄ"+Qn3Ô<ÅŸ³Nbü–Ï-Æwï/×ýA"ÍRT¿àõgë>iPƒc˜›È9ÔW½î5Ò,ÌõI\¼}°gm3‹ä‚Öè³p—IôOÕÃœî`§Êì³òCñ1~fãŒÃðF$ÄñÏ_Å©[dÐvÖ×Sµl;ú ˆ‚ý°ýã?m7šm'YÐG‚±»ãÖO·‡ióóe½¾yýÃøÞïv‡ßÔ'Ì}3’«yký%ì}³{äRaF_ –  8 +€9¥=`ïNbxJ˜ÔðLû<6DZ°3Ù-ýhóä‡áõ{,%ÕqÆ»£8qCMP79«â#¼ˆ¸€ä¡hvÁ«31ÀÏt.\‘êi†J9(‘´-Z¡lIzL’ƒX2áÕŽË"ŸÝJÑ]`DaÌ??M…j‘ÎÕiOWV…E-{ᙃŽLœ±*ªpŠ§™ÿ”/Ý!pûC|¡ÞÐ(ßÓ¬yÀßdwš›Ç3zJsë`éÛ)qüÑ¢éP]3ž·4,y2ÛÇ1”¨^ŽFI6†|J£¡Î\G/g ïpÒb¬'C!™ ¿«jñ‰+4ÕáÞ#—o>ÝD&±KÂD(hæ*.OKåM”œY± +ž¸»±$•’;š•Ù<Àyhêj«\êâa6IU^•„©t,ѯå)¬¢&a}ýEˆ°~ðõ n@"¥×±ÓsDÌt ˆÍ¬Q#ì?ÑŽ¦ýìsWSaO&JxÇÅklÿk³î| Ôfc[R ºÙ>MÜRÔÂÙyeîÂ.l×@­jvªi™X72õ>]ùçêG›ã"tÃôXÑó&PÁ^†í„´k=†ÏðvgX…4B5TŠôŒ² ¯KásÚFžh¤´SO Îx‡—{|ö9á[ˆ’\hcº1(9žC9ÛøH3=è$ô¡¬! …¦ŠeHb†9$µxn`çÞ„eµ+œÛKÜdg*Sö“H¸ñQ hÔ1Ž9HÒHtÜhQÛAØÉÀ@ÒüyÉÈñ@à ɤ׊ƒp°”e;m»Ý6±e€s¾;’ýðâÎyAÓ’U¡*¢óéåøÓî¡>tÐ*úÄF!:I×åIÄñƒø¡tŸZ{g?îúªaß ±ÍsA?e•§Ûi<†ù:sSl'>ª‘eý¢&Z·@Th»ãº£r]6sTÑ[?høq ‚i°Å8ÔÝ‘Ž–Ê*¾ÀÚù¬Õ„tú—f6€Qü²½ÒÍS „ «ê+Da:âߘ ~@&µàj‰k#6iÚYqR¹,Lå‰ó%l<çÌÒ&ÚJG)îäQÅbwÉCs…“4ÕZšs-:òL§c^(Ç·ÿî$”¨Üšx¯êÎ÷öëã€é7/õâU(]öÝ¢½Î€{“g¿QÒ0œ´yÇ1‘š8‡¿~ë…g}ÂÏJ¤Ìñ »°QrÄwÓs'FK«§ õ[ Ÿâ“‘| +^¾H'fÄÿqnáo¹…¢Y½j`Næ³àWWœ¶_MRø‡x2Š^ÞSTGHIxBíÓìüa°TÕ1ØìÍ~0LО$¯B¢3˜˜ôJ½½Ó©Ø“zîc4¹’b(Q§ý:ÍëyŠ˜½ÔLš¡Cê%€Ô=dtIÕJ\$ר".LR!d<^¸b–â‰è´Ý$ôÐ×iVBdõ0Œµ®]Ž =WR•Ù9ª"mñˆ V-3Ô{ ¬ÙBúV€`¤rƒÚJùöšŒ"⚊::m7\ª8Òü˜y&ý¢Èɳó™q”¤8¸MI5qGò¢':ù›>Ïãìž²o¾ú 蚨R?Dm'ÜÐ&4¨D¨.9œñ7²‰æ¶6Ðù:°lçÁ)MëÉ {4½ßvbâL$–ÍÔù`W’é‚Çü×òkôò'(»aŽï $CÒSHÔvÕ 84 Ýt4R_në,-J®l‰ÁE¢ú7¿ˆ wƒ]3“\7;þk;Y·©=Q)uíÐ ßäçPó4åÓ‘ÂÖÀ1my~Ánò¹Ø4AT€W®Uõ=Ÿÿ›ÇôõYT÷k‚‹Šl“,àð" +íЛIdþÆ@>‘’úhüà8ùVþ¦ã¡¬NŠ}¤Hµ)fæWB‰Õ”XÙ]…J-³Òè1!B°AOò!=‘…iZ®YÔXV$ˆ€#®ˆF&© Š±&¸•‹6@[ÝÅ#R\4aQ_¹¨O2›tjr?oZ_øéEM ¦"_Žß+qŽ’|¼c2n‡ìbÕ RÀ6í…w¶»ÿP°¨Œ$X¾ÕdªŒ*4jWévÒ7 +³è ŠbBnðRL€ñ³ýåíWG_Z.r¥ßÞÏym›ŽCܶ®å‰­àDØ(*o~~øP·DÌë “˜ yb}ï1œ={úqn†M§*éQTZnfÎ$w*?ë €F#üiVG?wëˆÚQ3í~ÝÂÀ-%Ñzïw…ÄÔi´ŒŸS6;‘ŸÑÀ,#wôn77A7±)ÐŶÚÅÁ7Ï9!úÐ9ÿjýØô€¶‡,ÑóÇäÕ® +çö¾ÞÏ.P!i‡äTàh™T3=À|PÛÐk\s¿Äu|Ðú3ýæ°Â8QJPë]“Ðßo’3«uÁî þ®n6U™Ša5ê4Óëf{·WûL–ØçS$·yïûT>·ày(y¸&óH8dÏÏ×cß¼ïýV)gb•4q6W‰µ]• ùÂâ¡öG¾q›E¾îÖXvü‡jd&Žþ)ˆ{œ;ÒÇ+I0çê{ˆ&}Àøáš¿• =ø­l®›£ñˆÁ‘vQb¡B>Ò8V/SgF;ñí*Už½!Žb4ßo¾”iÈCÍ“¹(‹eФ¸W[lSÓÀ´ë"ЀÚ+ÝÎHë?eCˆJ$ĽD£H"uêPz«"I˜fƒVhËí3!•Á§Œk¼‚!XöV7gÖÕ› ˆÒ€í6èÉ ÒbƒK²èE@òS¸Bÿ„ØÍ̬»Ù¡±A‘ÉÎ~87®ø:£F“ÇØŒó½ [FtpÈBZ¨ê)è][>†“=¦0¢ìÕð·°tp¨ªóF‰ðêðo¥Ë€~ì›±6i¼¨€\sŠßŽg{Œ½š¨¥Öè#¬)Ñ +v…«è—½[vÐ!ÑcÂûÐD¦} ÊSj¹]§ðKx(®hD·Â“Ô’»Î&›K”Ó§·RݨNÊ© +N 5Ð"ƒa¶Ëüøñ"ïɦ—åéφ)Óp°/R²ˆñsÑá©#Ò°9,_ä–ýhÔ<”Ñ:7ÏE-mOoˆkt êG<2 l×]‡¨²Ì©°ò‰‚–®@Jòž¸êÚÍX³4¨oN4šôßú…X¨yìI”:’&7»zOÍ#ÆïO”LvÎp¡3íw¡}84hŸìÃfJAÌJÍóãP5§p¥Ö*>˜ûV[ï¿Qóü(Ô<”ö*vx+æWö*‹|ªúQnjKsÚ¨,"4íÜÃÝe£²Ì‚©¢Õ¶³Å [ED áz˜ìHǵÑXgQSöJ†Ñ+ª.‚õd@Å„él¯ºTõhš‚ªG{ÒFu]E¨0f¢Óf×7ªËLS¯n°'Å{µÓ\²‚E'~ƒÈ¶Q]iUVšt?êFwÙÇV Kîêè.žs$ùFý¤E}úzÏ‚fnÉ¢¶´/æõäL@r«½Æ/ÍO+À¥É >8Ú×´ñ·y$S$¿æeºúH–m°®}îú†—˪ëG¯KÒ”ÿ‚Šë7o¤ÍzËüzÐdƒà?EB%¸V’¼Ð¹JmÑy›?Š8àμÇÔË0®ÑÛß?$íš4reÕ®T0‰Õ.§;:9\R´f;€/œd€'#uFé¬Ù2lZܺ˜ÒÄ®‰oN½*‡Av†èV=“”HT·wU¤â™ÛSíRZÉÁ‰ì5¯H^W ÏÄä~¥d’œ½L)¸¦!AÚé]VªÃGž'9­ê4®ú”ÎçgêlšÀÂ:Ááè½Ê3W;I–ƒŸîäõ”Ãvé +rì™NŒ‡XS²¬¡Dšà»ˆéjÏ"¦ËÇL@%'Å;¼Õ®UÐN©¿®Û¼U^\Èå3/¾Ô‡Ø¦mººØmš)Ò¡)XZô {â=#Ј®£9˜:ûOoc6Õx`¤_t\@©*í8Úãi»pMë'ŒcWïÌT‰ŸØ$÷åŒ`×!´ôªVY‚”W.¼©xÉ%°ÐEeÉRœtØÞ°=Œ?Ì„YABk¼‹3ôBžKP .M%ýççy8síýI€6#š"[çL]E ©MMiÇ`­#–Õ³dÚÓîs +à*ì5jЗôMH䛫ÎÈgŽÚ”>ûúIî7šÝÊŽ% ÊŸÙlç^fÛT_+ÔØ­ú¯=ÔLFË3PY xÝPŽ¸u'>/J½ ¦'ø(ª> +ÑL鼟ýô²½Ø›±¯“ùX|@÷b¿¾èm6o䆛Š +Ûœ¹äl)N÷þtéåÑè»3×ÉΔ=I‰´“(ÃÝñš~ÙØ’Œ8⇴ڴøWñœžDÚ\áýHc£€É†F$;ÑŠnoqúQ/‰î!¡ö¹ñ{ Ç~¬”Ú?7Q*´¨›;õ;«æãvDq“œ§G,S)¦GoU„J3egSÏjÇsêP³öQ«T_tA +·ƒ‚ŒB…??)N¯nçö†x9Ÿ%[[7 í" + s.^¶=ÇU¯uFt}iÿÀ³ìSnoå¤&d€ÛAû—‡‰³ î=²<@— ­ã§cvÇŸ¨­ º×€b1©ñbÓ¡êSVò")CÆ’fvPªì/¸Ç"±1&aŒ& ›ÓN‚ïPÚy±Ú2L§jWd Ó®Ë4hòTúÒ‚Oyº@ßÍÛ’%I´Vë ‘@Ðk)%‰0Bxç‰&ðâ’ãbëƒA¶S”V /ªd³£@l=?g27xwõLNv'3ƒ.aì+ÊW=\ÛèSN +ÿfUM™Ò e‚£ †8±]ºbÕX‡Š,92˜|Zó*pDä#2ŃU–œ½3e”§óÐX|ÊgÅ7oŽð7h-¶¦{—‹ÎTZº…Us§èÐø¢0kZ ![]µÆ蛀¤kïðàNUeQÎ]±¸ú@…ÇO wªÝ¶ö?Ì<ô,Àg„YíØgýª…¶ã»Fû_ï¥{¹Ð&&αWû“RÜÌ«ý’³‘IÃåhþCjK½• G ®Î½ùÏØÔQ`n¯@© ÌI7Ø:YW›<+é)·'æé ˜ºMXÄí£ ” °êÒ0` ­€[™úýú•Ò¦L~^} ÒáÈšu·X³³È¯ÍÆù:e5MðB`Q¯/y™‰ ÖóÓ>³RkBi.-ãïjê’ÀWŸ¾@Æ!òõ/¥ÀfÇ063Ã_àÅÛNÑO~ıÙëÐ%lTüœr=Îm'D}¬âºé +ðÿ]¡M)Òq}I[WÀ ¯y&®ú‘Õ¨J)`S™ANW€ÅÊ4¨À WzÛ»Ÿª¬¥ÐZðj.Þϱ_’áµxàü bÚÆsËkÙ8„kMQ4‘¾I¡*EéÌ{GÀÄñ§®œHB¸¦è|v~§#P…ÔÍJKä½#ŽZxXçKStˆ1¶Š2úûŠ~€ÒÈ`Ðp†ß9À” UPÈo=E§ÀÄí'„Ëô¢Ô\P@ŽÞùLG‹€zŽ¡§S嶚òÀs ³É쪚äÖÐF=14l£–sãp–&Œº²Õ#`IÕH%æ—Q•<>G«…Mî´Þ‹GÀNR*ŠTxè«GPiRÍHN/½ó„ÃBßµkmãÜÝ;³nÔáõÆ#Àˆ#‘ˆòW'ÜÖ# Y*Ô­6°Gf™9*̲JCIðÈ úËôÐÞ#`Gª˜K†©FX,ܬŽÀçÅtB^.,Û6¥­J%Ê=½.tß­)mu¨®Íûû”¶Š$`Ìd¦af›ÒæQe2Ù*a, ìQ­™lµ¹Íç® lµù±Ç"åµÄ¶mX#mN5á¾O`»„àe·7ìmØ°e]*õ[21Íw6"\XØž:$c’÷ l&Ø~“[M`³¶·ð¼l nÉšÁækä”3Ì}9T! LH¨¬lµ*VæªSY»É`3HŽ€”˜©}›1÷GÉBÞûÈ`;e$ig¡_0f° ÎÍ!Žž/X2ØzG‡ç¢RÙL‰ˆ üM*{+éþRÙŸ³åßç²QV_{22‹£é^Ó¯Ø1_ËÌßd,©‹‚Î[rÙýãëË×´nv²½è,*Z~«×‘Ëæ«ab²Á̸f®wÍdc]{*É–ˆýƒ#“ 3€ãCŽé×Lö4¸'“ ©‹Ê†Óëçÿ/¬ùß5Aý{ÊO°8ãè›äD¢ìcùf:«-ßL +Û[Œå'f¨{¢ L}ã!–? 3Ô”8ixC,_ϹA?ããG$ü]0_ßB¿—}KWi æ'º"zꊧ}(?‚t^C,ž1’Ÿ.·S·üçpßÄíÃt~¸OÀ´ÙQ³•‡õÜî)8J@,õ÷^O&ÙiVVàžŠ8…«:®ÅÀ{.ÝLw Üë#‰_uØIXÕ5‚Ÿòh `üÆ „ð)˜åq'µÙÏ'„ŸJwt<º×3Qã5„¯*B•™lλ{†ð7?_BøI”bâž>¡R­!ümEVá3W”“á‹Ù6ê1„oßüé¦Ç´Cø[ ᛿eÖ5†fâ¶å'³ AA/4Æò ôKl¡INÉÓO˜D£ë*„òÍÀDÙF˜Í1’o÷:N‚Rùä×ÖH~ÂÅýí{LûH~ÂùÌTwþÉ%Fò©ÿTLò³‡cÚíÿþï¥^q ÁçÖû œ€}½m7åÜ°¬#"an[ÃK;ØKç‹y{nW×C`'rL渲™´eÈÀ̶ӫ-n¢ˆxuZ¢³E˜=\%ÐlÃXâªN*`€´é6ö èl;ìz iê"Ò¶Ãô\»`fª“N46>BÑ>Åv„Ùf7/³ËÇýþ9Çí…¥¶ÊoK@ÿbJ0©Ky%°Y›3U!šå‚~8Gá?âËj+ІN±( úí¢¾§S"º—Cº3/¶ù*æuÆŽ¨ŽqL/ÚÅ 1Žt©óôC°0Çã‡}u(l3¨‡#jwÈ<ñê)`¿“_<’(D*ìaU=RR³½F¨¶—Qve xqðͦm,Ÿ + p¶`#@NqÀCpÑ|‹L=‡ÌÁ—° õH'å:€ æ]Îf8Ë쬢îŠsâ| ¦ò¸«¦Òì$ÁÎíªª‹’Ýú)yfÚê<œ¥JpùàÁÚn°¹Å+¬˜ÅShï‚‚—¾Æ 6z¢‚Ù T‡È…2ýÄÎQß{Öß)`aÀÝfÚ4§À’á@#ü7ÃïÕg’ÀWËØþ”#qSÀ1~U~pÚª™EN, øsó9Oņ鳇+šF؈ïÓ%; ä:;uxTœº"ÈæãpXVZÓ.ò¹ˆðÌ>…Zî<4”Ø°£œée!‡zÇ%8b9ŸãMŽÔªø…GÏ›fµL q ’glYj›Q®vªmíMÚPïöÔ'çû¹ÅrdÂ.uUjÝUN©»¿À&&`FD6hsäBŸ°ñÚL©´Ë6ƒRL¦z1ćû›Äá'„«‹J«Ä:î±´»9 Þ¼PÌ wãÝŽr;&õñÉÛß—?x©Œ#~ÕÆÆ£`ü¾û«ú„Ç;2kFÀFŠ¹„»FdÓþ!uüÿ»x2ï ùˆw3‘‘?’*‚‡„iíªX@‹'bJ­Ⱦ„Làõî:ƒ‰¸Bkª0±.`¬ìVç­9/O¼‹ÉY(\ py˜ªgM2›‘䃤{ÍÄ4[YjC>Ÿ(€o´‹f×*+S |èb`LPÎý#ÿ¯;\—CÌrÁ&dòöã%alƒ<x˜qìÌf̨GÛB`ì +œƒ®Ër8ýMnRH`(]µ¹"á–…=mXX0=laÁ·QSB™.uŠÜ]šLŒ‚খÜ7¡$&„¿Ðsºcä›ýŒ¹fÏqÂ:Ü÷àˆ8ªŽÙW³6æ £¡­í’J§¿êΖÇ‹sØ@i|éù&’Pà>Å:B†ñOÇtÕU}d`6hó&ØoÅb¿!y1‰Âž‰Ÿ–ˆ>M&ïq‰Ž„¢rñ¦³ãjD¶§I^³2³È™E¡ˆ+Ù×ô7bŠ#šcz¨’l³ÙMUy¹›“EŒµ›`TÖ‹ H.dX.¢FkÊjz߆IÛ§‰Ñ<ÎTm“^¢Ç¸eœJüt3F‰ØòÁ¢ B¦gö¼½šøž½)ƒ$¥Dá@zžš s¡g‰»1Ì“ìhÂWR‰÷ z‘+µªÓ cÛ7õìáI,Hò*.\7NJ÷t×Ôf=ÏÞz‘Tαü¿îìkšêS¨ÿƒ \q2P rúVÍJ¨p¼ˆ=÷­äÍIBPjà õ'a!|ýËÙl´‰3)tÛ˜·N A«:((X×úÞÛ[­éö!’z‹]§ù—\þ%ô#¸ßNDIº¼ • ýÁ,Ø +VE†Í½#‚xЉŽeu8$ BS]jÙ?Ç37£ #ß×pøÔQwâ)pµm Â\¢ùÛ\Þ!VoÄ´j³M-`h²gáFqߧËmù8€b’à.›jl†Ã‹aÿɽîfßlDêåMºƒ¸mRð5=iÁv^~pÀ%Â*|®³ä7dùÔ |G›wmaç!)›zàÛ/M*/LªyØIÚ4ZÎÆ1 +ä›ö’Y„UÏñJ'ð_õ¡Iû°‰ù¬ýûªÄøðz$½l«”èø¢?®£b‘äÒÇæm06NyT‚Ù3ëÄdñ0&ú5² ò™ûæçM¨SŠ&ÓÒ´ys¸cûáá®0njŠ0ÃLщiÈ7©"A²ñЋV®}E“Ü{ϛշ¤ˆ —lž–G«ô™jÚlÂ:"§ RäÂFm +€ï ›l{ Ô®Ûô&¤Áæ`‘LE‘B–f†_<›°Û¬nv‡S³ô8däñ@‚QK²JZùjú@0QMí³y§ZÛW§ðÕiÄMÏ«D!é…eDãÐÍ€ld­Ù)nÏ®‚š&¨¸F䈢°' H&Ýì”Ë앆†ÉÎKàQ¢EWåc{B0k· Äh?[Ô߶œ¹l€ï• ß6ìNx[XA‡f¸4(ƒˆä^ÐÃÀR©DÝé;Ýêó,xUªmãÞ÷Æ.È׬çRyÎm‹lbÚ6ÝAõ¾îˆ& +Õ‚´Ò˜]„:°Z:Ä|Ô4j?ÅÖƒ¦±A+´³µºx­<ÞÅh½ e]1ÁF²ÿŽ†QC{´ŽUá³W«±d§À¥Î +bÃht–átÛ^:0,Án%—´&·`1}³Õ-lU+ьΧ€QÁ7„5ö±Â­ï4ÿ!±0hœ†é^Dç… aûÆÔ÷žc„¥ï]ΔÃ!<Ùé«ë¡Ä )‘Óõ‹û" ÂÚ•9¾ø÷Cµ€ê+“ÊsêÆyB†)# +ÿüð»âvn[¸kz}”J 8ˆmÞ°w‡ìÂ?‚arïýÎ +Š §-éK÷•’Á*½Ö +•_rÇÆ•w—œÎ%…2(»…ó+¸õü€ÂŒ²•¤ðÌ&* ÜGÚRÁ}l´/‡ç̹>&¢ÿŒGÆrª°ÙDÔñQ8£ž‘%j¼ ‹Ø‡ú®'¯ŽXX+ôÁkÛ!‚j݇gøž#‚¸€ шîx”Rø«€ð;¥ÞÛðRY“•jo±áÇ0Ÿ­ÂXš(…^C](Nh²¿–|1#ñÖ$ª’v +n×À› ƒ(µC jkˆÝ!G<ã Œô¶–᎛LTÌ>ÆI™UÊÇí¼#ŠñH²‡ëDÌ6û¢6 ²}Y*z“ÅQ¡)‡,ª7™ ³K"4€ŠJÌ(ùÜQ_w¨<•Nl»‹šé*¤áÊ‚jÐ˜× ‰C:¿¨c `Œ©ˆÔŒ1mÇ øJ¨$6ko‘ËS©?fšan¡~§ªS BžVÄ+h#çÒögƒNR×gEÖÕ-¦7Fêt”ü¤ ˆxLà\œÀÍf¦ž”†¹Ú†Z]TâU’½j{o^t˜›ÒÈ6)vÀ¨«-dn#ôñ2ý¡š¹*× gqUa=`‡-LªýehZ&îè¯õ½ÔþÚv5ቴӨºj}Íö&;Ÿ¶³ „ÏcçÐ>§»„)vÐÔM@ž\nsWIš}Š Ö›-ÖÇ(± «fíÞð)ÍÙéÏh"Æ!™£ †!(ãmF†YfƼõ 4¬¬}˜`¡àÁ¶ËÝ_è)Ë2õ[­ÛTUjö+M5êͨËMba³cPôàiOÇzÝxÞÔ4B·ÇKÜ»Ð_0!nuoÎÿ‡:“ üšb9I'ŸË× jì~‰‡Ñ¾ßÌZ›Ï$G9júnp?Ht—iá@*Ü×ö§]z/`ÌJ—} ÛzBlAuÓ¦Aa¯©µ£´Çky/ïÄI"^S{êFôB§EíA1'‘Íe¯é¹®m¨þŒòì…¶—TBóyU=ILvgòÑ"TÑÈÖÊ&¦Ì |^ðN1‹” +‹—; o^JK¬š”LMnÐyæÑk!BÏãK¾×PÔd5uÙ<³ƒþ*ç×6Âq¨oÑãŒñ¿&K[=ÝxïÌœ*è{§AE‡Ô–sÏäh`[Ýíµ ïù‚© +ù$¦j¡E,˜@ø(’,¤:\¾ZP¸ª—ü|c—rY-1SäfQ°ñË[­9’ŸÝC¾ëÆ*Él¹^lJúÕÔ¸öéõ`ïÍRz(mŸRuØT»Z·”#yËG¥8eš·«•|:ød4°Ù«i®îÂDþÖdJŸ0«…ßïñÈÕ'÷ÄñÀðÒ0ø€vÀ×brë¼+x(×á2Á¹—*iÛ}åß%8I"³@pRÝÓÖ>–}†Ú +aº÷Ãâ§üÆK•\p\õèï…;ܳ\üÆÍsV¾Ez„&ÑvwP×ÍÙ*8_{V€õ';´yº_¢?ïXÝròÚ…àéÎ-Ü{Z/É^ÓTñ…ÈP)AuŠ9Þ n +–j‡äd`@@«‘ÂœZˆf^BŒCíHÝ¡çIYÄ ·ˆlB¯K·ù.ÔB¥¶ ¹`4T½„p w¨]ÃìfŒÐö‘¨ñ ní=¢.Îci‚ q$…wq't„ÌÓ,ÂÚÃVE-WI0z<îÜïÂ_|ÔwRAFŒ¢ñ"kA3uO"‡`œ­’§õaÀÛ­=a“×ärÕæ²ÓjvÅã³¾ %r—— +59k8!É›@<º¯ÖtHÔË!Q¯‘ÏáQ±±‚¿N¤ôB¬H+Õã«ò•oY#µÜA3ùt•ÞzK1Nñ‚m›òƒ,.£É‹RB¼œt§WíQ Àÿ<²%x͉Tjêk\|&r/߇ϑbjnHB?!Ú%Ï1s-Z)[ó^¥w|Õ<Y!›¤^óší[krIRÏ軼¡Ñ!fYñƒI™¦˜ÇPÓ´ÓI@?bɇ°i;~b:«m£ë%äTô}UÝW^+2S2zó Ôœù‹HCH¿Ìñ»Ì6-½•ö¶”^³F”t)ºøf`hÍ9a’— é*¨iÞ (ú!íÅÒ©é`î6?{MŸ¡•|¿z.É’‚S‹Ø¥ž,³zZÌä1­icÂ2÷šäEtA£þNõ“®YEVæÖÖ¢K·l²“ˆïÏ”óÎDë=V¢{j Q‰ˆ”Yó¢à2]]Ùk6kÚÜa§ðT¹uéªÞæWÀC®÷i1â"è„N³‡-ùlûyåçæ‘€ŽŸØ~ÝücÜš'´æÛ÷óšàÂ:TC\âµb`¿UÖʃ°ãÖâ…¸i×"ˆýæ_‹)â!Zë1žóª9¶Ç7T…¬‡-, â#¨¼:¡¾%ˆªP#ÄÝZi³•š¡`g +ÝPëÄt¨ÚŠûµôhU¡v)èYú´U[¡rj*½Ptôe(ÞÚêÝPôw($ 6@¨G{kB„r¶`„’¸`Å„Òº­54+ó‚1Šú‚Aj·†ÝZZ8ÂP”íɵ¸1Ø¥ßìŠ$ƒyj,ƒ™ük}§ê2ƒ‘*:ƒ¡*Cwö~(0 nC(R®ÇZãºuaB­lp…B™mp§bïÎ-‹w­Þ](b¨;Þ:š¡~yuXc ôçïØ9Ï›»'|–sÿ}ïÝÿµ†<ºÿ¡ü<„"Bû6¤Êá×ÐH,¨Q•X¿ Ê„jþÛ 3,ú vQ¥Ø—€b í #zš!¶Á¯ØT±Ñbcƈ‹ oÂx±?dÆ“5’ZT¶É¥Ñ%Ä5C«Lˆ†–›·¡Õб"³¡ëgÆrg¯Ð.;Ö8rìVZcѱëiÓŽ=Skl<´]…°zhßÚ†çCXˆîÇN²5C;Òv™†øœ5c›ãÖ¬Gl²ÛeOb³Þš…‰ k&'6î2BuÐÞ¬R=¶d–4jÂ]0G·˜’¦µ®ÎÚæ¸ØÑ‚~7[ß­ú5U.t¶Ç„(E1ÝVIr KwÉ:8]EKfbÃ쵓~Ì=¹D·;Ï;&±Ë±g׬ã4ѲŸŒŒŒ‹ÉËÚÁ +Ãj5ãG8êkTÎI²£ ”Vb.µ*€f[æ„šlÏšm©«<¢t»¬n|ÊšŽ_²&˜ãˆÖDõwÛ™YÞϼ®érõÅ„eYÓîqi×Ìý³3v‰ÿ¸±Ö‚¸9ׄ¸Çwµ ñ¬¬5ñ¼­eŸoøeL8ñ¦M¸ÐÏ]±… ‚«uKü0”–\…‹0tâÑæQšÕäã²…:•43ËÛÌ [p¦|úδü‚_ïðC~:‡áƒ¹è#¦¨Y2£ÂÞ'ÞåÂWaHP[ªˆ~’;»ßÚá)·'Š  +3býÓ$¹ˆ7CÈ›á¬wørŽÙ?y{Ý‚€›ùÜz¼.â_5ªûé‡?ˆ©PÒ62šf•›ùì¨ñ&p\ë9“š#ÒU?@¦þÊ•¯×?ˆÐìÉÇ~7bwû_©º¯€zdA;-ï‹7¼ÿêïFèêýMµ8D™…&4ÚTÈŒø>!ö+½Ü¯ß¤I8øÉdEyBâ÷9lÅ•Ò‹¿^ç-|Áç¦ÿ»÷«u°½L€Ƒ(’Ø6¢ä¾ó¡ìëºG²êA‘è²ÿ×]†—”=?OhÐ÷›Ã_&ª¦~=<Âa%ÉTâqAVJÅ ÷P²« +OYŽnüõôÇí„È:-á)qb)öÙõYߺCibP<­ƒ”r9hr¼«1IPƒ«bš§ >IÌ +bŽ5÷×?Т_´ZÊ»ã·ùÕ²)7/\ïX?ü»Ø ·¢ |"9ãøaïŽP"DŒ÷Öêµ¾ŽÀl¢4]SÏO3Ø»ógxý„Ï®Á<‚ÀšèÊD?aÙé9S¡DhF™½×_ñJ“…´…ª r@ƒÍ\ª"0¾qŠj7¤¡¦©™5×Wñ@ÛAжy=ž’ Ð3(D`»rSLoå©ëV¨Õ<Ê¢íŽd +ªïMZÑ8i;€¥:«N#“^§“¯ŽÚbV*À¢ºC´è“: zAì@¨âçòOVÈÏO±ç:Z3m ´™i{e ^bY¼Ô—ŠzóºB˜¡¡Ãõ¸ ˜tæÜ‚•&¤l¦XåÆ.šQɃ-)É/ù¾5®TmÏUyo¬xÑ­òüPP Oš¦6IQ|ôYƒl¼`#äá™É$ùsò”eéõôb3›†öªY:˜’¸aØsjQZÈ2wø毪¢²¢ØVáú¾Nœ±F“ôõŸ +æ°œEQ٬ç-L!·‹Äl}0™4b­Sÿ`¾›³ô&lë&·‘Hôh›¥ïªïMü£Ôn/=Gü›C·Ùˆmà\DV9¹a3·ìàê F!¡”ï™:¤ ÙîþlWë^äÎc Á}ÎeìîTWðÝ.Ý@¸½‘ ¢¤†¥U¦£xÉudÛ1²î¨OgAÈo~JøØ$¡qu¿6ãÁô‘"7W§ŒXRœÌÚ! +ìçê ]§”$çEÙ¡:½i]š£—¸cHg½Ä]Rsij¥á¬Þ›Ý<yKæ$ÔÜmïŒØH&¿ßð%yq”© sïÍÁÉÍ•Úæ‘ù‰çNI>ZH25%×K<»ö&…¯ÌÝ딺E@ñv +éÑ2Lƒ(ÉÎÐEc ¥¾Q‰|UõzÉkᦠ;+Q] £©œßK¾ðó <×/2x;)ÂÃ<¡æ2(횬:(,íªÅæ¦XUàvKM¶fÐÆëîJ}{H‚q[°+Â]Gº=÷ÁÈYåÇgí¤ÏØZˆ1aG/þ¯ŸüÍW{pÔ«üÁ¸%}!*<³4t›_³›ÿx½>g+ùPàÓ„?bAá_sïObe‚}ƒÏùï?à¹ÿWÒùn¢wˆq¢ß:ü¡3@ì„α:#Äþ¡s…Øc?t®û|΀°¿âs€ý·øœWâs~~2?ÄÓßâs<ý€Ï¹âéx΀§¿Åê xú«sâéˆÎ¬¿…ètdý€ËõÂf@ÖßtdýÍsÞ#ë Î€¬¿ê Èú¨3 ë Î€¬¿ê û¨s…Ø@+Ôþ; Îaê û¨3ìo:À~êœûŸóó‡ã›7Çèh&ÉDó±t}ì)wÁø=¸£1ŸuC¹K9‹mLúNÏÒj¤ÜùÄ“*Ø`ǵ§ÜMbzJ0H ú:Rî"x”"oôö ånš5vœ¡™1÷…s7QõlÖ›í)šßjäÜ¥½[³¨v©"ˆ Ëýdiçî‘=…o™‘Œ+箹C8‚Fiäïʹ˷¹ 7Ççp0“sTß´™¾ô¼aßå°éî1ì.*ÊÓÄ ¤»â8#ÀnFo$Ý[?¿j6œ»°1ISõDÚw¼håÜMÝ-<ÛVvÒÒ†s7±?˜iøê‘|w½Ãß´²ð"bQN&»è“ ¯ /¬™¦ëªq t¼¥zëBEòNñèxq>h©¬Øs¯…˜—Îj*Îkeo˜y×ùÍW?&3ï;úUª·m[}OÌk·¼ã她så“53ò=ìõÊÌ‹| 0«ö"ðj´$“Ž+3¯iàC(K“¼–*Š©rw/{ˆy:Jì4~ î \ô¶¯.Šàw̼ˆ–·Ä¼fM}áÈœ—·B uÀ”[™bY¦n©ýÉ7²‡–WúWâeðée¥(1ùÑhy?Ú¿àý?Wä½Ûi¾È,¹j¾È,¹j¾À,¹Õ|Y2h¾É,Þ`–ܪ¹@-Ô\ –œj.2JîÔÜC-¹ª¹•Z2h¹@-¹ÕrZ2¨»H29Ô]à–Üi»È-¹j»È-¹h»H-¹Óv‘ZrÕv‘ZrÕe‘cr§í"Ùäªí"Ùäªí"ëäNÛEÖÉUÛEþÉUß}þ þH +oʃ7¨ïþIóÞÓO^3±¾¼§LTg²OšÃЫ¾;õ”Ø'Í©ëæ<ˆß/zÙîÉLàË×àág(ŽÝŽ}’*Á7ä“‹þe#sîI&èH¯êÎüºNäz¥žüÝöORèf¼©î(í¶O(?WÍG›â¿m§üËŽ1M@•èÛöE®Æ4[àÒ鬙ó†1ÍäŸpãÌá?#µ¥L«æ‚R`ßì­•O?Û@ Li¡™ï-Sš‰Hš©l2öÜ0¥1ÂÃ6’.ªÈÀ”¦òVõ£Ûñ>'©úJ•FÏ¿†—#0¥ñá‚ 8€Ü¥‘[Ñ@ÌÐìtl‰Ò$”3ß›¼+¥ÕA–’20w÷†(­BºIæùUjbÛ¥™|ð7ï+ç†( ¢?åêŠYÐ-íjš”†-QšP Ôøi'¶•È“VFý egÇÜ ïÓhÒr²ªs{Ù¦yRó.$ªÎa¶³Ó¦ÜøÝ%¦mÉ)ŸŸÔ×O„ý%½!<þ–´¯'µ—NÈ{j¯N Š%ºÃ> I›/ÌòY2E¯_CÆMÓ™š³üÖ/ì¦ÓÌàÄ¿3í^9ôÔzÅó%Ti¿ÃE^öŽ+­ íóO0BšöM:.li×çp}¸O~tÐïN—f›k¾™…Ÿi¡µýˆh3[¹B{À!ˆôV:b’?ÕÄc¿:Ý]&ŠYA™=ïbÆAEË8?LDö[þâf›ú)*Ó>Ì•Üä´x˜³/¡ªè]#½ßê…Í^7UOf³¥?¼ê‘ &ņx$O®Øû°ªZm¼Cå‡+÷ô¤4Ùa³eˆlËÀ&þHƒQrùfÝMÄñ>¼sãL/ó£fÅQõL2]Wh#UsÜ^ÜêYiŒhÜÅ6ˆÇ¨¢A·Ñ(ôÑÙî^Ÿ Ǥ}A¡ê<4x úRÙfY¤}œJÏ‘o?ì¸g0vˆ¤ÒÝYpî7ùû8 K‘OáCo_ +Š£·¿íc!ÖׂGŒG€.'A±ÿ|iÛ¥~Wê2 ¿Iß„üËëüQŒØ¼þ¡ÐZ»Ÿ~0©Aj±¶YF\"t!¼¤bëu+Ø'œÚ,¸Œ÷;Ê̽Ó-4a]›‰yCºßÌʾ7›šŸ¨Úä^ù  vóÓ0àMLÞÑú)Ø{Lr¹*»‘·—O;íý|Û'ìåq¶öç¡^ÔBPó‰ñÖ†^«Æ7Òd1+ÂÒRPôf/ÈZwG1EÏ1-BËjØexêð5ì.íu—^0A#5B`X67‰=‡`Ï©½-ÎSŒvDçÊ<Ùë¹ +?g3¼?œñíçQ¦!H™0•AZm—$H½°´Ar†í%ð›Ý5åvØ’Qä¯Ûúsšc¦È¨¯¸Ÿh±Šù¯ëB¸{ZÖŽ\ƺ@ëG£>ŸB² Ýnj´;P¸€‹Ì²Î3 «¹K@9¡Â:yÕ¾Tuæ2b"°ÓDdæA㢛 Â‹¹D'ÛD‚ 'z6Ü?EP˨ä"ú‰ÛrºyØ©ˆQ¨jIõ (†\¶ +úf)»$7O‚ÚQ u]L-nö„èÖmuU·‚¨È Î#)´–yŒ¤F;/=æ¥Ü,ñËüÿ­ÆÙ^¼,ø&âeŸY†äv~ìø#°—â]Ò. +Þ­½þž…Añí/jÄc»ðˆµAQu,­ÉÿÂÒž,©„LÉx´zÃnN´(<¦ì%ˆ Xhm:5&(Š +‚¢³Ú©üÊ ÙK¡e,RáT7”Uñdó­]êé}stŸ›0ÏT˜ú›N_B3¥É"kä´=S8u¨|ýšøöUm[Þ[ B½•½:0½£(z{*tj.&:aÂ1¦«yáC¦_µ×ö)øªæ3¡×&(EzNƒBQÚ|îèüÎ×k(‡Ï˜¡œJÑñ-'5ýðLå{¦äÁês¿à,ÊvÔ¾^ƒlŽJ°ã:ñ^»÷ÅG/ˆÎÅ…’º©ˆÎam(Y¶åµ™ ó·|~Œmæ¯ ª{ Ÿ˜º ä)µ;íñ—3ñ²~—áõ›áÙRÛüŶøÉ?Íðx]¡Êáá(ðî(­ø7ëß}]¶¦üÁËþgó‰~þ×/ÿî'?ù£ï¾ûûþô¿úKîuÿç‹C‚»}ô¯Ø \¬T])ÊÓp˶§+•(aWÏ¥dª”ìA7wúlÃŒ¢âÓ$¥¿À˜)¢#KZ냇¾6ÁÄ*IJÖ& –R?e*a©ðcä-ÌÞP'³Æä™éÍ$duºïZÉÈL:'À©ûžöpCg„¥ÿwäÂo I¡€¨Rº&ü¹0>åvˆÄCðÞ·ê<¨ë5 Š‹©e +çhðN)†P¬Á Z®ƒc&)!Ý[¥èOŠÒS‘ˆÌ“Jæˆæ0l(â{Êž¤LqeÊÅà qÄøŽLŠ`Ë̺n_ù–©=½!•8¨Ê†ÚÃŽÓ‡ÒèÝ·fãÚ&ƒoV8>pÊ‚Í3»ÄVÍ.ÀNÓ¹8÷Åy””u4ß6¯ªÍCÆá»[lPvzÀÞ… Ýà|w£00̓Ø$Š»/d.q_Òlg fQn=ÿËç’} ¸Z³ Íð8³éeÐi) hʹ¢v¡ê©:Å5YŸ›‚nS %"‚Óò••*¾¥„ì’È8(n·i%Â+Ž)Á²5s†sv(è» +³Œò ŸT5*˜ÝÄf‰w¨H$<Î5¹l­ŽpɪTšÃ§&BÞæFˆ4'ÜaîŸ (DH?Î>Þnêæ öòå„t-z-«¤ÔzC?‡¥M™8ùÐv±‘AÛ­åu/ØVa/0ICáÑØœN6; ÐLááиÄT7ÿ€Ø¡IhÛÞ?þ1áC5N(ÕV§êwÅç@ØÏñ¾Lèuïå;UVxœ°•°hÉùì(ßwæTœbG2¡JC¥IPtÄ€qË!¢'BC<)Óø ›TíÃÀ牳` ùByù·þƒ9·TB‹“ hsy:·#ÜQ$P héí¬ŸöÓ‹ÝψïþLU¡Ã½7N*8_ø}¾ÎÀ-^ZçSAš$Sm•’(Maš<Әɂã†-í(*f¿(uÕ¨ÍíD*™ +Ìâ B&©“ÆÝîûšä•*ErKÌH¨Ë’£Åw”xeYpˆjõ_ 2UÀ7”ÄRÇ‘Oo5äDû‹’ó‹Éçï#Þ!Egë0=¶-ijÊl—WõO»š9pán¬BÞ„h³9ÎÈáŠçØîDΕd÷=*²hóÈß÷Aê‘A5+¦Ø½¥GèóIÀ»÷Õ”¯µˆŠÔ˜Ës/È´gðª 1ù«ªé«lpµ@²™%øŒWνöŽwŸûÄžw_£*°yE†- ¬ÙéèNô[öÅNɇ5¼U'!—,#WlÛ2uG.sÝI=6´7×ëø"øu;(²ñ¬ „K}ÅÀî±ÕíäÝý@$Gzi¨Tèn3Éá¨t|VÇBdße„ÍÂñR!ùâÓ©„¯“»êɇÕT…÷ã¶HßnS‘B.Ëð§Ig*k*I`3úPüc¶C‘VjÞcÍÞ d˜ˆ 8ÞãÀ”œ!gKÚï°µ8@c³’?oè2êá‘žptTb/µê ,œ\û…¶¯IqüqŸ²:¸Ž:e—ãh‚”t3 Þí.s(XxL©…–ŽqõaôîT¬¢ÉЀÿàeíœò÷…]Zµö¾¹ÌL»E¹[Æài‰;ƒBæØÓM|…‡à<µæhn'˜=‡á’_ãK‡:øÉÅ*ňA£› ì>dPŠ6A5eŒîs$_‡<…reVõtM*:Î^—4¶Äö +ômÉñ »¸ «#ú˜Dkk¸ãªžÀ¥JÂÕç,ܵŽ—ÕSs#ìO­Ö],màئ=½yýv;¢·Zç.¨#âf +Tá94VD³ëŽõ[ÂÛ1…»ÂÜ`~ÒÝy·ó:¿õP1Y‡ÉÖi«N7­h0ÖõÍí `™p’znÒû—¶³ŽÙî3¨nõ|Ú‘¾Ù¯æRùD’kÚìyµ˜òæýœ°íÑáD’‹èr×vG°V¯EYήYI>“U°ÃI ½ËF€€„§Ÿ›i–Ϻ‘AÇ)8Páû»—´‘e7@0TÈ­’^ÉvGªtÀ  "øë%xB“?kéöþ ¬Bâåf˜\¹ÝèƦb,DØVÍö¥¨xV¥uÁÇû^ÕA«èUS‚é%ŸEÇ’¶ âFC«­ú,Q¿+^¹1 '¾ÚÞœ Vz\çwÖ±ï øüÎÁÍl=íí .ˆ­¥Þ£ëŽ˜ÍÛY¯½ýv€ûÕøKÓ)6+Æ6eÝf DÕDÙúhˆjµA÷Ô]ipƒDÚ•MtˆLx1‹‘#&pWºZÕý¬ƒ\ã:·Æ<@¤dJa¾É›‚K`ÇÞŽñ*Ü匫[‘G’ÐÕ{Ý{'©(ùô¸48òÈgú U¼¡pÇÖ™ +w_Œ¯Á‹@£™…¹qãj#vu'úlë fïº ®¤M‡Ì°s'Îþgnظ´á¦àÃüIbàL4ßîœt°™)*³é9ª¹uÒs€8äHäÍs +ÄàH~Áñìà™(”oÈõeJ7äÀǸPžÚä”\ç&öAsº6]!zCAÄ&‰XS×&“½;‹¥/e±DsÀ–ÊE "J”ybÊòë)Æ–ðèa Ñ(“YÙ±½7Á¬kð&¬0³MŠXQÖàY:Y%"(Ëm νÝúDîn¶±¸5ÖG7Æó>Tx–ñk¤±b¥•`Ä&ÉšC>¹hÂïÖù5£º÷"©‚ Éý]üÕ¶Ù¥jÒ%h Õß²†{¥ãÏ}ŒS'KXBÌ ]¤k†¤é[lܶ‹cŸ‚îOôûTúãåW“˹‰µ??ñð<êo佋çÓ×ðLÈÀQˆ>ZsÐcŸ³BèmöM&”–“BJ?×,†vÐñÉyäè=©ÞE%0¯Õ6j¾…/„ö,„®˜«YÕy “«oÇ7±z£íá&hH¶mÓ¼À  +ŒE µm:‚ ¾rqÊF'ô'Q¾ +W²„7Ëö /F–­ÌÁ&—v’­Á0 ˆY{ô +Œ©OPt¢Ši¾¶ö4óœÏœWÖØ‹V(m¡IÛ>Êáéi€u/ÐĪíXhË„a½ªWGå ´}`¢æ‚Œé}˜iRâŒû¯Áýg¢ÎL òò¼"Fð(¡4åù.æŽø¡˜ƒ°ó-àH®)ž¡_£' < “Ö„ÂætÝóA ÖÇü +©þ„™u½® +Ÿ„Mtй<š_ÇJ’ÍEŸQ:žÁ¿KOS9 +p§'mMsŸ¨Ûé_JÞ7ÎC-á.š;Ð={ñªŽ hºÞ<þ¶ÙÊWóÚ‚C¯þÏI¸`ã#“pñçÉ!ëlçù “pž7PÍÕnABO›§ rÊÆ('Oà<ÈîqîAªG#Ñ€h¶xwò¡Ø0%D|CÜ6/Sª§@]¶'m5Y$긖Ìj¹Û°¨‡ØþßmŠ7H„= ä/®Ë‰¶ÊjìG?«ån R±‹ÀN>Þ”¼@XPvp©¶×fŠi¾µÀkbßšæÙŸÂöÜ -ŽÐ§MmÅ㚣ê~°ˆA¾q ;µ ƒýnª#º÷93¶4Ô‚\4y¥˜r‰á÷OÕ'Þü)ûpÒ§Šd}LZ°“H³£†µÍ­ö à54ìTÐìcÓx…}œp†F§É(ÉÁaµyf@Å ¸ac°!”Áë<mÅ“¦ŽÓ5M°;à•€¸^ ZÒ£ù¸9~=Èy€¦E+ˆÓ¯‰ +¥¿¬†{ä‚Æœp{ŒÒr‘m°3õØIïìº*¿‰íß)|§³i± }hêÐ…>!MÓòN×r˜DöÞj˜jžÌ­£ù_‚q ‚F¢7}ªÕHw|7,ËwÆöæ9‹ÁÎÒu‰<…±xy›š£Þ?0ü¬Eàz :xƱr."ÊN¡¾7j&®¸üjÌÇðLPÎ>ÀŸ™'àG‰l²0‹vƒY»Nz”U<†>ª*]Ôã=ШM¹‰¢ÞL©ÏüXTÈ1ehWš¥,ÜÅ.t`“äÊÔo7kü1”-C¿¿Ä÷‚d_DjnZ¹´Íg¯wøö]Gžgî» \I:è>»¯ë¦N6šd¶MO491ÛF?¼û£)EíÖPV»u@Ì/¸D~º¹£ë§2f¿àç·0éˆ%rþ¯oX¾÷»zz8$oøÙóN£î8á-úóeF®©ˆºM1Øœ$0ÍORx%ÔÊfß|\{á">œ—u_/[¢_L?lÿxóI›Å‡Ym{CéåÜÝqë§Û ¾ù¹/þë Ç?¯ûîÍ©98‘ÉæKe:rÖM'\`ÕE›]²9|ª[P°€z5°[ÂO 3¾ãƒ¹;˜PEÏåÿøx ‚äúµÙCxû„Ÿ‚šfñ UÂ!%**“PñLëA¶'ýÚÌ5}ÚC$7,&êþe$r³¨NÖ Ï·¯ß2'ŠîUl;›M I0š*ݤZz TëoNð.)ЇQƒ;v‡X –ãÖA¡ÓNy0}ˆ‰Æç‡ARûA3¡ítè(êN™wæ_€ý §çÔ£µ(:‰¶‡j1±‡Sš-'˜ä©+@lÐ +$a¢@Û L”J±45It–J)›Ã›@éÀ®tA”¤ ¼Q}q—9òx`ƒmOÜãÚè_å¦]οØ4TÏ[…“ÍNŒ×ì+BÇŽîžp¤ÃÝšå)Éc’ÝL>•ÖÄïHb©·;öà㡈¯º%UÁød”ËÃNæ½eUo†i”ãÊD‰„ìß®±8êMláÔ³tgÓ2‡È9;×ýý9œ¬{¿© +Âx³# ¹Þn?µILcϾßÕ˛ϳ£_ˇ£8cŠÃ-¡ æ‰*¦±U;€r‡–³¸n{¢aA#ÊNÞ Ï ÌÏTÍ„›¦ªÿ \äî‘|®… n¼c*|æ9맀ÀïÊ!èþ; Aß:pß}¾Öi‘¹@í²=¼‘ö 3JRïÜ­ÇÎÓˆë*x¢æc·“眎Zzª=ÄÞÂ^RR ø@_¿ ‘Ä 0„-ˆX…-Ý“²_ÁÁlŽ +É<•'˜òV'À¶ðæÔ—p2Aò¥4<œëÞ†9a~®ù@¸*øÓAûÕK0ÀþªªÛ¯É¡M’—Âd±D™?&Swñ9«È Ÿ$ïvDAnÏ ™’>Ìàª(ö+κ Ag…-TßvgögPÃëÆž:|{,‚柧*Ø á@†!nv°a‚|ø¼ô‘…„úgb¼w,ƒ·—œƒˆAÒ)fËïe š8ÓÿÆ2øûÅ2H´»8YÄ¥V²B–ž¤œ(÷Ð ¹,x8Õ\BŒ\=‘4v¨v_ ±:C¼‰çÎÎÉ3…¿Ÿœ|´Çó!Z”|ÁjñÆpPšèÔ–?oýÍØi‰²UóEô2‡ê”38ÏKÁ(H:àúZŒÜã!¦ÍŠ¹G‹TIÙŒŒ »Ô„»”æ±7?Ø‹ƒ¸\áX%o)·3Ј“wÈÛ}¥ }eój›;–]¹C…å¡ Ä„dÕ$²¤]”7j….ÞŽŠ+L”î«’¨ÏøÂ-ÏôeÑ'«tfñt&+M³5Ö4[.ÍÓÑ1£0¤ìŒ©/TiÜ‘¤zÐÚèV¥¥ú}ÚÌS@Õ vÕ7] À9¸5ŽÛkÝëíÓL[³Ô_ÄÊÊɱRö7IŸí™vþ|;“c5N×Æ;i:$šÔTàò¹CðÍ›ã¢ÂBÊhä ʉ˜£ Ž‹ —Ô@bâ¡ÎNŠû¦û–@¨zûÀv…Pfˆx°7õfÙÎK—¾2ÉŒ +*=Ž ä µ<U2IMþÞ~ˆŒ"SœØ…TœèÓ–Q?Úpz^´+Ô{¦S¤éHM™R/êNLPáƒE!A’yf¶8ø®Ê`¨¡îˆXÍk¢˜\€Ì‡çn‘¿¥:¨“ãØf{RŒªèc+f¥ÒìP’£–Ú©,鲕Òì¼>x¤‰t/×g›C#yÉÐÌu©j”Ône!š¤Eûä0jɉèØûí+[ 0ãï¢ñÔ6˜¡ +ŒÞpKSl'Ž¥lN ‘z‰©Œ +¢þX¸¹W £—Ô $ä·Ôp@Š}¡ºËjwuð$Šs?Ù™UÕlk•FE$Z*PRJÅ_…d5¶âz‡À¶­26¶cTeAQ+ÍùHȉk/ÊêcÅÄ¥PË”ENYëÓ{ó'¢Ê9?¦TqÍ’¿»¹€* + `ÛëPûÞ´XvQ}xFïóç雯þè1ä( žt€÷¥‚k³_&uîU8Ý-­gVH؇¢4B˜ÍŒb[Wûoa—ÛiWVoÒÚ#Àh.¢GÒ^ä•ýã¨ú/y¢”ÉÏ„pM ÁH˜ZYNÈøò;6@È@ÀΓ Ð>Ï„j‰/%uÔ‚´K]4á²íz˜¸ú¤üFf†'…§¦eÐë¶÷í]Êœfmâ–þ ð‹ù‘þ˜*ðhÏ<Û« B†]|/É{ÖuÕ™)ðL2ÞBF¢ŠÔˆÝEbÃéžà¢ÙFw̺T2ÌÿÍJRpç±²ºMs£þùyóMŒãy5£”çTþàsË(ԯͨ©PR¨.u5‡ù‚ÿºx|¬R^¼Ÿw2ÝÊ QÄÓ6ËWÑHXuy]ðšø¾2Êý~)§‚ÙOQ7…}WЙª³l‡ê?ÂÞÅŽÄAU!{Ú„ogXuyÎé~€Yåé½Äo9«ãò˜ð<U|qL^R¯Tdš½uF„ÉN™„IYúA¤ҳ+î/Š¨„ µ®ûô=Eõ ®k|Š÷C)žJ|¿M åÔsuŠu³ÝÐbŠñ7ÊÃÓ³ÝÞïÖ>“¿¦®IÆÞû¾>€°£ˆ¨®º&TÿzÊž_¯Ç2¾x=Þû áÆäJ˜Ã Ÿ¶k1Å[XÊ Ãnˆ‚u³©‚|{3ÊøuVWL냰\zùßüâÿÕTîÿÉ_þêWßÿòç?ýã¿ûéŸ~ÿ·ßÿ対ÿ+Ôï;uûJoñÁóêËþ±)Ý7ûõ³_ýâ—zä¯~Æ­?ý³üÛïý©ípË`sÿÿø³~ú'ßÿò;Sá?ý_¿Jÿçó_ùÿ˜™ðÍ/þöúí/¾{ó8Þó‡ïù£¿ÿÕ/þóßþåw?ûù_ÿôÏë_üÇ?ÑË0‹onþŸù‹ãýèûÿä—ßÿúgßÿƒ}þý»Ï=òÏ~ñ·?ýÙÏú?ýò?‹lXJb™PgɇŸþg?³)ú?~öW¿ú›ñCG&<2aúÍ?üßÿì¯ÿæWŸû¼o¿ÿ/¿Z¾ïã›Ye[²Ÿ}ÿw¯ öÙIúÓ_üÃßmwåøêø£?.?ý?ÿ«ñKþ¹ñÏ?ýO¿øùŸüòg?ÿ•-Ú×_ûåŸ|ÿ×ö™oþðÕú[þrù_~òË¿ÿ»¿™ÏyÌÊþ_/dÿþ?ÿá«¿·M{Ñ.ü£ýÃÿbÿãÿµKÿðR_þ·—ÿëÿ>^þŠ{ÿô«¯©¯û¤"qþë»`&"ÞzŸ×¾}í€Ķá·o~»»6~ûs}Ìÿþ@ÿˆQLÜ“d ÿÕšâA6œ+u}b:˜\–qJÙ3 oçIèîøô&´Œyú¢„É VÞÙ¯˜¯>A™t•å?#¶Ç¯ÖbÞŸ.æ‘%'SçÅ9~µ "*ý²½à÷ÒÍ0`ÚߟjùýÞþÍWÿE?/Ð9éúI-xŸU¼I·ßT¼úૉ0%-ßîòíxzÿ¤°qúÓaÛä—EÓìi7ò‹¶ùÅ_x•c<™\Ü´8lÛø0%æç™»ý«Íó¬ãrƒ<|\¾QK~ÑÌŸñdÒ^¶ß;§‰ŽÍ{L~'»óøœK—¯k ä“ìY‘vŽ‹Gö/¦8¹Ô1Ê}|0“ã[‹×¼h +p^<ë|€šVüjÊuÞJ+ö¸Hb\ìó÷Ë^‡öþÁ?Œ•3Sl>ø˜{­ê<Ów½¿:7TÖç¸~19çñ¢—G—In…£¦žƒE‹?ðÈq±”q- Øk r¯4džg/ÐBƽľ_¶ßõÍóÁb…õ“„òûa\.lQ]N÷5?ø"¢5NÉñìz\óÞc,gxîërÜWÃÈüÜ ó¥²!ô2Ÿ¯2ö;é¡9³ŒM2䙢|ä!ÌË™SdÄ5‡ñl åÃ^¿ø|^wÀ7×”ñ”lÇ<t‡Ž×AŸ5î¼ç§±yÝC×1¾BTlco;Æ_Á½Û/øæ‘?¸CÐ\×ùÈŸ[¨Ç¡Òùȉ³^ón‚öóòÕòV7ð§þäœ|ýNulß6>ƒ–µç@֔ǚRæåFÈÒ/ÚÆ4Ó¤;/ö4öÕAjÔgèvíà÷–GÞ™?UûØáæÚÍG$5­éDÓÞ~Øë//œ_\S›ZþçoçÝ÷Þlæq±ÎGGúÑ@žsÖæÜS4ðl¢{|uwð¨¹íûyL9}”çêòŒ!uÔÃS§`«ý0.—ZÇTSè(1Ƥ¶³¼^^ž1Öâ¼_ÞÊã2Y3¿œ€Nð‹½úüUªu\×â«óÍxÀ9;J°Í{oÜyÿä±ýK¡×hJÄt¤çw¾æÐŽ8žšzš’Ò¦v;„¹ Ê›?hÓü0.,±qÏ{|}ã!xè¿áÓ¸wî;‚ÞãÞRú܌瘱ðßNób+/Û{ýâÔ¦TÍСþð›_H³õ¸ŒáçíHM!NûÒ¸HÉ/ +wÞzßóÖcÞ +»ixèò]¯B`J|¾ìθvlª¢1êcŒõS1ï­ã+´gž§Î¯hÐZ¾`Š °Åûx2ƒûa\¥wdzõ¼¦Œc>ݬö;ÏcŠ +ûMÚÚ=D}>‡Ý|Óx?ÅJ:ký€ïž9»Ò1¯#{ÁYæ— :o +ι»AQ¿æLPã1d^+SR«úÄ•aió …×}ûºvwzýêwz¾#Ü?î¦zT—¡m}½üþ!¯2ï¼Ï±Ú ¨<2ïÁÈž ÌÝý¬uqÝïûý…iÄ™‡)ƒ0¶J¹¯)û5¿–Tç#ïz›;>÷¡…2jlVYàÛÏ=¢÷ýlØÖË3®ãYª5Ƶ\Öó•Ar¨ó|çóe™Uÿ²)tÓ›ÉÍôén?`~âó±¦pL˜—y@ò0Éíâ[ïïºæÅ+1”*Žäy9ÞyÎo  æ¹ØÒÔüÏ—zݽM9U1™æ< +(zìì{N$Mvã^”ϸøŒ«ä7:±æ: +jÇ·9»åþÌ9…ùáïšÊï|$4ÖÛïUˆ÷c0Éâš +‘Ða•æ±ÀÅÎÚÃqä©äÎaóákN‰Ë÷´úîqä!í-CUŸdзðÍ3å¦.æƒqŽŸ­ûÑGsgTP-eì•kŒ‹Þ; ‹åuÊÌL›CÃÕž+ü~=ï¿à³Ÿ–_ =¶P.g›‡ân¿y3´ì}¾lmÆS~ñlÏe¼ý97užõ£¶ß<4ª+üÝȈòËkø˜Ô¹“Z°­Üq…!Ûý»xÜyÞ‰F÷Oƒ­${™¿v÷sê¡3—ÏëG´)²Þ^ìÔìûÅÞçñ£Öùy@þæù¸€öWõÍtªˆÌoL¡×¼°³Öç÷"¨î4 éFWÊ9ÂG€z‹ç°ÃNN÷ëšcG‚Rñt°‡ã–ª‡KÊqéâÕ¯Wá´¬Á·ó2©<]~Nj Ÿ~‘:ì—íÚÎE'Ì4·ˆb ?ŒËÕ›|äÛ¶©[©ÿnZ/´õuhí²qÁF8†™ªÂg-Ç>ç°¥<ײM–Êõ\ŒBœG<æ–_?ëý ¾}DÜYÚ㢗öˆ8s7ÆýgÞD¥Zë\ ®òXrì’újó7jÉt¹ŒX™]tøJíiú®ý" Óì8ó3ôX ÷«@yìØÓùñ$Hü̱åëõ ®ó‰¹ÜÓ@™§)rÎ§î ”Ú§Iþ¨q{èýÈ‹–¦Ûó‘”>ö¸æ`§»Éµ{ ßÏ÷ý:…ý~ûÄ-Þ]NöÏ#WáH»‹µ½>·çG>ÖÛ#žïã~Öë|ì“û|}À£nÓ¼ë­[2û¼¨_íͼ̧>¦PysD2:,nÎô:1óN©ŸŸ™í×< éΟù‚q­ž¯ŸÿÊ;ëgæïõÞ#§çÞV6«²YØEG~FPî)ƒsŠííí™}ÇvÅeÆM{u¦R'DúÄ‚X&¿–{y.úA¸ôÓëHCgÐñêg]S6ƒø²}ÿ뇕1“”íÕûù°24 ñÊ:Ÿ<=õëÕR’¥‹užQb_#`Ä^G‘§Ìã[î‡v¤6F1w`›Íó ÔMeh*åÍÜ”'ZÈ¢<žÞ°\ V1s¼ñ,s½gð5 +iç®>ÏíÃ$¿pÍ´kú¶ÏÛ©}k‡MŽ¡<Í°~Ý3’IÄäÛq¹ŒtÓPàÏåûwÓ·ñ\>ZÉ›™…Â{<ïÖûc_½ûˆ7Î5½¿£¥+88ý|´ø«p’[ùíþ!S©é°?1ø™¿ár™¹„“JÖoÇe͇Æã÷ÛýCÆÓ/[Ü.lwš1{ýé…€7ùí¸ÜZ—Ûu=ÀÅæ[|óžÁ\þP§Žæ²‡ဢ]æÛqyR6·@¸˜çÇÍt[|îx!ˆUõ ï´pØàITÚžÔ²ƒ•ú¸(ÙO×úÆke(D¨í¯ü(Ä>fH}tâqÏLJ6×öì/ÛïÜÍæ«ç'ð0?ÌË#Æpy¬Õ/¶{JCU˜Ž‹ç4̯ºâêˆ`=Öykv›ý™QøÅÿ¹÷ü•$IòľèO+zºÄ“©eèO‘Zkùòiýªê•®®VÓÝ£vvfy{3Ç%¸ûrH‚I€"Í<Ü<³DI ‰­í鶊ôðp77û™t­E¬#Ïk{üG ù(ù­¿Ïög?`ýe"$ +»iêÄl£ÀˆŠy—–ã/%¯äðȺ0s¸NU¤üþt\ñB[^§2"…ç|9@ï²Î;iùdŸmÑ¡i–pž~:¬x¦ü¯Ñ€tuÙÐ)RÎSÀ}¢¥C“_—àAðQ!“` -€/“ΟÎ`}î(pdmr°DšÎ†@èØÿPØÈ^|B'Z•óå&X™Æ•.UK$êë°üt´hæFK_Ä¢@´¤ájø'Éà÷1Щ³U"jL‘p…Œ^A˜™Ä¦ÑIÏþÍUg[b<Ëw1á¨/r bí|öüocxë‚Æ„Äünþmœl8RŽòˆ'Ú¼§"î0äDµ"ÑFNö—ÏòQ‹°ï33ÃnD9À°Íˆ?3lkSp×#Q•*‹_pâ àŠ½‚ý™!YÂRçH—³åÔ>3rDc“Ôa2€Çµ“ûC”âQAAïç~\þì¤\ñ6B2ø,]#ÕíH¿Š +a8t âòÏ ØÔ)pºvb`³>Ê6±ÉwÌ妰y“×ÏÎ@¬‚{Kˆf¾¾÷D¶U}ƒLDª’)×AhA,Óº@øž8‘ѳ¶0ŸÌ ,7h ä±ü}½CÒô3š¦JÑGÞ&Cÿ"N›¤I§–"ÌQ&Û昽~=/U&Šð¾”4/Eì&º8%ç(ŠM‚€,¼%‘@3UMî&3Hhðk&èYÂŽîç/|:ƒõÔL™ƒÅirjÆZé:-nSšÖýýí5ÈnÒ³r(ãGU­¿}0lù(v)³’É+üR¤Ï~`T¼@V„‹…ˆ¿d]$Q`Pß7}¨úö“ß‹DÍ!¢*¬¬jô}1ª°ÄÑ÷¢!QàòO'@Ð;þ9X2&ÂÉŠIdŸÑè3;¯öóm^$šŒÀÀN™ð#3œ¬ÈüT-$úѤùÙœF1äOç%ÄÖÚú>„»6àØïk` Ð#xÕ”æ"‚Ådû…Ìñyx3aÙ}ú:œGìØÄÐ!m‚Èî^…é‰e“(G:‚li¾aÀ]ã|2XêàÇLO ÿ;ŽëƒKžãXþü»¿áíÊðAÝòÁ8›§BÙRØŽÁXªêûÓ;‚l+¤ü4Ûç$jšx–W–úDGQ„õ ävók„JfÌÇÜXëö—ÒÄòán 7°,Í+Ó¢v^ §Ú:¦èÿœÁ$P“dx ýÿd­¹Õö”x¯;åp7Eþ^\P§¬O=K¼Ãè këŒ0Þ$h} +èP#®‚âdzlÇWaœ¸þ‡ fó&&ô{ÕOÃß;ô¤a“gÖys8¬I¶.fÅÄì¦p®ð—Ñîp…L/3Ÿ¼¯3«û!eäZƒÑºŸÔ€s\kcëeE°2Íø û„øù¶#¨ÂÿÃ:¾OZ›Ä㜇‘ÀÇÖÄaÀgÄ°XZÌ¡6 [4Û K “å¨*m$ˆB9/UÕTbÕ/ãÊÏ'bQ¯OþSÿòAGÎ@¤ñexÉ–OÄV±PUUEЉ[ÿúú¸¡³¶›ü¥17>ÂT|HD± +ªïˆó‰¦eŠÙ:’LJäþú¹ˆCñv¾N±ÅŒx³Bÿ§åïáØ~ò¤Müñ°e•Xi°Ì˜bÐK¬õ‡‚< ˆC§§xí‡ÿ¬Ͷ.}( ä9À"gU/®+ügy†¡Ø*ß Å|+¢+L`;Rš`M@‡È¾ožŸO߯jË,|™€y¼3#YÏëxvš°éYK$à³qÅ3m™†ú×4×GšB¿˜‚3ðHЂS—-éxjèMóÀBJqä,?:a+”AË'%øˆ +™Œ¸Ù˜!H‚‹ó¨Oô¯(ä»`ø ïËòß3utò½þ² Â)Y\»bJbä$úœËÃ'Û‚›Â„Þ +FÑ=’œè×ð1 z>âwÞÿH¡D¼>CÌ ¢"|–ÌViÍq(Â(¢¿èËÑ…A‡¾&Í‹Ò¶JbÁ1mò)c{ù{0¨¥/H’}æøÑX1¦06ð¦TÛZû’d|R]» ãElnÑÂ~Þ—%”Ö—Ñ|]5±/xU;¥4瀑pT N™Fغ"FÅþ´³Â¨aØ›~ý–ð1³”`8ü½BjJa:­ ðQsÝÅï†dË2étIoœ)„¦Â/º«"ÂVŽ[ï ñéÌKÆ`„,yÚ¸O¶7`3¿¼Ï'[¿Æ÷ Ǹi WÖ ‡W©‚½t™U+,mŸHé}l­g-}Cða=‘xÔRˆè˜* jôU¨ÑÎņ>6I)ùºÒD!imØå>Mf +oýgÈß‹lÐ ÐŒÏÒêŠ/1Iž­--ìÀ¢Ð³¦jÈguB{üªAñ$¿ëÉ?‡rÛ¯ñ? ÏzGM‘·À$´áÝ[¤ØÐ|xÌÛ#J$¬3]Ž«ùÎ&~êüß[)“Iù`ú‰wx¿³BFºeÈ•±( „ ëPâF•y˜×!!¦’ZvhÁPÄ0"Z„˜° ‡ˆö2r!#,Z¹¾@5ù½%2Ñ)Úk2Ç€·"3ijªn¼aùÞ:ìV#œù¶ÏÅþïuR5"õ–H†3.üžwijëXº" 6“Ô¥è³|€gû/ÊÉjò„òÜAŸ`Q¬!ö:¥QU’ôLf›:Õí‘'F•æ½!¤užñ>²ª!€Áy½y‹Oû#øŽÝƒ|9lj~$Ÿ·ÒÉàuHU˜:óq”¬Ëañj¤Øü¨ +®¢Mª•2 ðöC]#„%M[ ZëÂ-ˆ?Ú’„ˆD“°·­R쇵¤Ë‰ä™ŠÉ„’máãÂV˜Žö {it ?€ð4 Óú )ªƒÚF|n´‚ ü¬).U}QkpA&¦Å“GhCÀ§ *Sk¾@0P_¬ñ ¥CùPÜûi@Ê!J…¤Œ{c]uçø5Qe¦ •ñ Q!+NTŽ~.‚dLì2Åïm9 CYâ«Qn‡Ci@(­ ¹q–5Ÿã1 ) E¶ñ†ZKÂ(iWò€%án.Œ|"Þª*4+v Éĸ¿ºü=U0YÌ…áN‘‰Æo*¦ X*I-îØ¥L‘ +ä+è•ÏÊ÷{ë|Bæ6|!þú©4YQôº6ëð[m²G¸ËH.­‹ÍUŒüV%69ØZÅ4)y“‘;;ΙTžMõD†.‹ +zÈ/PÖ™J* "~¯¢JÏZ~Ò˜yC +e³¾+ÆPeý¢åûÁýÐb•àŽp«V4Y_†*«Àñˆ0M΀‚Œ˜&,_æè$tQà†Ó²lÒ¿[‚-‹Iyv‡¾LXç&àšbsŽôdc›ßÙÊýš!×VUèuTéjð4!1€)â9:£¸™o‹üG©*{1¦(4[$á2Y¶áÞÀQ‚ ) ŠÉÚ}”ŒÂZŠ ànDàÈ(ÂðízZŠŸÂÓ½- +êR]$Ï#&¢ØðD8Z·TaeG1)#Ì‘9þ¶<7ºCÐ_á¾gCyÙ·x’_]â“5Fè" €M’üàÇF¢$&®(ïL²it¼2‘PwõuÙ°¨d… ˜ DEf¡j8X’§—‰ævX–Á› &‚“¼*Ù'®ŒoŽOdBý‰¶4%øY2Ø‹+Ó2lǽük©<Æ–eô¸´…G&ù•çTSéȲ,+¬d•ªë*Ûq6ÐSðç/ÇŽ™¦*ĶÄÖº–_\L a36¸N¥ddM>jÉRQ!ÿ?§g)WÕ4B·({ML'—E%:ÕÙóqe5´pFp¢¬³§lWü‹0d^),,IâY>ѱ)Y@7T:7ŽA9F†¹!Öõز ;EQ}^ï%ˆÎººZ&ò(~9§Ê¢!&ˆå›]â÷Œ +ñxº3 @åî¶ïöîÙ ¼!Þ˜BˆuÊö(UŸ”µ.Š³ØÖ|ð0˘JÍM¿0Å'ëÖBþ¡WdfŸ×)ƒÀ…(Á91að<§Uæ+Jíªm”Lð_uˆlQ²7ßõŽPæ–Ôz¼AƒPæHÚ´_NÕ2eé3°¨FhJ¡ó ›¾Kq—.kmu©q°>D¡uXk2KºåM?Àí#:]¡ÖTµgØ2E¹YLiÎzŒM”YímáUZ>Q%^C.9‡ù>!t2ywx™v!-òŠc®‹CD¦˜´6ù×Ð:²©¨›yEUVŽC6•^qFŸHïE¢a“Eš¨ü]>?£(÷Q×T2fu…øy]ù„-ØÊ&Ù0[)C_ül +Û–åØÝVÚù믥|TÓ”ëmù9>Q•Ê J·ˆjPÒ¿\uÊ_ãø)ù_ÅZ'ÑóG©è—@4©,]¤ž!QTð'¥_Æ^W°j4á—áü­ÑŸ$‹g-Y‹LÎ%SÖXÒèô/†¤Hd‹ÈId\ñ¬)ϹA¬Õ’¥v²”ü—S›FLCá s]ifùaXA”ù®¼~„\aä\B¦×È—¶ÎÔE³ÉSœ,ÃriðYYn‹Ÿ¯3 ©?Þ[hQý sÔµ+ÎШÊé±wµBIyš¡“ã‘,k£Ì´}È™êãôGJEF…ð&ov(^Ø.½œë6+¢ˆ½¤²²bqø I¶oÊÒïu›JtE¥g±¹´ø‘ü·á»EÉ&½×š#N¦¦cܧ-á*Þ$ɶF¡9~i’‰®‹z]Y>tAçµI +¯AÎvZ¶¬ÑÆd&Y°On?K¡ä)\/™Ý†dÃ’FS¼GÂXúb¼-@£e°Ê°T aqq)ÁÂ’¹EÈ_¦Iá &Î#.®NMf2[²lÊ2dw[p"¿Æ@ö‡ñu µy’ÖI{ØØÜ$™lSÔ'zø+(ª -SlP–È—[ÒøM‡|"í¬é»z’<î·Î¡æ¿Lá@c]‹îZ ¡¿¬§õCϽŠ£@@—8à#Þš£ÒEàÏ–á[/Äï-Y¼‚ý±Å³Y1)Ü`­›.¡¬2ÖÑOl*Kê^¥Hé:kÝZÇ¿Éž¶ý^54€`U]Gð-f“–’1mZTÌ&îAýOCdQÓ©«’jÊà/…™7’ŽÀ :däÛÜ)s¤e¾¥cã "ÜhóÔiš¬écAÃ!²ˆE9’íh‹AAªÇÖE|ÒV¥:±¥cÔV)4ÆŸ¤>¶VUHqEßäµWGfòhR}ãÙ6Í0é]–-ÓŽÖÝ|l?MÐVߨÜRDœ×æ•ŒiщÏÖ7*²¹%[ØòhÛÔ§swt*bD›/=[TGµ¶¹Q&jýgIÏÙØFPæ8Q—7G\˜ã$%µÀ$)iÆiÒ;gó Z N‹¢NÛ”Åkík›~‚“!ì&°„Œ6}l$Ê‹džmɤ^Ôƒêz–EZÕþAU¡<´”êdÉNæF)!Þ«å/«Bì +ª½°×Y¨ÿ)àc;2/Ùò }âºrœç'úDSTL#sˆ°ÙIÌ–Uæ6óÓmýõQ7GÙPö<9‰à(–‘£lÁ‘ûc“¿XƒnPÁ‚ƒ ®änR4ßéç¨2_e )fµj”LNõ'Ž¾Ñ2@>N$ÊŽK¶Œ¡;Ɔ£Ã‘µ¹H–åh<ìKdÇ!àIešx•Œªxuµ RV’•ÌŽ¹×üÓä˜2*hÁ·ŸhÊjx‡¬tÇÚ@ÎÔãÁÁÓb|¼àöF¡êÚídqE(¾L¡à#Vk©¤((ê8²Iĺ[‡Ãd¤·œ„’¥­‚×,гÒR¡ã„WóèƦfæ°u¥‹Mõä +Y€šHSa¥xà¨Ä óþH_å¿ÂL£F–U /ð"ß+:¶Ò„¤3eƒ|VøEЋéÇÒ†{ õ‰Ì& Q PÛo2!o’á½T:‚ì(k­ç»™IÄTç"Ú…"Q!¹¼Na¦¬t·¥!Œ³JÊNæHÌD… Yæl4Y]×ØnTØ~S/"–|Ðœ Ó¨÷„HçgLÖ™‹=/Ó°ñ(µ‹Ä+ÑHÃ+²øÁftl¸!Œ?gŠB™®& Îií£T*” Æ¯ØÞH•¿'4¶!9§¯ £¡IG<½nSý՜dQ6¡â§‰ ÑÖé4+ªH,büŠ6Ù§Ö׺ø¬ÃÈÞÍ‘ø“R&q«] ;R¤wq¢!‰¾K‰êzTUYàȆ6ÜwÑd‘!Æ7QÈ:$3•I¥ø#kXIˆÉ×1îå‡áâ ÿYC%‡‚¨ YQ–·ŒlUC‘êÍ íY;lÑ )ë*aÛO +A"b_:¥¦2ß„&ƤGz쨾ã‘ñõt¨­€¶ñ{Jȳ8†Eš-•ˆíßÅè-‡$šAV3óC +²ŽXgÓ@÷sÙhÈ.?¦!'€N]2Ì Ý1éY¦H­m‚h;’3ý3Ê.à ”³&Oh&8^-ɉ*es_†ð,r²lO!ŠöhK¤«ø}tù“6Q4ê„”ë.lº°@‘jËãÉ×J%X7ü X8ï~/Œ9bX}Ãû$òF¸6,Dc6þ¤ì3£Qê%˜%²ÉE †AðûŠÓÈ~+°?V¢möTó`Âód[<ëÈC+Ê©ÒŸ•xAÄaø³ƪ8.HTå 0’™áÈ‚B_3ì®’£Û~¬¦ú1ÿY?4ŒÍÒ×v”.&ø¨mRLsDæ >¬Š¶NIyÆÛ•SPR`ZNô3ô6SpºŽ*ù~êŠ_îJuÂÕÑmòYÈ·ˆdM¥t6U7ñ¬(ËW‘€´uÁŒa‹Äþ{Y* 1«b MV´bªt ñ¤c‡p|­%R-™XpU€(ñ´à›ä4æÙøÈ ™Nóy–!YQ0 ¥©tÙÔeV¥_ZˆÄu©éwã¼lÈÔ9r÷!YÑ(ò/*6˜jQ«E|Öï|Æg “ x ‘Õ•8]M¼LßH©ð[3#M0x&™ðð£Çh²rm¥-ÉS³R]¥¤Dî 4Y®`£ß·#È +“y=˜ÀLBÅQ)3ÉPÄ‚ip¤‰iD,ˆŇ©¾²#Y%b˜¼ÂǴijÂaÇkÍMYkþqY8 ;SynIVZqÆ’ ¶¬ØÃ{¶è÷¢Z——gË‚ñIÒ¼DÁ%ã‰?²pÈ_[®AäÚê©dá^äiÚ©|[F‡™o¼úDgÛéG¹P2¿-O^kF!p4M +ò)AW¤ 0/)” ¶ápŠþ¸¡qEƒUž&ço™ß}Ÿ×í*•8ê1•ÁÏ“­‰ß[r²Â$G¢"Ë+x$ÊÀ ˆ„óR4ÂB°’CY[–-@ 9ˆå"Qu(Q\ÊU=6åkRkN“C)OÌdâçºF[ÀsÊüßëä å5e¾vxE_ÊSDÞ¯*XÞ ‡M§ÌaÛ70qÐuœ¨ŠÃ(â„“"ŽÏ +Eˆi'½Œb—ÌÇ›bB n$Uò0ÊOâÞ8ŸHùŸ!1…\-î牢ùŸ¤-™Ÿ'Ç4º††×+k×”jÒ‰¢*0t˜ÉlfJZ¢ˆ¦ú- çš.A-¥C£ÓŽ +oDš Ôõi2¨lo—ÕCŽ¨ˆexáuèÑhDÑ•’o‚ôø©@›jù™*ñ¡lĉ;oV¦ãW‰ÚxÕ¿ +Â'*’i)‹· ûf?'R‘)²ý¿m€^&“xC?ùW@•ÍJoeˆ}$à‘I˜L!3Œê ijëò‘êƒ^SS±õÁA¿n 'T‹BInüŒ8ä¢u$ª—Ñ;¤:Tß¡ÖB¢Eò€:@⬴ y¶v[&í!¶;Œ’.6Ž³#m3UÜÌá©Œ·Žê@‡Úɪš¬Dwdœs£„ýÙIT +)"QzºMÇ‘á_ÎÂ%~ÙGÀ¡¶@Ïw#Ù%:9òªüQ1è`e¾J+ [`¤@ê +^¨JQjéÀo§‚%;ƒPÆ *µ)–…Á +Y²N5¢8¨º%B+:]¨5‰EŒ dU–¡û(Þƒ„w³úDÒ•œËůu¬Î• U,‹Ž°\6B`Dü¨‰ v¡Qw*"&B.b(¢GŽÍÛ /ë*õt°4R°ûƒJ¥7Ì\÷y0å3Dóg[&ùªÕüŸL€zòÙ2‹ùl~/ÈŽ_NÈ5¬CÑüu-†mÈ»JPÑ\X#r°~“u–ˆ<$Ø7dŠÁG ¾pKm&RÞK29ÔtqËÐFÚ¼è³.ˆ2ELßÈ4tÙÜÖ {]'oõÛýôý81ÅßPcݾÔïpwOT™BeœH¡&¾Ý‚hÊì3‰$‘Ì(4F~0KT£KGdeˆB‹§(.áÙ§D–‰÷¶ »Ê>ÏͬÜ&‘ ¿tC 7j,Jd–ÁD|XS(+Œ|$Ü€¡íÚO‡ÅWƦ´¢˜+¡˜Òw,ÙöRxÕ7ʵ8‘ªyQ}òo•ú‹”B|VDÓ9Q–ß²ÂG3Øh#)»¿èÔ•‰SI›óô;A4©¡ˆðMÿ²Ä–X‚ôŽÌA¢Øêϵ›üܬD),äRÉDÛ^´3Pq¼œ&¿,4e%Ú/²"QtÅÇr^RÈøÓ÷S4DýtÕµ¾”ÇÞHœç¹ÿT}`;2ú-ð-¿ùÙ¦tZêvÿQ6åþc‡.prÄ|º½q‰”hœôé¼ÖÝò…³Rq|;GvËW)ÁÃÒLjn­ËâËÈ ‘D”7×!Y£p¬Î6ºí¶lÊ!ÚÔ²M­#Ë?×z…5y›%Gú´ÂÂqÌÓ_h%E}=klQ— |ëƇy.H–ÙyÝ™amD3© +dJùá¯uº:…Êúx‰L—RM"~4}ú.K¦¹Û~¸…z Sõ-ÓT°Eä’Û’©ñö‹’D [gp!K©ÔÒãG¾6¨%Þ'3X7ösd¶­¼„ÉF–l]‹DF ü¶NõÔrÄôMš²ø½hÂ/©ïŠLt±ÈƒøÉë׌@m]ÿp#¨&u?çI1tÔÖ]ÊMcMýhŒÎºý£duÍVMÙ4“’kD¹+U7Q‡~|¨‚ÆReU¦¨äý1é´h²Kpäì(yšÏjßTMVÕ|8¯òºËç‡Ó„¹Üõɲ,E“•êšîPYŠ¶¾»Ã´Ö#w”#ÛqPƒ¬Jrà3_&E¢ì\ÏÅíßñ€ËNìÎFØ—·è|~Ò:uª£–ô÷‚,LFÜ•†1Ö®\G–‡cù¦,¥ª=¬÷P¨Ù…)ÓÎðuŒzÍ¢ÅÞH/ëy¬ã³#̧Éb%ѱâ^UÙ‡.èC¢¸¥€ù*øDQÒî\R•†FM¨ +¹óù·­Ž*·E+!Z8]:0)åÐ×q„:h…Ö]Qd÷aÙ ˜»:uࣔ¥vgŒê^¹$íî8´§“{ʘB:6eŸÒ‘ÍÒƒ +éy¾(5K×5rVm”e¬„‘%5 æÚƒµ®iX»'yàÈ'Z:5«°…3ëT2^LK[—e8šì§Zô.~§Œ0t4šÕG_°îÿÑÈôi†ŒÊÈj C‚CÙvD\Ã+L-Ê$çEÔÏ̸(»pšÚ/}—߇Yݨ%䥢÷‚ÌTºP‡­#ãë;<Ö«O‡èˆ±5ߘ1d’Á=‘åMmðo4º¸Ž“­õuäëÔ7¼]|Gü~}Wµˆ»}ú.j? nÈöŸš_ss/ȸÔü2)ŸHÞÞM<äiÑÖI¿8€(«Ýð´:\ÿél'ÝYÌ?ø· ×tNªÏ/K¯ß½¹œ½}{õú¹O,]ÝÜ=ÿ€™>~vu¹Ã©;@ÞQ£`³yðÿ‹÷ü~zuGáÿ·øþ£ÿòHïwTe§»³:Rv.ñÑÑIÛf¶ù³y«R¬$»G¢júU¥krçC2ö|BÓ£³9Äg‰ëžÃ;G_ð{RLºAµ@˜xá½ †e(†bZ¦8¿§S n[èMá÷¨X¦iƒ…Îøe½è2cfZÚÏ&þhó¿àHìÓ…öÖ{çÀ™Yx`P·`)žì {VÄØéâlñ&K]ãÍäý;}mÛ¬:Æ&¹óÅ–M ÝÄÞ FK£µÅ˜9õÿâœ4ÄŽÃ@ÃæiÂ(é˜ÃþnNçÍ?@¡ÂyÃwÞ‹Mà,6/Ù@ǽXo˜µA~ƧªìôÿßžìÙ'<'æÍ]6ÖÅ8¾ÌáTÞ®NR?d3Û¿ýc"ÿ¾‹XR´žýpP¼ž]!"½_ýÄI>kù·ç}8êšøÁ 6Ér¶ëQ7?ëøø@¼ã»ˆÈÅÿœEbhÝØÝMºõK7HëéIâúC6†[òÆ›7—‡á-gºùɳ°ÒG£®‰¼“,gºuýAúÅ/ +K.Ù.¿°äʽûÿ…háG”$Š¦ûøÌæÿ_Ñs2 Ý'xÐ3!¸ïÏæÕ=F@|†Q@ñÚêøoæw=±y3aä9¾ÂˆKÙ"£ÁŸ] 4ûHÂa^~(Ј̅µƒ-K ëÃ!0 +…qÜõë¾&Aƒ™¿4+›·ŠW?œº#nÆ\fù ŸÌÂzQ6…Ôÿ—»wöŦ¬BT_¶¡ÓüÍÓü¬N¶1B‰DŒ†ø|+¶‰¦oMq¢»s!vß„ñ¿ŸÏĸ¦WrÖ‡O "ÏÖàÏ:âäñŒIƒ^f¢ïés³ýŒdq¸•ûÉ'a݆i}ôI ²˜ñá'!îAëí£OÂj,müè“0¤…€Öÿ$t䙧QðQÖkjìÃút¾ÿ—åÁÿc4¥iâ)uÇ{ù TñÞ.Š£sï¨À ­tÔ¶Ñùè/L¼§Ä§®‡ù,qcÄVsTqŽð½æIíîÙUÆÿWþ飳7ðÕ_ÀBÁ\ë_¤+W_ß]\ê¥Eï‹bƒäÿÆ»|ýцnUÅÂßÞþŸ?C¿ ÿuvt˜jó‰ÄLJ¾ùÜ*goÏ2;èÖR¿X4ÿ+Ý›ÆY;™ëZ•¹][Úå¹îÎÒ…¹Y;uZçfå@+ŒãVÓðf^ï*Û:u‡©Â0–éÄXK)5obxÓt¦“©Îòí£òèº0¸bí »unÔO³Ý»x¦HçI–0+fiœíœ‡w¹áÖ½6šÇIo¶›¥ÁY®s¤ÇùÎuyô¬6c5NÕâÂn_jåƒdvµšZiaÕÆqyú,]^ì(¥x¶¯¸˜|Ĩ+¹a¡s^Ÿ¿Ì®­æQ4ÓVÜQªØ3k“S3ª³ÊüEmõMóì7ùÉ ¥¼ˆåûÑL§:}Þ>ý¾ºz[™¿j~—<ÑꇑLǪ­ôò<™ëÍZPñR¬¡æ;vebVfÕƒwiî<ºQÖ‹çze 3ÊsV?4Ê Õ[¶—ï«Ó×JifÝ}½4êñLÇë_åÛ§Q»5kÙúq¾}n•bV'¬ÕR™n®~Ø=xoTr}V= ©ÕíX.fµ¬—d½Df`×O­Æ™Q=Ö+‡{Z-l¶’™’«ù±S=‚¡’¬¯»‹˜Ó heX±dvgݸÓÉ5N +­s¥0Nf†£ù(–{Ëí¥½`ÚS³]³8²` ­fX+§Yk/‘}6÷7b5àç1§§»K­0Me‡a½»`V™î^"3Aµº—tS™Y>ÔÝ»rœižg›'³³oó»¨YNçÇÙÖ…7¾ËuÏ"f=¢À+úš Ëuª”–a»6vå <¼õºj¦WÞØõƒx®¶éü5Ï3­ Ë›”º‡!Í ªž]Y¸ƒg™ÎQ?7FeåT w5*jqXÞ6Ž¾ö¦Ïs}ØÇãX¶iúôý¿ Ïߧ ƒt~Pè?iŸü¶ºøÖ¬$ò#àá„ÓŽ;­¨ÝˆÚMØ#Õ›Á_%ó“dnÔk{éL;æóO­Ú"U©¥E¡Û<ü~|ñÓÉ«?¥KýX¶™ë^T–o»—¿«®¾1Û—fãÔ©ݼþëòÙî8l5Šƒ§Ã«?¶Ž2›—fó{ýO·Ï÷®‡—¿iŸ}g·NÃfƒ¢AÜéê¾V ™M8Åþe¡sfGNqzñâÏÅÁEÈnì(®éΚ«ïjßÞD+tCJIÉv½Þ¥æ-RÅi¢03êgîàicù&×:K±–Û9m¾)Ž¯ú¬Áþ²öeyòìðéJ£[¥0°Ë‹Òø¹;}eÖÏ’Ù1H*X"%?ˆ³ÖN*PKÀc Ü4o™ÊÏÍò±Zœ›¥C¥0 +›U­0°ªÓD¾—ö¦åéËé“]Üÿ»ÑíOI¯oV¦ÅÁe~tÝͳòüÝøòwï—×?µV¯¢¬ üãnAve{7jåÈh\i8ì$S?NçÀ½ûje_­ªîþ3¢×GX ]R càä€ÕÜw:ñâ"íkåÓâäµR™ï©¬s¾ÿ$lµö”Ê£x1l6agcV=ÁÚ ¦àŸÉL7n6ƒIw/^ØOº VÛmej‡;ñ<0з"™Xn;žX-Õ[äºwNó†5®¼þs·û$fw­Ò$ȇTnb”µÒQ¾÷¤±|¯•æ"ÖN2b4ŽQZ€ˆQӎij»惀RÜ´Ó1¼CÅ=ˆ;ƒ„ݳÊ'ùú ÈŠlu¾—.½­gð±°¡éòI*?Ôòýt¶·e­¢æF¬zî’UKpB/~†-þûD¦}\›Ýƒ%Aî¹ËD~’.Nƒz$”¯ƒVš¥Üq<"eäµNŸü„hCÕ8•ÃLë2íÎ÷ôjШéÅI±s^^'s§y¨UzíPñfå0•FZ}òdxùƒ7¾©No +£+³¾Êv/ +Ã[ø§š×u¯ +ƒ‹]Õ h%7ɵ¯ôÒêÂaœ„ÌFÄiÅ@ýYµ ÙŒ:C§vžïÞ±híÃtal”æ °Xc•iŸ:Í“Â஼x[˜P2èي Ð,Á´³š ûöR¥í„TëA¥¬•pº«¹›.lÅ3_ã¹@ºÑAv]S9˜*@²îãdnO+Áô€ió[»v¢{KXv8Ñ€–añ£V äÌV4³—p÷^ÌèèÙiDTM³ÞvŒ}Ô…3 ¶tÆ0`,·“,¦ì9Ý°ZM”.L€oYëXŪéÀØýí¨ó0 ÇMøÕpWq§ÜÇÉ’]^ÍÎÞOƒÌfÙCq—¹Î“æÁ·­å·®2¥Awv×X¼0k‡zy©h¯L½î9`ÎÎÉw«“ofê1Ö,@þõ àÎÉõå+Òq«å o`—-JÞÂ=m®ÞÌ®úáOÿãòùoSÞÈidû·Ùþ˜ ¬}^>1jÇ€ß@ÅìèwpèË©ŸçáO^¸ý»ãÛ?,žü1ÌQ6ÔË«ló4ß¹ÈÀAè\Ö§ÏGÇß{—‰üÀ¬,Í +H°‹êümûä‡üà¦:»ÿæÿ¥qôj+í2à4u.±ƒDN Ï àß÷•RšuXㄵÎAΛ mÜ¥Q>`±Ÿ.î§ +!µ°ÁªƒØl¼Ï¶Ža€OR9ïÓ8¨'ÐÔ•cPwq™`ÃídáqÔÞKæBª³†;ͨ²ò*W?K°Ȩ„ ¨²»›,µ:0ˆ”ýTi?]J8!£¾›Ìo…¬¸Õ¤*[ÑÂNÂÌÀêgðØ& WAì«ÅiÔéÀœÍò +l `3ø¹îNíòAHo•r¤Pªó™]û)/”.ƒ–|¶@mÇ@ +ÁÈøO˜CÔlÿ˜îÔ“Û¿Måúï5o®—`m—J~RÊÀ«0I-?Je:[ÉœA½4·+sÎ{©š„ $‹SØ‹lë¦}ôÓàòîà‰Sžç«³Ãë_/¿õgÕíúa¾{V›ÜõŽÞåû—?ŠíÓ|ë0^}ú²2yS™¼-^¸ãWzu-SZôŽß†ì: q£tÐ;ùqzóûÅý?Ïîÿ‰ ÎÁØœ\ý¦<É’îL¯¬*³WÕ·`¬e;×0¥\û4{Ôé‚è3ËËÊôEcùõìúÈ£¹¹Z>VE”çNíÐiÞz×zù´',,kìWÝiuörv÷ÇÒü¥;yÚ>~—*Ž÷µšê΀ýÀÙÓË»hÕNÀâËk'QdÞ¬>yªzÓTt“æ€rj§I³· + D­žr?ôÒ¡Í |˜Ên?ŽvR¥°Ý†WÀÆíA™ì+µ}µ Èg'žàöÚtçÅæy{þ&S?XÝè>»§çZa¨5e·äï&  4ýÂ+"ZMÉBj#¢5¢¨E0ÁHœ„ñ™¾];†ËœPè\:UD¡1»exóBç +Ö0™éï§ÜZI:ýýd ¸ä€ºý¤’0éô@šmÅr |CZþì%A––àEéì„€:-;ÌÕŽ÷Ôòã({²¶Ã™˜ÕuœmƒÚÝI»°hÉLk+büÃV,À 8 väçI¶užmŸ«¥)¨x-7(ÖƧßzã[Ðz…ÞQ?RJ#@ËÅîʮ´Ûè+˜½R 8¼€FåU2? Z½TaêÔ«£§¹ÞyÀ¬hÞتz—¬uÂ:çJu•*Í+³—‹§*Ï_€ye4Nõêa¾ãM^æ‡÷Ùî˜Ûð|”u€»€Câ™n¶såŽ_gzÏÔú¹^¿¼0ê©üˆ5O´ÒŒeÀº HÓù™7x¡¹“ˆQͶN °à܉R^(•°yÁbõúOš‹·ÑL'â4¹^"ׇ?°wÛ©ò®R •­ÖFw 8†¶Æ7*K0ºÃéj8]N9Ø­°Ö*†·Ê÷žg[wNåØ(,xŠ³cÇ2#öW!#b€°ˆ; – [öÒ.سÀ `cÀšgêG`Œ„ÍNÄláàT-˜d®Ÿ¯,“Në«€ºÏË¥³ WG©,à„~š òõ£˜Qýr7µÏÒ ñiOÍòP¸…öU¡ssÚ C3ƒ‘¨¦ ñÕÁ «x¥­ˆý0dÛM¸ »ŸbC”“)“í ânŲ»`b$]n!0`íXǃŋ»—ÿõãxöv•XVÏMŠ€–[©L'“r£fýqDÿ‡­ÐnرÆ/˜a2×S+¨y`éƒ8-´Î½Îe¾sËà‘Éu.Õêb_+¦òm³4Ê5rí“êüU¾ ”K¼0<Ÿ,ŒŒÊauúª6{‘ižÍڞꦋô;ŠdÚ`€¸¨}òs l÷*YV&÷åÙKä¢lÏiž×W?t/ÿß«•%Uo–òÆA§‘é\»ÓwùÉ»,pNeËô"v¤¥7~0`{ Ð\Ê 9ÛK{j~\è^ƒtÚIæXó´zðuqüN‡ +æ’ üßÈwÎsý›ë+ž:§µÕK£q¨WìæY²0‹dúIxGß·OH»S«²\–ݹŠÆáL´duö¦¶zkU§±þUÈiíkõ¥ &lGmò€ ÿ|û$l·v’¥€Zƒ(áôª£ûöÁ;«¾J˜õÞÑ›ìàtW+ìjå}³ƒ(îdrñ›ÑퟌÖ9˜WÕéÓtq1x`+uwÞ+¯q²È„Õ¢šë=ŽZ[°Y‰|r©à½Æž’Id; âv'nƒ^k짪ûi€µ »´ëeø µÏ+FŒˆ‚Gh=uYí@BØiij0{aÑ §xðO=ßê3ßk/£Nö"fuP¬b²º1£ L¸u¬â8e7ƒiOÉŒâv/Éúj~vœ ɺ7 éÀ<ûŠ üHWA@Å­àÿÝd6¨’6̶ö݃}È(ÐŒ £ÜÎå7ÿô?%óýûê~" (àý¾RU0`V¯‡÷ƒ©p´r +ßKg@*·4ãvKÏG§_+ù‘–k¹aÜ‚íhï¥óéLÃmò܉CJ=iw’™Ž’8Õ%þÓÌ®mÕ#™ÝTq6Âi[µ£îñ÷x¼öÅÍ·ÿͶšªž’íë ÁÜi¦¾Ö*tχËWßü•uÏv•rØl€ hxËòøÅòÙ_ºç?+%DM¥ÙóX¾a~Vã¤8|Ú>ùuåàmstûúÇÿXž?ù2d†Ô ^°ã€ Ý;³z jxö},? @¬{@æn÷Úí߀&µ +“ƒ'¿Õk£/÷ðiw"Ô®,ï~?ö'Ö½(µOžý6h•µŸzÔõâfú¥Ò¿…ÇD/‚1ØyT·cN0]Hr»¸])GUJ£ë”•ƒ?[1@bM­0Ï5®ÀÔkà(Â_ô¯öÕG!ô.ÐnkÍòa"Ûe[;)P£(O"Z¬-pÒ“vËk7¦×;ªËÝbC0—Àv¦Ê1­fÆ¥Î9°‡‘dc°èè RBE†¯èßéÞøQÄÝÇAÉN8¦88Ê,ôòüËle"¿Ÿ(ÀÈû©ò¾RœvúâOûº·st§½ŸÌ=; =QŸÝT~Ò˜¿ðúç;aC³+V¾™0ÊÂö~ºlxÓBû Ìv`€Ih+%@˜€ç›Þ$ÛXHßM.‡h…>,,èM£0®žª7+sòéL;Éšpf#vS)ŽÀòu;gÀOW–! Ä]q­´§•ôÊ€º^>tZ7)oU¿*î\…Œr"ÓŽY5X·°ÞŠ˜`ýuÀ è¥GQó«²5U˜çäEãàucvÓY> ¤]P.ÙœãQDÛOa)ŠÖ8·jÇ1ÖKÌŸ€ZLòªSËè¤>}Ý:ý~WGÄ +†Ý‰˜;Q{'æÀFdšGÅáµ]?Ž:¸U ¥A8d¶B€°p®#v/ÆVå´<~§xóD>¬•@UÁ… ˜v'êôÍÚyíà°éÉL/˜v·B —ó)½U+Z¶ßßUØ‹V†uš!4ëÀÛ§«¹S8Mj¶ÿ`/µͦ­vܬƒ~ÜèÏî'AÆVcpHã,iTò5)`6f¶£Yø0ÉÀÀÇä^”S{Ôvb™íÎQ¤¬—«Ÿ†Ôj0é¹ÕãG!àÀäN$ƒo·Ú`0> +;{Éâ.zVዪIg`y+À*Q£L— `­~Ô]Öq+¦(èšÎe¦~w»vs­C0AV'òãD~®”Žj³oºg¿­®Þ+űYšèˆ: àa€‘ôãÙhí½”«eÁØ9ŒØu§:ÏÖ2ÍóÚâ]iò2íͶӞ^]ÕÈC8×ùÉ×åß Ó×ðWñÌ@qçj¾z0=ÜU€E­\×Èõ’lC’ +í‹Ç‰üÃXn_oÄ2èŠáážë|÷*‘*³Àÿß?Ž€ªMšõ¸^ÝŽæ)/‚].>êpúÔâHÉ ÀÔ…S 1™¨#+šˆFJó8ëâ²;(²xºCá–, qït~^îµmPýFÝ\a5v’°ô+ÛÆòUyrŸÌO"VŒq0?#†guØ ÐªùÚY}ü¨,÷ÍÚNÊkÍ_ÛU8×8ìôŒÀc¤­&`ΘÞ8Ê:`6ê•C€îJq +j É€Q  øÏ]©"ˆÇíT~;žÙô¨5Xí„ÕOÁ¨} «€L8éÂШxã»bÿZ+صSr_´í˜“fŒS*À„vðp,3HRŠ)6ãU`Y”ÂÜ©žØîM£ +'@(ˆýDq/Q ¤«p4Àï-îg׿þ +À-²å8fu¶cyÐS{©Ú¾R9À±­­hn'’Ý +gv¢Ù@Ò}Î< +±@²7ÙÚ +8 w³é܇}7 €1,um?í>Ž²`ºˆg¨?ÜKŠ[x5¦7;³×†Ûû‡­à—#a˜^,»/îÅ]8Î¥à +l´/·";Lƒ гjn ²5ΪÉí©˜[à¶ÃV$ÆÕso‚)µÒÏÈzõÁž†©eÀÞV ó»ìž]?Ì´OÍÊ`C:?ÔJ¬}ÎZ§I8³­ãÂð:׿ h5ø*$‰îÍáF£Ôž}øUÆi…f|n›kŸ›e _fªK·s\ìæÛçZyeWWve™o(….X‹ÕÉ3§vfšYZìéÕ]0ó`uûV?H:t®»£–ðÔ¨õˆÝ7JËlë`{­|4ÛÞVÂ$oà Þ¤· +ؾ´d­3¥4ýUÈxÒÁ6Qòc³|`”WŠ»Ô¼ȱV:À?м“ËßM.Ÿkß™•KppB` ©^ÂA|;á&XWÁë°tŠÁ!Á± +«%0Ç™q¶qYl]‚YR;Š>°`Œc”a5Ìò¼Ø=Ï÷.¿ŠæÁä&™¶ÍÆ­nÂÇœqÚÅT“ˆÕ‰i Á¼‡AûaÀEÆGðB`U±!œ5;ÈV–1ÍÝçV0ðV$ x8á @î$Ëü݃à—[±SQ)÷`÷ÍÒQÔ\—ÜiV®è`6€|°VYéuŒXEødŒ'Ʋ`ïÇ-Ì_¹Ä?ª‚f ¤Äß=NüjKy ¯ÖblÈ´€0mo¦åºÀ–Àx´7Ûð9ÉÜ$×½ª-^–ÆO¼áueþ̬ÚÍÓL÷ +è•Ùóúê]óø;¥|˜,NÓÅI<Ó‹­6wÏ õ +œÜ^ä›Y;Ψ.ss³v¨ö7×…uÍjK·{VÝ93Ö85KKÀÏ…îåòê·£Ó‹ÚbCEŠ;Í´.$ƒ-æTW¬uá“öàìƒUžvgÀó¹þM¶w­€Ž3Ûû +¼§q¤•QÀ9îÜ?o¾×ëÀê!P¹·.7‚•—íÝÖc8¼©B"ÓÉõ.³0áêá¾ÖÙ˜.L­Úq¶}œœí(ë&j6’¬«ç…Î-|{s®º +kçpRŽ4÷@/­ÂN7žùc€úÀ?s à°wåe&l÷cPñõÝDN/ŽSÈÆÝDn²zpèòÝk@­³€$¨WwÈގ瀵ŒÂLÍM82Þ<È¡Ê0[À„À]Z*.” ,qud1nÔv¢hÕ˜Ù‰âÕ ìٞ㎽ÎI ).«.ªÓ§À<¡æ|r£N£|s¶UpN¦;hVO´òa>9?ØŽföb WíA+ Vb˜=Û³*åñ}mv_ŸÝg;— X³Ö*{ér„õÁªuÚ¹ÞÕàâ}eyÖPeö,Ó½4%†×­£¯‡·¿«}“é\hÞDͶëógNë ¤„Y¤1Ï÷.ŠýK§uTšÀÊ/ +½‹ÚÁ‹l÷ØÞUY¼ì~Û>zSš>3'Få°2z +Àž5@þœÀžf;g…ÞùðìÛLç4Qi•C»}<+Œî3½[­zú.ß½h¼TK“êòMýð[»}‘öæNëÜF­åû—)o”vGñ|_+Oá±|ïÌ?©.^—¦/€W걉"kË o ’*`4BVÌCÅÍz27Œ±>Èpü¨ÚŒéÔÒôuŒ5£VÕ©.Až§‹cøpà§vT<-nZÕ•Ó”Z*VF)n×JÃÓt¾kW&ÙÖ‘]?;Ntqð<ß½ŠUŒ'äÛ¸AWõ×ÙÁuyþ¼±z•^§¬uàŽ.Wßõo~]ßúç¥þéêþw…Á•Vš€ÊwÏa†Þ¤Áynp‘ëœ NÞÍŸüœ–rgÀµÅ à«ÖÑ›æñ×N÷–µo3ÍSÃhî UeÛçÕù³Êì©;¾K•fø9ƒ§ul7@læGÏÊ_WWï‡ïK³n÷|uÿÏÕƒ·álßnœÔ¾®¯Þ—毽és³¾R½q"׃ã̃ ì²Ú¢wòntõCçø]ïä=ÈäX~»í\Àǘ‹C÷¦«…ˆN¯%ó#À mÔâت,³Í³úüUapÍÚÇÀ¨ÕÙSÖ:Lú™Îy®{Ç<……êž~Ó<|SY<Ó›‡;z=‘eðü•;yÿL†ðdmþ|ùô÷³÷¹ñmãðmó>çmqü¤{ö¾¹ºîÊ4ýÓ—ãëïš«©üH/-Ð\]‚±ìõ/4wÔÁB©™l_Üåz× àí¥É,©Î?SÅ‘^^–FOf7?On~¶Ûç jàtƒ…ЪqÖ4¼qitÓ¿þntûÓá«?ÞýüøØ뜚µã° +è8Ó>¯Mž.®î_?¥ug6´\ß®:² +Nš/{g_7W/KÃó8k¦Èv¯AÚ§1Eí°<}V;xž~ÀèöĨ€ùvP]çÇOÜé}íàÕðêûÉÝÞøÜ6ïk«çÕÕóæ ìãmëøµ ¼Ô¿ÈtOÕÒÔn®2„dõƒWƒÓï.Þþû‹¯ÿµwú®<{–í_e»g•émuö¤{þ}ýäÇñÝŸK³çš7Ì5áÐ @Ê•Æ7 Ž««·NÿÆî^w/®LŸzCÐS§ðÕ•ƒ·îìEa|_>í®^^¼ûWoö,à´`ã@ëÕ–o +Ã;\íÎ9&-·N@6& ½¨]5Kƒòø²6¿i¿\ýÐÏïª.«wGZ›z.©§cN©LG¾â¢+5Gç–ì‘6¬Ã_U?¤Çáß‚°;bk×ßÜ;lª—™ØaM<ód{®h×™ c“þü\¬³í+,@y€ÐZ5€&„"88›ð—Ö‚µ-ä%Äœ +ó˜( r\¦¨®Š•ÕhýT¶»g¢âð ¨P½Åu*: å!•7Bõ.³df²ƒ:4\žŽMp¹YW´kÎptK:&‰ˆÐ€U"Òæs °ÂÑöŽÝ›×Úy#!j,£;E…'’ÝÝpk#X[Nìù‹ í•K àªß†ÚZañR}íZsýZ¹w† +WpAÈÏ™˜à_ÿãò`35y>ÚÜËËÙ©}h|™Õcñ$AEÉhG*¯¦¦Î$§vcÍ©[ÉÎ&,²Æ)Y=)¨:&3joûJ+žät´µ£tJ*ņËÔØ­LT*/Wî.._™Üºuéñ·œR—&Ò½«¾Âª‘Ι٢#Ø2sE5±x dtÜÌ€ÒÑ*›œàs‹þÒ*¯Ö“r½ž‡ŒOh Rªd&ö¨hÏ›õU¶èÔ<8{gùªŽJŒh¬OV“Áê©Pýt¢»®¯÷NÝš?ÿê/yÓ½\ïBiñ ¸p!ÙÝ),^©¬ß,Lž½úÈ÷Äò’‘Špñf°²'ïœÎÌœ+,_M´7ksgÚÛ·P>•™Ø¨¬\k›D¬›˜Ú¯lÜ–:çÜ¡Ÿn8Y !¢þ²3Öes Lv6X_5OQñF¼½ä+ÏÁª;Ò4s)2Ú`âm:ÖÉN_öVõdñ¥¸ÔêKcBtdN™Ë.Dê;Ñæ6-³Ñ +.©xG(.xss¡ÊJ²µ6µu£¶xji÷faãííüÌ~¼µªm +ÅU‡X/Nœ)Î^Pa>(Q(‰Hs;X]ó—ò³Õù+§.?‘lo)‰°+:A&f ì¡œ±Y:½mŸwðy„*'d§Ü±&Ÿ›¡âm.ÕTWñ@\ º +Þ—™ ×VüùY§˜·PpÃÄÄž+T÷egü…yÔWðe{Ru™IO¹¢;ÖŽ6WÄBÏîM‰•E2R²ÝTwŸM¶#õå³÷<'äz°ùé³å•ëñ©ý@uÏÌØ…*ðm´´:uêþ@~Fm§Ý\¬çr¶¸p.Õ;_\¾Â™‚ {B…Bˆ ’ó™Ùk©©oqÝ[ÙðæØD'T˜Uçä6vÜÆC˜ÂÀhb9“\ºg+¸7•¨-ûÒSjGwØü2™ê;¯'59l¡GnƒK2ºDö":² Çn훧Á.™H‡IT¥òŒ¿8Çg§AUÊíÕ‡ž{ÖGåðybÂìÝðÈÏ^Âcs½duñæíg¯=ýÍÍì•Wn¤g/Aÿæ篮’щ½Û—y…JÔô.L€Ž˜‘ªk¹ÙsS÷Ä;Û•©í;O¾¬¯@ ÄëRe9Xßh­ÝX¹øÄäÞÓ\z©ÔZ¯õ΢ެCì{1í +…åÔÔùÆ©;“gŸˆOìu—ÎÕf¶>»Æ$»ÎPÝnÙ™Öæíæ΃Þì ´ –ÌLÜá/Ø…"¬˜Æ•T;ÞôBbâ´Þ(v‡ag@¯œá†;RO57Ïßó\~jÇÄ&â“áÖžX^Í/\†NÁcó@†V:Éǧ¡Óu¸Ÿ·„|¯ºtÐ\¿ž™ÙGÄš•Í¦k›{7^@¹¸/ÕJO줺g¡&2ž›÷åèxÇ“˜`b-™•ÖÃ^«þÂEeþâÊÁãé‰Ó“‹çº+0_þ¹TY„šÏÏž_Ø`ÿ¾ç¤üìÄÜîÅÛÏBÙKèÉ„ k±|:Ö½ïžg"ÍÉÅ3«»×½Ñ¢ÑåÇ¥*™$³|f¡´t/›žÓàiòÅ%¨B,öÐé9X71?{úÆS“›Wv/觉Jj[BeÝ™V'|åå{î}Ž–jv>'”—øÒŠPÙÌÎ]R‚˜»ÀÍy ºe&ÞÔÖ“Óç¥æ6»dvÅò­m·”“#N”ú s±úzªµ–l.¦¶€ö ¸SÌ„+áê)±¸.æ W¢•…å½û[›×õN)T] àóó‰©sþÚ™˜r‡êk»÷l_{Ì*žÐ9‰@ƒINº£M.7œÜ ”½Ýßøy¦»í²ÍŃÞÙG‹k7ëk7¦w(.Ý$ÄúÆé[[wÞ€ U×¥åpm-=s.Ú9K•¥Í…³O"PZ 66<™Y.3ml57n§zW˜øT¥åXê¨ÀC…Š™/›<âZ¼˜jÄËswÐ)•(H¸Áš#PáS3©É3@ž€—ñ©s`”V6æ UL|~À" £"T…ܲõj0Ÿ7Ñ&CÕt÷T¾·ÇæÍ|ÑD¦¢…¥ÓןqóÂtqþB¼³K¥fp±@Š&iòdÅeO¼«Å}¸?í×(èšH‹ŽÔù9—T©MnÕ{»vo&Ñ\+ÌËÍìg§Ï¦wÒÍeO¨rý¾§^çƒöÚ•q›×HÆ.çÍ.¥S¾â:Ÿ2»£{—zì¹·BÙŽ•ID[»¡Ö^´³Ÿî]ËÎß BðÃT¥wN*-Žé1“¢T%_ÊLžÙàUðn6V”ç6ÞìÉÕX¸,pE¨¼2½y³³|ÙÁgµ…@±gbãzwáJtl2PZ“Š+Z\ä¤2kÜ!ØðPgt +“Zˆ'ã 7ÔvψCØ/ÓÉNn·7nˆµ£'uRïÔz§@ËBvÁ_Ùˆu¶J §oSÉ.âIDúQ7Þ7±ý@eùz¨¾ª,ÇT´¡tød6ÎnÈ°‘ q™i(ðÜÜÌ™ôä)"T2¸¼l´,䦥Új¬³jn;Âm“;âÖ@ @¦f#M;×Û}´T‰w¶ÂÍ : r +üÎÆŽé w´ +ÜJ˜XÒ»ƒã6Nk–·o‘±öáq«Ú½9à p“ѹc`ñ|¼«.ÊlL¸¼P_½ï^d’‹ù©ƒÈÄ.®G+ËkWž²p©á¯îìèßô!Ôèôr¨{Õ“ž1Ñ&2*ÍC†‚JMÍÍ´Wnµ–®‡+›B~ÑéMÏoßL6WƬ à(¨ä)w´G%zRm‹Jtmlxvý ”4¹£˜«:K'fˆ`“ŽÏH• >ÚªôöE•_Aš•kpÑí/FËó‰Ú’…‰é 0 Æƒì@è/,xSSþä¤7Ü€µÊ¶·‚õM"Ü‚¬jb²J{ î¯Uæ¯8C5¥]°ñyg¨‘ìì&:;ðHµOCÊd¦ÖÎ=`uàüÕM*5 À_;u»²x ææÌdlëà¡ÊôÖx?éØ,ÂAüÇVŽpNÁ~àåo„ÚW\–j›©6föhq ãR\¤<¤ÃVñæmB2………ŒVÅ•½SyèPŽIÔ˜Dðbj¹Y6Ör éDa¦¹r‰ÏM#¾,âËáb™ ·Á“šƒj„m‚â æ¢ÍÍhs#ÒÞ²Êj—DJ%!3©vpb²Þ;}gòôCáæŽSªé  B™Š™;!³ÉôÊÄØXƒN´\±&›žÔR!«'–: »iãR¾ÌL¢³—šÜ W—ÜRÁÊ„Æ,ôÆ)31'UÖ!-%šjoGÛ±î:;G%:*»Ç(†Ë«ttÊÈda,MH)ÑX‰·6dv^e÷z¢](u.Ú„‚I¨@B·yÒÎ@Ù¬*>RY¯,^r‹€+™ö).5£° ¬?¿¸}ƒ–‡Mn63+V·’S…ÞU:>má‹z*ær+Û7ÅDK‡‰Ra¾2{!Ñ8%ä@ó}ñN¾s:‚‹¶š«·ÒS|²GJ 2ÔDø<œi¨Œë2Ì%»L¬“éì”.ÛùÂ8 7»rï3L ¬B+›ÂýEÜÜ;ŸîîWæ.+ÌŒ”lU."¾¼eÔmtŠêþG>ƒ#hÄ* Ó˜;a\…°JLè_õŠÍ—bã¤ÚùµZ=à¶ÉÌ*tÜÂy{ \†`““‹—¸ü¤ÌÁj]¢ÎRaw¨í $ƒÃJÄâ PCíq™9*3¡w…åÅrðqð§ÛîHÕÙLÔƒ6…XcfõRu~ÏÂdÀŸ™Êww 3ç¥UwtÂ!Uõî0¬g0׃ÍB½ù@iv$7µßݼ·½vÓîKIJ͹í»…âítèZØ Ÿœñe–áäV¯IjÞqa÷Ä ƒ”¯d&NC¼šÜz°8sàOv&.f»gP_¢d@9âU;D2ÐB™ŒÌä7»­TÐÈÑ»ÓZ: öä7Ðn¾œ–ðƒüÆëÛ>ž¤OÃqŽê:»ÏéÍ ÊLÇF4}•ˆ¶ƒ¥•hmÝ ø휡xm#;yV‹LTØj€AæáÞ4„• ™KvP>ca&*aõäØÄt´º‘›ÜÇøœÒæ1âO¸®°Ò”ƒ®gb] J2Òµ³Y"X©l™±ÿöTÎî$Û»•ù«Å¹+ÁʪÌæÒbJ³[fvjì<8 ÄnΟh»}Y2PP:,t‚Nt¼é®Ã—·q •@™”Æ–j «û¨ìÜ¡AÕ¸Önv”XfC Ý?¢ÆLìåï¿WŸÜTXtËÆàæ<Éy1·*æ–úÃ"”(ÉËP"°° TÜoóÄB†‘ + gÅÊ” #MŒDÇkîhÅ.ÂŽ$à µVÖmSá¾—éݬ¹ñˆè!ˆ³Ý—E›^¿ªÌ»¢U‹7­§ÃF·èô§¥ò€Å¾xM¸ØsIy•Ã{Rï´ñ97pxn‰OL*Q–*òzaæljâ”Tœq *“kÎÎîÞù‡å6‹§àÚd¸ãúoÿÊ «˜_Æ„„Âäpzc‘Æ*@H}éR¾·Ÿh­Ó‘ª7ZYÛ½>·{k@GÈ8æ¯àyâ“›Õ"> l"!š\"œ  +ó:‚u$ÐpÛbùT¨¶c b#F‚ ¤ŸPOLÝŠò(›²3)”NhmÞÁQèÒl°{&F«˜/t¡D¼¸P„~„”:f"ýùi¡´@§&-lB“õRb!^[·PáA:¤ØH»ƒ ¸Ly•‹uû·À¨1“+ÌÏP"ˆ'ÆDÑÆ:›‰NÈ-ŒÜDBXФà~hÏdw?7s>ÝÙó‹V6§@P96&*3j €¼VZºšêî{3PÌ'T˜–¬|ÂÆÇìÞ4¨ É15ëä2ˆ;hgbǶãc&¹Ö®6¹¬„$Ó»äèˆwy3+[×ý¡âÉQ³Õ‚îC¹¬Ý“ëòE‡4¬§”žAéâIJ+ôl€‹×ãåôÔŠT›”ªÝTg¶¹vfbë\~a‹ˆ&<ñ&4i٨ȰWÙ(‹Óçô%˜p‰WœÁŒ+œ ä[§.?T_ÝK3¶úâ\¢œè,0Å  +¿ï gÚžHiXm³Q’… +[i 2~û4™šÑSaÜ‹*Q¦â«Uº¼f>H'ÒéÞ‚Øœ1pA#%9ƒU#,š•::ª‘™œ€`vÉö6æ ›ÊÎId´`aE\Œ¹#9Tˆ{åâÔRmy‹L–4.ªÓÑŽÑ7ÒG†´ÇFô +)f¦½I€œ¸Æî<Š‹îx T t|Ù@‰|ªŒù#ã¨[î`m¾D¤¾f¦#âø°No¡ì.ÉN† ˜Oa¡ÇÌ”ó9!k''!)€q¡·u¹µvŽŠëm£6Ú ›0º#ÇÆŒ'ÆÍÃjLçŒÎà°ÆñÕ æ-»¤¦7Ýc£¸/ÓÛºÒ^Ù“Ê]#- 0îqFʈ˜•Ùi+Js–>%­¶ &*“l-Î\¢Âµa¨ ²Qoª ÑÀ!äÁ%桯Ãõ…pc•„tËŸ ä¹øäÂþ¯Gdß<©µQQO°ªÐ‡ÇGŒr=íôb¥¥`na2C*ǀ̪wxíÞÔ7Ç wéÊ-6O^Ìo©;–hw èôF#¹Zg~sfuoãüËwž¸çÉžxå7üó?ùÃ_þþÏO¿øû÷öáþÍ'Òë˜ëߢ®qÙœÁp¢‘È6R¥n¶9_îÎO.o­½|õ'ÎßÿÄé{Y>¸yêêíýûž¸xó±—^{û¹7Þî®ìN¯ž‹”§0oÜèô»¥<*!ž0åÅËíúôâäÒÆÊÞù37î»öð·Ÿ}yûžGׯÝ›TZ(…Ée#¤Tæb._AL¶X)ç×Óõù™Í µÉ™•Ý‹nœ¹zß ¯}ÿw_þý÷úûúÛŸ~~ÿ“߉ä&ŽÉl'ä#å¢SÁìò˜ŽQÙµVŠà’ÔÚý:”·’B…¤dmnýÜÖ¥û–ÏÝúè™—^Þ½peza‹‹Ôb•UÉ›éB5–ÌT' FÍ7¦&æ–f®ž»uçÆÅ«çwωe”—±âN­•Ð#Ô˜S#2-jEiÁIÄóÅR£Òhί¬Í­®t¦;§v׮߼pÏ}×÷/_Ù¿vkýÜ¥Òä +ž´'Öqúój“Skvšì¤Õîrº=«›gVvB±x>X^šÝÛß½~ÏÝ÷?rû‡?üÁ{üöý>þ䳟zñù­óû•ÉI&¡i+DW”Ñc,ÁV6ÎoîßHÕ&ÅZ³ÙšíMíìl<ýô£oÿøí?þô³ßÿî퟼ýÝ×_}ù¥ŸzâÁÛ÷^[^ÝÈÕ¦Xp4³Ëìô«Qî¤r 2ªÅTIò±hxi~áÞë×^}ùåWß|óÕW_øå/ßùóŸÿøÜ·ÿí¹Õå¹BµÍ—˜{Lk³¹ýð08x½ƒ·:y” |R,S¨ÏÌ,œ;súÁ‡xú™'¿÷Æ«¿þàýÏ¿øü/ýó{¿úñ“O>º°—*d ˆË`÷ i‰CÊ£¦“ã3Ê bÖ/&©îìÜÚé3g.\¼|õêµëßyñ¹¾ý£—_}å… endstream endobj 26 0 obj <>stream +çŸ{æéo½òòwî»ycs}3œ(Z\^…Õ©¶Q&܇ҡ`ºÃŠ)3âöK±ù¥ÍK×o?õÌ ?ú­KWn<úè?ùé/~ð£ÜwóÒã÷ßó؃ìžÝ›_\(UÛÁt3;±Å†ë_]í_V[(¹5˜áXnqqíò•«ßyõ5`¥Çž|òÁ‡|þ¹çÞùÙ{O?ûíË—ïîÍ/'r%»‹ƒ8£´²ã|\eÖ˜p½Õi¶“´˜¤êùúLczÅJ±jÓÙ]J«Kn"`ÍNA¦Ç•[ 6Öìðꬔã;g³Dz¾PÚ#Æu&Dc¶j-(êb’s)9Ê jP¹0=îÖ£GÆTã› e´&€PN!ìm§xŠå­6Aà¡H(žIÕÚÅ­½‰…e_$jt£ZdPcÕc + £0Ñ£:bÜ@\” æP*`Â=%xýAïó +þP4,ÅâÑ\©ÜåD) •*Õt*ÅR£Õ¥CY. t„µ¬Lìk*™‘¤…¼‹Mr|8—.$b É/ˆ~¡7·X.g“Ép¹^£98H;°Q1êU¡Ã +ËÌxrL70fѺ4é¦()c¹ ÕÎÈtÄ1¥½?ŠÁÊ;}yÒŸC(p·ÎârQA_¤ Míô$SµµÆÂåòÔž› ë zžãC¡ÍúL!×#PE„Oµ÷È`{Xåø?¾1¤±òfBRšÜ°Ëîñù‚ÁP< ‡¢YŠòØmvÜᦹ8#¤¹@žWý›X.Ú ü¥oUÞ5 ;1f ÕZÜV2âàRÑ\wvû–ñ@¼E™8Æe$LΠÙRÙ )Žcz\m¥GÔˆÜ`×ÛÍŸÛ›˜ŒPQ»'aÀxÊqxÄÃÃòãƒZŃ#°±::e¡’ +Te¡ÔrPfÕɦ1™n¬ÿ}Ž8+¤qÊoÁhîW˜Øa-> +3b96/D›pIm¦G6™Þ5ª§F Œõ›=:L°¸álknõ¬ÎŒŒ©pÈt`Nцq£ð Cš™Å䌙ÿÏCÃÇGf‡´kLëTZ=*„‡ãÊ÷D›:»÷¤ÌDŠÜ ñÜŸ³Ú9É¥1•æ*Bø¨à© TÚÌ•PÈ•þ’Ñ’ÛØŸhá¡ÅjiòlªyÚìŽÔŽ"ÉŽóß5¤TaãzXJ̬Ÿ”«ñA ñ¨MÔ¨Ú¡±°ZD0ö/u뙤Ãíc#æ£cºcãšã2Ý€L¯E}f2jçRÞô¤Õcí`v +ãS(›Ò;"d¡ý‘¡¸¤D¼_?>~× ª_BjÜê`Ló¤,®P,7-¯5ÕÉŒ„•I¸‚MB¬¢\J‹p8H—gmlâø¸ñ„Ì0¨4kÖ—™§ã“N©‹éé@ZiqCÉê]J›_‹…BM*léÒ ¹Eer‰ÑšñŒêðA• ªNƒ ¸7Kx‹_ÝR”>(<Ø_X1…‰±¸"L°jaÂÇÆ * çk¤T±Rq•]²Ð™1“g@i×cÞ1öõãò»N(GÆ Ec*›Åá·ºÃz»Wa¢äFæúã2Diá4h@e“à¿V2®ÅÄQ ¡µ{!õ÷'é)ƒýÁÂ!6ÚÒÃ{|ÜtrÌdÃÅ™íðqåá!ÃÑqtP‰k¬^>ÔÕ!â€Ü +!%zdPÅ B½2+gtEèH—‰÷”V!%:T1Ó1…™W:ŽêOŒ™Ív²°„¸¤ðU áòr› 3s2k¦SîP‡ÔÕ¨`"üRâÍozÞäŽCÙŒ›Øãc&Ò“,¶wõ¨p× •É±Q œM2B~2(G¾yT!W!Íɳ34 rXÜQ´ÒI·Ôs+8_V8F`ÑPþä¸ùĈ^k¡mdÔÑ¿Ü´æ¶1.xȤA9¶rác +ó°ÎõÕЂ3Xåâ©ö–?7°QÌ—C¹ j¸C-‹'«Ä$™Í\n#Ðbý{ü‡-V"lr†u˜HðY§X92f[Ôk$îHËÌeFÌ´eÅh±³pó ¡l.ðþˆ ¨j™Ñ­1ókûÕ™S +‹K‹0PÞ(“±Ñi‡·DøëðWöH(¼#CÚ!ª³÷ó”Ã_ñ${d¨kvF¤T—•+Ì} 6“ÜoÂ8Ÿ³R êCp¢0k¥cC|@f…ýU£%"*¬>6ÖQ¡Ü]CZµ8…q-&×; ©O¦k P]„Jö'`›YXgR!ÂQ™yDïÒ¹T¨ÈÍ \zì«1#jT„ÿkr…lTtT׿éÆáÉ!düë‡Ç—Ù’RÛÁ—4ˆ\GÉ1Hãp4f$Ç-̨ѭB¼gLODõŽ`eúL¼¶ #ü6:ŽÒi›+Š¸c6w\eóÁ¿:>¢Ó˜¡]#:wÀñeWêË÷GjÛ +›On¦0. j0¢g Ž(î+QRÅ#ݾ¼Ó[0;ƒ2¥µ‹Ã*(lÕñaÃà˜å(üWŽ€Ó"üC™Ùƒq©1¦wÊ”å7Ž«NŒZµ¨ ³ ÿr×ð‰ÉÎí ªÝ'5ô •ë›#æ»Fmàn½åƒÊâ…#jû‘´zËÎØ WX+ëTrófËõååshÉаÉ=l¢ÕŽ°ÁǼ52:7báA4Vî„ …CèJ',ý/ý‘׸/grù¬lØìI詨͗墉ŽÚ¸Œ+Òá³K¸¿lt¡t5„¥£n!çs—¨vøT6Îâ +ôç·hèA¾À¬ÁÞ¬ì¿} ž=¦±ª9ê׺V®ìð•BÑ)–M„ÈGÊ@éƒJ›ÚÆ!³;ÕÅË3+(;6nTX XÀJ&çPOÞDÅÇôê–¶® ÉÖ‘aí°Âª1Óz»ÏáÍ;ø¼ÚÂÂA:Å¢É<2f8.·k1àUsk:\D<‰úÂy"Xé¥9¦÷4’Ŧ¥Š7>¡02Ccf æC˜Ø]ÃƯæCÃfb‚ o´åàSN!§°yN@ 0ºäÆLÆÞB¤ºYZ¸„0Ð}L¶¹­¬ëìþ!5k>¬Æᨬ„Pn>:¬b2sÞÌœÎ1zFm>T¬ëÛÙé}\(Œ›<66kŽ0 ™Á©0¹FUýÙƒÐV‡‡-GGt#‡ÌèÔ!¼Ü@)ñ!¤¯5Éŧþõ¨ìD_LxÒß°º’_Ýß-6ÁFÈôÄ]'ä#jTÝÿ`Ñ«D|vEL¶Épé8,&訨ѓ¢SÓV_ÞHEÉ@yjùÊҥǵî ÁpI.6åMùS=g¸sDå@]¡j{›ðæ¿yR;¤vèÐþý•L¨Uœ9ˆV7„Xeqç••Oys3ÊŠT] 5N…Z§ñPËDEü‘ʽ=_ìíBKjlŒ +á4˜ˆry>9‡ U™‘R£Þ¡þ\Y¿…L˜É”Á²29*ئ"-¹…>>¦‡„eúꎵC4¹¢&WeÓ„/..à| +Ú“‰´´¸èøêk,PÑQq9*(lx …•Y=6*Ž1°éòµí¡Qó šPÛ|6:Gø*n©Ž ÐE+ ç¶qi£3¨²ñFB²ëŠ%¹‘ÄÉàÞÁƒŸøúqå‰q‹ÚƃLš2‹ÌàÆØtÅ%ÁÁøÒSþÒ"iÀV¢Þœ H8T3âÒ‰qÛˆŠÐ"‹+Žó§¯¡´ŠC·— ¼Q&5¬@FÔرSÿ¦3«ÏJf˜è„˜™RÛ!ªôg±Ú„aÑ¿7q¡Ä'ºpNOŒ"CjÓ°Õuô˜ñ$ dxÜÊ¢BÞÄÆ•¸H§¦œýËÿfÒ½ôäyo~ÎÎÆÄdÇ“l›Ý’÷é0~ÜBSÛmd$YYż™#Ã½Ý h"".o.\^â’¡ìäÊÙû­¾4êOGšk‰înròL°¾î/¯Úø¬•‰ë ßyí'‹çnËmŒóá|–*Bz)XÜ´{K¤XŽVÖ4é¤7¹˜¿áŽö|ùSñÎE6=§ÆE=Ƴ¡ŠÔÞÌ œFWˆ +N©ló¤©H;Qšß»òxyö¢Ì Ù'Ǧ¦ÉÄŒ#Ø&£Óžø‰ÂÆÁoª1Q0.«± +r=3¦w‹‰ŽÓ›ÚÖVW¬Àá/»¤Z´º&—ôNInvC¨ÏuJ°Jb}Û"”ìþàåI¥}DcW£<ªcB‰KL ©žÖ7èœ'd¶1=R„ñ;ŸG¹‚‘L ¾".Õíþ²Þ€ß0ºcÎ@Å-¹P¥Ò^ÕcÜÀ¸Qca 1!T4ù„ÜvtDsiÕ؇•VÄáå’+Ÿ¦¢„/ ž ªÛèD’*ˆ˜“4ö Üì3xLN°Œ¢”žß סüÞ¸vî1GÊH„FÔRÈ+,ì¿9:¤—ë)™€|Gø  ä<à6h1uxH>¬¶©l¬®ï¤ŒÞ†¬äIÌÄZgèX›Mu±`Õiºbm.Ó#BM ‹JyñÊg +³gŽŽ•J׿n‡}¤B 2ØÖ +=nv…ef¶ÿ VñÄ=©®X\òóJxO´×mÞ´]ÈàÁ +ï‹KéÖV¥·?³}Ÿ «íª +ÉGbstÆà +²ñŸ›³zŽËQ>ÑKvϧ¦ÎqÙy«'«qE˜èðæB…yB*[Ó5â|º±Z\¼ØGý5L¬cbmÔì)6ÖÎ\:éjpL¨É‚Ù÷§¥â¶/ ‡JqKe™Þ Ž3¢ή̬œ·Tpy›BØ”CȹC5©´D…ëz;Ú¢2Q#'m,¾’âP¢`IfÔJ‹ÂH˜\¸<ŸäR=_°zr®P'ÚØ Ö É1é‰Ô3Øî-€Ê„Í:ýE P ¨%eê—ÜÍL%ì ìáæ¨Îù/‡†‡åÖ“ãȶãZBnfíž B'ílÖꊌëÀ£éQ ¬këyA,,ø •É3gï~–‰Ô.¬nð™yg°‘mïº{RemÄêPØlT $ÔÉç¡úå¤r¸}©`¡gr‡à|Á‰ÈHMt}ÅE26A‰…Jgs÷Öóră 9a)R[ÃõPmÝ“šÑ`>—õDëCú4`‡·èË,Ô×n¥¦öû#pu¸™ )í¾a#9 Át„äÔý¥ÍÆò->ÞöÄ[⬠œPY N‘‰5kËwïyvõÒãÙ™³v!+E‹½Ík®põ¸–êóåzþÜ&6ÅübarÓ%fV^*Ÿbã=+qú« äýiuL´´¨¯ kñ»uã&xÀ¬Ø}¹þwA$›Ú×U`,«ggΔϧ&·"Í .3‹ +Y”=úÜ6îèïIÊ&fCí}©±ë +¶‡µn`Å|mqjíîoœ4ªll µSñÙƒÔܶ°¬ Bt ðà3oM¯] ²ñL¼mïM~85q^nᎂ3!¨£rÛI-1fáQoÿr)Hg”Âê­TB+ë„šÛ‰©sÕÛ­‡Àƒ ~ ™s­-:Ô3Pý—O¬^;_JO@Z‡¬4ÓÀ«¸'¡A¸q <6nu®þ­£`iT8 ü#³™1Q‡x­+¨„ ŽéÀ+=fÈ» ç‡4Ž1 ›npK(ŸæSÓ‘Ú2üRa¸±-V7¹ìBª}4­?Wƒ›0ÞéKiì¼ÜÊÊÌü¨žÖjDþæÁ$Ø$.i0 +eÆ­Œ…M†+«ùÙ‹ùÅ+v&–o­EkËZ·hãR|¶G'&Q_)T^M¶wÙÄP +ÆçüÙ9>Þ§Ä_Ñàa D‰5¢@Ð dç„Ü’¯°Â¦fÌdãSÁò<.Ô˜KT¸–¬/ÍïÞZ»ðP}é¢Od«sg¯?áOOeËs{íÝÿz~õ6™Ò“‘A-R )·0Ò¿nÑ9D°H™ÅñŸŠtäöȘeLï6àA ; .Ȫƒ‚Ê !ÕT}~~笘ȠÖá1y’FOJ+\^˜Ý{П™6‡?1qlÔ¬õ'!“Z”&;Ùæ&Ø¢—>kã36oÞtÇ»Bq6ß;»q÷3™©3GäØ +ë:ƒM3“„ uBŽô{9‡sÉ=¡5RÎp—ˬºÃ]2<1¬…ç¶Ra5Êépÿ䩛ןšØºU˜»D'{ +,p\Ñsúî¹y@M=¢Ã-=†’só +D¸kP¯²ál×É%ŽžTþë]à +‡ÕWaókÞš_1;ZT$Åš ¨ÆÑãörLnb¯ä&º2?±ì=f¢Fû_!`ÞÐi+P/&äˆPÕÄ&Lt’¶ì&ê-š #C6W@ ù…CF@é<úãªúÇh€ÞDaAÌLÜ5¬>®°Ê­¼C(Eꛉà3[ç¬Íï+0NÈöê+7K ×£ÍÝbï ?{‰5N¨0:T·{’:;×gigØÌœV°¼îð瀫ì&g°p×âÉçŒTûwGÚø4¶'ÖFÙ8ÎÅR­ÕÌÔnjòt¨ºÐX¸Pš9KJ6ÚH´¶’­ÍÌÄ)w|BIDFÌÐ Œ7Üfç”Øq‚y ddÂl¹‚-‡¯rR…+̌ՒÇ(âÓ½lï"ÀœéÏή_¸þèKî@iÌHpÑ)µ¢õ­Þé;ëמMÏœ‡Ò·|²EÅ;*Ô7f¢ÈìC:·‚i’¢ÿæ@Zëð¨í +Äçð×|¹…éÓ÷žðÕÕë/p…å!#kç3zWXe÷š(¶8„¯ŒûÊPE‡F ð´L¢gcóãf„ þW¹‚î`Õàšâ‹S[¹ÎºždS=Ä_·p2:‘ž¾¬s%Fô ë¿£8úÿOW°ùTÄÑ1«Öî‹46ö›G†¿~xD+Àܱ)»¿ªÅƒÇäN90‘©zè˜b\OËMœÜÌAŒ2!yÿ†ý` ´ˆ8ƒ¡ô„Ñ3‚SðV©ä¼3<á-®"|Fiîã"°«™MCï@MÂOdRf TfÄ¥‰UiàÒoé  ±²ÁZâúˆá¤†xEoÖ¬¡’7VIÕ× Œcžxyö\yñ ÖÞ #+Ö»ƒG:%͸_¡wBrw§ÄâN¬¹_œ½[ßþ òcÂu0ÍQ£[ne´DÄlQÑ ÔW„(‘m¯¥ë+§ßd÷„ 3ÍÕë³û/œ{p÷žoÇ[ÛN_ñôî½÷> i-î׺£ˆPñe—ÒýêÜU£+ùÍaÓÐwœŠMaÈ•!èî° Y B7Ô˜;T§€Be ‡ØHðéPq¶°tÉÊe•Vo´´–jž•² ÕÞ~­ pnÜD1þì…û_ £íãr‡ + `ý<8aóä¡ïõM¸Ú@bÔàtô¦{þü|nêôÒù[«×¡Ä„«gî1;%¥§BM¦+æ–0_}@…hT#|æǬ*D Kñ‰m`Ä“28|B¢1nt\PZÓ{µ·Ì/ÞŸÜ7{²w ™MXpqï‹'~xÌ4¨s+PØ…Pd°t*XÞ>:f9rR)Óa–þû-¦a9b¤¡ÂÍlžŠÎÂ+Žªl:”TöÇM舅Í8•Úâ¥ÅKO(áŒØ¸Î.@¡:%w¸îŽv˜ô<]61I³3ÍÏ"LX‹zA¸ŒS(õA:P›Ùãc†»N*k ivèúþ·iµÅk,tøåzÜÁ§Ý¡ª7;åË÷"ÍSt|å³^¤hÉLz-$Ä[ç“ðk”T壓TTšHÔ-pÿ(X ÂalÒF%¡töƤ&qBf…f‡â <¤qÉÍ<ÆçÙ,^ îeBY2 ÒINmf÷#åÅHn¶»|*N›é0ïH•u °9=+–ÖÜÑBSÐ%\jXKŒC6Ô:ûð€(™€p‡0YS?_øª¿~R®¶±l¸åMÎ…+[µµ{ÜÁ +åÏM®^e’“ ãFRýÕÍ•ä£íhmõ¸ +?)ÇpOÁá-Œè]wÆŒ$Æg˜H[‹ÅG+V2,7QJ =;¯ÄâLkÂŽÒÊÁ¦[§&©2³£'Äÿ##†QÃFÇõDÿ +vHètŠ‰v©JFš¨¯`÷ÓíSè9I7V­ ø‰I)0¿åq¡ä‰Ïøò«£Â·Tãsl`c‘kcFï]ÖQ ‘­Ì׺ëý1q +DxbE(­Dš§AÕŒZ—ئÃ]·¿p>¬ÆáÀ§F î!S:jPËÈ<j8|é1!zêA‰@IÈMn½ +Þ !ÔB§ñ@÷—¸D'T^䲓Þ\/Xßd2sD¸ +™Æfwí^2T1P2³W‹…ìt +«Úʃ!*Ì4˜þ Ú1¨!Fõn­Õ§1{ÌvÊgÛKl¤š¨BÜ»(ÍJå.¿€F2ÎKÅéÅó­Å» Îà€•›ÝV2†ûŠ›túú×Z;ùd8;i&üý7ÕQñ”ì<¸gfÔHà ŽêˆzœwP…õ'áÛýV6æðgB²ºðàÓo<û½_•fÏ8„Œ73Ågf2§cÕ•RëTsú4%¦i1KJ.‰ 6¬TfLϱêB¥%Ó»Ž#ÃÆÃÚA¹uD ¹>ÀkùöVcþüP¢^3¤&¸T +–¤â*¬A°*´Vc¥y•“›H¨7‚óPºÍNž1ž·š¨˜ÎîR:@çu(1 6\S™æ·ARðåød×Ý¿¦.‹y‹6‘o®–&NA7ã2á¢ÒH꠯弱⠌ê (grJ_ήS±6<Éá &Ê›{·×íÞJ°¸*m±¡¦˜˜àcm3<.³»¸T4;ˆ{xX7¦gtŽ8៨/ÜW]º‡«'äv‚O±œÒèú×ãò¯4¤Üâ·’i.>a$CZÂ'·Pw ¨àqBaC¸”¯0¡+Þü2—žC¸¬Ø¿3º® 8w´Ü힙ܼoõâ“[מŸÙyå˜'a¥ã:,}tRe?:¬?9nV˜è! a¥c€ÄC:×°Ö¥2‘àJœÃÅDºµ¼|övqî´Šàq0ÇÂR´~ZÈ/ö¯³6áÕû ˜¸ ŽÐîµåyrÀ¨ñê©të´SjkI£]@Ù Æ­4(­0lâá1jd/aA<åFנʦ²R\´ÖZ:ß]»­-¶öz;×Ý¡<ÊF¹D›ŽÔÛK²;WHƒ2¤?>±pVJ·! éP¯ +ñkЀÁFé„ñj-ìI™y°?ŽÏË50n:)·8<)/HzzZHuq>1¹|%ÝÜ0,GÚgRSà¤èP­9>^^„Ìn£â¾Ô”˜rz¿=qbÜôµÃ2…Î 3`‡'Æmý÷íQ/pæ¨Þ%7Pýw‡ô®c2“:œ)-1Á2ÔÒI9js†(!;ª¶6¨Í´ÚDxXðÀI¹mHn×À/ˆc: |东pz¢tò1”‰ñ±ÖΕ'f6o¶™]1Â[O—òK(5a,êöÝàzw€÷älΙ0»££jŒð„¼Á<ο•Cp›S™™mP i0y_*™ã£†a5 + ¢E£Žëñq¡21à +DLceu6Ïña•ÖFÙ™°Üà‚J‹&›KUö½¹ùx}”Êò±xÐ'X+ иƵn­=àðW:vbÜ- C<*¥íF)™1ŸLƒ+lcfÄ°Óáoÿú‘‘¯9< 8©°©­’OÁ!%Ë=!ÖW£n>¡D<(Ÿ&‚569åÍ/¦ºç’ÍÎÄÊk?x¯·u2#¤!1ßãÓSR©è”Å“TÚXŒ uæ.hñÀ±aÕÉ1µL‹Œõ¿ÝÆ Aûsß7:ƒŸ£ã­Ds9×=+Íì_¹³sí:ZµqinYÈ,$§›«÷•f/¦ö©pSŒµæO]Åý19Jj Iߟ'–!ƒ-Bl¨Pÿ¨‘2CÇÄA£gÈè9,CÔÈY|r–O\zaýj¾³æ äØDœK¼½Wž¿ÖÛ¾³výùx{+U_ZÚ»Å&§ép ”Q6iê{¡Ó€CÐkCTióh1Ñ%4)i¾Á!Áš[½&=¤Âš2õx LYnˆK_Ax õ&lLÖJK„R­>PêîÄ ½XqÒLI>Ê…ÊŠþw pJ« 3²j‹!B©â|®±~xP¯¶zûÿÇÑ{?¹qey¾ÿÄÓ=IÑËÃ{ ‘i4Hƒ„÷ÞªPÞËUô¦èD‘r”m#Ó’¦Õ;Ó=ênµŸÙîžÝÙíµ/âíÛx¿¾ E E&óæ9ßóý ï½'81ð':$–|BÁEÆJÝíjïÐIYœ^b´2!zxQZÙË&½D$‘ï%Ks£hÄ™=œ“A¡4{؃Ìì終’¨ìB©…ÞÆÜÞÉìþS.³à lxtjéø“ýJOvƬpw~:ABj bÕ8hê’b®0sÀÇêã„ÓÈ$4iÅì^Î …X!é „­>aR&¡?ÑuÑ9f#`r0Hòt)Þ::ywÜI¸(ÍϧñHËöåòŠVìwg·¾õY®³ p8ÝÚÉN +ÙY6>í« A +ráö“ÏP¥ôg†N]˜8d€Çl ì(£—*3k‡jÈ—OÌÁR&˜lÈ…éLo#ÚXôpQ!3¥×·¥4ïùù,­Uq±ÄG;ÉÚ2!ÇÅt`ŽÁ'YɆˣnnØNAl†ëV<â¤âCæ¥Q÷¨‹5ciAÁ,LõWn:”V +efb­Ý\ÿ¨½voùàiiñ*Ÿ®Ï^~ë_“Ñ–S`±€IE`,™èô7]¹;€n€ÁK5¶=S¾ÙØøÍŽ ΠJ#¡â3vуúblp÷yÐ&» + vª»Oªe"”¨t¶t€ü\2ßÞ(Mï`RÎ]dDE@Ìس3€d;°Õ+Y´ –èŸðµ'¥Ì°9oÆ ­•½¦Ö6éhËJD,ˆ 2‚WÕC'€±úŠ÷»¸ãƒ@š°ã@F&íø„ ÏX}¡Q úfåš “b– gùd«ºr³»yÒÛ|0»óhn÷a¹µàÃ%»_„¨¨ÃÏ›=ƒžM2áÐCzS_¤•ÆÍ°ÝòÀò¤sù _&;vqÒ Ñi>> ¨ÇCˆg'ý§/ºfÂÎzÈ$¥61!ÏGªNLÈã +„Ïó3•…«ýÝ“þîãâÊM;¥É-Ï#JÅÍ¥¼\ÚE' .ƒp ʽ ƒŸûö™± 3jõI'7á\ øôLµ¿sÊP‰Ï/P±¦˜›IÎì…›Ûþp‘R{'½õ¿à@?—ã“ýjÿJ{ñº”î〗 Ýg/>Mw.ÿùiÃE³ßàã,¨&$¦ÒÍ]&:e†ø™»l¬ùÒ¨ó‚5£ª=¤˜HÌôåÙéåÃúâ3&`r6”jq±*®äâõåÅýÇk·Þ÷‰y¥¸¨Õ6¼ÁœUœ ú ª Ñ@¤©ù­°`„¸Óc.\G½óM£Õ¨É>g$Î ú˜s§ÆmcNÌE(l¢'W#õm½¾cÃ4-Ù9yþéòÕç @Óz…ÏMs™kݶ!¢ÙÇ‘b1ÛÞ‡ƒù ßøvƒƒ˜pjØåÂcÀQW:›Wî¾íü8&´¦6¬¡6Õòzgû o4ú»{·_d;Ûo«ƒUxÄGî÷Y<(|=ÎNDÆ\A3aãýxu3œlV¦¶ºÛ÷ßX8~}æò#&Þ6ø8˜ŒUW³S—½T„” n: j± WG­ä˜…œ´Ó“VÂé Ʋ}V.ëôÄùQ—ÙEí¸ÉNš<˜ƒó³éé» úÿ_§Œç'à“j“‹÷¬hä¯Î;&Ý”å­ïe#D¤¬{mý^º Õ»¨Ræ³ýhs°%2ð{ßÌšçW@âÛHuÈ>Ø¿×à -nvÔŒ¾ŽWÖÎLø‡@…µìDtPX K±öž+ÍÕ–*½mðJƒ¥"*W´Â<(‰ö®˜êCÉöÈpñ¼ ?; ¨œÍ´2ùøQ‹±æ ¦CLÂÇ&üBʆ ŒRŒ•çR¦8{¸xüf¢»«ç±pÞ‰+­œY>|õäÅ—ÍÅc•`"S0—3z8“Ìpó¾2ì¹0‰cS#6ìÏN½2ê6"“6ÊêWü¡¢äfä2ø4@‚ñînoÿùÒò 7=T4ÙØèmžlßz¯>»éöÛµ•káòlañ8Þ?âËë¡ÔloåÎöõÕ…#@m\b:ZßPK+XßH á—‡,gF]Buáaƒ á&#°˜ÃÂ>7#å¦çwînßy[Èw#õ…l°=Qy-ѺRœ½£UÖ­˜dÇ·•A8Qz“IÌú4˜G䆇JZ`qÔIxè˜'˜5¢3n Zjà3HnGÅc~.ʧ{¹é+•ÅëÀÅøÇÅt¡· ,¬‡Q¥Â|}åÎÚµ3{¯I¥E:VuQ"Âi­Å#RÉ8H !>†æP ËOóÑšÕ/˜ÜA£{ÐœÅ+XS½XeÔ‘SC6O ‡PJ¤RˆÕ–ê+7äò²ƒPç÷ï>ýSsž€ší^Iu®èå .ÖsÑCfØ‹{GO³õµsƒ&³‚›NQzG.®&»‡ÑÆ%˜Ö¯Ü|mçø@x‹_$RY¾»uç;K7>ä‹kDqcâÛßùaíxÌͽ2æ›p‡èèŒ]ñ g'|V_0ßÞÔkëL¨˜ —øôl09óɽÛo\{ü]>Ö²!>ø)¾§—Wz·/Ýý Ð?‚ùĦI¹>éfNØÿòÌø© 6àÁGùP©ÃuÞbñ0J²7n¥†& P7í˜>h9ê`&íBÃ0'õz¬±™lïAB™–ó¹în¾PY8\ß?¹ñÚ÷3Ý .Ý]=zkïÑz—_õâöó5VïS‘F<77éq(ÙqQFH$„Êê€s'ì„æe½RÎ?”4¸I  ¸\Ôêë±în}åVy鈉W»kÇù¹C¥¾.×7Øô,ªÖ³š£uP¹ØLÏ]¡¤Í‚«;6¾<kN6ƒ¨5>7ï ¦a¥«u/_B¥2ÊC\Ú³Íú騗Ö\tX+¯4756åç®';»jè[ILÔ§wÓb®_ß|¨4vñN@«bZ¾êÀBã΀RXf¢$˜a#u;¢\´ÒBz8¥¿:o?;æ5ù‚(ŸŒVÖèHÓ I6RˆÖ(?n÷Ý8 ù½»ïìßyÑZ¹¡åú´Z2¹.$êÔ Á?n'|L,^YŽ—Â©î¨ ;uÑ6j#ìHØ6O@ƒæ¡5!%‹=FI›p0J¥¾zszÿÕòÂU)¿àåòƒ¸è½çï7f/y€À~.cEÔq'`OÎìe=x8Y™§leœtr> +8êZ@k"m?_£•^\¹ûåR2^ž»žºª•—¤TÐŽZ “—ʵ.¬97ásN*0—ö4;"¿<䘴¢åæòëüøâ„ïô}ÔpJk‰ÉÙp¼PRufƒSæÁö\U42åÆ‚‰úR¦·››ÞMw6å «WRÕîνÎÆq®·ÄSÌΑ¦—Kñzµ4µM)¥ÁT–ÐÀÏûI@%ôˆ¶úXð7ÊÅÇl( / &Úµå›Ë×ß›ÚyXœÝ›šßþñW¿=¼ÿ65Øüªmï…‹«z}»½q²~ãÆêíL{óèÉ÷ñ®U‚ÙùhûrbêÚÌÁ;3‡o—Wïäš›ßûòw¯}ï+3¦Ž¹ùWp̲ÑÉ@´Î/¥k+7_Þ¹“=,-]ËÎîË•…âüA}ñxåêk—|ªaª±¶{§Ï<ÍÏ Ù>"ævârÓ‰£ÂŽ©VLªˆKFoÀ| WŒŒ E+±CÀ›áÊ7+\v¢&(à«Òߧ"%9]OWgµâ<Ÿø²Íùé¤æÏÓE„„Íσ·LºØAãé£âNDOú(T%ˆø¦µ:pe,°Ñ˸a D8‰ )§ùD³8³×X¾Ak r°¦ã›w~“›qàe“l¬å@e£`ep‡&ô¸ ·x"Åë•ë¹é=>3ëfScŠ•òb¬é&4ŒÏx1?aÕšŸ‰}œg°ô1ãV/æ¢ëô9Û©ó–æBD* \xþ—^1ìĈù« –Ó£® lÔ˜°bX(î ÈR‘òsáÂ2ÎÊ`‡êK¹Þ¥hs;˜œ +HY1VÓJ}TÐ`N µÎ)ÅåHm °žÁ˦•€T8? ap +€‚½¤npR ÆÎ;ƒrà!UBÎ ¹Ùds³>8½¼·{óIsq/ÓÙš;xÖÙz²rôvçA¢¶ÌEŠ|¬žªÌ­_yÈ&Úbv¶¼p­»q@¤ºJµI½É´¯Þ}kçöV\ &ûT´+æ–föÞèí½­,7¦Ö?ýòç{'oSm½ºìvia÷þ»oüàþí¿}õ£¯6¯¼úÁ÷ÿæî›? +óB~žMÍj•­ÚÒ½•k¶·žù…J•À১ >~ÒtÓºƒŠÑ©¹üì->Ó÷PZ¡½LYZkÓSh˜I\®f»{‹WÞD¥$«å¸h‘+™îaaîv0³â$3ÀùÓ±¡5†“.BµÕì ÓœE¶‘~.«®cBÎ ¾P¶²x;5u™Šµ‘:&ç­È`;Λ}¤— +£|“rx8GGʨ¥ökýf´<„—m°8iØ@5‘Ê6ˆ—žRKf4l‚CP0I'z‘ê%\iÀÁ4ÎÆp.æ¥tF-\ìƒè©¡BP*°‘¿xfØ ¢ÔChNLpá@£ ˆð`f “Èøç°=7 ]°Hr"ƒŽ9\<¯Çê+"¢Æ +J¶ + !ÙäS€’‚±Žš›“ó}$¨Žä¼´ÊD[\z°!ð9—•b½prj°¿“r"‚â&Ô¹1çé‹VN8Œ…r.LE³ÙöÖ ßý‡‰Ö2*Æøx¥¾x$çºz¡«d;Ÿò’*­TXµÊÇ>&ªg•€’÷ó9' *F©Y^/ê…Ÿl|œ^Yæ3ÓÁD·2{˜k¯«ñbµÞ»yò¼¾°ÍjùÚì¥í;o>yïÚ³ï_}úáÁãw–Ž¶·ï=]?zZël\»¼|·ºrÒÝ8©ÍéÕU­‡£Õë¯}R[:~e² þP* 5äê¥tï*JB±{O?d•¢Ñ'X-”íëíîÖ“ÙÃ¥•ûÀÆl\¾sÿùG™î¶›M‘†W(ùÄZ¾w´põÝäÔ“Pöo¾–n®8€Õc®ˆù¸|¦},:£V*­æÊ-¨ñæ:¦”ì• +H5ÖÚðKy£—bÔð“˜×ÂŽK^6ÁDë¥ùkÏ~˜ŸÙiŒw€9·6Ž0ºX`Qlàë¨ÅC‰pª N'ѹÂ$g}ÁœL]âL©-“WÈÀ'ZR¶‡IE!nú°Ð]p0ë +è£60‡g'üç ð°à6â>5ì¬ã0À§Ç!ªÐ±v0ÓgS]:Ú$µºÁ+²bÜǨcÌ!ÎÍ«• ¹¼Ž«å€˜TR5&Z6CL(ÖŠ·÷ôúN¢uáspv|Ôäçå‚)ø¹1ûÙQûE# XØO§}de’™æº‡‰àB*¯G›kt¢É90&ñæ%ˆ‹L: ˆPRŠ«Jys°ç®‚"ÂE~! dÖ†(0—dõš„B.‚¬ŸtQÃfØOif5áÄxØ‹‹lHDóÑL“à£\8™­Í§—3­5ߦõ"›jÈ¥~²9_[Ú0TímnÝyQ[½ F’•ó0­9ÎáÅ#±ÒÜæÍhu°jPo+©>ïjÝA(VB…Å­C7¡ÂB%”^H4/u·õ/?n®ÝÒÓdP¿uïÙW¿ý÷ýK÷|B^®lhí+…õ§—þ`n÷Aª±Àë™Ço4wéd¼`©،RÛÉͯ^ûP)Ì˱ú­GìÞÿ`ÄÅŽûx#HL©ÜY?Ù¸óáÚýB¹…tuñîÛ?Œ×VLÞ ÉÏ@N¶öföž]~ðqyþ&ŸÞ¹öf¤°pjÄõʈkÄJ:ñÌeõÂBcéF€Oï?zﳟ¡¡ŒO¬Ò©y*ÞçÒ‹3ûo-ßü^¼±çD”+wß9›>¬®Ü--›Ý¬ "ì„7Ì8`Ø‚>JÆůñ±Ìê eÄDˆf7ã'up¾ ã\¸8f†Ü~Öá!Ü>Œd‚^?ŒQDŠ|ª‹i;.Ûà'Äç–.¯ïߣåHU93›nî‹éE¡:¼¤ÙŽ€“²{Y*Ø}Œ⼄ 1 "\EÙ´ +…„äúúÑÑÃ÷m¸hôÑL´ùBN`zQðà'¬ð¨~oÒFX¼œÞऋ6û8”ƒƒ÷ÑŠçc¥ÙHi^ÊôùdÓ;h(ë@D^+‡ô*ˆ+®ˆÉ)%·æåj ´L.nxÂi´y)NÁ¹ÈKçM§/ÚÆl/™ô³y³BaÀbRj + ¦Qq` º¿dÇ5`Œ0Úaþâ˜ËâÀí€Þón`Hœ„8΋…ÝhpdÒ”rj¼ã‚E‡—sø/¡C²xHà?…xyÌê*Š‡»©(­yÑÕ…˜m>xßTÑh¶Ùnò4±Áü¸ FwÄè7¹¨SçÍÃ^'Äí;¸^ïô0Fîð±Å©MZL[\„ŸŠxp̓+©¼8î¼0b7ú|ˆ04îšðX=!"y^ÍÒ¡¤üË3“§†l¯ ;.–ÁúÇ,0TºQnØèyé‚ ¦)¥&Õ¢”Ò˜1:± “î—Ιí>'´@4J‡S¥)5ÓbÕ¼ åM.dÔì28ˆµ@q£!-ÑL¶7Ï™üC“Ρ1ëÅq«×O`R- +Ñ "D ’à"…êL,Ý8Á0%jA-HÜŽ…‡Œ°Ñ°zh2”dÃ9š°úvÄìÂí z˜13æò‹‚’M§bÅŽŸ•$Èe£›otA,È—/Ø.NølÞÁ©¹hs2V4‚ðEZm£mV+Ù}'ij»9 ¸¸€\–Xi!mÛiÒû)fÔÁÌ:/0 °‹/íò3F'2ÊS0Æz1ÎæñML.Æ-X0 ³‰Á}j :ÒhÇ_:=>nð¡¡‹¶³çLç‡lcF¼PéPø-ž[ݹá§Ô1l¶“@U,nÆæ¦ýXØéåF&=B¤&Å:_È‹Ëf7}qÂûíÓ†f÷…Àñ¼|v܇ñ«lؤ1BçF 8݇ÓQ/"Y”ÒI©òʘã¯ÎMþå+c§†Ì ;) “¼ ik—¯µÜ¨ 5ºi'&BLå=T|Žº±uãñwþnâþ쥡3CF«‹°VŒ ‘ÉJë»×zkWM>ê„kÒALØð ;î#D”É`8Éèù>Ê@¤ê!©ŠÒ¢ÂP‚¡C1;DZݰ͉@XHÔËNÜo4›‰!D¢6„œp#NŸÕ¡%5,^ÁàfÏM8Á)Ûøã~F³hõ‹vH„> …ŃÓá´V^´c’ÕËXÜ(àÿR|‚“³£Vÿ+£¶1+(4ˆØ}ì˜ÅŽD‰¦4Mçf°zÆÌ^ ç¡hM//|óK é§#éú²”lÅ2.+)&úi ^\îƪ‘dÝꂤ¤àE8/„ÖâôMZœr4¹¾wä©¡‹ÖÀ×g ç.X ¤ +Ê=*““uBa«¶X]Ãc¦ó#&“Í%ð|*¡gÒqÔürŒËÐ$(a– cŽáqËÇYÔ¹‹–—N¾ôò¨ÁBÁ ÆÄmÎF’š’#YVÊ€vxhÀk O¸_~eâ•óÆ‘1»Ñc´î‚¸ñI×Ш}Ì™ì/¦º1Ýè Ï^´¼r~’ %|¸tnØ2<á™´âþ€’2~T2ZC#^«ƒdÇ°}Øè³¹xRfBP(R šŒ+ÅfÍÏÐ6?RãÉBMŠ&ýLÐâv»=n!¬& 1QyyÈ|~ÔnqµŠJ’'Á‘¹L|jaAˆ§ÇаÙgñ²!àœŽ(zR×ÚììÔ»rõþ… ûùqëùQ‚3$E‰"Ï°¬ÃåæyNו…ÍãV› E=šL¥ÕÍõ¹µí•úÒT¼çå -NŒ2<èökðƒL§9!æ É„”1 ¢Øž±IøŠX¦˜ªw()ª¥j%¹`Êå§Môìˆí[g Fç Ä.Œ{ÀÐ,^&¤eËæÌä‚Œš¼.„g%;Y$ñPr¢4Å(*œAƒ± bó¼hЇ1$#¢TØ1^LD¹J«$¤,‘)L2l0DŠúpÜ[©$¦ú-Œªˆ9¼”݉»!Ö‹67cqRN¡’ÓCZ¬›Õhµš9Ž+¦"Ýbl¹W8Üî\Þì=|tki¥/‡%´{)Pû,<<îþ‹—†O_0,ЃÁåqaBˆ!…X6ètÃV Äp¯ß‡$Œ2J IY‹›8;lzéôð©W&/ŒØ‡{¹F'ÑSç/³¹è7!F+f4{¬¯K»¡€Û‹s!Ýîž…‰ÎDÇÍÞÓ&ÇŒ^Ÿv:üV³EìbЗK0íšÞ›.eKz4)”‹‘¥úñþâÂR3ŸWs™ˆÑY0;}êìèè¸Ùáðp$%Ñ ["<Ôo&³-(Q¬È™ ŠÊe³ÂtC;¹±qÿîîÍ£Å[×WSùä…1ó…q»Ãƒyb6Äà ‰´¯ž ÷Û‰j^Ð%¨šá—§R'7×Áã³Ç¿û»w¾þŧï¼wn6§hd€¥ ::hž +ŽêJ± 6j‰Z%æi=¢âc°:¬6{ˆcó…L¥]™Y˜¿óôíd­e|6ïÐ0@¯”Úô1©s£ŽÓ§Ïú!¯¦(Ùl&žˆ¸4b1;ˆôÊEû¨>svòÌÙ‰áQ‹Õêµ[ì Fe¾\LK<Ñh&K§“ª¢kjÂí#N› k²ú= ;xÔ˜S¼›]åî¥ÚñVýÖ~÷½g‡½sã'ëïÝëýáG÷ÿù7þ›¾ñä`y¾£ë:„’“F‹Ù0Ž8:ãmÆàfÜ9“‡/Ϫë…K+yð¸²Ý¸u¹ùäúÌÓ[ÓïÌÿá'oüûß~÷³w®odbaØh6]0DpÖëñ’>{Y†6êÌn?¹”{ÿÁÒ›·§Ÿßl¿ÿpî—_Üûçß~÷—ŸÝüÍg×þùë×ÿ÷ÿøíß~ròôÖâåõZ,¦{|”ÓMÁŠª¡|‚©ÊS‘v™Êå1]×û­ø\;v°V}ûÑöïûo~õë½ÿÞÃû7×WW¦€D &»ß±gG-/¹`3LH”[c½ÑO"Me­¦CÝŠRc}J{vóµ‡û'·¶¾óæñO¾üàñ£›™œÖ™žjÌítÖO(¥à´šBŸÂ@EÝlË÷vë·¶J[mþáAí_>ýúËç/î.~p¿ÿ?yþ¯ÿîó?ýþ£Ÿ~ïÊŸ~õü§Ÿ\Ûžó—ÃjŸ48}~0 æµÈ¤­›&n¬¦þúÝý¯øäÓw¯¾u§ûñ«ó÷½+üéóŸ}|í§^þÓo^ÿ§ŸÜ~÷NõÖvae:ÕÄX,c¡p* ÖSŽøgóÌÑbòµk3vË'—‹_¼õ·_½øÓ?ýíïùÝß|yò?ÿøþÿ÷ÿüÓõÑÉ^íÓ×·~ùåã7O6#2?iôœu89ìVÝäwXÄ„Ç;éÀþ|jo1·ÞUwg£÷:Ÿ÷Á{O.½÷ìÚÓ755bó‘ˆ%$)šql¹L]Ÿ—ß»Ñþë·>kÿçßû?{ñÿþéç¿ÿÅ`þø7ÿô»ï}ôÖñr/‘¹ÉŠr”ÀÐ îÍËèt¾Ô¢fåÇÛéÏž­þüÓ{_¾å“×7¿úî•ÿøÕ³ÿùo¿÷_~÷þï~tï¾¼ù›/n=>žKG‚$Eb°¸\ÀO„Ä”Ä÷ä£5õÍÃÌ;Ç¥Ï_[øûýß¿óO_¿þãïüî‹kÿòó'ÿô““Ÿ~°ó³·~õÉþÞÙ¾²–Ë¥t È#yà~«8߈͖Å+ó‘ïœLÿ݇—?}mñã×VþðÓ7~ýåÿ~qùoÞÞú¿ÿñãÿóß¿þ—ß¼ó‡/oÿ·ß¿óÃ÷[Èkº02j´y<Œ'ÉV:X–]%Ѻߡï¬Ån­E”Þ´ðå»;Ÿ<_þù§·þë?~þ?~öǯ^ÿ×?|òÓÜߘ/·ºSz®å@øˆª§µP5X.âàíïßlýò£«ÿÅ­¾½õÙ››?ýøæùÝ¿úüΗ/¶¾þøè?ÿúíß}zíÓûµ÷¯ç7[¡¤BjdÔrúÔˆÓh”¶fÔ»Óà®Îë'{åwæ~þƒûÿòëÿág¯ýãO^ýǯž~ýÑÑ×ßÙýÛ7æ_÷¦Ã•(®aØç 0‡Õ\µRï7ÓÍ$ÕM {ú[í/ÞÚùÉwüË¿úâÆ?þìµ÷ó×ýù­_wçßýèÆþÅãßÿè槯öÞ{ûʽëë(B8½0`/‹ÐÌd‰ÝŽøp§üÉÓµ/_ìõÁ•ýÍÿç¿ýòŸ¾zíïÞß{÷ÞòÒtN(‚z`†“!))ºH“± o*¼ÔË\_+ÜßJÿèõøáƒ?þôƒ¯}x¯÷Ã×W~öÝ+?ß¼u©ºÑ/r… œ"ù$@$ÀbƒŠ¢&Ò‘H”Ck:¾ÑÑ·§¢‡³Ñ·Žë?~oïWŸŸ|ùîág¯o½åé~ûÉNåÚrf®Î'õ ,.œà²¢Z àL\¢‘àÑ^.tk£ôÉ“ÅŸ¼·óÛ/nþýþñǯý˯ÞÿÅ'7~ù½½?}ýô_ùúo¿¿óùÃÒÉvº“å`¯Ýhóñr!šh±œÊ|+Š¾)Íu4z²Wxv¥ú½GKÿõß~ð§ß½÷‡Ÿ<úç_½ù¯øèç?¸óѳõ÷¦[‰F(҇ÓÙ:GÓJŸ¯EëQ¤ónT©[Ë©§‡“ÝÆÕü»7º¿þôÖÏ?¾óéÓõïÜí?;ì\î%¦²¼DAÎ@¨œÌЈ†pÂk—0[EÅjúz/·ÚJÌîí”_»Ò}~mîµã¹­¦²Ýo6µ©Èuàè(§€ç£8c…Œ¤ã‰¤Â'Dj¶ÝžÊ^êDž6>yuá7ŸüË/žýô»Gß?™ýÞÝî“õèÝ9åd5;› òˆñùà#-/¡MZ`·CÜvsfEd®ÜjÐwVôÇ;Ùïöþø“'ÿûOý/¿~ów_>þþ“½Ý~±žÓk†x'DºvÈeHT¤³*UOgKÊN?³7Ÿ½½YùîƒÕží>¿9{¼Vny"|F‹iÌ`7{h%ÑNWVäx‹¦%ÃòQys~jk®Ç/uå»Í_»~¼5u°Pï]iG+)Ii‘à€bõ‡mˆê£8ŸÆB)-QE5ùB–ù„ÌeT¦_Ñwf +KõË åµn¦•‹7Óñj\SEÅP#F1á a*îEB‹Ón±ø].žÔÒ©…VþÒtüúRüý»3_¾¸ôýGK?þàøÇïýðùæçO–¿oþÅÕÆþŒ*Ñn¯ËŽS¼Õ‚/®ì„׃§"±R\×HGEõ­Õ¥ËSÚÑ\ôó×ÖÿËßï?ýîï?½óÅÛWõVK½zTxZLòz¢"%;!Š¢¥b¦ Q ‘ÈjL‚dÂl3­ÕãR'Nïö’×Ö;Û½\Y!ÓRP ‚‚ €ÁNñ³#¾¡ ÀLQÌ .' ù4ILFõL4šÓÕ(Oj,ãéŒÊ‰¸‡‡= +ËårM@èn?ãÞÀÏF2-³0VÄéBi2˜Œå¦º+Sõ^^ ÍäÂW + ùàåéÌF;9Ÿ +R/žÊ¨‘±Œ&vry>;lÿ«Ócãf·Í"=ÄäÕ`œó5ãt?ÜŸÖŸõÞ¸>ww«µÞ*¨‡ðP8ÃgæRÝ#˜Š<†F)’¸ @Q\0(äsÙTLŽkÁ¸ÊJ¡@(D³¼à'9;ÎØüC¯ÍÏBÊÇh#vÌâåF îåebÉ‚Ïå™@ X")ò +I0‚¶9|6'fsf1jA†Mˆ“ˆ†³‹ra æ´”FH ‚© õøH&,+W«³ÍæB¹Ü“Ã@/ƒ|8kq!§‡'ǸˆùùJ@*³á"LHCÆáQ“/Ó!* +rT‘òIeªß™+,5#Û3ÙKs~%UO†« -­)6»ûôÐĤc°¶ý¼É÷ÒEËó1`ˆçRÅÅxªI„ÀÀi™Šáë[®§N®n?¸±i¹?].ª<‡cˆB`6âÀT;¦Œ»Jo'[Éú^49MRánwáÒþ]DMN8Q,$I‰X¬JWt-Mx»uy‚#ãÞ³ÃFÌ +kF¯dpóNT%‚ ‹ÙãvxíNxÄà3CÀ™[=Œ ‰©!¤æ…ƒÑît$-. +ctö“1R©ã]ƒ'ø­³Fƒð"a˜CJ‘’ª®ø DZ![r™ía^èE>¨À^Ü!”7Bg†lNTñS‰13öçßþÖË£Ç=çUAŒú|¸Ï Ó#q<áus°'HøƒBàÂB0n0@#&˜Öšzc7ïrÑn¶»Œ—í(ã¥TF«FJ«r~ÞG+n”eB‘bsvØæ=3æ±SH¸Æ$f˜h +>6I„ÒÅ©m\­Œxx'=X”á¦â&ŸèFÂ~Lœ´øÎ ›A&z Š~6cG*[ ÿfÃíQ'=êäÆ]œ;eÔr(Zc•|8^Ÿ(x)朸ê&£¨X$¤)¹X“Õ«#¤œ#3Ã’ ›ý²+u⃇s¢D«””ÁCqJ.HÉŽ«•œ¡1’ãDäCÛ¤ rÀ)7¹Ìª“NŽÚñWÆ\/ØÇ\¬‹ÊjÓèeG¬èˆe°'?LF\»Ý0ˈ±lÓmÈ`y2¦XÄŽiF(tjd°ýµ•RA+¬Â|É)&oІˆ|¢+&{ÃFÔèd|T„s¤V³aƒW°Â2!–ÁÈL8™asÀÇdõò픺“Œ_´1F+I±:Šä‡ùB03'––cÍ`ºo'c“ÎP³Í-”M¯ƺRfQ-¬ñÙå‹náÔ$rz2ù‹†þâÔØù ¡4Äò¦R^gc=£›?3ä°¸Ø óí³¶³‹JP‘)1³Æê=#$»ùstÜŠ!”æôó¯ À™8?옴ânLf¥<ÉÅFÆí“&·×ÏyÁý,ƒ›s0ž×Mˆú­—'¸Ì¼'”FBñ饣ìÔ¶ƒAbÎÍ&q9Oªå`´•ë]žD‚Fˆó‡RL¢åd“à½ö@×Ú©æå•kï¨Õ•W,˜Ö±¹P~ ô¼\Ö‹ƒeé +öÍÝ1;®9É$¡v™xW꤆4kð fD¶a +ÌÓѶ™¨U ÅÛzys”\ö 9,Ò’k—„òF(·ÀÄÚ˜˜ÍÏcáªæ yÐN‹ŠuàpÅÉeÝÁ, lËÔ~gýŽ“Ž^° oÈÊ[QÕÅ$½¡œ SƒJ)ßÚhÁ^ârÝÉ<¡*&÷ bÇ#&¿8â Ƙ +Ä<®t}lÁè¤Í.š’JˆX´b*8þpi\#ŸXó‰UrD†ùÈ5,¡¡ŒÙ?ØdÛEÅüBÁÍå‡=âYæ£u£‡ºh‚̾  `bå Nâr¨XbôΘ“µ¢š#r3y{ …«lÎàó“~Zܲw‘F_Ð+äÝBÑ*Pz —Ën*ê 4Ú#a–íL†Ë­±>—]æRK¯L Ãæ01o‚ÃMÄzx´çár¯Œúýr–Êátoùò“xsÕˆ‡lÜ®oª²c°{¤1˜l“æÓÝI$4ä¦'ü²_¨ÆÛWâÓ×ÕÆ®g°m¬ˆ†+T¼O%æ<|ÙN§Áø3 ²ót¼k„ÃH hm¾°Í¦WPy°™ÐÁ`JÃF(&ˆGÃ%6ÑUÊkzuK+¯‰Ù993;½yU+¾pImí—ïÕ7”WîiÍKÉî!%WÕD§µ~ˈ…t „•˜—·ãÝ«±ÎÕpyÍA Öœj+ž`Ä'ª·0¥Z>èm?Áäòl@>»i†ËØ´’f5d âRfˆ3û¹!'3ææ­~ÉEè^6cÅ£\bšÏÌ:iÕPít–[`0 6©¾55û„b÷mó½h'lÊË—|B‹ô`±DÌêeYµLGš¨P´Â½€Ö²KáÂJ8¿4îà¹Ц°p|— ‹ŒAò9 mFu>½àc3çM°=ñK%\ï`‘)¿T™„„A#ÔØ”ƒN'èâ²Lr–d|Ư´Œ¨Nh1»8ìaÉÛñ8HðùLlÊê¦ÛPŽviÄBvî… ›b~Ù/ÖÆlĈwAÞÝ{éìä·Ï™^ž@LX< ¶™Ø4"–ǭ䤓Á&y ¦™ñˆÐAº”¸ÐÑê¶^Y‚àbc°T`S=;££RÓ$Bµ&eûÉö­C!L«¹@- ÞPÖHZ¹ŠÉÂåPjÚ€„Áðz‡MÍ%¦Ž››Ï²s·­Tø=Ñm/\5‘ `'Ш‡-’Ѿ\Û•›—‰HUóR¾ï—K„V!ŠÉU"Ò Ë|¦ïÒ\²Ý?xÖºt2Ø6ª¼N&çÜ\ä{¦¾Ö^¿sÑIbá’^¿m%g®GÚ{x¬gg³|º_[¹©n¼’˜™o­Ý--\Ëõ»ÛO¨dßÉ$}ßt­…x02дêâýâƒXï¶PÞvS)'ºjÕ³Vr„¸­ÏÑñ>·*—Wí„H¨e¡°Æf–‰øl¢w”_¸=ØÕ'>“l" 0T,\À#m&ÑãÒó˜>3 »…¢íÚü‘M¼<ˆ+&ÚQª»Dt‰N{øŠÁŽ–ÖùøÔ°‹³“)3¢»è “Z¶p½oÆârzzq÷ –Ïšð v +$´¼Õ¼ô&*U7®¼úâã¯ôÒâ9+=ɘ֎ö¯ç×%goÒÉÙIHRŠK@FÜÙIÄA',8ˆ­ºJ/O8Bf¯JŒ +™ý@»ÎZéa+-:½¢6¾=ä½ÈgæéD’jÞ`ÁA&Q±šì€çÏš¨IÄ$¦øü‚RY6/Çk¥ÔôLqöhÔAŸ5 ¸j©U,Ò%@uã+n®tÞBËñöîñsN)ÿÙKc ~`Ï‚é•tÿVÿàmPk&Ý¡­ko‡SX2"ŠOî؈¨ˆÚÊ®&§nÎî¿×Ù~•Q+K72u,œgôŸžI47ÓÝËé驲‚‡ ±ÊøpA¥RAPXAš°©ÙHcâsn.ŠZÝ‹÷n§æokÝD)K±Vo똟±Q›œQ*›å;+‡Ï–¯½[˜;R½bg;7µCëM¥´’ž¾ªT¶b­ÆÊíÙƒ×äb/×Ú¨ÍߤÓ n=|ÎÉÄÁùÅ:—^DÃUJ+û{˜Zµ‘k¤æq ØúTº½ºþÄ$›ì ¥µÚÚæΫjûŠOnNbªÒцÞÜ÷“½ì“*ˆÚ&•ÖˆhxÎófÌÃ$I­Ž«EDo"±9*µÊ-‹¹y.Û11Z^¤"UDHñ™^¢E®¬(¥e­º¡Wì„ô0”] ®r7˜›K›dj8‡H}‡Mö&¼Áds33s•JÌ°‰P €âùB)37³ur>.ÕXº¶}ï;ӇϋËwbC.Þ@½¸ugÿþGXv1!5“™>JϧfÓWmÄÎñóÆì W!.‘è–—OŠ w’½ëÙù8ܼhãàP΂ÊÁô,—] Ó³~e08 "GqÎHÚPdÄ—Ï|çÍðY#f'’ˆT(MVkMºØ—‡lÀ‚œòñ›ó‹“>ÀSÚôÖ“p¦oô‹v2 +"ÁŠE€ëv1)X,ÓóZeä2Jwú;jaÚiµ­nµ×Nº[RS{\jŠçfûW'<¬ÒÈhS«ì¤{7#Í=­¾#—I¥)/a“ÓáÚŽXÛ$@úˆ%½´Q]}À¦§´žì\ÖÊ«¥™½¥ýGõÕ;¡ô4%—š3û¥þ*æµÂüòáóùƒ7Z›jëbË´šÞØ»¿|ù±P͘æ—ʽb \Ù¦S PÖrýí;/„LE¹¸Ê¤¹üZlêêìÕõÕ15;·ó¨´tï–B¥e"µ©STjY*íÆ;WQµ‰‹™ío–g«ɸG({¥*¬Ô|Á\´¼‘jlO¯^-Ï]ñˆ5dЖºm)IÅ€Ò–od“Öå¬VßJK±Ö%!¿ œ0?j Dé²×¹®¬kÍ}­u%Õ¿iб(@ÇÏæ÷€÷«s5h°[\‰5/‡ ËÁD8X0U_WX,x×;¸ÒXÙ{¤çzV_p°âµµ©®ñÉŸÕî¸_»èÂ%;¦äúW{‡o7K]¥¾-W·µ7lçÉpµÜÛGù‚ÕϺpÙìW|L6D¬Zaí•qÈâ—p©fÃõs¦€Á§`R •ZlrÀ)(ú´ÝLÒˆ›á°?˜l ïå,„æ f(k¹y.Ñ…ÃE*â|X)>3£”— µ¶¿¸Jħ^™ðB+ÙÙOt®b]w0=<9—εwI¥„“™éýòò0þÍõGWSýkh¤f'µÜÌ—–F£ô*"Íi¨…y­¼„ÊE¥ ±š”¬DË3R~d7.á´–ŸábÍ‹v +à-VR±2o‡²‹BnYÊÍó™® —h¹°vôÖÔÞó`~IÈ/©@Uô:)Úó‡@Ül:Ö½i‰åm¥}L&—A¹95Ñ^¼üàÿgï½~$K³;±wAó PÜátwU¥ ooÄõÞ{Þ{“‘ÞWeùªî®vÓ=3d93.Ý\b¬ô @€^ôÿHçÖh+=éA€D~•qã3çüÌßé¼x”g(kûô㯽ùKoü´6¾ž¾þÛû?þø7ÿ¡ µµÇ‡ÖøÕòÕï/øçɋ߃]}ñÅŸÿý?ÿ¯½ÍëœÐà[Wló†k^®þìè«j}¨Juté/?îî;±JYc9ÚÔ¾üËëÏÿ¢>»9¼ý¹Û¿£µ\?¬¨½³€òìm¶ ¸ .øp±¸üns÷c8{É83ÁvW/¢ñ%댵Ñóèð«úÑ—Áò-phE¨W…Z}ñ¬ê_զϣñ­ÙØäpƒVƒúô–ÔÌ(ÚCŒ,Ú ¹ø¼ôƒR;l/ž· ‰„f‘òŠL=‰9%¶¡4NƒékR`Všwe!#3/Ö•ÎnOµC™µ¾(ðžT[Ôׯƒùsotz¬sñK¶±) +?{jŽ® +BH9`—„h¸gv.üñXu\® O¾œ}ˆÏþ¥Ð<@í>ጣùK`yÜèSf_il´Æ¦1½m¯îÜÑe–uy»çN #’½®ÑÚ€—i} +„+ÖŸŒT[gRã0ÏšJ}®î†ç_¯_üfúôÇë ŒeÕç³ëobß*Å{DˆÁÁàô‹ÑÅÃó/A#‰îToVïU$È‚`òôüíï@á¸Ãkà/xp¿ª8½ Æî¥0¹"Evÿ¤³¸™>öù¯“KDn›½§öà…Ù¹›–ò×^ÿzuûýÉË?­*íªÚwfïgê-¿0zWæà–ö–`ØÏ_ýº·~öQº´;ïl>¿ù×ó›Gï[‹§à"þúÿ§ÅÅ·Ucâ/ßÖãmÕÛ§_‡³¤Ö;¼øòŸÿ—ÿýë¿üo±K\ ´/ýÙ»öñ·õÕ§n77OßþÅóoÿõç¨ÑÅÝ9™Ýk©qF»+BFƒ³Õí7Œ7Á̪ól|ÚJ fÍ«Í‹_ájSô&zíÀÀ(~ypó=kO³ˆyÕ¬=ýgM¶sb²b‚ZŸÿ¢±|Ö×'A÷ì/ÿá¿ùá·ORäV^(ÊCÚY×&¯“À›¢Ý7û—à’`V1©nvŽ8o.4OqwÉG›Úâó`ú&˜=à ~Ö]iŠµÒ™ö„‹·—/þ\ïŸï¡ +çÎIØp-_0î׺„Ñå½Qóà­-¦œáu¸zÓ9ýùôî7W_ÿ›Áåw¸Ñé7¾Ì‘*6 ´Ó’°¦Œ·j+T«û½¥¾Ìs!n ó\½À×@S³ùÊøs8øÕ»_«¨b¥u¢õ¯!ÂwÔ^¾:z÷»ŠÒf¬­àÎ ë£Ù­;¾ªÄ¥÷šZc*€%¬(}öJªoFÊbϯ0ng|}ûå_M.¿¢6ÀØÞÑ9˜©»ªÖ«H ¨ÆÓÓ/;óç€ZkcïÚ‡_.î~­>/Š,fûíãÙÅ—Åœ=“Ç`ÖÀ㧣³oÂÑ !7No¿[¿þ¸“³‹ëðYógº~ñ”9ô+Ñ« µŸÃ]„‹r”‡é=­ydµÃ6þ6™©µÎ£Õ—WËUÕT¯R®h´øpÍú+šÐÖØ_m0_Hq}¥‹ÁÅ/ÀŽ½Ë"ëWX?ÞÎ]iX½ó?t¯wúäãÕ÷Rc“¨IÍ ³™}±}„Ûƒð¬7fÏ@вî opö•;}f®­ñU¸xªõÏ1£öß^hH¢µÔ8UÛç\°ÁŒQInA†_Góg­Cï+=Ê#ÚcF¼c¼Ù»˜?ýeY¬CÇÆgÚ›ÏAf˜ópò â¹,6ä`¦Õ–ñv|JGi+Ý30‰UµIš-Rozc§{”Åå4*‚ÍÜ-)¨:€ uÆw¬5ÔÚáéëÍÍ×\¬?aÃ%åÍ­´Æ‘,󔥅“p|iu½>tû´¢Oò4¸òvoýÎ꟧p­*¤Ú)Ð!ç,´önϲ¤#€ú|ÿpÊ[HäŠÑ:Y\ÿ8»þµÚ}ºUÒ2¤Çù«4fi¯@¹„Òua5ÃÉ Lê“KÖjcJX‘ë„9V›ÇþèÆì]˜W„ 9¿m¼…Õ”[§\ý8ÍÔp¹ºHkž`RSvÇ£ó \á… …Hsï÷E9gO¿]½­–Ž£ñygórzû]çð]–ôÁ- +æD²'[i>U19Ì^ N¿¶:§.L!®vÃéÙ9{œ$vs ÂxÐa¸™½ëÖáwÖôsk ¦`T•Áíñî4{1 ++Ìš–Ô„Ÿ,ËŒMŸF‹7@¦ÁìµP;¨è½‚ØÌ&­·ªrìÖ=ývpñ½Õ¿…v—B´ƒqózxöª5+j£ª7ß/¿üëç?üÓôî×Úð¶Ì`BÃÕ[Tï#R°Ë\_¿ÿÝìòKwx>…Ù´ÈEH CøðŽß>üjxûç ZD»Û_?s{GŒ="Œ!¬ŒÖ% €k¯$²ÎDŒXšÎiYídSšÝÕ+«y¸•AŸ¤+iL^U˜=­è}XVFiDÝPhIL—'ZïÚ½QW…—NlÏÁóhrVæìŠæùiN½ÑK³w ËšÀMÈ5(Àí–•<`ÆT¨6ïû‡_7—¯· "¥´‡o ¶3¸‘§ÜK*­ Ú_Qî4©‹Ó/:‹;DˆÊàý•ÞêÎ?׺—EÑK2*º¹¼ïLÔú¸;~68ûfxùmrØÞqmö¬{ôîøÕ§o~Ìï˜`Ñ;ý|v÷KkòtÓ€èÃÉk¥jáBnn€½Þéôò»ŠØ€U°ú—µÍ7í£¯@nqánŒªr“5{ðŽ¨àr]­û“³ë_O¾Ñ›ÇˆTOTUγpóg€œàÁÍÁ3€>ÀTéæH·"Ö ¶ ´ë n{ç?ïžÓ¿üE°ú²jNÁÚ¸½ãñù7öàŠ€nžpáFk_ÀËãêrJ½9:}õí_Óö8I8`ÃoIÙoüŸ½žITTBíÍ3Á?üµÑ¹Ÿ¾WQŸ¤V·Mc½qE¬Wù:.·å`IY#ð§IÜBXw}ý¡6½*pžÞ:n¬ß{ãgzëN¢æã¹[àeo²“¥i£NŸ/¾ùëÿaýúwVïb7G£”>:zãôN!2*ó”"M%£´ˆoÈãLrT þ8ËBÐÂ_½­¸]Zqd¯#ù# >˜œ$"ºWý“­rѵ+{ã<©¦ª"¡¶ó„hCäè:û‰U›ŒVO•˜ý “,Ȭª)+Þ+£~bî@‚X-ñ5>XBvBÊpÁJ®“—áâÖ;Ï1þVžGø¡u{@9#½sæŒnÅÚš4ÚŒ+a!yJ4_Ù‰?£ùíèæWc”¨Ê¤ìšáÀªµúLªÍÁAŒÎ¿»úîßv¯¿cš‡¨Þ×ëkÀS „hÝ?ùæùwÿîö›?¸úó,nW×é©­cÒ@ìIõ¡vÖ\åî0µS"u³¾2šG¢?+¨½DÅÌѾP?êã où`Ú=z㎯¹ ȆiµuQ[ðg`gÚz4:~ýÀï˜=¤£¡sAG¡¶™^}Û9xM›]¯sØ=þX[ïÞJ°ÊþR­t–/fç_1f—³‡ÑôˆÏ`|«·Ïq}Ô¶_V „Œp™G D ðáMíþ ï,µú1h€<úÃ%Ze1³*6){¦÷nƒÅÛæúsÚ–’zg|þ•­PµKÙó ]ƒ•"„3»{9ZñFaÿÄh,½á5iR¸‘BDÍÚõu>þÒ\h¶Ï¤`ÍÙSTj•ùÆ^IÞ+ +zý€w牲†)]L2n\w S»ñÆïEÂi-£Ñ5„ÖÅ{ýI1â!Z™‰Š„›ªê;¶9»^\}K(ÝGIž©g(¥SÀͽ‚ðYÍã6B‡„ÞÛŠ÷çäÖ1C9KÆ_ƒDãø ¾/MšB°û0:ðùQßæø(Mú”9-2nÒi£Õ=xwúÅß÷/ꇩøˉ&­øŒÙbݾÖ:´F7ऄú h`ƨQŠCˆ6c¶åúJi¬AaT‡×bÑ>¢ôFUbNŸÞœ¼øñðÕïŒÑ«×`´ï +bˆÊMoøÔh_‚†ñ¦oäÆy‰o”Hˈæ¼ÕN”™2çŠÁ +áš´9²†×ˤÑœ~g}G[ý2ñþR®AÈ}];ü„%¬»ŒZ«Jã ÏzJû|põÃÁëßÅŸHž~+ ð8ýùmkù,ÏFBxÜÚ|}üê/®¾ø׋ç¿ögwÎ;‡§/~ÕZ<+0N–´³„ ±­7ŽDkÂ(­L…w[›áñÈVʜوs—ñ…íK‚v—¯{G_ÊÍS¡vH£4nÁ:ò6h‰7@¬7m¬?‡ÆãdoÙ"$Qàx½)˜MÎh4¦OµÖ)¸§ªPçÝYòžä¸""|=UÑp©%ù+ÖžñžÒ!Ð*wÒU¸ À…YW¾o +³iâ.½áM‰oþ«í +­2©¥0·À7ŠBÔEgõÊn—áU¤‡Ë]ÔI•äýb\r^áëI:ø$Eî—…"©ƒ Tš§µù›‹÷wö¡ß›ýÛ i4©µ¥Ì…Æ ßº;àõ¾œÿ×âl£1Ó›K@-2:Nðí}Ôq§Ãõ;pˆà‘zvÆàà&—?„ÓçYJKTYL”æ‘Ö:"´€¿ì»›7ý«ïÅÎn±I#>X£RäôOíñsè¸!ð’Y\g­Þ¾ íi\sÐ9£WÎäX;§~¢Ì~–ª ¼/†›,îHŸµÆzãPôgv{c5×€òþ ä ãNÅú±7y]_}[W0·BÏ âé‹›Ë,¬Þ/ˆDîÒö$š>c¬®u‹Wƒóo†Ço:ëWBm] %ìÖQkz«×– D,ÒnŽppmĸ³dÕØÊr;EÑj¬»ë%ÆIÅÛXùB¸ž^|?»þQŠŽ’Ö»ç¯~=;ûª,Ô)k¼i÷®íÁÖ:‰¯F@d³}No¥ù¤(æØ3&àž@IuN©™Áh}û­Þ9Éqµ'%7Çe©ƒ({x—íDŽ±¢e{öâ9U5%É­,‹ˆm­Ý¢X"»w^[½…èeÝ¡ r”ŸÀA|öYg•#Κ胧ÞòJn#´Ÿ©è‰¢,™“Úà"Q`~¶S~œ hu@©½2ë#œŸ@Àõòlz#FugPŸßõßõOÞξtFW )c ×Ö ìáÖZ¿_<ûíðòW£o@±€NÀÕãt«jMižX£×Bë©ÞÓ9øvxôÕ9ÏÑ&¡Ö¥hNm\m‚–@•–ÖØlžÿæì‹NIV•"in˜”WêàwfµÕ—ÝÓïo…ŠµáúÎZž‹òL UÀAÏ;š›o)ks^fA¿Í¥úx´,ã—ùzEn“»ÙÓ+`¤¶èÍHk ëU› Œ=ã/Tv“UĘٿ¹yý[ÒYí”Õ$ªzqÚG%Œ¶/ÕÝɻͫ¿^=ÿ-ã/R¨J²½¼k/ŸabèÁÒÇ×®¼sfïIk +ªf»ÀÆõSjëDIÚ-#X|æð&˜`ìiUe¡L[’;(PZ…wP ð'Ü¥à­Y{ŒŠuÑ_ BþD˜ ½º*VAÞàDôGÛZ´FÁà¦6{eo«b«Â¸ï)^_õº(o V¯9¾Æ×Ñì…¢ÆøINL—2¾¤ªž( €i¤Ö×êGzë “Ú{EùÓ}œ·úÅSJkìäùÏRlšŠ˜Ú™½ú®yòÑ8( rÔ^÷ß‚[üé¶[T„šs¡uk÷îò˜ù8©ÞduñÝ“$õÓmž@ZsspSá#ÈbL©SFW­/ÆBÆ¢×Áå°o¥Us;gáô™ÚXSFžà @Á±¨®“óU[fWŠÖF÷JjžCús.Øf)>=Ø”GÖàjˆ^0Œb¸äÝ©?ºâk ÊK ©uÞ™-ƒámëè&< ¼¢õÒ´[„yp&˜9Lb¦läÆã®mÚ8GûÐ'u\i‚ºHW¤¨^ÓçRx$EÈÜ,éÑÆ "µ“„KØ3Ú?P[—ààJb3E;EBˆ³z¼„Þ¡ƒ Bÿñ]’0g™ nÂrWå6®÷„hC{+ ôÎúm0y.×ׄT;FÛ#ðe:Ü«èÚ×ê‡ÀΘÒÛÉsq¯0½ÀF¬·ÖÚ7IÜÍÓ>$#¡4E»ŸFøx·jf¾-+0D©ªY•;à8r´›¨È„1V›çõõW  +Œ·•erõàìËÆð¬ÊºyÜ,2&D¸Â-] hNŸ¯®ÿ÷ÿAvºiÔHc$#ž¢rL‚yTÊV³µÜQ3Jt® “@D1FŸ”êYD(`eLØà€ Qmº_“E>[%«šü“™¬f7¾"Ë?—{Uçg»$h6޵à ª;í­uÁØË"Û.È&Fo‰ÞÄí±F'ÞüDô‹dìSp©¦†K³}ê .µÆL¥pˆˆ~\>¯¢2Æ$Îi«C-Þ‰áBnž`rP{b¸6ûW“›_Íî~cO€E¬µØ”‰­²2hŠwô^ªõµÝ9µF³Æˆ6 ÁR _*Ýà¬,=å>^ÔZb&Y5¾­6Nik¸•ÂRVp‡YÂaíª S¸Ÿ!Lí á¦,õ@‰ µÚßpáQ†‹>ÉsŸæ™®qÖ°Êù„àÍ ˆµ{ÅÕŽ½Ÿ$ P\˜Ò«Š £}áOßxÃgË›?ó†·²Õ3ê‹dEÜG¤²Ø¦í¹Ó¿œÿ ÕÏ‹l}¿$)'GXûeðMÎ_Ó ÝÛ— ð ”³“c³¸0Žimðƒe©‰#ÐðJýÌîÝæøI «@ˆ‚Vê‡f÷2‰; D}PïŸ3 +„˜«ª ²¨–( +É’RÂuË´§ýùeªÌï…ø„žÚ‘kÇÂÛ-Ê?Û)•iÛ5êÉn–£Ô®t!I)½Ñõ$I|¶S¦¤WÓ‹Ò˜'¼áäQè¯ÊGE&ØÉKœ9ê®_£J#K˜pw»ÙÑ¡Í)üÜ)(a÷ø»ßþ³×>ù,Amå4\€2ÉZ™uAír¢:qE©ÍxŸ^§¯FsÑ›‚$0[NÿZiœÄ§\0*ÕJŒ] MàBkƒ"*q!©‚–; gÏ«Wáì©-âMØŒNŽ´ ¶ ¤CC±àMäú!„¡ÁH[yÚC•výà+{òνÕzquBï‚Š&.8.6ÜÁ £{'E'Rí„õ–ñ†±ªB9Š?É“ŽZ;1Ú·Œ{GÕ;7q\™]«q¢¥"÷(oS’G„97;WVû²HØ´®.Þ³Î$í^\¹Þh‚Í,qÞ^Yü¹óñœ$¦õ2±âÊ0^9Lc2®€$˜úà´ÈÕwÊF +umd¶.Ä`¹Ò·A3#R7@tÒ¸¾‡è„9 g¯Ýá ¡´ž~ø«ÍÝŸÖ¬ª {šû˜•£=HÏ `hª­3Æ7±¬Ò>P-PØV†ç­I… !„0¡Fëc«qbÕc¡¾Sà>–¶×·3Ø^–Ù/kUu=SÀ$RÁ“43X¿>Ýʉ{(,Y ÛÌhFëƒ?ÙE?Ý'6*Ò^¢¤ngxøY #ÑY¨á +壭,(K DUƘêÐÌ~™ö¢ÞÑhóü~Ùb½c E¾žgÃ’ØÊrÑã’´S³¸É;3§w!… L®åi@W9‹Ê¤y…ó·öðeMkLêýn}–¦¶ B 00­[`¬XHˆXÚ™’æuÆpË0ÞNIJ¡:DN +W0àh$¸s‘rí +m¬þµÖ>¥¬qUi B–P€3Ú+KÂàí‘èÏ!ت|H)ñnœ~ÿT4$E +Qa€´Þ¢$ÀUYCÎ'Â_s® f5˜¿×{Ï ,Íö9˜…øt·Ùÿž!ÌŠÜ´û·z÷†´FàÄsŒÿ¤Ä噀4Æ`´s¸y”£j9t{'‰Ú;@Ýí“F4Úbt¢wŸ©­+£\9ßC$@'`º iïTdÆY ÁÆ\·â“! <ˬg×fïø‡ÕÕ‡ŠÔ¨ê#Ò[άªÆ×­Ó"åSjÃéŸ'*`\ŠPcª´nÍþ F‚ÖmÞW6 `Nx¦îŒ_ŽÎ.‡‹'É*Á‡(¦Qü&ÄÛ^Iy’“ÊBOëÜòïæÄ + Ý…Y%•&V–pÁËhö:…h9̆Ê·”ð‚¹L ·’¨.s„ þd·šDôŠÐ½….9kT"ƒíŒ´_2kVêŒw@Xs¸aÖ4Í*ñéhk,K ~Î^hÍKL–¹“ë´5P¢M}þÖê?­Hý"×Î1MDè&ªV ”ñ+RÐûàð‹àø ®AwAÔÙç@ ˆÐ,ûTÄt5.Za]LŠ8¬¶Ë·ÞähTªÓz‡3ÚõñE÷à50{UbÈÁœV[ÎQ*Z=C i­V‚ªŒ<‡P©°^ª,±zÇjl$gLªm›6£W@W÷@óäi›µzZóˆ‰k™Í@93öPmlÊbü¸ ø—FÿFˆNhgQ |°‡ŒÐÕÎ’ Ž!Qµ-Õ’¤óÇOŠ€Vûœµ'ÀSûed<8ýîá‡ÞÑ×¹Ud=opIQæ?|û7ÏõߣW’L³ +¸½“ãŽ÷ +ñeö¤>¬M_6–o8wš@´ÑárxPdjû¨ ¢¢*åhuó÷õ?çÁU‰í’Ð"Œ©Õ{Æ{k ŽÕ:j¨Ü®ˆ-Þ5u ¹Pàâsœ;‹©&`©v)×ÎÓ„º:Gz ©@Iáè•6û óA¸ˆ0zÞø–áø<]+³!!× )øByu‡Jk_ýÛ¹ eÝ)!7)Œm´)þìÆ?µ»•(G´GœÑÍòN¦œE…iÊÑasýž²†9ÊI"*£¶£“GÕªárSŠìÑs­w]‘ɸȲº´À:¤5¨¨=Ð9Ró Rmvå—îø™Ù»&Ì$¸QÞÛ€w+Zº*‰þÒŸ¼¡ì nô¸pµ‹;?ÛÇJ„-:£lUIYðãð¾€üáô¥Ñ§ Ê-ŒÆà²+bÀµ)CÆ?Œ›?ýåúåoœ8꦳³o¤ ¶‡RãW‡¤Ò§´^Ž0 Yxg$KPqIDW†@Rú˜Ú+0Á§»Èv–ÙÊ󻈖@Í"ˆvÆ/À ëcgp‹K­ jªµSB—Ú(Rnº¢>Ú/?NT!lp©! +çÍ^-^ý¾sþC}ý¹Ú¼HUÝOö„‰…Öã$U"]à½uÚ˜¿Q£CBËCÆÍ/~n4N@›øÛ¾í^üp÷‹úâ×ÿtóþ7YÚ«€S{¸Úç¼E‰¯ƒfV£1Xsþ7úiÜÞ¯Ú) S¡Mš#€ˆ“ íVÔvñ£Ó/rñ)â8…¥H½[`}¾˜Ô°ê›Îòa†òI½—"LF¸2 Œm-A¡†|[€emJ 7&µå»hõ–Ò­ÂyÊ,0ñ§%6(²5L³p$úkàßÇ9z·@“J£âú,¤Ù}̾€%KâNšÂbެǕ&ônh%DG¬·È1N¢"pþTn77Ÿ‡ë÷¤3¯(€luø¦5QµDVbí¸¶þº¾þàb+K%Ê2*5>._º ‘ºLp„*=°º€öŸí£‰›ÇÔTY¨ÄŸ- c„H-Ðrr4/‹ç üÉk£÷T _N)«>º$Ö¤`æõϳ¤‚Yk‹µ5h­%ËÚvžÉ *°7?Ý.—hWôfœ=˳]DßEÔÝ¢„þ(Cƒ2)±!èÊsÞš¶f°.{%q7ÃΓ Ž‹){PéËœQêH‰N²”ÏK©¶fsú(ÏoŠ<âì‘Ù\›ÍÕn¦1Ò„eÝ-);% Õº\0烵Þ<,¾-§µ¯ôƱäŽi±–Fä¢Ð†ØEÄíÝŠ· éJþÂ<µÇ/øÖyEŸ¤Áb”ãkò¤‚«1Á!í®`!€—!ËBr$[wmÒÀ­r V3G>X°èDà²qµ R¹‘IÄʶ¹x¿zõ·Íó_ ÊRdTjz ºÄ×ú¾àj›*XP¿„†Ð“Ú˜ÒÁä¡œI[½s[Uºû$$x4yåžóÁaÑÞg%Îl—D0þÑôΟ½EÔa– Á¢ +>çjm…°vUª‰ÁbrþÝúîÏÙ`ža½4mqÞ„óWÊ+h}£wg^‹µ³<>É’IDDø@&¾‚—kBÚRî3& öD{˜Bd\j€Ù¿õ/IwS Ÿz‰qYkPbÝ vF—ÃUóà‹Öá‡Öæ"w@è‚.J"q8mçØdž +Á áÃTEÞÎÐû»*Ô>K`;y˜‚w—áüe– žø0†¤]b¬t|I²W’¥Ø"`15@®I—:´_ù†ˆ:9ºègön`¤³¦t½á3z)ÕŽŠ´úv«Àì#%·òí³=,‹»Þàzýü‡p~S5€’âεú1P3hx>< ïºGß—ÄNUa•J?My»ˆ²[–1©Yß N~>ºúu°úÄÝ"ø¹@‡Àn%©Uч|t +Qªu.Át€$ƒôÙ)©–U©#Gç Õ`)UØg‘ÓÀ«ßõ¤™J¨5ZÛ ”ÛßíŸË!¤j£È€ìkÌ™âîÌ<Üüƽʂü( +ûE®DY’37š—¤9á ŒWò— Û@秪0«:ôÊê]ŠÑz‚©ƒªÜ—‚5eôó\­(¶YwU_ˆ–ŸóáQUé;&µ–è­öA´¸ãkÁüuçäëÎéÏ ™âö®ÂÙÌZ”¥mM(pF +¤‰—¬ðeVÿ ¾þrþü÷bë@ f+±ÉZÆêQVËè õ3ÂX¢J|†¤ÄE{% Æ’ÇMB®‰á ›T;‘ÏZ3Ö˜¤*ÆVŽÍb&ÂÖ2p_‰ ”ÁûKÆœÒ~”"¶]âôø$O‘ lÄkoô´>Í»ÓT…/šÕ\Û­ÓOö0°·)9ò“{ {wŠ<©ÿ•úi\vÙYÙVõ$ï\v³%É<éBx”ÄÐ_†öŠÊ“U¥=VoZQ:TxÔ8úùäúGoø\pæJR0ï}¼ €¹P,éI3à‚$aA$`bÓïžFÃËhrs›¨ºpä4îy=)r)ÊBԾܾvÆÏåÎ8 ÆË}€Œ`êà€²»®ªcø}Q’¨Vä‚øû ÝK­kôo9g +^ÀæVs—¼ÞœsÆœ ¬ÚÎÒëÏ£ÅËÑÕ÷Ë7Ã7Nw+:ȉ"JV—ÖZyÒJ¡*pÚºÐ[WR°ÙÍ3û6>=H:` h{PàÀ4¥q&F ×çñur›r`ßäÚ!¢ôÀm±Á1ï¯h½KmÎîéÝÓpù|ùìÇÙ³?3ÇÏËò ÇÔ(tÎ +Sš¸>“¸²gó¤¢÷Ó ˆm±Âùj´2âëuOªædʹ晟Òv¿½yŸ'!|¸¥4ç`NË\½D{»9 +á§sl´OåÆYEêæÈ0ƒCp6”Vù0.…#6´Æ±bƨÈ4vŠ€i2ˆAæ +.÷ÃÕ,ˆüÉs­y +Æ6…ë¶ÍéâÍÿè³L”R?0†ÏÄæ5mMs™I*U5@rö´@¹y*üCÁÛêH ãÏFe¥ÂÕ@uÖ¥q§¬ €\&Ęï Å§‘×ÂÅ[½~HéCBîVÅšÝ>ìo^ 5C{E±…Û³4íUÍ,噈Ö r*‚‹Å*hMê+„ïe©Þb«$TÔªËr+'„¨5¶Ç¯»W¿s–_àÆ + ½ÀÕŒæy}ñAh\£ú4ËÖÓñµ+ Dãóö ø¦´c!Á+%1l>¡ìAüåkP€Òi¯_×—·\0¨êª +8öT¨VÅÒZ)\K¢²_`ì#Z²bdP;]5ª|üñª58»=8ûRj‚ DÕ> àj†pÄhE;³ƒÇ—~ÕÕÎë.òŒ ¶á=ÁÉá€sº ö(wÙ=ù¥Þ»ƒÞ¢R;‹iœ?GµvŽõ |¨vNf×? Ͼ5›¬ÖÄ…ä}mõ3'P9º6‡ó«wf÷$jÙªQfꜭ¸=Ï05H–Ý’@jm½¾*p>í, kÅÇýãLpµ‡Š­DY¥@ƒj’:ñ×Çø&i,Ik-‡$Ÿìc D¦Í¨÷½ŠYQÞü¥9{çÏÞƒ,y”f¥h 0_„1Èóèa{®ÔÏ!ìIµàù$ËÑ«‚õËAŒÕÎ@ðÄ`EeÊÏbñ¦_ÇÕA¢¬ï”,ÒdŒaüÝ ¥™§ÜÇ`ðVpç”1¨µn{sDnïàzš ’ñ׺›¨1#…Ѻ±Gj"Á¤0o»¨g0@˜ø½wYѺ jåÙ°ohUåô ÎøÚ5ëÂqÊœOª€¥=£}5¾ú3{|Ç«<íïUTXˆ²–ÜHáÈkð*Z|[GÅxkôIô•9ÈqÁ."di_¬Ç_ªÂÔæ§IâI–…¬°õø£F¡žáj˜1”[g˜5~R¾™ÁÝ æU¥VUnÂËqclöï*Úh«¬¤ ©ÇZçü R[‰>à‰+9«yH;Æ›'pw«Î˨Yž!}TÑ[‡ƒÓ÷Ëg¿0û L‘#Ù[xýËøsL¡©5N­îu|zíh§$üt+—AuÊAÖäø:›£wá:®ÀEêýéK”·´Ç/Ç׿mþÀ7ÎRL D(½bleÅýøbõ¨$´”æåæÍß5¾É°aÝáSkð\KBmÕwQƒ1ÇZí°ÈÄàYZ¸6°{7VÿYQìÇçaø8 w©Qj¾–Uº„Ò‰­¢ÙHˆ7¾ˆ7€» DÏ€Xr'¹AX#©qîNߘýÛ,éoçÄÝ‚˜AÍdYÊáZ×w2ôv–C¸šˆáÜßn‘ƒØ0‡ æwrc¯z—¿R:Wœ»!õI<'þ´><.RêÏö+U}Ê„çÆàMcýs.ØÀ¨?Kfý`õô¤ÑÙCÔŸ&ÈÝŠÁ‡Ç –Õ¸®wü‰°Vߧƒ6ó¸ •¤iÌË/G—VâŸl£)D‡p­1„ý£Àœ[#,˜½+Bí¦pÇ›¼Ü¼þ»ÖáwJûz¯êÿÿBÚøÿïüÕrßÚÃ@î[{È}k¹oía ÷­= ä¾µ‡Ü·ö0ûÖrßÚÃ@î[{È}k¹oía ÷­= ä¾µ‡Ü·ö0ûÖrßÚÃ@î[{È}k¹oía ÷­= ä¾µ‡Ü·ö0ûÖrßÚÃ@î[{È}k¹oía ÷­= ä¾µ‡Ü·ö0ûÖrßÚÃ@î[{È}k¹oía ÷­= ä¾µ‡Ü·ö0ûÖrßÚÃ@î[{È}k¹oía ÷­= ä¾µ‡Ü·ö0ûÖrßÚÃ@î[{È}kãÿí_Hó¶þ…´ÿr_=™+“«ÉOÚÆOöEÁýhryµ¸ø‰÷“ýQQ¼¸RfW§'“‹g[$Œ¡Ü8*Êí­’jêÐùTU/1!çLõÖ,1s¦ì’xÕê B«ªM¥uªõžš“·lý´$·3¬Ÿ¦µqbŸ«½+¥unöï˜`ƒèýå`Z¯"·òŒ¿jû%±@eÖÁ•:ª4÷Ê"¼"´R¤›&½,T”t¬*·H½_•Ûe±cwnÕÆEIê%Iw·¢íWõ,åˆþ‚µÇi\O£­Y{ŠÉÝ æ$­@¹ŒÞw»—ðŽ%Æ'Õ~¢¬>É0ÌΑ^žôrT€ëc̘TÕaEéï ZµòTPbke¶F¨8Tžô+B;C¸{ˆ 3–§Ã,éf ‡1Fœ5-qµ<îUÍÏ2Ì£ ³S÷‹b™vQ>Â`13‰ÈEÒÚÉÑŸ&Ñ’Â xy†ð*Bá:LVtXTíæ(w'ÇdPc¿¬îä… r¿"tqeH™SÚ5'OÖA•‹l¶fbí€q')TO•à-|D€é—¤Nw“UWºr¸ÝY™ò”p…ëÝ,ã%q£È†¤9¥¬&Ö%·Ÿ@Äý²ˆÇÕP(ç ªOQcVUzqo¡–®*e>äõ1¸'Œë8Ìp°LÃÛ? §·E.(²çoìÑ;µý ÕF96ÊÄu2í,a¥q#Ç•:Êbþ”gëy¦¶_ÑvŠqñ½ aÆň´v‹ÊR›ó×fÿymöztþuQò3´É¸3¥såÎß«½§¨=G1¡ V¿ì}È µ$fðÁa¸ø` _£æ5§®3™§¼ÀÁ?Ö‡á⺗¤ ±•cwò|\ VjÃl—ÕnžoøfkÅNYn&?– BõqšŒkƦq;[Éj\‚,‹›>,ÐNÓ“ˆš­š9ø¢A •éšß¾°š§WÛiz¿(mgÙGI2¨yˆ"©G³²8(qÆXqÖ2QÑò¤[dªÔ(r0ü¡OÄà°ÀŸ¤ñ?IT!2)m 8 Þšhï³™(©Y,ÞÂü“ýÊv†,“&÷quPa£'ºQ¤ýÝ¢°È0Âß)iÌ(-MÌ^¹0…HYÌ؆IÈq%:TëçzóJ­_’úÈjœ¾úæ'_þÑ“,ë-㲊“;Ü'QãcYÂݯªñfب )ÀûsΙTùˆà³Óoø`–À­’€ +M³w§uŸVÅ:¹‰’ô± ÒÛ¾‘ãšU}"‡Fç’±&Òœ±Õ¿äk˪ÞFõ¬/iÏåúQÿð )Z—¸—ÛRíDhœ£ú$Oש`ŠJl%­­»W– ÆܱS`[¨<,ó-TŠ7MO¢*˜Úȱ^QlȳÆæûöñÏ£õë¼è£Jƒæl´¤£U՜ȭëÚü½Ó¿é,_[½ót\K'‚5`í­ÊÊ j,ø°uJÆES‹ÒnYÙ-«e!® +’ªhRä^Q*s5ˆä=ÌÜ%œ,ß.ŠCDóõ‹’ÒÚ)‹0Ϭ¿IbÖNIù,Ë'QV6ƒé9Ò˜‚ŸyÊÍ¢æ~^ØÉr»y`!‡Ù‚5¡´þV–… ‚ǧ¨­ ó$K§0«,¶÷€0W¤±ýÁÝdp“Úq9¶V`êUyˆHÖÛ[Dj}’¶ò,À 0NUjÄ@·3€x¸ûÉúÉ^5QŠ„Sû%¡›%‚îaòˆÕG€´ÚÚ)ò[Eu¯b#| Z”G6DX¿H{Ód +QÊLDªxBUèJ_‚ ½%þ¯·r”=ÔšÇ`yÀ=¡“cëE¾±_QùÓç‘š¡–‹‰E¢5nÞ8–…:*™…ÖNEݯjqÅ!g*…˸\†ÙG”vEë—Ä6ªô L˜®jz}Î_ˆµ•ÚXqÑÕ{´;ãÂ5ü¬ó +M§}Î³í²°‡(%¦ÎØ‹ŠÔ- Œõj¤+ô‡iû¨™&BB›²îik÷‹\­*µ€°H£GÙcÂŒwÍ—ÛW\ý¨¤´•æ©Ò8¤œ±Ì0g`ž N~.Þ#r+› Tƒq¢>ʱ»ˆÂ»kgøÚè< ‚™£¸2* 0Þ€ì#êvIJÄÕ­Ãå#|F +¬—åê±W–F„±RZÏA äâòקŒ°WÑ·‹òNQ­Šm$ÞÞÎÓn‰­Îã91¶ÒtÑRˆº_¶s\ò+|3Y1¶óânQI":ÀÈ'ûØnQÌR>¡¸½_13YN„Iâ¢R]›$aÃ_KB Óg€êéqàZ‘ °3„ñÃí +[+ÑÁ£µ_RöòÂgûxžˆ`!aú2¸OH=\ÉÞBz§(ï#Æ^YI¡fYìRαÒ8‚Uqo7Ïmç˜G‰êã‰òõ°wźˢÔÝÇÅdp ðWû@”Œ·Ç |«Ä7XkJ©­,@d#r‡°æBx¬D¤Þ-1žäÌjów¶–!¬å¡j'®Šz@íCrñÞÊ蜄èqQÈÑpðmLH€å` è4O¾ýÅ¿[Üý"'9ÎCÍåB°YÝwø7⊠bYÂ+ ]ÒZsáž³Þí® w(¹ÉûË$éY§¿Ä79wÅxkÒYͳååµé]‚0ªrG®Ÿ:£Îäµ3yÅGk=Ú¼ÿêïÅöj»ªÕ–Ä^IêÒæ”·g¤ÖUJEƒåFãjœQšðá–Á^üd+ßø$UÝ+Áp¼ƒpF¯ôÎ9é,f‰á +VPÛbthö.›Ë×/¾þ7“w1"¬í¯iÿ,iO¹pSÕ† ßˆ2¸œ" ¾}Ê€ÂKo ×_´7’d&ÊܣÍ1ëÌ(Hg®7N¢áSÞ›çØU:¨—îT[Wöè¬ÔæñÓ/þÚœ?.Š ªMÎ; *D2 @– +à÷Ý’T$¸T‹5œGm„NU…Ø-ò».QÖA6`Ê`ÓîÞÆÝó<ÄIxoÄåJ€©•!ª›ïäÈðIž{”ÆwòL¢,f0£*4XJŸ”{Œ>É‘`TUénç¥}D‡HÙ-H»E)G8‰ª¾g'°,fï”Çin+'€f õ Ü lö*j\•—o¤ úŒÊ=ðfðòŠÐÀån¢bì—ä}@¡‚ýÏ ˆ®Ý‚˜(ÊÀ’Ÿ%1€ '@!8rüúFMˆTh= þºÀø ï±U‘`n;%6L”dˆUè$ÂFÊyœç +Rëcõrw§ÀÅ–@’oÀZÐÖʼæ_Á†[¬Úì/_ÖæÏþ0­ë}ÖhõopÍús¼=f­¸þ›Þ8Sê—JýŠN…ÚyEí QRÛ^%pÔxUêz£WÕçí㯚Ç_’ÁÌf}ñVn§˜(/4+JOiž½g`Öhg ]bìq\Ý‘púP¹£4NÎMsùò4Ó*Ëè¹Eh}€ \sÞ²"w=abIc²¿,4ÔæYóàƒÔ:ê‡öðºÀ×v­,4!üÀ‰ìTänZ µQ\2µ¶r<)6õúaYll8à&Dì +Ú8zÛ0“ð ¢ÐÏQEêã…(ü*n?Êr[)‰Ûðp7‹9¢¾[ŠkòÙÊ2 ãAüÀZ£B‹7§vë’Ò§)ÌM÷á·® ªµ€Û ò·ófî£ú…·H!Z‰‰e#…é*Ð"Xc0‰õdü׆à0í œ3'ÔX…fp«*¶8gs˜§üÝ‚(+yÂßÍK€c êvó" ažðÍg ߢÀm'X*ÁéÀDB‡Œ6Ü)ËÒä£ö$Ie0 è˜6†@»[E&-OYSÕ?~œ-Å¢š ¸ ø9¢­)mOËR(a^ÔÆÏÄÚXóVU}P’"P˼ÛÃU趟+hž—¹:$/ÒHîåÙÆ>渡Õèñ¦{¨‚ˆ5LërÞœ´F¤3-©½‚ÔRšgíïåÖ)Ø«ª1®¨}Ö_‰õ36<¦Ý°Ûðü4é@tA„d)—vBí‚òŽÊú´¢Ï@oïUõ‘æ‘`–AëÙ¦œ"B=UUikT'ÔKr»¤tÁó‚cýÙ¾JSNŠ0sŒ—c|¸ÁÚ=)ÈÛ%ŠVûZtƒ4L°püªÒÓ,ªÉ¢\ XÇI,‰(U±Çz'´u@(Ã*×Þ‹³˜ó(C}šÂ–¨¦ªàÚYx¡زSÀÏB$€5€À€9§ô˜‘$ê¤P Œ¡1è$ã³J'OX?Û+oei¹" ¸hÐ ~‘ X}©ª?Ý.lgé½" (í*wA0árö‚s–ÂÍ׉åÀøêà“eÞ@éq +ÿ4‚ÛÎ 9Ü/aŒ“ÀI{¿$<ÎÐÛ`1ò€[ ,n;Ú§gß=ÊÒ¼]ÚÊЦ΃Z¶ºJ;SÒ¨þ(Uùãljíd` Ìo".8)æ¬Ö>"‚Ó8嬩èÌYg’¡â”aœyYmï"|µQ)bŒ.cÔÖ9ë/A`¹d¹ô|ž‹ªJ_mœkÍSÊí£ÚNY(òaQˆR” Žè"{ôF‚„uy>TêÇró,Ž"Ú#Ì©Þ{áο kÇqÍX2>ÛPkû„A9K¡qÍÖ¯iˆ¥—¡¼nZŠµÃ½*l1.Œv‰ ÙÜ)Še¶Æ¹K@§­É±Ÿ&ñŸî!1~â&"Ô(£¿Ÿë³ ¥ž(s’¥Gûùljrº¢’JOÜ l™“í~\ÙtÕc½'ÔöÊB20©¾_UßaøiÌ€îÁ/Œ5Aµnº*³î°,Õ³¤ ²ƒÈû¨"Íê?gÜ%Â5”æa’Ôa“ոll²ª"|¨6Ž¤hÃ9c­wV5ú¥‹›“<×LQ~oðÜ¿( + Lé€.g‡;‹,WKR°¤Ú¼ÔzW˜ÚFú‹aí"úVIËË¡ÕO@hògíQ·¶òÒ^YC”#<5:¶»×˜ÞË¡º7¸¤ƒñ6Âm#ò.ªÁAJB½>{­¿®ZS°Wjã°È)„KYZ6¸š{ÏÑé3Ý#xß÷ù=?¨·ª4H‚`älütb½“ucŠ¯ë}ÔÂà h „É6¸‚Âa½ÏCÄ-^iÜJÙüªÑ«˜‘ˆ‹A.€¾NvR± ' žgÔFþǬ”ÑÿØŒŽÛp³FK@¾»uÔyðæƒ2G‘¡úêñGÍXäÖQû¨ —ö~ÔÆ€Ž¹Dˆ‡ÑÜ|4ß?8n™4¹=у)V?°" E2ze'SËË6Lu`qG fô@9‚#VÌêÉ`œç°‘˜° fïÿÿMæ>.3øIjÉæ–‡Oߦ󶇡¾ ‡/„‹`x¨`­µvÇ·S64â|Åz …‡«±ÌLcõ"®¶1“n2 ‹Ê0ñ™LïB¸ºk£®‰NN°ˆ ñóˆ%"6,m±ÙEImÏo_eRÝ/M¸'ì<8^ÈqE<ÜqsE ¨XeÀi`ƒÁH@ºgN†›d¤JêÁµlwßÉ«_5Á­¤ê²™ÎÑTï®ÑÁr¡·?îa¾ßMi¨˜J?l"À.BxtÓ…Çu +\½›MÉ[ýA3"AÏê¼’P!ù’¡ +€gÜ˹PÌ‚›NAfqR ³bŠšžÞ½‹ˆµÆ!æ£Q2¸T¥b]¹°ì‘œ3ø>á`aØ͸™„œž°™ñÁ—zÄb¶¾qâʳ‰Æú!cÀK×B?l£=\ŒÍâJ—Ë­šÙ‚Ù#ä:úxŒ@>\:x'ZíLzùƒz°›¯+ý¢ŽÛhÍNª©üüéËOüâ­ãäµ–ƒPÇìÌ!#~óH©Æ¨ƒo—­±øƒ&¯`E•Ãàoz'sJcáøý†@ø€µ ¶VÔŠÆ@þ&¯6{kw&›Û_uÜ6f1ú,06h[IЂÅŸ<¾|Ô„K‚¤vØà$X¼!Bª°‘†—O Û(PLàÉÛtÈ(ˆGF锟NbžàÀ"b¬êyŒÓˆ}ÒFZ<2D6„Ï‹ÉchóQ36fÁ&¬ô˜™€œiÓ¤œ.ÌTç=8î1á0#½Wwp£6êÖI§—LRr ¸úèŽÛuvÚIäÜtXž3¬Gô6z¢ë¤gHXðÊMlÓ,-yë¤wÈúË8°˜ ’8J6<ºl!l ø\05‡Fšv:iFó`ay˜‘‹¨|| W}„µ‚WµÃ¥§]LÒE'½|ÖAØ¥ØXÃ-d;…Ì qÔDsÔA9)ÍÇg`‘¦™ÔœlZȇê„FÅ:áòºXÞÄÓV63Ạ¸ƒqÐN6FÝÉä}rËBå˜ø£öÁ\M¸“?hðð°n“NYç†ô‚8æ¤èÝ·LØéÝv§6#fçÅd+”éŽYI”‡®qÐ9F­,j!bÕà H’8ÄŸ1;a¾¹…êã!•„ļ\Þ8ì8.h(xÛa{Xï6ø ~©@Äš^¡¨÷…Œ~ +äà?4á…€¾Öyõ°e&¾d£RÃ&lÒAƒTA&]0ìÞqóU>» +<E7û•q+yht³89½u )Þ·C-d3¼­OšÄ:°­ó…dºÉŽFn±ŒèQ«'ht¾ y¬£5ÇrhR#bv±”±Ñ?¤Gá6É5ØøЀ÷¬ÐP>þ¶qÇ°Á?dg=)¢„ò„7S$W<04ëüƒO÷!0˜ô˜‰Ãƒ#«0#Îì‹z¨x½K·2c&€VDï­‡t,A­ ÕýBÞèïÈy€ÀÕ&,nÂR6ºÀ'WÕ}.·b#ân: Ô¡÷‰€a°‘7=@Ĉª Ú#ÒBØÉ뼂K¡|Ö/Uùô­ÍZ©ä•rr9—>„¾Æ´e&¿‹'æá¿Œþ¨LÙ˜ulÌMáñ29¥¸ƒG§lDZ!ÚNÃ’ÂØMz‡¸QÍËW½|ÅzóLöfglDÌ-d,dÌ>஌524¸ÍhEÁØDt‚¡O¬¢JÇNeLƒbH[ª›ʃA +sS9ŸTwi'‘„~=ÕÙðQ¸ó!@—/~VÆI§[´vjxPÓÀa:¤óXhO±S4ú5ƒ_sÒ„+½è¬Xqˆ½·Tƈ(.2E†Û:°ú¿Í´ºøI3¡·s>¦h hÌà‘‡€!"ìxÂN¤=\VÚ¤\3ãF‡8n&ÿyÈü·MÞ:æŽu¾uÌbwèæ–¥‰6SÈòÖq/d“17ftNQP[¨ƒîsøBÀ¨`ŸÀ6!“PäLP¿mÜs`Ü3bÄnwÿóˆ,™Î)ŒÛY(Á¨™sÀ0»I'1fatvÑE&',è,aS #k@… LZN÷-¸28ð/”ZT|QÎm"](=tºèû`H`]Úˤ1¹‚…Z^6&ÄB!T&˜šóì*“±^¬qŒK/YÈ$¤?Bíûäú¸ƒ×¹E&³Žk V& ÅrPiB¦S‹E'CC%Ç`­iD#cÎŒF!!"B ä Š~ÏŒªz_TÙÍt'[QÈøe›²uÐXcƒCv\ÍH¼ÁS@ge\ÎAF€ßt.άøä^»ng¥K©ÓúsræÁn`ÄN¤ B2³ ÃfÒÑÕɘp@ðs £@°V–Î88ž£‚VŽÛ…[G]`Hå`l€Ã'œÁ WÈ/5Ùø"í{˜¬Ÿ+Ø°ÈAƒò騃u’šúØjœœÕÃ#TbhÒ}`Ô1jÀfpD-€­Á-ƒÙ0#ÊÈàèhÄ…§Ìވïœþá þoÓüŒ/vüÓ!ë°<Þ`œØ¨•5ûcC&â–qŸOYPÕ‚&íXš‹í,b™`Õâ ß2æºuÔ@Õ;Eubi‡_Ž‚@ÊxÛ„wÒÆÒÑh½Ãfâ'42ÀlÔ +aô‘1|›41û£CfìD¨ò8¬†…>¨C'l¬Ÿ¯±î¸ žÌº˜¬›É™ ˜[ñFÿ•„ÀË%ú.£s f4bD +¥‡ðhcÀâ"lžj`eØì3!‚R=bÎ) ¶Šüb‰Tš¨Ò6ãƒ-k 2éá[ñ+ËŒÆ9@ª¤½mÌ1†ßFOXÀMÀP é|C†@ËÆ…zÌîÏx©Ð…ÏèìÜ?Yn=ì6 œù-‡ã&z|@ ´Á?Y“/‚ˆu“_wJFlÒ± CvÔÎ @¥²Ifb:bôC´<0æ7¢—88èg¥'íœmp†;d}眨bG(Ð!ù–eÈ«†aÛ9 7‹? +T3lBM"#&r ÑˆÝ`ùˆ6Èž~倹r„²î¡‚fÕN¦=BiÔ aÁkAD³Wp0l£77â!Ÿ +Zß+æn1"z_•Ë¨XþŸô¡õž°Ñž·ùx®˜pË,‰Hu\iGÊÛTznØÖT@å*dí:ß-“îÛ È„‹‡ü A{Â)Ù°$4àøkÌÊ€[³áq.9n蝹5ˆ«6€¢€q G+•ôb1cBî,‚1 ŠÎ=Øü`r—ŠˆûÿFZ)™´"€|ˆQ'>ˆKÀT°bT¨"¦g†Í„ 8Ó.ñ6Qcà6]ÔbÍ,Ôa#þ¥aÛ—‡­#†€ÑrÀ옌Ù+錟M™ÛÆm#Fd';i£u6FNÛBLÚÁw¹¾tÈ8fÆà‡àFU?Ÿ‡ 9ae:àS7ÕyNá#NÊ78©Æ€‚ÿò!ým‡mQO ì +(f$j„ ‰¨x°vЄÝfŒ:Eƒp(æævO 7L¨ +R±Ìÿ?ÔԚ݂ÑÉ écjÌU&nwB÷Ù ÕˆBÔ…®Óh¨`ª!déiàFè” –Ý7 ,0Þàîäf¦H'n¶ÂËñ؈‹éw ðqØŒCÒgc-13Çh}3¦é<„qˆŸ:eD¨¨*ÆW„øŒ‹ÍŒºùa %§æ½ô5 Í>682ÂHÐkgÜT\„ 6:Ù+òA8Vzƒà‡ ~,ñ„° Qï—…e±á)Wò’éA$qqС`*@ FMĈ‰³rÐÆ•t?ÙܺÌí–qƒ'4dÀ@§F,ü¨M0ø±ò!}`X‡šôëÑ13ypÒ`3ÓF—ˆò9@,Xnóàl:ò¶1çax$0Æ°Ôü¨•<¨GƭĘÑFý¶+¸8Èò€UƒS +%ç]¤òO‡Æ¿tP7 Ã3 #FbÄHB;ÙX0WѾtH7¬óB4l £ö@¸+`Õ€ÆoÕFìƒs«Ænf‡C:ÿä`5 îiƒ );ìçBc&'wëˆcpjÀÛ#Îïò*^!ï–Ýllƒ‹9è,¬"rÙ =+ñX3©9xx€Ä“8©<»Qv~Ðûð*?¼<é“tàäQøß@°êfÛ—~.C†Š„RÆ‚U“ór9/›Áä’ CZä´žÏCLsÓé'wØ ž9škC„¬Ù'Záa;=è» óF\t•‹`ÛÀØ;˜ì¸;xÀD2Qàä½ð&T +ØÛޞΠrÅF'¾<áºm ÙĆÅÝLÖÅäldÆAå€Ç& üì(¯V?¢Õ‚7[^‚Æ™€€f§L¾A2‘&$ll°¦ÁK[h"ÚjÒNC3ùã¨X'ä:Ä +`õ±¨Â8ƒÕp3)"\Å”ú-z "?€8í°5zÂ&_Üà‹[ÉÁ©&:OÈà£n÷Þ6æ!»ùÔ¤*$]cG£(›18ÈQcÀä ƒ>¤CÁ›|Q`c©üÿpëø—†uˆ} Ê +TßMônðuæ¡ ‡' €»› &ÒÊÀY9…ÁŽ•ÞSì'PÈûFÏàüUज़“ŠÀcÜ-KüÃAÓ—Ùàí¼‰ÝàgÌ>8ÇšÐåb2V°7bðƒÄ ‡M'· `6´ M‡…›àZuî’q'7lld-ž´4´ •3’á–„€®ÁÖ‘¶áIHâÀucfÂèâ‡õ>PUƒ;¤,‘àbG§B¥1+6a#.Í%¦7O(…Æ,À”ƒ]'“r­ÆÁçø#PA7Wr0y#L‹éý#àUï­ãž1;kœ=«xØ,ïóɾ죡º ÖÁ-L:Ø+£C"j}ÁZ@iDk+l¦iˆMöüẠkÊ…åXû_Xõ‡jJ³£A!ÕóÉ` 7 N#…)5"R÷ÉZƒ•OãJÏΠá*À>‹MÏ*åµ`aNôÜbÉÅæYu +Œ="ÿ” ¦h¨‚+ÕXeÍ*›pÕÁæ½Á&íáj߯´\ô ×Äì¬Ö¸Ì‚_ókV*å“«ªPÀ"u ¥ZIÕˆELž†)*ÞåÒótb°êãŠîe% þ¨ÊS¹Ä ñÐFjãnÁˆpø`R|ÞÓ']t +ôÚ€Hzçã2ÀçV" øø§MÑj<¡‡Ëù¤¢™LŒ#Êo˜¤´¡¶íLê0„¼mÂ%\b µèÄ”G*ºy ^ÍÉÄ-xÔFÆa ÉXÇ'U`N6cÅUp‰&$dt‹VŸl‡4á“è5¾±×ÍfaåtÆF$'½|®7/±I¿Pb#mqôh=€œÑ‡´ˆ +6Þ’òÓ\ªCkµÔÔ6›ìC¢7¢ªW,û¤º‹ƒŒSÔº¨µ\Æ€EJ ð3lea05‹Rmôòt¬lÅÂ^VCå‚W(AŽƒŽ&¢ÓX¸ñp‰ÃV¹yB¾wP †ŸE£M&5-ææ°X–9Kªµhc=ÒÚ"â-¢£÷ψWÈ‚;òŠ@%);«T–¥Ü,«” 7í­ƒSÔòL¢Çg§€‡Áî¶æb!¾e µ‰Å»d¢Ïgçb ­³MÅ«”Z–ò}>7Í妥Ô±-çIÀR¤æ—ítÂ+åü¡%²sÑòzmñ|mùœR^b’=4Ò@Ã6Ñæ’ÝpuC(mÇ;§éä´ƒŠ$hº(°os¹E_¤å 7Ãõ]61EÅ@§Ê0k6»H&gðxM…s³µ¥sT²7æ“¡p z|fu«ªNZ–KÀf\Ñ{97eâu>Õ +ç¢M©°èÔ]Y_{ô¥wÄÂÒ˜‹“ÝêÂ9)·Æç×cÝ“ry”}~÷º[*ê½25pVK©©éþ™ââ…LÿèÚ‰û6/=EÄÛÑÊZ}íŽööåÖÖ¥ÅÓÍíkÝ£÷6—Î_â[ZgÇ'e•ÂLª{&R˜?S]½Øܽ^œ;ÞÛ¸}îÔ}D¤\]8ÖÝ»¦õŽ³ùÅâò…’ó…t;R™¦5'«ñ—_ Õ·‚µõTÿhzæ„T˜.ÌíÄ:›ljJÈÎø•²˜›æäü|måj¼¹ïSx¬¬”ˆX…RÀÃ@›0e¥¶•íŸÎÍœ’sP®Ëh-©0¯¶¶¢õtw¯4{dùä=½íËÀ–d´…+Îj¬^(ÌžL÷Ž«­}Zë·no­ßa§bQ€DvæTjêH¬µÓX¿<µyíÄÕgJs'ml†Ï-ˆÅU€=ô—_—+Çrs—èHà aD*Å©µe!?©¯J…9¥¼˜ÚgS bÐøXu)ÓÛ‹7Ö9­JYPÃâÂ9>ÝÕVãÍM"ÖŒÕÖ’S»ÁʲOi ù¹ÜÌžÖ\#£e­»-f§ÔÚbyñ,~¨4—íïž¿ÿEµ¾ëÐX9ßÙ»QX¾˜:©®’êøÛ\{ùÄÉƪƒ”…D=߃¹œom],¯]jíÞ aMÊ.C†µ"¤Òfuý®òòåhëh´{,ÚØ +çÓÍõüÔ† ™°„) +„&&gI©¬‘Z—‰–‹½ÝXeÙAg˜Ì|¨±+–Ê./éPÙà¼|ÒÇk,Ô"·´ cËÌžMÍœ¹ fçaq‚Å©dg5ÞÚˆÔV€U:sû½ø6¬Ž…óÓÍõ»áÑX¿À Õ×JSÛ÷>ôÕ»žÿ."çê«ç:{÷TÖ¯@ÿ66¯5·®‹¹…cçºúÄkR±?æá):b59u¤¾~qþÄý…ùSÝåS<ûFª¿(LMvwSýc³GîÙ»ó™¥sÏ+•öìÑÞÚy"Z£µ‹¹Eµ¹[^¾4}â‘¥óÏÎ-î\ì­žÂ#e¨Z°´È¥û|fZ­­ÎhæôW¢µ¸­íèx“T[°bN¾äàŠÑÊVqጇ+˜2PÄUà+.3-dûå™ã—î±±|  K—3³ç´Î~cë*t +“ßgK‘ +tº›‰G +³jcmjçòÌÑÕÕ ¸Ö „j•Þñs÷|P +±òleátyñ<žaÆ뛱Ɩ\˜‚ùYs@ö@-RSñæEwóνËOWÎ,m_\Ü»ƒŠUàåÉî6`¾±~ië£|1ÙX_Ø8{çC_Ø€—ðˆy வΙüâ•Ââ¥`vfiûöý³7¢¹–3É)&»$×#Õ­öΡʆ“IŠÙ™HkðÏj°=reÖMk¬Ÿ¹ç¹¥ã׬dø‘ʉÞIµ{”Ï.€WgcÝÓ\~àE9Ù##uµ³iï©Ýãµk`‡l@æ<¨yĉ‡øD'XXÌöŽ–V.%gnÕvü|¾1{JHÖ-8GDrñæF¾´<{¤4³Ý\> nÂËpZ5Ó=–™:¡µö@ŸL7×ÝÚ=÷ðìñ.™žÚ ˆ46‹Ëã½cbqYH÷œ½ÿÔ]OñéÖ¨›cÓÁÒ’›Qê+¥¥³‰ÎöôÚÙ—¿ù“êâ)N­Íl_^;ÿdëȽý#÷¬œ}´µs/«õ¹ïäOƒºß€MOM´w3½#•Õ‹¹ù3L²»súž™­óh¸˜ho§¦…«ëJu=7}ræØCåµkÁÂrwpæXyØŠƒƒG¨]¤ƒ„!ÄÍ&š›ZyºÐÙð +).Ù– á¦zt¢)¯–—nç ö²°|„2Êsé.iŒ¡ªŽHâê”ZßµQ'‹çÄôTeñDcí\¤¹é´±œkñŸj$š+­Í; +óg¥ò*£5ÙDË,!áj¢µ.,º˜¯™ž]“•³ýDcƒOv{K'ûkgÉhµ8s¤¹q±¾z¡¶r{såtef7œîÞxð¹7ßyîÈ5õ‰y\©Gk[jûD¬uT*,û…ܹ+=õâ·Óµù@°˜›=›ž=—›¿PY»«¶y›†?–»k“ím½O6"aHˆÉ>0ùNuéö ¾ÚÊ÷³M+ñ‡«àjP¥¾"ÝÙ[9~ïüîU:R-ö¶­5$Tð\iËù¥DûH²µçb4%Ù‰äç¼B*Êå–©ä,®†3Ó2¬÷Rx(«Î×–NÍ»GëíùÂåq0áf=œ + ¦:jm+Þ=–Ÿ?ÙÞº¼pæ!©´ˆ‡‹ÙAÔ­ƒö-œz´»{#Ý?–îî¦I¹i3c +Ÿ™ñŠŸ˜Vª+ÐÜúêí•¥lºí壡\G­¯${ûùù³é™Stf²Ñ\h²dêPv† 'Xu yøH¡ŸŸÚ6cÁLg«¿_añÎ`i»±|9»p–ÉôsÝÝ#מC•²î敃‹>Ôž\ÙM/^W6õˆŒˆét{2Tª¼|¾¾~ynï¾Ù™îqµ±ÍE+›§î-ÍìA°£Àr§„ÜšT\KöNJÅE,”Y?z9][B„¥Âª®ËÅU65#V“Ýc‘Ülwí‚¡ò¦I „ +NFâ­\g³ØÛAƒy ”v’1è;È`ãÍ­hy9^ZŠf¦a­js'Sýãlf²*¬ÙÈ$&Þën^ãÒ=©b‘—ž.ÍŸ-Ο†Gyî ¤ìDuùÈÅG|†Ÿ:.•WÀð÷N<Ôݾ¬Õ7übþäåǺ+'Mƒ@š’óë¸ñS3á! ®p@ƒƒÀÁ£U êXk7Ù;N'{FØÅ$)¥¬d;“nÊâѦv!S !ÈhSL¢«÷p*,§ëÁb/Xœ{±ˆMO«õõP~–W+ÅæêÌÞ•H}ÕðXÑ:bfŽKÍË€F(€3ÕÜÈÍÏÍËÎÄŸ“mµºä ­Ô_;óÈÒ™Ç23§¹dÏÍ&aHÊPH(~eÔŒ™=,̇òÓrq–ÏÏ„*K.)çkËç¡š˜RŽUW‹óçÊKg3S;B²¦¨<éäÌHpܘt1ÑòÜ©ìô©üârmC*ÎÛÉ0Ÿhe:ûrnÙ¬APÈ&b²8½W˜=f&#v2Î-ÔÝŒ†©MDíBBÇÂ.ÑRS6<’íínßPëÛ`Wªs'”òª•TCñÆö©{"¹ŽBÕumêdiùrsíº\XA#-”çÕúÞ©{µâ¬›Ò’ÍÍîúÅéj} (|~¬0ߘ?¡äfgöï«,_Ž”ÖÄä´˜žÁ# ˜i\Y¤ sÓ¨˜QJ‹Áü|uþtgë*išÀr+ùk¼Ltì„•™x‹‰ƒïݬ,^èn\µúƒÉÒlwëN<Ö°A.û8Í1Ø +Œyé”JØÑàôÆ%Ppãvm§BzNS:Žri jÀžRݲ0‹>cAè¦#LJÄ+sBvšI÷CÕ5`PnFÍO¯î_™Ú<‡S>1¯.7Ï6W/%ÚûBnNNy„ ¬gª¾Å"¢DûT¤¾|añøsGî%cÅ|mfãÔ݉檶éªFJ«±ê. ÀˆbÁ’“Žš¼,ÎCio_«.œxµtò+­ÕËñÒüÂÖµÅÛ‰X¢d@ uК˜˜%‚U3"˜üB@Jq‰Œ\(®¸ä +È“™¦¡Ýbuú-ôOÅ 8„Kd¤ã4x87ã¢õ 3rXï°Dn.ÕÞËõŽFÁ~ /—.ôŽÕ–λ˜"eøô4Ø<&ZÁƒ.C@VJóD¤Š‹ˆT „ë¡âJnêX}é©Û°°I„3}k@v +t}0¿®RÌ.’¡šWRJ枪 ™ùÒÜÙîæõÖƵTwߌ…']”Í/˜ýœ“Œ€Bœ‰×7âÅ9!VÍ1ÊE¹8­,Ò±*P©HËN4Ôîmí_xÔN*'ì&é§UIë„ÒÓn2®w0t0ÿêwÞí/›°¢n<ÊÃ;l„K›Z}_«ï na#"¼øü˜ ‡…¦r2q,\ Õj0Ù\Ø:¯u—Í”ˆ“r¡'交) ]P,7'eZæ’°æ6<ìeÀ‡@Îd¬ +‹¶rôjº»Éç¦ÐhÅ#g|‚ÆÅ+ÉΛ€ceКLkO6ìttÜÃa‘º>¼¾).Ùˆ›ì&:G›«çË '’­U.Õ´ÓÁúÌúúÙ»©XñCÃM:9'fæ9upøׂÇÀ²j]J-Zš‹æ³Óû`Bú;WkŠ³GåìT4×=röÆÆÙûÆܬW,Pñ¼pa‰ +Õ\xÌ Ed5„×`‚v*J§úxbšNÍiéÞi¯”×ûX1QûD„óVèV"B„Êd°LÈE0x 6¿— ãÁ¼˜š¢b p6<ʨ-èGH©FDŒ7VÔö–\^BCMN"*iÍBï(*e&ìĤÌFEHMÇ@e:ûJ~qp ŒƒBøtª±1jÃñp>˜›ÎM…±!rÑ‚-ˆaÁ ”ÂÄ¡=K‹ê«—*ógµÆv T· @Ì™ÖÉðÈkíëåÅ áâ*€yÔN¹X5)b‘<­ˆ‰žZZÕÊëœRÅ…ÌX±#bq‘„°I³G³z'ÃG«{'oÄÓ­qƒ?À§¡û¥F†ëƒ/65éda=“•UB.ãá²ÁKÛг ¥Ð/ÌïV–÷’½¥äÔby~}æÈí '/6¶N²yPÀb¸0#›ÍÎbRVça옄r1.V fÚ¡B—KUùL-ј=qõ±þþ­=ïW2XA)vŠó[ÁÖ‚™€çG3Õ¹p¶­s`˜”D¥´) '!ãÏË«)ÃIJZk^*v¤B3 æl|ÔIÉÅJemK›Yõ*)Ÿ”äRS>X´€4lpš¬ˆ]iîË ¬D*I1×DC£å…lP áb§µ¼ÓÛ=)–ÚN>&¦ûrnÞÇåL>yhÒuXï±"¢V]‰–Àäœdì™ÚÚ +‹D¢ë•  Ë^I‹”;T99á¥ìL˜Ëvp­f&å@0‘lo@XÆ#U€´ƒT©Zš=ßZ½"ez:À&¡\´<Ñ€Vël¼Ék èëL+3½Ê·ÕÊl¼¶žhl+…¥I+ù¥!ómã.LÊ…SSV{hÌ:¢÷Y<2mæÛ;©ú¬NÚé1sÀCGÉhù6£÷€Ñ3lA±pCkSË[¤\1{e!Ñ⢹l½7¿y|uÿܱK÷\}ä™ûŸýÚ3¯}ó­üäƒ÷§¿þý“ÏÿúpáÞg* G)µ>¸DÝÉc\*Sœ.Ö¦ËíÅÚÌfgqsi÷ä‘óW¯?úÌ¥‡Ÿ9óÀ»—ï=qý¡ >sç½O½òÆÛ/~ó펳+û³e*Zðqq!ÙÓm<œ‘âùBg®¿²½´slïÜ¥Ûïyð®ÇŸy諯žºÿÉ£×½ôðóO¾üW¿ûÎ7ßyïõïÿü‘¯~c~ÿ</º™ˆÕÏùi%šnÅ‹].–Œ—ê­Å͹½3½íãµ…ÕîÆ^çÔÚ©;|êÅŸ¾÷ᇿþÝ׿û£Ó×i-î«Å¾ ‘l¾ø™„VœË··ü\Bp¶¹¸WžÛJw–ÕÆ\ª»¼zòúÙ»¿úȳ/}ã{—yöüÿrñ‘ç…LËŠ)Ž@ØâáÁBpц™=VóóZ$ßÖªSùV¿2³Þ_?½sîžûž|éÁ§¿úíw~ñîG¿}í{?9qõ¡þÚé\s… —À.Ú!3"âÁ,¥”XØì“6¥– 3ûñÚœZ›Þ»pãÑ}íÁçÿíÒƒ_}ô…‹÷>qîî¯\{ì…×Þþù/¾vç}_Ù½t?-ÙPɊ𘘓%¿ÀÇšZi6”¬Ç ýJsõø½¥Õ½³w»ãžÛ¯?øµ7¾ó›/þúÛ?üõó?üåýO>{øÙ¯gë ‡Íب…ö±9%·œªíÝ’ÞNº«”p¹ä"ãn"“¸”N–zG/ž¼òàîÅ{/Þ÷ÄO¿´}úZså\iö8)˜ð[†]Œ µ¦–W¶N;}ÇÅË×{êÙ×ßüÎ{ïúño~÷‹>þà£_ÿçßþó“Ͼxã;?|ü…WWŽß™›>â ’\%Û¡h>™)5»s[Ç.œ8ýÜÕ{zâ¹×¿ýÃ׿ÿÓçßøî/½öÍ·ò³?{ù›?xûßú×ÿøû+ßýÙ#/¼¹sá~0cÙÖZ}áH¶»"§*™Jk~uýÌÅ‹÷=ôðÓ/¼øÂ+¯ûŸ|ðëßïÝ_û÷Ÿ½ýó¿øã_ÿ¯ÿûÿù€ü³/¾ÿ³_]ü«µÅ#J¾„‰2¡X²XiM/mïÃcãè© ×ïìù¯½ôÆ7Ÿþú7ù/¾ù½ûÁ¯>üøóÏ?ÿßÿç¿?øõçO|õµ“w>˜®/ðJK‘ì|º½ë¡5/ «…zgaóè™;î~èÞ'ž¿÷_^ºû©_ùö~øîG?þå‡üË_þø×ÿúå'ŸòÛßý›ï¬¿ Ì!i„R3t¤.Î…sSõþÚÂæ±S¯?ø•Ÿþ×'¿ö¯½ùöÞûøÝû³ó«O?ûßþü_ÿë¿¡[ø‹O_|íÉÆŠÁËOØI`N£WpàTʉZ#S›YÝ?}õá'®=öÔ¿|ýýêãŸôëoþðç¯~ï~ýÙG¿ýÝÏÞÿè÷øÓÿüÏÿ|ò›Ï^xí­“wÜ›¨NãRÜâÅŒnÒ…‡ –[˽ŽÝw\¾÷ñ{î•·¾ÿ“_}üýŸ¾÷æ;?ýõïÿô—ÿü¯~óÙ‡Ÿ|òÿùŸ?ýà“»}¦>¿—é숩žup6rÒBPb’‘S‘T³ØYmÌl4g×6O]¼öÈ“ÿúÊ›?ÿã?ûâÿþ“_|øëÿúßÿýÅ_þöÕ7¾sïãÏ\¼ë‘PzŠRš$™ê¯¬n;qêÂ¥+W®ßsã‘Ç}ëÛoýîw¿ûâOúð“Oßýå»/¿þÊūז¶öRµžR˜ñÒ g Dð*#D•¨V©6wöO^½û‘{yò¾¯<õ쿾üß½÷ƒŸ¾÷ú·¿ÿ½üø‹?ýLçk¯ëå7Þ:{íáÙ­s•™m!VV³ls.UJ›³K;kÛûÛ{»×®\ùÖ·¾ý­ï~ï{?xçƒO>ýüσrüüýO>ýõoþú·ÿøôóÏßúá®?òÈìÖñHaŠSËl4/„¥úÔìÊÎѳ—÷OŸß?~úê{^{ó­_¾ÿÁûŸüö÷>zë?zïÃ÷óÙgúɧŸ~ôÁG¾òæwÎ_{¨Ô_GY eÁd'V^ÆÄ,%&bZ±Zïníî>üÈcoÂ8~ø³—¾ñÖOÞýÕúË_þö÷_~ôÁ_|þ‹÷ßùÕW_|õõ­3W¥t áS@V‚ÖTò½h¾ÓߘYÚØ?uŸûê˯¾øÚ›ÿöoýäÝ_þõïÿë7_üùçïôƒýà_|ñÉgŸ¿òÖÛ=þ\µ¿ã¢T£›39Y"»‰(PD©³–¯Ïl9ùÐSÏ=÷Êë/¼ñí¿÷ÁþËù¿÷þŸ|òÅÿøÙï?ÿù/ßûèãþî/yú™sWïKT¦%cAH£‹´j8U‹gZ‹ëGn<ðÄoýûß}ÿíÿôÓÏ>ûâOþè7¿ûåÇ¿ùó_þïóÎO~ôãŸÿôçï½÷èÓϹtßüñëŒZµvØἘhqâ*ÔšY;~îò‡éo¿üæ[_ÿÆ7ü³_üíïÿõçøñ»¿úÅ»¿|ÿÃ_xåÕ³w\[Ù:©d{ùî~8’­·f*Í©|©:µ1jszyacgýòõ‹÷=rÏ×/½x{¾V”¢Áù¬—ŒÂèÀÍ."@Èj,[,4ZíéîôÌæÞ‘ý½ù•ùgܸ÷Žû¼qáêµ wÝwôâ•öÒj(Yàb•p~ž‹7çòs)HžÂûÇï\Ý;ΕâîÎú¹ goÜ÷ÃO<ô½ï}÷Ý÷ýÞ‡}üéGϽüÒÉKºKKÁDVJT]‰ ‡ +±ÁÄÞ±KÇ/ÜSî-[½™™ÙõµåÓ§=ÿü“oÿàí>úäÓßþæí¾ý7_õ•—Ÿ{æ+=p×îþ±zo9Šæçý\ÜA(ãÈ5¸ÁEÙ½´(Fò¹ÌÎæÖ7îzýÕW_ë­×_ÿÚÏ~öÎÿøûÿí_ï¸xzw£9ÕË5:>J0º0LˆÃÃKGóÃýô»ßÿîƒ÷^yúáûŸúÊ£gÏŸÛÜÞjOÍ¥*3µ…“¡LÿæéÐ4貕,NÂëç2ùúöö‘«×®ýõ7À+=õì³_yü+/½øâ;?~÷ù¯þÛÕ«w¯mîëm’W ÎØ!““1ÙýN„ñ8?)ÊZ%Qî7ú«Ó+{)äÀ)7ÉÛ¼a¡Ž~N5{˜ƒ#&ƒõb!?u¤¥à¤‚‘àcC±t%¬Üîô\(AðA\Thµl!” 'añ²›ð01‡2ÚMN !‚.„õâ§À{“RD +EƲL:›.T˽¹ùí“ç¶vcÙœ .|Â0x(+´"²ÁÍš¼,«ä‚©:!%&LIj4ž +GbQ5žÎe’ùB®Þî,®+Z2‘J·»S•r9$…}ÞM„F%äl(?æ¿ †xK ”R#p)?Ÿ¶cáIî%#<|pÂz`ÜvØä›°`^6,ò©V›"”² W1Qé¬c¡âˆÉ7jöNØüN<«nÊ…%.Ù…ÅŒç[r¢bC€œÁÃÛ°¸‹JÓj/Ù<é¡“£ÔŽðZ®çÂÃ73aÇuNJe¢56ÚºyITæ¥c<¨/¬˜ ¢|6˜šBƒ™Ã&¯U­'&»©`'“¨\5"á1é¡¢F7uˈåÀ¨MoòíJÇBÆCF­ˆdñoóŒ˜qª8‰„KÂÏ€XpQšÁɺÈ(¤þÁô¬ôÄàÆÂéPnZ­Ì»éèˆ 7"£éÍØ¡Û¡Iï°‰˜°1Î@4’^tãÚ˜%I14á0؉¨9 øø¬œ] Öl.&åt×/ç­þÉFLxF~?/5wp>9¦]MâJ©f¿bFB~¹,¤çÙDßA¨‡R˜9­l"B`cBB#FD —Zsg=„z`ÔŽYL*2¡RPmÀ_&,ømÃV‹ŸY:íçÒcvrn6KBrF«ï1‘¶ÎJëaшȸÉ?ª÷¸PsôàtÓžš£”Æ¡IÄI(´’ (™ÃV¿ÎÍß¼ h“KM)……òÜÉx}å¨XPªbzZHÏ¢ášJš±øˆ…³àÐbƒküuh€Í \ÆMil¤ÆiÝ!£Ì‚:ðOÌðÙY¿RÕûeÒr­ù­Û©xk  + {ÕË n1¨6û§ŸÝ‡ yL(ر¼jDïvú¡y½[Ü`‡ÉÆj{ý݇³½SV,fñK”R6Ð{‚^:ÇÄÚR²N¶„Xƒ‹6ý\Êì•\¤¦³°í#:ŸØXN´à…f˜RÊz7kôpŸ°¼uÄ>j¸ÕMªÿt@7ªw#¤ngÂ!Œ;åQ;›ÞÀ€º­í^înß1ä '}b Úáò«JóˆÖ=*•–¨h­Óßݽø¨KLëA‡È:ãå +T´'æ6ôh8ÁPFÍ ( +. ¹ˆîñ2¸å5«#|,ÊøÃE”Ãbu&ÙBä¦Tùì|¤¶ÃÄ;>>Ðu²qBÎ j×ê^^sÐ1;¦ |jlpÿ×A½è ÄÔ™‹Ö0qpø4(Úè$'츅ˆ»øb@é᧶8­ƒ°Z$Û—>aØâãÒ~!èRRÕ½ËD0Ø„LX^*€“ëD¸H£‡%8uçä µ4;¤s鬧_ö1:Ú # ‚ArZ RCFïˆ% sQàW1/¤znFÃÃÅþÖ%6ÕÜ”æ°ÔÓË&Q.#'»Ñ‚Õœ4úQ*†ót¾›ÿAßM¥ƒ©éhn–Ž”9µnÅ£(|¼ úÅmf§Ž··®àAè¾`mf?×=ê&ã“Ö\ç``T6>añë<àXÔêF´ºáæsz_؀ŭ›êŸª­\`Ô¦ c¡:¬9,š½œá öÁ½¡­éÐa½[ï¤Í¾0(ˆ X¼â¤™tr@}³›WøHùKÃæÑ™DÄøt€/ݼ¾[;¨C f{`Ô¢wŽÁÆbÔ†ÇÈx,ŠVš3íX(JuK9_¸,—W±†Oʉ‰Îòîµ+O»„”—KðÉ®’_Žæ—ãå5.3?d§ >=5wŠ6nwM:h71¸¾2˜žm­^ÎMSóÝíÓ÷¨‘r´¾šèî%§Ž¤§O¤gÏ0éYDÊƳÝ{©µvZÒ‰í¸â¤4BiDJŒ:eöI":9¸¯l‹~±ìÊ`]JÍIÙY *=°›Wô8h ásŸ%B6ÖÈ´¶˜HÚ3˜u1}ók*ÑuK ¡Z±PÎ58ÞˆáZ(7GJyZÎK‰ª“nÄ'W&Tì².Jõ ™@¨DDªJÕ{8“é´â¸”'‚E1Ó/,_tK#vÂP\xÔMÇà#¬XÈcRó ÓcVüËc®ƒÿ„ƒu`1L®³±®ì3*´F+‚9f0¥âãRv,âc“$x]­mñ‰Œ˜:÷ÿ’ô^OŽ¤ç¹ç?±:Åž¶åQðHoÈL „÷Þª +å]—éꪮjïÝtÏLÏ4Ç;z#RCŠ)ñ<:”âèìj#¸{qÎÍîFìí¾˜@LÔ`0@æ—ïû<¿'Í÷ÝÍ(ùo\´]š :Pì`2ÀÆ/™ƒf/GÊyèš`(£•–cÍm:݇C‰«U?p²ë£—fÑi;ãÂâÁPŽVÚ¬Ö·!ú¤“ Ð (o\*NY±iyaÚ?zè Ѿ,eõò:@ः€¨2š‹NٙѳɸNG›J~›Á†³|4=éðO¹p¨.÷1,\ðò©YDÆ£5¿œ³ÑºX\fG·ÿ­–n”– BÎê……pa>À%|´æ&•Ù xÁA |ºÐÞ#Õò¹)¯‡ˆÁú™tH­¦Z;‘Âb²²´{ó¢•ðX)=·Ÿž–®½ƒXkU*ˆ”nô¶~ô³Ú¾ýŽ•¼¤F+&ÚŽ–vŒÆ!¡6y½•iï;©Ä¸ö‡òd¬ÏeÖ´ÚÕÜÂ=¹´á u©Èɶ Ô> MzY_(ÉDël¢…†KBz>ßܼñèãÖú=s²OU.®ðùUʘç3+á܉À'¤î¡ãd¤âD¢dò„@‡õü«–¡¶§œ 2À ¨X+”èf:ûzcÇÃ&,B ðÜP¼ £¤÷ŽƒÑ&k^ŽÛˆi'áÀ>Ù#£ÍH~%Z\ó2ÆSž ³i7{ÉŒš<H©Ô ¥†Gê>>i :Ñ#b-O(ŸðqY6ÞæôF$ÙnÏïyÈÈجÏ” 1aB4ù’=?í¡#%JÊÍ8‰)‚qqÞHaQJBfSêX¸,%{¨˜Íd…'¼LÖM&œ„a h&oØÏ‚e4¥ðr7®˜`óf Úy´0GÑÇ$§­YƒòŸ½1}~Òcñ(Ÿ‡|ÇÄê ä^:®fú¢^<;i™r vTvœTò°)ÈJáüjvp]ÌÎËÅ!itØô\(;)¯1É9 ‹vkûÑó/ëë×Ï›|¶ àÝ7¨Àq’}ÞèO¹«‡„Ræ€AÇ*^À,첩l +(@Ñ3`ï"ð ¦Ù±˜m4EgÊÃç‰D_i]+;n¡à¦5.Q§¢5T.br‘ŠV¹d7ÑÜR=¡€¶ØýÂ4ÄI4ƒï„¤†©c6,)Ú´ú(./å–"Å5T©#áj(¹éŸÆëûdÏMû=Lêì€Vë>P9É6Ö Å€Z²fý +ƒ»„<žš›q³öÆÔ”Ÿ B¶u1–€L„˘X ä +JϺÁ£Å' ¬‹Ž¶¼®×·bõ­öÒõ›Ï¾-¥{X¤ht®(åMÖèWæOëÉöþ4³¢¨@ e•4¨œì§úšŸKÂþ‚ñéžœjm>»(èõöÂáé‹ïY°0­Æê;éîA¼±ê]IvÂÅU'©…¢•p¦7 ¡ÏISjC+oõö_—o¦ÀuÓ>i#´)?æ$ÝL‚Š÷bÍÃþåJn>œÄë^>~ÉŽxY]ÊÎu/ß;}ùí½WVoÑJ"ÓX;|Ju.º >­º«núœ^Û®/†ô²Q­«rn Ël¬¹À2š­NÊ4·ò½ý)}fÂ=ë—€BF›Ðª£5€ ’-_ƒßµ’²œíUV¯7·ï—ŽÒsW"åuý4VZÊ2ZW6n|tãÕWµ½w˜ô²‡OO¸\È'ª[Ó£ûF07¥ƒEšƒaˆÿBzÁ”Ï™‚&ç¥  ;'ðòY&Ù‹·@C:ÅÞææµç¼Ñöó†‹ +ûÃ_¸¥•jm­ßx+/My'Ë/Þeži42P™;[ôÒ L© JUk\f‰Ë £õÚÚÍ+Ͼ,/_?g!ÁVÄì5æR2Ô% ==êå*)üù%ÛŒO`SÃHyK ùÔâ” r‡)qÓ±¥«ÏŸ~¾xô¢¾ñ@,¬YÉøEëh*˜“g?ˆW75=tRL ñŠ10µÂm>ÙT³íboÂ8εÖo·¶ïgç¯ðé6"¥<œá¤F³“ z#@Ǭ’{ÈXÖײs·ëÏ<£ÉŸA^²Rª¦9ãã,ˆäbÒ¬12‹¸Ö€(Q™ß/õvƒlÌO„SõÕ¹½§ë·ÞߺýúôåsƒcVkœœ¾uÿ­/ÈhÉEÇ\\‹¶µÊNiáVgã±/Tøæ”ÿh—²‹| +reº{ÚNlCƒÐ 5Æ%{Ph¼r¥”l¬×w ‘Š Q3ÍýâÜÍDe«³v«;º¸:ë¤Xåî«ïó™ù‹ÊŽÇÉQ\DÃ5è;@}?ƒv˜ñ²€Žji-VÛ¬.ŸìÜy=Ø{‚E›R²¿wýe€MØPEHÎÅÊk °zu‡ÔzcvzÚEa¼áÀ”Ì›;Õë;¹Åc`,\ôRZ4ߟõ…f¼!(­•Í¿®m?Ë-Ý +„+g&~ÒؾñA0œ;kòO¸9+G! i4¯­ãó¦à¹q›ÙMGç[üS^~Ú'B…äšY:…_œ±£n\ž°¦›p3É \¦âíîöƒíŸÚ`䜛ˆB¡²ñ&—êq™©´)V.û¥B€52µuLJ¹ð0$Rf£u >HŽ€|Ñä=3n;?åzcÒ5f% ëG«úDGPE ©˜ÅCSJ‰KvÔʲV[KÏ]s‹¸RQ2ͯyˆ·:­àcB¢£d–œ¸nóó8—ðÒ±°,BÊT(@/¸‰8)•0)ÉŒ@³C ñ žt†,…TjñÊš`´|”¤U)YáSÐ) …å£úú­tk;]]^¾Ÿl¬ÄT$·h€Í¥u½¹ÏeVÝ„&#­S‘┋™…lèbGð€Eq>á“*þQ¾ÐÞ˜q|cÜâ@e95P ©öQwÿ%g´…Xuiï±TX˜õñŽ¯îÀ„‚’™Ït÷.Úéq I‡ë”ZŸö„ÎÌxߘñš|<©”¥ô¼‡Ö¨ dÚŸ²ø¡Øè(Hqyp ÂŽ ‘`Kƒ«˜T°ä' ñÿÜ´wÆI¡¼1ëaFw°CB‹RfItøô®Õ‰X£4•=Ó¥þn~pÞñJE+³â +m†s«ZmoÆ A˜âÝH~CÎll3z×äSÏLgœL¥½ÙŒ¦‰³bVL¥ôv´¹›ž;U?7 'ZFin *á ^\Hv"¹£•!_˜£³+vT£”2©UP)Î-ÄšûZ}¯8¼«7ö„Ìœ Cú.ÍÇ‚Ö&! hf$jÒN@ÅÛÿå×ë2Œ&«—ÑPÎG&PÚˆ&ýµS¿˜‚€W\>Ù¼ûéæýÏ‹ë÷Cùeø!9ZXX9œ[»Ò›D4„¯qÂGFÕdËG*/)D œZ„Àâzdr³~õ¢c2yØi'yaÆ;šjÛËûFgh;”2º|æ¤T2œ-Îm·×»w®>^y²víekãŽÞÙ÷ +©\}etâTNÛPÖ*¡x´B‰“`vãÉn¢xFK§e¼TÒHØäb(©@Cd’!­è¦Tø­€À§â-1Ù.t¶w¯¿ÍêuØ‘/Vœihù!ÞÇ¥Ò¬WPSí\ccÆI¿1ážqó\g¢-è"RÒfe°?㢧£ +±ÊVix£ºzsxåe´²½_éï,î>˜qsN$âÁU¥YíB%BLC7eZk”R€J³C»!a&ÞR;ùµûóûoiù…¥Íë?ûÕ¿F2 Sž0iK¹Tk÷ʃÏvïÎóg§?g”*ôXÃ×Í¥…ôy15äb €ó) >5ãå&!L¹… —dñ)b²Oi%“—=õ`à$,~΂‚ç „Åïбf$¿lmG*KjuÍèJå &Õ§ãõrÿp¸ÿŸìN{s@u‘IB,Bbu +¢5 ‚éO8¨ '3ãá\ˆæ „„¦$k•ù9ÝÉw î=Œ7×­­Hm ÷}|NI4V¶ï ¶ŸyYcÌ[Âgi­ÉVÝkÍ*…Te)ÀÄF'Õñ(n +¸gyÆ'Âθ™i'=Î;a'G3á1DÎR± +¯:[¯¿øù·ÿæ¿4ׯSѲZ^VÊ«åÅ“lg·9¸:·r"è%Q¯ðñf(Öä>"”Mùü4rvrt…JËìá&¬Ô¹)ß٠ׄ™¶A®KF·6Ôß¼3éel¸êf“&N'šñúN¢±Ç]VõÁ^¶¹iG#?ïõf’ð‚`ž,-d*K—¦}ã³H8AȺ mÒFλq…”òrªkH^2†BRЪJaÈî©«jƒ’óµ¹½æâUà:ŽòI?­Û|¼ úzðˆšíb\|lÆãÅ#~6AiÕPª'dçá{ü”jä[‡7^®jÛh'›GrrNÏ/*Ùù€X¸h&B‘b¦² ˆ{vÊmòHn*ÇÄ{[owv^2zç’…`”¢¯Ú|¡?¿hùÆ´wÌÉ[‚1„/Er‹>>éb4KP83f‡×%+ŠEŠZ}B­·ÕÚåHi‹TôÑ“Ñ=+á24àîðúÒáÛ{÷>;zò½Õkïá‘<Î#bÎMÆ¡ÆíÄù)ÏølÀê' "f‰'Ý¡)WÈîçÁ•¼t„Öó¥ÁåË7ßilœØ…s¬ïdz'ÑÚöè>ÛÌüúhB)ç‡-$T,/\FÍu®–'lbnÊÅûˆ(.—I¥ˆ ´Ñ)¿¯Ÿ x Ãâiñ…&ì¨"™î`çÎpÿa¦»=غ±ví)—¬ár&’ŸÓ½ù»•ÅkÞPÒ‰K|,·¸u3Qš‡4äÆU;sâq/•ÂżS]AyܘMÇç‡á›õ[‚T¸¨‚¤—V¢Å!­ä—.?*Í]ñò Òh¥ç¯—ïÂN‰ÉîÜæ\k2;*ä´âr4¿Ìª_ ‚‡¹4ëÿ‹³f«;`ö’oLX/Í¢£óö¸ +œ9ã Y¼Âèì'tÁìw‘QLÌ'¤›;’Ñ‚Z·à(›¢•qqÊ눿à¤ããtÒœuÂt“›¹hÅ'Øp†‘ VÉâRVÉ®=útõð9`[ ”eÔxz¢¶ƒ ?)ã\lÖÇë¹4®Qr5œ^ p™É„“ªQÂù³ó`sVŸ4fF'¬Ø¤“´Œ¤Rº8ãrà*., ‰š }bÆϧq¥ +­g´w£²vô¤·}/×Ù­-u¶ïÕÖniÕ "R0*C.^µøC^B™õ†À;¦mhÒ)¹ &ZÀc…îöÔpLÊÅ››+×ÞÚ¹ÿIeñøôÁû§Ï¿”ò ~!‹ˆÅ §£­H~-3w»¼þT*®r®Ø»’ì쉙9ˆ-îÑl·ŠOˆ‰?›ùË ×7Ç]ç­D@iðŽT1VD•©¨oÃ…Ëzq~ëèñ‹¸}óe¬µõõeÐêpÿÙÜÞc6Qu3ª”™¯-g[[áTÇ(ØHzÖ…é*æíˆ<ífg=’ ÑÜ„`ÓpÜÏŽ;'͈ÅÍ°jE4X¸àóJ²¹°u=šï!R"RZhí<\¸ú"ajþ“œ³©Âpïökw`V?†è¡^2ar„¼¨ÆGJ½áÕáå‡oŒ;Ï\´@aCi1;¤c-8”`jÙúÚâå—Ý bR>(¤Q!í'5)Ñ@Åô¤ %B#FõгnÆî—¦¬ˆ˜‘Ýhøâ”Ý… +„”²xCPiéúÒÜD•[ju3×»JE€|l߬_6ƒ8C³.ÎEÄ©X³—fƒÐn,l÷ ®Ñ5ÊD€ÔÌN|ÊŠŽ™X(Nˆ©¿nr˜]˜i´º + !ƪŒ’ß÷±©TÅÜ ?w¹:¼šm®Þzôîµ'ˆ™)Ū—£å­|ÿdnïíæúÃúò-!5§g›WÓ±¬ç]LÂ3šO¬ÌFïÛñØŒO@'õ _xÒ>kÆÇä,¥°.çV˜HiëàqmaŸWåüh:—ÜüÖ擵ãw÷Ÿ~/7TìíìÜx!VÄT—·p¹à-Å^ÀÄàôÚ¤·¡a©‡¢sBb¾—J˜`Ì•’J“vòìŒ ÓCgÁ”íà†tâkïâj•Ò0V.&Å&º½ÍûÍáµ\}-ÛX + JÉD’-ëh ”ˆ ‰š}²#¨aL²ØجöÎNxAÞC„Í#>IaZUk~.ÓX8l@'!dÉ©†h4Ym´†o4)°É\u˜o¬Í8±i;æÊAZ£t¥i+nràŠQ‹çºVMò‰Úpíäùêé+¹´ ×ÜLzqëÎþÛT~Þäbîp!Ǫ-6Ú£´¶m´¨Åi•Úòu%ÓuPNÓÌâ¢=ˆìÃ"’šGB1ª’baÜ‚ý'¨®IŸÉÍù0Õî½X ¥¢Ù¹ÛÏ?õ±~ÞÀ•"“ìÊå½¹cÔWV_|ø“ÊüÄáâÜÕòÒ µ¼*e—p­m!§êµGoÿ„Š7þüüÄÙq3„Ä +ar‡@Ø)1Õh-ïÞxJÔA¾P­BDKá|O¯-•†ûéÞfPN«¥ÅT÷ +ï„!f‡¸RŒ6£5”ô|¾³ÍêY­Ø†˜cE£N2êfô™€<åá1©Äê]“ôñÙ ¯xf&0ã—´Á%ç°p™Vvî×æ÷y£)-gæŽ++·»O·¯¿jlÞRÊKÝÕkþø7\zÎAÇ ­FGë–bzéëU¹ç!ÝàzWFk¦|=-°íëA;0ÊÉH}ÜAO: ðG¨±ÑÕçÑ2ÙmìÂÂ)—h²‘\kþJ +"¿œ¯öKWéhÅŠ…ý\ª"¤U lš<"$Ys!Q§WðQzcfÔž‹–¦\䘃a¹òêÝDç@HϹؤ“Ô¡#”lÛJ…€ ès$;ÍâÎŒ +Éìa@F,Æì¦á™±Ó:°¯Ÿ\óÓ1N+s±²’Ÿkïѧժ’lûh "?Îת˭[+ÇÏWŽßªï<ðð Üæú2Þ +ÈD.ú…&—Hì Öî[qù/Ï›ÌÊ…F­>Ùì Yý!'|{©½rõ>ÉX©¨RÝà3}­²œ_>‰õñXU û'χ{÷¸ê¥â¸\Qò+í•›ƒÍ{Ñâ +y¹¶ðÞÇ?*Î_ûO笓ÜŠÊNÊPs‹Åþ±˜^t`Êòþ)Ó?3ãwR*á ¥!©A&ÒJÃ\suiûFwó¦ƒVi½)ÌÉ™6¯d»Û›§oí>üÕªñú¦ÑÙGÂ÷ûP ;i@¥%ªFmÆÉçL~ Žcjþë…VÓv4vÉÆ^­c.Ÿu›|´ŸK¹¡Z¿œì¦ºWÝ´aä矿þÑö­×`ÐBª¥T–äÒPÌ @·Ý¤æ@eN«—§D¸:þ5·[½<Ä„³S~?“¢nÍÜ|ò>:9 =:Ñs…rl¢Ÿhî;-g{½•ã“G—ç­AÅG$FkT1I”]ïsy0>kP˜…çá/ZH“?ì “Rv%Û>ˆåû­Å+ ‡Ï6_qç[Ë×^ŠÙW‰p>Ó¾\^¼†ðIN¯„>stream +îã â.:åçSzy çÎÏú Ó-¨êa ˆüP½ ýÙÊÜêîim¸ë&£}¹·÷æêÉëå×­õµ•“Hq!–é¾÷í¿½ùÞOÌxÔËù¹ãþÎóLû$Ó¾&¦l¸i4–í0jiÚß5‘çfÉ)·lÃŒ XŽ•–¼¾ñüSöñBusiZoæ{{Ź“Ìà¦/\ö1ºÞX­öò©Ñãö|ÖAhN2VÞ-çç y˜DP,Ø” ÓÀaÁ‚!¤Ùh¢²à"cÖ@˜Ñ©î~yñúâþãÎÆÑ…K5{ãÞ;ßûê7åÞÖ´G$Â52R£Ô¦—IY‚N:RD8cÜ›ƒ"­Õ`¬ˆh#5š–çíd}cyãäÃ/~:¿}«¿÷hþÚ«âÆÃÂê½þÁ[KWž®\y’î\Î6×úË?>~ï>R¢5.Öq"*Áƒ÷¤XtãìÂFk6T:?í=7áüÆYóù I“ %çY½•¬­szW+L²OÅ»LrÀç–-ˆDÇÊzu:"d4’ÝÝÊúîÞ“ÎþóúÎÓxÿ˜Ë.èùáþ髵£˜ZÂÕZrtWÉ.“Z£b‹£åÝíÔS`vt¥€·á&oÈAD=Üh潶­-e;kËGÏ–Ÿ_¾óîÜîƒTo×[^.M|}ÖCªßè~3Jk×jË·c• L)Ïú7ø®L{h3äM¹DÅzbv#VÙaãÍ)oòqA> ¡fÆË@ä“xu%ÓZ‡Š©6&$2íÒÂÕÂÜ~´¼šhn%›\¢Ë´· v®»é(Ä·dkk4•MªOD[£•F„ _>~š®­š¼"X˜‹Ly˜<¼ °ÊÒµ¯‹‹0¹ •VµæåêÊÇŸß~ﯻ{oÄb¥µvùäÎB%ë—++š›/2ý;RfȃGo¼•míž7ãà°î‡MŒµ¶•œøCéJg«5<„Où ­Sz˨­ƒqäÇZa€0Yr±ú˜¹8‹R¹TsrzÑŽ*3n&¤×3ý‘§cb•r¸ZpÓª¯gšë¡h©¾zcóι…ãx}ŽU}LRg¶o¼óüã¯ú›w‚|NL.rÅ”í>ft‡ƒ ¹0·0áÌâ´›þ³³¦ 3)iqó.<ŽGêè"&êMø6H‚Ù…ãáéë­û_T7ùt¾·?y7ÚØ2m?¯‘²1·y›‹—¼œFÆê˜R…¡ äJfªKJºãÂU{ l Œgqq'¡k…a¦µ >rv %!à°ñ¯e:[ÝûzsÛË&Ö7OŸ¼úRNT‚¡Dyáfaþfª¹/gVL~aÂAø íäö«rw÷Òh‘Y5 øÔ¼^¿œ_¸‘îBêæƒw¯Þy"¼Bim?¹òøÛ[÷¿Tê»N2 µ¾ý×+»wLù‚ 5"BzY-ïàáÚE3êBÃÕÁAª³7n§œ˜ê¥ãÐèXC)®†óKŒ’?yôþÝ·¾£dæÜd”Š¦š;ÃýGGO¾¨­Ü&”&9½k ˆç¦=q~öì¸h¾ +Á©cßs:ƒb5ZΊÏzXTÌd[ÛÙÆF¬°0ã¦ÏNºgܬ‡Œ¶Cq´ ` l4_ŠñÂ%³WŒ·º—,¾Óܸ­n rutr@N?}ýyoõš)ËârÉE&f}=e"™X¾µ.Œ²•Íâ“Qˆº2ú¡äWjZºuÿåÇ7Ÿ|LÉ… —m®=*.Þ2š[Ѥ/“žq±v„¯ÌA×\2£~È)j‹‹hÈðú^‹‹jö·¿õÅßOšÑsžgÈÇ&ycN˯Ʋ’¶—÷Y­àMÏÕ¦’‹:œën•†Ç•¥ãâü^[–R­BïòÂÕ§óûw*Ã}O­¼Ì&ûˆ\PRíÆâ!oŒne‰Œxç •ÓÂ…Jð7%gMn +Ò +"$ùAgûÁö½Ï¯¾¨¯ž,®þý/ãÙGühò«•ôà$V¿œêöŸïÝÿ¤wùQippûí V*.¯§×r‹w—¯²|ã£æåÇ•þÁw¿úûßý¥ƒN˜Ê´?lB"n!JcÕ­bgçúƒw–®>.¯ÞhlÝ-¯žê­úúõîæ[ï^{ó pÃBo÷øé·_ý`ýú«êêmµ¼Bj£{;½oÅ´/ë¡.ÚUd¢-1Õ#”2kÙ0Vë.22íÁ€Í˜ø×+Ö „Òv, ᫵rÊ'z±[l¯õu1»Œ)MTªàBÞK(c_¯" %áÆø_,~i´p€›Cù¬ÔáM”O+aRê[0º@eãN†ˆP̉±>6ÌéE%ׯ/Ÿô¶ï F+(ÀFŽžéøúŽ;Ü}x¥¼”™óRº-1·"¯0ëfœˆh'£ÙÞ^oç^eéD)­¤‚ÉËKѪ–éXƒVJÁPJR¢ƒ‹Y*GÓÆ…(ç'ýç.¹ÏŽ9§í´ŸÔ‚”JÊY`þ3ìV;í ¿9î<7ãwÒ3žÙEÓ‘l0¤¹x´º«m3’žênU†Géþa8¿Š–µLÇh¬PªAȪ`TØX%^ßNv®@Ö³"2Ô0k…¢µ1 6e#` #\Êêã¡ÆÆf}ˆÁ‚\‚Õ«je5ß?è®ßXÚ>9~ðvó¤4eíú{óWÞÞ¹ýÑÊÕ7sm9YW2ÝBkmïæ )7ÐÊ«Í» û¡’ííHaÀ¥êÉÒàÖ“¯>zßÅ$Âù>½ U¶–OÞž¼Ÿnm÷÷~ôÕ¯Ož. Rí-ÀíÆÆéñ³Oßÿñ?}ùwÿõïÿòàæ;_|ïoŸ|ðcµ¶®VץªѺÒÙzºs÷ËÁ•÷pµÁF + üâ’U,H8 ¤¼|F(¬UW*¥• oÔÛ¬ZŒ^¸¸Jw&½]^8Ù¼ùÍKFEN·I½UZ¸Q[{.íø¸¿²Fßy-ïg ðVûzq–è”›Ãåb¦½G«;¡¢‘rkóQañŸ™ %»´^u‘£Yh<Œâ@9„QJŽŽV˜XEH6)µÖ@~í=H7×A™XÓMhOÈ nmº1=Ÿh8¨˜ˆ`á¼&ÛGL¼G„‹Œ”aä §ÄDÓê—&l4&d¤d‡RëR#¸v~*Ud ­C¸ð’±' "<º3Ð'b\þuÊA]²`ãNÖŠE}¡ähÅ9Îv3Ý/›Ldjñò€Rsj¾/ç!%…3ó‰Êš^]!ÉjÉ +"$Äôœ\\…lœCÊåhfË/Î@ö÷ñ>Rua²ÅÇ_2ùÎMº Døˆ©øiL„ÔÊåÁ•5H¾§/rsÛ”–Q²­îæm½²ª-ÄËó¤R@¸„oI‰¶’é¡bÚ¨¯æz;¡xW*>œÎàe%UOÕ†J¾gEåTk[)-…s ­Õ•Á^"[ow‡ž¿înJFµ³ztøøƒov÷½ïÝzõåõ·>ÙºýbnóðÎÓW{·_uV®ïßû¨¹ý¤½ó|aÿygývª}9(¤béö½wØÙºsÁ‚M8)…6gçg]£‰#l~ Å ûÐÛ£Ô2ÉÅ +ØÜüM1¿Š†+^2uÄ™OÌÙ"ƒ’›‹–‡t´bã€Ð“~"\ö‡R3nÀáE3>f%¦œÌ·éÀÙ)ÿè9+qn³Sq!3—V¤Â‚îsF׊h’–EÅ„ÉKûBQR-Æ*ë‰Ö¾ÞÜcÍ–:bºéÀÄHf.;8Iu¯æ殑JÅ {áaf츢״dÃêe.™<g<“6²0.Q.G‰ùR/(&µÉvÓý]!7 õ +ŒI¶„ÉI‹5 ÕF¼~9Þ<ÍyÅ$ÀDätWË ³n2NÈy)ÕÑ@(ô:t½ÅÏO9œ7AÞìc|L a4)’H¦«éRŸUÒr,_î,×—¶Ksˉê@HÕ¥BOo¬äûë­CíáÁ•Çw.?€‘”ô*!>Rö"L2ÓX;xno@V §ñŠ’ç]/wI!¬n^¹`„ÚŠ7rý£…×+×Þêï>R‹K\8õðé{¿üý_9zŠªU½µo nÖö^]{ñãµã7 ½ %Uzë£ï¯=7Ããv†Ï,Ç;W+Ëw.ßý2^[×3݇/¿8~öÅ´_šE4f´9¿÷|ÿñ—»Ï¾©lÛ›O>úëlgÇŽ„í¸ 9?w²|òÞµ7Ð\ g—®Þý YÛ8;í¿0íŸvq>&CÈåTm£·u?¤Oï¼üì'ÿHEJ¨Ö +ë|vE.n.Ÿ~¸ýà»ÙÞ‰Œß|òÉçŸj…Œù%eG"6CG»-Ì„•Ü9xpz÷"ã‚åçÍÿé ë™ ¿æÙð1`¿êü>­U9£CªȹÞÐh1#R.@È +2Ñp²æ&ÂnRAÄŒ˜×;ÕÅãùýÇrqè£ÕTu5RÚ©ÑcV³~iÊÅZý")$x%ë²VB†"V76ë¡}BÚ/å£Gü +~!gE1^Ó«Ë…åÓ¥“wúWž•×hµ]“õ`¢˜¨»e’þ´›w#T¸dÔ6°pÉK'ôüíD"AJ‹eÚ‰Òu“Eù8©äød½¾zméðqoëv$ÓÕ }%?Œ°0ÅG‹” + +Šâœnö†Æ¬˜ãëK< U©„Â9’K`!Ý £B–L,Òq)/À­x¦]ïm;›Xdt5³¾r37w-Í…b…šÇÒŠžC4ÄIMËùVÁ¥ŒŸ‰ºPÑàX)£å˜DÏT¢åu½¾%§ûÍ•ÓþîýL é¹RgóðQ^BB©xe%Z^’3=V/ƒ”݃«ìh^¯ä¸•°#>É.GKB¼CJiQIê õ…]ø€ÖÚU»¹áíüÂõxu ”3!.m]îÝùÑiXN­Ì:©YpÓhåáóOŸ¼ú†Èg§ƒ.q7‚JCoìéÕÍp¬ðÁ·úðíÏLvêÜ„wÖ#ü'ÚÇ•ígP!6M =× ÐQ TL)ù9`¿êâa®³…ˆ)NNίçZk64ì&¢l¬ËBJ»‘Ù‰ÎX½nd—Ïï +जIÍ £¼çq¥JðzoawýÊ#­´2jiY)múø’=(⬠+§í^Æáã‚tcS´Tˆ¤ç —A`Á¸Œ¨V„H¡Ò\ï­ßrPQ/«ûX݉„Ínv´Í¼ž¬,κYÂ|ÊFù¨ùBÎâR +²!£å<”¬d»Ùî®VÞÒ •ùkZaÉKiÕîv®¹fHÝ„êaRV4égË!­ ›dqd5I4fœ@’²u”Â"NLGø¬  ‹#2© b>œ¨©.Æ'¨ìˆ„îžæ¡pV ÒJÑ0Nfp Ê„ýë€C+&[Ðì$-n~ˆ Öy¤´Z€ÏZý!£:ŒVœ˜ˆñqN/Sá,¯•x)NÚ<+§bÏ0Îé§!èB¬Â*Ø«—âub£[¹€Fà/"òRœâ^\qc«¸xÒ¨8z"ãR´œ•bE”R,.b|ÆV§cç'=—¦ýf;Š‘!†SâÉF®º>"v+é¡2H¸²>&Žr<$æëýˆQ97=–]ª,Ýhïô´ª^Z-öOµâfMxÎá!a§<ˆä§T*z1auḺ±6%ýX$¢æ÷önß~ñ¹›Ñl¨à¥57ñôRðRÌ.bƆïYܬ‘]„fC¿à@eJÊÂÆ£B<È(™Æj²±-­(ù¡œš§"e/©)F3’jC]ù™¸–_ŒWv¹¥e÷ËSfŸÍðrœ‘“gÆìç&Ý&wáò¸Tõ³ƒ,-,’á"€Š—N‚ê:ñ¨‡1ŒA=„2iò;½Œ²³ 3H|l +rBÇTxÚâG+‰ì¼ŸÐ¼ˆìÅU„5`“œAøSÍ6M.T”‰@vKP‚P—Ÿt¸QØ·P·9Üno HEòI7¡ÌºiÝin÷ógÇSfć)6OÈÇ‹T|AÑæf¼¨T_<´¢ÓÏâ|2ÈA&Žqôàä¬o|Ú9kCQR˜õM˜ƒ®`$HÆÔdUI”…H>@Ä þÅyËÙ ÷…)ïäè1XÜä$Øpb`€’§lÁ3ã*\äãÍ0LaŽ7L.Òæ£Ç-3—Lê£ÂNL „X¡±˜(ÍI‰ªŸRì~rÆá·zYŒ/ˆ¨ˆ‘ëç—ìø„Å7arMκœ¥Y9Ï‘‚¦¦K¤š¶9VNÖÚË™bþðÁ0å:a£ +IÜCÇ&l„Ír.’—b?1»p›‡tø8`P49h?®©ñr±¾˜©Ïãl8ÏC/Ûü~L‚‚|cÜ=iFÝÈh×ÎOÚL>ÑE%I¥.$záô@2”—ÕLoõ8¤—! Å…ôf47ŸilDÒ7µxœbbtg).zÁ‹69ev›í¾ .x‚!g àÊÇšL¤•à@$01ÑÍ!•¶’ßæã]70ƒŸ·yX››Ÿq9ü,4äzQ/‘RjÚŽ°ÏÚ‚ƒ¥ƒí«&6ëÀìnÊãç9)åÃ$/*ZÝÔøŒÇOȤ‡¡}ö£a²ªâ ˆî€€Ó1"O[‚j²ÍÌ{ÑÂ莀0iFþòœuÚN{ÐlÏgQZ¡¥”ÕM[\ä´ »4í…â `2#¤2êò…(>ÅE[LÞo^²üÅÓÙ t‡Íœ¢FÝkwÃFeÆMÙ‚Ö01KDªA>;ÀIì_¹ÿÖ·ÿÁ‚Évfâü„ÍågCRRÒ2j²ÀIѽã»ÃÝ[v”7û-^ÖìfÌe5JÔ¸p,–,¥ª+l¤„q‰ ;jUJÐM±¢Éx0Î Ü>£#Zª‰ñ°ã¸Íá`C¬šL»IÎ Í>Ô ¹Höœˆj H—Ì>ØeÏèþ,.­¸æ!A"TøN +gbE£¹é¡£.Dtx0ø¯¼’“õòŒ ¿0ã6¹Àhd"”ô ’ɉ–ÄÓÃHÁ¾Y]A“=¤;©æÆ×gZ8\H»ÛÑüd ++ÈTÖp!Åk5„IÀ°'2íd¾ëò é,§"¤ŒaŒ”œ>ÔâôééüÞÉí ÁOLº¦!__´^wÂP\ìŠÊî“|XÌå%œ.ÿ”É>6m·»ýª¢r©R1+ƒç70.°0ç¸É;5ô£²ÓË_štž97sæ«“eÃ%Z̺}!šU£‰‚ž,KÑ”´7(@=kM™o\0_³M›<6'A )?&ÏZü3“ ³{BÐ)›W¸8é¼0fá"9”‰^šrN™ƒƒ‡ôH´„SQ›+41¸¼Ü¨;¦š6 +Œú Þ v/uqÚý‹V[P†2ã³A:«#F¹9ß_^ +(ÈŒñ“Š…Þ)“ád×sE1^âc%*œ1»Iw0„Pa”9Q£ø˜Z£ä%$8>̳t:U£1N”‘0E¢…2 ÒjåWæhT‘ö"¼ÇÇ0 !Uw@túx*cTÔäœ.§Ûes¹²,× É…zf{X»q8í`øâåí=E™°áÁûH:65øó3SçÆíV' bµúƒ~š&Y-¢*‘ˆ$…}ÂBÌ TˆAp”a9‚C¼‰–öâ”ý̹©³,ãÓž‰ÑGþ uvÌwæ’ûü¤wÖNÚ\´Íty‘x¦ÀB„‘#)˜Ÿ"Ø$#¦gȹq‹É† ¨àóâ.‡"=Z­äÄA'5\j”©t^mÖ“W·ºwN77¶úÕj¢RJêÉ”¤AfÎ^œ™™ux½A™ã£-Τ‚­ôó嚎ò’&…ã:+„â ¹\V—zÆóûûÏž?¸½ùðÞåB5?nrŒÏz¼AäI¥ˆÈ¨¦ h·[äÚU5ÅÚ%e{±ðüÁ¼~òñ?üÃ'¿ù§}òÙ³µÕJÜàB éÑâ¹DœåbéT¼^Kô:¹N+S„T2Áð¢Õåu¹=YªÖJ­Akycýñ«ò9'†šÝÈ„ àô*Ä'ú¨X¸4ã=wî"Ž!F<^.—²¹¤?èñúGÃÈ +áZµttz½ÑŸ‹$+;ë‰|΃QöÉ*)BN*é6Æ)1!x¥ŒT¡P‰Ç´l\ÊjÄÎJõÊî°×ÌÕ²ÊÖ|áÑ—®|üêæÛOîÜØŸŸoÉDÐx¼˜ÃMb”šLÒBˆ(¦„v%Ú,G:ÆáJæáAýõƒõ/ß;úΫ£ûíO÷ë¿÷ôêÞÎR£Q‘DÙ$=~;ç š‹¡4q$ W3Ѽ.ôk‰å¹ü ]ØXîݾ¾wíêÊý[Û¯ž¼xrïøÊq£ÖH$‚Àz½ž‰i§ÃËN[± “ž+qþ¢åüEóÔŒÓåBsëƒGK¯ >±öÏ?}ú¿ÿÎ?ÿäÁï~r÷?~ó­ÿûüþï~øüÕÃÍk{L&Dy_€ÁJ'"Õœ´ÜÖ÷“ƒ"¹Xe›9v©›Z™Ë® 2×wÛ½<üãïÿæ·ÿùçŸöâÙƒ½Ë;‹ ѳ6«Ýƒ»1éâŒóÌùq·Õ冄¤#h”³GyW»YhÐ{‹Æ{ÏÞ}qúüá•opç_}ñÖË¥Š1¿´Ø[»:¿÷œ×|.{$„ÆE¬§úÓãîÃ++åÅõÎ?}õê7_½þøÉæÏVþå¯ÿÏÿöWúã÷õÝ›úíë_ýðîájV ù½.ÅêCqŒFœ:ç^(²÷/~öééoþúí}zëÃÇ ?xgý¾{óß~õúp÷W_^ûÓï¾õï¿xôéãöÃÃÚÎR%mh™L&¬e"±B(ÄÅ¥`3‰¯VÅÛ›ùwï.¿yÜ|~­þÓÏoýþ—ÿéßÿîÿüß}õüþÛçÿïÿõïÿöÛï??éüè[Wþù«·>x~Ô‹-86ã³y*ñz\A÷Y%Ò&fç‹¡ÓõÂÉfeo!q¼š~v}þ¯¾óægo}öÞÝWo>0I7Ê1‰#ɼêgéí&o]ÿìþàg^ÿ«Oýƒ§ÿë?~üÿüé×ü§/`þío_üéßýþ‡w¶‡å¤.‡8IÓÓ,M…¤ªSKEâhN¸½ª¿uXüÉ{—ý£§_}~ó‡ß:øåwnþo¿|ïþ×ïþøü?ú/_=øÝO¾ug­˜ s<buúýÀ­U†9îån⃥Oî4þêÝÿòãÛÿãŸüûo¾õ÷ß¾þ‡ŸÞýßýö¿ÿâù¯¾¸ú_^ùíOþÉáÍÝJ¥rBùi,H¬ÌÕ×{™Õ¦vs=ùíçKÿðåµ½»ùƒwwþõWïÿç¯^üìãkûÿ‘ôÞÏr¥ç}çá²e’3ูsîúäœû„Î9Ç{ûæ€p\ä4ƒ™Á$Í É!E‘’H‰ƒDZT•¤U²D[öZ’·vW²·ÊûÖߧÇU]¨ Ã9ç}žï÷ó=á}¿{÷ÿý‡ÿÏÿç¯þñßüÆßýâýÿþ~ã¿ÿôxšÃÑÐÍ•Õ` AZçùiMíÙ©®}¼)¾>/¾w^øôI÷ûŸýâ7/ò­“¿üé{ÿí~öïÿä‹¿ÿ³oÿÓßýäÏÿ£‹ÃÞt¶•oN¤žsó5O¸“ÿþ»Ó¿þÑó÷ó÷þð»wÿà;wþüÇïþ׿ýÁßüìõ/¾w÷¯~üâ?ÿò»ûÓ·úÑðûï´îLµŠ#r¬°²¹òÖJ2´¹Ø¤€^Ž•ç‡ùõ¾÷úà/ÿ£üå÷þÏ¿øæ?üé×ÿáϾñW?zñW¿óàíð{/;v²ýã¨!œd\6ë6ýÑÞ¤6©³2ùhSü÷6~þë—úçò;ÿæç¯þá/¾ùÿòÛ¿üÙ{¿üáåü£Wÿù_öþèÝŸ~}û;O*]v%+žb‰I)')F«ê4=¦Ÿ§îͲ¯Nœ/òßzÚþé·NÿÃüËŸ½ó×?yù?þ¯ŸÿÿôÇðí‹?øî³ß¹M‘lå²½$:'¦{¹Û`lš¿zÙûÉ7Îñ½Çöƒgÿôo~ð?ÿû_ÿöÍÿíû~óÓ[;MÇà(’BI6+šU1Œ¼)òEÛª«÷·ë?º[û£_»ýïÿð“¿ÿóüögç¿ýáö~ûô/~øìÇߺóÞýÁÅ^§Ýl«v•×+‘ ‹ÍÅ-×r¹‚B óÌÅfþÞVáé~á×_Žþä·ýÍÏ>þÅo>ýƒoßýþG§ßx¼ñùeÿí“úÁ8Ûªz°‘Ã* ÓísŒTrŒ‚Á–uj»©½wÑýÉçÇú[—ÿöçïþ»?úÕ¿ÿ“oþãß|ÿ_ÿäÕ_ÿî£ÿòWßø§¿þö¿ý½ËŸýj÷ã{µÍ†B ñ` Óív¡<•WçÈV–î˜Ø–—z±£}þ¨ýųÁï~zë¿ýï?ø/û[÷§Ÿþßóú»ýåï¿þÑ·¿÷ÑÑ䬙YRp0&[kŒQtTæpXÈY½ïT¿ñtóããWg­ß|5ûåOßûË¿þé7nÿÎ{_<Ý|¸]Þjè–€³Œ„SÌâJ‚À[t¬ïÒGÃüííæÙ´úä°ýáeï›Ïfßzûà›/îNœ{ã쉷Ն^¢FæçÔ#e£ª¨¹Z©\qô²)ìw ÷¶÷7s_<ÿäëGÿæ'Oþñ_ñç?|ñ{ïÿî³Ïo>8p>>kì7UŒ“XŽš›¢¬çi”&Óq“N6Lò ­Þ‹¯OóŸ]6~ûƒí¿ÿÓÏÿ¿ÿò¯þñ—ßùÛ_|ö{Ÿ?z°×5Š½a\Oâ|*ÇSq% ¦Øp…QYÝï:—{õG‡÷ïôøÉÙ¾xð­w÷_ž÷& O,Œ„Öñ0":åZÿÔ.MEÑ’hºU°ïn=¾{0,1÷gö'&¿ýÍw^ÞÝzrԆϞnúUË5ES7Ήf²1ÒÅÄ2£×h­ê•¦ér8¦±tÙÖ˶Rw¥½~þr·ýäÖèáQï|VŸ6K“ZiPò\S¢hÔd0Åú"!”PR‹D’ñH$“Jé,7¬U¦­û;¥wn•¾ÿÁî/¾wÿ÷>½õ'?xù'¿õâ¿uçgŸŸüÞ‡‡ß{>~¼ëZbMÅAbÎåPÆ ÄYaª¹b·”÷øDßÅÎGÖÃ-ïÅAágß¼ý_ÿÝïþ§¿ýí¿úéëŸ÷Ùg/öžœu·GÃÒE³¢çǸÃ;‰ ‚huêÝœ&”M¶áIe«gåIÍ•¬Í’ø`»òöíÍ{ÛÍžÃ×,ÕUU‰pœ Äùå ³°‚-ú 3è I¦’<Žy–Y)äë…B3ïtÞ“Ù¢.Ö]Åd@Yi6'ÐÓ) l‘sõ)ô,Ì`”L¦(‘W+ÅæÖìtk´Ýr´ÝföÙQû¨¥>Ü©_lT[æQÛÚ®g·ênÝ”"«Ë"Žé”ÒJÙ…åø×®¬-¯†Ó±¤Æ²yMj¹jIÁ&%q¯©>ÞÉñbû×Þ9øàîôö´ír,ƒ3Z¶®×ª³„P€xÀCP/Šj‚¢ªF«Ù¨í’§–\ÙÒ8MeÝÈðJœQVb™ÅËè¬QÅ$o%NGP…•'?Áç \§Ó„,™ÅÆãÌ9<™”k :§½ ®½µ2Ÿþ:AYœÕöÚg„Þ âNUc¤©—gfe{9H“&äX³É{à  F”°Y³GÆ—”–Ã&5ò½ ¹0ãœQ’/-Ť`”ä¼  òz[­˜Ý“âøŽZÛ‹óE?¢`œÛ˜Ü¥ä*ü´ZœYõc·}®7N–ÒÆ[~òŠaó‡†~å­µ>ŒuÆfïŽÓ»-·ƒiýêb"’’}1é« ±žÊBnˬŸËùí n¯§õëj=J“‚—Ìè×!àøn,'üQ&MÛ²Õâ•âÊzÜJ£!ç׳ie-!ù-ÂäC¤û•7}JýÑj¤VÚ¹õ¢±u/!q³™–+ŒÝâÝžZ˜6·úI5ˆ+­*•§I¹Ÿs%ÆÛ¨Nž¾ýîàôZ„W<ÐZw™Ü6ª4"„é›?–îÐ_^‹3^’¯°îL*í1Έwà6˜&ííàjK,lõΤ„‚VÚÈ÷N³)ؽŒÑ¤sS{xßè]hÍ#©¸A›ÖîK:;ˆ:kÏ—ÓŠ›D¶ŸTiµ!¶l=Þ¼ý:)nF¨ªe´V”rSRÕš1ÚUnkzóÆó¹ÄíQRi#Ú€¶·¸gr¡Œ¹’`×tW9³Å83Ln“b8% +V—4;QÚ…íÏvOaŒ0sˆ™ƒ9izz-FX”Vgæ“l§„bÆh§•Ö2b.hLÌa)„‡15Æäh³Gh-R‡ƒSÁ•&ev¥üæZRŽR^‚«¦¥Vœ+ãÚü)›…qßÝù%ËõÄTÔh¥¢µ…ü”±{i¡`=híÄðv\ª+Í3¶¸§4N”ê­k>j9Âj“6[!"›`J¢ÎäAœÓ|)’É.†hD®*Å-¥°!Ý¥ãCLÜPΆìôc˜rcýêèr€ §å“¢f”´Ã˜–fœµ@d +W.nŠ¥¶¸Í¶¥ym5“±Ç„ÕËÖ¶O~^šœ™lR.Åa|«ÛÐóéÜsãùÍ6½6ó“ÚbZôeìŒ1(m<+í¼ãŽ óicM*ÛJ{BùÑ{q±Ç_‚>jŠ¥YÈp“ó6ôö=¹vJÙó)Èâl6€vÆ1Ö á:•íÊå™Ó;Ïîz½s³q`×÷wî|D¹},Ûu§:'Ž.>é~èMîWfO{à–7§·ß ÒÙ„X„ÒʇÙÞ½Òìyqóy¶wžæÏœ²îˆ4ûˆÚ€ú¤òSÚ vžlßûœ¶{WCD€rôÆ­Üøi¶÷7'1®†®á‹¸R ãJ8£,&¥µ´ÍX)6Êõ(SPÊ;z}?)º1΋užÂÁt@mª{Q®ÆŒÎì¾\˜Âþ.Åù¤\Eõ.fŒèÜ6av@Ä¢¨,»=17¡ŒN”°@ô8oÓhÜʶO³­[ë …µÇœ·Eg'ð[!:·†Û×#b˜Êëµ#L®ßq.—±ºL~“Îme¬¾7æ ¡·b v0¥4¤ÊaùÒnÆ™©<ëmšã唥gJÐ,ðýRq ¡ó1Ê ³ b­Døù̽¸²i¶N2æp-Æ®D˜õ¤ +}º÷Æ‚ÿ«×CoúÈ]âÜ ©¸Cš½õ(ïOJŒmÒŠÐ^˜ÉÅØ<´çLa  ƒ{ùþ9BJ.V[®nÇ¥¥ÝÊÆ.…áÒÙ6“ÛÊÛJíÎïú‰ùÃnZacxø“ËT‡º’ +›Îà[Ø% ;ˆÞ`ÙB÷¶^ÚZN)q¾&ó)±.UO´ö]&¿¦KvmçøÁ'!Â^17ã2Ú»;¹ÿÊ\<ûú÷~ügùîñõ¨¸ŽÛ´·QØ{§uþieÿ]±²ïÇ-§s Ôi5¡,øÉ„XŽp%¨oð@«øZ5Àb¸º΀v-DÅeÄ œ©X;uÇO¾º˜¼áCb¤&pÆ‹Q¹8[ +àn˜)Iå«sîK+«óÙ·ì$_LI5£};%T±ŒiMÒ™r…ÒôYçôÃŒÙ0ª›àtÅñU¶}(Fœ×g¼~yöP«ïÑN?“í«µãüäi~úT© z{-£ÇÅ‚Tž¡Z 7:´·Iç÷c¼±ó¼6»ÝV+Û c\~ƒ°†\qGªïó^rôtçò±´.Ÿ• +•­'­³¥Æ±^žŽ÷.õ×^™]9/“{£GµW­ƒ÷ò£ÛIÁ[‚@§Ö¥åÝ´QryÊ·Þßþ¼¸ñˆö&Œ=¨Îžö )×ØâNaó©Ú8ar3£yK­í^ ¢ëeÔ§Vs&•7#|5¥öøÜü~ûí‹wî~tÚ6%Sæ°1{58û\n]bÖˆÔÏ?ùq¡wÊؔ՞¼ÿì³=ûúOÆ—ŸñåÝW’¼ñ'ßþ™Rܼ“b\U(eëû¯7ï|³8}‚«Õî΃\ç`1Ái·§z÷Áðìë­½—½¬3Bä†×¿"Üá°Þ”+í’î Ô/7|ÐÝ ùEt'vï›ßbrÖ£r‹6ÚÛïÝ}ý› I¾¬×Žõú¡XÞÆ­!ª¶|…2•Ùøû…àG,©¼¥·ŽœþíÂäþoTpk»ý« q!@á0jÕ3:7cÁÝô~ZéÞˆˆviãÁËo)N±v3DŽ _˜xýËÚö»¹É#otiuNxgëÝ‚ +‘+;Ùá¥9¼ÃBû˜Ý|÷bpö‰\ÛIˆùÊæC¯wÖÝ}tëñ§£³×ZmG°»“ÝÇݽG”ÙòÚ‡'O¿uøäצw>Þþ¤¸ùPtk>:yøYŒuô—±z\~ +5íß«G¸Ñóš{÷^Ϩo(Ú3©v¬´Î‹[Ï÷Ÿotö±YÝ?¸ü´{ëUÆíjݶzŒ»[BõÄê>(m>§Ü cÖï½úNoÿåüiG¾„=ÔÎS›…ÞEu|oçìyïàbÉù²Ô!hI¡JÛ=~w.›bÞnï{£ «{«8½o´N€Ä~Ü!TéI„ñH»›íßö&½é³ê޹ɱ¸ tñò‹ÃGÀ~}€«ù»Óâäa¶}¢–7`3jutürrüÒªƒÚoð… ½ºSè=ølëì½PƽqïàÅðöGJûtþÀ{~“qƧ>Í7·£˜:âuz778×+Ûri—pgëo)¡rÙnœvš{Ï·ŸþzçÎgÖø3ºgî’îör\糃ÞöcJoG3rŠ±Ã“Úl¤9ˆÞµu<’±kcò×C\shkJYS¹ráL‚vZª$¸R˜ÈfÔú|yT‰°¢ÖkÍC¥<#²e2z PJ¯ï:½ÖB‚£û;gli뚇Ҫl>.o>犳´Z[&WjͼÓ%ÕJ}çqïä5ÿÉíOÇ_¯î½Må†qÞkî>A@OÈH4g춽Þ-Êî ‚g‡V¥_èíZ­}èn"Ûc³5¯µ«'Kq†4Úð‘(ïDx›/mhc£yb5õú,ÆX¢Ý>ñë[¾¥¶n­[.¨J~Ä[íç i¹Vœ=ÏM_˜½{ÎÆK¾rU´–VÜòÆñÃOòýã7}hZªÏsúøÖº¥Õö³µÆðôó_ûñ«Ï~è§ra¶ˆC©vÒ9ùxëÙ÷ëÇC\=¾ûÞ7¾ÿGÅÁé:ád¼mÌÝÅÝòö;£ûßqG—QÊÖ«[jeëËÙ}+«i©F[ÔéÞG;wÞ·›»Ã½'jiFZ=ÚFØâ:n‚³y–ÆÄÜX€Wy–1Ûí­ÇƒÃWfóª4 µQè[µ-L©qÕ#kxßÝ3:çà¡ÂŽY»}s9ÊøÒ2®w˜ÒÑ}ht.p½•ŠÕÍÛãó·ÁM"¬Ó1£k÷ïgGVmOtë a »±—âÊ«qªèFXXK™I®ì¶ï”FϘì0×>ò ‰7Ö¨½W‚˜Ã8S£qšâ«ÌŒÛ‡¾ y8òdvÈäg ¹fói1ÏÚmF£²m»wj´Ž´ê!ðX~ösÂЛûbuÛO˜iâRŸ°º {b~¦×!ª'èler·¼qiµ„Òáöcr)©Ô¬Ö-pù„PJ‹%ÆpÎÀiì庇juk S3rQ¯N“‚EkÁ@–ÉîÀfÓ›€‘¬·A9C&2vÝìV6ôŽ_7ö_­cJ•$»ÕÜy8Ï­Ô|ŽÒè—§w«³»•Í{ÀH¤Úà½)hõ]`Ô÷7Ï?ÂQ+;à_ð—7£ŒRœ¡rq9NG(K.MòíÝúðèàλN}+LçÄâ¾\>óÛs 2¤õžVÚéî=Üz;Êä¢lIiÞv6ÞÖ:w…â¶XÞC´öÍ“w‹½cˆÿa¦€¨­üàr|ö«­ÝgåÑ…×Þ‡ñé·Òž=Š +u½snçÓªç¦ÌæqŠ+g÷¾ÿ¯þíƒ~ŠÄ &·¥7oçÆìî™RÚ”ÝÁþùûG¾ÀôVL($ÔVƉ…ÊÙ@Ôn’¯XåîÞCT«ÇÅfŒ¯ù°ùi+Òhæ'ƒã— Ö%µ:ŸíkeØ‹ýݧ˜ÜXK̳b¶?Ú‰KõkëäRDZ«m>w:ý|Ý(l|ôÅïݺüàÊrêªÐDéeë'Ný|“”Kbi RÕ8e‹ù®µwšP;kmß1gFó/äqwÆ’Ù~J©'å:nõA·;Çïñ¥Í1W› $˜Ù¶:ǨZOp…¤PÈhU·NYm)¥²cvÏòÓ'Ã×Û¾YÞzœ +¤ÙÐj[ë)%FºIØ©“”¨Ö&²ÝgëÅ>cw|¸™*>Üög²ÀT¬_Q½_¾}û]Öé3Þ„+í@…j5×9Ýþ0ÂäP© EK¨Mèz«¹§Ö¶#ó¥÷\ÎéÅÇ—I³©ÇõщœÞs×Ãüb$Ū òËÚg‰n×íöž¶ž[ùCiM>·áC4‚”XÒëí½·GçŸ3Åà QXŠJfyS¯í©ìÍ»aý˜Ê±t Õ×Q5JZÙ@Dð£_ˆ19ˆº^û¬4|èv/!F˜’Þ<¡ìÈHˆ´àýTÍ×vöî}Rߺ9/ˆ½ÕMS‡Q®¡¨jé½|ë´‘órí07¼×>|×êÞ ùµ¸¬çÆÍÙ=èb\nRÎÂC£¶_ÝxhVw“´3Ý{ÜŸ~9“³šàó³Zo÷ŽßO‹5P¿ b`\eKë 5Œ[ëi-Î9w$y}§2ÁæO“‰œ·iuï…ñìz”]ög¢i•¼ŒÙÃônRª#RM¯m 9˜»Ô|}¥Yyöâ˜PÜ +`zÓçÓ¹3ŽTÜü_›Wœ>«:n?¥œÁbLª•Kq±DæF ¹ŸÕVó€S« }åûjã@ªîHµm³½Ï•6ãBâ¿XÙñ#ÐD=Ê™²¹MÜÄ…jö CËÃS«uáòø>SL‹UpdPû¸0Ÿ1^,ÎZû/B¤ VÛ¸Ì îfˆùM³~õ"ÚhrÙÎ|:>&ϸc¦°!1ʺ)ÑKñ®–+…ÑZ‚^‰‘3¯™[†¢Uj‡˜T!Øìpz:Ø}Â…éuÌ줵¸çŒ(£ãKKœY7k[Ra¨•`³§¾îC •çŠ½ÛRis9ÁEI#Åæýˆ‰+m.·›k)…ú¼x Iùê*&¸"x“öΫæλlaÿj[Mi¸Þ]‰ Dó§Õ$ã]HÎЬr ’FÝ®oaR.ΘÚNŠ5ÖëÕ]±¸b! ·µ—ëŸÃhÒÞ·Ç+h6A‹8w§\Z­U7/\áƒÀB)±:Ÿï+­lì?jŽIÉcÌšUÛÌn5ö燷×R:¤EpCB¬SrýêJf9"âzÛh—§¤ü4‚›Ëa.ÁÌÆ¡˜ßxk)y} £l0¼ÄâŽ7|,5îH5Õ(ej1£VRbq®BF7.5‚lÊ4:!T¶ûVû ÌÔhžÙ~„/úIw5)"¼¥-p·ÂôQyöT*íÂ@+å-Âj“F­48­lÜqn„u¢¼ þ¾uïÓ£gßi¾ËUöBB¨Ù=ñEâ0UíRË;;6·î©•ÍÖÌÜ‚Bɘ}øþÜð~eï=€R.”zjq„ÊÕ¤P¢Ê]ÁÛ‚áº$1¥NZ}†&? ±ùUT‰3n¡{"¹Ã««±++‘•8 <Ê–ãr#—`XQƱ + ´¥8O9®¸#Vê¢ðñ¤âƒØÓ?²ê!\ŽP¦/“M‰ ­zK,îÁ°.&Dè5,@®^1>Ĉ ";uÚ¥á·szÍO¦™\yxµ½š|iÕà’Œ7CônZmøâl{z7ß> V(UäùÒúrL ô¦^Ú\‰R7üèÿšÿ9=ßåRˆò–‘ùB?fuǪî†p³±óhÓêK¥t>3§º"QÚÒr=¨ùª¡Ò|æ½z0×Ò ¤BÎv/ÝÚç³”ÕÑyÜèkÕÊè#|å¦_ 3N}·4:»f×âÒbˆ½¶F@éªå=¥´Íd}qŽŸ?íUK’îZT ­ÖÜSôF„ò’|´ Z ì ^Ð\~“-ìâÞbŽRJ#D˜ A 6Lk(_Îè]¾´¯¶îp…­©­$éP´Û¦´jF©³ö\­”7V¶žáÖ`= ±·–mF·Ç'¯¦gï­CÔh§wš‡/¤úþ8FoÖOhaF»0D­8ml=ŽŒ‚TÚÊæF÷·p³ŸªQÚÅÄ"übŒP“´ÍÚc½~ÜÜyQ™<äÝq˜²£,®¶0xéMPNÈàbù¤t ÆÖSj„ÌBmûU)ï7Ÿ6–¶žÝ{Q±ÑF-Žk›åòvjØàæ€ËÍàãóÕåÛ­NO}ŠÈµ¥¤1Õ:i¹®ÕŽõæøÌb„M²%ÁÝ ô>¡÷„üäôöÊ +ŠñóiÓ0­!íhÆNÐ9Ú褥*äÓ¥„ÆÔÞÎe¶±íÇ5Þ;½ ­vÀ{3ø류øÖrêº?Ckõ…5òfc¿2{zëÝõN?”Š³ëëH,ÍWGgJq +• RéK+ €i “[ Óó y”úzÚk ƒ¢…Eù£F¡µ<¥WÁøàà,…é¯ÝˆþË«¡Õ0‡²Z«ùRìr”L²9_JÔ¡Í>mõcDþ¦ŸÂXåìå zs]òÓ˜ÊÒÒ|® {"—Vƒ™lÆè@wBËàF—¶ÇFý–Ù¾Í7×Qýª/Îd“\•Ëi¥Êç7”ê™í¥„œ?Žãx’Ò« ¹2?¿FóAu÷eD¨.Fé­ŠfYÊÖ8»Ie[ ª›·ÿZaç1êc|‰·{4ˆ'iV¯4yxôø»{¿WÞ~o-!GQUÉXoœËP{”=!²nï¾Z>Œ³ù`Ší®àŽH½1_y„-.FÄuD'ìQ~üP©ìeŒFat¦Öv´ Êçʬ7Ëö.õ&Ä™oUǧÇå +bõ‰ü ±DvÐØ~”ïŸ"bAË ã»àÚ|a‚QÖ;¬ÝÏwŽ››÷Q±€Ë«q ðiÔöøÜf‚¯‚µÝ 1PB‚ÙY‡D@X@øð£ri7£t8{ àCL½²ËXݵ¸%Ý´Üä‹{FûÜíÝAä2hiŠÏ×6ï“V7ÆÒrkÉÂQ* 7ÖF«š¥‰àt´ÊNJ®.'„å0ÉiÙîùæÍ™bnƒ2z¸ÜˆQ^(ãÜÒ7o÷3jk1ÄÅ™Bœ¯ ê|ݱ8[˜OüH*^Ǫî@iý³ù\Ô\ñÂ\µIu9Ê/ø1·¹ÓÞ~”d +o.¡ð†Õ(xV5Íäý ñ†Ÿxc1æKÈaÄLòÅ«óù9ñ0¦Ì¤•ª÷Ñ!ø€>Cî[I‰„ÑøPƒø¼âj{ëk%¥§ÅFUýi¼Bÿöôî7J[¯{¸<8QD=L-qÞPªîB’"ì 00*dÓŒ’$eTÌÑv—qz€FÙÊYÜ‚ %äFiÞ‰ÆÜÓ»“ãWÓ…êIwP.ŸQ*ҌѮVÙr[À0ZãŒv6ƒ'˜’«•‘r‹!4„«¤Ñ ã."V¥ÊôrJ(J)ß;D¤R(ceô…’{>°„qgª×=fœ¾Ó˜ÜfyûYÿôÃùÉé#ÂhCÆ)µö¼Î³sì ŒOÞß¾û«í£wõæa×ÍüpzüÒkøQe-%¯%e¨mÞ‘Re¼ÕHFõ•ñ%tkZ¬ƒ0 W;ó-r[€‚aÜ,tN‹£{´;%²Ã¤P]IH0ŽXâ lÓNï”tbÞìN3 Býx†w ÑÅÇiìsÞÒS”°3jÓŸÖ®¬ãÔ gì嗠„–âmD©A‚«o=3Gkin1ŠÅY‹qGœ7Jrˆ?­× +ƒ³ÒöS2¿‘kaÒM ՌыQ&tRšÊµ#Ø0HC%×<&•ùÜ ‘ó5åªR=Qê·Éì&"”CØË‘pF'ÍÁZBõ§tLªñÎÔ›rn ¹=ÐpÆÌèMÀTmöX«ŸÚÝKÚÛ†c»šäWcäôø•Û9ÆÀdù’ŸÌ‡é"×­Æ*\ÞiŸ”7VÆgùÞ ‘íù)XBöF^cÏvÃdQדJ‚«¢js)*\]ääô +½ã ª,ϧ±Ò ³×˜=mî¼¢¬ÑR„GøÂæÉ»Íû!ÂNKuðM¹¸#—w9o2¿!L‹¹‘ÙØ 3ŽYq¡Nîéý4eãLV4ª½½G|~²Žg¯Ù„X Qù0S–+‡ë iq•¬N®y êy9*¼¹”ºº†…Ɉ?”Öõ%,¹¸™ížCõbj3É•×Óúbೄ)Ýõ¤Ku¾¼¯uÎÈPt.Œè«~1@Sb=[ž-úѯ,„ÞZL#l9ÍC˜ÆõÅ0Ä¢¢3€7檮”íÖai|»4¹(oÜSªÛ@‰i¡Lg{öðòzíƒ*[/ÑC à„›E•B”Í2îDªžÞ>_:Ë÷UF¥üæ:"&Y›²ZI!—`]`‰ãqÎ`pôzãî·ÁS–¢L %j œ4cvÙònBif»÷ +Ó§¨Ö‘ÙJïP/MDÖ‡[>4c A·òýKwð(-µà˜‡0à·eo@F[CõPÆŽÐ9£~ØÜ8@åH­™’j0^!Ò…^@å:9 ²°%ÆÄÒny|opúAJé.„Ø¥ô¢äFA‚¶Ne‡jýöàäÓîѨÞ^Ž‘@’¹Îa®s'M †~~ïÊm¥y‘’@5×üØ|ý”lo1H]÷Ð#ñù™Ã]£u‰Ê(a.†ˆ"QjÙŸæ"%Fþ˜„Ú!´&×b¤ "Cêí0aÃ?%E@¯ÂjzNAZyBêÕk«)Uòn¶y"Wö¢¤AÕdFc´«b‰ŠnmϪíXÍcÂÆ„Ú•ur%D¤æ·TÙ‹A4-Å•8{Ä{q*w#@íf"#•òíý4ç,ø2o,c+i ÍnÈÝÇîä¡àôý1ÚÊõJÃsH‹¿²¿`“zLlÞž\<ôÅÅ·ã¬VïÎ_YJÿʵ0¼!%µÄòn$cAÇ;-X»#8mÖª‘Z>A›ÁùTZY5¿a6X§—òð¥ [“6¨:¤NÂW¶› ”Õ +Û”» í«CÜàüô K;#©|ª#iv2jC¯ng²ýÅ´¶R\A)n0VǨìy£‡¨ÙOkí0W\AÔ¥+Kq‘4´3CÕn’«¯#ÆÍð‰`\ ‹•ÄAÕ³¼³I™#Ê@箥4D(G¨ÜRRMÊMDï³Þ$¸ é.'……%„KEÞ$ŸGŒfaû•ÚáRR|k ]Mˆ0ÜQ:—à‹„5@´.z¾wnÔh»—¤²Ç¹ +¹l1oDx?¢söÜ9Î|ø|«â¼³0­Çåv—ªÑ¡“ŒKÊ¥•pf>[5G>G]DÐËQ1Jç!q¬#êb„N +5ÖÝ´{÷ü¨vu ]°ý{Ne#Š©¾„@µ8a%^+þ4‚ó­îÎÇßø!­VbÂJ\‚f̘Ó=7A_ŒZ‹¢7 ÔêZ\"ÆJTLˆB…RŠ²×„?Î¥…:fôqcã7äR ³!))LþÕÅÔRD ó;²ÔÚåÌnD•¯\O³eÄš®Æx%7á¼*wè£We‹µ{r~ +´A>.ÕÂ\HU¹ÅvÁ³‚0@rÿò¦>`Q¨™¥¨ÎäXgŠH•«Ëñe?F¨•µ¤‚ÉíSYNè«)#Ζs¢Š@bDv‚èÜ­âÖW}ø×|èr‚Ã¥Jד„.¸€¶°gÇa¾´”€¸âŒ:%!7ÓgZå ³ûŽVÙƒ’ ¥¢`·—"äÍ0"sˆÜRJ{õÍg”½Àì›A:VÖ“Òͼ‹ë=Ð=·„ïO+ ëØZBs9ȃ!Ê U`xÆÞ‹{I±v%_Ë@‰ 3öP,l-%@#’/Û¥M”2 ×£,ðÀZŒ[ KA&˜à%­ì•§¥ÖÖr(s3@ÌOè±y:;^Mj×ôW‚!DVçŒ:¹¾†§Ù|‚)@“¦ùTו¥ä ¡4•ÍÌWÓ³Vâ²/©ù“Š/&€ýE3V5|.V ½Óã¬%Eøßk~èŽ<"6àÏ?cÆ?ø¾–›¼±˜¾ºÎ%ø2‰/É…0ðl7I»PÕ°aPWiÖÏÓ«”X«Ej @Ñë+¥Æ™ÌO¹  +pŒÊQÙˆàI.DÄÍ ,74›GN÷Älî“V{> ›_OIPÛþ”‚€XÈ&´=„òHr°§žÑbLÎîß—ë·Åê9Wœ¯Îä @Ñ)¡&ž µ|,)kBe'˜Ö™O»šŽ¤F¯ûR +›¹=TíÃ7ƒŠòùÝy]‰É´DèbZéjRl‰ùm)·HÊivg˜RdBôaR›¯\/ä†3ƒ¸v#DPzKùòœ$[œ+®Îs‚ý¥Í•8` I¾ +rÀí…°S’\Uôf¤Ñ¹h”™Ã”›€ò+ þF˜OŠu³yªVv“Œ·ùÉàðí¤ÔŒ2å¤ÜðÎ͸´ŽhО« C‘õ6Pu>‰eÑÁjÁ®®f2R=‚™PBq"‹ð5É™HöÊPê ~üË¥íùk«ñkèÍe+°… +pÄ´qe-÷NÀO¯®“7ü4X˜/.¶‰VáËÿòzìk7“aÌ + Úb½¶š?ýˆE*mÖìÆ2ÖÕ5d1D-†Y 2T¬D ×—ÔCˆfGÕÁ)üÍ„ic€„@ÆöafôÖpë­ µ$×bFi*Åe¶ãtÖ‡€ºÒk1:ÅX`^fë\®§¥*ÕR|i9!½±’¾æ'üˆ…ç +~Tšƒi@´A”FJ¬`J ^«¨¶¤–c$DªJ¥.7MKµ(ã B—P@2º¢üI!#WI½Å͘if>§^šECS,‡YØA„/‚Q&!UI\ª'¿¢bRU£uÁ ,ÅÜ&„…ùén±ù}5)FhW.íñ…Ý”T…$¾ŽêW‚¸5RB ‚özB€>ZOg×QàöüRL^ðƒ2ðjqœŠ` +1™´&|á€õ¶…¢ ’\=F—I}€ò……µôMHio.%A$Ó &Õ#©r4<ÿìèåïÕ“7—P\ò'ä…õ Øñ ÿü6û_É6n93\m,†¹êð.möhöfL^L[ +(eÔÝ}öè“ß÷Aª"sAÂK + ©xÑz`_®Ö‘ѹée ©1ô‚ŸŸÓÀÕ:d|,hí•”ÎBe·èìæJR®^OiÀTÀE”9{EÄp>и·’BQ«íeLøþ¼Ɇ03IÛIÊùŠe4L­0Þ`~÷o~„SIÚA)“DðÂÔüÚ^Û— ³0nÓ +)Wq¡°ž¤VCk1Ÿikèö.ÒRe=­,…Y”ÍBÞc×c\‚v)«/W¸âN„v–æ‹,kÀ¥~LIIå[ΡÜ1H%ëŽ@»ÖQ5c¶ÕÚXÜIŠMh:H£mÙÍŸäV¢©wôúYZ®'„"nv¯'”¯ÜŒ“2©T×¢ÌRƒ<¿ Êo6n $e 7ÓªÍ eGHkc*¨:¿×ÚÑ»õZ™W]£¹ñ2æñrÆ0Ð ¶’bJi®¸ž Y2J•2:@qKa(.…Ä”âlÑ_»¾¶†^õe®‡¹Å˜hGu?a¾¦”÷”·Ùì4ÉCJuiu%¾y3ôÖbÊ&A¡D¡ã´æIûäãüæ3»w‡ugËQõ«7’atZo-¥ƒ)ü…÷¦N댵†I¾.ך=œ °™?ãa¹½ÂìÙáóïÜ}÷;»¯×-AŒ-&Ø®µƒ˜™µú¤ÑÃõvB(­$ä›Qy̔ȥÄ*H +ÔÉ*¢FØ\àˤÀ秈çuH8i¾œâ ~LôSŽdò(ÂÕ´žâ‹ËIÐ(Á”ÓB‘:€C1ÃŒ¤=PŒ`ã%„z¶sÛêžÃC»EpÍŸýèüjE3X6Î@X‘zü÷­uäºI1NŒ´‚3ÁW!BŠ…}£yöh)H.¬¤ãsß´ç+Mðù„\Eô.a0­½Ž*‹×´;vwÌÞEJiEP6þ)ι1Ö# ‘™g{ìÞ‹«kéÅ£Üpf¾|!pW˜* Æ(Æ!ê‚Ú¿q3¶èÇ|qv9DDæ×+I¡¦<`9Új…HWÊzýT(îóó…/i)9:Hf)£©•6×R3çm’Ù°Vµ–BÜ5ºcÁýý ñk×BAD%µ&.7}˜s=Ì_³×ú›«I3CÒB ×zˆÔ„q¹$¯ûæ²seY™/¦¬­¥¹ÌYš­2Öd-­gŒ•€³C8}Ó—¹"t—«¢ÛÝîuF L†õzYr1®€­ŒÑãÝ Ð"ðk`9.·Í;cJ­!dv%LˆÔ6õRB¾á൚R)½­”÷åÚqÆÛŒðõˆ¡ù½¾”À€g7Pcˆ¨]ðe¨Æá@¬$¤/guðVÚƒÑ\O‚>H0èÐD²lP9•™œ“­Û¾èž|în>1eø’fÄ('À{°ÙÆü^ßc<;ˆB„úM*¨A½ãT.Îäãt…06(gÏïE™ÂÍ D +ܪŸ¨å£Œ1ôãí%Lé@`Á¥òµ ÁßjêÍó0[YCMˆ¨ˆOeŸÍvØ¥²¤Ñ®o>­UL[A$\«ãzw5­@¸’P<”ª§dvÇšWÖRKa2œ±@dæwðâ.´mZÄ…:À)W–Ãt‚r ì—ÇçzûVJ„ØN>ˆª˜Tbª8ÃÓf×íßõ†—Þà2Lçt‹–Âórº¶Ž-…à=&a4Ãs9B_[Enú (ì(‘}c1¾à#À)2jÇlÝZÃŒ+þÌÔD¥•ù-=Ì ýæ2@ŒøÜ€Àõ@JOPyDšßù0¿†SÖÔO,îž¾µ†Ç™‚V9Tª·¨ì(€(À·WýèÍ0‘¦=_„{ãF|-¡jåÞÑ3³µÀ’æ—ÎÁ9{ Ö Ÿ1‡fûvaô4Hæ£L5RÉ”VÒÚõ0s=DÇ)7[;,OžT·ß5º—K u!9‚ö#&¸[ò"|%cM¡J¹ü„@2hŸ… e¥ò´µ ¨C¹åÁ}n„é•ÿ¥W_>ëJ˜šdó¬Õ“ó30?æ@îÖ+G´ ­êP€À—ŸÄ•FBm +åýòîk©z²ø nð`Z¢”–àn¥Ä:n`_`p)½Øœ¿…£ÊÃVIÅ-ÒêÁ–ÄÙr”.QF/-”|x6@æ0µk÷.­ÎŒ9Š2EHÇ)Î#õš”ë[íÃL¶o´Nó“ùéT€äB§¨Åm³y—Ú!ªŒHõ4$#ÚD[ŠdB8Œ~ßîÝk}Lz3¹[‘.&ÕQ©˜–<¡8"ì¤Ð‰1ó3$Aܺ¤`_| 1IgI³ ÀFeÇù˜ÔÄ„úrD¸ºŽ­ÅÅ0–]ƒ÷И`½ƒŠM í7—S_¹ÄùùIžjú1 7zZußnfÔÆr$ãOq’Û“½éWoÄAü!Þ.äÐÕ8]\í]dR| ôŸ±§óe—•^óVb¥ !e¯Ä¥7—R¾” +å$‹`«IãF€¹²œŽ"Æ{I.fòis䌞Ôw^i•#BiA+QF«0º¾ ‚y¤\RQÎxÁRR‚Jˆ“®^˜Z•-«~Çv1ªÂ7¯$40¯+|9-…ÙÛQjGt~MöûŒ7àÐÁ&AÕ^”­Á/„™¥Àùó …-®´'”öp¥YÄŽ3Œ®v2z²9®Ôp\5·†H˜Þ²Ú·ªÛO;gŸeœéõ8±&)©€pž/%-ÇXðÖ›ñÞ6e ®ûЛ~l~z0¥@4@ä²7@Ógƒ´×[óûè\ZiC|£³Ã0S„´…ãŒÞEø"äp¹È¦fç¨sðªyðŽX; +Ñåu4›€sºqÆMðuÂœÌWöt'¾´‚l“\g­®0¿_wëa$©É¹¤Ù?EäRnpk~ž$©Ãk9©¥Ä„Óníúz:Œ+J~,䦴³¡ +ë)s5Åé0Œòp¾épΘ°†q¡@…h ðCð_Rš/÷ƒg%¨üúçN!Ø.'xh[·qÈh­öÆ:(Æî •ÒÝA¤Æ:TæRz9*ârßV}iƒÐ‡„6H²UÖœ_?Y 1< ÔZ·’PBLÌ%ŒšqÔˆAvà槑Á×Ìö9oÓ|%I¢dVÎ Kƒ“@RE]E´é%äæ +j݈Šk¨åC-„/äD5>§ ^Šï†3ŵ´?q5HDØ|Œ¯„ho0cRM®¶?T:wB +$ÝgwÓn_ÎNŒo¬aöÊüÞ'L˜ó%æå&ø)F`9T®A?BVZŠ ó“|Y.Ͼ\M)¹Þ©ÝÙÃr”ÏGYб}";’fšó–ÜRŒ&æ·7ÃÜRDXÉ+Q!š™_>ˆq.çÊ÷(w@cK $ ««I…´ºˆÒ\G pðù­_Ù!›ßÆÔ¶U!6†3¡Ui³Œ+€½´Ú)L^ðÅCØÚ•[‹s¸ÞŠq¹uL÷gL6?iî<«l<Ý>ƹ ¼ÏvÏãbjÉBÌÁõ¾Ó½-&+1n-*„PŠº5!·VÑ,4Ëõ ‘âr¼Ýõã:¢t’R7cŒKãg & ¶#½Å› j¢òóÇÇ2nJ褤a ¡H¾z3¾¦± +ô~#"Fزֺ%6oëÍ À’7WÐ7—°_I¡ìË8ÁÿŸ½÷ø‘$ÍòÄîë@Œ¨®!]K3sÓZ›¹¹¹ÖZ»‡{h™!RDjQYUYÕUÕÕ]-·gz»3˜ÝE“ËÞÝË\ö@€A|íéénþ½ï½÷fþÎLôÊ!”=cttMLª¥ÃtÿYqËÛ‹±:1ƽfctŠD]”άǤ•0Ž ‹”›x· r…|÷ôã- ŠŸÈM¬ ¤JH/ok…|4ëôcb1&57©â&ž#ÍIaüÔm&¹l’u­æþàè‹íÇ¿Îö. Mkqe ÐO¬(…€†/es™ñÁë¿«|C»SÐê›q•O +½+èÙÿê'áÏ6È”ÒjŸÿªwúVZÀ 0›)[*î‚Z6¼}½½+Âfe œÚÌJBEÔ.cOª³Wýão±zga”å  ìï¸éÆ„2íÀêÐF+D¹¹á£å“ß×·ßëSžÿþ‰ŒÁÿßøÿjÜrÓÆm 7mÜrÓÆm 7mÜrÓÆm 7mÜrÓÆm 7mÜrÓÆm 7mÜrÓÆm 7mÜrÓÆm 7mÜrÓÆm 7mÜrÓÆm 7mÜrÓÆm 7mÜrÓÆm 7mÜrÓÆm 7mÜrÓÆm 7mÜrÓÆm 7mÜrÓÆm 7mÜrÓÆm 7mÜrÓÆm 7mÜrÓÆm 7mÜrÓÆm 7mÜrÓÆm 7mÜrÓÆm 7mÜrÓÆm 7mÜrÓÆm 7mÜrÓÆm 7mÜrÓÆm 7mÜrÓÆàÿø'2rkÿDÆé÷{}x2ü¤aâWÒDþ^ŸL>É}âï'•£}1>Yìï .ÖXx +I`øZ²4þôÊ5xÑZþh1_ìÁ“åñpg^‹ÁK9øïd ]C>þ¯qñIœ¢p*(N¢B3øEãtCŠ"iŠN‘k»Þ‹èT"EQ‰¡(ÓÿøE;Þ‹ˆÏ1p‚D0xõñ]hŠBiÅÿô¢ôqÿY/úG·õIý“½OŠŸ0k¡ðZ£þÉአõâú¸tæbgÊþéêã?¬#¼b·>IêÓ³ÅxZ²ÔµF÷fo)ÿôcí?ÅãE›"¹Ö]Ãt EHïý[ÿyïðfôßõÿŸ^KÁ,Qêã„ñ5ï½È'~ÿÇ9z¹g×ȃ¡Ÿ4ÒÿEJ©F™LœÏ’z2›”VOɵ¤X'Ì팽…‰å?mù­ä¦œ3 íNB,FX7Âx›h`JWªIÖešéj¥™X˜2™1åŒpkÀeQ6ëKò¾8#tB-sn_*.øâ’ÉÎðt/®”‚TZ- y·‹JeÁi¥³~LÚTjP™ ¦µâ\!L¦1µAZ=ÚîiÕ¤ÖXCTo¿f¹“áÂEwdÕ„ÂŒLwÃl‘K )G˜•mâFM¯ï›íóôð©PÙG´FDȇYרîeWFûD¯¦;—|a‰Yë’f;¥Õã|ÞO˜~DI06*¸”^!ôšUà³0¹b²a&å )½ õ:cup­*ÍLóQ=BÔvÉn¦L?nEYWÉO…Ì LYaÂ䬞‘Z+BºAÌL°YÞêd[Çð‰ŸgŒN5V#|„ÌĘ\œÉÅØe H{ˆ½”ÞÙÀÌ áÄÙ"”Q¡L]8TœÉ§äF„Îú0 V,ΣL6J»¼Ý"–ãlч§ïEøû~#©ø“ +Êe ©DÂ’é ¦%g#ÆÝ ˆ"mx{„Î¥ä&&V\1˜² „ÑŠ±Ù!l?jlÄå[ ´NJnQzM¸t¿6<·ë‹0¡%…2猕ò‚ÏC„Bà#ò˜ Ë5@ÔfÊq›Ò[ZqKÉŽQ6§ç”ÕŠò¹ e'…"“±Î˜T*j¶À?ªPÞ¦¨;¬»À­aq½ímä-—øŽJE©¸ewϔꟇ<ö""¤©ºýàÛâèAR,$…‚˜_fúÏŒÆaöcB)âm—™‰ÒN˜²Ã”·aªÔàŸâB%Ηý)s#éíÁ¡ÓÞžDf#!–Pµ!æ·Ò«òøIÿðmRÍG¸4ŸëÍ“ìä¹Ñ>'2ÂÐfw~ôusçuL.I[*l§¯Þ"=!Ò£„X†•Œ³9„/ÀÁ?n#zÛ_2.$b-&lÄ%oOXµ«­¸TMHµ„XK*MT«?îDXƒ0ãm¦2aÊ âÞNdQ*’Š Î “V3¢x:ÿ„™PH(WÎ7ŽœÚ>ÔÕz˜ó'Õõ¨p?È0#U¤¶i{Œ*]Dlòö\tf”g²I¾ˆ«M¨¤á×ik¨¶BáN˜úI‡ÊdÍ®ìN%g’àr÷BL1¢¤÷Mæwü©õƒ2.©u(£›J«¦‘äò›IÙi°:¿ØaÒ…*MÈ523MŠÅ¦FI{!&"\ѨZµ£rÌX}§ºÿøÝß ^ýÙjTÈͼ݇—”3öÇ&*Dé¬7¼ïÄ&ÒÐR~"ºC\*ÑRu¼ÿN*Œ”½†È„\K·/ÍÖ9®T01@Ôû"M0¥‘ª1±†[C¹°m7yg˜`Ù8c©<íau!¿Lf¢Uv:Û/ÕÒ"(­¡–÷äê!a ã\ +– +QÆYK>T…pÔfB¨Z•ê„ê}wz00±@Õ˜K*U­zP]~ÙØý¢´õ$®ä ½*&BiÆ•æxz¨ÕOË“çnç¬9{â´ÃÞ–:¹°ØÅåæ¨ÞÅí)æ¶ÂZ=oïÔ¤º‰ê›¨ÊÞæ ¡”y?Äø’**–¡’}dz“v£R#©ô0m UŽ½¾*°ÎB~$ D¿•‚D2!­“˜‚Ÿq6%Òþ¸¼7ã2ÀBŒÌÈÎ5;kQŠ +ž_ ±k~5Ê…HU|vA§çŒ=Uò{rv¡²¤Úðö’Ê ¾‚k=Lí +¹¥Ý|€©õ;!r-. ãàj  ¦Ä£²w|Ä@ä$íâJ‘[Qº£r¤Ö¬>`gÔ7’ÒZÒð¥2˜Tƒ`!¡I­ŸŠ˜Or¹•0Ât”/1F^€ËMFï¨Ð¡ãkHñŸ¯ÅØLϬíBÅ÷äfL¨$¥ª?eò‡?®¦Ör9êí)VRœAoùÀØ•+´·_É$)×7R†7½‡Ü‘Zœy»f¤;˜ÞH™Diz'ÁøiU–ÅÉC¥<7ªs±4%¬6—‹Å-ø‰óÊ5·q(Æë¨ìÃt„¯ð™iJm¡24c%@Ø!Ú‰ý‘¦ŸH‡é"mŽ„삱µ;I±Œ«u ,Æn³™ö¾<_kœˆ•Doèµ}½ºÍº¹0&Ýnº{ÑÝûPœ>Ç´z€JâD½61]Ên¹½'vóÜÛ,Ý÷6HåÆ«0?f¬#jÀÛäºcó˜T…Hõ¢b%¡´QµOÛs½~J æ킽Ïç¾”µžÔ6’®40ïËá3q.‹À™¤·&öZ˜ cf3ü y=&&Ø|JªSöz\ÙLêA̹ã'7“J”ÍÓV?@eü©t •âÀ‰°½„··T Ê&Hgà_¹NZc@u€t¯ðh3€©PØÚ…zˆQ™”PF¸ÂýëGt_\¾ç§ât – /BåiµiçZn%½‘Ôü˜íCõ‘F•ëîêÕC¹0SîmÆÅõ?€¯„BªÛ'Bv–T[~/.>B©€Ï”Ñ¢äsKx>!Õ©*8#Ö¨GI¢ ¦5ig"wõÒ‚±ZŸSÝqyò,!”#´cs„Ñô6G=`t ¹¤ÜÜn$äÒJRŽqpð.g€å Эí}þáo§—bJ!&æˆtŸÍB±8­³l½ Å¢t•[Œ³%Äâ¡Ûá²sèV«IùYÉY‡è<"ÕÄìœÏm1î̮̎¿*.´kM­²ïöºÃ'îð±TÚ²JËçoþJiÌ×q¨QÚˆÚâÒ#)3fÌŽ§RR&¤›ð6å,…é<<"$ÀKn•ªwB¸prqÐZRÇZªàªrfÂ9Ý»1~#¡G™ +ëîHÅ}1¿Ã¤ÇI©ŠRãKˆëQv3!p—ë´Ö¦µÂÄÂm`RŽi྄º“ý¨åGL(­ ‘éõ¤¸e?]O®Fy_R¥€F³P®Þ.Õ4H²ìý8¿©0=(ZÁÝ¢Ì~Ji²CGƒZ†Å“àÌJ˜ÝˆÉ1%‚»)®BAI&·a>ó§îY -oc 8`ÄÛÞ1bÎQ˜()V ng¥BêÝ”;¿¦ïúRQÞU\Gäû ù~\¥´vmt½™Ì,‚flÈMÞ]¦[NóÄ«²µ…ÝØ'ÌNJk" Úõª’ætû—!ÒÒÖjEŽ@‚ò·ÆPnÿ±Õ<!%¥8‡,“JJC)m§Ûǵٓ‡oÿº¹÷,¡”h§Ïå·¸ü,“‰Å%nö@¿E(8E.,@}ÑÖˆ…ïíÀµèm½l,_™B˜)¦´6—Fp'Vu¯Ô;—r“˜P ô&¡{;xõ“Lÿ¡P˜µÝó—¿µ»‡+I$ÝäN@±¢B%DÙüyQ“ŒëíØâŒç @¹‰k6ˆÍ¤´™¨²Ô{›™Öoc÷¸u’àÞ«Þ®%ÀÔzÕ-5cLq5.ÞSq>€*ÒÆåªb@ï0Z›·†1¦£@Uf×㪳 ÀR6êfRÑ.Àc·ÖãÂJ€Œ’_B_ ‹k14c áeãKÞæ¼R5L»0gBkƒ×€2ƒ·§ä*¥µ)Ûh~@¡„ +ó.ˆ®Í„HjÀ’÷‚$@ÐjPŽìý„9„‰4Ô!7€žäüV‚σ¼Ç”zJ…µm"B1€hP«0IL(%Xw%.B¦ÔúÇM̳ ѳ„’RrÁ9óL÷IaòR.,i­.µÎìQyrôGšÊêÙ¡YY产B~òCÊ ÇÛΪè•c½r"öåòaÊh ±j#×; P¨q\måú«óÝ7µÝWLaf³2}ªÕC|).×Rz[¯Úí 0kœ;ƒ)ñ™·É#è#´¦^Ý·›gµÙKäa¾Žj=T„V§ÍmCn‰¹YJk{ÂÂ2vd?*WÚAmñZ­È•íLï4!•71•kP~àD6RZˆr@-0fßÛ9špÖb£Ô¬Ê6ªT×"p¦´…hs'rë°’ð$f%hÐÏ¥”Ú¡> +3Pø8•¹×jÊÀGÀ_£T!FW6o !P>kQd<ˆÈ5!×¥ô(S?f­QˆÌ€û¨&Õ1±ª5Ae@ä¯ÇE ÍØGõ ÂL„/P;„Ùah¬1˜ÄJÐ{Mž2{Ðà°ìP ¢;¡ O…F(Wê¢;…5Œ³ùÍ„@õ8ߌ«P€c ê6ã + aœÎš­Dx ߦÃc#XªÂ%¹€ˆ:Œ+òfoÕûr5ÈFH蘳{@»kI-Î:+!ü/V¢ˆ·QT zÒ?ûœ3â2#T­Åc|A²ºåÁ…RÞÖssÜê"j Ô²”mSL;ã+¨¢bš—i¤µãBÕOæb•¶zFi›Ï|„Ž)eÒl‰¹ ãôw„í„Z×kí·Z}ìnRFGÈÏ•ÊPÜå² °Ûðú0ãBuA…DÙ,çNåò›ÛA­QʃÞöáVB(1é>¦VÁ,ƒÖ M +5¥°É•npN'W­è-ð¼àX•ü2Ý8 ³nˆNÇø\ŒÏÃr·šÐÖŠ3:fiƒ6 Ñ8>®7Át“F0©%h²³$ƒ˜Ž+m!·Ç9 ZïábÃçu12æ~„½¢> à!t-ÊAÉ%9ÀÕR‚O2ÁêFpãÓõÄz”ó%ñ@iW ­‚WÌLEw¡3À¡ ™½íbÅ*0>”:xÀ ªÃG(­„¨»„ÜzLŽQùSôp28™ñ#òJ„[‹—Ü``)Üq¡±¿8x?ÊýÅ:²áR|Eµì´¬åM&!‡ ë~(õ+õ °æ7àí;©Äù,X-?¦€Ó8‘âNwa½–áÝ j461)!dµÄÛ->Ó7ê‡B~È%*–@ÏÇÅ®wŒê¡YÛgÓ}?an rR*&åRˆÍ€#º(Ó¿V¡a³Ó¸TÔ+»ZíÀ«".G§GVûavò’+ïz[Ç2ÞÙ†„RöÓ6ëÎäê©P9å rôv„Í…¨  ¥RÞöá °o4°KDËòÅ@RY ÒzhSVs3ÞÕ˜p7H}êÃ<ü¤Ò˜\fíκw®/Cë•*Þ"÷ýñ•NŒÞ–7;ŠŠZ¦ãmðJºZžÞ“Ë>TN°6©Vü¸üá‡I¦à!a¶Â¸&d{¨Z‰2.4Ȧù DšÓ¹â³3L¬êµí cÁ‚qo÷Ø n`RѨ¥èÌönwRz‹Jãb-Äæã€xÝ«ÌàaR®’zt8;ÊFÅrÍKµc³}B-ÐiL~ MÌZC4°l³²B ”¿é)g-®úP QŒÎ¥ÝLë”´Ú1ÂÊu¹Â`×1m“0á ˆ\©ŒŸ–¶ÞâÎì•QÝNJ…ÂC‚€T¦äz\žÝ_±ATBùÜý0¹ÉŠñÐø€r ôž½°1Î>ŠRn”^³7ÆfdmƒRÁ”X¨Õ¨¥ÂM€‚{ž{Ê2æDB¶£œ ¶’è9Arˆ?SB>œÒ !ç”'aÚ‚\DH׃5 &2Á3P„kaš”Ê *íO*[ŠR¹8“G…"øè Àä”R ¤tÐ<›ˆ õïKPQÒý¿çüˆ§`¶ø»;›©û÷;fŒ’ÝÉù«?Ä…üMt3ÆJy¿‰h@…>Ü{Xh:»÷ý‰`Œ y›rIPª%¥œ”P, Ρ„ eŒ/FIHGf#)$Y[Îô@y®E¥bÅ©ÿ°¡9m4½Ÿr¥6{Dš»!v=!­C"è iv³½+JQÌ.£U^E„ +È`àîA™ËÙ™œŸ“’b¥µ|–2KŸnÆ Æ¤\¥¬Vsñ¢¾ó–ÉŽÕÌ »óÌOj?ÙLÜõ§RRfü¢ç@XÐ&) Ì {Ç®FhRŒôÅP툥«¥Yˆ•ÅÃc%J,‰uÞž‚Õ7 ráO|©Ï6Ñ{x€ Ø d-¡ub\6Â9k  QOB˜n :=N9J¦gWgk¨üñ´Xìx7oÃë„ÁLB,«îÊ +,h ÆŽö#ªGdÞGd‹½Ó”R¾¢€û ® 4È9ÐQ„˜ìüaR6c"y3¡m":è´ÁþÛÍ”²¡Stf3ÎßÒÀžŸâÙ„P±ëûJ~´Ä1J'…t ×î©Í¤†+U13ÛNÜ$׳ƈ + +t#è|B©pv }=&\„ +ÁÄ<,,ð&.–­Ò6¨zB¯È'ÙLœICφ¨4"•ÀùÊîŠÇO¸ŒÙ%Ô:x–”REÄbRÈ•Z{{׿Šs?Ø|®FFL”âÒéž‘is¼m…3Ý5B#´ªÓ؇b‹‹Þ^‡¤ÝkM®^ÿ¯ªÓË•(O) u½SèëˆJ¹x æ–Fû<®wãtQr&@è 1ø<|.¨tÐNji¤ÌûaPˆÐAëZQÖFÕ2¢VP¹Tï½ûîï"¬}Ç!\ƬÌ1©äCµ•¨øñLiE+y›Ì×$ØLŒ²’\nô­ÇwŽ‘›¿úm„ÏÞ‹p .Æ6É’\è/˜²ÌÒlçâÛÚìñ§›Ø]º¥07hÛ˜N0!”¡>bt訞§¶¡¡”+¥‡z~J™õ5DÆœ¼b|À ¤Ã©uV­Kv^€ y»8ö¼Âi "r‚tÀ²1fÇ®í€0†6ߌ ¾„Hª¾¸>Ü"0šd7d§XäE¢îû‰˜…)Ç›ˆr'˜¢äšâŒ«ÿü^è¾ ¡jJjj P^³fˆêë$WÃL‚˜ûXÛjB¨BKÞ R«1à_ Š8q®¸õ +xx9!!¼‹pYÞlgê‡\~†ªµ8—‹{ kBDI!EE›m!;¢-0kݤXÒJ £±‡k5\­Qf S€ìêzqJX­õ”µ˜€œ`çm471%¥Th³ ëÌçgq¹’ÒaÆY‡R—*Jq‘\Úƒ‡bu/©7$Àë™qLÝÀÔ”Þ¡žÒ:´3O(m­|¨•vA\p-Æf"¤ ëL9!ÜŸ ЗRï…‰ÏÈJ˜@až•}»ud×ænséKÊ@.œ ]ƒß a›I –BrçŒ="Í^„É‚ûãC¥øÇK¨´ ΨoUœÁÕzÊS\ÐPpص±¦Ö"4$‚Mw¥âŒ²zaÚ’f àÀ®H0€èë•‹0RhåSD©¯Å„ ¦UA‚‚8LÛ Óy™­sÀHzœÍù“òJxYH¤Œ0ªc\>]^øQÈ…‡ÃÒé€gël¨íÅä*tÊåïl$6Â\’ÌD oOäUŽQn3kD I£L× l#»æà2 ÷.|T÷’ÐP´y×­EØÕˆgœÃ¤LŽ·ÔðÇÙèÝ @Æ×B¬÷éd ã½ ½—Ö½3«‘§ ¤Ò­ÆRóÅ ´òa¸ܱã+ +\ãNX«¥A»fy§°:&”cBQ»fí<;|f´ R™PkaÚ†ùQä£\ X{#!c˜Nˆ²h£Î™-6=2§jå ©ÔV“JÊhãFðúZ¨œik±zÿe ˆ\G5ˆºè#<”ÖÎõžˆ…mDj„ÁD£*,)Ìm%Hy§¸¹ +eŽ(s˜üx‚‚ýèÁ5D*V3!Q»šI¾ +`!¼fLr lò!RcHÛ#.·@•fÌ»R n«DèÞ©<ˆ\¡´éô“)©ý|BÄͨ—ø +Tí‚ž„•I© ï-ª¬y9å×#ÜjˆÝH¨!:—”š¸Ö‹²•[I©]ÆèE) xÖ +¤8Ø^°Û˜ÒŒ29\®ËÙ­Hý‹P™$nãR5h­—à+ ¶!5¯`d€T¬¢Rƒ4ºjnKvF¾¸Ål\þËÕøŸÝ Þña€QÀÎw|8ÝÊÇK–I&¿W"à5ò?Þć>Ä +¥l«4ç¬6tF»€¨ Ÿ@®Gx¨ ŒÜÌ”ŽîúÉ{~r#*|æ'þrIJY~T‡lÆ%P…è‚).É—ÐB¨Ëµ¦Ï‘¦µ®š_$ùL€ÑNc7!æ¼ïðÆô\)Ÿ8íÇ|~ ©‡N·ª»4L ÔU)­!8CÁSzDþFBa”f¦~àñŒdåâNqúÒhœ&ä¸?©´K;?f†[k^Š•ã¤Ö‚daJC*í©õ(Å”TäÜ>æ]_›E¹¼3â\"cõž ‰ ÷â\)LBàÝH€;'ÉÇzÀSÁ±½SvÆ8Î×"Tf#Ƈ’o´19ø 63¤¯ýøGãœ[*¥½°‡œFÜ»˜G¥:xCp¦>ÔZ‹Ë(X×”v/€ñ÷áÖ&lR…¥‹zçsJÀ•~Ôº³‰Cƒ+aHe¸Ë¦gzùD)ì’Z‹5ºˆ¿¡ÀŸnbzJ®`bÖë’;‹¤Œ$i2Ju5HÜÛÄ6#"£y÷Q€Õ‚Ò°ˆ8“ÛðÎæq±§ò[rú³ûá?¿Úˆ°P?YÇþb%¹ç]Á%¶™Ôãlq5&}æ§Q±žàJ ®† + £²³«eF :û™¿³‰A¡†S6ÂRBcK€Q`(€ï¨ ¢«…}h½õ¸öÊl3 føÑ2õ6•fòq¶°—A€€,ûa5êý@tÖÊÅ¥‡ë¸Ö"´v ŒyR¼eW“2^£ºKÍnŹ|”ñ* æÑ@â2zGJùÌp-NÇ UJ¤ÝNYÞ¥"Öî˹—ÛŠ‹Þ%•$Ÿ’ÆzRÜHJ ±â\‘´G¨5ˆ³•áèIo÷¥´6QÝ;óÀDg”Ò@M¹#ÊÖ÷|@ÁhUº\zœ’k)æÙ‹rEB®oŸ~½õàC„ɬF¥Õ88q ò 0œâVŠÏÅR:¥V70å³0±’I½£·WÅü"B¥i J´¾e@…©|R¨€Ò†Ê‰{ç…”$í‚ë_‰ +>L‡9ƒÇçô«÷0¡Â]ÐáI:ë‹ó`Í| —ê Ïcf‚xf ±Âd!W?áôòfœN²Î&ª0i=J®„RÐÎíðrI¶Zw}˜?¢ ¦xT«!z5ÂCIpö êA('\JlRJà‚›!ÔøËÕÄõÔZÄS柭§ü1ÕïˆÁৣóŒ=‰±%*e‹A"³X« }5¡ €´Üh œDdºeÁZÞóáþ(Ámï¤_R ¢âÝá ¯Æß—C¹$h%&‚„eYMÀj‹ë0mÔpK°€šµ³d6b²ÇÑàˆ |X›xO6w/ÌÜW“R€ðø8«¨Ü ­þf +Ì•`ì8eaRÛæÇ ñàO­Ê.e·?‹2a:Ë9ÎîþÉ ´s˜ÌFé<àâ~ÐKŸwSMD6þ“•ðÝuJ”ä³8Ÿ‹3…(I¦$fÆ÷cÂÝ¿™²#¬w*æã垙ƸP Ø +ÿ~?T'¬hÊX ó¾„âK@–¥;þt*•¾VºDcœ/¨“bÒžQëQ&ë-;íAoPw¸ÅUÒÀdzÓIx»XÜÀÍU ~Ü"AWöZ\§¯çvóP«ìÆ…Jˆ´ÀŒƒý áJ”± Àª‚9´Êû¸ÞÜ$̵„âÔ(úZ‡f÷ygÆ@àÉ`bì ¡”ÃŒ ¶1¥w@º#RhŒ¤7¸ ]WÂj”]õˆÙŒÙg¬˜ÈcU@dB#$Å"®·”òBÊÏ0©E™@¹Ï|Øj„N2. æPŠÊ€Ž°%ÄDJ00ïŒÑ‚eAÄ:mô)¹áYÜ€Q±“6b’/i@k€Ï5vk³GŸ¸õʲ!ÝÕˆ<µ‘07+BCÅ:+a~-Ä­Ùµ0ç‹Ë÷ƒì½ã‹«QÜæÌ6T,Hî¸w7|×—Z€Fa Kmn&åûaÆŸ”|Q„úÝ$¨8ðòP«‘TÚ­árî/VüŸÞazn#*mDehg¢ƒ¸öéJh-D5ð{Ð9”/¶3ö¤Àø¿z÷Vù>z‡•ôVì^Å» …š€~8WŒ¥Œ;˜wk”7éx÷wQ9Êê°™¡7A6$…"¦¶˜ÌˆqqèY§'g|~âÃLxÅ’¤”:<¼«Q¨éõ>¼‹…ã8A:%Ïà_ù̈м˗¬Ñ”Ýž”™¦µ)£MéMÁé#bÜ¢QÙ¡ÍØ4Bml¤ŒõhæB{ë%cµâ´ä³k¨êu j…¨<®69§² „=¦µüDæ^LY‰) ä)8ˆRôFAÛ«MÆ"jõ'ün ÞÊ„Öµ6"71¥ 8À4xä0oeò¼2yÁg„Þ\‚Æ €AC•íðÕ˜c²ˆwµZ:Á@• ­‚¨ +v,Æ–9{"9°€ê°{Ð f +CƒÕ ´º” ¹Éga,?!`Úz˜‹’Ù]ŽÐå¤ìÝj"ݦÜõSw}$ÙÇ#(pULºå +œÞŒ`òf”‘YÐÀ+!ôpŒ.ãrÅaÿìŽÿÓ•ÈZˆA=RÎAö µ&@×ÅWÉç@ÝÌ‘nÅSV)Ë»b¦!dïzb„¿%½ûW—>•‡‡Ÿp%þì~ì'+È}(xÔŒ0Å7h5@aBRJ ã³P–Px!Ïof œ8_á³S³q –—Jq¦×w«C¥lv +Ïëµ=«}šî]"Z'.U“R%ÊæÁqx®M®EùbJ‡ÎÍEß |n4ywD˜PÅ_> „5c̦œª¥m{@¨MÐÏbvÒœ>+ ®¤Ü8áy±"`"WYg "¼m´§ðI*ÐûàÊ“r jžÏÏ¹Ü Ž#2›<À‡¶»˜ÖƒÎ‘ëJyÏé7á`ÂFgK6&Å*iö¸Ì0:™Ëƒ» vœÉbR]t· ö¸wÏUa2¼ ÒÅäVJmélT(ýŒ0úP?>Իǚª ךI˜0•°@ñÖzŒOIå„WÆÙ_ 9h:!;Õ"\$þ”±ó.d¯Fy(-\¬¡|…†–Qê¾ïQá@Buy—–¤"ÖÀ‰ÖùâR7×Â4°j„pÃÞY8;—£å²âö}I!€ÈŒÑ0ªÛPo(…ÆìBÈQïªSIH·Á[ùAç°yÈ aô1­……Âj˜Ýˆ®Rwü¤Õ#Þݳ9Roiå]³¶kÕv9wƒu ¬ ¦o$µ“WKgÆ|nZ?Л»à†ôÚ›PÅ™Ó=+n=7»ç¬;Æ” +Êe¬úí %”F]È¥ü„vºjV¾!æÆfkŸËŽ üà³ôÆAnp‘é«ÕÂîãzG/mƒ°glÀŸ>ä”s‡bnT^°î &–0½Cef\aG,í²¹-Ìèß Ù±Ý:@ÕŠÑ<¶:TfœTê´3"¡P­®Ÿ$”RR.E…<¦UáeBn¨”—FãH­îC­ÒFð «a ˆÒ¤òáv€´Á"rÅOXq¾aò€á^PfŽI[=\­_G˜t˜4h£ xž”Ê8ÔmvʶZÚMHm:Ý‹ËU?“ 0pÀšRÙ‘J[¨V_Ô@Ãa«¸ÕÅí¾Xœ«Õm2Ý#LÞJJ+'Ä"—a åâ‚Na)½™K cŒ%ì$í à&h‡^3»`{ ½+ŸR›ˆT Rù|.•· Rµúz~ Lܽ0­%¥²Ààà9««—çéΞQ_¨•q}û±^ÛGåJ”= ÓÜÓM—&ve+e4#BÏ¡~Ö’:Dá©%Xpj”2Õâ )d)½Â9]ÊꃃŽ– +{BvÏFu=)1oȧ¼M­ÎW˜iõ=»}('baÀ8-¹4.L/óóGRy.æGj~ÐÞ}.¦˜Z€²#˜¡R4ñ…1ï ýÓúò TZB®A5š}¨+§{œîÑÙ-&³Å¦¸RÀäBB,q™‘QßÑkÛry‘Pk^8…9íô¨t`S(íh­3£}jw¨µ}9;jï¾1Z'A.OÙ}«ufµ¨õ#¥ºGXmT)Çø´C\ Ì2f#×?-Mº½Ó\ÿ`rD(A8w Á‚˜Ç‘Rª!Òñ]ÊŒ %Ѐ6¨T&õ&—ZõC±0c2=(T£¶Í8„˜gÝŸAãÈ•mX¨ìà<Ý9Ö;©tg-eÅøkÃë§re~&Ä"¼Ò¬ï5·_¸Ã|yË»ΉT^f‡Òí]¨.6ÝÈʳËt{?!”RjÃ;l4Á,+ù1&ý)p(&Ø({©°às3Àøtµ²ô~=ĨC}&¤RJkª¥em~]™_S™@ t78tfD™4®”ÕÒ¡ós*;ËN®õê¶Rž@ÔzëD®í‹å]±¸mŒO¿Tj;>ÚÄë™Íc±¸ðVÛy7-;}ÀƸ˜ S¡´òĬÏ3½ÃÂôaº{Âæ'¸ ²#¬ÔIãl\oƒ¾Šó&Ý÷d¤\ûVÔgºŠÓ'¹ÉUntX.ÎíA¥Ìíö•êR©n& º Yry¢lLméÝ2pmÔi³ƒµå‹­G¿(m]‹å™Ó‡9V¶®[ÇŸ·O>/Ì®ôÚÒi.‡Ç/F§o;O ³Åebi‹/LäêR¯.·_꧔*Wåâ\*n ù¹Ó=mî¾,/®íæ~cñ0ÝÚ‰òÀª5­²Ì æO +[×´;JH³ºê*zÅnîéõeaò°öaþôÇìôqqô`÷ñ·zu'Æוê‘V?ÒkÄ«öáv÷ÓšI0“”KJaäNÍö‘Ó?o~^^>‡Úδ÷:[W4Èrïˆ +)µ%d·ÌöƒìôYmçZÝ- ÏI« †Ž²Ú¹Ñus÷óÝg¿;÷/w®¾Þ}òÝÙW«[ÅÙUaþÐê–f—£³¯·®´{ÇdºM˜uÖi™£±_ß~ÑÚ{åôŽ÷žý²uðŠÌõI§cÔN{¯8l®œÉ¹u¨Ôwᯨ’’¼€¥º÷®rð¾¼ÿ®²÷Î\ŒŽß¹½}­:ÉŽ/Ìö‰Ñ:¶;§åéUçèmqöÊ€.Âg0½ +M¤”æÀàv縼õ¼¾ó†É€rhë• ªd™tM)Mr£‹êö³ÊòY{÷õøäK&Ý…#H¥à­VÝM·O¡ýœJ{mXÝy>{üýîó_ì¿øÅââ‹ts©•g¥é“€üó.—×÷§ß´÷ßT×ã³Ðøq>Ëeû€¢vû¨¶x68û¢ö¾³ÿòìÝoúG¯`‘SFÏ ê2£«ÆáÛâÖÓlÿAûàbÔP!í6¢Û ¤d>Ó®-®çO9¿þáôÍo¾ÿ»gÔö´ÚñðâçÅÙ3Ú™°î\­°¹9¦·Ã\–²Û16Tè´·Ýþq~ò¸¼õ €·:µÉÅþëowO}¸n×–£ã/Óí ³sY\¾qÙ®N¤{aÊQý¡²óJïœôÎ>,_þ¾vôµÕØÊ÷Ôê8¥W¤òÂ蜸“G™ñe}÷Ecÿuº»×=|R\<ÔëÛVkŸÍ ìö^¦{ètŽÆ~Vž=#íºXäÇRq¨”¦€Ã›rnü¨µû®½ÿÖi/ÜöR«ÌÓÝ£ÒüQarÕX>íøjñôÇîÙ‡êöóüè\.mƒ¾mo=;{ýWÕé9&;VuÒÙX¾š?úzpñÍüú—`ÖÒ­3𰛨&"Ý8ºüÅàì»ÂüEaù²0}äöŽ³ËÎöUBpcBÌ”DÓ‘sš^È•¥Vôv®‹Ã3LmjÍ#wzm<æÍNCœ¡-ʬÑfE‡\´6an̓÷õýÏ.3­#XœLo»¶8/ϯòã€*‹ÃgóÇÿÖU‹ÙÎÞìò—ð˜^þ…çN.úÛýûû‹ý¿1N{rþåâ鯆—ßCÿNþ0{ôs»}üòËßÿì_ü÷éÞ®4•tÄymûùäòë£×¿í½]ž½ýëõ?ÖwŸB t÷^Ô–×õÝ—ÏõôÛ¿?ýò_ç†O¶^ì\|%ÆjÅãb»}Rš]ξÙ{ýק_ý}÷øË“'_óÈZ¦b4vÍæ^i|~ðê÷ûïþya|å‚Z¨l±™®ZžÉ¥9¬XÊìcF¯0|Ô;þœ4A +6­&$ñðÊhîY­ÝÁþ«o~ûÇéÙ;ÆíuO¿k|YY<›>útŠÖyÊwúùîètB+绥éÅö“ïö_ü8:ÿ Vvxw<Üyõå¯þ)×-†Çï'_©}`|ò°8}ät²½ãLç Î;$䢾]ž= X>üöéw7<þüôñ×'Oª‡ðöÚò1Ôüôò›GþðáŸý±6½<¾zÿíïÿ-” h Òî€Yu]Y|Þ9ù¾{òM¦µúø‹gï,´ç´YÖjÛZëÔî]æG¶žüÎ^¥´šÝÚÏÏŸ@ýë•ÈgxëV™^~þ«8}õCR.~2éAuçMiùÂlƒV׋‹ëw¿ûîwtj;r~RZ<Éo=--_¯~9„˜›Àæù”èšÕE¦{ÒÚyÑðMmÿ wü„5;Óƒ·Vm’ )ß.Ï®:»/Ïûûggo@í(ͨŒšË—ÍíוùS`1£¹l/]ùW¯~$Zcû( ?}Ø;ûº¼óÒîYÝçïûök6曄¡W÷2ýS«½Ÿ›<蟾¯.ï]¼ÿoÿçÿstòÖ(÷wñÕ¿œ?ÿõîó_=xÿ‡ù“_ë•Ý—ŸÿæÍ·ìz>´±ý¢ºuÝÜy><ÿº}ô¹V[>y÷«ýG_qÙ^uëq}ïevt™]¶÷Þì¿üýàâ‡L÷léÝ96XKŠ @àI¥%›_0Y0qÕÙÃÊ`¯»¸¢¬ºQÛJƒÃ­ï¨Õe~p>8ý”'ÈËîÙ×@”¼Û1K&?õq¥TKÛ¥ÉuB*¤”b¡wh7¶‡'¯§_ægÙüœ±íÙ“Ïü7f}Z=˜?üi÷è}zp®UfzuNeúLvT_g»'¸VÔÊC«¹“†®i8­Ýêôʬ-wNßì^¼— £ÞþóÙÕדóã_̼î_gËÿÙ?üOÿþÿ>|þCL(ÐvGÌM +ãG¥­×Åù‹t÷ŒµÚ_~ÿ7ûÇ×ñ™^ûà}ãàËöчáÅ/Æ¥7àÉÁòâëÚÖã0íD™,8ÄÚ. ù“Ñéþð?w»ÝÖâaRȳÙ¨.7]ÑX<}ðê×G×?Só£ÞΣêü‚q»¤Õs[Nç´ºõ¼6Šk•\m‘ïRV2j´Ï”Ú˜e›{˜œ SŠè6Š££ñéÛ׿ªì<¥³?i4JÐv}Q?*/_vŽÞl=úîøóߧû'b¶×ò¬î¸ïøí–×?6v_6–×ݽ—éö¢ãBÎlîSv“¶¹Ñ(àÜÉùÃÓ×zc‹2 n{Qš<¨í<ë½oì¿U›‡ŒÕ*´wÀ €§v[ûr¦KÊE§¶ì½iî¿túÀ ¯ï„\·0<¶Ú{R ŒÛ–RÙ"­zLÈáréúíoìÎáJŒ'µº\˜€Îá@çdF„ÕŠÏww;ÛãB¦¹x´ûì7Ý“o3ýÇÓ³ïZÇïµæn{yýü‡àrƒÐÇßìð~飴㠯'?φ‡±­‡à¡ Sƒ³¯&—ß>ýÍÁ“›ËW¥éc£0|øö×ýý§Q>rPü”Õ¾H÷.j;oÒ½Ám^¾ø®1>e¬¶R‚U½tzçz}ßéž×–/óíƒåŇ +˜Ê"w»)­b•çíÅÃÞÎ.Ó!u  FJ.BßwAXž=* ÎÊýÓBsÖj|ø¦¾ûJo€We2cD®‚£ÑÊ;ˇ?D. ù©ÑØë½ï½ƒÇàðspÙÕÑÙó¯ÿÀ›U˜RyûUzðÿÎëß/W™\±vçÍw³|ð&æÒºÓ¹s`?+1ÑMˆ9`Ð3¼y±0 .ίk;¯ÔÚN”ÍâZMÉ r­EP’|F,L…Ò<ç‚GÛÖªË0iJÖiL2½Loä Ø"½±Wš\º³4ìÍÎ÷Ÿ~ŸŸ<‹c±8Ñ* »yhÔ2ƒ+¨FHg}vÕÞÕÞÙ:|#T˜Y³k[¥Ñ)¦æ*ýÝ‹Ïÿúôó¿iî¿3j;„^ƒ))ãÀ¡°¹Í¸'u)Óq;{NïÀìì»ÃS<Ýà³ñÙWM!7(ŽÎ{G_Nß7·ŸXµŸiD9'˜2âLÆòA\ƒ¾mí½íœüÔ_¥{G¨œ5«óæâ™Ó>£3cÈÞÄ®õöžv^Æå<*²í(uB«¥SZ‚C²C£º°êÛˆ˜o-_,ÿXš<¹2:|œ'å’[ž>~û«|{b,wtYÙ~Ó?ûnvñs§û€ËÏÉtÇ,Mž¾ýu¥w@(•Úìáòò§½½×¥É#€DÐùÅîÑôèsèˆ\û`ÿÙo†gßåûvmÏnì‹ù)DZU–³›¹þI¦s4:z·xô39?‹äÎu~øÝ¿ÉT¨TâÝVžkeн‡'–W?K²™Zÿ`ùè[±8E¤ &:´QÁ¼KEJ­ÓJå2{W߃ƒGEQJÞ]O€Øù…ZÙócÆO6ñ”\µê‡ëqÖ‡J1.Ê´‡”énÿôñ÷¹éi\uq³B˜ T©ZC 0Ю‡‘3ÔP{¹ÑUº³GšÍ—嬆šï +éjyxhµö´Æ®;ºôpÛû¥ÎÞù³ï·~Éeê´]-Φ'ïgçßT·žYícµ¶MZMXÏúä’%¦Õ­—‘ÉÙ‡“W¿;|þk¹ØëŒ÷¯Þþ²:;‹ïÒ¡ZçÜQ¾^]Ã|AÈôSj!Fér¶dëñ£ãÏÁ^¾ùçóóïÊý£ãGߎO¾Š°ZàbS+võ@ÊŒâŒc->]7ªS˜¹Õ{€;C '«¹§B»'¸^øíî¾-‚qÈöåüæ! B.…I ά‡SJ´ë[OÛ;/ + ¿­&e4º;/ǧ_áZ•I7ÍÆÈ<­03MDtÀ çúGR~ÄezLºÇg'nïA{ûåäôƒ’Ÿ B–ÖªÙæn’wRRº>Ó9Ui·NdwLˆ%>Ý‚”ÑÞ驉Õ<ê¾_>üùüê‡úòY\Èqa­8k¤ä<0 Ø™òäªÜ;´Šc»:ó!*çôœÞQax¢§œÝåÒ=)3HqîÖΣgþ€Ê¹û4†Ë¬ZJWncËaLS3ÿîý¿vO_’!ºnŽp•í?¬LžU&O¼/‹@¤<8/³ãCDXX@ª”V²]µ4ÊÔfǾª,ÏâŠÍdjNwÇj/å +d¤“Äy·Ø>L7=.#­¬9"f)½ +xà,G°h^ü¬±|h¶·¹Âtš´U1ÊÃÚâJ¯‚ÊØÅpMs~aÖ¦¨Z𓆟X Ã'Oò½SDrõÚ²ºx1;ÿjpüº6?7ê3TÍLö//ßÿR)öV—©µC»yd”¼Ó¿ ±’µ2½VJ½$£…NkïˆÝ'ßO/>ô^8­íB{ùüýWïã#tÊî*åxÙî©âŽq±˜‚$êƬ@€¨RPë»buO­V¯;ï¨t'LëvuòIÊv’ЭR^rrf 9=\("Ta)9+f:v}[)NA] bA+Í¡Á¥F»<}PÚzä N9· ¦)%Ò•Ywç—nþ¿$½‡—ÛÖ¹öûO|ç¤Xu4•½“Q €@ A`ï½ ‡Nï3ÒhF#4êŲä"Ë5®±ã¸ÛqšOœ8±c'îIÇ©'çœï»çܵîºwSw-®Y#ŠC‚ïû>ÏØûÝÃF|ÄÌFΣŒÊÔÖýÉ^ Œ‰„ÜQ¥´pÄ€a¾¤71ž? +Ž âÓ:§W±¬ ¤P!ž™Þ™âìÙ\÷¤\ZF„¢ƒÈA½ -D›Püx­ºr%Û;ãKÏ‚`>b$-´„ˆiTLnJ™Y9;Ïøó˜G!¼É=: †t¹:¢µyuø˜™ròk[×BÑÊ +FÜQ}¸¿@øŠý;_´2b¦ÁxFr³8ŸÅ|Y•Ýe@@Άý©Vª»š›^‹4§"^¶;ßÞ85¹µWZÚ¢“@Ó¾T›n6>rñQeD9'd‚io¬*¤êŒ’wÇ +áÒÄñKµÖÏÈÕ.ì!Á”?]Kw—¼•I-^ˆå;¾xuÔ„¢\ÄÉE5ŒßÙa³³6.Fãr¥Ë¥k\ªŒH ƒ;‹ +ŸÎåæ–äö¬Ý¯8¸£4`Ðîʬ…`-€Øe:Ûd0Ñá°‰²S)9é‰q)åK×*Ó+ÍÕ-6S5»ƒl´Å'º&¡qðG,‡Çlzˆ•ó3 09)3öLª,{R=<\·ó) ËvN³52×àK@ƒéxkæÒcfz`Ôjsr„;B°Q;Ô;y5Ì™È X;3H‡–æ¶.Mlìq©Ê€ U¡¼¸ ä¤Ã?¬vÑÀ£&Òê’Œ2jvÝYqà%5w¤ÈÍ ‰I*˜ŸÛºÜYÛÔz>2l'”‰×0¹ %xÄŽT,cb„´‰ .Ÿ™8]™½ÈÅš£ 6Pl K*Ò¡²[.¼Žµ–bãKB²*å&B…ùpiÙŸšÑß8¨½kÈ‚r ŸÒÐÛèƒú1‡ÎÆ3r²º¢—0o~ÄèÔ"6W€dïRÛ÷©m‡tNÔW’KǤìÁç´vÞ®0D¼Øì.nήï;{ýÒ­'n>ù½'^yó­ŸýòãÏþø×üçç_ýãGï~|æž'r“GI©Ø_¢nv£ŒK§ ãÙj¯Ð^¬õ§V·6N_ºòàgxbçÞGV/ÜsüÊýgî{âü=½ôÚÛϽùvoíäÌú^¼6MR&䉔ØhóŸP2Uë´f–§VŽ­íž=uý¾«?qÿ³/oß|ôèåÏ>ðô£/¼þòßyó_ýéû·ž}½»~š¥­”¨‡ØåD+¡t FB™b¥·ØYÛi.o&gë k­•í¹íó÷=öܯ>üä“ßýñÅÿâÄ•[•Þº”n ÎàÌN¦Ârº“¬.ÁLxÌâ¢|ñro-ÛYŠÖ¦¥RG©OÏn]9y÷×n=ùüë?¹pëÉÓ÷~gïÖÓžXEúMˆOgs ÁJÀìÁì–ÅdUÎ7’•V®=ßš?±²{ýÆ£Ïß÷ø³?|ç×|ú‡W~òËã—îoÍH”g_ØE#"h!óÆIÆ„ú´¢)ÓKµ×C…ŽT_;síÁï¾rßÓß?{ß×|fïžGvï¾}ù¡g^yûýGž{åüÛ«goŒÁÉé!7ʆÙHÍŸœtËrfBˆC©V®µ8»y®95»vòü±s×O]¹ï{¯ýè÷_ÿãþÇWþûGŸùÀ“/Æ‹“‡µèËA'ü‰i¥°ª¶rcF‚p´?ƒñ ²â"ÂF0.É4Žîm]¼ouïž½ÜûøóË'.—gv3›”˜Ð`ß:d¡þüêƒO»k±Ú +«4õýþlĈ'ÙÅ+¢RN×fKí…òÄÜâöÞå[~÷¥7Þÿè³Ï¾üúõûå¯?ùÝýŸÿþúïÿ|öµÝóð{Wo Ñé/”ØhÍÌ.;¾}æìÅ‹W®_»õЃoýð­?þñ_ÿõ¯Ÿ|þÅ¿ùà…W_Ú»tyjiM)4ý©¶Ý6#î–(OÀsùòÊúÖ¥»o]¿õèÛ=ùÝ~þÞ‡?ûÕ‡¯þð§?ùÙ»_ÿõßÁ×ùÞ«?xáµ·N^~`bi7×^ö³R¼/w”|#œ.OL­Ì-¯/¯­^¾xñ?øá~ü“Ÿüì?ÿâ«¿ýœŽ÷?úü‹ßýþÿü÷/¾úê­ŸÿâÊ­[K›bªÁHY:ôø™bcbfåèÉ ë'N¯ož¸tíú+o¼õ›>þèó?¼óá§oýì~òÑï¿üò³/>ÿâ‹O?þô“—ÞøÑéË÷gZóNZvÒao¤ÌN£lœdÃA9/Ö—VW¸õÐà8~þÞó¯¿õË~ûõ_ÿþ÷þço>ýø믿úõG½ðòËϽüêÒÎ%.ZÜ +(V¹ìO6ÉZ½»ÐžZXßÞ}èñ§ž}áåç^yãû¯ÿà—üæÿù¿ÿõßÞÿèÓŸýâgþúëÏ¿üꥷ޾ÿá§ò­ )©­ŒÆL ÞŠ@‰ÈÔæ’ÅöÒÆÖý=õÔK¯>óÚßýðã¯ÿö÷¯ÿþïï~øÑÇŸþõ_þò埾zÿ7~úÙ'ïðë[?±{éF8× ü1D¨-8'ç}J!«ôæ7®ÝûÈkoýÛ»|ôö»¿úâË/¿þëß>ýýóÙïÿö÷¿ƒ÷yç—¿x÷ý_½ÿá‡>þôÆÙÝÍ+”T<¢Ã°ÏäðØQVgÆR¨´ç6w/\»ÿáç_ûá o¼õâëo¾ûÞ¯ÿùŸÿù»¯þüî¿ýõ¿ùè“Ožyéå“ç.Ï,mùãÍd}Ý'Æ‹•v®ÜHfòI€Q‹ãÓ“ +ó®ìݸuýü•³'÷N% i.àE(Æ‚Ð6ŒS›pµ ÓZpç¥`<*UªãõñöâÚÆÂúZw¦{üäƵ{ÎݼïÚ™K—Ï\½qtïbujVˆ¤˜`Ηì2¡’ b,0,B¸o}óüìÚ‰h2UÊ¥WWæwÏœ¼vóî¹ÿ'?ùñýîÃO>ýì‹OŸzáù­³gêSSÞpœ 瀮¸×F +´7¼vììæ™ëÙæTºÒl·'æç¦Oœ8öôÓ¾ý³·?þôó/þðû·þöëo¼úòK/<õÄíûコº~¬Øœ€¢Án˜ ™pÿ p ¦²F»‹eÅd"¶²¸tﵫ¯¾üò«o½õê«ß{ï½wþò—?=÷ýïžÛ;±¾ºPn4¥šƒô¨-(ê ‡Ý%Ú\"ˆ8Í#É|¹5;»´wjçöÃ>ýÌ“?xóÕß|ôá—_}ù׿ýåƒ_ÿìÉ'=sa7[.Ø1·3¬¶Ðû GTÐÆ ã~I.„äÌxwº7¿°±sêÔ¹ ç/]¹rõÚ‹/<÷“·úò«¯|ïùçžyú;¯¼üâ}÷\ß<ºKWœî€aL(QAœ*¹® gaÌŠ$W6/^»ÿ©g¾÷ð£ß¹xùú£>ñó_üêÇ?ýñ}÷\|ü›Ý~ðäéÝÅå¥j££äÚ…É-!Öº3ÚtÙäätfÜ3±dqyyãÒå+/¾úðJ=ùäí‡o?ÿÜsï¼ûÁÓÏ~ÿÒ¥»çWÓÅ*áöœ1 ‚ÆLiŒ°¢l,/çÂÙV©5;>³†p‚ #­„Û€¸u Î#ÌHZµ@£Ò9í¨»V„CH?FøQøX!Íùä”ÂÌ0bqâ¸Û‹±~—”Õáþa3®³Ó¶QÁQ~PmÔ˜Q÷Z ÚŽqŒ”Þ›àDN¥i*¦òÙf§»¼µ;¹´Œ'.VeÁ†ÍˆÊFê^=Ä«¬´ÆNÓþ„W)â\¢|$'BŠO ¤P4‹$S‰bµÖ›÷Ë‘°­Ö¹lVà|Ämň’p>.$'oò›ƒF­ƒå¥’[ÈøÅX1WN'Ó‘$‡¤¹…åZ­ÉÄj­†œ(‚ƒ$ø0Ê%íxPeÄGõέcHmT;Æ,n3Þ3ÉŠùt¡§Ä‹©DîÂ¥«GmfÓñFµš/'Æ[V5‘*Z`âà À¨=8h<4l¿h,ý•øæYQNGâÙp²@óA«aXQŽ}D"]®7§›­)ÇE¢IÁ¯ „Wk¥ˆ~+Dd‚%6TÄ8 ,«Óíæ”`¼’šñe²Íñ¥Kµé]?fµÛD¿&y!a´Î†(¢¥R¶³Ë*Q£ë}{ÄŒˆ01@p–IÊ *J4VbÑDã|JP.ïOy¥œ?\c }ñ‚àOŒÓ¡ê·ö j¨í „Zœ„»üÙD±7¿}C‡ùÞâÞéÏ#1 +ìŽQ߈³¢ óíÖï2Ö8†u¨óម7\cļ‹K8=Ñ~O+9jÁÌ.ÉBFL˜Dp)) `Ã¥G8;&@„`„i­†ùÌÑrÉ#åŒ0i‚Ià ì‡x‚t0cq‰*¹cGôè7uU6àu-x ¿ †:YYã`Ô6Ê„ðc&Lg'46v=<ÀdŒK¾´¹@Üå“Œê†-f§cã*Œ +y>1íä2ƒzÜèäLNvX «¬:=¤ÖZÕ:«¢)Gq!'É£TH £jT˜1ça5ø ¢"&˜Ó£Z›[eãTv¯i`Ÿ•”œîp¬0±°~ZŽåÇÔF +0NFFI¿ +¼ÃˆyPë„\Ê þ×ý£*ì +Ú¥¶0ÄgÄDp< ò}‰¶• i!.\ x€ç¡"BøÍÛl§ëíuŒ4‚·²s9Ø_ÅW†ªOT‡ú ¸8j¦€Èz"ã\bJ*­G*k™É3\|rØ€9q6žm[QáÛ‡4ÃýŽ>^nT§NgÛ;°'upØ6¬²Ç3];Ú7b6’(°+UÃùy)5¥3QƒÃfó™ Ner™‚“ý©ný&“|¬sx >¤¶Ö˜´ÖA­Í‚a6Aø³ÜâIÈÉŽR˜&Å,.dm®°Ž:ùt¿e(1`o hö û!d¢W˜ô¦I_ÖéŽ&‹ ‰ÚÆA•ã°ÊªuЈ7íVÚ´ÜÀýY æ§Øp®6 +éãˆÖ>l€Í˜Ì/ò©)&RƒJVøpÎàô€SÙÜ4d!£.©)oÙ\‘#:§rˉ¦ó©¬Ô°Qg&%*P •;K*HPÊì® Þó¦æ ˆˆ±>Z‡ù¤4×À°íˆ†‰P¦¼‚¹#ƒc@W#˜¿¤C%-ì×BÌg=Ñ.n™p ¢CRRíÜ"äI°Ñ@€b}™Jç¤ —ö1Bž8Ê¥)!ã•Jà™av×!½Îˆµ§NÀLtÐèrzVZAøŒ'Ò–‹k”XÕ»ÆÀ áâ>2f³8y”M¸úÓM›¥CúKF 3îwùcˆ?vXZÝwº€–¥áOMf;[¡â &$È`÷çÙè¸':áô dD‹†tÄ )Ö_ã?êDèÄĬ¤L‹F®TÃ:§ lÌŸ€ýù1˜7á‚œ¨t—N‘¡Ê H(Ôg§$;Õo1¢Zëð˜azqãLcö¸Þé¶`^Þ¸7ò9W J‡ZàŸ..æ‹”Aౌq+Ñç)W¨îË̱ÑÌÄ#Ù­ èá¾ì +AT˜‹—6ãAŒ +¥ËóŸ1SƒZœ_6`² +É®÷ﱘQÁÉH ©³¹@R“n¡AÕŸL¿6,€q¶SQ#&ÒÂc6·Õ“æ¢áâ,îÏ©ï´1á2ø_ÈE¹„ÊÚ_tãò16õ­êý:ðMÙHÇ%VÍXHcåGt$  ’ƒãp¤v°§Wåð±€IÚè„Í¥ÔgN¥šKV:„ò)œÏ¡îæI¢ž” ‚¿³ša‰î1«§ß`‡Š k­ÕâÍm=ÔÁéÏ€j0fóÚ] *Xå"u_¤â –˜@f­³ò¨¶q`Ô>¬v?u¨Æ|¸þP ûHvÌJ«mŒÎÁ°üö€ñˆ +±à’•þeßè‘1+DøÛ6y†Ìü£û®1xŸ +ê6·z¡¾|î ‰q°H Æ$gýå ¹~”ËL‘B­µºº÷ …ŽBžQˆ7¹bv&EšlbaÌ)‚š`FüG´@PüŸÆù´³ßã¥ßòš +!wb°/mãh°HE*Ÿ@ýyw¼+V¨PÍáV@èšéÎ';ƒK.Á†kÓ«—W.>nñ(v&ìŽÔýÉé@r:”cb݃FîŽ6:Ût t×eÄä²âýõ•ÞèDeöB¢qLJÖ—O\A…ˆÙ@q6\_‹46¢ãÇ£;Ttââ¡xýÞ‡ž¯Ì)iF½FÌo&eÜ_3 ”ÔÐ:8é÷• 9Ù4Ìfíž,â-rJ‡‹Oèœü€Ú º³¢Çä’!wrÇq!GK±Ê%fAzzãJvÝ™bM†ëV.¥Ã%=*‚4jéχ×#ÜW‚Kºø$Λq_¿A;âJcó€R {âRrxbˆÁż‰”Æl̨™<¤Sa\÷¦ÙX+5½gõfŒ¸ñ[°€Õ¡G-âC¹Æ&Lê±oZö«àamBƒ(_¤ƒuO¤EI 5*ˆ¾c õçŒbDE!€×•«:K±Êî…Û´˜þÖ€áˆÆiBE ÇÀLÂLøˆÖ©µ{H! ²Æ鎀ƒ æ¦CÕe*>N%(BÀ G›*rDƒŽi vºS”Xg‚ãD1{`*Â÷fGõؘ‰<<õ!A„Í{“r~˜ÀP¥ß‹•Ftm2.SRUL÷Àa0¾$+ÅGLШWƒªë +ÛÈæËØÙ˜p© )%óÙi¦?ýo6×ÝÍM ”!)gº¾LöDTÐJŠ'ØD lw¦>aíìãñÖqpx“KgêÓ;#&ÒåO+õåXg;Þ=Y^¸Ò>ú°Rß… a#T¨`ç€1óA\öæÕ°\´ÊƳ7ü 4b!C¿EgÌƦ‰È¸X;ÆV¬\ÆJ=‘²K*¡B².©è‰6#Õ.Ö²"¨-Fˆ8‰úÁà›©aA$ @mpê4䎕÷¦¦üÙ9T,#¾¢;ÚMŒŸ —7ɃltÄ3*Pv0À*Ç1¡À„* ŠbÉÐïúåêsiöX[eeþeÿè¨Ò8Ûj,´_ã3„P@Üqh4¯2“À¯‹ö¼,——Bå¥úÔ©Ów?ë·0Vió‹Œ2^èœ,÷v#õ1Ä7¨GQ. *(¡ŒX‰Ð'£ËÌ*å9Èß(o é^°²Ì&'9¹\ïnž¼ñ¼óQR1T^‰7†+˱ֱhó¨/;k&ƒn©àK´Fô™]À»•`~©µq#;}¦ß×JÁlÔ@Gì ™´ÒW¸ªnŽ¯ÞS_j"\™·³á#FÄÎÈÞd»¹zþäÍg×/>^˜=MH…H¢2·yÕk Xhàú‚ŹPq”Ûri¹<µé–ózDŒÔŽ ©9„Ï3¡à]¿[7Q]J·6F-Ô¾a«ò?àVêD°Øß Ùô ð¹zR’­Âì©êòÙìÔV¼}ÌŸŸÇ¥ÎG}îÇ›nYéÀÒóÑΙÈøI·Òµx€W,5—§7îþöÈJ”2!5§æ/d. åU=åÃåÛÏüpfãâ00T¨èMõÝ釳“guNÿ! çÞ(ˆC:tÈB«"èO—t0Á`§FF¸8€V.Ù¶·ÓÓ{c÷o=4ÄÏþ¸8±ÅG'Ôv®ßâà  Äjnú u€À˜~•ò¥Í˜_cgÓ ²ÕÝ_:j^—ÿ£EaR¶b`—¶“ŠÚ +´Ò6%Ž˜]j§NºÝÁÅœ˜‰7WÁï€ +cãÛrcÓ_XÊvNšÖo‡k¦ Rd‚Y3!êA ‹*›0jfMXøàùAÁ¤…Œ«šIŸ÷j¯SÈÄêë¥ùó¥åË„7YšØH4W-õgÅŸžÂƒÕhm=Ó9)¤'K!Åb¨° ¦z@Áùt ÕÍTŒEIˆïW9€E .,HÅ•`yMÈÎÂlœ³Jm‘Š”Md–«\¬™i­,ž¼±qî¡ÖÊyTL §¯=ÊM—¥ÔŽ-ì>¶ûÀ¥õûéø´[hœKGŠKcýy#˜Õ%‰Ô:}ÿ¹xWçªj›ÇN)ÀÝ™©ˆMÒѨ`á¨!lkqñÄuV©C¬bqù _ÆáËЊՖæwo‡òS£vÚì +¥'÷€Œ2JKÝï„ÌZpQÉt íM ‹v*‚‰TÌ£’'1åIõ¤Ê|iîô±»ŸÉOŸ:¨#¬ðÉ£´ao0Ô5ÖÏå"åÏ|ãˆAåà˜XÏŸ_÷ÄzllrÔ8΃p1î·R¡©ã×7¯=5¹u£¼p‘ÏÌéÉð€¾ß +fçîÂÅE`5mT”Mب9¹¸¨Ç¤}Ã6£“zŒ?}hÈð}£ƒz¬ ¥@yC*­ÁLڂˬܴáòÁAÓ ?¢!ŽèH$¥ƒ¼ +K“À{«!NÕßB"EàF¨xGH©HG†ø ­Lá6¨Àt„d£¨;lüBÇ#à|<úíªú Çx`zÓå%9?¹oÔ4 Gtˆè’ªñÖfºw*R_òÉù­½ÛÍÅ3zÒ/æZk÷T—®%Ú'+sJóéèø#ÉG[„/c%ü}/ÍÄ`¡Ì„'”ÚQW¨ta#1 +° Àî:@ñ‹.CÉýÕ‘¨˜Û—ìàBŠò'³ëùé“Ù©hci|é\uö4© ‰ñôÄVfb3?yÜ“š4Ðñ1$ˆ7W +‹ƒr@‹‘2Ÿô(ne¬)=ìEAý­ŽÜŠGiØÝÀM‰•é­b÷¨”›²sX¨åô—A‰`“¹™KVwzÌæå“ý+Šªÿ¿»4Ò‡Ôˆ…Æ+KfT¸ëàè·ŒéÀˆeOrš5,”rXÇè°0ÄfµøþÃz×A~ìe§£ºþ‚}%\]Æ%š›t0a Fà+Êë\f‘‰M*똘7À}»¼+,ä@Ïhí¬ÖÎaÀ$<ð¥wØöر +J³ßq}Ì>dÀG^Ñ +(0JVÉz¶µ`œô¥jó{µå ÉÎ16^G¼1›G1»úÝÉ8¹S!½äîV¦åʉdûLeþn[¿ù3(/Io¬DSåð诅Ž3Ê—˜Äƒ€…ÎF®µædBá‹•gÛë×æÏ<¼´wûäÍï§&¶™`eçä½î}š”r*dñ$0©,¬äºg WîÌ]£ÐAPC<).9ÉÆWFAv)`¶‹è1扶8àBÃ5'—ØH‹¹he¾¼rñ H QÝȶOG +K¹3Íþà¢⼡¹¾Ç&::—“}œD}%wÀêCT¤0*;¬c 7*-§wVÎÞžX¿ŠIUot|ýÔM˜‰P‘‹¶Cù9P`åâ +l ©1‹ c&öͼ1b’\^IMnoƒù²vWPJkn•Ý Bkf÷±ÎöíÒòÝ©©3°¯°o†Hey÷§/u@ [=zœ…$p‘Jõ¸RÛ>¤v2h­¤³½µ³cD8,”¸Ä,p§àUFÔŠ Æ~» +u +yW¸Þ\¾¸|ñ øFBÊJH P™pÕky]on‘/¬BÞ Ì(‰Ò<æYpŸ 0ˆ?ÏHeàú˜`a@mß7d84jÙ?bÔ ëû»:x“3€[è +él”KÌy¢@a:Xš‹·ó©I\,€AJTa6àdÞÊ”˜/ã" 11eÆeÄ➈ +©€”`~RÈ \ä‚•“ÞæMÑ" Ù†€â $xÄìÖÁ")–Â…9N©9\'ðF l dJ73½Už?¯-Ç‹ó½Õ ÑÊ ÌÇü©n¤~”¶97/W7<‰Y« @“â–Ê.vÔBkZ˜¾yÀ$œM¸Ã¼¨ÏÁý*Ó·†t&TbÌB¬¾ÕܸéQê\¨8µ~Å›™@ã`Mww`\FLtÍõ#5¤#)_Ù(ÙÜûTöý*»ÚÁ’bÞïØ(ÙŒrb¢Ž°1Ä€Ó#D%RœŸ8`Ç€ÀÁæ&ŽcÞŒTfàÿÁ1»ÊìBYEc£û3Ø¡óYo¢KGl¼ËD¨’ë'A=çã¹ñµôÄ1ðŒÝ›Õ“!=.RRÕ—š –ÖUvÂ.O¤éO/)`6–i¹©vö:UfºP_löŽöÛÄé1=pÉu©ºo~PåôEjJ®="á0°xa.Úð§&è`ð…ÖÜ¿ºbDƒ.1O ¨7éKuCÕ`y=Û;'WÖ¹DÛ@ú}çÚ[ÀÇ‚Z×oBµ6@(®#"xûoßÙ—a°ß¬^@Ý)A)EŠTÆçNB| ^vzgñÜ‹žÊÎ_p§§Á R¦;³ÙžÛsËUÂkaîG¤ˆÖ¤¨³“œ”ñ²X àé” + H Lj3f&«ìýVÛvÖÑ¿BÛp‰ýÛgfW€ô%³íåúÜæÄÚ¹îñ»{ǮθY[8+76ì\,Užé_8┣‚w¸…ûkîH×A'a¶âÀ$[-ˆhëo–°»¢F˜7#>µ…vy3@6.êf­®ø,˜†!í +×øh=ÓX^;u#—ÁÀöu¹¿ãL%˜žáÂã¸7§±sX=UYP™©ýÃV••5á2-Õ@Fþp‚R-Ll¨,Ô¨©c%TXÊõv‹³§{ÇnJ…%û…ñ•Éµ‹*«ÇŒømxŒ’ÆBYP/ÁÇA6%js.1"ÍÒ ñÑáZ ²’ž»ÐÙ¸7˜îN-žzííßúÝQ›ó×½©…XmíØÅ'×.j¢€…:¥²{FLY¹a‹Wçùè¸+˜SÛiÑ* „„òØÜ à=B|Ž +7¨PÕŸîFkËþÂT 8§´6½ù:6N…ËùñÍÞƽl´9fç´pÀBF > ˆÕ„ˆ@õ0DØä6Ó*›Ç‚Í°&‚b´Tè¬ñFºpïR¸:©-ùKKxxÜÁ¦ÄHefùìÄòÝvF4á:؃°I*XÁ„ ìϵfÄL¬0Ó¡þEu\Â|UBê™W9xðUVzÌì9”wØHö;á!DHºB&\Î4–n?ýæ³?øuuþ”KÊòÓb~6?¹“l¬U'Ž·gv89ÇË6\u‡ª¬2ŽpyµM84†éß¡¡¥µy†õ®ƒ£ŽÖa2f\ö*ÍRgk|ñìˆ6à+5Ña*R —W"•uVi°*O¬'«‹FÔ¯ƒXTo: +Ì£¹n¢0udÌ1¤A`ฤ•Ž\ Î[q‘ô¦…XÓ{íd¤,Š™ž§?§®@*.!]j¯W'‡©0ÊF!J68XæÖ׆ûÉ&æ ªlvÜ1W°èŽµ¸d¼ä +(éÚæî `׉@]©lG«[B´-§'Ådæ3ZÂíÏ& +ÓÀ⵪m^«+E‡&[K÷5VnÒr㈎ Å¬.îo è¾5f4³:gasþÔ¤ƒZè ÎÉí4‚Ç=Šù³Áò€>J®J«þÜæ/Èý•Ñ-=í÷$q`w{§¦6ï[?ÿäÖÕçgO<ˆûÓ¤/ð)+y4d$Ú†4°âGÌ4Â'%±ºG-n#ÄU²S~JNç&VWOß_YØ1Ò"ı¼’híH¥åþ<ÛD|z¿¡7#$(<_xÔTãxnb‡‰´G-¬ƒp!OŠ„•V…DðP9`/Á0‚â©s¸‡¨áü‰æÄÊÙÞÆ¥DsybiwîÄ5O´„ ºÃÇ[•s…ÉvwÔŒ{ÙPjrét$×4dÅF,dÆÃvW çÓ6,`q +CZx¸ßŽÃ5¨†tN—/%=7#e{”˜žZ½œk³³R©Å;§²ÓçÀ—â£ÍöâÙTm0;Ê¥‚Ùi)=Íî}D}ó€VouƒÐÚÉýÃú#´ÝŸ©²¹uv®uÈæ>¬…,¤„ñiàâÕ¯R±4¤ÃQ&ÊI•‰µ›`ÞñÀx8©ðÑ95fðYm%Ž è1`'_‚ö*Œ˜Ä½I19qâò³›×mƒÝI:Pš)­à\"ÜÒ8<@õœž80<”¯äŠ¾ø$ìI¨L$í‹”p8ÿrHÀÈœÞáÔ¢ÃzlÄLêú¥Ò; ²šp`T,˜ H.ÔŸuð ˆãb¤žR_ã”ÂÜÖÕÖòùTc­4¹ÕX>_š;,.þŒRèyÂE䶢ÆîÚ1f@.Ù%døH ø±Ló¨|S-lrr˜7®.Îœ¸wåÂw +“Û'/>|òú3Þtâ’ŸuriJªùÓs‰ö^~þš7;C +©lëX´±Î'Ú[¬ýn·¢ð‘.Ä$¾=l¹kÈrHOÀbÏx ÚþŽ¨^›‰”œbÁ9ÛYÚºrãÑï/Ÿ¾ª-ݹ ZìmÜÝ^¿ÂDŠV:àMtJ“ÛÉÚ’/ÖPòŒ?®±àN*‚òi#"ŒY`56¯ Z fâà¼2h•f^™À|e˜M‹Ñjwé””n!Þˆ?×­­\ê¿ 0ÖÙ¥£mì‹ezë{·mžÐ ÓC,D›+b'#j“ÛŽY®Õ;Þ[½´ȼo@ÃéŽóɪS D-Yž›\½xÙ +Š˜7íäâ(‡È 7RAùøˆ%Ü}ª±Q+m„¼£z 13"XQßÀ¨Ñ‚r„7¦³»A¤ÅËSí€*gÅÅTë¨T(Ëú´@ÌnÅc!®Pã“G4NVÌg„8Kÿe&ƒZ3>ªGÕ0æ|ìÛ‡4ß:8öÍcõCzÔ„øX1 )S›“’ãîÓ̇‹9Zi +™é@i9ÛÛË´u'×^ûñs[×3’KsbnP*—˜vú2T Ùhwᜅ +5©MZ ¦îïnƒ#Á‡Š´˜ºï`R,ò©‰t{µØ;ž¬Îž¹|ëÄÕGøDõçBÅU)¿”ßi¯ßW¿Tž>ÃÅÚrrbñø*”ÔᬅŽØúýÄò¬2AËãF<¤rp0`pRvøF¾Z|Ðäœ%fæ…Ô íÏ-½Rên0á¢î·sIuvk‹Wç¶om\{>ÕÙʶVVvo™>ÖdÃ5\È@ý­Ø3ŸväÚˆ7 > )»¥6™€owEÔ`Ì‘€Ë›1’T0™6* DÙÔŠÜ1áM¬‚¼&JñDÛ˜/Où23+J V©øsÓ‰övafobíÚò©*‹gÄüTsöÄ£/½ã‰·MT˜–(© Œ%Ÿº³+wÐ 0x™Ö±þž)wÚîtÕ:|(›%ýå!5b&€>‚ëß}îo“];Ó=é‰TªÖ9È/¤‹•©ã”TÐc>ÈQálªm< Y² ’ÙÎA„@oP‹ƒjï‘r£rÐD3J;?{.Ò8ÊÅÛ&j&eb²îpGœ\ +PŸýÉ^¿‹;Ý$­eDg£µV +szbñ¼”¡/—º>þb¶sâ_êGL¸Ì.%šÌŽoóñI&No\õ&Æ÷©Cf—ɱ¹ã€Ôs½Tuvjy·¹xÚD(9ïÏ´…D’ÍåÅ“÷®]z + ÃåE¥±ø +WØÔÇ1’ +ˆ´HqA)-0á ÂÀyŒuîl´7¢¡#æHsá€ÆªvPö¦zòj´¹k·RŠ’î\¿ýâò™Û@ ¹XM,L ¹Ÿ˜uÛJM¨à –ó' _qèŽo×ÛY€ F!ˆNG]ë=}õ1¼qLr+-*Ò²¸SLdûÖé_Öâ’VÒííñ•ë‰úN¢~‚w ¸h4”lÐܘ= &jÈQ«`À'Ÿ妷.ÞÞ½þ„ƒ–ƬŽX=qJ®¦[ëÙöNbâ´Ã—wв\™µÚÎÆúËíÙ¤‰šÉP¾wº¿ŸÍm£#N> 31  + $@º“‘"…®… éa-WbÍüä©É+…³ý—äîùûŸã|kiÌƾé/¹U;Ó9ýÀpRþ,âQ†L¸ÖÉSÁ+BªÄúmyî‹–¦v}úÕÎò™ñõËd.efϽwêص™cWãÕduþÕŸþæʃ/8È '•<¡† ,Ðñ–7TuãÀ(ÂH%ê=4f?8lþÖí¡aI tG;Œ\‹–æ=rèè¸+ܤ£ljZ‡x©P^.NƒŒp+•hs­0¶¹~µ±q½¼r-<¾íIvåtoãäs[7°@”¢ýY%ktlΚìoïntVÚþzÈ€«ín!Ù<ýÎ3riV*M%sÓ[wOo__={«½v1ÖÚÀåšÝ'î\ƒµ‘£?ß̬f'N”¦÷B…LÌk Î +tÇl”ð¦s…Z|r!TXaÂÕQ«vxœl@ÊNd£pq&Q›?ùXã"‰Ær®{<ÓÞò³‘êR¤²è‰ÔB‰úòö剕SVJø­-õ[ÙÄÆ ©ÖßiÔD80ßêöµxiVmç„YȘNƒl¥0uââíÊäQ€E˜ñæfƒÕÕâÌÙÍKOí=øzsý^˜Ïjs«;w“¾$ˆ±hyµ0s¹ºx#1~Ö›èdÃý›»÷&kk‡´ø0PX«ÛÆÄûÂZZJLì@îx¡±Tëm‚W:Ù!•]rM)ÍáHMl3]`£ùž'T4Ò¬OåÞ¼[i ñI#*ª¬´[.'ÆûšŽñ)Ô›Â+àÃåDuÞ-åʳ»‹gIu·Ãåy*TtÐaŒ‹œYÞ½ÿúãoŒ/žu²)>:IƒS0:èþ 5rxÔ9¤£}‰É1+õ/Ô‡Uð¨ÔYY ÆýeÈ"ÄËUðn€“ÝíÞÉÛKž..\t²ñtk£wôúæ¥'›³'·.?ÖX9ªÎ–Ï&göÄêº?3Û[¹²yþñú 6!5onD*+îÄŒÍ6û‡Í‡T“‰@tHï(ÄÀž(,P¡’X˜– +Sóǯn^y,PìF› ù™“ýöDÕµTûtyöŠR[·P’êßVáÄÆÆùÔ4›âRó¤Ür²i3T9'—púò@X\ö€AËh>{âÉm$ x +âb¶W˜:][<\ˆ:˜-õ6…uò©4ß\¹²vîñé[Re‘KÔ!6H +J{qÏÎÙ=A2TÆÄ" +BHaîh¢8%Æ<`„}¸¿9‹™› 9˜é%j‹@G [î(&\ñ„K‰ÆRså‚\]¶3‘ùÅ“WxFˆœîH¾{:Ó9«n‰5Ä ›ˆîì=o®éo2€¹ ëÈåÕtw7ÞÚ"¸Øé‹·ŽŸ½ ¼—‰Ô–¯»òìÒ…gÄòš™ ÃTð±g_ŸY;«†…ÃjT û¹øt ¿‚ûJZÔ‚úŠGcõ!£ËŒìTªˆÙY_zŠÓ;—>wïwÅDÛJJtÿR|/V]ém\Þºútif+˜7ë‘›:˜?8fûæ!Í!+pƒà­P(uè®A³ÙɇÓ=…Öb@7mT¬¿å( ø,ä +BÒk&ZGÓ;X ÊÉÅBw»8sª¶°»~òú…[ÏçºB¶»º÷èÎÍ—z'zqùö›­Õ»Ùh+Y˜›ßº×ŸnA¬ 2’Ëœ«µ1B”cµ +pþþ´ö€š@Ëe¥¹žèn7W.U—öød½»v¶8·n®ËÍ ovÖiôg5Ç›@¹¼¹žÑ-Cþ´Õ›¬·ŠU-¡8¼92Ò óN_–WˆH+.©Šù‹˜5bÀl{q.Žp +Ä…”êÊøÆÍÖћŹóéÎv¤ê[%˜jNmßíÏO 3Í£7­ãîdÇ­Ô±N.Ÿ±S~Ã.-óñéËy£MÖCÊ*þX8¥»mjĈú\b:^[ã¢ã&LÒZ=xÓî56ÜÓ€äw®~çä•ÇÛ+” ©a7DúNõw Óãƒò‰dm9YYeº*+u`Ī²262l›Óƒæ䔑ÒåÎÑÚùp­¹zqêäýÕ…3RqŠý‹BüÚí§Z³'ÔNP`\ÈYȈÆØS0!^'J×æ¹>[te£n¸•qwtKÁxíÂÍÇO_}Ü%dœžduîrvòŒR]’2]@v:®²0F„-´·@ÖÑ¢à”@²¨[±‘òþa»Î⪎/?ôôF´èÁa›Êìv0QViÓ³¡äÉJõé &˜1õÛsÕ]ÑI˜ò¥šK¹Þvaj;Û9*—¦½±Z¦µÚ=~­³q¶ÐÛÅ3˜Ÿf¢ãˆcõÊä&®ô§²øû~÷*áÆL„õ‚ß]BRmuZA¸¨/5ÑX¾¸|þÉÉã7ʳ;“ó›?úé{»w?Æö›_ÍÄ'vBåÕXssbãúú…ï´V/ç&ŽîÝ÷œ;Ùջ¾ü||âDjòÜô©ïLï>V]½R?úÜïßzî§&*¢†Å1ȧFüV.íŽ÷BÅ¥lcåÔÅû§Ž_ÉÏîV–ÎågOʵ…òü©æâÙ•3·NÜó4PÃLkmûÚÓ{¼0êâì^ ?Cûs;iy\UvÆFE,”ª"-ÕøX‹ód¨fÀd*P¶þ1¼¾s²BUØ7b>_µ™“l´"g›Ùú¬Ržç“Ó˜XE½œKÛ qðÎ.Ò $¬¸þDyûX=(›t2xec@•0oÄ7§4+2„‡Ý!3Æ8ŸGΊ©ñòôNkù§Ôœ8ÈþšŽ;3îp#Ì;ðzÓÞDÛî’ 0À4VûuvNc¥Ío$¥dk½µr¾0µ#æfaoFmg½R1˜‡…sNwçrÞHç“Tpö—c&Ú‚ˆ‡F ƒG¬ÍcF +"ƒNW€’Àóï;lÔÛ˜1y×ù  +2S*›[k¡(Òé–ž°Tœ •–i¯v¨¹TèmÅÇ7}éI·”&JeÆP!À)&T——£c€õôˆb˜ ÕÜRiP‡ð#ž˜ÞÁ‚Ô89pz"Œ\ fÓãÿIïÙ%Ç•¥çþˆ»$ÍôÐÂʧ÷‘a3¼Í0é½7•Y••å åQUð ‚h@4MÏ64Ýä4[3ÝÓÞΨ»G#iFjì]ëêjݯ÷dk­ø@2*"ÎÙûÝÏ[‘gŸýöÚÕáÖéñGÝÓBÿ`õÊãþÁ£í^Ì´¶„DUNµsÕÝk/ñ™µ¸R_¿5Ø» ÑÜ’r ´]M®¿ðõ£çßò’¦˜1ÉZÚ\>}kéô­dc«³¸ûùW?=}ø¶˜[°››·kë—¼ÿÖw~þñ_ÿ›W?ùÑþµW?úö_½ðöw”ÊšR^ãs+Vã µyûÖÇ £J’r50øù¡–ç!1ÌÚ&ÅæVË+wåÂ(ÂX•…-J)²VGÌ/Æ’m“¤Þ,N7®½kYÞ* É&¦7 +ƒ«•ÕçÅÂv.ògSK”ÕuF8F͆( ÔV7ÿÓæ,Ú¤Ž +ùTs—PJ.T¥bcãùÜâ “êÅmB/{±q?)»ab⸜!´/±‰:®@iÀ~íÞIÖ×€0’ñºUçý1¨&Z݇È`ê³æÆã.TBÄ,›YJ4/‘Fó$Ÿ"…ÄØœYw„ø '°)>Ñ•*p©#=Qõ™É0ˆÒe ˜‹Ÿö@„Çß rÿ;éÆÏÎ#ç=”Ñ‚±ÄxÇ!-¦Û©öv€J˜©ŠQ\À•Œ’í +™Eà’ÄTß,­êå&š1Åb%ˆ5¹dOȯo8ŠZj)ž]œÞ?È1Å‹óAæìLðé ^`"‚hœJ!BES‹Å…ƒUà|/¿”émájJN7Ú7ôÒÀ® Œb“sm²Fƒ7›rªsI«º’élÇŒrT.iPé,Æ,ÊvÕ®,ÉÙŽìÆ–\Š™Acåjia×LW›í¥;ßh¯òV¹µréðÞÛW}pëñ·¯¿öñ•WÞÛ¼ñRoãðæý×vo¼Ö]Ù{îúÖ Í퇃½‡­µvób„µãÉæs¯ÖÚ¼ùì<2áÁ£R.fuôæ¥üÒuTÊƤÔý×>æªV<”%Gvçppðhå껵ícöNî=xã“Âà0Ìg©DRj°Ú*/ÝX¿þ~vñJ2.ßy=ßÝ™ +rÀVÏ„$?•‚…raá +@tÎl4úÝí» PÓÝ]¨ùc6®Ub‰fª·ÕÊNˆáÌàIBɃ¹ð“Äg¸d»¶vëÊãï•—/ƒ4Ó}‡³ÞqãgˆˆâÏÐÙÅ•")eâ¹ð8™þ5.»‹¥7`“]Új; •WÓ0gΈ`LÔ|¼´f6öôú.iÖcjÖȵ¸dÝpRª—^8µÛG™Þ &—<à)üä´+*ë5Qsȳ3þ3Óþ Nxá(›‡é Îe ÝÝ— •œ”n'»;lfÐK`LÒÝKˆ˜b@ 0¥fT/õýqÏ+ÒEDHv¢JȬ3P!ËÛ-…^Y?b&Ýh”±Üf.HÉ8Dª¼d&’åd¡KÉI!ž-¶–«Ã­BoÙ,/°v•ÏuôÚ(Û]kmž3Ô\Ú?¸÷nëâ0’¼^FY+ˆ ˆL¤j«ûw’ÍuàUE{ÁÈätŸ6ÛÊðF0VT6®†)UR~=Ó½48|ytòJwçy%?¤EûîýÇ?úÍ¿]º+e½±g-\«ì¾vòÒwV_ÌuÖe»ðÊ;Ÿ¬^z8Ï»H&µl´ŽJË7/Þúب¬é©öÝ—?:~ðÑTˆŸ…e'HL­Þß}¸wïãŸH¥õ|sã…w¾—nm» Ñ€AÎöN—OŸ¼øi}íŽÝz;QYj*ôìThÊKÉ*íÊzgóvLÎ_¾ùòßý .`µÉæÖ˜ôHÈo,_þúÖo¥;§A̸öÂ{W¾¯æÀ˜ŸuFAf¹ )L¥­}fžœp`Ûûw.ßzÕ þìüŸ?3÷/Ÿt<1r¡€yÖýh°_¹¿G¨eÚjaJ øÜ@l¼™&ä€ÉŠš˜¨øPчÉ—âÒ}½¶]^<îïÝòKAB±Ë+RnÉ…—Y͆øI/åqk2rÚ¡^‹I2ë'‚l2ÄgÝã%~¹›q@,gTôòrnùòðôÕîÁƒâÚµ²3¤üÇ™U@YÒNÊÇx".¬Ê:"„©gûà–ÜïBµ©çFU7"'Â*%PÙ½oVÖ@Ê3Jc’HŠàj<Õ4 §Ïø`˜109Ã$ªÕ•“áá½Îæ )ÕÖs]9»c¡ÍhyœµÃ˜ŒÅ´(­ÏbçˆûO¯0üQ)L(AL"3m"1Ýa6Mˆù0Çô(oƒà–‘jV;ùÖ"ßfVG×2½}­Ð‹Ås1%)Æ“²žq.Ì]p„©¥(Ÿ +‘šæ¼ašâSjv@š¹ˆ¬×ôê¦ìÖG—»;·SÝ](¦g +­Ã;VqÅl£4ÒŠC!Õ¡ô"²0©û£ +5îë•8ï@)Ìd¥ô²VXgÆ'99‘«ªƒðµ±£Ôv2K7²ƒ+Fy(?Â¥ ”n^YÚ½ ¸    šÿ–VJ³|TS­t÷áû/¼ö‘žšŠLx¹Ù¨‘kzmW/oˆñÜÛßøòî£f\øÓY? àßl—¶€qú ‚5õL+Lh@4`Ζ³=À~åÅÃLkâlZHôW3U',úPŠ×¢Bå“>(6ç§_‰±‚7ò†£˜²{—Øqï~T.£ŒÞì¬<¯cVM),Ë… SpE¸(¥ÆĤ+@ºƒt„0Ê&øœ”ì_ B§8¥ÄJ¹R}­³vÝkJRºç|Ôøž=QZœõQÀOÌG'x#À_é(Ÿ¼!©fü¸ §ÛéöŽZ\瓃RÿDÍ ¸Znoeê«®0ü Uü¤í€!ªS›à–æ=aAIJfmÚHRpŒ]˜äAtˆI{£q@ÂܘLJ0—S ŠÝF˜*ÄGÇßž̃G)%BÈREh0ƒ–'\28„<ãŒÌy°y .„ÅÆû<âj%̤¡˜U^ÒJÂ!ŒAëE\L3j^6 +11áôã”´òDzÂXˆ¾@gã%JÎÇãà0€x=Èø«\€FÀOÇðΘ¨ìC€¯Ú§:Ì&an¼"¡mBHóñ<ŒËó^ôüt Xˆ?sÁv*4ç‚,FÒ²‘¨eÊ#ácbw`~<‰¥0›’LKÑ—­v%«ôôtx¼.,=, ¯6·_¨­ßt‡ù0¦ T<ˆra” bØD˜ÑI5'§[rª…ò6''95…Pª;ÌEi„H’’Ýݽqã¥}¤ê„Ù¡ú`) ‡<çE§à½yå/ª:!q>ĺaçÓàæaÖˆrª¶’¨­i…‘œ]ì>.˜*[uÉn‚¸ +‘†š]4J;Сå + “sA§bƒOœs=}Á7ã‹At6Ê—CTÊ‹ÄÓr‹˜˜  @u=QÍOZŒ0úQùÂLÈ ýÀ;;¡ssa$AÊ>"âa\œš‰ZÉL÷C¨€„@T( Ü’'BþTÒõo¨()ïfâ¬á’7„¹}0 +Ø7Wuº}¾@8‚KPÌ‚™„•g}Ý)gÔbž:çžœƒ‚ˆìôÇü`¾09áœ>2óÕÅ}VÍ{BT”IDH+Bm¼0rÎO”Šs*-Æ㉂]QR¡Í5NUœUÃS+¥üí £¾ †’j×ã8{Þ §MPîAP¹‚|‰{¨Çšœq›r¹|!E–s»O  æ×À¸L̃æ9?˜œ„`Á`Î^ð<ñôôON;<%.í ÆJÑÌœž(òZ„t ‚x¬59~òÙ¹gÏ9§füNJ°vfçCÓþ'âòÇ Â ¶3Àž¹àyöÜ<-e`R;;陜‹Ì{ÉhL—´BלÞØÄä Ðãì˜ôO:a_ˆ'3çŽ`ƒS8ÃáÙ´Qí¶¢ë‹b’™ÎVZZ2åDO8Ž„•¸™­tÔLãÉ ÷¹i¿'Ô*©iv <„@— +éÅõu%Ÿ "“nØñ¥‚OvÖ¶3ÖÊÊâí^,µGççüçf½ç¦]ÉÑ £ª2ÇóPX–Û6b îtÏz}.X–˜Ë›û»«;‡ÛíÍÅt--ë"«(AB2<ÞíיΠ+Š—9EôFëœZÑ °gf>.‘*Tsí>£%­\ a´Ê„¢¬+€Ÿ™òýù‡3"Db©ó³0tÄIV±Þï.¯²¢ dÚ…0™×@î11aôLm‘3 +L¼€‹©9æ‹Ä \„ ŽæTœ‰‡"T\Èá¬I3"CÉ„¦hqšãEIÄ1Ça’„Ìâ¨G0@‰Äøƒdá!Lñ…9O ‚kÁíñz|^§×ë¡šK ª©­¥ÊÕÃþÉþÒK/ßÝÜéq &E?Ä€Ú‡ñÉÙð¿zbòéó.‡‡âp„"!‚À(URdIây1F½@ˆ‘P&)ŹcIZѦÎLºžxzò©gçÏOù'Æ=ŽBÓóøSç‚Oœõ=s!0ëÂœ^ÂéŽx‘ʇ‘X"Éöóã(• ¹ä¬zúüüŒ‚a6ˆzÝNó«"\Êp -{iX+ÖìdV©WG›í›—7Ö7»å²Y*$ô„Í«À³³O™žžuf4–`QOBFFÝl±b‰ë¼hè3L¡XT†ëáí½/ß¹±q÷¹‹¹röüŒûü¬?!E Ë0ŠÅB:“EüÐx)V¬” —._©u{’iŽ¶×ÌlÆà®0FÉ6*$äd¡e +ÇÈ8lËÎåJF\M|ZE·G僥N=SIË›ýÜó7·_~þàÝ×®=ºéæÕ½~¿n%ÌHò·Cph2Fð(‚²14o³Í’V/jƒ–u8JÝݯ¾qgíãÇ—¾ùÚ¥?üòË_ÿô;ïínkµÏ ¡æ¹#£´EÐF„fÊhb9¥eu¶[1—{Ù…fn}¹sãÊîÉÑèöõ­×œ¾ôÂsÇǵJÍL˜,Kþ‰);@M9g/ø§è3gæŸ9379íñz!¿ÇÏQxR—ëÕ¼&sr©|>k¶efÂ0õÔÙ9°.o4ð±h@Æ%Ú/\jÝzåå;…’Õ.vVú»£ôº¤lpHÕÀ÷ôûÇí»µƒù¥+­ŸõÚϾzãÝ6>z0ú»¾ñÏÿö‹?þî“ëÚùÆ?»u¸’–c¡€×?ïÂQ0 ytÚ7ÈS·/æþòýË?ûÞ£Ïß¿þõ{ƒO_]û›o]ûÃßøɧ·~üñÉýæ?üðù÷ï5ïV¶‡¥¤¥¦R)QMIñ\,F|¤žˆ®”¹Ù×o-¿x\xRýòÃë¿ùÑ»ü‡¿þÝ/¾ùë¯þ?|øÿý?ÿð‡_~òð´õù›¿øê•·î'tyÞ97tH<&üÞˆß :xÌ+¢³ý|ìòZît£´;0W’®ô¿øæ‹<ºôÁã[¯½xÇ2>˜Ž ¼öÙ›û?úæµÿð£Çÿãß|ë?ÿöÃß~ÿþß}uç×_Þ}åæj>!Ò Äá …OHjN“—2ôË;æÛW ïݬ}ñúúß~çÆÿÝ{ÿð³7ð+¿ýòÖ?ýôÑ?üðá?:úÉÇ¿üìò÷ß;¼¶S*ålÈ£DÐQ¯ºÖI­ÔÕkk‰o<þÍÇ'Ÿ¿¾ñéëÛ¿ÿñ[¿ú꥿|÷ä¯Þ9ø¿ÿþÓÿýß~öO¿~ï÷_=ÿ_÷Þ÷>¼ºÑK ëüÔ´Ó‰D™¦{y±®‡jª÷rŸ½·“º»“|ùJí×׿zÿè³7¶~úùÝÿò÷_üÝÿáGoþóï?ûñwì­Õ{ƒE»Ô `r´ó–ÔLƶª$8ýÃ;½_|rýo¿¼û½w¾ûöþ?½óŸûÑ/¿¸÷Õ»?ûôÆúÕ;¿ýüÖçZ>WÞïIYƒQÌÔ´ç駦‚N§óu“ÐQG¸¾f?<­¿{oõ§ßyðO¿z÷ßÿäõ¿ÿá«ÿ£×~öÉŸ}ãø¯ßZ{÷fõto$ICDQ8ã44›¥f£=êæ»YfÁNûì{w¾üúÑ¿yõß8ùå—·ÿþ'¯ÿÛŸ¾ù«/îþê›Gÿöû·ÿÓÏ_ùÝ÷ï|þêÒÛW²ŽjÅ´æQ‰1.Á J9g”,²a㇃øí-ãážýÆÕÊçolÿî¯þê‹ç~ñÙÍÿù¾üŸÿñ¯¿ûæÞwß¹vÿ¹]£‚P,¼G$ØpÝ–‹Ôq_}é¨þÙk;_½{ùG]ûç_ô¿ÿë/þáG¯ÿ͇§ïßßÚ– %†cxåx5+iYE±U–N‰ðbA¼´Txn§òà ÿý·vÿî{/þáÇ}üÊÎÇ÷—¾÷æöO¾yíÓ7öï^jREÔs´œ x±qE13ùD")à-›ÜëÛ‡‹É«+ɯßlÿàƒÓ_~ñð«÷¯~÷̓l¿vyáÑQãÖVaµ/gzPžI EÕlÄH.m(I…ÊÈøRIº»WûìÑÆ?8úÍ—wþöû/ýá¯ÿÓ/?üùg·ñ­Ó?þìµþÅ›¿ùöÑ/ÕæûE…üN,ë•d¦Ç ¦ÃÊq¢ªÂ‹VèÆPztZy|­ù­—7ÿË¿ùè¿ýà÷?|ùùö?ÿþ“Ÿ~çÞ'wß}°>ìe$5Ž1LÆóŶÀ²†H®µ’í$6HA{MæîVý‡ÇÛËïßüêó»?ýôÞç¯í~ã…Ñã«ý“¥ÌbQÖ„"9WÉLLP„¤ ¿Fø&±Þ²w—J{¹+k•ûGõׯ Þ¸µúúÍÕƒ®q؉ïw­Å +Èu@t I2€ùcb’äS¼’ÄD>ÉrFeVjÉÃÅâ¥~âñÕÎg¯®ÿú³+ÿôóÇ?þæo?\ùÖ ƒG»ÉV‡‹+%QÆüGLô Êš÷ aˆÀÂ~•Ulµ"tØ{Ûö+GÅ_XúÃý¯?þå?ýêíß~õÊ·ªíb +@¯‘ƒ +ø‘_¢°¤ÊM¦WjÆѨpºV|~¿ñÍ/~òøø;+7wê] Oìô¸f~w„52 ùƶžî±¬ÆD9©ï¯-^>Xm¥ÉKýÅãîǯ?wó`ñÊzœ»½lä4SeUYAc†7÷a&ÌfH9OH9+ÓTU3†ÀEdt9£ “5ì£åÊ•ÍöÉz}gPè•ÒÝ|º™¶L•Ã `¨1gˆš °(“†0Éã ú=žh($S±V>·Þ+_¦ŸÛLøÂòWï^úöË›?øèæ>¸ñ½7ö¿x´õíûkï^ï\^656 …ü$#{a ‰% Òpø)(Bæ©ZÚ¶è@ÄwÚÚÉ¢uc5ùÅë»ÿùo¿õûñÏ>¿÷å;×^¹1ºr±¶ÔN*šÌªYÙî Laô Â0¬V-Ô“Q©¢ÅeäX!ÎwóV;­õÓìñRöÖnÿp©T7è¼&š¢ÈQ ‚Ä~zÒIž™‚'æ€gJQ i¶45›´ ÉdÉ6“2mñTJf ¦ ’¼P*uCG¹ `ƒ(Ÿ(ô@΃éôbÁÎÒb6UZl/¶—ʆ´\Š_[¯¬—Å“aao!»VV×+ÚR!¾X0 *癞tÌü°Œ å?3éÿ‹§g&§Ýa_P¢([âʦ˜ànš•ÄËCûñ¥·ž[}á ·Û«˜1ŠDH)^ «¹Á ”I{€ÅšUQaA•r©˜KéiKL›¼&Å$‰åe%J ~R˜òE'/*SJæ¬)?ኳÄñ»¼B*[C‹ËdUÙ ).F±¾ì ¾0åQÓlÒ…©d¼¸¡W6Q!ÃjyŒÖ”åd¦Ašð¼Þl®t»ëõú’·€õ9(Ç‹žöôäüL€ôS©¨Üˆiu>^E)mbÒ19íŠbàc–¢$UEOZ9k,¶ÒG«•Ínâp¹xiµ3jäÚÙx3cå-Ãç?=17¯m?ç‚Ÿ¸à™p0†x5WÝHçº4E)š×™´ˆ¦xx«{xýðÅÛ—/m†õª) $E åÂôÆl˜cì…lïJ¶}šÌi&>¬_ºüŒ0ósAœ4-“JUrù†m噘ì÷⡈85 ¹œt^ÔrBš#,q“3w$€üAtÊϸ@æÞBAbZmA(çpúƒAàiOˆ!8;BÄ£tŠ6Zbzàˆˆ~ÆéðQG)]2ªŒÖDHFhi•b-äöÇe;aWeÑ@!2ŠŠ.Ï:‘g&|A܈2™7ñ/¿6ùçON_˜D¢¢,›Šš„a†P–â endstream endobj 28 0 obj <>stream +4A¦ °€FD**Æ(Eˆ+bÚá@¦\(kuíα”ÉAqp$¦ë~œƒ“³š‰ÚE½¼³Fç9)Qí®Lú gfS~‹·¸Ì2—\• +Ìg))_]<$ÍÆTD²ãEa&í‚Õ0ê¼~fÒ 2¢lR©Fù‚3|¸îATòO ·§ƒìtP˜ áX’3ëR²ÅåxºIIðZ-I3L'qµJi5Z¯ +©.o7)9Ei¥@,åF5/wGõP,$ŽˆÄ„5­@JiF¯hÙ¾’j‹F)Š±-ªMË™ ‡oÞ‡PÖ»BábÍNûÉggBONùgB|ˆ)Ðf× ñS^|Ê3îÉÒ `®Ãa”çÔT± ÇT6^žŒñù‘ð–‘žš·¿àZL«X•‹¨\s"† }˜*gjviÒ‰;ƒÌ$(µD[-‘p@ŠÕ)µFf.ÈMºc0W´ë{|r3ÚA:}ÁÇ9½4ÃÛŒ”–•+baU­m¥:ûb~ä§SóŽ™ÅîÎçÀ¥ÅÔ@+l˜•¹¸u!¬<5==‡¸àñ¢¡õÔ̹9˜2:j}ߨïò©%gX~f"à ñs>îkg|gæ“a‹ja‡·—œˆ>–Ï:ðY/1V0*?; ÎܹÉÀ¼— :¯•i!55ëŸw…¡¨ÁÆï³aa&ÀÍE$i»0óÏŸœ +k)Iéáæââa€O!j)ÌgI½L›u1Ù+-Ìc¢¢RŽËô‚|œë¥Ik!×=Ù¾õžÙÜ~ÖC8P+–Z•Êdb ŠT/K7ˆ?½ó“VÎRæ€KH£M`H‹Xqcº0±Ì&”Â0f6CLRJ/ØõmR-1z=ª”ˆDOo]Rê{RiK-j±¼|“ˆ7=¨Léãí´˜T7‚B1,y€-‹—û»÷‚lò¼w@RT*{q3Äe!©ä#LѨ•{‡1«3î%®·ƒB%"5 }É~2ኪSj&@x1¦–Icógu‡XF«ajÕK˜àþãµm0G°Ú‚Õ¦9¦£r äšÕp©àŽŽ›l‡˜TT©„…òdD=ã `ÖvF˜ .Ä ‹>2A¨uT*c2œ,"”pµÆÙý™ ïÅ­@,æÊþX‘Æ«lÎ8ÐsóQÖ¿²œ ÑNX„”rX©F¤ +c÷H½f’Ê©=QæQÝÏ„ÒE*5Š[BnóÙ9|ÒC£b‰PË.4 Ó4åŽÚ@œÃtÚO¸ˆŸR‹Brу™<ä\DEÔ&n,ðFà çf¡¯óN:P î0 m'¤z1Ý KaÒ˜qD¬ÀåS}6=¤RKdr)"”žŽFõªÕãù¥­“GéîE'òi?˜ßÜÈŽq;÷Dgüe›âPÎæ1i"ÌÎEõ¨ÒL/\KŸ3;Ç‘qÛX7˜ôˆÉ¬F亟̓ñç@×ØôÀ‰Æˆ³äÊ!ŸßÆõq 2?eƒ ŒŽ2\ˆŒÇk|f`ÔwìæUßQ‹«zae¸ÿ7p¼föN«[÷Û{/Ö·ï[ÝKÙÁUFoš™~o÷®“ˆØ-&³¯¦×SýëñúN€¯9¥Ì6¦6"bÄ'n÷£Ù^Y:|Dèõg\¨7äâf¢s5^¿‚¨]_,ëYC§!çFwT˜r3aÙÕB” ñ/™2C¹°dM_Ìô³TïÁ4€ÚäFÞXÒ +ÕÁ%>ÙÏ{ÁOù$×`¥M$–Pµ +DÌ ñ¼Yg]\©zQ ˆ^Ìê+ÅÍxe;^Þœ ”Þ‰Y‹D¼ ®å"3ˆ~Öúq[ίÃ|ᜠõÇQ­FÚ}"±Õóˆ2Þ5µ`ó@Á†„"—]Eõ^Ž='nSV_-nL@XÊ~2 ’ü~.µu„°}¸Å&Έ6å¡Ç{ȦZÞŠª­5å!gƒ"È; {Oœ™ÿÚYדs˜‹HÇÌ.5ÄÔú¬—žr¤Ò¤ì!,7™ðQ6H·˜ÑlÚ !>…j>·äçl\+V‡Ah¶´â(»pÀ&Û³ˆDX­¨t’ŠŽXÖ+4ü\9¯K¹¡‹ƒá¥ì>Ÿ[Í,Þìî?.®>ïe2€CìÌ`aýº“JÌœÀ“¾J'GzëXïžP‰6o–µò(ª×(« B”ЛT¢«V¶äÂRòBvatåqïÒÃqÛ¨ú.] %ï…öÎÂî½ Ašˆ×ìö¥äÂìòs‰…S2µäç‹r~ÔÚ~!ÑÜs@šZXëí¼P[¿UÝ>b²£ —…ÿ´k-"ƒ€Ñ¦57T×_L-=¯ÔÃL.H'€nãV󌗞 ‰ˆPaíU6½.—.êõ‹~J¥ÌºRÙá [Tz%³t£¼þü¸«Oz9»p%–añ +™Xà2KB~°—çÑñb7)¹ÐZ»ó$ƒ¸â’}£yL%—±ä0"7p<á§ÒÄt“i.³ªUwæÂÂô¸û–¤S!.¯TvCL6Âf`©„½Xr˜î]«nߪE%ו.ÕÙA¯¬1ÉvÌjV#38‘ +#ÂhDã 1¿aw¯Ú½«Bn’+3QÙÏ&¹Ì’òˆR%¬>a"J xqx=?8º-f—€ŽÅìTkÅRC®°B[îúÕáÑ‹lzT1p.—\È.^)_|È7äL¯3:zéë_f{Θw¬öi~x»¼z×nïë0tb>"ä½´V +Θ[ÊêóÝG©…SÂê’z37¸ŠêÍ Ÿ§RÃdÿªXÜ"¥´)æ—ŸqB³\)ô´Ò€Ëô=t.$ÖéÄøûöK{÷‡΂´ ñ¸Ú*n7/>âËG°ÖƤâõ?MÖ·]Q×­­ç¯½òɵW?ë½Bg–}±4gu^|ó !Õ?ïã|±“^7/Vîõ÷_Oõ® b®6? °v¶bÕ/Ö–O7/¿Ü¾xOʽÖ]¾\âjÙª¬m]}cíÊ[½ý—[»/¦ú'¬™ß;}°uòŠ2Ý„Õê1»b Þ8dsëˆR·J£Ã{ï*…%Šzõ"—ßÊ;©Åë+×ßm_|¨æVV^®mÞŽš5©¶Eå6s‘Émiµãtÿ:nvIµpxûíúÊÍñjG:QêÖD,–’õ½\çpxñz}õZDmaãm©\ %™PÚÚƱl²¶^Y±Ú{Zm3Õ»¤”·‰ø1[ J·<¤…éµxc×ê^¶z×r£Ý+lj  ½›×Nû5\7Ø­n§º'ñÊ–˜é‚Š¹öÆÍîÆM­Ô~N.ȹa²¾¾~üÊâÅ»®¨ÆZúêÖ=^ðn÷I£³}ú²]ZòÂâxÅkï ÑÜ‘³K|z5³QëB@ŒÅk~Â(®/]ýzuÿ­sl´õæf.Múe:Þ¬/]ÆåŠ7ʇHÝ5`®€ S›^ÔzvñD5RkùHû¬+æ€ BëáZÏns +Š>0Úa.ˆ¥Ýh<*Æ ä!ÁCY±À³VZ24^õá*)çJÉ…e£¾E™-àà€ýÕ‹TzñÙ9@8­lÿr¦=–„Åü `r!_Z8¦&f ÃËõ­{`ü»»/wö^ÍnቖŸ¶JËW  ÅØMLšÓ1+kV}׫ÆRR--ÛHÖ—µò +Èn4^§ây«¼,¤ºü$¦TÀ)^ÚðÐ:^ŠJiK+­É…ÔX½²sã닧oˆåM¥¼iU±Û´VYX» + ÌçSƒë‰Þ µ~h,ܤ³[ Šf‚™YØ8yÑnl<9…¹ÂاwŽ¥ò¦”_‰ç‡ÅÖö£·>½ýÊ7çñ„›JÁJ‹ËoU·.^û°°ñØÕƒ»¯}øýTs{5¢Öl.#æ0³ô\ûÒÛfûÈ‹ërnQÌ.þ©»oÀj˜ËZSêtø`¸ÿ¼^Zn®ˆé¦Õ ½å¡R³ˆ +* Ïtç˜MtpdQµRY¼Ü\»­–6!¡„ŠÅdmCË/ÂB>–[×Z—ôö¡RÝ5Ôƒê^4®WÖÇœô’sa‘«dzU©(Õ=D.‡™T®¿ÛÙ¹ªÉ5Ë°RÓ—âÅu-?bæl€‰PŠ^…b™i¿¢èœ›™ ©ÁXƬì§Û×Èx+QY·@¡¦#,9 ý‚_pÂiô”âvˆÎ`&ÍÈ;ª#F‹·H{à‹nʳ6¥W棯èõm¥¼.åÖÙƒ°Ñt Š\ZasKó¨€]j Z èkäü°ê"žídŽ´Ê:“^D͆O…¼VÞU>À¤Ãlš4š1£iG‰Úš˜[œÅ(Ÿ’s½ £R’±šÀË$Ú‡@`QµË%€‘”µ€­9˜%õ‚Z[Ëöë÷Š+·gaÁ qœ^. Oƾ÷ˆÀ”F¦wdû‡€‘0±H[= Õç<8È¥°Òß¹GÌAý?<ï%…ÔâS“~ƒk|ºkW– ­õÕý;FaÑM$ØÔ +ŸÙ`í%`s€eËu)=¬®v7oyÉ„—J ¥]cá–T=`RKlf‘ªÀ°÷·î¤êÀþ»ÉdD,ÛÍ£ÎÅ—ÊË×2í=«²\ÄËo~Vœz™‚\ÝÑ;ã¶ê‰Þ±ZÚÅR­Áá‡ù›ãÅv" +™X”K»‰Î©^»(¤û¼Ù\Ùy~ýô1,—}L2 –£Z›Mqc!"Ö‚tVË,ÔF'Tð³%ŸƒÇ¶Â”’ÝÜjnÜ P&&èxCÊ€§¸ÑX¾ +óÅ™ €yŠ7Ú+7®ðì,vÁÃZË÷¯Õ=`ýtAI.Ò˜ ±˜Z’SB{‹·[çܱ³nz&¢Ùä—Òá˜Æš5³¶V_½ZY½®U×ÆJ*щ…¹ˆ¤ ĦåÂjet«½óˆL­9Ñä/§fúr~èÄãç}Ô5ë_eÓ«\ac½˜VDõ(D'}dX]«r1Ý:1kGÀzÈ´\ÚÂõ&¦Ï{ ÑÎG‡//E˜/`{s}`¦Ö¼±”@•/öíò:ÐƘÕäók‰ÖaeíŽVÛw`öŒŸ—Òàd1—p£Ì0†J~%·p¢æ–ƒ„Ñ]®t¶ÿÔÉY Ð6ðYåÕ[õçÃl¨Ÿ3¢À± D¥g¢ÑfÃ’ŸNÅÌ6g5Œl¯&ccV_«º‘ø¬—šœzÃ"ÆXQµ˵ Wˆpy9¿Ä$Ààãý•™Áu`ǘԢ–=°ÂËX­†¥2¨V1£+Õ¹0S j~‘K¶¤4¸íž‡.ÌE€+O¤ê»\º?ˆy1%DÙó*±ÄR€/Í„ÐçÞ=à”Ÿ™Ž€"p…±º•áíÒð•\yÆ›Iˆ\›ò3Žˆ4ƒ¤è‚3Zja-DR)è…E˜KøIÕCèA6O™9·Ì¦˜{PÅ,0›„ÕCôÎ)ÀE1³ëÇMBÌçúG\Á‰€…BlnÜï+,,¬œVÚg‘j^Ë÷íæfqtÙní΄dàA5DÙΞ™ŠNzXD®(¥L{DtÇTR-®±öÂS‚gg!7$›Z­Ë\qŸËSóâ +*¦¢b6ĦÆ*¤Ôü\ÑIe@øaJÕñZqE«\ÅT)m£ñ†‡NÍcætЖ—Ð@uKöN3ƒ«\zL´YDµ +¦äÓÍíìÂ%_ÌôP†—6A}_<|yýÚÛŵ;±ìÈU€ Uk;>:¤Ø'v‰™ápï~iñPÌöËc0+: à¨Ú¿?Ѻ”ÝЂñÉt}ULµ!>d² ¨¢J±ÁÃá:çÄ`¡€i LÝsQö4$øI3YÛâÌÖ3Ó¾§§Þ]ŽÔÍ\oëô埿€ ‡¤j˜/Hù ¹´êÌ„‡ +RiÆ\@å*×{|ú9õôÓã¶i°”÷`º7ªˆ¡TÃ\øÓ Î ‹õáQ¼¸4H´Õ1ê{R~•¶àÇ|ìS“¡³óQB*œ™‰D[-®dW7ï|R߾ϥgg#¾0k_R=™@*çÂ@i( 21á&Æ y„ÂlXü©-øWˆNb2B +„dãr>08ÜÄ_œóþÙ3®iw ¢’„”Ÿ Q“^,H%æ D BmZÇÚççq˜2¡˜>é„ÎOCæ ˜54æsã^z—Ϭ°êŒÆ£Jd'HD©zG)lª•ÝXª? ÉÏÌEÝÑx0–„øLXÈÑö‚añzˆIÌû§ â©•€¯´Çïh^È-ßô0¹ /"DVÍpñ|L/áñ2p¹þå¥Ëo%‡—!³å£Ó´^'€xb +ªÕÓÝ“õËïŒNÞÍ,Ý ð^Hì6euBlÄ®wÑø‚Y¿$fÖü”í Ѭ^cÌ6&Ç;P© ;‘Q½mwN„ì(ª“í‹b~è&L lþX†²ñú‘\v&Ak¹Îöó ¾ûùlDk ö ¢5Ñx³¸tj7¶#lR²[ÉΨÚtr„ƒY–«”Þ°«¥þ%ˆM"|V+nøTò#:ÑÐ9PÚλHBŒZŽÕ჋òéå¨PéÀsUÎ.“ZmÆÏz13Ì—èÔH©ì˜õýŸZ¢í|ÿ¦Õ|T2Ì—§#qðDNH‚°És³RÊ©é.cT¥ì0Äç&̤‹IY^¯ÏÍ©lbWê_ôá–+jœsç(­7¢byÂó“I?…Äñ¾c~*9nüî +VUË Ahý_ã^øXñÜ1¤9‚⤗>3›¥aeé4H&Ÿ¼L{AÍÊ…I{>Àž›GŸ˜ðÍxwD Ò©gÆý97,˜ UH®DÆè3ð}S!UªÀ>ä:@|nÇò£Ù¨6’Ãlщóa:ÂXÉÆnïàµôâmToMŽ'²R†X Ó1«Åå–“Bõ.``ˆ‰‡I!ˆñ› ôiÔÃHe‡Xj)&ÑÓ†UÆ5½¸ÜݸÝÚºÏ䶜ˆÅì¨õ`ª0¥ì +“X #/Fß5œ!ŽÑÊQ.1á‚\ˆˆ)57bFØ—‚\1iTHÛõµ—vEµ¨\%â äŽã­c–`Þ)%gÕ6H£1Kd¢ŸYºÖؾ?~#Ù;E• +ð8éòȪ®ÎÁªv¬æqgëù¥ƒ—*ëwäÒš‘U»ÕÛ¸iUVç!a&ÄÏyÛ´ÑƸDZÓž¨h5³#­a¶ÔÀkˆXÑ"±PШÉêvª}H˜=4Þ +2¹©æ1Ê–¸Ê,ú>éÀ8Ù ¬€™˜G¢´‰²&ÂFq%fõ€{ò¢zT,͇¥§g¤º£ú¤'À-\®Á|98î)­‚²å#ì)/ jÁ<¢Î€pêà¢`”@šÈ™E)»ìŒšÿòY-26é磆5]ص-Þê¸ÀY!)@$Ý€èp{ÒIœwŒ÷9ÜÕ/D”¯M†Î»PGˆ64{ñòÅÁÞ« „²éÑtˆÒDÅ«¸RFnÔ6ðz'™þíñ~CÏ%Ú¬Õ +i‰hâ¼OŒ^¶¾ ܇•B´òÀÁ¯©Åõ™pl ÿÿì½×$éšÞw/h.„5s¦»«*+½ ï½ÈÈHï}–·]ÕÞÎô¸3{ÎîÙsv¹»Â¤H!BÒ…t¥ÿA’ôD’ à…5ˆþ&»¦*+ÍgÞ÷yODT~‚?ð&·ÁôV +¦·»[\·zñöü‰ØÚ1öDŽ¶fï’wúÈ dußÚ½GÇà†à%kb¨ÇëpöLm§{¶¶ÉöCrôƒ=|ªF«"­?*³ŒÙµû×5±MÈ]=Þ…ã»{Òš]Ç“KècöÍî ðFkۣǣoG?»Ó˜ÛªVyûþë¿™œ­£È†+ž3îBm ŽßjñR æã³맿Þ<þn~ùÁ^Î,ÑšÞN_‡Ãó"c“j».%b°ÕÚ'%.ÊÔŒ,iÇãËÅå×”–”Ó±êZýËãgqòòoœÁm‰ ÕpñôÃßž<ùSÚ)ñêfkù²µ~LïÒ«·9»í¿b¼Éi×õ9нî•âŒ oØìm/_ÿY8¿«ÃÊ›;Ú™3ÞºµyWãb]‹糓oÏe.Ú+É™šÎØ3ˆ?B+GÚœ5h-Ÿ/¾Gôêí)XוnQ|®ôä¢.õŒø(\¿éœçÁC¹3FíVÙ°HºNóh¸~V$´¯²ô~QQýµâ/i½ËÝ"[´lè=ðFªêÉztúnõø‡ÕÝë'¿L¶/@‰J´v‡—{ܦ—?ž½ý‡Íó¿ßþÄNý¡–,8èMîâí·ÖôM¸ún~õg›Û_Çó§uµ)ù#gp*E3ÑŸ€%xoŒ¯¯ßÿý“Ÿþ5jJ‰óH9²Ú+TR³á¯_‰ÉÉðâ—‹û¿Ð:¼=Ü\¾ë®îHkØ0 mÈ{pЧó«Ÿ'צħ˜sZ¿:£'ðh5­K›#ÖõŽÞ¼ùvÀ™Ù9Þa½h{‚\ÐZGvú•‹gÆš«WëÇ¿¼þöää"Kû%>½$³[JƒÑî:ÛöÑ×þÙÅûкgeÞIÎÎßÍÎß +v¿ƒ¥O¯]ù!9ùQŽA5‡„žîŸ2¼,RNŽ°#BzäðUïôg­uÌYý"mÑjì´×„°fÂ;П¾Õ>·:—zkÇÛ#ˆŒÝ=c¬~%5^‹ª’RPg}gw·‡UÕŽ·½õ«áɇÖæ5gOY­-™¯³ò; ÞŒ­x9Ù½ì^N¾¶7|´;¨ÛÚ’ÓKªFEÊ‚¦ÉÁ*݆Ó'‚3Ë“îÂhÆ«ùÙ%g棲^QÚðIëâÏ'w¿ŽÆWïf—«›ïá¿Ì +9Ò+J]¾yjM_·–ïBs¿(ø£‹g~PR¾m®_±æY,x#%Zø£óh|ævvg.º}*ý(­a{þ¤üÖ_*ÑHÖ Ø5c êpÌ×ðBj.œÁe´xáLž"ý6lÈ„JNÜñm¼~ Cè…a´ûçfû¸»}a¯ŠJ§ÈGr°H–O¼Áyoózzûk­¥tΘ`YQÛ$æ!9š›’д{×îø™Ö¾‚ ظ®ö +,ød$zÐE…u(ª> ÇOþ­3¸FæÖ䎭YgV’ÚRëDí^ùÓçpp”=)KQ–TBF¼„ðJá\í]ëýô?Ù½+IÍýšV›XnΉáÒ\« ôùå÷½£÷îèRr†°cjk _VUûy6$Ôn0ºAu¼e¶a¤½BBèË`öª$¶jÉ(y»µª0fúiÕf~fõ.`ˆe®É¹s8ŽºÚ.²®íüÉÓÑ埂­“©iuÖ¿zòËñæ §·b“Ô:‚5­>nBQðôâå?þËÿÍM>ª1’ÑìßónZ¼Sã¬æôÚjokBD©½ +“QZ´’Q±!P¢#½weônøà¸@Ú%Ò¬±¶ÏÁäŠr‰š‹ôŠ¬öî½3~–ç’¯r2˜Ílî¢áM•“Ù]0}¦µÎI-µ]È&-œÚ£öò‰ÍÓ?±»¤œúÑúýóæì¾³~Œ¯œÞ±Óß0v7Ý>õµh¤±û§j°(b¦Ä5sæïÕx“) eB·Ú›š”è­3ÞÛ”ÅnUî þÊê_ÓÎ$f ïÔîµÑ¿­ƒ ãaC+‹o8£+YÝhr Èñ/Œác&\•¤Ä%xSHtŒ³ÇÑìY÷ø»Îæíù«¿îl^#$ÜxÎJ¬]`Úž©­ÓdõúèéoÑSR(—T’ºhüÄè^ª@÷Ùs>¡$Ùº^#ȸÌàig"D[0¼7zÒZ¾–š» +/ë!DAÂÞ覹x^ ‘®G«§š‡0°ëœ¨ñA‘´J”G‰aÜYO×÷«ÓçeÚ,Vz@ÏŸ»ÃÇU©“#ݯ²­¶Ú)£Þåj†âÏEo$U¢ë $=ÊÒŠ34ÓÝô¡Õ:„”4øå3¤ÖË6£¹]\~Ë{ãšÔć²c®6ñ5KxýÅã?ÿ‡ÿµ3»{TT2õ@ × “†ÐzxŒ²+¹D5:†¸RüIú9½ÉÊœÚc Asz•¬^zã»ô‹–€ygHi-Bm¢FHÁ DD}ÙËÝôOÞ/>ôOÞ؃³ôCØ¢y]ŽÛ„œ¨Ñ oâŽnR€‘Nj‡÷f£«?mýÐÜ~,ÓݤpŠ–£Š¸hÛ믣Å;gpç ïôÎyú±U…U¯{ÔxÍ^kí+¼2T4œ¿J㪹ˆÇ7€Ö]*kÊÝJÍÓæüE<{NJ-Õî_<ûQO¶@&µ{#uÒë£Ù l&etò´åtO“Ç$lB°¬¦>È 1^·_\ÑKárJ£,•ùD +¶Íé3»wž‰-03ãLÄ<0¯ˆaž ¥æQÿäÛöæ•äMßüüO¯ßý)>á¼µÔ:nXã‚×ÕÒ³*B ›þô‰ÖN?Ä’S»(µ(a™ªiÆG¬ÞG ÖP wñø.ÝjÑ¡ž%Œ[Û‡‡U!_Ó +tÀùô¦À€$*½ƒŠ¶¾ü€zš©ÛyÂE k1°­98QÃõ/rüÂÄèRí)ÿ°jâ+¡ìäÌï_ðæ SS‹´Sd|P™ÖÜpÖ¤!uiµ3XÞn¯¿ÀèXï<$模÷){Z3û”“¥ìšØ4““dùÌéŸ î°¡B]ÝïÊÞÅ«ú}kóµkñNWe1~TQ ‹P{ !XZœ‚„݃µQ“c¹¹Ñ“nU­“¥œ2"B8)Ñ\ÈÑÖjŸ#ÝáŠoãÕË`v¯Ä;ΛB‘e +8£<íRd¶¶v÷ÁÆ™}ÅK?³»zŠFR”TÃ% +¥WoŒx‡šˆßjÍ5\fµwúc¸|‹°lΞÂ,¤‡»› ø÷ªÔdÝIkõ:\¼’ã-œx]ëPFCëÉÑF».FÈ£º2¬kàöy‰oe (CØ^>–£% +h…oÙƒ»pñÖŸ¾ˆæ¨•§yÆ:¡ÒUåV–uµäÂê]Gë—³Ûô`È“Ö;­áÉ¿ýW/~f1nåι”œp~z @4½'•®â“ÕÓ"@Ó-Ñ•{Ó×ÍÕ×@#+Xœ\ÿÈj]$lb.uªÚ(Ù}³}ú·vPâ$³Ïý +Âo"Þò”wPwhkÌ_Aùsu›ÕQ£˜UÙ› `Õ¤6\p4>8ù¶Ìu¡…oN½þ-‚™VzÀ­Ú½SÆèý"Ç•˜µÆvçÌëŸñ–’{‡U§@5µø„²FZçJŠOqâãŠÖC¨¤‡£ãÕ;Gé7ZgÁä¹lhc ¸#5^{ƒëÑé÷ñê ë¬HcV×&Œµ(rq ªuYg$…+ø_>´¯ºw!êZ›7( Œ5¡ñ.¬]áÒ @Y½-8£»ógÇçßwŽ¾ÛðÎH çF4íž-®¾EeçÂ5Ãíªþ”5@©/U(FÐWƒ!gõ8ù¡Âê2íèá<_;ÉNög€-XÃèQ¸z æi¨-=^"/‚É­–îevrÖZ|MÛ#øq;øo¢Õ+kp§&¨„Ò…=„0àêäÜè=F2òþÌ]•ääH`<{ª·ŽP§ +´ Œ‡Ó_Üü¼¼ýëNI½ÓY¿@RÐFTPä‚Š!fÖ÷?Ÿ¾ÿ;wr+ËÑÙ DÂÔÔvUî@ܲ¤§‹`t^ùè+ÓÍ¡šÇp¨ y6È&«÷dož«© ;ØC.ØÊ­ó`þLŠ µlCGrá‰%Ú3;Wjr-G¼»¶»×Z¸ÈÖ”BÃ"åÎ^I‚H*“íûxóþæûþþ¯þ÷ÞöÃ^I#„˜[Ùº‰rœ'ÒËìåp3<þf|þÑ>.2Áöæ'·EjÃß**Ò¥Ü^¼úíŸýÓÿ£WeÏ(k*EÇñò­Ù¹Dáø¸[Çwg¬=5ÐÔcäa¤Ç4Œö<> ©]‘{¨,Îð¹;|Z‘ºàêºÜS‹œþ5Ê«Ú\óAã„1¢eg÷Úìãõç uHë}ÉIÎòÅ›½½ñ¦×éÕ¿ó×R½},¹cÍé[ÑX¦Œ“ž»éîÞ´Ïc@)‰ÝÚÑ¢.¹Ù*]ã-Bnºƒ›ÉåJ¼©+I‰ñ5fEóï×ù@t'Îવ},_²î¸”n²Ü—z"ÇkÖ_‚sœÉcH¥?¹…vÕµ¶Ù?kïÞ6—/¥æ ’nÔì\ûRPá»{Þ=úNi‰ÑÒè_äÄä«‚@I-;ÙÖ8¯Dêðãx_(ÿø›h +§ rëvÏà²Y»§¡´y­žŒ;}ó——ßü}’FÝñÉ“_;½Ô:ãÇXhÑßÈÞJ –u)B²˜ÉÖéƒâJŒŠ£HÞJð—„Ö{˜ckZ¦a映È7I@»Ö%0Ãá.Y¿i•oúÃ{)„K“J»Âú{z¿È!lDg‰EÆuN>œ}øÇùÓߎ.ÿÄŸ<+síy‰ÑRÐÚ/)”ÜF} §÷ãÓïüÁnPå‘q§Ï~ïÀf„9Õg¯Ï~ûîwÿ槿ý7¯~üûšÚaaÄü¥è¯ŒÎeŽÀÌþàÊî]Ý31ZUÄVk•QL­™ÜÜBR'UµÍú3ò£Ó'ôq‡ÖX ×r¸ ô.ÐWpÆñèz~þAXUºr¸,KM ‘è­•èLÏC<ÄÐIîŠAÛ¼© Ï\|%Fº±F‡Pš„–ž­ ô©fáÖî^¢þî×Õ¡ÊÞ˜·”ÙÃ-,dsñ¦wòFT¢ìlEÒº9Jwšçbk«v/¬Á­Þ9«kI‘µŒî±;y<¹þ“þårrÊzP¶~%Þ£ÑöØ>^þjtù+ÈE¦¦i—w&Œ™n_îbœ…Ö»å½%¬.ÔþQ/zCð˴Ŧç7R´eœ)XÎœÒvÏHÖÝ£o£å›0ÝøòX‰çðÑ”=tz'ÕÓš˜ƒéS{x Ö¢´A‰Z•÷Qý ±ùð¦Ô¶Ý91Z' }œcÂãçH }¯ª‚L(½Q¢Ñ¹Tã¬Kž²sTvªj%ÝL¹S¥ÜæLñ·Þ஦tÍÞ¹3¼Fe‡9Ýk˜‡dĺ[£µmN.›“‹\Ó8@ÑIJæ(/K|°0z§fï2œ<¡^ƒå‚Ù‹püØiïT{Xa\Òš!¶AÔ%±•cܪrÛéž%ë7­Ý×æô)U`1èôÚ€†Ü0Ã'ZïFm_`!P—´5FŽTÄøã'ŽNTÔVwŠÕ¬KЇ‹Ž$‚ËýP™DdJ)ÙNÎ~¼øð?LžþŽ÷ÖxRïñÎXF„St»—^ëûµ1¼æ`a@¿RC õœ™àÍwcõž8ã×áü5ç- +,BF‚Ž>´×ïÍÞ aÚWzrÃbÄëCʆñ¿ëž|Ïø›šÖ‡Eµ >›7þð‚Ñ[œ3´{gGOÿüòÝßé½ÓªÞ©¨±Ñ92ºU¥CB‚U´|o¿µ‡OZÿ &—›1™ô +^c‚´UÚ×Btس[›2ãŠÎfýøûîÙ7rûš¶ÐÏÒÚz¼¦ô6¡Â΄nÿbrõÓôæçéõÏŒ;è‚‹JLN‡u½Dã1}«w˜ý2ëVÕa!°9kø¨(d*…Ù>ïŸ~SÓ{„™…1”[”WÒKz¼<åî•uRí i×I¹+:s5N¯|HÏ!òI]AýšËWé~ͼEgó.Ù~ã oI5ßf­ÀXŠ;m°Á£¼PÛõËË÷¿íŸ¾â"”¤ôÔ9 +b0zŒÒ †7û7ý³·AÙsÎÛ +JoUQ:9ÆËÑ®àL†»wë»ßl_ümïâç’ØÎ’ð.¡öQÝ(gʆsp( æÏa:€dHŸ,å#,9gîžÕ°”E.DõÉ3nå÷zõño= TÀTÉŸûƒËÖü + +¡á»»›÷n©:&5@à*˜ß ɱØ>‰ÖoÖ¯þ>Þ~¨?H«@”;Éi4y.7Œþ5Æ‚ÅuºçÀ6p~™Ã¬†èU¼|n.ÑÁ_sîÊé]*Ѫa I{¦·/F—?ÎÿÄìßrÞîX¦vwÏ®gïÌáUïôÛùݯæ÷¿Ñº×\dJ{ù¢òŸÑÎZ8#iÒ)±&m`õ¯F—¿<}ÿöôD ­VöD´x©ÄÓhykžHÑ9ï¥GH(c§Œ¥!6%wh÷OlÎð1 _Oôè¨ÌF™º^šŒ>¬ +¨}=$&J†Ù=ך' í½²üÕ!e„éARëúÀè]v¶oF§ßšíã2krO.[Óûyâ{[ä¸[Á]¡½YÒ”ÃôßݧÛ.'—¤>­ð§s—]â½’ÜÛÊ^¢üU¥^žôÊ +§vôp*3Æ›+ýÛñíoŽ^þMgóÞJN‘JNïtqû'¨›Ì¤U²{댟 ”¤‘ Ø“îâ~°y>8z‡¹-rm¼rEì xFY‰åÎ^&»÷îü €ñ(Àø¦/(AÛ—œ¿Ã÷YÆ+ñiôÒ¿_X<V¯£Õk#9†—‡øcž± +FûÜì^Á›ÉÎh¡ªÎjj¬wOgßl_üÅùwÿÜßçØ8Qfl'^¨Á´!ÇeÞG­ñ§ÏÂé §wkhBOÊ ¬ÚZFšæŸØàúizƒ;S’3Ø7wxÃxK¸-½÷Øì^¨áBfFk.îûçïÏßþÍÉÛ¿nîÞÓ •œs!x1<²úwéΞ“;6\U4À¶Í]p¥×ëÞqÍ#€Ÿ“'vÿ +õTm­f×ߤÇI¤.ne©#7OaNicD©\]aŒ$™?Žf÷îø ë,êr¿*"8ÇÞ«|“n…cƒñckp#D[RgIhš ø±ÂMCŠÓí~ŒaŒÈ?zLîalËbˆ´¿ó:§𨎉òFWÑæ­=y©ÆÇuDfI)s ÐhJ»¡ô¬îÕ¹–ü­ßOÏAFŠ´ÇCP´®"&´·Fqa´¾ õxx‡ =ŒŒºÖ?û>Ý(áFrœ=lÍnV×H)†¢VÕiOÅÖIEä¹fM4´®9¬ÕR +º”à Æ\Ö”Þ"CY¬?çà íNëVŸw­Ý·‹ÿ}rþ“m‘PtÂF“§£³Ÿ­ñK><®é£Jzíʘ±úéó­ÔSL#XNkíðJ%!‚Í—Âukþñ5°*'³ËoGç¯Þš çœ{c ï9»¯Ó²”x×J/ˆ +LPb£*ߪpg¦§ø`l´fë'¿t&7BÞ_AI «U)±jrR×z¨àé¥_ÃþBoŸ5´6l#cv¬ÎÖí¯dØSÚç‹»¿ —ïÐ[Þ™Õ„ÀèžòÁ¬®w ³ïÏïN^þvóäÏš“+=˜ˆÖx?¼ø^h!¡êê6Çè^/~h.î*|Pã"Z!8‘­bë´ª ‘,9Ê’ƒY8º Œ®šœKñ…Ù{¼zü[ˆ‰è/y{Z¤} jræ韙9:—ãK«wƒ yPŠŒ«6· ÷<Ûdýuçô›æÉÝ“%{m¯¬¢|À|IѺaŽ)ðpëÔ=EØËþ +âyPÓPhPUaýꈱáO*VjVº5!ýÐƉþºH‡y«Iˆ‰mÒ¿Ýð& ¥½_U!³Ð[«}ªDkÖÖÅVgþ”qgY1¬½RúgÝ>:‘£³hú°'s ‘­ËBç «&½ð&\>gƒÈSníÔälxš~y¸}×0&>nè}"ý@ƒçNÑC½÷ľԻ7xÚèÊ>´tÍ^ì^üuk÷Në]4Ônžõ±´K•Å¸¯? Î~v§·dúÑø„_5×u£—c¬šÚµGéU þäaI:¨éÈA" ÒSÖ¨j …hãNŸñî€psRÛU¡Ã9SÎàéb´k®Þ±Á6C{e$õøq0?è /ì.ô¤í$ëxr£&GZç´(¶3œW R,¯ÊQ÷ÂéÍúþÇó·¿k®ž…Q—·sÖY=OÏcZ“`|/^¦×¨o³”õe¦^åC¥¹EÖÔÍ‘>xÚÜþпLwà’ÃU]Nö*J]j"Hu5©uX7÷+7 ‹Ù9M/T{ÝõË—4ؾ2±OùX„7¼q{çxk%ÞÖ~ÝœçÄ~ŽëÁiïäûdù”TÛ¤’„óûݳßÜ|ø§íÍ[,S¦ag ~ÆÈî]A4òl¤¶Nžüüo§OþJJÎÀ깆£5{›÷ÈÙÿ²k/–oþqóò¯ÜÁú‰ŠƒyPš«2ß, m ‚AÊ~zþ«ÿ9œ¿Ñºçpµ9ªéCš2þNO‘;´9 FWj<¯ +N +η9cawn§—¢öÎ +RM®O^üŽvg|¸bÃEÅ¢(یޕ­Ò]¡¾Ø<œýz|õÛpñš±Æ@ VïÙÉ._Ûƒ³’Ò?ä:”µt/Ú›íÁ“,å•…8ý£È£ï¢Õ{:X¤¸À²;«É­ƒº–©k´Þg¯ZÛZç¢$FûuƒP»¸ÔÍšÒS:ç­Ý7»—ÿ0½ÿ­9~RV0Žä-‹L”©Ù…ôbõeM½ÉóëïþÅøê×U} µ6íÍ›xýÞêßPÖ0LJ9>Òš»`xCj©xÒÖT Ö­å«xõ–´Wéqó£„ Æ“Ö0Ç° ¼·¼yj…ŸÔVúÁé‡?àÇ"ãÃÅk€¥ö뎥x댟¶¿k®^×äîaÝÎv•o–h§.„f«êaÍ`Œ¡Û»²û×p9Ò@l4Ç7€ùlÝÒZËçåÍ_ík9äSkŸò©µÏùÔÚç|jíó@>µöy ŸZûäSkŸò©µÏùÔÚç|jíó@>µöy ŸZûäSkŸò©µÏùÔÚç|jíó@>µöy ŸZûäSkŸò©µÏùÔÚç|jíó@>µöy ŸZûäSkŸò©µÏùÔÚç|jíó@>µöy ŸZûäSkŸò©µÏùÔÚç|jíó@>µöy ŸZûäSkŸò©µÏùÔÚîÿü¯¤u2ÿ•´ÿ¶PðïN½£G_Ì¢/ +v“ßâçÁÑógϾè|QØ’ö³ÞÕÉ‹«û»£go3 +˃³£ÛLù÷ÌàA™î³«‹«;Ü9<9º=«dêx¨Š_P:C}üoöö‹†(J,ÁŠ"#04ÍsRF”$ŽàXžEVfx)óøÿñAœD0”,Š‚„ß ™ÛôAüÇûdŠ’xbð ñã³hV¤%Žæþ_ôŸ½ÝíIŸn¿¸übúÅÝý/äL¹’™M¿ÿGßaÞ¤tÞœg/Ÿ_öŽ^`Jî~§s†9ùOî.ïf>Þ›Á݆*˜%ÿf¯¿xùŸN•‰ñÍ5îz¡©L;³\S™Óô¡ƒ´ã2Ÿv–ÿý¤q! +¿ÿ9ùf?þ?ùÿÏþøø;¼óÓÿðÞtúÞã!¸º=S~ÿíÇÑü»àÀØчð Ò;{uur6̬óÅw6>Èï¿â§“Ëó¿Ÿz>³Î0‘éÓ/ÿ‹žöçß?éßÿ_Ê°è#-~ì.—IŸH}Q(|ìaÎJFÀÊÑ_Ìšÿ kkr«¡µo*sѲք4¦|°“âcÞ[0Æð÷»˜Û35ÞIÑŠ0úU%©Êé¾ Œ=âì1©$Š?Ñ[kwpnôÎäÖ‰sáNm_Õ”vžÔò ¹Î{¼3T“­Ù¿Òú×rûœknö $6Þ‘–¬is¨'çîà6˜>¢mÎÄÖ)ã.j¯"4g&„)Ú¸ã[Òe('Ý‚Ú¡óe.¤´¾‘‡Ó'zï\h®+J‹²„ÙáƒQQ +8âMïƒå›æÑ÷úèžrgU½[Q|×Ú½÷—/¼éÓæêÖ»fÂUYI„`ɺӆÖ-ðA² 9¢õDôF¼7ÉÓ6Þ‹±¦e¹]‘;5­ÇzstŒs§r¸âÜmÏ[ó×þøå,Kr;Ç.¬)‰Ý=Ó[»ŠVø@ 7zëXpU!)1¡´µpÕ^<Ç;RZWöWEÚ?¨jU¡U—; ¹SWzb¸¢#Îß°Þ*Ë%>n(=JÒúPò×x©†Üe­YUjç3ÖPû5¹]“-Úñ1e J?Ï5Uµ½ª–%íiÓj›7æPh–—”ãl]}Xâ³”U"<½*uXkÎcBí—Ø«Àû‹ºÒÎÖµ*h?Û°¥Ç»+ÖZˆÞFi«ÍíäèM4½ªð.©ÕøÄ^ií£2–)¼E—±0];Ê™—Äv‰‹Doáö/íö ­t¼þ….jZ§$F¤Þ—›ÇJ|"Ø#§½*2v¶ÅtŸ×[%¹âÂc>:á¼eº7¹5¬pmöÍþe´~eï´.ÖqS5°Lã›×Ý?~M=RïÝëÖöö–¶u}PMwmÕ¤¸"F1݃„¶'øUC5´a ²dº­`Uj¦Û,3ÂÐÎÌè^6Wï‡'ßnŸþŠtºUµ©µO¼ù‹öéþò ß:壬/žýåüöçº5, ‘Ù»éŸýo¾å›§|ó˜0†˜É†Ò¡´^üãÎ7Fº£§œ`!2u=Û0Ómnf›ö sL˜˜öœv'¥›!ñá®"§»áVÄVEŒK\º¹ZMl²fŸP“Š–¿Æ5ëø huØ=‹'÷ˆ«ÃŠZ Ú¾W’‹Œß@9K):¡í5e̵èˆϋlÐÛ¤Öçœ9b€40ü©Ù½Bï=¨ˆ¿(rˆL%X[É™ŸjçQY.R~MH?œýA=¬Ê´œîJô׬>8¨ªè©vs¤U`\Ì@Uêf©¨"$ˆRš­3Òè—§&D‡˜„ºA©}ô4œ¼ðGÏåpï?üú_=ûåÔôÎyºaäÑ;1Þ•øècõjR»ÀùéÇ|óM¤€Ù=5’#ÎHæøäþ×fï¤(FÊâ­Isù.X¼áìc´‹”óq«§SÆžæ¸nL¸ðÈêÝDóçZ|Dȱ•ìâÕssxÎ…3>\c}åÖ©;º]Ýüä .)£'º3gxgŸòáQCB©0E”Þ«Éq†Ð󴃃¸1öœÐ§¼»¡Í)ï¤_â}Æè þ¸®wH{쎟Œ¯ÿböø7ƒËov—÷ÆfïTœ«ƒ ®yäN_OLV¯æçßÆ˧•t— žÕ»„v© Ú[sÑ“¾ìH 7év°¤“£½íÓVºßI™ öÊržthcˆHÎ Íœ”ÔÌiowgŽžQÞ4KÛ˜g½{]â,å=ª™%¾‰•­ +a]nA¦ðµ¡´k|³Ð°²5#×° u¡eÅGJ°ÊÔtîß/+™ªvPSËBLÛ3­}%5/äèÌîÞYíëªØœYº=ž>$´çng­w®£ùkÆ™>( ™†DÅáœ$R€nW¡xbûAž犔EJ g¯(kQ“zu±#¸[=ÜB+Tš%Í éçÙcN0X,(én ½Ïè]RíìWä2ãÑÚ@ö×xgÍeoå CO¾Ãÿa¦®´6Áä1¬ݳæu}DšãëCù+çq&„5¬¥Û¤ ìx·¹þâ€jH[#)Ý‚å”´¦YÖ/pAº—RrìôÏÓ@š+Æ›±ÁŠ²g¼·"´~… ÂÑuÿôk{xá/ŒÁ.Õö‰Ñ¿ÄW•×š$³§Fïä¶òŒGi#­uÆ: ÚB2ŽŠ|T–â*ÊŸøfEêKÁ±Þ¾’#Tíi 9gŠ‚%GK¥µ“šé~îì…1º¥¼™7¹÷Æ7J²³z'B²n®ß®ï~×?û‘q§E±YäŒ ŠºW×sŒg¶/“Í·ÑüMºÕYs›îùªBÆÇè@ñ)§˜îÛݯ+]Æc¤¨z5cDØKÚÙJÑ…7}¨§{ßkÝ«<’n–ô9{ƤŸwßj¨mJAgÈtN¢LE­0A™ñ „uX7¥Ëš“6ì镘2ò  äH»¦t¥p[[¶Yd›%5“°!Òí²›’ÔÂo)k*„'PuHzxRPdvUJu±ÅêCJíí••ååÖ£‚ؘF$,¤¯*v%gö/ÜÎ1B:Kº&ÊÓ^™oÒöBI{ã§Vï¢"¦º—k‡um¯Èí—eÞõ—/ôö9é, +鸴ªè@ŸE…B©u®q?aN)s¬ÇÇŠ?­ ¢ãÎ¥øÔê?öWr¸ ´Ž“œ O ôaUŠëJ‡÷çé~oà…ä2;Ñü a öI«®âÅ×jt$C–{çÀdr÷g¿ûŸÎÞý®n÷êF‡on•6‚íI¼xÕÞ¼£t/ +¤XMêÐÖBŽ/þ3£ÿTïܪí äŽâNÌîyIî¢X—¥.eNŒö…Ö¹”“óhòäüùo‡ÇïŠRĹswtŸl¿NŽ¾MŽ>˜ƒËppýãŸþK{vqÈù(µ”½¤œ…Ú<6['r°J)… °Ü|ºÏè "uq« +—.…l5ÇÊ\žÂp: °–9ËÓaUìêØjªñúa]Ë^M)É­Ù¿7º·ró„4'ˆ@–&O‡5%GèEÎ笩ä.%wA)ý +—¹•¥@ZU¡‰Ú—%œƒºU Ã ´êR»,4Ic¿¦|yHÔ´<é•Y”Ñ6Â5Ýx[’µ÷Z–qÐ=­ž\ŠÁ–µç˜vd4h“_bèÌ~EÉÖ­lÝ®r «ŽË4È'åÎAUþªÀ>*)([é^GxÁjºc%Ø“Ú%ÚG%"âVŽO*‚·f 0v÷ "=̳5ÏêRÖaí5Ñ]NŽ¿Ë‘ÐÌ>˜ÚÀ ¬¹–\7oãù[À•âôÚ“«hvÏ+ÖS€vol·ÁœÉö]YHôæJ U9Á+ ÿð1l?„ó§éšÛý ¬²Q²göহ|>9ÿöë_ýó»{ Å[µ{©v¯`äÖ±Ñ¿æ‚ ø BTÔ«wú’Âc „Ÿn*vµ¹üivýsIîUä>ë.ÕæNON$BrŽï›7fç´®÷xoÎ{馤þôEkûµÞ»ð'ßüôÏ¢õÓ}ÒqȦäÄEE$CjJßç(‡”“tšø:ÏCm¬9ç®6Xˆiæ£H‡ÀÁÛ@6[‹×é^õ qBh÷qº *µ·AÝæ¼.÷Æ^EÌ6´"mW…ˆ³Æ:`À[ÉîR êrUA•íÆS`B$%G89Ò©K ä±È…‡ }¿(Ô„Vžðö+F¦näð7„MžõÓý†ÍqEJÐgÞ]Âk ÌðtÖ‹î¢ÈFÊ-@…ý¯ò  +GØEÒE•|T AU¨^9ýŠ>Tø&⇷f(OV÷’кÀ{Æž²ævNéý"å"VÑIFJ²ß0ƒ¬3ý¸/{;K©%„Hšc¬…_´ÖßöN²z×’;ÕýÉêü›áé{”?!X‰áJo£«Îú¥Þ=~˜­§;Û…ã'Þè¹7zaöî­áSÖ_BÐgÖÙ¼(Š!hœsí‡ñÅŸÌÿéäñ/åÞ1Ìæèì{wú´¬ Ö„õ–Þäi´| ³¦&çè’ÖÚ¥ûVJmHïνñ}459ÿ @^Ѧ´»¡@îT +VR„ ¼4:笻@õÄÄÊÑØO[còdrõ³3}bnZ›—„9Ì1mM~p"YÖ-‹1hA¶éfØ|œ©›²= G7´=Îjc/ BR°kðCÌ$îdBB?Xg%~3>‡ [{5#C8%±…·À5±W—F9*Ý ä“©iÀxÀÖš·¦fó¸5}®„Çe¡]Dí;Œ9eŒ ¨•[€üÆ¢YÿH¿x‹2PÚ HGe&ªp(‹°Æ0‰£Rú˜®là˜vD‚‘œJ~J¡U1æì©‘œaJ7GXEÚkHÝ\ÃAT@Çu¹† %lH¨Ù~UCñ-2nÙ´ÔÁ‘êâ¨cÔ¾l²´»W‘÷ŠÂAI© +1ʱmPv3¤…Ik(ñ~™û£ý•î}5A.ˆ.ôs«ÆÇjë˜vÆ(ñŒÖ3Ãõp÷Ö^¢ê .\Sδl¶—¢n·Òc“§´1Bò*@#wÙÐÇ¡Cc)Üøƒ­sœç=Æ +ÁÂèœÊñVNŽ)I8Soòdvó+wz{ÅE;Ö_éÝ {ôDï?VÛW°Ûx|EN]ˆšÒV“3køLéÜÒá1ž€·ó\Hè¹¹eœ1Ì2XBJê»wÏX£2ç«ñ–ƒƒ³F”;£¼ýI>NwÕӣ „=,H‘’œ[ã—ú襊Èñ–U¥S[PK{x“çØvºåì’ 9É’6­ö9Ô)ÓÐäæÎ_¼2‡wÈvÉBüGzr¬u/Šr²×0½P?dæŠwf´9ÂZ€x1ðGpñy +‹»]š¬ÖC>bű:ˆ`JK,R{HéãºÔ‡¦íUÔ¼¥VUlþ~§½ a#x`çTûU=K˜+Ý° ¯¤tR‡åÎP‹‹´ ãŸ#̆Ԭð~¶®¨îÐlmQ©ót£üýª Ðb´a‰qqX£äD°ÆX{®yˆÀ œ2ëKÞÚìÞ–„æʘ£Á™BsÒ-N•Î^IÎÔŒíÿq¶¾Wáó” ÃKký"i”$$EžtéTm†NçÂ{P×–Ä/óLªŸb“±†J´:Lõµ$oT¤½µWhìé +ëËÞÒ‚nÈQ6ÜÖ*ݳ–‡º†)ïYÃë­ïˆR´HÐ;¤UD´4kbÌêýÁî¥}Èhýš€åheIT"«µyfjf‘ +â¿Û£]òçéWk49ÿFfËÊ!ab!¤–¬Û›÷»urñöŸÐz¶)µËBÁÒÍã—-£}ܟߟ½ùK¹}tH¹%>‚äì¹;¼Ÿßþ®}üå¤ÔäLîªz·,'?!Úšý›Ööoñ¢9¸|öáïÜéõ—E¾H ^ø8„¢Ñ¾âý ªô¾ª€Á ¸{¹Õ>·º¨¤‚1Z\ÿÀƒ/suŒ‘´FP1\̯þdzû+¹}â´vëÛ +‚û‹ñ°À²æ=A}ñºW(XHÖ„L胪T ÆG_Œh§LD”ç ÎË2(KÃm¿ +k2ÆT‹Î`µà¦£Â_äÙ¯rô£":áBuÖò¶«jœ!PFS=)3Ü™Þc»µ‰ÆçÚúxX¬»ï–îáM¸U&à¡“#<8½§€1ä5º@9i!KߢÝß¼díᣲˆÚ‡t ÔÀ LœGñFGføfKY×su¯œ#ÜåÓv÷¿Ê±v®*±R+×ÐöJªgZO¹6¡¢é½Ý=Δ8Fô½YçÜG%1Gºœ=6ZG°íÒÇ}SkL9 Lp#8Ÿ·Gj´„¤ÖMà""„1º˜XÔMΆƒP=ïM!ò¤ÒjÈMälYlRæÎ×JŽ<ÑçLTÌ5ïLáYX{L}Rï wwßýƒÙ¿(Àæ«=£yl÷¯ãõ+¡™jNºSrká]Þdz{[ÃH·o¢ÍâôýÏû¿ŒÏÞí×4Ñ릇Ð)GðO­þ£sí/ß4¼uCê›ñ) +zŒ¡uñ¾ t°“3¸*‰Á^E‚ +ñX7¬)í )gD[ƒéêÙ¯ÿæßV•èA¡ÔV0º`ÌAžv÷kÆÇ#¥#wZC(­º’jç|›Ö»Øïœ=ÿå?«jíGU•P»0¶¤Ò#Õ>Ê_‰ ƒÁùíÛ¿žœø2Ç<ÌÓÙšD oHÛº 'HèCÄ'aôÁuà¨"ׄS;¬JˆBLÌæ‘×=ƒi†²Q1¡“ËrDˆUgª8S3ÚàŒÞúg¨ž{§,]¢,BˆaÙä`MnÆHó\CÏz‘tò >nÍŒfV¼‚¥#-‹{>[70¢Š?GÙJ¬hMìøZý‡Ê{ºL;¬¹äTÉTä +å`]KÂAE&dp`çcl;„>FJ>(‰uÔ_—Ñûœ¸Úƒ$fjigQ— “ÒJmkÁ²5}ªvÏigÒP;tbŒˆÔ»*)Xêíc)„Y[“ÆÀ\ù³;ÎpÎD Œb7õúg|¸8dÃ,@9açQmQ4sŒÍÚ#)˜cžµîyñެ"LJusd÷¯Ú»wÑîkc|Gzó¢¹KR3Î8YÆa½9@uWR|AØKwøÔ<\9·®´ªB€y+±q™‡ûK`ó¬ó¨ÂU¤ö+<~Žî£Å³hr‘̯ó¤…â¢ÈîQ™É‘&¦ÂL.äèX6U¹ 'û“§ÍÆÇS¨Rg´ ÇÏâÝûC6%.$^6Sæ31S•°JsmöÏÅpS‘’šIˆƒ²_`±Èë²Ø©Ê=Á۹×”=ÍÔõã TaJºT¤.‹7Ð,zCéHk¿ˆº¬¬_¡=Fí6‡Wk7ð²R³˜Úº±]–ÚŒ5F6Ñj÷A–ÈVTRhÕøt›ç cMÍ5 ±~IZ“œ§ØFå ¢âLâÒ#艄’‚‡&SUª©q®@HåŽ_hØ–¿yTD62e%}w¡Ãø¨$eæazd#òRO°—`• +H7_Ghu+j=Üqœ‡E­IN•pU“À®m-^ÁB«ëú°®O)gLÞ´~ð—¯)sÈ;HGEŠÃÀÈ Ð­©Tí,a1*ÌΪ,†’?Uƒ…Ò<f/ÑÒž6ë/9 =D^ë£Wîê;cü ¿ª)=ÊšÒ.FÝÏó6€Gt—Í·Fï†2g˜hÚÁ”¢oû%1=Ä­ŽÄàX ŽÈˆ0ØÜ¥Ì>Î «O§Ú5'µ1İʧÉHª›nYð` ¥èXí\Ñö¼žž)†Ûð^z(#‚ ãí¥ÔrçïŒÑsÒ]`±{fîœé „"köÕdˤç×Îkj7Ïø µ‡(‡[”',"x¯¡*R¯ ï&@îbR…Çß ÞbË8p¬ýôÒÐ&U±•­keÒÕü%cu¡oeÎWZGRœÖë÷Ñ8w®íÁ]%UN¿‘ž ìÒæÞÎ4O‡™†Eú²î£"ãŸç–t0uµôxε²@‡r®` /²­"—(ÍsoøÂî=Ü…â¯)½»WáOsŒÇZ#ÆèIÞÔLΫ¬O +lJü£“«²›^G«…ЂØVù°Ñ;Ùôè +h—3¦ ±Ë(#„ÓìUþða9[U¿8dþhŸÌTÁxéLXŽôJÿ n~UhcJ¨BÐúÌï;7ˆX·uLHí¯ò܃ƒ@­°%÷X}Æ(h *ãâX¢<§wÔ;l˜°ŸHd„YŽ„YF} PLÓ}²ånCé4,à,V¹€Ù œ½²Z¤<%8²ú×ö8wÁ»Ë:Œ9i<ª)¤Ãë þ¼Ì… µ[“Ó +Uy¤\ ®ì­Ìæ‰Ö:Ê4¤ºÒö@ˆ–l˜ž*R¢­Õ9W;— #=¥BjÝ’à’F–4X µ/DÇt¸k(££ão7Ê aŽöÒ#jψY4•d?Ž¨=½Ë£ÓMÚ^«ÍÖš°6ú¹©©}ÞšÞ¼üËË׿«Ê­ƒšyЀ9½ Àˆû—¬Ö©³žèŒ³ŒýU…ß' Á[9ýèªÑ½ªŠMÉEˆN÷k2¨"vI}ÒFä4ÒãB6)%pýû5=Ïxè3<¾ê­oÃè³2—€ÃI©oh°fyÂãÌ)‘zÌV‰ke¨°"ô:Óª7Ì5$R‰s´]dÌÚ°_f‘ΔkÖÀ +óLÀO9E4¥¨ÊÒAUCH¨Ñ®–J¨Ðàј‹ör!ó2íÿññàÍTS2ÿê-ÔB* N•ÁW¯.uåè´® +l³¦ôK|+K…„—£„ªƒÛ¹BÙ‚SÀˆPL³5ÖòQž+ÔÔ*¥ýH§DûTz…ƒwÐ@}÷YµC«,Ð~݀Ĵ˜mãݦ}ˆ¡ô 5™º¼_’³u+­ÑpÄ¢af‘O +>°J[3!ÜæX˜‘£†2fÀ–ûx"þ4=£åW5¹"µÕx§FkèIh!+B»&u¡ó”fZQäcBŸÈÍS£sÙÝ}°gO3 Ð4TãcxíGeé«ÿ°*¹þF»È6)}‚<'”¹<é‚Ö(cèOîf—ß NÞ®R ¹a4øèŽlOD½/C=\îc(·*wÊ|z^ñ£Ç;£¹‘ÃåïFåH›±&„ÖEäÃFí•ì” +3f'GÑì>Ó0ëÐLº ðËÔí¬±þAEËvžÀ*› +,²6”ÖƒÕEÖÚP “U–›)8ÓšÜN§]J% à ºKÅ­á@H‹N“xºÑÏrÁJ? +à +!Ê4 8}¯ÍŸº£Ç }TB˜qØÏ2g×äkªªGáðžóæ9>Èv<}&úÈkÉžOŒð`FZŒ1‚áíaEN`Yot§Ì1Ê.Œdž Š\„„ y< ôƒš’=2‘låpSƒu,Ñ6 ‰@}Î[ØÃ+³{Θ 1ØAå¾Ê3U‰” 檂 [àáª2)“{0ﲿÀ´PÆTò·¢5K- ç#C(¹º™­›yÒGjÀŒwf'çß|¸MÃrX’ƒªŽ:•%‚V%Dl¼_Ñ2eu¿¤d*j¾aí•”GE9ßpj\¤KD,»‘^Mg=̳‡U0ÀSäHk¯"H3_Sê³$(^±Ze›Éäguþh¿ðå^¹„îUÕlÍÌÖ,¤sžòWðh_î—3eÖ ž@Wimm—£# düAYËÒéµUùÞa¿¬”Òـݥ'¤èêÇ©ý:ë?È2é¥eo!N¯ï;b¸RZ;Þ›H½Ï8 ¹u,Ç»r6Þýs­{šgÜPb $¬=Å-=EiîãY +^'.IÍ2H^ëá·Zë˜wÓÓ—Š?·’ÙÙé­cÆ]ŠþRôæz¼¥Œ6Ü¢?º•‚lï̲¬È‚™{ËËŸäpÑ"Rkgh'Í:,‹]Ι«ñØ°gÜEo=ªÛûu$/âEì)Ô›Û;s9>¢œñ/ŠÜÃ" oBéCÞ]üß$½‡—×.øG¼};c[S³s#çP…BU¡ª€J( +¡sNsnv“M²IŠIIIT£8 +c%Ë–4–³%[Ù¶,Ëq<3ïíÌž³g÷í98}š ºpÃ÷ûêÞ ®ê<1¦ pxì(¯\Ø” [®@ÕL¤— p! é±q[?€÷ŒÛ9]ÿk¼´ÆE$AY é½ÇÆ!']@ýˆÀê}bïS,„q£aÆ#(—s󅯸!òÓŽŽ8Ç,ܸ-4j i=ý¥&ÃvÔ †}ã„õ²¯®€ Bª²¡jôNÑIÄF žãc®q øÎa'øáq›llòÈ£Žÿñõÿpçè‘a»¾/Ê<̾ٛ1ƒ¯›8‹…jd´ç ·«62eîS–2êuX ˜jÀDZhˆ‡:|ÂLM¸‚£v8¼ß)_®i£R&oôzÔÎŒXH>×¢!è8`ÀæK’rÝ+UÀZHÕƤ&<áv~ÐT0¹J=9 +!¤Á— ›¨¤‰N#Á²7\·0)³ˆW6â! "êªEí_-È·ö'¨H%:N±„GZ´Úu ˆ˜±ûã)/„ò2*#BÎ+dÔæ&" ^ÊÍå …˜ ls‰y›ÓÓ‘Ú* MãQ¾hpåON2©W±*&cÂDƒGÔ ’3## B©{BUWéwG,Ûü)+“ÚtK <>CªÓtbÖ«´=\Nmî’ñ©!§`¥ÓT|†Rg½‘n™)U…Æ]<”Ä8³û¢|zZ*.±©i>= œ<ê–`œl: f‡ [ü}GgôM¸%ðÀ6z4d!bN&KEºˆX²RTR©Ûý "8Øœ‹ËAáxä: —™c“D´adGŒÔ¸KrÐðú¢GnÀO „Wú"­X}‹ÍκB:1Å$¡;Sh¨Æegµ èr0Q!Ó •µ­qKFo´ÿ 0ƒ°Œ yƒ'x Å1`ŠU_€w÷ʵþö2øÔ ’y¥šR^“ËkÖ@¨ªú€³3&,ä•ÊBiAª¬&º'«kWÄþ˲v6cö¥†¬ @)G ç“ëÑÒŸ[DCuøLÚà¬TÜ‘•*H3ñŸaÔŽ7˜³ûA)œ\ Ø^Û_¢–Àà _¼å<ôïnË&â[•JîPÍnúâÝ`qQ®®`¡&e˜DÓ§¶HµÅ¤a+þTÏXò.£÷†­Œê`û–ŒŠwÅÌB~ê\~æ,Ÿ™Æ•†S(:¹,®JË-Ré•Põ”Wi° ‹¢弡2È1©NÙ„²•+q…5"\Ç‚ Sè5Ÿò(m$ÔD‚uNíä§ÏbJcÀ應ÕóÅ&‘`µ?Úl®¿hÙŸnœ@ø+iöŠx¨à‹”©®X\b’S¡`òA¥TF!c Ù—l¡‚¿šp‰v&Ý·‘å+²êS} 9,®ò…E>7.'›‚ +2úâ66…kX¸.–]0YžPyØJPð–ý%@פÒ¦å2³Jm«²|·TYCB%ÚÜ•+kñÉÓêÔi±´H(5¬–ÜÊMïEëf_ÜÈ RÅ%<᮹ٴ”œ4bŠÞöËh°âÊþät¬¹ª®Ñ±v´ºÄÄc.PU—k|fV,¯Š•5›Ó ²/\×ò€12k‘šXXJÏ”ׯsÅ•`n¶¹rnŒ»"X¸‡Gz„“¸¨vÏÐÉ9;¦õgZ<&æØÌ´OíùÓsj÷t¨¶ ب­DeѶ¼¿H4zãn®âSg¹â†Ò8å 7¥ìœ…R!ÐY)(‚Ï­Åš§›÷Íz¼±x¾¹zN´½áJ°´(–—¨dW*-äfÎWÖ®Ó©I £š}‡?î‹6Èh;Rߊ·vü©ÉÖÆ=ñÎŽ…O[ü R©úÕV°¼âË,ú «žx‹4áŸzL²àð·@,áÖ)¹³jŸ’[§ØÌ|nò›jãá—Ÿ÷©Sd|’NL‡Š‹‰Þ^°´ ð¢u DŠ“Ê àtb2TÙŒ4víp*!ôggL*ð¹ùp}C®m¨Í“ù©³v& W@¥*ð-n2ê4ÀC©nG›§øܪ#4ÓÀ òþÄ$_Xð©]<Ò cmàT®˜iD†¬ñ®PX†V»§wí±ïÈÕUç“íHm:’ìÎÍ/­]Kuw‹gº{·P!“›Ü®­_•;Db*5sPÛ¾­ôÎSÑŠmyÃy#!£¡*™˜b ËüB¤¹mŸd’­dw5X]""u*ÞvðZm’]¢—Ÿ½*mXèÌð™I4˜Å¤"ð0Ð&t™Ï/Ç›§Ôöž_­²j —ËL²'•—ÅÂb´¶žîlÎìÞh¬\¶ôˆy˜¸Tw¯8wììF;RyÃ+7Ë“gÊ õX +ˆ·÷"õÍ`yµ¸p©¾tõ䕧ÒÝ]ó©“tj`µ@&üÙmµ{Á+ÍF˜Pœ”Ÿ¡m¡0Ç$»|f*^ßÀÃuP1¨|07k¬‡Š ¤\t2qPÃÔäY_´ÌÏ…JKh°ÌÏ+õµ@vÆÆ—©DWm¯Ë¥y˜‘k+t¼.å§2S{0ølºo®»÷©0ãPœ=W]¿žœ9×7…ÜœGªƒ¿U+3'ç ?.$Зsååó™ù åµ{ ¬1ñÈ°Çõ(„&½”[¸;3sI,o‰µm±¸Ì¦zÑÒB¢¾¨q³ãnÂB““3Ígç=r 3©ÆZ0;cðÆðX-®Ñ™¾òr™éa§ÔFY}ŠÍ'0ê$Ð&´-ÖÙ´Oƒ\â=œ@ª®TçBåE!? ¬Rín<òÂ;0>zoK´J ÷À£¸p€ÇæÓõ•›·Ÿ¿ûÙïÙýjaîluýFvá2Ôoqéjiù­NnŸ½}å±×˜TsÀâÃ$¨ˆ9¥¾YX8ß;yo²·W›Ù{ðé7"ÍuÀ@²µ¥ÔÖ"ÍíÎæõ»žš>û,Ÿ]­t¶óçP1ï•ûZL«SRi-3s¡uòÁésO%'ÏN­žoÌí!Bf-ž"£M_¬%åç:;·Û§ó‹,¸¹â$½¡’G*È}i™³Ë©ÉÓXÁƒIœ¾"c-*ÞÌ´w.ÜûBqæ”M%§/Å:gåêFqù +T +žXgèò§…ä,Tº ÉŽTœ¯¯^jo]ÏÍ rÃÅæ³³7^Bùd0ÓÉNžÊLóFÛÐxa)X\ö'{\j2èL¸ü˜‹H=TZ¢¨-ݵ~éÉìäéé•óSë±`þ\©­æ‹ –:¸ÿ¥¸0¹¸×íç6à%,t¸k¹z:1u99u!oO¯œÙØ¿.ªe›/„+u<>M§„Üreõ>6»hÄ:ÞÊ«€B®‚íñgaÜäâÂéÏLï\ÕzDàO;“ 7v¥Ú–/> ^V×NÝwé¾üJÃ#¤êªPY—j;ùÅ«`‡t@æ>PsÁˆ°¾p5œŠ7¶Ò³”ö6¿êð%Š=J)hÔPi1ÑÜÊt6Óí•ÒÌ.¸ýA+NʹXm;V?)—×AÅÈXM­-¯} ³sÝB*ÑúH€P\JÍœ5¶éÔ mnîß»w÷¾hù¸™$­@zšRÛ|a6=½®®´æ÷_þöOsS{¤”o¯\š?÷xyófsóÆìþCåÕ›„ÜÜ>}k÷®'AÝÀoÀ›Fë[áÊZ¬±™;¯öNãJmõÔöò9'— +WV"­m.·ÀçÔÖn{ûvfþj 9Së¯ËÑ"àÀà¡RÍ!T털N¸´$gZÉꢕŠJ…„ixÃ5!3—™>Îìeræ<¥‹MÑš](8¥aTA¤ºTXÓ ¢ Š©.­g§NçÏ +¥%‡P¶Óµ´zúús¾H1\š-/]Löö™Ì.—ˆpÙHÛ¹\¸¼Æ%§Lxe©Xƒª‰wüñf¸¸èSjéÝæü¾GÌ¥Ú›¥Å󅹃üì™Òì©l{‹Ö®ßÿÌ›ï~Ðݼ:îmtá b~Yªœ –·˜äŒƒRÏ^~ä‰ÞŽæ{®@JíìG;gÕÞAvþîüÒ " +Ofjóç•ÊʈÍ?fç !*M`òÕÜô™ƒ‡^ífÍxuIë\\“σ¯ˆV×gwnöÖ®x…\ª±.ÏÛÙ¤…J"|ÅŸ˜W6•òº —y¥*$ºV* +3Jª3˜ÒA¸k<܈CØh0×ËOïu·oÈu—9a¡Í„…” èHUÊ/‡jÛ‰ÞneùÒäéÛLz +áRñ~Ô-€öMî=T[»mnGkkÉÖ6£¶tÞà„›÷ÅÚV:f££|n`š[˜;“>ID+VŸÈªU©0«46½ýh{ÏëÚ©¸¨6€ @¦fãmO iñýJ-ÙÛµ·ýiP“ wn>)f')µ…JÜ*˜\±P‘q7oòHk{·èD÷Îq—xÄø'øœ@ÎL%@â…d3Q_™pbÕåæÆ­äÔ]ôJqæR|r5ÕÚÚæÕgœ|fø«ýMRß]‹N]ã²K#v¿ŽF+K¡`¦23ç + —ºë·:«×cµ©¸BŠÙ¥½›éöú˜+vXò¥Î3©y¥±Ë¤¦ÜllaëR4?m§TL‚Q]ð§æˆHÛŸœSjÛ‚Ú©ÍÈ*¿2i.6iÄe*TV«K©Æª3° @Q£'uÙ a¨´,ffBéi1Ö‚±Êww#Í"Ö¬jäuž0$<Ô¨-]%£ Gr E2ÚJ÷öS½SðÈtOCÊçf6Ï?äò…¡I¡ú“™Ãß8y»¶rI.,:èÄî¥Gj³»ãý@ñ'â§<Ž°„'ûW@Äu°¼¦4v¼JcÌÁ™pã3|¼:dÆ´®"ÝR 2…“…ŒVÇõ iÁ8´H5©6؈ED´%ØDÇ'eS¥¹öúe¡0‹óH°€ËU:Ö%#½@fÐÓàŒ”ÕöŽÚÞŽwwÝáªÁ§ÐJEÊM¼¼œnΟ~púô#±ö)Ri˜ š¡Ì ÅÁŸpOX4`-ªãK´Ùì´‰‰º¸D~æ̦›Ïss©ÞÙÌô~¬¾J)%W :æôÉ {à„Þ5d¢™î^¼µ—˜ºèÏ/2©žÞÃùÂåXuïÎØy˜#'dZIµÖ“í  ÷ˆœ:P7ã²[*Ù¥$t7—%ÃU*R×!B¼¶U[¹.VÀ®äº'ùÌœÖ#±¡âÊÞ A­Û)6· ×wÓ3—Jó×üÉY§P¶0 ŸTXß»)§:fLVJKµ…‹©ÖI©° ”>?˜ì{§¡"xµÓÞ¸•¹$¤çi¥EGÛˆP„ž†Á• Éa³×IÇøôT ÑËõNU—¯x„Ò8Xn>qõ¾ç᪕\l•ñøÞ¥ìÔAmñŠÖPÒÚò]H°¨CÄo#eCÿV`ÐêØ°°Þh-^‡0®GX&õW=c U¯Ü:a ÿñ¸Éè S‘îÑ Ç€wÀy‚÷@ùÁ¦§W.óÅé /kòÉf_T…©h—¤#Ã:ÄIF¨{|n‘‰CÀLY|1“sRQ¯t3áP¶KÅ[x´Éææ=Xµ-%Zs—ëKgˆ‡r3Å©ýÒÜ…peƒR'½JÝBÅ`<#…y˜,T,†+Û0#…™ƒ©ûº›7=ÁT"ß^Ü»'\š›@Äþ­CoÄÉæ„ô\0· иDw môŠãVÂÃ% ƒTV®æ&OC¼šÞ}¸gÂÃv&拶@ Àæáb Ätˆ2Ÿî¡BÎHÙ™”‹+°©Yµ¾]˜>À„‚ÎÍÙð0kj]~#ÊCÕSà*éø”‡Í›ÉÅÄaÊlý§ +T¬—îî×–®•¯FjnnÈ„éÔ„ƒ4zP@ˆ3¡Âb(Õ¥‚y:\Ðyþ”?Õ³SÞ`ÑI'L + dŒN¶ÒXÞ8xHïá êÇM‡Wbä*m™=¡î $^ýî{ÍéíA­ÓŒ°l®°È¥—ä†\Xí¡CH^¾Ä€¦2â!7—ôJ¹€Rš\>'×f&0ÚPüÉ¥Ö<2ÌH +ir±AµËÄúZf¡sÂY‰0ð!³'˜ƒA›Ýº­-ùÔºSÌZü1%“¡¬R]$ t0Z+Ïû”¢Þ+ž°n¡@/¬ +©iÊJ-\Ý*ÍËLžTÊsd¤¤÷ +í……ý{°`êNÛÉ•¼J—ŽõH©ÿñ¯ ‚e•‹k˜”ÒÚ½¤˜ˆ·6À„4W/çR-¼.ªµÍýë‹û·Ì„•Nb¡<.9±y4Â$²Ý'Cõ˜è4‘pËéÊÕ“ÑÆ)+“±t8 ö åZ¨VT@ÙŒ'Aý)“[µŽêV‡t¤Ž‹à.tˆˆKe¨GH©cv:Tœ•*ËþÌ´“MBh2¢"#—’-'Ô£C0Y*Ò +‚ÊT7øÄT Œ³û¢‘ââq‚p‰€ÚR[[Ð6»?¥q4v‚(Ay¦§ +s²½}¹¸âb Z4 ÈqÔ ;apûáÈk•Õk™©.5`>®ÇL„äRn!á³t¸!¥çäÌÉç*â $ŽiÝÇÆì“Ç`÷¹eÂB hÐ#îsë»×CÑò‰Q‡Ë…êCù¼‡+ôï|‘!#ã©dçPá2£V¯Î5æ“Ído-;³®4¦•úT¦·ÐÞ<3¹{¾¸¼K$@S\²Í€›wÜL|Ø‚ëÝŒ“ ’ÁT Va“52’óÅòábçä•Gšr¥çàc®`’OUS½å@yr…׋±\—‹W† n7£8™è¸Ë¯@Æ3s&†ãr¹Ç¤ªL²ä’TOt*›_–ÛsV>bc2R·Á ¹˜#£Æ ; ÖÄ.ÝÝÂ1;Áxx…VKNVÆå/ R’KUË3«µ]:]1ú‚t´éW{6R·ù™ŽŽX´vZÎÍŠi09I£G{&•W¨ä®YýIÐe+# ™*Š£”Æ˺ƒ©xsÓÁ¤FŒÄ±a³ÅÉx|Š‡ŽZ± Öés0,HBÖNOCRá°Òüî•Îæy&Y>fqºý¸ Ä¤Š³w 0³W²‘‘a£÷«L¬ú”¶˜gÕI<˜›ß½Ú]?«T§l~eЊéqŽŒW9?áñ»a¥²ariƒG²3¹tç\yî2k 6Ü~”UÅL¢W*¡’O.B]ǚ˱Ö2›¨HÙN(¿.®ðÉé!­çO|ã„Éͨ\¤®µwhØ4?)–•ÕHa ä†ôÞ —Å+zÄÌ7ƬwŒYŽhœn®(·¥Ì²ÇŸ°ú©p™Õx¡Ñ[Ú™Û8»}áÆ•Ÿº÷é—žzíÛoýð§~òû?ÿí?>ýâoßýɇ7ŸÊNnaR¡¿EÝès“‘Xª•Ê·2•©|{©:µ4½¶»yîʵ‡žºðÀS§ï{líÒÍ“×nÜÿÔ]7Ÿxåw^øö;Sëû³çãÕLLÚÈ¥éhábL(‘¬v›³+Ó«Ûëg/œ¹qÿÝ>uûùW÷î}|ëêCxöñ—¿õê÷Þýö»ï¿þƒ_<øü·zçˆPÊŒ Zéðòb´JÕÈ JÊSKÝõÓ•üä\mq½¹º7¿w×ýO¼ð³÷?úè7¿ÿæ÷~|êÚƒå© )ÕÔÙ5:–SÝDeÙA†GL^œ‹—¦Ö3ÝåhuF*v#µ™¹Ýkû÷þÝkßÿéÉ+·›ó§ÔÒ¬‡Kƒ]Ô»Ø ;âŸ6¸¹ c'"Rz*ÙÞå»R¾µ~pý¡~íþgÿåÂý^yè¹ó7;{ÏÃWyîµw~ñØ ¯Ýuëáµ ÷zÄ´ÎÉhí>7¦•*Ÿ˜ôKrºÃ*…P²™m.Íí\lLÏ­ïßµ}ñÆ™k÷¿ôÆwûåß~÷Ç¿}ñÇ¿~ðéç<ýÍxaòè„û¸Æk#T^‰ä×ÆÌ̈Þcr1ŸFüi“'dF­ LTI7·Îï^¾íüÍó·»ïÉWN]-ÍžMwvp!slùÚNÉåúÌìòÉíSÏ_ºöÈO¿þæwßÿà³O~ûû_~øɇÿæßÿþïŸ~þåßýѣϽ:»s—ÚÚ4`Š‘‚J…J,]ªu—·Nž»vöÊÍÛ=óúÛ?zý?{öï=öâkß~ç§?ÿèó—¿ýÃwþõgû·ÿxå{?ð¹7Wî3/Ï&7ãµY$Ë–{s §ÏŸ¿uû'Ÿ{á¹W^ûÝŸ~ø›?|ÿ½^ûן¿ó‹¾üÓßþïÿçÿý#€üó/ðó__{ôùüÔ&Ÿha:q6¨¤²åÖôÊ<·ö®Ýûȳ/½øÆ·Ÿüæ·}ù[/¼ùÝ¿÷á¯?úä‹/¾øßÿ×}ø›/{þµÝ»î&}|ÁC§…x/ZY³xe«;ÀIÉBuriëôÅ{nß|ìÙ›ÿôâ=O¼ðÊÛ?þÑ{ÿäWýé¯ýÓßþóWŸ~ñéïþðÍo¿;¿s7˜C\(²‘¶WÈs©.§Ö ÍùÉ¥íÕ½ó×îøþ'ÿùñ—¾õÒ›ïüøýOÞûäw?ÿè·¿þìó?þù/ÿù¿þ ªõG¿üì…×~¨gG­¾A½˜sÌJÁɨ´\ŒåÛs§®<ðØÕGžø§o¾ñã_ò‹óíýâÕï¿ûáo>ÿøw¿ÿùÿáþïÿþïOûùs¯½µ{ñf8×B˜Æê3{L ±$UžiL­¯¼xéæ£7}æ•·~ðÓ_òƒŸ½ÿæ»?ûÍþü×ÿÏûùGŸ~úoÿþï?ûðÓ»zªÐ[UWéHCÛ?ŸÍ3¤A1ZÁý!RJUçŠíÅRg~iïüÕÿçWÞüÅŸ|òù—ßúןþò£ßüçÿþ¯/ÿú÷çßøîÍGŸ:÷ƒl´Žñ%.Ô›³sKÛ'÷.\¾|íÆõyè­·ßúýïÿåŸÿüѧŸ½÷«÷^~ý•óW®N/¯Gò >Ù¶zÃF‹ú$œyQÎæJ«»Wîyðƃßzø‰§ÿùåýüýþìý×ßþÁ÷ø“/ÿüoЗ^ÿÎËo¼µõÎòÙl{… +f¤x5^êFrõpªÔ™^_ÙXY_»zùòw¾óöw¾÷ýïÿðÝ?ýì‹¿ü¦ã|úÙo~û·¿ÿÛg_|ñÖ~|íÁ;Ë;B²NJBLP\8]¨wfW·ö/mœ:·±sêÊõ¯½ùÖ¯>øðƒO÷îû¿õÿÿÑ¿ýüóO>ûô³Ï>þðã^yó»ç®ÞN7œ„ì$Â¥Ì̸é8F‡ƒr*W¨-¯­=ðà#oB;~ôó¿õÖOßûõ—þë_ÿþ¿úøÃ/¿üâ—|ðò«¯¾ðêë˧¯0Ѳݲ¢äŸhˆ‰j­·Øž^ÜØ;ûÈ“Ï<ÿò«/¼öæ¿|ë;?}ïWûÿõÛ/ÿò‹>þáøÇ/¿üôó/^yëÛ>“k®š0iÌLŽ ÝoFE ˆtu>Qh/oîÞ~â™g^yý¹7ÞþÉû~ù—¿~ù×ûÉû|øé§_þéOŸÿá‹_üêý?ùèïýòÁ'Ÿ:{åV8[Çù˜Æî3¡Œœã"ùP¬<µ°yý¾ÇÞxë_òÞïüägŸ}þù—þËÇ¿ýý¯>ùí_þúW¸Î»?ýñO~ñ³_¼ÿþCO>»yáVoç.ŽkP½ƒ3Ø(«›ÖÆ–Ûó;g/]¿ýè‹o¼ýò›o}ó[ßþÉÏù÷ÿøß|ñÇŸ¼÷ë_¾÷«>úè¹W^Ý¿xuvy—7µ NˆÊíl©žHçê“£–Z3“‹« —®¿õà»®]Ø?&‘O1bÀ…“&aA˜1:f@&L¨ õKÁx*Y,WZµV{i}sqc½7Û;¹¿yýæÅ{ï¿~påêÁÝ·¶Î_®LϱJ’ f¹D  vÒä íÚåñ‘·±s×Üú©h"Y̦ÖVÎì_¿÷ž»ýýïï½~óþGòÙÇϼüâî…ƒÚôt gÂYDW4`ÁX"^ß¾°sp#Ó˜N•ívga~æÔ©ígŸ}ü¾óáÇŸ~ö»ß¾ó£w¾õæ믾òò3O=|û¾»×6¶ Íás!ÊŸ0@®AFM˜Þê¥i!¡ÆV—–ï»~÷믾úú[o½þúK?ÿù»úÓ^ø—¾xþÔÆÚb©ÞP‹UF™Ün*«W°x) T¹Rsnnùü™Ó?úгÏ=ýo¿þ«Þÿü‹Ïÿü—?½÷Ë>ýôã—ÎfJy+â³zD0Ãc&âЀîø¨ýĸÓò’œÉéVofjaqóô™3/ÝuåÚµ»¯óå¾ÿÎ^}ýµ—^|á¹gÿéµW¿yÿÍ;[;±TÙéµ.ÒàfìxõG#Ù+gRK«;—¯ß~æ¹—}üŸ._½ñøãOýèÇ?ûÞ¾wÿÍËO>pï?´îìÒÊr¥ÞdÛùÉ]6Öüj9´tÙàd4FÔê c‰ÂÊÊæ•«×¾ùúà•žxúé‡}øÅ^x÷'ï=ûü¿\¹rÏüÒZªPñøxˆ3:;nÄÇõ£·¸H‡‡öËÙp¦Ylεf×] k@0³Ç§sù4væÑAJüбñQÓêf^Ñìb\xx·|,Œf99i¶#F‡ËäDQ_¡y¯”Ñ ü ÕX ;¶àÁa zxL?ntÛÑ€ÉNX†”’à½=ŒÀ°‚Ëí&<&s™F··²{vry-Wm^zÔ„ ]£Lë híþQ31n%^ D +(¶ãÆHb( AQ +EÕ˜’Hª…Juj—•p$Z©Õ³™ Ëp6—ÏŒ²v\Býq6Ñqÿ8 Ÿ°Ñ~©ècÓ¼+dK©DJ IrHš_\©Vóét¬Ú¬Ëjéñ‡ÝLŠGõè°Ö94a;1f³˜|F®™ …\*?‰’jöÒ•»·¶w2©x½RÉå V³Û¬¨É‚Éá9|b "À°Ö}x@dпŒ›ú;ñ"̈AN)ñL8‘'üA³ÍEÒ‚-p¢ª¦JµÆL£9M1ŒM°|Äå L˜‰£:Oÿ(—@‹t¨€0 ,”Ùéó1‘`¼ +EMréLc³µ|¥:s–âcf«Eà…h4ágƒv„ÐX@!3ݳt¤;¬÷þ_2º¡èìÌ2†sÁ`$M†#±¨šgÎãöà^ÊÏ'R–…X]Ûß¼Àòj‹U¾~DwÇÀÄñ1+P¨ÉI¹è¸—Ϩ…©…½[„ƒx‹’Ÿ#a'#_Tïæ†tˆÕ#îРöŽº£ã¶AÛŠp( „«¤ó2ª“ŠöÏ$1cÃ&Äè•L˜b@$“´aÒ cÜæÕº+ÂÚ=¬ÞALX ‡_E‚YB.RRVïÀ  ”Õø¨ L›¼Â¨´ûbǵîе€×5¡bC tÒò¸³à—Ä€h¬žq‹Çá Rbb2¨.eÅFŒ{9ùÎaͱA“ÑÉ!t܆‡ÝlίÎ8™ô€Õ;ƒ“œp Žš5ZûØ„yLc6ÛqVÊâLȉùÝxHkg‡Mø0̈ó輑ߎ+‡Dëž°øF-̨5 CCãÎŒIN_8–ï,nœ“c¹‘1=™¤“”Ý? +W2L8íÞÈÀ˜ãÿ<4|lÔæð†€»ÆL¤ÎÅéÚÈçÔ¶Ù#ž˜°3ár¸°è‡x*¸<¼Ñî3Z‰Z{!BwèáRV&ëà+(äÊPÅFE5nN‡ +ÃFD–RZŒ:-7”òzzò€‰Oê'JÇ3m³›ýú‘ñÁþ‰œ_®W¦ÏeÚ§Tòð epÔO÷¬XèŽ!Ë ·ÁJ´T ç¤ä´Æ€ íg°3£¯ÑÉšÉÖ_êÖ?dÒëq37›0LXLhÐA«>#f§]”*'º‘ü &dP6cñ†­DÔéOõ Å"~íØøƒú>„ ¸ËÆ)ŒË8}ÑDaQ­nµ5OØW å‹´ ¹ŽòÂãt8[]p³©cã¶ãÖAÈ°ÁÜ’?9M*5ÌP¢ìguN + 7jñéÜ!õJ ¥´kñ*Ç5N½Ý'« šñA½PgÄ$\Ìbù«-P™ÕàÁüˆií§/ˆÔØÑq«ÞÉSrƒVj.&©÷(NnÌÎ è<L3c_;¦¹ã¸ndÜ\4¦w;½!³xD­ÑØ_°›@tNÞˆ†õn~ºè¤ “G„É#Bê§õöŽ²jKÊöÌ^ñظýĘÝË#î;éî²Gu¸Ñ% +Ñ)3"h\ðÒ¡‡Í=*N¸x›/îO’ó:—€ÐŠ?ZsøZ;®ó´s8<¡tiñ)# « +Â5niÂÁOØY‡?CE{D¸i@%;‚’lŸ³Kv* °·³ÇÆì4—.w÷-¨tÇq½Š»™ΦRžÔ ß8¢Õè‘öô)Ð{”j&".šRÚra*ÃZï *œw±˜œ~7­zûËMT¤‹ñÅ;‡ìF”÷ò1;ªu ›}_Z"#u>9™éî† +³«bÁÊçèh‹Švœ\^‡)îÐ1g@ƒ@‰õ÷ø;]DÌNÆ̘LyR®s hœ„µÑ1_¼ãàs#¿eeµÜ[>ƒ…ÊPPnΊKV¼Ä zÂFÄÒæA}î¤Öé3!€7ȹýY¯X!BMø§—‰qJ €wxÈ4¤GÍž~žò†j\zžŽN9ȸ’™òG«Ç´Ž¾;h+²ãa\(¸˜” "x(UZpùCF|`Âók@Ã:DÖº‚l¢§Gù;†LF7ë$¥q¦±x¡¨OÀ¤›`]„I÷OÀv°0ÎV<ªG¤#Ž‹ÏL¥˜h'\˜CùìØWÇŒPþ×uÔÜßtãå +üÚc‡Ži §´Òõ +#7û‡4¤ ̇ö@8³ÑãÎÀ¨Ò#¢•LXÕâÔfÏ$Ëf"äö'QÖíS*ᦒzwþê؈Ùè€Jô˜©þ;x<˜_o®=oìiÝAƒÁø4°Áˆ%`õªx°Â(5N)SÁ")–ddÂʘ<ò°€­?6lsŸØØ.ÃN88ŒÏŒ˜‰1 ©±1˯Óu™PÉì‘þçÃÇGÌvngÐ@0úë}ßqÜ1êu›_»T[¹xØà²Ñ.±J&æøÒ¦\ÛbÒÓ˜˜¯6×ÖÎ?d¢£ÃvjØî7xcV2‰‰ Z]q +À F|…Gü)ÔŸröÏxéy v_ÐÅÆ\ʨî`WÊv¿êæs¾xOȯ⡪Íè‰êW)©à“ VŸlðõnÞé‹ ôÏo1±}Xƒ:“bÞM÷?¾MŠ3zõˆ ™|)_õ«¤T&媅x\ú Împó62ê €.>R[¿„GÇíƒZ— »àäÊíLrÌB ¤´º{]Jw›†µ.£Ãoñ½bÑ+ NIÊe;93à “^±¯ïT–/#¨¾@¾½¡Ö¶ÌžÐ€16àÐ*Ô8Ž [À±H¹E1·hö©#6nÔDåZ¤¹—Ÿ=À¥Ò¸s³s$š°’Z»oTß?{ÊêÎaç‘óˆÑ;aã@@A͈ ±ÒC:|ÈHõu–.û„Ì?™8Þ'µ\¾ôWû»åCÃv˜ˆ qÇq͈5ôo,Š:$è ÕÀ¢Èé.«ƒÂ$3£Ú¸Œ?3ë +mŒJ‡«3kWW/?i¢"V2ìSj|bFLÌ„2ód¬wXïE}Ñzw‹ß8a2xÍhe Ú)Ï]RëÛR¢¶rê€Ê%dÄÂ\¸¶®Ô7£­“ÑÎi<Ú±3ñP¼vß#/–ç÷¡$î€ᘌòE!½ˆKõ c@Å¡þ¹²!'rÐ+•q +L¤ËÄ;§ÿؘ–ý«=¯l÷©v_e³D°+/ãBÊ3ï˜pÙûÕk,\33I *iÝPа©¿^ëQ.Ϫ]“ðúL8gD¹þA>p5n¡€ŠTÜ„I6*æbÓ¨3`Òˆ…6bÇA:µÂ$Ð@ŠŽ5“3çÍô1=ªsñ&D4{ƒðZ7;áâÜL¡ ÓZäL‡FƒÂàºý"X£”&.Ai”],ô1ææ³62¢w 6Bñ€×•+Ó‘³—&„Ô׎éŽ; nä̤ƒ ŸpNX)ŒMAÕ8} +4&˜ UVðx ¦ vpÂц WŽ»Gô„ ;}I\¨‘Á–Î%)®¼Ñ@fX‹Œ°£#öþ¦3WÐEçꤜ[8dð@TéŸÅê–†õDo2*ãREHMA3H.AKñ!ƒ}Ø„ŽëzÃ,„pi+w±¨T´³I.û33dùß\¶w6;}A,.zØ„œîq鮃RlxÐŒ ãNÿQƒÇMÇÓµ L̶Zë $Gža ¡Â@¼|ºç²ŒÚC„ÂåѦ۟êŸd…*V"aÆ£'¢qǬœÉ(+ÙYÐ0äfTƒæ»(çþsdlDtÄ॥¢ÖÉþÏC#G†, ã¦SïˆP ˜ÜŠ‡Eµå—3wi† n½›5÷•4`!c•¸Ô\¢sÆŸè²™),R'ãm_¢Ëçæ‰hÜE­ºrõÆs¥…3GÆl:'cî¯`™h‹Ž´†Í„Ö‚;|± Ûÿ†—á’\fJ.¯HÅ%'£€yOu·ÜbÖ#åðHÍŸìEÊ«ÙÎnmþ`nï~73x6Z—ÒsÞþ‰Íaà«/Â&{BaaÌÅÓ Bj>=u!3sžÏ/¹¸¼Ñ§j1Ù+¢¥%B©:¹´›Oc"Ø×ox‚y:Ѳít{UÈMÙp!ÛÚ(¯\S'ÐP“›˜ÜupåÖæ™ëφsSƒF“6ºä Š¡ÌŠRÞ f¡Ù*¸J©NXHPœ3øÜÙÚÂ¥õ OÆ›'¡y“˵™ÓCÌ˧"µ•Xw/ÞÛ/-^ko=©ârÑ +Ê[0fœI8¹1‡.zÔB‚Ù;~ ê‘®DgÌB§nö10àuÝý–—äÒr¨´\›>sîžçñ&Âg"õm!·DFZùî~iê¬RÛqqZ·›I€A +%…"BNz/ÌDJóv* +ý%¢ãM65,¯Ð‰IF.Õz;û·^Ô .B¥Õxc+\^‰5·£-.3gÄ‚>)Ï©Í!}F/`¯Xæ–››·23ý#p͸ƒŽê<Áa=`ÄÌ„â 7C•ÖÚ-!Ùå’pyÁJ‡ë]VR$Úµ»öï}~ãò“ù¹s)¯¨åù»}±ú1®/X˜1¹-WJÓ;>9§u Jõ$›œwùsd¨¹@Ó?­. V–SÍÍa~Ç yÜ?à‹Ô<ÁBÿ;€ ’Íœ‚÷Õb,›hæçÎTV.d¦wãím>·€JyÔ}ü…ïí\zÐLˆ'ô(›Zˆv”Ö¾/Ò6Qà‹•™Í{¾~¦wKx¤#5N&.e¯²¥5-õ‡K?÷öìæåA0Tn!œR»ggN?š™¼ qòG@ÎQÀÀû„‰s +¨Ø_.é b‚ÎJ¸HÙÅÄ!´2‰^´½—š9_ß¾ÝÚ}4ðshÈQèìú£1+Ó?ââ‰Kô•ìÌ%Hëu?øUœK~ÜJƒwËf_먼4*Ý þgÂíÀd3"º V0)+3ƒVrÈÂF†ŒÞ1' “n¥TÈ +™Ùxc ~‡TkíÉõ>¿œéžNë‡kÄí˜@3F q±aÔÂi¢€ÿÏ„I°i6V1bœ Œ»N6«mî*®\õÅΦÚX3Q²›Ïùyj V¢ÕtwŸMM‚KÁ„B(¿($§@Áý©ªñ¤ÄÆÚÀ¢@Ð ç¥Âj°´Îfæt2‘ê®” X+L¬‘n®.íßÚ¼øHsõ.·Ê×Ï]*”—©n/ž}âìo7nñ 4(“R +Ë#ýu#ˆÙ+ƒDN89ˆÿL¼§q²‡ÇœcÊŠGÀÝqÅJ'ˆh,\©gšKK§nБšŽ˜¼œKÛ¸,@+V]^8ûp(7=l%ŒÞPjò<È(iŽõOB¦M¨I÷òíE+® BÞ-äÜb‘R§©ä”T^(Ο۾ç¹Ü̙à dÅŸ˜"#mG ê¸é×rçÓÿp\7jcÈØŸÛ bStlrØ9Žr11Ê›ñÐôÉ;ן™Ü½UZ¼ìOÏk±ð1mÿ(˜Ó÷¼.,Õ´àQ¬cÁc9¹°¤E¤;-z'ËO‘|êÈ Ý?Ü1< õº‚5¶¸)–6¥âºƒL™P™–T><`G{Žk0¥± *‹“›à½ÇìÌhÿë!$L,€Áã]-*bRˆÖílÊîO‘Ž'ÜFŲƒP0:êö…u_ˆdÔ_‚Gÿ¸ªþc~0½©Ò²œ›¼cØpLëÒ¸¯T‰7wRSg”Ú2'çvÏ?ÜX:Ðb¼”Ÿo®ß¬,_WÛûåùKÅ…ËD´u\ù£M—6{ø¾—&c¶D†;‘ê–7T< $¬÷ØÉØ°»6 O¡`cÒ¸Üßé²`°¹De“8ŸÈt6r3û™éÓÑúrkùbeî­ÔXµ•êì¦;;¹É“TrRGÄGP 1ÖŠä—tر Kt|’Št|‘Ž7X;¡ÇµŽ€‹Š¤AqÀ Ùùüü]`  t(¿°uñúã¯Páʘ±â2©tÔæîüé·î~>;w ;t‡IöôhpÌî?<á2S<Jdò*Úþ‡Y“74`ðh‘ 7Ô–gOßwáá×7®¿Ä—Ö†l¬GÈY|1½'4jg¬DÌI¦ˆ`VE‡F¬pÙ@jÞÍÇ„ þWù"T¤nõ›Ê3»…Þ–”f3óH¨éäK@´:™½bö¥F,¢ÿ‰âèÿº‚;8 'ŽŒ¹Lž`¼¼lt³ß8<üµ;G40B‰JÌxBu9ª!5HØNg&ÐCGµã¿ÆÎk<Ä(+Õô7ìG•„ŒD³“62 b]KLz‰ŒMŠå DÈé}»ÞÕÁf¡v“ðÌ„•ž°2z±Eggu6?øÒo Y Y!±²‘FÿÄõë : ñŠˆ bžŒÔèhELÔ2ÍMã—¬.œ¯®\Jt·éx͈Y¨ˆÑÛ?Œ‘Ë<¤µÜ}‘¹|*Ñ>(/Ücéþ ô’Äš š£6Jã +˜ˆ8é0ê$,C”Èw7³Íu'²{¸Xi®½q}áàÑåóïßû/ÉÎ,ŸÞ¿ïÒ}ÏbRÖ„‡L”ŠHµ`~5Û;¨/^³ùÒ߶¡’Lb’ŽA®ŒBuèq0ÛÅ tƨh“®:™$ÄFBÈFË ¥ÕË.>¯s‰je3Ó>§ä—ëóþภ„òx‰V»Ç4^=ÆúypÒÍ¡îÀêÛñ”‰Q+ ÖQÌ·ŠK…™Ó«îlÜH•@´µqæ^©èÜm‡ró@°ra 6ôøˆÉ‹Ð"ôÍü˜KHri59¹Þá2VoPJµÆm¾Q« 5{ö‰îÞÃÅ•{’Ó.ÇÃŽEVÎ>æä’wŽÙÍ”…YH€‹ŒTNFª{GÆœ‡Oè&̘³ÿy‹}ØJØü€p[dÔ9p§ðŽ£z·euýã&ÌDÔÉæ¼áZcåòÊå§tÐ#6iöHT2\¡bMJí²Kþüš=vµ¸€b&”óBás¤T×éÀà`Yï8¡;2l:4dÐz êûßhóœ¢ l¡7¤±à^!KEëb~&Xœ·Oú““¨Á ©-:iˆ·2.¤áeŒRÔi#*ëì4J)V<4 +R‚ð›v3i¨³'Œ²H u|ÂÅ1È$xÈèÓ8L(†óóL¤jóŠN\ Dót *¥—žÙ--Ä«+ñÂÂÔÚ¥hyÖáñÉžRÛÂÀ6gäÊ&¥Î™½š">©äå3Ã&b²¡‰ì›DBé„;$·÷óEðШák'47ËÆ:bz1VÛmlÞKEjL¨0½q-ž·Ñ†¯6w LZP»jcã˜?¡Áp®äK#ߣÖC£Ö1 ¹@¼kÁe£›Ôš‹Ži쌜žGÐáPq®s +ÂŽÎÅ‚ƒÍvN"´ÞÁŽIˆÿ‡G¬£F¯›ŽŒ[ˆþ +vHèþL@íJŽ·Ñ`É*g»'1às<ÛZOu¶ák £ÅBZTÀ¥ +—œ 7F­„½”ÒàS‹lÌÆ +!7ÆlâÃÎQ#‘¯-5¦¶úÇÄi-"zåšTY·O«urJ5’mŽ‚Å 3Ñ:ŸìÁä‹ cÿÓ½;èrX0ï$¸d/TÙ –62Såò£¶ué;ÛÞ \×?„Ðœ°@Bñé=€Xðö_ÿê{ú‡Õ³n_Ò†)n<")åÖü¾Ýƒ€—™9½tñ©¥KÏd.ùR3ðF¬”îÍî´çÏûäŠÆåŸp0_%bņIb´jÃc¤4%f °ØÁ=Éq»xL‡0ý½ç“$Ç™æùOÜ’@ëÒ•Zg†Ö*CdFFj­uUV–V]ZµÖÝh4º¡ à ÉIPÏ ÉîîÌ.gVžÙí­Ý×ó„Y¬:‘"Üã}Ÿ÷ù…‡»O¸é1~rÜ3XjÛÃywh›„2>s*LåºkÅÝ©«½½{ý;‹‡ëËWôæ–‡§+óƒ§R +ó¤Vd"mT®3FÏK¥€av¡À$»œâl–ô1›_p@Á 'EˆY c´œ‹PÁoù`2D¤.ÄÙæÚÆùG´^;¨ËƒgªZfžtP1?éáÕx#]]wÏ»Æ]œÕ©Pd&Ó¢µâÔÖ¸“µVÂÅÕ|ÿBiábça¨¸ +r¿ØYŸÙ¸1îbìFUÐK“NÒ ‹˜Ù”¬/JDš ¤¤"uµºžY¼>½õ¢–éÍ®œÿþOÿ '{£î "7Äôr¼¾±sãíëïPÑéccˆPJ ä( ßHH.ѧ…xŸ W9µ“ÀÂ:5îaGL¹øa§hö*B¬Chù  ză!aö±n< 4 äÉH“ ×äL/V_“‹³ji1ÚÞ ËT¼CF*…ÎnëE.Öóð&¿êÄc˜Äj‡P-~ýa;1ì ÆݬÒþ Ó”X¹8½.%š™&À½›‘Ú’Q_•Ë«h¤ãåÒŠQ_»2µvÏCGÏØQ³Ÿ…¸©U)Kkƒg­i%/Îú©ðà¦:B‚5LÕ³0î@Ç]Ô˜ƒ9*ï° ¬„…!)E„‹t¤’m®>}÷‡ïÿÍßזΡ‚Z˜S + …™£Ts£6µ×?âõ¼ ¹H ׸hâ néÄtld0BBËäf‡-ÄñQï±aç°³®ˆÑVyz¿³reÄCYQÕEÇìT„4j‘ʺQÝä¢-V•©ÍTmÅËfçêMÅÀÀ<–ï%‹³§Ç¼C“ø>å´+tÞ…*¸˜‘â-›_ôàa‚VR²}vðL]W«„”)w7k3{Àýdæb>R·z97Ö×Êjª…°‘3ãn*ûhƒÐJL¼Í§¦Á÷ø5š©ï^xì:¦6¢ÕƒXm_ŠuõÌŒ’šö ÙS&Œ‘sÉâ°¸ÇF]nÑE¤©ðL{õQsý!¥7O›1JÉI‘’ÕËüÕ)ó·Æ²Q + Šce=Ù> +•×ÏÙ&»à× ˆi8CL…AÉ –€GM7÷òSG´Ñur^,„J\©BPÚШOǸWöt#O³—¶Á6ˆ—“­©õ+ý­›ÉÖÚÔê…Åûl¬ŒJI93-$ÚÓëW‹3‡&æ@E.œžY½hä§ ¹PÕ†„hÄCÄQ!ãFTg@2ù‡Ëñù@w™ô ™D0§IÏχr}RÉÌž½•ïîx8ÖÓçssWA£„X«»r%]_Ìói-7ÊÌÑê7‚à¦NOúþú˜Éâb@˜<øóÖӓðà¾=ªŸ9îfÌ~pwÈÍœ4ùœx2À'$jëb´biÈŒÂtŒÇíØ©QÝ/Ø}022d†GÌIxƒ>áÂA9eA€ ƒIJŒÒJ +SJjêðÖ[ »÷mó3)J­‚šn”×Q>éÃ%” OzYPõl2X&¤R01ãg“ãvœ +ÆÔh8œ¿8aàÊœÅ+ž1ÁÃdÄ›R)ž÷ŒÚQ`TœH H.XÎy…¤K J ¤^´±ÁG‹‹ûwÚk×ÒÍòÌ~síZyñ’VZÆäl´Øg#%³ñ`ʤ‡µcÌ +²‚Q~,ÛÚv€–šüöˆéHmeþðÅõëogÎÝxõÜý÷ÄLÏǧ !à3d¨.g“ÝË…¥»bn—Ò¹öN¬¹)$»[\ƒÕnjFÏG'¿=ìüÎó„ó+u ¼"MƒQELÌ••u¢’ž›^Ý¿ýàõOÖ.> ×W¿-õ·îu7oÓFÉE©brºó ®‰F#VcuÒMNº(›Oµ @Ä䂃§FmN˜ÇĸÙÀHKTf»ëU.©¥•t{(äcíúÀÁú$(fÒÉ:±n Bêôd¤€ Ú|¼s0FiøqÍä@G-ð™ ?ÂD0!þí“ß:>ö×ÇÆŽ± Y`;ä”8¥l}1”êLÚQVÉX‘ ªä©hKÊΩåµ\ÿr¶»Ó›ÙøþOþaqÿ.`F@CzyQÉÏJå“s`Ö +K8ë-_u’‘“£¶¡ »É‰L v·‘Â%JIƒºï¥£¸RÒS™îÙR/U[¸tëåÃ;¯ É&,çÃ¥³¡Âj¦sÔÝ|T[ºY™»ÄÇ»zjjeï6N™QÎIîÁzb.:Eé÷ò~Àà¸>ì ŽxƒÇLè;8KÉ.IéyJίnß.÷¶èHIÊ –sIO_¨¯ÜY6Yíí6úû@'dIñª­ÑÚ`/.ZƒÄ DÇÒ¥~¦º8î@Ælˆ= HJ{@³ vT‰–#é–ÅMâœQîo-Ý_8÷XÊ/‚e•˜Y½òÉÏLO8iàîP>M«u:Ô&´†u°©ÁjÅòÜy%Ùšt` œÆ̈ÙIº!É‹È¢š˜°Vq!;dFþD׈wÂÅzÕæÑNFÙX È`v~ýzyz‹‹Våü\²{Pœ¿<µqwíüãêÊ%¥0ÛZ8|ý{_³‰®Œ`Z™ U€±³ßìÊ= è¼l{g°gÊ7Ë[¿YÔä Â\—+CvrÄúbl0ú<Ø&» v¶wŽ5j´œ®OïÄòK™ÒÔVuv -HÐÇÆ@T0ZÀæ„[$k‡ÃN(äðð>,@ïŒ jφò£NüŒ¢£ÝÂÂU£¹Í'ºN:æÀuJªáeŒŸÆ賜êVq§drS@FÌnÊä"Á+NX·‘:of®ùÈ0«ØpAÉtë7zÛ÷ûÛ/,ì=\Tòes“#fásJjPO€ÖN™Ñã#~«G0¹Å›áŒ©–”XÃKjy|Lø|­4W_¾4pþàÅÊú 7gÉ­-]Á#u¿”…¤œO#R—@9(×ú[Túö‰ “pÂ!‹W2y‹q€oÏ7æ÷®CrÒB„”Ò2—ìhŹÌÜQ¸³‹†+´šÝ:ºßß¼î@UA¥¢’™oÌ_œZ¹ÊÍS€—˽'Ï>ÍMþåqˈµÀ’ƒˆªé™\ç@HÌØen뎘ì<7îrvÂp3 @j€‰´|?][˜]»ÐZ¹h'UR/ÈÙ®”lP‘bªµ¶rîÅ›ïÀZ)RY‰6· `ÑID¼ ú† ‚H3JËÑò²‘ŽOøpãÓßl´š°ÁáÓVúô`séؤkÂKú舘³±Ön¼µç"£ÑÌôý§Ÿ®]z + +4¯+ÅY)ß’S@·]¸f‡%V«¦ÎaÁÒÐ7¾Ýâá&õù¨$pÔõéí‹wÞ@7ÇBL´Mm'“¦ŽQÛœÞ}$¥Úíùƒ£[Ï +Ó»–€b‡ƒ=ª¨ÌÆû>K€ŸçæN™ñ _ÐNÆÄÔ|ª±Îtê3;½Ý{+W^]¾òÊÜáC!5eAU,˜I6Îf!.Æêe?ŸµØGãNvÂÁšÝ¼ÙI{á`²0/êµo7÷Ù}¼ÕMÙܬ- Nx$TÌÍnÝÕÿÿ:f=cÆOfŒŽ”ê;‰ØwÎxÌ~ÎI(NRÄ«ëÞܼ››¿JÄ{D¤¦æÁ’ÈÀï}ó,ÐR¸´ßÅÃîÁú½ïð‹ãvâùÁöUî;æ"4'6¡òx€g¢6YóIF¨4W[»km%gsÓûéö&)ÿìL`)JÑÏ&€ÜHW³¥ÖÊÞ•—„Xóù!»Ÿ6ÀT¨ª‹8ɸ‹ë…Y8˜>1é™n†U7È¢hªØ]Ø8Wîo¸p%Ú8ÛÞ|aáèéÜîÃúÒ…òü‘œë…“­'ïÿíÅ'Ÿ™Ð‡ŠfºõûÉÆQ²q($zVT4N5)5?ææŽMàÇ'ñQ—dE¢¡ÎÏíßxzáþ[^*4æå̘êb¤^Ë´7sÝ£äÔEo°à¥t½º´ÚÃÅÓí¹”Óx¸Ð¿8ØÎÏ͸)# äüt܉h Â‚ =@‡Œbω‡-þ ¥Wã­­ÂÌù™­ÛÍå+ƒK5uáÚKßýòëB{uÌ-`Á2.— µæ¡âæ€ ')ç 6:dGMÔÊ ¯°P5>X–çQ¬²<·|ôú»_L¯]êlÞš>|œ[¾™]¸ÖÙ~qvçîüÎDólª¶ôÅW¿¿ýäc/®ñ¡2n: ã@o‹áÐc£*[añĘçø°ã[ÇL'†½$TŠ‰MÓz=V^bõ +ª©X‡ˆ´¨Ø—ž3C".è¥9L´km—®´6ï4·îWÖïF:lª§gú[ç/î?@Ô<ª–cƒ§J6¨ø"žlïn#NNø'#Ôð0v,äf+Ïèå…Py6Õ\œÛ¿7wpÿì•—»7âí-T¯{ØöÍ=X7®š½ƒçÍ­–›:,Ï]—¥0éã] î Ê˜›4Þ”òD¸-¤–ÃÅu:Rõr^6ÀÅÔŒ{(À€\¬)Í'ëKà¿B¼ðF²¹–ïíe»[¡Â‚Q[5ª+¬Q'k·¦ÖÏ»ÈÀ·X}u°”M¼ƒ…êƒFí˜ ž=¸›(/LxPÂœxÜMeÀ;Zœ=¼ñ´:³ °‘²b~A«-Í_Ù½ùÎå'?hm¾èrÅúâÙ£{x0b,V9[œ¿U[yì\“=à€Ü¨¼{áÅT}ã„ ÖŸéÄ °–W“SG>&Ql®Öû»à.‰…*„^–—@áHOhÙ0„±BŸ WÎبS“È€ÊÅíJ‰¬Œ»(F¯$;ƒšŽiXL£jÖEªB¤’¬-1¡|eáÂÊ•×Ò½ƒHe‰ —¼TáãgÖ.¼tÿÙ—•+.-Äf0©h H6/5xÂa:92SÁä̘‹ü‹c'Çý£VÜìâœh•+n#XXÐkàÛ ¦zýsOW¯¿[Z¾à™öVûþîÍ·[ çöo½Ñ\¿®-”W®¤æ/+µM9»Ð_¿½{íYcù2 6)=›hmÕu&9ïfÒÃVìùaljq_€6|TØ(DûÙ¦ÉpY)Î…Š³K{wvo¿¡–z±ÖraþÜ`y¢ÚFº{±²p;Zßt’!79VáÄÅ;BzŽŽÏòé%\o¸ŒÓƽt€O‚+ ,>ïgA§eMŸÙ@r7(žD¥„’ëg/ÖW®âŸÒråþ.°°Á•—Zë·7®>›;z9T]á“ §áR´»r™ä=¬†‡+ˆR]Ii„‰%K³J¢éDU›?hõ6gq`¦kÙ~²¾êȱaW€‰À¡#U6RN6W[ë×õÚš‡6–VÎÝyüždŒQè]ÌN_Œ×¶¤äü„¶c>L;ºü¸ÐÚ8=ØdVõóY.>­WÎfzí}Œ_¼ñòÞ•Â;Ð ‘úÚÛï¯^O©l8ðˆŸÔÞxÿóW&üÒÉ Øä—ùÄœZXGƒåS&Ø KSÛñææp ª‡Œ@#ÃU%·ÌÌRJæèÖ«W_ü@Iv]xˆÜŠïÇkëý­[ûwÞ-Ï_Æ”*"æX½eö ÇÇÜ}bòØ ¸AðU°*uø;gŽ€Éô'Ü° uÓMÆ[ŽúULÈùˆ0&¥Øx+ÙÞÎL!j×KÅÞAiþ|}ùÂæ¹û×_þn¾·%åzg/¿~ôð{ýÃW@½¸õô‡í³÷¸X;U\\ÚQÎtÇ|œÑhµLˆqÀ¹&7íÅ=^¯ç/g,~h¥W¢­Ídï µ~³¶zYH5zWJ‹"­M½µ%æ£9xª9Ñ•KÌ÷mŒî“3.1Fkì¹”š ‹zÅ0ê@ߪZº5{pO.ÌjÅùÖöƒH{IM3Ñ: Ö™µKRžô2‘òš˜Æƒy1Ör㋇¹¨o§ô3îS J&Qßàc;2¹X5Ñòʤµú)@òGwÞ}§½p8K£RÞ‰“^Àž’T8S_âle5{%˜ŽºÉD;Ll +UÊZ¢~ý᳋wžR6À¦j‹·r3—¢µÕP¶èÀC%Æ´ âŠÝ}5§M°pŠZǤÌDݸþü°Çì$jµWÞýñˆ >>ìw0^:ÆE»Zf!œšÂ¹Pcn‹Ö²öÁò\ "6ã'ƒéÖj¾Pœ=ÈMoëå91^϶ÏööîNo])ö·€xj…9:Ö¤¬oTgv¹Huð(‹<ðó( ¨„³cNXRjÂEZøX0=Õ\»±ví홽•…£™¥ÝõÛ ÷Þà‹_Í'¦ŽÂ•³ñÖîÔÖýÍëo¶ÏÞÊOm_~ô!“êYˆH°°”˜:LÏ\;ÿæÜ…7jgo;Û~ù»—?üÊN~eÌœ€dŸaýpi5×\?ã¥Ù½Û…… ÕÕ«……sz}¹²t¾µreýÒˇ/¼ ªa¶½qp÷ÝË?^:ÿ¸´pY-ÌãÚàÙNJïXmÜC»IÃIF*R¡ºocJ×­ˆNª'.¹àͨÈ7+\ó3 ðUŸ?ÇŪz®•k,D+KBjQj°XDùŒSÎ|³‹4 ª€˜}â`ã s)/®ƒa.ª"&A|óÑpeCÌ + ~&ì@h/dõœ’îTæŽÚk×ùh=Àƒ“Ìéøæ‰;Ôæ¼xÅŒ˜ìzÝê˜ÆYü²ÙÃOº($ØðPª½Ù^¿Vœ=Rò ~1;ááÄPIKvüt”Tò&‰òyÑh¢BÊ +KÁÔ|ÌN9!åĈïøi×±3Ž1éõ¡âR +xþçNÚ,nzÌŽgÈq|Ü7ä ÇÝŒÉI’r*Àè6*-†Ëk”¨Ç€j­ûû‰În03Ä +Z²­Îj“T>Z¤ÃÅHe-Öܬg$Ã\¸Î„ÊgÌȨM ±q‹—1vfÒ ˆ”ƒkÐzI-.d:Û­¥ ³kG7uVŽòÓ;‹çŸLïîá’|v±´pSÉϸhyjV |´ÌÍ0‰0“”Þ(ôŽV.¾F„2b´(%¸^Ï÷.”oóë^6œ?ŸìÓÑŽ5 pZÆGGAmµÃáo6g ºXTÊ%›¤Z´a*,ê+·²3‡\²ËÄZ¤^râƒUhÜ”b‡Yˆ Jš ©p‘Õ5Jì×æDm #®¹0Íìf\ š„j.D—ž3ªv"lÃd$˜áÓýXcŸŠ´±`Ž“””„¸¸`Ô,>qØJ"|RŒ5 µ(ØHªõƒ( ÐQ/©¸ðàáq Dxðd W@Øøç¨8mF†´ y™Ø`Ç)Lµ’­u3’åHaŠPÓj¦#¥g%“ÓFqQ/ÍãAƒQ£|¬ñ†èJ¹À†ÀçàR!”ì‡33ã€ý½œWˆdör§'¼ÇGœ"¼X˜”‹>RE× +…©E@¾ç¤»k„–TRõÖÊe½Ø‹—{‘Â4®d!Öà#uÑh(É6,$¢•…t{‰”P¥èeA¥‹rFA‰Wâå¾’i[`)^_Sò³Át¯¾p¡8µi¤*VÿÆý§­å]1Zj.ìïÞ~í£·¯>ùî¥ÇïñÍÕ˺+»Wî>Þ¼ü¸9~ëÚµµ;õû½­ûÍ¥ËñÆÙ'×^þ¤¹zå¤v¨œe¢m½±Ÿë_Âä #'ï>~OŒT¬°ê £ra>ÞÞíí9ÌÏ‹ÙŸè°Ñ–ÒD- Æ„‡ô2!\Í…‹KF}K¯mRFÑ2‘lSHÔìˆ '»©©£xk/Ý=Ä•¢´ÂMÛPE/k±ªÅCžpŸwX1ÀÂ(ŸƒÙ4!dòÍ€£Ô¬œj%:|zŠÔ‹ OR}DŠ™½8P\­F*g#µíÁšW”Šˆ”h£jȬ `RFŒ75 zd½ÙÇÚ1”‹ÚœÉKy©0Di¢lÄ¥D¾C+ )œ)4ç*³kùîœQšâã1ÛÖ«ó™ÎRsõÀP£¿½sûYóì Г¢^Âø¨—<KV·o$Ë€Uƒñ©Hv^IM³FËCGœœª+;ü´©u9·œîì÷vξØÙ¸¥æfÙ`üæÝ'_ýö?Ìïß…Õ’^ߊN],o>>|ð½Åƒ²íe%žñ÷ï›Á!Å%ç"ͽâÜ•³Wß‹”—ôdëæÃwî½;æ'aÅ +3T›Þ¼¿uû½{ÉÅå\cåÎ?H5×mPІJ3Ý£¹£'‡/|\[º!¥f÷®¾+/óó9Y/•Ä¤B¼¼Ü^½Î(¹sW¾ýÙÏ9k >»Ä¥æ¥ÜÊܹ××n|˜jyñÈÅ;o^¸ÿ––}~ÚŠ‚̲A²ŸN’¡Ö)35lÁ×·oœ»ú’’¾uÒü­¦¿|ÞòܰφϳìÆÂÀû•¦·H­ÄF›¸Zœëa›áR@V€ +cetá +$$…Ô´^]/ÍLoÝ–r}/©ÆK r¶o#Ó¬&}⨓¶øœ78%å +Ð'„3²Å…LºI/Ÿð‰û`Š_Öǧ-/DÊzi.;wnöè¥ÎνÂÒ ­¼1á¥Ýˆ °²Tþ˜‹sd"˜–—‘`ÞCzfœ’mXhÌ#Ø1ÍŽÈ€Dxµ*»òHyN-à\ÂÉB 'F~ +ô  s\Is±Jeápv÷v{õ²œléÙŽ’™òlaœ å>îÇœ ¡¬nò0g,ˆý›! 7*ûIÕ‹k@˜`g „Ñm ̧È`ÎO…ýŒŽŠqp»I6*í•\s‘£™•ù‹éîv(ßeÂYFMà EO€‚hò #v’”2ŒVDŤ +9aÁégi1©ez”Ñ6”PaI¯¬J‰Nmþ\gãz²³ 1z:ß\Ù½-ÌBLºÀh pJf‡_R²Qw')Y&;âRN4 œ°0p&EXÈ“Sj¼…p1`¨lˆžžž‡@i5@*JCXx;¬ Û¾R™°LÜì¢ÀáÌ`ŸGB+û¹”ÅÇDKýP±ç@„‹°z¦8-§DòL0fu´”ˆæÚ.ˆrøq ¸@çÃEZÉ‚æ€~°x8àxÈàQ.àFÀ+HàÄÁTq!€«¶ÙH æ°0˜‰°qRJ‰áL(f'64î ¯N†OŒ¸OùL6ÁŠU"±jº4"|àØ-¸›HBÁ¢ŸOy©ÌÊ(#d*9Z<>îÌ KÍg/4ÖïT—¯Øý¢W:ìÅ?&xq`Ø‚0§SZVI5•dã‚’´$Bkv¿€²qÐ^q>J›°#~Tôh?L²BB1’“VS²-L˹)Ý…É’šZ\=ÜLó@’U!: +NÉ`ÿTSµ ' +T”’»…ÙéÃí.Þ7[±Ú].?@È…¹˜ S&]$0ºcVÔæ㎱š /¢XÝŒ\/\ñ«‹òÀbef›×rr± P„‚™ô9&­0Œ«Ã“ÞaSÀxX•£ÀË?0ø×'Ìdž]'G=#ƒi°è„£ƒ€~Bµž²Á©Év¹Hu‰[½äÙÿÜé »öAÂ|8[1ò]Ñ(ùÅæÃÇí>‹‡F8P "~BŽ¦;™©íÓ6tØìžpŽL:!”&iIŽ¤q^Sy\MX,-Åʹd® þð‚nJ7ƒÑ q7¶bV/ã ð¬œÃE!›œ¨ÕÛ}”TÀ€0a'}¨¦F +¹ÊL²2ÒÁ`$rÙêÁ}ˆòù!׈ vAƒ¦±Nx'Õ +o´ƒ‰)1ZuÃœ¤&Û Œ^€\£×BééduYNL¹ðÙC¡\ŒÁ“u0°‹ˆ÷¡‚Õ‹š\&›7€òîãð3 À€qåÂ5JÎH°C"(‚Ñšƒ+ %³ÆEZ.à|œÕM[]øиÓî£A"®ô<.ÆÇlȱaÛ¤505»½¶w¢Â“vÄæ"Ü>Žã^DôÀ‚ÅE »}˜„óÐ@Ÿ}0cFŠ)¹°Ébó`Œ@´Á &¦ãÔÐu¬ÕM=w|rÒ‚NZ‘áשӶ3î +H^ g ^NßMÏî]G9c†ÙÝ,P‡_pùy” {!iÌPcÍPrÚË¥Ûýüˆ úöq˘tÃ28ŸçOM¤BŠq‹‹4;ñ1+rzÌ‚ÓHŸ€ðÓË\œ ÕONx¾sÚü×''Ž ÛAv¡ Æ*j(ºqx5-Ž»«Ÿ÷’"¤0¹àRãÀpÈÆÖÎõßÿ;3"ýÅsÃ'†­N͈1QKª±,+†6®ö7.Ù`nÈä3{h“‹2¹)˜ÖAcƒáp,/ÍÓra=HU‚×üIÐ/'Ýëôc./Ž²¯!h8jµÛi†Vc Κü¸É ;ý8+g¹PÛ©¿xÚäMvàO¡BÔ  ÕÜ8|'è +G€âùhmÅM†œàðs @€ÿË)iI/Œ;Ñ“ã® '(4ÆÄÜ°8á€Á™DÙh4Úfq&ìÐs9ьז¿¹Ó¢|,×Z e¦€b™—ÈT€ÔP>Îieˆ2@·ÉF,Órú0 é4«B¸áA^Øìðê‰ÌæÑåÆ 8Ç_Ÿ²œr…!X”{T6¯èEÂNæpúF'lgÆl6—OU”l:žÏ¥$PókS _†Í „9†&<£“,9<ÜéÇsÇÇŸ{~Üâ é`žR./CÒjÈÈ걂ʃöxÀkšüÏŸ4Œó¡¼ÍCœs}ë”ÅLrh2ºÎâ€9Z¨Mwæù dÜùpE Ü)àÁX€ÓÓÕ!’çÂy"˜4¹pW€ˆ L +¬ \؇©R–à – r4™ˆ…ÔP˜Ä $p˜ `Š‚êõôÌ|—ä€*’ˆs{)?"B¸êò /ç…%„y¬Ãép9­N§]’¤J6Ö«$×úå »Ó‡Ûýo®®ÏëáLÝjN†G'ýõÜèñ!›ÅA ±X|Iâ´&«Š,‹bÐëÇœ@ˆ9ž`(…)šÅá¢r¨àðÓ§FmÏ=vÒ<4æ¬qä7ÇÎxŸ;í:1â™´áV'iµœ(’ÌùÆQ’w{€ç'0:F ‰I;t|È•ŽùnoÐ4,—òûçÎW;]Ù0æ×—ŒLÚ6?N+qLŠ)‰Â*äg(ñh<›-FÂZ*"¦4l}¾´³Ño×Ò唲:½ueýá­g/>º»åÂÖôt-3>ÈíAì.!t É8)bÆ3X.Î7Š¡Z!ÔkFwç“7·+Oo,½÷dÿƒÇûüÕ¿ùù÷žÜÝÛ\Ÿ­V‹¢ ù¸ÛäŽBÙ(ÉF 4Q( +–’¡ŒÎwÊÆ\73ÕÈ.ϵ/Ÿß<Ü›¿~iíñ½£w®ìTËU#fð<íñ¸‡Çv=fANŽ¸Ç-؉Sæ§L£ã§r;ÜM$t¥VÉ…Ab‰|6™ËeŒH{ëüµ­|2ŒYí¶ «£‚8%Bˆ…Ý5Ùj }åþ~ñV_»5ûôÆÔ;ùÅÝùí¿üìÆo>»ú/_¿ò¿ÿÇoôÉýÇ7W7›Éd<s^? +aÈ¥´8×зfbS9|¦D×Òôl+>ßM-N%Ïo4Þx¸ûûßþͯ~ýÃwÞ~pïÆæÙõ Ñ“V‹ÍºñÔ¸ã¹C.‹)Äù£"”ák qÎFNîÕ£ 56g¢Oîm¿üàÜý›;ï¿vå'_¾ûâÃùbtzv¦½¸7½yŸ‹”½N›ÌÀ©Dˆí)ýîAëæNugJyp¾ù‹/ýåÓgwVÞ½7ÿ?yúïÿüùŸÿÑO?¼øç_=ýé'WwR +ãó8Ýf‹FA‡ $äÐYW/G_?›ýþ[ç¾þÁ£OߺôúíÞÇ/-ý݇ÿøÓ§?ûøêOß;üóo^ùÓOn½u»qs·¼>[LDµd2Ô’r8Ë0lD ÔbèBI¸¼’yùêÜ µû‡•/Þ¹ôÛ¯žýùO?úý/?øÍ—÷ÿçßùÿþŸ?ýñWÝ?j~úÊÎ/¿|ñµûÛ1]1[gƽVE0²Çí ¸m¨×"âÎ 69cÎ-eVŠ›=ã`!qïüôç¼ðö£ý·Ÿ\}ü¨sÁlYϨL'E®Õ¸kKúÛק¾ÿúùÏ_?÷óïþÇŸ=ûÿüóßÿâ]ÐüÛþ݇½~e­_ˆéÊšž I"HA%˜Íaû]þò‚þânî³'gþéÝ/ß¹øÉ+Û_}pñ?}õäþã‡ÿö»w~÷ûÿðåß|qóÅ+‹¹XåX`B,Ÿø Yˆ”~š}¸a¼v!ÿæ•êç//ÿý÷.ÿß¿ù§¯_ùñûç÷ÅÕýù£?ýäþOßÝûÙ{;¿úäÜßܽ¸Q,fãG> `óÝÊR;¹PÓ..ÅÞ¿?ûwï~úòÊÇ/¯ÿ᧯þúËßvø·oìüßÿôñÿùï_ÿëoÞü×·þÛïßüÁ;Vº1² []@€TX¶› Öt_Usž›æoo$on$ž¯¾ópùË·ö>yºöóOoþ×úü~üä_½òïøä§ß»·µTëöfâÅ®WbF<• f­B¿s£ûË.ýý7ðÆÎg¯mÿôãÿö»wõùí/Ÿí|ýñåÿòë7~÷éÕOï5ß¹VÚîÊ™ÏÐÜظãø±1¯Õª3®NÚkK—–â÷jÏn/þü{÷þõ×ÏþãÏ^þ§Ÿ¼ôO_=þú£Ë_¿ð£W—ž]©͆ë *Ä08À!Œ ‡b£Þšïä:®—Ʀù7oN}ñúÞO>¸ðã÷õÅõúÙËÿüóW~ýùÍ_°÷Ï?¼þ_~ñâïxãÓ—ú¯ÏÜÛ«R!·–b‚¤–²‘b”ªÇ‰Ý^øúZäþVüé…ò§O×ÿ£û¿þüÚ/?¹ò¿þÓÿë?ÿè³W¶>{ãâÝk›N{!& ØK c¼¿Åç +ôÁ´ö`¯öÉã/ŸûêÝ‹ÿþ›wÿÏû埾zùïÞ9zëîÚêl1¢2N0AÔ2r(£ªqg“Ax&Üïç¯m”ïíä~øêæ?üà…?þôÝ÷^Üxïnÿ¯¬ÿ샋?ݾ¹ßØš¯”‹å že• @$ÀbƒŠb¤s±XB"šqjk:¾;“¸°xýJëÇoýêóû_¾uá³WvÞ¹·þøÜÔ£½úÕµüb;\ÊëA;|-4£ÎPB*¢&T:­ý¢|s«úÉ£•Ÿ¼½÷Û/nüýüñÇ/ÿë¯ÞùÅ'×ùáÑŸ¿~üï¿|å·ßÝûüAõþnnº aÛꂽœHwEÉP¼&+<õ]ž••Ÿ\l|øpõ¿þã»þÝÛøÉÃùÕkÿþ‡~þ½Û=Ù|voy¶›–µ0ÎE`*œ+´$ž©¥f¢•À{Ih«ÁÝ\Ë>¾0}ÿ }ýlé­ë½_zóçßþôñæûwæŸ\˜>ì§g +JˆChJ@8™á1†P4ä‘®ºA.7ã›ýâÙnöüRùî^í勽§W_¾²¸Ó‰ì¶ÃÛèLä:ptEqÀó3Á%&E5+c¹T:QÒ·PMìÎö§cO.´?yiù7Ÿœÿ×_<ùé—¿{áÃ;½G›‰;‹‘ûg Å ‚»q€Œu!:jv`~ˆÄýnô4|±Üió·×ã/îÞ»ÓÿãOýï?ÿ_ýÚï¾|ñ»Žæ+­B˜^;¢xÖçq#>·Lã /\+\¨FöæóGK…[Ûõ^8ûÑ“ƒ§7®lÔ:@žhØê°MXÜöIOåêëzªËó!$K }{iæÜÎb3Eí÷ô:ï½|íÊÎÌùå2øìúT¢ž ¯)*ÆDœhØ…0Ÿ¦”)g£é†¦ Ë4™Ö•´.å a¾ß›+Ÿ_m.×6zùn1ÕÉ¥©¨¡  €·úh“‡Ç¸„ˇ×íp >ŸB3Í\v¹[ÚŸM][M½sgîËgûß}¸úãw¯üøíË?xºýù£µïÞ]zv©}nÎñ~Èç¦8Š˃¨ˆÅMC*KVSñ(ë©ðF+t8½¼˜øüåÍûûÿóïÞûúÓÛ_¼qñÅËóçÏVû­„Rx-£ÄÛC8Ý‹pªä«1™Kkt!*¤&;¹h+šNñýÌÕÍéÝ~±as¡  +4‡ ŒÅÍŽZ©Scð° 0S‚DqÜçe8Ò2‰x>‘(Æ„ÂFE:©ðyCÒ¨€‚"¢T,v¡ûQÁ ¼*Æò]³0­NÜë#x6˜Igzë3­~)"Ï×ËË¥àál~k*³TÒ–Ë¡~><“7òšàµ˜=nX!¤’ ŸuçøÄè¸ÝïòÊ4—…’LIp'ÅσçfãO.÷_½¶xg§»Ù- M!”Î+ùÅlï2Æ%žÃË«RPå8)TKÅB6©§¢Á”!†dF–yQQQVrSÒ˜ ¶@.T¡Õ,,DÇܤ’h!Œåå“™2ìóèS(–ÎhJ„¥†æ]Øå%]~Úî£Çø¨ ÷Ò‰paE/¯bRšåp6„`\PI`¤‰(êÆB§³\«õõp ÈA%\pøðã£æ 妓¨RgB51\ÁèÐð¨et܆âàmQUMhªžˆ„J™ÈL3µ·X^íÄvç +û‹íùz¶• 7ÒÑ\4ârû›ÌžÁÜö36ø¹Ç°„ÐÅ‹ÙÊJ*ÛaiZ°œÎ¥‚XR„×ZÙû—v_¸~nm~¶V1‰"ñ‚cbÌCn22é¸øT¦{>Ó:JdfY.Üë-#œÙä%H9J'“ål®æ8Fq; _ 86 ñŽZI'µB!‹_ñL;ì¿r{±1 2 AE1T-à a<-„$…†üÒh¡U)¬S 2fÃøh'Þ>S=)Ñ+ôö‚©š› ΢Xõ¬^Z‚ùˆŸ9Vé,Œº ž17‡‡›BzNHô µ ‹ZÎUfv)£>P¼ü`R†ŸKÙ`͇QR3;à£v‰§Ô +*æÝxÄEèD£¾Yp{ÜË{¥IŸäg‚Q“M1R +§¤”o`µ¢—2ül‚Ð*t¨Êê)Ùã ZIÒ¡¢‡IÚ±ÛQÝÇ$¼T̼„ŠðÊSrŠÓË¡Ì´šl#EçIV’´8«¤‡-.³ ñ`!VïHù³^>3î¦NNøžsOøD—gŽǜĘc°&?ÆÆ\ûý˜(hÉBf4>˜žŒ‹9s“Q+",í!BL¨-ŸÅ”ª‰Ø   ×”tOËôG­„Õ+À\ŒÖŠl´é"cHub:­Õ@Ϙ¼Â¨…B¼¶%&zL¤åeS#.Áêd91ÎÉ €ü˜RæµêZ²½Ìͻ٤9 ÁŒQèìbüt0Ù åWŒò†RXñ«ÇÌøqbƒ“†þêØÄLGÚZm;RÛ“}«_91ìqøD“Køö)×)âãÒ\lFËoˆñ¾Ñ'ýÊi 1é$q.êE•“ÃpLgF=f'å'u1Tb¥äؤÛlóC¨ÀãY¿4áLÙAÅm¸ñ­çMR~) çp95»z¹0³ë“ˆVô‹J/±F-˜èû‡fÖ +!µäW+¹ÌÅ»”^ós ©=P͘îòRñ,œ— +kRvõ¤‰u°X°Hj%öP)–´£q Î~6å@ÃÃ62 f¥äŒ”˜qàƈƒ24Dk‘)1RwÁÒ™IèÛgœ£ h Ý/z¨¸Òœ¸n‡e?™° @Ö@àŠÉi>5K'ûT¢Š'ÇQToc¡Z8×_;|”ꜵRa¯˜rƒë›íƒì,çk¶)Ì*¹ž—‡ý¼ ÕQµ‘šº˜š½f´ƒec5"\çRó\z1 ÔÜ|ô¿ò¨°Ä§zV,lA4&:¥”wÅÜ:¡– sÓqpd¤í¢#6D!ÂU1Ý‹Ô6âhmC+,êù…Ùí{„Q‡ÃU£{TY»ÛÚz¡¶~7ÚÙÏô.pzÃHOw7oZÉ°‡O‚ÐâÒKáÚnªw)9})\Ûðpƒ9§´Ñµz XñIÄ»d¤Ñ˜=ßß}Dêµ6ÌBD”Âj¬}!\;h“±ƒ¬a“ˆ”µ#’•†½Â„_q¢!‡Ä¼“JHéY%¿àå c¸ù<¦wAgF€ÚdçL«•Þ¾˜è‚öŽ¸Y¯˜…”*¬¶ÈXÓ*@Äœ(5>Ö!ÔŠ Ñc¢Ója5\^—V'=­·™è î€ß²‘± D?íàíD\É-ÃbþŒ s314T¥âÓdl Õ͈:Ø59ãás@A}RAÈ,bz›MÍ¡‘®•ˆÓÑi­°2êa©¸©HðýBr&êwQ>ѳBcv°r/¢ÙÔJk¨ÖœpÑcjÒytï¹SæoŸ¶=oÂmdŠ1¦„ä,®Õ&¬Ù+P*H“’ƒŒÚ©˜‹Žƒtc"]p¡Ýx}‚OLb¡²˜í»…8*‘Ñ6 ‚Ðh† +ó™©>ÑšDd2ÚôZÀ¦!¹`a2N©îJL¸&gg-xt/Ÿ³‹é™+í'…Å[N. |H<Ý›Z¾d¥c&`'ˆD@¬°‰y½y wéXK4J¡Ò<ªWéh „(©7èXG+¯)ùyHÍI™©ùóOºû÷ËFÕ6ÙÌ¢_*‚|Ïÿÿì½×$g–/ö.ˆÂêjw8$»º\f¥÷‘á½·i"½÷™å½éªöÍöMÎCÎ ¹œ™µ³»X­ +í^’½éE/ú¤½º÷A€û @…‹ú&ÙÓ]•&Î÷ó3‘ßY>Ú{ü}‘¸â¤¶ü²¾÷Uûä—Õ½×|ã(§õ­îéâæ×Õù“8î:½‹ÝG¿ž\þbpúõÁóßËíSDm»Ö’$L 0m~õÛñå£ïìéó‚ÜA¤*à6ëÍW3R5H}¤ÔΕæ¥5xXš>Ì ŽP™Ú£GZïZhžµŽ¾^~çïêÓyl5ƒ¨ž“:)¦†*=µsmŽžñµÓ×,u¯^þ˜¤K«I~#§p±¡Óg;_þ‘uçOÞÿáþå­M®Ö2J„,qÞ^ýô—ÃG?µÏ¾UÚg1Ò-:…òújŒÉ+­´Ø„üñæ/Íîu4o¦p(&E›) +°k5£1›.ï*Ý›ÊöÛO7‘õ(–eÜSNQ^–­æ„fœ¬¤ø¦Ú:wÇ¢=äï¾UB¤ªvíÑcTncJ‹0LyW¬7wßo~C9}»³L×Ø~IU]Èõ¥èÍxoÖ:xeöN¹òŒ*ÎŒîUmç]m÷Þ9Ç­Q˜²rJ]màf—´Çœ·ÏÕN1{Þ?þÐ=x¸m´ÇÄÚí.ÄƱÚ;“¼ÙÎå»ã?*Í=`1x­Zßk¾>üAí_Y­ÝíÓþwÿSûàIBô¨â¶·|Ý=þfxþ«Úò1"{0tFÓ»©R°{ ±ÆzÛ£óï¶ÿ¾±÷šóvøÒ¼sðŽ.Í­+4ŽëûïŒþ5_=°ŒîÉJ`¬ÝÛujk?-uPc*UýëížüæøÙo× lQuýƒoæ¯ _î’1û~ü—úô&I•Xw¶¸þîýïþùýþÝö‹ßI­“¬ØT½íÿæÔûY5+väæeqþ¦wöýþÓ¿lì¾%Îäøeu|¾™\…,íZ“—‹‡ž~=>ýJ(/1­ïÍ'éJ’. Þ®ØYO+¥æÞ˯ÿZ/OÿÍgá8òÌèÞtOuúöïkbóÙ/þÞlÆi7Á”‰Ò~V¨g…p“ÙØ>üöìͽÿüjeöàÉ7½ýÇ\q¨ÖV÷¤µó´{ðª{üÖÝðÅQcö>”)O ´X¡L´ÎYuûi +zÈ®2Ý<ú®sñwð–)OÝÆîѳáIVö´öIyötûúû›wuý‹mwŽÆûχ/”ÚNyrÓ=þPž=kl?Û¾ùîìí_–ÆGƒÝ'‹‹o•Ö1ä-f µ D9K½{Åç²7Ÿ¾æ*ó´Põ½Fç‚Ä®vO9üä¤Ö>µ'¾ßyñ‡ÊÞ{¢´ãªI¶¢Ô·k;O"”$Ö¦„;c*ûp`îä‘Pß͹žâ0µ-yK¾2fj;Lã\î<0×Îà"Ë—rœSŸ^ÉÕ9cw¬ÞQëô}ivSž\{ó'Þø&'x€‡fÿÒôWùÀ\8“§Rç”CuùBkEq£½ó´wòAnh­ @<¹½ó“g¿¥í¡w¶üâùoþÛãw=¾þ¾±ÿNo€¡¾zöý›ßþsœ.奆Ý9éÕ=ùºsò®uü!+¶^|ý×ÛgoQ¾Bê­ÖÑ»éõãËïÛG¿ì_ü@wY6i¶dtÏôþ¥Ò=£Êþä#€¢XKHY¶•EZÓÕ8±ž¢W\Nh3îR,ïhÞn Õ>ßÌ‚&„š"¬ª c#ÀOyÇÏ~_ì&(''Õ!2\T7ªvhgjt/¼Ù¨eÎì¨ŒŽ±¤Tæõù³½G?<û©søZï +ÅÃþé‡(¦ådOªïx³Ý£o«;¯½å w|-•çÕéÈ­}\\¼pO(gR›<™?üQëç•Z{ÿ•7}89yýàÍOˇߛÝc¹4Ù9y39}Í:Cotqýî¯/ÞþíîÓŸlì¿R*Ý'¯{ýêwY¡’â<ÊŠµ]Èâì¹Ò¹$í©78}þý?ؽ#Š¥ñCµ{¥5?œ}ø‡åÃœÎÙù‹Ÿ&¾¡*sr-t®Èʡܹv'/›ûØÊïôžóÇéÙ×þÝŽR³§¸;§Ë ÂÔ§O:ÛÏ~˜ž¿Çœã·¥ÞKBIÊ @ÚÉÕ·>l*µÒèÌ[>q'»_ÚÃkPb ~* ÈÒë4ï1¥IqöØÛyãí¾ïœþººóVi=ùú¯.^ƒö›¸òìŽo;¯Š£k£µ +–2:Ë«¯w®¾v{€ö{R}Ïêק——/wøðWIÊU¼íéùW‹Ç¿ÕG7þ ïµ}¾¼}óú§Úà(Cþ¯»ÏªóGVûHkžÐ•ƒåò†Xœä¸òàôÃÑ»¿?ý»ý²¼|^š?c*GÁœ%çÓ£7¬5ÊPÊ—RT™PG Øgž¡½û2M¹¼»Èòµµ¤'Êœ»Ëº»ZûÌ)>í‚Ú΋Í]¤Œž¿<®§3z2˜µÁ…Þ: ‹ã,ëðV¤”Õ;)O¯…ʲüPhÞâ Â!µÚûoZûÄÆAÁè†A“ëÝÁÞK©=q‡gPÝtq*»ÞðDoìrñFgà"~ú›7:x‘{ÖøQiÛßV½ºûÒ\¡bcqðüŸþçÿýåoÿ@ìióÕCkð¸ºýº4y¨7÷µÊüìÑw—¯ÿŠ°†Y¹ž7†”»TêÇly3&ˆÔv[{“ÓW¸ÙË)ƒ¬Ôþi+ÆÔæ×ó«¯óB…1{Rqf¶ Š¯f'ï­F@Ì Jq¶<ûšT{÷#L ­€Zëî(Ÿ€õËK=»¾÷Û¿úï¼øõ½ º¥ã\Ó§ÅÞu¹w¼ÉhM¥y. f5Ç–”Ú’4‡te7oŒ)w^=µûíÁ%^ðã°î¼·`Š3Tï!Ztg€Ûã«_IÍýõ,O@¹ã+ÜèåÅ:"×)³S™=bÝÀ”Þ>v&k»oûß½üËÖ᛼\gœ¾Ù=Œ z–© h§1¢öqsD'Y±d5f|i%¼ÜŽ’¥UMÎä+n áÍ+”' ŠyoGlC†ÓF§:¾^>þMš¯âj ’–6PõîàÔè¥ýÖ{±<Íòå(ª0ÎÀjl÷–×Zm±ž×RR³¥£ü +%Bt•Ê¤2¹˜ž¿pÇ>C™©ºÅL€TiZ½óÑé/–~Ï7.t=QÖ¾Õ=N°Å¬H 1¢„kc¥y®ö®"¸‘aÜþÁKˆÀG¨TÏòU°ºÞèasñª2y–0Í7­Á5[šŒ$žŸÆZ÷øôù½Ã/1¹ +𶷳fê"#6Ò,ªn÷ymx Ø(zs­{Q]<]|ëNžÆ™Z8§YÕíÁÁs¨bR°åm0k` íîYgï•Ó9A¸òîé›ÑöÍÇœ¼TŸ5<ÿÅô껂ÒôK`6!¶p¡É)ÒÌœÔ+KÕ›•Û;„7™"zûîäyŠ,F2B0Fe +#{”3%¬ ¢ö0µkuä*˜g¬ß_é uðì˜Ü8ŒVš°üíÜù²ÚØÿ×Ãkì¾ït~ò›Áás£½?ô…Y?NºH C(gï_]|Ù>ýˆF«7§çFc‰kDnCRQöDö!X®õCè=Æñ°4µÝ¤P ázŽ¯Ô'×je±ÊÞÛJoå0à¡•Óúi© ËŠóe·¾ +-“ØòŽØ8V:W ê2ðrD‚í™]º½½$©¥Y'JQ¥ov(SXÖͼµfhµ$ÅìœÜ§‹»åÑ“æâee|s?Æøjkñr;”—£#FØÀ’¼w€Y“‚Ñæ„Ñî³Úè"E»I +²È‹¬`Vf­ÕÜßÊ°ë1ü_÷.ø!7“¬ÄüF?NçØíœ$I§üÔ¦;}¤6qgN#dpÎ5«SÈù$n⪿óÕ9÷1qâ™Öjó“•î)Y·Ÿäj¤=3;ç¬=äöF”ÜJñåÞIsùp#%„sêfR¸¦!uÖ©Þ<ÎPÅhN”ü»½ºS gdÎúœbõÓ¬‡HuÀ.(% 'x@Eˆµ}¡~Bzǘ³Dõ~’v‚òB5Q0q©EY©yf ŸŠõÃ8cn!\TteÄšJï ¥9(p£{ÞÚ{Õ>|OºóHlo·88¯/o_³ûð[{xÛ£ÆîÓÁÅWjïl='Ñ;½¾jါÌÍÆnÿðMš)Ã*¨ÍÃâüUuù%È-Ò™ååN†«J>1KWJÛVïjpüU{ç•TÙN±¥ÍŒ@CÖ<¸Ò:èÈòõj¤™"äv 3ôÖicÿm}ÿUóðƒ=yžQú`mŒÆvwÿ•Ö:B ‡+;¤3«ðr¿»_ªtv¯_ÿ„iÝ¢ƒ ÇÍqAë™Ý+kðxf3- BS®ìÑÖŒ¶¦rí|úzZ¸·…’¿mavÓL)C•ò\•³Çµþ4WS„1=~QìÅHSò¶ËÓ'f÷\òàǬòE]‹QœÙ[ c˜\súgíƒw¾ýçéÍoÔÆÁZˤÎò¡Þ؅̨Œt@i4(¾º™âü yô^¤àƒ& iá·¸Tå:ÆëœYc­LN Åý|=óg+ÉPJÄ…:gv£¨Ì0ˆP EËœ3ãÜY–®mÄXB¨àb)˜À7Bx Æ*p¨/¨þ^¥­u"Äj‚*RöªJ†´'\iÛî=pFÅÆ~·V¢TŠ*"b×Z½#ÕöôÎ)Sœ¢r5–#9aMÞ€¯¬ùßÑüºsòuZîlf8”3§¥»biÀ‡à :ûoŽÞümýø ^Yd¥¦TšržŒM»ÓæΫË7úêZG¿ +çµ nèµ¥àm£J r-íÐŽÊôK£u‘j TRJ¹²d¬¾ßyDhl¦•fÑ¥emû•Þ>¥ì~}ùÐ觸 + [Nl ÞAqú€©Jngûæ;à÷œÖÆÜ];ÀÜ9]œ÷^×f7˜R7k‹úö3`m©~ÊÂ*[c¡4«¯û_âJÔÚnÿ +ħÝ=•ªûy©Ô¶‘ä!…dgG@» ðáCµæ ¥ÅÒ6h€(æXíÞ„sJ†©´Ô8µG*Ó§˜Ö,E¥ZwÿKÆd…zA†°"D”À-ÈR©¯G0Þì8͹<6ÛÇ¨Ö æå`ŠͶVšFý›æ¥ºÇÚSRëgY/I•×Üzœ–J3Ên&Å_ÏImÜðûŽå„º¿ñ{ѽ±Û9†Ôú¯?ÖG¼”˜ÄÝ8b3ÒjŒ¨ ŽGG¯¾þy‡'„2ÀY_‹å•õýÙf6š×R˜ƒHN2Eè f +ú·¦ ÑÁø>ƒïÛBÚƒ}èlø|#vO#”»…Z¥ÇXAÂd¯>{¼ûì/š‡ßÐ¥Eп9QÁx W<ÂhŠÞB휀“¢K; q¹Xàu„Ñp¥Ê•&|y +Ò £Ð>f‡`¤äê² •3´íszÿdçê›ÅõoäÎu‚,ãbÒÛiÆÉr³}&WAØý‡\y?A•¨*»CJ­n&ñ$i0ö$EV0¥£¶¡–Q¹IëÍÚôS›IÊ¥¬1W„”{Y\¼a ë.ØorÅ—gQÂä«û­£÷³›ßøßHíxœæðÔŸG —v¶½ùËíëýùèò[kp‘&-§¶Ø½úÚÇp=ŒjaDƒÜ–ÊKFíá¼JS†7oo¿€j-(=@ƒ8á’ÆؿТzR0E:õñMcùœ«ìÒÅ"w¶ò*¬#¥–x4A˜ýòô)¤tÞ/örœ°!C6c$%Uh¥BÊårÿLôvÁ=eèe bó^„ŒãNŠ*ÓbžõXkBhCÄßSÚÚÊrµ­Œ\#0¤+U‚…Y‚2±Z‡fû$AUþ«ûiZIR æŒUŽÓePµÉµæm'áU¨™çê)Ptl-˜à6â~X¨ ¦S”Å8ópÞˆ¡¡v¥ò‚±Zu®V¦€)Ê¡¬ÈÜè3¥m³wSš¼à¼#˜Û"…²ÌîÕ7•ñ$+5cL-ÅÕ1­çöÏqµ‘kåÑukÿU{ûamzM§1¶ ZBó–^ÿT*Ž7SL3"ˆž;¸1dä•0¹gÔò´>½JàzÐßÆÊ¢iÿàÝàøÖ]Ò&Õ÷¯¿ì}™¤Kµ¼©5ŽµÖ‰èíøW#¤8¥ºtú')¾r/ÎD7'÷XÀ=kV`K$_TìÎôôµTÛ‰Å{ !¯t“l-Å·´öE$¯nFpÕW Ÿƒùóº&RLÀRk-ÎdhWkì' { c€ˆ­HÁÚ̃ølú$‚ؤړZgæø!Š«¦0+”–6ã«ôŠ­ƒÍþ³Õä›Lh„F’°R¤µ™[Ôˆ6è ÕõVixÑÜ~ÜÜyÒÚ{®wŽ@%äWœ‚°‡‡7}2:ÿuûðëòò(Ð y¡ˆëõŒPä+;jç†öΤæÃÚìu{ùJ­íG0J¬;Däj^¨€–ÈòžXžÏ/¿ß{ö7À) GeÚh“RÎDhäõAqò¼¾û7'Y¦Øž^XÍ8]Œ’n/fypÐÃÚìEeþº aΓè·![ÚÆ­$UJsU»w18û& v€­2æU»°^I¦µ€k=Æ¿¡²È0 Æ”æIkûùüæר>YM +¬êE¯.8m‹-.ŒÞãùõO“Ë_ãÖ(˜e@IVÇÕñyŽqLXzÿÚ•Çúà ªöAÕÜ~ÿ”ât3Á®Åh¨‘œæðľÀµ~†v6“tSY£+ˆiJϲ€?mŒisJhÝ,Sa¬QŠ.Á¯¤W=TðUÙÚa¬Îýƨ»uR\kíÓ ã¥q¡LÞl +f=K©´Ú¨tOÝî±;¸¢ÝEVîÞ‹0[Iõ/©*m&hÀ4TlŠ¥¥äíåØêzœûùFžR›µÑYA,¯F©Ï‚ÄVÁÅ‹{ÚäMeç•\žÅ²œ[6À-þéjn-Îo"VVÒÞ©Ö¸ˆæ”/6s‚Ù›¼¹(üéý<U‡Jë$M¹PÅ9¾TëBi,—G‚ÛeÌZžsþVZE£¶çôÏ…ò´ ×à z l+Å”ÕÁuâ`¾ŠD©³îT®±•}(ÒRIø§+\y©¶ÎÁPCö‚adœ1eô­ÎUœmÌͬŒŠu½±Ç»c»}ê-_áά`ŽRbc 3â0z/§´9…±ç\ù7&ˆØmÁì4è“Rž¯€ºØJ³ P½(•÷Ygɺs¨Ü0jbr+ÍVˆhÌš Þ!8¸S "òjƒ"Õ/"Õ0{N8 8~½{@”/Âx(¯Àrg¸j^jÐî3'@èµé#»wÉ•¦[;†iðe!ÌYOK1ÌK `çßX’þQå¤áæT¬žòF³ ¾ÂhÍ­åïVMÃÌWi{†Š"˜Q2\ G36Ó"w…Ê~iú%h†n®„ñHZ˜í=/·÷2„Í+qÜÌÑnžvà±+`¤4œÿðÿ–Óë[Yy+§B1RÎn–óI0šeÃZñæ´Ñ çäfoedI ¢p¹‰²¥pŠŽåÄ‚Ü#ìi/²b#ÎâT8Í°j 4ù§›h -+uÿŠ,£{É–Ö3úÏÖPÐl”Ò•‹‹PVÒ«;¢w€kã8îÛ.¨&\ò³g4ö¹æo~ÂXqÔ÷)y¶(8c¥ºk¶ÅòŒµû¬ÓN1–ß>/-àr$ ã 1µV=Êì0Έ«ìä¸"4gª4z'_.¾W;qÒ‚'À"¦é¢oÊ/É·šü½Ú‡BiªÕvA-€‘Ï©Ý”ØÂÀRµðõà¬,Ö'?^ÔZr&QRTU(ïbj{%˜ ÆÚh‡ÐFY¾Ì[!ÔÎ MÚ™'Ù(1º¸ƒYsÒY†H÷Ó(ùó(Ì‹¤ÚÎB[re"G¨‘Åí”Ô 2(®ïŽe˜²\=°úÍöùøä—fûR‚SriH3)6ÉT1m¨7O{ûïÙÒ~œ(m$¸xA êF|…´¦H÷ê!(üXA_á¼ 0ž«à“l%'w@Ãó¥=­qŠ(Ý{ x[R”0_Z(õÃ@^iÄH­Rsç! ˜HF=Ί›q:àyI5[^k·9< &©8íŸÐj\q;„˜kqîg«‰$¦¾FÝY “¡–çëP¤© Ùu/€|¶š,°EÊï¦çnå´(bÆ=š•þ2”ÇíÕ(K*úô&˗Èÿ¼ƒê¨aJþ\ñN}ûͯÿɬî|¶YX‰ˆy©Ê$ŠˆIÂy ´‹pÈj80È«‚Pñ÷éÕ›‚;dÌ>HÅ›éÍc¾¼ãŸrÁuÀY¶˜Àµ¦G bQ‚tP´ÜÂ\–'×ÎàŒqGþ&lr-‚ªÛ1TÇä6ˆXð&\i鈩ÅÌ,_-;Ôz•Î#±áwg@¤:¨hT®‰ç™²Ñº’묻Ãwsìo*¤ :oõ¢¨.wäê)nÌàE¥Ú‰ŸWJ]-/@´¤¹FÁœ'¸¢ •Ú‘Z=Œ#Æ8“ƒ'„ÞÉ„Y Äô;×ËÕØÌi®'iÖêÏIB²åÄFÈ÷Adâ圭—çAô©p'K«I9˜Õ±£xŒ=^i”×@3§ØJ^=PÛÊKë) QzÎàÆhŸ ¼wöâÇùÅ/uá[ˆÖÒåœÁL(ÏPÀP¼=Üð7±Ì`P-PØJˆ¢Ô^šp …rt“ºjyG--q¹©¾#?¶¶—î‡rëa|#)f„6!˜ ±`ßÛÂ[ÓkàÓ•³ã€Â¢9d›â0©õgkÙŸo )ÂcæfB¸¢àÏæ2úHp&YÊ] c›Iv3%€*Õv†®D+‰™ncٙ߀€ßHª„¹ "!N•¢„“`¼0é~‘`WL8¯Pú@o°Î(Ç£ +Îr(ïy9ÃGZûª öqµ‹JÍ`^ýl«p?FÇ0#'Öc¸ê Ækƒé}Tiz!Ü\M°Á¬’'•çsÀQ‡6† #¹â  +¦vÔæ±XÝ-¨Ý ï B•P€3ZO²1D¦´c !Ù2”SàýÝ8­æ¨h(Š`J€1©D‰€«RÛ¤ÚN„ßâJ \̪=|"5Î!-•ê>˜ÿt·RÿB”4WÑš§RýU;àÄ#¸u/AFq•»`´#yê(R(FpÐíµ@V[2HFc•@ [Yqw¤ú¹àÉ5àÊázŠt¦ ¡ÚjšÃõ mÏåÖquéŸ å™$L­8xòþ¯&G/Òl9#uPsŒèƒŒà_ {»ñ‚UÊzs3-ú-Ñ nVîóÞ©Ò¼iD‹õÁüI· `·Ì3„—ôîƒÎþ[ÎÝ dÊÉ’ÎVV¿ ù¶žàïEØ$Ýk'€ük&MG×aVQ¾„F pÁríÐÜSb$§Á#Ky¼³„dNl[¬ÄØÃiÿÙZ&’Òt™1G¼3&ÕNµï‡Ø„‚«ƒ endstream endobj 29 0 obj <>stream +]ÂÍ¢á‘Sû[¸ ©âŸŽV»´=ê'µ‘X9̉í$é渦¶xw^>R›gi¶'«¼’¢ë›5 Š[i¶„HMð¿ø€Eðþ ®AwAÖií3 †]I§¤™­Œß4M9Ö%­®PÝ.™½+Ð6Y¶„I5R®–ºõÙ 0{Fjbpö¼4©ƒ(eÔˆ!:˜XÌÐv†FBª¤ 3˜d ©¦–ç¬ÞE…* ‚MlÑKР« y¢˜F¨ ¨ ±²Äý^fPθÖÊó$S?ÎøþÜ<¡ÝLŸGÄ +ØCÆèj}LÚÛPŒY¡Ê–fTÿ7÷â€juŸÐzÀSId<8ýúâEcù2ÍyqÂ4[GPIÒU°™·r2äLk÷ÅðòW\e™¥ÑcP‰1Ê cF5ÜVã<&ÖÅÒx룯ô›C)}pÀ ëiq5F¥ åkka ÔØÃŒØAµ±X;@ä:¨µÕ(Å/ $yÊœaú{Y®ÅXs\ª¯† Q:ŽšŸÉ€IçRm_.ýîòëÿÎî\Àc95–×V#ÐñzÌ¿Ì•ÚÅþƒòø!iô7SbgñŒsfq¼¸‘Õ6 nœ•²œœ¼ýãÿWÅT´‡È}µqN™S ŽÝ:ŠY®šf]GR¸/´¾¨ü"y»åáCÁ] RX*nxðV.ï€6‹QQ=­¼¿øðÇgßþñäÉ÷aÌLƒy¡Iš£UÍ,¸3Æž’Ö(/7·òÚFF ™ÒUTé¤@ž„0#-Tã~œôOûyH— R •ê1Âé›cËji^_C† +*5‚ˆÒ(Ï· +òSÇ ‡²†T á<@Œ$È6ÞË˽âø±;yK å–&ÍXA‰áþ· ÂŽÅfaÉXSàß/"ØZ Cùr–q”“—:`!•ú™=xÌêV!çófÉï4!ÕòZ³&´»$ÌQ×7Ó4iõ¹ÊveþÔ™>Aõašd+Á¯rb%+”È’L™)n§/KÓ—+áÂf’˲•å·/Ý•b븽Ìò °º€öŸmd7cD4'“tÚÿn±Èë–ãÜa’±I½eõnäÆ™ä7¾ìÔøèSdíÙÜ£*fÑÛgŠSÐZ Ü $ÅûQ<”€ýcyåç÷“ Ì`Ì© ¢Dy-%­¥„µ8 „þye’ Ð!¹KšSLÀº¬'˜µ¨;÷BØ–ßLÙ JÿØæ¬ txw'\°({ÌçÀì`N?R÷ãršëZG©L•Êd- +Óèi²®%øÕ„˜ë¤=¤ì©TÙ,¾-'V¤ò6kt1¦¸•ââtru ¯­¥Ex„PƒµFzëLë^QÞ~ZêmÅHú×DQ YÜÃífL`!€—!“tjd+¯~Üq´‚·r¬f|PaÑ¡ˆÀeç…:Hå8d&â+ÛÊèÉäú÷•ýY¾o'ì,[F!$Ûö¯õ½"‹ó XP¿ˆ†Ð;ÇVs|-ǵi{-ŸJµÓ _ßHƒ„@¡ÀÝ޵Ѻ¤ìEŒÑÞ$ô1RmÝO0`üÝþ…5x”ÚaÜ‹Jø´Ï„â$Eh¶ÈØ£Þþ›éů{"Ì-L%ÍiMB3 6åÆ…Ú¹aŠ{QܹF)&E¹2þ¼dʶ`ÌsrÄ£µƒ).Ï–Áì·¶Y£¨1OÒpœR7µ• ŒvFâœIeöÌ[¼ðæ/R\ „.è¢@ÊO§û"„ç8´=HQN0ÍÝa1;C?ÛÌ­Fi` +Ê;Ãa¾£VÁ¢ZW·üKzøõ÷yˆcvÎ' 2Èõ8jåÙ¦úW>øß!fõVôS'éa2Ç×Íö…ÞyÀ—qL}»Ã7Rtó¢iñ³õ\8o˜­ãéå{gx’‘’ü¯ÎÅÒ6P3hxÊY8£Çõå»SËð@%ßÜ*˜k)~-ÉåØJ±{ÑÚyÛ9úÖž¼äÕ8ø.†9Àn ÖKKmÊÝ…,k‡`:@’Aù¬&HË [ãÜ}j°”› Øg=Åmý+^}¼× +d*"ÔwªÕ€PbD|·Õ¾ä(ÕrØk;9½Ÿ7rë¬uò½Ú¹ƒüˆÓq2QPY}(WQ¥G:sˆ—µÆ Û@ç30«•Ú8dÜ)INhe¸&kO r3JãL•0&¥é wü”r–¾î=ÆêªÕ™;º Š3{xSÛyYÛ}‹[s€\¨£qä æÔQ’maj¯Îˆ‡21i*IÂêÏJÓçÃËï@Àg+¦B¨=\mTOn,éÒ"³¼†$Aºë b‰æ„+2Î[Ü‘O¨BîÓòJ„ç”Q å€ûl(L  ÊãÊ”öçAôg÷¤äŸä‰ãNŒpI{jvÎJÃÊèÓT ÕÊTóv?]Ïøƒ½ ‚Èá:9®r°w5N¡RðŸ/íúm—õiœð¶²&k.ÀeoåÔÏh5 =Lè/„Øëqþ^°ÁLBò±šâkgY^¾íc¶/i}¥ÄÚÃúò)ð&æ@°¤µdË{ÀD…LÈ1«¾ë¶ÝÞÌífÆ€wÞÊ›@^÷âd° ¦„&W=Ö»—\mMâ%>@ÆÛ0uð†²1Í]øûjŠdÅ8iû÷/ÔÅæ©Ü<%õ>xy˜gXÒSÖ ¼9©wI XµÆTº££wㇿ£Ê»ki äD0Å°j½(ª³pàHÞkÏ×¢øFŒðO¢:XLkÅH0/ï1.Èõ¡W-è#°o\q‘âà¶{›²&˜TÇä*©5¤ú®3¾Ÿ38ÿ¥Ò½Lr­^,È s&9¾’—z´³ãwö¬ì¤¥æb›I“–àNdÿzÝŒÒK@RbeqfÀ§˜Ö¬ÎøçI AÄD•!˜Ó$YJ`æZ¤"u½¶-Ww¹ò^š­GP'”‡ä,ó.¬òÂo…ÔÅò6í.rr'Ž—Wã€iˆZjGÕo÷CUÈüÞ¥XÙcÌKP¶•þoÿä³L_šÉís¦rŒ©ýdf ÌÈ I­+Ñ‚M[ Úœ#BGpüïF6“|š,‚ê¬ÛÊëI¾ä’ÂngÁ;ˆþidà5gôH*- +RáꦨUÍùuQQC˜g¼¼6ØÂÝõŒÆÝ(îbR DNš6r¾ +š¢Ò$E5Â>b%A§…ZVj'9/B;Yµ«uoêG¿ÑÇÏòr + + =FåÊ~iô‚.g¥~˜(mù×®”S´ã·˜×À§0 åp­ õ^)“Áæ#RKkù7_ƒ ¡zuzSŸ’v+#Õ2àØ]ÜÍ0NAô‚y1åhÿòy#%Òr(«meä å}ˤVmí=g+ „Y¡ H¸BtÆ`ú ‚ÛÀàþ¥_Å…P;"ŒQ7À6¦(“6;œÓ"õ:ˆ½‚1®ï|%5.àh³l5œIk˜«ŠQŽPÛ¿oï½V*3B¬äiä}qò(§ô  "XliÍÊ“ÇJ}g++†3r/ArBµæµa/B±¬%hT¬J¥IŒ´0}Œ¨ÊÞnn¿0É ,ãm&…È`PMlÍ¿}Œª òU§´½€$ùt#·™â0¥ê}=­¤…–9|  [ƒ' K>ßÂ?b@`¾¹¥Ê ÐÃÚ/íCÚ£BÀó^¢Vë+îàñÁ +³“+œó7 HQ¥¼ÐÚLJë1>Œ@†Tp¹íß»ÁW¢ã‹0 xKÂÜJÓÅH^3kû)®ºš—¶H;àßÖ]ÉÊTÉÞ!ˆ=T¬á r+˜3ïÇ¥PÆ¿ðFj¦Å:(OTëbú¨8ô·"—:Q²²‘U£„ó74p3œGHØ{Tñ˜°ð>IÒBÀÒ†\=êýRë^àö$ŠYëi"É‚%—ƒyäŠÚºvG/8o÷·†àAŸd@_)­i¯¥è0f1%ÿ¦ªœPùy¹& ÓDÉÿª‘.…ÈbNnsÞ^NíÞ‹q)ªÊ¡œ™a½ W—çå®Ò¼H‹•$„¢.o‹µðƒ|qÂX€'«·ÔÊÓ{¸9ÜÌ+ p^@ Èò*odyÉ[´vŸŒÏ?(̓͜*"‚èœ92›‡þ÷˜tE,ïªõcÿõâr5AÿéJ$”• +Jª&B•w_麠v"¤¡jkyg-cæÄ¡=x¤7öã˜/èRm·{ðvqý£Ñ>‡eZ‰2+€~d‰±gëiÓ{/þÁÛûÑG Õ×¢,®ôíö%Ôìñg[?[Í¥™zãì‡öñל;ƒãÆy((Í`V æ +9 ìÙáË”jg¸5Þ]M•ÂD1F{)¡›“†P;IÊK3L­…rlV$¹jVj¦¥úYRÙFÚ3Dnú]¡I'¯ôÝÑ«òì½T?MÑe@iÂfô®\ž3î(PpîgÌÝàÜ#£ý„q÷V|0§ú7EöÊÍˤØØ@ÔŒˆrÕ0ªÝ‹à+þ¯zþI4ŸÏ"±4’Ê¥L&äVò’‰åHM!h>\Ùþ|R>Ëfél:‘M¤Ò+KÿIü,ƒ¢’ÏärxRæã«P4™GùüÿË“þo·ü9¦å'ÓO¼Ov>q>AW‚[+Uï“ýÿWÒëãÔ‰³å¨ð¯eG0ÿ×< vé“8?:™ F®Ä®TÍO+'Ò‡èÇ¿Áa Ùÿ’Yi­¤É•d"ç¿|úõÿ—ÿþEÿþÿ‘•4c2ÿñp3+ßá“Gè¯|a%—FSÉOªÊ–fÊaT‹âFŽ÷òb-Ïyiº'½¬ØEÔ~–¯§Èâ¿6üf̦v¹#PA¡~ SÊ0åxA/BkqG¨6È«ýŒÔÅŒY¸`¬Çñõ(ÉòY¶ˆéÊ™áÎ5Æ¥eÜ@^aí®·’T‘ÐÇœ»½ÃœÜMRÕ¼6Lqõ(foå”[ÍImDnsåeœ«®$X¿[3]‚ƒf¤îz_òö{œSZ[-A»1ÊÌŠ¥MDÌÞÛgJïQÚMpÕamt¡¼£u/…Æïí+Í Üž§¤f° çÄFšó¢¸µ‘7L •“„žçKY¾²ždà³R´D-Ô ãvš¯Áe8•š®šdjZíT($ØF5ÖÒâFF +tÆZw+/meELjZ?ÇÕC9=c—šFý>1[¨ÐÜL +÷Bx(§EP3Šš‘‚—º9¹—Úi¾¹šY5Z°D1I¡oE­4] !ÆzŠƒ‹bN5ˆŽËRí'Èb´à¬g”ÏBøç!|5ÎlÄ™$fd)7s˜S).Žª«ìçìj‚ædxy1Ót-E–c˜HK° +Y¡)«<”•7’Âj”Žì,×LÓõ<ß.(}LéTzg²7ÛÊrq¢ˆ©¦8Ã^0+ðVŠ†éê&ØZ o2rž¯sΔ1É‚É;“¼Tãf /Ç UúucJ¬ÑÜL1I&ï·D]ôYFêgåA†oøm¼éâV†ORåLåÖ SÞÁ-XÇvˆ„e*/NéôOã¤'lÒškÇBõ<+v"„ò›ejaDÝÊË[y¿]G’©À¯¢D)Š7ÒâjÜïÀB¿#‘X‘n’­’ÖTi^7ý—qÖ +a +n øÚ‘1|"4βÚ0+w±59øª¶|¡‹œLÙ gôBmßd•aVéÇÈ"Ìd´`&pÞüc“Òo~‰ê°+b5JùaÙ*ÌvR¨G©rŒªÄÈJœ©%¹Jàcß ¬ÔÝBýƱ[ym+¯2~²p^ISN Ó·rR %„3J~•!‘’XѪ¨•]È«û[ØFœ½&> ›)! +YÄ6ydZ ²†ËRo¦Å(jÄq'ÃÖ â$„ï!R±1Âþt+ÿg›ÈÌ‚Ø¢õ¥c˜ùYÝL᜿ù§éû!4‰ê9®™Zi½Âà0☵§7RÌ@±VòVN‡,Ñ•œ6Š“N0ņsò}˜„™À¡´/UŽ„Ò!*uÔòîõ«¿é<ÿ“{aÂû½{yµÈÊ‹È#ÆFFðwÄÎ*P”5$õ^†rª<Ø}Eكͼ¼’ ³tEi\ˆõ³ SJ‘Æf‚ýØi˜bª1ª!+©GÛ ¹vˆ«½ªÒzWmRÅqFªf¥¬/ª ¹Ò²¹xƺÓiç¹*[Ü¡ËûY©ÅŠ€T0E £êJŒXO²cn)¦#¼,×NR^–õwNd…iç„r„0ãL™+ï•çïªÛoÝéM”±²|™²‡„;ÆÜIFéqÞqqøDožÔÆ7jcËo¨cÓö° 3'I¾•‘G)ÿmK©íwN³kI~-)$i¿5H0-~D×ãl’,B&¯ç”5DSÕ8ÓNq]ªtà½Õ$óLXó@N]M🅩@V• 太LÁŸÑ‚Î*Qz5L®Ei€…HN£Õ^Al®„ H*øùÁÂJ¿Æ‚95ÉTqc†(T1ÖmÌCy#ÇVýNrD1†—2\;Ŷs.×NS¬÷i0·%q2l  ;ˆ—7>]Ï~ºžÙLÐqDÏ0Í]#v$oæ¸!u+0Á[S+qa=­¥¨ + ç:1ÂIV3¿ØBƒ)>‰»¨Ð‚'dèÊ7Y¨ÐÁCXâÿr%RÐÚbe, +¸G×"D)N•7Ò ÿÖÇyH±•] ûÅ\Fí¶çO€ “t ñ»• ã´·š62¢ßvHï³ÎØ4S|5-6L5Ë7c¸³•¥ÒÜ^1ʼnPžî(+50c@:Sø3ÌKWôê>iî'éõŸÀK¸6J³õ$ ÅXÚÌÊAD ýåĬ²…8ˆØ'Œ*k7ãd1Ãz@X¨Ü(h]Dñ·ÎçªGdi™à«|e—// +z—¶9½¥´Î[;œÑ“çmæ•Í¬q¢~!ÖRŠ¸0P°}¡¼…°5É™pfRz5Îm¤äõ$Ì*I¦^зùò>mO¶ò>î­EÉûüóÍÌA4K•œÆaŒãl}à åYÀç¼Ð¢ÄÍ9üa.1cµSà*”5 u±T…4&¸9Eõ±\Ù¾/ö/69ÃÕ¸Ò®Þ¹Ò{7zïšr§’;òå_0ÕÉýŒT›` ¶Ž)}J bÓW)i–;ë·ät· ¡À‹•€j¥ÊŸ3ë ÇŒ‚Ö¢ªëI)”·cX™Ö†˜Úúy_ña´TЗ”³KZKTÄ© +d ( +Xšõy?\X‹›!C{×@¸z¢àleÕ`FfÙˆÓ¡œÜ·cïEèÿ“¥÷`rã¾Ò~?ÄY"93ƒœS£h¤FÎ90˜œ39Ìb’(’¢DeYq•¼’µV²l9i-GY²•m˲×ë}ß»{«nÝ{šï­BM ‡àÎyžßÓݘ¶fJKïªâ¨ Òyn5 ë0•‰UÛÀFƒP®Ê=ªÝ€dÁ#lÌJÃð h½RÛÅ—lT–:h_ãôƒÎ iHwOL! µ19T,{¥šAª ‰¹¸J„êzoÄÁf¬rÿN.µ(mz#-.Ù_=¸ßW˜2Q€@vè&©ÄŠ +• + óDàûq3mB$å~-þ*è¼Ô†ÌØ™<€ lĸ‰7â“°ÁÉA6Ùå¶îêĈ¼'”{–€S³EP7‘Ñ#Ña~Dã3`“JëôÙÉ„`€Í#L*z$¥wUG ô„U€I7Òã&Zï–@'í¨Á;4éÔ9*#;¤ÁGô$0"Tàe£²qÊ­y‰„Æ-Á˜L²”üºL¸˜ì¤Í7af&@…Œ4Œ_ëºÆÔ¤‰—<<å Ö‚ +Á++_a ‡õã Ó`Od¸mÄ€÷V*e£am3fotÒÌ@­Â ­^Ù葆 8ô Nݼ…yp̈+‘D’HÀ^ þV °©‘®›Iy¹d¾¹«o€ý9ù¼KÈ{ƒ>Þ –¼á:à({ýÊMà„Ä,_`ã‹Dd@Ææl\ÍC§CÅÅI—4n§³¡Òv¢u,Ý?™ìG"U›ñÆ“šSc²LÚØ›œóåÖ ¬¡R†„ÊÊ-ÝA>“a_f9Ù< ×`) S4E0)7Ÿwû Ûx¨ic²àž°°ˆ¯Øo!\r6Ù9A§fÉøt ¸d$bãVÞB&¡ü ‰ŒÙµË´€ð%å¾Ñÿˆž@¨¤Ÿ¶P‰#Þd¥² Bn¾lp„Fa%á‡VÁè~–mtÞuÌ€ðíPT®À>b¤§\x ø§ÎÑ»ããfåB@>#: 0àöÚA¦±H-x„ªÚœïs…¬DÊŠ'Z®@þ¨ÓÔߤ_x µ•7cò¤Å§¶ú4v°EˆÆãSÊsÂ.¾ Ë•€Ku7§P¨Öå·S)\jÀ<áq#9ia î𸆪¨7P „wÔlH‹ùNZYxŒ@Kix#*ƒ8ÔYÑ(ÆÇ,Ì rdÒ9<åÑ:ý`Ǩ¯¶;b"aÑ ÿÚþOC:³r›¨$ô‚‹ý,¡þ*¨ZèX¼‹B!V^£bmp=<Ô² 3--Áœ‹ƒa”cÉ9 ‡æõ19ƒ71á ñ„[(rò4ªª¬•Š9ù,ª#þ"UÍ\ÎH§Øälzú“@¼²ûÊ6.ï ·¨ø¬7ÚGƒˆÛð| "AuA…è9ðˆ¥ ?f!MDÔDÊjO$8à¢@i—†† 6 D”÷™ä¬REhÈ-V…Üf°~€ÆúÊcåhƒ‘ŠM¸}©I&–¼ñ%*‡Íi=!µ+jIŦUvlJ¹;Ä% "drÌDY¼1<Øu1`ˆXæ²ËDlºÃq‰„ú÷y¥*nM"ÒÕ êÝkå Ó"{Ä ? )^e†M„¸­wŠ6,ý;»1åàaI§lÐÚ1³7¡wGAÓŽhP¼Ý­Kü?7¥1RP<硨†´Þ1#qÓébjÈJž’°˜4xñ¤…à?n$ nQãàÆô”‰8µÊ›¹!- eÅbSVæ+£:3"9É„¢½]…Â0Ójçf D¸7åi\ãÚI§@s”»zBG¦>aáþyLDãP™)¼,:i¢†§ÜÐ*cQÔ&F‡š ¼Ãzï¡)×-*«¢Ÿ.ÑJÆ<¾ü¨r¬/àfã“üÈ”ùÈ„ahÒ¢±q›#A7ŸÎ‚3¼r{W¨« ðSYH£Çç¤ãvü¦¯qú`xð æ¯8ø¬ÆÎxƒE ×!4Ș•™piþülZñ›œžBXð)»rïØ);g%¢\¢GË]\*ó¹Y»/oc³.±bÀ“jOØŠWØ”7MdÂÉf€‹ Ù¹¤†MyBà’\rÏ-:¹,pnLºýãVaÄÌ@dƒíàã3Z@þÞ@iÊå1Ð* Hïqr?]r +9½CÐHyÔŠZ™q/b&ãñÚžÜ>e÷W!^q‰iQ;<(ØJ™ +@Êó•†õž) aÁBG4Î!Ø,=*gÄ€÷|cf•Àt.Iç_ó¹q`mÚE§§l <`¡†u^(Eµ)8¬¤§ ÂW¦Ü>*Aì…MT’ €œ™‚¯6oXccÞ?V׸Ø ­SRd ŒÉÔÚP„#·“ˆ]℉2{d+d@Âorôh²ŠNÚX`žq3 õ¯2q P:§ü?j@'̸Á£% ßÝ6n;rónÇàŒ Q¤T_=þ¨Á¾mÜ2®GïÇÍ X¡ÊîƒxÉÍGòý#Æ)½Ã‰ùœÞɪÕ"ê\~›7*——Í^ÙêY±¨Î Û3yM(yŽèˆI³`pýÿ·3wså+O6·œ|úÚ3j$Fa#Ü'_7x¨@­µvÇ°Å;a¡ÌhØ +¦Üg=¥…«ÑÌ ±z VFÍÌ”ÃÐNe˜Ø Ó»¬îši…šèäŒÖV#€ŸÓW"¢ÓÒ›]åöüö&Õ½eÒ1iáx!ÇA)âÁŽƒ+‚@E+Zo0@Ò=9l’á8©g»û6^¾e\s4‘qP—ÍtŽ¥z§`” ½ý 'ó•qã¡ ›ˆÁHÀ_Øp ÚÄF@”n›° kÝ&Üp3Cµ› ¨(––›j( ƒÇHL´â)Ì×€¨ip€ð+*Û­ã–Óð.p7ÀZ“×£A-ê1‚*z¢¶²¶@!¡Ó .?(úÍ yó°Xâd7åv×FFkåxŒ–ªPvoÄŒHàÑfZ12å-‚Ñâ’ŠV»Àû ÌhÀ ˜ +p8ʇ¼¾4|3[©÷Žëqxåq#3nfÓʃSã6j\붹ãìÈ”ÜSñS{ÐèûR*\™²[]¬Ó+êíÌá)׸‰±S  ƒ‹U*Úõ–¢¢9ÊM……ã`þôŠÍ€+w:túŠÙúƉ»žM4Ö‡t˜‹ÖU¡ši'W'£³x¨ËåV lÁàŽþ:ú0†÷Jv¢åΔ‹?¢qƒ +9X`]AçñY蘙Ž[H9•Ÿ?}ùi­ÇwÛ„ÕŒøxËJÈ* 3¤Ão)3²r‹Yð£' w &44 +|«øŸ 5Žß¯Å‚‡µ¨ C°5y"&4 +ö7ex¹Ù[»3ÙܾeÜzHeÓ¹06h[=IÐèA}ñ(ppÔ¤]„¤6ªuC%]!VØpÃŧFÌ8&èä!5¢qúQ:å¡S„¯O°zþhÜó€Ó˜eÊL~ˆlŸ÷%{ÆÐæã¯Êè4Ñ*9Ò"8áK“þk¥ÀìRl´á²£6aÌ̃rBœ·Ó·R6*îæ3°ÎX¸i ã66­Aü£PêDœŠv‚åu_yO̘Ø̤äNR¸•³Ò66 ncònËHå˜Ø#÷®&íŒÞÐ:yX·)›_í€ô'ATÙèÃÇ­“æ!ÃãŒ|Ùy_²%eº* æ‚òÐ5öÃj븉€¥ ¤â«:ù¢ B‡ø£²†›§PÝ<$£’˜÷—7Fm +qACÁËŽ¨#×ˆÖ á D´éŠ·¤sò“&ÏФ lôµÚÒ"'[fbKf*5¢÷NYi°*Ø ); [ҸþÊgWA`Ó žÐ„‰š_ömœÆÂZÑ°ëLX`/üxY·8©Ä:Ô¶Ú´’ è& ¾mÌ8¦AM΀ΡÜyÐQ‡Ž@c9-4©1ØY/’±Ñ3¬Aá˜dWN|ÄA÷LÐPnþЄuDëÖ*ÁYBŠ„0¡â}—/w«Ѹƒ¨¿Œú + ÿSδ³ÆԹàóf7ï$ “¿Ñ›DÄ:j‡ËÛTznÄ +h* þ*díÃj÷­SŽCZdÒÎCþ… =iÍÞ$4à0p‚Ú®21@kf<Æ%gÒí]¹¶qÕ %„€C¬äèB%]Þ(‚ǼBnÁÌh‘Ú¡œ†úñú˸XD„Üÿ95n¢¬dÒˆ…¡ò!F™‚pC\¥‚£¤Š/=1zÐL‹à7¢§T@›vöB‰fFjT‡ß2bþʈiL‹éœ’fÇd .Qce­G9sótOÓlèQ¬bÀÿ?QƒÕ‚ÎÆ k0•‘Ra—‰Û&lÐ}B6cˆºÐ5L¨†¥FD…Fè” *ËîV$ ÀèN7 B:yóè´ ~ŽÙùa°~»à®púF 8$}6Úòeæ˜xßà«„qˆŸj;¥CØ pU/_b;›wð#FÊŸšwqÐ×,4»J92€a$`ÅãgTLƒHmlÐÝL$Àv!Hªìü¤Ýÿ0 ÃFï°Î3ôhõ!| Êj`§,@&4‚ ÚÙ,ëᦕȺø2¨Ü­*ë°ÖmB$7„SP(BWxX둉'Í„‰@xG¸,,‹O¹¹’‹L+‘ÄÎA‡T€AŒë‰1=¡2qÐÆCé~²¹u+À­R–1­SÖzÁ§ÆŒü¸Yк¡býClDMyF4¨Ê@™òžDTZg÷¡|*Û \MGRÙFµÀHÆ°Ôü¸‰<¢A&L„JçP?4fŠƒ,µªµ‰RrÞN†þihâ–#ê)žÓc:ÚYef® £Ý2¤Q» L('ÐQ mG|@5ñÛÔؘE¹¶Ju3; ©=SÊj@Ü‹+'¤,<¨ŸêmÜmcVåÒ2(o§_¹¾Ër yO ì`3€ &oÔJg‘@ñ— гþ"mbáºÊÊÃ,”ÄF¥à¡œ²ðJïÃoyàuüSnQ $Eà±@ÕÁ(§/=\†”ŠD¨ì T­LÎÅå\lÆë/™ñ ¤E.Þsóyˆi:=fãFmÀÌ‘\û²·Ï„G,´Ò5Aí +Ûé ê/¶Ø[™ì„#pXO é) y¼•õ¶ÛÓÄ_1Ó‰¯LÚMÚ ›˜½1“µ393™±R9бI+?üç×ÆëÇ°@ÇÁ–@— q&! Y(½[ àÃzRÍÊ Ö4°´&’¡­¦,4Ä1½'†úê„¿±T]vEú@`!ŒCa0°&E«ÞPýV"?!hÚ¨Õ9ƒzwL뎙HåRµSÒZAÁ¨C®C*'ÙÍW &!U!Qè AÙŒÖJŽë0½3 <¤F‡õŒ«´ž¸mâ–!툱(¦‚ÝwиÎ0*-ÉM©¸*­€&k½2ì*Õ`²“8lTBíô+Dgã ^ØÔÆBÄœl+Bj4‘@ +•KN#þ¼{¤*¬BãñiX¨`yUÌ/°éžMÌØ=&{|ðüïÁW#…gò©™Ìô1©²‚ÅÚ¾ü¢X€é,±n°²"æúP]1.ÏÆšëbn`ôÊ6:­æ2–©pÍJF'lPxˆ PöD¤ƒ…š ðît¼«|<„KA} ÙÆdh¹›líÆ[»®@¤ººÊÊéÑNÅh¹n®Ëíü܉Îò´ +"•|qÊTôª||:ÝÜ U7ˆØ4˜ŽÆ œé³ba—:rù@ +Jbv6TYs³t´ªCüàh° joR.QË3‰ŸÁ ”³Ûq; ñ-KÈMo¬K&ú|v.Ú؈w¶©X•’Ëb¾Ïçf¸ÜŒX‚}lû‹ó$ÔR¸æ –-tÂ%æ<’‚dBv.R^¯-ž¯-Ÿ •—˜d 7Ð`…M´¹d7XÝJÛ±Îi:9c¥¢˜M•£c-°c.·è·\Áf°¾Ë&¦©(øTfÍfÉäõñèt07[[:G%{*·6\Ï,àÑŽ²ÚRU¹hÙ_m4à!‹sÐ&VçS­@q.ÒØ ‹žpÝÎC§´µ„©”“S,ÛÎ怯 XK +F’É›b¥H} °mì„ê¡ê*<`¹P©dã³n©D%ºTb:Rß‚ê‚Í"c-µËg%€-•K&@®¹ä,(m°¼’ìkoÝ-·wñXÓ_‚1ÏÅÛ»Ù…3¹Å3‘æ›ìú3ÝʱêÒ©toÏÁgÑ@—ÛX¤N&ºl¢ë•JraÁF%-d‚Œ¶ˆhÛnù K™þA¬³ëË ÒM1ÛÓaàªI&Þ •W"­H{×-UDœOL[ظʂ;ظ/3溑úfiùBkïz°±­®ô·ïd==–¢óLjžMÂ&näæn÷V­LfÌD‚f:I™ŠT¥òŸ›÷—VssgbÝ£PÛÜL¾½á,W>@±ÑYo°ÍçV‚ýdï4èË•U§ƒ@çr ¡ên¦¦¿ÿÀêé'{û;—|ùhG›‘Ö¦P˜“›ëÕå‹íÝë¾â‚SÌ9ø”ÇŸåÓ=.=HMËÎ÷föïÉÎw†JNžKvü¹™hk›/oøë;dvŽJõáŸ*<ådàwAX3§ã³gcƒÓñ™ÓRy­ºpZ*˜D=X[ãs‹\vÁ—_Š56òó§¢Í-(:-°² h"Jnƒûò ±öÑTï$rȱñº… +"b’’ë¡êZbz?ÞÝÏõOÔÏ!b^; ·L¢/æ– <’ƒtÿt¨ºã Ô&>Ð@py~!T_çssLªÇe 3D¼í”ð84à4› ×·`…ss§ÉHÃN†Ýl܆`ÜBYÌ,”ÏffR½c¹…s±ÖÖÜÞ] à.QiÃDïhsû®þÑ»Çîî¬Ý.fºL¬)76@ðO9]ž”—.å'ãÝÚòh|Dƒ%PQ_n>ÙÙ//ß^Z>›,Ÿ¾¯4ÙÆ%±`ª.PÝHÏŠ¶÷‚¥•Üìi3—´xE)ÓÁ¥Â¤Ä¹dg·µwOk÷êÒÉûîzú;\r†I.TÖ®E›ûnÝ#µèÔ¬'Ô²²9 tùrzO¬ÐŸ›–J áúv¬½ÂkòÉúÚ£/½ã+,©ì¬/Ù­.œsk|~=Ú=é/o‚³Ïï^sˆEËO)dµ”š>‘îŸ).^Èô­¸oóÒSD¬©¬Õ×îho_nmÝQZ<ÝܾÚ=v£¹tþÚߎwvÜb6T¤ºGa"…ù3ÕÕ‹ÍÝkŹã½ÛçNÝG„ËÕ…ƒîÞÕxï8›_,._è<”œ¿(¤ÛáÊ ¨ÙØ8ëpùE©¾¨­§úÇÒƒba¦0·íl²©i!;ð„ʾÜL 0çÏÏ×V®ÄšûN_ +–Cå"Z¡äè0È&L9TÛÊöOç§ü¹Ž”ë2ñ–X˜—[[‘úFº»Wš=º|òÞÞöePK2Rƒ+Îj¬^(ÌžL÷ŽË­}:Þo-ÜÞZ¿ÃBE¡D¡$²ƒS©é£ÑÖNcýòôæÕWž)Í4³>·à+®BÙC/pùuå 7w‰7FÄ"Hœ\[òƒp}U,Ì…Ê‹Ùé}&1 .]­.ez{±Æ:o bÜ°¸pŽO÷£µÕXs“ˆ6£µµäôn ²ìµ„ü\n°o®‘‘r¼»íËN˵Åòâ)X|©4—íïž¿ÿE¹¾ëÐX9ßÙ»^X¾˜>®®’ò4ðm®½¿|âáDcÕJú…D=߃¹œom],¯]jíÞaMÌ.C†·"ÄÒfuýîòòåHëX¤{ilIÅùts=?½aôJzoÂF“ÈY +UÖÈx—‰”‹½ÝheÙJg˜Ì¼ÔØõ•ç –—Ô¨_ë\|ÒÍÇYØ‹ÜÈ&Œ-3{658vÈÎÃâŠÓÉÎj¬µ®­€ªtæö{ñmX  ægšë÷À£±~ +Oª¯•¦·o<ôµ»ŸÿâÏÕWÏuöî­¬ßýÛؼÚܺæË-œ{èʯ‰Å¾ÊÉS2tÄjrúh}ýâü‰û ó§ºË§yöTj 0s,ÙÝMõfÞ»wç3KçžUvÚ³Çzkç‰HŽ+^ìË-ÊÍÝòò¥™,¦°pnqçboõ.îJ‹\ºÏgfäÚêìñ‡§¿©mH@ ñ¶'P cMRnÁŠÙø’•+F*[Å…3NP0#d`WA¯¸ÌŒí—Ç/Ýÿbcù4" K—3³çâýÆÖè&¿ dˆùKá +tºƒ‰… ³rcmzçòàØõêê<ÞäZ¥wüܽ_'B…hy¶²pº¼xžNXñúf´±å/Ì‹ ü¬ó;a/RÓ±æEwóνËOWÎ,m_\Ü»ƒŠVàדÝm¨ùÆú¥­ ^xðÅdc}aãì} ÊXÂéËCXºŽwÎäï*,^ +dK۷ɵÜ|ŒIN3Ù%_q=\Ýjï< U6lLÒ—„[;Pÿl¼Øã¯lÀºÅëgî}néøUýDÄr¢wRîã³ Àêl´³{ú˼èOöÈp]îì„Û{r÷xmã*àÄœ7Ûp‰Ot…ÅlïXiåRrp»TÛñðùÆì)!Y7âÎÅšùþ±òìÑÒ`»¹|hÒÅpñj¦{™>oí‹q™n®»µ{îáÙã×\2=½ nl—/Æz¾â²î={ÿ©»ŸâÓ­qÇ&f¥%!7ÕWJKg홵³/ë§ÕÅSœ\l_^;ÿdëèþÑ{WÎ>ÚÚ¹ÁÆûgî;yçÓànÀð¦ééc‰ön¦w´²z17†IvwNß;Ø:‹‰övjæ X]U×s3'•×® +Ë]åʱòˆ À#ä®'ÜA‚âfÍÍxy¦ÐÙp ).Ù!á¦zt¢.¯–—nò¼,,_£Ä¤<—î"ᆠ+•ÕD—§åú®‘ˆØ¨h¤8çKOWO4ÖÎ…››žp ñ•sÍ3×_àSDs¥µyGaþ¬X^eâM6ÑrJH°šhí ‹v&ÊÄ*B¦'B×dgýÙ~¢±Á'»½¥“ýµ³d¤Zmn\¬¯^¨­ÜÞ\9]ìÓÝë>÷æ;̽ª÷Fܾ<ªGj[rûD´uL,,{„ܹ»{êÅï¤kóX ˜›=›ž=—›¿PY»»¶y/›†–»k“ímÛ¯C‚“}PòêÒí}¼[Ê÷³M“7ì VjÐP ¸"ÝÙ[9~c~÷ +®{[‰Ö"œBµýù¥Dûh²µggâ¡d'œŸs iØðP.·L%gñ`5˜™±’A‹Â¥t´:_[:5wpo¼·ç–'œÂ¤ƒur24‚/Õ‘k[±îA~þd{ëò™‡ÄÒ",f•¨[ï[8õhw÷zºîîfÄÜŒ™Ž¼!>3pù2n_:T]2Ï­¯Þ^Y:Á¦Û.>"å:r}%ÙÛÏÏŸMNÑ™9DÈFr=È©¥ì€ œdÔŸìæOfþ8È ð;o¨©,¹B†àÖ¦âm§Ò{CvRÞ=uŸ/?7¤ÇœLŠŒÔsPàœ@Õ!äÁâÃ…~~zÛà d:[ýýû +‹wJÛåËÙ…³L¦Ÿëî½ú*«o~²CùЇÜóWvӋׂ•M âG|ét{2ìTyù|}ýòÜÞ}³;×3Ýãrc›‹T6OÝ( ötXpTò”[‹kÉÞI±¸è•2ëÇ.§kKˆ£dXÕuq•M ü…Õd÷ œ›í®]ˆC¨¼ i˜T°1q!ÖÊu6‹½4w²`@i…¾ƒì@knEÊ˱ÒR$3kU›;™êg3³U‘@ÍL& Ñ0±^wó*—î™IÙnpé™ÒüÙâüix”çÎ@ÊNT—^|ã0¤Øôq±¼Àß;ñPwûr¼¾áñåO^~¬»rR¯Ò”?¿Ž‡ ~Æõ¸dÄCÈ xÃx¤ +Bmí&{ÇédOç Ú™$*‡²)eÂx¤á•»)P 2Ú4“èjœœ“ +úÓõ@±(o ±é¹¾.ågy¹Rl®öî +×Wðh Ö™xÇ—™ãRóòT#lgª¹‘Ï ²s'½‰Ž•Oú’m¹ºd¥CñRíÌ#KgË NsÉžƒM ”¡P<¡qƒ×àd‰@^ÊÏø‹³|~ U–ìb ækËça7½¡r´ºZœ?W^:›™Þ’M,Ö¡þ)g@lÊÎ@‰–çNegNåïð×6Äâ¼… ò‰V¦³ïÏ-»5Ø#²‰/YœÙ+ÌÈ°…Œs‹Pê&î•›ˆÜ…„î V¸DGHM›ñp¶{¬»}]®o®TçN„Ê«&R–bíS÷†s5"HÕõøôÉÒòåæÚ5a ·œbž—ë{§nÄ‹³*žlnv×ï(Îœë[ ‰ÀùÑÂ|cþ tD(7;Ø¿¯²|9\Zó%g|énÀL@eá‚ÚA£¾L¨´ÈÏWçOw¶®á¦;”¿úÀ DÇBȘTfb-&Ü»YY¼Ðݸbò’¥ÙîÖx´a&VÜïæâVåT`ÔE§ÜT‚f6.ƒC·à’™’•«ž@±Ã:>3aå¾2n·‘ !57jð¨,„ y{¡*+•–¶ï +5– ´dçã>m¡BzN ô¥ÔfåÒ ÔP{¡ê†˜…€Ytò#D…4.xÅD¬2'dg˜t_ª®zH¹œŸYÝ¿kzóH¹}‰Xu¹±x¶¹z)ÑÞr trÚ)d`=Sõ5Ø,"ÒH´`GêË?0wô-ækƒS÷$š«<¢œ:¤S¨T —V£Õ]€‹x%Ñ»X2˜‡ ÒÞ¾Z]8ñjéäW[«—c¥ù…­;k‹·Ñ:D-È€F¨/] Âu³7èfÁLß„ùmDº>_ªôeI©æÀeLÌ–¹•ÃSu!3_š;ÛݼÖÚ¸šêî¼Á);eög#Ãà€gbõXqNˆÖ|‰¦ÊL£þ¢¿8©,ÒÑê+ b‘”m¨Ôîmí_xÔB†ŽLZôvÒCËb¼#¥gdLceè@þÕï¾×_:˜4¡\’òð +ÁÒf¼¾¯ï(,ÂL„!yñy•‡…¥²11o°@ËÕ@²¹°u>Þ]6P>$ôzB®KÆaGŠ0H;&EssbFñ2§„57ãA›=q&£UX´•cWÒÝM>7F*NÆ-ĹX%ÙÙ` ¾h¼&ÓZã“ ™prÞp]¯ï„‹KfBb“ÝDçXsõ|yáD²µÊ¥š:P¬¯Ÿ½‡Š‡Œ^4ؤ“s¾Ì<'+‡x5ÞØ¥ä¢ ¡¹H>;³Òß¹«±v¡8{ÌŸŽäºGÏ^ß8{ŸÊÁº|*ÖÀ –(©fÇ£6ØD6Žðq˜ …ŠÐ©>ž˜¡SsñΉtï´KÌkܬ/Q|"‚yt+&¤2(þ¢Ý™Ôº´f‹ â¼/5ME@f<ÂÈ-èGH©:Äk¬Èí-y • +šlDDŒ7 ½c¨˜™´SV€Šš‰‚ËtöCùEå#0V +áÓ©ÆƸǃù@n&7s Ɔø‹F4`D| )L Ú³´x¡¾z©26ÞØƤº‰H@åx9ÂZ½~øòZ{çZyñB°¸ +ÅöÂko¿ûÄ‹¯ÝyßWw/ÝOFJfT4!¼×—ð%;¡ümÆK³R²+ô+ýÍÕãwô–V÷ÎÞypǽ·_{ðëo|÷7_þõ·øëøËŸ~þð³ßÈÖF Þq#ífs¡Ürª¶«sˆ iÇD6TÂý%;saÌ—ÄÅt²ÔÛ8vñä]î^¼qñ¾'xú¥íÓW›+çJ³Ç™pyLß:bg„xkzyeëÄÁé;.^¾öØSϾþæwßÿà³O~ó»_|øɇÿú?ÿöŸŸ~þåßýÑã/¼ºrüÎÜÌQ+•târ4Ù–"ùd¦ÔìÎm\8qþÚ¹+7zâ¹×¿ó£×ð³çßøÞ/½ö­·úó>ù[?|ûßö×ÿøû+ßûù#/¼¹sá~€±lk­¾p4Û]ñ§*™Jk~uýÌÅ‹÷=ôðÓ/¼øÂ+¯çŸ~øëßÿ½^û÷Ÿ¿ýîG_þñ¯ÿ÷ÿóÿþŠüó/ðó_]{ükµÅ£¡|e¤h²XiÍ,mïÃcãØ© ×îìù¯¿ôÆ·žþÆ7ù›/¾ùÝ¿÷á¯>úä‹/¾øßÿ×øë/žøÚk'ï|0]_àCuÒW +gçÓí]'wyA¹Pï,l;sÇ=ÝxâùÿòÒ=O½øÊw~ü£÷>þÉ/?úã_þòÇ¿þ×/?ýâÓßþþßzgíø݇L¸!¥t¸,ÎsÓõþÚÂæÁΩ‹×üêƒOÿë“_ÿæ×ß|ûÇïòÞ'¿ýùG¿ùÕgŸÿáOþ¯ÿõßЭ?úÅg/¾öÃdcEëâ'-$(§Î%Xñ0*æ|ñF¦6XÝ?}åá'®>öÔ¿|ãÿê“w?þõ·~ôî«ßçÃ_þño÷ó>þýþô?ÿó?Ÿþæó^{ëä7Õ\Œ]^ƒ´ãÄ’bk¹·¸·{âŽË7¿ñøs¯¼õƒŸþê“üìý7ßùÙ¯ÿ§¿üç}ü›Ï?úôÓÿøÏÿüÙ‡ŸÞýè3õù½LgÇ—ê™”¿ÏFN Ê—dü©pªYì¬6ÍÙµÍS¯>ò俾òæ»|òÉç_~óßú‹~ý_ÿû¿¿üËß¾öÆwo<þÌÅ»‘ÒÓT¨I2áéþÊêæÁ‰S.Ýu×µ{¯?òØ£o}ç­ßýîw_þéO}úÙ{¿|ïå×_¹xåêÒÖ^ªÖ .:aÃ$‚—!ŠÄ+ÕæÎþÉ+÷<»u®2Ø¢e9ÛÉ6çRÕéD±9»´³¶½¿½·{õ®»¾ýíï|û{ßÿþßùðÓϾøóß`;ÞýàÓÏ~ý›¿þí?>ûâ‹·~ôãk<2»u<\˜æä2É ÁD©>=»²sììåýÓç÷Ÿ¾rýÞ×Þ|ë—|øÁ§¿}çýßúáßÿèƒß|þù'Ÿ}úÙgøñG¯¼ùÝóW*õ×Q6Ž²‰@²-/{}YÊ—ˆÆ‹Õzwkw÷áG{Æñ£Ÿ¿ôÍ·~úÞ¯¾üÓ_þò·¿ÿòã¿üò‹_|ðÁ˯¾ú⫯o¹"¦[Ÿ±âÍP¾Éwºóƒ¥ýSç{ú¹¯½üꋯ½ùoßüöOßûå_ÿþ¿~óåŸßýàãþø‡øòËO?ÿâ•·Þ~èñçªý;%ëœÞÆš¿ƒˆ€D”:kùú`ëèɇžzî¹W^áïüäý¿üó_¾üËüäý>üôÓ/ÿøÇÏÿÅ»¿|ÿãO>z÷½_<òô3ç®Ü—¨L3¡Œ!uvBŒWƒ©Z,ÓZ\?zý'ÞxëßòÞoÿägŸ}þù—úóÇ¿ùÝ/?ùÍŸÿòxw~ú㟼û³wßÿѧŸ?zé¾ùã×¹>n$,ž Õ-¸¼>£ Ç(©5X;~îòõ‡éï¼üæ[ßøæ·~òó_üíïÿõøÉ{¿úÅ{¿üà£^xåÕ³w\]Ù:ÊöòÝý`8[o *Íé|©:½1jsfyacgýòµ‹÷=rï×.½x{¾V#ŒáìëÄE•ÐYqƒÀ¿Í V{¦;3ØÜ;º±¿7¿2âìÑë7î¸ÿÁë®\½p÷}Ç.ÞÕ^Z•’.Z æç¹XÊpv‡>Œä9!¸üÎÕ½Óé|¡Q)î»pöúý÷<üÄCßÿþ÷Þûà×ïôñ'Ÿ}üÜË/¼t¡»´HdÅDƒèJœ”Ä{—Ž_¸·Ü[*¶zƒÁìúÚòéÓÏ?ÿäÛ?|ûÃ?ýì·¿yûGoóÍ×_}ååçžùêCܽ»Pï-KàhÞÃŬDh +¹×Ú)‹‹öùÂù\fgsëëw¿þꫯ¿õÖë¯ýç?çüý‹ÿö¯w\<½¿»Ñœîå7%èì^¯ƒ‡‹;é0Æ… VŒ&óÕfuuëâíg¾úø£Ï¿ðì·¿õú/?xÿó/>ÿÓŸÿøÞ/~øì³O^¸|®Ü¬¹pÞEF†uvöˆÊ<®E&ô¨‡ÉñZ,^š™_^\ß8zæöÛï¸|ç•k×î¾þ—_üþÛ?xõõ×¾þÒ‹/<ÿ/¯½úoÜ{üØñL±…òÆY½"ÂD :U™—âe.Ä’ùÍãw]è¹¾þø“ÿr×Õ{Ÿ|ò™ýøgßûÁ÷¼q×ÓßÿÔW={þÜæöV{z.UÔNJ™þÍË¡iðe+*m„ËÃeòõíí£W®^ûÆëo+=õì³_}ü«/½øâ;?yïù¯ýÛ•+÷¬mîëm’Aœ1c’ÞÆè-Â81ÎCúüñJ¢ÜoôWgVö0Q²â”ƒäÍoDXØG'œÌ‘1½Öˆº¼’‡Ž80£B8ò’À±R4] Æ ·y0;J|÷…h¹l$B“6Âèb6ád¢j'1¬³èm^„ØÖ…‹œ\ö&Å°(…1¯—e™t6]¨–{sóÛ'Ï-líF³97íÓÚñI¦uR&4`BüZ«w±l(HÕ 10AJ”#±T0ȱt.“Ìrõvgq=O&RévwºR.KbÐñBB™ðg¥ü,ÈEe1¸}~¹ÁK¥P8S¯4‹ùb2&ÇcòÚÆv§S+•2þte«g›º6燊JuTë‘À•©^LÈyð¤Ô—‚ ˆ¬Y‘Š[Vû,Ó=­n<• + >"NŠ…ÚjW~õ0íh‘”íaoëImõÊ×" Žh¡º¥RW# ~Êk‰V/ÝØ·Ê[ž3·Dˆd‘:WC„›Nu›6™”ók7—ÑÎðMWpÆžsGB¤‰ŠÅ„V3ê[˜P´KkÙæ6¥×Hµ¡ÓQ6—+Ó–¡LÆG̸®,ø§!`0:M)*Y‹ó¹Rë Ø?¿îˆÝt„Ý1S*|v•µ‡¤V #¦ëý}\­Ì¸b³îè‚ ªÙ8”Ë[\fƒ™*uåtÝ äÞ‡§BTŽ¶F™Î½™õÄýoG!"é3 ~¢.HYŒÑdîû%”²(mBàÁõ…ó"Jœ/(Ùa\ÉßtEýqM°Gbf€Ie"—N$9çKD(æ>˜ñ\™õ-»bP‹œ~3· ºš!´¶·Ü¨æFTT® ¹u6=¦RÊ«Œú!"”!l\ˆ:ãDÄdµ»ö0BZWfýˆPÀ¥ +£V« ï,xˆox=~buëÊåæüt\(†Ù,&W…̪Ý:eôÞ’—^†A#õy:» Åe\,ÒÓé¦#!»Fiík‹HÔh-iù›^t)Ì¿ïÚá²C­¼Q[»—jíj‘2[¤Ös+BnO6}TƧf<‰9)6]ã¿ÇØ<ÂåÔÍêMÎ\w¢sžx€Pcbž/LP­±ŒÊRµ‹Ýõ£GTª; …'£Œe¦-& ªÝ1!ˆ²‡çO‡»w½q>D(Þ¤ÒÀå:môØÔþ“–òÉLïúbhÑO†Sž¢SƒduOÌm¢\!SÛ”sý/:•`TŒ2)„I3z “*AÒ$˜T¥³É¥Å 3çÆàúÈ´°½˜©–Öý¤ve1ÄÕ8g¹B”'BCRÏÃE±Pu ©:퀪0ÎQ&ç'¬nt9‡…Š”›¤[»¤Vw¾o3 mø¿ŸÃ¥¢#<]tC'[„XþàšóêŒÎT̬Ñz/H¤\ayÑCMPrŽàÈ]qÅü„åJ¶¡³ƒGåÑQ˜Már™”ë8_$„.”ý¸ 5³¢‰ürX˜6Øa +fót|ëëÂè¾7=¨DiU¨Ë%J³'eÉLW0ÛœÑA¹¬;*…ö’Û?³]pÆoÀ¿ª±œîºÑ$¥Õ–ì3Âyb„åÏgü³,DZá„õ³+K³Ëa$¡ÛYóAyÖϸŒ^qà n{·^Ž_\$c"fô¹Ò®Ö9··¥êe4ûã[·ž}sKˆ°„È:åÊ”1‹ËqjBÓfÝ (!WH¹Ÿöx™¶¼fÌ›˜šG“•ˆTÄÍ“é"r×|a]ož0©~ŒÏBèÙ)«ÅÛ­(ohÓkq>;7íߺºòb êÌM\œÞ¾Mí &ü„‡L…ø +¦õi³ÏY]Îî#¬­úàÒ|x×b\J]Z¶¿{ú’TJ7]È‚‹RiL‚šÜ"“mD*;#,ÉY'÷ÞZÕÉõ¥Ð’ ¢r$aÒF›ÖÛ¸ +ÉÙ]DÈ^wFg<ØRˆ¿Šˆ%!; +36‘¬Œž³ÙÁ´)Íͨg”ÍĹ¼œå oLYt¢qÊ$”Ò•¥Øûzu S9%»b'´^㬖OÎPÄxO\AÅ2mt +ËÞÑ+BìSš«gÅÁíp"µ`aÌ— Ʀ<è¥8«q`4Â|q9–tà&i²ãûͧŒÕq!I\mÁ˜JÅå¼ïðO{BZ][ŠßX/iw,  +"&tOT\ô1‹AJßäð¯×þú†{vZLt1µ‚ñÕ÷ë»í«K\w„½2ëYéƒEÃG˜‰Ô,Š]]ó½(Ê +KÅX²&×v0³“Šbº¿}ë““W?„„l”Kó™VÚ6JÛ©Ú—_¿î§I>7\»ÏíçC‹:LN×W*¹Iw÷eqxÇ* Ž¼ƒ ÂôšÑÚMN3ÃóÜÊÝÜä’ÉM©* ¾ø基{!%ƒ¸â'´ e“Z[¯0ÖГ¤±8í+›Š‹T¬E…¦´¤ìšT˜xâòŒ3„…¼_Ñ m„/"|Të¬ÙÎw½é©&!ƦßO±¦Òƒ°Tö–ס-…¦ó὘A&›jq-!•h¹$¥A29mÄg ®\J1*B”ò˜Z%õF€²–#ÜRšéô„T"•Š˜—·Ÿ…•êŒŸôaZˆ0´ _áÅU7–Ä¥2!–@¦ç¼ÄßÌ…®:Ð…ÀM\n±æ@ÈŒ R£‹©pŽy\«Ç¸¬×cl&^×îyb"#f¿üŽÕ+Ìøf]ñ®ƒƒ™D¹ô¬;îŽ +”Z¬‰ó8³¾ê3…¸”¤ÑBÀ çF1&3ë—ýlˆHÇù2£8sŇًAe2Þ¤R[òËêæ22]t†™˜ØPŠvcLàb ¨2íÅŠ[K~vº6™´«§W6á0¸dI´ +‹d)D:¡êÒé•"’Õ¨˜wa*iµµìcl¹¶ÍM§ÿíÖ××·ž탄Z²«ëÉê*dbŒ¦tW\¾Hàb¡:8£ŒÆõ¥h$‘‚DØo´òý­º‘kn>ù3ëdª^X=¯l>¬n=ÊŽo§úg¸ÞÄ”Bw|ôÿüߎŸ}åÁ•(e2z“µVý$Û½H=ÑîçA:3ïc¾B¥V„âžÙ¾[^ÿH­;Béjn‚j*‹Q.ÆçX«Ãeúx².Ö*½ÃÇŸüÐßÿÈû´ÔÚŽXÙ¥³kbq'Y>gâÅ5øÍeG˜4¥5ƒ˜å‰(ÎuØ®¬sFb{)Èb|¤,êó™QqxnwO"\ƃ +5p!@sùtFÉß[½DªörÞ—X&¤.æÆ”ÕÓ*;Vm/Êf¯.EnºÐå07ëÆ J¥wz›Ô:1±B˜]&3N¤ú> ¿ &”¸ô@°»Zn0X;‹PÚœ+Œ+@L„Tš<ëÁo,G­N+eG0±äÃ! …W«®cz]*®z‡H6”Ü—+ÓNVd&Ê–ÂT&˜ÈzPÓM"HF7SßÝC&u'ž # §sÔbln9@‹VÛWvuùÆbÄ‘p±|Ǧ:PÉ£LÚ(®ÈvíÚ¢g)€ûq5*¯”·´Ú®w°d‹Ï­W¦;ç@²×—‘[€x9`ŒNŒ«\ Ô&—êB ‚Š±ä›výJ‚º¡R%…=¿ês?»º´äÁæ]q`[Wˆõ j"Ù äjBmb|Á–A +L x]|zä»s”ê ¶=ùôJaLhµìðŽÞ8ä²+͵‡ÍÇ™Áù2–œóâ¸Tƒ +%”ÓÛÓpòÓ‚YËvö!ç J$ÆjeÓì‹¥ Éî Ö/~ö'‘d¬VªsRÝNwóã;¹Ñídm7H™¼ÕLÇ‹}A 0mtÍÆÑøü³ÚöÓi Ü0ƒŠ9_Â\Š‰sA*Ìfèô8Õ»X¹õ™^^K–'éî~TLÏú±(g+¥ÕÑ­~þdzW?4wŸ$¬f¦ØÝ»xÃç‡3!\ŸÙÚKµ({Õnw¶.x»áÅôLÿ®ZÞÃä—x¦Ýê”bï¨2>_ +1WÂ.D?Àg ³5Ýlû|¯—RÕÒ¸¹û¨wü¼¶u¯°zGkì“V“”s¿ùÛÿrñò›0kÌûIµ²Ÿ[{šYyÈg×–BxÅöèxûüÓŸÏÇü¸Åd'ÖènyÿeíàµsËËæät统þÓÎù«0T¸®”7‹k·/UÛxî‰k7@ΕÄÀ >bq4¦Ó¥€Î|QãlL*´J¥õÜêýÊö³á¯Vîý4âçê"ښܓsgTš¶¸<ÁŒ„Þ«o¿Zö¡2øU&Y š+*‚sáv˜Ÿ.‚—&­kàÜ8JÙaÂÀ+¤J”Ê:à•IX8˜@}1H;ã*\ô¨!õº^Û)ŒnÁÏ@…ù•ûöðBkÕÖAM›¶Ã 2¥sf-˜Ð=˜êFuGD] +Š"þcö!Š®.Gác•Ê®¶]h hºÕŸ²Ã(nJïnßk­ß¶ê[jmHãZJ„Xܨï|æ+ËE.Mï(:þÿî +¸9çgo8±PÂ,t‚¸úáõ¥®-{`ôŽPÚN¤†!&{ÓÃyˆ4"ÖæÜäÕ›^WDö šÕ£¢lÎ3]°ŸM÷Ž .›«oĸ4ˆœ‚Ñ9“ª‡\~ÃèžzÇNí"xWT­Cî@LÂ;î¨èŽJ~4 ØâCT_L_úábäêbˆUÍŽ¦×—£ó>r ðŠÍF“ËÄ\Ï( jãs€q*Yîï?ë¿,­Ý LÉG„lžv'“ì.ʤ¼ÈÏnÛÝ¥Õ§ÝýO#ÓæÏP^JJ~ ¢éˆ L ±.;‘Š¤Ù”h®×ǧq.…$’ùÎîêÙÛý§¿:zöÝÃÏÿ¾<¹Ï™Ýˇ_¼üâGʪ‡˜TH(ÖÀlžÔן^Çøê‡KÈu¨!BY*mˆyàÊd÷²Ÿ³ ,Ð 1&äƸÐt?.•Y½žëîwN^aZÓ‡ÅÞymõI¦y4Ü{:šÎn¹II5_|ýg±¸6ã¡ýdššòàžlCÞÕG˜¤ G”ëhÔ÷RíÃÖöåÉóï&go«§äVÎ}Žr®K¹ÕTc +¬Ý:¡ÌñœŸYÑ„˜ úÔÌ;1?aÙ“òÆ}ð6D²¥M«²âŠñŽ(¡µóøûµûßµ?-o=E“Í+‹(Beÿ:ž,_s" aÁKÂU(‹Ìöîfû÷o8ã×ç}î0ŸÞoA–¢ârL†GÕ¶TÜw +ßèðãaR]ðMÛM„Ù\\mÐéÁèøÕñ«ßùàŒÔr8aA réž Åu¥~(7o!Jå²Åö>¡äCd’ÑœÕ×t@ÕgôʼïÆRèêbhΛ€¬Ÿî“q[H§<†ÖëBnh4·Íö^aõ®\Þ õ¦©ØCE#.ÞÚŒ^…_“2C½¸$m"’B&ʤ %„F©U\ªB.„iJ©JeÖA²†@ñ ^ òT§ôvº¹'eû1Úˆ3†’kŠyÈ”õêö½ÎþÓBÿ¸ÐÚß¼õ2×ÝAå¼V^Ï nS`›ëûvï\(î†i€¦,ouh­¶b]À†!nj‹+w„ÒD¦|a^u>˜÷pUÍOŒêA~potþ¹H©ÖÖÙk¥ºÀïwRU/®Gg3~fÞC1Émt–#üGôª#ꌉ”ÞP +kÆâ’^`bÞƒH>pz ÝÇXPŠ“;>L[ŸÜ%”ªUAðÿúrÔ¤q1늰Óì@èrM)®³™¡XX%ÍN"Õ­¯Ý¥ žË…úÊierÞ‰*5/•ò’:cõ’å]³}æˆÓBf¤UÔ2˜cÖ9cÆ•¥¸#È6‡£ÍÛÓ6q^ÂK´=°z§…ÕK¨ê×ñd¦Ÿ­¯ÎA$Ü‹—–rC­·šGûÍ•“ÓWŽ°Ä´iÀ(¹BLWr²©Øߣõ*DšÒ K²é¾Ñ=©ì½\;ÿ¬¬o>úçÿùÿÑŠëK‘$¡ ”òA¾zçÕïO_þÍ®][Æ:Íê-Ȇ÷%’Ëäí59¿)¤º`Η X8Ð)GTX˜ +K !ÅÓåÜ +mÖQ Úñà# $<ˆá!à€Ð¸\gÒC&ÕÓ*ë¹þ±ÖÜ2Z{Ùñ…Ò8`ó+LºÓX¹Ø<ÿBÌ–£’5BT.!×€X˜‚èEeý…½d!„™A4‰&L=×n®¨…ae¸÷qº·Ÿéií#2½Ëz¦»sü|rüi”ËÎH*`b‰1»„ZåÌé\kN¯æ›[(›šÞT'-"ÙKè ž GL†t„Ùå 9ʻ০ð)L-Ñ©&—îT‡GßýøoüÿÞþ#Újm½±Ûظ, O{“»«;—’]—í¦˜îñ©ž˜]Á¤†3¢ÞXÆ®-NŸÐAh¹#‚—¾¾»¶Zð`Ë>àú´’µ×î­>_Œ²>Òs¹›f2½tç$Ó=³#«Îä¬Ô;ô㚨Þl^æ¹úz±¹5»›wa(ø©N˜‹>ê|˜Ô)¥¢æG~T‰R)HÁléÕMa:§®I]Z­´WÏzwÀQ&‹9„±}11B$ÁúFHÍ(!=çˆDI á2´Ùâóc©´ŸƒÐF¶Ò¿xüØõ„1Èvïçz÷Ôܪ]ÙÐKk¨\q'x­VlnƒÅ½¶vF”0]fSã£/‡'Ÿ³öpÖ“`õššnùbü_Ïx>XŽÎEO<…‰u­¼s!ÖôÄ¥+s~xÍzqB«™€>Æí[Zý€ÐšöteôØËjBqX»»ùhëâ˳~ïÍŸv|Kj*YÁär˜JCÍû7–"ó.Ô‹È‹A“K`‰ÃüRˆ÷#"¨R”Ñ»RŸÜºõä«îÁ¥ŸÕÇÎIq|iµ§ól‹«ðíÓ†J#L8H^²µ<¼[Ÿ\r™Õ¥KX¤Ú ô.&C¥µ–^Ž˜ +ö†Š§'Æ/øq?&iÅÑääùæùÇÅÑñäèñÞƒ·B®MªE­²&Æk'/š¢|.H*bª¼qô$S_ +“†ŸHÉt”Γr%B¡¸:ïF¦íø®92ï‰ÓÉš%½¾cÕ6½²uë“úꨘ¡²ýÂÚ£Úö 8)97Z=|^î³ãRÙ¬m[•mÎx_"ì¬ ù›kno˜‡pG©« ÞY>½oOà3Þ•¦w‡"üM7¢,B®€O(ôN”lbiÞCâ\N²šŽ@bf)@å"ƒñˆ3éy¾è‰»‚ð ¶3LŽÌx °\²È*YN/‘JI/M|ò»Ý‹w`ÛP¾Ä]ÐôLû„”Š¥’BÊ@õâB “lÓj+YØ@…¢#@±Éœ‘mƒÃùÙ €Èœ7¦Ì¹ñ/±¤<ÓR©Ì8¢KŒJˆHÑ@ráZ-&±@ê-H½ìàTÊ6÷î½Tž¶7î ?jï=5[ ­šmn +é–ᣠÝåA;–}xœ¶iµ*gúàǪ£ÛA8S7ˆK„RN÷w|qòò·Íû_ýêở”Ê:"•0¹—*ŒÕ×*{ÅÕgý·Jm‡R˵ñÜðL.®¶„§Ýnu?™‘3ëWüùBèÃùÐ oÕû xGiº§;¢* ¥šéÆ¥lˆTíÚÚѽןýæïŸ|žê½ ÚÚ<ÿtõì5—i…YC)®µ7î—úGÉü0Û˜pZÁ"ãL—+~L]s`]Ň™á„r¸î×惋nÌf9£)g'D²ƒŠ=×[?zdUƘ’Ñêëý“×ï~H˜_{ÌæV=h2_Ý<{ö]DHÍ/"‚ FèL”Ê8|7E­>Þ¼»yëã«óÁ+3Œ8_K›Lª—D­ÔÙÛ¸õ +x9 EL©Ä¥.ÊT2]\.,úð?õ¨®ã +³~DYòPÄ‚˜Æ“3Kþ.%”¼'ÊC¤:[«'€*OÖay|*UÊÇñË©ƒET7H@w…„P"M§„\šuÅ!ÂDÒH¡é3Ê J™î ¹äÅçœ(Á§rþç7\\_þ›kË×æ¼ó^<€%E½‡TíïY¥W€ôŠH’zÍŽÔê¶Ñ>®m>«®ÞYß8ýçÿòîÝ{ Ì4d·÷ôú6PªTÜŽ'«>\¥ÄÜúÁ‹“¾¹äŸwÜ!Â9ÝÝ#!§Z¬^ÝqYJoÉåIeõVkón©·ûô“o¼ùµ\âZ=Õºe5Ž*+—«g_öö?îl?•ò«virx÷5“*yH1Äf"Ó~b 1;aí?™rÄ$œ²bÉÅXòš›œ ÐÀYzu_-ï°Zýèöëöú9—n©•i;—òÚãþ᛽ûßœ¿ýSyí^m|ròø3µº#çGbºOªUdº{•ë`‡ ׃¤O†(›·V¥Ì~”Î8aÌ1ƒVê‹~êš“aJ Ê~PC&óÞ„H£‚+«›ç2£ñáËÞæƒrg¯ÔÝB¥ ­µ\ß;ÝEóa–;¦â&ÁæjÝÃÖÊík ‘@\Œ$’î©?Éf7ÚˆPì®_ 6ïAÈRó]9ÛãÌé^b¶‡)ŒË•[›•îž#H,û‰@\36e ®,{Ig€Ô³ítyä0”˜iožï]¾Û}øµZ?ˆ'Ûa¶°qôüïÿíËWÖœ!Ü)•9£ÏYcÚø¦›zЂÙlo?Ò‹#W0á´ì!À, cÓ§ÏÓm²`°«ë…LÓÊýµ;y@~µÒšœw·î2VÓK$!QÁ›M€MgD’ à©f£’°ôæÜ$T{Áª/…¨¹ËeW»/2ÃÛRa5Äå‚” ¡—1>—Ê`l >k¥ÍiwvHî eÄaÝaÞ ášÃÏtïW®!LJ0Bª¡WV'¯Öo¿Û¼ý‹Ý»ŸïÝÿ¬·z€³V„4 ±%õ@|ºgS\(G¡Fñy-¿’‡/Êv]D$®Å¶'È ¤åËa=!ÕôÒ.POœ3g<äõEÔ•Ý%.TÄÌ +c´ôÜ Æ˜€<ŸŸo¶¶ûOwî¿Û¹ÿEçäUDÌ@Éíí?§Ò}T­bj ‘Ê„Z§TƒvoóÜKª?¿átènycª;Ê{>Ÿ^ìÜ}‰iE/mé­±¸b6·+Û—©• 2ÕáŒêùå»Í³—AÒˆÒiRmê•ÁΓÉáGVm‡^n¯ûÃ?ÔÖüÕuïb€ôâjÎåÚÊ}¹° ôíó7JqåŠ#6¤t&€Ԁ‰Ìúf¹·»uüxtø$ÀŒÝЪ«jqÀ¦›¥ÑñáÃ/N?þn¶ÒÃìðK6Ct:êCgüT"-Ó:ȶ|„z݉pókï7Z-øñÔ¬›îc®^s…1áÒJyÓèÜÊ.ò£»a&›­¬½û~-åûzsK­oÊÅ Ôí0epU0;ÉÃD²5ÿÞ·{£"`µ%a‹à¨ûk·Ÿ¼ùžœÞ³øì˜ÉŒC|™Ë¬dzgk_ª¥ñxçþå'?4Ö.¼qÝ—ÈL÷¨bs¸0}ÞŒ‹ |޸䂋ˆ3ʉ$LN)í”·S••þÆõ‹OŸÿêàù/·|.—&^ÒH$+ÅÁ­ÆÆLÌ v•* Å›q„gPðD$Oˆ‹áÉbcG±{\wÏ9"ù"¬?"øã†3ª’Jmëü ¨ÿÿpÍ7ç¡À'ó™µ´¢sÎE=¨¢õ£cJŽËuÁºÏÞÖv^Ðùu:ÝÓ;…•iKdð{ïçí§Z'øa!³™öïõF¥ ª8ôÕéöU‘Å@"L›!6å&5G\ⳡØCÔŒÕÚî¿ÊÎ ƒ[µµ{åñ™n‚ŽL°4i½‰ +°ÓéFµ5:¼ûü+97¼:@¹ üÀZPŸ@"bòˆ˜·²|ÃL÷àF„ÍòCôBí/5WwO¶7OԞܟýb÷ò»í‹ÏûûÛ;—Zm=U}ûÇÿøäÛt“V”ÍVVﯜ¼+.‹ƒraÝGê@£©Ò5êËñš“ºî¢–ªÈÆåFª¾}ïÕwßý.ÆZË1Ñ“0ÂB±{•ñYmõ²8yK6b¬mw÷¡VGÅüt¹½X +$Ì •jl>™nçá#l&.×P."LPX`€ô8gešë!*åE“¬ÝÍÎ6Î_žO\¥Ç}õ§ý¯ñÑrDN$Û”Ö¦^”Í{âNF«aBv>@ºã2c¶a¬V7?mËóe®s°}pù›ÿiíøéÊÙ'k¾®|\Ýýhåö[wÞîÜySÞ*õöÿéßÿï×ßþ]Œ2%«-¤†AÌHˆ ãc%Õƒºqm 㬶Wn,G¯/?¸æ¾± ±%>·ÆÙý\{_°;¤Ñds+tzÄæ&byÛƒ)Lªa·¶!#øl77:mî?½ž¿ëœ¼M¯ÜJëveóüá×{÷>#Œ:i´sÓY%§l~NmL·w÷Ó7¨kú¤€÷‘Î(HXaÚyÆnïZí­ÒpoûÞ§Û÷ßÝzþÍêé«üøœ´ûQ¡x6BžØt¾möj“ííg©æ¡7\ˆÝ!õåãÞTëtj,—RÍ.Ý[Š‰Î˜s5Ž( (æ†éÖN±¿ÿÊù!eŠÃãúúÝêê¹ÕØÍôŽ2ÝC!ÓOÇ÷?™œ< +3à[®4me“_IXýéN£DŒHÞºÿ¶ÐÞuFe°•°xA`g›[^}×ݸ XD¨U¥¾könµvž_|ü‡gßþËèì T®5û{·.?¥’%ˆ±\çVsç“ÞágÅ•çJqP„Ô.QêŸÞp“  °a>¦ÂÚ>*N.¾Ðõ7/à7ãb1auh»Ÿmïƒp”'÷Íê:Â\cSHuæü쌋˜R¹Ò೫jaÃëŽ0ËÛâÊTÓ ¹Œ+eÒ¨†CNwŠ½}Þªwv>ÿuyý~º³Ï¤Z16MHyÀ™ãÇ_½ûá_WŸÇŲœÛH¨M_\õÇØé 'vs)>ïa“Åå0ó³kΛtÉGyÂbˆL“Z'9’HÉv> H°´~óáwG/l¼Š‹…Êø|óö»‹?Ú}xï“ï‡'/R½ÝöáóÒÎ3½w¦Uw7O^_|ôÃààP›ZÞ*ŒÎ3ݾ¸áË ¾ÄÕ…à ç2›òÆ…8TÈ%Ì&“jëÍm«¹µ÷ÍÅëïÖzntÐØy8mOÔ;-¯>éì¾ÎöÏBŒa¦•!œÄüŠ\Þæò[RyŸ²Çq±L˜Ž—ŠñdÄ%ÕQ­ê| +€ä +(ž!Õ‚^Ûln=é~.âŸ5kíÍ °°q9cµ÷G'¯O_ü°}ùÕ=”ŠD4)5»zøLH×£‚I¥:„Þ‚¡H¨e‚Ï[[za" ?šô¡ÓÍY‚‰t0a›ÕÍbÿtäÚB8Îçp¸tWH·‹Ã£ÑÉK»wå2û‡ß|ý“šiÆùLcýIuíI¾w®wœˆ´H óòÙ×Ñéìt“Y•ªb~Íîܪ¬?.Œï%¤ü“WßÜ}þ @ø i‰ôßÜyýÇ£—?éÓ •Fóû?þËÎés'ªÞtânT“ +ÛFã„L¶gÜxO¶&·óóy?$Œ(“@cR]½¶›¬l±zåò“_½øâ?èÅÕ0e±Ó[ñ›ùÞÉæù'÷ÞüØÞy–л„Rì‘•¯/Gþæ†ëÚ|Ü |.ƒR§>œ ãrº²é +‰ nt3Âä§[Ž¢FB®!t*¡–„ü¨8¾]™\FO²[Íõû­GýƒÇgß½üæOõõsµ¶~ëÙo.?ÿËæƒ_‚^|òÝ¿o}*æÆ¥æÞþ½/´Êê2"ú“3Ú´’ÎuG¸XB·óý.8­âE¨ ¬ÝɎΊë÷G'÷ŽžÉ¥ÁúéóÖÞãôèÌ+µ]:3œÎj.Œ@¹”ú¦Ÿ·­Vê@0æànXï¹Ù˜R§2C½¹OÖén"3Âô.mõ­E¨5?f[!¥&e)•í¬œ>¾ýykï£ÊÚýLê[×,¶îª5¶ÌæÎèögéñ]¾´Ægû@¬ÇO£ŒæŠñéö±\X£’u%7ŠPi/"ábVËÀ)}8™qb~½ÿöÇg_ÿÝþ£¯[»ÏŒÆeNçv²öŠ—0Q.ÂdBLª"kõåü8¡7¨TßGØŒÑ QÚr„oƦß_¬Tå ~" ðÕßy(æºvmTìf;ûri›Ð{¸Ò$¥J4¡Ï½ßEB"Lêð'D™np±£lxó J„R„ø–²#peóÁ„“Q>$¸—ìš^^él_Ž_JÙ~\‚ƒœ®éx?ãŽô£r €W©(ÅÕ(mûPÀ4Ñ‹jž¨ä +³ALöSVi|6>ù¨¹u©×wQ¥êŒŠŠÕ2‹+(—eôzœ/’R]É I¹äÃÕøtéµ`C˜~c¹>¾6\ö3eÆiƒRKàù¯Üô{#Ür€úp>xÝÌG„w‡F+Åy;.¤­Ö^ª}Ì*vìÐ訹y¯°r‘¬lðVÃ,³ÝÚÈ&TCÊ6¹T3Ý9Î ïëy1bXLõy«=ç!–| 8 `LÈ{c"ÄØœ+D r2œÝ2š»••Û£ýÇ[Ç—÷_}¹rxY_»³÷èÛµ;_ž<û~çî/ÊÃc5×Ñ‹£jïìÉgJyb6v{/ÖÏ_CäÇZu"ä;¹úäé›ßÜýäW!6“¬ìˆ…u³y´}ù«ÍË_úÇã³ø×ÿõòݯ“ÕI~pv»{ððþ§¿ûÕ_þÛOÿéÿúêÏÿ~ûÉW?þé?¾ùõ_Œö¾ÑÚWª»ÙþáÑÛ“?Mî|K]N«vaðk[^\÷`ITÊGÅ¢TÝkí~¬×wâb¶=9挆”'k|af’µõËÃ'¿¦­Š’mª…e÷ëëÛ{Ÿ$ë'1¡Î_*nrÙ_\Í +ÂeA[xêýæ,ÖRX ÕZqpÆMÂÀµFÿð“êƱ¸ÊçFŒÝ +QÓ.4Và&¦h½ÌXM6Õ”r=Ú¨ƒ4Ð`¿Î^zûPÙT/œ0=> jbõ„—^ÌttʟЈdE*oæ÷Øô8‘¬±J‘U‹˜˜—3=/¢,øB**¹!mt€RÁFIóÆ +Qç²1ƸˆR)G"<“ !ÿ¹ g=Ä|óVŒÏMwÌQKÉÒ¨8:‰r¹L±nLh£lTVÔòPR²¸–iîÙ­*™á¬”kbRF.¬ªµ]`Cð9”Ú°Š›©Ê†Ø?&Æ(#D¨ž˜8ëŒ]_ DÄ)Fk"Œ "B™ÆäÎïÃÏÊ«Ç´YÔKýÑá3»¹žo¯§k”^Å„Œ”î+™^ãr!ÛÙ-Oøt‹Ô›1”.+fz¾“ooꕱWóýc½¾•,¯÷w7'g™Rg0Ú|õî»ÑÁ…’m wï]¼þõã/ÿâÛ?=ýú§G_üöèÙg«‡Ïß~}öìëáΣó¾ï¿œ¼[?7Ü–ÜŠKùTaðÑ7?;xú»ÊÆ£—~øê›ÚÊérL¬v"Z„+âj«>y]ÎôûkwVN>†@-­œ1én„ÏÓV›Ï Š«ç¤Õòa¢œi‚ŸdŒ\‹kaJY.Œºû/}û/­í‡æÉÒ˜CWhÚ8‡(`QÂpã3Úh°Z9UÀé”מȕ]<ÙŒ2ÀÔ=(ÎbfÕ€ zyÕjl2V"ÄJó`¡‘D²ðyGXâs8ã&缉¥ »ඌ^[B¦ë8¼‰ë.ÂO§¥â$YßQªëRaEÈŽ¼˜©˜%\Î8£LŒ·(£–jîgúçvïŒÍôx³’®åB/@ÈZqµ4¹Ìî–WPz3ga~R·Ûf®ë²³ÎÈŒ#²èK “R Ê´\©¯œÅåkTµÒ¨°r*•'ŒÝ„1)­Ü#Ôœ'FA5 Œnºs+Ý»=íyÅf@DÔ˜4PfÃT:¡V”üЄBaw ë=ˆ¸Hb6Ý16Ʀ0ÖT´L®Ð*ÔW8½ ¦*ávg븾ºiM¤|G©ŽíîNeext 04ؼ}çõÃ[¯`$»•²1Jbl®ØÝ»ýª08VMæ'éêŽ^Z2£(—Å))iÞyŒr™„Ñ×jå•{ëŸï<øbåô£¶%$ó¿ýößÿûÿ»sï-n´ìþyvò¤}öõƒÏþ²wÿÕñž¯ñýŸ÷î½sÇ“ó~V,n§‡w›ÛÏo½ø)ÝÞ·‹£?ÿñþ§?.#Š ×}˜VoíìÝùëŸN?ý³Ö<¨ ß|ÿ/¥á‰KúI¹²z¹}ùíƒ_ü]oÿ•ZÚºûâ×¹öÁµeäæ2²bl1¡6òíƒñÑK^¯=|þùïÿñ¡µ:n¤ê¾XÚQk‡ÛsüêoKãË•~òæ·ßýάN`Ìg}$d–ÓP®ÈX£»à¥Nn¿zø⫦~pÓóÁ ÷_]õ^Y@ü ð<‘D +¼_kíœ1[BvHMàÜ(?Ý̈R«YqÖJæÚáD2Lé˜\”Kkv÷¤µqíüµZÛŒ1F¾µ«U7ýôt™• Q–Bœ‘))#ê¥pœó†0Š×¼aÂabRQ*é¿*"•½˜$§Ûvk»ºýpëò«•;Ÿ6ö_™íSgŒ‹²œé„ÁʲY ýå°Œkt²žmÉz”ÉØ•58¤¡øÖrT$Ì¡‰HF”=„)™ö>¤¼h4(±Ä´8m¦ŠƒL}Bȶ3ŒãbšÒËb®ÓÙ}°uñz|ôL+ŽìêŠ^™ S[˜­-åQJ§x‹lw”Ÿó÷0"¤†2FŒ2¡ðÉ2%dÞöÇ“¸Tb’5”M¡¼M*yxÝJñamxHhÓ§™'åÕÛV}•OUy£Lt»‚èFåÅèÞl’Ja­.‡PSŠfeÍŒÝqÝjìÛ#µ°ÒÛy¸rú²¸r†ñv¹><¼x•mla|>Ýܱ[jqÌÙ (e(kGHƒ›öõÊÍ{¾¸†Š­´mÕ¤ôR +²ž«vÖ;ë§ð fÿÔèž–7ŸUÖ¥[{Pù ¹ˆ%ä­£G›g/ÁwTÀT‹ÓÛ°‚Ñti¨©ÕüøÝïÞ|ý£S¯-ÇB²‹ÌÆõ®Ý=³[‡ÉTõ×ü§¿ü½ÓO__ˆº"˜ÿÌà~óøSˆ_„a¤Œ]¢ŒE—óze¼_kã¢<<Âä¼ æÖöî—û{><NX\ªKªÕ„Rc¼;ˆ;¼ÑpŒà%5CB(I©Åüê=iÚÁ{Ô[ ѯŸîßùĬoðÙ®QßÖë‡1±îË$gòÉ‚?ÊbBœI\žQªZa¸ €…Š²Ñ”´j³·?Þ ­(gÇ8;ˆ%ÝanzÌ¢kn¸Â€ð‚‡\òÑ1:E_¨%RÉA`²f9B«ziTš¥°Þ\{`V·¢´Ù—{{~T 'Œ›÷â9„kðæÉDU£ eºŽ 8IÕ;¥0-HؘX +‘)pÂòÔ™4q¹’,NŒüˆs`¨ü˜BNgOç¡IΈ3zœ5 ÁÏÀõÿ{Àat§/îRž0 _DñÓ}i³Š%/Âg[›Vs=HÈ„˜ì,‰fMO×ùdΡ9µ­ÃD)„.°¥T“Ó«p:0Þ¨Ž7HL§rw¢˜,*iZÌDI=LWÝÒ=\*àòtE$!䵤¤j8­{B‰yG4^IÝXŒÌ.#n?NP<+èé\·ÜÚŸ:v/¡‹X²‰J¥›ÆäåJgEË6¯;Ð麰ÒVsëñàäM÷àyUPÊ ¸T,!£ 9FaKâ¢ÍšU½4ԋÄ’—õ‚l Î  2)äá|!ãÖt”T¢qÅANbd‚UB0õê(aÖ"¬NhªQÚ;zpöð­dW!Uíúnmå¡Y;Œs™(&"œTSÚˆàr”P1Î&ä2—ÐJ !4ͨœ={öÙ¬éÃ¥(c†q-¦—†—î%>üž'Ì15”0}XÒƒH\¥•<.¥ã¬^ìîæºûV}G¯lªù5ZkD)SÏö´üâ +aÓfe#Ý<ÅÔ!„–Q—Ü1_Õ4«æ®Ìù¯/†a*¤ÒB¸bˆH‹YÕ *Y£erPuƒ¤a³`Œ¡0Fú¢ FÙ°³›s£`Hb\8cR(\ö I«™)­! 3Š©QÒÀ¸,R0.€ÿ4J=gˆ„*ÊjÀnZÊb´B¨@O€÷­v|p8ŠÆi 㳸˜ 'tW˜£»ì#ýˆxm.°äÆb„î‹ð¸^”‹Ë¾0Å•ÎÆmɬŽsq6gÓ„…\tÅæ—ƒ.ŽSÆ‚+¶àŽ‡âZœJ¹–žiHZM¤ÿæ†çÚBøæRtqº –t\2 ˆÒê’/~e>@'kbº—S]Ó]gˆòŘyzeÖˆà1:$$ZJU»™úª’i!´îG(GñF9B-H£´–-¯T&·gýä‚'¶à -ºBÉ1œª¥Ë”d…:e¼qSsíÁv±6†b0Låa2Û0©_ÂãCqIÐ*Jª‰Ðš;Dú"Ta# €qÙ`Ò4ÒZg£ØY#¹d2]\ö¡ +ü!B(WçËn<ŒMOíƢϓCtŽÒ;Rfœ,L”l7‚‹ªQïÞçí¸8ÞîYåµb÷@+L”割¤˜NÈ™éÌ: ¤ v1ŠI)ûbÔ’;ìöÇ⤉óA”‡ã*¦z¬VƒH` +‡œAÍ¡ô^9Ó£0xDôE8_˜šw„‰\/ÛuJÉ/û‰k ~—/>Ùº}|÷5Ʀ\¦#ˆ((ù¡DqÙ¦ç$¡RR†ê3‚‹xBL0 +ƨá8îöú‘8ž`dˆ&YI(åésê à‹°W®»\^Òå#Ã3³þ¹…°ÓÉ«R|FÒJà[²¥æ­»/I1ãô'ªJ•Ã¨D2©¦.{âFnh×¢¸†±v•ÝØϯ{—ýL×àx®Î¸pFg”¼7ÌxBÔ²˜]ŽBp¢„ÊJŒ²B1žó‚Õ¿éŒ~8ëù››Îk ÈÙª$Ý°²§^$³MG˜ö¡RŒ1 ¹”ÐZq±äáeÎï¼üâÿ“‡PveáÆ‚/„p¼’SÌ¢‘« +ŠuvÿÅæéS?.λO”s‡Yw„Å9“–M!™JåêùÖ§Õ !禩JK&J04'KZ1B!4ŽQ£™ù!‰“¾@€ã9#WS‚¥Ü1<„R‚V­q3¼¨2ëŽÁ)G¦øK¤œ´’f„‚aÀgÂP㬔ªe{‡ÆúÿHzó/GÎó¾÷¸'±%“œ™ž™ÞûŽB¡ö}A +U(ìûÞ@½ ·éîéžîž}#g†CÎp7Q¢DJ¢,’eS‰%K–¬ÕŽ$Û7±ÅÉMrϹ¹9÷×û@9§Ïá4–ª÷}žï÷óPïëCo„ƒ€¿rj^2*ó¾ø¹yÿ‚ŒF˜t &.xcp&©LѲl¸6§/ºàA@Ï•L×níüá“6ΧK½=½°Š2Ó +2%µ8osZ¡Lv3ÛIz¾0’N³ —\FqÑŠ9¼!#S8¼q/ŠqÓ3¾9È×SÎó} 0k‚ÝCQ¹CbMú‚˜×ž]p_˜s»ýᄪóv¹”“Àó[+0.Ó°0ïÅ…àìb4“¼AîüŒ÷™3óÏ<;ïôÒ´\&…œ?ÄtB7‹Fº"êe(é`”‡zÖš]Š<{néÜ×ÜBÀåÅHÞ£Ò¢#<=Xp¡îƒf„´]A~jÆ{Uò1J??ë]Š:|Tœ1½'t—™žC|AvÒ³YWÌñä–ŸG’¤F1=jd÷Öê·O†×¯¬½úÚ£Kûc#©Ç(9€pà}8™œ]ŒüÑ3³g.º^4Äé GÃ$‰Óš’PEåPós<ÁPHºÒxçáö7ß¾úÑ›W÷óïýò'öö˧‡ûëÍfU¤p„Aî¨8k‘l* +¥Gòº\Ëêƒï×ÍAa¥SÜÙX¾wëðúéøÁݽ7ŸÜxõ¥®_kÖ›fÚäy: LÏy=AzΉž› Ì;±³SŽ³SK³ó^Ÿ xMd µÕ(éª ±D¹˜-• +fʶÌ|$F?w~ Öí‹Gƒ~ ª„«šB®ŒR/]íÞ?î=º9úàíÛßùÚƒ¯½qøÁËk¿ýþ“úå7ÿÍǯ~ù[{ÛCÛ¶Q‚u¸¼ç"rÙÒÏbý\h£†]ß4oÖ¯î×à¸s²üèzÿ6Þ|´þÁÓíßþðËÿç¯>úî×o½pTÎ&1—ǽà +c”ŒS"EØX e G=áÚšúôjõÃW.½÷âú;W>|uëgß{ùŸ~õÑϾûð—ß}þŸ~úîÿüo¿úËOž¾ùh÷úa7›µ£1.aA°2¦RË‹ãh5½RÂWkt+O¯÷ìñ ·µ’½u¹óÕ×N~ó«óó_|ÿÃ^}òðð`$zÑåtâ~Tœš÷>sö¢ß¹¤sKD2JLgÝ:çë””QÛ‚Ö8\µÞ~rå­Wo>}tü­÷îÿðóo¼þÚÃrÕ®¯.oŸr©zÈçV˜XJ@)âÊŠñòµÞ£ãæñŠúê­îß|þæO?çý—v¿ñdüw?|ç_þá³ßÿæ;?þößÿüòüÉfNeÂA_Àá Åâ0 (‰x Ö?*ÑŠþõ›?ý‹7>ýúݯ<}ü¥í¿úößýø¿þøùóúïùî?þðů?î<:©ï¯W3––Ífe-«$‹ æÄh+߬ ÷v o=¿ñʵÖÓëï}x÷W?zÿ÷ÿø—¿ùÙG¿üüéÿ݇ÿßÿó¿ûùwžÞè~úîñÏ>ý½§WÒ†êpE/̇\AŠ`”`À ¸ã!§ˆûdlqXbnnoìVGæµÍÌ“[ÃÏ>zåƒ7®~ðöóo¾òÐ2ÓþEEÇ ¦Ÿ#÷ZÜ ÛÆVþü+·>ûÊÍŸ|üò¿ÿë÷ÿßßÿä7ó ßýÛWÿëoç+÷÷Ö*iCbXQ324IÈR3ˆõvuÀßÛ4^?)}÷탟|úòçÞùäÝ+?úèÎøÑÛÿýß}û?ÿúÃ_ÿå¿ûüá/¿÷èõû[¥´Ìr,@ˆÓO(ZQW×òìk—Í÷n—¿v¿ùÙ[;ûg÷þÛo¾ö?}÷ߺõëï=ÿÏ?yãøôÇß8ýëoÿü“›ßÿÚÉËÕjÑöB…I4ŠíåìfK»³þÖÓõ¿úæõOßÚýø­ýßþøË¿øüÕ?ÿú¿ýêñÿý÷ÿ¯ÿë§ÿü˯ýöóÿëo¾öÞÞ¤QÄ}qnÞåF£¤Ê²ƒ’Ü2ÂMÍwsÈ?¾œ}t9óڭ懯í|þõÓOÞÙûɧþËßöw?xûw?z÷_~ûÉÿìÉÑvk0Zµ«ƒ ®¦M»d) ³× àé>üì;wÿö{þâ«Çß}ïÊ?~øŸýŸöøó÷úñ½ÿô‹¯þúÓç?}Òýð…Ú•RHñ ÍÍÍ{Ï<7r¹ ÆßÏ §ËÒÝmûéÖû·~ògOþùïÿû¿~ëïø¥¿ÿÑ›?ýνŸ~ëÚ_~yûýûëÉv†JÉ‹2‚Ž1ɤYí´{ã~©_àFyüÆÿÚ£•ï}åô‡ÝþÁ·®ÿü{þþ¯ßú‡Ÿ¼û‹Ïýâ£Óøþƒÿô7¯ÿæû?ýÒÚ{· +ON›•œӀĸ¤D­˜ªZTÛ&NFÉ{©§Gö;·ëŸ¾³ÿ›¿|ú‹Ï^øÙ'÷ÿÇøÞÿøùÝw¾ûÕ;/¿pHàta’½2ÍGZ¾Q¡¯ µWO[Ÿ¼yùó÷oþèwþå—ßø_ÿõgÿø£·þêÃ_yïÒz5•`œˆb‚¨½HØÏfåØjY¾ºV~árýÉqéû_>ü»¿xåw?þÆ7_¿üÍ—×þâÝý¿þèÎÇï\ytµs4nÔ«uÙ(²j"d±‰£˜ùR:‘ˆ®M í“ÕÌíÍÌWî÷~ðÁŸöôó¯ßþî»Ç>Ùóæʧíç÷Ê[ËÉZЃö†)Zªhf›¡„\*‘IÐy•X«*ŽšŸ¼±ûÃNõ½‡ûýW÷ƒ·þùçþÍ'~öí¿ÿé›ÿò³wõ§§Ÿ½Ú|zRV$ ¸ü1Õ¨gòQ2U¯%Ɇ[µÂ÷Ö•7nÔß¾Óùök—þË¿ûÆïýÁoøÚ?ýü½ùíw~òg¿óöáûOvÖyEKâ\*F%K•žÄó)™Úîfz|”EŽ:Ü£½â›·‡O¯-?8¨}ýÁèŸ>úÉÇ?}óð[/ß¾=¼¾–_­¨:‡Ò”€ ™é¹ †R4ÐIÛ$wºöáZõ`P¼µ]ù´õÖÑ;Ïo½u븟:YN^é[«uèu :Ž¢8`~FÎPbVL%9]Êå )5¯q›ÍÌÉjåê0ýöíåO¾´óËOnýóß¼ýãîýéÓÍo¿4zã0óÒVêéAe³*«xÅ!àÈéB[/AH<ÐÈPE÷êòñ2ÿxß~ý´òÍ—Ö~÷Ã7þçïÿüŸñÞ¯?ýO߸qmÜèU²½T ¡l8@Ã…Æ3_1¹^^Þl¦NÇåÛ•¯´?zåà;o_{çáæýË­>ÈsyÝ Î€'ʧò+¥ö¾‘ð¼.d-c\Ù^½y¼ÕÍQWGÆ+×úß|ë…ûÇ«·vêðÜý•L»¨›¯© ŒIùâI?nÆø<¥–H¥hå;šf2hL¡É¼¡æ ©l +ã¶}ºQ¿u©w}§uyTTsýR®“³LM HÔ¸+L/yŒË!¸âõ†^o<Vi¦[*î jW×s/\Ê}øÒÆçï_ýÓ×.ýà÷ðÁ½¿xçÊgoìýéËÛïß]¾¹aê| (NõÅ”I#TÊ ‘(ULg›9Ûbƒm3v¹§__µîme>{ëð?ÿí·ÿ㯿ùÓOï«w^¿7¾uÐ\ëeºÊkÕ^F¹4Ê!”ãx½Qn¦.¯ÑKÈ«L9)öKV/§süµµÂó‡Ã“µj+Å–tÙ”eæP”qØY55›^‚Ì”!ã8±hÌÒµBÆ.g2UŲ̂¬%ÒY•/›’FEU,š¥jµ =BÀq1]@ÏBÀtùðP˜àY¹­®ŽöW{kµ”²QMÞÙ©ïÔäëë壕ÂvMÛ©ëkåäjÙ,k‚w~Öéb*!Õ‚Xrj6ð'gfç=H¡i[j¦œ“bý?®Ê7×í·ï­}ù…­—Ž‡ƒºÉÐJ)ɲZÞ*Žîa\â9Fp,ŸäÇI²œ¨U+Ŭ‘³äœ)ê +£(¼¨&⬠¤9|Ú‰øã*(Æk.@z‰,yò]^9[¨ÇÂAC`*ÅòMM±´Àм?ó‡H„ö„éy/>ëÆCt&YÙ5ê—0)Ïë%œÕQŒ“ÕL4ÆB›ˆ¢Ñélöû;­Öš‘´ zAªÉŠ7ŒŸ™u,©«mFo‰ÉFëÓ³ÎÙyw‡‡Y‰DFK™”^+¤V»¹Ó­ú¥~úd£rukyÜ.ö +ÉNÞ*Y) rfzÉœÜÛ~Á{fÆ;í!c ñV±±›+öYšNXÉàr2–c{½âÓ»'¯<¸yuo¼Þj˜ªD‘xÅ11$Í™ZŒœ½RÜ*ônd +ë,—v®Þ|)†rŽ¥A*ºžÏfëÅRÛ¶J£|D8*Ï-"S3¡YéÃ,¢;#jˆ0i9ïõD#A$Â朱 +dî‹ +a ÓÂY Á§+ +Ad½aŽì(™Œ³Y6Õ•s#gTþ”Ëé§<‰Ñ†’jpz¥R1”…J«WšaO ©Úi»¡Ê) ¡â˜Œê¢ =;í©8—_ðÿú‹³_xv~f1˪j&´L,FÅŒ§]Ri$"aQ™ŽË ’ 9çt¢snŒ·úöò5%7’2£ÊèTε„€p¦`uÒÍ£¶ãSB”t£¿9ëGÎ.çžì +ù !³F$ê1±@+¥Æê e¶ç¢jˆŸÜ”árî˜Á“qRsxcgg=ЉmS‰F\,ð”Ÿ0¼¨FýaÁíù?’ÃR„ÉfKÉtÅT-™ëRÀjÕeFØ ¡5h½É )Ûí­fi½d²L÷áIOÜ3™•vF¥‘@y“ÓË”’㌺^&²=9Uã<ÉJ’f³j~ÚéwøÑ ¦³F_*„øÂ|€:·~v.°Ã\™5û.DœósÞÉšü›†p‰`¢ e+ý£ùñÉíɸX +’éi¹På¹¹Éò×ABgôºU?ÀÔ¦ M¹Ùkj~¤Öf]„+$ĸ4­UY«ë'ÓN$áà ZkÁÈ,…„Y*vëHÌŒ˜T/Äæfü‚ËÇr¢Í)ˆü˜Z—Ë[Zs/»|E.lÖ•bŒYébÞZÎŽôò®Y¿¬Vöf"‰çø™%Ô›Ü4ôGÏ-\XŠÑ©e­u%Õ:³k®ˆzv:è ‹K~á‹Sþ©%4Ìå¹ôªV¾,Úk.ÔXŒ¨çÄ¢Ä9+WÏMCÀYº0tø¨iˆz•²s‹‡;‚Ä¥(>ù>Ë‘‚ÂRTñR¶7¿ðì’TÞŽ*%\É­_ºWY= ŠYT«FÄeÔX³%gÕµë\v¡R\) +ùAH,ÀsLŽ²VŠýëûÏÍììŸó’NÌb²[Jí˜J¯!RÅ‹iK“ÛÒSä¾ PVˆ-ÐæHÈ©TMÁVœ±„7üd +•k|f%Q^gÌN˜Ë(¹»µOiUÎhÅU2=0ºW­#¥º#dWH­RÛ¸O&;^L¥ÉvZ\vˆ%Û!©‘+"`ËêÍáá㟹è%œˆWj> D©úISN5kƒÆZž¬%nôBR=ªtHc͉¦TÚ×æ‚ôBô¡2£Õ¨Ô(&Ö]!Þæ9½‰k iÂù'›û0G1­Ó:~(rÜÀÔ*ôšÓ ¥ì‰OÙsÙx¢‘j³QmÊIÆxÛåfܨ'&û©4©µ0¥†«08TªZS°‡ !ÑGXA¦j&*“»l¦œØGœ7'_Y.†YWLFµH¢Uêœ= ŒV„Ëi Z{.šp`F@(KÕ:;–*{RñÒ¹%bÖËbr•Ôjn,¤r^,ë‰Û Î6ç'§ÝdT,JÙU)³êÅÍ/µÕP­C¤VÄTÛ“.,"_¼à›ub žˆ¤l¢ùpÃS"TjÁYƒÂ³C>·NgרÌZTªž›ÇeLo%Kk{×ßÈõ\T2$æ0¿Å5èŽÉrîéåÉm*ëjiäÀ•é¿7â‰NnåNnýsùZt²l¬F$Û\nÌå·¢j+À—`üè£Ê6Ÿ¹°¤ÕkE­Ÿˆ¥}˜,A m82µì§SnT%’M1?Jµ.Ûc«uY«låÍõ+O³K6ÍÁÆÞ˽£WZû/[ý«…ÑmÎè˜ùáàð‘‹Lù,”—ßN¶Nr£»ÙáÝdër›ÜsJ›=\kGå +Ô'aÈT§³~kíä ÒhucN"¥V.¥—o'[·P­ïg +è6‹JE*yâÒtHXˆ¨¾¸¦mD,û¨Œ”_WË›!Þô3f€/cÆ3jSû˜Œ'–hŒ®Š™\ïL€ ‰EDmÆ=2½†i 1"Šf‹O÷‰DÇé zŒ5LT.%ëûÉڥŠDËŒµJ&ûð^n2½€ç½¼‡°ÕÒNL,_pc&×›”=$Ó«q½í@“P³«A¾ + –*Ba 3–ÙÜF<5p6m µÊîlÊR P9hx}!»!m?añ™‘+ªÏyÙÉʽhdS«íŵžóR‹!útï™)ÇÏ»Ÿ]ÂÝdŽ1W„ì:®µ}¬#$P h“š—´¹ÂÊ-& …a’É:•^òkRi›´7Øäf7%³ÒݾóND…º2ÃTçÙÀ3ëQµíŒ%3ÍC5·:–lуÛa¾,÷”ú1e=dÎ(­ï^{ÅSnêb@á"@B[Çý«ïzçèΗÞÿøGvs÷¼_D ÒZÉŒ_¨]~­°ù/l:P=Õ¸ê4”¦xÏ{™ÔÕ¹¦”ö–‚ŠI€ÅxPyÚíšòñ³Ñ–ð¥}sùÖ§C–¢~\÷à)OÜòés¢¦‡Ê ù-½qy)"ÍOVß2Bl6,”õÃ0Wˆòù˜RÅS&³žÜiì¿×*‰âœ.»|E•¬os™cµ)«]WÊc2ÕŽ'Ûri×î߶·¥â¢Öâj€Ïù¢”ÐDƒ´†¤=Ž&º0à•õ»¥Ñ)è¶\XcìLï2Ùu¡¼ÉZíþÎíõÓWøÜ +¸etŠ£Û˜Ñ ‰%:»žÞ–+{Tz”¨^’Kg]Èb”H”zu$ä‡^¶–[lzò{ûµ£—ןœ‡¶ ‹„Ö­ŒtÞk§1½‡+•»¯|œií»ã¡·»{/Þyý;w¾ôÉòéël~ÃÏäkù•w?“²Ã‹~ÁϹÜN²s³¼ùxxå­ìà*›ë×Ò­é í"MÔ¨Ík݃/ÕÆ÷ã{tª+VûЙn,E[&·›#P¿t÷Zsó>äÞì­Ú^¥Ò}ÚZFÄ™¨¯=:~üuÇ›WK»jy›Ï¯¡z‘ëA¶@hÂèüû”›sDu!¿ªÖvRíÃLÿê¯3KÍ{óA~ÊI 0kÅ2=¢ÁÝÔvDj^ðòFnåÚýw¤Të_=³pÑ<“Kû¥ñ£ñ­¯‚×8"Êñó_Uò«NLwᩘ1ôÓ?oR*…Õ‡›7?ž|I0Û—Ž”‡‡d²&Ø]µ´‘ï_)®—Öoéí}*Y϶/ÁûÀ„â©&´6˜ +´‰XÜL/Ÿ¢j5"ÁìÌÎÜÚ‹Åí­Ñ-<ÕÒ³ƒµãW´Ú†Ÿ³ÄÂFª}eyïñþí·÷žÿz}ë~¢¸ÖžTWOy»Ÿjî—Öï¦ÚÇÙåãåý7o½e4Öªƒ£îöC>¿uU«!!o×zRi—Hv8«Õß ÍŽ—NO²Fq›ŶWKã:‡¯AMŠ…q¢y¹{ùqÿôKæʘÑwi7aò™e»´g»ÓÛ¸9„Ó›—éÌ +0ç +¬Õ£Ìn÷ñìW¼¤T÷´ê¶Ÿ2¤–iíréž(ªåµüøŽÑÞO5÷¬Î‘ÕØÐè¡RÙQ&³<’«ÛZó +[ÜrH÷NÅÂÚ"úWÊw¹ü†˜ßƒÅ‹)u½¼µqüKTcRqùÒó'/kýö;½ÇÙám)7‚@½{üøæ“ï81#ÈfÅòú½ÒÆýâÆíüú]?“?½ÿÎòæ­0e¢R>¿v»µ÷´±ó¸°öBeû)–ìÏø%L©z C.mJ•¾´OMˆâ¼‹õ6tª¶¦œ± lÊEè®÷˜T_´Ž°øì´˜z*¦¶Ãb5&71ÈSÖúñÉòØ×l*ÁG¦ºÃBÓZriÛjA/“Ji8>5ëë!ÆàÍN¦s¼rùéèøµâê ©¸J'« +ã»KQ1ÀYl¦oµOKkÓýVïTo챩Nºu *D,¬'»§Z÷ + í£5íæQçà±´äíÂðºÕ:hnܸtóµÞÁc¥´ÎÍþÆÍæø¡Õ¬úöÞíw¶o}ypåµîá+ÙáuÞ,Ýx²wýu?mzH+®·{5lŸðÅ4Ѳªã“Çï'ÊkŠFã@(íJµËÙÕ»›wßï<ÕŠ›[§¯5/=ˆ›M¥¹GwQs•+îéÍk¹á]ÂìSZùäÁ{­Íû“»Ù\4ÑBô–êÆäj¦uT\>Y?¸ÛÚºÕºød[ê7´$—¥mî>œÈ&oõM«w¤7/eWµ= 1€³ Uºç¥,Üh&Û‡Vÿ¦5¸S¿”îßâ³k`@G÷ßÞ¾ì׸šl°ÛØÏö¯'ë{r~—‹½ÝûýÝûzÔ~…ͬ¨ÅõLkgçÚë«Üq·–[[÷º‡O¤úþä†w{H¥–÷o¼fW×|1yrÇëà8ݹ¬ÖÄÜfŽãÖLPf’Í™ªŽï®ÝþJãÊëúòµTïÄèãæÚl@e“ÖÚMB­ûâb˜2<ñTL¨°áZLJYçQo\§ô®Ÿ²Ï»g,EêBˆ…=§`ú´#B!Èä[؃*ZˆHf~e÷ú+v{÷Ù%$"”'9}ùšR»¤”6“¥õJwÿ/üàõDÚCgc‰®PÚkì=]½óay÷)ÄÕÝãGo~øýlgKÅ­µ˜¹šëùµzWß3{§>ÂP‹«raõ«ûV#B‰Ô;¨ÓÉ“õ+/Õîø–œáz‹4º^:»ˆjà,@ž¹åk|z™ƒ#?ŠkõúêÍÎö­z ‘ª˜\É4wõÒjL*1Ž{Õè$—ÁC½˜áÃ’F}2æ¬ZŠˆ¨Ú r[‰æõDãUk.[._~ÜdÊK/ÅÔX¢i´¯&+;zi̧:‹A.J'ŒÊ8ÌäçTÑ·ÖBLÞ¬_ÉõîPÉnº¾cAa¦3¢8c& ¹b)*5HTöÃl€™2ÛÐwnL‡ #'»”= +ŠmGx›6ꎸB$ëFk?QÛQŠÛÀcöè^,Õqb µºÉט‘ .µ1½ ºÇÛ#µ´ Q=H& ýãüÊ©^ßár«˜Ùö‹¹TÒk—Àåƒ\.Âç¨T‡IuR•qº¹-Wbr\̪ÅAˆÓI%ÃYÈ2éÞ ,¦5cj0’¶VˆTw)ÆSFYkn†×Z»+›c’£V]¿>É­Äd<ÑÎŽ‹£ãÂð —+¬5­¾à%  åÍáå—päÂ:øüãE%eGˆ˜ ^Bs}»¾Qîîl]y˜*¯zÈ4ŸÝó»¼½1"CDm)¹õæøvÿÒó>*í£sRõ0µò¼Ò8æ²k|~U؇{³­]ˆÿ*•kvçtùàÕÚÆ|ïȪoBŠxíÝOê£>®¬6.Ë“eÕÓƒkZu7Ìd»£“ÿüWמ|Û…&¨ôªZ=L/ß0šRn(šÍË/îÜx;¦Öü\&(×âzϬ©•¨Ü ±=¿Ò_G”r€¯úÙÒRlò±ž¨Ú½Îîý mâJ™M¶•<\ŽöÆí˜XYÌÓ|²ÝÛ¼ +ås‹øŒ—Z+ ï¦Gý‚l9‘YyòöŸ^:}éÌløìæ$ Q©•,ï¥Ê»à›¸˜ãs«’`T„ÁÛ=T©aæ (7âz'Y¿’¨$ª;x!üSVO¶ÃR9$–Q½ ºÝØ}Äæ†ü*WAIbZ]oì"r9ÈdB\&®ÍöeB¯ƒLI…u­y`nU¶¯]{+¿z3Èep­¢”VÃ’7C,°S#$T¥Ž%›~ÆP³mÊh,¡Z+,¡†#ž¦‚d øŠ¨5xñµÃ‡tª TLY}&·ŽÉÅtc¯wø²—J#BŠ“«Ðõzu,—Ö¼“­÷L&ÕòS©¥0kU5»\îí‰v÷‚‡9ïa¢‰_A~i#Æè¼Ù4›Û­­Ûõ­»zc{âPJ•M¯,E‚0ŸSË[õñó½ËoPÙm–™ñ Z~¨–Ö]Dò¢ŸžñÒŽ˜ˆ >·%”wÙ‡ë•Ñ5@Dð£0›ñSiˆºVý ×½n6O!z©œZÝ#ŒȈ×áñ^D¶Këã“WÊ«W£\ä boqajÛÇd½U©28±k; ŒÕKÛéîI}û¡Þ¼âÄí…€¨¦—«£èbT¬©ek ¥ÍâÊu­¸"SƒñÍúòþVr–ƒ¬ 9«¶õ|k÷Å_õsE1&йŠìAõň`³ŒÙ¬vªÐMî&ãk¨7O V¼l¦¡Rz¦„6`‰TŸÉ®óÅ]€:<=$-Aìiïèå7*z m)ž ó¥x‰ÏŽaZ§ƒ<ôš  Ï»©¥h"ÀU°ä U?Êu¯™ýsªu˜ôž>Ù]Ž2Ìâ`ïÆkQ±4’ †#J#"–•Ò®Z=Ÿ™öÒ!:Ç™+˜ÚÆÔg¯CN¿à¥ÏÌ!1v²lZL)yqÃ7‚dšL4"BòéLPðÄäÖúi²²æ@ÖZNµŽ”ÒkàŸgüüs³áóŽ8©”§¢QÎÖ*›…ÑíK¿ÓÚYÈŽÎ/Fý¶Ø;²¨LÊ¥ˆ†ÉPéi9YG*/F4pðçbP´ðW„MSr&JI¤bjŒgÆCþÉߟuÏ{„ÎJi)LÏúð^Š@‰¦H­Mêm?f_t1ÚDcÖ…\œGfdLeœa²V†ÑóÛ«®x2žh@wBË ‰&i,'Ê—´ú!“."êÙ¥¸'ž 1DÌG¤"k¯HÅ1žl…¹´#€ºhˆP(½ +¹Òž|GóRqã¾—+NûÈ0)óZ^H–£J$k ŠÃ›k7¿œY¿‰˜]?›c â‰'0½•ë_ß¹ùÕñõ÷ók‚¢‘%»G[Ëa>µG},¹b¶®Êùím»Â,o49³‡«•ÉÎ#tvÚË/FUÌèÙË×¥Â8ž¨dzriÝCš l&O[£dëT­BœI³zqyÿEð÷€XˆêmÌEõ–ìTÖnØíý(ŸQìnfù\›ÍŒ ˜eµAm»±[^Eø *ôÊ.Àg¢4fÓà [k»è¦ „8­±‰ÓðáMÅÜF\j0Æ20ÀRTS ”Þ\ð>ÜŒˆU6;NÔ/›­+Q1ZfíÒð*®7ýt&"Öæ£I¸"¢B… |æÂb”RŠZ®Ï¥Ja=,gƒÜ¬g”‚h´–&7Íi|z…H´P±â',wvÊ3«ëõµ!*óì ˜÷g#”íòØ3Óþ¥ è‰j!6{v²>'ê‰I3©¨-@t> ÏûæÂ<–h@|(.ƒø<`JãŸ>V#|ʼnȎå¬Lûppüfnõftg'7'òQJEx+&ç«+7 IaFá’J +á"§I£I¥Z€FéÂ:ž]… Å¥{6åÃO¯lôwt÷^æŠ{.4…0v\*xqÍOšJa“K¯Ã(•25tÅS®°À鵸žv#nTÆMjFù¢PX‡^s9LÊÙ­í¨sÇõ¸Ú “Pr×’Ýk–0ït¢h5w©T{)¦Péa~íN{ÿåÉ7’ƒX¢'W[­¥˜ŽiËVçÚòÞ‹kǯÖwªÕm/ªjvw°{ߪo9i!,.„D¨m6ÕÃ…2BYóÞ¸lu +˧Э¾ jàŒé¨Ü˜üÐ"½ +(èAµLc?Û;!Í–솸â\P€yŒ‹À`1¥’j]’Nš=åŒ% B¦hœ51ÞD¹Tª²ÉXHO>̈ËUGD9³ˆ:Í7f½L°µk¡ÉšÒØ–Ÿ´ç|ÄÎûñÁî³±“esÜö™¨XÖ+[ˆ 2vª¾—^/,Ø­=,Ùr)` ÑêY•1›lL{pgT^ IA¦ˆÈÕwvrâBª•iíºiv²Œ•Ši­Êèvuý¡÷f¼l”Í ÷VW®º1#"”Á7Å캘ß`¬þä×’O÷´Ê†‡2Ï8ñŘàÊèžÚŽJ%ùD±5¾ÁÚýE4yÆEù’›°=T^,l/…éEDÐéê%¨çY÷ìLøìB̃§Aü¡´Î;q¦‹Ùa²yª7&WCL~1¢N>s1©¹J B™Ío* +2™öDÕy/;í$ ¾œÌ¦È¦ÜÏMG¢t>BgÝ1ÕƒªÓˆEÙ¥Xxc¢êRÞ¨mç–sý£üʉT\JŒpy2Ù°‡ÃjÕ·^*¬ÞOõ®±'é$"e|t’2ûBq³6ÙÜݾQè]ìáb”Ñ¡×B\:H›À~ÊbRÎÎã•ãwÁSf|”3Ìarœ4®5éüFPª&›'™ÁmDiúñd¡µ­æúN,¹„êKHÒOA‚®ÙíS³s#"Ô`ÌÝ1à·a¬@F[@TwÜð’éDy»ºùÀ q€HãJ5,”`¾Ü¸ ½€ˆe|rCefƇŒñ¹üòIgÿ¥°ÔœrÓ3~èEJ÷\m•HvåòagïµæÎKˆZŸõã@’éÆvº±À5¦~òÛ•C©z*@5ç±Éþ)ÉÖ´‹8ïÀ G“O7µSD¬ø0mÚ¹£!çÆ—üè†É LiÅÄ’7@dpµîÁ øSˆôÊÌG&¤äû¸Z<7Å…b"¿‘¬î‰…±·¼ˆŠ+”’£•Œ?.`BÖ,õÒº^ÝÅô®Ÿ+YÄçÜXxò“*cÚ…¦…™côXk%@¤/8É?¹Œ 9»¾aRSKñgfcsI®ˆÍ›fÿ:—j;ü¤žn店!-þÑT༓š©~¾†Yc1»½àŸ›ÐJ¹9ºyf&òGç<ð€°PãóÞ¸] Œ—¡—ªÓz Wì ©¹&Ki%e{E«lÑ©V„³áR6ïÁ PuH„¯d3Äg½ÅeÖsíÊCL×äãA“Lõ„üj¨^Œ¸ÖˆËµ¸O¶§#Ê´Ÿ 3)»BéDalõ®#Z;¢Ô=Lv.*;a¤r€/Ìx<Ñ!S#Dn†˜°ñb4qÑ |b)èbÎK¸PPõ$›ZÐ;й a%Êå½Dz&$‡ÄjTmÓÖ*$8nΆ¸)gJ² ¼!ÖŽ&:1­ ç/•¶gBüs È|‡éö‘é ›ÅôNTi‚¡Û­Ë‰òi´BDâXT,B.›j¼¬#ª2FÜ9@e§–ÐÉYXGL)-&½1”—¢*4cˆ2q17ç‰OV«Æ`äÓX¢ šbÖÇûHÇbTžö’!®D›C£u˜Á(gE/Ý^9IV|1y)È;%€éALƒcΉ¢l­¹þôÍH)3ççæ4c\øɉ .ù‰Æ[L..8W41çã“¢.&Œæ0®K´ÑD×ÏT.:ñg|Á‹‚ LþÅéðŒ—ã3“_dÉ¥"5ºà“¾p> ÌçK\²;ïg¥tŸ±FˆØp"“ØÝ„°®”åìJŒ³'‹Ÿàª3<É)A"Ik >=Pò«LªM$*„Vðàêdûx`ÁÏL;±å +²‚’·òƒ\muÖ¿èÄ&èÑ6™\ž)çä¦\î¨(Oµ~ÐvÊ@“FØT×™™Ð3Sî‘ŒOvÓÓçâRHq„¤%?öç‹ëN$1µD |1ÓÚ÷S©…ÿ{ÎÝaGù +üwÊAi™å›/}¨¤ûÏLGÎ.2A6d²bÜ1ðl7DšPÕpbPWÚœ¬Ó+åh½†+@ÞjK¹u*ÕŸ|ä‚HÀ~"éBDG”1i "ª…i`¹®VÝI5÷´ê&®×'‹°qöbX€Úv„¥(Wˆ…lB](Wj-E?•6ÚWÅò!_¼Ìd'»3„Ø Pt˜Ë€‰ñ”œßå2Û„Þ'’ý˜Ò˜,;ñF$J-/…%:ÙçÒcDnÃ+ƒŠ²öƤ®øŒê´xÉlDé¸Èbˆ¯ñöš^u†Ä(®5GG1©ÈU»!e²s=—îBÌt¡Ê7F¨5éŸIB±˜ìü$¡n¸^R› A + b‹ §NÔ˜rs³~)Äyk„'‚"0³‡0ƒð€=d/xØ_Öªûra#DY›§¯t¶Ÿ U•‰•%,u1 ,FhÏù ˆ!O[+ˆºg¡IŒ$ÎÌ!ùÖøéÙEü‚ƒ [ +€m¼^²ù?>ïÿ“‹!OLwF•i}n>ÿuDu\ªÓZÓ×Ï.D§ÝÄ´‡*Cø‚3—Bª;ªèÙ^±³Ñ-Ä”e€gÜXŠi.ÜZ@õç\Ä” _òq©*eG„VÉ¥(¨+¹à'Ôæ¥Õ.‹…݈PA„R˜ÍÍ…gæ"ç˜#š€Â0"L@O@´‰J•0_ˆI%8æeÊEÌúY¨$©  AŽŠ˜\Œ$“mH(Q¡(äÖ™ô "”|”2]BÉè‚›p„¸¸XÄÕ›/®E¨Éjœjn M1ë¡á£lŒ2©J( B <þŠðyHm0ª‰Ú›Ý‚²äÓC “»ù ä÷ùï%M17f3a¡I|QϸÐ%$æJ´ƒôÑb$¹ˆ·Û3~qÊÊÀÊÙå0—ó‹¸Þg3[´µÆÙà•µ Ô œn>,NyIDjb‰—_O÷&†yºcŠ˜¬Ýy»¹vê%R>¶V!©ê£'¿à¬3¢Fè””N{ÀÉ–èÝÏU(kÌçv0&SíyvÄ<¤Ì#†TºTÞ"µú™_(®ùQmÎÏBÞ„z»à¢Î,n,ËØ üçqo <:£¦L0¬… )˜³Wõêþ¬‡Y ˆpøã¥õ ˜Ý‘àÖŒŸÅ5šøãó¾ëÅR¸R§´*]áĹy⢋G„ª 3¥jp„Ê’€R™|-”°D¬댹` +nTFTÈSzǨ]r›^"çDÓ‹ˆéÁ2Ó>a$Q½„bsAðA‹àõ®» êÄÂ&Xƒ3Ýð.^|Î7ÙÔ“„Žª%:½œj\VÊ»À6~ˆ²6Ê¥Ò(ÓÞg÷±yP 2Q‹Ò–•Jq!Å`´(“ôa Ž\ƒRñÆ”Y7cm!Õ!¤R˜N£lL‚ž ®Îó,EŘ…¾`Ì2Ùˬ +䌈:Õqãäq|ð—¸Ü¦÷£R<ÂQ!‚0:€«¥šX†fôÓiÂhÏ„¥uÆ (¤‡1± >uÑÆCÒÏtO³½k^ÒrÆ%¿MáF ‚i3à fòƒÓÚÎ#Ò왬Q?JtÄõ…¨<V@ܦœT”É0Fcî¹r²9_t¾pÁËL9âÞX"LÙç¢@w}L1,6{â2@kSK1h.x⌛Š+í¨Ô 1e?™ÇÕÂf¦"—0gXyv&"1)î…îå×wî;QÜ{vqGPœZŒƒ_pL~ff ÉÊ¥Tã•+Ó¦Ø=&µ¶I^ô‹ÓÝI¥ôšwn¼ògKªð´ ³B\EÈnÅ•ÇvëHúÉ´·âÐÔ2ô‚|¦ÊeÈø0YÐÚsá8 ‘\%“ù +\½V€©€‹­öåsÀù@ãTqY¥4ŽkðúöR4éŽi!Ò:È—?®Ääeu&¿þµÇ ¤1¹"S¡a\*ÊYbòÝZÚ3#ª»".Q.³"§æÝ ~ÌæI½k¶Ž"Ba1"Íxh„Ncœ½ä§ýL4 ½-w˜ìº—LÍL6YV€K1),ä½t8‡0—A*i³ÚµˆÈq­.—¶øìzˆ¯BÓA+ÈnŽ3ç#pµ¡–"b9ÈeQ­y>(}ábÀq©¸à£fœ1Èãð¾ üZågARrÓôÒR¶O `mT‘'_ÆÕ6ïµ.=–&UW©®\'“xH¤–a¢ƒt!Lå"Lv1ÄA³Ä¥"‘hÅÍx( 87• ÐY’ø“óžs ÈÙ¥øy3íçíˆê€fKR~$¬y?O'!RjÊ‘ç¼ô³ÝÏMû l‚DJ:N©îÕ÷žÚÃ;Fë +mŽf}ò/„<È´ž›‰¸Â2ø k RµZï†Ø¸Éß_ßû¬:–Ìnš©uïö½ha›LÈ;¿]«ÿÍèKoêÓk£!äæ}tšXxÝÝ[}5¹~xk¥qs¼ö…?9±úrfëhë«¿½·ÿ7Imן%ÎvŸ~ +ç·ÌÜ& 25µ9µq´±ÿ÷÷ÿØEåÆèjÆLoéÛh_Hz—ž~<¾vhfŸ©KÜþ¯®˜/ª·†Ã¹«jÒGlñy¼¸­§.7³¾«¦Ö&jÏVw~¿øäÛ4¾|9ºôˆìhojcz­¾òôëÉ)Ìó¾žÚØ']Ë_ÿRÍ2<~ÝÌ‘ô¯Fwþí'Ê[žZ©OÜ­ßÚüL/|¦ç>™$þ«ëc¤™x·î‘2ºøbbel©Nûò¹7õÙ0ØÎo®]C3å•ë¤¥s›³Ñ¹íÙõ㣫·×ö¦7¾"ÉNÆ鯆o2²èÏlOÜݾ³µgëÕgôŒë$4i[?óf?õæÍü㉵ÝÛkû [oˆ‘¼&]nþáÙÂæÑôò‹±©kzfdò!á6iÔ_Fw?óçéÿדåéÕFíÙ·w_ü»Û¾öv®‘‰¡0œ¬’Â0±ñf|íõØò+Ú’Ë„jr“häZ´ÄG·ÆH¶Î< Ý¼X¢M'""+;š{Lªòaf Ív«ñ7¯~÷m}ýÏfö=däÖš™ÞLа×ëûï&6¾ +È„!í7®‘AMÜ;œ~Î> +gžO®½™ÞüfáÑ7Áìã+>© øúÎï–Ÿýp{íuu‚”ö§·j{d°L,=ûÄ›"Ãýå÷«õ?ê¹ç7Æ:IÌçù·s¯ô­»ÁôÆÔZcçëÿ°ÿýÿvkm÷ú­•kcK+;«¯®®Œ˜ºøäû¥íßOm¼¿÷›É—zJß^'&ƒ Þ‰-"ÛÑå¯ÂÅRö¦î>¿ªg¢éM2öŸýqµñÛdù+5Iã\ðÆ—o-=ón-WÇÈœY˜¹÷jëàï¼þÓƒ¯þ¤g‘¢KzÑ—èôÉÍ[_*ºæÞäZ]ß¾wÕŸùäúØ•ê$!v0¹ñW_„ŸO’¤¸½¼wo÷·7n­ý¦zûS2 “»ÞøÒ5¤ôÌ~îÍüêê­‘±µh“Ôõ‘d5š~4¶„ÌÄMíæØ}â~wž¼£™þúÆD8ûxåù÷µíßNoŽŒÕH¿­TǯèÉÑ™Ãþü_}Þˆ–WžïÿðO÷vß‹$’:'8ÿˆD3éð·ï½¾×øëLJÿÑ›zÌn‡Ä*gŸ^]ùLÏ~¦fÂé­ß?;þÇí³ÿ¼öêO_FËŸŽ1S»GÒÍ›~à/<¿½~BX:ÿè-¤’ù|êÍZÓfÖ¿&U¶ò‹`¤Ïçzæšð+>ëAœŠÔÔxîÑÜúþÝG§$Pª·6Éî^}þÃÌ="ÕÍ‘qRŸÎ?:k/£åúâ³oŸ½ûß—¶wƒÔ‘É+#ÞèÒtmwqëmrggâÞW4ÚÜéÕ=RÛHÏ¿Ъ.Ш–ž¼Zߧ‘„sÏ‚™§Ókû£‹O‡'6F¦ÞZ~uÿOë{{ûÞa0û„¬ãdþÁÔê‹¥‡ëïoo¬íþþÑñ?<:ùÇñÕ¯ˆå¥,?9»WÿC¸ÔPÓÏÆ–vFÉ2š%2YùÒ¿­&h÷îïÿýîÿeêÁ)1H«©­[K;ãKOF—,>9œ¼ÿ&^Ü3³ðxëŸ{Ó4—áèN<³1u¯N +ÛôÆ)ù·–ê·w®ú‹•›·n„wô­ë!ɾ5"L·W÷ÆïÔIÓþÕÕä/>ñ&àä¿W½µ>±¶¿²ýíýÝßß^~yÕ¿]Mæ—¶öï>8ùËÏCbþdÞ^%%gf;œyBê"ñÞOGn' /ˆÿÏÞ?AÛåÚþÈ­×ÌÊôÊk²²¯…K¿ú2N– =¼©'$þ®ÇkŸÌþæêh0¶rkáA<ÿPÏ>½w¸yø;çÿiåù“µ]"¥éµÝLJKr“æb$%W§7ß,ø2^"L§¶VŸ¬?»¾ó=­íÁ2=ùZ´BÂë7#WG—ôÜÓ™‡çµ?̼1¶tkuw½ñÛí³ÿ¸÷‡ÿv{óä3Ô‰«zjzéñØüƒád骙#Y3÷àtáÁÙôÚWŸ _©Þ‚{0©‘i0v÷YubxÚì曩uR×w‘ç0óp´Ö ómf㵞}BÖÖ­µ£Û«¯Æ->œ¸ûdáñɽ½ö¾ûOõïþõ΋Ô̳›ã£‹¤ç¼ +g·¢…É{Çèì¹uì/<½6NÊö”?±:·þjùºÇÁM +ÒR}~ëÍÔ½’§cwŸ>üê·ð“Ä«ôÿ«ñJrg—ŒS5qß[ùì樞¨Õ-><™Ù|ãO?¾™Ü»rnήÓ.¿F+œ©ÍùÍ£Éõ×áâöÈøæ§#ÄÓfHù™\x>/¡ÝÏÄÆaþÎó['dØ^ˆl·^~?»²û?ýÕMZ¨Ùû‹Ï¿›Ú:[zy“0óËÑ«Á")w_VG—‡G×&W_O®|ÏmÏÝCüˆØÈjÖŸØ ­ƒxݵ¨¦fŸ‘pÑã÷Âñ5C¶Ã<ÜÈ$×î5þ¸pÿõèÂóxæq0µq÷áë§_ýn$^"Žz}ledêAt·~m|ýóàÎñõáñõ±…g¤äø“Ë!´ ýdᕾýäÆè:½¢âMúsÌÂs5óàæä=³ôâî‹ß?>û?k{-nAK¯Nl,n}}¿ñ§ÉÍs³ðòÆ­û×»²©'ï¡ÅüÝ:ÉSZFÒåÆï¾ z$[éËp‘ÌüxáÙÝg8|Màõ¤öpÿ÷÷÷¾™X{,< +æˆ};¹qLÝp5šÿÒÌL"=`ñŠžÿÒ_¼nî^ ƒÛ˜ù͉»Ÿ½ùûé­×¤š¹§ÄIˆ¯^kSë¯Æjõ›ãk$Á‘úµñzîÑÙ­åÆðø2™úöÊäÊö̽gµÇ¤ì.ï=>þ_ž|O£5Óo„ó«»fþáÍ[«ÕÛ÷æ×Ïÿéù›ÿåÎÖÁ­ù­hrÔûW ïìAÝÛ 3gbõ`óÕ_ßy||ÍÌßÕø}BN¢Öèîîõñ "–ϼÉdþáÂýWÕ‰Õ±Ú^¼ôêöÚÑÓ£"fÍ=1S¾Ps£¤“Ö4ýÇÇno%‹{ÉÒþäÚkB’¿¼~¡gÆîl“öþ¹ÇŸ{¶²ûÛ;õ¿^­ÿ ©%¿º6þ««c$>ÈøŠŸ ßÞôH¾»;{ÿkBûdî)1ÏßÜ'ACR•L¿›„coHá³[S£«7B зïGsϾP ŸWgoÄ„![ã‹Ïqvcvkxtù××LjÍ¿\Þ]|æOnÜŒî®<úZÏ<ü4Z¸6±ö%Žuo™Åz²ØX|ð–”½dþÑ8aÂⳫáÊ'# ×Câ0H¼YxòÖŸLšgr÷ÅX­±±‹Rä ÛßOl]1K÷îUQÐ`=˜y@#¼µöæöÆù­Õ×ô5±šÌ/}²øðìÅÙ¿Þ}ñýøÚ«á±ÕÏý9Ú5M&ùâÕh‰Ô•¥g¿[oüiæÁáJCÌ’~~uçÙ͉µÏôä±Õ©û8TÎmýÛ/ãßܸE4è¸u¡ÆÉû×'6ÂÅç3Þ„K/~SÑ··®GË×Õ`úA0³E·G‹/î<ýÞŸß®¨Ù«DÔ›GóÞ‘=8»ñjj•øÉòtíÙÒÖë±ÚÎøÊîÑrÅ#Ëë.±R˯'‹WÌìƒ×ÏNþf﻾óôô‹pž´ˆ›qmf¥±òô-☓[ó›'KÏ‘£¾qø©7ùo*7¯›…Ñ;ÛD57oß¿µþõí¿¾·\ÉÂÓ›IíW×FoÆwIH©»ßýäæí__'‹› –Û+»H[[}vÎ) SWÌ2‰‰_{s´5„B³¯gÖöèÕ£KÛ7'îݼýè³èÞgÁJ8¿»VÿcíÉ×#cË#£µ…G'/Nÿñõïþëòóïh›*ÃSâ~÷§Öˆi|î/ŽÝ­¿ùÓÿýàͿĵéêŸ Oßy¹öü¢Ùÿñ¾öŸ†þÔã'ßþ—ççÿ2³~@ã$‰Cë0zçéUsçj¸LªB¸H¬ìïÞþÃÿ³ðèÛñÕ½k¤Wß¾ãÖFuòž{.ìí¨ÛæïŒ-=ºNI;n–ƒ‰ÇS+‡öÿvl­q%^\Üúª~öÏjæ¡Yxê/<¾6±AB™Ô¶‰µƒxñ)ºBOÜ‹î¼\oüû̓ZxüžÜ$È¿µ6U{±¸ùÕÔzãËÑ{Ÿ+Þä“™õ³åç3µþæSoöj¸„C‘;X|úƒšr%^ºÌ'3o$wss¼rs\ݺ·ððÝÝí߯¼ú2Züõ͉êØ*ýÿ77oß]]Ù»ûâ·/Îÿ'ÿt{óÍÕQZÀéxöÉz±rcê +’Õ׽ɳ[o¿úÃß<ø÷×o­ÇwŸ/?ÿvéÙ“÷^{“Ÿ™…ÏÌâøó¯GÆÁ<ÕäƒhþÙÝ'ï–ž~72õ~˜ÛÌëéÍ‘ÉêíM2ÌìãxöL!=7rk}…/PüþüBÏ‘?NÊÒòŽ?³/mOo~½üòwž~s#YýäæÔgÕ©ëæΗjúf4_>½>öÉ =±1³v0uï+²þ>™ ܸ³ùš”ùOoNŽß}õäí¿Ì>:›Xþ*YØÁš¬¾¼ÿühdtî/®øÁÂËñ{_/>ûÃæþ?N¬}E³þ«/ã;÷^}ûÏÉâ£ÏõÜ¿ù"ùÌ_¼}å9ôõFDxþþW$ÓI7 æ×ÕioúY²¸»¹÷÷ÛoÿÕ›ØüËOÌU½@è-½ ´ÿÕc_„µ›·6â%Z½³xîñÕ¨¶²óÛ¯~ÿß¼þ³Ï?Vÿ¿KòyÑíü\ŸÁDzí3˜H¯}éµÏ`"½öL¤×>ƒ‰ôÚg0‘^û &ÒkŸÁDzí3˜H¯}éµÏ`"½öL¤×>ƒ‰ôÚg0‘^û &ÒkŸÁDzí3˜H¯}éµÏ`"½öL¤×>ƒ‰ôÚg0‘^û &ÒkŸÁDzí3˜H¯}éµÏ`"½öL¤×>ƒ‰ôÚg0‘^û &ÒkŸÁDzí3˜H¯}éµÏ`"½öL¤×>ƒ‰ôÚg0‘^û &ÒkŸÁDzí3˜H¯}éµÏ`"½öL¤×>ƒ‰ôÚg0‘^û &ÒkŸÁDzí3˜H¯}éµÏ‹ÿ÷’|V*—äó?\¹2w¼;»s¶3ôpqèÊÔ³M¯ï¼=kœ­ ]Ù™:=›=¨Ÿœïœ~W%WÕAed½±sX¹*WVè¢ÊêéÁ«ƒcnÔw×*7éÒ1úgÈ«¨ŠÇÿ{øÝÐpQÕ׊cMÿÑ•(Žüª1žo|ÏxÚ¯á"WýX‡:/Ë⢘`A’Äq„a@|W’¨(ñ¢H.*½®­‹J¯Ûz0tÛVCßžíÎ6Þì`@¨ØÙ›.|r/Ïß#½~˨KÂÂÎùÛ·;ÇÓïc/*Øm£Ýnû|~·Œ¾ý‰´Ïæw»Áç;¡•^gj'{{og琉þ¥þUžaÿÑý!tedÖOONG¿Ù¯‰“•@߶ïm´W_8îµpòöüto§Þ°)¨íÎ*wÓ€Iüsyû¦Q_= õŸç´mó??Ü999~{¶sÜþÔÊ7v!üÙé,ç¾}srÜø€Y6oì'K…ìOú´»Hß·½*ßwÖ6L¥Çݪ“méÐ1Ð%Udíäàø¬Ö‰Ëìãø*–bkVÇè?íèr†:´½®7|¨ã¸CoïQ¿™KNéžÐ¶´xýWDf?péÅça´=‘÷¨°Ù‰¨^Ö°_·Oõ¯»AôíOÄo"~&²szp¶Ô8k?ªÝOòóR†í;V +z]‚œ­í¼ÏÀˆÐöv W¤oçžäŽ ¯Kû¹Ü8}ÕÀJöŸJÔ)©]â-ùxã$ ~lNê²%ý‚Ÿ ›¶÷®Ç}©PVϧXÍœœNŸ6ß· ½ŒùUªÚöÉèÓ݃óöiÎ]~á{»{p¸Ó~ˆ»ÿÌÒK• ×öDú%ÜþÎ춿3m2•£šžœ¾Ù?9ƒÒËm°^d{lG“Jûçiû±@JÛ–yßð¶gôø@¯Pß^G™D{‡‡$¯va[Ž;mç`“Á^_>i?6sCïò—½Ó“£ö·‰/¾øSmóÀæ¼ÒׯððÛœ`á®.8mÛ&°zýüèüý™Ùéen¹x ÿ=ºx&„€pí‡äê ŸÏiƒmȶ·kw÷àìà]›•Þpñi*‡ßì|×öv‘T>Û9íHŠËõ½18;iß^>éÆDÚÞœÝöë¸Éµ]²LvŽŽ:`eéŒJ—¿Ži3=fçÕ/]ÚLû3ê—È mæÇD[w\Á`XSÿåïYäÍô]ÞLûeîú-q¦}lìÙÔω3mk£ý‘8syÄRdÒó‰3õK—8ÓþŒ‰3ƒÄ™AâLG»õ HœQ¿”Ä™e¯ äK—8ÓþŒúEïëÄ™¶£ÇIåR'ÎÔ/]âLû3ê>ð3‡ˆú'ý§ƒ­ì”‡ti+û¸Xa iƒÍèߪý³oÝÃ¥)9ug{–ëêlwæ_ºdI¬í» Å’0ž®—‚ûØÊ»Åb? <Ï€¿µÅßÂ_2k{òþ6àoþÖWümî”õíR±·ötÀÝÜmÀݘ» ”·wp·w»lÜ-6Úî,8~ɘ\Û“ÿY"¸}%QDý’‰¨íɈh@DÙƒÃ^Å´(Ó^ï 0sÇ…«d¿ n[Ä +×¾m®î|·ÝÙyÌKÆOG'ï«©Ð_5iŽw{Çïm‰šÍ{ÓØ9›íà æŽ ŸßÛ7¨ºÓîÜú©æN›ìrP£æ§§7¨Q3¨Qó ®Q£ÚÇÁ¨írÑ“©¨1íU”¡{úgŒ¾ÓÇè‡ÊG?Ò³¥ãB6½¢J]Â΋k%_t +píãûå¸@'sú…¸psä2†o:³grŠêùƒý'GoNÞ’¼zþÆõ3°¹.±†7Çþã m;_¿'Þ“Ù\zñ&_Ûy£-;Õ ]»í‰´Oú¯»AùíOä=Ù`Ù‰øÝ0QOÎögí³å~’¡ ûC–vhõ“$ýøå(/(íŸðÇmmߤ! N!âσøóOL´'âÏÕ\ÄŸñç®ÆŸ/oÏ—Aü¹<ÃAüyÄŸ;ò¾\¦ø3«›ˆ@ko¬#Õssî]Ko»1çAÌys¾˜ÀîÁÞÞùÛÆÌÉ1IËãöq­tß…#ÝwÃÓoÚçáÁ«ý3ú}¸Ž"¤mO³x[ïFr„ø7:ê»çâ5‹¶Ùüùé©ÀÍ-So³Ä—Ç–Ö/›ÔêpZ—¸-ñÀ7pÁõŠ ®A=ðà üp?ÜŸ=µKë‡û¨=~»b:m4ŽÇH»lŒ‘Œ;xu2öîàä°q6vÚØ;9Ý9~_œ}à »h,ŒÛv™î|pt~öž¾‰Yžá®¿pl ÛžSãþèÈA—¹£kÞŸÙvkÔ Œt9wfVÌ”šÕ‹úˆ3]z/ÍÀ—Ñ'¾Œ·ouR‰O/âHÄ…‡ÛFB»{}Ë7ö2©ÙÁÎ}û†ì¬˜eóÆ3gàÌ8sÎœ3gàÌ8sÎœŸA¢Áu#ÎëÙaŸÎÀ™ÓÛ¶ìÀ™óç8sº¡}\¶¼¤òLmX;©]S—³4Çø8zÝms)O_¤/ªKÜjPž£ûå9Ú/Ñãå9Úß‘/ÏÑþDå9.}‡’Ïzìy)zxp¶¶sð>÷þ@„Dè ÂÕ ÂÕ@„ö4sîuéùA¦u¿HÐAe«KUÙªãmTµú€qtw —¦ªÕ/«ôÓÆþÎîÉ7¿ì¾C—°pAÛu¼… zKAèïÃþm·Ý}WÍæE|Ûk"m·ÕåK{šVz©ìí½mœ2N»±ë~ã«<Ó_”ðá›{‰…^ÙœËÝ!}`Ä]:#NùÞ•vñt¿ÑISzùÅ;šƒöçôÍÁn öê‹è¿S{X™¶7¯}ݸªq'3i_9î†nÜtü¢Ë^_B÷G8pô§áÓßîø²¸?ÚŸÈÀýÑr¿qûcàþè%¾>p Ü}åþøÖ0âÎv:HZ»Œ&ÜÞéNýlçpåä ý|j¹¹Í=voºàyÕ?^ÆÐEçíVÛ®órçmcþ´ñõyã¸Þ¾z]¸ëâ·ªmJ¿T1:>Ù8;8«¿Çšõ%àêû‡œ~ÏÝsñ嶳ÉÏV‰Ã¼ë`jÙ[zÅ´õ—õ—zIþížµˆ|ñÅû•Ú—уjR?=½nV“T_ê»êK^µí¦\g'íÛ7']˜ÊG­#ÕoRÇÕ”>Š'éþùéËóCâÌ}èj¼œu`:PÇ{ÜŽ¿˜ò/]rÀé´ö'íëK˜…ÖÁœYh=æ,oëz;­ýyô|ZSYÙ~O}€ËÂ8kSi믆Wõ.¹ßû6ðü÷›ç¿í¯?Ûñß%Uv`öš Ø £X+p`¾QVàÀ +X—Ë +lÛ^¸ŒVàeMdóªmW”˜…© ÌÀ6ö‘r`ÌÀ80fà‡›LæšÌi`ö˜8¬.‹!ØÉLzÞ|pr²ûêt§}éíÀKÙð¢mm{P•¢7 ÀŽªôI×¾þ®°1(0úSTØø3•¿‹¯°Ñ•^6žö³Õ ¹ho÷!ézÒ‘~ôåáNýõXE@'ovêgßvà6~{öÝaûnp{õÅ'‚c®—¨:šT¿ÐÔ<±ÿHêr:M;ðK+e{¿Ñå× ÞrÅÌ™ËÈòú¸¿aÛuRlÓÍ™“cnÞÞ¾mQ¼ïÂï›ýŽýÚFðÃmpÎÌ,‹·]¼C½3v±Qßé@éËÝsñ§]ÛÖdÏO÷vêÎæ–¿i`ÿsé°/o¿®NÛ _Þº0Êo›w¾?8:ï l—^áÛ¶]˨qHtäZÏÜqáób‘ÔÍjFEÕš=`5¢ÖI˜û#U ˜¾P³¢¿ÿt¿_„b4ÐúD{ø€¾Äý¢A´­œ»5èØÊ*ßØ…“ÎrîÛ7'ǘeóÆö4О>žö4Pž>šò´aI¹µ§Ë0ø)ÜëŠÅÅ$[÷þ>õ[3>öª·-_¿'LŸÙ\Ú»šëë÷\™ˆêÂDÚß‘ö)ÿu7¿ý‰¼§*Pv"~&²szp¶Ôè C?ÉÑK™‡Ü©'¼ç¥èáÁÙÚÎÁû,ЈЋ˜H‹Ðöu¡íOd B»(B;eν.=?È´HÐË#AûÇU=h*ÙÚÑGç‹ý³%oý‡—µÒ%ì2ékû¶[=§ÿBù—¿´Óööë’ éçÚNG;ô¨¶k‹ôWÏþ¯Õ·Òî”ù[ûé.ïÙëRmn}ûþì¹~¶ON—Û À^d—PºÔeQ.ô(`—ª¿KŠ _šš"Ãm›#½^TäB\V<§Õ‹?Ü%~Яå8.g*`ÇúÜå%¤Þà N™Ý¿ˆrÚ— ÓÛÓëä¤ÚïÌÞ +xvËÚ>FÙo$Õßø¥“PÀ¹‡{ÓùƒÓ^ð¢ôÊ>Ÿí¼lûÁç©+m;Ùyî[95s÷t¯Øùq}½ÿ˜Ê¥C¶jTñ*¿ t[ [÷ÑMýR˜Ût—UØÂ9èû§;Ço÷ÚïvÑ;Ø9='¢ÆõºjúZúÁo"Së{à9ÉogãMöÀÖôÊ’|ÎÒ@?`ÝÇ´°º2uGyÛsÇ»i++€ Û+'Çkô®Ç4,àéÆ«ƒãìC+oøü´ñÝÑ˓á« Ó—$O¯ y•)úçá7CçCÍ<‡ßÑKôå+}S *Ë•'ϼÊ.®\ÖºšÄ*¨h¿ê%*® á[&)¤–B†é‹ +â€@é}­`Í;y(«C^ÕÓ§¢0I¼PÇ:¨xÕ( +#$òÃXy>ABúª”è IL 1üj¢• ãPGñ’˜~¤Mx* ½(p›Ÿ¹Àó“Êáaeª‘¢ïÃa•ÀQDóÖ^U›(® GÕPzØ°VU––«Tb*øªÐ£Ê°©IäÑ•TÃ(¦/A5òbO$®ãÅ€xq'•zcXUÚT†ýjš˜ž©üªï ¡á9ª¥ðß¾‹–L…xïû’ +ªO#Áëãȯ”g23´×ÜU¥EBl›W lˆ¢Ñú±_QQ52ø3JLRÙ¢F½)®ª$ѸÉo>&æe¥¡úq@ûUMÐ+ …&ÄS|Ïp…¡™™„o*L¡fò"‰ ˆ†wDŠVûhˆ$¡%ˆ–¾hì4ˆ€ð¦G˜–Ð/žÐK¢4V€4ô¼Q?À°‰<í1-hÚEH¬eµù6Ÿ(‡060>¿<‰tÈ¡r]M`•Ï­¹Æ§•¤7%]K"ÙÒøq•†B{Hdx‘ö9è! I"¥|Ÿ›^ækå Kñˆ¶‰8<šžGk†‡«„É’¶E@uZÚ5†Ð4–""„fŽ¡ í+Ý£Aš–‡COÕ €xrteá)‘Gø³…I¾1­€ÿ„˜ÀÜJ“rè‚ÿ%Aà\Š<ß +½0"4»”ñBOùqäiÚuAB§&1âfxê[ú³×1gÆ~ÑŸšùª¢E£•%j¢KÁti¦qàÅüx-‹B;Š—XZà?V2s=V9Ät˜¢0í„–ˆÿíù²ºÚ endstream endobj 6 0 obj [5 0 R] endobj 30 0 obj <> endobj xref 0 31 0000000000 65535 f +0000000016 00000 n +0000000144 00000 n +0000017836 00000 n +0000000000 00000 f +0000021120 00000 n +0000633418 00000 n +0000017887 00000 n +0000018284 00000 n +0000024102 00000 n +0000021419 00000 n +0000021306 00000 n +0000020161 00000 n +0000020559 00000 n +0000020607 00000 n +0000021190 00000 n +0000021221 00000 n +0000021454 00000 n +0000024175 00000 n +0000024527 00000 n +0000025972 00000 n +0000048925 00000 n +0000108714 00000 n +0000174302 00000 n +0000239890 00000 n +0000305478 00000 n +0000371066 00000 n +0000436654 00000 n +0000502242 00000 n +0000567830 00000 n +0000633441 00000 n +trailer <<500C02C772C940E1A7C5BB3C69C06AC1>]>> startxref 633644 %%EOF \ No newline at end of file diff --git a/presto-docs/src/main/resources/logo/web/main/blue/FB_Presto_Logo_DarkBlueBG.svg b/presto-docs/src/main/resources/logo/web/main/blue/FB_Presto_Logo_DarkBlueBG.svg new file mode 100644 index 00000000..a36cb2b3 --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/main/blue/FB_Presto_Logo_DarkBlueBG.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/main/white/FB_Presto_Logo_WhiteBG-01.svg b/presto-docs/src/main/resources/logo/web/main/white/FB_Presto_Logo_WhiteBG-01.svg new file mode 100644 index 00000000..de751848 --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/main/white/FB_Presto_Logo_WhiteBG-01.svg @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presto-docs/src/main/resources/logo/web/main/white/FB_Presto_Logo_WhiteBG.ai b/presto-docs/src/main/resources/logo/web/main/white/FB_Presto_Logo_WhiteBG.ai new file mode 100644 index 00000000..9e0c9578 --- /dev/null +++ b/presto-docs/src/main/resources/logo/web/main/white/FB_Presto_Logo_WhiteBG.ai @@ -0,0 +1,2523 @@ +%PDF-1.5 %âãÏÓ +1 0 obj <>/OCGs[5 0 R]>>/Pages 3 0 R/Type/Catalog>> endobj 2 0 obj <>stream + + + + + application/pdf + + + FB_Presto_Logo_WhiteBG + + + 2013-10-23T20:38:56-07:00 + 2013-10-23T20:38:56-07:00 + 2013-10-23T20:38:56-07:00 + Adobe Illustrator CC (Macintosh) + + + + 256 + 80 + JPEG + /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgAUAEAAwER AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7 FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq03LieFA1PhJ3Fe1cVfPP5PaP8AnrbfmjcT+aJL46RS cao91IWtZWKt6X1ZSeH95xK+mNlqNumZeUw4dmmAle76HzEbmE/mlbeYJ9MtRpayvbh2+uRwci52 Hp1C7lR8Vfozc9jTxRmeOr6X9rha0TMRwpj+XsOtw+XI01cSCb1GMCzV9RYaDiGrv1rSvbMftSWM 5j4fLrXK2zSiQh6mS5rnJeXy2fmP/EDMEm+verVZQGp12PLpwp9FM6WOTD4XThr8fF82ng1f5q6l 4nFz/b3fZT1DOafSXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F XYq7FXzr+bP/ADkV5u8rfmFc6FpVlbDT9MMQnFyjtJcGSNZGPIMvBaPRafP2GXiwCUbLRPIQafQO mXovtOtb0RtELqGOb0n+0vqKG4t7itMxSKbwg/NOsS6PoF5qMSCSWBV9NG6cncICadhyrmTosAy5 YwPItWbJwQJYd5A8+a3q2tHT9R4TJKjOkioEKFN9+O3E/rzb9p9mYsWPjhtTh6XUylKi9GznnYsI 83+aNVtNUNnZyehHCqlmCqS5Ycv2gdhXN1odHjnDikLt43tztjPiz+HjPCI18b97IfLGqz6npMdz cKBMGKOwFAxX9oDNfrMIx5KHJ33Y+tlqMAnL6uXv802zFdo7FXYq7FXYq7FXYq7FXYq7FXYq7FXY q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYqkWr+RfJusapDquq6NaXuo29PSupold6KaqCSPiC9uVaZI TIFAoMQUk/NH83PL/wCXlpaPqMM13eX7MLWzgoGKx05uzMQqqOQHiT260ljxGbGcxFGeQfPnl38x PLL6lYRMIC7Wt9ZXKjkkgUFkahKspVgQR1+dQCRLHLbmoIkE50ny1oWjvJJp1olu8opJJVmNK1pV yxA9hlmfV5ctCZumMMMYcgxV/wA3tIXUzbi0kaxDcTeBhXrTkI6fZ/2VfbNmOwsnBdji7v2uN+fj xVWzK9R0DSNUZJrqESOoosisVJXrSqkVGavFqsmLaJY6vszBqCJZI2R15fchtY1ax8uadDHDADyJ W3t1PEbbsSd/HLMGCWomST7y4+u1uLs/FERj5Rj96l5c82xaxM9u8PoXCLzUBuSsoNDvQbiuT1eh OIXdhq7K7bjqpGBjwyG/fbIMwHeuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KvCfzw/PjzV5H81 JoWjWdlJG9pHcm4ullkcGRnUgBJI124DrXMnDhEhZacmQg09q0a7lvNHsbuanq3FvFLJx2HJ0DGg +ZzHI3bQjMCXYq7FXYq7FXYq7FXYq84/OL8mrL8xbaxcXx03UtPLiG49P1UeOShZHTkh6qCpB232 3y7Fl4GE4cSP/LT8v9H/ACy8oz2cmoCZfUe91LUp+MEYPEKTQsQiKiDq3icjkmZlYx4QnPl3z55L 8zyT2+h6vbajLCKzQRP8YWtOXBqMV/yhtgMJR5pEgWJv+TtdSLLfhdNL8uHE+qFrXhWvH25fhnQD t70fT6/sdf8AkPVz2Zrqnmby7oZit767W3YqPTiCu7BRsCVQMQNu+afDo82azEW5s80IbEoXXNKt vM2m29xY3KNxq9vN1Rg2zA03G6/Rlmmzy00yJD3uq7W7NGsxjhNSjy7t0P5a8rHRZZb++njLqhA4 14IvVmLMF7DwyzV63xgIRBcTsnsb8pI5cshddOQHfZpMLTzdoV1dC2iuP3jHjGWVlVj4AkfrzHno csY8RDn4O29LknwRlueWxFvCvLX/ADlxFNLet5i0lLaCG2aS0WzZ5JJrgOirF8fwqCrMxYnbj3O2 MtL3Fzxm70jvv+co/wAzNQkluND0K1g06ImtYZ7plH/FkqtGn/CjJDTRHMo8Uso/Lf8A5yni1fVY NJ83WMOnvduI4NStSwt1djRVljkZ2RSf2+Zp3AG+QyaahYZRy3zfQOYrc8P8yf8AORNzoP5pv5Sv NPto9IhvILa41N5HVo4plRmlYfZATnU+wzJjguNtRyUaSfzj/wA5b2VvcyW3lPSheohIGoXxaONi D1SFKOVPizKfbJQ0vexlm7ns3kPzSfMXkbSfMV36UMl5arPd8PhiR1BEtOTNxVWU9W275jzjUiG2 JsW8Y87f85VyR6q+meSNMj1Dg5jW/uhIyzNWn7mCIo5HgS2/8uZENNtcmqWXuSL/AKGU/N/RZI5v MHl2BbN2ApNa3Voxp1CSM5WvzU5L8vA8ijxZDm91/LP80fL/AOYGkPe6aGt7u2IW+0+UgyQs3Q1G zI1DxYfgdsxsmMxO7dGYKI/Mf8w9F8h+XW1jUw0zO4hs7OIgSTTEEhQTsqgCrN2HiaAjHjMjQWUq D58m/wCcpfzO1S5kGh6JZpCnxekkNxdSqp6c3V1H/CDMv8tEcy0+KUf5d/5ym88/pm10vWvL9pcS 3E0duI4fWtJuUjBBUSGcVqenEZGWmjVgqMpewfnR+ZF7+X/le11mzs4r2W4vo7MxTMyqFeGWTlVd 61iplGLHxGm2cqD5D/Mz8wrzz55jXXLu0jspVt0tvRiZmWkbMwarb/t5n44cIpxpSsvTNO/5y08x WWn21kug2jrbRJCrmWUEiNQoJ+7KTpgerYMxfRv5f+ZLvzN5N0rXruBLafUYfWaCMlkUFiFoW3+y BmJONGm6JsW88/Nb/nI3RvJ9/Nomj2w1bW4PhuSzcLa3f+V2FWdx3VaU7mu2W4sBlueTCeWnmP8A 0M9+boQag2kWP6PO/L6pciGnT+89b/jbLvy8O9h4snqv5Tf85C6J51u00bUrcaTr7j9xFy5wXBAJ IiYgFWoK8G+gnKMuAx3HJshkt65lDY7FXYq7FXYqwP8AO/yrrfmj8uNS0nRfjv2aKZbflx9ZYZA7 RVO1TSq17gZbhkIyssMgsPD/APnHf8sfP2n/AJi2+t6lpl1pOm6dHOty13G8HrGWJoljRX4l/iYP WnH4etaZk58kTGg1Y4m31ZmC5Dx78xvLeuHzLcXsdtLc211waKSJGenFApVuNaEcc63snV4vBESQ DHvdRq8MuMmrBZv+W2kajpflz0r9DFLNM8ywt9pEZVUAjsfhJp75pe188Mma47gCnN0mMxhum/mO 1nvtCuoLX45ZFUoAftcWDEA+4FMxdJMQygy5NHa2CebTThD6j+ggvNLLSNTubxbaKCRZuVGJUrw3 6sT0pnR5M8Ix4iRT51p9DmyZBCMTxe7l7+588/kR5c0bzD+Zul6brFst5YMs8r27khGaKFnTlQio 5AVHQ99s0uaREbD6XjFl9v2tra2lvHbWsKW9vEOMUMShEVR2VVAAGa23LfLH/OV3kvTdJ17S/MGn wJb/AKYWWO+jjAVTPAVPqkD9qRZPi/1a9TmdppkinHzRo296/JzzDP5g/LPy/qdw5kuWtvQnkP2m ktnaBmb3Yx1zFyxqRDdA2Hyt+dFmt7+eWsWTMUW5vbaEuBUgSRRLWntXM3EagHHn9T6V8qf84/fl h5dRGGlrqt4o+K61KlwSfERECFfoSuYks8i3jGAl/wDzkZrp8uflRc2mnhbX9JzRaZGsQCBY5A0k qqF2AaOJl+nDgjckZDUWKf8AOJflDT4/L9/5qmgV9RuLlrO1mYVMcESKX4E9ObuQ1P5cnqZb0xwj a3uusaPpus6Xc6Xqdut1Y3cZinhcVBVh+BHUEbg7jMYEg2G0i3yJ+Stzd+UPz2GhiRjC11d6Pdiv 2xGXVCabbSxKcz83qhbjQ2lTMv8AnMU3Xq+VRv8AVeN7Tw9SsFa/7GlMr0vVnm6PS/8AnHe50CX8 rNJi0p4TNCrjUo46eotyZGLeqPtVPav7NKbZTnviNs8dU9Bu9M069aFry1huWt5Fmt2mjWQxyIQy OnIHiysKgjplQLZStNBBMoWaNZVBqFcBhXxocCvj/wD5yoghh/M2JIY1jT9G254oAor6kvYZn6b6 XGy831R5a03Tj5c0om1hJNnbkkxr/vpfbMKR3cgDZR8++YP8L+R9Y1qBVWTT7SR7VaDj6pHGEEeH qMuGEeKQCJGg+Y/+ca/JNl5u866hrOuoL+30lVuHjn+NZbu5dvTaXlXlTg7UPU0zM1E+GNBoxRs7 vrwxoUMZUGMjiUI2p0pTMByXx1/zkJ5Pg8j/AJiWmqeXh9Qgv0W/tVh+EQXUUnx+kP2RyCuB2J22 pmwwT4o0XGyRo7Pq/wAn68vmDyrpGtqAp1G0huHQbhXdAXX/AGLVGYM40SHIibCb5FLsVdirGvPG qanYWlubJjGsrMJZlFSKAcVqeld/uzY9nYYTkeLennfaLWZsGOPh7Anc/cPj+hF+Ub+/vtHWa93k DsqSEULoKUb76j6Mq12KMMlRcvsPU5c2nEsnOzv3jv8A0JlqU1xBp11PbJ6txFDI8MdK8nVSVWg8 TmIHbl8BS+ffO7+YDrr6zeDWPU5/WRK4YGteIWvEJ24U402pTNpwRqqcPiN2++NDuby60XT7q+j9 G9ntoZbqGhHCV4wzrQ7ijEjNWeblhgv5wahqEUNhZxM0dnOJGm4mgdl40VqdhWtM6DsHFAmUj9Qp 1+vkRQ6JL+VGoX8fmL6lG7GzmjdpotyoKiqvTsa7V98zO28UTh4j9QOzToZHjro9hzknbvi3/nGf /wAm9pf/ABhu/wDqHfNjqPocXF9T7SzXOU+f/wDnMAj/AA55fHf65L/yaGZWl5lpzcmXf84z/wDk odL/AOM13/1EPkNR9bLF9L5//Nf/ANaCvv8AtpWX/EIcysX920z+p9pZrnKeE/8AOXgP+CdGPb9J D/qHlzJ0v1FpzcnkX5e3f5+ReXETyUt6dD9WQp9XigdPVr8e8iluuZExjv1c2uPFWzJf0h/zlr/J qX/Ii1/5oyFYmVzQn5cflt+aUf5raV5h17RbtA9+13qN7KqAc5CzO7cTQVZuww5MkeGgURibsvo7 8yfy50Xz75eOkakzQSRv61leRgF4ZQCOQB2ZSDRl7+xoRh48hibDfKNh81av/wA47/m95VvWvfLs hvljqY7zTLg29wF942aN6+yFszBnhLm0HHIclTyx/wA5C/mf5P1VdM82xS6jbQkJc2d/GYb2NfFZ CFct3/ecq+3XGWCMhYUZCOb6u0LWtO1zR7PWNOk9Wxvolnt3pQ8XFaEdmHQjscwZCjRcgG3yb/zl b/5M+L/tm2//ACclzO030uPm5vq7yz/yjek/8wdv/wAmlzBlzcgcmE/85FBz+TfmHhWtLStPD67B X8MtwfWGOT6Xnn/OHZT6h5pApz9Wzr404zU/jluq6NeHq+i8xG980/8AOYxh9fymBT1uN/z8eNbf j+PLMzS9WjN0es/kR6v/ACqPy36teX1d6V/l9Z+P/C0yjN9ZbMf0hnuVM3YqxnWfO8Onam1kLYzC KnrScuNKgH4RQ1pXNlp+zjkhxXVvOa/2hjp83h8PFXM39zIkaK4gVqB4pVDAMKggio2Oa8gxPmHf xMckQecSFLUL2HT7GW6kH7qBa8V79gBksWM5JCI5lr1OojgxGZ5RCUeXfN0esXMls1uYJVUulG5g qCAey775l6vQnFESuw6nsrtwaqZgY8Jq+dqDflh+Xza7+nW0CzOql/VNwYx/eVr6nD7HOu/LjWu+ YfiSqrd5wDm8L/N//nIvzjpfnO+0Lyy0NlZ6VIbeWd4lmkmmUfvCfUDKqq3wgAe9d6DJxYARZap5 DdB6V+UHneD80vJkx8wWUTX9hP6F2EBVHJUMkse9UJBIIDdvA0wcc8E7gaSAMkakGf6P5c0XRlca barb+rT1GqzsadAWcs1PauQz6vJm+s3TLHijD6QmWY7Y+HvyB13SNE/NHS7/AFa7jsrILPE1xKeM atLCyJybooLHqdh3zZZ4kx2cTGaL7ehuIJ4VngkWWBxySVGDIynuGGxGa1y3yb/zlJ580vzB5j07 RNJnS6g0VJTczxEMhuJytUDCob01jHTuSO2Z2mgQLLjZZWX0P+Uflufy3+W+g6RcoY7qG29W5jPV Jbh2nkQ+6tIRmLllciW+AoPlv81//Wgr7/tpWX/EIczcX9248/qfaWa5ynl//OSHlm4138rr1rZD JPpMsepLGvUrCGSU/wCxildvoy7TyqTXlFhgP/OKPn7SYNNvvKF/cx2941ybvTRKQnrCVFSSNCer K0Ybj1Ndum1upgbthhl0fR2Yjel1r5k8vXeqPpNpqVrcanFGZpbOKZJJUjVlQsyKSVHJwN8JiatF hbrfmjy7oTWo1nUYNOF65itmuXEau6jkRyaijbxOERJ5KSAmEFxBcRLNBIs0TiqSRsGUj2I2yKXz R/zl3qPl2e50K0gkil1229f60IyC8cDcOCS06VapUH38czNKDv3NGYh6x/zj9bXNt+T/AJcjuVKy NHPKobr6ct1LJGfkUZSMoz/WWzH9LwL/AJyvVl/M6AkEBtMtypPcerMKj6RmVpvpac3N9T+T7u1u /KWjXVtKstvLY27JKpqCPSXMKY3LkR5KHnny+nmnyVq+ixOrNqNrJHbyVBQS05RNXwEgBxhLhkCi QsPl3/nHLzxaeS/O+oaRr8g0+01NRbXEkx4LDd2zt6YlJ2UfE6EnoSO1czc8OKNhoxyo7vr1rq2W 3Ny0qLbBfUM5YBOFK8uXSlO+YFOS+Ovz884w+fvzFtNN8uk31rZKun2TR7rPcyyfG0Z7qWKoD0PG o2zYYIcMbLi5JWdn1n5S0KPy/wCV9K0SMhhp1rDbM4/aaNAGf/ZNU5gylZtyQKCbZFKBtNd0i7um tba6SSda/AK7068SRRvoy+emyRjxEUHCw9o4Ms+CEwZfjl3/AAQ2oeVdHv70XlxGxl25hWID0FBy Hy8Msxa3Jjjwg7OPquxtPnyeJMerrvz96NvdQsNNtxJdSrBCPhWtfDoAKk/RlOPFPIaiLLmajVYt PC5nhi2klhqlieDLcWk6lTToR0I8RiRPHLukFjLFqcW1ShJCaR5b0vSpZJbVG9SQcS7tyIWteI9s sz6ueUVLk42h7Kw6YmUBue/7lY67pAvvqBuk+tV4+nv9r+XlTjX2rkfy2Th4q9Lb/KODxfC4xx93 42t5B+aH/ONNn5t8yTa/pWqjTLm8Ia+t5YjLGzgBTIhVkKkgbjep8Mlj1HCKLkTxWbZ/+WP5b6V5 A8uDR7GVrqaWQz3t66hGllIC1CgtxUKoCrU5VkyGRtnGNBl2VsnYq+C/yn8lWPnTzrbeXr2eS1hu oZ2E8PEsrxRM6GjAgjku48O4zZ5Z8Itw4Rs09KvP+cTvzBgkeDTde0+Wxau8z3NuWrseUaRzLuP8 rKRqY9Q2eCWa/ln/AM4wad5f1SDWfM16mq3lqwktrGFCLZZF3V3Z/ikodwOKjxrleTUWKDKOKub3 XMZueH+bv+cc73X/AMxZ/Nya5HbxzXUFyLM27MwEKoOPP1B19PwzJjnqNU1Sx2be4ZjNrTKrKVYA qRQg7gg4q8B89/8AOKGlalfS3/lTUF0ozMXbTbhC9urHr6Tqeca/5PFvag2zKhqSObTLD3MU/wCh WfzTmC2915g082a1AX6xdyAA7mkbQqu/zyf5mPcx8IvSfyi/5x9/wHrn6duNba9vTC8Bt4YRFDxk oSGZmdmoVBH2cqy5+IVTOGOt09/OH8n4PzGtbBW1WXTbjTfVNvSNZoW9bhy5pWNq/uxQ8tvDIYsv AynDieLyf84n/mNbSMuna5p3pN1ZpbqBjTpVUhkH45k/mY9zV4JT/wAnf84jpBeRXPm3VkuoIyC2 n2AdVkI7NO/BgviFSp8RkJ6ruCY4e99FW9vBbQR29vGsMEKrHFEgCqiKKKqqNgABQDMRvedfnJ+T Nh+YdpbTR3X6P1qwDLbXRXnG8bGpilUEGld1I6b7GuXYsvB7mE4cTxmD/nE38xiTbza3psdkTuEl un71r6ZhRff7WZH5mPc1eCX0l5G8u3HlvyjpWhXFyLuXTrdYGuFUoG49KKSxFBt1zDnKzbfEUKef /mp/zjvoHnS+fWdPuf0Prcv+9EgT1IJyBQNIgKlX/wApT8wTluPOY7dGE8YLy0f84m/mMWFu2taY LHlXaW6J+fp+iFr/ALLL/wAzHua/BL1n8qf+cf8Ay95Gul1a5nOra+qlY7pl9OKAMCG9GOrfEQaF mJPhTfKMucy26NkMYD1XKGxbLGJInjOwdSpI60IphiaNsZx4okd7EtC8k3en6ul5NcI8MJJjCV5N UFfiBFB18Tm11PaMcmPhA3Ly3Zvs9PBnGSUgYx5VzPv7vtZfmperSLzX5dm1mCD0JVjmgLEB68SH pWpAP8vhmdotWMJNjYuk7a7Llq4x4SBKN8+W/wDYiPLejPpGm/VpJBJIzmSQrXiCQBRa9qLler1H iz4gNnI7J0B0uHgJs3ZTXMV2bDpPIty2uG7+sp9Uab1j19TduXGlOP01zbjtKIxcNeqq8nkpezkz qfE4hwcXF587ru+NsxzUPWuxV2KuxV8W/wDOM/8A5N7S/wDjDd/9Q75sdR9Di4vqfaWa5ynYq7FX Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX//2Q== + + + + uuid:0dd95273-5bdf-bc4b-ad5a-8acc8eb336aa + xmp.did:a2c08580-f82a-4993-b0de-979ce9677d23 + uuid:5D20892493BFDB11914A8590D31508C8 + proof:pdf + + xmp.iid:c2b1bc2c-62b5-4bfb-8e8a-d96a731546f7 + xmp.did:c2b1bc2c-62b5-4bfb-8e8a-d96a731546f7 + uuid:5D20892493BFDB11914A8590D31508C8 + proof:pdf + + + + + saved + xmp.iid:a478fe57-ac17-43dd-a456-3029de78c422 + 2013-10-23T20:23:29-07:00 + Adobe Illustrator CC (Macintosh) + / + + + saved + xmp.iid:a2c08580-f82a-4993-b0de-979ce9677d23 + 2013-10-23T20:38:55-07:00 + Adobe Illustrator CC (Macintosh) + / + + + + Print + Document + False + False + 1 + + 6.955882 + 4.317257 + Inches + + + + Cyan + Magenta + Yellow + Black + + + + + + Default Swatch Group + 0 + + + + R=56 G=227 B=255 + PROCESS + 100.000000 + RGB + 30 + 220 + 255 + + + R=0 G=0 B=0 + PROCESS + 100.000000 + RGB + 0 + 0 + 0 + + + R=88 G=144 B=255 + PROCESS + 100.000000 + RGB + 88 + 144 + 255 + + + R=255 G=255 B=255 + PROCESS + 100.000000 + RGB + 255 + 255 + 255 + + + R=15 G=38 B=72 + PROCESS + 100.000000 + RGB + 15 + 38 + 72 + + + + + + + Adobe PDF library 10.01 + + + + + + + + + + + + + + + + + + + + + + + + + endstream endobj 3 0 obj <> endobj 7 0 obj <>/Resources<>/ExtGState<>/Properties<>>>/Thumb 12 0 R/TrimBox[0.0 0.0 500.824 310.843]/Type/Page>> endobj 8 0 obj <>stream +H‰ÔWÛŽ7 }Ÿ¯ÐXI]_»-úEúF/Ùiþè!)íµ×Iœ"M±€wŽ†’(’çˆó槧ðæíS +ß}ÿ¶÷[ +B)ö,¡$üçkàï߶_Ã_Û›§_R8~)¦2ðK?Ü9„G¼ýoÿø°½þ(PâX @oúêy£8Z Š¥S(‘¤c7Øpè±Yà¸Q…yaê‘ðãà²qL#«é|I‘k ˜Øà%·˜¤……ûÇ­DieAŽL9°zS–HU`ôç6bOð¥ÇŒ•ž1«ŽRì'À.Cg +V阅GxÐqêwÞÖL×B±âPÀü:á³;V«ÚS”œƒ ÔØÛ“á˜ñ™áÖ:ns‹‰9#¬GI}G(ªjë› 0§Øˆ×±.Ž|Ü~ß~>OhQj³ªõ•ÐCŽ™«®Ùá9æÁ2Œ8ciìÌšÔ‰«î"±¶j@ÞG¾1Rb‘lXbF™œ0džإ8²…‘RƒàPCó’R›!›Na¨7Ëë‹SU¼)HeLîƒÏÊ´ÅÖ±‡¦Â ÇÌ'ØQ,1”*/Qëˆç(Kä’uzh³Í­\´r*6ƒÿXDÏYH«2 ]CEh¶³Ô…°fFÁ”ý-¢Ý;”|ÎF'.j­e0›5áE±ÕbuYµ”çj ºê= "]‚"ÏûÀªj8š°Ý^䢞wây9œ¨šŠFfðÇåÆĨJ*±`³…@éY² +g¬Þ]åÖ.J{-gMý©2%Ôí‰A14²šÌ¤RRgIb+ÄÒsƒr‚SP>D®Í¸jLÚ88n9Žù¬¤í˜ÑçVdVç'D‘ ¯“‹”oòiY-ÕÅ¡§AÂÜG§±æoA„ˤ‘t Z—³2™òšŸ@ݬµ µj«ˆ²º™+ÝLÀ´ •Xð ›KŽùTÑ“(L»Wœ'F¦zÑYI+ÓdL%݃tæ$Ö¼ª¦"ƒ8m³§fë k¢tQ1L; dšœ €å#¡$Ó$׋)¸nTIŲ·3žlgd£"œ—˜ƒ×¢š¹ÃªÄvk}P +VÆ=ƒ9´#Øc2±¨v™J±êd»1 áwÝ›S^d¤·óTºfHÎð>Coæ3¨;ÊÙ–Ëþ個ö #)zÅYê» k†Ž*ûCkÎßLI ø‡] 2‹÷v_¶i¾p¹Q©X¾Ÿ¶ƒ7ì„íiܱ*éG‰ÉÉb(a»ÀØ:…d²”ö§ã¦ohÇ:÷ií«Nì{úÄtÒêYÝϦÒó>r°FE½œÁ“·kÄÛ‹5ߥ#írbÃ2á©ùÑ) íK®ßrÎ\œuh»©VòŽí” ©ÈFBòi•ÏKj´b­ZCQ!ß5PRM“QESÅÍg8˜ÜÒ­ltT-$̤ÝÆ·pCv»[Ô–VízEÿ‡z»p^åeý+VÈ ©j’-¦²«³}ÜÁ´u#ÿ5éÒQµØWuó´Ûø¨ê3ÏuoúÝ»6Ø8`C?W.\gHYë:ÙzÖ–çz¿²Ý]¸©Ûø¯9^ÕÕ¡m½¯âÆÉL´H|Ôì|9ÿ½åvNPÒ*(ÐÒbÉNKö¾Ï;òÎÀzJ­Os[?,p7Nó‰Ô|™ù¯uºÆ´˜ëO”v+¶ùþè[ž|‡fýZ)Úo\U?¤W:Ò5´”Ëê/®þ—Ê6Y‹æ¨X÷dçi¦=ÓØÁ´u#ÿµÂ×Q·˜‹·)wËÆGÕ°ßÉGnî4|à @r&R^·_·ß%®œWî·Ÿ·q!G„N¡¿j#òµÕ•¨ßS"ˆhׯVŽ=¡>G‰®z þZRt¯—¶n¢iJª2Ü%rûb"·GˆüJ7aD0‘™ÆÓYM>ä>M©eŠ½ð+ +*—o#ð{͘ëT¿~ÍÈ£(:¸TÐN×énÔëGý¡{ëµæ=gÏõUî?¼} +ø÷ð\Þë endstream endobj 12 0 obj <>stream +8;Z\q0b2)D$q997bBRiHQoIG\+!:gffBG4hX!e]26J]e.OKCpro/FJ5daBco$JSD" +>dk'a:a`kH#7T(',^"Lfmrra/F1p64Y+ga\9%X!7u/',M1rt94n.,B?<:p +aC/aMoBL8$FmiWMhNR^F&jB"7_0Nm\rRQ/\IG]/3m_Z6\Od.jGEF.ZA>FcHgl6H2+ +q;gLO&&u2S_,s?'J#q6%S]!4Q~> endstream endobj 13 0 obj [/Indexed/DeviceRGB 255 14 0 R] endobj 14 0 obj <>stream +8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0 +b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup` +E1r!/,*0[*9.aFIR2&b-C#soRZ7Dl%MLY\.?d>Mn +6%Q2oYfNRF$$+ON<+]RUJmC0InDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j$XKrcYp0n+Xl_nU*O( +l[$6Nn+Z_Nq0]s7hs]`XX1nZ8&94a\~> endstream endobj 5 0 obj <> endobj 15 0 obj [/View/Design] endobj 16 0 obj <>>> endobj 11 0 obj <> endobj 10 0 obj [/ICCBased 17 0 R] endobj 17 0 obj <>stream +H‰œ–yTSwÇoÉž•°Ãc [€°5la‘QIBHØADED„ª•2ÖmtFOE.®c­Ö}êÒõ0êè8´׎8GNg¦Óïï÷9÷wïïÝß½÷ó '¥ªµÕ0 Ö ÏJŒÅb¤  + 2y­.-;!à’ÆK°ZÜ ü‹ž^i½"LÊÀ0ðÿ‰-×é @8(”µrœ;q®ª7èLöœy¥•&†Qëñq¶4±jž½ç|æ9ÚÄ +V³)gB£0ñiœWו8#©8wÕ©•õ8_Å٥ʨQãüÜ«QÊj@é&»A)/ÇÙgº>'K‚óÈtÕ;\ú” Ó¥$ÕºF½ZUnÀÜå˜(4TŒ%)ë«”ƒ0C&¯”阤Z£“i˜¿óœ8¦Úbx‘ƒE¡ÁÁBÑ;…ú¯›¿P¦ÞÎӓ̹žAü om?çW= +€x¯Íú·¶Ò-Œ¯Àòæ[›Ëû0ñ¾¾øÎ}ø¦y)7ta¾¾õõõ>j¥ÜÇTÐ7úŸ¿@ï¼ÏÇtÜ›ò`qÊ2™±Ê€™ê&¯®ª6ê±ZL®Ä„?â_øóyxg)Ë”z¥ÈçL­UáíÖ*ÔuµSkÿSeØO4?׸¸c¯¯Ø°.òò· åÒR´ ßÞô-•’2ð5ßáÞüÜÏ ú÷Sá>Ó£V­š‹“då`r£¾n~ÏôY &à+`œ;ÂA4ˆÉ 䀰ÈA9Ð=¨- t°lÃ`;»Á~pŒƒÁ ðGp| ®[`Lƒ‡`<¯ "A ˆ YA+äùCb(Š‡R¡,¨*T2B-Ð +¨ꇆ¡Ðnè÷ÐQètº}MA ï —0Óal»Á¾°ŽSàx ¬‚kà&¸^Á£ð>ø0|>_ƒ'á‡ð,ÂG!"F$H:Rˆ”!z¤éF‘Qd?r 9‹\A&‘GÈ ”ˆrQ ¢áhš‹ÊÑ´íE‡Ñ]èaô4zBgÐ×Á–àE#H ‹*B=¡‹0HØIøˆp†p0MxJ$ùD1„˜D, V›‰½Ä­ÄÄãÄKÄ»ÄY‰dEò"EÒI2’ÔEÚBÚGúŒt™4MzN¦‘Èþär!YKî ’÷?%_&ß#¿¢°(®”0J:EAi¤ôQÆ(Ç()Ó”WT6U@ æP+¨íÔ!ê~êêmêæD ¥eÒÔ´å´!ÚïhŸÓ¦h/èº']B/¢éëèÒÓ¿¢?a0nŒhF!ÃÀXÇØÍ8ÅøšñÜŒkæc&5S˜µ™˜6»lö˜Iaº2c˜K™MÌAæ!æEæ#…åÆ’°d¬VÖë(ëk–Íe‹Øél »—½‡}Ž}ŸCâ¸qâ9 +N'çÎ)Î].ÂuæJ¸rî +î÷ wšGä xR^¯‡÷[ÞoÆœchžgÞ`>bþ‰ù$á»ñ¥ü*~ÿ ÿ:ÿ¥…EŒ…ÒbÅ~‹ËÏ,m,£-•–Ý–,¯Y¾´Â¬â­*­6X[ݱF­=­3­ë­·YŸ±~dó ·‘ÛtÛ´¹i ÛzÚfÙ6Û~`{ÁvÖÎÞ.ÑNg·Åî”Ý#{¾}´}…ý€ý§ö¸‘j‡‡ÏþŠ™c1X6„Æfm“Ž;'_9 œr:œ8Ýq¦:‹ËœœO:ϸ8¸¤¹´¸ìu¹éJq»–»nv=ëúÌMà–ï¶ÊmÜí¾ÀR 4 ö +n»3Ü£ÜkÜGݯz=Ä•[=¾ô„=ƒ<Ë=GTB(É/ÙSòƒ,]6*›-•–¾W:#—È7Ë*¢ŠÊe¿ò^YDYÙ}U„j£êAyTù`ù#µD=¬þ¶"©b{ųÊôÊ+¬Ê¯: !kJ4Gµm¥ötµ}uCõ%—®K7YV³©fFŸ¢ßY Õ.©=bàá?SŒîÆ•Æ©ºÈº‘ºçõyõ‡Ø Ú† žkï5%4ý¦m–7Ÿlqlio™Z³lG+ÔZÚz²Í¹­³mzyâò]íÔöÊö?uøuôw|¿"űN»ÎåwW&®ÜÛe֥ﺱ*|ÕöÕèjõê‰5k¶¬yÝ­èþ¢Ç¯g°ç‡^yïkEk‡Öþ¸®lÝD_p߶õÄõÚõ×7DmØÕÏîoê¿»1mãál {àûMśΠnßLÝlÜ<9”úO¤[þ˜¸™$™™üšhšÕ›B›¯œœ‰œ÷dÒž@ž®ŸŸ‹Ÿú i Ø¡G¡¶¢&¢–££v£æ¤V¤Ç¥8¥©¦¦‹¦ý§n§à¨R¨Ä©7©©ªª««u«é¬\¬Ð­D­¸®-®¡¯¯‹°°u°ê±`±Ö²K²Â³8³®´%´œµµŠ¶¶y¶ð·h·à¸Y¸Ñ¹J¹Âº;ºµ».»§¼!¼›½½¾ +¾„¾ÿ¿z¿õÀpÀìÁgÁãÂ_ÂÛÃXÃÔÄQÄÎÅKÅÈÆFÆÃÇAÇ¿È=ȼÉ:ɹÊ8Ê·Ë6˶Ì5̵Í5͵Î6ζÏ7ϸÐ9кÑ<ѾÒ?ÒÁÓDÓÆÔIÔËÕNÕÑÖUÖØ×\×àØdØèÙlÙñÚvÚûÛ€ÜÜŠÝÝ–ÞÞ¢ß)߯à6à½áDáÌâSâÛãcãëäsäüå„æ æ–çç©è2è¼éFéÐê[êåëpëûì†ííœî(î´ï@ïÌðXðåñrñÿòŒóó§ô4ôÂõPõÞömöû÷Šøø¨ù8ùÇúWúçûwüü˜ý)ýºþKþÜÿmÿÿ ÷„óû endstream endobj 9 0 obj <> endobj 18 0 obj <> endobj 19 0 obj <>stream +%!PS-Adobe-3.0 %%Creator: Adobe Illustrator(R) 17.0 %%AI8_CreatorVersion: 17.0.0 %%For: (Brent Couchman) () %%Title: (FB_Presto_Logo_BlackBG.svg) %%CreationDate: 10/23/13 8:38 PM %%Canvassize: 16383 %%BoundingBox: 96 -505 404 -408 %%HiResBoundingBox: 96.0111903913639 -504.737810490416 403.320695133076 -408.616260490418 %%DocumentProcessColors: Cyan Magenta Yellow Black %AI5_FileFormat 13.0 %AI12_BuildNumber: 256 %AI3_ColorUsage: Color %AI7_ImageSettings: 0 %%RGBProcessColor: 0 0 0 (R=0 G=0 B=0) %%+ 0.059415001422167 0.150419995188713 0.282377988100052 (R=15 G=38 B=72) %%+ 1 1 1 (R=255 G=255 B=255) %%+ 0.117647059261799 0.862745106220245 1 (R=56 G=227 B=255) %%+ 0.345098048448563 0.564705908298492 1 (R=88 G=144 B=255) %%+ 0 0 0 ([Registration]) %AI3_Cropmarks: 0 -612 500.823529411764 -301.157476056923 %AI3_TemplateBox: 396.5 -306.5 396.5 -306.5 %AI3_TileBox: -127.588235294118 -744.578738028462 606.411764705881 -168.578738028462 %AI3_DocumentPreview: None %AI5_ArtSize: 14400 14400 %AI5_RulerUnits: 0 %AI9_ColorModel: 1 %AI5_ArtFlags: 0 0 0 1 0 0 1 0 0 %AI5_TargetResolution: 800 %AI5_NumLayers: 1 %AI9_OpenToView: -6 -289 3.16 1624 1073 18 0 0 46 64 0 0 0 1 1 0 1 1 0 1 %AI5_OpenViewLayers: 7 %%PageOrigin:90 -702 %AI7_GridSettings: 72 8 72 8 1 0 0.800000011920929 0.800000011920929 0.800000011920929 0.899999976158142 0.899999976158142 0.899999976158142 %AI9_Flatten: 1 %AI12_CMSettings: 00.MS %%EndComments endstream endobj 20 0 obj <>stream +%%BoundingBox: 96 -505 404 -408 %%HiResBoundingBox: 96.0111903913639 -504.737810490416 403.320695133076 -408.616260490418 %AI7_Thumbnail: 128 40 8 %%BeginData: 5528 Hex Bytes %0000330000660000990000CC0033000033330033660033990033CC0033FF %0066000066330066660066990066CC0066FF009900009933009966009999 %0099CC0099FF00CC0000CC3300CC6600CC9900CCCC00CCFF00FF3300FF66 %00FF9900FFCC3300003300333300663300993300CC3300FF333300333333 %3333663333993333CC3333FF3366003366333366663366993366CC3366FF %3399003399333399663399993399CC3399FF33CC0033CC3333CC6633CC99 %33CCCC33CCFF33FF0033FF3333FF6633FF9933FFCC33FFFF660000660033 %6600666600996600CC6600FF6633006633336633666633996633CC6633FF %6666006666336666666666996666CC6666FF669900669933669966669999 %6699CC6699FF66CC0066CC3366CC6666CC9966CCCC66CCFF66FF0066FF33 %66FF6666FF9966FFCC66FFFF9900009900339900669900999900CC9900FF %9933009933339933669933999933CC9933FF996600996633996666996699 %9966CC9966FF9999009999339999669999999999CC9999FF99CC0099CC33 %99CC6699CC9999CCCC99CCFF99FF0099FF3399FF6699FF9999FFCC99FFFF %CC0000CC0033CC0066CC0099CC00CCCC00FFCC3300CC3333CC3366CC3399 %CC33CCCC33FFCC6600CC6633CC6666CC6699CC66CCCC66FFCC9900CC9933 %CC9966CC9999CC99CCCC99FFCCCC00CCCC33CCCC66CCCC99CCCCCCCCCCFF %CCFF00CCFF33CCFF66CCFF99CCFFCCCCFFFFFF0033FF0066FF0099FF00CC %FF3300FF3333FF3366FF3399FF33CCFF33FFFF6600FF6633FF6666FF6699 %FF66CCFF66FFFF9900FF9933FF9966FF9999FF99CCFF99FFFFCC00FFCC33 %FFCC66FFCC99FFCCCCFFCCFFFFFF33FFFF66FFFF99FFFFCC110000001100 %000011111111220000002200000022222222440000004400000044444444 %550000005500000055555555770000007700000077777777880000008800 %000088888888AA000000AA000000AAAAAAAABB000000BB000000BBBBBBBB %DD000000DD000000DDDDDDDDEE000000EE000000EEEEEEEE0000000000FF %00FF0000FFFFFF0000FF00FFFFFF00FFFFFF %524C45FD58FFA852A8FD07FFAF5B85FD07FFAF3D438BFD68FF000005FD07 %FF5B5B30A9FD06FF3D433D3DAFFD67FF2E0052FD07FF7F315BA9FD06FF67 %1B433DFD68FFA87DA8FD07FFA98585FD07FFAF6760AFFD83FFAFFD69FFA8 %287EFD07FFAF5B7FA9FD06FFAF3D4361FD68FF050000A8FD06FF5B5B3185 %FD06FF3D433D43AFFD66FFA80500007DFD06FF5B305B5BFD05FFAF671B3D %1AAFFD67FF7E537DFD07FFA97F5BFD07FF8B433D8BFD7DFFAFAFFD04FFA9 %FD69FFA82E59FD07FFAF5B5BA9FD06FFAF3D433DFD67FFAF0000F859FD06 %FF5B5B305BA9FD05FF3C3D3D3D60FD66FFA828050553FD06FFFD045BFD06 %FF673D433D8BFD3EFFA85959A8FD25FF520505A8FD06FF7F5B3185FD06FF %613D1B3D85FD3EFF530000A8FD26FFA8FD09FFA9AFFD08FF8B8BAFFFFFFF %AFFD3BFF5900057EFD2AFFA82852A8FD06FFA95B5B7EFD06FFAF3C3D3DAF %FD39FF530500A9FD2AFF0506002EFD06FFFD045BA9FD05FF3D433D433D7E %A8A8FFA87E535353A8FD06FFA8A87EFFFFFFA87D7EFD05FF7E7D525959A8 %FD08FFA87E5353537E84FFFFFFA8A8A853000559A884A884FD06FFA85359 %537D84FD19FF7E00000600FD05FFA95B555B30A9FD04FFAF3D3D433D3D28 %007D7D28000000050059FD05FFAF00057EFF2E280053FD04FF2806FD0500 %53A8FD05FF5306FD0500052EFFFF7D0005002805060028002EFD04FFA853 %FD0500052EFD19FF28000059FD06FF5B5B315BA9FD05FF613D3D43610005 %000000282E2E0005F853FD04FFA805002E0500000028FFFFFF0500002E53 %532705002EA8FFFFFF5300000528532828F852FFFF530000050028000500 %052EFFFFFFA805000505522828000005FD19FF597DA8FD07FF7E85A9FFFF %FFA9FFFFFF616160FF28052828A8FFFFFFA8052800A8FD04FF0528000505 %537DA9FFFF5305007EFD04FF5305007EFFFFFF2E0028A8FD04FFA8A8FFFF %FFA8FF52060584A8FFA8FD04FF53002859FD04FF53000553FD13FFAFFD08 %FFA828527EFD06FFAF5B5B5BFD06FF000505FD06FF7E000552FFFFFFA805 %002853A9FD04FFA8280052FD06FF270027FFFFA900052EFD0CFF590006A8 %FD06FFA800002EFD06FF2E0028A8FD11FF613D3DAFFD06FF00000005A8FD %05FF555B555B5BFD05FF28002EFD07FF28002EFD04FF05052EFD06FFA800 %057DFD06FF590028A8FFA8280053FD0CFF530500A8FD06FF530500A8FD06 %FF7D05007EFD11FF433D438BFD05FF7E0605280084FD04FFA9FD045B7FFD %05FF000528FD07FF280000FFFFFFA8280052FD06FF530000A8FD06FF5900 %00A8FFFF0505007EFD0BFF5900057EFD06FF2E0028A8FD06FFA800002EFD %11FF613D3CAFFD05FFA900000005A8FD05FF5B5B305B5BFD05FF28002EFD %07FF530028A8FFFFFF05052EFD06FF7D00287EFFA8FFA8FFAF7D0028A8FF %FF7D0028002E53A8FD08FF530500A8FD06FF272828FD07FFA9280053FD12 %FFAFFD08FFA82E527EFD06FFAF5B5B7FFD06FF000528FD07FF2E0000AFFF %FFA8280052FD06FF53000028000500280005000600A8FFFFFF5300050000 %002853FD06FF5900057EFD05FFA928002EFD08FF050528FD0DFFAFFD09FF %AFFD08FFA8282853FFFFFFA9FD07FF28002EFD07FF530028A8FFFFFF0505 %52FD06FF7D002800280005002800060028A8FD04FFA8522E0505000528FD %05FF530505A8FD06FF050528FD08FF28002EFD0CFF614361FD07FF8B433D %AFFD06FF000500007DFD0AFF000527FD07FF280500A8FFFFA8050053FD06 %FF5200007D84A87EA87EA87EA87DFD07FFA8A85328000052FD04FF530005 %7EFD05FFA8280028A8FD06FFA9000528FD0BFFAF3D1A3D85FD06FF671B3D %60FD05FF7D000028002EFD0AFF280052FD07FF2E0028FD04FF050652FD06 %FF7E00057EFD13FFA8280028FD04FF530505A8FD06FF2E0505FD07FFA828 %0059FD0CFF61433DFD07FF8B433DAFFD05FFA80028050553FD0AFF000528 %FD06FFAF000528FFFFFFA8050053FD06FF7D05007DFD14FF280505FD04FF %53000684FD06FF5900057DFD06FF7D00057DFD0DFF85FD09FF85AFFD07FF %7D000528FD0BFF28002EFD06FF5905007DFD04FF05052EFD07FF050628FD %14FF2E0028FD04FF530500A8FD06FFA8280053FD06FF280505FD23FFA8FD %0CFF00060053A8FFFFFF7D050005A8FFFFFFA8280052FD07FF59000528A9 %FD04FFA87D7DFFFFFF5359A8FFFFFFA87D000028FD04FF59000053FFFFFF %A8FFFFFF28000053A8FFFFA828050053FD08FF603D61FD07FF853D60FD07 %FFA9673DAFFD10FF28052800282852282800057DFD05FF05052EFD08FF53 %00050552525227280053FFFFFF280028282E2828000500AFFD04FFA80500 %28282E0059FD04FF28050028282E05060052FD08FFAF433D43AFFD06FF43 %3D438BFD06FF8B1B4361FD10FF00062853FD0500287DFD05FFA8280053FD %08FFA8590500F8FD04002859FFFFFF522800050000000528A8FD06FF5305 %FD040028FD05FF2E05FD050059FD09FFAF3D3D3DFD07FF613D1BAFFD06FF %853D1B8BFD10FF280052FFFF7D7E7D84A8FD07FFA8FFA8FD0BFF7E7E597E %7EA8FD07FFA87D7E7D7EA8FD09FFA87E597E7EFD07FFA87E597E7EFD0CFF %AF85FD09FF8BAFFD08FF85AFFD11FF000527FD7DFF280052FD7DFF000528 %FD7DFF28002EFD7DFF000528FD7CFFFF %%EndData endstream endobj 21 0 obj <>stream +cÎŒý¢?5óUE‹F+KÔD—‚éÒLãÀ‹ùñZ…v<0 "/ °´À¬dæ*z¬ rˆé0EaÚ -ÿÛóeuµ ‰Où²Ý„^:&̧õ¥—*0_ÚF­‰¡ÝN©zšùh`æàstYc`x¥Åäv ‰84±Í€§¨’È ˜áÑ«Ðc¢Ç0¢…`½L‹Ê"+É +áË ˜ -NhXæ$f):„&¦}„>‹CÈJ&º-:„ˆ0–U&:c <½Î÷L$¯+ ½ (JãK˜¸hèQ€Å Yä)ÑX;ðB06 é +‹^o)=aü$ŽBZHÌG4Îf豜 <ŽE ck|K©$ç†DÊ>DËÊ8Ô"¼õ±¢ÄH Ë–’TT-V´>ÔrÝË{SØAº(1—r*Óˆ= þGÌœÉrNÇ #;8Ú,–θ¥£å 1ßÈ’+¯ ƒˆ›'&3€Â’w…jhBó?„^ØxbÍš¹ iwaÌC†²„é Q‚ÄÙ¸ ÕN˜ÿFCZã4EWE¤&Xz¸ÇzeÂ"•gBZ7U,-”P(´zæó’ö^Òï‹F@ɆdáiZk?’Û[  ‰Fy´k´öq,oϯ[W4æaV®˜¤ÂÂ’ˆ(^DãÑžBj0¡,A‚ Òa)Ê«Øçûh´l ©{¬àB¿Vj`óÐh°D/Wq ÝÐy[®Di´56ðb`N^ˆ@¼ÚÊô³ åæ¶BšP øç;>Z5¬ÊkèРߥé&ßø†í9é‚€X:,©?J4Ú0Ò!«*†Ù#<Ç7[>ÆÎD§·ibŽ•òšIYý Þ†¢lA´#b•Þc X“øQÇÖTfÅŽìa«Èôž$!@"–r( +/jÂ÷h˜`d8E¹La@\™POteг ²É¸²·ÄàÊ´Ú1Ö‰õ=è¦XþÀ祠õŠq…`‚0#Çq5²ºKȆ–—/$þIcÞËÓÁXý÷´°H³]a͹A g.L¬0óÂÚ• ‰bÓËN<•­0Y0rBíŠû¡]sFm`”a;‡¸6t}(D0 eÑCŸX*žC†3Iy˜°¦aZ­z7øi×p1¹Š3)ûôv€Bh´L]РáZ Ødó™cð¢íäaŸ€È!`²à3³ðÅXaÔgYGK˼…5‹DÞ”@#)Öš×_»qt"Ú¤QØ$Ú©È*¸oì»A/xeì €)Š-Ö0¶î³{ˆ$gOq%^Ÿp^3îx~È–…a2óÅcÒ–hEã˜åuð ç±ÄÍýJva"ëͯTWhz ò(6B<Äj ÇŽÉY@VÒß:ŽXfÊÑÈÁIÙè2숄ç * _Þ/Œê»!YÊ…|ž˜8Ò¬ï{¬ÌÀ§íù¢ÒEà.`_a`MãÄÀJ<Ï„Š•<1Ñ`1E´'Ûlµ‰„!Õ\D+‘&  ¢E`qêæ<ÛbŽàò¥‰ûš™<ÕÇCd×ÅŒ <ˆ¬:ÅI5’RÄeæ `½4嘘6ü [Ç0…ò,ó²rŠŸäø,ØJ~Bú „Æx•!‹YØÂ-j®ÀI QÛóБpxŠÈGü”>ÇšDO0l ÀîLR…!†AI½«„•³C%Ü­µÆïÜʽ*GïJñ½b 7^ÌyBY¼Nƒy+q@ˆbîü‡°bvr`ã)'…NÊ9æ…à]1¼W +2·Ôl#`gWH+Àî`03L³´W]‘@´Ó{i§€—­%Æ”ÆZk4,…k-Ãùå;h=v|ö¦³0'½W…,ÃS—³2Ƭ8†¯ŒØ‘ì-v~\CË&;Œ}6ˆu0O¬A;–ha`ñ†d“¼ …l„Ò‚\¼²©¡XŲÚ>ÈðxHV–ÑÉcoN8V`ý¥É‚»–¤­•]2ð xP¤b¿„ßÊù66Û@U怳'l,Oœ”LQ¸F”6zaÀÿUC\Tna]ÁîrtQ庑æ@+ ›<sá71T’k]à#<;¤âqÐ؆OKù +ó/f5”3 +¡úÙÖáü|Ìÿ—»t8 !èµ ûÂP×L&I‚ªa]—‰ÄK— %/´Ž8Dv¬‘mŒ[sÄ‘^ ¬ +‡Ëå'Ÿã:JVÑ„Öƒqò*À–+|…Ê2z1µKÂA-ú5€e5ÌÞnÃù¥Ó¾MiÓ¢C)ÏH 9QˆTá*„)”3e¬ªbq¶›†ˆQ±{2MÔà±Û•ÁCm*]d”äŽ*‰–æÌù Å•)¯^iëØ@‹sŒÆ2H®fGëuìq…Á/h"”òI8n®Ä¶gap¼AUíû¢¾0DÙWÅÿIÒ¤äJ$%ƒ)Þ8ñWÚ•®ØfÀlc“/R?6á¶ðkß:²i98À3Ìb“æóSn Ü©àsÆ +¨df±—…xôBq^KJ +1.'L`ö:K†³±Y$PõÙà+gK8ddœ·±V^ÑúP‹u/ïMqë¿ ´ÅbË’÷‚#Ë©mÎ4ç0ÄH’|¼š06;Š 7ï†Z¤å”SwJ 0ŒÕ±Sì,o]´…ŸTÚÏî$_”²[ŽÁzÅ7ǹ3¾ÇÚ‡¤DÌdá-â¯Äu ŒHR†fÉÎÁ+UI…E§5–µÀźæqz¶â—V“…C¿`E¬‹Â#Ñ‹(5¥”¦úP‹Ä§RjT1}ªÎY»*žèSi›­Wc$]KT=D¼‘1Ëápvw`Õ™Á—róß µHà/¥øÓêC圤rÚR‹ä¦ý¡Ò3Nd1 Œ!¥Õ‡ZäRµÈ·*feͶÌÝ*&³¼j™óRÊ‹)&ÏȪ‚ÊIC­R‹b«ÚLǽDC0$ %ë2°Á¦Èšm";I ÇeØå”ͺ̎ˆÝ®œü —¬¹¸˜á¼”'´ Ä.¤µúô§Ù£À¨ð±Ú=V2_ðP2@ðŽVð¶ðVEeLG‚ؤÑÈJ O¶Âê‰òp$ ±?Ž[!IRˆ;²Átëø>±~pó˜ƒ6¸Œ%L©µLU+¦³•ÒšD˜rŸÊéQ…*a<VW/r‡M³Z.áì¬l†jäïÒ À‘»Ä†¼Ûu¼l3ÒröÍø±06xì9Dþ»Èê:µ;áMOØ'ÁI$@P ”8=óøÒµ”C6Ë®’Ē‘°5M+.3p­ÏAhÄM€X¾ún"Y‚Žì ʲ“Žj %³Ï“@gÚ‘T‰! ð`OŸuÃcC6†ä&F<_Ƈ1‡ÂA²1îsÒ™ uK–4g!n…6ê0g[Ø/á…ù--mz1$Õ#þŒ´2XÈO•¿möhV9b$‰‡$ç"¶@1­Nd\>󮔚WHß“ñ»žI aà±Ûɾ(¿ÓV¼IÈÁ=ˆ5鈓dXf­ ËlÂÙ‘ÍJ ]—ÏñŠ+²â°Û±HSÄ¿ åùN*¡ƒ°¹"Ê”*x³-³0+QUcºØ¸Ô? Å©)æ +»É& –r ™oÂj +Ùq¥ü¹BŽ]sìŒM`°‰MI•ã­ÐݽÈ&=G’©ÇéS³,Üy4H¤ŠÙ六L„¤Ö@¶E»ñ8ËCrPc›\Cj·ƒþÄŠ§ «°Þ(4b$#˜a(iÎh5ºcëµÊ÷+eÂrÊÞ ³ÎJYiÅ|,6» ~s¢“œœ ­…â‘ -:Æ"…e=öãŸX7³aO$…›c°¡ó,ªK &C«¿†ö $;x>ûÄÓŤ`ÉJW,×CNoŽ#1J—7˜-¯±¤€GB< ëöf—³œ¶·)%¯lYJÇ,lawä©ÙìfŠ,7;æ“;l:EMïOÌñ|ÄêyÉZí^~KPDÆ,Ãötl +F5¸zRK¡˜SÈܱwXÊL,$/Êþ ÷ˆå€8H…’„es<ÙaA(«Ì šCq"44&­ÄæÐpÜC™@'xŽM„—Ö‰\Å:s‹$ÎRžg)´˜-Zo:ÿA‰¤AHðOÛä5ÎÞFµ:GlÌø¢GbÄ8+™ðÙÈ *™Wˆ¥‚œ ºï†Ê ¼¹üÞÖ©±«î”³cËÙ ´ÆÌ Xí…Ž ò‰%˜)+ËÖ<‰*v’Ðr:²±:+”YÄ: ÓôªÀïHâ°/Ò·LµÊ2.¤!³”‹yÌuš…vKñ |Ú5¤’±Ò¬âRo}¨UŽo18›%<[Ê#.®å»nPrzxÑÅç}œæ{·tâ} ¹PJ«Ý)î_q‹€ AIXTì+áÓDœü§pœM©R2s}¨˜î\̆ÎgKבÂ3ùFœÓ.‰€çù¾|€ÖÁÇ,˜”8LÉéGÄ,J9Æœ˜ÏB.f)ò˜?ØjŽ­\„Âÿ÷¬ÆÜ ûÐ3±Ð·7ð ôIksH À"6ï`¶¸q”·˜ Ô‰‘k؃ô"8r΄&àÒëΛÊ@Á€ÃNnšÅi#Û.³*…+.iú€Y±K#Ö6íÞcåˆOëb‹¢P„6˜5Ü;¡uò”ß •™óiÎ…4èÙÒ"u# L3+ŒuI#éHRdóz$WáˆÑI]ż¯RbX)y¬N¼Á¦B*6+Æå?qTFùœÏ-¹(8•Æ!c±…#,¨ËIYÈw*&DµÈ™Ò’÷Âæ+³.š3'<#žÄ™ÒBAÄ^;¤*sÈ­I"ZXEV±s®PÒÒ +tHÏ!ð«€”wÐ]Èîp?1ô)öÑD…íÔŸVÅ]U¹Mý‘RE~Ìoœô“ŠKÛ÷ØžŽãrx}¨œ+Þ"¼Ü‹» + À¥áRqù¦âP÷­£9°Ñ&l"c!7•’4Kiœ…ìGΆËgH–r(Kè t(b|‰$ +Ø€{Bö”qf/ÔR0ú [QÊïÃ=ùÀRŽ`«DÂVé†å”ÄBRX}¨EêX9½¬˜‚Æ™k6–fA ° (ÅòÙE|K1©œ¤TLœûF0Éظ2ÛžC6œ…mZ亰±\̈)gÍ”rk$Ÿ‰=c¨Æ‘HŒòl„Z\)¥Ä¾«œ=Qΰ(DÈ%1¢GokÏÇÙ“RŠ¶Œ)æ#ÖAµƒŽÈ!§È¢Âi¬”Ãp6|WÖ•zå°_)6XŠæ#Fò2^gm½´f¾Iø£±„Eâ„™«ÿÝ£|KÁƒZt±ÜxŒðO_ _`Þ7ÅÔ•°‹<²çRÙ/.ñq1SvÊØÔÈœã¦èÙ)ø~àÆ+ŸÁÍÒ-`­=$GC-½(OKÉÞ¨…q®”ë@Þ)k=ï7(ú +¶5Øvé\jáÜjÞæ„Ê[¥E«µh9A;ÊÛVÓ« á×Y庋ýh*.«8°ÑÉ¢~]*kàE =¯ž²‚;ëW< Øê´`ñrˆŽ¹uÈk 'ñë³Ö£xÛš‹‹+pìŠâÌZ¨9\h[GBÛ—çɤ™ÝCà€Un1\p_ ‰8±Y+‹xÁC{šþbÖxùh—¤oy,{yA°C¡Ïö¦¶'w‹ˆ +“ukˆ+ ï …•«×*îÛš¾¤‡&>ñÌBÒj˜‚ã í(>/&Yϣω¬ÝËy6‰+ t§È8 áj]8´ÚÜa=6bAzá._ãž’ì{X6 bÏçzxö!ÐE¹on¸A¤ÈxR°{NaFrü€”S"VaÏ#öx+Ø¡ZqZ>KÜ1&%' ¾²!ðÄ9žÄΠ¤È¸Â¬OÇ í‰49‡éVÕ|ËqQŽ +ÆràrÒ©ͤZÔÓôÙ1b/ò-'pOIÍUuavkˆŠ,OVöLwfY¢]”‚܈ÓæÄëÊR¡Û,Il(6*z×ô`Ö6–K­yâHÂ:Éö¥° +­Šœ¹1½*}~­Å;AÞ>»²áfsGç iCž:þSK¯Áb\å8°áÉ&ÄXå y—»&HOyß%1º(f|Qœþt –©Åþö¹þ‘ÏbË^¦% êq*q`27¸ŸÝckå7áõ0›•çâÌœ$”"eNÀTK¥¤»îÝX…ö´ x²06$Töææué[j-ßÍcÒŽ9pF<ÏÀ)Nd½_-sœŽrôï´U3 ‰ˆdou—¹7ÔZ¼µ&‡]ŒM9#‘(9ë’3SÖ‰SË\¦m&KÒP31ç`Dwa’»7½Î¾¢V~«ŠÏE3´EN½ ‰Òc¥(½ +uJ­æðá? òírfntWéæ*ßh‡!U¬§‰ÓÒü.ÝÄw•’¢ƒ* +¬)‘»Q®r¯•ßÈÃ@ˆPÜÑr@‘sFžË6¯5¯òm¿Öâ¢<&Vkܺ²skúì<ׇåÊÎ(ý“^Óì‘!Ý-Zô»¸}tôöøðd÷¼±WY;Û=yÙȵ¾Hm¾µî‚¡¼| $º ð¹èÙ~Fz3ŠÅaœœe¿HjŸ·³áâ­ÃåçK6”ÝíÍ/î'¯ù•­ùXΦXPé­^3ó‚ÖãêÖ º‘ìU^žíî6NÎ+¾Æ “`d•Ü¿g^2ÊG‘=E:÷p %¨¢c–$ü[èy¾}—\KZVÈÉ´¤ƒðHLÔGš«†w[®#CíÁqö¤ÍF=;âwØ›à”ïøe!¤vÁpV&Á'J*3õÌË H@1 +R‘i0ð¸þDÓšÉNK&ÖCúºGêŽ}ŒèƒÞ†ÒNq ƒ=ä1ƒWçß.§˜¢<âê!éyò¢/BNéI!’8 +/úiv˗͉ée ÎøÆÐ +¡“a·  ÏÌü ©RyªüØT©é¡8Õd_ãG!H’žqOÙh„Þ*ô;$H¡‘3J Ið‚¨{Én„¦DýùÑg,ä9Ü.};no/h3sýP•7>j¢ÜQèpaçÃy +¡¯¯âXqF5ŽÌ°K,á,5 ƒî§šYv Gûáä4¤fÒ»Â{*Ûz¡TBƉzA©b׈ˆÄtb:Ý.aiq$‚3Ò†Ì ºø&C\žŽ¢Ñ(hǽ‹¬gzÙ÷~À‰Šïú©PC^Ç:A‘©¦\á-’dÄ£fyƒâÿ!;¯€Ë)ý0 lec]‡™ì¯¹?†D´áÌÿ`iV<+\-¬ù#nb·GúWóer!g^à¡^ a~ø†áyÓðqøg6ücëQ\½Ù¡ÉLJ9[ ›ã”³º‘æ~.þù£×–†žûÙ2jíµx_i¸4Çd‹€Y˘íâæü±×üølrÙfRZÄÙòºÖ$¤È±j˜Eë\h¤p¬lˆ;C!R爧±"2áÈáìX¤E3@Ƭ’T¿’¯Þ+æd‚T›Ÿ¨ò`:Onvneå2Ké7´Îœ/îÕVoBß?õ+;6yº¯Óé_Ùù°¸²s=eúäôäÇ<,^åÖÌéé]x[ùë»ç糓ù4k_|{¸'c¯ì¸Æ¶Ù‹¾}}zv~Ï6EíÜU³~vZo¼ySÙhÔÏwO^=5^ÇžnéAtAŠJºj.Ò¨€¥"Éqßvú$ùø8„…CU¥käÜs˜ +qa/ð4Qš +’Ö—‡o>dv¨|Ípñ¢Ú\Ôâe¹·r~â¥Kf‡ø@(¹&'± ×Ôø<$1!Îy—®àášÂ3òÃmyIamJ×”W¦Õ%¥eÇÚb¨ùé–WäýøRûAc¼=[Ü#-I%!/ŠæùicP¯.@u'1âpRYEK{4Ì/¥è±ñ#\…ÈdAå÷8mÇ¿bš¹ +çCŠtþ2tê%û×ùl'­å~Ã0”jA’°C['IøM¬è,ãÒ½HÓ hb1iåÈxý·…A"¶y`ø°W +,8Ë|"ŒHf.éÅ~ˆƒ%ò®XÞaŽ3WpÄÏ|¤-C“ ²føüé +ŠfñoðÆ âzÀƒó3K¦#ªþ(Üñ§{YÈþ}“Y´¢ëà§äW¾ð+7hZׂ;Ñdô¾ÿð¯À¿œWjæ µš—\˸­šœŒp¼ ubt·ÕøF .7¦ìž4>à}”0M½(?Ь°ÒÑÚu>@õª_”¶´p²z¸$¬‡ÛcýQ$e\aJqK.©gÍüĆ²?æ«9"rÝ÷ü<\üýGÞ[³Îøâ%8ú“­Åó3¿[&a6Œ¹€»¤Æ¹Û> +å²OK?g($ó{«Éç.><;´òÈòëšùq¶¸%¹9ós3QƒÜÏ<¤æÙ~ÊÍ4û[«MÌýXxhœ †GóC{;ûcŸ‰8ö[Žl= E_6¥päÎÁQË/³g§¶Û ªeAÃM˜»±¨ I=Ê™žô*o¾xqzÒÚ•Œ˜KêO·öd£‚lå›JPðe3÷ãv\ +‡Ý¤«gÌÝ’Â|µÛsR(Î,J1eŸ¸*ÊQ(¥8F ‘úÍihöéqnHlÐU UñQóV‰Ò#ZNxú¶ó2’BͧาFõéé¹ñùô äÓÛšK²£> +*¢áÔK€ëSOäq˜EG‘ÔC®*ªà Ñª»‘ŽÄ¡ ÍÇà¸ë¨ÔÚð *u*Nå3ÑRãŽW-äȶœŠyÕ8]†^u\ú%ˆµ‡\‚>oÏår_tG¤5²5¢Øã6œ’(‹eÓ¡ô’´íü&¥ÙZèªë#‹W²oÀåã<®¨D®æ¸’AöÀ»ƒ«±ÔÊ·l4ÂÙd[A#”3Ê'åmÓ$ròµ¶ÅóP#¦&Çå¸Ö!sØÎ÷µkÎI)ÚxÝÃù¼ŽìC@­%¥_®Õ®UnmžŸž¼¬\Ý<Ø}ݘ9jœì­ìž×îÎ4öOÏ,1_iy%‘þÁí9{ɼ¤É®â ;6±æ X®QÅ…³HÝ£gûŠ=ó ¢€kÊAz0O€¤! +òG7‚Ò7h Œ²Ž|Õ“ÐÙZ‰Œ`®ô(=>°jÌPtMIÎId›ÂEÆ’]qy(ãY2âÇSãCò\j– ãè»-ù¾14èÄGON¸ù “Ñ Þ…‚ŸœåŽãô|F ŠqÀ\C/JšI¼–žG*1RØ&ЖŠ"cËGñ3(FÏ )NÁì +HE;ËÇóˆ"Bý1Ÿ9©”t lŒ‘9Ó—jJ8{+¦Q¬*°FŸ¯¥“a 6[jIÇ .—¤P°A¡!!Ø-ºØ +ÌdAFâFZŠÐ£@€ÂË1:× ‹¨"œh‘ÒmÜQ‘˜$JíÈItÆq0O!MT½c,æÓñ +õ0nŽ®ªŒæ¾å\R§Æ+Ø2>¸°‹Ç=~Xˆ… ƒ&@3tVä…åÊÔ¾t²•b—d0D*Ÿ¸ ‚Ë®àx^À•\IN ›å³J”lð!Ը—T„…hz"]|^°‡X0Ô«ØaÛò—è|`˜žå\Ck®Kán&Âm­bô¢’úÌ$±“”óVh¶í CWüX®Å-=vØ£`Q‘¹ƒ¥ZˆâMŒl Á<<ËÄR”SôT_qÝÝJ 퀋èi©#A¼ ’s 8ÇMP¹Þ³âª}¡ÔW®”³’ÆöZó|QÌØ lÆNÜhn#'e™8”è™°9ÂET²¶U†µ­†gpžÚcÔƈ³õ8ª¥6 +¼8´º‘Àffs!Àjth‚/,¡Ie'OÄ8¡©‡c€Ü±Ãç²°F+\ ‡1@ óÑ–íñJ†ÜîLla„‰±á#…¾ôå°:4Î%|b +¤òk„¡àü-7,õmS œ¹¢kÂËËkæ3°r‚ PqÀ´Ï5§'K l·t¯ñ¤©“4âP*”&f±=>‹e³¥´}dqâŸN2ʘ é¸RÕ›ËgÕÙê“Qpà`*MK‡Úò%åK3(Ú‹¼‹|ZWY¶ ÒH¸R‚­£ƒ*\×±€w@F¿P8æçÃ=ùÒ^=ñ#¦îØÂm^Ò¶`~–q€¶.>,U8eѹÈæN¿%=äY'8ÜÈ\ÝpÇ[FF­ìÁy®¬ÁæøH '•Œ#’ÂÙP© ,fC”ð@}h–É$ ƒ´u,Z¤àÀ±’Ƈ,Ô‰ÆP¸0aŒúS1£j< ® ÉxVJÕ-ŠªD\6’«œk©êD”F½¨t‘о`Áð_‘¶y­X1fŒJ|-@«@ùʪˆ¶à¸¶G²QÛ×碊¶… +ÇÂÍ ºba¡6ë‹ .’mˆ£fr‘Q±D’Ó‚Üñ-–só +-*Ò¥¦¾í%ÃÄl{¿q~©'5Òñ‚T¢À +w^5œÕ(xÙùÐè0×_ ¤ÈS…¶ûŽHò~Y"nÈ^PW$Š_Â%Í +XT五܃ƒ˜ pVª;ÚY®°¢Pí#a¬²YŠI,ËkëIÏy±¡ˆ%,¥t¹Fº?½Š + Ù +g>Ÿ ”SÅÜëŒÛ×>ò'~ÝHøŒ«‡ÅÕ,CŪ’â‚lÀ.°oÈ)˜Špihš¦¶ÅjùŽµž±î\å%ÔŒµ„5R)”ʵ‘ ­˜fÒ7YÊü£ê}ƒsE +™É‡RuØêr:¶´PTQŠH»Vp¡â:¨h'Â7>™Íu#æ~H–X¾ÛŸ8f‚㢹‚‚\ZÛµ³U¶è#WÔRF´yB)éUÆUm¤è;÷‚AÒÌ ‹–D±U+Y8ƒXI×èäì}¤|DŒ±4q_Ô_>8­¸æŒ7 "g éÄG¿ùñZ*"›‘гUP³Ø„Ƴ™™¸^Š«Fiaß\_<‚ÄŠbŸišôdé‘)Qß êøˆ!Èý¨ð’ȳ¡h;¾gÍc•BFDt`7TCØ,¶—ÛR=ƒk¢3R)h¼‰-\BTJ2 úñLÑ<L™%I¨e\*&Á¡Q,†HH¸›ë£Å]T¥ ´Óù¸k¼Õ[ÝN#¡Óx‰í"kpÞ;qòœE&£¹"š¬ØHP`AG®L†‚­‰Åß°jËðJ‰»Dº€æ¹ï‹~n‚ªMÄÅ’µ­¡‡QBqF¸8´ ‘p™>=/ö틤"å+˜Á©Äöƒl‘Ì´/}Ç,½cSøc®FFöl/NÛÓ’ø~(:«'‘ÚðèØH±ÆPqK“‰<Ú1.j˜w¢›8Á‘v.mÍÇ.P*^s³ŠH4<öµŒ·{XxU -Šƒ‰örÏ©`zð<×¹•Ëôù\ $®m‚zcb. Ä‚¥â¢O\@+Ò!«M8ð…Z記­¹Ø vb+‘ˆáÐj¯ô.6Þ´½éBÆDV”10&‚ÀÖ0‘B¬²Jgª€íŸ{> “—N°(D-Â’A'Ôh©‰U£¸ÀÝSÙ×rñã(ŽÓ‰hf(ÌRÅÍ•cÔ£Râ*¢o:-*.¸èš¸±¸‘ H|© g䘷-Ì=CG)i;UšLÀ§âX$q¨cÑ8ùõÌ3µ½Ï›èÁ5¸;"·‰ââÊÙoQÖ\ÒZ:b+”¯¶Z¬F%ºÀçÄi^E±ÃòxÇÈÖbĤâù„Œ‰è8”ô/ð§øA¬§þJÁFM ­ƒÄ{5XV„|¸ÅX…ŸëNÁÄÈOÏÑs—®ä'åí°Q>—Ê“^º"ÿ#©‚³Á$æ¥\?W¨æBÔ ã)™âªMød.ª¾GMaKÁq.îHqc‹X’X'Ár°Ä¶.ƒòµTÁeÝ]Û] +€@´ì¢<;ÿ«mOfœ6á’炽> §Èul­Â\WF_Z>W´íl+ˆ(õwÐû5ÒÖØl,Å8²[úË‘S¸Y« !o,wA;E…ÆúoØL…[S§ç +˜¡Óú1 Šgê‡%Š•^ ¶Ý.;,¶ø´ï%"žqª5Çé]BkÏVJ(<´žQ¦„&z<$ŽùPjyEb¥Ã©!¥bmé!qÍ‹GËÝÈŒ¨uÂÉùæq; -³›Y¤¿«º)Þya\µ^ÜóÎÑ,Õ»H¯‹bYúXº´²ƒÞîWûd½L˜;º²‹Þ77ÆaCšÇÅ.{áîÆi³¨Æ>ú˜»0*.3ËÌ<ö·ó±—>ŽšÍ›K¨¥a SYp18ÃtÄ®”Z--3BQ‚’´J%s͘µI®"(zŸã,Òع͚ZØà®…0·†“Þù@\³8(ÎyîôìÜõui„¢=Ö…"–¸ìš÷Yù°½;¥£¿Èw´¡iTÄQïÜòF8›†¦*^y¸›¥í€'éÙ˜±“³>y˜ý¶9j ó>Ø9?½Þ8³>õéŸÈ73• ݸ-ß¼‹m*{j¸òM9úù“¹wvx\YÙ={õ¦òäê7‡ç•dxóè°Þxr-,´G´Ú­ ây•„»éHw¯ÌŸ5ù“ÿU*|m^tbëåEɱǥ¤Lo”„èÊ¡yçcHˆO²¥D‰*!™ÐÝ_bí‡nÐWoÐÔßœŸñj?½Æëûàû¡aÖ:¸tÛ0+–ôõÈ~õ=|ë¬ù•KB¢zT*Âm„A“Hñ³]ï$%‰'…Óî:Dz”˜€¬VˆßCté‡FWUv|Òe—Çç^Ÿ_ùšŽÞ«ÄÂÛr!üØ$¾"ÒÌBÛù†ÌpQ&£UPÍ…ô‰ƒ"ÃßK~x€ÚaÉÝûó£â 2_›4Jó…è"–$ÒmAà + -g5<‡ÓŽ`Èû1G™Ñ†®ÀygZ¿ÞÞªoÇÇV†ŒÏ½>?(¾ ó5i>š^HÓî$¢}£È +=û°ô‹ôÉ" +X`¶MZJ£þ:¤Ü0°ôU:@÷þü¨|•ÿÚ o'QmXÄvÚE2#H!"iü„F YW›FͲ ±TMñ›$’•Jò_Ó\…Ýàx·òÙMUiÜ„`žfNyô7ᤑŠ¤ W™ˆB4pùáÑ…Ž€ÓÁ©&}dFå¾¥Có Çèù¤ø1ÁA{MDÀÙ˜¤ß„\…P“ªK”mp~)ï‡GÉhš´Ð$…t&û%ˆJ`ðŽkRS¸TÙQž!›‰H3Öìý'=ó8P ñ#–q¨tUR”Oß®¢Ü·t d‘‡ +òœTNÃÅi]"Rÿè’ˆõYmœ©ä ¢I"#Ñ)ò¨tIš#ÐI”›äcÀJÈ×ôYoãø¬ŠhÁÈÂ7éëéhVŒÔ\9÷‘Ö“>’:¥Þ»c +¾;=²d¾&¶N'É||aôMÂþúFÊ +\”àT€9PŽ¦D¾½U:S½ë†îNmÜö­¦ {£á¾ùë#Ï +-„(7 +w'›ÿ6OË™’} + í‚ËA OB¡Ê°È»ÉõÏ“SÂrÛgW–˜‚ªt +*ì=wŽÏóà€ÛD•€œá˜åêÈ\¢ë·Ñ·¼ˆ 'Â5%IºäMq“¥R) ´Â ³bÉI8¼4qB9£›¢ýqVbrt$#V±PvM™œRNV&’±—¿YlþŒØ—õ @ÊON‹ÄLÎê÷ (M?¯½ÔRU”hQ¥=R¥`YHmšÍÊç\ Ž•{¥7uÍ0JU8«¬q¶³Iõ¶Ì×0ꢮÙñ@»å}‹±©!RÊtäTMQ P@’奼Õç&xÊÈMî‡`S¾†œPX‚d”ÀˆØM%ÿ0Ž ç_ÊFiê*²ƒã¾Èâ¡Ìì Þ°"°ÉÇ +‚-ÕBÐõCàqjq†è‡©‘È )áSÄ¥ˆeD‡” NJÄda$Ç-uE¶xX½Ø§4ÕbŒI•«àkà§Aæ«1]Ôb:h·ƒH3œèBCÚp"w½–Ñ6­ã&(i.QhÉxE$3P©ÒÁn}@Òp¢³@è‰X5OVÍKM ‰c9Pº/IR¸.´‹ë¢‰uYbRi4›µl–‹&”Èà\4BÝŽ·MDªh$¯pÑDgkd£‰AUb‚¦Û?–NIND·»ä.Nè‚|Ä·IkÏ_'ÝdùíCtÂ"t3 ˆ ³ŸØDZÉÓ\@1Å™€bèŠü4†Ø ÞÉKgâ‰ö €.žhƒ¹ ²qB#‘ä¼Iþ2iÜ- 4"sì Ñ}ˆ 'BýäŠOqNtj*@i8ÑioAœ†k¤Ó DÈ4?Vû³ÑÄÀ4ÀyŽ [ ”"~®ÓOKRk<´Ë–F·ÍqM„éÈ‚ø~ ’F3@M ªAººÆÅëÄ +âÕ3 )±×yNëÃÑN©&wc 4šØSÕôRœŠ0( Lsûjé0C2¾ ¸Mä#ïÃA”F 5¯È‘ Çøsב{šõu3ÏÍÕAGvíl8ÑØ${¹pbÔ '¦ÀÄ… œIN4VmdÒMÒñŠO +ßj÷ÆzJ²q wÈƹdöº€;y (eÜœÜ-ÂÈkº˜8À/rT¸§ùÎ‚Ì TO$Ù.œ¶(šO ÃÊħ”,v’‰OÉñ@žµSB‘) Œ®jÿ%™ø”Ç~7º,ŸÍ€øVów ›ú´MÉÝìD¢Óš5w··#q;ŒRÆ퓲w:©ã´æ $õ„e`Ö_ˆGEŽÕ+÷ÆÈ·""nN@Yî|”E:b™ÚZªdïcÛÝÔÙ(g”wNTW ¡”4ñ36öÍ¢PåØ8|û–G9g0ú«úÂ+­Ç8 Iw:t*€Ë"·Y|l›A©/[ÛÐú¼µÕúâÔ/® s*/å=ÍmÏÞ‹#6^’{/GÑe MûH+w]£’3„<-£JAÙUèücܘ€Ô‡¦b;Ϭ¯M9.ÛŒQ©&ss† /W +²ëÆÇ?ó›ïÛÄÖjšÆ&KMÖT3Ö볫¼Ñ©Ÿ d‘]$ÈoêÍ ºé†ö:åRãDö1È¥uѶf@Î’t̼yγžÆ§å­~úV§Ä4'É~<…ÐNAªß¸©Ú;c§teE +É–KV—€©ö‘nˆÕOã¬+Õ‰ß4Lÿ¼±¤’懄?]˜* Ê`T +lbž0Ñ~"¶„Ç\ø'ïQŠÇ)UHJŽ¢käH*ó Ù›ƒ ©¤q*.ç§íÀjêÙm}“s™Û?ÄTufKí­2ÆÜæÃ/¡uM¹–*/09öŽ0JådV8,ÊŠ@JmeDJºæI&NÕe5)0u%*'Ÿšq*Å9¶9‘è*XdD§õóeÄk`w)'›SÒIr¹#|`.{³-õ•ÿRˆ¢ &D¬ÄæÔ >¬™W:"íÌæLöH^iª<Ò#“iã1Ò\9gŠ”›À ;£äL%œô­ŠKy(Ç’(Ò›4 +ûrU¾V!JSä*­r>ºÊhmºˆ?õ¼X‹T’KÝ-™¯ßEQÇ펋ȆòÃŒÒê”a?Ê(­H9˜zøÐ#@ÍphÄ5¡Ê°’Pöº4º*¼J ƒ‹¥ ÔRãzgQ.€‹6l…›}ö'ó+ÒÄ)»åHÔçP2Ù‘2’‰9‚o‚2Œ¡ Lˆ¤)ä¸ gùQŽùrÄ7Êð,[h$Êp6Irp Ô8ÖM˜›=›Â´eùÄ0šЇí`ð$eå¾²cù‰Ü˜‘ ìB()$+dÐ)­TÞ$jf‡†2Ö¬\“2‘¦òäµ½.•’~ÄHæD¬ä9`šãÀçW³7Çœ¸•{…$4ñP2‘3öa˜&¯H,ž—KqIA¹dè•1›Î€¤áJI¬à%oÆ+cv£å¶ÆR\vÿ¸ÂC +r~–u…½_Žå‰¤ž‘ær«Ð?€NmE#ovš¡±¹º~˜Q[qÔ8°×YEÓØš: ò¬Ï@)“Ù[ùÐsáºØâpóiâdá·úM†T´ÉŽN|]<‡ÐÎAü8n®Î“Ç1ìܪଳW\?¸!¼¤ ä”ÌŽ˜ªZüvz+¼~ad÷·é†K€ñ@WÓ$=§¸fA:ãEt@‡|ÆéΠ(ÞêÇTÆ蔟CyõˆLŽ0pÚF«UÁƒc2¤f EÝæÍMjI_‘Hõ¦,$OÛç¥N§D¸`˜j®PvšÀЮºD;3ŠÆÁNç##`'9ªrž©ÜZ9–¥*šÒöKy„;6r®¿dßB†¿Ó{U䤆bEjSZ˜ªô_lÊ”tÑ›‚' JTh™±ùq‡*‰ÊI茻Njä5ŧ±ùT!klÚYNB§Ô“•ÐÆžÉÝ,qŒ¼3NB77[²§2JÊÉ9zÛÙW\˜^—©å½sJgöó8ëLË ‚oK…d^­ ˆ…‚£–5ý:‰ã©_§ ÉøuR ‹Ödæ…²4™—rN«óθÁyœÁ&S°¼+ãœQ^Fò¥A”¹.50<¿ps Ÿ~þ) e‡bË–4‡ìÛ|ÆÌÄ2 \öq +t ÚéÚYÜN&e“½åhZfoÜA©ÜFÒb¶žŽ57aìÈ´°Ä¢-c¦3M¥RÈ^dR'Etˆµji‘?õøöºôxX“@çT½j‚L33© Œ]b±ør +­ØÈÅWI}Û¬‚,‘Æœí±ÿ'GáÍEÉÚ¦! +K–€Ê<çý8Gõ¾Ñ9ª÷¥®y–ê}+Ýy².ᤠJ% LJbf–ØnËm…o}¬™-s¦ÌÆúMUŠA)Ap©B1ê9ïaORª“#Y '›ç^,*Enx\*¹8 mù~s¶:TQaMš ÌÚ5)ÍZ½ ;»§ùŽïûß.;r;›N¡¹ÿ¶k¡$²‘C +¤~ È¥µ2„Ç?•TŽWJ ƒ‡ç¥l6òÜ,$žãÞ|Ê‚Ò5ÉSFîæ8óÎæ>dÆæö A†ŸÐNÉ¢SföY”ðÙ…¸s˜åe9é–úlPÉuItäAÃA +/CR7#BKv¾ç53åAäš M¬— p7dšåìØ)vÐI]4~ó€½ß<`¯š§ê3_ýnVèx ÝÍâѦœÅ£M)‹G›R6-²x´)fñhSÊâѦEOþ:Ï-ŸÅ£M‹,mJY<Ú”²x´i‘Å£M1‹G›bl¾¤™#Y<ºYÀeçhSÊâѦE6¥,mŠYáÅ£è^<Š>Å‹ÇWzñ(ú„¢OñâQô /EŸðâQ´)^<ŠôâQ´ ¢MñâQ´ /E›ðâáEÜdû«q/E›ðâQĨ̋GÑ&¼xm‹GѦxñ¸v6ŠHˆ[Ú„¢Mzñ(Ú„¢Mxñð"ñ +Pzñ¸öOÏt©h“^<Š6áÅ£hA/E›âÅ£h^<Š6áÅ£øÊS/E›ðâIÙt  ŸŽ¢MñâQ´ /E›ðâQ´)^<Š6áÅ£¨^<Š:Å‹ÇW^ü*Ú„o­Ü‹GÑ&¼xm‹GѦxñ(zЋGÑ'¼x}Š¢Oxñ(ú„¢OñâQô /EŸðâQô)^<Š>áÅ£è^<Š>Õ‹%)E›b'à…IOÇcì·øWímÂN€Úœ,™°(˜xEv„±ßN€;á·p­Äg'ðí¬»„  €PÔNàC®) +M&îí°\gÇéì.óôŸÄ¢ ;;¯³ À·ÂÜ‚vaWùNÚ (AM\² NØ x™k'0An!’¢Ê˜mS2VK¶‘°H #LéÙP!Ú.Ñ |þ£y‚l›ñq  <µ­êô’@¢]×p {qhd/{FTÌ¿ŸÙàwNó{ùu`î=·î tš3 ™Õ@OQFu Y¬¥k4Ðy–Un4€øÓ( +¨ôE,-¢€¬ˆ·D_D_=ê週»&*£óipRWYwÌh 3ñ²Øü†Ø+rB¡¦»-iäWn4`º,”xF®ÿaÂ"ÙäõdZä]ÜzE¶"¯ `¨»©Üh ³A²* ø͘¬ºFšF”¹W^:[€g3€½Rô@5œ]V]£Îk`Q®Ñ +iKn3p­:ªk3Ði®&,rm:óÇw|_訬º6Rd´7Ïf ³ÈPh™ü˜CœÛ þ¾\lîÕXÿÁ!(2(k…y¸&ƒ“gÕÕ¸ önàÆLn‘"˜ „Bn20Ü—žÜf`ПfÝ5`E8¸&ïÞßµÌs_V|WÖ¶I¡/˜lzZ›ePäš àÆ–ž'×f ³¨4ÛSS¢›ÎT"Ì‚ÁÏ$üíÚ töôŠ\›Îd((dFð² N ®1Ú²¼"ñ©KŒP³?×h §XlÅ3èlŸ×f ³ŒQP¤N”¸6¡Û  /]f2€Ù¼D5š¥&T\“ž¢•dê”* ö ºÔʵè,³&ËäÇÖ¤0Ì<9J|f4¶ dï`†ÁC/È‚¹ÀàGX¬‡©U±7f.0˜EÌ·NÈÁðƒ› Œ”…bäc›Pä™ ÜB•› f³ÇÌ— UJYKMc§›› ¾ªkƒg. Ac­PüO,Jð —dÓ8[Ȇ$ë©EMV]sÎ.¨®¹ ÈJ}æyÓÈŠÏ’eÛT}þ“¶*bd.q*{%"{$ÔÌqMÙ£«DÓ$œb\9›>÷¥c‰ÖýAé¨äêkl H.‹AéhaÙ”Ž½®ƒÒ¹Ç{,ï ŽÅ^¡—ŽåÜâÒ+Äx³£—{² Ž»WúÔY\è¡B£ÈÈxdº2Yæ +î~0éTŒOÇ(…?@73—wGPúÅ_$ ”[è¡W÷å߇Ȇ®«„7\Dv52ûΔ{SèóŸ”u¶‘žÿ¤wT\ÿIT+ü§[w‘Z0÷s%EðŸt‹DÿI¯ wÖ‚ÿ¤ÌاO•MÅö)¸ŒTýG +|~Å­fM%M RYp,Öì=Ö=öæ+4ƒó<(e™¥ãl€¥œ÷³ ³vŠ\Å»*xPzE¢¡[èzPʺägx:•îüŒQ§Ù¨E +wÀS·]'HÉp•káHÑÓãRGß± +!êŸÌx¹÷Œ‚Ó÷ÚBaÉ=©‚ª¢èAãÍû–f|”Š'&»# +»µpAaAœŒQ›£‡p1‚ E œaŒkOóJ„‹·Ð»q;ó.FÜA½ÛarüÖBanÓ‰TXl\ßiæ(¾ã̯P|ìUfᇄ‹Ù`4^¼¡Ù…)«˜ÍÌ·0¡H”¼B.H¸yÒ†ÌÅA(‘5.©¸Â }wî“p$Πħ±ÔÜwš…Bñj„ÞT¢|/X(e†º¾BÊèTÁ’ÍAL‹ð=žzÏ’ÍxŸ*˜²]s‘gÊöŠS¶WhñW¯4 +œO¨Õ0dU@øei­}‘›1‡žO”f¹_ýr¸ ÑjmsïPafÏ–Mãd +_ạxòHgç?ù܈Œ«åÆl¯H0f{…îá³t&¯{Öl‹jÁ˜mÙ\8g[¦°ëZacšöÖ-âgJ à¼ñG„ò®F”š°B  MfÍôÆe)HÅÙ1à_ËU+.–& ðÄ+@çºôˆFî°xo®çÞ€˜¦7Ö]ƒ·ý4ʘ¡LØ5;ôQM ÐU†Ý9Gs•dn‘uoFÀtÀ×@E%™]6 È$¹ ä¼Î<ðºcŠÛÀ1ÂÝ,O p·T¦æ`Õw/â®\ÄWdôîE„B‘’ù˜›{/bØ𴌈Òd†Šd¡}^WÂùS©ðò,Á&LÃû<Øו•øáÀ €ñÞ<°Ò°AðÃcôà>™6ƒ¸Ÿôí²[ÆÁ€ªR º¼)…™伶Øßþ1©ÕE +Ø|Nv,ÍÜÇŠ*bU•=ß ¢ãm ¥S÷f6-ÃòÍ[eO}a} qÛ‘ ©ÌmdÔ>”ÒÔÄ"¨n!õý¢½±Ý•]»´Í0ÃÛI•‹¾ÞŽƒýší8Ç –ẇS®L!â”Ê}Ŷ̷ÂC[•_ ‹Ø ’>“á……¡Ò€wT¼oñ^™§nPòžð¬Rš.+„rCì‹£3¨ ÛG]ð}¥òXÎ~„¢ï«!B¼¢zẉ€BKfG#y´hPD&È´NŠˆüõlš C˜üŒ—:òEX/³ƒ7~‚É7ÈOU9_¤·/™–É:÷ Ò$ÆR%1éÍ„ƒ¿]l¡é$0.¸ÄUE·Là^©Ëa Cʦ=>Ä왎¥º‡Õãm< “ÈUödÀÏBA#° U©Ï…¿9½Va47±#"‚"t„d=R/ +ˆÆë%È¢<@Uñ:C¢[#Y¸.ÖÚ Âds!K KDïRàòˆX‰2[žNv :Ý Ù'¨òFCãfQð•–dŠP(ö»{Ê®!èšÄ©2Ú>% üì +Zè}&aF2M÷WˆyŒ†ÿueˆÆeù¿QVC?‡´ÐñKÊ­k<_7KÄZ^ÀB,r]‰0_7û"¦ù&½ ÞeÚ¬Qµ!u%ÙJÍÍ.C{('ª!^B a¾9SðÍÀ?àø3†Ÿ9:Od;f2…™Ld–òHØu–vßcYºîÿ‹U¿ê¨Lpi?qêӟô\R·ºð—ÊûT…«G¡½š‡Tφ”Þ¬é6ÙŽÑ™ÍÔ±=_øJÝ0Ü©Æ}’ã®”cËsoQ-J¨N²•âß^u÷/^ ÿbl_{-41[éßuŠªUþ¯ Ã߯þþÛP¯D‰ø—Š‹>|´Ž°dq#$o#Èg¤p´©»ÿ?ú°=‡\æ Ð{/`«I*¶¬Ñ»q¨ "¯‹.P]D¤)ÉèN*øÐØ; £‹°À€‰>D@æß_¶)2Y³-–#$ « «ª:SÉC~ìr7ª‹;=¥1]@”)ÃCž¹sÿÛ Õÿ,ºü½të¿9ÿ)¶Û‘°/’»/~2ÆÉ`› nUÎLø°Ua5ÉÆP·Vs¢Pd°“ ½x¿ˆŒÁ×ecJuuŇÕkbkªb3,œ(™ò´&ò´1¦ÃåÙS%[“å. ™z;‘UµÐá®Pó¾é›¡q 6( +ñÈG /mÞDÖmÕ…G TdÐSš ÆNŒ¥ÓvHbXЬ7ßà*€e³oóJEö>­‰6uŒ)0ú>‚çsóÏ87ßA6þqJÿÐ)ý£2úlpÏD àD9A駌$½ y)xP§ì 9l²™G6¦ž%*ÙM¼ÉŽƒvÆŸ\ÈDÃiÄd*ˆ¸\8vSN?—K¦ò™g|L=ÒÖĦÏ8yrÜ”†SÎøTL¤2âÄɘ2DQgœÁ)‹ +6œvÚ§Âí»¨??NÖ?ódýýÚsü§ã?¨)ÎÜYø4ÉMg2ÓË…ès:;Zu¿:ø5´NŠdÛ„›#ÍÖ%I&ð‚ðr²,ce›Šd’Ó2M\Aú©¶«£AëÒ2ËîËêYu4r]»ÜnÕr½Únu_w­Æ¡ók<´Fªé¡ô~wä«,¡òkß¡Iÿ¹^¯5õ³Ö/NûÌ4ú¨ük§Ök»ÕZÛùàÝÊBmÞ/­²;n5œ¡[+8xñ—~o0ò†Ý ÿ_°C±xèæz!J*JÏÅnƒŽ‰¿µç™o—•Ä®ÈŒzƒYclj“-Î’ÿßü¼0v¡B¯ ˜ùÿ{ó+ùq@þx'Ex¿º”B rèn.@ÚfO!-nΨ}Y¢¯Ÿ,7Y†ÎB&Yz9Îã•0o•!Š=!e¶Æž +Ñ[IR¢R7Ñàhù…&™H[SíaÑĺ¨ ¤. H‰²ccy™–P—l‰ú¨@Àd°‘$!+^>„ò'32ñµŒµ “q§èÕ±¯f°<ù‚8DPf«Ï*¥²Ñx7ðÚ£Oe –È%.ð¦ÇÓtƒ`0s"“ÄËhÔ÷•$?S  ¨¥É”`"fªAS¦[áÙ¯W¤¤<ç p²›: Ô1xýo#É +ÁKz¼5mªØåLz ›‰W¿„ˆ˜*¸—jJ2¨7ŽBßXõ+…ÍÄxi:Öb² P0L´ÅžƒðØô•–(6"F²·0¡-Ä–8ì, _Gªcrjpƃ|xnA2ð:0Éñõ )Ñ1°•…1†dßx¶[™gñ:_f¯< h²îš4-Š†yD-×G@GCüÈð®6Mxú„íàý<ù2á–^§íïÁ3ƒl‹ª¨ì-1z˜»Ÿ@e@r ÞG£–iönÁݧ¦³¼\d˜ó,â[Ô9r*€™.ðõ<4²0ª; >×æfcÙs?tð– ƒããwÓ¢ïÐ4žtÌ„ F&öâŸÌP¦V„3á•*Ù˜",Æn*lÎP`hJÈkD°Ÿ§ü uÙÅ23ƒ'…?àe.|01I¶Möß1:Ë<­É| •¶…*e …¾¨ƒc92 ÎfØ4œdzÇ÷ÄЄH"!ÝJÑ1,[–I Ýb!ÓÈøp–užp„ü†€4ä·­¦ð¼¸ªä³cknp£-Sr@D*~£c%ØœFÛ …é€õ&m#Ù*(˜Šek +m„ή®g¦°¥ á¿P ó÷Dâ©é@+u Úd¯tR7h¶m`Qð‚´0©Ó©¾:éÓL™x +4ÀéŠ2]¦¾0؆>"Ñ%xô½RW.HØ(‰0Ópé¾ÐÈšB38ðF€¦ƒKšè@³á¸0 +.t`#Ýk +¡Ó¦ÆBù“-ÓTÑδ1É5ÅQÍTú¦àªÃ«“ú3Ø+/(ÁÜÐè›olDÙ ŒK‰Ów58ëùð™8˜åˆ†'²ip®Ht£÷K,÷›Îßâ’šmIçùÖU™F`Ú§²Ðú +¤Ó…Nu ‰-±1KSp<<Ÿó­"4£O‘óéd¦ð!D¾è‡¨ ,À|1àù‡{J% 5 +A] ô‹×à1™EêL „0PÁñ• šLð ;øW’"“§{€G¡:D–Èñ&uwÔᵊ© ǰЫ ŠT mÓ·~:x a¼wHPE(£¤‰k (¦º”(æÜp` Àä0W'¡¤ÏŸh?4Ú”¦ÒíÂüd˜B |=ÉIÕLúš™ô}&A¤&bðU;šžÎFJ¡é4«:4±id  À80)ðaÓ zð á“Î$ ¹€ìé¿Æ@è,–,éúí’ƒ‡hi"[ä}ºEi±Ž"6˜ºdL¶€GL,NB­¨4F`Óy† TDÌ ¿U "•>l#dS;@rÄ@ ÓYÊmƒFá$”‡è9*=Mp†à\ëm^ˆ)ÒØ6èðŒO¤ 0 +@ DB#‰²%*JÛP€L„ç ¯ ÏxŸ¿BaPúc*ìM)Ð!¾ž‰YËtšAÀ„´xLMšõÍ„œ$00”0MÊ8  Äã p!GA×-JG5ðÂÃ^pŸZÙĘ´t ‰Æ[!4tºä73Ò1Å‚èd)Œ¤4®2 +u˜ðusè×Àv:…˜0`.ˆ³&„ÙÄ,ÅOìVÅó`™þQðκJ“ÔE¤ÙBÑøiªx,á Úô9”`‚QBI4rþ6ÓœíÊÔhVz¸1X Òf;äýæ»ã¶!|Žb(fñ!G¸‚Ä2߀K¢‰!ÑàÙBm[ˆmXB~SbdJ5è+'Sòò*A~-Ò§Få?U£Â’¡Ù±WƒF¦4X > ðd åÐF¥a€K<Ób‚†J9C’hɘVj˜:kƒQ ö®Reù°uH$¤x«áD+R@W`L4Ô U€pïÕ‘”(` Í Ê”‚”yÍ€%1BcÎÆ€¡Áœ»“‡6ZÒ1Ì-2eŒ©)SÕx! ÕcÓÝ X™(‡$ ]v ªbÐ8|Ê`@0MŒGì†çñ ßÈà3q,ØD*Σa*,÷p2•²yæáž,‰!€¦pFAº ¹¢ŠÂ-úÇ3Q§ÙcUþ€º® Îé&RØR2œ Ñ1L9š°7€ÔNÈ„ø6&NÐY 0v.Ò)Ï› ˆU¬ƒJ2œ °Ì@"‚A£°€ÔŽÒ7 )„Y"“•á1šrýar€1‚ßDCDh4ä;DÐRâ_< cB؈+`Y)Ô|gOd‹± î¨kÁDAÛU¨˜e’Ù¨s2ÍÒb1¢ÆXAé‡wÏÁ1`Ü1Hã4 `;¸ŽP$SŽ´@h7õÕu—€)™= Tæø¥ƒ[³EÅ–ux¶ +J-=xüX†n…ã±.³šDÅS]âIÅeÒ,äßÈ¡&é"à6¼4 d–*UŸs«,´0`aÕ REøìÃ`nDiƒ[ÁEãѦ“FtÊ/€ÔD“w€"@Ž&9Ûž.À#Ùfo€J¡’­H<Õ—Lƒ(2{ʬBè62 n%&zÓ©2Œ{Iƒj`²-²@‹'tzÅMþbSµ!¿ Ûœ“:8aŒ ŒZ¦ f¤º*%¤…ÊÞ¹Xˆ’e` +p*6zÏ+H1-I‚mµY†YÕ¦ñõsl‘ „ht 5|€L@M¦L³ÕhÈ~5&Ù®¦l–?Ù„4z«h$Þ +¦š¢øøAF²¢a·–‚UŒC‘Q£B‚GÕU›e¾äG‹šëh¤‚Jl*Ú;èSA?¯’2dK–¨P"4³Y°~RIV1Î0ëæ¨ZÚÍ6 +ƒ‰HT|aQÏAA´¨P„hI•†îÒH¾ Lei‹1&9<[0Ú8 bÚ9äël(Ðû@R²Y(yú €ð¦tŸ:€ã@óËØ<#ÁcK¦l› 'c Kof¤Øãc›1•ÓOâ£x“Š2 âŠh¸l1 Ž2¦3Æwô¨(4²úâ ß·¨É‰*>4<‚#~X˜\ W‡›†)WA§`B²?>a7¬Øs)¹¢öHÙ ÑÖ©¼Ç‚ÁT ‡j -YŽëYIÙ¤'&©ðGI:Ïá-±Ô2ì–Du:›†¸$B,H$Tˆe1ç44iàé¢Q-öÊ-ÅNçñtaR°i:ˆ\&-1ɽa(ÑÒI:ÒéhºÊ„Ù¦舅&µ™ âGP ÁTS£!¯@—-ºÎë4=ÔAá’¦¡öM“Šl°sšŠv›=='˜@Ãa€åßÒ¨xFu"™…éƒmE\¥HÁv6Œ«P³MnasùÊlT™TàŠµjÓãFz§oé‰.fÓ$6ð< ¡‚:‹–ÑøF` +Ãá€Tƒþ„¦w4ßÀ‚–$´Zã,0t ”îêl +(hhYTi:>z¤š%È<¸²SºL-¡,ã„ ± dûhŒá˜6 €`rЕ(‡dòµ+N <m½4Ô¼Êts"ÒĨŠD‰°7\”Bøhg²÷„€RÀ‚¤‚.&Éžd b ƒBWǤŠ °1 á”2ÃTYÌP?LMaÀ´1 hõÂ; :í ŒfOŒ +LØTéæÙ,ñ«A!ÑÎe1Z?‚+À0*ôœ:KŽkð{‚tf;60¨»BÍnôšÅ›&Aiú pF–h-š6”H6æðàV ²`Y¥Œ´o:u_ÍÊx‚¨ÍßÆGbÔîG¹…σUvòØc/‰Ú+¡µõªàüàÁ%€DQìH4À.(?ÈKT§Sfr–KŽ,Ì=ÂíRôi,ä¹E‰Sµx¨«M{ÊZ´â%Ш¼*³Í¢6O¼ÅÁt[¢±PRb#–\ZÆ—{2R~4cIÈ€>s_BPË7.ªƒEƒùšðí†dO0}Ø`æ¶ nú€d¹pm†a:Óe{ +@¿’5*2ȃ T†K7804¾CÈ>`K¸y’E"NQ×Éæ€:ˆÈ×]äÌXÄœ”B2øÏAve™óxZO6xÂIx¾*³°8°M2[ÊrOËTEeR×sЧié*0à¨0h=ø€¶QÕfU¥Ù±¡‰Mïà µÌZônt¯«HŤÁþ1’†Dc‘‚„ 2ºL¼#éSøyVXf  +5•(<¬Ô;-Èæ4±‡ŒÏCüÉ4•\0 +%J>T±!l5ü–dÓw`F°íTÃ[KX("´Ÿƒ\&«Â½™ ++”‚’ƒŠ­ªÐËD"ñ ©p×bÒë7”k À¢°[HHªÂR´šp E44 ´.˜/Ü¡q¤6=™š{5z K/zqh‹ìv¯çà Â)Y Ô[+r2 Gµ©|¸Ó÷±`¾#èØ©³«N•ÆãЀ!ôJ/Ét¸*±ÃB4P¨aXE³&›‘¨•š’hH„£X/Nl–:‡Hå†Ä²_«ejH5Y 8" ™`>"´‚Oç ¶]wІÇâx©Âbó!©{ªFÏ8Ȃǘ`–R4›’U¸\6à†.!Œ†¡ÐWÁÀhP´É˜xå1)È+Æ0Ì`²1¹ÒÑ« ý2@{ {(ã…8ØíTÕšAm-)&#£Â*i`#?é½6\\¢E¢~p0à Úi´3l¡…)þüÞ³Ï@¼÷45zÝ®à•ˆFo1ÈTù-9ðg°ýÁͺj‚ýìF6\£Ûæd2Zˆï­ 7‡*žr¼S'GÑJÑGÕTœ°€ºjà;†f^¼8F€Ì×´¨\&ãk°™˜`BâQßÀ†¢Pu#ä‘ß "ë¡…Óµ<è+oaƒrq0ñ‚IÓ™LR* ‘¼ Ó‚>mZôTÂoðR€EÖI2K™1ÈÆ +ÀÑ$&¸SÛžÕ÷úWè½”`}…Ê>èš „0¶ ¶Æû0RòäÀÎA%_Ь64y¯Ï’¤ç@HÖØÕ …tL»€ŽpûfUD5–&-&B¸è#[¥!ÅU£e²Û`—AÙ.(¨ª«±p±¦5þ]‘ɲÁæŠ 8‹hxVA“„6¸¶ðFuGƒE£ž\÷ ‚ j¤„H8 nAdz© 7K*µ’¢£‰jÐ{#ƒEñUÁÐ¥b [:Mø©n +§Z¥31ÀPOöFÕ)Œà‡¨`ýCÚ Â"験€ˆfžU–h ¤™Ì]…TÙDÖ1O4Й9[¡Âf$'‹&-2iÐ+!bš·ðÀC5—^ïQæ×Òþ@lÕ³EÀg]á‹Àë3°Qê¸Jˆ¤¬!x,w£Á¬`R0)4ØÈLDŸN„é,ÁŠf#§²À_¬ƒhpGƒDw"s¡% ˜!",ø áUP#ŒA¬oЂUtÑAã&èŸpÅ ‹…ÐÙä¤h`Äl*;€Ü@ÆÑrTÐT•Þß¡Ñ ÇÔ)ê€8dÓµ*àÁcQ˧'žÈ&zù@ †ã†ß ðªŒÿË zIÝO¼t.èÜ`ÀÒ¨ÅLÅ´&þòá’/ªàb"y÷6œRw 2:®5W‹™ãâ(ô‹P$z3ˆ-@6‚ß@/4z×”‹ -ØPÀ?цGÍGZEnCÉÄðÆ…xoä7ÆÏGŠ¡AÒ<aQCócT[@=›"HŒ„ÛA Í“ +•¨AAáV7D+ÐÒ8•ÚX•5"P§¯ZÑ‘ ÃÉÊ£jýãwô«[ua½ncÜ}¥éÆB(}VŒ¦ôR zÎ×g¡à‡öé€5$=ŠÂ.`otáÔ{D jÀGZÁ1DãQJ’­hDã”dï5ƒ¨¡DÅ7Á«óîÙ‡¢ó"N¤x”xVîJà…Ê#ÊC zò½ÿ˜¡›ìô·„ +„]tßJð–jÝ4Èjœfh#´Š 3Ñu#„ ‰°üϱ"°ôh2О6›CgÇùÏhÏì·Ûc”ð{ƒTµO„È4=.¤‘Ã:æë4tD;Tl‚¼OjWZ£?²¹îRZv½ß¶¨Nãƒ"ätØoW¥?ã3ÐC òÌ ”SÖŸ³… ȵ¢Û}}ÿæâ>væ­ ìôW­T\‡·¯‡óÿK±páO‚…«?^âON÷´ÑÀý‡!<Î~ù„¾½o,äjicœã‰¨ÑrˆjÑ™&m£ðsð¼8—€ìÎá»ã8M˜)Žü×ì°,Źÿçïð¿,šò'‚å;ÑÉ/âw€…tñ§>†9àl;M ɦþ¿ þ‹.Ò7D—ÓñèÈ.oâÇÛä"WÑfdƒÏ¶ Oµ ÅV £ÝC\I 8Hªlš)éð‚nfˆHŒh2ý"±a[‚À«O•drXh@¢‹þ›„#N­~ˆF5ã4~ÈF?D@®påÞ¦ã‹42[ÀþOÇý¯%þ ¥FEþ!LOCý²ô–¥sÎOÙö^sô¯’¦ÿnsè—ñô?Þ~‡ž0w ‹ôßK3t ðÄl}2:üHŠmè– 7àk2‰¥ªTiüÛo¿†Ž«Ã?Kñâwsžš5ßÌ|VB± ñð{È\tqLU„«­? ¾.¿“ƒ£×Ž/è´[8%è0óÞõÿëÙ©®1ÍC£§âÏåªÿk ¶0èõCå·j£÷óûoã°ÿ¤óýGôH¼”êú0x‘†Š´^ªÚšNý§ákÜŠÑÞúpP‡„ÛÌ~µ5€ôðâ¼_­O§ß7[úï%ÍÞö ëícAt‰?9ƒ‘ã¡sV>ʵǃ¸®Z»Ûðµ{ë ~ƒÝåþêàJT^Pã}ÜA€qCÅ_úUBŸsN³7pBWÎ`ØWŒ€Iò{‘ö1ˆp?hú¿—¦ÿ*Dß0%j¨É†_±©$<î³ÿ"…¨2¨v‡„¨tæ³·ÚLî"tô5ÞâÂxX¯¶£V—zÈËœ bñÞs© +±Ðã´ý2èª#ç¹à¼œi®üšvÇ|¯ß¢ÕÝ¡Éœ½Në7Çק÷øl4œ6*¨md¥8ª$Æ=su²‹½]ñÞ46~«{Ök¹5Ùlœf›ûÛi…7ÓÜ H[ŒøN0÷ÅY:æ„øÿ“‰ò5“Éßë¦fjäóƒ9~ƒ9ÊqLRïÕ$=‰·ðbñ{üA °Çñ?ÈÿaÜQýKŒ?X#ð5Hcɦ­k– ñvåYœR§Œ•%!t´©PEÒ¶þª›µ„ñaüËi X…5ýyùû=Ü,&yCA¼e|‘˜’mKýAP~”5Aùá2ûŠæ“PTH9 +E…¨IÿkôñUþRüù'ì¾Ä¶Ÿ›þ'7ªÿÁ…øå5f©×nU_çÝòËÌóeæ÷?¤.‰e¼5Ä^9ŠÑ"¤Ð=Ú®âÓKBz%Hö‚¼–Q7Cý¿ð|ÿ…• Š¬Ø’¦á³1ÉV ]1Mò­aà"Û&’­šeCL; dêóÅN~PÃ?“â³Å *V†n~PÄñ_è²÷ß®·Êÿ,½UýªÞ*ÿ7é­ß‘HŸÒYmôƃBë¿œDÿ“-ò{§ +½‚=áïoÿJÃÕ¿õÝîWwòeÔ‚Õ[‚פ)Ñ!וbb E7TòºX’fÁ¬&"œ·9ú‡³Ðà[É‚H°Ø?¬*‹ÀК¡|oÐuÃêÿ¥\§Ja¾)üY7óNçýü`îýü¿žÝ]|ßw³‚àMAø?!ÌË¿T)úaËý ©Þ=)­ŽÛ£GØ•[~Û%v36àßtr(œ += ±c‹Ý†9ö›ágψ¶E$6„ÇY†…R€G[< ß?¦mXŠ®+–¬Ñ܈†))ŠlCj[CÓ „¬ÄÐ$ÅÖmÂÐ ¾ŒØ½Ðÿ1Žð¾ùm!X¾pV_€Œò +Œh*¦¥Ûè]¹º$Ó†´†šdÒЪ,Ê”NvD6m”!hLUÉ€|Öå%»ØÒÐîdJÌm‘©³‘?&¿Ð±ÿÔU_lId‰ ‰{…tLC0YÆÄÆËÌ"’‘ ‰]-Ë`!8S*š ­˜¹e¤($K!³±ºbË"ýC>߉¾àè>ô'uÈ ùw¢yð´§VXÒ8Û¶­ê¶¬b 0ÄLªd· Hã )„,"4’_äeG2oHh'É&J…ÞzÑÉ¢MìG!à$ç˲dI‚¤Åd2ÌAµÈLEœœ¯fv–s‘žãöï ¶|DÐR¤!þ÷šSeª ƒ©/¤{#ç»c +hïç'ñG"GËýD“T@7ÿŒ5èÕáL°>ªv_ }]ˆ]9`G +íZø RW§Õ­Ž)?JPX!CÇÕÁÇ0ôû¹5z ÙILæñ@´jµ^—ü±ë jΠ¦È £Ï^|¡Wwœî¨PU!% ÿÙH|~ßôÎÔ¡Ø/v—|N%tЪG<ÛG:;Tÿž.þ„þ…Zõ·V»1pº´ÁÀ¿Â?#7_Il¹;|þ©:n±Åª?UÛc^ʇ3ê&O«±™ }¿þ¥ÐéöºÎÓîÕ?œÆW Ãk®}ßuÕZÝ™¨ü…µä(;£\Ä·×'Öþ“¶ÿ?„„<_Z }ñ¿¹«ÿ¦ƒ¾þÓ—:TýÎ Ë«‡£^çûR²¿ׇUPH€ç‘#öUtüËÏEáþc¦òßpJ‡ÍŸÿÁÜø;ƒ!“ÿò]¶ Tí5ÿcq#ã¬%¿9­×·¯2·æ÷¥Åªd¦ˆ>G44HŸ-+ßZßÏ­ûæòXÅï»:²y’,˶¤ýÕPío­î—/Ñェ$Ù­”©šD{Öl¢ZßZÖ¯_YÖ¯ß{Yî…Õ¬eÔz#"29ÍÑé E4˯¬j²Í?@<@ºXîu'æ†ï..ö½§ÐqFÕ—þÓyØÿá<" fŸø +v •1á©¢)–›wÔU£Z¯:h„ê½voRBàöÓýbÝW= ]WõÙ5åßQSÿ›uk.LdCµT·*·ßä«ÝŸªÃ2sAŽov_–Cggè ~rBç—Q¨ØhªµV»5òr¸R’•k;NŽç•·üÅN~ÐëgN•ÞdNJ>âäN`˜e‹aV>OÅ5M[Y²eÀ·f’nØŒç¡W HìbÜv"¹àbyŽYL1‚-| ˆÏœ¾ì¶êC8Öx£ºN^gÙ ¶û¤ónÆCICçqá´+½ :œÓYoØ‚ÅâW…O+©J²¸õË]ÈÃÄ‚àõ¯÷ÓµÚ¹}ÜU»¯ãê«:ëõÇ}¶5ýà/B Üéuð7•?ø0~+À¤a¿ÇN–êò¸~µÑôVm·<‘9»ÊŽG½ÐEu8r"vÎ@õ@g¸Z~¬C­a¯]9ä08h„™[ù£Û«ôÆ#rÌz(BU;Ô¯öAhØêŒÛx1âçŠP=àúUBÍê¿’nZ R›/á›Ã;±±ûc§ÑwBYÀ˜]ÎtÚÆBjÒ‘Ó%ÓÄ< + XìEÕƒáá› e\Ž0š°¹^‹Óñ¨O 3¿æp·Ý«UÛNܺ_ýtã$h?á¾Vz}á‡òGvÿ¬: ‹"s^¿µêogƒ^³ÕvÜ´Õ.úùª‚߈7yZ‡ÅNÍi|­Ã³B ú$P £¦[&?8g¤VßAâyµŸKí^o°ë‘pU1 s^Ýœ»úoV½à<ÄÛä©õNûÕº;YÅPäY“š.ÌU·lKžSÕ›*0¦9¿0Q”öÄy~cI,AÅñYUóN»'Â'H3û„ŠWTfOs¯GA¯»'èÊŒ…OO•n„šîi­vG­!„Õá´£C)X8 úµOê-;í½êˆP2g°_ +\aFÍ£^½ÚZ!Ö+TÀè£2 +%k))% B9¥q»Í鋽I¾N.\Nèm»ÕuB#"gL +"ÓjÑ …ÁI*£}0›óqÎdèîù8ÏÜI¤CÅ_F® (ÍØGB€ÝIÊA±#X¹ ŽÒ´¶%ÛrÈÒ´o·U]Ï8{ÚW-çg²X"ÕŒªÝºÃ мcUªÖ,^1~£2‚$P{öQÀÚ¾#+Š§Sk»t`¬±Ö×( Võ«¹´EÜiO®•ç2‘WòÀŒ{ Wà$<·ï;GeànkøF¶DØ¿/o=tpÖ®vz؃]€—Å—» ¬Sh? Ânuô B&øçÌo…4÷÷7„òýnÃù¥Ô ¿1¿`“²SïuÖ=€€U嶓/À躸ó–a~mg™”òl-öðî­×Ç×6׫?u›Ð¥é+ðóú ¨/:ç´w@VþÉA´™ÅYä¾Ï5ÐÞOΠž Ãù êíVŸÈÒpoó Ñ©_ e-LOQöµ $›ü‰úZiÕ#£ÂƒŸÔW׈°Qê‘Ù\©žÉf¨<"*Q?Nù8_M ŽynôÛûægŽiÏ–îS䬌ðF}N¶\GvBÜÔ¾UÎÀÎíŒÀ@Û%˨͊þZî戵~é§|Ûá©™ZƒA@ÿ@-gZÍ×`ÍõÜ |¹O™SJ#ók¶[ý¹C‚ð‡²Ÿ¨¡N«Øë×{ߨ0œ³ãX¡1ö˪ +C†¹?âöú¿G ôäYÚ¦ÍeÐ SN¤ÊÆ7j5ÇÝúŒ¢uØ¡æX›?=lSív¹}Âc'jÑ‹îÙ\ï¤:®¸»L•S¡k§Ê÷ÐYë!V¾>={ˆ‡~RæψtÓ§zçìí!u qZAáe¢J9ë_?fƒV¨Ø£qôfÕ|ïÕRµÖ¨SíO9BSNøkç#Uµ¥×l¦¨Ÿ …3«wÀ‹-X}Úòü‡Ñ7PEâû.zÏw¯ ΀d%>3ßÄxÃQ;Õ ="çpú~A3Vß;_iÓo2µ»_žT¿ñåÎ)&»-¦’ñþà›uØèl[½kÇ`=áÑ´gÖ¯RekV­V§úê¤ÚNó‹5Gœzé¶6¿æÀÞÔgÎ’Ö4Fph¤Pí×Pa@„œÁüM€^|Tqr zj³+‰@›ÓUO°ŽÌé,Pm’Z@-´nΧ gØzíN±Ž+â騱۳y=bE0έWRBõ¥3­æ3©z{ ÷Y5¸$@´Ç9kš®([kWëŒ6›ÊÔÚƒFª7€PÕù Tlƒ§NA ìÔZ}v0pÔ×Ô\äcu~š”ƒµð¥8ïë›õ~š¿Æa½ß®ÿ:›*Ñ:õnP© ÖMH²f¬ìS»Úÿ6X½9sG†õM9k ùÛšob.V§tÚ;_iCP~ñØL »÷,–LH<˜Ûü´h?þFú 7GÆ*}B_[Ýæ « „§)ߌ9µê hT-Qxìå •GÂ’¾UWà1_ƒ¼i|¡²;/Ô¦1í5»£T£=ŸäÑ:ýA³×Gï ÚhË\¹œ¡zÚ©²ÆHž øÎÕ<»ÎkÕ»bž©.€6Ü«nBCY^9ä¹E›µîÝ´µ~qÚgÎ2Nù§~TŽøMì~Á?¯^ÃÊâc·ç™?B­.š7€ŒÏ¿'.•PvÞ¥à°-ÛñÙµ%MWž'“µÑ×*3wØcÝíö|? ¨2:0¹Áüþ†­>á°Ý/FhÁïbÇdy“á4l:èÕö %›K¸)§ì:£2õâ«äÁ¢•g­ Ñ¢5Q•÷V/Gü—Mp³·LYŸ¥A´aOôz7Í åD+Ì·ÑMÏÊs/¡£Œ{þ)CgSæìwUJ³!%MÔ¼Zg&Ÿ +{9Øep¥¾ëx +Œ]õrØÑûùy7ò´Ä/ +¢ƒÐ['ØÈ?ÍúöX€ž´ºíሔ±g¥Cîw?BðJM8Fé\‹÷-ç÷÷-½àÀÁ†ÚÎéò}bózke£z³v .Ÿ&s™Ánçmýµ>(…×b+ùV55\2.÷ŠFd=s¹»}¬í¬=¬gãºY**ÇVTÖ´ˆ$ ï…×5i)³ñ”ZÍl®õ‡™á¡’^ˆf6ŽÂ^é`”{Ý;?ÊljN9ßÚÚ®R©•×‰¡Ž·d<³PŠ®›w»£ÂûcN»K®e;½£av¿š×f®i߬7so£ü›y'ûÀñ²\¨ËGŸ™Í•kÚ™ò0ÿôúÔ#-öûá\Òz_Ê–“‘.ÃMµ1^ˆÚï±D½X×Ïcù7íyc3U—¹“µ—D&¿rYÊ;ãÕí«ƒÈÛF½^ý€¿Z‰bóèŽ,Kéª9h-½¬·ž¹vtg%9H<Œ³GååO˜<³qð¦.D«ÇL¶[_é$¶Ž7Òfça«ešéaSÍêûrâc]v{¬†WlæŠc^«Rc½•OWÉþÊÇ[±äš“k›gº‚Û£h&¿¿¹.®ÙúìËþ½Ù6ó½§ÄæUã~]©E±Ûín”,hÛXÀ–Ü×Æyà´ûˆI†šW#I~ŒÒÕÍåR8q7€Q øð„½`•…¨T[Ü×ðïÄvi“ýµy]<¤ÕókÅÚ™r«ìÔ½‘ÛÛÅ5¥°óºÅú¹ÞÚÜh¼Ÿ<áNº&ýæt6 +©”;p'ðèM@Žm]@%GÃ2=œ+<#¨ ÎpG3îŒ÷z¶RxOšéÃÏbµº²”3j—ç›{…çílå­>Êž-Õ³E%»Ÿ5o#¤Mã®xó²3vAD±Ö‡¦O^gV;5ØãvÛ+4ËÅ“t['¶#×t‡ ç…hñYN\å´›ƒRf0x»ÔÖ¯wp‡,½50Èæ­®%r=û)JÿÂE¸s8Ñ…®¢‰­q¬ThË9é@_'ÿìJ-Ú϶Qëe6*£Ålå`4že`'¸ó¿„¡¬BèØgª• Âi|a;¥Ør?ž3.îŠ5isµà k’s¶µáN„‚ÃÆÑnöù@FlÛL¾”द +ûïf•ž}º¡Vù³s˜=}Ê—òÍK’kR¾Ñ¹Eâ9evsmcëÚëÛmÝåö*‘­À¢dÎia÷#ê¡Î7¨Rsýº79Û`½:ùkm´Û_¬ÙvbC; @dcŸ(4ùÖ°eµLÞŸ)ñÈþž·ª g}¹CNòEÐë8±upwÀ}} tL%_£G«»£çF'[~9(¤[[±í ¹²gd+ǽ×ÌUe¿Z*Zç7 ÑÍ„T|pÁÑ/®õN”Ò²±~Cö|lëŸË|< í*%ÌË÷+G„Í}VdËX].4³+O>pQ*=ÇÃo¸ Bs»9Rz-ð•í>|ÿðq¡õ*a¢»½âZ¥¾E x~MöÍRb{ËPñk¶¼–éæ[­ó,- U*…Ãýdi³¨Çñ+¬åuµK«WŽ3Ñ\û@JÁ^Ý{çÓ´vZ«ZæÊ‘Lý®{)Æ:EòO#'Ê,ýëàvË&Ýl“²jF,“_s„ZzmXÍ*kx{Íó†ï䯸}†mxk>–Ñbý©·sAþz/Ð*PF‘ûEqzPIìGé‚]“w[|4 +l6tJ09Ò Ž‚SÁebð3ž+Ó2Ö® +GÖA±)Oz´‰°Q°ÇØýŽÐú`tºI*]îî³"(iå§ ‡âæ¹ +Ï»•ÞËSÖ]‹o£ælíï؈À6àZÜP»Xd¢¬úý6‡“´ž÷“â bŒì›®ÅÝKÿZ¾²%¸ú_û*þ$£ÌBŒo¡Å]øH\•‡*“8 £Ì…7B‚ƒCX3Tž +TÄ aÍýÇä)ˤmØxnþÙˆmq¹÷Û¾A¡?2(î‹ :ŠW)ì G:G‰iG…ÁšÁ‘”®w¿ÿP _ä7ÛˆÉdq¬»ñ^z _ó §ñT +àØqã”ÿ… ‚eЕ’3}ØëŸþ®2¾gg»Ù‹æë!+û†‘­\vKYþXˆîFwŸ‰X¢Ç A6ŸG7KÏ£ÊjfC: 'ŠW7{œ•%—qB +‚Z’Xo!ŠŠ_ Nª™l…å0g¬Ñ/~?S›27îŽJYõcéõÿ‚¨ ,D­Þd“kM­p¸ó©‰£äžìE·|™-ö;ÅÄQ:æÿÚ.eìhŸ+qOù¢ìßW>³§ùÜÈ0LºêøC4PÂü+„ s£~y‘5. +ƒÕ—idõ½Çýìáõ9‘“ÙÌ.£»ÃSgÀD¿##GØöCÊÕRóu‡ùšH\w€ÎLóòÑ9,4χåtëìž dñ3OåmUY¼Ÿ¯ ÍT…¢¢2äa(,jØ7ÙÓRáýú9ßz7ÒÍNô•È·«2Éö‹i^4z0¹¤'11¶Ü~ôº"˜Ì:ûoÒ,ŒU×°]xMÆwèçêö ÔNWòo‰Û|ºµ½µÎöÊ'Ÿ.òAÐ>UòaBaÄ=8ÛCð“AÕŽ(Î3ä&2K©XÞ¦x½6Ñ,<ùß/ý µ?œÃȾäßÂë‰íÝçgŽY)²ƒ©ÃÜÇq‚ˆögMïÌ®ƒ¥å)÷¡l…½®!@ZotÚARù }çY;ˆÆ7eì?{dàbll~tû©óÄÎUõ“P13‡·‹D}| ûû>Í–wïd_¶Jæ2lãàlÅÆŠ(×Ä„Ží¼ ǸÉö9!ªd”§*é¢2,>÷åZfãr%%ɽ½¸ÑŸ½g+—Õ[2Àz[*=dT2›½¤KƒŒ…¨Ý.õš³0FvkVøé;ûâéUE‹âüó+)Fv¹PÓœò7O¯08½¯“Kñ³öF© ß)‰íÛÇÕÉ¿ å Ý §7wôü†ð$û²mŒ ©‹F2³~òþᲨŠ±Xêæ™i _²ÿÙkÒ7Ï'ýæ<½<‡lr»…-FA¯·t`óJ®sºM¸˜Oð8Ñr§cõŽ€úbXL¬Mcyx¨d,E>­Ìúõgk£¹W{ͬߔĮ(Ŧº»][ˆ"ÇÞ\»5ö°ow³šRŒèé£:Ù¿»‚Ó?=Bqˆi(S +‡•ãEoxóª}T!Œng©ø,u ÍJ¡¹-®õŽÇY=“½/ÖÏÚñ€¸Ä,“¯ÑBPíEs˜>¹ÊVŽ2p íäää7c™AÒgÏ.Òë­'«Ivüz•ì‹ ÞPv´•&s´J”‡ËÒóÆ[:8¨²ýÙ!›üÎ|îÔïìÕ8kÕú¯)yïj•‘-&òXeé3YŠõìxà“\;ÚW¼.\þ +S>IæÚ©IðåãÎØ^?½Ílômyri¬ì ©iZç½Õ™•°Êz£Qûœ¬r—,HöÝJ)ïÙJ¡*çÚVÄ’”µÏVfýô¼&P˜ ,Âû‰­úÍ=94…ãìE%ó9‰‘a¾µu<̬—¢UcǸÙÍŸä¯v\éÐ`üÅÜlÕï ûõ‹³œv­Œ&6þ)&ÌC:/ÅvŽì½‡Òˆ ÁlSÑÃ¥l³ðyÔÌõ~*OÄ¡Öª×J}ÐYÓéÒÝoÆ—¢Ï€Ý›ÅZú®ï—¼é̬ÊòR¹ôY.“õ%ŠOï£:Ú 'á¹;*Ör«±…èúqB½ÏV*Ëa²ìì»Ý1+C‹#J„ùχ²)å³DDÌìß:VX«è®ÍÎÖþÈ+YÓ¾xµµ—\‡,2'ͬŒ  +ªR<Ù]«¸âÙ–J´¤ÌævÿšðÊvWZžÒ…“ûXä´ÛÅ˽­måK+ebž¯—XêFùØ?!êG±'ž;#Ónx¶N²\]áÎöºX—­ðI)~{6ÊÞÊ aЭÓF¡Ñ±×Ä; 2³¡ò§¯ý¥D¸÷ò¦ 'Õ½½ 7qÒ{/4íƒv¡¶ÚŠÚG/ƒÍR¶Ý]Dsh_ÈîÃUÞúI>ýP8<ÜLSQM9/'KE¹+­g/ù†^Ç寤­Rv;gDûza¿ÚŽNПöYœÀæñhiªp¸¿˜'ÔÒ€6×Æg·¥Œ4z&{Þ­•Â«k2Qa/¡­“£¾c¸c ݪ«±8U.ÍŠùÄï®2QÿíÂF)¶ßÉÚo'‘Ráì¸\Šö5B\®×Œš»Ó¼³ÈÄ ò—'`á^Ö,GòYòÿ½Ìf2Ÿ: +TZÞcñ©TT'ÏXä#«År—ÅØ°ö„DK®Ýმ¼9Jù(§Ä›•©]È+ P™³½·ÓUQsšwRí¹Æ7ñ]ãû=ÞÀ^)…jy÷a7ºûh|ù •GG¥G²û™%EðŽäd#s\H¾m +X»m¬õJ«ñ5ƒˆAZ”¨™6^6n8výŠ]Á—âJEÄŽRÚX[ôø Ùv¿Ñ,Åwmq“WÏ"ð¡-×úMª-*Æâk®³h½ ’Rf¯ÿÉO¥Ç_p”ku³jnœŸžeËãKe’¡èƒÂ~§5$l+×!3l¥—ã¨=[8]ß¼ ;nE¦o7—),»—®k½½ä7êU–¯‹Õ—ýÅ\'±,0hŽc‚ô±srCvº%ÜÛ:ÎÝÄJ¡4ŸÉ&î­~‘{ÿ„sd$Ÿïƒ•›Òò¸ïä´qŽh:DÓtr¬Ò‰Y„‚¦ŠíÜûûY¦°ß8b±\ƒîÓ{£l»ÚAѨ8ZÞó‘ÆDŸ^¦I°LŒD€wkÙóÕgÂ!ëY$Çã‘´¿»8l-D@©WÖOžjãÒË“ÒSÝËô;•¶œœ@þáœS8D¥{Ânb…ær?bŒG[¥çv*í¥õ´öJvc¸j®?%Êp”î‹ïªD ЪYme¥gËׇ鬵;Ûèö«7™aüf±pxðøYp6Ÿ½ C™ÿE#,ºÙ'äã!m¬×N—¿ÚÚÚD¹ÚÛ+=“¥ÌŠSñ_ÆxP(…áŠÁaÍ™ä›EÆ[Ûݽ³*ÅZ%{]xÕ—[êå.N³\.MéÛ’"îÅB¯ óo÷›Ÿ¥ìY¦¿~<÷ü¦AÃø”í‹B=_ý(¬¶ÒusS-:Å»æ~Ñ5Ob•29ñåð¦jÖJ¦Á²‹d !vvgV^öÒ¥Â8‹×’çDâXIJÝì1៕xîcc3É”@¯„À•Ç•†ç¸A©!ëô|1§ÇKiBÇë…WZ²ï•ƒ2ƒÝeö¢wôblÆ?D•`œ[Ù<*®=戾o(Ï8‹À"¼÷¡E ¶ý¹zP¨Ý8ä´mïíÞ>÷_FºÇgäÝÍavÂ-BÀ“¥ˆ18ßÛ!¢Ï!чŸ9œ·Ž*ç±àÓ¦qyÖH““z¹VŠÊ1¢7Ÿ_¬ÂMI\­Ü—«ÏfYB)zd®úv++-ß½v ÍÞcMGSí׌Ôç,°WvÇ©þSöð.ny».+ÅúxñÉ:ÝS+¶ºv°Qz^»Z¬Åí§Rìض¿Ÿ—üë[in½-ß”ÝúÆn¶÷9LƒwÇiá±uo«=°~P½:öÖïS£Ï‰ü{\ZÙ–ö:®ZD¤ë¥Tá ÑÅ‘Û£¥b½¶Ö/‚ke‡•v—aãŸA^ÛôYºÍÌÆSrœ­äÍg¢u6ˆ†NV³½¬Sò&G{QcÙÌÆãeÎìîWäÂãë£Bôýr;“æzqÖ ç¯µIìE‹¥åÝ£¾çŽ” +/ÛW†qÐz5*ÉÛ;¥NæÒoÝFuÉ~½¹îAœœo…ïWÈyI¬Ö? ÒR²‹ì]çWe¢t‰¸YÛËiTVÿoÛõƒSÀ›4tÚ·ºa¨€1ÖžåêOÎñ¸=jõÛNÖï— +i0DŸK±//ÌÉ7Üöb÷BÚŸÐa«¼9C'T8¡Ñ›b¯ÈBCga燡ŸßœnhXý :«vÅ\C!x窡˜&":+”ÜÐM©Ðå»$ÿú;ûµ7õÛ¤Q¯r-ø‚CÓî^!ƒRË7ÐZˆ æ6í:£tQwB-ôÁ®†ÚÕ_!LTµßo·ê4 Òp\ƒéíw øÊ놎֭¶Cc2»^Ó¾5 »%5”°qd¹Ï¤ëú ÕMÄ0šV¹<ª4æÖÔ°[ö„r¶û5xcBÍÊ ÕÁ„s;Ū<ÞAǹý*´_Ka“¯x·B8œ¯Ö®Ì}¥á«zá{R7smgÕWgîC^óÂyý"¼h ½Vw΋]^µTŽ®„ùv¿ÐóúÅÐ'¹ù¯eüÓõÞbÎÙ\6Œ|&•…ÖŠpÖ#‡çWÁ×x漎{Ý^ýmÐë8rVhdî çyÛÅž9 §¼HIA¾Ú§a [μh_ñ§$,»Î¯'_},P€Q‹>¯ôÚÎzÆ<µ×ÎÑ›>\†&^à*E7¾…:²³K™1Â7c?ÍcwPýÕC€ÃV÷k/Ÿýh É„WóÖã Qã¼ç“=ˆi³œwB„]OÅ\`«#§ò6îÔºÕV{ÎK,> ᑸµÿÈÙt†nTEˆiø\¸ùQAž{¼½*QeâÍ‚¿Í—~ „me‘¾NE²õA¯VUuóéºB›!ÅÛïÖ ñ l°ö%¢÷{pð›ñÒæ­pFœXeÞqC·ißPôÇŽö~ƒpšV³ÅŸ‰M}éòG|ø^"«&’ÊÏ=BòþxГÀÁ8GøþÉ#Õ¿o*‚82ú~Ò¾¢Ì`jÓvn¢dk½ŸœoSH¶s„DÎ:óçê_gpª_8³cDis Gc.Œ~Ç®æ¾efKOÞšªéËQZ×ÁšÝ沎yQ-§=†@0U¢ÿ’/ô å]u2ø¾Î¾ªíW»Ž÷–”êÈßz?ïµÁ7}Õºn, O뤂·ô=ªHŸÜŠ:ö$¼‚cÐðØbíÛoÜÂápqlŸäžÃ·ÛÛj!}ºgo”ªÑÁV®Ü<Ž©'áðàZ +/k{™õÓÌÈn•Ž~³NoÇoh¸øí·ßÂáøK!^]Y ‡c‹ QK?*žþæý‡ŒV2ƒó}™ü±4€Ÿ•¬ùøÚ&DÏÉ?‹Ön.ãìâ|–ßໞ¹züŒ“?VVðgñ¥¾?7ðgNµ6vàç)­\¹«¯ÃÏ:þÌZçjŸü[„¾7³ÖÙ{“üß‚Ÿ•…èîJ*»Fþ^…J‹»Ñ•üµv­‹ùÖ°’†Ÿ8Q½ºHÉI¿–òÎà~^ÂÏdñeû3TÉ?¶žSÞjü‰k±ÉÌ ½ã}/å›[¿wFÿ­X«H‹¥ÍÂ8›RvVÕ\:y–­|£¥bnU!°Ž=kKë÷x¡Z8«O2¦ +V?üD6~S +ûê±RXº_—^®ä‹ß~[î/ 2¤Îv‚ïÇBþÅ-†Í»l²Í[ý¸£…Â×¥MdàœxÆBÒše®>‘⯻¸»súL¾ì_x³5[rçåK}8IÉ»¹Û»‹ìEcg9WLG4˜O,õúù”(ll—{7«»¹ã%÷±ÞXË|Þ¶r*y½ž<ƒ~…å7–?Àuâu\Ú,íŸÆa4î­'ŠÇG28„˜‰âQéÄP—Æõß~K†Û£ÉmlëìÊkc´5;Û)¸õ3‹wÍRoW҇̈́fvîåËÀlŽ7ö‡{§Åš´6ÎÞFšd'ß¹¤}~/WàÖ<ÓVsm=Zó½"´áx,ž‘zÙò8ü–È•ænV$áAcÛáÔBîøx;¸¥t/Cþ-‰ËÉp$]ï„{ïápáZ#8;ȇÃ;#˜ç"Û«øêãb8~ró^]·WÇq_I vbcð+RŠõ,é7ã:ÙNd†ñW >%·ÃËáÅåÇõpd­p^RµpÔ$GwyëT'¸¿².í†c;íûp¼Xî…W7RáÄùb.¼võtN>ßéZ2N¿¶¶ÂRçì<,M'¬FFKau|i„õå̓°±y +›òó leŽÕ°]ÒöÂë—á§ðF½Nv(¼µ|m†··NÃ;W[¯áLWN„së«…pþ>ò..–Â¥³ÏLx/ÚïŸö#áÃä8>ªFŸÃ';‰xølÙ؟ײ­pyÿX _J7—á«ñûbø¦·V ß=o½‘Qn.ÍðÓyû!üRÑ×µ«‹r¸qÓ_ +7ÛGá·^uþX–öÂõÛÏpï4µþ¬_†GkÊÞbx¿:^ŒôsNj˹Èòbìíær1‘ß‘“ÃèË¢t\ÝXTåãÎBtQ_?X´NVb‹ë݇Åí•êæbfpû¹˜»(/–žµÅýÇýöâÑÃÞéâiãX[¼hŸw/GW•Å[‰ôóm/.>—ÃÏ‹µ¹¹±¯,¶.IHÿq±fí“QF+—Fd±2;„þ­lo…c©¦}3lc=¶õnbÅÒÆMì˜PÅX¥[^Ž=íÛesõA3L;Þ(~¶ûtxc^m+fcÐ[5‡ à-ǧ×ÖæÞ^ß:Ìç4ën{ãÄjfÞíÈiF±çðÌÎ-_õíóâÛ¦ýâ,‘ónm®­¯¶nÏ××K‹aB³vv×o·þz«²UXˆn,¶{Z~·´Q(¬ 7*…Ç£ÆõÁÊƨ*]oJ±ˆ¾™É¿·6ÏÞêû›Uë!¾ùÙ¼{ÙJîÜæ·¶Wñ­“ê[}ëùjép« «Ûk»‡ƒí­Bíqûäpuûùî˜ìþv¿Õ_ÞI&ó;Û'ƒÇÓþÑÙNµ´VØô«v&}º›ÎdMu%SŽEÙF磟 ?=~dÕê}+[lܾf¯_¯›Ù÷•ê[neóã=gŸ;¹Ã¾4È=åv#¹þ žXˆæS'«j>«mçË£ÑA¾YÛ½*,ÝŒóôdX88M¥ +×oÙBÿå¸\L/o·Š9k5^¼|g‹­ÕÎmiåú}XÚH¿Ù¥çã²T; wÃëñ­]=i=íîÇOÈ(»Ñúñîg">Ø“ìÃü^álð±wóVÜÞ묶_÷×*‡ÛûÙxü}ÿò¾žß·wñ±rr°íDåÊÛãÁÛ•³};½nÝ=]EÎ&ô²5:ŠYƒÛ£­g%·=*¯dWÞ®îßãrøêx»•ËW.[Êñ{n#|’0_ßN²FñþäZœœt6 +§©ãÒÆiáIRNïÇk§ŸÅ•3å­9ÛßyŸ=žFçáÇæøÜ*õÏOíXt!zÞLñ‹XúLºØQßÍ‹++½sÑ9¹<(§åËònú¢V~:÷+áŇDŪìlVÎÖc§•×Q³v¹Z¿Š\æî×/ïÊ™óËAeããJÚ’¯Žß¶¯šËÅ÷ëx®¬-D¯³¯ÍËë;+ºx=hnoŒ½Ç÷›ÓDlãæõã¸~›xY2o —µÛÇ{ź W[Í»õ·£»ÊÚzï®SŒÝËÍáêý¡úútßxªm?Ä·êá‡|´u÷ðPëo?†SË ÑÇ“õÚãåyùè±Wi›OZÕˆ<†ïšO-sõò9uyY|ÞÉæsýæ-ñ×öÇ/ù®üñòt?ªW—ökÕÿßÜwn7ÎëŠ>ß!NâØŽ›zI/né½W'ñ$3évrÏ9î³_€”%J"Ulï³îþÖže[A‚ aåô¬s¹²ÔùÝØ?x˜?Üß8}:ÝèÍ>>Z»?§™ÜãQ_¾züÜÚè<åçORïéýôªØÕ6%½»¿x¹Ö}[Ÿ?ü£¶¿ÿìÿþyý³¢>«Šºñ¼w4s÷üÚÿ̾¨ó/ûµ÷Ó—·×^ï¯v]œÿ{p2wõ÷cw¬ÏÆþãÆ¿£ëÒû¿¯çÅW{æíéõ´¾j¾öÿ}uÞÖÛůÜ}Ÿ<_z_Y8z¿Uš›ù‚ÿhäåëÇš´øY6«?Ÿ›mõìóåaiáK‘ö¦¾öîî¿>KÙÍLîÛº_Ò¿Ï6:“½¬ZíöVª7§½»Y³Õ/æþؾ;hïíW4À\D«Øºž¹br›Lqï¬WßÞŽºèçw¤£ÓFˆi¬ø'ÜÉž´°øê îcïÏàCsÿpÿùßCw‡[Û!šßŸÏ­nï#ô'õãº?ÊC÷º}íoÜqŽ»ï_oìkóÝ'ç¯ÐâIcÓñÇŽáiIï›,» +ììñ#˜Áÿì}ìwï÷›_üÏÓFð'x ÚÐöð8ÈàY†sØ_¸1ŒÒ’~o––OjR­´S)-¿ü¨øIÑæT÷Áû‰<˜W—ÖêìÖk{êp±Sÿ#],¹O•Òâ¡ñ’-ªíÅl¥–?/ ´ôºÓ¥-¿ü…G÷À£ù;Ê–wÎëÙŠ´£HµÅ‹A¯g׋Z_éïÀäê¯ÚòÞý’ºf©xþï—äݤ@Ô{*µïºë™\TÝÃjùkwseËî/Yí…³jóóR;mô®/¥úeó⸹¸ºø(Ï®šuú ´^™=|Û†h¹tAUuÛžÆ,Î Ðv¿×›ï’ò†TÓŽè2¼™õ­æÏ©rû ~ÎÓŒL0ïyÃön¤þŒmý––ZÙ²p²/õ×j­oµ4ë{þ|m½Á__ÔýH¯{7;×|¤-óVŸÛ¸­r‘ÞNï‚…@ë!5þnµr|¤ ÙB¯/çz|¤ûòµ6©ÌÍzHÁBsÑöó助þRèä/|¤Zñº´ <óW +þ赩“¯·mÞZ¥æÚÖŠ©15ýÑ+. +^ÜJÍ?»GRò^+m+»0#oÞs‘¶Úʱ¼ÊåÒñ=A +¼øÐðïépòõïÒ6¢-†wµp¡^½ÉE@ª}†Xi[Zsîçó¤ºþ~÷å!õ8™¢½ëݼ| ®t ³9-s‘Þ.ߊ¶3¹iµ0Í_ëBö¦?õÒ=ä#=X/.}O¿oó–æßæ–<¤°/~Vš]¾\Ùã#Õ..¤¦-ír‘N6ŸÍéÃwe‡4““š77MÁZ©ÜëÇκiGj•ïOùH[Òj¾[0/ÒL.HàŸ©ò²ƒô¢RxyW[tÈÛ¸~mú^.HÛfEF¤³¤™\²Ýý4;К_AþݾºìSæëSóV„´.íÌÞØ)Éy÷¯uã{nå_o÷‹ôh>¯ +‘nuT‰‡%¿vU‘ŽîŠ“¼µö'·ÚÝÝ«Ëb‹ô4ÿñ,Dzôrpÿ@‚?ZëUK:ÝúZà#ÝVs'Í•…E>Ò¯ÍIRɈöô®5ù# ðÕžt¶¼Uç#ÝYjÜÝÜÜp‘Þì½n¤¨_Âkýw¡?5H¯-é¦ÿVá#Ýý÷õ¾g[j)`!hï6*B÷rG¥¬éʼnÔøû¾ÉEjíV&³+7c€vî;xh~ïÌ;éƒZ šâåvg– UòK…¶¥ÛÒýìÜ*"-{H ¢…aÿ}„þb/ˆ´ÿ9³è ýY.Vš½î\)Òµ yÃ/˽þÉR°ÚjX*mTÈZéúOH¶' ŠtYÞªaùKÛ¥šFž[ß"H‹ƒvº×ë<| R)€´×[í~øw3@Ú×ÿÍ9šfyî  ïäg÷þ8ãØÝÎáŸTûdæösNøôâ@~?=}‘Ú÷¹_ïiHòƒè­Ïþö`¦´>˜×¿®xjXòݱó´ÿj…N¥ññ2yÎ{N…âþÆܵð©¥ÌÞ‹Ÿ¾tnó.ÅÂÏW•™ËGáÓíÒDz"~ú°÷gÞ{ ˜1µ7ùøÐüµÕži/œôéÓ?ùo;ð·'¥¿ãô<9¢ØÉÃæÑï9•rõÒkOøô,÷PËŠŸ^×çã<¿ÏÝšÓ§ÿ~ö¿Ú§¯§ÊÚ÷4D±÷×æÒ­è¯aJóºð馢-‹)–{üx8ÚýõLvfãzVø´±ºóÐ>ÝT–'e1ÅV³ÊtqNðToKÅÙÁšç +ó§¥ãƒþ²ót½º<•íãûÖìª÷\ëkåC¿¶.=å¯ÖùsŸ{ç_Ž;Ú¼Ó©èYûìïÒO>9¦üL£‡¹ž­lÚàaþ;ƪø[3[®®ã?gÄs½7*#|õäëM*ùÅýŠ#ÏÁÏñÙcKSjþpóœ ôt˜QÛ±>òàÇžÿ‚hœ|\|Sµ¿‹EN“Þï}µâ·½ÉLÎCK<Rc +ýœ+>RíâRˆ”È?9`±k%žŽ)ª¼GÒ'©v>2ƒÖÚ];b>ÍÌLyH‰õï"UäEëß]ië͇´xA¼W­À‹Š)±þHÁëÿÖC +kñ­õFˆÜ×ÄHÑú"ÍäÐþá¯u![Bº"%6…‡Ͼ-ÚǾ]íÚô䓳Åõ˧÷$p·¿ïìÙ@Sß·ó½X8ýÅá;GZ¬Áš¯Wi:º÷…w¤Î! Yjþöá2s´8ñµ¿Sse¯Âӱ㬣L¢þþÒWqá˱@†X²ÛÓÿÈ,–ìÖUW§˜UO49è—9çŸòΧƒ€˜À®m óÅ-~Q @ô ÄÞZí¥[Ϲÿ²³3Ûs눥Å.÷~sÙ‰)Á”Oöá‡4hgÔ¡v»#Ž™ìÏø èFÜ`ÂÒ¦žË‘!Ïü^oR¯öF`Rì”î~óÀº;e%¿,1q´@8¤åÍïÚVÑñgòÔ™ç¬/Ÿ®/“óVHþîà`ÿŽ&ã÷oÊ]߬Ãc¼¢ñzA¬äû÷Iø“z ‰1˜<»ö¯)*“„ÝÄš¯¤â,¯|™rVëú;=å¹tãæÒáä‘)¯¬]Ú›Ñtψ‰å=Å°è¹møE*=™8Öm\oôXºSöp†Š>ínhÃ8̧Ü(ÓÚÉ|Ú]T²B±-:•$Ç]ÚÅdú¥±ZŒ,N]>=Þ‰#ukkƹMàN¤^ +¬ŠÕb̪ºûÕg÷Cb´XN?S-ˆ'a€wÀ;ghëC–Ïï‹30¹óº`›Ê_rg:ß&s qþ! #‰µôËgiqÀ9 '“ 09é`¼¡Ý™[žÙð¹ë4“ž»èSº²3yþ¹—SrùD.ã?W%æ’"Ä  -ÞvÅØ Å`C½@t˜7`}® óÆÞ;%f^d-?-îÌ–Z}Ñ"W¤îÏI-d……·ÄóøvJá-yjÅYaIEÏ«ÕÏä„Š)Íèƒ5sõ#TK™\šým!MÚé­'ÞÙjùå4‡X;Ÿžâ˜ÒÏò¦PWî:óIbÒÁ”ÄÝ`ë2¹d;^~¤agóûvð#iX¤Ø¿Öþ»7åä;úH0/8•‰ó+”´óÜð:ƒ]D;1iÉZu#Sì®7FŠù%ZÊy}ùã0÷f/ä¸þ,õ‡´Ž}÷/0Ÿ6Ï´\­¬¿dcäc`óOåÏòäØNåÚù÷t:G™Þdžv²W&ÛmÐOO‹É™¤Î‡k[©³ÔwÕrlo¶B{^â·i©íâ&âprx*1R ÑD¶åp4‰9öÌD¨\¨¨ç6†®¯RøäôRÒŸdäæö(7ý‡*ІOÈóA °³6 W|ÈF´Ù‘É¥™ÔÔÀ/-ñÈÝüä¢ÕrR°á €¤þ¾x}ù$Ï$#ºÏVˆg-y/Ö÷o/õÎÆÁTõ¹qÓoy’Á— Çu|>9ðþ‹š”?«(b%?âB·‡xI@¬”GÜG,÷ˆ“¿P§íךÿ€oâZüGœëuÇG—€k'“EH¢b ›Álp6Q!Ù@„ä}ÓoÁ œâø,,9HÎlb^ÂNÒð†ë%§²ùëWËC„7€¾² ’©Ú­ä{Щ‚¸O&¥{ßÄËÝ‹‘â>H–øX_@â5H#'3rÓiøò™Ò+¿ASø@Ltdrc©¼4¥ÓÐŽÚcd>¹èù$/¨Ë'f>ÀŒ/–Jn,ááÎËÜA1ziñ|¾å)?W¿ þÚ +ê=Ñ‚2¹NÞñ+½aÀòINÏÄìy2ÂÜÅé3U'²e:E§‡X[–®…QuxY•gó7pUð[fž:†“×a>Õt1ïðšÏxÚÎ+šø +Å[yÑVêYˆâ­8XŠÃÇ÷—àÐdr#_R]‹Vy™ÄãøÉt³qîÄé8£ž@:ŠëMz£ dršqÒë=×ßç æ»!L§Dý÷ƒyb]” ~‹s¤©Žå†a$ŸfrñÇ0ºÁÁ”jê;>¹œ³‰HɘŸsß>-”hý×bH¢õ_ƒx $7B²„YùU¢xmས±àƒƒÅK´LÂ$lx‰æžÊÖéç$ìO¢¥>û0Nz‰Š]ÐqF—h8Êèw¯dœˆÛ òZÁ½Å©er]x¼'&Xà×±¤#¡üáæa9ºÒ—<å¥am»^Ä–°¤,'¸};?%VˆóÃ`£žnf^œ³kA±Dæî`'‹È#‚½¬&:ç·§ë@ÏÓZDä*A:]`JBŽ ¹=±Ç+¨˜Ö«•ZZ¯Öù• “²vþ$LÏg® ü}˜™’Œ” ×C|Ù¶0˜ðîÆö$M¨‡ÁÔéù­íqDH€bf:Cƒˆ;yWMƒ¥ƒ ûEdò-åˆÌ ~ŸhR 9"t¸L®§Ï–ô®gáì©–ë?g+wø6ÉF¶²ªÞbÝ\ƒWAGßÜ2z «ÅÂtŠ¡¡kè¢+èˆLC )© V ¦¬¡‹® c«G©¡‹® óY}#ÔÐEWÐùªG¨¡‹® ‹¨LUC]AGªÇPC çÔV\C:¸¾ +:׳±†.º‚ŽØcñ5t¾„äˆ +³ý|“gm‹sàÅ•@d—SâFzëñ‰Û³É꥚yèdØzÐúå’Fz¯ê~mŸnë˜;¾z0ach:ÍÏúØÕ_+êêøB’b°î¿îi\ÅÇ5XtV²õ‘X_\å\âõ"Wáó’œèµôST>ÆD®¢¦Ä+™Ãµ¤*šK$kT/·']ÕOªTš?ÆMI6É›ÁFŽW]T¦¢—–IXìæÏ„L$œ {>jĘ»‰®mü^R\±›Ø?¾p`¤e#2ÄçšÆ¹!@5ƒ¯R8Ù`þ,9|có |ÇU>&“Yf*§—œJ‘Ûû ŠS(S•˜’Ú·„U‡ñ¾=‹w—íÚcáÐY\àðÕú +„IàoO¸I³CÜXµ„f—° +Œ±”‚u`±fWâ:¾ß¸øu|qõ/S¾ •¸4M\mJ‡sàó‹/S¢I­ðJ4Åû›Ÿ8‹à©å`r÷/iŸÕ‹«‰‰bv:6¶ +Ñžwó!ªLI3X<Ç'§˜?À:"ÅbËD“/Ò“hCQÌøUÖÎ/*þD§v ±žSÉ•Ì– Å|Ãn–{ÔùqËèz¹À‚^'ý™3Ľr+ã?÷‘§»ÀÙs=¾gï¹ØÙ ÌQbˆ¸W£À>»oÕHV*7ÔÝIñúe`Òð–¸Å2‘!i¬J8q!¶ª‘ËÄ”9'¸ò$ Š~åI&ä^EØ‘sýP*¡þ“€äÌ͈ˆ“ÿm¤ ïp\%j)ÅÈ…¤Ž¸"ꇫ͇°Ç6’žxï¼ í±éÂ;â2»`ÑÐtz‰‰4d®„˜RFd|SòÕ#ä”4áˆ)ùs¼‹ “–tJÑÙéñEqQS +¾»£ˆÿÒ’¤qDd6IDfÄ,5°„§Ddœ;¾˜òœé¹êÌ(Æßß=""²;R”¡ ‘ U Ž‘Á2´@DFT‘WŸ§¥ŠÈpãü› Ês’ç`‰¸xNN–¾‡­:$¿±¼åËâ¼ñ$Ʋº|RšJÀ Ñ5‰_[ãÉdÀ}™;È'¨K›+Ääô&)C2Õ/X@&Ž.¥( ¥.p«ãêêR'͆3TI]]Â|½Èº:¦t4¢R8>ºù’*æô2¶edªÞ~ø]¤ø[BE7¨y%&§±Œ#§.º.ýûdž©‡ãeAï¿»n„:‹õpQªã«‡Ã¨õˆ'0A=ç Â"®áëá|ÙPƒ¿É».øÞQZ7îz¸Ø·Œ¥Îwc• Õr¸z¸ /&ºÖÁJ¶Ñ+ëQÁ-'†òçDŠlË$9‘§Ÿ‰r"ãÎ~ÿUÕ X' £W£“qÊq»Ÿpœ$u$ÂQ¨=FÆC%W(“!">^¢aq8 ÎwfzÙ2žóß³¡ŒçüwÌF¡0Ús~:Ê1ôŠ˜¨Â¨Ç‹|­ OŽ áÅWÒca#½Óù‹\FÏÛ€É8鎠ÆK!d½–L‹ÅŽ#~%°àmZ¢4ëð+c߆æJÞÖX,‡cXX|ýb§X‡Ú­H•'‡MaÖ,z±ðP©É»E¤ +R;“IB1¥~c©H½þOE*Ž3ŽŠT¬½"GGE*Ž“ì5З˜—×Gˆø¥©©“ŒŠg÷ýÇð|äc(…ã˱q—‘|þÈŒžq”Âq÷eì¥pÃÇ-ý‹öÝSø•#”±ï "Åpÿ‘R8NTá?P +Ç%ôÏ.¢ CV°ïOŽ*|zæª&y#¼ß[¯Æ¸EI½p(=QD1A¿Ù.¥ zÉpÚ˦ý"áNÔÛyA”óTž‡%¾¸9XWw-|NP9 ¢£õà_ìWæásö€´¸\Ú{i½vîêSO¿¦=½rÛ<^Úo¬Ô~ò ù›ÇËæi^¿lôVVÛÆñæúZõq}}­¶…MŽ¾ê(÷柲qò×bñªÎÐKrj”®ÄugÖÁÜ>Ë^þb·ùǽ6øÌ Õ_fs ÙOQ…vqUìöT"•šÇkQv¹½¿;Òû¤­ŠÍ ÖbÍ•úL.X즿l» %`“…¨b7¹@êïÇ7ÿ.ª°3¦¦¾ßkQ±ÛUTÕÙ{t…Ýçñ±éÌv÷åI„´Ýïà\Œ´±{Õ’wúC¿!=ðWØ‘]…묚|rø|.!Ü<ŽF|Úõv¢µâ>…sTç¶Á1Dw^ÒRó÷-¨P£bÇ Rnýl郮ÅwG´?ó9žÒžø Õ`ÔDÜÇê;Á”˜{±ˆIÅ%§ÆåyPïuœäxmG8ïTI]Šê$—* +wUOœ"S IÞ ™"¯/®‰\d^_r~Ši"'\¯ï[\³‘¤ë‹ï5˜èq}FïR¡\òóÒêŠ Ó]“6 ‹ŽôSM7\&m5Ïôâ_5]Â÷ÃŒXMÇ‹ ÏËèÕt¼Zºa+ÅÕt¼XL›;aw¹­DµJ Û܉*¹’Ò%JˆÎOöݶW­‹¯$]f"£T‰ÚÜÅF‘0#·¹sl˜èFw#·¹£µo1î’]}má=Wêò‰½‰Éë,¶Æ–¢ôµå™ÜC×òÀÒ‹IùSx˃ êbsãøËèÐyŽŒÀ'*¤K’Ñœ‰¯0'q$®/,i‡)’È:ÂПŸ““uØÌSù`=¾4U¸fÞ»¡D÷=­ý÷! »|ÙPÀT1ùà‰³¡`¨Øü±ÄÙP0X²ŒîèÄf"ûÆRù8úUvŒx¸³û ÇIÔÛ•7 +“Ùu4ŽHF U²s+…ãkÙÉ`‰ +kE= ‚…µGáÂÚ£ñ½K#cÂc0Û$ÂŒÕ]BRóHÛï5*‡z6|¥‹•xá êHO\”CÝí¿Ž¥2Å}óqzK"<Ôöï¸:ƒ}ñqâ³’ªQµèö ¯lNÐQ¢²:SŒ3†·ÐqFíVMGBÞÓ媞$(dHuÃ[,qŽái‚B†Dø‘;Ü ê+E=î†8†œäŠaûV§ëpUù8Â10šÓán,òb뉒uʵžÈí”7ú1Œèpç¯KJR2L‡»¨~¯Øã.}‡»¤o5Çê§Ñ k¯¿=3Gä½&.¬…ÁâÍœLC‡ö¦®°6Xù¨?ÂúÂn9tº÷\‘q†›¡wDà8c©çÜ‘’ùb±ãˆkaEL™¸×LvyiêÛyELì­( +ˆrèÎb6"A\kÐ÷-aS’"¦üâmÐe´X’2&ÿ"cb§ÞPî¾˘æ +‰*Ô#Ýv÷TÎ’œÊ$ELùÅ ñ=¬_yžªˆI”A„½#4m:ÃðÜ©áá­üÃó²Äó Úb5®ëUñ[ÅÐIÛ‘.U»GBJ±=v1¾WS]Û·1Õ¸^_M•Þ†yè§j÷Ó)¯6d+s>Ý4ŒUÏñ5®0¥ÄUÏQ5®N7bÛäã˜~XñÖ^<®¿ÊëkµÍ³úTwó(“«/—ŽŽ?ïK|jí“ê¾æùuóIÉ/MÖ©â!¡]&vì|òáí.²H}õp™\êôbŸ Uùû°Í­_]êáŠâ"¼Þïýœì—É2/»Ÿœ"ÌßÙÅš÷pêº$„cë+¥ûÙ¹Õ#–æ>žš„5_hA£Ó½¬q¢=~å¹'v]´I ª^$·A¾.fñ‰“ t3Œ½,lŠˆK’©UÂ)!'‹'µž¤´)¾°©?®Î,$ Ó™%i$©Z%ܺPg@p”&-ªê.ðæɪÒb24)•2Éø)pé•v}nÔëîRwYL)”¥V‰ð0ƒy°éª“Ö"ņ¯ôè-7ü5Vi«“Öº>òPU€‚)…jÙûJ:ŸðªFoÊ—¼#Û(Mù˜µü›ò…¢ +ÿ‘¦|qÙÆÓ”ÖŒD(£7åËä8VáØ›ò…û#ÿ'šò‰û#³)_¢þ•£4åc£ +8)C8)‡Dq¥Áâ®~£¿jsï†JØ×/Ñ»¡FîëÇ,m ï†õõK—§4l_¿è®~C½ŠÓ×/:*Ä÷ÄÓ÷õã3_仡†èëÏÉãèëâF®FìëWÉ5¦€ndW¿L.U@WØ×/ziØÅÐ}ý| uõ¢·¯_ô‚xªÃôõã–#F½Õ<9aWÊrß=D_¿è7¹wâ#öõ‹TtëD‹¡¯_tX8Õ¨"úúE»ºÜ,è!úúñÊ$½£7Z?¾1Ô¼§êë= +ÆùÇÑ×/úB%“xœQ +߃©Ãöõ㘚LW?¶;ƒ4B_¿À1”ü]ý|ïìJ]ž"¹}ýbë^ÇÒ×/º«ŸËÉ#öõ‹N¸f*RGêëçÕoñx1ðž«¡ûúEX°cèëqÖú¯ê˜úñÍF’¸_L;¹ÄýøFêëçŽÂ=>i«9B}ý"êb³m»iûúE[òøƃqôõéðÓðMâpõ[ ÌùÁMbÜ1Œëë}ßìÏ"¾¯ŸŸÚAoqØ~|é’4ÄýøÆp|Ü®~£÷ãKRÄ÷ö†p®FºV|ÁŠÔÑúúùÛ¡®~´bhô¾~n©TT_Ñ‘ûúE›9KÝ×/º«ßÈomrúúëKØ×/AÝëúúEwõKÝo¨*Ü€´ ÷õ¾žéêÇ`©¯_8”Ìvõ½å,m_¿èô®€º¯_t¢S ¦4t_?ξ0]ý¢ß —¼¯ßðqK?ÅÒ×< üÊúúy‚‹×ÕoèìÁ@_¿HŽ`:€ŒÖ×/Ú0¤Þëè}ý¢ b)Ï:¡Ó!î.WpÄË*Ök|*woÛýë»ßCgìûâ¹Ëm— +3›ûù§ä¡Ø¥æžÊf-÷gÙuÔ4ó˜¦p¸öV}ž]Ýü³]ßÚÈy‚ĽÌYwƒ3LÌÁ»I\Ss¿ÝÁ|njdêŒr¢‘Ùª3óWºó©Þùë©Üéûjp6{ëÇ7ÍúÛäãêáñu>“kò`ã¯áoNQ¤´«Ä®ÃÔ›*‰D ÂHC»&ˆ)µ%Îîロæ¾~c…ôq?ØЭY*CÊë¹÷·²óÛ‰Œ<½¼až¶×ï_WŸ|Ru[’ô‡-܈-™•†kß±“Ÿ÷Pö‚‹O¢Zpé׫ŽÕZ/Óߨ¶hmP$Ö¿µ·É¦Ü®4e€»½|ˆ²í`–ØìmßþcmF=Nñ´´_‡–*-5ž¤§ÜG­‹ÅÇÖ|!½®¶Î\뢭¬?hž½I~ÃFÆ;dà>à÷aÿŒ$ë×yßEvæRÙdLhbQf/Pnþ åyâ{ÃÄvßO°ï ‘Ñ?›ÁÔ+o€£AOaÇ)Þ¦ÒËQàÍ»¼R|_3Ñ.£=KÈçEç·Î÷¬óÛëd Nôƒî²ë“îŸxÿBVMÞµá5Î^9›íã—šð Z!§)ü®‰sûD»¦æge–æßNvQòïu'ñÑ…ÿÕo_γŸµ5îzkW»ÌÁ‡éÃk`>­ìVÁ€b¿YB1;K‰©õ¯fNÙÁKFì)o>¬÷:`@‚µäë~? VóHÉá|^™Räu ”õþ‘K„k–¿W{. ¯<)óH€œì#‚ºrP(8Dè,ï…Ó‘Ò,»%Q$p+ëI ÂýkÅ-—í›êä¿“|@óñ(›{¯ ”_™¤¥Û«]ÄyúJ :oåù€ÏJQZÑ¥ÙÜ(žCg²UŠž¬%r,3 µ ¢‚n ‚^kÂÝðæq*Ù݆§ Þ^ÀZ|C$¥DÁ#¥xËs¯dò®›Ù@ö‚G‰ˆ9D²à1¶Ô®= Åœ—Í|ø!‹ô-JôÅ9ÇkîÆ_Ü6ǃšÇX©;J¨º<6ÔÑ'>±”ŠÇ<:y,9›2'^È ƒèû”BlJŠ‡'eì†7€Çc‰‡(²!¤ƒo€Là¨G•¿EVúæ@)–t7ŠÞnD3U`Lܲ8ªü-²Ûé#%µ.’,ÃÛÐTË`¢ +E‘àJLÊD9›¯_8C(ý[ÿ³£žÏY’wáð¥ÑsH ‡PZFÍ‚åËa8b–på€bñšˆ£‡f“r¥XZÎ*kòóõægnÓûªNÏ­o1_—O¶·Ý¯%¤³Dö…‰±—œ8©û•&i»_é­Äà+ xy_}ó*yó¢œ\¢3#WFÝÝqŸY·ƒô•Mû2[±*—ÙÒcµ-ïË$î×Mn@¬]\l^ôaÐQºû³b»Õ.Ý2¾hgúýšÆfò‹ú pGÜÚÊ ŠzÝǯn`°w?Kó÷ ¯‡ð¥&mê‰cJB™.ˆ~]¯V‰:XóÂêòé×ÚÀ× +n³è‹[–í7ì¸Ubã­Û%7 ¹Å4Ë·¦ÚòðݨÓóÏ«|{²GéÅàIkqÖû•Û‚¿Ï<À¸íàxþp¢ϯK4pÓÿ÷ eòø. _÷Wàì»CÜ9•ý&¡7ÈîqðÉ µ€Çöz²WÃ_ßÓðõ„ ;KC5µý+Õ˜–JKS S4j]Uðœ/¶½ òýË=PÙ¸¬Ôk…™¹çüIs¡¡ý øŸx“vÀ¦ø²I¼|þm&ú±>ža½AÉ»8 '“a‹›Ömµš+ùîq}ãi1ëÄa.ÎåÛ³AÉâÛÝ”úèh. :€åÇv¶îa¯B® €Çháá¤æ¢Î%ùa{£Ÿ®d'föp«,Ùà×:êàÓ“F†p6ùõŒX!—øðõ|6|½’Ù¶µ×[ó”üòö’3Ÿ×'•yp›UïœØéÎwÎYËë»A\uÍR­ùîmñÏú‹ÑÜ[{³¦,_=29š˜'@äýC³ÊÙRíç¼ê"}t˜æçV’[òJ>uäÁ§'/HýHÞÖx+üù§…æ£ÿœ6g‹Kóæier§y¿“³iÀvæ®oJ›­Æo çTžKSÄŸçE0ɾ81Ìÿ\°¸1Ìÿ\“p²ÃüÏE039/†ùŸ‹`’µ81LÁü¿KÓ´õ Ó²ô‰Úáï[··×ûûü÷c¢œ™ÏÔV7dùäãé³Ùëv»ÿýSÿ|ü}ï~üLÌMÔVÖ76,½Þ}ü|êN”é7]™Qq&ë=Ùëv_4”ò×êìÖk{êp±Sÿ#],1±RjU}gË/ÿÚ™*Õ\¶|÷t•-+Í£l¡Ý/ã× *¢J•{éK¤ÑìT½Û[ûm”v¶Ï7$Fdß·3¹ÆÙZ÷p¥Ö^¨µW/·ê—ÍÓ£•ÚôI´²cø2˜yÎ>X?3Ùâ®–­Ôò§x‘"2;]·jÙrqeÝÌ–&wq}»ø`9[YUŸ²åóvvö{ª‡P‹3Jɹ.ð”]óînÀŠûŸD¼fèݶ\~~^"WŽœÄcGnYÇ®F¾bºeÁùô¯K̃¢c俱½M»ä|+`­]^ÿlkÎNæ~gxCònFY žâšÕ»deÔpðšÕ{ 4’ +ôŠzXS!©¡@R‡6’ +™œH§ç#tºO'°¨qœxÄÁ˺SšŽˆLe ¤70‘7­Þ<€µ¤6Ò›ÌyIl ¤729±­2¸ š)GüÏÞ4a÷Qœg˧· ŠgçÙâîeÁ‘›¿sû(RÁÙ*.g‹ß–²•ƒk”¥6ê…iæ‚—8XtÃ/¨¨ƒC³Jx³Þ…ëÑ—Ôù® ’R´‚—”âæ‘ä‰e€Ò²äHËý™wVZ:*<¯äW²7dÛQZ®WËÄVn~6jϪÔäø[ž+‘+EcM^A<ø^»[ÝÇ'€Åìy¢•Ê±çä*±@¤âÌHÌî•Z>Lò¬ÖákÍ‘7vH>q¿-ši ·N +–k½8¦PëYCŠíÒ”°ØVjõÍ…O ÆÙ“¨ðd$vëD!ÿ”˜Š#7ÅÖËŸaîi_ê`‹GÜÜ„@»Wš2׸ÕÝûɃ@èúòöŠóÑÚ +Ý‘þúîûá;ÒÙVUk¯¾nb?m¥|—¹•æ2·`_¨¾Ë\c*›[›®†àܯ՘äæyK&·ÏN²…sìÓ‰±^û»tÒ Æ2ÚáóÁo§ßƒß.zµ¿{%/ „°œÏÐwîçØKÊ7t¬£—”‹Šàž–ØÏî%eà†Î ¤¥I’€b¢Kʨ›ZùË%Â-KsHpÉ’fãÁ«õ•ê¦6xOëFv£.)£âx—R'‘lI,þ”tçºzˆËêå¹ýŸ÷´Nú#÷Æž=påOË5i²EBn AÎ%›M +Aše² R\08R„b©—q2WKuk>•eûgvžFWºÀ¡XRJ ç]‰[Fga×@½z“-¶€G.­KÜe$$%¦wÊd€þd«½VrÛŒdr³W=õ›w¿è@6ÿÉ%NC‡Ï;wùy ”Âh<¶­U½„<9ÄíŠ4*å_ +£ñ˜ôQðXâ!¶KåXa ‹ðóØ­Ye7ÀŸ_¬ñK:Dk{2þ¸òpRlÑàK,ûøs¸­òI‰s I0±»f쉚&XͶhÕF“’è—¨e´+Ítg#4c[öà†:íåC%f½¥šÌhä¶öS=Y`†x¸Í¦žK%7ý1jsø’ÅL˜ƒsöƒ³xœ6+#qÄce±ê‘’‘–1šˆÂXçqáÂÒòqyc™ýÚÞ[a¿œ¬:IкLS?¼Fjd®V%û¨Õ—KwçõßNk{ýf“”ÖЃ¾âž®Á‡|ê—ƒ†ª½„Ê¥Öq® L0ºq;·àx|{Ý'rþZQîÚOeâsÓ Oc7qR©û¨.ŸLRoz]šÊ“—SûZâÞ8^7‰gÀ\é×›~—ÄãòÄ»qâ0¨˜ž]·—h àÏ<ðGý™'²{Ë»Uõ÷AfÿaœëáêA„Õ TŽ©DX B¯ÆT"¬ñ^Ï:†z’¨$“c=ˆ°$“c=ˆ°×2¶z’¨Ö2¾za5FÇV"¬SéÖƒ »ª{yä0ùÚ •YÎAÚùpþp¯;K©˜_<šuêÖš¿oDNaXBräÔY£3ˆõñN2) +44>+ɳðÒ.jJ½rU"•knX±BªRIñÖ¾ž; µUt^.§7kWi¸oÐ7¤AoçWô$U¥<(¦BanpG2G–Q`eòÒJ™©ð\Ú¨‘ëJzZe…¸JN4/x×|à\W.ÎîiAŽá‘¢jäX…Š0*ï¾æjDv!'ìØ%Z…\³xc{w-ƒŠÈþÿ]ÊÌgrx[y×øxbo*3¹ürÔýùýBýn­ûü÷c»ó?Ý^Fž ÿIðþkÚ²bM(º_tüuû!S °rqbû#“»«­ö~êþ~~tzÿ31‡?ïlŸlÔ'æ&(ìÀÎO`6Ò@ã"^ÞÁ ï2ÒÄ*üÿü¿2¿ðßì/|®Ãÿ÷`.RÕ2Ý4eÛ¶UÝ–UøES ÓÐTÉÖ$C±FS-Ù´à|° ˜hUÒmMÖ%IÖE6Lø¾i8Š.[–IÆQ,E5MÛ²dI’te¢p¸(ë­EÕšX[4•"!ÀùkFr¨3üø² þÁOÿ5!K;W7ÒÄÎþ0#ËJUÓ5ݲmS· {¢¢Ùrph²¤Ùˆ~â=#KVUÕ4Ö$Ó6mªºf™¶jë–a ˆeWMX¢bˆR5`)¶¤(ª!bUeÕ0sâ1TMÕǘØÎØFU’eÙ–T ®¡Ú 4]®*¶¡¹è¶\5¾+L\)UÙ4صézÿÀ¤Va§t[‚ɆŒ@ +ÀV\ $£bh"Y2ì>‚HUöÝ£l‡ èa;`Êh¤hUÍ”È0\,gÅM²lØT]#S¶«–ª{ë‘R¡²Û²Ô*ŒÙyE­ª†¦X¶)I*™rˆ9â™ì1óŽN³ÿš$‘-ÖÈìš@²©†Š@@›ÙY2«&àZÛªEdž`Ã=ZËH4¡l[³8 @Ù†½W}|G¯ªÀïxÄ‘Œ6p‚j2”ÀwES|t€À—ºbycÀ<48ȶ$Ø"•ò ÈÕ·86¬Ρnq@ÉQ–SA„ã¢ðÆêÂRáä€2 2¤NÔQö2uÝÛÐõ$[ù»>{2Ÿ2ÌÄ#üO7$27È"¬,‚ó S`&`L:;¾xì¦UÃB¦’(ºMg§HŠì-@C¦Uie` A `~økvSÂ@6ž3ÃbϽjTMEè@U=)GÌh(f@2ØŠ¢Ãî`í0’"J°9û‡@ ž ²f*TШUˆÁÀ®x4@>Ârpùĵ"U3uàXÝGcð¤cX1†˜jæcÃ^‘ÙØD3cXXT¦ÆlgÄ®ê¶åY]€* d™ m•™2(Ø^ØLyŠÊ±bÔ“»@yàLK± ÍÀ½§²ÌNOÔƒÌËÓ)aÈ5²djšá!û•÷˜”¶ÅØæ01€€õ)°rbvÄ}E‡†7ŽŠ‚¡>•¦¸iŠgÁp d‚ÌtQN…aª¬Q®€+Ú +1ˆ[#)LÈààJ£ÉU¬  h(ÀW4€Ò&¡ †JW1£8Øà'O<Æ6dK#Ë*²J)¿|¶$@°!¨ÂÜ á"•™ø‡$÷G°•%ž–D>$Ö¿a€9I¼$Ž"Eo40!ÛÒ¸ãX ÄÑmE‚€ƒæí¨GPÇpx€>¦Â«âœ38å¦ì¯cœQãŽcaÀÆ@Rëà<ؼÙp@« +…¨žMˆÆñ{å¨Î“ ‰Ô?eŒ‰BqâüŒÜ) .€sM¦’ f …WÀsª)™®²A¼‘P?ˆd0¹b€}cÓk=Ì?e2^èÆDkôÌÄÚ"hF÷Â@…ÔÀ2E©Â0¸°ô€YÿSŨ$Bý+{FÇ£°ŠÁG *g>(9  +R´³IQ©@F†ÝUŒ°ƒTf–Œ7 Šg¦ó@`~¶¢1 !Ths«šÁlgÊàj5K·'8 ƒ„ÈAgA†£b©°MñCS怿WÈXª ^µ–<¨O0´‹qŠ$±[ +nø£`õªè¢xRá<˜L€ |9q:€€¦pA ´ጫ`ûšD»Qáâa3ÀyÄ[U² üb˜ìâMªqAÀŠµ%×O1¸¨äª­Y¬ÇÉ™²ŒAyÃ`VZ8ÑY09¨€½1L®‚ #¢"4c +!KRyëŽß*²£Øð êà{Ç®Ó$…ÝQ ìÛõ*‰]Ö ezvŠw8°tðŠUÞ*(h)ƒp$DE׌£€-N$œŠ~ˆb14 ƒÀZáy*0_Á/dSΔU :›'´p +¢ + R|¨@§Û¶Œ@0’Ê›1ƒ¡uÇoÕé«_M³–k)xŸG­ +Žºg¶ڠÉÄ ö³¡9ž7X½Þ€0—ª¢5jB *z­àà€F£Ÿ“·6º~SõÅÎuÕ ;LL\U¶yÂDœ:&ô‰˜B>J¨ ©CË& ª;â 2 fQ^Ï—x!Ä;ÎØ‘)ˆ¥¸ëöù v +w•›]PX-NÔŽ~z?ž' +G/¯îÚ[÷ãi¿óó2H,(l AŽÿç«ëÏ>øËÍ*pouà*”ǘV€×G¸W° )M¨ª`Û–¤aÌæ¿èÔV .‹­PÛÀ²À65-h¨H)ò@nL" +¬1W\ÀŸèkƒÄµP¶ƒFC ð¤%&Ü ªÝK ¯œH/ ‚w†– 3‰\:QÛÀ±–0)ôºT$Jrû8 tæj‡‡ +,LÇ*4e ®²!&Πˆf0 H@p‘€2¹—ࡲÑgG*«’I‚édʋʦn=QuîÂc÷Šhð$цÄÀ=Þ&¢Â±Ð‰g¯ÙAÃVÁæO•*~ßE˜7ðxMßÖô@I¨iUâbÚATî}«T o†p0[°h‰â70^ÎȈ†I3r$*,K™5î9SÆè(ëúqá ²ØUa>y &wÊàÂã÷Šl)X…&^é©`öÓ€3‰p0é5Ô‚·pT°´ :^rcÄÝR0‡AhÃ?:/ØѶ@^R#€ +)9 ýÁþÖuÉ1@ß2Á+€h$r ‡‰¸Šž¨@ðT$.GRe•:µàaÊ22Qp¸ ž§ÅY8aB'rPyn‚ „1-p¿SfŽ²¢…¤X…3ž@‚½Xðš‰>¨ +NMÖ«U2¼­ ¼ª€$ÊJ”ƒ€fŠ®±æ%8ƒà( 9Œ²ƒ‚·i` äÁ3U*\¼QÕý—pÀ›6æ`‚‰ªc¢oÊísa΂Äî•kðÁYªÂ0¬¥ +dC£‚=ª`ÎZ:âÁ@³Àõ‘ ›ñ,À(–t( kîcFFYx#¡ÃäÌACWÄÖqi%G 0i,•1¬ ˆ,QbžJÏêá Ò1gPµXöñOW'yT à Ö©VÂQ4lHf^…al0mÝ0mÞt)ˆD@L’M@ͤµ%Ø£ÿSïÿ„Rp»ª`t‰0¡’T:¸<*xTÑ`1€@¢KNd›ç^ÁŒñ®N›Þ^‡!,P¾ A:Øn0ÁKDIÊd`Šµ„EgÄ0D©blÁÛc*àMbïvœktÆ$n-¡y 0$¼n +ÂÄl~¢!ôÓ >*Œô€*̾ÓxS& ¦A@4Iç-<~«Äœ»6n'dJ°‹à¤‚ðvÌ?˜£Î2Ž‚f,H{ã`øÈ4þö1sô™J O²­sA€ŸÁD½î ÉD ¢B£qÂZ"¢\ Ü`ó@4¼PW#Q¡;¡ù˜”3etKdÆ8æ,œ€°¨ J@bGñP#@ð»ªp§Œ ŠAA4wá¬xß+b–€æ;º!†¬k( MTÕK~ þ Ù ¤ÐŽD 4ueÏìGç +ä•Î•IhH!˜Sƒ6èvÌà…‘tŠÉ§ÁT‰&*nCQ¦öØUŒ&䀠`¨TÄcT¦N­?ðtªàŒUtJ$ÖÕ -; "“†2ϹB¼9ÞŒ9 ¡uÇîÔÀm¨cF¨[B•qÔ„e£e`Ùt, ]ƒ98`\) ¦Ðµ²Mã‚ Ií«¢¢Þ ¹±hÚ4»Íw‰õ* ˆAA$Çh3À:a胨lÊ2(’vDƒûzƒ0eN™:%LD‡³pQ\ãÏžà ’}>,L0ú-[2HærhÊD ØëÀ&Ø+ê ¢ñ‰™y@F°G©ù‚wïo$ó€S¤XÄ¥FÛ\‘ý÷9FŽÀ¾E &µŠy–¬Ó =EÅf£ «‡ªØÂœ•¦¸£ù®›  ˆFATÇÌÄóèó¨Ðõ³ýá¡ð”áÏÀ¿dÂá…‹u5 ú*”€²ì¢òÅ t£,ÒÁ#ŽghÊÄ •ƒ…,Hì^y®ƒIÒ«q]¶Iƒ”&ªL°œº„š…î%΋àÄdNÞ#6&êZ6Þè +X+<Mk¼ÙÐe'¯Œ¢b¯™py’„qy°£M"6q†¾@«”€Ðœ<ÕŸCMQ¡·‡¨L'‘ž³qUΔñÏ &G³p„pyä Âªß9 M™‚ÀlðZæ­;~«þw<ˆ“ŒíÞ+;ãÿ•+eõ¤¡H™Ê½ +æªO‘è©æDO‰Jvñïi d8‰Á’xA-"Û‰åª*•66hL˜À¨ÁpBFmÅ8'aD¤ðEó©¯ð„AtȾ[Þð² SÂ!ØÀà0ˆ°AÒóÈ”Ýj {Âq‚Æ2wÙ±ET +±ej:ò‹¬|)êÔ µ¨A+ѼxÛï¡S³XsÌb“ ÖµâXךÂÁ40ämZé䎜/í•¢bä’‰qPa¹‰ì‹…g GÈÆå,A,&\EÉGíxGÆq0¡‘®9v¼äΘRSß±ãÉ]qhÝñ;E6E0TaY– ÿÒÉ4Ów €¨‚6.¨J[FyÉ038»ÀÊèþ¢ÏdsAÌ`ªsuê1 Ê&SaÎ7“ýÎ!…ÌÅcf­[–?~š2Ö¦±õ8œ…SÆŒƒJÆÊ<]$®»Sfl:‚ɃBSuƒ ß«k8ÊÌò[\™K÷‹! ‰Â *žÌ% Óë: a4m¡²­ú…&Lv‹ÍÐ /;´çaòñP…dnˆC27¼ìØ\êHº:7–çãQ3>PìF6Kf¯«,&BÁTt b‚c^6Ö_¨ŠÉAEÓ²àÔÀ8˜Š{Du†/8 HU‰BE +KüÉ%á)“íbÓ m:! Œf<øI®4 2°Ö$Täë·P Žƒ×æÜ…Çïu¶ÑŒÅuÙàNå¨Qð9A5 Ð q-¤$ÁÃĤ5ô\eE—¹ Nþ20•d’­àèQ̤G_>¦Šðô $¨G9¨ÂÚ3å "å,<¨H Ñ•D|LºRU°LH¹¡Ðz¢ ¥ ¡…¡uÇoîh(}Ó3¸½OÛkôµ'òr‰J%“Ëíwž»Ç½Îß·n/óÜïüŸîDçããó§óÓý‚'Ͻnÿç³×è¿|þþ2Ïå{ÍÌÿM’ñ³ endstream endobj 22 0 obj <>stream +%AI12_CompressedDataxœì½í’$¹u%øþ±?dFÎn§ßwgÍ2"25\£$Ii5&k+u—Èšé®j«®¦Vûô{ι€DDVe³¸S=œ +tfWàp¸¸Ÿç^üÕÿòëß~qÿõ›yùE¸›Ó_ýÕéíËïÞ¼ýÅAß~ùÍ7?|ÿî-¿úÙo~~pånF£û_Ö/{Ã|ùöûWo^ÿBUª|äÕ?;¾}ùúÝáô懯þðí‹×??üìç¨úÝ«wß¼DåãñË_¿}ùý»7_þêÍïß|yüæÅWÿíø7wßÿñ÷?C@ŸçïÐØÍíÃ_»p¨¿õðë¿eƒ¯ÿøâûï_ý¿¬Î¡|w|óÃë¯_½þýñÍÿó‹Cˇ/ÒœqŽ‡/â\QÿŸ^ýæå÷îfç\›Cs!‡ÆKâ] ¥º9¶9ºŒëÃ]ðsnÉ…0—¬Îî²Ë>[ö|~óÕßâaýöÍW/¿ÿþôæ›7o¿ÿÅáôï/^þöÅïQóâðŸ_~óÍ›;èA'¼¾ôåã«o^âM}ûâÝÁ¾·û_:ÿåñ‡Wß|ýw?|û//ñ}Êü:|©.ÿá{ô…nùo~]¾üå·øæ·/ß½Ãá†|÷¿ù›ãvøRåg¿ùóáoðsü3ßðÿzÀT¥]šg½w¹àü]kxØZ ^ø|ç«¥´Š72Ïɳ—Ð&âø‹·®œ +ª|bù{Üȹ’cÁí|v¥5|S³/1¹9{?û˜ìê”y±/û‹CLs«s¬1Ö”9¦dÍÕ·›·‹kÅÅ.ÆÝÅöèÿü›—¿¥%Œ%õ_~ÞßèÛ7ß}ûâíãk;|‘?àMÜái“Ç[á€_„Ùá•”XòœróÁ®üÝËo¿û SK(` %¶äÿ¶ô¶˜cµûÂùr—êÒ=|Qb¼K¥–Pg_cö‡ŒëâxWxã¸(×]ët]n/ÿøêå¿ýâðwo^¿´5uÿöÝomSÄ8ÏöÛj~óÃ7/ßþÃëWïlÜÿ²Ù¢úÛ7_¿üí—ë¿y¡µ¤âÖßÖàw/Þþþå;l¤7ßüðN»¾Ž;`ÑþêÅ¿¿äÊwvƒ¿ÿîåëß½ùGñ l_Û!ÜaWaûDlëx¼C̼ðqO·þ¶®Ù»ýLﯱôÿþí«ß¿zý‹†,³·=ñ7o_}½n‰âÕ~é!î0Z~°éýÜ|{î7MŸ’]ªØ-ÏùÆÞ^æ»w/_÷7‚í}úÛÍvïþö·x’‡×_ŸÞ|Ëýžd ‹õ5¶ð7o~ouË¿UƒËøÎÞŠþþ“ÿë·¯^³ÏéïTS¿üõ7? êoÞ¾ùá»_¾þ×7ÓÏŒ”ÿã˯@¯±>¾>üý¿üWü¢,røÝÛ_¡ü½´¹{ñ껟¿·»_óâõ‹·}+íË_½ú#j^`@» 5ëwîôüò_A×kíÛ‡×|ùÍ›ï^®ß/ß¼xýõáÿ~ñö»wÉx»éAŽÿ?ãi±ò¾ÃËѳ¨ÉÅ“¿§Á¦ê7zñîà/_ýýÒ·ý¹£ö݇ûûíW\ƒoÇ·?|ÿ‡ÃïÞ¼ùfév_µôÞ¿Ö·lÿӸǯuÁë¿m/èúN½Áå@Ó~rwAë§ï€ÊŸrï§ß|óê÷o_|÷‡W_ݺÁúåNV÷cÖ¿û/o¾yõý·ëzÚ|óëoß½úê›—¿ý÷ïß½üöÙ“{xøúˆÜÛø½m~ûo/Þ}õ‡_½ú—·/Þ¾zùÞÝÇ ø×W¯¿ÆÚÿí¯Þ½\_Лo¿£zøí^|÷RñîjùÛ¥Ãô%Hÿ–¸ñÅ{¨>xêñõ¦þoÞ¾øú +$íxýúÅ· ø¿ï_Üϧ[_’ Ž_Oÿ<ýÓ© d”„OáäUÊ|šÇ”3ÊéÈϽJC©(%£¤cD (~:ú£;ÎÇùþåáþ|B±ëøi(¥¨d”„QŠGq(óýÜÛC;·óÔN(¼¥®ç­(¥e•ÔÒôâApåŒ+NhÂÊÔ é×ÇúPO¸]­¹Æê«+åŒQ·RJ‚ôïÊœðôGôT2¤M +ûé1Óq¸ …¦˜Brñ1>àDÓS ÑA[x'xçg÷€—yïÚäŠCOÎ;7?Îx»ð\gH€3z›ÑR2Sþ¶Ÿñ/¿üöËïÐì7;úÍ΢~'ýÎþ‡ÇD)(U¥¡Üëgü>.¿1ë('ýŒßçþ/ý5mªÏËï³~?à72ãQâqWNï)ç÷”‡Q¦õŸ½<>]Òüžâ¬Lã»â·Es’3–GÅ"¹ÇR9å3Í#Þ¦+‹(b)e,¨ŠeuÅuÂ{(xÏK.`á%,¿‚EØ°Xç ëò“¹ë4bµBñ¾Ç2?aÕ?´GL‡ÃšØ »¤`Ï4ìŸ#vÓûê“ä°×^l:æ ;±b“Ücž°[Ž˜‡ðvöw±]Âëx¥]41I›¸ô.š6ë±wt`g³ˆ; š2vš~TnQîóÓñÄÏåå‘‹âìPü9 p6Ó¯ïŒWt®gîo< îÂKøy˜ÎçǬ£ì›ÿ€]¦™Nxçx¯õåñ-É×=n\ûÃd<ò¥Ù6²­Ò÷TÿèX°ïãÑfS‡ÙÍ\±wÙ”U*¡¡y¿ÞKûC<žîí-VÌHÀž~\o¥}¸»Ù¸]ˆ4ªžÝÐçJƒe]ÙtbB×Çúg %ªDMÝ"ÈK¤ÏçÎ>sµ$­&Veˆ4ci= 9i} |Qô,Ë—ø¦õç³ïêju†N|hõ.8èg”•/?í ŽßOº®²™ÃÁ¬áº B›²wøÕrt6$‡·YAV¡tg `(öÒ®=Ο‹é]ï™ýFÖÿï|_{Vð«œñZSÀÿ¨gBužÑ[Jœ'ݳΠ6$°½ª/Hò]c(U&_j+*>Å6³—¸{õî½ãqwd›÷ðÓ“ÞQu)z o¯X×¥Ræ¡bˆ%ëîPÌf Z=m=i:îìj ¼ÈoïåxUØ Ð5~“7-0çë€æõõü4†ƒ7s‡}4_R›©8›êœÅµ¤qÐt$ú$¾ö ÌÒ¤M—¦6M}š¦¡Òµê6Ért”%élêµlS±½LOQ†¨ÜUí*cÓ}W¸©£Iéê3­\CN‹þ\ýÙ´çÓО»2îdZ ]%—þ,åêó$ýù¸èÏÒž¥?›öìí9-Ús]´ç¡?s¥Ø2wS_ÓaYµyY˜mYz§em=.ëÇ/ Ä–C±™Ÿú4Û”žûìÙ|‰Á>‚»91ÞÒœA¢+¦ã^\úŒôZê@ÌuX\þ^v!Úxe–ñ`’d… +q÷{Âc?`,³ä 褦@?ß"3}Ø$se‘Éï³ÉLÏ1Ê|Ø&#ˉ¬&“&Ý&Ü&Û&Ú&ùاWSÛgÕfÔfÓfÒfÑf0N}òªæÍæÌæk±jt©§Ë=éç¡ÿ>ëç,ÖiâYè"iî%bÐl"Fˆa–ߣì½Y"íº³ŒS0Š & *pÞP… ±ÐªãêãÆ-6qyvúe\9ß±¯eëU¾(Æ*üÎV•6öª±çl×­û®[®lï­6ž÷-Ò mSÕ^Ú¦ÜoÊqSN›r¶²i:QªW’;µ¦™<êÇÊ©Ïô¹¯‚DZ2´Nl`Ú)cŸp—ph‡,û#akì7Æv[ôMa3ß.Jí³¼e%§ÝºØp |ú7Ãt~»ÜšïÐMæ±Ì×’Õ6Igî%$Ì]ÐŒŸ$’µZÌ‹V W ××Qgg/7Ÿ»äÜö²³Ô¬!=ßK~.]‚¹¯Œû«ÒžQêeÙY7 wðø×M“àû[ÉF3·æ¹ÖjKÞÚ. E‚E;h8*²þ%,š+'ú2‡ «Hv!:Ÿ+¦+ÒjV/,ègg3Ši±µ lsmÉÆTý·×j¬ù‰Kï +û2Íz-ô-ï9‚À8¡Vèµâ˜t'J@X¥ÅÉòª ­Êe0ZÜç°3aÝ´ mM²í{¬~?©QÙš‚,KMاxýB†|bö°¹#®çý[IÁÍøcƲYÚ<ôË9£çgh7Q|²4Æ2ËFÉí¦ÎÝ4üýDÆóq–¿xÓòŽþË_×¢œè¥QÉ,»ÆðÒñGºH+›“Ñ.ŠÉ” {1vB6âLæ²»à‰;»;oÄÈUŒ²êP˜âä(%RN²šTirå,)[žºlùÐÝæÛ I–ˆ˜Üö¥ šÍ„ÍIò¦Iœ’€gÉ&y’­“±“µgÙ¾ª|“5ì(6«?K)„˜2‰çÏ2¡Éê:$D»yQ?Å>RuJëÅÜäÇ^NV¦r^ÊVé|Ü—:_– ïÔ2·[>9~·¥{?(‹'ÉãQ2¹yB|÷†tˆ¤t+ÀnéîiSŽåþ²˜¢ó¸(µ§>¯÷]᩺Mî3eñšg'÷£f{Ì÷I–›ûIÓÞôØE“Ÿe湡³h)\ë-[ÍeÚ©/«¤MšÀf}Ñ<˜ö+“•Ö…нÀ XF“L[eA˜Æ,-˜ùp6x³Ž9븳ǎ=ø3”éØos\?÷KiK©KÙÎKÞZíÒdÿS‰7JxªŒ}¼0V ;ç°}Þ/³Ÿw¦œ­U{ZåþsºQŽO”ûm™ö.+üƒek-Ù˜ ÍHøãM„^.iû·I¶ÓM#áÖLx¿3^ + d%ñ´ÁWmÑUic4l‹…ªÛ¦ºÓÃ/ ªQx¢Ñl G¹ÑèL{Û`†œãäQò­n¥ñ­<>$rA».O’ÉéÌ£<~/ïy@÷yÚ,Âû\±ƒ£Þ£øG˜ðÝ€¸B®Üú´H¯+,ò|ñ{ÅBÙö<]! –ßçîq\çÞõy¹œÓ2ºþE—%ŽKÃûî€Æ*TíÿU†“lî)!ò£>ÿƒt¨gßáâsÌpX³œÙçŽp /» oöÔÝÙrhŸäÐnÚYh†­K›(†{ˆ‡K;hÃÑ©ý0|ÚÓ3œÚuãÖ&.fî@—S_U‚w‡ ìTû ?Ÿn1ô÷¹Æh~áK/l|ïm$'Fš±{ýœª›!7™ö‹xŒý²ðõq®Ë!c×aÊã%xº¬î|K'JÝy Âê"ðíŽõ ¢ÿÓAï 9„6ØnÊCjð%Ï-˜£M~¤’j Î%ÌëlA„“C\-XqÙB ¶8{¬à§äî®Až:äx—yÇåM|ò‘Øûà såNªXå|³)2OBNÞµZšn0‹,ADôö ^: ‚(H,nÛ æ~ë{Kº*íÂ.†…›l^ÎOjXçæÊ7Ý\™÷°qs9À“=iú´š 9ƒÝ/lÈËÀ_e“7Dœ;ïçßSgº÷ƒ¶Fšèi›]8,¹+ÝG§Ž¡hņt\Èta‘hÝ"1ì—Öˆãæ„m³ ²jƒ4ÑÑcÁ¶¶ˆºØ"Žkô—-vmØ$¶V ¿·#¿Xâ½v‰§YÑ4Ç-šæµ¹"5/‘™{(æ5³Û¼¦+üå颜/ÊõgB™ž@aî˜(;ˆætÒüÓKÒ§ÇÛ±‘rù íp•WOwî²ÞQâœIèAÈUB? ¿¶Cê=sf?N’*C÷c¯þëó¹…BÞBÊ$¶  'ZÄ:Ý0Ù|Ð×=ts˜ïþå,!¨I "1§°ïèÚT3aIµ[Ä Tû°€i©®'¼²â­ù9Š²œ7:Ö@Ï&³uN‹¢uß-ž'í´³ô?SD‡Ý”US]‡½þ¾[A­Hî¨+ÃO–²ÆçMÙ:hÚ¶luô5`uû¹¢ +ŽUHà¦n¸Uü3Ë.Úyz_(ô3J^ŠùqÒ4þñç*¡^ë·«ý¶^»ªáÂ?¾%¦^bü»ôß#D¬,ºf½ñ3¼æeu¦§ {ºøWYê·7(Ë0znü“þé7ðÀaÑ×ÿÇ'ÿ¿ûfZÞÉuH[¸PºÝr«NØþ5B¸/tvwÑd[ÜÕ­Æ÷›áLíÇz«Ä÷”þÈÏmþžn¶#¼=Ž÷&5¸|^Êô'é=å/´ÃAÐ9ÁæŒÍsñ™× Y2*ÏA.=Ë÷XAgcÄ£ø¾ÇáѲ;Ë"¨<ægèx2ˆÐÒ–ø»-8þ¸˜?ºñc5}ÈîÑ­Ó3Í{ÃÇjúXÝú1uˆ™@Ö¸ÍÚ=ùÊ\dÆ¢'ÍEÓÎ^´ä\¬F*zï]n}XŒGçÎßMXŒ1Éã¨pÏ^:’N>Éû™äDeýøMéF·ÉBD7Tj'Žû‡«rÍ»·ÀûÓ´ùãIÌVB}NÙÃ>® È÷5 +üãÊ_d‡=¬©ûÇ +¹:v2ðyð¾‚H܃HÁI8ƒ"b@Ѽ: KÅ°ÜNè=hK`n蹃0 +é-éÎB:mL¤;#éÆF:¬¤q ÷î ¦sG/™M¤cêÔ#ÉV²8Ò†+m ¿4Mç­izÚX§]>w6j£6ÃRmTç¾ÃqúÔÒš:)ŸØ˲÷7¹{¶‰¶¼q§û]纔¨o¥+º 2Ú7‰Þ.홥nË´ÿóG—rY¦ë¯>®üEv¸=ó +‹¹ 1O4æŠÇŒ O@¨iAá{@ÿ9Äí;1ó +¶Û톃YÎåi—À-.^åŧ¼Ò gòÖ|åLžnx“Î_û”;nÀ°]ÝEC– p# Üø´¥ÔMÙ£'ó¦tê0ÜØcáª<Å^ÝZ¦í–îýå2jí²L_<|l™ž¨xºíF™þôKÿ'êp0;÷âT6¹ºZ*Þ… ¦Ì@2zK艅hÿ5ßÍ ãi%†cb⬱O7%å] $sÙk{ͧÃñûÉ’Gçƒ ‡¸x÷_ŽVw¥\„S]~ýqîžzÓÝS?G5}ŽjúÕô9ªésTÓ稦ÏQMŸ£š>G5}ŽjúÕô9ªésTÓ稦çþéðsTÓ稦ÏQMQMÛh¦mD“Hvc*=aÞ6h ïdêÉ+O#ëá.ÙŸð™>îò«Ï™#¼¿R¨çÄ0ŵ;¦¸¸W ·@Ì]Ûd ÷ »5!™5iÎTx„@­‡EÌX\IØ<ƒ —¾|ÚòsÔOÏxÒ¢;k>š(0~´L]Ï8~Æ¿ö6nêkú¬³NòÇõ[ÿZc±ê&ëÜ­~†õ4;Ì@{¸E#eûhÜ8ËÉM]¦(=(é:"+(:)wsÞ±‹=*Ëˈ·É9õ°¬óÎÚ$ÂÜÆg­ıë:¢ÜÒbš(Qèäf8K;¸†bPÏ óºA7æ”ͶÄJ £êì6Œª{“ê€ÓŽp/ƒÓnƒ½d1,ÚKvR{¡UrØYòWèX{J\&k+[…²ç1„}.Ö„©4àÞwù)ózß·ô7À·>ÏÏí¶À¶›é(?XnàÞ‚ýÉÈ´ic!»QžN{*0íœ.ÃÒ– ´1šžÄ­£5-_%‰w] ‘h§©ãˆîðК*>.à · €dÏuÂxYD§.¯ôì©=kê°¼׸·‡MÜÛ%¦ãªã©È·tú@äÛŒ}›±ï‚ßs復ghBôû © 鮥OQׇðiêjºÃ,ûŸ@@݉½€I #412šÓQÞyÊnŽx¡EÃy¢0¶¯n±Yr†›±±unËv633ºMF§³]\Ýe¸|o@ÝOjXçam7=¬íçÚ¤Ýú+OK9/fì®,ƺ}Zù¡Ó»mGš˜ë(ôDï#Ýû(©çXKÙ”ÚóÿuÒ?F`Kí?÷=Qü}7Õ¯æW3ߟú¿N=•üi8d‡<|ˆOïìÖÏ´œ!³æX?VN›²d° +ÛƒrÖƒuò´ɳ¾Ñ‘ñy WYcÒÌ+8¤°áþËË!kÝÜzR.oã[eqch¢–sgZ‡,ç~âŒëgÍœèp§Š¦ÙŠK—CC BHÖÄ#CLX(!Í:-ä¤ÃbȈB?æÜžì{« BNn*àâ\A°#dÀ<ºN,ØQ{Ê`E®D;ã-4h•~Î`«ýH¿]b§ˆð=WVîâÝñw#?x¹Ý‹ÛôÉŸr’²ó`ÅT³“s:߯g&“aÖkg™¿·Ç´Å~°á¾sôtá^/¾‘pw±õ ÇðQüßß^ß#žvÔwºÅ™®€ÒKÐèº4‡ÛmŽ›“‹Q¶åc+FQ`P›ik–¸—O³òñù²ì¥©^$gMûC‰6Ø‚ërºQŽ—eºþêÇc96Á<¯ì¬ †C0FêÜO(K:hõ¡‹UqQiÂE‘XA^þý÷~ˆ ìw—=.g;çE˜Ð dzÏÆuÄs?€ìa9ƒ®H°P*‡°È´*XhÒ8‡ÌL #Öˆ¿aFF„{Ý„ ¤¥cç kå–ãW:`kî—sçÆI‹¹Ÿ=gÅ.ûá‡KÞá”~½=œ§u/ÙHh´=«pkÕ¹BxÛžôáÓ6æör™máY²ñq±ÒZƬº˜Íl˜|Ý}Áo=EV‡XrFüý8ˆˆÞß]·Y®Û‘ëÔ ¯n|oÇ»Èä: €ä‚‰44äÿxßM®Ú(Ûãˆg³ó®ÒL¦dr¿9òjÍkq©&­€»Ëˆ­öi!/eC€V-èò³×~n”i9QëVùDþ9°ù{¹ÉìBنų_68[³ÇÏþ£¬òó¶pÛ¸í[‹è'˜Ÿ7žþy9L|õøÇ™4p¸&p—Òqê²¹a’v˜Ü¹ÇJùžäa sB7u…`(!R¦×ÝÕp\r"¬‰V@Ìš¼o{†)Öï´h/~W®\ͤEÃœ6jÓZžÿ©—eºþêÇÑ”Mš s·¼ï°±§sš–M>tG:­ÇŽƒÇêæ0»íQvÛƒì´c·*Ǫt,jÇrÝö(»íav»ãì&©"ë™vçáàëŠÉ8~oºJ•–”ˆã¨x;î®NýÌ»qnü8;~œ€wêçà²*ƒÛ\íqZŽšß³\«¨—ûuºŒÝûqÅŒùÛx¡ë  Ë`œKùo+ÞÛ„—bàj¦Úš¯.DÂa— §¢á ñð¶`xKœ~¼8¸”û[eZþyÅ{n–~¦÷Öþ 4aú(‚R¶y˜¡¥wÝ4têç¦S7·Cü -Ø ò¾WyÝÏÝß>2r‚îQioâ)_A{ ƒ¿À†Bé‚í1?àmÑM’=ô•³:É®ÕPÀ>©ãÊøµÿ)a¾ŽWaSCÕÉYÇt•<{tAÎä}•¿Ñû‚Bµ¯­ !Zâ¾Ýye÷Eš>ó!øp%¦r«gãBo¡4Ÿß.Zf³‡Óù©5¯ÓÊf 2eG:›]mæx¯Åz$ÎCyýÎkÂé WcÌmx7﯎ûé é£,=!ݲôà[t¼xzžÄ†Œ­¸ˆ­›ÄhŒ Ûƒd ‚aÄ"I)7·ó©Ÿ ^:5rãÐÐíç6$k”¨@\#etñõÅ+¸{wꤑ®÷ÿúžÃ_uñæHùõPù܃ÀÖÃå½p2Š³Cæ÷4ï²¥àR¬Ôz5íÐ[J˜¡ñ¤4gˆÍäV”bÇtpÊåyó9ï‹««¹ä»t~sòÝÏ»ÚÙs)7ì—£ˆc„âA+'¹™á^žŽÕ +iÝÍØÚ.Ž`¤7uKxEQ÷îÔMÎÚr´yìO7ˆÚÃ7ù‹;_mgúŠ†}Žî5b˜6q°kìþ€è5þõGDO7#Æ®]·?åˆèé}ð>'žtk¸*ܦaË«K·öWº#=Î+&DŽÒ%z´¿ø¹£æœ& LKÄh^œ»µÏI³Ñ>7롾v¬ó8Ôw¸x½Å|NBaçàYä2µÈÍö¼ˆÌù‚öoßÑe4¦ÞØš'n‰y‡¹Áñ—k>»x© ËisYç}œåþ¯ûe +Æïû)€rXO÷ÇÓøTDã.ž‘ž‘i籌{ÇÈŸõ‰Œƒ¸Ër·Ò + ì?*³ÐíÜë?›Þ—yý9QÛû/c÷=ºkZªl£ßw*ó6ðòLf£iÀÞ¯’«ìR«\äU^ﹿô5ŸÊqZ¼Öu>ÚýQÏ‹«»ßÅÕí£êö1uKžÛ[ñtûhº}$ÝC·  ë¡sÓ.rn17båör» ¸‹08|¦MÈÛ>¨m™v ¸¨ŸªÿTÀ½øiîÿqRÁM.¾½’ Ü ¹`àaLëi¹`'‡y¿T¶ÁþÌŽ¹s”B•ŒùþH‘mÍFìˆ1Ä­ør¾lÏåqÉ÷¶\Î]ä™$â¼ÏöºÏåÚȦ¶y$NËñý+Þÿçâ3;.s.Óã2û\XûQ<• kÐï1–=×{d ì:¤§ãøÏc\â-eÇ[Öxó}´ùknc›–Hó}œùœåÏG·<ôÞ¹åȓăá?b¥òÐ4 gpÏ̃è[ci˜ÍrR.ÎK¹a›¢G”c‹MOP®O6‚£]þ&íò´J¬~vàK‹‡Ì’õC;¤|-µå`‚±Ñ6L¶g†ïrQ—ŠÌ:º‰5ˆóÖöKÂ00€pMÎó8½ª6[ÏŒ_Ù]~+{±ßünÔú~²µ¹¸i[mÁ‘m#"2@rH‡kBÚÐåBJ…›'—4øСµc:ýÔ%ÀüwêÍš-oÝ‘Rg{qÍ——zJMKªÉwÚæùèrÜÿÿ½ö Oä°OûÄÁ ½¤µLK!Š¿nùŸTKâ8ä*|ÚÆï[ØûÊÊö‰©Œý6<˜ð¥Û|?üTÔÓ/¬Q=pï~h"ŽÖ…ì ù´)U°F¢cñ0Qr-$WV¡te3ñOÉA­Þ§o­SŸrÝZÕB¨ G«Y!D'C­Ø;xo’ë°}1õPe$õÚ¬»Ü‰Ã¢ýÌo].¯÷Ïþ_„­±ê“aXì"Vj¨ Bn’¬›ó<û +¾‚o,9a,ù|íS°·Œq +®äŸ2fTÊÎ^÷‰†0VA ó\ âÿ²~¢S¼6ÙÿÁse%-‰Ê3-6¥¹hÅ~Å_Ú>†Ïq‘Œn¡Ýç–òSÏGñý›Îˆô9¯ßç¼~Ÿóú}Îë÷9¯ßç¼~Ÿóú}Îë÷9¯ßç¼~Ÿóú}Îë÷9¯ßç¼~ÏÿüÒáç¼~Ÿóú}Îë÷9¯ßç¼~Ÿóú}Îë÷9¯ßç¼~Ÿóú}Îë÷çÁÔ¥|WÐ×'Åõ1|Îì÷9³ß'Èì—n¢B“ÛÅ{mÆ}¨<ÿŸÍ^œú?®ÄMYOY.›¥’;¬¢ô¼“¶0çSO?y¿8¸W·9¸;íØ͵éfü˜ÿ:v™­0¡RíñJ÷‹÷ù´ 4–¹¥·Ø‡ãM´FßãÓ.=Ì~VG² ¦9=bƒR”æ4lò‰¬éƒŽÓ>Díj“ç>«—ÑgôfSç§èðù¬º®µÕžò‚(Yë§Û!“fÆ{xvyú³ñ4M7ÜM·ýIk>²³ j·#N?Þ”¸;”º-VÄnAœnL=o|ôÛ|~÷KÕ¥«~cõ®¼õµë`C ë‘m‹v[[t±icàÝëbOD¹™.fóùøìò¬™žn:Ÿ?»Ws;m'w7³›¹}ÖÌÖÛŽÜÌbn§gâ0â³84ì¡c¯‘‹šÛ©Oí6|ñznãåÜj>w.ã÷ÿdÙ»Ó}Ì·–Áå¼o#ç®e·×y~zŽÇüj†§‹ Ç§“jn¸¦Ïì´Û´–€sØL.­&OíØÓ6uº0ž\¥Þ´ž\äŒaRHèjÍ‹&S Zì ÄÇ ˜þàf†ÂHRÁPQ +Ô_è§.· $õöCå.^e_ízñSqƒ?ñH·vÞ=­ÛjœqºhQ©hsâÐ éJs W™lêžd4¹NEË›—p7‡P¶åO8†Óšo¦XÀ·?Bkþ –|¡"û醆ü!ýxâ¼TŽ§ÊÐP…ÂÓºñmÕx uöY«åtØ$TÚ±Ûžóê,Œó$„ÅcO¸ŠÎ[ý¸+B×ò.¦¦«ÈSGïuäá5›Làkð²IÈwjççèÒÏÒŸ·jó´Ñšë ­ùÔµæ+y«0oÕ¨é=sY–JÛiÌc‰¬ +óF]ž6‹d¯1—e¡Ü+éê5öæÒ~¢2íÐ7çž’Õ´fßµæ±@†Ö\{Ãyت͓˪c„ kðÕƒ¬è·B°Ê²hº]åO×°/,\[ezÚÕp¥KÿßФŸR£§E~¼Ð£Í±º½êÑE¨ï½}¿Îû¤I?Ó…Òõh¿‹]gY]çš~ƒºú\Íu²¤Êe£IÛL?ô™všé5›j–ÏbÌò.àa¤K:I°¬¼y1›Ù÷™^\¦6Ëaq®Y;×YþTJè_t‡Ö°ýΛ¹U±+‘ô·uì¡e˳ ç1Šø°ñ<["xž£Pû¾TͦºÙ‡ôîø>üÎt%·?Kç~ZxŸ6‰KVÍìfÞ’ÿ¤j6-ù§õîv¶Õ¼w‚ü.¿Ì¥'ôJûîžÐ àž££?oîw6—Ûúøuñ+E¼Ï÷ôM|ÑÓ6X­=NëÆlO]í–&¾Ó×z(ø-…m™ïéFLÄå|_jãÃÒrKqÛø½÷ºÛs|ßW3þÍýƒšú¥š>½WG†þ”z>݈TðOF*<=ëËœO—[üƌߚï65›íéj{çgyÙ 'è2½WK¿mW{å°×çyóŽB¥ÍIó­-âôÏ ý›êb‚úˆ»êÇåÃ×R pÒ©zR3^¹bãÖí:ç'4úŸÌˆ>‘NÏÄ£ñëôË>J§¯·TúJ>]ÄßþLW±Fqw–Èý‚ÙX;ds‹mÌâºmxZÏ Ù bÿÝA =æyÚÇ8ãp9´cº:•c Ý1ž7åÉÐÚ2¦ÍË_•p£¬±ûÒsÿ¸(ÏOçqÁôÌS ž*î³³þ§êpÝ·–×#ËÙÎ˳O½”~Ä0öhîjþY3vøν¶â#ÍP“¬Ñ#ñÆC‡äÄ´qZÌKa±»É`÷y‰Þ£9ùq Ĥ!¹jÒOK ¦Ó¡Li1ßë|¥~ŠËr~K¬ÝOm™–ƒ#í¬;¡Å+dc ÇÌzEm‰Ø8.®,ǬôðL7õƒT–£Q¯™³™{(Gé^^³*ôóò–ƒ\†ÖÓcv§%Zpõe„¥ÄMÙjÀ;º°×˜oÄÞŽ¦}ßç*d÷vy¾ã~S.l OBŸ]¦ïâ§Úášb|®Ê­w|ºY–ýÓ&Wÿ6qî­ò¡Ãâ>Ý:2qW.³Õ| L›pù)ÉãþŒå9ÆS¦×üs‡?é·Ù^·9]ng›ý@6—žÊå"ãìUv×ݱ¯©çs –­]kºiÈÜš2·ÆëýaCÇ‹Ó_Ë0e_þ:ŽÝþuóä¯ËS¿î§«£_oøõ¼3¿dY›n»w£Ì·Ë lÓ$ïÖQ±·ËÅѱÓå?²\46Ý<}ì#Ê_p‡cßr×6ìØ„ýê”iõˆUÊ «A¹U¹3ïñ¶¹!ÃÙi#ž´‹6žïòžh<ž´½Ìb<ëdÞÛÉb6­}¨Û²rzV˜Ûj^Àfî.ž6b§«ÐÄ50ñá*0ñ241õ{kt¢›6!Š=ŒI²§%Xñh¢oΡŽ“0Ç™½ï)ŽOXÊ +ØÙÂé·'îîsËî¼S8_•Ûg >èî×2mÿ¸(íO)ÓÅ7õøS¦ïâ§ÚáêÙ˜{q*?úK;*Æ”x&uóEɸ¹¸#ôä´aB ku¡äà¡JGÊSIp÷XB¦?½pDè£|Ãò)qü~rŠ0͇¾–³œnÔŒöw9¡"»;v!Þe—âîºÛ-ö×ôš|>øù.øVn\Ùb¹¾”‹Ì­—_œS#Üôj„Ý¡Ÿàêÿ½ï {JGq—üÜ)~¹Þ,G¯!¦ª@Iç—‡žòemöÐ3 zb ‘ð'/icw3è‚yä‚ù3§ÚèÑ6×fãƒÀè˜áúÌCªž™ +Üâзù>ôL]ãøÞqpoêÇõ®&¢Õ@´MéáN–²þf®÷Ìâ¯Ë…Ƕ5Ìck)ä +&Lç_ \X>CìÍâµÑ )ëtˆ»€in£+Ôó“Ù¹±I ±«—öSŽÂ¢Û“›KK…aÕn¶=ÞÞ …y™õUº +/×Ù¨Ÿ8eáêܾÿŽ÷|‘Â_þݛ׿~ûêõ»W¯ÿÅÚµ­˜þî;Ö«ùõ‹wï^¾}½!jý› Aß¼«<]ÔÓóN;þÁÕöBŸ(L‚§„š¬Åìss>À‘¬…s)´•¤à^¿ÿéßð¿ôO×óüÓ¿ëÏÿ ÿü¯øòßÀs{øçÿ2¾¶K~ƒÿ]âò‡oÑêƒ#9üêV««'úÕ­;>¯ÕÍ;¾ÆÏ_ß¿}w~õÕ»Wo^¿xûï‡_ˆýõñÍ›o?»ÿeÝ_>|ýêÝ›·__|õß0m_þîÕ7/¿üÍ˯Þýüð¿á‚ÿ?ýýý{‰¯Õò*äïPf¦šà^š!¸1´àf¹ü±˜B#÷—dÏ•]õ‘r?ØŒSvÿ«|äá*‡»_²1pi×Ã?½Àâ\˜Ì 8—K;´|ðOÍ ++1ú¼±8\ jOá €å9ºOôq£Uäl…É`7Xˆsï¾ú)ÎF™ž’Kk1ð1°¯›Ý(æÞ}º£·ºÍ b dzlˆÔ\su±‚¬Ð 6íöƒ• Öðúý]˜y[¨ÚX{ã{õš¢6aÚŽƒî×`x(–mâÃP‡à:õôúÝl®<'wæüZ÷×ÊîEªŠ•H¼±±¹JÝXî©Ý•”8)˜Î +ŸS³rQaão¸Ü߸ +­¥Æ»DîptCîÌ©EÒpÛˆb ø;žãШŸ”±fØŠ/­ aY+Há­ +/S žsË<®Ü1·ˆè M* +VœUF¿Ôl-°×›DÚå>sŽ6¥dkSyŠÚ€&õ‚eÅjrýJ½na« s‚f ó€—oôCò ÎñÌ•@Rñæ@6—Š[‹wÔ¡ûBÍ5Š#à.XS…„[jÃ2ƒÎqà^Ê\+ØKXYd*y4À® Üq#Eã8 ’U¬Ác¯‡˜mÝ`0Áxkœ'˜.nðG=+Z¹¤Ê({ºÖ÷ ÷¼*¥WåBy +?Çáò4´`R7µðÜ3 +—ô®¿K´"—@+×ZT+í´™hêÇ õ<ùÌZ0)SËj*”Ø {oC&êÔ&¤ï\HZ­ +·hÌólp0¬|§å¥ÃJ9Ü'Çf †ÚÁ¥š±¸k +šö×-t§ +‰ïû3”z«l«;BMœé̬@®PL’ˆ2›ˆ•;H"ÊÜY0+A¼YY˜›”í*>+HÙ$ +áÕž ØÝ¿JV<³ãÊáQØ+x¤ôì¦`ž³BrJકfhºâ•d&¼?”)Š™²ø²kžR™Ë¹Ùæ!AE†tj†>“k¸AiƒÍ¨Žq}¨ËŒ¨e]*3¸%J0?µ¨¡èêĉC (ˆŽ-¸òÕ¢ÕÀðdS- b?òܼú²V[͉­Ø/[1=Ô- òNÔ5³µðT.#6`VMÜd—Én„Q°žÜÛšɆðŒ6k0]x1œ¢T´õ@°'!£âp÷ŠTAŒâ$F훇('EW³¨"ÏXG¸B¢æ«7ŠÜ•U’ %4J2KÝÔ»™J4DLæH»%Ü-(¹sÒðº½ÇÜ@.’À„•Ýƒä á®‡*¥ TRPÁJ9â¹Ô‚[Æ£H + $³ïtf§¦íRˆ[Ž´Tj½A èWÙ‰FX‡™ 1dЧn<4z6¨3_$[hÉ¡% ¶àL ^ +™6oä(©áFJú0>È=<<É$gRŒ +¾…·)¹`˜æ,2o\QÓp•)Òræ?´GN}OAä“Áˆ9•îà!ôApµM„ÊöÏJL¯âbDd¾Æ +lˆ¬ + +Ïø;Çμùã” qæ{A%èª5ž A¬4‹dû,ÔoV}ð IõRæ(fWL¾+”¯@vA”±Ò±M(<ÞÑörˆ1™\wŒ×í— +õ lz({iûU3ÍÄ~¦ïxÚ$ôFBx¯ØcT7ð:ž<`§."-+ÁU‰ùCeNàNºJ[ +`9(&4;9ü;®÷^Ê]•jñ+Ñ 9…d”ü+¥_Ù£¢QøQEì3³»þ1•´Û,ÜZ=4ªZÔʱGÁx^wvqã"§¢C…”:$¼4?wMm& Á‚n…Jê qS ŽÐ8!và4®mŒŽoîª")•_'jW•Ø£•cp4dc¥aBŽ +L+¨U1¦žM¦ài6:¼q"¢Ÿ$£ßë +H‘Ù­»‹JÌ%Dp‘ju⢭,!z +¯Ü +w¤øÆ®ÐK$;@/Xû´yô ¼¦í‡>30ã­´LÃW#ûÓ+Sw/ã©Ô9Èì0―«Á+)hÒ\Éâî2•ÿˆg•‰…šó~r°”“Ô*©´2û£N[ÄU"Û ö¨/!3Ànæ´ÑïŠÂÌ—¸ „èLˆµ‘W<+X+ÏJȘv#–- |Ï­oXT”ÙþÚ&¿£ôÁÆàèY·Ð#±‚t·Š­{VÌ!ô +çTê‹îhrpö@Êhõñ(à +YKbV<åZ.f\CˉRñÑÊMf6$H0yì^Ü Ò,“¥@ˆÆãa£’Û%Zȸ> 8-†Ø½ ²¸‡ô½Êú¤³ 4”‰{”Âu`ÅB…Y騭—ª‘Ì$Ij˜t ”T<ä1I¿ÁªÊ|`Ùžœ§ÖÞ]î1Ðr¤NˆcÏt%̺5Å:U@êH¼mibˉ.Oë³Æ¨#”w¦~&V„ç¤wÎÉçN,PÝB̓ÞëïtãŸyЧX¨#§g/¾©‚$+/ 2°Q9UqlŽÚeOÖ¥‚-J±ßQ$ÀÌrS¨j:+ÀýŠ* (ZuVÐW ×Ö5]UÎ"nh­1߃Ÿ‹ê¥Þ= ±‚©!RÚÖ©“q²+t+-;É6|tì(¾'<(n™¨¯`lWe–"1Þ×®¢dôu¢5^ DßB &a¸ Óiô JbÌw f~ÝB7p ËÎ+_ùÀu?3V,­·Ð @i•Õ»€æPH½T%A,ñ$P”ðœŽÖ«ælQ’Ôðr\ bîèo§VV™ª„´‹»3Ý«fÉAgÌ9Óó†Èð`2Š’ ¹\·ÐaÈ™¤9Þè&P1YÄöøT;º†¯+ +¾.kaW‰‰h”\ĨHS”màŽjÿoÒF +ʼ|¯Þ %¨wxÊè¹^43e3S8c+íö$ëõFE¢Õ×î.+ñŽiH4:·ˆøLª‘È5uÊ8_ ˆÁ¨3O{gžŠãæ*°Ï¬Ó´[á“€Ü20*¨áÒzSL‡NœÒX©¬Ô>¥ÍË„ +sƒP…6 u+H³¨N+ ™ï"3@•[ +)Ý~£;y†G»¸[k–NËâ[Û^¡.HnÃ#ÉÇ8$áƒÈ©†MÕ!‰öãýÐ\•U’¦gÚ‘LÍ"­&×'­Î4YÖn‚Hôgs4 C²Y*¾¦[šÅ1ÇhØ–JZ|èÈ€àY:ks%¨××[ò{U™1óЈp4lúd ƒõ@èÒ! +4œA×ÂÜQ$§^@#q*ýƒf¦aÙEÛ@e4R¬(TO EaqÊÓH#Xn¦pWŠÜTiA¤JéÚcE(I?Ýà4”z£$Í8“gPößÿQ*âl¤‡8snú56 +ž´½Q´¦7Œ2$5Qº%)®:LÑSëŽiÖ‘–6eþjVGÍ_ÔkY >w’dÑ*hPEwC³2ÜÔ¸z8P9*Iªÿ ÌœFš:V¸$5+ȱÂQž¶î ™5>¾¢«"­U¬È² w€ +:ù}à$4ÚIAÈ2ÍãclA•Ê—È€ä.t“¨®âÄÙ*˜»u|°=™·6%(‡úŽvþM~H6nSE¦Jã7OÔöé?OݨÇJ)6<ÁžK’øX¬;ÑV`˜¬h„ ¨;¹EÐÇ æâÎ|¦& ±}Fr_ŠxF,ÆèôÄb¸U83ŒÛ8Èp°5ÐiFŒOÝÞâ í àóÔi+­6ôôˆÆFb 4aWSi/¿‡~9•fÓÛ×9Ž•§WdOùSgÐÑ@ß,®HZãVÝ¡šl>$z¶âi&¤Q‰˜ š¼SFZFèVó¬éNØG37*©F£äK(Ìlè‘&K^+t9´hFW+!@r3¶lŠ;YÁw²ù¢HbR#ù°3 ´PÍÙ"Ð Ä¡P¦W a2"Þc®jef–9.Ò7Ã5ùßAÔc[î”dçΊ®Çérɨˆâ˜xC2`ò +.^A +°0‹L´hX¿ qæ8h.=¨À/‡Yñ""Mì"^s“óû.Š}˜ÔH©†÷ÉŒ©ÈjÐfzMðžJ–ت–Qtuz‰ö(Z3p9DØ ´<±ž˜†ûÙœ‡h—mô“eºÞÒ‰­ !©•£@“†Ö:!×׊S?¾ÐOÆå–ì ¤`W:€¼="(‡.åË]¦@"H%)j|ªµ|ÓÁÜÛNš@"p+4-Ö^1¼D´5ž8 àQ¯lžÙd²æ‹Z¾hâaà€”ñøNÇñ·¼fA9–6™ ‘›Íœ.ö'¸?E‹¥Ïa®ú}{›©–bJ=9 6Ó†@òEÅŒ 3{«gîYi#£]¢2%£«6D‰˜ã;.Þêg–+K3•8Ÿ0ä$dê¼D¯ÕU Ê +¸5F‹Ç©mñ}+ÂŽí›h´…^(Á°ê¸›v ûNfÁÙln5Ðé›5CŠ9C-w'[U"NÖ™M£÷4&ÚS²ˆâ®¦Uz®­qR|0 6ˆšSÒ¦Ú[¤³ HíË}Œ™ã²@Ù–£!*’¸˜H7[PÍ©DÒS<ÂwJÿO¶4: Y%qaI0ÇuÕÈ–@\ P°uÍ,ÑwÃ9‹˜{*‰j—×Ú3;-µœ°|o;úB>Y.Å ¢‚Çyz£èí¬”'.+lZcŸÖ±Ï®ZQ;h|–™2Å>ªÒG’Ùs¦”ÖŒRd,†XD9 KÑÝhñUwüËZ2}¥éF?|AD&àÍâ~I¯UþU½jˆ¥ô¯Ê– ~’••ÃÔ%»åF¦KFË÷P¤ÓB^Ì>–Òu#Ýèx/9 ñº•'á…ÐYÍnÅ “kà@y!’77©ऄ„ ©{^<*hUµˆSƒÊÖ +‚-[99¶ÐTEÄÃ’aÄØ_¯Z0ZЭ DŽ€BpÜÉxX"ñ·Vxw‘­èiU?I'Û24AP–Ü¡y(”݉:ÇB½@-(ÓdBÉs4R3×[¥]…¼/:Mx_ êN+D6?¼Gc+ÝÞÕg +â ÕT°®ZdÇ„çÄfÆŒgºjÝ€{”H-ª5¢št MºH¼;I.v.E+šÄeË_uQÆW¢ Ñ;ZF¯úÁªÍ´Å4®²ðRîh5à ‚ÐsÝb¸Åëº =”Hi©"ȇxp:‚φì 59Oo½Ì$P* ñe‹…^i‹)q´P^÷CÃg% Â<ðÖ§cê’AˆnúPC‰»D:™ìuG{Ýn¸¢åLMjEŽ +Ñ\$vŒRÚµØ@àáB}Ú©…ךç$sEgªEJ¶ Yq¹‘‘Ýaw/²èdbˆ§P?$.lሠ+%bf¦<– +‘åjÀ¨Ó*œJ3LÍ\ðØ…ï/¾»Ý5¼USJj¤œIlg —ê%M¤„(—M+Øœ(g]·pý­4¹‹íF—­è bABãñŠÁdtBXi”%H™f(üMäíž3ú63‘ž lÙE™Ú ú+ªà²‡ý؈d|bÒwéL ‚” ŠÇ!$,! ØW‹PÖûJGhEL¹µ²šÞl¸± sAÏ­)·ŽØú³éÊcTz!°ÉÓpñC¼ñ-r¯b‰xÅh€„áJUXÐÏ<\ÀŠãI™ÂÞwjŠXHYc½g…-J†‚z/×2:è =S’šA$èî ꈘiµö¤ž™€]ѹ’ó^ªÖÎ ¦ù~ +Šƒõ‘íNQ™f÷( ¹}š†b Ø¨o)ž`U`Ü ´2È!K’Ô\03ì&rûäÅÏ'œj&‘¢P !‚Gf¡GTA]ƒŒÆ gp–Á³Ð$Êð¶Xç!•©• ö¨ê™‰b—ËDä&·ºµ £d°šê¡äêS¯'f—ÝÏÃýGeÌ4-,2J\ 06f¤50ï2,‚¤;‹NtƒlAø +Oü&XA›{,C²ópñFáÐ9êDÝF:x>EbùidvhtŸzS°ñz“¼ò)t\d3‚v¡có¤öþ‚²¹ŠA,"Í°ž2(Är¦À¦å›€ð—uv õôþ*pG$û¨ã“[8Ê»Êý±}¸\;>œvG¢ÑN…Œ‡þv½jBüÁ)õV¢Þ¹þ(¸…¢5&–‘ŒŠ-¡?„‚D¸c*ÙŠ,Ž­2írlþWØ*›û»vÀ‘£0c h3cƒœ8êÖ £˜É[y£gÝÈ §4ÝøÑÑÌJu­ÿIÝ·:»Ó]ˆ!sñ ážh€é.½!)ì‚hQ¶Xœ&Ì<øc´5ɽå$Љ%ÒîIt’@€21þ‰QwT$‰{—drÆËc¬…¢m¨°Ç2¨† åõr§G)ÿ<dˆ<Ü«h÷Ì4œ …AIq'¬uÎ#±¥™Nø¬p<îĈž4ë^v'¦w'OcßÄ¢x‰¬xqëä8m¼S¥[ ´;£EâJJZy™^$èÜJqà WaÌ ('* †S–ÕÜJ^)l@Ògbè£L{¹§ÒIÚ‰EÈ]¦µ§Ï“ƃ—bubþš™% vitë‚ú ×ëg"‘cªhNG,o&¢7𤡳`‚ÜørB³•â¶¨PéYÌÚ-,¡ÐŽ¾‰é³-HÖ"(–¦§âÉj†ÞÚ]ô˜Xêaà¯|3a$‹cÖ>–&I’¨5,€µRÔ@Fv‚Ëß:[ØÅe†™ÍÅ 6ÓŽˆpæZÌ”“øÖégåŠ \çæ³£Óp;ÓÊChm³(ŠƒN3Úk]3gIEóI´Q¤§½hÞÄr `"3ED5óÑBŠÍfF›[Qt4·’v¨IPÚqŠ#ò„†䎺(E—‰ÔÆLNhãºÌ$ÆJ©Š§zaÌ&Zx¡ß?$¾ AðFš†Ÿ}ÁÀ|0aÿóÃ_ÿöÝÛW¯øÙñxÿÕW?|û›7ï^°í.?ƒÅêÌÎI^atHmÂðÖŽ1ID®rR1@êuZïøŽ)‹ØËýóS4<25Äk‚éH—4*¥žæ~úÐ`øÊ'ÖÕéϸ„i“)6 +ßäÏ ü:)µð?¨\ßöVR™ˆ)DØJQddL›l;o`%eoªmƒ´Ý$ž›ñ'ÖF«Ð\çd>ghê"47âÄZ·¥JVsÁ2³[Në‡Ø+öCm”-<ù35fÆC„$É¡8¥Ü`ÌöbCÊй’Ô្²ž &ŠEï>M0tÂÓµ.çå\*ä±ÌV"@y%ñn„jç*`NðtÃI°TÕ ìù2tpœUbáÁmž¶+èIöŽÆJÉx´!¶%ý¶b´žµ¢‰‹¨ò¬°¾$ø[€‡µPwâ»­N-È7Ø¢ÔÅtÙ°…ŸólG}G­¨„³T[ÉœÉw8`&%F‰¢¨¾U¦!Ø ä®œÉdWl" wBý%„„ç)J‚ÑD$† +þCc6šÀ¬iÃnèÕc7Sˆšõæd£„B#èâ¼ñ¹[ú<Ùtdd3GCL#ÒU .³½RÔ-r@И»ï´¨•@¥OaQ–ÊîI™M$FŽ0šî⺡UE~‡î‘aöš¢~3Ëlš³€(L@ñ²PÆuâ¯ÌÇIþJlÓâ³ Y–°Èx-óÉyKCmÍTp¸ÎãGœC)f!'ÑÀA™j5a3Ñw¥F>¬¸4ø=Ýbt‘²Qž“iPÞË<¢ƒì)J77ÝäÜŒ´>­ßä¢È’so,òXQâŸM‚Œ4ª(ßÓ€{ûFÙZ³`3Œ`°|4ðFù‹-e…@9[Bw:¦Blòñ™ Ý5ëõ¨Ù) dÞºKyþI‚‹=Ÿ‘£Ûž;ùÀ±Ó'qb,[K¡ºÁpßbå-*Y.ZxIF4Õd†ð \§W<åH)bÆðÜF8`f¾k^ôs3ȇ¨Ü#7¢W´¸ÌÆÌXàQðGæÃÔxþ0aèžX&åÔA†·ËEæ˜v„ð0²>®:åzao˜eO¦ínÙÁ³-\Th0`¥)h ý2a%ñ~¬¨„„ð…+Šš0tföbˆæDý‚²“uº=­|ÍD2†‰Qi„WtölÑœ9™X]Õó|X¦ùs*;l£Ñqî»Z‚©GRzïhŒdŠ.§/Ý@ÃJÁâ”Þ¦‚.+|S¢³;™‰XÑlt3Í[\Ücµq¹4ÉÌt‘XA•¡®ÇŠÆ„o÷(#ˆGŒ iõbͤÁE1þÒ_·Ö+e c}8B 9Qf€ ûK£ ¶2щ±%$ˆÂ×’,ÉvF¤+•S£‡¥g¦<òºJÁ¿¨˜)—³¢r†X! k™RÀ]yí®ÛB9({UҚŠ+EÓßlØ«Â2 𠦊$1;ˆÑ,t§ÒW(¦ÊœÑè9gFlUæÚ$ ™jÒ»êB®m¡T»iûðæ—aþ& ©´’Ó¢I¥Ÿy#aé­XÃo¦´ˆl_У-‹©ÌˆLªŠ;U"@ö eÖMŒg§F`T d]Æ8 „¤'4H‘L̹#áÉΘܠòˆ{Ù îh5Xð˜òg£Â[" 9yp&«Òç ÊÀL{ÄrÑO†Ýõ°æÔ  —P ¨Û´—âMÞõ89­@¬%Áᘴ4Ó[Ö¸&¨â¶F–äöQYóp$/SÚÔƒÒáTCf(òÍlÑ•(W²žNÐ\ÏòÀH}è.h%F îã:Ër˜¹Yy|Ï€h+ÎŒüˆKÜ8]€Ù ï <Š-pô]’ëšH‚›PówJ–-‘…cªËLƒÕº‚ËJ&óc%!jK%¨Ix†„Ã4 +¿äzg…Ô0Ì e{…‚„–'¹ðA•ÌÉÃx}å +Pw²S€« Ê–7•æ¨î㋦êW¤g9×íM¬×8W‰»#Ü*2†CÈ'|o©&” bø²5ÜÈsA‰h +¾K‚Úy¹+©n¯$Ó)÷¡ÒÜÍ©œif¨BŽx×á³ †É\leô@M¦wbvÚÉ9ç@„-Ø$“¸ žAc(²ÁÊ5Jñ†)0¥I.ÍXEjˆk<©â¡ñ¤LJÈÜmNJ&R&@šÓ`š¨‡Ð³6 ÖCËŒbÿš€%Æ1-±¨ŠÓácgØ,îÀB`˜s«°2JA+³¡¿›¬ÊäùœÍ®Ü”J4iKjŠ}XÄ£Ï&Sæ,Ù5±ykè9™Å0¨âôJf‰¤D@6"N“%‹ ÜÅÁ -„iÐ< R¨cŽ +f}²^ª˜×T(o!‹hÂ`…RLR]êÕB€YA,+‚Ÿ›óŽî!ø•Òahò)Ûó{ +€ªJ.Ã< #ÌÌݯ¯ÌhƒÁgán9Ñd]L G¸7m&Äi“lÍ–Ù¶¦s—WfÇxõA´§îL££e +PôMPÚ %ôÞYŽRQT$ÅA0ÂÝx Yíx_Ù8Xw * ‚äŽcHÒàˆú‹¸H¢ß™ÒƒïÉ@éýòòÚÇ÷P%ñŽ3}•Q9h’&<ù“4Ù§Xaù„ŠïfÈþÒË«q¤ÑY¬„LéUI;*+èkŠÚjªPª«ðQœÌzÇ`äMo\aè¦Iº3¢)dËhAO7àŠÝË=%£„dN4á4`RŠ©Vw©ÃÚhÄÎ̆ڄ…`þ?ñP&ñF,Y03a2’•™0!éAnŸW*;2ƒÉ2'¥)Á—3‰‰[4FI!SHJfƒ›g5sê™Ê,Ñh‰J4HE±Ui?å„›@›ÕWIdŠå^Ë„¹Ì,r +¬¡£þp/&¡~•¢ž +p&&UšCeu4ˆM ´ ÒRhÜËR”x¥U>àû®Ç09á'ÆÂñRÍLCC¦hK' +ÂYÂFìq)XËÌAרé $·Œ’¾d&kJ13Nˆ§§àiK&ÆTñÚ3¨)ŠE&Ì +À ÈçÚõ:êGL8n#t7Õe‚,„Y83(x‚®,„„ZLÔʳÅK)P$Ö–ÊlEI•YZM›•(E:P¼ì)v-í#”*ZcìR$:˜ùlDPN’é¦j ½ÙªºÞ*ôn²5ˆóhÀC¶Ñ aÛwÙÂylA¥X©7âz#òiºþ‰ßb+Ʋ• ,1vx±\zjÁ¼€lHÓØ Ù$NÖ ÛXcRh¤$„ô`Ø€€Ò“3…7b˘E%¦„èµD‹ÀŒºl!‹tUcoièhÉ«ŒanLÌ=Ï\­2êP¯Vž~GÞŸ¨'[ÌDå¡Oê` Lu±;5Ãæf¥]š 7ƒÄ@FXç«NæeÌX¡ÌB +ƒ•sL)ñ0u„èµdv emJJmFEãa1ü•A +—˜ŠÒ³¬)ácÏÜ—zn-”ƒÙ)¥ÍEY;]…#YTpêåù8Ë4pÜ©)«LRLÊNxÿ Üö´ U½Dº*×fU%)Ì øtŠÍ'Tˆ°„.ïó!!—)"×V©Bð¡+ei`?‘Él,¥ N#@‘¹Q¡$? 0^f´ü¼Âf­Äq-p9)έŠ„°)‹¥&EŠ÷¢r£ ršù$IÖ"º"òùS&“bf6ãxµ|L¥åX¦ƒI°¡x£æT<“¼A.|i{DÆ@žŽ!ÙÚY‘qTv`‡á;ª g}5…žYÔ3«3¬¨ð'4qQ¦žªL.÷ØPï,]ªPàÄèUSþŒÝ*o¯[º—»ö`E +0•‰Ž-ª¨)i dÞW;ŒÅ Ÿ¼ß© 0¨ÃZ(nÍ1¹þz'á\Ê.¨’¦XÞO…%@EgpŠ×õsO Z­=-!h)·ä•³8š!Hù È +M¼To V­ØaÂs;²mGž#ªTU±IFt#{Ea üN˜do#ÏÝ„Dò.†1ü ÝXïí ¬1:CÐñ*L5~mÆfŽÍŠäTL9oÈàg®|Ÿe ¢['é,¶BÞÄ'2NÆ: +¿´úÈô¢ÖŽ/U+æÚU7ÅZ‘Š±â¿š,ôÃ[Ì0.³Ä¬l|=Ùv¶rëí¢’Šl‘ðÀÀGgÒÁcézBE2(ÃXöçѹð™À2+ÛwúÿX;»Uir,=_ïá;û ¬EÚ…1cl|bÏQÑT·aÀ3ÝŒÛ ¾{ëy–ùUæ®iª«)j§”‘ +ii­÷'…ˆ¡ê$Üä]m³•5•›¬iÆ{+ XÇë +Õ½Ûn­¡‡hÍvAê± ù€Vg“´O˜‹°ÂF}›F1^…›B–s´¡µOE !ŒM”f•N°ÎÕØ2ôì=(U÷è¶ÂˆD·‹’ Ìû|ë6©Sµl6£í"‚ôþh œf§ŸT î!“°XŸÃM÷óKÍÓP(Ä9 °Š ·mÂ~g\LœoG‰;‘ËZ2<½ôú“Š5ÅKJlŸ‘xýƒwxÕ¨nçŠÃ±ìG) °|äÔî`0@/¦ :ÁˆŠ+Ð[+¹!”ù>¨ÞF&#%Íxåè­ +zã›{Có‘\ÍáŽÚt†™±jr +*.–àK¼ˆ”̺­#‡Ã¼K.„D"ì<#VŠÂ{æ™f¨™ângªÝ­¨#œêÙÚ Ð\“NÞUp˜ò)¨¬—ún/±>“ý ¡PÒÖš_Ä\¬É3ÈèÈ+=Œó°ƒVäu=™9öñ³b¦ã©_ t¸âë°*9ù57’.܆ÓNÛKuœÑ ì÷U¡ìœ¶ÕùŠ–Ž‚¿ÜÕ+ä·§VÌK3’#¢¯ÅZ9R”á ÅÉUüõˆè·Ü_€Ê‡ +¶ÂE,ã‹ú¢p\Š×} ®ßÅBÑ$×!‚7x™ßVc<<­´ç¦y[ü¨¨Üêž_0‹y°‡HÿŽÌc> +›)hú«W!†t§.Õ’Aâò)®Ë€ +L6Àãï­xCV·¢K‰cÌ—jÚ¶é=÷o}v¹ÝÜ‘l®ò`I…‹2 ØC{ѧ݆V700xErNWª%ÃÜO%kî1ÕyHe8‰vÀ+kÃru©ú«êqM²½2}ì¹nº TRQƒUM¤-_+R0¨­¡Âš…y,¤41D#´#4èF,ÔÌjSîÛSÃïaõ‚Ú½œ9Àÿôà**+ ¿ìî» ¦]Fx÷ f¢‡dXòplð/ÿä*EÈ^m_“½jQ_ƒ‚¨Z]Ä4+á%K9XN’å¼rêˆ w÷ƒ¾nS×YúDÓB›€Ü! už>à2£d”“<Éå#”ÝïóPš`Êèuz ?è½*Õ«å­ëpÃ^‚ VÞ ÂÖ÷§N"@`GÕ.ö|d‹úÆ0À †¦Æµ†\€ +õe+ÔÏã®Þøc¬£È[‚›`âûFkg­bØ_”à ®¼âo2ÝOƒ×øî„sI&É CyÞáœÌ(ÜÎð#za +Ï£þñި߲`dñtA8³´µân_”.ŒøoðnÊNίa¸m‰²Áîq4'¤Ëw%±ñ}³S»¢“2&T„Ã&¦gõX@Æf/¥"Þ{@Ø(Æõ¡}Ñ)m~•ñx~eËS§µ¤ÍPØ/Ì cË`7ã!Ù®ƒ|ô8Ã&¨»uÅþ_õ¢X7%j* YD[°ïmqiï +ÉR6_C.ìö­žƒÌ÷CƒÜ(žÕkR°Q–VågèÆ$dòŒw’Z>„ß¡3e°8/Ëk²ÍÛ´)³Å{÷Ê…Zк١²â&ÐÄg½oIªä> 0§Ðš¶‡Xz23%ÔÊcøâïåO©;úA£óˆœˆ” ™üÁLíf…tzÙzk*4@äð™ ÜÑ+WWn^ÓQ?{ÄSia×!$òFnÑkÄÓœ.fO\(5âꦕägþÃöçÐleŸé£e`ž.†È2Àkèm¡E¡N5Á"6ñ6™Á¾¾ÝJ!Í=Úg¸i(Å7•X긿âîPv‡á©WXïÁ¢¬èô’1lÌXPÊŠaª²vƒ(Õ#„â³{BÚÏN(* @.d]•ìlªÊÄÏŠ:àªù;b„û‘\0ÑÖJ°úèr•˜ÒiÔM…§eì²Bj ž’B¡Vä_vÐO«v;é!HëôXÑëõ?¤9oÔ°W5½×FÙ'Š¨t‡ým¶Ví& Âã”x }« ™å Ç!ñWE ØCXŠÓ'ß`÷tÈ\ò|¯ˆùÑoT­‚¶éä53á}Ñ㣴žUÐC}勒X%±ëƘ!‰†þ¹Ò­orôk¹õ¥˜½äÈÿ—ÜÔÝkê½Ó¢V?Cé rŒÑ+¶Ò!åQ•|˜ngæN¦">67Ó ÙN–_«ÝêUÁæ,›Pœ¹®B¼‘G]}Á¦­Æµáò`!ç­Çϧ´¥±ToºF‡â_ÐêÖ‹œc‡Š:k Ûg‡/×Æ÷Nú^27c±}‘/P%<˜b€ë$ º¯@¯Y “ÜмÉä^úÿ¸K»p Æ®t„*!Å®ˆ’ÄÁ”ݦT¥õC‹¢ÞJMl€G–Oú–’¤Ñá 6J×wF§£„Ñ);I$â©E2. +”˜wêÀG¿ìäÓö{#,3%pX“{HÈÝlhP¸ˆ‚r¥æ*+.šhŽI\k¨tÀ¼q‰vªÑiöÓ‰‡M§1÷ãaHÕw=âö˜Ñ@^ô „£Ç8gÚFÈS·¹Ù‰IvâD§§ŸoÅ5Ñ 'HôZßdÿ=øã…1N[ z’¾ã±Ü9ˆ bõP¼Ìt®=Ù[,¶Åä§uçxæpÕØNŽê“aÑPŽìú‘ôÄ¢ôì‡tÏÑ#j†²ëƒYÄöŸ4txié•ÝÅ#Qˆ ƒ„HŠâÕ{€µnd9:…CÂDŽbPmá¹ @™Ôý¦ìp2X“Bд‡Ó°ëu\æe¾eŽoµ©~4¨3`¾è »Ç òF1’£ZwGÓzl|x^àÏ“™å¥…è4$Y\R +pv>À„Ö¶?%ßÑÛ* .yn¦ú[Äue»#caÇ8ÊU _ÙvCtH$9&Q#/‚_LwÈy¸Ÿ=àɤdnz¨sË%Ó«iÀªK#(äVÑg,¸¼ +/káÎG±Œ ËV{dž€½9kX¶×"is‹Ø]|%^êè;Ý—‹ ~Wý€‘xçÌÈÆÝgÛ²84&)”W8®†‹<†Ö‹ˆz+†®Ä7Ú¡ ÖøL2Õ=7;øPö(!DGÆ àî±K|dhtAÌS“¿¢¶p¸³~j14žxßØU^;¢\Qï jKQ98 HCÛØã[MjéÐÚ†®¤p¦g9²6a¬ƒ -*{Ty¥yq°Rc†‘±J'žžôÏDþѳ Q–ºÍy +!QãºÃ†‰ŠÔë½\ï’‡8>Ú$ìR‚!9Ãߨæ«E +Yw£Ül¯Nq9?W¨4ž qN\g'ÆÕOJªü˜îÚ¦eW0­PN"mÖÕ1½BAì£!Fæ{ññuFÞÖlÛ¡cvÑT ÓïÏÞüço8ψ'ò²:X[E‰‘ø×WSé“d3÷núÞpŠm{…ÜÈ ˆôÍ7å·’*’O.L&Ô,ÿž½Á²;U{¸b›y%1­· Ý5E··/Òªï=n6íó…íÁCÓÆšTíê:‚£q"´)- ÁXq=‘Õ5¸d1·ÑZì/Ü\à+²¸¹4 RÅ—|9Zàm§–ÞW¨“Œ±þ–6JÓ·°˜S¦·¸E®À£„Àrg`ª”ÖÊ„½…‘%Òìjâãtþ\C}’.V¤¹à{ƒP«s°·6Ðÿª7ˆè"xîSégÉ+›vÚMÏc–ŒL¡nUª5`ˆ + žÂõa…¨ö÷ÏïXÜN¹øGÍ‘­®ïTuÒÊâ>®uâõ4<ï]àb_ßZ3´Úi‰*§Ctx rÂÞ“³1¼‘ã˜~–}W·>ñ£_F¯:EÃåJקd…ååØFÓh$ +åüÔõÊ›½ÒuðhJå5­K@žöõâL¯©Œc/y}´É›—ávô°5o%•4+MåÞ?ŸñSâPe4,*cl=H«IÄêkìþÁT°årõ×™Au": i*r®CÏ·òšÅTeBX'`Eò"°àFerß!P–ô±lŒ2”v9& ;UÆ!ž%© C¨MÎä•-½"þ ¢V [t‘Ð…×—tFšF¬!DÊ$ +¤Þ9ÜæØŠ°à™"L-ªB¿¾Õ¶ÌÃÚµ!ÈDàôï móâpï5¤å¢î t"Š;Ý›æ©vƒ—!Œ¾|ç±nÁ¥Ï+E²ƒ¹¡V ??Ÿ)›[r@ù¿Fªb¨b”€¹é¡N¹ y‚DRïÔSèéÁ0aÍ'ô Á†!µ*§ÀÄÍÀ²º»tŸ ˜7£‹Æ+$Wû”®Wwî³z‹ zviŽú4ÄáJ¬ízã„ÒÞþVÚÔ“¾ÖVአ-ª®Aê€Ô)’Å=v°Ñhܺr7=ê7s°‚oþf¡{HJ¼ì!×$ +X¾ˆºùh@äóxç½7f8¶"fún3LëÐ{ƒÌr—€'â0<=a£Œ¦…µ­[ß,¡K?RYÁž‰vDd‹‚7ðH4®÷2hÁ옒 +#°¿F”,žªÂÀì3¤^(ZÛJ8~ËýnàF†Ü9u +v+S6”˜¹­"¼ëjUG­ý©Úº@£³¢w(Š¸ë…^5{/ª3£¬üxÈ\3*T^JM¼<mÕó1¹©±¯ÙXÅ-÷­’ak+7ÆÓpàQŠEj=´ÓˆŽ½h¬àòù[qêM:Ê‘b>ü^( öUG’04š»oF¨ìè6·^—ugTQÆJ‹9+ü(P®CÅù…úÂð–+D¯ÂÛ½NöLjéö–t°Ýž8Ïveÿüå·ÛÈ"üU&á¿ò` {Çþ=UÐÿøýúñíoþí·ÿù?~ýdùÛ¿ÿüãÿþö7ÿáoÿÛïþüç?üó?ýô·ÿç§ÿò»ø§Ÿ~üãŸþßOü_?ý§ßÿßÿó?ÿñÿþ)Núõ7þûþô‡ßýù¿ÿiâw?WðAxü¯ü;Xø­m7”–‰¶f†žÖkCŽU„02Ž ¢1I†µ$âcÖríéÀ )"-4>©¯¿Ô—ˆÍƒ­ê˜^_Ò,×* +©ÒÁúÇð'Å­ p¢Lwi0ÏUÁ‚ÀPoØØ׊d‹ÿö^„b¶, +1…*V˜%4³«Ž¦ ÓÐi³}+°‚V,q#ÄÒ„Däe¶2kãáñÁ1 ¹:á_fNS‘}¸ŸkwòÍí¶ÇŠÌCÍ-¯XŽ‰t\ƒŒè:FË;/zïão]4\0,ù¯G‡´= šÔªmAô"S%II_+ÓÈ­y t'â½í¿Bú#ˇ*¥T)Q| ÙaV¶gjk½©[¨øÓWʉW®YûtIÝ©ôB¶®;ŸªÇ5okjÔ‡Ö¢¾&ê|]aKW+Ð~ߊNªÃ ¸ràCF¡’šŽ©©lÇó5i +” œ%^©£€Ã}­ÄÀÓôÂJú ÷¿ÁƪtíØ‹B侘R~&ÝvŽ†N(Ò 5ßè„á!ø!{—¨6Ã×äó1ãèˆ¨à—§f¯6âŠúÝ’RžÃ;Å¢6¢òT¿¶_ÌØÛ>øËm»…S½µ¬ÍØåÐYRÈ3íÇ¿°ÖÒhXƒZË‚µÒ…êÜú6ÔELŒç¢™fôI…ˆºOÖÕó9Gͦýø%EµŸç;×Ökè‚Œ=.V‘]Zî_öà÷ópÅ:ïÃtšÛóÞŸž†Ðp•_T:ù] òCÝwŠqbÞ<Á +€„ ~|ëŽe~ýèY´WÄõäæ OCòÛkBé²°ø¨³Çaë.eæ²>Ž^œÉ`¯øzøa®;~…eÌ3 ½‚8 Í +G€83 YºçÓ{/кƒJ=²cÌywÙ€Õ¬R¹–KbéÖ%¼0ñ2¸É¼/ïᙌ×br ¿„Ïã(Ü ´œ +s4ÒöZ¾’<Ò ð½ÇWÃø£€¯l9ØE‚MØv@ôªü-Ù)¨ìH¹ÂT*: õ•~ô-©–Õ3 H ývUÚº]ý ظa–\LºÿÅ»°ñßå6ùE£ª¨|^´vm<£œ«0ðÄÍxcô`FU–¶ñèl¼’ÞCQBÅ6æÓê5¬¼^iWó[ “kzz€ìc=¤‰é †ˆ'J9{ˆ¼IwŸe‹Ù¦XÊ4Š'Ÿ % ªàå–c6bM×´œWI[Ù;œQƒÔ:?{8…æ°—ÃtŒ¯ŽSê\&Æ'zC¯mï%ÁJŠ`s±ˆU6]Í6fqÚ(Záô_bOM´-c"æ¸_¤N$”ÖzÊãëçUe?a›Ì)>7S¼xŽƒÐ¬<ö\a‹ÄsÁufË°ïPôÜ?’ðÔñ±¨gí¦„&ædoI¶2 m½ðÖN)«À;H›Öø6– L‘èå&ÛÑÖ©ó°Ç½VCzd½‹(Þ Íº°=ÝxvXx&ÞÁÃ^¹g)‡Ñ÷ +ztò}ö Æ¸zLOtëÈ@‡:£}6‡fWÈÔó.b$_Èé3€¼@Ÿ‚÷­– ‡®TVG@¡‚¬‰•÷ÚRÉ¡¼‰¨k  u˜g}~b·Å‹€³Þz_4=“y"„tÍ•DJ«\0,¼ÐÀî ô ئnú5ó®×¢‡¬÷NÎÚ8Pº2 +ªA±$¿f^Ê2öšÑà“œc·A7k¸f­ÇW4…u!¹Ž× °w¢—%xÑ–’Çj´èA‹{£‡£Ž„ø—ƒ– ,ÈÜîG2ƒ‘ѦÍš:fÛh¡…góz¯³†™§oý%ùq¿Tq"–R˜“UÁAa5Þ"Š€¸¬HÓ]…J`~‰A¾¾„¿; *ñÐ` À|/ç|†7e5v^|Pø !|‰iX‹îŠËHq_ÑPó5w7 Ðl<€µt"^úVôGoð„º¢d§!69°;>k*¯o1ç¡Ô2•4oÉ!a°ÆÏzÉp1׋d}CîÅž:ñ¾}×8Z‹ ™Â*`ë}Çɹª¹.ËaÜ™PuÙˆX>S1Žö|3pÉÑ9¶ëÅ£ ÆåˆÏ׌Àç™ôRÅvCù +ºV`rHoóã•Gy¨ñÕ1¹¬œ + 9 O3Á¸~º8ZeÁ5ý@O¦Q8æêžW•âéóN +BNec>o®k0–‘_ÛͦÕÀXŠÔh©ùiˆ« £$Ö¥‚Ûöó­ÚbúilP‹ÒøÇçÉMë—MMø]ÜŒ2Ùà€z‡O† ÷£j¯ Á7Ì£ ÃYS%ý¬Ú{$Öðÿ {m¾Á9+ÑD Üá9‘ [xÞÝQe¦ÑÍkA\4GŠŸI»3‘§d'-èŠ “GJA×À*¼©ÍW3t®¼½—Û4š‘îãc-(hàu¥AÃ00õËÔ(a-óŽB2ë‘^ÑM"-HL4ˆµ!+DNb¥<’V1Ý"LÁxD“êçcPd±Pc‘?¸Vž‹É—ë؉áz¥‚ Ö*« DïÞ·™sÞN-™ýâE-QÍ)Øwˆ_ ]ˆ8P/Eù ÿ>ÈÄû5(ïåzI0ÓE+€Ä§ÑY™Êô•¶gz\k¦A¿75UwÁ]‘ +ïB±0PÄ´õº¥ðÍý ­£L¨GÔƒ—%%Žƒâl`-EwsåU1nÜ¥7 ücÆÔÃŽŽí7÷#pÉò›+¨·›£…ÎkøŠœÁL¹¦Ì ˆ>ÙµÞÔï¼Ð…O¡žF£5ŽHͧfj^çЂk6°ç K`2¥½ŽæNÛè1ès·ì¬³^ßG`{¨™ª€•^Ï.h…pó†C/b›khe¡B9½>C¦û£Ô×C"|kÓ0“Ã%ÙM#Ô" +˜¤ÌæÕclå#î%óÖÃùQ¹Õ¼¹~qôuÑ«ÌêüÚF†¹b^¶’µN놡ˆÿÓœ¶S"ðk™ºè_$è*7Í`· ®’Õ·.¸O–¿Æ/`mbÖ‚˜S¿èežyVô,ŒÔ?ŽYÜçØñ·Tb[ƒœ;£áÊ+`ž…T‹Öhkåæ±»ö¾·mëÄå“ÓÑÿÖ͈Âá8ßk Skõ\r *?z +ƒ½8Ü{ã@šJúSAÂÂI6)›”¯OÃyøÃÍ[¼‘_ßê!i‹A”ÔÜN™—íit nZº™¯m±ÅnG#¯*ÛNØ3S·÷…ou?¸¾ÍJI[åu–‡•i—ÝwÆAªjp)<”h˜„‚¯BYU~nÂæYÏsÌ÷øI8±£(jYƒ£—Îaæ°ÇÊ(ÿ“Mu:ôÕñj > qëÂöêÜ„ÕŠ$:ªäÙ?0ã;¬óÆ '©JérnÞÍË©'Ö)¥žµô R¤§!~c¶@2˜Iú»oÝ‘¨LÞ/B£Ñ†óàsËÞ\ SUGc¿›× B}ölɤ0ú{ è–§á ÙØÀ#·çç[xí q˜Y”00X+Èç§Dåø¾7ª¾Íîcîâd¿áF­¼‹¹„Ÿ"CNCüÒ+˜Â‰û¶ý­1´µ¹ñcᆹàöù-líÆ6ÉÛà«tV¥"ìW;¼ ickiPÏ}˜p­Û7TyOÖõ5¦8¿xã™J¹bÈj\á3瞈°ÈL8çHÖ/4ÄþÚo«¬o Q_f´TÅ¡Q"üB•÷S‡H¨Ëà{ל.Ñ3T•Ø³8 ±@e),w½¾…ÁAÄ Ãˤò¹Q 䆆Ô{ãDº(b9`'†œ m¬Œ[úª(Üa>{ű>SñFP{4ôpïCÁH·)© +Þ€›Úû UææÐwÝÔ’bÃë1³ãàó¡ëšxȵsŸÐ„L±µŠÖbS¡&ýåm¿CC oi&A6ÖW@`>r¸]þ¼Î_46íG"˜X+­±Ë Ö¢o6‰ý¥!,òjy†Åè¨B£xþý÷€c²a¶ WúŽïÑs5\\{q‚]r¦$ +]öÅìs7ŽAdb‡Å$. S +¯Ü·k§*N®`- ›®ïÒæ£(*’"l{¾„ S$È`Îì{Á}4übÚ}oDíEÏo7”<×½u]m‹j ü xêÒQ÷vª¦y2T°Û2€â0¨N[ª¢À‹ƒ2— °›ml!Èn£+f/vgCž›0õ0PĶ4°®ª`Ï¡H«ŸÿÅ b)‡÷·>MkƬëeºˆªGŠð‘]¨xÜâEÐ6EHbëZb +µíÞm¤Þm°Àz"GC; m7Ü!"zŸR4ƪÔI"·Kεá05ÄÁËÕv–öÒ+ñiˆðŸ<ßbõ¤Š^×Å + Ç4|µíÇú¼foò¶p¬îȸ¦¹¯ q†2CtÉ8ŸŠ|B<”ÛEŠÊî{ŸwÉßì§"Ï_t²ü $!+˜æ_R³´ï½-«šÃ„ýŒÀÇÄ…¢,iº{#„©ôN‹ÙäÆKdÅzÿlm­ºµµÎ–ð£ÂÔé‚š:€-篰û„ÿ©ÿ-5%WVÀž: +òŽßµ~©ØE/…8% Mƒú#¶6U«dkV \p[`•Ù¶›ôKHB/½Îäê…k•2Ä©"ŸSP¢÷E{ÃÙV(ÖÚ)Ë¿÷Þ«d.LÏ´ÁˆP&ð´]E’ŠºüMÛevWÑ#?(‰*U‘ì®ÑÆ°¡7‡éקY}…ó–>•ž(÷FØTƒL ð³l€DF©<'RÓTÍ1¯sn|=A(õQ.…˜¾ºk«G'Å\âé€&ãéÜ5Ž¡ [_ +œãÚHÛ4å”F¢–XèBÀ_Z!f¬¦îšjÔ¼ø¢é¶ú`{Þ“Ôä"™;ã/' bå“Wßvu"añï=â3½õºP…ûê8hnu’ÖàC­Çy$âjd9®ÃE™£‰‡êæe3® ª€$ÛïÏ^·¯Ç=SKº»2Îïì²ÉRþ$`¦“ÜouŒ¡²2‹~ö –Â`›÷^pΈSóÒƒ}Ú‘Ä»>…°X·dß°¿”Êb‡fœ\ߊû!´•7饘šÒä¾îÊùÞã}mŽ,é{/ÕÖU ;âÃf˜k¬Rz*1S”Y‘¿¡?0IgŠ°½Œ vDI™¼$É\•ü+SåC~ +lͼ&W$è@ÒJf'9Ó-l©¼æa¨n› †n¸OÍØ ð¤æOR I·Šžâ<ܲõka GáAÀeJhþ~éTz&/TÓv8«0Hýê,ë5©–#’Ák²fÛ@/O˜k0Þ}ÿ8Rú32 ‰]àÂpàßU×v5ÄÂâ®t¤!?{°æöq4ÖÞgH{¨-¨E +¾¼N‹¬× –ÃZL 0྅[~ÜèÙ7Hç5œwf+­x*UØ˼h¿Šw9€I…ß1ìd¼}Om«[Å— 诒ãlŒM¸æ ´Î»ÆO'}Ʊ7aµ¥DÉaê‰KSP™M”«Ê(?C×,q HÑ6ü6íJ8•¹6[Ü‹ïnÏ*mÂGa–L-ï¶)nkÏ®ÊjQH*âzvÕWm8m +§¦áüÁÇÂÔÖ—“¿ì0+‹ÁZ×Õ3sG¬’Á +)õ©iаÇ#‡Ø–^­ÍÝ+ äÙâ21A¡ôu/ +EkŠu냬ç*—/Ðzí½ÚFôóŠ²FóŠ¢_+ãV`{ÍïÅrb=eÞ*V€ªaÃ=ÝÞÎHu¹ V'yè1q`Z*ñî2­¡9žë‰ë[†R8EGfÌÁîÕ—?]*]Œ[‡©;A»È[Bk Ö:l_=æÌñ /ž‚ìTï¼!EU™?‡ CC¡KQre¿Líà¬R—ê{6[àQÓ‹;ƒK`›_W—²(9aõ$]$œj|Âë:n:q˜½Òn ¦Ê]3¾nÞ'¡ßKZÇSJf2Š‹ðPënGŒÃ› ¡gDP”1ÈžeSºˆü@¤£À,AzZÀƇ)( ‚±xÈR íÖB41¨?Y‡ƒ£šÁÚJÊ|².îMÖ:ÆJC™×¢ÌºHìξDlØRƒÊ²ðŽl=à‹5|:àÜ’²6Éñ n‹Áà!RÖÔ#sÅ´’\£¦\ÖîiY¾ +ó œñÈêaZ@E3âèA ¡/ø;‘ eÂFqŸ {½Äu^ñ Fõ‹Cˆt˜ð® !ÒÅÓÁåÖŒ¿Ì +AjC’å0Üs,h°OÓ½&9wÜØ-pcâ ˜i*%-‡º$=:ú,Hƒ*žZÉáÔ8“š£žÂ32”±×ë ©¶R)ÐEØæç8…]z4y» Ás"‰wÍ݃ 0Ç`Ô’ÉEIšr<ËaŽ±¸Þªõ˜[3ù +F.¢+To#>cÿ"L4\#Ö}挲‰ŒR<ÖäB9ð©}‹ü\ÑP$‰mîò;aãz”ånMɃ2æWÖØê¤$èQ¼“Aë»MÁÑ  ÜXÐT·÷µv¥d HÝŸ® <{PÎÄcãL¤³¾A¼Uâ6À§Ý‘7€Ï!±¸¢ÊL¥RéãÝ0ïÈœ¦;‚T¥ë“2 j\,g$yM`F_ቸ\”ÈP‚ä5)nSï0#ÿqCäÈå‘3-SB´,óHe‘ÍqŠü’2Ò~I ~é™é$'+ íö2ÍæP(#L@á2bÖqœµrô %Ôvc†‹؈Óø;×H¯ZÜÈcM¡M2EŠyœ‹q˜\>ÚÁ4ºœWn:=Ø›„)4§Çki¿²1µ\ŒSîF÷f]SÇ‘ˆ*Óœ¬lØ|—ãºiIë´8o0A®‘C¥i£‰Bû†Ãú×”(gÕ¤‰RQ|•¥Ê +¬ùž’ŸqÈ4¦Á4‹˜&É€*+92p¶×ñW [‰|”áyç”ÈÊìØȵÔ艂ã@ÅìT:{ì-þÅ0í|¿… õæà ž%¸ôZ f ‡Üá&^`+¨O¶oõhOdÝ€8²Ià'ÎÀàÇ Õ6r„}ÒñtÍ%ݾ¼‰þ>¶}î…Lµ(ì'&£ÊûLãßšø$Œ]¥³!P`åbÇ50ÿd˜[Ê Yè|WajgÐr‰© ü¯`+²øh’ò A "`¡&P;öH5ÊÄkZAJÌ_w+C¬"­^ôq‡/BL„ŵ<­Qýàô#8ž›.Øx“»ç«Ò(러ê¦ÀÒÑ€ÕEÖýÛó¼`Þ쮧§èŽ1U˜nÏ}£HF—ÈÀ£Ãæð„®@Ÿå˜½ˆúbB\® I®±yÏ5¢½5 Ž€MþÊÀúñ7Ã$_¤Þ¼V™}&,I%?’&ÿ¸{!šKŠ¦˜zÎšÛ C€Hâ(³îgc2á8ègG–Gø:Yæ9k§Õ³p/`¤ì5)»ŒÚ íze¿=°›;†ÐØTõý/Ô/¼J® ^E¤{8æà =ŸlѽÑ7ØËð, \êF™^Õæ†5n“–…Jƒ )êCE!€x€dÖè/%wgE YtÛÔK˜‡•…P±Y#ÏžEïàn‰å5 À™ ’ŠÔèx€P9J[TØyÔðìjÚÇPºé“l2_'¶jen1z<}p°nú°iëzg‘·s“—•XïäIÒƒ¼XQ¶(±cZ+…Šª‘UniÀªqÞîA³z´Ö¬×¬QQ(²!ˆÎOõï¾Õ´dqŒ^]c€ y_¿.>a…™©í½(·Œ¨†zí%ÌV‰1h€ìü> *¬˜9vŽêž¶KH º‘¹ÚWŸÚ>)OŠ“¶ý«È¨®_Õó¡c!!¡™iŽ_¿“‡ÄP°ÖƒLŠ©†«Z-°œÆõPNT…®À¤55àäò”‘¡7RÆÕ1ä$‘g`ß'äÃÒÿÞ`I²è*jdS/ÛõUR8Œ´ïŽ+åeýàð{ ¢zz"h*—{ÐËÌ"j¢f:²ÊI¢},îÔ2ºw–bÖ”NIÓ`ö¼×BS%ø´MÒa#É\Ù X†Æ9€ÕË©øËΨµ:Í+è´«_Ûo}`Ñ¡ý‘­c0QÀ¶$šLÄ£þe>»½½Ø®ËDQÅŸªÃ±Àë½èd š7¸QfkX¶r½×[2z ðB–e2ãÁ£LA¯€Áˆ›K‰Èhðÿr= Í"Oò胬Uè¾ÜloÙØ[®@˜n3B"l½èkŽ(Áµìj_®_-þ@)NÖï¯g»ÒZQøÂ^”ž·APeE'äÙM€©é×ÎP-dÛŸ7¸î!óÈ!¿Å-ÆÀ])Ç·J¨"A`¿ì¡FéÍæ_§eõÈsǗܘx FKHN"B‘ê%ëºF²imÄ/žó¿ç¯F4¿å€-°èºcØŸ+ @.Ú°iý²õ°ñlï°•¿d+g¨íÍ#žEÖ +$÷ž,\Ãg-ØúÚ¶+ðþh!s Ä2ð(•¢0Aºé†HõKÅ9ÛKº‘FdáilÊÛ¶×Eàâ—Žå çiæJÔ¤i æçÚ]„ÊU;%ì‡$]“«Û*Ì•€äPñ[È®–kÃ7YZDÊÇÑp ZK`£uÞè$Ü}1 ,N9dÚð±Ë¾Ò{ºœ¨2Ga¨ªÅÀ a£âw$õ60—ŸcYèçÒe¶)6R­#7Û#IaÑ Ø¢å:o9oC¨˜±áA@ÔmJKõY6ø‡lVP<ÙÝRÍŠJüΘ|4‰òÃ\aÉ– .‘]”^P„Ð|‚jèˆJCPªQ…o/2 (É`Yo)OXH‰;Ò*{ÍE…?ùk£Õb™¨zplZDȳ`HžôØw3 л`¢C”cm +¦]ðZzÞ 9–QŠ«TìÉæÀ÷&WôøAR¥½¤ ’Y÷„ý«”8‡ßHæµJ*RõESÀöÂûŠ¯¾±ØÇ?D`BÁŠ"ɧ+€rÙêD¤®‚_eÝÙ³YÛÒ àgÖëLÆ5¼=èzL–Èüˆ¢óÈíRª^†ÔÑB8u>§rˆÊ—¬ÌVˆ5GWRŒ¤Qþb߃負KØ.“ÚºÛQ'" 5U¯zùI·¡ó‚eTŽ ˆ¨:eó ùøB•mÍfãôùÇœçÑì {ÜkŠñ.vž¡žþÈ;^‘®$ù"[p»ð +ܹÀÉP¶×C`“’€.·ë…2#úóvô£_0•vù”ÓTU$'+3‡3—j–›úÄŽ© OeÚ\ó}Å÷ +p³C}Ž+t,Õ•ÇÜì›lš¡Fž†Øø;ЂWpßÚœÊ#!”a’ „¼<©Š±±ÔH)ø¦‹ÃW7äq® ,q'‡:— ß,›Í±•˜ªî¡´Œœ~Ä ¤^»TþÍ:2œa–¾Üí@uƒXÓÀåÆ+pÝ{B®ý”3Í;æH +»E¢w½wIšÚm6s˜DÙÂ5o3²­ÙÌHÐFÆ'®îƒ«.¬e#ãR(êN”ø9ܤ¾] ýŒmFÆccëä,ÊÞfÑ@˜ ^ö`Qñ†tÉŠbƒ¤Üs¸té©™¢1Þ>B76û Uu(¤¸ñùE|ƒüö ™éròh(`¥åñºÕÒÜ3ü纓UŠŒ~Ø<'¶ ˜ê€¢>Z„-²gD¼,ëÍ’L“UÀ0å ’y Ï¡ô:5,i ¦ §ú(é¹_ikš¹X2þ@zßÔ2¥:RËÈ-ð-ŸÌÀ‹œX÷#!€@‚ä9l„ªb»„œL÷Á§Zo«a­ˆÕ‡F? ‘7¸PvW³äqŽ¡Ñ¢ÕjDÛÓF2§4(Ë·&8˜ûžGXý/üªT ¯×£É ãJÊ ÉAX—¡6Uµž'(B³õ帠ëˆ"UÀ|ŽœåÜ‚ƒªtQSæÀV0\®×«Ê>•ÜÑ0E¡Ü&¾^!ˆâuçF(:5”¨É ŽÐ¬¢¨9"„´˜D„Ìå‘%}bAJ|¤&ûÖ íPpdk“‡Àš"5uáG {Go½ø¦_‹†ÉÕ|spzÉ'K@5„]qcOÖBÇæ5tulÞp« “"‡¸–Vݘðc‡Û¾=$JßÖç{ëR`…ç9…} ã‚Ä Å90«Å©1K}H +ÈÌ8\Aý0 P×í “½ËsÉ3àfÝ­àB%z¶¹yA ²z>‡¢‡m·þAìC€AËuv§,tÀqÖ–ˆ¬¡;•'•°9‘à5BË&%H¦2ËGb#>‚ý`ÊyºgðPUoqº¿™…˜Ÿcó âc«æ†nc…òAžgÇŽ|¸…Føq‚ˆ"xÅ_öÒ®•^ufìótðâ!Æî!Ú›õ{°Ö쨽NÄâÌ+} ½.­åÊPM§Ì°H"³ +üÑ¡ìÞfÝâ5¤ÇˆkåóçD†Øvjr€9ðîY½€uy§žB'ivð©­UÜk[Ú“N’:ÇV 2"YÏIs)¢,eEd2¶mqü¢#!Ê›Û׊&!•Ù#+JŠ÷ë»Ü™¬º(#Ÿs2SDÔ:֘²ãÚ ß®¿'0S AÃb‚Ì ¢|… +…#äÖ‚>&•’pågú“/³TK1ÊqŠ%Šj=Tªë0»B 'çz²‡(ãLA¶êzŽH­•u¼©Ÿ¶Þê¦éá0D b$0ø1(¾`ªFà#¿_Q† +&B7:èŒà„ù(Èvµø TËÐòa5éÈ¥”7ÀÏžI=§x×#›D8œ(àÀ7 +=Q´¬^  æ&Þ#ÍO艇âzob(AS—N‹OJóTbyZ1…›"Ñžj+lmêß]±Š¯c|“«xC£n]ð -ŸäÚPÌP©¸tA±ƒ©ú´MR'µÃù+Çl!uÒžÅòÁ&à:Ƈc3&sX(A«l¦l„]—äi:,z€oÒ%Ùó)¦pWÞ·×ã% ‚LXw„oeÊ©Ü;Ä¢UÌí°3 šT#Z,!q)X¨høH‚²k +â8ÓV+u“êµ³â5Pó,î Ò8Å`§k¹àa ©ùâ€Ãœ‘ÃMñ×57…CïÔÉ°‘–l%H%-1DreoÅTä…8i" H}ð[ÄÔ^BD†:k7¦k³[q².l²OHh,Ó®Á}Õ|¡@½9Ü ’â%‹·.¹A"å( âjFtG~E}¼mÊÊm¦ˆýÍìÑMaIæG¤)·`¼@ÄéWŒ*ªêÊ*¢öÆÝÍX\P0BÃ÷–±N÷,‹9ŒB°7ãa‘r3ÄâŠZnÚ·*B§40³Øàä ¬i>G ¸rÕ-4:T¯NŤ@ldÇ*«stÞ4…ö`Q›Ï FƶI¸5rŽZ4I‹$¤¿ˆvQ× Ñ.ÈŒnÌë…½ööî…Ò4ݹ á¿Š=RÉHx²—½ßÀ«œ˜d:5d¨é$¨s¥Á­:ê!#–º9Õ-aÌRò6 Ì$[µ0.¡ÿ)–@4Ý•Às ð-ø>(¦¨Í´ueˆ‰%&ÂäSXµyÈDÉrÄqÜÎQZS+,b5öVwÌŠäC"ø;1!Ê¥â G€¾&XvHnt”øhÈ5ŒOÉîóCFS<çÞà­W +Så®#E™¢ŠÏÐB©œ^ÍrÞ>w¨k†Öú}-:T„.èp_ûŽQUe×]«Ò©â‰©Fú;«`…V¤ ­è¥Ó€=X9AÆJxÁâth¿"8EF,å“R ‰e–Á+ÈôÅ5Lªôæ% ØN#iÍNpß=ª/u•ÉBÔ*ˆhK ¹zS1OÊ.Š/^[‘œ§Ð¸ÑõÖ4畾‰mšvˬoV7Ï™„h%&Û;,s†QLGë[µL Žpð6K±Æv d9ØôP³`Í÷P +pòŠ6‘+“ý(!°­eA4è½ásrÙ›=¬à­fåÌÍO¥¨4`¾§6VþÑŠï/ËÛJÉQC ›B†Ò­æa¸Ø×ȯY+;È%]ÂE&Òë_íÀZSð‚³‰¡ºÍGꊙÐÁo¡”¯?ŒBWh´Å‹…<¡ˆZÀ—ز=˜ÆÖ%®gK?¶´[tÞ¿k‹­ѳ„+™øž`¯”RÏÏáMª­º½QÂh-3¡™>b†øt’/{æ‘õäê>Ó¾Å죇D44é¿;“Ò¦zéµ=IØ––ÆuÖ,ùÖ²(ˆzŽ°¨ü¨ÃõðöèØ–L‘CÞ‰@RmÛÖÊ M $νoã$hG§Ï€“wf ¼bÙ^0^˜g±¬Y‹“¦„jó ¬ èmw};ú T—Ïr ? +ŠqoÏÜLŒXÀ­gٛɜp·'D§«†ß¨?(–1Ò\US.rT7’Ô^håvø%z1–é…bÁßý› u˜ï¢¨ÂþWÍ¿æüÂvˆuJEAÄ—ÕÒ¶@p=ÛußÔ,x®c"ß}4Bf 4Ð_xo¦XÚóPEø XxKQe?r<8Áããú²~R™2Ò±S!4Å—*(‡ð¦¶á™®@tLjy»t¬ÿ!Ÿ8·Ýjõůz ²ÒM|i §b£³ºÎþ…ôœÈKn=•‘‹Ô36’ .Òk ›Ìˆ°5U#d.6;¸ÂY†Ò¥s YAáÄw-ìkç¸Ë ,XÕ˜ˆœV·I|±Æ©¿7qò_ +‚®CÊFçð¼¦tf¤þB•›Ð]Òyò h¦×x}+ÃÉÀPJ‰wé[wr©á2SÍÍoDêG.Fo'ÒŽtb·]Ýj{Å”¨Í&ÓR4‡[Jw7Ûâ ºÊ?nÀ¥iu º`EèÆ FŽLGþ×@(¿%Òåßýªçâ¿ò@f†ñOöŸ¯lås”[ÑF„WÜóËñZ/ÉÝ€Ú roöºÍ§¾÷’±áÔuÁr´Ã5ƒèášÈç—;Ѳãz²lùz–<ð© tQ’ãÅR±!CLl¾X–É-¶jƒnûö!üxÇF•âláF)à¢1<ÆâÎÏUŸ¾/ƒ':À1ü(¹é;,i$*…©pí£äsòЊùªžÏišfί9èùõ£‹PgõÕï ­RH÷šÒ’×v9÷±‹I”žNƒËNŽàŽL¡ÜçK&¥È V!S/¥¬Dù”Íai%,.pøÅP© QÞ`§vWv¢*ÚN¦‚¿ø¥*ŸSÍFÂzvLC+î*ÚPÌìÿ $¨6ÝÛ§•@ó ÌBå;…ÕQ3×$ê$(‰‚ñ©qÛ“ÓÝ‘±H° V‘Ñ%[E +’ê¬ašwŒh€š§!6êAØ;Ÿ;»qŸ, ð˜Y9ÅŒ/ˆ$RZ¬[$4X·ÒÑQ§—ùš‹²OôÂ÷›^ìc=Ž5ÆÕC 2 çT ðöÖþõø_ 4þ•žÌA«³ÂjŽtQ֠ȯÀZôTîÖTŠJ,@ÂÃí³Ž¶œšó¨Õ"-GPç.ë»PP£ÚkÓL’%m°ˆ6U¯†‚š/ÚÛF\Î’âZ ³ù©æs¸‚k›Fvö6I ”9b'á:à,T0•‡[÷×++ Ç’Â7â(¨;+ÄÝ~pû ÛCL?'7ÓÎ1ç6$cA»…Áõ{ÌwLë­ð7¹4Y©sk8Wì°ƒÊá=¢†ª@ï5ÕhPßCY÷ NÝB Ï» ´(m1¶¾ÅE³˜taŸWC‡S…„P: Ë¹·ÍŠ*íÖåB—”Äú<•8æí^kž?Ð.ý;\íÜf5k60l"B'f&ßæ–Ã\Íd\]ÂèmÀýøŽm1¶¦.,³¨ª«¡d1;ïá}S_#¢ L{5ÞHà%펚¹.ƒy—¹ÁuƒÐbH¦>((óªs@ ú\^ÕylÐè%ÇYxÁ „N]Ãú}í»^ÓjÀ££¸UesEÅ¥ì¯êö¹N0ž’ȶ¡^Ãd-Aƒ#·qãÜɲC©EÅË È$%ëÃ*OãÊ(»em…½ôˆ}nÙuœ„m|Ч¤X”‹z°‚‚§¦n7!œVˆË3|‘ûuÇÄþr0p³•ÈÍë| \kÒo +ðo{Òt$X±ÖÕ†]ù™ðN¦;…L¦\ÍwoÏ„“ïs¦ÈF¨ºÚƒ×x>pdaÜqñZîõæ8…W˜4Ñ)׫’Ðâ;ó±‡é1eþ%bÌ×æìô\9L’ˆ¸•MÛ3º‘Cê&†ô_ “%y_!<ÛggÙxV#Ó:–r´ÅUtÍ!ƒX²xR'5X>ÂÏ ­Â彇ÀM³4kº|,W>zõ]ó®àÕJø…(Í"!L{-Çã)º|Ô“Ö¥Þ\>{ü¼³®’¼Ó +RBuóã8Hµ†&uk¯Å‚  ìQc®X’7˜Øwø£€fÍ6)-»„ï87ŸêŠx¤ hai“"TÞ£”¤Ä_XYgÌs¢¢AÐçÂkþ‰ô¨VM’ ÍÔ¯½#W'!~ëzB%󑯡Lœe¢ë!H¢Pœçæî›1@ü\õ˜Y= ‚EãLËj(ù*›e–»'H›ÜR]K¼ãl0"AŠ,;7d¨‹°öë»±î\ݶ  +Ä4ï¯zÔ0•¥€Õï3¼÷‚„@A`¾o}7?Ü´ÚšŒƒ]bXÚðLÞ?q¼ÍÕ0W)¢ß¿mô£ÔSc@UbêãóÞR§QMX/ä#¡óÑKC>r¾˜gÀ½áwû({|€„UÏ<žc²•Ÿûª×QZCu-Ì0+ ÖŸ;±ˆ‚ªd3 +µ¡kù>¬ƒ˜¶Èwä‡ánÍPtž 7Äg3»  Þ ÅE`l5 ‡³p®{+$R“À *Ý‘)dõï°ƒwÍ…\üàÇ¿ˆÔ#ËÑžz¦Ihæ¸[P™¬ë‚¸ öôB/׬“w‘ûv–Ham»vÍw{éë™ÎÇ1ÄÄ@À`!0µ¯èûŠp¾èq³¦²%YkGF#ŽŸÕ¯ò4@†+ÔKQ$çä×Ü ¹ðN;1ß0c›Ñ >wíÀa6o¢ØšYÊjJ4C@¸Å[&“DìtÄ®M§4È‘¢Êƒ EŽ¿ý–› ðöÒþôIÆ4N• l€uqC@æ<n† +Áên¥Ij۾ĭ¯Ù¾mζå­*Ê­L© +ið8‘סG`ø¡ul÷zI¹oÛ~Ù^¬vôÒüÐý+D”uÿÅn¨—3©Å\¶;=dâóÄcÌ—OiÝÞæ¬~9Â[7\6ˆÍKàNœ¨PP”^${&‘¥Ý†V蔋"íDXÈ“¶ÖÜÂD¸ºˆ­‘î A›k ›B×L{„fÄzåëaŽ€¤ÀH­ ƒQ­ mZkr‘^aýŒìä~o/a„ MÞ׈ °Oª±6 3«'¹Ægê¤Iáܲk³|z0-ý@ƒ’c¥K)92§‡ +@PÊ®¨…©<1r€½ž3Aù¡WÙ×㬅ê$ÜE{¬…žL­±xb²šÔô7õ¥!j…ÀrÏ“.fªsÛYÓµ u‘¶kÐÊDa¸—gX”׫œ“ÌÇåÄ#¸ãy(3“öÒOaÜ ýGMÔ4S㞃º¾ cVRÃèAÚÝú >Ž +JÛìy$ѯçä‘ܯªõßÁ ݇×XQ@Iù²%åç±éÉaÄi¶Ã$,Gb©óÀLwkU8¬Ú*0Â#³âŽö|î%¾›Ö<_*3’Ú*S/‰Ÿ¦6&HÛ±{ŒÝã>öGÊã½Q[d±Ææ‚A¦&,b Â,ŸC؇3Ów2~½ÜݽK” vCù•þÞ±6i¯vE/UI˜|ÃÒeZÁT²8%‹åú¼wh$ð )ê±,û쓶l¾w<¼¾å¤«4c&5>x†*LQAÜUgÐLeä×í›_õ¢0'\¾ûésÇÖ ÿuMÓ!M† Ðh†¢¥0k{Ì}6LGjå{…j®ÊÆqîÕ¤ÂÆHNŠ}à•B§vƒôPm‡ˆ$ª€LäP Ì^ᶿ]ͤ·ÕƒÍsô'_±'uñ¼£ë,晜ÃÖ +r‚Ö*5½* U£Ã&/™@=Œ r˜BPçh‡f ÀP ^Ÿ=â3Œ5*z8×.ƒÏk÷ñtEÞƒ  \“eó«T°µõ«ý‡m¥#Å9ÓG/è¡<ÝR³ ';ˆgÊV\7P!™¸Ì Sßw±×«÷gÏ”‚à©6¾:¾íz„] p‡1PzïaõN£La÷˜þèEPËF ¦×p¡Ò®Q9Vò²“6Ð؈J™ÿ(Æ°dsöÙ㼦Å{CÎû¾¿8$Ò\0$ɱ¨\*¶|Ùƒ½‚ëF‡Áµ±xöÎ* +Œh>£˜d}HÎ\ª b”’."%×RÖãMh8|hB²{ôñÄ âDÀVÀúè%8âFîÓP×Rµ%œ©×¯z´@³uÒä'Øùè¤ `F¤ðD@õ b>-ø¿@h.äÿš‚;êë¬I9×Gg«Ç´N£(ˆC¢áZúÑ€‹Ýíá6$ø{°ÔÐãè ÉCgÖ/÷õœ Ô6WxWç½Y» ­“}¦M×//q&³¶è1Á±gRö3¹vÕS:‡±yÅFÛ '(ž~cÎëd^õñÌù“qQhYVÓÊÌzõ>{lPÒsjóÿ¼÷O¿ì¥6Xm0ÄŠ53ôâáùJ)õûkzG/÷±¡`Ôw§i±Éds¬t”2+ë*ÝO#9#á‰[š¹Sª¨‰ÍÍ$[s˜B:מNÖxPÎÒYa¸ +u}ï·šÈâA¼z‡­$ Ï?ŸÚ–ºísèíüy˜û:¬9°Ü=¶©¦Ð€%ÜøèñåùÑk-&Ê‹£5tK¹ûCùÖ½&m±ÿa™fÝ@ÉŸyÓû¦àõü¿g Ï.3j›JNœY­0âBö¤``.) ? +ðÃ"ý"uiª;É=BTŸ¾ ÔH¤^æveÏ¢"ØcÆ‚ÀG¿ì‘S÷Ç×5e†¹«r ¬s1²~ÒMÀrbÃU‹Kk~‘ñ—”eŠèÊNeîNÐ_¾´Cü«)Dr9u"B¶uÎ˃(|dGÆDf7´¾ ®ðdV´åc_-œ4µ€fhiÏßu÷uw á0È´’Æ…°Höp«QÅ+6´­Ç—š,RÁßÂYˆDm°î‡A áGzÃF64w²‡›Šþ–æpl ¥ŠC"âü¤PA罟#xî˜õu#xgµÑY¾ÎPåÉkR¤€rmó|rW‚ƒ>°$’•p»É3þ>Þ¼¸iŒ× uæIM…z„„œ¿‰êó4§ˆs5a(ÄÏ`/5.díøáà(ú=é»›â'!7Ó)Š¬;Õ†”BÃïPšÆX¶Ä!Ž6Va>"Oã߈ÚSˆ%Ž¸7ØÍÊPÂAËæ\ˆóbá}Yñغúi¹:רܗ4ÉJT +oP ÖŠ… #¸÷†¸7ï‘ÄÇ×±ÇÑÄG[ŠŒÜšQ`I~öðæ?'xk„GÉ{—ÕÙ¢lRåheË'Œc?¿Î®šŽØ +ÄVÔx…ßË¿ÁwݳÀ¾äH•cp÷F$?¯4b/G¤oðΊ!š¢,ÂÉÉf¶[O>ê ¬*¸T§r‘^}ïqsæùÂùè~iãTŽ«kã”E¨ÕùèIzçs^;e!Õ5ˆc1ÂBˆ7©amef¿„,„jhÝ7ièÖtõ]CxìT[·Vû"1ogƒ^,»g„.Gi†D*q ½Ôg\Ze*p$3_’l« Àá㤹À7ýh@ìȼ·ûgO'¸™-Åú½ê±±5FŸ@¿ÛÓÓ䈤aŽêë[¤=É¡·@¦¬äBúáýó;V¸S;þEã‘i¬:x³$Cþº@üŽ@³ƒGQ¿à¦Þö4<¯]¸Ö®ÈÛp¾µfh<¨º‚fg#Dz4Åx½%'X*ÈnL?˾ª[מ§è—¦h°\ã&G™¼µ¼\ó9Š©ò¼KìG¥Ç”0'wBÕÃtР’ñ•J€¦¡ÀQ5¹ ?H\þø|Æo‰Cõjl¼ª)ö¨ÊƒûzìÄ ÷å·«uîRäzânøR^ÛVHŽÏ·`êh2·†0s©TW +E{;úBÂ#¦ìᆈ|!DÎœ¿qMÒgc½º$ò[Û@ù~Çþ’´}6¬_\ÇÙ°½5fH´"f®`¯-/=4ÈðitÖŸ¸fí Lxz"ÅàÏÁ™YS^23#"QÉ©XPe¢(ÅúT›‘þÜï{ck[OöÎáŠQ¶ØßlŠiì ªE•Ðn=rgs UT¬J!›z†¥†µÉTb›ÂmÖ_D(1»yh²*C¯™@±ÓÚŸŠ­‹òŠË‰¡a#jCªø4{® +¶n]l Eþþß„Þ¥dÝŠèßúÒ "U\5J*8%6>9°ôïnA iÞü4¬AÔa;Yëõ-Të—܉Øì~þV¾‡jÓ”Ey +w#Ó?s8^Ô4ô)b®Â¾ ¢¡2:zÅuò¡ˆŒ¢)Z:à@Q“u`”Á¶ŒßªkØլ…½¬ívïësöàaJjè‚M,Bûò["mä þ*gð_y°„cÿžèü~ýÇøö7ÿöÛÿü¿~²üíßÿÇ?þñû›ÿð·ÿíwþóþùŸ~úÛÿóÓùÝ?üÓO?þñOÿï§?þ¯ŸþÓïÿáÏÿùŸÿøÿ'ýúÿýúÃïþü‡ßÿ´Nñ‹‹»Ÿ+ø 6þWþ-ýâš–¨;ðÝWÐÖÂz½ +dV³\´á¼PÞpùo@ +25u”ËÊ G~þBšo­i»ªœ°Ü~E¨œ+†EDyP¶vÿŸUìÑÎgЄ©—c© ÊMØ̾l¸?a¤°O ¬’üJý¸5¡‰œÃCÔYíšuÝÙ½æ-xTâe¾1÷º_¿ûú!€7rØMTaÖ¢¡ÝÑ ,+E’g~£íglM9ƒ´ì +AƒòüºÕˆÊßúEßue5T2„îg]I?Ɔ`¯ t<¡9” ©eÿVê]H‚râ4›nÝ4õØì!—\í­°oFÐguîð¼ê<ÀºoÂ%E´-E€Ý6?UÈq›k:P‚ÊrÍ|¨¼öZâa]À¶‡­„p °™\$­'½AÖnlÜ&ûQÚ\Úa£w?7½>3t¸çöº=¼>¿>µÄ £”›ÌD|«o0‰ÕMF Šæë,­Ä•íý—f¹=´Ü#®›‡ÐaU9L~ar+ÛXΟ1ñÆÝ¡.¯@øY€yƒä^ÿ¾¡…`ìÐë1^ms»¯³RiÞ÷ƒøRáF“½ižÔ®÷φÀ'ðÎ~ш5Ïha¡O¤¦äº=€N À—&ph:>–?ìÜÊÖš÷Î4?Ô{(DÄk•‘k@öäÀVBL¸G9P ¤IôQT8'ÐÒ‡‚mÀ‚h ô@–¿i/Fp9$ÐàæÜ uŒvMrXhÒUâõùQ-™v¶^»@EˆNyƒÊvGémhЃïzÑ3ÎÄ?IB÷{ƒ¯0H£Ô?“XÂ0ü¼d$óу"$(Kê‰h+»ýµ¿¹7>ÚŸuxañHlמ‘„Í2ùs”uòË‚\¿[`…Ó•> N wä=(S°-gÖ¹·àëĉ–P‚˜ccEh¤œFqGˆÚK¶ÊAY‰„÷n ¶½R¥äÜaj¢B3_È›X•Ù¶Ý!^+Ëê4  å}׉tŽf´±iCž+~ÎÑ„²”/1aê3Tt{s=JÏú³ÉE|°™5jEÛavÅ{á};áM0 ™AѸa-þž°n#D¨Û횆Ðõm9Ó&z9bÙI¾ãÊ®ê({±²5J=¦èfµ8|Ù«:¥ R^'eU÷Ë/ª{/aÚBgÙÚ2·ügJ$ÞÞ¡I„tÑØû¦L—ty‡ no۪܆Π“œ ¹}lT*@:”vG Ø» ó²OÍŒ›ˆVëp¸ù¦dè(ŠŽ¡Û±-ŠGÎZ7\6×|©âÒàq}[½ZÜo +áAë©Õ:ÿ’:pè ¦ 9„M»ÜA9 ´k³6æuó™6Á+4Èv* kê!ÿm‡fÁ=ƒÎ¡ÃÛm öÐ|„OI%pþ ˆ8ìÁ:^§B§‚X¸@7™;k«¨%=M Pé=@}ºƒÑ˜Š„¹Mm½e÷—ºA}kŸ¾™ÌÀê‘/åT-–ÅÀå¼naú4¶»µ»‡tuÜÎé%øñ¼2ì‡ñTkü8çŠRÏxK°M¢×õT—7ë ~ËáÜp ‡:'4 Áá`œÒ0Ú^šh$—«¿™¡‚–ÅuŒö K‰¸AHdwáÜ'¿ñø€0QeEr×ìÇ« …›ØûÒ‚Ö0Dý)PÿEÉÖ‰µîÓà2BIQ±§ø ¨º€ÙŒqÕ­H qÉ oVÙY+"S¨W+RF*é™K¾pCc Aè’ú^Ÿ9g‰û »  FEÅ‹sc Ðÿ!'…fytP‘C??Fd>Õ~ÌO SlÈ Ù8è–tôQ4ÿü¨`Ù‰èç3kÊÄÕYóŠ«L·Dço/ ——9ŸÏp~##œ/_¯¿×º‘àç#öðÓoLó'¢šÍ€uSAÿ,CYÍÆgÍ›„à\ôQ!’ïp¥ÆÈ;;8Ôö3“¥,¦aôš;.þ¬êzôW×—yÝW.¬3Îjö§¨8<÷ vûý&¾jø \o(—Dtê9÷úÛ±=ª]Ïg7…²vmÆó· ÚMÃ8ŸIxBö2 ¾›Rø©)`ñó“/¶’+Äž·€šcëÜ’›„/N +›¨î< ‡žˆä®2ÅMP*D[0ŸÄ«÷2³gÎDCja[˜Ú¿`yöp"RÑLë©ïM|“¢Îªˆ… ±U‰a¼$YQÝd¨çãCNc#_5Ö çï˜ÂÒ°OB/f½lƒ¤•×ˆÀg… e„ì ÞiDÿòVþ,^«, ”P'« z„‡Ùs|kÚÄQÀ 4 vÖÍu¤+ÎÅ´tAÙ‰üSc‹IîË™öD/p?Óc+äxkü‘.˜K7:ž&ëA^ÖO·ÑKÆÃäÂÓ®ˆsg¸Óde ñŒÈ ÇÜ›<èÊ Ä\OkìÛ¼•ZuûJ LÕà6èMaX«}µû<J>p4™§ÑNaõc'°œÃ•éy.éW¸{ ¨/=¦üG‡9ì%[ü°4ÕM¤¡#Öõ”A +H¬ÆEXl¢0”./›ß¢þ(x µGÉ%KÝȪû™TTåL‘¦Ó„ŠM¤J+¹gÓ¤?Ÿ,zägÌX5}知ü“ +)šh`;qIzØù4®»l#ehv¶úº%U\†9@2¿ÌH}}`ÈÈiy,Èã¤×³D÷•ÒŒDc~9âÑ äù˜¬C§÷½4ÈÙÚ¾·Á´EòÂDq YgjÇLjvœ ¸ñú¢‡ïÍ…g ëõ„Ü!|lP +=PÝÚ&Î[-¡ …Ö+Àþ`Ÿp&HTÎŽäÀí†" 1[¿Hí?2&$¯P×pe@¤CÑ÷±?RÿÄêŠÔH}ëÅCJñô,íHëBEýl88š3~Ñ(ñÌÀLÄ‘€eWA6 ·"BøOƒ—‰¸êï„»ý~5BÂ#»¶ÖØV_WT ®/H<ç³Çüh¼¢¨.¾>l#&9LÄ8+ +ë­fa:ŸŸ‡?ØUtNЮח.å±Ë©Zw +AlœðÑaÂéäS»ÅJKaq¸o*‰i~DíÌ^—Õ°öç¦âD×dÍXÙµTaØtö ئmM¤m¸ƒFM. +³ldÀ3^ÖK¬9Ñ°†ß ùå7·-~RC³³J¬©AÐ#û˜Kí{®ac®wȾ=^2ŸÏã±ê‚ L0òÕÈíF!ŽÛ=ýI=ä¡?*6$ûâ>w­Ù}ir~‰U »»u“iÜOCüD',໼¾ukîàºçÖYð£a FvîØ{#bLTdzÊI4àzÕÆÆaŠßÂÅ—x©˜¢` ¿˜bߡ©BÊÂÛ)·¢3ÎTS¦‡ºõ$iȪÌ_Ï~Š:IäN ô€…šÐ’›Ö0j€‘òË‚iŽ†´:‰~êX¸»Ž@îÄöŒTL7ÑLª„’²bþdŽ-|Ë…>Ÿ›»Æµ€™…½õ÷; @©¢ªk¢ o‹ÜEù¿í‰ÄGk$ÅGCü¢Õ±gÿnûï]z­µEõ,€rÿž(]kù­®*ø¿ãñŸ‰ÍÆ×Íx×"výYI…Û\q£Ü[è¹U7xV|ž!Ïf˜N»)Žïr EN>3M€&î€rn êiÀÝI¹}‚pµ ‘„à¼þ{ÃwHuãœ1vhMd‘½õ¬”{ ¾!Ù¦{ßZš¹æºÓpyÀåµ’Ýaôç>ß9@Mf'ögÀB›ª5Ãß-JÆŠ[‹q¸ÉÜà·ÈÝ¢d¬Øæ¥~ÏN(@¸Ìˆ6!2Œ9 !S½ÿõ.‡=nãQÂ]o;×í,°Ö§ô—’¥†{—×2²¥Uè´æ=¶rͬœiF Öh´SÅ_ôü„ÂXï°z\=PP°Û@zP^Ý–õ®‘6±€^4%¤Ý¢¼œÎÇÑ/Ç᜼éˆÑ Ù@˜`µÅ¯× ëÐ’6MðyjUS?±ý-’ÿT>g¶[ãEÖö)kB$µÃ ósú¹c†¬|¬®_“`i÷ªFfz¸¹_=8_ùÀÙÜNÓ’.Ib?‹c€ò\ + $Ž½¯½ªNak*ØÔŸ2XS60Å|ÍËkÖ6€^$ÛKöËÚÉÊ)XCãõ°]šÉ•;• êÞÔR@ü×;ÇPŠ¨¯†p r·G9®§#ì{[€Ü  â"C!ì<)ER5`yÕ§Á‹¢N 4c\ëA¯›þ|«Š­‡~¹×=8ý€ÖE+š–EvÇ +òÞ |¾¬©Žº*zœ_­Ââ„*Ê;6–‰zí©ÈÇöˆÇÅ’ÄëDμŒ/ŽãcçÕICX¹ô°|b“O⣇„‰ºíž¶ Ã½rˆüOïgK¥ää’ÜOË]‘©Z7cÞ˜tŸ£ØÙN¹ªhÂô[ªûái«u:&K2=¢ì…èHŽÉË’*¢5%r>ª_cŸ©„…ÒçL.ƒ«— \¯J%ûFÑB艾+Å61QðÞšy›”°E§ÃЈˆ"é%ž]¹âs"wÞàRÈøÁ·t#SÈñóÔXs«éÝ5'©‰ÍiÜQž†xoKûó-̶ÓŽ8à£!žlÛOöy#ß{_C›Üݹ¬—uo*œ&&Ž¥R"ÇÇáóòÞáç]²5cˆ‹sôÇa(í±RÚSƒ„½‡*£;œ0Èù­ Å@eû{ JmQDrèÏJÞìꔼ!±ÔûÇç¡XS·bÍÞ`}tB¸¶ïZ_‰¢"‘ŠuÊ{<„®ÕgêDCÌBž™8áØCêu0¯ÈJz`ÝDq0ø +¤eåàYÖÇ®y= *§oÏF2ÆZ>v³é}«¹ /•^gŠ5|%`–Sm ŠrÛœ=dÈDâRv´í:É’’Ò÷öØÊÑä©öx'þ $]wÊ*Øip¥lúN÷›)#ì ˜Ã㦠˜Õ¾è1ØqˆC)ù¸5~öšÛA G-H` TK‰øºDDy 2BB²ê2Ÿ ?ïЧLˆÑ$½?{­KÌ9 [Õp RYû¾WC:ø§ç#…%Ê–¸ô8\qS±Ð¾Á6Ô;ºã~ECŒûy 8}ïñÌ 9Ô¹þÅa ¡]‘wÕ”ºcÀÒö" †Uåkß¿÷ïüЩÍn§J0O¯p¸]³XγµÒ¾z„ÿ)–[i¡ƒ£8Gc$&mlÍ(Ѭ©žÛíš4®X“볇†Þ¹sî ìgF¨Dâ(êˆi„Ýf¸]DŒÌÒš#²‡8ÚÃÜ8ú†<ýYÃX ^3rø»Ìæ ø>¤ÇkŸàÚî¯zl%#Íò™üß{)€ÓXZt©¼#s0¡]GŒéÚbL‚gÆ=¯ÏRhkäùц.K6¸(¤íH›y‡ l(ÅÞÌ!¸,b±o‡ŸžìCâƒ3‘¤m”]ºàÊ‚Ä0à’¹í»5ˆæÙ£¦„[ÀÈe, ¦1ÇŠ“7”}ük´Ï÷W)ªÈ:ÐÔh!QÞnÅ«áÞ µ=}?àù{Æ{YöÍmã%¹'Ðzü’T¥S†Û…oüsà'·ÿq"q¨Q»Nžý¨Y⪗ý‰î$…+ŠvŒ¿³l4úP¸‹âMSÖ…†ó¸ Y ¡¹¦[Êâòäk@H•ïY x{0Ã%ÐvþDt +E$Œ¼uzÕm\‹fg2|5T}Û7¼a–$¸ Í MÄQÑD4ÅÔ/>v—Knèä{ãÔ°Ú³a?ŒVe»=óÞ–¾ÛJ;¬r”Í?ÿ.NµÖé˜8ï¦æ ’"ÚR^±lPUÃMú]®SÌ2=ò“°ñª! z°[ÿÛö!v¢{,pö¶yö¯åPXjÍÿwã?ŽÓ®¨Ö’7N¦õÈÀoø­Œ/z¼¯"‘úzï…þ€"Š¼ÒW,͸¤n…3xÖ–²Œxã",Ìh(©m©X²5Ö]Ým®¼ŸëY† 9¬¤jl(/Òµ3r¨6!yÓcg£ô‹ÞŸÚŸ¡ ë` DÔ¸`.P½dR Ö]½¢ù,Cv­u¬=I°ößï"Ëú-(ô h³-*u‘Çþ‚Ùû‚‰²¾Ö!ÕlDà×Ñ°™O §”½>:D^5Å6yÍ3ÙZêûQô‘âI$#cËp.*°ÏÔ0û8::o3ˆÛJHª±Éijù ”ì +zÿ$û[ÉõMÌÿqà‘(kI~¨m*´ {ÌõUìº`©ñý*á ¤|ǧßð=‘!‡Z#ã‘`¡«.«'Bayc8Û.Ûè8ŒÇ‘²¿±¹l‘P­BÄ_¨!RKâ¹.o<´-!뾊›)lå”ñ|Ð~u—4? +eê-c»ÆŽ%¸É¼ùkrro l(ã•Þ² XÓNo`pSÔAbò©ò™fX½J`‰gLDœ]HÛ¯÷@i{R‹îȘëcj`ƒmô´Çp÷å. Uý?_ç©wHä¯ú%)Ðʤâz­ŒÀ8¹ZŒãÌÂF=\0 ãwê\›‚¶Ñ¿¼³E}Æ5JªkÒ0jÁ,1 +vkBk[X¸WŹ#‡O°É^ƒm +8X—ª38 èxã±Ü@ ¯0wܱçJDhü¯7Mýue)¶k¬nT²/’Î%¡8«&^8¹ŒCGï +˜¶Ã?QÓ]k3r1S/¶ߨRJvèù^6/k ¬kh¡¹µ5Å·nýˆ¨á>ú´YFsuŒn/2Â~H¨EñÝî4L+ŒèË•h¸[ÔþNeO;‚o}í[œ=$¼IhÏ8Žy +lµÚµÇT~ŒÙ"Ï@TPLºÊÿgímveY’óÊ'Ð;œ¡¤AÉÿÝcØ}!(t£'-Ž +D‘ hª‰M ß^¾–¹ç>7s/ +¸,¬³Ã2"3ÂÃÝÜìû cd´½ÎUf„ã—Új'sÖCf?â?6øqÓ–eZgFom8ß~›ò²H¶Ÿ¡óÞö]‚û`'¨©ßG>lk ävA|€TB¬€½Þ¾»´ÑC¨˜~ºÀŸü¾Zï\N÷¯²m8372¦è/6d3ѳnҪРôa‹@Sø„ ˜PASÌ´KF?YÔé^V%áñó +s8:­sÅÀ€6º‹¦0[Yþf: ¬k83ÍËfl¬mÔgGÝh[¶€…¨üÍ;L¥"zÍFdÊ2MÆƹ€rr”Ã_MD…ýyî˜;£d±5SºšˆòQmÐcüº`ßùÊš'‚AÐTX‰o«¯3²5㮃™‚¿#ŒMÕôNÄ ¾—ªE{2(?4©Ü«ã٠آ'Ùµ5j‹ñë¨ó!,ý¼žv ®‚" ö=dpV:<%ˆ¼~{…W°¤V9X#P¹¤÷P¸u@ù§Çú‚¬AfÅ¢Æ<Ü- +ÑbpUŸ—Wb9{„p!÷¨¹ glL›6¦1¼]ñõ»vŠüpÇÏð‚ì豫•G'XJ­„5;ëU妥ï)~=4?é^?G „·V¡ÇýkBñ<¥ +õ–èZ’ ï´Ù.áC€Υ崿1ßÛ¦‘èDÓß}û”1¢úËÖ¹»@D¹»£Å âwîQ^oq·R Àpñ®óeð2D4Ncã;”Ýyk¹¸vI$$@e`ç-Î`Ë•vK ºí  ¾(Lœ­V>ˆ°ìµ¿<ÿp<\è•ÇdPtD•´…Jú¾ñþï'xáplA*†,)xpˆ¨À.§2Ÿàm‹ü&¢àU`›M¨ƒÉz¬$ dKþ“ÚéáG†æ:Qñ€Ù}RH“£…ŽwDjü›YØÍç~÷ÐÃÝæ„m0'tªjHqÿãYôqâå-àôÜðŽ˜—"“4Q`OÍO,éÂBg¸ï„íËÁ"ÌQ£¢ŽÉ1fÀÝž,bRXq/(òe¥ü¥PIïûÏÁ¤xÕT BLJ¯Ýò䓲çãR|Xƒ"}!‹:O52‹¾îíû +f©‡`$9hTêW°éÇéý’dÙÍ„œ ÁÎG_Oš*åK©0ßm&&)¸NûM@.ë°Ž\.åÈ]OöÃÊS…g%‹Ù®1˜K%®ýBãÊÖ›Õôn/¡"³µ„÷€))ÓÇË ng£IEB¸B"ä®±lüzãÙâ“}VC~ÜÅÉÚ¨{Ð¥ÑÞfD^ûíûåwËÀ ž›ÌgïE•a¢”ñqpLk:·zpA’˜¤È| ·“jK)M© šsÕSQ¬X—º¥Œ,›ˆ*ÅBR$1M= î–°À­ ïä]¹ÉözÆ"¤¿ùSŠü)Úþ2Ìáhrº¥dåLÛñùbZæÝs†óïZoà +˜Æ‹J“Iæ`Eð‹—PÞÕm![P‡oØq¢ô +[ãûãT„ˆ!S¼w©£¬»†'…6'¸Ú >KQËlvQCðs¯mëâŸr +YxÖS³Ä ¢‚&*}&gøãâ‡2ê7ûÀ<7XIèÓÞH#öFœ*Ã*¥þ&'vô­GY}'7(gM™º\H˜ª`o'®i¶09)H¢ãn~¥G(šÃU£ð>Xe-ñW‹½{LÑv&Åg¾ª`ð<âèòÖôTÁJì§û)öÚ_Ë$R€[ãP—ÉgO™¬u¾kH•Y,›ÈöèTÇ…¤P©1Êöô]«ß…qjÄÂEè«wƒ‘0½›âSs¡T2à‘åyì5O5¡æÙ±q÷ÈÄm¤ aã<ÎÖ-9]ï—èN¬(ˉiG¦]"Vñ<ŒoŽSwâx;¿èàâEs}…ºù¥Õ¨åËOÁ ´²©F.õžWvº£Ï*ÕQÉ9kJf,«Z³¶ŸâËôºU¾7ä}nûˆ +A²8, #O†¶gä–¬ +Ò–~3²§±Ì.PŸNQ“l?Vô?w¥eÑ‚.1h¥\NL:ôHI¡q.„¨+qüÂ#­‚(ˆ—l*\Åä¿' +ô |3s/ ½Ä|¢?™ìŽËuȹüBXVÜÔ–¸ÿ#®:Àz‚¾òÑìZì #§ ðƒ õØ'îפå¯n*g¥ÃÑ#XThh?7 +_û¥´Lë- ÒÈZÒ­@„½ÿUΩÀ8F)-½ìï/Þ8ñ%Ú óÝýÕ“æ¾é Þ1TEÕt!ýºÖb2_’ùÚ…y"2È¥†OmOÓ§8Z;ïö„šéæáê´Ë·îÇ6ð¯ +N¢™éWîržû1ÊáoàaíüÃ÷¥€šåEËëêRÉxÈw>“VPšŒ´ÊÐýôzÅyaÜí}+¡ĽW=ZÎÕVèåʲ+Âj<×›ÉQ–°pyÓÍ™þˆ:33¹Z¤ûkt`Æo°ÙcÚ¦1ÁŠ‚Ô:«RÝçbê¿_þ&±i¡ôåÊU@$à0§@ëŠ]ÿ~É…knäŸäÚ"<ÑTò”>ë¿Ëõ¯€ÔBPÔaô^-œ4y#´‰ðgÚ’º5‘€°¿Ô=ù^p@k—¼!´Lêµ[Ëü®ÅWCr8 iÖ(^$<³`]zoTªšâ-ÔÓn±ÆÓ¥¢`âzSZ ìÏVdŸF?žðÆË%¼ñ§ª÷ÕlŠ½¸ ZG8l¤Yè©ÝÍñ.½hVŸQü¼iæ›\#R€”­ý`Ÿ"²., /“0ê–ƒ°‹HbѦu@@W- –¢âíõöÓ`ÜâK”ªØ9䃵‹ }ÝTx¡®|4“p„³2šÒá‰4#òáal¶Ùyü8d’°Èý ¬A  ¨0@³\€´èÉÐFª˜oGb?uo_Ÿ#! ²§ºuˆ3ßFi†UbÛ’¯Í aþ.þðR™ö'—vFhM‹6Rû:?Å„5.biÖQö¶÷9çÄzwœd‘V¨¼ @@„h‰{¡-`Ô¨ìÚXž‡z|—½cR±S”¦TQ[áå=(³˜èëÊOº$Îaçjç{K;ΕX·±?yê=¦Ó€µ™t“âVÂœ„8·€Þ^ú§%¦E)!C˜t _qìó1²?ÍO£X‡ˆZ=¼K 7$5yž`çº/t½J‹ö0&SÚSÆ(×Ýôu^IæÚ•îY{S»õ M[š® 0C:‰\Î T)|óXãÐa»© )RÖƒm‡*ª=تÊ@I§p¾§Á2ï($RMmeR +{S_óA0̓šìÿZˆ·=ªS@?cµG:¥Ë/û‹ýûxýÜwæ cˆnSÒJ™ 6áG‰tDh KÞ_:7‘º8T-ft÷²R”íF °ª;aºº0ZY§CÜÛÔ-âгÀÌ4×þ‚­-¨á½FJ,Xìbé#7,b·÷èAüøýNÿ$•¨“£Vßã€'Š©@–*h´ÓpÚ¶‡å}ôrpⳎð%õýÓ€#**×Ä&®¤k6'~‘BZußg„V•í××羘^/~7åGû„èä=‚*ôˆ;upIФåÉWÄÃD@œú +q‡³S=Ë÷eªŸ¯ú82-é.ùc•¨f¹¢í‘=OÈ ÀD‰€(-cÖ ð})¼ä®®R(îùIU=ø ]Ý(9Ñ ¶+‘ýð)< 3´jœ¡ÞZ#ÞÑ{~¯nâ°‘ÍViàA,+dL¼3Q†ÒR¯u$z³Ç…êÎ?æ?"w»¨""Ý‹Æ òœÖm€· L§kŠT¢EMcI} °â1CY[ó«%ÚŒ»ò+@)@\?9¨–Q ¬W²Õ ÕxH[#¹1cÆ?£‰¦a.¥J£ŒˆçéóõÀ© $DüÌÃ*Å€”Rá@Òø6±g·h¶Ž 1®ËøwÕAÓˆ¤ÅèË"#ÓMtwr 𾀯¿¼­x­3G±,OO¹g¬±6]€¤0›QØé9Ý.S9Ì‹KОVX'Ñ0SH®^\—F_§&¥OEÞër +üÕ„Œ8Û-rW^«.E‚¾ð‚d:^ ÌðÕŽˆæÌ“†Âü ç >”Ž¾íuXGöLÌŠ/qfâL +p§´‹¡`š‡Z)Ö +¾2ð@#zZ€O¢qŠç¸µ”Æ9ÙÒøE…ù U+ó¸”ÉÎÅ;Ì ÈfÍžÍ[¬%ñ]˜ºw3"îv›éRÑŽðÎè01O9‚SŽ¿XE¸/«“ÓsY¦µêÚ2|Ж±:LÓÇ…½ÜKòyÉÈl&»9JÆruYQÈ8äL=’ÑI¦[Ìl˜-Ü ¾n5íKÈA"ûþ&ƒ +¹Gz”,€¨Óôi?d]´hŽ4úA«i5Æ~}¥GM£œjÊËníÕhÚ3>gláM—ÂàŠ-ÿÛœÅxYø©ÑƒÅ RÈ}hN¡‚3µÄKÉæ-¶G¨?¹|-DÑá&ÊÙ.x0Ñö-™ÒiŸá8œ¯g#ª ¸ÕW +°iU†ó^(ð€:RE÷À4z6uÂ}$o,¥‰úÜ!á—ík +?¢TðDBf\\‰‚z®JëTÔ «_±zgCßûªP:ÑS\«ÇÎãÖÙ¾}5æA"±*y[S8kM€w^¨t{ # ÷WQyîÁUD˜`š;ŸójƾgOÑõ,“aÊ™Q +Ä•Þ"’]M}ùÓGç+a“ô1f Ë—k²:®¢#Ö‚¢eKKþÛ¦x·‡× IÒã4îä¨û+ôÒA¬Õ"#hÁW!‹^¯`‹…þìÿÄT° -¹]ˆh¸¸]TfùÌ?mØK ¾]·óHñénË!#ñ =¸Á­Ô™8ýRµE+aû„3ÎWÚÏ­sÇš^Á¡[[ÝIKú ¸¦’—W*t@èI¦z`i¬ @®TÏIXc?ž§¡w¨/ÏÑN@b‘a5\éËUØAcG—,‡1",L'1É¥žµu,º/á‚ñÌ€=Dfy@n#šã9µüuTÈGص†žK¢}•.4b–4%/fð³ßÌ_W/A×Jvaa)ö;¨…kçkò‡ù+“IG*·ÛÞ¢ÛþÄ +µ—j:ÿ¾ä‘­ƒ Ämô¨Þu©-m*´]¬.,õqÀ)±S–ÔiEMÄ-óB¦¨ëÈRZ¼Úð Ó`­Àqco"@qˆìHSÈz¹ÇVÙÓ1î–.X³¿ [h’Gía¯/ë’ýa†LÁÛè ›k>SÎÆt\CWEr¼¡Û +Ù›x³vºaqx}à%ôã¼èÏVÛ +j—Bµì\ã&8Eò…RÝgsh¬áû Ö­^-v±èî“Ç=„éJäªÎgœÓ¦w“F‰aµ)Žù©•s¸ðžÇT´³2&|xÿjËð0«TÂ@Âq§ˆ½Î«™BÇ‘2ª–ANLA®šV”xק…Õßž­ŸxôQ•9¡*¸>ª{*"+†´TMÕ‚eTa Ú ëU‰h)$;GCd†Mž¦6€ÕC¸Q¥ý•lÉ»’ |q >ZK—õ#ŸvͬÑ|Ë+Z‹” ›êTþø,n+bóÚðÑzäÜ¢ïðÞ|ÿ=ûÿþ/šÙü•'úp‰‰ÿdÿó_‹jävTW3ï܉p—:<' Õ¼Ÿ;jYJ}3-v2Ѹ;%üµX÷%…Èì)µ]®±ÃIúIž‘>Owh ò’jŠÌs´³$ÆÒuöbg¶¾6¿¸+-5¶ö*ÜÄ/êÍëf3\@ÍÐM&5¡»8à&t•nSËK™u£³;„ †òÇX ×Úäã`Žù¿@aÆؽ2€*Hš=Eu6¯aÁC8¯F×wçç$¶¶xïgrJq!u€Æ0õ>°‡sÚ×W—cPøö1 +ÒjýÒóé$xí0 ¹Ïsó}`¿s +™¿P=´r²>%.¥,VŸK¹KV›É0zÉjZ±úÅY®RRÑþpô|{=…S®Ö8 ¡Ï¿§/Ѓõ˜ÑO™!|Þx´èTW®®3˜Ýçõ§8ÛÑšU@¦ïݨÕ|øÕÓ?Ø”‚b§Ó´W`Ä'GQ_ªh °oz Œœ~±6ÎH2_¯qfaOñöƒÞû±Sã„Boÿ¿ø©œ)ø§ kÀS¸õNóPw)q(þ]£¿ +;ªG±ê@n³'sÖÑ?ÏWØó¸Z%Á¼p€4…×um`¨ŸVRÄ_ CÆ,y›ÝZæû°Ý;RG4¦Û̯±K>™¦=êOŸJéìÓ±¤½žO5Ž¦ò’û³è²XÚQ±^¥‹o&ªK¬AP%ÖCý^'ØHúoÀ i+î³DA¡€£®˜ã’™þÕ•÷_Í0þJ—»ÃmÉÏ)î)‚M<øUà.òTfhܱƒnÅ;¸ìÕÛ>F㦛5å:Vä¦ 4]!¾’8Ýu€S¦š‚ép1`ʧšwâàØ8Òg°<’§Säòž.šñX¨Q–}¢(Ö3 ·…ã( * DZ±>Ëêe ÍvÄ÷°4Ës¿ Ð@K”ý¨èã«-« ¨0z߸뚔Ôüå\`!/ÊÍS8ÚÑé(`¢ûGƒBO¬r¼Á…W ¾Ū¤xqƒ¦ÿû äîn)8ªINò|Š¯­½l +¹`Ÿèd¤«û„æWÑéš–"aèP#fy¥ÅZÈ“aë—ÌL:’þY#JÖ曡àî€õ–9 U¶ÿÂÈúå÷Ä »Tó½M©! ¤.ˆŸ|Æqa²¢–÷ð¹¯^G%÷‰æ¸¶@dárz Ñ"¬ýQ4bq4ê]@“¹|µá!Ò•5;°aÐejëºÌö€ÍU +z©úÐÏ3»à÷Ó9GRb>JÔ·Ë">ƒòÏ:Š`”1´î9›ò +ÃyÀ¥:NŘZZ%ñŽ3Iï{D¤§ý´0 ºÛõx‹’ã,m-‘¯*Œ`ò?Àm(u.Yʸ=Çc7¬ß¿tŸe|Õ+S·h• Ù‰ð%mWw6Š^–‰™%§ƒXºöȬÁ¹TêæóªõS] ðEJ‚§DIfÞ`šF¼W%8èOSÄèK‡Õf-œoË]¼9_Bú+& €NÓÎÿaÈ’ÕHñfC*!œ +¹yá1;%ƒ½aGyzy :Ò­ÐÜß«ŒþµþÐ*cýiŽØ⣠õŽrÜ©Ó­J±&í¶¤Ós@„¡ó=Ñ\ýù×Á~:Ø{º­.º¸ä]‡Â*¶Ö1´Rþñ·6»ßL+¦o:Sé“È“+îø%1…?­Iÿ””œ¾Ýå·ßƒØè|E/ؾÄ)Vã’¬öð¡àc ›•M”zPÍϧ·ì:Ï<2æ½@ +Q lRÕÎ[9äPäcI‘nU=L§íQë +óvàÛZ +fv¹íE厕ÆëBhÎ5„ÄQ}èËÒóˆ½À®Y¶/ã8³ m˜NŒK/AŽy_ ½¬ƒÛw n~8¡e:,´ËÍQóŽÆØxºIÜrýö-oãù.¢†¯ͦþÜ÷ú= +ˆ7{îÓ<÷Î÷¬Jb£:¢ú\ñ~eG{¶®ÿSÕq—A[hÒÇihhº­ïlø‡¥>wktè\ñ£( ‰¨Êæý;D‚µ¥ 0馀Ì(m>¨DfÛä0¥úÕÀ@ªòj)XÑ…Þ4[íäÌ>~dÞFtH3ó]V”!?Õ¿St¨…h—¹³çîWb31Ã-ô¾P¸º•qqW¿# / +ä2ÖîÀhìç +‘qO:£$¨ø)§3¥VLw†žA‰OÌ ~Ч¥Äíaþç,ö&Ã)R“w]§ˆ¢>»ö'Ë ¾¯Š¡á"oAþL›Wpç!_íbõ~eÏš¨0¨,_Ý5f-•ÞU9KxŠ%ä ôó&°²ÜcV'-¾h+œ|£ë „¦ÓþšoŠóA&æ9Ö±í¬QéùzrP#îî¼Áô`{×OõƒË%ica%QæË…”g¹6°­ç¦“ˆ¥°ê³tW&C¢Š¢bö˜¼¢bôu Üõ‚Œ[çAa|ìæK|Ðp:¶×,#ý‡ìYºý—ÝÀÍó—⺆Á^QŸçø.«¦[Žšî,/]Sýtñ^”WŠP*;ÔnIM1HÊÑ—Ï—ßt•öïß°)#ZD¥ÕàŽ80Ž0ù!|Ÿ?é;(^V´—¼ñ½–¬É4÷X7»Ö¡›G-û8逜ÐzÅxµx°çƒ{¡z$©®0$;vLëù&¢Qiví¬×¢ä› t”}8ŠûÚšÂ:“tŸ°¨y&ašþvà<¸9Ï<ø«ƒKa×håWuïh݇|º"¹Ø, œTyí„…¬ Tã’šÏj~ÈALáRËfTû§¼/w-h²¦Q=[§ÕŽe<¥PÅÿ÷>;-žƒ{ÌðyݹFQëÔ1âéÂvuÅ“Þˆ¦¨ñJd*T#¨_¶4õ_m¨“îŠÿꀚÚøJt‡ƒÃÛ\›v®Ô —2Ð`v:ÐÿpT¶÷W;:ó_i¸k•[301äƒÊ€9åE¢õôyÀßÈV·~ó)ü?Ï.`ŸµŠ¯°­ÆËå`¿/±PΗ¸Ç,è«|¸Ó‰ªÑD;‚ŒAº`çš´àÐcåãÀÀmÐ=ºÙ¾GQBR66fØHPÑX¾9N®l'š Ú•×Ï?eL }V å`¿ë¸6U€-S×àfÏü‹¥? Ѽï¼'¤µß E“ç,š-Å¢¾±ý8 endstream endobj 23 0 obj <>stream +¤«ó‰§ÃÕ?Ç@vö`”°N›A½ ™¢ÑpÇ S ”°Gn¡ü¡Ògz…l9.AÎÍ%JñSÍïÍ×zÍ’æă½¥êeïÛ“¡a/nžסCKÂkl6D™™”ØP#—ë˜ó@9Îƺ§—ƒ½¹bxXÄwV>Ù(|hýÙ Ï+ÇxcN£Àf›šM¿»ëÞöí +C»P„xɾ±«dÚïë\ÄAÂÑ°¢®·‚«PÐô*êcaÌÝ(cvë>ëž.ô•§|>*PÚãt&H9ì¼CY  `Þ°nãîÝ»ªœ“/j·ÉûRŽ×fè©–ÖûÛúÓˆ~Ô¹ +-žÃl»ÕBé¡…²–åÄ"0é]K;Á ßÉ׊÷WývŠÔR™LËwÞ}ôqu eóØ‹˜‘íÍS"¢T=c›6G8n£n£&òÐÒ) +÷ß¡±øoþt«ñ€ñÙhoÿ“ x>øz¨Êr¦uø(Sï¥÷ˆï¦íÏ(Øœp&÷$Yt`,GösGõÐË_€{ /ZXS <‡¸D¥|xI;gcø“§S=žNàzäè¨Áw0ñŠ@ÒÉg/ªÍ*~)Ì(‡Œ>Hü2ÙÝÅÞ2Ë(Îf6„Æò ˆzQN9 Íþåì}c·¾öªŸo>>”1!m/õÔ"³D[HP<@’‘ÅD#ô$™j{ñÉWýª†iŽAù±ñ$hÌs +(£Î1#Bi#扰vÑB·Çˆq¯tŒêv”b²’¥’d)Ú+Ö ,ÞRlvDµkð³óë˜iù'; Ð5B»úñ…tœGðÞJµX DÙ¼“•öÙþÞ©† 6.á'¤„ +"Ëå*§^9/¦÷3øšÚî3PëêTz~p¬f·îFbÕáEÌ6æPW¢ö³Sº–,ú•L IKq P©Jp¡ìyàiŠ:w +ö<ì¸÷44mä=TðÈÇ ’ÌúºÀb«!k±\*|ü‘ô]„ +ÝÀVZM%.pUy¡š f;o) +‹1mShì–7†žÊcû…ÎœâÛ†÷dÚÜÉsÛëÜX¼¨]œuêSR˪QE¹í +­T +|{ãX¤î~DÀ[‘ºÅtôµÕ#HiÖÍ‚Q(¡Nk@ „€¼Œéu–ý)puàöOÍ*èÞ2Â-ö¨Ï¦´I"Ñ ²t«èÅvöQâ~üR£EW¢ƒ„Mî¾Â[u´ë/ ”ºr ¯ì0¼>íüMf²èp‰u* Zª.œÀÝÃ…lŸHîNq÷OÂãåî`õ‹ šp'AúÁ sõã!=ñ£SØb†½T>ôü)žª$U8öÁpÚÆ›{|ªIMÈ ë<šŸ8`¿5×¹4{vmJ§ìÎ"áuŸegG/a¯« žºÙ"–î- cݽMASl¿P+Su+Ûì®Ôüö@™–&©úK"âãS²R  Nüy™ÒL$Ã’j>§®‘¬tgõÝãï4¬‡#¼wNAãŠÏ@Ã_áì+ùSz]챕óg2¹ÞÚäÛÁõžŒ‹m$˜Ì“dð$·ÏCOˆ=õGJ‘¯/ ídŸ^)ʼ>ö>ežfõÛ߯é×ë¡þ|l†žCÖc\ãXZŽEK±s Ä=Úúè öuÿôƒA¸w©(ŽjýØê„. •¥B­ØÍ>ÁJ¸`#þä²¼£÷ŒcãX€ÀzDÇÓ­rü?ỈgØý&Σ¢’Í·ŸVµÄ3{ïvŠ:ò{³î‹ö€|Gy7ã×>#â¶Ìâãã(’‚¯£ð‹ ªr],T’ç3ÂÇqÏû~P|`çkµøZ#ÔX´g¢ôQCpåå¸Â¢ôý„pÑx À€Î`“„Ñ„Ô>~ `<õP™R€?ÁÏìwÅHëÀ @ô䨈-ݬJHô +*ÐÁ #iæÌ÷²ØGÄuL¿…ÌNgj*dVØá܇@’âðÝ2Õ:>g¿Ç™‡½áëlýx °NÂÇs:‚N!™{QVr§¯è + +Ü×9¥,ñžÌ‰~È5ƒ#7lï¥ÐÞ¡¥,«Œ£®Hf¨Ô8‘õéWÔ¹yq…î |Ï’ –/.}ãü»NLb*‰?CŽÎB?™r­¡åÐÏâÇ.RòÒRâ@Œ²ç(£ª-áö| +Ò7)Siˆ%+8õù÷'Ö¹ÛûÕÁ+×E*IîÙvAˆBïŽ çÎñúûëÍÒU-A¦l_‚e¡ÄÀ¾7%‰Í¡’Ã÷\D„rsnª„ ¿3iÊ´]=¨Bx=\(’šAq—¾=ŸSB…E½®/!Ù°âlÃÖÏu-oÄǧýêÂP!]>6/"Ôp†[q2ê=]9§Ë8ÒU@æÇm×Îhq³ýC´Hùæï3~BŒ¥IŽ÷ôòòM·V¯@¯ÕE‚JêîoAìˆGxÔknðj!¥ˆ*9;Ä>β°žCï,%dD¨FBɈ܆JÐkLÔÊL£ÔÉ4ýz¹K_×]dG€¼ xÅßæЩŽ¨%‰IE y·ŸÍ`4Ùm1Œ£¶¯ÝÞ›á1äßçíw¯ÓßgÀŸ¦‰ʇiŒŠ·ªU?´Ão áûÁRYò^¤õ l¾—AªÏ!™=þÂò3^ajÙMCµ«Œ°ÚTŽ£¶N!!SLÌSúÖtÛ Bø·æ Ñ…‹c¸ŸIó:5„÷˜íwÂ`îT/:[Ñ:_ +)9Œgü&åó›C êãÝë—¨õûÁÂ{å"ÞØLÁ#Âë%1 qßÖ_bã³bù†á†øÁ=à$8ºý†ê‚e‹„ÃDH®›û÷x“cŒÍÓÒtˆ1!ÿ›s­'Œs&yª­„¥%- o ο¹Ë׃òüMoYÝúòÁ‘BtßOòËgœWü"VÓ+e‡žÜÛIFÀå¶k÷€ý8ðÄèwÚõóï÷;ð,|X]2å/BM/›AÉ3„þŽ¶’BQàZ@ª0)vJ\ÃtÑzø(dd§þ„º›„çÓ#C¾ÆX|å9«}Úì†ÈhùR¶n½Bí€Þ.øܽãCþõïÓ(u3rþDÅHã\ýþ¼þ­êÇÒeôÉåÜ×A&e´§Á¥¡bª+2S.ªš·tx_j‹Bs½Ý÷͆²þ@äÔÊñ£A·G + +#±¤h²¼—ŒÚïûã¿g^öÐ_dý•'K!õŸ)Bþ—¿§šúãßþ»ÿõÿþËË?þÃÿþç?ÿ¿?þíÿöŸÿÏ¿ûçþ‡ÿñßÿøŸÿçÿËßý·ÿþÇ_þüOÿÿÿüÿüñ?þýûçÿô?þüÿýS\ôûOü_ÿðOÿðwÿüÿÇ}‰_}¹çõ >ˆNÿÿwçh F÷¾ÇI!s&ãF-\ ÷ìFú&„Õ®AdwŸÍÿõÆÞi®&¬yJtÛYfå/õë_øgð)·H,k/¸¾¥W5–‡Q4†DüëG!‘ ƒßø„”HhhL]É÷G|7})ˆÕöÁÉ×ÀÛÑÕæ§ï—®˜äžW'ÙÆï6ÄIÏà‘Ý[Ùî´%Àd(u¨éiXô]³>ÝëA¬|¿A5i†³'F,•bulµè0IåHéVµXµÛŸ0¯hFàrIAŠÏ)ÀaQžszÀÇñ§&“¼ò´IZ|h ÑQ"€Ð鱗¦ãÉÊìð‰X'Yh%H¿x¶Pxß·á4pá+»‡N:î÷.ÁÖÜvšïÈš6Àª¸xìÅH%¢²NËmO}'à‚­ƒ"škÉ*YX-MÏ™=•;íç]#ûÊ£G7ÉaÅW)ï±]óaSSrY‰^!2@äBŸw§aá7V˜ÑSÄ ñ›äEPná­³ç­=$vs1"ÈÇ{SVKöø~%f:"¢ö¦²Å<Q¸Aàkç<ãXè<ñqD(8 8 "Õ±—ûII-½€0B”ê{D‰#dÇʦCú]ž7Æ Ôæý°bvÄ#Îþn'Q;yej§éq^0)™ÖJ↰„s Õ>›æ}MÁìbJVUÂl×(·öæ[¸·“Uãõ*8ô³IG(£²¨SÜI‡V<Ÿ…ßÏè<œù›¨¶¨d€v*¸j¾#¾Œp]Îû½¦`Qaø\ÞÄ ™X¾ùÚ6Œ¶ýW×<Ý«ÐaXôïPòMOh­³ùCÞ„*.$ªtïQÜf €¨ú%*"K«qðD€©¨®­0ÊËşÐñ#S#•Q×ù%!‰z¤R¢D·šñ…Ý)-ÔrG·M>Öôx¯Ø™3õqS@!/?íöĹ/;rê”ä+Êò”#)=©–§)na9°ªwŽú4)ƒÛ×o"zø ï‘\®ùg:c¸Þ̧8ÁÇfçj& BdÓªâ á³ß FË[@Œ±ö´û!Lº&g1›bd=a––ÇèÊáëÀwC÷L°‡öxo. 8êÎ@(;ŒA1 ¡à ‰Ž(ê±HDÄ^.ŽËj¥3ÜbÍOÉâ Î4r㎚B>ÌT«œ<Ö`wª=A£ ’£j²Ù±(êñqÊw™ÖŠªVQA¬¤6½Ø4JÀ¦ˆ6„çn΀ +í¨Œ´M ¤¼Q+3C®ŒY´Dá@EúÙŒ«©{wßeo›¬fÍzÈ < T£Z¬bú¬…†ãƒ>Ä@‰r,æøšáÒŽÓØGa=FŽ¤ðú{„WZ‚'ÙÓTÿæRª½ç¡¥mü„}:•õX4J†ûW,¶—•½sAÙ¯-DíöZB§aSÖ«âi¼YÐKô?rÔØ؇Òßô·ýh÷$sdщ¢x±£P5 a5Wæ†'•8òä½T¬eg´8œt?Ü»˜/ «*e•tóEh×ß\öFø>œ`öKTê%¢ÂR¸ì oo‰ØÙÑ~Gö‡‹£`œ©®,-ì‰Ð‡œÀE‡ò˜ƒH§ññ"E[åÇGt„p?/|²¾bDªÕ$¶#9Αœm{èôS!œõþàG­« +N¾3d§1Så]g×Ä[¼O‚˜ptÈÊûÓÆé7¢»éÚ,°»UÒ‘]>\sù– ïI:M$~†¶˜&–Ødb´¯¸@LÀŽ}|"ÁV +\šXÏÿè*úìir¯özC!ÏgG\;‚2»Ò´jº¾Â +€3ºGOwŒ){Ù'.ÎÀiR–Ü#¶%*.Ö ùë/i¹p®4nžå˜#ðIÏÍö@;Å×ÜÉœB»ÌF |ÃwœOdˆj·`ˆyåM»âm‰/’T;ò,š7Ó›Oc€œ?ceD&Ý1;…K&•q §Ôum)Ì˨²÷ÇéŽEªf–{åt.P#XUÆg‚Љ°4áíQB“– +äS×%V : ZÊÓeµBÔ“¥Ãëi°!'ÂVµO¬´LQ°'`Ž#m‹j‡äÚþu”¯‹z õ9ãÓûùiè®X=¾@Vêïà¿ ¨ìT<²ÆG¯!e'eûä:²~£×Š7¼Çý˜V(ú6ó}?[ìí¿‰P²q¾žÄ¯ÂìDð´žµ4• É@T +è*!LÁWúÑ1@óÕØI÷¯ãîÕ²ý­Ám}ž'i#‰)íhÂW˜ð! öÖÔHá— øøŒU¾ëŠ·‡½Æs ‚8Ï> è„s-dHŠªTC +äÒv¶ˆûÝĦ§`Ÿ€h…•3øId'X¤¯$Wì#Ÿ„Í<[ Æ|›Óê‹g¿tº*8´±¶Uê¤e~”iј߿§Ï¯Áõë(ü„L§÷ËlU¯Ð *ê» ¤$@Jià ÞÆJ¹ ã¾÷2h{œ÷Äcêý$ õ˜Ÿ"‚êwPêÀ†ˆ¯ñ€DÂÑ ×çõðúˆ¢Í0}›R0‘ií¿Ã͆ÝÚdÔ£Dl@L䗮ψ;Ô›/ýxç?σYÖN(wy; +Ä ö{|-%>w¶G‚1Pê&ÿÖ4…5•÷b]6Ïs¦ ä/Õ¬ ˆy—ðå¦ ÂÞ¶læ`¶äDتD¾¸(à%‰Àåu%[žtëöÇ#ŠÁ¶ÓêLó†ˆÝ 7–¦a —¡9¦£¨ÆÔó›"i²wÖ_Œý½ &à|BIpÏ'V>:½“ƒ7Óád"¹ïžg°í[uÞu’_ÄuØ®Ôo>L^ÆVŸ½P‹ôœ =­ +šKßE£¢ýz_æëgÂW*;c+À¯`Ú•A +™%&æHBö{7ü•x2-ŠYLÇoǽž +®“0ÔëÇ)h +±B"„YF|ÑæÊqäu¿‰˜Ñ;b4ï#ùˆª$d~) sâË>rA‹V?J;¥™6{ÚîH>îÇ®•íë{Ä}5£6X‡6oïA”k÷)×&t•ù*ý›?³§Úï{ªVo‰ó= +z_† H|88øvNEÕUh\d5Î÷/ØÓ®¥V€%®·ˆ¸c]†8(îªóÖÇy*Å´Îä­×ÂÉÄúÉĸcõ‰=+”±B['^CW$}OI㈤ÇfåçZü8øÄ–_J=>3å;³nÍaó=Õ4u^‹Ì#³Œ(L[œ”câYíá8þϦOPZâ ËHF\“Ԯϳ¤NrG‚G‚b!µ½’¼#ÞÓr@Õ%£Ð¬t7 RSˆ +"Gëÿ‹Ø²ýC&³ßªoÏóœ·)3£Õp×^JZOm >#V>4VµMë+{}‹jGª6ýɪèÿ.ö®`Q(oñ¶°ëö'í…•œ¯>þè ä=þæ¹Üu¹ÝÌ ÅØEÆÒqHE¾THÑõÓV¾zúÿ~$ïw3÷ú:o, …S—¶Õ·2É/â¼8upeåËÖŒòâ(Ü]¿›7¡@@¬AÅ=§~œMIÒ- ¾dŽÇÿÈ­bÿŽ­‡Ý;§•@ôž—ŠTl}¾‰@ପÆM»ùæ™Q9Ü`ú¡;0Zd¢Ctô°aè–ˆëþ)¸å××Þ)¸ \ýúTUí‚d5˜¯å†êŠ„ûwX8äx^ºU–Y š¡]ÛUŽs‘Ð‡Ò•Ú +¡ØÁ²®&Yï\Œ[ ÞÛZk(±íimÆ—¤‹WëøŒøÓm©ÑÓœpòæóEy~ôÞ`íõeĤbß‹ªºzá߬ŸA ù(`Ð^`¸7Ìgé¹ï´Ûï½èMÕPñ_02m¬Ï÷°Ñ­4€k–£Õ !ÑQ@©Œ7ѽ=×O©µ)C-!ÓýWÈ7qÚ3xÉüh¨¾R\ìZÍ¥×*K{9El”ġS;¤ÄßSÛiýáH·5â…vö_h84]ÕØ>jJ ‘qVd5 Œº7M±B¡§ÝÑŒܤ€r6DÙBÜQêÑÉ j:™ÄA‡{å›VÁé…3o±yTÀ• òöÍöºR·«ÆG>ß'ûq÷6 ß웩‹¼Ÿ`†üh÷€ì=Å^?çj³pG¶FàUdÁ—+-ÑóCv¦ÒOÇ—ªVé•kGHÓf]©GñhΉŽÂ©>ª4²Ñ¦þìž(ƒÂboŒ zÝûÁ¶òGë) 1;"Óœö+€ð(ô(ÏkIÐNŒ b(£-ŠüÄ…° æB€ð‰“áW£ðµtë‹ÈBXÉíÖë˜(.fƇ_¢ãɾÓUøÖÎÅb÷j™hþt›~rÝ Û¼ÈÚ{PnØN¤$s"-†@¾ïïGÔŠ”Õ¢OïçûÎó}wþ?Ùù^œ2ízb;{$ƒ}pœþŒøÓÂÙv™PûéÃœX ¾ µžYý©¡Í<ˆ#["P&âÇ#ä±>à p©;L¹6¢¦%Ú†óÔÃ{‹zxû.bÐìAðhügò}ƒ°…¦èÿÀÙ³›¢ëvf_l‚„„?þ íÎÐ’\õ÷ŒBF·À_#×î +µÚêÅÍ*"ÄÂýo5"2ÏT•¨j„Ó™vsõëJ±Z¢ÝU#ê!•,5,F`†]8ÔÄk0Ë~ˆÝ“WbWÍwa‹fÄ +‘átMömhƪ!¿RQF„ᳫì¦fFA¡†YšÈ0}î +¼q»ÊïE^ ç=ÞŸôÕrˆ^Eb³œqiäû}¿;o*ò;ªÎÜæˆ?¬ ]wB{Šüæ<Å^ËŸRÿ•UËÚSÎo"vzq*›ßËÁÅâGÈRLPŸœ˜eDÚ¬7oó’•I?eYÞo¯ ÆÊ‚áý,0ÓÀs.™ò5¾$E´=Ó²!ƒÖðiÄ÷ºÅ½®·Ëýmê§ÚØ74ADcÝëÆŽ*µÈÁѸ8áƒðB&6R ª¤®DÈ\¾ŠûtÝ€‹Z=Ìdò<Ì5DöN½Nz&“ùDçˆÓD—S´“ný. ª8ï †¤BªÈfáGT {{q/ô&¶®{UkßD Nê]¡Š{Š?£28‰ŽNw.¢¤[½¬_Zp}(mî÷ªf="ôÈ€-ñ£®9Åì÷ŸúÍy`.ÐEÞc + \Qø´ý[Ö`‹ +í¾‹L"Ðo.5³³[¦ úÔœ¾@>Ù‰ò×Aõ9Zf ìa­Ò&~ÌgÀØwÙÙÔUVùŒ*ò½‹™G¿¦@öC[Ù¯o æ`bž +õ^~"ü ýä""¿0uÔ¾àX1`Ö9(w†b‰û#½Ã‰ ¼#0 ꌃ¢ w±gZ¯ MGÌz¿g šØŒ,¼>)U(‘Í­#2 T)>Þ£Ç{رŠ7‚REž¡Ò‘§ˆe‰Ç)%M©xϨ€44~ô4wyW…úïŸ5Îþ&‚Ri}aO}0‡¸Fò aïI>žie²Ÿé¬)à±tˆüŒˆ{ÂŒöjòíyÆ!ýÍ¢ +4ßODUVÀ¼H<ôÎ.6(¦ÏuñÕ/w°¸›*iý3êoÎ×›±£Ö!×VpžíµtTdEí½8/36&trò~aQ¨ú&¢i/™Ð ¾JP¿ŠBYÅc’=j ðÎi¥U&VöŽH„wô2&BÕßDÜýDŠ°‘€F¼Ïâlê(PØBê¿gæ¿=]m¤Øqz"ý4ØtsÁ¬tÈ^äõ@.¡ ³2Üh<‚vÒE­üýïÑFiQÐÐÖ8ÑǧMù4éŠÙ¼’hoò‘ÏV^]êöéÙþ3H£‚ýýU•%GöÉt±HlðÕ€"/¤X1ɪ§%Ð5W˜¨ó ó’RtŸ¿ºS×áÛV4PvpùÛˆ¿‹ ”èÿë]ú¯èæ +Íò:Ácd.²ýÞyù1Ùé‡ÄtÐ@fÉž3L‰¨î»ƒâ<”öi†P®!BÆʳ£Ç'7.Xp#(†îˆz÷ Ï<ðR:Ü"Èý/0 ô‘À]ã9ÈÍûŒ!w߇|¨åh`1HQmÖ1«Îq!ÐË ƒÔ®™QÎh{Ó¸ýîiÄ$SÊ_#"`~º]E ’ˆBŠÁ~ž+¡ÍÚôu¥úØÌA/òD©Bnœ"ªÃI×òáN¿˜bMŸd¦ýS¨õœ  '…PËÞM)í¼{´!ONÒØÁ ðAê¤{-°ÄøËã¢-¬,h˜,¥Õõ¡£@Ð0{±X±?Ö¶Ûä-ã+áh"Šî(îyçÔ¡ÔîÚB-¢Öd0ªì[ qÉÓgăåa bÂê‰h7=ïu0FûñW!dû+¢G7ð”©Üq ›LªI¡é¾Íø‘1m¦%÷]¨°§œÃÇýöHˆ_À‚Ýê©`9°1_IÖ¯þ±ÜoÒÙÈ"—âkYrËÏd,«E#„é‘ä–~"Às›Y‰»mÙ´nWÛqýË ‘sÞ@f 8–ƒéŽª)©Ógï:ƒ»_©y÷êôÉ¡½Ò'çº{±Œ ÈÀ¹RÀì_YŸpýÄV“ë*9U/Ï¢J[ÿ{”={»‰àïa´÷@­fÓ•Œ¼.Ëø¹¸uè&ëaÀîô_‘bCèà?¤#T™\t½•Ÿ´ 0Ëù¢2)"Šÿ¬=J¤2†¾‡ù%B¨Á5óÐélzºGãÇŸ&BKÓö‰5+\˽ä½R¬†ûñVÁȧÙE˜çJŠÄe¾!A¶É-+ µà襦#лQiÐZ–“dpu-ØjA8UOEf +RÎ\ «ŽÀ]*'ü9.-cx·t¼î–¬+X…O…¶kŽdZ ãë ÚÔ’üHË2)Em‚Bn÷|r²3ÛŽD™~‹ Ö_~žv¼/Úý¢âGóæQœˆ õ8Mè%ÍP}?2áD­Ð4í«<5vù%l2«¢¯w@Æëùé¨ò Ⱥ‰ÐªQa¹P:vZÖ‹÷~<Å(Ç}­”–ºl@ï|ä­{- ‰¡–Hêß~=~ù_IJR_(IaÖî–üaIe'F)éO¥~¢*reDÉÿ.4Âáå«HÐ=Ï@NÒó€ÞˆÇ̃gJ[Ò@h—·ýœZyÆŠÆ5­^¢¨GÄyr¹€Ìa¤$ª !êÕOà» ­µŽËò=Z!4³(‰Õ +QQáâï¶ÇŒ¦'ˆÐ våØý0Ô”XN1ÃR¯ÀW9X®ZèHº§ -)L—ž¨4[çÖ–˜{IÈK¸µ»½Hw+>ð,#iÝ ÷U5‚_×É÷Š¨œQ”ø’B_Ýóƒñ8$"ä’„#X‰Ðp‘_—t8¯+í œQÔˆbûé0n=®t¾'9ðce&Ç/Ù™ep¼r[D øLú½òÁ:‘¶<ØþaŒPKèGÚ2 ’ß”Mf“åyhhº×«´ñJz¢§e™ßí\1Xê,°;hãÒlgg%à‹â3Éû´ýÂæ Ñ†Ék%¨”:QÆÓGJ±Áa üÚ¥öƒ "Pìo—ÿ‘†^J…†  ¨¯*ÆÎ&}’·gSgufðùÜ+íû9,Rþ‹Îפ“¨"5¹©¡ …¿ ²$›¨,ág›•BTé_Bk EÜR.dïICÏ[R|.Ÿß#;'4>É ÜM/|yÙ}”ç#o<{nü6郃cl!hÖ¹ffàØow¥ß5*#.ì·“ª3ãFúI°`B­pÆÖ˜y­1ä`#TÊ“£Óü@¼./.×ç‡e¥=wuÁTÑ×aÛ"‚f™öºÎ×EÓM…¬d7Ån£¸± s¨øQí»ÌÎ/œ†RŽÛ«%¡ E¥M(«ÄÓˆ•@>€‘ôÖªSñ2gQmYm¾ §•¸x¤ÑÏ™÷YZc€ª Z™š+- Yˆ¢ìm˜«h{‘²Û#NáoLñ½ÜÞmM—œ^-ÀàáCÄýuÄ«HõýÑŠ§(fDŽˆuÚ«£å"%nQ$Ú;‹tÁºà Ñ âæ—(W i'* ŠÊ<²¤µ6›Nžg@âY÷¿GUDgFþ¸êh/þNÚ€T<9D¢èošû ¼‚eß%`·{¼ÏhHµÞ¨™+C ý‰ŒòÑ‘•µì11sW–.Ž™lý£JÆB$eCì(½ßr¶“> *€Zº;—>ÀO¢ü·Ôn"¡Ä¸Ë¶/$òþz46¡‘ôá¡ 'ëœãxŒàï0ñ²º‘ƒŸÙ¡¿%“&5 qaZ†¼g9âÖqûVÅ8ÅàçgŽôa‰`ùóbpF +ÿ\¢S¹¨åª…œ?¶§èØU%‘¶óhœ•¿tqøÍœæ÷ÜÔ°·'ÃeºæçTã‘;c1ƒÂɬ£’Њfúƒa›“Â:ôT+žÁ¨_–ÑÏ>éU˜x,€'i‚ŽÁ%åô™ÜQx²a­…¦QALÀD„箈eCÿV^>„k*rÌÅS‹""öä åSáER®È|„HDù‚î×öàKq%’j"‰ñ]x÷7N¡ËùXT5Bl˜ùñRA¢¥3«TÜ›¬^Ù+64FG@œ€VðâÄôÛw~Ò´c"ç+i —ÔöñQF§Úé šq:¶¨Ëí /•—¡Öª<2~p æUó¡ŽßœÔ3´ÅU ¢Äì`Ó +p€P€°Èd3þ¥v¤Ø׎rH”’"jF¨&)MñÐdJ'‚z°• fz"ŽŽÄ¢ŽnŒˆö>BÝ5E•8¢x›‰:§Yá=WšÍ¼t%|áL_…ˆÐN*XDe¡Qr¹t€ê^OЃÔˆMÛ^Õظb[^Ú¯k@PÄ@à ¢\íJ- R%Ô ^PIàCRl4.ïá @ò…/Z@}âØa_ä› øòÞO†6i"ÀûüE×k£Ñ’a½§±?o¤?€ž-…3ͱ ÷Êé¾t5+ó]•™  :¦åèzUZ +CêVS%äaïécµ`‘ƒ¬ƒ>”=^YÝaÿ É2¸>Å Ðn^($8'êÄتFéã¹ %^šÃÝ ›Çìë#ˆ»ñ¸yéMk0ñG&þpC¼Gù©ŽÙã¬Uî…¨IÒ'eÞ¬Qšˆˆ±'¥ª§f-sejAÈAóã=¦üÑâ+¸?Ù5.­fZ;OÏùÖ”l3ù½½cúAÞ"Ø$d)Ð¥`´‰-*}nœÈè¨ÄÅé¯0o…d]„Ɖ|Ûíè°TúD;\¡g:¦ù…´dY,xÁ’qGùî´vÉ+D”ã…Œ£Q®Z6XئÃÞGèS ºBë¦B5áJiêÏtcf™ç ÅÒŽ4ú´íÿ>` ¾ÿ@<½²‰àMk+„¢†¸p‚¤“ã¾eÓ$ªäèÌ5ÈAõŽâ»J9%j}=G +¨yw="Ÿ`¦0 V¨ª¶u¾°lD™h:¨t@ù¦|m}ÃÇæ¸VÓÀÑXûQ]<ª©îO¨bÑÛ@´Äý9[Wîèž£Ê Ã:îK¤î}Æ^p¹`=/v_¦ÙÞ£4Y +Ô¶®ŸA™®zt·QÖ††•%ÇÊúb$[𞇄LmêMäw•‰¯\LÑ:µ§j¶Ö¹C<ðè`I®E·ÙñQƒ‘‰!Žšn‡Rã㪣ç¦'"ô-º \#T2O:KD„.Åž‰úi‘%ÛIs€PýŠ«ódïðôD¨†w£}*ä«^E £®¸PŠ/»îÞYxáñ J3Da¨ŽàwÈî‚VÌ 2(ÄVåË‹UŸðœFjLz…[Òqų³åf«GÏ’ß'eŽ­´:aÊ ïá~´ö¦V—ûþ s£7‡\‚Ù–BOŽÒc/íÉ]VÚe¿KòƒýQòÒ‘¢ƒµÓPæ=“äÈ J DX˜”^…¬h>ë œfž½sëz×C¢bN¨‘·Ð$ÔaG¹Ù"¢€ÕGì‘|È€b-U»YN×_‰Œ~–ŽææëBÚ“'4³™Up©~*¶Ð`$²>bPüì(ÍÎÔªþ €þé:¼/ ÓѯÑÀ™®Ù›ÐÄ‘X¬aÊpw¤ØÁéA*—‘HÛD? åI°Ø=ë ýÛÐW0w™ÒƒžªÚ¤©n©0žŒ¬‡ûÓAFMAz-hKx•2Þh+?çÝ‚´øª~PX +€î§úƒW nMÌó®9ù$]¹S«·¹Lƒp¶ vÂ`Û µàð ·Žš‚ëÞ±TV~ˆ¾ë±‡Ä“NŠnjC]Ñ0¬ÒšØå¹ ŠV”ã÷|Úëa‚Úxæ-¢f™¡ó =ˆÎ=F±{‚ŠÏº¥²l%&æå}g²u‚Å„½s–yÉùIœ,‰à†Ò–ˆ,Í //¾»Þýnß:„›˜§ñÇ|Øù£ÈÙsŒÍ41'êï*áE .Crf6¸¼ctá*b)J“vÏfSµ }.@ÚƒêšÈ!™qW J->ZÁØ R¹GÔœ¹rŽîHÿr +€ry@½†‘Ú‘×ÛÙ9S–FëCn¥ŽyF¨¹¶demg´?Ÿ°º­bÞ!hî'„¡‡_øÛ(VÀ_ûœgDÒAåYùžr"dªì¯)`˜jUD¤çëJÚ㯒}š=5 >ÛçG9c ³_í¯¯åƒÇ›/…Á~ã +›=´®í ’K,-Ö¾›J;JJ¸ÊLö ïŒe¡H-üM³F’ÒÃòF ovÊ()f7¦fØ;°N™"´—Fˆ,6,8[õªŠÀnQUì6Ï÷w…ÙŽ°ëÅž(½(š-A‰²^I…sH­‡^%`³u„0Âkt Bï­‡±ö;Ö"€šcÅå¯þD#P493£ö ËP›Ú¡ÁN’!DX¯˜™¢­Í•ž " e& Yr‘î—BÒƒ°g‰¥D•Û„ÂP—ÔC TåÝ +JéqŒ÷UäÎØ)Gº_HKÅ-rÜ +ê~^ù%$,{Jæ ªuB+€F¦ö±’”P5j”HD×Ó’ªª1Óõ”AøfÍùº’¿i_ Ñ]->‡©ÖÎÛä=Uí²(“-Y%4õ ­´U\yÀÏ’ÔÔv§ó Šý—3à‚ñ#£’ÌuaÚ/½0s+!œ§!Aj[jp–¢)«Œ)*]°÷C—Êê–'ú®˜_ET&e)$*!òí2N Lš,»¯á ç‚:o·ö…ŠlÖ"¬”(“À‘M¯‡2Cäp0h{Õ &¡áñẖ³@‰üL›CəҲ"«ñúMÉ*éXØ>ë—@<´Ø ªø9@óv‹'{Y^Ò~TóqÞ>”z_™¨RdðËŠý©i²‰åæõJá——1•W˜Xfq¡…ï>r(3Î6;Bº&çŒ=Tf‰HWÿŸ!TÄ?„èbü’u@Nîß“— Ø ¥`#V|€M¨»¼¦K¶ãTJ˜çÁMMIz¢ê#cÚ©!· ®O©á ‘MöC›šwî2…ö㱆Ϝ‘Ñ4FØ“NÈk†I˸ËÅ*l¾¤$vW²´ˆ DùÉý!ž¹Üá€\C¿ KUtyûÒ-ÁŠu.2(Úƒ‡v@ôÃ)Ìa²;<ÍŒ(f8¢vñ7ÿ&p%N€ù‡Ö;ù…6l¨£Å‡éÉìQtš@ÈŒ#ýòýÑš`¿?`ÀxÆ–¿Ó¬VKø-œ +yoVÌVÈÏ+ìçC›Á®‰§Y)±:º>–$Œj»SÙ^ý¼ ²„J[Ñ‘uÞç•ewÁemq øH)Ý.¡:9^*ò+<Ù<*›Üh-«+¹oy ’lìvf ÔÒÂ$ ·¥aÁs¶oÂSÛ÷ŠMrØÙ-L¶Ú¤ÕÑú)'I_û·ƒí·OjµÕè·°k úë6N@tBóÛûr„ÁM}IJ¸#0-<ÐûJÉÞ/„$|ÍŽP§Uù*„í“…ÿíÙÍJG´ŸÉr™ŸÃ—ÐX¯HȱA8Ú*ÀÙ¾=ñºOa`—3+ã‚æ0„0CßÃ/iF6C6µwÓ‡&k.KˆlK਺ё­7N­KëghL×àa÷ýÓ•Ñ—B~ó:ö\Ùy+d—βÏƆ汓=“7òRݪ?σ”c´·¯s8( È­=…—…)Q‹ÑÓ¥TFjçÉу<(Oü7z¿'ðßÿ§ú¿úD~óñŸì¾s~—ÎW嚧†g”TA`ìçR Dé|ÎIsžÏ ÉzNÈCµ$#¨3xÀÕ)e6ÿã9{ ¶gïÖµÈÊî*f‹‡¢³º?ac³ùPuaozVHÑNô4ãenbÈ/ï ú’xðá&Šc‰Ð„'/žG-±‰¤­CÀ;#´{${xAxŽçˆ>qfæi¯+ÙdØQy0I {Ƨy $GàÊ]a©¤’5t{bïžTF(BáÓ,ž¥]Z4Ï•z×>yO.@A8†iÃÖ©…΢)u–ÙK™£dˆ˜»¶»ØMØ]ƒƒðA°åhÊö%×ïdmíD±óaß |ï!–z@c”³ª¥ìˆ˜ˆ®µÉdŠ¯ „Æ”ƒˆŸ‚†.RûDˆÇS§d…ƒÈ™I¥ Åw>9<ǾÙ특Ö¡A7™‚Apbkl¡ÉÍÇÑeØ«ûAì5Á5{&ÖF„~e··àŽ}Dx¥U¼üBù>?ßœÆÚ'‰wF2`!äA 1 §|ì!œA– È +÷2oAÔBXë2²ÕìÌ`T>ŠlW¨ý«ƒ“ª„®ý=ài'”½uºÉÌ ÷Sè««•Òþw+«Zû9!NÉG…öãÁ«oÔ5mâ âÀê4m®˜îV‰³¡è8p/ ˜ ,&Ä9c›Ö«­ˆpq‡bðõ ¬ï! 3Ï×è(Þí(0éû<#šç;8xDL ü³o»À‚¸¼.t{x‚.ÅÚŽ˜v^å Æ"NsjÍÒã.ŠùóÃÝ“õ=ùŒ*GHœ[”,G6]§,è¢.ªc¾@} +’»R¬$}3Ÿˆz"¦ªÝÇÁË7Ûìý½”Õj›­YËèßD|7Z?£ +Ø@=t:‰ÞŽ+Ö+•g<ñ †+Ë+ïÓ´båáŠh"ÁƒBÆbd$¦ÒÜ´gy¢V +‚Š›Pë]¬Tã  ŒšäÎûµ–mÊNþhIŒÌjJz:DtªxT±B ¼†§k+÷Dµ÷¾MD eëa–¥{¡¸ùqVspÿi„9M€I™2ö#! ­c #C––ÎUÒX§±çÍîZ@ú$ÓwF +ÄâZùU[CPl´ç‘Úí¡çñuà è~œ+Z´‚D½­SŸÌL-l-–j eÒÈcØ0‘Ç”«ËB”ª0ôHFDÑk Š"‰ç±ë/hlTJAú¬éÃßÎÈþÕ̳ʓyþ:z½˜kðÌšÚ΃¿êb]CIòYÓnÄžŽØ®ŒgRC a±lkäç²£³™P·Zu8Þ!EôÛ)Ìá#D˜ÜKÿ–¡? +Vl#àô’Vƒ¤0fðû%Kêcæ£âLfÑâJšvPî[íÚóAÜ =Â)¦§ø¸¢Pz…ætž$Ô6;‹vÔq±kØ€^tЮC7’¹®! †ó4—1©Èˆ@6µµPSa%P/ˆŽ%DcÃ_|²O?v‡º¢uøÒ_æ¬ÖÖy“`ŠÕ] Ϙjæaêh/*›í°o%hªº.9}ûû`² «*;»VhzYîÀÙº«³€qíQÍ;žP‡uæ<®Nä~áŠÆŽ ÀòÊ”F/Õ ²ChŒq¥¨bÉ;U ¾ØÍ÷r#å¶ÇùøRxÔˆ@M™¢=ضˆ¤ºßçñÿQ¶¿Ë”jiÅÍPv÷N!‚¯ÄR= =òêo¿4¿üŽ/ªup‹vtBrB_²M ßÐAëžDüÆÁ‘%Ԧإ¦õ¸xí6©6IÁ]¹Š¯ïÔOºj½è&:‰í û£A-Èѯ”R—'JÂÓó¨S¯j›D°Ð«ãí| À§÷ð¢ 04TžàÂëû ÈFë6‰! LR€ó„ö«&¿uÉkûˆˆ}ÔÒLÖÏ(vCMô ö°¤ø ¤Í"fÈkTªDàekØ´¢¯v‡'d-p¼‡³C”Sú|3‡õ™sÅ8òöD¸Î'5÷âJ¡âƒlÐ ="O‰È·Ì‹Ý„Ûu¸’qš#HS55#lºø­4žüÊ"‚2íв .¨ç9¤Ž¯>™Í©‚3+Äå+G¡YÁFu U Òb9Õ3Pyàâõk»®‘ЂÔqë!é¿÷[ôé_ +…ؽ@ƒ4 +Í![yŠŽTÀDžÒØ;©~‹ÆØkÖ7ÇEÌ[VMtO!â#êúvFN+¬CwŽ g)œ(ìÑûYÀ£³wåÒ€ß"þt%¡UÓQ+íßç@4 ßÅdÒ80 +)¿$ÛDçQ¸aÙ{Øs]®qëmZ5¼-r ƒž ©tš©¨>²3Ú¬Ç"]ºWj!H¯ð±ûÉ”9E{Éȯ¢ÍÖ`j„´¬¶JJF§§ŠÛŠP¶ª€9ôg$§Õ¯>ê[‹^Až‡+Þ# +È°ç GFjÈ3¤QéÖóc$×ÍC3S<ñ¹ãlá +ûðÜ‘¡rd´h~–"~t3ùûÞ?"=u¯Úýœ7ç= +¦È è‘çÞwËå{{Ü­ ìâIÅ›üù÷˜Ô®q6é’ß?mò¦¨e¬D#ѦØÇðÔ•ì¾?£0¥¥Ió„«©²53za¶TI”!«wE¸Ñ¯¼j åÊîiïe'üCÍUié?ž´2HÖ°‘f/7l\ZFK‰t‡W^¶}~ð+9iÓshW(Ççƒ +V‹Œ)!(.¥P¨L¹=ü¸¥qj†¸túÊYn÷¾h\²>(®X\@cŸ™G!{ÚM´ªNvû½¬Í® $@ÝÇ L ëÊëh™°Ò¸,ýÒ½”“ãøò3‚³0c*=äÖe_–¼"Bª>Û7ˆÓgÙÛˬPHÔŠ#ðÐÈ}«FVKš§V4Žúžäj»¤ZhS.•Maájip`Í%6IUoº0£HÆ:YèrÕ¤#ñ[¹Ðï¹C‚¹DCf§>ˆOu¨…êåaàpõ1pd-GŽæ=¶V¡çRÀhý¨8œðè’ÆšG­Ån4dš–l£åûgáNFبÎð¾ê¡ <éÈíà•ʤÕuÆ6ý@öëûÅ#ÇŸ!™°RßöIG3ºz´æJœcÕV_"Os¯°Z$‚ {/0¸#Ùšú…Ÿ‹0|6ߊ½€õîÞÛOì6ÙKÔ—"HABƆ +·P‰3”1èëí«)¹³#µmŠæ(•;-Ûà [õÈuçE±ý=XÀy3è¡HI3T?¹B:¹¤6°œTÒ· ðîLq»\ÅþæX; +‘–ˆRÆfÈ + 5²a@?ý4¦[,±)ß3¦¤È‘ö/ò˜ÑוP"ªïìETkÏáZwe™z°,áp®åäoÏŠâïÑÙ¯Bâ–=ö_jŽb<‹šƒº<Q{Àd)ö}ÁC +y@ð*r‘=Ç©œEÔáÅ%¢6Œ‚ìdßð©%±58ô]q}g¶÷øôY>#üþÞ„ïQ8BÐcA’dâLÌm£.òXRvˆz^j£vgŠ·ƒ$UžC¡4)Ƕ‡!mî±€fQË'Â{·YbÊÍíyqË5Êãts9ÜP j+‚ÄŠ2?‡ó^Ï„Ãï€<Î훈ܣ+U’ùð«ððë(Df8ˆH!.ljcï¼´Ø7¹0‡†ºjBÚ>§‹÷qÇV(bðv}E7{ª%@Ùœ4p$h±2YíýåsÓ§—DòÕ¾)×t-\%ÚŽêðÃ+€êÒ§ñÅMA—Æö.ûçe+Ss%þÌqéã·çnã…^ZáD…)"Š (ëˆôŽÕ.Çu¤è1š)só;r: Òþ&å +š‰o ; át®óy¨¬…’&+ ¶‡Ãî)^äq^äQ?#â‘´p0cêJë`2‰"!Aß±É:ÐÎ/J{‰Ì»O­>"J@uÅÎ\°Ï(Dx´)“H)<úFõ¡¹od(¤ªH‰´_æq¬ã?"âž‘q7‹ŸU‰ëÏó¤hÏð=|È%fy~2IÙG€íg~R™ó"/~„êM®=Gï¼–×&Ïu°½™^Ñ:Õx[ä¬L‹¡¦·-'Ï÷ˆû~§\‚Kûæ6©LGŽ¡é®„¹âžð¸€š5eþqÆÞs[îßDÄÍËH©ˆæóa>KßÐ +Ý¿Ç›ÚíHÍßá‘"ÝùŽ•²ôš3^çÁ9×–}Þy0·Ì×+XXî—Ñ㧡ÆÂÚ¥H¹aëÉSŸ Ïm")öØ[ãîqˆqøÊ?"G›Ö¢ë椟sø³!çF­Mn;€¹c¼¾®+ yÅîÖ¯ ³¹"y†9h•#¬Ôpä¡,‚ó¹¶Öèû¼5ï”ïcÓ¸µï'@¥¡¨p6ò„Ÿ>"|Ž#Ïq<õÀ(ØïNXõùvŒ­ïzÇðAÚ+ÂHTñFhíèeÝ(þ¾`4ì+#ç‹ÖÿŒI%_*õ3ouJW8{&ejÃ@ ÅEU¥?JPu±Ö÷8UæÚ<ƒ£ºÈ~ìÌUëèß¼?"Æë!ŠÕd9QÝU*†àÒaMäÐ +1zFÑúp™"l€O×OX—Íf®'2ÇñÈ«­_ôÔL¯m…¹Rá‹ÄRÇ•äÊlìˆà1¡$§‘·Õ '‰’<Žô‡í[¿Î=j§$†p.FËǵ¤šSÕ xÅß3U\ûÂ1Ʋ[ÔU¥)¸ÁõÌ +PïØá0 »‹éP›¡KÝ>m}Nû$@Êû^cQEŽ÷„Aõã÷ƒÒšÔ˜ÖUbÊs_Ä>÷P¹&c:ÅÕ-60”¬ýÈ"Òâhž©‡(å}Ný.¨Ì·q)§üy€bäjͼ(Â[Ô‡¸6Í}³0$·Âù8ô9°Ó,ïjéÌ©ÊË{ÄkÁ 1(»0çYëž"SPZÏ@ÂqÝÎŒVBn÷J¬¹Ýë¡í  5sÚ®ÔüNÉ>$ç-Q› mêú{ƒD¨qF‚DŒòš›Jº/3-2‚4ì´¸{å4!"t‰r¡øŠ]b#X¯ªÔ™_b»cXâ¬ÃÎ}¡É„SäÕøÁy@ç€?’Ýmóu€±ê-@áä~Š†FifŒ|ÚtÌ+ÙÝ!2Ϩ¹eUŽäH +Ñ®»6ÌÃM¬Œí3"Û†€í`¢éüÍy qÞªYÑ™ ¼?J¡k› auh<1TÿMÅÜtRÌ{5c%B¶CÔø-ð(wž1Å›Àñ#!~ÐÚC¢7¢Ç %DA±T¤o®MéŸ7H-,îõ¸ŽùOUaèm¡n±>Þçv¤¢¼Ô¯ûQP4˜·#nʶPÏGY¡îXÕèä›óô-¼ÏØøÞKp-5÷o"ÊÃmElòy*ŸQGô—1œ÷f\‡ ^O¥¦íà‹“#4.Ó=dm“kxÓª.[ë­9Ƴ­>ÓÑã—£Ye”ˆä ˆ9çÓʳq@Žx1¦Ÿ-€<­°|])43Ôl¿¼ÖäUóèÄhÏ•”||’  ¢"ª-á±m¿Ú_ /Ès;TáGvþ9=)±¦È)aýªÉÇ ¸sEz¶çžF÷ÔŒû»Ü]O5qD$é[ÔØÞ¬ Ðâ´8õ'åÜ^ ¼äGÐ;äXûŒÈ•—U¨á‘~;ϱùˆôWÕùmAH‘K)e3PQÒ•gt?ÏÖï#Q‚Z#ªéù°¢¨pKà¯ö÷¹Ã3©í6ûRÐ0•(ùОƒHdG^t)ã3ËŽ) ^¥õ_”Ž~u!p±Îƒ*+Uý¡Ž.Ë4S­’¦ÕåY(26¦`ÌËùµE’ùÇ\; D(D¹)«òÅYb¨‘( +)ÖA £ÓG*Ÿ mJËïQ¥Ä•¡£>¡bä³½+¶£2 HŠº* Ôl[žñEz;æ¶4±4˜šèVź¦62ïÅ–ÑNk:0•HµÄÁØ·”RG”üo»ÂW‘"©”¿ª¸r.^ ó@ AE#d_Ë&Ür:Å$†ÔShß²ÿbC¨yôZ±éØ„›OÞv*à‡8=\ÚÔך +5d +r…aˆÔpUö]îQÃÌUöuÃoÝëÖ»öÔ[Äj +–ƒ­ Cï= +=àCõd#r)‘+Àá9€¡]ì[àPÎûdÁ§"»v§U¯\Œ_ujΑT~ai¡?h4kAÐbiW¶Îs… Òæ2>>wE¢sŠPYsêO÷®LZS«ël`Ä¿‰¼ûsâüäàÿ³<üåÉ,ïÇ/‰öþÃ߬8üÅ¿ùñßþë?±òãßþû?þñþø‹÷—ÿå¯ÿô§¿ýÇ¿ÿÝ_þ¯ßý§¿þ»¿ÿÝÏü‡ÿó»?þßý‡¿ù»?ýÇüãÿþ‡\ôûOüûû×úÛ¿ùݺį¾Üýúrÿ™ÿ… ó×yàœþô@_¯üºégE†êòwN#Twœü­÷¼3 P®^?þ§þ2’Ii_ÿ¦ +UöÈÊô«@}+Ré_µ{5'EÔmK[¦Ò—…µ:«{ý·(¥DØÖ®À¾F¥áÔ+¼ Êá9£1v¦ÊT"OxxØbÔRá0UmIÌï=ƒRÜïì‚Ê@vM9‡uZ#çî'€þR|Ŷ¢â.2ˆŠÑo#V"UÚ2ñËÜ䬜ƒîË:¶dö`î}þH¼ò'E¶«"kQÇÔal}è¶7LÄé sÿÚÕ«¹«W©‰èÝsP}ÉF4Pƒ|NCí*ϺbÎŽH’®«âëwj`1«ÿ£8º²ÂhÇ +¶€xí¾¶¾ºP… ÏÕÀh-‡•„aÄf@¥ýõTÇyÕ‚ìzÏý9‚‰+ÑZªH+lÁ᪨¶ºüÔ»{ ßš)²#zMãì¥[:4Î;ÎF^^K;÷Vá±÷nD¡lLV ‰ª çÚ„îóèdX#.àÇï™COTg>ne¼ 3ŠéU2ñ¦ˆkéF©&iNÃŽ'ÐpÂEÒ´s»èRÞëïÉ×› h†F ‚ E;O±wP㬠}ý ûðRROEÐ@ôlÏß9k±¼°•VIðõŒ¹hù1¡´B}˽ú&OÝ«­}þ¨žZ:íÆ5îTžÁGèF;‰ükA\34¤‡M=C#É÷^€1ÐøOÝIÖ¯žYf1"×B—jc¹cn(À·ÈÖiçãßêAŠãT®GOTô´oßæ|œ^ÔÐâ>zÚN5šÿøZÀ_:¼ŸQ +sr*r'ÇëJ o%ªßcë‰mîD§­A‰(­^A¸™ñpO(±Šd»ú§²w.3DÒ¡ªÓ#õv¤U<7‹ $>ã°L:‰†dàó»ˆá>¼ö«Ï#y"“<Q:´zS¤§ì VÒÍ2]…”Øä‚ÕM†'åå-ÀëÌ + 2xïßÏb&¿¦¸A)œ9tmš‚Ha2÷÷ˆïñG5³Y%V ÀÏÈéÁë‰Ys~Ç ÊÝ%AEG{"º?û’´`ø™ë%o4ÄyчºÃÖY#AðJx®yîb7óOÞ†pR8HEpˆ° °7”{¨¦©À; +»Ÿõõ·:¢‡^™J¨ìW÷Ø’P]5£ì^T›ìÝ‹Z©ËÔâ‘Rv‰c×0Î7»·cÍŠX“ⵯD-†(ÒK–3Ø™,g,ÈÚ¿¼k·",ëÈŒU!1l¼t(áÙæg„“h‰5â*çùÝy(¥ ïºR µÐ]†0TK¾¢vOҕÔG ÒßïÄ(`{€Žô:I{ÆiF?Û.üò4K“:A¨!u‚¶ùÛ)Ø“‹GQ ¹ƒwbB2¬*JìP °ÿ k†ÍXµ¸ +6˜Ma¼YaÙÏоÌoPkyç‚ɽ¬ +:kÑ÷é[CΩ¨’ÑôMýÇëô"s.šØÞ$6€øžý™7Ûž7œáM ~ô0ö•î€®½¹-p<Í-V·=ùô¥Š€ˆv…©ñÿ¶¡—€ÙOƒŠ·{¡wãP¨%x°ák k9,Ÿ)#-Mái'ÅÑËÕˆÂ>)„l¿ÛåœÎ3ÚÎ3°eð÷^oÕ©«…;¾¼PwС¤¨ÎœW,?8«ªëN¨Úº­÷v½;#6&mc+9xÞzš»dIv'´Þÿ³…>àê¹l$úf:Õ [h²âVÉÝØmWuN_§Çd– fâDõ‹¿Ràa[‰ôWWèã—‚8[t+Ê÷Øx÷Ih?ÍÄ+¨ñ—ä/­CÖ¯fîÐY‘»÷ÛŽ¤bu¿Ä¿ýŽü¼ß)¹,Mµ2qGNETz„èFýaGá,ᑬ£#MïاaÔ¯Ó yiÄ–Ï£ÛcX.þnÊ7hËí¤†ƒ®òéÎ}#J½ÒÜ9ûö´ŸÛÓ¾Aª½".éã§;__[IÎí^(ókѾ’…Xe§ŽLÎÀLþ;#µ¢q÷ˆK‚‹ +0e¥ß#²A)*¶S¹ÀoΟlèxù}ÍòºÁ4«Í(±áñrkFL~2Ô$Y?iñŠÒT”š= ¢NÚ€@YqÚÚk@$á<²†já¶!ÅnÿÎBÇßÇF@q0•tس¥$ÊTë”ëdzY‡Ž‘Ú¨ø µ¸=bgÉq­4ü¬€´ìŠ`§ Õ*¤wE¶ži€:¡…Y@í°Vú"Pþ6»»µãu#5E×Xô1ÐãŽz2×_iAc+ôêÕü±ømôZiÿÑÐx P»ŸÏœ_í¶@9ÚWúˆª ßU’?5ÍÂFìT×fŽ‰’/N}M]ùªŒ<äÊgD~S YY¡+RoŸçÁ½žÉ–f“§)IºÖ9;"sÝžGñqlüÔRH®J7hUe§a G›Y›YžiÖzVçäœ6.D ã ‚7Õ“³cïñ–³hÁW·¢þ¹ºG\…»×n{[<±ö\ÅBa‡âm‡¯n7ÄSÉ/ÏÿäÆÎE¼ T6ˆ¸¯\©É.ZkŒ%ÅhžœXîîLªÕ=0Ÿ+ÉÂS˜Ê¦)ñO¹xjÀŠ>î²Ø¡ä7ò×q€môälŸQw”ÁÏý`“ê‡jÙ›e·ïÀ›®ZÑGrýÛˆ™µZXÁ ø縿9Ïy(÷qk-¨”ÎÙý||`«“×1€Mø& +’]?J(LÈXK9m¬¤[ @5Ì…ñ3fßPÂqð"gEýyŸ_¦Bë»Ð Ó,Ã` ZÆ8#¦%lªÄE| ”³ ìÐýäÚö™>›‘´Ô ëFÕu¶µ<¼Ž¨ÑˆJÕɈ7&ÍÉ´â8§¼~ÇñvqÑ@ãô[“ñJ‡À+½Õb ú²Û[ +…§²»9²»àÌ‹ NJÚ)†6¸î¨¦8ô€ýÍU驽öhCßéëV¢`yB£Íñq?Nté˜aD¼™á~Uá'ëÍí¯ÓSœçãöÎø”»z ªÑ1þM‚®zgÄ[<ßÀ­§ ³ô¡š5˜jט¨¹ó([ºëõè&K©ïJ““ÒÞAŠb‹ ÄEäèÿå\èÙ‰@y) %º7‘ÇJÉ}ÜáŒdM .ô?,*.ë nØÇÚ…‚R€˜®výXg&­‘.¿VæãÒõPm³ùÞÞl~ëŠh/®Ì±N!æ|qNBÿZÏzm0µV½áº}Ðw‡+sgWBÄU’©YÓœøqu%*çwgiUÔøPê™opcID?5F¹¾¡²ýœèÎ!Q…&¢”ñйÙ{ qÕÀ%E±Q$þ°OùäK×UJÕ£ÅÆj%6îü„«Û&g‹l×ÞÂ|.pn²ò=™ZI ’áÔ½@Õ”¨Åð#C°{ÄÚ´nhç1Ò2žÔJ‘0m§ŒöÔJ¢ˆBGרô‡þö?ÒÀ:º ¬êZÛ¶/YvM{p´b‡HÑàõøºÌ¶§•2Ä+€+ˆœpcFškäúhóäwÈ9‹A@DGùSÑ€^æ}2£§²á›KjF Vt÷ÒcÀÓ³·ÿ&(t¯'ñëƒ(<áÐ2¤f¼èâ ~Ñ2õÇäKƒ’%îö©€ã·¯Ó/ †79èhí›ó€ÑGñ½3™r +nk¤ [ì 2* ·n´ï8E÷·½h%u#N¢Ý﹕剪l«6xzÐÇ‡Ó +Îæ—Íyì)´myHo•~Q V…Ïó ãÓ_¯Å˜±ÃY÷ˆívÓðô3â_þ ¯_G©¯¤U, ª;6Ë:Rc>sDdP )»tV¾ýᲤñë€l®ïx¬wQƳøï2ä‘¿jD£Pr³Ýò@;XKö!{h_ç=J°–/Ôšù¥»hlÅØžÓë±ê©’b­}F<£í´šBÍå,ßœgĹ–Þ‰† ž]ŽµäœÒ€G€’·B>[Þ?€^-žj³ PF-.1ChÛ0¿Áûm,qòF…˜Mù +°kÉ‚SÅ·­¹> 5*éÏ…ì~­³®DÉ!i(TσØ"ܹMÆó@R¶¦eÿ¤$N-šN¹R‘÷ÞÊwzf÷Ä)}ŒC¯{¼€ËápĺQ¡Ü2÷`ÓXš·]é·€<¤ ýþøp×GBûͳ$Aôzg$~~F1`¥u=3Î{ÐîÐ’ …0æ¥+SsR‘‰¿µ˜õ#V{Õ÷ã¹WCLÍ¥>¶Žó¯Îq§ÿqxÏóM»‹Çé¥1ÓGìd#¯gò…Ò®PË5Bï3ßö¶x¶’>±K}WošêUW*ŠÕ‚,IýŒx^Í”Q ®ã›ó0‘ó.dl Þ|ógöUUö6Oó=êØVK(¤Ëwm¿!+geGD€Ð?¼\)€à3{¬?¤Ìõ‘[vEK +‰÷çy´£V`œÆXßéØØé?Ugós;¨?0åãI-ª$¥{˧æ`Ð-L‡ñƒW3 >õ‘1 @Víg#Ü kÈXö9Ò®¥åp¿®$ÑÖÊ#)dæT,¯ã¸Š²`ÜÐÛ8ne°#(tÄQµg€”¬íSø;9’°Ñ?ŒgÏdmÖI`×ÇÝpq¿iÕ“– l°¡Ý®ëy´.»‚ŸÉ6ŠìSÒJƒßœͤžÎJUarwsñ÷ˆ«D@‹µÖ^)ì[TìäÀhûL*Li…Ú–;©ç;~ ˜s~Éô·^Ûk0ç="TüÞdm'–¡‰Š_qô¸<ψ<¶Ì‘I€!û¢ÚMf<”jm¯óºð)ÊHYYy«Pêòi^_/<ýz3¥¡¡wÔ~þtzëDPlY/Õ7òs«z…]yèTìV¦®S6}t¶Ü Ý6ÍÅR"7<Õ½x‚Fab­|`µs¥Ï¨/Ë'Ù ½’P’å’ŽÍVŠ8.<^Ãö¸ïÐ þŧl]±wX;êÀ|-·Ó%}:JF± Z)4¥ +~|*¬k2CíÇLò<4ÕE}õ°|•”UÆc'6ªÂ~LÊÌ5?è§Î +þ´$¾äJnK¹o¿ši‚BظŒþÍiÖ.ÙÖ´E +_GMÇ‹M‹šLß­‰Aˆ¨°í¡¯P涑¢š¿–~ô:t‰·î´ALŒ‘Ú cñS´t½wÇ|š ¼EBU»%ܱ^ESé4ÈCBJA¥K{£j»¡¦&ˆÍ”}M,§Dܳ«»DsÊñ··«z=0C)Šô‰AÙ÷¹_þÉRËÜ͜־Ž±ç ㇷs¹í¬3Û! ¬“žÂ¹A zY«Ô«^ÄÁhr +rÚOMf`tçãÉß•š»µ'¢ïˆ{»Ÿ#‚ çs%µRZìcVÓxÜkÿÔîœÇ'¹"jßÆ9›¬Û¦å="ŠŒžµ‰Ð¨±oYÊ\‰Ë3÷4úïÓ¨p‡Ëš ô-jq·hóc!ÃßOX¹bvlšì3ïì3­ø+.wF¡(iÝ)ŸN5¢ Š~Ì1@°ÁÖUT øVÏuÁ£ÿ¢tº×•J0ߦxýu³(鯩©'àÔ¯L3B#œ& ç\A¯MÊ ëþ^«48* ÐàΑ(åTWÔ¹irLØŸGð¿q¸OÅï½L\ÖK{„$¨˜E»“¸§×Q$DѶ<ÈÃH̲ƒÆÑ°ÄÔ࿆ÄU½ƒ3âîknE ¹;$ |àqrYʵe?™éÁ5ÿ(ñÕU‹™n°(J¶4ᯢ3<¯å0@ JÓF\au€&¸7¿ó3 +óh*k¢§Â + “1©*Õ™.fvöëfIº +Ãý^`å±0êÃ3}?˜‹VpOæBôr´òY‹É€ÞÖdA=XŒ3# uTœ[]¤øµ¦™üf•oŸ„a·PJƒŒ‰ÛÚUÒÅย­P=1ÒuR‚ÔMC,S¹Ž(Ôõ3êfÈ…:Qq¤ôÛlƒÙÜÒÚµ[²4WùãOPyX6Ë:'äÚœöØ|e W *‘é¨]uR%Xn_CñŒ‘Öç7¬Xᆣè¨Q’7d$}~+ÌûJ8ÍsD§r {kJ $e•.mn¤ŒP³0tµ€bp‚%çW [éOî×òŠ/Í]uZY†ÎhëáÞ¹¼5.¤-Ôš•_šF@N¥jŒ¸oRyž'`ž¸~µlü;²^ EÜ_{תÇüÅ @Woæ. *Í™}pܱçi˜–<Ï[ÇJ14B¯a!Ü~+ä4ò~ ù­…ù~Å•eùÕþüýܼ™õò]7é9M]4î½y¾ÈKˆðè“ùÛ5'­ðXè€ I¥|­9ûE~‹È3;Z»ÉuÑÃý<¨ÎÛ,ê+µ£’ßü.E‚3úGà3 +j*þM,JüÞIï£EWfË£"4í+ >ó43ƒÊðóë®_U€x“5ŽÛÆ™Xƒ8¨[@A茺#@Ã5Å8®„oÍvÊÃZ‚Í3_—1‹YA´Ÿòe\ÁV²ëƒø`OP‘UU®‚íA5{‹ë3ü "ÆC %Ê‚u)9MVjà½Ü±øK K³Ï>yL^ˆ¾›Pk êzû‘Góž¶}œgŽ@„âáW¡•¶¾ +b2ó3À!Ð3Êëõ} bhº6 cìo;÷·åt DZ;† +‚âÀ½¥{·Ïˆßo\¼„¡2$J<6Tƒ(!ˆ3Â^A9si…À0ßÒf§À•,Ï;5EyÑ0¬» +®XÝÝï"”Ëm[.w;}Fáø36f +E®ÛAQ^wýîe*[æËùd‚Æ€rôç!VЯQªì#øqD[ mwx­ºNÖ@­+>JXŽ@ÓW„*DD+¶©Ýûº’ë$n’ʳAÇ"‰¬Î¯·çAm€ˆaNò»ä‡ìœ¼Ò­ÎÞp‹¥ñIÄ1¯ÞRíöÅ%1ÔîfŽÔðWf†X´©í‹:åýMD½BÈ,ëÞÔý‹>£0aVdÐéär²Õ™…’ &É+°V>£X«›‚fï¿ßÉ’äÍ]HýæÏ¢¹u‰¹uÕL¦äºdL¤>Î`½×žvœóÁU½GÕ8'Þ»Ò&ÐJ8?H”±ä=‘]Räkúò"=X®°‡KDy!éš2 üÚ‹lÞƒöÑ殹×:ëR¶X¹Ô.&¥¤0ú?ì¡0»Ø,]¯ëèU@ÐðköpÔd”–j•Ê°°ØK&"þ"ó– æ“hÍ“¹[Îa‰¢ ¹¿ë§ÖðÁí›D«þ)dÙí)ó‹<ãQ¯Œµ=ø𛈕a<‰ÝÇAÆ׌ù±°Òum'p5ag‚«‰¹Øqœ÷ψ\`&/lwnÞ7çY¯¸àµÇÇiÏï'„JG‹;eÍëœæbhy_vm|¤ŠqFýÕþ>i&®\Òéïiü®pÄQ™¶~l5§3?ªvŸ,í÷w-À#¬žzy@ÂïQãˆ#Ú[ +Ñ—•Ü1Ô¸>…kY°!4Aé¾3øVÚ–”¦P÷ÁˆD/aܲy£ÕBú´…vœ6h¯õâ•o"Þåªß£Ð’”Ë@)7“B7û³ p†;àªñªIö š[”âoTùt-Ï‚zGì¹€¡‘CQï¡ÐæF‰ŠLä&¬\!8‡3ÞkB8•:T ÇIÐ]HÄä±°©`—t“µ‡ÀJÄ ÝC‘oØðSù-•TÖ’%õ¿ïv\H’,z£hÝbN‹B5Xª™lBþÍ`>ñíúü{º43UŒƒÝãwQÈ Ø¬½ñ~k‹;Ò.þ}D€n¡BáQ€øꑘS\KIºËØ¢È7“/xà%"ôzëön½.÷m­dÖž§Œ lÖ÷z€ùÔ„í9¾ ½¶ˆ¦Dwrâ¼zÿøm”߃”áÿ º»ôó_òÌÛöí „D£óÛo\æNÄxÿ°£ÿ»³Çá׸'s0h^©8ý }¸ R¼M¢™ŒxBÍ9¬RÓ{º1Ì+æ£4‘[¼Ì6ÈÿB•œ œ5Í£>éGCÇ­±Dh¾}“Ê)Å£n®,ØnÄŽ –4jëÓ:‘­dè ·¯Ï!Øc|é"¹;åî³;U°|E°ía€Ï¡žÿ¶&uÿz|]ÅL¢ºHg¢ÂœEK¢>ô…%Bñ¾Ê§ÅAGxý%šüEÿÌîÝ%÷ak=ÃÂÎkÐld¤‰­·ÜÐT³P2IÉK¤¨zrû£ì´¹(ômÐ â/îFE¤t•*Öô¿Çª]nfXdPˆâéNݷθ+û ­‘¯¿œ•ÝQœàmè'®Ÿv—3YsK°ŒMÕ'ÿ.NL…Û)š2¶Õb ðöc¬Ö¬_=]é³QÍ]såÇ•¯j{y€~¤8U9Øå/`çW¯ØÓôÇÇMxR¾–å¥õ×~­ïˆ©Ìü­ÑŠÊÈñPÛŽPþw]àI¦Ä×’qÑËmÏžDOää²›r%S°–¬¡R_ MË [$!®c†n»Þ¤ùlÎé‰Ë’^ùEÉB:²#;|:<°Yµ¾ä!ö™D6ß¼vF½@Mkø ± +'Î\_K*þ3™Ó¨;&fw:eM0‹o¨¢õ=Mj¹Ng¨«³?Ÿ6ù#ó9í&>­‡Ÿš15Ó^¿npÞ`ɪ†«b¯“·r m%èZûÛazˆn¤6¶sø ˜áòwÀº3é´—ôÔ*Á¢.†,—Tˆé7‰uºP«8wúYr”öìû<¯2¹ *n1J¦ ù|Ë•Ø/Ñl»Ž?¤õì¶ÊœC0ßZHL¿®×…î,†¼1™ÆR7†ÙÒÐAiÓòƒêðÇ7I8L.‚Ÿ S’†ÛÆFž`üºÝÀ…Z(Rôד¦) qÊ˼R?Q°ƒ>:|drÒÜÐ`èºö£º®e/Ñmûô¬™ èT£‘@*»ÒÖ–Wâ¡q%•Ü¨áê‚TqÔ€lQOAYŒe=ûºÜ +ìñ˜®Çôž®Ðëê4•äÊyÀ}0›Ÿ3d¤vyËnöÀj·ìéö<µ;7›ê /·Æ4hXq±€îpà?T„´¨ÈÈ›ÉÛÿOž¦…ö|ˆxÖh{×Ï¡"Rª¼.¹9 F#üâ:çòÓ.ºrg®äo]ïþxèxãš‚¬9T’²ß‡y¿—3ç  äÒèÞëõªfÜy"x´Ð÷éæ­èì ôt1 ½¸åð }uèòRàüÌ:J7V¿öÄ›ZÁ‡ºækÝÏô—Œñ¦p½¡p±ÜáoUl+Z®‡8_+a]««†"þÐê5‹Õ˜B s]PÛæ#óê“•—ÇyOOÞ£ËF½vù‰.êÎŒ\JÍÜó^ñ½*ìî¯lEþå,íIø¾UrBªs­»ÿ·JN›Î.U ïzÐ×ÜŒyꟀ2®Ž¢§/ áÖwiEBÿ¹ÙD멬©âC !Z8¤­qÕÀ˜ï­oP'õ*û¤ø…’‡ùÙ¤òÀÔ=”ïH„i(*“âó{ Ôãc#¦!t o5ë¹R–? `BŒw›Ñ:òÜW +Òqí”ù.àà½vI£¹Òõðç,Ÿà¹¸ƒg ²Í~®€©ÃA2×CPƒ¢Z£Î©ý/ +›tCá¿eů—*Ú»rWúJÆsO½¥9¾î­ít‡‚üpK4ò9¹,‘¦ó«¨ŽGu#>ãs+¬EñbËöN¶Jn>7ñ´ zûg^~’²úO‘*Ê¿Y³Èã ɤ@Þz8^X2]áÂÔ˜åJpaò5´Ñe—*ºNÝËÛ/ª¯!Á‰¬,[ê{ÛýR¾¨yuN5Ù‚ÆZ´×0¼’ÝyÓöøÇn ˜¯ÉW¯ã0ò}DkE~ ¢¨åAü-è²´Kì'ŒPàå@­È +Ã^2ˆ0‡(ÛŽ»~]ˆ-[°™¯£˜'Ö0}äBõx|¤J|¤jÉ/1™è(²ÍeD)c=‰«|Q§¡—ÂpõåÜè³µŒÕ‡Õú–j=Y(08Z–hÙÆ1`[²R÷‹#TÜ‘Áy¤¤Ù×t7|-·¯‰í878Ïú²Æœ=/;ÛÞ÷FkŒGØŸG¨žönµÜž•Ú®ÔhX¡ÜvÅ“9ß¾J”ð9*e=e2 Š%ó` '‡Y‡y?W"÷qŸq#¦ùEq¤kÄ··±û¬sÛg•û¬hÕ9DĬÌüÞûVæ1äèÆs¡C$p ¾@–Mja%ÂöÖA›~à;ÂÞ;†<¶%ùI©{HæF¦®Þ×Wßó¾Õlg‘O” •—¦ùqa”ûÞ][ÓX™CŽã5E 0yZC>Àɯ*«}ÉF˜9°"`,7€Nûë “˜ÉUûþY$”8Û3G­¯(ÓÕÓ5uRÒÿÃ>Jkc³wÄèÑLäÑLµêµ4k’ö6–ºÙ‹©eŒÀ-ÒV«¯S˜2hV˜¨ÛcMä!ØZ`c¶ÄR¸PAzçÞG®·ý”H‰¨ŠÉ“ P•çHŽ)á‚~]øáãõhl2£fÅÌ àæ#s%Öum댗TÊyÊ-<Êžñ˜zZYÂþ­iU"ßY·¨€åZFÛKÀSš­ÿJ±–ZçÅï º?pÕLŽÜ‚hRgåkùªX=CùIô¼à’äÿrFóçÜÓ R`ŠÛeÕ”(]òÀæ¢ÉŸ_Hñœ±9€ª¥qØÏwTYÜx?U̦L+ë°†Œì'U³¾U¦ŠÀˆr߈ê|“:J¢Â:8Oi"Š—Èz¢‰¢‚HÙD𭵿o¹ZûvàõŽàÍ ]$öÄ+³U"J :åOvDk¸*¶V”ñº4ñ¯ðÊÕ$ëÛ€b¡û±¯¿ÜB¬\À†å(”ì†G¬îÂ’4lé‘nh“ÊØuU_8<˜4@J _¶#¸MÄ‚[QH=6óÛ/Ô‘•‘ZËáÔsð²Q +ðH²4‹ÊŠ"²”™Æ¨äŽ(€Ê€ÞIWZùëuþ¬•î¯ln‹"É(¶¬íðz0ŒâÛ(g£¤W&@&Šú™Rú¸òÑu? bH#2!¦Ê£AÜÛ­ËD6|nr½ªLˆzìSu:KŠR›ÄXÞs¡X|µEͲüè39÷…òiæ8+ABùûÜ/2–8en›'\ƒzf®ô#eæ]QºãV†óçŒÉ×%·í@Ø<3Į́†1±Í0—Ü8‘ÐÁÓÍ#yÖs9¯ G¢uàN'£­©(>«Ùó“vÏ j(è<Ð «0äÒQ»Ûí[²ŠÆª¶óÄœ(×,Û*3cß„†,Ç9”¯xv@PcÛ–ikwG–€éò ©2·ð9Hµ‘òàÇN1ãÞܼŒväã6ØËúáJš±¹äŽ©ïÑÎP,‘?³ Çßz{ñ•-åzíKô»î­ï€UçÜÂPL»#j=èÙú‚„W˯oí‚5OŒúµïuúX·Ì¢r»N]Ð8oØp•I|þÌå›P—»õ-Ö\Š³µ_Öd®›øh싵ˆ+i¥å]Ù3ÊÒxÆ®ósÕR"e)íð}5%ŒwÙRa&½Ý—;jT’€ªŸV߃ê§(xM‚Ô™XcXDÝ÷É,µsÉ=¶{ˆÙÔv1s‰êƒ*î€á±•ó,ÙvæóÛˆèT ÄJ.#¢ÙF±¬&"úkúãz]é.‚ìÕF¾¹óÔ`ØgYÓdoû§ÕýÓj˜«—.«køÍm£~/ùìk»¾’ï E³Ô]ôÇB¶û$?+ í{cuàiœ8ʶDGØç|„±{*Ìì^œ5Ö£‰»ÎP–,ö`‡¢—ÀÚO3ÆøH­}4»V:P¶fÄìý¯×^"’ÇÍlŠ*΄ëM8dl’Б#wh*wÎ#þS< ¹2]¬3É¥©õÀ'ç¹Uc/¦,ë[^PcƒŽÅÒNìs‡¯͹—4ÄgUÜ;çº` +G=.yÊ@<óu!›Ñd²ÅÄ~ì> ÒáYTºî¸?ݹNd†‚Ú;‚¼tíWdæi•£€npýëB:t]›¦ oÒ¥]5ÞïãéƒBÌÀ†Æ鉩á–H©ÚAJM6)Lʼך“÷*"y^½o}uši<À(ô³QäB-|(…Í&&*OhÆòåcÍC¹„%`>Ò»r0ûkš^pVÛ‘½Q~ žƒ·¸ý+sR­2Ž+Û4A@l ”פQ*lvù&‚Êü3a°ßt±kq:õUlQʶñÞTj@5ñÖI›0)uÝ)u»6¡ô +¡´=´Îõ}2·PcƒWLÏn2ì¼sC>7T“ÖLÀunÔ†`„Žó™úæ–ƒ¬¬/´öŶœ;×t¼6h¾ºL•Ð¶h8ñâ» ’!¯jRÔ aFÏ:6ûǶ‘’ ¤ÜÞU×9¯ÔæÞÁ/ùLñÌ=-ˆþÐvjÆÛú×y¤i¿Œò'P%! V•ZÅá„b1?$>Æð­\1 uŠ;¬`x/;º•¬ðØMÙžàAΰ=a/×õƒýZm—„–m?ŒÈ±„0†¥ŽüVñU·Mwlx±cæþ6Š…oEMýOçÖ•6b>›‘á¹êŽ~BÝïÚ”§ˆ8î¯+‘{1Õ`šIÔ¥Cý¡×TÊ7‡¨Ø~n÷5¹«mý]ß ó£hDŸÊ—eE~]ˆB=…¡aïtë•óGNÏ­Ó­Þ“Ím±kˆ:ì®#Âîß% 3›‰Y>ìlîÁtn3©ŽmêP#*~éÑqo@;<>PT‚ñ…ÁVešà4" +&+lQÛ…ð“û%ïöD"¼Â/"[»{þÙXv:$~d¸Žé°n °ž×³ E#ÖN¯–|*.•M²EÁ½¶¨3Æ 3€„IŠ;ßš´IcàçöºËÌk/ܤd¬iôê!«íÈè-tnl€cìüÄx³Ç¾Ü…“óÖÂG ν‘úxd”°b3=4Õ<ù»hWO{e‹§ëÆÔ ÈþjÊÅðóÚ)nˆSwœ¬íƈ޸d¹‚ë- à£Å\ñreÓ|!1)ïÏ8RÜAëïÞ+ëŦtRlx4¬•JÍù”z/cï<½íB.ã¤ÛØt=˜ètûŽGÕ +ëaàŸ$¾Ó<|=Ú†ôÍÒ³¨ Dô9?û˜k' +ìmoº q°¨ÎzM9Pg¬Œ®Ål‰þ™¦$^Hd]ÇiÓ0äƒ=¥q T°«u [Èìš'ßÒ6¹ÒXkXM—‰ï\/°i英Pß-´b•×ôþÊ8‰RW†²šª•s[L®ß¾Þéy"²7Ô±‹'›â;Hµ ú + «Ê†¼(~ô£¯ßܼ¯ +  ! ápY iHfÖºV¯`"„*ÚàäBÏm3Z6¶c†î¬Úcï7ë£$Ë’&rŸz„Ÿå Wê8¢;{?ôpZ¦LctòŽøŽ ‹ÄË¡Bà^”+öÝâï`ò*i2 ¸‹™¢z¤®¨_QÁ¥~…š@ªO×C2ÝRw×ñTô5•ôJá}] ]ŽjV+è’¢1’¦(kÕÚïÓºÌzçŽçV!‰Mk†ù¡s¸‰!Á½dê°Q\R=}“·ƒ0©• /“|*«g(Ë%¤f§Æ}¡-¥[[tER€)ÀÎcß™¡ÐYO8û9Ïp±ˆ{¤Ì]嘀ÍpõÐÔk°3ÈÀ®‹Ì”ï´²<,+¤Jœ›+~€Ý¨Z’X°€ºtV ÍŸõϤ{µXrRظìwð¬ÚÂN@2›,ËV—nt`7;áæU2&Tɘ ½žL*ÈŽ[h)Zè?w†ú¬ØóJ°_ä²$îpý¶î…[T£y¦ØªvM?Ô)üZÓ™7°¥›Diür~¾ÖxyH +»D݉7Ôñéá¢gËC*¸áQ"›-öß,ó_Km‰¥àl{bV·5ùQɨP-çuºâ@x>YëŸÓ…f±~~œ3ZÒôvõØ feMâÊ…§Âj'%û™[¬hîów}zÙfýçnf‰ÞàüãÐÀmÐS6úÚɹ«nƒ7»…LÁ{cŸ†‰`<ÛÌ•>¿G:ûI6á÷R“¥LDM–yVÙn˜MOûéGêÞ<¸D/=r“¼R hñ =“çÖ¹8š.½ Ð2¤àrý ª€ªÇÔVYØ&2ìLÔØxM` ß~‰Y:¢`ÖʦXs¥;tƒ! ›.Þ¡@€ö‡ro°æ´©< 7U“¶2"åɶV3êªÎ€y=HÑ`÷¢)8TÝ&\=Yn[?×Í9(ÇÞ’[þ´$ùrm¹`/¶zÀRMß͆–9侉¼¼®$ês])^œwª‹k+7)&¶± ¯hgHUãø‘Ǥ:;N0ë?ïñšÆJ‰Çîfdwü”scKÐÅ‘Kxl%M,TÄÂ7§2©ø”ø â3ž–€¼ÍáA®æÃ5M Ó…­^a†/áåoE;Öx0PºÌféØWFÜmïãy(è< +ï_“ÀYmðKÖÂí³u%?pÖêñÞŠ1åiIYÀ~Þ>|ÇÁ3ƒ°vâ•´¯ä +ÊÅ- þ×F¨M6±aRD÷Þœ%üxÞàzR^€úr·)^»¼ÜG|ÎoñT-ûír¦eÒ1{·°¸‹{²Yx}±¿Y‰[w¥k‚?ÙmSg¢ñˆŠš~X8¨nõý=y×@ÈP6âJ>k‘_àþÒÔÙ‡<u[/²„ˆ½î *ÄeCø.飑ÒeoA¾)Ó‚;÷˜_@ÇÕû‰‚"1<úBàA€õCT6«|½DÝ[<Ê£sû8Lý3Âi§IŪ³ Úëêüú‡¼|ÇÁïÉ"ˆ¤ßlE¨@EËÊÏÏ{Ô.—(ÄìŒb0µžÞ_ý?á¶Y£%wyèÀ-©/ +¯è†Ëà߶º+‚íW\Ây;((øé.°þÞkóŽ7ûJƒ‚om‘Ýzƒ’)–©"ZîþÑs`¯ÔÄ°®œÿ©àERˆž!U?*Ù×ØCcÇ^ÈÎö¼©·DéEï#/^$#‹‚št{â+ÛpýG©c$X«¢kÒnAf—K–È•rR‡T?˪d˶b‘Dù^s .zª\ôÃ6dæ a%¼Y&3*M±€`hYJì!´ö½4¨¶I°_tßsp"Ü‘ +ÒóéuGÔ3ÚVýÓ¦¨b~yœA$¨ïÿ¾÷°R +Z÷6¾î͉Ð/Æt"HÖ¤‹21üû™­G~jë{>H2nÞÞÁÌØŽTŒ KÓ¢TÕÓ1L¨·ô(íÙÔ.²ÂHZá0ò»Ö@qÑc†8U}n!TõýŽGÚƒ…ïÒŒ‹¡+TªLl·U%ë{µ·XHÿ%^¢ [ÈØPÉrï†m ¿fûãÑ`ß"®kN¶\´®qº|®‰Ä¨BuwB±þ#ËyJza°õúúMÞŸï÷ÿþ³¶öÿÊ}øÅç¿Åÿ~çÜ.Qo }8xç E#™c±¦÷µYÝÆ…0õœ‰¨ +u~‹’‡ç4|ŘŒÔan"äôü ÃPdׂöÙ­élU¸Ç3,zI•Tî)ÐÎ{æ“ żÎMÀ¶ +)²GGWZ8…—îÅŠjÖ;=™Ë¨wrÑéyšØiÚûwôSÊ]ß‘*äµ 8Ç‘s¤)\˜wúëJ¶°ès+ë'ßë¾È+ðt ËZ„Yð2w†DÚîV!‰ÐѦÄë4[¹mÇX›Qȸ‡j«¦6-Á;˜¹9Ü?o·s„Yp€´éî{™øAÙœ×#œ}g—(nÃ/Ð7&á@z+NHJ!Q¹·ÿõî&…ìCO%Ê)@˜-ØÍvΕ¨õ‚<Š|• ,4Oªå¡l‚4#¼2H:Iï kvlS³8w¬|>°wéÎßYþ b⥃pR.ºõ0ÏLf¦"HW‰‡x7”ÛµJaV;_çG ƒ(÷ÇÜßrIæ ¼{§YÃoì¤ÐóUDEÎ-­1q:|V[šy0K×–´lܬ“Yg§LštG[a]ש’.µ‹ FJk5ìbñ4¶„fz}F˜&”dãt܃Ux?NÌ6[àªÌ‘d<À- œ|À¬9‚ËêÆ*}QÍ @Æ¿R¯Ž€QóKW{Ø°§}×.gK4Xúë@¾=˜Ô}H˜žOipŒ Éçà…Ã)[ 1I>iWèŽ[®Pž]Ûó¢ ÃQ+ÐÛ)Uͧ¥ï³Yj#"ЫÁCœF°3ÃèLDàÂÒÀ×÷§†Çk.KÏÞ& ˆvøú·­qØÿ%¢ÊH²_Œ¸üikç¬Ç$‚ÈÀ…ž]g€R½‘¿Ï¨rRâ<©3âÞ\͹·­s€s9GR‡Þh·o¢êV}g2X;”.8Vä À4ˆÉM«„ƒ1pÛHÔN0s"ÚŽØÜ–^õú8ÖaZQ ]_a@u‡c Y@òäψo‡êGTñªÓÍÚÃ@ü‡2Ú«"‘7º€?¨v‚/!D§Il;<ðó~ô¸ÝV“vfî\Ó{áEkîñ”š[åß’Öz½¯sCŒ‚ôOAû[Õj‹Bœ”Ö8ixQòV—üTêž$¾Xß>n4Çó&éoT<ïY©VA¸>‹X—§‡DÄF=Î e¾X›^"D¶p Êè$çmõiŸ ¡[ƒ¯Ö72þŽé+Ãxäežºò°¨kA 2FS2¹3‘Kô‘TH÷ÝZ™š$z¦ä­…:Ø6$d¢•¸ÔGXEvÙûtZ7h„­¨j¯îÊTXeâ9)4û#•ð/f`ÿb¦¹y/ÿ:¶¼ˆ«ìtJ¬lƒ­¢WÃ/¡JiÏ|DÙ]dorÞ“2a´~íkVH}ÉÇe- vÌ×ì('`š’<š·æá €W˜)¹ÄŒEÈú½£žs ¬&epíZÌØ‘€VβoÁåµú–&B‡…÷°^=pš%IÛ¨#WË Í´í˜,°•š–v.€¦ÎDh6T±§zMwT×d&a(BúByèä8‘0^nGéñú Œ ˜•×¸€“›ÜA0ZE§m ß2è—žêùÚ½YŒ»ög˜»\Ñ×|£a'fðÚƉNxj×|+UïÎXŸn3$šO/Ö„›m²G5jM÷3ÞLs½„æ®óLÍêã[v)+õ³Y>CÎ3‡ZS§.ö·¤NEÿXÙv¢æVŦ +ˆDwÝh¬æ˜Fˆ¥S¬ôqÙººÁŽÜÈòhão›Ð5&42Ò÷Ë6ÝŠéôu¸Tè7ý?~û…ùùÏø’J)Qµ‹8N$!߲ׄ'ƒxKì\ß±m´)1A=JÕT¹.™)ð<6ê¨u$b¤­*é,|Gt\Jð…ê)æõa*—t‰^:=±¸Ÿ6öZé’º",€ýÙº?zå8¯‡H@Ý:p¯Æ)¬v½¹yL ©|é3Yî¡þRw'È +/M©ÚCV]Ó…µˆÔ–êC¼×ÇiRŽ%Æ–oÀA•.!EÊe¬Mý™ .Æ24Ã$á¥ZYS=8è0ÔèÅ]ë¢`c2ç‰cËÐáØ°Ì•"ÅÃKO]—Þ8"Ê«Œ[wùàöâ¨/Ó|l|¸ÃZ‰ +Þ§+»Cg6f˜rÚ¹Z6íâvù_ÆV#³ý¸ƒ´²BB‡nM5G}­±ð?âíÖ~“’ µZ]ºLe£iZ`0©^kª¥VûyeWã€cQ³û„F:R6‡òïaìJ±*¬Wß +oápËÓÊùˆÚ=Êl‚Ç0íS5NSSê28ž¢œbïtZÀ©ÔψßïNˆ²3kÖì +2}žgc/xèW¾ ébPÚXÎŒ£ÌúzØÚøˆ‚ºmsá:ð½öÖkw¦Ì c¨ÖLÌÓ”Háµ7Ó Äi§…mãs¡AC˜ðç¨ël¢^:«¢颭§ÎÃÐéTdß:#rW'ð™3ª‰*íq#ÛW‰> +Z“âqë{$jûJwà0¾ˆ–©üðs£ß¨£ƒ…a¼(áâÏeÖ“ÝaZ|Â-Kwk· ×­ÛUÖˆÜCäÓzø#bSM«ëÐ3¼GAœøÓ¨äÞ ãšœmÀ…-/튵|þ=y›[îÏY¿ù4`KWÂe/;…v½>´¥€ ÷¥î÷Åž:P&mG ñQÖôLÉ‘ÇŒš«´òhÔG‚pžãåø[C7C,ÐFŒ<ë­ä Ê‚) +,†ÚÜ£á•ÔÌX~¢^-u¾Üì"ö•Øž! _Ø\nÀoPU(í Áâ’vn­ç›ÿ\¾³×úN#àz+«k_”VJõ¬D{êQÓƦuz¥ê§àçmÉŽ‚ŽpímÚ%†ÕY¿ÓfYþ8AY–Z%XsšíÒ øŠ ´˜aSRÓקÕUm ð•ÓËÝð‹çôãŠ{áVÓª½ã*3ì!ÍÝ([{I—?P4~X{ë|ë@‘‘zèÏ·F­1âa]Pd5N Á&¯5¦ƒÐiµšyEþF2ôçܱÃQ¾ge#ÕÆ`;Å+§ÅM¼§¤{Y,–©ÛxÄYp¾¶Um‹U­³ Ò+ö›aÁÌxûªÜÚ¤£}£D £«íÔ àË<–ýptE›+ìí"lUzEœ‘¹’ +s%ñ;u;Ñ¡ ÎÃ-D<®=:þÚWÑ׃L*ȸ÷(wªZûþëý˜®_1D®öÔg5뀊ê9o ø¦3åŸÛHâJ+ªöÜÏ$ +–!tq¹í¹¶B{G”6ÞÔª?ÂÓT‚.W4ìå†÷ÅøÝöÚ¶¥ME|¿¸HR³…šž›+ª[B´ønÃœ¨ÎGœÕÎœ»õ”P“1JÙyö„Wý¶céÝ©AMï¹g‘Uñ#ݽ²IÏWºÓv+ŠÖfíëJÐ9‰ªûûdÚ¢Ç}oò4–舢•º/ñnw{”Ê”.âïéÞ¯´w<ç¯?o:’¢mq| [ð¸$X¼f:›®g¼²ŸˆW%’ôÐQš;jÖÍ¢ºxÈ[†#Eý¥³7•¼ €ÙF®Ùƒ¼p}èü{º‡}ëàà©y}}F'aŒÄïBlc·Y·îÚyÆCKƒœº rîùe¨›Ùí™ +¨Ùx'„Êd£¥Ð¬ž?Òil$[˜â’X„_¾øõ?l?NƒU%=•N³˜©«XË"÷…ÙŽ8wÄý8ž=sÅÛA¡ÏÒ¨Ñ_±%Ô`±E.;XŒŽµ§EQeqe§íž'âØO¹F¹›¡aǨ‚ÕA“^‰:Ç%/~8ÁX |”‘.TU?s¼J¿ +ÒñcÆñãÜBçŒ&¯ÑÕDE/$Τô™6Û=»ˆgdM sÖí›ßEѹ–‰ÓH5ß;³;Ãz¦‹äý˜.båÛ}uÂZwäñGx'ÃÿΆBgŠ +lŽ2Î8ö{{„ÿ\ÉTü3õt>ÚÌnpªÿ«¿n›¶îÝŒŠ9 Ÿ + •°WÄÚ®ÌD°%#Bh"WÇò9´²õJÁŸˆ¹~uò5vÙ<[µ:y_tÂñ8Eñ§ÙQû&"÷®}„Y®îmçyD;aÐÖ>vÆ¥ÓÄZºZt;@Y/ÕЗªÄÏ£ à=¾œ[Ü3µí;IªIâÖÑ5jû@—¿6ØC¸Ô‘º ºŸxyÆãcÄããx-4Güè:Ò¿gHx‚‚ŠB„98Ô6úFCf+C.z瘖wa¸¹R ÂNæÄGb±GÓ©|ŸEa5Í)æaÍ åx‡H£×¸”ÏÂèPIõ¾ÄtQwÄU_‰Š-`I€ª>¢TkHzÉ:ÕæVø«¬‰°šj|D”ÀPÑxp÷ŸQ=îZS7éä¢÷gŠDØÃ"ÏË/Aœ_r»d¢ r*µº²³—®æȼÎA½ÞªíÈÈcD`üÖÑíZ±!©±4Xjˆxä•-•0±£@yÉlKì…—^áñ䯪r%ëºè/¦Þ¾ë>”kGXY‰Ayò ù˜ì8 ”QÎèt^ÿ­ðF)Ù!P–ÑX% Ï4*ËõMÄÁ" 5ŸÝÏG”R #R e—á+Ìèêçw<µ‹­üÎvÇáË}dw­–8AVax‘Ï(©Ô‰BŒ…Ç‘MwnVÒøºELoö%•ëç= ëXøh\hpgA2¹Û›(É4_Ï€Ó︌³Ýù øýÓÅ;-íƒ%œßœæ¾>.TŽ‘í,5&¶³Ì’Ÿß®‘ïQ,(±G £â°ûJ¥lÔ *?uÛ‰ÐmRÛâ5Æ@Ñü¼›i1 ^ÎRHç–:|¥†ó°çƒ=ßõ^?GÖJ½Æðùò°TsŒ£lS”®[û…¯=µ7Þ}¸«ÐÍ(ÍÀ&ëzä/©Cÿ´+IÎÞogÊ*h;ÜÁÌ»÷AÆ÷c®o@£4ÆÙ I&îMÿŽÙ'%£ÚŽÚ?/ æÕ´2<žª~­÷Ž˜;B“c š>;‚\®±?}ªodá9ÔxJtœ 7ËÒй<|9­¤Ý’ÔüÞŽõâf‰ÐG“ñeÕ$h³ 2³4ïÇe±x#NF°† +ë”Yƒ‡} ²˜!âfÈ(nÖ×sþGé›.å³)ÒÝo@¢ÊÛcN‹ þ!ËlW­×Ø©ªåŸ»¿]úì[éÍ*۸陋áü‹œ‰?ÍŠz„BAÅzhE°ÙÙç ›ý4¹¡ ¥®+,ùœ×™ù@EìŒ âµ)½E¾‰PωĀV­çg0W”ª+=”Õê©äž5ÆœÇÕÏ6bYõÎ@þóÖÒ† O[zÉŽ-(Ë$ÃÛFó +Ñ:ÔuŽ'j$øqÊH]ö +¦Ìç–û´>n9éþ.b}zÇ(­ÎW«æˆƒQJÐí4B©e“Þ¢µÞh{D™+¸GÖí¿©*F¸S()ÀBäwª„ï|"¹+ ÕFÅ#£3Ó¶ã5SÁѤWªÑÜ.›[¢DVF™yÀ¦ë{KG+é$–3zÜF¡FLÖ 6¼•x6¯@FX”%‚U<Ó/{²¶å "#­ãíË[õŸï€¸ 4s¾#ã‚Î÷•³oMoÔ¡«ºrªwoDtk«ÏGþ¾††ýñÑ"µFçé«*QÛ_¼õ’¬dßf<ÍQ„1ž‘}jò÷â^­(U ƒ™eΣ;Ð*Šï`—3/‡$ð#%WØÃL(¿8'^–ÃŽS›n¾ +0’¡ìÐs!+)ö¨J®4¥©Þîxs%ŠÜ\‰M¬ +lˆX¡À6öw±Â…ûÌYs¥àl¬*¯§#+fÝâSaf<¦À5õvÍ3ÜÛ²æ¹cÛ´Q +Ðùˆ°˜¯nÝTnc\½Ú·çÍj½ÞÄ~ÜꬔóZøéóÞ%Jò6…­ÇówÏ +úQa“'õ|}%"iz\›å ì 57Õíß#°P)Öá¸3á=jœ©Ga3MúÍ8…ZŒ¦c,[Ñ/_Ë´‚vÖ%SoxŒäõ8eÈðíÕ¸ùø¸f›Ñ›3urd´ò‹WªRýâù‹!4àQZl½Ì., æfn² ¦ä®­ÖFšr'A +¥¬yüâ6çOꆓ¥jˆ\Ž§Ûî—´î+Å'ýBª|ÍýÑa=©‹cð­ÓÍcqô5¢Ùmvnä&¥U[ñDTígT¡YP¤±œ;ÆëëºÊTæAÿç&1+sm:•ÂA“ªk0Dž‚âŒp‹÷¹»ïìîc9ú|Ý®¡¯2ˆ?¦)äÛçÑ¿‰ð9Ž<ÇQm˜÷(Q÷L³Ñ{Ñ-Шf{r -—ÇKãšìL öB^Ö¼xæ}ò'‘IƒªêwaÐJ%í ì¦î)j¦¥¨gôžÆe­µDÔÜ–v´(*‚ªbI_(i? ß㊩e빜(¶Dͨh‚F±Ò-٢뒉že½XþÜ|ð +v[¯#,ŽãQ[¿è¡í•XY¢ª>§¢®ðJ·¬f®yºxÔvãŠô¼(zCJŠ+8Ú-¥¬þuþk×oé!d‹Ñòñˆ­!`!4¯ |¦Šk_$$<¹ì ­f\Ð^ë¯Ãö"zÃÕu(ÃÀ q^©dw3– ÿ>h!~qt}~óF¿”ÏDénh17¶ÅJ°å¢y ‚ú¿4ëi‘˜RúgDV‘s«5E¢à›ó æCÝ5ª¥ÈãÙ—ü8@Q’Œd ÷5nýƒ·¨Õlý‡¼hˆ¼—8ÿAgvnK•sÖŠÉá=â5…Hܽðp;¿9Vú¦’MK¨s‚ +ÑÚ}v-r·Gõn¯g¶/€"ÔÌAª¼²ð7RMÌâYž·DWb%7ÔYr^³”©Òˆ IcÒŸòššzº0e÷ʈÒyÓ"ï•ól·²P FÈÛ[ËÂQò]ô>("|Wµó1¬ö?¦`÷¾í´µxC¨ñù‚2]f¾Y„ÖÏJêþ~aæù¬#i²QwÙMó3MsƬmЩ˜¡ùo‹IDGœéÚ äÐù®ÏˆŒ©-ãˆYH™ý›óÀK‰ãŸ“ym[y¿+`7e>êæ¢vPsóHqå)ql<­:¡w:Æ.µMÝÀÜ¡t¤šÐÚêH7v£–ö’Y + T\;êÇuJÇ60a¾Žh{+C£ëÞ}ß3&T aÂá?ÀKP*˜Ûb +d"^ƒu-ñ^—öÍyæAòemã{‰°Åüú3 wfóØN«ïJ%œµž‹wš¼ŽÞ@§{YÆgöS¾ûMià5£_E](Xà)±²îsÆð'{R¼˜]U'HÓ\,r™ñË)ç×®(äsø—! ÛôT^XªðsŃ®ÁYw„ÂUà³Ïϵ5™é{Ôšª4Yèl1Ô‚,1ΫÛ=ÔŽ‰PWß³ZNz?Èïëö(ñ(˜Nè½Z?§h³SK°eôÒ”PÕQÛ¦PN\}E©qD™¥ ma˜UUæWïVÆÅ àaî× "Í8°ÁÙ]¿é:…8áEÓ”„]ÓÝêý¼ÆÍYø&¿Hq4´«öa‚¾®" D´õ΂TÄ]ÚÑÚL—§¶®,Èê@×½|F<ˆ¦9eáë›óPê–/MB¬â85·öuÁA{Ø·°¡œ÷9Èïò|ˆ³kr±‚ ÑýS_q­YÁý ÌDÇp ]áp„i‡Ð’I¥ïç žSÝ „+D¾Þ-±½÷0”>êã6Õ«þñÛ°»?'ÈO¶ý?˸ÿWžìÀ£~ü’Rï?üÍú‡óÇ_ü›ÿí¿þó+?þí¿ÿãÿç¿øwù_þúOúÛüûßýåÿúÝúë¿ûûßýüÇø?¿ûãÿøÝø›¿ûÓüÇ?þïøÕEï×™?ÿÿ™ÿ—“vMÑ,ƒ™N³§õ"¯›yVuÅܸ™XH;Mø‰ëå…+¦Îˆ²…ð:Ëe—–3¿þ×O­5nâàv s}+30ÁÓÔøB‰†Åâ ¸?ä íˆÏƒê­©Ñß‚fçýÄ^e3Pv:Éöö.qšŠr©M»HöŠ,íyÀ;î!i‘²Ç®•d†E5Sˆ¯ ŒZEµ×dÈ~ç€ZúuS¥üDßM’$U\ÀRàú“Öµ‰+àÊjGy¦Ìq š7Û×™³Gq¸Á!Îð§î®¨<:PŠ‡uUó~ Ö¡­É»Œ¤¨iKNïÞµÛ`ô)z„rJ*«ïÊ(¯EKRï‘Ùâ‘J&¿Jª½Ux´h1©|áem·òCU¿Z÷öⲧs+ŽDM¢~¨¯²’”g?3Y=7B<%¡T|êgA›>–<==o!ŠL„ª@ë¡ú`67sºØ+ÎiqÏ I&€D—r*Á5_®?”x¶-Q"²ÄQ.É?0O=•Vê:üaÛÆ7ÿšÉ6CtÿIUÕ”æÆß×z;ﯵDXXú3¦¿öÔ|Ù&?‰ `‹aD½wÅéèiŒñy P'^èobé©7=d°{)Uv=3ðRÝ°Õ4{~"µ€ºå°- +Üp?Õ¶â©o–Î2\GÒVk>´ÇƒÁ÷˜ÉýÔ¶ëÆíwÞ,ûT;{’ þNSÒ÷y~ÈC'‡†î‰5n,ŧ­\VÖÐ`Lñ­Ô»{þ>’ñVª§ê´ÑAªPrÒg=Úø«çPôøQ`Aµûãï¾ÃàñqìµlÑžJ.wõø~H–åxf¢Gx^F1‘Ÿ¶Žj{tÅöxeÙ˜ökÏI¢ôiú®MÁ•Ä ]‰•¸] _ZÝú©9ƒ>xƒ Ùð3™xέ›: +Ií¼ÕŽ²qg ´ª?X0Úó§üL3ÉŠp‰±vqkbÉwÜ5¹%жl¢ç;0omeý)Ä!æw¤Ÿ*]hé@T¹?ÿÎÙÄÅÕožêDçŸ)n-AÇ×ÓBÈ‹Y‹0¶/{áƵ$ŸZf/uç½­›+Ú÷ ä“ø„¸½†ËOËZoã&$QgX™<*.§£)$­ÑoŠF]{ü,á2Ï5瀶€×µ£êé¸Ç£ÚRE#Ñó™KÀ+¤…gÐó}Óªñù^ J>„gØ) +-¨¡Û´AëЈ[ z¯j$nåBX×Vj)p3¯g·µ–Ô ˜´XQ#šˆ•‰r¥ÑeQAs–PiDPð&¢ö;k—)Kýë\ Ú+wq…¢0϶ÁqŒ¸|?­G€÷Ü:¥Œ¿¹‹ÈÒÒ^[Kª{RfÈËi«¡2°ß6ö#‘—Â¥2òÍNF7’ëí9¡Ü‡×5±S6<µQ‰ÝCÛË#_X÷gÚEyàΞa÷7`ÍO6º·¯åÑãø´¬ãPزª³…©ªC¾.Bý˜Š«Š¸Z²/¼–‘Î!¢" +3 =Cl—zrЭ#A™\'âñb_È +9JÊ®Çè¦>Ð3Yw˜ª]Ñ Ö~c=ãßx~~Þ˜¦`øÀÎJ$^jGÒ_Axç; +] ¶;H +Fè‹v…wpžÇñ †rbã[÷²š Áµ7à䆚{ñ%Š¾…îZCI“õÆÒßk=›Ý󬂾 OîŠÖ0£¬ß$¬{¥0‚W¢Õ®Ý[*Ëm;?ôɪs<ªì(:Ö¦.«Ã´È•×˜M WZÇø&ÂU ˆ¥²*oOtú•o¸¢‚±;Ø5Ü+ÍԂʦååõh%¿ôP8xÒ2Jñ!ëüÝE‰E~=ÇQz§ÌßdžŠ@M ØáA|à9ˆ«6U%’UÐY†o½3üâ™_Å…7ìÿE°:åÊ8í½ %X£Oá‹3@9¯9öö84ú˜BÍ݅įlMÕ¯’a&o™8<ÝëàùÓn3ÓtáùI(£ëä5uÿ怯âù“Â+Ù jè‹ÂælEÇ×ßÑ«ÔÄŽð„®Cõ½ Pûè÷¨U"¿¹k¶»Ö†öã& -Q‚jôtp2ÛðZ¸3%éý Ö`XoÉQAýæ )sÇ-/·ŽÍ­-V±›ª +DûR±_»$L3 +’´•WêÁ’"uJîèS)­ºµêuÙÐ6<¾5®½RU¸v‡Å™uc½k§±¦ +ÜÀî|nžôa£YRX‡„(ÚLÓvJ~(´Ç™¬)àDTEiØ(=;d’‘2äš­çææݯ †ó˜Ô&àDõD‹íÄmz,Qè¬ßðnÎÍÔ:­«Û<ÏE£fE`´dDŒé”¶Ëw!mþúË@¢umGÁÅûR¨ìÚõkûQôC\S.ÖºPÖP«é4T�Z?™°æh,¤k·×äÍÁl>´_^Sðº)rtѾ|5kç½e÷Ʊ®ÈÝ&ckôøÖ3Î,·òbñºü +Ø2QTŸŒÆ6@Y> õ†[¡Agëùâ:tjÕ¨z&¨dú+g"\‚àû—Ìo&ÈD –<ÞboœI⩠׬þ4n0eÒ‘}×À^r%,~˜ÝD ìBï¿ä`D ãÍ1^(þï+­;qIó\? ŸéÕƒ»–£zñ†¬«Û‰ Yœ˜/8—Ei8¦ÞE›#V¡¬@Îç:ª„RÛ^/yúÄf¢ë}Fn&Ô{ÆÃRSë÷%ÓÒ±H뉌óNœdP-l‘„ÒôÔAðgm: C§ê)^çïwÖ-27ÿ\tª>¦= \Á5o–*ýuö#ÎÚS8EÝÏIyõ.„ïJ…}hHë®&#×Gr)¿ãÐ/ùî‚Q<ækŒ¡’î·:G^/SPäD¦S,î0Nç¨ œ!>hš-æ7ÖÕæëAüú â.‡d)ÖßØfô g 1,еˆò —ŸW…½ü¸¾‰pš¸¨h0Øzq›ýyžIÒY}, €• ’ž­½˜ùÜ5…ó½a‘œ¸ãƒÚtÌ¥ª +Øc ÎÉ27/ÍxJ»mt÷Ø$™¼"]\UDüŒHÙV=ëWŽúÅ=òÌ©šØD“{cÇzÔùMÄ;Šô]¿ŽRQ%þ«S¥3šZ«:2ˆ€§MY—êëÈ¥SéÁF‹†ÜG€¿)!tÿG±ŸgYéýèf€Šº®Ý1WµM¾ÇG ý”¡HìÝÃg”p ß'<·È¶½0âj Ʋ ª¯#ÕTHŠPVº´ŒÏ€g´ló×hƒJøÍi.EéA/5W½[µŠ€Õ PæäA×d’6?oÆ–L4ŠIˆ¨ãŒºC-nÍFJYHß‘UÐïBv‘—F,'ýت +)¢|.´–,ö14JÐxC’vxš:Äýb?šÖëc[©DÍ…Ö[À9ÖàÙ?)iT‹šK®Ôm…—nÍ„RX q@eƒ~¡MÁ^nýïòGãʦ:æz[3ê¸G¾íu~Ï å‚´GgÛö¤D8˜ ¹/u›¼¢ãuŒï"Ž˜+‚Ѹöëúô6*˜I™v“˜X;ãG¢fkÜ}ÞïÇs«.¶Áìvy ëÇ9ðN„ ¥‰ÍöGî®!kÄ—þmÄLƒ¤WÔ&ö#ùˆB]SXÕX÷™o{[ßiPA"Œo¡­‰‡:SA}°z01ù xÞPwšŸŠ N ¦5\º)ßü™O•ä‘¢JªnïQǶNA¹XX:÷˜cO?]TÔû@îìR”Õî  ~Fä>d:6œ¿9ÉÈ¡ø× Ícgdcgd¢¯FöŠ@$Ž'‰ÀöðÌ>¾Ö8g‡,J+nÎ'…/öT¶4ºŸPÄ]“÷Uè×ã=ö9¨ñˆÁØ‹‹ ùAþEEkDfÊ¿D0`‹Ú:ÎÒã­: +»Gü Éì=ƒ,T&Ÿ>Åÿf.–Ɉì“ ™;h®(IÄ.ò + çá¸iþÊJbÿ~ éDç®™ÿÀAÿîã#ï }¦XÐ+bF§·°‘ä¡%yxèŸQ=’rwÜ3@KóÁµÆY»qE[•dˆ„ºƒÇ‰$¨.7›é—Ì̱ó3zÎèç†ØV$úŸÔôÔÓvM”‰PI;å[¹ Ô\¢z§UŒäÛëÎÿD ç×¹kLhàîóŒ© y`ÉZ[SdA¶^ÏCán¬ïUÃJ+â¥ñ$¶ØýȈ—+?NÎl¯LÂL‹lžÀsdU©.”•­Ï7 N¨3¬åÛë\é3ªÄ˜nËË& /OËæøÉÒ€\ä³»õÚßs„®Òì9(B—ýÄåäK¹¡–L›å¹ÓíÏ}!ÛåOO™pMe+!Oy«o WæÛ¡g±„ö)­ )pŸÂ™°0ójª‡€½@_^à;®Ÿ¶IÞ#~ÿôs`¢R˶Uòqž†yÏJ™!åè.ˆà–ô*ô3ûwß­AªðP`§ëÊÐ4øúÜLÏ}n£Lü›˜Ž»6fu}Ù5—vmÜgäÄènyïh§Ì½¶."kcK¨aZ§¡÷ ›â%Çf¢£PIrKÿÄB8­Õ 8ÆUL§p’O[Àí õô5´O0wÉ>€šÛˆx•v^lejquâ.%ÒýëSK'¢è¶Ý†sl à%;im ëÞu"þæ{çß|€±|lå«=óvŸý9@aª˜>§kQ˜¨±[ïÅ»À§t ÷@¨u Á*@±Ó¬âˆé…rž®sí3j ûp^Y.œÔTQ½ø÷¦ œ>’¹½×½{®ÐÖtÿægPܽý0º u®‹i|t¯Ð®ge._Õæ ô®)–®ƒÌ|`d©Rù÷S' ݽ8pª6ܨ Oý¯Gñðt7`œj…ú×ÿ|äÏÑؾOÎ=™mW:,§@ºäÂÖÕhÐêÀ{Ù½(ŸusÙ>(Á˜¾e›çA^´&Õ¦_(ò¬5 ­Ç  Y\ÞÑ,mêmN`H¸\F…„JO,._®eNm¢Óëà¤ð„qâ®âùÁb{ÎTÓqn¢ÑR¨ A;UäÑWzsQ$K•FR·è§Å¸ô`÷p€õÅŸzßáHvùﮞ†í6¶A…†»“TÌ\×¼èœ3"!½wä]¸Äü°rZʤG)§²·ƒ­AGr8&FÜdT%ö•ì˜ lãgÕÏÁ(6ˆF÷jD$äV-5תSx~|[téÛþ.´Þ+©½Å*öªA×¹ùñ4¥;š¬uëB–eî° +üŽ”}(~iÖ+µ^ˆYÄþ<¨é—‰+Š67=ò#„Q·l~ÝüÅÀíj¯­Œnî¢àëà¸ãKÑ0Ë*y¢(pnÿq€P„\ ƒ‡­ }3úß +“ð}ß#ü¾¿,ž  +˜Ê>΃ ¸¥J$Ïkùäx~Fœ\Þï¼ÿ…Õ›x»5ŽëâÔÍŠ·HíózE;¹°f•:)…ª¤ÿ‘g Ï#[‡íoÎãá=cZmqÊÀ:Nò÷U·ÅùáÉ~FÑ‚£XÞĬï[“Óâ±fÃfŽR¶01:Œ ÍV¡³f¡n=¿žÉEÿá +­AE÷ªOÂJܨՀÚÔŸÎë%à¢& šãÑÔ#f3Œ ¥!Ê`E1ÕæÛ¸ðÐÀ'ªN©8aš‘AJ·œï[™Ô3…2"d"oß/y’ã!Me™‹¶ ˆªzÄ¥=ùi3J]PH;Z” »çY)¯o‘wÙÇÜ­~FäÙ¼g<ç¸"+ð«EÏsëv¡ÓÅð#ÂQз-÷ëMºzœ7׸;ÆØ_xæ ß[…B¾6jÀÔi#¢5&ê=à÷Âd—÷R¦þÏÓ€taé¢=ZÒ[×5ºCQˆÖ–—š}g%ˆŠ1W õ€Ö¹Ì!†EJ—…dIõ ÆÏÅ%Û—Üþ A48°ô”ÛÑ_'EÉCG`¤û5?Ì3ä“F?xë£?ü¯O+ïV¬Ã(…©¡Å³*:ˆ±­?_Ñò‰>ð¤Kñ¹“\sŒˆ¸bCAðëJY]ioÜQ¾§îÛÌÉ·ç੉Úx´`eW„ۯĦ~¬x".é+ææ~]X¹Hç×:G\Þ%wàV馠¥ .-×¾y=7ïqÉ%ˆšA4Ä£ôƒþ J?lƒŒ P9K¼ÏÕo$Êþ%#¸¤ñÆ9¤ª®ü¯ˆ(ã¹×D1ÏÝvR1HšBÄû ŸìåçúºbÒå 5…¦,L:™(ðúœñ=herŠþ LËžóç1tÿA[I ïÒlϯQò6âSzÁø÷tm&&ùÜG8eo #¿?ëÆ{”z” :ßHïTŠðJº¯GõŠ.êXwó¶C~}pD‰sšŸ/XuWÜ P¤¦S†àg|èÊ<êÞ\b™ìÿÜ°è[Zö‡QöÁß÷T÷ú.0tmBJÞ…Ìrp‘_Ÿ S°Ú;GÍæbÌ謙 €™dú÷¿»)<‚0z?6èT—耨K²~æ×ßÏÀcëÊ:Ï=q¿‚ˆ‡{"ž’ˆÎ¾)@/Ëœ¼Õ?ÌvÎ@ÈŸš!$ m.ø›ya«?cM†sËâÊ`síß‹ò2ýáG¨kòœmú8¸ä4þáàŸb\ND²žÙçÈÝzxênº1Š|js'}–™çqT!eUrgÙ¦ÑY¿°t­¿8€¶I{àvùOpýMÔjDï5.=wûîÏn¿’ñvW’!±5*š¯h–ì}ù⹫+e$ÙwD• ˜>þU.µèL™vù +­ õ*Pp)@´AEÖ3@å¯-’vvÿ:8Žhqišvý¸®@Ã?DY6Ø ÜlŸÙ±YB6 °iã8¿9O¿(¡yqˆÉ¦®¼Åú¹ð +?#Þ—Tߣ°9-Œy1Oé¦#m ê9È;tHt¶“6uñ(Ïl ;@Ò€Eç Š¦| ¦«E×+Û@øs[ï„m¿‘*®0|ÜQâ%O‘ÔàÓì7…Á1¯d¤ÉTšc?=Åt‚˜ÁS×>¥‹JÈïkï*ßé¿ï$99Ubš¸e/|kh½a(H}mxH^#imèðÞô¨éîÇöË]7îÒUû=Ín:™´šïGP0PQ.…]»ºÕ`Ùf@7¡w`ÜÑMuÖ__•6ǶVÆb°E¿ÞÂTÙª2h•j{³yD"ŒÕ †¯fZU¿ ßz€^ò4An”løKVa‰î:ãw TÚ¦Cßî\Àv”xŽÒëVþ‰K•}‡;…m`Êo8'ÎÚ + +“#o\WתWݲØT<),f@‚-íÌƯÅ--”ÜôÄf•#Å=k4­[^y‡UßíÔ[°­ŠøÍcƒàŒúÿY{»U]–,=ï +tëPÖÁvFÆoÚcml|bë¨iJmXj!·|÷Žçy#çªZßì* +ŠÂVï•cfæ—1bŒ÷Ç=K¿"fó´L[{óra¶m37š1VQÙ?ñØ?!;›ÎÈ4bTÊçÏñ˜s‡uý¼ ¢ê}d¯ðƒf1WžÀH…Ó )˜q_hESf`¥†üåÐû üpÎOÑä‘î@MSœ9Þ6õžÛ1éJ•¡ ÒÄ!Y²'I,g«ÎøímÚ D„Šfàx²fHlFhc¼@»&ÊpÌuEÊžÒ¶Ÿ"má> Z%ÎÌÚµ+¢ýstÕ¸—úÜ‚>О†šÅˆ=‡  ‚÷ø±šëJ‚ÕplÀaÇ`éË{B8¥Šê0DYê6sBª(xȸ!fŸ¼a• þ9oyÓ£€6P‘-W=Ø“ÜŸåŒîš:>DLUƒŸq žb®h'®;–i"D¹Ò›½ˆ¥Ä),å®ì1ª^M®Í5±0ŠT À´Vh©NaS¶…£^¬SÉþ×ù&P ìe\ [1¹ÙݵVÜ[¨+o Áöé´ô£2s•×GXõz*ý8¿~5à+ã}Ú¿] …¥6¨B*hm?sp™FP·D—~ÜaÁÏ œ;’„ža$›«~]@ïTR²Keîª/«öUs%¶obŠ£+¢à—Ì…ù¢$sŽè#3 HV¤øúºÒ“áÅökú2}SB Õ…µõ–k~½ÃK;Ç'‰åŠ(؆E§ç…×Ò +,G hâar-î¼4é³½E&oÿΑ~Sýí±Ò§ä šði^¶(‚¼jé#«oü ”v¡,¾sP@h^‰&4W’\‘zC àyb–0°W4¶¡ÎöÅ¥·²±^‡>Xê‚eGÀ”]MpÖ …@®àÕiöštuŸ _ÄrfÓ‡fµú&~*Ë°8 CmÇþ{¡¤ÓUèZZ*™…I/õn›°t¬Ý]˜¤U£t(‘¿Z@É0vå`!a/îÙ¦s +©{g±ÙÎm¤‡ËÖDIbÉ@ÒËåÄ‹kYŽ'} +”¬Ln+dñW–L2É+äáeØ(–Q~ÆÛE¼VГ/áäTM~qïb. bO DP% ”-ØÞ$ÊB5ï\pno;JQJ‹£´eË|—&è7)à¹];r×uDÛT0»÷’z@Nìn;°šî‘1ˆIµK +5<\lÕÝö²j‹Ô&ºTýÿòwòûßðÛÀÛk6Ù8=¨GÊ2óŸsp À¬X©~ˆŠâN+Fš]ñW€4oZ{wD"§ð-ͪT*&íeåYÊá öÚ·Îñ·¥² PQ²Ì¿‘P‘;-ÿŽã̧QF­Éíì:˜§›þ¨š[m|Ó v_GÕ_¯ØMÕ#⫉%™6”Õ̪ ‰É³ï‘ÆeMBx%!ŒÁº¿ ¯kô.8HÝ Î(Jþû±¬T^ÏÙIŒÒî9­’wËFü×ÙnÅèJôÍöÁ n‚u=ÕR£ÄRÃz K7eØ(Å—Zy¡N‚ãsž>›Õ]-~Kl8ù8@iÒt¼¼ŠPÌT¢ïSÉk5Û=ÀBú!ÁzF—€D¹’eÞÙ“…Š°ŸGl"{/#»=O¥Sa}m'3§•ó4öXéš5´P×q¢ªP®Aaº÷A٪Ŗѩ¸ù¼m±ÛqÒv¦ 48h©—`›‚aÆX=(¯æ̆ ÆàQàñfKÇ,PPÔ~B\]8JuárDgGø@ær~~!ü-#6År€?¥)FÙž|ö´Ñ²Ã\FÀy|[»×oøÍÛµº})ÜŸž-tÔ«ç±E?Õ gÊÃÛ7LáÆî ù¾Q˜êQûº}]HOßÙbåv‘é‚í¢ U"(ÑÎO¢£ë \ÅÐ3ˆŸÊªZpAhïùqwÄÍ¥}=;ÙøOò'b£±¨ÂíH.L7É[£† ÿÍŠJwAômnQñ¸lœŽè÷‘âUE(vnº…°ÅÆ*w€ÆppÿÀñΪº"8û*f°KZa :Ä9˜?ë-H’˜¨¨'Ê[nì^vß³A«â-^íäß ˜èM;¯¸…)Ìþ¦0l ôàý +µeV1€_(*D¬¨°ÓÑ¡¢ÅVØnÇ~Ü š&5èàý…ͨt#¤Ju5NÊÿÞ‹ƒî¢ƒó8"æ—*ËõêJÏ7+­,G?À¥,yédGòàžCÞÚ?}"[b…”ɔńj +FÆ`oQBÒ!põ;å0mPƈ‡Ž§H7»µép…k×ÁÐådçú"ëè=RÑ„ÙCÏßò/$ ínº‹à«Æ åá+XÜÀä*dì¢uk«FLfOžý}@4Tíœíq©Û±³"fKªpö¹ÿ=îAk*AŠi ìQ_ÎçpÃY•HéÈ^Í(4Q/Ñü†Š +ç{D #hMÔ´Z-Â/J!*lBÙb¹.”ÔÅvV§Åµ×'´çš¨0ËíwvÊeôôœê•ËaËLW¦ºØÌÈ·^¿°%§~ Ð:Cà ú×s€…‹œeÜÇ°­©-÷t”pOÏqÌ]µ÷¶Î]³K+×™Xiì¢|ùDûÆÝö<Ÿ¸7?¯@6B¬ä@Üb•W +ÖÜ1¦¤<Åîi¥ÄÉ'¿î#«Bâ̧È"Ï}x’W̓g)©©ï,¶%P}żùT"¬¯"¤ÝK¡²qºX8çL¾Ó°ÅåýÙ£Z³î¨ç*Ç€T® ÌHŠ»Ü·¯íGI{r>J*ý?&ª-#4ÛTN€«ï7¿`&Öˆ ÛD îÞc!~¢uö.Œ›ÄÇëhœav Õ$0S®¤ëïYV‘}›aÕR—¸nQ¤»ÎªxA«)¦Õ5ôäà:’p&]yáˆ-éX¥-·ç'¨c× +ý½ÑΩh.ŸÏ o¨–Ø%¾-Ô:²ñì¢-^bè4¥=Q¸Ä kåÌö%8si”B—ó&Ø|VÉ ÕL°›ØÛÁîoï§C"öQ«îÊP?Ħ-‡¡¹«P±—P¡{•úÓZêR—i¾Ÿ2=ü‰uTYòÙöø¬t¸¯5‚â2Ñö»6ßAnž³]¹þz·={ÂPÓqâ:[uýaÁîŠäd2XÙã9#^x¯ù2Ø,¨­éD{½¿xüß$ PšŸ«bªX‚"9$°µÁv9ë{µGS\¨°!"u¡¬ýæT8wǨâ*ž¶°˜‘m7wêBrºuQRãå7”ôi5¢Xpå¯b}ú…Š•€\ áÙ÷lí4þS2eÕéü ä†\DŽýÉÒóÀiÃÅ ¯õ:z-ÌйВºpŠ¡-d7°×ü(³!‰(¨Ñ 1µ×õ +jŒTìR/;—ûÊäðjÇPõ‚¬Y†,N ÷»6En â’®ÝóÀs÷ö±š‰K·„°Q÷u#d¢`RÈß÷dŒ\`Íùu^Eo©ÚŽ 7³Á°åªö‹6~£©îÄTµÓ¥Žj",!6gë¯ ö©ø ª•”ëD†íáSÅ«Ê[ÈU?±‘ì–{(·ýCSÙ÷À’˜o§UX²FĹ„,•©`O­"÷K²r¨À•-ý ‹ºbPÆ+m­úoSƒv ½Ã˜”Ô£"*I¢d“7@iºÆ—¶Ðz3*{Š\ ÓñuíVå÷ÿ÷¤2 ÀvfçiÍ:jvÌì7Æ/ã;Ô>’¬Lû‰Îh= 5®†É`*0ª›Ñò¹¾d@ÊÑt}sD¼™ÂQä"¹#@ܘ€+Êì)@aü÷^Èn1ºlì[ÖY²o¯€òºN ª‘EæB¶ ØÞJa%ª©Õ\ˆðŸõ@ÃÃaäú]ƒdLOÞž¹€Cdö › ¯:î =î<í+UFØçܸEìÑY\wWä5öOé¨Ã9S¿ÑÁüVœAÙHÖßS3"—D™î1©@'"²E°3f¬Q+t¸Ûí¿ñ<èUxˆq½NÒÍDʨI›G¤–ëv=÷/,ÑP^ƒqUsŠw .‚ª…íƲOù‘BD×›D°ëm2ˆ%€ƒŸtÊ¿äÏP°`¸Aõó2IÒQA–;ì:÷2FªîGªpÕ ÷,¦ï ¥b`PòV2=Ññ+Þš¥/T®ñNs契Øu½ÕIMx¼’€š1‘ÆŸr+ ++ÓVDDÔÁHHÁ »ñvdŸGÕz0q£êþªŸK(ãIÖ¢0ôE5`V“(Þ¾ñɪ猀(—í·¯ºø€ÐÒ|©´¤þŒ7ætMãæQdýaCÄ…DÑÉ¡×{¯Ó£³*Ôyç±ôgÄ€|¦úZá,Ÿ_t‡‰V…|ÎÕÉ2ÍLðõ…’j +~ +Çp\nÙèÊ,`ÞÉ)–Š|³ý\*dÁÜž¨¿2ï¸2s·¨4,ëÑÇŸz'ŒØïÎ*Ës¯ëìàrE!§‡ã7ǪQ脹?ÄyX€ÉµBJ'B°úÆVt¢¸F*:àbžÑ¬%m±u_ôS¥’*$Üx‹Þ +ø: +“PI_pUÉ­0=tŸ4þÁâ}q¨ëQ!Ü»P o x“Pš²ó:Ϲa9&9MËÆ7ÉŽ'Ènz².!˜Õ½|Ä'¸íþðt«s¢Ÿ4Zf6ØI=¤‚ŽC¼hÞ2ÛؽæOy@Ç¢õåúô}¶wÅíÛpÖb-ïAÏ qæ*™a¨ ]°ân)Ú'Å&•åø°ví¹I¢ý›CuW!°3Zv5uá¼)—¹çŽ½*ŽÔlÌÔy4‡íÆZ“µ‘Kq§Ÿ«ùJQ3B½E¸‹  I%þó$ÔGç‚b‡4\óak!CèkI% +Q·3q=» SÍèÞ(„" |DþC¼ãª¹Ruµžßs¥ƒ¨÷ì½RpÌšPDYB¹JõGØ(Á±u’Á#ðö<½‰Çh 툩™3>5JîgQ_òæž›Ѷý‚F¸ŒíV9JìœdXu_Ç`Ò ïp^'º\1Ï_GƒdyL¤–&Rס¼5¨Ê5ÿ®%,íÿZ¢u|Ñ=h3¢>Dè"nñ¥ŽÆ¾\ìê~·T ©n¡h#>ݪ¦r|áêŠZè퇋1Û‘çn=°”ûEäã6]-ÊîûZy¾˜b2"°wº•7ä–ƒ¤ @s·®˜ût¤Ç>eFçü×ÑËÛB ¥Ù‚?µé]aK`ô©ø¯i?›_æ¸ÓÚ*…é˜bÑ>‚`ò!ë•cLtÛ‹Æßý›ÐH”žÅ—m§z2G‚®Ûîq=êy{¡EQßÓЈ¸ÙÀÍt&V';ØÛ¼£Ö¼_ •C¨©4L´fÄ£‚I®ZÍ×äu‘[¶¨= pfº=G™eÏå8fª;K®5O½€‚õÝl R&¹Í2ÕÁ¥Š­¨-_ÌÕP4)7µf•à 'i²9¯~ªÊæ‘+êuíléKp·Å+]!E'ÛqK†¸ b‹RK6É ï˜HŒZĬ{)YO„ôÜñí£ äâ÷²ªùð轉å¤A;®P1öS£fêa¶Ñ%¢8Ѧœ3 Tõ×ÝèÔgݞΖUÿ½ô…®Ë¾Ð¨$®Å2ªi-^s–ml úÎÎWêϨj\™hÕ¬CÎ-j^ÐÀÎg„«‚¥HýâTùâ篤*½vózúJ*á*CG! Â!Ì¡è:!S˜‚Iü¿¡íÝËñ:(l °¯“v±F®±e›†ìÌÉêgMO[Ä¥¨1^PlˆÑ@å$9ûd +>o8j¸·‹2¨vûÖ»¼°•B.go¥þUäÑßÞôïþUпòDFœù_ñßYbŠS¡(W¦ò +Pg ÆŒ½{"¸2Ì6;jQºþˆòñ7§L¤R¢ú9àŠÎsK'ûzïÆMËKV!~‹î$&(¬옵f}îÕM‚µIPµnQÏfytO‚¥ëXïDàú"›O<ìÄD°uG—\tzžÚÑLHY3FÄ»{iO!vf¥¯o÷´0´¯ ÙVÝq«^²þiÐ&7µ+CÁú>šŽ‚ÒªæÞˆÙžw4h €_Ã[XÇg Y¼çH’2D‘#bšènŸÙ¹l#8¹¡üE«¶Q†$ûÅõòe‘ÞÙ|÷²íôÓA]·XIÔ$?°ßƧ‡¤…[x4Ê'FílÆt8¾Î…Xðyf”M¢Ã:ŽL§.¹o‡gÛK¼^~¸“·¸—9óLz0K~¬ªÇVÌúZ{’Òƒl¦Ÿ®±è×MÚdÁî=jÐQŸùþ}æ%ÂÉu±¹ð˜Pnžij|] ŒôBÙ=È/™PßÏË­À  Ü‡ÛèƤ@Ðr+Y[<¯æá´OåÏ&³SzÅ7Ô¶™5óè°Êø¿Ÿ³°Ðô祡8¬jÉjÀr;ζûO"¼@3GLr'㛨Û2çôÓìó¼4;a @¯á3Dz­çs¥¨ke™Æמf: «½8ÆJ­ [iâ­n3’5×ëù:΂*ñB•fýÑ_•è+àV®¥'BƱö‹²lÔtªâOc‡' õõ¦h5Tõºl‹„ÃX¯éΟ«€°zØÐ_…¡¿ÈØl5Û±1Ñ;“;+ü‡VÇõóXÍÃÆô>·Ñ‹»ç¼V€™ñ= Ó¦WCJàâ-X å¤¸\àmÖ1uFdþ>¨$ÊŽªœžcµÒ­ç%¥ç×a¦]a¦,Õ7Q÷Ì]‚ÍÊé°žH‰è¨~Á“Â1 j&ROD=ýµzÕÕøã˜Çâ†Þ$fæ¶ÎЋŠ}ë¯ßÉ(ˆ®U€~©‡õMWÌì%g=jL2$Q[à³é~Þ´;ÿß²[Ã:0UéY +œ.Á_ŸÊ´Kf +Á®u¤tµgº5<À(˜ËH@ˆq¤î|øÖÃŽ!ÌŠTè,œ#¯L"Êpu§þB÷ýbº–·ž7k‚þS˜âÞöòú­«ÚuÐP!Z™ž +ŽÍëdÆø’Ë;æä”(Ë/דÀŽ/éR~² +qùQJ‚¥[Ó&úýwHøÿ× Áý3"ÕŸCsÅFº|{6u¢tÚÊ\’R÷Þ1®àn]Rz÷{"©éF‡„å~õˆêòei$$!B8„( +Ç«è6eº­œ‘2)ŒAîKSþ³©×ŸM1ÿJ‡øCYe£k]÷ È&ÚtÄÊM÷€ZÐ2§ÄWÅj‡» ½¸w œ‚ßÀ+¥'Ù–TtÑ :Y¿eC»Ù1+Lø…iÆ$aògWDéÓþ¿1Á5cqÊyÚ“ˆdèTˆhhs%Yá\^¹’ÚäÖÝ»+,ó`göjÛ-¾ÔÈÊ1q!™ÓTu×·ŽO­`>Å›\|#ôi@côíÑŽBZª–§¤OÜ¡1£Øiñmá0f€ðåà±P+½£è§É'B¶M縣‡¢cüüé@g½œ¢ÑVUô´ xº3ÎZ¥ËiP»bhg­GœÌ¦>ߒσœ´Ûyév¬ä_Ü»å5ï’J¼Ï¦4Dtþ\n9O¿cºqŒ^(Ù'"$aLq±˜Ø_9æ]¹@K{œƒ²ÄšÂfþ7RlsƒØ +hž31AèìÑU^Ýäc¦¶?í+Ã[«RÇ«;j²šÞc ¶¤Ý6´õyôéËøýoù5‚æàŸ® ¨šÈœí)¾ž]— nXï{HaáIy/AMéÚKÞ@§äHQ ¤H¥D¤ +âB”ð * öû ß)ú«Àþ¿èh Mbû2=OÔ(§›€Á<+Ý%Ânxl¼ëœãBÆk'X/¤œž¬°.ª²ËßtÇø³ a4K)³Ñ÷¯úY§† PhœW…8ª6ÌK€äü‘}Q?-Ïzw ‚"=i²ßñS¤ÊFµNëžÈ®¸‚zÁ(V¾)2üðM¯^ú±È ' +¢öI`¥î!]âââÌÐŽö0.ïH°™+Eùb¡ÎéÕõˆ(_%Û~j~”£)#ð‡Š†Ešn¨Æ]OÎÓn%@PyMDgæï\ z0´%~¶ºÒ`BEnâ«áñ8Oš^ªÕðAï'7ƒª+·\f6M¯ä(jOß#·Lz„ÇÕ»ŠÒWgÑhdNôÕºS»G0P§v¾çb(Í&Àùg1Û=¡j¾M€÷àa7!® Eç"ñHQ_Eâ‚ÉÙoOÔã3ÞvÉþ(íbüñ‡ÓÅPÐA’õM”Ê¢'ïd‡þw…rk±½þÊŽ¾2ªmÄz2ÏZ3Û¤ž +úaVî £þ8WÔj+‘çA€4W¡yLƒå-d+øÕÔ¨NÔÞÞÚZ’Ž­ß)õ…!(rë:P.0½ì‚US~ + +xýY2G7—(œYC÷쉱ëym)» Nˆ—hakâvÇËEŠÓÚ“.Ü.JbA¸Žß¹Å™pyÒˆJ{6²·þ< èëM0©éÔ\”‡Ÿo˜·Ñ•ãÛ~…?£$!ÇhM<¨FÍ–¯Í†'oŸÑA”Ò_Îóý +tíUÿËõ°‡%¾/Ô$­CêÍÛßãu¸“@ +T[>ÆõSñªtÅãYÛ[zT öJKfL×cè~^ ’«ŸTzTU#Fú<~!`§¬îíAH)db×Ô…¹Ú¹€:…CÀδ/ýM8TYóF ÌOAoùþ¢÷‰€Tü–ö¨±,Ϙ^ä”ÍaúU´ù(´%½ôN­Ÿ*'Èž!¶Ýw¨EÏeð|ÍTÉuŠŠºíËmI M;€Lj"EŠ*Ý+ëhƒ…£†vÃÄ¢§àq¸ÅF´¢p [Á÷‰Iâ<ÂùÑÌÇFÀ3¹SîJ«wÊ'¬—Ð_ÈŽþ–£vD7ªf¿–B-n¿åX%öÃëTýnž´N÷þÁžý8asÀ~2…R“fu$cžsÐÕ ¦ë>¿«ëŒ,Ÿê휥¨®›ÀŒÒÒ¿XõTÍÅãÜy:ŒmÎb³0°oÁÝïŽë*$ P¼{€oô‹¥³%ûXGyüŒn©—ù€Mp³ûÞG½%Ë?àÇSsò ð_°iÔáöÖ¿cdtSäš–œöCž‚RÖûŸvÆPΞ_ ±–, ü@tî}`*9Ceò>ÞÏü²g"ã§ø B4’Õrvl+Œ~´z*YÉ Ïò.Yó'¸¥/ï£_Ž+zÜüL@]{¢\Ø°äÀ8^/ùLÎ?‘=U¡øÔ–÷žæ¥Ìû-¼•±§1à×F^æmÄxàä Š8³à`[9(fA¼xN¯6ˆv\ißD`Qs»Àšê}m’ÿ4JIóIó=šè¹DF¼"Oà +J׸ÿÐiÖÏçíN'‹_ª#QƒÁ"Çëz’€f¹žÃÈÙ?ó€„ó=¡‹ ƒêµsˆUß‘~…ºŸãþšpƒ™Äs>+¸MŠ[—ë˜)!à¶?==ÉËfP—£wÇñt®×û"÷I€Ä@Õ|5Ð÷×…X õ¢[$r• þZ2üÉßò…EINå™F6ž1\J¯½÷8$é©®?:pª),âTÝ~¤òÉ3¾ö'TÕ'É„±Î’EËrãP1 Šð'¢u_¿ù+ý(3Òµ‡æžáï¿+ÞÀ­¡1vŒ˜Ï1ÄInhA@ßçeëX…®³sÇR…\Å[<ý>¼c÷FDn¢$º¾ùsÊZŠþGRÚ¹M ”}ÖÏã&Ó%òöøO~•ÆžÐëvL‹}ÄcæÎ-ïܯÂXÑ¥.«ªg0mðœÁ†ReÕ_øLØ6~w0gÏYXÛ•…u'°£üKUsw¯!âˆHÔ@Vž2ºøou`þ€î<¨ˆèš÷èš¿»¡ìSÉ_ ª(úüúW-7UNê ÷ñ¦d…s Žƒþˆ^MjÛ=¿>»[8—Zc“uy/ó&0û9í•p/‰ë ;ÜçÀñ¢ïÇåïã wó¨Ÿ0t÷®d±™øy ´WÐ÷9“à×Á† ² ":¨ ¸‡¨€ž *®ª_7ìHh¨êÎ7ìdÄA½P”¤]ü•ª@{žðý<@çJ‘^ªªxSôCÄõ ‰·%ä¢øVöã€ÙÊMô¬‘‹¢æçóWŒ³­Doò,Kül“4äèñQÛ/€ý£Ihê4‹oÐÿ¡µ¿0²¶3…y’Á¯ƒJŸõHŸQµ[Âüú«à~×µë%þwx ‹ƒª10Ô0™ËÌlß„‹+ ¶ô«M0Ï’„ò{f¹ô¾ì¦•BmO×1Bik݈ïv¿ +ÄR½_üáí; 5€÷´Ï(ˆ}aâ‡Æ¢H>Nqœ|եψïfî¨ÛV 3RÀGØ.ßQ´ÂàÇK~ÜÒÀGë“À˜°öyø]ÓñÿýgOÉ´†#šÓ°]¥-QÑx%Ž|;dÀ>­¤RÒ‡-2 íùWÚA"?x";ÐëøŠ’ð3Q¡`ÏÞ‹Ddo_²ÃÇ9;älé×ê­<ßüùžÁÚ<Üúj ’€¢%¨­+Ø›{·ÎÖ8ÌG¦zJìíuW0ªž¨òFY'X0¤Îy„Ü\‚…1O„®çõà ‰°ÂQ"ÓGŽ½¹Ò±sF݃É@"æ"&rSŽÆc@LÍ7ï@oÛ73±lϘÿH‘ßë¨ZM­JO謲y–×o]v÷ÞÇ“aQù@qñ:DÓ0i<ª–CÍ2¿Ž©¡66}Î’§ÄXý¹M…Oyn˜½d}ÂiMP×îÿÚàÅûõ®ƒR®5Êÿ#Zú´ÕˆêÑ$z~CDQŠB,ž‡…rŸç²iù +šíwÏúº¿.°TØ9úú²_à0b¾‹P1‡Õ’¦’`㯳€Ì5‰·b…ŸlƒårNaFÞú_L?5¦¸®»¼=6´G‘çðµW=ûî]¦re0{ÆÈn?²¦xŠ0ì+®V­ÒîG#ê3b?Qz[(1Í·ÜlÔÍÜ(?»¯O«²¿F3 &AÖîý•vaï‚>PõRjæ½ÝÆ^.lKã/1–‰B[‘5ZÜ)ªòšÚs·©ïSF"‚î,‰U=)N ûö“lŽ …ý¥ò¼ ÆËmÛ¿¶Úµ#nFQ¡B©O˜s0Ä8‡qh¢ê`ìùÿ\©‡ +Òx‰¾ª¯eq¹åŽ0Á¬E ÉZ 5™=ç‹ ‡Î)Mó$ë»tß„ƒïÌæ‹ÒëqrÜç]ýꉂ\DD“ µs“qÇËXí´¡¦´ÐÈÁ@Ú Ô›z €XžŸWr§L[MÒÜ»®åÒþ÷꟢ŠÌŽ  dD×lŽ_ÔŸ2åëõˆÇG?»²  +Ê<·ç×ƺHþ)º‰PÄ¡¥; zùÚ_ê("šIÔÃ8ª_^ïó@»³úB…õ¼æ¶bHÊ—ªÿDT½®ë•vë-àÿ¸Ñ©¡>áoé㸠” <õ²ðuàjºñeͿɯ ‡ÝUÞ2L²ªºƒ›Õ™»tÕá™ôÖ¡<# )å׈ ýç8Xàs÷o΃¤ ÔSi^^5÷‡5G Ç%ËÆk˜çÑèc@tFè¯_³„¶ÃþöÍiör­›=uÈc jß°À 2ÊO“Ö Øó¹Ë èŒüs%èSÃ(zû‰¢@)Sø@moI,€ÎFWg„ƒÑu=ùˆ¨Hgv4#Þùˆ(Úî;ʪQúoY´\9O¨NHˆ‰ø(‡T®6Bp(ÌëÜ‹ñnù¥þßçJE®Í÷[ºöƒü‘ÃÚÃ|ú“ V:z ÏzW†•´Ù(Ú€%‡"˜0#·òĘÂÜb¿7YÅ6²õ‹¬Eã3"cêHÀ}üùj‘÷î¨^PÅÀßÉ­ÕÞSÉWVšÿ-Xä>7ú¥ûVÏ&ˆ/šàª@öÞÐ× MLpXk5¼…½ùl3ÏÑjHG*ª¦oqƒ=Ö‚ßæÆ$‰±ÓÊŸÒô80®bf¨WL>æˆQEQ\n1„ý=ÅŽ9NÀLÀ;RÕ;éÂ×}•oNSŽF â^æ".‚]eáˆòræ;Y/†ø#ꊞ(ÞAæ—בÀi˜¸Èàį©Ò3ã'=yT·Ž{zõY¢—+-Œ:¢åÄ$6á=®UuÓ ðŠÞD€¿`DÑßKTp'Âz¡oñÓ`%ö1 ‹øã‚(štøÌ?÷oü+ä¤×'Ý[ÖÍ¥(ó>/ZñÃhk×}½úÛ0ÿgàh3À·¨+ví8‡:µÊw4†•zu/ðí—¨~¤{Ëgxà¬/Tò +Þ"%E´SxÐtìŸ)=¬dzÐÕ7ú5J ³ÂPæ.ÀV~E ÷s¨pWR,Q©y.ä+ç‹™–ˆW¢î•(¬HþîÜÏCÏé–Ìžß­Áß0a¦ê3òlM4(Ð¥°â"‡9¯úGµ".HôM‰‹šA¿+‚S--`OÂ",V5.QIãçö¾Qî3Ž^9£ÕÂúêð%Ž=u¨êØtûâ~YD=çG²ä m読PŒ¨ž¡…Ø€3yMS`+='gßãm”y­{­çÑ)¼ŽîH¶ÅÎ+ËÊBnƒ¦úü-}±û9jJJ=+ڥԳ™½ß–:ß²Šáî_¤Nªõ““ƒsÅ:]ɺþ µEF¨ {'{בZ3BÚ\ŸýªhJí‘¡øÓ¯#µ‚_ŽQ{6ÓcQ](%‘‡ÛY9Ð)³áùˆ“ðl_Yy±¯¡¡ß°âP‹€!höy€Wµøb>¢ˆàÞˆè”Ø›.a·HoøjFôGLs+v°M—ð!öcÍF£ðÐ[¦¼×’& ï†ÇùtOˆ­rt°ªNDbqÚÐK ´¾·¹Ðœ_Iê;fKy-‚Øþ0@9  3îfÄÞMeÅš~LGÇ›èù'äsQö¤âÝ‘L ùˆÓSl”ˆý…¢3#'bgDPËVBL.'%ðv:” ¯odëö€À_Ô¢R}ü—J7dô‚E y¬ œ÷Âßzzùûסš »Tn#vT\© ”ghUxçÕŒv¿K‚Ã~Y”ý +Ž‰ƒ%Ò;õ} 3Ò*‹i‹ìŒ àu)‘"ã>ÁžÉ¢I NjzÅFy¾Ë‹ ^^ô%W*ú©O¦lŒƒy_é¹EÝPÅBE5“êDÉtbn©éP¢x;“½ÆíiÆù÷½÷¯Á¡x 4„!gÝ`߀Pèÿ:?t=¢ & +3Q a|#:Ž­ì#Kîsï)[‡=À@\¼´CÈ…^î úÚ Î=³76$…ÛŠÙï¿F|ÎMj¾•GeÈÀ©™­ÌùwÏÚ›ß! Ö¯¿i¯@ ¸%¾`1ETØ4|ä7¯w¿¾õ.d¿=ÅöþÅ`U=Yƒ0xüƒŸz¿­ÕÑGÏëúå@îgíþæ¯(p»ülËܾ½[¦q?5v½ƒ§2ôÝ¿ÿï|ÊF9{Ÿ—!ÖŠ¨‡WoDlxaÇð^zfœ=¦êå­\Aíï!˜D0IMªoýëB’BqfØDg› ø3ž¦ÑðÖVº$`§Ö°ÿ•©‡BÄBûȉÿ½L—¦ ]¾†ù]CQú° +Å_4`˜”ö££‹…4N“ÿóñ¸?š äúò¿‰bØûi +QJ-Ãî— ™dï¼t¶&N,îÆC›¯ôˆ 3E²÷¨¼ë›ó˜R1º†í‘} M“Š¯ßÞ÷Ø„k)nƒEáWJ§åökönïy õ€jQ]q9$Ó ((¥œ¼IË¢šÛ™]ø>žBKõf’pæÚª,5OÀ¥" Œ*݃”_$kGâÀ­‘\Õ¶‹´5‰Àç;à  Æ p&æÚƒ¾Kø`П‡9ƒAÜQ¤:Q|)D¡!èy¯aöÜ9‰È&ñ”ÃNBÚ<æ[Á„dlÏ·[±%FƒŸêFV±=µ?$M±ùf;ß(Å’Õ¼Á- .§èM™É’pRúˆðJKä6e:ßœ§)í0²ö/ßÛÐÖU¹0O¹’§”u1§š§\æ:÷+Kcó8OŽ«ùŠšä; H"ø­BòPí5gbJ6gÉ™þ|îõ&áa·ƒY(ä ×ê“*¿¢`¦jŒjò˜©Ì+Î¥>’ fø‡"ìc/.ãÉF±ÜÉcçVN{óü££Û~<»8ÖX' ÄÛc0€÷³ÄÛ£ž©ÇöÎ`ûݱ÷d…,8Ï4YÏ4yçüLAD”rZtL· F¢¨ZÅy¡¯ÐŽ=ÓvŸNÛ8!q¯ËˆQ Ø+ ³ »\ÏZ.4Ø‚…)óº.)<…ˆØ^W?ÏÞ›.|dLgˆí. KY„ð°'q€óê­-§ð"áÜŒJ³ìQ•D»‰ý­»Çˆ”‰vj’ªâ·ëÞ]]IÝ?ò{€^üØKîççYèsÑ¿¦]^¼¡4´ÐP0ÿ عÚË¥ú8v+¥æ#f´c¤Áçb×Æ0¦Nu˜½f‹JRÀµœSâçJyÁC÷~OD×™Ðn7A¥ª>‡UÐ6s¼Ï5|nаæÙOÖ;|š>Iˆ»-”«QZt)œÁü/å~°ßkÃ(4„!¢Z“X¨HçNE’±ßîG«*Kç`²Ïˆ0!=O@™û$soòå Ó~¿ÐÒß»5ÍŒÞTí#ꊔúˆxpÃPÚx2ÒÁ}T|¼n?Ð2¡Ÿ¯±úGDŠ(4ß•ÉHѾ9P7k<7 +×Ô:z”´©Î¿úŒXGÕ›<è«8ü…¡@ÚC¯jŸGEø¾²ê~T.\»+µ¤›ÍÇí&¿âÊs¡ŠU×)ðÎßâqÃ5*þù47ö@7©†Ð4Ú¯#¥ +ŽðöµV=¯gïKšc&¹ç¶•nVO§ìè`évKÑMf8‰®«^ò‹~æd¤ì>Ñ¥ó—º{Æ¥åô˜(Jè§Cæõz7!;Sü ÔÏêJjÒìtì²N=HkI™:M,Ö{60BJŠð)Ü m2þƒåבÆtKY™'ÑÙh2Gî5^»7«:ŽI$ßNNJTa+:ØÌ­Ü´&Fêy,Ö²†€ð0‚Õš+Aiëî½÷÷1xÄ5õ#‰Æ—ÍCp!*镦fÝsäüdž—+÷Ƀˆ +MË{líœO2C¯0ä­§ÞÐÕ>½¸ 4 ¹Í€ûEµuž.ÆhƒŸFõ©tjÖ“$,O˜Å±nƒ™…ý8îÐ>stream +rÿÍ„Dk á9ì£h^Äòó ~Æ~'8·Ü<€ýD(Hì§rçGSN’WCíig„‹J?ŽÙ´^ûˆðJ˜«Ý(i>€<¾;Ï¥}ƒæÝ(wï+dlQm§ ÓÑ‚VÓhus·ÝÏ+üÔœ³9Ï> êü¨ç» °J° t–œŽ€£[C2¥IW¨‹vg£O¼ÚDk?–uI=ýˆð'U6øø²d¶oNÚ5o)ۺוȉ`ùwtÖÝ£‚Ð;¸þ$¦b4o½'ÕÆ ­—h6E¥-KÈŠ2é¡¥ WÀöaÎüM€º¢ï½ŸÝ^Ý ×}œÆRc§X#µK3@FÂÕ‘ãÿ.@ŒãKVÃyrQôÀ# <¨þ]¡ÇÍ Œút‘EQ¾!Ý£ÀË‹q ú%âllõl ×›oÎðËg«w;P.èU.”÷ºú ©?åmƒIÍÞ{SÝ>WzÎ<´¨¡£äPÌßyLÎãN1Ä-á1ÊØû-/#ìR¢ù¦šˆšÈUxtëëJv;G< Gãræô47ÅJ±Ò•¨f"€r!óQTªêùIÉšP¿ßŸ„+ƒORê—0¥P +dsYeƒÈˆTjá6âq™¹žõŽ·ý¨Î×nNòKD®„äDýîÏIÎ,®-†¸I•4;EõöMÀ±T¼ÐÁ{äϨˆIyŸjðS»À\d~N&R÷ê†ZXZÆT³$ùÿá•0IwÅD#¾~s`k¬–:cŒÜ°>îבÛÿŒˆB#2¹Ð/Q-àgJ¾&ÌÞð£‰ÒcÝÊtóÑ©gÔToË·µÍψ÷M}eŒþd ·‘ŒÒ¦úŽùÈç?»½ÂQ§1Þ2ç¯Qp¥ü*hÅ™Äaˆ%˜£ÊÅÂE耟`O§e®_#òĺÂkèùÞ¾9O¥ †¤c×Ëédeýde<±úd÷:ö:n“$ó:Mp‚}åàÎ9[ºçgŸÜ§FbGŒé¼úøÎ ØO×wé; n¦œÂ‚kî×zóHX%ª<(%r!¤±Ýó FÄ5m¡Èþ表ÿÈ9–0¤Œ˜ýý%H 0a‚7\á/ûRÊEß Ž8Àá ”åDóíä¿LJޚ"xð¸.”>"²¦-;@$6ûÛúö<Ïù¢ö•Ó¡ÑG]DÇM¶ßD¬C«½¾rÙ?B*JA:@Ìkš£Ì¶èjÚÙW=ËoÂÊu_å‚Û]p +­Ý7lo"Fê©Ú¾«WOÓyU;¢‘ +W}Ñs ‘Œ£Œ­ŠÄõuÞ¬·nãv÷N®«ïyÉ&¹²Â‰x¨o¼?&œ>½7šOrµ[ª/lôæë”N¯ÉÝ 7YòúA·î¨‹Dçû(Ë´ÂR ¼ƒ],ÁÆ7(+VûFs½¿F'óLr&«- éÛy°v@o·G4X÷Aå¹|ø¾;[cøÞ(–ý•,6˜DRÆNŠ¹ªHÜ((漈ýXh½± ~­Ëyäûà[¦#Î*KÆÀ¨-|ú—kðÀf‰ŽpŠ­ûÿ²!à×8¿*dÙ’žî`•ÿÁ+c9•?ëëBð@™TaóõT°éãÏúP,la‚z–y ç!ÌÊ0MGŠ­sý È´ðKÚöy¤ßÄRÙ÷Ù±}³œÔjÁŸ‚vÁ‹ðúˆb£hÎ:ýèç†ç¹áÓBD܇¢}ú=5îô‘vÜòMÄÎÌDtƒÌãã<`£„c£lA„†æ*-1ÄÙ™C›ã`^°)Ô‰‹ú÷ƒ]c¥»vO÷´¤C=}žÒ8¨zJã³Ñî€ÍÔyQQÈ·ô º‡*ÌE}±$ôƒpß[aCòZ±q>ööD-?‚>”õ4êrO°S’“®J´]ß&ãCS9^$)Ý "0ÅYpËz&6g´¹ÿ¼Rˤ.QÉäÞ¤B¤ô<œ–ˆAcŽý‰(÷…ö]I„{jîÅ…Š=ö¸|]\H?„hÏ»\dûysÚGÚÖ÷YËlÁ¬8ß‚»§-æ_#ž° A"”úÕàý“(ú |ãôA.9==+„L¬CUáå>Iï¨?ÃíøˆøÃIšîņaï… +e­ó çLañ)™X¦¥" ´uHûˆ@ؽ}¡þäDa¸¨™ +ÛRP‘œ­Å(þ{Ó~ʇ²gwÒ¿Æß}F|M*ÐV€fzãçAéA¶ŽR5÷)UY!¿™¥¦KÚßHYçq·<îþvÝünùmœ°¯…é»;¨ÖÀŽ³+ˆQŒèŽy.dvƒèe$ vI^w¢¯ eÚ>M8Š¼T‰ºÊ¹aæ"Pö· +|‘î)×MûqÀ9Ÿ O÷{+D<_H«þ®C´7 4JaD1AíG×O†»7éõêy%9MŠÀ|(¿F0žóT`è¼µ(”½h¤ãsEѦµšÝliT,n.²Ë ŸÅXŸ!û% îe’Ù»ù"dø׳À0}ت ÂQÃÀfꆌú}m>‡T‡ôn˜K;½=aa(ƒ%ìà¸gËÌ÷ž“„믰²AKwÄjYcy#¡H_™‡¹ÄŸ3â~»×³eÍh|D© í8*!lܘ2Çv·Àä$!üÓÝ>#„jÎ`´!ö0©ŸQU ^1n7;í`z¹ôÔLø»DÙãN;pçJQæ¹ kmƒÁßÿ<À@ï „#›NW™®Ác¿‡ô3å _·ÇÉcƒ[D$üó $:3*˜àN"í”  =L¢p¼öÌs„rh mà–ÐÕvÔ¨#Õ+ˆtÚÜ?"ÁÁmY–½s/ü5»É2rK{áºÞZªÕ !PõN>¡¬%½íoð•ðW?`t—Õ,r"Qñ|1{mJ¾9u—*úâÞsÍ[üÇìTØïvA9Kßèψ<«–Lñ™y€ßœgòŸ4šâý±ZsDb‡ +Ê!zvS³_Qô~(±Ñ± 3Ì¡ú»s;p4¨£’]:Ã,EØ»æX¤ö†œ/ú™éÁí¹`Ïgͬád»§ñn >¢°*#WAªÌ4 "Yv5ª­*ù•h§Zô¨ #‚ëéwïV FuR +»ßœgé¤k—Š“ÅäÓgÄÇòœäö×(´v$9`üT“$6‹Û´:œ¸Êaìüw–TR²u¸*0T'ܧ¼´…´ e;ØtA©±—]1ÐB¤šŒy©‰™¼<–æ®ï¥Å:ZgCz›6´üs5/®}àªI÷Å1 +µ“\Jd*" GA -ðp÷J!»Ÿw•=^þÎâ'r +Úw‘,·˜€¤% œ'% }Zí7±wúÏ飴”4tUá»øõo¯%©F— L#¡§Š®íígÐzr(9´··õEj°p½hI“»ï¤;C×@qk²;E~@€Vhí½þyÊÚ¥ï¤z¯~Ó‚x¦!°×£Ií¸Ã.›Ç)ëüÙQ}¼ðròþþï +J÷ÿ#î»\ô?!ÓÈñ;a3J£¡Ï–là|´ÿùD BOµH4iú¡-<ÍÍ9OUå w1"LânÑË +tÙâ5ÞR ºhŸyñ™êBFÇCKu ÚÆèÏPƒ¤f€Ê› öùâ–ýƒá—Üëa¥!²„nnÕ©öEa¾%†Æ™ANgXÑU.wÊ”óm.±a¥Å£TÊÌenrhöó\ÍßliÿèBa‰ê¡‰’WèÓÈy0á"¢Zwúúx Ó/æÈS¸€¹ÏDà+IúÞ¡¶1R齞4§:>HžôUàÅ«¸Ä,üLVÑêÒÐéjü~1ìíØðì¿?k?ØÛÉÄÅžK[Ø=ˆ¨ºß’µvJhŸy§¬P­;¥4(#,hh-¸Gšä°üÓiÄ䜥a §RñP €ç:p£ýùWñd8Å7û¯å@¨U‰‘ػԋÉÚC›#J¶Ä`Rêyî¶A•:Hv‰$ó€»<`Øì –Ý‘€5Ö˜og§á Å鉌ÚΔí,//Hqê9Ó?¿Ðñ!BÐYnÐL{7­‡›]Ùe<Í+ΩKÓÃ+k4;Qªµ„ï§á ejQM7 ý)lQ.™É»]§Q.gz([Ûv6º^¢lbY¢2²GÓw.š×Á>]ee¨˜r¾2#`„);½7®H×À™¢Ñnõ²QÒÅ–Å~sè ÄÐi£dö»&ŠjØ8¿ó;ᳯÅU Ö܈­›hÇýƒÙùÊ3t”¹ûE@ ˆ‡¤YÜwbêòVVDöԉŽ2º£vë×A¢6@Ù +3Þ=ç+-¹7Bú!q¡©ë¼õ]þ‹ã¥1ÉÖH ƒq/®ŠàîYYÙ®¬·6÷O¿s'ˆÑxŠÁ8Aë:å¤/Œj[•È+´¨sû@#•Ò »_ +GŽÅc¼çOÚ[kP‚¼}‡ýö[;è>i•C}Žw¬ éùJo¿Ó•BÁ½ÏÂGó’÷ˆ/ßè]ò'C·áý ñym;—X'Ÿ…òàWž9w^YÔ¨U€³êôhBŒ½s¸4®:o ÿriŸU‚Žñósà Z[?›?Φ‚¡Óãü‰I·R;ÞúSÔ™L %v ãh ï<\duÏÔ^[èA,}(ºúmˉµ¥”Ùõžâ+ëEue?tZ]™h3u O*C’¨Aí ,æa*øÏs…!­ˆÉô†º:²Ó—Ó“E_•þq"j×3|ãOºdC´PÉuüûóõŒ¸Ÿ¤%Ž1çŸvfô}7̈8ZË`AgwoÀ“Ðï°A"¾¢„ëTåÄþÒÞÃ? Zù„%̓!gãÿ…ѹ¤=œ_âög ÁÙ2%&ŸÜyË|s¿›í(†é›sÑAÔ ïç)9°Ë¼÷f#%7AÙsçõJA³){ðä0˜q '±º_éî$Eí$EƤ<Õäë&­bi%­²gÂYTJ@ê +­öÏ'po.ø­âˆæ“¿ÊW;ÖbÆ‚-Ê!Ø×_úIÁ$ì;-Ø>ÎædjonÃè´»(eCìzŽíÐ(HÇ;–ƒ­Ø.Ý9He I@1û”$˜¿Qå+QÁ4þ_#M(á…²dÒd§Ø¼¾ÖÙF¹T¼Ëì>½Hä~s–À"/+C­”,ä´ä:«f[€Iò\[¯uÌ@už•“RС<É]V7C-P§SÑæó¹ã>ª%%ùš„ÿG=è`ÛM÷÷ÑÜ#íkÙ«Ñ2ï>gåÛÈÄV;©´dp×Õ’IâÄÐ6¾ävÏ'A+¨ÙvtnÞi‚¨mÔ­—VÕˆX£çGóáÊâ‚©jMôVЦ#"Ûå/„ +M. e§Þ#Š#&Ùˆ*U»‹ßÇѵòìì<¹>» +4§ÛZ?üál»µâ½¿2üú£~=‰´µ šv1­u߈°÷Iÿì—ñûßðcbô8|÷(¶e¿—r°óK'²éwXõÁB•*ŠeD1&D²?²¦%¤Æíc6õ€…=œ;„„ªa¸{¤©õÔ¦ô«Zj”5¤'vM9n0Ìœ çaY!Ð)W¹‚¾RB[±ÒCÐH›ñ«ƒ˜™5,Ë¢¢8•*Xl©€O ¢ +Øì‚=9¤4o>!õCê%ж »Úi÷lh"&…räm™Ùǃ”[N2"á´«í™ ß ßZ·5OòV +‹ï#Û_äþš‰b +I5>lßÁÝ»hH/5 m±çÇ©ÿ©õðH¬aÇj!•ùóB˜Ä]";s!¤5‰*#äÚs—I‚žö¹TùCÑñ¸@·WeîîÞ~ÞÒ{Î*î  sÛ:µü¦È7»,OcSsŸ¦Any‚)6I;õúê]ÝãT:oÈÃ/Ñrüª_jÏdïÓ/žb. »Ÿ(ëÿûÉN K߆û•=®%¢~W~a4[ÂõmH…ÒÎy¢¬Q±xJ?± ‚˜ÜX ›gOì¤ ìŠ¾ZVuX DÞ,­¯iá¤h©h‘Òó”„dn•ä^híÏ'Ø( ååÝg'‹²ÍEúB€0ð±a¸z6…ˆ;¬y¶×<›HñÚ}N„Mú‹Ry œØP/ü4³5*/ÜÁv¶Ýª†\gûç"@`1STaOî>'àN¥—wþ]ó¥÷ü—ˆÁ½f1Ýk6í¶‚DÄC;PÎNK³ëòýrÝy~©À„ÀøöÚöÏG ÚŽÀˆ± ï‰Ñ^ØBþ¡:&© Ùo<Š†å±Äp¹¶½÷”ßãöÃ’‰W,·\hX×k„L Œ2­ª +¹b6°^V›[ùyZ™‚ê;§¡!“>‘­.ŒP‡¡ËnÕuÅ;æsrð*ö¾.6ïÍ–>´îW^áüÕµò¹AvDŽ/È‘ê;\œçU1¢G‚j· ;ôw¯$Å-•"ˆ¡åÀ*ÙÇ¢ €y7ñ¼ªHÏ›ýòè?’زb­“êMŢΔ&F!Š­5SH~*/|ðYñ°¯=âˆ$@²«%p6ÄâoïŽv™˜+¸ c³'Swñe’U!>Ö’Ù~Îç Ç{KßÅÉz“·A7€¾ûl'éóËyÔ`˜ %týh[]+vÂ3ðäW“¼bkFщFK¨ꜧí›E3€x˜Z7Ÿ×^PÿýmsEåê:*W5ZŸä¬Ã¨¡¨äÀ²íŒ“G ‹%Ê@ÎÛy¸ÝÛg~Ì<æü‚÷² AŽiT*â]AIRòç“›¿å¾…ƒ¢/šLœ°ÌS“_ÔdO*…lCG9¡ÝÛh?ˆ8ð~Õ ¦Œþ®žqÕ%™HS€#ERŠj%]Çðyüs•`xeàˆjÜO£ ’(º»DI)QõD9ˆ$lòuhØê Fl„ÚÁcµ œ™‡íÕ™ø£ç:°D£¼ã† w®DrMÄ©Ë•p~dŽ?ùE×­#·òKQ‰~ŸÚrj:û Ãñ¼mjì -½†“ÔC2G‡~ÏÑKíˆ*„žþˆÎkN7þ;®SO¶SPfn²ºý5‡NÆÏ…NÖ"þ Ô'L奋¦"’ šði½:ºê©ì×~µ>Þ¦³-»u·™¨–çÀÖ,úIŠU<¹œ2KÚÁõ(,%u}¨À´Ý‰íùºÒt„ø9úÌ·lO"”­Pjô^¡–xÇu†0qT”n¬1kðûúS{G¾ä–’Ù¹Á8å3¿FfÿýU°Žæâä—ˆû!È]óSúõ™Ã~´çáÚ<ô嬒QËuݤ*È^!—¶ uS|®’YxªôÅÙ¡ Û˜/i¯v„LPèÇCq1¨6uFœx)¦QYwãpO÷ÙKvÉ ì- a¯ëh 9£c{Equ¡—~…L-PF×ÔЕõjÛŸ5}¦,ê¨$!K†9Ãí]†-¼­/|¯ø†…ÞJ4 ÒÚx•Õ@ì;Ð=F·2c=ÈuJ¡t çuäÅØy½ðJºüŠñfT iÒz’„ÃDöSbƒ¿¸ëýô†=ÍÄð§¾è‡S£@OG,·MÎ*@ fÔL즢‚ðÊq¥M1p ,¤’€šFbgy(G~*C^È·Gý²5ŸÉHëµ\ŠRƒ²o0K©ôÂS’{„žÝ×ù³Z‚±(YUð€Ý3²“›ûÁP£ÁÄ3¨!o¤àtªÄ;Q)Þ‹íHd¼\%F͹¾O€ôx™u}Fñ@·06ú}ØùŠ:©[‘wÇX´”§ÌΨp±íþÃ>=¨–õÔT)XƒˆªÌB¹Rê_{Aa`í¹ý)ù–­æÂ&¢Ëu&^í \»ßâ’í&Ø4r´ çÏhƒ<™?…ðî‘Í"Ÿ–E Vl÷súW-©A} °DÂÿã~'ƒÍ¾;Ž˜U$LדK†RMc†ý×l4Òš}f{× ÁåpYÀ zÃèõ²º¡ÍÏRspå–äqC‚ ®nzÁØî'€ç W‹á@ŸÎ/³„ËWÙEðµe?jÌÕqòáq©‚Õ)ºiÚ²Ž#ë@Žê¾Õˆ# •/GྠL´>hh+ôsÙ'nð &²·X°Á%¾6¿Öšñ«wòj£ÿöy|Ôûs2Ÿ”jnÚee<> Ç„ºÑ!åf¿à1×H³å9JÈêa-vvqîˆ{ì1{‡æxŸª?Ý+µÕV=Ü®‚? ÍCÚW^f]QC“8‹3«ò®’‡¦Mš +ÂB¼˜ +vªQáÓ¦vAžº[ÐO!»c¸îA¶|aª: ùø<ö6"¶ŽL:e$sÉì¯HÇ爨ѩG¨BOýº’ˆw}"Æ|Å_® JzdPeÍýi÷ùi=œ×½¸Ð¸[r¡+7û¼›w²Þ"†!ÊAÙ"? µƒBŸêî,W™,˜ º¤©ýê’,Ð{æ«×]¬8kîýi px« «ÂaŠ Üw9Ò{:=íä>¡Ë€d4j®7aÖ€r¯¡íKƒr¤ÉªH ®¢$øF’µ–¡Ûþa ¼Aí¡%¾ÝûÒ¨™IÍÕΧ:s‡£Š2yf×B¢dÞQœÛ¨.A®Ö6OŠ¿ó*Õf5B3d7/;ïSDÃÖÑvŸla©SuLV»NV«¨Ú}ú"#êîyh²ÛŒ(O"¢…ßés(«wÕú8N- +ÊN +ž?ºŸË¾Î¸Žž d=ÉÀ"Ø‘XˆÖ¶ðøY|`P{:9Ú$ŒîYŽT ¯7…o¯08†»£*€ªjæ§Ø]Ed-›™æ(@Q²H±-†ŒeÜ­ã‚Ìpk<Žsn]ZˆU?@¬®4JÒ*}¡‹9Þ`¢6‰CÄ–bp%¤äŠÈËÉ•ÿïÿ&öÐ +ô¨°N¯øµƒduÊAÎÕ $>‹"‘¯Ôpã|ü¤Ö÷I­[¿Ó°P¯wã7m£BÓ¢ÕÂi\¸.ô:¡V7Æì†h¡/h&³ò Å¥Z:•_ŽcRC¹˜3§aO_N ² +!,ÀÎjÊSáàõ –†³ôP†…2Y9UD*öí%û’lÕ„£çº úŸ ä{™©gÚ†CuI¢•¹žR¨H&4…¯Sk¡f–ðÁ¸‰‚i…à)°Œ\PB+¤åû1ô Æ ã¹ÐßÁ$Mw}“"n(¢“Š ß$ðHrµØG 5ÝÑ"ºR-ÉOݯ¢RÒOCÜæÞ‘mü.Šå(*ʼn‰h×™ƒˆ¯+IVÙ¿]è*@,¯ÚÏ+‘É:‘mFZÀ±£Èp;@,W¼u½ÙIõSè M„ 6#7‹Ú{Qý+2ŘìÊYŠÌg4ùj°kÙ­Ø`ЦÆÖ»EK—’H*ìA@š~ñïg»@0(1àq+I~Êîã`ßaÿ-®$t_¬,;èìPƒ±ñt{šˆBPÉB“䋲tË×­ ÁaTèúJ>žF¥LN§-†¹„þ+=1H$Ò t~×oÁŸªù«@¼¼¶T)u¡«)\þ˜5JJ‑á'{ +ÊãœÞ~¦á¯b7vÖ¬Ymgì¢ ŽS_/¦è%œ¶*ùW 8íåVýËú……¯„Ü뺵9‹–| ½-Ó+~ͪ©hÆr×Êø£HõVËS<æ+ŒZ¡Í²ì‘“”Ófƒ»J‡7Þ b5É“ÕS"Ð3«Îº]½½bïIú ›GøË”]i>ðjX,Ü­ñ‰íÔ~|d` +gQÕ%%}”õ Å®”¿WH²ôHÈ“ŽKñc¢_`êZ„Ÿ*´@•ÙåŽQ€Œûµ>!Çý>ÞK=ÜÖ\ Ù÷`Qî•5·ùWnfñ¹í/¡ÏVXa°Ÿ?%Öa'¨ØÁî\´Þ|÷Ú:@ÆÑVEžÁºõ·ë«ó®8ÐltrÇ"Oö«ÐK³«….ö•y¥. Qz KS…Ïãr{tí[ļ@MÙHfè ¿£¦>±ÿ¬'M^"Èøß+]Qê/±ä}ÞÀ^FNOÔòlF¹¡âÈ_´Pž& Ë1cú=ÿ6Üdõ•“¿çÎåÒ³?}õ¯G?]æ[×Cëb²›žj‰qf"Nôyª)Ë°3ÙÇ)ƒ:i *^)GtÌâ$2‚ØC ö’¦¹Î\ÕîöÙ°¨k9ë~‹¢&™óZ´ó¤¦ÈÏw+…2Jþ(Gœ4Œ¼¿m"”OPÈ ¢hUdD9¿è +lª?µ¤4¦î€°~ÍbùQÊ@¬à ©ÁJKfÏKÉ@³Ž-x¬¥$ù›# ªœÀv¾O6Ì%«†ÃDÄ/ÛÁ ôH Xï(Bù›iTì©æXÂÉ8f ;wéù´o¹}ß9sÆ<8ø…ÌŒÿTETü©2Í™V•¸y(Tñ©€ß¿Cì'G~WÂ$@ð´\Ö8øj@J(ñÑ‚K)9 ù•KÖÕ‰“|wÖPyœhõ¦‰ÉWV¥WEhW¨ ìÜØEù +^íh•©Ï®#I<ÔñpÇÅS/0Z+õ×hß,…÷~.îŒümæ)¯º"ì꺎۠¼%²:…[˜MT9ð4Q!—·4ÙÆ»Q¹tç^2¡Õ¨‘œ=ï¾Â!„;'·ƒYrs‰UqŒëewo¤š®”  ›5×4ewE¿ñ¡T¨Re4]°;8áè2œ;JšpÉaýƒé˜M†ªæñènê+Ê=%f]ßÓ¬»š`¬{·P²Ïg°+d$¬¹Í,Ã`÷Ý!Á%;ìEÞß¼~DÎÒ9Z§sDj]GJã@ŒÇ“ÓdcÙQ ìAYƒP m)3Me‰Ü"Ÿ5ô‡½© ºGc=jB£}U_ʦ%&E«KsF_q4E¬‡VVˆYÀp§¬þ"PaT€½ B‰²®2z ha«\‰Œè:¢кmÅÔóì@cìˆý•µD÷µ1ÖŸl唩¸’eWÄÝ­‡uÍÑ€Ð]ðÜœF(„ˆùO¯ô„‘0¤pÓÜ»ÔyÚc§¿t‚ýpÕªxš°¯˜îÈ~:‰ÁêíV•OŽ{ ¤`u »Ò¤GЦ–$ºÔžç›¿¬„u6h5bªT;OBAdÅ)!lÔ”8ÙSÕêjñ0¡)ŸJ1_Wò'Ñ×Õ’1Fñ•Þ•/JÙªwÔ/œö)<RpÜ\ÿXá“RºeBƒr}I˯X‚ÚC@Yé[•I•Mpºã½R*!£´>¹¬ŽLô²ƒÃhŠ5þ;ñ&iì©~!C|3ä±OKF°?nPþöÓ(µ>²’˜±»w[vO€uÒæØc¢è=ÜÉaléÁ¡üÆuä7Ž‚ HL¬ú™ ‡râÿáli¥%ÚÔ¤o¶ÆaNÜÀZÂ˱%ìÆæ +´²ê¾_;˜@0‡š]oÏf¦ÅÔdù]@[ÙGÛOÚZæj +÷uúðÿv¡uhw{¨¸þÁ©\@HŽq¿¡…ñʃ°Šíߟ* ƒÃ·¢ž6U¶»Î²ÿ |GO¿ƒ1ÙS_ùGEçQ÷í8§ç½8)ÿ~ èÖžÇÎ…þ¢·Q{è—Óƒ…"l‹ÔóôuÚd{~ydÊÿ%˜Þßøïþëú¿úDôù_ñßYÁKë» rë.b0tÕj!XÁêsJÅrë/1röœÇ1=#‰˜aûe­sd hœ- û—p¥oV 0¿÷ż…2_YQŠ§u&Ó¡ÇþðámGÄog5§!/ƒúã+xGU«Ë·Ê—FñuëTCðêÁ¾3DÕ(Ä›º3ž£æÊÊQ9(/Ÿû¡×°£Š}RŠãN¢èX?Áê Ëb!õ9–¢"1Ø#$,ìêE‰ 'âzÝrlGD‹@\„˜«D÷ÜZE™ýÉÜ;–œ({2">nê¿bwzœo“ÅlGc©,õ¨cTN̉ +—jŸ^U(G(D‚—©ªà,í;5¥‘‹a[®DñÝ7ÖWÍ‹©ŒÐ¹ÁIŒe.¯#IëAžMëh^¡Øv׺ F¢kv“ G?TÆ/¹~?9Qe„¡+#|¯0ý¬Ç-¥g2 ©‚ªS<’ðú²ÔYÄÌ(Õ6 Œ\ ?eŸU¦:"ò”+Yqe>ÓÝoç^„P¶3Ût,›ßµ—žßÞvô78ÅÌk{y£Kœ‡+Ū 0Åeѹ[„í +$ƒ¨Ÿ^éè„,´›ËóÍi¬’wÃ% Ä$–^õ.r] ˜0²ÁŠ4, ßËüÔ]ëiƒdÚgiéÐôûµW„W°_¹ªèðªî#qxZPÁ ‰:Me&…÷¯´ÓA5¥½ TdZ›5ä[Fú<.•ý˜òŠøy?9Òߤ[¦‰@€5pˆVþœýLf "€j1ìØÍ™mZWh:A#\ôN#3ÞÜTÍÊ<·Ñ hã½Ñhïhöh¹Öèæ^ú]䣒Æ#ø†º¹ÓJ¡µà0~ Â`-%.ÊÏgDʆ!êþ9CKVÎA¸›)‡SïqÃy d'nDyL{õYôz—ð¨î Kø3¡¡Ö ¯ôþ2/ëdRzÞ@I¡ ÷—ò±?›vV‰2Ï_DZù`žÙBG¡v@X0fkr@ë—¶Ö«”íFv+M4Õ±€°ç¹“²l’sYu;9O“£¤ØÍF‚2Ž,.s;ƒ¹îòJ…¨ë…ŠËÐ'e¸Q2BHäTëCú^ ƒÃáJ=ìÐÁƒ\éz¯½Ø JÚ#Ž1ýP¦Ô†¢T “Ù ajª±HÛ!‘0˜íˆÙÎ:.Ó}¸LÃ,ûË Bvºc’®2þˆ£öû6÷÷TVÌÆ'Üc‚¨OZÅýýù2lµ´ÎwPܨ~kˆ\‘5¿‡ÖiÃ]kÌÐ^Q¢^²úöý4áœÏoÙÔY²j_< ÷ßðGÿýQ¢®šz¯«/§ÉÊô¼>i#³5Ô%ù¢@k‰Ø:w\[r¡uäÈŸÚâAùØWt›ò %rPX Ý¼ÿ\Q"AȪ*³ko¹Ç0R +lãWÿ˜’²ó•œ¼¤§9íÖ©êñþ¨aúAPcy~üåOæ÷¿ág*E-Ü[:q9Q˜¼ÑŒëù@³9õ„Ûj•Q`ЈbãnSïò³¹ÁFÝŸTƒOª¶?ÁQ^^©»¦]/Äéî_PÑo1çqLÂÌàÑ°F€ªý`Ù¾[êã¨?ôpî%Â2 Òü_ýÕra|&OXñEñN†Ô°$V/A$f§ö ˆ +S‰B¥×$­í#"›¨']Ò*ëg”òS*yð¢gêOA©¥œÈlTŠD‘žé–´¨Ý¾¢·|Ê={æyAÌh1As®Gêž—ùaKs)¾üE"UO‰hì—æf½Ê: µ]u®œÆ-J"åD0e1½²´ïøÖ쟙 ×£úÐ+_rgf¢M¦òÕý*!È;Ž¢ŽR]$ÅòªÛÁæíÍ„*Eõu’lP~kð6RIi]?VË°k²å™ ·ˆ×¡’‰8ƒþú[3¦e6÷µãøn¾©wL)ñ”"~š¡ö) —K‡ÀHÐAá[ªg +õ™°;ÖQJJ'†µf~Füá4J"Z³÷:AS|œç`5¿´ä^¢i6ÂÏöÀ(2¬ü…4ö[=ÿˆ‚ómïa¿~ÔÀêÛµÒe¦ò(I{–=¾n3#åÕiE‹¶„>¡Õðƒÿ*¬«“öA'ˆ¤îèuöš +}¶ю¬&KÛ@UØ^@¬BŠAðÔíhÏ̯+E‚‹ªô<”ñnÔX÷¹Rì)"ϤҮçÇ+‡¨»rF+D/Øæýpmº‹ŽÏ|'Éç‰ðc«Ú…ÍôwN~ª¯Jóìíâ~8$Õe ùË¡ï#ê:ð R€"ùKºõò„–u0âoñ‰é¥"ý¿ü{f¶×R{¿O‰t ‚ÆgÌÍÆóÍ€¹8,ÌýÒ +üŒbsÀ“f§jØÌôÂì©N¼Ç4¦gG4èîù}<Í¢=Øöà +÷?¯::á1˜¥(uDùû×Á€|K*ip¿yI÷ûé7wzù + ùrðÁÁ^™è4VìËÕ£!­œeõ›MÙrf‰qDÖ\ÚH¦”++ °ì3óL‘ÅÒoì‘jk‡Z€NvþÖÞ®Õšæ:Ïýû?<‡Î>Pºªë£û0!r’äHYCdG1äßﺮ»z®÷™sÉÚÙ Þgõ˜ýÝU£Æ¸?Dwok0¯w"¥Ñ¢Pý°­üz¿‚¹P· °ÜjôºL"·ˆÖ;Ù‰‘Ü˸PPÔ•Ÿ£’àÁ…À°x°[¥'×mƒÙ_èÔqÎ8U´\>´j{á›Â«ØýÜ +‹æ¼¬ç„2#JçÞðÿ‰¨8Ä?—ý%×I,uàRCŠ>íÞ3½5m⢷9û”zaÕc¢=ê#í"+{ûãžñÇms ·Ø®<ŽÓWœ’Vñ”¢³ï+í–m_¨ïðe2ÛŽ]/>ê*ö…q.VhÂpdzòa-Z¯D°<2¢[t®fHà¾D²æ‚ájÉïm ¼v¤¯1eõ‡.Ô¼ómzŽôKE®w7Œ9¿¨g¡1•[ƒMË 5tCù\¸ŒŠ‰­Ñ;Ão²¸èjï°fY«¹&Ný¨õ4¨f÷§è6–> /x1‰mˆ †H»›Z^d³…cg =­8¦#U»êÈD Î{¦|¸oЩUQE “—QpJÓÒ›ª!5´½Ò™n™by|»éW“¶ÇEµ‹ÆŠ8$ Qw±¾N<ç“A pe6ì +ªiÆ7Iö’ì°§µ_h±î{v§#¬ÁÁ8øv}e!ͳ°Ñ8ÖÑl:›ÍõnŒo"ÆÎFÌ¿vù,QsGÝÛÈ¥1“cÄí:UjZSgû»æw7eÜv¨.ñu½ø“ iÿDÖ§u…ø$¸!Ï,<¡3.]ºïT™ + x_Œ¸…ݬï¸sW»®XïL¤FÔË?S@bÿ‘^S3är+±8¤Š¼GxúF…ïQÚavÆ”*÷ð¢µ6Æb>#Òá5U{ÔÞ7‚ã[q‡xlr‹€r lÖŒ$KôÊéa_¥3×Óö"â._J>æe¨ä´½ú㸶 _/ê±iüP´c8ô.øˆ¸jSëeû¥[ÞÏAš‰Ìm&ò< +m9xõfŒ…*cÈš±¬›4ñ&î>Þ{ÀóbEãlíþ&ˆ^¶œ›ª9+Þyfmv¡É<7Ý_J÷zCLUçØ<ª»÷óõÜê†aV±» @¤³ãÌgKK@9λŽtÃe/|'²lž¦ÁŠ˜÷Š.WZ·7«Xí g|_&Z™gp»îX„ΣçHaëÜ°æ‘·Ì’—2pú×*n± ç.hê•}HªvŠrE=ŠÄ(‚®ƒùŽ7$ü§ˆ<”C3gYêeu‹ï¡ÿÜ•®nRoLë’ÿ.âXWðÌcX÷%‰âÜ$Šâ£¡PÅœ£ï bNÅ­,ʼnÝHûŒÈM£üÑ”_;%þîçH‡†ñ)ߎó|\ ÃßÔ—>ÆSOy±MÈËŠaЕ“Õ€to±¼¹+:k2,vÐúSVoe€ŽïÏ÷©7ò +Šg|ì§ÈÝ¢lYÏÌ1À<hÿ.‚õSKëf·¯Êêjçq‹¬»ë– [™ÁUžÂyOáœ. +Ô¹ð[Â]=&ªè\ß6Õ¾‰ÈÍ+A$Ýä›õþf?æ'®ÎíI·Ú‘tkl]‹RVt…2§?^Q×3ß3ë¥;¾–ü{’µ¡±ƒÚ½Àö[á½l¨>¶¤“¹®¨\âò,š‰ÒînEM!£ÝõNU3û.§úñJfÓGýˆ{[(An–†¹^˜GbEÂ+Šû> »ç¶0žb è<ÃJö<&P±!v€¯ìÀm5ç —€¶‹@©ò‚{*ز—Ï(µÌ++ÊEMo…ÂÖrðfáúÜŒÎþÈK°ôåB›3 ^êô²å¬‡H„ÕbÁ@ž ´ejˆ UZëy½´8ë <·ƒmu-꣙ãƒR}·Í˜3FY¨*vnÀ#7¨Æ ­Ù®×þ#£…õ6d¡¾Î‚{cWꓸâÕB*º+ £Ëš'RóDúVL!„öñîØBp¡ÙLÙZÚ.LëúAjÚJ5ícçtÒÐ|¡äLúRZû&¢Ö$¼ŒÇ6Yý#êÊ+KûíÜøÈ bƒ¦UÁ›|¾ß:øHÏC+æ’ ¿£Ô¥]\Ž|œ™&¹/iÍr{=kñÔ»Mïö”èõë=»&üÀ7®?œÔToÈG Rk¦¬f³9·r" +òñÛ§‹§¿foHF~³ŸÑâîúöJ± eƒHþGÀ·sã{:ÉÖÛÝ$·{\¸¨"=¨yߘbçd‘´M/¶24Ê/æ[æg ­ZÚÌ+õáØc{%Ç^2äºí^ט÷¸I]~Ö´&§y£<ÔŠ²õS—iÛË%/Hѳèý%ÐßHEÿ¼ëGŽÛïá±ZL™6ž¶°kž•AG’2f‘q4YI8C€ÕãâcЙ ÙŸ Ê8é8ŸRÙÀÏ}GÌDôöh&BL¬dÒD”ûåÊt!@ :òŸÍ¾Be:òëmRüôÌßY¯_Zê0B—â2¿hÑÛÑñåeá¢:3_Aª‚Ò å_lÁ(²©W"Ï|‚#Û&MR:P‡<@yÔÁQ¡zCèöj$½?SÙõM¯§Ò|w,U¯w‡:ȹ{¸Ê/5eSQ»!„fëc¨Iz½¢†Î1°S!{V °ÄížýT¡§£…Ä×_ìWëíaýÂmq`„¤¾ï›Í.òWÎ_3p´–Õe€Á+â ™§v[ï}ùS1•¬4Y÷€|(¸ÆÈ¢îÆõh Må>Þ…n‡ò úØ žhLžQSíZúZ x}ê1O4C«‚ ¼4N4‘t׸T±ÞBtæ7³hœý¾o„Ê4Å1§=–ýUâ:µ:Aj5ýÙë–X_:MŸ÷€Ì ç. WQèŸQ +ûP#DØÇFdWañ›  ÍA®Z¿ðo1o¢ÚtóMs¨†!lÀ9 ̲ÍSÔÐÁˆ`~F¼Ù»ÔåÁì"+èщ¼Ñâ\ðƒW ]ÀË,¤x³éØÞˆ<™"Q3 @v´£6(Mà#‹÷‹ømPˆÐº°_3“>uï%‚¥//.‚ªÝp;bÄPºÑ GS¥4Õ ¡õ—ãHØ£+Y·³˜êõ5rQ#y>,éïr.TC4mŠDºíÀ +Ëeæ¼S¸_ýfTRïÀxqK¤RÉšÕËÜ]ñ‘®¸(D›¬{î]M(ªõóØ°­¢ + +¸çgD vÔIä¼ïoö/ôVÄê ªm×:¡°ÂJm,Ü©†˜²>xjF.ðî)ñuœ˜?ÁÜÎ!k÷81|óu™×f5d4/!<ÖD†¡›ìÇáêtm zöÚ6Öägt?Õ¡™¥6'ÛŽŠŠ]à:Ú¦óžNȶ;"Òôu½/(_–_J€^ßì§mY¤µ¤+ +׋꽬lLZbßD¢@÷…ë‡%‘¶%§ O+.=cÞ ­d(À%1è fhCU‹-ÆãG2 „¾æDbߎ†,p×Ýpn ;9;µIÅCÓÁ9ŽvV0ÝKO +­ ]Šò:Žr<8aÙZççÐZù9ÉØ ykHÒ÷O©™mÊwÔËŸ`è-¾¥ž¯VRð ‰>ÈÑq>÷4b¸k4SÅì3p•Z¤ë]?Ÿ#½GñšµÞ‰ðëö"bƒ†ó8蔳Ûh œŸ9RÝY¬5·rðÛ~ŽÍ@ì+Sà÷…«ÃyZ@)›tÚQ#/Þ—<à†7ë#xQS#êÔêaEÕkG­1êoöùÜቑÐ2Ì3åô=¬ó¾p<¤N°rÞ墟ÐG£Q~ Ô6~Q.ú)ª¡g±vS”i¹¬Š9Ë Í@«VAuf~¼ffuΗ +ã¦#-(6PÆÉ‚~¸óK å’–]ä¬úÖÈR©|β©%¿G­q+V kÞáÙ¯Øì]qµPGõ +aaÍØÛ¬Û9·‹Aß.Û|Û2XôöNu—H2çÄ=µ‰ €ÊiY ¨Ý¯QkD¿á m.¸ +ø+‡»¾Ñ9^ “ÀuòW¹ãÛvãgÚI(ž5QèÖ²öb)¨q4v(¯²ñëVæ ¾ Ðì’î†ì„²eÓ˜G“"ð,éFSyBIì·îuë[ aê-âA1» UÍ{T½´+©1/Òw¥¯÷Dƒ¾Zî†j¼62á3W_X ãÎÒ"¨è"£W9ÊzS8£s;ÚÝÊ;Ó¸¨óýzcåô$§vmÎ& …ëŽÙDob(îÇm¬¯Ù²Å{ä_†Ùý%A}rîÿ$ïþ_¹³·ûþKb½ÿñwë?Æ¿ú7?þûûÓ+?þí¿ÿÃþ׿úwý_þöüÝ?ýÃoþúÿæ?ýíßÿÃo~ý‡ü¿¿ùÃÿüÍø»¿ÿãü§?üŸÌA¿ÿÅýÝ?þîoÿø»¿ûÍ:ÄO'w¿ÎàCà?ó¿!­aú:LÓ€h•®ÖÊ qý@º‚qž*Î ƒë]_ÜS„Qè¯#]„@Y›HNþr~ýKÙ©¬ÕxUwþúV”`ͶѸ?!€¹<=‚˜Õ=j­¹~¿£„9RM­÷øŒªê·¡0‹ç˜«#øµµãCG‹p#à¬jnµeÒ+ËfÊ-ÂÁ[£‘j°ÿ¨¨ŽÿÞ.‚Ö4Å’~åL‚ÈÖTuˆv£Óq¥2ç^'T´:½¯ÍmÒ·b1‘m¯q׈vÑêSY#«¿¤dõDˆ.]ûhe7^î½ÿ-íºþ¤¼öµRµ²–Vl¸¬.¬‰Syi"üŽÑ»`E°&‰•QBdö4Ö^iÈ R¹*EÎj¹YËW´DVȺ……ÜŸ²W®6~úe_ÑŒ^!Q¿§3U駠«¢>‹ZˆòÔL`žpèJiý•vu;9þQÅ‘ß(+Ý= ¸k«+!§°Sì +‡"óJ«×€Ú¶Bãeip=u)*ªÐtN>]C‚ꬽ׳ þÛQÞ¿PR¹ƒkÌÕÕ=ÍM£Ž|ÚeÑ !Q5‚²°zÀ¿«'@=ƒˆ (¯­_žú-6F§Ít½ö‚¬îõ¡¤<ÑsžXå±Vw-ØÖ)SÓªœ³Þ +8Ôm" {÷Wì1Z@¹bg­ñ\0íªÑ2…¶uÛ‚äÆùGÁàG†µ~eœÏßÙk±¦·²ªøúF kÎé‚S- +bzeæãû̳x¼ÒËÞý{KôÃ%4â;k7hH誳^Ô;ʺƳˆØu|nðÜA›5“0¬‹^¹ÄBF@Ú¬Á)–†zß__ÒãÚêF|'³}x×"ÌTû,?§Õ)9^QÒNõIÏ `¯[g÷e0®hmýu H®}ˇm¦®Ý݉±~›â§ZÓ“Úüˆ笣TÖ¾õΑºÈ9lq[ÄÝfÈÚáÌTît^ФL ³¤ +˜ÆŸân(<ôu«Ïç™|D]‘÷š´E:7¯Šœ¿QLS¿μŠ`kFÁ±\MW±›OÝ…÷„ÔöëóáY³.XrP +g˜ÆµéÔ½aFùMÄwïñG ÒuCf|,oàÎdspyâTp #ðƒï \7ß×a]v;È+ƒ`¡€Ñ2Ë„­`¶…墾ŭ«-Hvp'Ù=(Åö.lÊx(5–vn`X¿nQNSvXÖÁéYîPäó˜X¶ä˜/É{pãLï‚ž žÐD(ǾƷ©µ#R„é zÑ°§Q&}«q‘ñDÕÝÁ}{-AN‘[@u„™×‘»âš»"nÛ(·2äçüŒp-1°¾Ñßí§" ò»YuŒ&Ë‘ŒåçcÍÊzã·;5¢ðdPµœCû0j½£ÍÝP› âÔ\Oñ‚{'O#ÉÓ¿œ„=ÙxdÎ2Äé¬E<©NHó¢ÍŽ 6i¼¨ÖVÁ³,Œ%kaåÿã¬[x‹• "¼|KTNS7Š‹7 ã"ÆFU±­WäÖKèþ"ç0Ó3h]. fðÝÍy–ðŒ¸/ÖÊ+Q½Žì_©Mä>ÊF†Ÿ0†q|ñÞžAà…ž›…ºu¹µq#¢S^6Bþ)o1²?°~‘_ï/êDœ‰i}Ò…*ês·ƒ+#C(ê÷; +Ç`¬ÊoJ@©õw]¯q¥`©Æ!´ô5êØçA͉\Þ 5׈ªSMÆ÷˜¸Å‘‘íõóf-oEEbPµYZ2¤Œ ïxâ‰hêµ­Ó¿VD·²Æþµ’de¿KU‹D’Ûµ°ã£µ™ãºP>ÓU嘘…**dñõÕU¸vL)´ b±õ‘õI›¸¾ËNú›ýœ§2Jú'ÚJ¤h—Ûƒˆ²>çA7›Æ&oÓ””ï|>.aDi%JѸQøGÞBY¨ñ@QÔPœN¡T¾Å•­ÅL‘ LtlØ Û,¥#¸+Ån·Ã¥Î1¸oàýï–BµÔä¹½c‰°¶…Šûa«4TÝL“û02FÌ®éÞJi›§4¢ƒRDÜ¥ƒfkx¯=O î²˜æ¦ÌkÆA¢„ª…v«h1Ñ⣰ˆÚËS¿ ¸·Ý%Γª}DÝ#Æ·úV·'Œ2]Ia(ðÒå£À Â*ˆ¶‘k0.¢+Þ#2Zaÿž+Ezð±Š'{ðÔº«°Ï7Àñ›´i™OɃÆ&|vßzè+oþQiô1=¾FÛh»iÄÝYK­,òW©Ù»Öp°‰ÈúõÞ?¬è\V¸ùù~ f®är`^r’BQÆÿîEQ< +E¸3\—k+0ÉêÝÇÁ ¢-É×ÓÃc’[—\5•¾J¹îNO2=8êrk Z/|VNë‚Oo“ôûˆIz ›(K=ú²»ZÎÇ'Ã×:ž¬¦†]!Žà\ìkÒ aD>‚ 8ôXsÉj‚›»qren|ŽÒÏ­ÊA­Õ¤.–Ç¡ñ7ÛûýD8ÂÑ“º ˆ3Ë~»Ýk–y¡Ç!…Ž_Û2ãéµ ÉËM3æ±yëŒ8TÓÃ×°å\à°‹kÔOáÏLí@RZÙÒ™þ³nV+Ö;¢ÞdXi]OrU7µùñç“ g¢‘¢n ë-<õ…©c±qšdK(éwX¨O\¢"¾91/’~Ž"—.Ú9^D-×-¡Phò´ÁÏä2âð¥DÿîIHWTŒM×2¥÷œ÷áTF”³Ò±m/‘<@QWy¤Ñ2>»!â*IЪÐ{rcèý$W,§uLmŠJ:s·/pðCñâutë†IÜŽÓrë}%ë‚^ê±MUA݃ƒ9ú9uÂá;h¤¼š¶S£"²¨¾5Xw¸’¹|³Ÿµr‚–~AÿŸQy-/Êwt‚‹¼JEýö‘Þ£„i102èœv´âbãÖj]ýx}®‹í³ó3ây߆…Ê-›Œûó~z\*VËÿHrØÞ`Œ>øÖ{ð‘,!V†0CÀ>dÌcB\3±Ð¨"E¢dÃŒOÄ¡Lå­t†Úªôøׯ&ã4½/ÞjÒÂòÐQéŒv%hêç@6> ãà=–pF;ÝÆ/k¢+˜  Fe‡0#eIZö%s*QoÊŠ„ à#k Ü2¨­Å#öˆ¿2Îö|QåP«F_üûzÞ7¤‹.ªçgDžRßßü¼é¡éæ(IÒãÿŒóÊù]ÀÔtöîJTžýD½Oœf-ƒ18]¡“ –@ú­© lÌV²Õψܳ.¬ŒîRë[ºùm?w: úœ9ãæTRµ©þ&(úÁÓ9ÌPö‘Þ¢Ô×Õ$™'¯·õ!ï)üFEÔ ´T¦ŠDèVï J$õ3âùFS"D#xk²ü¼,ÃÐð."K87Ÿv‰%A—©ñÔ:ߣfü•”EG€™È‹¨AÔé\s‹&º7"PýíÐ>‡¥®÷ˆÜ²KùP®úP~î‡4åPU|ÅÕ¶S³¾S³;yª+XÍÓ òËT»ÊMÒ½‰¾÷Ÿµr¼:yèîiQ>nŸš +³’Ëdîðþö>,ªÑzæœz-Qh):Bg.ä³ñpÇQøÞ(F¤>8¸»ñ“‡¾ì>zßê³[4‚ÿn§X7Øu·s¡O…Aå¤èB¨WD†´›â +«€¢iFS¢ÑÝ–ÇgD’"íÔTJ¬ØÇ~%J"ºÁóz@«KŸå»ˆK‚äýz%´?G!ô{ +/Ãcf&}¼£9첪 €0‚©xÎ¥L/¶Ç Áßþ c\o’8Ú;0>D»?e­±Ó×Ýà× p§°v“'ƒ}8i7š¿êÍ‹ßý®¾uIJÛYT»-Þ±a¨ Æ~ìGá¥ó(˜4ˆ ð²"^Ror\™À1½òЩÚáDŠãu-æ¨cO6*™lœ=߀¡¶ŒhZî·ë#jÍò8Xâ]ìi3NmÖ3i¥€fëZpœÚ,ì ¯—vð=ô_¿¢åèJãª)cXÄÊÄ° fü!\ÍqZ´X—žëJ;6©ÄÉL=ÜGËÁíJe½gõ'¦5,0­iúéÔ]hEž­éã9®™KÌ÷í¿}ši`é¸ÜxßÉÑÓ˜ +É‹ABk–û.>"¾ߣ"fr©ÒÅ“Xû¹äÿ­ˆr¨µ[ÚöÔ¥VÅ·Râu¸r#B=¾Ž'½Ä“nãî3óé&œšH§:o¹vdck?ÇK«pjÙÏR-Õxª.öjjƒ8KÙ×ÄeJ’Íêí^RÕ{[ƒf?­çEˆ¤Ú'^{]›·½6ü³Å¤3C7Úùµ­Â4Ÿ@[ *³µ¤ 8Ði1 $Ŭ# K¼ÔÂj¦.7ÞÏFÅ`* ÿüÜj™ø»²#ÚŽODÛ÷ö`5Dį©BS±]‘É?àIjm[StóIBAoÛ,' Ýhý&¢tËrg”YîX3Þ[ˆ2GZIízÁ^*R볘û)ª›ºh;£Ç: ?y€ÄÊ—°c£VFœ&ýú‚t•0«/ +¢ƒ:êØuÔ©2´/H^EìlìÖì[SB1è[%;ï¹îxÚC²—åMŒNº?Ÿ€‚ÛúFŠ =´}~ðÙÊ?£D‹'Ó$h +…|íßrà +:Õ4:zT™Í˜›QªÜ1"4À°çćL‹A*>½¶!þ79|ENÔí £ oÍj”Þ•õùõV¬|¦ÆöVyvº˜+ Åw,ï¡!Í«Á¼Òd'÷Ó9¼¥„owW5˜Ö­—0䥘}%zOnÙw=¾xwÑsÊàïQ"­g´îî[X'í$µ¨Î` tfá5 ŒDŠˆ4K1šÈ[BCä:÷{<Óüƒ»xÀFr$Œoïu `ï=Wš}ÁI:8¢rNQuÝ‘ÌRë;ëQ„=7„s}ìl‡¨*š²È’žÁl£S2ÛãXÛ)‘ŽDteXiæ8 3£µTŽ#õHÁæ9Ž> \ÚPz6şܬ§¼i«ìü‚÷_(‡V·$ \¸­ºW±»n§¿f­+h•-ÿií¥«Prã,XLµû‰îï\¥Ï„£æH#"1ŽŸëNÇV»Úç{£N]Tðnn5”ŒLúÚˆ~—ÐÈë *¦Àl‰ޱΠ<«L£NZüÚ«F|,/ÎYD}z$¨k©«‰¦•çFù ÷ŸHå×Xr*-²=¦ª5ìÂëP¤â÷HyÕ16"?‹W(?ƒºàk$/ùF¿6ÏÆ:ôäÁM¥ã6PÍìÁà‹ÇY|Œ4,éVš£­~ü=¥ù-~ù¶¹UžùÏ|öçÏgh3€?Ô¤gN–bqé¹ÚöSêÓ-miF ^C«É±¦I3„:¯Rœoy^CaµÙé +÷c?>÷õåðÜO kAžqôý&-‚3þóÀgýNG Ô×èß?â_Œ3ž÷“rÿZ‰t +L’¢Ïmá|ýò®_+¤2¿éürÙŸÑ£¹"„É?ª;B0œþvÝU{kSD»¤Þ×Óé½w£mÛLn•g°¨¢7MÄe ê’­ÓƈÓ`Å¿ªÁ[øèëk— >r(Qî¨OÉ-R‡¢Àzõ×ß½eˆ«)^5T¦¸]èSì­æ{š~Ôd©}Fäá¼¥mŸû™žìD}³{*ª¼Ø–SýàKÐöK°±]A,AÙ^LytW=ݹOzÒ(Ñ£¸Pê¥üu ýϽç Ò[Äo7 +Îî‹Æ0ß ‰:EAݬÕ=•À‰žb("iÌò ”¶nŇÅ)ˆ%+õu3pÀAM‘^ôë.ˆ«‚"Sÿ&B…ÜS…ÜyoôÃg½ÚõëRÒD¨ÅA.P/@ ŽYÍTÖËW„ÉH„±UÇó”{H+HЦQ¬U'?ɳ¯#"œvJø Â/Œô¸8}EDÄbÜ[Sþu$§I¬¡•¨@A4R.tKß8žVnQ$ÐAš˜—Ó1„ ”/Ùz‡W»¾T”“¬fÛb¤)lEÍ’‡}晘ac6Ó¾‰¸»¤L„sÐò}5vŠB)Ú‚È +1%¬7JN3b•@ƒ`íסZÆ>"~»s%EÇO×!õ›ý œ§  ~ÑT#r­¨«“Ý{ÂßíIøi[¤,k¤,…jA2oºn¬Ü‚Ðß oœëóœ@œX½ôcœxx&V NŸª¼½ïU˜‹é‹á¯GÞüЮ¯2,Nág½3nñpïsßî–Ûýø†Õ nò¨Ë£s†î* 4vDP %‚f¥úU˜NuÉÄæ¸i¡ñß$a»¾Ž”{·ÞÔKƒÏ°¢Ž²Ï¥&›[Ni’$¬wJ u3–>îÄéq}ÒxV×37@àñ"+…”)ýYÞx0šÝ˜zàtâ›ýà…Î]iÁç8oA*ðö^csìEŽ÷¶£W˜¼² + °^zÚYll©Oø¢— +o{éaí]×;DÜQô‘¢ÀÒØp‡RæY*c%,ï”{G$0¸5kZÞè?´±=Òõ%»è!Ë+§ô¤ˆḊ·ÔâÂÌt5UJ2rzcÉNŒB·>5KR̦-W‚|T…$硵Ä[DÐOsC˪àÍoöSOW 7À‚ˆà\QàvÊ[ÿÁ÷%üvj¸ôuÇ5q ö…Abê¿¡&â„-ýI$qï$J^“"úhþ©œrÓÞx°i§r \ðÕúÞXÂ|”XO„5¶F»”-R.ËDnÚ®°¾(¯5ðl"ƒx3êmËqCtE9ÅS!K€Í¦LòŠˆ±eÁšSÁ9ǧ¡ÅJ(@½è3PÌñi½E#ÜÑxd º’ßF \õ\ÆÇƦêLî¤ò3ߦ}T´ºgßvÄ3­}Fä3éá Ee~»Ÿ•s^ë|”Y)›QÇÞ¨}u“ —¦õ׊­íˆéÏý•€‚©$§æüŽÐ é3{±T#Ý¢¡Ê1žp×ö„;úV-™â-¢ê)P‰0£C_›Þ³>hæâüG€F£¦“ÅW`P" +‘ÃØÆjÅ–‘¯²¹ï`xÏ 71•úÿ‚Ò]D½ŠQ +,Œ¹%‰@‘©3ˆÐYbEt!)oU‰Â9Õh"\/ê +º#cØé¡zkÊmž¡ X!:¨°óòûŠÖÊ!ðà‘4QÜ3N²’&Õ>|6-d°ò+•>Å9*VZñþ®Tj ÿ|ó²¢{UóÖ Sç!ðö<6èi§ÏñÞ˜k¢ÜÂØ4íãÐs±ÞG¹êJ£a(«Ñ{ÜÒÁG0£“øœ[‰=ž2S,_ ‡ôÝ`PSN®ì…©„}%3’ÛEáù¸Ëµí‚å:€£µ€ƒ"5OšÓ6’€q9„+Ëà:qÇ®€UXë–ò$ûùßÒ |PøÑe»:ÃÃ$¨s!z-Û¦RBêiÌÈ ]»˜Â&Ó%árÊUöt°,ŸroZå¹i•¼H¬å)ÙÚë±9 ƒ§ +@Qûµ÷¡3CËe¯_ýµ|7,ÊF+Tr5ùåï l/#Z„ŒlÈ^>x¥¯´e¼mºfÞú2ÊÛ¡¯†6„jág Ös¥]$¶$5Xæ’ÔgI=' ¼¦ƒåky°Fߤ>mÆi9õ§fŸ!鳩é“KÞ¾¯PÒb6ÿ³‰Ú“ó}+霃äÿ§¤Ófµ LÑKeê6ÁCÉ {ÑËñ)JœUŸ¯ýf­xž‚˜»ŽÛ¶ny«›ÁJòñ]œm;épál¤øuƒ™ìþH .¯pð½s‹C߈QŽD4Ñë¹=­¢0Íšóë@™þúúšf<øÒiÄ.{ Ô±bÏäv oÉ*9Nds‡¥Iqmë'‰lœMìãyWJˆ¹Œ¦:D¨Õ‰NtRnÕHÐP\‰JÅØê»m¤-wÓ`É¢íLw|ÝY•i}·NXS(Eï„Y~7 +j ¶XÈo$OªaõFv€¾ÒÑ>™WIÍçfÿ·µÛSE^Q£ºx³µ¡øª„åuê 1»#0ÔÃëbŒœK˜1¶ÂX<̘œF/âàßÍ©]jç \¬¯úŠîžž)'pœ[Tũ•õ“:£Ã€¶Ÿ°-§Ü.žZßLkå\ ]Ö˜G‰Œ{G,åÏ~¿þ ~ÀºßM \qÕ ‡Aåk +©¿ßQ8{Pš²ö¨>6šp º¥7Ä ®EC£HÀ~p½Ô‘7 f6…¤f÷®ÏÈuCû~ÑOS+*ðû›Q…ûwQµúåªy*j†²ß$+bå%§I¨anƪ]Aå›}¬ׇèzo8 Ú%ÖlbžØ×-»t À #ÐÙî#ØÛn@µnœUhh®M/²]¯.¦ª[öÜ"ʤúT¤«¥d+k¶žL†Åo‡Ö8J|%”¾[W¼’0E~´Œˆ*Èb5\ÑÎ$¶óVXYÉç'õž0e¿#uTšŠÌF({Ù”" õ2¿ŽÃ² Š DÙËF’&!¿: © ©væTy^¿¤ÑœÆK"žF>â€ÁtîŠün Ú çi׮ן©×›ƒ^q¡^{iÄ@?·ÿ×v³¤ê÷jOõ{W4Ûñº.Úܧ0Ô`ô,/kÈIº}œm+0q9»´Çª%ûAòøvÑdwg%¶Ç©çÝñ˜÷ŽrA}ˆnçÚYÔ»ËVË8¥pXzD/éfY|ëºÒØ5¼;Ês¤u7¸ç¤ô¶l ODƒ†5åú“õgÛ9+ +83qKhnâ)÷^¹J°=Vö4)Tªìâ“ü4+›d ˆ©ž··Æ2è©øŽP\ÉË Ú^`sËÖiÞ{³Fl¤•JÊ4ÿž½b°XÃèµV3´±Æ{Ì8Ì+#8Oÿhû¨<*ŽÚöe‘L ôÏXE\¥šÌÃÀ,#”>àÿ(®uK¿Óv‰íDΈvâ@aQS +ÔéÎ[jy±ÙÎðéÑo«+æ¬g03¢!ƺ7¾ªc£©˜ÅéîL}1Dlåk¿òÿ*©L;³K:AÀðxE•|9ù$ŽtÕb98#“ tÊîÖYýZtê#e›5$ô.šŽdGÚNõ×ÇH‘}}Œ}š“¦i`U§=9”úgZÍ-Å!Ÿec&/UÖ×Ïñ·‰’ÙV;:;z·´½#ðØÎc{ʤ͹Ns0{f]J…K%ƒäRµÆz°¿iÙÉ÷§u[‹.Žò :ò‚kyz°Ž +¼‘øOšЛH*•‚TÙ·<µmãy¿@ºUZþ£4Ö›L \ÝÖ{¹rŒ!‘‘ìx-ó/4j»"+§Qî\ÇËO|ÊÐÕ쨚QkN¸Í`Éwc×ÜemX³dܤ¬êé @#O²¸L=Ý,ñÿ¼2<‹-OòǼ¤’'À»üªêsÅø]ǃ=­Z¶ì«'Ü«¯nL¾Ere´—¯]™üšAT‘)ƒS¤8ú^s=6µùK.k*&&$ºëŽ]Àð]êZ{_ka–æ&R…nª<@p©ŽGtœu'yÒš—\x;ïЖX®k<`d*˜ BRã‚C«Ç¹ùG-¨¬ßî#­(>*)‰ckMŽNg-[S¨ÑHQTäÚìjuo#¤â 9!õk«Z#úH†8ø*๛”ÛkŸñŠšgŽdrÍ%ZÛã\”%o,ª3HÄilYŒK±Ôô÷qNmeñi‘”@;¥©,O½î´kÔ¤¨W|*ÄÇ—­Ö±v§uÈf}“Í,êq€TQódðùñF®·ú;塈þ±^“¦ +QÓ©„_ÊHªi _æi+«çÕJú@-rX˜EI +(ú¬¸Âž¿Ï-š4Ïfô »9JòMVýë8SS_X›…χl”àMÔÛ»ivŒ‘<ßÛGÏÒe&"ÊH–¤¡UÒ— Ždûv\Ê4P§Ê² )ßøy§®DñjX¹o[ñ\(ý¼N«à¤‡Ÿ&Âðú‹F+ˆ—#ìRDËIS²"=G·Y|Õæò¶FÕìdÓ +ªñC}(xàh#Nòè°CáH‚c%ý6f—kÝd蹤6ξkñ݆Ÿ ¡ûBÐw#à)¡ Ê +.œ2€ Å¢"rl+8ú¡cÃ`Q>Bð ž0r¾<(uG{r}jâ],ßšþ[nxÚw©Í3Àyú(¬ÑÚ‡~åú«7A༱Q #•žÏbè +Mx}QJB®µ{3¥'ß +ûØïÄôå*‚EÊ÷Võ]ùny°ºë¨Ô€A»=C +h^.è‹Ì´9Ù UÞ˼âFôTôŽœàõ.®äÿUÝd÷¬¹lgdu’Õ”Z³ÛÔ¾Û.~$}š¹GEQY4\øù&@Ù.pmJXPa-õ’h×2WJT #§€G[¯Øp×ñS€ÏbªeÁ/¦ ÑoMü4¥2/Î>R#%=š±Ùþˆâ–Ü.`TuŸ›p?,NIï5Â|±Î’â”Éõ­ckÔíðmÔÌ«©)–<ŽB(¿núêá&Æ0¾Î139enÇ'hBñ4Øû¿Ò”8)6ïÒ’Í%\ºAbUÖôÄËÜÚ +qµ{‹ˆ1¸ÍИ:þŒ7®ëíYÉ=*HëÉŒ+>á²øytd½œÂ©ÀøJ5Š ¬P ùV“ñDiõ6iD#}Ø{¶×lÓ€ÃL¾ôG1èö%F•—EŠfWTЕÊâ8èÚ\¤×3ƒŸ¥.L?°–µâkå(Ïñ¦–žázasE%]y•€o‡ŒK]7(13·QÅ©ø+¢l·“Eìñhõ]YT®o¾DÐklÁ'°fj[Âs ï|lq±VÀç ]¤ý˜O®ó?5‰¸`Ã~­|5•+GÊÊëd4Cc¿¡ÅUžM¼þÌåÉåX÷³`ߢ£Q3´F;ô^›^jƒØ§øŘQ=ï +ÂÛë½´prŠÇ¶ÖOÏçDÀ­ìnÒÊ &ó¦¤s“ïQ&7±4åm~Ú—B­Wplft0£(:EnRX +K0ž@Í}I[ ežSHéKøMÔ\¢VêE®˜ž1Ì7Ì#ÊÝŠ5ø@.1¦«™Î-8!>ç|I ûŠju¢È{œñË㉭NU#³%XˆvŸžcxöÌmƒ/º_ëõ5Š|÷UŒÊzˆW,“#Ý Èªgº8"Òmd-abÎt¨V5îùhq·™]¾Í´çã²ÃÛ£œôÐfÙã°¥†¶¢M0FVŽ\î+<({3Úv‡O„ÙãÉ¿ŽÝPEQUoK)H489(ÁS¾&ë®rg?ú +䯗^/º]ëϺv]ó@ríye=¢¾P·Ô ”ƒÛ.M-îÇûKɦ(zQ·YÚ‰{@5"U³«B„I +QO™Híè¶5QvBXªžßÕ»† <>ç¢Ò^Y÷ -]çÄozó1˜BÎìþ:ílÔ¶©^ØdßÓó=ì„Ü*Ý͇º ;®kŒhª£Š†¦:J¯d/ѡȵç$µ#ú­ÃÎi2 mõ)«c‹£–‡øc(GÝ#‹@®‹²0cÝGÈîÇcñƒà|Ž½Òž3«í‹HÕýöJ0¼Åî;ë_›wÍÉ÷ÄX ˆÕnóv xeŽÈ8% TîŸ.Ûup¾˜Îbv +6víæ({Íx‡Pͺ=vÒ&dÕ›¬LèúÖc¤º&°ñP;Ë‘‘¥QbÉL×óTõž2DŸ©Iw&à:½}Y8z.}Ð5´Ta‘¹+æäe˜;Xh¿7ž.߈6¦åÀ+š«,ÿBcz†"SÐ6µ+:¦/’'&JŠãXȸEF¹ôÞ¼ÿ4ˆŠ ÆÅb£z—â6ýÐØØhbWq„)ùU@\[ö“úÎë}ŠyB œ=$L"L™àGÏîí¬çôø]V=Y¡Xs€…oì(øxñdà+6>Þ,¯/à$@“ßÌ)|$ÌÏ`ŒÑ©WCø±·õTÝ“¿‹w=há¬Ñ•åÝ/زëã*Ù«+¶FïC×;Œyêåà ÒI˜+8ß²ü#-Tɶ诟-;âäÉ^û´²³fdD3*I¶Ò†üZ@ÐÝ*®3Õ‹O”&άQJWB¦Ä~:÷f¿“Vù «|jETfÛ^úMÏEÃð¹N—³:û |-ÅúËIàÚ²Âåö+ke?[lÁGD†.áa…åKXú²à f0³}»ÿ0ý×;G{á9P&Ý]ÎMô!z+yГ«FP®4 iX4HšŒgí½Ôìp­]”GŠóhQÛÁ¯)qp*¸u%ÃBˆy¯««®“·/b\o†Š…áÍÚùš•úZËLJNªÚ€EÐ|D‰D³I³ÄàˆMP~cçÝì¢Jx¿Å¢7­H^•ôÊ΃¡OÏ6h:¨Œ!Š@¯D{ž ÿ)©™¿¯GuFÃu*åZݲ ¯*bs)œ÷5µ3v`R£´)ŠEz˜ƒÀ²ðô ;Aàž‘ÝaA}C©cdna³¼ñhëVè+Cà™KA°ùÐ!\רäZ4lš,˜ld¿ŽYlåg<Ë"%qå[¬«—VKÐ¥†ÄÉà¯#uÊš;Çn¦ƒ"”f6$ÅX'Ô7‡™M\{Y XÐi  ¢NSùR’òÐÇZtñGEžN­²Ãªà»±WÃPPŸÅ¦Þó¹%]u‚;+rDHá_¬ê‘-Óë†ÖÍ>Uj<«DKÌ8X‡cBM÷Û!ìX#M½£d’j œeÍmzaÔ´ä*Iu© Ü÷£éº‰…·78R¨ðAºIô¼Ô¦Jî'|I²»«í»YÁ¨uÙož›¯#‰= |Üñîæ[åmbêÉwÜå¡WuµK6qíÒÏXz kÁ€°Áü}“5Ö½DAê8lêK]ØQÿŽ>©œAŸ&×rÏÍí¿i>`Î}± o2´\Bc”eÈ;g‰z·‹¾Â_ÂÑßêó®Û#“Àæ¶êsÝ©“¬‡´rÕçDºPŒn Ä=ø/٠͇ëT~`µÕbÆŸÊFŸ¨ÛÏÌç7rm8 Ö0ÜGšc«¬¯Srýûª­©dÔXqGŸ¬eóäkÜÏõÙ’‡%”¨' ™Þ_ú´#5Tuî·GcÌPÅuÄݵä¡vèHÄ¡+T›–ÜqPÏ蘕y¼ iÿ9‚ˆ£u&º•¨­„N=þˆ[Γ¾ù&»ÈÜðîhš•²™¯áRA”¡Î)Nê’‹'Ž^wéu±úi^ÈÇGÁÛFI²º¦Âܸ‡{´žÓ»âJÁão*²"²'@² Õ`´¸b¥Šsy™¤äûܾյ]CH^ vu x÷‹~›ta®nˆch{D^ÂS;¢H…uå–—Hmð¯REM(åÅZëâ¿ùÂr³2Gcîrú#}{ñ&‘ÿØ*&—L¡Ø;„ývPYðÂw@«àckqÔÀ­ uWݦS…)Gr…(—ú|<­X´¬˜xŒùBiØ+.”ǵ…ÚÑØMD—ǃÅpHö·5– ÍhQZ¸5˜=ö: Éë„GÑC¯Àõ½¶NÇ`En²V’•.I"ÍI9åR£ØšUÛ ’éúòKÔžY"`^B~!Ù ó:ÍV¢]¦*Tù©*¶¼Wk2[×õB°–ZŸ¬]•­‡P?¹!ôÆÆÓï:û]S%Ñcļ¨B;BÁ.sløÿØ Y)‡üv[ØôJ˜7’ ×ØK¡¨ç†ºeÍJ9åà+ š Zæ#2;ãG²‰óÚ¢ítë-Ge­ݽµŸ–jÉ £F?Þ©úÓþ +ÚA’Ó¤ÒñH|0ó‘®³hµlõxÚYå„Y¿ï€0°Tª—öR½à½ôžº•î s{9Û0_,8q‹FëõR”Ø«QJÎî~(5ýP÷ã:Eˆ? ÃûK‚þþß?ésÿ¯Üч|þ¿øÿßY¹‹sª·ºËhöYle±>kÖ1¿ßA.1üPúîç oŽ£ðº9g*6ê¤ñ÷yï›Æ¤€«‹ˆòÏjM»«âBb6˜täpJW—høøç-C9EwÈ®¨RÛƒCwÎÌõÞ ¤›_,ßµž¢JX¬ª¢MATÕ_†ˆ¦©_¯Ê}ÐÂfÊH%òÚ¦ìãÈ>…£XPJ½¼ÎʘÒQÙ‹°#¨…‘â~„^©C& •¢š0îgF(£Fë]s"Ⱦ‰8ž™¶msFZ–í–#éR䵋Fù¦»|æÓŽëŸü‚òHsÙ{ ÎaÚͧúoÜb Š‡û˜·¨³šcjR #…ó¼?]¦dM¥µÖ8¢´¼°L“P7µž,ìàQd¯Äìrc•-` _Pì5Ìñ©Ý\glÈ ìÛê,v¡š½&+²zîCŠ®ó#|Û„ú­£y6hkÒ3x×ÉV™,ÍÙëS••÷nè˜Q˜Àól®çxþ$wz¾@¿ hšçckÕ]VçTvF‘ã—;¾÷@9Öƒ/:›±lMhåÀ‹¹ˆ5ÊáK@³ÚY¦J³ÊD}q +º$\Ÿf %É8w! »±Ê ‰ñ fð‡ö['æçó3`rý0O–9ÌGÐÆ°$Àð„v}ÀP ˜S`P”G–5èÝÕɆœ=À9xÈ —¯_!Ž~Wmn%þ¦·—V&Av‡îØçJ†ß™Ûh_®m]Ù¸ôÁÍ@À¹òë5núk ÷àã÷)kŸ¶[zñÃo­G"‚Fph¢žHÙÖw~´ê‚äÿÒ£ì¢®Ü +X×ÿ©51¹Y>D\^ÚX—ƇÁ:5âh»YÇŠÔ0Chµ±ÄšK<¼÷“:£(¶±­®³}·¡m¬‰ ¥‡ëþe¯:?Htó5D€/ ¦§³8{`W'EM&¯qíˆsGlÄO«1zßf¿à¶èAr1™§™FjÏÞ?·÷¢~DAÀG¢õ( 'NuOEþ*Eаl­–®ªŸÒ¬Iÿs7-—5!s­3C¥II«ŒŠµ*nsKþ[ÑBÚ÷Ú£Vˆ"ÕmTpgkeÚŽ]¹ú¿UÖw6ØwDHâˆåÜçCb#K8oU ÷¦ã®¢åÚ^Z\ívlØcÛðPn Îzf‰È†S 9^°k¿éÓn7Ü[Û¯£¤Qu+ßo +t>ßòÔ¢‡IýZ”…WbÅä–Tß>#R~ +ùþc£Ë?ëR“É~äYñ¦ß~æ¢8¦ß’‰Ë8DG{Vˆ²ByÑnM”ñ|ìíœÛOñå*‡Äiª*<ŠÞ±.ÿúÓÌM|ù×QæÅ5Xc§P"ö€ü×39ˆ]S)Ò»0¥I)µÝ„s0þç#¥m2Ž3¬«FkV ‚ÿû^}”µ,ª!”Ã+TÌÏJLY„¬K«e„nàÀ’aÛÒ4ê(s$ûÿ(¿—I» Êgǃa½Z05+Jv¢QG@øJ:!›¶Ý“ŶÃìm vG鹞<犘­>:ò·ÂkàWZ<\.{ÈÛ«:ï=>æÜÒbù3e±£Z½¹›±'1¹Ç®Šb„‹w¿ŒU-ž¯$éÈLÜ4D[k¨óˆ¹>'.rtûöSËJ +‰|«5WÅŒnmXãRñ&\j“©´3þP´?‰Búʱ¹Ã§Þ“*5ÅyH+¢÷‘Q˜4¸ð¨ÌJºæ4µû"J*a‰¨äÚpÔ}€óRYgœ.7Ï1:$˜xÞl{‡®¡0Ù;Àø{ÔÂ]£[-Ñ\R RC!âÏ}.¿þ ~¢RJT®]N£ n" +)ȨçãÔOB 1j#†—Õo¡IT¸.©)Ð<6òè òˆ;LDQ0]ùŽ¨è½´òÝ:¿°=¸Š%°IâÇàÅôê~.>½5|XzT惲¢;[÷O‘âÇ«}Ú6æk=¿c†ß^ÔÚNgœÒp”©ÈèîÝ¢¸ ÆïD?Û‚úuHQûˆH]©î(Ÿìüf7ÕrFs¤{x¾fnSᇨYE0£Gµny·vEmöA€Eéà ¿€hËŠRWÂå½­t'Ž-IO„3ü'vÇ=z<À ÚH#ëÐuãnfGœÈPëö23+ê8²Ÿ¬¯¤ï#ÁOYSä‘î[±µ ‹~Ю‰z×W,­'üë!Í?ÂB$:#´ÀC™-”›•¶Qwh´Átzd¥Oß¿”zêNøšS)´Þ¶óJDd‚Êj@Y‡ª$ +Ú÷ÎÁ) +Ó.t,ë·$5MubÈ„û…xš¡é)Xy”Ô7â˜Nõ.󬫈¶çÓj­R?#~»;!êϨ—«åû±Ÿ ÂDgµî&òÚHM;ƒv ÃK¸Gnì3ŠOÂæÂ(š\Ý–âæãb'˜QÂõ\s!Eű¥„1’V²x&ãAz»<‚týšâ⌺ÈÙQØìgr«ÝF[«FR=PÕKžÌFä®N<ØFôÝ“2öëH —n¥ø¨!w£p¼Ï‘,󭈦}#/š 8¼“ÊU7oÕôGÆÑ)Ç[Îò0= K>ø¹*ÞD»+=Ãuë¬Þ×(ÞsïomÝÞ#6áô²Züøç}Fý³Æ¢ºï}·2N.@ŽM9UxÄŽ˜RòPÞÿžyÛ^îã÷_ƒv°x%pö²Uhæð±Á´ºÎEîýx´~DÁ)  3ãFªòNÓt +Ѥ«ä²%ÐlŽáÖêã=Ö:–kgÓ?Úk ƒ{#BÝï¯íò”ÍÊ–ÍfÐÃék;s £MRay¹¡¿Ñ&ѹ ++¶ŸÎ­ù¬%Г;5ÂõN VQ-ÀIÁðÝUU€ÕZ=ÓÑz¦ ar«[Hª2* 6–ÕTtÄmo/Á¬ŽÐb)œÆË*ÓeMƒm69ÖlkG 6ø«µVc–xjDèZzÔ-1¿’×ÞÒµ%ã&âËaÐÓ5öµ­{p¿*6ØEji•¡y¦‡¢¿€ãzö5Oæ¼%ÑË|0l€•®H|Òó™® .]å‚R>Ϫ®Á½ýëý“Ö÷çs¢¿ä" r’‚>ë+­öuËܪLÇðËJkÀŽ†`¿“ÙÖi‘ñ£"Ö6Jëxm[Îß(»®ŠëZñUëû–Û10Ýœ¬3¾zšhŒžNv§ÁÓýX˜^#z0=ž ¨½h-Q·/ *ë<CTäú®ÂEºFÑm”䃔 <jÀºÖ¾ýj€çãô$‘•ÞÝ0æüb• ¿}í*®ØJ䪗M„k 7•k+µј·b8ŠA­óX«‰MZ¯ÁL?¡h/(rmåwGx}¶´©‰ïïöOj¾ kmdFÊxœ™Eq“Û=2­’–]ÛUªg¢:BM ­y*mäE¤ùÜ2É–öDPWø|¥=m¿bEœÒÏ(8DÕ}>ŒZDuiŒX3= ­™¼È1™ÎÁy{Mtôé´}¯Ì>@MÇåJzÈ„v ÔGòˆ† – ½&Ÿ±É;Q`²ô1vBb +ö(¨5w”ð˦æc˜DH:Sƒi¬PÄV€Ìªe™?ÈçÊò÷ôÛÄjs}ýHšËalÄñÂl6ø9"7+«v9 z_v‰øÉá¼6GŠ4Áήk†¦vˆ:÷`4«»Úqô_! +Ëo`Öé-À³ÿðüØMéª4úÅŒÐ-¥'_™ÍFŒq?þg‚×ûÆÉŒ%¡á>Ò?½c<“øÒ”àu)üs±ïþк6ZO›KÄXùRæ19Cõ†F®Qàâq2]öo˼)ö@«†¢ƒõƒÏœÝª‰ÇñKc»Ÿb4ÿ˜ÛüC!™¾m4N ŽfVÕ:¾…‚âì·»vïÏk5õÎÐ÷MPU!‡Ï”b·6|§Ë3”Ú +‹Véûq_ÄG¸ùÝ„"µîôù ƹ7H’ÿõDü)¨iÁ{ãæj +Ô +ݘš zXÒUA3#uH+ßWƒÝFíiKü4J+C(†]¡2òŠXë•™™8k?,÷ˆÈ»Ì¼”¹3$(™ˆ 6Àšä> 4XÒÔp±CîÇ|Ê,£ÇkÞ,_·‚JÓ…óúŒÈf¬ÇË6‰'j^‰"ùæÙÊí÷£/&j0iäFÉψ;`\á1·ÜG”^6g—M§Qà´8jô¢€1¬—ê#·:øJŸ÷g„G‚,é8dßíGgmx$jW= ô|Z ÃßE²Á£KòCšýûûÊùj’®œ´]ÖYóa÷˨H· •–oŸÏZ½3eÜ÷7ûÁvG®l%ó…Ze(–Žo#XD§D!õ«¶z‡¸‰ê½J$Åz„. Tn•¹K-g¼g ˹5ý„0åF÷gÝê}Ÿ¹w-ð#¼sAÌ|îçQð„I[[ß W;’pau€²g…Kî+®+Ë`õ¸çƒ#k¦{›PžÀ „£mtî Vb+¼ùñƒUÕŠj€Ûé£Ç飾æ™#æt+Jè‚GwM×(}e?Í+(Sì«òn®d`îTÌÉÙE9P¼ÂPYdˆZ´ÑÌ÷I€$=#˜EkJiMÆŠEfÐ'Vuz>„†©™…«3p]"Ú1_YŠ-@I”?¢TlHn èx(Ï­ô×Ê›înšñQv[_ÛÛ§pÿ¥mg`Õ¤‘ ‰ KÀÄ5ã)Jét—וܖK¼ƒ.F ë¥­Ù·¢ä¹]ßhª DêòÝ*D`|œ*D<‚€©‰À÷äu€(c‘Æ ziÕƒ1\KÏ}¤d¯wW@-»©ï¯¨BJÝåÚD4ÌØ“%÷ÏåF#µª>þÆ|¼îè©,\íR”Á]uÂBé™ÝÎÏì© ´ž¥ÏgÔ('s,ƒj+±éZÆw&<¨îßß[ïøú"ÿÒ7ûRgvý•UA;¶^' +1ʦ֬¹`3ÒÊsŠÜˆ.¦7{Êæúõ„«î —ŠE¹-ËN´ËA2±ÔFú;×iÎj>{ÇqœšÈgÀoŸ6Þ°¸?΢ðüûnøîÃËu¡“•ìŒòq§ôðñíùEß­=dx‡ªw‘J¡l\„®¶½ä`êÑ ’7¶ ÄVˆFù…!|‹!<_-,8€G°ê/û{÷ǪuWʤaLíYÛ)§â´‹, KEGÙúq_ÐeúSž™ÛzÞ•ïmVÑ·hÿjˆóNúˆøç]Frìþøùš¨©¬Õ[´jæ¹×=¢&U\äM€Æ8ê>rŠ/”¨ÇåèHveP;ˆbA4+ÚñTÌè¿wÄL„p(#推Oø~"ÊýòS:Ž°œºå)ÎǦ8_á+¡>oOéy(ÚÄßYù2jmNb–JæsÛîk[w³…s*ò³ùŠ<§ºY”@R(;S¤õ¾"∨«52£L„r€Gñ›6å³ BÂDßç3ÍóY„WDž÷]²Fy¬©œhÂ4"õxÙ¤zÊrò%ü8NÌÀÈZC ¤sˆ—ZØŽ±ÖäsïçŽ-z 0.Øб_‡å±# cbò€òãÛÍÈ.æƒ+W jÍÉÝ6 +=‘˜§ˆ;jü9×"}œ2™àW®¦â â3)]6PÖ–[—IrW7¶¹û=§†Þ*°\x«·ŒÔõÛ >ÆVû¬¦ù5úeŸë–ÊÉFóÕ¥9âá`™jݵgøÁçf¼Er]°ùºAåÜ Ç5“ +LÀ—,CÜ)Ž j1YÂe6|KàÀ$/Ø RfäŒoÐúà”Îm»a Z°;¦·ª¬à¢Kf¤‚Õ47hÓ€ó½h(I±ï9U1,qa¬¦ˆ;ôóØ dð÷®Ïˆ¼R[¿jb™í›ýôoŒ5¢2'ñ{T +B¹>=¿ÇÜðSj°ÇYýôx•@¦3š¬á8¬AaÈÄšÛãn nc³cC jÈe^ÞRK#t”ÆRK0gèk°Þþñì'4mÆ#zÞŠ¾ÐÙº£‰ Š‘u´ÍÚ×5…òÀÜSc&èÌè Ú‹ºœßìgné#[\çFö^âjçQ¾ x¸«'æ‰Êê-úÍPŠ~8Î-+CzÎM ¢Žx(—\r=·¾Á´äc5\5Ìb~Œ ¬ÑÙÒÇŽˆÜ5Õ~øºópÀ§Ÿ$$šþÈÆT`X«’šBÙzMŸQ„¨‘ŸwqÙ©ü«¤¹¹™M¤öÚ +Ñy×Sʆ¶äa±è!æcí°úZÿGt?ÎôŸYRB”zôªû–FçÖçø&€Îˆâþe’vô§ Þ+ƒÖàØï"Ä•áiT¢eýuHñEð9˜¯ÏˆÜ°ž¬éùÛŸû97ÝBÎVf©"èp±rÒ6¿t%”æÓâ¹sS«ø­u N¼H¨U¯µrô¿Ùçsgð'‡eð¾=ú ñç Äë§ûâjq׉~ ìr +˜<Á²}Õ‰~ŠjV0½I%ºÉ:>è Ae­n.Ðç%©å÷Qª’m«ì°ae5n9€/œSJø•kHÀoG›Z›ãvø6±&)}BJÅ„Ú¹r¨É¤e^ݦ¡’ xС¶ ƒò­‰=K´³n[‚¾m ¶A<ü ;Ž¢>.äÏS&¤Ki9äjbÞJìÕ GqùYjy·Âß+ïRIü~„n×:Ç Ðaâ úVΟî,FÅ}˜wh@Cñä+·ðŠï3Ø ®ÿãÿ‰ä9Ú´ E¦Óè’膪ðƒ¶UÑÖ7ë@¹=L£©þ „õ[PÕz®€c>"S€ÝJð7û¡º-EZ·ˆ(“É×dƒ½êç|`ÏFæxHŠzµ•š"RD´tèTt.½±öÚyÈKº(ïýz#å”ój +²tÌb…{*$ ×0ÄîÇ7¬ë9ðãÏcìþ’ˆ>Ùõ’aÿ¯ÜÙ=}ÿ%…Þÿø»õãÇ_ý›ÿý¿ý郕ÿößÿáÿëÇ_ý»¿þ/ûÇ?þîŸþá7ý¿óŸþöïÿá7¿þÃ?þßßüáþæ?üÝßÿñ?þÓþÏ?æ ßÿâ¿þî÷·üÝßýf⧓»_gð!ðŸù_ÙjhF¯¯²0Ò +ªÒÍZ¹€Šc(T0Š8á6zž¶¯—<’ÐxÖ_:JÙë…9€œüåüú†1üjM†j·Ôë¯oå&Ž£"ªŽ dq”‡§ýûlDQìsãDv—¢3¥+Õ¤ +´—ó‡ +F*=¿z Yß%.TÑ5äÏý‡ò:de„FŒÐøLˆ q'1Ÿs'ù÷õ«€ÿ™³þ^vüÁ†vgk6ƒpÃÑv%kBŽ \Ëú“*××Ê©PD¹5\Mlók+Èã¸AX7´æ/¤³» v)t$¢Ôƒƒ¶þ@f ëɻœ©i»NhZKpHùªicx¶=„ oØJBëÅb-èÞB¶…*][¡u‚¶t™3ègT@Q‹­ëû¯!Aïð5À+Ò“ÒE >\H^¯œˆ™vƒÇkW§ëelÈÈsnQUð8k¼Â|ŒPH¯´²¥¥Ø(¸qÓªØ.†_i˜à46†ÊQ.wÜU”]¿úVT,,°ï(ðC·[ë¡ihœ¾›ÑÈB]ñ4])8¥ðÏ¿³7sõ›ƒjØŒ¢Ntü¿&¡Ãik%œ¹BáeO]Hr3u­±ƒ_­'³';iüLvÃÉîû)òI‘Âê.¬3VŽ‰þEÝT%ŠøkQ ¦ñ÷ ‰ÁçÌ>C–Ͷ»,,ÅÖ×|»ø.²D+ˆri4z>#`‰NùãV/t‰Ö9&eF¬å€ˆ® +ºùèàGv{+¯Agöºà‹$¢”Ï-‘ÂømM5ঠé98RÅ‹#­‰¡$ÉG1€¢ø­ƒ÷€ í•ÝŒ/]ñIó{øˆ@ÿdÍræK ±äEÿ±¡uŠ“óÐW& ïv½ì÷…)nFHÀHì÷¾?5är#;ÇbŠˆ€Â +¨ã”³^b1Ê€  Kn߈Ðd%nÛ>Ï¿ñ]wgúuo ¸`Z–àÈLÌ2elÌ/4¥n„ŠPJÒ!™U›òu°6UlWÄÛJ2¼£oFDÅC¹Ã† ¶8ÎÄtiÎœHQEן+§º±Å•µÉ>ä¯É¡Z¦ÎÀƒµ±ëSÞ ’õ‡® köø3ŸÀ¯ŸïÝ7Ü\GƒÛtªRu+Þv¾…¿ßQíPne^‘r¨Ûó:qpþá~|½Y8Iâ¬Ù Ò¤©8R1-ðª×#¾MÍ Í·,šê{‚ƒ¸o÷£Ú ]ëŽhb9xaW’g·GJ“#iÏw)À¹Tá1X¸¡q‹h"¤˜5Zkƒ$tÈ–ú-jŒÝsEÀwZȱº÷o"œÂ¸°ò¹q ãoi¼ÿ*½Ì£¢þcôˆ›šr˜Mæc´4йÒêÆn&6o ¿JŒmÐâiàó×Nz6tõ¼fÝó«Êw#õµFÆS3ÒÁLVÝp+ñ…—{›0Ò<9ô§ +¢${o,­©›a»¾1@ä*îõÓOID†«°]UµàãØ­ÝÉ<Ó·}èmâ> UOœe—Æ[ ÛÕ¯?#CQ§óklíÛ¯³Þ¢—þÃz %c$´fÌýÀ¤Ì3ÙÖûß½,ì#çüØöÊÂ\]Ú (™cûøµÉÈ}o^«ÊÀq }!­„(ú±žðzí†M”+•dí"ײ2{P2=5fäáÙr®±›ŸÌsÿäT±1|g_^,Äi«Ý‚4ú³»ÑÎ{;¯µõ(Ö²†³|í¶¾Ö±7SŒ(„ình86£K5Mh®Ûý\ "Ѭ%}& ëpQ-§B²ü¡ü—€Nk:‘Ö³fx Ñ¬Ê(Þ¦Ó>SÒ¶’~„åöŽty&Ïyš~`¨ŽXÐBÌÄ1œÃ +–®v é`ZOÐé]F‡o=Sɦö·ðHú”q¼èÛq#`E° +9ukC¬qøNb¤S>ꉉ¡QºGåXNŒ]×@ ÜÔ•‰`âáHCÃÉš¼˜ˆ.¸´Z`¿ûã×H“>­4JPxªZf~äHuz.—o7èØ¡Ši;õ0-6Åé2ƒöÙ©ö{Åâë¡sR]–ô9$ÞE×jÚÇ?ÅG-»k‘yÛŸ™ÂØ!¤°ˆÕCš.ßb`·¥ »q¸HiRún}·‘‹ù’Z}·‡Y9ˆ•òÃý\mgž˜ë¸Žc  †&Âp>W â(Î ŸÑ|ž¯çðóF8àhG¢ +'ñ€€¬›ÃÔÀ48øk‰ëŠ + u>–®ŠÀ7ŽŠÒ˜Y×ÕõÇ~Ð!8"´?™£Úµ"‹$M–²#F·@%Gnª•èçw„î¬Ê%Ôép±r¡Ù袥av›‰jGvM“YÃÔî öû3"Õ0$É•kTðÅ3=òÔY¶v-/P˜ÕxÅþŒø[ú¼]?Åœ +¬4ý-0¥ˆêZ£hÐÀÊ\ŠKwoÊü+âÒÁô`}EÇî3Â+š:rÐ5Ck훨2pxÒXåè¢&mª«‘ +‚滚Dz˜Q·­øÞ£l00$âÝJÅš6e÷…,Ûµp6‹j¤è-u¤ÖúgÀó¾ ¾õ¾ÕÒ®ovs)T÷§>oN|·âjºV-î  Ë†Iv$P’Œã±í‰ˆ:œˆ&(Ú1g?|â+¸±í]s ’’Œl¡ÛÜMï‰Á®h×ñ: +‚FðoÇ“;tºt‘@?2ÑhƒŸãÉ»¥ î+JU¢ï’ç3`bgxdLA¯Š5º68«®Á‹ó]ߤ×LëØ/ <¿|ï=ß{Ÿyï@W"ö¼ñ»(ž5òÊ<ëéAŠEXLÎï¦Æ‹7ò=å©h|F½!’ +f“GFßd'¬ÐIÄ”Ü[Ÿµ(•qFä–úáuCÜä®ßì‡l)-njN´9£ ÙN%õ#ânvH¦ ²GŸ·(µ7µGiP<ãÛ:wdó)3 ›OK}],Üo'Ÿϧjéêã×­ ʇÿþçŸ]UÅô™½žÊÛ{ÔŒ¯Š*ÉH²rܼ}3Žd}*²íƒ´€®½C ßyÈ}¢HKžŽ²PÞ÷sSøQlTõþÌÎúÎÎPvÑ+yã)ë\Ì ”ß hcl²Pf1Úãø¬ƒNSýhk~#7mXI ÕE‚€ Ê<6îö:ÒÕÉÅðâÚ’ÿYÔ85“úqE%ÿÚ¥«Á=ktH´kY\p?—‚>± œ=.aÓ³(Hýª½5ukÜ„/Ó ç +XØ©±±´’÷ É.¼œ +ÀÆ“òãç=ßMCJNº#¸BW@ÎüŒ¸d!ó²Ë¹~òÖŸ£”? †Îj @´Àü•Z6 ‰<•¾T&4Ö/M‘á«D4Š.ý|’þc§kk8d®70n¡†"¢¶+E×v š‰˜w6€AqŒH6ÜdÆ´dχ,n¼Õy7`³§Î´²ä8Zi+âP{EXm)z®<…»±N_=C"({¬ˆ—‰zwuÒãùÊã×ï¹P؆iÅ©ãý¡Oƒ8«R(·:å´mAÿ1¶:/mëÀùBiª6p¦Y¨BÙRŠ…O‘©<\*>½½£‡Ï&(moÔýµ…öÚ·çäÚ !ª˜¿—B4+Y’® O¡p¥œÜ JîL€¥(‡Ì‹gÙu#>fÓ#3x©¨¯¥|ˆb)í™SìjÂõðÛ§ŸK•bvüèÞö²î^>‡S}2|Ò§Èð雈o'Á÷(e¢íò­wi=ˆg)„{Š¾1mi‚æZÓZi:œa¥Ð1ÁÖËM£ó“îÖnzÐO{­œBPØ–sCFoý$¦øugDχõb÷VÂi­®• ²*Mf$«Ð¯ÜþÔcœ(ø¼¤!ÀÒŠmD@”E ž×[™ÇÈNQø)ú+ºõZ¸æ¼›‹¡[›ú5ÖßÌ"û/³¬t!k=Ú;ì5ØØa;ÆÞО ð¼°—ÏÞ8TL' @éPY­›ýˆïExwxʮɉ ¬˜5²8bˆ¡ºœ»CÙ‹u/sݺ’*@â‘^ ÙåË‹‹&“Ö·ûÄê÷Þ¡G* 1ƒ)ÙÒuÉ{¿ˆŸSä·/s kSRo}8elΔ X0l”رÆ&º'ài‹ˆŠí-uüì½kÓ-Çyž÷ øÞ/©¢\…Íés¿ˆì0¡-ÛR¹R(€($" ¢`*ú÷y®ûéžµV÷¼Ü€ +qÙf™â~§×¬Y3=ÝÏá>p æäXº¦è£ œ4Og)·Cè$dȆ®èo‡·)§Sçáú*ЇÅ*®ctI«X<┊­óF"Q£¿ è&+¤EÞ²Ëï Uê¦öš£çæ÷N:^šùÒ4‰o‰)X$Û-ºØfîf™(‹=ý½¸obºqÂ~æA˜Ò'Lã³ðÔ…H5h|趂ÂÃ!Oè¿aïz¢<åÕTTKn™¹®¦ºŸëG{ùp9k§@&D¬h„åX±Ñ9=è¤ÓïJÊÒ ¼"!¼F'«¿“Õü.Hf™…2Ðå)M\éy}R#PQ ’äUNTº“à]Șóú&¦ £Txñë úx•3†®kódà¡Öe:C†U8Dßã›ôíDIwRü —ßn)¼ý„ÛJÒ…6Œ Žñ+ˆ1 ++w» ¨)û¶$‘aÿYq¬åñø% ŠX§Î£!Š0 ¥ªƒü¹è´mCˆª˜j-åRXÔ—ž©£b ¸ ú¼Od9û75öºƒ/ ű¦ªQ +“•¯˜÷™„±¯' è‰FšüD‹«Ý°^‡³Jå/Úš±GtÀJG¿Xñ(^Å¡ +).U=@Ä£øžL ¶,€‰ƒt˜äz{J¼‘¶ºZJ€…*è$™w,gQMG¶ÝëU tƒ.¯Ü‰ñßGåìL@Ð_¨šS@‘>Þ}ÔÊiî¸Dê[â Œ„Û:Ÿ+TPVàSõ°›óh~t-û‚¡ƒ v¿f”Šm¹™7¹WódÒî£piÕârb]lçqãtb[1”7Tw;HëVT×#×’Ù]áñL: “à ] &k«Îãàh&0BØ¢Sjæ‘™ÈðÖñMÑ,]MGF)°QŽåãz¤à€>4‘#„DéXjø$¥6Íbw `cy +U[hEBëשܥ]sª¬'Ó”#h¿D^çìúlA3ç9aR8=QÛvTh°ð§³D<ûy°Óy2YéºÄºw-ÞŒÐ<ÈcLÎ6ŠT‹Hº£'øMWÜüŠÏ!S!Rw§"a-%€@tÉ$Ö·ør ˜Ô±ÀÚT+ÿvà.ìÁl’U^óª€Í‚Dm[LAÝ‚CqØ ‰îèû<ð4¢Æ"éJՔżh°î¤;™¤;ÙÎÑjß7°Ï¢”Ií·‚4Áy(à`©­ŒàÄ‚4à»T{ë|âl?m½ÓÕ<EÀÝuËä/ÆÄŸ“Ðë@³t]°À=X#\xÑ^OHðó›|s¥×!Ô@j–:ªHÙÛ¯Àª5h#¸C¡ey•Y¼ôM¤¤ðh%¾Âˆ®5^èGñ¼Ý©ªÝYÏOZ€MYfŽŠâ€AâÀ 'YŒòÍâí +¢3Ö«1ú2ÊæËPì +(í'­¿bÈyY_d¤J…é[çîI r?ðå²b“EOUxu +q—]VM’k³1Î2_ŸÝyýÉuà¢ëÀ±ÙËî) F8lÀݨCRlÄ°dwuq-ºåæ4è't·t8–1 V®ÈU‹­ˆ•K÷/ûý›.º ²G¨A:5 +ò`ââiÅJ¤ØñâfDG.ËFŸÅÁ[qœÃ¢TAV òdº¾Iëq]( }RÅfA ã‚ñ^Ø„M…ƒþ6ý & 0d„½w~ŠÆÔ¢8>@8Üÿ¢ƒb2§iîñmŸb屉Cé Œ"µê8_Úí(#˸`ŠúmÁÓsäKû(ÉU–ìr•2È¿¨¡·ö©›Ú‡njSQ‘DYÿ®)…^hÛ1lQè,‚0F­š®™Ì¼  +~ä—4]þŸ2ôcTŠèXY +ã2@°Ñ220-‘ @{$‚&-Œ¢³°ÕØYXæ…9¢'…Ø.•8¸w´¶J m kÁ…À–ìd¤:r Ö/c“—•Ã¯ƒ§¼aAÆ+]5ó@ L›Ô¡¨_#bÈ0R•zŒ3Y´Ä1ÛŒ!›+š9¶~¢'Å´Dœ&áªãtMÏ„›(›Ý¦.ò©ªtǸ¦=E¿¨ï)˜êÉ·íGÇ,ÐÕŒ€˜?“ÙM +Ùà:m£ªao¸{¥±õ|ùç4awãoYÚ°º,>ζղÇ1 +nØì,vDŽ® ÚTüÜ4ÄÈ#°p‘® +dyˆì‰*à_†$¤8Zº’N£0€à:Pv±TŠí4pdm$€254MöRX°/«màñÓqÛ/ƒ@ëYdÌc ¢¯´1;ÝŽ8G½a‰Mò“’g·bu¸‚t_ ØÐÂéjIS4/’QíÜûF|tt@µß¦óQ†l¡TékruûSf]Ó49û*R}êÖÂÁ$+khôRCZ™Kø˜k¢¾Äê|¢8ŠrøÌ- .ÊÎÔµç¯í”Æ)9‚½Å¼¢¿£î¨0dšœÓǪ°;/n ã<âàQ%©­¨p£I`9d5){yؼj¾#¶„»"€ ˜»ÒÐW”+ ÌkúȽÑ@~]a.øzÔ%³êwMìC0Ǩ¹àG¥ô7Åf5HiK9¤{à ­Yö>²ó0žÓùT!• Šr½ Hü K’¦5A¨îBç9ÖÓ¥S•Ï3Å¡L¨5¦KÔÏ£Öù¤KÑOíï š©R, ûl(ãüØvŸb1¸(XöØ`Üi«Qm‹_¦"%!­,¹°A‡,Ô›ˆ,c “Z4ób,€º‚§ƒOß/„æo¬[‘^0„<‰Ó×¹?Õ9´H Ö}ÄÃÊ’ŸSº&¬ÄDœ23BÝzH ‚–Â5üTƒEÄ è¥®Bœ?Àû#³@Ü_È“Áp¯‰;5»”¤‘>S^GÅ8M/ ­P1> ©¹š†ð8HM ‚©2gà‡'ÐM}BÑö»¹ºüYïc1J”ßÊä6é‰Ú7#äi ŽI@ƒ —#?‡kƒ½’ÜðÍ×7>Ã$²¨;¡%\ÊØ*„ꥊ,ù îÕ1~SçOnF£P*‚õÈ”m>í,Ì3šÎØÜÄs¶êÀÔ6NJbEBä¨ê‘Bq6{^`Á‡ìRÝ«/9ñ•!vŠ´¼¶, +ßøùÉ[XeÓDŽƒQÊ‚¢Øg™4Éu42"` +;?®`äÓ»µdØ5šf}£L?ÙÜF9öbdi7O–ƽ )‡ã×Ѻ ææ@ ¨k¹Ì£`¦ÕŒÃ©ÂFÙo¤ÀE-Ký”žÁ¯ zÙÌY +[½ÊüÓÕ•u¨€w`é™4 +@£@Ìja–-û+*u‘x\G& ÀL€IžQ|‘~¨¢ÍB\‚ ¶t'ürh$àïyê4ÂØXšWݘþ~‹©Ä‚Vö3¨E+¼Ïª¼6Y[ÚxQ°ªß©?Û q;·Çpp˜>~ˆ8‰Ü=«OÌî¯]ñF›}œŽ;Í¿xˆ~.‹§£%Y<©Ò&Ý¢Èö§H)Žà²@Ž€lÅHI1o€'vY8“\y@Öš^Éâôyt”-Š.ôq“å+ÚÞ>‡ÍîVŸƒ&< Æò8v!¦aY å¶çîµ#ȇ‡~ ¦)·[ö…ë%‡#:9)„ŠðMœŠ£4ŸdosL4»…ßç ÓºähŽÅ£ÆlK©ÐœÞŠ> ²"MsDfŸðPî3" äeñx8ù¹"u6ȃ×}èä¨FYo|¸êÏòÓ:Å-ð³"ÍY-u<}´éSuÛ _—Ë©T‹£k¬¥ðÔÛÝAaÁšK6A¤¥f¶§pÿkÊuh/Vo +a€3ò³êM\T²X1Er>v”>GH,Â(·¿êRºh_D?¶C鉱loZµ;eÆ#ÉÃÄÀòájgÌQBe,È=ä Šcp‚N’ðDàak'#x1à5n'CRàÙÑícT .«Ö”JͬëzÁÞi€²ò´Rñ¯´9•¡ï&µ3[ÈÎ8#c˜Ü̼"…=WÅÌ@iìŽS¢áÖbâ€ûPt]Šàb›Ù?úŠ|ö¾•ˆ‰œÉÓëè2ëÁßùA®i +ÃݶDÖc•ŠÜ«»íŸ‚µ‡úEs¥b¸wjj¦‰')£„cñÞæ`RÕŠ;{ÃT¦)và°V_SåMpºÊ‚qP!¨1 dQæ‰O¢µHjÓ£÷æiÏtdÛŽ¡ø_äHÌ=´ºãjÈ\r„ 7RÊLQq|ŸõE»¼‘â#&<<&”àà䞨:HáËR-$ý:w—OÀf +L + ø]f‚ü=—axñȎT k:JZ¶’qpÐÜu#¸ë›:ò󭛧̡ëM¢å€³Ä‚§F»_õ]ÜB¢šO\œ‹ó­W熪D&› (&£ @æÕÂ6ROè&U:©¡Sh´àƒÐì¶nÁ8´aÞè¡n4_ª'|v*@†*Ú ÓF[‚BSéÜ» +¯";SØaågïîdP¨KC]r÷Âqÿ9f®Ðè‡9KJà@‡ƒ÷—ˆ ø§ ",Âx%ºKÊÉ’ã­C\0“b€'_ný Hzîü õÚŒJÚùÁ5¶o‹¤{Öó*Y3ÈþcD¦­ ™HG¸eœN$*]Kt.*!M‰Êa%ý6ZWQOpKRªòDçQ«¾I`´4”»G¬6€¯@îÏn§£‡ô2Húë‹tZvS)eŒ¬dŒX³hÄ£Fäñ“atôuJNÆ:X™”C90Waû;r:4´éŠð™(ÂG·täÔìTéDÙý¡çJ«J•¢7Ø‚6@¼«ú§• +:—qª>A—³$ÇD¨Ó-Ò³¨:ªuùŠ‹ (w/Êæð8­¨P´À½Ñr@¥‡Y+el&™>æaÉÔÐå?Õxg¤·áú\qsñ—¬:õÖ]’Ѳ¨x¡(¾ ÁÜŒ×V¾Ö{4àWU&a3õBzNRk8§¦HKÛÒµhú‚øj‚‰ûu÷ªvgzµ+.ø$ZUnCK¹ÀÒ5gx(¾Hs{ëßhÒàû•ę́$JÒØΉYDK«88r[ÅHuz²”åð‡|xV¸¸¢Þx†•ÍI’<—%õúN—ó~ZH0W/CAPCZìäßt°ð¾)UÀŠü@H·_Rù…ЗA‘Ä|y—Éd㌩ß'ÖÕG„ó:~ïõM†‚‚YE]Ú»ïOsgɤ8KÕ˽j—ïîd%¹\¦ë˜’ù•áQjC[Âö*Pc+¸ÔÁÄnÁ™ØC!ˆƒYÎÛMÐC‰žÒ°½$6„äÈh+KÞ‰ùZú%ð òg²Ä8ê"˜ñI§S»Sß#±Ò,ÙÒNi°ºÏ·Xïqd“öœ…ã”míZ$üdzƒÁ W7•+v%W<,Í‹0ßBÜÕ:‘`/™~F,Þ 1ˆW‚WN{Hg”èâ±Ù/ŸØ\zš÷:Ð%  +¸ÉH!¿Õ¡«†Í9ÏFÏÍkÉeN q`ôç×pê®#Þt@b‚6b›—<´±{=‡³eS¤ëçâ¸zI +o[Á +ö—ˆpØƨô>@žÉì´è)iIº²»ì“È›`yIa5IšR<ê£[p¾Q’‘ÅŠ)eŽ!,ÞÐ@Ä›¢DBå§=@¥‹«ABÔ¸*—Ý¥Žè®ÀO…ú…60fÇBÉu—ràzôŠ˜ìRêP”Ž:qZ‡–4Ø„òkAaµ”]Lo9߀რï‘gÉžZf:nPÔ3ðþK à—ÂËê†gA¼½\û¿ö›éekú¹² !÷e59¹*ò!%"ƒKtà¹:°ôÌüB=HÀþæ0 ´õÏQK­Ê;Af×f£þc/*_ØéäÓ2îC­öMå)NTû îËßD¥)§oA—"b*à’±ÀÖ+ÈÜL/V*/ðä |J7ªÎ³h©Œ‹ÍšJäÞy••²6GÀ˜“¬Œ°G(í8¾ŸçÏvø÷Ÿ3ïÉÑÅ15O´Ï’ÂH1´»¯m‡´2•Ë' ËJê„UÖkj}ÓyĮՎµR‘i¶œÃ k™KÂA¹Ñ¡û9hìîI-š ] +IÂûJ¸ª–T?„q1Ymº(ìšË¼+nJ¶(Ž9ÁA§æFIär0Ók%Maî”=BR EðÝ›ÏFH›i}Ù’ "Îe‡ VÉ. +z)Ö†¬óÏ9…î;äIü.-Š^–O +× S–\<æ¹3Gf&‚5À(Çç‚Õ ]ź„¶‘2µFˆCÊVˆÝ;˜ÁÅh 1.nþ‡Gè‹…}—ÉxöV*ò„6Á÷Q øPîĘÛô +’“%-hÒÿRAÉE;ißð¢¦c´ç€Yô´#IŽºÒ'åeWVÖs`ÇÈWAdf–¸hÔ,{bUèýî9„àD6o¿OB*AaÖÐä¿Ã;Üç \ 8“ L?d;KLD”(pÕdÜÊ®”†uíA³¥¹NAAeœ°° ImÞÆOZ0€ðL¡$-ãÓvWÒÏveçÉàd:Ñmé@!Á +öL”²±–³@cƒ©•ü i¾ÏBU#(‚|.+±Íe¢L°®4–ÀºFÂADö½W‹TˆaýK]7†8\¬g÷BaXAü¡Å?HaåØk 26—²ª´Ÿì«„éL…{Å(A—Fá |½!¾ …É4B1ˆd›µI0‘þMÔÔa±‰æh¡!õŒæ%)°¢²QømQBUÞTÛˆWB.Œ«²‡rÎô¼å[~SQɧâ™U·µ%‚²ˆ¶kÐ €ø€MnùpìC/w*Ì$GŒ°¬b±åñ†RÔf=)Á!@´º…SÈ£Y×-´à87{ÔŠZ¦ÇÍ‹¯;¼ñ“¢"Ѭ·³ÄáÆÌ\ô‘& ëðsÔeÂØ¢¥P\h)U9oDQî®í‚ÿ-´iÆ—°¸o²ž2-ùê‚ ]õèédÝ) ¾s^'}ÚêC|8¿Àw[›‰Ä÷«Î÷“Ü6Ÿ¢ýbïd:Ú`zÈåôtFô‡Ú±j:þ‡¨s„0U"eaÔ¿ŠÅƒP$KºÇUÈYf×^¨T-YHrzòUÚ]üZX#Šî¶Œ7ìn·‹P†:a-ðÒ5ë‹¢“xú’òNU1à 3ü, oÑ‹‰//r§Þ˜m%žë˜d§#¸ËÉíMZÕÖAàLÍvBùw&§ ²ùŠéeóQ´ÿ‡«kN×f(© Zmþž#§BC3H~Ât¾PJ`½IÌYÇ.Eבö‰BŒÇSPÏ,}ñ+–*èEV@žÍeÔܤ“N_@$£YµeU¯Ú(`GÀÒY®Èi(té=6õîeM-öø=K —]Uvð¿-N‡$Åéÿ€a*Pˆ“¿ÜTº0²S¸QÀqµã³ªD—Œ…Ž|ÁÏGH+:Vÿ¦¤-œ~ñoàúz%DÈ +Ð,»êꃺ˜Ä5KPVœÔáØÊ´Îà,u‰5j„.¶ty2Âã^Uöojð,¥R­{j† |ƒ]‚^‡06ÌÑì" RЧÃ"ø²N‡ æ6b&“ÃF-ÇQD€,ÙŠ«ÛÝ¢,ôÑÙ\•h0B—%lt+ä]a\ã@)§†bŸ“À‹ã•,¦À"ÙµMH‚úfÝxàFQ†ì +€í$<Ãä hi FANO™’‰ØÕž`šÞ‡ª!¨ZIö…(Ä…( Ë#µÏ,z)ê~O&Åj !\à=,éN㦙.c šÂ9ªFª¤T¸>1×=w "z~ÌʨŠLk.Îrò¿c%˧Á!h-„,N‚_"Èi„KäR†Õb}xM4RB=“r%]d,NYr\o‡¦]A‚ H²PîàdÎgƒê5ªdeBXØuj¹ƒ¶¨_âpðæÊ_A«ÕÔ ‘u“ÙÀæ‚ !mád³3DeD{2wͯ…Ñ©èE®´ý4m”JÔ\$ñUÕȦѠ1û ?$63Ìv·_ÿÌ %¤…K.Ù`‰Ew4ˆÕÐ…ô*¡Lñóˆ ¯iéjÖݹ'6˜Ë¤ €Å¸œFæ€jY)W?š*õ*^1'(;K*ÂOȼ ü¤'™F;ÑpäZȳxÖ4ÉÏQ5€\QOÊh-±'}¨ã¢vØý…ÆŸïa +&, UÚ½ ‡Ø…îãU>´¸ÁzÞì Ú–¤,Aï´b SÎΧ$¼Õ^*°ë¢c*„·>š„ó†Û“Ä1ÁS$b§g“:OÊ‚ÑØÚÙšQ…¥U©´N!-,µââˆHGP2{5xw|G”¢x÷íššÌåˆÑT®µû‡¾£ê'u½ê?©zv* V ^˜cŸE˜XÈÃC#—þD‡·+{¶—”ìÙqè4vA”-Ò+œŒø‚Ãnå1.‰ö”²—¨ÔTÜ/ûböžä±O8WX]:àY‹¶oo;T÷wÇùõ Ãq5RƒÑY"É@E6öI6i½jvRþ9‚C€š,s +¯¼SÖŠï_Ô»$íßUë«ÄÕk’é¦äâ1ñ=’hƒR¾'<¶Xä›$Í*`—“„Êm„*ó›@ƒ,båŒ:9-Ùb9X:ò2x÷Ú¥]]fÓ¸1ÇÑòØ7[€Ñ:êQä´ŠiÔFiw7'¥#(<¦¢’"7¸_rüDÑ6ËNÎeQdìgKÉ!UÊ娒ã1 T£dlDø\òÆì~ZAØen-ß:RâS2}]ŒžDw2¹£ÒCu¨©¹™êxVÉqó Åh¸gMòcyb¤gáö[b/,ô}àkÕðý,öˆ„lÜN|A§BQªª½Gs?V[ +°²Süêf°(%Íý×$ç€#õ¨–ý}SdzU<\ Q´2¨ÏH‡ºTW¡æ;@šöMBÛ?ªà<æã‡;pQD eu3A(ª¢W’‡®\죺Kd?çeq/«‹>LíÔoY¨^@.¯ê¢¤Ro½Àg³& Ñ §X+Z²£% +ÌJ¥sµÚª/4M°_¢¤SðRùðTŠ`Ò‹EÕNš6n…"d*ÖK6k²Túñx%Ÿ ¥âm2à«Àat3+ :ÀÕQUXQÈbÜ&FÈà 8„`}Å_V}Qºn¼œ—e¢m” q$B·˜Ë Õ¿I¥´Êî‘œ­†WrpQçà†(ͪqˆ%´é?Ð%qžo`УMÓPøVèR&¹Pò­Ž™.‹˜àíãAÒO™å)Ãay» w°¬ø9¢#øÖµ… 5±ZíÌÜî j GžFÙü9i޲Рß% šRÞRŒü |6eèÍ%ú¤åŽÞI@‘·@sÉ¥èŸ@ÖqžNBusZ€©Oü·PÞ…÷®¨P1pÞx,  ÑÚEhbów¹íN`át~~±~,Ü:ƒ´õ¦.ÆáŠ)v¯¨Å 9ݱ›fyPˆiBt8ÖOê~é=VÔéÇ,$…Úªxûª–•wDõ‚Sü?ÄSU²®äq@ ¯^½îoÒ„ÎqPì@·È!^¼,úT¨~¢6vVJ;\œ´É9àÍo[ÍG€„s—|BÄAÊP,ñ•žL଀@M’'ÉþÍuŠfFáI…$#p>ÛQBì¹$Ù*PL@F°‘ñOõÆDb¿F€ÈêÂà@œ¢Sp`H‚@CˆA®ˆr +3]ä>zËÆq¢ @ bžÞYHq(÷Pv²žÂgК—\Ñe„´¬ÇçΨ ë ˜p¨Ãùe0ÂÇŸØu”Ï9Å¿t‘OÑäqºËÌäÙ‹áÀ. Š® éiè ­ n„¾æÄR°&ѽÑ£7Lìé>÷¼’ºyêæ(5ô©)Ž«0žGAöÄÒ|]þ>mkZ¦_ÉŒ99¼ +J@Q;g˜-±ÕAÌa;ÐfÕO‘6²„½©J`•Æ<ǬTºj„3Qè/èïB àæ8Ò+t×E÷B+=9–ˆ +צV›ˆäá(ø”átz¥â×®oMþÖPøêÕYÄB"`¬ÑNr!'‰Oƒ†—zÊôÈÄI¸<ïì÷,@ˆ-ëÚD]ôqàt°¦pÃyæ:(ýûT\ÿ¾ˆÒÖ|É(cË^fïåñÅ×úߧ›O!x?2G³ÒP%½þNž +ô¨#‹žGªº ûaÛëûóP¡v̨m­¨žÐ9sÜ̹ÕÖKè!Ü|œÊ–Äÿ'pK)IÒØZj{;N0íðtaÕ±Óyv6&»$½KÈXvÞIÍOºÐâ)´"€†J(]a¾ô/¶º' ÈÇÍA×;;ǾšßW-›¯ÉqÀ¿¤mné +9‚¯?ÉÕ„ ³Ë¥¼éÔ.ºmd90Ç$I,ÂÕÍ‹«›ÏlHmŠàŸJŠÊ¡Â4}ŠFŸËï(òÌØòwQÃù»›sdÙ~²Fm»¸°îÐÇ$ – ê‹$]± pMVÏÒ}ÊùèòȽ˰üÛRU0TÒÅ]ÅðÇ,`€ìÉFn”?‘E˜ApŽ"P'[ûaA! ú\ȇ‚OE¸_þB£ñÅ»XW®(ôft×uà@É2-‰¸aèÀIp$ À²C¾ÏÓId…ƒQiŠ:M÷ØBòMt¨~ÉTTúüi$•´ÍF9´@À—ñJ@ +ˆ€!±V–X¶]ŽñbÃŽ‘4<{‡í§{ì=#i¡4ªn‰}zÝ/~Ï5þ9=Gê ´ìì EYp‘‰Ë¸å0ŒÅ„¾—¡ ¦ÛÓÄCùÌ×ñéõ°¥›±Ö:ܾ­ +GMq¡‡_ÀzàËÙx€îCsJžv¿Ï™„˜£I½9c¹†ˆsò6ânÙÞGU° +Q¬ôŒçYˆCÀ\ \'ÉUà“,‚°¶å€ÜE°ö<˜Ê†kx2MÃôY]]˜¿]}‰e9aO­ÉN,£¡Å99 +dX=dÀµ¹1dä×3ÿ<†Ç(n¦ +…ˆ9Qå:I•Ý[òzà#=ö„Þ1z7—=4–M§.,¨àÓâ'‘l€W!ñf©ºÄÓD€ÌÌ-ê¡Ó!¹%¡…9ˆäTEÓ>N#Ä ½Œ6F4Qòü¢6FÄiðcF„ór[­3â‚Jw%ñÒª3,èÆ1 ÉÕ°O÷ÄF¬èTLª Iý’ý‚½yÑ5]¸ZÌ8è¥öo!í—ÕÄJDïàÈLŠòd€9ïµ"Q½u¶>•AáÍ÷aoˆÊ \ “2Å+\¤Q›ëØ`º_8­ *‚ú×ɺ¢èôT‘tj(¼Õõiª1 +f¿ÎCIPäè<§»zFu,Ï?9] +”åé 9ÁžT)²â‚"=[¢…=º!Wèóô”ôìo|Át 9ˆY¬ÂY6b8ªcMÅŠ–FÓ{ÆúVäÇ.â^±µXŒx²Øñnö4æ¶Ý!—+ƒÕBèˆ(r‹ê¦ÈÃNmµRÒÑÈw# 3ߺÌ˯Ú+<3 +¤u¥5€ÅiÐqWº8˜·:& _‘ìDcE`gp ëæê£îTíƒø”ýBOàÂ4ÇêÚ‡\#G ¢ 6nŽB¼WÈI€÷ÔK¶' ó†3(r ƒVš½ÝQ@½îC #¶Zê=­Ù}q…Yň ŒÓa9%… * +¤Å£JI1ñ ·a.Y »ŒÐüãªtIg»Up*$©y…9“L‚äÀÊl„ LÁë:¦Lnqr(0ã.}ŸcØw•jÐ͈ú žž¸@¸â9¡SLe”TÜ¥ýFóåè1éjÃì·]£—9ªh{MA·v”EÄ5º«±Þt¤ OÑB“ã *ò@0Ǹfí Ø(<¾G»]5æб§·Éˆùo_¢ÿddüôEŽsvâCð× ì?¼C„ÄG@;‰vwºŽ*V¹¹²<]dwCçÎ=!hqë¸C¡Y^)H¸º‘ý¶Ø^ÏÝá%Ci<`mŠ™ú‹È·î n)“\züï:NCU!Våë3h›²L]¶•A0ÎB–,ú6"ܸ¶©¸ÂQA0H¤OXùS÷n¸IA‰»hJ–kŠ»ѣƽHÂQ}ÄerŠI­f‰¥R~ÜÎS’³k˜%ââ…!çb >È­}À«qì5‰žÇHì_µ€dÞ}e]¢'õÃieäz ÄÄP8n¹úQ&jç!×ÙØ ë ËŽc‚¢»‘&•h!¡ðlƒŠ5ñvüM! ²LµŽƒ"7u}¡±5Š£ˆ@¡§Ã‘¶Š‹8ÊlHˆi61ƒ‰oˆ!”™’ ã7šloOÓ>vX\hôàŠ{î;ºÐ#ZðóÄ\z|ªíµ.üÕ\6Xjiùq~m¢Uü`1 O­öƒÃ)*ª]É•øÀ3¢ dÊ4ÿ‘¤z‰ô8ÜŽþx™«Ò4´´ÑFñÅ™1ìDBÈ“ŠŽµÔy ñù:þFÚþf¡ÑZF”dþ–$ŽŽÊAhnsÊGDtFš°fáSÿöXÁ ¹¼8;ûMðKé7¨§)é1>:§Ë˜E55H^–è ï‚êh㌭ˆÇ*¼£·Çt ‹½ ¸–¹Û]A?e? Š/’¹Áîp­c º£Q@ÆHwÚ"%ð&ã®#Ú'$ñ¾aß®E7ßG!˜Š_[a¶Šd¦u@žIú…*G#Hxq)1€ävƒ¢ÂrŠzøu:»éUrirÖËBÊQ ÒáA=ÍñK¡Î9ÎËæšÄ]ßmSOöü0üÔ@aãB¾Ü+˜Üý@ ã—× þîƒdHhƒP²¤¤Í§`„S¥V’›Rdií¶™£q-YQì*SÝGø”ÚoÛÇA€–±ñ¤púØ5ûô:'±|]† ÒØF&ÖáòÑYžžúð4ažEPˆÎw¶…WUyÚ1ÜoØOç;S!(v¡T)˜æ D·§ûTΣð¨ðz y?‰Qœr,è!¦Ã >0bE—.Â2›Š>\ÐVÇ€æìÁ_ $‡¶4‰"Þœ& e@Ã$ø?ì‚6Z%7#&MÝÏc†×QªÔ‹EÌQS4T̉€¯77à;UãÖ~Ñéw*ŠbÂZÛ&’$ÒÉ–…®¾+8;L$·£ƒdSe8äؤõYýüèÂj5)q%œåq&\_ä¶1´ôë°?€Ô²!à8º”44¤«X4¨—à-ž úv‡Lfæ g¯äáŠÍ®º-çÉæŒ]¯Èà +ךçT1Žz7 ZIBÛLOó›ÖQÌ.‚$›a¥dÑ¥&™†–vBM« Qä#ÈsÙGxÁ¡{”U£m 0×Ê ¸ ¨Ëe›w +°&GŸÁ~“‰î‹?ጷ̤ٷÃqL—Š}Œ²(ý×ãzN÷òl¤bIm¾O¨ª|I©GsðP+ŠC…"ýQ&J’‡Íµ\ŸêCÌpæíCAJ]wÏ_Ú•K!1jÿ>’pMˆûKò-;¿(y©TT86÷"üvp–—E^ØñŒ§`ÛvPs…{Œ¢dˆ’£Ën SåÓìþÞØ yŽhÝf[ mhX—¡a=ý`!´ƒPzº â¢ÎÖ: $ww7êÏEGØ‚_êt’ê’À³3«ô.F ]®_Ùû\p-ª¢„º ±ª,©•³@ÏÅ4œP¦£# JQvKúÐW,4KɘV´ÖS’Oë¿ð +ÁrLÏópá]d©ã«(vÅW˜hþD®ƒE›fC‡eËx»è—ˆƒIq Ok¡´rsÌ¥=!Ñ:¥¶Ï^Sâ0ìÎ2†Bó»H Ð5íÉ¡Sû.ç§ûˆkû.ßöGžìÀ¶<jõ?¾¢%óöó?yûËÿøþ—…·_|úÝw÷öó_þêÏ¿øþû¯ÿíç¿ú‡Ïÿì‹o¾ýü³ïþþŸ>ÿîo>ÿÓ¯¾ùþßþþ»ÿü÷þ¥÷ŸøÍ×ÿõßýÕçö/w~ô +Ž·_üæë/^NÈW~÷ûÏÿço~÷ùŸýû/¿þöûÏÿ·¯ÿÉÏ›ß~ñ«o¿¿Íÿùâ¯ÿîk¿ð_÷åã3å½Ïüùï¿þÃ7_ÿ£}äïþáÝŸ7ÆþÅwÿ¹Ý–óûï¾?‘ºç0au ß{?ä/¾±Ëúß|õýß^üãcÿ—¯¿ùíß~ÿ±+úõ×óýzIïæfÚùæëø÷å7ßýã?Ü>Î?ýÙÿôË_¥ÏÿôÛ¯Æ'ýߟ~ýÛo¾ù×o?ÿ÷ß~ûÅï¾þêmüå-þÉÏnþæUð~T9«ð¡R\J–€rÓð¾€°bù§¥šë¿Ôÿå?þÌ©ç“u^߈ÿÕþçÿeüGË×ßþìí?ýŸÇÛWþÁß(è^.aý&Áð>z=Ú>·Qóçüúî‹þèÁÛóûƒ_åñ4?ýâËÿÛVŸZ¿ùúËï7ºþñöðß*l[ f{’õ [Ôí²ý–3fÀ„ÚV_ÌK)cç#Ó ø ÔT€ÿˆÚX¡2R7Qbó–K¸€ÜN¡“ÛŽiƒûŒ“yèn§ãƒ6^ÐÓ J’y» lé"t:“GtH/Þ/êp$ páԟβðTã FOPð¨Ôyé‹é¡uY>ð´I*H©x‡T5‹ä¦¢»3iÅŽ.õŠqÞî}VÊ…¬AÃÐóµ.³ƒÈxW¾½p7éˆ +H3FPoÿA§Ië©àZäMÐÈHË}{÷®ýÕØ¢è§k@7ŒAF~ˆ…S¸'ëö"`,ÝV$½Rô4Ù¢‡ö¶È9‘òGÄR`~K丨88qz›B¹(E\ydK)wÌt_bpuÐÕ~5A>÷ðåCèöXô‘#ÕõZ>mW!ó\PÇ=ivRbñBº8“LjÚø¢Bü%ïÜØe'_]õ<!€%ˆÍ•³k­Ãp>¸c’ß”}²ô-ÍKRðÖˆf¦à~Ì)t8†Mʦ·§ nšQ‘ñ/`sÚ‘0¡©”žòe˜Cqþ„ ö®ÉíwÉ–PÔÆuӫ웎ÊNÞUÀ¹øæÊØ >Vä"y€,¨æ ø®W èäÉ¡RÞǪ¸•Yß,-pv:&­N2A0öcib3O¡9»k~å&{[ÀN{zÃêèWÈV©&LcÇ›…—¯àuqÉRÁ“°»ÚÉyPÓŠc ‘&Q®à¬rôqª_‰ƒœ½ÉZåî΀ È5±ºè±úªH³4H [nåI"L¾*Et‰6Ã9žÎd+s•yV=ØäãÕ"9G»Ú[⣽w ‰‹,8]ÒåXKª© ±® ÞQÕšû€š% ÑÞZȶIJcª…NÁò ò¶âªÿjN'ê›=–.w¶,N¼€ú¥ùµ„¨v6È,Jö¼SÈ +áK=oÚ™Ç7ˆ þI;U+®¨.cº>œiÇâÄÁ…ØnÆVQ}µD“ÆBEÜ–™>”@éÚŒM‰]_´A¥Xö&’ŠÀ̪R8¶ä={Õ#!‡|ËUÍ(z¹Ke… ±–Aõ–B „SoèÙV•¤G 5Bõ‘ÚëûoDÐYX–ãq¬ZÙø/áQ‹Ž$JSKQo0®6Uf]9½éꦱ:5éç8tzšnU’(Ú{¶3‡ñKd‘)õ8Ûˆ(ŒçÁ'®4udö?%x…¯âv¡w÷íÝ’/øô¯]±çoÿÅWßXVñ–ÊŸ¼}‚÷•¼^,fÄ+”]ÈfÝ7·ÿ¡$­Xò™Þ •±À›@“ËæÆÛ§¿µïø$&¥èÍÕ¼-+û„JvCêY”kJ›jØB£üù‰ŠÑ”"m`ªfŸ‰r»¦nãòÔHPÛ‹nS¸ qs¾ý%ëÌ' Š3Â{öèa*ÙõEœD#ö°r>þu‘-ã +†î%ëoDV¬â¼ãÛ§¬5ŸUaaس÷ðšÓ3Ix åÄôÛNGN7…ˆ'¿ M§`š1ùô'ümŸà1™«@ö F$Gl#±@Ðñ§3±?Þ>ýô>´þÅó¿ß~ñ¿÷½EÑßýþ+›Jžtýѹ¡LïϾ°Lþÿ±Áo?ÿì—¿ú·cþÅß|÷ûßù¡“[øþÕwýõç¿üÕI.ýï¾ÿ'‹Ù_¿$þÁÓ|ÍïøÌïùßšÌã~…Ãî=µ‡’\ç”q°në(P£ºr$AŠ? ÄyN(X;]D1·âÂE£B|Åò)Por0´Þv;AB‚8Š}øÛú¦Ë‘Þµèè¾s¤äBjó&i¡Ta0 Ž“‹¶ýtšƒÆûÀêˆÕ+ +ˆxn»ý8ÝK¡Œ&›ïÏ"±˜-¼@_©ÇÑܳËw—Fü?Šë aDûfµÎ,¤N,„A¬+d üR$;n¡Õ?ê®ð7õ¡êþ$\zàQáÀe·±Œ§ÁŽ!i@8’f`úŠ_‘Ý×Å€(Âó‹+Íq¡ÖÑhƒµ¬,~vÑ-6ΓŽKu<ÊæT +À»„/°}'}1;K슻;€ MÏ´¢CÍvt¸¶Ƶ é†þŸíŽ„í ÙB“ôà8]s²‰ÐO'8ÛÓLÙ9[IÄÈÊeƒIñÓ¡XN˜£ê¤rœ¦k`qý™ÜJ¹£J dA*J¤C¢fk†{:q'$Ó $ÿ¥GeäŠfö!NU»X »ÙÛ@00/ןõpã¨`KÇgíë3ÝohFæ$éng›7ûŠ¼4dýtëAO{«äQˆ¸q“½þ˜ü•³¸špfZO× OØ”!ñjž®új¿î@] t[¶´Â0‘Ø"ç蜹êÒôÀâ±åɲ.TÈÉœÁ.ɶܲÆ8cògL[I¨Dó*cFÙ<Éo€ü_²Uð$WðGNàMìµ<ž#‚Fö¿%S,_g "ID¤ä•Ñû¤ÙCº% Â&ƒçÃâЖå¬×&Aä1o2È!†k¥Š~VU Øí" §NH ×™ÅÛ·'ÃK‚d“^FÂXUîñöl‚*B8Ëq‘›–D¿S+žræd)5~@‘ðâÖ8k`GæÚ+¤Š…Ðrä$Ñ!Ì$ž2L¥iÍ4¯§c’YÖ›gP™kŸÈå±ËÏŠf&¯¥vÔwÑ·àáV +ðÝÙŠpY•… x•ª„Vó³5·ãDÍÀ¾ð uûÂg÷ˆ@†„ætTÓÎï— 6 ¾3i0kÔ`wÕ|HÌò]_JË/,¸Ú6IÑ>}CfŸKaµÑþi“)’“ƒl9PÔtWh®êÊ +ºÄ#yh‰7E³¢w›T™³£ 2ö@¦¿Mž°©®*ßØw€ûî©äébiDq±€—DqŸï?4½>À#Õ‚<¥LŒ¨õ!Å8ÆdhÍVIA0žº¡­–gz̈‚V6Ðð }þÅvqVÍÃ!fY3@z¡Ûo’—JV按ï¼ý<Ø‚e¢aPK Q“z¤‘(~¸¶:¼ìŸæÑ ~{‹D»øÙX“è§5˜ Þ +õð÷ù³Qh–.¹eÖÌ >{ƒ¯£ðس,‚€¥ÀïÉû +fÓ–y;èW[ÄÍÍôˆ[pgw¦ÓMqþ<¼)‡$¯á°Vñê,Š tî§_ÿü|IÛÁùC +E˜D…¸K^¬úå¥ó y@é¼Þ®ñ^¶¹>nr‹.OÃÓàþ ½§È|úéùx¢:!~x<êBÊ®D +ׄà+µØWȦòyîèd2ð¯ôHZJ…¢\ïCA6TŸBA°ëxFÃÞ ‰É<½t%Gç‘WìŸ%]寪ª­§øSyõëå¦x\È“A#{^Š”ž,Z°µ¡>­…µl@õâŒõ¦”Óe{ +ÆT‚=V)]y– 5²絶Q¨¸$|€æjÈ÷ËEj¤öŸÖPQ™ƒcÁ-È«<PÐ+s….Ê)áúÙšÈÝåZÕay݃(9 ¹¼Ê¼:pðsóà[ÔB©êÀÓ–S TÚÞ«ÀµQQ¾gËö ¯Òµµ穧Ä/û`‘F\TÑ¿C›g!$U½ðˆÐ¾Ûr¿Øñ¼Àl¿ìÑ… ÙEIkÇwö2£wl›ä= Œ(NQºÜ3pð§Ù¼b ¬B÷h£@lÃf¡"q€VþQþ kÈ`ðp¡Hì~W`S¤?ZäÉTöÏPȯºùUÛ+ö71eiñnù +¶ìuv —‚óÑ/<ƃ¼ðÂC?Çt±U°žlýŠ E8D¸bÇíÀsĹœq*5zêåxØi¥š‘-ëŽr/PépˆžÃ`.]Ú*àƒÐŠ¡3ϵlýçS(>ÿ6ãvÞReë‘>÷ ÃáŽË»Ês~ÀHÃÍîÎiOA­Š²dÀÓò‘… +,:R}'‰)rÄf.„´áÊèvh&QHR3S¥Â¬áñÚ…$ +Ï ?N]¿"‰§óJ˸íÃm»í'Tš™È©g¢CL¢rŸro$¢S¤p%Ef;õÊ2mnºÅÖç8ŸsS‹–Ü3á{ø)3£åS’@7dàHy›ÆTiÝçÏeÄð3ë¦uÙà®ÛŠ#šÁHÓ¡ã3mË,„ô’ÛÛæ4® K[aÔhZ¸@>ö +yñdÁÿIéÿ©î‡º$(³×Y¤ Sâº)ð*⣪Áêm'`@¬ÓsD j|dÛÑ°uœeµ% BÑ– YgñÝÎ2}ív½¼gh”"=ÍŽ‡’úUÒ¡õéjqÊÒUb3¨’Q;NÑÀ…£2m„+ 7Û\f¹‰“áJÀÉP~½ +T4ú[€XPÔx.kmŸšÅ0© +f’¿³|Æ}c7{®µq¿ØufaNTMI[úÀ‹n5½¢¦ï&õZô«knã\yõÜãWdªÔ%uù3*1‹É­»Ë«³Çn×A‘ÏbÃ|L$´,ð”H:xH„QâtRC=¥8$ ˆbýèâÕ%:X$ôê¢d%û<v9ÿt¾Ý~¼¿EUÍGÄv(>ŠÇá¬x7œÎq®êêdÝœk¸-gh5ž[C A‹_‹ªžÖŒôÕØTåÀ­¥5šáºÌî\ûͧÈâýÙ,ÌÅEWTjtxÐ5 § «½†—*6 šG'’oB¬ÔÑ„Z?ÅyÛ¿9Õ €l—ä0Ö_€Oo>½ÇÔ9Úî„äòÁÈHÐûæ†ÆúÁâå&3dˆg¥¨lyt¨èd’üÙ_Ü×çõ‰òEä>.;æ›™ÁOJøi6I5™±Î°Dë†Î>ÝËéera¡ÀÁ¾›ÛŒC Á¸s°Ý¶WC&Ðee¤‹,ØúvÅ,¡ ¬2ÂÛ_R¹Ëó ìVÌàþ²'7”\+¥ò¹<Ð}³‰w¡ Ë;×Ôší§0¿ñ1Ë_V QIeüâ˜,‹}?iDBþÂØ~[ûhN ¾B‚¶’îAúí4*·ÅVú Úå_öàu¥¦}n‰²®ö-ùpyYæEÙ"º«Gù¶ítb$G\ `aß` dùJ^v%»éà]žÄMzë~F³¹ªuDgÑ~W–§–Ò¶ƒÂŸà‚?Ñ]$wÙ|¥-ÞÜÅÞ±¸ïÝ€ÈqGÔ.ðg¯ð²õ3J*øö óß–˜[–žògЭz攢[ƒn²&#š 2f€Ÿ”ªÂÌ5J`‡Z3ž,q/cñ³-“<Ü5£MOaÓæ1Ì¥›.½#ÖlA?‡hÀŒûôu²aÚ^Ƀèç@Qýq|+*²Øä|[¼Iá¶Ì0s‰Rµöéü¬ðŠð—Wf°r*©b,{|ÌO¤ÉO<®t| ¯$œº¡Äø”£×È\þȼé[`¯J ê‹`Üû¸Ck^ Óã(*!|çÛ–RyHgßÀÆëž‘l#¾1ÿKB³Ÿg¤@à\™7€°€Ujƒ§§Œí%qû¶æìÖü5ÛG¬¹‹!›nÍáZT×T0HÓ{“ÞÏMŠ õO&'€ÿ){lЉOH¬4ç•Ý&£¼’Žú²w(ëN/Ù¯ð”íø†#.×ä™åIŒ3¤+Ú >×Ü[ÚÚehk£ì½eíŒDõ«S9Ô’ô³Ð‹kiO$ É›­d@“@3j]« €¥,ÜÉñ”¼ÔZ¬"Š[ |¸³í­Ôo’™µ@¤é†(F:"úk +gÖú +ë± + kEÆž†l«'YÑZÐa+#ò1Å´Öz8{ž4¼ÔKÁas­ªTf-?IUY˜P|:ïz«^‰”]Fë¥ÞÅá#Ñd*U-å2F-+öEŠa|ÁRm“5n‘Ây‹ªm…:Ú ý€š[UŒ½”ùl @œŸ]åsm0 I“,`r}»ª‰Øtj'¬ð»îj2ª”Ð3й‘¢n%ÌÃp¶¢§ÄÛ!OYÊ$ý«µd*å£HW§=Wº^*®PëàC­‹Êå—Z-ì1Wϲ›¡úÇZé44¹¸{ 3öZêÄœUmpC‡´Ý׳OòézVk…Z—RüR$–âuô¥¾ Õâ=&'ñm+C¥Uk-¨+ŒŒ‚ ÙýRS[=P¦#e!ZKùd‚é³?4ª/k#@v±Ä öJó‡,M±?5µ[îÛÞ} ic^gµ§¶ÆÐ_¢  ¿~—WÉ–¶zãÛËܨyدY;&Üv¹¬}̼5Ãl}Ni®Ûk—F4F!KejqÓâ-E4NDmä!ö¬@Gßûœ:û¨õ¯m)¬ºicÚ»÷çÞÍÂPe(Ø@ÿ_´4ÃÖÓì¶õBöw?hµÝ˜µù·ßܵwøÎCRÃq>Ù­1¹MŠÙ̼S³ñ¹ÌÇÑ]çñÖQ½{¶~ìöJ­Üí­ÜÁï¼Ý¯mä¹6lmçmYÙºÖ·ËÓÖôÞ–¹­_¾-•£É~·Înùm¹ÞzúÛŠ¿AnwŽ Q°n@;al];‚ánçÛëºC'ÖÍw^<ïÝ;\cÝú7¤Ç>l@‘Û0dÙláÌD¦Ìhƒ±ÜÆOf‹Ã6ü̈à6ÐÍmü·av¶8r¢|fä¹A‚¶Àõ³;@ÑŒ{7Ò6ÿA—µ ˜¶ð{➶È}CKÝ%ÔjË#6”Ö–‹l ¯ÛœfÈm¹Ñ/Ûò«}Ä]ž¶ZÒ½‰†ÛÅBw›o¾‚ﶤuÃìý7yóͨ%íÞÀ†[ƾ oóþ 渟eEHn5ˆiyWËptæ¬l έt²a@oK0„t+ålèÓ­tWŸŠHÒu«Am Ù­Œµalo«`Dw«¦màÞ­"·ƒ„ï*{Æx­nàäYPÜ ÍwåÈ =‹™„z«‚NàõmuEkoµØ ïÞʸ(ü®¼!Êg)y… oEè ÀþTÂÞPï[|ÌoEô o[Œßàú[ICúÏnÀƸm&ì_›3akllĆÛÉÆ‹Øú+;·bmÍì»O%ÖEÔÏr€ä½­¥U¤_Mt̯Nzé–nQ•1ŒÜ¤×é¦kUÉ)›-t¶Ìy´¾v¿,|ù@è=ˆ^óÖ@kô4èFØõ+Ü7â*Â'èðCk {;O1¤ éY”‘òÞ´‘#ZeY§%³¶ý!%÷Ž±`žñ¹u(k÷`½ãº‰|ÎÚçTds1Ó(+ç²÷K› +k1åKV«m1îgb•¨çíþ©µ¿»óÚ#Þ~ÀÚjþòö6¬-ëýv®­o¡Ç×Çâ}óõi®íö}NÜµí·©µvÿ·Ù9Ûä~†lïÆ)ì¯ÕŠoø8…ŠëvzÚñªF—ØÁMŒÆ|äâ[I–„ Ô^=YZ ÁH±s“E¶àí<Tdÿø-Ûð$ zu ߈֙Lj—úÝèT¨ÍC >îÎÓå¡K Å%D¨­Œ +㧹úŒ¾ÝG¬lźؾÁ€Ì^+qÌ|s½ëŸ£ëïÞγÝ;b!èð˜ŸÑ^Þ¹õçôÛŸ °1ÎfÔä`Í|ÄUÒBxÔŸ† È6*«.NlœÅ¾ßêRÀ&nêUdûú$:¦m²ûßýtjN>ð63î¾w±\ÿ—³xô2JÒârå<D0¸©4ÙíDkÂFIs hÞÞî×h· É£d´f-©Aj‰êÇÝœg½—ûµ|ì™|ùú c~y±hŠ$a)0Ø“´Ø29ðßH¸®ÚBs(ä['í‘"{'ªÙå‰ +Ý>‘Ï6 ’ôu”|[0åÁví¥Hߤ/@Ï,¾ÂvöO/ïç~뫽ÿ’»%b¿#Ûy¶»º.WÏÏa}ËÖù‡XÉãåZ&`qâX¬2>¸ž§›— +H;Ÿ¸}—ŸZ¦Õã{Öù6®îËÛWŸ5bÖ>Ó?¸õÝöÊc}5a û{6?µÞ¢ë{Þ¹wó% A2‰:JyÝdbU”?ÏvTÑ ‘(âª4IÀ“EÎBI¤]½Íì~Ú+Öe3á<ªëÌK'è4žã6 h‘²l,ºÉ˜i4±†Y ÒN!±7Ì”]Bx'N<ªc ,Œx1Ê"Z|ñGIÊø±&G»]v‹‚kOõŠ +ÌJ-GÁS‘"·‡H±Ï]|ÿáȼd®yÞAêd†¤ÉÚId% &F*£ú0°^È­Ôìi½Ap œà‚Ëë=ÁN¥V©Òâ±»f2žX{¦†rÍ9ZétÂ5 R¦?½§['  +ÉÊà4=”:{‰#aA矨k‡nU­k£?r3ŸeƒàcGöÑDR·a^"±ýÕ_'ñ‹}œŠ%ž`ëË€ÊE~Þå]’[x—í5<‚W}Z¦üôㆭ°À–8Ö{?¯¿ûø¸íÅËjqJö&ŽFüm¢×¥êñ¡e•{|ͺHÞ^ܶخ¿m[°çm™Ëüó=Ü7‡õìÌú÷ênìÞ:›öMs‘ûæ{›o›øú~lqÀúŠí?úîUÝÃ’õM?¢y/bi‘ôÊ1¤&?ýÛŸÝë õô'cHøJB½°l¦¾Í–è^ÿøI“´å´5C I¨éà`š[©QºšÎ“¼Í¿zG‡õ¿ ›—}'x³ßè?.iS$4ˆ‚a>Xƒw9>eЋ“¶I*'!D%:žÉPà¿a_$Ü +÷Èè±suªÂ6×m ‘H^=„ÂMà¬8æ.½¥¶£R %P\-85‡­€´/Fç?ï˜pìH'¼±¯`‡r$«¬ÄÈÐD^ F,¬µs£à‡#ðR9˜,<I´^µe˜wFœa\r¬ï¬Ž_G* ¾Æ>.G,MYGöüµu LÒz§š&›¶ýœÞ+Òˆ¶ zhFœ&|EZc,Ö Âš]¦?¨'€L€“ÚÐ^,¸ÌóSTÝî”ñ½=‹†-ªÝÖÊ"˜%}©¢›ýbå\;Íã›ÐFë§ä$›¤ Ù¯Š°šÂp£i9™çœ8LqÁô:ZðŠë9Âzi~MOWZSÊ‹ÃøT<ꕹµʘq­“Þb·©pKìf©Cž×NNWRb£Ë蟖ØávVTº‹Õ~ÞA¢ çZØŒ“AÅ}ôíøìg?@]Íî›ýœãíD©š†EJŸ\ J¡Y‰Cn¾T;PXÖÿc¹Ç„á¨ØˆâuJc­A¡N|@ä‚õøTªwT­”€{™Ê'Ÿ~CÉïŒ +i[’jã“ïW'g§úÒ¨PÛOOçý>4 éâõãÓÓb ›Þ®ªöø(p_ }? `ŒO[çSeþçü@ÛÔøìŒÀW#Óz«ýWø…|aÃ/šnTÉ;ü(«}l–üÿ@Zínrÿ‡$Âi±Ÿ­?Q*î¶6X>stream +ƼŽŸè©$%z²W$Ñ“Òƒj˜·KП$¯µÆ *Rt€rÄt¨^“gÚ 1¾ é|„Þ’£UŠyˆðÉ‹ÛTÛ;t¨\“§’ ¸À›Pyôñr¦¨zÑ©ù0‹n¦µG­‡Ê(¹ôQt­šp‰}K;7—4XF€`«k$¿œå »=$(Y°7?üI°ËcW²_APÖ ”2Ô;¿ã —}ÛÑ7X± ÐQl»}¶ [¾yBÝòëͧCC•_žv÷è Ô‹~d”sóø»ü²ÞîvË;Ó22‡bۖ캶éˆz*D X¨QωL˜"ñÝ¢.¼TŽo>ÔÕ¹;qoÒËZ¿Ø² HdÍ+ëìÙÐÁœ”½íF(+A¿øìâê—{I†’Õ´ÔG—²<ˆ€ÿ ׊ïè9¡íy’×ØË­.ÀéÕé׉€AˆŸ&“ŸÝ̧à\÷ΫŠµMK@™t¦É HïSû %ÅO˜ï*~`Ñ}nÙ›Ñdýú†íçY_ÒýZÖwýþ7­+Å~oÖÕf»½ëZuû”¶5o{ØÛº¹Í—mý½›wû:¾Îß›`yÖ}cö\~”‡ÈpAN4b…Ž@q¶'¾Þ¢6ñ¹GÞ dÎsWÔ¦Œº‰h#ºN•ÂB^ vQŠ¾Ñf”Úú™yÛÃ.¢MØ«a¬¹ŠhÛç²DmºIŒùFD›ü&5ùƒì7mdíåZœ²Ê»ˆ¶. +{Ò;Ü‹hÛ¥¹-r’ŠvßE´#–;ÔVíìˆ{k÷’ÙüLÜÍù™ö·¼‰fÇR~»jeG `<çdó°„{Íì ¬ÔbJ²±]3›Úð|{ï1úM»fvltÛ‹Tv´«Q~`Zh«U*›óZô{p^‚á] Ûâ/ÁÊ°á‡~¯•ÍÝ#]¶ó Ùv­l¾ÉÏ,=ìRÙÝ)2,îåfv,c¥J@šÚ.™-åz¥Åx’æÉì¿z?(»û¸¶ðÌõ\;9€œáò íl\ãÀV'’F)>—ƒâ åÖ¡øP§ì›¢}Ú©Ý”ÝûÑãC<Ö Ö;Êô€yvÙ´Æð“úÄæhoj”˜ôöu/@¸öÚ¨ ‡^b²ì´eYiij#åOÈ5dyR϶e(ãõ‹VÓüu%‹ aï¼=܇zvÜx$y?ÕÃwŒÙÉŒÀ‚òR϶"çã9ÞO#Ÿý/o¯+Æ÷?"-ê{¨7(«¸ý<+Ñ곃¿a%è"˜-í>;bÅ„W{U¤Ì9AâõÝgª‹¶=6—ÆVŸV–HÔlíO>;"Ä` +¯Ä£.\Æ´cà³ÝíDyIsh!gÝvÞ½Óh‡%ÔXÜ@÷Îh‡:©—Õ¨¨¨D»íD’›‚ƒ<=<ø]«Óh' Å'”‚ +øÎi'Š ŠË¶T¼E7§(ÌÄ1² +Ê‹Ó )¿)–‹,gwN;2‰Q-‘Ÿ +puÚ£*PQµí<Ôº[äÀ ã âòÒå×rç´£Y*[›‰¾îÎÓÅ?åe‹«Ë°Úáùfv»¢%ßîO®á<v‡ r¢o¯Æ:©ìÙ4ŒZ…¼5Ö!2Ò·ÚC;E1[|u€ÙY§†ªË]luì²ÔrŘ³â¥zk«ºý8nluà2Œµ’E©Ümux³Êƶ}åñÐ6[~ðÿÍN‡ª3&¯Taív:}=ffÁÅûô¶Ü†µè¶ ³œ­ûš.;nDýfë¾p/)²Û½dÁ¬/ݲ§$56ÃÕ ÁlqhrN{F$›CŒH)î¾ ƒ]tã½gÓ-í]ó § æ»fåÒ…!°Î¢#uÌ…ÚKFË°~e·ªïMίÖh!Œ-{¢Bб¨ÓžÄ}&̶ƒHºö`œ‰–m<З ^Õ2ų3æp߃Dßd?IféÁhúCÂ)˜0žaïÁðEJ*,ʉºc{Æ™ÑÜ[&Ñ«š=RAù‹˜Œg¿z0¼ÞþrB:÷=6¦eÛ}@íÚ{0pwû819¡øµõ`>þjü³{0?¬ýC:0Þ.8F»àÜÛÄ’ +‹ÏÖa'.¸mÀŒ”áÈ]íƒö #Ll|Û2¾Ë?îÛlBÂNÏèÙ/X\GÜúCz/?þ§Yn¸´^Fþ§ÿm|Ý—ÆË'ž6ÿÜÆËæbºCí-=q<¿w±Q@“„&§U[vÅË(ÌVåƒò~‰e* +#¯¶eù¬eMÖoB穾I ‡’ÅpØW¶K¯“í›Çþ?sùÕ¼fûÔÔ%pôõÄ‚—ã —š(ëÕ¡OØ:ŸÎò¢AÊé<ò·°í™©[J0 0k³}˜×(¼aO""„ëõ¬Ê†ŸLk¸SdÚr‡fõP_UlGÙŠÊh‡ñ÷$Ùª°®R|™,^:¯¬Õúw˜ —0,Å*{_¨¿Áñ|Q“Õ6B9X.«- Z‹G½¾–EÎ~¸Øðümû§ø]lõ~u±!áÐ…:@ þ5äqm¼¤)ˆ,˜~¸Ø@§÷èÐîè ³ïN…W.ÆN._÷oÁ}çûÃÄFöî"˜>ô c‰7±s‡&y© +³çÉki¨{ |s9Ø3-P•uì0Vàva•l·K‹´*òAB"€OQÌbþL“^K¶ÎgGæ}Ì9»×ß¾RDrûÊŠÒëtžˆÃžØÏhÄ>ÛUø¶FRζV&×Á«…ŸØ6ŠùiErµö>>{i¨+¢×¨GšmÓ€ƒÄYCB‹iØ¡FWsÃÖºóÕÀ†þ 6åî$Ó$€EÌÄ3¸Ó9v"k(Û#U² äÅ„D¯¸R ÛªHï‡s‰Î¦¿ÃÑ’Úœ;°ÆJ—ÆÂP˙ʓ;Šê^¾eXض ?•-Mž,A+ƒøñ€£Û‹oKP3 î`77–BqÆuLšë˜ØTºÜa¶Ïž2ÛA¿lusª Jä"R’ûÖ¸'{žìõ67ôu¼$€x]š9Þ^VÐÂ0›ýüëŸ_®i=xý‹qÎÈŦà+Äøé,H¨ C4ñõ~·®Z^ÑÒÃç$GGr²Jëás#ê êO!=?IT~t.„*(ÒͧæD¨XrÉcº .@`ƒº€m/SÌN†À'#¾zLLFõø˜ÁDcúÙˆa™÷.ät½$¨Skpƒ†’¯— +±1[Ôc!Þó›{|ý|kµkž×ÛMå ÉóRГ?à¹n€]@— ì‚݇cÊiÝÖóu}âªxrR@n×Z†Ms>>5Š’(f>[|ñö°ºN?0‹‰ÒÃ6Œ-¹¾8Œ±¯«†0ÝÈì6ž%¾Ún^Îü]vhô¹ýßî›ÆNm_òâ­V> á9w¯&¥óÚåœö—ñ”4Õµy†%ÓA“ÒúÜna¥R{¶µ‘Nö‹[ Yš +Ó®­t¹Å¨¢WØn›_¼bND7DÓõÈÁC×Õ^Ã{ʶ© ¡’eò7b˜PÅÛR¯õôÄ(!^Œæ¶aO¡äTŠg\EpÎÂo/ªÍŠøˆÄÖ/aÛzðŠñˆ4vXÝl#~ƒ2ø ÐÆlɯ60â?ì^¸¯Z×éêÇßÿûsL»¼"a5h®#]õꊑ?ªØð”LêkÀ“72P4 G| +D…Ý*´ôS`?´ŠîÓ˜½ìŒØ)íÐ+›€¬¹rÐSmüã(ãñ&òýW?½àA/¸PCWªÃí§¯ÅzMýõJŽ€¦i}=AZ¼c ÊÅ b RG=¼™™¥£Ô;37Ò@êàÏiž=Pš3<¥QfÆHv©¿¡ Äå.Ýô¨7_I*Ô`¨#™íRÐŒ/‰/e.¾çòv!Ñ?âÃÒ%û"ðœ{JªDÓÀ¥}ùþr£µ¬AB[ÏŽµÀ¹0÷†K¼ + +šçí¥æ`KxäIÎEËÂ4].¼mÌöRõhÈ›åz•H“¥L3k)Ù—>×]æ‡Ã°mÜ磚£ßÐó«o±] «Ü,ÙÐãÜ +J¬‹ê“6kå *ÔÌÌMZ„kËéa˜‚Û<ùþ‹O +ßö(€ndQ’@6@#È`)WMíOÓø†‹z5w~'p–”Êm6'4Íj|ÇÝ$À‡ë»`ò'!Û­›ãÓv ›ž%ó|›UɼŠ;‡’ëÚÙŸëßù‘Œ[³»Ì{¹ª]OàÖsd<¶ÝiÄïn02æ­BÓ˜7›¾Ó5ÑVi¨}žÞIL]Ó|S¨ïÃ.n5^ž{m¬ñ¦mÒZó•ÜT¹Æë{«éu}h(Í¥`×ëÆ­ôØXc6á²¹mšgcÝzÑIó•mW+à¥Æ6VÉ[C±¢npséÝí;Æ:}ïÚ1õM¦n®þSØnîwjxs3™zs³Y5÷ö­êNºoîo›òßØ7ÍÀ¹gÞJÎ vS,ܶäMõpßÚŸD·€`“Z\C‰M­q DnU·8fSÜB )«>wêê£@´ ²ÏBҦ徕Ÿn5á·âÕ¦-?Ë]S~«ÝjÙÏŠÚ&…¿ÕàV5ýµ€w'É¿ÿVaÿ½l¸¬Ç[3­^¹yl¥Îic°Ho½¶òêæ¡°—mW†}ÄŸÃ>jýÁ[Ay¡-…è[‡Š­Ž½f+ŽoW²¸ýEÛ¨íάuûýî®eÿwžÒÒ5¸òÒfاÇÚ¢¸Ÿf[‡c¬kkd›îkcåö¥YÛ2Û›·6tö—wíÝ¿ûK7éZ9ÖîÓ¾Ö,½«û%kí|mKßÖ4ÛVÍ­çv·än»mÁÞš}ÛZ?[„÷[ÅÚXÜvœÙ‰;ÔÞ¶¼Û×ö®çº-îýÒuGݺ­ÏÛðÕš]÷ì­™»m÷[/ø6XØZÉ[¨±u¡gp²õ®oCš­ó½ED[Ó|„Pk£ý6ìÚÚô[жuøg”·ánm,6TÁc®X„[‹ Ê°º+ +b‹‘7Å]„½!0¶ð|oì!þŠý¸OVèÈžr °Éž¤Ì·9Î<¸fH+îeO²VØÌ}²¶‚nö¤o…ëüwyç>jÍ_˜Ñž¯ ¥ûTzÅ8íçYáQ{Z¿Á¬nË+Jë**¬¸®½±ÂÂîë+ªl¯¬€´½Ä²âÚž 4·•u6ÔÜ^X»Û‚Ò†ÐÛêQ+´o+díÁ»:Ø0\Ëi2qÖß6<ã]ÑnCCÎJ߆ŸÜjƒ+úò¶²¸B7·ºäúÜJšfô® ºNgu"S·rë†g}ªÑn Ø­®»ág·’ð¿½-(oèÝ­½g{ƒ ßÖ½÷¯Uó‰OÞêìªù¶L¿¢·*ÿ¬^;@û®¿@–Ò ”dÖw©“.} +ýXc~l¾ñKW`¥{@I‰÷ý‘(Y8Þ87õ‹¨®¢x¶.æ½1i1"n쟶þm[μdUªßgÚ»B +$ᜭñÚSÖ†q”‡¬÷´›vÔ øÔ«ZzXâ©í~cYÁ¯äÄvyEZ[ß,ª®u^ ¶HZX&™æéÕ©â¼6íöïYû}×eÞS\¿mí-î·æÎb»ÅÞÎÜŸÍÚýÜŸñ]ót›*këu›m³W»MÖçÆî:ÕgxIÖ®ñÇÙ\öjHÁB¥ZõïxhNq£=fÏ–Ô—C +˜ƒhÀà rx^Uây Q;QÌƽÚ<%‘ĺŸäí<9È„Ñ‚ù:íÊ÷Q;½M4‹ëö:E÷/¢ûû¬Âýû¹3ØoÈvží¦®fï>“Çýe8æÝT<.þ{5Ê%¶¿ßúR¼ÿiw]§ÔhÅß|ï6âÆÃaµzA°Ï¥¨øpõÝOb¿Æ;_ +"ÞÖ%: éßgÜÂýÞ¹ñ_¾<àWå|u¢ÀDØEÚVw’Ž7öÍ´“´zïØM0QZ¿q« Ímç¹Duëq­kðê%1O¿¹ÃÌ‹º7—?e=xýøuYïÔ:Ï×9ñ䱜Û,˜®Ûg¯ˆ?ò)æÓ9ðä½pým5ÜL1·vØßµ'Cˆ› >?µÞ‡ë{Þ¹As>RM¨ñÕ‚JˆWhc÷ÇŒÒXmJ¿[-7Zî¤ôU[íA\î5áɶy +ÎàÊSVIy¢oÏUm›õÜeéÉfà nQåQa²wHĆU_ZE¤.J¼¸…«¾¾§w§§w©¿ãÿ°ÿàUïŸo’J`yì­VžºãRpPžÊ÷–\η:Ż؞}wn7®*©£k™zPIëÎ=QåäÔÈ mæ ŠóUÛwÞÝ·aŸ¾wþŒRWEÍ7þÜ• +k¶K çݵPÃMÍš¯ÚÈn‰!d:7·ÿäìwÓ¿y»½à%t[îËïB0JUc=õxó E®·ÈÞ}Ÿ'È PÎ&ezÇBg-7s5%°´úЂ°Í÷H8ÍõÙj Y‚Û÷Æî¡„îpé븿vvÙª;Ux˽޼ºë½]8oÓ¯‹­¦›…DöehØç§5î¼:Æ9UÔŠž•ÛßÿÔú}«RüvÕw‚óû_…ë÷¸jßïâ.…ØèªÅ?§Âªâ¿N£;/€íÇ®–Ûl^] ¶—âÙÔ`{§¦Âö6®Þ ûK}çÁ°-ÓÂa[W¦éö,=9El‹Úõ¡u=¼¾f[No¯r[–·_;Vôëö¬ûÀËMݶ‘í™l[ÑöX·-ívvl[ã6Ƕíu›sw¾ÜÛæ¾½$[€°½hÛ½}a·€e{ïßu–ÀˆååÿkcˆJý5£Ü)ÿcˆg ñ/ Ïͼo MÛnáE[âjî%Ë_¡’–ê”7¦P-AWNíY„‘]•íÌG~G§;H´¢½ÈáoBÍAù\Ø5ž¥À>zäòª-=΢eÅÁynµpj‚Ô ô>#£)ôôš5ŸQÙÊ–˜æMœWMl²©w齿åÞƒÍ'Qn2,IeÙÚMíîãiÜöd/ëñvÙÕ±j²‰óíÏHc³K²´yÜ +Ú‡mRÄóæ¦Q×M¾f:û/÷œ²”R=*ìåæ‰I2ˆÈ,k¾yØÕb¦C¶E?½£Ê@Ñ®±sØT»QåV›ðä6 ±9mQClPÈ‘ðêFyó¡E‘xÿâUÕxÿwêÈûXU–·{¹ +5ïOâNðy¢‹nô>Véé}FÝIXïs•ÂÞ'÷*§½¿w²ÜÛ+¶Ÿf}I÷KYßõûŸ4ˆíŽlkËzS·•éöÙÌ…m{´Ûš¸ÍŽmm½eÛ½ÍÖ}u_gü{›Ã,jý×ãÞý^ +ˆ=´ˆK¢ö½’K.Û( ~- +¹qCj Òtµ–hbr°¹!Á†Cä8ªÉ3˜‚Ó©Ù³øâ¦{¿ñEâÐiÇ؆¡í€™sd Ó>nõEªÐòãRE@éßb²ÂÝ©žþ RÀ…z÷m¾H±I¶ëOH§m¾H¥y /bëw¾H…¹AψÂDyÏ©Ìo*¨NÖ_$âbö {º QÄÝ©‚¬hQÇÈ×7m¾HDà–FÜš©r3FBÖÌf¯}˜ŒtcŒ„"7¨ Tm˜›5HjÁÉRIš »Gé…Ë(¼Ü˜%Ÿ§`lw_i}Óæ–ÄyŠ­œÑÝÝ-Izéts‡Ë[Òúö|ö³ÿ2·$I$ÇÃ%’‘é2«¼c–Ôeék·+Àf?²k<¯Æ>Ÿ¬s·^IîÜ6¨ÀÚÌ’x³%¿|”“P¶(øÖIè¡¥~¦>ÁãðdÍ°KHɲº[·$uQï·µý‹hõÿ‰rK’[#‹zGz¹nnI?ÙO¼Ü’ìÒíè’Òþù'É·,Ëf~ñï,súö·o?ÿôÓ_~ùåþÝo¾ûþ ÆúúþÃD·ß›Pÿ ËûýË«l Å©/u[¹qJ¢Â +9âi}Ó´ŒBRÐa FÔ¼`ÕúÙ…ûíXSµšÐQF=5‚ +nGþŸJb¢ pψbK’¯`"ë ¢Ëp°KË2– –²¾ž}3k¼)åñ´ëîîÉ-|5@v˜bv–à˜ôˆ@,ºÔµrâ¬i!V²HåpÛP¥øñt wèf{‰XtœÖ×Õ¡·ÞÅ©¹]‚‹æb…b—ÙåXQ!¥l;A–v~¿€%çÚÊ*˜¡d7B–Ç¡ ÂÆÆèû³bÿn¥<Ös›GDGƒ®4‰úT?)Ê€-éràk‹1’ã½ì<\J“p<Á©_°%,\0ûµ¨ãîX.‰Þ î5ö­ÊÑ•ÛÝ#>µí6Ê + +¢• +pôKƒ BãÄ(ÔnO QˆÔ´SW|þlNM‚©Ê„þôˆ¨³%" +`nÄT FXŠÊ³f&àaíó7;WáOöÿeï]w|I®+¿'èw¨/FôQÆ53?Ší±Gv{,hÙ‚ar«G¢1M +EAoïý[;"«*vÏ!§%QcFþOVf\÷}¯…èdv‘È~ÁÅÒ¢]È„bçÀ¢·:]µE¬Îª!%.ª +-ŠœeÅ‘í…µhxmupkó¢6c +¡S+±6œÅ'ß4‰e”%Meº•€gxÙ9?ðj ©þX>ª¤ZÝVþÎ*’N¢¥–q@+R¢ö⢶—Œµé„[3±Mƒ¼W…(´£S,•E'0¦bF>EDµõÿÀûL©¯Y˜vKm¥¯šLÚÜFÒu&Á“Ų¦X÷åó×czµS“h×ü2kTÛOæiÑR q9°fAœ-˜Æ,à,,›YdUà]lõDˆNÊkmʤ˜í¥ðÞ{àRÅÃåw ¶êô4ÔûµÙ +/ ÛDA¾»²^ŸùR?]ÎÔ ÷îî#¤JA>€G«jÊ6#·ÃTcJÅND;p4Ú¯[.·ßM<Üø½;ã Vx¢¼ÌQAÍO’ƒchQB1áÂòO¦ESȃd;ž’Ì™A_½§›_u¼f>~"µo“ωCÞ×¾Qb4››lËj>ˆÖ™ò¹ñ~ùT¶0!ø¾†¾# f+l öõ‡,epå£0絟'à9BÞÀ]¼L£ü"«4Úæ÷IPÄ™Ê9pz.ïÀ€HˆÎ‰r ™bw¯R‚Ü©PÍöì“ÙWS¿CMuZB¹&è ÔSHÛ›T³m-hkxEŽ6¢ÀŸÚèf¥è(Ù²SoÑ•<és ÅÜÎp]à¯fÙ mO”ËsY餕Âhí—ì‹ÝÑNšLÚ9Ø”$VÛüC¼Û¾¥ªÉŽÃUAú°õ$EfŸwqfþYwÀ-šÛ.5„ض5µºÀçtfðràâ‘Ò?á»ÀZµ«Lß¾DIFñ{„òM4 ¨ª§¨ 1s©ïÌA7ÁÃd©âîpêÄÉÑ麘¤Ø·Rfo·uÿ€É¬¬S‰ç þˆ#ÂNbFÜ(%Æ `b©ž¤cmÛºæŠÁasµùåÙ8\•k½H?Ùí·§èªáBš63‰ùÒA °ï'×ùf¨2*2ço[6,6x§"Kþz±¥Ûݶõ7%œ%)hYcÁ ²Ù{ú WƒŠÑ–G[ô|¸º ÿâÿ]gËô´.LM˜çî-—o™ÙŒæ˲±§)'[h“õEñ ÊíøÁ’qŒäÑæ!Btæ£Ð(y*v:«_¥7*þ«Í¥Pùi6og×.)$ú˳÷v/%ö胺iÄö·C(*³ Drj½Š€óøäžuó„*hŸA}÷qÃSv]¥RÅ&œ5ªRÙÜDmë0Žû©Ù9;e×W’`²CmGPÿ±évöuò .Ã+j‡£IÐu»ù·cY_`žÐÛVTÊŠÞæ’›DêU úö OÜ0¬9­Æ…&ó[8J¶ZÊ0ÉW¸ÔØÚßéÚ8{Ø´µƒ'‡@ѱ‡sã¡Ò,ìÆÕ¤3¼Ll+RhY4e õ‰=Õ@†Â'~h_"\o–'H€vWo}é¦m“B-³Y¦Ð­žoÓ ËÙm©h™ì³´ÕÔ,+ëù Gæÿ¨¹‘xHbÕ ï¢žÐžSµŸGç'"ÚN*ÓÌ8ôÒ_ÛÉ@+‰Têʪ޶¥Ÿp 8g´ç sèR3ƒÓ^dKÙ¹´©˜½ÝçޞĘ +@÷| ªÖÃþ<+MwÕ)(q•c›Å™éE6ѻкl²¦ŸOBƒ4ê˜üä‰6¡ÎPáJâgBìÛÝ °ÂNtz¿QçÀzÃñ‘ÁF,›yKYVŒ¢\P‹Qw‘OSa„ÛlÖço°ùV)a §]àÕÔ%a†³¹lÍõÂ^´ä¡7ÑmÙw‡Ö(øþx‹=qy+#Tî2´4ØeÔqƒð.¢R…Té2Z„QÚ/5íÐ>ó'9®¬¸­Ë{™\P®ÝW÷ àÐÓ¥eæíÀ’¸„!`_²M¡AxzU¥ŠcdkPue®g»É¸>?ðak×8~ÐÑÍ ³GVºydDí 6²p]\Döy3Š@ÙqEÎG‘:ʪ–×ícørá³h©’/Fp·öUãžéFM—è8‰Ç_ rŽÍ»vŦ§Y=þ +at™˜:ÇRCi²ý6µg]JÝÈ—Ø*XkNaC«¸ªÎ"„ÞÊt¦gëÈÅÛÇK2ÒÌ"©ž@%û„N-+m®:[Í×éïÇqu¯P¸¼Ãì%<æê-‘”´Á(Ä'ÅRWA˜À»m/t©Ú*x¼lýì”âNÞæ³Ë®†Jù„'®ê=£ð±£ã#>µNzØû0iJqe +1ðRÕ–i¦K®uîor[ÞÛ“è~LTdH] QFAJxb7¥øÔº4°ï’´²¡à—¸¼+aÓpþƒÙnÓš8 ÛM„ÖºÖoÎ-¿Y ¶'ćÛÓÕhÏÓû©I›ÃÙà<ÕX½›SÍd§7÷ó¹¿úÐÑô!SÄ›»Åîeb[ÄhÐ]n§ÙcIÍRÐj{Ç/`. +ÎP§5Ê +REþåpÒFÜ4úEmF&VðÆöb 2Fúž.ó¶(vŠâO±¢„‰Ó&VÎ(B›(â×aIպń„d £7vr¼ÑK­òš$¥©]Ij}7K:²­Ñ|9ŠÌ÷D¹®šˆà$ è‹d2”A›1Æ3ÄüôƒªÙ­V´18‹ÛªTÐ ¶hv‚iMlQ17øÒÌ1挫ùx§ß¶š½ËüPÏÛj&˜½çÜÏ' HÅjj0F’^À_tbc²4ÊÚà.7¡`’錦1ON²=AÙA‰æá#/zNÂ}Ý[_¼'‹ò¥D+®á8*4xAÞW£%Èîç%°õ¶%Iµ–˜9»*ñÅ2mÓY·U0Z·å Ðº2t¼Õ8öwz$JÖ‹XÇjd7ZÔèì"Èõ_kÄU#ͱµ¢ÑÏ-%sÑ)nNCt|֧Ϛ6þ×Á»@yÁyLuã»U§ƒ ´èÿ HmE{¢êÖ‚-nN<;‚Öˆ>`u‚Ù9Eÿ->±óãS«?Ix–’’ÁRÁ'Eâ{²d<§iãÑ2]±¹#RIM¬n1g@qÄžøóo« £[†ÌèêB,€ ¢Â˜ìõÉ9ÜÆ5*ÙF}PË»yÀ¹¡†(ðßD7¨28ûTœýƒ(‰ùª£Ò Ji‡h ‘t](Âæašt:‡,Jn¶áž¦Ô­Âá'u·1lòÿkäI!{Ù]€IûKŒCà oÂ_dÚ(ãÓðH<Ęj‚æ®åÇV*ÛÁæ ¢ˆ˜`Í­4wBðüã¡,Áàvu„=7“uÜÄ“Qļ|Š¬ÈÜkšd†–i"NûBØ(Eðl D°Å(¸›Dgw“èjû`:É@svêIêbAyò|ž§%ðÛÊ ç£ÞÕã}àmSdýœÀµ'“ÀK¹:¼´Ð¢’$áäÕwZñJÛ¥2â[< ‚7UÊâtÓ)&PX{È"¶ùŸºü5{£t£®ŽítÏ›B³èvck“Ë]I>ìS<Π®Ñi]ÎQLz ç´uÉ-k…üu·Èþ˜=MÙâÐÒØÄO»òrùú€Á©‚¥žÏô2+ücÆÆ…àu[ +·Yyxtx3ZoR;TmV2$¤`N¼1Îw'ÑGÏ¿Ä’K‰z¹IbïÉÙ¯xs¸„`:Ìe›Íͤ™twwdé°¨À5Á3GEKVqgÐ)¦ÝL·>BÃDÅþú’Ž›}I¥íPý¹]h*YH}±9èdÛJ—9T*°ÎQ1 ï0/ñ€ÐKñZ=âX7jÛm<ÝEYB¾JÅe[¢Ó$(H~‡j]Ä÷(¥‰Và-/q,×éý]‡iOZZ✮[/ßm—ä>ÜŸ4»Ó5uXZ3ÑͪãT½*xwHg·} +‰`;w™’E$=nÅÉzZ˜ ÖìAA‡e=tD°:ŽÃKØÂÁ­0ª¡x“ë%d=÷UnœƒKåußM©ðööTBV”>‚mرßÖ xU‡°°ÙÙ,Úæ >¬ó@–é¹ÇïåBm X ×Ë$fž’vá(ØòôGüTÚþl—JækWéU)é°cO3 …QzòÞ7B³BwÁ º œ}/6+Q4Œþ@Ó²Ê튶ݷ5yé_©Ù&=‘O˜ŽÛª9*p0ÈöUéØEWüá‚šˆÚ ¼*+6p¼ÏÒêã¾S‚•–ùl®594ŠÞéÑ +6U”æÎU +Z¸ªiïÐLÓ©A™W*²î/^ž½vµ̳Bk¢ +¢Þ ?X32´Ú"5Ë•£QZ1Jñ׉¶QåF©Û´ºN'-&§›«5\ú*(̦„¢mlj“E +y +pr`š# õ„ÛjÞs* Ã8dÓ™¢›ÀT醹À„Cú›Q¸óÎݨB3ãššÀ=Td½¸,€Ï&œQs +®ÏMç$s2m5ý‘ÕƒÒÊ› gå±_‚VBu±ò…¢æèÍ…'¾žÏ;¯0¾g8”l-uÄ´v˜‚»ÞüCWÍÏåÍïÝÐJàÅ.Áu6—àôÆ'VßÙN¿ÀiÕX‘k{výŽ0”º£‹ øñUèðØÚ*‡L›'¤.˜¸÷>Àm¥óÛZH>†Då^4ZLŸåH+IÒ ´½é¼­¤3AlÈÓ€‹~ Až01Uщ•C5¾‹Žœƒ’‹ñ¥5äS©š$"·F‹ª9ÔØ (ˆœ{ŠQ§J› G“ºD²Ïö¿‹^U˜<xZ£`|Iêó¢[ñôqˆ¦U2vU¥2T‘”—‘39úIrÀ.85¹1²WAÆóÔ$íÿcÄk„§¤¹@ÂÅÚQF;›ò}à1§q>F+k'~õXïbµ]DUÍÈ5^ÊfP£À‰(Ä]yBp«ÔI¢·Æo+] Ø!—:^B¸Òhbgë‹/D’Í'⟲K;Ôø‘¶#ª‡™Û:o/3˜]9ÿÒžÉþH­K,Ü´À'š>0©×7o‰©WZ!¤I!Aœyˆ·qy[µ½\^8?ýß׺ßOÌò]R Ò"A rI'pê.íö×Xä!+Áw¯ÌUÍMØAO%ìÛìï!gr©ð‡÷¬ŽVN{µ&Z4Š[™ô9K¾‹|a›µPÔ_BΧ‚·&³ÔÄ·LÛ5w$”‹ñŸé‘'ø.…ðiã{-³Þk«–[EÈHU¬›˜ « ´.;@PÁ3Ìø6©fÿ>Ζý§³œ/!/WùÁL¹ë«L +é=3°ì/tˆÍ¼gzoIV6êk^‘•WÑîEWõÓ“ƒmY.ÉMìJi'Ρöœ5AZ{^奚–rož8“ꨉK¤>fá©u–kηÂõÆa>OÛybjkꇎÒ3¬búäÆ”–txOHc‡¡Ä'vSŠO…¥YSóqy×ÿ»ÍQYÀºŸ¡Œ`ž„Y|°=?£b!¾QáŽm¨ØžþPgoÑZ©nb¨øøàF¿/ ‚!ÔžájXv2*”¬’.TÓa9jq¶¢6Tò‘Š€¦°5Do4D¨@ +ú%T1ª¡¶º.TU*³‚º ^[µ +Å‚úÅf«jÖ¶†H¨} Í,› ¶P¨º{cI…š½`‡…º¿`Ë…úÁ­Mêƒm9KƒY* ƒyûÍ®’2˜É¡"3˜Û¿Ö@—ÊÎ`¶Ï¢Ðiè‡BÒ­Ÿ +Rƒ¿ŠZƒÏjc·¾O(­]]¨P¼°øÄΛ‹O­^á,4åZ¥¼õKß:ç6JÁ;ÿzóÔ➇ÚïàÙ/äÛø@¨CoYkÙC¬"ÖÄoB^OÂ%¡?„\BAÿ6tB(ö¬a¤§5a… + k0+4G„pXh²ØFÓB³FˆÊ…†Ù‹#»aè; Æл²†(C Ì6ÒZiBÄ4táÌPëìÝÙÅg×ÆŸÛBk,8tmCÊ¡I)D¦×>§ÜíRÛ yh» +Áöа5ãô¡ßkæíb!YZÎBÂ!4®mñ=#ï:æBÊ$tÞmS/¡/dnbà’󉽄»ÔQŸð ÅN£gÌ–”æŠ=Í\‹.ß’ÅÂþÒÆ€Yt­b2¬ã}bÝ<#ó~Í©™ùõ –o’Ã!-w’!!·aá×r›Ý3 a~…@QSLÊè$Â/ƒƒ ¯iÆ©™Ì[Pô¨WX²•¾AEˆ=ÔÃBï“žýrB-³«é }R¦2oîSˆ6ÕNmn̸žŠ¼AvšÛdmMÆ +\«¶Ùßøž5‹Dzf¢ÃLÖ„öwÛYãq]×»zhÂþ¬‰úu‡×T<&»’pÚÖʃp`CõB8÷o‹µYK'âÍ[«/>ß ÌDV"€G¨Yx&Aóºâ¹P1N”®ý6ç¤f&¼}œ¸—8Q,£Œ2ÒúÈ6uÝàˆª6ú µÿùMû`¶ãEe9yÏ_]§˜¶ tWLMmËîÔ¾> Ï„íe¬-†pw=K O4á;_fß—ú[ÖAÚ¢ÛqD#ñLO,²å6CÍS|ñ=ë"Ʊ|n3¾{¿y¹î/i–¢ú 7¨?[ÏIƒÃÜDΡ¾êõ¬‘fid®Oââíƒ3k‡Y¼ ´~@Ÿ…§L¢ªæ +ŒÝ·þt{™6¾œ¯×/¯ÿ0ÆûÝîò›€ú„¹oFr5o­¿„³ov\*Ìèk#Ô2G0§´§ìÝM o ‹ÆñÁ²ÏkC¤;“ÓÒ¶1O~^¿ÇRXg|J1Š7Ôu“³*>‹ˆ HŠf¼:ü™î…+ÒB=ÍP)%’vD+”-I¯IrK&¼ÚqYäÓ£[)º  Œ(Œù秢©P-Rù³:ííªÓª°¨e/q…¦:Ü{äòͧ»€(Ã$vI˜Í\Ååii¢¼‰’“#+vBXÁw7–¤RrGó¢2[8M]m!€K]<Ì&©Ê«’P"µ‘Ž%úµ¼…]Ô"¬Ÿ¿ÖF à$Rz;=GÉL·ج0ÂþíhÚ¯>O5öd¢„wܼÆñ¿6ûÎN 6Ç’jÐÍñiâ–¢Îî+kNa»jU³[MËÄz©÷éÊ?Wo<Ú\ ¡¦Ç +Œžç2êö:0l'¤]ë5|¦·»Ã*¤ª¡R¤g”Ày] +Ãiy ‘ÒN=58ã^îñÙ÷„±%¹ÐÆtcPr<§r¶1H3=è$ô©¬ ! …¦ŠeHb†5$µxn6`çÞ„mµ+œÛK<dg*SΓH¸ñQ hÔ19IÒHtÜhQÛA8ÉÀ@ÒüyÉÈñBà ɤ׊ƒp±”e;í¸Ý¶°e€s¾»’ýðâÎy‘AÓ’U¡*¢óéåúÓî¡>tÐ*úÄF!:I×åIÄñƒø¡tŸZûf?îúªaß ±Í{A‡²ÊÓíD‚4Ó‚|]¹©¶ ÕȲQ­G *´Ý ŠŠq=‰Q¹.‡9ªè­Ç4ü¸PÁ4X¯bœêîJGKe•_`í|ÖjB:ýK3À(~ÙYéæ)ÂŽUõ"€0ñoÌ¿ “ZpµÄµ›4í¬8©\–òÄù6sfim¥£wò¨b±»äÆ¡9È€ÂIšj- ͽy¦Ó1/ŠãŒÛÿî$”¨<šx¯êÎ÷öëã€é7/õâU(]6nÑ^gÀ½É³ß(iNÚ|â¯HMœÃß ¿õ³>ág%R æx†]Ø®(9â»é½ŠŠ£¥ÕSz‰£†ƒOñ‹ÉH†‚—¯RÇ Àƒñœ[ø›pn¡hV¯š˜“ù,¸ÇÕ§W“>ï@¦@ÑË{ŠêÉ# O¨}š?L–ª:&›½ÙO† Ú“äUHtSs‚^©¯w:{RCÏ}Œ&WRìã¥%ê¶_‡à¯ùÇzž"f¯5“f(Àz u]R5…ÉuªH€ “@ÈOE…®˜¥x":í4 =4ÁõCš•Y= ãC­k—#h@Ï•TevŽªH€A[|À'b“UË õ(«F¶¾ ©Ü ¶’M¾½&£ˆ¸¦¢ŽN; —*Ž´>¦GžE¿(ròì|¦G%)nS’ÇAMÜ‘¼è‰Nþ¦áyœýÃ[öÍW_€]Uꇨí„Ú„•Õå ç;þF6Ñ\ÂÑ:B綃<8¥i=9a¦÷Ûn,Pœ‰„#À²™:ìJ2]ð˜àZ~^þe7Ìñ”dHz +‰Ú®‚´›ŽFêËmŸÅ¡EÉ•m1¸È@Tÿæôn°kf’ëfÇm7ë6µ'J"¥®š¢á›üjž¦|:R8x#¦-Ï/˜ÒM>[‚&ˆjs0àʵʢ¾òóóœ¾>‹ê~MpQ‘m’^D¡]z3‰ÌßøÈç/RRƒßç/?ÊÿÂt<”UÀI±©6ÅÌüJ(±zâ€+{ªP©eV=&D¨6èI>¤'²0 AË5‹:ËŠp$ÂÑÈ"µA1Ö·£rÑh«û¢xDŠ‹&,ê+õIf“NMîçMë zQˆ©ÈÈñ{%ÎQ’wLÆí]ì`¸rcA +8Àæ½ðîÃv·ù +•‘Ë·€L•Q…Fí*ÝN£0‹ª(&ä¯!Å?Ç_Þ~uô%¡å"Wúíýœ×Ö¹¹!à8ÄÝaûZžØ +N„ýeBåÍŸ¾Ô­óúÁ$fB^E‡ØGã=†³go?ÎÍ´éT%ýc"ŠJËÍʙ€äNeãgý`Ðh„?ÍêèçnQ;jF¢Ý¯G@8°±¥$Zïý ¢˜:–ñsÊæ$òg´F 0ËȽ;ÍMÐMœA +t±­vqðÍ{NGˆ>tOÀ¿ZÇ›Ð6ð%zÞ✼ÚUaøÔžÀ×ûÁ*ä"í’œ +-‹j¦‡˜jz[cî—¸ŽZ¦ßv' +@ j½kòúûCrfµ.ØÔßÕÍa£*S1¬Ffz=lïÎjŸÉ>Eâñ˜÷¾OÅás ž‡’‡k2„Köüùz-ã—×뽟Á*%âJ¬’&®æ*±¶»$_ØÜ 4ÃùˆÂ7³(Ã×ÓºÑˉÿPÌÄÑ?qs§BúØ q% æ\=`Ѥ?\â·2¡¿•­us418RÃ.J,@"TÈ'BÇêeéÌh'¾]¥Ê³7ÄQŒæç ×2-y¨y#åý0r± :€÷Šb‹Cbj˜výƒ4 öJ·3ÒúŸr D%â^¢Q$:u( ½U‘$L³ÎÁ« ´å6LHe°Æ)#ƯàßC–½ÕÍ™uõ%¢4a{ zòB…´Øà…,úü®Ð?!v33«ÅnvhFPfd²³Î+>‡N…ð¨Ñä5¶âŒ7aˈYH U=½kÛÁp²×f”½þ–UuÞ(^>VºœèǾ{“Ƈ +È5§øí(p¶×ا‰ZjO€>š­`W¸Š~Ù»e=&¼MdÚÇ <¥–{Ðu +¿„‰'€âŠfÔø*P]?úôø\’¦üT\¿ù ýkÖ[æÔƒ&ÿ)*Áµ’ä…ÎUj‹ÈÛüQ$Àwæu8¦^†qÞþ6ø!iפ‘+«v¥‚I¬v9=ÑÉá’¢5Ø|á$<ù©3JgÍ–áÈÐâÖÅ”&vM|s2èU9 ²3t@·ê™¤Dêè º½«"ÏÜÞjOÒJNdO¨yEòŠ¸}&&÷+%“¤àìcJÁ5M ÂÐNﲚP>ò<ÉiU§qÕP:ÃÏÔÙ4…u‚ÃÑ{•f®2v’,=>ÝÉë)‡íÒäØ3 ±¦dY'B‰4ÁwÓÕžEL—™€JNŠwx«3\« ?œR]¶u«|¸Ëg]|«±MÛuu±Û5S¤CS°µè÷Ä{F ]Gs0u"ö?½ÙTãu€‘~Ñ=Np¥ª´ãh§ìÀ5­Ÿ0Ž]¼Z0S%~b“Ü—3‚]‡ÐÒ«Ze R^¹ð¥â%—ÀB•%KqÒa{Ãö0þa&Ì +‚\ã]œ¡ò\‚Zpi*¡è~ž‡3×ÞŸøa+¢%²}ÎÔUdšÚÑ”v Ö:ÒiY=K¦=í9§®ÂP£]pIcB"ßDXuG>sÕ~¤ôÙ×Or¿ÑÄèVv,IPþÌV;ð2Û¡úZ 4 ÆnõÐí9 f2Z†œùƒÊÁë†rÄ­;ñyQÚèM0=Áw@)PõQˆfJç5øìO/;‹ý°û:™Åª¸ûõE_³u#7ÜìVTØîàÌ%gKqº÷§K'(Fß¹Nv§ìMJ¤Dîvˆ×ôËæ–dÄ?¤Õî¤Å¿Šçô$Òæ +ïGšL65"Ù‰VtûˆÓ·ˆzItÿ µÏŒß=öc¥Ôþ¹‰R¡=@Ýܹ¨ßY5·#Š›ä<=by˜J1=z«"Tš);›zV;žS‡šµZ¥ú¢ R¸d*üù“âôêvooˆç‘óY²µu“Ð.¢:÷/áráeÛ{\…PñZaD×ðHûPϲ¡Ü"ÞÊIMÈ·ƒx¬º ”Ú¬sš.ŒéÖzçOžMvPšpÐ’5àD4‹¾ƒ$stø¨ÿ\ï±Å‡³ÚtOUðÙf`rµ``ÀãúòÙž$ªâJЃf ÆÞ49Ñs»ŒÇ9³ž$ÿåHÙÃiTž æÁN&e X—á^îlÛ1gŽÿ‹™xgšKõ®akU0ô\®PßV½\¾*ÐSí¬·È+Š{Œ}pÜc­…°µÚ‹‡;ÏO°j†I€B;ÂÌ2t*p4´êM`sÐöt_ýËÃÄYP÷Y Ë †ÖñÓ1»ãŸ¨­ º×€b1©ñbÓ¡êSvò")CÆ’fvPªì¸Ç&q0˜&aŒ& ›Ón‚ŸPÚy±Ú2L§jWäÓ®Ë4hòTúÒ‚Oyº@ßÍÛ’%I´Vû ‘@Ðk)%‰0Bxç‰&ðâ’ãbkÀ Û)J+ †U²ÙU ¶žŸÀ3™¼»z&'»“™A—0öå«®mô)'…³ª¦ÌFi²€ÁUPHC„Ø.]±j¬CME–Ì ¾ ­y8"ò™âÁ*KNˆÞ™2ÊÓ‹yèF,>‹Nå³—â›7×øƒ´ÛÓ½K€Eg*-ݪ¹St h|Q˜€=-Э.Zc4& éÚ»¼¸SÕ@Y”sW,.¨ðøâ äNµÛÖþ‡™‡žøŒ0K£ýûl¢_µÐv|×hÿë»t/ÚÄÄ9öjÿcRŠ›yµÿáQ²;b62i¸ÍHm©·2áÔÕ¹7ÿ™›º +Ìíõñ(•9é['‹GlñøKmpáô\{7J¨FÔ¸Pú˜ztrq>Ûtúñ¢`ë¤CVøërëdÊ·s&‘xÒSݾ¤Ãl_¢$é±ö÷ºdñ^Ë\ã°Œ*ú6 RCt²rTìp3Q›ïÇ ÈÔ‹k…ÌÖIݹ¹ƒ©eN!°wN7 Ó^p«œüÌî¼w>Ã~ÔBºÏÕ&ÏJ:@Êíyz¦®@qû(%¬º4 ØBA+àV¦~¿>A¥´%“„ŸW_ˆt8²fAÝ-Öì,òk³q¾NYM|XÔëK>‡Ef"è†õüÄÅ´aVjM(Í¥eü]M]RøêÓè"Â8D¾þ…s£ØìæffØã œ xÛ-úÉ87ûº„ƒŠŸS®Ç í„¨ïU\÷/]þߣ+ ´)Eº ®/ië +˜Aã5ϤÑU?²ºU)l*"Èéà +°Y™¸áJo{W€âS•u Z ž@ÍÅû™ öK2¼Oœ?ALÛ|ny-G€p­)Š&Òw iƒ# ÔA¥20yï˜8þÔ•I×Ï®ïtªºyPi‰¼wÀQ«ëŒ4EG€ƒ`«(£¿¯è) }iøSL™P…üÖSt +LÜ~B¸L_ J­ä(á/Àr´è¡àxz:Un«)<—0›Ì®ê In=ÔCÃj97.O`hÁ¨+[=¶TTb~UYÁ#`8Ú-lr§õ^<NªRQ¤ÂC_=‚J£jFpzéG ì6ú¾¨]kàîÞ™u£¯7F‰D”¿:á¶É:P¡nµm<0ËÌQ)`–UJ‚G@†XÐ_¦G°€ö'RÅ\ê0L5zÀb‰àfu>)¦#òraÛ¶)mU*Qî™èu¡ûnMi«Ó@phÝߧ´EP$c&3 3Û”6—Œ*›ÉV `i`hÍd«Ím¾wM`«Í3v)¯%&°…hÃhsª ÷}Û%»½ao›À†-ëR©Ø’éŒ lÆÙ \<ˆðÚÀ¦¨ðÔ%é\“¼O`³À¶ð7©qô×6{{ ÏË&zà–¬lF  ·œiî3ØÈ¡‚ eBBeÍ`«U±²VÊÚM›IÚt¤ÄJí3ØÌ ¸?J’ðÞGÛ)› I; ý‚1ƒ]pn p„ôüÀ’ÁÖ{¸:¼µÊfIüB\@àoRÙ[I÷ßÊþœ-ÿ>—²úÚ“ÙYM÷š~ÅŽù‚\6`þ&cI]tÞ’Ëî§__¾¦u³“íEgQÑò[}Ž\6£†ˆÅ3ãš¹Þ5“uí©l$[" ö;LŽL6Ìv9¦w^3Ù?ÒäžL6¤.*N¯Ÿÿ¿°æ×õïA(?Á⌣o’‰²å›é¬¶|3=(lo1–Ÿ@˜¡î‰60õ‡Xþ€ÌPSâ¤á ±|½çýŒÁHø»`¾ÆB¿—¥+„´ó]=uÅ€Ó>”Ÿ@A:¯!ÏÉO—Û©Û~‚s¸oâöa9? Ü'`ÚìªÙÎC‡zî÷œ %  –úŠ{¯'“ì‚4++pOEœÂU×b` ‡À=/—Žî ¦;î5HâWvvuà§äCøæo™u@a™¸ícùÉ,HPPA€Á ±|‚ý[h’S²Æô&Ñèº +¡|30Qg¶fsŒäÛ³Ž“ T>ùµ5’Ÿ0Bq»ÀÓ>’Ÿp>3Õ„r‰‘|ê?“üìå˜vû¿ÿ{)„WÜBðy„õ~'`£·ãজ–uD$Ìmkxigé|1oÏíêzìD®ƒÉW¶’¶ ˜ÙÃNzµÍM¯NK”`¶s†«šøK\Õ©@ pƒ¶Ü&Ã>mwƒ³#@o! RCDÚN˜ÞkÏÌ @uÒÆÆG(ÚPìD˜mvó1ûùñ¸ß¿ç¸½°Ôv¹àm è_L &u)¯„6ks¦*D³¢\Ð+Àç*\à§S|YmÚðÁ)p#V%A¿]Ô÷tJD×ãrHwÖÅ_żÎØÕ1ŽéE»X!æ‘.u~æxýp ¯å5âpEíÉ™'^"œwò‹GŃH…Ý!¬ªGJj¶× Õö2Ê®Ì /î¾ÙT£ÍåSÁ¡ÎlÈ)x.šo‘©ç9 ø6¡éD¢\0Á\¢ËÙÌg™UÔ]qOœoÁTOµÂRš$Øy UUuQ²›@?%/ÐL[‡³T .e@Ro±ë4Éå#¡Áýpê$JÒåM¨dèVÁv°*2lîăNt,«Ã!yLšêRËþ9Þ¸¹ø¹†Ã§Ž*¸O™€«kæÍßæpñ±z#¦U›mjC–=“7Šû>]nËÇwÞTã0^ ƒü÷H6èu7;pøa#B P/_ÒÄíh“‚'¨¡èI«¶ûðòƒ.vq€às%¿é ˧>à‡8Ú¼k ;AHÙÒß~iQ!xaQÍÃN²Ð†è¤Ñr6ŽQ ßt–Ì"¬zP:ÿª&Ã&æC°öïS¨ã€×#éeGå DÇ7}üã:+6I.}lÞcã”G%˜=³NLc¢_#» Ÿ¹oþ¼ uJÑdZš6_Olž +ó¦¦31¬™†|“*$Û½èaç*ÐW¤1ɽ÷¼Ù}$Eì„HÀ¸üà𴌠+¾!¬±þkÓü‡Ä q¦{2„eìSßgxŽ–¾w¸SN ‡ð䤯®„ƒ¦DNÔ/î‹0kWæøâwÞÕª¯L*Ï©ç ¦Œ(üðÃïŠÿ°sÛÂSÓë£TÅAló†½+8Œ`ø “{ïwVPL¸m™H_Ú¸¯” VéµV¨´ø’'6®tx*¸ät.)”AÙ-œ_Á­àf”­$…g6Qá>Ò– +îc£}9¼x`îõ1ýg<0–S…Í&¢ŽÂõˆ,Pã]XÄ꧞¼:ba ¬Ð¯c‡ªužAà{Žâ‚D#ºàQB\Háw¬Âï”zoÃKfMjTª}ŦÃT […±4Q +½†ºPœÐd+~ ,ù.bFâ­IT%»íÜ®7AQ:j—ÔÖ»CŽxÆAém,Ã7™¨˜}Œ%’2«”Û}!Gã‘dy¨ÂRÀ„Æ·áLIJƙ×b4”:vmµÉ5•OÍPÈ’}*Y|pû¸,µË„ÜBXWUÝ}p‡tkh¤:- …}ˆd MET¥ÄµªçÁPÈ¿£ÜnnÙñÝGË1Œš> ;Mž†§Æbó¨óB¤yÙ/GÙÆ"Fؘ!àýª ãú'ªÓ—$é:ü÷›p¬¦Û\E|Íšó`/u(P5ÕzH›`ßxŸÃu"f›}QÙ¾,½Éâ¨Ð”K– U‡›L€Ù%@E%f”|í¨¯;TžJ'¶=EÍtÒpeC5iÌk&Ä!ˆ_T ‚q0ÆTDjƘŽcü %T’@›u¶Èå©ÔŸ3­0‡·P¿SÕ)P!O+â´{iç‹»A'©ë³"ëê–ÓàM„‘:%ÿéb8'p³•©'¥an…¶a…V•¸G•d¯ÚÞ›æ¦4²-Š]0êj ™›Ç½E¼L¨V®€ÊuÃY\U@OØáD “jÙ#š–‰ûúk—Ú_;®&<‘všUW­¯ÙÞdç³Âvv€ðyìÚpºK˜bM½ÑØäÉå±v•¤Ù§È`½Ùb}›°ÚÙeÖé CiÈNF£1NÉ5 Ao+2äȲ2æõ¨gˆ aeïà ½¶XîþBoY¶©ßjݦªâP³ØiªQof]n ›IJ O{Ó¸ÖëÁë𦦺=^âÙ…þ‚ € q[¨{sèü?Ô™áçÐËM:.£ÔØý/£ßÌZ›Ï$G9\júnp?Ht—iá@*Ü÷ö§]z/`ÌJ—} ÛzBlAuÓ¦Aa¯©µ£´Çky/ïÄI"^S{êFôB§EíA1'‘Ãe¯é¹®c¨þŒòì…¶—TBóyU=ILvgòÑ"TÑÈÖÊ&¦Ì |>ðN±Š” +‹—; o^JK¬š”LMnÐyæÑk!BÏãK¾×PÔd5uÙ:s~ƒþ*ç×Âq¨oÑãÌñ¿&K[=ÝxïÌœ*è{§AE‡Ô–sÏäh`[Ýíµ ïù‚© +ù$¦j¡E,˜@ø(’,¤:\¾ZP¸ª—ü|c—rY-1SäfQ°ùË[­9›’ßÝC¾ëÆ*Él¹^lJúÕÔ¸öéõ`ïÍRz(íœRuØT»Z·”#yËG¥8eš·«•|:ød4°9«i®îÂDþÖdJŸ0«…ßïñÊÕ'÷ÄñÀðÒ0ø€vÀ×brë¼+x(×á2Á¹—*i;}åß%8I"³ApRÝÓÖ>– Cm…0Ý{„añÓ +~ã¥J.8®zô÷ÂîY.~ãæ=«‡ +߃"=B“h»'¨ëæ½+Àú“Ú‰<Ý/ÑŸŽO¬n9ù Bðtç‘ +î=-Š—d¯éªøBd¨”‚ :EŠP7KµCr²0  ÕHaN-D3/!Æ¡v¤îÐó¤,bˆ„‹[D6¡Ï¥ÇÛ|j¡ŒRGÐ\0ª^B¸†'Ô®av3Fh ûHÔÀx È·vQç±4A8’»¸:BæiamŠa«¢–«$½Nwž‡wá/Æ5ÄT£h|H÷ZÐLÝ“È!g»äé„D}ðvk@OØ$ǵù¹êpÙm5»âñYß…yJ +‹Ÿ +59k8!É›@<º¯ÖtHÔË!Q¯‘ÏáQ±±‚¿N¤ôB¬H+Õã«ò•±¬‘Zž ™|ºJï½¥˜§xÁŽÀMùA—ÑäE)!^N :ƒÓ«ö¨àžNÙ¼æF*‚µ õ5.>¹—ïÃçH157$ +¡Ÿí„çš¹­”­y¯Ò»@¾j‰¬ÍGR¯ù­ö­=¹$©g ô]^×Ðèˆ3÷ÁÕ¬øÁ¤LSÌc¨é +Úé$ Ÿ H±äC8´?1ÕŽÑõr*_U÷•×ŠÌ”Œ¾<5gþÁÄ&ÒÒ¯sü.ó£CKo¥}-¥×¬%]J….¾ZsN€äåBºJ`jš·ËŠ~H{±uj:˜»Ía¯é3´’ŸR/Â%YRpj»Ô“eVO‹™<–¢5LøC†â^3‚|ˆ.hÔß©~Ò5«ÈÎÜ:Zté–Mv1âò™rÞ™h½ÇNtO $*‘2k^\¦«+{ÍaM›'ìž*·.]ÒÛüj˜pÈõ>-f#C\Ðiö°-Ÿm#¯ü<<Ðñõ󶣛ÿ&·æÉã­ùöýú†&¸°O#Õ·x­Ø•µò œ¸µx!Úµbø×bŠx‰ÖzŒçþ…jŽíõ U!ëå_ K‚ø*o…N¨o ¢*ÔÈq·VÚl¥f(Ø™B7Ôú1j†¶â~-=Z•F¨] +zg–>mÕV¨œšJ/]}Š·¶z7ý +É‚ êÑÞš¡œ-X ¡$.X1¡´nk Íʼ`L…¢¾`…šÀ­a·–N£0%F{r-n vé7»"É`Þ†Ë`&ÿZãT]f0²CEg0ÔCeèÎÞ¦ÁmEªÑõXk\·.L¨• ®P(³ îT¬ãݹeñ©Õ» ÃÁA uÇ[G3Ô/¯k¬þü;çyóÔâ„Ïrîà¿Ï"ð½û¿ÖG÷?”Ÿ‡PD(c߆4B9ü‰õ#ªëñwA™PÍb;¡!`†…B?Á.ªûPŒ¡aD¯B3Ä6ø›*Ö ZlÌXq±ÁãM/ö‡¬QÀØc²FC‹Ê6"¹4º„¸fh• ±ÑÐró6´:vBd6týÌXîìÚ€c§ÑGŽÝJk,:v=íbÚ±gj‡¶«Ví[Ûð|h ÑýØI¶fbGÚ.Óß³f,bsÜšõˆMv»ìIlÖ[³0±áoÍäÄÆÁ]F¨Ú€Uª'–̒f-P¸ æè³S²Ã´×UÀYÛ'ZÐïfë»U¿¦ÊÀ….Àö˜¥H"¦Û*Idé.Y§«hÉLl˜½ÖbÒµ'—èvçyÇÄ!v9öìšuœ&Zö›‘‘q1yY;XaX­füG}ÍÊÂ9IaT„ÒJÌ¥VÐìÈœPó‘íYó±-u5£SƒG”n—ÕoY“Ãq$k‚9ÎhMT·]™5áý¬ëš.W_LØ–5í·vÍÜ?'c—øk- ˆ‡s­Aˆg|WËïÊZïÛZVñù†_æä€oÚ„ ýÜ[ø¸ ¸ZÄCiÉU¸C'^m¥Y @1/Û¨S‰@3³¼Í¼ptgjÀ7¡ïLË(øõ ¿ä‡ s˜>˜ñ5€>bŠš%“1*ì{â].Œ +C‚ÚREô“ÜÙýÑo¹=Q\ÐP˜ë8L“ä"Þ ! l¦³>áÛ9V#üy\ÈÛë„ØÜÌçöãuÿú«QÝO?üA,H…’v!Ð4«ÜÌgG­ˆãªXϙ֑®rø2õW®|½þý‘|ìw#v·ÿ+U7àP,h§å{ñ÷£þn„®Þ?T‹CÔ™YhB£m@…Ìx€ïb¿ÒËý:~$MÂÁO&+Êz¿Ïa;®”^üëuÝÂ>·ü߽߭ƒãe4ŽD‘Äæ²áؘ%÷e_×3’U‡ ŠD—ý¿ž2ü¸¤ìùyBƒ¾?¤þj4Q50õëá=.+ L¦¯ z°R*^x†²]•PxËruã@ÖÛ'´"버·Ä…]¤Øg÷g¹|ë ¥‰Añ´RÊå Éñ©^Ä$A ®ŠižV$ø$1+ˆ9ÖÜ_ÿ6ý@«¥¼»~›¿ZåæƒëëÀ¿Û‰ z+šÀ'˜3Žöî +%BÄxo=¡^ëë ̆ ªAÓE1õü4ƒ½»¿ñÏã +¯CøìÌ+L ‰®LôV‘ž3J„†`”Ù qÿï 4YH[Øé Ú™ 4ØÌ¥*ã—ñ¨ø§ö@jššéQs}´m›×ã)™=ƒBv*!7ÅôVPžºn…ZÍ¡,ÚžH¦ úÞ¤3‘¶ˆPª³Úè42éu:ùÚá¨-f¥,ª'$ü@‹>©¢Ä.„*~.Š°â@~~Š=×ÙšiK ÍLÛ+Kð›È⥾TÌЛ× ®ÇMÀ¤3ç¬4!e³Ä*7pÑ F%¶¥$¿äûvÔ¸ +Pµ½Wå½E°âMD·ÊóCA%ZH25%×K¼»ö%…¯ÌÝ딺E@ñv +éÑ2Lƒ(ÉÎÐEc ¥¾Q‰|UõzÉkᦠ;+Q] £©œßK¾ðçAx®#2x;‘)ÂÃ:¡Ö2(힬:(líªÅæ¡XUàöHMŽfÐÆëéJ}{I‚q.[°+Â…]gº½÷ÁÈYåÇgí¤ÏØZˆ1aG/þ_?ù›¯öà¨WùƒñHúB0Txfi(趾f6ÿãõGð9XɇŸ&ü +ÿš{+è|ÎÿÏý¿ +Îw ½CìŒ ýû†Ðà·b? tˆý€Ð!öw+Ä~è û sÅØßàs„ýŸ3ì¿Åç ¸úŸóó‹ù!žþŸ3àé|ÎO?Às<ý-VgÀÓXO?@t`ý-D§#ë\΀¬6²þ 3 ëoÞóY?udý-Pg@Ö@Y?udý-Pg€Ø@+Ä~ê\¡ößu®û+PgØ@` ÔöPçØŸøœŸ¿ß¼¹Fïx@3I&šÍ ëcO¹ ÆïÁõ¬Ê]ÊYì`Òwz–V#å.È'žTÁ;®=ånÓS‚AZÐבrÁ£y£o´o(wÓ¬°ë Íôˆ¹/œ»‰ªg³ÞìLÑüV#ç.íÝZÝ@µK±@\Øî'K38wìÁ(|ËŒd\9wÍÀ4J#WÎ]Ææ2ÜŸcÀÁLÎ]PüÐfúÒó†}—!`ÒÝcØ]T”§‰HwÅqF€ÝFo$Ý[?¿j6œ»°1ISõDÚw|håÜMÝ-<;VvÓÒ†s7q>Xiøê‘|w}¿´²ð"bQN&»è“ ¯ /¬™¦ëªq t¼¥zëBEòNñèxq>h©¬Øs¯…˜—Îj*Îkå|o˜y×ùÍW?&3ï;úUª·m[}OÌk¼ã她så“53ò=ìõÊÌ‹| 0«ö"ðj´$“Ž+3¯iàC(K“¼–*Š©rw{ˆy:Jì6~ î \ôv®.Šàw̼ˆ–·Ä¼fM}áÌœ—·B uÀ”[YbY¦n©ýÉ7³‡–WúWâcðée¥(1ùÑhy?:¿àý?Wä½Ûi¾È,¹j¾È,¹j¾À,¹Õ|Y2h¾É,Þ`–ܪ¹@-Ô\ –œj.2JîÔÜC-¹ª¹•Z2h¹@-¹ÕrZ2¨»H29Ô]à–Üi»È-¹j»È-¹h»H-¹Óv‘ZrÕv‘ZrÕe‘cr§í"Ùäªí"Ùäªí"ëäNÛEÖÉUÛEþÉUß}þ"þH +oʃ7¨ïþIóÞÓO^3±¾¼§LTg²OšÃЫ¾;õ–Ø'Í©ëæ<ˆß/úØîÉLàË×àág(ŽÝŽ}’*Á7ä“›þe3sîIèH¯êÎüºNäz¥žüÝ'öORèf¼©î(í¶!”«æ£Cñ߶Sþeǘ& €Jômç"× cšmpét V‚ÌyØfòO¸qæ🂑ÚR¦UsA©°1{kåÓÏ6SZhæ{Ë”f¢G ’f*›Œ=7LiÌ„ð°Í¤‹*20¥©¼Uýèv½ÏIª¾R¥Ñó¯é%ÁLi \°ð€¢4r+šˆš‚Ž-Qš„rf¼Éû¸QZd))swoˆÒ*¤›4lš_¥&¶-QšÉÿñ¾rnˆÒ úS®®˜ÝÒ®¦iAiØ¥ Å@Ÿvc[‰ÿ3¤iߤã–výxׇçä÷@ýîtiv¨±æ›Yø™Z;ˆ6³+´’щHo¥#&ù[M<ö«ÓÝe¢˜Ùó.fT´Œó£ÁDdË¿8¤Ù¦~Š¤Ê´se÷E#9-&Àl$T]¢k¤€¢÷[½°Ùë¦ÊàÉl¶õ‡W=ĤØäÉ{VU«b¨ü°cåžž”&;l¶ ‘mØÄi0*B"ßl³»‰8Þ‡w.`ée~Ô¬8ªžI¦ë +m¤jŽÛ‹[=+»ØñU4è6:Ã…>:ÇÝë3ᘴªþÇûGƒÇª/e‘m–EÚàÔPzŽ|ûa×=ƒ±C$•î΂óp¿ÉßwÀiØŠ|’zûRPå¸ýÛ>6bý,xÄxèrûáK{Ü.õ»ŠP—UùMþ$ä_^×bÄæõ…ÖÚýòã€I R‹}´Í6⡃á%[¯GÁ†pê°à2Þœ(3÷O¶Ð„umN&æ é~3o(ûÞjþDmÔ&÷Êe°›×€˜†obò֌֡`ï±Èå¨ìfFÞ^>í´÷ëAnŸ°—ÇÙÚK\S„zQ AÍ'ÆGØz­c¤ÉbV<„­¥> hœf/ÈZOG1Eï1-BËj8exêð5ì.íõ”^0A#5B`X7‰=‡`Ï©½-ÎSŒvDçʼÙë½ +îfø~¸ãÛáQ–!H™°”AZm·$H½°µAr†ã%ð›Ó5åv8’Qä¯Çúsšc¦È¨¯¸Ÿh±Šù¯ëB¸{ZÖ®\ƺ@ëG£>ŸB² Ýnj´;P¸€‹Ì²Îš3«µK@9¡Â:yÕ¾Tuæ2b"°ÓDdæA㢛 Â‹¹D'ÛD‚ 'z6܇"¨eTrýÄí9Ýwu~g‡ë5”Ã0f(§RGt|ËIM?<‹Eùž)y°úÜ/8‹²]µ¯WÇ ›c`·ì¸N¼×ž}AñÑ b¤óãBIÝTDç°6”,ÛöÚJЊù[~?Æó×Õ=OLÝòŽ”ZŒöúË™x9F¿ËôúÍôl«m}ÈÆb[üäŸfz|Î~¡Êáá(ðî(­ø7ëß}]¶¦üÁËþgó‰~þ×/ÿî'?ù£ï¾ûûþô¿úKžuÿç‹C‚»sô¯Ø \¬T])ÊÓp˶§+•(aWÏ¥dª”ìA7wúlÃŒ¢âÓ$¥¿À˜)¢#KZ냇¾6ÁÄ*IJÖ& –R?e*a©ðcä-ÌÞP'³Æä™éÍ$duºïZÉÈL:'À©ûžörCg„¥ÿwäÂß’BQ¥tL*ø!ra åvˆÄCðÞ·ê<¨ë5 Š‹©e +çhðN)†P¬ÁZ®ƒc&)!Ý[¥èOŠÒS‘ˆÌ“Jæˆæ0l(â{Êž¤Lq¼?{2!¹C˜D…ÖÞ¬UWûTµ+™D œ¼ŒÙôŒ@e !cF`ÿ-0H0lì“zšÉU¯.\î(ð2BF¬TRVlK +à ]hh0ºÐºŒÂËiQÌ`ªñµ¥&GmÚ‘<‡¿`+C7oj¶ è“4 +5Κ•º胪úÀÆ)d¦º ÷Aêr¿ªð™¥¦Ó3©DËiBì­NvdG& Q’¸ËaŸ2åbð†8b|G &E°ef]·ï|ËÔÞJTeaCíaÇi 4z7Å­9¸vÈàA§•Žœ²`óÌÀ.±U³ °“Àt.Î}q`%e]Í·­«j³Äqøé”Ý°wEaC78ãn&â£y00›Cq÷…Ì%îKšílÁŒ Ê­÷`ù\²Wk6¡ùg6½ :-M9×CÔ.T=U§¸&ësSÐm*á±D„'h€Ù1—#²œ¦k>ø ÜŒ;é¶+ŽÝÖ‘d”D*T=3‰%(fÑÑËGBpZ¾²RÅ·”]Åí¶¬DxÅ1%X¶fÎpÎ}Wa–Q>ዪF³›8,ñ ‰€„Ç¢Æ#—Ík Õ.Y•JsøÔDÈÛÜ‘æ„'Ìý3…éÇÙÇwÂCÝÁ<Á^¾œ¡E¯e•”Zèç°´)'ÿÚ.62h»µ¼ž;*œ&!i(<[ƒÓÉfš)<zb—˜êæ? v¨€@Ú±÷áÏLøPJµÕ©ú]ñ9ös¼/zÝ{G§Ê +Ó¶²¶-9ŸåçÎœŠSìH&Ti¨4 +€Ž0ny $@ôDh‚'ed“ª}ø¤9@2ØÖY ‰ÒäÖ¡É3Í™,(1nØÒŽ¢h΋RWêqáÑÜN¤’©À,Î d’:iÞmàþØÜÁ‘±Ñ$¯T)’[bFr@]¶-¸£Ä+Û‚CT«ÿ• S|CI,uùôVCn´(9¿˜|þ>âRt&°ŽÓcÇ’¦¦ÌqyUÿ¤±« ™îÆ*äKˆvQ0›ãŒ®xŽÝéN䟼{_MùjQû€¨¡H¹<ù‚L{  +“¿ªš¾ +ÉW $›Y‚Ï|åüÑkïx÷Ù¹Oì}÷5ª›WdØ +Áš“ŽîD¿eßà”lsØÃ[urÉ2reÀ6q,Swä2×ÔcC{s°€/‚_·‹Ò(Ï +B¸ÔW ìGÑNÞÝ/Dr¤—†J…~á6S‘™ŽJÇ°:"ç.0 l&®— +ÈŸŽH%|ÜUO>¬¦*¼·}Dúv›ŠrY†?M:S¹XSI›Ñ@ñ9EZ©y5sx/a"6àz GTPr†œ-i¿ÃöâÃJþ¼¡Ë¨‡GzÂÑuRuŠ½Ôª¿‚8°prí/t|MŠãû’ÕÁuÔ)ó¸G¤¤›UðnwA°˜CÁÆ‹`J-´tŒ«£wï¤" €dM†&$ø/k—à”¿/ìÒª½÷ÃefÚ-ÊÝ2žOKܪ0Çžnâ+<·xà©5Gs;Áì9ì—ü_:ÔÁO.VA(Î@ ÝüÀé£A¥h TSVÀè>GòuÈS(W†aUOçФ¢ãèuI31Aa[lï¡@߶Ï°‹Ë°:¢I´¦¹†'®ê \ª$lS}ÍÂSë|Ù=57ÂþÔjÝÅÒžÚÓ›×o·3!z«už‚:"n¦@EqÞCcðHd0»žXÇžØÎ)<Öó ”îη%˜×õ­‡ŠyÌ:¬€H~´OCXuºiEƒ±î7hnË„#Ôó¨Þ¸´ÝuÄÈöœAu«÷ÓŽ|ôÍy5—ÊŸ ’\ÓæÌ“¨Å”7ï¯à„m¯7’\D—»¶»‚µz-ÊrwÍJò•¬‚þHé]6$<ý¹™fù¬tœ‚¾¿{IYvC…Ü* á•lw”¡J z‘ ‚/!±žQ‚'ô71ù³VnáŸÀ*$^n†É•Ûýî`.*æB„mÕ<`_ŠŠgUZ| ¹ïU¤±ê€^5%˜^âñYt,i+*n4´ÚªÏõ»â•ƒrâ«íÍ b¥÷Àu~gûž€Ïï ÜÌÖÓÞê‚ØÚ˜Qê=ºîhÙºõÚÛo÷¸_¿4b³bìPÖi6°@TMä­†¨vtO=•gA0hÁA¤]ÙD‡È„³9bp¥«U-ÐϺ1È5¯skÌDJ¦æ›L°)¸víí Â]NÁ¸ºyÔ) ]½×½w’Š’OKƒ#|¦ZÕXÁ +Ol©ðTðÅ ^Í,ÌP±¨;Ñg[o0{×ep%m9d€;qö?óÀÆ¥ ÏæOg¢ùv礃ÍLQ™-Ï!PÍ­“ž‹Ä G"oÞS Gò Žg0Ït@¡ÄxC®Ÿ(Sº!/>8Æ-€òÔ!? ä:7±šÓuè +Ôûƒ +"6IÄš‚¸6¡˜ìÝYl}q(‹%šn´T.¢h Ä P¢ÌS†_O1¶„GfˆF™ÌÊŽí½ f]ƒ7a€™mRÄŠ²ÏÒ1Èú(AYncpîíÖ'rws܈ŭ±>º1îœ÷¡Â³ŒA¬‘ÆFˆ•V‚›$kùä6¢ {¼[çK<ÔŒêÞcˆ¤ +‚&÷wñW;f—ªI— -T7Èî•Ž?÷1bL,a 1ƒt‘®’¦o±ñØ.Ž} +º?ÑïSýéWŒ—_M.ç&Öþü‰‡çQ ï]<Ÿ¸>€gBŽBôÑšC€ûœBo³h2¡´œâPú¹f1t‚ŽOÎ#GwèIõ.*u­n´Qó-(|!´¿`!tÅ\ͪΙ\};~ˆÕm/7@C²›æÆqP`,"ímÓñ•+ˆS6:¡?‰òU¸*%¸Yv~dÐx1²le.6¹´“l †¡@ÌÚ£W`L}Ù€¢ULó½µ·™ç|¾à¼²Ç^´Bi MÚ6(‡§§zÔ½@«¶c¡-†õª^]•ƒÐöA‚‰š 2¦÷!`¦I‰3ž¿÷Ÿ‰:3ÊËóˆÁ |„Ò”g\&2ÌñK1'a÷[À‘\SÆç&\°Š1È$\üysà@HÀ:Û}>è$œ÷ Tsµ[P„ÇÛæ-ˆœ²1ÊIàx¯²{Ü{êÑH4 š-ÞßÝ|(6L ß·ÍË”*Â)P‡IÛG‰:®%³Zî6,ê!¶€?Çw›â ÒaOù‹ë2d¢í²ûÑÏj¹B‚Tì"°€7%/”\ªíµÕŸbš±xMl¬iÞý)ÜiÏÍИá}ÚÔP|0Ï¡9ªž‹ä—°SËðo°ßMuD÷>wƶ†ZëQb‚&¯´S.1üþ©úÄÛ‚?eÿœô©"Ù“Ä$Òì¨aíp«=x ;4çØ4^á'…¡Ñi2JrEpXm]§P1nØl +eð:OãAGñ¤©ãtMìx% n€W‚–ôh¾nŽ_Gr iÑ +âöëC¢Bé/«!Źà…1'\DãÁc†ôŸ\dìN=vÒ;»®Êoâøw +ßélZlC`š:t¡O`JÓ´¼“õ&Q§½·¦Zçó@ëhþ—`Ü‚ ‘èÍGŸªÆc5’Ãß Ëò±½yÏb°³µ@F]"O¡A,>AD^à¦æ¨÷ 'k¸¨ÏÜKü‡Åm©°Gu:ZlØy`¡÷±ƒïz©gðÈÑѪrù°§œeqóAš$˜Îþ½wÅP|?¡{0ü§ ¤@ecˆ©BcÕ%Ša.OqHT툠B¿z³ÜýøÔ:Ë­¦p>Á©à­YŸJ©€ƒ çsfH–ìQ|ÏšŠc Olçž +k³fÕâú®Ù¹w»³äöÂÖ.IÁçL¬)Åý‘ZS“ñd®éÍxº×4éþ–¬éÖxÙÖ„m¼°kâwß—üqk +:Jœ%“½•[k>|•~!¥äfHÍo¥nHñ¡ª¦˜Ÿ5oUÂZˆʨaª(”@l5Y(¥Š0”cª:¶x…½êIVÊR¶æB(o ÆF(‘ vJ(µycÜ„B`…bŸ`L­5C[S,”C.”/0”ASò›]9U4Iײ¬`ÚþzÔѼ+ïŠòZ"6MêP_¶5ÄCZ0ãC­[tÖ’¹½K±VÜÏdÔêEWfþÃÖšÿ©µ¤0:ckeâÞ©[ £s¸ÖH~Á[ÿ4>µú¹KÕgt•×âѽ˽¡Æ÷¬…¬Ñý±»(ÂZWckmnŒc¬5¾ûxÈZ+ã*¡Ü8„fÖªåm„gÔ<¯ñ¡P-CL£Øz˜ +µÚ!®µ–{‡€X¬ßÅÓBÑyˆË…Âõ5¢êß·ñÀPG‰¡ ×þ]ÐríqÎÐ>°ÆECóÁ6ªzBPv¶?„0nèžØFCFˆ!‡þuíÛPuè ‘îЂä¡ecïùÙ7"ø¡íf›Ý;!;€–ÌCl$Úå-rr +a@»²,kþCSÄ”fŠå5s"LÛIÚ»a“vÉ‚¦\®]Îõš´¡™^Ð@UcºÖŒXC̸éÛ\‘°èö.j".1Ó$Ëì=Ûr¯ê_“TX\nãÚ8°†× ×4 •ýZÓbòß"ð¦ sóVhµ +ß1ó–øºc®Žª'A‚®œÚ.Õ÷üùšŒ_^³ŠÏx×\äwog¹f0ã­ пøj]¿[ë5÷kͼÆmß%nÃéYӾᆄq8ÇoÓÌë-XSÓñ­9íÏ7ö1•sQv +õ½Q3qÅíW;`>†`‚øsÎþÌ<?Jd“…U´ÌÚuÒ£¬ây0ôQUé¢ïùÍÚ”›H!*àÍ”úÌ¿‹ +9#¦ J³”…»Ø…l’\™úía e‹ÀÐï/ñ» Ù‘š›V.m3ìõ ?¾ë¬Ã{âÊÝãt+IÝgwàußÔÀÉAS€ÌŽé‰f"'fÇè‡wÿhÊDQ»5”Õn]ó .‘ŸnžèúS³_ðç·0éˆ%rþ¯_ÿ°Œ÷»zzÿ8$oøÙóN£ž8á-úóeF®©ˆº™-1Øœ$°ÌORx%ÔÊfß¼g¬]Â+þÝ»¡…ªÝí ‹¸ +IgÙþ.g~sAœSáÒîà*ÐŽõÁ­îÝzäAÂ*éƒÓõúW~Ëž×ëøŒåÍÝ}>~{á">\—õ\/G¢_L?lÿñfH›Í‡Ym{CéåÜ=qëO·|óç¾ù¯ÿyŒî»7·äàD~$›/•éÈYpUmvÉæò©n@ÁêÕÀn W ¼%¬\Çk;O0¡Šž7Êÿ‡pñÉõks†ðö ?5Í⪄CJ.TT&¡,â1˜ÖƒlOú ´™kú´‡HnXLÔýËLäf.P¬Až1n_¿eN*Ý«Øv6›(’`4;,TzHµô¨Ößœ á]R £O ì ±@),,Ç­ƒ8B§œò`úá‡IRûA3¡ítè(êN™wæ_€ý §÷Ô£µ(:‰¶‡j1±‡Sš-'˜ä©+@lÐ +$a¢@Û L”J±45It–J)›Ã›@éÀ®tA”¤ ¼Q}ñ”9òx`ƒmOÜãÚè_å¦]οØ4TÏ[…“ÍNŒ×ì+Bljîžp¤ÃÝšå-Éc’ÝL>•ÖÄq$±ÔÛ {ðƒùPDWÝ’ª`|1Êåa'óÞ²ª7Ã2Êqe!€DBöowXõ&¶qê ›Yº³i™Cäœëy€~‹œ NÖ½?Ta¼9‘…\o·?µELã̾?ÕË›áÙÕ¯åƒËQœ1Åá–PóFSƒØªÀ¹CË]\'·½Ñ° e'ï +g ægªfÂîMSÕ.r÷H>×B7>1 >óžu( 0Á»rºÿŽÂBÐX׺,2¨]¶—7Ò~aEIê»ýØyq_OÔ`ìvòœÓQKBµ‡XÂ[8KJªá¨sôËIÌÐCØ‚ˆU8Ò=)ûÌ&ábà¨ÌÃQyÒIñ‚)oul oN} 7$_JÃýîm˜fÐQè÷z‘Ôˆ«‚?´_½쯪ºíòšÚ$y)LK”Iñc20qß³ŠÌ0” y·3 +r{.È”ôaWE±ß‰ pÖ :+‰ ú¶'+¨Ðp>ƒ^öÔáÛk4ÿ¼UÁf2Lq{±ƒ äÃçí Ì($Ô?Ëà½c¼¸äDL’N1Û~/kЙ¾ø7–Áß/–A¢ÝÅÉ".µ’²ôt åD¹¸‡fÈÅ`¹Àéæbäꉠ±CÕx°ûjdˆÕâM<pvNž¡(øýä¤à£=&˜Ô¢ä V‹7–€ƒÒD÷™ á@”NM5ÁDžUGâÀöâðE†va‡-Èûò0M4µñ$‡ÍÀúÑ|'|¶õ,ÅÐØ„L +Ø£©?×{úH+@FLòê HèâðøÌzNžA¢ãþ ÉâÃiKú%ºq6³Fjݳ@J2WÚ.1uÑÔfggÁl*@ˆ`AsÏÎæÁ‡ÆS¹R3 +œ§+÷×,€ÊÅŸ‹‘ü;hÚÂG›À'8¹í=0¯41|!*’Ñ—ÊX³púP[þ¼õ7ã¤%ÊV ÌÑ˪CRÎà8Y¥3‹§3Ùiš­´¦Ùšp!hžŽ®ˆ…!ewL}¡JãŽìˆð ÕƒÖF·*-mÔïÓ`žªµ«¾éÎÁ­yÜ^ë^o_fÚš¥þ³ø"PVNŽ•ª°¿Iúl/È´óçÛ™«¨qºÞIÓ!Ѥ¦—Ï]‚oÞ\RF{$çXPNÄ|eht]L¸œ ‡pvRÜ7Ý·BÕÛ¶+„â0ó@ă½©Q˜e 8/]úÊ$3(¨ô82ƒÔòTÉ$5ù{cø!2ŠLqbRq¢O[BFiü kÂéyÑ®Pï™N‘b¤c¼"5eJ½¨;1A…7…mHBæ™Ùâà»*ƒ¡:„º#`µ®‰br2ž»Eþ–ê NŽ;`‡íI1ª¢£tš•J³CIŽZj·j°¤ËVJ³óúà•Ú$Ò½ü>ÛĘÉK¦f®KU£¤N+Ñ$-Ú'‡QKNÄ@Ç®èÜoßÙÿÎ%omƒªÀè ·4ÅvâQÊ攩—˜Ê¨ð(ê…›ÛÁqÕ0zIÝ@BþpK ¤ØÕ]V»«ƒ„'QœûÉf[£0¨4*"ÑR„’R*þ*$«±×'|vl•±±£* Šú°XiÎGBNäXûPV+&.…Z¦,rrÈZ_Ø›X?UÎõ1¥Šh–üÝÈ TQ¨HÛY‡Ú÷¦Å²‹êÃ3zŸ¿Oß|õ/@ˆ!Gõ¤¼/\›ý2yì¨ p¯Âyìni=³B +Ä>¥ÂlfÛ¾Úÿv¹Ýveõ& ½Œæ"*p$íE^9Ñ?Žªÿ’o!JYüL×l€„©•å„Œ/¿c„ üç<Ùmx¶!TK|ÙÌ(©£î¤]šè  —×äÀÕ'à033<)<5-ƒ^·³oßRæ4ë·ôol€_ÌôÏÀTG{žàÙ^e2ìâëxIÞ³®_™Ïô ã-Ä£æhÎt}ˆšçJ#ËN«ýíôÀBÓÆ-Õ@¶(¤aÒàû,[Vqu¨+Åìä›'µ­ØyuìxØ Äà Æ6È]ìBUáAIyâ ¯öä`ý]‡Úc…B¯8p…`¤Æ¼û?x‚ž»ÒUMÁUô]mëTwŠŒÇFøŽÅ'ûmWúœåc$ªHØS$6œî .šmtÇ,¬K%Ãü¿YI +î¡ÃuÏAñ~¨3ÅS‰ï ´œzO£N±nŽZL1þFyxzŽÛûÓÚgò×Ô5 ÃxÐ{ß×vÕUׄê_oÙó×뵌^¯÷~C8„ù¹Ö0ȧí^Lñ¶2Äp¢`ݪ ŸÃÙŒ2~=ߟÕÓßú ,—^þð'¿øÅ5•ûÇò—¿úÕ÷¿üùOÿøï~ú§ßÿí÷ù«ïÿ +õûNݾÒ[|ð¾úò‡lJ÷ÍëþÃ_ýìW¿ø¥^ù«ŸñèOÿìÿö{k;Ü2Ø<ÿ?þ쇟þÉ÷¿üÎTøOÿ×ï‡Òÿðíü¯¿üÌLøæû?ýöß½ù8Þó‡ßù£¿ÿÕ/þóßþåw?ûù_ÿôÏë¿ø0¢—aß<ü?ÿòÿ0æûÑøÿä—ßÿúgßÿƒ ÿ¿þÝç^ùg¿øÛŸþìç?ýŸ~ù‹ŸÍ6,%±L¨³äáÿÙÏl‰þŸýÕ¯þfü¡#ž™0ýæ?üßÿì¯ÿæWŸÞ·ßÿ—_-ãûøavÙ¶ìgßÿÝë†}v‘þôÿðwÛSù¾úþèËOÿÃÏÿjü%ÿ¹ñŸúŸ~ñó?ùåÏ~þ+Û´¯¿öŸòý_Û0ßüÃWÿéoù—Ëÿå'¿üû¿û›ùžÇ¬ìðÕñòGößÿç?|õ÷ö_Ó^´þÑþÃÿbÿÇÿk?ýÃK}ùß^þ¯ÿûxù+žýÓ¯¾¦¾î“ŠÄù_?Øf"â­÷ùÛ·ï;à±cø훿Ýý6þöçÌÿþ@ÿˆQLÜ“d ÿÕšâA6œ_ +êúÄt0¹,㔲gÞΓÐÝñéMhóôE “7¬~xg¿b¾úeÒU–ÿÌØÿµóþôcYr1uþ8ç¯vAD¥ÿl¸Ç³t3Œ˜ö÷·šE~¿„¯óÕÑŸèœôûI-xÃ*^¤Ço*^}ðÇÕÄG˜’–‡o÷/ùv¼½Ò +°yúÛaÛä?‹¦Ù¤ÝÈ´ÝÈ/þÁ«ãÍäâƤÅaÛÆ8À”˜Ã3=vû¨Íó¬ãçyøøùF-ùfþŒ7Ãö²ï\&:6ï±øìÎãç\ÊøùºÆDN0ÉžiçøñÈ>bŠ“K Üç39¾õøñš?šœ?žu¾@M+þkÊu>J+öø‘<Äø±Ï¿_fð:µ÷/þa윙bóÅÇœØëlUçù›Æõþ×y  ²>Çï'“s/zyô3É­±qÔôÏ{P hñW9~,eü–ìÀo rQ2ïƳÇh!ãYbß/Ûq}ó X¬°~“P~?ŒŸ GT?§ûš¾ˆh[Fr`¼»×|öÛÞûº÷UÆô0"?÷Á|©lH½Ìwç«ŒóNzhÎð,ãF“ y–(yÈóræ™qÍi<hØëˆÏçsüwsOIOÉvÌk@wèøôYãÉ{ˆÍ뺎1 +Q±³í;Œ‚g·#øæ‘?¸CÐ\×ùÈŸ[¨Ç¡Òùȉ³^ói‚öóç«å!­nàOýÍ9ùþê Ù~m ƒ–µçB֔ǞRæÏ¥ÿh3ËL“îü±§q®R£¾B·k¶<òÎü©ÚÇ 7×n¾"©iM7’˜öv`¯#^>8G\S›ZþçoçÓ÷Þæñc¯Žô£‰<÷¬Íµ§hà9D÷uwð¨yìûyL9}”ç×åCꨇ§N9ÀQûaü\jK}L¡£Ä@‹ÚÎòúóòŽùr°çóòV~?“5óŸÐ þc¯¾~•€j?ŠkñÕˆùf¼àŠ%Øæ³7î¼yÿRè5š1éyÁ¯9ÚÇ[SOSRÚÒn§0Ayó:4?ŒŸ –Æ8¸ç=FAÁx úoÏÎsGÐ{<[JŸ‡ñ+Fðͳ§Gºæñ"5÷ô¨í|/lùñvµŽåá¦v÷üÜÙ˸Sôܵs¾€Ö²¹ý)½Êßq§ÈÇiþØÊËv`¯#NmJÕ ê¿ùƒ4[Ÿ1üüG»RSˆÓ¾4~$‹ä? +w>zßóÑc> +»ixé2®W!0%>#»ó#n‡›ªhÌú˜£fýÁRÌgë…ÎÌóÖ9Šö­eS\€-ÞÇ›™ÜãWzG†¡WÏkʸ1çÓÍjò<¦¨°ß¨­ÝCÔçsØÍ7÷S¬ô¡³Ö|÷¬Ù•Žù;²÷œeŽLÐySpÎÓ Šú5W‚!óZ™’ZÕ'® K›)|îÛ×½»ëÐëW¿Ó3ŽðüxšêQý mëëÏï_ò*óÎû» ŠÊ#óîñ#ÙS”yºŸ½.®ûýÇ~?Baq¦EÆeÊ Œ£Rîk +Á~ÍÑ’ê|ä]oóÄç>´PF@Ã* |;ü×+zßÏm½<ó:ž¡Zcü–Ëz¿2HuÞ¯ã|FvYõ‘M¡›Þ,n¦Ow;€92ÄçcMá˜þ0>æÉÃ$·ßz×5¼ò˜C©âHž?Ç'Ï9 +bž[ššÿùãeP¯ç£·)§*&Ó\GE“}Ï…¤Én<‹ò?>ó*ùN¬¹NCƒ‚Ú±Åm®n¹?sDNa~ø·¦ò;Éõvü¯ +ñ~ &Y\S!R:¬Ò<6¸Ø]›s8Ž<•Ü9l>|Íé/ñó=­¾{\yH{ËPÕ'ôí¾y–ÜÔÅ|1Îñst#úhîŒ +ª¥Œ³ rñ£÷èÇòºdf¦Í©ájÏ+ü~½ïGðÙ¡¥ÇH-”ËÙ楸Ûo> -gŸ‘Í©ÍxÊo¸–íùo®Mwý¨í7Oê +¿@72¢<×ò>&ucî¤l+w\aÈv?Â~<î<ŸD£ûÐ`+IãYÄ»û9õЙËg®õ#ÚYoìÔìû½ÏëG­óó‚6üÍóqmWõÃtªˆÌL¡×¼°³ÖçïE>PÝiÒ®”s„õ?žÃ;¹Ý¯3hŽqJÅ_ÐÁŽ[ª.)#Ä¥¯~½ +§e¾?“ÊÓÏÏ­B äÓ¤ûe»·sÓ 3Í#¢XÂãçêM>òmÛÔ­Ô·G-Úþ:4‡NÙ¸`#ÃLUá³—ãœsÙRž{Ù¦ +Kåz~ŒBœG<æ–_‡õ~ß>"î,íqÑK{Dœ¹ãù³o¢R­u.Wy,9NI}µùµdú¹ŒX™ýèð•:Óô]û4tL³ãÌÏDÒc5ܯþå±ãLçÇ“ ñ3ç–¯×\çs¹§2o%RäœoÝ(µO“üQãöÒû‘-M·ç#)ÿ ö¸æd§»Éo÷œ@¿Ÿïûu ûý¼ö‰[¼û9Ùÿx^ñ¸ +GÚýXÛë{{~á3°Þñ|÷³_çcŸÜçë u›†°à[oÝ’9ÙçCýjoÖe¾õ1…Ê›+’Ñañp¦×…¡˜wJýü¬l¿æmHwþÌÆoõ|þk(ײַY¿×gœžg[ÙìÊfc?ù™A¹§ >Î)Nt¶·wöMgØ—7íÕ ˜Jé b›ü·ÜËó£_„K#˜^G:ƒˆW?ëš²ÔÀ—í÷_VÆJR¶Wïg`ehâ•u¾yzê׫!¥$K?ÖyG‰}€/xEž2ÿo¹ŸÚ‘ژŀ›lñ½c±Î{sù‡:u4?{x(Úe¾?‚BÊæ?æ9¸™n‹ï±ª>á6.<‰J;“Úv°R%û â·þ„ñZùÿ˜{Ï_I’$Oì;þ´b¦K<™ùRg†÷”‘Zkýòiýªê•êªÖÓÝ£vvfyw³ä’¿’ @àŸH37Ϭ®ê! $±µ=ÝV‘îæf?“."^mÏ ¥¹BbX¥5NÃb;4Ú[×Ùùä¼ä„À|–+e†‹íaˆ,} Ì÷µúD›“4¦’è0gäºBªô z´èQKâ6økü +Ÿhòµˆeê÷¢¶ÇÔR’ßšù}¶?ùë/“!QØMÛ$fs8FtÌ»t˜¿”¢jPÀ ›RBÀœ œ®)ùýñ¸ò… [^§X*"…ç|@ïH²):iùdŸ]Ù¡iŽtž~<¬|¦ü¯Ñ€ruÙ2)R.RÀ}¢£‘CS\—àAðQ#“à +K-€/SÎÂÏ`}î(pälr°D™Î–DèØÿPÚÈ^|B'ZWó&X™ÆU.UG$šë$°üx´höFK_ä¢@t”ájù'É÷1Щsu"\Sp…ŒQA˜™Ä¦Ñ)Ï„þÍUç[b<Ëw1á¨/ + âì|òüoãxë‚Á¥Äünñm‚l1%GEDK]ÑS‰L8 ÑG­Htd€Q}A%ä³zÔ¡G%ìûÄäÌ°‘E0l3âÏ [ÀºÜuåÈ@Ô•ÊœøDÍä¸b¯`fHVF…´ÔÅÊåìøµOÌ€ÑXà¤t˜ +àqDíä>Â¥|TRÐÇû©—?9$)W¼ >Ç4Hu3å—CQ!-F—!~ ÿ ŠM“§k'6ë£l—|ÇBnJ‹Q4yýä ä*Xè±w¤hëû@dW77ÈD$¬ª˜r„–Ä2 `ª¤ïI9=ëJóÑ ÊjƒÖ@Ëß×;¤LO93 º}äÍá*ô/#á´I†rjiÒåê¸mŽù³×¯ç¥«DÑ—’æ¥ÉÝD§âMsI…‚·$(à¶n¨Ýä qÍ=K¸€™~þÂÇ3XOÍV‰1Xœ¦¦f­Å‘iÒò˜.¥y`Ýßß^3€ì6=«ˆ2.pTÝùÛÃUb—9+•¼".EúäHFÅ de¸XŠøI6eõ}Ó‰ºoqõ½H4ui`U£ï‹Ñ¥%&‰¾ ‰—<‚~ØñI\ਘˆ k6‘}FC¢Ïì¢ÚÏ·y‘hs;eÀ̲¦ðSµèGkægwÅ?ž—GXkë{øîº\‚c¿¯#AäU[™‹z4›ÿíræó*6ðæÒ²ûøu8è{ °‰!#m‚ÈîA’¥é‰e“(G:’ì¾a \ãb2XêàÇLOIÿ;ŽëƒK‘ãXþô»¿íÊðAÓñÁ85—§FÙRØŽÁXºîûÓ;’ìj¤ü ×ç$†|VT–úD¦iÒz0-r»ù5R%sîcn,Èõ{ˆKibùp7ÐXVæ•íÐN3‚WÒ)€¶Žíú?e0IÔ¤ÞAÿ?ÙEknuý%ÑëN@9ÜM™Å€¿—ÔiëSÆ’4ï°zÃÚú#L4 ZŸ$2ÊcÄUÐüx–Ë|&ˆëo`ä@ØX0W41¡ßë~‚þžÑ“–Kž WåÍá°6Ùº˜o³ÛÒ¹"^F»#2½Ì–㊾Î4®é‡”‘k-N˜~RÎAr­‹­—5ÉÊ"6ã€îâ;äÛŽ¤Jÿ?Ë|Ÿ"6´¶‰Ç#€kÈÀÏÈa±´X@mÀth +®E–&«QuÚH…j^ºnèÄ +º%_&”ŸOÄ¢^Ÿ(ý§þåƒLÍ@¤ñexÉ–OÄV±Pu]eÐIXÿæú„¡³¶›ü¥±7>ÂÖ|HD¹ +ºïˆó‰¶cËÙ2Å 6¥ +ý\Æ¡D»_§¸rF¢Y¡ÿS‹ò÷p\?yÒŦ þxزJ®4Xf\³è%ÎzéCAÐä¡3“¢öÃVŠf×T¾ê`‘³.—Ðþ³"ÃPn•oÐâ>ŒÀ15®p™’&XÐ!²ï›çÓ÷«º* _&ažèÌHÖó:Þ„&\zÖ‘ ø¬AœAñLW¥†¡þµíõÀ‘¶Ô/¶ä <´à”ðåbKg:žzÓü°R9ÇN¸eЊII~¢FD®"n.f’à<êý+ +Å.X>@Ãû²ü÷ÃL™I¾×_vAØ2%KhwKA©SœœDŸryødWAp[zƒÐ[Á)ºG’ý>&AϧFü.úé2”ˆ×gÈy¡@Ԥϒ»: a0FFõøE_Ž) :Ìðµi^”¶Å(‰ÇtɧŒíÔïÁ V¾ ]>Jöó£±rLilàM©®³ö%©ø¤¾vY2Æ‹ØÜ¡…ý´/K*1¬/£Åºr_ðªv"*i.# ÀtN¹A¸¦&GÅþ´³Ò¨áØ›~ýŽô1ó¤d8ü½FjJã&­ ôQ Ý%î†dDZét)oœ-…¦&.º–«"ÃVŽ9_ï ñéÌ+Æà„,EÚ¸Ov7`³¸¼Ï';¿Æ÷IÇL€2$b¤ŒËEðË'‚Uòˆ~oQP_à(?ª(z¢$Ïtµý²a¨•µÈ! Tªd/SeÕJKÛ'Rz_ëYÇÜ|XO$u4"2[§A‹¾Jd©Ñ¦,·áë%ÄÆ2t±›$ŠHÑKå%¢«ºEêÇ¡DixZ +$diŠcw‡€ 2>‘¡üfeiò`+4Ér†£~o“AhISÕÑ(¯ O˜ïeHS +; ù¢¯Œ~Ž­ò(®È ²Æt‹ì+¼TìoÛ”Ò‘¤kÊcd£µf{Iäl3ʾôõïלhqz¿¡$*¹ìñAƒv†(6ôqIJÜו6 +IgÃ.÷i†4SDë?Ký^fƒn€f|ÖRV§TtxùˆMòlmiažµuK=kÚW Ê'Å]Oþ9T¸~ ˆÿYxÖ;’l˼® èޢĆáÃcÑQ!a“›j\Ãw6‰SçÿÞIÚ\ÉÛO¼Ãû52ÒK­ŒC!dXF‰UÒÔa^‡„¸Nj™Ñ‚¡ˆáDt1aíåäBF"X´j}j‹{K(dbR´×¶TŽhEfÉguÓ’xÃñ½uØ­F:ó]Ÿ‹ýß›¤j0Dê,‘ g\ú=îÈg×±tMB1l&i*Ñçø϶6$2ö_T“5Ô ¹ƒ>À¢\CìuJ£ê$é¹Ê6¶MªÛ#NŽªÌ{KJ9 š><}duK ƒIò{‹ŸîÏà;vòåœ ú‘|ÑRÈ$ƒ—‘ª°M*æ(ÙTÃâ5H±ùQ\E—T+eà퇦AK™¶6´$Ö¥[/~t#H‰6aoW§Øë(—É3“ %»ÒÇ…­0™ñ{t ?€ð4 Óú )ªƒÚF~n´:x!7,ÂwêÛ2é¯}ó}›º¾^®µ›=*ä’m[I®S.#Žj¿ž¹4(yAïUN›¸ò²©2»ª[„½…{Ý'š6El-•Ÿˆ´\Ú1Mêqq9 gr[à¨j L‡ò)l(™|™¬+B'¨¿¹H\kl×] ò"}mZêY—p“%™Ö}Š‰i +~#Yf°pÁ~Ö”ª¾¨µ„ “ÓÉ#4€¥ŠS5…© _ X¨/Öø„Ò¡-Fþ÷~…rˆR!)ãÞZWÝ1¿†‚¢ ºÊ4¡2$jdÅÉÊÑOAŒ‰]¶ü½+!‡…a(G~•&Ëí0`¨ Ñ +ÚgÙð9ÙPdo¨uŒRv¥XîÂÈ'â­ªR³R`ÇRLŒûkªßSUWÅ\î”™hâ¦bš€£“ÔŽ]À–©0\¹2,…ZŬ|¿—µÎ'ä>`ó0É⯟N“•E¯k³¿Õ%{D¸ŒÔ¸¦ÜlYňÁo]a3™Ó­Ul›’79¹ƒ°ãœMåÙTOd™ª¨¡‡úm©¤“ ÷*êô¬ã'Y˜7¤Q6›å»b,]Õ/:¾Ü-U Îd€[ß°¢Éú²tUŽG„jdÄ4aõ2f’ÐÑdNËqIÿ2¾þW“Š ì}™´Î&àÚrsLy²±ÍŽïlþ ÃRk«kô:ªtµDšÀ–ñ“S܇ʷEþ£T‚½Ó4š€+“p¹ª +Ûpoà¨Á À”ÅUí>JNa-Mp7"pdaøv=-ÍÏéÞu©.RäQlD"­‹‰[jÊ°2ÓlÊc*ÇßUçÆdý5á{¶$Q”}Ë'ÅÕ%>Ùà4€) +Ø$É~l$JbâŠFðÎ&›ÆÄ+ µ W_G’-‡JV¸„É@ÔTªƒ%Dz™ à`n‡Ca¼yÐâ28)ª’}âzÁÄæøD.ÕŸlAP‚Ÿ£‚ݸ¸*-Ãe¾è_Kå1®*£Ç¥µ(<ʸ +äoTžSM%Se XV XÈ:U×3*Ûa èš-ùó—cÇL%„é±-1‚³®…SB˜Á­ ®Ó)ÙP:ªTTÊÿO…ÅéYÊÄÕ C…ÐÊ^ÓeSÄÉUQ‰Iuöb\U -‚¨êì)Û¿À¡,•W +K J’DV§Od.% ˜–Nç†Y”cdÙò`]­ +*°SÕ×áõ^’ÈÖÕÕ*‘GóÓÈU q¥@ßì’¿çTˆ'Òi*ww}·w‡Èå ‰ÆR¬T¶G©úø¤ªuјŰ~äCä€9–ÀTjnû…)>ÑZ·Öò%¸¦2Öø $¸I®,Ü@ ÎÔĤÁg‰œV•¯¨´«±Q2!~Õ!²CÉÞb×;R™;Jë‰ R™t"iÓ~9UËV¥ÏÀ¢¡)΃iû. Ä]¦ªµ5•ÆÁúÖa­Éå–·ý·èLZPÕžåªlLUäf2¥9[è1¶%RÖTµ·ƒWiùD ”D ¹âîûx¤T0ÉlÝáUÚ…²xÈ+Ž¹.Œˆ\³i'\ò¯¡uäRQ·(6 󊪬#›J¯„ƒ‚K£O¦÷"Ñrɇ¢LTñ.ŸŸÑ@Tûh:³¦Fü¼®|Âle“l˜­”!(.~v¤•íªrHìn«ìüõ×R>ªm«õvüœŸ¨+å„ +•[D·(éO\.‰&å¯ ü”øÀ¯â¬“èÅ£TôK Z¼‹T–)SϨ +*Ä“Ê/ã®+X š€ôËþ6h€€å³ŽªE&ç’­êetúCÒ ²ÅT‰$2®|ÖVçÜ"‡Öj©R;UJþË©ÍŒÓPxÃ^Wš9~VU¾«¨!W9—é ò¥­óM™Ál‹'‡Ä°Z|V•€»òçëÌBê€÷:TÙ¾vÅY¢S9=ö®Ö()Ï°Lr<’%ãlÔÙ®où/Ó}<ƒþH¥È¨ÞÍ%Òà ە—sÝfEñ¢—TUR,” ÉõMYú½éR ƒ©éô,6—–_ “ÿ6|·(Ù”÷CsÄÉÔtLø´\Åû€Ù5è# æ—&Ùèúw¨×•ãCt^Û¤ ñål' åªmLfRûäös4JžÂõRÙmH¶e¤0Å qŒ¥/ÆÛ ZW¢ GW—,•[„üeÛÞàò<ââšà1T&³£Ê¦Kuwp%'Šk T_:›'i´‡Ím’É.EÝD¢‡¿‚²šÐ±UÀe‰z¹£ŒoÙtÈ'ÒÎÚ¾«'!â~ëj!ñË´Öµèþ¡uú«ÚyZ?ô|Ñ« +¤L…Ó>â­9¡!Sþ\¾•ˆðRþÞQÅ+Ø[>K‘›Â κéÊ*kýĦ²¤îuŠ”®³Öuü›ìi×ïUCHöÑõußá.i)Ó¦EÅ<`âÔÿT1Dv  “º*é¶ +þR˜q#éÀ¢sAF¾+¼‘*GZå;P:6ž ®H¦ÉÚ>t1¢:€8”A Ù݈¶¸¤zlSÆ']]©W9F]BcâI +àckU‘PôI^{uT&¡Ô7žmKÒ,›Þå¸*íhÝÍÇõÓýaÍÊ-MÆy]Qɘ–ø\s£"[X²e9€«Ž¶K}j0wÇ4©"F¶ùrѳEµqTûèÚå`²Ð–ôœ‹mUŽuycÊàÂ'%q¨ý&I)3ÎPÞ9W\ÐJpZuº¶*FXk_×öœ,i?p5€#e´íc#9€R^$ó\G%õ¢Ô×3pÒª–ôº¨ +Õ¡¥T'GuŠ°7J ñöX,UânT€Pí…»Îú@ýO—©¼dÇ/ô‰ëÊq‘ŸèmY1ÌA"Âeª“˜«ªÌ]î§Ûúë%£nLÛPö"9I À´ ˈiE0DþØÄ/VÆ`F§‚T°À0Á•ÜMšá;ý˜®òµQÚòaV«AÉäTÂÌ–†ôq"Qu\rU YŽ¦js‘¬ÊÑDØ—ÈŒð¤2M¼JF U¼ºZ)«ÇMªJffoDÆ ÿ41[E%-øömU ÏÈJgÎr¦ O‹õów7 +U×nG Ë+Bñe±ZK'@AQÆT“ˆu·ÆU¤·œ„’•­‚×,гÊR¡ã„Wó˜Ö¦fðu¥‹Kõä+Y€†LSá¥xàèÄóþH_Xå¿ÂÜ FŽU/ð"ß+:9¶2¤¤³Uƒ|VúEЋéÇÒ8†{ õÉÌ& Q P×o2¡~o“½T:’Ì´µÖó]ÜÂ$bªs‘íB‘¨‘\^'p[Uº»ÊÆ‹Y• eK'g +³Q#C–³&«ëÛê×oêEdËQ¯“š“cõžéüœ«:s¹çeÖ ¥v‘x%ixM?¸œŽ0„ñç\Ó(ÓuÄÁ9­}”£B jüŠítõ{Bh’sŠ‹ú2zštäÓë65Â[-Hej~š]“N³¦ËÄ".®hS}j}­‹Ï2NöžlŽ$žT2IXíjÜ‘2½K-Eô]2HÔ×£êÚz¦ÚßEG’e†˜ØD)ëÌu®”æl`i$ ®^ÇU¸W†ËÏüg- +².fEY +Â2ruId–¦Ô›EÛ³v¸²×ÖU®Ÿ‚DÄ$¾LJMå¾ M&ŒMjõØÑ}Ç#ëɨ­€±ñ{JÈs†Eš«”ˆëßÅèFÍ"«™û!ÕNG.Û4Ðý\6$ZªËm©  S— 3Ëd6=Ë5¥µmK]¦8Óÿ1§ìJkŠ„fòˆãÕ’‚¨Sf°ðeHÏ¢ «ö²h‰®BºšßGW<éRÅ n@X@¹îÂfJ ©®:žb­$QuËÏ€…óî÷rð'À9“ÚÞ'™7‚ĵa!³‰'UŸƒR/Á,QMj0 ‚ßWœ¾@ö3Xý±m³§š?W^$û»òY¦­,wþ¥JO|Vá‡ÏZdëò¸ QWG€bÀHæS-¥¾æØ-\';Ætý:XC÷c*þ³~h›¥¯í(SLðQצ ™Ádæ >¬Ë¶NŒ¤<íÊ)()1­ úz›)8]¦«@¾Ÿºâ—»Rƒ´ÄpeLפ|ò-"ÙÐ)M7-&Ÿ•eùº&¶.˜±\™8"~¯J4)fuÌ!¡ÉÊÖB\W.!‘tÌhæk-™jÉå‚ëDɧ%'¸$§1ÏÆGnÈt2˜/Ò°,ÅŠ’i(M¥#ɶ©²*ýÒB$®K l¿˜àeK¥Î‘»ÉšA‘Y±Áu‡Z-â³~ç31•%4€¬®ÄéòeæFJ…ßši2€!2ɤ—@=N“Uk«lI‘šÅéš:%% +— `¨rý¾IÖ¸ÊëÁf*L§Ì$K“ fÀ‘&¦‘± A”¦ûÊŽd•ŒaŠ +Û‘ÏJ‡¨5·U­ùÏËÂIØÙ’,rëH²ÒŠsÎH‚ºªbïÙ¢ßËj]Qž!- .&Ió’—\$þ¨Â!m…QkkJ§>’¥{Q¤i[¤ò]æ¾ñêÙ:·Ór¡ä~[.‘ ¼ÖŒRþà6†äS‚®Là^R)—l#àý =BãÊ«"MÎß2¿/%6ú>¡Ûu*;aú1H•!ΓkÈß;j²Ò$G¢¦Ê+D$ÊÀ"ˆ„óÒ ÂB°£¬-Ç• …Dœr‘¨3JWrAKùšÔ@ƒÓÄ(å‰Û\þÜ4h DN™ÿ{“´¢¦Ì×n¯(âKyŠHÃûU%Ë[¤ñ ™”9ìú&º.€“Uq8EœpR$ÀñY©1íÄ —Qì’ûxSÎ@ªÁ¤J1NùIÂç)ÿÓ!$¦‘«Eä(*~Ó©] B¾6±iaD¾&Õ(;CÀl“rý¹¯ÑxPcššðe"QW (’™Z+Ù`Z\ÝLD©X‘H=ŠPò’Ò4êQ$…¹¶Ÿ¸C«Eé®h?1ZW?‘cŽÊpsÈ6œM™‹ŽÁÈüÒUö¾(lL|ºÐÛ'k*³VD ä¨eÃIá ìB•”ªÒ„µHz•*ùqþ*ÇÔ6m¬ÌÔç•ò)96³§äbj¤‡_À•Ì!CœûAIÿI´½/Õ¨Â]Z¢˜–R–eE€¸8’òÆ­µ%ÎÈA©Ë»Ž$ƒ!¾YžâÒLÈD.îRSi‘TOîn´UiI¶mÒ_Ž¬.á.]ƒ Ò2M„;J#éí ¹«ð‰î÷¸õŸ•Ñ) ý N²Xä$ú¿—M ýÍ•W?pÇ÷$‹A)YˆÛþÏ|æ²µ‡ÁQ²ÈöÍ@6ÌÊo‚ÁE>’ª‘§ú/®z|ŠÂQÉ^6ùÛp¹l5€ŒH‹’WrVq‹|B~î9£Ö-`d·6êv]•Ï-æt:ùH´ƒÒÅÜ" BÈÃPè*á`6ù @J:T¢K=¾€¸!{]É‹&5¬%qP–XªD„‘Z>$“$•°¯‰¬ëE§ÛA¸jc,ÒÇý½40ÿ“´%÷óä¸A×ЈúcmíšÒm:QT†3•ÍLIK@”Ñ4B¿eé\3¨¥thtÚQáLs’¾>M•à-àªzˆÉŠXŽQ‡ƒF”])Å&(Ÿ¾´©–Ÿë +ªA‚(±ófe:~•¬×ý« |¢¦˜–â±x ºoö "¹¦Ú/ˆÛèe21I4Tð“ÿptÕ|A£ôVŽØG•„É52èÞ@>».ï©>è5µÕ[”ñë–pRµh”ä&Î#-S¨^Eïʨ¾C“¬…D‡äu€ÄYòlí vlÚC*lgœ’.6Ž3S¶™.oæð‰TƇ[Gu ŒQ;YÝP•èLÅ97J¨ÑŸíD¥" §Ût˜ +ÿ +Þ.yô£¨>ŒÚ1‘ïF²KvbêªüY1È°2_§P-0R t…(T¥¨µt7ŠSÁQA¨ã‰ºËÂ`…*Y§QT Ó‘¡ .ÔšÄ!F²®ÊÐý@”èAB‚K¸Y}"éJÁåò×&Vç*Ð*—ÅDX®!p"þ¬‰ v¡Ñw*2&C.bh²GŽ+Ú /›:õtp R°ûƒN¥7Ü^÷y°Õ³dógW%ùêÕüM€zò¹*‹ûlþ ÉÌ/'–Q4]‹áZ*À®T´×Öˆ\‰ßT%"ö-•bð³ P_8¥6)™j¦¼eh#m^öY—D•"fnäFZ¦jnkQ‚½i’7Žúí~ü~œ˜æo¨µn_êw¸{ ªÊ¡2HA¤P“ØnI´Uö™B’Hæ£N?˜%jÐ¥#ª2D#‰%R”—ˆìS"«Ä{W…]UŸÀŸÏͬÂ&Q ¿tC‰0jJdVÁD|ØÐ(+Œ|$€¡íÚ‡ÅWF§´¢˜+¡ÙÊgŽj{)½êåZ‚HU‚¢¨>ñ·JýeJ!>+£é‚¨Êï-Õ@ág3Øh#©º¿˜Ô•IPI›‹ô;I´©¡ˆôMÿ²ÄVX‚ôLåÀ Qnõ§ÚM~jV²‰réd"€m/[Y¨¸^KÎ@“_ڪ헊Î,U‘(»â㌜—2þøýÔ Q?]uç¬/åq7çEî?U¸LE¿%¾7?»”NKÝî–MD¹ÿÆ¡ œ˜¼‚Ït7.‘’“>ž×º[¾tVjÌ·sT·|<ææÖ¦*.q\ÙˆÜBIDus’ +Çš|£Û¾åª¦²M¥ÚÔ2Uîøñ¼Ö+l¨Û,Ò§–Žc‘þB+)ëë1Xãʺé[·>ÌsA²ÊγèÎ g#šIU ëTÊmÒÕ)TÖ'ÊHTº”nñgÓ§ïrTš»ë‡[¨2U¹*MkQd.¹«˜o_p(IÄrUq†²”@-1~Dák‹Zâ}4ƒuc?¦²mÕý \5:pTëZ$rJàwMª7 –#¶oÒ”åïe#qɘL}×T¢‹CÄ^¿fjëÊüÃAŒ ÛÔý\$ÅÐQ[w)·­5õgctÖí«®n«¦™”\#Ë]©º‰:ô3Lð¡ +GWU™²~PôǤÓb¨ +|,ÁQ³£äi1ª}Ó UUóá¼Êë.ŸLr×'«²CUª&£²c}w‡í¬'Fî(¦ÛqP‹¬Nrà_¦D¢ê\/Äíßñ€«Nìl#ì+Z t>=i“:ÕQKúI–&#îˆNÃXkW.SåáX¾©Ê_©jë=4jva«´3|§^ó–lq†7Ò«zDëøäÄóªXIv¬xd]5Á¡ ú(o)àþ… +>Q–ô£;—T¥ePªBî|úmë…£ÊmÙJˆÎTLJ9´ÔµFÂû¨jFe!Z¡uWÕ}X5®N“øÙ ÊJ»sNu¯B’vgŒö”qµ§"€)…!sÉ(ûxŽj–Î,*¤ù¢Ô,Ý4ÈYµQ–±nF–<Ö$ØkÖº¦aíž#Ÿè˜Ô¬Â•Î¬SÐÉx±c]–Á Õ Nwè]âNiè4«Ÿ}ÁºüÏF¦O³TTFUkX +ª¶#ò^ijQ&¹(â ~f–ìÄ…DÕ…Ó6~é»ü>ÌúF-¡(}d®Ó…:|_ßá±X}K¸.·°ÍŸ+Z•b%ÙuÛ¯*]“;’±çšÍ!>I\ðÞ9úLÜ“bsÐ º²ÀÆ ï]0ìK³4Û±-°ÀÅ=&p q×AoŠ¸Gűm,t..ëE— 3Ûah?Ûø£ÍÿÖ€#±OÚsXïœgfQƒºKñlÝ›°"ÖNg‹7Yš†øl&ïßéëºØf•Y›äÎgcX`Xb4-L{xƒE,ÖçäXÔü‹s2L;|  GX8¤A;X dbŽhø»9Mœ·ø>ç ?ÜùJn‚(`qEÉ:äzÃ|œ ò 1Um§ÿÿödÏ?â99oá‚t±.†ù2GPE»:EýÍ\ÿþõŸÅ÷]þŒ%eëÙÅëÙ5"Òû%Ñ@^‘ô᳎{Þ‡£®‰Ì`“¬f»uó³>\€Ë¤Á{±‹ˆ\üÏÙX$ŽÖ-€ÝÍÑÔ¡[¿tƒ´žž"®?dc¸õ'o¼ysy8ÞrfÚ=k+ýlÔ5ñƒ÷o’ÕL×£®?èçŸ~ù‹ÂRH¶«Ïµrïÿ!ZÄ%‰b˜>>sEã?ÍWô‚ èÅôÉ^ôBÊáûsEuÈÏ0(^—¡>ÿæ~×W4Fž+Œ¸-’ñ0ZâÙµ@s±$æå‡ÈBX3lYj9Q(Œã®_÷% Ìü¥Y¹¢U¼þáÔ™¼sý™åÏ|²ëEÙRÿ_îÞùg›² +Q|Ù†Nó7Ïð³ÙÅ%1âó­Ür$Ú¾5%ˆîÎ¥Ü}>Æÿ~at¾ã:˜^)X>ˆ"[C<ËäÉ“½ÌFßÓ§fû É„•ûÑ'a݆íüì“0äpëÃOB܃ÖÛÏ> «A°´ñgŸ„!-´þ'¡#ŸÈ"úƒbX¯ið?êãùþ_–ÿÑ”a|ˆ§ôâëÿ[ Jô>°pQ˜)¼£Op´ÒQÛnüEçgaã=%>u=Ì'‰C ¶Ú˜£Žs„ïµOk÷/®Óþ¿ŠO_ý,̵þYªrýåýåõ¨^ÚYô>[!°Ä ño⸫×ï`èV×üíÝÿùÓø7ô ú_¶cÂìtWL >>ôY0(æV9wžÞA·–þÙ¢ù_˜ÅiŒ·Ù®S™»µ¥[ž›Þ,•ŸÛµ3Öº°+‡F~sšVqVì]gZg¬q”Ì£éN”·´ÂÐ(N¬â4•î¤«³\û¸<ºÉ®yûÒm]Xõ³L÷>–îRÙ@‚ÇíŠ]g:§…á}vøŒwo¬æI¢8 +¹ÍÒà<Û9Ö ã\ç¦lÕµì0ß¹¨Ï_ç7Nó8’nkÞ(YèٵɫYÕYeþª¶úºyþÛÜä•V^DsýHºS¾lŸ}W]½«Ìß4¾Ížõ£pºãÔVfyžÈöƒv-¨“¼¡ç:nebWf½ï2¼y˜w#¼ËÌÊ&f•ç¼~d•zqÙ^~U¾ÕJ«ï U¥;Åþu®}që»–©ŸäÚNù0êtBF-™îfëGÝÃ/àZ¶Ï«Gzu;š:í8ï%x/ž¸õ3§qnUOÌÊÑžQ Ù­Dz åÆzn̪Ç0T‚÷MoeÝ€Q†Kd†1Þ±N¶qšo]hùq"= XÍ'ÑìÓhv/U ¦Šz¦kF¬¡Ó åoíÅ3Cöžæ…ü<Êz¦·4òÓdf2ë° võ0žîîųQ»Ô«{ /™Øå#Ó;t+'éæE¦y:;ÿº1¿ØåTnœi]Ç÷ÙîyØ®‡5xEßð`¹Î´Ò2ävCVí–‡wÅîU†·ný0–í…ÜF*7äÍ‹tëÒ)NJÝ££Ô‹neá ^¤;÷VýÂn\Z•«ZÞ8bUô°0¼kYœ¾ÌöaO¢yئéó¯þmxñU*?Håùþ³öé懲oìÚi<7Ž³vŒµ"n#â6aôâ þ*‘›$²ã YÛK•`ÚQÌ?uj‹d~¤—ùþ]óè»ñå§oþœ*õ£™f¶{YY¾ë^ý¾ºúÚn_Ù3V;¾}û¯ËŠ{ãÓ( ž¯ÿÔ:ùÑn^ÙÍ‹d~ +™H÷´ìš•íx>â´¼±Ïí% +‰Lß,-`µõêa¢0MfÉü,U\ê圣th×Ï"¼ EÜvÄm…¬Ú^²s›fa˜Ìt"N=dTcV3eÔ€‘ô̸¿xÛš½¾Úd‚©Òn,÷4ÄŒj¸¨´bK½x¬å—ÙÆm¾us`Ö¼›Ê­Òx •‡ÏŸ³úyqð<™<Š¸¿>°€3Óµc¯s]h]%3½'a~ UcNçi$ý(hîF‡wœò‘[=6s£íh¦‘Êô÷S^Ð(à +DYOkDœpiÒ›9íëT~6J1§± ‹Ïk™auò¦>{W|Á맭é«þò?Ÿ¿ý§¿ÛŽåz7ë߶Ͽu[g!»!Ñ ÆºA«ºoTì&Bÿ*ß9· +#V˜^¾úKapyà6v4ÏöfÍÕ·µÃ¯­âÄÈw´’–é{WFq‘,Lãù™U?÷ÏË/²­ó$oy³ÖÑ…ñU_ØõcØ_Þ¾*O^=ÿcit§ånyQ¿ô¦oìúy"3IK¤å1ÞÚIæz x „›Q\&ss»|¢ævéHËBvÕÈœê4žë¥ŠÓòôõôÙ¿_<ü»Ñ݉bß®L ƒ«Üè&3ºµšçåùûñÕï;G_.o~l­ÞDxøÇÜìÊônõʱո6pØIº~’Ê€{÷õʾ^Õ½9ügج= ó@ª¤çÇÀɧ¹Ï:±Â"U<1Êg…É[­2ßÓ‹°Î¹þ³ÓÚÓ*Ob…Ý„:õ8oƒ˜‚&ÒÝ˜Ý &¼½X~?áXˆ;m¯už®íÄrÀT@ß +§w¢ÙíX&ì´ôâ"Û½gÍ[Þ¸.ö_zÝgQ·ë” Ù@>$³«|b”Žs½gåWFiþ(ìì$r AãX¥ˆ80í(H<·û(`? +Xš—b«x¤y‡16ˆ»=§|š«Ÿ‚¬ÈTç{©ÂNª0ÛFa š*Ÿ&sC#×Oez[6*zvÄ«Çð€å-yå¨'ôò'Øâ¿ß‰§Û'µÙ0Xäž·Œç&©Â4hVAòGÄ:¥YÒÇr RFÅÖÙɳ߃pm¨{V9J·®RÞ|Ϭ­šY˜:¥áM";`Í#£²0kGZqaWŽ’ÙaĪÕ'φWßÇ·Õém~tm×W™îe~xÿ´@óz³ÎâM~p¹«{£¢e'ÙöµY:Ô=8Œ“»f­(¨?§´›6dµ‹\÷ž7@k¥òc«4…Å«tûŒ5Oóƒûòâ]~òB«,*³W•éótçÌ\:ãæñ7Ç/ÿ:¼þ½Qž¸Í»ßõi<·oT +Ý»ÎÉå×ùÞˆwË›ØÑ +Õ]­t`ÖAAÄÓ}£0…/­ËO’Å•^:eÛÊü;@ñL¿6y•íßÌúnª¼—ªZÅ…Q˜ GeºZnr&…kÒ؉d"F-lTƒIo7žO¦ûfa2»‰â~ª2ê FýT1–î³úéۚͳ²@'Â"œ$3PˆÀ6!Ö†¿Õ¼¹S¿©"ÕŒ0v”u€ânÛ̵Ìài8Ô*„÷$è&Ø–,ˆ¾¨Ûg¥e}x[î]Kï¥ÊA£Ð+a»©Ó‡Êô7¸¸(÷öùÝxöéµæva2\½ËuoR¥Ã ~W6ê–@>»Õ#P”ÙÞ3 ' s­0͵.ÒÕyÌAÔ6ÊKÖºò†•Ñ=¯jÙ^©s9¾ú]27Ž²V<ݳ«KV?〪Gp¸ +½ÛÆòuÒm¥¼x?Î4Î9ˆåÁ ÀÎìå¿üõ½þö¯ñâ žïÙÍÓt˜íuëðËîÉ×n㾎XŒõtï·îò÷ùá›\ïE¦{ g']žú7!Þef}­0Ëwo³½;Þ¹iÌ^ß|ñÆß°†U^–'¯:§ßwÎìœÿPÝÕGÏ~ÿÏÿCqq»kUAÕjÅ•V:Ì4/ +íK^;B”bÖ`»íòQ„õáOÔñÒ×à´¦ÂV@ƒÏé%k½uÉÌÔk_eZÇãÙ½d%Æ'é΋ÂðU¾ÿ‚7/S…p +Øš@2¿Kï'sVÕò欼båC-=ŒØ­°ÕÍLyQ§ ºo/YÚŽ{A½ÔjÀZqÖ ;ÍÝT~+–þÕnj;– ¤*aÔhØ5™…©$ë>Md÷ŒL˜6×¹sk§fq Ë'Ð2,~ÄiœÙŠ¤÷âÞ^¼µ:ffÖUS¼·åŸÍ'¡4¨-¤1 Íî$ +I{¬Ò« ‰Rù ð-o]«8•c3»¿af̆_ w5ïiÒ{š(¹åÕìâ§ýÈÌ!`í04o™í4Ë«Ló,×¹LÃAè\Õ§/G'_zWñÜÀ®,í +H°Ëêü]ûôûÜà¶:{øúÿ¹qüf+UdÁiê\b‰ +œ  –À¿ïk¥ïðÆ)o]€œ·AÚxK«|À6b?UØOæô:À§rb³}øU¦u»|’Ì‚xŸÆ@=¦®œ¸€º Ë8n'òO#î^"{ £NÃò¦9•#^^eëçq>wUvw¥ Q‘²Ÿ,í§JqÖñx`Õw¹­'æ´ÉÊV$¿÷3ðú9ü¶ ˜Uûzaa˜³]^­l?7½©[><0A­)”,Áü£v@×~²x*ƒ–|r@mGA +ÁÈøO˜CÄnÿØÞԓ׿KfûïâÜ,ÁÚ.µÜð@+¯Â$Ü(™îl%òpÍÒÜ­ÌA8ï%óh‚,La/2­Ûöñƒ«?zƒg¬<ÏUgG7¿_}êÏ©¹õ£\÷¼6¹ï¿Ïõ¯~Úg¹Ö `¼úôueòEeò®0xåߘÕ´tiÑ;ywàÖ[¥ÃÞéÓÛ?,þyöðO|pÆæäú·åù›pv”ðffeU™½i¬¾c-Ó¹)eÛg Ù#¬ ¢Ï./+ÓWå—³›? dçzùDQž³ÚkÞå{7fù´',,oœì×½iuözvÿ§Òüµ7yÞ>yŸ,Œ÷šîÍ€ýÀÙ3Ëa·h×NÁâËk'^àÅY}ò\/Nw’yÐMFñ¤«%ìÞ.¬$z’~™¥#W3@ø0•Û~Ëï$K!· ¯€ÿŒ¹ƒ8›ìkµ}½ Èg'–àöÚöæ…æE{þEº~vº ûÜžQ˜ù Ö¤Û¿›ÈƒÒŒ ô ¯5-;:Ða£±@-‚i Fâ$„ÏôÝÚ pXvà„|çŠU…FÝ–Uœç;×°†‰t?éè•ëï'JÀ ÇÔí'Š ¬Òl+šå{`TàÏ^di ^”ÊŒ@8¨32ÃlídO/?ð§Îv(uZ Ž3P»;)-‘nm…­ØŠ‚8gÁ-ƒü<Í´.2í ½4od…úñøì›âø´^¾wkÕµÒÐr¡»r«0í6ú +foôüo Qy•ÈMƒN/™Ÿ²úIuô<ۻأ8vj‡ùÞoòÎ…V]%KóÊìõâùŸËóW`^Y3³z”ëß'¯sÇL÷Ìmx>Â;À]À!±t7Ó¹öÆoÓ½zý¬_ÞXõdnÄ›§Fi +Æ2`]¤©Ü¬8xex“°UÍ´N-°à¼‰V^h•C°yÁb-öŸ5ï"éN˜5ãÙ^<Û‡?°wÛÉò®V •©ÕF÷q8†aÛã[•%Ý¡T5”*'Yvg+ä„ŒŠU\åz/3­{V9±ò‹žâ À˜§Ñôã°ûù¶ÀFXÄè…%È–½”ö,p˜À°æéú1#!»¶[`8°ê‰“Ìös•e‚µ>è;± °\*ru”ÌNè§ø W?ŽZÕ_í&wc™@ +$ í©]>À +7ß¾Îwn¢¬ :03‰Z~ +XlÀ^W€PÚ +»lr»q/îö“|ˆr2 r²Ô¼­hfLŒ„Âí X·Öñ`ñêþõù4–ù‡]m'š1³“ åÖa2]ÇÉ$½ˆ]6ÿaë`7dX㎠Ì0‘í‚©4Š`éƒ8Í·.Š«\ç<šÆ#“í\éÕžQHæÚvi”mfÛ§Õù›\ÿ6(—X~x>‘Y•£êôMmö*Ý< Úµ=ÝK†)oN·Á" pQûô§Øîu¢0¬Lʳ×ÈE™k^ÔWßw¯þ˜?è•%õâ,YY#ݹñ¦ïs“÷àœÊ*šî…Ý6HËâøyÀ€]¥€æRLÈÙ^ª¨çÆùî H§D–7Ϫ‡_Æ/átè`.yÀÿ\ç"Û¿=৉

ä¹+hõ„ÛI¤;ZnÀªKü§7™ÝüÆ©-‡Ó»ÉÂ.lk;µãîÉwxŠíËÛoþëm=Ô‹Z¦o‚ó¦éú +X+ß½._]ý¯¼{¾«•Cvl@«¸,_-_üµ{ñ“VBÔTš½ŒæúaÞàç4N ÃçíÓßTß5Gwoøåù³_Øz /ØqÀŠùî½]=5<ÿ.š›  Ö= s¯{ãõoA“:ùÉá³ß™µÑ¯öãð)o"Ô­.ïÿ0ñgÞ½,µÏŽ_ü.蔽Ÿ|4ÍÂfú¥Ò¿…ÇÄ,€1ØyÔ·£,˜Ê'„] Ü®€£*¥ÑM˜ÊÊŸ­( ±¦‘Ÿg×`j5 pá¯æçûú“ô.ÐnkíòQ<ÓfZ;IP£(OÂF¬-pÒn«Ø>iLovtO¸Å†`.í L–£FÍÎK `+7HÆàÐÑA­„Š _Ñž¼7‹ã'at-3à˜ +àà(;ßË5ð/[°•ñÜ~<#ï'ËûZpÚÙ«?ï›Åý(3Y{?‘}b =QŸZÝdnÒ˜¿*ö/vB–áVœ\3n•Ÿ„ÜýTÙ*Nóís0Û&¡i¬•anœo'™Æ +Dún¼p8ÄÈ÷aaAoZùq}ôP½]™ƒO¥Û Þ„3v›Za–¯×9æ ºU«óØ.ÍÁf1‹S-?Låz£Ã—/úO…ámÌüÌ ™|ó¢8|Ö:þÒi¢Ìmî´wì²]ž¶¯€Ù`¦Nãäðê»?ý·ÿûôúÛ­XÖ-ÖEú®VrªWÞðu¾÷¬ºú:Q9N°a¡u +}0F¶ï”Ø©4º¹µ§RȮ֭ÇÒ ½4ÖJÝÍÞþå¿ùߢéÆ£ ¡eڵɭQôòV,/<¥“òèÐèšdºwë©Loð-ê»VµwýÅ?ýçh¶û$šIfú`ئ҃Tfê/dÖk£›ßüÛìæ‡_íú^Œ%anplã°“¹1ðg2?\8êÀj‚¥¶eÀ I·ShžWú×nm¾£Ac‚œ|æÐ N+Sš§KóBã0rýÆð´çSN{zHó’N L6^;jÌ^0†c¾ŸÈ’¹ƒT)(€Ö"h´Bcᵎ@ᗆݧA{/ž‡/Š¸­ QÝ׊B¦ëÍŠ­KÕÿ$ü4¨‡õ’YXÙ¥CÒðÌN„G´RL׳áI8°'x»”ÌMáH> +¹Ûqпe#7ÔÀÏ @$îÄ÷@/' Z¶£eºÙÚª=“éßè¥Y"ÓKàÂÖà‹R¹>0«­rÝ Vcí8••G÷ÕÅK«<³J3·vhAÙÍ+Ãk»~¸kÖ÷´HN0çAÛ‚ÒÜ7ŠfqÂjKXçlÿ&áMÌÊ"Â[»Àê…Iqxß=û¶qö}~ú2UY8 î:hŒ¥=£dV–ÔÍòkÝ&‹«òøMyôàêÀ*ÇÓí¨Sƒu ™­° Ö_ À€Yz±??ж"¶óœ¼j¾mÌn;Ëg”Ê%SƒSc= û©,E¡sËNí$Ê»`‰ƒùÐ Be5°ŒNëÓ·­³ïvMD\p `Ø°½qw¢ 6"Ý<. oÜúI„ubNí Â!½uà€ç:ìö¢|àTÎÊã÷Zq¾Ï…Œ¨*Ø ÓîDXß®]Ô¿9›žH÷‚)oëôr.iV#zÅÈô›ãû {ÑJÀ°¬y€f]x;̺†7…Ó¤gúö’{‘LÊiÇì:èÇm€Ž±Ì~dl5 +‡4ÆV%W‘fcz;’?“, |L@î¥à@±Úã ±MoGÑpŽ€ å½lýì@¯E¯zòä80±NãÛ6ŒOBl/QØEÏ*|Q5ÁNqX%bµ‚©r ¬Õ8 ëÁ:nÀ]Ó¹J×b °k7Û:du<7ŽçæZé¸6ûº{þ»êê+­0¶K3Ö)0@?–ÖÞKzFŒ£°[gÕy¦v˜n^ÔïK“שâl;U4«+«ºyç:7ù²|ôS~úþ*–hÞ\/ÃWv[^õN~Ìžk…EŒh½K +sÛ +¹èâÎLÜÚ…[;O |¬°ÁËZah×—Io¨£ìZ¦²S†Qc*À¦v*`²ÆE¦w¯—qŒƒµ5²+èʃ/+Ì.®XóÊ(,Ì ΠèÓ°–ßzIlw±àIX³´À­^ÜÁ=ÍîF3Ûáô^²f½Tai•ObéI4=1KǼzsë G`­ŠƒÙ æ¶Q\ÆxÏòæ^÷. P?šÖÜvʪ……ˆ^eå“dv’L£NkÆ¡ç§zaáTK½;¯uHäcF#˜ðþq;ñwCÈ(ÐÎ(»-²Lñþn¢[#é= +º`›´j@«‡ÍF}t›©¯àô¬à`ÂÝh8$d€F^¶Go'Ag/–ûéÍÀú+ŒXë*hÔÂv£¼ü6?ù"U>„Í2Š‹ÂèeiþXÑ, 3Sãk7±L?`T™Xˆ¼~ +ê 6ð^"3Š°Al7Ä]+•ÿÌ©¬@Ø%°X‡è²«^&²³¨ÛÞ‹gér¶º2¼>È·°UM·ÏY õuІsïYqô2‚’³šÀh`_/ÌÁ6Ë4 ×wž¦«Y~r`€á°êû `S%XºúsF +ƒzýѾ V9áfûÀꤛ7•ñ»âàÁ)¦«ÇZ®ÿ4ê‚}ºoTLobä¬2/tn¢f5åÔxqº²ŸìûÑ0s #=hÆÇnH«”¯àèí& +`~ÂA6ÛO± ú±Êväv"=ØNxÀ€]Âj$KOÙ­’®{ÃgA ®XåC»¼ŠƒažÊ?‰¥·S¼ÕéƒS]†­z"ÓqÔP0µ2@\^9*4/³íó‹óº^9•YÇPQºqêõn2½»DC*©l?äTwSù½T V"3tzý,‘žœ_üxòðÇ€Sß×+èyÈ ò­ ³ hª³'¾¨;¬7õâq¦yiz3³ó<‰e†¶7þþ_ï¾úk”··c…íXâu+×€ÑÞ™Ù^ܬ¸¥éžQüo\ÅÓ£ ÙŒ¥‡!»½§Õ·“•}½lÚ¡Ü»µ–|(Ó½XLË'+ËD­:ýR¥^Õ0á²ý^53==Óƒ ÚŠçÁ~„eÙNÂjçwaÚz„[2=Q³ç[!¾÷PGƒEl䫃i“Û3Ý{áÀŽHä·S…ØqVuoáÔO÷M0Ü$o$ܺQè`Ûx°Oë“·±ú<Æ#¬›ieÇ ÿCNŽsÄéÆXä¼ÆjN¡²âÀn%s3Þ¼Ê÷îúg?ov €¦õLëlí'aöyÈ~åV ì_0´̦–›ÁÜœ¶©2 5-?®Î^.î~]~æª,Á{€CŒ,ØÑ=^œ¹¹!ÏsõÕ,‚VŽò^ØƸ0ðO®u–ožðúÊwFí§Š†7KfûÀù`F= 00—@RÁŠ;çÅ«D!2SoðÛ‰€6­&ìšfÉân,ÿ«í×;©½h6æt øºò2á6#F9]YlÇùã ¶ãÈf%¤•ÂZ9 H;Y逻¬_mʼn£FÝΌҵ#0$Re^šgks¡Ñ8…W< áöaRM4Úø¯·"w5`Q'Ûµ²½ÄÀä£|ûòi<÷8šÝ7Ñ4ºbD¸ç&×½ŽgF JÀ¬ðÿ÷Oàjv=fV·#Ù@²HÂ.M8}za¤e`ê©ИȎTƒ‘æMD#¥yŒwqÙŠ,Þ€îP¸%J H„w:?Ï÷¬Ú6¨~«î®p;‰Ú30µ ØÛia~—ÛsëGéö™]YlHå†Fé·/xë,g¶u’ÞdûW£@Å€$1‹søƒÑ(½†g~•†qZ!Ö ’Ïào³í »ŒáËtuéuN +½³\ûÂ(¯ÜêÊ­,s­S-ßk±:yÁjG`¦Ù¥ÅžYÝ53VwäõÃk¤²Ý½„§F¯‡Ý¾UZfZ'ÛØåàÝ~/nÅ‹€ä]¤8é­¶/-yë\+M}`=>0Á6Ñrc»|h•Wš·4Š+cFèÿ@óN®~?¹úC¶}oWNA.ÁÁ9M/ÆàÛq/λX€¥“ÑŽUH/9O3«Bë +Ì +ê(ØQô€c£ «a—ç…îE®wõy$&?0!È´ÝH&ætãle㔇©&a§5@‚ÝÇ™¡xV©Ñ3ƒLe5¼ýX6îto…3€‡ãlÒØò&húïµÝ s•rvß.GlÀu‰íÃÉöÝÌÖ +"+³Ž«ƒOÆxb4ö~ÌÁüUKâ£úð'h·@JüÝÓø¯·´§Àðz-ʇ"Ak8Ð-ÎŒlØ/Œöf>'‘d»×µÅëÒøYqxS™¿°ëGnó,ݽzeö²¾zß<ùV+% +ÓTaK÷Áâ@«Í›Å²C³'·ùf×ÁŽ³ªËlç®*†ýÍvAaÝðÚÒëž—F÷¬qÎgvi ø9ß½Z^ÿntö]¡w™D[l²Hó¦éÖ%€d°ÅXuÅ[Ç |RE8û`•§¼ð|¶›éÝh ãìö¾¯‡5Žò"8Ç›Ç/[G_™õC`õP¹·*7‚•—é=­ÖS8¼É|<ÝÉö®20áêѾÑÙ˜ÊOÚI¦}~89ÓQ ÖMÄn$x×(Ìó;øöæ\u5ÞÎ6à¤Þ¡YZ…X7–ùå€úÀ?s à°wYåe +&ìö£iPñõÝxÖ,Œ“ÈÆÝxvràôàÐåº7€ZÃvIЬîÄ1½ËkYù™ž082Åy žE•a·€ »0´TXhùXâ ë‰B̪íDhը݉àÕ-ì™óÆÅÎi •;Ð<^]T§ÏDB)ÌcøäFF¹æ +l« àœtvЮžå£|rn°IïEA®º‚N@¯D1{¶çTËã‡Úì¡>{Èt®â°v=dTöRå0ïƒUËÚ—ÙÞõàò«Êò¬¡ÊìEº{eK oZÇ_ï~_;þ:ݹ4Š=Ó®Ï_°Ö9H »Hcžë]úW¬u\šÀÊ/ò½ËÚá«L÷ØÞUY¼î}Ó>þ¢4}a7N­ÊQeô€=o€ü9…=ÍtÎó½‹áù7éÎY–¹3Ö¼²ª`ã7GWÉY]Fsƒlïøg'U¯€Ï¥–Lƒ•QŠ¹µÒð,•ëº•I¦uìÖOÁŽƒ]¼ÌuoâT§»© ù.nÐuýèufpSž¿l¬Þä†WùÁoz£ËÁõ·ýÛßÆ·ùþE©¶zø}~pm”f  rÝ ˜aqÒà";¸ÌvΧïçÏ~NKz3àÆÚâðUëø‹æÉ—¬{ÇÛwéæ™UÞ ™eÚÕù‹Êì¹7¾O–fø9ƒ[Ö:q›Ç 6s£åÃ/««÷£¯J³W^÷bõðÏÕÃw¡LßmœÖ¿¬¯¾*Íߧ/íúJ/ŽãÙ‡Dd`—×½Ó÷£ëï;'ï{§_LŽæF° ™Î%|,€I°8Ìâ4ì´Ñ™µDnؤ^;•e¦y^Ÿ¿Énxûµ:{Î[GÉ|?ݹÈv/ààx“ç°Pݳ¯›G_T/ÌæÑŽYgGé<íM^À?“ù!ÿCçü«ìø®qô®y Ÿó®0~Ö=ÿª¹zîJ7ý³×ã›o›«WÉÜÈ,-Ð\]‚±\ì_Þ0h‚…R3ؾ0¸Ïön@ÀÛK“gXR& #³¼,žÍnšÜþä¶/@ÔÀé =`Tc¼iÇ¥ÑmÿæÛÑÝGoþtÿÓàcç¼sf×NB.( “tû¢6y¾¸ù©wñ]aü”N„ÎlÙ¾[?tä6@œ6_÷οl®^—†1ÞM‘éÞ€´OaŠÚQyú¢vø2 ü€Ñí‰Uóí°0ºÉŸyÓ‡Úá›áõw“ûŠã‹âè¬yôP[½¬®^6OaïZ'o=à¥þeº{¦—¦ns•î $«¾œ}{ùî?\~ùï{gï˳™þu¦{^™ÞUgϺßÕOßÿ¥4{i‡Ù&ºH¹ÒøÔquõŽõoÝîM÷ê§Êôyqzê ¾ºrøΛ½ÊòÃçÝÕëË÷ÿ¾8{`-Ø8Ðzµåùá=®vç“–[§ ù^Ä­Ú¥Ay|U›ß¶OÞ ®¿o¿K÷¯¬œ”»(ÂŹSE•mUV€¯Ùož"ŒôfBX¡¨o5¼þ±wõ]ïâkøË•éœÀ 2k‡¬sZœ>+NŸ®~Ü›åoÃnÃ(¶Ä” ×ÕÙk´Ý³¯fÏþp÷›ÿ4ºû)?¾iÂœßLî~:üâ_Vïþepó]eö¬µ|vþÅ.Þÿyñâ·ví0Ó>Ë+oú¬2}–뜎Ž¿0‹3Ý›zÃÛÂð.׿m¿_>üq|ÿScùjqÿ}óðE, ZuVž<ë}5¸ýqp÷ë\$ “Úô¹^™ô¼]™4–/+ógƒ«ïO¿üëíoÿ»îõ˯~ø·ÊôE<;/Nß–ço+3ØÄïVoþ]ãøk£¼ÜKy 3oT\tÎÞ×Vo[§_¯ÞüËøÙï·Û«—Gwß1€åX@40K‡¹î]mõU÷úw³)MFç_;õtn}"¢wñÓòá_~÷ßý—ÿåÅwÿúðãõô^Ž›WÚç?±³ã ÎÕ•™s$H3˜sX9wUwuuuNênI­V–GÁV²dÉAc9Ê’­l[–å8Ï÷íÌž³g÷R{Nw©šÞ÷Þçù=$pIÆ›Ž`É_˜÷܉6_˜ËLŸ-­\'¥®žŠé\!3u…kÎp3T݈6¶h©ÛX»;ÚÚÒ³I=wŠe:Öð—\©y:·l¶±PþªÂ¸=ÿ„%Ø8)´vÍ“Bã$“êgº'©‰sÞlßë9£]2>ÈÏÇ;;þÂ"”ݸգ&‚ÐD_'ãÝ@i=TÛ6y€b„Sa^%b|ŽÍôƒÕ5¡²«ŸÈööLTžåË ·x°NŦ <Äòf¸~’Í,›=Ùa .OÇ»lnÎkã¡š3ÒA…’ΓDhÀ*ms¹EXáXû¤Ý—×Ú9#!h,^£;EEºÉÞn¤µªmĺ{âb{õ¸´a°¶^XºT_¿ÖܸV"4¾Ìêµx“ ¢d¬#–×RÓ§“Ó»ñææôÉ[ÉÎ,²Æ)Z½)¨:Of>ÜÞñ—V½É™Xë¤Ò)ªl)#LbXc·zbby¥¸zwqåÊÔö­K¿å¸ØM÷¯ú kF:gfŠŽPËÌÕDlÌâ5±I³¬ŽU™d—Ë-Jk ¼Zw\Ìõxþ215¤%H±’éîQ±¾+>ç¯lÓ©pöÎÊU%hl@VS¡ê‰pý”ÔÛÔ7ú'n-œ{ ”|é~®¾´t±¸x>Ù;YXºRÙ¸Y˜:sõ‘ï åe#eÍPeN$Ñ9•™=[X¹*µ·jó§Û;·P.•énVV¯µ-"Þ“¦÷+›·ÅÎYw¸Ä¥Ž`VCh ìŒ÷˜Ü¢';ªo„›'¨D#Ñ^ö—ˆPÕmšÙkxm:ÞÉÎ\Öôdñ§ØTõ§1>: ² §Ìf£õ“±æ+3± +.©D‡/.úróáÊj²µ>½}£¶tÔÒîËÂÆIíüì~¢µ®mñÅ5‡P/vOçΫ0?”(”D´¹ª®û‹Ëù¹‹Õ…+'.?‘lo+‰ˆ+Ö%¥Y({èg|ŽNoÆÚç\^a„’@âøì´;Þär³T¢Í¦zÑꬂ‹A@Áû3S‘Új ?çò* +n(u÷\áº?;(, þ‚?Û«+žô´‘-ºãíXsU(ôí¾”PY"£U>ÛKõv`ñ™d;Z_9sÏs|®ëŸ9S^½ž˜ÞV׹̬¯ßÆJkÓ'îægÕvÚÌÅkp.gŠ‹gSýsÅ•»!¬QÑiÈ°ÇT(„*¹™»–š¾è+nø*›¾ü"#uÂ…¹xu^nc&m„) Œ&3Ŧûv¡‚ûRRmÅŸžV;"x¤ÃäWÈÔÀy½©©Q =nt\¢Ñ%°±.È&[¤µjž»ôD;°8©*–gÅy.;ªRn¯=ôÜÛ°>*‡ßoæî†G~î +“ë'«K7o?{íé˜èXnv¯¼z#=w ú7¿p¥°x•Œu7÷n_~äJªé]1+V×ssg;'îItv*Ó;wž|-T_…H46ÄÊJ¨¾ÙZ¿±zቩ½§Ùôr©µQëŸA}Y‡0ðb2Öã +©ésw¦Î<‘èîõ–ÏÖfw.»æIöœáº+Òà³³­­ÛÍ“ú²ó ЂP2{Ž@ÁÎaÅ4®¤Ú)ùÒ‹R÷”Þ(qG`gA¯œ‘†;ZO5·ÎÝó\~ú¤‰‘S#­=¡¼–_¼ ‚Ç€ ­t’KÌ@§ëð—hñù~uùbsãzfvjV&›®míÝxeþT+Ý=™êq„›ÈxnÁŸ_¤¯ÔõÄ[2+­‡½U…eŠÊÂ…Õ‹§»§¦–ÎöVÏcþ4üs±²5ŸŸ;·¸ÿÀþ}ωù¹îüî…ÛÏBÙKèÉ8„ k¡|*Þ»”èóD›SK§×v¯ûbE£+€‹U<:EJs\f±´|/“ž×à"mrÅe¨B(öÐéyX7!?wêÆSS[Wv觉JkÛ|eÃí«þòÊÉ{/Þû-Öì\Ž//s¥U¾²•¿8¤1w›s„qËžD/ZÛHÎœ›§™ì²ÙÏ·vÜbNŽ8Q.(ÌÇë©Öz²¹T˜ÞÚ6àN!©lFª'„â*¸˜3R‰UWöîom]×;Åpu,€Ë/HÓgµMRšv‡ëë»÷ì\{Ì.Ó9‰`ÓœrÇšln&9µ,/5ú»/¾ñóLoÇÉg›Kûg-®ß¬¯ß˜Ù} ¸|“ꛧnm_xÜ x^4\Ý–V"µõôìÙXç.V–OÞh.ž±x¥`i)ÔØôfæØÌ\¬±Ýܼê_ñ$¦+ƒ+ÇR‡ÊWÌ\Ùä…× +„T#Qž7¸CN±DA ÕÁ +—šMMò¼LLŸ£´2qg¸bâòC~¾ÊçVä¨Oƒù}R› WÓ½ùþWX0sE™Š–O]ÆÊ 3Å…ó‰Î.•šÅ…,‡°?…I„‘#¬dpx9Ä—¡öWÄÚ–C¬M˜½Z\ÄØ-è0…Õƒøò6¾™ÂÂ@F«âÁʘީǼt8ç‘j© x±ˆ7øÜo¹ø´T˜m®^âr3ˆ?‹øs¸P&#mg¨ãIÍC5Â6Aq† +ó±æV¬¹moÛ‚eµK$ÅŸ™R;X!Y3uê¡Hó¤S¬é B™Š™=&³Éôê‰3ñ-µ\ñ&“žÒRa«7ž>»icSþ̬ÔÙKMíFªËn±`õ„',ôˆÆ)3yŽ«¬#ZJ4ÕÞ‰6vâ½ótvž’:*»×,FÊktlÚèÉÂY ›¢ÔXM´6evNe÷yc=(u.Øø‚‰¯@B·yÓÎ`Ùª*.ZÙ¨,]çsK€+™ö 65«°óL ¿´sƒ‹•GMn&3'T·“Ó ý«tbÆÂõTÜÅçVwn +RK‡ ba¡2w^jœàs‹ ‰ÀùþD'ß9ÁÆZ͵[éé‹\²OŠ 2ÜD¸<œi¨ŒKŒê2Â&{žx'Ó9Y^¼lç +“€ÜlüʽÏx‚eÊ[™(âàÞ…to¿2YaöˆÉVeñâÏ+Q¡NA=ø(Ðop„ŒXPeñ4æσCW!ŒãW=bse‡Ð8®v~í˜VcºCí#2ó +´x€<=P6C0É©¥Kl~Jæ`´.Aç +«° ;Üvƒ’¡Q%bq†A¨¡öØÌ<…€)é]¹Åkq‡\ÂFé¶;ÚÀÃu&Óõ`bM>Þ˜]»T]سxBF2ÈLç{»…ÙsÁÒš;ÖuˆU½;ëÊõa³P_>XÚ„ÉMï÷¶îm¯ß´û¥x¶9¿sw°0+C|ƒ! “á’³þÌ +€Üê³y’‡oÒ@ؽqÈ ¥¥+™î)ˆWSÛg/’îâ…lï4êÏAÔ‚ (G|j‡@[¨'#3¹'Ín+rópäniFK§ÁžÜ‘†ÚÍŸÓßD}ÇÁÁ›´si8Îq½Sg÷;}¹a™éȘf ±v¨´«mø¿Ýƒ3œ¨mf§Îhñ ‰Š¸Â 0À<Ü—F<%BC@f“”ËX<’‰’¬Þ#ÍĪ›¹©}ŒË)m^#ôFê ++­AYèzO¼TIF{v&«Cx+…-3ޞʹ#d{·²pµ8%TY“Ù¼#ZLivËÌN„8Èͤ¶ÛŸ%ƒ…!¥ÃBK´Ôñ¥{ÞB&,”„zR Sª-®í? ²³†U“Z»ÙÁSB™ 7töÀ˜wxâ/ÿ½úÔæ°Â¢C&Ï0ïM.¹5!·<¡D9H^®ø…¥Òà›7áà3±Ð]<#T¦eiòˆt¢æŽUì숩µ2þX›Š ¼LïaÍ•ˆ×@AAœíþ ,ÚÌÆåpeÁ«Z|i=1ºg -–ç‰ (éO×DŠ}—˜W9|ÇõN—s‡ç–9iJ‰2„X –7 +³gRÝbqÖ*¨ž\snn÷nÌ/”Û,Þ‚Cl“‘Ž“¼ý+Gü€¬B~ã%…ÉáôÅ£5€úò¥|_jmÐѪ/VYß½>¿{kHGȨàyS“Õ"~ l"!˜\œ  +ó9Bu$Øp„ÚBùD¸vÒ@ÅÇŒL>¡Þ¸ºåP&e÷¤PZÒÚ|Ãã†q¥Ù`÷"ž8ªbþ<Ð…ñá|úRꄉ ägøÒ"š²0 MÔG …DmÃBE†Uèˆ`#í5üà2å56ÞÜ£ÆL®p(?L‰ Þ¸'Öˆ56àØL´$·xä&‚$@{&{û¹Ùséή_²29„ʱyb2¡¶ÑðÈk¥å«©Þ¾Wš…b>¦Â´oå$·ûÒd°Æ'g…Ôœ“Í îÝ?ª°0ɵvµÉe%D™Þ=$GÇ4¸Ë—Yݾ›­®0tÊfíÞÜà“/"4¢!`=Åô,J§ojÜàPZ¡gƒl¢žè¬¤§WÅÚ”Xí¥:sÍõÓÝí³ùÅm"(yM +h6Ú²QÑQ=®²Q§ßé—<‘“¨8CW$Ì·N\~¨¾¶/”:f6bõ'X©,u=Å® …ß÷E2mo´4ª¶Ù(ÑB…'­´¿}ŠLÍê©î +Å%•©DÁÊÇ”.Ÿ™ ÑR:Ý_š³6d¤Dg¨j„E³R‡Ç52“ÐÌ.ÙÞÁüAÙY‘Œ,Œ€ qw4‡ò ¯T.N/×V¶ÉdIãò“á:ë±I#}hD{dL¯0‘BfÆ—ÈIhì>À3¾¸äNôÐ`Å@'À— ”À¥ÊX :‰ºåÆæ—¢õu3%iˆ££:½…²»D;6`~……ž0SjÌœ‚¤Æa øþöåÖúY*Q<ª·Ûh7PP¼ktGLMšGÕ˜ÎÁ¡Qã«;<˜¯ì›¾tŸ‰uq¦¿}¥½º'–{FZ6`*Ü댖!+³ÓVOP,ÍCXF¸ ”´ÚΛ¨L²u¦8{‰ŠÔF¡6l4ÊÄ|©6DŸ#—‡¾ŽÔ#E&^âÓ­@v.˜_bS# +û¿’}ó¸ÖFż¡ªBOR3Êõ´ÓWˆ—–C¹EÄ“Q9†dV½Ãg÷¥¾9a¸kBXn±yóB~“O-Úé´Ì@»ƒE§/ÍÕ: [³k{›çn\¾óÄ=O¾ðÄ+o¼ùãŸøÉþò÷~úÅß¿ÿ³÷o>‘în`|np‹ºÆes†"RCÊ6R¥^¶¹Pî-L­l¯Ÿ¹|õ'ÎÝÿÄ©{Y¹xóÄÕÛû÷=qáæc/½ööso¼Ý[ÝY;-Oc¾„Ñp‹y2\B¼*O”Ûõ™¥©åÍÕ½s§oÜwíá'n?ûòÎ=n\yàÜýO?úâw_þÁ;o¼óþ«?z÷γßí¬!’çf§ÙÁúÂÅ€TqúÅ@2Wì-´WOÕ–¶²ÝÙÊüj}y§¿sá¾ÇžûÅû}ôÛ?|ç?=yõN±·ÆKu¥‰R…3ãAAjÇK‹fgpLëÀ½ÑBo5Õ^ —§ù|;T™žÝ¾º{÷×ï<ùüwxñΓgîýÖÙ;O»#E…U[½r½ ÂéË»!³û³f—ÀÅKB¦/ÖÓ͹úÜÉå½·}þ¾ÇŸ}ë_½÷ñï_ùáÏO\¾]fìÞ$à¢ÊÊÈL$â‰blRmóÊŒ”‰ñÉ^¢¹ȶùlcuÿúÿöÊ}OûÜ}_~à™³7Ù»ûÁ+=óÊÛï>òÜ+n=¸rî»/©´P +“ËFI±ÌÆ».AH¶1HÔÓõ…Ù­óµ©ÙÕÝ ›çoœ¾zß ¯}ÿw_þý÷úûúÛŸ~~ÿ“߉æºGd¶cr‡‘ˆ±±éPveBG©ìZ+E°I„NjíÊYI¡Âb²6¿qvûÒ}+gož½õȽ?¿tòJaf/ÙÚ¹ÔÑIä뇵¸[(V§gOlž<öâÕ‡{òÕ׿ÿþŸ}ò»?üêÃO>üø·ÿùÿüôó/_ûþO~æå™­ ±ÆºõïKŒ/.F’…J{qsÿÄ™«{—oÞ~ä©WßúÉ«?úÅÓ¯ýà‘ç_yãíŸÿò£Ï_|ãÇoÿû/þþÿ|鿼óÌëËû÷ŒE‹ý\w=Z™¡CéHºØ™;uöì­Û÷?þÌsϼôê[ïüüÃßþñ‡ï}ôÊ¿ÿòíw?úòÏÿ¿ÿŸÿ÷OPäŸù£_þæêÃÏf{ël¼‚iÁ¿(¥‹©¥5xÌoìì_½ç¡§_xþµ7ÿÎw~ñ»Ï½þýŸ¾÷áo>úä‹/¾øßÿ×øÛ/yö•í ÷…s]›³“I.Ú —VôÁ`óxùD®Ü]Ø8uþîÛ7yúæ·ž¿û±ç^zë§?yïãŸýú£?ÿíoþûýúÓ/>ýý¿óÆ;ý­k‡8—gBM—õJmo¬š«÷» ›Ë;g¯Þ÷à}ÿÛ£/|÷…×ßþéûŸ¼÷ÉïùÑï~óÙçúË_ÿëý7tëO~õÙs¯üXÌÏŒ\Ã*;(ç„Á­F8 #…|$Ûœ];yùþG®<ôØ·¾óÚOóÉ»ÿöŸ¼ûòßùð·Ÿüû?üòƒÿø§¿üÏÿüϧ¿ûü™WÞÜ>3˜i T@n°MèìZ„X"§k½Õ•ç/Þ|øæÃO½ôæ~þ›O~ô‹÷_ç¿ýã_þöŸÿõñï>ÿèÓOÿã?ÿó~zí'rÕHy™ Õƒùlö9Š‘"N‡¸PA*Ïæ›ó…Vaçì•;þÛK¯¿ûÁ'Ÿ|þåwÿýç¿úè·ÿõ¿ÿûË¿ýãÙ×¾óá'Î^»Ã„«[°ã\µ>3»°ybgÿÜ¥KWo\¿óÐo¾õæþð‡/ÿò—>ýì½_¿÷â«/½|ejq5”­±‰¦ÁÔXÔÅãnëÒ™ÂòÚöå»ïܸóè­{òß^üÉ/ßÿñ/Þõ­ýðÇ?ûò/ÿ§ó«ß{ñµ7w¯ÜßZÜK7—Üþ-G íP¦” +­©åþÒÚÒêÊ•K—¾÷½·¾÷ƒþðÇï|øég_üõ°ï~ðég¿ýÝßÿñŸ}ñÅ›?ùéÕ;wZ‹[\¢êäS„/îö“¹jkfyc÷âÚÉ3k['/_¿ñÊëoþúƒ?øô÷ï¼ÿñ›?þéû}ð»Ï?ÿä³O?ûìã?þè¥×¿æÊíd}ÎB"èËþÔ´ŒbdÐ/H™\eqeåþ;½Çñ“_>ÿÝ7þÞo¾üËßþöþúã¿üò‹_}ðÁ‹/¿üÜ˯.žºL…‹&WÄÊ-ØxÍ/W:óÍ©ùµ½‡êÙ_~î•×¿ýÝïýü½_ÿýŸÿëw_þõÝ>þñOü§/¿üôó/^zóíÛ?•©/k1~BçœÔJ­C} Ér?žk.®oß~쩧^zõ™×ÞúÙû~ù׿}ù·ÿøÙû|øé§_þùÏŸÿñ‹wýþÇŸ|ôî{¿ºóø{—oÓUœÈMö -J o(ˆ{së×ï}äµ7ÿýgï}ðöÏ~ñÙçŸù—¿~ü»?üú“ßýõoƒçyçç?ýÙ»¿x÷ý÷xüéõs·:[Wq>wLŽªÌ^µÑm°‘r bŘb³¿µwñú퇟í­_ó;ß}ãg¿üÕ?þùÏß~ñ§Ÿ½÷›_½÷ë>úè™—^Þ=efq›Öâ•5/Í›éB5žÌT»£ÓÝù幋WÏÞºsãÂÕs»gOdzåóXq§ÖJèjBN¨™µ¢4ïJ‰|±Ô¨4š «ëók«™Î‰Ýõë7ÏßsßõýËWö¯ÝÚ8{©45ˈ §?íwœ¼ÚäÔš&;iµ»œnïÚÖ…ÙÕ“áx"Ÿ–V–çööw¯ßs÷ýÜþáðÞ¿}ÿ£?ùìã§^|~ûÜ~ejÊŒRÁ´¢+êÑc á ®nžÛÚ¿‘ªMIÅZ³ÙšëOŸ<¹ùôÓ¾ýã·?üøÓÏ~ÿ»·òöw_õå—^|ê‰oß{mem3W›fÀÑÌ.³3 FÙãjÈ5ȸS$ÉÅc‘å…Å{¯_{õå—_}óÍW_}á—¿|çÏþãsßþ·ógO®­ÌªµX¾lÄÜZ›Í€‡ÁÁéœÕÉ¡åã™B}vvñìéS>üÀÓÏ<ù½7^ýõïþÅçùëŸßûÕŸ|òÑý‹{©BÖ€¸ vÀð„–80¤<6 endstream endobj 26 0 obj <>stream +n:>i1£,/dB²Ñ™îÍͯŸ:}úüÅ —¯^½vý;/>÷÷ôò«¯¼ðüsÏ<ý­W^þÎ}7olmlE¤¢ÅåSXjeÂý(¥;Œ2#î€_XÞºtýöSϼðð£ßºtåÆ£>ñ“Ÿþâ?úÁ}7/=~ÿ==øÀ…¥ÅRµJ7³Ým&Rÿêrhø²ÚBÉ5¨ÁìŒÄsKKë—¯\ýΫ¯+=öä“>üàóÏ=÷ÎÏÞ{úÙo_¾|waEÊ•ì.âŒÒÊLjðI•YcÂõV§ÙNÒB:˜ªçë³™U+ŨLgw)­.¹‰€}4;y™?ptr\n1سç³RVŒEì¬ÍËøÃi¯Ð™Ùªµ ¨Ëƒ¬ƒOÉQvXƒÊ „‰êqÿ¨=4¡šÔØL¨Gk" åäÀÞvŠ£Îj³Ž†™T­ÝYÚÞë.®ø£1£ƒ×"Ãë¸SX< +=®#& ÁÆ<¡JM¸£x_ äåü>>ŽEÄx"–+•{s¬ CáR¥šN¥Êk´ºt(cÂy”Ž2ñ–ÕÿÚJf$i>ïb’,É¥ R\¼àûóKår6™Œ”ëU!–ƒƒ´ÓA7 þq:ª°ŒÈŒÇ'tCÆ1­KƒÀsÆI.#e{¡h.K_¼|mcs+%E«¥R&›k5êíz)–ÈiÍöCÇ' Œ*l‡†T‡‡5ð‡IíàN|„R¾'Hb4Œg Ú¯3Z$'„s^_,&*µéZ}ÊMQb8ΰ!«Ý#ÓG”öÁ(+çôçÉ@¡ÀYÜ:‹ËE…üÑ24µÓ›LÕÖ‹—ËÓ{n6¢3è9– ‡ã4ã7!„\@|>ÕÞ#CíQ•ãÿøƈÆÊ™ QirÃ.c¸×ï…‰`(Že)Êk·Ùq‡›f>Íó\¤ªܼÀ°±(}ã°ò®!Ù± H¨ÖⶒQ›Šåzs;·äˆâ-êI`l@Âä ™]a•Í;¢D vNx +î:®<2i–Û ˆuG<Á²“Ë8¨˜ÅÌ$Ña£ZDã൘¨Fx;•0büq¥yÒèPX)˜ìŒÊLÈ „™Ž!þ4!äÝ|ZeÆÔf Ê`§¬n?áOjܸÑirEŽ)l_’×ëjQßà†Âo!…I£sB«­ô˜‘ì“z»Ùáwû2“*f÷JŒ£|Q‡W88*?:¬ÕX¼5âA“¡cÓ*9¤@UJm!‡eæáq\ašé&ßçˆ3|§Œ¶á…‰Õâ# 0c–#ðB´ ÕfzLa“é]ãzjÜàQ¢I³W‡ñW0’mͯ"™± ™¬Ó)Ø0vžaD3$³˜¡¡ óÿy`ôè¸Ñì€vMhJ«W…ppí+ JFSMùÆáÉáÁD /-TKSgRÍSfwâа~xÜMv Xà®ý° +›ÔƒÀò$_ +fæøÄ”\ kLˆWm¢ÆÕ…Ñ"¼qp©Û`È$i3žÐ™Ô•é†dz-ê7“1;›ò¥§¬î˜o‡²Ó—B™”Þ4a - F†â¢ñ}ýèä]êA ©q«#ˆy$Ì›²¸ÂñÜ|¬¼~hÜxd\'3Vä +5 ¡Š²)-Ââd0]ž³1ÒÑIã1™aXiÖ Œ?³@'¦œb3/ÒÁ´Ò→×»”¶€ ;øšXØÖ;Äcr‹Êäb5-â×áÃ*Tãq_–𿺥)38üPx°¿°b +“ÇâŠzBU‹'rdÒ ²°n¡FŠ+•PÙE ™0y‡”v=æ›Ða_?*¿ë˜rlÒZ4¡²Y«;¢·û&Jnô|cHT†(-¬ ªl"ü×J&´˜0®!´v¤þÁ$=…cx0X8ÌÄ|º£søŽNšŽO˜l¸0&³<ª<8b8<‰+qÕÇ…{:D’[á1¢D ë T¨Ofe®(íy}¥•CH‘WÌt\af&•Ž£Ãúcf³=,,#.qh |UDؼÜÆË̬ÌĘé”;Ü!‚u5Ê›ˆ„”Dó¤/½`r' l&MÌÑ éMÛ»z”¿ë˜ÊäŽÚ( g’>?–#ß<¬«æÔI³3<¤rXÜ1²ÒI·Ør«8WU8Æ`ÑPîø¤ùؘ^k¡mdÌ1¸Ü´æµ16pĤAY±²‘# +ó¨ÎõÕЂ3TeÝT{;›A˜æÏ¡l† 7Üá–Å›Ub¢Ì8*·Éh±Á=þ£+19#:L ¸¬S¨š0É-j„1’W´ef3cfZ2B¬ØY<ŠCÐP6¯ç ø`ÄTµÌèÖ˜‰…õýêì …Å¥E$³ÂþªÑ V?ï¨Pö®­ÆÆXœü¤“ëÐÔÇaÓµ¨.B%°Í ¬³«þ°Ì<¦wéÜns³(›žøj̈àÿš\a× nºqxs™øúÁ‰Gåp¦¤Øvp% ˜ÔÑ#r ÒFGáx MÉI‹gÜèV!>ƒ3®'bzG¨2s:Q[Ô@é´ÍCÜq›;¡²ùá_ÓiÌЉ®1{0`ú³«õ•û£µ…Í/7S›5Ó{ Žî/QbÅ+Ýþ¼ÓW0;C2¥µ £*(lÕÑQÃð„å0üWŽ€ÓÁ"üC™Ù‹±©11¡wÊ”å7ŽªŽ[µ(¯³óÿr×è±1ÉÎí «ÝÇ5ô1•ë›cæ»Æmàný•‹•¥ó‡Ôö#iõ•ñY¶°.T6¨äæË–ë++gÐ’áQ“{ÔD«ƒ3ùjdl~ÌÂ&h¬ì1 +‹ÐJK–ÁŒ—ÁÈkÜŸ3¹üV&böJz*fóçp±h¢c66ãŠv¸ì2(]!(] @阛Ϲ„œÁ%¨~•µ¸BCƒù-Úcz/0kpg§/k#o_ƒ§€DOhìÃ*DŽ´.ÉÊ–þ²“/:…²‰¸h(}XiSÛX£3lvÇ¡ºØPyvõ"ꉙ4 +¬,h¥@“s¨7o¢zuòËÛ×ùdëШvTaÕ˜i½Ýïðå\^maà BÑäš0•[Gµ𪉌»C5. ^©¾xŽUCiŽ¨Á= „hqFh±âKtFÏÈ„Ù‚ùOü®QãWóQ³ {B _¬åàRN>§°yA 0ºä™L8|…hu«´x ñ@÷y²ÍµXeCgŒ¨ XóQ5Ge%ÃróáQ= Ÿ™÷eæu®Ø˜Ñ;nó£B%TßÉÎìã|aÒäµ19XsÄ#É N…É5®Ì„¶:8j9<¦Ó8dF/(˜ áärD‰hœ }­…K..õ¯‡eÇb‘†Õ•üêþnáÀ¨ 6B¦'î:&S£êÁ‹>%â·*€(B²MFJGa¡0^GŌޚ±úóF*FËÓ+W–/=®u‡ ΠK¬°ñi_|:ê;#C*ê +WÛ;„/ÿÍãÚµC‡î¯ô„[ÅÙ‹±ê&¯,¼EeåR¾Ül°²*V×ÃáÖ)<Ü2QÑ@´rïCÏû»Ð’›G…°L@Ù<—œÇùªÌH©QßÈ`®lÀBJf2ep§¬žjSÑ–ÜBÐCÂ2}uGÚ!˜\1“+Š2iŸq.í鉶´¸àøêk,XÑQ 9Ê+lHШvp=¼ÂêC½Y&Ö¶Sq§‚ ê r‰PW“z7H±ÙÕb¼Ñ±2I”˨1~LïÕ`ÇÀ:BÅQDFê‰é³:Oò¨ +UZY-âÓ9üð +#³zmT!ã`ÓC +äkCÚãæa5¡¶ùmtŽðWÜbç¡5ŠVÎ1bcÓFgHe㌄hÖJr#‰“¡½‹œôõ£Êc“µ;˜4;ƒÇd™Á1tÅ%ÂÁøÓÓÒmÀV¢¾œ H8\3ââ±IÛ˜ŠÐ"A‹+s§¿¡´ +#·¡¼QOjTŒ©±#c¦ÁMgV¿•Ìxb]!38¢¶CTÌbµñ£*bpo2*à|‰“zpNoœä£#jÓ¨ÕuõXñ& ddÒÊ |ÞÄ$”¸@§¦ƒËÿfÓ½ôÔ9_~ÞÎÄ…dÇ›l›Ý¢÷ë0nÒBQÛmd4YYÃ|™C£½= h"¢._.R^f“ÝpvjõÌýV ¤£Íu©·›œ:ªoÊk6.kõD‹õÅï¼ö“¥³·å6óã\–à+|z9TܲûJ¤PŽUÖ5ñ¸7¹$,ÐpÇúþü‰Dç“žWã‚ã˜pE joöŒœFW˜à N±ló¦©h[*-ì]y¼Õ7¡£ú#“æ1ó˜Ì6¡§@Š0®`çò([0’â/âbÝ(ë]AøP£;î VÜB‘ W*í5=ÆM5$&„’@“Ém‡Çô8›vxãû¨ÒŠ¸ƒ ¼l²cåÒT¬ƒpÄ›ñ„ë6ZL²BE×a¢Æ’›ý¯É –QÓ3à;ä:”›€Ã›´"Ð΃/æH‰ð˜ÚAòy……ù—c‡Gôr=e#%ÈwD JnÀƒ¾XƒRGä£j›ÊÆèNêÑ;#•¼Òl¼ušŽ·™T UѦ+Þf3}"ܺ¨”—®Üx¦0wúð„Qi¡tƒë9ØG*Ü CQ¡ÐãfWDffßÐa¥o›ê Å%>¿`¡D€w©½aó¥í|UèD'T\N·¶+ýýÙûlLDm§˜p•OÎ:›ƒ 3WˆIt¸ÜÜ„Õ{TŽrR?Ù;—š>Ëf¬Þ¬ÆS`‚× ±lñ&mló¾VÜњݟ%ã5.ÝL6—¹LψséÆZqéj¬»j˜PÇ„Ú¸Ù[l¬Ÿ¾þt0ÓÖà_3’³;H-‰Å;”âË2½gLœ;S™»¸zîñhý^wq¿2}jD9X)TYŠ´w¢ÝÂüÕæÆáÊ).+)â¬0󚨸ٓ™0s@Ñãz'ÀÞQàįBÊÁˆÎˆž”ìbƒ+oÒÙe•Ôá~·Xpðy“B˜”ƒÏ¹Ã5±´LEêz;Ú¢2Qc'm,,¾’âR¢`IfÔJ‹ÂH˜\"¸¼'1Ŧú6®`õæ\áN¬±,¬C’=4fÒQ¨g°ÜW0:•£“uŠ A PKÊÁÔ//¸›™’ì ì‘æ¸Îù/FGåÖã“ȶ“ZBnfìÞ B'íLÖêŠNêÀ£éq ¬kyA(, +‹•©Ógî~Ö­#l*TÝä2 ÎP#ÛÞ-ôöÄÊú˜Õ;¤°Ù¨8*H¨“ËC# ÊIåpûS¡BßäÃù‚‘Ñ:#õüÅ%2Þ¥„B¥³µ{ëy9âÅù\ °­m‹K‘úf¸¶áMÍj0¿‹Ïzcõ}°ÃWôgëë·RÓûƒ¸:ÜL†•vÿ¨‘Ò`:BtëÒVcå—h{­`qÎ@©¬§à‰7k+vïyvíÒãÙÙ3v>+ÆŠý­k®Hõ¨–êóçúÜ<&4…üRajË%dVN,Ÿ`}+qª äƒiužXiQª¯jñ»†u“&ð€+T±ûsƒï‚H6}^W1L¼ž=]Z:—šÚŽ67ÙÌÊgQ:üès?غxGGøŽ«PFš ·÷ÅÆ®+ÔÕºóµ¥éõ»¿qܨ²ñx¨Å×N$æ.¦æ¯0…¦ƒ…ŸykfýÒ0•ó$z±öÞô©‡SÝsr {ì܆8,·×õ .—‚t1Ai ¬NÁJE!´RñN¸¹#MŸ­nÞnl?õs`ÄœkmÓáÖ„Œ¸„xbõÙ¹Rzú"¤uˆÀJ3 ¼Š{% ÂNHà±I› s n5K£üAà™ÍŒ :Äg…XAI,4¡¯ôš! kì&œÑ8&, lºÁ-¢\šKÍDk+ðgH…‘ÆŽPÝb³‹©öiдÁ8\ nÂ8§?¥±sr+#3sãzfTCªø˜“`’L¤¤Á¼*Ô3iõX˜d¤²–Ÿ»_ºb÷Äó­õXmEëllŠËöii +õ—Âåµd{—‘º@)— dç¹Dœ–:H ¢Á#ˆ=0nD ÌÎó¹ea•IÍšÉ(Æ¥Bå\,¨1?!”¨H-Y_^ؽµ~þ¡úò'e«óg®?Hw²BåÍù½Çöî=¿v›ˆNëÉè°–@)IÌ-Ž ®At,RfñBü§¢¹…94a™Ð» xèNƒ‹2N„ë `Á2hH5U_X8yƒ ULdHëðš¼I£7 ¥)/Îí=ÈL# uÏ‚:Cõ‰Á$dR‹r¡d'ÛÜ[4à"Âem\ÆæË»cSîD/Îåûg6ï~&3}ú[¡ã=g¨iö$!C“ãcƒ^Îálò_)Ç”3Òc3kîHŒtGµãÜV*¢FY˜:qcëúSÝí[…ùKt²¯À‚GƒQ0§î~1˜[ÔÔãa:ÒÒã(9!· @ø»†õ* Éöœ¬tø¸ò_ïR8¬þ +“_÷ÖùüªÙ)iQjzT84¤šDMÚÉ1¹‰¾’›<Еùî:°÷„‰|=ùr@#x´­@}Ÿ#ÂU#™è$jÙƒMÔW4"F†m® ò Œ€Òx ÆU ŽÑ½RaQÈtïUUXåVÎÁ—¢õ-©wZ¬,z…ÌöÙk û +Œå³ýúêÍÒâõXs·Ø¿˜Ÿ»D„ÇT®Û½I°´3bf +Î`+TÞpò‡@„Uv“3ظkñärF*‰ ƒ»#m\Ûo£Lgã©ÖZfz75u*\]l,ž/Íž!Å +kH­ídk+Ó=áNt•DtÌ âñE¡ìÂ;*C0_ŒvÝ¡–+Ôrø+ÇU¸Â챺£PÒà8E\ºŸí_x€S Ù¹ó×}É,M).8ÅV¬¾Ý?ugãÚ³éÙsPº“’K¶¨DG…ú'Lô!™}DçÖã!p"­CT ÞHk!µ]øš?·8sêÞs¾ºvý¶°2bdì\Fìqe "§Dø˸¿ Ut`ÌOë‘ú6&?iöB|Õ‘+äU . )®8½ëlðé)&ÕGu [‰ cÝôÌeKÓ{èøàÅñÿº‚Í?¤"OXµv´¸¨±1ß<4úõƒcrX®àŽOÛU-:"wÊ‘ ‰L ÉÐG“zZnbåfb”Ë7쇂¥%Ä +§»FgÌNÁWX£’ ÎH×W\C¸ŒÒ<ÀE`W3“†Þš„ŸÈ ¤Ì@©Ì^ˆ-J£4ÒÀ¥ßÑ1@beBµÁÄõ1Ãq%: +ñŠ!¾¬3T!Ã%_¼’ª¯CǼ‰òÜÙòÒÅx{“ŒV¬žˆÞÒ8ÓÉ(¡hÆ +½’»+4-OÆ›ûŹ»õƒáÏ /qO¤¦9ntË­-u†ZT¬‹ú‹%²íõt}Õâ ˜ìÞHa¶¹v}nÿáųîÞóíDkÇé/žÚ½÷â½Oc|Z‹´îÂWüÙåtg¿:ÕèJ~sÔt4Ä â]2¹2 Ý=¦Â¶!‹Aè†s‡ëPh°l¡ ..Ζ/YÙ¬Òê‹•ÖSÍ3bv±Ú߯ ®ÎMš(O {þþÈXû¨Ü¡BƒØ vmÞ<ô ¾ @;HŒœ€Ž¾t?_ÈMŸZ>÷`kí—<áÆÚé{ÌNQiã¨p3éƒÀ +¹eÌ_RácZB†Ô7€ù « +á…Âr¢»lƒxS‡Ÿ—“F׸Á¥5³÷X{çÁüÒ݉©}³7{׈ل…–ö±x'LÃ:·…]ˆE†J'BåÖCÇ•2f¼ßb5cF*ÜÌä©Ø,Ð)¼â¸Ê¦C™aå`Ü„Ž[˜Œ#X©-]Zºô„ΈIèì<ª3XrGêîXÇ“^ ³+&OÒì Åòsˆ'¢E½È lÆÉ€ú ¨ÍÌÑ Ã]Ç•‡GµF´C +;týàÛ´Úâ³:r=îàÒîpÕ—öçûÑæ :ÑE¹¬)V2“> ñVÀ¹$ü%V¹Ø””&u‹<0V‚°“´QIè=ˆyÒˆG:&³B³C ñ ѸäfãòÁlŸ +•Ÿ÷yÂY2ÒINoæö£å¥hn®·r1\œ1Ó6Ñ+`szN(­»c³:„¦‹/8ØÔ¨–˜„l¨uàáQR‚p‡x²¦A¾ðWý¸\mc˜HË—œT¶kë÷¸C*›Z»êINLIõW7w T’‹µcµµ£*ü¸ý‡¯0¦wÝ5n80n˜0’—ñDÛz\ÐØ(.V±’¹‰RéÙ9%΃gZ'!ì(­ lºuñ$Uff\ã„øhÌ0®qØÈФž\Á NybB¬’Ñ&ê/ØÅtûzNGÓU©µ ?1xR +, @9œ/y³þüÚ¸‚°Ã-ÖXižIl,BmÂè»kÔ2®!²•…Zoc0&N(ŸC¨ð¥Õhó¨ú¡q‹W,‡ÒÍ!¨„#€xA*\e-Ÿ|!Ó Þ]QÙü.ƒù³6OÜ›èJëþÂZªw^(®Q±¦óBúN7·cAëCÍ~™ŠcDe‡Š¶ÿÆWßË04VÏØ\ #&Úð/ý]€—š>µpþ‰…‹O¥æ.º¤ix!†Ovf¶šý³.¡$·Ò23õU"ï —'7`ŸtûRXL@DbÒä;ªÄÀ˜&ôÎ1 vdÜ0µm ƒwh«nðñ™ÆáüñTs©Òßj­žïœ¸»·y­òžòü9¡ºn "‰ÂÌàS&ª´Q¸?ë +ÖQ¶ì;F"À¬C’uZ+§|uZÌà«Ì´ÆêÐO‡ÈF…]þ”Îáƒ×2Ó ’#X¦Ã•duiõô}N¡ ;ðåÁ7ÎýÒ l žô¤òE*‰âü¸?0¬בjT ø2t„Íi0¡R¶µ>®ÅGÕƒVÙÅto/7{¦·yŸ]„ÞÏ6–»«—Æun•Õ£>X¥I-®µyìtº)Vî;¸$Tš +ÚÍê%‚e_qYê_l¯ßë—:S §_{û7l¬3ª÷"lÅ“˜”W7/=¹zñ)"Ô>8f59‚—ƒ^køJ" ¹ü.¡MGzî@à|TÂOÜ#¦tÔ°Ö#7rt¸áð§' „èq¨%%!7¹õ.(x7„P ƃU-¶Ó)H¬j+†¨0Ó`úÃjÇ°†×»µV¿Æì5Ûý\8Ÿm/3ѪT…¸w9XšË‹l~ 6Œd‚‹3KçZKwœ¡!5*7»­d÷&éô®µvrÉHvÊLoª£<â-Ù9pÏ̸‘†×cô88ï° +L·¬LÜÈ:ƒ…duñÁ§ßxö{¿*Ívð_fšËÌfº§âÕÕRëDsæ%¤i!KK®@‰ 5¬TfBϳ|B¥%Ó»‡ŽC£ÆƒÃÚa¹uL ¹>è Õòííƹ¡D}:gXMq±,,‹Å52Tƒ`Uh­ÅK *+7‘&Po" æát'–:6f<>i5'PqÝ?¢t€ÎëPóHL¤¦2{ XÀIÁŸã’=÷àšº,æ+:)ß\+uO@7ãA6á‚ÒHê/ ¯e}ñâë (krŠÎ©Sñ6<Éá Iå­½[€ëv_%TÜ —¶™pSº\¼m¦“Gev›Še§qŽê&ô#AºõÅûªË÷Bõ˜ÜNp)&˜S]ÿzTþõ1Æ”[V2Í&ºF2¬%ür u× +Ç6„Mù óúp¡â˯°éy„Í +ƒ;£ë +‚uǪQÀÝÞé©­ûÖ.<¹}íùÙ“ ¬„y%+ÐaAè£ã*ûáQýñI³ÂDh+$ѹFµ.•‰W2à,.HéÖÊÊ™ÛÅùS*‚ÃÁ ˱ú)>¿4¸Î6Ö„W 4ð$Lp„vŸ ,Ï›FMTO¤[§œbsTKí<Êd0®h¥AiùQq#x Ëâ)7º†U6••bcµÖò¹ÞúåXm©µ¸×?yÝΣLŒ•Út´Þ^>Ÿíž4¸ÂÔCÝÅ3bº iH‡úTH@ƒ ŽJKzħµ0ÇeæáÁ8>,×Фé¸Üâð¦| éé>ÕÃ9ijåJº¹i E,TŽ¶O§¦ÏÃIÑáZsá\¢¼™ÝF%ü©i^švú¾=qlÒôµƒ2…Î 3`†Ç&mƒ÷íQpæ¸Þ%7Pƒw‡ô®#2“ãZNˆ––=¡2ÔÒq9js†)>;®¶5¨Í´ÚDxXðàq¹mDn™ÔÀ/: |䨜pzc„'ääâ¨'ÎÅ['¯<1»u°Í슾"xº˜_F©˜ cPw`Òè׳¸£<¸7ï`rÞh×쎫1Âö…ò@8ÿrXÁlNaô Élà +dDƒÉRé9:nU£*ZÄ ‰šËƦŒtÌDFQ.­ª¬R¡lûZ}éB¢ºšïnW—.äûûþܼM†²=w0'7¹ vnÒàïSÚ,ÁÁ$i± <–¬mhàLefµ…B<‰`iaæä½Ë¿•íîì^zx÷Æ3©c¢âV:e¡$œ/³R?Ö<›™»îIÍ`L"Uß W×èXb‹n0í–S¡"-vLÎØ7†µß<®=¬°›¹²~âÉÊ߈ê±{’baÁB…´(#¤Ú‹ÛWo=úí¥3÷Ê‹_} šë­ßÝ\»ês:Â牵óÝxyÑ©†2-'Ô¢\´Ñ’Êʌ需‹“zÒê×Ù³3 +û~ð¸fDf•ë§/K‡Zˆ·`&%.\ê,žæ¥ºÕ#²éNyùrçÄ= #í="Ü”›½‘doíìƒzwà¸Q˜H0D½C4`â„Úe°ùI6]ïè­\>p\s×Q9†Å¥ã=Éæ‰ÂZi‰ˆSüÿ8zï/7Î3Ï÷Ÿ¸lK$ÅÐ쌜BET*  +9çÜèFçØì@²™C3‰"•¨,[Á’ÆòŽí‘m9ÏŒíïîÌ®w6ÞsîÞ=÷×ûBçàè4!„ª·žçû|?¨÷}Ÿzcñz¹w1Qœ‹—¦Ý´Šò±P¤bô@ ½Ò„“3{D¤K‹ùææ‰!»ÙCÙáàÄÀŸèXò +u·«½ @'dqz‰ÑÊ„8èáEie/›ô‘D¾—,ÍZ döpL…ÒìaG þ1³Ÿ× +J¢n°c¥zs{dzûO¸Ì‚'X°áÑ©¥£O~ø+=Ù³ÀÝùé!T©ŠUã ©JŠ¹ÂÌ«[`N#“Ф³{9'b…¤7¶ú„I›„þD×yç˜tB‚ÉÁ8 ÉÐ¥xëÊñÛãNÂEi~>Gê\¶/—W´b¿;»ýàÏrM€ÃéÖNvúPÈβñi¿XXR ·†*¥¿:5tâÜ€Ä!ÚIÖ– 9.¦«s >É‚H6\usÃv +b3„\·â'r0/ŒºG]¬ÓÈH +f±`ª¿r½ÐÙ ´R(3kíæúWÚkw—ž”/óÙéúìÅ7¾ÿ5m™1 ˜TÆ’‰NÓ•»è¼TckÐ3å›mßì:á ú¨4*ž3cç-0¨ ÆwŸm²«À`§ºû¤Z&B‰JgKÈÏ%óíÒô&å PÐEF@TÄ€Í1;HÖì [½’ÅA»` €ÞÙ ?P{RÊ [‘³fœÐZÙÙ«jm“Ž¶¬DÄ‚È #øxÕP=t Ï¡xo°‹;>¤ ;ddÒŽOØ0ðŒÕ5a: oV®¹°0)fÉp–O¶ª+7º›Ç½Íû³;çv”[ >\²ûEˆŠ:ü¼Ù3èÙä! Q=¤7uðEZiÜ Û=!,OZ0—_òe²cç'½æã³€z<„xzÒò¼Ûè`&쬇LRjò|¤êÄD€<®@ø|1?SY¸Üß=îï>*®Ü°S*Üòü¢TÜ\ÊË¥]tâ2ÊA¡ÜÛ0ø¹oŸ›0£VŸdprŽ€Á°€OÏTû;×½¡˜•øükŠ¹™äÌ^¸¹í !µ±wÜ[¿nñ Tñs9>Ù¯ö/µ¯Ié>x¹Ð}úüÓtçâ_ž4œ7û >΂jBb*ÝÜe¢SfˆŸÙ¸ÃÆš/Œ:ÏYP3ªÚQ@j€‰ÄL/Qž^>¬/^2c&gC©«âJ.^_^Ü´vó]Ÿ˜WŠ‹ZmÃÌYQÅ ªªš Dšš_Ð + Fˆ;9æ‚ÀuÔ;ß4Zš|á3FâÌ 9wbÜ6æÄ\„Â&zBq5RßÖë;6LÓ’ãgŸ._~ +4­WøÜ4—é1±6Ðm"š})³í}8˜?÷o78(€ '†].<u¥³yéΛþÁcR@k`jÃHjS-¯w¶sñF£¿»wëy¶³mðð&q°:èQ…G|äà~ŸÅCÂgðÐã€àìÔéIdÌ4c6ÞW7ÃÉfej«»}oñèµ…£Wg.>dâmƒ_€ƒÉXu5;uÑKEH¹à¦“ »puÔJŽYÈI;=i%œ¾`,Ûgåò·NNœu™]´ÑŽ›ì¤É#Œ98?›žÞ¸ªÿÿuÂxv>9 6¹xÏŠF¾sÖ1馬(oÅx/!"%`ÝkëwÓý«¨ÞE•2ŸíG›ƒ-‘ßûf.Ð|8¿ßFªCöÁþ½mq³£fôÅAû*ûy3lCE+žð‡F=t@+’±²‹S¥üLyùF¤¾­®¦;uRÉÿl,`É¡|ÎMFü.¤òõÅ£—™HíÅsf7¡‚?p©ªV¬˜î¢t9;í &N;A¦Oú;®äÑ ´?žkÍ®ízk6„ת«õû³{Ïf¶Væ ý½PºŽÕŸ~ð·—ž~6á—¸–lí6WŽcÕ½Xõ"íý< Ñp¼† ™;ub 99Ž Û8#¤y˜l83sáƳÃã·¸4â¤&aÁFF1¹œl¬§[{±ö%g0ëÄe¹4´ÚAéƒåöTÜ ‹$œí]´ó³ì¸êaÒnB·B"¨° H÷’šëZ‘°ÁÄå’^ßÈNLmÜ®- n\ +ñÃk/ï˯³¥; H¨€ +e®OzBÀpb¡´—ÔΙý `¬`©¤¶åy).Ì,ì½ñÞåËÍõ[‹OÒ 7S³×š›¦·îö·îDk«ñòü_ýþöÓˆHK2\³x˜u¼Á†Ë@7N { ©`ô±§F'‡,ß:1qjÈ @ÒŠÇ‘!W"…yR.ú…i¢J´©Ä̤—ÅÂY9?2" •"õµÜüQ}ýNm㸸rWiî’ñ®œìmì?™»ð2~¡Ì*YÃõ94<5hïnB_sîàçŒþ1GÀ Kvr°óŒ\˜• +ÓñÚÜÌ…{3»Ç«G¯´Önè ¿\qQø›ß`íˆ0éÌ7CÅrº}±0s%œ[€ø츋¶ºãçGìØàM.ƒ†L|!œ[!”ò°“s’* fÔ¤"5%ßUæÁ½ +Ñj¬¶œéî¤ZRvV-/©¥ER­„cÕåÝ[í•&|‹T–[ÙèMXª :ša'\ݽ-ÌŽ9P¬ˆnÇ“à[ËM_¼ñ¬4µ °âRlfV,¯æûGÛ7ß½òôõõGn&«Ì­îÝC‚qc‘âj®«¼ø Ö eŠ³‡‹G¯'º»Jq ç¸Ñ:À™åןÙ\<òP &2s9£‡39ñÁ ‡1ïKÞs“x065bÃþâÄØK£îa#2i£¬~Å*ÚAŽÀaF.ƒO$ïîööŸ-]/¿pÃCE“ÞæñöÍwê³ûn½Y[¹.Ïâý+|y=”ší­ÜÞ¾ö¼ºpP—˜ŽÖ7ÔÒJ Ö·CFøÅ!Ë©Q—‡P]xØà(D¸É,æ°pÏÍH¹éù;Û·ßòÝH}!ÛßlOT^K´.gok•u+&Ù±ÁmeN”Þd3„>M'æ¹á¡’Xu:æ f€°èŒ›ƒ–šøLF’Û@ñ˜Ÿ‹òé^núReñpq þq1]èm ëaT©0__¹½võùÌÞ+Ri‘ŽU]”ˆpZkñ +©d¤ˆ„‹ŸCs (‰å§ùhÍêLî Ñ=hÎb ,‹©^¬²êȉ!›'€C(%R)ÄjKõ•ëryÙA¨ó‹ûwž¼Ï©9O@Ív/¥:—ôòë¹è!3ì‚Ž+O²õµ3ƒ&³‚›NQzG.®&»‡ÑƘÖ/Ýxeçè>@x‹_$RY¾³uûƒ¥ëïóÅ5 ¢¸1ñÍ~Ð_;ss/ù&Ü!::#dWüÁÂé ŸÕÌ·7õÚú9j¦@ÃÂ%>=LNã|rïÖkW}ÈÇZ6DÂ?Å÷ôòJoãÖ…;ïúW`¾±iR®Oº™“#ö¿>5~✠¸AðQ>TêðwÎZ,FIöÆ­ÔÐê¦Ó-GÝ̤]hæâ¤^56“í=H(Ór>×ÝÍ÷* ‡ëûÇ×_ù^¦»Á¥»«WÞØ{øýÞÅWA½¸õì‡Õ{T¤ÏÍÍ_xJ¶F\” ¡€²:àÜ ;á„yY¯”€ó% nh.µúz¬»[_¹Y^ºÂīݵ£üÜ¡R_—ëlzUkƒYÍÑ:¨\l¦g +È®PÒÆfÁˆÕ_ž€5'›AÔŸ›÷Ó°R‚Õº—/¡R +å!.m‚€ÙfýtÔKk.:¬•Wš›ós×’]µô­$&êÓ»÷BÙi1ׯo>P;x' U±N-_v`¡qg@),3ÑÌ°‘ºQ .ÚGi!½œÒwÎÚOyM¾ Ê'£•5:Ò4CÒ„¢uÊÛýF7H~ïÎ[û·Ÿ·V®k¹>­–Lî€ :5è@gðÛ ‹W–㥅pª;jÃNœ·Ú;¶ÍPÀ yhÍGHÉbQRg&ŒR©¯Þ˜Þ¹¼pYÊ/x¹üàÇ.z÷ٻًc °„ŸËXuÜ Ø“3{YNVæé['œŽºКHÛÏÄhåúÃç—î?á;9dµœD„ÒZbr6o#”TÙ Ä”y°=WL¹±`¢¾”éíæ¦wÓM¹0Ãê•Tcµ»s·³q”ëmñ³3D¤éåR¼^-MmSJi0•%4ðó~P =b†­>ürñ1 +hÅKG‚‰vmùÆòµw¦vg÷¦æ·üÕoï½I 6¿êGÛ{áâª^ßno¯_«±z+ÓÞ¼òø»x×€*Áì|´}11uuæà­™Ã7Ë«·sÍÍï~ù»W¾û•SÇÜüˆ+8æ Ùèd Ú ç—Òµ•ƒ/OïÜÎΖ–®fg÷åÊBqþ ¾x´rù•‹÷ßÕ0ÕXÛ½ûÞ•'Ï<ÉÏ^²}DÌíÄå¦G„S­˜T—*ŒÞ€ù,®!ŠV$4b‡€7Õo.V¸ìDMPÀW¥¿OEJrºž®ÎjÅy&>ñe›óÓIÌŸý¦‹4 ›Ÿo™t±ƒÆ6ÒGň žôQ:¨JñMkuàÊÎY`£—qˆpARNó‰fqf¯±|Ö*ä`MÇ73îü&7ãÀË&ÙXËÊF7À4ÊàM:èqnñ2&DŠ7Ö+×rÓ{|fÖͦÆ+åÅXÓMhŸñb~:ê5?7ú8Ï`é2bÆ­^þÔy×É3¶g-#&Ì…ˆT@¸8ðü/¼d2؉3òs–“£®slÔ˜°bX(î ÈR‘òsáÂ2ÎÊ`‡êK¹Þ…hs;˜œ +HY1VÓJ}TÐ`N µÎ)ÅåHm °žÁ˦•€T8; ap +€‚½¤npR ÆÎŽ;ƒrà!UBÎ ¹Ùds³>8½¼·{ãqsq/ÓÙš;xÚÙz¼råÍþÎýDm™‹ùX=U™[¿ô€M´Åìlyájwã6€Hu9”j“z1’i_¾óÆέ׬¸Lö©hWÌ-Íì½ÖÛ{-ZYnL­úåÏ÷Ž_¦Úzu ØíÒÂþî½·_ûþ/ÞÿÑ?¾üÑW›—^~ï{{çõï …y!?ϦfµÊVméîÊÕ÷Û[OýB‰¥J`ðÓÓ?é ºiÝAÅèÔ\~ö&Ÿé{(­Ð^&„,­5‚é©@´Ì$.W³Ý½ÅK¯£R’Õr\´ŠÈ•L÷°0w+˜Yq’àüéXКFC‰I¡Újö…¿iÎ" ÛH?—ŽU×1!g‚_([Y¼•šºHÅZH“óVd° çÍ>ÒK…Q>I9<œ£#eTÈ€Ò€ûµ~#Zžˆ‡Ë6Xœ´l šHeăKO©%36Á!(˜¤½Hõ®4à`gc8óR:£– .vȈAtŒÔP¡(ØH‹_<5ìQê!4'&¸p áQ Dx03ÐÉ@düsØŒž™„ÎY$9‘AÇ.Œ×cõQc%ÛF…„lr‰)@IÁXGÍÍÉù>T‚FGr^Ze¢-.= Øø„ËJ±^895 +ØßI9Á +q“Nê̘óäy+€'ÆB9&ƒ"‚ˆÙl{kïþƒDkc|¼R_¼"çºz¡«d;Ÿò’*­TXµÊÇ>&ªg•€’÷ó9' *F©Y^/ê…Ÿl|œ^Yæ3ÓÁD·2{˜k¯«ñbµÞ»qü¬¾°ÍjùÚì…íÛ¯>~çêÓï]~òþÁ£·–®«>ÁBh¡l_olw·Ï>/­Ü6fãâí{Ï>Êt·Ýl’ˆ4¼BÉ'Öò½+ —ßNN8 eÿÆ+éæÚˆ“X=æ +Ù‰˜ËgÚÀ¢3j¥ÒÙj®Üo®cJÉÐQ©ˆTc­ ¿”7z)FÍ?‰ ip-ì¸äeL´^š¿zðôù™}æÁx˜Ãqë`㣋ÅΡ±Ž +Y<”§ÚàtKLrÖÌ90ÀÔe ΔÚ2y€ |¢%e{˜T2á¦Ï }Þ³®€>j#qsxzÂÖ[ðan#îîÁ:|r2¡ +k3}6Õ¥£MR«¼"+Æ}Œ:æÀœ ÒáܼZÙËë¸ZˆI%Uc¢e3Ä„b­x{O¯ï$Z>ggaÇGM~^.ˆ‘’ÁŸ³ŸµŸ7€…ýtÚG&P&™i®{˜.¤Bñz´¹F'Ú˜œco^€¸È¤j€%¥¸ª”7{^á*("\´á²@fmˆsIV¯‰@(ä"ÈúI5l†ý”föPN܉‡½¸È†ÔH4Í4 >Ê…“ÙÚLqz9ÓšQómZ/²©†\ê'›óµ¥=CÕÞæÖíçµÕ`$Y9Óšá^<+ÍmÞˆV«õ¶’êóñ©Ö„bõ tPXÜ:t*,TBé…DóBwûaÿâ£æÚ-!=Mõ›wŸ~õÛß¿p×'äåʆ־TXrñÁ÷çvï§ ¼žyôæGsŽ'<Ás&œŠÍ(µÜÌÑêÕ÷•Â¼«ß|øÞî½÷F\ì¸7‚Ä”ÊõãÛï¯Ýû(”[HWï¼ùƒxmÅä šüädkofïéÅû—çopñ髯G + 'F\/¸F¬¤Á\V/,4–®øôþÑÃw>ûÊøÄ*š§â}.½8³ÿÆòïÆ{ND¹tç­Ãã·ÅTŒù£d–Ér1LªŸžÄ‡ ÈÊæý«/›½Ü·^šüÖ©‰¿|ÑðÂËϳ`‡ÃÀûå;˜˜'µ"äç:ƒfF—åÁ¥`¤`ƒƒ6„÷21&Þ‘K+ù©ÝÎÆm.Ýsb‚žŸ ¥z&t°ÌjÜÅ[ ƒ‹Ah•âã6a°z‘@È`ƒÆ혓ŽºØ¤y°Ä/å¢/Í(9?“šÙŸÞ{¹¹u/;C,¬9 ;Ä0jѬ,®Ò±QO f´ÂÌ80UNvÀ!™!ÖK#Æ ‹f(H„r ²[½¬Z˜)O Y„ŠZ¼!*†cU5Ó†yÌæóQ +Â'¨H±8{qzûvcéJ(V—SM>Ùvl¡NIi”ÖÝ$?)O8g ù›[vÈ ND"&R…²ÉôÑq,˜vãaw@ö³:x»¥ÄªÅÆbº¶…w3‹ýK‰Ö¦”i©€ †£¼qÂÍœ7c— ˆ9?sá’ÕÇXÝ$ÁÆÄdW^ÊÎËÅ%.Ú,÷÷›k×cÍuo@Ndj‹Û7´ì´7 +¹¾”æb BÎ)sã²Ý/ƒ}½"ç °ÑrSÉP|FÊ,ÐJ a£ I»ÅîxXYJk‰Þ•d÷@ÉÏ凘˜f¦—zë×ïV¦šü K +¹q :ª©”»yüö'ï½Ü‰Ï•÷k¾$—Öåüb0œzýƒ/n>~gÌ„žrŒÛi`þÕênnùˆ£ÃhUNÔܘDÃÇè|²¼_~j;Q[ò2:ÉE:s»‰ÊœÑ´Á.ù¹ÌFmÞÀ„Å7jpØœP€æ¬N—ÕíG¸˜Þº@vðîøù^××Äìíæ:ÅÔ´óõåDyÎäfÚ`ÁŽë_ÄEdbÒ¤ÅÍ ÑZµ'ɲ@²—Š[ýaà„™3Éù˜d0Öô:DE€¡2yYÿ`öð<¨Ÿ<ïÁEˆ”g0ûø!Ó7€ƒñcFÏ„™´áà‹À Ï#*ÜTÜà +hùž”ëZ ¢R΢Á8%¦y%FŒv”à¢Zºaóâ7âÂÈt8Gð)p:`  +8^ 4˜ÊÜxÆáe(VA)ÕáçmàªMR)û訬ˆ„Hãâl8íCùI+|nÔá^ Ÿ:o?3âš0ù $€“¼)%ò}áÇn@ìhÌ̹é¸W|dÈ`’ÅfHËuօŧsÓ‡Õ•;¥…#³›u#D„0ã†' [ÐGɸ˜âã5>VƒYᣌƒÑìfü¤Îdœ ÇÌÛÏ:<„Û‡‘LÐë‡1ŠƒH‘OÕa1mÇeâ„øÜÒÅõý»´œ©*gfÓÍ}1½è!T‡—4ÛpRv/ëB»q@œ—!&A„«(›vA¡\_¿råÁ»6\4úh&Ú|!'0½(xðVxÔ¿7i#,^Î +‹FopÒE›}ÊÆÁÁûhŃó±Òl¤4/eú|²Çé4”u "¯•CzÄ• WÄä”’[ór5Z&7<á4Ú¼§à\ä…³¦“çmc¶€—LúÙ¼‹ˆY¡0`1)5…ÓÀ¨8°P]‹_²ã0Æ@í0~ÌeqàvÀÎFïÙ 70$NBçÅÂn482é +J95ÞqÁ¢ÃË9ü‚—ÐÀ!Y<$ðŸB¼DwMx¬ž ‘<¯féPÒ ‡ þõ©ÉC¶—†çË`ýc˜*Ý(7lô¼pΌӔR“jQJiÌŠعI÷ gÆÌvŸ Z ¥Ã©Ò”ši±jÞ…ò&2jvDZ ¸Ñ–h&Û›gLþ¡IçИõü¸Õë'0‚ ) „…h¢Ip‘Bu&–n€?œ`˜µ –$nÇÂCFØè X=4J²áœ MXýF;bvávP=̘sùEAɦ‹S±bÇOƒJä²ÑÍ‚7º ä‹çlç'|6ïàÔN7Ž9+Aø"­6‚Ñ6«•ì>ŠbÙÝ€œ€\\@.K‰N¬´Š¶mˆ4éÀý”3ê`fHØE‡—vù£ž°M˜œ?m÷,î0`\©p¥A$˜½,(ŒZšƒðU>¹L)uð .Êh'Œ6äܨÕì"@"®gä Âê#&èÄiÜèiOo.ïÜöâáq3d²¡vE²ºb>Æ`CÏÚ]0‡Ð + + Ï.åƒ)c½góø& &—Çc ˆ,˜„ÙÄà>µ i´ã/œ7øÇÐÐyÛé3¦³C¶1#H^ ¨t(|‹Ï­î\÷Sê˜ 6ÛI *7csÓ~,ìôr#“!R“b‡/äÅe³›>?áýöIÈ ³ûBàx^<=îÃxŒÕ 6lÒŠŒ¡3#œnˆÃ騑¬ÎJé¤TyiÌñ3“ýÒ؉!3ÈFJÂ$/HÚÚÅ«A-7jCnÚ‰‰‡Cy†#¤nl]ôÁßMBÜ_¼0tjÈhu6Š1!’"Yi}÷joí²ÉG›pM:ˆ >aÇ}„ˆ2" ‡#=ß'BˆT=Ä UQZtCJ0t(f‡H«¶9 ‰z¢À‰ûf3 „HÔ†ndÂ鳺2”¢¤†Å+Üì™ '8eû`ÜÏhv­~ÑŽ‰Àg‚¡°xp:œÖÊ‹vL²z‹›ü_ŠOprvÔêiÔ6f…†ƒ»³øÀ‘(Ñ”¦éàÜ VϘ٠ô<­éå…o~i!ýt$]_–’m @¦Àe2åÁD?­SbÁ‹«`ØÕX5’¬[]0t‚¼çE‚ÂZœ¾I‹SŽ&×÷®x`jè¼uðõiÙsV 0(©‚r‚ÊädPØê€-V×ð˜éìˆÉds <ŸJè™tœ5¿Üã24 J˜åܘcxÜãòquæ¼å…“£/¼8j°D0ƒ1q›3€‚¤¦äH–•2 ¤ÄðZÃî_šxé¬qdÌn´À­» n|Ò54j3B&{À‹©nL7:èÓç-/$C .¶ Ox&­¸? ‡¤Œ•ŒÖÀЈ×ê Ù1l6úln žÔ„ÙƒJ ƒ&ãJ±Yó3´Í„Ôx²P“¢I?´¸Ýn[«ÉBCLT^2Ÿµ[œ@­¢’¤ÀIpd.ŸZXâéq'4löY¼,D8'„#ŠžÔõ„6;;uýÎý\½nÂ~vÜzvÔ„à IQ¢È3,ëp¹yžÓu%@¡Fó¸ÕfBQ¦Sius}nm{¥¾4/Åy9H ‚〠ºýü ÓiN…yFàBr!eFÌ‚è¶glÒ¾"–)¦êJŠj©DI.˜rùi“==bûÖiƒÑÃy±sã0t‹— iÙr§93Gy  £&¯ áY äN F<”œ(M1J† +gÐ`l†Ø</ôa Ɉ(vAŒQ.…Ò*I)‹F$A +“  Qć¢>÷V*‰©~ £€*b/ewânˆõ"‚ÍÍXœ”ÓÇA¨äô«Åf5Z­fŽãŠ©H·[î·;7{Þ\ZéËaɇí^ +Ô> »ÿê…á“çL 4Ä`py\†bHàC!– :Ý°1E£Üë÷á £L€ÒBRÖâ&N›^89|â¥És#ö¡ÁG®ÑIôÄYç gl§Î;ÆMˆÑŠÍ«Ã«ÄÒn(àöâ\H·;€çGa"‚3Ñq³÷ä¹É1£×磿ÕlD»ôåL»¦÷¦KÙ’M +åbdg©~´¿¸°ÔÌçÕ\&"GtVÌNŸ8=::nv8<II4FÖõ›ÉlA J+²AE&耢rÙ¬0ÝÐŽ¯oÜ»³{ãÊâÍk«©|òܘùܸÝáÁ<1 bp†DÚWÏ…ûíD5/èTÍðËS©ãëàñÙó£ßýÝ[_ÿâÓ·Þ¹77›S42ÀR4Ï…‚ Gu¥XPµD­’ ó´QqŠ1XV›=ıùB¦Ò®Ì,Ìß~òf²Ö²@¾ ›wh ˜ WJmú˜Ô™QÇÉ“§ýWS”l6OD\»Ã5F‚ò™ û¥f+¤ªý•y5™°C¨É¼s>Z…HÞëuðxèšžJå”°Wظ¯ôó[k½F9QˆóKÔ­£•‡·¶ž?¹ôøî…£ÃN§¬ETËkw@f¡2Ðdca¦pZ§«9©œ•º5m»»¹Y|vcþý§>|rá¿úâ7?ÿþÓ»;ë+Ó¥RŽe8—±»€Üá~RÃHÅBÓïMHÁ|LJÊt³ Î´’íjja¦qå`ýâNÿúåå'÷öܹ¶»µ[*”ÔˆJÓ„Ãa±˜Ĉzé¼}ÔŸ:=yêôÄð¨ÅjõÚ-v†@£2_.¦%žáH4“Š¥ÓIUÑ55áö'ÎL€„5Yý‡†˜ûåwÿå·þò³¿ùìê¿|ýêÿþ¿ýÑ'ÇOn.^\¯ÅbºÇG9Ý$¬¨Ê'Ø™ª¼1i§‘©{~gñ½{ýøɳûçÏÿüû~úÝKþÕ³Ÿ~ru{6Î\«}ÒàôùÁ€@˜×"“¶nš¸¾šú›·÷¿þÁãOß¾üÆíîÇ/ÏÿÝw/ýñ§Ï~öñÕŸ¾ñÏ¿yõO?¹õöíêÍíÂÊt.ª‰±X,(ÆBáT @*¬§ñÏæ™+‹ÉW®ÎÜß-_,~ñîåß~õüÏúÑïùáo¾<þŸ|÷ÿûþôÇ_}t¼WûôÕ­_~ùèõã͈ÌO=gGFŽB»Õc7ù±áñN:°?ŸÚ[Ì­wÕÝÙ轃ÎçÞçñ…wž^}rÿ†¦Fl>Ò±$‚$…@3Ž-—©kóò;×ÛóÆÁçoìÿüã»ÿágÏÿß?ÿü÷¿xŒÀÿöÁŸ÷ÝÞ8Zîe#2 YQŽĽyNÃZô•YùÑvú³§«?ÿôî—ï^úäÕͯ>¼ô¿zú?ÿñ»ÿåwïþî‡wÿáË¿ùâ棣¹t$HR$0!‹ËüDHLI|/A>\S_?̼uTúü•…¿ÿþ•ÿñû·þôõ«?þààw_\ýן?þÓOŽúÞÎÏÞßúÕ'û?|kûÒZ.—Ò- \äû­â|#6[/ÍG>8žþ»÷/~úÊâǯ¬ü᧯ýúËóüâß¾¹õÿÓÇÿç¿ý¯¿yë_Þúo¿ëï.¶"×tndÔhóx<O’­t°,»J¢u¿Cß^‹Ý\‹><(½ûpáË·w>y¶üóOoþ×úü~üô_½úoøä§ß¿·1_nu§ô\ËðUOk¡j4°\ÄÁÛß½ÑúåG—ÿþ‹›?xsë³×7úñÿò»÷~õùí/Ÿo}ýñ•ÿüë7÷éÕOïÕÞ½–ßl…’ + ¨‘QËÉ#N£QØšQïNƒ»<¯ï•Ÿßžûù÷ïý믟ÿ‡Ÿ½òO?yùŸ¾zòõGW¾þ`÷G¯Í??*îM‡+Q\ °Ï`$8«¹j¥Þo¦›Iª›@ö:ô[7Û_¼±ó“üÁÅ_}qýŸ~öÊ?ÿüÕ_~ó×îüó¯ÿç_<úýo|úrïõƒä½R6.Ù]°Äa8!ŸRr^ÑÑínøú²r¼¡?;,|úlå÷?:þõç×~ùÉÑÿú_ü¯ÿô£Ï^ÝøìÍKw¯­£áô€½,B»Ë2“%v;âƒò'OÖ¾|¾ÿÕ{—þí7ïýŸÿöË?}õÊß½»÷öÝå¥éœ"PõÀ +&CRRt‘&cAßT&x¡—¹¶V¸·•þákëÿðƒûüé{ï?Z{ÿnﯮüìÃK?Û¼y¡ºÑ/r… œ"ù$@$ÀbƒŠ¢&Ò‘H”Ck:¾ÑÑ·§¢‡³Ñ7Žê?~gïWŸùöág¯n½{oåÉ~ûñNåêrf®Î'õ ,.œà²¢Z àL\¢‘àÑ^.ts£ôÉãÅŸ¼³óÛ/nüýüñǯüë¯ÞýÅ'×ùݽ?ýäß~ùêo¿·óùƒÒñvº“å`¯Ýhóñr!šh±œÊ|+Š¾)Íue:ôx¯ðôRõ»—þë?¾÷çß½ó‡Ÿ<ü—_½þoøèçß¿ýÑÓõç÷¦[‰F(҇ÓÙ:GÓJŸ¯EëQ¤ónT©›Ë©'‡ãÝÆõÕüÛ×»¿þôæÏ?¾ýé“õîôŸv.öSY^¢ g TNfhÄC8áµK˜­¢b 5}½—[m¥æ wwʯ\ê>»:÷ÊÑÜVSÙn„7›ÚTä:ptŽSÀó‚Qœ±BŠ FÒñDRá"5[ŠnOe/t"OŸ¼¼ð›OþõOúá•ïÏ~÷N÷ñzôÎœr¼šÍyÄŽøüp‚‘–—Ð&-°Û‹!n»ˆ9³"2Wn5èÛ+ú£ìûwzüÉãÿýç¿ù×_¿þ»/}ïñÞn¿XÏÆ€é5C¼"];䲇$*ÒY•ª'‚³%e§ŸÙ›ÏÞÚ¬|xõ£§»ÏnÌ­•›@žŸÑb3ØÍZI´Ó•9Þ¢i‰Á°|TÞœŸÚßš«Åñ ]ùþnóýW®mM,À{WÚÑJJREZä8 Xýa¢úèΧ±PJKTEQ @¾%d>!s•éWô™ÂÁRýâBy­›iåâÍt¼×T‘A1ÔˆÑEL8h˜Š{‘Åâ´[,~—‹'µtj¡•¿0¿¶÷ÎÌ—Ï/|ïáÒß;úñ;W~ðlóóÇËß»;ÿürcF•h·×eÇ)Þê AˆW vÂëÁS‘X)®k¤£¢úÖêÒÅ)íÊ\ôóWÖÿËß÷?ýîý¯?½ýÅ›—]鬖zõ¨ ñ´˜äõDE JvBEKÅL)¢"‘՘ȄÙfZ«Ç¥NœÞí%¯®w¶{¹²B¦¥  2Aƒ6â§G|C€™¢˜A\Nòi’˜Œê™h4§«QžÔX"ÆÓ•q{–Ë嚀ÐÝ~Æ ¼ŸdZ g`­ˆÓ…Òd0ËMuW¦ê½¼šÉ…/-òÁ‹Ó™vr>/.¤^&<•Q3"c6L:ì>åò8|zØþ“cãf·Í"=ÄäÕ`œó5ãt?ÜŸÖŸ^é½vmîÎVk½UPá¡p†ÏÌ¥ºW`* +ð¥HZà‚EqÁ ÏeS19®ã*+…¡Íò‚Ÿäì87bó¼6?O)£Ø1‹—#-8¸——‰% >—CfY€b‰¤È+$ÁÚæðÙœ˜ÍM˜]Ĩ6!N"Î.Ê…%˜KÐR!%¦‚|Ôã#Aš°¬\­Î6› årOk½@òá¬Å…œžsàv"æç+©Ì†‹0! †GM~¼L„¨(ÈQEÊ'•©Z|g®°ÔŒlÏd/Ì5ú•T=®&´´¦Øìî“C“ŽÁÚö³&ß ç-CfÌÇ€!žKã©&I§e*„c¬o¹ž:¾¼}ÿúþ…åþt¹¨òŽ!ÙˆSí˜2îf(½l$ë{Ñä4I…»Ý… ûw|59áD±$%b±B*]ѵ4àíVÔå ŽŒ{OŸw1+¬½’ÁÍ;Q•&,fÛáµ;áƒoÌ gnõ0.$¦†šf F»Ó ´¸(ŒÑ=XØOÆH¥Œw žà·N 6‹„aB)EJªB¸âƒHi…lÉe¶‡y=¢ù {q?„P~ܲ9QÅO%ÆÌØ_~{ø[/Žž÷xüAžW1êóá>/LŒÄñ„×ÍÁž á Á¸Á˜`ZkêÝP¼ËE»ÙîN0^¶£Œ—R­)­Êùy­¸Q– EŠÍÙa›÷Ô˜cÄN!ᓘa¢=T(øØ$J§¶qµ2âáô`Q†›Š›|¢ û1qÒâ;5l™è%t\(úÙŒQl¨lDü› ·Gô¨“wqî@”QË¡hUòáxã¢à¤˜s⪛Œ¢b‘J¤\äbMV¯|ŒrŽ@Ì KV$löË®@Ô‰G Ή +­RRÅ)¹ %;B¬Tr~„ÆHŽu’O l“6ÈK¤Üä2«N:9jÇ_s½8bs±.*CªM£—±¢#–Ážü0pívÃ,#ƲM_@´!ƒåÉ›v`;¦¡Ð‰‘Áö×T +H­° +ó%#¤˜¼A"ò‰®˜ì Q£“ñQBÌ‘Z͆E ^Á +Ë„X#3ád†Í“ÕËl´PêN2~ÞÆ­$ÅêT( +æ ÁÌœXZŽ56ƒé¾ŒMz8_@Í6·P6¾:ëJ™Eµ°Æg—Ï»…“ÈÉ Èä,ú«cg'|„ÒË›JyõŒnþÔÃâb'lÌ·OÛNO@.*AE¦ÄÌ«÷Œ<îæÏÐq+†PšÓÏ¿4gâì°cÒŠ»1™•ò$·OšÜ^?çA÷³ nnÌÁLxB\7!ê·^œà2óžP ŧ—®d§¶l sn6‰ËyR-£­\ïâ$4Bœ?”b-'›ïµâ¸ÖN5/®\}K­®¼dÁ °ˆÍ…ò[x¤çå²Xœ,KW°oîŽÙqÍI& µËÄû¸R'0¤YƒO0#² S `žŽ¶…Ìt@­º¨h(ÞÖË+¸˜£ä²_Èa‘–\» ”7B¹&ÖÆÄl~æ W-0OȃvZT¬‡+N.ëfY`[¦ö;ë·tôœ5xCþPÞŠª.&é ål˜TJùÖv@k ö—ëN®à U1¹g€;1ùÅ1æÀ¬P0 æq¥ëc F'mvÑ”TBÄ¢SÁñ‡K+àùÄšO¬Ú@#2Ìç@®Ù` eÌþÁ&Û.*æ +n.?ìO0­=ÔydömxËp(ð`p’—CÅ£wÆœ¬Õ”›ÉÛ (4XesÚŸôÓêà–帋4ú‚^!PÒ[¸\vSQ¡Ôñ“°lg2\n•ˆõ¹ì2—Zzi¶p0‡‰yvàq 3ûu În2nñ‡‡L˜‡Mq±).:eAÔó|Â#BbUÚ¬R±ù¸³ãÞoŸµ` f7ëÀu£W´"²ÙrãʘÁd .ëÐñi"Öã=—{iÔï—°T§{ËÇ›«F<ìdãvp}S=ƒíÜ#Ád›ì4ŸîN"¡!7=á—ýB5Þ¾Ÿ¾¦6v=ƒmcE4\¡â}*1çáËv: ÆŸy”§ã]#6@b@kó…m6½‚ʃ-Èì„S6B1A<.±‰®R^Ó«[ZyMÌÎÉ™ÙéÍ{¨Zñ…Kjk¯¸|·¾q¿¼rWk^Hv)¹ª&:­õ›F,ì c ´¨Ä|¸¼ï^Žu.‡Ëkj°æ”PëˆXñ³ >Q½…)ÕêôAoû1&—O™`ªðÙ¥Hã0\>€Ä¦-4ƒ¬!c—2CœÙÏ 9™17oõK.B÷²+åÓ|fÖI«¶€j§3°Üƒ©µIõ­¨Ù'»Øh œïy;édS^¾äêX¤‹E bV/˪e:ÒD…¢–€è´Ž] +VÂù¥qGÈ€6……›à»LXd ’ÏXh3ªóé›9k‚íˆ_*áz‹Lù¥Ê$$ ¡Æ¦t(08A—e’s°Ü ã3~¥eDuBëˆÙÅaKÞŽÇA²€ÏgbSPG0݆jt´kôH#r°s/$ÙóË~±6f#F,ø¸3òèÞ §'¿}ÆôâbÂâµÍĦ±.‡RÓ$ †—Ð;lj.1uÔÜ|š»e¥À‡è‰n{ᲑˆL;F=l‘ŒöåڮܼHD꬚—ò}¿\"´:QL®‘¦XXæ3}¯æ’íþÁÓÖ…ãÁ¶Qåu29çær ß3õµöúíóN —ôú…hûJræZ¤½‡Çzv6˧ûµ•;‘ê†Á+‰™ùÖÚÒÂÕ\ÿ¨»ý˜JöLÒ÷M×Zˆ#M«.Þ+.Üõn åm7•r’ Û¨V=m%'\Aˆ+Ðú_às«ryÕNˆ„Z +klf™ˆÏ&zWò ·»úÄg’íƒ@†Š… x¤Í$z\zÓg&áÁb·P´]›¿âc/⊉v”ê.A¢Ó¾bð…£¥u>>5ìâìdÊŒè.:ä–C…-\ï›±¸œž^ܽo‚åÓ&üœÂ… -o5/¼ŽJÕK/?ÿø+½´xÆJC2¦µ£ýkùµ‡ÉÙtrv’”âP§QwzqÐ K âG«î†ÒËŽÙ+€c†‚Cf?ЮÓVzØ#ÀJ‹N¯¨ƒo9ÏNxlˆdF³_³¡;7@ª3‰9©¸6áæF»oÉN2æbÒBaÝE%=tÂÊ!J+Ž·.WîúŬê€Jkl€  +æ©h= Up­’è^ eú˜Rñ‡+Áô¢Þ<Ô[‡\jÎËÆü¼Ž2‰®7”†„"¦u0½ïj`À³Ó—ÓÝ ÛÁdèX@oÃR-›f2³¤Vi.NïܧãmPÅÀ{™h;9u_=f²‹|¢Õèï.ÕXºº}÷ƒéÃgÅåÛ±Î!ï ^ܺ½ï#,;ȘšÉL_IÏ¥fÓ—mÄÎѳÆì W!.‘è–—‹ ·“½kÙùc8ÜÀSÚôÖãp¦oô‹v2 +"ÁŠE€ëv1)X,ÓóZeä2Jwú;jaÚiµ­nµ×Ž»[SS{\jŠçfû—'<¬ÒÈhS«ì¤{7"Í=­¾#—I¥)/a“ÓáÚŽXÛ$@úˆ%½´Q]½Ï¦§´žì\ÔÊ«¥™½¥ý‡õÕÛ¡ô4%—š3û¥þ*æµÂüòá³ùƒ×Z›kë÷c‹´šÞØ»·|ñ‘P͘æ—ʽb \Ù¦S PÖrýíÛÏ…LE¹¸Ê¤¹üZlêòìåçõÕc15;·ó°´tݯ–B¥e"µ©STjY*íÆ;—Qµ‰‹™í믗g«ɸG({¥*¬Ô|Á\´¼‘jlO¯^.Ï]òˆ5dЖºm)IÅ€Ò–o d“Öå¬VßJK±Ö!¿ œ0?j Dé²×¹®¬kÍ}­u)Õ¿iб(@GOç÷€÷«s5h°[\‰5/† ËÁD8X0U_«Ýq¿vÞ „KvLÉõ/÷ß(n>’»J}[®n!joØΓáj¹·ò«Ÿuá²Ù¯ø˜0lˆXµÂÚKãÅ/áR͆ëgLƒOÁ¤*µØä2€SPôh»™¤#7Ãa03Ø@ÞËYÍÌPÖró\¢ ‡‹6TÄù4°R|fF)/j lq•ˆO½4á&„V²³Ÿè\ĺî`z xr.kï’J &3ÓûååÛ`ü›ë/§úWÑHÍNj¹™/,FéUDšÓP óZy •‹Jb5)Y‰–g¤ü,Èn8\&Âi-?ÃÅšçí8"À[¬¤b!e2Þe…ܲ”›ç3].Ñráÿgï½~$;³<±wAý Œf{šdU¥ ooÄõÞ{Þ{“‘ÞUVeù*’EÙ$Û°gº{zvÜŽfG»FÀÊAôèEÿtnµv±Ò“(±ÈÁ¬ŒÈˆŸ9çgnÜøΛ_ü×—ßüWîüE0ÑTééÑâìö;@Ê÷.~hŸþ"\Ù8û¥>| Q”¥œæàìù‡¿ê<g(kûô㯽ù oü¤6¾ž¾úÛý?þø7ÿ® µµÇ‡Öøåòå.¿ÿçÉó?€]}þÅŸÿý?ÿ¯½Í«œÐà[Wló1×¼\ýúè«j½¯Juté/?îî;±JYc9ÚÔ¾üËëÏÿ¢>{|xós·!Fk¹~XQ{9.fåÙ?þÚlp\ðábqùíæöÇpö‚qf‚;í®žGãKÖk£gÑáWõ£/ƒåàЊP¯ +µúâxÌTUÉS6ç/•þÓ`õ!X¾ãü9eôFçoßü +Ød§¢æYŸ Võƒ¯jÓgÑøÆllr¸A«A}zCjƒ æ@í!F– mÐ\|Þ?ú^©¶ÏZDB³HyE¦žÄœÛP§Áô©@0+ÍÈ»²‘‡™k‡Jç·§ˆÚ¡ÌŽZ_xOª-êëWÁü™7º=Ö¹øÛØ…ÀŸ=1GW!¤°KB´Ü3;þø¬:.׆'_ ÎÞG‹gFÿRh vŸpÆÑü° W·Ãó¯×Ï;}òcŽuƲêóÙõ‡Ø·Jñbp08ýbtñÅðüKÐH¢;Õ[§€Õ{ ² ˜<9ó{P8îðø ܯ*N{)L®H‘Ý?é,OŸ=ýü7É%"·ÍÞ{ðÜì\ÍË@ùk¯½ºùîäůªJ»ªöÙÛÆÙ¯¼åFïÊÜÐÞ ûùËßôÖÏÁþ#J—vçÍûã×ÿrþøûÁÑ»Öâ ¸ˆ¿þÇÿiqñMÕ˜øË7õãx[õöé×áì9©õ/¾üçÿåÿú/ÿ[@ì(íKö¶}üM}õÚéŸÛÍÍ“7ñì›`ý9jtqwÎGGf÷ZjœÑîŠÐ‡Ñàluóñ&˜9CõqžO[‰Á¬³y¹yþK\mŠÞD¯xÅ/ÇÚÓ,b^5kGO~ÉY“휘¬˜ ÖÆç?4–ïÀúáú$èžýå?ü÷/ÞÿîQŠÜÊ EyH;ëÚäecòxS´ûfÿ\Ì*&ÕÍÎçÍ…æ)î.ùhS[|L_³g`xÁú+­C±v@:žpÑàöòùŸëýó=TáÜ .¢åsÆàZ—0º¼7j¼‘¢À”3¼W¯;§?ŸÞþöêë5¸ü7ºb8õÆ—9ÒAÅ&¡ƒvZÖ”ñBm…ju¿w Ô—y.Äaž«øh*pÖ _¿zûµ±U¬´N´þ5D¸àŽÚË—Go_QÚŒ5€ Üd}4»qÇW•¸ô^Sk¬Q¥‘'M1œù½ãÉÑK»s¸‡h»ˆž¥ÂÙ¿jÕ"³¹j®n×O¿[<ý!ZÞÆ åÍôöYžö +H³ïOž.n~uôæo•ÞmIè&«V88÷Ç×%©¶ªÉŠZ`댽4ûO­ÉóãVÅhzñ5HDà#Rï¢J¬nkñºø¡¹z–°¢ôýÙK©¾)‹<¿Â¸ñõÍ—5¹üŠ6Ú_`{Gç`¦n«Z¯" OO¿ìÌŸ6j­=¾m~¹¸ýM´ú¼(v²˜í·g_BsöLjƒYcŒŸŒÎ>„£Ç„Ü8½ùvqüêãNÎ.®wÀgÍŸþjýü/(s èW¢V0j?‡»å(Ó{ZóÈj4†'lüm2SkG«/®–«ª©_¥\ÑhñášõW„5¡­±?¾2Ú `¾âúJƒ‹ÀŽ½Ë"ëWX?ÞÎ]iX½ó?v¯wúý¤ãÕwRc“¨IÍ ³™}±}„Ûƒð¬7fOAвî opö•;}j®­ñU¸x¢õÏ1£öß^hH¢µÔ8UÛç\°ÁŒQInA†_Eó§­Cï+=Ê#ÚcF¼c¼Ù»˜?ùEY¬CÇÆgïÛ›ÏAf˜ópòâ¹,6ä`¦Õ–ñv|JGi+Ý30‰UµIš-Rozc§{”Åå4*‚ÍÜ-)¨:€ uÆ·¬5ÔÚáé«Í㯸X†KÊ›[i#)Xæ)K 'áøÒêz}èöiEŸäipåíÞú­Õ?OáZU HµS CÎYhí+ÜžeIGõùî·à”·24È£u²¸þqvýµûd«¤eHóWiÌ(Ò^r ¥êÂj†“[ @2˜Ô'—¬ÕÆ”°"× s¬6ýÑc³w `^‚æü¦}ðVSnrõã4SÃåè"­y‚IMÙÎ߃p…‚"ÍQ¼ßåœ=ùfqô\´ZJ8ŽÆçÍ‹éͷ÷YÒ·l(˜Éžl¥ùTÅäüE0{>8ýÚêœV¸0…h¸Ú §·fçìa’ØÍ1ãA‡áfö®[‡ßZÓÏ­1˜‚QU +·Ç»CÒìÅ(¬0kZR~b°,3v4}-^™³WBí ¢÷ +b3C˜´ÞªÊ°[÷ô›ÁÅwVÿ1,´3¸¢…Œû›Wó¯P­YQU½ ü~ùå_?ûþŸ¦·¿Ñ†7e>®Þ z ‘º€]îàúúÝïg—_ºÃóy,̦E.‚@‡püöáWÛ?Ñ"ÚÝþú©Û;bìa !¨ø`e´.a°\{%‘u&bt ÀÒtNËj'Ã8˜Òì®^ZÍí ú(]Ic*ðª:ÀìiEïò2J#ꞀBKbºÔ8Ñz×æè9ˆº*¼œpò`{žE“³2gW¤0Ï×Hsê^˜½XÖnB®¹@öh·¬äé3¦Bí´±x×?üº¹|µ])¥=8| ±Á<åØXRi]ÐþŠr§yL]œ~ÑYÜ"BTæ!ŠZyÊO¡†äÏüþyº*í˜?îÿLÅCî—¥VŠŽ ý„£ëhô¸Ì…Óëï@mFë7Vÿš‰wæt“e‘‘#¯½†˜/3cÅ;ÿø£§1&ˆa‘2»³yü¾9>áôšMËr‡ ¼ÑS)8 õá~žK#Jcò¸ôzQ³˜•(«ÛYB×Ü8ýë*_ËcšÛkLˆÍlÕ£yÌ)þ´"µ½ Ø©ô7È­s®vs­k:<"iY‚pµ]¢}¯¢>J3¬o›ÆzãŠX¯òu\nËÁ’²FàO“¸…°îúú}mzUà<½uÜX¿óÆOõÖ<œD͇)r·ÀËÞd'KÓF'œ>^|÷â7ÿÃúÕï­ÞÅnŽF)}tôÚéBdTæ)DšJFi'9ÞÇ™ä¨üa–… …¿2z[q»´âÈ^GòG@|09IDþt¯úg[å ¢1jWöÆyRMUEBmç)цÈÑ*tö «6­ž*1û&YY 8T+RV¼WFýÄÜ‚±Zâk|°„섔႕\?&/ÂÅ[­wžcü­<ð5Bë2ö€rFzçÌ݈µ5i´ WÂ8Bò”h¾²Fó»Ñã_VŒQ¢*“²k†«6Öê3©61:ÿöêÛݽþ–i¢z_¯¯eO1¢uÿäóoÿÍ͇;¸úó,nW×é©­cÒ@ìIõ¡vÖ\ån1µS"u³¾2šG¢?+¨½DÅÌѾP?êp†7|0í½vÇ׈Üdôں¨­ßû3°3m=¿ú àwÌÒÑй £PÛL¯¾é¼¢Í®×9쬭wo$Xe©Ö:Ëç³ó¯³ËÙÃhúÄg0¾ÑÛç¸>jÛ/+BF¸Ì#"Pøð¦vÿ1ï,µú1h€<úÃÇJ´ÊbfUlRöLïÝ‹7Íõç´=,%õÎøü+1Z¡j—²çº#*1>Dgv÷r´âÂþ‰ÑXzÃkÒ¥p#…ˆš7´ëë|ü¥¹ÐlŸIÁš³§¨Ô*ó½’¼WôúïÎe Sº˜>dܸî¦vãß‹„ÓZF£k­?‰÷ú“bÄC´2 7UÕw +lsv½¸ú†Pº’ þDòô!X€ÇéÏoZ˧y6ÂãÖæëã—qõÅ¿\<û?»­p~Ø9<}þËÖâiq²¤%lˆm½q$ZFie*¼ÛÚ ßC¶RæРÈFœ»Œ/´h_‚D¸°»|Õ;úRnž +µCÂ¥q Ö‘·AK¼š`½icý9„4'{£È!‰ÇëMÁlrF£1}¢µNÁ=U…:ïÎ +”÷(Ç™áë©Š†K-É_±öœˆ÷”¶P¹“®šÀ.ÌB¸òuxS˜%Hpé —øæ¿Ø®€Ð*sZ +s |£(4@]tV/íÖq^Ez¸ÜE@ÑITIÞ/Æu`!G`ྞ¤ƒORä~Y(’:Ø@¥yZ›¿¾x÷wg_€úƒÙ¿É@“Z[JÁ\hœð­ ¹^ïÃàüǸÞg™Þ\j‘Ñq‚oÓ8®ß‚û@Ôë´37¹ü>œ>ËRZ¢Êbj¤4´Ö¡µüeÜݼî_}'vÎp{ŒˆMÒñÁ•BÈ §jŸAÇÀ —Ìâ:k ôömO㚃öȽt&oÅÚ9môeö³Tá}1Üdq·@ú¬5Ö‡¢?³Û«¹@ø÷g ow*ֽɫúê½Üº‚¹ÍzOŸÿØ\>gdõ~Aì r—¶'Ñô)cõp­ÓX¼œ¿î¬_ +µuAj€–°[G­é^[&±H»9ÂÁµãÎ’Uc+ËíE«±î®Ÿ—'ocå ázzñÝìúG):JVtZïž¿üÍì쫲P§¬ ð¦Ý»¶µÖI|5"›í£púQšŠbŽ0c"îù”T甚ŒÖ7ßè“W{TRqs\–:ˆ2°‡·9ÜJä+Z¶g/ žSUãA’ÜʲˆØð‡ÐÚ-ŠU!²{çµÕˆ^ÖÚ Gù ÄgŸuV9"ଉ>xâ-_+à¡ä6Bû™Šž(Ê’9© .æg;å‡ ŠV”Ú+³>Âù lQ/Ï 7bTwõùmÿømÿäÝàìKgt*‘2rm Ân­õ»ÅÓß /Ù8úŠt®Ö§[UkJóĽZOôþëÎÁ7ãVç\©ƒÇ¸3«­¾ìž~Çx+T¬ ×·~ÿ¤(Ôò\”gj¨zÞ9xßÜ|CYs˜ó2 úm.ÕÏÀ£e¿Ì×+r;˜ÜΞüX; µEoFZcX¯²Ø„\`쉡²›¬Š ÆÌþãÁñ—›W¿#ÕNYM¢¨§}TbÀhûRíмݼüëÕ³ß1þ"…Š $ÛËÛöò)&†,}|íÊ[göŽ´¦ j¶ l\?¥¶N”¤Ý‚9‚Ågó÷Œ=­ +a¢,”iKrJ«ð*þ„‚»¼5kQ± #ú D¨ÃŸ¤W7CÅ*Èœˆþh;C‹Ö(<®Í^Ú۪ت0.Á{Š×W½.Ê[‚ÕkŽo¢ñu4{.D‡¨1~”ÓeŒ/©ª'J`©õµú‘Þ:äö^Qþtç­~gñ„Ò;yþ³›¦"¦vf¯¾mž|0TŽÚëþáp‹?ÝÁv‹J‚ðQs.´nìÞm3&0Õ›¬.¾}”¤~ºÀHknWø²Sê”ÑUëK£±P£±èup9,Å[iÕÜÎY8}ª6Ö”Ñ'8P°D¬ªƒëdÀ|ÕV„Ù•¢µÑ½’šçþœ 6¤YŠO6åÆ‘5x +†¢ £.ywê®øÚA‚ò¨Aj]§w¦DË`xÓ:úÀ„”·@´^šv‹0Î3‡Ĩܸ`Ü¡ Açè`¿ú¤Ž+MPéŠTâÕkzã\ +¤h™›%=ÚT¤v’p {Fûjë\Il¦c§HCqV€—Ð;t°aÃCè¿3¾MæÃ,“ÁMXîªÜÆõžmho„ÞY¿ &Ïäúšj`Çh{¾,C‡{½@ûZýØSz;y.î¦؈õÖZûqwó´ÉH(MÑî§>Þ­Z€™o Á + $EªjVå8Ží&*2aŒÕæy}ýh†ãme™\E=8û²1<«²n7‹Œ‡ .„pK(šÓç«ë?üý¿“n5Ò˜Éȇ§¨“`•²UÁlmw”ÅŒ¤«È$QŒÑ'¥z +˜F68à‚CT›îÅd‘ÏVDÉê€&ÿ$A&+†Ù¯ÈrÇϤÆÅ^ÕùÙ. š7ÇFí0ƒêNûDk]0ö²ÈĶ ²‰Ñ[¢7q{g¬Ñ‰7?ý"û\ª©áÒlŸzƒK­q S)"¢—Ï«¨Œ1I#†sÚêPF‹÷Fb¸›'˜Ü#Ôž®ÍþÕäñ/g·¿µF·E·'À"V„ZlÊÄVY4Å;z /ÕúÚZ#YcDÐ`©†/”îcà¬,=å>^ÔZb&Y5¾­6Nik¸•ÂRVp‡YÂaíª S¸Ÿ!Lí á¦,õ@‰ µÚßpáQ†‹>ÉsŸæ™®qÖ°Êù„àÍ ˆµ{ÅÕŽ½Ÿ$ P\˜Ò«Š £}áO_{çËÇ¿ö†7²Õ3ê‹dEÜG¤²Ø¦í¹Ó¿™œ/ÕÏ‹l}¿$)'GXûeðMÎ_Ó ÝÛ— ð ”³“c³¸0Žimðƒe©‰#ÐðJýÌîÝæøQ «@ˆ‚Vê‡f÷2‰; D}PïŸ3 +„˜«ª ²¨–( +É’RÂuË´§ýùeªÌï…ø„žÚ‘kÇÂÛ-Ê?Û)•iÛ5êÉn–£Ô®t!I)½Ñõ(I|¶S¦¤WÓ‹Ò˜'¼áäQè¯ÊGE&ØÉKœ9ê®_¡J#K˜pw»ÙÑ¡Í)üÜ)(a÷øÛßý³×>ù,Amå4\€2ÉZ™uAír¢:qE©ÍxŸ^§¯FsÑ›‚$0[NÿZiœÄ§\0*ÕJŒ] MàBkƒ"*q!©‚–; gÏ«—áì‰-âMØŒNŽ´ ¶ ¤CC±àMäú!„¡ÁH[yÚC•výà+{òÖ½ÑzquBï‚Š&.8.6ÜÁs£{+E'Rí„õ–ñ†±ªB9Š?É“ŽZ;1Ú7Œ{GÕ;ã¸2»VãDKEîQÞ¦$snv®¬öe‘°i1\]¼cH&Ú?$¼¸r½Ñ>›Yâ¼½² ùsçã9I6LëebÄ•a¼r˜Æd\I0%ôÀi‘«ï”êÚÈl]ˆÁr¤nƒfF¤&n€è¤q}Ñ sÎ^¹ÃÇ„Òzòþ¯6·¿"¬YUö4/4ö1+G{žÀÐT[gŒobY¥} Z °­ Ï[“ +BaBÖÇVãĪ1ÆB}§À},m¯og°½,³_Öªêz¦€H¤‚Gif°~ |º•÷ +2PX³@¶™ÑŒÖ¶‹~ºO lT¤½DIÝÎðð³@G¢³PÃÊG[Y:Q–ˆ +ªŒ1‡U¡™'ü2íE½£Ñæøý²ÅzÇ Š|=φ%±•å¢‡%i§$fq“wfNïB +˜\ËÓ€®r•I%ò +çoìásÊš2Ö˜Ôû)Üú,Mm„@``Z·ÀX±°6´3%Í!ëŒá–a¼’”Buˆœ®`2ÀÑHpç #åÚ8ÚYýk­}JYãªÒ„, g´W– +„ÁÛ#ÑŸC°UùRâÝ8ýþ¨hHŠ¢Âi½DI€«²†œ5N„¿2æ\Ìj0§÷žBXšís0 ñén³ þ=C˜¹i÷oôîcÒÏ1þ£—gÒƒÑÎáäQŽªåÐí$jït·wL= Ð4j‹Ñ‰Þ}ª¶®Œpå|‘€é2¤½S‘g%cpÝ>ŠO†€ò,³ž]›½ûþVWï+R£ªHoI8³ª_`´N‹”O© §ž¨h€qIt*B©Òº1ûÏA Zw¶yWa|HØ4€9áe˜º3~1:ÿ¹.%«¢\˜Fuð›o{%åQN* =­ó7'VXàè.Ì*©4°²„ .Øè\F³W)DËa6ÜP¾¥„GÌe*¹•Du1˜#\ðg»Õ$¢W„†è-”pÉY£lg¤ý’ÉX³’Pg¼šà ³¦i&€P‰OG[c!XõsöBk^bÚ°ÌE˜\§­mêó7VÿIEê¹vŽi"B7Qµ²¡Œ_‘ê„Þÿ €XÇu º ¢Î>j@„fÞ¥"¦«qÐ +ëbRÄùcµ}ÜX¾ñ&ÏAÛ RÖ;œÑ®/º¯€Ù«úCæ´ÚªpˆRÑêÑ€ZHkµªT%`ä9„J…õRe‰Õ;Vc#9cRms Ø´½’ººš'OÛ¬Õƒ¼ÐšGL\ËlÊ™±‡jcSëàÇÅXÀ¿0ú…è„vVÀÊ{ÀX]í,¹à’UÛRý I:ú¨hµÏY{<µ_AƃÓï¾ï}]‘[EÖóWeÎU¨jiÌ€˜œ¾Ÿ?ûs¹y„k½úâ-¨Äei7Czn;E…ÖºZ}™þè+ãâPæÜðÂ^EÛ)ð6 •În–uö°ªH{©u.£ jm'ÏBrÁ “e…÷hgChTˆþ†Ñ»;Yj?/IïA’¤LFϬá³Ã7óì—ÿ]0zù É0«€Û;9èx¯_fOêÃÚôEcùšs§ D~!‡E¦¶Ú **J RŽV¿ÿæ¯þç<¸*±]Z„1µzOyo Äñ±ZG •Û±Å; ¦Ž! +\|Nƒs'àña± µÓdÌ"Õ.åÚyšðAWçH4è")ܽÒft>¨ñFÏßð!¿“§ke6$ä:!E_(ï±îPimâ«;7¤¬;%ä#…‚Ñ "ÅŸÝøã'v÷á¢åˆöˆ3º9BÞÉ”³¨P M9:l®ßQÖ0G9IDeÔ¶`tò¨šC5\nJÑ=z¦õ®+r#Yö@—X‡´µ:GjTªÍ#À®ãòáÂ?5{ׄ9ƒ¤7Ê{ðnBKW%Ñ_ú“×”=Á®vqçgûX‰°Eg”­*É" ~Þ?œ¾0Zà”A¹…Ñø\vE  6eȸñ‡qó'¿X¿ø­GÝtvöA +b{(5Ža¡quH*}Jëå’…wFR°—DPqe$¥©½|º‹lg™­<¿‹h Ô,‚hgüÌ°>v7¸ÔÊ ¦Z;%tp©"å¦+êƒýòÃD—z¢qÞìåâå:çß×ן«Í‹TÕýd@˜Xh=LR%Ò~Ñ[§ùk5:$ô!°š(°yLM•…JüÙâ0FˆÔ-'Gó²pÎÀŸ¼2zOô¸ð唲:à£KbM +f^ÿXëÍ3À"àkÐrZûJoKî˜kiD. +mˆmPÔIÜÞ­hpË®ä/œÁ{üœoWôI,F9¾6 Oú ¸ÚÒî +x¢±,4 GÒ¸õqÇÑ& Ü*·`5sàƒ‹I.W» •‹™D¬l›‹w«—Û<ÿUp" Rƒ„Ð[Ðí ¾Ö÷9WÛTÁ€ú%0Ô€Þ˜ÔÆ”&…àLjÜ蛪Òݯ€„ !Á£ÉKwðŒ ˆö>ë,Á°pÖ`»$‚ñ¦·þì ¢³LUð>Qk+„µ«RM “óo×·Îó ë¥i‹ó&œ¿ÊP^@ë½[kôJ¬å™ðQ–L""ÂG2ñ¼\Ò–r7˜1±'ÚÃ"ãRÌþàø¿xAº›²ýÔKŒËZƒëh°3º®š_´ß·6ï¹BtQ‰Ãi;Ç&ËðœPf¦*òv†Þ/ØU¡öYÛÉ À¼» ç/²lð¨Àï€1$íc¥ãKz”½’ü Åé‹ ¨r½Hú¸Ô¡­øʇø3DÔÉÑu@?³÷Fú0ËaJ×Þ:£Rí¨H; o· +Ì>"Pr+_Ñ>Ûò¸ë ®×Ͼç«PRüÑ9¢V?j χ‡áâm÷è»’Ø©*#  Ré§)oQvË2&5kãÛÁÉÏGW¿ V»S!èØ­$µ*úN!JµÎ%˜d>;%²*uäè¤,e¢ªûì!rúxõñ»€T S µ£Fk»s„R`à»ýá39„Tm}­s‚9S܃'ƒÇ¿µF/³ ?ŠÂ~‘+Q–äÌæ%iN¸pcÅ•ü%È6Ðù©*̪½²z—b´†ž`ê *÷¥`MýÉSdÂqÁÚ=©Ï_ñî4Uá ¤f5×vëô“= Àìm +DŽ<ÂäÈEÀÞ"OêcÀ¥~—]vÖE¶•F=É;—ƬI2Oº%±ô—!‚½¢ò(EUiÕ[„ÖF”5Ž~>¹þÑ>œ9¤’Ì»GŸo`îKúGRã ¸ IX ˜Øô»§Ñð2šÜÂÜ&ª.9{@^Š\Š²µ/·¯ñ3¹sŽƒñr@ ã˜:8 €ì®«ê~ßA”$ª¹ þþB÷RëßýΙ‚—ð‡y†UàÜ%ï€7çœ1g«¶³´ÅúóhñbtõÝòõßðÓÝŠr"…ˆ’Õ¥µVž´R¨ +\£¶.ôÖ•lvóÌ~O’XÚ¸0Miœ‰Èõy|ƒÜ¦œØ7¹vˆ(=p[lpÌû+ZïÒF›³{z÷4\>[>ýqöô×æøYYä˜e€ÎYaJ×'BxWölžTô~š±-V8_VF|½îIÕœ ¬™Ö<ÃàSÚî·7/âó$„·á‘æÌi™«—ho7G!œãtŽö©Ü8«HÝfpΆÁ*Æ¥pĆÖ8¢CÌ™ÆN0Mñ#èÃ@µg·g_JÍC„¨Ú$\ÍŽ­hg–c`ðøÒ¯Ú¡Ú¹bÝEžqÁ6"¼'x#9pNÄå.»'¿Ð{·Ð[Tjg1óç¨Öα~ÕÎÉìúûáÙ7fó€Õš¸¼¯­Þ`æ*G×ÀæpþAcõÖ잤Q-[5ÊL‚²·ç¦ɲ[H­­×WΧ%a­øà¸ü=€ ®öP±•(«È`PMR'þúß$%i­…à‚ä“},È´9õ¾W1+êÀ›¿0goýÙ;%Ò̃ ôæ‹0y¾Q=lÏ•ú9„=©ö<e `U°~9ˆ±Úž¬è LùY,Þ4áë¸:H”õ½‚’% BšŒ1Œ¿»¡4ó”û0CÌÞ +îœ2¡–Ãm¯sŽÈí\OsA2þZw5f¤±0Z— öH­Ã@$ƒæmõ _x£÷.+Z”'iigQ›Ç[‘ë£Û<×ÜG­<â ¢ªÜ‚²Á_»fýC8N™óI°´g´¯ÆW¿¶Ç·L°ÊÓþ^E……(K`Én\±/£Å{¹uTŒ·†P@ŸTA_™ƒì"B–öÅzü¥*Lm~š$eYÈÁ +€[?jꮆC¹u†YãGá›ÜÍ`^UjUå&¼7Æfÿ¶¢¶ÊJ +’ºq¬uƒTj+ÑÃĽ‹H¢P+ð ° ¨Ò%”Nl…µÈFB¼ñE¼ùÜM *¸xÄ’;©È ÂIswúÚìßdI;'îÄ j&ËR× +¸¾“¡·³ÂÕäà@ 7àþv‹ĆÙ81¿“{Õ»ü¥Ò¹âÜ ©Oâ9ñ§õáq‘R¶_©êS&<7¯ëŸsÁFýY’0ë«'?FgQš w+ƒZVãºÞñ'ÂZ}œÚÌÂT’¤1o,¿]þºÄ5>ÙFSˆá„[cû :99¶FX0{W„ÚMáŽ7y±yõw­Ão•öõ^Õÿ?þiãÿ¿;ðÿU»È]k÷¹kí~ w­Ý䮵ûܵv?»Öîr×Úý@îZ»È]k÷¹kí~ w­Ý䮵ûܵv?»Öîr×Úý@îZ»È]k÷¹kí~ w­Ý䮵ûܵv?»Öîr×Úý@îZ»È]k÷¹kí~ w­Ý䮵ûܵv?»Öîr×Úý@îZ»È]k÷¹kí~ w­Ý䮵ûܵv?»Öîr×Úý@îZ»È]k÷¹kí~ w­Ý䮵ûܵv?»Öîr×Úý@îZ»È]k÷¹kí~ w­ÿ·ÿDš·õŸHûÏ÷÷Õ“¹2¹šü¤müd_4ÑÜ&—W‹‹Ÿx?ÙÅ‹+å`vupz2¹xºEÁC¥RÝ*F‹ÉÑVêÏÜ‚'mù«ƒx°6›-Ò[9x* ÿÿ¤—ˆ+]\_®ƒÉ¼ääJ xÍÿíáTãädr¼˜o}|t ÞB4ý“Ò–ÿ·o~r ÿ”·Jÿk?…;ü²‡n¶Ê¥-w«7(mÍã§F?Éã8‰nánÇwªXÇþxßùïW>þëü‡çÿ?ï|þ ¼óùxïrüÞçK;8ZPüõãhþ¯ÉK¥ãþé?)*‹Ç³E¤K[mï'½­üÇéûØýø—ÿ¨x¥PÁñ2¶5ØBJe aýÿö5ñŸÿýëþý¿ÄVzZÆ?vººõñ?ÙßÿØÏxÑ©-¬B"埴Íÿ¬"6²¤g\LiáZ—[¡YäZ¨6&¬)ªt®öÇZߢ· ­1aô \˜¡œ WÏ@ÄzUl)‡R›¬=£%,H{†[Óª>¦Ýƒ,å<™CTªÑΈ˜pCºËª9Ì‹Q7¥`Â8ƒ2_c¥i­KÌ—ù6nϹ›§ƒ4f"RÓ‡„1”GE¹½U’âBÍB:Ÿªê%&䜩Þ:cƒ%fÒ”]¢ï¡Z=AhUµ©´NµÞsò†­Ÿ–äv†õÓ”£6Nìñ3µw¥´ÎÍþ-l½Ÿ¢LëUäVžñ÷Qm¿$H£Ì:¸RG•æ^Y„÷B„VŠtÓ¤—e‚ŠÒŽUå©÷«r»,vìÎÚ¸(I½$éîV´ýªž¥Ñ_°ö8ëiT£õ!kO1¹›Áœ$¢(—Ñûn÷Þ±Äø¤ÚO”ÕG&ƒÙ9ÒË“^Ž +p}Œ“ª:¬(ýDK¢Vž +Jl­ÌÖu‡Ê“~Ehgw‘aÆòt˜%Ý,á0ƈ³¦%®–§Â½ªùY†yavŠâ~Q,Ó.ÊGÌ!f&¹HZ;9úÓ$ºSR˜/Ï^Eè \£@‡ÉŠ«€ªÝåîä˜ jì—Õ¼P TîW„.® )sJ›£æä‰Ñ:H£r‘­ÑÖL¬0î$…ꩼ…0]ã’ÔIân²jàJW×¢;+Sž®p½›e¼$nÙ4§”5Ãĺäöˆ¸_ñ¸êåTõ)j̪J/®à-ÔÒU¥Ì‡|¸6ÅÆ ãÃ:3,Sãðæ×áô¦ÈE6àü=z«¶Ÿ¢Ú(ÇF™¸N¦%¬4n¤ñ¸RGYlŸòl=ÏÔö+ÚN1.¾—!̸‘Ö.pQYjsþÚì?«Í^ο.J~†6w¦t®Üù;µ÷µç¨1&´Áêâ£÷9¡–Ä >8 ï­á+Ôœ£æ´ÀÕ`&ó”Wb8øÇú0\\÷’t`!¶rìNž‹ÁJm˜í²ÚÍóß,pÍ¢Ø)ËÍäÇ’A¨>N“qÍØ4n§q+YKeq³Â‡ÚIczQ³U3B4¤2]óÛVóâj;Mï¥í,û I&5Q$õcV%®Ã+ÎZ&*Zžt‹LX•:E†ß"ô‰Øà“4þg‰*D&¥ gÁ[óí}–"%5‹Å[˜²_ÙÎeÒÁä>®*lô(CC7Š´¿[öf Cø;%#9¥¡‰Ù‹"¦)‹Û0 9®D‡jý\o^©õKRYÓ—þqrñåŸ<ʲÞ2.«8¹Å­q5>&Q%Üýªo†š¼?çœI•¾1;ýÀ³nl•Thš½[­û¤*ÖÎM”¤‘æˆØ.ð׬ê!84:—Œ5)–àŒ­þ%_[Võ6ª`}I{.×ú‡_HѺĸܖj'BãÕ'yºHSTbƒ,imؽ²1à†ˆÛBåa™o¡R¼izU.ÀÔFŽõŠbCnœ56ßµ­_åEU|0g£%­ªæDn]×æïœþãÎò•Õ;Oǵt!XvÑÞª¬ ªÆ‰[§ôa\4µ(햕ݲZ⪠©Šö Eî¥2WƒHÞÃÌ]ÂÉòí¢8Dä1_¿()­²óÌú›$f픔ϲ|5ae3˜ž#m€)ø™§Ü,jîç…,·›r˜-XJëoeY*xüaŠÚÊ0²t +³Êb›qsE Ñ?ÜMw1©‘ck¦^•‡ˆ4`½Ñ¹A¤Ö')l+Ï ãT¥6@@t;ˆ‡»Ÿì¡ŸìU%¡H8U±_ºY"Èá&X}XA«­"¿UT÷*6Â7a°° EyT`C„õ‹´÷0M¦¥ÌD¤:€'T…©ô%ÈÐÙkXâÿr+GÙC­y –Ü:9¶^äû?ýq©YjÙ¸˜X$Zãá怰aY¨q¡’yQhíTÔýªWr¦R¸ŒËe˜}DiW´~Il£J¿À„骦×7áü¹X[©-P½G»3.\ÃÏ*0¯ÐtÚç\0Û. {ˆRbꌽ¨HݲÉXO FŠ°2@˜¶ši"$´)ë°v¿ÈÕªR ‹4z”=&Ìx×|¹}ÅÕJJ[iž*CÊ Á sæàéàä‡pñ‘[ ÜL Œ õAŽÝEÞ];ÃWFçI\ÌÅ•Qi€ñt`Q·KR"®næ(á0R`½,W/ˆ½²4"Œ•ÒzJ —¿>eüƒ½Š¾]”wŠjUl#ñ®ðvžvKlp¦ω±•¦Óˆ–BÔý‚°ã +”_á›ÉŠ±w‹JÑF>ÙÇv‹b–ò }”ÀíýŠ™¨˜É*p"L°•êBØ$ þZZ˜>THЈ!ˆ‡nWØZ‰¤¨ý’²—>ÛÇóDÓ ЗÁ}BêèáJö¦Ò;Ey1öÊJ +5Ëb—rŽ•Æ¹¬ÒxŒ{»yn;Ç®÷Yw¢Õ¼Á5ëÏA~ðö˜µâúozãL©_*õ+>8jçµ€FImox•ÀuPãU©ë^6VŸ·¿jIS0›õŹužb¢¼Ð¬(=¥ynôž‚Y£%t‰±ÇquGÂèCåŽÒ85:›Ë/@§™VY–@EÈ-Bëášó–¹ ì KCýe¡¡6Ïšï¥Ö™P?´‡×¾¶‹he¡ áNd§"§p Ô©â’Ѩµ•ãI±©×Ëbc«À7!bPˆÐÆyÔÛ†™„½@€~Ž*Rÿ(Ì@áW!¨pûA–Û*HI܆·€»Y<ÈõÝR\;”ÏV–âÖZ¼9µ[—”>Man¸÷¾…pMP­Ü‘¿ç€4sÕ/¼E +ÑJL”()ÄHWÁƒI¬'ãçø¸6„‡i‡Hàœ9¡Æ*4ƒ[U±Å9 ˜Ã<åï„DYÉþn^‚¨Q·› ó„hö0Ãù&n;yÀR Þ¨HG ê:d´áNY~&$°GI*ƒY@Ç´1ÚÝ* +0iyÊz˜ªþéÃl)®Õ„\ÀeÀÏmMi{Z–@ñðú 6~*ÖÖÀzœ·ªêƒ’ZæÝ®B·íø\Aó¼ÌÕ!y)Fr/Ï6ö1¯À5}¨F‡Œ7ÝCD¬aZ—óæ¤5"iIí¤–Ò=8ûöA–þÓíÒV†®0uÔ²Õ-PzÜ™‚Fõ©ÊŸ>Ll'«k`~qÁI1ϸ`µöœ>À)gMEgÎ:“ § ãÌËj{á ¬Jct{¤¶ÎY È%ËE çó\TUújã\kžRæhÕvÊB‘‹B”¢lpàà@Ù£×$¬»Èó¡R?–›gqÑaNõÞswþ];ŽkÆ’ñÙ†‚XÛ' ÊY +k¶~MCä(½ å¥pÐR¬îUA`‹qa4°K XÈæNQ,³5Î]:måÒ«ÝÇ|í²£ vI€ø7XgÊø«é<Ès½€~}0W¨Ô.óuX P¼0ðÏÀÅï•`Ánç0³Â°â°:ITƒ)MV µk%¶‘#BÀ´i:Þ„ÁÍ?Ö£Û*ˆ<`ç!¨fØÿ‘éj)ðJ”;,¹ \œ(Ë`üw |ž0Ó¨º“£h¹ÆÛ#`ê½²¶[RfxZSK"òŸmgK¤ƒ 4Xûª¹ Q’R•P¼”ÄÌOÓønY¤`N\”ò$É­,·_VÿÅNîAÝ+‰`xËL˜(Š’$Å^Q.ÇhS“¼%ï£ûiÿéã'n"B2úÛñ¹>›Pê‰2÷ Yz°Ÿ˜(§+*©ôÀ ÒÈ–9ÙîÇ•]Q@W=Ö{Bm¯,(“êûUø†ŸÆ èüÂXT릫2ëËR=K: ;ˆ¼ê Ò¬þ3Æ]"\Ci&I&µ_ã=l"C!<~ŸÖÁðPþjsõŽ!+6n¥,hÈ 6øŠõ,@ V"é~}å",¶0“N2 ƒJ3Ñ~º{!XÙ±Ð×D'¦uXHƒÀø¹Ä"™ò7ÙÌ‚¤¶æ¶®2ÉÎW&œV/ä8€"l;¹T¤¼®Ãb`ƒÁH@ºgNd¨ JêÂc™ÎžW¿2j€9šÉP¨[ȤÛG“ÝSH°JûKùîÞ¸‹ùê¨iÿ¸ÝNDa$ /l¨ ‚mb' ö[‡tžq3n¼•‹íÅÒjCƒ€ËòÁ㜘dÓ>±Q Ò4ØQ0„_³ß6j=0áâu[ëdr4¨CåaÈè€O46Ò0$tºÑ-Sþ‚o [É[‡Å"— » ¾éÚÄèl¼Ò +ÀÃ…½à1hô¸…Ùà#‚‘¢ŠиAû ,h Œx*°sࣜ¸‚‰)øÇ!(¥5àðΣ&fÔ‚O+õOÚ©QÇîñ}'= ž=uMXLLö©PexÒas³.L28˜“îQ3ã â¸¿ ±ÝsëÛqÑØBƒÃß>ßIÅP1 ”~Ø@€]„Øð,,è¦ +ê¸z'›’7{ýFD‚žÕ¸% ¡Bò%eϸ›s ˜y'„Ìb§â\:x'ZmOºùƒZ°“¯+è½¢•ŽZ蘕T“¹¹Ó—ŸÖyÅ}ã6 êçcM¡ŽY™CzüÖ‘Ò£¾]´ÆäõÜ‚Uƒ¿èÌ)õùã÷é|Á:Ô„† Øš½a3ù›´ ¼Úè®Þ™hl}eÔ¶Ì:¢÷˜`lж’  ‹>Mx|ø¨ ‡Ií°ÎH0¹„TfCu7Ÿ¶P ˜À“û5È(ˆKF餗Nbž`ÃBb¤êyŒÓˆuÒBš\2D6„ω‰.chóQ#6fÂ&Ìô˜‘€œiS¤œ.ÌTã>8î1à0#­[·q£jߤÝM&(¹ +\ý4Ç­+m'²N:, ÏÖ"Z =ÑuÒ5¤ELø@å¶i‡–Ü7é2€þ26,b$Ž†‡õ€.›‹/`Aƒ>>ëO΢¡†•NQÅ8XXfdÆB*ŸÅ‚a-oÆUFms©i“pÐ 7Ÿ±Q vI6Rw +™ÃvaÄÂsBœµÑµQv*æáӰξPÃHÆìlJ‹È‡êDŒŠ´ƒ¥5±´Ç§ÍlzÂt„q=b£ílŒºÉy䦉Ê2ÑYFí¹šp0¯_çâaÝ&í²Æ é/pÌNÐ:o›°Ò:­0ÎX_Ì̉‰f Ý3“ .(]ã8 ±š X +"ÐDÄŠ‹/è $qˆ?cVÂxk ÕÃC2* +ñ9¹´~Ø>p\ÐPð¶Ãç°Ö=¬ó@!¼Ržˆ4ÜBAë è]ü„ÈÁ{h +}­q+:$ìbKLtÑB%‡ ؤ©‚M:`Ø­'ää+|fxŠnô*ãfòÐè2f²sZ+kCCR´=n…ZÈFx[41ˆu"`[ã ÚÈ8t“ í1hQ³Ë¯w¾ y¬£5Çr:hR=bt°”±Ñ;¤Eá6É1Øøˆ¡<üþqÛ°Î;¤g-)¢ø„Ò„•7R$W804k¼ƒOwù!0˜ôŒ‰Ãƒ#«0#Îè »¨,x­C73c€VHë­‡t,A­ Ô¼BNïïôÉ9€ÀÕ,jÀ’:Ï'V‚å=.»l!¢N:Ô¡õˆ€a°‘·<@Hª Ú#&Ò†BØÉiÜ‚‡K¢|Æ+UøÔ"›1S‰!3eç². |}Å–˜ÜŸƒÿÒ{Ã2ie`Ö‘1'†ÇÍd•Â6ž²)-„h+ K +c;4éâFcn¾âæËæ[ˆ`²·28c!"N!m"#Öw¥Í¾8¡Î9hF3 +Æ&¤q± =bUÚV*mìCÚRìàPÌR˜“Êz¤šHÙ‰ô è©Æ‚À½º<ð“°2v:5Ø¢µRÚúëÐ!wÄDk<Š™H;˜‚ÞÓycv:p½[µ+±â¶JëÅA&É`KV_絸ýf?i$´VÎÃL¾˜Éѹäá`H +·)—§•)WÆŒ¸Þ&ŽÉ2þÃþÉ}c6à(Pç}c»C·¶,ÍHè°‘ÒAÖ0‘ûÆÝMÆ,ܘEÐØEAm¢BºÏæ £‚}OxXç„LÚ@‘Ó~unÿ¸ëÀ¸kDÝ6îüçX2]·²P‚Q#18æ€Åav“v .bÌÄh¬¢ƒLLØXÐY2Â&O‡ÚfŸ +@˜”œê™pepà^(5©è‚œÝò…:Pzèt!ÞóÀlÀº´›Iar 4ÝlLþˆ‰B¨´?93æÙT$#ÝHý—Z4‘ H„Úóȵq¯qŠLz Í›™ ËF¥ušN.íD mƒýµ† Ù8#†„ˆE'("ø=#ªj=a d7ÐlF!ã—\lÈÖFCb ÙqU£/¡sûG >™ñqY~Ó88¯¿ì‘z=•¥NkÌÉ»!+‘„lÉtÌ* I+DW;s`ÂÁÌ!ŒÁšiX:ýàxŽ +Z9nö:ÀÊÁ؇OØýŽ€Wj°Ñ*Üs1/—·`¡ƒ:7äÓQk'c6<ìa“D ¡³sfPñ¡IçQÛ¨G˜ÁyµZ@¶:§ fÈ(#ƒ£  !ž4ºC6o àôµÿ¸_3¢ó0¾zØöO‡ÌÃ:ðxƒLpb£fÖè ˆÛÆ=V´Þa#ñ`6j†° úȃ˜¾M ½á!# ö"TyVÃDÔ ÖË—ÉHgÜOfLÆÉd ÌÍø½wÈLBàåâ=—Ö8#Ò#…ÒBx´0`q6GHUŸ¿ ZgƒŸ¬ÁBÄšÁ«ŽÛ%½72éôX„!;jå ŒÒÙ‚¤31Ñ{!ZsŒëQCô3Ó“VÎ28Ã2‚¾svT±¢ +è‡üË2d‚ÕÆð­›Éª6 ‡&‘9ÐhHÄN°|DdO¯r@‹ìƒaćÌÄ„s`¨ ÇYµ’)—PµCXp›ÑèlD Ûè­xȧB¬ç³·é­'ˆÊ%TÌÿOºüÐÎZWPï Ï[<¼‹WL8e–@¤®´B¥-*5;lk* r²öç¶Iç~2áà!ÿBО°K, 8>Aã33àÖ,x”KL§Z;ju⪠(àCl>ÈÑ +B%ÜXÁ£˜=‹`atˆ¢qö…?˜\Â¥"dÿ¿ƒQ£fÊF&L¾ bÔÁIÀ€â0¬(‹©þ°‘0gZ%0~Ãj ܦC‚Z ¢™‰:¬Ç¿2lùê°yDçÓ»6˜“6º%­ñ²©!²Ü2¢G°³“Zca4à´MĤ|—ã+‡ôcF þ¨³ NTõò9’f¡“>>yKá>âàä |ƒ“jt¨!8ð¯Òî?lˆº|A‡O1"a=IDÅýÕƒl¿Î7juÞÁ¡˜[Û= ,X7 *H Ä +0ÿÿxPRkt +z;7¤õ™¨1T™Ø7n‡î³ªÅ†¨ ]¦ÑèSÁTCÈÒ ÒÀÐI=,»g@Y`¼ÁÝ ÈÍH‘NÜ::m†—ã‘?Òï\à+\â°‡¤ÏFšbz–‰õŒXLã ŒCüÔ8(="@-@U1¾,Dû6=êä‡M”œœssÐ×,4ûØàÈ<#~ƒ8㤢Z$±ÑÎæÀº[ˆ8È.É1?áá×Ã`LÐã Ò{‡Á=ÚD„/"B B ÔqÒJÉ„F0㛡¢m"Ô°7_–»mÌ6¤ó˜‘€Â)° +€Ðí?¬óª`‰',„ CxG¸ ,‹Oz¸¢›L "‰ƒƒS1j F Ę™ƒÖ€0®¤z‰Ææm`n°Œê\!:5bâG-‚Έ•i}ÃôФwX‹ŽɃ“ÞȘ‘Ö;D”ÏbÁrgÓ‘ûÇì‡uà‘ÀÃRó£fò 7cz/õý#fpqå«:»HÌ9HåŸå f†§CGôĈž„v³°`® £}åfXã†h0>Ø@G­¾(p;"–ÁªïÓøF¬ƒs«Æne‡Cïä`5 îÅRVØÏF vn߈mpjÀÛ%Îïr+n!çõ—œllƒ‹Øè ⯠rÉ=+ðHêÙxx€Ä“Ø©$<»QV~Ðûð*/¼<é‘4àä}aø_Ÿ¿âdÛ—^.M +„RÂü“usY7›Æä¢BZäb]Ÿƒ˜æ¤S#vî° > Xã VÃÉ$‰`Sj·i1ˆüBà´ÃZTï +¤AÁÍk ހO©¡0`.7j“€ÍxÜÅPy|2ªCºÑ:E#´I<Ђ¹ç\-ˆß'B§ämdÆNg'=A=¦BüÔ!a°ú€Ÿ1ëàhv@—ƒI›aÀîÎ /6øìDÔ4€qÐà‹M¸h:,ØתqÀŒÛ¹aÃ`#{Hïh9ð„Õó@ËPÉ1ƒo N@èl-) ž€$\7f$ô~XëUÕ9ÚÁ  vTñQ*P3cáR\| +pë„Rh̬üà³ØÔŒRZõççéx×)lŽU§ÀØ#"ðOjŠʸR‰”W½’WmlÎío á.®ö¼JËÆåAï°`UÌÌXé—žr«nÕL%=rÅ@òX¨f¢T3©ê±‰ÃÓ0¥LE;\jŽŽ÷«®àPVBç [¨40Õ˜Cœp‰-dlÜ)}LŠÏÃ{z„‚ƒN‚^ëIëâ<\øÜLDa‟çbS´ÚOèⲩ`$ããˆ2À&¨X—P[V&yBHƒÞ6îò±ˆGšt|Ê%œ<oÌÎDMxØBFa ÉHÛ#•av6mÆUp‰$ wŠfl…4á‘è5>±×Éf`åítÚB$&Ý! |®Û/± ¯PdC-q´>h=€œÞ‡´ˆ +y6Ú”rÓ\²MǪɩ-6уD¯GU·XòH5'/©51Ö²siö)UÀÏ°™…YÀt@ÔL^H´ÞÍÓ‘’ ºÙ*çÝBrt4žÆ‚Mø‹‹‹6È­ò݃Õ…Ü n0Éi1;‹Ejx¸„ÈR­†ëk¡æ&mâ¡ +*e{GðpÝF'€ °`FHÅ€ *¾pÕ(‡‹‹ÉÎ6 ÍD&|ª¸’óóRaÉl!þ–W*9¨° ›põW¸d—ML‘Ѷ‰N ¦nzä‚[Êmbj—É,qÙE1·L'úd°’íä2 “hÈ-…Ì’]¦“sT|Ú)d­TÔàS Œp`áSJqQ­o +‹Jq8Y‡©P4P…É‚™„Äa§â—;Íe§¥"Ô±%æHÀR¨ê –¬tÜ-e½%2³áÒZuá|uéœRZd]4TGƒe6Þâ`e](nEÛ§éÄ´Šø$hº0°m‚sÙO¨é6‚µ6>EE@§J0k6³@&úx´‡G¦‚Ù™êâ9*ÑóÈP8P=>=GÚƒÕT'-ËEàF#®hÝœ“3ÑŸlú ³áú†”_ð†j:¥¥ƒ L%]Ü@²lü•ÑF¤âÀF’‰[d5 z~9RßVjëJe°\h dç3ž@‘Šw¨øT¸¶ è‚b‘ѦÆ-Úð–ƒS&€®¹Ä 0m°´œèmmÞ¥¶vðhC.˜gc­Ìü™ì™pcMtät§<´²x*ÕÝuòÔ_ÂÕ–/\#ã6ÞÁE5?o§V2NFšD¤……šr~1Ý;mïˆé~ª½!eºz¨j‚‰u”Òr¸¹níxããSV66fÅlLLO³ÉN¸¶Q\ºÐܽ¬oE*˽­;Ùx×àKRñ9&9Ç& ˆëÙÙÛÅüŠI˜IàL©RáJ ´ÈgçäâJvöL´s°íÏNçZë°åƒ ˆÂv:ƒ[|v9XßKtOÓñžZ^q Ytn! ¡TvÒ½3½½ûWN?Ù]¿ØÛ¾$æút¼i¬‡›B~Vm¬U–.¶vnˆ…y—”uòI¯œáS].ÕONÍL— óÓ{wgfŽ»”¢KÎq‰¶œŽ4·øÒº\Û&3³T²¿Z©Ð¤‹×±Ä§OÇfÎFû§cÓ§¥ÕÊüé@¡ÏÄkÁê*Ÿ]à2óbn1Z_ÏÍŠ46@t:ŸßÆÆ¡‰(µ +.æ棭#ÉîIÄÎ!ËÆjV*ˆH J­)•ÕøÔ^¬³—í¨.œC¤<¼¡¶o™xOÊ.<íc©Þi¥²íõW'œ"p ¨¼œ›Wjk|v–Iv¹tx†ˆµœþ"ƒœb3³¡Ú&¬pvö4®;ȇÙÑ P’ÒóÅ…³é™cÉîÑìü¹hssv÷ +¸[´a¼{¤±u¥wä®þѻګ·Kém¨õuÄ_û7Ø.OöK‹—²ý“±öNué4¾ÑDƒE`Q1;—hï•–n/.Íõ-¾·8wÙÎ%|Á Î_YOÍžŠ´vƒÅåìÌi —°bR ÝÆù ;éógíæîÝÍk‹'ï½òôw¸Ä4“˜/¯^4öÆB-²ó@›0¶ôÌÙdÿ È¥?3‹ã/L%Ú+Ñæz¨º ¬ÒžÝ{ìÅ·a}¬t$˜›n¬Ý úÚ5^ ¶ZœÚºùà×îzþ{ˆœ­­œkïÞS^»ý[߸Öؼ.fç{ðê¯I…Þ˜‹§T舕ÄÔ‘ÚÚŹ÷åçNu–N=üìÉÞ.` ?}4ÑÙIöŽÍ¹g÷ÎgÏ=¯”·[3G»«ç‰p•Ž ´XÌ.¨ÒÒ¥é/ž&?naûbwå*AÕüÅ.ÕãÓÓjueæøƒýÓ†«ëp ±–ן§£ RmŠÙù¢+„Ë›…ù3.¬`ZHCW€¯¸ô´é•úÇ/Ý÷b}é4(ä/§gÎÅÚ{õÍ«Ð)Lnœ¡O.†òËÐéN&ÊϨõÕ©íËý£7*+ðXר–»ÇÏÝóuBÉGJ3åùÓ¥…ótªÏ×6"õM9?,Ìûs3FŸì‚Z$§¢m ŠÎÆ»—Ÿ.ÏŸYܺ¸°{)ÃË-À|}íÒæ…G.<ðb¢¾6¿~öο°/ásÀ]ÇÚgr Wò —ü™þâÖí{go„³MeSLfQ,¬…*›­íûåu;“3ýPsðÏÆÚ`{äò:¬[¬¾væžç_3“aàOD*Å»'ÕÎQ>3^´wNßùþåD— ÕÔöv¨µ«vŽWׯ²™ó æ!;àãm~!Ó=Z\¾”èߨn{ù\}攨™pŽe£õ\ïhiæH±¿ÕX: nÂÍp±Jºs,=u"ÖÜãÒlgsçÜC3Ço¸¸Djj$ Tß(,]Œv‰…%!Õ;rö¾Sw=ŧš£NŽOû‹‹B¶¯Ô–‹‹gãí­éÕ³/맕…SœZío]^=ÿdóÈÍÞ‘{–Ï>ÒܾÉÆzÇÎÜ{òΧAÝÀoÀ‡¦¦ŽÆ[;éî‘òÊÅìÜ&ÑÙ>}Oó<,Ä[[ÉécÁÊšRYËNŸì{°´zÍŸ_ê Î+ ›qp`ðµã µ‘ „¸™xc#VšÎ·×ÝB’K´$H¸É.ï„J+¥ÅÛÁy‚½Ì/]¡ôr\ªƒ„êc¨ª!¸:¥ÖvLDØNEÂ…Y15U^8Q_=jlxCMD,eÛgn¼À'ëñÆrsãŽüÜY©´ÂÄl¼éö‘`%ÞÜ æL„‰–…tW‚®ÉÌÈ™^¼¾Î':ÝÅ“½Õ³d¸Rèi¬_¬­\¨.ßÞX>]îïS<÷æ;̹fÀÂ1‡+µpuSmˆ4Jù%¯=w屧^üNª:çó²3gS3ç²sÊ«wU7îaSðÇRgõb¢µ¥õÈz$ 1Ñ&ß®,Þ~á‘×A»¹^¦½aÆBÞ`\ ªTÁW¤Ú»ËÇoÎí\¥C•Bw3Þ\Ey—Ç•–œ[Œ·Ž$š»&¦$ڡܬ[HAE@C¹ì•˜Áƒ•`zÚFµn +¤"•¹êâ©Ùc÷廞`iÜ%L8Y§B#ˆÉ¶ZÝŒvŽåæN¶6/ÏŸyP*.àÁBfuk }ó§éìÜHõŽ¥:;ùécRvÚBGŒ˜Â§ûn1íSJe`š[[¹½¼x‚MµÜ|8m«µåDw/7w6Õ?E§g!Îv @¦dú¤?ï"#r¢“Ÿ;™î“‹  '@ï0%.Ï ÙiB…àÖ¢b-—4`ŠƒTwNÝ+æf|.&I†kàsPð9þŠSÈćò½ÜÔ–ó§Û›½½{ó wú‹[õ¥Ë™ù³Lº—íì¹öª”4·®ì\ô¡våòNjáz°¼¡EdDL¥Z¡ R¥¥óµµË³»÷ÎlßHwŽ«õ-.\Þ8u³ØßÕûü`Gå O ÙU©°šèž” + X ½vôrªºˆYJ…U]“ +l²/çWc¡ìLgõB Bå-“æ äíLLˆ6³íBwõç\,PÊNF ï ;€!Œ66Ã¥¥hq1œž†µªÎžLöŽ³éȪˆ¿j!ãh˜h·³qKu-¤Š…ê\jº8w¶0w¥Ù3²ã•¥#ññqRtê¸TZÃß=ñ`gër¬¶îs'/?ÖY>iÒ¤œ[ÈŸ10á +48¼!<\¢Ž4wÝãt¢«÷L‚RJJ¦=é¤Ì>?®cj2€Œ6ÅÄ;Z碂rªæ/tý…>؈EljZ­­r3¼Z.4Vú»WBµe!&ZjeÑF+±boõÌËgK÷Os‰®“MÀ ”¡P¼Ê¨3ºXŸ ä¦å ŸëÊ‹)å æªKç¡š˜RŠTV +sçJ‹gÓSÛB¢áó§ô¨æäSV*.¤fà@1©±à(—¢ì)•u)³àâÓ&4ˆ +):”Ǥx´<+d¦™T/PYödûjnzeïÊÔÆ9ÔŸôˆñhe©¾p¶±r)ÞÚ²ótbÊ%¤a=“µU(®Ç[Ç "µ¥ ÇïŸ=r“ŒrÕþú©»ã#lÒI4P W"•€ÉÆüE;6¸Y2˜ƒ ÒÚºV™?ñjñä£Í•ËÑâÜüæÕ…Û‰H ¢d@¶Ñ11>Cø+FD0xŸ”äâu¹PXvÈe'!=MC»Ej6 +ô›ïŠ@pÉPÆ©sqN2Â…kFä°Ö>`‰ìl²µ›í ƒýÒn.•ï«.žw0qDJó©i°yL¸ŒûÓ\†€¬çˆPõ©à Ö…åìÔ±Úâ*T³`A¦{fŸl'èzn\¥˜Y U'®ú¤ ”Ì386at c&Bkgøpe÷ähª9®óúøt¡TÉ`m°óÅ&'í,¬g¢¼BÈ%òb}ó$›,ó} Ülf“2cÅ$”‹p‘‚?Ý +ä;\²Â§«ñú̉«õö.ÄZs^%í‹ä•B»0·éoÎ x~8]™ fZ†I TJ|r2þì±´â’ÒL$kÎI…¶”oøÔ¬…{CI¹P.¯nÆú+n%é‘\rʋ擆uv#µ±+Ξ¢"i„•H%!fh ÆÄrB¦F¨ù`¡Ý\Úî-;S=9;çá²<4é8¬u™1VYÁääídì™ÚÜò D¼ã–ó Ën)*µ©hÆ@&:€E +™Þ¯TÐÚÙÓ…J$Ÿ Å”›Š˜QYï•lT„ƒ¬]\„¤ÂáfÕÕ“WgŽ\”òͦÃd\PnÞ#dë=£¯ÆF9iÕÃ%5vúÖ~*Üæýpy5g"•Õ“×fwÏ%Ú 91᦬LË´ñXÕHÊ><ÑZ‡°Œ‡*i©"R¥8s¾¹rEJw5€ L&Ùpi¢­ÖØhƒÕ¡¯Ó½Íôôf ×RË3ÑêZ¼¾¥ä'ÍäW†Œûǘ” &§Ì.öИyDë1¹d.Üȵ¶“µMÜ_™´ÒcFŸ‹“áÒ~½û€Þ5lB±`=V?¦–6I¹ltËB¼É…³™ZwnãøÊÞ¹c—î¹úð3÷=ûõg^ûÖ[?ü釟üîOýû§_üõ»?ùðÂÍgÊóG)µ6¸DÝÎc\2]˜.T§K­…j£½°±¸sòÈù«×yæÒCÏœ¹ÿ‰Ë7O\ðÂÏÜyó©WÞxûÅo½½°{vyïb¦½D…ó.*$êbª…ÓR4—oÏö–··íž»tû=Üõø3~íÕS÷=yôÚ#—zþÉ—¿ùê÷ÞùÖ;ï¿þƒwþÚ7çöγт“ ™½œ—V©f´Ðá"‰h±Ö\ؘÝ=ÓÝ:^_é¬ïö¶O­žºó§^üÙû}ôëß}ã{?>}ýáæžZèYÉâ ØÑ—‰Ç +³¹Ö¦—‹k4Ì4vK³›©ö’ZŸMv–VN^?{÷ãW~ö¥o~ÿòÃÏž¿ÿ_.>ü¼nš1Åæ š\ʵb•©\³Wî¯õÖNoŸ»çÞ'_zàé¯}ç_¼÷ño_ûþOO\}°·z:ÛX&ƒE°‹V_Àˆˆ¸?C)E4z$„MªÅ…|/ZU«Ó»n<ò¯¯=ðü¿]zàñ«¼pñæçî~ôÚc/¼öö»O¼øÚ÷>ºsé>2\´ ’á11.&ÚJnž4bÅ™@¢Í÷ʽ•ãwtWvÏÞyìŽ{n¿þÀ×ßøîo¾üëoÿð×/þð—>ýü¡g¿‘©Í6b£&ÚÃf•ìR²º£wJZ+éðI¬RÄ境Œ:‰OLàR*Q쮽xòÊ;o^¼÷‰ûŸ~iëôµÆò¹âÌq&T1à· ;!ÖœZZÞüäÃýŸûÏO?ÿòïþèñ^]>~gvúˆJ¸p5’h¹DºØèÌn»pâüõsWo>øÄs¯çG¯ÿàgÏ¿ñ½'^zí[oÿôç}þò·~øö¿ÿì¯ÿñ÷W¾÷ó‡_xsûÂ}`Æ2ÍÕÚü‘LgYN–ÓåæÜÊÚ™‹ï}ð¡§_xñ…W^ÿÎ;?ýð׿ÿþ{½öï?ûݾüã_ÿ¯ÿûÿù€üó/ðó_]ükÕ…#J®„‰2H¢PnN/níÁcýè© ×ï{ìù¯¿ôÆ·žþÆ7ù›/¾ùÝ¿÷á¯>úä‹/¾øßÿç¿?üõO|íµ“w>ªÍóJ‹¡Ì\ªµã¢cnÌTóµöüÆÑ3wÜýàÍ'ž¿ù//ÝýÔ‹¯|çÇ?zïãŸüò£?þå/üëýòÓ/>ýíï¿ñ­wVßæ ÕÉ>ª ³ÁìT­·:¿qlûÔÅë<úÀÓÿúä׿ùõ7ßþñûŸ¼÷ÉoþÑo~õÙçøÓŸÿëý7të~ñÙ‹¯ý0Q_Ö¹ù + Ì©w 6<„JY1VOWû+{§¯>ôĵǞú—o¼ñã_}òîÇ¿þÖÞ}õûï|øëÏ?þíï~þÁÇ¿ÿßþçþçÓß|þÂko¼ãf¼2KQ“Ó;I€XRh.uvwNÜqùæã7î•·~ðÓ_}òƒŸ½ÿæ;?ûõïÿô—ÿü¯óùGŸ~úÿùŸ?ûðÓ»y¦6·›no‹É®yp6rÒDPb‚‘“¡d£Ð^©÷×3«§.^{øÉ}åÍw?øä“Ï¿üæ¿ÿôýú¿þ÷ù—¿}íïÞ|ü™‹w=HMQJƒdBS½å•c'N]¸tåÊõ{n<üØ#o}ç­ßýîw_þéO}úÙ{¿|ïå×_¹xõÚâæn²ÚUò}7·û¯2BX ÇÊ•ÆöÞÉ«w?|ÏÃOÞûèSÏþëË?úùû?üÙû¯çßÿáO¾üÓÀt¾þú·_~㭳ךÙýì׿ùëßþã³/¾xëG?¾þðÃ3›ÇCù)N-±áœŒkS3ËÛGÏ^Þ;}~ïøé«7îyíÍ·~ùÁ‡|úÛwÞÿø­þøý>øÍçŸòÙ§Ÿ}öñ‡ôÊ›ß=íÁbo ec(÷'Ú‘Ò&f(1‰*µÎæÎÎC?ö&ŒãG?é›oýô½_}ù§¿üåoÿåÇ~ùå¿øàƒ—_}õÅW_ß d%ÄJ®ε;sëýÅõ½Sç{ú¹¯½üꋯ½ùoßüöOßûå_ÿþ¿~óåŸßýàãþø‡øòËO?ÿâ•·Þ~ðñç*½m¥êœÁÎZÙI„"ŠíÕ\­¿yääƒO=÷Ü+¯¿ðÆw~òþ‡_þù/_þå?~òþ~úé—üãç¿ÿâÝ_¾ÿñ'½ûÞ/~ú™sWï—§%mBH½ƒb•`²M7֎ܸÿ‰7Þú÷Ÿ¼÷ÁÛ?ùÙgŸþåŸþüño~÷ËO~óç¿üÞçŸþø'ïþìÝ÷ßäéç\ºwîøuF­š«7hónL4ÙqhöWŸ»|ãÁÇ_zã;/¿ùÖ7¾ù­ŸüüûûßýÅ~òÞ¯~ñÞ/?øè£^yõì×–7O*™n®³ ejÍ~¹1•+V¦æ!FmL/ͯo¯]¾~ñÞ‡ï¹óú¥³oÏU RØïc8‡uá’ÞFèm¸ÑAøYd +ùz³5Ý™îoìYßÛ[ž;qöÈ›wÜ÷À W¯]¸ëÞ£¯´W‰<)ss\´nC8‡—CHÑGòœÜ;~çÊîéT._/v¶×Î]8{ã¾»zâÁïÿ{ï}ðë÷?úø“Ï>~îå—N^ºÐY\ôÇ3R¼ìƒèJø]T€õÇw]:~ážRw±Ðìöû3k«K§O{þù'ßþáÛ~üég¿ýÍÛ?zû›o¾þê+/?÷Ì£Þ×ÎÞ±Zw)Šæå½\ÔF(ã6È5¸ÎAYÝ´(†rÙôöÆæý7îzýÕW_ë­×_ÿúÏþÎÿøûÿí_ï¸xzog½1ÕÍÖÛJÐ;0LˆÂÃM‡\tÈÇ…VŠ$r•Foeeóâíg}ü‘ç_xöÛßzý—¼ÿùŸÿéÏ|ï?|öÙ'/\>WjTÝ8ï&Ã`†õöà˜eT‡ŒP/¡¨±j4Vœž[ZX[?ræöÛï¸|çÕë×ïºñ—_üþÛ?xõõ×¾þÒ‹/<ÿ/¯½únÞsüèñt¡‰òa³³aÂD9•,Ïb%/.D¹íãWn<øÜ _üɹríž'Ÿ|æG?þÙ÷~ð½n^yú¡ûžzô‘³çÏmlm¶¦f“å~uþd Ý»u:4 ºlC%“p{¹t®¶µuäêµëßxý ðJO=ûì£?úÒ‹/¾ó“÷žÿÚ¿]½z÷êÆN¡Ö"yâŒÅ0؃ÕkG—ó’¢+ÇK½zoezy×'l8å$y‹7!,ÔÑË©FspÄ 3¡n,à¥ÃNŸä£œT0|l ’*cy'‚Û½>J¼Z-™eÂN˜Ü,ÂÆ]LDã"†ôVƒC¿aݸĩyðÞ¤’!†±,“ʤò•Rwvnëä¹ùÍH&ë¡EŸ°ût.ÊŒú͈¬s²7Ë*Y²FHq„ R’Ž&ƒ¡HX¦²éD.Ÿ­µÚ kJ,O¦Z©r©‚ï$£r&›ñùs_³=¢¬Öù@Q ¥kåF!WHDÕXT]]ßj·«ÅbºÝ›Šek0HRŽcRÎMDtVBcF'žq½sLïÑ:x;ï™C•Bu!™©å³åËWï:zìx©™jµ*ÕÚÌto¶×Êæk/94®‡ 1cCcÖá ;üÃà\‰³q)\ +Å +‰L)ž«²rÄéñqb(–ªÃÙl¡Ñé.u{‹‚$%R¹€’ô‘~£“=l!·bð…¸H]ŒÖp ”Ep¢žL§²UI +’ÉЂ¬äýjY‰×Cé)óàâ…€’f£­}ÖcÆQ½(Ô +>1C+¥lmaíÔ½&<ñ–ðç)¥Fá’^>eÅ‚“ÜM†lxðà„ùÀ¸å°Á3aÂÜxÒþx› Uh)‹ +©Á=Iœ”ÆÛiÕA%l¸JJy¥Ž[¼möIn<€«—5ºY¯œÅ#e6VÔ²ÕKÙ¼8(7)ù„):èÎÃ!|zÔŒ}uÌ4¤s×uáÁ5lc§w16Ÿ¬µá&7ip‘^:"„+“q)K n*$…3t0vHc™pØÑ .ffD:9¦÷þóA͈Î㥣À]zgñ­xÆÈfûN2R±#¼ÝÍvú{8=4f…·rKe¯Ò" WF[!e‚"¤±3 ²BbZÊ.ªõ½Ds·8AÊÌOXp”3¥¾ ì6L îh”cS­Åó¥þ¯špMèܙ✛Š˜tMX)ƒ VÕV¼²¦æM6flÂŽàA"él´ 8pÕ38Õmp“I9={XëÖ;ì#Fç˜Ñå "^1K*¥pyÑ'dc¹Ùdu‰ +•ˆ@ÉEÇÝl +• ƒ[†2 ¾mÄp`Â:€ñÑqÊ_ ‚%”OåjëÙö‘!ç°Îiô°>OöÙØ¡”¸Âˆñr{ F žQ£{ÂâµãHeCÎ/r‰,f4×”ãe *ät.Þ‚ETŠV»‰ÆI5¡V„e»<¨s2V Pg§T&\eÃÍ[—TP@en:ÀƒúŠ™?ÊgüÉ)ÔŸ>lp[QEˆuÅDÇ'å­d•+z$8f!]TXï¤n1µh à"½Cé¨OH»È°‘Lÿ¾1׈· Šˆ[±üô‰yÓÙY†Ô?¸“ž™žÜX8ÈN«å9'1 ãzcbZ#vhÄrhÒ=l &,ŒÝ¥œxlÌäƒÇ¤…šp¬DØèS<|FÎ,øó«_rªã•sfoÀ`¡G&\£z¯—ŒÛ8ŸÓ‚®&p¥nÂT£W1"¯\Rsl¼g#T„BHÉ÷O‡ËˆØÀˆƒÅæìY¡µ"B“ +L èWëð— ¾Øl²âýÅÓ^.5f¥Q!ëd“>¹($ú±Ú.jiÌ´¼£Z—•11KN7í +ÉYJ©šDì„B+iŸ’>löjœü­»€6¸ä”’Ÿ/ÍžŒÖ–ñ@–ŠÔ¥"¦¦…Ô ¬Z¨„‹Ž˜È1-6¸Æ_ƒúØ4Â¥TŒ U¹XgHï3¡6<àÓ|fÆ«T´^ÙFbÙæÜæíT´9 …ÝŒêf·˜T=‚ÝËn¹0µrÂŒòÜð&üL.ÓáíÁ¯´”&¼¡IǤ•p’ƒ¸2¸ b¯qPªGHûE"T±QªÖÅiìÔ(H§Ç¥á/ˆé^~é¢Ó_±ŸâÀÃN:aÆF_“ò¸˜™3ã_ýIz¯çFÒ3ÝóŸØ1’Ê=ïôiÌHxïAô®hŠEYÞ»®6Õ]jïäÍh¦5F#4£•tæh&vÎîl„v/ιÙ݈½Ý½ˆ6 d~ù¾Ïó{Ò|ߘëìL`ÂÁ8P «ŒÖæ=: +­Ñ@dØÇ)ùXÃŽ*>&AëêM‹§yãæƒ×Œ’ÿÆ%ÛåÙ UÀŽ&lü²9hör¤œ‡® †°1Zi9ÖܦÓ}8”¸Zõ '»>:qy¶3., åh¥Íj}¢O:¹€òƥ┛v§ý£‡Î áËRfQ/¯N:ˆ*£¹XÑè”=›Œët´©ä‡°l8ËGÓ“ÿ” 7êRqÃÂ/ŸšEd¿Jó|f%œÛ2±¢ø¤ƒÔ=tœŒTœHÔâ‘Lžè°ž_`Õ2Ôö”“ABX k…ÝLg_oìxØ„%ÀA¨žŠ7a”ôÞq0Ú$bMÀËq1í$¸Â'{d´ɯD‹k^Æ8;å¹8˜v³—ͨÉ#€‘JPjx¤îãó˜Ö ="Öò„âðP—eãmNoD’íöüž‡ŒŒÍúœA &äA“/[Ð Ó:R¢¤ÜŒ“˜²!á¥$d0¥Ž…ËR²‡ŠùÑLVxÂËdÝdÂI–€fò†ý,XF#QZß w㊠6oÁ G s}LrÚAñÑš5(ÿÙÙé “‹G@ù<ä;&V%÷Òq5Óõâ¹IË”µ£²{䤒‡MAV +çW³ƒbv^.I£Ã¦çBÙùHyIÎ]´[Û_|Y_¿qÁä³÷è¾AŽ£ìóFÊÍX=t ”2äÑ +ˆˆ…sáâPolGk›A!ðžŸ?@Õ-ÓF[Ì-Ò਽v{õømTN9ANv¢…Uj4cstÆ2äÜ‚R]7!áK\ɯ†w‹Ëw"•M$\q†2VR§Ôj²¾É$ZÁpHðµÍ¥»„Vá³]¥4W˜ÛQÊC­”ú{í'™ÅÛx¬Kê=RïÎÂþþg_ÄËà 'MF»>¾àj±âv¢q¬•`³3@)\¢eö°à8ÓnàÜ•öúƒÝ»§{×`ó·n·—O&$ÉííÔüqzá´¾ñdîà}£}Â…ð :Vñ +fa¿ HeS@Šžñ°{—€g0ÍŽÅl£):S>O$úJëªXÙq 7­q‰:­¡r“‹T´Ê%»‰æŽêy´Åî¦!N¢|'$5L³á`IÔ¶ ÕÇøC py)·)®¡J WCÉ…Lÿ4^߇${~ÚïaÒPÏ`´Z÷±€ÊiL®°±H(Ô’m4ëWÜ- ä öÔÜŒ›ý³³SSd|6ÙvÖÅX2.cb+H(=ëgœ$@ °.:Úòº^ߊշÚK7n=ÿŽ”îa‘¢Ñ¹ª”7Y£_™?­o&ÚûÓHxÌŠ¢B$”UjУr²SœV4êk~. û Nħ{r~¨5¶ù좠×Û ‡§/¿oÁÂt´«ï¤»ñÆvªw5Ù=W¤ŠV™Þ$„>'L© ­¼ÕÛY\¾=š×Mø¤Ð¦|ü˜“t3 *Þ‹5ûW^*¹ùpno¬{ùøe;âeu);×½rÿôÍïì=ü¸²z‹ˆV™ÆÚáÓPªsÉÅõiÕµXuƒÔçôÚv}é0¤—­ˆ’h]“skˆXfcÈ–ÑluR¦¹•ïíO¹è3îY¿<2Ú„V­‘lù:ü®•”ål¯²z£¹}·¸t”ž»)¯ãÑ +.&?üÞ/¼ëfÔq;.çדó·ýÓ1?åâ€kÝíåýçß÷ÙÑ(m ¢Ýk¹õÅÇrýŠ•IŠñúë/ÿ~eÿáªH¹afþæòÉûÅÅ»–`äع”„¸`AÇ]Œ)¨àêèv)Hgl^auDHCh² ɹãüòÎÕwúGß‚ú9;¨ŽÄäÀäFS\Bü oš|‚—ÖÙÄ Ó;Z;y÷àéwJ«w¡tgƒ¼R¹;®™üây31éæ<´Nä¢ÖÑÉ’‹Š9+¦Q±®VÝZ9yëîëŸí=ûA¤~eÒ'JÙJى،_ð2© ›g´­µ ŠÎN{ák¥ü*×faA£¥ŽBgt¼! )¥±|T]8ˆ––äâë#u>³XZyäå§=’˜QœùÿgW@µ1;sÁ„¸-ÝØr¢ò·ÎO}ãÜ´F@©sÙe"ÖqÑÆE kÁâ~¾8fÆÏ^´ÎzD‹?b D Fy™¤eôÀ¾onc¬‘,-úØ8˜ì‚Zß +›ljQmìaJÙá"°k@.Aï@MÂ;f/oö +ö@b‹Í/Û|"pé·&=g'½Xe£;šq}Ú;nç ^1¦VX£Í'›j¶]ìíC'ùÖúÖöƒìüU>ÝF¤”‡3œÔhv2Aoè˜ÕÃBrËzãzvîvcý¹g4ù3ÈKVJõÀ4g|œ‘\Lš5Bf×%*óû¥Þnù‰pª¾:·÷lýöû[w^Ÿ¾ù£Üà˜Õ'§o=xë 2ZrÑ1—Á¢m­²SZ¸ÝÙxâ ¾5å?Âå„ì"Ÿ‚\™„ÓÛÅ tCqÉžo…ÄFF)%ëõ‡H¤bCÔLs¿8w+QÙê¬ÝîŽî®Îú)V¹÷ê|fþ’…²ãqr”Ñp úPßOÇ $f¼, £ZZ‹Õ6«Ë';w_öžbѦ”ìïÝx3À&l¨"$çbå5X½ºCj½1;=í¢0Þp`ÊæMˆ‹êõÜâ1° .z)-šïÏúB3Þ”ÖÊÍæ_׶Ÿç–n•3“?ilßü Î3ù'Üœ‡£Š4š×ŒÖñSðü¸Íì&ƒ£ó-þ)/?í¡ÂrMȬÂ/ÎØQ7.OØFÓM¸™dP.SñvwûáöÃOm°GrÎMD¡PÙx“Kõ¸Ì‚TÚ+WüR!À™Ú:&¥\x˜‚ )³Ñ:P¤G@¾dòž·]˜rtY èúÑê>ÑTÀB*fñДRâ’µ²¬ÕÖÒs×ÄÜ"®TT¤L3À«Aâ­N+ø˜è(™%'®Ûü<Î%¼tl¬‹r +Ð n"NJ%LÊ_6#ÐìC@<Á‚'!K@!•Z¼²&-¥iUJVøtÊBaù¨¾~;ÝÚNWׇW$+1É-$Ú$`si]oîs™U7¡ÉEëT¤8åbf!ºØ<`QœÏC¸Ã¤Š”/´³3ŽoŒ[¨,§ja#Õ>êî¿Ém!V]Ú{"–@f}¼ãë‡;0¡ dæ3ݽKvzÜBÒá:¥Ö§=¡33Þ³3^“'•²”ž÷к”LáS¿`Ò#).®Cر!2lip “ +ö€<ãd!þŸŸöÎ8)”7f=ÌèvHèbQÊ,0‰ŸžÃµ:k”毑 çbºÔßÍ®Â;^©h%cV\¡£ÍpnU«íÍx!S\¢ÉoÈ9€mFïš|ê™©àŒ“©´7»ÃƒÑ4qVÌŠ©”ÞŽ6wÓs' êçg‚áDË(ÍA%\Ä‹ ÉN$7`´2ä ³stvÅŽj”R&µ +*eù…Xs_«ï‡÷ôÆž™³‘aHߥ¹#àXкÑ$„Íì„BMÚ ¨X`ûo~½.ÃØh²z å|d¥h¢Ñ_;õ‹)xÅå“Í{Ÿn>ø¼¸þ ”_†’£……•Ã¹µ;!½iADs@ø:'|dTM¶|¤bñ’B´À©E,~ G&7ëW/ÙH0&“‡v’g¼£©¶½¼ot†¶C)£ËgNJ%ÃÙâÜv{íp°{oáÚóáÕ§k×ßlmÜÕ;û^!•«¯ŒNœÊi*ÐZ%ïá‘V(±àc²Ìn ÙíBÏhé´Œ—JÚ¢ ›\ %hˆlB2¤Ý” +¿òT¼%&Û…Îöî·Y½;òåÑŠ3 -¿"Äû¸Tšõ +jªklÌ8é³î7ïÀu&Ú‚Ž " ÙhVû3.zÊ1º°«l•†7««·†WߌV¶ ÷+ýÅ݇3nΉD<¸ +£4ë¢]¨Dˆiè¦LkR +Pivh7$ÌÄ[jc'¿ö`~ÿ--¿°´yã¯õo‘Ì”'ŒEÚRn#ÕÚ½úð³ÝŸ3Æü¹iÄOÅ¥ +½ÖðµD@si!}^L ¹Xà|ÊAÂOÍx¹ISnaÂ%Y|Š˜ìSZÉäe DÏ@=Ø0( ‹Ÿó„ à9¡A±DÇ;t¬É/$[Û‘Ê’Z]3z‡RyƒIõéx½Ü?î¿Å'»Ó^ÁP]d’‹Xˆ†h ˆ`újÂÉÌx8¢9á¡)ÉZe~GNwòˆ{âÍõDk+RÛÂã}ŸS•í»ƒíç^Ösà–‡ðYZk`rÕF÷Z³J!UY +0±ÑIu<Š…›„îYžñ‰°ƒ3nfÚIAƒóNØÉÑLøD ‘³T¬ÂÆë…ÎÖë/~þ¿ý/ÍõT´¬–—•òjyñ$ÛÙm®Í­œzIÔ+|¼Š5y£e“G¾0œ›]¡ƒÒ2{¸ +u~ÊwnÂ5aA¦mëã’Ñ­Íõ7ïNz®ºÙ¤ƒ‰Ó‰f¼¾“hìñF‚U}°—mnÚшÅÏûA½™$¼ ˜'K ™ÊÒåißø,N²nB›´Q ón\!¥¼œêÚ’—Œ¡´ªRr£{ê*¤Ú ä|mn¯¹x x€Ž£|ÒOë6ïÁ€¾<¢f»›ñxñˆŸMPZ5”ê Ùyø?¥ùÖáÍ—€ë„Ú6ÇÉæ‘œœÓó‹Jv> .™‰P¤˜©,âž›r›<’›Ê1±ÅÞÖÛ7½sÙB0JQŽWm¾Ð_\²|cÚ;æä-Á—"¹EŸt1š%(œ³Ãë²Å"E­¾¡ÖÛjíJ¤´E*úèÉèž•‰p™Npwxcéðí½ûŸ=ýþêõ÷ðHž ç1ç&ãÐGãvâ”g|6`õ‹“N³€Ä“îД+d÷óàJ^:BëùÒàÊ•[ï46NìŒBƒ9Öw2½“hm{tŸmf~}4¡”óÃ* +–®£æ:×Jƒ617åâ}D—ˤÒ@DPÚè”_׌O¼„añ´øBvÔŽ‘Lw°sw¸ÿ(ÓÝlÝ\»þŒKÖp9ÉÏ‹éÞüνÊâuo(éÄ%>–[ܺ•(ÍCr㪋9ñ¸—JábÞƒ©® }øþé‹/¥ü‚_È"b1(äéh+’_ËÌÝ)¯?“Š+¤œ+ö®&;{bfb‹{4Û­bÇbbÁÏf¾9áúָ낕(-Þ‘*æÑŠ¨!õÍ `¸pY/Îo=yùá¶o½km}}´:Ü>·÷„MTÝŒ*eæk‹ÇÙÖV8Õ1Ê6’žuáA:Šy;"O»YÀÅYdC47¡Ø4÷sãÎI3bq3¬Z®ø¼’l.l݈æ{ˆ”ˆ”Z;®½ ‘05“IÎYáTa¸w絇‹;0«ŸCôP /™09B^Tã#¥ÞðÚðÊ£³ãÎ3—,PÁPZÌéX %˜Z¶¾¶xå!äe7ˆ˜” +iTHûIMJ4P1=iC‰ÐˆQg=ô¬›±û¥)+"æDd7¾4ew¡!¥,ÞTZº¾4·Qå¶ZÝÌõ®‚R ÛFë—Í`ÎЬ‹sq*ÖÆÄìåÙ ´€ Ûý‚kt2 5³Ÿ²¢c¦Šbê›f¿q~ú/ÏMŸ³Ž[Qæ•"lR¡µÍög8§ämXWJŒÑ• Ëjm»8¼S˜»º°¸û׿ø×µ£g! éµ5¥´ )UÈ,Ã*“|raãž‹Ž_œ²›ff­nƒHˆ±*£äÀ÷}¬A*U17ÈÏ]©¯e›«·¿{ýéb¦ƒFJ±ê•hy+ß?™Û{»¹þ¨¾|[HÍéÙÁæµ't,kÁy“ðŒæ+óÆ€Ñûv<6ãÁI}žô…Ï™ñ19K)¬Ë¹&RÚ:xR[ØgãU9?šÎ%7³µùtíøÝýgßÏÍ{;;7_Ê…1Õåã-\.øGK±0±8½6éÄmhØEê¡èœX„€ï¥&sD¥¤Ò¤<7ãÈôÐY0e;¸!ø»¸šG¥4Œ•‹I±‰noóAsx=W_Ë6–B‚R2‘dË:Z%bC¢fŸìj“,66«ýƒsG÷aóˆOR˜Ö@ÕšŸË4ÛÃ#ÐIYrª!MV­áÅMDÊ#l2Wæk3NlÚŽ9‚rÖÁ(AiÚŠ›¸bÔâ¹®ÕC“|¢6Ü_;y±zúJ.mÃ57“^ܺû£Ÿÿ6•Ÿ7¹X ;\ȱj‹ö(­m-êAqZ¥¶|CÉtg”Ó´³¸h"û°ˆ¤æ‘PÌ…ª¤X·`Õ5é3¹9¦Ú½¢‹"¡T4;wçŧ³>ÖϸRd’]¹¼¢7wŒúÊÂêáËZ™?€8\œ»V^º©–W¥ì®µÍ#„àT½öøíŸRñÆ_\˜87n†8a%Lî;%¦­åÝ›/C‰:ȪUˆh)œïéµ¥Òp?ÝÛ Êiµ´˜ê^eã0¤ÑìWÊ‚Ñf´†’žÏw¶Y=«Ûs¬hÔIFÝŒ>§<<&•X½ëb’>>;áÏÌfü’ƒ6¸ä.ÓáÂÊ΃Úü>o4"¥åÌÜqeåÎ`÷ÙöWÍÛJy©»zýßü†KÏ9è8¡ÕèhÀRL/}½*÷<¤¼BïêhÍ”¯§¶}=#¨ÙFù"©;èI'þ56ºúZƒÈãÅ€óµêrkãöÊñ‹•ã·ê;=|$·¹~—Œ·r‘‹~!‡É%R;¨5‡ûV\þæ“ÙA¹Ð¨Õ'›½!«?ä„o/µW®=@"+Uª|¦¯U–óË'±þ!«³jaÿäÅpïW½T—+J~¥½rk°y?Z\a /×ÞûøÇÅùë~Þ:éÀ­¨ì¤ 5·Xì‹éE¦,ï?•2ý33¾q'å žP’d"­4Ì5W—¶ov7o9h•ÖË‘Âœœi3ñJ¶»½yúÖî£ÏQ­¯o}$\qQq¸•°“TZ¢ºaÔ6l˜|ÞäÇà8¦æ¿^h5mGc—mìåÑ:æò¹Y·ÉGûÙ¸”ªõ+Éîaª{ÍMF~þÅëoß~ -¤ZJeI. ÅÌtÛMjTæ´zypJ„«ã_s»ÕËCL87å÷3 êÖüÁ­§ᣓcÑÑ£=W(Ç&ú‰æÞüáÛr¶×[9>yüqyþÐTìÐqDb´F“D¹Ñõ>gã³…YHpþ’…4ùÃ:)eW²íƒX¾ßZ¼ºpø|óîûw¿½|ýM1;°â*ÎgÚWÊ‹×>Ééµ€/ö3‰grr`q±>4œ)¯HzóçÍc3~‡_°y»‡³U“WÆ¥âÒþSpÿÿéœmÌB'‡}9;tQÉoy-ÞE).ZA¤$›lºwöžWîQ©*ÞTÊ+éþhJd཯ïZUw ñÝ\bÂ3š¿×êœiÆA-_å™tnJs113™ +!£Îeš~9­.7·&»ûéö• endstream endobj 27 0 obj <>stream +âüQ®·ÇÅ+ÀÏžÑ,J©¸4àÄèDºZ¨v7¯Ý}GLvÎŽ;lþ`¢upwÑ)?ŸÒËKh8waÖnAUc@ä‡êíÏVæVwOkÃ]7©í+½½7VO^/¾ÙZ¿Y[9‰b™î{ßù»[ïýÔŒG½Œ‘Ÿ;îï¼È´O2íëbzÁ†+FcÙ£–¦=ü9y~–œrË6ÌŠåXiùèáë›/>õ1Ñio!T7—¦õf¾·Wœ;É nùÂe£ëuÐj/Ÿ=nÏg„æ$cåá­Ñr~ž‡IÅb€M¹0 ,Bz&* .2f „½‘êî—o,î?élÜ]¸T³7ï¿óý¯~SîmM{D"\##5Jmz™”%à¤#E„3Ƹ9(ÒZ ÆŠˆ6R£iyÞNÖ7–7N>üâgóÛ·û{篿*n<*¬Þï¼µtõÙÊÕ§éΕlsýg¿üã“÷~è#5!Zãb'¢<øxOŠ5A7ÎM!l´fC¥ ÓÞóÎoœ3_˜ðAt1ÙPržÕ[ÉÚ:§×qµÂ$ûT¼Ë$|nÙ‚Ht¬¬W—¡#BF#ÙÝ­¬ßíî=í쿨ï<‹÷¹ì‚žZ;z‰©%\­%Gw•ì2©5*¶8ZÞÝN]4fGW +˜qnò†DÔÃfžÑk«ÑÚR¶³¶|ô|ùøÅ•»ïÎí>Lõöq½ååÒÄ×ç`=¤jñî7£´fqp½¶|'VÙÀ”ò¬_pƒïàÊ´‡6CÞ”KT¬'f7b•6Þœòñ&ä“jf¼ d@>Ù‰WW2­uø§˜jcB"ÓÙ.-\+ÌíGË«‰æV¢±É%Z±L{ûøñ`熛ŽB|K¶¶FSÙ¤úD´5ZiÔAø°ð•ãgéÚªÉ+‚…¹È”‡Éà +Û¨,]øº±x±“ RiUk^©®Ü=|ôù÷þ¦»÷V@,VZkWNž“á,ÔX²~¥²ò¸¹ù2Ó¿+e€€ëyØôÈXk[™Á‰?”®t¶ZÃCødÏÑ:¥·ŒÚ:Gnp¬“å!«Ù™K³Ø(•Kå1'§í¨2ãfBz=Óy:&æP)‡«7­Šñz¦¹Š–ê«77ï~[8Ž××éXÕÇÄ1!qfûæ;/>þª¿y7ÈçÄä"!WlAÙîcFw8˜‹SÁq Î,N»é?;gº8˜²‘7ïÂãx¤î!b¢Þ„oƒ$˜]8ž¾ÞzðEuãaOç{ûÇ>뮞=þ¨³s/Ö\­mÞÍ®ÜQš{‘ÂêpçÉáýÛw µÉ¹¥tw?ÑØ eV<¡Ü„8;á¼0ã² ?³! +±.Ih:VS*ËÑÊÒúµ§‡O>R« ÉîFyåt4=Qs77w«¾úÄhí¹è¨‡]V†râS}1·Ì¦–„Ü:©÷‚|ÞIh3>6(d‚á² –P +p0h3Äg. ‘ÜCBŠ§q9­‡•¥[­Íû@qPÿŒV¬ aƒb"Z[ïî<Ù½÷ñòÉ»ÑƦiûy”¹Í;\¼äå42VÇ”* !ç°P2S]RÒ®Úa[`´8‹“ˆ; ]+ 3­Mð‘sî`( ‡7¸x-ÓÙêî<ЛÛ^6±¾yúôÕ—r¢ %Ê · +ó·RÍ}9³bò ÂOh'w^•»»—G‹Ìª¡À§æõú•üÂÍtïˆR·¾{íîáx’HkûéÕ'ßÙzð¥Rßu’ñ­}ô¿YÙ½k +ÈM¨9ÒËjy×.™Q®R½q;åÄT/‡€FÇJq5œ_b”üÉã÷ï½õ]%3ç&£ÌèTü0ÕÜî?>zúEmå¡40©Èé]K@d´ ±.nßöÒ‘Y_(^ÛÓód¸$%»2nõ (oDR] ¥oy.™;¦”|ºµ+$û,jvsjºë¥”Yn 0äOž~rúäã¹FeEH4쟌€OV ³â³3ÙÖv¶±+,̸és“î7ë!c€mÁP-((Íׇb¼pÙìã­î•‡K§ï47nG«ˆ\ÓÏ^Þ[½n +‚À²¸\r‘‰YdOÙHA&–o­ £le³ød”¢î„Œ~(9À•š–n=xóã[O?¦äBË6×oÍ­haÒ—IϸX;ÂW掠k.›Q?äµEÈE4dxHýì„×⢚ýíoñ“fôü„gÆò±IÞ˜Óò«±ì€ä£íå}V+8FÓsµ©äb€çº[¥áqeé¸8 ×–¥T«Ð»²píÙüþÝÊpÄS+/³É>"”T»±xÈÇ£[Y"#žÇ9H%´ƒp¡üMÉY“›‚´‚ÉpnÐÙ~¸}ÿ³Åk/ë«'‹ë‡ÿðËßß|þ?šüj%=8‰Õ¯¤º‡ƒý{>é]y\Üyû{¡ì‚•Š‡ËëéÁõÜâ½åŸ,ßü¨yåI¥ð½¯þðî÷~é ¦€2훈[ȇÒÃXu«ØÙ¹ñð¥kOÊ«7[÷Ê«§zk£¾~£»ywçö»×ßøÜ°ÐÛ=~öÅW?\¿ñªºzG-¯ÚèÞNFï[1mÆËzè„‹6@™hKLõ¥LÆZ6L§Õº‹ŒL{0`3&þõÁŠ5¡´ Cøj­œòɆ^ìÛ«F}]Ì.cJ•*¸÷ÊØ׫HCI¸qþ‹_-àæP>ë#uxåSàJ˜”úŒ.PÙ¸“°!b sb¬ szQÉõëË'½í‚Ñ + +°‘£g:¾¾ã·D^)/eæ¼”n @Lã­ˆÅ+̺'"ÚÉh¶·×Û¹_Y:QJ«©`òòR´ªeúÖ •R0”Á…’”èàbÖ†ÊÁѣ䴃q!Ê…IÿùËîscÎi;í'µ ¥’r˜ÿÌE»ÕÃN;Èo;ÏÏøÇôŒ'dvÑt$ éA.­®ÅjÛŒ¤'‡º[•áQºÎ/†¢e-Ó1+”j²*6V‰×·“«õ¬ˆ 5ÌÇZ¡hmÌ‚MÙØHÁ—²úx¨±±Y$b°ƒ —`õªZYÍ÷ºë7—¶OŽ¾Ýß<)Í_]»ñÞüÕ·wî|´rí\g[NÖ•L·ÐZÛ»õRÊ ´òjsãÞÂþ(€d{;Rp©z²4¸ýôÃkßw1‰p~…O/h•­å“÷‡'ï§[۽Žõë“„ ƒT{ p»±qzüüÓ÷òÏ_þý}ç¿<¸õÎßÿ»§üD­­«Õu©°j´®v¶žíÜûrpõ=\m°‘B¿¸dE )/Ÿ +kÕÕGJi%ȵÁ6«–£..†Ò]€IFo—N6o}@Eó’Q‘ÓmRo•nÖÖ‡K;>®ä/d†¬Ñ·E^ËûY¼Õƾ^œ%:åæp¹˜iïÑjÅN¨h¤ÜÚ|\X¼ÎgæBÉ.­W]äh£8Pác”’££&V’MJ-5P€_{ÓÍuF&ÖtšÅrƒ›D›nLCÏ'*f'"X8/ä†Éöïá"#e9ƒð)1Ñ´ú¥  )Ù¡Ô:¤TÀH'®]˜ +@•YÃGë.¼dlÆIƒî ô‰—„rP—-ظ“µbQ_(9Z1GΆ³ÝLwÇË&™Z¼< Ôœšï˹EHIáÌ|¢²¦WWÈp"¤B²‚ 1='W!çr9šÆò‹3ý}¼T]˜lññ—M¾ó“.>"FG*~Z!µrypu ’ïéËÜÜ6¥e”l«»yG¯,¤j ñò<©.!Ä[R¢­dz¨˜6꫹ÞN(^Å•Š§3øDYIÕSµ¡’ïYQ9ÕÚVJKáÜBkõfe°—ÈÖÛÝáﻇ’Qí¬>ùàæÛŸÝ{ïû·_}yã­O¶î¼œÛ<¼ûìÕÞW•û÷?jn?mï¼XØÑY¿“j_ +©Xº}ÿÝu¶î^´`N +BFOo‡·‰H>É<{õ¥¯ÛPÕÉ‘òJªw¸põíÕ›7vžÆì_òüõJ ‡)Ï&{ˆÚ@µNuxgãö§ùÅ>6~úðÝbwÚ'B¬6ù#6ƒÊÕÒà º˜hµæ¯öwA¡fû{t¼á ¥¨h-”lgæöñhÕ†ðb¢ôö(µÌDr±Âv'7K̯¢áŠ—†LÝqæsvD…È äæ¢å!­C…˜Â8 ô¤Ÿ—ý¡ÔŒ›ƒÄpxÉŒY‰)'3Ám:pnÊ?zŽÃJœŸÅìT\È Â¥©° ¤ûœÑµ"š¤eQ1aòÒ¾P”T‹±Êz¢µ¯7÷˜D3¤åã…Ž˜n:01’™ËNRÝk¹¹ë¤RqÂ^x˜;®è5-Ù°z™Ë&Ï¥Ϥ€,Œ E”ËQb¾Ôß ŠIF-D²ÝtWÈ h½c’íarÒâ#A Hµ¯_‰7Fs^1 09ÝÃÕ2Ȭ›Œr^Ju4 +½]oñóSç G7ûCMŠ$’éjºÔg•´Ë—;Ëõ¥íÒÜr¢:Ru©ÐÓ+ùþzgëÂP{xpõÉÇ+a$%½J†”½“Ì4Ö¦ÛUéA¼°¢dç¹D×ËÆ]AR«›WoØ¡¶"Å\ÿháðÍ•ëoõw«Å%.œzôì½_þþ¿­=CÕªÞÚ7·j{¯®¿üÉÚñ…Þ†’*½õÑÖŽ^˜ƒáq;Ãg–ãk•å»Wî}¯­ë™î£7¿8~þÅ´_šE4f´9¿÷bÿÉ—»Ï©lÛ›O?ú›lgÇŽ„í¸ 9?w²|òÞõ7~Ø\(g—®Ýû YÛ87í¿8íŸvq>&CÈåTm£·õ ¤Oï¾ùÙOÿ‰Š”P­-ÖùìŠ\Ü\>ýpûá÷²½¿õô“›/>Õ +óË6:ËŽDl†Žv/Y˜ +¹sððôÞ;DþÆEË7.˜ÿü¬õÌ„ßNólxˆ°_u~ŸÖªœÑ!Õ +ä\oh´˜) d™h8Ysa7© bFÌÎëêâñüþ¹8ôÑjªº) íÔè1«Y¿4åb­~‘¼’uY« !C«›õÐ>!í—òŽÑ#~¿³"‚¯éÕåÂòéÒÉ;ý«ÏËëµÚ®ÉÇz0QLÔÝ€²ŒIÚÍ;ƒ*\2jX¸ä¥z~6ÉIv":í„æÀ"DµÎîB¤DmZžWË$Ÿv"‘ ¥Å2íDi€‰ºÉ¢|œTr|²^_½¾tø¤·u'’éê…¾’FX˜â£EJHH… EqN7{CcVÌñõ%  ЪÔ@BáÉ%°n†Q!K‡‹&鸔‚àV<Ó®÷6‹M,2ºšY_¹•›;ˆ–æB±BHM‡ciEOƒ!š⤃¦å|H«àRÆÏD]¨è +p¬”Ñò L¢g*Ñòº^ß’ÓýæÊi÷A¦¿‡„ô\©³yøÐ(/!¡T¼²-/É™«—AÊŒîÁUv4¯WrÜJØ‚‘Ÿd—£¥ !Þ!¥´¨$ õ…úÂ.|@kíªÝÜðN~áF¼ºʉ„—¶n ÷w +JÕüè4,§VfÔ,¸i´òèŧO_}aCäsÓÁ —8‹A¥¡7öôêf8Vøà;?{ôög&;u~Â;ëþíãÊös¨›‡¦…„žëè(ˆ*¦”ü°_uñ0×ÙBÄ''ç׎s­5vQ6ÖÀå!¥ÝHÈìDg¬^· ²ËçwpRÎ¤æŽ„Ñ Þó¸R%x½·°»~õ±VZ  µ´¬”6}|ÉqV …Óv/ãðqA:Ž±)Z*DÒsË °`\FT+B¤Pi®÷Öo;¨¨—Õ}¬îDÂf7;Úf^OVgÝ,á >e£|T ƒ|!gq) …ÙÑrJV²ÝlwW+oHé…Êüu­°ä¥´jw;×\³$ȃnBõ0)+šô³åÖ†M²8²šŽ$3N IÙ:Ja'¦#|Ö…Ç€„Å™TP1Î ÔTã“TvDÂGwOóP8«i%Èh§38PeÂþuÀ¡“-hv’7?D†Fëã «Ó± “žËÓ~³ÅÈÃ)ñd#W] +»•ôP$\ YG¹óõ~ĨœŸ Œž Ë.U–n¶wž66î:R€T16æ#Ä!úH¶0ÊëŒVP²%Ó!¤”¨¤E-ƒ±š# â\ +ö:ÎÏh&À%o  4'†œ yã4¥Ð%´¢‡ÑÝDDV³k[×÷NŸ zZU/­û§Zq3È&¼çð°SDòSª½˜Œ°:&æØX›’Š~,Qó{{wî¼üÜÍh6TðÒšøz)x)f1cÀ÷,nÖ‰È.B³!a‹_p 2%eaãQ!d”Lc5ÙX–V”üPNÍS‘²—Ô£Iµ¡®üL\Ë/Æ+»ˆÜÒ²ûå)³ÏæFx9ÎÈÉ3cöó“n“;„py\ªúÙŒ ‹A‹Ép@ÅK'AuxÔÃÆ ŒB™4ù^ÆÙÙ†Œ™$>69¡c*dŒ Ç8zprÖ7>휵¡(©NÌú&ÌAW0$cj²ª$ÊB$ bÿò‚åÜ„ûâ”wrô,nrl810@ÉS¶à™q.òñf¦0ÇÇ&ióÑã–À™Ë&‡õQa'&PB¬ÐXL”æ¤DÕO)v?9ãð[½,ƃÄTÄÈõóƒƒËv|Ââ›0¹&g]ÎÒ¬‰çHASÓ%RM[ƒ+'kíåL±ø`˜r°Q…$î¡c6Âæ ¹‚ÉK±ŠŸŠ˜]¸ÍC:üŒ0(š´×Ôx¹X_ÌÔçq6Žç¡—m þG?&AAžwOšQ72Úµ “6“OtQIR© ‰^8=Œ†åe5Ó[=ée@q!½ÍÍg‘ôÀMF-^çㄘÝY‡@Š€‹^Dðã¢ÍGN™Ýf»/ˆ ž`ÈA¸ò±&)B%8 ŒCLtAsH¥­ä·ùx× ÌàçmÖæ&Çg\? ¹^ÔK¤”š¶cç&쳶à`é`ûÚ„‰Í:0»›òøyNJù0É‹ŠV75>ãñ2)Äa(@Ÿý(ɇÅ\^ÂéòO™ìcÓv»Û¯*J!—*³2x~sã2a sŽ›¼S³A?*;½üåIç™ó3gÎÎX,.ÑbÖí ѬMôdYŠ– ¤½AêXkÊ8{Ñ|qÌ6mòØœ-¤ü˜¯÷;¸(¸q2’Èækh:‹ag ÔX"_ëi¹ÖÙ ÇØŒÇéµJG£©ì„ÌUJÙÅ 5[œõaSÔ‰H«2²KÆSùT*g¬®.>xúF¥»2nöŒÍºÆfì$#r<¯iŠ(I^@QäT*â)›cÖå¶STÐ0Â…bâ`om÷p§»µ˜md=,¨ª–A†G«ýZqètAV#1ET刞$9]ÔÊP½€=&‹~"Sªºó|4m:õ¼ì^êÒ´û—¬¶  eÆgƒ0tV'"FŒrs¾¿¼&„P;â') +½S&ÃÉ ¯ç‹b¼ÄÇJT8cv“î`¡Â(-r¢Fñ1?&"´FÉJHp|˜gét2ªFcœ(…#aŠD) +e¤ÕÊ-®ÌÑ<¨"íEx `Bªî€èôñ>Tƨ¨/È9]N·Ëær9dY®’ õÌö°vópþúÁð囶vVôXe„ï#éØÔlà/ÎL·[4hˆÕêúišdµˆªD"’ö1/P!ÁQ†åJ ñF$ZvØKSö3ç§Î]´ŒO{&Fsùg,Ô¹1ß™Ëî “ÞY;isÑ6GÐåEâ™b FŽ¤<^`~Š`“Œ˜žu çÇ-&‚¢‚Ï‹»6Šôha´’Ôp©Qn¤ÒyµYO^ÛêÞ=ÝÜØêW«‰J)©'S’™]8wiffÖáõeŽ +´@8“ +¶ÒÏ—kF8ÊKšŽë¬Š'ärY]ê/ì?züðÎæ£ûW +Õü¸É1>ëñ'Q”""£ +˜& ÝJlekWÕTk—”íÅ‹‡{ðúéÇwÿðŸüæŸüÉgÏ×V+qƒ I<&¤G‹çq–‹¥Sñz-Ñëä:­|LRÉËV—×åöDd©Z+µ­åõ'¯>Êwæœjv#&€ЫŸè£báòŒ÷üùK8†ñx¹\Êæ’þ Çë #+„kÕÒÑéF.’H¬ì¬'ò9FÙ$«¤9©¤Û§ H Ä„à•2R…B%Ó²q)«;+Õ«»Ã^3WË*[ó…ÇwwÞ||õãW·Þ~vt÷æþü|ÓH&‚~ÄãÅn£tÐd’–ŒBD1%´+Ñf9ºÐ1W2꯮ùÞÑw_ýûoö»_ÿä½g×öv–Š$Êþ éñƒÜ18gÐ\<¥‰#¹h¸š‰æu¡_K,ÏåíÂÆrïνë×VÜÞ~õüäåÓûÇWµF"™ÖëõLL;^vÚŠ]œôÌX‰ —,.™§fœ.âqzD–JëJ³^Œ*¢ÌQ¥B¦XÌ'â)#‘  ì¹ËfhX» zÝáU([%Ž,ÄŸuî^í>:]øì½›?øäÁ'oï}öløo?þŸ¿ûòoøòý·ol¯Ï§R)Œâ,6§Ã:Kúl)égˆ~Ö·\%®¯&nìÕŽvªðºuØ{t½ÿöýåW–>{±þo¿xÿ¿ýþ»?ýôÆýýR&FØv“ÍO0a’‘ ¡ž¦ŽíwÅã¡òâ¨òù[<^zýpðù˵ùÙ³ÿüýwÿå§÷Ó{ÿù›oÿßÿý÷ÿ£¯m^ßëd2© ÊûV:©æ¤å¶¾¿˜ÉÅ*Û̱KÝÔÊ\vm¹±ÛþèÍÃ?þþoû?ÿüóÏ^>¸weg$zÖfµ{p7&]šqž¹0£|Àtrö(ïj# -ZcoÑxïùÁ»/O_<ºúîþâ«/Þzóa©bÌ/-öÖ®Íï½àã5ŸË ¡q«Ç©ƒþì¸ûèjãê@yy£óÏ_½úÍW¯?~ºùÅó•ýÅëÿóù«?ýñ¿úÞ­?ýöõ¯~tïp5«„ü^—Çbõ¡8 F#Ns/ÙW +ýééoþæízûÃ' ?|gý¿wëßõúŸ~xïW_^ÿÓï¾ý¿xüé“ö£ÃÚÎR%mh™L&¬e"±B(ÄÅ¥`3‰¯VÅ;›ùwï-¿qÜ|q½þ³Ïoÿþ—ÿé?þþÿòÝß}õâüûçÿïÿõÿþÛ¼8éüøÛWÿå«·>xqÔ‹-86ã³y*ñz\A÷Y%Ò&fç‹¡ÓõÂÉfeo!q¼š~~cþ¯¾ûÆgo}öÞ½Wo<4I7Ê1‰#ɼêgéí&]ÿìÁà¯?¼ñWžþú‡Ïþ×úøÿùÓ¯ÿøÏ_Àüûß½üÓ¾÷ƒïnËI]q’¦§Yš +3HU§–ŠÄÑœpgUë°øÓ÷®üúÇϾúüÖ¾}ðËïÞúß~ùÞÿø¯ßû?þðù~þì_¿zø»Ÿ=zëîZ1æx Äêôû'"Z!ª sÜ›»‰n–>¹Ûø«w7þËOîü÷?~ò¿ùö?|çÆ~vïÿõÛÿñ‹¿úâÚ?}yõ·?úÿHzïgÉÎó¾ópÙI3sgææιOè“sî:çïí›ÃÜ0éNNÀ$ ’H‰ƒDZT­$K–,Ñ–¼ +ÞÚ]Ù²ì­òþ°åß÷i¨ªk +˜épÎyŸçûý|OxßGðkwŸž7›Õ|ú(Eã±7펋û=óéaî·>Üù·¿ùয়ÿø³Ó¿ýÓïüò_ÿ7_<øß¾wçÿýûÿ¯ÿçÏÿá?üÚßþâÝÿñ7¿öûßr<ÍáhèúÊj0† ­óü´¦öìT׌>Ú_ß9/|ü¸ûý~ñë?ùÖÉŸýôÿþ÷?ûOôùßýÉ·ÿéoò§¿ûÁ­ÃÞt¶•oN¤žsó5O¸“ÿþÛÓ¿øѳ¿þù;¿ÿ½;¿÷ÝÛúã·ÿÛ_ýà/öê_Üùó?ÿ¯¿üÞ_ýôÍŸ~0üþ[­ÛS­âˆ+¬¬F.½±’ m.6) cåÙaþǽ/^üÙï~ð¿üâÿüwŸýýãïÿä›þ£çþ[÷ÿð;‡_¼è<ÜÉö Œ£†p’EpÙ¬ÛôG{“Ú¤"ÌÊäÃMñ×ÞÙøù¯^üñŸüÑo=øËŸ¿üû÷Ùþ³oÿògïüò‡ÿù^þ×ÿÉßüÁÛ?ýÆöwW>¸è6JV<Å“RNRŒVÕizL?OÝe_ž8ÞÊëIû§ß:ý›?üð—?{ë/~òâþ_?ÿŸÿåïÛ·~ï{Oßë&E²I”ËBö’蜘îyänƒ½¿i~ý¢÷“ožÿâ‹Gòƒ§ÿô~ð¿þÇ_üòÙ¿ýþÃ_ÿäÆNÓ18Š¤B’ÍŠfU #oŠ|QŶêê½íú[çíîÔþà;7ÿÓïôwúƒßüäü7ßßþýoŸþ»>ýñ·n¿sopk¯Ón¶U»ÊëˆHÅæŽâ–k¹\A¡†yæÖfþîVáÉ~áW_Œþè7þåÏ>üů?ù½oßùþ§ß|´ñéEÿÍ“úÁ8Ûªz°‘Ã* ÓísŒTrŒ‚Á–uj»©½s«û“Oÿø7.þãÏßþë?øúßýÑgÿð—ßÿ÷?yù¿ýðÿü›ÿôßþ¿sñ³¯w?¼[Ûl(Æ0ÝnÊSYquŽle鎉my©ç;Ú§ÛŸ?üöÇ7þûÿþƒü«ßøÛ?þøÿþËïþÓßþèÏ~÷Õ>¿ùÅG;Ó²ffIÁÁ˜l­1RDÑQ™ÃaaT gEôÖ@xç¤úÍ'›Þ¿æon7ϦÕLJí÷/zŸ=}ë̓Ï^Ü™8wÇÙÛo« ½D'0ŒÌÏ©F.ÊFUQsµR¹âèeSØïîn5îmæ>2þÉ7ŽþÃOÿÿÿüOøüw>Üÿí÷fŸÞ,¼wà|xÖØoª:'± 57EYÏ!Ò(M¦ã&l˜äA[½3_æ?¹hüæ{Û÷ÇŸþÿøoþá—ßý«_|ò;Ÿ>¼¿×5Š½a\Oâ|*ÇSq% ¦Øp…QYÝï:{õ‡‡wo÷øÑÙ>¿ÿ­·÷_œ÷& O,Œ„Öñ0":åZÿÔ.MEÑ’hºU°on=ºs0,1÷föG÷'¿ùÙ[/îl=>jÃgO7 +ýªåš¢©çD3Ùébb™Ñk´VõÊÓt9ÓXºlëe[©»Ò^?±Û~|côà¨w>«O›¥I­4(y®)Q4j2˜b} ‘J(©E"Éx$’I¥t–ÖªGÓÖ½Ò[7Jßo÷_ÜûoüÑ^üÑo<ÿýoÝþÙ§'¿óþáÏÆv]KL£©8#èQLùÊ88‹"L5Wì–òŸè»ØùÈz°å=?(üì³›ÿí¯û¿üÕoþùO_ýü{O?y¾÷ø¬»=*–.š=?Æ….ØI\D«Sïæ4¡l² O*ë\=+OjÞ¨dm–ÄûÛ•7onÞÝnö¾f©®ªJ¬€ã\ Î/™…lÑ™©@gH2•äq̳ÌJ!_/šy· óžÌu±î*&ƒèâÈJ³9„žÎHI`ƒŒœ«O¡g!`£d2E‰¼Z)6·f§[£í–£í6³OÚG-õÁNýÖFå°eµ­ízv«îÖM)²ºð'â˜N)­‘]XŽíÒÚòj8Kj,›×¤–«–lR÷šê£üçÏ·¿óÖÁ{w¦7§m—cœÑ²u½~P='„Äs‚xÑPTCU5ZÍFµh—<µäÊ–Æiš(ëF†W⌲Ë,ÐXFg*&y+q:‚*¬ä©ókyõb¥¥¶Ä5 Š•+¦îð¬Ä±b,Å’t,͆Sìj„\‘I¶mÛí„R­É[8!¨zÁxhY¶ƒýÉä¨×Û¶³D/èA=Ûˆ¤ÈKËþµg‹½ÏY=9Û!Xkq9°¼Êð6Ï0 +¦a«Uq¶†¥‹ƒöIîînãÞÁx¯_U²ƒ²WóœX<}iÑçOÌŸm¿Â^[Š,†iL‚C|Pí—ªže ‰¨ÙBI%Š2v2ª~øìîG/Ý;ÙÛéu\]ahÁIBÎ%h7N;ëiIÈoT¦+£‡…Ê/dg³£{ÞÃpÁïKR´fYåb±]­õó^Màôx”J!êÊ:º°”\ÒQ ¢V ­')—UË‘0’N ñ$±ÀÖÂ8y‘R4¦GòJH`<™„ ÈGR-å:›á‹¼3TK³¢~e!ˆ±(™%X[s:‚5ÀÃy¨´v£› +dzz>—ïèªC L†PqJ_â—cIÊÉåµ0ý¯¾ºü•×W—Ö$£êºk˜ c0”YÉRtM+¢²•c %k¨¥@_ ¢7Éïk¥™R˜5fj©§$Tp%oëžÙ­CLtÒ”,i¹Îd9†^^K¬Ä2;”Ê»Ra›2Ú˜\aµZgë.ãöW=)ÎÊH ¥f¦Él†6ýìòr:eóŒÑÉÈõ8éÄ(;‚›Ì—n¯&ÅÕ¤²žRÒ\Ar{Za(;­li@+xo6“Œ›æ ”Ùa­.ow”âDÎX½ÈZÍW V”̆3vŠ+$™\Q’”‹®`Õ­$Øm«²iGªÓÌ"Í+Š™çõòb æá Ââí‰R?KŠ•Õ8se-õúJ|-%§„:ïN‚¨¼¥V"ó9ù >á:&dÉ,6&gÆÈùãɤ\Kй8íqí•ùô× Ê⬶×>#ônwB¨#M½<3+ÛËA*˜”0!ÇšMÞÆè\5¢„Íš=82¾¤´æ0©‘ïÝ’ 3Î%ùÒRL +FyAÎ Z"?¡·ÕúÙ=)Žo«µ½8_ô# +ƹÉJ®ÂO«Å™U?vÛçzãd)m¼á'/ùð6hè_¿±v͇±ÎØìÝvz7åâv0­_^LDR²/&}u!¶àÃSBYÈm™õs9¿Äíõ´~5@­GiRð’ýÊ"ßµå„?ʤi[¶Z¼R\YûCi4£ äüzV ­¬%$¢E˜|ˆt¿òºO©"ZÔJ;7ž7¶î&ä"n6Ór…±[¼ÛS Óæö?©q%£U¥ò4)Wà³q®ÄxÕɃÓ7Íœ^‰ÐÂãŠZë“ÛF•F„0}óÇÒúË«cqÆKòÖI¥=ÆñÒF3¤£\m‰… £¾Ã¹ƒ”PÐJùÞ)c6»—1štnjï½[ZóH*nÐf£µû‚Î"„ÎÚóå´„â&‘í'•FZmÈ€-[6o¾JŠ…ë*€j­¥Ü”TAµfŒvU§ÛšÞå¼ñ|.q{”TÚˆ6 ííîÄ™\(c®$صÅUÎl1Î “ÛÁ¤N‰‚Õ%ÍN”vaû³ÝS#Ìbæ ENÚ„Þ„^‹¥ÕÙù$Û)¡˜1Úi¥µŒ˜ óADX +áaL19ÚìZ‹ÔáàTp¥I™])¿¹–”£”—àªi©çʸ6Êf!@\ógDw~Ér=Å15Zi£ƒhm!?eì^Z($XZ{1ü„—êJóŒ-î)¥zãŠZŽð„Ú¤ÍVˆÈ&˜R„(†3yç4_Šd²‹!‘«JqK)lEHw)Âø7”³!;ý¦\[G¿z-º @Ãi9Á䃨%í0¦¥g-YƒÂ•‹›bi‡-n3…mDi^YÍdì1aõ²µí“Ÿ–&gA&›”Kqßê6tÇ|:÷Üx~³McG¯Íü¤¶˜};c JOK;o¹ãûÈ|ÚX“Êö…ÒžP>@ô^\¬Áñ— ‡bi$²Üä¼ ½}W®Rö| +²8›‡  qŒuB¸Ne»ryæôÎóƒ;^ïÜlØõýÛPnËvÝéÃÎÉû£[õNß÷&÷*³'‚=p˛ӛïélB,Bi åÃlïniö¬¸ù,Û;OógNYwDš}Dm@}Rù)í ;·ï~JÛ½Ë!"@9zãFnü$Û{Œ›“W C×ðE\©†q%œQ“ÒZZf¬›Gåz”)(å½¾ŸÝçÆÅ:aOá`: 6Õ½(WcFgvO.La—â|R®¢z3Ftn›0; bQT–Ýž˜›PF'JX zœ·i4ndÛ§ÙÖõ„ÂÚcÎÛ¢³ø­[Ãí«1LåõÚ&ׯ…ˆ8—ËX]&¿Iç¶2Vßó…P‹[ ± + ;˜RR倰Ç|i7ãLƒTžõ6ÍÆñrÊR3%hø~©¸…€Ðù剅Y±V"ü|æ^ÜÙ4['s¸cW"ÌzR…¾Ý{mÁÿÕ«¡×}dˆ.qî†TÜ!ÍÞz”÷'%Æ€6iEh/ÌäblÚs¦0Ð…ÁÝ|ÿ!% «-W·ãRž²Z´7æ¡Ý¡ÕØ«lÜ £u\£½a +¼€/£Z#ÀU¢J?.µ¸lO«îÈ,^6¿)WÊ[/&·?o¼ÊÀ!ùòlãèYÍù'¨"wøž=¼oO°¹‘춬Ö^Æî²ÞJ”¶lnb¶OôújÔ”ÊÆÞãϧ÷>œOÕ»ÉWÒJú½>:߸ùj)ÉÓÙn~t¯°ñ¼²ûVnã!SÜŽË ½¶7<}/7¸@-³~8=¯{ôfsïÅìî§Be/)U°/W­Åu(4mpüAçè£âö»FïnZ¨&ùè6å ¢¼/¥âJ[̈¥#½yf÷Îâ¬Éº=£}.×OØÒ~yûyëèÝù¬>¥ÝÊÆc.…áÒÙ6“ÛÊÛJíÎïú‰ùÃnZacxø“ËT‡º’ +›Îà>[Ø% ;ˆÞ`ÙB÷¦^ÚZN)q¾&ó)±.UO´ö&¿¦KvmçøþG!Â^1×ã2Ú»3¹÷]ÊÜzú/~ü'ùîñÕ¨¸ŽÛ´·QØ{«uþqeÿm±²ïÇ-§sÔi5¡,øÉ„XŽp%¨op_«øZ5Àb¸º΀v-DÅeÄ œ©X;uÇ¿º˜¼æCb¤&pÆ‹Q¹8[ +àn˜)Iå«sîK+«óÙ·ì$_LI5£}3%T±ŒiMÒ™r…ÒôiçôýŒÙ0ª›àtÅñ-(ªlûP(Œ8¯ÏxýòìVߣ~&ÛWkÇùÉ“üô‰R=@õöZF‹©+6*[[gJc½<ï]|ýW^™Ý +r^&;öFk;/[ïäG7“‚·N­!J-Ê»i£äò”7n¼;¾ùiqã!íM{P=!ìAR®±ÅÂæµqÂäfFó†ZÛ½D×ʨO­æL*oFøjJíñ¹ùýöÛ·Þß¹óÁUhÛ”L™ÃÆìåàìS¹uY#Rk<ûèÇ…Þi(cSVxòîÓO~ôô?_|—wc\IòÆ}ûgJqózLŠqU¡t”<ªï¿Ú¼ýYqúW«Ýû¹ÎÁb‚ Ò.nOõîýáÙ7Z{/:{ÏYg„È ¯3D¸!Âa½)WÚ%ݨ_nx¿»ÿò‹èNìÞ]6¿Åä&¬7Fåm´·o½sçÕ¯ƒ<&ù²^;Öë‡by·†¨ÚNðÊTfáïB‚±¤ò–Þ:rú7 “{k@¼QÁ­ívöŸ¯&Ä……èUÏèÜŒwÓûi¥{-"Ú¥û/¾¥8½ùÚÚõ@ðL­ÖöÞÙ{ü=ðZ»óæ÷´òV€°‚¤ƒÙ›1¶c‹àMZ㬲õöþ£ßؼû Éí߸õ²¾y“ζ¤üP¯í–'·k³µÇVÿ”ɶ‹ýð;0 ¤Ó…Öc…6‘«û¹ñ®7ÓJÌÎ<,m¿[=|×›=&žUœnßùÈlíÆO®ì:ýÛã“W§O>?yó×Û/ŒêvgónsëBÌOœîimç™Ó¿Sߟ¾»ÿø3»³ÝœÞ¾-–w n½™”JðCs¤ÔŽ©ì@ðz½‡´;ˆ°¹yÖ¨2 Øù­ÚÞ[ƒ›CMÊ•=£{><5¹ø†»ñ³'~:¢\±0ÎOn­gx.ßì>én†YÝs¶°Ìy-L#R…÷FŒÛ!ó²x Tohͳycì8mzÇBn@U½¾]Þ{j÷Oî‰7¸åuNã¬z¨5Ž´ù(ÏÔæ¡Ù½ÍWO€r£ ¹²íCÕÊäv}÷™PÞ•Ë»` x˜Ö¶ê»w> Œ&¦TÇ7Þ¼ûþoí<ùVçäUqó‰RšA >¾óêÑ? +v‚/ÕÝúÎóÚî‹êî“òγW¾xñ­ñþããâJ¹¼ý¤wòaçèUeû­Æá‡Dv²S­¡lµ¶¯4ŽÄÚ~Æ™p Š«A>F塳p½·À®…‰… g+¤5✉ìMý)ùõÅ0!ô¦÷SrS;~ ò”·sçÓl}/˜1ã|*!J瀺SR•0{jíÐëß‚^¦µÚæÞ…ÛÞIr¶è +ƒ;çÎî|\Ýz¨T·Øl‚acã‚Ç&^ÿ¢¶ývnòÐ]XÞäz7 BäÊNvxao³Ð>f7ß½58ûH®í$Ä|eó×;ëî>¼ñèãÑÙ+­¶#ØÝÉî£îÞCÊlyíÓ'ß:|üéí‡7?*n>ÝÚ­‡œ<ø$ƺaÚËX=.?…ÈöïŠÕ#Üèyͽ»¯¾0êÛŠvçLª+­óâÖ³ýg_ŒÎ>4«ûwo¼Ì¸]­{ÂVqwK¨žXÝû¥Íg”;aÌúÝ—ßíí¿˜?íÈ—£‡ZÂbj³Ð»UßÝ9{Ö;xŠ˜Cr¾,õFZR(‚Òvߞ˦˜·ÛûÞè–Õ½QœÞ3Z'@b?îªô$Âx¤ÝÍöoz“GÞôiuï½Üä±XܺõâóÇÀ~}€«ù»ÓâäA¶}¢–7`3jutübrüªƒÚoð… ½ºSèÝÿdëìPƽqïàùðæJûtþÀ{~“qƧ?Î7·£˜:âuz'78×+Ûri—pgëo)¡rÙnœvš{϶Ÿüjçö'Öø¾3ºkîîör\糃Þö#JoG3rŠ±Ã“Úl¤9ˆÞ•u<’±kcòWC\shkJYS¹ráL‚vZª$¸R˜ÈfÔú|yT‰°¢ÖkÍC¥<#²e2z PJ¯ï:½ÖB‚£û;gli늇Ҫl>*o>㊳´Z[&WjÍû¼Ó%ÕJ}çQïäÿÉÍÇ·¾QÝ{“Ê ã¼×Ü}Œ*€4ž6hÎØmz½”ÝAÏ(­J¿ÐÛµZûÐÝD¶Çfk^kW)N–â i´á#QÞ‰ð6_ÚÐÇFóÄjêõYŒ±D»}þüW·~KmÝ0Z7\P•üˆ·Ú‡O@Òr­8{–›>7{w|åªh-­¸åãåûǯûдTŸçôñ}­uC«ígk;áé§ßùñËO~è§ra¶ˆC©vÒ9ùpëé÷ëÇB\=¾óÎ7¿ÿÅÁé:ád¼mÌÝÅÝòö[£{ßuGQÊÖ«[jeëËÙ}+«i©F[Ôéî;·ßµ›»Ã½ÇjiFZ=ÚFØâ:n‚³y–Æ÷ÅÜX€Wy–1Ûí­GƒÃ—fóª4 µQè[µ-L©qÕ#kxÏÝ5:çà¡ÂŽY»}s9ÊøÒ2®w˜ÒÑ}`tnáz+-«›7Çço‚›,DX¦cF×îßË6Ž¬Úžè ÖÂvc/Å•Wã +Tѵ°°–2“\Ùmß.ž2Ùa®}äAn ­P{)®1‡q¦Fã4ÅW˜·}",òpäÉìÉÏr#ÌæÓbžµÛþŒFeÛvïÔhiÕCà±üì9æ „¡7÷Å궟0Ó +Ä¥>auA÷ÄüL¯BTOÐÙÊäNyãÂj ¥-ÂíÇäRR©Y­àò ¡”KŒ3àœÓØËuÕêÖ¦fä¢^&‹Ö +‚7€,“Ý%Ì.¦7#Yoƒr†>LdìºÙ=¬lÞï¿jì¿\Ç”0*Iv«¹ó`ž[©ù¤Ñ/OïTgw*›w‘HµÁ{SÐêk +ºÀ¨ïož¿„£VvÀ¿à/¯G¥8CåârœŽP–\šäÛ»õáÑÁí·úV˜Î‰Å}¹|,æ·!æ@dHë=­´ÓÝ{2¹ñf”ÉEÙ’Ò¼él¼©uîÅm±¼‡hì›'o{ÇÿÃLQ[ùÁÅøìë­Ý§åÑ-¯½)âãoÿ¤={êzçÜϧUÏMï›ÍãWÎî~ÿßüÇûü;ˆLnKoÞÌÚÝ3¥´)»ƒýów~Žé­˜PH¨­Œ5 ;”³¨Ý$_±Êݽ¨V‹Í_óaóÓV¤ÑÌNÇ/¬Kju>Û×Ê°Ïû»O0¹±–˜gÅl´ÿ—êWÖÉ¥ˆ´VÛ|ætnAôKðu£°ñÁç¿sãâ½KË©Ë>"@W¥—­Ÿ8õcðMR.‰¥-HIpTã”-æG¸Ö"ÜiBíd¬A¶}ÛhœÍ#¼ÇaÜoHfû)¥ž”ë¸ÕÝî¿Ã—6¯Å\m‚’`fÛê£j=Á’B!£UÝþ9eµA¦”ÊŽÙ=ËO7_mßÿ¬¼õ(!H³¡Õ¶ÖSJŒt“<°S')5P­Md»1ÎÖ‹}Æîøp3!T|¸íÏd© Y¾¢z ¾|ûæÛ¬Ó*f¼ WÚ +'Ôj®s2ºù~„É¡RŠ–P›ÐõVsO­mGæK﹜Ӌ1Ž/%’fS/Žë£9?¼殆ù5ÄHŠUä—µ1ÎÝ®Û=ìýr&g5Áç!gµÞì¿›k ~AÄÀ¸2Ê–Öj·ÖÓZœ/rîHòúNe‚ÍŸ&9oÓêÞ ãÙõ(»ìÏDÓ*)x³‡éݤTG¤š^Ûr0w¨ùúJ³òìÄ1¡¸Àô¦Ï§sg©¸ùÏ›Wœ>­:n?¡œÁbLª•Kq±DæF ¹ŸÕVó€S« }å{jã@ªîHµm³½Ï•6ãBâ¿XÙñ#ÐD=Ê™²¹MÜÄ…jö CËÃS«uáòø>SL‹UpdPû¸0Ÿ1^,ÎZûÏC¤ VÛ¸È nfˆùM³~õ"ÚhrÙÎ|:>&ϸc¦°!1ʺ)ÑKñ®–+…ÑZ‚^‰‘3¯™[†¢Uj‡˜T!Øìpz:ؽÂ…éuÌ줵¸çŒ(£ãKKœY7k[Ra¨•`³§¾îC •çŠ½›Ris9ÁEI#Åæýˆ‰+m.·›k)…ú¼õ +’òåULpEð&í—Í·ÙÂþå ·šÒp½»ˆæO«Iƺœ¡Y?ä@$º]ߤ\œ1#´k¬;Ö«»bq ÄßWZÙØØ“’ǘ5«¶™Ühì=Êo®¥tH‹à†„X§äúå•ÌrDÄõ¶Ñ<.OïKùi7—Ã\‚-˜C1¿ñÆRòê:F5Ø`x‰ÅoøHjÜ–j +ªQÊ ÔbF­¤Äâ\…Œn\jÙ2”itB¨l5ö­ö˜©Ñ<%²ý_ô“îjRDx/J[àn…éÃòì‰TÚ…VÊ[„Õ&ZipZÙ¸ãÜëDyü}ëîÇGO¿Û8|›«ì…2„P³{ã‹ ÅaªÚ¥–wvn½ßܺ«V6[s0kp + 8$cöáûsÃ{•½wZH¹Pê¨Å*W“BŠ*cto v„ëZÄ”:iõšü4ÄæWQ%θ…î‰ä/¯Æ.­DVâ,ð([ŽË_‚aEÇ*L€Ð–â<åL¸âŽX=¨‹ÂÇ“ŠbOÿȪo„p9B™¾L6%6´ê ±¸ú˜¡×T°¹z5Äø#.4ˆìÔiß* ï»Ó+~2ÍäÊÃ3¨íÕ„àK«~Ì—d¼¢wÓjÃgÛÓ;ùöa˜°B¨"Ï—Ö—c¥7õÒæJ”ºæGÿyþçô|—K!Ê[Fæ ý˜Õ«ºÂÍÆΠM«w.•vÐùÌœêRˆDiKËõ æC¨†Jó™ôêÁ\H39?ؽpkœÏRV#Dçq£¯U(£ð•ë>|%Ì8õÝÒèìz˜]‹K‹!öÊ¥«–÷”ÒN4“õÅ9~þ´W-IºkQ¶ZsOÑÊKòÐ.h%°'xAGpùM¶°‹{;ˆ9J)a‚%Ø\0­¡|9£wùҾںͶ¤¶’¤@Ñn›Òª¥ÎÚ pµvPÞxPÙzŠ[ƒõ4ÄÞZ¶yPÝŸ¼œž½m´Q£]œÞn>—êû×â½Y?el …íÀµâ´±õ(B:0 +Ri+;xÝÜÂÍ~B¨Fi‹ð‹1BMÒ6kõúqsçyeò€wÇaÊ^Œ²¸ÚÂà¥7A9!ƒ‹å>ÐSXO©2 µíGT¥¼WÜ|\Ø|PÚzftïFÅDµ8®m>ËÛI¨aw‚›.7ƒÏW—cl·:=yø1"×–’ +ÄpTë¤åºV;Ö›·Àg#l’- î¡÷ ½'äw §_‹°—VPŒŸO›†iµiG3v‚ÎÑF'-U!Ÿ.%¤0¦öv.²m?®ñÞØéÝÒj¼7ƒ¿^Š‰o,§®ú3´V_XC!o6ö+³'7ÞþQïô}©8»ºŽÄÒ|ut¦§P™ •¾´FÉ0¹Å0=ŸG©¯§Mpð7Ö0(ZøW”Ï1jaZËSzŒÎR˜þÚµè¯\­†9”-ÐZÍ—b—£d’ÍùÒP¢möi«#ò×ýƺ(g/Ñë«è’ŸÆ$ðP.–æseع|°Ìd3FºZ7º´=6ê7ÌöM®¸¹Žê—}™p&›ä +¨\N+U>¿¡T÷Èl/%äüq<Ç“”ÆXMÈ•ùù5š÷ª»/"Bu1J§hU4ËR¶ÆÙM*Û‚QÝ|´ýè;…G¨;Œñ%ÞîÑ ž¤AX½ÒäÁÑ£ïí=ø¢¼ýÎZBŽ¢ª’±Þ8%–¡ö({Bd7ÜÞ=µ|góÁ/Ú]Á‘zc¾ò[\ŒˆëˆNØ£üøRÙËÂèL­í„i”-ΕYo–í]èMˆ39ÞªŽOßËÄêùb ˆì ±ý0ß?EÄ‚–ÆwÀµù£¬wX»Ÿï77ï¡b—+VãàÓ¨íñ¹Í_k»b „³³‰€°€ðáGåÒnFépöÀ‡˜ze—±ºkq1Jºi¹É÷Œö¹Û»ÈeÐÒŸ¯mÞ#­nŒ-¤åÖ*’…= +¢:T.®­#ŒV5KÁéh•”\]NËa’Ó*²ÝóÍš3ÅÜeôp¹£¼Pƹ¤¯ÞîgÔÖbˆ‹3…8_AÕùºcq¶0Ÿø=T¼ŽUÝÒúó¹þ¨¹â…¹j’êr”_ðcns§½ý0É^_Bá «Qð¬jšÉûâ5?ñÚb̗ș䋗çósâaL˜I+Tï¢Cð}†Ü·’ £ñ¡:ñyÉÕöÖ3ÖJJO‹ªúÓ<"x…þÍéo–¶^öpyþp¢ˆ0:*z˜Z⼡TÝ…$EØ``TȦ%Iʨ˜£í.ãô 0²•²¸AJÈÒ¼%Œ¹§7v'Ç/‡'ï Õ“ î \>£T"¤£]­²/䶀a´Æíl3N0% V+#åChWI£Æ]D¬J•èå”P"”R¾wˆH¥PÆÊè: %w?;¼` ãÎU¯{Ì8}¦1¹ÍòöÓþéûó+’Ó‡„цŒSjíyfæØÜŸ¼»}çëí£·õæa×ÍüpzüÂkøQe-%¯%e¨mÞ‘Re¼ÕHFõ•ñtkZ¬ƒ0 W;ó-r[€‚aÜ,tN‹£»´;%²Ã¤P]IH0ŽXâ lÓNï6”tbÞìN3 Býx†w ÑÅÇiìsÞÒS”°3jÓŸÖ.­ãÔ gìå— )o>¨ŒÏò½"ÛóS°„ì¼ÆŸí,†É¢®'•WEÕæRT¸¼†/HÉézÇATYžOc¥f¯1{ÒÜyIY£¥ð…Í“·›÷B„–êà›rqG.ïrÞd~7B˜s#³±fÜKr³âBÝÓûiÊÆ™¬hT{{ùüdÏ^ +² ±¢òa¦,W×Òâ:*Y\óÔórTx})uy “9(­«2JXrq3Û=‡êÅÔf’+¯§õÅÀg SºëI—ê|y_ëœ1¡è\ÑW#üb€¦Äz¶<[ô£_Y½±˜FØrš-†0=Œë‹aˆEEfoÌU])Û­ÃÒøfir«¼qW©n%¦…2íØÃËëÝj¼WÙzጱ'$Ø,ª¢l–q'Rõ”ðöùÒY¾ÿ°2z å7×1ÉÚ”ÕJ +¹ëKÄsƒ£Ww¾ ž²e)PKळ˖wJ3Û½[˜>AµnŒÌVz‡zi ²>Üò¡Ù º•ï_¸ƒ‡i©Ç<„¿µ({2Úª‡2v„ÎõÃæþËÄ*GjÍ”Tƒñ +‘.ô*×Éù•…¥( 0&–vË㻃Ó÷RJw!Ä.Å8 %7 +¢´u*;Të7'wÞCõörŒ’Ìusƒ8ij0ôó{Wn*Í[)©TsÅÍ×OÉöƒÔU?=ŸŸ9Ü5Z¨Üˆæbˆ!¥–ýi.’QbèI¨Bëar-FÚ 2¤Þ6üSRô*¬¦ç¤•'¤^½²ŠRÕ(ïf›'re/JzTMf4F+±Z!–‘©èÖö¬ÚŽÕ<&¬aL¨]Z'WBDj~K•½$@ÓR\‰³G¼·§r×ô×®'2R)ßÞOs΂/óÚ2¶’¶Ðì†Ü}äNNߣ­\¯4<‡´ø¯âWÌbR‰-ÂÛ“‹‡¾¸øÆbœÕêÝÙ£KKé}% oHI-±¼ÉXÐÅqÆN ÖîN›µj¤–OÐfp>•VVÍo˜Ö饅<¼A)Á–ä ª©…ð•í&Åeõ„Â6ånBûã*Ä78?=èÒÎH*@ †ê…ÀHšŒÚЫۙl1­-Æ„WPŠŒÕ1*{ÞèjöÓZ;ÌW5ÇA©ÇÅÊR\$íÌPµ›ä*ÀÆëˆq=|b'èb%BqPõ,ïlR戲йk) Ê*·”T“rÑû¬· .HºËIa!€@ áR„7Éçc€™CØ~¥v¸”ßXCW" w”Î%ø"a ­ †žïõ#Úî%©,Ä1D®B.[EÌkÞèœ=wŽ3Å>ߪ8ïÇ,Lëq¹Ý¥„êCthÆ$ã’ri%œ™ÏVMÀ‘ÏF4ÅrTŒÒyH눺¡“Bu7íÞ=`?ª]^C×#lã®SÙˆbª/!P-NX „׊?à|«»óá7H+…•˜°— 3æ4FÏMУ֢„è µº‚ˆ±“¢P¡”¢ìµ0ási¡Ž}ÜƸÆõ¹ȬEHJÊ“u1µÄÂüŽ,µvD9³kQå+WSÀl±&d‡«1^ÉM8o†Ê:]ÐM(ï‘Z]-n`B~>ù ©R󜒠²¬ÙsS­¼Å9}ÊhPf%Lêóåó",*TiH³…Hù´àe´*i¶iw§‹I¶Hš=±´]ß}Ñ<|%U¸o€AŒÙy(#½SišÏèUÙbížœŸ-@Kµ0WF RUn0…]ð¬ ÜÀ¿¼©Xjf)*†39Ö™"Råòr|Ùje-©`r;ÆT–újʈ³%„¨"‘ ú7G«¸õUþ5ºœàp©Åõ$¡ î ‡-lãÙq˜/-% ®8ãŽEIGÈÍôÆ™V9èì¾¥Uö $h©(Øí¥y=L…È"·”Ò^}ó)eo0ûz¤•õ¤t=ïâztÏmáûÓÊÂ:¶–@Æã\ò`ˆrãBž±7äâ^R¬] +Â×2P¢@ÂŒ= [K ЈäËvie  Èõ( <°ãÄR &xI+{åi©µµÊ\ózlžÎŽW“ÚÕý•…`‘Õ9£N®®ái6Ÿ` +Фi¾Õui)ùÚB(Me3óÕô¬•¸ìKjþ¤â‹ `ÑŒ@…‹ÕBï4Æ8kIþ÷Šº#ˆ øsÁϘ…ñ£÷¾¯å&¯-¦/¯s ¾ dâKr!L<ÛMÒ.T5lÔUšuçóô*%Öj‘Z@ôúJi‡q&óS.¨£²ATö#"xD’Ëq3ÅË Íæ‘Ó=1›û¤ÕžOÂ&ä×SÔ¶?¥ B ² m¡<’ì©çC´“³û÷äúM±zÎç«3$ùPtJ(€‰'HG- …CÊšPÙ ¦uæÆ®¦#i…Ñë¾”Âf'BnUûðÍ ¢|~w^WbAr†-º˜ÖAºš[b~[Êm’2BšÝÙ-L©2!ú0©ÍW®rCˆ™A\»"(½¥|yNŠ-ÎWç9ÁþÒæJœN0€$_9 àöBHXŽ)I®*z3Òè\4JÈÀÌaÊMÀù•-Ì'ź٢ ’\=F—I}€ò……µôuHi¯/%A$Ó &Õ#©r4<ÿäèÅoÕ“×—P\ò'ä…õ Øñ5ÿü6û_É6n83\m,†¹êðmöhözL^L[ +(eÔÝ}úð£ßõAª"sAÂK + ©xÑz`_®Ö‘ѹée ©1ô‚ŸŸÓÀÕ:d|,hí•”ÎBe·èìæJR®^OiÀTÀE”9{EÄp>и·’BQ«íeLøþ¼Ɇ03IÛIÊùŠe4L­0Þ`~÷o~„SIÚA)“DðÂÔüÚ^Û— ³0nÓ +)Wq¡°ž¤VCk1Ÿikèön¥¥ÊzZY +³(›#„¼/ƮǸíRV_®qÅí,ÍYÖ€Ký˜’’ʶœC¹cJÖv­£jÆl«µ±¸“›ÐtF3Ú²›?É­D)Rïèõ³´\OEÜì^M(_¹&eR©®E™¥y~”ßlÜNWcƒröøü^”)\B¤ Á­ú‰Z>ÊC?Ð^”\*_ ’ü­Æ¡Þ<³•5Ô„ˆJ€øTöÙl7ŒÉQ*Kíúæ£Þá;˜ÑZÅ´Dµ:®wWÓZ€+ ÅC©zJf7|¨yi-µ&à Df~/îBÛ¦ÕA\¨ì‘re9L'(Â~y|®·o¤ÔAˆ€í䃨ŠIå ¦úˆ3œ"£vÌÖ5̸äÏ,@0LÉATZ™ßÒÃ\ Ò¯/cÄˆÏ È\¤ô•G¤ùókˆ1e±AýÄâ.ìékxœ)h•C¥zƒÊŽˆ|{Ù^iÚóE¸×®ÅתVÞé=5[»Q,i~é ‘³Ç`ÍÀðsh¶oFO‚d>ÊTã •Li%­] 3WCtœr³µÃòäquûm£{±”P#h?b‚»)/ÂW2Öª”ËoAè$ƒöY²P–Q*O[›€j0”‹QÜçZ˜^ùg½úòYP*ÀÔ$›g­žœŸ¡ø1r·^9¢MhU'€–¸ü$®4jS(ï—w_IÕ“5Àq=€Ó¥´w+%ÖqsûƒKéÀ6àüå(U¶J*n‘V¶$Ζ£t‰2zi¡äó2‡©]»waungÌQ”)B:Nq©×¤\ßjf²}£ušŸÜÏO£ú$:E-n›Í³¸ÔQeDª§!1Ð&ÚR$ÂaôûvïnëèCÒ›ÌÝŠt1©ŽJÅ´ä Åao$…NŒ™Ÿ! âÖµ ûâKˆI:KšM6*;ÈǤ&&Ô—#Âåul-.†±ìj¼Ï€ÆËÈèTli¿¾œúÊ• ÎÏOòPÓY¸ÑÓªûvë4£6–#Š“ÜžìM¿z-âñv ‡®Æé"à"hïB “âk ÿŒ=/»¬ô˜·Ó(m){%.½¾”ò¥T( Yû[M×Ì¥åtÑ0ÞKr¹0“O›#gô¸¾óR«J Z‰2Z…ÑmðMÌë à’úˆr6À –’TBœtõÂÔªlYõC8¶‹Q¾y%¡y] +àËi)Ì–èÜŽR;¢óhâ°¿8Ø`¼‡¾0 ‚¬ö¢l þ{!Ì,ŸnÌŸ_(lq¥=¡´‡+ Èò þpœapµ“ÑûÍq¥†Ëહ5DÂô–Õ¾QÝ~Ò9û$ãL¯FxÀ‰å0II„ó|)i9Ƃװތ÷¶)cpÕ‡^÷cóÓƒ)¢"—ý¸šÆ8¤¸Þšßç@çÒJ↙"¤-Ìgô.Â!‡ËE¾05;Gƒ—̓·ÄÚQˆ.¯£Ù´œÓ3n‚¯æd¾²§;‰ð¥`›Œà:ku…ùýº“¨X IMÎÝ Í>ø)"—rƒóó$I^ËI-%¶ œ†p;ˆhW×Óa\Qòc!7¥UXO™« (N‡±`”‡ó¥pH‡sÆ„5Œ Õê,@Óh€‚¯ø’Ò|¹<+Aå×8w +Áv9ÁCÛºCFký‹×Öá@1v_¨î"5Ö¡2—ÒËQ —þ´êK„>$´A’­²æüúÈÈbˆ‰àY к•„bÊ`.aÔŒ£F ²7? ¾f¶Ïy{˜æ+Iº%³rnXœ’(ê*¢H/!7WPëZT\C-j!| 'B¨ñ9õR|7œ)®¥-ø‰ËA"Âæc|%D{ë„“jrí´°ý¾Ò¹“ªÐP é~<+¸›vû‚pvb|c ³Wæ÷®8aÂœ/1/7ÁOá0Ë¡r ú²ÒR\€˜ŸäËryþð5àjJÉõNíÎn”£|>Ê‚ŽíÙi”4Óœ·œà–b41¿=@¸æ–"ÂjL^‰ +ÑÌüòAŒsp9WÞ¸K¹CÂ[%]]M*¤ÕE”æ:j€ƒÏoýÊÙü6¦¶}¨ +±1œÑ­J›e\)ì¥ÕNaòœ/ÂÖƨÜZœÃõVŒË­cº?c²ùIsçieã¡èö1ÎMà}¶{ëÐPëHb®÷îM±0Y‰qkQ!„ÚPœÐ­ ¹µŠf¡Y®‰—ãí®×¥“”ºc\?1I°Åé-†Ø4`0P•Ÿ?>–qÿöÞãG’4Ë»¬1¢º23´kifnZk377×Zk÷p-3"#E¤•U•U]U]ÝÕr{¦w°;ƒÙÙ]4ÉHbI€Àà…þ1ä³ìݽÌeÄ×^Ñ™žîæßûÞ{?afácÏg[.ì@‘Üñ“LãÒ}Pï¾T:ets“Géñ³üø9È’•0¿â€>À|Ñv7.UÐÙ‰^9‚²gŒ€çZ”¢Vëƒ+‚àñÀŠ+ l>Jz_€IÊèP˗У4TH·{Þïnèµ8›]p³€·rvÂÚÝ”\ŽQ™\óÓ›” Aï׺k„=fì©]?±Ç˜M*Áî†ÈÜFÒŠ€0Þ7Vû$e¶@y2™çNËï«È­þƒ¸XóN\(&¼/4(áZf(¥ò™ßã bž1KÛvãtpúmfð€/Ìã\Þ—2 ¨ +–ÜQȧû¸4}­Õw“ÞWCè OpÐWénL,lar”Ë+ï—ªH£v7H¯EèÁ€€Pñ.5Ê•ˆX&ížV?$ÁZBäZ„ÊFÈ®Öq­o§ìAºó eö×Q=M]Ý3›çàõò\ÉždU·ëÔv8wÈç&*»Ž€óÊÔ€,0¶ŸÐ­úN÷àùìòCºs MP1ÚÕrÓ\çÄ»Ž)×ÌêÓ:óîQ/ïn"ò§ë±a±é>tMLª¥£tÿYqÛÛ‹±:1Æ] ³1: E¢.Jg6bÒj˜Ç †EÊM¼Û¹B¾{öñ–ÅOd&VR%¤—w´Â >šuú1±“š[Tq Ï‘æ¤0~궒\6ɺVó`püÅÎã_g{—¦õ¸²è'V”Â@ײ¹ÌøðõßÕ¿¡Ý)hõ­¸Ê§G…Þôìõ“ðg›dJiµ/~Õ;ûF+-`žÀ8°lº"Ò!2 R´Ê^ž¼ý{«yÁçgaÐÕR%*”r3¤5ÞA¥ºYYpN3BªAÈ8‘ÅÅ–’Û­o¿à +S?mÛµåøôª5«“²Za± ¤ ²M,,h»ãí +-©ô¨4}W]|eµîcr$PJ((îÀ®.•Ò4È7ð"·µÒi¶÷\)n"zˆt¼_Š^Û+ÔlûiÇ›ŒÖˆ2™µ¿ãQ¡h5Î3ýÇ|n¤ìÕ˜˜àòðX‹IQ¶Àæf™Á£ÁÙ/ê_IÕà ¨Òz;€ÙëQÅïݬ^Bäº^;Y^ÿ¾ºxJt¦—í]8Ý+¹¸ƒÈå-ÂÚ"l>=0Ë;IÞOT®Sf7Ó>w:—I¥ã‡‘>â€ÕÂÔjR.'¤*XBoÑzÓ³B˜‘J²÷ÅÞ—?À_˜.ž±”¦´*íôÕêQvtîÜ2ù˜²•P"D:ˆª1ÊLPÖf„ÛˆŠ˜XÖ + ¥¸÷·•¡6ÒÕó›1™ÏÌÛ'ßèÍS1»d¬¡·&ùQ¥·—dÏü)ÜñÅ#»{]ÝþB,,!ê{A:]YÌ/>0vÓ‡Ÿ˜­”-÷@-Þ¾ÞÞa³²Nmf5¡"j—±'ÕÙ«þÉ·ˆX½³A„0 ʉrPö+.@º1¡L;°z§´Ñ +Qnnøhùä÷õ÷zã̇çÿŸ"cðÿ÷þ¿·Ü´qÈM·Ü´qÈM·Ü´qÈM·Ü´qÈM·Ü´qÈM·Ü´qÈM·Ü´qÈM·Ü´qÈM·Ü´qÈM·Ü´qÈM·Ü´qÈM·Ü´qÈM·Ü´qÈM·Ü´qÈM·Ü´qÈM·Ü´qÈM·Ü´qÈM·Ü´qÈM·Ü´qÈM·Ü´qÈM·Ü´qÈM·Ü´qÈM·Ü´qÈM·Ü´qÈM·Ü´qÈM·Ü´1ø?þ‰ŒÜú?‘ñ_úýÆþDž?iØŸø•4ч¿—†'§ÓãOrŸøûIåøT_ŒOûÃãËužB¾ž,M‡»ë¡?½r^´ž?^Ìûðdy<܆×cðRþûYGבÿk\~§(œJ Š“(‚Ð ¾NÑ8À†¢Hš¢Säúž÷":•HQFb(Jàô?~Ñ®÷"âãs ‡  ^D}|š¢PGñ?½è}ÜÖ‹þÑÇmRÿdÿ“â'Ìz(¼Þ¨rôŸâB½¸>.¹Ø²ú£:…Åøëo€Ø­O’úô|1ž–,u½‘û¤í½Ù[Ê?ýXÿOñxѦÈE®w×1]GÒ{ÿöÞ;¼ýÇwýÇÿ§×S0K”ú8a|Ý{/ò‰ßÿqŽ^îÙu2Å`è'ô‘RªQ&糤^§Ì&¥ÕSr-)Ö s@;#BoabùO[~+¹)ç h»“‹Ö0Þ&˜RÁ•j’uY£&dºZi&¦LfL9#ÜpÙE”Íú’¼/ÎÄP˜ۗŠ ¾¸d²3<Ý‹+¥ •V CÞí¢RYpgZi׬Ÿö•Tf‚i­8W“iLmV¶{Zu7©5ÖÕÛ¯Y®ÀäC¸…ðEÑYõC¡0#ÓÝ0›AäRBÊf%@›¸QÓëfû"=|*T­òaÖ5ªû™Á•Ñ>ÕëGéξ°Ä¬NˆuI³Òêq>ï'L?¢$\J¯z͇*ðY˜\1Ù0“‹ò…”Þ„‰áZ±:¸Ö@•f¦yߨ#j;Èd·R¦·¢¬«ä§Bf¦¬0arVOÈŒH­!Ý f&Ø,ou²­øD„Ï3F'€k>BfbL.Îäbl²¤=Ä^JïlbfpâlʨP¦.*ÎäSr#Bg}˜+çŠQ&¥]Þî‹ÎËq¶èÃÓ÷"üJ„ßL*þ¤‚rYB*‘°†d:ˆiIÆÙŒqwƒÄ&"‡HÞ¡s)¹‰‰ÕW ¦,Èa´blv3ÆGÛ›q9Á­“’[”ÞcÓ#.ݯ /ìú"LhI¡Ì9c¥¼à³Ãa…øˆ<&Ãr µ¤²Aܦô–VÜV²c”ÍéÅ9eµ¢|.HÙI¡È¤G¬3&•Šší0Å*”·)ê.ë.pkDØc\o{yËå0®£RQ*nÛÝs¥ºÏç!½ˆiªîÜÿ¶8ºŸ I¡ æ—™þ3£qI˜ý˜PŠxÛef¢´¦ì0åmØ*5ø§¸P‰óeÊÜLz{ðEè´·'‘ÙHˆ%Tmˆùítçª<~Ò?z›Tó.ÍgÇzó4;yn´/ˆÌ„°´ÙÝÜ}“ËAÒ– +;Åék§÷„HOˆô(!–a%ãlá pðÛĈÞö—Œ ‰X ›qÉÛVmÀj£F+.UR-!Ö’JÕjÁ;Ö Ìx[dž©L˜r‚¸·Y”J§¤b‚säÄŒ(žŽÁ?a&Ê•óc§vuµæüIu#*¬™fÄ¡ŠÔ6mQ¥‹ˆMÞž‹Î,2ãL6Éqµ 5!ü:m •ÂNB(Ü S? àP™¬Ù•Ý©äL\î^ˆ F”ô¾ÉüŽ?µaPÆ%µetSBi-ÂÁ4’\~+)û1 V Bç7;LºP¥ ¹Ff¦I±ÂÔ(ioÀ"ÄD„+•#«vjTN«ïT¿û›áñ«?[‹ +¹™·»âðå ‚„ý±‰ +Q:ëÇ ï;±‰4´€”Ÿˆî—J´T¼“ +ãe¯#2!×ÒífëW*˜˜ êÇ}‘&˜ÒHHÕ˜Xí¡\ر›'¼3L0ŽìœÎ‰TžáVƒ°º_&3Ñ*»—ji ”ÖPËûrõˆ°†q® HK„…(ã¬'ªB¸aJ3!Ô ­‡JuBõ¾;=H˜X jLÈ%•ªV=¬.¿lì}QÚ~Wò„^• +¡4ãJs<=ÔêgåÉs·sÞœ=qÚGaoK‚\ØìârsTïâöó[a­ž·wjRÝBõ-Ô@eosPÊ\ 1¾¤ŠŠe¨d™Þ¢Ý¨ÔH*=LH•cD¯o¢ +¬³_IgÑïE¥ ‘†ÌFH+Æd¦àgœÍF‰´?.oFÅ­¸ °#3²3dÍÎzT€¢‚çWCìz„_‹r!ÒA•Ÿ]Ðé9cO•ü¾œ]F¨,©6¼½ä„r‚¯àZS»Bni7ïcjýNˆ\ ƒÀ8¸Úˆ(€iGñ¨ìqLJ9I»¸ÒAäV”.Ĩ©õ«XÁõͤ´ž4|© &Õ XHhRë'„"&ä“\n5Ì„0åKŒÑ…àr“Ñ;*tèøRüçë16Ó3k{P`qÀ=¹*I©êO€üá뀩µ„\Žz{Š•gÐ[>p6Då +ííW2IÊõÍ”áÇMoã!w¤gÞ®é¦7RfQ„ÞIðÅ0nZ•eqòP)Ïê\,M «ÍeÇbq~âÀ¼rÍm‰…ñ*û0á+|fšR[¨ ÍX vˆv"@¤é'ÒaºH›#!»`l`íNR,ãj‹±Ûlf@§½/ÏקbeÑzí@¯î°î@.ŒI·›î^v÷?§Ï1­ Ò„¸QWb¦KÙm·÷Än^xû‚¥ûÞ©Àx&àÇŒ D x›\cl“ª)°^T¬$”6ªöi{®×¯@ ļ]°øü—²6’ÚfÒÀ•æ}9|&Îe¡8“ôÖÄ^saÌ a†?!oÄÄ›OIµ`ÊÞˆ+[I=ˆY#wüäVR‰²yÚꨌ?•¤ÒA8¡—ðö–jAÙé ü+"×Ik ¨îm0 +;B»P1*“ÊWX ±~D÷Åå{~*N—`¡aú"TžV›Vq®åFPÒ›IÍÙ>TiTi±îž^=’ ó0åáÞV\܈ñ+|5ÄR¥Ø>²³¤Úò{qñJ|¦Œ%Ÿ[Âó ©ŽHUÁ±F=Je0­I;¹¸§—ŒÕBøœêŽË“g ¡¡›#Œ¦·9è£Í%åævó0!—V“rŒƒƒw9{È,f€nmÿó;}ð!¦bbŽH÷Ù,Û¡Ó:Ïö.(ÛÛ¸Z,JçP¹Å8ÛbñX, ¹].;‡Þaµš”Ÿ™–JÛViùüÍ_)ùnÕ"JQ[\z$eÆŒÙñTJÊ„tÞ¦œ¥0‡G„xÉ#ЭRõN÷!N.ZKjøP+B\UÎL8§{7Æo&ô(SaÝ]©x æw™ô8)Õ AQ@j| q#Ên%„nàrÖÚ´ÖBØb˜pB¸ ÌâOÊ2 Ü·™P×b²µüˆ ¥£³!2½‘W£ì§ɵ(ïKê¡ÐhÊÕÛ¥šI–]‰ó›˜ +Óƒ¢ÜmÊ짔&,;t4¨eXü0éά†Ù͘¼S"¸›âª!$‘drkæ3ê^Úò6‚F¼í öèl5€‰’bê–q&P*¤ÞM‰ ±ókaú®/%à]Å D^IÈ+q•ÒÚµÑõV0³š°"7yw™n]:ÍKW¬ZÈÖvã€0;)­‰€h׫JvšÓí?‘®î°V+¸p”¿5†pû­æé(é(Å9d™PRJi'Ý>©Íž<|û×Íýg ¥D;}.¿Íå`˜ÌH,.q³ú €(B¹À)raꋶF<(|o®Eoûecù:ÈÂL1¥µ¹ô@pÇ,4‚;±ªû¥Þ…”›Ä„¡7 ÝÛÁÓ¨Ÿfú…Âܨí]¼ü­Ý=ZM* pè&wŠ* ÊàÏ[ˆšd\oÇg8OÚÈM\뀰Dl%¥­„@- ¤ÞØÌ´î{»Ç%¨“ð^õv-¦Ö{¨n©cŠkqq%LmÆùªDH—«ˆ½ÃhmÞƘ`TŒU™Ýˆ«~Ì‚HÙJ¨[I5F»ÜÚˆ «2Jf| }5,®ÇdÐ Œ5„”/ex›óJÕ0íÂœ ­ ^Ê Þž’«”Ö +¤l?¢ù…*Ì?B¸ º¶J ©KÞ ’Ak@!8²÷æ&ÒP?„Üz’óÛ >òSê)Ö¶‰Å¢A­Â$1¡”`ÝÕ¸=˜Rë71Ïn&DÏHJUÈçÌ3Ý'…ÉK¹°¤µº`Ô:³GåÉÐiv(«#d‡fe‘ëž ù È)3o8«z¨WNôÊ©T8ËG)£ €Æª\ï4@Y Æqµ•ë?®Î_4öÞÔö^1…˜ÍÊô©V? +ñ¥¸\Kém½vd·/Á¬qî ¦ÄgÞ&t ÐšzõÀnž×f/A‡ù:ªõPZ6;´ E¸-æf)­ì ËØ=ý¨\5j‡µÅkµ~(Wv2½³„TÞÂLT®AùÙLi!ʵÀ˜}oçhÂYIŒR³*;¨R]OˆÀM˜Ò¢ÍAœÈmÀJ“˜• A?—Rj‡ú(Ì@áãPTTf%*®'Ô •€¿F©BŒ®l!ÞB |Ö£<Èx?kB®KéQ¦~ÂZ£™ ÷Q9Lªcb Tk‚Ê€È߈‹@š±ê>"„™_ + v³Ã8Ð"Xc0‰• ÷šBÇ”2i¶ÄÜ„qúŒ;BŒvB­ëµÃÆÎ[­~ö +·)£#äçJåP(îqÙØmx}˜q¡º B¢l–s§rù˜Íí¢Ö(eAoûp+!”˜tS«`–Aë&…šR8ÀäJ78§ƒƒ“+ˆÖ@ôx^p¬J~™nœ†Y7D§c|.Æçá¹[Khˆ ų´ˆA†hH×›`ºƒI#˜Ô´ ÙY ’ALÇ•¶Û眭÷p±á󺘳aÏxЈÒÀ MÀ–ͤ ~*¬¬9kuÁŒ 7D8`h£GÂ$ù¼ 7ã´ó™]rPrIpµ”à@'ä“LA°ºÜøt#±å|I@Á=œLNfüˆ¼á6ÀbÄe·XÊw\h,߯D¹¿Ø@Ö#\Š¯H –V‚µ¼É$ä0a­„R±Øâk`~Þ¾“JœÏ‚Õòc +8}€SÑ)îDp‡ÖkÞ Fc “B†PK¼Ýâ3}£~$äg °\¢b ô|\,ázǨ™µ6Ý÷æ&*'¥bR.…Ø 8pp ‹2ýk6;KE½²§Õ½*ârtzdµf'/¹òž·u,ãmH(e?m³îL®ž •3*GoGØ\ˆÊZ*å[ñöG»Äƒ…¬m&T(‹Ù ÓzœgÒ£u.•÷¡;P°K2Ô¿-¸#>?0îJ\„êôƒîƒµ"Ô*U  x!ð{àâ}$ìvŒL§øô#d²$LXÒ` +Z»ŒÕ]L[ sžð¦3*ý§méÖ +Øy(ªÕˆ°™>2]9^‰ÍyKkP ŒÿVBŠÓé0alÆXN+K™>0µ5·c5"ÐÂørÓ~²E—”«a°öxz +QC)ƒÖ»R~7H¦ï†©-T%Õ:`Ž·(›[ 2ëQѹ[ >DËòÅ@RY ÒzhSVs3Þµ˜p7H}êÃ<ü¤Ò˜\fíΆw®/Cë•*®‘|5€†S£·eÀ ÆŽ¢¢–éx¼€®–§÷䲕¬Mª?n¿CøaÒ†éÁxgH˜­0® ÙªV¢Œ ²‰i~‘æt®øì «zm'ÈX°àAÜÛ=6ˆ˜T4ª»ji)º³}ˆÛ”Þ¢ÒøX ±ù8 ^÷*3x˜”«¤Þ]ÎŽr§Q±dsÀ’FíÄlŸ’F t“Ÿhg ³Ö ,¤Ã¬ìƒÐå/dúAÊY«>ÔC£sFi/Ó:#­vŒ°rÝ®0ØÀÄ LÛ"L8"W*ã§¥í·¸3{eTw’R!D€ð  •)¹ž—g÷×bl•P>·&W!Y1P.ÁƒÞ³76ƹÀGQÊRÀköVÂØJ‚¬mPj#˜Òà µ C¸ PpÏsOYÆHÒv”sÁöB='BQàgJȇS:!äœò$L[‹éz°ÄDf#xŠp=L“R9A¥ýIaKQ*gò¨P_}˜œRŠ”šg ‘¡þ}I*Jº ÿ7âœãÌVwg+µòq¿c`FÀ(Ù\¼úC\ÈßÙB·b¨D÷[ˆTèÃm°‡…öq¡³·âOcÉÛ¤K²€ŠP-é(处bipŽ%L(c|1JB:2›I!ÉÚr¦Ês=*+Ný‡ Íi£éý”+µÙ#ÒlÜ ± iAgH³›í]àQ2ãùåO×PÁ*—O‚y;­·¡´Äì¨Ø<˜^|Íd‡ˆ$lð€¸ÒÔÊÍÝÙÑ5¢zªI­íG„|ˆqAø‘v_*îdúôÖiº´}üøgZ}ùi€ &(^ðqPŠbvA=¨âð*"T@ƒwÊ\ÎÎäü˜”+­å³”Yút+1&å +@(eµš‹õÝ·Lv¬fÝÝg~RûÉVâ®?•’Ê0à=¿Â‚6II`Ý;~t-Bû“bü£/†jG$¨(]-ÍB ¨,«PbiL¬óö¬¸i£ âK}¶…Þ À#\Àn k ­ã²ÎYOzxÂtp[€ÐéqÊQ2=»:[Gå§ÅŠ`—À»y^'´fbYuGP¸P`Ac0.p´Q="ó>"[쥔ò½Üí€pN ©@ÎŽ"Äœ`7à«Ê˜°áÈ[ m ÑA§ Þn¥”­¢3[q~%H{z|ŠgBÅ®(ùÑzÇ(Ò1\»¤¶’®TÅÌl;ýq“\Ï#*(LРó ¥ÂÙm€ô˜r*ó°°À›¸X¶J; ê ½ Ÿd3q& =¢ÒˆTç+»C(?eà0f—PëàYRJ‹I!Wjíï_ÿB*Îý`ó¹1=RŠK§{N¦=Ìñ¶Ît× ÐªNãŠ-.z{’v¯5¹zýý¿ªN¬FyJ­ëBß@TÒ˜ÈÅC1·4Úq½§‹’3BßÁçásA¥ƒvRK‹ e®„i@!B­kEYUˈZAåR½sü‹°ö?†p³2Ǥ’ÕV£âÇ3¥­äm2 \“`31ÊJr¹ зß9Fnzòê·>{/Â%¸<Û$[HrE ¿`Ê2K³ÝËok³ÇŸnaw}èf”NÀÜ mc8Á„P†úLˆEÐu £xœÚF„†JHP®”êù)eÖ×pònˆñƒ§ÖYµ.Ù=x&äíâØs„Ó&Däé€ecÌŽ]Ûa m¾| !T}q |"¸E`4ÉnÈN°È‹4D­ø‰Í˜…)Ç[ˆr'˜¢äšâŒ«ÿü^hņP5%µ µ( ¯Y3aDõƒu ’ka&Á€Ì}¬m5!T¡%冀𯆠Eœ8WH\z¼ ¼œÞE¸,o¶3õ#.?CÕZœËŽ…5!¢¤‡¢¢Í¶Ñ˜µnR,i¥…ÑØǵ®Ö(³…)@vu½8%¬ÖFÊÚDL@N°óÀ¶@š[˜’R*´Ù„uæó³¸\Ié0ãl@©K¥¸È؃‡bu?©7$Àë™qLÝÄÔ”Þ¡žÒ:´3O(m­|¤•ö@\p-Æf"¤ ëL9!ÜŸ ЗRï…‰ÏÈj˜@až•»ul×ænséKÊ@.œ ]ƒß a[I –BrçŒ="Í^„É‚ûãC¥øÇK¨´ ΨoUÁÕFÊS\ÐPpØõ±¦Ö#4$‚Mw¥âŒ²zaÚ’f àÀ®H0€èë•‹0Rhå3D©¯Ç„ ¦UA‚‚8LÛ Óy™­ ÀHzœÍù“òjxYH¤Œ0ªc\>]^øQÈ…‡ÃÒé€gël¨íÅä*tÊåïl&6Ã\’ÌD oOä5ŽQn+kD I£L× l#»æà2 ÷.|T÷’ÐP´y×­GصˆgœÃ¤LŽ·ÔðÇÙèÝ @Æ×C¬÷éd ã½ ½—6¼3«‘§ ¤Ò­ÆRóÅ ´òa¸ܱã+ +\ãNX«¥A»fy§°:&”cBQ»fí";|f´ï#R™PkaÚ†ùQä£\ X{3!c˜Nˆ²h£Î™-6=2gjå0©ÔÖ’JÊhãFðúZ¨œkk±z ÿe ˆ\G5ˆºè#<”ÖÎõžˆ…Dj„ÁD£*,)Ìm5Hy§¸¹ +eŽ(s˜üx‚‚ýèÁ5D*V3!Q»šI¾ +`!¼fLr lò!RcHÛ#.·@•fÌ»R n«DèÞ©<ˆ\¡´éô“)©ý|BÄ­¨—ø*Tí‚ž„•I© ï-ª¬{9å7"ÜZˆÝL¨!:—”š¸Ö‹²•[I©]ÆèE) xÖ +¤8Ø^°Û˜ÒŒ29\®ËÙíHý‹P™$nãR5h­—à+ ¶!u¯`d€T¬¢Rƒ4ºjn[vF¾¸Ål\þ˵øŸÝ Þña€QÀÎw|8ÝêÇK–I&¿W"à5ò?Þć>Ä +¥l«4ç¬6tF»€¨ Ÿ@nDx¨ ŒÜÌ”ŽïúÉ{~r3*|æ'þrIJY~T‡lÅ%P…è‚).É—ÐB¨Ëµ¦Ï‘¦µ®š_$ùL€ÑNc/!æ¼ïðÆô\)Ÿ:íÇ|~ ©‡N·ª{4L ÔU)­!8CÁSzDþfBa”f¦~èñŒdåânqúÒhœ%ä¸?©´G;?f†[k>+'I­É”†TÚWë§PŠ)©È¹}Ì»¾6‹ryfĹ8DÆê=AAïŹR˜.„À»‘wN’? õ6€-¦‚c-z§ìŒqœ¯E¨ÌfŒ%5Þhcrð-„lfH;_ûñÆ9·TJûa9¸w50Juð†àL}¨µ—Q°®)í^ãïí-ؤ +KõÎ甀+ý¨ug ‡WÂ0<Êp—MÏôò©RØ#µkt!¿¡ÀŸnazJ®`bÖë’;‹¤Œ$i2Ju-HÜÛ¶""£y÷Q€Õ‚Ò°ˆ8“ÛôÎæq±§ò[rú³•ðŸß mFX(ŒŸl`±š\€Æó®`‚ÛJêq¶¸“>óÓ¨XOp¥WC…†QÙÙƒŠÕ2£ý̇ßÙ PÃ)a +)¡±%À(0ÀŒwTÑÕ´ÞF\û e¶•³ üh™z›J3ù8[X‹Ë À@–ý° u%Ä5‡rqéÇáÅ:®µ­cžïEÙµ¤ †×¨î‘F3„[q.e<† +ƒyD4¸ŒÞ‘Òc>3\Ó1ÆB•i·S–w©ˆµûrnÆå¶ã¢wI%É烤±‘7“H¬8W$íj âle8zÒÛ{é#­-T÷ÎÄ**7H«¿•³@%;NY˜”Á¶õñB<øS«²GÙíÏ¢L˜Îr΀³»€ÿA2í&³Q:8Ð&)å+„“jLz"æ¶óƒÇJãhijqμö½ýY¸a¸ þŒv •F„4àè„îKj Ö±lÔöÛ×¥ñ%ØUJ‚ÉÁxðÑ9F©QB‘Ë‚Õ^…E@´“ Þua¨Áˆécµÿt2j+©`r-Áç¡òÁF­¡h°K€T°bŠ;´ëq)˜‰¦Aø­Ç¨M< ¹ð¬YBÙˆŠŸ®#?YOnFø(ébÖŒSé0¦±zc-ÆÜõ#›QÆ+€”DÔ¢…@i'¤ +º ÿt5ê‹ ðd³®Äš0’¤Æ¨uÞ¬dgÏœÂG¬½ôy7ÕD`CPà?Y ßÝ@ DI>‹ó¹8Sˆ‚‘dJbf¼îFø­”a½S1/÷Ì„ì4Æ•€JÀV€øÿó•Pmœ°¢)c-ÌûŠ/Y–îøSÐ}¨TBøX]èq¾¢LVˆI{jD­G™¬·ì´Y ¼AÝyàWHÏN'áíbq7×€úq‹]AÚëqœ¾^œÛÍ#­²*!Ò3ö3„+QÆ‚\« +æÐ*àzs‹0׊S?¦ èkšÝçf$ƒ‰°3„R3.ØÆ”ÞéŽHU ]0’>Ü à6üu@Bx\KkQvÔ#f3fŸ±`j AT‘ ‹¸ÞRÊ )?äeå>óak:ɸ4˜S@(B*z8–@)ÁÀ¼3F –ë´Ñ§ä†gIp:DÄVLÚŒI¾¤­f<×Ø«Í}âÖ+Ër„t×"ðÔfÂÜB¬ 묆ùõ·d×Ãœ/.¯Ù{ÆW£¸Í™m¨XÜqïn:ù®/µÂ–ÚÜJÊ+aÆŸ”|Q„úÝÍ$¨8ðòP«‘TÚ­ãrî/VýŸ®„‚0½·•6£2´³ÑA\Gût5´¢Àø½ èÊ—Û{R `üNˆßD½{«|½Ãjˆ z«v¯â]BM@?œ+ÆRÆMÌ»µ Ê›t¼û»¨euØ̀Л ’BS[LfÄ8ƒ8ô¬Ó‹3>?ña&<€bIRJÞÕ(ÔôzÞÅÂqœ ’ç ð¯|fDhÞåKÖhÊnOÊ „ÌÓڔѦô¦àô1 nѨìÒfl¡66SÆF +4s¡½ý’±ZqÚNòÙuTõºµBTW›œÓÙÂÓZ~"s/¦¬ÆPòD©z£ íÕ&ã µú“~7o‚eBkáZ‘›˜Ò `<ò˜·2y^™¼à3 Bï.AãÀ ¡JŒö øZLŽ1YÄ»ÀÚ-` ‰JÐVAT;cËœ=‘œ Ø +@uØ=è€3…¡ÁjZ]ÊŽ„Üä³°–Š0m#ÌEÉlŒ.GèrRön5 ‘nSîú©»>ˆì㔸*¦]ƒrNoF0y+ÊÇÈ,hàÕz8Fq¹â‹°vÇÿéjd=Ä )ç û„Ú  ëâkŒäs î@fƒÈ·â)«”å]± +Ó²w=1Âß’Þý«€KƒÊÃÃO8€¶ûÉ*²š¦øñ­(L¨@J©a|Ê +/äùÍ „ç+|vj6ÕòR)Îôú.au¨ô€ÍNáy½¶oµÏÒ½ˆÖ‰KÕ¤T‰²ypžk“kQ¾˜Ò¡ssQÀ7‡MÞfT1ä—Ïaͳ)g‡jiAÛCÆjô³˜4§ÏJƒ+)7Nx^¬X„ÈUÖƒH/FmÆé|’ +ô>¸ò¤\ƒšçós.7C€ãˆÌÏð¡í.¦5 säºRÞw:÷SV J=ðñ‚àÞZRƒj—Çåöü¸³Í›c¬Ëç&LØèlaiÀƤX%Í—@'s9€bp7aÂŽ3YLª‹î6Ä÷î¹Ê"L†·¡Sº˜ÜJ©í +%°Ÿ¦RêLJz÷@³CuáZ3 ¦ò(ÞÚˆñ)©œðÊ8ã+2M'dg ZC„ ‚ÄŸ2ÖcÞ…ìµ(¥…‹5”¯ÐÐ2JÝã=Ê (B¨.ïÒ’Ô@Ä8qÀ:_\Šâæz˜VnØ[" `çr´\Vܾ/)™1Fu +à㠥И]9ê]u* é6x+?è6$Œ>¦u¢²PX ³›ÀUꎟô¡zÄ»{6Gê-­¼gÖö¬ÚçNb°„Äôͤbòàjé̘ÏM ãûzsÜ^Ûe³J¢8sºçÅíçf÷‚uǘRA¹ŒUߥ! ¡ƒÒ¨ ¹±”ŸÐNW­ÀÊ7ÄÜØlpÙ”|–Þ8Ì .3ݵºKØ}\ïè¥öŒ øÓ‡œrîPÌŠÃKÖÄĦw¨ÌŒ+슥=6·]à;!;¶[‡¨Z1š'Vç’ÊŒ“JvF$ªÕò“„RJÊ¥¨Ç´*¼LÈ •òÒh«Õ¨UÚèdÕ"lQš€T>Ü6ØCD®ø +Î#L0Ü ÊìÂ1i«‡«uàë““m4Ï“R‡ Í®QÙQKÛ  I£M§{q¹êgrXS*»RiÕê`"€h8l·º¸Ý‹sµºC¦{„ À[Iiå„X@ä2¬¡\\Ðé!L#¥7“b TbŒq£„¤Üí0Ðkfl/¡·`åSj‘jA*‚Ï¥2ðTª±V_Ïoƒ‰»æ¡õ ä¢TÜ"guõò<ÝÙ7ê µ2®ï<Ök{àè£\‰²tz‚àqºéÒÄ®l§ŒfD(ð¹1ÔÏzR‡(  µ .CR¦Z$…,¥W8§KY}ðqÐÑRa_ÈÎáÒ¨n$%æã ù”— ©Õ9ä +3­¾o·„âD, §%—Æ…éƒüü‘Tž‹ù‘š´÷ž‹…)¦Ö „ìf¨T F|aÌ»ÃBÿ¬¾|•–kPfãêÊ鞤{çtv›Él³é®0¹K\fdÔwõÚŽ\^$ÔšNaN;=*ÝØJ»ZëÜhŸÙûjí@ÎŽÚ{oŒÖiËSvßj[íûjýX©îVUÊ1>í³ŒÙÈõÏJÓ‡nï,׿˜JÎC° &Áq¤”jˆt_Þ¶;§é.„s*•—Ùáýt{ª‹M7òƒÃòìAº}J)µá6š`–•ü“‹þ8l”½TXð¹à|ºZYz¿bÔ¡>R)¥5ÕÒ²6¿®Ì¯©Ì ºº3¢LWÊjižŸ=(m?é½^\ÿ¬à½lȸÂì)  ›™•Æì:7º’Ê;@:at¦ñyÊj:¢l€‚~ºu˜ž§Û‡jqe` +.;´Oz·¨u´ê®ÙÚ硼«Û\ûÖ’J3¡¼”«{fë¨8½ª,+å‘R¤;{f{ßhï§ûÇm§w,C-åÇlv€ªU*Ýf]O’Y­£ÂàÁøô«ñù—¹Á™VÛåòS.;Ô«ÛFm™]YýÇåÅ;µ¶)E> MW”SËs c£}JççTv–\ëÕ¥<5€¨õÖ©\;Ë{bq'Û>Ÿ}©Ôv}´‰Ö3›'bqá­¶;ònZvú€q1¦ B-hå‰YŸgzG…éÃt÷”ÍOp:e;FX©“†GÙ¸Þ}ç LºïÉH¹ö¬<¨Ïtï§Or“«Üè°\œÛƒJ™-Úí+Õ¥RÝ)LAuA²äòªl_·N>oŸ~^˜]éµ¥Ó\O^ŒÎÞ6vŸf‹Ë ÄÒ6_˜ÈÕ¥^] +n¿Ô=I)5T®ÊŹTÜòs§{ÖÜ{Y^\Û̓ÆâaºµåUkZe™Ü/ÌŸ¶¯iw”*fuÕ+>T$ôŠÝÜ×ëËÂäaÿüÃüéÙéãâèþÞãoõênŒ¯+Õc­~¬× ‰Wí£/ìî¦57“2`&)—”ÂÈœ™íc§Ñ>ú¼¼|µiïw¶¯håÞ/RjKÈn›íûÙé³Úî;µºW^V eµ"r£ëæÞç{Ï~wñî_î^}½÷ä»s V·‹³«Âü¡Õ=*͌ο޾þÑîé6aÖY§e6vÆA}çEkÿ•Ó;ÙöËÖá+2×'ŽQ[8íýâü±9¸r&OäÖ‘R߃¿¢J>Hjð^–êþ»ÊáûòÁ»Êþ;wp9:yçö´ê$;¾4Û§FëÄ§Wã·ÅÙ#(ºŸÁô*4‘RšƒÛ“òöóúî&Ê¡­W&¨’eÒ5¥4É.«;Ï*Ëgí½×ãÓ/™tŽ •€·Zu/Ý>ƒò¨-^6öÞåFOØÌ8@Ø€ÀòNç$7y`¶´ú®Ñ<œ‘*ÛD¦/V wôÖQ~òV¸}ôN.Lq9Oë•—C[ƒtó¤ú¾yø²¾û¢}òeyþèèé÷àTÚkÃêîóÙãï÷žÿâàÅ/—_¤›K­<+M¯˜ÌäŸw¹¼~08û¦}𦲸Ÿ€ÆóY.ÛµÛǵųÁùýó÷ƒ—çï~Ó?~‹œ2j|vU—]5ŽÞ·Ÿfû÷ۇ +i·¹Ýn %ó™vmq=úËùõgo~óýßý;£¶¯ÕN†—?/ΞÑ΄uçjýÍÍ1½沔ݎ± B§½ãöOò“Çåíg¼¸Õ©M.ÿð_ÿ{»{æÃu»¶|™n_šÅågð˜ýøúçDº¦ÅSVgõ×½Ï{§š{/._ÿæá7+•· ÃËÉåO·7ôÓþé»Ùã–/~=;ûêçÿâ©,žÐéV®{P_>‡@ºÇŸ.¾ž]ÿ¼wôj÷ê‹£·¿‘òƒÑÉËåÓ*»¯ôÎiïüÃòåïkÇ_[íüp_­ŽSzE*/ŒÎ©;y”?¨ï½h¼Nw÷»GOŠ‹‡z}Çj°¹ÝÞÏtœÎñøþÏʳg¤]‹ƒÜàD*•Òp`BεöÞµÞ:í…Û^j•yº{\š?*L®˧ýÃçço~µûø;@K¹0†ÄõŽÞN/>tß4v_•æÏÔÊÞüä‹ùƒŸ¢JJJ¢uð¶¾ó¼82}ðÝÎÃ^ÿìïûGo½i¶OìÞ”=ô‚Ñyà _¶¾QóSÌHºWŸ[ƒüä"Ý=Ê N[;Ï´ê°t|qtÖÜ}Zž>0*S.Ý6ì|i6öŠã‹òì¡TœÇ—µëÌðœÎÍ­ÎQûàiev)•åc»µSŸNßÂâ»ý£ÖÞõW¿ýcir ë0½ÿÕâéÝóÕçùÑ…\Ú}ÛÞ~vþú¯ªÓ Lv¬ê¤³ ±|5ôõàò›ùõ/Á¬¥[çàa·P LDºÿpôàƒóï +ó…åËÂô‘Û;nÌtv®‚ò`¦ šˆœ³ÜðR®,µÂ ·{]žcjSk»Ók{à1ovpâœmQf6+:ä¢}° sk¾¯|t™iÃâdz;µÅEy~•ßTY=û›?þï°>¨ZÌvög~ 郠ðÜÉeçñ¯ÿoñ¯ÿ7ÆiO.¾\<ýÕðÁ÷пӇ?ÌýÜnŸ¼üò÷?ûÿ}º·ç#M¥qQÛy>yðõñëßvß.Ïßþõ¿úë{O¡ºû/jËëúÞËÃç¿zúíߟ}ù¯sÃ'Û‡/v/¿’ +cµâq±Ý>-Í®çßì¿þ볯þ¾{òå铯w/ÞŠùd-Ó?5{fs¿4¾8|õûƒwÿ¼0¾rA-T¶ÙLW-ÏäÒV,eö1£W>ê|Nš ›V’xxe4÷­ÖÞààÕ7¿ýãôüãöºgß5¿¬,žMý :Eë<eÈ;ý|÷>t:¡•óÝÃÒôrçÉw/~]|+»¼;î¾úòWÿ”ë‡Ã“wƒÓ¯ÔÆ0>yXœ>rºÇÙÞI¦sçrQß)ÏžP,~ûô»¿ž|~öøëÓ§?UŠCx{mùj~úà›GþðáŸý±6}prõþÛßÿ[(Фݳ꺲ø¼sú}÷ô›LëàìñÏÞÿXhÏi³¬Õv´Ö™Ý{=Ú~ò;wx•Òjvë ?õ¯W {œá¬[eúàó_ýÃÙ«’rð“Iª»oJËfë´º^\\¿ûÝw¿û£SÛ•ó“ÒâI~ûiiùj|õÈ!ÀÜ6ϧD׬.2ÝÓÖî‹þýoj_¸ã'¬Ù™¾µj“„hHùvyvÕÙ{18|Þ?x<;j?@iFeÔ\¾l̟‹Íe{ùèúË¿:|õ#iÔ;×@ùéÃÞù×åÝ—vïÜjì=ÿÛ·¿ø[³1ß" ½ºŸéŸYíƒÜä~ÿì}uñxÿòýû?ÿŸ£Ó·Fi|ðø»Ë¯þåüù¯÷žÿêþû?ÌŸüZ¯ì½üü7o¾ý;`7Ðð¡ÕíëæîóáÅ×íãϵÚòÉ»_<úŠËöªÛëû/³£¹Ñƒöþ›ƒ—¿\þéž/½;ÇëI”<©´dó & &î°:{XìwW”U7jÛip¸õ]µºÌ.g_€òyÙ=ÿˆ’w;FcÉä§>®’jbi§4¹NH…”R,ôŽìÆÎðôõôòËüì!›Ÿ3ö ={òùÿƬO«³ûó‡?í¿O.´ÊL¯Î©LŸÉŽªóël÷׊Zyh5wÓÐ5­C§µW^™µåîÙ›½Ë÷raÔ;x>»úzrña|ÿ‹ÙýwÃëlcùã?û‡ÿéßÿßGψ ÚIaü¨´ýº8‘îž³VûËïÿæoÿøïãc>Ók¾o~Ù>þ0¼üÅøá¯ô<9X^~]Û~¦(“‡XÛ$2:ûâÃþàn·³×Z{{ôòW•Ý§tvà'­¡“F Á®/JãGååËÎñ›íGß|þûtÿTÌöZžÕ÷¼ýÃòúÇÆÞËÆòº»ÿ2ÝÞGÔb\È™ÍÊnÒv#7ºeœ;¹øbxöZolSfÁm/J“ûµÝgã÷ƒ·jóˆ±Z…ö.ÀxðÔnë@ÎtI¹èÔ–Ýã7̓—Nä5ðë†'V{_*qÛV*Û¤U 9\.]¿ýÝ9Zñ¤V— Ð9èœÌˆ°:@ñùî^gçq\È4öžý¦{úm¦ÿxzþ]ëä½ÖÜk/¯Ÿÿð\núø›Þ/}”váuãôçÙáÃ0ã0v£±ý<djpþÕäÁwGOsøäÇæòUiúØ( ¾ýuÿài”Ï€”?eµ/Ó½ËÚî›tïTp›^|ן1V[)Áª>pzzýÀé^Ô–/óíÃåå‡ +˜Ê"w»)­b•çíÅÃÞî.Ó!u  FJ.BßwAXž=* ÎËý³BsÖj|ô¦¾÷Jo‚We2cD®‚£Ñʻˇ?]D. ù©ÑØï¿ï¿ƒÇàèspÙÕÑùó¯ÿÀ›U˜RyçUzpÿîëß/W™\±vçÍw³¼ÿ&æÒºÓy æÀ~Vb¢›sÀ gxóba@]œ_×v_©µÝ(›Åµš’äZ‹ ¡$ùŒX˜ +¥%x +ζ£U—aÒ •¬Ó˜dz»™ÞÈ°Ezc¿4yàvÍÒ°7»8xú}~r_,ŽÅâD«,ìæ‘Q?Î ® !MPœõÙUûàUûàeëèP]`fÍ®m—Fg˜š«ô÷.?ÿë³Ïÿ¦yðΨíz ¦¦Œ‡Âæ¶âBœÔ¥LÇíì;½C³sàÏðtƒÏvÆç_A6…Ü 8ºè98{ßÜybÕf|¦åœ`ʈ3?Êq Jtpô¶µÿ¶súSg|•î£r֬Λ‹gNûœÎŒ!Gx»ÖÛÚ=|—ó¨\ȶO¡Ô ­"”fLi ]Èêªï b¾µ|±|üciòäÊèèunp‘”Knyúøí¯òíEˆ±ÜуÊΛþùw³ËŸ;Ýû\~N¦;fiòôí¯+½CB©Ôf—~ÚÛ]š<H_ìO?‡ŽÈµžýfxþ]¾i×öíƘŸB¤UPeùnˆP9»™ëŸf:Ç£ãw‹G?“ó³Hî\ç‡ßý›LuJ%Þhå¹VÝûpxúayõ³$›©õ—¾‹SDÊ`¢CÌ»X¤Ô:­TQ.³õ 08˜qTt¥äÝõˆ_¨•}?füd OÉU«~´g}¨ã2 Qk;¤Õ„õ¬O.!YRaZÝ~ ™œ8}õ»£ç¿–‹½Îøàêí/«³‹¸Xð.ªuÎåûÅÑ5L Á„L?¥b”.g;àA¶ÿ0:ùìÕÙ›>¿ø®Ü?>yôíøô ©8«0!0µbW¥Ì(ÎX1ÖâÓu£:…™[½û¸3z²šû*´[q‚ëe€ßîÞÛ"‡l_ÎažÒ ä¢Q˜âÌF8å¡Dû¨¾ý´½û¢òÛjRF£»ûr|ö®U™tÓlìA€ÌÓ +C1ÓDD r®,åG\¦Ç¤{|vâöî·w^NÎ>(ù "di­šmî%y'%å ë3SP•vëTvÇ„XâÓ-HížšXÍãþÑûåßϯ~¨/ŸÅ…lWÖŠ³FJ΂)O®Ê½#«8¶«3¢rNÏ醧jqÊÙ].Ý“2ƒçnï>zö᨜[  1\fÕRº²pû„\cššéüwÿëÿµwö2äÑu;p„«lÿaeò¬2yâ}Y"åÁy™"ÂÂR¥´²íª¥Q¦6;yôUeyWl&Ssº»V{)W #=˜$λÅöQºéqiÕ`Í1KéUÀCg¹8‚E»ÿâgåC³½Ã†¤Ó¤­ŠQÖWz TÆ.€kšóK³6EÕ‚Ÿ4„üÄ>y’ï!’«×–ÕÅ‹ÙÅWƒ“×µù…QŸ¡jfrðàÁû_*ÅÞjBà²3µvd7’wú7!A²V¦×J©—dT£Ðií?²÷äûéå‡Þá §µSh/Ÿ¿ÿñêýo|„NÙ]¥¼ /Û=SÜ1.SD½Â˜U +j}O¬î«õ£Êâuc÷•î„iÝ®A>IÙNºUÊKî@Î $§‡ …@„Šü¿$½‡—ÛÖ¹öûO|ç¤Xu4•½“‰F$‚‚{ïe8äpzŸ‘F3i¤Q/–%Y®qÇÝŽÓ|âĉ;qOâ8N=9ç|ß=ç®u×½›ºkqÍQÜxß÷y~ÀÞï6ÀvÜú’ŒÒ ‚%à. ¨HJ€RÕ*ÍHÕ%.;åäSšÌ˜ÈÊåTó¨“ ±09¯2*S[${ý%0&òD•ÒŠú“¾Äxbü(86ˆKëœ>ÄX°‚’B†@zfzgŠ³gsÝ“riÙÅõXDâKh!Ê„pàÀkÕ•+ÙÞzó#a¡$—F„$.æ˜pSÊÌÊÙy:G½ +îKè‘5¤³à&Èã¢"Z›wP‡™I˜_ÛºŠV†T°ËÙ‡ +¸¿Ø¿óE)#f +Œg$7‹qYÔŸUÙÝÈÙp ÕJuWsÓk‘æT¤ÑËvçÛ§&·öJK[T(`ÚŸj³ÀÍÆ'6>j#뤃t0í‹UùTVòžX!\š8~é¡Öú¹Ú…1W0H×ÒÝ%_eR‹×‹±|ǯŽš„8Ù¨ÆÅEãwv˜ì¬‘Á¸\é²é›*»¤„Á#‚¥s¹¹%¹=k(6B+ 4{HeÖB4°@ì2m"ƒ(D˜DÙÉˤœôÆ‹˜”ò§k•é•æê“©š=A&Úâ]Ð8¸ƒ#–Ãc6=ÄÈù1LNÊŒ‹ÀžI•eoª‡…ëv.tÙÎÊB¶F„âÌ«sóH0omÀlzÌL ŒZmN÷Dp&j'‚z'§†Y¤kg¦)á°SÒÜÖ¥‰=6U°!*„ó”œtxã‡ÕŽ#xÔDXÝ’ƒVFÍî;+|„XóDÚbnŽOL’ÁüÜÖåÎÚn¤Ösp‘a;a$ýt¼†Ê-ι|áHuÀ2*äAH›p bó™‰Ó•Ù‹l¬9 +bá0>!f; ÜR‘ +•=r äu¬µ_â“U)7*̇KËÔÔˆÿÆAí]C„Mø•†ÞFÔŒ9t6ŽËÉêŠR\B}ù£{Pë²¹E\ÌÞ¥¶ïSÛ霈¿$—ŽIÙ%œËiíœ7\¡ÅD¼Øì.nήï;{ýÒ­'n>ù½'^yó­ŸýòãÏþø×üçç_ýãGï~|æž'r“G ©Ø_¢nö ´K§ ãÙj¯Ð^¬õ§V·6N_ºòàgxbçÞGV/ÜsüÊýgî{âü=½ôÚÛϽùvoíäÌú^¼6Mˆ)òFJL´Šúcl(™ªuZ3ËS+ÇÖvÏžº~ßÕ‡Ÿ¸ÿÙ—·o>zôòƒgxúÑ^ùÇï¼ù·¯þôý[ϾÞ]?M…ÒVRÐÃ4ìˆÑJ(]§ƒ‘P¦Xé-vÖvšË›…ÉÙúÂZke{nûü}=÷«?ùäw|ñÇ¿8qåV¥·.¥[ˆ58x³S€É°œî$«K0³¸I¼Ü[Ëv–¢µi©ÔQêÓ³[WNÞýð¥[O>ÿúO.Üzòô½ßÙ»õ´7VÑ#“˯³y€… Å’0{°{d!Y•ód¥•kÏ·æO¬ì^¿ñèó÷=þìßùõŸþá•Ÿüòø¥û[s'åÜŸvÑèⵃúâD cBüZ QŠ”é¥Úë¡BG*Œ¯¹öàw_¹ïéïáK>³wÏ#»wß¾üÐ3¯¼ýþ#ϽrþÆíÕ³7q1cp²zȃ0a&R $'=Á²œ™à#ÅPª•k-ÎnžkNÍ®<ìÜõSWîûÞk?úý×ÿøßÿñÕŸÿþÑç_>ðä‹ñâäa-rDçvP‰@bZ)¬ª­ì˜·¸X*A¹ŒY1ÁÅDP6É4Žîm]¼ouïž½ÜûøóË'.—gv3›¤Рß:d!½r¥1=³tü؉s{®<ôØ“¯¾ñ£?úâ³ßÿñ×öñ§¿ûþÇç_~ýÚ~þð3/ÏlžOŒo˜ˆˆ •‚‘*/&#±L¹ÞY:væøé+»—î¹ÿ‘§^ýáÏ_ý鯞~íÇ<ÿÊ›oÿò½O¾|áÍŸ½ýo¿úÇ¿ÿçK?~ïÖ3o¬œ¹ ÌX¼2WœÜˆ×g8%ËUº³ó;{{7îàñgž{æ¥WøÎ/?þÝŸ~òÁ'¯üÛ{o¿ÿÉ×ùÇÿýÿü¿Aþå×?}ï·W~¶ÐÛ$[ `:I>Iç*ãSËëà±ptûÌ•›=ý½ç_{óñ_ø…ן{ãG¿øàãß~òÙW_}õþ¯ÿþøw_=òì+[çï‹'="Îd„x7Z]µ¹e;âóK©bmrñèι»ï¿ç‘§ïùÎów?öÜK?üÅÏ?øôÝß|ò—¿ÿý/ÿø¯ß|þÕçøÓ‹o¾3·y˜CR(ñJÛ-üéŽ?Ñ(¶æ&­lï]¹ïö}÷Ñï½þ½7ÞþŇŸ}ðÙÞûä÷¿ýâË?ÿõoÿõ¿ÿdëÏýÅs¯ü,RšQÙ=ÃFTNµÝkB'›`äR¬Ðž]?qéG.?ôØw^|í¿ýìýO÷æÏßù'ï|ü»/?ýÃßûèÓ?ýù¯ÿó?ÿóùï¿|æ•·¶ÎÝΣlHgGÔVÜ‚òKÒ•éfomõø¹ ÷<|ÏÃO½ôÖOùÛÏ~ú«ßxçW¿ûÓ_ÿþÿõéï¿üäóÏÿý?þãW~õÁ'ŠÝµXm…Qšú~6|D‡L„äA)§k³¥öBybnq{ïò­G¿ûÒïôÙg_~ýú¿ýòןüî¿þÏý÷>ûÚîyø‰½«·øhƒ”qRh´ffß>söâÅ+ׯÝzèÁ·~øÖÿøǯÿú×O>ÿâƒß|ð«/í]º<µ´¦šTÛî›]<æ‘H¯å\¾¼²¾uéî[×o=zãöcO~÷…Ÿ¿÷áÏ~õá«?üéO~öî×ýwðu¾÷ê^xí­“—˜XÚ͵—½Á¬¯ÅË%ߧËS+sËëËk«—/^üÁ~øƒÿä'?{çãÏ¿øêoÿ§ãý>ÿâw¿ÿÇ?ÿý‹¯¾zë翸rëÖÄÒ¦jÐR–“^8SlL̬=yaýÄéõÍ—®]å·~óÑÇ}þ‡w>üô­ŸýâÃO>úý—_~öÅç_|ñéÇŸ~òÒ?:}ùþLkÞIÉN*ì‹Ô‚Ùi„‰L8(§óÅúÒêê·zÇÏß{þõ·~ùÁo¿þëßÿþÏÿüͧýõW¿þè£^~ù¹—_]Ú¹ÄF+GÅÊ+—ɦ˜¬Õ» í©…õí݇êÙ^~î•7¾ÿú~ùÁoþñŸÿû÷_ÿíý>ýÙ/~ö篿þü˯^zëíû~*ßZ±’ÚJkÌ”⬘JD¦6—,¶—6¶î쩧^zõ™×~øî‡ý·¿ý÷÷Ã>þüó¯ÿò—/ÿôÕû¿ùðÓÏ>yÿƒ_ßzü‰ÝK7¹ˆé \mÁX9ïW +¡X¥7¿qíÞG^{ëßÞýࣷßýÕ_~ùõ_ÿöéïÿø›Ï~ÿ·¿ÿ¼Ï;¿üÅ»ïÿêý?|ðñ§7ÎÞèn^!¥âf„ý&‡×Ž0:3ê"øJ{ns÷µû~þµ¾ðÆ[/¾þæ»ïýúŸÿùŸ¿ûêÏï~ðÛ_ð›>ùä™—^>yîòÌÒV ÞLÖ×ýB¼XiçÊd&ߘµ8>=¹°2áÊÞ[×Ï_9{rïT²fEŸ‹¤-.ʆ²j¦6¡Z æÂ8)O§J•êx}¼½¸¶±°¾Öé?¹qížs7ï»væÒå3WoÝ»Xšå#):˜ó'»t¨d‚h LC8ãÂ=´×¿¾y~víD4™*åÒ«+ó»gN^»y÷Üÿ“Ÿüøƒ~÷á'Ÿ~öŧO½ðüÖÙ3õ©)_8Άs.€®˜ÏFð”/¼vììæ™ëÙæTºÒl·'æç¦Oœ8öôÓ¾ý³·?þôó/þðû·þöëo¼úòK/<õÄíûコº~¬Øœæ¢Á˜™°À p ª²F»›a„d"¶²¸tﵫ¯¾üò«o½õê«ß{ï½wþò—?=÷ýïžÛ;±¾ºPn4¥šƒðª-â ‡Ý-ØÜ‚‹0Š F’ùrkvviïÔÎí‡|ú™'ðæ«¿ùèÃ/¿úò¯ûË¿þÙ“O>zæÂn¶\°£;.3¬¶Pû GTÐÆ cI.„äÌxwº7¿°±sêÔ¹ ç/]¹rõÚ‹/<÷“·úò«¯|ïùçžyú;¯¼üâ}÷\ß<ºKWœQï¢M ‘AŒ‹*¹./gaÔŠ$W6/^»ÿ©g¾÷ð£ß¹xùú£>ñó_üêÇ?ýñ}÷\|ü›Ý~ðäéÝÅå¥j££äÚ…É->Öº3Ú tÙädufÌÓ±dqyyãÒå+/¾úðJ=ùäí‡o?ÿÜsï¼ûÁÓÏ~ÿÒ¥»çWÓÅ*î œ1¸x™Ôa3DÚ\4Œ3œœ g[¥ÖìøÌš‹åM(aÅ=—GQà<´¤µ‘û4*ÓŽð°[´ºX@ñ‚Ë£9¿œ²B¨vYœæñ¡LÀ-euX`ØŒéìD…mdpÔ†T5fÂ|ˆ²£,-¥€÷ÆYå‚PGSùl³Ó]ÞÚ\Z Æ7£² Ãf—ÊFè>=Ä©¬”ÆNQ„O)bl"ý+‰!Å/E)MÄ"ÉT¢X­õær$¬D«õF.›åY¿Ãå±bdØ7¨=¢¶ƒjqz]LÜÈ&Š½ùí:Ôðó¥ˆ@ ˆV`OÔˆøG ¨L¨ÿ°~ßá°Æ1¬Cì¨óÆ|á-äÝlÂéö{’X‰Q jvK"bB%œM9iÈkn½‹µ£<„óF˜ÒÚ)˜K Á%—¼RÎ&˜ÊŽ³.o +f,nAå !Oìˆùæ î Ê¼®û j¨ “‘5Zm#M.nÌ„êì¸Æ†Ãî WÌLFÙîOÛ ãn¿|`T70l1;ý(wa„Ïs‰i'›ÔcF'kr2ÃZxXeÕé!µÖªÖY­ÉK9’ 9 !Czˆµ# ÂŒ9«Áq1ÁܘÑÚ<*«²û XHû­„äô„c…‰…õÓr,?¦6’€é€tÒ2BTàF̃Z'äVÕð¿îP9`wÔ.µ…6¸üFTÇ"ߟh[qqH ±áJ¸¸À<]xÀ yÌvªÞ^G©ÐA#x+;›ƒU pe¨êðFuˆß€ £fˆ¬72Î&¦¤Òz¤²–™<ÃÆ'‡ ¨câÙ¶á¿}H3ÜïháçäFuêt¶½{S‡mÃ*{<Óµ¡}#¶a#¡±+1R5œŸ—RS:98l†P¿ bU&·ÙÉ[PÉÑŸêÖo2ÉÅ:‡ÇàCjëay@kÔÚ,Xfx +æ¦\Þ„œì(…iBÈb|ÖæÛ©¨“K÷[†’*~k@³oØØ!ér‡ _šðgžh²¸¨mT9«¬Zåò¥=J›’X kA$ÎÕæ>= qÑÚ‡ °åƒùE.5EGê`0CÉ +Îœ^r*›Ç€„,DÔ-5#å-›;rDç4B9Ñ´ ~••6" êÌ„DŠJ¬ÜYRA€RfwAàó FLùœž¸Oi8}±Ã»ÑðÊM&Rw±)#qry5ä4à6BT[‰o èö1Œi ©ˆÓryc6\ÔC¬Îáûö m@‹œ36"ðÓŤ,„¬2S\Ôß蠟w÷ Gùĸ”ëZÝâ€RC)i‘†#öClØ@š]¢íYQyPçvpØ +‚Áˆ‰ZWÀá‰sñž/5gp (á¢u˜Kêa^cp ÛŽ¨aeÊ+¨'28t5‚J:DÒÂ-ÄÃ\ÖíRá– “ * %Õ>!æ!o +„âÔãÏT:'m˜´ïˆòÆ6MòŸTÏ ëлéuF´=u¦£ƒF·Ó›°RŠ‹Ëx#m¹¸F +ÕQ½{ & ià#c6‹“C˜„»?Ý´éU:D t`2cw æ +ÄëáQ«çNÐ2­4©Élg+TœAù,b<÷F'œþ‚ˆh‘ЀÔ¡ ÅúküG.*Ñ1+!SB–ëÕð ÎiByóÄ'à@~ æL/'*Ý¥SD¨2 +ñÛIÉNö[L€¨Ö:¼f˜ZÜ8Ó˜=®wz,¨„7æË#\Î-V©P üÓÍÆü‘2¼ƒ#–#fÅû<åÕý™9&Úƒéx$Û㢵=Ü—`˜±“!ˆ “BÑŦÍX%Céò¼‹KŽ˜ÉA­ œ_6 ²Þä“]#Ø7b1#¼“–4Bgsƒ¤'ÝBª‹²™~l˜ãl'£FT:¤…Çl«7ÍF'ÂÅY,Sßi3bÂdð¿'Š° •µ¿èÆí/¢Lê[Ôûtà›2‘Ž[¨šÑÆÊè@ÇàHí`4NŸÊá5¢¢NÚ¨„Í­ÔgN¥šKV*„p)ŒË!žêM"Þ” ‚¿³ša‰ž1«·ß`‡Œ k­ÕâÍm=ÔÁ,È€j0fóÙÝ 2Xe#u¤â –h± ÓŠÖÎZpyÔÛ80jV;Ÿ:Tc.\¨…ýD ;f¥Ô6Zç`AX~{ÀxDå²`’—þeßè‘1+„€Û6y‡ÌÜ£ç®1xŸ +ê6·z¡¾|î  q0.±F'gå ¹~”ÍLb¡ÖZ]Ý{ÐÂDG!ï(Ä™Ü1;"Ä&“Xs + &˜]#Z (”Kc\ÚÙïñÒoyM‹'èâc°?mcH°HF*—@yO¼+VÈPÍáQ@èš©Æ%¼RÑ#íÙä‘€Ó£ öû·XöÙ@ùb Ô™ Ó¿| 4”hµ6¢:,dñ¤]š;X£¥ +-× Jâ5àÒ‡ ˆ 8è(ìM‚è +(µÙµ ˜/yX ë]v"ìbAM.bþĦÔ6 +£¥•­kRfâà¨eTï2Ãœ ºÅ’[(™œ<8HZ®@^å Ú> sZàW!&éUšVRFýéÖÒYJ©÷›Ò6õ´S'ã"u15©wøFÔ°“¢¾ä¾QǼ¶QŸ2.&&ÜB––ŠzÄ…ãsú`&åËñÆfué"êÙç+´×õ£V<4b¢À˜šHpT.*4¬ƒÚ€c‘ò b~ÁêIŒ9ü*$ˆÉu¥µ]˜9CJe äGø"sÔ—ÖÚi=äQû½AZu³Ž™ÝZ‡(A+*èì̈1Ó ôM,^ôÙoÒé »<™;ë»åý£8ZµïˆnÌ„™ú7EÄCu`QäL‡‰UÀ@’•M8üY.;ã +–l‚ צW/¯\|ÜâUìtØ©’Óbr:”£c݃F7æ‰6:Û”XºkÈ2br[±þúJ_t¢2{!Ñ8&%ëË'®ƒ r Y±8®¯EÑñãщ2:±ñP¼~ïCÏWæN‚”4#>#02( ™Rjh¬ Gú}eCN& 3Y»7ëòY¥ÃÆ'tNn@m„ÝYÑcrË'y⟣‚¥Xe‰² =}ñ )»ïL±&Âu+›Òa’@ µôçÃë]"æ/ð‰Î&Ý\’ ç͘¿ß Èq¥±yA)†½q !9¼1ŸÁ„¼‰Ælô¨™8¤S¢ló¥™X+5½gõeŒ˜Á° ¢Õ¡Gx­Ë°)”I™Ô£ß´ìWÁÃ&Ê„®HëÞH‹”@jT\<øŽ1$sЊT^W®ê É(»nSBú[†#§ €3 Óá#Z§Öî%ø4ȧ'&˜›U—Éø88•˜X„€Ž6däˆ3R4ìô¤H¡NÇ .yÄì…ÉoÌ—Õ£c&âðÔ_tæ +º˜¼/1)çç 1áUú½XiÔHõ×&c2)U…tíO2R|ÄZ05¨ºî°¡þŒ‰i\<&• >e e.;M÷§ÿÍ溻¹©³biç“r¦ëÏt`oÄA­„ qr‡M8ÂÄ3õuB̵Ûð@ˆŠ{Äb¬¶ÈLF Sk§psX(oo¤{'3S§”ÖÑPm +._¼ÒZzñµŸ/ïݯC|v"H +JªK¹¥²‰‹UF®%êfwdÈ@Bž4÷&æ‚¥ã©îy>·`"e!ðѺT{Ø7b§ž(%•éH ñçØx']]ܽüxmþ¼ìSä³3LzÖ­t˜ÄŒ?µœ‰ €WšÙF†‰@Áì’t6ŸÚæuXNwi1b{ÔL¹< +` Ü¡š'ÒL46äÊŠŽè`/€p"€æzÂU0JrkÛ)UñPØË!>fÆM˜ÀD[„T ¤g¤ìœRöÚkà1+}D‹¨m,(E„PÆ…(;˜4¬‘ªÙÛ#”o{’@~ŽŠ¶»¨×–/_¦<êÚap²Öþ¼AœG6:Î(ã£VJo#aOL óý:\êOù³=¹²,•l˜÷tç("æp)O*u.ÕU*+¹‰­úÜ™Ùíû>fÂY>Ú2³î~Çæ0¨3v§ºBq^íòè0!=—éÍNï +‹.ÁìIè Ù-£åE*Rsú3H CˆÀ¾Ö½ñ&,0ɦkgÚ+B¾ç …ÜøzeùJbò jr‹›*Ø_ß8uíép¾7l& ©é`Ê°·Ê.G*ÛÁ8ìp)ÞHMk£âŒYÏ©Ï_X;ûx¼uÞäÒ™úôΈ‰pÒJ}9ÖÙŽwO–®´>¬Ôw@ár12T°³À˜ù!6 ûòjX.Ze£Ù~ ѡߢ3fcÒxd\¨ã ++V6c%ƒÞHÙ-•>‹òY·TôF›‘ê +kÙpÔ#ÄŽœD`ðÍ€ÔPqЀI‚Pœzy"@å}©©@vÊ.Ñí&ÆO†Ë€dŽA6*âÈ)–4°Êq”/С +(A b€X2ô»~ùºÁl…=ÖVYéÙ?:ªs iœ€m5Jó¸?rœ/¸8hÔ`G4ø¡ƒxà¯tdeirxo5ĪúÛCH„Xn„Œwô˜HHE*Ú€ø4Äe(e·1±S‚‰"ž°ð Œ€qeðè·«ê7ã€éM——äüä¾QÓ€Þ¥s n©om¦{§"õ%¿œßÚ»Ý\<£'Ra®µvOuéZ¢}²2w¡4‘ŠŽ1\´…û3V<Ð÷Òt æËtxB©u‡JA6â­Ûì®O¡è`3¤Ü_‰9`°ýÉƧÈ@2;±žŸ>™Ú‰6–Æ—ÎUgO3‘:ŸOOle&6ó“ǽ©IƒA‚øÄظRX4Z”ËL|Ò«Lx” w°>d$õ°Ï僊L‘›+Ìæ|&T˜?zîÚ£/yÃUµƒµ“2™H´¶ævn½úlnö,]“2lªkÄ‚jˆ;¨ÅG¬^©%²¸#úþÅœÅ4áz4è5ƒÅ¥™{ÏÞ~uýÚ÷åÕ y›'fÄC*ˆµS1'¦‚52XQ´ÌÞÖ—žCø’öêouäQ¼JÃînJ¨Lo»G¥ÜŸCC-g  J“˜ÌÍ\²zÒc6—ì_QTýÿÝà ‘:¤vYð`¼²dFø»Ž~ëÀ˜Œ€Pö&§ñPÃB*‡u´ CLvP‹í?¬×Ø8ÐÁ€Qv*ªë/ØWÂÕe”V¢¹Ib¾‚X^g3‹tlR¬¬£BÞ÷í"ð®0Ÿ¹b<£µ3Z;k„ý[ oppÀ—Þ5bÛ?bÄÊ+Í~Çõ1ûxE)¨X •:­ŠÉz¶µ`œð§jó{µå ÉÎ1&^wùb6¯bv÷»“±r&Cz ÈÝ£LË•Éö™Êüݶ~ógP^’¾X ˆ¦ÊáÕ¹|*N+lb VJ:¹Öš“A¸?Vžm¯_›?óðÒÞí“7¿ŸšØ¦ƒ•“÷^¸÷iBÊYÈÅ›@¥z°°’ëži,\qx2wBA ñ¦Øä$\Ù=f$Ù, Ę7Úb לl +`#%䢕ùòÊEW `p‰‰êF¶}:RXjÌiöç5ë Î=ð=&ÑйX˜èóà$â/¼V"C €‘PÙi`ÅÜ\¨´XœÞY9{{bý**U}ÑñõS7a:b@6Úåç@•‹+D°5h$Ç,n”QL¨Ð7ój—•äòJjrxÔŸµ»ƒRz\ãð¨ìZ3»u¶o—–ïNMý…}#0D(Ë»8ý©jhØêÕcà,$‹TªÇ•Úö!µóàAk%œýë-Шsp Âa¾Ä&f;Ÿ¨2"VŒ6ôÛMX©¨“Ï»ÃõæòÅå‹OÀ7âSV\J‡«ÞXË›èúr‹\aòe`ZI”æQ_Ì‚ùÝ€AyZ*×èÀójû¾!áQËþË Yßß=ÐÁ™œ¢ ØBwHg#ÝBÎmˆ…é`i.Þ>Î¥&1¡ ƒ”¨ÂŒèdÞʤ/c# !1eÆdÄ`Þˆ ©€” ‚Ï l䂾êKѺ@² ÅHðˆÙ£ƒB(… s¬Rs¸E')ú¢&2¥›™Þ*ÏŸ‰×–ãÅùÞê…heæbT7R?JÛœ›—«ÞÄ¬Õ  IñHew ;j¡4€ -tß< ƤÜ¡¾Ôç‹à~•é[C:Âó± 1³«o57nz•:*N­_ñe¦@Ð8ÓÅ(›Ds}ÀHéÒ_v‹å1›gŸÊ¾_eW;BÈûâ)›VHÔ]LL±àôpÁ@J ç'NØ1¸xà`sÇQ_Æó*3 ðÿà˜]ev#Œ¢±Qýì€Ð¹¬/Ñ¥" &ÞÆ‚e3»EŸ̶—ës›kçºÇïî»:wâfmá¬ÜØ°³±Ty¦á”– <á¨y"]•†ÙŠ“lµ¸[ë´„Ý5ÂœÙåW[(·/Cdc£ž`ÖêÁgÁ0 iw¸ÆEë™ÆòÚ©ûh¹ ¶¯Ëýg*Áô Ç|9cõTeAe&÷[UVÆ„É”T +ÀiðJµ0±¡²£¦þ•Pa)×Û-Ξî»)–@îÆW&×.ª¬^³+`ÃD0J iA|8Ù”¨Í¹… ˆ4#H7—Ÿ +×ÄÊJzîBgãÞ`º;µxêµ·HtGm~4P÷¥bµµcŸ\»ð¥tŒ¹ w˜Š €4Ü) ¹‚¹ÃÅzÞP˜óQ ,Ð)•Ý;`ÊÊ[|:‡ÀEÇÝÁœÚNˆVx0  $t×æïêärd¸A†ªt7Z[¦ÄâœÒÚôå¨Ø8.çÇ7{÷2Ñæ˜Õ¢…ˆâ\«É%AÔÃýa“{ØL©l^‹+h†ý0¢¥Bg…7Ò €{—ÂÕùHm)PZÂÂã&%D*3Ëg'–ï¶ÓÊ  ÓÁ^“$ƒ”ÏÐÁþ\kZÈÄ +S0ê_TÇ$Ô_Å žy•ƒ_Pe¥ÆÌnã@y‡D¿>rñIw¨@‡Ë™ÆÒí§ß|ö¿®ÎŸrKy1?-ägó“;ÉÆZuâx{f‡•sœ\`ÂUO¨Ê(ã.6¯¶ñ‡Æ\Fúwè@himÞa½ûà¨ãÀ°eXç3®û”f©³5¾xvÄN0ÑJGMT˜ŒTÃå•HeQš¬ÊëÉê¢ è Õ›Š‚óh®›(Ls i\0ð lÒŠG nPç­˜@øÒ|¬i„}v"„R…LÏÛŸSW ÄŠ›O—ÚëÕÉãÀa2Œ0Qˆ” Ɔúõµa1ÙD½áA•ÍŽ :â=±›ì€÷Ü¢’®mîÞvëJe;ZÝâ£m9=)$;0—Ðâž@6Q˜÷À¨UmóYÝ)*4ÙZº¯±r“’Gt8%dùpÑàð|c@÷­1û ™Ñ9C.&HM:˜¨… +êœì¾A#xÑ#h ,/è#åºXZ äÐ@Ané©€7ш»Û;5µyßúù'·®>?{âA,&üi—²aGCFüШmHë!nÄL¹¸$°Ä#VϨÅc„ Jv2@ÊéÜÄêêéû+ ;FJ 8–W­©´ÜŸg›hƒOï74ð¥ p„¸ˆÉóGM5Žç&vèH{ÔÂ8p ãó„Pqq ÒJ£*ì%FP&”š\:Éu Y1ш†ÌXØîŽa\Ú†Š'?¤…‡ûíø 0\ƒhHçtû³"(é¹)Û#…ôÔêå\û˜‰J-Þ9•>¾m¶ϦjË€Ù6ÌNKéiZ¼SlÔ ôÍZ½Õ@k'öëhþu{L>SeóèìlÿêÍsX Y åÒÀ'Ä«+>¥biH‡!t”• +*>0j7Áœ â€ñp’á!2¢sjÌà²ÚJУÀNÐþåSh!‰ù’BrâÄå'f7¯Û{’”Xš)­`l"xÌÒ8¼@õœÞ80<¤¿äæ‹þø$ìM¨LåŠJ 8œ9¤àdNïð j‘a=:b&týRéPÙGM0*Ôˆ$È:¸ÄÄ1¡RO©¯±Janëjkù|ª±VšÜj,Ÿ/Í ð@F)ô¼á¢òØqAc÷í3 N·ìæ3\¤üX¦yÔ ¾©69YÔ— +WgNÜ»rá;…Éí“>yý_º ±I—u²iRªÒs‰ö^~þš/;Cð©lëX´±Î%Ú[¬ýn·‚‹p‘.D'¾=l¹kÈrHÃB Ïø +ÚþŽ¨>Ü—‰”¬bÁx9ÛYÚºrãÑï/Ÿ¾ª-ݹ ZìmÜÝ^¿BGŠVJô%:¥ÉídmÉk(ù :×X0'A¸´ÑÅYi`56ŸÁ´â2LÇÁy?0dѺtVŠ œ2úË0“¢ÕîÒ))Ýrù"\·¶r©{ü&@ÂXg—Š¶u°?–é­ïݶyCC&T1@m¨M;d¹VïxoõÒþ!ó¾ §'Î%{d¨N%µdynrõ"àe+(b¾´“#l"‚¾Háâ#÷ô=ªÆFj¬”òêQPÄÌ.ÞŠøF„Å}1Ý"-^žj¯T9#S­c Rá |,_è;Xˆ× 0{4¯»Cu”KÑ8A +XQ¿b-ý{”˜jÍبTè'Œs±oÒ|ëàØ7ŒÔé“ËÏYpH™Úœ”ט0¯6 ~LÈQJ“ÏL‹¥ålo/Ó>Ö\{íÇÌm]ÌhH.Í ¹i@©lbÚéÏž`¢Ý…s2|xÔ8¤6i-¨º¿» Œ*RB +较V¡È¥&ÒíÕbïx²:{æò­Wá $ W¥üRz|§½~_uþRyú kËɉÅãWÈPR‡1*bë÷Ë3Ê%±ÊÁ€Á yØáqøh±A“p–™çS3T ·tôJ©»A‡‹|ºßÎ%ÕÙ­-^Û¾µqíùTg+ÛZYÙ½Ágf¸X“ ×0>õ·bÏ \Ø!k#fÌ€ø-„ì‘Úld¾ÝQƒ1w‰n_nÄHPAÀdÚÈ$e#PC2rÇ„711øâ`¬,TŒŽ4[‹ª½©ò\²2³·Dkúþ(ƒKÒ:x“3ˆRÑle±8~ôÀ°Íädl¸_Û÷'14XAÄäMTº›õÞ¨“²øX…Sªt°¿‡£T]¾´‹Ž¦Š½teNeFÇŒ¨ÉÉ;I¥ÉéÓcj&(¥pª©·‘)õ6æv®Ïž|€Ï-8ý%+Ÿ\:ûý7ߥ;j ÜƦh±FK-w°nèoêáö ¥éSB¢©1ã œÆt¨ÎBÚ\¼ øĴ˲ "Áe†tè¿‚èq¨­^*휕\ž˜”lï]Bã !FÁ„,mòù¹º¢”gº³›7}¹Ð9 +p8Û>žŸÚó³¾ä¬kûÂ+Ê¥Ë÷½ìW¾qhøÀ@â°W[= °»¹X¥6½¶{Ã)ƒò… ¸”ó§[ri*×Ûˆ·|\ÌMÆšÇèpÃh4ÙÄ<«Ô©`EˆwÒeZN³u€9zD2’•’U0?jcP_Ž–›*ê`’ÃvnŸ +VA>©x£mÔŸ'ý™™• ¥Î£T¹éD{»0³7±vmùÔ•Å3B~ª9{âÑ—ÞñÆÛ&2ŒK¤TÆ’‹OÝÙ•»è¼LëXÏ”;m w:‚j~„Éò‰1ã@AŒõï>÷·É®ƒéžôFªt Uë‹äçÓʼnÊÔqR*èQ?䂨ð 6Õ6¬ Y\’ÙÎB¸@oP‹jï•r£bÐDÑJ;?{.Ò8ÊÆÛ:j&dB²îðDœl +PŸÉ^¿‹;Õ$­eDg£´VÒßÇœ? ±ª$D‡}©žX^67cÍãVRQÒë·_\>s4« …)>×ã n[‰  á½Ár~â$î/Ýñíz;0áÀ(Q à¨k£§¯>†õ/ŽI¥EFZOŠŽŒGªëÍûød«5³½sùñ|gSïŒ ãðH**Šxû÷ûÌNŸÞÉjÁÙ˜¡†ü&2êKÎ$ëGCéñÚä±îæÝ‹g^8ûÐô‰›\rB‰¸?¨¯æ'O¸˜¨W.Álh1DET¯ÚìÕÙX…v þD~Æ'W¿uP;¨‚Lk°QF›×èÕvóe§6®õÿ_ ƒ:ødOdœOö,îè]ƒvÌXÜ‚…\¾(­ëÞX¿–9çŽuÝ᪟‰÷["¿wg.Ð|¨¸ßê Ûúý{õvÖ ûT&÷þþöU¶nu-TH‹TNÖ£”½‰*ÄG¤âtuùb´¹¯¯f;[©Öº7\þÙÖ_ÀRp Øv¢!]Ì›‹ÇÏÞÏEû‡L0¿PR¨ [ÈÄÄäüâOÒ8@¦ëÑF)ùAô‚ÚŸ,´g×N–zkVBPê«­õ{fwnOoÞ¬Íï–fvÙn(Ñ|ðÙ·N?ø²“ì”’no¯\OÔwõ\¼kÀ@£¡dƒsc6怚8¨!F­¼Uœ\>”›Þºx{÷úJs0:\´zã¤\M·Ö³íÄÄi‡?ï d¹2jµ‰õ—Û3I4¡|ït;?›ÇFEœ\¦c4H0€t'-E +] ÒÃ~J®ÄšùÉS“W gû7.ÅäîùûŸã|kiÌÆáþ(¹ÅªŠéœ`8É@ÖåU†L˜ÖÉ‘Á+\ªÄúmyî‹–¦v}úÕÎò™ñõËd.efϽwêص™cWãÕduþÕŸþæʃ/8ˆ +•¼¡†Ù%â Ðñ–/TuãÀ¨‹–JÄwhÌ~pØü­ÚCÃ’*é‰vh¹-Í{å2&¨è¸;ܤ¢LjZçò‘¡¼\œáQ*ÑæZaþlsýjcãzyåZx|Û›ìÊéÞÆÉæ¶n bKÑþ¬’5*6çMö·w7º«aMÿN5dÀÔv —lÞ~ç¹4+•¦’¹é­»§·¯¯ž½Õ^»km`rÍîãw®ÁÚQçèÏ7s«Ù‰¥é½PaòˆµÝÁ„1©¼ÉçÜ¡—\VèpuÔÁ¨^'P£²S€™h#\œIÔæÁO.VGÙH¢±œëÏ´7¤ül¤º©,z#µP¢¾¼}yb唕”¾EkKýV6±q\ªõw5áÔ¿º}-^šUÛ9 a"f£Òà[)L¸x»2y`Êg|¹Ù`uµ8svóÒS{¾Þ\¿æ²…ÚÜêÎÝ„? b,Z^-Ì\®.ÞHŒŸõ%ºÀÙ°Àæî½ÉÚÚ!-6 Öê±Ññ¾°––;'^h,Õz›à•N&Ke·\SJó@8RÛÁLÂh¾ç •Ô€íS¹/ïQÚ||Òˆ*+å‘ˉñ¾¦£\ +ñ¥01c%E.\NTç=R®<»»xö‘Tw;\ž'CEFÙÀ™åÝû¯?þÆøâY'“⢓8_08y£ƒêÏpP»:‡t”?19f%ÿå€ú° +5:+cÁÂX l9‚‡8¹ +Þ `²»Ý;y{éÂÓÅ…‹N&žnmôŽ^ß¼ôdsöäÖåÇ+çBÕÙÒâÙäÌžP]df{+W6Ï?^_ØÔƧ¦âÍHeÅ“˜±yRÃ|ÿ°ù +rÒˆ +é…hØŃ2T +ÓRajþøÕÍ+‰Ån´¹Ÿ9ÙoOT]KµO—g¯(µu )ÙÈþmeNLlœKMÓ±)65OÈ-'“6ãA•ƒv² §?o„Åæ`/´Œà³7ÜFŠ'1>.d{…©ÓµÅóÀÅø§‚ÙRoXX'‘JóÍ•+kçŸÞ¹%UÙDb‚¯´÷¼áœÝ$BeT(‚¡Àùê‰&ŠSB¼aÁD#ì7ÀýÍYÌxØŒËÁL/Q[:r`ØêôDàÐáŠ7\J4–š+ä겎Ì/ž¼úÀ3|¤àôDòÝÓ™ÎéXuƒǪ!vØ„Cxpgï|síH“Yf3L¬#—WÓÝÝxk gc§/Þ:~ö€ðfL$R[¾zìʳKžÊkf" “ÁÇž}}fí¬æ«-`ãÓb~ó—´ˆñ'ŽÆëCF·ídªÙYzŠÒ;—>wïw…DÛJHTÿR|/V]ém\Þºútif*¨/ë•›:˜;8fûæ!Í!+pƒà­(uè®A³ÙÉ…Ó=…Ö¢@7md¬¿å(,â\r‡p>é5­£é‰T¬²r±ÐÝ.Îœª-쮟¼~áÖó¹îŸí®î=ºsó¥Þ‰‡€^\¾ýfkõn&ÚJææ·î ¤Ûcc@ƒ´Xrûb€sµ6Ú r¬VÎ?ÖÃ^P(¹¬4×ÝíæÊ¥êÒ—¬w×ÎçvÃÍu¹¹áËκ#þ¬æx(—/×3zd(¶úr€`‚õãV¡ªÅ‡/GDBaÞéÏâá +iº„Š[ª¢"Êg(0Û>Œ»XbCJue|ãfëèÍâÜùtg;Rõ­L5§¶ï䧂…™æÑáÖqO²ãQj€X'—ÏØÉ€Æá —–¹x‡ðç|Ѧë!a”@¬ œÒ]ƒ¶µËˆøÝB:^[c£ã&TÒZ½b¼iw f€)@ò;W¿sòÊãí• Ja†TŒ°"@§ú;Ðé1F¸D²¶œ¬,„2]••<0bUYi¶Íé ƒAs² +BKér gŽhí\¸Ö\½8uòþ꩸àâ‹ý‹|üÚí§Z³'ÔNP`iŒÏYˆˆÆØ“7¹|N*”®Í³}¶2è<ÂGÝð(ãžè&”‚ñÚ…›Ÿ¾ú¸›Ï8½ÉêÜåì䥺$eº€ìT\e¡.¦ÐÞYsD‹@€SÄÎgb#äýÃvÅ]_~èéh‘ƒÃ6•Ùã £ŒÒ¦gCÉ ‚‘êÓt0cê·çª»£“0éO5—r½íÂÔv¶sT.MûbµLkµ{üZgãl¡·Šg0?MGÇ]|FˆÕ+“›L¸ÒŸÊèûyÌ ¨„3áÄ~wóIµÕ hÅÅFý©‰ÆòÅåóON¿QžÝ™œßüÑOßÛ½û1¦ßüj&>±*¯Æš›××/|§µz97qtï¾ç<É®Þöççã'R“ç¦O}gz÷±êê•ÂøÑçÞxÿÖs?5‘5,ŒA~µ+`eÓžx/T\Ê6VN]¼êø•üìneé\~ö¤\[(ÏŸj.ž]9sëÄ=O5Ì´Ö¶¯=½÷À ó§(Îî‰ù"ØŸÛIÉãz4¨²Ó62b!P)©ÆÅZ¸'B5*“bÙBÆl(ðfTøÎÉ +UaO܈ú|ÕfN2ÑŠœmfë³JyžKN£Bñ06mÇ…Á;»Hƒ°bøäëo`õ"LÒAÈàI„‰UB} ߬Ò®lÈŒ\ì ™QÚAû½rVH—§wZËX¥ædÁAö×tÜ™q‡a΀חö%Úv·l€¦1z8 ³³+evqFBJ¶Ö[+ç S;BnöeÔvÆ'ƒ‰q˜VH!çô$06ç‹40.i@xgé1f¢,.áÐtðˆõÀ yÌHBDÐé > <ÿ¾ÃF½3w ™ª !3©²y´’ $Ùé KŹPi™òÉQ`‡šK…ÞV||ÓŸžôHù`¢¡TfÜ¢‚ó"«þ?’Þ³KŽ+KÏýwIšé¡…)”Oï#"Ãfx›a2Ò{o*³*+ËÊ£ªàAЀ$hšžmhºÉi¶fº§½QwFÒŒÔÙ»ÖÕÕº_ïÉÖZñ,dTÄ9gïw?oEžT¼dT·­àõb˜‰7bZåÜ<2éDÁ€ †hÛd@Œ› G ÊA„6)½¬”V²ÝýöÚÕáÖéñGÝÓBÿ`õÊãþÁ£í^Ì´¶„DUNµsÕÝk/ñ™µ¸R_¿5Ø» ÑÜ’r ´]M®¿ðõ£çßò’¦˜1ÉZÚ\>}kéô­dc«³¸ûùW?=}ø¶˜[°››·kë—¼ÿÖw~þñ_ÿ›W?ùÑþµW?úö_½ðöw”ÊšR^ãs+Vã µyûÖÇ £J’r50ùù¡–ç!1ÌÚ&ÅæVË+wåÂ(ÂX•…-J)²VGÌ/Æ’m“¤Þ,N7®½kYÞ* É&¦7 +ƒ«•ÕçÅÂv.ògSK”ÕuF8F͆( ÔV7ÿÓËY´Iò©æ.¡”\¨KÅÆÆó¹Å&Õ‹%Ú„^öbã.4~RvÃ4ÄÄq9Ch%2^bu\)€Ò€üÚ½“¬¯a$ãuªÎûc>PM´º‘ÁÒ3fÍÇ]¨„ˆY6³”h^"*æI>E +)ˆ±9³îñNaS|¢…+UàRFz¢ê3“a¥Ê +:0,>í!€¿ä:þwÒŸGÎ{(¢c‰ñs„´˜n§ÚÛ*a¦*FqW2J¶+dKS}³´ª—G˜hÆ‹M” Öä’=!¿¼!àL(j©¥xvqxÿ Ä/"Ì™³3Á§/x‰¢qB*…L-Vó½üR¦·…«)9ÝhoÜÐK»20Š}LÎA´É ÞlÊ©Ì%­êJ¦³3ÊQ¹¤A¥³³(ÛU»²$g;X°a(f•«¥…]3]m¶—î<|£½~È[åÖÊ¥Ã{o_}ôÁ­Çß¾þÚÇW^yoóÆK½Ã›÷_Û½ñZkteï¹wê[/4·ö¶ÖnØÍ‹ÖŽ'›Ï½þYkóæ³óÈ„J¹˜ÕÑ›—òK×Q)“R÷_û˜7ªNXñP–TÙÃÁÁ£•«ïÖ¶ŒÙ;¹÷àO +ƒÃ0Ÿ¥H©Áj«¼tcýúûÙÅ+Aʸ|çõ|wg*È[=’üT +Ê……+Ñ9³Ñèt·ï‚@Mww £æÙ¸V‰%š©Þ^T+;!†3K€' %ÖÂOjŸá’íÚÚ­+¿W^¾ Ò\L÷ÎzÇ#œ! ŠŒ¡³‹+ERÊÄs `8™þ5.»‹¥7`“]Új; •WÓ0gΈ`LÔ|¼´f6öôú.iÖcjÖȵ¸dÝpRª—^8µÛG™Þ &—<`~rÚ•õŠš¨9äÙÿ™iÿ' +¼p”ÍÃtç²…în„KJNJ·“Ý6³@è%0'éî%DHÌ1 ˜R3ªúþ¸çi‚""$;Q¥dÖ‡¨åí– +„B¯‚¬Ÿ1“n4ÊXî3$ƒd"U^2Ér²Ð¥ä¤Ï[ËÕáV¡·l–X»Êç:zm”í®µ6Oj.íÜ{·uñ˜I^/£¬Ä„D&RµÕý;Éæ:𪢽`äFrºO›íex#+*WÔ‰* )¿žé^¾<:y¥»ó¼’Ò¢}÷þãýæß.݇•²Þس®Uv_;yé;«Ç/æ:ë²]xåOV/=œ‹ˆç]$“Z6ZG¥å›o}lTÖôTûîË?øh*Äϲ$¦Vïï>Ü»÷ñ΃O¤Òz¾¹ñÂ;ßK·¶]èŠ +À g{§Ë§O^ü´¾vGHn½¨¬?5zv*4奃d +Šve½³y;&ç/ß|ùƒïþ— +°ÚdskLz$ä7–/}ëηÒÓ f\{ὫßWs `ÎÏ:£ ³\¦R„Ö>3ON8°íý;—o½ê†„?vþÏŸ™û—O:ž˜¹PÀ<ë~4دÜß#Ô2mµ0¥|n 6~™&ä€ÉŠš˜¨øPчÉ—âÒ}½¶]^<îïÝòKAB±Ë+RnÉ…·Y͆øI/åqk2rÚ¡^‹I2ë'‚l2ÄgÝã-~¹›q@,gTôòrnùòðôÕîÁƒâÚµ²3¤üÇ™U@YÒNÊÇx".¬Ê:"„©gûà–ÜïBµ©çFU7"'Â*%PÙ½oVÖ@Ê3Jc’HŠàj<Õ4 §Ïø`˜109Ã$ªÕ•“áá½Îæ )ÕÖs]9»c¡ÍhyœµÃ˜ŒÅ´(­ÏbçˆûO0üQ)L(AL"3m"1Ýa6Mˆù0Çô(oƒà–‘jV;ùÖ"ŸfVG×2½}­Ð‹Ås1%)Æ“²žq.Ì]p„©¥(Ÿ +‘šæ¼ašâSjv@š¹ˆ¬×ôê¦ìÖG—»;·SÝ](¦g +­Ã;VqÅl£4ÒŠC!Õ¡ô"²0©û£ +5îë•8ï@)Ìd¥ô²VXgÆ'99‘«ªƒðµ±£Ôv2K7²ƒ+Fy(?Â¥ ”n^YÚ½ ¸    šÿ–VJ³|TS­t÷áû/¼ö‘žšŠLx¹Ù¨‘kzmW/oˆñÜÛßøòî£f\øÓY? àßl—¶€qú ‚5õL+Lh@4`Ζ³=À~åÅÃLkâlZHôW3U',úPŠ×¢Bå“>(6ç§_‰±‚7ò†£˜²{—Øqï~T.£ŒÞì¬<¯cVM),Ë… SpE¸(¥ÆĤ+@ºƒt„0Ê&øœ”ì_ B§8¥ÄJ¹R}­³vÝkJRºç|Ôøž=QZœõQÀOÌG'x#À_é(Ÿ¼!©fü¸ §ÛéöŽZ\瓃RÿDÍ ¸Znoeê«®0ü Uü¤í€!ªS›à–æ=aAIJfmÚHRpŒ]˜äAtˆI{£q@ÂܘLJ0—S ŠÝF˜*ÄGÇßž̃G)%BÈREh0ƒ–'\28„<ãŒÌy°y .„ÅÆïyÄÕJ˜I;B1«¼¤•„CƒÖ‹¸˜fÔ¼lbbÂéÇ)!iå;>ˆô„±|ÎÆK”œÃóà0€x=Èø«\€FÀOÇðΘ¨ìC€¯Ú§:Ì&an¼#¡mBHóñ<ŒËó^ôüt Xˆ?sÁv*4ç‚,FÒ²‘¨eÊ#ácbw`~<‰¥0›’LKÑ—­v%«ôôtx¼/,=, ¯6·_¨­ßt‡ù0¦ T<ˆra” bØD˜ÑI5'§[rª…ò6''95…Pª;ÌEiŒd\ˆTgÜH8Ê"T&hN„¢(Á­Ê¹6ªæý¤îC%AI¯nžì^¾Ïê9ªza%ß½¬æ7"”€h·ƒòC|Wü0@ˆÒ.CÅ›8Ÿ!’¤dwwoÜxéC©:a6@¨>X +èÅÁ!ÏyÑi'xoÞGy Á‹ªNHœ±nXÀù4¸y˜5"¤œª­$jkZa$g—»KŦÊV]²› ®B¤¡fÒ$´@h¹BÂä\ÐéƒÁ …Äç\O_ðÍøbòå•ò"qàÅ´Ü"&樈P]OTó“c Œ~T¾0òH?ðÎNèÜ\I²ƒˆx§æC¢V2Óýª !U Ê·ä‰Ð€?•t}Æ*JJÀ»™8kA¸ä anŒöÍUnŸ/Žà³`&áCåY@wÊu…˜§Î¹'ç  ";ý1?X/LF8§ À|uqŸUóžeÒŠB[ /ÌÏOyf0Œ)³Á‰¹ˆ7"E°¸’(Ëf‘•²a4ÌàŸ=3ÿÔ„ïÙÉÀ…ñ6Ø茥DØÀ0.L:#Oœwãbž1ê"˜\1j3^Ì$ÎχŸ8;ãöÃA\ô ,ÎÆsµE³ÐãÍr—]!lÚr(„µÀã’•éföϺ¢óÁ‰ï…Y/¥JŒ ƪJ²€)IG„¦„D¥¹œÊwÀÁ4eZ¢UNÜOÄ'œ¨3óFXZÊòñR—æ¼Q§s‡H?¨€nÆM„¢ªbóÕÅTµ¥DÑÈ‚\v†ypbáA@>yÞwaöAã¡=sÁ9ä¼x“«¬Ù“ ¼UóÃŒ ¤:+Ç1½  ¸˜^×2ýTm]J.ø0m>@FåÌñ7ë ÀÅĆ¢œ3ˆMÎùæ\ÁH”õGbžp W&^'¥<ˆ7ăÂÁ™m 9˜Ü”³[ŒÑöf1N?åôa秽îøzN/`¼=åBžšpÍ:# Ãý­£{Ÿu#.î14o>s~~ÚBŒ5ÀT}Á Œ2(ÁC„à‹ÀsW(£¢…³(Ÿ?§ö€©£~ò‰§ggÑY'2qÁwæ¬ëÜ„oÆ ’WÀb&+¥·XéÒÅ£ÛQÆœq¡n? TÅæ|a6Jă05Q--ÕÀDêî0{aúÚÓŽ)á‡%p?Ož™… ™àm‡˜÷bSNäìTgH6 aš7ÛÖÏÎþâìüŸ=;óÔ„d§eQZV4kçä–h•¦}¸3Ì áÒ¨TŽ0éi’¹wpû•oüÍ<"ü‹'&ž™pzCTŒOðjJIäh^Û=¾µ´sÝ3ççBójÎGÎùI˜RqN¥Åx-€š__ó21J˜çüL`r6‚O€9{ÁóÄÓÓO<9íðP”X ¸´/#(E3sz¢ÈkÒ â°Öä\øÉgçž=眚ñ;=(ÁÚ!D˜MLûgœˆËƒ3LØÎ{æ‚çÙsó´”Iíì¤gr.2ï%£1]Ò +Q\szcS7@³cÒ?é„}a žÌœ;‚Q Ná ‡gÓFµÛŠr¬/ŠIf:[iiÉl”=áp8Vâf¶ÒQ3''Üç¦ýž P«¤¦Ù10.Ò‹ëëJ:?D&Ý°âJ!%ž0ì¬mg¬••ÅÛ/¼XjÎÏùÏÍzÏM»0’£FUeŽç¡°, ¶mÄÜéžõú\8±,1—7÷wWw·Û›‹éZZÖEVQ‚„dxü¶_Gd:+(R\æAÒ­sjD/Àž™ù¸DªP͵ûŒ–´r-„ÑB(Š²®~fÊ÷çgΈ‰¥ÎÏFÀÔ9<'YÅz¿»¼ÊŠ2PiÂd^¹SÄÄD„Ñ3µEÎ(0ñ.¦æ|˜/ƒp&8šSq&B8ˆPq!‡³&͈ E$š¢ÅiŽ%Ç`‡Ij42‹£ÁU$ã’a„‡0ÅæqÖ÷Ì…À¬ sz §;â @F*Fbaˆ$ÛÌ£T‚ä’³nèéóó3N†Ù` êu;q̯Šp)Ã-´ì¥a­X³“Y¥^Mm¶o^ÞXßì–Ëf©Ð6¯ÏÎ>ufzzÖDšÑX‚E= u³ÅŠ%j ¯ò¢¡SlÌ0…bQv¬‡·÷¼p|çÆÆÝç.æÊÙó3îó³þ@„òÄq¼Ä‘ +‹¨,Ü.ÅG ™fY±5¤Y·sïì‚ã»ïÞüíß¼÷³ŸþÞVWJ†EÇxa“ã—ç¢EÇ“¶Q­˜V¦ÕÈÆeÖN˜$Ã9¼¯Ï/ |¹Rh,4–××î½öN¶Õó 𜚘ðô*Ƙ]˜Ë<ýô™(Y†Q,Ò™D(â„ÆÓH±b¥\¸tùJ­Û“Ls´½ff3~w…1J¶Q!!'›-CP8FÆÀa[v.W2âjÚàÓ*º=*ì,uê™JZÞìçž¿¹ýòóï¾víÑýK7¯îõûu+aFB?€¸}‚ë@“1‚G”¡y›m–´zQ´¬ÃQêî~õ;k?¾ôÍ×.ýá—_þú§ßy|ÿhw{X«•xNE0È¥-‚6" 4£PFË)-«³ÝŠ¹ÜË.4sëËWvOŽF·¯o½öàô¥ž;>8®UjfÂdY*ðOLyÜjÊ<{Á?í@Ÿ93ÿÌ™¹Éi× ù=~ŽÂ“º\¯æ5™h¼KåóYÓ°-3†©§Î΄uy£‘€E2î,ÐþÀxáRëæAûîåÁ¯~òÞí÷í~pé÷ßð¿þø_úÒ[®l­õmÛFpzÞéq;f± Óæ n +í¦ƒËeôdż²[¹´]ǵÃÎÝ“î£ç–_»;üàáÚïøÖ¿ûÍ7¿ûþ•çö +©8êt»fœ!”1’‡" ûë:²×掗䇗J¾¸ùöóÃ7î,|øÒê/¾¼ÿ¿ùæ/¾{ç×ß½õ?{óý÷ßüõg_»»q²ÛJ¥ìÌÃ4¬¤)•3ürSß[L,ä±Å2UÏPö=ê¥WRWvšï¼|ø»ßüë_þêû~ðÒƒ;»·DÏ:.Ô‡ðg¦=O½õãOþøë7ÿá‡Ï¿¯y÷°²=,%-5•J‰jJŠçb1Úà#õDt¥ÌÝØȾ~kùÅãúÓê—^ÿÍÞýã?üõï~ñÍ_õðüáÃÿïÿù‡?üò“‡§­Ïß<øÅW¯¼ýp?¡ËóÎȹé 3@â1)à÷Fü®hÐÁc^íçc—×r§¥Ýy¼’|p¥ÿÅ7_üàÑ¥ßzíÅ;–™ðÁtái Ë*±nšØª3Ï­éÜ^ø˯_ùâë—úéýÿ“wÿß?þôw?ÿÌÀþê¥?þö[Ÿ|ýæÖR1¡ 1šWõ$Eà" •u|˜G/õØ+ú+‡ùï>¾øÓÏïõáµÏÞÜÿÑ7¯ý‡=þÿæ[ÿù·þöû÷ÿî«;¿þòî+7Wó ‘fh!O(xBRsš¼”¡_Þ1ß¾Zxïfí‹××ÿö;7þûïÞû‡Ÿ½ùƒo\ùí—·þé§þá‡üÑÑO>>øåg—¿ÿÞáµR)g{@…$‚ŽzÕµNj¥®^[K|ãáðo>>ùüõO_ßþýßúÕW/ýå»'õÎÁÿý÷Ÿþïÿö³úõ{¿ÿêùÿú»÷¾÷áÕ^\秦¾H$BÈ4ÝË‹u=TS½—ûì½ÔÝäËWj¾¼þÕûGŸ½±õÓÏïþ—¿ÿâï~ðø?zóŸÿÙ¿ó`o­Þ,Ú¥^“¦·¤f2¶U%ÁéÞéýâ“ëûåÝï½sðÝ·÷üéÿüÛ~ùŽ¯Þ=øÙ§7þÓ¯Þùíç·>Ðúð¹ò~OÊlŒb¦¦=O?5t:õ˜¯›„Ž:Âõ5ûáiýÝ{«?ý΃úÕ»ÿþ'¯ÿý_ýû½ö³OnüìÇýÖÚ»7«§Ãx#I"ŠÂ‘§¡±xÜ,5íQ7ßÍ2ƒ vÚgß»»ðå×~øÍ«?øÆÉ/¿¼ý÷?yýßþôÍ_}q÷Wß<ú·ß¿ýŸ~þÊï¾çóW—Þ¾’}pT+¦5ˆHŒq NPÊ9£d‘ ?Äoo÷ì7®V>cûwýðW_<÷‹ÏnþÏÿðåÿüýÝ7÷¾ûεûÏíâ„bqà½8"Á†ë¶\¤ŽûêKGõÏ^ÛùêÝË?úèÚ?ÿú£ÿý_ñ?zýo><}ÿþÖæ°d(1Ã#(Ç«YIË*Š­²tJ„ ⥥Âs;•ù￵ûwß{ñ?þèãWv>¾¿ô½7·òÍkŸ¾±÷RsoT­”*¢ž£å,°HÀ‹+Š™É'IoÙä^ß>\L^]I~ýfûœþò‹‡_½õ»o|ø`ûµË Ž·¶ +«x9 Ѓò„HJ(ªf#FriCI*TFÆ—JÒݽÚg6~øÁÑo¾¼ó·ßé?xýŸ~ùáÏ?»ý‹oþñg¯ýó/ÞüÍ·¾x©öð0ß/ +(äwú`Y¯$3=^0åVŽU^´B7†Ò£ÓÊãkÍo½¼ù_þÍGüí¿ÿáËÿøË·ÿù÷Ÿüô;÷>y¼ûîƒõa/#©qŒ1`2ž/¶–5Dr­•l'±A +Úk2w·r¯]í?<îܾX~ÿöàWŸßýé§÷>m÷/Œ_íŸ,e‹²Æ É!¸Hfb*€"$ù5Â×0‰õ–½»TºØË]Y«Ü?ª¿~mðÆ­Õ×o®tÃN|¿k-V@®¢cH’Ì“$Ÿâ•œ &òéLÖ3*³RK./õ¯v>{uýן]ù§Ÿ?þñ7o|ûáÊ·^<ÚM¾°j<¼X\)‰2æÇà(08b¢QÖ¼ Cö«D°¨b«ñ ÃÞÛ¶_9*~üÂÒ~øèýñ/ÿéWoÿö«W¾ýèôxTmSz݈DèPÀ„ü…%U¶h2팸R3ŽF…Óµâóûo¾xñ“ÇÇoÜY¹¹Sïy¢`§Ç5ãð»#¬‘YÈ7¶õte5Ž ÊI}mñòÁj+M^è/w?~ý¹›‹WÖ+àÜí…d#§™*«Ê +3¼Ñ¸3a6CÊyBÊY™¦ªš1–("£Ë](˜Ü¨a-W®l¶OÖë;ƒB¯”îæÓÍ´eªNC9CÔ\€E™4„IOÐïñDC!™Šµò¹õ^ùÒ0ýÜfúÖ¿z÷Ò·_ÞüÁG7ðÁï½±ÿÅ£­oß_{÷zçò²©±a(ä'Ù KH,‘†ÃOA2—HÕÒ¶E&¼ÓÖN­«É/^ßýÏû­ÿøÛöù½/ß¹öÊÑ•‹µ¥vRÑdVÍÊva£†aµj¡–˜ŒJ-.#Ç +q¾›·Úi­Ÿf—²·vû‡K¥ºAç5ÑEŽb$æðÓ“NòÌ<1à‡e\(Ðø™Iÿ_<=39íû‚EÙW6Å´wÓì¨$^Úo,½õÜê ½Ý^ÅŒQ$BJñ‚\XÍ n LØs<(ÎЬ"ˆ +⨔KÅ\JO[bÚä5)&I,/+QZð“”/:á€|Q™Rr0gMù $Pœ%ŽŸåRÙ + +è\¬¬X&«ÊMq1Šõ`_ð…)wˆšö`“.,H%ãÅ ½²‰ +VËc´† Œ('#0 Ò„çõfs¥Û]¯×—ô¸¬ÈA9^ô„°§'çg¤ŸJEåFL«óñ*Ji“ŽÉiW³%©*zÒÐÊYc±•>Z­lv‡ËÅK«Q#×ÎÆ›+o>ø鉹ùÀxoû9üÄÏ„›€90Å«¹êF:×¥)JáмΤE4ÅÃ[íÜÃë‡/Þ¾|ik4¬WMY ,‚`(Ÿ¦Ÿ0fÃc/d{W²íÓdvH3ñÁ`ýÒå`„™Ÿ ℤi™Tª’Ë7l+ÏÄd¿EÄ©Yè̅ओð¢–Òa9ˆ›”˜ñ¸#áä¢SxÆ2÷F¸ +ÓÂh B9‡Ó H{B ÁÙ"¥S´ÑÓGDüó3N‡‚°8Jé’Qe´&B0BƒH«k!·?.Û »* endstream endobj 28 0 obj <>stream +‹ +‘QTDpyÖ‰<3á âF”É̸‰ùµÉ?rúÂl$eÙTÔ$ “0„²§ 2…4"RQ1F)B\Ó2åBY«kwŽ¥ô@HŠƒ#1]÷ãĘœÕLÔ.êå5˜5Â8ÏI‰jweÒ=3˜ò3X¼Åe–¹ä®T`>KIùêâ!i6¦"roÊ3i¬†±x”Pç=ð3“n‰e“J5Êü˜áÃu¢’j¸=d§ƒÂlHÇ’œY—’-Þ(ÇÓMBH‚Ðj)Hša:‰«UJ«ÑzUHuy»IÉ)J+b)7ªy±¸;ª‡bÉ ™pD„ ® ¬ÉhRJ3zEËö•T[4JQŒ%hAPmZÎL8|ó>$€j´Þ +ƒlvÚO>;zrÊ?âCL6»NˆŸòâSžqO~”Ns£<§¦Š]8¦ú°ñödŒÏˆ„Ÿ°œˆôÔÔ¸ýu×bZŪ\Dåš1\èÃT93P³K“NÜä`&A©%Újùˆ„R¼¨N©u03sAnÒƒ¹¢]ß㓃˜ÑÒé >Îé¥Þf¤$°ü¨\ «jm+ÕÙó#?špÌ,vp>.-¦ZaììÈÅ­ aå©yìé9Ä7 ý«§fÎÍÁ”ÑQëûF}—O-9Ãò3OˆŸóq_;ã;3‡„˜ “XT ;¼½äDôÙ°|ÖÏz Œ±‚QùÙ `pæÎMæ½d˜Ðy­L ©©Yÿ¼+ E…6~žå 3n."yHÛ…™þäœPX‹HyLJ7o| +QKa>KêeÚ¬‹É^iédˆ•r\¦ä³à\,MZ ¹îÉö­÷Ìæö³ÂZ±ÔªT> KPô êÜx[ºAüé队´‚t–2\zDmÚSZtÀŠÓ}„ˆe6¹ †1³b’RzÁ®o“j‰ÑëQ¥D$zzë’Rß“Jë\jP‹åå›D¼éAeJ¿N‹IõÑx#(Ãb‘زx¹¿{/È&Ï{p$E¥²7C\’J>ÂZ¹w³:ã^âz;(T"R“Зˆá'®¨: f„cj™40_qYwˆe´¦V½„ î?^Ûk«-Xmú@c:*—@®ùP — +îè¸ÉvˆIE•JX(OFÔ3fmg„¹àBÜ°è#„ZG¥2&ƒÉÉ"B WkœÝŸ ò^Ü +Ära®ìei¼ËæŒ=7eÍñ#ËÙí„EH)‡•jDª0vÔëa& ,ÚSeÕý\A(]¤R#¡¸%ä6ŸÃ'=4*–µìBã2íASî¨ Ä9L§=Ñø„‹ˆð9!µ($=˜yÁCÎETDmâÆo4|°pnúÚ9ï¤èóÒvBªÓÝ°&GÈ\>ÕgÓC*µD&—"BéÙéhTï Z=ž_Ú:y”î^t’ñ ŸöƒõÍ-ì·sOtÆ_¶)åü`“&Âì\T*Íôµôð9³s·UñxƒI˜ÌjD®ûÙ<˜äQqMœhܨ1kA®òùm\· óS6¸Âèø(Ã…Èx¼ÆgF}ÇnXõµ¸ªV†ûp³Çkfï´ºu¿½÷b}û¾Õ½”\eô¦™é÷vï:‰x€MÐb2kñúazp=Õ¿¯ï˜ñžSÊlcj#"A|âv0šÍá•¥ÃG„^Æ…:pC.n&:Wãõ+ˆÚõŲn5t +rnDpG…‰ 7–½Q-DÙ_ð’I!3” +AÖôÅL?[@õ˜L¨Mnä%Ý°R\â“=0Þ ~:Èç ¹+m"±„ªU b^ˆçÍ:›èâJÕ‹j@ôbV_)nÆ+Ûñòæl@ ôNÌZ$â]p-‘˜Aô³ÖÛr~æ ç\¨?–ˆj5Òî‰Å¨Ö˜G”ñ‹PS‹6 0$¹ì*ªwèôrÔè9q›²újqc2ÂRö“i,à÷s©Å¨#„íÃ-69pF´)=îÜ‹(@6ÕòVTmÍø¨)9AÞÝ{âÌü×κžœÃ\D:f.p©!¦Ög½ô|#&ea¹É„²AºÅŒXèdóÐnìAñ)T«ð¹%?gãZ™°:4B³¥GÙ…6ÙžE$Âj…@- 3TtIJ^¡áçʱx]Ê XL/e÷ùÜjfñfwÿqqõy/“bg ë×TbàžŒðU:9Ò[Çz÷„J´y³¬•GQ½FYm¢„Þ¤]µ²%F’² £+{—ŽÛFÕwéìjX(|/´wvï]ÒD¼f·/%nd—ŸK,œ’©%?_”ó£Öö ‰æžÒÔÂZoç…Úú­Òèæàð“¹,ü§·Ö"2hZsãAuýÅÔÒóJý0Ìä‚tè6n5Ïx鹈Ö^eÓëré¢^¿è§Tʬ+•¾°E¥W2K7ÊëÏ»ú¤—³ Wb &¯‰.³$ä×{yov“’ ­µ0Ÿq@2ˆ+.Ù7šÇTrK#rÃÇ“µ]9½8ütÎÙ!¶À嶤ÊiÜDZÏ7Ž_t¡úyÞÏá„Öº—ÞƵæÞµWßýôGvm㬗EtÂZHŽž+<]¹ÃfWæͨnušgæ±›ñÄÒ ~¬æ±”ßš HnH%ƈî(Ю3^v2¢ FÍo›+_›ž›‹ø0Íî¨åÃ~*í@L7™æ2«Zug.,L»oéA:âòJe7Äd#l–J˜Ñ‹%‡éÞµêöý¨ZTr}PéR=TñÊ“lǬi52ƒ©0"ŒF4Þóv÷ªÝ»*äV!¹2•ýl’Ë )(UÂêö(¢´À„‡×óƒ# Ûbv èXÌ^@µV,5ä ++´Õè®_½È¦@çrÉ…ìâ•òŇ\qCÎô:££—¾þev°çŒYÑxÇjŸæ‡·Ë«wíön±.C'æ#BÞK›a¥àŒÙ¸Õ©¬>ßÙ}”Z8%¬.©7sƒ«¨Þ òy*5Lö¯ŠÅ-21PJ›b~ù'4Á•BO+ ¸LßCçBbNŒ¿o¿´wxðà,HÛ«­âàvóâ#¾|kmL*^ñÓd}ÛÕq­ÑÚzþÚ+Ÿ\{õ³ÎÑ+tfÙKsVçÅ7¿Rýó>ÎË1éõxóraå^ÿõTï +"æjÃãDuu"@9 Ñ{rí¸uñÕòèfutƒ2Ú¾h5v]¨éB ÊêÅÒ˘9ê—h×Vnÿš]½~HÙ‹d¢KYˆ/JeiïîÁ½÷<éŒœß klf ÑZX ÐY\mfWÀÏϸ˜ùˆÆeåòºÑØMv/Íâõ2f~¹ºrc:ÀžqàXµÜE"1 @u“a¡vÎÃêé…ã›oFý_<1sÞx&æ·ó£»£+ï€Z3–n½#e¨æÄ Xïû¨¤JÚ$/fï¬\þ ø*g66÷nú»D¼ÌÙ-9¿œéîç'ùá­±MÆ+©Æ&¸XP̨ÔEV&|n%Ñ9BäRXÈbg6OÓKÏçÖž·W0£®¥zK/ªåecñÙe£±ßÙº·}õñÖ­÷+«7•ÜRµXZÒ>ëŠ9`ƒÐz¸Öã³[Àœ‚¢Œv˜Ëbi7Š…qyHðPVD,0À¬•Ö„ÌW}¸JÊy€RraÙ¨oQf 88`õ"•^|vB+Û¿œé_¥a1?˜\È—Ži£†‰ÙÂðr}ë˜ÿîî˽Ws£[x¢å§­ÒòHHc1vÓætÌÊšUßÄõj„±”TKË6’õe­¼²שxÞ*/ ©î?‰)pŠ—6<´N§¤â†RÚÒJkraà#5V¯ìÜøúâébyS)oš@Uì6­UÖ®óùÔàz¢wC­ 7é숢™°`f6N^´OÎAa®0öéc©¼)åWâùa±µýè­Oo¿òÍy<á¦R°Òâò[Õ­‡‹×>,l<vuãàîk~?ÕÜžE¨µ›Ëˆ9Ì,=×¾ô¶Ù>ò⺜[³‹êî›°æò„ÖÔ:>î?¯—–[£+bz€iuBoy¨Ô,¢‚ÊÈ3Ý9f™AT­T/7×n«¥MH(¡b1YÛÐò‹°åÖµÖ%½}¨Tw@ õ ºë•uà1'½ä\˜Gä*™^Uj'Ju‘Ëa&•ëïvvnjrÆCÍÁ2¬ÔôÆ¥xq]ËX£9`"”¢G¡XfÚ/€(:çffBj0–1+ûéö52ÞJTÖ-D¨éKH¿àœ°A=¥¸¢s˜I³òÎ…jÀȃ™Çâ-Òø¢›²Ã¬Mé•ù¨„Ç+z}[)¯K¹5Àcöàl4¨"—VØÜÒ<ª†`—¨VºÇÚ9¿¬z€ˆg»™…#­²Î¤Q³áãÓA!¯•7A•0é0›&fÌhÅQ¢¶&æg`1ʧä\/Èh„”d¬&ð2‰ö!XT­Ár `$e-àFkfI½ ÖÖ²ýãúƽâÊíYXpC§—KÓ±oÅÇ="0¥‘éäÙþ!`$L,ÒVhõ9²@)¬ôwî³CP¿ÀÏ{I!5€øÔ¤ŸðàŸîÚ•åBk}uÿŽQXt 6µÂg6X{ Ø`Âr]Jk£«ÝÍ[^2á¥ÒBi×X¸%U˜Ô›E¤*0ìý­;©ú°ÿn2Ëvó¨sñ¥òòµL{Ϫ¬ñò›ŸU§^¦ Wwôθ­z¢w¬–6B±Tkpøá_þæøÁG@±ˆB&åÒn¢sª×. +é>o6Wvž_?} Ëe“ ˆå¨Öf“CÜXˆˆµ Õ2 µÑ $ülÉGççàñŸ­0¥d7·š7”‰I:Þ2`7ËWa¾80O±ñF{å&žÅ.xX@kùþu£º¬_€.(É…¿½yôÂÓ“¡gæP‘õxaË(l€º‰ñi6½\˜U?®³v‘ʨ٠ˆÕ¨ÖŒWö•âE¥´ /ðã`ÝI«…Å!¡ä ˆÖº]ݸK§ûç|$"–€’ÀjE«n@b!K™dTÊ™\«™²CµvÑî])®Ý[:~=³x9À$1µ(ågC‚3ƒ4`§j+BR×|1]N5H½:‡¨&;‡èóÑ8`*ାBrüò¥Ý;”QTLZÝXz"s‰êV{÷¾‡L@\-*–@Ök¥‘˜_òŒ_½gÆŒº4æB,¦–äT§ÐÞâíÖ9w쬛ž‰(A6Çù¥t8¦±fͬ­ÕW¯VV¯kÕµq…’Jtba.")±i¹°ZÝjï<"SkN4yÁË©™¾œ:ñøyuÁCÍÃ:ÄWÙô*Wؘ…D/¦ÇA= +ÑI™Vת\L·NÌÚ°„2-—¶p½ dÄ…iàóH´óÃÑá‹…ÅK&ä ØÞ\˜©5o,åÁP勽C»¼´1f5ùüZ¢uXY»£Õö˜=ãçåD§48YŒð%Ü賌¡’_É-œ¨¹å aôF—+í?ur´ |VyõV}ãù0›êçŒ(p,QéÙ€èF´Ù°ä§S1³ÍY #Û…Ç»ÉؘÕ×j‡n$>ë¥&ç£Þ°ˆ1VT­Ãr-È"\^Î/1 0øøýJƒÌà:°cLjÑËX·s' .Õÿ?·—ê]+t\ºŠÍ /(Rå ›ö³i,Ñð'¨³RQ+­ …žÌÂ%±¸Êå†\~I­¬ÄÒ}?“öŸÍç# ‰ê¸Ñ£}Diú™œ“°@†fZÛZyÕ³# î“©0›¨½ŸwŒgSƒòÊ ¦ƒË/%šû3X»¯VA<»0ƒPJ±xuÜŽ´I³C&€IôRfˆµB´)Ù!Ùž S> Ø̳NÒGe@Ð +ù5˜Ë¢T¼ÕÛn.á‚å¬VÃRT«˜ÑÆ•ê\˜‹©5¿È%[RÜvÏCæ"À•'Rõ].ÝŸ ļ˜¢ìùˆŠ•Xb)À—fB +èsïpÊÏLG@¸ÂXÝÊðvix‡J®<ãŒM‡$D®MùGDš‹AÒtÁ-µ°"©ôÂ"Ì%ü¤ê!ô ›§ÌŽœ[fS‹@Ì=¨b–G‰ÆXMÂê!zg +Šˆ࢘Ùõã&!æsý#®àDÀB!67î÷VN+í Œ³H5¯åûvs³8ºl·vgB2p‹ ¢lç ÏLE'=,"W”ÒF¦wÌÙ=¢Nºc*©×X{á© Á³³’À ƒƒM ­Öe®¸Ïå)ÈyqSQ1bScRj~®è¤2 ü0¥ê‚x­¸¢U.‚bª”¶ÑxÃC§æ1s:ÈFhËKh º%{§™ÁU.½ ZÈ,¢ZSòéævvá’/fz(ÃK› ¾/¾¼~ííâÚXväŠ*À„ªµRìÆ“@»ÄÌp¸w¿´x(fûå1˜ˆ pHTm€ßŸh]ÊŽîhÁødº¾*¦ÚŸ 2YTQ¥ÆX‹`°@¸Î91X(`ZƒKc÷\”= ~ÒLÖ¶8³õÌ´ïé)Ï”ŸÜKeü|ÑC§Á²B¤¡%»€Ð.øiÜèÆRC6· Î N +sÀö4ֵ‚ á=¸:‡Ø¢”ÛdS#°¬äšJŸ;ë"ç"ŠŸ)¢ñžQÙK·ŽÍêö³óX˜LdZAlO˜¹°8+ J’Ö "×ÂbqÎOUzveÍj®(ˆ"k.,Oú\.Ééþ”?7ýŸþÏáñÓ.ÜšŒŒ_ô£æ†ZnÙ…¨ÅáU@›Z}‡K¡qgNñ‚ ƒMJÔAÌ» âÆäÜêX0ÕfPÞn.™ù.BÇq­è"lDiH¹U\iDèìù9dÊM…åtûây75ãç&\Ô³3(]13ÒCo4>çÑãÝ^ù fÎxB+kŠ\ôàVNí©Ê8@FÄì>•\F¬aDm‡„¢ U¨„3,At&*×èôŠXÞ%˜4$€¢Í +.å¢BÒ›€ÀÅüjfá$»x Ñš³a`{óñÒj²½ÛٺݻxG)¯AJ%ÕÛ/­Ýà ++çü1PèÕÂ6©ZfD)Õ+.^ö`X.½ož$Ú—n!j#À伄 ³)pE* Ò;ra£4¼‘ížÐfÇë^ +Ë08äPNàÁÙÌ*> >29=XÄö|D2£TÿJ²’^¼®Ô½lX1ÕÉ÷OøÌRÄ°ÙEÔf,1§ß.Gêf®·uúr„Ï_ +À†CR5̤ü†\ÚufÂC©4c. r•ëŒ=>ýœ‡zz +‚éqÛ4XÊ{0ÝÕD‚Pªa.üé…ç†Åúð(^\šG$Úêõ=)¿J[ðã >ö©ÉÐÙù(!ÎÌD"Œ­W²ƒ«›w>©oßçRƒ³³_˜Îµ/ +©ˆL •sa €4 ™˜pㆦ—ðx8ˆ\ÿòÒå·’ÃËÙòÑiZ¯@<1ÕêéîÉúåwF'ïf–îÎx/$ +v›²:!6b×»h|Á¬_3k~Êv†hV¯1f“‹ã7P© ;‘Q½mwN„ì(ª“í‹b~è&L lþX†²ñú‘\v&Ak¹Îöó ¾ûùlDk ö ¢5Ñx³¸tj7¶#lR²[ÉΨÚtr„ƒU–«”Þ°«¥þ%ˆM"|V+nøTò#:ÑÐ9PÚλHBŒZŽÕ჋òéå¨PéÀsUÎ.“ZmÆÏz13Ì—èÔH©ì˜õýŸZ¢í|ÿ¦Õ|T2Ì—§#q0"'$ƒAØä¹Ù)åÔt—1ªRvâs“fÒŤ,¯×çÆ›æT6±€+u„/úpË5Î9‰s”ÖQ±<áŠùɤŸÎBâø½c~*9nüî +VUË Ahý_ã^øXñÜ1¤9‚⤗>3›¥aeé4H&Ÿ¼L{AÍÊ…I{>Àž›GŸ˜ðÍxwD Ò©gÆý97,˜ UH®DÆè3ð}S!UªÀ>ä:@|nÇò£Ù¨6’Ãlщóa:ÂXÉÆnïàµôâmToMŽ7'²R†X Ó1«Åå–“Bõ.``ˆ‰‡I!ˆñ› ôiÔÃHe‡Xj)&ÑÓ†UÆ5½¸ÜݸÝÚºÏ䶜ˆÅì¨õ`ª0¥ì +“X #/Fß5œ!ŽÑÊQ.1á‚\ˆˆ)57bFØ—‚\1iTHÛõµ—vEµ¨\%â äŽã­c–`Ý)%gÕ6H£1Kd¢ŸYºÖؾ?~"Ù;E• +ð8éòȪ®ÎÁªv¬æqgëù¥ƒ—*ëwäÒš‘U»ÕÛ¸iUVç!a&ÄÏyÛ´ÑƸDZÓž¨h5³#­a¶ÔÀkˆXÑ"±PШÉêvª}H˜=4Þ +2¹©Ö1Ê–¸Ê,ú>éÀ8Ù ¬€™˜G¢´‰²&ÂFq%fõ€{ò¢zT,͇¥§g¤º£ú¤'À-\®Á|98î)­‚²å#ì)/ jÁ<¢Î€pêà¢`–@šÈ™E)»ìŒšÿòY-26é磆5]ص-Þê¸ÀY!)@$Ý€èp{ÒIœwŒß r ÜÕ/D”¯M†Î»PGˆ64{ñòÅÁÞ« „²éÑtˆÒDÅ«¸RFnÔ6ðz'ÿ?{ïõ#Kšå‡½ êaw¶§ï½U••Þ…÷Þ›ŒˆŒÌHï]eeyw½é¾·ÍôôŽíÙáÌ.v„],EŠ ’¤'ýú“¤5$AÀ*õuÞꪬ4ßù¾s~&"*Ïüîi¿!5ôûGÞ`¨%t®ŠÚ¨ÀDQÿfqú-¸R¯'E+ppë?&›5Ñ-Ò +ëtìÁ¥;¼äÝ!€¿ÕZMοž½ü­1¾åÂi ©µO3 +Šf7áêL ÜxÉç)ÁÜ=—ÂMÚs0\FËÑú[£{'ù³"¡<+S¤Ö2’ó×D…–¬¼þ…Ñ: +GçÁàp€Ô­uòFnnŒÞU¼þÔ;ùÁ¾„µ­ò^•1n¾üã`÷¥$ëÍPcLZ)\w6ïä`ʹãþñÇùÝ/W_O?êÝSÔ샖‡—Ãͯ»+’&5ë|ĹK¹yT¢ýLMÍbFÐ?œ~‰ËQ9ý«–žœnžÿöèÕÍÎe‰ò$or÷ñonÿšÐ{b°Þ §¯Âùkwx^@ZÑe²yMÚƒ̨+Ö_›€{­3Ñì©v·Ñ^ž¾ù•7¾®«ÝÜá+“ö<\¼¯sA±.Ýèè+Èç2íï•„LM!€?¤V3h½Nïº'ß@ö*Í#Þ×ÅV‘ñ9S¢“:ßVƒµ7ᄊÁCY#RjU)¯ˆYfcÝ?/¢òYb¿(JÎ\t¦„Ò"ÕV‘[4E”6èÕ£yoû~võíìú»ùíÏ£åKP‰¢?·º§ ìá6<ýîøÝŸ/~ß¿ü%(Ð œÓ•£ ítíÁu°ü¤ßz³¯Çg¿Z\þ2ßÕ¥ïôÌΖ÷Gœ3-ÁØC·~þáïo¿ÿ×À)%ÚÆ_o΀IµäÄ™¿æ¢£îÉÏ'7¿•ãÆè.Nß·fטÞEÔ"wôv|öÃàüWb°…5'Ðo[³w ­&·­GY£öúýÑÛ?R`Ì‘ Á +ö‹0P r¸6Ò?¨œ”hÄXcöz~õóóO¢“,á”ÔK4ºÄe0Ú-³{Ñ\{þñŸN>üIn—”äh÷~´{ÇI [Ÿ^»òmtôl@Õ¢JÚ?¥{ZÄͪC°é‘Ã×íír¸¡õ¤H脘Í9*º”1&àO¢7wz|ª„+ÆèÈ­cRïÁ¯øH¯IULUP<¿6ZËêdËöüu÷èc¸xCCJnòZlÇ3'ž0Z ÓÁêMgõªsô¥Þ¹`üÕAݨº^RÕ+â:`šàÎÜÞ¥7¼eÍQ³ž8-˜ßŠn?‹hÏÊJEìÈÝÛðä׃ë_úý3”±:£ÓÙÅ7à?ϲ9Ì.ò-¦±Õ‡oÂé{„mìY'^Ÿ<ÿõAIüü„Á¶1Mi¨bÖî‰þÄéíüþ±ÓYñ˜³<ý(­ns|›lÞ9ýSÑâ9(Ø9iôÕÁuÊ`¾º'|cbvNýÉKspå¯6Á† ðôðàÀê_ów`¨!{Á0ÉNknZË—Z÷¬(ÆEÆÜI4½µ;»öâÍðò—rr&ÆǤ;­HM Ö!Z³E‰mís«ÿ\nžðî´q]j(Ð'=΀º¨P&®ªw½þ™\šs¨ÜšKþœ2G%¾É‡GRë̾‡ƒ2ïg1 RH ¦¼¼7–ÚçJróVïK|c¿&W¹l7m8oªwÎ¥ø}|úM{ýÁêòfì˜.Á—U¥$Oy¨Ôr{Àά=Í"j:+ÖC•ŽŸº£×%®‰H-(FÞá¬Bjé§Uë°ò#½}†Š¢L7hk Ž£.5‹”Åû+gp×;ýkÐ ¨gjrrÎnÞ_ÜÒJᘳz‡Ó¸UPQR½íÉ«ø—ÿ›M*Œ_a(F-¹a¬”ƬÑzcx®7—5ÖÇ¥v…öA&ˆ’ý™`öj¤Ž²®è¯•ö™Ú¾`ÜM3J˜V£ 3ƒ&RJ”ߘ¤Wd5WÌþó<}‘@³i•ß½¨2^4ºv‡Ïåp‡É©í‚j’½¡¯›Ó[ŧ~b´0!õ)œÙu’]ctÏ_¸ý3³½1“i´Òöy”#û 4F²•‚±èµxi$ÇÖàšµ¦¼35’ÓÆìåúõïÞÿ}°|©-xl"¥wSSf {Д~¢×â…Ó; Ç7 ÀȳÁŠtçXªÅWöä5pnÔû‹ú@‹BΔè©œþ,2e¶Œ*zsQã#%¨ÈóÓÀ§™º‘G- 0„ @¶5:G’7ÿiŽyZàI¥ƒIqw«|E¥Ž;É £u25©H˜EÒU&7´>@ø!Åéåòüø(ñˆLë!J‚ÚÚÙÇÍ,nÔ¸†EÓçfrÌZ]DtµjŒ%Ø ¯dûM¸øR 6r°¼Y™ žUÄCTG¥6$ëNP9H…„Ñk#E¡±P¢ܪrœÅÍ2ãA†à¤8›µŽ–zs 2ÒêžC‘‚e0{åŽnÄ`EÛC€A¨2 +pFyÂDy_ —Fk ÉFk‰h§ŸÆÙš= EQ&Pò¦@”<¸ª`¡+àDø­Ü˜ƒkƒUmo¿ó¦ï -£;0 éáîÆü{•oPÖ œ½ñ&¯…` N¼.·p‘Û‚¿£]ç|¨£ºØ­Ë ÛÇ%&Ì¢€ ^sz%øS Ð +koòξôÇÀ•Û•I·Î†pc´¡\B2bäV‰ñŒö–TÛ?ÍÑ%Ò£ô¾ÛÉN –¸Ð>¬š¼!G¸Þ“ã3>Ø 6¹ ©’ŽVz{Ô¯†Çîàë.µÃZ=)˜ÛóÞö›`ö–2g˜:ªËRŸé *·(³Ç{3ð¿ø€Eðú ®AwAÖ…‹·@ ¤> à](£B§ @)¥ÉšµµrFWýÝ7ñúKÐ6ŒÙ“¼±êz«ç“³OÀì´7Ä°Ú[ÉRj¢Ô¦ †›Hn—ÖÛ´ Œ¼…T¡”¸L˜Š7úçf´œ‘ +‚̓ÑÃuÐÕSÐ<ˆ*ÁêÂ\Êi/³#PÎr¸púç„Ñ?n¤þ+öZï\KÑ p*¶À0¢ «£Ú¾‚bdœ‘Ù;+ ÑO0À`t§„kà©a€Œ§?¹øazù ÊbJÏ_BQj ª H»Ö‡œ™ßü°ýðwÖà’s§½ãoA%¢Z§&5«B à–Ålɸ½]åÞW¦Í¡pÀ yÊÍ¢¥´{œ«I îÀÒîRwîø9ïO@­eŠ žX"l->“¢sÞ]3ÖÜhËÞ$[ ˆŽ ñ^‰L–‚Ň‹oþù‡ßÿïíåǽ’Œ²Ê…ÙºtœGÓËìoÑÝ|Õß}­67EÒ]^|o%g˜Ü-0aQì`&¨”Ë“×?þêÿ\•1Âõ!ïo‚é;->â¸ïÖÑe¬e µÔÔÔª¦Ç4Ôæ<>l”vEh³˜ÝV÷®Â·@W×…4è"39z•3Ðù ÆQµÃûÓxõFKàõLjÔ%”„·z¼Ùøb´Xi.ìáyzõïø ©ÒÜðV_6ÝïKþ4Ós7­ÕÛpòœT;¸áRõ'uÞÊV‰££BÃê\ N¿ƒE]ŒJ¤#;#Ý#ŒSg\Θ³pùÁ¾¢¬~)m²ƒ.E•Hæ”3c®*Á%`W]njÉqsõ®1}Å7Ž èÀjñ9x7”w+´i´v­õ×b¸æü©šœä¸è‹‹ó¡-k´]Âðãð¾€üÉæ+N”[ÒY=—Mm¨Í^ÈÍôdÜöíßœ~õ÷Qšu›£Û_šíÔšý+ØhÎYöLt§uÞ‡bÑ¢¥ÙÞŠ+‘6¨8Éž±Î•ÛOsäaMÎ ZŽt‹LÑ.·PXaoÍßpæ°Ê4œî ïKícb³B9{b¿HCÚpæR*.>úxüñÆw?öNæ ž—éæ“U[Çœ?«paË@¦úHh,R OªR“rFؽÓÇÔôqš‡z_ôæ‚7A•H_Öì½óñî#$aUl Þ´Ì7@qö\ô¥`rˆ0Ôz¼5Ä @¶ÙCÎ_wwßvN¾-†r£Ô¨œž­À•6¦tYÌÂ¥Ñ:þݯK9Tì>ctp-á¼%XÈÆämûè{ˆ¨„يȦ¼ÙK;Mxc.\J­½s©ÄÇu9*RºÚÚXƒ«ÁùÏ’Óï„hKÙ€l=øë§DF}£{Õ=ýEïô™šX$,ÆZÚ¾tiNäö%cOÁêÚ?+0ETAX§LèTznqÁûKÒ‚–³:[Âh«Ñ¼µþäOßziãËŒÁGãF×lų»š€`v‡wF÷´.wJ„{ˆÈUÆöG¹ÆÓC—šF|¤†GˆÒÏ‘^Žtr˜„¾W•@™àJ:DôWj|*G°/yÜÈ!)ìT¥JÚL9®‚J¿os&:K»s][Z{gvÏÙÁœî!Ú!æSÖR —Áicp’C`;@š°­9ÜÎâ.ãNÔöVkŸzƒ[À"àkÐrîè¥×¿2›+ÉèVH ÓGÛ ¨K\˜£\¸U…¦Ù:ŽæoÃÕ—ÚðŽòÖ°Dzm"´@0¨Ý[¹}!5O`#€—! ½5Rá‚ûOHÀ­Öv³Î>°éPDà²9gRƒÌäSe;8þîäãÿ0¸ûcÏáE0¥Í˜}À´Ûéµ¾_ªÝs, ¨_>C èÍš#Ö³ÖBoßšý7Þø mO +H +¼³þØœÐÚ¨ +¢}¦D;0,j0?Ä 0þÍûÖÑ7¤³¨É XTÀgñÖéžJH›]£}¼¾ûõéû¿SÚÛªW¤@×jë¤*Æ €;ó§ïƒå'£{‹ÈÉAM(‘©udÒ+xÕ”­ØX ”j“AÎÜñ5m¸æ‘?;ý÷Áòc ä¦03ÚúƒBc­&ç l®ÙÚl_¦aU=˜U0}atNa&¬3§­™Ù>ý¢v1c¤4Oz§?tv?Ó’KÚž‚;Ü¡ÑZ£³Îñ{­{ÖÞ~_ÿb|ó¹u •Òœ¾LŽ¾fƒcÂœKÁZgdC™Ä%J#TØý³ÞéÏ·þÁ>HÙÊ(ÁZ¦b0ô§—zï–÷wŒ!ÁÕN7!„kðV×HŽ@°™Ý+ùJp¤øë2ågêJmJ·Ê÷µ¡02´ÖNnÒÞ+ _⪗äÁäU:jû4^¾ím?iÍM™ÒPÁ §áðæIžð{[‘c-Yk +r°7‹i‚·ü·{7iÛåèS†&6ã pÙ6Ø+ ˆÐ„ôÀ)Ð_•oç1û ,ÒR¬xCÞ‘öXL.û—¿Y¿úc¼ø G[(%³½\þ x³P,Ùº4û·À%>€L`AkrÓY¼è¬ßÃÚé&¼r…‹¼0µ,¤3³F¯¢Õk| Ž†…xU ñmX:xA¹yJ;+ø>KÚ%ÆÅÔvú÷ “îì?{£Fðòþ°Î° js§µÎÀ›«ÑJ UG5)PZÛÎñWË—¿Ý}ýϵþMŽò@N”Ià &’;D„ Ì8À5Îð¹7|i¶Ïsˆ\@•ôð 5Â9ª¶Óìþ­Ñ¹¾M¯s°Fbt öÍê^öÜ–Ò¾ÒZ'’7‘ü‘N½ÉM²û°{÷Ç£wh¬>Ö¼.wEtÎ k8o­'×igÏÁ5åÍ*2ˆmƒR[NçÄO¯×½¦kRpänä øT +g£ó¯Òã$| ne>[0§„ÚÃ¥8WI5ŠÆWþèÆêßRæ¤.$U’³ow`—/ÒV8Fßí_é Ö_br?‹¦Y ~toðAÚîGíùëîàŒm™ó l›÷v¼ý‹guX(»wæ/ÞƒWR°©Cf–Ä2íƒTà *6±­·.ôøœw–N’ž?)6¥vAuÖU¸ˆ°ç@.¤œ°r›ïই‘×’ão¼Þ…è-xkBÝpt1;ÿˆñ jUŠ1cÈ…G¹“§5¹ƒÈÉ›ƒÈ¡ô&›ª SÁ;!µiMìÀ[dprÆŒ· ¬a]O˜`®>M^þ÷Ñî{Î_BA¤£j×ÜõŽÐû¯oSSz•ôÚ•>©'i‹ùðø–´œ® Á+•Xl>ïÍÃyúÇ× «B4:ýÔÛ½QÛsÚÓàØ[½{C‰èËœ[b,=½<À/n‰ò«LX¡}ZKO0n_ GóÛŸ›ƒ „Œ3$\­ò‘Ñ9‘¢£ºÜO/ýê^8ã—Jó‘›`I-Ö㥕ÌÕhbOlî&×ãMßÃlsTc]µµeÜQ]i¡Z⌯^ý¸¸ýUcp¦¸Nïž|Ã6ÖPPu© 6GmõO¾mL®+Œ[£}BîArBµrá¶*w¡Xr¸.¸#¯w‚ª-)ÚñÁ‰Ö¾š]ý`Â9SÆ G ªÉ§>¦ '§zû’äI-’–ÔX‚zÏS Ê™ÇÛ¯G߶Ž¾Y²W‘÷ÊИ/ÞŸ#Z=níÞ¤½àÌ<j2 °*X¿:äX÷O +VR›[56ýÐRëqμHxyÔ®ñ!Ù_¤»a±¹_•foõæVôç”Þ­sa<¾#­Q–ó*j»”þY÷€ñÿؾ±'¸c2ÁŸ—Ùøóª, Lzá7}A¹PžB¸’¢ãî6ý(roùQ&@”M?РC[C˜¡Ò¾Õº¯”Ö¼¡¶°tê^®^þ!\½—Û'ˆÔÊSla‚%÷Ë\r%˜ìÿ` /±ô£!lÐ'4è«Æ¼®¶s¤^“ZF/ý£*Ö<-ñ5jPzé©F½WU»¬¿°†·l°:@-RT¹f•isH[x:篳÷”»ÌvŠºåŽ_ƒ´»'F ð¤iFó`p!Ek9Þ¹fçÔ€,¯ +~±½áÅüæ»Ý»ß5fÏ‹¬ *¢ÎGV|Ï^¤ç1õÛ¿ &¯ÒkÔ»—Y\ÿ<¬kû7-Þ¦— JíÖüÕý% FiMìãl ¤Ý½°Ú;xk1XÖÕ¤®s\’£cÖݶ¾‰¦w˜ÔÄÄÈ߬žÿæâã?6ï`›2ˆ‘ôS{Fû @#OùRxtûÿÞþžŽA«çSnlÚ‹P³ÿÝO+_dYʘLßþÃâÕï­ÎÌÖAlÌÊL£Ì6A*°>@Ù÷/~ñ?{ã·rkW]­õjJÕ‡¤³b½-Ô¡ ÝÞ™Œ«¬Y‚gš´:1âËáéϤöq÷ýÁùÑËßÖˆñf”7©¨] emjûŒ÷giWh5á›Îñ/ûg?z“7¤Þ D)m#Zùýs£s\“C:Æõ©ÕyÙ\|gtn³¸]fƒô"×_û³„;-ðAvkTƒºœ©Ë„’x£×áò£Ÿ”8¿®¢R nu­&¶Åx®¾Z½úÓðæG­[aMÞžI?S3 +éÅê\Úƒç_ÿ‹þÙ/«J‡ÍÅÛ`þAO.p½›c¼ãË•Û½Àä< }ȹópú:˜½ÃŒYzF»ÇoBš}Lï¢Z,cOx{œZ!ÒÁ”Žž~ðEúáðc‘tÀÅË –škÊêóÁÒìß57_7fojBë°näP£Ê4J„Yç\”ó²Ué°¦’j×jŸÉ9¸¿¦Bn4ú æ³u]O¦/~o_ªÍsÁ[§kÒÚôW˜è|Q ho#'wþüëþéoÔö9Dý¬Ä7zg'o'øã<é|^r”¯%W –´¯wzFØí§ƒ6³š¸9üm÷óå‹?àjÿÉ!S&=H'.XAÚï¥"Õ•.Àê½äI™‹âõWçŸþÅðâ×öèUžný_ÿ•ŒÕÿßøÿj<òÐÆc m<òÐÆc m<òÐÆc m<òÐÆc m<òÐÆc m<òÐÆc m<òÐÆc m<òÐÆc m<òÐÆc m<òÐÆc m<òÐÆc m<òÐÆc m<òÐÆc m<òÐÆc m<òÐÆc m<òÐÆc m<òÐÆc m<òÐÆc m<òÐÆc m<òÐÆc m<òÐÆc m<òÐÆc m<òÐÆc m<òÐÆc m<òÐÆc m<òÐÆêÿü¯dÄ™ÿJÆ[(8×[{ýrýÙÈÿ¬`4˜%üÜY¿xyüü³ø³Â3ž¿´ÏŽ^žÝ\¯Ÿ¿ËˆpŽ’të¯/3å??2Ê´žŸœ]ÃÝ£õåq%S‡‡Jðï3C8Ž§PŠãH–$†æ3ÏÓ(M1 ÇQÉð™«ÿÇÑ;:îxff6…;‘ûùóWøé~qæÏKÏdæ' 6}úéÑÒùüû'ýûÿó +æHp÷Ó¥3éñÏ +…û¦é,fXØ9â³Qã¿¡Œ~M¹ÉÚCÎsÖÒ˜:dÜl{BªÝ?w17âc)Xñþ U“ªU…´/iôh£‰‘è ”pnuvjûX¸`C{+©yV›yLÎ#B±³+EK-9““s¡¹£ Ä蔸†Ù^ËќкJ´³:—îðë¯mÄ…[Òš R»Â6HsÄz Þ_XýKÌep3mA­÷`òeÚÃåD6ÞðViïØƼ"†¸ÞAµ˜q{EÞ¥=¼q§oëo”Þ nªJ«"FNÿ:\}p¦/íá]cö^nŸ“Þ¬,F¬;¥¬!"· +Œ[À Tð %âìcò„ïEêòЬqMnSö&F[CÁ›ÑÖˆ0ÆáøÓŽ›Ó’ÐÌQnöjbd´Ž•pUá¼ +ãJÞB 7¬5©²Q‰tQ±){³æä¼#.·gV$œƒª\eú#B\Ûœ·bý5í,({–%Ý bWº„Òå9¼"´(}Tå›yÒ‚C¤¤&4k|$ûK5Øàj“<ÝxV•÷ªr3 +˜AHMFë°°†l£DZ˜dëÒÓ“Åõ2ëÃÓ«|LécRí£RR¢<ØÆ™ÔÅf¶.W¿@8YDGÅ6cÍ(}ÂÙ ±±‘ËÁú­?<«0¦t¥àÈèžÉÍu™ñÊ8¼E‹Ôa¹V¸9.qÍísöÄJNæ!ÆvrÂy“š—8S¡±ƒ#Öè™ÍY‘4 +„Á¥}^/ÅèŒö6ŒDÛÓ´7¹Þ­Ð6¡%ZrêÏ_ýk¹û¸¨ª°Mý‹7H6o0µ)mµu.¿uFïwYW:Õ´hXム+çW¸´ a àWˆÒCänr³XÚV°Ê7Ò6KîU;„9R[§Ù‡îѧåÝ/0³U•róÈ¿ln¿s¦o™pËø+ÞŸ<ÿ›ñåu½[b}­}‘ÿ,>1-ÓØ jVc\nËßw¾QÓŽžB‘©+YDKÛÜš#Xm™ ZÕ¨:ÀŒ1a J÷ÍoUÒn¸.¬pA‰N›«Õ¸¥%¨UX¯D:5ºQ‡_‘.$!u[£çÁàòê°"0ó°¦ì•„"é Eæ”÷cŽ«cÙ?Qƒ]‘r¡‰É mŽ!0ÂòÞÚh_ JûI…ûi‘†ÌݹkÁ•âge¡ˆ;56ýpö'ê°*BÄZ3ΙSJç *Á40©•ÃôiÁ +TùV÷+lYŠê6<ÆÔ¤Lš5Ö?„E¨«¸”8½;oðÒé½¼eпùø˽~þó¿8¨)ñ.m¹~Ï«ãßQ»Æ7 ´“~Ì7Ó€ÐZ[5ZÓZ‡×úG7¿ÔÚGEÎÏà:£Ó÷îä-môHµYÄÍûVO[Ò¡Z¿®ho­·/üñ 9X£B G«`öBëîhoÄxsØ_!ÜZ½ËÙÅ÷fçWÛœ52»×zÿŽñÖˆÔ¤‚%•vM2¨’'LÈ17ңʱ„6dÌôãàKŒCªmÖéו3úVÿ¶þÛÑÕo:§Ÿ£ÅØ}­½U:;©sB7ÖÖðUwû]4{=Þ} +¦w•´KP[oŸvIñ aÏiÿ˜L_¶'z‹´,fæ;G8„žö;)Sî^YÈc&¡v!“ól#ÇG5m„ ÒZi½ç¸=̬³Ò:/±A·ŸÕ´Ó€­²^]¦à+"6kL£€èÙššCt€…:êÁZtg™šI÷ï—ÅLU>¨Ie6 Œ‘Ü<ã'‚l´®õæy•k²æ(m§tQ¹G[ Òœ+ñ¹?~CšÃ'e6ƒ(ƒÀ8´9ˆ(€iWñ¸æ“<ó$Oqã#Ú˜áú¤Æ·ë\ÌZKÅ[VHÎ0‹iÌÉS!© XØPÌZ¢JB*-LŠ÷+B™´ ¹#8sx­{fB…} [ü—™º.ÜÁ$¸§ëJÓúÊä¯Ü¯iP½[KÛ¤uŒ`µ8ÿÀØÐ{|Ú‚e‹éÃ,åh7í¥mÌd—6iÌH{D¹3Ü1ö •“ +íz½ódû¥Ñ=qú'jç˜ñ¦RóHMNá+ Ì«¢ÑÚ>:$ôd½#@u€ô4ñx·HšØU>‚|¨s!¥tq©½W ¸Gôgá;°ŒP°}U®Å›c/9±â ¤t³ +¤Ÿ'ì2Ó Œ‰]Ùý;½}RáRÜË!êa]Þ+ÒûeÑzÉô¥ÒÜaæ¤Æ%W9ð™sf@”r|÷£Ú×úJ°a +IkÌ[=¹²;g‚7ÁåØŒŽºÛoQ¥[僺3Î8í÷zÀ™Aqiñ‰?¾EõÎ>¦×%xñ¹ä¯€åö0\ÿêwÿÓñûßÕv]™ÆRlB²Ý“×ÍÅ[ÎO{Q@‰Õø˜Ð'Bpª&ÏÕäN‰/¥æ ÔŽh ´Ö®$´€¬Ë| ×jóDŽO…hçnw/~ìnÞyŸ¶ÆVï&Z~­?EëZçÔëœ÷×ÿÒÒP-nLqs"56Zx$¸³T¥P.l7“öíTøܪ,ÀK ‡jÕúOÊt‡pb´–6Ê^•k£R_·R0Z—³¨]zbt©%7jëRhaÚ2lMUkbUŠ´CëCÞšòÖ“ +”i˜¥€éU¶Ü—E̓º^ ¼îBjÕùf™mbê~Müü;¨ÉyÌ.S@£MH×´ñ6’¬¹‡ÈYÒ„éAÒ*Ñ)ç.)c Ë j¿Â€3û1[׳u£JG”Ô/ LˆªÂêYIÚJ{Á VÓŽ•(ˆ=¾Y"`"LíAÞ +ÁR…µç” +»uPáŸæ©ÏJq}Õ÷“³¦ƒÍ×9 03ÍØ×ÇrtÞ˜¼ Æï@\‰f»98óG7Œ;£¬1¢ÝîÍ hÎhù¾ÌFJc&z“ªÁ+° ü½#ÈhùÑß®±‘œÀ.³JÆÈè\4¦/»O_þâ_‹>XJ­S©u–A7jrN» ÐoDU.NÑÛg ¾xo#ƒÂO›Š-N¿ÿPÚ!¡¬©ÔX)Ñ‘…m½þugñV‹·u¥ÍØcÆN›’:×áòK¥}â ®Þ~ÿOþün3@ÑPMÑ; *d2 @MlÃ÷9ÜÄ„(mBlç@}L[36°9LË¡j‘ð@6°ö`3œ¼I{Õ#ä *¼÷ÓF,ÀÔö‚Õ­ëBr€¨{.‹ÈE¨²>­÷öL°¦²·® mÀ¨:ª²yˆ˜ÒƒHÉ¡f3ë|ðX¤½CDÙ/²56Ì£ö~EÍÔuÐ ‚·†¤MžrÒ~ÃZ¿ÂG0gÆš‚×€4ƒ§SzŸ³&EÊ/àVP5aþU&Ñ•C"fK>+±AU@!xåô+Ì¡Â4 }ô¤·NQ¹òž4†” k;Æ•¤ˆ[«0IRé b´¨Pƒ”9¼ïËÞÌ¢jj $µ>ì…œ„óOíí÷zûœ·†Š3˜í¾ên?ý±îŒófJsíöÎâù+¥µù¡…+%H;Ûyý[»÷Âî½ÔÚ7z÷Žr¦h¢9Š/‹œjœ6'ñòcÿäg£«¿\ý\hoÀlöŽ¿±†we¹ƒèÊžÚƒ;úÌší`Jr¸JûVòM€>ÆÛýüz°ûyEÖa ywÆû„§j¼£¬ °',¬à/@özßÜÎ~0‡·zï"\¼BµnŽt }éN$KYe.µ ¸Ë´6dêš` ¼Þaô3¨ +ÜD@!Þ]!L|+ w’ʃ~îP挻f ðiH*.Ü«©Ô,q!¼üXãÚu¾—ÃÓ®H |25d<ˆØkFjM8|!z›2Û,÷q1© IuªåBù‡ˆ +¤Y¿W¿ðeÒÅåN‘ðˤ_¡ÁƒIì•ÒÇ´8wË™ F[ÞIUh• hc¨FÇ°†ˆØÊ¡z‘°¾•CLÈ +À1u9Ä$DøÐl¿*ùInY°Ô„7¤€ˆ:RJdw‘%¬½Š°WdJb• €Ž%´›ÁtX4D öËôOökxÚûjµÀY€ŸK)ØHá†0û@ñ¤ÜÖ¼ywõÎèžë©ñ íÍq³jYkN9¦¦Ç +w„ÚƒâAYSDéØUû¼·p:r¼É36itYw¢Æ[!X +Ñw¦¨9´·£‹_XðW´¿¢œ™Ò:1z·Jr%5ÏÀnÃã+BÙR›Rt¬wŸ‹ñ%ám(ïôvžöP¥#4–¤Ù³ Z€SFû†Ô{eÚ‘‚% NïáÖ·'àyÁ±­óÆèeEŒÊ|£.Çu¹7Ø»Ô:Äm`(É™¹³:”a™‡í€×§í1˜îæ”0 å#Øý["mÚ˜*ñµœñö‚VGù´Š%1{Uñi™û¢H—ið£¼0lÉb:øYÈ°°æ¢73Rb¢2€qà “”[Š=Føà‹<‘©Ir˜¸ÚA%Ð -Lh+Þ¼J;Ÿ¢‡5)âÒî3Ö®«Ñ®Ê‡À¡ ™Ó¸jR<`‰°á-”öËÜÓ"Bî°®×¹*$)N¢€“a×÷«Ò!X Dp+‚åpÇíÑÍÙí¯÷jÒOñLU¢äžj9˜ ¢—NÕ+Œ·W¦~²_<,Ñk`~‹i+M‘›`µ +¤NàT 6F´U¢uULKFŽ¶„3Ê‘ª„ŒÙ‘ý‰.áÒÚÀr©©ÐóˆÚ¡í™Ó¿s7bcY`Ü,¡cZ‚鲂#º(\~mBÁ6-±{WÖà6Í")æoúesû½Ô½J»á +éÑÔèx_Œvzÿ•Ò{%AæØÓª—¹ÐÒè^äiØFÚò ì’ rÅ BéªÍ S‘…ÆÊ™¼Öº×PØ%òßW¢Ü:) +Ñ¢BöúAõÁZ1æˆÐz° x!ðgàâó8l"Øí:Û ä6Ô#ì8ì@D‰qaIK”vWúu>LÛ«H©ðæÃ*×øs§½ j@ò€‡¤Ú¯*YT»gºn¼’§Ë ŒÕ¾Qaœl]”¬®.©ó„›ÃýªB‹”»%Òúéa "VïWÀÚÓCH Ü,SoϵÖe‰m<­p9ÂdÍ!`NÚâTŒ÷JB¦¦篲õ½ +“Ç 0¼„œ1ã ÄCQä1‹HѦkÆ;Þƒºò´Ä}ž'Süä¤ÞýÙaz¬/äí^‘P÷Jø^Ù/Ê쩸!ø5BµÂYÚ³–tõR½§wó„ŽŠ>kö +´üáWX¦ßÈÁšq'ÚRš ÂìÕ„ +$KZÆ‘Ì>ÈÍ©öíÁEIð`ÁKtÚ·D;¤–8ýK³s®F+wzKû3Êžp5¢Êb Ä›W_bzŸµÇ ‹ÀÙqÑqMí–ÄXÒ¼p§/Yg:Mhù GzÜËÛáö®AhòWÂe‰ 2ˆ™'0Du>v:WáäëMëŒÏ_HíÕ!©’VŽqáEp½×;ú¦sú :Ø€½rú˜Ö.3 <4HØJJ†àòüåA],!Ç{v6«.CáÊ¡2è=?‹‹u)>ªqQ^ós¨“Ã@ÖŽ8sT¢,¸ÁBÔHÅ2í‡[@…yÚ{Øž>oÏ®ö +h©Î°²Ï*1&*B¶4j\@)IgõW:¤Ò%å¤ÆÂv„YLÁD_ <35­ˆ{÷ïz´óÎ8ýª÷»¯Xwô´,¢Ú!l²î¼¹ø‚ÇNÞý³B).µ(@°´yüRKmn’ñÍñÛ¿šëCÜ*1>x@Ú[Ý›ñåïš›¯q3UMæສ´ÊBÂõ—Zr.¿²'/ÓçÿΞ^dŠ„ Š|¤¢Ú(\êpMrt£ÆŠ?‚oöa+ëJ®®Â+çP+‡Û ÓV7¿ÈQF®ÊS|˜Cä½ì™ò)ÝD•ž?¼1Z›L‰&9›UuÚzVâr˜E}5\ƒmçïûþ¦Ö7Aa‚nÏ=ÉŸ¤Ö5‹!¤Ú‚…Þ¤Õ®×¹UÏØCyL ¡5[æ¸Öç«GkHžçÐ0æœ1‡àY(£« ¦ÄÉõõ×Ò’“Ø|© FFmlŒä<˜¿f)椒Ãy†±«Œn Ù5mßÈú‹ÉöÃû¿ôßï×dέ›B?ÄMÖÙêÉ­Ÿ;Ó·ˆ=GøD ¶@èYÐr ÞT:h'³sVâܽ +(ÄØ u½šèf7{„ÞΞÿòÿ¶*úO +$.…nï„Ô:yÂÚ¯©÷GJ{V'í› \ƒŠaó0)>}›ò]àÄÇ/~þOU¹ù¬*¡R Œ-&¶1)ú+QžÛÙ]¾ûÃ`÷ñóù4Odk< +sƒ²­kàQ¥ ù‰ª è:ÐQEºNí°ÊC& \¤5Övë˜s‡ÜÆœ|ZòÀ l ™CÑjþ@*-?9öÜá”%J¸Ž²X6ÁùƒKÆPæ9DÉ£J3óˆ>Ü"0šæô`X”FZæö +L¶®BD.(N7ž”(NÁ`õ_>+ïˆ2aRÚ”1'€Òð˜LE¨àf¬k‰=¨¨:0¾ÏmUúP’OJÜAø×"•'.µ3µ4³À˨†Ë.5ewï¤ÖŽ0ˆ#麦´ ©xwª47¼fmŽ©«s挮ik@›ÎÝÐNŽorHyYÜä;l ¤™# ÊèñîÖYní½GÙ£ŠBªk=#9k®Þû«/Õþ5f‹,À]”šqÒÌ’&eA¨SÖŒNPcjuï¬Îˆ«"mÕŰʺ°n%*(3àþ"0€yÊ|Va¾(âû†€yönüÉspÏó˜ä"¹P5ô³2™Ã4X +-:ü ë.ªBœ8ØŸ<¡!÷§PyœÑÒë?V©TqAAÁËfÊL¦Âeªª±np÷‹,@بë2W…6k¯¬î+ÜfêJ‰4ª`ƒJ4L;ªð-Æݸ“·€°éˆ0}¿¼¬ ”S!lRj5ºgö"@àeùF1µu>äv™o’zª‰ZO²h¶"alXcÒ6Ï kRŒuªP¤5¡mÅHÛ(T$¸L¢Ó=À= +ŠwŸÈLU<¨¦Æ¹@*IJ·*N1tgñ¬ˆdÊbúîl†ñY‰Ï"Úazd"r¾ÍSÐ*:(`V¾©Õª°ÀõàŽƒ€a•I‹“@شʬ Æ÷7R|Fãzz¦ÜV‡±ÓCy¸0Ƙò-©(mõ|ZÆÕ\Ô ²ÙÅG 'ae(s”ž¢%ŒLº§òaU:(‹YÔ,ó1¦ikQ{U±G™sÁYÔ8xÖ +¤8Ø^°Û¤1® 1­õæi¤~UĹ£Ý¢U‡·¨ÜCŤÊ™4at€BíÚˆuæf|ª›<¢ÖH¿€èu€üÅÓÒ“< ìü$OÙíߟ²Ä„Ö!bTÁk ú“Þ$;yÜ+S¾×9‘¼)TÉG€¨ Ÿ@VeÈ Œ<;ÏŸØg6[S¾(0•%A’•)¯@Ø°9DK9(}ˆ®DpiyÔ*>­Š¤ < DF˜·æfë “;0Á£+TÓïðÄƉÑ}L?Ê­sØz¨t¯ÅÔH@]“³FJ°V¢ΞÈÏ¢†`ŒÃámÄ3 PÐÔ“Ëäø{gô +Õàþ´Îl ¤[f|kü^í½À¬ liŒ´Îµ9| ©Hi‰-ÉôüÚ®&µò¤ƒHmpˆ‚·z‚M½‡H +ß.ƒwcîL¿bí)€-i‚cMÒCvÎ"ª\˜­ËeÌ’)©·ßÊ´#†k>Hùº@ßçøÜè\WRätôl`‹Ð†à Á™æ /ƒèXWÊzV$Áøçi/‹™°tµôxN¸²@xOr4$0¸r6€áE*,Ò‘ØØÙÝ—FûŠµ&¢3Ç•Ö^•š#mJï‘j›·‡Z´«Rƺ‚Ñ?(1Ïrd®ª +VzX-H-Û*€Ø@„8›=mÑêáZ¤Øƒtú‹½Ê_>-g«"$ÆOÉŸìc™*h¼ô &(±f#brP×¾(ð„:D¥* eä$ ;±V¸AùæyúIŽ„D­P>.´)eDŠÀ(0ÀŒO‹\ ·Íö ”Þ!¢ý„B†4Ëa`–] Ó´O¶ÐBÄö¢ƒ< »\€Õ@ͽ²TÄmÑ]ëÉy†Û´5a¬iŒ9¦>«‰˜†×é_±Î¸L{ˆÔª )CUÀ<âH\Áži#9\g¾.x„Ñaý)奧ŠD©Ç;)>EÔô” +&·J¬sˆ©YL‰…H ëoo…ˆ½õæÓâêû<ëå;=ò µÕ`CY ¦¢ì}DÍáu(˜hÆ\jQú€2`ž‹š”0úðâÕßœ¾ù]UjÚNÜ£•Œ 9¥ä¸NÙœÙÏ’ÆfUY{f&€«jë¬Ê5x Rt¸_@U¸¦ô@iCæ éq!ã#pýû5%OÚ0gðø’=í©ŒÊt:ã›yDk–GmZ¢©Ç Kt˜Á½ +ÛŽ‡/%»›CxL r„Q$µÃ»_¦ œq>õŽîMžæÉ<~Ü,¢ ¦dTeþ *CJHþª–B(‹0§Ž9c +pÁªã2áüÕúäÊTSeþÅ!U¨›…@Ì* _í:ßüm]ì¨FMLJL˜Å½ÔÎ.¤°ƒŸmS€ˆ€L³5¬å³<]¨IUÚOúaf‰pðô +û~w()&¤6h¿®‚„e9@aµÕC˜6ḡb &SöKB¶®§ Ž˜ÉçµAÁ{Šñ³Šð|¢`Z‘Iø8«„>b½eŽ³À¡‚p©µ@°åîOă?õzWœ?ý¢&Tø¦¬$ø_bC(ç +Û¬ñ-ÀyœwY­XQdT­Ÿ¶VÑ]†iêIÁ¼ö³2ÿE‰yZŠ´ þŒv‘jàÊ +ðtB™Îc¨5\í:ƒëÑé×£w`WqH !BÊà£cÁpJ"¨]Å›îÃ"àVUˆËLz^òG Vjc!xÓ?ŒÊa©P¹™6j¯9Àƒ]¤‚3¢µ?ºÉ Z0“h€ðËÔ<¨Mº{‘Z3Ô8¬©ŸgðŸf°lU®± ÑYc„kTHK´GuáiÏÖ„4(»„›eÜ*ƒÒFµº‹þ|¿–G¸³JzŒÔÝÉ"f æPv‡÷윚Sx‹½Rº}éE5UØøO÷+OqHQVnÒrŒíI¡£†G{uåiUÎQ~ULÅÜŸîÙ)ÍãºÔ*[âÿ/÷Ê@µãÕ(ç "çQ#Â.kO +T¡up¹ VªD#"w@TƒÉ* T˜ÃšÐL—O! „7¨»Ü€´xtƒ§«I–v€úi]ÁúD§o''þøÎê]!J¯Ìz`ÆÁ~–i£&x°ÀªŠ»öº7´=Î1n5‚ásκ¶¡Øóé‘1x`FBRíaŒnEˆÀ6Rö ¤;®õvÁHæi·Hûðã!Tx<@•ƒš˜õHú‚»¼˜ØÇa€È„BÀÔ„¶'F÷LkíHm¹+@¹/òäA•Ç„ˆs +¨IÈ… ‡«b$q×P¡ æ]p&°,¸:ä%§RKB;P¡ *€ ru-[×ò˜¥f<] v_}â6MËn•ª +ðTus¸Wå!cƒýŠœ)Kû%1S‘òˆ¾WŸ…ß/gÊXƒBz]"ä.`»à¯AªŒ?)ËY"½¶*ïöËb)] °{½ô„áúÑRR§œ'Y2½´ Ò› Ò뻸˜ófb¸bì1ÈLIHs"„!X!P³ÁBMvrk›']¸Å’PÆnéÙ(ÂMkž%Âë%¾Q%/·á·r¸a¬ôô¥èŒõh¡Å+%ÜÖ”s¦œ=V‚%®6Á-:½KÞMcÌQ–r)ÐÌíéé÷‚7Ax“›ÂL«†ðÊÿ7IïáåÆu§ þoߎƒ(¦f't#çP…BU¡ª€J( +¡ssn²›l’ÍLŠ")‰ÊaGa¬dÙ’ƒÆr”%[Ù¶,Ëq<ž÷vfÏÙ³ûƒöœ>M]¸áû}uï…M0ûâ®@l{#ž8f Þ1†Ý9†“·ÁE°(°·¼½/îäô¾È×Íß4A6Ñ{ÂàgÀÐ_cÅè2ãq4Ø&Œ:Aâ©ÃcnÖöa̹åA+EçáÊàZ‡-,’c&òÐXÿFöA eFƒ[¶CÉ`Ñ1w_2,! «k é’8pÝÀ8ª1ûØAUG-ìHˆ(3»‹·{ÛÐyõ^## ÀW J¡0SÐeMÿ®“äaTÈVÇÀç8˜A ™1âI tÙ#q^µ}ã˜uÀ@ŒöWÏòV"‡[~¥E)-[ƒq°PCFâˆvjíÁ‚›/‰…Y"Þ‚4D(M'W´$Bå@j&TÝô§æœlÁˆÉWŠ6í°„…§õðT(Ú)Ÿ #Cø‚?Ñqqy€¼ëòÙù`jÒiZ茙HRŒ½ƒþÉÀœºØÂçC¹y'›C$#‘´Ë.±‰H-'_5’)Ð;W ]ƒO&ã“TrÞ,è°¨=·P©”G(j1Iç•4ÁˆGàe>‡…ëdlÂéVídÚÒ§,eÔ)ê±80Õ€™´Òõ^ù˜…w‡Fpx¿Sþ\ÓN¥Í¾(èõ¨ƒ±’v2|®CÃÐqÀ€ÝŸ"å†Oª‚'´’ªI{#Çü .¨`r•ªµ¨dÂã>©®TÖäÊš-˜ªê†„>`$5ÆŒ…}RE(/HÕÕdïDmíŠØYÎÁf-þô (í æýr#V^ãó‹h¸¢3bŸIÝ‚J€;²Ñ@&Ñås3ŒÚõ…òG”ÂÅ•íuý%jI<Òô'ÚnÀCÿî¶l& ¾%P©ì ×½‘–?Ñ •åÚ +ÎcR–I¶üj›TÛLæ±HOxKBÁÉe ¾ˆQlß’Q‰ž˜](L-Ìœá³Ó¸Òt %—#"UR©sùE*³®ô)m#r3Pt"°œ/\9&Õ)»P±qe®¸FDXt* ½&S^¥ƒ„[H¨Á©ÝÂôLiØ0q zþø$ªõG›Í÷-2Àã?b#->ýÑJ0ÝKKLjÊ)Í~¨”ê(a,j%û’m&TðWãnÑÁdú6Ò«|EV}ª¦fC¥U¾¸ÈççàÃåbÓPA&ÂÎf°H‹4Äâ2  &Ë® Ûh# +Þ²¿dèšTºÀ´\vV©oU—ï’ªkH¸È@›{ru-1yJ:%– ¥ˆ×s“[ùéÝXsÝâO¸‚YDªºÅ¢7R'"u›‘R“&L1x#ÞP U=B%šŽ·¶Ãµ5:Þ‰Õ–˜DSãUUp¹ÎggÅʪX]³³y-*û# ! !Óñ6­‹Å¥ÌÌ~eý:WZ åg[+‰HsÌÅ"xt‚P`ÕÞi:5gÄãGt^àL«WÂÄ<›ö«ÌœÚ;®o¶ƒj;Y]´ƒ-ïo M¾„‡«úÕY®´¡4Oú"-)7g¥Tt6JŠàókñÖ©Öƽs'k.žk­ž§“_¤*/Š•%*Õ“Ê ù™sÕµëtzÒʨÔHøcM2Ö‰6¶íãôd{ãîD÷¸•ÏXIR©Ôv¨²âÏ.Š«ÞD‹¶àŸL²âð·@,‘öI¹»Û'Ùì|~ò$›îà‘"W˜÷«Sdb’NN‡K‹É‰ÝPyàD7ê‰&U@Áéäd¸ºmî8‚àTB.0ÎÁ(˜Täóó‘Ɔ\ßP[' +SgL +®€J5à[<ÒbÔi€‡RÛŽµNòùUg°0h¡AåÉI¾¸àW{x´IÆ;À3¨\µ3ˆ Ø =¡¸ #¬öNzÅ’Ù+Ø ÙäâÀÀØ©,ŸÌLíÅ»ÛÑæ–:y&\Yî­_·1ý2Œ47Ë+—[›wu¶îªÍŸfâu<\–J‹Ž`ì_ÿvy´“>¯vväÚZaf +Ü͹¸ °(­N(µìÌéÌÌ^²³=sòVfâ8 ²‰TÜ\PÌ/Æz»¡ê:—™U»'õ¤bð0l¼†°©A“×T•ÚZeýîÊÚÕé[—Ÿx‹TÚ¸2™›¿*oØE'[ñE»N¾b$Ôg£Õ1g¤0 6Ø̤P\ W7€xÍTR)Î?øÂ;tjzÀLÐJ=?y†QçýÉ…P}']eŸX»faÒ#¶ÖwVÓÑƉXëTzj?ÞÚš?qkéüãh¸*ææ‹óª+—*Ë2S'Ë+Wë[7ËÓg¯=ú]¹¶jg|ª­oBGR§òsçÊk×Ò½ãÍÅÓ½Ý[¨ÍOn×ׯÊÍãDr*=³_ß¾­Lœ£bU!×öE +&BFÃ529Å—ƒ……hk+Ö9Á¤Ú©Þj¨¶DDT¢ãä³´Ú¦zäDaöJ¸¼a¥£H(Ëg'ÑP“JÀÃ@›Ðe¾°œhT;»µÆªu\®0© ©²,cõõLwsfçFså°¥W,ÀÄ¥{»¥¹ýTw'Ö<.U6|r«2yº²pÁ€…¢‰Dg7ÚØ UVK —KWO\y2ÓÛÑq¿:I§çöP dr!ÛV{ç}BÉa„IÅI…*ÙŠsLªÇg§ <Òƒ*À‡òÓñæz¸´@Ê%“5LOžñÇZ¡Â\¸¼„†Ê¡Â¼ÒX æfì|…JöÔκ\ž÷ŠY¹¾B'Ra*;µ ƒÏfz‰ÖÚÙ{ž—Šó0¥Ù³µõë©™ýHcSÈÏy¥ø[µº1sâþHiÎè P‘b² }9[Y>—?_Y»Â“˜ {Ô€Bˆ`2Kù…»²3—ÄÊ–XßKËlz"V^H6µvÌ#@˜Â@h’`r¦ùܼW®ãb6Ý\ åfŒ¾8Ÿ`Kkt¶¯¼\vzصS6¿b÷ËÌ…: ´ m‹w÷¢S —ÁÄ N0ÝPjsáÊ¢P˜V©õ6~þmƒ/Ä%Ûå…»áQZ¸ +Àc‹ó™ÆÊÍÛÏÝõÌ÷µ8w¦¶~#·pê·´tµ¼|V'·Ïܾòè«Lº5`õcTÄœÒØ,.œ›8qOjb·>³ûÀS¯G[ë€T{K©¯E[ÛÝÍ럜>ó Ÿ[­v·šógQ±à“ûZL«SRy-;s¾}âé³O¦&ÏL­žkÎí"Bf-˜™"c-¼-æºÇowN>$Yp rÕLùÂe¯T3ù3F2-æ–Ó“§¬~°‚q*“8|EÆÛT¢•í?Ï󥙓6š¾ïž‘k¥å+P)xr œ¡;R³Pé<,¤ºRi¾±z©³u=?·ÈM7[È5Ÿ¹ñ"ʧBÙnnòdvê¬/Ö!€Æ‹K¡Òr 5Á¥'ƒÉî¸;`…¹ˆ6ÂåU ŠúÒÅõKOä&OM¯œ›Z¿€…rðçJ}0_Z8¿¼ÿàþ}Ï+¥…ÉŽ‹·ŸØ€—°ÒI à®åÚ©äÔåÔÔù`¢3½rzcﺨVìþ0®4ðÄ4^òËÕÕ{ÙÜ¢ WèDG¨¬þ ¹¶'[„q“K §n<=}üªÎ+:˜l¤¹#Õ·ü‰IðêD¨¶vòÞK÷>Pš^¡(ÕV…êºT?^X¼ +vHdî5LëÔ‚©©Ds+3{^éœf «N²ÔÝ¥”¢!QA —“­­lw3ÓY)Ïì€Û´á¤œ×·ãreTŒŒ×ÕúòÚ™û»Ç¯[I%ÖX JKé™sáæ6ž¡b­Í½{vïzÜ«µD¤ÌLSj‡/Îf¦÷"µ•öüÞKßùY~j—” +•Kóg«lÞlmÞ˜Ý{°²z“[Û§ní\|Ô ü¼i¬±©®Å››¹¹sêÄ)\©¯ž¼ÑY>ëâÒ‘êJ´½Íåøü‚ÚÞélßÎÎ_ ¦fêý•cÙC:œÊÏ¥'Îd§÷âUJ)»ƒ1+0d"ÇÁc÷ˆf{»‰önrêB °È¤' ^ΩÄkuÆ,À¹ ›ÐJº½žên{ƒWäÔ)€º—=RÙ!Õ!¡{¸©Qцõ­úÊu©¸v%ß;Ágçt^‰ —VvojmØA±ù¹±“™¹Tž¿Hͺ„Š•Iú¥âúîM9ݵ`²R^ª/\H·OHÅe Dðù¡ÔDiâT¯v;·r3—„Ì<­´éXJÐÓ¸2!5lñ¹è8Ÿ™ +&'ò'kËW¼By ,7Ÿ¼zï³ÁHÍ€Jn6‹‡+x|ïRnj¿¾xEç *™n}ù"*éÑ  ØIÙØ¿²ù¢v,bpÛ‹çAÁ!ŒVIýUOÀØBÍ'·ɯ5›¼*Ú;<î0 c® 8Oð(Ÿ'ØÌôÊe¾4=îcÍ~Ùâ°ëQÀttX¸È5`Ï/2 ˜i«?®uq.*æR&Îõ¨DµØü<°«v¤d{nãrcéŒ+µÓ‘p~¦4µWž;©nPê¤OiX©8Œg´8“…Š¥Huf¤8³?uüÞÞæMo(,twçƱëÐu±y!3ʯA´nÑ̘|â˜ðrIÈ Õ•«ùÉS¯¦wªÌ] +g&&—/¦N£¡"D-È€ZD4úd:ÒEƒùq5æ¤ÜL”Œ” åTzÖÈ‘éíÕ—®U¯FëãnÈŒéÔ¸“4yP@ˆ3áâb8Ý£B:RÐû\t =!æ¦|¡’‹N¹˜4Ìš\lµ¹¼±ÿ ÁË4Œ™½NŸÄÈ56Ö¶xÃ#FÜL¾ò½÷ZÓÛƒ:—aÙ$\a‘Ë,ÉÅ ¹¸Ú?,B +¼üÉ= LeÂÃ.å“òA¥<¹|V®ÏŒc´#¨RMJ­{e˜‘44ÒìfCj‰÷µÌJ)0æz„³àC go(ƒ6»u%V_ò« —˜³âvJ&Ã9¥¶HD @éP´&^™÷+%ƒOh@‡Œ`6rT´•©mðÉ©þ#æðÇ¢¥Å£zá’Aµ­¶· mŽ@Zë +j4„ P +†òÌLíçÎç&öäÒŠ›-êÐ ÇTÇ„Ñ€_ ¯UW¯e§ö¹ô€ù¨3’[H{„¤WÌÑ‘¦”™“³ $ŸG¨¨7˜<¢óÑ8´f¯Ñáwʸ•Т#&Ü/æ×w®‡c•c£N·?Õ‡ò/Wìßù"¢C&ÆSÉÍ¡,ÂeGm>½j6§Z©‰µÜ̺ҜVSÙ‰…ÎæéÉs¥å" +˜æRÜl¢ëaÃVÜàa\dˆ ¥ƒñ*›ª“Ѽ?^ˆ”º'®<ÜÚØ—«N>î¥øt-=±¬LŽ£ðz1žïq‰ê°ÑãasÈø½StvÎÊÄñPB®L0é“*»%UïB4Îåæ—åΜڅŒ6ì0hnæШiÜA‚µ±Ëôv±PÜA0^^¡Õ²‹•q9I%Š¨”âÒµÊÌjsm‡ÎTMþkÔ ;©ŽÙ‡Ì‡G¬:-çgÅ ˜œ”É+‚=“*+Tj +Ôm貑…l 'ÆPJëc=¡t¢µédÒ#&âÈ°Åêb¼~ÅKÇlXHç +hœŒ ‘µ3Ó@8l„4¿s¥»yŽIUŽX=£ž.(9i§‡5ö£cÎa#fñIv2:lò}µã ˆ‰5¿Òsó¬:‰‡òó;W{ëg”Ú”=  Ú0Α‰"ƽw0¢T!,#B môJ&Ÿéž­Ì]fâÍaÀ†'€²ª˜íA4ðIE"\öË%¨ëxk9Þ^f“U)× "¥>5=¤óþÓÁño3{•‹6tVâÎÝ‘»Ö År²º-.#ÁüÁ70î¶úD¯˜ý¦Æv‡ÆzHëòp%¹´-e—½Ü¸-@E*¤¨&ŠÍ‰¥ãsg¶Ï߸òÀ“÷<õ⓯~çÍýìÃO~ÿç¿ýãÓ/þö½Ÿ~¸óÉÜä&û[ÔM~§ÛéB;[*t–jSKÓk;›g¯\{ðÉó÷?yêÞG×.Ý9é•åL—UŠáT+×Zš;~¡9=·¾wqûÂÓ×î{ñõïýöË¿ýîûâýàÓÏïê[‰âäáqÏQ­ÏN¨¼:-¬i,̈Ákv3ŸA³7lA7­ LLÉ4·Îí\¾oíÜÍs·½÷‰VN^-ÏžÉtãBöÈòõCfœ’+™ÙåÛ'/œ»tíáÇŸzíï½ÿÁgŸüö÷¿üð“?þÍüý?>ýüË׿÷ãGž}eöøEµ½iÄ+"…”*+&•x¦\ï-oïŸ8{íÌ•›·}úµ·~üÚþÌëßô…W¿óöÏ~ñÑç/}çGoÿÛÏÿöïÿxùû¿xàÙ7V÷ï3–¨Ì'7õÙ@4ÏU&æN;wëöýO<ûü³/¿öÖ;?ûð7øÁ{½úo¿xûݾüÓßþïÿçÿý#€üó/ø‹__{ä¹ÂÔ&Ÿlaºp6¤¤s•öôÊ<·v÷¯Ýóð3/¾ðúwžøÖ·yéÛÏ¿ñ½Ÿ¼÷á¯?úä‹/¾øßÿ×}ø›/}îÕ‹÷ÅŠ“~¾è¥3Bb"V]³úd›'ÈI©bmriëÔ…»oß|ô™›ÿüÂÝ?ÿò[?ùñ{ÿôWýé¯ýÓßþóWŸ~ñéïþð­ï¼3ü.0‡¸Pb£ŸPàÒ=Nm[ó“KÛ«»ç®Ý÷Ð}OüËc/~ûÅ7ÞþÉûŸ¼÷Éï~ñÑoýÙçüó_þóýTëùÙó¯þH)ÍŽÚüƒ/0§ÆFÁŨ´\Š:s'¯ÜÿèÕ‡ÿço½þ“_òîÇ¿ùÎß}åï|ø›Ï?þÝïñÁÇøãŸÿû¿ÿûÓß~þì«oî\¸É·&¬µy4¯a!–¤+3Í©õµ.Ý|äæ#O¿üæöëO~øó÷ßxçç¿ùßÿúÿùño?ÿèÓOÿý?þãç~z׃O'ÖãµU:ÚÔõÏgóiQŒVð@Tˆ–Óµ¹Rg±Ü_Ú=wõÇþåå7Þýà“O>ÿòÛÿö³_~ô›ÿüßÿõå_ÿþÜëß»ùÈ“çîz€50¾ìÅ…FkvniûÄîþùË—¯Ý¸þÀþùÖ›¿ÿýï¿üóŸ?úô³÷~õÞK¯½|îÊÕéåõh¡É§:6_ÄäfQ¿„S"/ʹ|yucçÊÝÜxà±[=þÔ¿¼ôã_¼ÿ£Ÿ¿ÿÚ[?üÁ~úåŸÿºóâkß}éõ7÷®Þß]>“ë¬P¡¬”¨%ʽh¾I—»Ó«ó++ëkW/_þîwßúî÷ðƒ½ó᧟}ñ—¿Ãt¼ûÁ§Ÿýæ·ûû¿öÅoþø'×x »|\H5H)KˆIŠ‹dŠîìêÖÞ¥“g7ŽŸ¼rýÆ«o¼ù«>üàÓß½óþÇoþè'ïôÁo?ÿü“Ï>ýì³?üø£—ßøÞÙ«·3­!»ˆHP©…²3:Ñ‘œÎëËkk÷?ððÐŽÿâ…o¿ù³÷~ýåŸÿú׿ÿãWøå—_üòƒ^zå•ç_ymùÔ&Vqø£@V”\æ“M1Y«O,v¦7vÏ<üÄÓϽôÊó¯¾ñ¯ßþîÏÞûÕßþñ¿~ûå_ÞýàãýäGüòËO?ÿâå7ß¾ýÈÓùÖª“4rÌDè *EdjóÉbgysçöãO?ýòkϾþÖOßÿðË¿üõË¿þûOßÿàÃO?ýòOúü_¼û«÷?þä£wßûåOùí_þúW¸Î;?ûÉOßýù»ï¿ÿàÏlž¿5qü.jQƒ“3Ú)›‡Öš7ÆV:óÇÏ\º~û‘^ë¥7ÞüÖ·¿óÓ_üòïÿøÇo¾øãOßûõ/ßûÕ}ôì˯ì]¸:»¼Ã'šÉú'$Š•N®ÜHfòIˆQKí™ÉÅÕ…K×ÎÝzàÆÅkç÷ÎNÒŒtã¤ÙMXFcD5FdÜŒºÑ€J¤S¥Jµ]ow–Ö77Ö'f'Nìm^¿yážû®ï_¹º×­­s—«Ós¬’"C9.9A†KFiv’/íöúIŠÛ8~qnýd,™*åÒk« gö÷®ßs÷ýÞþÁ¾ÿÞ¿yÿ£?ùìã§_zaçü~}z:I0‘œ¢+´b,Œ¬oŸ?¾#ÛœNWšNwa~æäÉígžyìí½ýáÇŸ~ö»ß¾ýã·¿ýÆk¯¼üÒÓO>tûÞ»Ö6¶‹ÍÍéw’a#Ê3B®AF͘Áæ£i!©ÆW—–ï½~×k¯¼òÚ›o¾öÚ‹¿øÅ;úÓžÿ×¹pîäÆÚb¹ÑTK5;Fĭ‡Í'X}‚›P‚ )É|¹57·|îô©‡yð™gŸúîw^ûÕïþÅçþËŸÞûåžzê±ýKg²å‚ ñÛ¼"˜a™80 ?:ê86ær¢¼$Âr¦=13µ°¸yêôé —.^¹ví®ëßzéù¼ýÃW^{õÅžö™~õ•oÝwóÆñ­ãñtÅåunÒèax Ä¢¹ VÎ:*¬$—V_¾~ûég_|ä±¾|õÆc=ùãŸüüû?üþ}7/?qÿ=?ôàÞÙ3K+ËÕF/šë&wØxë«åÐ>Ðe£‹ÑšP›“Œ'‹++›W®^ûÖk¯ƒWzü©§zä¡žþŸ¾÷ÌsÿzåÊÝóKkébÕëç!ÎèÝì˜ 38MÜê&^: ç"ÙV©5מ]w3¬Á,^¿Þí×:˜G')[ñGÆFµ.›‡uúD‹›qc<âå=^ð±l(–ãä”Řœn³ EýA„æ}RV‹òƒ&Tk#DÄŠ‡†­èAaÌäq A³ƒ°! )¥À{{a·ÇCx,Kå³ÍÞÄÊΙÉåµPBµûèQ32hrZ1+¨sF-Ę x5-¢LÄs#‰á('„D)SãJ2¥«µ©^V"ÑXµÞÈe³,ÃÙÝ~ Ê:p $Ød×L~mÀ0n§RÉÏfx!^Ì•ÓÉ´–ä°4¿¸R«2™x­ÕÕ"4Òˆx˜¤ ÐakhÜ~LcÐØGÌ~×LÒB>]˜Š&Š)5wéÊ][ÛdzéD£ZÍŠÝv«×ªª©¢Ùé=xL`Xç98`84h‚_ÆÌýøaĬ §•D6’,Åî&iAŽ9QUÓåzs¦Ùš¦F‰%Y>êöÇ-Äa½·ƒ[ C%:\DPÊâòû™h(Qƒ¢&¹L¶¹Ù^¾R›9Cñq‹Í*ðB,– °!Bh­ ˆJÙÞ:Ú6øþo ™Ü‚“Pô +fùP(¥"ÑxL-0 çõxqàSA)ÇGJB¼¡ëo^`yµM„«ß8¤¿c`ü¨ÆjvQn:áã³jqja÷–á Þ¢ÁÆçÁH8ȨÓ3x¸!=bó +F„;0¨»ã˜þð˜}Pë±!JŃ‘)ä}Œê¢bý3I,Ø°1ù$3¦Éˤì˜tLï³ûtnƆ°/kpã6ÂP‘PŽK””381£eó2n*D„2fŸ0j'þøQçkÚƒ£VðºfTìo¨!B.Z³“+ntFŒˆÖæ³z¾%æ!&#ŒêåÒ6L`Ä„“ïÖ4›\B'ìxÄÃæêŒ‹É èPƒ‹1ºèÁqçà¨E«shÆ-­ÅâÀY)‡3aðàaƒ6ãCÀ0#®Ãx£€WŒÎÀˆÎ3nõZ™Q[P†Çœœ“\þH¼Ð]Ü8+Çó#™¤“”=? +W2 Œ»¾è€Æù>2jwúÂÀ]3©wsD€öò9µcñŠÇÆL¤). ž‡‹n/orøM6¢ÞÙ@ˆð¸”É9ù* +¹2\µS1­‡Ó£Â° ‘¥”6£NK¥ ¥²ž™Üg“ƒzÄ…Ò‰lÇâa¿qhl°¢Õé³ÙÎ)'•:8hµ%26,|ÇuЀY`%ZªFò RjZkÄM„3:˜Q£Ïäb͈dï/uë2ˆ÷8i,‡ÇLGÆ-ãV3rÒª—ÏŠ¹i7¥ÊÉ^´0ƒ Y”ÍZ}sÒý#CqEˆ_?2vÇ ¡!#îöE°`ã².,Y\Tk›Gí‡G-ãvÂLû£Bn |ÖŒð8ÉÕ&Î)eÞÁ!óµxûyÊ®s™y:6å$Jv*«Ñ9ûì¤mxØGp¡èfÒ&4„àátyÁH™ðq7̯èYç±É Êß1d6yX)™1­ÕE} &ÝLë"L¦¶“…q¶á1"wŽXý*Íĺ‘âÊç4_3bDeø_‡?æaÔQKÓ+"têëwjÑBOi¥çª&$¨÷=¼Œ9©$ ‹ÖæÖ/¡Áäá1Ç ÎmÃ"n8¹ˆr%“ÒX ””Vw®K™îÁaó°Îmr¬ÞO,ù„’ÑÅB#I¹â ¢5¶#Z÷°¿ê “T´iÁe„K·–ÏÑzÿPšÃFPO¡¸Èx@©‹©I=8¤qº°LÞ1lÿêá<0ì´`±`´-ª]Ÿ%¥¢ÎÃ…@a÷k]A'ò‰åDãxuù2„ê :j}Ëâ  óa#­ráA­óа‹”_ó‹¿:bçF=!T®G[»…Ù}\*98[„1G‚éq©søG ý³¡¬îv±Œ˜|ãvDЂZ=¤Ç‡L$P_wé²_ÈþÓ¡ñ£}2èpÛíÏ|µ¿[>0쀉·wÕŽQcÿÆ¢¨GBÞp,ŠœéÑñê(L²0ªË²³îPÉΨt¤6³vuõòf*j##~¥Î'gÄäL8;OÆ'|¨?Öèíbé›ÇÌCFŸíï¯ Æº•¹Kjc[JÖWNÞP¹…¬Xœ‹ÔוÆf¬}"Ö=…Ǻ&NÔï}ø…Êü”¤É4 ¼ “Q¾$dq©1ngŒ¨8Ô?W6ì¢ÓN:k£²î`‘‰ö˜DWë +ÑX!a9¾ÚÑcôÉ¿êð'P6G„JñÊ2.d¡<ƒ‰®—}_-±Æ"u “Ò¢’Î# ›ûëáunå +¬Úó2I_ ÉDò&”ëäWWcV +¨ØI%̘d§ân6ƒ +y#&XÉav¤S‡ L ¦éx+5sÎÌ1 z7oFD‹/o¡ó°ãnÎä: 2= C¾6`>0ê4FOÈ(¡:¥´p J£âf¡qŸ³“QƒG°Š¼®\ÕÚiœŽž¹ô!¤¿~DtÌeô Ç`&däè¸kÜFalªÆåW 1¡ÜL¸º‚'Ú0•¨Xt€Ž5í¸rtÌ3b ÌHÄåOáB µõnyÈD9qà³Ã:dĈqô7¹Cn:T'åü˜À!£¢Jÿ,V4l ú{“Q—ªBz +šArIZJ ÃfT¬ë‹X±0Âelt|ÌÍ¢RÉÁ¦ô¸ÈÎýås¹‰3¹éóbiÑË&åÌ—é9)ÅŽ‡,˜0æ +6z=t"SßÀÄüÁa›Õ†t ¿XŒ×VùÌd¬0½~ö~w(‡†s‰Îfzj/3}:ÚÚ +×6å˜wøÓX¸M©ó¡Ò‰ÔÄE6·hÄe+&°±ºØÞ²‘vŒÊ¤Róp9&ÑKW—Î\}¢¶pqÜ Ù§Èfgéôœ/Ú£ÕY.µÎDçáá•FL¶âŒ/˜Ü’ÖÔXýÀÃrz‚ó€íaáöGAÀøÂ5¿ÒT›reÕJ*Z'¡&4שÂ(É­]—Tõ†«`/é½#&¯èX “ª|zVÊÎÛˆèaëá1爅<:îÑX "L({…Ê—ít Up¥å ׬þ¼ÀN%ÉH’+|¬^ïmX1~`Ìnr!1!L8ù¨ÖshÄŠó9_05jòëÝâå3n!ǨˆPF¸|0ÖòÒý“¬PÅF$-˜bòFµÎÆÆ9HŒŠ’›ÝCnA 4oÌ@9÷¿˜#k'b#F-•t.ö94dÕZ†|G„ËÀä6<"ªí€œ½sH;lô<¬¥¯¤A+‡¬Ä¥ç’ÝÓdÍNaÑ™èø“=>?OÄ:à.굕«7ž-/œ>¤±ë]Œ¥¿nP€ydbm:Ú¶:+îôÇÇlÿ:Ü„KqÙ)¹²"•–\Œæ=ÝÛòˆ9¯”Ç£õ@j"ZYÍuwêóûs»÷yظÑË°±†”™óõOlŽÏØüQ65!4nîˆÒó™©óÙ™s|aÉÍL~U‡É>±+/JÍÅe<|Á¾Ö©DÓ*Ðɦëd:«B~ÊŽ ¹öFeåš:¹†›˜ÜÂä樓«´7O_&’Ÿ4á˜Ô´Óe'U +gW”Ên(ÍVÁ¥PJmÜJ‚âŒXÀçÎÖ.­Ÿ"Ñ:Í›\ޯϜ2b>>­¯Ä{»‰‰½òâµÎÖ#Ñú) .7­àá‚cÆ9˜¤3˜×8pÑ£VÌÞð3HÈ€„õý#:ãV:íUÚBm;PXµ0 ¢”²O*yØ,Âf}R‘Š5•ê*oY½p‹ÁÁŒ@œôð0ø&Hjˆ8 GA’œ¨õ.pøPù`jšÏÎ{„²›+úcj{/RÞ„${pÄa%€g\,ÛI°Ê „-á +P0`Iß?õ‹us2i/{¼3j!ÿçáa­ûؘ ²í˜™Ð:Y/—G/[pûcÐèÀ¨ ^×ÓoyY./‡ËËõéÓgï~.˜h!|6ÚØòKd´]è핧Î(õÍ77 óx˜$T PR(A!ôádðQ¡l´<ï bÐ_P":ÑbÓS¡Ê +œdär}âøÞ­´‡KÅpy5ÑÜŠTVâ­íXs‹ËΙ°_*pjkBŸÉØ'VBùåÖæ­ìÌ~ÿ\ î¤czohØN˜0 ¡ø"­põx{í–êq©n¤²`£#G n)“æÚŽ{žÛ¸üDaî¬W*(jeþø]þx㈙×*·‹‹˜Ü‘K+åéã~9¯s J훚wòd¸¹@Û?­.¨V—Ó­Ía3~Ç eÌ?àÖ½¡bÿ;€ ’Íœ„÷Õa,›læNWWÎg§wm>¿€J4{ìùï¿ô€…P6½ëí+í=´7l¦À+–š+3›wã˜Ýà‘ðhWjžH-\Ê.^eËk:"ˆ”zö­ÙÍ˃`¨žFCÕXm#ÓÛcÓ“àR0¡., +©)Pð@z ×MxRbFí`Q èE +‹Rq5T^g³sN: Ùhm WÊF,DÈU&ÞÌ´V—önm^x¸µzÑ#¤ ųן ç&ÁeEkÛ‹g?sÿ¥ÛDbÆJ'Íʤ•âòHÝbñÉ ‘ã.â?“˜ÐºØƒ—ÆJÙð(¸;®Øè$kƒEjÀ!lkiéä :ZwÐQ³sp;—hÅkË g +秇m„ÉNOž%£-Mÿ$dÚŒ +ÑÌD¡sdц+ˆPðyX¢Ôi*5%UJóg·ï~6?sú Y $§ÈhÇÌ@†:ªÅGúµ\ÄùÌ?ÕÚ2>Åç7¨øŸ6CŽ£ÜL܈ò<<}âÆñëOOîÜ*/^dæuX䈮Ì©»_Š—ÀjZñX Þµâq€œ\\Ò!ÒƒVƒ‹Ž¦H>}è˜þŸîÐùÜ¡:[ÚË›RiÝI¦Í¨LËM+*0Œ¡GǼGµ˜ÖÁ‚¿Ò:‚P•¥ÉMðÞ3Úÿz ‹àFðDO‡Š˜T$b ›v2D´ëtP±â$ŒŽyü=ä" ”áÑ?®ªàXLoº¼,ç'ï6ѹµnÁ'U­ãé©ÓJ}™“ó;çj.íë0^*Ì·ÖoV—¯«½Êü¥ÒÂe"Ö>jÀ±–—ËX¼|ßK“q'[&#ÝhmË.6xdlØ];§P´3\îïŽô90Ø\²‡²)œOf»ù™½ìô©Xc¹½|¡:w–Vê¬ÚNww2ÝãùÉTjRO$FœP A1ÞŽ–ôØ‘qËtb’ŠvýÑ®/T?fÀuΠ›J¤AqÀ ¹ùÂüE0Ð:\Xغpý±—©HUcgl¸L*]µµ3ê­»žËÍ莹h!ÓeR4¤qŽ{‡,”‚™}Š®ÿá@Îì ½:$ä 7CÅåÙS÷žèµë/òåµ!;ëòVÜà :w‘i"TÃC5@Ñ\6˜ž÷°¥1'!¨ÿUGþ(mØü঄ÊÌNqbKÊM³Ùy$Ürñe ZÌÍ^±øÓ#Ö` ÙÿDqôÿ?]Á0‡4n³7”¨,›<ì7ýÎ-Œ€P¦’3ÞpÃŒGkI-qÐÙqôÀaݘ5 uðZ'1ÊFÄ´ý ûÑHu!£±Ü¤Œ€AÄò“Y"ã“beòzgß.‚wu²9¨À$<3n£ÇmŒÁÉAlÑ;X½=¾ô›CÖC6H¬l´Ù?q}ÄvLC¼"¢ˆX £u:V“õlkÂ8Æ¥j çj+—’½m:QwãV*jòõO'cäŠë¬$$wtF®œLvö+ w[û‡?½$ƒñˆæ¨Òºƒf"AF»Œ:‰†*% +½Í\kÝE†^.^žël\_ØdùÜC{÷ükª»K†*§öî½tï3˜”3ãa3¥"R=TXÍMì7¯Ùý™o;‡P)&9IÇ!WÆ ºG 8˜mÈbºcT¬Å€ Ô\L +b#!äb•…òêe7_лEµº™íœU +Ëùýf pqÌÁÃ… ÷¿H«½#ZŸ`ý<8éáJPw`õxʌĨë(ææÃ¥¥âÌ©Õóu7îB¤j0ÖÞ8}“Tô‰uÂùy X¹¸Š…Z|ÄìCè¨úf^ã6 ’\^MMî‚·A¸¬Í’Òí1»ÔæhÍžy¼·ûPiåîÔô¾“+Ü1ät`Ñ•3º¸ÔÇ …Ò¡0 Ip‘Ñê‰hm÷Æuð˜~Ü‚¹úŸ·8†môˆ=w²%Fw +ï8jðXPvPß?nÂBÄ\lÞ©7W.¯\~R=bS¯@%#U*Þ¢Ô‰`n)PXs3N2ª–`ÜŒr>È |ž”Êàú ìíŽcúCÃæC檾ÿíö€Ñ%ºÁúÂZ+îrT¬!fB¥ùDçD 5‰ + ’ZuÒ¢‹†x+ãB^Æ( A6¡²ÞA£”bÃã %±“Z°x#X0‡ÓGÇÝPìC€5¡Ô·0°Í¹¹ºI©s„¦¨_*ûøì°™ƒlh&ûæ‘P: á ý|:0jüú1­Ñòñ®˜YŒ×wš›÷PÑ:.No\ f¦Æì´ñ«Í“ÔžÚÜ8bÀi1œ+ûÄòˆÕǨíÀ¨Mc§1!Lô¬¸lò0‚ZwÓq­ƒÑƒÓó +z\*ÎwOBØÑ»Yp°¹î $˜18ÙQ ñÿàˆmÔäóÐÑ1+Ñ_Á = ª„Ò 4Tö†+¹Þ ø<ȵ×ÓÝmxÆÌê°°p©Ê¥æB¥Qa¥4ùô"›³±BÈM]¼cØ5j" +õ¥æÔVÿ˜8¢CDŸ\—ªë‰Î)`õƒ£.N©Es@Âa°x&ÖàS]"”‡|1nêºbð„|B <Á$—šW7CåìÔ¹²Á¨=ÆAúÎuvÀÇ×õ!t†Æ­P|C/ ¼ý7¾ú^†þaõ¬ÇŸ²cŠJJ¥=¿çÄ!àegN-]xréÒÓÙ…KþôÌÿÇÑ{>IrœižÿÄ ´.]©ufh­2Dfd¤ÖZWeeiÕ¥Uk-ÑF£ª¡B ÎõÌœáîÎìrfå™ÝÞÚ}=O˜…Áª)Â=Þ÷yŸ_x¸;ø!)”íÍïv/3zÍ &?ÿ ^<¤Æê^\1{p>”eÕpTzÒ§ž²â 0M¸é1~rÜ3XjÛÃywh›„2>s*LåºkÅÝ©«½½{ý;‹‡ëËWôæ–‡§+óƒ§R +ó¤Vd"mT®3FÏK¥€av¡À$»œâl–ô1›_p@Á 'EˆY c´œ‹PÁoù`2D¤.ÄÙæÚÆùG´^;¨ËƒgªZfžtP1?éáÕx#]]wÏ»Æ]œÕ©Pd&Ó¢µâÔÖ¸“µVÂÅÕ|ÿBiábça¨¸ +r¿ØYŸÙ¸1îbìFUÐK“NÒ ‹˜Ù”¬/JDš ¤¤"uµºžY¼>½õ¢–éÍ®œÿþOÿ '{£î "7Äôr¼¾±sãíëïPÑéccˆPJ ä( ßHH.ѧ…xŸ W9µ“ÀÂ:5îaGL¹øa§hö*B¬Chù  ză!aö±n< 4 äÉH“ ×äL/V_“‹³ji1ÚÞ ËT¼CF*…ÎnëE.Öóð&¿êÄc˜Äj‡P-~ýa;1ì ÆݬÒþ Ó”X¹8½.%š™&À½›‘Ú’Q_•Ë«h¤ãåÒŠQ_»2µvÏCGÏØQ³Ÿ…¸©U)Kkƒg­i%/Îú©ðà¦:B‚5LÕ³0î@Ç]Ô˜ƒ9*ï° ¬„…!)E„‹t¤’m®>}÷‡ïÿÍßזΡ‚Z˜S + …™£Ts£6µ×?âõ¼ ¹H ׸hâ néÄtld0BBËäf‡-ÄñQï±aç°³®ˆÑVyz¿³reÄCYQÕEÇìT„4j‘ʺQÝä¢-V•©ÍTmÅËfçêMÅÀÀ<–ï%‹³§Ç¼C“ø>å´+tÞ…*¸˜‘â-›_ôàa‚VR²}vðL]W«„”)w7k3{Àýdæb>R·z97Ö×Êjª…°‘3ãn*ûhƒÐJL¼Í§¦Á÷ø5š©ï^xì:¦6¢ÕƒXm_ŠuõÌŒ’šö ÙS&Œ‘sÉâ°¸ÇF]nÑE¤©ðL{õQsý!¥7O›1JÉI‘’ÕËüÕ)ó·Æ²Q + Šce=Ù> +•×ÏÙ&»à× ˆi8CL…AÉ –€GM7÷òSG´Ñur^,„J\©BPÚШOǸWöt#O³—¶Á6ˆ—“­©õ+ý­›ÉÖÚÔê…Åûl¬ŒJI93-$ÚÓëW‹3‡&æ@E.œžY½hä§ ¹PÕ†„hÄCÄQ!ãFTg@2ù‡Ëñù@w™ô ™D0§IÏχr}RÉÌž½•ïîx8ÖÓçssWA£„X«»r%]_Ìói-7ÊÌÑê7‚à¦NOúþú˜Éâb@˜<øóÖӓðà¾=ªŸ9îfÌ~pwÈÍœ4ùœx2À'$jëb´biÈŒÂtŒÇíØ©QÝ/Ø}022d†GÌIxƒ>áÂA9eA€ ƒIJŒÒJ +SJjêðÖ[ »÷mó3)J­‚šn”×Q>éÃ%” OzYPõl2X&¤R01ãg“ãvœ +ÆÔh8œ¿8aàÊœÅ+ž1ÁÃdÄ›R)ž÷ŒÚQ`TœH H.XÎy…¤K J ¤^´±ÁG‹‹ûwÚk×ÒÍòÌ~síZyñ’VZÆäl´Øg#%³ñ`ʤ‡µcÌ +²‚Q~,ÛÚv€–šüöˆéHmeþðÅõëogÎÝxõÜý÷ÄLÏǧ !à3d¨.g“ÝË…¥»bn—Ò¹öN¬¹)$»[\ƒÕnjFÏG'¿=ìüÎó„ó+u ¼"MƒQELÌ••u¢’ž›^Ý¿ýàõOÖ.> ×W¿-õ·îu7oÓFÉE©brºó ®‰F#VcuÒMNº(›Oµ @Ä䂃§FmN˜ÇĸÙÀHKTf»ëU.©¥•t{(äcíúÀÁú$(fÒÉ:±n Bêôd¤€ Ú|¼s0FiøqÍä@G-ð™ ?ÂD0!þí“ß:>ö×ÇÆŽ± Y`;ä”8¥l}1”êLÚQVÉX‘ ªä©hKÊΩåµ\ÿr¶»Ó›ÙøþOþaqÿ.`F@CzyQÉÏJå“s`Ö +K8ë-_u’‘“£¶¡ »É‰L v·‘Â%JIƒºï¥£¸RÒS™îÙR/U[¸tëåÃ;¯ É&,çÃ¥³¡Âj¦sÔÝ|T[ºY™»ÄÇ»zjjeï6N™QÎIîÁzb.:Eé÷ò~Àà¸>ì ŽxƒÇLè;8KÉ.IéyJίnß.÷¶èHIÊ –sIO_¨¯ÜY6Yíí6úû@'dIñª­ÑÚ`/.ZƒÄ DÇÒ¥~¦º8î@Ælˆ= HJ{@³ vT‰–#é–ÅMâœQîo-Ý_8÷XÊ/‚e•˜Y½òÉÏLO8iàîP>M«u:Ô&´†u°©ÁjÅòÜy%Ùšt` œÆ̈ÙIº!É‹È¢š˜°Vq!;dFþD׈wÂÅzÕæÑNFÙX È`v~ýzyz‹‹Våü\²{Pœ¿<µqwíüãêÊ%¥0ÛZ8|ý{_³‰®Œ`Z™ U€±³ßìÊ= è¼l{g°gÊ7Ë[¿YÔä Â\—+CvrÄúbl0ú<Ø&» v¶wŽ5j´œ®OïÄòK™ÒÔVuv -HÐÇÆ@T0ZÀæ„[$k‡ÃN(äðð>,@ïŒ jφò£NüŒ¢£ÝÂÂU£¹Í'ºN:æÀuJªáeŒŸÆ賜êVq§drS@FÌnÊä"Á+NX·‘:of®ùÈ0«ØpAÉtë7zÛ÷ûÛ/,ì=\Tòes“#fásJjPO€ÖN™Ñã#~«G0¹Å›áŒ©–”XÃKjy|Lø|­4W_¾4pþàÅÊú 7gÉ­-]Á#u¿”…¤œO#R—@9(×ú[Túö‰ “pÂ!‹W2y‹q€oÏ7æ÷®CrÒB„”Ò2—ìhŹÌÜQ¸³‹†+´šÝ:ºßß¼î@UA¥¢’™oÌ_œZ¹ÊÍS€—˽'Ï>ÍMþåqˈµÀ’ƒˆªé™\ç@HÌØen뎘ì<7îrvÂp3 @j€‰´|?][˜]»ÐZ¹h'UR/ÈÙ®”lP‘bªµ¶rîÅ›ïÀZ)RY‰6· `ÑID¼ ú† ‚H3JËÑò²‘ŽOøpãÓßl´š°ÁáÓVúô`séؤkÂKú舘³±Ön¼µç"£ÑÌôý§Ÿ®]z + +4¯+ÅY)ß’S@·]¸f‡%V«¦ÎaÁÒÐ7¾Ýâá&õù¨$pÔõéí‹wÞ@7ÇBL´Mm'“¦ŽQÛœÞ}$¥Úíùƒ£[Ï +Ó»–€b‡ƒ=ª¨ÌÆû>K€ŸçæN™ñ _ÐNÆÄÔ|ª±Îtê3;½Ý{+W^]¾òÊÜáC!5eAU,˜I6Îf!.Æêe?ŸµØGãNvÂÁšÝ¼ÙI{á`²0/êµo7÷Ù}¼ÕMÙܬ- Nx$TÌÍnÝÕÿÿ:f=cÆOfŒŽ”ê;‰ØwÎxÌ~ÎI(NRÄ«ëÞܼ››¿JÄ{D¤¦æÁ’ÈÀï}ó,ÐR¸´ßÅÃîÁú½ïð‹ãvâùÁöUî;æ"4'6¡òx€g¢6YóIF¨4W[»km%gsÓûéö&)ÿìL`)JÑÏ&€ÜHW³¥ÖÊÞ•—„Xóù!»Ÿ6ÀT¨ª‹8ɸ‹ë…Y8˜>1é™n†U7È¢hªØ]Ø8Wîo¸p%Ú8ÛÞ|aáèéÜîÃúÒ…òü‘œë…“­'ïÿíÅ'Ÿ™Ð‡ŠfºõûÉÆQ²q($zVT4N5)5?ææŽMàÇ'ñQ—dE¢¡ÎÏíßxzáþ[^*4æå̘êb¤^Ë´7sÝ£äÔEo°à¥t½º´ÚÃÅÓí¹”Óx¸Ð¿8ØÎÏ͸)# äüt܉h Â‚ =@‡Œbω‡-þ ¥Wã­­ÂÌù™­ÛÍå+ƒK5uáÚKßýòëB{uÌ-`Á2.— µæ¡âæ€ ')ç 6:dGMÔÊ ¯°P5>X–çQ¬²<·|ôú»_L¯]êlÞš>|œ[¾™]¸ÖÙ~qvçîüÎDólª¶ôÅW¿¿ýäc/®ñ¡2n: ã@o‹áÐc£*[añĘçø°ã[ÇL'†½$TŠ‰MÓz=V^bõ +ª©X‡ˆ´¨Ø—ž3C".è¥9L´km—®´6ï4·îWÖïF:lª§gú[ç/î?@Ô<ª–cƒ§J6¨ø"žlïn#NNø'#Ôð0v,äf+Ïèå…Py6Õ\œÛ¿7wpÿì•—»7âí-T¯{ØöÍ=X7®š½ƒçÍ­–›:,Ï]—¥0éã] î Ê˜›4Þ”òD¸-¤–ÃÅu:Rõr^6ÀÅÔŒ{(À€\¬)Í'ëKà¿B¼ðF²¹–ïíe»[¡Â‚Q[5ª+¬Q'k·¦ÖÏ»ÈÀ·X}u°”M¼ƒ…êƒFí˜ ž=¸›(/LxPÂœxÜMeÀ;Zœ=¼ñ´:³ °‘²b~A«-Í_Ù½ùÎå'?hm¾èrÅúâÙ£{x0b,V9[œ¿U[yì\“=à€Ü¨¼{áÅT}ã„ ÖŸéÄ °–W“SG>&Ql®Öû»à.‰…*„^–—@áHOhÙ0„±BŸ WÎبS“È€ÊÅíJ‰¬Œ»(F¯$;ƒšŽiXL£jÖEªB¤’¬-1¡|eáÂÊ•×Ò½ƒHe‰ —¼TáãgÖ.¼tÿÙ—•+.-Äf0©h H6/5xÂa:92SÁä̘‹ü‹c'Çý£VÜìâœh•+n#XXÐkàÛ ¦zýsOW¯¿[Z¾à™öVûþîÍ·[ çöo½Ñ\¿®-”W®¤æ/+µM9»Ð_¿½{íYcù2 6)=›hmÕu&9ïfÒÃVìùaljq_€6|TØ(DûÙ¦ÉpY)Î…Š³K{wvo¿¡–z±ÖraþÜ`y¢ÚFº{±²p;Zßt’!79VáÄÅ;BzŽŽÏòé%\o¸ŒÓƽt€O‚+ ,>ïgA§eMŸÙ@r7(žD¥„’ëg/ÖW®âŸÒråþ.°°Á•—Zë·7®>›;z9T]á“ §áR´»r™ä=¬†‡+ˆR]Ii„‰%K³J¢éDU›?hõ6gq`¦kÙ~²¾êȱaW€‰À¡#U6RN6W[ë×õÚš‡6–VÎÝyüždŒQè]ÌN_Œ×¶¤äü„¶c>L;ºü¸ÐÚ8=ØdVõóY.>­WÎfzí}Œ_¼ñòÞ•Â;Ð ‘úÚÛï¯^O©l8ðˆŸÔÞxÿóW&üÒÉ Øä—ùÄœZXGƒåS&Ø KSÛñææp ª‡Œ@#ÃU%·ÌÌRJæèÖ«W_ü@Iv]xˆÜŠïÇkëý­[ûwÞ-Ï_Æ”*"æX½eö ÇÇÜ}bòØ ¸AðU°*uø;gŽ€Éô'Ü° uÓMÆ[ŽúULÈùˆ0&¥Øx+ÙÞÎL!j×KÅÞAiþ|}ùÂæ¹û×_þn¾·%åzg/¿~ôð{ýÃW@½¸õô‡í³÷¸X;U\\ÚQÎtÇ|œÑhµLˆqÀ¹&7íÅ=^¯ç/g,~h¥W¢­Ídï µ~³¶zYH5zWJ‹"­M½µ%æ£9xª9Ñ•KÌ÷mŒî“3.1Fkì¹”š ‹zÅ0ê@ߪZº5{pO.ÌjÅùÖöƒH{IM3Ñ: Ö™µKRžô2‘òš˜Æƒy1Ör㋇¹¨o§ô3îS J&Qßàc;2¹X5Ñòʤµú)@òGwÞ}§½p8K£RÞ‰“^Àž’T8S_âle5{%˜ŽºÉD;Ll +UÊZ¢~ý᳋wžR6À¦j‹·r3—¢µÕP¶èÀC%Æ´ âŠÝ}5§M°pŠZǤÌDݸþü°Çì$jµWÞýñˆ >>ìw0^:ÆE»Zf!œšÂ¹Pcn‹Ö²öÁò\ "6ã'ƒéÖj¾Pœ=ÈMoëå91^϶ÏööîNo])ö·€xj…9:Ö¤¬oTgv¹Huð(‹<ðó( ¨„³cNXRjÂEZøX0=Õ\»±ví홽•…£™¥ÝõÛ ÷Þà‹_Í'¦ŽÂ•³ñÖîÔÖýÍëo¶ÏÞÊOm_~ô!“êYˆH°°”˜:LÏ\;ÿæÜ…7jgo;Û~ù»—?üÊN~eÌœ€dŸaýpi5×\?ã¥Ù½Û…… ÕÕ«……sz}¹²t¾µreýÒˇ/¼ ªa¶½qp÷ÝË?^:ÿ¸´pY-ÌãÚàÙNJïXmÜC»IÃIF*R¡ºocJ×­ˆNª'.¹àͨÈ7+\ó3 ðUŸ?ÇŪz®•k,D+KBjQj°XDùŒSÎ|³‹4 ª€˜}â`ã s)/®ƒa.ª"&A|óÑpeCÌ + ~&ì@h/dõœ’îTæŽÚk×ùh=Àƒ“Ìéøæ‰;Ôæ¼xÅŒ˜ìzÝê˜ÆYü²ÙÃOº($ØðPª½Ù^¿Vœ=Rò ~1;ááÄPIKvüt”Tò&‰òyÑh¢BÊ +KÁÔ|ÌN9!åĈïøi×±3Ž1éõ¡âR +xþçNÚ,nzÌŽgÈq|Ü7ä ÇÝŒÉI’r*Àè6*-†Ëk”¨Ç€j­ûû‰În03Ä +Z²­Îj“T>Z¤ÃÅHe-Öܬg$Ã\¸Î„ÊgÌȨM ±q‹—1vfÒ ˆ”ƒkÐzI-.d:Û­¥ ³kG7uVŽòÓ;‹çŸLïîá’|v±´pSÉϸhyjV |´ÌÍ0‰0“”Þ(ôŽV.¾F„2b´(%¸^Ï÷.”oóë^6œ?ŸìÓÑŽ5 pZÆGGAmµÃáo6g ºXTÊ%›¤Z´a*,ê+·²3‡\²ËÄZ¤^râƒUhÜ”b‡Yˆ Jš ©p‘Õ5Jì×æDm #®¹0Íìf\ š„j.D—ž3ªv"lÃd$˜áÓýXcŸŠ´±`Ž“””„¸¸`Ô,>qØJ"|RŒ5 µ(ØHªõƒ( ÐQ/©¸ðàáq Dxðd W@Øøç¨8mF†´ y™Ø`Ç)Lµ’­u3’åHaŠPÓj¦#¥g%“ÓFqQ/ÍãAƒQ£|¬ñ†èJ¹À†ÀçàR!”ì‡33ã€ý½œWˆdör§'¼ÇGœ"¼X˜”‹>RE× +…©E@¾ç¤»k„–TRõÖÊe½Ø‹—{‘Â4®d!Öà#uÑh(É6,$¢•…t{‰”P¥èeA¥‹rFA‰Wâå¾’i[`)^_Sò³Át¯¾p¡8µi¤*VÿÆý§­å]1Zj.ìïÞ~í£·¯>ùî¥ÇïñÍÕ˺+»Wî>Þ¼ü¸9~ëÚµµ;õû½­ûÍ¥ËñÆÙ'×^þ¤¹zå¤v¨œe¢m½±Ÿë_Âä #'ï>~OŒT¬°ê £ra>ÞÞíí9ÌÏ‹ÙŸè°Ñ–ÒD- Æ„‡ô2!\Í…‹KF}K¯mRFÑ2‘lSHÔìˆ '»©©£xk/Ý=Ä•¢´ÂMÛPE/k±ªÅCžpŸwX1ÀÂ(ŸƒÙ4!dòÍ€£Ô¬œj%:|zŠÔ‹ OR}DŠ™½8P\­F*g#µíÁšW”Šˆ”h£jȬ `RFŒ75 zd½ÙÇÚ1”‹ÚœÉKy©0Di¢lÄ¥D¾C+ )œ)4ç*³kùîœQšâã1ÛÖ«ó™ÎRsõÀP£¿½sûYóì Г¢^Âø¨—<KV·o$Ë€Uƒñ©Hv^IM³FËCGœœª+;ü´©u9·œîì÷vξØÙ¸¥æfÙ`üæÝ'_ýö?Ìïß…Õ’^ߊN],o>>|ð½Åƒ²íe%žñ÷ï›Á!Å%ç"ͽâÜ•³Wß‹”—ôdëæÃwî½;æ'aÅ +3T›Þ¼¿uû½{ÉÅå\cåÎ?H5×mPІJ3Ý£¹£'‡/|\[º!¥f÷®¾+/óó9Y/•Ä¤B¼¼Ü^½Î(¹sW¾ýÙÏ9k >»Ä¥æ¥ÜÊܹ××n|˜jyñÈÅ;o^¸ÿ––}~ÚŠ‚̲A²ŸN’¡Ö)35lÁ×·oœ»ú’’¾uÒü­¦¿|ÞòܰφϳìÆÂÀû•¦·H­ÄF›¸Zœëa›áR@V€ +cetá +$$…Ô´^]/ÍLoÝ–r}/©ÆK r¶o#Ó¬&}⨓¶øœ78%å +Ð'„3²Å…LºI/Ÿð‰û`Š_Öǧ-/DÊzi.;wnöè¥ÎνÂÒ ­¼1á¥Ýˆ °²Tþ˜‹sd"˜–—‘`ÞCzfœ’mXhÌ#Ø1ÍŽÈ€Dxµ*»òHyN-à\ÂÉB 'F~ +ô  s\Is±Jeápv÷v{õ²œléÙŽ’™òlaœ å>îÇœ ¡¬nò0g,ˆý›! 7*ûIÕ‹k@˜`g „Ñm ̧È`ÎO…ýŒŽŠqp»I6*í•\s‘£™•ù‹éîv(ßeÂYFMà EO€‚hò #v’”2ŒVDŤ +9aÁégi1©ez”Ñ6”PaI¯¬J‰Nmþ\gãz²³ 1z:ß\Ù½-ÌBLºÀh pJf‡_R²Qw')Y&;âRN4 œ°0p&EXÈ“Sj¼…p1`¨lˆžžž‡@i5@*JCXx;¬ Û¾R™°LÜì¢ÀáÌ`ŸGB+û¹”ÅÇDKýP±ç@„‹°z¦8-§DòL0fu´”ˆæÚ.ˆrøq ¸@çÃEZÉ‚æ€~°x8àxÈàQ.àFÀ+HàÄÁTq!€«¶ÙH æ°0˜‰°qRJ‰áL(f'64î ¯N†OŒ¸OùL6ÁŠU"±jº4"|àØ-¸›HBÁ¢ŸOy©ÌÊ(#d*9Z<>îÌ KÍg/4ÖïT—¯Øý¢W:ìÅ?&xq`Ø‚0§SZVI5•dã‚’´$Bkv¿€²qÐ^q>J›°#~Tôh?L²BB1’“VS²-L˹)Ý…É’šZ\=ÜLó@’U!: +NÉ`ÿTSµ ' +T”’»…ÙéÃí.Þ7[±Ú].?@È…¹˜ S&]$0ºcVÔæ㎱š /¢XÝŒ\/\ñ«‹òÀbef›×rr± P„‚™ô9&­0Œ«Ã“ÞaSÀxX•£ÀË?0ø×'Ìdž]'G=#ƒi°è„£ƒ€~Bµž²Á©Év¹Hu‰[½äÙÿÜé »öAÂ|8[1ò]Ñ(ùÅæÃÇí>‹‡F8P "~BŽ¦;™©íÓ6tØìžpŽL:!”&iIŽ¤q^Sy\MX,-Åʹd® þð‚nJ7ƒÑ q7¶bV/ã ð¬œÃE!›œ¨ÕÛ}”TÀ€0a'}¨¦F +¹ÊL²2ÒÁ`$rÙêÁ}ˆòù!׈ vAƒ¦±Nx'Õ +o´ƒ‰)1ZuÃœ¤&Û Œ^€\£×BééduYNL¹ðÙC¡\ŒÁ“u0°‹ˆ÷¡‚Õ‹š\&›7€òîãð3 À€qåÂ5JÎH°C"(‚Ñšƒ+ %³ÆEZ.à|œÕM[]øиÓî£A"®ô<.ÆÇlȱaÛ¤505»½¶w¢Â“vÄæ"Ü>Žã^DôÀ‚ÅE »}˜„óÐ@Ÿ}0cFŠ)¹°Ébó`Œ@´Á &¦ãÔÐu¬ÕM=w|rÒ‚NZ‘áשӶ3î +H^ g ^NßMÏî]G9c†ÙÝ,P‡_pùy” {!iÌPcÍPrÚË¥Ûýüˆ úöq˘tÃ28ŸçOM¤BŠq‹‹4;ñ1+rzÌ‚ÓHŸ€ðÓË\œ ÕONx¾sÚü×''Ž ÛAv¡ Æ*j(ºqx5-Ž»«Ÿ÷’"¤0¹àRãÀpÈÆÖÎõßÿ;3"ýÅsÃ'†­N͈1QKª±,+†6®ö7.Ù`nÈä3{h“‹2¹)˜ÖAcƒáp,/ÍÓra=HU‚×üIÐ/'Ýëôc./Ž²¯!h8jµÛi†Vc Κü¸É ;ý8+g¹PÛ©¿xÚäMvàO¡BÔ  ÕÜ8|'è +G€âùhmÅM†œàðs @€ÿË)iI/Œ;Ñ“ã® '(4ÆÄÜ°8á€Á™DÙh4Úfq&ìÐs9ьז¿¹Ó¢|,×Z e¦€b™—ÈT€ÔP>Îieˆ2@·ÉF,Órú0 é4«B¸áA^Øìðê‰ÌæÑåÆ 8Ç_Ÿ²œr…!X”{T6¯èEÂNæpúF'lgÆl6—OU”l:žÏ¥$PókS _†Í „9†&<£“,9<ÜéÇsÇÇŸ{~Üâ é`žR./CÒjÈÈ걂ʃöxÀkšüÏŸ4Œó¡¼ÍCœs}ë”ÅLrh2ºÎâ€9Z¨Mwæù dÜùpE Ü)àÁX€ÓÓÕ!’çÂy"˜4¹pW€ˆ L +¬ \؇©R–à – r4™ˆ…ÔP˜Ä $p˜ `Š‚êõôÌ|—ä€*’ˆs{)?"B¸êò /ç…%„y¬Ãép9­N§]’¤J6Ö«$×úå »Ó‡Ûýo®®ÏëáLÝjN†G'ýõÜèñ!›ÅA ±X|Iâ´&«Š,‹bÐëÇœ@ˆ9ž`(…)šÅá¢r¨àðÓ§FmÏ=vÒ<4æ¬qä7ÇÎxŸ;í:1â™´áV'iµœ(’ÌùÆQ’w{€ç'0:F ‰I;t|È•ŽùnoÐ4,—òûçÎW;]Ù0æ×—ŒLÚ6?N+qLŠ)‰Â*äg(ñh<›-FÂZ*"¦4l}¾´³Ño×Ò唲:½ueýá­g/>º»åÂÖôt-3>ÈíAì.!t É8)bÆ3X.Î7Š¡Z!ÔkFwç“7·+Oo,½÷dÿƒÇûüÕ¿ùù÷žÜÝÛ\Ÿ­V‹¢ ù¸ÛäŽBÙ(ÉF 4Q( +–’¡ŒÎwÊÆ\73ÕÈ.ϵ/Ÿß<Ü›¿~iíñ½£w®ìTËU#fð<íñ¸‡Çv=fANŽ¸Ç-؉Sæ§L£ã§r;ÜM$t¥VÉ…Ab‰|6™ËeŒH{ëüµ­|2ŒYí¶ «£‚8%Bˆ…Ý5Ùj }åþ~ñV_»5ûôÆÔ;ùÅÝùí¿üìÆo>»ú/_¿ò¿ÿÇoôÉýÇ7W7›Éd<s^? +aÈ¥´8×зfbS9|¦D×Òôl+>ßM-N%Ïo4Þx¸ûûßþͯ~ýÃwÞ~pïÆæÙõ Ñ“V‹ÍºñÔ¸ã¹C.‹)Äù£"”ák qÎFNîÕ£ 56g¢Oîm¿üàÜý›;ï¿vå'_¾ûâÃùbtzv¦½¸7½yŸ‹”½N›ÌÀ©Dˆí)ýîAëæNugJyp¾ù‹/ýåÓgwVÞ½7ÿ?yúïÿüùŸÿÑO?¼øç_=ýé'WwR +ãó8Ýf‹FA‡ $äÐYW/G_?›ýþ[ç¾þÁ£OߺôúíÞÇ/-ý݇ÿøÓ§?ûøêOß;üóo^ùÓOn½u»qs·¼>[LDµd2Ô’r8Ë0lD ÔbèBI¸¼’yùêÜ µû‡•/Þ¹ôÛ¯žýùO?úý/?øÍ—÷ÿçßùÿþŸ?ýñWÝ?j~úÊÎ/¿|ñµûÛ1]1[gƽVE0²Çí ¸m¨×"âÎ 69cÎ-eVŠ›=ã`!qïüôç¼ðö£ý·Ÿ\}ü¨sÁlYϨL'E®Õ¸kKúÛק¾ÿúùÏ_?÷óïþÇŸ=ûÿüóßÿâ]ÐüÛþ݇½~e­_ˆéÊšž I"HA%˜Íaû]þò‚þânî³'gþéÝ/ß¹øÉ+Û_}pñ?}õäþã‡ÿö»w~÷ûÿðåß|qóÅ+‹¹XåX`B,Ÿø Yˆ”~š}¸a¼v!ÿæ•êç//ÿý÷.ÿß¿ù§¯_ùñûç÷ÅÕýù£?ýäþOßÝûÙ{;¿úäÜßܽ¸Q,fãG> `óÝÊR;¹PÓ..ÅÞ¿?ûwï~úòÊÇ/¯ÿ᧯þúËßvø·oìüßÿôñÿùï_ÿëoÞü×·þÛïßüÁ;Vº1² []@€TX¶› Öt_Usž›æoo$on$ž¯¾ópùË·ö>yºöóOoþ×úü~üä_½òïøä§ß»·µTëöfâÅ®WbF<• f­B¿s£ûË.ýý7ðÆÎg¯mÿôãÿö»wõùí/Ÿí|ýñåÿòë7~÷éÕOï5ß¹VÚîÊ™ÏÐÜظãø±1¯Õª3®NÚkK—–â÷jÏn/þü{÷þõ×ÏþãÏ^þ§Ÿ¼ôO_=þú£Ë_¿ð£W—ž]©͆ë *Ä08À!Œ ‡b£Þšïä:®—Ʀù7oN}ñúÞO>¸ðã÷õÅõúÙËÿüóW~ýùÍ_°÷Ï?¼þ_~ñâïxãÓ—ú¯ÏÜÛ«R!·–b‚¤–²‘b”ªÇ‰Ý^øúZäþVüé…ò§O×ÿ£û¿þüÚ/?¹ò¿þÓÿë?ÿè³W¶>{ãâÝk›N{!& ØK c¼¿Åç +ôÁ´ö`¯öÉã/ŸûêÝ‹ÿþ›wÿÏû埾zùïÞ9zëîÚêl1¢2N0AÔ2r(£ªqg“Ax&Üïç¯m”ïíä~øêæ?üà…?þôÝ÷^Üxïnÿ¯¬ÿ샋?ݾ¹ßØš¯”‹å že• @$ÀbƒŠb¤s±XB"šqjk:¾;“¸°xýJëÇoýêóû_¾uá³WvÞ¹·þøÜÔ£½úÕµüb;\ÊëA;|-4£ÎPB*¢&T:­ý¢|s«úÉ£•Ÿ¼½÷Û/nüýüñÇ/ÿë¯ÞùÅ'×ùáÑŸ¿~üï¿|å·ßÝûüAõþnnº aÛꂽœHwEÉP¼&+<õ]ž••Ÿ\l|øpõ¿þã»þÝÛøÉÃùÕkÿþ‡~þ½Û=Ù|voy¶›–µ0ÎE`*œ+´$ž©¥f¢•À{Ih«ÁÝ\Ë>¾0}ÿ }ýlé­ë½_zóçßþôñæûwæŸ\˜>ì§g +JˆChJ@8™á1†P4ä‘®ºA.7ã›ýâÙnöüRùî^í勽§W_¾²¸Ó‰ì¶ÃÛèLä:ptEqÀó3Á%&E5+c¹T:QÒ·PMìÎö§cO.´?yiù7Ÿœÿ×_<ùé—¿{áÃ;½G›‰;‹‘ûg Å ‚»q€Œu!:jv`~ˆÄýnô4|±Üió·×ã/îÞ»ÓÿãOýï?ÿ_ýÚï¾|ñ»Žæ+­B˜^;¢xÖçq#>·Lã /\+\¨FöæóGK…[Ûõ^8ûÑ“ƒ§7®lÔ:@žhØê°MXÜöIOåêëzªËó!$K }{iæÜÎb3Eí÷ô:ï½|íÊÎÌùå2øìúT¢ž ¯)*ÆDœhØ…0Ÿ¦”)g£é†¦ Ë4™Ö•´.å a¾ß›+Ÿ_m.×6zùn1ÕÉ¥©¨¡  €·úh“‡Ç¸„ˇ×íp >ŸB3Í\v¹[ÚŸM][M½sgîËgûß}¸úãw¯üøíË?xºýù£µïÞ]zv©}nÎñ~Èç¦8Š˃¨ˆÅMC*KVSñ(ë©ðF+t8½¼˜øüåÍûûÿóïÞûúÓÛ_¼qñÅËóçÏVû­„Rx-£ÄÛC8Ý‹pªä«1™Kkt!*¤&;¹h+šNñýÌÕÍéÝ~±as¡  +4‡ ŒÅÍŽZ©Scð° 0S‚DqÜçe8Ò2‰x>‘(Æ„ÂFE:©ðyCÒ¨€‚"¢T,v¡ûQÁ ¼*Æò]³0­NÜë#x6˜Igzë3­~)"Ï×ËË¥àál~k*³TÒ–Ë¡~><“7òšàµ˜=nX!¤’ ŸuçøÄè¸ÝïòÊ4—…’LIp'ÅσçfãO.÷_½¶xg§»Ù- M!”Î+ùÅlï2Æ%žÃË«RPå8)TKÅB6©§¢Á”!†dF–yQQQVrSÒ˜ ¶@.T¡Õ,,DÇܤ’h!Œåå“™2ìóèS(–ÎhJ„¥†æ]Øå%]~Úî£Çø¨ ÷Ò‰paE/¯bRšåp6„`\PI`¤‰(êÆB§³\«õõp ÈA%\pøðã£æ 妓¨RgB51\ÁèÐð¨et܆âàmQUMhªžˆ„J™ÈL3µ·X^íÄvç +û‹íùz¶• 7ÒÑ\4ârû›ÌžÁÜö36ø¹Ç°„ÐÅ‹ÙÊJ*ÛaiZ°œÎ¥‚XR„×ZÙû—v_¸~nm~¶V1‰"ñ‚cbÌCn22é¸øT¦{>Ó:JdfY.Üë-#œÙä%H9J'“ål®æ8Fq; _ 86 ñŽZI'µB!‹_ñL;ì¿r{±1 2 AE1T-à a<-„$…†üÒh¡U)¬S 2fÃøh'Þ>S=)Ñ+ôö‚©š› ΢Xõ¬^Z‚ùˆŸ9Vé,Œº ž17‡‡›BzNHô µ ‹ZÎUfv)£>P¼ü`R†ŸKÙ`͇QR3;à£v‰§Ô +*æÝxÄEèD£¾Yp{ÜË{¥IŸäg‚Q“M1R +§¤”o`µ¢—2ül‚Ð*t¨Êê)Ùã ZIÒ¡¢‡IÚ±ÛQÝÇ$¼T̼„ŠðÊSrŠÓË¡Ì´šl#EçIV’´8«¤‡-.³ ñ`!VïHù³^>3î¦NNøžsOøD—gŽǜĘc°&?ÆÆ\ûý˜(hÉBf4>˜žŒ‹9s“Q+",í!BL¨-ŸÅ”ª‰Ø   ×”tOËôG­„Õ+À\ŒÖŠl´é"cHub:­Õ@Ϙ¼Â¨…B¼¶%&zL¤åeS#.Áêd91ÎÉ €ü˜RæµêZ²½Ìͻ٤9 ÁŒQèìbüt0Ù åWŒò†RXñ«ÇÌøqbƒ“†þêØÄLGÚZm;RÛ“}«_91ìqøD“Køö)×)âãÒ\lFËoˆñ¾Ñ'ýÊi 1é$q.êE•“ÃpLgF=f'å'u1Tb¥äؤÛlóC¨ÀãY¿4áLÙAÅm¸ñ­çMR~) çp95»z¹0³ë“ˆVô‹J/±F-˜èû‡fÖ +!µäW+¹ÌÅ»”^ós ©=P͘îòRñ,œ— +kRvõ¤‰u°X°Hj%öP)–´£q Î~6å@ÃÃ62 f¥äŒ”˜qàƈƒ24Dk‘)1RwÁÒ™IèÛgœ£ h Ý/z¨¸Òœ¸n‡e?™° @Ö@àŠÉi>5K'ûT¢Š'ÇQToc¡Z8×_;|”ꜵRa¯˜rƒë›íƒì,çk¶)Ì*¹ž—‡ý¼ ÕQµ‘šº˜š½f´ƒec5"\çRó\z1 ÔÜ|ô¿ò¨°Ä§zV,lA4&:¥”wÅÜ:¡– sÓqpd¤í¢#6D!ÂU1Ý‹Ô6âhmC+,êù…Ùí{„Q‡ÃU£{TY»ÛÚz¡¶~7ÚÙÏô.pzÃHOw7oZÉ°‡O‚ÐâÒKáÚnªw)9})\Ûðpƒ9§´Ñµz XñIÄ»d¤Ñ˜=ßß}Dêµ6ÌBD”Âj¬}!\;h“±ƒ¬a“ˆ”µ#’•†½Â„_q¢!‡Ä¼“JHéY%¿àå c¸ù<¦wAgF€ÚdçL«•Þ¾˜è‚öŽ¸Y¯˜…”*¬¶ÈXÓ*@Äœ(5>Ö!ÔŠ Ñc¢Ója5\^—V'=­·™è î€ß²‘± D?íàíD\É-ÃbþŒ s314T¥âÓdl Õ͈:Ø59ãás@A}RAÈ,bz›MÍ¡‘®•ˆÓÑi­°2êa©¸©HðýBr&êwQ>ѳBcv°r/¢ÙÔJk¨ÖœpÑcjÒytï¹SæoŸ¶=oÂmdŠ1¦„ä,®Õ&¬Ù+P*H“’ƒŒÚ©˜‹Žƒtc"]p¡Ýx}‚OLb¡²˜í»…8*‘Ñ6 ‚Ðh† +ó™©>ÑšDd2ÚôZÀ¦!¹`a2N©îJL¸&gg-xt/Ÿ³‹é™+í'…Å[N. |H<Ý›Z¾d¥c&`'ˆD@¬°‰y½y wÿöÞãI’3˻ӈm¹œi4€ÊJ™¡u¸‡k­e„‡‡Ö:"µ•YU…ªB  èFwÛbvfvvƆCã’³k4’f¼ñ ÿòy w4Òl4cÚZ~¨®Ê áïûÞû ÷ï Õ¥VºÃ3ª<ü%¤(Wž Õ]gtcõÎp»«·÷ÏÞþåÞW?ÛFMŸJí‹¢>€zï-Ÿì?ý!„H\iR[~Ußÿ¦}úËêþ¾qœ×úV÷lqû›êüYwÞåÞ“ßL®~18ûöðÅåö¢¶‰]kI ¦ ˜6¿þÝøê§Æñ¯íé‹¢ÜA¤*à6ëÏW³R 5H}¤Ô.”æ•5x\ž>Î ŽP™Ú£'ZïFhž·Ž¿^ý:ØÕ§yÚÞ+V!1*\iÄW÷ÕÖ±Þ½äj§q:¸Ùͬï/.¿!´V· ¯Ôú7%ÔO™ú fÍD©>yj5¶P=/uÒL UzjçÆ=çkgi®Yîž\¿ú)E—WSüF^àbB§Ïw¿úÖ?{ÿ§¿û·ÿkmr½–U¢d™ó÷ëg¿>ù}ûü{¥}']oüÐ)\ÐWãLAieÄ&ä?evob3Û@1iÒØLS€]«Ye ³ioOéÞVvÞ~º‰¬Ç°ã¦/Mù9¶šš ²’æ›jëÂ?‰õp°ûV‘¨ÚµGOQ¹)-Â0ÞžX?iî½ßþ–rúv瘮±ó ’ª4º”ëKÑŸñþ¬uøÚìqÞŒ*ÍŒîum÷]mïÞ¹À­Q„²òJ]mâf—´ÇœÀÕÎ0{Þ?ùÐ=| ¸m´ÇÄÚ>í.ÄƉÚ;—üÙîÕ»“—?)Í}`1x­Zßo½>þQí_[­½³—ÿâ_ÿOíÃgIѧJ;þòM÷ä»áůj˧ˆì‡ÀÐ]Lïf¥JÑî%ÅëïŒ.~½óôý7œ¿Ë—çÃwtyŽh]¡qR?xgôoøê¡=xdtOW’xcíÞž;8T[©ƒS©\oüì·'Ï·e‹j¬³è~7üGmø’p—ŒÙÿðÓ¿­OoST™ug‹›_¿ÿÃ?¾ÿÓ¿Ûyù©uš›ª¿óÓ_ÿzã`#§æĎܼ*Í¿îÿpðå¿lì½%ÎääUu|±Y’\…,ïY“W‹Çž};>ûFð–˜Ö÷gOSt%E{‚¿'6O™Ê! _uñjrþ-ø¥²[ž¾jG|uWðwpmÈÙ£ãg¿zþÃß<"RËê^[½K¥uLº ܤ6ëÌÛ‡oáç«)9Ž¹jëÈ^y³§õݯ" x³r¥{:>ÿ&\PV, «ÖyÌU`7kVÔ'ë¥ÜÜõí_éÞôŸÙHP ÏŒîm÷ìWgoÿ¸&^4ŸÿâoÍÖQ‚v“ŒG”rB='4€›ÌþãöÑ÷ç_ÿ×/þ¤Vfž}×;xÊ•†jmauO[»_v_wOÞº³[¾4jÌÁçÀ‚2ÞJHˆÊDëœWw^’Ö ¨w€ì*ó7Íã_w.í¾e¼©ÛØ;~þ“3<ÍɾÖ>õf_îÜüpûî/o~ñ÷£‹oíÎñøàÅàè¥RÛõ&·Ý“ÞìycçùÎí¯ÏßþËòøx°÷lqù½Ò:¼Å¬¢6áƒ(g©w¯ÙÒ\ö§ã³7\ežª×è\ò€Øµ£îÙ/çO9©µÏìÉ“Å“v_þ©²ÿž(ïƹjŠ­(õÚî³(%‰µ)áΘʘ;y"Ô÷As®§9LmKþ’¯Œ™Ú.Ó¸;ÌÁ3¸Ìñå<çÔ§×ruÎØ«wÜ:{_žÝz“þÌßæðÐì_™Á*ƒKgò¥Ô¹åP]¾ÔÚÇ1Ühï~Ù;ý ·NµÖ) aŽÜÞÅéóßÑö€Ð;;~ñâ·ÿíÉ»¿ßüÐ8x§7ÁP_?ÿáëßýc‚.¤†Ý9í|Ó=ý¶sú®uò!'¶^~ûW;çoQ¾Bê­Öñ»éÍã«ÚÇ¿ì_þH—vC96¶ltÏõþ•Ò=§¼`r€@Q¬%¥[ƒÊ"­éj‚XOÓ«I./´w)z»š¿GµÏ7s  ¡¦k†jÂÇ ðSþÉó?–zgIÊÉKuÈ„,WÕªÚ™ÝKö j™3»g/+£D,+•y}þ|ÿɇÏß9z£wŽ„ÒŒaÿìC Óò²/ÕwýÙËîñ÷ÕÝ7þò¥;¾‘¼yuú2DkŸ”/Å—”3©MžÍÿ¤uO +J­}ðÚŸ>žœ¾yôõï—0»'ry²{úõäì ë ýÑåÍ»¿º|û¯ö¾üýâéOƒ×J¥ûìÍïn^ÿ!'TÒœO¹S±¶9Pš½P:W¤=õg/~ø;»w B±<~¬v¯õá“Æчó·|ü£Ó9¿xùûÉ£ï¨ÊÄœÜk²r$wnÜÉ«æÁ¶²Ë;½ßýÍôüÛànG©‰ÙSÜÓÞ‚0õé³Î΋“Ǧï1gÁm©÷SP’rvrý}›J­<:÷—ÏÜÉ£ÆÞWö𔈟ʲô&ÃûLyRš=õw¿ö÷ÞwÎ~SÝ}«4Ž€ž}û——o@ûÍ@\ vÇ·Ý×¥ÑÑ:Kåõ·»×ߺ=@û}©¾ouNêÓ««W8zü«å*þÎôâ›ÅÓßé£Ûà†÷Úïíܾù}mpœ%ŒàŽ×½çÕù«}¬5OéÊa”òCC,Mòœ78ûpüî_¿üƒ»óÊ[¾(ÏŸ3•ã­¼%•æÓã¯Yk”¥4”/§)PG Øgž¥ý‡Q2C¹¼»Èñµµ”˜ <ÎÝcÝ=­}æHŒvQmÄfš.QF/Ø@×3‚=ÌÚàRoÒ¥qŽux« RÊêzÓ¡²Çì?šGc8ˆpH­öÁ×­ƒbã°ht# Éõî`ÿ•äM£Ý;ùzzóÌÿîÓßï<ûSçìlu‘—üÁé[\Iã˵9SÌÙ©Œ.ýé#¶<Ædßn,Üö¬>=u‡çPÝti*”ºþðToì†òq&k{oû—?¿ú—­£¯ rqúf÷(Šê9¦‚H ÆˆÚÇÍ]šäIJ՘ñåqŒt +r;F–ãT 48k¯¸5„7?~ú½àM@óþ®Ø< §Nu|³|úÛ _ÅÕ$-m  êÝÁ™Ñ=έ÷*¢7Íñ^ Ug`5vzË­¶XO‹ki)‚ÙˆÒÑ~…2!ºJeR™\N/Þ.>¸ãË€¡ÌTÝa&@ª4­ÞÅèìË'ä—IºʪNëÀêž$ÙÒFNe„8QƵ±Ò¼P{×QÜÈ2nÿðHDà#Tªçø*X]ô¸¹x]™¼K˜á›Öà†-ÏFRŒ ÏÏàF­{röâ§ÞÑW˜\øÛÛ93u™U·¿÷¢6¼lý¹Ö½¬.^Œ.¿w'_&˜Z$¯YÕÁá ¨bR°Þ˜50†v÷¼³ÿÚéœ"œ·wöõhçöãNÎFAªÏ^übzýë¢ÒôKb6!¶p¡-iÒͼÔ+KÕŸyí]"¸›Lýwò"M–¢Ya+Ne‹#û”3%¬ ¢ö0µkuå*˜çlÐ_é°uøì˜Ü8JV†°‚íÜyOmüÓá5öÞ÷@:¿c½ùfHjˆ(ͼÒdªË‚ÖJÏš}wp‚–0:}­ý¯Œþ…Ú9Q»ÇÎè\läåØ¥}Ç ˆ¦¬·'TH{ž—;I· +m-nÝáEF¬aÀû|£¨t€‘íór°c¼Ò8ž“bÊp`Ýý—Õù— 3”ÚÓ»€|N1gÄÒ8ØŽ¯ñ•¾¾&1+TPÅG¥ŠYÛÑëËHÛÎ1`3×’|NhAÒêÝKBmÓBi±w;?}ÀEX=ÂÍ!°•è-Y{+ª¢ÓsºGj}a6á°÷2R/†+¯6¦OÕæÁVAÌ26*Ôâ˜Cê#±z\ÐT§A}>ûœòJ¹"û»£“ï'ß õó•¤FMÒšlçåfÆ‹Âû .Toáô.EI»Wîj5Ï;®Œ(]¡²cuN•Æ€y†¶+óêì ¬&çï‘åm¼Tà ‹ÄÊnž­pF·sð„+¼´ªt‚ý¾Šúþù›ÑòšQ}ÞéºÝƒÚüQÿìëÚâiµÀ-ÒJÕz+ÛÔVF!­‘=¸ní½Rk{ÒÙJ‹¡îô/•Úþ!d-Š§qJãÄ_|­ö¿T»` +:YÖ¦e´Q¥ =É«ý¤Ð‚ôcìq +×Üþ¹;z djnéÒ,#5âL%Œ(˜äg9Ø­¾÷¦uøNmžÂBë­#Ú1v·9¿mï•+ÁËJà÷£¿¿zÿ7ýËïÅöYŠ²Á„:“'9©Pœfë€]FëääÙoG/ŒöÁ0fýéB"¡œ¼uñUûìW Z­Þœ^%®u¹ IEÙÙ?‚`¸Ö“ ¡÷wÆÃÒÔöRB-Œëy¾RŸÜ¨•ÅJ8÷`;³À€g…V^ëg¤&,+Î{n}Z(/±Þ®Ø8Q:× ê²ðrDí™]¹½ý©eX'F•P¥ov)3XÖÍ‚µfhµÃì¼Ü§K{ÞèYsñª2¾}gŠ|µµx ¹.ȱ¢'l`IÞ?ĬIÑèÇòÂhïymt™¦ÝYäÇŠÖVNf­Õ<ØβëqüŸö.!7S¬¿…~œÎ‰Û9M‘Nÿä¨MwúDmžàÁÎœF(ÅàœkV§ó)ÜÄÕ`ç«s`ã$Š2­Õæ§/+Ý]R*±n?ÅÕH{fv.X{†Ií¹æ½Þisùx#-DòêfJx¡!uÖ™Þ<ÉR¥X^”‚»½ºS‰deÎœbõ3¬HuÀ.(% 'x@Eˆµ¡~Jú'˜³Dõ~Šv‚ +B5Y4q©EY©yn ¿ëG ÆÜF¸¨èʈ5;”ÞÊsPàF÷¢µÿº}ôžtçÑ"ØÞnipQ_>ݹùnïñ÷öð·G½/—ߨ½óõ¼Dïônù2¨…C®2B4{ý£¯3Œ« 6Jó×ÕåW ·HgV;Y®B( øÄm \Y(ïX½ëÁÉ7íÝ×Re'Í–7³i xX@NðàJë p Ç×£¨‘aJÛqÌÐ[gƒ·õƒ×Í£öäEV郵1;݃×Zë®ì’Î\¬Â˃îr|¹ÒÙ»yó{Lë†l8nŽ‹ZÏì^[ƒgÀ3›šreŸ¶f´5•k'àÓ×3ƒmœ‚mÓ³›aÊYª\જ=.ª𧡂š&ŒéÉËRÿ8Nš’¿ãMŸ™Ý É?„‡rÊ[èZœâÌÞjÃäšÓ?o¾{ôý?No«6×¢X®(u–õÆd&@e¬¨‚H£AÉðÕÍ4lÈ£÷¢Eü‹I ¿Å¥*oÔ1^çÌku€ø`rBiîçëÙ?_I…Ó".Ô9³C…­,ƒÕXRÔãœçÎrtm#ÎBË[I|#Œ‡â¡‡Š‰¢ì•QÞÕZ— A¬&©e¡:¡dH{•wìÞ#gôTlDqk%F¥©"Öq­UÔ;Rm_ïœ1¥)*Wãy2™'ÖäÝøÊZðÍo:§ßfäÎf–C9CqZj©+–li¢sðõñ×ÿª~ò5^Y䤦TžržŒM»Óæî뫯ÿöìõßµŽ)hYÜÐkKÁßA•ä[Þ¥Kû•éWFë2/Ô’¨¤”'reÉXý óˆÐØÌ(QÌ¢ËËÚÎk½}FÙýúò±Ñ=Is@¶¼ØüÃÒô¥5;S•ÜÎÎí¯ßóZsgtísçtiÞ?~S›ÝbJݬ-ê;ϵ¥ú «l…ò¬6¾|…+uRk»ýkŸv÷Lª¤PÛFŠ‡’q킇Õš§”>Ë; b˜cµOywÉ+Y¦RÔRãÌ=©L¿Ä´`)*Õº_1î$'Ô‹Ú0Œ• ¢$nA†J}=ŠñfÇiîÊÞØlŸ Zg« o¥Ñlkåi,¸iÎQªû¬=%µ~ŽõS”·žäÖ´TžQÆp3%æùz^jãFÐw,/ԃ߈îÝÎ ¤Ö? öúcÄK‹)ÜM ÆVVZ•ÁÉèø Â×?áð„p8«Säkñ‚²§?ÛÌÅ +Zs©±ìÏI¦ ÄLQãÖ$:Àgð}Û¨BÛc°ŸïÄîY”r·Q«¨ô¸/J˜ì×gO÷žÿEóè;º¼Ø +nNT0ÞŸ0š¢¿P;§à¤èò.h`\.ya4\©rå ïMAaÚ'L㌔\]%/KÛ§÷Ow¯¿[ÜüVîÜ$Ik”ÞÎ0NŽ«˜ís¹zÆì?漃$å%QUv‡”ZÝLá)Ò`ìIš¬`JGmŸ@-£r“Ö›µé%¦6S”KYc®)÷ª´xÂÖ]°;þäš÷f1Âä«­ã÷³ÛßßHíxœæðÌ_Ä—vvüù«›_?ÿ£«ï­Áe†´œÚbïú[tÇõªE r[ò–ŒÚÃy?œ¡ ÞÞy ÕZTz€ Â%qp¡Eõ¤`štêãÛÆòWÙ£K DîlTXGJ-ñh‚0ûÞôKHéBPì^‚°!C6ã$%Uh¥BÊž×?ý=pOYºLƒxÑ|%¸“¦Ê[±Àú¬5!´!ì)ímå¸ÚvV.ˆ“NÒ•*ÇÂ,A™X­#³}š¤*ÿÕà ­ )nå8å%hÔEmr£ù;)xj¸z[ÛJr‰ ,Ôž¡Ê!Ìþt ÝHÑ TÈWöJÃLJÏþ´ÿ„ÐJó,ŒªMBiÌÚCÚÛ¥üC®^ïuëà» ß©ÉÞ@ªŒµPwg“ªnätÝÛkOŸ‚ûHÓ&*•1½ ®wôÞé_EŠâf–È ._YŠþ}ÎêÖç›Çï˜Ú~A릙 +*w({šc¨ ½¹§u¯àÀÀ —Œ$BmIÕCLë=µŽÞ¹Ñ{O™Ò&77SÄg[™4e1οúaÿù_§„²|•i£ LJ9¡uZХɋúÞ;Üœä˜R{zi5wt)Fº1¼”ãÁAk³—•ù›¢:„9O ß†lyË2~7Êäͦ`Ös”J«J÷ÌힸƒkÚ]ääîƒ(³¢Ñà’ªòf’LCŦX^Jþ~ž­®'¸Ÿo(µYEo5F}¶El]¼´¯M¾®ì¾–½Y<ǹÕisñÜ⟭æ×ü&bå”!íŸiËX^ùb3/˜½Éá×BÅ?{˜†' êPif(ª8Ï—‹r](eo$¸]Ƭ8'l¥U2jûNÿBð¦E¹OÐ[ `[i¦ ¨®óUš Ju§rý˜­@ù“ØJ28=Xá¼¥ÚºC Ù †‘qƔѷ:ÇTi¶Y47s2*ÖõÆ>ïŽíö™¿|;³¢9J‹mÌHÀ<è½¼ÒåÆžsÞ!nL± Ú8ŠÙÐ'å_u±a“$ zIòXgɺs¨Üjbr+ÃVCˆhÌš þ8¸$SÙBäÕ)Dª ^Dªaöœppüz÷2„(_DðpAåÎrÕ‚Ô Ý9fN€ÐkÓ'vïŠ+O¶v Ó:à˘³ž‘â˜%–ÀÎy¾±#ƒ£ÊKqÂ%Ì©X= ŒfA1"|…ÑšÛi*Ø­š†™¯Òö ÅVVÉr5pQÌØÌpˆÜ*åéW ⸹Á£a¶ÿÂkïg #VP¸™§ÝíÀc;^ÄHi89ùñ/þ §×·sòv^…b¤œ½`,ÇF²´âÏi£ÉËIÌÞÎÊ “@DáreË‘4Ï‹E¹GØ3Ò^äÄþF‚ %¨H†aÕhòO7ÑPFVêÁYF÷Šõ׳úÏÖPÐl”Ò•K‹pNÒ«»¢ˆkãØ.¨&\ò³g4ö ¹l~ÂX 4ð)¶$8c¥ºg¶ŽDoÆÚ}Öi§+hŸ—p¹ ’†q†˜Z+Ê>evgÄUvó\Œ3UšÇ½Óo—?¨ËiÁ`3t)0eŒŸâ[MÁŽ^í#¡<Õj{ ÀÈçÕnZla`©Úøú)pVHë“/ê- +9Ê*iª*x{˜Ú^ÙÊoÅ ÚhGÐF9¾½U°Â¨š´3O± Pbti³æ¤³ “î§1òç1|« ’j;KZmÉ•9ˆ¡~L–vÒR3„È ¸ò¼8–e<¹zhõ›í‹ñé/Íö¤§6äò(”a6ÒlŠ©bÚPožõÞ³åƒQÞHr‰¢EÔø +iM1îÕ#Pøñ¢¾%"`ü¹çúÎ׿ù³ºûÙfq%*¤(“"¦ä1Ð.ÂU «áÀ ¯ŠB%اWo +î1û ¦7Oxo78å‚ë €sl)‰kqLŽ@Ä*(¢$é h¹…3¸ò&7ÎàœqGÁ&lr-ŠªÛqTÇä6ˆXð&\y鈩ÃÌ_-ϾÒzO•Î±tg@¤:¨hT®‰Ïh]ËõKÖÝeK»„96Œ 3E·z1TJ»rõ 7fð΀¢Rí4È+¥®z -®Q4çI®ƒ(C¥v¬Vˆ†1Îäð¡w@2aÖ1ƒÎõru63Išë)šµ†úÇs’ly±|™‚x9g;Ïx}Dêœ&ÈòjJÞÊéˆØQüCƯƒ4*h ™Ól¥ ƒ¨m¤õ´„(=gpk´OÞ?ùÓüòˆ:Èò-DëÇho#¯F1Ê3\0T7‚M,³˜T ¶¦(µ—!H¡<]¤®êíªå%.· ÕWãäÇÖöÒÃp~=‚o¤Ä¬Ð†#S@$íÛxkz|ºeÖãPX,¯‚lSÜ&µþ|-÷ó $M¸ ÌÜL +ÃüÇ\F Î$G¹+l3Ån¦Pe¸ÒÎÒ•b¥0Óm,;ó[ð)•0w@$$¨rŒp’Œ!Ý/’ìj’‰JèCÖå¹R tå"9å] /gøDk_Õ>®vQ©¹UP?Û.>ŒÓq̆ÄÈ‹õ8®B‚±ÁÚ`zUÚ„Þ…G7W“ìVN‚ Iƒ“*ðyà¨CC‘\iS;jóD¬îÕn–÷¡Ê(À­§Ø8"SZ‡±†lYÊ)òÁnœVóT4ÅVZ€1©D‰€«RÛ¤ÚN„ßâJ \̪=|&5. -•ê˜…àt·RÿF” WÑšgRýU;àÄ£¸õ IÆp•»`´£ê(Z,EqÐíµPN[2HFc•@ Û9qw¥ú…àË5àÊázšt¦ £Új†Ãõ mÏåÖIuœ å™"L­4xöþ/'Ç/3¬—•:¨9FôAV®ý½DÑ* +žÞ<Ø̈€AKô¢›“û¼¦4¯AÑb}0–Á-(ØmsÄ ãe½û¨sð–sFBY„rr¤³“ÀoB¾­'ùQ6E7ÄÚ) ÿZ”ÉÀÑu˜U”¯ endstream endobj 29 0 obj <>stream +aE\°\;r·[i1š×à‘£|ÞYB2§Š6È­PNbìaš´ÿ|-JKÚcÌïŒIµ“Dí‡av#©àê I—qs†¨CxäÕþ6nCª§£Õ.múIm$VŽòb;Eºy®Œ©-Þ—‡OÔæy†m&Èj¯¤éúfV„âV†-#Rü/>`¼?¨kÐ]uZû¨!MWRð)f;4ÍFžuI«+Tw¼ñ³w Ú&Ç–1©FÊÕr÷°>»fÏJ-@ Îb‚Ÿ!u¥ŒÚÀ1DKYÚβÀÈCH• an¥XBª©ÞœÕ»¨P%A°‰-0zItu4O Óµu!V–xÐËlÊ×Ú‚7O1eðãL àÉÍSÚÝÅô pD¼h=`Œƒ®ÖǤ½ŘªlyBõþ ¨V­<µ‘b@ƃӯ/^6–¯2œŸ L³u E‘"MP›Yq;/Cδö^¯~ÅU–±Q=•§Üf„QÀm5Ácb],·?úÊ 9”Òw¼°žWãT†°Q¾¶Á@Ý=ÌŠT‹µCD®ƒZ[P\ðÂPŠ§Ì¦Ï±—ãZŒ5Ç¥új¤¸£¨ùy,˜t®ÔöÕâÉ®¾ýïìÎÍç!<žWãm5J¯ÇƒËìQ©]ê?òÆI£¿™;‹çœ3Kड़¶Yt,¨”åäôý›Ÿþ‡¸*¦š¤}Dî« Êœq|ìÖQÊqÕ ãS:¨©¨…8œÓ x|X,(ímÔfaKG\é`±@WGQ4è"Ö™½bJt>¨ñ8é"rÃìžQ¼-†•R„ƒpe„u¾r”ImÞŸWÿÖÎH £pÎ:´ìa²Ÿfƒïn¬î¹V?L“n²¨3Z‡”ëQ„[ §"9:Ž*œ»¨LŸÕv´¨‡Ò.Ti¹Ë ÑœXà*¬;Ó:Wbã$Ãy¡ É² º4Nè¨ÚÊ Ð9le R¨,»¢¸A9#£{¡4NeEn”2çàÝ∸eklõµ^AnÎd­ ÿl#ŸD4FïD²|(A€‡Ïäwúdœ2(7Çí‚ËÎ06ÔÆ·q#ø2nxþÍôÑzuýÁþkÖì!ëíÀB„6Ê7‹b#ŠÈP,”Þaí1¨¸Pš—‚Dâ›y¡Çퟯ¥F𕵖7sJD;nÅa†¥®Þ:+°~8§¥=D—ê%ŠÆvFø|#õÅfÒ¦À6 E¡âÌÁÍèæÇÚÁûòôK¡r¸•5>]GÒx ´¾“¨ü"ù{Þð±à.© ,7<|+{» Íâ”OTÏê‡ï/?üÍóïÿæôÙÌÌ€¡Iš£$UÍ,¸3Æž’Ö¨ 7· ÚFVÛ2¥«¨ÒH< cFF¨&>:ýœ"òöŠR •êqÂé›g=µ<¯o ÃE •[ˆÒ¨À·ŠòSÇ ‡r†Tá|@ŒÈ6Þ/ȽÒø©;yK å–!ÍxQ‰ãÁ·IÂN¥<faÉXSàß/¢ØZCy/ǸIÊ)H°JýÜ<‡ˆBIfu»˜x³tšj­ƒYÚ]æ(Šë›š´ú\e§2ÿÒ™>Cõa†d+ïòb%'x@d)ÆcJ;¥é«òôÀÅJ¤¸™ârl%Mí Aw¥Ù:n/s|¬. ýg¹Í8Ë [):|·ØFäNšõAËqî0Åؤ޲z·rã\ +_ö‹j |t’)±öÀlDP³è0¥)h­$î†RâÃÎ Àþñ‚òó‡©$f0æ€Ô1Â[KKkia-A¡Æ@™$ tHQî’æS°.ëIf-À΃0¶4S6àÒ?¶9+ +ÞÝ-ʳ¥90;˜ÓÏcÔÄœá:¤ÖQ*S¥2Y‹Á4º@š°¬kI~5)æÄ:i){*Uö‹€¯AˉÕcÉÛa.Æ”¶Ó\‚®Bnƒ¢´µŒ0j°ÖHokÝkÊ?ÈH½m°©àÚ€j` Kû¸½ÀŒ ,ð2dcŠö F¶ êÇG+p+çÃjFÀŠ\vA¨ƒTN@f"²­ŒžMnþX9øã[ð& Âα + ùpØvp­ï5YšgÁ€úEt0Ô€Þy¶šçky®MÛû¬w&Õβ|}#…w{7F늲qD{“ÐÇ`XHµõ0É€ñwû—ÖàIZhGp,* àÓ>J“4¡eÙcz_O/EØÃ0anc*iöHk.š @±)7.ÕÎ-SÚá΃J3iÊ ®à%+P¶Ecž—{ ö­½•æ +¬f¿µóÄ=ByŠ†ã”’¸A¨­$aÄ1°3çL*³çþâ¥?™æj tA…ÒA:=Œ¡<Ç¡íAšr¶2ÜÃ0¶§!±³té³ÍüjŒ¦ Œ±3|!ìqjŒ!ª%qu;¸¤‡_OrŸo ÌÎä\O V­ajpåCðbNbe@?¥q +‘~!ó|Ýl_êGli™Àtз+q|#M9?–?[ÏG +†Ù:™^½w†§Y()øêQ,ï5ƒ†§œ…3zZ_¾K2µ,ßÉTòÍí¢¹–æ×R\ž­”º—­Ý·ãïíÉËPÁXM€àâ˜ì–dýŒÔ¦Ü=ÈR±v¦$”ÏjR€´Ì²5Î=©K¹™•€}ÖÓÜö?áÕÇ{=©@¦"BMp§Zí%Nxà»­öç@©z D`S¬íæõ~ÁÈ­óÖéjç&ò#Ao$ÈdQeõ¡\9B•éÌ!X\Öƒl¿•…Y•à¨ÔÆãNáHòB+Ë5Y{Z”›1²”`ª„1)O_ºã/)g™åàŽQÑg¬®Z¹£Kª4³‡·µÝWµ½·¸5È…J1ÇÎàq^¥Ø¦öŠàŒx(3”¡R$¬þ¬<}1¼ú‘ñ¶b*„ÚÃÕFQõåÆ’.ï#ò8ÇgH’¤»žd!–XAA¸ã @°±¥ù„: äÞVF^‰‘¼’&Já8š<ÄK}€Œ·aêà dcšºð÷Õ4ʉ Òî_¨‰Í3¹yFê}ðòþ0Ï° +¤1¦¬xsRï’°j5‚©„5tG:ÇïÆÿ@y{k äÄVšaÕ:&ú1TÝÊ À5‚(ùǬ=_‹áq"8=ˆê` 0­'mÀ4ÞÛg\ëÃà:®ZÔG`߸Ò"Í7ÀmöeM0©ŽÉURkHõ=g|5¾ønpñK¥{•âZQ¼T”AçLò|¥ õhg7èìYÙÍHÍmÄ6“!-ÁÈÁõº»Y¥—¤ÄÊ>ãÌ€O1­Y? +Γ <¶U†`NSd9‰™kÑbšÔõÚŽ\Ýã¼ý [¢N¸Ééñ.¬ò"h…Ãx¢·C»‹¼ÜIàÞj0ñCKí¢í~È’ +™ß»+{`l· +”m¥É›ÃöY&Š/ÏäöS9ÁÔ~23TÜÊÊ I­/±¢M[ Úœ#BGp‚ïF6S|†,ê¬Û.è)¾ä’Æb%Ig„ZNj§8?J;9µ«uoëÇ¿ÕÇÏ r + + =N–äÊAyô’öNrR?B”·ƒkW¼4í-æµð)L#h9\ëB=‚W +åe°ùˆÔÒZÁÍנè^Þ–Çg¤ÝÊJµ¬8vN—ö²ŒSý­‚Êqtpy€¼‘C9œÓ¶³r– +¾>ȉ©U[û/ØÊaNh’®†q'˜>ˆâ60xpéWi!ÔŽ cà °iʤÍç´H½b¯hŒë»ßHK8Ú[äEÒæÄj”°â”#Ôv'ïÛûo”ÊŒ+Úy_š<É+=(¨(V›CZ3oòT©ïnçÄHVNáeHN¨Ö‚6 ã%(–µ$ŠU©<‰“¦uBÙ;Í÷&¡‘cüÍ”P ª‰­·QT£ê”¶$Ÿnä7Ó¦t@½¯g”ŒÐ2‡”ÁSkð dÉçÛøç[И/DnÅ(/ zXòåH{Thx>ˆà@4Àª`ý¢c¥}<XavªhEòÁ¦iª\Z›)i=ÎGÈ +.·ƒ{7øJ¬h|ÆfoicX”[º-hfí ÍUW Ò6i‡‚Ûº+9y€Ê#Ù?±‡Š52Anmå͇ )œ„ .¼‘G±ÊÕº˜>* ƒ­È¥ÎeŒ¬läÔáă Ü,çÃö>U:!¬¼OŠ´P°´!W»Ç¿Ôº—¸=‰aÖzF€…H±`Éå­‚ +rEmݸ£—œ¿L[Cð O² ¯”V”´×Òt³˜rpSU^¨ü<„<ˆPƒ¢|ÕH—Ãd)/·9?¯vĹ4U ŒpÞ̲~–«ÀË rWi^fÄÎJŠß‚¢övÄÚ)øA¾4a,ÀƒÕ[jeé=ÜnŒ•$8/  dy•7r¼ä/Z{ÏÆ”æáf^EtΙͣà{Lº"z{jý$¸F½´\MÒ¶ 礢Ҫ‰ReÂ=P:OiÐ •šQTÿ|»EHuD{¥¾ØÆÁqƒa¡Ìap¹ f[­“—409h⋤K)Ä—œ=†.ª(éD©ÚZÁYËšyqhžèƒf$ŠºTÛë¾]Üüd´/`™VbÌ + Yfì€ÆzFÆ´ÁþË¿ó÷¿Eôhõµ‹+}»}5û_üùöÏVó¦Þ8ÿ±}ò-çÎà8q`ŠJs+§lå +y ìùÑ«¿—jç¸5Þ]M•#D)Nûi¡›—†P;)ÊË3L­…ólV»6î¹kã>»6î¹kã>»6î¹kã>»6î¹kã>»6î¹kã>»6î¹kã>»6î¹kã>»6î¹kã>»6î¹kã>»6î¹kã>»6î¹kã>»6î¹kã>»6î¹kã>»6î¹kã>»6î¹kã>»6î¹kã>»6î¹kã>»6î¹kã>»6î¹kã>»6î¹kã>»6î¹kã>»6î¹kã>»6î¹kã>»6î¹kã>»6î¹k£û¿ý'2Ì•ÿDƾ±!ìùÞqüÉ£ä:ðo·wt<:üÄüd£“`ùÙàx¶·Û;¼X)Â’ñtv%áŽzË•­zæ +þ Íý‡ ìJk%L­¤’ùàåÓÿ¨¿ü÷/ú÷ÿ¬dàS…‡›]ùøŸll|<Â`å‹+ù šN}RUþ³ ãEP-†yÞ/ˆµçgèJ‚ôsbQû9¾ž&KÿÔð›1G˜ÚEäfœtÂE=Œ-4ÒL9Ëx‰¢^*„ÖâÜ1iPmPPûY©‹³HÑXOàë14šãsl Ó;”3Ã9jŒ³J;Ƹ¡‚ÂÚ=\o¥¨¡9w)úGy¹›¢ªm˜æê1ÌÞÎ+i¶š—ÚˆÜæ¼e‚«®$Ù [3]†ƒßÊJIÜ!õ¾äïö8¯´¶‹Z’v㔙˛ˆ˜*¼¿'6ΕÞ¢¼—äªaÂÚ.ê‚·«u¯„Æ1ï(ÍKÜž§¥æVQÏ‹ çÇpk#'n$™8*§½À—s|e=ÅÀg¥i 5¶Q3‚Û¾–å|Tjf¹jŠ©iµ3Á;L²j¬eĬ)êŒ5"´îvAÚΉ˜Ô&´~ž«‡óz(-Æ‹.5ú|b·P¡¹™„ñp^‹¢f 5£E» uór/+´3|s5-†rj¬h'‰RŠ(!B Þ*†ZºFŒõ43ÃœjD—;¤ÚO’¥XÑYÏ*Ÿ…ñÏÃøj‚ÙH0)ÌÈQnæ0¯„Ò\UW£ØÏC¹Õ$½•—áåaÄÌе4éÅ1'”‘`rB=Z4V£x8'o¤„Õ/Ú9®™¡ë¾]Tú˜Ò©ôÎe¶ãD SLi†½­œ´•„°Ò4LW7ÉÖB#”• |s¦Œ1HMÞ™¤z7C9A8¨Ò/ªƒ¡›i!YÄ6ybZI²†ËRofÄj$p'ËÖ $„ï#R±qÂþt»ðç›YÈ̢آõ¥ã˜ùÙº™"ù`óO72Ãh +Õó\³ ´2„û ŒÁa$0k-Ao¤9˜0b­&åí¼Y§+ym” ­4ÉËa¢ds„òT9ÊG¨ÔQ½½›×Ý;|ñÏDsôVì]Ôn(',";‚Y!Ø;§@ PÖÔ{YÊE(o°÷š²›y%IçèŠÒ¸ëçY¦œ&Í$û±+Ò0ÍTã”%+Y©GÛ ¹v„«½8ªÒzWmQ¥qVªæ¤¬/ª ¹ò²¹xκÓ$i¸*[Ú¥½ƒœÔ‹a%@*˜¢$aGPu%N¬§XÈ1·4S‹~Žk§(?Ç;§‡rBš´ó‚%ÌãqÞ¾7WÝyëNocŒ•ã=Êîs'Y¥Çù'¥á3½yZߪƒí ¡ŽMÛSÀ.Ìœ¤øVV¥ƒ·-¥vÐ95Á®¥øµ”¢ƒÖ [ñó-t=Á¦Èdòz^YCôUM0í4ץʇIÞ_M10Ï„5åÕÕ$ÿY„ +åXÙp^Š¢Àü+‘œ²£W#äZŒXˆæ5ZíÅæJ„€¤‚Ÿ±U\ ã"ØV^M1Uܘ!Ê•GŒµKópÁȳՠ“QŠãå,×N³-œ˵³4뺕_‰ƒÀ8Y¶ +P‡Ä+Ÿ®ç>]Ïn&é¢g™f’®G;Z0ó\‡:€˜à¯&¨•„°žÑÒT‚…Mp8ᤠ+™_l£[i>…»¨Ð‚'déÊ7Y¨ÐÁcXâÿr%ZÔÚbe,¸G×¢D9Ayûã<¤ÙJœ.E‚Žb.£vÛógÀ†)ºŒÝJ† Ú_ÍY1h;¤÷YgôÌPši¾š›I¦šã›qÜÙΊRyî ¯™ÒDð&¤;ÊI ÌÎþÌóÒ½z@Úƒ‡)z=Í'ñ2®2l=EC1–7sò¢†þòâFNÙFDìÆ •µ› ²”e} ,Tnµ.¢[çsÕc²¼LòU¾²Ç{‹¢Þ¥íA^o)­‹Öîgô,Íù›e3'B\€¨ŸG‰µ4OS½}+×΃®`J'hŠŒ{piáa’Ý Z\;Ñ¢•¦<ˆX/B–ãL#ÅvyÂûW ¢Aì=Üš­g¤‡ n5!d™j:Ø^‹aF’(Î$‚9‘W¶±í´¸•6âôÃ(/ZªÊÈcÌZ‚¥%€‘O7òk &R´©³YÐ62ÊfF eaÚñ ³TÒ&„hðÛ$í祠:@zxˆ¸™f!±Ãˆù-h¢”ÄìÏ·ŠI~=F¶Qˆ!.L#,@_¸`!lMr&œÙ‡”^Mpiy=Åoå”S/ê;¼w@Û“íB€{k1òaÿ|3ûÅš£ÊNã˜0Æ ¶¾Ä…‡ ,àsAhQâæ~§ü$åj¿(ø‘<‘–æjˆ:¤Þ¡R=‰›¬>( ŸÆ‰RQ£E3'Ô‚Öh „&eNäÚ~œv¿HÐQ Þ¼…É=`Ùê•Ý7þvtù!ÊØQÒÌ)¢ɶ¯ÖOöyAÚ6@‰E3E×QuJ:‡¤s@˜K̘@í¹ +eC¨d½…XIªBÜœ¢úX®ìÞ—ú—›ˆœåj\yOï\ë½[½wC¹SÉ?ûê/˜êäaVªM2$[Ç”>¥ P±¨”ŒË ZrºÛˆpàÅJBµRÞ§[Ùõ$„cÆ@kQÕõ”.ØqÌ£µ!¦¶~ÅWã|-õ%åì‘ÖUÿ'KïÁäÆ}¥ý~ˆ ²Dr"f9§F7:4ÐHœs`09gr˜Å$Q$E‰Ê²â*y%k­dÙrÒZŽ²d+Û–e9®×û¾w÷Vݺ÷4ß[…š1À?œó<¿§»1m"’P@°5*#>ªóŒ½“vÎN¦ÜLÎÍdÍž¨ÆáWÛ}à,&RëÁûÆŒô°žœ°fJKïªâ¨ Òyn5 ë0•‰UÛÀFƒP®Ê=ªÝ€dÁ#lÌJÃð h½RÛÅ—lT–:h_ãôƒÎ iHwOL! µ19T,{¥šAª ‰¹¸J„êzoÄÁf¬rÿN.µ(mz#-.Ù_=¸ßW˜2Q€@vè&©ÄŠ +• + óDàûq3mB$å~-þ*è¼Ô†ÌØ™<€ lĸ‰7â“°ÁÉA6Ùå¶îêĈ¼'”{–€S³EP7‘Ñ#Ña~Dã3`“JëôÙÉ„`€Í#L*z$¥wUG ô„U€I7Òã&Zï–@'í¨Á;4éÔ9*#;¤ÁGô$0"Tàe£²qÊ­y‰„Æ-Á˜L²”üºL¸˜ì¤Í7af&@…Œ4Œ_ëºÆÔ¤‰—<<å Ö‚ +Á++_a ‡õã Ó`Od¸mÄ€÷V*e£am3fotÒÌ@­Â ­^Ù葆 8ô Nݼ…yp̈+‘D’HÀ^ þV °©‘®›Iy¹d¾¹«o€ý9ù¼KÈ{ƒ>Þ –¼á:à({ýÊMà„Ä,_`ã‹Dd@Ææl\ÍC§CÅÅI—4n§³¡Òv¢u,Ý?™ìG"U›ñÆ“šSc²LÚØ›œóåÖ ¬¡R†„ÊÊ-ÝA>“a_f9Ù< ×`) S4E0)7Ÿwû Ûx¨ic²àž°°ˆ¯Øo!\r6Ù9A§fÉøt ¸d$bãVÞB&¡ü ‰ŒÙµË´€ð%å¾Ñÿˆž@¨¤Ÿ¶P‰#Þd¥² Bn¾lp„Fa%á‡VÁè~–mtÞuÌ€ðíPT®À>b¤§\x ø§ÎÑ»ããfåB@>#: 0àöÚA¦±H-x„ªÚœïs…¬DÊŠ'Z®@þ¨ÓÔߤ_x µ•7cò¤Å§¶ú4v°EˆÆãSÊsÂ.¾ Ë•€Ku7§P¨Öå·S)\jÀ<áq#9ia î𸆪¨7P „wÔlH‹ùNZYxŒ@Kix#*ƒ8ÔYÑ(ÆÇ,Ì rdÒ9<åÑ:ý`Ǩ¯¶;b"aÑ ÿÚþOC:³r›¨$ô‚‹ý,¡þ*¨ZèX¼‹B!V^£bmp=<Ô² 3--Áœ‹ƒa”cÉ9 ‡æõ19ƒ71á ñ„[(rò4ªª¬•Š9ù,ª#þ"UÍ\ÎH§Øälzú“@¼²ûÊ6.ï ·¨ø¬7ÚGƒˆÛð| "AuA…è9ðˆ¥ ?f!MDÔDÊjO$8à¢@i—†† 6 D”÷™ä¬REhÈ-V…Üf°~€ÆúÊcåhƒ‘ŠM¸}©I&–¼ñ%*‡Íi=!µ+jIŦUvlJ¹;Ä% "drÌDY¼1<Øu1`ˆXæ²ËDlºÃq‰„ú÷y¥*nM"ÒÕ êÝkå Ó"{Ä ? )^e†M„¸­wŠ6,ý;»1åàaI§lÐÚ1³7¡wGAÓŽhP¼Ý­Kü?7¥1RP<硨†´Þ1#qÓébjÈJž’°˜4xñ¤…à?n$ nQãàÆô”‰8µÊ›¹!- eÅbSVæ+£:3"9É„¢½]…Â0Ójçf D¸7åi\ãÚI§@s”»zBG¦>aáþyLDãP™)¼,:i¢†§ÜÐ*cQÔ&F‡š ¼Ãzï¡)×-*«¢Ÿ.ÑJÆ<¾ü¨r¬/àfã“üÈ”ùÈ„ahÒ¢±q›#A7ŸÎ‚3¼r{W¨« ðSYH£Çç¤ãvü¦¯qú`xð æ¯8ø¬ÆÎxƒE ×!4Ș•™piþülZñ›œžBXð)»rïØ);g%¢\¢GË]\*ó¹Y»/oc³.±bÀ“jOØŠWØ”7MdÂÉf€‹ Ù¹¤†MyBà’\rÏ-:¹,pnLºýãVaÄÌ@dƒíàã3Z@þÞ@iÊå1Ð* Hïqr?]r +9½CÐHyÔŠZ™q/b&ãñÚžÜ>e÷W!^q‰iQ;<(ØJ™ +@Êó•†õž) aÁBG4Î!Ø,=*gÄ€÷|cf•Àt.Iç_ó¹q`mÚE§§l <`¡†u^(Eµ)8¬¤§ ÂW¦Ü>*Aì…MT’ €œ™‚¯6oXccÞ?V׸Ø ­SRd ŒÉÔÚP„#·“ˆ]℉2{d+d@Âorôh²ŠNÚX`žq3 õ¯2q P:§ü?j@'̸Á£% ßÝ6n;rónÇàŒ Q¤T_=þ¨Á¾mÜ2®GïÇÍ X¡ÊîƒxÉÍGòý#Æ)½Ã‰ùœÞɪÕ"ê\~›7*——Í^ÙêY±¨Î Û3yM(yŽèˆI³`pýÿ·3wså+O6·œ|úÚ3j$Fa#Ü'_7x¨@­µvÇ°Å;a¡ÌhØ +¦Üg=¥…«ÑÌ ±z VFÍÌ”ÃÐNe˜Ø Ó»¬îši…šèäŒÖV#€ŸÓW"¢ÓÒ›]åöüö&Õ½eÒ1iáx!ÇA)âÁŽƒ+‚@E+Zo0@Ò=9l’á8©g»û6^¾e\s4‘qP—ÍtŽ¥z§`” ½ý 'ó•qã¡ ›ˆÁHÀ_Øp ÚÄF@”n›° kÝ&Üp3Cµ› ¨(––›j( ƒÇHL´â)Ì×€¨ip€ð+*Û­ã–Óð.p7ÀZ“×£A-ê1‚*z¢¶²¶@!¡Ó .?(úÍ yó°Xâd7åv×FFkåxŒ–ªPvoÄŒHàÑfZ12å-‚Ñâ’ŠV»Àû ÌhÀ ˜ +p8ʇ¼¾4|3[©÷Žëqxåq#3nfÓʃSã6j\붹ãìÈ”ÜSñS{ÐèûR*\™²[]¬Ó+êíÌá)׸‰±S  ƒ‹U*Úõ–¢¢9ÊM……ã`þôŠÍ€+w:túŠÙúƉ»žM4Ö‡t˜‹ÖU¡ši'W'£³x¨ËåV lÁàŽþ:ú0†÷Jv¢åΔ‹?¢qƒ +9X`]AçñY蘙Ž[H9•Ÿ?}ùi­ÇwÛ„ÕŒøxËJÈ* 3¤Ão)3²r‹Yð£' w &44 +|«øŸ 5Žß¯Å‚‡µ¨ C°5y"&4 +ö7ex¹Ù[»3ÙܾeÜzHeÓ¹06h[=IÐèA}ñ(ppÔ¤]„¤6ªuC%]!VØpÃŧFÌ8&èä!5¢qúQ:å¡S„¯O°zþhÜó€Ó˜eÊL~ˆlŸ÷%{ÆÐæã¯Êè4Ñ*9Ò"8áK“þk¥ÀìRl´á²£6aÌ̃rBœ·Ó·R6*îæ3°ÎX¸i ã66­Aü£PêDœŠv‚åu_yO̘Ø̤äNR¸•³Ò66 ncònËHå˜Ø#÷®&íŒÞÐ:yX·)›_í€ô'ATÙèÃÇ­“æ!ÃãŒ|Ùy_²%eº* æ‚òÐ5öÃj븉€¥ ¤â«:ù¢ B‡ø£²†›§PÝ<$£’˜÷—7Fm +qACÁËŽ¨#×ˆÖ á D´éŠ·¤sò“&ÏФ lôµÚÒ"'[fbKf*5¢÷NYi°*Ø ); [ҸþÊgWA`Ó žÐ„‰š_ömœÆÂZÑ°ëLX`/üxY·8©Ä:Ô¶Ú´’ è& ¾mÌ8¦AM΀ΡÜyÐQ‡Ž@c9-4©1ØY/’±Ñ3¬Aá˜dWN|ÄA÷LÐPnþЄuDëÖ*ÁYBŠ„0¡â}—/w«Ѹƒ¨¿Œú + ÿSδ³ÆԹàóf7ï$ “¿Ñ›DÄ:j‡ËÛTznÄ +h* þ*díÃj÷­SŽCZdÒÎCþ… =iÍÞ$4à0p‚Ú®21@kf<Æ%gÒí]¹¶qÕ %„€C¬äèB%]Þ(‚ǼBnÁÌh‘Ú¡œ†úñú˸XD„Üÿ95n¢¬dÒˆ…¡ò!F™‚pC\¥‚£¤Š/=1zÐL‹à7¢§T@›vöB‰fFjT‡ß2bþʈiL‹éœ’fÇd .Qce­G9sótOÓlèQ¬bÀÿ?QƒÕ‚ÎÆ k0•‘Ra—‰Û&lÐ}B6cˆºÐ5L¨†¥FD…Fè” *ËîV$ ÀèN7 B:yóè´ ~ŽÙùa°~»à®púF 8$}6Úòeæ˜xßà«„qˆŸj;¥CØ pU/_b;›wð#FÊŸšwqÐ×,4»J92€a$`ÅãgTLƒHmlÐÝL$Àv!Hªìü¤Ýÿ0 ÃFï°Î3ôhõ!| Êj`§,@&4‚ ÚÙ,ëᦕȺø2¨Ü­*ë°ÖmB$7„SP(BWxX둉'Í„‰@xG¸,,‹O¹¹’‹L+‘ÄÎA‡T€AŒë‰1=¡2qÐÆCé~²¹u+À­R–1­SÖzÁ§ÆŒü¸Yк¡býClDMyF4¨Ê@™òžDTZg÷¡|*Û \MGRÙFµÀHÆ°Ôü¸‰<¢A&L„JçP?4fŠƒ,µªµ‰RrÞN†þihâ–#ê)žÓc:ÚYef® £Ý2¤Q» L('ÐQ mG|@5ñÛÔؘE¹¶Ju3; ©=SÊj@Ü‹+'¤,<¨ŸêmÜmcVåÒ2(o§_¹¾Ër yO ì`3€ &oÔJg‘@ñ— гþ"mbáºÊÊÃ,”ÄF¥à¡œ²ðJïÃoyàuüSnQ $Eà±@ÕÁ(§/=\†”ŠD¨ì T­LÎÅå\lÆë/™ñ ¤E.Þsóyˆi:=fãFmÀÌ‘\û²·Ï„G,´Ò5Aí +Ûé ê/¶Ø[™ì„#pXO é) y¼•õ¶ÛÓÄ_1Ó‰¯LÚMÚ ›˜½1“µ393™±R9бI+?üç×ÆëÇ°@ÇÁ–@— q&! Y(½[ àÃzRÍÊ Ö4°´&’¡­¦,4Ä1½'†úê„¿±T]vEú@`!ŒCa0°&E«ÞPýV"?!hÚ¨Õ9ƒzwL뎙HåRµSÒZAÁ¨C®C*'ÙÍW &!U!Qè AÙŒÖJŽë0½3 <¤F‡õŒ«´ž¸mâ–!툱(¦‚ÝwиÎ0*-ÉM©¸*­€&k½2ì*Õ`²“8lTBíô+Dgã ^ØÔÆBÄœl+Bj4‘@ +•KN#þ¼{¤*¬BãñiX¨`yUÌ/°éžMÌØ=&{|ðüïÁW#…gò©™Ìô1©²‚ÅÚ¾ü¢X€é,±n°²"æúP]1.ÏÆšëbn`ôÊ6:­æ2–©pÍJF'lPxˆ PöD¤ƒ…š ðît¼«|<„KA} ÙÆdh¹›líÆ[»®@¤ººÊÊéÑNÅh¹n®Ëíü܉Îò´ +"•|qÊTôª||:ÝÜ U7ˆØ4˜ŽÆ œé³ba—:rù@ +Jbv6TYs³t´ªCüàh° joR.QË3‰ŸÁ ”³Ûq; ñ-KÈMo¬K&ú|v.Ú؈w¶©X•’Ëb¾Ïçf¸ÜŒX‚}lû‹ó$ÔR¸æ –-tÂ%æ<’‚dBv.R^¯-ž¯-Ÿ •—˜d 7Ð`…M´¹d7XÝJÛ±Îi:9c¥¢˜M•£c-°c.·è·\Áf°¾Ë&¦©(øTfÍfÉäõñèt07[[:G%{*·6\Ï,àÑŽ²ÚRU¹hÙ_m4à!‹sÐ&VçS­@q.ÒØ ‹žpÝÎC§´µ„©”“S,ÛÎ怯 XK +F’É›b¥H} °mì„ê¡ê*<`¹P©dã³n©D%ºTb:Rß‚ê‚Í"c-µËg%€-•K&@®¹ä,(m°¼’ìkoÝ-·wñXÓ_‚1ÏÅÛ»Ù…3¹Å3‘æ›ìú3ÝʱêÒ©toÏÁgÑ@—ÛX¤N&ºl¢ë•JraÁF%-d‚Œ¶ˆhÛnù K™þA¬³ëË ÒM1ÛÓaàªI&Þ •W"­H{×-UDœOL[ظʂ;ظ/3溑úfiùBkïz°±­®ô·ïd==–¢óLjžMÂ&näæn÷V­LfÌD‚f:I™ŠT¥òŸ›÷—VssgbÝ£PÛÜL¾½á,W>@±ÑYo°ÍçV‚ýdï4èË•U§ƒ@çr ¡ên¦¦¿ÿÀêé'{û;—|ùhG›‘Ö¦P˜“›ëÕå‹íÝë¾â‚SÌ9ø”ÇŸåÓ=.=HMËÎ÷föïÉÎw†JNžKvü¹™hk›/oøë;dvŽJõáŸ*<ådàwAX3§ã³gcƒÓñ™ÓRy­ºpZ*˜D=X[ãs‹\vÁ—_Š56òó§¢Í-(:-°² h"Jnƒûò ±öÑTï$rȱñº… +"b’’ë¡êZbz?ÞÝÏõOÔÏ!b^; ·L¢/æ– <’ƒtÿt¨ºã Ô&>Ð@py~!T_çssLªÇe 3D¼í”ð84à4› ×·`…ss§ÉHÃN†Ýl܆`ÜBYÌ,”ÏffR½c¹…s±ÖÖÜÞ] à.QiÃDïhsû®þÑ»Çîî¬Ý.fºL¬)76@ðO9]ž”—.å'ãÝÚòh|Dƒ%PQ_n>ÙÙ//ß^Z>›,Ÿ¾¯4ÙÆ%±`ª.PÝHÏŠ¶÷‚¥•Üìi3—´xE)ÓÁ¥Â¤Ä¹dg·µwOk÷êÒÉûîzú;\r†I.TÖ®E›ûnÝ#µèÔ¬'Ô²²9 tùrzO¬ÐŸ›–J áúv¬½ÂkòÉúÚ£/½ã+,©ì¬/Ù­.œsk|~=Ú=é/o‚³Ïï^sˆEËO)dµ”š>‘îŸ).^Èô­¸oóÒSD¬©¬Õ×îho_nmÝQZ<ÝܾÚ=v£¹tþÚߎwvÜb6T¤ºGa"…ù3ÕÕ‹ÍÝkŹã½ÛçNÝG„ËÕ…ƒîÞÕxï8›_,._è<”œ¿(¤ÛáÊ ¨ÙØ8ëpùE©¾¨­§úÇÒƒba¦0·íl²©i!;ð„ʾÜL 0çÏÏ×V®ÄšûN_ +–Cå"Z¡äè0È&L9TÛÊöOç§ü¹Ž”ë2ñ–X˜—[[‘úFº»Wš=º|òÞÞöePK2Rƒ+Îj¬^(ÌžL÷ŽË­}:Þo-ÜÞZ¿ÃBE¡D¡$²ƒS©é£ÑÖNcýòôæÕWž)Í4³>·à+®BÙC/pùuå 7w‰7FÄ"Hœ\[òƒp}U,Ì…Ê‹Ùé}&1 .]­.ez{±Æ:o bÜ°¸pŽO÷£µÕXs“ˆ6£µµäôn ²ìµ„ü\n°o®‘‘r¼»íËN˵Åòâ)X|©4—íïž¿ÿE¹¾ëÐX9ßÙ»^X¾˜>®®’ò4ðm®½¿|âáDcÕJú…D=߃¹œom],¯]jíÞaMÌ.C†·"ÄÒfuýîòòåHëX¤{ilIÅùts=?½aôJzoÂF“ÈY +UÖÈx—‰”‹½ÝheÙJg˜Ì¼ÔØõ•ç –—Ô¨_ë\|ÒÍÇYØ‹ÜÈ&Œ-3{658vÈÎÃâŠÓÉÎj¬µ®­€ªtæö{ñmX  ægšë÷À£±~ +Oª¯•¦·o<ôµ»ŸÿâÏÕWÏuöî­¬ßýÛؼÚܺæË-œ{èʯ‰Å¾ÊÉS2tÄjrúh}ýâü‰û ó§ºË§yöTj 0s,ÙÝMõfÞ»wç3KçžUvÚ³Çzkç‰HŽ+^ìË-ÊÍÝòò¥™,¦°pnqçboõ.îJ‹\ºÏgfäÚêìñ‡§¿©mH@ ñ¶'P cMRnÁŠÙø’•+F*[Å…3NP0#d`WA¯¸ÌŒí—Ç/Ýÿbcù4" K—3³çâýÆÖè&¿ dˆùKá +tºƒ‰… ³rcmzçòàØõêê<ÞäZ¥wüܽ_'B…hy¶²pº¼xžNXñúf´±å/Ì‹ ü¬ó;a/RÓ±æEwóνËOWÎ,m_\Ü»ƒŠVàדÝm¨ùÆú¥­ ^xðÅdc}aãì} ÊXÂéËCXºŽwÎäï*,^ +dK۷ɵÜ|ŒIN3Ù%_q=\Ýjï< U6lLÒ—„[;Pÿl¼Øã¯lÀºÅëgî}néøUýDÄr¢wRîã³ Àêl´³{ú˼èOöÈp]îì„Û{r÷xmã*àÄœ7Ûp‰Ot…ÅlïXiåRrp»TÛñðùÆì)!Y7âÎÅšùþ±òìÑÒ`»¹|hÒÅpñj¦{™>oí‹q™n®»µ{îáÙã×\2=½ nl—/Æz¾â²î={ÿ©»ŸâÓ­qÇ&f¥%!7ÕWJKg홵³/ë§ÕÅSœ\l_^;ÿdëèþÑ{WÎ>ÚÚ¹ÁÆûgî;yçÓànÀð¦ééc‰ön¦w´²z17†IvwNß;Ø:‹‰övjæ X]U×s3'•×® +Ë]åʱòˆ À#ä®'ÜA‚âfÍÍxy¦ÐÙp ).Ù!á¦zt¢.¯–—nò¼,,_£Ä¤<—î"ᆠ+•ÕD—§åú®‘ˆØ¨h¤8çKOWO4ÖÎ…››žp ñ•sÍ3×_àSDs¥µyGaþ¬X^eâM6ÑrJH°šhí ‹v&ÊÄ*B¦'B×dgýÙ~¢±Á'»½¥“ýµ³d¤Zmn\¬¯^¨­ÜÞ\9]ìÓÝë>÷æ;̽ª÷Fܾ<ªGj[rûD´uL,,{„ܹ»{êÅï¤kóX ˜›=›ž=—›¿PY»»¶y/›†–»k“ímÛ¯C‚“}PòêÒí}¼[Ê÷³M“7ì VjÐP ¸"ÝÙ[9~c~÷ +®{[‰Ö"œBµýù¥Dûh²µggâ¡d'œŸs iØðP.·L%gñ`5˜™±’A‹Â¥t´:_[:5wpo¼·ç–'œÂ¤ƒur24‚/Õ‘k[±îA~þd{ëò™‡ÄÒ",f•¨[ï[8õhw÷zºîîfÄÜŒ™Ž¼!>3pù2n_:T]2Ï­¯Þ^Y:Á¦Û.>"å:r}%ÙÛÏÏŸMNÑ™9DÈFr=È©¥ì€ œdÔŸìæOfþ8È ð;o¨©,¹B†àÖ¦âm§Ò{CvRÞ=uŸ/?7¤ÇœLŠŒÔsPàœ@Õ!äÁâÃ…~~zÛà d:[ýýû +‹wJÛåËÙ…³L¦Ÿëî½ú*«o~²CùЇÜóWvӋׂ•M âG|ét{2ìTyù|}ýòÜÞ}³;×3Ýãrc›‹T6OÝ( ötXpTò”[‹kÉÞI±¸è•2ëÇ.§kKˆ£dXÕuq•M ü…Õd÷ œ›í®]ˆC¨¼ i˜T°1q!ÖÊu6‹½4w²`@i…¾ƒì@knEÊ˱ÒR$3kU›;™êg3³U‘@ÍL& Ñ0±^wó*—î™IÙnpé™ÒüÙâüix”çÎ@ÊNT—^|ã0¤Øôq±¼Àß;ñPwûr¼¾áñåO^~¬»rR¯Ò”?¿Ž‡ ~Æõ¸dÄCÈ xÃx¤ +Bmí&{ÇédOç Ú™$*‡²)eÂx¤á•»)P 2Ú4“èjœœ“ +úÓõ@±(o ±é¹¾.ågy¹Rl®öî +×Wðh Ö™xÇ—™ãRóòT#lgª¹‘Ï ²s'½‰Ž•Oú’m¹ºd¥CñRíÌ#KgË NsÉžƒM ”¡P<¡qƒ×àd‰@^ÊÏø‹³|~ U–ìb ækËça7½¡r´ºZœ?W^:›™Þ’M,Ö¡þ)g@lÊÎ@‰–çNegNåïð×6Äâ¼… ò‰V¦³ïÏ-»5Ø#²‰/YœÙ+ÌÈ°…Œs‹Pê&î•›ˆÜ…„î V¸DGHM›ñp¶{¬»}]®o®TçN„Ê«&R–bíS÷†s5"HÕõøôÉÒòåæÚ5a ·œbž—ë{§nÄ‹³*žlnv×ï(Îœë[ ‰ÀùÑÂ|cþ tD(7;Ø¿¯²|9\Zó%g|énÀL@eá‚ÚA£¾L¨´ÈÏWçOw¶®á¦;”¿úÀ DÇBȘTfb-&Ü»YY¼Ðݸbò’¥ÙîÖx´a&VÜïæâVåT`ÔE§ÜT‚f6.ƒC·à’™’•«ž@±Ã:>3aå¾2n·‘ !57jð¨,„ y{¡*+•–¶ï +5– ´dçã>m¡BzN ô¥ÔfåÒ ÔP{¡ê†˜…€Ytò#D…4.xÅD¬2'dg˜t_ª®zH¹œŸYÝ¿kzóH¹}‰Xu¹±x¶¹z)ÑÞr trÚ)d`=Sõ5Ø,"ÒH´`GêË?0wô-ækƒS÷$š«<¢œ:¤S¨T —V£Õ]€‹x%Ñ»X2˜‡ ÒÞ¾Z]8ñjéäW[«—c¥ù…­;k‹·Ñ:D-È€F¨/] Âu³7èfÁLß„ùmDº>_ªôeI©æÀeLÌ–¹•ÃSu!3_š;ÛݼÖÚ¸šêî¼Á);eög#Ãà€gbõXqNˆÖ|‰¦ÊL£þ¢¿8©,ÒÑê+ b‘”m¨Ôîmí_xÔB†ŽLZôvÒCËb¼#¥gdLceè@þÕï¾×_:˜4¡\’òð +ÁÒf¼¾¯ï(,ÂL„!yñy•‡…¥²11o°@ËÕ@²¹°u>Þ]6P>$ôzB®KÆaGŠ0H;&EssbFñ2§„57ãA›=q&£UX´•cWÒÝM>7F*NÆ-ĹX%ÙÙ` ¾h¼&ÓZã“ ™prÞp]¯ï„‹KfBb“ÝDçXsõ|yáD²µÊ¥š:P¬¯Ÿ½‡Š‡Œ^4ؤ“s¾Ì<'+‡x5ÞØ¥ä¢ ¡¹H>;³Òß¹«±v¡8{ÌŸŽäºGÏ^ß8{ŸÊÁº|*ÖÀ –(©fÇ£6ØD6Žðq˜ …ŠÐ©>ž˜¡SsñΉtï´KÌkܬ/Q|"‚yt+&¤2(þ¢Ý™Ôº´f‹ â¼/5ME@f<ÂÈ-èGH©:Äk¬Èí-y • +šlDDŒ7 ½c¨˜™´SV€Šš‰‚ËtöCùEå#0V +áÓ©ÆƸǃù@n&7s Ɔø‹F4`D| )L Ú³´x¡¾z©26ÞØƤº‰H@åx9ÂZ½~øòZ{çZyñB°¸ +ÅöÂko¿ûÄ‹¯ÝyßWw/ÝOFJfT4!¼×—ð%;¡ümÆK³R²+ô+ýÍÕãwô–V÷ÎÞypǽ·_{ðëo|÷7_þõ·øëøËŸ~þð³ßÈÖF Þq#ífs¡Ürª¶«sˆ iÇD6TÂý%;saÌ—ÄÅt²ÔÛ8vñä]î^¼qñ¾'xú¥íÓW›+çJ³Ç™pyLß:bg„xkzyeëÄÁé;.^¾öØSϾþæwßÿà³O~ó»_|øɇÿú?ÿöŸŸ~þåßýÑã/¼ºrüÎÜÌQ+•târ4Ù–"ùd¦ÔìÎm\8qþÚ¹+7zâ¹×¿ó£×ð³çßøÞ/½ö­·úó>ù[?|ûßö×ÿøû+ßûù#/¼¹sá~€±lk­¾p4Û]ñ§*™Jk~uýÌÅ‹÷=ôðÓ/¼øÂ+¯çŸ~øëßÿ½^û÷Ÿ¿ýîG_þñ¯ÿ÷ÿóÿþŠüó/ðó_]{ükµÅ£¡|e¤h²XiÍ,mïÃcãØ© ×îìù¯¿ôÆ·žþÆ7ù›/¾ùÝ¿÷á¯>úä‹/¾øßÿ×øë/žøÚk'ï|0]_àCuÒW +gçÓí]'wyA¹Pï,l;sÇ=ÝxâùÿòÒ=O½øÊw~ü£÷>þÉ/?úã_þòÇ¿þ×/?ýâÓßþþßzgíø݇L¸!¥t¸,ÎsÓõþÚÂæÁΩ‹×üêƒOÿë“_ÿæ×ß|ûÇïòÞ'¿ýùG¿ùÕgŸÿáOþ¯ÿõßЭ?úÅg/¾öÃdcEëâ'-$(§Î%Xñ0*æ|ñF¦6XÝ?}åá'®>öÔ¿|ãÿê“w?þõ·~ôî«ßçÃ_þño÷ó>þýþô?ÿó?Ÿþæó^{ëä7Õ\Œ]^ƒ´ãÄ’bk¹·¸·{âŽË7¿ñøs¯¼õƒŸþê“üìý7ßùÙ¯ÿ§¿üç}ü›Ï?úôÓÿøÏÿüÙ‡ŸÞýè3õù½LgÇ—ê™”¿ÏFN Ê—dü©pªYì¬6ÍÙµÍS¯>ò俾òæ»|òÉç_~óßú‹~ý_ÿû¿¿üËß¾öÆwo<þÌÅ»‘ÒÓT¨I2áéþÊêæÁ‰S.Ýu×µ{¯?òØ£o}ç­ßýîw_þéO}úÙ{¿|ïå×_¹xåêÒÖ^ªÖ .:aÃ$‚—!ŠÄ+ÕæÎþÉ+÷<»u®2Ø¢e9ÛÉ6çRÕéD±9»´³¶½¿½·{õ®»¾ýíï|û{ßÿþßùðÓϾøóß`;ÞýàÓÏ~ý›¿þí?>ûâ‹·~ôãk<2»u<\˜æä2É ÁD©>=»²sììåýÓç÷Ÿ¾rýÞ×Þ|ë—|øÁ§¿}çýßúáßÿèƒß|þù'Ÿ}úÙgøñG¯¼ùÝóW*õ×Q6Ž²‰@²-/{}YÊ—ˆÆ‹Õzwkw÷áG{Æñ£Ÿ¿ôÍ·~úÞ¯¾üÓ_þò·¿ÿòã¿üò‹_|ðÁ˯¾ú⫯o¹"¦[Ÿ±âÍP¾Éwºóƒ¥ýSç{ú¹¯½üꋯ½ùoßüöOßûå_ÿþ¿~óåŸßýàãþø‡øòËO?ÿâ•·Þ~èñçªý;%ëœÞÆš¿ƒˆ€D”:kùú`ëèɇžzî¹W^áïüäý¿üó_¾üËüäý>üôÓ/ÿøÇÏÿÅ»¿|ÿãO>z÷½_<òô3ç®Ü—¨L3¡Œ!uvBŒWƒ©Z,ÓZ\?zý'ÞxëßòÞoÿägŸ}þù—úóÇ¿ùÝ/?ùÍŸÿòxw~ú㟼û³wßÿѧŸ?zé¾ùã×¹>n$,ž Õ-¸¼>£ Ç(©5X;~îòõ‡éï¼üæ[ßøæ·~òó_üíïÿõøÉ{¿úÅ{¿üà£^xåÕ³w\]Ù:ÊöòÝý`8[o *Íé|©:½1jsfyacgýòµ‹÷=rï×.½x{¾V#ŒáìëÄE•ÐYqƒÀ¿Í V{¦;3ØÜ;º±¿7¿2âìÑë7î¸ÿÁë®\½p÷}Ç.ÞÕ^Z•’.Z æç¹XÊpv‡>Œä9!¸üÎÕ½Óé|¡Q)î»pöúý÷<üÄCßÿþ÷Þûà×ïôñ'Ÿ}üÜË/¼t¡»´HdÅDƒèJœ”Ä{—Ž_¸·Ü[*¶zƒÁìúÚòéÓÏ?ÿäÛ?|ûÃ?ýì·¿yûGoóÍ×_}ååçžùêCܽ»Pï-KàhÞÃŬDh +¹×Ú)‹‹öùÂù\fgsëëw¿þꫯ¿õÖë¯ýç?çüý‹ÿö¯w\<½¿»Ñœîå7%èì^¯ƒ‡‹;é0Æ… VŒ&óÕfuuëâíg¾úø£Ï¿ðì·¿õú/?xÿó/>ÿÓŸÿøÞ/~øì³O^¸|®Ü¬¹pÞEF†uvöˆÊ<®E&ô¨‡ÉñZ,^š™_^\ß8zæöÛï¸|ç•k×î¾þ—_üþÛ?xõõ×¾þÒ‹/<ÿ/¯½úoÜ{üØñL±…òÆY½"ÂD :U™—âe.Ä’ùÍãw]è¹¾þø“ÿr×Õ{Ÿ|ò™ýøgßûÁ÷¼q×ÓßÿÔW={þÜæöV{z.UÔNJ™þÍË¡iðe+*m„ËÃeòõíí£W®^ûÆëo+=õì³_}ü«/½øâ;?yïù¯ýÛ•+÷¬mîëm’Aœ1c’ÞÆè-Â81ÎCúüñJ¢ÜoôWgVö0Q²â”ƒäÍoDXØG'œÌ‘1½Öˆº¼’‡Ž80£B8ò’À±R4] Æ ·y0;J|÷…h¹l$B“6Âèb6ád¢j'1¬³èm^„ØÖ…‹œ\ö&Å°(…1¯—e™t6]¨–{sóÛ'Ï-líF³97íÓÚñI¦uR&4`BüZ«w±l(HÕ 10AJ”#±T0ȱt.“Ìrõvgq=O&RévwºR.KbÐñBB™ðg¥ü,ÈEe1¸}~¹ÁK¥P8S¯4‹ùb2&ÇcòÚÆv§S+•2þt—¨Ñ'+Óý„˜÷áz€4—Ã,ˆ¬˜]•KÛ©Îy¶wVÛ|.7D’”Šõµ(®ýô†gqÖÑBWœQûY}í+×c‹®x±¶§ÓW–b‹AÚƒ›’RýLó UÙö…ØùÅ0Bè!Dv…˜pR‹©ÄlªÛ¬É¤RX¿¹‚ÞpGozÂsÞè¼7!mT*QFÝjlcbÉ)¯çZ;´Y'µzŒÉĹ|R©ÎZ†²ÙaýdÎse18 ¡‹1Z­Òz=)äËíÃÒàâº+qÓõ&8L­ +¹5ΑF=B¬”i p­:çIÜòÆh˜Ðìæ‘RÙæ³CÌt¹§d¤!çŠ <¡óLjœí>ˆ1Ù[¾dœÒ8Bè®(»Ä!êÂtŠµZœÕû¸¤‚†Rgl<¸¾0b~DM +E57Jª…›žx0iˆÎXÊ1¹¤²I¥éFôù£-w”þÉœïÊ­ÀŠ'µÈÄ“L 1Êò#²/¡þt>6ç%I#Lf‚xþŤJ„v\a.BY@ý³Nz~fqÖX8¯•VS(cÍy7‚³ÎŠ¿6¸¶¿á!l³ÌüV”pæ}¼–äõÅ(C´¼˜‘ŠJqK­ì0“²J~ˆ*e?ªyÌÜbì–E©t­{JÙùÐÕ,at|xÊ‹^DC•º˜ßà2“™B¸4@Jeí‘Õ8BÄ +„Ñæ܈¤×zëcdêÊ­ "q¹Êj55Õw}ÄÏnø}AbmûÊççƒLR,E¹¦ÔÄìšÓ>cÍþ²ŸYA#Ízk%I*¸TbfÓMÇbn6:×–0i0F3 +7ýèrTøØ´ËçFFe³¾þ ÝÞ%´m·I£)åWÅü4©·tÖ‹§ç|Ô¼€›­ñ_Nb\á QÚáÌï ¯»Ñy_2Dh © §¨Ñ\A•©9¥ÞÆñ:Ý›‡„Âõ8›Š³³ÕÞ„F¹£‹ç£½ûþ¤!ToRmâJƒ±ú\zÿÉÈ=Û…À»¾Y +’QjÆSLz¨×ö¥üʳõ-%?˜ó£3 F¥8›FØ k¶1¹&m‚MW»˜R^ +³ó^ ®oˆÌÇÙZy#HW–"a\Kò)O„öÅHê¸èª.!×f°Q Æ9ÎæƒDê†]‰ Q±*秙öi4ÜÛŒ„Hþ/"äq¹äŠÎÝ0z›*?¹æ¾:çƒ3•²ëŒÙiOTYòÑ@´R„ã8r'$ORu%Ä aÅùrŒ+ŘÜp÷Ie|åÒ¸R!•.”±Œ‹• nÃ_Í­DÃ(d¢°g vØ¢Ý:›Üþ¶8~èÇm*ÓF ªÁJL3%ÖîËÙ¡ží‰v‡·º(ŸóÆåå,!°ƒsËñEwòüë# +™ü¡Õi£¾åÜ1Þ—!,:¼åÂ"d*J¥þìÊò­•(BàvCâBX¹~¶‚^qá nû·_O^]QK ³|yÏè^8Ã;rm›¶ZƒÉíÛ/¾‹HùeD\F”SˆóÚK¥Ã•¤ 5!Œ·¼ (¡TI¥šœõx™µ¼fí6"ؘV@õjL.áv›Íö¥„M¡¸a¶NÙô !ä tÃ\šTJbª-8í¸à„;ˆI!7?ëß¹ºƒòb êÌ[-\šÝ¾MíS‹AÂG¦#B3Œ=àS=Þ œcàÒx7|Ë]Fn°wöšTË7=È¢‹ÓL†šÜ&õ"WÜ1ŽäS§Þ§jÓëË‘e?F•e3V‡1;¡¤É;=DÌ]wÇç|Ør„¿ŠHe17Ž²¡W'Ç/¹ÜpÖ”æfÔ3Îe“|Aɭʦ?¡.¹Ñ$mjùÊrâã ½ºŒF鼚[µJSƬó©¶×oP$_RE¥ +cu‹£{ýã7„ +Ù§¶ÖÎKÃ;Q*½â`Ì—C,Æ¥}èå8–TóÐjF…ÒJBwá6é s“‡­ÝçlªëAt\kØjÕçýˆà +ÎzBZ][NÞX‰®„oBŒ¦/.-Ø¥0¥ozôF0ëqÃ{kVLL)½Š µë»«Ë\oŒ»rË·"C³‹V€°©ô,ŠS[— +ý9(:•K ½®Ôw1»“KRf°sû³Ó7?DÄ\œÏÙ¡QÞ±Ê;éú>_ظdH!?ZÈYŸ-D–BL”œ­¯TóÓÞÞëÒènª<¼³H½¥•Ö)¹Ì(e9Ó “ú¬A…¸òÄD(ŨXŒÐ©„XÀ´i6Ctj%Æ/‡é[ ~‚ˤZ• +“Ê΋¨Z› ’̈V”±á+ü¸æÅt\®RdzÞOüå|äª ] q!ÜÆ•6gÅì„MAjô0 α€Ÿ âf‚ËRàu¾/!±Rîéëï9³ú“¹À-O2„› Ç`&Q>sË›ôÆEZ«BÖ$…,ŒÝØI÷OØâ*\JÒj#à„ó㛽åÁW‚\„È$… +ky{5€9Kae³Þ¤Z_ö+!úæ +2[t†Ù˜ÔTK›NóLàRˆT™õbÅSËAn¶6™tØT߬nÁaðzYJ—BÈr„tCÕe21:M赸Tð`™ê Z%À:J}‡ŸMÿÛkl!ä¹T—Ïp½!׫ý£§Ÿý08øÄ‹û´µú®TÝcrëRiW¯‚3ñãüfˆvbl†6Za,å‹©î˜uØ©nðVb{9ÌaB¤,“ÙqitáôNc|Ö‡Š5p!@s…LFÉ™QÊëZ}‹ÎøâšP^7šû\~ ÜÅppòÙ‡?tžÜp'I9:›7hÂu”ó«Rnu9Êùc,*¼¨6Û¡S½¢×·œÞIªs””³`Þ«ëwp«A¥šln¨T6r½ÓÆôÁpÿùÞïq­¢d-?JÕö˜YÇæ Ô™¸Ó*fûÀés>Ò¬î׶^Öw^­#Lo……’Ÿv«ïqÙAR¯áF¶À¾Å☲[Ryl6Öjk§fs+ÁšÕóÞÉÛÒæs2=¦ íŒ]¨Þ[½xòþÇLsk1ÌÒ©qBê¢b']?ÉöÚ 8ì¸1;ðÆxPœ•(øÜÝáÁë³—?'÷áð6Ÿw.—B4cTsÓÂúÃâÆãîáÛµ;¿Ì /¡paR–M·â23‘˨Út£&¸hWŒ³7~†°ƒD:0kÑYˆIU*»jî*­Ó¨\‹²¶˜í2©®Õ ­Î¤Úb~œíŸÊ…IŒ2¡¶yp7`ðÃ@j„5 A’Pê@ÒŸà! *¯V¶ú>nv1½-ä7J«3Ý Ùë+HŒ+B<ƒ°V7ÁƒU.Z‹O÷ AÅ€X +̺~é n¨\¥ °Ö\QþÏ®./û°OØÖá|¨FéMB©QZ Šž(h´â +Ó`Áëâ³#ï:Ýãt÷x¸ýäÙçT‹¨çFwÍæŸ[m­?în=Í/V0}Þãr *”PÞì@"ÌÂ)Ȉv=×ÝGÄ<œ/(‘TœhÕ-»w"•7e§;ܸ÷ø‹?ùMµÓÝÓâøN¦wR˜ÜÍïèõ½0m ©–^š,ô…0ÀŒÕ³›Ç“‹/ê;Ïg-p£,*唽œæÃt”Ë2™Iºoõöfe]¯L3½ƒ¸”¹Äâ¼£–×Æ·?yüåÏßüÐÚ{F¥ZÙRoÿÞ;¡0š‹pàúìö~º}H;kN礻}Opš~ÌÌîk•}LiòépoÖ­N-õ«“‹å{e1êATðBnHÙíÙ@€d;à{ý´¦•'­½'ý“—õíŵ»Fó€LµH%ÿë¿ù/÷^ÿ"ÊY AR«äןgW ¹õåˆ^±3>Ù¹øü§ ‰ žbsÓÔø~åàuýð3­{ÛÏå•L÷û?ü§Ý‹7‹`¨pS­l•ÖŸî\þ²¾ùÒ—4n€œ«yˆ>|!¹“&iͦK&âÆ;˜\h•Ëùµ‡Õ£»ß¬>ø+Ð ˆŸ«Kh{ú@ÉOÝqyÖâð³(³ßØy ´@ð«¬^ †'.óàNT˜-ƒ—&S×Àÿxq”v¢„…VÈÕ8sGA+uX8L!¬¹fÜI .z\Ì’fìïÇ·ág ÂÂêCgtÏh×ן@M›µÃ ³mòv=L™>Ló¢¦+¦-‡¥‘ÿž +&§Õ´B?LëARõ`jR«†çƒO:'ŸQj¹3½(oGD7êfk_©n“v??8¯­?Öª›àRh³nš•-Pp¥ºA¤‡a¶@CQÒŠW] °(z™Öaª}jwÏ´ú*i³ž±Ùnˆ¶9§/ƵÉéÑã/.^ýÕäôܬ¶F‡ÏÞÿ6ÝØ—•Ü=|ú›§ßþkçü®¸“Š‹Ž”«ÙöñÊlÞeHoRü—‹¾¤vÝtÇÄ8›wf³q©Ìå'PÁ2¨!£úäèèÑ)7D¤\„ѽ–ÐZ…ÁñÁÓïÓÍíå8fÒÕÍ £|nâžuB–"¤™«m´Öî,ÆÙ,a¶p³‰[±´-V¶R½ƒÎþ³»Ÿÿ¡¹óäºYQÊ[|n UkÀP·|ìÊ,—Û¬Qû‹[WBæ [Fó\,lI…ÍåpœˆÉ…iDÙôöý÷Þÿ~óÁÝÃ7JmßOgæü³V0—Ÿÿ]¦}V3Ææ•Â4Æ äœö‘ŸH]YŒ“R¡µÅÕ ¿¸²<ïg0{¨u.¬îEªs†òÕéHÎ8F:×çCóò–‡ºå£}ˆþʇ¨•Í ðÞnDvͶ‡HÑVÜ[\÷“jsù¢U¥Æå¦Tf´z(—¥¥<.dÀ/\Tºðšµ«š5SÀôV»ÇNsóÊrhÎù0“Iõ‹“{Õ­'Ùá±î4¼ø~|ôÜO©Öþäìçýã÷¥µÇ½ý׃7\~õVVòJ¯E)cæ¥ùªuùÌ47¸Ã¤;ס)„Ïm»›€âi¶ruf«#q³[/¯“Z…5Êõéysçq}û2?:^=~Õß{&e‡Ziµ:}P›ÞknÞ+›®¸‚B‚¨Va5×:šÐs^‚¶ºRqSÌM…Ü”±‡ AÖª˜X„ÅSd6ö[ûŸ€y€SÒ­ƒ;¯ÞÿúÅLßã¬Ãg§¥ÉƒýË_Üy÷ÇÆÞK]OR2kS¹²$m7¢\÷RKQ1Ææ@‰"LÖ?»9Ј0éùå'l&=¶ÛÇ»—_½üþŸÏßÿ­Ñ½½”Ð(³ +A*íBä8WHòUΰö¢èêJ>V­îãZǃêA³­Ž„œ˜ÅpSfoçA{ãNª±­Õ÷‰ô$it¡DH¥ÍÆî§Q¡ºS•ò쎢ëÿﮀÛóAPv±wƵŸ]_þɵŒ€ÙË;Tzas7}¼È R}ÞK^½é÷ÄbøP0*Îå}³û¹Lÿ„àsùÆf‚Ï€Á)XÝs¹vÄ6­Þ9a6èÌ.‚wEµäÄ$¼ãKÞ¸DuÀ–¢ +øÒŸ-Å®.ÅXµÜxÖq}%¾ —¯¸aµøÜPÊ÷­ò°>¹§õÊààÅàäuyý®Tbj!&æÂ̬;™ìôP6íñ@îBnÇé=*¯=ï|›5†òRV MWBôaj„+ò¹©\Ú$í Dký¢19Kòi„Ò Ý½µó÷ÏyüâûÇ_þ}eú·{—¿zýÕtªaÓ±D¤†vë´±ñ|tø6!Ô~¶Œ\‡"Väò¦T®ÌCv¯Y0ÛÀbÝcb~"ƒ Í ’r°‘3ùÞA÷ô f´˜Uê_Ôמe[Ç£ýçãÙඑÕtëÕ·+•Öç|LÌÐ3ÜÄõäX}„MC:€‘pÅy°ŽVc?Ý9jï\ž¾ü~zþŽHõÕüêù“/Q>ÀM9¿–nîCuÚ§´=™²+†r!Âœ™y7$RN÷´²ù¼ ¡×㌪®z‚+.@hí>ýÍúÃï;'ŸW¶Ÿ£zëʊ铧¿Jê•knd1*úI¸ +ep‘¹þýÜàá wòúBÀ¥“³û-Èr\ZI(á¨Ö‘K{àNá]AÓ ±´¡6Ž”ÖmD­¡|®Ô9 ÔB„Ô`£É§ºàú€B¨6çŽ_YÜXŽ\]ŠÌû)ÈúÙî %”´0°…LÚc³!æGVkÇîì×î+•MÒlY`J}T²’à­Ãš5ø59;2KÛaÒ )fãlÚRB´VÃåäB”ÊÐjƒP«·¼$;`O॰àCMÚìdZûrn`¬$k©ù–T€LÙ¨í<èŠ3ëZ¦H1çûÉJ|>,ù’iLj•Í„”p¶/)_™Âë–'ŒºÝ=èc¡Õ¹m4 £åÌVFOüœ!–FE°»[O¶ï}}þÉï¼ûÓÞ£ïH£JëUL©Dé äÑBº±[ð ~DY +s˜RK¼–#B‘@•â¬Á:ÕÆôöígßô/ƒœÉ‚8vOK“ËTçd6϶´ß>kh V8BÊÂAòô6xÔÊè~czÉg×–#R‚J‘Z“6{˜•6µŒ˜ðr%4°—0ŒP<} a1ˆ1Ù(§§/·.>-O¦ÇO÷½óR+Õu¥8Y?}ÕÚ|òaR•Ò•ÍãgÙÆ:ÐP”´‚D:LfâLTª1Š$µ/º8kLJÀpÍ{_’Ñë”ôÆnª¾ÅšÕíÛŸ5ÖîÆ¥,ןÔw^ÁI)ùñÚÑËÊà˜—+v}'UÝá­!ÆÝò yÍë +Þ8}uÑ˃ÏîÛ“øLWLðÅåÙÝ¡˜pÓ‹Dè¡TÁ'û§jn±´à#q>/§Z®5·¡JQÀx$ÙÌ‚_ò%=aøÇ¥AGæüØ ^/qjŽ7ˤZ6ËÓGŸývïÞ°m¨Pæ¬hz¶sJÊ%„ÖH1íIˆ zI±†‡Õ;ŒÖÖ‹›¨Xr…hNÏ[¹8œ?»áp™ó'Ôy/¾è'–´oV*Õ9W|9D‚Q‰: $nÔJ ‘Š¤Ù†ÔË Ïä\kÿÁ»ÉÉ'•ÑYgóÁèä“Îþs»}Hµ\kKÌ´}ˆ§LO\íX àIÆa´š’€«ï„áL½h()j%Ó?Ú}ôÕéë¿nm>|üæ—?üA­n rSêI¹Ê¦Fu¿´ö¢yð^­ïÒZ¥>¹›+¥5À–è¬Û­$³JváK?]Œül!rÃO¡æ€‚wÔ–w¶#ªJ©µl÷()ç"¤æÔ×¼ýâ×òìËôàøãcÐöÖÅçkçoùl;ÊYji½³ù°<8Ö £\sÊEO„L²Y\©1m%ʃ]ôÄÔfG)å‹pݯ-„—¼˜/ÊñVKÉM ½‹JU3ßß8~’ªN05k46§ŸnÜÿ°°þ”˯ùP½PÛ:ñ}LL/„?" ƘlœÎºCB·%£1Ùº¿uûÓ« á+s>Œ¤PTÊ[lz—D­ÜÝß¼ýx9 +EL­&å".ÚV³=\).pJ˜yTOŒõD¹ ¢.û (baL‹âúÜr0‚Ë”Zðň´bw{íPå¹Õ>ªLîB¥¢ |œ¼ž9XDó‚„ODŒP&=$”ò-OR JèADŽÌžQfQÚö†Ée?>ïF !C)…ŸÞðüäúÊ_^[¹6ï_ðã!L—Ì:Rm°Ÿ*¯zB¤hV„Nš .7Öj;V社õ¢¶vwcóìßÿ—ÿsÿÁ{`F !§³o6v€RåÒNR¯p–ò‡¯"læærpÁòF÷lwŒ„’nsft?Áçh³­T¦ÕµÛí­ûåþÞóÏ~ñèݯ”Ò7éöíTó¸ºz¹vþuÿàÓîÎs¹°æ”§G÷߲鲔"\66ë'Ö”rSÎY ’iWBFÁig1¡/%ôk^r>Äg™µ­²Ëã;o;|¦­Ugí\*ëOGïöþââýŸ*ëê“ÓÓ§_hµ]¥0–2R«!³­Øk„Ò;¹¶&¸¡!µ&g7ðãLÖ cŽYŒÚX +Ò×\˜Ì[Q‚²Ù&|LZU\-ÂXE¸ŸOŽ^÷·UºûåÞ6*g³däþÙ(FKyZ(i\¾Þ;j¯Þ¹¶ %¥¥{gþ¤@Ø=Üê b©·qo¸õê$@–Vè)¹>oÏöð’r}L­b|¾ÒÞªöö]ab%H„’Z’u@(CIuÅOºC¤™ëd*cŒ¥¥lgëbÿòÃÞãoµÆaRïD¹âæñË¿ÿÿ[¡ºîŽðàîH¹Â[>5aìa`¶©#Ú­Îγ4ö„)§á‹°1LK†jU1!Á-Z©-øˆ?‡èZJ¸£b‚°‚q%N¤0¡*¯½øð[O‚G¤iÖ¹üXkî:ýÓ\wwcïÞ¿þ§ÖúÀáúÚýæöS«¹§–·I{èYÑr:Ÿ}ýOL¦÷7¯-xý”;*@ag”Bo°söô !Û…ò…Û-*ÕЫ§³Ýغ(NŽ’ZÑjlÆwùÌH-o‘fSÎ 9»g׫£Þ)Ûõ!`ŽO…éT”s\¨¶“µÁ;ã—OHåŸrÅ…º5ÄæÄü¡7Y½¶{úº³~!åzFc§´ö°µûbzöþäÉ·½£çfs{¼÷è×ÿø_ÅâZˆÍPv‡MuÁX*Åí»r¯Ý€Á«MîÎöLùØ8ð±#¨7¡ãR6º !v)L>BŒÍž>϶É‚Á®m<³}Þ¨ Öïùµj{zÑ۾ϦZ~BGÄL;fy˜²I¹Æê³QÞšuqçfäqPF|1Îeán¸‚,@ñqå¦E»)¦›fumxúfã·­;?ß»ÿåþÃ/úk‡8—Š‘6!ã¤JÎölJŠ•8Ô(¡`V ðE¹ž'DÅ’F’r|a!-(_Á»äùn–÷€z’¼=ç#¯/¡¸â©I±*eWY«mæ‡ ÖäA„4ø|»½38|¾ûðÃîﺧobRJnÿà% Z Óêˆ\!´­tú[~Rûé ·7ÄDð”?¡yã‚ÂðéáîýטQò3)³}(•VíÖNuç2½zLwy«vqùaëüu˜´âL†ÔZfuw¸ûlzôIª¾Ë/w6¾ûáêëþüº)Dúq-Ìä¬Êf}õ¡RÜ æÎÅ;µ´zÅ•X3!&Š@jÀDvc«ÒßÛ>y:>zb-Öiµ5­4ä2­òøäèñWgŸþ·Û™îQnté­“I€ú0Ù ƒH˶sá]w#\ÇÂúÇV‹A<}+Àßšíc®]óDÝ á3jeËêÞÎïÆ÷£l.W]ÿðý?œ<ÿZ. ÌÖ¶ÖØRJS¨ÛQÚášhw›ÓÇ”Þ^øèÛýq 0áÚ2‚p%pÔƒõ;ÏÞý†œÝK ¹ ›D„ +Ÿ]ÍöÏ×ï}­•'“݇—ŸýÐ\¿çOšAÈ8*;Û£ŠËãâìy_8)ðù“².&Íùh7¢‡Ø¼ZÞ-狼«ƒÍ»÷>?zùË×µóèK¥<õ“¥WKÃÛÍÍG˜”*WA‹.늈î°è‹É¾ŸÀõRsWuú?¹îw!!Dĸ`L &-w\#ÕúöÅ;PÿÿáZ`ÞGƒO²«Zy+Âä6÷¡R„1#¬‰©y>ßë>:_ß}Å6˜LßlîWg-‘Áï}œ tnŸBâGÅìblÖ¿×—èê +1WgÛWÅ–BT”±#\ÚK®¤,äºb©hÙT{§ò&?¾(o××T&çb¦þ96[ÀÒbÌ*ÁNÌn¤[µöøèþËo”üèêBå³ð—ê‚ú„¨L„- RÁinãzå†'™îí—ä‡è…Ú_n­í=îlEi37¼=9ÿùÞå÷;÷¾<íì^õtiüÝÿã³ïþÉK¦â\®ºöpõôCixY>RŠÒM—GœÕX‰I×Üôu½ÕD.©4Óo¾úá· .µ’|”‹¬Ó¯NÎëk—¥é³„ÞLpŽÓ;€Z— +³åöR9DÙa:ÝÜz6ÛÎ/&ĸlR©£|!BØ ° ÁéI>•mmDè´Õ9§W_47Ÿl^¼¾œ=¸´ÊO?ùæOÿú_›“㕘BéÚè0V?Î|I 'kÔ11·"½I…µ;0VTªW˜µåù:ß=Ü9¼üõÿ¼~ò|õü³õGßÖ?­í}²zç«í»ïwï¾+Žn—ûÿüoÿ÷Ûïþ.AÛrª#¦GaÌ¢$Ðñ‰šîCݸ¶Œñ©NWo¬Ä¯/†rÍ{c1 áÊB~wùÎètI«ÅåW™Ì˜ËO¥ÊŽSÙtÓiï@F¹^~|Ö:x9>7ºøÐ=}ŸY}(–7œêÖÅão÷|AX Òêäg³JθÂ>“ÞœmïdnºQÏìI· Ýq!D¥bâ¬óŒÓÙKu¶Ë£ýŸï<üpûå/ÖÎÞ&¤3ˆ‹Eêã=Ømù³ùfŒÝ¯Ouv^¤[‡„Ùô rt‡4Wb¬xSk0é‰R>L·NùL9!¹bRÊÔ¸â0 ”eÚ»¥Áü«†„œ-N÷kk©æ^¶œí‰ÙAº4ÈÍyˆ•«M!·¦7ƒ¸éŠr‚Ó-­Î4P*¸Z!­Z”µ”L·Ô?RîÞÓ£—¿ªl<ÌtØt;Áe¹8sòô›?üëêÑˤTQò›”Ö +$µ`‚›Íppc7—“ >N/m®DÙ?»æ¾éB—´/*EÈ itc#TZqúði@‚å‡[¿?~ýcûðMR*V'[w>Üûôwã½Ç>ûÍèôUº¿×9zYÞ}aöÏÚÞÖéÛ{Ÿü0<|Ô¦U¶‹ã‹lïT(íÆ„Êb€ºº¾áB’|áÒþ$ ŠyÊn±éŽÙÚIµ¶î¿»÷ö7V{#?>lî>žµ'êŸUÖžu÷Þæç6cg•!œ¤ÂªRÙá Ûrå€v&I©¦lW‚OÊ¥¤Þ aÉ T„A«yŸÅ" yŒŠgI­hÖ·ZÛÏGŸ€‹ƒøçìzgëXؤ’MuƧoÏ^ý°sù‹TïH. ɦµÜÚÑ 1Óˆ‹6îf†‚Ò*„/µ·Íâ(BZAT ³ÍYÂT&L9vm«48¹¶M +y>Ó3Òèx|úÚéŸÄùìÁÑãwßþA˶’B¶¹ñ¬¶þ¬Ð¿ÐJ»nD^ Qe_¾ø¶9>»5ÛdÖBåšTXwº·«O‹“”\xöæ÷_þ>L¦€D'ïî¾ýãñë?˜Ý³0AYû7ü—ݳ—nT»éƽ¨!w¬æ)©wæ¼x×ÛÓ;…ÑùB VœÍ ±éžYßÓ«ÛœY½üì—¯¾úwfi-J§¸Ù­ø­Bÿtëâ³ï~ìì¾ Ì¡ÖEgìC•ë+±¿¼á¹¶7…+ ÔéŸÍ‡ÃI%SÝòD¤E/ºc ³-GQ‹Rꓦ´²X—&wªÓKÂêËN»µñ°½ûdpøôüñ‡×¿øScãB«oÜ~ñëË/ÿqëÑ_^|öý˜Üþ\ÊOÊ­ýƒ_ÕµD +6ouµœëñ Êt +ƒ8£êGE¨ œÓÍÏKǧŸö_(åáÆÙËöþÓÌøÜ_¨õ=&;šÍj.ŽA¹ÔÆVPp£U@0öð~Ôì{©\BmÐÙ‘Ù:Hêu*Ó£²cÌì1©>a´ ­$Àl«¤\Ää"§sýÓÕ‹/'w¾lïR]˜@}ëÙ•ñöÃÏæ¶ÝÚßù"3¹/”ׅ܈uóäyœ5< !Ó9QŠë´ÞPóãñ#2.åŒÂœÒÏæcsn,ˆëŒY-ÎäüjˆHy£¢UÇÓ#($ùýaíôu®µ+g{AT@htj¶ŸôÄx\)•'åÞaº¶áŠ²×–¢®(£Ó`Û’B-)çp>Uín)™Ú-o\É Æ·ßl?þ¦ø<Õ>Ä´öìæ€V|ÿýï'{ÜI(°<©5"tÖ“öÔB˜šäÒÕÁÆf#lª"—(… e6éô @8¬ÕÐÆJŒoÆe>^¬tŠABøì>–ò=§>®÷rÝ¥¼C˜}\m‘r5N™ów‘†ˆ’&ü‰QgDE\*'hÞÄ¥¨¡– ¾åÜ\ÙB˜ +` +*¤ÃŸàuÑ©›•ÕîÎåä䵜$e8ÈÙšŽ3îÈ ª$xÕªZZ‹3NL“ü¨á‹Ëž(Æ” *OÎ'§Ÿ´¶/ÍƪÖÜqIMµíÒ*ÊçX³‘J¤ÜP³#R)p-9[z@¯„¸fÞXB®ßŠ^›¯Y„¶“ŒEkeðüWný1~%Dÿl!|Ý…,„YWLðFXÖ('')fRíýtç„S<Ø¡ñqkëAqõž^ÝRM»4Êõv+Gi–œkñéV¦{’ÝÖócÄ°”©Î¼XPp +@Á˜Xð'$ˆ±yOˆä )fy§mµöª«wÆO·O.¾ùzõè²±~wÿÉwëw¿>}ñ›Ýû?¯ŒN´|×,kƒýóg_¨•©ÝÜë¾Ú¸x žµ©XèæÓçï~}ÿ³_F¸¬^Ý•Švëxçò—[—¿,N&›çÿð¯ÿëå‡_éµiax v»wøøáç¿ýå?þ·?ü§ÿ뛿ý·;ϾùñOÿñݯþÑêXíµ¶—Ü¿?}õ‡éÝïH«Çµ ~}Û›>LGåB\*ɵýöÞ§fc7)å:ÓÞjʹ‰^ߊc0“œ3ln\=û“ªª¹–VÒΠ±ñ´³ÿ™Þ8Mˆ pþri‹Ï­’ŠdW>ÚÂÓ7gI-GER«—†ç¬Õ +Rn4GŸÕ6I¥5!?fv„žu¡‰qf1)͘6ÕâÒ-9ßg¬HöëüM±…‘K÷£”í‹ QP“T?J˜pé¥l/Ĥƒ”AèU¹²•>à2J¯sj‰ÓJ˜TP²}?¢.XB.©ùcuRÁF†IûÆ2 +Qšäs Ö¸ˆÓiW˜…"<›˜P1ÿ¹bnùˆ…0ï'R !?Û1G+ëåqi|çóÙR'Óœ2VŪ®j•M $½´žmí;í]ZÏ +VNη09«×´ú°!øZk¦J[éê¦ Ø?!%h+Bh¾„t˸¾ˆHPiÖh!¬"BÛÍæôî>ïã/*k'Œ]2˃ñÑ §µQèldšë´YÃĬœ¨Ù¡YšàJ1×Ý«LN…L›4[ ”.'e›f¡[èl™Õ‰× +ƒ³±­W6{O[Óól¹;o½ùðýøðžšköÜ{û«§_ÿîÕwzþíž|õ×Ç/¾X;º÷òý·ç/¾í>¹øä7ý“wÃÓF/ +ÃÛI¹.?ùÅߎ_Þô‹a†4jBnâ Ô·žSFU0Jï¿ýƒšép+ÌçŒænaroãî×{Oè~6æâÑÛÏ¿ÿÛÆÆ=T­òù fõp{ÔÞzqøü·ÕÍ' >óøÍ/ê«g+ °Ú1¾„kíÆô Xt%;¬ß]=ýµ¼zÎfz1¡À¤:B~XZ» Sí&)ÙøI֪õˆq)L­(ÅqïàÕ“ïþ¥½óÒ\/¯ƒ9ôDf#ˆ +% +ç09g¬&gTÒµ)œNeý™RÝÃõVœ¦îCq–²kAÌd0+k©æ›êB„xQy,ôBéMD(¸¢"˜Ã9/9淪ÃÜ2€Û +zm™­ãðS×=DÉÈ¥©ÞØUkrqUÌý˜­Úe\ɺãlBHÑV=Ý:È.œþ9—í v5S)Å~ˆPŒÒZyzY߯¬=¢ÍVÎ"ƹ‚¤étì|Ïçn¹cs®ØR€&å:.V¥ÚX=O*yΪåqqõL®LY§cR^}@hy_‚†j@[½L÷v¦gÖóŠË‚ˆhÅ i5¡ÌFé ¥UÕÂȆBát!ë}ˆ´¢H)JJÞ—àÒg«F6_l«¼YÔÒÕæh§»}ÒXÛɶ§r¡«Ö&No·ºz0:¾nݹûö‡Ñí70’ªÓ¦ä\‚Öâ—/õöï¼)UõÂ4SÛ5Ëëbvç3‘$-ëÖÑݧ(Ÿ¥¬Q?¬¬>ظ÷åVÏ>³êÛ¢^øôýwÿößÿßÝïq«í .rÓgóo}ñû^›š…ÆW¿ùÛý¼I}!ÈI¥Ìè~kçåíWÈtœÒøÓ/|øù+ˆêÁÍ$fª¿~þáâíÎ>ÿ[£uX½ûÍ¿”G§AL’ruírçò»G?ÿ»þÁ­¼}ÿÕ¯òÃk+ÈÍd%"&¸¥5 ÃÉñkÁ¬?~ùåïþéaŒnåÚTÞÕêG;}òæoÊ“Ëyö~ø­]›Â˜ß +YAÌ@ù›Ïù¸E?}zçÍãWß„0í'7}?¹áýó«þ+‹HÏs£ÒàýÚë¬Ýs#ÚjçÆ…ÙfF´VÈJr)=߉Rz”61¥¤”×Þi{óáúÅ[­¾•`­B{Ϩm™Ù2+¢.Gx?¢ÐrV2ËÑ$ï`´`ø£„'Æ&ä"¢VC³%~5D®ø1YÉtœöNmçñöå7«w?o¼±;gî#%Û‚•år@ú+Q)œ4½‘ëz#Îfê:RˆPƒTj%®„(;D@"²Õe`j¶s)/YMZ*†1#ÉØéÒ0Û˜ŠãŽâ¸”¡ÍŠ”ïv÷mß{;9~a”ÆNmÕ¬NÑ™-,H©:#PÚ¤…):Þ¸0ï'BaÄHe­mCô +-f Á &u\.³zåÒ¨àj^`·2¥awrTÆìifw÷YeíNª±&¤k‚UÔÓEÓ)‚ zQe)IJZU°[¤ZB¸TW"¨È«%»ºÁe'Þ¤™j8Ýc­¸Úß}¼zöº´zŽ N¥1:º÷&×ÜÆ„B¦µ›jnk¥ ï4¡”¡œ#-~Ö×+¿à§I•ªFy'Õ8”3#Z-*f¾ÖÝènœÁ/؃3«wVÙzQÝx’iïCå'”F)ÛÇO¶Î_ƒï+¨‚©–f·aE«å 3PÓTëÓ¿}÷íL»¶’\Œ(2—4{NïÜiééÚ¯þøÏŸ~ý;w¹¾÷Äd0ÿÙáÃÖÉç!ËÊY§2BÙ \)˜Õ5ð~íÍ{•Ñ1¦D-¿¾ÿ°2Øàz”Jñé©Õ(µÅowùãÑ!ÈZ$DP’ÖJ…µò¬ƒ÷:i¶)É™lœÜýÌnl +¹žÕØ1G ©L*$o z1çB 1Éf¾Àª5£¸\ÀBˆ%ÅjÉF­Õ?˜<1©8ï$x'ŒéÞ(?;fÉÉ·6=Q@xÑG.˜“&€/´2©æ!0€ 9»c4³<.Ïìæ¡ZÜh­?²kÛqÆnO*ýý ªF)+Æüxᛂ=„Cò…QÍ*Ùž+ NRóÏ(Ì&•#dœ°2s&-\©ê¥©UR USÉÙì)ð< É[IÖLr6!:àB¸¹ü8¬é$½aÚåà‹ha¶Ï#cwP©ìG„\{+ÕÚ +!eD§ÉèeÉ®›™† ç1†×Š¹ú$Šqa”FXàGN·x³§ãàKàxÃÄl*¸x'Ž)’ša¤lœ4£pÕ1ÓÇå"®ÌVDbÕÊjºŽ3¦/B-¸âIðêlúÆRìÖ +â â-p¢™É÷*í]ˆð™c÷Ó1¦„é-T.'¸ .¤ T»«F®uÝ…ÎÖ…•·[ÛO‡§ïz‡/C¨ŠÒÁ§”‚RJ‚æã’ÃÙ5³<2K#J-(fQ±Ko‡P… p¾qg»CJªñ$⬨èI±’Fˆ¶YSv=Æ9QÊЬòþñ£óÇïe§©ê4öê«íúQ’ÏÆ11£á¤b˜Š0V W℆ñ¡TøôQëaVõüüÅ‹/~åì.ÇY;Š 0½ ¼Lo„rð{¾(Æ´e0݇È!\cÔ2<.g’œYêíå{©Æ®YÝÒ +ëŒÑŒÓ¶™ë…!ÄÂeìêf¦u†i#­ ¢-{(&iNË_™^_Šº£&VIµð¥‘KÕ6i½F%Îæ¡ê†ÉTŒË1†Â£Ì%7Žs1`ç6ïEÁ$øpƦQF_ñ!zª•-¯#”Ç´8ia|)œÁZå¾;BBå `·,#ç0ƈ t(ŠSà}kÝ@(£IÆÀ„.壔鉲`tWd‘®Í‡–½X‚01!׋6I%åâ¸Úݼ#Ûõ0“R>Éå’\†sƒKžÄÂJØÀqÚZô$½ÉHÒHÒi+ß6³MÙ¨¢T`ð/oø®-Fo.Ç—fË`Iw˜âõ ` ÊhËä•…£×¥L_S[“2=w„$Øzå–;ÃŒ&dFN×z›ÙÆššm#ŒDhWñÇyB-È Œ‘«¬V§wnÉE_bÑYòD0’gyÍÈThÙ¶Š Ú*ú“"¯å;ÃR}?$`˜*#=×±éÅH‘¤,U5ÝBÃ!1:„p1PÀ¤â±i[™f½»Yꮓ¼®gªËT…?DòêBtÉ‹G±Ù©ÝX +¸J„ÉÓfWÎNôâTÍõb¸¤Y¥ÉÞCÁi €‹œ~ª²^êÅi”Nùâ)e(%;›Y‡TÀ.Æ1!•@‚^öF½ÁD’”cI!Œ +``\¥tŸ3ê !LáP²c¨9´94«'RfÏ€HˆÒ ®Há!€ë§A«…• qm1è $§ÛwNî¿Å¸´'D£L ‘Dµ Ô8®ø£Ì‚+†P-g`( >#¸„SŪ«E“¸×D’8Å*-¬^¥ÔÊì9u†N ĸ+×=?é ‹Kѹ[ÁùŨ;É«ÑBV6Êà[råÖíû¯I)ëR¡˜U%Œ*QT&ÙtÓV|I+?J•Öã¸qN•—¼ØO¯ûW‚l 7àx®ÎypÖdÕ‚?Êú"ôJ€¸µ‡àD “‹Š$F*ˆ©ÁMwüg·|yÓ}m1Ù¡¤ª”hZ©ÜÙ£Wz®åŠ2TN°6¡”)£”Ê.0Föâîë¯þø?ùíÏ®,ÞX D^Póª]²ò5QM?|µuö<ˆK ^Äç½QÎãpÞf[ÔÓé|£ÐÞå!f“ü,UÙF –áÙ(Å1‚RÑM°†]èœ8…x·òÅ(-zÑÿ¤÷þrä<ó{ÿˆ{ì]I$g¦g¦s#g +•s@ +U(äœh s˜ŽÓÝ“93r†IL¢D‰”DI$%jEÙ+­rܵ¤]]{eËk_Û÷œëës½äspx§ªÞ÷y¾ßϨz_|!óEpV)rú¢I8#âù…œr`|..X­q-€ƒD$à=a(¼QŠO–¬ÖN€Ô}ˆàp`ðWNÍKFeÖ?7ëŸóÑH“ÄÄ9o Ž$•)Z– çæôEç<è¹’éÚ­í¿|ÓÂÆùt©·«–@1@¦`ZA¦¢¤çmN«#” Ãnf;éBÏÆ@Òi6à‚Ë(.zC1‡7dd +‡×ïF1nrÊ7ùzÂyþ¢†`M°{(*wH ¡I_óúÂÓsî 3n·?œPÕbÞ.—rx~k ÆeÒæ½8œž†c’7ÈŸò>ufö©§g^š–ˤó‡’NèfÑHWD½ %ŒòPÀZÓ ‘§Ï-œ»àš™ ¸¼ÉÛaTšw„'gs.Ô`ÒŒ¶+ÈOLyÏ]p°J>Féç§½Ó Q‡Š3†¢—ã„îò1“3ˆ/ÈŽ»c:0íŠù# žÜ‚'ŠÓAœ@r©F¿xWÌ\¡ÞÕ3…¸ {#‘H4’Hš…ú¢–o?=é¹0ð†@­2ºn3p[-ç–··¹Ò|öļˆˆÒ JJ$Ó)»`Ûykccùþ /U{ëæ}fÝ8%°§iª ŠÁpDU%ÛN1áòÌûün‚ˆZ–\,™—7Nöz—–sÍœjÈ|""%áñn¿Î8t:/%”¤*$$ÅHã¬!h¨^Àž9G>"[n{CNÏXÅ.ÊéaŒ Çyw˜˜ñfÂéŠJQ&{q> +Cçô"‚bUZÃþÚ&/«  ³n$Œ«¢½SÁåt”3òÍe!Uæ’eBÎ.øq”A9F +¬ \2Œ +©R‘àM–“9šÌ¤õ„ždQVdDŒ¢v;¿¼> 9PE2ˆpAEOø#‚7Ä…bJè¡(ëõyý>—Ïç‘$©QLÙÝ•ú­“áµË+/¿òðÒÞº‘Ôc”@8ð>œLNÏGþê©é3ÝN/ ât†£a’ÄiMI¨Š"Šr(‚ù@ˆ9ž`($£h#†³½âÐÓî§ÎL?sÎqq&09^ã(<ë ž¹zê¼ÿìTpÞ»|¤Ëõ‘T¶A™BIŠó¦„̼9sÑ1çBb1>Œû<.hr¬š–ºöÊj³Ò´3…D«‘>½Ô»wcgûR¿V3«å´‘¶E 2;ÿÌÄìì¼'ŒJ,§ó$yÓ*ºÞ/Tê–¬s¢&Ê)ƒæ™”)U*‰ÕEëÉý£Ç/\}pwçásûÅZáâœçâ| ¥@žAT*Á£ëU“ëKùN-aëh§¬î.Ÿ<8„ÇwÞ½÷Û¿ûòÏ~þñ—ß{¼¹QMY,#r(Ÿož‹¥h6™±Sº¹ØÍwÛ…¤ÊÛi“â§/èóI¬ÕËí¥öÚö֣׿Tè¼hlÁLÎ<€^1œÙ Åó³Á3g&â(b¥R•J9—O‡£`x<Œ4/×kå+7n6ûÅ4×÷¶ÌB>€îN«6&¥ÕLeU‰0Û²‹Åj*©åRbNÃöÖkÇ+‹­|=§^Ÿ¿·÷ÊóÇï¾~ûµ¯Ü»u4¶¬´ # êñã(a€&㤈¡Ï`%›ïTõVEu­“õìÃË·l}íÍ+¼~å¿üî¯úí7_<=Ü[m6«¢ …£x rGÅY‹dSQ(Í8’×åZV/|¿n® +KâöÚâÝ›‡×N×ïßÙ}ýñõ—_xîêñÕf½i¦Mž§ƒÁÀäŒ×¤gœè¹©À¬;;á8;±0=ëõù€7 ÐDÆP[’® +K”‹ÙR©`¦lËÌGbô3ç aݾx4èç± J¸ª)äò(õ•î½ãÞã÷Þ¼õ­/ßÿòk‡ï½¸òûï?þç_íß|øò^»¹»5´m%X‡ËëqÎã!—- ý,ÖÏ…Öjص óæaýÊ^ ·O^ë¿öÜÚëWß{²õû~áÿüÍßùÊÍçŽÊÙ$æò¸ç\aŒ’qJD¢ ´ ô¨'\]QŸ\©¾ÿÒ¥wž_}ëÁÒû/oþâ»/þóo>øÅwüú;ÏþóÏÞþŸÿí7ûÑ“×î\;ìf³v4Æ…",VÆTjyq­c-§—JørnåéÕž½>Èm.eot¾ôÊÉï~óo~ù«ï¿ÿÞËîï-ƒDÏ»œî@ÜŠ³Þ§Î^ô;t.b‰HF‰é¬[ç|’2j[ЇËÖ›/¿ñò'¿þν~úÕW_yP®ZÃÕåÅÍÓáá.UùÜ +K h#E\^2^¼Ú{xÜ<^R_¾Ùýù§¯ÿìÓ·Þ}aç«×ÿá‡oýË?}òçß}ëÇ߸ýç_¾õãž=ÙÈ©L8è 8œ¡X%¯ÁúG%úþ~ño¾rãgß{íã¯Üùâ£Ñ‡Ÿßú»oÜþÃßúɇÏþøk×þüë·ÿøÃç¿ò¨ó𤾷ZÍXZ6›•µ¬’,2 ›£­t|£&ÜÝ)¼ñìÚKW[O®5¾ûþßüèÝ?ÿño÷‹~ýé“ÿþ‡÷ÿ¿ÿçøå·ž\ï~üöñ/>}õ'—Ó†êpE/̆\AŠ`”`À ¸ã!§ˆûdl~Xbnl¯ïTGæÕÌã›ÃO>xé½×®¼÷泯¿ôÀ2ÓþEEÇ ¦Ÿ#w[Üs[Æ{÷—þæ‹7?ùâŸ~øâ¿ÿÉ»ÿïŸú»ŸFàÿöå?ÿößúâ½Ý•JÚVÔŒ M2…Ô bµ„]ðw7ŒWOJßysÿ§¿øéû·?zûò>¸ý~ôæÿwßøÏ¿}ÿ·ßñ>}ðëï>|õÞf)-³ âô†ÃÀŠVÔÕ•<ûÊùέò—ï5?ycûï¿}÷¿ýîËüÙÛ?øúÍß~÷Ù?ýôµ?þðÉ¿zú“¯ÿò£ßÿòÉíƒjµh{¡Â$ÅÖ­ÅìFK»½•þú“Õ¿ûÚµßØùð½ßÿø ¿úôå¿y÷Ú¿ýÒñÿýþ¯ÿëgúõ—ÿéóÿõw_þÞû·viq_œ™uù£Ñ(©²ì $·ŒpSóÝò²2¯Ül¾ÿÊö§_9ýè­ÝŸ~üð¿üã'ÿðƒ7ÿð£·ÿå÷ýøÛ¶ZƒÑ²]q5mÚ%Kéd˜Ý/ÿÁàߺó÷ß}ø½/çË?þðÁþíWùÉ£Oß=þÙ‡wÿÓ¯¾ôÛŸýøq÷ýçj—J!Å3473ë=óÌLÈå2?ƒœ.Jw¶ì'×[ï>Úüé·ÿéWïþûŸ¼ñ?üü?þèõŸ}ëîϾ~õo¿°õî½ÆõÕd;C¥d ‹EAǘdÒ¬vÚ½õ~©_àFyüúÿòÃ¥ï~ñô‡ÜúÁׯýò»÷ÿñ'oüÓOßþÕ'õÁé?}ÿþúù«¿ûþƒ?¿òÎÍÂãÓf%§Â4 1.¤)Q+¦ªÕ¶‰“QòþnêÉ‘ýÖ­úÇoíýîoŸüê“ç~ñѽÿñ¾û?þãß~çí£ï|éö‹Ï8B˜$d/Ló‘–…¯Uè«CíåÓÖG¯|úî}õö¿üú«ÿë¿þâ?zãïÞ¿þ•w/­VS †À‰(&ˆZAÑ ‰„­ñlVŽ-—å++åçêKßÿÂá?|ï¥?üø«_{õàk/®|ïí½Ÿ|pû÷.?¼Ò9ZoÔ«uÙ(²j"d±±£˜ùR:‘ˆ®M í“åÌ­Ìïõ~ðÞõ_~òäÓ¯ÜúÎÛÇï?Þ{ýÆÒk§ígwË›‹ÉZЃö†)Zªhf›¡„\*‘IÐy•X©*š½¶óÃ÷NóÝÿý—ÿðƒ7þôË÷þÑý_|ãúŸöú¿üâíß|óô“—›ONJÊ„!—?¦õL~ J¦Êàµ$ÙÐbËVøîªòÚõú›·;ßxåÒùw_ýóoßûý_ùç_¾ó/¿ÿÖO¿ýè[o¾ûx{uW´$Î¥bT²TéI<Ÿ’©­n¦—ÁGYä¨Ã=Ü-¾~køäêâýýÚWî~õñß~øèã׿þÂú›·†×VòËUçPšP"$39ÄPŠF:éo›äv×>\©îŠ7·ê/ž¶Þ¸=zëÙÍ7îm÷S'‹ÉË}k¹½DÇQÌÏÈJÌŠ‰¢$§K¹|!¥æ5n£™9Y®\¦ß¼µøÑç·ýÑÍ?ýüÍp÷›O6¾ñÂèµÃÌ ›©'û•ª¬â<‡€#§m9¼X!ñH@#C ߬ËÇ‹ü£=ûÕÓÊ×^XùÃ_ûŸþ›?ýêß~úê7_»~u½Ñ«dz=¨BÙp0€† +g4¾br½¼¼ÑL®—¯oUž¿Üþà¥ýo½yõ­÷Z}':æòºçœO”Oå—Jí=#7ày] ÉZƸ¼µ|ãx³›£®ŒŒ—®ö¿öÆs÷Ž—on×áµ{K™vQ75^S“òÅ“~ÜŒñyJ-‘JÑÊw4ÍdИB“yCÍRÙÖÛöéZýæ¥ÞµíÖÁ¨<¨æú¥\'g™š@¨qW˜^ò—CpÅë ¼Þx8¬ÒL·TÜÔ®¬æž»”{ÿ…µOß½òÍW.ýà«÷~ðÞÝï½uù“×v¿ùâÖ»wo¬™:AŠS}1eÒ•rh$JÓÙfζØ`ÛŒôôkËÖÝÍÌ'oþç¿ÿÆüí×~öñ£ï~éö«w×oî7Wz™„®òZAµQ.rFå8^o”›i…ËktÅò*SNŠý’ÕËéÃu¥ðìáðd¥ÚJ±%]6eY 9eœvÚEMÌÄ& 3eÈ8Ž‡C,³t­±Ë™LÕ63*k‰tVå˦¤QQ‹¦D©ZíCBÄ…°A\L—г0]><&xV.d«Ë£½åÞJ-¥¬U“··ëÛ5ùÚjùh©°UÓ¶ëúJ9¹\6Ëšàv:‚˜JHµ –œ˜|îÌÜô¬'â)4m+BÍ”sR¬Ÿã׫òUûÍ»+_xnó…ãÁá n24…RJ²¬–7‹£»—xÀƒË'$9Áq’,'jÕJ1kä,9gŠºÂ( +/ª‰8+(iÆŸt"þ¸J'Š1Áš ^D¢Kÿ–WÎê±pИ +D±|ASS,-04ïÆü!Ò¡=azÖ‹O»ñIVvŒú%LÊóz guãd5±Ð&¢ht:ýþv«µb$-ˆ^Ѓj²â ãg¦sA*@gãj›Ñ[b²Ñúä´szÖÇáiV"‘ÑF&¥× +©ånît³~©Ÿ>Y«\Ù\\o{…d'o•¬”?93¹àŽïm¿àŽ=5åô1†x³ØØÉû,M'¬dp9ËŠ±Ý^ñÉ“—î߸²»¾Új˜ªD‘xÅ11$Í™šœ½TÜ,ô®g +«,—¶¯Üx!†rŽ…A*ºžÏfëÅRÛ¶J£|D8*ÏÌ#S¡iéÃ,¢;#jˆ0i9ïõD#A$Âfœ±9 +dî‹ +a ÓÂY Á§+ +Ad½aŽì(™Œ³Y6Õ•s#gTþÌ„Ëé§<‰Ñ†’jpz¥R1”…J«WšaO ©Úi»¡Ê) ¡â˜Œê¼ =;é©8—Ÿóÿú³ÓŸyzvj>˪j&´L,FÅŒ§]Ri$"aQ™ŽË ’ 9çt¢3nŒ·úöâU%7’2£ÊèTε„€p¦`uÒÍ}£¶ãSB”t£¿1íGÎÎgžì +ù5!³B$ê1±@+¥Æò e¶g¢jˆß”árî˜Á“qRsxcg§=ЉmS‰F\,ð”Ÿ0¼¨FýeÁíÙ?’æÃR„ÉfKÉtÅT-™ëRžÀjÕeFØ ¡5h½É )Ûí­fi½d²L÷áIOÜ3™•vF¥‘@y“ÓË”’㌺^&²=9Uã<ÉJ’f³j~ÒéwøÑ ¦³F_*ï‡øÂl€:7~z&0Ã\™5û.Dœñ3Þñšü›†p‰`¢ e+ý£ùññíɸX +’éi¹På™™ñò×ABgôºUßÇÔ¦ M¹Ùkj~¤V¦]„+$ĸ4­UY«ë'ÓN$áà ZkÁÈ,„„i*vëHÌŒ˜T/Äæ¦ü‚ËÇr¢Í)ˆü˜Z—Ë›Zs7»xY.­ج#*ųÒ?&Ä"|´œéå³~ Vv§"‰gø™Ôß4ôWÏÌ]XˆÑ©E­u9Õ:³+®ˆzv2è ‹ ~á³þ‰4Ìå¹ô²V>íjÌGÔóNbÞG✊«ç&!à,\˜:|T„4D½ÆJÙ™ù€ÃAâRÿžåŒHsAa!ªx)Û›ŸyzA*oE•®äV/Ý­,ŸÅ,ªU#b2j¬Ù’3ƒêÊ5.»P)®…ü $àµ&GYKÅþµ½g¿lvöÎyI'f1ÙM¥vL¥W©âÅ´…ñmé)ò/¿Ž(+Ähs$äÖ©TMÁVœ±„7üd +•k|f)Q^eÌN˜Ë(¹%»µGiUÎhÅU2=0ºW­#¥º-d—H­R[»G&;^L¥ñvZ\vˆ%Û!©‘+"`Ëòáᣟ¹è%œˆWj> D©úISN5kƒÆZ¯%nôBR=ªtHcʼn¦TÚ×f‚ô\ô¡2£Õ¨Ô(&Ö]!Þæ9½‰k iÂñ'›{0G1­Ó:~(rÜÀÔ*ôšÓ ¥ì‰ÙsÙx¢‘jÓQmÂIÆxÛå¦Ü¨'&û©4©µ0¥†«08TªZS°‡s!ÑGXA¦j&*ã»l&œØGœ7Ç?Y·YWLFµH¢Uêœ= ŒV„Ëi Z{&šp`F@(KÕ}:».Uv¥â¥s Ä´—Åä*©ÕÜX2Hå¼XÖ·Aœ#lÎONºÉ¨X”²ËRfÙ‹›S^j!ª¡Z‡H-‰©¶?&]˜G>{Á7íÄ@=1HÙ.Dóá†'¦D¨Ôœ3²…+f‡|n•Î®P™•¨T=7‹˜ÞJ–Vv¯½–ëﻨdHÌ`~‹+ÐãåÜÓ‹ã‹m*«jiäÀ•É¿7â‰Nnévnõ9sñjt¼l¬F$Û\nËoFÕV€/Áø ÐG•->7raI'ª1Ö’Z?K{„1^‚,@ÛpdjÑO§Ü¨J$›b~”jØc«u U6òÆêåÇ„ÙŽ%›æàzc÷ÅÞÑK­½­þ•ÂègtÌüppøÐE&ƒ|J‹Ëo%['¹ÑìðN²uäÆ÷œÒf×ÚQ¹õIØ2Õé¬Þ\9y4Zgݘ“H©•KéÅ[ÉÖMTëû™‚º†Í¢RуJž¸4æ"ª/®‡iË>*#åWÕòFˆ7ýŒà˘1€ÁLÚ×}LÆK4FWÄÌÎw*À†Ä"¢6c‰™^Á´ˆ˜E³Å§ûD¢áÃt=Æ&*—’õ½díÒ|P¢EÆZ&“}ø,7™žCó^ÞCØji;&–/¸±“ŽëMÊ’éå¸Þv ‰ñF¨Ùå _† K¡°‰‹ln-ž¸›¶†Zeg:e©¨4 ¼¿]Ž‚¶Ÿ°øÌÈÕg¼ìxå^4²©ÕvãZwÎOÏx©ù }º÷Ô„ã³çÝO/àn2ǘKBv×Zó>Ö¨´IÍKZ*í§mh7&5€‰ÎtNìöBXÌbz],®›Ðk¤µÈBš]½²^X:æ3½yT!­n¼€Í#JÅÉ|R; Ô˜dK)®:ñ$ /mÅâf~ù^ÿò›•Íç}\8ÄΖ¶ï¸èôà‘‰Š 6³nt¯ýktº'š5½¶7š´Õƒ%îkõ]µ¼Ž$JRaiý曃+OÆËFµÙÂfDªB¿—{K‡¦B,™lÚ½+™¥»…µçÒKשìJ@¬¨¥õîÞ éΑѵòÖàà…æö³Õõ{£“׸ÂzH(Äþ²k-ªBÁ iÇí—²+Ï'Z'®bÓ Û„Õ™ð± a•ê¼½Éç¶Õê¾ÑÚÐm¶õ±¼Kç6ò+wkÛÏWõÉ­–n2i( “LÖ©ô’_‘J[¤½æÀÆ7»)™¥îÖݘ˜w"*Ô•¦:WéÌžYªmg,™iª¹åé°`‹Üóe¡¸«Ô){ÝCæŒÒêÎ՗ܘ1á¦.D.$´uÜ¿ò¡wŽnþÝd7wÎûøyÔ ­¥ÌúsµƒW +ø†ÕSK N³AiÂù¼—ÉAýX«Jiw!¨xXŒ•'=qЮ ?M`©_Ú3o~v2ta!êÇužòÄ-?‘Ð9'jz¨œßÔ iv¼ú–b³a¡”¨†¹B”ÏÇ”*ž0™ÕÜàvcïŸVI‡àtÙÅ#(ªd}‹Ëô«MYíüèšR^'Síx²-—vìþ-{pK*n"j}.®øŒ!J M4HkHÚëÑD¼²z§4:Ý– + cŒ½„é]&»*”7X«Ýß¾µzúŸ[ƒ× +™¥ÂòÍÚþ¡²£æ‹ë§/ñ»…Ñ‘‹±âÉE«w½´z¿¶ùÐî†8k +\ŠJ%kFecÖb}óùÅÃײK×I«OâèftBb‰Î®f†·äÊ.•%ª—äÒÚY2%å^ ù¡—-†å›_o¿rôâêñãóжa‘к•ÑýÎþkbí4¦÷p¥rç¥3­=wÜ ôvw÷ùÛ¯~ëöç?Z<}•Í¯ù™œ`-¾ôö'RvxÑ/ø™"—ÛNvn”7 /¿‘ÜDåbsõjº±9¤]¤‰µyµ»ÿùÚú½Æú]:Õ‹Š«}èÆL7–¢­“[Ãͨ_º{µ¹qò oöÖ m/Sé>m-"bLÔWŽ?ú +ÈcˆÍ«¥µ¼ÅçWP½‹Èõ [ ´Natþ}ÂÍ9¢º_VkÛ©öa¦eˆ×Ç™¥µÆÆÝÙ ?á$P˜µâ>™Ñànj;"5/xy#·tõÞ[Rªõ¯žš»èŒžÉ¥½ÒúÃõ›_¯qD”ãg¿¤ä—˜îÂS1cè§3~: Þ¤Tö Ë6n¼7<ù¼`¶/Ý/ÉdM°»ji-ß¿\]+­ÞÔÛ{T²žm_‚Ï ÅSMhm0A0Vh±¸‘^û•úæ½Dq¥1<©.Ÿòv?ÕÜ+­ÞIµ³‹Ç‹{ÏoÜ|Ãh¬TGÝ­|~ê6ªVCB>(®õ¤Ò‘ìpV«±~4;^:=ÎÅ- +Û^.­?×9|jR,¬'š݃GýÓÏ›K·cFßA¦Ý„ÉgíþÑ|œeìVLoãæLoЙ%`Î 2*X«G™ ÜîãÙM®xI©îjÕ-?eH-ÓÚáÒ2 ÔŠ˜Ö’K[Vûz™TJÃõS³¾b Þìd:ÇKOFǯ—¯KÅe:Y…`XY¿³œÅfúVû´´ò Ý¿nõNõÆ.›ê¤[— BÄÂj²{ªu/ÓÐ>ZÓnuö_K«AÞ. ¯Y­ýæÚõK7^éí?RJ«œÑì¯Ýh®_'´šUßÚ½õÖÖÍ/ .¿Ò=|);¼Æ›¥£ëw¯½ê§MiÅõc ’í¾¸&ZVuýäÑ»‰ò +€¢ÑØJ;Rí »|gãλ½ý'Zqcóô•æ¥ûq³©4wéâj.sÅ]½y57¼C˜}J+ŸÜ§µqo|·#›‹&ZˆÞÁRݘ\Í´ŽŠ‹'«ûwZ›·£ZoK½ä†–ä² ´ÍcÙäm£¾aõŽôæ¥ìàJ¢¶ $ðcv¡Jw½”…ÍdûÐêß°·‹ë/¤û7ùì +Ðѽ7·®ûµ®Æì6ö²ýkÉú®œÁÆåboç^çž^µ_b3Kjq5ÓÚÞ¾úêòþCw\ç­ÅÖæÝîác©¾7¾áÝR©Å½ë¯ØÕ_Lßñ:8NwÔŠ˜[ÃÌÑ|Üš +ÊL² SÕõ;+·¾Ø¸üª¾x5Õ;1:Ǹ¹2PÙd§µrƒP뾸¦ O<êl¸ÖñaÖ¹yÔ×)½ë§ìónÆK‘ú€ÐbaÂ)˜>íˆP29–ŒËåñòˆä¥­¨\æ ¬U·¤üK6ü„F©%@)µ¼–jíÒf ØßاsËç€p(­ÂðF~x‡ÉŽ"ri˜\*U—®²©&.Ê«7Z»`üû‡¯,}¾¸þ,‘îX«ºv‘i,Îîàh΢Yß²Z—£å¬D¶«Ú™Öš^Û€îÆ’-:Y²jkR¶? ðD^âcS^Ö`sKJe'QÝÕ«[jyä§tÞ¨Üýâòõ·äÚ¥Dí’ ªb÷X½¾´u  "–²£;éÁ]­u’ZºÇv¡Šæ"’™_Ú¹ö’ÝÞyz‰åqN_¼ªÔ.)¥diµÒÝ{í Þõ‘öÐÙX¢+”v»O–o¿_Þyquçøáëï?ÛÙ›ÇRqk%f®¡æj~å¹Þ•wÌÞ©0Ôâ²\XþËê¾€ÕˆP"õŽêtòxõòóFu­»~SÎp½E]/G5p ÏÜâU>½ÈÁ#?ŠkõúòÎÖ}­z ‘ª˜\É4wôÒrL*1Åm½{Åè$à¡^ÌðaI£¾ sÚG-DDTmP¹ÍDóZ¢q„ªµ—-ž7™ðÒ 15–hí+Éʶ^ZçSù ¥Fe=ÌägTÑ7ÖBLÞ¬_ÎõnSÉnº¾mAa¦3¢8c* ¹b)*5HTöÂl€™2ÛÐwnL‡ #'»”= +ŠmGx›6ꎸB$ëFk/QÛVŠ[Àcöèn,Õqb µºÁW˜‘ .µ1½ ºÇÛ#µ´Q=H& ýãüÒ©^ßær˘Ùö‹¹TÒk—Àåƒ\.Âç¨T‡IuR•õtsK..ÏÅ丘U‹ƒ§“J†³:eÒ½XLkÆÔ*`$m-©îBŒ§Œ²ÖÜ* ¯¶vU6îÏÇ$"F­ºzmœ[‰ñx¢GÇ…á 0.WXkZ}ÁK@$ÊÃpäÂ*øüãE%eGˆ˜^Bs}»¾Vîno^~*/{È4ŸÝó;¼½1"CDm)¹Õæú­þ¥g}TÚGç¤êajéY¥qÌeWøüzTi@`î>ȶv þ{¨LT®ÙÓÅý—kk·ó½#«¾)â•·?ª®û¸²Ú80Ç˪§WµêN˜ÉvG'ïÿÍo®>þ*(¶ MPéeµz˜^¼n4÷¥ÜP4;Ïo_3¦Öü\&(×âzϬ©¥¨Ü ±=¿Ô\¿†(å_õ³¥…Øøk+“[… +Çäbº±Û;|ÑK¥!E‹ÉUèz½º.—V¼ã­÷L&ÕòS©…0kU5»XîíŠv÷‚‡9ïa碉_A~i#Æè¼Ù4›[­Í[õÍ;zckìPJ•M/-D‚0ŸSË›õõg{¯QÙ-–™ò Z~¨–V]Dò¢ŸžòÒŽ˜ˆ >·)”wæÙ‡ë•ÑU@Dð£0›ñSiˆºV}?×½f6O!z©œZÝ%ŒȈ×áù^D¶K«ë'/•—¯D¹4ÈÄÞâÂÔ–Éz ªRepb׶A«#–¶ÒÝ“úÖ½yÙ‰ÛsQM/VG'ÐŨX%R‹Ö &JÅ¥kZq-D¦ë7ê‹{YÉY²6ä¬Úæ³­ç#| ÔÏMĘAR>;\á¬~}õ~uõÙ8ëbfà +ª6gœ3ª8"rˆ²€.„TW+o1 ’‰²Q^Ž é¥yI#Ä—hsQ-®ñÙes/–0këéöÌ&i PcqIÉ,pcö„IÊ¥âðÀ^,æ‹ãõ¾"ÒÒÆõzo,J+饡ݹTY¿awçÂ*¤EpCŒ/bùìL|ÚË£j=QÝÉ® +öÀ‹jÓ&Hg´Êo/=3:?x|vÕêÞ*—…„‚¢H`r6.Â|v¬B‰f@¨¸è<”žh¸Q¯lèõ}0ÓDuK¶½lÖ›³!>ÊZ>RwË ®çG·„ÜL´”_Æô:ž(å:{…¥+~ÆôÒ)k‚¿/Ÿ¼²}ûÊÖ¦°îŽ' „jÍ?›)öÐ.9¿ºzôbuùD. kc0«8Q + 8$®µáýÓÝ+…õ‡-¸˜Éµ6ål‹!®EO49kN„ë‚ Ie\oS05öÀMÛ³ˆ ÌLsW0»ggýgf¼3¸ÎÄŠ—ÍÁ´"TJÏôЦ,‘ê3ÙU¾¸P烗‡¤ˆ=ím½¼äFE/¡-Ä“a¾¢/ñÙu˜ÖÉ ½&ƒˆÅónj!šp,9HÕrÝ«fcïœPé|wj{6È-DdG,.IY£¨ÚŒÈ•…]Ûõ-¦»ãPEÖBDös„ZUsÃqÁüïõŸ#ãSι k::ÞèG+®êÅ57ªUVomê­!·ŠŒW攧Ü8BêJº5ïFD¯ü£7Çš€k·‰vgíÔ,õQ6Iè7i£‰¶RÜ$í([¸¸€Îx¨Ty-×Û¿è¡ç¤›>7‡AéÊùu)·ê‹' ;¾Û«ÂÍ9Gêµ±§¨/a…Ø h´Ø< #{HgÖPk5ªõÂRÅi AA:íŠ(›«M6·!×.3™e'®Ì„È P´Y'”b\*ÓF\.mæ—®–o£zg>±·”¬nfz‡‹»÷ûµ-$QÏ.W·î +å Œ^+ïQЈ4;`ˆJvPY¾áÅS0 Bn9Ù¹–î]ÜBµv+úH3Ægáý˜" ÚXTË;ÕÕ»…þ5Ö\ôƤFåZ j”28ŸßéðS™ù°ìÅ“PÛŽ¨,å׳Û™áµÜòDóÄÇW ÚÈÙÅÒðš˜_ A ›}Të0é¼|¼»e˜ÅÁîõW¢bi*$A G”FD,+¥µz>3é¥CtŽ3—0µ©-Î^…œ~ÁKŸ™AbìxÙ´˜Ròâ†/nÉ4™hD„"äÓ© à‰É­ÕÓdeÅ*¬µ˜j)¥MÖÁ?Oùùg¦ÃçqR)OÌE£œ­U6 +£[—|«µ÷¢Ÿú#l±·/eP™ •   ’¡Ò“r¼ TžhààÏÌÅ hᯛ¦äL”’HÅ&Ô" Δ‡üÜß_ŸuÏz„ÎJi!LOûð^ˆ@‰¦H­Mêm?f_t1ÚDcÚ…\œE¦dLeœa¼V†Ñó[«®x2žh@wBË ‰&i,&Ê—´ú!“Î#êÙ…¸'ž 1DÌG¤"k/IÅu<Ù +siGuСPzr¥=þæ…âÚ=/Wœô‘aRæµ¼,1F•HÖ A‡7Vn|!³z1»~6Ç-ÄO`z+׿¶}ãKë×Þͯ<œ Š>D–ìm-†ù<Ôaô±ä’Ùº"ç·´í +³¼ÑäÌ®VÆ;ÐÙI/?U1£g/^“ +ëñD%ÓÛ—K«Òe 0yÚ%[§jâLšÕ‹‹{σ¿ÄBTocö(ªw°d§²rÝnïEùŒbw3‹ÇàÚlf€YV´Ñ¶;Õá„Ï bA¯ì|&Jëlzd‹`mÝ”§5æ!`:>|¨˜[‹K ÆXXˆjjaÒ›sÞ‡›±Êf×õ³u9*æAKì]^Áõ¦ŸÎDÄÚl4 gäBT¨”Ï\˜RJQËõ¹TC)¬†Åât›öàŒRÖÂø¦9O/‰*Vü„厧.¸È NŒ5Úq¹6éfT&Ày¼ïX€ÎŒ~w†$«¡W¡´þñZÄXñ<ŒÑ!yÚÇN8bfuµ¾r=DežžBà ³>ð¬b„²Aþ‚{jÒ¿=Q-ÄfÏŽ×çD=1 `&"5µˆÁôrßL˜Ç ˆÅEŸûLi}>®Ï„Õ_q"²#ÂF9+Ó>¿ž[¾Ýéñ͉|”RÞŠÉ9Æê +Å5HR˜ÑF¸d„’B¸ˆðiÒhR© Fº°Šg—!Hqé^„Mù°ÄØÓ+kýûÝݹ⮠M!Œ— +^\ó“¦RØàÒËÀ0JeŸL ]ñ”+,pz-.¤'݈•ñDÓƒšQ¾(V¡—Ã\“rvk+*äÜq=®6È$”ÜÕd÷*€%Ì;(ZÍ*Õ^ˆ)Tz˜_¹ÝÞ{qü‹äà:–¨CÆÉÕÖ­ÆæBLÇ´E«suq÷ù•ã—ëÛÔê–U5»;عgÕ7ˆ4çB"Ô6›êáB¡¬Yo\¶:…ÅSèÖ_5pÆtTnŒ/´H/ +zP-ÓØËöNHs€%»!®8`ã"°Ä>ØDL©¤Z—¡¤ƒãfO9c ¨IgMŒ7Q.•ªl0ÖÒ“3ârÕQÎÌ£NDóÄi/$,BmÆÄZh¼¦´¶å'í^à@µ9(׸ +£m¢æ—•Âš+nþës^-7 +ÉLdG<åÄR@vsW´Ýðª°$3 :žv‘ã}`¡GàĽqc*šøìtø¢s†Yˆ”9HÖöGGŸ_:zÂçÖgÃHl‰–êÇ­iCÖ»–Þï7„Š\ªÊš P­°¾8O_ôKRjPhBúð`J˜5¢R \yù¶VÙž‹0“¾X€Ö)³ÇX½cø“j)ÓÙÏ­ÜÂí¥ Xòàf˜+Æ-?¡AI¹XÚ†ƒ4Yr.ÈÆ„<›EÅÊxÏA±(w¥ò!žF¹Ü¤;öÔ´×Wq­3”a5&”ØTW«bº#˜-ÐO\‹«UÀD®àÆ¢RÞ3š§¤µc;bgýø`ç¾Ù؉ɲ9n{ÈLT,ë•MDÈ;Ußͯ÷íÖ.–l9ˆ°„hõ¬Ê:›lLzpgTžIA¦ˆÈÕ)wvpâBª•ií¸iz¼Œ•Ši­ÊèVuõ>¡÷¦¼l”Í wT—®¸1#"”Á7Å쪘_c¬þøjɧ{ZeÍC™gœø|LpetOmG¥’|¢ØZ¿ÎÚýy4yÆEù’›°=T^,lÍ…ÉyDÐéê%¨çi÷ôTøì\̃§Aü¡´Î;q¦‹Ùa²yÕ“«!&?Q'ƒŸ¹˜Ôœ%P¡Ìæ7”Æ>ŠL{¢ê¬—t’_NæG“ä3îg&#Q:¡³î˜êAÕIÄ¢ìB,¼1Vu)oÔ¶r‹‡¹þQ~éD*®%F¸<™lØÃÃjÕ7_(,ßKõ®±'é$"e|t’2ûBq³6Øܾݾ^è]ìá|”Ñ¡×B\:H›À~ÊbRÎö£¥ã·ÁS¦|”3Ìarœ4®5éüZPª&›'™Á-Diúñd¡µ¥æúN,¹€ê HÒOA‚®ÙíS³s="Ô`ÌÝ1à·a,AF›CTwÜð’éDy«ºqß q€HãJ5,”`¾Ü¸ ½€ˆe||CefʇŒñ¹µüâIgï…°ÔœpÓS~èEJ÷\m•Hvåòag÷•æö ˆZŸöã@’éÆVº±À5¦~|íÊ¡T= +  šsŽØxÿ”dkÒEœw`Ð#ñ7‡k‰Ú)"V|˜6éÆÜQóŽãK~ôGÃ䦴bbÉ 2¸Z÷`ü)Äzef#c +Rò}\-ž›âB1‘_KVwź·¼ˆŠ+”’£•Œ?.`BÖ,­ë¥U½ºƒé]?W:3ϸ±ðø’*cÒ…¦…™côXk)@¤/8ÉÏ] Æ…œ]߈0©‰…øSÓ±™ˆŽ$—Äæ ³Kµ~RO·rÝH‹58ï¤&CªŸ¯aÖº˜ÝZðÏLh¥ÜÝ83ù«sxBX¨ñù5o\‡.PF„ËÐFƒKÕi½„+vÔ\㥴’²½¤U6éT+ÂÙð)›÷à¨:¤NÂW²â3„Þâ2+„9„öGeˆ!¦küõ I¦zB~5T/F\kÄåŠZ\‰'Û“eÒÏ…™Œ”]¢ôF¢°nõ®!Z;¢Ô=Lv&*;a¤r€/Lx<Ñ!S#Dn†˜°ñ|4qÑ |b)èbÆK¸PPõ$›ZÐ;йsa%Êå½Dz*$‡ÄjTmÓÖ2$8nN‡¸ gJ² ¼!ÖŽ&:1­ Ç/•¶¦Bü3sÈl‡éö‘é ›ÅôNTi‚¡Û­ƒDy›4Z!" q,*!—ÍFµ ^ÖU£ î ² èø¨¬#¦Ç”“^› +Ê Qš1D™¸˜›ñÄÇ«Uc0òi,Ñ„@M1íã}¤ ‰c>*OzÉW¢Í¡ÑºÌà@”³sȼ—n/¤ +K¾˜¼äˆÀô ¦ÁcƉ¢l­¹úäõH)3ãçf4c\øɱ .ø‰9Æ[L.Î8W41ãã“¢.&Œ9æ0®K´ÑD×ÏT.:ñ)g|΋‚ LþÙÉð”—ã3ã+²äÒ6‘]ðIŸ9f‹ó%.Ùõ³RºÏX#Dl8‘qì‚nBX WÊrv)ÆÙãÅOpÕç” ‘¤µŸ(ùe&Õ&B+xpu¼}ž—F¸ ®Õ¢‚ᬸRĵ:iöd6Dgq­ÅçVÊk÷ª[„â–Uá 0‰^,9e¸å¦ò Mã½ +Ë´ÑíÐù€Pò0ù(DªÂ%*³žå‚ +è_.ê…š™òñžxšN ¢Báìt`ÚÃäÂ\HŠ‰u?U˜ª³áD€ÎaZÇMdÄ°d?ªvP­7‹êŸ]@?·€LT(øP5„©œÙÈ¡3+hrÑÃæ¦BW€²@‚@Ç|xŠKÔʾRØl¬=§Ö¡$H!Ëõ)/~ÑC¸ñtT¬I¹õòð6a 1㢋tF¤ùpÑ o¢j+ +èž^ÂwD¤‰ùØ\0iȃn pE`xÊX³ë!¾tÆoKA‰ SF—Ï,O%@#œÍ¹!BAàó>x`ÎÏL:±)å +²‚’·òƒ\myÚ¿èÄÆ_èÑ6™\œ )çäg&\î¨(µ~ÐvÊ@“FØT×™©ÐSî‘ŒwÓÓgâBHq„¤?öç‹ëN$1±@ |1ÓÚóS©¹ÿ{ÎÝaGù +üwÂAi™Å/¼¯¤ûOMFÎÎ3A6d²bÜ1ðl7DšPÕp`PWÚ¯Ó+åh½†+@ÞjK¹U*Õå‚HÀ~"éBDG”1i "ª…i`¹®VÝN5wµê®×Ç‹°qö|X€Úv„¥(Wˆ…lB](gj-D?•6ÚWÄò!_<`²ãÝBl(:ÌeÀăxJÎïp™-BïÉ~LiŒŒx#¥–ÂìséuDnÃ;ƒŠ²öÚ¸®øŒê´xÉlDé¸Èbˆ¯ñöŠ^v†Ä(®5GG1©ÈU»!e¼s=—îBÌt¡Ê7F¨5é/ßIB±˜ìì8¡n8_R› A + b‹ §NÔ˜psÓ~)Äyk„'‚"0³‡0ƒð€=d/xØ_Öª{ra-DY§/u¶ž U•‰•,u1 ÌGhÏÙ ˆ!O[Kˆ<^ÄÒUÁjÁÂÎÎÆãBÙÓ „X2Ê–„T_0z—‡RŸp ÙÚž=7¸0‡\t3>ºG¡IŒ$ÎÌ ùÖ.øéÙyü‚ƒ [€m¼^²ù¿>ïÿÜÅ'¦;£Ê¤‹>7‡ÿ:¢:.Õi­éëg碓nbÒC•!|Á‡™ !ÕUôl¯ØÙ€¿èbÊ"@‚3n,Ä4nÍ¡ú3.bÂ…Ïù¸T•²#B«ÈäBÔ•œó“aJóÒjba'"T¡fsÓAá©™È9æˆ& 0LÆcÀm¢R%ÌbR ³ˆ2á"¦ý,Tˆ’T + GEL®F’É6$”¨Pr«LzJ>Ê„.¡€dtÁM8B\\,âj ŠÍ×"Ôx5N57Š†¦˜öÐp‚Q6 F‚T%P¡žEø<¤6ÕDíˆÍnBYòé!„…ñ×Ý|òûlˆ÷’¦˜[g3ka¡I|QϸÐ$æJ´çƒôÑ|$9·ÛS~qÂÊÀÊÙÅ0—ñ‹¸Þg3›´µÂÙà•µ Ô œn6,NxIDjb‰—_M÷Æ_†yºcŠ˜¬Ý~³¹rê%R>¶V!©ê£Ç×pÖÀQ#tJÊ '½ àxKôˆîç*”µÎçv0&SíyvÄ<¤Ì"†TºTÞ$µú™)_(®ùQmÆÏBÞ„z»à¢ÎÌn,ËØk üççqo <:£¦L0¬¹ )˜³—õêÞ´‡™ˆððÇ-JëA1»# À­)?‹'j4ñ×ç}SÖ‹¥p¥Ni T(ºÂ‰s³ÄEUf J;$Ôà*3HJeüu´P °~T¬3ær€)¸Q=@Q!Oé£v ä6¼DΉ¦çÓƒe&}ÂH(¢z #Äæ ÿ‚àƒÁû]wAÕ‰… °fºáS¼øŒo¼¨7&UKtz1Õ8PÊ;À6~ˆ²6Ê¥Ò(ÓÞg÷±yP 2Q‹Ò–•Jq!Å`´(“ôa Ž\ƒRñÆ”i7cm!Õ!¤R˜N£lL‚ž ®Îó,DŘ…¾`Ì2Þˬ +䌈:Õqãäq| ð—¸Ü¦÷£R<ÂQ!‚0:€«¥šX„fôÓiÂhO…¥uÆ (¤‡1± >uÑÆCÒÏtO³½«^ÒrÆ%¿MáF ‚I3à fòƒÓÚöCÒ왬Q?JtÄõ¹¨<V@Ü&œT”É0Fcæ/¹r¼9_t¾pÁËL8âÞX"LÙçç¢@w}L1,6{â2@k 1h.xá”›Š+í¨Ô 1e?™ÇÕÂf&æ"0gXyz*"1)n …íîÁ«Û÷¾‘(î>=…8‚#(NÌÇÁŽ/8Æ—Ù‡ÙB²r)ÕØGåʤ‡)vI­íD’ýâdDw@)½æÚíë/}{RžvaVˆ«Ù͸ÒãøËnI?™öâV\šZ„^p ãï4P¹ & Z{&œg!’Ëdr8R«çà +0p¡uÀ^£|8hÜê!.«”Ö㼿½MºcZˆ4B„òå+1¹@YñÕ¿ö:iL®„ÈBh—Šr–‡ÿv£–6ÄÌȃꮈ„‹E”Ë̇ȉY÷œs„yRïš­£ˆP˜HS¡Óg/øéy?$MBo‹Åm&»ê%SSãM–àRGL + y/Î!ÌEJÚìvÍ#r\«Ë¥M>»â«ÐtFãJ²›#ÄÌø\m¨åýˆXrYTkžJŸ¹p…D\*Îù¨)g ò8|.(¿V¹ÄY”Ü4½4‚”íÅXU@äñqµ»­K¤qÕUªK׈Ä8©E˜è ]S¹“qÐ,q©H$@qS +(Î …DåtÖ$>wÞsn9»?ïa&ý¼ Q0ÂlIʯ kÖÏÓÉAˆ…”šrFä/ýôE÷3“>(› ‘……ŽSª»õÝ'öð¶ÑºL›£iŸüÙ !2­g¦"®° þÂZƒTmŸÖ»!¶.WûÿÙ{Ó嶲$Mð†?ÆæGOUWf"Eh#tϹ÷Ü…ûNŠ‘"µ‹¢@Pdˆ‹‚¢Kvn•U•V=]5mÕÝÖ3Öc6ÓÿÚlleiÆ?÷s.î‚Ê0€Ìˆw9‹ïîÇýìÏK['¤›U'ï?øæÑÙ?}ÿÏÿýïÿÓ÷ÇÿrctÕ'Clþq4ÿdbµáMÞ#y~ãpzý`b­-=¹ݹܹJÂtêAr{‡X +áÉõÑþÁ-¶ôoMÀE <œÚY|š,>ªŽ¯‘êÎl-ßûêáþo ¯¬%‹¯Æ·I5ŠæžŽ,5F—÷I2Ä 'ïų÷‰c(RÛæîGK»›û»ñò´ÅDnþÄjuävu Ñ +o|ýÖøf8GÆÂÑôÚÉß_Þý¬:šÌm™é oòn´¸C&äíGß®×ÿžfô¥7ý鵑ró:M,>Œî쌮½œÚ8_mÜ«}áOM¬½˜Ý>Þþêïîü1©íùsÄÙîÑOᶙß"A¦¦·¦77þñÞÁ?»¨ÜùBÍš™m=‰ö…¤wé™GcëGfî1™ºÄíÿæŠù¢:>Î_US>b‹Ïâ¥=sŸt¹Ù=5½>Q{º¶û»¥Çß.¢ñå‹‘å‡dG{Ó›3ëõÕ'_ßH–Ia^¸ÿõôæéZÞØÆ—já“á±ëfž¤5ºý¯?QÞèÊôj}âN}x|ë3½ø™žÿìÖ ô_\%ÍÄ¿KzÈÈÒó‰ÕƒÑå:íËçÞôgÃ`;¿º>z Í”W¯“–ÎmÎFæwæ6NnŒ¬M®ïÏl~E’ŒÓ_ O~rkÉŸÝ™¸³s{ûàööËφi7HhÒ¶~æÍ}ê-˜…Gë{“ë‹Û¯‰‘¼&]náÁùâÖñÌÊóÑéÍkzöÖÔÂmÒ¨¿Œî|æ/Ðÿ¯'+3kÚÓoï<ÿ7“÷¿öw¯‘‰¡0œ¬‘Â0±ùzlýÕèÊKÚ’Ë„jj‹häZ´ÌG·GI¶ÎާݼX¦M'""+;šDªò-ÂÌšívã/ûmýÏfî)=äÖøº™ÙJïӰבëûo&6¿ +È„!í7®‘AMÜ;œyÎ= gŸM­¿žÙúfñá7ÁÜ£+>© øÆîoWžþzrýUu‚”ö'ãµ}2X&–Ÿ~âM“á¿ñâûµúôü³cwÉD"æóìÛùÍ—züN0³9½ÞØýúß|ÿŸÇ×÷®¯^]žXÝX{y}dõq€…'K¿_ÞùÝôæëá±»¿º‘|©§õä1dðNlÙŽ¬|.í’²7}çÙU=Íl‘±ÿôøkß$+_©)ç¢7¶2¾üÔ_©Ž’9³8{÷åöáßßõ§û_ýIÏ>$E—ô¢/5Ðé“›ã_*ºæîÔz]OÞ½êÏ~r}ôJuŠ;˜Úü›/ÂO‡§HRL®ìßÝûÍñõ_U'?%Ã0¹ã-_CJÏÜçÞì/®Žß]!€¶H]¿•¬E3G—‘ù€¢©Ý½GÜïöãw4Ó_Þ˜ç­>û¾¶ó›™Í£[£5Òo+Õ±+zjdöþ°¿ð7Ÿ‡7¢•Õ§o~ýOw÷ÞK$’:'¸pï˜D3éð“w_Ýmüí££ïM? ævBb•sO®¬~¦ç>S³áÌöæóüyçü?­¿üÓ—Ñʧ·ÈŽ˜­ŽÞ%éæÍÜ÷ŸMnœ–.<|CF©dD>Ÿzó„–ÁÌÃÙ¯IU£­ü"X$é󹞽&üŠÏz§"55ž8¿qpçá ”êøÙÝkÏ~={—HuëÖ)Ož„µÑJ}éé·Oßý—åßÞ õãÖÔ•[ÞÈòLmoiûMr{wâîW4ÚÜ™µ}RÛHÏ¿Ъ.Ò¨–¿™Þ8 ‘„óOƒÙ'3ë#KO†'6oM?_yyïàOû7y÷(˜{LÖq²pzíùòƒÃÆ÷“›‡ë{¿{xòOÿ<¶ö±\¢”•Ççwë¿—jæéèòîYFsD&«_ú“j‚vÿðÞÁ?ìýú¿Nß?#&i5½=¾¼;¶üxdùþÒ㣩{¯ã¥}3‰7±ñ¹7CsŽndz›Ów뤰Íl“’?¾\_Ú½ê/UnŽßoëñÍë!ɾu"L“kûc·ë¤iÿâjòWŸx‹pòÜ»[ߘX?XÝùöÞÞï&W^\õ'«ÉÂòöÁû§ýyHÌŸÌÛ«¤äÌI]$Þûé­Édñ9ñÿ¹{§h»\;¸5~ÿšYY}EVöµpù_&ÃÉ +¡‡7ý˜Äßõxýó[s¿º:Œ®Ž/Þ蹇#w¶Žþ¼ûö?®>ûõTmHif}ïÑÑß‘Ü$†y…XIɵ£™­×$ ¾Œ— ÂéíµG§ÏÞlì~OkûE°BO¾­’ðúÕ­‰«#ËzþÉ샷µç¿ž}øš,šæ;AâƒÔøuZ:z`L yå ˜Nß?Õs_š…[ë8¿ðèÍ“o–ž|3Q{A¶<1ZgÚ…‰•ýɵC²Í'jÏ'îT}pcty|mo£ñ›ó¿ÿûÿsrëô3‘Ô‰«zzfùÑèÂýádùª™'Y3ÿlñþùÌúWŸ ]©ŽÃ=˜ÔÈ4½ó´:±N:½»ÿëýïþcý»¹ýü×jöéͱ͑%Òs^†sÛÑâîÔÝtöÜ>ñŸ\#e{ÚŸX›ßx¹„|Ý“àö®&i¹¾°ýzúî!ÉÓÑ;O|õøIâ5úÿÕx5¹½GÆ©š¸ç®~vsDOÔj—œÎn½ögÝLî^9·æ6h—_¡ÎôÖÂÖñÔÆ«piçÖØÖ§·ˆ§Í’ò3µøl8^F»Ÿ‰ÍeÂüÝ_/lŸ’a{5Z$²Ý~ñýÜêÞÿô77i¡æî.=ûnzûíèò‹›„™_Ž\ –H œ¸ó¢:²2<²>µöjjõ«x~gþ.âGÄF¾PsþÄ&iÄë®E55÷”„‹»Ž­²àF&¹v·ñ‡Å{¯FŸÅ³‚éÍ;^=ùê··âeâ¨×GWoMßîÔ¯m|ܾ1¶1<¶1ºø””j%„t,¾Ô“oŒlÐ+*Þ”?ÿÐ,>S³÷oNÝ5ËÏï<ÿÝ£óÿ½¶ÿ÷ÑÒ±ôêÄæÒö×÷šÚzk_Ü¿w ¹+[zê.ZÌß©“<¥e$]nìÎs¢G²•¾ —ÈÌŸÞyŠÃפ^Oj~woÿ›‰õ§ÁâÃ`žøØ·S›§ÁôÝ‘…ûW£…/ÍìÒ–®è…/ý¥ëæε`)˜DøÀ,lMÜyðôõ?Ìl¿"…ÐÌ?!NB|õz\›Þx9Z«ß[' ŽÔ¯ÍWóÏÇWÃc+d6êÉÕ©ÕÙ»O'jHÙYÙtò¿.>þžFkfÜ&Öö̃›ãkÕÉ»óOêoÿéÙëÿåööáøÂv4µAêýæË?„·w‰ nŽn’™3±v¸õòoo?:¹fnKjì!'QktgïúØ&ËgÞT²ð`ñÞËêÄÚhm?^~9¹~üäøŸˆ™DóÍôý/Ôü©Á¤5Í<Äñ±Éídi?Y>˜ZEHò×WÂ/ôìèíÒÞ?÷oûóOW÷~s»þ·kõ?’Zò‹kc¿¸:J⃌¯xééðä–Gúð½¹{_Ú'óOˆyþêÆ ’ªdúÝ$Û|M +˜ÕèºY»¢h€ž¼Í?ýB-~^»†l-=ÃÙ¹íá‘•_^%6Küvjeodé©?µy3º³úðk=ûàÓhñÚÄú—8Ö½m–êÉRcéþRö’…‡c„ KO¯†«ŸÜZ¼‡AâÍâã7þÂ#Ò<“;ÏGkÍ=”"_Üù~xbûŠY¿[EAƒ`ö>p|ýõäæÛñµWô5±–Ì/}¼ôàüùù¿ÜyþýØúËáѵÏýyÚ5C&ùÒÕh™Ô•å§¿ÝhüiöþÑ-”†˜#ý$ ýêöÓ›ëŸé©£kÓ÷p¨*œßþ×_Æ¿º1N4过PãÔ½ë›áÒ³Ùû¯Ãå翪ÎêÉíëÑÊõp5˜¹ÌnÓíÑÒóÛO¾÷v*jî*õÖñÂÃwdÎm¾œ^#~²2S{º¼ýj´¶;¶º÷E´RñÈòºC¬†ÔòëÉÒ3·xÿÕÓÓ?î÷Ï·Ÿœ}.q3®Í®6VŸ¼Asj{aëtùÑ[ä¨o}êMý«ÊÍëfqäöQÍÍÉ{ã_ßÞùÛ»èÀ•,>¹™Ô~qmäf|›„”ºñOnNþòÚYÜd°L®î!]pt}íé[Ni˜¾bVHLüÒ›§­!šÛ|5»¾O¯YÞ¹9q÷æäÃÏ¢»Ÿ«áÂÞzýµÇ_ß]¹5R[|xúüìϯ~û¬<ûŽ¶©2<]!î7qozý˜ÆçþÒèúë?ýß÷_ÿ‡¸Ö ]ý³á™±Û/ÖŸýšhöüŸ¯ýÕ§¡?ýèñ·ÿõÙÛÿ0»qHã$‰Cë0rûÉUsûj¸BªB¸D¬ìïßüãÿ³øðÛ±µýk¤WOÞ»1¾Yº¯çŸ‡‹{D;jòþ½ÃÑå‡×Ù/iÇÍJ0ñhzõèþÁß®7®ÄKKÛ_ÕÏÿYÍ>0‹OüÅG×&6I(“Ú6±~/=AW艻Ñí»uøO‹¾ÑS[¤ùãëÓµçK[_Mo4¾¹ûI°êM=žÝ8_yöÇéןzsWÃeŠÜýýÒ“_«…ÇWâå+ÁB2ûàFrçW7Ç*7ÇÔøÝÅïîìüvlõå—ÑÒ/oNTG×èÿ¿º9ycd}duÿÎóß<û¿Ý?ý§É­×WGhgâ¹Ç_è¥Êé+HVßð¦îÏm¿ùê÷ÿmëðß^߈ï<[yöíòÓ_OÝ}åMm~f?3Kc·Ÿ/l¾º5橦îG Oï<~·üä»[ÓOà‡™d>°øHÏlÝšÚ¬Nn‘É`æÅsa +éù[ãS(|âôçzž¬ø1R–VvýÙ­xygfëë•¿¿ýä›ÉÚ'7§?«N_7·¿T37£…j´øéõÑOnLè‰ÍÙõÃé»_‘õ÷Ù­ ÂÛ[¯H™ÿôæÔØ—ßü‡¹‡ç+_%‹»X“µ÷žß™ÿ«+~°øbìî×KO¿uðç‰õ¯hÖóe|ûÞáËoÿ9Yzø¹žÿW_$ŸùK“wI[žG_oD„î}E2tR`~Yñfž&K{[ûÿ°óæ_¼‰­¿þÄ\Õ‹„NÑòsBû_|1úEX»9¾/ÓêÇó®FµÕÝß|õ»ÿvÿÕ¿›{ðöó`íÿ»$ŸçÝÀOõL¤×>ƒ‰ôÚg0‘^û &ÒkŸÁDzí3˜H¯}éµÏ`"½öL¤×>ƒ‰ôÚg0‘^û &ÒkŸÁDzí3˜H¯}éµÏ`"½öL¤×>ƒ‰ôÚg0‘^û &ÒkŸÁDzí3˜H¯}éµÏ`"½öL¤×>ƒ‰ôÚg0‘^û &ÒkŸÁDzí3˜H¯}éµÏ`"½öL¤×>ƒ‰ôÚg0‘^û &ÒkŸÁDzí3˜H¯}éµÏ`"½öL¤×>ƒ‰ôÚg0‘^û &ÒkŸÁDzí3˜H¯}éµÏ`"½öL¤×>ƒ‰ôÚg0‘^û<ÿ/ÉgµrI>ÿÕ+ó'{s»ç»C–†®Lß6;ô÷Æî›óÆÙÐêЕ[Ógçs‡õóÃӓݳï*#òª:¨ÜÚhìU®Ê•º¨²vvøòð„€›õݣƵÊMºt”þò*ªâñÿ|74EATõu âXÓt%Š#¿jŒçß3žö+ǸÈÄU?Ö¡ŽƒÀ‹ÃòEG¸(&X$qaÐEß•$*J¼(’‹J¯kë¢Òë†î ÝJ*W¯UÜÚÊ|£u‹±n3go߬ïžÓ’œ¤‹¹³zz²~vxr~xòrxXÀ3 ZªìC«¯ñ‹òä§ÍíÅ…Ã#ìÁ­ô+­ý­+µÕÓ½¾æ6å­\ýöøè„~¦a¾x{Þxƒ½¡}=Û-\Q?8<Ú;kœàw]¹uûä¼ùþuþÝkÞ׫Êó®\«ÜÚ:9¬x“ž{ò2é»Ý£·ríAãðåÁù_}²{ÌÓXÜå7{yNßî´=%{õ…ÏèôÅWúùÌéÛ“=ÞÌé·íNoŸñ.=Óö$s÷\øTÛߺ÷,BfFßöô<¾k{ßuaÓ·w¦^ìî¨vçs¸GW¾g&¸æf#î6yþöìÅÛ£ÆI½Ñî*È­mn¬{ÏEãhÕ3íÎçÅî›ÆÂYãë·´íclá® Ÿ!ö­Ýž5Þ¼=j_¼¹Ë/|N'§›ç‡çõ÷H­æ¼ÞðÕ÷íË€Ü=>CÝîÔNÞ¯ÕÏwßu0³ì-íp ¿ú‘ñ·æ~¡Ü¸×&;iŽûGöà‚7çð¤ÝÝ9}Ý8Û=?=k{oš7\8Êmž¾=«7Ïv_ÖÛ–tïYŠ¬À;éqîwxòªËMFw‘xfO_Ÿ¾9<ï€v>Æ8XKnw·æû•Ñ Ø‹s˜€p`¶c?gpÿl—ôÆ£ÕÓÃ7—Ìl۲؀…© lÀ6öyÍÀØ€p`lÀ¿Ä^ +.¡ ØÁœúÃÖ—Å4êd&=oÍ4Þ5Ž6v÷N¿ùyGÉD€²­xYÄgÛjè›ó½¹Æ»Ã] ¨;{Ó…OîÅÑÛ÷H¯ŸÀ2ê’†°¸ûöÍ›ÃÝ“™÷αì¶Ñn¯}>¿× FßþDÚgó{ÝàóÐJ¯3µÓýý7ó÷SFÿRÿÏ°ÿèþº2² ë§G§g#ß¼×ÄÉJ ïŽÚ÷6Ú«/÷ÚN8yóöl·Þ°)¨íÎ*wÓ€IüsyóºQ_{ûê?ÏiÛ +:æÿöh÷löôäÍùîIûS+ßØ…ðg§³œÿöõéIãfÙ¼±Ÿ,²?éÓî"}ßöª|ß [Út0•w ¨N¶¥CÇ@—T‘õÓÓóZ'.³ã«llZŠ­Y£ÿ´£ËvèPÐöºÞðAN Žã½½Gýf.}@8¥WxBÛÒâÕ{\™ýÀ¥Ÿ‡ÑöDÞ£Âf'¢zYÃ~Õ>Õ¿êÑ·?¿ý‰ø]˜ÈîÙáùÁqã¼ý¨v?ÉÏK¶ïX)èu ztx¾¾{ø>s BÛÛ^‘¾{’;&¼.íçJãìe+Ù*Q§¤v‰·äãcx4H<ú¡9©Ë–xô3:|2lÚÞ»÷¥v@Y=Ÿb5{zz4sÖh|ßv4ô2æW©jÛ'£Ïv÷߶Osîò ß۽ãÝöCÜýg–^ªd¸¶'Ò/Áàöwf¯ýi“©|ÕôôìõÁéÑé˶9zï˜-îÖGÜíÒð´Kw4°ýÓ>žvJø¥É½4ßvX§o(¾³C'}@òýš¾{Ykt€a=Ný^ûJfV;xѾÝ'ìíò×:h;'þ/®uгÈØ/’©Ÿ«6¼'K%CA¹èÖa‹K#”: “žÏóxÑ6–õ‹üiF3±‹¤´š=Ø=9im6ŽõNt’å»&Žæß¼>Ú­7Ž'ç+»¯ûO&ïÒ£Ú'÷ƒ­äUÜÿ*¥¯*÷µÝYó·ö5>wy/3Ê^Èm»íúFˆuV»±4ñYœ^iƒ}ô"Ûë`;zœTÚ?OÛRÚ¶Ìû†´=£Àz…úö;Ê$Ú?<:ê$yí¨ ÛztxÒØm;› öúÊiûy´™z—¿ìŸ·¿M|ñÅŸêh›6畾~•‡ßæ wuÁi{Ò6íÖëoß¾?S";½Ì-¯á¿GτЮýÐ\}áó9k° Ùövííž¾ë`³Ò.>Måè›ÝïÚÞ.’Êç»gIq¹¾w#ç§íÛ˧ݘHÛ›³×~7¹¶K–ÉîÉáq¬ì#QéïòwÃñ m¦Çì¼ú¥K›iFýâ¤ÍühëŽ+¸ ëqê¿ü"냼™¾Ë›i¿Ì]¿%δý"›ú9q¦mm´?g.Xê€Lz>q¦~égÚŸÑ qf83Hœéh·~‰3êç’8Ó£ìu|égÚŸQ¿hâ}8ÓÁvô8©\êÄ™ú¥KœiFýÂ~âQÿ¤ÿt°•ò.me+ì !m°ý[Õ£öâ㣻c¸4%#§oïÌq]ÎüK—,‰µ}wá XÆÓõRp»By·Xì”çð·¶ø[øsæomO~ÀßümÀßúŠ¿ÍŸ` ¾]*öÖÀž¸Û€» ¸s·ò6ànî6àn—»eÃF;Ç/“k{ò?I·¯¢„"ꀈ¢Ÿ3µ=ù ˆ({pØ«˜¶eÚäfî¸p•ìgÔm‹Xáúá·£õ£Ýïv:;yÉøàYãøô}5ú«&ÍáÉ^cÿðä½-Q³¹`¯»çsôÏÜqáó{óUwÚ[?ÕÜi“]jÔüøô5j5j~Æ5jTû8øµ].z25ª½Š2ôo¯BÿŒÒwúï(ýPùèçQºb¶t\ȦWT©KØyñb­ä‹Nî }|¿èdN?ÓnŽ\ÆðMGxÖãLîCQ=°ÿôøõé2ƒ×Þ¾‡qýl®K¬aÖͱÿøBÛNÄWï‰÷dö—^¼É×öDÞãhËNDuC×n{"í“þ«nP~ûyO6Xv"~7LÔ³ÃóƒãÆyûl¹Ÿdè‡0èþ¥Ú@ý$I?~9ÊK Jû'üñA[Û7iHƒSˆƒøó þü#í‰øsg5ñçAü¹«ñçËÛóe.ÏpÄŸñ玼/—)þÌê&"ÐÚíHõÄœ{×ßÒÛ.¤AÌysÄœ/&°w¸¿ÿöMcöô„¤åIû¸Vºï‘î»ÆÑÑé7íÎóèðåÁ9ý>\GÒ¶§Y¼­w#9Bü›õÈÝsñšEÛlþíÙ>©ÀÍ-So³Ä—Ç–Ö/›ÔêpZ—¸-ñÀ7pÁõŠ ®A=ðà üp?Ü_<µKë‡û¨=~»bzÖØ==Û=y_œ}à »h,ŒÛv™î~xüöü=}³<Ã]áض=§ÆýÑ‘ƒ.sG×¼?s‡ìÖ¨AérîÌœ˜)5«õgºô^š/£O|o^7ꤟ]Ä‘ˆ ;¶„v:öú–oìeR³ƒÿö5ÙY0ËægÎÀ™3pæ œ9gÎÀ™3pæ œ9?DƒëFœ9Ö³Ã>3§·mÙ3ç/qætCû¸lyI=ä™Ú´vRÿº¦.giŽðqôºÛæRž*¾H_T—¸Õ ¦ö°2mo^ûºq7TãNfÒ¾rÜ Ý¸éþøY—½¾„îpàþèOç¿Ýñeq´?‘û£,ä~ã÷ÇÀýÑK|}àþ¸?úÊýñ32¬aÄïv´vM¸ý³ÝúùîÑêéaûùÔrs›{ìÞtÁ󪼌¡‹ÎÛ­¶]-æÅî›ÆÂYãë·“zûêuᮋߪ¶}(ýRÅèätóüð¼þ7jÖ—€«ïupú=wÏÅ{”ÛÎ&?y{¼Fæ]SËÞÒÃØ8(¦=¨¿4¨¿ÔKòoÿìô¸}Dä‹/Þ¯Ô¾ŒT“úñéu³šÔ úRßU_òªm7å:?mß¾9íÂT>j©®x“:®¦ôQéÚ×ß6Fl"ƒ +¡òwñ6º2ÃËÆÓ~²z!íí>"]O:Ò¼8Ú­¿­èôõnýðü»‘ÜÆoο;jß n¯¾øDpÌõ²UG“êšZ"öI]N§igvi¥lï70ºüÄ®˜9{Y^÷7l»NŠmº9{zÂÍÛÛ·-Š÷]8â}sÐÁ±ÿ#Û~¸ Ι™eñ¶‹w¨wÆ.6ë»(}¹{.þ´kÛšìÛ³ýÝz£³¹åoXÁÁ\:ìËÛ/‚«Óv×·.ŒòÛ¦ÅÝïßv¶K¯¿ðí Û®eÔ8¢?:r­gî¸ðy±Hêf5£¢jͲQë$Ìý‘ªÌ _¨YÑߺßÏB1h}¢=|@_â~Ñ ÚVÎÝtle•oìÂÉŽNg9ÿíëÓ“Æ̲yã@{hOO{(OMyÚ´¤Ü¿ÚÓå |€îuÅâb’­{Ÿú­™G{ÕÛŠ¯Þ¦Ïì.í]ÍõÕ{®ÌNDua"íïHû”ÿª„ßþDÞS(;¿ Ù=;XËjóm>Qal`|~yéB庛 À*žZ%rO+IoJ"º–D²¥ñã* …öÈ.ð"í3rÐC@’DJù>-6½Ì×Ê–âmqx>4=Ö W “%m‹ +€ê´8´k ¡9h,EDÍCÚWºGƒ84,‡žªA&ñäèÊ80ÂS"ðg“$|1 bZÿ 1-€¹•&åÐÿK‚À3¸y¾VzaDh1v)ã…žòãÈÓ´ë‚,„NM:cÄÍ ðÔ·ôg¯ endstream endobj 6 0 obj [5 0 R] endobj 30 0 obj <> endobj xref 0 31 0000000000 65535 f +0000000016 00000 n +0000000144 00000 n +0000019946 00000 n +0000000000 00000 f +0000023203 00000 n +0000618518 00000 n +0000019997 00000 n +0000020402 00000 n +0000026185 00000 n +0000023502 00000 n +0000023389 00000 n +0000022276 00000 n +0000022642 00000 n +0000022690 00000 n +0000023273 00000 n +0000023304 00000 n +0000023537 00000 n +0000026258 00000 n +0000026610 00000 n +0000028084 00000 n +0000033848 00000 n +0000093814 00000 n +0000159402 00000 n +0000224990 00000 n +0000290578 00000 n +0000356166 00000 n +0000421754 00000 n +0000487342 00000 n +0000552930 00000 n +0000618541 00000 n +trailer <<9479A8A1D6094FEA9CB836067FE2BC7D>]>> startxref 618741 %%EOF \ No newline at end of file diff --git a/presto-docs/src/main/sphinx/admin.rst b/presto-docs/src/main/sphinx/admin.rst new file mode 100644 index 00000000..650933da --- /dev/null +++ b/presto-docs/src/main/sphinx/admin.rst @@ -0,0 +1,8 @@ +************** +Administration +************** + +.. toctree:: + :maxdepth: 1 + + admin/queue \ No newline at end of file diff --git a/presto-docs/src/main/sphinx/admin/queue.rst b/presto-docs/src/main/sphinx/admin/queue.rst new file mode 100644 index 00000000..285f8234 --- /dev/null +++ b/presto-docs/src/main/sphinx/admin/queue.rst @@ -0,0 +1,105 @@ +=================== +Queue Configuration +=================== + +The queueing rules are defined in a JSON file and control the number of queries +that can be submitted to Presto and the quota of running queries per queue. +The filename of the JSON config file should be specified in ``query.queue-config-file`` +config property. + +Rules that specify multiple queues will cause the query to enter the queues sequentially. +Rules are processed sequentially and the first one that matches will be used. +In the example configuration below, there are five queue templates. In the +``user.${USER}`` queue, ``${USER}`` will be expanded to the name of the user +that submitted the query. ``${SOURCE}`` is also supported, which expands to the +source submitting the query. + +There are also five rules that define which queries go into which queues: + +* The first rule makes ``bob`` an admin. + +* The second rule states that all queries submitted with the ``experimental_big_query`` + session property and that come from a source that includes ``pipeline`` should + first be queued in the user's personal queue, then the ``pipeline`` queue, and + finally the ``big`` queue. When a query enters a new queue, it doesn't leave + previous queues until the query finishes execution. + +* The third rule is the same as the previous, but without the ``experimental_big_query`` + requirement, and uses the ``global`` queue instead of the ``big`` queue. + +* The last two rules are the same as the previous two, but without the + match on ``pipeline`` in the source. + +All together these rules implement the policy that ``bob`` is an admin and +all other users are subject to the follow limits: + + * Users are allowed to have up to 5 queries running. + + * ``big`` queries may only run one at a time. + + * No more than 10 ``pipeline`` queries may run at once. + + * No more than 100 non-``big`` queries may run at once. + +.. code-block:: json + + { + "queues": { + "user.${USER}": { + "maxConcurrent": 5, + "maxQueued": 20 + }, + "pipeline": { + "maxConcurrent": 10, + "maxQueued": 100 + }, + "admin": { + "maxConcurrent": 100, + "maxQueued": 100 + }, + "global": { + "maxConcurrent": 100, + "maxQueued": 1000 + }, + "big": { + "maxConcurrent": 1, + "maxQueued": 10 + } + }, + "rules": [ + { + "user": "bob", + "queues": ["admin"] + }, + { + "session.experimental_big_query": "true", + "source": ".*pipeline.*", + "queues": [ + "user.${USER}", + "pipeline", + "big" + ] + }, + { + "source": ".*pipeline.*", + "queues": [ + "user.${USER}", + "pipeline", + "global" + ] + }, + { + "session.experimental_big_query": "true", + "queues": [ + "user.${USER}", + "big" + ] + }, + { + "queues": [ + "user.${USER}", + "global" + ] + } + ] + } diff --git a/presto-docs/src/main/sphinx/conf.py b/presto-docs/src/main/sphinx/conf.py new file mode 100644 index 00000000..1bc99f8c --- /dev/null +++ b/presto-docs/src/main/sphinx/conf.py @@ -0,0 +1,97 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# +# Presto documentation build configuration file +# +# This file is execfile()d with the current directory set to its containing dir. +# + +import os +import sys +import xml.dom.minidom + +try: + sys.dont_write_bytecode = True +except: + pass + +sys.path.insert(0, os.path.abspath('ext')) + + +def child_node(node, name): + for i in node.childNodes: + if (i.nodeType == i.ELEMENT_NODE) and (i.tagName == name): + return i + return None + + +def node_text(node): + return node.childNodes[0].data + + +def maven_version(pom): + dom = xml.dom.minidom.parse(pom) + project = dom.childNodes[0] + + version = child_node(project, 'version') + if version: + return node_text(version) + + parent = child_node(project, 'parent') + version = child_node(parent, 'version') + return node_text(version) + + +def get_version(): + version = os.environ.get('PRESTO_VERSION', '').strip() + return version or maven_version('../../../pom.xml') + +# -- General configuration ----------------------------------------------------- + +needs_sphinx = '1.0' + +extensions = ['download'] + +templates_path = ['_templates'] + +source_suffix = '.rst' + +master_doc = 'index' + +project = u'Presto' + +version = get_version() +release = version + +exclude_patterns = ['_build', 'rest*', 'overview/concepts*'] + +pygments_style = 'sphinx' + +highlight_language = 'sql' + +rst_epilog = """ +.. |presto_server_release| replace:: ``presto-server-{release}`` +""".replace('{release}', release) + +# -- Options for HTML output --------------------------------------------------- + +html_theme_path = ['./themes'] +html_theme = 'presto' + +html_title = '%s %s Documentation' % (project, release) + +html_add_permalinks = None +html_show_copyright = False +html_show_sphinx = False diff --git a/presto-docs/src/main/sphinx/connector.rst b/presto-docs/src/main/sphinx/connector.rst new file mode 100644 index 00000000..0c89f3e4 --- /dev/null +++ b/presto-docs/src/main/sphinx/connector.rst @@ -0,0 +1,19 @@ +********** +Connectors +********** + +This chapter describes the connectors available in Presto to access data +from different data sources. + +.. toctree:: + :maxdepth: 1 + + connector/cassandra + connector/hive + connector/jmx + connector/kafka + connector/kafka-tutorial + connector/mysql + connector/postgresql + connector/system + connector/tpch diff --git a/presto-docs/src/main/sphinx/connector/cassandra.rst b/presto-docs/src/main/sphinx/connector/cassandra.rst new file mode 100644 index 00000000..381e4fc9 --- /dev/null +++ b/presto-docs/src/main/sphinx/connector/cassandra.rst @@ -0,0 +1,172 @@ +=================== +Cassandra Connector +=================== + +The Cassandra connector allows querying data stored in Cassandra. + +Configuration +------------- + +To configure the Cassandra connector, create a catalog properties file +``etc/catalog/cassandra.properties`` with the following contents, +replacing ``host1,host2`` with a comma-separated list of the Cassandra +nodes used to discovery the cluster topology: + +.. code-block:: none + + connector.name=cassandra + cassandra.contact-points=host1,host2 + +You will also need to set ``cassandra.native-protocol-port`` if your +Cassandra nodes are not using the default port (9042). + +Multiple Cassandra Clusters +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +You can have as many catalogs as you need, so if you have additional +Cassandra clusters, simply add another properties file to ``etc/catalog`` +with a different name (making sure it ends in ``.properties``). For +example, if you name the property file ``sales.properties``, Presto +will create a catalog named ``sales`` using the configured connector. + +Configuration Properties +------------------------ + +The following configuration properties are available: + +================================================== ====================================================================== +Property Name Description +================================================== ====================================================================== +``cassandra.contact-points`` Comma-separated list of hosts in a Cassandra cluster. The Cassandra + driver will use these contact points to discover cluster topology. + At least one Cassandra host is required. + +``cassandra.native-protocol-port`` The Cassandra server port running the native client protocol + (defaults to ``9042``). + +``cassandra.thrift-port`` The Cassandra server port running the Thrift client protocol + (defaults to ``9160``). + +``cassandra.limit-for-partition-key-select`` Limit of rows to read for finding all partition keys. If a + Cassandra table has more rows than this value, splits based on + token ranges are used instead. Note that for larger values you + may need to adjust read timeout for Cassandra. + +``cassandra.max-schema-refresh-threads`` Maximum number of schema cache refresh threads. This property + corresponds to the maximum number of parallel requests. + +``cassandra.schema-cache-ttl`` Maximum time that information about a schema will be cached + (defaults to ``1h``). + +``cassandra.schema-refresh-interval`` The schema information cache will be refreshed in the background + when accessed if the cached data is at least this old + (defaults to ``2m``). + +``cassandra.consistency-level`` Consistency levels in Cassandra refer to the level of consistency + to be used for both read and write operations. More information + about consistency levels can be found in the + `Cassandra consistency`_ documentation. This property defaults to + a consistency level of ``ONE``. Possible values include ``ALL``, + ``EACH_QUORUM``, ``QUORUM``, ``LOCAL_QUORUM``, ``ONE``, ``TWO``, + ``THREE``, ``LOCAL_ONE``, ``ANY``, ``SERIAL``, ``LOCAL_SERIAL``. + +``cassandra.allow-drop-table`` Set to ``true`` to allow dropping Cassandra tables from Presto + via :doc:`/sql/drop-table` (defaults to ``false``). + +``cassandra.username`` Username used for authentication to the Cassandra cluster. + This is a global setting used for all connections, regardless + of the user who is connected to Presto. + +``cassandra.password`` Password used for authentication to the Cassandra cluster. + This is a global setting used for all connections, regardless + of the user who is connected to Presto. +================================================== ====================================================================== + +.. _Cassandra consistency: http://www.datastax.com/documentation/cassandra/2.0/cassandra/dml/dml_config_consistency_c.html + +The following advanced configuration properties are available: + +================================================== ====================================================================== +Property Name Description +================================================== ====================================================================== +``cassandra.fetch-size`` Number of rows fetched at a time in a Cassandra query. + +``cassandra.fetch-size-for-partition-key-select`` Number of rows fetched at a time in a Cassandra query that + selects partition keys. + +``cassandra.partition-size-for-batch-select`` Number of partitions batched together into a single select for a + single partion key column table. + +``cassandra.split-size`` Number of keys per split when querying Cassandra. + +``cassandra.partitioner`` Partitioner to use for hashing and data distribution. This + property defaults to ``Murmur3Partitioner``. The other supported + values are ``RandomPartitioner`` and ``ByteOrderedPartitioner``. + +``cassandra.thrift-connection-factory-class`` Allows for the specification of a custom implementation of + ``org.apache.cassandra.thrift.ITransportFactory`` to be used to + connect to Cassandra using the Thrift protocol. + +``cassandra.transport-factory-options`` Allows for the specification of arbitrary options to be passed to + the Thrift connection factory. + +``cassandra.client.read-timeout`` Maximum time the Cassandra driver will wait for an + answer to a query from one Cassandra node. Note that the underlying + Cassandra driver may retry a query against more than one node in + the event of a read timeout. Increasing this may help with queries + that use an index. + +``cassandra.client.connect-timeout`` Maximum time the Cassandra driver will wait to establish + a connection to a Cassandra node. Increasing this may help with + heavily loaded Cassandra clusters. + +``cassandra.client.so-linger`` Number of seconds to linger on close if unsent data is queued. + If set to zero, the socket will be closed immediately. + When this option is non-zero, a socket will linger that many + seconds for an acknowledgement that all data was written to a + peer. This option can be used to avoid consuming sockets on a + Cassandra server by immediately closing connections when they + are no longer needed. + +``cassandra.retry-policy`` Policy used to retry failed requests to Cassandra. This property + defaults to ``DEFAULT``. Using ``BACKOFF`` may help when + queries fail with *"not enough replicas"*. The other possible + values are ``DOWNGRADING_CONSISTENCY`` and ``FALLTHROUGH``. +================================================== ====================================================================== + +Querying Cassandra Tables +------------------------- + +The ``users`` table is an example Cassandra table from the Cassandra +`Getting Started`_ guide. It can be created along with the ``mykeyspace`` +keyspace using Cassandra's cqlsh (CQL interactive terminal): + +.. _Getting Started: https://wiki.apache.org/cassandra/GettingStarted + +.. code-block:: none + + cqlsh> CREATE KEYSPACE mykeyspace + ... WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 }; + cqlsh> USE mykeyspace; + cqlsh:mykeyspace> CREATE TABLE users ( + ... user_id int PRIMARY KEY, + ... fname text, + ... lname text + ... ); + +This table can be described in Presto:: + + DESCRIBE cassandra.mykeyspace.users; + +.. code-block:: none + + Column | Type | Null | Partition Key | Comment + ---------+---------+------+---------------+--------- + user_id | bigint | true | true | + fname | varchar | true | false | + lname | varchar | true | false | + (3 rows) + +This table can then be queried in Presto:: + + SELECT * FROM cassandra.mykeyspace.users; diff --git a/presto-docs/src/main/sphinx/connector/hive.rst b/presto-docs/src/main/sphinx/connector/hive.rst new file mode 100644 index 00000000..0587e6bf --- /dev/null +++ b/presto-docs/src/main/sphinx/connector/hive.rst @@ -0,0 +1,134 @@ +============== +Hive Connector +============== + +The Hive connector allows querying data stored in a Hive +data warehouse. Hive is a combination of three components: + +* Data files in varying formats that are typically stored in the + Hadoop Distributed File System (HDFS) or in Amazon S3. +* Metadata about how the data files are mapped to schemas and tables. + This metadata is stored in a database such as MySQL and is accessed + via the Hive metastore service. +* A query language called HiveQL. This query language is executed + on a distributed computing framework such as MapReduce or Tez. + +Presto only uses the first two components: the data and the metadata. +It does not use HiveQL or any part of Hive's execution environment. + +Configuration +------------- + +Presto includes Hive connectors for multiple versions of Hadoop: + +* ``hive-hadoop1``: Apache Hadoop 1.x +* ``hive-hadoop2``: Apache Hadoop 2.x +* ``hive-cdh4``: Cloudera CDH 4 +* ``hive-cdh5``: Cloudera CDH 5 + +Create ``etc/catalog/hive.properties`` with the following contents +to mount the ``hive-cdh4`` connector as the ``hive`` catalog, +replacing ``hive-cdh4`` with the proper connector for your version +of Hadoop and ``example.net:9083`` with the correct host and port +for your Hive metastore Thrift service: + +.. code-block:: none + + connector.name=hive-cdh4 + hive.metastore.uri=thrift://example.net:9083 + +Multiple Hive Clusters +^^^^^^^^^^^^^^^^^^^^^^ + +You can have as many catalogs as you need, so if you have additional +Hive clusters, simply add another properties file to ``etc/catalog`` +with a different name (making sure it ends in ``.properties``). For +example, if you name the property file ``sales.properties``, Presto +will create a catalog named ``sales`` using the configured connector. +If you are connecting to more than one Hive metastore, you can create +any number of properties files configuring multiple instances of +the Hive connector. + +HDFS Configuration +^^^^^^^^^^^^^^^^^^ + +Presto configures the HDFS client automatically for most setups and +does not require any configuration files. In some rare cases, such +as when using federated HDFS, it may be necessary to specify additional +HDFS client options in order to access your HDFS cluster. To do so, add +the ``hive.config.resources`` property to reference your HDFS config files: + +.. code-block:: none + + hive.config.resources=/etc/hadoop/conf/core-site.xml,/etc/hadoop/conf/hdfs-site.xml + +Only specify additional configuration files if absolutely necessary. +We also recommend reducing the configuration files to have the minimum +set of required properties, as additional properties may cause problems. + +Configuration Properties +------------------------ + +================================================== ============================================================ ========== +Property Name Description Example +================================================== ============================================================ ========== +``hive.metastore.uri`` The URI of the Hive Metastore to connect to using ``thrift://192.0.2.3:9083`` + the Thrift protocol. This property is required. + +``hive.config.resources`` An optional comma-separated list of HDFS ``/etc/hdfs-site.xml`` + configuration files. These files must exist on the + machines running Presto. Only specify this if + absolutely necessary to access HDFS. + +``hive.storage-format`` The default file format used when creating new tables ``RCBINARY`` + +``hive.force-local-scheduling`` Force splits to be scheduled on the same node as the Hadoop ``true`` + DataNode process serving the split data. This is useful for + installations where Presto is collocated with every + DataNode. +================================================== ============================================================ ========== + +Querying Hive Tables +-------------------- + +The following table is an example Hive table from the `Hive Tutorial`_. +It can be created in Hive (not in Presto) using the following +Hive ``CREATE TABLE`` command: + +.. _Hive Tutorial: https://cwiki.apache.org/confluence/display/Hive/Tutorial#Tutorial-UsageandExamples + +.. code-block:: none + + hive> CREATE TABLE page_view ( + > viewTime INT, + > userid BIGINT, + > page_url STRING, + > referrer_url STRING, + > ip STRING COMMENT 'IP Address of the User') + > COMMENT 'This is the page view table' + > PARTITIONED BY (dt STRING, country STRING) + > STORED AS SEQUENCEFILE; + OK + Time taken: 3.644 seconds + +Assuming that this table was created in the ``web`` schema in +Hive, this table can be described in Presto:: + + DESCRIBE hive.web.page_view; + +.. code-block:: none + + Column | Type | Null | Partition Key | Comment + --------------+---------+------+---------------+------------------------ + viewtime | bigint | true | false | + userid | bigint | true | false | + page_url | varchar | true | false | + referrer_url | varchar | true | false | + ip | varchar | true | false | IP Address of the User + dt | varchar | true | true | + country | varchar | true | true | + (7 rows) + +This table can then be queried in Presto:: + + SELECT * FROM hive.web.page_view; diff --git a/presto-docs/src/main/sphinx/connector/jmx.rst b/presto-docs/src/main/sphinx/connector/jmx.rst new file mode 100644 index 00000000..0a10feea --- /dev/null +++ b/presto-docs/src/main/sphinx/connector/jmx.rst @@ -0,0 +1,55 @@ +============= +JMX Connector +============= + +The JMX connector provides the ability to query JMX information from all +nodes in a Presto cluster. This is very useful for monitoring or debugging. +Java Management Extensions (JMX) provides information about the Java +Virtual Machine and all of the software running inside it. Presto itself +is heavily instrumented via JMX. + +Configuration +------------- + +To configure the JMX connector, create a catalog properties file +``etc/catalog/jmx.properties`` with the following contents: + +.. code-block:: none + + connector.name=jmx + +Querying JMX +------------ + +The JMX connector provides a single schema ``jmx`` that contains +every Managed Bean (MBean) from every node in the Presto cluster. +You can see all of the available MBeans by running ``SHOW TABLES``:: + + SHOW TABLES FROM jmx.jmx; + +MBean names map to non-standard table names and must be quoted with +double quotes when referencing them in a query. For example, the +following query shows the JVM version of every node:: + + SELECT node, vmname, vmversion + FROM jmx.jmx."java.lang:type=runtime"; + +.. code-block:: none + + node | vmname | vmversion + --------------------------------------+-----------------------------------+----------- + ddc4df17-0b8e-4843-bb14-1b8af1a7451a | Java HotSpot(TM) 64-Bit Server VM | 24.60-b09 + (1 row) + +The following query shows the open and maximum file descriptor counts +for each node:: + + SELECT openfiledescriptorcount, maxfiledescriptorcount + FROM jmx.jmx."java.lang:type=operatingsystem"; + +.. code-block:: none + + openfiledescriptorcount | maxfiledescriptorcount + -------------------------+------------------------ + 329 | 10240 + (1 row) diff --git a/presto-docs/src/main/sphinx/connector/kafka-tutorial.rst b/presto-docs/src/main/sphinx/connector/kafka-tutorial.rst new file mode 100644 index 00000000..ed38089c --- /dev/null +++ b/presto-docs/src/main/sphinx/connector/kafka-tutorial.rst @@ -0,0 +1,613 @@ +======================== +Kafka Connector Tutorial +======================== + +Introduction +============ + +The Kafka Connector for Presto allows access to live topic data from +Apache Kafka using Presto. This tutorial shows how to set up topics and +how to create the topic description files that back Presto tables. + +Installation +============ + +This tutorial assumes familiarity with Presto and a working local Presto +installation (see :doc:`/installation/deployment`). It will focus on +setting up Apache Kafka and integrating it with Presto. + +Step 1: Install Apache Kafka +---------------------------- + +Download and extract `Apache Kafka `_. + +.. note:: + + This tutorial was tested with Apache Kafka 0.8.1. + It should work with any 0.8.x version of Apache Kafka. + +Start ZooKeeper and the Kafka server: + +.. code-block:: none + + $ bin/zookeeper-server-start.sh config/zookeeper.properties + [2013-04-22 15:01:37,495] INFO Reading configuration from: config/zookeeper.properties (org.apache.zookeeper.server.quorum.QuorumPeerConfig) + ... + +.. code-block:: none + + $ bin/kafka-server-start.sh config/server.properties + [2013-04-22 15:01:47,028] INFO Verifying properties (kafka.utils.VerifiableProperties) + [2013-04-22 15:01:47,051] INFO Property socket.send.buffer.bytes is overridden to 1048576 (kafka.utils.VerifiableProperties) + ... + +This will start Zookeeper on port ``2181`` and Kafka on port ``9092``. + +Step 2: Load data +----------------- + +Download the tpch-kafka loader from Maven central: + +.. code-block:: none + + $ curl -o kafka-tpch https://repo1.maven.org/maven2/de/softwareforge/kafka_tpch_0811/1.0/kafka_tpch_0811-1.0.sh + $ chmod 755 kafka-tpch + +Now run the ``kafka-tpch`` program to preload a number of topics with tpch data: + +.. code-block:: none + + $ ./kafka-tpch load --brokers localhost:9092 --prefix tpch. --tpch-type tiny + 2014-07-28T17:17:07.594-0700 INFO main io.airlift.log.Logging Logging to stderr + 2014-07-28T17:17:07.623-0700 INFO main de.softwareforge.kafka.LoadCommand Processing tables: [customer, orders, lineitem, part, partsupp, supplier, nation, region] + 2014-07-28T17:17:07.981-0700 INFO pool-1-thread-1 de.softwareforge.kafka.LoadCommand Loading table 'customer' into topic 'tpch.customer'... + 2014-07-28T17:17:07.981-0700 INFO pool-1-thread-2 de.softwareforge.kafka.LoadCommand Loading table 'orders' into topic 'tpch.orders'... + 2014-07-28T17:17:07.981-0700 INFO pool-1-thread-3 de.softwareforge.kafka.LoadCommand Loading table 'lineitem' into topic 'tpch.lineitem'... + 2014-07-28T17:17:07.982-0700 INFO pool-1-thread-4 de.softwareforge.kafka.LoadCommand Loading table 'part' into topic 'tpch.part'... + 2014-07-28T17:17:07.982-0700 INFO pool-1-thread-5 de.softwareforge.kafka.LoadCommand Loading table 'partsupp' into topic 'tpch.partsupp'... + 2014-07-28T17:17:07.982-0700 INFO pool-1-thread-6 de.softwareforge.kafka.LoadCommand Loading table 'supplier' into topic 'tpch.supplier'... + 2014-07-28T17:17:07.982-0700 INFO pool-1-thread-7 de.softwareforge.kafka.LoadCommand Loading table 'nation' into topic 'tpch.nation'... + 2014-07-28T17:17:07.982-0700 INFO pool-1-thread-8 de.softwareforge.kafka.LoadCommand Loading table 'region' into topic 'tpch.region'... + 2014-07-28T17:17:10.612-0700 ERROR pool-1-thread-8 kafka.producer.async.DefaultEventHandler Failed to collate messages by topic, partition due to: Failed to fetch topic metadata for topic: tpch.region + 2014-07-28T17:17:10.781-0700 INFO pool-1-thread-8 de.softwareforge.kafka.LoadCommand Generated 5 rows for table 'region'. + 2014-07-28T17:17:10.797-0700 ERROR pool-1-thread-3 kafka.producer.async.DefaultEventHandler Failed to collate messages by topic, partition due to: Failed to fetch topic metadata for topic: tpch.lineitem + 2014-07-28T17:17:10.932-0700 ERROR pool-1-thread-1 kafka.producer.async.DefaultEventHandler Failed to collate messages by topic, partition due to: Failed to fetch topic metadata for topic: tpch.customer + 2014-07-28T17:17:11.068-0700 ERROR pool-1-thread-2 kafka.producer.async.DefaultEventHandler Failed to collate messages by topic, partition due to: Failed to fetch topic metadata for topic: tpch.orders + 2014-07-28T17:17:11.200-0700 ERROR pool-1-thread-6 kafka.producer.async.DefaultEventHandler Failed to collate messages by topic, partition due to: Failed to fetch topic metadata for topic: tpch.supplier + 2014-07-28T17:17:11.319-0700 INFO pool-1-thread-6 de.softwareforge.kafka.LoadCommand Generated 100 rows for table 'supplier'. + 2014-07-28T17:17:11.333-0700 ERROR pool-1-thread-4 kafka.producer.async.DefaultEventHandler Failed to collate messages by topic, partition due to: Failed to fetch topic metadata for topic: tpch.part + 2014-07-28T17:17:11.466-0700 ERROR pool-1-thread-5 kafka.producer.async.DefaultEventHandler Failed to collate messages by topic, partition due to: Failed to fetch topic metadata for topic: tpch.partsupp + 2014-07-28T17:17:11.597-0700 ERROR pool-1-thread-7 kafka.producer.async.DefaultEventHandler Failed to collate messages by topic, partition due to: Failed to fetch topic metadata for topic: tpch.nation + 2014-07-28T17:17:11.706-0700 INFO pool-1-thread-7 de.softwareforge.kafka.LoadCommand Generated 25 rows for table 'nation'. + 2014-07-28T17:17:12.180-0700 INFO pool-1-thread-1 de.softwareforge.kafka.LoadCommand Generated 1500 rows for table 'customer'. + 2014-07-28T17:17:12.251-0700 INFO pool-1-thread-4 de.softwareforge.kafka.LoadCommand Generated 2000 rows for table 'part'. + 2014-07-28T17:17:12.905-0700 INFO pool-1-thread-2 de.softwareforge.kafka.LoadCommand Generated 15000 rows for table 'orders'. + 2014-07-28T17:17:12.919-0700 INFO pool-1-thread-5 de.softwareforge.kafka.LoadCommand Generated 8000 rows for table 'partsupp'. + 2014-07-28T17:17:13.877-0700 INFO pool-1-thread-3 de.softwareforge.kafka.LoadCommand Generated 60175 rows for table 'lineitem'. + +Kafka now has a number of topics that are preloaded with data to query. + +Step 3: Make the Kafka topics known to Presto +--------------------------------------------- + +In your Presto installation, add a catalog properties file +``etc/catalog/kafka.properties`` for the Kafka connector. +This file lists the Kafka nodes and topics: + +.. code-block:: none + + connector.name=kafka + kafka.nodes=localhost:9092 + kafka.table-names=tpch.customer,tpch.orders,tpch.lineitem,tpch.part,tpch.partsupp,tpch.supplier,tpch.nation,tpch.region + kafka.hide-internal-columns=false + +Now start Presto: + +.. code-block:: none + + $ bin/launcher start + +Because the Kafka tables all have the ``tpch.`` prefix in the configuration, +the tables are in the ``tpch`` schema. The connector is mounted into the +``kafka`` catalog because the properties file is named ``kafka.properties``. + +Start the :doc:`Presto CLI `: + +.. code-block:: none + + $ ./presto --catalog kafka --schema tpch + +List the tables to verify that things are working: + +.. code-block:: none + + presto:tpch> SHOW TABLES; + Table + ---------- + customer + lineitem + nation + orders + part + partsupp + region + supplier + (8 rows) + +Step 4: Basic data querying +--------------------------- + +Kafka data is unstructured and it has no metadata to describe the format of +the messages. Without further configuration, the Kafka connector can access +the data and map it in raw form but there are no actual columns besides the +built-in ones: + +.. code-block:: none + + presto:tpch> DESCRIBE customer; + Column | Type | Null | Partition Key | Comment + -------------------+---------+------+---------------+--------------------------------------------- + _partition_id | bigint | true | false | Partition Id + _partition_offset | bigint | true | false | Offset for the message within the partition + _segment_start | bigint | true | false | Segment start offset + _segment_end | bigint | true | false | Segment end offset + _segment_count | bigint | true | false | Running message count per segment + _key | varchar | true | false | Key text + _key_corrupt | boolean | true | false | Key data is corrupt + _key_length | bigint | true | false | Total number of key bytes + _message | varchar | true | false | Message text + _message_corrupt | boolean | true | false | Message data is corrupt + _message_length | bigint | true | false | Total number of message bytes + (11 rows) + + presto:tpch> SELECT count(*) FROM customer; + _col0 + ------- + 1500 + + presto:tpch> SELECT _message FROM customer LIMIT 5; + _message + -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + {"rowNumber":1,"customerKey":1,"name":"Customer#000000001","address":"IVhzIApeRb ot,c,E","nationKey":15,"phone":"25-989-741-2988","accountBalance":711.56,"marketSegment":"BUILDING","comment":"to the even, regular platelets. regular, ironic epitaphs nag e"} + {"rowNumber":3,"customerKey":3,"name":"Customer#000000003","address":"MG9kdTD2WBHm","nationKey":1,"phone":"11-719-748-3364","accountBalance":7498.12,"marketSegment":"AUTOMOBILE","comment":" deposits eat slyly ironic, even instructions. express foxes detect slyly. blithel + {"rowNumber":5,"customerKey":5,"name":"Customer#000000005","address":"KvpyuHCplrB84WgAiGV6sYpZq7Tj","nationKey":3,"phone":"13-750-942-6364","accountBalance":794.47,"marketSegment":"HOUSEHOLD","comment":"n accounts will have to unwind. foxes cajole accor"} + {"rowNumber":7,"customerKey":7,"name":"Customer#000000007","address":"TcGe5gaZNgVePxU5kRrvXBfkasDTea","nationKey":18,"phone":"28-190-982-9759","accountBalance":9561.95,"marketSegment":"AUTOMOBILE","comment":"ainst the ironic, express theodolites. express, even pinto bean + {"rowNumber":9,"customerKey":9,"name":"Customer#000000009","address":"xKiAFTjUsCuxfeleNqefumTrjS","nationKey":8,"phone":"18-338-906-3675","accountBalance":8324.07,"marketSegment":"FURNITURE","comment":"r theodolites according to the requests wake thinly excuses: pending + (5 rows) + + presto:tpch> SELECT sum(cast(json_extract_scalar(_message, '$.accountBalance') AS double)) FROM customer LIMIT 10; + _col0 + ------------ + 6681865.59 + (1 row) + +The data from Kafka can be queried using Presto but it is not yet in +actual table shape. The raw data is available through the ``_message`` and +``_key`` columns but it is not decoded into columns. As the sample data is +in JSON format, the :doc:`/functions/json` built into Presto can be used +to slice the data. + +Step 5: Add a topic decription file +----------------------------------- + +The Kafka connector supports topic description files to turn raw data into +table format. These files are located in the ``etc/kafka`` folder in the +Presto installation and must end with ``.json``. It is recommended that +the file name matches the table name but this is not necessary. + +Add the following file as ``etc/kafka/tpch.customer.json`` and restart Presto: + +.. code-block:: json + + { + "tableName": "customer", + "schemaName": "tpch", + "topicName": "tpch.customer", + "key": { + "dataFormat": "raw", + "fields": [ + { + "name": "kafka_key", + "dataFormat": "LONG", + "type": "BIGINT", + "hidden": "false" + } + ] + } + } + +The customer table now has an additional column: ``kafka_key``. + +.. code-block:: none + + presto:tpch> DESCRIBE customer; + Column | Type | Null | Partition Key | Comment + -------------------+---------+------+---------------+--------------------------------------------- + kafka_key | bigint | true | false | + _partition_id | bigint | true | false | Partition Id + _partition_offset | bigint | true | false | Offset for the message within the partition + _segment_start | bigint | true | false | Segment start offset + _segment_end | bigint | true | false | Segment end offset + _segment_count | bigint | true | false | Running message count per segment + _key | varchar | true | false | Key text + _key_corrupt | boolean | true | false | Key data is corrupt + _key_length | bigint | true | false | Total number of key bytes + _message | varchar | true | false | Message text + _message_corrupt | boolean | true | false | Message data is corrupt + _message_length | bigint | true | false | Total number of message bytes + (12 rows) + + presto:tpch> SELECT kafka_key FROM customer ORDER BY kafka_key LIMIT 10; + kafka_key + ----------- + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + (10 rows) + +The topic definition file maps the internal Kafka key (which is a raw long +in eight bytes) onto a Presto ``BIGINT`` column. + +Step 6: Map all the values from the topic message onto columns +-------------------------------------------------------------- + +Update the ``etc/kafka/tpch.customer.json`` file to add fields for the +message and restart Presto. As the fields in the message are JSON, it uses +the ``json`` data format. This is an example where different data formats +are used for the key and the message. + +.. code-block:: json + + { + "tableName": "customer", + "schemaName": "tpch", + "topicName": "tpch.customer", + "key": { + "dataFormat": "raw", + "fields": [ + { + "name": "kafka_key", + "dataFormat": "LONG", + "type": "BIGINT", + "hidden": "false" + } + ] + }, + "message": { + "dataFormat": "json", + "fields": [ + { + "name": "row_number", + "mapping": "rowNumber", + "type": "BIGINT" + }, + { + "name": "customer_key", + "mapping": "customerKey", + "type": "BIGINT" + }, + { + "name": "name", + "mapping": "name", + "type": "VARCHAR" + }, + { + "name": "address", + "mapping": "address", + "type": "VARCHAR" + }, + { + "name": "nation_key", + "mapping": "nationKey", + "type": "BIGINT" + }, + { + "name": "phone", + "mapping": "phone", + "type": "VARCHAR" + }, + { + "name": "account_balance", + "mapping": "accountBalance", + "type": "DOUBLE" + }, + { + "name": "market_segment", + "mapping": "marketSegment", + "type": "VARCHAR" + }, + { + "name": "comment", + "mapping": "comment", + "type": "VARCHAR" + } + ] + } + } + +Now for all the fields in the JSON of the message, columns are defined and +the sum query from earlier can operate on the ``account_balance`` column directly: + +.. code-block:: none + + presto:tpch> DESCRIBE customer; + Column | Type | Null | Partition Key | Comment + -------------------+---------+------+---------------+--------------------------------------------- + kafka_key | bigint | true | false | + row_number | bigint | true | false | + customer_key | bigint | true | false | + name | varchar | true | false | + address | varchar | true | false | + nation_key | bigint | true | false | + phone | varchar | true | false | + account_balance | double | true | false | + market_segment | varchar | true | false | + comment | varchar | true | false | + _partition_id | bigint | true | false | Partition Id + _partition_offset | bigint | true | false | Offset for the message within the partition + _segment_start | bigint | true | false | Segment start offset + _segment_end | bigint | true | false | Segment end offset + _segment_count | bigint | true | false | Running message count per segment + _key | varchar | true | false | Key text + _key_corrupt | boolean | true | false | Key data is corrupt + _key_length | bigint | true | false | Total number of key bytes + _message | varchar | true | false | Message text + _message_corrupt | boolean | true | false | Message data is corrupt + _message_length | bigint | true | false | Total number of message bytes + (21 rows) + + presto:tpch> SELECT * FROM customer LIMIT 5; + kafka_key | row_number | customer_key | name | address | nation_key | phone | account_balance | market_segment | comment + -----------+------------+--------------+--------------------+---------------------------------------+------------+-----------------+-----------------+----------------+--------------------------------------------------------------------------------------------------------- + 1 | 2 | 2 | Customer#000000002 | XSTf4,NCwDVaWNe6tEgvwfmRchLXak | 13 | 23-768-687-3665 | 121.65 | AUTOMOBILE | l accounts. blithely ironic theodolites integrate boldly: caref + 3 | 4 | 4 | Customer#000000004 | XxVSJsLAGtn | 4 | 14-128-190-5944 | 2866.83 | MACHINERY | requests. final, regular ideas sleep final accou + 5 | 6 | 6 | Customer#000000006 | sKZz0CsnMD7mp4Xd0YrBvx,LREYKUWAh yVn | 20 | 30-114-968-4951 | 7638.57 | AUTOMOBILE | tions. even deposits boost according to the slyly bold packages. final accounts cajole requests. furious + 7 | 8 | 8 | Customer#000000008 | I0B10bB0AymmC, 0PrRYBCP1yGJ8xcBPmWhl5 | 17 | 27-147-574-9335 | 6819.74 | BUILDING | among the slyly regular theodolites kindle blithely courts. carefully even theodolites haggle slyly alon + 9 | 10 | 10 | Customer#000000010 | 6LrEaV6KR6PLVcgl2ArL Q3rqzLzcT1 v2 | 5 | 15-741-346-9870 | 2753.54 | HOUSEHOLD | es regular deposits haggle. fur + (5 rows) + + presto:tpch> SELECT sum(account_balance) FROM customer LIMIT 10; + _col0 + ------------ + 6681865.59 + (1 row) + +Now all the fields from the ``customer`` topic messages are available as +Presto table columns. + +Step 7: Use live data +--------------------- + +Presto can query live data in Kafka as it arrives. To simulate a live feed +of data, this tutorial sets up a feed of live tweets into Kafka. + +Setup a live Twitter feed +^^^^^^^^^^^^^^^^^^^^^^^^^ + +* Download the twistr tool + +.. code-block:: none + + $ curl -o twistr https://repo1.maven.org/maven2/de/softwareforge/twistr_kafka_0811/1.2/twistr_kafka_0811-1.2.sh + $ chmod 755 twistr + +* Create a developer account at https://dev.twitter.com/ and set up an + access and consumer token. + +* Create a ``twistr.properties`` file and put the access and consumer key + and secrets into it: + +.. code-block:: none + + twistr.access-token-key=... + twistr.access-token-secret=... + twistr.consumer-key=... + twistr.consumer-secret=... + twistr.kafka.brokers=localhost:9092 + +Create a tweets table on Presto +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Add the tweets table to the ``etc/catalog/kafka.properties`` file: + +.. code-block:: none + + connector.name=kafka + kafka.nodes=localhost:9092 + kafka.table-names=tpch.customer,tpch.orders,tpch.lineitem,tpch.part,tpch.partsupp,tpch.supplier,tpch.nation,tpch.region,tweets + kafka.hide-internal-columns=false + +Add a topic definition file for the Twitter feed as ``etc/kafka/tweets.json``: + +.. code-block:: json + + { + "tableName": "tweets", + "topicName": "twitter_feed", + "dataFormat": "json", + "key": { + "dataFormat": "raw", + "fields": [ + { + "name": "kafka_key", + "dataFormat": "LONG", + "type": "BIGINT", + "hidden": "false" + } + ] + }, + "message": { + "dataFormat":"json", + "fields": [ + { + "name": "text", + "mapping": "text", + "type": "VARCHAR" + }, + { + "name": "user_name", + "mapping": "user/screen_name", + "type": "VARCHAR" + }, + { + "name": "lang", + "mapping": "lang", + "type": "VARCHAR" + }, + { + "name": "created_at", + "mapping": "created_at", + "type": "TIMESTAMP", + "dataFormat": "rfc2822" + }, + { + "name": "favorite_count", + "mapping": "favorite_count", + "type": "BIGINT" + }, + { + "name": "retweet_count", + "mapping": "retweet_count", + "type": "BIGINT" + }, + { + "name": "favorited", + "mapping": "favorited", + "type": "BOOLEAN" + }, + { + "name": "id", + "mapping": "id_str", + "type": "VARCHAR" + }, + { + "name": "in_reply_to_screen_name", + "mapping": "in_reply_to_screen_name", + "type": "VARCHAR" + }, + { + "name": "place_name", + "mapping": "place/full_name", + "type": "VARCHAR" + } + ] + } + } + +As this table does not have an explicit schema name, it will be placed +into the ``default`` schema. + +Feed live data +^^^^^^^^^^^^^^ + +Start the twistr tool: + +.. code-block:: none + + $ java -Dness.config.location=file:$(pwd) -Dness.config=twistr -jar ./twistr + +``twistr`` connects to the Twitter API and feeds the "sample tweet" feed +into a Kafka topic called ``twitter_feed``. + +Now run queries against live data: + +.. code-block:: none + + $ ./presto-cli --catalog kafka --schema default + + presto:default> SELECT count(*) FROM tweets; + _col0 + ------- + 4467 + (1 row) + + presto:default> SELECT count(*) FROM tweets; + _col0 + ------- + 4517 + (1 row) + + presto:default> SELECT count(*) FROM tweets; + _col0 + ------- + 4572 + (1 row) + + presto:default> SELECT kafka_key, user_name, lang, created_at FROM tweets LIMIT 10; + kafka_key | user_name | lang | created_at + --------------------+-----------------+------+------------------------- + 494227746231685121 | burncaniff | en | 2014-07-29 14:07:31.000 + 494227746214535169 | gu8tn | ja | 2014-07-29 14:07:31.000 + 494227746219126785 | pequitamedicen | es | 2014-07-29 14:07:31.000 + 494227746201931777 | josnyS | ht | 2014-07-29 14:07:31.000 + 494227746219110401 | Cafe510 | en | 2014-07-29 14:07:31.000 + 494227746210332673 | Da_JuanAnd_Only | en | 2014-07-29 14:07:31.000 + 494227746193956865 | Smile_Kidrauhl6 | pt | 2014-07-29 14:07:31.000 + 494227750426017793 | CashforeverCD | en | 2014-07-29 14:07:32.000 + 494227750396653569 | FilmArsivimiz | tr | 2014-07-29 14:07:32.000 + 494227750388256769 | jmolas | es | 2014-07-29 14:07:32.000 + (10 rows) + +There is now a live feed into Kafka which can be queried using Presto. + +Epilogue: Time stamps +--------------------- + +The tweets feed that was set up in the last step contains a time stamp in +RFC 2822 format as ``created_at`` attribute in each tweet. + +.. code-block:: none + + presto:default> SELECT DISTINCT json_extract_scalar(_message, '$.created_at')) AS raw_date + -> FROM tweets LIMIT 5; + raw_date + -------------------------------- + Tue Jul 29 21:07:31 +0000 2014 + Tue Jul 29 21:07:32 +0000 2014 + Tue Jul 29 21:07:33 +0000 2014 + Tue Jul 29 21:07:34 +0000 2014 + Tue Jul 29 21:07:35 +0000 2014 + (5 rows) + +The topic definition file for the tweets table contains a mapping onto a +timestamp using the ``rfc2822`` converter: + +.. code-block:: json + + ... + { + "name": "created_at", + "mapping": "created_at", + "type": "TIMESTAMP", + "dataFormat": "rfc2822" + }, + ... + +This allows the raw data to be mapped onto a Presto timestamp column: + +.. code-block:: none + + presto:default> SELECT created_at, raw_date FROM ( + -> SELECT created_at, json_extract_scalar(_message, '$.created_at') AS raw_date + -> FROM tweets) + -> GROUP BY 1, 2 LIMIT 5; + created_at | raw_date + -------------------------+-------------------------------- + 2014-07-29 14:07:20.000 | Tue Jul 29 21:07:20 +0000 2014 + 2014-07-29 14:07:21.000 | Tue Jul 29 21:07:21 +0000 2014 + 2014-07-29 14:07:22.000 | Tue Jul 29 21:07:22 +0000 2014 + 2014-07-29 14:07:23.000 | Tue Jul 29 21:07:23 +0000 2014 + 2014-07-29 14:07:24.000 | Tue Jul 29 21:07:24 +0000 2014 + (5 rows) + +The Kafka connector contains converters for ISO 8601, RFC 2822 text +formats and for number-based timestamps using seconds or miilliseconds +since the epoch. There is also a generic, text-based formatter which uses +Joda-Time format strings to parse text columns. diff --git a/presto-docs/src/main/sphinx/connector/kafka.rst b/presto-docs/src/main/sphinx/connector/kafka.rst new file mode 100644 index 00000000..689ecb95 --- /dev/null +++ b/presto-docs/src/main/sphinx/connector/kafka.rst @@ -0,0 +1,387 @@ +=============== +Kafka Connector +=============== + +This connector allows the use of Apache Kafka topics as tables in Presto. +Each message is presented as a row in Presto. + +Topics can be live: rows will appear as data arrives and disappear as +segments get dropped. This can result in strange behavior if accessing the +same table multiple times in a single query (e.g. performing a self join). + +.. note:: + + Apache Kafka 0.8+ is supported although it is highly recommend to use 0.8.1 or later. + +Configuration +------------- + +To configure the Kafka connector, create a catalog properties file +``etc/catalog/kafka.properties`` with the following contents, +replacing the properties as appropriate: + +.. code-block:: none + + connector.name=kafka + kafka.table-names=table1,table2 + kafka.nodes=host1:port,host2:port + +Multiple Kafka Clusters +^^^^^^^^^^^^^^^^^^^^^^^ + +You can have as many catalogs as you need, so if you have additional +Kafka clusters, simply add another properties file to ``etc/catalog`` +with a different name (making sure it ends in ``.properties``). For +example, if you name the property file ``sales.properties``, Presto +will create a catalog named ``sales`` using the configured connector. + +Configuration Properties +------------------------ + +The following configuration properties are available: + +=============================== ============================================================== +Property Name Description +=============================== ============================================================== +``kafka.table-names`` List of all tables provided by the catalog +``kafka.default-schema`` Default schema name for tables +``kafka.nodes`` List of nodes in the Kafka cluster +``kafka.connect-timeout`` Timeout for connecting to the Kafka cluster +``kafka.buffer-size`` Kafka read buffer size +``kafka.table-description-dir`` Directory containing topic description files +``kafka.hide-internal-columns`` Controls whether internal columns are part of the table schema or not +=============================== ============================================================== + +``kafka.table-names`` +^^^^^^^^^^^^^^^^^^^^^ + +Comma-separated list of all tables provided by this catalog. A table name +can be unqualified (simple name) and will be put into the default schema +(see below) or qualified with a schema name (``.``). + +For each table defined here, a table description file (see below) may +exist. If no table description file exists, the table name is used as the +topic name on Kafka and no data columns are mapped into the table. The +table will still contain all internal columns (see below). + +This property is required; there is no default and at least one table must be defined. + +``kafka.default-schema`` +^^^^^^^^^^^^^^^^^^^^^^^^ + +Defines the schema which will contain all tables that were defined without +a qualifying schema name. + +This property is optional; the default is ``default``. + +``kafka.nodes`` +^^^^^^^^^^^^^^^ + +A comma separated list of ``hostname:port`` pairs for the Kafka data nodes. + +This property is required; there is no default and at least one node must be defined. + +.. note:: + + Presto must still be able to connect to all nodes of the cluster + even if only a subset is specified here as segment files may be + located only on a specifc node. + +``kafka.connect-timeout`` +^^^^^^^^^^^^^^^^^^^^^^^^^ + +Timeout for connecting to a data node. A busy Kafka cluster may take quite +some time before accepting a connection; when seeing failed queries due to +timeouts, increasing this value is a good strategy. + +This property is optional; the default is 10 seconds (``10s``). + +``kafka.buffer-size`` +^^^^^^^^^^^^^^^^^^^^^ + +Size of the internal data buffer for reading data from Kafka. The data +buffer must be able to hold at least one message and ideally can hold many +messages. There is one data buffer allocated per worker and data node. + +This property is optional; the default is ``64kb``. + +``kafka.table-description-dir`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +References a folder within Presto deployment that holds one or more JSON +files (must end with ``.json``) which contain table description files. + +This property is optional; the default is ``etc/kafka``. + +``kafka.hide-internal-columns`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In addition to the data columns defined in a table description file, the +connector maintains a number of additional columns for each table. If +these columns are hidden, they can still be used in queries but do not +show up in ``DESCRIBE `` or ``SELECT *``. + +This property is optional; the default is ``true``. + +Internal Columns +---------------- + +For each defined table, the connector maintains the following columns: + +======================= ========= ============================= +Column name Type Description +======================= ========= ============================= +``_partition_id`` BIGINT ID of the Kafka partition which contains this row. +``_partition_offset`` BIGINT Offset within the Kafka partition for this row. +``_segment_start`` BIGINT Lowest offset in the segment (inclusive) which contains this row. This offset is partition specific. +``_segment_end`` BIGINT Highest offset in the segment (exclusive) which contains this row. The offset is partition specific. This is the same value as ``_segment_start`` of the next segment (if it exists). +``_segment_count`` BIGINT Running count of for the current row within the segment. For an uncompacted topic, ``_segment_start + _segment_count`` is equal to ``_partition_offset``. +``_message_corrupt`` BOOLEAN True if the decoder could not decode the message for this row. When true, data columns mapped from the message should be treated as invalid. +``_message`` VARCHAR Message bytes as an UTF-8 encoded string. This is only useful for a text topic. +``_message_length`` BIGINT Number of bytes in the message. +``_key_corrupt`` BOOLEAN True if the key decode could not decode the key for this row. When true, data columns mapped from the key should be treated as invalid. +``_key`` VARCHAR Key bytes as an UTF-8 encoded string. This is only useful for textual keys. +``_key_length`` BIGINT Number of bytes in the key. +======================= ========= ============================= + +For tables without a table definition file, the ``_key_corrupt`` and +``_message_corrupt`` columns will always be ``false``. + +Table Definition Files +---------------------- + +Kafka maintains topics only as byte messages and leaves it to producers +and consumers to define how a message should be interpreted. For Presto, +this data must be mapped into columns to allow queries against the data. + +.. note:: + + For textual topics that contain JSON data, it is entirely possible to not + use any table definition files, but instead use the Presto + :doc:`/functions/json` to parse the ``_message`` column which contains + the bytes mapped into an UTF-8 string. This is, however, pretty + cumbersome and makes it difficult to write SQL queries. + +A table definition file consists of a JSON definition for a table. The +name of the file can be arbitrary but must end in ``.json``. + +.. code-block:: json + + { + "tableName": ..., + "schemaName": ..., + "topicName": ..., + "key": { + "dataFormat": ..., + "fields": [ + ... + ] + }, + "message": { + "dataFormat": ..., + "fields": [ + ... + ] + } + } + +=============== ========= ============== ============================= +Field Required Type Description +=============== ========= ============== ============================= +``tableName`` required string Presto table name defined by this file. +``schemaName`` optional string Schema which will contain the table. If omitted, the default schema name is used. +``topicName`` required string Kafka topic that is mapped. +``key`` optional JSON object Field definitions for data columns mapped to the message key. +``message`` optional JSON object Field definitions for data columns mapped to the message itself. +=============== ========= ============== ============================= + +Key and Message in Kafka +------------------------ + +Starting with Kafka 0.8, each Message in a topic can have an optional key. +A table definition file contains sections for both key and message to map +the data onto table columns. + +Each of the ``key`` and ``message`` fields in the table definition is a +JSON object that must contain two fields: + +=============== ========= ============== ============================= +Field Required Type Description +=============== ========= ============== ============================= +``dataFormat`` required string Selects the decoder for this group of fields. +``fields`` required JSON array A list of field definitions. Each field definition creates a new column in the Presto table. +=============== ========= ============== ============================= + +Each field definition is a JSON object: + +.. code-block:: json + + { + "name": ..., + "type": ..., + "dataFormat": ..., + "mapping": ..., + "formatHint": ..., + "hidden": ..., + "comment": ... + } + +=============== ========= ========= ============================= +Field Required Type Description +=============== ========= ========= ============================= +``name`` required string Name of the column in the Presto table. +``type`` required string Presto type of the column. +``dataFormat`` optional string Selects the column decoder for this field. Default to the default decoder for this row data format and column type. +``mapping`` optional string Mapping information for the column. This is decoder specific, see below. +``formatHint`` optional string Sets a column specifc format hint to the column decoder. +``hidden`` optional boolean Hides the column from ``DESCRIBE

`` and ``SELECT *``. Defaults to ``false``. +``comment`` optional string Add a column comment which is shown with ``DESCRIBE
``. +=============== ========= ========= ============================= + +There is no limit on field descriptions for either key or message. + +Row Decoding +------------ + +For key and message, a decoder is used to map data onto columns. If no +table definition file exists for a table, the ``dummy`` decoder is used. + +The Kafka connector contains the following decoders: + +* ``raw`` - do not convert the row data, use as raw bytes +* ``csv`` - interpret the value as CSV +* ``json`` - convert the value to a JSON object + +The main purpose of the decoders is to select the appropriate field +decoders to interpret the message or key data. + +Presto supports only four physical data types onto which the Presto types +are mapped: boolean, long, double and a sequence of bytes which is treated +as a string. + +``raw`` Decoder +^^^^^^^^^^^^^^^ + +The raw decoder supports reading of raw (byte based) values from a message +or key and converting it into Presto columns. + +For fields, the following attributes are supported: + +* ``type`` - All Presto data types are supported +* ``dataFormat`` - Only ``_default`` supported, can be omitted. +* ``mapping`` - selects the width of the data type converted +* ``formatHint`` - optional, ``[:]``; start and end position of bytes to convert + +The ``mapping`` column selects the number of bytes converted. +If absent, ``BYTE`` is assumed. All values are signed. + +Supported values are: + +* ``BYTE`` - one byte +* ``SHORT`` - two bytes +* ``INT`` - four bytes +* ``LONG`` - eight bytes +* ``FLOAT`` - four bytes (IEEE 754 format) +* ``DOUBLE`` - eight bytes (IEEE 754 format) + +The ``type`` column defines the Presto data type on which the value is mapped. + +* boolean based types require a mapping to ``BYTE``, ``SHORT``, ``INT`` or ``LONG``. + Any other type will throw a conversion error. + A value of ``0`` returns false, everything else true. +* long based types require a mapping to ``BYTE``, ``SHORT``, ``INT`` or ``LONG``. + Any other type will throw a conversion error. +* double based types require a mapping to ``FLOAT`` or ``DOUBLE``. + Any other type will throw a conversion error. +* string based types require a mapping to ``BYTE``. + Any other type will throw a conversion error. + +The ``formatHint`` field specifies the position of the bytes in a key or +message. It can be one or two numbers separated by a colon (``[:]``). +If only a start position is given, the column will use the appropriate +number of bytes for the type (see above). string based types (``VARCHAR``) +will use all bytes to the end of the message. If start and end position is +given, then for fixed with types the size must be at least the size of the +type. For string based types, all bytes between start (inclusive) and end +(exclusive) are used. + +``csv`` Decoder +^^^^^^^^^^^^^^^ + +.. note:: The CSV decoder is of beta quality and should be used with caution. + +The CSV decoder converts the bytes representing a message or key into a +string using UTF-8 encoding and then interprets the result as a CSV +(comma-separated value) line. + +For fields, the following attributes are supported: + +* ``type`` - All Presto data types are supported +* ``dataFormat`` - Only ``_default`` supported, can be omitted +* ``mapping`` - field index used for the column. Must be given +* ``formatHint`` - not supported, ignored + +* boolean based types return ``true`` if the field value is the string "true" (case insensitive), ``false`` otherwise. +* long and double based types parse the field value according to Java long and double parse rules. +* string types use the field as-is (text using UTF-8 encoding) + +``json`` Decoder +^^^^^^^^^^^^^^^^ + +The JSON decoder converts the bytes representing a message or key into a +JSON according to :rfc:`4627`. Note that the message or key *MUST* convert +into a JSON object, not an array or simple type. + +For fields, the following attributes are supported: + +* ``type`` - All Presto data types are supported +* ``dataFormat`` - ``_default``, ``custom-date-time``, ``iso8601``, ``rfc2822``, + ``milliseconds-since-epoch``, ``seconds-since-epoch``. If missing, ``_default`` is used. +* ``mapping`` - slash-separated list of fields names to select a field from the JSON object. +* ``formatHint`` - only for ``custom-date-time``, see below. + +The JSON decoder supports multiple field decoders, with ``_default`` being +used for standard table columns and a number of decoders for date and time +based types. + +``_default`` Field decoder +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This is the standard field decoder supporting all the Presto physical data +types. A field value will be coerced by JSON conversion rules into +boolean, long, double or string values. For non-date/time based columns, +this decoder should be used. + +Date and Time Decoders +^^^^^^^^^^^^^^^^^^^^^^ + +To convert values from JSON objects into Presto ``DATE``, ``TIME`` or +``TIMESTAMP`` columns, special decoders can be selected using the +``dataFormat`` attribute of a field definition. + +Text Decoders +""""""""""""" + +* ``iso8601`` - text based, parses a text field as an ISO 8601 timestamp. +* ``rfc2822`` - text based, parses a text field as an :rfc:`2822` timestamp. +* ``custom-date-time`` - text based, a formatting hint is required which is parsed as a Joda-Time formatting string. + +===================== ========================================================= ========================================================= +Presto Type JSON Text JSON Long +===================== ========================================================= ========================================================= +string type as-is parse according to format type, return millis since epoch +long-based type parse according to format type, return millis since epoch return as millis since epoch +===================== ========================================================= ========================================================= + +Number Decoders +""""""""""""""" + +* ``milliseconds-since-epoch`` - number based, interprets a text or number as number of milliseconds since the epoch. +* ``seconds-since-epoch`` - number based, interprets a text or number as number of milliseconds since the epoch. + +===================== ========================================================= ========================================================= +Presto Type JSON Text JSON Long +===================== ========================================================= ========================================================= +string type parse as long, format as ISO8601 format as ISO8601 +long-based type parse as long, return millis since epoch return millis since epoch +===================== ========================================================= ========================================================= diff --git a/presto-docs/src/main/sphinx/connector/mysql.rst b/presto-docs/src/main/sphinx/connector/mysql.rst new file mode 100644 index 00000000..796b517d --- /dev/null +++ b/presto-docs/src/main/sphinx/connector/mysql.rst @@ -0,0 +1,52 @@ +=============== +MySQL Connector +=============== + +The MySQL connector allows querying and creating tables in an external +MySQL database. This can be used to join data between different +systems like MySQL and Hive, or between two different MySQL instances. + +Configuration +------------- + +To configure the MySQL connector, create a catalog properties file +in ``etc/catalog`` named, for example, ``mysql.properties``, to +mount the MySQL connector as the ``mysql`` catalog. +Create the file with the following contents, replacing the +connection properties as appropriate for your setup: + +.. code-block:: none + + connector.name=mysql + connection-url=jdbc:mysql://example.net:3306 + connection-user=root + connection-password=secret + +Multiple MySQL Servers +^^^^^^^^^^^^^^^^^^^^^^ + +You can have as many catalogs as you need, so if you have additional +MySQL servers, simply add another properties file to ``etc/catalog`` +with a different name (making sure it ends in ``.properties``). For +example, if you name the property file ``sales.properties``, Presto +will create a catalog named ``sales`` using the configured connector. + +Querying MySQL +-------------- + +The MySQL connector provides a schema for every MySQL *database*. +You can see the available MySQL databases by running ``SHOW SCHEMAS``:: + + SHOW SCHEMAS FROM mysql; + +If you have a MySQL database named ``web``, you can view the tables +in this database by running ``SHOW TABLES``:: + + SHOW TABLES FROM mysql.web; + +Finally, you can access the ``clicks`` table in the ``web`` database:: + + SELECT * FROM mysql.web.clicks; + +If you used a different name for your catalog properties file, use +that catalog name instead of ``mysql`` in the above examples. diff --git a/presto-docs/src/main/sphinx/connector/postgresql.rst b/presto-docs/src/main/sphinx/connector/postgresql.rst new file mode 100644 index 00000000..d0f6f2c1 --- /dev/null +++ b/presto-docs/src/main/sphinx/connector/postgresql.rst @@ -0,0 +1,57 @@ +==================== +PostgreSQL Connector +==================== + +The PostgreSQL connector allows querying and creating tables in an +external PostgreSQL database. This can be used to join data between +different systems like PostgreSQL and Hive, or between two different +PostgreSQL instances. + +Configuration +------------- + +To configure the PostgreSQL connector, create a catalog properties file +in ``etc/catalog`` named, for example, ``postgresql.properties``, to +mount the PostgreSQL connector as the ``postgresql`` catalog. +Create the file with the following contents, replacing the +connection properties as appropriate for your setup: + +.. code-block:: none + + connector.name=postgresql + connection-url=jdbc:postgresql://example.net:5432/database + connection-user=root + connection-password=secret + +Multiple PostgreSQL Databases or Servers +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The PostgreSQL connector can only access a single database within +a PostgreSQL server. Thus, if you have multiple PostgreSQL databases, +or want to connect to multiple PostgreSQL servers, you must configure +multiple instances of the PostgreSQL connector. + +To add another catalog, simply add another properties file to ``etc/catalog`` +with a different name (making sure it ends in ``.properties``). For example, +if you name the property file ``sales.properties``, Presto will create a +catalog named ``sales`` using the configured connector. + +Querying PostgreSQL +------------------- + +The PostgreSQL connector provides a schema for every PostgreSQL schema. +You can see the available PostgreSQL schemas by running ``SHOW SCHEMAS``:: + + SHOW SCHEMAS FROM postgresql; + +If you have a PostgreSQL schema named ``web``, you can view the tables +in this schema by running ``SHOW TABLES``:: + + SHOW TABLES FROM postgresql.web; + +Finally, you can access the ``clicks`` table in the ``web`` schema:: + + SELECT * FROM postgresql.web.clicks; + +If you used a different name for your catalog properties file, use +that catalog name instead of ``postgresql`` in the above examples. diff --git a/presto-docs/src/main/sphinx/connector/system.rst b/presto-docs/src/main/sphinx/connector/system.rst new file mode 100644 index 00000000..c3d6d17a --- /dev/null +++ b/presto-docs/src/main/sphinx/connector/system.rst @@ -0,0 +1,57 @@ +================ +System Connector +================ + +The System connector provides information and metrics about the currently +running Presto cluster. It makes this available via normal SQL queries. + +Configuration +------------- + +The System connector doesn't need to be configured: it is automatically +available via a catalog named ``system``. + +Using the System Connector +-------------------------- + +List the available system schemas:: + + SHOW SCHEMAS FROM system; + +List the tables in one of the schemas:: + + SHOW TABLES FROM system.runtime; + +Query one of the tables:: + + SELECT * FROM system.runtime.nodes; + +System Connector Tables +----------------------- + +``metadata.catalog`` +^^^^^^^^^^^^^^^^^^^^ + +The catalogs table contains the list of available catalogs. + +``runtime.nodes`` +^^^^^^^^^^^^^^^^^ + +The nodes table contains the list of visible nodes in the Presto +cluster along with their status. + +``runtime.queries`` +^^^^^^^^^^^^^^^^^^^ + +The queries table contains information about currently and recently +running queries on the Presto cluster. From this table you can find out +the original query text (SQL), the identity of the user who ran the query +and performance information about the query including how long the query +was queued and analyzed. + +``runtime.tasks`` +^^^^^^^^^^^^^^^^^ + +The tasks table contains information about the tasks involved in a +Presto query including where they were executed and and how many rows +and bytes each task processed. diff --git a/presto-docs/src/main/sphinx/connector/tpch.rst b/presto-docs/src/main/sphinx/connector/tpch.rst new file mode 100644 index 00000000..b5e38691 --- /dev/null +++ b/presto-docs/src/main/sphinx/connector/tpch.rst @@ -0,0 +1,58 @@ +============== +TPCH Connector +============== + +The TPCH connector provides a set of schemas to support the TPC +Benchmark™ H (TPC-H). TPC-H is a database benchmark used to measure the +performance of highly-complex decision support databases. + +This connector can also be used to test the capabilities and query +syntax of Presto without configuring access to an external data +source. When you query a TPCH schema, the connector generates the +data on the fly using a deterministic algorithm. + +Configuration +------------- + +To configure the TPCH connector, create a catalog properties file +``etc/catalog/tpch.properties`` with the following contents: + +.. code-block:: none + + connector.name=tpch + +TPCH Schemas +------------ + +The TPCH connector supplies several schemas:: + + SHOW SCHEMAS FROM tpch; + +.. code-block:: none + + Schema + -------------------- + information_schema + sf1 + sf100 + sf1000 + sf10000 + sf100000 + sf300 + sf3000 + sf30000 + sys + tiny + (11 rows) + +Ignore the special ``information_schema`` and ``sys`` schemas which are +provided by Presto and exist in every catalog. + +Every TPCH schema provides the same set of tables. Some tables are +identical in all schemas. Other tables vary based on the *scale factor* +which is determined based on the schema name. For example, the schema +``sf1`` corresponds to scale factor ``1`` and the schema ``sf300`` +corresponds to scale factor ``300``. The TPCH connector provides an +infinite number of schemas for any scale factor, not just the few common +ones listed by ``SHOW SCHEMAS``. The ``tiny`` schema is an alias for scale +factor ``0.01``, which is a very small data set useful for testing. diff --git a/presto-docs/src/main/sphinx/develop.rst b/presto-docs/src/main/sphinx/develop.rst new file mode 100644 index 00000000..85575967 --- /dev/null +++ b/presto-docs/src/main/sphinx/develop.rst @@ -0,0 +1,13 @@ +*************** +Developer Guide +*************** + +This guide is intended for Presto contributors and plugin developers. + +.. toctree:: + :maxdepth: 1 + + develop/spi-overview + develop/example-http + develop/types + develop/functions diff --git a/presto-docs/src/main/sphinx/develop/example-http.rst b/presto-docs/src/main/sphinx/develop/example-http.rst new file mode 100644 index 00000000..703a582f --- /dev/null +++ b/presto-docs/src/main/sphinx/develop/example-http.rst @@ -0,0 +1,150 @@ +====================== +Example HTTP Connector +====================== + +The Example HTTP connector has a simple goal: it reads comma-separated +data over HTTP. For example, if you have a large amount of data in a +CSV format, you can point the example HTTP connector at this data and +write a SQL query to process it. + +Code +---- + +The Example HTTP connector can be found in the ``presto-example-http`` +directory in the root of the Presto source tree. + +Maven Project +------------- + +The Example HTTP connector uses Maven to build via the ``pom.xml`` +file in the root of the plugin directory. + +Project Dependencies +^^^^^^^^^^^^^^^^^^^^ + +Plugins depend on the SPI from Presto: + +.. code-block:: xml + + + com.facebook.presto + presto-spi + provided + + +The plugin uses the Maven ``provided`` scope because Presto provides +the classes from the SPI at runtime and thus the plugin should not +include them in the plugin assembly. + +There are a few other dependencies that are provided by Presto such +as ``javax.inject`` and Jackson. In particular, Jackson is used for +serializing handles and thus plugins must use the verison provided +by Presto. + +All other dependencies are based on what the plugin needs for its +own implementation. Plugins are loaded in a separate class loader +to provide isolation and to allow plugins to use a different version +of a library that Presto uses internally. + +Plugin Implementation +--------------------- + +The plugin implementation in the Example HTTP connector looks very +similar to other plugin implementations. Most of the implementation is +devoted to handling optional configuration and the only function of +interest is the following: + +.. code-block:: java + + @Override + public List getServices(Class type) + { + if (type == ConnectorFactory.class) { + return ImmutableList.of(type.cast(new ExampleConnectorFactory(getOptionalConfig()))); + } + return ImmutableList.of(); + } + +Note that the ``ImmutableList`` class is a utility class from Guava. + +As with all plugins, this plugin overrides the ``getServices()`` method +and returns an ``ExampleConnectorFactory`` in response to a request for a +service of type ``ConnectorFactory``. + +ConnectorFactory Implementation +------------------------------- + +In Presto, the primary object that handles the connection between +Presto and a particular type of data source is the ``Connector`` object, +which are created using ``ConnectorFactory``. + +This implementation is available in the class ``ExampleConnectorFactory``. +The first thing the connector factory implementation does is specify the +name of this connector. This is the same string used to reference this +connector in Presto configuration. + +.. code-block:: java + + @Override + public String getName() + { + return "example-http"; + } + +The real work in a connector factory happens in the ``create()`` +method. In the ``ExampleConnectorFactory`` class, the ``create()`` method +configures the connector and then asks Guice to create the object. +This is the meat of the ``create()`` method without parameter validation +and exception handling: + +.. code-block:: java + + // A plugin is not required to use Guice; it is just very convenient + Bootstrap app = new Bootstrap( + new JsonModule(), + new ExampleModule(connectorId)); + + Injector injector = app + .strictConfig() + .doNotInitializeLogging() + .setRequiredConfigurationProperties(requiredConfig) + .setOptionalConfigurationProperties(optionalConfig) + .initialize(); + + return injector.getInstance(ExampleConnector.class); + +Connector: ExampleConnector +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This class allows Presto to obtain references to the various services +provided by the connector. + +Metadata: ExampleMetadata +^^^^^^^^^^^^^^^^^^^^^^^^^ + +This class is responsible for reporting table names, table metadata, +column names, column metadata and other information about the schemas +that are provided by this connector. ``ConnectorMetadata`` is also called +by Presto to ensure that a particular connector can understand and +handle a given table name. + +The ``ExampleMetadata`` implementation delegates many of these calls to +``ExampleClient``, a class that implements much of the core functionality +of the connector. + +Split Manager: ExampleSplitManager +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The split manager partitions the data for a table into the individual +chunks that Presto will distribute to workers for processing. +In the case of the Example HTTP connector, each table contains one or +more URIs pointing at the actual data. One split is created per URI. + +Record Set Provider: ExampleRecordSetProvider +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The record set provider creates a record set which in turn creates a +record cursor that returns the actual data to Presto. +``ExampleRecordCursor`` reads data from a URI via HTTP. Each line +corresponds to a single row. Lines are split on comma into individual +field values which are then returned to Presto. diff --git a/presto-docs/src/main/sphinx/develop/functions.rst b/presto-docs/src/main/sphinx/develop/functions.rst new file mode 100644 index 00000000..41124f65 --- /dev/null +++ b/presto-docs/src/main/sphinx/develop/functions.rst @@ -0,0 +1,122 @@ +========= +Functions +========= + +The function framework is used to implement SQL functions. Presto includes a +number of built-in functions, and an internal ``Plugin`` (plugins that have a +dependency on presto-main) can provide new functions by returning a +``FunctionFactory`` from ``getServices()``. + +.. code-block:: java + + @ScalarFunction("is_null") + @Description("Returns TRUE if the argument is NULL") + @SqlType(StandardTypes.Boolean) + public static boolean isNull(@Nullable @SqlType(StandardTypes.VARCHAR) Slice string) + { + return (string == null); + } + +The above code implements a new function ``is_null`` which takes a single ``VARCHAR`` +argument, and returns a ``BOOLEAN`` indicating if the argument was ``NULL``. +Note that the argument to the function is of type ``Slice``. ``VARCHAR`` uses +``Slice``, which is essentially a wrapper around ``byte[]``, rather than ``String`` +for its native container type. + +* ``@SqlType``: + + The ``@SqlType`` annotation is used to declare the return type and the argument + types. Note, that the return type and arguments of the Java code, must match + the native container types of the corresponding annotations. + +* ``@Nullable``: + + The ``@Nullable`` annotation indicates that the argument may be ``NULL``. Without + this annotation the framework assumes that all functions return ``NULL`` if + any of their arguments are ``NULL``. When working with a ``Type`` that has a + primitive native container type, such as ``BigintType``, use the object wrapper for the + native container type when using ``@Nullable``. The method must be annotated with + ``@Nullable`` if it can return ``NULL`` when the arguments are non-null. + + +Aggregation Functions +--------------------- + +Aggregation functions use a similar framework to scalar functions, but involve +a bit more complexity. + +* ``AccumulatorState``: + + All aggregation functions accumulate input rows into a state object; this + object must implement ``AccumulatorState``. For simple aggregations, just + extend ``AccumulatorState`` into a new interface with the getters and setters + you want, and the framework will generate all the implementations and + serializers for you. If you need a more complex state object, you will need + to implement ``AccumulatorStateFactory`` and ``AccumulatorStateSerializer`` + and provide these via the ``AccumulatorStateMetadata`` annotation. + +.. code-block:: java + + @AggregationFunction("avg") + public final class AverageAggregation + { + @InputFunction + public static void input(LongAndDoubleState state, @SqlType(StandardTypes.DOUBLE) double value) + { + state.setLong(state.getLong() + 1); + state.setDouble(state.getDouble() + value); + } + + @CombineFunction + public static void combine(LongAndDoubleState state, LongAndDoubleState otherState) + { + state.setLong(state.getLong() + otherState.getLong()); + state.setDouble(state.getDouble() + otherState.getDouble()); + } + + @OutputFunction(StandardTypes.DOUBLE) + public static void output(LongAndDoubleState state, BlockBuilder out) + { + long count = state.getLong(); + if (count == 0) { + out.appendNull(); + } + else { + double value = state.getDouble(); + DOUBLE.writeDouble(out, value / count); + } + } + } + +The above code implements the aggregation function ``avg`` which computes the +average of a ``DOUBLE`` column. + +* ``@InputFunction``: + + The ``@InputFunction`` annotation declares the function which accepts input + rows and stores them in the ``AccumulatorState``. Similar to scalar functions + you must annotate the arguments with ``@SqlType``. In this example, the input + function simply keeps track of the running count of rows (via ``setLong()``) + and the running sum (via ``setDouble()``). + +* ``@CombineFunction``: + + The ``@CombineFunction`` annotation declares the function used to combine two + state objects. This function is used to merge all the partial aggregation states. + It takes two state objects, and merges the results into the first one (in the + above example, just by adding them together). + +* ``@OutputFunction``: + + The ``@OutputFunction`` is the last function called when computing an + aggregation. It takes the final state object (the result of merging all + partial states) and writes the result to a ``BlockBuilder``. + +* Where does serialization happen, and what is ``@GroupedAccumulatorState``? + + The ``@InputFunction`` is usually run on a different worker from the + ``@CombineFunction``, so the state objects are serialized and transported + between these workers by the aggregation framework. ``@GroupedAccumulatorState`` + is used when performing a ``GROUP BY`` aggregation, and an implementation + will be automatically generated for you, if you don't specify a + ``AccumulatorStateFactory`` diff --git a/presto-docs/src/main/sphinx/develop/spi-overview.rst b/presto-docs/src/main/sphinx/develop/spi-overview.rst new file mode 100644 index 00000000..cb0884f6 --- /dev/null +++ b/presto-docs/src/main/sphinx/develop/spi-overview.rst @@ -0,0 +1,90 @@ +============ +SPI Overview +============ + +When you implement a new Presto plugin, you implement interfaces and +override methods defined by the SPI. + +Plugins can provide connectors, which are the source of all data +for queries in Presto. Even if your data source doesn't have underlying +tables backing it, as long as you adapt your data source to the API +expected by Presto, you can write queries against this data. + +Code +---- + +The SPI source can be found in the ``presto-spi`` directory in the +root of the Presto source tree. + +Plugin Metadata +--------------- + +Each plugin identifies an entry point: an implementation of the +``Plugin`` interface. This class name is provided to Presto via +the standard Java ``ServiceLoader`` interface: the classpath contains +a resource file named ``com.facebook.presto.spi.Plugin`` in the +``META-INF/services`` directory. The content of this file is a +single line listing the name of the plugin class: + +.. code-block:: none + + com.facebook.presto.example.ExamplePlugin + +Plugin +------ + +The ``Plugin`` interface is a good starting place for developers looking +to understand the Presto SPI. The ``getServices()`` method is a top-level +function that Presto calls to retrieve a ``ConnectorFactory`` when Presto +is ready to create an instance of a connector to back a catalog. + +ConnectorFactory +---------------- + +Instances of your connector are created by a ``ConnectorFactory`` +instance which is created when Presto calls ``getServices()`` on the +plugin to request a service of type ``ConnectorFactory``. +The connector factory is a simple interface responsible for creating an +instance of a ``Connector`` object that returns instances of the +following services: + +* ``ConnectorMetadata`` +* ``ConnectorSplitManager`` +* ``ConnectorHandleResolver`` +* ``ConnectorRecordSetProvider`` + +ConnectorMetdata +^^^^^^^^^^^^^^^^ + +The connector metadata interface has a large number of important +methods that are responsible for allowing Presto to look at lists of +schemas, lists of tables, lists of columns, and other metadata about a +particular data source. + +This interface is too big to list in this documentation, but if you +are interested in seeing strategies for implementing these methods, +look at the :doc:`example-http` and the Cassandra connector. If +your underlying data source supports schemas, tables and columns, this +interface should be straightforward to implement. If you are attempting +to adapt something that is not a relational database (as the Example HTTP +connector does), you may need to get creative about how you map your +data source to Presto's schema, table, and column concepts. + +ConnectorSplitManger +^^^^^^^^^^^^^^^^^^^^ + +The split manager partitions the data for a table into the individual +chunks that Presto will distribute to workers for processing. +For example, the Hive connector lists the files for each Hive +partition and creates one or more split per file. +For data sources that don't have partitioned data, a good strategy +here is to simply return a single split for the entire table. This +is the strategy employed by the Example HTTP connector. + +ConnectorRecordSetProvider +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Given a split and a list of columns, the record set provider is +responsible for delivering data to the Presto execution engine. +It creates a ``RecordSet``, which in turn creates a ``RecordCursor`` +that is used by Presto to read the column values for each row. diff --git a/presto-docs/src/main/sphinx/develop/types.rst b/presto-docs/src/main/sphinx/develop/types.rst new file mode 100644 index 00000000..e9a4d08b --- /dev/null +++ b/presto-docs/src/main/sphinx/develop/types.rst @@ -0,0 +1,34 @@ +=========== +Type System +=========== + +The ``Type`` interface in Presto is used to implement a type in the SQL language. +Presto ships with a number of built-in types, like ``VarcharType`` and ``BigintType``. +A ``Plugin`` can provide new types by returning them from ``getServices()``. +Below is a high level overview of the ``Type`` interface, for more details see the +JavaDocs for ``Type``. + +* Native container type: + + All types define the ``getJavaType()`` method, frequently referred to as the + "native container type". This is the Java type used to hold values during execution + and to store them in a ``Block``. For example, this is the type used in + the Java code that implements functions that produce or consume this ``Type``. + +* Native encoding: + + The interpretation of a value in its native container type form is defined by its + ``Type``. For some types, such as ``BigintType``, it matches the Java + interpretation of the native container type (64bit 2's complement). However, for other + types such as ``TimestampWithTimeZoneType``, which also uses ``long`` for its + native container type, the value stored in the ``long`` is a 8byte binary value + combining the timezone and the milliseconds since the unix epoch. In particular, + this means that you cannot compare two native values and expect a meaningful + result, without knowing the native encoding. + +* Type signature: + + The signature of a type defines its identity, and also encodes some general + information about the type, such as its type parameters (if it's parametric), + and its literal parameters. The literal parameters are used in types like + ``VARCHAR(10)``. \ No newline at end of file diff --git a/presto-docs/src/main/sphinx/ext/download.py b/presto-docs/src/main/sphinx/ext/download.py new file mode 100644 index 00000000..d3a4e1f3 --- /dev/null +++ b/presto-docs/src/main/sphinx/ext/download.py @@ -0,0 +1,60 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# noinspection PyUnresolvedReferences +from docutils import nodes, utils +# noinspection PyUnresolvedReferences +from sphinx.errors import SphinxError + +GROUP_ID = 'com.facebook.presto' +ARTIFACTS = { + 'server': ('presto-server', 'tar.gz', None), + 'cli': ('presto-cli', 'jar', 'executable'), + 'jdbc': ('presto-jdbc', 'jar', None), + 'verifier': ('presto-verifier', 'jar', 'executable'), + 'benchmark-driver': ('presto-benchmark-driver', 'jar', 'executable'), +} + + +def maven_filename(artifact, version, packaging, classifier): + classifier = '-' + classifier if classifier else '' + return '%s-%s%s.%s' % (artifact, version, classifier, packaging) + + +def maven_download(group, artifact, version, packaging, classifier): + base = 'https://repo1.maven.org/maven2/' + group_path = group.replace('.', '/') + filename = maven_filename(artifact, version, packaging, classifier) + return base + '/'.join((group_path, artifact, version, filename)) + + +def setup(app): + # noinspection PyDefaultArgument,PyUnusedLocal + def download_link_role(role, rawtext, text, lineno, inliner, options={}, content=[]): + version = app.config.release + + if not text in ARTIFACTS: + inliner.reporter.error('Unsupported download type: ' + text) + return [], [] + + artifact, packaging, classifier = ARTIFACTS[text] + + title = maven_filename(artifact, version, packaging, classifier) + uri = maven_download(GROUP_ID, artifact, version, packaging, classifier) + + node = nodes.reference(title, title, internal=False, refuri=uri) + + return [node], [] + + app.add_role('download', download_link_role) diff --git a/presto-docs/src/main/sphinx/functions.rst b/presto-docs/src/main/sphinx/functions.rst new file mode 100644 index 00000000..3874e948 --- /dev/null +++ b/presto-docs/src/main/sphinx/functions.rst @@ -0,0 +1,23 @@ +*********************** +Functions and Operators +*********************** + +.. toctree:: + :maxdepth: 1 + + functions/logical + functions/comparison + functions/conditional + functions/conversion + functions/math + functions/string + functions/binary + functions/datetime + functions/regexp + functions/json + functions/url + functions/aggregate + functions/window + functions/color + functions/array + functions/map diff --git a/presto-docs/src/main/sphinx/functions/aggregate.rst b/presto-docs/src/main/sphinx/functions/aggregate.rst new file mode 100644 index 00000000..9cc6f34c --- /dev/null +++ b/presto-docs/src/main/sphinx/functions/aggregate.rst @@ -0,0 +1,186 @@ +=================== +Aggregate Functions +=================== + +Aggregate functions operate on a set of values to compute a single result. + +Except for :func:`count`, :func:`count_if`, :func:`max_by` and :func:`approx_distinct`, all +of these aggregate functions ignore null values and return null for no input +rows or when all values are null. For example, :func:`sum` returns null +rather than zero and :func:`avg` does not include null values in the count. +The ``coalesce`` function can be used to convert null into zero. + +General Aggregate Functions +--------------------------- + +.. function:: arbitrary(x) -> [same as input] + + Returns an arbitrary non-null value of ``x``, if one exists. + +.. function:: array_agg(x) -> array<[same as input]> + + Returns an array created from the input ``x`` elements. + +.. function:: avg(x) -> double + + Returns the average (arithmetic mean) of all input values. + +.. function:: bool_and(boolean) -> boolean + + Returns ``TRUE`` if every input value is ``TRUE``, otherwise ``FALSE``. + +.. function:: bool_or(boolean) -> boolean + + Returns ``TRUE`` if any input value is ``TRUE``, otherwise ``FALSE``. + +.. function:: count(*) -> bigint + + Returns the number of input rows. + +.. function:: count(x) -> bigint + + Returns the number of non-null input values. + +.. function:: count_if(x) -> bigint + + Returns the number of ``TRUE`` input values. + This function is equivalent to ``count(CASE WHEN x THEN 1 END)``. + +.. function:: every(boolean) -> boolean + + This is an alias for :func:`bool_and`. + +.. function:: max_by(x, y) -> [same as x] + + Returns the value of ``x`` associated with the maximum value of ``y`` over all input values. + +.. function:: min_by(x, y) -> [same as x] + + Returns the value of ``x`` associated with the minimum value of ``y`` over all input values. + +.. function:: max(x) -> [same as input] + + Returns the maximum value of all input values. + +.. function:: min(x) -> [same as input] + + Returns the minimum value of all input values. + +.. function:: sum(x) -> [same as input] + + Returns the sum of all input values. + +Map Aggregate Functions +----------------------- + +.. function:: map_agg(key, value) -> map + + Returns a map created from the input ``key`` / ``value`` pairs. + +Approximate Aggregate Functions +------------------------------- + +.. function:: approx_distinct(x) -> bigint + + Returns the approximate number of distinct input values. + This function provides an approximation of ``count(DISTINCT x)``. + Zero is returned if all input values are null. + + This function should produce a standard error of 2.3%, which is the + standard deviation of the (approximately normal) error distribution over + all possible sets. It does not guarantee an upper bound on the error for + any specific input set. + +.. function:: approx_distinct(x, e) -> bigint + + Returns the approximate number of distinct input values. + This function provides an approximation of ``count(DISTINCT x)``. + Zero is returned if all input values are null. + + This function should produce a standard error of no more than ``e``, which + is the standard deviation of the (approximately normal) error distribution + over all possible sets. It does not guarantee an upper bound on the error + for any specific input set. The current implementation of this function + requires that ``e`` be in the range: [0.01150, 0.26000]. + +.. function:: approx_percentile(x, p) -> [same as input] + + Returns the approximate percentile for all input values of ``x`` at the + percentage ``p``. The value of ``p`` must be between zero and one and + must be constant for all input rows. + +.. function:: approx_percentile(x, w, p) -> [same as input] + + Returns the approximate weighed percentile for all input values of ``x`` + using the per-item weight ``w`` at the percentage ``p``. The weight must be + an integer value of at least one. It is effectively a replication count for + the value ``x`` in the percentile set. The value of ``p`` must be between + zero and one and must be constant for all input rows. + +.. function:: numeric_histogram(buckets, value, weight) -> map + + Computes an approximate histogram with up to ``buckets`` number of buckets + for all ``value``\ s with a per-item weight of ``weight``. The algorithm + is based loosely on: + + .. code-block:: none + + Yael Ben-Haim and Elad Tom-Tov, "A streaming parallel decision tree algorithm", + J. Machine Learning Research 11 (2010), pp. 849--872. + + ``buckets`` must be a ``bigint``. ``value`` and ``weight`` must be numeric. + +.. function:: numeric_histogram(buckets, value) -> map + + Computes an approximate histogram with up to ``buckets`` number of buckets + for all ``value``\ s. This function is equivalent to the variant of + :func:`numeric_histogram` that takes a ``weight``, with a per-item weight of ``1``. + +Statistical Aggregate Functions +------------------------------- + +.. function:: corr(y, x) -> double + + Returns correlation coefficient of input values. + +.. function:: covar_pop(y, x) -> double + + Returns the population covariance of input values. + +.. function:: covar_samp(y, x) -> double + + Returns the sample covariance of input values. + +.. function:: regr_intercept(y, x) -> double + + Returns linear regression intercept of input values. ``y`` is the dependent + value. ``x`` is the independent value. + +.. function:: regr_slope(y, x) -> double + + Returns linear regression slope of input values. ``y`` is the dependent + value. ``x`` is the independent value. + +.. function:: stddev(x) -> double + + This is an alias for :func:`stddev_samp`. + +.. function:: stddev_pop(x) -> double + + Returns the population standard deviation of all input values. + +.. function:: stddev_samp(x) -> double + + Returns the sample standard deviation of all input values. + +.. function:: variance(x) -> double + + This is an alias for :func:`var_samp`. + +.. function:: var_pop(x) -> double + + Returns the population variance of all input values. + +.. function:: var_samp(x) -> double + + Returns the sample variance of all input values. diff --git a/presto-docs/src/main/sphinx/functions/array.rst b/presto-docs/src/main/sphinx/functions/array.rst new file mode 100644 index 00000000..e296db4c --- /dev/null +++ b/presto-docs/src/main/sphinx/functions/array.rst @@ -0,0 +1,60 @@ +============================= +Array Functions and Operators +============================= + +Subscript Operator: [] +---------------------- + +The ``[]`` operator is used to access an element of an array and is indexed starting from one:: + + SELECT my_array[1] AS first_element + +Concatenation Operator: || +-------------------------- + +The ``||`` operator is used to concatenate an array with an array or an element of the same type:: + + SELECT ARRAY [1] || ARRAY [2]; => [1, 2] + SELECT ARRAY [1] || 2; => [1, 2] + SELECT 2 || ARRAY [1]; => [2, 1] + +Array Functions +--------------- + +.. function:: array_distinct(x) -> array + + Remove duplicate values from the array ``x``. + +.. function:: array_intersect(x, y) -> array + + Returns an array of the elements in the intersection of ``x`` and ``y``, without duplicates. + +.. function:: array_position(x, element) -> bigint + + Returns the position of the first occurrence of the ``element`` in array ``x`` (or 0 if not found). + +.. function:: array_sort(x) -> array + + Sorts and returns the array ``x``. The elements of ``x`` must be orderable. + +.. function:: cardinality(x) -> bigint + + Returns the cardinality (size) of the array ``x``. + +.. function:: concat(x, y) -> array + :noindex: + + Concatenates the arrays ``x`` and ``y``. This function provides the same + functionality as the SQL-standard concatenation operator (``||``). + +.. function:: contains(x, element) -> boolean + + Returns true if the array ``x`` contains the ``element``. + +.. function:: array_join(x, delimiter, null_replacement) -> varchar + + Concatenates the elements of the given array using the delimiter and an optional string to replace nulls. + +.. function:: array_remove(x, element) -> array + + Remove all elements that equal ``element`` from array ``x``. diff --git a/presto-docs/src/main/sphinx/functions/binary.rst b/presto-docs/src/main/sphinx/functions/binary.rst new file mode 100644 index 00000000..68270b87 --- /dev/null +++ b/presto-docs/src/main/sphinx/functions/binary.rst @@ -0,0 +1,35 @@ +================ +Binary Functions +================ + +Binary Functions +---------------- + +.. function:: length(binary) -> bigint + :noindex: + + Returns the length of ``binary`` in bytes. + +.. function:: to_base64(binary) -> varchar + + Encodes ``binary`` into a base64 string representation. + +.. function:: from_base64(string) -> varbinary + + Decodes binary data from the base64 encoded ``string``. + +.. function:: to_base64url(binary) -> varchar + + Encodes ``binary`` into a base64 string representation using the URL safe alphabet. + +.. function:: from_base64url(string) -> varbinary + + Decodes binary data from the base64 encoded ``string`` using the URL safe alphabet. + +.. function:: to_hex(binary) -> varchar + + Encodes ``binary`` into a hex string representation. + +.. function:: from_hex(string) -> varbinary + + Decodes binary data from the hex encoded ``string``. diff --git a/presto-docs/src/main/sphinx/functions/color.rst b/presto-docs/src/main/sphinx/functions/color.rst new file mode 100644 index 00000000..f4792117 --- /dev/null +++ b/presto-docs/src/main/sphinx/functions/color.rst @@ -0,0 +1,68 @@ +=============== +Color Functions +=============== + +.. function:: bar(x, width) -> varchar + + Renders a single bar in an ANSI bar chart using a default + ``low_color`` of red and a ``high_color`` of green. For example, + if ``x`` of 25% and width of 40 are passed to this function. A + 10-character red bar will be drawn followed by 30 spaces to create + a bar of 40 characters. + +.. function:: bar(x, width, low_color, high_color) -> varchar + + Renders a single line in an ANSI bar chart of the specified + ``width``. The parameter ``x`` is a double value between [0,1]. + Values of ``x`` that fall outside the range [0,1] will be + truncated to either a 0 or a 1 value. The ``low_color`` and + ``high_color`` capture the color to use for either end of + the horizontal bar chart. For example, if ``x`` is 0.5, ``width`` + is 80, ``low_color`` is 0xFF0000, and ``high_color`` is 0x00FF00 + this function will return a 40 character bar that varies from red + (0xFF0000) and yellow (0xFFFF00) and the remainder of the 80 + character bar will be padded with spaces. + + .. figure:: ../images/functions_color_bar.png + :align: center + +.. function:: color(string) -> color + + Returns a color capturing a decoded RGB value from a 4-character + string of the format "#000". The input string should be varchar + containing a CSS-style short rgb string or one of ``black``, + ``red``, ``green``, ``yellow``, ``blue``, ``magenta``, ``cyan``, + ``white``. + +.. function:: color(x, low, high, low_color, high_color) -> color + + Returns a color interpolated between ``low_color`` and + ``high_color`` using the double parameters ``x``, ``low``, and + ``high`` to calculate a fraction which is then passed to the + ``color(fraction, low_color, high_color)`` function shown below. + If ``x`` falls outside the range defined by ``low`` and ``high`` + it's value will be truncated to fit within this range. + +.. function:: color(x, low_color, high_color) -> color + + Returns a color interpolated between ``low_color`` and + ``high_color`` according to the double argument ``x`` between 0 + and 1.0. The parameter ``x`` is a double value between [0,1]. + Values of ``x`` that fall outside the range [0,1] will be + truncated to either a 0 or a 1 value. + +.. function:: render(x, color) -> varchar + + Renders value ``x`` using the specific color using ANSI + color codes. ``x`` can be either a double, bigint, or varchar. + +.. function:: render(b) -> varchar + + Accepts boolean value ``b`` and renders a green true or a red + false using ANSI color codes. + +.. function:: rgb(red, green, blue) -> color + + Returns a color value capturing the RGB value of three + component color values supplied as int parameters ranging from 0 + to 255: ``red``, ``green``, ``blue``. diff --git a/presto-docs/src/main/sphinx/functions/comparison.rst b/presto-docs/src/main/sphinx/functions/comparison.rst new file mode 100644 index 00000000..7a5e8c54 --- /dev/null +++ b/presto-docs/src/main/sphinx/functions/comparison.rst @@ -0,0 +1,122 @@ +================================== +Comparison Functions and Operators +================================== + +Comparison Operators +-------------------- + +======== =========== +Operator Description +======== =========== +``<`` Less than +``>`` Greater than +``<=`` Less than or equal to +``>=`` Greater than or equal to +``=`` Equal +``<>`` Not equal +``!=`` Not equal (non-standard but popular syntax) +======== =========== + +Range Operator: BETWEEN +----------------------- + +The ``BETWEEN`` operator tests if a value is within a specified range. +It uses the syntax ``value BETWEEN min AND max``:: + + SELECT 3 BETWEEN 2 AND 6; + +The statement shown above is equivalent to the following statement:: + + SELECT 3 >= 2 AND 3 <= 6; + +To test if a value does not fall within the specified range +use ``NOT BETWEEN``:: + + SELECT 3 NOT BETWEEN 2 AND 6; + +The statement shown above is equivalent to the following statement:: + + SELECT 3 < 2 OR 3 > 6; + +The presence of NULL in a ``BETWEEN`` or ``NOT BETWEEN`` statement +will result in the statement evaluating to NULL:: + + SELECT NULL BETWEEN 2 AND 4; => null + + SELECT 2 BETWEEN NULL AND 6; => null + +The ``BETWEEN`` and ``NOT BETWEEN`` operators can also be used to +evaluate string arguments:: + + SELECT 'Paul' BETWEEN 'John' AND 'Ringo'; => true + +Not that the value, min, and max parameters to ``BETWEEN`` and ``NOT +BETWEEN`` must be the same type. For example, Presto will produce an +error if you ask it if John is between 2.3 and 35.2. + +IS NULL and IS NOT NULL +----------------------- +The ``IS NULL`` and ``IS NOT NULL`` operators test whether a value +is null (undefined). Both operators work for all data types. + +Using ``NULL`` with ``IS NULL`` evaluates to true:: + + select NULL IS NULL; => true + +But any other constant does not:: + + SELECT 3.0 IS NULL; => false + +IS DISTINCT FROM and IS NOT DISTINCT FROM +----------------------------------------- + +In SQL a ``NULL`` value signifies an unknown value, so any comparison +involving a ``NULL`` will produce ``NULL``. The ``IS DISTINCT FROM`` +and ``IS NOT DISTINCT FROM`` operators treat ``NULL`` as a known value +and both operators guarantee either a true or false outcome even in +the presence of ``NULL`` input:: + + SELECT NULL IS DISTINCT FROM NULL; => false + + SELECT NULL IS NOT DISTINCT FROM NULL; => true + +In the example shown above, a ``NULL`` value is not considered +distinct from ``NULL``. When you are comparing values which may +include ``NULL`` use these operators to guarantee either a ``TRUE`` or +``FALSE`` result. + +The following truth table demonstrate the handling of ``NULL`` in +``IS DISTINCT FROM`` and ``IS NOT DISTINCT FROM``: + +======== ======== ========= ========= ============ ================ +a b a = a a <> b a DISTINCT b a NOT DISTINCT b +======== ======== ========= ========= ============ ================ +``1`` ``1`` ``TRUE`` ``FALSE`` ``FALSE`` ``TRUE`` +``1`` ``2`` ``FALSE`` ``TRUE`` ``TRUE`` ``FALSE`` +``1`` ``NULL`` ``NULL`` ``NULL`` ``TRUE`` ``FALSE`` +``NULL`` ``NULL`` ``NULL`` ``NULL`` ``FALSE`` ``TRUE`` +======== ======== ========= ========= ============ ================ + +GREATEST and LEAST +------------------ + +These functions are not in the SQL standard, but are a common extension. +Like most other functions in Presto, they return null if any argument is +null. Note that in some other databases, such as PostgreSQL, they only +return null if all arguments are null. + +The following types are supported: +``DOUBLE``, +``BIGINT``, +``VARCHAR``, +``TIMESTAMP``, +``TIMESTAMP WITH TIME ZONE``, +``DATE`` + +.. function:: greatest(value1, value2) -> [same as input] + + Returns the largest of the provided values. + +.. function:: least(value1, value2) -> [same as input] + + Returns the smallest of the provided values. diff --git a/presto-docs/src/main/sphinx/functions/conditional.rst b/presto-docs/src/main/sphinx/functions/conditional.rst new file mode 100644 index 00000000..c2d950a8 --- /dev/null +++ b/presto-docs/src/main/sphinx/functions/conditional.rst @@ -0,0 +1,88 @@ +======================= +Conditional Expressions +======================= + +CASE +---- + +The standard SQL ``CASE`` expression has two forms. +The "simple" form searches each ``value`` expression from left to right +until it finds one that equals ``expression``: + +.. code-block:: none + + CASE expression + WHEN value THEN result + [ WHEN ... ] + [ ELSE result ] + END + +The ``result`` for the matching ``value`` is returned. +If no match is found, the ``result`` from the ``ELSE`` clause is +returned if it exists, otherwise null is returned. Example:: + + SELECT a, + CASE a + WHEN 1 THEN 'one' + WHEN 2 THEN 'two' + ELSE 'many' + END + +The "searched" form evaluates each boolean ``condition`` from left +to right until one is true and returns the matching ``result``: + +.. code-block:: none + + CASE + WHEN condition THEN result + [ WHEN ... ] + [ ELSE result ] + END + +If no conditions are true, the ``result`` from the ``ELSE`` clause is +returned if it exists, otherwise null is returned. Example:: + + SELECT a, b, + CASE + WHEN a = 1 THEN 'aaa' + WHEN b = 2 THEN 'bbb' + ELSE 'ccc' + END + +IF +-- + +The ``IF`` function is actually a language construct +that is equivalent to the following ``CASE`` expression: + + .. code-block:: none + + CASE + WHEN condition THEN true_value + [ ELSE false_value ] + END + +.. function:: if(condition, true_value) + + Evaluates and returns ``true_value`` if ``condition`` is true, + otherwise null is returned and ``true_value`` is not evaluated. + +.. function:: if(condition, true_value, false_value) + + Evaluates and returns ``true_value`` if ``condition`` is true, + otherwise evaluates and returns ``false_value``. + +COALESCE +-------- + +.. function:: coalesce(value[, ...]) + + Returns the first non-null ``value`` in the argument list. + Like a ``CASE`` expression, arguments are only evaluated if necessary. + +NULLIF +------ + +.. function:: nullif(value1, value2) + + Returns null if ``value1`` equals ``value2``, otherwise returns ``value1``. diff --git a/presto-docs/src/main/sphinx/functions/conversion.rst b/presto-docs/src/main/sphinx/functions/conversion.rst new file mode 100644 index 00000000..8b633f9d --- /dev/null +++ b/presto-docs/src/main/sphinx/functions/conversion.rst @@ -0,0 +1,23 @@ +==================== +Conversion Functions +==================== + +Presto will implicity convert numeric and character values to the +correct type if such a conversion is possible. Presto will not convert +between character and numeric types. For example, a query that expects +a varchar will not automatically convert a bigint value to an +equivalent varchar. + +When necessary, values can be explicitly cast to a particular type. + +Conversion Functions +-------------------- + +.. function:: cast(value AS type) -> type + + Explicitly cast a value as a type. This can be used to cast a + varchar to a numeric value type and vice versa. + +.. function:: try_cast(value AS type) -> type + + Like :func:`cast`, but returns null if the cast fails. diff --git a/presto-docs/src/main/sphinx/functions/datetime.rst b/presto-docs/src/main/sphinx/functions/datetime.rst new file mode 100644 index 00000000..ca0359c2 --- /dev/null +++ b/presto-docs/src/main/sphinx/functions/datetime.rst @@ -0,0 +1,328 @@ +===================================== +Date and Time Functions and Operators +===================================== + +Date and Time Operators +----------------------- + +======== ===================================================== =========================== +Operator Example Result +======== ===================================================== =========================== +``+`` ``date '2012-08-08' + interval '2' day`` ``2012-08-10`` +``+`` ``time '01:00' + interval '3' hour`` ``04:00:00.000`` +``+`` ``timestamp '2012-08-08 01:00' + interval '29' hour`` ``2012-08-09 06:00:00.000`` +``+`` ``timestamp '2012-10-31 01:00' + interval '1' month`` ``2012-11-30 01:00:00.000`` +``+`` ``interval '2' day + interval '3' hour`` ``2 03:00:00.000`` +``+`` ``interval '3' year + interval '5' month`` ``3-5`` +``-`` ``date '2012-08-08' - interval '2' day`` ``2012-08-06`` +``-`` ``time '01:00' - interval '3' hour`` ``22:00:00.000`` +``-`` ``timestamp '2012-08-08 01:00' - interval '29' hour`` ``2012-08-06 20:00:00.000`` +``-`` ``timestamp '2012-10-31 01:00' - interval '1' month`` ``2012-09-30 01:00:00.000`` +``-`` ``interval '2' day - interval '3' hour`` ``1 21:00:00.000`` +``-`` ``interval '3' year - interval '5' month`` ``2-7`` +======== ===================================================== =========================== + +Time Zone Conversion +-------------------- + +The ``AT TIME ZONE`` operator sets the time zone of a timestamp:: + + SELECT timestamp '2012-10-31 01:00 UTC'; + 2012-10-31 01:00:00.000 UTC + + SELECT timestamp '2012-10-31 01:00 UTC' AT TIME ZONE 'America/Los_Angeles'; + 2012-10-30 18:00:00.000 America/Los_Angeles + +Date and Time Functions +----------------------- + +.. function:: current_date -> date + + Returns the current date as of the start of the query. + +.. function:: current_time -> time with time zone + + Returns the current time as of the start of the query. + +.. function:: current_timestamp -> timestamp with time zone + + Returns the current timestamp as of the start of the query. + +.. function:: current_timezone() -> varchar + + Returns the current time zone in the format defined by IANA + (e.g., ``America/Los_Angeles``) or as fixed offset from UTC (e.g., ``+08:35``) + +.. function:: from_unixtime(unixtime) -> timestamp + + Returns the UNIX timestamp ``unixtime`` as a timestamp. + +.. function:: from_unixtime(unixtime, hours, minutes) -> timestamp with time zone + + Returns the UNIX timestamp ``unixtime`` as a timestamp with time zone + using ``hours`` and ``minutes`` for the time zone offset. + +.. function:: localtime -> time + + Returns the current time as of the start of the query. + +.. function:: localtimestamp -> timestamp + + Returns the current timestamp as of the start of the query. + +.. function:: now() -> timestamp with time zone + + This is an alias for ``current_timestamp``. + +.. function:: to_unixtime(timestamp) -> double + + Returns ``timestamp`` as a UNIX timestamp. + +.. note:: The following SQL-standard functions do not use parenthesis: + + - ``current_date`` + - ``current_time`` + - ``current_timestamp`` + - ``localtime`` + - ``localtimestamp`` + +Truncation Function +------------------- + +The ``date_trunc`` function supports the following units: + +=========== =========================== +Unit Example Truncated Value +=========== =========================== +``second`` ``2001-08-22 03:04:05.000`` +``minute`` ``2001-08-22 03:04:00.000`` +``hour`` ``2001-08-22 03:00:00.000`` +``day`` ``2001-08-22 00:00:00.000`` +``week`` ``2001-08-20 00:00:00.000`` +``month`` ``2001-08-01 00:00:00.000`` +``quarter`` ``2001-07-01 00:00:00.000`` +``year`` ``2001-01-01 00:00:00.000`` +=========== =========================== + +The above examples use the timestamp ``2001-08-22 03:04:05.321`` as the input. + +.. function:: date_trunc(unit, x) -> [same as input] + + Returns ``x`` truncated to ``unit``. + +Interval Functions +------------------ + +The functions in this section support the following interval units: + +================= ================== +Unit Description +================= ================== +``millisecond`` Milliseconds +``second`` Seconds +``minute`` Minutes +``hour`` Hours +``day`` Days +``week`` Weeks +``month`` Months +``quarter`` Quarters of a year +``year`` Years +================= ================== + +.. function:: date_add(unit, value, timestamp) -> [same as input] + + Adds an interval ``value`` of type ``unit`` to ``timestamp``. + Subtraction can be performed by using a negative value. + +.. function:: date_diff(unit, timestamp1, timestamp2) -> bigint + + Returns ``timestamp2 - timestamp1`` expressed in terms of ``unit``. + +MySQL Date Functions +-------------------- + +The functions in this section use a format string that is compatible with +the MySQL ``date_parse`` and ``str_to_date`` functions. The following table, +based on the MySQL manual, describes the format specifiers: + +========= =========== +Specifier Description +========= =========== +``%a`` Abbreviated weekday name (``Sun`` .. ``Sat``) +``%b`` Abbreviated month name (``Jan`` .. ``Dec``) +``%c`` Month, numeric (``0`` .. ``12``) +``%D`` Day of the month with English suffix (``0th``, ``1st``, ``2nd``, ``3rd``, ...) +``%d`` Day of the month, numeric (``00`` .. ``31``) +``%e`` Day of the month, numeric (``0`` .. ``31``) +``%f`` Microseconds (``000000`` .. ``999999``) +``%H`` Hour (``00`` .. ``23``) +``%h`` Hour (``01`` .. ``12``) +``%I`` Hour (``01`` .. ``12``) +``%i`` Minutes, numeric (``00`` .. ``59``) +``%j`` Day of year (``001`` .. ``366``) +``%k`` Hour (``0`` .. ``23``) +``%l`` Hour (``1`` .. ``12``) +``%M`` Month name (``January`` .. ``December``) +``%m`` Month, numeric (``00`` .. ``12``) +``%p`` ``AM`` or ``PM`` +``%r`` Time, 12-hour (``hh:mm:ss`` followed by ``AM`` or ``PM``) +``%S`` Seconds (``00`` .. ``59``) +``%s`` Seconds (``00`` .. ``59``) +``%T`` Time, 24-hour (``hh:mm:ss``) +``%U`` Week (``00`` .. ``53``), where Sunday is the first day of the week +``%u`` Week (``00`` .. ``53``), where Monday is the first day of the week +``%V`` Week (``01`` .. ``53``), where Sunday is the first day of the week; used with ``%X`` +``%v`` Week (``01`` .. ``53``), where Monday is the first day of the week; used with ``%x`` +``%W`` Weekday name (``Sunday`` .. ``Saturday``) +``%w`` Day of the week (``0`` .. ``6``), where Sunday is the first day of the week +``%X`` Year for the week where Sunday is the first day of the week, numeric, four digits; used with ``%V`` +``%x`` Year for the week, where Monday is the first day of the week, numeric, four digits; used with ``%v`` +``%Y`` Year, numeric, four digits +``%y`` Year, numeric (two digits) +``%%`` A literal ``%`` character +``%x`` ``x``, for any ``x`` not listed above +========= =========== + +.. warning:: The following specifiers are not currently supported: ``%D %U %u %V %X`` + +.. function:: date_format(timestamp, format) -> varchar + + Formats ``timestamp`` as a string using ``format``. + +.. function:: date_parse(string, format) -> timestamp + + Parses ``string`` into a timestamp using ``format``. + +Java Date Functions +------------------- + +The functions in this section use a format string that is compatible with +the Java `SimpleDateFormat`_ pattern format. + +.. _SimpleDateFormat: http://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html + +.. function:: format_datetime(timestamp, format) -> varchar + + Formats ``timestamp`` as a string using ``format``. + +.. function:: parse_datetime(string, format) -> timestamp with time zone + + Parses ``string`` into a timestamp with time zone using ``format``. + +Extraction Function +------------------- + +The ``extract`` function supports the following fields: + +=================== =========== +Field Description +=================== =========== +``YEAR`` :func:`year` +``QUARTER`` :func:`quarter` +``MONTH`` :func:`month` +``WEEK`` :func:`week` +``DAY`` :func:`day` +``DAY_OF_MONTH`` :func:`day` +``DAY_OF_WEEK`` :func:`day_of_week` +``DOW`` :func:`day_of_week` +``DAY_OF_YEAR`` :func:`day_of_year` +``DOY`` :func:`day_of_year` +``YEAR_OF_WEEK`` :func:`year_of_week` +``YOW`` :func:`year_of_week` +``HOUR`` :func:`hour` +``MINUTE`` :func:`minute` +``SECOND`` :func:`second` +``TIMEZONE_HOUR`` :func:`timezone_hour` +``TIMEZONE_MINUTE`` :func:`timezone_minute` +=================== =========== + +The types supported by the ``extract`` function vary depending on the +field to be extracted. Most fields support all date and time types. + +.. function:: extract(field FROM x) -> bigint + + Returns ``field`` from ``x``. + + .. note:: This SQL-standard function uses special syntax for specifying the arguments. + +Convenience Extraction Functions +-------------------------------- + +.. function:: day(x) -> bigint + + Returns the day of the month from ``x``. + +.. function:: day_of_month(x) -> bigint + + This is an alias for :func:`day`. + +.. function:: day_of_week(x) -> bigint + + Returns the ISO day of the week from ``x``. + The value ranges from ``1`` (Monday) to ``7`` (Sunday). + +.. function:: day_of_year(x) -> bigint + + Returns the day of the year from ``x``. + The value ranges from ``1`` to ``366``. + +.. function:: dow(x) -> bigint + + This is an alias for :func:`day_of_week`. + +.. function:: doy(x) -> bigint + + This is an alias for :func:`day_of_year`. + +.. function:: hour(x) -> bigint + + Returns the hour of the day from ``x``. + The value ranges from ``0`` to ``23``. + +.. function:: minute(x) -> bigint + + Returns the minute of the hour from ``x``. + +.. function:: month(x) -> bigint + + Returns the month of the year from ``x``. + +.. function:: quarter(x) -> bigint + + Returns the quarter of the year from ``x``. + The value ranges from ``1`` to ``4``. + +.. function:: second(x) -> bigint + + Returns the second of the hour from ``x``. + +.. function:: timezone_hour(timestamp) -> bigint + + Returns the hour of the time zone offset from ``timestamp``. + +.. function:: timezone_minute(timestamp) -> bigint + + Returns the minute of the time zone offset from ``timestamp``. + +.. function:: week(x) -> bigint + + Returns the `ISO week`_ of the year from ``x``. + The value ranges from ``1`` to ``53``. + + .. _ISO week: https://en.wikipedia.org/wiki/ISO_week_date + +.. function:: week_of_year(x) -> bigint + + This is an alias for :func:`week`. + +.. function:: year(x) -> bigint + + Returns the year from ``x``. + +.. function:: year_of_week(x) -> bigint + + Returns the year of the `ISO week`_ from ``x``. + +.. function:: yow(x) -> bigint + + This is an alias for :func:`year_of_week`. diff --git a/presto-docs/src/main/sphinx/functions/json.rst b/presto-docs/src/main/sphinx/functions/json.rst new file mode 100644 index 00000000..0f3889ae --- /dev/null +++ b/presto-docs/src/main/sphinx/functions/json.rst @@ -0,0 +1,63 @@ +============== +JSON Functions +============== + +.. function:: json_array_contains(json, value) -> boolean + + Determine if ``value`` exists in ``json`` (a string containing a JSON array):: + + SELECT json_array_contains('[1, 2, 3]', 2); + +.. function:: json_array_length(json) -> bigint + + Returns the array length of ``json`` (a string containing a JSON array):: + + SELECT json_array_length('[1, 2, 3]'); + +.. function:: json_extract(json, json_path) -> json + + Evaluates the `JSONPath`_-like expression ``json_path`` on ``json`` + (a string containing JSON) and returns the result as a JSON string:: + + SELECT json_extract(json, '$.store.book'); + + .. _JSONPath: http://goessner.net/articles/JsonPath/ + +.. function:: json_extract_scalar(json, json_path) -> varchar + + Like :func:`json_extract`, but returns the result value as a string (as opposed + to being encoded as JSON). The value referenced by ``json_path`` must be a + scalar (boolean, number or string):: + + SELECT json_extract_scalar('[1, 2, 3]', '$[2]'); + SELECT json_extract_scalar(json, '$.store.book[0].author'); + +.. function:: json_array_get(json_array, index) -> varchar + + Returns the element at the specified index into the ``json_array``. + The index is zero-based:: + + SELECT json_array_get('["a", "b", "c"]', 0); => "a" + SELECT json_array_get('["a", "b", "c"]', 1); => "b" + + This function also supports negative indexes for fetching element indexed + from the end of an array:: + + SELECT json_array_get('["c", "b", "a"]', -1); => "a" + SELECT json_array_get('["c", "b", "a"]', -2); => "b" + + If the element at the specified index doesn't exist, the function returns null:: + + SELECT json_array_get('[]', 0); => null + SELECT json_array_get('["a", "b", "c"]', 10); => null + SELECT json_array_get('["c", "b", "a"]', -10); => null + +.. function:: json_size(json, json_path) -> bigint + + Like :func:`json_extract`, but returns the size of the value. + For objects or arrays, the size is the number of members, + and the size of a scalar value is zero:: + + SELECT json_size('{"x": {"a": 1, "b": 2}}', '$.x'); => 2 + SELECT json_size('{"x": [1, 2, 3]}', '$.x'); => 3 + SELECT json_size('{"x": {"a": 1, "b": 2}}', '$.x.a'); => 0 diff --git a/presto-docs/src/main/sphinx/functions/logical.rst b/presto-docs/src/main/sphinx/functions/logical.rst new file mode 100644 index 00000000..d0d43f79 --- /dev/null +++ b/presto-docs/src/main/sphinx/functions/logical.rst @@ -0,0 +1,68 @@ +================= +Logical Operators +================= + +Logical Operators +----------------- + +======== ============================ ======= +Operator Description Example +======== ============================ ======= +``AND`` True if both values are true a AND b +``OR`` True if either value is true a OR b +``NOT`` True if the value is false NOT a +======== ============================ ======= + +Effect of NULL on Logical Operators +----------------------------------- + +The result of an ``AND`` comparison may be ``NULL`` if one or both +sides of the expression are ``NULL``. If at least one side of an +``AND`` operator is ``FALSE`` the expression evaluates to ``FALSE``:: + + SELECT CAST(null AS boolean) AND true; => null + + SELECT CAST(null AS boolean) AND false; => false + + SELECT CAST(null AS boolean) AND CAST(null AS boolean); => null + +The result of an ``OR`` comparison may be ``NULL`` if one or both +sides of the expression are ``NULL``. If at least one side of an +``OR`` operator is ``TRUE`` the expression evaluates to ``TRUE``:: + + SELECT CAST(null AS boolean) OR CAST(null AS boolean); => null + + SELECT CAST(null AS boolean) OR false; => null + + SELECT CAST(null AS boolean) OR true; => true + +The following truth table demonstrates the handling of +``NULL`` in ``AND`` and ``OR``: + +========= ========= ========= ========= +a b a AND b a OR b +========= ========= ========= ========= +``TRUE`` ``TRUE`` ``TRUE`` ``TRUE`` +``TRUE`` ``FALSE`` ``FALSE`` ``TRUE`` +``TRUE`` ``NULL`` ``NULL`` ``TRUE`` +``FALSE`` ``TRUE`` ``FALSE`` ``TRUE`` +``FALSE`` ``FALSE`` ``FALSE`` ``FALSE`` +``FALSE`` ``NULL`` ``FALSE`` ``NULL`` +``NULL`` ``TRUE`` ``NULL`` ``TRUE`` +``NULL`` ``FALSE`` ``FALSE`` ``NULL`` +``NULL`` ``NULL`` ``NULL`` ``NULL`` +========= ========= ========= ========= + +The logical complement of ``NULL`` is ``NULL`` as shown in the following example:: + + SELECT NOT CAST(null AS boolean); => null + +The following truth table demonstrates the handling of ``NULL`` in ``NOT``: + +========= ========= +a NOT a +========= ========= +``TRUE`` ``FALSE`` +``FALSE`` ``TRUE`` +``NULL`` ``NULL`` +========= ========= diff --git a/presto-docs/src/main/sphinx/functions/map.rst b/presto-docs/src/main/sphinx/functions/map.rst new file mode 100644 index 00000000..34ca6310 --- /dev/null +++ b/presto-docs/src/main/sphinx/functions/map.rst @@ -0,0 +1,34 @@ +=========================== +Map Functions and Operators +=========================== + +Subscript Operator: [] +---------------------- + +The ``[]`` operator is used to retrieve the value corresponding to a given key from a map:: + + SELECT name_to_age_map['Bob'] AS bob_age + +Map Functions +------------- + +.. function:: map(array, array) -> map + + Returns a map created using the given key/value arrays. :: + + SELECT MAP(ARRAY[1,3], ARRAY[2,4]); => {1 -> 2, 3 -> 4} + +.. function:: cardinality(x) -> bigint + :noindex: + + Returns the cardinality (size) of the map ``x``. + +.. function:: map_keys(x) -> array + + Returns all the keys in the map ``x``. + +.. function:: map_values(x) -> array + + Returns all the values in the map ``x``. + +See also :func:`map_agg` for creating a map as an aggregation. diff --git a/presto-docs/src/main/sphinx/functions/math.rst b/presto-docs/src/main/sphinx/functions/math.rst new file mode 100644 index 00000000..08f7ab1b --- /dev/null +++ b/presto-docs/src/main/sphinx/functions/math.rst @@ -0,0 +1,176 @@ +==================================== +Mathematical Functions and Operators +==================================== + +Mathematical Operators +---------------------- + +======== =========== +Operator Description +======== =========== +``+`` Addition +``-`` Subtraction +``*`` Multiplication +``/`` Division (integer division performs truncation) +``%`` Modulus (remainder) +======== =========== + +Mathematical Functions +---------------------- + +.. function:: abs(x) -> [same as input] + + Returns the absolute value of ``x``. + +.. function:: cbrt(x) -> double + + Returns the cube root of ``x``. + +.. function:: ceil(x) -> [same as input] + + This is an alias for :func:`ceiling`. + +.. function:: ceiling(x) -> [same as input] + + Returns ``x`` rounded up to the nearest integer. + +.. function:: degrees(x) -> double + + Converts angle ``x`` in radians to degrees. + +.. function:: e() -> double + + Returns the constant Euler's number. + +.. function:: exp(x) -> double + + Returns Euler's number raised to the power of ``x``. + +.. function:: floor(x) -> [same as input] + + Returns ``x`` rounded down to the nearest integer. + +.. function:: from_base(string, radix) -> bigint + + Returns the value of ``string`` interpreted as a base-``radix`` number. + +.. function:: ln(x) -> double + + Returns the natural logarithm of ``x``. + +.. function:: log2(x) -> double + + Returns the base 2 logarithm of ``x``. + +.. function:: log10(x) -> double + + Returns the base 10 logarithm of ``x``. + +.. function:: log(x, b) -> double + + Returns the base ``b`` logarithm of ``x``. + +.. function:: mod(n, m) -> [same as input] + + Returns the modulus (remainder) of ``n`` divided by ``m``. + +.. function:: pi() -> double + + Returns the constant Pi. + +.. function:: pow(x, p) -> double + + Returns ``x`` raised to the power of ``p``. + +.. function:: radians(x) -> double + + Converts angle ``x`` in degrees to radians. + +.. function:: rand() -> double + + Alias for ``random()``. + +.. function:: random() -> double + + Returns a pseudo-random value in the range 0.0 <= x < 1.0 + +.. function:: round(x) -> [same as input] + + Returns ``x`` rounded to the nearest integer. + +.. function:: round(x, d) -> [same as input] + + Returns ``x`` rounded to ``d`` decimal places. + +.. function:: sqrt(x) -> double + + Returns the square root of ``x``. + +.. function:: to_base(x, radix) -> varchar + + Returns the base-``radix`` representation of ``x``. + +Trigonometric Functions +----------------------- + +All trigonometric function arguments are expressed in radians. +See unit conversion functions :func:`degrees` and :func:`radians`. + +.. function:: acos(x) -> double + + Returns the arc cosine of ``x``. + +.. function:: asin(x) -> double + + Returns the arc sine of ``x``. + +.. function:: atan(x) -> double + + Returns the arc tangent of ``x``. + +.. function:: atan2(y, x) -> double + + Returns the arc tangent of ``y / x``. + +.. function:: cos(x) -> double + + Returns the cosine of ``x``. + +.. function:: cosh(x) -> double + + Returns the hyperbolic cosine of ``x``. + +.. function:: sin(x) -> double + + Returns the sine of ``x``. + +.. function:: tan(x) -> double + + Returns the tangent of ``x``. + +.. function:: tanh(x) -> double + + Returns the hyperbolic tangent of ``x``. + +Floating Point Functions +------------------------ + +.. function:: infinity() -> double + + Returns the constant representing positive infinity. + +.. function:: is_finite(x) -> boolean + + Determine if ``x`` is finite. + +.. function:: is_infinite(x) -> boolean + + Determine if ``x`` is infinite. + +.. function:: is_nan(x) -> boolean + + Determine if ``x`` is not-a-number. + +.. function:: nan() -> double + + Returns the constant representing not-a-number. diff --git a/presto-docs/src/main/sphinx/functions/regexp.rst b/presto-docs/src/main/sphinx/functions/regexp.rst new file mode 100644 index 00000000..db8fd629 --- /dev/null +++ b/presto-docs/src/main/sphinx/functions/regexp.rst @@ -0,0 +1,91 @@ +============================ +Regular Expression Functions +============================ + +All of the regular expression functions use the `Java pattern`_ syntax, +with a few notable exceptions: + +* When using multi-line mode (enabled via the ``(?m)`` flag), + only ``\n`` is recognized as a line terminator. Additionally, + the ``(?d)`` flag is not supported and must not be used. +* Case-insensitive matching (enabled via the ``(?i)`` flag) is always + performed in a Unicode-aware manner. However, context-sensitive and + local-sensitive matching is not supported. Additionally, the + ``(?u)`` flag is not supported and must not be used. +* Surrogate pairs are not supported. For example, ``\uD800\uDC00`` is + not treated as ``U+10000`` and must be specified as ``\x{10000}``. +* Boundaries (``\b``) are incorrectly handled for a non-spacing mark + without a base character. +* ``\Q`` and ``\E`` are not supported in character classes + (such as ``[A-Z123]``) and are instead treated as literals. +* Unicode character classes (``\p{prop}``) are supported with + the following differences: + + * All underscores in names must be removed. For example, use + ``OldItalic`` instead of ``Old_Italic``. + * Scripts must be specified directly, without the + ``Is``, ``script=`` or ``sc=`` prefixes. + Example: ``\p{Hiragana}`` + * Blocks must be specified with the ``In`` prefix. + The ``block=`` and ``blk=`` prefixes are not supported. + Example: ``\p{Mongolian}`` + * Categories must be specified directly, without the ``Is``, + ``general_category=`` or ``gc=`` prefixes. + Example: ``\p{L}`` + * Binary properties must be specified directly, without the ``Is``. + Example: ``\p{NoncharacterCodePoint}`` + + .. _Java pattern: http://docs.oracle.com/javase/8/docs/api/java/util/regex/Pattern.html + + .. _capturing group number: http://docs.oracle.com/javase/8/docs/api/java/util/regex/Pattern.html#gnumber + +.. function:: regexp_extract_all(string, pattern) -> array + + Returns the substring(s) matched by the regular expression ``pattern`` + in ``string``. + +.. function:: regexp_extract_all(string, pattern, group) -> array + + Finds all occurrences of the regular expression ``pattern`` in ``string`` + and returns the `capturing group number`_ ``group``. + +.. function:: regexp_extract(string, pattern) -> varchar + + Returns the first substring matched by the regular expression ``pattern`` + in ``string``. + +.. function:: regexp_extract(string, pattern, group) -> varchar + + Finds the first occurrence of the regular expression ``pattern`` in + ``string`` and returns the `capturing group number`_ ``group``. + +.. function:: regexp_like(string, pattern) -> boolean + + Evaluates the regular expression ``pattern`` and determines if it is + contained within ``string``. + + This function is similar to the ``LIKE`` operator, expect that the + pattern only needs to be contained within ``string``, rather than + needing to match all of ``string``. In other words, this performs a + *contains* operation rather than a *match* operation. You can match + the entire string by anchoring the pattern using ``^`` and ``$``. + +.. function:: regexp_replace(string, pattern) -> varchar + + Removes every instance of the substring matched by the regular expression + ``pattern`` from ``string``. + +.. function:: regexp_replace(string, pattern, replacement) -> varchar + + Replaces every instance of the substring matched by the regular expression + ``pattern`` in ``string`` with ``replacement``. `Capturing groups`_ can be + referenced in ``replacement`` using ``$g`` for a numbered group or + ``${name}`` for a named group. A dollar sign (``$``) may be included in the + replacement by escaping it with a backslash (``\$``). + + .. _Capturing groups: http://docs.oracle.com/javase/8/docs/api/java/util/regex/Pattern.html#cg + +.. function:: regexp_split(string, pattern) -> array + + Splits ``string`` using the regular expression ``pattern`` and returns an + array. Trailing empty strings are preserved. diff --git a/presto-docs/src/main/sphinx/functions/string.rst b/presto-docs/src/main/sphinx/functions/string.rst new file mode 100644 index 00000000..14ba1022 --- /dev/null +++ b/presto-docs/src/main/sphinx/functions/string.rst @@ -0,0 +1,130 @@ +============================== +String Functions and Operators +============================== + +String Operators +---------------- + +The ``||`` operator performs concatenation. + +String Functions +---------------- + +.. note:: + + These functions assume that the input strings contain valid UTF-8 encoded + Unicode code points. There are no explicit checks for valid UTF-8, and + the functions may return incorrect results on invalid UTF-8. + Invalid UTF-8 data can be corrected with :func:`from_utf8`. + + Additionally, the functions operate on Unicode code points and not user + visible *characters* (or *grapheme clusters*). Some languages combine + multiple code points into a single user-perceived *character*, the basic + unit of a writing system for a language, but the functions will treat each + code point as a separate unit. + + The :func:`lower` and :func:`upper` functions do not perform + locale-sensitive, context-sensitive, or one-to-many mappings required for + some languages. Specifically, this will return incorrect results for + Lithuanian, Turkish and Azeri. + +.. function:: chr(n) -> varchar + + Returns the Unicode code point ``n`` as a single character string. + +.. function:: concat(string1, string2) -> varchar + + Returns the concatenation of ``string1`` and ``string2``. + This function provides the same functionality as the + SQL-standard concatenation operator (``||``). + +.. function:: length(string) -> bigint + + Returns the length of ``string`` in characters. + +.. function:: lower(string) -> varchar + + Converts ``string`` to lowercase. + +.. note:: + + This method does not perform perform locale-sensitive, context-sensitive, + or one-to-many mappings required for some languages. Specifically, this + will return incorrect results for Lithuanian, Turkish and Azeri. + +.. function:: ltrim(string) -> varchar + + Removes leading whitespace from ``string``. + +.. function:: replace(string, search) -> varchar + + Removes all instances of ``search`` from ``string``. + +.. function:: replace(string, search, replace) -> varchar + + Replaces all instances of ``search`` with ``replace`` in ``string``. + +.. function:: reverse(string) -> varchar + + Returns ``string`` with the characters in reverse order. + +.. function:: rtrim(string) -> varchar + + Removes trailing whitespace from ``string``. + +.. function:: split(string, delimiter) -> array + + Splits ``string`` on ``delimiter`` and returns an array. + +.. function:: split(string, delimiter, limit) -> array + + Splits ``string`` on ``delimiter`` and returns an array of size at most + ``limit``. The last element in the array always contain everything + left in the ``string``. ``limit`` must be a positive number. + +.. function:: split_part(string, delimiter, index) -> varchar + + Splits ``string`` on ``delimiter`` and returns the field ``index``. + Field indexes start with ``1``. If the index is larger than than + the number of fields, then null is returned. + +.. function:: strpos(string, substring) -> bigint + + Returns the starting position of the first instance of ``substring`` in + ``string``. Positions start with ``1``. If not found, ``0`` is returned. + +.. function:: substr(string, start) -> varchar + + Returns the rest of ``string`` from the starting position ``start``. + Positions start with ``1``. A negative starting position is interpreted + as being relative to the end of the string. + +.. function:: substr(string, start, length) -> varchar + + Returns a substring from ``string`` of length ``length`` from the starting + position ``start``. Positions start with ``1``. A negative starting + position is interpreted as being relative to the end of the string. + +.. function:: trim(string) -> varchar + + Removes leading and trailing whitespace from ``string``. + +.. function:: upper(string) -> varchar + + Converts ``string`` to uppercase. + +.. function:: to_utf8(string) -> varbinary + + Encodes ``string`` into a UTF-8 varbinary representation. + +.. function:: from_utf8(binary) -> varchar + + Decodes a UTF-8 encoded string from ``binary``. Invalid UTF-8 sequences + are replaced with the Unicode replacement character ``U+FFFD``. + +.. function:: from_utf8(binary, replace) -> varchar + + Decodes a UTF-8 encoded string from ``binary``. Invalid UTF-8 sequences + are replaced with `replace`. The replacement string `replace` must either + be a single character or empty (in which case invalid characters are + removed). diff --git a/presto-docs/src/main/sphinx/functions/url.rst b/presto-docs/src/main/sphinx/functions/url.rst new file mode 100644 index 00000000..9ec64fa2 --- /dev/null +++ b/presto-docs/src/main/sphinx/functions/url.rst @@ -0,0 +1,44 @@ +============= +URL Functions +============= + +The URL functions extract components from HTTP URLs +(or any valid URIs conforming to :rfc:`2396`). +The following syntax is supported: + +.. code-block:: none + + [protocol:][//host[:port]][path][?query][#fragment] + +The extracted components do not contain URI syntax separators +such as ``:`` or ``?``. + +.. function:: url_extract_fragment(url) -> varchar + + Returns the fragment identifier from ``url``. + +.. function:: url_extract_host(url) -> varchar + + Returns the host from ``url``. + +.. function:: url_extract_parameter(url, name) -> varchar + + Returns the value of the first query string parameter named ``name`` + from ``url``. Parameter extraction is handled in the typical manner + as specified by :rfc:`1866#section-8.2.1`. + +.. function:: url_extract_path(url) -> varchar + + Returns the path from ``url``. + +.. function:: url_extract_port(url) -> bigint + + Returns the port number from ``url``. + +.. function:: url_extract_protocol(url) -> varchar + + Returns the protocol from ``url``. + +.. function:: url_extract_query(url) -> varchar + + Returns the query string from ``url``. diff --git a/presto-docs/src/main/sphinx/functions/window.rst b/presto-docs/src/main/sphinx/functions/window.rst new file mode 100644 index 00000000..ea0921af --- /dev/null +++ b/presto-docs/src/main/sphinx/functions/window.rst @@ -0,0 +1,123 @@ +================ +Window Functions +================ + +Window functions perform calculations across rows of the query result. +They run after the ``HAVING`` clause but before the ``ORDER BY`` clause. +Invoking a window function requires special syntax using the ``OVER`` +clause to specify the window. A window has three components: + +* The partition specification, which separates the input rows into different + partitions. This is analogous to how the ``GROUP BY`` clause separates rows + into different groups for aggregate functions. +* The ordering specification, which determines the order in which input rows + will be processed by the window function. +* The window frame, which specifies a sliding window of rows to be processed + by the function for a given row. If the frame is not specified, it defaults + to ``RANGE UNBOUNDED PRECEDING``, which is the same as + ``RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW``. This frame contains all + rows from the start of the partition up to the last peer of the current row. + +For example, the following query ranks orders for each clerk by price:: + + SELECT orderkey, clerk, totalprice, + rank() OVER (PARTITION BY clerk + ORDER BY totalprice DESC) AS rnk + FROM orders + ORDER BY clerk, rnk + +Aggregate Functions +------------------- + +All :doc:`aggregate` can be used as window functions by adding the ``OVER`` +clause. The aggregate function is computed for each row over the rows within +the current row's window frame. + +For example, the following query produces a rolling sum of order prices +by day for each clerk:: + + SELECT clerk, orderdate, orderkey, totalprice, + sum(totalprice) OVER (PARTITION BY clerk + ORDER BY orderdate) AS rolling_sum + FROM orders + ORDER BY clerk, orderdate, orderkey + +Ranking Functions +----------------- + +.. function:: cume_dist() -> bigint + + Returns the cumulative distribution of a value in a group of values. + The result is the number of rows preceding or peer with the row in the + window ordering of the window partition divided by the total number of + rows in the window partition. Thus, any tie values in the ordering will + evaluate to the same distribution value. + +.. function:: dense_rank() -> bigint + + Returns the rank of a value in a group of values. This is similar to + :func:`rank`, except that tie values do not produce gaps in the sequence. + +.. function:: ntile(n) -> bigint + + Divides the rows for each window partition into ``n`` buckets ranging + from ``1`` to at most ``n``. Bucket values will differ by at most ``1``. + If the number of rows in the partition does not divide evenly into the + number of buckets, then the remainder values are distributed one per + bucket, starting with the first bucket. + + For example, with ``6`` rows and ``4`` buckets, the bucket values would + be as follows: ``1`` ``1`` ``2`` ``2`` ``3`` ``4`` + +.. function:: percent_rank() -> bigint + + Returns the percentage ranking of a value in group of values. The result + is ``(r - 1) / (n - 1)`` where ``r`` is the :func:`rank` of the row and + ``n`` is the total number of rows in the window partition. + +.. function:: rank() -> bigint + + Returns the rank of a value in a group of values. The rank is one plus + the number of rows preceding the row that are not peer with the row. + Thus, tie values in the ordering will produce gaps in the sequence. + The ranking is performed for each window partition. + +.. function:: row_number() -> bigint + + Returns a unique, sequential number for each row, starting with one, + according to the ordering of rows within the window partition. + +Value Functions +--------------- + +.. function:: first_value(x) -> [same as input] + + Returns the first value of the window. + +.. function:: last_value(x) -> [same as input] + + Returns the last value of the window. + +.. function:: nth_value(x, offset) -> [same as input] + + Returns the value at the specified offset from beginning the window. + Offsets start at ``1``. The offset can be any scalar + expression. If the offset is null or greater than the number of values in + the window, null is returned. It is an error for the offset to be zero or + negative. + +.. function:: lead(x[, offset [, default_value]]) -> [same as input] + + Returns the value at ``offset`` rows after the current row in the window. + Offsets start at ``0``, which is the current row. The + offset can be any scalar expression. The default ``offset`` is ``1``. If the + offset is null or larger than the window, the ``default_value`` is returned, + or if it is not specified ``null`` is returned. + +.. function:: lag(x[, offset [, default_value]]) -> [same as input] + + Returns the value at ``offset`` rows before the current row in the window + Offsets start at ``0``, which is the current row. The + offset can be any scalar expression. The default ``offset`` is ``1``. If the + offset is null or larger than the window, the ``default_value`` is returned, + or if it is not specified ``null`` is returned. diff --git a/presto-docs/src/main/sphinx/images/functions_color_bar.png b/presto-docs/src/main/sphinx/images/functions_color_bar.png new file mode 100644 index 0000000000000000000000000000000000000000..b01f4e0dc8673a457ffbeeb05a44e6ffaf35bff4 GIT binary patch literal 49267 zcmeFabyS?a*6=;`7AO?A7U-bG26vbi1}j#qxVyW{&{Evpp?LA)E-mivR@~ifc&EMP z^l;AUdERe5f4pmby%r3U?ChQ7m+V|gt_)8T2ne>mdCk~>vA%#mt>_kZG-aBv@=X- zU-=>SqO-I|eNAouC2ZmkNz?c8b+1aORJ8LR=M@3@I)0L$)v zBo^~WxfQMIh8EWvrh6y;4PXk*wG8RoLjar4Js^F9!kvbsq(*c=yb+)U0|NsAU<3Hv zed#BDJj=u=$mCG786w}02zdEIMuxh5rTgpkImTkAmr?XB^%WuFgdyOJHdI)4v+fzD&<~V@sd5ZO4~mr0Yn1E6G$Hi$(d8wzS=Wa3ujNljs(0U{5Yk} z?i1pxOesKw^jLZ*UCLuV?KxN`5Q6m-+yshPY;8uGi}-r!Z4X!ml%`gYXt165i^j4s z>8%UUn(@YeWjvAr`iVTx1PWKT$y&OVse8_j#q;I8Z?2N01e`-w?0X;1oBOv?~KO4$tGv zI|k4RkK>eDuY-#9M%fsRt~a_QjPVAn^X@^si>XhfykU2+0-PT{_rl=y_HsH+X|?ZK zRp-)H*0W|XCC07iL+Gb7s?rZRBq9@FLt z;(raXO|4xe;&3}opElU3XAcjuzhLNuj-c~imb-eI2XeQ%wDB*ug+0R6xRNGa1Tmt0 zeA0idEj26U^@(Mj#3qpAHM5&Y!biL;zajeUIneco+2{PnLZ$W(p9>J|KgU~U8)iRZ zbGa;EuX+AJ>Y*f3+XqG+z!NEo7*b4~m9~mkXu@^D_@v80&l+tkEN$;d!MnV#TniQV z6TrjT1GUd7x((G&iHhpgxe&j#X}!GY#_pOYZBTvmw7M%wL<9rpF#ze)%V!*PZjY$d z5d|w6?$kYm;E)kv`3N;K?S{DD7NWmn_4$d;r-s+L%>u^Fwwo zGxdtRY8T1H-u>2lBKZ$^$znK9GCp9=y{vem`T>k8(DY;vf*0~i8(ouEyz6-#8Kw7Y zP2$3UPo%EEz$fHBR*(l4arQ3{pMZQ&Ncf9{pAAHpkhO-WYCpjhBMX&oH!kv~CdCfx zY(LU|Tp5*1E)j~|wxLaa=D(TwGE2%a9Ct~`LGrl_b%eN%q?93nthlTs^AJzAk6~)Q zRDulXC{cG{f+@;2&1Fr9!)JMr|B%ZOw>D%(Y;-X1Q1<-Rv>;UX6TLqUG7_Nx26044 zck~wuEg5=S24qHY#(c&q9J6TA41z(=wRV5)40@}W;+W3pxR{HW?k?nIVqLIbGlM$& z=S%rQY66iKN-Y!hEu92)6%CuhnHO1HkUS4Hg%6pj;WZLnT<+cmRd8%OjwpGmP#9* z49{4UuxqhPKE0*KlNl@BTlv)?KR?$rogj@ztX8J>^;PgW>5=MVoySH2PaY?>a0M6! z5aY2jpEH*-CopRD)DLwT#po(EV52k~hSH%zr~wL6ELxgI!SQDUiL zDPS2x)B(RoH)-%mqgB|780c96 zTY-+MUM=p-mzjgwAjkTXz1@(rr-x^Qqy4PCc0pz*_Hlb(_c&X?`-~?GXC?<5hb!lL zr_h~QTy?Al#7~dxar99s9!}w&lC;rMuN=Kzh9#_q=A#*MERz$s!~K_)>4U@Ea3|BOFF;Jo0lz>(N)Je?h{5fdlJb4RVi zMw|;A1JOZ<<@^Vq9P*h2ZDwJvtmjyTl7%^1JI&)wn9WxVG3@N7mPd=Ndp&zr^OI;L z=o#dj!uDY}@fE;5@lcact}IdoZim;FCi|1&ec>J9Jwp~*R??s}+zg)qyg`Mv_BGDI zi+Y3$!!0=#>8Z?cu}b=3-Sw`ll?+_*&Lmsos&ng^Lq1%8|8Re9TtbJ!8a!2~uFP^q z7mw>h=(Fn>qBoxf`iKwF-y$SwpXtcz+UQ;A6!dJjKWfYBSi|)2X7N>B6j(Oxc8SA| zE2lD}1Cv6b@1$M{d_gUYWRsXu)R$r_{>(Q|^dig}>kK20W<5+m#10F+S=tX@hl9aq zGiB;6Ea38$4^-Huy&AWj5xILP{zamLMdVd(dU0a7XB3PsF0nglnxWOW=PNtP^8M~< zmZ0b;_5z){=F8@TYAgu#SnV4fjhU}%*z7df%7^D0;RSJ6_#oBKr~Ry`vqKRU9cenO zQKz)y=QLM&tCPp+`68^;SIV>$V6{0JN!dLMT+@&oluD8Yt?aD7y z9kgT{vZo_rBhn*+-byH|)O%KEICpQ94vr?~SmsPriR&adtE`+y9|ho3v*u3iT4?pO zYz%)HzF@^QQLI|q(*3ebrF6S|bJDP;RCB{?hktf<0;@2uuB9r&kkiVk{kWAg)UM>} z+Y#TEcw+)4CwL2OXMO5feRb(bahYvUeROaIIMHYj3KCmRy(+2PuC?iftvRGC@VFRR&J zPt30MJ8ZsMb@xL-Czx?)f*x+3%+G>tXk4iRT7RO(j zjT6Q~jU7!dE?2;Ju4~4kCPt5V&oy_KisbGW*w)%QO%D)`La)WX#JwwD+CiGxa@{$q z+v4l+BEQ4PQ^b>ht$mWUW_7qy?S9YQ9W62Z0pKGbSV5toQ^idY@1qOun>h+wS^yw< z4E8*VZ{KA1BOH%VB$9DIw6! zJVvf#xGyi(K)&|vTf_2~FA|)9b(TKmw6P;VGx8ZXHo&!1e&399^!nDFDuWbs$|W}d zfJ4|oM&4RpOq4^*%!F1$+e}l3*51S%zQ_OoxWM-28d^p=)a0nQ64>=$VLE*=gyS>6zH*sfifq=ovwD zj39bO8hQo}I(80vW}-hYAP)lk9harHE{7CE;Lq&fA#R|)wY5112xMnxM{CDOYi6kj zqGxAk2hlNr7#L{aDQK)5OszHSX-ut%|1k2G9f*#VmZgEYwSk!_(RaHVnr1fE+(6*> zjQ+m$9( zjh|B9v;pt&+8Wjx|063ur@dk4M}5pJWz5WsdH(R4f2{T4OMMD44J`xH?`!`TKh^5b z^go%>*5c4Lvoz7L<}onQ(9;2#o9c0a{t@|8%D=l52cMa-nI*i2Iy{VApnn(rZ?)hX z5O4n2f8c=_SeYAZIQ*>qlk_L;ADxs#*wo5e!&FO07{UXOpfxbi=Fp|nWnp7vW}wk! zfR6)AteSK*?95C$G%Om7?7A$B8rnJtv_DO2!%f3S87(bl zRwf!|W(F1-CT%u$8g>>PMjCAmMpjnl@4s41f0_PC{a;K=SQ@|wNe$zl(tI!d|7{lk z#>~HDC1hY_ZD#54Q3nxqZs1?u`uF(v_we>z z=ZDjCXlQ*O2zjhDY<0A`K>rl?k3#*e_otGf97?+o9!PS@S`#R*$Mv|JO0&axxU}@IN%Ns?|vFT9F3j}^e=J$=rDhzm;dg# z#x{SXXJVp*Um*JLuYccmz4?!OhctW| zsl&s<%*@El1-co1Q{#<+y^gUw!~i}_TYVpV=;_(n{?xiD`nReI|4?OMg9H7M-w(~7 zB!6oD;e0>M`F?oeAF=QmC44dq`g=C}FAn;5tN(Al`A3!hH?7{V^*fUrKz<$kjqBHZ zZb*OQx&h?Z!QZ%k&F6;nH?A8%ejWUc>(_j4NPpwH0p!=g-?)Cw=Z5q*t{Xsp9sG^! z*L-eBf8)9VKz<$k zjqBHZZb*OQx&h?Z!QZ%k&F6;nH?A8%ejWUc>(_j4NPpwH0p!=g-?)Cw=Z5q*t{Xsp z9sG^!*L-eBf8)9V zKz<$kjqBHZZb*OQx&h?Z!QZ%k&F6;nH?A8%ejWUc>(_j4NPpwH0p!=gU*bae$LB+J zOyM62wS#~9Q{>4tBK(u1L|Q^pVgP`N4FF*E5dgTH1_0c(0RZz=0AN!e0HEUm0G3 zGEEBhA4vu>rwwr&y-~g(>Egz#t;s85O=y|y7b=@lY~)t5D4lL8txX*=QO-o2#BYt< z5(*I(38(PIfV@L_2)a!)BYzTfFBI@7Sf1$R`+*Zi1_5nzO%2VSS>>53TzSz9qj(!|a81}Y`^;0gb&m}gJ?XwK4^G%sWD(?Za*A3jqG?np=d|k*UQp{h4!+F$XHu^ERI``zd zM5=04si|icW`Cg{c}Pm#TO=KRss`kFOujwtER?v!4cpD*zLV-AqWxm_^hLEjRr%>$ z6ybE)%i`ApkUgKPdVC%iuj{D@4p=7*K_7wNvuk}&aI;EDa}7*5buY(pkP$P3p+g2p z?=;|ptnpMncujvS%$sw8UXKfPMLfoMz9*nu+kfdcnL<9MrsEbtep1NO<3QSNz2KhX z<*6NIhABcMq|9s)*l?@~xj)s#uKrlaM|%d`p35X?UCEeGK9ID-9$RG3-r4bSN5K`e zC*Te`Gffn-lF3DQ#B4A^uhL_6*k~It1o@Vb76{a4|H>Ev#6Kw z!8%xGOITp%gN54TcusK$2}9C^Vd?Z7*e*Tr+S2bqNvmcncmh%Td6iS0Atl+oF}qF& z=cSm}5x$|V{`q>WN@BkWA`?}{ZPt2bwlac(*hBW7IH@pYJRbdm@h`zRdu6uACi)W~ za^U{m0^_PO``4axANW&OTLFz`vDA}kHXJnr_oiJVj2qZB^V=&!k$S*IiH~TIm)Qc*w%);q9J7Prz zJE?AaFOKBICg?&}t>2c_geODKTc{u#s&F!HH8bBN5!0j;(=2i8>``J3kGgi*vq1zQ zsgr^@=V3W#?*j&I@PX&C{Nbsf)4S%=SHKc;6%T|&Tjd!So!wpgMA8D~-D$sI zdI2GllI-=_x`KsiM-zjE06`bbCn&e;++WV@33P}*JHaeT*iUK22M-!-6*A5ieB6@3 zud>ddWsXR>3hg=OyTbS=vDusT+Jd`9k+(>#fQE5opqi}kB9n{puGA;T3%~n&n{o0`>-|Z-MshyH*#X(=BQLU2 zn|%7Be%D$3i&A9?=NLzpA)Y*0`2l=T+-_41C;;hLx2r^kz@REqkoaNPo8Bj=S(r-k zCuG7eA-7RCoGeZ)%_3uBGG>;~%?#7m&-D5<-C>2ba!*CL`EpI4>9dqEU<65UJL%}} z*~L>0Kr1qjQAmxswn**N%?JDDjTlGlCaKjh$OSVqdqlENs5~GTL1LFHe&|ZElYwgx z2t;@mB_G8)lh)tBiKVRWV+sx_Z6$cxfmnn+m`J)ID8j>+{APa{T6(c2rA1h3L4%^TK8a6`k z0@u-O{W4Wcfq2j{C3_wCy~AYpHz8h+KL zQ-zD~W23?=*Ujo@EiY?%1&V61Hck?bVa(LSHu%>|4m_`%s9#-171i!;nql;TYlzQ9 z#0ZTUcb=e_+hI(>wQ3fB~Go0N{lF7aDvYL+~gb&a{DrBealWUjMx1}Yb zUX6ASt2zexOEM}GNL}ZkiWI44F!)wmHQ5F}L3yQ9p`DqgHH6wZ0H zfRtyAwt2hXx^U6xb2ODv4&GP*m!0} zPsN$kK1WY_`gsD^R*e9J1l<5-*hae+*j9Yi^>ll4W8ej$tR<^ySn8L1 zsiw0^w@g118%pB&I)!rT_bc3mK#;Gy!z#m?JLXZPulcv%vr6yDbz8~NMof1{u+S=_ zCo>wUBzDf@g7HBmqs}(@grbn!A^enPq25EhKwxFCg5>3acdv4IKo*Lx3IYbrpM%5O zNWztXBy$zo!nM`d864&EJ++J#^pe)lCaBK@;tuToENt%CfLf6P5@!?SHc;T?TJb{g zJgS~5+JbokJ@I@;{F)|tTRozX4+05QSsrl@{BrME?%L2M+nIaE(CLLT9T&08R8Fw3 z?o&is1M3k}j_jot_Ptlvr+brrOCZ zOfXYNgxZik9(p;p%7*3j&|3EB!`r!=C#49rZWQ!a2WxD0Q37AZuZ`!nK#e{$%+*p? zN4w*b<*tijOooQlVhCYw-t@HtPua$kuDCctg?f7Ey@U88DWrO5rz2g3zX~Y$hF+f7 zSi11YrJ>bH4;GOhNhz$?^X)i{Zz+1Hz6AQ@`^76iD1=^K+J&n*3zHK`)vK2#9$R8N z#nKLS=s+NI<))7bdTo7D%$(H+E3(S7<|K~LaOvObOi!M~^~}_jIa#M4v^k7m@7RCy zJJ}mHSvZOv!X?5zt?WDtdKb-`HkBf?^R`phIrWOlw;XftLniDZ%}7YzAuZM@X#Rm% zTo4;bPo|KSt2ul4{n)Pm_2{ix!7pjN3TMt96F?H+*=Sc}W<26TjNC;+aqmYNbX3v@ znN}*Xk&4YzYs|L9ypRd^NMBoeN#ps?#AAdHFNAZ+LJ@sAB{SDPdghp3Ut8agstV8g zTsIgV{P0EW!^`4H*MVv>J#DpKn%Tj)5-c>9we6!ZXP4*MAx0c&%9Y*mc!WwD`^p4K zr;-HY(RfQOTGF$X^oLX#N8~mF_$Q4R3Fjdo+!%{|3YbHa9_@bQP~Jo|w^YgL3-=#m zj)v=0!KtR#q9$;hkAswEd<^w^<2igXCW3dJZUExCz<0jzvJ-`7fzg7(NSI4Ob;zFY zt>nF+eU2V+pXe+sYt|NQtsd}^3_U9u5O&(mnVO4px*s{#kd~bJ9`VEKxQ~Rb)e6NZ zWT%ToqV~1e?Ht>KS7%El__>~f7|N}*mn{K^65Terwbop%3$`v{?k-`Fq1{*2nh*G3 zpIv&*x90-#Z9%M2{Mc*Y;-f~Mg?o?-W2`Ni_6s1iKy}t?i?cEt8>xih0#rD zMJY_pNJx9s!s@{UVc zX!{-K4L>uk-Db`i@gajs3w1Wj4XP_EiP)uFb=SSO6^F;GJlx}_uc~$6aQa+RLj(0h z-D}RpE=tJgEOfU;D#-$!j2GgV$bcb2*N~@eeu}ax`!UPCRvA4IjbF^Rs5v-VAAuge0F(`=`awAK|1&%^IhGK#9 z>G=jPf@I7)MS>gcwmws(LXTBY%Ae*PzH-Sgm5t{f1(iWzh( zw`0t&paKT47kUDfj0>>+u~cFna@3HI!m}{bgBHCCBh13W)`BIAxmIwE(>HfLUGfm$ z=q?>f1h(qEY!Ga(0cCb!!>~T`JnQ&bHOqwDqP>#Oy)BDHiTJ52C*4#_4W~`3jD%6C zQse6j+-)@x+M*ic3$bdc$p#g<*LPhbMiV5Kt_CU};e5T%?=wPGbm{Rf$E&Tn+$vxO zy=TbGXduZuQ<0rsY@Mt(k~u~%gG*CpSbKa=wm*wC*T!ML#Q9Q)CTsTN>DJjs(L2d} zfk;D6uKD_pr}2Y_F2vZpbFAb~o4u1P{mS?KlfLew??nvzc5kr3I`_Xf$>J$U_kSIhOik`Q=U!6>Hav8ki?w!t8sLX~I0#)A`I#zq@>w|%^Q4y5^ zHRtY)b1E$;9obC6MhS17vc+9z)FSsX{?fosK8f3LC% z8FM=AUBe)zk{bEvsPMV%(T)t~N$z@nM+dQ(lG=dPbYmJf+a9`Eij{{x{SqcMaUHpZ}k}l~5f+sI)m?I)z zQHkC#Lm_OeNHAu@-!E@mZs>_GqgU)z;2a@^l4>6V1j1{OBQ=rDL{Mb>xmWaRG>({D z4x`&Ai%o=t=(Q-Q?$Hj4KM9kgr?#dIwpi{mmh(O1@KL?`W8&duJ?Yfgu>$h*>L?KI z)|d0gP|0MvbObKT9v7FDqloR2#^O5r);kiHfnZK+QKWm*OEZytb9*ET#jTmi)ydn} zeU$6XygW2KNykb!$>EQ9R^nlf2`xa9*WyX>vGw~>HN8wV#-UaxZoN*Cb;xL3jMr%& zc1~BxN#+SEBDP2H^_?+uRLke|4WHt$bMCEe1{hsv431djOpp+TIW`)#Ao{U*swV6& zQYD@4LGd(BI_{G6)$|SF5hURO50Wx%fS5s8KQC|g6x92D^|Ur$U$NyO2u%&YI^)c!ms#ffQi3R631XpEbDp|xPR$>@ zB}KEoByzXVYQ_T_t;&ms!^D_NQN#Y_^EeedA#GWCRRhTpB$PvY#A`B!TTypdHU`@^ zvQws04^kCa%BEZ(afL^viduq(eeE243m|1ec9r-G^_uk@H~ViV2VEPdcxbteZ)-mm z;a`*~5Qx^tH;{2XlJbjQQi5h-Jq(^IcwTg(l+Yp0e01gxfsjdtWDaH1Tq_x(JnFUJ ztS=|1f4176B?slJqs4Bi)^1TB7_>-SzuCJW5UnOwWsSL^LkExn4(N|i0Wz6XshrUC|Ad84PG z&OcE_Z+VzOQ$N5_rGH2Dsxntm2!atPT5yVHykBIt+whpcY}GD+YYp!?Fs|pEMtV3* z(Z}EW?gTcqCxO`)Ig98aMfav?EV*oD)u{8j8O9~GV%d{gHYi!xX13|ZG16l6y!?ft z(U$A*Cbsygs?FjAEZv-I!_ST@oMNWuE>Hu$5z!^!@$OTN8B$M6CU{s zMh3aocCzh(a$HzIcFH*>MxG?`PQsd*pHRI-Yc%!duzHUU2{pow{le?9xibib#Qif zt1x^|t>jor9h%B?b!NE@iio?63P7bp%nup!H#Az06rQ7ceRTF!0|-p+{#^JuVDr58 z_zqvvr~I>cYSa+Xlr8ES6Y>lx?1zih)`-_53R%mxcAdAidEVwE=|e9n2EkoA3ou# zy*#vfCMZ~WfQOn@?&BAi{0(p->yhnc2{wkd5I#ajeMH@Hk8>@$S{J+GqCqZPHjkBH z$o%o}b=d8B1Ex*!v-?_iMmhax=-umcviEe1_<{^fsD`aI zMRbe|PrM~29+)NuprZPu9o>1XAjnAqwDf#>0lv0glDMB=Qs;|`LSJUTw`_A{flhw+ zJXNgJL)i%}ayce~e{1ieW&E^DKd6{$sBkVw+2ATDARs7xzZt^-ud6BUR=#%nWE5LU zXBZ{4uo6_umSaS?Vk;&jS|3yFV1P1GR#TU}R64N~CB0FCr&wv^%2@7bueOuxQtn$F z8d`&?xNeakF3o3V!Y3iyZ)n3Y{Pcs^IZaFy0SgRzBwE;6e0sK0FuylMGPA}y#BE~zqN!Xq4&<)W>=I$Sa6 ztmhzyrC06IKMHvXEh)_#pf|SVLQpRN(W*AXiSIV#exyWImaq$oRi3ML{T91py=K{2PbYTY4TN zkgsH=!vd9&2DmQ$TCUaapQC~G>vmk`+LGAOl>+cxjUz3=wT*Sj%EbjHk&$v;%5}`1 zW0_8oRik=W^+!`{FD@l|bn;EDM`lhk=+BLkR+6q@$+!dh*+H>-gt1~(u=#T|3tgv7 z)l@Pd!?}@Y z?9C_e#3yjoYAEXp52!cGi+Pw=~jHn}yi;4(i8TJ3CyjrwHj!AM6)^>h^*?D)!|cpA@{&8LCxZ?YuY*TS*SoFwcO(#o*cU#1Wbboro$b|6 zl6~Tnb3I`=8>5vT$(Y)hi9vILsoZW{_a^4Jd^Hc;b2T_g5o2_3K0ribB#*_#=OVz5 zBS~?Y^7FApfG8-E`)#sAuI85{-IS}w?lLgAl@!{Jg$+ou+Tw`p%%IXP0jP~QEJqpnP7@`&R^MCnSb#N|PNgv*Mv zz5$9XZO!%5D2bDbc&Z;Oha$hE_Xm8k%xYn%sLjOa5B67c-~$F9*GKM1N|-}tFMPvE zAo=RdW>$@I=`0g)Ch;VoTEZbUCNA#7Y}~1~-H6B=YC9`d-1dzZylTCDUJ`7V$4&F^ z#W)s9)CWqaA}jgYHV=FQ`r-FJ%h7tw&F#NL(!fOUX*qj5v1g)@ z1rr1doXeUHI`l5T47F?;e@o)#&mxG7+rZJ%^YiS)z)vCrzJ^Fiu(arv*YzF8Rtn}U ztiMEs2Oz*r?&i1#L^kaRSYf?t~+`I?4MGj`q;{p5Ol?K zIeZ%!COpG_%CUwt?KoeYUgpe(zL=Owl(Gw@W{!MoYgA$MsZgytN{!u`AvT@oQgWrA z3OiA8|9PWp)GUuv=tFY!W<$``LcOfc-N{YCCN6MQ$Ps(Y?apGz*$O_Gd46alA$+%f z#in-2BC(2lFEz_Yx=tuN)MKCpvkBaLDoR^R=1xwaqH!6nR1=|gi4mFPiIOedDFVwQ zE&4@gp{n#S))S2bbF*T z2LfqtQ;1p+4Pl*rYa1h4y?7g#qOHk*b*euw9mSnq)|s{)>0KyU_TRM!y z%G@fMW6n{`{LkkdV1n1r%!;2U=S*j!iuicTt5Ms}Y$)eH+Vw8yU7RHtjwHyFp~83_ zmaK+Rzm%b}SZsQ&lOFB;bxvcrp()hEz(tXYj0IO%;M^r}&>{ZNq{TB+mI)VU%LbEm z)YC4Yiq8EGXe{`GiMButq^VoDF=`Z|hVjG9Z899g0}iq~GW2Tv?zD|7rrJ zYX?cQBV(x?pZR{JAZM1?@$+zyS>=hsJxR@VA4K+To84>f4)}w1T<{XkkEeV4saqx{ zPOtbO?Havaah?Y-q^9d}lM@jAOLu1vefcF;%8#VPZv|Ao4Z+3kU8#mO^ zJ#2J~uXx<~U(UtkWQ&MnJjjK5&Wv<}K6!h`zsnhRw=`xE=+S<%V(BTDXWRS$@BB)kWRxRy-AcOuttfe2oLGIx-ht|ywV z9l?Y%G<$0LPVYF+oM|B*xh!$;4YJ3Nr^)y-r!M!&o|S*xy2NCyI)YT#3*r|i)+J$jS=II{oL1a{p)CTFqP$_vV6x49!{-S^9g(ldZ zyH&5USt({waMWC;_lYEz;|5jDd8zmHe9Mm#knX9O|0qFV_4}M|^ZCSXFDzO|)U&hh zbUDafc{NdxnX!F*Wu=DsZJk?{3bWISC+0m@Z$lgFsNy`Qyd~PiCa0$?g}7j+ONFjY z;CaG5D0w-ye0B0S4`e?vq40S7c`ncQ{A@?{^`)kAEm>g485f%iOMDUlV9JM@oMmKl zSZD|rEbvHh>-3BLymiw0{*Q*ZnEmDrgYcu!f(&{aI;QP zAOisMMnDYsnT7_+KCnUGtHgQtaG z>PH&yDd1Y`v1CAC+(chnpEYn`1!Z}U%p^0mpALyMUhmQY8ank^;a}Xyz(*$QSN-kr$Jc zD{#2Vl7jrCBWms<-G?li|2dzi_vbUl0#SgneQ9eX9uUG z-34){<&CPmMBP%y{#)9>t>h|XoBd1a9uKG?hR7ijABj_%Myq?r;jH~TMPR{~Aowwq zw5!XH58rOdRcI?byA}7 z<;UqF_vdK(^^(G^W@(v!Z@8Mi%`}>?Xl1Dr)&ipWgsVVAj9<^8%uX??@9;U?0lc)W zpn{k1bN1A1Z7BT92ilU@v>=csW0_bik}a!@fIHH}To<#w=GTgr(!OJPUUbNJ0dDEv z@`>Q=4PQ55bq#LXoo&h9&v6rKTXc;plO~^~z{K^~FI$3E^O>;;6l%lURGk8z#hVu1 z?|Uq`p+3VZOd;$mgMHI)PN4djN1D%*)Qm?6`3m86`T>5U@jZJ|fXgH`itnJ8{UeF@ zLqOd#HM9F3j{pFX#^N7sBSKq%j)r;mp8z`IcbD?9s`6 zN%)zd&z65wFZOQN`@GwnVupTh-a_$vMKBSDZ9>LO0nk(P?%MnrGJ)r}0Sz5h*d27Ilp7zPFwFc~cZfn1eoW`-Ho}kA zc*pP5dy3VX#3>*_7JCk#OHWz3JnDtwi%4KS^$rVoj<$L&q7-IfIYFAwCa;s8J zp>y2l11sXa_p@N7k#yGG7rjCZM3^%wYgDJ<5)`<>h#{_7hh#iUxT1H% zh3d7>DLq@y7=w3n;I&#Al=y^0nsoRoO^x9y+&5%Tm{8z6eL%1mW{h_SO`a-6^Vz4I ztYs6#pf+-rSGVu4S!H89WbuB(!!J^xorSPeseKlG7B0c?iWe@DBDq)7!5Bj1`RY7= z@liPW0Cqf2h(tKe!8_ec7z?x)Yj(j2oVVat-V~OB*QrM5QaRM7AP%$@N62E%`m8TFdatQRh z+YXUxdKk|(p?^>lhcJFvJdVxnFG=kwtU1ug4Q5%X5g)X z724i7ie6yk%GO83#_xCQ|8`N(cN)dn`I{9|#|SulF57aandF%%T7nEeQ^xR|5#wyx zDYON`?*i~k-G#!O@zF+gS(R5hMBBA{l`U8YQ+bI;WWkQAG%ss`9Fx(`6nA|cCPNb{ zIStyu+_|3KHMooULpi*fw=9~q))s{oSrpkxusGn3(QM(gQwIYp{1^qx?3L^Y@lNT= z6nvj7s$Cp!$y-NmDKsXUS1 zKMr!0Jf!-eP*Lppo?{Z_F%l|#Qmhm;&5lOuOzn}HPw0P{;Q}nfo8^Vip%|^#N;;=Q z25r}n5-d#!UN{8Zck*iKY4P?JU0)s*dr1Am6ouYpA|_Mq+nj!MRDSsJnk82X(~<+b z)T#Qa^?T`UrQHL2Ig~!}89|R#5oFDv{X~gZ1i4_2v#sR0uw)PR9h3a)jJMf|z2e4w z;Ib3nR4}nZr-zB>{pXxZgj3etQA_T_O8Klthv5~AFzuCqd(h$?Tbw&b2)OOXin+6P z6Bno|Kg#hY!+sibNEZB@7?`qpYAUF@6HP0YkN@-GnD0kfUebh&Qa_z#si`krx~Fo^ z7VO=Wx88!dMe|^34aA^`nyL$7MkBj;K(&bFDai;l89~iIXpyLR{%On_gZ2K8Y23FS z9E5E~iedOt>&seUY4?%!`U}XJ`)gEGNe1hl9d>+wd-?qb&$wZSC7$R?hPJ{LP_Nz@ z0@wM8;gw9?_^DesgH+vX7aAYT$bGya{=khD+x7|#y#egrbGs8N9#h`j&F2YdU;b#? zCEwP&lAVN9mG86`e)977=x5|=mN@?|~ESSK-} zH|VGeu(`wsx3|RpaviQH+-k-|0~- z!_*pN@{&SQ$d2ye4$i+yRaPY@0U`+29N3)!=_NwFKiJ@zgav!^n)y%62B4v8G0M!q zmsy%jtE#ZDu;c^R5AfLz{K~yZRyTL6_92tuE-_{477;%1nk z;ULWF`gj+bDcQzVk< z#FO_F`QuAWT1LhB83nDFzHqhNDjIIw>h|)>v4-xz0GD;EjG%i3T@v_W@h4+GpL(|m zM0;v+sxjKm91pVS7z&di`k1!jHeKnc*2lgF8V-OTj)U*Z=b1zVzH1EAW-Nm>85%E{&XUG+ zYpt1i&m`g>k=BxLP1Qezt`u;!n6Q8GELfZp{_6a)u{-5){mvs`#Sc>7#BmTpXZKDR zC-g8O^Xv6x6Lu3;B!jYcaTqZVu%a;{yejf$6pr<5S7IWTm4fYFwH6&c?6x!jz7v;)Phj;aQ2Zkff&!cNl)tnJ@Nd}HkBXXbY*n)O%@A(j) zJG4z$KXMMIag7tMNW$M^lLw-0ebVQSrji9ND`;})Gbns8E}nW78Uo=V9{Y0-Z284g z8-m*X@i~62vbCWvS7z*k+nWxpP}Jp_2)KFphK~;XphUwg$5Zb=2UV@xGdrs#KW~(9 zoaeKo+9ZXBpb5XwK$&pzb660coSZm*@9Q1fn25sqnE3M_OnUGT&~P1Oi?%c1_>e@(^O3+(P0SxJoIb=B)?ejrh#P=lw|AQWtI#!mh^SjdE1GRx`Zb7nsrTvXJJpa<8} z%=_WQ6+_k5Vmr}CYQczF!tY|ALxQKdJfN7Ea_9F}{}9gACt zghJ_NcIqa!l#ecVsR%oL$oOjMVm63LnGV1$0uo-nQdV4@(le*xe9i6*A>Q&pmU#bZ z|5NA^UWaNLS7&jtVdqH>lM)1j_+4}W%01Jo9lk(V%8;4HUOP7YK)=m7?}UiPR!+m% zq+X#bUpGW6YoshF<9eso9GcPh$zm9r@%bV}Ya@9u!=C3^qlL=i2>K|)L z?7z39yKeAl;)S?CF^TOqZnrU=<;Dgxp1T``L$#AVFm=@*f0kZI7UP_C#a#E>9y^T7 zYR5C1n^J+4m`HOW{IE6kIC!Ie8jllQ(c0X=Qj&|}WLU92Sv#e0a(a~Upb~QSaQxBr z(w#t31`r9$0Ac((!AZmy(1u}IYbZ48>DDuVdIHkDli}-`7lZSJQTFvtAFmvDw`pHB z`Z%!=hU%TUgPo0f8}!viTmtvPU_VF69bYv% znp`kELboq50=Nx7?@CRQ;{+r7li${Jfqh{+Q3J*$Z!Mfx*W5)PoO+h4-G%xn9?^5t zXNYj9D3(>JRADB$xR!miK+N;P{oUM<(xO`4xb4@9)5oPdW82(1F&8TEIcv*u6sOAI z!GVI1E>zCopt0CFMMLoYe7iQ=0TgyN`N3E_UACp3D?n+b^_@>&rQ5YL+q&ozR8BnW zg3nx>dH<)i^NMP!-PgUJiUmXrMWk!!UFk>@30**XC(;F^S81^Vp-Pt;=~5%T6QxRx zKN#z^LT`~P{K-&^cH|J9`jCUWb*B?*)SEgu@! zGeVut#{xo+_S#;*t&VSZOlrn7&32yvr?7x(@;M~JXv~ysIQ6Nc_E?;sh~c#Tvw^|s8~H^DJ*9P;b)gOSaeAK4~h2u!72lmliOC^V)% zAEQ*`M@L?2$p8&4PcD$RbpMHAWJZP#zZJcrG2O_zvd)N7c7u+G(>i(&E?N2ph3pj^ zCwd~J5_i^}#LW9$-iEpywO_hqiHATEMkZ(vyF!%qJt}cu9Kcv+U|(-<@v>)y6)eyQ ztWf;1xftB6q4Zfd9iJ70^9!s{AqGNucgI#wgI199y@$3!{j4^lrx5mh61WA?A? z7a(TzwrNdJ*sKFOTsHkwfNNYx2(64$wa0x39InWBZWO#WUseMc3IB|v~gsK{&U^bK_?*()p%zLMK(X&ivFHkZTflH{li?1+&J@v zuMbN%OuyMTM$}tIy98pfwZS(g(WH@SfYu-V=phPvabjLlZ zrX0ZUM1|{)Y{vuhFuZ5_d6$+)4w?+P+o?WbT&(4er$cuwXYcgDJQw*~3##|_D*#{n zel45&2{51^4khTEB>Vb)ml9)qJDzZ20W7NKH72?S%7W>$Oj*ejseeRIw9EZ_kdAgfk5yWa&FGsZ z*P;HmN<&8Pp*toN!;Fi*aX;{V@)$D5n-n!Ry2n-cDujbReWTZa@!2kuTy7Q@SZiZg zTSGxlQ396O1=SBY(^saGzJIP8ZL3l=FRxpVsk-g&#_Ho1?l>DXmD~kqW*T3#g_2HsyfY)-w1f7h##KnU;VFm}6$%v>ww*p7UlJFbco~v@Xfh?8>e)!I z?d*;ht<>80_xh8%aVy$rLp5U8fHtKZ0@`#RmAGcEAZ@t!Lk{5_bOP5=jWEUXt2&$q zL39O3_^1W-;7YwfTEieV=k4y-iKPncUC?zF5mVJqyeH*a5f<=tbF1);=pY7upri{| z`jqh9VTmBIU_~R`@>#x)omyJk>Nvry#1>+&a4U?u(I7q|U$T{tNWcuH(}|a6QG83% zx7$>Gagyq(W|+Wo&dMp`bgVkV7ceLnP5I@E^7K4dfHs~k=u3|=RIKn^1cQSKHm-T# z0-d;tlnvh3OJ1vbEtf7Q(^$U(gUxtj8EIekDj!KV8T6u?HrJN zc#oP@Ki|@1P(J4oGF>+eApj^Y39NsC><&`G{KD8k%B0s9PS#Rxc9GodTig~X-Uu3B z31i%1{#b0(ZKMmiKAu$}+w+;GJ^Oj+jO9z8Kzb!e%uW0fiQ%yHb03&PH%Z;zd6LC= zD1v(2xD0c5`=GC$aOuLAWI5S90Z7w!GdIg< z%X04RSqeJ*29sw{&2-2U8@!?RF!Wlt)`>e6UhjMW{KC8cIOBdnV2FWL`vJ=|)ccs+d!(95QV zh8a~iL9u0Gi!|&d=uZi&5`s4u&n7N-!ykXTMQMVP=OpCxkx8B}1c@2rhwH+*nCCLT zhFcfe6`UGDKOab7^rp7#Fb0<)tw2C^A7T4Mv+tVj`~9?B-fgGJ-M%a9vZe<@`1xhj z{GxjL8g6^1 zB7!Q*na7hZ_4!YCa3IPs2L?rxgt|iR@r2q_<{zOqCh6O137hlS8*McDYinUj*&B84 zzLK5cAUC(A#tCNhq|2!!6*gRL=7S;RcO9m;B*Eq2;1|V6iL{LH{DMUk5LbC&Xzs`V z324)&I^9!hN~RMkz@26B99K!Ds3wCUXm$UX8!>`;;6UU~(M515e!R2TIIVoJf(tjRDQ#HST-kmz2$(!+w+~8u zFPYz=(Pz1cfA1(z`!=N~$r`FOOEzY`z7{G_#)3mAO4YZ1fG_Mh1k#g*7kKxyQ; zku&j$gn^^qOOb@GE_BW!DDCXq(3z7As1{wLK(K6y)W6Z4>YoR%`}Z z%ZF_Nq0NDj^h5EcfZqP<1^ImZZDB+dNMuzMY9N+z*L51#e;~O$dU8pWf|f$AyD!f) zZ@}*+ds;KerVX7O%PmpzUX2sX(AHwQ+NVF2uw{BAwkEH4IzNO^9??!^<xk9II1zXUZknpGbN)&T#PN? zoDL*Bn!q0%q&bf~PyRSJ>}BX=$?>t=j#NC$V*%+)Juiwkn23Q5g!f2He8O4%FbSoVXO9J8>@aDG&P@*U3diIaW^+IVdmOg z_6C9n`Fg*D2eK?5w_kT_ql{uiet5Y@(5S26vHp;dA^NMIDsoU9tUhC z;YT-_>OxN=B8Bu_y-QEOMdu~ZH&{I>wg}t*nz@)p`yQA`JnU0v9%TsOI2-(dvh)(6 zb$`=ByMt|{u9QX2wozMVzxN4o1k9$y|EUTD*n$@~PNU1FOMnBK1{7ssY4qEKA6^m9 zsf)j@b(Ry30QL-*!i7AGdHua+FZsYHRA(GONoGn?7in;~VvWOa{%FkEifsvBZ1P14 z+sBY>sevE67VjaDY&{H0d$VWPjPP|je-l+iNx{iD?8>i3_*vHoQ=1P)x_BJ7aKHuI z#m}N_LiM~MZwXPbWx#xEDQ2WPF_y_^KF1WeH*i0n&J3AI3W~&J~iQzQ*e)CK(`&SJm7rN!^I6ZNkdrcBGpP`_m z4B0I9n6L#%Q}g&q*Rc#Bpgy2XW#%!wxuhOw;c!4w_t>=$@oX4@&e}Qagxz#F><$4! zok)^ z5-fb!+kcOOJYa`4?UhoUvGb=L9;&H-#3LGe&f^hSBldEg9qm`Gtj>fja^#CKiCKxv zq2!R&`y%vvEMX|qVAx*%xBfhjl&iF)#Wz6th|1x!VsD=A+Q^8AqqhNe%tO|EtWVv* zLSAn2iD*b@4BLahqO_3>Q|L2SBls{t$i;OzZ*z1&7NjdyP^QgA%U48y8M7{4I{t; z)1RbMtE<|fbqZAase3ZSI#L(+@96@NH^WntBY`_~P!YJc%>Bpf^(l9NvUbcnH(?N{ zTyx@Ji;eiQVmduZiAo?b9}^g?WqekRVI)tVKM=OR8~qW%TKS5DhOXoHe5srwkU^{{%|@Buwd3K>`au#k2uRNqA$&C8?WtI+Y%KPDlrv~9T@G+{Uwd1 zj-0u%Z?C?VSSEbt)exxLd|c#9k-2#SPaT8Zm?0vFE>tlnPc})Zs`35 zVn+MuIg)ngsSNfA^N2{Rd2=Sc;)aMgoJ3G%G$M=6SK{N0XFmryJio)0#!J9PE-D3j z!q?#;a}Nk}_~F)5zE5oYCs)WOIfkk?n6NkW^s0zEv+Xx9`P_Dg4~YVIp-@Kk6lteQy&bhe)VF;_)ayd zr#uo>p7c6htW~Z4<(w98uWIkQ+B|V$zYn``ftZPSz|Q+_Wm|+ri^Px}?7j$$-Ls5@ z;HRMf;vnRrvhH}{Rk~y{>wx1iiGT-{vBj$$DK}V8(!Vi|Jo(6>9;3I_x%KYQxjoF& zrF-2`xV{wF|m0)CI=HW9`-V-ewfefJbNWI>huZ>I-}H#upD0SJ>9 zL)F=&3vr6c>x_L44-`+b5Ni8*eIMeHW4n`Xbq7;iPT7e-kiq0(E`>3dv39+p|A4d$ z79d(Eu@o~s=P)`^tF-t#;gEDXURr*r6zgvzsV=z~njJ9|8t4xO7Zmu#(AAemXMlTk zXK7lnFSm-Omx?rsQ)!~Y`Afx~=C4$ll2po1fYt!VY z#XvVnFt^+IWFhpf$CRXD8}FBlhnO$ATr|=@hfvec@b+uD zM|*b5U0vn&>-#DP6sTp~DUUew3QODpx9^SXRW)xNQ6)8*nl|bu5wz&#Hkk)@Y>MGPQxb-jLRQ@r0v|eT z`wiZ~SE8}#K*En3c0xmnz3n$kW|BPcZToH1h9cP|U2XpME<%=zEMp=^Gc+Ukd*w5Hg>Y`#hrNpY0s)MC;dL#Z6C%N%vNcr z!lf<5OQ`_C>(rcwx<+R@=w2`No4yLiq1fW2cAXoxCV6Q$Z%QS4#t>!63_KzvEllW) zGn9>Ssn_`_xa%9|uT{qZ-Kg4p4q5lErChE7BaLQIz3af@`?lv z$6M-0c2?655yO>u1yPt6fpE1H4-gP>s9t$+d@((wnh>>`Y?gn=Hw|^)K1;5ZVLdiY zj11P(NjG)N(dDX85r!6j=Sf;AmP*_`-F*Z8M+FyKG6Swa<^**~u!qmmCIMqO-#@?B zZ47IkFw%HwNL1pYryEVQS?!LByT@QPe`W z^|KJ2;5Ay2~(# zUBAjUIO06CVEkp)s$XRAqdJQqF;~ML|-l_zEP&vrz}%OiYe1=7V6?=KI&N1XU19;gj!Rbw zi2We(ZR%)H`S?w!G%dp5hhybx(Fx^_s~(HSu`fF|b@tPSK$4fMwsI`I)8J>8vNv?6 zsj%4VgsA+ZU$(s;P0_IRpdn&76t1Qa6CH?{uVBJ)dZ)Dqb?9ESfX8H%^EoMQXU653 zBa|yOQ}o@jBcr;9=^Wy(cATMn6hK0iZJ6L60Izx;gR+ZomWFo>hpL$TKfJ&1*r`gwmz?9j9;gK?PLzmk_dL`x*3emFe5%w7R#1Sn9?yS+0m0|R zuWvqcM%rws9a*4EPZKVD8-zkg*!?T}c zGvk8yjT-t4^SkqOI-r`TMf1m9JGGtp?0sZYt0}fdGnGS9Jgk{N@}sd=O$@KL|B<%& zKrC0CVWT3+SeLYflR)!m-f;u=z2Rc4MkMt;DQFJxSdI;b4k{YWbx~$@lXPJ4!+{1Y zD+5S=rhdOOi)MTL_H(_?5Dw|zs`gG6U|)AxzO@vs(6~6241iN!__TWuZ_6-60?skl zZH-3m-ewP>4dvFfosWX)A?~JkM54D-D9SmbRu_n$U zOCgv0h~y5&oQo2M8paBEr?1A>`Kx%IQnjHUmLT7QClkD1?5fMyCYjX-(_Li?Zp<2M zfBm-rO$Su*>M?<@9!{jV&wsY6k-2k6T{r2pPEk{y-;5~U&Mvh1QfQ7xrod?y{P&QFieG9OX z0d@v_mYpbloCtA=KbH|=zJXJ?vvBt9K{CS)I2Km!eUx%@T@shKP zRCwW!27FlKd_eVYD)2II*S|btJ*J6N?PL+)`u&Vo8<`x7QKHkz6BdcGBMYLq9g!}9 zdhaZG|NgKB#Oh^Zu`4n>L`OqEg{+=U??XCrU{MkK>Rrfp8F$B%FDV>msB;=l(t>a1 zvua&u(5?QS)t$B^vi|`4lLLmbMDwmUQO>Xq+INXvR8f#UeNp)4KbU_4Q56@@(vE)G zbu3QlUm@d{;pY~<4A+e7m|X-U*>eJ*4);h=WWzy)D-j6i8@wr_5pZ3hi*ntPn1jg? z00VN4c&%25aH-*9tIieOq&kdF@{u8GUwEXfMj`@kF{0mIU*{3jCmG{a0AL2?^jB%a z{)_Tw?HviZ&ZIi~dHpIg$_^32JM#5h@5Pq=)_d87IsMBW94XOBh< zH;Lz3D${kB7Au9x{Ao8xZv?rsReiS#cRb zXoxg8Q8#wX`?w$6K^Q;=WB>u26Hs2vYteRoY2A0WcJ_qX^l_QEFFO^9ldr$2$r&Pb zxub-p2D3o#lt0^sgkVB>`}|^A?t)*3>^F+}*NS%cXlbK^#`7wPRed|s)O;+a6BVxb zx%WbJt8WU?8PkMzFn1GQK80IZE#PmIB>^8mBc5PjN*a<_%Kh)sO_Ynst3KK}GL%7CS(<58< zAw_M$q@1hv`#|Q2KyayXx9x43Fu#G}wqluZC=HykLy;t~0WZB~o)@NnAEfO0VDVhD z7T-($<+$EAi6wiYd1ogoT`@$(+Zol9Nsw%I3xSWuTcS z+rR2J>`wpI->^Gt&3C_Knw!8x~>|GEMD4N~HGAX~ik4|P* zA;n@)bMnsl^hCh})BiCd^&sGrVHP_%ychYR|I9XO-fx8c*ezEhSEF+mAfMF_!c;#? z7+7xrG+wL3+Wk+KzZOCTHgWY61N_RsDONLv(4=0%-X$g5hu6+4G-JN@rEF$|Ywu!a z>mE99e$tflx&*#lo+7i3w!k~*SpPq8w!;jyxA8^8>}`d>nR{RzEB`uwl(~OW=9_>)cs*Ew?49 z!vzAd3gPp>?#I-SJ1I@yyex(Akx&09v@u%#KV$+=y)d84B%Nc{;Z_3c=;U^h8`OEX z4r$^5O6bDRJ%i{qd3!4N`T5a^Yalx^Bd+l_NGZG}@)_*z2776;65lgoHK-o0^suH7 zX2=O{ckvkP%POaO0!}h}1Zg4!I*hSjnEEFOn|NswDUd5mlc8kU{hyUE%*5XmHdmp4 zqp+QXD)2XoPB!Ml^7M!eAI~g2n_)@RDt&toHrizC16Pll1rd!PNl~uz&D!plsQ4M6 zPWBFj(eDr7i>~79*TSU?yBcpiELNZcAY)N=e)^K6@lf1~{UKKkjL3|k*5@NP)4vI5 zerzOsFmT-;y4eF1H0NKlcIyu1?<|J(b%zpb)+smk$Vv{pPvGAJ+J%pq=)4*%Zc*hl zEghY!rP)<6604ME&qVw{JlBr$yKD7o?+Iw*x)sg2fMl#3yoSmM`WAyx-o{%ScHBULryAi7QYXe4kv*9Nfh z(skNu0nfeXl2fXz#1>y08+7vP5;2gLYr2kA84^7(9*Tz_+St_Jy=WI)vXgk|7~q(1 z_xPM-2HUkddKKnZ6E&8}HMfy@s!yz(L&@*mD_zMU10VPBXZIEND}lZsoR%v(*QgrW zs?CVHL0M0i-es+@0vRKYD4owW@*MO{4#9)YE4Zso>$5iGs!SLMeXTPHX88%-)!azu z-&C=6r}B5;;lwI^v?JZu@qkIO`-=(1}OjG*v*{H0anP`_jFSQj*J3Ya-VgB88= zW(wWSB>U9fQ*|+EQRUoO%=eU~cnz&p(Nya?$#EBa{+^WBv2-v|F3`WRWf`I$gg}Y! zp7s`QBF2N|1%yGRTPXT2>}_ak$9djuk`wYWZ;wpI*X|>R6gi?3sK$C5a9K5?0@pow zI_8exqi+g(@am|R?CNW~QzL;9HH2a606oroP8FW*!1FOxui&d~u|H2=M*~}pCq-tic-lI; zD$}upM@MH>P!Z8aEqz!~u3MrNLlsgBKqx=dWZM@OJ)$sXB@gpCfRQWPmw>Ogz1c;w z1G1WzL>@!Fyl|d|dT&&!rMwa}2$vsFt)&8q3;9s6`dQW~{(CaZ0NZ<94gfYY3moE( zwv(z-1fJ{rsVx7B>2$XvwFnf2enxfc!ETRj@UG^P`|0GA98;Qz2xIYm$xk18OPIOJ zxOk8i91cNOeH$F&_)RB#NJVAt|K>?met+}i>;C#!Q6Zu=6npP?G&NV_)S4pwa=v`e z!+L&$uH(=+)qtEOlJ-BeE!utMbK2G}lB*L>3-&_||*>}5_u(Au5m!@a}OaH5SHx4C;$H|M;jG<)@b+dvt&j%wXxPy}MWXqksy5x`cbQLQ0zTo=CP;1sJdCP%W& zRcOq@nvcx+FK|_BdDGiVCM(x;$*7#i@&(V7K87eg)8zX|VYiiR`epAD^vln^#UzBf z!_Zq`-bwRRSFO>7WquQcK$@VJDb5uzkZILvzHUUm197L_@8JqFIDn175HL9j;o!cX$^}V`MozR5XY(99I*WNVag(>{ikU{0OPp5Lu=bI zA>f%H;b>JzcyCx_Pp3HXc-Paf8A!>Q7dBbXoUV5<_j;0rscJ#HE5la9Cf_rhBdCJ3 zt}Fqun623C32dO%NE;tDIiCg>0nlk2$JYU!MzywoZ!e{fDjWSt%3xymI4EJn-ktwy z9WBld$V}4_)Hc1|)jpFx8%)=|^qnCl^bl3M!C>pe{3VGYHYW!$n8!m~rq63l)$29JM&n(hZ7tK8RujM69OkDn5oKRdZOLX1 zADNm|-*uYLxI{Tnb$9$P`5bB%AfLm{N%;9F^~H_oM+E42a(L_@pKIGUV!4jVqv+pJ zZ~X^zijMz`Gcq9C7hXLs&at@)fZfa{y7WdFP~gGp!oou?cM}X}L({I%F1z|5P{;#% zq4Pn7+Fm#*(_xI7(`Z4zbY?$>RY3wtZ7V(|Sn{g;u>*jX8@Ffn+ggt8mD*2c`-{%H z^V5?0!{&!W?G*bgRM$9G#h+;~(M9|Za*e7yklHh!dAcO2+_btp#>0#JSSvPs#{yw9 z)$K~PJ2>=}*N#p~LRcl+(@nnyvXaluO371QAG)%y;IDaIioqZKqBMaxG9eHrqJ{!R^G6jV+pHRDs0 z`HkbpayO;@jyxK#HM)6Y5@R+?r)~hT)gL` zK?1mnr;?-}J`{ZDuhL%z{U*P(M6$|C+qmB`LLH&>G8Zw(ktCrA7wvM7@=;AeBoHi~ zg@k0z*E0a1eXn3tP8|GlvTX{J*mq*;tg53k|&qL8>HCA1B2~TO~SS@ zH|K3HzI2|}AlPP85$&DsvCgnRTqEQ6=Uk)1nCiQ?rIp&-a}y2oA{TQ5+eWPsG9FA# z(w@TziXDaOI{%Uw^ z>2q7|Lw9DztE!Mp2CCw#?l~&0lBzwo)cN{Q*mG@Sm;c(UY(bB8IAC=0TSy5!IK8!y zzRrBkGpbxtxsJ6g9I7OS0}jlE4Y-dRK=97byI<@2iQQwq-yzKTSlIt*n$gWeXNDYq z0~%m=p{Fs;t>Cf7lg5<~GxII)G%GV}j8-#a z#bI5juYYBZBKHvS`vI#J>YOn8e}ev;)D>{l=5%n35XVOQtca$={k!r$9nVoqnz9Y@ ziHo+}e?UXxFSNSFePHWhR81(;dIFjK^~L^KW{6_dYLSLRTPquLrxFV7Iu2dfgIEcQ zNET;yetjh||uJ{CoVyZ-c@+hU}D$qa#5X&IUgXFO7Q@?Qv^J6F#?RujT;vBtPo zdeCw|%(HU`vs^ksN5^Q(hQMYDsq{$r_DuYPtP^(yXf~ewoyQ%0M|g>O1~pTT8^Hen zyDT@IQ(L0^!RKRanMon@+0=`F(yzaN`M>}3MF5AqTj1c^QABuSaz~XIa|4%^PGPC< z*Vxv5eP%xKp$r6)n@*}Lkz^Hj!hdi5U;^QG**#DHUz2OskVCA_{&^X5vPnu19uS>6 z04_a6xsiHG2(DLT*ys6FscXkjfVR;a6yhSu)v3<~FeI^Y&UfO(rZNBFHl3W#fwbm| zUcdUuBli#XpZh~aS}>g6_+5`54_5l#>|)Wq zBu5NR78iD#edWM#<04iX@hjr$+3&B|`1>o8T*E450a+^1EtTI{s>_E=yU5`e66PkZ zjDNmpVtDDK40$?a5)neCm^l_R)MQzI7-wBFqX601pflIKQ{5MJi6X=OF9Oa5lXMsC zCA89~nFj%4R!KM5m(N(LR*g33;z`J)DH`ZieaL}JcH)Iv*;KH?yYT8(4}tjMKiRJG zmzv%7(=Ud=x67UyA8-Jf*V&!@`!^RnrDy5LPUlgW#aiFZ^ImiN7H>+kOLOMz!?9W5 zVXm|v|Hf7dZ^eV6*H-PZ<|D1dZeI_Na{4rG&n9)}sy0o+5=F8@qZb9|Z-=%W2NwH^ zusHqBC7lGTXVtPL7m`TrERwXCsHBw4KKI<$kADmQ~5tmW8NnHyQ_Wxm4 z{`Q_L9EFbAS^B$zeq?g--wWvSN>*Y9`42##wlmlkeE$Q~Y&rJv{5`@Ezvs&m-;?tl zt6}#vRh+(5bFZ`oa8FFFD>XO7}oh-2aWYZifE+w=ouChk{B%zQcaY6 z(;m2l;`&Gw{rbIhT)GBvc<{wJ;N5e6y$j49&pdg(^?&5*8xjGBqhD6e9C-d%!W{ty zT~M?|b$S_8Uhm)P$nWNn95UQ1sl2$6{=S(AH^N#0f2XPJf6w?BJO2%4khgYN``@_? zyc3Qb`w8p4v{L66x{SSKo_K`N19sE4CfKvp*ofKHvL}CQrX$czCqbz?b(wHtz6&8# zLw>|mP){V+;;t9xD<4%`>4Ie^H#LHcICUIX;XfXQ=WN9L_oL)mZxO2$Ta;+ut_R%% zZ_295*MH;?aUBJ1kiEmR=uvbMjY)Vh!&yLwc}E7>9DtY?IE=KLOYr?5^IfeRI8G9^ z2tICWB+h@{9{rL8<6@!OTY{lP{^!d$WJHnN{u0W1L6Gn_-_xw`^UnwUr3{taYztb$ zyz5!$@pD=x?4qBru|&8nQVS=yw{8v=nh4ojGkA+LgH}j2^ZIv*;7V*tz82~8eXJu{ zwjOAV=$S1TJf@Jdq)Hqe5)#62V6trpYt(&7GV)>UqtqK8Rm-AdIZmJ&w6a2Kt)XSM z(_p{z)Zla`XYpJfOuR{CSH%pfr~MS5cG!@)tY8sOrY!v;1uDUJ;$L-E8f{75J?}OQ zy5l!sm^zPSk@3C)J6cNbVV@HK>Xf-9TUYKADsaC2ecn>nq-saUNT%2wg?4MLPpB`&?IMIPnt0X9Gf_5aKQ7x6 zN~kcW08}{@h3`M;-=`)|)60su2M$%7s(rZPZQa1>$E5>QhJVRSc`Ykx+w3&>?mT_; zk4IZ3Uqa{rnEeXoxtkjr{x1XPVF&y2eS<-jCRm zT6VHTLrJ8$Bo7_I+xEOCKI8a~Z@1n1IxmkV`MroQvhM|1R>1Nv52)qOx}IbISEKXN z{vreHQu6L-=UQ|hOGwDOuC#|>%b_f{jBsv4^)OVe9khtbYoG zhh#6Dq|6_9CdlmoUi>N1beh`J)`)y)UK!uo=h5Uw?-Y+M!f)zy1uPJkbX@}m9QWA- z8h%Xby`g%px}xN+3(N-#WRa4KkcQs%f)T(4%;7q1iN^DGiKVO30_zcBi$?=kjBWru zl*9VXRG*!qy~O-#tTt8(WwC+yv;4fYz<2vs1CH%_HpE9WN#$XtJIc2}ovmvnVSCac)R7!av5v7s<(r%mS}#KX{V8gRhd{bfjZ{l!2GwU0=TG zz8}E9Ruhj;n`zomXvis*F0>2@iJ6{1$Z3*GYQx&MUD`fEad!itv}#FbC|u;2Mb_;>VG;-U*Iy98ol{daNq#Hc zOi}OQAwt1)clZK$YM&{~dLRB!++>cdJ~i(+z3}Ka58rORzu4(D#3In?eoQ#2^{M#= zQVXwX{uYdY+f!V1BC6RJ*mILxuDf;xC=>!X2neOmP~bkx9It8os;b3#*l)>AH9k&T z5@{&OLrw7hJyB2Y%Y7TWZVDjWCoRF>q+x4k6VjDBfb&9K-i8?O)6~K5890nr(u^&5 z3OJW3_yOm#V-Vn6wv*%hUkE857Y`nz92PpcQI%##pYY?o+D*?t`sscO_*Pa&cI~}Q zNgnv|zK;!mIX`{aviPT27V`3$$JvHgJ*NbIdz^*#pYNclNt6&}LDNS4ld80zE$OUaMFmwAMn zKuE7WYOvP&dmGld+;G}7VAO#fZ)+jnfsMqo1N|iE4SnJ=ctnsQ<-~r_3|SIj(zSmj zZ>FbFOjciazI!-`QF|v6RdyPe?BFHUeFqo;+w1>iy4{-Ql@W1a&Kk{Y+Pg?>nuKUi z6x%MP&_qm0eN6QO?5^F78P~UXH?(FN1~EU3Cw^$_wj${_Jg;i$Uj^*POGvVhh<+E* z0&-@-rWejVlNJ*-iz<~u!XDxAxSY%XRMRsaoZ{~in!c%L5A>pbn7bO~QSUHMI*Ay9 z@-~^|9Bk+u4|qW@5eUy`@%#2T>)(gWUF|(=FSFbv>6f&t15NnX4%M5s+z#G{D2-Vc;M6PO z6J0$_X+sINiCj!pTUhj+j#H;)x| zX*}ahC9x6+N_sL}J8-q-`CurjYtXyp$ynpDuXskJ@rxxb!&n2LPG8aXXlL5X_Py{L zc0jpvM6_f}lwjh`0>bhSZyZ9YH9+v zH!U5b{`C-6U}IUw*7t5$U-Is>9;0(tNkNyXRwyL5j{WVrRC@PU`-J7=NFXQiO2VJR z=TO;GEhJ|4vg+exVv}i?wqQ5Tr$c&v5x_lZq2lQ^Vs18#R>qAu-|}zZC+*4{6t_S) zTfTKwaY{(Tz4UEx;YbtEV$c2pg+9Cf*wy|s&w`%rmrz!YxaNOyFcxRM5ELMh1C#ev zQq{haZTRLJv0Psra8CXTdL!v`HY*}{9}3yn{6z6yOKil}cjRGDm3NWtV@tL5G5pP#}7g3JXD|FL&Z7Yfd6V;7X=)gdL1pS}4%ShH$r literal 0 HcmV?d00001 diff --git a/presto-docs/src/main/sphinx/index.rst b/presto-docs/src/main/sphinx/index.rst new file mode 100644 index 00000000..6af14d58 --- /dev/null +++ b/presto-docs/src/main/sphinx/index.rst @@ -0,0 +1,18 @@ +#################### +Presto Documentation +#################### + +.. toctree:: + :maxdepth: 2 + :numbered: 2 + + overview + installation + admin + connector + functions + language + sql + migration + develop + release diff --git a/presto-docs/src/main/sphinx/installation.rst b/presto-docs/src/main/sphinx/installation.rst new file mode 100644 index 00000000..32c05ba3 --- /dev/null +++ b/presto-docs/src/main/sphinx/installation.rst @@ -0,0 +1,12 @@ +************ +Installation +************ + +.. toctree:: + :maxdepth: 1 + + installation/deployment + installation/cli + installation/jdbc + installation/verifier + installation/benchmark-driver diff --git a/presto-docs/src/main/sphinx/installation/benchmark-driver.rst b/presto-docs/src/main/sphinx/installation/benchmark-driver.rst new file mode 100644 index 00000000..22aed87a --- /dev/null +++ b/presto-docs/src/main/sphinx/installation/benchmark-driver.rst @@ -0,0 +1,97 @@ +================ +Benchmark Driver +================ + +The benchmark driver can be used to measure the performance of queries in a +Presto cluster. We use it to continuously measure the performance of trunk. + +Download :download:`benchmark-driver`, rename it to ``presto-benchmark-driver``, +then make it executable with ``chmod +x``. + +Suites +------ + +Create a ``suite.json`` file: + +.. code-block:: json + + { + "file_formats": { + "query": ["single_.*", "tpch_.*"], + "schema": [ "tpch_sf(?.*)_(?.*)_(?.*?)" ] + }, + "legacy_orc": { + "query": ["single_.*", "tpch_.*"], + "schema": [ "tpch_sf(?.*)_(?orc)_(?.*?)" ], + "session": { + "hive.optimized_reader_enabled": "false" + } + } + } + +This example contains two suites ``file_formats`` and ``legacy_orc``. The +``file_formats`` suite will run queries with names matching the regular expression +``single_.*`` or ``tpch_.*`` in all schemas matching the regular expression +``tpch_sf.*_.*_.*?``. The ``legacy_orc`` suite adds a session property to +disable the optimized ORC reader and only runs in the ``tpch_sf.*_orc_.*?`` +schema. + +Queries +------- + +The SQL files are contained in a directory named ``sql`` and must have the +``.sql`` file extension. The name of the query is the name of the file +without the extension. + +Output +------ + +The benchmark driver will measure the wall time, total CPU time used by +all Presto processes and the CPU time used by the query. For each timing, the +driver reports median, mean and standard deviation of the query runs. The +difference between process and query CPU times is the query overhead, which +is normally from garbage collections. The following is the output from the +``file_formats`` suite above: + +.. code-block:: none + + suite query compression format scale wallTimeP50 wallTimeMean wallTimeStd processCpuTimeP50 processCpuTimeMean processCpuTimeStd queryCpuTimeP50 queryCpuTimeMean queryCpuTimeStd + ============ ============== =========== ====== ===== =========== ============ =========== ================= ================== ================= =============== ================ =============== + file_formats single_varchar none orc 100 597 642 101 100840 97180 6373 98296 94610 6628 + file_formats single_bigint none orc 100 238 242 12 33930 34050 697 32452 32417 460 + file_formats single_varchar snappy orc 100 530 525 14 99440 101320 7713 97317 99139 7682 + file_formats single_bigint snappy orc 100 218 238 35 34650 34606 83 33198 33188 83 + file_formats single_varchar zlib orc 100 547 543 38 105680 103373 4038 103029 101021 3773 + file_formats single_bigint zlib orc 100 282 269 23 38990 39030 282 37574 37496 156 + +Note that the above output has been reformatted for readability from the +standard TSV that the driver outputs. + +The driver can add additional columns to the output by extracting values from +the schema name or SQL files. In the suite file above, the schema names +contain named regular expression capturing groups for ``compression``, +``format``, and ``scale``, so if we ran the queries in a catalog containing the +schemas ``tpch_sf100_orc_none``, ``tpch_sf100_orc_snappy``, and +``tpch_sf100_orc_zlib``, we get the above output. + +Another way to create additional output columns is by adding tags to the +SQL files. For example, the following SQL file declares two tags, +``projection`` and ``filter``: + +.. code-block:: none + + projection=true + filter=false + ================= + SELECT SUM(LENGTH(comment)) + FROM lineitem + +This will cause the driver to output these values for each run of this query. + +CLI Arguments +------------- + +The ``presto-benchmark-driver`` program contains many CLI arguments to control +which suites and queries to run, the number of warm-up runs and the number +of measurement runs. All of the command line arguments can be seen with the +``--help`` option. diff --git a/presto-docs/src/main/sphinx/installation/cli.rst b/presto-docs/src/main/sphinx/installation/cli.rst new file mode 100644 index 00000000..0eecaadc --- /dev/null +++ b/presto-docs/src/main/sphinx/installation/cli.rst @@ -0,0 +1,23 @@ +====================== +Command Line Interface +====================== + +The Presto CLI provides a terminal-based interactive shell for running +queries. The CLI is a +`self-executing `_ +JAR file, which means it acts like a normal UNIX executable. + +Download :download:`cli`, rename it to ``presto``, +make it executable with ``chmod +x``, then run it: + +.. code-block:: none + + ./presto --server localhost:8080 --catalog hive --schema default + +Run the CLI with the ``--help`` option to see the available options. + +By default, the results of queries are paginated using the ``less`` program +which is configured with a carefully selected set of options. This behavior +can be overridden by setting the environment variable ``PRESTO_PAGER`` to the +name of a different program such as ``more``, or set it to an empty value +to completely disable pagination. diff --git a/presto-docs/src/main/sphinx/installation/deployment.rst b/presto-docs/src/main/sphinx/installation/deployment.rst new file mode 100644 index 00000000..fe8021a0 --- /dev/null +++ b/presto-docs/src/main/sphinx/installation/deployment.rst @@ -0,0 +1,257 @@ +================ +Deploying Presto +================ + +Installing Presto +----------------- + +Download the Presto server tarball, :download:`server`, and unpack it. +The tarball will contain a single top-level directory, +|presto_server_release|, which we will call the *installation* directory. + +Presto needs a *data* directory for storing logs, local metadata, etc. +We recommend creating a data directory outside of the installation directory, +which allows it to be easily preserved when upgrading Presto. + +Configuring Presto +------------------ + +Create an ``etc`` directory inside the installation directory. +This will hold the following configuration: + +* Node Properties: environmental configuration specific to each node +* JVM Config: command line options for the Java Virtual Machine +* Config Properties: configuration for the Presto server +* Catalog Properties: configuration for :doc:`/connector` (data sources) + +.. _presto_node_properties: + +Node Properties +^^^^^^^^^^^^^^^ + +The node properties file, ``etc/node.properties``, contains configuration +specific to each node. A *node* is a single installed instance of Presto +on a machine. This file is typically created by the deployment system when +Presto is first installed. The following is a minimal ``etc/node.properties``: + +.. code-block:: none + + node.environment=production + node.id=ffffffff-ffff-ffff-ffff-ffffffffffff + node.data-dir=/var/presto/data + +The above properties are described below: + +* ``node.environment``: + The name of the environment. All Presto nodes in a cluster must + have the same environment name. + +* ``node.id``: + The unique identifier for this installation of Presto. This must be + unique for every node. This identifier should remain consistent across + reboots or upgrades of Presto. If running multiple installations of + Presto on a single machine (i.e. multiple nodes on the same machine), + each installation must have a unique identifier. + +* ``node.data-dir``: + The location (filesystem path) of the data directory. Presto will store + logs and other data here. + +.. _presto_jvm_config: + +JVM Config +^^^^^^^^^^ + +The JVM config file, ``etc/jvm.config``, contains a list of command line +options used for launching the Java Virtual Machine. The format of the file +is a list of options, one per line. These options are not interpreted by +the shell, so options containing spaces or other special characters should +not be quoted (as demonstrated by the ``OnOutOfMemoryError`` option in the +example below). + +The following provides a good starting point for creating ``etc/jvm.config``: + +.. code-block:: none + + -server + -Xmx16G + -XX:+UseConcMarkSweepGC + -XX:+ExplicitGCInvokesConcurrent + -XX:+AggressiveOpts + -XX:+HeapDumpOnOutOfMemoryError + -XX:OnOutOfMemoryError=kill -9 %p + +Because an ``OutOfMemoryError`` will typically leave the JVM in an +inconsistent state, we write a heap dump (for debugging) and forcibly +terminate the process when this occurs. + + +.. _config_properties: + +Config Properties +^^^^^^^^^^^^^^^^^ + +The config properties file, ``etc/config.properties``, contains the +configuration for the Presto server. Every Presto server can function +as both a coordinator and a worker, but dedicating a single machine +to only perform coordination work provides the best performance on +larger clusters. + +The following is a minimal configuration for the coordinator: + +.. code-block:: none + + coordinator=true + node-scheduler.include-coordinator=false + http-server.http.port=8080 + task.max-memory=1GB + discovery-server.enabled=true + discovery.uri=http://example.net:8080 + +And this is a minimal configuration for the workers: + +.. code-block:: none + + coordinator=false + http-server.http.port=8080 + task.max-memory=1GB + discovery.uri=http://example.net:8080 + +Alternatively, if you are setting up a single machine for testing that +will function as both a coordinator and worker, use this configuration: + +.. code-block:: none + + coordinator=true + node-scheduler.include-coordinator=true + http-server.http.port=8080 + task.max-memory=1GB + discovery-server.enabled=true + discovery.uri=http://example.net:8080 + +These properties require some explanation: + +* ``coordinator``: + Allow this Presto instance to function as a coordinator + (accept queries from clients and manage query execution). + +* ``node-scheduler.include-coordinator``: + Allow scheduling work on the coordinator. + For larger clusters, processing work on the coordinator + can impact query performance because the machine's resources are not + available for the critical task of scheduling, managing and monitoring + query execution. + +* ``http-server.http.port``: + Specifies the port for the HTTP server. Presto uses HTTP for all + communication, internal and external. + +* ``task.max-memory=1GB``: + The maximum amount of memory used by a single task + (a fragment of a query plan running on a specific node). + In particular, this limits the number of groups in a ``GROUP BY``, + the size of the right-hand table in a ``JOIN``, the number of rows + in an ``ORDER BY`` or the number of rows processed by a window function. + This value should be tuned based on the number of concurrent queries and + the size and complexity of queries. Setting it too low will limit the + queries that can be run, while setting it too high will cause the JVM + to run out of memory. + +* ``discovery-server.enabled``: + Presto uses the Discovery service to find all the nodes in the cluster. + Every Presto instance will register itself with the Discovery service + on startup. In order to simplify deployment and avoid running an additional + service, the Presto coordinator can run an embedded version of the + Discovery service. It shares the HTTP server with Presto and thus uses + the same port. + +* ``discovery.uri``: + The URI to the Discovery server. Because we have enabled the embedded + version of Discovery in the Presto coordinator, this should be the + URI of the Presto coordinator. Replace ``example.net:8080`` to match + the host and port of the Presto coordinator. This URI must not end + in a slash. + +* ``query.queue-config-file``: + Specifies the file to read the :doc:`/admin/queue` from. + +Log Levels +^^^^^^^^^^ + +The optional log levels file, ``etc/log.properties``, allows setting the +minimum log level for named logger hierarchies. Every logger has a name, +which is typically the fully qualified name of the class that uses the logger. +Loggers have a hierarchy based on the dots in the name (like Java packages). +For example, consider the following log levels file: + +.. code-block:: none + + com.facebook.presto=INFO + +This would set the minimum level to ``INFO`` for both +``com.facebook.presto.server`` and ``com.facebook.presto.hive``. +The default minimum level is ``INFO`` +(thus the above example does not actually change anything). +There are four levels: ``DEBUG``, ``INFO``, ``WARN`` and ``ERROR``. + +Catalog Properties +^^^^^^^^^^^^^^^^^^ + +Presto accesses data via *connectors*, which are mounted in catalogs. +The connector provides all of the schemas and tables inside of the catalog. +For example, the Hive connector maps each Hive database to a schema, +so if the Hive connector is mounted as the ``hive`` catalog, and Hive +contains a table ``clicks`` in database ``web``, that table would be accessed +in Presto as ``hive.web.clicks``. + +Catalogs are registered by creating a catalog properties file +in the ``etc/catalog`` directory. +For example, create ``etc/catalog/jmx.properties`` with the following +contents to mount the ``jmx`` connector as the ``jmx`` catalog: + +.. code-block:: none + + connector.name=jmx + +See :doc:`/connector` for more information about configuring connectors. + +.. _running_presto: + +Running Presto +-------------- + +The installation directory contains the launcher script in ``bin/launcher``. +Presto can be started as a daemon by running running the following: + +.. code-block:: none + + bin/launcher start + +Alternatively, it can be run in the foreground, with the logs and other +output being written to stdout/stderr (both streams should be captured +if using a supervision system like daemontools): + +.. code-block:: none + + bin/launcher run + +Run the launcher with ``--help`` to see the supported commands and +command line options. In particular, the ``--verbose`` option is +very useful for debugging the installation. + +After launching, you can find the log files in ``var/log``: + +* ``launcher.log``: + This log is created by the launcher and is connected to the stdout + and stderr streams of the server. It will contain a few log messages + that occur while the server logging is being initialized and any + errors or diagnostics produced by the JVM. + +* ``server.log``: + This is the main log file used by Presto. It will typically contain + the relevant information if the server fails during initialization. + It is automatically rotated and compressed. + +* ``http-request.log``: + This is the HTTP request log which contains every HTTP request + received by the server. It is automatically rotated and compressed. diff --git a/presto-docs/src/main/sphinx/installation/jdbc.rst b/presto-docs/src/main/sphinx/installation/jdbc.rst new file mode 100644 index 00000000..2e95a564 --- /dev/null +++ b/presto-docs/src/main/sphinx/installation/jdbc.rst @@ -0,0 +1,21 @@ +=========== +JDBC Driver +=========== + +Presto can be accessed from Java using the JDBC driver. +Download :download:`jdbc` and add it to the class path of your Java application. +The following JDBC URL formats are supported: + +.. code-block:: none + + jdbc:presto://host:port + jdbc:presto://host:port/catalog + jdbc:presto://host:port/catalog/schema + +For example, use the following URL to connect to Presto +running on ``example.net`` port ``8080`` with the catalog ``hive`` +and the schema ``sales``: + +.. code-block:: none + + jdbc:presto://example.net:8080/hive/sales diff --git a/presto-docs/src/main/sphinx/installation/verifier.rst b/presto-docs/src/main/sphinx/installation/verifier.rst new file mode 100644 index 00000000..43b4ba65 --- /dev/null +++ b/presto-docs/src/main/sphinx/installation/verifier.rst @@ -0,0 +1,45 @@ +=============== +Presto Verifier +=============== + +The Presto Verifier can be used to test Presto against another database (such as MySQL), +or to test two Presto clusters against each other. We use it to continuously test trunk +against the previous release while developing Presto. + +Create a MySQL database with the following table and load it with the queries you would like to run: + +.. code-block:: sql + + CREATE TABLE verifier_queries( + id INT NOT NULL AUTO_INCREMENT, + suite VARCHAR(256) NOT NULL, + name VARCHAR(256), + test_catalog VARCHAR(256) NOT NULL, + test_schema VARCHAR(256) NOT NULL, + test_query TEXT NOT NULL, + test_username VARCHAR(256) NOT NULL default 'verifier-test', + test_password VARCHAR(256), + control_catalog VARCHAR(256) NOT NULL, + control_schema VARCHAR(256) NOT NULL, + control_query TEXT NOT NULL, + control_username VARCHAR(256) NOT NULL default 'verifier-test', + control_password VARCHAR(256), + PRIMARY KEY (id) + ); + +Next, create a properties file to configure the verifier: + +.. code-block:: none + + suite=my_suite + query-database=jdbc:mysql://localhost:3306/my_database?user=my_username&password=my_password + control.gateway=jdbc:presto://localhost:8080 + test.gateway=jdbc:presto://localhost:8081 + thread-count=1 + +Lastly, download :download:`verifier`, rename it to ``verifier``, +make it executable with ``chmod +x``, then run it: + +.. code-block:: none + + ./verifier config.properties diff --git a/presto-docs/src/main/sphinx/language.rst b/presto-docs/src/main/sphinx/language.rst new file mode 100644 index 00000000..63f9d14f --- /dev/null +++ b/presto-docs/src/main/sphinx/language.rst @@ -0,0 +1,8 @@ +************ +SQL Language +************ + +.. toctree:: + :maxdepth: 1 + + language/types diff --git a/presto-docs/src/main/sphinx/language/types.rst b/presto-docs/src/main/sphinx/language/types.rst new file mode 100644 index 00000000..04e54c26 --- /dev/null +++ b/presto-docs/src/main/sphinx/language/types.rst @@ -0,0 +1,118 @@ +========== +Data Types +========== + +Presto currently supports a limited set of data types. +These types can be used with the standard ``CAST`` operator. + +BOOLEAN +------- + + This type captures boolean values ``true`` and ``false``. + +BIGINT +------ + + A 64-bit signed two's complement integer with a minimum value of + ``-2^63`` and a maximum value of ``2^63 - 1``. + +DOUBLE +------ + + A double is a 64-bit inexact, variable-precision implementing the + IEEE Standard 754 for Binary Floating-Point Arithmetic. + +VARCHAR +------- + + Variable length character data. + +VARBINARY +--------- + + Variable length binary data. + +JSON +---- + + Variable length json data. + +DATE +---- + + Calendar date (year, month, day). + + Example: ``DATE '2001-08-22'`` + +TIME +---- + + Time of day (hour, minute, second, millisecond) without a time zone. + Values of this type are parsed and rendered in the session time zone. + + Example: ``TIME '01:02:03.456'`` + +TIME WITH TIME ZONE +------------------- + + Time of day (hour, minute, second, millisecond) with a time zone. + Values of this type are rendered using the time zone from the value. + + Example: ``TIME '01:02:03.456 America/Los_Angeles'`` + +TIMESTAMP +--------- + + Instant in time that includes the date and time of day without a time zone. + Values of this type are parsed and rendered in the session time zone. + + Example: ``TIMESTAMP '2001-08-22 03:04:05.321'`` + +TIMESTAMP WITH TIME ZONE +------------------------ + + Instant in time that includes the date and time of day with a time zone. + Values of this type are rendered using the time zone from the value. + + Example: ``TIMESTAMP '2001-08-22 03:04:05.321 America/Los_Angeles'`` + +INTERVAL YEAR TO MONTH +---------------------- + + Span of years and months. + + Example: ``INTERVAL '3' MONTH`` + +INTERVAL DAY TO SECOND +---------------------- + + Span of days, hours, minutes, seconds and milliseconds. + + Example: ``INTERVAL '2' DAY`` + +.. _array_type: + +ARRAY +----- + + An array of the given component type. + + Example: ``ARRAY[1, 2, 3]`` + +.. _map_type: + +MAP +--- + + A map between the given component types. + +.. _row_type: + +ROW +--- + + A structure made up of named fields. The fields may be of any SQL type, and are + accessed with field reference operator ``.`` + + Example: ``my_column.my_field`` + diff --git a/presto-docs/src/main/sphinx/migration.rst b/presto-docs/src/main/sphinx/migration.rst new file mode 100644 index 00000000..4fb5b73a --- /dev/null +++ b/presto-docs/src/main/sphinx/migration.rst @@ -0,0 +1,9 @@ +********* +Migration +********* + +.. toctree:: + :maxdepth: 1 + + migration/from-hive + diff --git a/presto-docs/src/main/sphinx/migration/from-hive.rst b/presto-docs/src/main/sphinx/migration/from-hive.rst new file mode 100644 index 00000000..ea1f80c2 --- /dev/null +++ b/presto-docs/src/main/sphinx/migration/from-hive.rst @@ -0,0 +1,134 @@ +=================== +Migrating From Hive +=================== + +Presto uses ANSI SQL syntax and semantics, whereas Hive uses a SQL-like language called HiveQL which is loosely modeled after MySQL (which itself has many differences from ANSI SQL). + +Use subscript for accessing a dynamic index of an array instead of a udf +------------------------------------------------------------------------ + +The subscript operator in SQL supports full expressions, unlike Hive (which only supports constants). Therefore you can write queries like:: + + SELECT my_array[CARDINALITY(my_array)] as last_element + FROM ... + +Avoid out of bounds access of arrays +------------------------------------ + +Accessing out of bounds elements of an array will result in an exception. You can avoid this with an ``if`` as follows:: + + SELECT IF(CARDINALITY(my_array) >= 3, my_array[3], NULL) + FROM ... + +Use ANSI SQL syntax for arrays +------------------------------ + +Arrays are indexed starting from 1, not from 0:: + + SELECT my_array[1] AS first_element + FROM ... + +Construct arrays with ANSI syntax:: + + SELECT ARRAY[1, 2, 3] AS my_array + +Use ANSI SQL syntax for identifiers and strings +----------------------------------------------- + +Strings are delimited with single quotes and identifiers are quoted with double quotes, not backquotes:: + + SELECT name AS "User Name" + FROM "7day_active" + WHERE name = 'foo' + +Quote identifiers that start with numbers +----------------------------------------- + +Identifiers that start with numbers are not legal in ANSI SQL and must be quoted using double quotes:: + + SELECT * + FROM "7day_active" + +Use the standard string concatenation operator +---------------------------------------------- + +Use the ANSI SQL string concatenation operator:: + + SELECT a || b || c + FROM ... + +Use standard types for CAST targets +----------------------------------- + +The following standard types are supported for ``CAST`` targets:: + + SELECT + CAST(x AS varchar) + , CAST(x AS bigint) + , CAST(x AS double) + , CAST(x AS boolean) + FROM ... + +In particular, use ``VARCHAR`` instead of ``STRING``. + +Use CAST when dividing integers +------------------------------- + +Presto follows the standard behavior of performing integer division when dividing two integers. For example, dividing ``7`` by ``2`` will result in ``3``, not ``3.5``. +To perform floating point division on two integers, cast one of them to a double:: + + SELECT CAST(5 AS DOUBLE) / 2 + +Use WITH for complex expressions or queries +------------------------------------------- + +When you want to re-use a complex output expression as a filter, use either an inline subquery or factor it out using the ``WITH`` clause:: + + WITH a AS ( + SELECT substr(name, 1, 3) x + FROM ... + ) + SELECT * + FROM a + WHERE x = 'foo' + +Use UNNEST to expand arrays and maps +------------------------------------ + +Presto supports :ref:`unnest` for expanding arrays and maps. +Use ``UNNEST`` instead of ``LATERAL VIEW explode()``. + +Hive query:: + + SELECT student, score + FROM tests + LATERAL VIEW explode(scores) t AS score; + +Presto query:: + + SELECT student, score + FROM tests + CROSS JOIN UNNEST(scores) AS t (score); + +Outer Join Differences +---------------------- + +Adhering to the ANSI SQL spec, Presto respects the abstract concept that the *whole* ``ON`` clause is evaluated to determine whether or not a row from the left table will be joined with a right table row. In a ``LEFT JOIN``, all the rows of the left table are always returned out of the join, vice versa for a ``RIGHT JOIN``. In contrast, Hive will *first* apply any constant filters in the ``ON`` clause *then* perform the join. This can produce very different results when ``ON`` clause predicates refer to the outer table. + +When you want to convert a Hive ``OUTER JOIN`` query to Presto, remember that Hive treats the ``ON`` clause predicates as if it were part of the ``WHERE`` clause. So to get the equivalent behavior in Presto, you need to move your ``ON`` clause predicates into the ``WHERE`` clause. + +Hive query:: + + SELECT a.id, b.userid + FROM a + LEFT JOIN b + ON a.id = b.id AND a.ds = '2013-11-11' + +Presto query:: + + SELECT a.id, b.userid + FROM a + LEFT JOIN b + ON a.id = b.id + WHERE a.ds = '2013-11-11' + diff --git a/presto-docs/src/main/sphinx/overview.rst b/presto-docs/src/main/sphinx/overview.rst new file mode 100644 index 00000000..ac51cf23 --- /dev/null +++ b/presto-docs/src/main/sphinx/overview.rst @@ -0,0 +1,11 @@ +******** +Overview +******** + +Presto is a distributed SQL query engine designed to query large data sets +distributed over one or more heterogeneous data sources. + +.. toctree:: + :maxdepth: 1 + + overview/use-cases diff --git a/presto-docs/src/main/sphinx/overview/concepts.rst b/presto-docs/src/main/sphinx/overview/concepts.rst new file mode 100644 index 00000000..ebcb19d9 --- /dev/null +++ b/presto-docs/src/main/sphinx/overview/concepts.rst @@ -0,0 +1,278 @@ +=============== +Presto Concepts +=============== + +To understand Presto you must first understand the terms and concepts +used throughout the Presto documentation. + +While it's easy to understand statements and queries, as an end-user +you should have familiarity with concepts such as stages and shards to +take full advantage of Presto to execute efficient queries. As a +Presto administrator or a Presto contributor you should understand how +Presto's concepts of stages map to tasks and how tasks contain a set +of drivers which process data. + +This section provides a solid definition for the core concepts +referenced throughout Presto, and these sections are sorted from most +general to most specific. + +-------------- +Presto Servers +-------------- + +There are two types of Presto servers: coordinators and workers. The +following section explains the difference between the two. + +^^^^^^^^^^^ +Coordinator +^^^^^^^^^^^ + +The Presto coordinator is a server that is responsible for parsing +statements, planning queries, and managing Presto worker nodes. It is +the "brains" of a Presto installation and is also the node to which a +client connects to submit statements for execution. + +Every Presto installation must have at least one Presto coordinator +alongside zero or more Presto workers. The coordinator keeps track of +the activity on each worker and coordinates the delivery of data to +the components created to execute a query. Coordinators keep track of +a logical model of a query involving a series of stages which is then +translated into a series of connected tasks running on a cluster of +Presto workers. + +Coordinators communicate with both workers and clients using a REST +API. + +^^^^^^ +Worker +^^^^^^ + +A Presto worker is a server in a Presto installation which is +responsible for executing tasks and processing data. Worker nodes +consume data and produce results for either other workers involved in +the same query or a coordinator acting as a go-between for a Presto +client. + +When a Presto worker becomes available, it advertises itself using a +discovery server making itself available to a Presto coordinator for +task execution. + +Workers communicate with both other workers and Presto coordinators +using a REST API. + +--------------------------- +Presto Data Source Concepts +--------------------------- + +Throughout this documentation you'll read terms such as connector, +catalog, schema, and table. These fundamental concepts cover Presto's +model of a particular data source and are described in the following +section. + +^^^^^^^^^ +Connector +^^^^^^^^^ + +A connector adapts Presto to a data source such as Hive or a +relational database. You can think of a connector the same way you +think of a driver for a database. It is an implementation of Presto's +SPI which allows Presto to interact with a resource using a standard +API. + +Presto contains several built-in connectors including a connector for +JMX, a "system" connector which provides access to built-in system +tables, a Hive connector, and a connector designed to serve TPC-H benchmark +data. Many third-party developers have contributed connectors so that +Presto can access data in a variety of data sources. + +Every catalog is associated with a specific connector. If you examine +a catalog configuration file, you will see that each contains a +mandatory property "connector.name" which is used by the Catalog +manager to create a connector for a given catalog. It is possible +to have more than one catalog use the same connector to access two +different instances of a similar database. For example, if you have +two Hive clusters, you can configure two catalogs which use the Hive +connector to query data from either database. + +^^^^^^^ +Catalog +^^^^^^^ + +A Presto catalog contains schemas and references a data source via a +connector. For example, the JMX catalog is a built-in catalog in +Presto which provides access to JMX information via a JMX connector. +When you run a SQL statement in Presto, you are running it against a +catalog. Other examples of catalogs include the Hive catalog to +connect to a Hive data source. + +When addressing a table in Presto, the fully-qualified table name is +always rooted in a catalog. For example, a fully-qualified table name +of "hive.test_data.test" would refer to the test table in the +test_data schema in the hive catalog. + +Catalogs are defined in properties files stored in the Presto +configuration directory. + +^^^^^^ +Schema +^^^^^^ + +A schema is grouping of tables. Think of a traditional relational +database such as Postgresql, MySQL, or Oracle. Each one of those +database products has the concept of a schema and a Presto schema maps +to the same concept. Tables are grouped into schemas to organize +tables into schemas which share a common purpose. + +A Catalog and Schema together define a set of tables that can be +queried. When accessing Hive or a relational database with Presto, a +schema refers to the same concept in the target database. When +accessing a catalog such as JMX, schema simply refers to a set of +tables used to represent JMX information and does not directly +correspond to a similar concept in the underlying technology. + +^^^^^ +Table +^^^^^ + +Presto's concept of a table isn't too different from a table in a +relational database. A table contains rows which have data in a +series of named columns. + +------------------ +Presto Query Model +------------------ + +Presto executes SQL statements and turns these statements into queries +that are executed across a distributed network of coordinators and +workers. + +^^^^^^^^^ +Statement +^^^^^^^^^ + +Presto executes ANSI-compatible SQL statements. When Presto +documentation refers to a statement we are refering to statements as +defined in the ANSI SQL standard which consists of clauses, +expressions, and predicates. + +Some readers might be curious why this section lists seperate concepts +for statements and queries. This is necessary because, in Presto, +statements simply refer to the textual representation of a SQL +statement. When a statement is executed, Presto creates a query along +with a query plan that is then distributed across a series of Presto +workers. + +^^^^^ +Query +^^^^^ + +When Presto parses a statement it converts it into a query and creates +a distributed query plan which is then realized as a series of +interconnected stages running on Presto Workers. When you retrieve +information about a query in Presto, you receive a snapshot of every +component that is involved in producing a result set in response to a +statement. + +The difference between a statement and a query is simple. A statement +can be thought of as the string that is passed to Presto while a query +refers to the configuration and components instantiated to execute +that statement. A query encompasses stages, tasks, splits, catalogs, +and other components and data sources working in concert to produce a +result. + +^^^^^ +Stage +^^^^^ + +When Presto executes a query it does so by breaking up the execution +into a hierarchy of stages. For example, if Presto needs to aggregate +data from one billion rows stored in Hive it does so by creating a +root stage to aggregate the output of several other stages all of +which are designed to implement different sections of a distributed +query plan. + +The hierarchy of stages that comprises a query resembles a tree. +Every query has a "root" stage which is responsible for aggregating +the output from other stages. Stages are what the coordinator uses to +model a distributed query plan, but stages themselves don't run on +Presto workers. + +Note: If you are a Presto end-user, everything beyond Stage in this +section isn't necessary to understand how Presto works from an +end-user perspective. + +^^^^^^^^ +Exchange +^^^^^^^^ + +Stages connect to one another using an exchange. An exchange is +responsible for receiving and transporting data from one stage to +another and for interacting with other stages to retrieve data. A +stage that produces data has an exchange called an output buffer, and +a stage that consumes data has an exchange called an exchange client. + +Note that data is retrieved from the lowest level stage directly +from a connector. This interaction between a stage and a connector +uses an operator called a source operator. For example, if a stage +retrieves data from HDFS, this isn't performed with an exchange +client, the retrieval happens from a source operator running in a +driver. + +^^^^ +Task +^^^^ + +As mentioned in the previous section, stages model a particular +section of a distributed query plan, but stages themselves don't +execute on Presto workers. To understand how a stage is executed, +you'll need to understand that a stage is implemented as a series of +tasks distributed over a network of Presto workers. + +Tasks are the "work horse" in the Presto architecture as a distributed +query plan is deconstructed into a series of stages which are then +translated to tasks which then act upon or process splits. A Presto +task has inputs and outputs, and just as a stage can be executed in +parallel by a series of tasks, a task is executing in parallel with a +series of drivers. + +^^^^^^ +Driver +^^^^^^ + +Tasks contain one or more parallel drivers. Drivers act upon data and +combine operators to produce output that is then aggregated by a task +and then delivered to another task in a another stage. A driver is a +sequence of operator instances, or you can think of a driver as a +physical set of operators in memory. It is the lowest level of +parallelism in the Presto architecture. A driver has one input and +one output. + +^^^^^^^^ +Operator +^^^^^^^^ + +An Operator in Presto encapsulates the functionality of functions and +other operations which take data as input and generate data as output. +Operators execute within a driver as a driver is simply an +assembly of different operators which are then applied to individual +pieces of data within a split. + +^^^^^ +Split +^^^^^ + +Tasks operate on splits, and splits are sections of larger data +set. Stages at the lowest level of a distributed query plan retrieve splits +from connectors, and intermediate stages at a higher level of a +distributed query plan are designed to retrieve data from other +stages. + +When Presto is scheduling a query, the coordinator will query a +connector for a list of all splits that are available for a table. +The coordinator keeps track of which machines are running which tasks +and what splits are being processed by which tasks. + + +.. NOTE: Chapter for Connectors + +.. NOTE: Explain how to use the Cassandra connector \ No newline at end of file diff --git a/presto-docs/src/main/sphinx/overview/use-cases.rst b/presto-docs/src/main/sphinx/overview/use-cases.rst new file mode 100644 index 00000000..beaa7cc1 --- /dev/null +++ b/presto-docs/src/main/sphinx/overview/use-cases.rst @@ -0,0 +1,46 @@ +========= +Use Cases +========= + +This section puts Presto into perspective so that prospective +administrators and end users know what to expect from Presto. + +------------------ +What Presto Is Not +------------------ + +Since Presto is being called a *database* by many members of the community, +it makes sense to begin with a definition of what Presto is not. + +Do not mistake the fact that Presto understands SQL with it providing +the features of a standard database. Presto is not a general-purpose +relational database. It is not a replacement for databases like MySQL, +PostgreSQL or Oracle. Presto was not designed to handle Online +Transaction Processing (OLTP). This is also true for many other +databases designed and optimized for data warehousing or analytics. + +-------------- +What Presto Is +-------------- + +Presto is a tool designed to efficiently query vast amounts of data +using distributed queries. If you work with terabytes or petabytes of +data, you are likely using tools that interact with Hadoop and HDFS. +Presto was designed as an alternative to tools that query HDFS +using pipelines of MapReduce jobs such as Hive or Pig, but Presto +is not limited to accessing HDFS. Presto can be and has been extended +to operate over different kinds of data sources including traditional +relational databases and other data sources such as Cassandra. + +Presto was designed to handle data warehousing and analytics: data analysis, +aggregating large amounts of data and producing reports. These workloads +are often classified as Online Analytical Processing (OLAP). + +---------------- +Who uses Presto? +---------------- + +Presto is an open source project that operates under the auspices of +Facebook. It was invented at Facebook and the project continues to +be developed by both Facebook internal developers and a number of +third-party developers in the community. diff --git a/presto-docs/src/main/sphinx/release.rst b/presto-docs/src/main/sphinx/release.rst new file mode 100644 index 00000000..95bc9c1f --- /dev/null +++ b/presto-docs/src/main/sphinx/release.rst @@ -0,0 +1,61 @@ +************* +Release Notes +************* + +.. toctree:: + :maxdepth: 1 + + release/release-0.107 + release/release-0.106 + release/release-0.105 + release/release-0.104 + release/release-0.103 + release/release-0.102 + release/release-0.101 + release/release-0.100 + release/release-0.99 + release/release-0.98 + release/release-0.97 + release/release-0.96 + release/release-0.95 + release/release-0.94 + release/release-0.93 + release/release-0.92 + release/release-0.91 + release/release-0.90 + release/release-0.89 + release/release-0.88 + release/release-0.87 + release/release-0.86 + release/release-0.85 + release/release-0.84 + release/release-0.83 + release/release-0.82 + release/release-0.81 + release/release-0.80 + release/release-0.79 + release/release-0.78 + release/release-0.77 + release/release-0.76 + release/release-0.75 + release/release-0.74 + release/release-0.73 + release/release-0.72 + release/release-0.71 + release/release-0.70 + release/release-0.69 + release/release-0.68 + release/release-0.67 + release/release-0.66 + release/release-0.65 + release/release-0.64 + release/release-0.63 + release/release-0.62 + release/release-0.61 + release/release-0.60 + release/release-0.59 + release/release-0.58 + release/release-0.57 + release/release-0.56 + release/release-0.55 + release/release-0.54 diff --git a/presto-docs/src/main/sphinx/release/release-0.100.rst b/presto-docs/src/main/sphinx/release/release-0.100.rst new file mode 100644 index 00000000..7774fd95 --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.100.rst @@ -0,0 +1,29 @@ +============= +Release 0.100 +============= + +System Connector +---------------- + +The :doc:`/connector/system` now works like other connectors: global system +tables are only available in the ``system`` catalog, rather than in a special +schema that is available in every catalog. Additionally, connectors may now +provide system tables that are available within that connector's catalog by +implementing the ``getSystemTables()`` method on the ``Connector`` interface. + +General Changes +--------------- + +* Fix ``%f`` specifier in :func:`date_format` and :func:`date_parse`. +* Add ``WITH ORDINALITY`` support to ``UNNEST``. +* Add :func:`array_distinct` function. +* Add :func:`split` function. +* Add :func:`degrees` and :func:`radians` functions. +* Add :func:`to_base` and :func:`from_base` functions. +* Rename config property ``task.shard.max-threads`` to ``task.max-worker-threads``. + This property sets the number of threads used to concurrently process splits. + The old property name is deprecated and will be removed in a future release. +* Fix referencing ``NULL`` values in :ref:`row_type`. +* Make :ref:`map_type` comparable. +* Fix leak of tasks blocked during query teardown. +* Improve query queue config validation. diff --git a/presto-docs/src/main/sphinx/release/release-0.101.rst b/presto-docs/src/main/sphinx/release/release-0.101.rst new file mode 100644 index 00000000..77fc681b --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.101.rst @@ -0,0 +1,80 @@ +============= +Release 0.101 +============= + +General Changes +--------------- + +* Add support for :doc:`/sql/create-table` (in addition to :doc:`/sql/create-table-as`). +* Add ``IF EXISTS`` support to :doc:`/sql/drop-table` and :doc:`/sql/drop-view`. +* Add :func:`array_agg` function. +* Add :func:`array_intersect` function. +* Add :func:`array_position` function. +* Add :func:`regexp_split` function. +* Add support for ``millisecond`` to :func:`date_diff` and :func:`date_add`. +* Fix excessive memory usage in :func:`map_agg`. +* Fix excessive memory usage in queries that perform partitioned top-N operations + with :func:`row_number`. +* Optimize :ref:`array_type` comparison operators. +* Fix analysis of ``UNION`` queries for tables with hidden columns. +* Fix ``JOIN`` associativity to be left-associative instead of right-associative. +* Add ``source`` column to ``runtime.queries`` table in :doc:`/connector/system`. +* Add ``coordinator`` column to ``runtime.nodes`` table in :doc:`/connector/system`. +* Add ``errorCode``, ``errorName`` and ``errorType`` to ``error`` object in REST API + (``errorCode`` previously existed but was always zero). +* Fix ``DatabaseMetaData.getIdentifierQuoteString()`` in JDBC driver. +* Handle thread interruption in JDBC driver ``ResultSet``. +* Add ``history`` command and support for running previous commands via ``!n`` to the CLI. +* Change Driver to make as much progress as possible before blocking. This improves + responsiveness of some limit queries. +* Add predicate push down support to JMX connector. +* Add support for unary ``PLUS`` operator. +* Improve scheduling speed by reducing lock contention. +* Extend optimizer to understand physical properties such as local grouping and sorting. +* Add support for streaming execution of window functions. +* Make ``UNION`` run partitioned, if underlying plan is partitioned. +* Add ``hash_partition_count`` session property to control hash partitions. + +Web UI Changes +-------------- + +The main page of the web UI has been completely rewritten to use ReactJS. It also has +a number of new features, such as the ability to pause auto-refresh via the "Z" key and +also with a toggle in the UI. + +Hive Changes +------------ + +* Add support for connecting to S3 using EC2 instance credentials. + This feature is enabled by default. To disable it, set + ``hive.s3.use-instance-credentials=false`` in your Hive catalog properties file. +* Treat ORC files as splittable. +* Change PrestoS3FileSystem to use lazy seeks, which improves ORC performance. +* Fix ORC ``DOUBLE`` statistic for columns containing ``NaN``. +* Lower the Hive metadata refresh interval from two minutes to one second. +* Invalidate Hive metadata cache for failed operations. +* Support ``s3a`` file system scheme. +* Fix discovery of splits to correctly backoff when the queue is full. +* Add support for non-canonical Parquet structs. +* Add support for accessing Parquet columns by name. By default, columns in Parquet + files are accessed by their ordinal position in the Hive table definition. To access + columns based on the names recorded in the Parquet file, set + ``hive.parquet.use-column-names=true`` in your Hive catalog properties file. +* Add JMX stats to PrestoS3FileSystem. +* Add ``hive.recursive-directories`` config option to recursively scan + partition directories for data. + +SPI Changes +----------- + +* Add connector callback for rollback of ``INSERT`` and ``CREATE TABLE AS``. +* Introduce an abstraction for representing physical organizations of a table + and describing properties such as partitioning, grouping, predicate and columns. + ``ConnectorPartition`` and related interfaces are deprecated and will be removed + in a future version. +* Rename ``ConnectorColumnHandle`` to ``ColumnHandle``. + +.. note:: + This is a backwards incompatible change with the previous connector SPI. + If you have written a connector, you will need to update your code + before deploying this release. diff --git a/presto-docs/src/main/sphinx/release/release-0.102.rst b/presto-docs/src/main/sphinx/release/release-0.102.rst new file mode 100644 index 00000000..2c8d6347 --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.102.rst @@ -0,0 +1,53 @@ +============= +Release 0.102 +============= + +Unicode Support +--------------- + +All string functions have been updated to support Unicode. The functions assume +that the string contains valid UTF-8 encoded code points. There are no explicit +checks for valid UTF-8, and the functions may return incorrect results on +invalid UTF-8. Invalid UTF-8 data can be corrected with :func:`from_utf8`. + +Additionally, the functions operate on Unicode code points and not user visible +*characters* (or *grapheme clusters*). Some languages combine multiple code points +into a single user-perceived *character*, the basic unit of a writing system for a +language, but the functions will treat each code point as a separate unit. + +Regular Expression Functions +---------------------------- + +All :doc:`/functions/regexp` have been rewritten to improve performance. +The new versions are often twice as fast and in some cases can be many +orders of magnitude faster (due to removal of quadratic behavior). +This change introduced some minor incompatibilities that are explained +in the documentation for the functions. + +General Changes +--------------- + +* Add support for partitioned right outer joins, which allows for larger tables to + be joined on the inner side. +* Add support for full outer joins. +* Support returning booleans as numbers in JDBC driver +* Fix :func:`contains` to return ``NULL`` if the value was not found, but a ``NULL`` was. +* Fix nested :ref:`row_type` rendering in ``DESCRIBE``. +* Add :func:`array_join`. +* Optimize map subscript operator. +* Add :func:`from_utf8` and :func:`to_utf8` functions. +* Add ``task_writer_count`` session property to set ``task.writer-count``. +* Add cast from ``ARRAY`` to ``ARRAY``. +* Extend implicit coercions to ``ARRAY`` element types. +* Implement implicit coercions in ``VALUES`` expressions. +* Fix potential deadlock in scheduler. + +Hive Changes +------------ + +* Collect more metrics from ``PrestoS3FileSystem``. +* Retry when seeking in ``PrestoS3FileSystem``. +* Ignore ``InvalidRange`` error in ``PrestoS3FileSystem``. +* Implement rename and delete in ``PrestoS3FileSystem``. +* Fix assertion failure when running ``SHOW TABLES FROM schema``. +* Fix S3 socket leak when reading ORC files. diff --git a/presto-docs/src/main/sphinx/release/release-0.103.rst b/presto-docs/src/main/sphinx/release/release-0.103.rst new file mode 100644 index 00000000..282ceecb --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.103.rst @@ -0,0 +1,55 @@ +============= +Release 0.103 +============= + +Cluster Resource Management +--------------------------- + +There is a new cluster resource manager, which can be enabled via the +``experimental.cluster-memory-manager-enabled`` flag. Currently, the only +resource that's tracked is memory, and the cluster resource manager guarantees +that the cluster will not deadlock waiting for memory. However, in a low memory +situation it is possible that only one query will make progress. Memory limits can +now be configured via ``query.max-memory`` which controls the total distributed +memory a query may use and ``query.max-memory-per-node`` which limits the amount +of memory a query may use on any one node. On each worker, the +``resources.reserved-system-memory`` flags controls how much memory is reserved +for internal Presto data structures and temporary allocations. + +Task Parallelism +---------------- +Queries involving a large number of aggregations or a large hash table for a +join can be slow due to single threaded execution in the intermediate stages. +This release adds experimental configuration and session properties to execute +this single threaded work in parallel. Depending on the exact query this may +reduce wall time, but will likely increase CPU usage. + +Use the configuration parameter ``task.default-concurrency`` or the session +property ``task_default_concurrency`` to set the default number of parallel +workers to use for join probes, hash builds and final aggregations. +Additionally, the session properties ``task_join_concurrency``, +``task_hash_build_concurrency`` and ``task_aggregation_concurrency`` can be +used to control the parallelism for each type of work. + +This is an experimental feature and will likely change in a future release. It +is also expected that this will eventually be handled automatically by the +query planner and these options will be removed entirely. + +Hive Changes +------------ + +* Removed the ``hive.max-split-iterator-threads`` parameter and renamed + ``hive.max-global-split-iterator-threads`` to ``hive.max-split-iterator-threads``. +* Fix excessive object creation when querying tables with a large number of partitions. +* Do not retry requests when an S3 path is not found. + +General Changes +--------------- + +* Add :func:`array_remove`. +* Fix NPE in :func:`max_by` and :func:`min_by` caused when few rows were present in the aggregation. +* Reduce memory usage of :func:`map_agg`. +* Change HTTP client defaults: 2 second idle timeout, 10 second request + timeout and 250 connections per host. +* Add SQL command autocompletion to CLI. +* Increase CLI history file size. diff --git a/presto-docs/src/main/sphinx/release/release-0.104.rst b/presto-docs/src/main/sphinx/release/release-0.104.rst new file mode 100644 index 00000000..7cb56bd8 --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.104.rst @@ -0,0 +1,30 @@ +============= +Release 0.104 +============= + +General Changes +--------------- + +* Handle thread interruption in StatementClient. +* Fix CLI hang when server becomes unreachable during a query. +* Add :func:`covar_pop`, :func:`covar_samp`, :func:`corr`, :func:`regr_slope`, + and :func:`regr_intercept` functions. +* Fix potential deadlock in cluster memory manager. +* Add a visualization of query execution timeline. +* Allow mixed case in input to :func:`from_hex`. +* Display "BLOCKED" state in web UI. +* Reduce CPU usage in coordinator. +* Fix excess object retention in workers due to long running queries. +* Reduce memory usage of :func:`array_distinct`. +* Add optimizer for projection push down which can + improve the performance of certain query shapes. +* Improve query performance by storing pre-partitioned pages. +* Support ``TIMESTAMP`` for :func:`first_value`, :func:`last_value`, + :func:`nth_value`, :func:`lead` and :func:`lag`. + +Hive Changes +------------ + +* Upgrade to Parquet 1.6.0. +* Collect request time and retry statistics in ``PrestoS3FileSystem``. +* Fix retry attempt counting for S3. diff --git a/presto-docs/src/main/sphinx/release/release-0.105.rst b/presto-docs/src/main/sphinx/release/release-0.105.rst new file mode 100644 index 00000000..35eaf7cf --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.105.rst @@ -0,0 +1,21 @@ +============= +Release 0.105 +============= + +General Changes +--------------- + +* Fix issue which can cause queries to be blocked permanently. +* Close connections correctly in JDBC connectors. +* Add implicit coercions for values of equi-join criteria. +* Fix detection of window function calls without an ``OVER`` clause. + +SPI Changes +----------- + +* Remove ``ordinalPosition`` from ``ColumnMetadata``. + +.. note:: + This is a backwards incompatible change with the previous connector SPI. + If you have written a connector, you will need to update your code + before deploying this release. diff --git a/presto-docs/src/main/sphinx/release/release-0.106.rst b/presto-docs/src/main/sphinx/release/release-0.106.rst new file mode 100644 index 00000000..6c7c4a93 --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.106.rst @@ -0,0 +1,15 @@ +============= +Release 0.106 +============= + +General Changes +--------------- + +* Parallelize startup of table scan task splits. +* Fixed index join driver resource leak. +* Improve memory accounting for JOINs and GROUP BYs. +* Improve CPU efficiency of coordinator. +* Added ``Asia/Chita``, ``Asia/Srednekolymsk``, and ``Pacific/Bougainville`` time zones. +* Fix task leak caused by race condition in stage state machine. +* Fix blocking in Hive split source. +* Free resources sooner for queries that finish prematurely. diff --git a/presto-docs/src/main/sphinx/release/release-0.107.rst b/presto-docs/src/main/sphinx/release/release-0.107.rst new file mode 100644 index 00000000..1f158ac0 --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.107.rst @@ -0,0 +1,15 @@ +============= +Release 0.107 +============= + +General Changes +--------------- + +* Added ``query_max_memory`` session property. Note: this session property cannot + increase the limit above the limit set by the ``query.max_memory`` configuration option. +* Fixed task leak caused by queries that finish early, such as a "LIMIT" query or cancelled + query, when the cluster is under high load. +* Added ``task.info-refresh-max-wait`` to configure task info freshness. +* Add support for `DELETE` to language and connector SPI. +* Reenable error classification code for syntax errors. +* Fix out of bounds exception in :func:`lower` and :func:`upper` when the string contains the code point `U+10FFFF`. diff --git a/presto-docs/src/main/sphinx/release/release-0.54.rst b/presto-docs/src/main/sphinx/release/release-0.54.rst new file mode 100644 index 00000000..b5d6d1b3 --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.54.rst @@ -0,0 +1,36 @@ +============ +Release 0.54 +============ + +* Restore binding for the node resource on the coordinator, which provides + the state of all nodes as seen by the coordinator's failure detector. + Access ``/v1/node`` to see all nodes, or ``/v1/node/failed`` to see failed nodes. + +* Prevent the :doc:`/installation/cli` from hanging when the server goes away. + +* Add Hive connector ``hive-hadoop1`` for Apache Hadoop 1.x. + +* Add support for Snappy and LZ4 compression codecs for the ``hive-cdh4`` connector. + +* Add Example HTTP connector ``example-http`` that reads CSV data via HTTP. + The connector requires a metadata URI that returns a JSON document + describing the table metadata and the CSV files to read. + + Its primary purpose is to serve as an example of how to write a connector, + but it can also be used directly. Create ``etc/catalog/example.properties`` + with the following contents to mount the ``example-http`` connector as the + ``example`` catalog: + + .. code-block:: none + + connector.name=example-http + metadata-uri=http://s3.amazonaws.com/presto-example/v1/example-metadata.json + +* Show correct error message when a catalog or schema does not exist. + +* Verify JVM requirements on startup. + +* Log an error when the JVM code cache is full. + +* Upgrade the embedded Discovery server to allow using + non-UUID values for the ``node.id`` property. diff --git a/presto-docs/src/main/sphinx/release/release-0.55.rst b/presto-docs/src/main/sphinx/release/release-0.55.rst new file mode 100644 index 00000000..55af8dca --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.55.rst @@ -0,0 +1,109 @@ +============ +Release 0.55 +============ + +RC Binary 2-4x Gain in CPU Efficiency +------------------------------------- + +Presto uses custom fast-path decoding logic for specific Hive file +formats. In this release we have added a fast path for for RCFile when using +the Binary SerDe (``LazyBinaryColumnarSerDe``). In our +micro benchmarks, we see a gain between 2x and 4x in CPU efficiency compared +to the generic (slow) path. Since Hive data decoding accounts for a +significant portion of the CPU time, this should +result in measurable gains for most queries over RC Binary encoded data. +Note that this optimization may not result in a reduction in latency +if your cluster is network or disk I/O bound. + +Hash Distributed Aggregations +----------------------------- + +``GROUP BY`` aggregations are now distributed across a fixed number of machines. +This is controlled by the property ``query.initial-hash-partitions`` set in +``etc/config.properties`` of the coordinator and workers. If the value is +larger than the number of machines available during query scheduling, Presto +will use all available machines. The default value is ``8``. + +The maximum memory size of an aggregation is now +``query.initial-hash-partitions`` times ``task.max-memory``. + +Simple Distinct Aggregations +---------------------------- + +We have added support for the ``DISTINCT`` argument qualifier for aggregation +functions. This is currently limited to queries without a ``GROUP BY`` clause and +where all the aggregation functions have the same input expression. For example:: + + SELECT count(DISTINCT country) + FROM users + +Support for complete ``DISTINCT`` functionality is in our roadmap. + +Range Predicate Pushdown +------------------------ + +We've modified the connector API to support range predicates in addition to simple equality predicates. +This lays the ground work for adding connectors to systems that support range +scans (e.g., HBase, Cassandra, JDBC, etc). + +In addition to receiving range predicates, the connector can also communicate +back the ranges of each partition for use in the query optimizer. This can be a +major performance gain for ``JOIN`` queries where one side of the join has +only a few partitions. For example:: + + SELECT * FROM data_1_year JOIN data_1_week USING (ds) + +If ``data_1_year`` and ``data_1_week`` are both partitioned on ``ds``, the +connector will report back that one table has partitions for 365 days and the +other table has partitions for only 7 days. Then the optimizer will limit +the scan of the ``data_1_year`` table to only the 7 days that could possible +match. These constraints are combined with other predicates in the +query to further limit the data scanned. + +.. note:: + This is a backwards incompatible change with the previous connector SPI, + so if you have written a connector, you will need to update your code + before deploying this release. + +json_array_get Function +----------------------- + +The :func:`json_array_get` function makes it simple to fetch a single element from a +scalar json array. + +Non-reserved Keywords +--------------------- + +The keywords ``DATE``, ``TIME``, ``TIMESTAMP``, and ``INTERVAL`` are no longer +reserved keywords in the grammar. This means that you can access a column +named ``date`` without quoting the identifier. + +CLI source Option +----------------- + +The Presto CLI now has an option to set the query source. The source +value is shown in the UI and is recorded in events. When using the CLI in +shell scripts it is useful to set the ``--source`` option to distinguish shell +scripts from normal users. + +SHOW SCHEMAS FROM +----------------- + +Although the documentation included the syntax ``SHOW SCHEMAS [FROM catalog]``, +it was not implemented. This release now implements this statement correctly. + +Hive Bucketed Table Fixes +------------------------- + +For queries over Hive bucketed tables, Presto will attempt to limit scans to +the buckets that could possible contain rows that match the WHERE clause. +Unfortunately, the algorithm we were using to select the buckets was not +correct, and sometimes we would either select the wrong files or fail to +select any files. We have aligned +the algorithm with Hive and now the optimization works as expected. + +We have also improved the algorithm for detecting tables that are not properly +bucketed. It is common for tables to declare bucketing in the Hive metadata, but +not actually be bucketed in HDFS. When Presto detects this case, it fallback to a full scan of the +partition. Not only does this change make bucketing safer, but it makes it easier +to migrate a table to use bucketing without rewriting all of the data. diff --git a/presto-docs/src/main/sphinx/release/release-0.56.rst b/presto-docs/src/main/sphinx/release/release-0.56.rst new file mode 100644 index 00000000..6c4e5f3f --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.56.rst @@ -0,0 +1,36 @@ +============ +Release 0.56 +============ + +Table Creation +-------------- + +Tables can be created from the result of a query:: + + CREATE TABLE orders_by_date AS + SELECT orderdate, sum(totalprice) AS price + FROM orders + GROUP BY orderdate + +Tables are created in Hive without partitions (unpartitioned) and use +RCFile with the Binary SerDe (``LazyBinaryColumnarSerDe``) as this is +currently the best format for Presto. + +.. note:: + This is a backwards incompatible change to ``ConnectorMetadata`` in the SPI, + so if you have written a connector, you will need to update your code before + deploying this release. We recommend changing your connector to extend from + the new ``ReadOnlyConnectorMetadata`` abstract base class unless you want to + support table creation. + +Cross Joins +----------- + +Cross joins are supported using the standard ANSI SQL syntax:: + + SELECT * + FROM a + CROSS JOIN b + +Inner joins that result in a cross join due to the join criteria evaluating +to true at analysis time are also supported. diff --git a/presto-docs/src/main/sphinx/release/release-0.57.rst b/presto-docs/src/main/sphinx/release/release-0.57.rst new file mode 100644 index 00000000..1a8bda35 --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.57.rst @@ -0,0 +1,56 @@ +============ +Release 0.57 +============ + +Distinct Aggregations +--------------------- + +The ``DISTINCT`` argument qualifier for aggregation functions is now +fully supported. For example:: + + SELECT country, count(DISTINCT city), count(DISTINCT age) + FROM users + GROUP BY country + +.. note:: + + :func:`approx_distinct` should be used in preference to this + whenever an approximate answer is allowable as it is substantially + faster and does not have any limits on the number of distinct items it + can process. ``COUNT(DISTINCT ...)`` must transfer every item over the + network and keep each distinct item in memory. + +Hadoop 2.x +---------- + +Use the ``hive-hadoop2`` connector to read Hive data from Hadoop 2.x. +See :doc:`/installation/deployment` for details. + +Amazon S3 +--------- + +All Hive connectors support reading data from +`Amazon S3 `_. +This requires two additional catalog properties for the Hive connector +to specify your AWS Access Key ID and Secret Access Key: + +.. code-block:: none + + hive.s3.aws-access-key=AKIAIOSFODNN7EXAMPLE + hive.s3.aws-secret-key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY + +Miscellaneous +------------- + +* Allow specifying catalog and schema in the :doc:`/installation/jdbc` URL. + +* Implement more functionality in the JDBC driver. + +* Allow certain custom ``InputFormat``\s to work by propagating + Hive serialization properties to the ``RecordReader``. + +* Many execution engine performance improvements. + +* Fix optimizer performance regression. + +* Fix weird ``MethodHandle`` exception. diff --git a/presto-docs/src/main/sphinx/release/release-0.58.rst b/presto-docs/src/main/sphinx/release/release-0.58.rst new file mode 100644 index 00000000..f239d687 --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.58.rst @@ -0,0 +1,16 @@ +============ +Release 0.58 +============ + +* Add first version of Cassandra connector. This plugin is still in + development and is not yet bundled with the server. See the ``README`` + in the plugin source directory for details. + +* Support UDFs for internal plugins. This is not yet part of the SPI + and is a stopgap feature intended for advanced users. UDFs must be + implemented using the internal Presto APIs which often change + substantially between releases. + +* Fix Hive connector semaphore release bug. + +* Fix handling of non-splittable files without blocks. diff --git a/presto-docs/src/main/sphinx/release/release-0.59.rst b/presto-docs/src/main/sphinx/release/release-0.59.rst new file mode 100644 index 00000000..ccc19e7b --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.59.rst @@ -0,0 +1,6 @@ +============ +Release 0.59 +============ + +* Fix hang in ``HiveSplitSource``. A query over a large table can hang + in split discovery due to a bug introduced in 0.57. diff --git a/presto-docs/src/main/sphinx/release/release-0.60.rst b/presto-docs/src/main/sphinx/release/release-0.60.rst new file mode 100644 index 00000000..032222e6 --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.60.rst @@ -0,0 +1,152 @@ +============ +Release 0.60 +============ + +JDBC improvements +----------------- + +The Presto version of the JDBC ``DatabaseMetaData`` interface now includes +proper implementations of ``getTables``, ``getSchemas`` and ``getCatalogs``. + +The JDBC driver is now always packaged as a standalone jar without any +dependencies. Previously, this artifact was published with the Maven +classifier ``standalone``. The new build does not publish this artifact +anymore. + +USE CATALOG and USE SCHEMA +-------------------------- + +The :doc:`/installation/cli` now supports ``USE CATALOG`` and +``USE SCHEMA``. + + +TPCH Connector +-------------- + +We have added a new connector that will generate synthetic data following the +TPC-H specification. This connector makes it easy to generate large datasets for +testing and bug reports. When generating bug reports, we encourage users to use +this catalog since it eases the process of reproducing the issue. The data is +generated dynamically for each query, so no disk space is used by this +connector. To add the ``tpch`` catalog to your system, create the catalog +property file ``etc/catalog/tpch.properties`` on both the coordinator and workers +with the following contents: + +.. code-block:: none + + connector.name=tpch + +Additionally, update the ``datasources`` property in the config properties file, +``etc/config.properties``, for the workers to include ``tpch``. + +SPI changes +----------- + +The ``Connector`` interface now has explicit methods for supplying the services +expected by the query engine. Previously, this was handled by a generic +``getService`` method. + +.. note:: + This is a backwards incompatible change to ``Connector`` in the SPI, + so if you have written a connector, you will need to update your code before + deploying this release. + +Additionally, we have added the ``NodeManager`` interface to the SPI to allow a +plugin to detect all nodes in the Presto cluster. This is important for some +connectors that can divide a table evenly between all nodes as long as the +connector knows how many nodes exist. To access the node manager, simply add +the following to the ``Plugin`` class: + +.. code-block:: java + + @Inject + public void setNodeManager(NodeManager nodeManager) + { + this.nodeManager = nodeManager; + } + +Optimizations +------------- + +DISTINCT LIMIT +~~~~~~~~~~~~~~ + +For queries with the following form:: + + SELECT DISTINCT ... + FROM T + LIMIT N + +We have added an optimization that stops the query as soon as ``N`` distinct +rows are found. + +Range predicates +~~~~~~~~~~~~~~~~ + +When optimizing a join, Presto analyzes the ranges of the partitions on each +side of a join and pushes these ranges to the other side. When tables have a +lot of partitions, this can result in a very large filter with one expression +for each partition. The optimizer now summarizes the predicate ranges to reduce +the complexity of the filters. + +Compound filters +~~~~~~~~~~~~~~~~ + +Complex expressions involving ``AND``, ``OR``, or ``NOT`` are now optimized by +the expression optimizer. + +Window functions +~~~~~~~~~~~~~~~~ + +Window functions with a ``PARTITION BY`` clause are now distributed based on the +partition key. + +Bug fixes +--------- + +* Scheduling + + In the changes to schedule splits in batches, we introduced two bugs that + resulted in an unbalanced workload across nodes which increases query latency. + The first problem was not inspecting the queued split count of the nodes while + scheduling the batch, and the second problem was not counting the splits + awaiting creation in the task executor. + +* JSON conversion of complex Hive types + + Presto converts complex Hive types (array, map, struct and union) into JSON. + Previously, numeric keys in maps were converted to numbers, not strings, + which is invalid as JSON only allows strings for object keys. This prevented + the :doc:`/functions/json` from working. + +* Hive hidden files + + Presto will now ignore files in Hive that start with an underscore ``_`` or + a dot ``.``. This matches the behavior of Hadoop MapReduce / Hive. + +* Failures incorrectly reported as no data + + Certain types of failures would result in the query appearing to succeed and + return an incomplete result (often zero rows). There was a race condition + between the error propagation and query teardown. In some cases, the query + would be torn down before the exception made it to the coordinator. This was a + regression introduced during the query teardown optimization work. There are + now tests to catch this type of bug. + +* Exchange client leak + + When a query finished early (e.g., limit or failure) and the exchange operator + was blocked waiting for data from other nodes, the exchange was not be closed + properly. This resulted in continuous failing HTTP requests which leaked + resources and produced large log files. + +* Hash partitioning + + A query with many ``GROUP BY`` items could fail due to an overflow in the hash + function. + +* Compiled NULL literal + + In some cases queries with a select expression like ``CAST(NULL AS varchar)`` + would fail due to a bug in the output type detection code in expression + compiler. diff --git a/presto-docs/src/main/sphinx/release/release-0.61.rst b/presto-docs/src/main/sphinx/release/release-0.61.rst new file mode 100644 index 00000000..403abcca --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.61.rst @@ -0,0 +1,111 @@ +============ +Release 0.61 +============ + +Add support for Table Value Constructors +---------------------------------------- + +Presto now supports the SQL table value constructor syntax to create inline tables. +The ``VALUES`` clause can be used anywhere a ``SELECT`` statement is allowed. +For example, as a top-level query:: + + VALUES ('a', 1), ('b', 2); + +.. code-block:: none + + _col0 | _col1 + -------+------- + a | 1 + b | 2 + (2 rows) + +Alternatively, in the ``FROM`` clause:: + + SELECT * + FROM ( + VALUES + ('a', 'ape'), + ('b', 'bear') + ) AS animal (letter, animal) + JOIN ( + VALUES + ('a', 'apple'), + ('b', 'banana') + ) AS fruit (letter, fruit) + USING (letter); + +.. code-block:: none + + letter | animal | letter | fruit + --------+--------+--------+--------- + a | ape | a | apple + b | bear | b | banana + (2 rows) + + +Cassandra +--------- + +* Add support for upper-case schema, table, and columns names. + +* Add support for ``DECIMAL`` type. + +Amazon S3 support +----------------- + +* Completely rewritten Hadoop FileSystem implementation for S3 using the Amazon AWS SDK, + with major performance and reliability improvements. + +* Add support for writing data to S3. + +Approximate Aggregation Queries +------------------------------- + +We have added experimental support for aggregate queries that return +approximate results with error bounds. This feature is designed to be +used with sampled tables generated using the ``TABLESAMPLE POISSONIZED RESCALED``. +For example, the following query will create a 1% sample:: + + CREATE TABLE lineitems_sample AS + SELECT * + FROM tpch.sf10.lineitems TABLESAMPLE POISSONIZED (1) RESCALED + +Then, to run an approximate query:: + + SELECT COUNT(*) + FROM lineitems_sample + APPROXIMATE AT 95.0 CONFIDENCE + + +.. code-block:: none + + _col0 + ---------------------------- + 5.991790345E7 +/- 14835.75 + (1 row) + + +To enable this feature you must add ``analyzer.experimental-syntax-enabled=true`` to your config. + +.. note:: + + The syntax and functionality for approximate queries is experimental and will likely + change in future versions. + + +Miscellaneous +------------- + +* General improvements to the JDBC driver, specifically with respect to metadata handling. + +* Fix division by zero errors in variance aggregation functions (``VARIANCE``, ``STDDEV``, etc.). + +* Fix a bug when using ``DISTINCT`` aggregations in the ``HAVING`` clause. + +* Fix an out of memory issue when writing large tables. + +* Fix a bug when using ``ORDER BY rand()`` in a ``JOIN`` query. + +* Fix handling of timestamps in maps and lists in Hive connector. + +* Add instrumentation for Hive metastore and HDFS API calls to track failures and latency. These metrics are exposed via JMX. diff --git a/presto-docs/src/main/sphinx/release/release-0.62.rst b/presto-docs/src/main/sphinx/release/release-0.62.rst new file mode 100644 index 00000000..3af5eed8 --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.62.rst @@ -0,0 +1,13 @@ +============ +Release 0.62 +============ + +* Fix an issue with active queries JMX counter reporting incorrect numbers + +* Hive binary map keys were not being decoded correctly + +* Performance improvements for ``APPROX_DISTINCT`` + +* Fix performance regression when planning queries over a large number of partitions + +* Minor improvement to coordinator UI when displaying long SQL queries diff --git a/presto-docs/src/main/sphinx/release/release-0.63.rst b/presto-docs/src/main/sphinx/release/release-0.63.rst new file mode 100644 index 00000000..bbb88f6a --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.63.rst @@ -0,0 +1,9 @@ +============ +Release 0.63 +============ + +* Minor improvements to coordinator UI + +* Minor planner optimization to avoid redundant computation in some cases + +* Error handling and classification improvements diff --git a/presto-docs/src/main/sphinx/release/release-0.64.rst b/presto-docs/src/main/sphinx/release/release-0.64.rst new file mode 100644 index 00000000..a2b5a0ef --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.64.rst @@ -0,0 +1,14 @@ +============ +Release 0.64 +============ + +* Fix approximate aggregation error bound calculation + +* Error handling and classification improvements + +* Fix ``GROUP BY`` failure when keys are too large + +* Add thread visualization UI at ``/ui/thread`` + +* Fix regression in ``CREATE TABLE`` that can cause column data to be swapped. + This bug was introduced in version 0.57. diff --git a/presto-docs/src/main/sphinx/release/release-0.65.rst b/presto-docs/src/main/sphinx/release/release-0.65.rst new file mode 100644 index 00000000..ad39eb84 --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.65.rst @@ -0,0 +1,7 @@ +============ +Release 0.65 +============ + +* Fix ``NullPointerException`` when tearing down queries + +* Fix exposed third-party dependencies in JDBC driver JAR diff --git a/presto-docs/src/main/sphinx/release/release-0.66.rst b/presto-docs/src/main/sphinx/release/release-0.66.rst new file mode 100644 index 00000000..d7bf6ec7 --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.66.rst @@ -0,0 +1,189 @@ +============ +Release 0.66 +============ + +Type System +----------- + +In this release we have replaced the existing simple fixed type system +with a fully extensible type system and have added several new types. +We have also expanded the function system to support custom +arithmetic, comparison and cast operators. For example, the new date/time +types include an operator for adding an ``INTERVAL`` to a ``TIMESTAMP``. + +Existing functions have been updated to operate on and return the +newly added types. For example, the ANSI color functions now operate +on a ``COLOR`` type, and the date/time functions operate on standard +SQL date/time types (described below). + +Finally, plugins can now provide custom types and operators in addition +to connectors and functions. This feature is highly experimental, so expect +the interfaces to change over the next few releases. Also, since in SQL +there is only one namespace for types, you should be careful to make names +for custom types unique as we will add other common SQL types to Presto +in the near future. + +Date/Time Types +--------------- + +Presto now supports all standard SQL date/time types: +``DATE``, ``TIME``, ``TIMESTAMP`` and ``INTERVAL``. +All of the date/time functions and language constructs now operate on these +types instead of ``BIGINT`` and perform temporal calculations correctly. +This was previously broken due to, for example, not being able to detect +whether an argument was a ``DATE`` or a ``TIMESTAMP``. +This change comes at the cost of breaking existing queries that perform +arithmetic operations directly on the ``BIGINT`` value returned from +the date/time functions. + +As part of this work, we have also added the :func:`date_trunc` function +which is convenient for grouping data by a time span. For example, you +can perform an aggregation by hour:: + + SELECT date_trunc('hour', timestamp_column), count(*) + FROM ... + GROUP BY 1 + +Time Zones +~~~~~~~~~~ + +This release has full support for time zone rules, which are needed to +perform date/time calculations correctly. Typically, the session time +zone is used for temporal calculations. This is the time zone of the +client computer that submits the query, if available. Otherwise, it is +the time zone of the server running the Presto coordinator. + +Queries that operate with time zones that follow daylight saving can +produce unexpected results. For example, if we run the following query +to add 24 hours using in the ``America/Los Angeles`` time zone:: + + SELECT date_add('hour', 24, TIMESTAMP '2014-03-08 09:00:00'); + => + 2014-03-09 10:00:00.000 + +The timestamp appears to only advance 23 hours. This is because on +March 9th clocks in ``America/Los Angeles`` are turned forward 1 hour, +so March 9th only has 23 hours. To advance the day part of the timestamp, +use the ``day`` unit instead:: + + SELECT date_add('day', 1, TIMESTAMP '2014-03-08 09:00:00'); + => + 2014-03-09 09:00:00.000 + +This works because the :func:`date_add` function treats the timestamp as +list of fields, adds the value to the specified field and then rolls any +overflow into the next higher field. + +Time zones are also necessary for parsing and printing timestamps. +Queries that use this functionality can also produce unexpected results. +For example, on the same machine:: + + SELECT TIMESTAMP '2014-03-09 02:30:00'; + +The above query causes an error because there was no 2:30 AM on March 9th +in ``America/Los_Angeles`` due to a daylight saving time transition. + +In addition to normal ``TIMESTAMP`` values, Presto also supports the +``TIMESTAMP WITH TIME ZONE`` type, where every value has an explicit time zone. +For example, the following query creates a ``TIMESTAMP WITH TIME ZONE``:: + + SELECT TIMESTAMP '2014-03-14 09:30:00 Europe/Berlin'; + => + 2014-03-14 09:30:00.000 Europe/Berlin + +You can also change the time zone of an existing timestamp using the +``AT TIME ZONE`` clause:: + + SELECT TIMESTAMP '2014-03-14 09:30:00 Europe/Berlin' + AT TIME ZONE 'America/Los_Angeles'; + => + 2014-03-14 01:30:00.000 America/Los_Angeles + +Both timestamps represent the same instant in time; +they differ only in the time zone used to print them. + +The time zone of the session can be set on a per-query basis using the +``X-Presto-Time-Zone`` HTTP header, or via the +``PrestoConnection.setTimeZoneId(String)`` method in the JDBC driver. + +Localization +~~~~~~~~~~~~ + +In addition to time zones, the language of the user is important when +parsing and printing date/time types. This release adds localization +support to the Presto engine and functions that require it: +:func:`date_format` and :func:`date_parse`. +For example, if we set the language to Spanish:: + + SELECT date_format(TIMESTAMP '2001-01-09 09:04', '%M'); + => + enero + +If we set the language to Japanese:: + + SELECT date_format(TIMESTAMP '2001-01-09 09:04', '%M'); + => + 1月 + +The language of the session can be set on a per-query basis using the +``X-Presto-Language`` HTTP header, or via the +``PrestoConnection.setLocale(Locale)`` method in the JDBC driver. + +Optimizations +------------- + +* We have upgraded the Hive connector to Hive 0.12 which includes + performance improvements for RCFile. + +* ``GROUP BY`` and ``JOIN`` operators are now compiled to byte code + and are significantly faster. + +* Reduced memory usage of ``GROUP BY`` and ``SELECT DISTINCT``, + which previously required several megabytes of memory + per operator, even when the number of groups was small. + +* The planner now optimizes function call arguments. This should improve + the performance of queries that contain complex expressions. + +* Fixed a performance regression in the HTTP client. The recent HTTP client + upgrade was using inadvertently GZIP compression and has a bug in the + buffer management resulting in high CPU usage. + +SPI changes +----------- + +In this release we have made a number of backward incompatible changes to the SPI: + +* Added ``Type`` and related interfaces +* ``ConnectorType`` in metadata has been replaced with ``Type`` +* Renamed ``TableHandle`` to ``ConnectorTableHandle`` +* Renamed ``ColumnHandle`` to ``ConnectorColumnHandle`` +* Renamed ``Partition`` to ``ConnectorPartition`` +* Renamed ``PartitionResult`` to ``ConnectorPartitionResult`` +* Renamed ``Split`` to ``ConnectorSplit`` +* Renamed ``SplitSource`` to ``ConnectorSplitSource`` +* Added a ``ConnectorSession`` parameter to most ``ConnectorMetadata`` methods +* Removed most ``canHandle`` methods + +General Bug Fixes +----------------- + +* Fixed CLI hang after using ``USE CATALOG`` or ``USE SCHEMA`` +* Implicit coercions in aggregations now work as expected +* Nulls in expressions work as expected +* Fixed memory leak in compiler +* Fixed accounting bug in task memory usage +* Fixed resource leak caused by abandoned queries +* Fail queries immediately on unrecoverable data transport errors + +Hive Bug Fixes +-------------- + +* Fixed parsing of timestamps in the Hive RCFile Text SerDe (``ColumnarSerDe``) + by adding configuration to set the time zone originally used when writing data + +Cassandra Bug Fixes +------------------- + +* Auto-reconnect if Cassandra session dies +* Format collection types as JSON diff --git a/presto-docs/src/main/sphinx/release/release-0.67.rst b/presto-docs/src/main/sphinx/release/release-0.67.rst new file mode 100644 index 00000000..c93d7b8b --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.67.rst @@ -0,0 +1,19 @@ +============ +Release 0.67 +============ + +* Fix resource leak in Hive connector + +* Improve error categorization in event logging + +* Fix planning issue with certain queries using window functions + +SPI changes +----------- + +The ``ConnectorSplitSource`` interface now extends ``Closeable``. + +.. note:: + This is a backwards incompatible change to ``ConnectorSplitSource`` in the SPI, + so if you have written a connector, you will need to update your code before + deploying this release. diff --git a/presto-docs/src/main/sphinx/release/release-0.68.rst b/presto-docs/src/main/sphinx/release/release-0.68.rst new file mode 100644 index 00000000..a578aad6 --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.68.rst @@ -0,0 +1,10 @@ +============ +Release 0.68 +============ + +* Fix a regression in the handling of Hive tables that are bucketed on a + string column. This caused queries that could take advantage of bucketing + on such tables to choose the wrong bucket and thus would not match any + rows for the table. This regression was introduced in 0.66. + +* Fix double counting of bytes and rows when reading records diff --git a/presto-docs/src/main/sphinx/release/release-0.69.rst b/presto-docs/src/main/sphinx/release/release-0.69.rst new file mode 100644 index 00000000..e03d0b32 --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.69.rst @@ -0,0 +1,102 @@ +============ +Release 0.69 +============ + +.. warning:: + + The following config properties must be removed from the + ``etc/config.properties`` file on both the coordinator and workers: + + * ``presto-metastore.db.type`` + * ``presto-metastore.db.filename`` + + Additionally, the ``datasources`` property is now deprecated + and should also be removed (see `Datasource Configuration`_). + +Prevent Scheduling Work on Coordinator +-------------------------------------- + +We have a new config property, ``node-scheduler.include-coordinator``, +that allows or disallows scheduling work on the coordinator. +Previously, tasks like final aggregations could be scheduled on the +coordinator. For larger clusters, processing work on the coordinator +can impact query performance because the machine's resources are not +available for the critical task of scheduling, managing and monitoring +query execution. + +We recommend setting this property to ``false`` for the coordinator. +See :ref:`config_properties` for an example. + +Datasource Configuration +------------------------ + +The ``datasources`` config property has been deprecated. +Please remove it from your ``etc/config.properties`` file. +The datasources configuration is now automatically generated based +on the ``node-scheduler.include-coordinator`` property +(see `Prevent Scheduling Work on Coordinator`_). + +Raptor Connector +---------------- + +Presto has an extremely experimental connector that was previously called +the ``native`` connector and was intertwined with the main Presto code +(it was written before Presto had connectors). This connector is now +named ``raptor`` and lives in a separate plugin. + +As part of this refactoring, the ``presto-metastore.db.type`` and +``presto-metastore.db.filename`` config properties no longer exist +and must be removed from ``etc/config.properties``. + +The Raptor connector stores data on the Presto machines in a +columnar format using the same layout that Presto uses for in-memory +data. Currently, it has major limitations: lack of replication, +dropping a table does not reclaim the storage, etc. It is only +suitable for experimentation, temporary tables, caching of data from +slower connectors, etc. The metadata and data formats are subject to +change in incompatible ways between releases. + +If you would like to experiment with the connector, create a catalog +properties file such as ``etc/catalog/raptor.properties`` on both the +coordinator and workers that contains the following: + +.. code-block:: none + + connector.name=raptor + metadata.db.type=h2 + metadata.db.filename=var/data/db/MetaStore + +Machine Learning Functions +-------------------------- + +Presto now has functions to train and use machine learning models +(classifiers and regressors). This is currently only a proof of concept +and is not ready for use in production. Example usage is as follows:: + + SELECT evaluate_classifier_predictions(label, classify(features, model)) + FROM ( + SELECT learn_classifier(label, features) AS model + FROM training_data + ) + CROSS JOIN validation_data + +In the above example, the column ``label`` is a ``bigint`` and the column +``features`` is a map of feature identifiers to feature values. The feature +identifiers must be integers (encoded as strings because JSON only supports +strings for map keys) and the feature values are numbers (floating point). + +Variable Length Binary Type +--------------------------- + +Presto now supports the ``varbinary`` type for variable length binary data. +Currently, the only supported function is :func:`length`. +The Hive connector now maps the Hive ``BINARY`` type to ``varbinary``. + +General Changes +--------------- + +* Add missing operator: ``timestamp with time zone`` - ``interval year to month`` +* Support explaining sampled queries +* Add JMX stats for abandoned and canceled queries +* Add ``javax.inject`` to parent-first class list for plugins +* Improve error categorization in event logging diff --git a/presto-docs/src/main/sphinx/release/release-0.70.rst b/presto-docs/src/main/sphinx/release/release-0.70.rst new file mode 100644 index 00000000..fb1e9dfa --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.70.rst @@ -0,0 +1,103 @@ +============ +Release 0.70 +============ + +.. warning:: + + This release contained a packaging error that resulted in an + unusable server tarball. Do not use this release. + +Views +----- + +We have added support for creating views within Presto. +Views are defined using Presto syntax but are stored (as blobs) +by connectors. Currently, views are supported by the +Raptor and Hive connectors. For the Hive connector, views are +stored within the Hive metastore as Hive views, but they cannot +be queried by Hive, nor can Hive views be queried by Presto. + +See :doc:`/sql/create-view` and :doc:`/sql/drop-view` +for details and examples. + +DUAL Table +---------- + +The synthetic ``DUAL`` table is no longer supported. As an alternative, please +write your queries without a ``FROM`` clause or use the ``VALUES`` syntax. + +Presto Verifier +--------------- + +There is a new project, :doc:`/installation/verifier`, which can be used to +verify a set of queries against two different clusters. + +Connector Improvements +---------------------- + +* Connectors can now add hidden columns to a table. Hidden columns are not + displayed in ``DESCRIBE`` or ``information_schema``, and are not + considered for ``SELECT *``. As an example, we have added a hidden + ``row_number`` column to the ``tpch`` connector. + +* Presto contains an extensive test suite to verify the correctness. This test + suite has been extracted into the ``presto-test`` module for use during + connector development. For an example, see ``TestRaptorDistributedQueries``. + +Machine Learning Functions +-------------------------- + +We have added two new machine learning functions, which can be used +by advanced users familiar with LIBSVM. The functions are +``learn_libsvm_classifier`` and ``learn_libsvm_regressor``. Both take a +parameters string which has the form ``key=value,key=value`` + +General Changes +--------------- + +* New comparison functions: :func:`greatest` and :func:`least` + +* New window functions: :func:`first_value`, :func:`last_value`, and :func:`nth_value` + +* We have added a config option to disable falling back to the interpreter when + expressions fail to be compiled to bytecode. To set this option, add  + ``compiler.interpreter-enabled=false`` to ``etc/config.properties``. + This will force certain queries to fail rather than running slowly. + +* ``DATE`` values are now implicitly coerced to ``TIMESTAMP`` and ``TIMESTAMP WITH TIME ZONE`` + by setting the hour/minute/seconds to ``0`` with respect to the session timezone. + +* Minor performance optimization when planning queries over tables with tens of + thousands of partitions or more. + +* Fixed a bug when planning ``ORDER BY ... LIMIT`` queries which could result in + duplicate and un-ordered results under rare conditions. + +* Reduce the size of stats collected from tasks, which dramatically reduces + garbage generation and improves coordinator stability. + +* Fix compiler cache for expressions. + +* Fix processing of empty or commented out statements in the CLI. + +Hive Changes +------------ + +* There are two new configuration options for the Hive connector, + ``hive.max-initial-split-size``, which configures the size of the + initial splits, and ``hive.max-initial-splits``, which configures + the number of initial splits. This can be useful for speeding up small + queries, which would otherwise have low parallelism. + +* The Hive connector will now consider all tables with a non-empty value + for the table property ``presto_offline`` to be offline. The value of the + property will be used in the error message. + +* We have added support for for ``DROP TABLE`` in the hive connector. + By default, this feature is not enabled. To enable it, set + ``hive.allow-drop-table=true`` in your Hive catalog properties file. + +* Ignore subdirectories when generating splits + (this now matches the non-recursive behavior of Hive). + +* Fix handling of maps with null keys. diff --git a/presto-docs/src/main/sphinx/release/release-0.71.rst b/presto-docs/src/main/sphinx/release/release-0.71.rst new file mode 100644 index 00000000..ed099842 --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.71.rst @@ -0,0 +1,8 @@ +============ +Release 0.71 +============ + +* Fix packaging issue that resulted in an unusable server tarball + for the 0.70 release + +* Fix logging in Hive connector when using Amazon S3 diff --git a/presto-docs/src/main/sphinx/release/release-0.72.rst b/presto-docs/src/main/sphinx/release/release-0.72.rst new file mode 100644 index 00000000..c180ae66 --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.72.rst @@ -0,0 +1,6 @@ +============ +Release 0.72 +============ + +* Fix infinite loop bug in Hive RCFile reader when decoding a Map + with a null key diff --git a/presto-docs/src/main/sphinx/release/release-0.73.rst b/presto-docs/src/main/sphinx/release/release-0.73.rst new file mode 100644 index 00000000..5907e30b --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.73.rst @@ -0,0 +1,18 @@ +============ +Release 0.73 +============ + +Cassandra Plugin +---------------- + +The Cassandra connector now supports CREATE TABLE and DROP TABLE. Additionally, +the connector now takes into account Cassandra indexes when generating CQL. +This release also includes several bug fixes and performance improvements. + +General Changes +--------------- + +* New window functions: :func:`lead`, and :func:`lag` + +* New scalar function: :func:`json_size` + diff --git a/presto-docs/src/main/sphinx/release/release-0.74.rst b/presto-docs/src/main/sphinx/release/release-0.74.rst new file mode 100644 index 00000000..023d243f --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.74.rst @@ -0,0 +1,32 @@ +============ +Release 0.74 +============ + +Bytecode Compiler +----------------- + +This version includes new infrastructure for bytecode compilation, and lays the groundwork for future improvements. +There should be no impact in performance or correctness with the new code, but we have added a flag to revert to the +old implementation in case of issues. To do so, add ``compiler.new-bytecode-generator-enabled=false`` to +``etc/config.properties`` in the coordinator and workers. + +Hive Storage Format +------------------- + +The storage format to use when writing data to Hive can now be configured via the ``hive.storage-format`` option +in your Hive catalog properties file. Valid options are ``RCBINARY``, ``RCTEXT``, ``SEQUENCEFILE`` and ``TEXTFILE``. +The default format if the property is not set is ``RCBINARY``. + +General Changes +--------------- + +* Show column comments in ``DESCRIBE`` +* Add :func:`try_cast` which works like :func:`cast` but returns ``null`` if the cast fails +* ``nullif`` now correctly returns a value with the type of the first argument +* Fix an issue with :func:`timezone_hour` returning results in milliseconds instead of hours +* Show a proper error message when analyzing queries with non-equijoin clauses +* Improve "too many failures" error message when coordinator can't talk to workers +* Minor optimization of :func:`json_size` function +* Improve feature normalization algorithm for machine learning functions +* Add exponential back-off to the S3 FileSystem retry logic +* Improve CPU efficiency of semi-joins diff --git a/presto-docs/src/main/sphinx/release/release-0.75.rst b/presto-docs/src/main/sphinx/release/release-0.75.rst new file mode 100644 index 00000000..a667ca38 --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.75.rst @@ -0,0 +1,111 @@ +============ +Release 0.75 +============ + +Hive Changes +------------ + +* The Hive S3 file system has a new configuration option, + ``hive.s3.max-connections``, which sets the maximum number of + connections to S3. The default has been increased from ``50`` to ``500``. + +* The Hive connector now supports renaming tables. By default, this feature + is not enabled. To enable it, set ``hive.allow-rename-table=true`` in + your Hive catalog properties file. + +General Changes +--------------- + +* Optimize :func:`count` with a constant to execute as the much faster ``count(*)`` +* Add support for binary types to the JDBC driver +* The legacy byte code compiler has been removed +* New aggregation framework (~10% faster) +* Added :func:`max_by` aggregation function +* The ``approx_avg()`` function has been removed. Use :func:`avg` instead. +* Fixed parsing of ``UNION`` queries that use both ``DISTINCT`` and ``ALL`` +* Fixed cross join planning error for certain query shapes +* Added hex and base64 conversion functions for varbinary +* Fix the ``LIKE`` operator to correctly match against values that contain + multiple lines. Previously, it would stop matching at the first newline. +* Add support for renaming tables using the :doc:`/sql/alter-table` statement. +* Add basic support for inserting data using the :doc:`/sql/insert` statement. + This is currently only supported for the Raptor connector. + +JSON Function Changes +--------------------- + +The :func:`json_extract` and :func:`json_extract_scalar` functions now support +the square bracket syntax:: + + SELECT json_extract(json, '$.store[book]'); + SELECT json_extract(json, '$.store["book name"]'); + +As part of this change, the set of characters allowed in a non-bracketed +path segment has been restricted to alphanumeric, underscores and colons. +Additionally, colons cannot be used in a un-quoted bracketed path segment. +Use the new bracket syntax with quotes to match elements that contain +special characters. + +Scheduler Changes +----------------- + +The scheduler now assigns splits to a node based on the current load on the node across all queries. +Previously, the scheduler load balanced splits across nodes on a per query level. Every node can have +``node-scheduler.max-splits-per-node`` splits scheduled on it. To avoid starvation of small queries, +when the node already has the maximum allowable splits, every task can schedule at most +``node-scheduler.max-pending-splits-per-node-per-task`` splits on the node. + +Row Number Optimizations +------------------------ + +Queries that use the :func:`row_number` function are substantially faster +and can run on larger result sets for two types of queries. + +Performing a partitioned limit that choses ``N`` arbitrary rows per +partition is a streaming operation. The following query selects +five arbitrary rows from ``orders`` for each ``orderstatus``:: + + SELECT * FROM ( + SELECT row_number() OVER (PARTITION BY orderstatus) AS rn, + custkey, orderdate, orderstatus + FROM orders + ) WHERE rn <= 5; + +Performing a partitioned top-N that chooses the maximum or minimum +``N`` rows from each partition now uses significantly less memory. +The following query selects the five oldest rows based on ``orderdate`` +from ``orders`` for each ``orderstatus``:: + + SELECT * FROM ( + SELECT row_number() OVER (PARTITION BY orderstatus ORDER BY orderdate) AS rn, + custkey, orderdate, orderstatus + FROM orders + ) WHERE rn <= 5; + +Use the :doc:`/sql/explain` statement to see if any of these optimizations +have been applied to your query. + +SPI Changes +----------- + +The core Presto engine no longer automatically adds a column for ``count(*)`` +queries. Instead, the ``RecordCursorProvider`` will receive an empty list of +column handles. + +The ``Type`` and ``Block`` APIs have gone through a major refactoring in this +release. The main focus of the refactoring was to consolidate all type specific +encoding logic in the type itself, which makes types much easier to implement. +You should consider ``Type`` and ``Block`` to be a beta API as we expect +further changes in the near future. + +To simplify the API, ``ConnectorOutputHandleResolver`` has been merged into +``ConnectorHandleResolver``. Additionally, ``ConnectorHandleResolver``, +``ConnectorRecordSinkProvider`` and ``ConnectorMetadata`` were modified to +support inserts. + +.. note:: + This is a backwards incompatible change with the previous connector and + type SPI, so if you have written a connector or type, you will need to update + your code before deploying this release. In particular, make sure your + connector can handle an empty column handles list (this can be verified + by running ``SELECT count(*)`` on a table from your connector). diff --git a/presto-docs/src/main/sphinx/release/release-0.76.rst b/presto-docs/src/main/sphinx/release/release-0.76.rst new file mode 100644 index 00000000..c3cba886 --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.76.rst @@ -0,0 +1,74 @@ +============ +Release 0.76 +============ + +Kafka Connector +--------------- + +This release adds a connector that allows querying of `Apache Kafka`_ topic data +from Presto. Topics can be live and repeated queries will pick up new data. + +Apache Kafka 0.8+ is supported although Apache Kafka 0.8.1+ is recommended. +There is extensive :doc:`documentation ` about configuring +the connector and a :doc:`tutorial ` to get started. + +.. _Apache Kafka: https://kafka.apache.org/ + +MySQL and PostgreSQL Connectors +------------------------------- + +This release adds the :doc:`/connector/mysql` and :doc:`/connector/postgresql` +for querying and creating tables in external relational databases. These can +be used to join or copy data between different systems like MySQL and Hive, +or between two different MySQL or PostgreSQL instances, or any combination. + +Cassandra Changes +----------------- + +The :doc:`/connector/cassandra` configuration properties +``cassandra.client.read-timeout`` and ``cassandra.client.connect-timeout`` +are now specified using a duration rather than milliseconds (this makes +them consistent with all other such properties in Presto). If you were +previously specifying a value such as ``25``, change it to ``25ms``. + +The retry policy for the Cassandra client is now configurable via the +``cassandra.retry-policy`` property. In particular, the custom ``BACKOFF`` +retry policy may be useful. + +Hive Changes +------------ + +The new :doc:`/connector/hive` configuration property ``hive.s3.socket-timeout`` +allows changing the socket timeout for queries that read or write to Amazon S3. +Additionally, the previously added ``hive.s3.max-connections`` property +was not respected and always used the default of ``500``. + +Hive allows the partitions in a table to have a different schema than the +table. In particular, it allows changing the type of a column without +changing the column type of existing partitions. The Hive connector does +not support this and could previously return garbage data for partitions +stored using the RCFile Text format if the column type was converted from +a non-numeric type such as ``STRING`` to a numeric type such as ``BIGINT`` +and the actual data in existing partitions was not numeric. The Hive +connector now detects this scenario and fails the query after the +partition metadata has been read. + +The property ``hive.storage-format`` is broken and has been disabled. It +sets the storage format on the metadata but always writes the table using +``RCBINARY``. This will be implemented in a future release. + +General Changes +--------------- + +* Fix hang in verifier when an exception occurs. +* Fix :func:`chr` function to work with Unicode code points instead of ASCII code points. +* The JDBC driver no longer hangs the JVM on shutdown (all threads are daemon threads). +* Fix incorrect parsing of function arguments. +* The bytecode compiler now caches generated code for join and group byqueries, + which should improve performance and CPU efficiency for these types of queries. +* Improve planning performance for certain trivial queries over tables with lots of partitions. +* Avoid creating large output pages. This should mitigate some cases of + *"Remote page is too large"* errors. +* The coordinator/worker communication layer is now fully asynchronous. + Specifically, long-poll requests no longer tie up a thread on the worker. + This makes heavily loaded clusters more efficient. diff --git a/presto-docs/src/main/sphinx/release/release-0.77.rst b/presto-docs/src/main/sphinx/release/release-0.77.rst new file mode 100644 index 00000000..5ee07a5a --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.77.rst @@ -0,0 +1,42 @@ +============ +Release 0.77 +============ + +Parametric Types +---------------- +Presto now has a framework for implementing parametric types and functions. +Support for :ref:`array_type` and :ref:`map_type` types has been added, including the element accessor +operator ``[]``, and new :doc:`/functions/array`. + +Streaming Index Joins +--------------------- +Index joins will now switch to use a key-by-key streaming join if index +results fail to fit in the allocated index memory space. + +Distributed Joins +----------------- +Joins where both tables are distributed are now supported. This allows larger tables to be joined, +and can be enabled with the ``distributed-joins-enabled`` flag. It may perform worse than the existing +broadcast join implementation because it requires redistributing both tables. +This feature is still experimental, and should be used with caution. + +Hive Changes +------------ +* Handle spurious ``AbortedException`` when closing S3 input streams +* Add support for ORC, DWRF and Parquet in Hive +* Add support for ``DATE`` type in Hive +* Fix performance regression in Hive when reading ``VARCHAR`` columns + +Kafka Changes +------------- +* Fix Kafka handling of default port +* Add support for Kafka messages with a null key + +General Changes +--------------- +* Fix race condition in scheduler that could cause queries to hang +* Add ConnectorPageSource which is a more efficient interface for column-oriented sources +* Add support for string partition keys in Cassandra +* Add support for variable arity functions +* Add support for :func:`count` for all types +* Fix bug in HashAggregation that could cause the operator to go in an infinite loop diff --git a/presto-docs/src/main/sphinx/release/release-0.78.rst b/presto-docs/src/main/sphinx/release/release-0.78.rst new file mode 100644 index 00000000..ffd23ec1 --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.78.rst @@ -0,0 +1,59 @@ +============ +Release 0.78 +============ + +ARRAY and MAP Types in Hive Connector +------------------------------------- + +The Hive connector now returns arrays and maps instead of json encoded strings, +for columns whose underlying type is array or map. Please note that this is a backwards +incompatible change, and the :doc:`/functions/json` will no longer work on these columns, +unless you :func:`cast` them to the ``json`` type. + +Session Properties +------------------ + +The Presto session can now contain properties, which can be used by the Presto +engine or connectors to customize the query execution. There is a separate +namespace for the Presto engine and each catalog. A property for a catalog is +simplify prefixed with the catalog name followed by ``.`` (dot). A connector +can retrieve the properties for the catalog using +``ConnectorSession.getProperties()``. + +Session properties can be set using the ``--session`` command line argument to +the Presto CLI. For example: + +.. code-block:: none + + presto-cli --session color=red --session size=large + +For JDBC, the properties can be set by unwrapping the ``Connection`` as follows: + +.. code-block:: java + + connection.unwrap(PrestoConnection.class).setSessionProperty("name", "value"); + +.. note:: + This feature is a work in progress and will change in a future release. + Specifically, we are planning to require preregistration of properties so + the user can list available session properties and so the engine can verify + property values. Additionally, the Presto grammar will be extended to + allow setting properties via a query. + +Hive Changes +------------ + +* Add ``storage_format`` session property to override format used for creating tables. +* Add write support for ``VARBINARY``, ``DATE`` and ``TIMESTAMP``. +* Add support for partition keys of type ``TIMESTAMP``. +* Add support for partition keys with null values (``__HIVE_DEFAULT_PARTITION__``). +* Fix ``hive.storage-format`` option (see :doc:`release-0.76`). + +General Changes +--------------- + +* Fix expression optimizer, so that it runs in linear time instead of exponential time. +* Add :func:`cardinality` for maps. +* Fix race condition in SqlTask creation which can cause queries to hang. +* Fix ``node-scheduler.multiple-tasks-per-node-enabled`` option. +* Fix an exception when planning a query with a UNION under a JOIN. diff --git a/presto-docs/src/main/sphinx/release/release-0.79.rst b/presto-docs/src/main/sphinx/release/release-0.79.rst new file mode 100644 index 00000000..9816040c --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.79.rst @@ -0,0 +1,20 @@ +============ +Release 0.79 +============ + +Hive Changes +------------ + +* Add configuration option ``hive.force-local-scheduling`` and session property + ``force_local_scheduling`` to force local scheduling of splits. +* Add new experimental optimized RCFile reader. The reader can be enabled by + setting the configuration option ``hive.optimized-reader.enabled`` or session + property ``optimized_reader_enabled``. + +General Changes +--------------- + +* Add support for :ref:`unnest`, which can be used as a replacement for the `explode()` function in Hive. +* Fix a bug in the scan operator that can cause data to be missed. It currently only affects queries + over ``information_schema`` or ``sys`` tables, metadata queries such as ``SHOW PARTITIONS`` and connectors + that implement the ``ConnectorPageSource`` interface. diff --git a/presto-docs/src/main/sphinx/release/release-0.80.rst b/presto-docs/src/main/sphinx/release/release-0.80.rst new file mode 100644 index 00000000..cc21c11b --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.80.rst @@ -0,0 +1,98 @@ +============ +Release 0.80 +============ + +New Hive ORC Reader +------------------- + +We have added a new ORC reader implementation. The new reader supports vectorized +reads, lazy loading, and predicate push down, all of which make the reader more +efficient and typically reduces wall clock time for a query. Although the new +reader has been heavily tested, it is an extensive rewrite of the Apache Hive +ORC reader, and may have some latent issues. If you are seeing issues, you can +disable the new reader on a per-query basis by setting the +``.optimized_reader_enabled`` session property, or you can disable +the reader by default by setting the Hive catalog property +``hive.optimized-reader.enabled=false``. + +Hive Changes +------------ + +* The maximum retry time for the Hive S3 file system can be configured + by setting ``hive.s3.max-retry-time``. +* Fix Hive partition pruning for null keys (i.e. ``__HIVE_DEFAULT_PARTITION__``). + +Cassandra Changes +----------------- + +* Update Cassandra driver to 2.1.0. +* Map Cassandra ``TIMESTAMP`` type to Presto ``TIMESTAMP`` type. + +"Big Query" Support +------------------- + +We've added experimental support for "big" queries. This provides a separate +queue controlled by the following properties: + +* ``experimental.max-concurrent-big-queries`` +* ``experimental.max-queued-big-queries`` + +There are separate configuration options for queries that are submitted with +the ``experimental_big_query`` session property: + +* ``experimental.big-query-initial-hash-partitions`` +* ``experimental.big-query-max-task-memory`` + +Queries submitted with this property will use hash distribution for all joins. + +Metadata-Only Query Optimization +-------------------------------- + +We now support an optimization that rewrites aggregation queries that are insensitive to the +cardinality of the input (e.g., :func:`max`, :func:`min`, ``DISTINCT`` aggregates) to execute +against table metadata. + +For example, if ``key``, ``key1`` and ``key2`` are partition keys, the following queries +will benefit:: + + SELECT min(key), max(key) FROM t; + + SELECT DISTINCT key FROM t; + + SELECT count(DISTINCT key) FROM t; + + SELECT count(DISTINCT key + 5) FROM t; + + SELECT count(DISTINCT key) FROM (SELECT key FROM t ORDER BY 1 LIMIT 10); + + SELECT key1, count(DISTINCT key2) FROM t GROUP BY 1; + +This optimization is turned off by default. To turn it on, add ``optimizer.optimize-metadata-queries=true`` +to the coordinator config properties. + +.. warning:: + + This optimization will cause queries to produce incorrect results if + the connector allows partitions to contain no data. For example, the + Hive connector will produce incorrect results if your Hive warehouse + contains partitions without data. + +General Changes +--------------- + +* Add support implicit joins. The following syntax is now allowed:: + + SELECT * FROM a, b WHERE a.id = b.id; + +* Add property ``task.verbose-stats`` to enable verbose statistics collection for + tasks. The default is ``false``. +* Format binary data in the CLI as a hex dump. +* Add approximate numeric histogram function :func:`numeric_histogram`. +* Add :func:`array_sort` function. +* Add :func:`map_keys` and :func:`map_values` functions. +* Make :func:`row_number` completely streaming. +* Add property ``task.max-partial-aggregation-memory`` to configure the memory limit + for the partial step of aggregations. +* Fix exception when processing queries with an ``UNNEST`` operation where the output was not used. +* Only show query progress in UI after the query has been fully scheduled. +* Add query execution visualization to the coordinator UI. It can be accessed via the query details page. diff --git a/presto-docs/src/main/sphinx/release/release-0.81.rst b/presto-docs/src/main/sphinx/release/release-0.81.rst new file mode 100644 index 00000000..f2875856 --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.81.rst @@ -0,0 +1,15 @@ +============ +Release 0.81 +============ + +Hive Changes +------------ + +* Fix ORC predicate pushdown. +* Fix column selection in RCFile. + +General Changes +--------------- + +* Fix handling of null and out-of-range offsets for + :func:`lead`, :func:`lag` and :func:`nth_value` functions. diff --git a/presto-docs/src/main/sphinx/release/release-0.82.rst b/presto-docs/src/main/sphinx/release/release-0.82.rst new file mode 100644 index 00000000..c6d9312f --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.82.rst @@ -0,0 +1,11 @@ +============ +Release 0.82 +============ + +* Presto now supports the :ref:`row_type` type, and all Hive structs are + converted to ROWs, instead of JSON encoded VARCHARs. +* Add :func:`current_timezone` function. +* Improve planning performance for queries with thousands of columns. +* Fix a regression that was causing excessive memory allocation and GC pressure + in the coordinator. + diff --git a/presto-docs/src/main/sphinx/release/release-0.83.rst b/presto-docs/src/main/sphinx/release/release-0.83.rst new file mode 100644 index 00000000..e3d44130 --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.83.rst @@ -0,0 +1,20 @@ +============ +Release 0.83 +============ + +Raptor Changes +-------------- +* Raptor now enables specifying the backup storage location. This feature is highly experimental. +* Fix the handling of shards not assigned to any node. + +General Changes +--------------- + +* Fix resource leak in query queues. +* Fix NPE when writing null ``ARRAY/MAP`` to Hive. +* Fix :func:`json_array_get` to handle nested structures. +* Fix ``UNNEST`` on null collections. +* Fix a regression where queries that fail during parsing or analysis do not expire. +* Make ``JSON`` type comparable. +* Added an optimization for hash aggregations. This optimization is turned off by default. + To turn it on, add ``optimizer.optimize-hash-generation=true`` to the coordinator config properties. diff --git a/presto-docs/src/main/sphinx/release/release-0.84.rst b/presto-docs/src/main/sphinx/release/release-0.84.rst new file mode 100644 index 00000000..7a958ca4 --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.84.rst @@ -0,0 +1,10 @@ +============ +Release 0.84 +============ + +* Fix handling of ``NaN`` and infinity in ARRAYs +* Fix approximate queries that use ``JOIN`` +* Reduce excessive memory allocation and GC pressure in the coordinator +* Fix an issue where setting ``node-scheduler.location-aware-scheduling-enabled=false`` + would cause queries to fail for connectors whose splits were not remotely accessible +* Fix error when running ``COUNT(*)`` over tables in ``information_schema`` and ``sys`` diff --git a/presto-docs/src/main/sphinx/release/release-0.85.rst b/presto-docs/src/main/sphinx/release/release-0.85.rst new file mode 100644 index 00000000..0d48c2c0 --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.85.rst @@ -0,0 +1,6 @@ +============ +Release 0.85 +============ + +* Improve query planning performance for tables with large numbers of partitions. +* Fix issue when using ``JSON`` values in ``GROUP BY`` expressions. diff --git a/presto-docs/src/main/sphinx/release/release-0.86.rst b/presto-docs/src/main/sphinx/release/release-0.86.rst new file mode 100644 index 00000000..de99b8df --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.86.rst @@ -0,0 +1,27 @@ +============ +Release 0.86 +============ + +General Changes +--------------- + +* Add support for inequality ``INNER JOIN`` when each term of the condition refers to only one side of the join. +* Add :func:`ntile` function. +* Add :func:`map` function to create a map from arrays of keys and values. +* Add :func:`min_by` aggregation function. +* Add support for concatenating arrays with the ``||`` operator. +* Add support for ``=`` and ``!=`` to ``JSON`` type. +* Improve error message when ``DISTINCT`` is applied to types that are not comparable. +* Perform type validation for ``IN`` expression where the right-hand side is a subquery expression. +* Improve error message when ``ORDER BY ... LIMIT`` query exceeds its maximum memory allocation. +* Improve error message when types that are not orderable are used in an ``ORDER BY`` clause. +* Improve error message when the types of the columns for subqueries of a ``UNION`` query don't match. +* Fix a regression where queries could be expired too soon on a highly loaded cluster. +* Fix scheduling issue for queries involving tables from information_schema, which could result in + inconsistent metadata. +* Fix an issue with :func:`min_by` and :func:`max_by` that could result in an error when used with + a variable-length type (e.g., ``VARCHAR``) in a ``GROUP BY`` query. +* Fix rendering of array attributes in JMX connector. +* Input rows/bytes are now tracked properly for ``JOIN`` queries. +* Fix case-sensitivity issue when resolving names of constant table expressions. +* Fix unnesting arrays and maps that contain the ``ROW`` type. diff --git a/presto-docs/src/main/sphinx/release/release-0.87.rst b/presto-docs/src/main/sphinx/release/release-0.87.rst new file mode 100644 index 00000000..d1497606 --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.87.rst @@ -0,0 +1,9 @@ +============ +Release 0.87 +============ + +General Changes +--------------- + +* Fixed a bug where :ref:`row_type` types could have the wrong field names. +* Changed the minimum JDK version to 1.8. diff --git a/presto-docs/src/main/sphinx/release/release-0.88.rst b/presto-docs/src/main/sphinx/release/release-0.88.rst new file mode 100644 index 00000000..12e0500a --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.88.rst @@ -0,0 +1,18 @@ +============ +Release 0.88 +============ + +General Changes +--------------- + +* Added :func:`arbitrary` aggregation function. +* Allow using all :doc:`/functions/aggregate` as :doc:`/functions/window`. +* Support specifying window frames and correctly implement frames for all :doc:`/functions/window`. +* Allow :func:`approx_distinct` aggregation function to accept a standard error parameter. +* Implement :func:`least` and :func:`greatest` with variable number of arguments. +* :ref:`array_type` is now comparable and can be used as ``GROUP BY`` keys or in ``ORDER BY`` expressions. +* Implement ``=`` and ``<>`` operators for :ref:`row_type`. +* Fix excessive garbage creation in the ORC reader. +* Fix an issue that could cause queries using :func:`row_number()` and ``LIMIT`` to never terminate. +* Fix an issue that could cause queries with :func:`row_number()` and specific filters to produce incorrect results. +* Fixed an issue that caused the Cassandra plugin to fail to load with a SecurityException. diff --git a/presto-docs/src/main/sphinx/release/release-0.89.rst b/presto-docs/src/main/sphinx/release/release-0.89.rst new file mode 100644 index 00000000..3e4ce65b --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.89.rst @@ -0,0 +1,21 @@ +============ +Release 0.89 +============ + +DATE Type +--------- +The memory representation of dates is now the number of days since January 1, 1970 +using a 32-bit signed integer. + +.. note:: + This is a backwards incompatible change with the previous date + representation, so if you have written a connector, you will need to update + your code before deploying this release. + +General Changes +--------------- + +* ``USE CATALOG`` and ``USE SCHEMA`` have been replaced with :doc:`/sql/use`. +* Fix issue where ``SELECT NULL`` incorrectly returns 0 rows. +* Fix rare condition where ``JOIN`` queries could produce incorrect results. +* Fix issue where ``UNION`` queries involving complex types would fail during planning. diff --git a/presto-docs/src/main/sphinx/release/release-0.90.rst b/presto-docs/src/main/sphinx/release/release-0.90.rst new file mode 100644 index 00000000..f220a0bb --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.90.rst @@ -0,0 +1,61 @@ +============ +Release 0.90 +============ + +.. warning:: This release has a memory leak and should not be used. + +General Changes +--------------- + +* Initial support for partition and placement awareness in the query planner. This can + result in better plans for queries involving ``JOIN`` and ``GROUP BY`` over the same + key columns. +* Improve planning of UNION queries. +* Add presto version to query creation and completion events. +* Add property ``task.writer-count`` to configure the number of writers per task. +* Fix a bug when optimizing constant expressions involving binary types. +* Fix bug where a table writer commits partial results while cleaning up a failed query. +* Fix a bug when unnesting an array of doubles containing NaN or Infinity. +* Fix failure when accessing elements in an empty array. +* Fix *"Remote page is too large"* errors. +* Improve error message when attempting to cast a value to ``UNKNOWN``. +* Update the :func:`approx_distinct` documentation with correct standard error bounds. +* Disable falling back to the interpreter when expressions fail to be compiled + to bytecode. To enable this option, add ``compiler.interpreter-enabled=true`` + to the coordinator and worker config properties. Enabling this option will + allow certain queries to run slowly rather than failing. +* Improve :doc:`/installation/jdbc` conformance. In particular, all unimplemented + methods now throw ``SQLException`` rather than ``UnsupportedOperationException``. + +Functions and Language Features +------------------------------- + +* Add :func:`bool_and` and :func:`bool_or` aggregation functions. +* Add standard SQL function :func:`every` as an alias for :func:`bool_and`. +* Add :func:`year_of_week` function. +* Add :func:`regexp_extract_all` function. +* Add :func:`map_agg` aggregation function. +* Add support for casting ``JSON`` to ``ARRAY`` or ``MAP`` types. +* Add support for unparenthesized expressions in ``VALUES`` clause. +* Added :doc:`/sql/set-session`, :doc:`/sql/reset-session` and :doc:`/sql/show-session`. +* Improve formatting of ``EXPLAIN (TYPE DISTRIBUTED)`` output and include additional + information such as output layout, task placement policy and partitioning functions. + +Hive Changes +------------ +* Disable optimized metastore partition fetching for non-string partition keys. + This fixes an issue were Presto might silently ignore data with non-canonical + partition values. To enable this option, add ``hive.assume-canonical-partition-keys=true`` + to the coordinator and worker config properties. +* Don't retry operations against S3 that fail due to lack of permissions. + +SPI Changes +----------- +* Add ``getColumnTypes`` to ``RecordSink``. +* Use ``Slice`` for table writer fragments. +* Add ``ConnectorPageSink`` which is a more efficient interface for column-oriented sources. + +.. note:: + This is a backwards incompatible change with the previous connector SPI. + If you have written a connector, you will need to update your code + before deploying this release. diff --git a/presto-docs/src/main/sphinx/release/release-0.91.rst b/presto-docs/src/main/sphinx/release/release-0.91.rst new file mode 100644 index 00000000..ea4956f5 --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.91.rst @@ -0,0 +1,10 @@ +============ +Release 0.91 +============ + +.. warning:: This release has a memory leak and should not be used. + +General Changes +--------------- + +* Clear ``LazyBlockLoader`` reference after load to free memory earlier. diff --git a/presto-docs/src/main/sphinx/release/release-0.92.rst b/presto-docs/src/main/sphinx/release/release-0.92.rst new file mode 100644 index 00000000..82631fd3 --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.92.rst @@ -0,0 +1,8 @@ +============ +Release 0.92 +============ + +General Changes +--------------- + +* Fix buffer leak when a query fails. diff --git a/presto-docs/src/main/sphinx/release/release-0.93.rst b/presto-docs/src/main/sphinx/release/release-0.93.rst new file mode 100644 index 00000000..6445ceb3 --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.93.rst @@ -0,0 +1,40 @@ +============ +Release 0.93 +============ + +ORC Memory Usage +---------------- + +This release changes the Presto ORC reader to favor small buffers when reading +varchar and varbinary data. Some ORC files contain columns of data that are +hundreds of megabytes when decompressed. In the previous Presto ORC reader, we +would allocate a single large shared buffer for all values in the column. This +would cause heap fragmentation in CMS and G1, and it would cause OOMs since +each value of the column retains a reference to the shared buffer. In this +release the ORC reader uses a separate buffer for each value in the column. +This reduces heap fragmentation and excessive memory retention at the expense +of object creation. + +Verifier +-------- + +* Add support for setting username and password per query + +If you're upgrading from 0.92, you need to alter your verifier_queries table + +.. code-block:: sql + + ALTER TABLE verifier_queries add test_username VARCHAR(256) NOT NULL default 'verifier-test'; + ALTER TABLE verifier_queries add test_password VARCHAR(256); + ALTER TABLE verifier_queries add control_username VARCHAR(256) NOT NULL default 'verifier-test'; + ALTER TABLE verifier_queries add control_password VARCHAR(256); + +General Changes +--------------- + +* Add optimizer for ``LIMIT 0`` +* Fix incorrect check to disable string statistics in ORC +* Ignore hidden columns in ``INSERT`` and ``CREATE TABLE AS`` queries +* Add SOCKS support to CLI +* Improve CLI output for update queries +* Disable pushdown for non-deterministic predicates diff --git a/presto-docs/src/main/sphinx/release/release-0.94.rst b/presto-docs/src/main/sphinx/release/release-0.94.rst new file mode 100644 index 00000000..e3d8fda6 --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.94.rst @@ -0,0 +1,25 @@ +============ +Release 0.94 +============ + +ORC Memory Usage +---------------- + +This release contains additional changes to the Presto ORC reader to favor +small buffers when reading varchar and varbinary data. Some ORC files contain +columns of data that are hundreds of megabytes compressed. When reading these +columns, Presto would allocate a single buffer for the compressed column data, +and this would cause heap fragmentation in CMS and G1 and eventually OOMs. +In this release, the ``hive.orc.max-buffer-size`` sets the maximum size for a +single ORC buffer, and for larger columns we instead stream the data. This +reduces heap fragmentation and excessive buffers in ORC at the expense of +HDFS IOPS. The default value is ``8MB``. + +General Changes +--------------- + +* Update Hive CDH 4 connector to CDH 4.7.1 +* Fix ``ORDER BY`` with ``LIMIT 0`` +* Fix compilation of ``try_cast`` +* Group threads into Java thread groups to ease debugging +* Add ``task.min-drivers`` config to help limit number of concurrent readers diff --git a/presto-docs/src/main/sphinx/release/release-0.95.rst b/presto-docs/src/main/sphinx/release/release-0.95.rst new file mode 100644 index 00000000..211e0364 --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.95.rst @@ -0,0 +1,8 @@ +============ +Release 0.95 +============ + +General Changes +--------------- + +* Fix task and stage leak, caused when a stage finishes before its substages. diff --git a/presto-docs/src/main/sphinx/release/release-0.96.rst b/presto-docs/src/main/sphinx/release/release-0.96.rst new file mode 100644 index 00000000..f376bea6 --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.96.rst @@ -0,0 +1,22 @@ +============ +Release 0.96 +============ + +General Changes +--------------- + +* Fix :func:`try_cast` for ``TIMESTAMP`` and other types that + need access to session information. +* Fix planner bug that could result in incorrect results for + tables containing columns with the same prefix, underscores and numbers. +* ``MAP`` type is now comparable. +* Fix output buffer leak in ``StatementResource.Query``. +* Fix leak in ``SqlTasks`` caused by invalid heartbeats . +* Fix double logging of queries submitted while the queue is full. +* Fixed "running queries" JMX stat. +* Add ``distributed_join`` session property to enable/disable distributed joins. + +Hive Changes +------------ + +* Add support for tables partitioned by ``DATE``. diff --git a/presto-docs/src/main/sphinx/release/release-0.97.rst b/presto-docs/src/main/sphinx/release/release-0.97.rst new file mode 100644 index 00000000..3ae98719 --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.97.rst @@ -0,0 +1,20 @@ +============ +Release 0.97 +============ + +General Changes +--------------- + +* The queueing policy in Presto may now be injected. See :doc:`/admin/queue` for details. +* Speed up detection of ASCII strings in implementation of ``LIKE`` operator. +* Fix NullPointerException when metadata-based query optimization is enabled. +* Fix possible infinite loop when decompressing ORC data. +* Fix an issue where ``NOT`` clause was being ignored in ``NOT BETWEEN`` predicates. +* Fix a planning issue in queries that use ``SELECT *``, window functions and implicit coercions. +* Fix scheduler deadlock for queries with a ``UNION`` between ``VALUES`` and ``SELECT``. + +Hive Changes +------------ + +* Fix decoding of ``STRUCT`` type from Parquet files. +* Speed up decoding of ORC files with very small stripes. diff --git a/presto-docs/src/main/sphinx/release/release-0.98.rst b/presto-docs/src/main/sphinx/release/release-0.98.rst new file mode 100644 index 00000000..2b3dc3c2 --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.98.rst @@ -0,0 +1,35 @@ +============ +Release 0.98 +============ + +Array, Map, and Row Types +------------------------- + +The memory representation of these types is now ``VariableWidthBlockEncoding`` +instead of ``JSON``. + +.. note:: + This is a backwards incompatible change with the previous representation, + so if you have written a connector or function, you will need to update + your code before deploying this release. + +Hive Changes +------------ + +* Fix handling of ORC files with corrupt checkpoints. + +SPI Changes +----------- + +* Rename ``Index`` to ``ConnectorIndex``. + +.. note:: + This is a backwards incompatible change, so if you have written a connector + that uses ``Index``, you will need to update your code before deploying this release. + +General Changes +--------------- + +* Fix bug in ``UNNEST`` when output is unreferenced or partially referenced. +* Make :func:`max` and :func:`min` functions work on all orderable types. +* Optimize memory allocation in :func:`max_by` and other places that ``Block`` is used. diff --git a/presto-docs/src/main/sphinx/release/release-0.99.rst b/presto-docs/src/main/sphinx/release/release-0.99.rst new file mode 100644 index 00000000..6bb1179a --- /dev/null +++ b/presto-docs/src/main/sphinx/release/release-0.99.rst @@ -0,0 +1,10 @@ +============ +Release 0.99 +============ + +General Changes +--------------- +* Reduce lock contention in ``TaskExecutor``. +* Fix reading maps with null keys from ORC. +* Fix precomputed hash optimization for nulls values. +* Make :func:`contains()` work for all comparable types. diff --git a/presto-docs/src/main/sphinx/rest.rst b/presto-docs/src/main/sphinx/rest.rst new file mode 100644 index 00000000..6e785577 --- /dev/null +++ b/presto-docs/src/main/sphinx/rest.rst @@ -0,0 +1,69 @@ +*************** +Presto REST API +*************** + +This chapter defines the Presto REST API. Presto uses REST for all +communication within a Presto installation. JSON-based REST services +facilitate communication between the client and the Presto coordinator +as well as for communicate between a Presto coordinator and multiple +Presto workers. In this chapter you will find detailed descriptions +of the APIs offered by Presto as well as example requests and +responses. + +.. toctree:: + :maxdepth: 1 + + rest/execute + rest/node + rest/query + rest/stage + rest/statement + rest/task + +REST API Overview +----------------- + +In Presto, everything is exposed as a REST API in Presto and HTTP is +the method by which all component communicate with each other. + +The Presto REST API contains several, high-level resources that +correspond to the components of a Presto installation. + + +Execute Resource + + The execute resource is what the client sends queries to. It is + available at the path ``/v1/execute``, and accepts a query as a POST + and returns JSON. + +Query Resource + + The query resource takes a SQL query. It is available at the path + ``/v1/query`` and accepts several HTTP methods. + +Node Resource + + The node resource returns information about worker nodes in a + Presto installation. It is available at the path ``/v1/node``. + +Stage Resource + + When a Presto coordinator receives a query it creates distribute + system of stages which collaborate with one another to execute a + query. The Stage resource is used by the coordinator to create a + network of corresponding stages. It is also used by stages to + coordinate with one another. + +Statement Resource + + This is the standard resource used by the client to execute a + statement. When executing a statement, the Presto client will + call this resource repeatedly to get the status of an ongoing + statement execution as well as the results of a completed + statement. + +Task Resource + + A stage contains a number of components, one of which is a + task. This resource is used by internal components to coordinate + the execution of stages. \ No newline at end of file diff --git a/presto-docs/src/main/sphinx/rest/execute.rst b/presto-docs/src/main/sphinx/rest/execute.rst new file mode 100644 index 00000000..098d4de1 --- /dev/null +++ b/presto-docs/src/main/sphinx/rest/execute.rst @@ -0,0 +1,63 @@ +================ +Execute Resource +================ + +.. function:: POST /v1/execute + + :Body: SQL Query to execute + :Header "X-Presto-User": User to execute statement on behalf of (optional) + :Header "X-Presto-Source": Source of query + :Header "X-Presto-Catalog": Catalog to execute query against + :Header "X-Presto-Schema": Schema to execute query against + + Call this to execute a SQL statement as an alternative to running + ``/v1/statement``. Where ``/v1/statement`` will return a + ``nextUri`` and details about a running query, the ``/v1/execute`` + call will simply execute the SQL statement posted to it and return + the result set. This service will not return updates about query + status or details about stages and tasks. It simply executes a + query and returns the result. + + The sample request and response shown below demonstrate how the + execute call works. Once you post a SQL statement to /v1/execute it + returns a set of columns describing an array of data items. This + trivial executes a "show functions" statement. + + **Example request**: + + .. sourcecode:: http + + POST /v1/execute HTTP/1.1 + Host: localhost:8001 + X-Presto-Schema: jmx + X-Presto-User: tobrie1 + X-Presto-Catalog: jmx + Content-Type: text/html + Content-Length: 14 + + show functions + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + X-Content-Type-Options: nosniff + Transfer-Encoding: chunked + + {"columns": + [ + {"name":"Function","type":"varchar"}, + {"name":"Return Type","type":"varchar"}, + {"name":"Argument Types","type":"varchar"}, + {"name":"Function Type","type":"varchar"}, + {"name":"Description","type":"varchar"} + ], + "data": + [ + ["abs","bigint","bigint","scalar","absolute value"], + ["abs","double","double","scalar","absolute value"], + ... + ] + }; diff --git a/presto-docs/src/main/sphinx/rest/node.rst b/presto-docs/src/main/sphinx/rest/node.rst new file mode 100644 index 00000000..29e658df --- /dev/null +++ b/presto-docs/src/main/sphinx/rest/node.rst @@ -0,0 +1,110 @@ +============= +Node Resource +============= + +.. function:: GET /v1/node + + Returns a list of nodes known to a Presto Server. This call + doesn't require a query parameter of headers, it simply returns an + array with each known node in a Presto installation. + + In the response, the values ``recentRequests``, ``recentFailures``, + and ``recentSuccesses`` are decaying counters set to decay + exponentially over time with a decay parameter of one minute. This + decay rate means that if a Presto node has 1000 successes in a few + seconds, this statistic value will drop to 367 in one minute. + + ``age`` shows you how long a particular node has been running, and uri + points you to the HTTP server for a particular node. The last + request and last response times show you how recently a node has + been used. + + The following example response displays a single node which has not + experienced any failure conditions. Each node also reports + statistics about traffic uptime, and failures. + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + [ + { + "uri":"http://10.209.57.156:8080", + "recentRequests":25.181940555111073, + "recentFailures":0.0, + "recentSuccesses":25.195472984170983, + "lastRequestTime":"2013-12-22T13:32:44.673-05:00", + "lastResponseTime":"2013-12-22T13:32:44.677-05:00", + "age":"14155.28ms", + "recentFailureRatio":0.0, + "recentFailuresByType":{} + } + ] + + + If a node is experiencing errors, you'll see a response that looks + like the following. Here we have a node which has experienced a + spate of errors. The recentFailuresByType field lists the Java + exception which have occurred recently on a particular node. + + **Example response with Errors**: + + .. sourcecode:: http + + + HTTP/1.1 200 OK + Vry: Accept + Content-Type: text/javascript + + [ + { + "uri":"http://10.209.57.156:8080", + "recentRequests":117.0358348572745, + "recentFailures":8.452831267323281, + "recentSuccesses":108.58300358995123, + "lastRequestTime":"2013-12-23T02:00:40.382-05:00", + "lastResponseTime":"2013-12-23T02:00:40.383-05:00", + "age":"44882391.57ms", + "recentFailureRatio":0.07222430016952953, + "recentFailuresByType": + { + "java.io.IOException":0.9048374180359595, + "java.net.SocketTimeoutException":6.021822867514955E-269, + "java.net.ConnectException":7.54799384928732 + } + } + ] + +.. function:: GET /v1/node/failed + + Calling this service returns a JSON document listing all the nodes + that have failed the last heartbeat check. The information + returned by this call is the same as the information returned by + the previous service. + + **Example response**: + + .. sourcecode:: http + + [ + { + "uri":"http://10.209.57.156:8080", + "recentRequests":5.826871111529161, + "recentFailures":0.4208416882082422, + "recentSuccesses":5.406029423320919, + "lastRequestTime":"2013-12-23T02:00:40.382-05:00", + "lastResponseTime":"2013-12-23T02:00:40.383-05:00", + "age":"45063192.35ms", + "recentFailureRatio":0.07222430016952952, + "recentFailuresByType": + { + "java.io.IOException":0.0450492023935578, + "java.net.SocketTimeoutException":2.998089068041336E-270, + "java.net.ConnectException":0.3757924858146843 + } + } + ] diff --git a/presto-docs/src/main/sphinx/rest/query.rst b/presto-docs/src/main/sphinx/rest/query.rst new file mode 100644 index 00000000..5b39862a --- /dev/null +++ b/presto-docs/src/main/sphinx/rest/query.rst @@ -0,0 +1,75 @@ +============== +Query Resource +============== + +The Query REST service is the most complex of the rest services. It +contains detailed information about nodes, and other +details that capture the state and history of a query being executed +on a Presto installation. + +.. function:: GET /v1/query + + This service returns information and statistics about queries that + are currently being executed on a Presto coordinator. + + When you point a web broswer at a Presto coordinate you'll see a + rendered version of the output from this service which will display + recent queries that have executed on a Presto installation. + +.. function:: GET /v1/query/{queryId} + + If you are looking to gather very detailed statistics about a + query, this is the service you would call. If you load the web + interface of a Presto coordinator you will see a list of current + queries. Clicking on a query will reveal a link to this service. + + **Example response**: + + .. sourcecode:: http + + { + "queryId" : "20131229_211533_00017_dk5x2", + "session" : { + "user" : "tobrien", + "source" : "presto-cli", + "catalog" : "jmx", + "schema" : "jmx", + "remoteUserAddress" : "173.15.79.89", + "userAgent" : "StatementClient/0.55-SNAPSHOT", + "startTime" : 1388351852026 + }, + "state" : "FINISHED", + "self" : "http://10.193.207.128:8080/v1/query/20131229_211533_00017_dk5x2", + "fieldNames" : [ "name" ], + "query" : "select name from \"java.lang:type=runtime\"", + "queryStats" : { + "createTime" : "2013-12-29T16:17:32.027-05:00", + "executionStartTime" : "2013-12-29T16:17:32.086-05:00", + "lastHeartbeat" : "2013-12-29T16:17:44.561-05:00", + "endTime" : "2013-12-29T16:17:32.152-05:00", + "elapsedTime" : "125.00ms", + "queuedTime" : "1.31ms", + "analysisTime" : "4.84ms", + "distributedPlanningTime" : "353.00us", + "totalTasks" : 2, + "runningTasks" : 0, + "completedTasks" : 2, + "totalDrivers" : 2, + "queuedDrivers" : 0, + "runningDrivers" : 0, + "completedDrivers" : 2, + "totalMemoryReservation" : "0B", + "totalScheduledTime" : "5.84ms", + "totalCpuTime" : "710.49us", + "totalUserTime" : "0.00ns", + "totalBlockedTime" : "27.38ms", + "rawInputDataSize" : "27B", + "rawInputPositions" : 1, + "processedInputDataSize" : "32B", + "processedInputPositions" : 1, + "outputDataSize" : "32B", + "outputPositions" : 1 + }, + "outputStage" : ... + } + diff --git a/presto-docs/src/main/sphinx/rest/stage.rst b/presto-docs/src/main/sphinx/rest/stage.rst new file mode 100644 index 00000000..7c1a81aa --- /dev/null +++ b/presto-docs/src/main/sphinx/rest/stage.rst @@ -0,0 +1,11 @@ +============== +Stage Resource +============== + +.. function:: GET /v1/stage + + Returns detail about a stage in a Presto query. + +.. function:: DELETE /v1/stage/{stageId} + + Deletes a stage in a Presto query. diff --git a/presto-docs/src/main/sphinx/rest/statement.rst b/presto-docs/src/main/sphinx/rest/statement.rst new file mode 100644 index 00000000..e7412f3c --- /dev/null +++ b/presto-docs/src/main/sphinx/rest/statement.rst @@ -0,0 +1,229 @@ +================== +Statement Resource +================== + +.. function:: POST /v1/statement + + :query query: SQL Query to execute + :reqheader X-Presto-User: User to execute statement on behalf of (optional) + :reqheader X-Presto-Source: Source of query + :reqheader X-Presto-Catalog: Catalog to execute query against + :reqheader X-Presto-Schema: Schema to execute query against + + Submits a statement to Presto for execution. The Presto client + executes queries on behalf of a user against a catalog and a + schema. When you run a query with the Presto CLI, it is calling + out to the statement resource on the Presto coordinator. + + The request to the statement resource is the SQL query to execute as + a post along with the standard X-Presto-Catalog, X-Presto-Source, + X-Presto-Schema, and X-Presto-User headers. + + The response from the statement resource contains a query + identifier which can be used to gather detailed information about a + query. This initial response also includes information about the + stages that have been created to execute this query on Presto + workers. Every query has a root stage and the root stage is given a + stage identifier of "0" as shown in the following example response. + + This root stage aggregates the responses from other stages running + on Presto workers and delivers them to the client via the Presto + coordinator. When a client receives a response to this POST it will + contain a "nextUri" property which directs the client to query this + address for additional results from the query. + + **Example request**: + + .. sourcecode:: http + + POST /v1/statement HTTP/1.1 + Host: localhost:8001 + X-Presto-Catalog: jmx + X-Presto-Source: presto-cli + X-Presto-Schema: jmx + User-Agent: StatementClient/0.55-SNAPSHOT + X-Presto-User: tobrie1 + Content-Length: 41 + + select name from "java.lang:type=runtime" + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + X-Content-Type-Options: nosniff + Transfer-Encoding: chunked + + { + "id":"20140108_110629_00011_dk5x2", + "infoUri":"http://localhost:8001/v1/query/20140108_110629_00011_dk5x2", + "partialCancelUri":"http://10.193.207.128:8080/v1/stage/20140108_110629_00011_dk5x2.1", + "nextUri":"http://localhost:8001/v1/statement/20140108_110629_00011_dk5x2/1", + "columns": + [ + { + "name":"name", + "type":"varchar" + } + ], + "stats": + { + "state":"RUNNING", + "scheduled":false, + "nodes":1, + "totalSplits":0, + "queuedSplits":0, + "runningSplits":0, + "completedSplits":0, + "userTimeMillis":0, + "cpuTimeMillis":0, + "wallTimeMillis":0, + "processedRows":0, + "processedBytes":0, + "rootStage": + { + "stageId":"0", + "state":"SCHEDULED", + "done":false, + "nodes":1, + "totalSplits":0, + "queuedSplits":0, + "runningSplits":0, + "completedSplits":0, + "userTimeMillis":0, + "cpuTimeMillis":0, + "wallTimeMillis":0, + "processedRows":0, + "processedBytes":0, + "subStages": + [ + { + "stageId":"1", + "state":"SCHEDULED", + "done":false, + "nodes":1, + "totalSplits":0, + "queuedSplits":0, + "runningSplits":0, + "completedSplits":0, + "userTimeMillis":0, + "cpuTimeMillis":0, + "wallTimeMillis":0, + "processedRows":0, + "processedBytes":0, + "subStages":[] + } + ] + } + } + } + + +.. function:: GET /v1/statement/{queryId}/{token} + + :query queryId: The query identifier returned from the initial POST to /v1/statement + :query token: The token returned from the initial POST to /v1/statement or from a previous call to this same call + + When a Presto client submits a statement for execution, Presto + creates a query and then it returns a nextUri to the client. This + call corresponds to that nextUri call and can contain either a + status update for a query in progress or it can deliver the final + results to the client. + + **Example request**: + + .. sourcecode:: http + + GET /v1/statement/20140108_110629_00011_dk5x2/1 HTTP/1.1 + Host: localhost:8001 + User-Agent: StatementClient/0.55-SNAPSHOT + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + X-Content-Type-Options: nosniff + Vary: Accept-Encoding, User-Agent + Transfer-Encoding: chunked + + 383 + { + "id":"20140108_110629_00011_dk5x2", + "infoUri":"http://localhost:8001/v1/query/20140108_110629_00011_dk5x2", + "columns": + [ + { + "name":"name", + "type":"varchar" + } + ], + "data": + [ + ["4165@domU-12-31-39-0F-CC-72"] + ], + "stats": + { + "state":"FINISHED", + "scheduled":true, + "nodes":1, + "totalSplits":2, + "queuedSplits":0, + "runningSplits":0, + "completedSplits":2, + "userTimeMillis":0, + "cpuTimeMillis":1, + "wallTimeMillis":4, + "processedRows":1, + "processedBytes":27, + "rootStage": + { + "stageId":"0", + "state":"FINISHED", + "done":true, + "nodes":1, + "totalSplits":1, + "queuedSplits":0, + "runningSplits":0, + "completedSplits":1, + "userTimeMillis":0, + "cpuTimeMillis":0, + "wallTimeMillis":0, + "processedRows":1, + "processedBytes":32, + "subStages": + [ + { + "stageId":"1", + "state":"FINISHED", + "done":true, + "nodes":1, + "totalSplits":1, + "queuedSplits":0, + "runningSplits":0, + "completedSplits":1, + "userTimeMillis":0, + "cpuTimeMillis":0, + "wallTimeMillis":4, + "processedRows":1, + "processedBytes":27, + "subStages":[] + } + ] + } + } + } + +.. function:: DELETE /v1/statement/{queryId}/{token} + + :query queryId: The query identifier returned from the initial POST to /v1/statement + :reqheader X-Presto-User: User to execute statement on behalf of (optional) + :reqheader X-Presto-Source: Source of query + :reqheader X-Presto-Catalog: Catalog to execute query against + :reqheader X-Presto-Schema: Schema to execute query against + + + diff --git a/presto-docs/src/main/sphinx/rest/task.rst b/presto-docs/src/main/sphinx/rest/task.rst new file mode 100644 index 00000000..801e778c --- /dev/null +++ b/presto-docs/src/main/sphinx/rest/task.rst @@ -0,0 +1,317 @@ +============= +Task Resource +============= + +The Task resource provides a set of REST endpoints that give Presto +servers the ability to communicate about tasks and task output. This +isn't a service that will be used by end users, but it supports the +execution of queries on a Presto installation. + +.. function:: GET /v1/task + + Returns information about all tasks known to a Presto server. + + Note that the output of a call to ``/v1/task`` can be quite + large. If you execute this against a busy Presto server the + response received will include a listing of every task known to + that server along with detailed statistics about operators and + drivers. + + The following example response shows a trivial task response that + has been truncated to fit this manual. A real response from a busy + Presto server would generate pages and pages of output. Here there + is a ``taskId`` for a task which is in the ``CANCELED`` state. + + **Example response**: + + .. sourcecode:: http + + [ { + "taskId" : "20131222_183944_00011_dk5x2.1.0", + "version" : 9223372036854775807, + "state" : "CANCELED", + "self" : "unknown", + "lastHeartbeat" : "2013-12-22T13:54:46.566-05:00", + "outputBuffers" : { + "state" : "FINISHED", + "masterSequenceId" : 0, + "pagesAdded" : 0, + "buffers" : [ ] + }, + "noMoreSplits" : [ ], + "stats" : { + "createTime" : "2013-12-22T13:54:46.566-05:00", + "elapsedTime" : "0.00ns", + "queuedTime" : "92.00us", + "totalDrivers" : 0, + "queuedDrivers" : 0, + "runningDrivers" : 0, + "completedDrivers" : 0, + "memoryReservation" : "0B", + "totalScheduledTime" : "0.00ns", + "totalCpuTime" : "0.00ns", + "totalUserTime" : "0.00ns", + "totalBlockedTime" : "0.00ns", + "rawInputDataSize" : "0B", + "rawInputPositions" : 0, + "processedInputDataSize" : "0B", + "processedInputPositions" : 0, + "outputDataSize" : "0B", + "outputPositions" : 0, + "pipelines" : [ ] + }, + "failures" : [ ], + "outputs" : { } + }] + +.. function:: POST /v1/task/{taskId} + +.. function:: DELETE /v1/task/{taskId} + + Deletes a given task from a Presto server. + +.. function:: GET /v1/task/{taskId} + + Retrieves information about a specific task by ``taskId``. + + The following example lists the output of a task. It contains the + following high-level sections: + + * ``outputBuffers`` + * ``noMoreSplits`` + * ``stats`` + * ``failures`` + * ``outputs`` + + This is the same output that is also present in the response from + the Query resource which lists all of the stages and tasks involved + in a particular query. This is call is used by Presto to coordinate + a queries. + + **Example response**: + + .. sourcecode:: http + + { + "taskId" : "20140115_170528_00004_dk5x2.0.0", + "version" : 42, + "state" : "FINISHED", + "self" : "http://10.193.207.128:8080/v1/task/20140115_170528_00004_dk5x2.0.0", + "lastHeartbeat" : "2014-01-15T12:12:12.518-05:00", + "outputBuffers" : { + "state" : "FINISHED", + "masterSequenceId" : 0, + "pagesAdded" : 1, + "buffers" : [ { + "bufferId" : "out", + "finished" : true, + "bufferedPages" : 0, + "pagesSent" : 1 + } ] + }, + "noMoreSplits" : [ "8" ], + "stats" : { + "createTime" : "2014-01-15T12:12:08.520-05:00", + "startTime" : "2014-01-15T12:12:08.526-05:00", + "endTime" : "2014-01-15T12:12:12.518-05:00", + "elapsedTime" : "4.00s", + "queuedTime" : "6.39ms", + "totalDrivers" : 1, + "queuedDrivers" : 0, + "runningDrivers" : 0, + "completedDrivers" : 1, + "memoryReservation" : "174.76kB", + "totalScheduledTime" : "4.19ms", + "totalCpuTime" : "4.09ms", + "totalUserTime" : "0.00ns", + "totalBlockedTime" : "29.50ms", + "rawInputDataSize" : "10.90kB", + "rawInputPositions" : 154, + "processedInputDataSize" : "10.90kB", + "processedInputPositions" : 154, + "outputDataSize" : "10.90kB", + "outputPositions" : 154, + "pipelines" : [ { + "inputPipeline" : true, + "outputPipeline" : true, + "totalDrivers" : 1, + "queuedDrivers" : 0, + "runningDrivers" : 0, + "completedDrivers" : 1, + "memoryReservation" : "0B", + "queuedTime" : { + "maxError" : 0.0, + "count" : 1.0, + "total" : 5857000.0, + "p01" : 5857000, + "p05" : 5857000, + "p10" : 5857000, + "p25" : 5857000, + "p50" : 5857000, + "p75" : 5857000, + "p90" : 5857000, + "p95" : 5857000, + "p99" : 5857000, + "min" : 5857000, + "max" : 5857000 + }, + "elapsedTime" : { + "maxError" : 0.0, + "count" : 1.0, + "total" : 4.1812E7, + "p01" : 41812000, + "p05" : 41812000, + "p10" : 41812000, + "p25" : 41812000, + "p50" : 41812000, + "p75" : 41812000, + "p90" : 41812000, + "p95" : 41812000, + "p99" : 41812000, + "min" : 41812000, + "max" : 41812000 + }, + "totalScheduledTime" : "4.19ms", + "totalCpuTime" : "4.09ms", + "totalUserTime" : "0.00ns", + "totalBlockedTime" : "29.50ms", + "rawInputDataSize" : "10.90kB", + "rawInputPositions" : 154, + "processedInputDataSize" : "10.90kB", + "processedInputPositions" : 154, + "outputDataSize" : "10.90kB", + "outputPositions" : 154, + "operatorSummaries" : [ { + "operatorId" : 0, + "operatorType" : "ExchangeOperator", + "addInputCalls" : 0, + "addInputWall" : "0.00ns", + "addInputCpu" : "0.00ns", + "addInputUser" : "0.00ns", + "inputDataSize" : "10.90kB", + "inputPositions" : 154, + "getOutputCalls" : 1, + "getOutputWall" : "146.00us", + "getOutputCpu" : "137.90us", + "getOutputUser" : "0.00ns", + "outputDataSize" : "10.90kB", + "outputPositions" : 154, + "blockedWall" : "29.50ms", + "finishCalls" : 0, + "finishWall" : "0.00ns", + "finishCpu" : "0.00ns", + "finishUser" : "0.00ns", + "memoryReservation" : "0B", + "info" : { + "bufferedBytes" : 0, + "averageBytesPerRequest" : 11158, + "bufferedPages" : 0, + "pageBufferClientStatuses" : [ { + "uri" : "http://10.193.207.128:8080/v1/task/20140115_170528_00004_dk5x2.1.0/results/ab68e201-3878-4b21-b6b9-f6658ddc408b", + "state" : "closed", + "lastUpdate" : "2014-01-15T12:12:08.562-05:00", + "pagesReceived" : 1, + "requestsScheduled" : 3, + "requestsCompleted" : 3, + "httpRequestState" : "queued" + } ] + } + }, { + "operatorId" : 1, + "operatorType" : "FilterAndProjectOperator", + "addInputCalls" : 1, + "addInputWall" : "919.00us", + "addInputCpu" : "919.38us", + "addInputUser" : "0.00ns", + "inputDataSize" : "10.90kB", + "inputPositions" : 154, + "getOutputCalls" : 2, + "getOutputWall" : "128.00us", + "getOutputCpu" : "128.64us", + "getOutputUser" : "0.00ns", + "outputDataSize" : "10.45kB", + "outputPositions" : 154, + "blockedWall" : "0.00ns", + "finishCalls" : 5, + "finishWall" : "258.00us", + "finishCpu" : "253.19us", + "finishUser" : "0.00ns", + "memoryReservation" : "0B" + }, { + "operatorId" : 2, + "operatorType" : "OrderByOperator", + "addInputCalls" : 1, + "addInputWall" : "438.00us", + "addInputCpu" : "439.18us", + "addInputUser" : "0.00ns", + "inputDataSize" : "10.45kB", + "inputPositions" : 154, + "getOutputCalls" : 4, + "getOutputWall" : "869.00us", + "getOutputCpu" : "831.85us", + "getOutputUser" : "0.00ns", + "outputDataSize" : "10.45kB", + "outputPositions" : 154, + "blockedWall" : "0.00ns", + "finishCalls" : 4, + "finishWall" : "808.00us", + "finishCpu" : "810.18us", + "finishUser" : "0.00ns", + "memoryReservation" : "174.76kB" + }, { + "operatorId" : 3, + "operatorType" : "FilterAndProjectOperator", + "addInputCalls" : 1, + "addInputWall" : "166.00us", + "addInputCpu" : "166.66us", + "addInputUser" : "0.00ns", + "inputDataSize" : "10.45kB", + "inputPositions" : 154, + "getOutputCalls" : 5, + "getOutputWall" : "305.00us", + "getOutputCpu" : "241.14us", + "getOutputUser" : "0.00ns", + "outputDataSize" : "10.90kB", + "outputPositions" : 154, + "blockedWall" : "0.00ns", + "finishCalls" : 2, + "finishWall" : "70.00us", + "finishCpu" : "71.02us", + "finishUser" : "0.00ns", + "memoryReservation" : "0B" + }, { + "operatorId" : 4, + "operatorType" : "TaskOutputOperator", + "addInputCalls" : 1, + "addInputWall" : "50.00us", + "addInputCpu" : "51.03us", + "addInputUser" : "0.00ns", + "inputDataSize" : "10.90kB", + "inputPositions" : 154, + "getOutputCalls" : 0, + "getOutputWall" : "0.00ns", + "getOutputCpu" : "0.00ns", + "getOutputUser" : "0.00ns", + "outputDataSize" : "10.90kB", + "outputPositions" : 154, + "blockedWall" : "0.00ns", + "finishCalls" : 1, + "finishWall" : "35.00us", + "finishCpu" : "35.39us", + "finishUser" : "0.00ns", + "memoryReservation" : "0B" + } ], + "drivers" : [ ] + } ] + }, + "failures" : [ ], + "outputs" : { } + } + +.. function:: GET /v1/task/{taskId}/results/{outputId}/{token} + + This service is used by Presto to retrieve task output. + +.. function:: DELETE /v1/task/{taskId}/results/{outputId} + + This service is used by Presto to delete task output. diff --git a/presto-docs/src/main/sphinx/sql.rst b/presto-docs/src/main/sphinx/sql.rst new file mode 100644 index 00000000..ba4cdbe2 --- /dev/null +++ b/presto-docs/src/main/sphinx/sql.rst @@ -0,0 +1,30 @@ +******************** +SQL Statement Syntax +******************** + +This chapter describes the SQL syntax used in Presto. + +.. toctree:: + :maxdepth: 1 + + sql/alter-table + sql/create-table + sql/create-table-as + sql/create-view + sql/delete + sql/describe + sql/drop-table + sql/drop-view + sql/explain + sql/insert + sql/reset-session + sql/select + sql/set-session + sql/show-catalogs + sql/show-columns + sql/show-functions + sql/show-partitions + sql/show-schemas + sql/show-session + sql/show-tables + sql/use diff --git a/presto-docs/src/main/sphinx/sql/alter-table.rst b/presto-docs/src/main/sphinx/sql/alter-table.rst new file mode 100644 index 00000000..7aad7808 --- /dev/null +++ b/presto-docs/src/main/sphinx/sql/alter-table.rst @@ -0,0 +1,27 @@ +=========== +ALTER TABLE +=========== + +Synopsis +-------- + +.. code-block:: none + + ALTER TABLE name RENAME TO new_name + ALTER TABLE name RENAME COLUMN column_name TO new_column_name + +Description +----------- + +Change the definition of an existing table. + +Examples +-------- + +Rename table ``users`` to ``people``:: + + ALTER TABLE users RENAME TO people; + +Rename column ``id`` to ``user_id`` in the ``users`` table:: + + ALTER TABLE users RENAME COLUMN id TO user_id; diff --git a/presto-docs/src/main/sphinx/sql/create-table-as.rst b/presto-docs/src/main/sphinx/sql/create-table-as.rst new file mode 100644 index 00000000..1f04aea5 --- /dev/null +++ b/presto-docs/src/main/sphinx/sql/create-table-as.rst @@ -0,0 +1,26 @@ +=============== +CREATE TABLE AS +=============== + +Synopsis +-------- + +.. code-block:: none + + CREATE TABLE table_name AS query + +Description +----------- + +Create a new table containing the result of a :doc:`select` query. +Use :doc:`create-table` to create an empty table. + +Examples +-------- + +Create a new table ``orders_by_date`` that summarizes ``orders``:: + + CREATE TABLE orders_by_date AS + SELECT orderdate, sum(totalprice) AS price + FROM orders + GROUP BY orderdate diff --git a/presto-docs/src/main/sphinx/sql/create-table.rst b/presto-docs/src/main/sphinx/sql/create-table.rst new file mode 100644 index 00000000..dc92c4d5 --- /dev/null +++ b/presto-docs/src/main/sphinx/sql/create-table.rst @@ -0,0 +1,30 @@ +============ +CREATE TABLE +============ + +Synopsis +-------- + +.. code-block:: none + + CREATE TABLE table_name ( + column_name data_type [, ...] + ) + +Description +----------- + +Create a new, empty table with the specified columns. +Use :doc:`create-table-as` to create a table with data. + +Examples +-------- + +Create a new table ``orders``:: + + CREATE TABLE orders ( + orderkey bigint, + orderstatus varchar, + totalprice double, + orderdate date + ) diff --git a/presto-docs/src/main/sphinx/sql/create-view.rst b/presto-docs/src/main/sphinx/sql/create-view.rst new file mode 100644 index 00000000..7034f626 --- /dev/null +++ b/presto-docs/src/main/sphinx/sql/create-view.rst @@ -0,0 +1,43 @@ +=========== +CREATE VIEW +=========== + +Synopsis +-------- + +.. code-block:: none + + CREATE [ OR REPLACE ] VIEW view_name AS query + +Description +----------- + +Create a new view of a :doc:`select` query. The view is a logical table +that can be referenced by future queries. Views do not contain any data. +Instead, the query stored by the view is executed everytime the view is +referenced by another query. + +The optional ``OR REPLACE`` clause causes the view to be replaced if it +already exists rather than raising an error. + +Examples +-------- + +Create a simple view ``test`` over the ``orders`` table:: + + CREATE VIEW test AS + SELECT orderkey, orderstatus, totalprice / 2 AS half + FROM orders + +Create a view ``orders_by_date`` that summarizes ``orders``:: + + CREATE VIEW orders_by_date AS + SELECT orderdate, sum(totalprice) AS price + FROM orders + GROUP BY orderdate + +Create a view that replaces an existing view:: + + CREATE OR REPLACE VIEW test AS + SELECT orderkey, orderstatus, totalprice / 4 AS quarter + FROM orders diff --git a/presto-docs/src/main/sphinx/sql/delete.rst b/presto-docs/src/main/sphinx/sql/delete.rst new file mode 100644 index 00000000..aae1f914 --- /dev/null +++ b/presto-docs/src/main/sphinx/sql/delete.rst @@ -0,0 +1,32 @@ +====== +DELETE +====== + +Synopsis +-------- + +.. code-block:: none + + DELETE FROM table_name [ WHERE condition ] + +Description +----------- + +Delete rows from a table. If the ``WHERE`` clause is specified, only the +matching rows are deleted. Otherwise, all rows from the table are deleted. + +Examples +-------- + +Delete all line items shipped by air:: + + DELETE FROM lineitem WHERE shipmode = 'AIR'; + +Delete all line items for low priority orders:: + + DELETE FROM lineitem + WHERE orderkey IN (SELECT orderkey FROM orders WHERE priority = 'LOW'); + +Delete all orders:: + + DELETE FROM orders; diff --git a/presto-docs/src/main/sphinx/sql/describe.rst b/presto-docs/src/main/sphinx/sql/describe.rst new file mode 100644 index 00000000..68a5a6df --- /dev/null +++ b/presto-docs/src/main/sphinx/sql/describe.rst @@ -0,0 +1,34 @@ +======== +DESCRIBE +======== + +Synopsis +-------- + +.. code-block:: none + + DESCRIBE table_name + +Description +----------- + +``DESCRIBE`` is an alias for :doc:`show-columns`. + +Examples +-------- + +.. code-block:: none + + presto:default> describe airline_origin_destination; + Column | Type | Null | Partition Key + -----------------+---------+------+--------------- + itinid | varchar | true | false + mktid | varchar | true | false + seqnum | varchar | true | false + coupons | varchar | true | false + year | varchar | true | false + quarter | varchar | true | false + origin | varchar | true | false + originaptind | varchar | true | false + origincitynum | varchar | true | false + (9 rows) diff --git a/presto-docs/src/main/sphinx/sql/drop-table.rst b/presto-docs/src/main/sphinx/sql/drop-table.rst new file mode 100644 index 00000000..216e1449 --- /dev/null +++ b/presto-docs/src/main/sphinx/sql/drop-table.rst @@ -0,0 +1,30 @@ +========== +DROP TABLE +========== + +Synopsis +-------- + +.. code-block:: none + + DROP TABLE [ IF EXISTS ] table_name + +Description +----------- + +Drops an existing table. + +The optional ``IF EXISTS`` clause causes the error to be suppressed if +the table does not exist. + +Examples +-------- + +Drop the table ``orders_by_date``:: + + DROP TABLE orders_by_date + +Drop the table ``orders_by_date`` if it exists:: + + DROP TABLE IF EXISTS orders_by_date + diff --git a/presto-docs/src/main/sphinx/sql/drop-view.rst b/presto-docs/src/main/sphinx/sql/drop-view.rst new file mode 100644 index 00000000..e15a62d3 --- /dev/null +++ b/presto-docs/src/main/sphinx/sql/drop-view.rst @@ -0,0 +1,29 @@ +========= +DROP VIEW +========= + +Synopsis +-------- + +.. code-block:: none + + DROP VIEW [ IF EXISTS ] view_name + +Description +----------- + +Drop an existing view. + +The optional ``IF EXISTS`` clause causes the error to be suppressed if +the view does not exist. + +Examples +-------- + +Drop the view ``orders_by_date``:: + + DROP VIEW orders_by_date + +Drop the view ``orders_by_date`` if it exists:: + + DROP VIEW IF EXISTS orders_by_date diff --git a/presto-docs/src/main/sphinx/sql/explain.rst b/presto-docs/src/main/sphinx/sql/explain.rst new file mode 100644 index 00000000..69c0e278 --- /dev/null +++ b/presto-docs/src/main/sphinx/sql/explain.rst @@ -0,0 +1,68 @@ +======= +EXPLAIN +======= + +Synopsis +-------- + +.. code-block:: none + + EXPLAIN [ ( option [, ...] ) ] statement + + where option can be one of: + + FORMAT { TEXT | GRAPHVIZ } + TYPE { LOGICAL | DISTRIBUTED } + +Description +----------- + +Show the logical or distributed execution plan of a statement. + +Examples +-------- + +Logical plan: + +.. code-block:: none + + presto:tiny> EXPLAIN SELECT regionkey, count(*) FROM nation GROUP BY 1; + Query Plan + ---------------------------------------------------------------------------------------------------------- + - Output[regionkey, _col1] => [regionkey:bigint, count:bigint] + _col1 := count + - Exchange[GATHER] => regionkey:bigint, count:bigint + - Aggregate(FINAL)[regionkey] => [regionkey:bigint, count:bigint] + count := "count"("count_8") + - Exchange[REPARTITION] => regionkey:bigint, count_8:bigint + - Aggregate(PARTIAL)[regionkey] => [regionkey:bigint, count_8:bigint] + count_8 := "count"(*) + - TableScan[tpch:tpch:nation:sf0.01, original constraint=true] => [regionkey:bigint] + regionkey := tpch:tpch:regionkey:2 + +Distributed plan: + +.. code-block:: none + + presto:tiny> EXPLAIN (TYPE DISTRIBUTED) SELECT regionkey, count(*) FROM nation GROUP BY 1; + Query Plan + ---------------------------------------------------------------------------------------------- + Fragment 2 [SINGLE] + Output layout: [regionkey, count] + - Output[regionkey, _col1] => [regionkey:bigint, count:bigint] + _col1 := count + - RemoteSource[1] => [regionkey:bigint, count:bigint] + + Fragment 1 [FIXED] + Output layout: [regionkey, count] + - Aggregate(FINAL)[regionkey] => [regionkey:bigint, count:bigint] + count := "count"("count_8") + - RemoteSource[0] => [regionkey:bigint, count_8:bigint] + + Fragment 0 [SOURCE] + Output layout: [regionkey, count_8] + Output partitioning: [regionkey] + - Aggregate(PARTIAL)[regionkey] => [regionkey:bigint, count_8:bigint] + count_8 := "count"(*) + - TableScan[tpch:tpch:nation:sf0.01, original constraint=true] => [regionkey:bigint] + regionkey := tpch:tpch:regionkey:2 diff --git a/presto-docs/src/main/sphinx/sql/insert.rst b/presto-docs/src/main/sphinx/sql/insert.rst new file mode 100644 index 00000000..43dd6cea --- /dev/null +++ b/presto-docs/src/main/sphinx/sql/insert.rst @@ -0,0 +1,37 @@ +====== +INSERT +====== + +Synopsis +-------- + +.. code-block:: none + + INSERT INTO table_name query + +Description +----------- + +Insert new rows into a table. + +.. note:: + + Currently, the list of column names cannot be specified. Thus, + the columns produced by the query must exactly match the columns + in the table being inserted into. + +Examples +-------- + +Load additional rows into the ``orders`` table from the ``new_orders`` table:: + + INSERT INTO orders + SELECT * FROM new_orders; + +Insert a single row into the ``cities`` table:: + + INSERT INTO cities VALUES (1, 'San Francisco'); + +Insert multiple rows into the ``cities`` table:: + + INSERT INTO cities VALUES (2, 'San Jose'), (3, 'Oakland'); diff --git a/presto-docs/src/main/sphinx/sql/reset-session.rst b/presto-docs/src/main/sphinx/sql/reset-session.rst new file mode 100644 index 00000000..98df37ec --- /dev/null +++ b/presto-docs/src/main/sphinx/sql/reset-session.rst @@ -0,0 +1,24 @@ +============= +RESET SESSION +============= + +Synopsis +-------- + +.. code-block:: none + + RESET SESSION name + RESET SESSION catalog.name + +Description +----------- + +Reset a session property value to the default value. + +Examples +-------- + +.. code-block:: sql + + RESET SESSION optimize_hash_generation; + RESET SESSION hive.optimized_reader_enabled; diff --git a/presto-docs/src/main/sphinx/sql/select.rst b/presto-docs/src/main/sphinx/sql/select.rst new file mode 100644 index 00000000..652f49fb --- /dev/null +++ b/presto-docs/src/main/sphinx/sql/select.rst @@ -0,0 +1,315 @@ +====== +SELECT +====== + +Synopsis +-------- + +.. code-block:: none + + [ WITH with_query [, ...] ] + SELECT [ ALL | DISTINCT ] select_expr [, ...] + [ FROM from_item [, ...] ] + [ WHERE condition ] + [ GROUP BY expression [, ...] ] + [ HAVING condition] + [ UNION [ ALL | DISTINCT ] select ] + [ ORDER BY expression [ ASC | DESC ] [, ...] ] + [ LIMIT count ] + +where ``from_item`` is one of + +.. code-block:: none + + table_name [ [ AS ] alias [ ( column_alias [, ...] ) ] ] + +.. code-block:: none + + from_item join_type from_item [ ON join_condition | USING ( join_column [, ...] ) ] + +Description +----------- + +Retrieve rows from zero or more tables. + +WITH Clause +----------- + +The ``WITH`` clause defines named relations for use within a query. +It allows flattening nested queries or simplifying subqueries. +For example, the following queries are equivalent:: + + SELECT a, b + FROM ( + SELECT a, MAX(b) AS b FROM t GROUP BY a + ) AS x; + + WITH x AS (SELECT a, MAX(b) AS b FROM t GROUP BY a) + SELECT a, b FROM x; + +This also works with multiple subqueries:: + + WITH + t1 AS (SELECT a, MAX(b) AS b FROM x GROUP BY a), + t2 AS (SELECT a, AVG(d) AS d FROM y GROUP BY a) + SELECT t1.*, t2.* + FROM t1 + JOIN t2 ON t1.a = t2.a; + +Additionally, the relations within a ``WITH`` clause can chain:: + + WITH + x AS (SELECT a FROM t), + y AS (SELECT a AS b FROM x), + z AS (SELECT b AS c FROM y) + SELECT c FROM z; + +GROUP BY Clause +--------------- + +The ``GROUP BY`` clause divides the output of a ``SELECT`` statement into +groups of rows containing matching values. A ``GROUP BY`` clause may +contain any expression composed of input columns or it may be an ordinal +number selecting an output column by position (starting at one). + +The following queries are equivalent. They both group the output by +the ``nationkey`` input column with the first query using the ordinal +position of the output column and the second query using the input +column name:: + + SELECT count(*), nationkey FROM customer GROUP BY 2; + + SELECT count(*), nationkey FROM customer GROUP BY nationkey; + +``GROUP BY`` clauses can group output by input column names not appearing in +the output of a select statement. For example, the following query generates +row counts for the ``customer`` table using the input column ``mktsegment``:: + + SELECT count(*) FROM customer GROUP BY mktsegment; + +.. code-block:: none + + _col0 + ------- + 29968 + 30142 + 30189 + 29949 + 29752 + (5 rows) + +When a ``GROUP BY`` clause is used in a ``SELECT`` statement all output +expression must be either aggregate functions or columns present in +the ``GROUP BY`` clause. + +HAVING Clause +------------- + +The ``HAVING`` clause is used in conjunction with aggregate functions and +the ``GROUP BY`` clause to control which groups are selected. A ``HAVING`` +clause eliminates groups that do not satisfy the given conditions. +``HAVING`` filters groups after groups and aggregates are computed. + +The following example queries the ``customer`` table and selects groups +with an account balance greater than the specified value:: + + + SELECT count(*), mktsegment, nationkey, + CAST(sum(acctbal) AS bigint) AS totalbal + FROM customer + GROUP BY mktsegment, nationkey + HAVING sum(acctbal) > 5700000 + ORDER BY totalbal DESC; + +.. code-block:: none + + _col0 | mktsegment | nationkey | totalbal + -------+------------+-----------+---------- + 1272 | AUTOMOBILE | 19 | 5856939 + 1253 | FURNITURE | 14 | 5794887 + 1248 | FURNITURE | 9 | 5784628 + 1243 | FURNITURE | 12 | 5757371 + 1231 | HOUSEHOLD | 3 | 5753216 + 1251 | MACHINERY | 2 | 5719140 + 1247 | FURNITURE | 8 | 5701952 + (7 rows) + +UNION Clause +------------ + +The ``UNION`` clause is used to combine the results of more than one +select statement into a single result set: + +.. code-block:: none + + query UNION [ALL | DISTINCT] query + +The argument ``ALL`` or ``DISTINCT`` controls which rows are included in +the final result set. If the argument ``ALL`` is specified all rows are +included even if the rows are identical. If the argument ``DISTINCT`` +is specified only unique rows are included in the combined result set. +If neither is specified, the behavior defaults to ``DISTINCT``. + +The following is an example of one of the simplest possible ``UNION`` +clauses. The following query selects the value ``13`` and combines +this result set with a second query which selects the value ``42``:: + + SELECT 13 + UNION + SELECT 42; + +.. code-block:: none + + _col0 + ------- + 13 + 42 + (2 rows) + +Multiple unions are processed left to right, unless the order is explicitly +specified via parentheses. + +ORDER BY Clause +--------------- + +The ``ORDER BY`` clause is used to sort a result set by one or more +output expressions: + +.. code-block:: none + + ORDER BY expression [ ASC | DESC ] [ NULLS { FIRST | LAST } ] [, ...] + +Each expression may be composed of output columns or it may be an ordinal +number selecting an output column by position (starting at one). The +``ORDER BY`` clause is evaluated as the last step of a query after any +``GROUP BY`` or ``HAVING`` clause. + +LIMIT Clause +------------ + +The ``LIMIT`` clause restricts the number of rows in the result set. +The following example queries a large table, but the limit clause restricts +the output to only have five rows (because the query lacks an ``ORDER BY``, +exactly which rows are returned is arbitrary):: + + SELECT orderdate FROM orders LIMIT 5; + +.. code-block:: none + + o_orderdate + ------------- + 1996-04-14 + 1992-01-15 + 1995-02-01 + 1995-11-12 + 1992-04-26 + (5 rows) + +TABLESAMPLE +----------- + +There are multiple sample methods: + +``BERNOULLI`` + Each row is selected to be in the table sample with a probability of + the sample percentage. When a table is sampled using the Bernoulli + method, all physical blocks of the table are scanned and certain + rows are skipped (based on a comparison between the sample percentage + and a random value calculated at runtime). + + The probability of a row being included in the result is independent + from any other row. This does not reduce the time required to read + the sampled table from disk. It may have an impact on the total + query time if the sampled output is processed further. + +``SYSTEM`` + This sampling method divides the table into logical segments of data + and samples the table at this granularity. This sampling method either + selects all the rows from a particular segment of data or skips it + (based on a comparison between the sample percentage and a random + value calculated at runtime). + + The rows selected in a system sampling will be dependent on which + connector is used. For example, when used with Hive, it is dependent + on how the data is laid out on HDFS. This method does not guarantee + independent sampling probabilities. + +.. note:: Neither of the two methods allow deterministic bounds on the number of rows returned. + +Examples:: + + SELECT * + FROM users TABLESAMPLE BERNOULLI (50); + + SELECT * + FROM users TABLESAMPLE SYSTEM (75); + +Using sampling with joins:: + + SELECT o.*, i.* + FROM orders o TABLESAMPLE SYSTEM (10) + JOIN lineitem i TABLESAMPLE BERNOULLI (40) + ON o.orderkey = i.orderkey; + +.. _unnest: + +UNNEST +------ + +``UNNEST`` can be used to expand an :ref:`array_type` or :ref:`map_type` into a relation. +Arrays are expanded into a single column, and maps are expanded into two columns (key, value). +``UNNEST`` can also be used with multiple arguments, in which case they are expanded into multiple columns, +with as many rows as the highest cardinality argument (the other columns are padded with nulls). +``UNNEST`` can optionally have a ``WITH ORDINALITY`` clause, in which case an additional ordinality column +is added to the end. +``UNNEST`` is normally used with a ``JOIN`` and can reference columns +from relations on the left side of the join. + +Using a single column:: + + SELECT student, score + FROM tests + CROSS JOIN UNNEST(scores) AS t (score); + +Using multiple columns:: + + SELECT numbers, animals, n, a + FROM ( + VALUES + (ARRAY[2, 5], ARRAY['dog', 'cat', 'bird']), + (ARRAY[7, 8, 9], ARRAY['cow', 'pig']) + ) AS x (numbers, animals) + CROSS JOIN UNNEST(numbers, animals) AS t (n, a); + +.. code-block:: none + + numbers | animals | n | a + -----------+------------------+------+------ + [2, 5] | [dog, cat, bird] | 2 | dog + [2, 5] | [dog, cat, bird] | 5 | cat + [2, 5] | [dog, cat, bird] | NULL | bird + [7, 8, 9] | [cow, pig] | 7 | cow + [7, 8, 9] | [cow, pig] | 8 | pig + [7, 8, 9] | [cow, pig] | 9 | NULL + (6 rows) + +``WITH ORDINALITY`` clause:: + + SELECT numbers, n, a + FROM ( + VALUES + (ARRAY[2, 5]), + (ARRAY[7, 8, 9]) + ) AS x (numbers) + CROSS JOIN UNNEST(numbers) WITH ORDINALITY AS t (n, a); + +.. code-block:: none + + numbers | n | a + -----------+---+--- + [2, 5] | 2 | 1 + [2, 5] | 5 | 2 + [7, 8, 9] | 7 | 1 + [7, 8, 9] | 8 | 2 + [7, 8, 9] | 9 | 3 + (5 rows) diff --git a/presto-docs/src/main/sphinx/sql/set-session.rst b/presto-docs/src/main/sphinx/sql/set-session.rst new file mode 100644 index 00000000..8321a7e7 --- /dev/null +++ b/presto-docs/src/main/sphinx/sql/set-session.rst @@ -0,0 +1,24 @@ +=========== +SET SESSION +=========== + +Synopsis +-------- + +.. code-block:: none + + SET SESSION name = 'value' + SET SESSION catalog.name = 'value' + +Description +----------- + +Set a session property value. + +Examples +-------- + +.. code-block:: sql + + SET SESSION optimize_hash_generation = 'true'; + SET SESSION hive.optimized_reader_enabled = 'true'; diff --git a/presto-docs/src/main/sphinx/sql/show-catalogs.rst b/presto-docs/src/main/sphinx/sql/show-catalogs.rst new file mode 100644 index 00000000..82b0db4a --- /dev/null +++ b/presto-docs/src/main/sphinx/sql/show-catalogs.rst @@ -0,0 +1,15 @@ +============= +SHOW CATALOGS +============= + +Synopsis +-------- + +.. code-block:: none + + SHOW CATALOGS + +Description +----------- + +List the available catalogs. diff --git a/presto-docs/src/main/sphinx/sql/show-columns.rst b/presto-docs/src/main/sphinx/sql/show-columns.rst new file mode 100644 index 00000000..898b233d --- /dev/null +++ b/presto-docs/src/main/sphinx/sql/show-columns.rst @@ -0,0 +1,15 @@ +============ +SHOW COLUMNS +============ + +Synopsis +-------- + +.. code-block:: none + + SHOW COLUMNS FROM table + +Description +----------- + +List the columns in ``table`` along with their data type and other attributes. diff --git a/presto-docs/src/main/sphinx/sql/show-functions.rst b/presto-docs/src/main/sphinx/sql/show-functions.rst new file mode 100644 index 00000000..fb6b8603 --- /dev/null +++ b/presto-docs/src/main/sphinx/sql/show-functions.rst @@ -0,0 +1,15 @@ +============== +SHOW FUNCTIONS +============== + +Synopsis +-------- + +.. code-block:: none + + SHOW FUNCTIONS + +Description +----------- + +List all the functions available for use in queries. diff --git a/presto-docs/src/main/sphinx/sql/show-partitions.rst b/presto-docs/src/main/sphinx/sql/show-partitions.rst new file mode 100644 index 00000000..51608636 --- /dev/null +++ b/presto-docs/src/main/sphinx/sql/show-partitions.rst @@ -0,0 +1,33 @@ +=============== +SHOW PARTITIONS +=============== + +Synopsis +-------- + +.. code-block:: none + + SHOW PARTITIONS FROM table [ WHERE ... ] [ ORDER BY ... ] [ LIMIT ... ] + +Description +----------- + +List the partitions in ``table``, optionally filtered using the ``WHERE`` clause, +ordered using the ``ORDER BY`` clause and limited using the ``LIMIT`` clause. +These clauses work the same way that they do in a :doc:`select` statement. + +Examples +-------- + +List all partitions in the table ``orders``:: + + SHOW PARTITIONS FROM orders; + +List all partitions in the table ``orders`` starting from the year ``2013`` +and sort them in reverse date order:: + + SHOW PARTITIONS FROM orders WHERE ds >= '2013-01-01' ORDER BY ds DESC; + +List the most recent partitions in the table ``orders``:: + + SHOW PARTITIONS FROM orders ORDER BY ds DESC LIMIT 10; diff --git a/presto-docs/src/main/sphinx/sql/show-schemas.rst b/presto-docs/src/main/sphinx/sql/show-schemas.rst new file mode 100644 index 00000000..826f95b9 --- /dev/null +++ b/presto-docs/src/main/sphinx/sql/show-schemas.rst @@ -0,0 +1,28 @@ +============ +SHOW SCHEMAS +============ + +Synopsis +-------- + +.. code-block:: none + + SHOW SCHEMAS [ FROM catalog ] + +Description +----------- + +List the schemas in ``catalog`` or in the current catalog. + +Examples +-------- + +.. code-block:: none + + presto:default> show schemas; + Schema + -------------------- + information_schema + jmx + sys + (3 rows) diff --git a/presto-docs/src/main/sphinx/sql/show-session.rst b/presto-docs/src/main/sphinx/sql/show-session.rst new file mode 100644 index 00000000..5fa84cff --- /dev/null +++ b/presto-docs/src/main/sphinx/sql/show-session.rst @@ -0,0 +1,15 @@ +============ +SHOW SESSION +============ + +Synopsis +-------- + +.. code-block:: none + + SHOW SESSION + +Description +----------- + +List the current session properties. diff --git a/presto-docs/src/main/sphinx/sql/show-tables.rst b/presto-docs/src/main/sphinx/sql/show-tables.rst new file mode 100644 index 00000000..9d549604 --- /dev/null +++ b/presto-docs/src/main/sphinx/sql/show-tables.rst @@ -0,0 +1,16 @@ +=========== +SHOW TABLES +=========== + +Synopsis +-------- + +.. code-block:: none + + SHOW TABLES [ FROM schema ] [ LIKE pattern ] + +Description +----------- + +List the tables in ``schema`` or in the current schema. +The ``LIKE`` clause can be used to restrict the list of table names. diff --git a/presto-docs/src/main/sphinx/sql/use.rst b/presto-docs/src/main/sphinx/sql/use.rst new file mode 100644 index 00000000..4605720a --- /dev/null +++ b/presto-docs/src/main/sphinx/sql/use.rst @@ -0,0 +1,27 @@ +=== +USE +=== + +Synopsis +-------- + +.. code-block:: none + + USE catalog.schema + USE schema + +Description +----------- + +Update the session to use the specified catalog and schema. If a +catalog is not specified, the schema is resolved relative to the +current catalog. + + +Examples +-------- + +.. code-block:: sql + + USE hive.finance; + USE information_schema; diff --git a/presto-docs/src/main/sphinx/themes/presto/layout.html b/presto-docs/src/main/sphinx/themes/presto/layout.html new file mode 100644 index 00000000..3cbcded0 --- /dev/null +++ b/presto-docs/src/main/sphinx/themes/presto/layout.html @@ -0,0 +1,38 @@ +{%- extends 'basic/layout.html' %} + +{% block relbar1 %}{% endblock %} +{% block relbar2 %}{% endblock %} + +{% macro nav() %} + +{% endmacro %} + +{% block content %} +
+

+ {{ shorttitle|e }}

+

{{ title|striptags|e }}

+
+
+ {{ nav() }} +
+
+ {% block body %}{% endblock %} +
+
+ {{ nav() }} +
+{% endblock %} diff --git a/presto-docs/src/main/sphinx/themes/presto/static/alert_info_32.png b/presto-docs/src/main/sphinx/themes/presto/static/alert_info_32.png new file mode 100644 index 0000000000000000000000000000000000000000..f0f5ebb8897aea32a6a65c005296e819d1f11de3 GIT binary patch literal 1530 zcmY*Y2~bm46n!Kh5DKI&Xhj@c5M(Tdt)M77gs_A_5^%u+1*a$$f1ZXQRl%abz?9kS?-qd$~|?$Bki?k z`XoE!jaTLrC+n?G&O7eO_x`75RVo!qK*j=-m#aiM_yCB24-PO=lq>+s{Z7oFoRR_= z%D%@HXotS&7?Afj0HpvAjCSze+0#^zYK;;AiW!(chboBU2oF9M0ODvz-G%Z{0277q zz-WzOFataYAft-X=NEi{h_Gk@cZUuxA}E|k3Ui_g7`MX=@Cf3Fg$YrD2rx8p1QS^R zsNrf*3o2ejYs9H%3qF()2f#d3mYN(KR3Sxqq$ojTp+JoakJ+#&Wq4$T9x0;)?SS(9R zODijDJG&iD91h3Z$0tA_5W<3jfSiinAcg^7|=G71aJ%F8P%E32xis%vWMFN?2T zZ@%5y*51{9|G|<;B$A%q-oC!R{(&chPoE8q$Yiq7(a{$#$H(Pz`NZqVDaFkDS(p+w z_hEj1VPRo$$v%Fx(f|5Wd@cbTcNc+u(TVSs`+J?&{61wNuXxy#&Dh5iP35J?I_`R` zw6hpY8Kx->G~YDZGgpb~3=Z|$_*tkbszxp9cRA`$`6o>+-jTH_mtAa*FgDt}*VFU* z^R@TP%|6fIg{qqF+%TSSa*hcQws;h8_4bX?4qi2OOT+VAbfvI}*=6j`AA7N4QyHI+AqrC7qG%M?<4mPl|2sXX&vTwn%!5q>KC})h-R0ZI^vX zqD$OJcU3xbCM{fYVV(3BwLRM=xP!T|YwaW3g}l=SZaP{|y<6hk3Kq24wiDKI4v{ob z?dvaC>7hH~`Tdjhjnd;Qo%ah&Lz=@ytcM5n{dvtB7`djl>)V}7LOaiupGe(2S?HxE zjW{QY9Tx|0AjemwZh0qh%i=!&>}_<c)c*rQPt&i?}Dw?Z#lySzba)-7wc@t^)^?HfbF|W9tQsarc z%(|!IZI3@u)SAufDHZZu`m?C$;-;L|zZ!VVtC6-+Z)V5AYY8$(t4qP*{KLjGEm8A^ zmL%$rchgd#Vap)9>Ad0T#+y5@G#Hg64*1+UaC1e|;vnHD4E@4F*<{O`MZ;!UQNbm% zBV+2%vy}p~xevpJ!&*P94wd?gs)A;8oxWlEl}4nlwkVp-YwCZj6DTb^eA%ScKY_EI zlgV+?>->1O^Ff9GncDTjnC0^<@ce@a5%rrbXbwM P^jUB`xb9-tu6Lo)-z&;LOBB?CjL0RzLU1O^7H84L{K`IF+0x-l>?Mh5tVxcY~s285>t zMx+NtW`xD&L?sp^WmY6-RivRx0!bhj;yfS&VjzT!O(}*TpkkmiAbNpp zpo0_A%Mvrn;lY)fQ=OJu12iflzb>n=;WwwHIWR&}N`m}?fix5_GBLBTv2$_p2nY#@ zNysTGDXD2_8=08d+B-Tqxp{c|_yq-pMaCqhq-GVCl$O>tG`6<3_fMQSbQDrKS1IoM~955_#$0f9JOnlS~S$U(DN*f4}DU63K;4_FR86PprAO`!?Sf z@6ec+zcM2~#6+647(AQe;QZH^-9SFO`B_D~2ZQIfW`Wp3ZiWN*Zx^5M?w_%Rcaogg zQy+^=4v%RD$BK1V?lO0r@}!-Mb%yy=ONP~)oNL@JuaZ1*#LH~a&#PV@(xO)!*3QxQ zjSTMz)K<(5o+6g@yyu}jH`}8!r)!Hf?w&a(`TlfFP*u+918s(KC4R@3N;YIZ?|Q~^ zEO}Y+)tMzOUldn1S+0Jh`Td80FLRE-%N5?Mc0G_#x#^=Ab$`d_wwl<`>w7*&uQu<0 z6ZF#d)AQxuA541Zeuq_g*XqV6Q;M$hILVhzn=;?gTe~ HDWM4fM8zSn literal 0 HcmV?d00001 diff --git a/presto-docs/src/main/sphinx/themes/presto/static/presto.css_t b/presto-docs/src/main/sphinx/themes/presto/static/presto.css_t new file mode 100644 index 00000000..b8510547 --- /dev/null +++ b/presto-docs/src/main/sphinx/themes/presto/static/presto.css_t @@ -0,0 +1,114 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import url("haiku.css"); + +html { + background: #fff; + -webkit-font-smoothing: antialiased; +} + +body { + font-family: 'Helvetica Neue', Helvetica, Arial, 'lucida grande', tahoma, verdana, arial, sans-serif; + color: #222222; +} + +div.header { + background-color: #374665; + background-image: url(presto.png); + background-position: 36px 50%; + background-repeat: no-repeat; + background-size: 57px 50px; + padding: 10px 40px 20px 120px; +} + +div.header h1 a { + color: #fff !important; +} + +.nav .right { + float: right; + text-align: right; +} + +div.topnav { + background: #eeeeee; +} + +div.topnav, +div.bottomnav { + padding: 10px 0; +} + +div.topnav p, +div.bottomnav p { + font-size: 1.0em; + margin: 0 40px; + text-align: left; +} + +div.header h1 { + font-size: 1.8em; +} + +h1 { + font-size: 1.5em; +} + +h2 { + font-size: 1.4em; +} + +h3 { + font-size: 1.3em; +} + +h4 { + font-size: 1.2em; +} + +div.content { + font-size: 1.0em; +} + +div.content ul > li { + background-image: url(bullet.png); +} + +div.admonition { + border-radius: 3px; + border-style: solid; +} + +div.note { + background-color: #f5f5ff +} + +tt { + font-family: Consolas, 'Liberation Mono', Courier, monospace; + font-size: 0.9em; +} + +tt.descname { + font-size: 1.1em; +} + +tt.docutils.literal { + margin: 0 2px; + padding: 0 5px; + border: 1px solid #ddd; + background-color: #f8f8f8; + border-radius: 3px; + white-space: nowrap; +} diff --git a/presto-docs/src/main/sphinx/themes/presto/static/presto.png b/presto-docs/src/main/sphinx/themes/presto/static/presto.png new file mode 100644 index 0000000000000000000000000000000000000000..1afe4580e38673e30622f7ac09cd42013f4020b5 GIT binary patch literal 10985 zcmV4Tx07wn3mUmQB$rs1(d+Ci9h=7#PJ5r=euK^UK3N}I#NGKtM1QENkxB_cI zMFcClBBJ1009gxS!NMZgurJ7>qOOYt8!EpS_SK0rb zP~{79cmPNwHl57R;bmfc4C_015~&OT7lYZ27swiP_n?yj08&?H$H;V!lWWKsUzNiznprF~&MrXJ%$8cs<7E z$$#^c`ZwlslP7#~xv3L877O+z_TvCijwFM-aP;N>`N+(WWBD3wdf1bmlO7$0jiy-6 z@_5|9=y4VbIEpMJtg-z(i$ulCx?Yta+EcatR z^Mx_-i|? zCSpmE22E%LIp5=z31dRW{m8HbFwQBWAkH`or6Ch`Nyg}z9$kY}9yO5%fjD*|hkS0p zL~bDlgdhho@Ie4lNQNxP02#0$07Q@j66}o&*mlbVdDs~P@XwMgl!{V?GPW;vD+JbT zj+kd_&33S}cLc>9Q)mF*vPSO?!n*R^mfZs2R)p=R;&`(aDgdq#z= z>8VbhT$($k!z%{D=OsLIf5`0?Y#*7EdM?$pTmmE1(dH zVLfby3fKvIU_Ts!BhUyZ;VfK)tI!Fz;6C&~KfHiJcn`w}f{+jf!bG$XeZ&N@LL3lR z#0v>T!jUK>0ZBqq5iuf179*>WVx$zQKz1WF$S=qVY}FT6x0>S@f2^R>r2>pcjL?V$zG$XnY zLx>5)G~yy+32{5Ip4dX{BK8r7NFpvY5P*Tt+@XK25$(eo7vus8CEO?vw}$pR$m$jHhRNbUA%3y_()aze|71 zU@%M>K8!?$jIowc!#Kxy#Q3PhRI*j#DDjn+DOD;pC|y^2rA$#aQTA1yr<|``rhHWS zn(|8(ii)Xc#4Z)H~D%HPkewYQ$;eX;f;Q(&%B~Sf(rv zD}%L;b%b@B^?8!+B;QGbNoyw6O}akmgXUySA5DQ~k!HQ-EzQqbY^^}8bgd0qjam=2 z@!FQ!Gqo3J@6f)W{c^JUWRJR9ST=`7OOqtmW4q^qYJtSiyos(ViN zr5;PqM=wopliq2)0ez;vm%d1Ull~d~=WGqOFFS)>&OXl`G|({!F_0VVHs~-IHZ(Jg zHC$;}Z}`xNYUFB^VzkMq)o9RI-*~$5V&huldnObUR}-Ph7LzuU52j|O@usUy8%>{? zX_|4&^37_^9+)%Cz0I@DtITg&5G`CS(k-@IbXua8PL@K;O3Q0jsFkyo$ZES)mo>rK z&01_-W!-HfwW+n~wPo3c+pe^2w0$+jcuL}w4O7~te6w@1%dp#Pci&#kKGc4t zeY5==2TKQ@L#4w_M}}jN<1)u4$JbM>rV6I+n0m*F=``JGwNtCpS7#SzsdJt4fQzw9 zlFK%iZdY~JNY@hAOKt==KeuIWC*3}|JG;x=kGTKkVdIhRalqr*G_z@fX?v&jc^Z0h zJ$HNdc(J{bymoo@cpG?gy{o(*`xyD~efIf0^|kO#^F8SM%FoVE>UZ4lgTK4~691L} zTtIL@Q9wtaT3~EoMc|_#!=Til+MvN;r{G1wEg^)Eu#gQQ-5gyGk5j`L40Q=z5_&$2 z9u^%|5%xITGF%$oJRO}LI(_5x2Qy4&h-WlLKm;dZW5mNqv&ihoUuP0$M$W99`6S9d zYH?IsG&6dB^nvL2F@7=YV(!P9$I4?{AoB!XKqfD zOdTs`+U4f*ALPBcUb&UI9~Zhwks|x{!+p#x$=|!Pvt+2tW8(4d&wD%S7`&uoo?%5x;|9p*0P0azF1M6zhT6yij!8r#z4+R`*sfEyQY za&BI|HRD#-?bzFQy61HF+~MDOb~oeh>wCHPhVL(bKzLB}P~~CSBb`T8zgzrX-{ahK zsyCpw{c-f;`+dB=7k}jZ@uk1u3FArGQ~jqk&m5ke90(fdd_Mbm{|m{B;g^Mfs{Xm{ zmHDe$eWV4T5qf0O?}ro6gl+hedhb&4@DofKJNeI`l;=6{O6~` z3%-!Plz+AO+Wal-+k=tJkr4@3${no*aM%+~Ndb6M4ZujiYKKk$s?t~uJgPv(>UNA3 z^}C`g+CO-#P9B|c2B`J{h+Pg4-U6@#>n0e}u~AXZ#rOc~=s3|ai=ySEI6901LYHEt z$;XkAx4HnNW`M8Kk&)r;BO_n;Vzv5RfR^mB+FPL}ssNOlvFy;o_HGRS(f$K$CjIT` z5=$fi000SaNLh0L01FcU01FcV0GgZ_001AzNkl?k>KZJ6-dHk7l9C5NIqC}MMY79 zvIYdgLmr|aU`QYYF(O!|5o2lO{J>4x~Id;V9qzE>(;&Jo_p^(_uR*+TQ#{L z2y8P9eF@};eiJsgKoZ!j$ahIU*sRFSihP$y;9b&;zeTKvS9g}T=2ft0EHR+cefytMtuh&kvGLXeMKv*xW!XETF5L@J{Xt>WWx z&D_t7@rm9?!_Dv>40)X3GV~%@M$(2eEj|Ow4J0th!@R7RkGx`g2PNC7T(DOk+|iy; zT<&7hXN2jxZxk*j|BOohljho|DCj84+zxyHBmL&K`DfXK;wg(&KWdc!V4%;UNDaRd z@i5r)Yugk*1OKuL2%x@={CpHHdMeMA+9$^MwR%#)OiN@PvD8*UEmZ@ zP>)YY75jC9!&12BG08cFZR}JyZHM5+@jcuh*H*CW4tv46WRaqev8a1H^Wrd1&)ark z`!GAD)N933kF75it+#hQO3cGMXO~Z!V;_~mhw7YJPXSJ*Nb79y!(g$3JzVL}HWM5L z%3|yBf}}5WSMnV2vK@(abQj-*f6jWbm_XO!8&=A-+0mE?%CJw!!>nW<%y$uikO{YO&AF=-N{P&w2Z$cUH&n)xdx!M-b{j+X9m-|L zxo?{TYb(<|vC0pg!tkqdpc-<}w^7N}S3iDJNx59E#6``N1xl#vz*S&V@MJ|XFk0;x zLe(YTVu2e272t18M-Sa+h(awzs>we6)=K+iiBh27do3dT zFes368v^7UiZyTdl~&nbu*wb?^b%X5kltLu3PI6Um-BX~7%%Lu&}S@V;exPEJv-V{ zWO<4qR4dbPfDL)5&ZR;)zWm~vsn)CEm`Ww7J7cK>L*^BgR=RJ{Z4jIXY9*;&D*yem0Ez&P~Sebr2pR-G#+n2>vI~`>e zJ>{ppMLYkNbL=@!m&Ru>$7fNb>Mvyq;(~0M64Mlw79Un8`>$8h#JM)2J+N`i4I@~| zuDD@ZI5qh6B#v$sTWL9$xD1lAyz_Alx|ifG^XNK3hWw(-M9Z>RZaU|Zi-v2JA-W0o z1O=hbn?~Qgz_}P9>+1$gbJ3x*Ts)6x)9B-T-^u%|7THV>lNEc5RWH^8N!Pl=a26{R zjj`E}yY7g0^ECyjtc^hVx))l0Jr zJgeMIrAXwEEJ)&OrAbD0As6wzj(^!ch`VhMmI9Kq_@Hmv_KxABfySwx%^l@pTC^&b0G3a zRWwQI;Dx;33LD;0wy(hDB(^QvBWMfO*~?t!KD>*~;_6&_icaUYaf;?cS*Xf6ajt!x zdGtIS3MQukX`z31tvyF@68_l-28%LVt;nbnwH-e?CaUMT|0IYnpgQcvNYZb;kVSFe zGP|a2YnxhT-+)DG^=#UA<`@8jHFn8wM%kC6maFtE;D^k~bL`*SI_+EdWz`{i=?!X| zx?qVdOcz_3r}5KHn0KbVDy28tVY1Yv5t^`ajpWEidk$n$qUMt#lfJIni0=-51ElS4 zuGBR%bUTL~k6)JAVK@q>mfmuHQ#laiH_ZpV)>B^VOz_yn>pj%smL|7ZHXEPXzr5mh2cDAxut~v1PYvD!eISh zjLUUAoT1M+6_z&2uR%6frUqDz7F7*_x2$}xEr;_;c&Jk*)=`Tq;6?qs+lE#(&0F|v z!a7Bu9;?nPR1X)-0^bHjnUM}VDWqu)aBvTj6ByvO>NC zNBfot_zkT2lA`! z*8BIcw+Ks#O$k`5S1ITC*|u|`4gXP@6AHQNy#EZ<{nkxq+0CjpzptCvv8#@vv#|a=>_!gj zNco;NN~3(Pvqk8t=>)1ycBRX~ZL#yKJ|DDLN%9%lxMSSsNpp@X9^Ln;m+n=D&)fYU z!LZ5S2!=;_M8lr0RaaMVzk5Uf8OREw*7qLn{}N#vPmd&p!)Q| zN}GSr3@b)0-Sce4bucK>55ZbcoC*FbxHb3|cqe!zh`+`O(+P`kX7CE)n#Ut_0Yd#; zZn*7$JI~|=DzCT4Svbw$6v7>zHZ$`WA#T0%spTw=l;q``707WQ#HDvfsbHsnZ>qhv zDU>J;BW(ULheCJ>q$*-;C&N1!7-cu`J)x8gB4uzqfny8?UDv=X` zzHNPIo4pxGMfy6+e==4#tQX?-@%LK%j1moyNyrdYPY)0tgA-jjWg%(6qsOowX$0|j zoib2dDR0}f%Wc3hPHtUXoR(tqWl~ApMQPjzQgEEE*?TXjX7jfqpB`^HM}ek|7(;)* zns~YVGJ_LTMW{&YHTV2QC(Y9#;Dw=;>B;FS2yqQAJ26j}C%sfdFPoIVq;qOMCB5M0 zd1lQip{hs9h4(*|OzP1Za+Id4AAS}Q))7qA*T)g84c^I4jzz3aRT|5iSbiOb?r=S5 zDP!*93ddRsuNFF?QWs45w>-Oio}(rOYOXfKLC$>l1{jQgtwb%7{FWt7+|2zzDpK|J zLyMg3_k-Gks7{L4RrkLIF9jP0?=XLGSG$Xq>aD!pr3H=_I-!%DE0)?JYh~5?wFh>w zKT~jZ&sncPx$(4>xbaL|hEi_eVY6z^lpHS9oTBs=OU{bzoz_w8wO{fE*pJ2c-8M(c zx}-1-X~%+BfUkhE(iPx!pdL~$1x1(H8ljb0>uCkYD=k=Gv}wiH?Iy+JU@zo5tv%Q7 zUP*WpxBht#?$X>zbN?*zckOl=?^<4mG7FsX>16(EXDBV?=_n|%3kUpTyu5qHf^%)H zl&`v}E`rKpD5rRJq-N&Mf!;@kE$OSI?TGX*x_T#jEhW-+XTjnn;Zy~r2tiJQt307N zaJgIl9|_eq5H4kTb=$+E>}f{_DRF)Bt0OQ(=dkX8=Z-l&(~+e|=_GvJOA*g*Z)ZTI zz2a^lHX6K&=k6Xl&1E-DwU%dkSXz=iWePmj{iZ+ZC3(^5IqK>)w~}6#Dk;lmr94OuQF4VUs0I& zIi4!R!;~xSLu_A8h1X#eJd$|5D7BaxU4PTrwn%i*`sh9h2Ad+2>KKosT~&chA$&48 z8k91g2d@V200mLnFs9CLRA~5Sr@`SOTzk8??F)&V+|#L`9~BFB?js{{h{u#!DKw`C z)K5NXIZoxNUVRkk%fQQX8VOgh2OU;s6XuDuXu>zEd|$z(cd_G1Cvl4>-6X(JQzSB| zw)Z=P|1F_SinLTt z?i2)H9g5>TqxYUpZ-jLQb}wHT8OKK{*3mV6oi~tcf6I!O{prgje)zj*+0t6d^a9wx zVDN^JgBxz}Sh6w%cql8yNyuu4Py&%+1*e7=r(ET&3x02h5|82hPpHJWZ&n6_!(9$| zu16m5lI?+J-F#WE0Ds!9L+4?0D3LFWpu}h=*}x*l{L2>=*outm4eTG4prLvLLEq3& zQ>1Dyzw6!#u;EIozmL-kxg$iE1-!Lzl|Oc090*cWnr}8o7b=s!Gr}G#{?!GPZtBBs z9O}{}p?NS(If^g4gI;-kHv^!MH1Lg1OWz!yA_0xBf$ zjh}O`SkycSJ=C@zlADC)6?zKcDd1>O7Wo1whrsQiAW9p?gmH@ZX?`en=|ony68L4O z6M13VaNF5Y)9rBxsPrEb{5(%feh+F*NNC=mb&E^5=)QM$`1Q?bed2umy@sVh1KRMaGPKq`=TkWPq(Ju-kP>O1qnBd- zREgKcD@!yUM5}wR#P<=_ET^X{-ao6dol=~z;jcOKp>D@J${#XUc-#_m0YkH1W8hP0 zM^LkU&~zuH&!eRKg3%ni^!YUZVPP|8TJGiE5tISQf}jXRca8Qe|!pTL#iAHaFwexUM1X&n!K z^4*-2H%^!9M7j$n&?*18Wx;30FAttQ==C7@@S8#4uB(FJfMvm|@ymj*PY8l8(TQiJ zJa;d~Wqt?=+1lQ?Z0>uFh-6y`D}RN{u(QW({7$_-;Xvt zYvZ8vO8FPUegUKvqJ)q9oUk<9YS*|FJQUP0>hlpy2l6{5vMBlhf?tdhx5Xo*(?R05 z0V?N&P-db0mQCd#^nd!i8;JTBQiTT$yb_roc49V|&AHDp9En)Rvb;y1sAl!HJ!c>0 z9)Gosl|}kFG-HL4m7DXl4mh`xM@rPaJ5r%5D4{EUS8ysQP~I%ytcIV@6t;3o)E>95 z62(O}iL*~`KL+X3-~TKhOr&lUlC_+=bs11a`j0QLA~=F2_@1TpmENoaQV4`4c`qK6 zoq3q6_d(3l*KJiu;Dsaw$?qx<-&$~RB~e!D^C!|v3F=&j(J=@g97u)o&89&KFO3u# z*j`(0?K+JpYnGA5Dk(m_;3Vo6$3xH3Q&2!rf{s5}(7w^Q*fA){DnRb!sULs3P%pxP4nkr~RB^^!|3< zSaK^kI<72~{|eB~+f7dr)Q5k~e-SyO1iT`aL4RK*kLJQSh)yCFXw@pHD;F|V9Rk$# z-l~&5%-{DnRA8ASn9B6%mG=+Ke{ z7SHLeE;5L}&h>0SeR7`%)O!Df3Vmk*I}v$-8f;I^x(xXCL$#}}UcYU_c;I>j&EA8d z>IDw6+^wU5bm(UHkzya(1D^sQJO|uTkulI|_TxZ3E|T)2id24&1YKISLwUCxuDpLh zb+;5o*tUE+MmhYOCONFtefx{IzGiVW_1`Igev>Fs ze32B(#7%%nKGN>)yN+rsa-_*FxA$i4bzpfytVnFKF3ruumJUO*YUL#jFJ8TOqs=AZ3O2S|`{C8cnzMtg-EPR!3Mu^M_c>^jA8^>d?K zeVSj71vQU0Qy$q`1FCq-psK_xZ9kN_00;QrBg(BPpbNk0-YD{#6R!%tYB6(WRWJ>e z@0RUYv;GexyyxTE(*i~wnJoz)NEx?GzLIiLqUzHCb16bf()EH$KJ7bYk>aCxp_D3A zC;0^UF;LJ*Sz(SA{;Gf(HXv;*f&=4m^E zX=KRp^cu3)^x7i6-uQRCBM~j5SMO1<|NOyC)9n~fpHW^?gcPZTjn3?-b4XcIUJ}R( zrBH2Y&aM!Mw$V|blC^)A*7c%F^(V3yq&55Noq9mSj~XY`=Qjnr^89;@0$QQ!niV|# znv%U256P~4Z(Pu)40rY=XJad$vu@NX#cwR=JD={;z}I~&1XAdLLhBUin=#tW7i+Lk zsju2QHQNiglzQ{dio%E{e)%x&TG;oxZV)L0<~o(8GJ1=8rvh*fY8xxVP1W=@C?Lqo(pQ_)i5l03{+UMtVK`qrh}F#GFnAk zYjf7GMVaj!ap^U~c=WFYsg-8=Z-Y^{J>`R>1epGx_hLDcZLAm#I z6x~;vn{~45$;2&{_GlhVQjX#ar081*cT{NUO9KZjTkQBI_?4?pg<+Iu)4IcHFdvn& z#|ve<;s*XJoUE!DCC1B`a<%QkcH>;kVH`a5uaW+*r^hbWtKPmIK2OH0I<~B97jO}{ z2dI0Y6eXqV>gU9Z2+sn2K7s=T&3F;&<*DPraGj?fk9z4zo+F>9R+^`&arNGry`lLv zm;`Hyya4pxnU=IE!q4-~e9cVg3!zhHsx*xI{HHg$&aw(PYZ=sOqcT*kuWPMqoie;K zw}8h~@HUQ7{*vq6rBdk%t_Ol%cA3hH(EEHXy=v*62{csci9Yo&`Ec5QdJ{u8bn0CS z9kSKE-k+!g;#;AucR<;53f%h|8Ou;zy1Ol}mtm-n`!78y&U_7}s*C2pYlS@N)^J+l z|9f$9;MzqrM4)L5)-Av|4HP_(szuWOedroWg1Gak_f=olWK#nOC)1iaWE-(@2^%=^U|NsD`UNy# zONQBv0L2Csx1NrF3|XkFNE)xNyR7D__a!vZOI^YuE;t1q^FtD0#b*X@E#!JePcq1? z595Z5iSVkGtiGkXL`=QnGZzpcr0$RCuydtTx-Li@|3=F?dF-`@QlA(3NdAF#B-@Y) zrYh1os9C_=Jk9<{Yi7Iu+?IDQZmM7BIkq?qU+1$(eO|Mz)jm{t-@nrCW(B#Bf2p98 z1bFrUuG|+GUhm>9Cg;y=)X6S3{sRnH-VMBa@v#1F1D!=fx5gRyXSKJwp68cKv@p^X zp?pIPQWfbMQt6vdKW3NU@Sa7FURH@;i*x#Jo-ZEv-*rB%RZAdPhg&JC(`ZG!evN$> zdY$V*w)Xn^)wZB>n0@wo{uh;oZ{ixZ7UfZ1ofqO|pcCcHhs!EgM%h+ijo)VAKHt@| zVeT&1;P|QMI$|txP6N?RzB1SyJp6&<+yeWRxjp{TM zA{tWUP-xI!YaMGZcK%wWG6dVbS&>6fX-QSytjHv(48gd~iX4JUORDl_MJ7pQ2*&+C X-AZT!gixv!00000NkvXXu0mjfzr>8@ literal 0 HcmV?d00001 diff --git a/presto-docs/src/main/sphinx/themes/presto/theme.conf b/presto-docs/src/main/sphinx/themes/presto/theme.conf new file mode 100644 index 00000000..c6e43bae --- /dev/null +++ b/presto-docs/src/main/sphinx/themes/presto/theme.conf @@ -0,0 +1,24 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +[theme] +inherit = haiku +stylesheet = presto.css + +[options] +full_logo = false +textcolor = #4e5665 +headingcolor = #374665 + +linkcolor = #3b5998 +visitedlinkcolor = #3b5998 +hoverlinkcolor = #3b5998 diff --git a/presto-example-http/pom.xml b/presto-example-http/pom.xml new file mode 100644 index 00000000..288a4def --- /dev/null +++ b/presto-example-http/pom.xml @@ -0,0 +1,134 @@ + + + 4.0.0 + + com.facebook.presto + presto-root + 0.107 + + + presto-example-http + Presto - Example HTTP Connector + presto-plugin + + + ${project.parent.basedir} + + + + + com.facebook.presto + presto-spi + provided + + + + io.airlift + bootstrap + provided + + + + io.airlift + json + provided + + + + io.airlift + log + provided + + + + com.fasterxml.jackson.core + jackson-annotations + provided + + + + com.fasterxml.jackson.core + jackson-core + provided + + + + com.fasterxml.jackson.core + jackson-databind + provided + + + + io.airlift + units + provided + + + + io.airlift + configuration + provided + + + + io.airlift + slice + provided + + + + com.google.guava + guava + provided + + + + javax.inject + javax.inject + provided + + + + com.google.inject + guice + provided + + + + javax.validation + validation-api + provided + + + + + org.testng + testng + test + + + + io.airlift + testing + test + + + + io.airlift + http-server + test + + + + io.airlift + node + test + + + + javax.servlet + javax.servlet-api + test + + + diff --git a/presto-example-http/src/main/java/com/facebook/presto/example/ExampleClient.java b/presto-example-http/src/main/java/com/facebook/presto/example/ExampleClient.java new file mode 100644 index 00000000..81a38745 --- /dev/null +++ b/presto-example-http/src/main/java/com/facebook/presto/example/ExampleClient.java @@ -0,0 +1,121 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.example; + +import com.google.common.base.Function; +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.io.Resources; +import io.airlift.json.JsonCodec; + +import javax.inject.Inject; + +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.Iterables.transform; +import static com.google.common.collect.Maps.transformValues; +import static com.google.common.collect.Maps.uniqueIndex; +import static java.nio.charset.StandardCharsets.UTF_8; + +public class ExampleClient +{ + /** + * SchemaName -> (TableName -> TableMetadata) + */ + private final Supplier>> schemas; + + @Inject + public ExampleClient(ExampleConfig config, JsonCodec>> catalogCodec) + throws IOException + { + checkNotNull(config, "config is null"); + checkNotNull(catalogCodec, "catalogCodec is null"); + + schemas = Suppliers.memoize(schemasSupplier(catalogCodec, config.getMetadata())); + } + + public Set getSchemaNames() + { + return schemas.get().keySet(); + } + + public Set getTableNames(String schema) + { + checkNotNull(schema, "schema is null"); + Map tables = schemas.get().get(schema); + if (tables == null) { + return ImmutableSet.of(); + } + return tables.keySet(); + } + + public ExampleTable getTable(String schema, String tableName) + { + checkNotNull(schema, "schema is null"); + checkNotNull(tableName, "tableName is null"); + Map tables = schemas.get().get(schema); + if (tables == null) { + return null; + } + return tables.get(tableName); + } + + private static Supplier>> schemasSupplier(final JsonCodec>> catalogCodec, final URI metadataUri) + { + return () -> { + try { + return lookupSchemas(metadataUri, catalogCodec); + } + catch (IOException e) { + throw Throwables.propagate(e); + } + }; + } + + private static Map> lookupSchemas(URI metadataUri, JsonCodec>> catalogCodec) + throws IOException + { + URL result = metadataUri.toURL(); + String json = Resources.toString(result, UTF_8); + Map> catalog = catalogCodec.fromJson(json); + + return ImmutableMap.copyOf(transformValues(catalog, resolveAndIndexTables(metadataUri))); + } + + private static Function, Map> resolveAndIndexTables(final URI metadataUri) + { + return tables -> { + Iterable resolvedTables = transform(tables, tableUriResolver(metadataUri)); + return ImmutableMap.copyOf(uniqueIndex(resolvedTables, ExampleTable::getName)); + }; + } + + private static Function tableUriResolver(final URI baseUri) + { + return table -> { + List sources = ImmutableList.copyOf(transform(table.getSources(), baseUri::resolve)); + return new ExampleTable(table.getName(), table.getColumns(), sources); + }; + } +} diff --git a/presto-example-http/src/main/java/com/facebook/presto/example/ExampleColumn.java b/presto-example-http/src/main/java/com/facebook/presto/example/ExampleColumn.java new file mode 100644 index 00000000..2ac9807b --- /dev/null +++ b/presto-example-http/src/main/java/com/facebook/presto/example/ExampleColumn.java @@ -0,0 +1,79 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.example; + +import com.facebook.presto.spi.type.Type; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Strings.isNullOrEmpty; + +public final class ExampleColumn +{ + private final String name; + private final Type type; + + @JsonCreator + public ExampleColumn( + @JsonProperty("name") String name, + @JsonProperty("type") Type type) + { + checkArgument(!isNullOrEmpty(name), "name is null or is empty"); + this.name = name; + this.type = checkNotNull(type, "type is null"); + } + + @JsonProperty + public String getName() + { + return name; + } + + @JsonProperty + public Type getType() + { + return type; + } + + @Override + public int hashCode() + { + return Objects.hash(name, type); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + ExampleColumn other = (ExampleColumn) obj; + return Objects.equals(this.name, other.name) && + Objects.equals(this.type, other.type); + } + + @Override + public String toString() + { + return name + ":" + type; + } +} diff --git a/presto-example-http/src/main/java/com/facebook/presto/example/ExampleColumnHandle.java b/presto-example-http/src/main/java/com/facebook/presto/example/ExampleColumnHandle.java new file mode 100644 index 00000000..027b58e7 --- /dev/null +++ b/presto-example-http/src/main/java/com/facebook/presto/example/ExampleColumnHandle.java @@ -0,0 +1,108 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.example; + +import com.facebook.presto.spi.ColumnMetadata; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.type.Type; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkNotNull; + +public final class ExampleColumnHandle + implements ColumnHandle +{ + private final String connectorId; + private final String columnName; + private final Type columnType; + private final int ordinalPosition; + + @JsonCreator + public ExampleColumnHandle( + @JsonProperty("connectorId") String connectorId, + @JsonProperty("columnName") String columnName, + @JsonProperty("columnType") Type columnType, + @JsonProperty("ordinalPosition") int ordinalPosition) + { + this.connectorId = checkNotNull(connectorId, "connectorId is null"); + this.columnName = checkNotNull(columnName, "columnName is null"); + this.columnType = checkNotNull(columnType, "columnType is null"); + this.ordinalPosition = ordinalPosition; + } + + @JsonProperty + public String getConnectorId() + { + return connectorId; + } + + @JsonProperty + public String getColumnName() + { + return columnName; + } + + @JsonProperty + public Type getColumnType() + { + return columnType; + } + + @JsonProperty + public int getOrdinalPosition() + { + return ordinalPosition; + } + + public ColumnMetadata getColumnMetadata() + { + return new ColumnMetadata(columnName, columnType, false); + } + + @Override + public int hashCode() + { + return Objects.hash(connectorId, columnName); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if ((obj == null) || (getClass() != obj.getClass())) { + return false; + } + + ExampleColumnHandle other = (ExampleColumnHandle) obj; + return Objects.equals(this.connectorId, other.connectorId) && + Objects.equals(this.columnName, other.columnName); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("connectorId", connectorId) + .add("columnName", columnName) + .add("columnType", columnType) + .add("ordinalPosition", ordinalPosition) + .toString(); + } +} diff --git a/presto-example-http/src/main/java/com/facebook/presto/example/ExampleConfig.java b/presto-example-http/src/main/java/com/facebook/presto/example/ExampleConfig.java new file mode 100644 index 00000000..e875966f --- /dev/null +++ b/presto-example-http/src/main/java/com/facebook/presto/example/ExampleConfig.java @@ -0,0 +1,38 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.example; + +import io.airlift.configuration.Config; + +import javax.validation.constraints.NotNull; + +import java.net.URI; + +public class ExampleConfig +{ + private URI metadata; + + @NotNull + public URI getMetadata() + { + return metadata; + } + + @Config("metadata-uri") + public ExampleConfig setMetadata(URI metadata) + { + this.metadata = metadata; + return this; + } +} diff --git a/presto-example-http/src/main/java/com/facebook/presto/example/ExampleConnector.java b/presto-example-http/src/main/java/com/facebook/presto/example/ExampleConnector.java new file mode 100644 index 00000000..6b7ebf4d --- /dev/null +++ b/presto-example-http/src/main/java/com/facebook/presto/example/ExampleConnector.java @@ -0,0 +1,88 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.example; + +import com.facebook.presto.spi.Connector; +import com.facebook.presto.spi.ConnectorHandleResolver; +import com.facebook.presto.spi.ConnectorMetadata; +import com.facebook.presto.spi.ConnectorRecordSetProvider; +import com.facebook.presto.spi.ConnectorSplitManager; +import io.airlift.bootstrap.LifeCycleManager; +import io.airlift.log.Logger; + +import javax.inject.Inject; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class ExampleConnector + implements Connector +{ + private static final Logger log = Logger.get(ExampleConnector.class); + + private final LifeCycleManager lifeCycleManager; + private final ExampleMetadata metadata; + private final ExampleSplitManager splitManager; + private final ExampleRecordSetProvider recordSetProvider; + private final ExampleHandleResolver handleResolver; + + @Inject + public ExampleConnector( + LifeCycleManager lifeCycleManager, + ExampleMetadata metadata, + ExampleSplitManager splitManager, + ExampleRecordSetProvider recordSetProvider, + ExampleHandleResolver handleResolver) + { + this.lifeCycleManager = checkNotNull(lifeCycleManager, "lifeCycleManager is null"); + this.metadata = checkNotNull(metadata, "metadata is null"); + this.splitManager = checkNotNull(splitManager, "splitManager is null"); + this.recordSetProvider = checkNotNull(recordSetProvider, "recordSetProvider is null"); + this.handleResolver = checkNotNull(handleResolver, "handleResolver is null"); + } + + @Override + public ConnectorMetadata getMetadata() + { + return metadata; + } + + @Override + public ConnectorSplitManager getSplitManager() + { + return splitManager; + } + + @Override + public ConnectorRecordSetProvider getRecordSetProvider() + { + return recordSetProvider; + } + + @Override + public ConnectorHandleResolver getHandleResolver() + { + return handleResolver; + } + + @Override + public final void shutdown() + { + try { + lifeCycleManager.stop(); + } + catch (Exception e) { + log.error(e, "Error shutting down connector"); + } + } +} diff --git a/presto-example-http/src/main/java/com/facebook/presto/example/ExampleConnectorFactory.java b/presto-example-http/src/main/java/com/facebook/presto/example/ExampleConnectorFactory.java new file mode 100644 index 00000000..9153e6dc --- /dev/null +++ b/presto-example-http/src/main/java/com/facebook/presto/example/ExampleConnectorFactory.java @@ -0,0 +1,72 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.example; + +import com.facebook.presto.spi.Connector; +import com.facebook.presto.spi.ConnectorFactory; +import com.facebook.presto.spi.type.TypeManager; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableMap; +import com.google.inject.Injector; +import io.airlift.bootstrap.Bootstrap; +import io.airlift.json.JsonModule; + +import java.util.Map; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class ExampleConnectorFactory + implements ConnectorFactory +{ + private final TypeManager typeManager; + private final Map optionalConfig; + + public ExampleConnectorFactory(TypeManager typeManager, Map optionalConfig) + { + this.typeManager = checkNotNull(typeManager, "typeManager is null"); + this.optionalConfig = ImmutableMap.copyOf(checkNotNull(optionalConfig, "optionalConfig is null")); + } + + @Override + public String getName() + { + return "example-http"; + } + + @Override + public Connector create(final String connectorId, Map requiredConfig) + { + checkNotNull(requiredConfig, "requiredConfig is null"); + checkNotNull(optionalConfig, "optionalConfig is null"); + + try { + // A plugin is not required to use Guice; it is just very convenient + Bootstrap app = new Bootstrap( + new JsonModule(), + new ExampleModule(connectorId, typeManager)); + + Injector injector = app + .strictConfig() + .doNotInitializeLogging() + .setRequiredConfigurationProperties(requiredConfig) + .setOptionalConfigurationProperties(optionalConfig) + .initialize(); + + return injector.getInstance(ExampleConnector.class); + } + catch (Exception e) { + throw Throwables.propagate(e); + } + } +} diff --git a/presto-example-http/src/main/java/com/facebook/presto/example/ExampleConnectorId.java b/presto-example-http/src/main/java/com/facebook/presto/example/ExampleConnectorId.java new file mode 100644 index 00000000..9b2faa1b --- /dev/null +++ b/presto-example-http/src/main/java/com/facebook/presto/example/ExampleConnectorId.java @@ -0,0 +1,54 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.example; + +import java.util.Objects; + +import static com.google.common.base.Preconditions.checkNotNull; + +public final class ExampleConnectorId +{ + private final String id; + + public ExampleConnectorId(String id) + { + this.id = checkNotNull(id, "id is null"); + } + + @Override + public String toString() + { + return id; + } + + @Override + public int hashCode() + { + return Objects.hash(id); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if ((obj == null) || (getClass() != obj.getClass())) { + return false; + } + + ExampleConnectorId other = (ExampleConnectorId) obj; + return Objects.equals(this.id, other.id); + } +} diff --git a/presto-example-http/src/main/java/com/facebook/presto/example/ExampleHandleResolver.java b/presto-example-http/src/main/java/com/facebook/presto/example/ExampleHandleResolver.java new file mode 100644 index 00000000..06700439 --- /dev/null +++ b/presto-example-http/src/main/java/com/facebook/presto/example/ExampleHandleResolver.java @@ -0,0 +1,71 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.example; + +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorHandleResolver; +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.ConnectorTableHandle; + +import javax.inject.Inject; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class ExampleHandleResolver + implements ConnectorHandleResolver +{ + private final String connectorId; + + @Inject + public ExampleHandleResolver(ExampleConnectorId clientId) + { + this.connectorId = checkNotNull(clientId, "clientId is null").toString(); + } + + @Override + public boolean canHandle(ConnectorTableHandle tableHandle) + { + return tableHandle instanceof ExampleTableHandle && ((ExampleTableHandle) tableHandle).getConnectorId().equals(connectorId); + } + + @Override + public boolean canHandle(ColumnHandle columnHandle) + { + return columnHandle instanceof ExampleColumnHandle && ((ExampleColumnHandle) columnHandle).getConnectorId().equals(connectorId); + } + + @Override + public boolean canHandle(ConnectorSplit split) + { + return split instanceof ExampleSplit && ((ExampleSplit) split).getConnectorId().equals(connectorId); + } + + @Override + public Class getTableHandleClass() + { + return ExampleTableHandle.class; + } + + @Override + public Class getColumnHandleClass() + { + return ExampleColumnHandle.class; + } + + @Override + public Class getSplitClass() + { + return ExampleSplit.class; + } +} diff --git a/presto-example-http/src/main/java/com/facebook/presto/example/ExampleMetadata.java b/presto-example-http/src/main/java/com/facebook/presto/example/ExampleMetadata.java new file mode 100644 index 00000000..b93c39a1 --- /dev/null +++ b/presto-example-http/src/main/java/com/facebook/presto/example/ExampleMetadata.java @@ -0,0 +1,178 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.example; + +import com.facebook.presto.spi.ColumnMetadata; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.ConnectorTableHandle; +import com.facebook.presto.spi.ConnectorTableMetadata; +import com.facebook.presto.spi.ReadOnlyConnectorMetadata; +import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.spi.SchemaTablePrefix; +import com.facebook.presto.spi.TableNotFoundException; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; + +import javax.inject.Inject; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.facebook.presto.example.Types.checkType; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class ExampleMetadata + extends ReadOnlyConnectorMetadata +{ + private final String connectorId; + + private final ExampleClient exampleClient; + + @Inject + public ExampleMetadata(ExampleConnectorId connectorId, ExampleClient exampleClient) + { + this.connectorId = checkNotNull(connectorId, "connectorId is null").toString(); + this.exampleClient = checkNotNull(exampleClient, "client is null"); + } + + @Override + public List listSchemaNames(ConnectorSession session) + { + return listSchemaNames(); + } + + public List listSchemaNames() + { + return ImmutableList.copyOf(exampleClient.getSchemaNames()); + } + + @Override + public ExampleTableHandle getTableHandle(ConnectorSession session, SchemaTableName tableName) + { + if (!listSchemaNames(session).contains(tableName.getSchemaName())) { + return null; + } + + ExampleTable table = exampleClient.getTable(tableName.getSchemaName(), tableName.getTableName()); + if (table == null) { + return null; + } + + return new ExampleTableHandle(connectorId, tableName.getSchemaName(), tableName.getTableName()); + } + + @Override + public ConnectorTableMetadata getTableMetadata(ConnectorTableHandle table) + { + ExampleTableHandle exampleTableHandle = checkType(table, ExampleTableHandle.class, "table"); + checkArgument(exampleTableHandle.getConnectorId().equals(connectorId), "tableHandle is not for this connector"); + SchemaTableName tableName = new SchemaTableName(exampleTableHandle.getSchemaName(), exampleTableHandle.getTableName()); + + return getTableMetadata(tableName); + } + + @Override + public List listTables(ConnectorSession session, String schemaNameOrNull) + { + Set schemaNames; + if (schemaNameOrNull != null) { + schemaNames = ImmutableSet.of(schemaNameOrNull); + } + else { + schemaNames = exampleClient.getSchemaNames(); + } + + ImmutableList.Builder builder = ImmutableList.builder(); + for (String schemaName : schemaNames) { + for (String tableName : exampleClient.getTableNames(schemaName)) { + builder.add(new SchemaTableName(schemaName, tableName)); + } + } + return builder.build(); + } + + @Override + public ColumnHandle getSampleWeightColumnHandle(ConnectorTableHandle tableHandle) + { + return null; + } + + @Override + public Map getColumnHandles(ConnectorTableHandle tableHandle) + { + ExampleTableHandle exampleTableHandle = checkType(tableHandle, ExampleTableHandle.class, "tableHandle"); + checkArgument(exampleTableHandle.getConnectorId().equals(connectorId), "tableHandle is not for this connector"); + + ExampleTable table = exampleClient.getTable(exampleTableHandle.getSchemaName(), exampleTableHandle.getTableName()); + if (table == null) { + throw new TableNotFoundException(exampleTableHandle.toSchemaTableName()); + } + + ImmutableMap.Builder columnHandles = ImmutableMap.builder(); + int index = 0; + for (ColumnMetadata column : table.getColumnsMetadata()) { + columnHandles.put(column.getName(), new ExampleColumnHandle(connectorId, column.getName(), column.getType(), index)); + index++; + } + return columnHandles.build(); + } + + @Override + public Map> listTableColumns(ConnectorSession session, SchemaTablePrefix prefix) + { + checkNotNull(prefix, "prefix is null"); + ImmutableMap.Builder> columns = ImmutableMap.builder(); + for (SchemaTableName tableName : listTables(session, prefix)) { + ConnectorTableMetadata tableMetadata = getTableMetadata(tableName); + // table can disappear during listing operation + if (tableMetadata != null) { + columns.put(tableName, tableMetadata.getColumns()); + } + } + return columns.build(); + } + + private ConnectorTableMetadata getTableMetadata(SchemaTableName tableName) + { + if (!listSchemaNames().contains(tableName.getSchemaName())) { + return null; + } + + ExampleTable table = exampleClient.getTable(tableName.getSchemaName(), tableName.getTableName()); + if (table == null) { + return null; + } + + return new ConnectorTableMetadata(tableName, table.getColumnsMetadata()); + } + + private List listTables(ConnectorSession session, SchemaTablePrefix prefix) + { + if (prefix.getSchemaName() == null) { + return listTables(session, prefix.getSchemaName()); + } + return ImmutableList.of(new SchemaTableName(prefix.getSchemaName(), prefix.getTableName())); + } + + @Override + public ColumnMetadata getColumnMetadata(ConnectorTableHandle tableHandle, ColumnHandle columnHandle) + { + checkType(tableHandle, ExampleTableHandle.class, "tableHandle"); + return checkType(columnHandle, ExampleColumnHandle.class, "columnHandle").getColumnMetadata(); + } +} diff --git a/presto-example-http/src/main/java/com/facebook/presto/example/ExampleModule.java b/presto-example-http/src/main/java/com/facebook/presto/example/ExampleModule.java new file mode 100644 index 00000000..0d8c4d15 --- /dev/null +++ b/presto-example-http/src/main/java/com/facebook/presto/example/ExampleModule.java @@ -0,0 +1,84 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.example; + +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.deser.std.FromStringDeserializer; +import com.google.inject.Binder; +import com.google.inject.Module; +import com.google.inject.Scopes; + +import javax.inject.Inject; + +import static com.facebook.presto.spi.type.TypeSignature.parseTypeSignature; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.airlift.configuration.ConfigBinder.configBinder; +import static io.airlift.json.JsonBinder.jsonBinder; +import static io.airlift.json.JsonCodec.listJsonCodec; +import static io.airlift.json.JsonCodecBinder.jsonCodecBinder; + +public class ExampleModule + implements Module +{ + private final String connectorId; + private final TypeManager typeManager; + + public ExampleModule(String connectorId, TypeManager typeManager) + { + this.connectorId = checkNotNull(connectorId, "connector id is null"); + this.typeManager = checkNotNull(typeManager, "typeManager is null"); + } + + @Override + public void configure(Binder binder) + { + binder.bind(TypeManager.class).toInstance(typeManager); + + binder.bind(ExampleConnector.class).in(Scopes.SINGLETON); + binder.bind(ExampleConnectorId.class).toInstance(new ExampleConnectorId(connectorId)); + binder.bind(ExampleMetadata.class).in(Scopes.SINGLETON); + binder.bind(ExampleClient.class).in(Scopes.SINGLETON); + binder.bind(ExampleSplitManager.class).in(Scopes.SINGLETON); + binder.bind(ExampleRecordSetProvider.class).in(Scopes.SINGLETON); + binder.bind(ExampleHandleResolver.class).in(Scopes.SINGLETON); + configBinder(binder).bindConfig(ExampleConfig.class); + + jsonBinder(binder).addDeserializerBinding(Type.class).to(TypeDeserializer.class); + jsonCodecBinder(binder).bindMapJsonCodec(String.class, listJsonCodec(ExampleTable.class)); + } + + public static final class TypeDeserializer + extends FromStringDeserializer + { + private final TypeManager typeManager; + + @Inject + public TypeDeserializer(TypeManager typeManager) + { + super(Type.class); + this.typeManager = checkNotNull(typeManager, "typeManager is null"); + } + + @Override + protected Type _deserialize(String value, DeserializationContext context) + { + Type type = typeManager.getType(parseTypeSignature(value)); + checkArgument(type != null, "Unknown type %s", value); + return type; + } + } +} diff --git a/presto-example-http/src/main/java/com/facebook/presto/example/ExamplePartition.java b/presto-example-http/src/main/java/com/facebook/presto/example/ExamplePartition.java new file mode 100644 index 00000000..9303a95d --- /dev/null +++ b/presto-example-http/src/main/java/com/facebook/presto/example/ExamplePartition.java @@ -0,0 +1,65 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.example; + +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorPartition; +import com.facebook.presto.spi.TupleDomain; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkNotNull; + +public class ExamplePartition + implements ConnectorPartition +{ + private final String schemaName; + private final String tableName; + + public ExamplePartition(String schemaName, String tableName) + { + this.schemaName = checkNotNull(schemaName, "schema name is null"); + this.tableName = checkNotNull(tableName, "table name is null"); + } + + @Override + public String getPartitionId() + { + return schemaName + ":" + tableName; + } + + public String getSchemaName() + { + return schemaName; + } + + public String getTableName() + { + return tableName; + } + + @Override + public TupleDomain getTupleDomain() + { + return TupleDomain.all(); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("schemaName", schemaName) + .add("tableName", tableName) + .toString(); + } +} diff --git a/presto-example-http/src/main/java/com/facebook/presto/example/ExamplePlugin.java b/presto-example-http/src/main/java/com/facebook/presto/example/ExamplePlugin.java new file mode 100644 index 00000000..03bc6f7a --- /dev/null +++ b/presto-example-http/src/main/java/com/facebook/presto/example/ExamplePlugin.java @@ -0,0 +1,60 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.example; + +import com.facebook.presto.spi.ConnectorFactory; +import com.facebook.presto.spi.Plugin; +import com.facebook.presto.spi.type.TypeManager; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +import javax.inject.Inject; + +import java.util.List; +import java.util.Map; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class ExamplePlugin + implements Plugin +{ + private TypeManager typeManager; + private Map optionalConfig = ImmutableMap.of(); + + @Override + public synchronized void setOptionalConfig(Map optionalConfig) + { + this.optionalConfig = ImmutableMap.copyOf(checkNotNull(optionalConfig, "optionalConfig is null")); + } + + @Inject + public synchronized void setTypeManager(TypeManager typeManager) + { + this.typeManager = typeManager; + } + + public synchronized Map getOptionalConfig() + { + return optionalConfig; + } + + @Override + public synchronized List getServices(Class type) + { + if (type == ConnectorFactory.class) { + return ImmutableList.of(type.cast(new ExampleConnectorFactory(typeManager, getOptionalConfig()))); + } + return ImmutableList.of(); + } +} diff --git a/presto-example-http/src/main/java/com/facebook/presto/example/ExampleRecordCursor.java b/presto-example-http/src/main/java/com/facebook/presto/example/ExampleRecordCursor.java new file mode 100644 index 00000000..e34f1fb7 --- /dev/null +++ b/presto-example-http/src/main/java/com/facebook/presto/example/ExampleRecordCursor.java @@ -0,0 +1,160 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.example; + +import com.facebook.presto.spi.RecordCursor; +import com.facebook.presto.spi.type.Type; +import com.google.common.base.Splitter; +import com.google.common.base.Strings; +import com.google.common.base.Throwables; +import com.google.common.io.ByteSource; +import com.google.common.io.CountingInputStream; +import io.airlift.slice.Slice; +import io.airlift.slice.Slices; + +import java.io.IOException; +import java.util.Iterator; +import java.util.List; + +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.BooleanType.BOOLEAN; +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; +import static java.nio.charset.StandardCharsets.UTF_8; + +public class ExampleRecordCursor + implements RecordCursor +{ + private static final Splitter LINE_SPLITTER = Splitter.on(",").trimResults(); + + private final List columnHandles; + private final int[] fieldToColumnIndex; + + private final Iterator lines; + private final long totalBytes; + + private List fields; + + public ExampleRecordCursor(List columnHandles, ByteSource byteSource) + { + this.columnHandles = columnHandles; + + fieldToColumnIndex = new int[columnHandles.size()]; + for (int i = 0; i < columnHandles.size(); i++) { + ExampleColumnHandle columnHandle = columnHandles.get(i); + fieldToColumnIndex[i] = columnHandle.getOrdinalPosition(); + } + + try (CountingInputStream input = new CountingInputStream(byteSource.openStream())) { + lines = byteSource.asCharSource(UTF_8).readLines().iterator(); + totalBytes = input.getCount(); + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } + + @Override + public long getTotalBytes() + { + return totalBytes; + } + + @Override + public long getCompletedBytes() + { + return totalBytes; + } + + @Override + public long getReadTimeNanos() + { + return 0; + } + + @Override + public Type getType(int field) + { + checkArgument(field < columnHandles.size(), "Invalid field index"); + return columnHandles.get(field).getColumnType(); + } + + @Override + public boolean advanceNextPosition() + { + if (!lines.hasNext()) { + return false; + } + String line = lines.next(); + fields = LINE_SPLITTER.splitToList(line); + + return true; + } + + private String getFieldValue(int field) + { + checkState(fields != null, "Cursor has not been advanced yet"); + + int columnIndex = fieldToColumnIndex[field]; + return fields.get(columnIndex); + } + + @Override + public boolean getBoolean(int field) + { + checkFieldType(field, BOOLEAN); + return Boolean.parseBoolean(getFieldValue(field)); + } + + @Override + public long getLong(int field) + { + checkFieldType(field, BIGINT); + return Long.parseLong(getFieldValue(field)); + } + + @Override + public double getDouble(int field) + { + checkFieldType(field, DOUBLE); + return Double.parseDouble(getFieldValue(field)); + } + + @Override + public Slice getSlice(int field) + { + checkFieldType(field, VARCHAR); + return Slices.utf8Slice(getFieldValue(field)); + } + + @Override + public boolean isNull(int field) + { + checkArgument(field < columnHandles.size(), "Invalid field index"); + return Strings.isNullOrEmpty(getFieldValue(field)); + } + + private void checkFieldType(int field, Type expected) + { + Type actual = getType(field); + checkArgument(actual.equals(expected), "Expected field %s to be type %s but is %s", field, expected, actual); + } + + @Override + public void close() + { + } +} diff --git a/presto-example-http/src/main/java/com/facebook/presto/example/ExampleRecordSet.java b/presto-example-http/src/main/java/com/facebook/presto/example/ExampleRecordSet.java new file mode 100644 index 00000000..46b7f7d5 --- /dev/null +++ b/presto-example-http/src/main/java/com/facebook/presto/example/ExampleRecordSet.java @@ -0,0 +1,66 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.example; + +import com.facebook.presto.spi.RecordCursor; +import com.facebook.presto.spi.RecordSet; +import com.facebook.presto.spi.type.Type; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.common.io.ByteSource; +import com.google.common.io.Resources; + +import java.net.MalformedURLException; +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class ExampleRecordSet + implements RecordSet +{ + private final List columnHandles; + private final List columnTypes; + private final ByteSource byteSource; + + public ExampleRecordSet(ExampleSplit split, List columnHandles) + { + checkNotNull(split, "split is null"); + + this.columnHandles = checkNotNull(columnHandles, "column handles is null"); + ImmutableList.Builder types = ImmutableList.builder(); + for (ExampleColumnHandle column : columnHandles) { + types.add(column.getColumnType()); + } + this.columnTypes = types.build(); + + try { + byteSource = Resources.asByteSource(split.getUri().toURL()); + } + catch (MalformedURLException e) { + throw Throwables.propagate(e); + } + } + + @Override + public List getColumnTypes() + { + return columnTypes; + } + + @Override + public RecordCursor cursor() + { + return new ExampleRecordCursor(columnHandles, byteSource); + } +} diff --git a/presto-example-http/src/main/java/com/facebook/presto/example/ExampleRecordSetProvider.java b/presto-example-http/src/main/java/com/facebook/presto/example/ExampleRecordSetProvider.java new file mode 100644 index 00000000..166e6503 --- /dev/null +++ b/presto-example-http/src/main/java/com/facebook/presto/example/ExampleRecordSetProvider.java @@ -0,0 +1,55 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.example; + +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorRecordSetProvider; +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.RecordSet; +import com.google.common.collect.ImmutableList; + +import javax.inject.Inject; + +import java.util.List; + +import static com.facebook.presto.example.Types.checkType; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class ExampleRecordSetProvider + implements ConnectorRecordSetProvider +{ + private final String connectorId; + + @Inject + public ExampleRecordSetProvider(ExampleConnectorId connectorId) + { + this.connectorId = checkNotNull(connectorId, "connectorId is null").toString(); + } + + @Override + public RecordSet getRecordSet(ConnectorSplit split, List columns) + { + checkNotNull(split, "partitionChunk is null"); + ExampleSplit exampleSplit = checkType(split, ExampleSplit.class, "split"); + checkArgument(exampleSplit.getConnectorId().equals(connectorId), "split is not for this connector"); + + ImmutableList.Builder handles = ImmutableList.builder(); + for (ColumnHandle handle : columns) { + handles.add(checkType(handle, ExampleColumnHandle.class, "handle")); + } + + return new ExampleRecordSet(exampleSplit, handles.build()); + } +} diff --git a/presto-example-http/src/main/java/com/facebook/presto/example/ExampleSplit.java b/presto-example-http/src/main/java/com/facebook/presto/example/ExampleSplit.java new file mode 100644 index 00000000..10f7c091 --- /dev/null +++ b/presto-example-http/src/main/java/com/facebook/presto/example/ExampleSplit.java @@ -0,0 +1,96 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.example; + +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.HostAddress; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; + +import java.net.URI; +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class ExampleSplit + implements ConnectorSplit +{ + private final String connectorId; + private final String schemaName; + private final String tableName; + private final URI uri; + private final boolean remotelyAccessible; + private final ImmutableList addresses; + + @JsonCreator + public ExampleSplit( + @JsonProperty("connectorId") String connectorId, + @JsonProperty("schemaName") String schemaName, + @JsonProperty("tableName") String tableName, + @JsonProperty("uri") URI uri) + { + this.schemaName = checkNotNull(schemaName, "schema name is null"); + this.connectorId = checkNotNull(connectorId, "connector id is null"); + this.tableName = checkNotNull(tableName, "table name is null"); + this.uri = checkNotNull(uri, "uri is null"); + +// if ("http".equalsIgnoreCase(uri.getScheme()) || "https".equalsIgnoreCase(uri.getScheme())) { + remotelyAccessible = true; + addresses = ImmutableList.of(HostAddress.fromUri(uri)); + } + + @JsonProperty + public String getConnectorId() + { + return connectorId; + } + + @JsonProperty + public String getSchemaName() + { + return schemaName; + } + + @JsonProperty + public String getTableName() + { + return tableName; + } + + @JsonProperty + public URI getUri() + { + return uri; + } + + @Override + public boolean isRemotelyAccessible() + { + // only http or https is remotely accessible + return remotelyAccessible; + } + + @Override + public List getAddresses() + { + return addresses; + } + + @Override + public Object getInfo() + { + return this; + } +} diff --git a/presto-example-http/src/main/java/com/facebook/presto/example/ExampleSplitManager.java b/presto-example-http/src/main/java/com/facebook/presto/example/ExampleSplitManager.java new file mode 100644 index 00000000..303de27e --- /dev/null +++ b/presto-example-http/src/main/java/com/facebook/presto/example/ExampleSplitManager.java @@ -0,0 +1,85 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.example; + +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorPartition; +import com.facebook.presto.spi.ConnectorPartitionResult; +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.ConnectorSplitManager; +import com.facebook.presto.spi.ConnectorSplitSource; +import com.facebook.presto.spi.ConnectorTableHandle; +import com.facebook.presto.spi.FixedSplitSource; +import com.facebook.presto.spi.TupleDomain; +import com.google.common.collect.ImmutableList; + +import javax.inject.Inject; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static com.facebook.presto.example.Types.checkType; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +public class ExampleSplitManager + implements ConnectorSplitManager +{ + private final String connectorId; + private final ExampleClient exampleClient; + + @Inject + public ExampleSplitManager(ExampleConnectorId connectorId, ExampleClient exampleClient) + { + this.connectorId = checkNotNull(connectorId, "connectorId is null").toString(); + this.exampleClient = checkNotNull(exampleClient, "client is null"); + } + + @Override + public ConnectorPartitionResult getPartitions(ConnectorTableHandle tableHandle, TupleDomain tupleDomain) + { + ExampleTableHandle exampleTableHandle = checkType(tableHandle, ExampleTableHandle.class, "tableHandle"); + + // example connector has only one partition + List partitions = ImmutableList.of(new ExamplePartition(exampleTableHandle.getSchemaName(), exampleTableHandle.getTableName())); + // example connector does not do any additional processing/filtering with the TupleDomain, so just return the whole TupleDomain + return new ConnectorPartitionResult(partitions, tupleDomain); + } + + @Override + public ConnectorSplitSource getPartitionSplits(ConnectorTableHandle tableHandle, List partitions) + { + checkNotNull(partitions, "partitions is null"); + checkArgument(partitions.size() == 1, "Expected one partition but got %s", partitions.size()); + ConnectorPartition partition = partitions.get(0); + + ExamplePartition examplePartition = checkType(partition, ExamplePartition.class, "partition"); + + ExampleTableHandle exampleTableHandle = (ExampleTableHandle) tableHandle; + ExampleTable table = exampleClient.getTable(exampleTableHandle.getSchemaName(), exampleTableHandle.getTableName()); + // this can happen if table is removed during a query + checkState(table != null, "Table %s.%s no longer exists", exampleTableHandle.getSchemaName(), exampleTableHandle.getTableName()); + + List splits = new ArrayList<>(); + for (URI uri : table.getSources()) { + splits.add(new ExampleSplit(connectorId, examplePartition.getSchemaName(), examplePartition.getTableName(), uri)); + } + Collections.shuffle(splits); + + return new FixedSplitSource(connectorId, splits); + } +} diff --git a/presto-example-http/src/main/java/com/facebook/presto/example/ExampleTable.java b/presto-example-http/src/main/java/com/facebook/presto/example/ExampleTable.java new file mode 100644 index 00000000..bf984ebf --- /dev/null +++ b/presto-example-http/src/main/java/com/facebook/presto/example/ExampleTable.java @@ -0,0 +1,75 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.example; + +import com.facebook.presto.spi.ColumnMetadata; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; + +import java.net.URI; +import java.util.List; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Strings.isNullOrEmpty; + +public class ExampleTable +{ + private final String name; + private final List columns; + private final List columnsMetadata; + private final List sources; + + @JsonCreator + public ExampleTable( + @JsonProperty("name") String name, + @JsonProperty("columns") List columns, + @JsonProperty("sources") List sources) + { + checkArgument(!isNullOrEmpty(name), "name is null or is empty"); + this.name = checkNotNull(name, "name is null"); + this.columns = ImmutableList.copyOf(checkNotNull(columns, "columns is null")); + this.sources = ImmutableList.copyOf(checkNotNull(sources, "sources is null")); + + ImmutableList.Builder columnsMetadata = ImmutableList.builder(); + for (ExampleColumn column : this.columns) { + columnsMetadata.add(new ColumnMetadata(column.getName(), column.getType(), false)); + } + this.columnsMetadata = columnsMetadata.build(); + } + + @JsonProperty + public String getName() + { + return name; + } + + @JsonProperty + public List getColumns() + { + return columns; + } + + @JsonProperty + public List getSources() + { + return sources; + } + + public List getColumnsMetadata() + { + return columnsMetadata; + } +} diff --git a/presto-example-http/src/main/java/com/facebook/presto/example/ExampleTableHandle.java b/presto-example-http/src/main/java/com/facebook/presto/example/ExampleTableHandle.java new file mode 100644 index 00000000..e80627d2 --- /dev/null +++ b/presto-example-http/src/main/java/com/facebook/presto/example/ExampleTableHandle.java @@ -0,0 +1,94 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.example; + +import com.facebook.presto.spi.ConnectorTableHandle; +import com.facebook.presto.spi.SchemaTableName; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Joiner; + +import java.util.Objects; + +import static com.google.common.base.Preconditions.checkNotNull; + +public final class ExampleTableHandle + implements ConnectorTableHandle +{ + private final String connectorId; + private final String schemaName; + private final String tableName; + + @JsonCreator + public ExampleTableHandle( + @JsonProperty("connectorId") String connectorId, + @JsonProperty("schemaName") String schemaName, + @JsonProperty("tableName") String tableName) + { + this.connectorId = checkNotNull(connectorId, "connectorId is null"); + this.schemaName = checkNotNull(schemaName, "schemaName is null"); + this.tableName = checkNotNull(tableName, "tableName is null"); + } + + @JsonProperty + public String getConnectorId() + { + return connectorId; + } + + @JsonProperty + public String getSchemaName() + { + return schemaName; + } + + @JsonProperty + public String getTableName() + { + return tableName; + } + + public SchemaTableName toSchemaTableName() + { + return new SchemaTableName(schemaName, tableName); + } + + @Override + public int hashCode() + { + return Objects.hash(connectorId, schemaName, tableName); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if ((obj == null) || (getClass() != obj.getClass())) { + return false; + } + + ExampleTableHandle other = (ExampleTableHandle) obj; + return Objects.equals(this.connectorId, other.connectorId) && + Objects.equals(this.schemaName, other.schemaName) && + Objects.equals(this.tableName, other.tableName); + } + + @Override + public String toString() + { + return Joiner.on(":").join(connectorId, schemaName, tableName); + } +} diff --git a/presto-example-http/src/main/java/com/facebook/presto/example/Types.java b/presto-example-http/src/main/java/com/facebook/presto/example/Types.java new file mode 100644 index 00000000..0092d21e --- /dev/null +++ b/presto-example-http/src/main/java/com/facebook/presto/example/Types.java @@ -0,0 +1,33 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.example; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +final class Types +{ + private Types() {} + + public static B checkType(A value, Class target, String name) + { + checkNotNull(value, "%s is null", name); + checkArgument(target.isInstance(value), + "%s must be of type %s, not %s", + name, + target.getName(), + value.getClass().getName()); + return target.cast(value); + } +} diff --git a/presto-example-http/src/test/java/com/facebook/presto/example/ExampleHttpServer.java b/presto-example-http/src/test/java/com/facebook/presto/example/ExampleHttpServer.java new file mode 100644 index 00000000..b9b51c9a --- /dev/null +++ b/presto-example-http/src/test/java/com/facebook/presto/example/ExampleHttpServer.java @@ -0,0 +1,94 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.example; + +import com.google.common.collect.ImmutableMap; +import com.google.common.io.Resources; +import com.google.inject.Binder; +import com.google.inject.Injector; +import com.google.inject.Module; +import com.google.inject.TypeLiteral; +import io.airlift.bootstrap.Bootstrap; +import io.airlift.bootstrap.LifeCycleManager; +import io.airlift.http.server.TheServlet; +import io.airlift.http.server.testing.TestingHttpServer; +import io.airlift.http.server.testing.TestingHttpServerModule; +import io.airlift.node.testing.TestingNodeModule; + +import javax.servlet.Servlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.util.Map; + +public class ExampleHttpServer +{ + private final LifeCycleManager lifeCycleManager; + private final URI baseUri; + + public ExampleHttpServer() + throws Exception + { + Bootstrap app = new Bootstrap( + new TestingNodeModule(), + new TestingHttpServerModule(), + new ExampleHttpServerModule()); + + Injector injector = app + .strictConfig() + .doNotInitializeLogging() + .initialize(); + + lifeCycleManager = injector.getInstance(LifeCycleManager.class); + baseUri = injector.getInstance(TestingHttpServer.class).getBaseUrl(); + } + + public void stop() + throws Exception + { + lifeCycleManager.stop(); + } + + public URI resolve(String s) + { + return baseUri.resolve(s); + } + + private static class ExampleHttpServerModule + implements Module + { + @Override + public void configure(Binder binder) + { + binder.bind(new TypeLiteral>() {}).annotatedWith(TheServlet.class).toInstance(ImmutableMap.of()); + binder.bind(Servlet.class).annotatedWith(TheServlet.class).toInstance(new ExampleHttpServlet()); + } + } + + private static class ExampleHttpServlet + extends HttpServlet + { + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws IOException + { + URL dataUrl = Resources.getResource(TestExampleClient.class, request.getPathInfo()); + Resources.asByteSource(dataUrl).copyTo(response.getOutputStream()); + } + } +} diff --git a/presto-example-http/src/test/java/com/facebook/presto/example/MetadataUtil.java b/presto-example-http/src/test/java/com/facebook/presto/example/MetadataUtil.java new file mode 100644 index 00000000..5922b0f7 --- /dev/null +++ b/presto-example-http/src/test/java/com/facebook/presto/example/MetadataUtil.java @@ -0,0 +1,79 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.example; + +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.deser.std.FromStringDeserializer; +import com.google.common.collect.ImmutableMap; +import io.airlift.json.JsonCodec; +import io.airlift.json.JsonCodecFactory; +import io.airlift.json.ObjectMapperProvider; + +import java.util.List; +import java.util.Map; + +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.BooleanType.BOOLEAN; +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static io.airlift.json.JsonCodec.listJsonCodec; +import static java.util.Locale.ENGLISH; + +public final class MetadataUtil +{ + private MetadataUtil() + { + } + + public static final JsonCodec>> CATALOG_CODEC; + public static final JsonCodec TABLE_CODEC; + public static final JsonCodec COLUMN_CODEC; + + static { + ObjectMapperProvider objectMapperProvider = new ObjectMapperProvider(); + objectMapperProvider.setJsonDeserializers(ImmutableMap., JsonDeserializer>of(Type.class, new TestingTypeDeserializer())); + JsonCodecFactory codecFactory = new JsonCodecFactory(objectMapperProvider); + CATALOG_CODEC = codecFactory.mapJsonCodec(String.class, listJsonCodec(ExampleTable.class)); + TABLE_CODEC = codecFactory.jsonCodec(ExampleTable.class); + COLUMN_CODEC = codecFactory.jsonCodec(ExampleColumnHandle.class); + } + + public static final class TestingTypeDeserializer + extends FromStringDeserializer + { + private final Map types = ImmutableMap.of( + StandardTypes.BOOLEAN, BOOLEAN, + StandardTypes.BIGINT, BIGINT, + StandardTypes.DOUBLE, DOUBLE, + StandardTypes.VARCHAR, VARCHAR); + + public TestingTypeDeserializer() + { + super(Type.class); + } + + @Override + protected Type _deserialize(String value, DeserializationContext context) + { + Type type = types.get(value.toLowerCase(ENGLISH)); + if (type == null) { + throw new IllegalArgumentException(String.valueOf("Unknown type " + value)); + } + return type; + } + } +} diff --git a/presto-example-http/src/test/java/com/facebook/presto/example/TestExampleClient.java b/presto-example-http/src/test/java/com/facebook/presto/example/TestExampleClient.java new file mode 100644 index 00000000..58a93aaa --- /dev/null +++ b/presto-example-http/src/test/java/com/facebook/presto/example/TestExampleClient.java @@ -0,0 +1,50 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.example; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.io.Resources; +import org.testng.annotations.Test; + +import java.net.URI; +import java.net.URL; + +import static com.facebook.presto.example.MetadataUtil.CATALOG_CODEC; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +public class TestExampleClient +{ + @Test + public void testMetadata() + throws Exception + { + URL metadataUrl = Resources.getResource(TestExampleClient.class, "/example-data/example-metadata.json"); + assertNotNull(metadataUrl, "metadataUrl is null"); + URI metadata = metadataUrl.toURI(); + ExampleClient client = new ExampleClient(new ExampleConfig().setMetadata(metadata), CATALOG_CODEC); + assertEquals(client.getSchemaNames(), ImmutableSet.of("example", "tpch")); + assertEquals(client.getTableNames("example"), ImmutableSet.of("numbers")); + assertEquals(client.getTableNames("tpch"), ImmutableSet.of("orders", "lineitem")); + + ExampleTable table = client.getTable("example", "numbers"); + assertNotNull(table, "table is null"); + assertEquals(table.getName(), "numbers"); + assertEquals(table.getColumns(), ImmutableList.of(new ExampleColumn("text", VARCHAR), new ExampleColumn("value", BIGINT))); + assertEquals(table.getSources(), ImmutableList.of(metadata.resolve("numbers-1.csv"), metadata.resolve("numbers-2.csv"))); + } +} diff --git a/presto-example-http/src/test/java/com/facebook/presto/example/TestExampleColumnHandle.java b/presto-example-http/src/test/java/com/facebook/presto/example/TestExampleColumnHandle.java new file mode 100644 index 00000000..f91e9a6b --- /dev/null +++ b/presto-example-http/src/test/java/com/facebook/presto/example/TestExampleColumnHandle.java @@ -0,0 +1,57 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.example; + +import io.airlift.testing.EquivalenceTester; +import org.testng.annotations.Test; + +import static com.facebook.presto.example.MetadataUtil.COLUMN_CODEC; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static org.testng.Assert.assertEquals; + +public class TestExampleColumnHandle +{ + private final ExampleColumnHandle columnHandle = new ExampleColumnHandle("connectorId", "columnName", VARCHAR, 0); + + @Test + public void testJsonRoundTrip() + { + String json = COLUMN_CODEC.toJson(columnHandle); + ExampleColumnHandle copy = COLUMN_CODEC.fromJson(json); + assertEquals(copy, columnHandle); + } + + @Test + public void testEquivalence() + { + EquivalenceTester.equivalenceTester() + .addEquivalentGroup( + new ExampleColumnHandle("connectorId", "columnName", VARCHAR, 0), + new ExampleColumnHandle("connectorId", "columnName", VARCHAR, 0), + new ExampleColumnHandle("connectorId", "columnName", BIGINT, 0), + new ExampleColumnHandle("connectorId", "columnName", VARCHAR, 1)) + .addEquivalentGroup( + new ExampleColumnHandle("connectorIdX", "columnName", VARCHAR, 0), + new ExampleColumnHandle("connectorIdX", "columnName", VARCHAR, 0), + new ExampleColumnHandle("connectorIdX", "columnName", BIGINT, 0), + new ExampleColumnHandle("connectorIdX", "columnName", VARCHAR, 1)) + .addEquivalentGroup( + new ExampleColumnHandle("connectorId", "columnNameX", VARCHAR, 0), + new ExampleColumnHandle("connectorId", "columnNameX", VARCHAR, 0), + new ExampleColumnHandle("connectorId", "columnNameX", BIGINT, 0), + new ExampleColumnHandle("connectorId", "columnNameX", VARCHAR, 1)) + .check(); + } +} diff --git a/presto-example-http/src/test/java/com/facebook/presto/example/TestExampleConfig.java b/presto-example-http/src/test/java/com/facebook/presto/example/TestExampleConfig.java new file mode 100644 index 00000000..aceee8b1 --- /dev/null +++ b/presto-example-http/src/test/java/com/facebook/presto/example/TestExampleConfig.java @@ -0,0 +1,44 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.example; + +import com.google.common.collect.ImmutableMap; +import io.airlift.configuration.testing.ConfigAssertions; +import org.testng.annotations.Test; + +import java.net.URI; +import java.util.Map; + +public class TestExampleConfig +{ + @Test + public void testDefaults() + { + ConfigAssertions.assertRecordedDefaults(ConfigAssertions.recordDefaults(ExampleConfig.class) + .setMetadata(null)); + } + + @Test + public void testExplicitPropertyMappings() + { + Map properties = new ImmutableMap.Builder() + .put("metadata-uri", "file://test.json") + .build(); + + ExampleConfig expected = new ExampleConfig() + .setMetadata(URI.create("file://test.json")); + + ConfigAssertions.assertFullMapping(properties, expected); + } +} diff --git a/presto-example-http/src/test/java/com/facebook/presto/example/TestExampleMetadata.java b/presto-example-http/src/test/java/com/facebook/presto/example/TestExampleMetadata.java new file mode 100644 index 00000000..4157de5e --- /dev/null +++ b/presto-example-http/src/test/java/com/facebook/presto/example/TestExampleMetadata.java @@ -0,0 +1,162 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.example; + +import com.facebook.presto.spi.ColumnMetadata; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.ConnectorTableMetadata; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.spi.TableNotFoundException; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.io.Resources; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.net.URI; +import java.net.URL; + +import static com.facebook.presto.example.MetadataUtil.CATALOG_CODEC; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.TimeZoneKey.UTC_KEY; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static java.util.Locale.ENGLISH; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.fail; + +@Test(singleThreaded = true) +public class TestExampleMetadata +{ + private static final ConnectorSession SESSION = new ConnectorSession("user", UTC_KEY, ENGLISH, System.currentTimeMillis(), null); + private static final String CONNECTOR_ID = "TEST"; + private static final ExampleTableHandle NUMBERS_TABLE_HANDLE = new ExampleTableHandle(CONNECTOR_ID, "example", "numbers"); + private ExampleMetadata metadata; + private URI metadataUri; + + @BeforeMethod + public void setUp() + throws Exception + { + URL metadataUrl = Resources.getResource(TestExampleClient.class, "/example-data/example-metadata.json"); + assertNotNull(metadataUrl, "metadataUrl is null"); + metadataUri = metadataUrl.toURI(); + ExampleClient client = new ExampleClient(new ExampleConfig().setMetadata(metadataUri), CATALOG_CODEC); + metadata = new ExampleMetadata(new ExampleConnectorId(CONNECTOR_ID), client); + } + + @Test + public void testListSchemaNames() + { + assertEquals(metadata.listSchemaNames(SESSION), ImmutableSet.of("example", "tpch")); + } + + @Test + public void testGetTableHandle() + { + assertEquals(metadata.getTableHandle(SESSION, new SchemaTableName("example", "numbers")), NUMBERS_TABLE_HANDLE); + assertNull(metadata.getTableHandle(SESSION, new SchemaTableName("example", "unknown"))); + assertNull(metadata.getTableHandle(SESSION, new SchemaTableName("unknown", "numbers"))); + assertNull(metadata.getTableHandle(SESSION, new SchemaTableName("unknown", "unknown"))); + } + + @Test + public void testGetColumnHandles() + { + // known table + assertEquals(metadata.getColumnHandles(NUMBERS_TABLE_HANDLE), ImmutableMap.of( + "text", new ExampleColumnHandle(CONNECTOR_ID, "text", VARCHAR, 0), + "value", new ExampleColumnHandle(CONNECTOR_ID, "value", BIGINT, 1))); + + // unknown table + try { + metadata.getColumnHandles(new ExampleTableHandle(CONNECTOR_ID, "unknown", "unknown")); + fail("Expected getColumnHandle of unknown table to throw a TableNotFoundException"); + } + catch (TableNotFoundException expected) { + } + try { + metadata.getColumnHandles(new ExampleTableHandle(CONNECTOR_ID, "example", "unknown")); + fail("Expected getColumnHandle of unknown table to throw a TableNotFoundException"); + } + catch (TableNotFoundException expected) { + } + } + + @Test + public void getTableMetadata() + { + // known table + ConnectorTableMetadata tableMetadata = metadata.getTableMetadata(NUMBERS_TABLE_HANDLE); + assertEquals(tableMetadata.getTable(), new SchemaTableName("example", "numbers")); + assertEquals(tableMetadata.getColumns(), ImmutableList.of( + new ColumnMetadata("text", VARCHAR, false), + new ColumnMetadata("value", BIGINT, false))); + + // unknown tables should produce null + assertNull(metadata.getTableMetadata(new ExampleTableHandle(CONNECTOR_ID, "unknown", "unknown"))); + assertNull(metadata.getTableMetadata(new ExampleTableHandle(CONNECTOR_ID, "example", "unknown"))); + assertNull(metadata.getTableMetadata(new ExampleTableHandle(CONNECTOR_ID, "unknown", "numbers"))); + } + + @Test + public void testListTables() + { + // all schemas + assertEquals(ImmutableSet.copyOf(metadata.listTables(SESSION, null)), ImmutableSet.of( + new SchemaTableName("example", "numbers"), + new SchemaTableName("tpch", "orders"), + new SchemaTableName("tpch", "lineitem"))); + + // specific schema + assertEquals(ImmutableSet.copyOf(metadata.listTables(SESSION, "example")), ImmutableSet.of( + new SchemaTableName("example", "numbers"))); + assertEquals(ImmutableSet.copyOf(metadata.listTables(SESSION, "tpch")), ImmutableSet.of( + new SchemaTableName("tpch", "orders"), + new SchemaTableName("tpch", "lineitem"))); + + // unknown schema + assertEquals(ImmutableSet.copyOf(metadata.listTables(SESSION, "unknown")), ImmutableSet.of()); + } + + @Test + public void getColumnMetadata() + { + assertEquals(metadata.getColumnMetadata(NUMBERS_TABLE_HANDLE, new ExampleColumnHandle(CONNECTOR_ID, "text", VARCHAR, 0)), + new ColumnMetadata("text", VARCHAR, false)); + + // example connector assumes that the table handle and column handle are + // properly formed, so it will return a metadata object for any + // ExampleTableHandle and ExampleColumnHandle passed in. This is on because + // it is not possible for the Presto Metadata system to create the handles + // directly. + } + + @Test(expectedExceptions = PrestoException.class) + public void testCreateTable() + { + metadata.createTable(SESSION, new ConnectorTableMetadata( + new SchemaTableName("example", "foo"), + ImmutableList.of(new ColumnMetadata("text", VARCHAR, false)))); + } + + @Test(expectedExceptions = PrestoException.class) + public void testDropTableTable() + { + metadata.dropTable(NUMBERS_TABLE_HANDLE); + } +} diff --git a/presto-example-http/src/test/java/com/facebook/presto/example/TestExampleRecordSet.java b/presto-example-http/src/test/java/com/facebook/presto/example/TestExampleRecordSet.java new file mode 100644 index 00000000..cc1fdbf3 --- /dev/null +++ b/presto-example-http/src/test/java/com/facebook/presto/example/TestExampleRecordSet.java @@ -0,0 +1,133 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.example; + +import com.facebook.presto.spi.RecordCursor; +import com.facebook.presto.spi.RecordSet; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import java.net.URI; +import java.util.LinkedHashMap; +import java.util.Map; + +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; + +public class TestExampleRecordSet +{ + private ExampleHttpServer exampleHttpServer; + private URI dataUri; + + @Test + public void testGetColumnTypes() + throws Exception + { + RecordSet recordSet = new ExampleRecordSet(new ExampleSplit("test", "schema", "table", dataUri), ImmutableList.of( + new ExampleColumnHandle("test", "text", VARCHAR, 0), + new ExampleColumnHandle("test", "value", BIGINT, 1))); + assertEquals(recordSet.getColumnTypes(), ImmutableList.of(VARCHAR, BIGINT)); + + recordSet = new ExampleRecordSet(new ExampleSplit("test", "schema", "table", dataUri), ImmutableList.of( + new ExampleColumnHandle("test", "value", BIGINT, 1), + new ExampleColumnHandle("test", "text", VARCHAR, 0))); + assertEquals(recordSet.getColumnTypes(), ImmutableList.of(BIGINT, VARCHAR)); + + recordSet = new ExampleRecordSet(new ExampleSplit("test", "schema", "table", dataUri), ImmutableList.of( + new ExampleColumnHandle("test", "value", BIGINT, 1), + new ExampleColumnHandle("test", "value", BIGINT, 1), + new ExampleColumnHandle("test", "text", VARCHAR, 0))); + assertEquals(recordSet.getColumnTypes(), ImmutableList.of(BIGINT, BIGINT, VARCHAR)); + + recordSet = new ExampleRecordSet(new ExampleSplit("test", "schema", "table", dataUri), ImmutableList.of()); + assertEquals(recordSet.getColumnTypes(), ImmutableList.of()); + } + + @Test + public void testCursorSimple() + throws Exception + { + RecordSet recordSet = new ExampleRecordSet(new ExampleSplit("test", "schema", "table", dataUri), ImmutableList.of( + new ExampleColumnHandle("test", "text", VARCHAR, 0), + new ExampleColumnHandle("test", "value", BIGINT, 1))); + RecordCursor cursor = recordSet.cursor(); + + assertEquals(cursor.getType(0), VARCHAR); + assertEquals(cursor.getType(1), BIGINT); + + Map data = new LinkedHashMap<>(); + while (cursor.advanceNextPosition()) { + data.put(cursor.getSlice(0).toStringUtf8(), cursor.getLong(1)); + assertFalse(cursor.isNull(0)); + assertFalse(cursor.isNull(1)); + } + assertEquals(data, ImmutableMap.builder() + .put("ten", 10L) + .put("eleven", 11L) + .put("twelve", 12L) + .build()); + } + + @Test + public void testCursorMixedOrder() + throws Exception + { + RecordSet recordSet = new ExampleRecordSet(new ExampleSplit("test", "schema", "table", dataUri), ImmutableList.of( + new ExampleColumnHandle("test", "value", BIGINT, 1), + new ExampleColumnHandle("test", "value", BIGINT, 1), + new ExampleColumnHandle("test", "text", VARCHAR, 0))); + RecordCursor cursor = recordSet.cursor(); + + Map data = new LinkedHashMap<>(); + while (cursor.advanceNextPosition()) { + assertEquals(cursor.getLong(0), cursor.getLong(1)); + data.put(cursor.getSlice(2).toStringUtf8(), cursor.getLong(0)); + } + assertEquals(data, ImmutableMap.builder() + .put("ten", 10L) + .put("eleven", 11L) + .put("twelve", 12L) + .build()); + } + + // + // TODO: your code should also have tests for all types that you support and for the state machine of your cursor + // + + // + // Start http server for testing + // + + @BeforeClass + public void setUp() + throws Exception + { + exampleHttpServer = new ExampleHttpServer(); + dataUri = exampleHttpServer.resolve("/example-data/numbers-2.csv"); + } + + @AfterClass + public void tearDown() + throws Exception + { + if (exampleHttpServer != null) { + exampleHttpServer.stop(); + } + } +} diff --git a/presto-example-http/src/test/java/com/facebook/presto/example/TestExampleRecordSetProvider.java b/presto-example-http/src/test/java/com/facebook/presto/example/TestExampleRecordSetProvider.java new file mode 100644 index 00000000..9b03c11f --- /dev/null +++ b/presto-example-http/src/test/java/com/facebook/presto/example/TestExampleRecordSetProvider.java @@ -0,0 +1,82 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.example; + +import com.facebook.presto.spi.RecordCursor; +import com.facebook.presto.spi.RecordSet; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import java.net.URI; +import java.util.LinkedHashMap; +import java.util.Map; + +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +public class TestExampleRecordSetProvider +{ + private ExampleHttpServer exampleHttpServer; + private URI dataUri; + + @Test + public void testGetRecordSet() + throws Exception + { + ExampleRecordSetProvider recordSetProvider = new ExampleRecordSetProvider(new ExampleConnectorId("test")); + RecordSet recordSet = recordSetProvider.getRecordSet(new ExampleSplit("test", "schema", "table", dataUri), ImmutableList.of( + new ExampleColumnHandle("test", "text", VARCHAR, 0), + new ExampleColumnHandle("test", "value", BIGINT, 1))); + assertNotNull(recordSet, "recordSet is null"); + + RecordCursor cursor = recordSet.cursor(); + assertNotNull(cursor, "cursor is null"); + + Map data = new LinkedHashMap<>(); + while (cursor.advanceNextPosition()) { + data.put(cursor.getSlice(0).toStringUtf8(), cursor.getLong(1)); + } + assertEquals(data, ImmutableMap.builder() + .put("ten", 10L) + .put("eleven", 11L) + .put("twelve", 12L) + .build()); + } + + // + // Start http server for testing + // + + @BeforeClass + public void setUp() + throws Exception + { + exampleHttpServer = new ExampleHttpServer(); + dataUri = exampleHttpServer.resolve("/example-data/numbers-2.csv"); + } + + @AfterClass + public void tearDown() + throws Exception + { + if (exampleHttpServer != null) { + exampleHttpServer.stop(); + } + } +} diff --git a/presto-example-http/src/test/java/com/facebook/presto/example/TestExampleSplit.java b/presto-example-http/src/test/java/com/facebook/presto/example/TestExampleSplit.java new file mode 100644 index 00000000..d8c693ae --- /dev/null +++ b/presto-example-http/src/test/java/com/facebook/presto/example/TestExampleSplit.java @@ -0,0 +1,68 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.example; + +import com.facebook.presto.spi.HostAddress; +import com.google.common.collect.ImmutableList; +import io.airlift.json.JsonCodec; +import org.testng.annotations.Test; + +import java.net.URI; + +import static io.airlift.json.JsonCodec.jsonCodec; +import static org.testng.Assert.assertEquals; + +public class TestExampleSplit +{ + private final ExampleSplit split = new ExampleSplit("connectorId", "schemaName", "tableName", URI.create("http://127.0.0.1/test.file")); + + @Test + public void testAddresses() + { + // http split with default port + ExampleSplit httpSplit = new ExampleSplit("connectorId", "schemaName", "tableName", URI.create("http://example.com/example")); + assertEquals(httpSplit.getAddresses(), ImmutableList.of(HostAddress.fromString("example.com"))); + assertEquals(httpSplit.isRemotelyAccessible(), true); + + // http split with custom port + httpSplit = new ExampleSplit("connectorId", "schemaName", "tableName", URI.create("http://example.com:8080/example")); + assertEquals(httpSplit.getAddresses(), ImmutableList.of(HostAddress.fromParts("example.com", 8080))); + assertEquals(httpSplit.isRemotelyAccessible(), true); + + // http split with default port + ExampleSplit httpsSplit = new ExampleSplit("connectorId", "schemaName", "tableName", URI.create("https://example.com/example")); + assertEquals(httpsSplit.getAddresses(), ImmutableList.of(HostAddress.fromString("example.com"))); + assertEquals(httpsSplit.isRemotelyAccessible(), true); + + // http split with custom port + httpsSplit = new ExampleSplit("connectorId", "schemaName", "tableName", URI.create("https://example.com:8443/example")); + assertEquals(httpsSplit.getAddresses(), ImmutableList.of(HostAddress.fromParts("example.com", 8443))); + assertEquals(httpsSplit.isRemotelyAccessible(), true); + } + + @Test + public void testJsonRoundTrip() + { + JsonCodec codec = jsonCodec(ExampleSplit.class); + String json = codec.toJson(split); + ExampleSplit copy = codec.fromJson(json); + assertEquals(copy.getConnectorId(), split.getConnectorId()); + assertEquals(copy.getSchemaName(), split.getSchemaName()); + assertEquals(copy.getTableName(), split.getTableName()); + assertEquals(copy.getUri(), split.getUri()); + + assertEquals(copy.getAddresses(), ImmutableList.of(HostAddress.fromString("127.0.0.1"))); + assertEquals(copy.isRemotelyAccessible(), true); + } +} diff --git a/presto-example-http/src/test/java/com/facebook/presto/example/TestExampleTable.java b/presto-example-http/src/test/java/com/facebook/presto/example/TestExampleTable.java new file mode 100644 index 00000000..cfd29b94 --- /dev/null +++ b/presto-example-http/src/test/java/com/facebook/presto/example/TestExampleTable.java @@ -0,0 +1,52 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.example; + +import com.facebook.presto.spi.ColumnMetadata; +import com.google.common.collect.ImmutableList; +import org.testng.annotations.Test; + +import java.net.URI; + +import static com.facebook.presto.example.MetadataUtil.TABLE_CODEC; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static org.testng.Assert.assertEquals; + +public class TestExampleTable +{ + private final ExampleTable exampleTable = new ExampleTable("tableName", + ImmutableList.of(new ExampleColumn("a", VARCHAR), new ExampleColumn("b", BIGINT)), + ImmutableList.of(URI.create("file://table-1.json"), URI.create("file://table-2.json"))); + + @Test + public void testColumnMetadata() + { + assertEquals(exampleTable.getColumnsMetadata(), ImmutableList.of( + new ColumnMetadata("a", VARCHAR, false), + new ColumnMetadata("b", BIGINT, false))); + } + + @Test + public void testRoundTrip() + throws Exception + { + String json = TABLE_CODEC.toJson(exampleTable); + ExampleTable exampleTableCopy = TABLE_CODEC.fromJson(json); + + assertEquals(exampleTableCopy.getName(), exampleTable.getName()); + assertEquals(exampleTableCopy.getColumns(), exampleTable.getColumns()); + assertEquals(exampleTableCopy.getSources(), exampleTable.getSources()); + } +} diff --git a/presto-example-http/src/test/java/com/facebook/presto/example/TestExampleTableHandle.java b/presto-example-http/src/test/java/com/facebook/presto/example/TestExampleTableHandle.java new file mode 100644 index 00000000..759bdd97 --- /dev/null +++ b/presto-example-http/src/test/java/com/facebook/presto/example/TestExampleTableHandle.java @@ -0,0 +1,46 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.example; + +import io.airlift.json.JsonCodec; +import io.airlift.testing.EquivalenceTester; +import org.testng.annotations.Test; + +import static io.airlift.json.JsonCodec.jsonCodec; +import static org.testng.Assert.assertEquals; + +public class TestExampleTableHandle +{ + private final ExampleTableHandle tableHandle = new ExampleTableHandle("connectorId", "schemaName", "tableName"); + + @Test + public void testJsonRoundTrip() + { + JsonCodec codec = jsonCodec(ExampleTableHandle.class); + String json = codec.toJson(tableHandle); + ExampleTableHandle copy = codec.fromJson(json); + assertEquals(copy, tableHandle); + } + + @Test + public void testEquivalence() + { + EquivalenceTester.equivalenceTester() + .addEquivalentGroup(new ExampleTableHandle("connector", "schema", "table"), new ExampleTableHandle("connector", "schema", "table")) + .addEquivalentGroup(new ExampleTableHandle("connectorX", "schema", "table"), new ExampleTableHandle("connectorX", "schema", "table")) + .addEquivalentGroup(new ExampleTableHandle("connector", "schemaX", "table"), new ExampleTableHandle("connector", "schemaX", "table")) + .addEquivalentGroup(new ExampleTableHandle("connector", "schema", "tableX"), new ExampleTableHandle("connector", "schema", "tableX")) + .check(); + } +} diff --git a/presto-example-http/src/test/resources/example-data/example-metadata.json b/presto-example-http/src/test/resources/example-data/example-metadata.json new file mode 100644 index 00000000..d5dd7552 --- /dev/null +++ b/presto-example-http/src/test/resources/example-data/example-metadata.json @@ -0,0 +1,128 @@ +{ + "example": [ + { + "name": "numbers", + "columns": [ + { + "name": "text", + "type": "VARCHAR" + }, + { + "name": "value", + "type": "BIGINT" + } + ], + "sources": [ "numbers-1.csv", "numbers-2.csv" ] + } + ], + "tpch": [ + { + "name": "orders", + "columns": [ + { + "name": "orderkey", + "type": "BIGINT" + }, + { + "name": "custkey", + "type": "BIGINT" + }, + { + "name": "orderstatus", + "type": "VARCHAR" + }, + { + "name": "totalprice", + "type": "DOUBLE" + }, + { + "name": "orderdate", + "type": "VARCHAR" + }, + { + "name": "orderpriority", + "type": "VARCHAR" + }, + { + "name": "clerk", + "type": "VARCHAR" + }, + { + "name": "shippriority", + "type": "BIGINT" + }, + { + "name": "comment", + "type": "VARCHAR" + } + ], + "sources": [ "orders-1.csv", "orders-2.csv" ] + }, + { + "name": "lineitem", + "columns": [ + { + "name": "orderkey", + "type": "BIGINT" + }, + { + "name": "partkey", + "type": "BIGINT" + }, + { + "name": "suppkey", + "type": "BIGINT" + }, + { + "name": "linenumber", + "type": "BIGINT" + }, + { + "name": "quantity", + "type": "DOUBLE" + }, + { + "name": "discount", + "type": "DOUBLE" + }, + { + "name": "tax", + "type": "DOUBLE" + }, + { + "name": "returnflag", + "type": "VARCHAR" + }, + { + "name": "linestatus", + "type": "VARCHAR" + }, + { + "name": "shipdate", + "type": "VARCHAR" + }, + { + "name": "commitdate", + "type": "VARCHAR" + }, + { + "name": "receiptdate", + "type": "VARCHAR" + }, + { + "name": "shipinstruct", + "type": "VARCHAR" + }, + { + "name": "shipmode", + "type": "VARCHAR" + }, + { + "name": "comment", + "type": "VARCHAR" + } + ], + "sources": [ "lineitem-1.csv", "lineitem-2.csv" ] + } + ] +} diff --git a/presto-example-http/src/test/resources/example-data/lineitem-1.csv b/presto-example-http/src/test/resources/example-data/lineitem-1.csv new file mode 100644 index 00000000..c221a255 --- /dev/null +++ b/presto-example-http/src/test/resources/example-data/lineitem-1.csv @@ -0,0 +1,299 @@ +1, 155190, 7706, 1, 17, 21168.23, 0.04, 0.02, N, O, 1996-03-13, 1996-02-12, 1996-03-22, DELIVER IN PERSON, TRUCK, egular courts above the +1, 67310, 7311, 2, 36, 45983.16, 0.09, 0.06, N, O, 1996-04-12, 1996-02-28, 1996-04-20, TAKE BACK RETURN, MAIL, ly final dependencies: slyly bold +1, 63700, 3701, 3, 8, 13309.60, 0.10, 0.02, N, O, 1996-01-29, 1996-03-05, 1996-01-31, TAKE BACK RETURN, REG AIR, riously. regular express dep +1, 2132, 4633, 4, 28, 28955.64, 0.09, 0.06, N, O, 1996-04-21, 1996-03-30, 1996-05-16, NONE, AIR, lites. fluffily even de +1, 24027, 1534, 5, 24, 22824.48, 0.10, 0.04, N, O, 1996-03-30, 1996-03-14, 1996-04-01, NONE, FOB, pending foxes. slyly re +1, 15635, 638, 6, 32, 49620.16, 0.07, 0.02, N, O, 1996-01-30, 1996-02-07, 1996-02-03, DELIVER IN PERSON, MAIL, arefully slyly ex +2, 106170, 1191, 1, 38, 44694.46, 0.00, 0.05, N, O, 1997-01-28, 1997-01-14, 1997-02-02, TAKE BACK RETURN, RAIL, ven requests. deposits breach a +3, 4297, 1798, 1, 45, 54058.05, 0.06, 0.00, R, F, 1994-02-02, 1994-01-04, 1994-02-23, NONE, AIR, ongside of the furiously brave acco +3, 19036, 6540, 2, 49, 46796.47, 0.10, 0.00, R, F, 1993-11-09, 1993-12-20, 1993-11-24, TAKE BACK RETURN, RAIL, unusual accounts. eve +3, 128449, 3474, 3, 27, 39890.88, 0.06, 0.07, A, F, 1994-01-16, 1993-11-22, 1994-01-23, DELIVER IN PERSON, SHIP, nal foxes wake. +3, 29380, 1883, 4, 2, 2618.76, 0.01, 0.06, A, F, 1993-12-04, 1994-01-07, 1994-01-01, NONE, TRUCK, y. fluffily pending d +3, 183095, 650, 5, 28, 32986.52, 0.04, 0.00, R, F, 1993-12-14, 1994-01-10, 1994-01-01, TAKE BACK RETURN, FOB, ages nag slyly pending +3, 62143, 9662, 6, 26, 28733.64, 0.10, 0.02, A, F, 1993-10-29, 1993-12-18, 1993-11-04, TAKE BACK RETURN, RAIL, ges sleep after the caref +4, 88035, 5560, 1, 30, 30690.90, 0.03, 0.08, N, O, 1996-01-10, 1995-12-14, 1996-01-18, DELIVER IN PERSON, REG AIR, - quickly regular packages sleep. idly +5, 108570, 8571, 1, 15, 23678.55, 0.02, 0.04, R, F, 1994-10-31, 1994-08-31, 1994-11-20, NONE, AIR, ts wake furiously +5, 123927, 3928, 2, 26, 50723.92, 0.07, 0.08, R, F, 1994-10-16, 1994-09-25, 1994-10-19, NONE, FOB, sts use slyly quickly special instruc +5, 37531, 35, 3, 50, 73426.50, 0.08, 0.03, A, F, 1994-08-08, 1994-10-13, 1994-08-26, DELIVER IN PERSON, AIR, eodolites. fluffily unusual +6, 139636, 2150, 1, 37, 61998.31, 0.08, 0.03, A, F, 1992-04-27, 1992-05-15, 1992-05-02, TAKE BACK RETURN, TRUCK, p furiously special foxes +7, 182052, 9607, 1, 12, 13608.60, 0.07, 0.03, N, O, 1996-05-07, 1996-03-13, 1996-06-03, TAKE BACK RETURN, FOB, ss pinto beans wake against th +7, 145243, 7758, 2, 9, 11594.16, 0.08, 0.08, N, O, 1996-02-01, 1996-03-02, 1996-02-19, TAKE BACK RETURN, SHIP, es. instructions +7, 94780, 9799, 3, 46, 81639.88, 0.10, 0.07, N, O, 1996-01-15, 1996-03-27, 1996-02-03, COLLECT COD, MAIL, unusual reques +7, 163073, 3074, 4, 28, 31809.96, 0.03, 0.04, N, O, 1996-03-21, 1996-04-08, 1996-04-20, NONE, FOB, . slyly special requests haggl +7, 151894, 9440, 5, 38, 73943.82, 0.08, 0.01, N, O, 1996-02-11, 1996-02-24, 1996-02-18, DELIVER IN PERSON, TRUCK, ns haggle carefully ironic deposits. bl +7, 79251, 1759, 6, 35, 43058.75, 0.06, 0.03, N, O, 1996-01-16, 1996-02-23, 1996-01-22, TAKE BACK RETURN, FOB, jole. excuses wake carefully alongside of +7, 157238, 2269, 7, 5, 6476.15, 0.04, 0.02, N, O, 1996-02-10, 1996-03-26, 1996-02-13, NONE, FOB, ithely regula +32, 82704, 7721, 1, 28, 47227.60, 0.05, 0.08, N, O, 1995-10-23, 1995-08-27, 1995-10-26, TAKE BACK RETURN, TRUCK, sleep quickly. req +32, 197921, 441, 2, 32, 64605.44, 0.02, 0.00, N, O, 1995-08-14, 1995-10-07, 1995-08-27, COLLECT COD, AIR, lithely regular deposits. fluffily +32, 44161, 6666, 3, 2, 2210.32, 0.09, 0.02, N, O, 1995-08-07, 1995-10-07, 1995-08-23, DELIVER IN PERSON, AIR, express accounts wake according to the +32, 2743, 7744, 4, 4, 6582.96, 0.09, 0.03, N, O, 1995-08-04, 1995-10-01, 1995-09-03, NONE, REG AIR, e slyly final pac +32, 85811, 8320, 5, 44, 79059.64, 0.05, 0.06, N, O, 1995-08-28, 1995-08-20, 1995-09-14, DELIVER IN PERSON, AIR, symptotes nag according to the ironic depo +32, 11615, 4117, 6, 6, 9159.66, 0.04, 0.03, N, O, 1995-07-21, 1995-09-23, 1995-07-25, COLLECT COD, RAIL, gifts cajole carefully. +33, 61336, 8855, 1, 31, 40217.23, 0.09, 0.04, A, F, 1993-10-29, 1993-12-19, 1993-11-08, COLLECT COD, TRUCK, ng to the furiously ironic package +33, 60519, 5532, 2, 32, 47344.32, 0.02, 0.05, A, F, 1993-12-09, 1994-01-04, 1993-12-28, COLLECT COD, MAIL, gular theodolites +33, 137469, 9983, 3, 5, 7532.30, 0.05, 0.03, A, F, 1993-12-09, 1993-12-25, 1993-12-23, TAKE BACK RETURN, AIR, . stealthily bold exc +33, 33918, 3919, 4, 41, 75928.31, 0.09, 0.00, R, F, 1993-11-09, 1994-01-24, 1993-11-11, TAKE BACK RETURN, MAIL, unusual packages doubt caref +34, 88362, 871, 1, 13, 17554.68, 0.00, 0.07, N, O, 1998-10-23, 1998-09-14, 1998-11-06, NONE, REG AIR, nic accounts. deposits are alon +34, 89414, 1923, 2, 22, 30875.02, 0.08, 0.06, N, O, 1998-10-09, 1998-10-16, 1998-10-12, NONE, FOB, thely slyly p +34, 169544, 4577, 3, 6, 9681.24, 0.02, 0.06, N, O, 1998-10-30, 1998-09-20, 1998-11-05, NONE, FOB, ar foxes sleep +35, 450, 2951, 1, 24, 32410.80, 0.02, 0.00, N, O, 1996-02-21, 1996-01-03, 1996-03-18, TAKE BACK RETURN, FOB, regular tithe +35, 161940, 4457, 2, 34, 68065.96, 0.06, 0.08, N, O, 1996-01-22, 1996-01-06, 1996-01-27, DELIVER IN PERSON, RAIL, s are carefully against the f +35, 120896, 8433, 3, 7, 13418.23, 0.06, 0.04, N, O, 1996-01-19, 1995-12-22, 1996-01-29, NONE, MAIL, the carefully regular +35, 85175, 7684, 4, 25, 29004.25, 0.06, 0.05, N, O, 1995-11-26, 1995-12-25, 1995-12-21, DELIVER IN PERSON, SHIP, quickly unti +35, 119917, 4940, 5, 34, 65854.94, 0.08, 0.06, N, O, 1995-11-08, 1996-01-15, 1995-11-26, COLLECT COD, MAIL, . silent unusual deposits boost +35, 30762, 3266, 6, 28, 47397.28, 0.03, 0.02, N, O, 1996-02-01, 1995-12-24, 1996-02-28, COLLECT COD, RAIL, ly alongside of +36, 119767, 9768, 1, 42, 75043.92, 0.09, 0.00, N, O, 1996-02-03, 1996-01-21, 1996-02-23, COLLECT COD, SHIP, careful courts. special +37, 22630, 5133, 1, 40, 62105.20, 0.09, 0.03, A, F, 1992-07-21, 1992-08-01, 1992-08-15, NONE, REG AIR, luffily regular requests. slyly final acco +37, 126782, 1807, 2, 39, 70542.42, 0.05, 0.02, A, F, 1992-07-02, 1992-08-18, 1992-07-28, TAKE BACK RETURN, RAIL, the final requests. ca +37, 12903, 5405, 3, 43, 78083.70, 0.05, 0.08, A, F, 1992-07-10, 1992-07-06, 1992-08-02, DELIVER IN PERSON, TRUCK, iously ste +38, 175839, 874, 1, 44, 84252.52, 0.04, 0.02, N, O, 1996-09-29, 1996-11-17, 1996-09-30, COLLECT COD, MAIL, s. blithely unusual theodolites am +39, 2320, 9821, 1, 44, 53782.08, 0.09, 0.06, N, O, 1996-11-14, 1996-12-15, 1996-12-12, COLLECT COD, RAIL, eodolites. careful +39, 186582, 4137, 2, 26, 43383.08, 0.08, 0.04, N, O, 1996-11-04, 1996-10-20, 1996-11-20, NONE, FOB, ckages across the slyly silent +39, 67831, 5350, 3, 46, 82746.18, 0.06, 0.08, N, O, 1996-09-26, 1996-12-19, 1996-10-26, DELIVER IN PERSON, AIR, he carefully e +39, 20590, 3093, 4, 32, 48338.88, 0.07, 0.05, N, O, 1996-10-02, 1996-12-19, 1996-10-14, COLLECT COD, MAIL, heodolites sleep silently pending foxes. ac +39, 54519, 9530, 5, 43, 63360.93, 0.01, 0.01, N, O, 1996-10-17, 1996-11-14, 1996-10-26, COLLECT COD, MAIL, yly regular i +39, 94368, 6878, 6, 40, 54494.40, 0.06, 0.05, N, O, 1996-12-08, 1996-10-22, 1997-01-01, COLLECT COD, AIR, quickly ironic fox +64, 85951, 5952, 1, 21, 40675.95, 0.05, 0.02, R, F, 1994-09-30, 1994-09-18, 1994-10-26, DELIVER IN PERSON, REG AIR, ch slyly final thin platelets. +65, 59694, 4705, 1, 26, 42995.94, 0.03, 0.03, A, F, 1995-04-20, 1995-04-25, 1995-05-13, NONE, TRUCK, pending deposits nag even packages. ca +65, 73815, 8830, 2, 22, 39353.82, 0.00, 0.05, N, O, 1995-07-17, 1995-06-04, 1995-07-19, COLLECT COD, FOB, ideas. special r +65, 1388, 3889, 3, 21, 27076.98, 0.09, 0.07, N, O, 1995-07-06, 1995-05-14, 1995-07-31, DELIVER IN PERSON, RAIL, bove the even packages. accounts nag carefu +66, 115118, 7630, 1, 31, 35126.41, 0.00, 0.08, R, F, 1994-02-19, 1994-03-11, 1994-02-20, TAKE BACK RETURN, RAIL, ut the unusual accounts sleep at the bo +66, 173489, 3490, 2, 41, 64061.68, 0.04, 0.07, A, F, 1994-02-21, 1994-03-01, 1994-03-18, COLLECT COD, AIR, regular de +67, 21636, 9143, 1, 4, 6230.52, 0.09, 0.04, N, O, 1997-04-17, 1997-01-31, 1997-04-20, NONE, SHIP, cajole thinly expres +67, 20193, 5198, 2, 12, 13358.28, 0.09, 0.05, N, O, 1997-01-27, 1997-02-21, 1997-02-22, NONE, REG AIR, even packages cajole +67, 173600, 6118, 3, 5, 8368.00, 0.03, 0.07, N, O, 1997-02-20, 1997-02-12, 1997-02-21, DELIVER IN PERSON, TRUCK, y unusual packages thrash pinto +67, 87514, 7515, 4, 44, 66066.44, 0.08, 0.06, N, O, 1997-03-18, 1997-01-29, 1997-04-13, DELIVER IN PERSON, RAIL, se quickly above the even express reques +67, 40613, 8126, 5, 23, 35733.03, 0.05, 0.07, N, O, 1997-04-19, 1997-02-14, 1997-05-06, DELIVER IN PERSON, REG AIR, ly regular deposit +67, 178306, 824, 6, 29, 40144.70, 0.02, 0.05, N, O, 1997-01-25, 1997-01-27, 1997-01-27, DELIVER IN PERSON, FOB, ultipliers +68, 7068, 9569, 1, 3, 2925.18, 0.05, 0.02, N, O, 1998-07-04, 1998-06-05, 1998-07-21, NONE, RAIL, fully special instructions cajole. furious +68, 175180, 2732, 2, 46, 57738.28, 0.02, 0.05, N, O, 1998-06-26, 1998-06-07, 1998-07-05, NONE, MAIL, requests are unusual regular pinto +68, 34980, 7484, 3, 46, 88089.08, 0.04, 0.05, N, O, 1998-08-13, 1998-07-08, 1998-08-29, NONE, RAIL, egular dependencies affix ironically along +68, 94728, 2256, 4, 20, 34454.40, 0.07, 0.01, N, O, 1998-06-27, 1998-05-23, 1998-07-02, NONE, REG AIR, excuses integrate fluffily +68, 82758, 5267, 5, 27, 47000.25, 0.03, 0.06, N, O, 1998-06-19, 1998-06-25, 1998-06-29, DELIVER IN PERSON, SHIP, ccounts. deposits use. furiously +68, 102561, 5072, 6, 30, 46906.80, 0.05, 0.06, N, O, 1998-08-11, 1998-07-11, 1998-08-14, NONE, RAIL, oxes are slyly blithely fin +68, 139247, 1761, 7, 41, 52735.84, 0.09, 0.08, N, O, 1998-06-24, 1998-06-27, 1998-07-06, NONE, SHIP, eposits nag special ideas. furiousl +69, 115209, 7721, 1, 48, 58761.60, 0.01, 0.07, A, F, 1994-08-17, 1994-08-11, 1994-09-08, NONE, TRUCK, regular epitaphs. carefully even ideas hag +69, 104180, 9201, 2, 32, 37893.76, 0.08, 0.06, A, F, 1994-08-24, 1994-08-17, 1994-08-31, NONE, REG AIR, s sleep carefully bold +69, 137267, 4807, 3, 17, 22172.42, 0.09, 0.00, A, F, 1994-07-02, 1994-07-07, 1994-07-03, TAKE BACK RETURN, AIR, final pending instr +69, 37502, 2509, 4, 3, 4318.50, 0.09, 0.04, R, F, 1994-06-06, 1994-07-27, 1994-06-15, NONE, MAIL, blithely final d +69, 92070, 7089, 5, 42, 44606.94, 0.07, 0.04, R, F, 1994-07-31, 1994-07-26, 1994-08-28, DELIVER IN PERSON, REG AIR, tect regular speci +69, 18504, 1006, 6, 23, 32717.50, 0.05, 0.00, A, F, 1994-10-03, 1994-08-06, 1994-10-24, NONE, SHIP, nding accounts ca +70, 64128, 9141, 1, 8, 8736.96, 0.03, 0.08, R, F, 1994-01-12, 1994-02-27, 1994-01-14, TAKE BACK RETURN, FOB, ggle. carefully pending dependenc +70, 196156, 1195, 2, 13, 16277.95, 0.06, 0.06, A, F, 1994-03-03, 1994-02-13, 1994-03-26, COLLECT COD, AIR, lyly special packag +70, 179809, 7361, 3, 1, 1888.80, 0.03, 0.05, R, F, 1994-01-26, 1994-03-05, 1994-01-28, TAKE BACK RETURN, RAIL, quickly. fluffily unusual theodolites c +70, 45734, 743, 4, 11, 18477.03, 0.01, 0.05, A, F, 1994-03-17, 1994-03-17, 1994-03-27, NONE, MAIL, alongside of the deposits. fur +70, 37131, 2138, 5, 37, 39520.81, 0.09, 0.04, R, F, 1994-02-13, 1994-03-16, 1994-02-21, COLLECT COD, MAIL, n accounts are. q +70, 55655, 3171, 6, 19, 30602.35, 0.06, 0.03, A, F, 1994-01-26, 1994-02-17, 1994-02-06, TAKE BACK RETURN, SHIP, packages wake pending accounts. +71, 61931, 1932, 1, 25, 47323.25, 0.09, 0.07, N, O, 1998-04-10, 1998-04-22, 1998-04-11, COLLECT COD, FOB, ckly. slyly +71, 65916, 3435, 2, 3, 5645.73, 0.09, 0.07, N, O, 1998-05-23, 1998-04-03, 1998-06-02, COLLECT COD, SHIP, y. pinto beans haggle after the +71, 34432, 1942, 3, 45, 61489.35, 0.00, 0.07, N, O, 1998-02-23, 1998-03-20, 1998-03-24, DELIVER IN PERSON, SHIP, ironic packages believe blithely a +71, 96645, 9155, 4, 33, 54174.12, 0.00, 0.01, N, O, 1998-04-12, 1998-03-20, 1998-04-15, NONE, FOB, serve quickly fluffily bold deposi +71, 103255, 5766, 5, 39, 49071.75, 0.08, 0.06, N, O, 1998-01-29, 1998-04-07, 1998-02-18, DELIVER IN PERSON, RAIL, l accounts sleep across the pack +71, 195635, 674, 6, 34, 58841.42, 0.04, 0.01, N, O, 1998-03-05, 1998-04-22, 1998-03-30, DELIVER IN PERSON, TRUCK, s cajole. +96, 123076, 613, 1, 23, 25278.61, 0.10, 0.06, A, F, 1994-07-19, 1994-06-29, 1994-07-25, DELIVER IN PERSON, TRUCK, ep-- carefully reg +96, 135390, 5391, 2, 30, 42761.70, 0.01, 0.06, R, F, 1994-06-03, 1994-05-29, 1994-06-22, DELIVER IN PERSON, TRUCK, e quickly even ideas. furiou +97, 119477, 1989, 1, 13, 19454.11, 0.00, 0.02, R, F, 1993-04-01, 1993-04-04, 1993-04-08, NONE, TRUCK, ayers cajole against the furiously +97, 49568, 2073, 2, 37, 56149.72, 0.02, 0.06, A, F, 1993-04-13, 1993-03-30, 1993-04-14, DELIVER IN PERSON, SHIP, ic requests boost carefully quic +97, 77699, 5221, 3, 19, 31857.11, 0.06, 0.08, R, F, 1993-05-14, 1993-03-05, 1993-05-25, TAKE BACK RETURN, RAIL, gifts. furiously ironic packages cajole. +98, 40216, 217, 1, 28, 32373.88, 0.06, 0.07, A, F, 1994-12-24, 1994-10-25, 1995-01-16, COLLECT COD, REG AIR, pending regular accounts s +98, 109743, 7274, 2, 1, 1752.74, 0.00, 0.00, A, F, 1994-12-01, 1994-12-12, 1994-12-15, DELIVER IN PERSON, TRUCK, . unusual instructions against +98, 44706, 4707, 3, 14, 23109.80, 0.05, 0.02, A, F, 1994-12-30, 1994-11-22, 1995-01-27, COLLECT COD, AIR, cajole furiously. blithely ironic ideas +98, 167180, 7181, 4, 10, 12471.80, 0.03, 0.03, A, F, 1994-10-23, 1994-11-08, 1994-11-09, COLLECT COD, RAIL, carefully. quickly ironic ideas +99, 87114, 4639, 1, 10, 11011.10, 0.02, 0.01, A, F, 1994-05-18, 1994-06-03, 1994-05-23, COLLECT COD, RAIL, kages. requ +99, 123766, 3767, 2, 5, 8948.80, 0.02, 0.07, R, F, 1994-05-06, 1994-05-28, 1994-05-20, TAKE BACK RETURN, RAIL, ests cajole fluffily waters. blithe +99, 134082, 1622, 3, 42, 46875.36, 0.02, 0.02, A, F, 1994-04-19, 1994-05-18, 1994-04-20, NONE, RAIL, kages are fluffily furiously ir +99, 108338, 849, 4, 36, 48467.88, 0.09, 0.02, A, F, 1994-07-04, 1994-04-17, 1994-07-30, DELIVER IN PERSON, AIR, slyly. slyly e +100, 62029, 2030, 1, 28, 27748.56, 0.04, 0.05, N, O, 1998-05-08, 1998-05-13, 1998-06-07, COLLECT COD, TRUCK, sts haggle. slowl +100, 115979, 8491, 2, 22, 43889.34, 0.00, 0.07, N, O, 1998-06-24, 1998-04-12, 1998-06-29, DELIVER IN PERSON, SHIP, nto beans alongside of the fi +100, 46150, 8655, 3, 46, 50422.90, 0.03, 0.04, N, O, 1998-05-02, 1998-04-10, 1998-05-22, TAKE BACK RETURN, SHIP, ular accounts. even +100, 38024, 3031, 4, 14, 13468.28, 0.06, 0.03, N, O, 1998-05-22, 1998-05-01, 1998-06-03, COLLECT COD, MAIL, y. furiously ironic ideas gr +100, 53439, 955, 5, 37, 51519.91, 0.05, 0.00, N, O, 1998-03-06, 1998-04-16, 1998-03-31, TAKE BACK RETURN, TRUCK, nd the quickly s +101, 118282, 5816, 1, 49, 63713.72, 0.10, 0.00, N, O, 1996-06-21, 1996-05-27, 1996-06-29, DELIVER IN PERSON, REG AIR, ts-- final packages sleep furiousl +101, 163334, 883, 2, 36, 50303.88, 0.00, 0.01, N, O, 1996-05-19, 1996-05-01, 1996-06-04, DELIVER IN PERSON, AIR, tes. blithely pending dolphins x-ray f +101, 138418, 5958, 3, 12, 17476.92, 0.06, 0.02, N, O, 1996-03-29, 1996-04-20, 1996-04-12, COLLECT COD, MAIL, . quickly regular +102, 88914, 3931, 1, 37, 70407.67, 0.06, 0.00, N, O, 1997-07-24, 1997-08-02, 1997-08-07, TAKE BACK RETURN, SHIP, ully across the ideas. final deposit +102, 169238, 6787, 2, 34, 44445.82, 0.03, 0.08, N, O, 1997-08-09, 1997-07-28, 1997-08-26, TAKE BACK RETURN, SHIP, eposits cajole across +102, 182321, 4840, 3, 25, 35083.00, 0.01, 0.01, N, O, 1997-07-31, 1997-07-24, 1997-08-17, NONE, RAIL, bits. ironic accoun +102, 61158, 8677, 4, 15, 16787.25, 0.07, 0.07, N, O, 1997-06-02, 1997-07-13, 1997-06-04, DELIVER IN PERSON, SHIP, final packages. carefully even excu +103, 194658, 2216, 1, 6, 10515.90, 0.03, 0.05, N, O, 1996-10-11, 1996-07-25, 1996-10-28, NONE, FOB, cajole. carefully ex +103, 10426, 2928, 2, 37, 49447.54, 0.02, 0.07, N, O, 1996-09-17, 1996-07-27, 1996-09-20, TAKE BACK RETURN, MAIL, ies. quickly ironic requests use blithely +103, 28431, 8432, 3, 23, 31266.89, 0.01, 0.04, N, O, 1996-09-11, 1996-09-18, 1996-09-26, NONE, FOB, ironic accou +103, 29022, 4027, 4, 32, 30432.64, 0.01, 0.07, N, O, 1996-07-30, 1996-08-06, 1996-08-04, NONE, RAIL, kages doze. special regular deposit +128, 106828, 9339, 1, 38, 69723.16, 0.06, 0.01, A, F, 1992-09-01, 1992-08-27, 1992-10-01, TAKE BACK RETURN, FOB, cajole careful +129, 2867, 5368, 1, 46, 81413.56, 0.08, 0.02, R, F, 1993-02-15, 1993-01-24, 1993-03-05, COLLECT COD, TRUCK, uietly bold theodolites. fluffil +129, 185164, 5165, 2, 36, 44969.76, 0.01, 0.02, A, F, 1992-11-25, 1992-12-25, 1992-12-09, TAKE BACK RETURN, REG AIR, packages are care +129, 39444, 1948, 3, 33, 45653.52, 0.04, 0.06, A, F, 1993-01-08, 1993-02-14, 1993-01-29, COLLECT COD, SHIP, sts nag bravely. fluffily +129, 135137, 164, 4, 34, 39852.42, 0.00, 0.01, R, F, 1993-01-29, 1993-02-14, 1993-02-10, COLLECT COD, MAIL, quests. express ideas +129, 31373, 8883, 5, 24, 31304.88, 0.06, 0.00, A, F, 1992-12-07, 1993-01-02, 1992-12-11, TAKE BACK RETURN, FOB, uests. foxes cajole slyly after the ca +129, 77050, 4572, 6, 22, 22595.10, 0.06, 0.01, R, F, 1993-02-15, 1993-01-31, 1993-02-24, COLLECT COD, SHIP, e. fluffily regular +129, 168569, 3602, 7, 1, 1637.56, 0.05, 0.04, R, F, 1993-01-26, 1993-01-08, 1993-02-24, DELIVER IN PERSON, FOB, e carefully blithely bold dolp +130, 128816, 8817, 1, 14, 25827.34, 0.08, 0.05, A, F, 1992-08-15, 1992-07-25, 1992-09-13, COLLECT COD, RAIL, requests. final instruction +130, 1739, 4240, 2, 48, 78755.04, 0.03, 0.02, R, F, 1992-07-01, 1992-07-12, 1992-07-24, NONE, AIR, lithely alongside of the regu +130, 11860, 1861, 3, 18, 31893.48, 0.04, 0.08, A, F, 1992-07-04, 1992-06-14, 1992-07-29, DELIVER IN PERSON, MAIL, slyly ironic decoys abou +130, 115635, 3169, 4, 13, 21458.19, 0.09, 0.02, R, F, 1992-06-26, 1992-07-29, 1992-07-05, NONE, FOB, pending dolphins sleep furious +130, 69130, 4143, 5, 31, 34073.03, 0.06, 0.05, R, F, 1992-09-01, 1992-07-18, 1992-09-02, TAKE BACK RETURN, RAIL, thily about the ruth +131, 167505, 22, 1, 45, 70762.50, 0.10, 0.02, R, F, 1994-09-14, 1994-09-02, 1994-10-04, NONE, FOB, ironic bold accounts. careful +131, 44255, 9264, 2, 50, 59962.50, 0.02, 0.04, A, F, 1994-09-17, 1994-08-10, 1994-09-21, NONE, SHIP, ending requests. final ironic pearls slee +131, 189021, 1540, 3, 4, 4440.08, 0.04, 0.03, A, F, 1994-09-20, 1994-08-30, 1994-09-23, COLLECT COD, REG AIR, are carefully slyly i +132, 140449, 2964, 1, 18, 26809.92, 0.00, 0.08, R, F, 1993-07-10, 1993-08-05, 1993-07-13, NONE, TRUCK, ges. platelets wake furio +132, 119053, 9054, 2, 43, 46098.15, 0.01, 0.08, R, F, 1993-09-01, 1993-08-16, 1993-09-22, NONE, TRUCK, y pending theodolites +132, 114419, 4420, 3, 32, 45869.12, 0.04, 0.04, A, F, 1993-07-12, 1993-08-05, 1993-08-05, COLLECT COD, TRUCK, d instructions hagg +132, 28082, 5589, 4, 23, 23231.84, 0.10, 0.00, A, F, 1993-06-16, 1993-08-27, 1993-06-23, DELIVER IN PERSON, AIR, refully blithely bold acco +133, 103432, 5943, 1, 27, 38756.61, 0.00, 0.02, N, O, 1997-12-21, 1998-02-23, 1997-12-27, TAKE BACK RETURN, MAIL, yly even gifts after the sl +133, 176279, 3831, 2, 12, 16263.24, 0.02, 0.06, N, O, 1997-12-02, 1998-01-15, 1997-12-29, DELIVER IN PERSON, REG AIR, ts cajole fluffily quickly i +133, 117350, 4884, 3, 29, 39653.15, 0.09, 0.08, N, O, 1998-02-28, 1998-01-30, 1998-03-09, DELIVER IN PERSON, RAIL, the carefully regular theodoli +133, 89855, 7380, 4, 11, 20293.35, 0.06, 0.01, N, O, 1998-03-21, 1998-01-15, 1998-04-04, DELIVER IN PERSON, REG AIR, e quickly across the dolphins +134, 641, 642, 1, 21, 32374.44, 0.00, 0.03, A, F, 1992-07-17, 1992-07-08, 1992-07-26, COLLECT COD, SHIP, s. quickly regular +134, 164645, 9678, 2, 35, 59837.40, 0.06, 0.07, A, F, 1992-08-23, 1992-06-01, 1992-08-24, NONE, MAIL, ajole furiously. instructio +134, 188252, 3289, 3, 26, 34846.50, 0.09, 0.06, A, F, 1992-06-20, 1992-07-12, 1992-07-16, NONE, RAIL, among the pending depos +134, 144002, 4003, 4, 47, 49162.00, 0.05, 0.00, A, F, 1992-08-16, 1992-07-06, 1992-08-28, NONE, REG AIR, s! carefully unusual requests boost careful +134, 35172, 5173, 5, 12, 13286.04, 0.05, 0.02, A, F, 1992-07-03, 1992-06-01, 1992-07-11, COLLECT COD, TRUCK, nts are quic +134, 133103, 5617, 6, 12, 13633.20, 0.00, 0.00, A, F, 1992-08-08, 1992-07-07, 1992-08-20, TAKE BACK RETURN, FOB, lyly regular pac +135, 108205, 8206, 1, 47, 57020.40, 0.06, 0.08, N, O, 1996-02-18, 1996-01-01, 1996-02-25, COLLECT COD, RAIL, ctions wake slyly abo +135, 198344, 5902, 2, 21, 30289.14, 0.00, 0.07, N, O, 1996-02-11, 1996-01-12, 1996-02-13, DELIVER IN PERSON, SHIP, deposits believe. furiously regular p +135, 157510, 5056, 3, 33, 51727.83, 0.02, 0.00, N, O, 1996-01-03, 1995-11-21, 1996-02-01, TAKE BACK RETURN, MAIL, ptotes boost slowly care +135, 67005, 9512, 4, 34, 33048.00, 0.02, 0.03, N, O, 1996-01-12, 1996-01-19, 1996-02-05, NONE, TRUCK, counts doze against the blithely ironi +135, 136248, 1275, 5, 20, 25684.80, 0.01, 0.04, N, O, 1996-01-25, 1995-11-20, 1996-02-09, NONE, MAIL, theodolites. quickly p +135, 115000, 2534, 6, 13, 13195.00, 0.04, 0.02, N, O, 1995-11-12, 1995-12-22, 1995-11-17, NONE, FOB, nal ideas. final instr +160, 14785, 9788, 1, 36, 61192.08, 0.07, 0.01, N, O, 1997-03-11, 1997-03-11, 1997-03-20, COLLECT COD, MAIL, old ironic deposits are quickly abov +160, 86382, 8891, 2, 22, 30104.36, 0.00, 0.04, N, O, 1997-02-18, 1997-03-05, 1997-03-05, COLLECT COD, RAIL, ncies about the request +160, 20080, 5085, 3, 34, 34002.72, 0.01, 0.05, N, O, 1997-01-31, 1997-03-13, 1997-02-14, NONE, FOB, st sleep even gifts. dependencies along +161, 102810, 341, 1, 19, 34443.39, 0.01, 0.01, A, F, 1994-12-13, 1994-11-19, 1994-12-26, DELIVER IN PERSON, TRUCK, regular sheaves sleep along +162, 189288, 4325, 1, 2, 2754.56, 0.02, 0.01, N, O, 1995-09-02, 1995-06-17, 1995-09-08, COLLECT COD, FOB, es! final somas integrate +163, 167545, 5094, 1, 43, 69339.22, 0.01, 0.00, N, O, 1997-09-19, 1997-11-19, 1997-10-03, COLLECT COD, REG AIR, al bold dependencies wake. iron +163, 120702, 703, 2, 13, 22395.10, 0.01, 0.04, N, O, 1997-11-11, 1997-10-18, 1997-12-07, DELIVER IN PERSON, TRUCK, inal requests. even pinto beans hag +163, 36818, 9322, 3, 27, 47379.87, 0.04, 0.08, N, O, 1997-12-26, 1997-11-28, 1998-01-05, COLLECT COD, REG AIR, ously express dependen +163, 192642, 5162, 4, 5, 8673.20, 0.02, 0.00, N, O, 1997-11-17, 1997-10-09, 1997-12-05, DELIVER IN PERSON, TRUCK, must belie +163, 126090, 8603, 5, 12, 13393.08, 0.10, 0.00, N, O, 1997-12-18, 1997-10-26, 1997-12-22, COLLECT COD, TRUCK, ly blithe accounts cajole +163, 190825, 5864, 6, 20, 38316.40, 0.00, 0.07, N, O, 1997-09-27, 1997-11-15, 1997-10-07, TAKE BACK RETURN, FOB, tructions integrate b +164, 91309, 3819, 1, 26, 33807.80, 0.09, 0.04, A, F, 1993-01-04, 1992-11-21, 1993-01-07, NONE, RAIL, s. blithely special courts are blithel +164, 18488, 3491, 2, 24, 33755.52, 0.05, 0.05, R, F, 1992-12-22, 1992-11-27, 1993-01-06, NONE, AIR, side of the slyly unusual theodolites. f +164, 125509, 3046, 3, 38, 58311.00, 0.03, 0.06, R, F, 1992-12-04, 1992-11-23, 1993-01-02, TAKE BACK RETURN, AIR, counts cajole fluffily regular packages. b +164, 17526, 28, 4, 32, 46192.64, 0.05, 0.01, R, F, 1992-12-21, 1992-12-23, 1992-12-28, COLLECT COD, RAIL, ts wake again +164, 147505, 2534, 5, 43, 66757.50, 0.06, 0.01, R, F, 1992-11-26, 1993-01-03, 1992-12-08, COLLECT COD, RAIL, y carefully regular dep +164, 108896, 8897, 6, 27, 51432.03, 0.10, 0.04, R, F, 1992-12-23, 1993-01-16, 1993-01-10, DELIVER IN PERSON, AIR, ayers wake carefully a +164, 3037, 5538, 7, 23, 21620.69, 0.09, 0.04, A, F, 1992-11-03, 1992-12-02, 1992-11-12, NONE, REG AIR, ress packages haggle ideas. blithely spec +165, 33175, 8182, 1, 3, 3324.51, 0.01, 0.08, R, F, 1993-03-29, 1993-03-06, 1993-04-12, DELIVER IN PERSON, REG AIR, riously requests. depos +165, 161627, 9176, 2, 43, 72610.66, 0.08, 0.05, R, F, 1993-02-27, 1993-04-19, 1993-03-03, DELIVER IN PERSON, TRUCK, jole slyly according +165, 58520, 6036, 3, 15, 22177.80, 0.00, 0.05, R, F, 1993-04-10, 1993-03-29, 1993-05-01, COLLECT COD, SHIP, bold packages mainta +165, 139190, 4217, 4, 49, 60230.31, 0.07, 0.06, A, F, 1993-02-20, 1993-04-02, 1993-03-10, COLLECT COD, REG AIR, uses sleep slyly ruthlessly regular a +165, 155084, 7600, 5, 27, 30755.16, 0.01, 0.04, R, F, 1993-04-27, 1993-03-04, 1993-05-13, NONE, MAIL, around the ironic even orb +166, 64888, 9901, 1, 37, 68556.56, 0.09, 0.03, N, O, 1995-11-16, 1995-10-17, 1995-12-13, NONE, MAIL, lar frays wake blithely a +166, 166366, 6367, 2, 13, 18620.68, 0.09, 0.05, N, O, 1995-11-09, 1995-11-18, 1995-11-14, COLLECT COD, SHIP, fully above the blithely fina +166, 99652, 2162, 3, 41, 67717.65, 0.07, 0.03, N, O, 1995-11-13, 1995-11-07, 1995-12-08, COLLECT COD, FOB, hily along the blithely pending fo +166, 45027, 7532, 4, 8, 7776.16, 0.05, 0.02, N, O, 1995-12-30, 1995-11-29, 1996-01-29, DELIVER IN PERSON, RAIL, e carefully bold +167, 101171, 1172, 1, 28, 32820.76, 0.06, 0.01, R, F, 1993-02-19, 1993-02-16, 1993-03-03, DELIVER IN PERSON, TRUCK, sly during the u +167, 171555, 4073, 2, 27, 43916.85, 0.09, 0.00, R, F, 1993-05-01, 1993-03-31, 1993-05-31, TAKE BACK RETURN, FOB, eans affix furiously-- packages +192, 97017, 2036, 1, 23, 23322.23, 0.00, 0.00, N, O, 1998-02-05, 1998-02-06, 1998-03-07, TAKE BACK RETURN, AIR, ly pending theodolites haggle quickly fluf +192, 161368, 8917, 2, 20, 28587.20, 0.07, 0.01, N, O, 1998-03-13, 1998-02-02, 1998-03-31, TAKE BACK RETURN, REG AIR, tes. carefu +192, 110252, 5275, 3, 15, 18933.75, 0.09, 0.01, N, O, 1998-01-30, 1998-02-10, 1998-02-23, TAKE BACK RETURN, TRUCK, he ironic requests haggle about +192, 196400, 3958, 4, 2, 2992.80, 0.06, 0.02, N, O, 1998-03-06, 1998-02-03, 1998-03-24, COLLECT COD, SHIP, s. dependencies nag furiously alongside +192, 82915, 7932, 5, 25, 47447.75, 0.02, 0.03, N, O, 1998-02-15, 1998-01-11, 1998-03-17, COLLECT COD, TRUCK, . carefully regular +192, 141003, 3518, 6, 45, 46980.00, 0.00, 0.05, N, O, 1998-03-11, 1998-01-09, 1998-04-03, NONE, MAIL, equests. ideas sleep idea +193, 92638, 5148, 1, 9, 14675.67, 0.06, 0.06, A, F, 1993-09-17, 1993-10-08, 1993-09-30, COLLECT COD, TRUCK, against the fluffily regular d +193, 153954, 1500, 2, 15, 30119.25, 0.02, 0.07, R, F, 1993-11-22, 1993-10-09, 1993-12-05, TAKE BACK RETURN, SHIP, ffily. regular packages d +193, 93878, 6388, 3, 23, 43053.01, 0.06, 0.05, A, F, 1993-08-21, 1993-10-11, 1993-09-02, DELIVER IN PERSON, TRUCK, ly even accounts wake blithely bold +194, 2594, 5095, 1, 17, 25442.03, 0.05, 0.04, R, F, 1992-05-24, 1992-05-22, 1992-05-30, COLLECT COD, AIR, regular deposi +194, 183523, 6042, 2, 1, 1606.52, 0.04, 0.06, R, F, 1992-04-30, 1992-05-18, 1992-05-23, NONE, REG AIR, regular theodolites. regular iron +194, 65994, 3513, 3, 13, 25479.87, 0.08, 0.08, A, F, 1992-05-07, 1992-06-18, 1992-05-10, NONE, AIR, about the blit +194, 145146, 5147, 4, 36, 42881.04, 0.00, 0.05, R, F, 1992-05-21, 1992-05-18, 1992-05-27, TAKE BACK RETURN, RAIL, pecial packages wake after the slyly r +194, 56176, 1187, 5, 8, 9057.36, 0.04, 0.00, R, F, 1992-07-06, 1992-06-25, 1992-07-11, COLLECT COD, FOB, uriously unusual excuses +194, 148984, 1499, 6, 16, 32527.68, 0.06, 0.03, A, F, 1992-05-14, 1992-06-14, 1992-05-21, TAKE BACK RETURN, TRUCK, y regular requests. furious +194, 167828, 345, 7, 21, 39812.22, 0.02, 0.01, R, F, 1992-05-06, 1992-05-20, 1992-05-07, COLLECT COD, REG AIR, accounts detect quickly dogged +195, 84590, 9607, 1, 6, 9447.54, 0.04, 0.02, A, F, 1994-01-09, 1994-03-27, 1994-01-28, COLLECT COD, REG AIR, y even deposits haggle carefully. bli +195, 93847, 1375, 2, 41, 75474.44, 0.05, 0.07, A, F, 1994-02-24, 1994-02-11, 1994-03-20, NONE, TRUCK, rts detect in place of t +195, 85446, 7955, 3, 34, 48668.96, 0.08, 0.08, R, F, 1994-01-31, 1994-02-11, 1994-02-12, NONE, TRUCK, cajole furiously bold i +195, 85442, 7951, 4, 41, 58525.04, 0.06, 0.04, R, F, 1994-03-14, 1994-03-13, 1994-04-09, COLLECT COD, RAIL, ggle fluffily foxes. fluffily ironic ex +196, 135052, 79, 1, 19, 20653.95, 0.03, 0.02, R, F, 1993-04-17, 1993-05-27, 1993-04-30, NONE, SHIP, sts maintain foxes. furiously regular p +196, 9852, 2353, 2, 15, 26427.75, 0.03, 0.04, A, F, 1993-07-05, 1993-05-08, 1993-07-06, TAKE BACK RETURN, SHIP, s accounts. furio +197, 98494, 1004, 1, 39, 58207.11, 0.02, 0.04, N, O, 1995-07-21, 1995-07-01, 1995-08-14, TAKE BACK RETURN, AIR, press accounts. daringly sp +197, 177103, 9621, 2, 8, 9440.80, 0.09, 0.02, A, F, 1995-04-17, 1995-07-01, 1995-04-27, DELIVER IN PERSON, SHIP, y blithely even deposits. blithely fina +197, 155829, 8345, 3, 17, 32041.94, 0.06, 0.02, N, O, 1995-08-02, 1995-06-23, 1995-08-03, COLLECT COD, REG AIR, ts. careful +197, 17936, 2939, 4, 25, 46348.25, 0.04, 0.01, N, F, 1995-06-13, 1995-05-23, 1995-06-24, TAKE BACK RETURN, FOB, s-- quickly final accounts +197, 41466, 3971, 5, 14, 19704.44, 0.09, 0.01, R, F, 1995-05-08, 1995-05-24, 1995-05-12, TAKE BACK RETURN, RAIL, use slyly slyly silent depo +197, 105880, 901, 6, 1, 1885.88, 0.07, 0.05, N, O, 1995-07-15, 1995-06-21, 1995-08-11, COLLECT COD, RAIL, even thin dependencies sno +198, 56061, 6062, 1, 33, 33562.98, 0.07, 0.02, N, O, 1998-01-05, 1998-03-20, 1998-01-10, TAKE BACK RETURN, TRUCK, carefully caref +198, 15229, 7731, 2, 20, 22884.40, 0.03, 0.00, N, O, 1998-01-15, 1998-03-31, 1998-01-25, DELIVER IN PERSON, FOB, carefully final escapades a +198, 148058, 3087, 3, 15, 16590.75, 0.04, 0.02, N, O, 1998-04-12, 1998-02-26, 1998-04-15, COLLECT COD, MAIL, es. quickly pending deposits s +198, 10371, 2873, 4, 35, 44847.95, 0.08, 0.02, N, O, 1998-02-27, 1998-03-23, 1998-03-14, TAKE BACK RETURN, RAIL, ests nod quickly furiously sly pinto be +198, 101952, 1953, 5, 33, 64480.35, 0.02, 0.01, N, O, 1998-03-22, 1998-03-12, 1998-04-14, DELIVER IN PERSON, SHIP, ending foxes acr +199, 132072, 9612, 1, 50, 55203.50, 0.02, 0.00, N, O, 1996-06-12, 1996-06-03, 1996-07-04, DELIVER IN PERSON, MAIL, essly regular ideas boost sly +199, 133998, 3999, 2, 30, 60959.70, 0.08, 0.05, N, O, 1996-03-27, 1996-05-29, 1996-04-14, NONE, TRUCK, ilent packages doze quickly. thinly +224, 150111, 112, 1, 16, 18577.76, 0.04, 0.00, A, F, 1994-08-01, 1994-07-30, 1994-08-27, DELIVER IN PERSON, MAIL, y unusual foxes +224, 108609, 1120, 2, 34, 54998.40, 0.04, 0.08, R, F, 1994-07-13, 1994-08-25, 1994-07-31, COLLECT COD, TRUCK, carefully. final platelets +224, 189967, 7522, 3, 41, 84335.36, 0.07, 0.04, A, F, 1994-09-01, 1994-09-15, 1994-09-02, TAKE BACK RETURN, SHIP, after the furiou +224, 166377, 1410, 4, 12, 17320.44, 0.08, 0.06, R, F, 1994-10-12, 1994-08-29, 1994-10-20, DELIVER IN PERSON, MAIL, uriously regular packages. slyly fina +224, 93857, 8876, 5, 45, 83288.25, 0.07, 0.07, R, F, 1994-08-14, 1994-09-02, 1994-08-27, COLLECT COD, AIR, leep furiously regular requests. furiousl +224, 50010, 7526, 6, 4, 3840.04, 0.02, 0.00, R, F, 1994-09-08, 1994-08-24, 1994-10-04, DELIVER IN PERSON, FOB, tructions +225, 171925, 1926, 1, 4, 7987.68, 0.09, 0.07, N, O, 1995-08-05, 1995-08-19, 1995-09-03, TAKE BACK RETURN, SHIP, ng the ironic packages. asymptotes among +225, 130565, 8105, 2, 3, 4786.68, 0.00, 0.08, N, O, 1995-07-25, 1995-07-08, 1995-08-17, DELIVER IN PERSON, REG AIR, fluffily about the carefully bold a +225, 198212, 3251, 3, 45, 58959.45, 0.06, 0.01, N, O, 1995-08-17, 1995-08-20, 1995-08-30, TAKE BACK RETURN, FOB, the slyly even platelets use aro +225, 146071, 8586, 4, 24, 26809.68, 0.00, 0.06, N, O, 1995-09-23, 1995-08-05, 1995-10-16, COLLECT COD, MAIL, ironic accounts are final account +225, 7589, 5090, 5, 31, 46393.98, 0.04, 0.06, N, O, 1995-06-21, 1995-07-24, 1995-07-04, TAKE BACK RETURN, FOB, special platelets. quickly r +225, 131835, 9375, 6, 12, 22401.96, 0.00, 0.00, A, F, 1995-06-04, 1995-07-15, 1995-06-08, COLLECT COD, MAIL, unusual requests. bus +225, 141233, 8776, 7, 44, 56066.12, 0.10, 0.06, N, O, 1995-09-22, 1995-08-16, 1995-10-22, NONE, REG AIR, leep slyly +226, 96909, 9419, 1, 4, 7623.60, 0.00, 0.00, R, F, 1993-03-31, 1993-04-30, 1993-04-10, NONE, TRUCK, c foxes integrate carefully against th +226, 137802, 5342, 2, 46, 84630.80, 0.06, 0.01, A, F, 1993-07-06, 1993-04-24, 1993-07-13, COLLECT COD, FOB, s. carefully bold accounts cajol +226, 37309, 4819, 3, 35, 43620.50, 0.09, 0.03, A, F, 1993-03-31, 1993-05-18, 1993-04-01, NONE, RAIL, osits cajole. final even foxes a +226, 40633, 8146, 4, 45, 70813.35, 0.10, 0.02, R, F, 1993-04-17, 1993-05-27, 1993-05-11, DELIVER IN PERSON, AIR, carefully pending pi +226, 117956, 5490, 5, 2, 3947.90, 0.07, 0.02, R, F, 1993-03-26, 1993-04-13, 1993-04-20, TAKE BACK RETURN, SHIP, al platelets. express somas +226, 82937, 7954, 6, 48, 92156.64, 0.02, 0.00, A, F, 1993-06-11, 1993-05-15, 1993-06-19, NONE, REG AIR, efully silent packages. final deposit +226, 117961, 5495, 7, 14, 27705.44, 0.09, 0.00, R, F, 1993-05-20, 1993-06-05, 1993-05-27, COLLECT COD, MAIL, ep carefully regular accounts. ironic +227, 165335, 2884, 1, 19, 26606.27, 0.05, 0.06, N, O, 1995-12-10, 1996-01-30, 1995-12-26, NONE, RAIL, s cajole furiously a +227, 174102, 1654, 2, 24, 28226.40, 0.07, 0.07, N, O, 1996-02-03, 1995-12-24, 1996-02-12, COLLECT COD, SHIP, uses across the blithe dependencies cajol +228, 4039, 6540, 1, 3, 2829.09, 0.10, 0.08, A, F, 1993-05-20, 1993-04-08, 1993-05-26, DELIVER IN PERSON, SHIP, ckages. sly +229, 83580, 8597, 1, 20, 31271.60, 0.02, 0.03, R, F, 1994-01-11, 1994-01-31, 1994-01-26, DELIVER IN PERSON, REG AIR, le. instructions use across the quickly fin +229, 128904, 8905, 2, 29, 56054.10, 0.07, 0.00, A, F, 1994-03-15, 1994-03-02, 1994-03-26, COLLECT COD, SHIP, s final request +229, 78526, 8527, 3, 28, 42126.56, 0.02, 0.02, R, F, 1994-02-10, 1994-02-02, 1994-03-10, DELIVER IN PERSON, FOB, final regular requests. platel +229, 176948, 1983, 4, 3, 6074.82, 0.02, 0.08, R, F, 1994-03-22, 1994-03-24, 1994-04-04, DELIVER IN PERSON, REG AIR, posits. furiously regular theodol +229, 155180, 211, 5, 33, 40760.94, 0.03, 0.06, R, F, 1994-03-25, 1994-02-11, 1994-04-13, NONE, FOB, deposits; bold ruthless theodolites +229, 105393, 7904, 6, 29, 40553.31, 0.04, 0.00, R, F, 1994-01-14, 1994-02-16, 1994-01-22, NONE, FOB, uriously pending +230, 185863, 900, 1, 46, 89647.56, 0.09, 0.00, R, F, 1994-02-03, 1994-01-15, 1994-02-23, TAKE BACK RETURN, SHIP, old packages ha +230, 194908, 7428, 2, 6, 12017.40, 0.03, 0.08, A, F, 1994-01-26, 1994-01-25, 1994-02-13, NONE, REG AIR, sleep furiously about the p +230, 7367, 4868, 3, 1, 1274.36, 0.07, 0.06, R, F, 1994-01-22, 1994-01-03, 1994-02-05, TAKE BACK RETURN, RAIL, blithely unusual dolphins. bold ex +230, 9164, 1665, 4, 44, 47219.04, 0.08, 0.06, R, F, 1994-02-09, 1994-01-18, 1994-03-11, NONE, MAIL, deposits integrate slyly sile +230, 18923, 6427, 5, 8, 14735.36, 0.09, 0.06, R, F, 1993-11-03, 1994-01-20, 1993-11-11, TAKE BACK RETURN, TRUCK, g the instructions. fluffil +230, 33927, 1437, 6, 8, 14887.36, 0.00, 0.05, R, F, 1993-11-21, 1994-01-05, 1993-12-19, TAKE BACK RETURN, FOB, nal ideas. silent reg +231, 158356, 8357, 1, 16, 22629.60, 0.04, 0.08, R, F, 1994-11-20, 1994-10-29, 1994-12-17, TAKE BACK RETURN, AIR, e furiously ironic pinto beans. +231, 83359, 884, 2, 46, 61748.10, 0.04, 0.05, R, F, 1994-12-13, 1994-12-02, 1994-12-14, DELIVER IN PERSON, SHIP, affix blithely. bold requests among the f +231, 198124, 644, 3, 50, 61106.00, 0.09, 0.01, A, F, 1994-12-11, 1994-12-14, 1994-12-13, NONE, RAIL, onic packages haggle fluffily a +231, 56760, 6761, 4, 31, 53219.56, 0.08, 0.02, A, F, 1994-11-05, 1994-12-27, 1994-11-30, TAKE BACK RETURN, SHIP, iously special decoys wake q +256, 88233, 742, 1, 22, 26867.06, 0.09, 0.02, R, F, 1994-01-12, 1993-12-28, 1994-01-26, COLLECT COD, FOB, ke quickly ironic ironic deposits. reg +256, 118399, 3422, 2, 40, 56695.60, 0.10, 0.01, A, F, 1993-11-30, 1993-12-13, 1993-12-02, NONE, FOB, nal theodolites. deposits cajole s +256, 129111, 4136, 3, 45, 51304.95, 0.02, 0.08, R, F, 1994-01-14, 1994-01-17, 1994-02-10, COLLECT COD, SHIP, grouches. ideas wake quickly ar +257, 146229, 6230, 1, 7, 8926.54, 0.05, 0.02, N, O, 1998-06-18, 1998-05-15, 1998-06-27, COLLECT COD, FOB, ackages sleep bold realms. f +258, 106194, 3725, 1, 8, 9601.52, 0.00, 0.07, R, F, 1994-01-20, 1994-03-21, 1994-02-09, NONE, REG AIR, ully about the fluffily silent dependencies +258, 196119, 3677, 2, 40, 48604.40, 0.10, 0.01, A, F, 1994-03-13, 1994-02-23, 1994-04-05, DELIVER IN PERSON, FOB, silent frets nod daringly busy bold +258, 161762, 1763, 3, 45, 82069.20, 0.07, 0.07, R, F, 1994-03-04, 1994-02-13, 1994-03-30, DELIVER IN PERSON, TRUCK, regular excuses-- fluffily ruthl +258, 132912, 5426, 4, 31, 60292.21, 0.02, 0.05, A, F, 1994-04-20, 1994-03-20, 1994-04-28, COLLECT COD, REG AIR, slyly blithely special mul +258, 35959, 8463, 5, 25, 47373.75, 0.08, 0.02, A, F, 1994-04-13, 1994-02-26, 1994-04-29, TAKE BACK RETURN, TRUCK, leep pending packages. +258, 146467, 8982, 6, 36, 54484.56, 0.09, 0.04, A, F, 1994-01-11, 1994-03-04, 1994-01-18, DELIVER IN PERSON, AIR, nic asymptotes. slyly silent r +259, 98779, 8780, 1, 14, 24888.78, 0.00, 0.08, A, F, 1993-12-17, 1993-12-09, 1993-12-31, COLLECT COD, SHIP, ons against the express acco +259, 161982, 4499, 2, 14, 28615.72, 0.03, 0.05, R, F, 1993-11-10, 1993-11-20, 1993-11-17, DELIVER IN PERSON, FOB, ully even regul +259, 23514, 3515, 3, 42, 60375.42, 0.09, 0.00, R, F, 1993-10-20, 1993-11-18, 1993-11-12, NONE, TRUCK, the slyly ironic pinto beans. fi +259, 195335, 2893, 4, 3, 4290.99, 0.08, 0.06, R, F, 1993-10-04, 1993-11-07, 1993-10-14, TAKE BACK RETURN, SHIP, ng slyly at the accounts. +259, 192201, 7240, 5, 6, 7759.20, 0.00, 0.05, R, F, 1993-12-05, 1993-12-22, 1993-12-21, COLLECT COD, TRUCK, requests sleep +260, 155887, 5888, 1, 50, 97144.00, 0.07, 0.08, N, O, 1997-03-24, 1997-02-09, 1997-04-20, TAKE BACK RETURN, REG AIR, c deposits +260, 182736, 2737, 2, 26, 47286.98, 0.02, 0.07, N, O, 1996-12-12, 1997-02-06, 1996-12-15, NONE, TRUCK, ld theodolites boost fl +260, 41222, 8735, 3, 27, 31406.94, 0.05, 0.08, N, O, 1997-03-23, 1997-02-15, 1997-04-22, TAKE BACK RETURN, RAIL, ions according to the +260, 5337, 338, 4, 29, 36027.57, 0.10, 0.06, N, O, 1997-03-15, 1997-01-14, 1997-04-13, NONE, MAIL, fluffily even asymptotes. express wa +260, 95286, 305, 5, 44, 56376.32, 0.01, 0.05, N, O, 1997-03-26, 1997-02-03, 1997-04-19, DELIVER IN PERSON, MAIL, above the blithely ironic instr +261, 1349, 6350, 1, 34, 42511.56, 0.05, 0.08, R, F, 1993-08-18, 1993-09-24, 1993-08-20, COLLECT COD, REG AIR, c packages. asymptotes da +261, 65662, 5663, 2, 20, 32553.20, 0.00, 0.06, R, F, 1993-10-21, 1993-08-02, 1993-11-04, DELIVER IN PERSON, RAIL, ites hinder +261, 173959, 8994, 3, 28, 56922.60, 0.08, 0.03, R, F, 1993-07-24, 1993-08-20, 1993-08-05, COLLECT COD, AIR, ironic packages nag slyly. carefully fin +261, 118455, 967, 4, 49, 72199.05, 0.04, 0.05, R, F, 1993-09-12, 1993-08-31, 1993-10-07, COLLECT COD, SHIP, ions. bold accounts +261, 60469, 7988, 5, 49, 70043.54, 0.01, 0.08, A, F, 1993-09-29, 1993-09-08, 1993-10-01, COLLECT COD, SHIP, pinto beans haggle slyly furiously pending +261, 96989, 9499, 6, 20, 39719.60, 0.06, 0.06, A, F, 1993-10-15, 1993-09-05, 1993-11-07, NONE, AIR, ing to the special ironic deposi +262, 191186, 1187, 1, 39, 49810.02, 0.01, 0.05, N, O, 1996-01-15, 1996-02-18, 1996-01-28, COLLECT COD, RAIL, usual regular requests +262, 60074, 7593, 2, 33, 34124.31, 0.09, 0.03, N, O, 1996-03-10, 1996-01-31, 1996-03-27, TAKE BACK RETURN, AIR, atelets sleep furiously. requests cajole. b +262, 58695, 6211, 3, 35, 57879.15, 0.05, 0.08, N, O, 1996-03-12, 1996-02-14, 1996-04-11, COLLECT COD, MAIL, lites cajole along the pending packag +263, 23960, 6463, 1, 22, 41447.12, 0.06, 0.08, R, F, 1994-08-24, 1994-06-20, 1994-09-09, NONE, FOB, efully express fo +263, 84557, 9574, 2, 9, 13873.95, 0.08, 0.00, A, F, 1994-07-21, 1994-07-16, 1994-08-08, TAKE BACK RETURN, TRUCK, lms wake bl +263, 142891, 434, 3, 50, 96694.50, 0.06, 0.04, R, F, 1994-08-18, 1994-07-31, 1994-08-22, NONE, TRUCK, re the packages. special +288, 50641, 8157, 1, 31, 49340.84, 0.00, 0.03, N, O, 1997-03-17, 1997-04-28, 1997-04-06, TAKE BACK RETURN, AIR, instructions wa +288, 116386, 8898, 2, 49, 68716.62, 0.08, 0.05, N, O, 1997-04-19, 1997-05-19, 1997-05-18, TAKE BACK RETURN, TRUCK, ic excuses sleep always spe +288, 98833, 8834, 3, 36, 65945.88, 0.02, 0.02, N, O, 1997-02-22, 1997-05-07, 1997-03-07, TAKE BACK RETURN, TRUCK, yly pending excu +288, 78406, 8407, 4, 19, 26303.60, 0.07, 0.07, N, O, 1997-03-14, 1997-04-04, 1997-03-26, NONE, MAIL, deposits. blithely quick courts ar +288, 161894, 6927, 5, 31, 60632.59, 0.10, 0.04, N, O, 1997-05-29, 1997-04-24, 1997-06-20, TAKE BACK RETURN, RAIL, ns. fluffily +289, 173280, 832, 1, 25, 33832.00, 0.07, 0.05, N, O, 1997-03-18, 1997-05-05, 1997-04-15, DELIVER IN PERSON, FOB, out the quickly bold theodol +289, 111800, 9334, 2, 6, 10870.80, 0.06, 0.05, N, O, 1997-02-18, 1997-05-08, 1997-03-19, DELIVER IN PERSON, SHIP, d packages use fluffily furiously diff --git a/presto-example-http/src/test/resources/example-data/lineitem-2.csv b/presto-example-http/src/test/resources/example-data/lineitem-2.csv new file mode 100644 index 00000000..460bd13d --- /dev/null +++ b/presto-example-http/src/test/resources/example-data/lineitem-2.csv @@ -0,0 +1,486 @@ +289, 16996, 1999, 3, 44, 84171.56, 0.10, 0.08, N, O, 1997-06-05, 1997-04-20, 1997-07-02, COLLECT COD, MAIL, ly ironic foxes. asymptotes +289, 39439, 1943, 4, 48, 66164.64, 0.01, 0.08, N, O, 1997-03-14, 1997-03-30, 1997-03-24, DELIVER IN PERSON, RAIL, sits cajole. bold pinto beans x-ray fl +289, 46285, 8790, 5, 13, 16006.64, 0.10, 0.03, N, O, 1997-06-08, 1997-04-06, 1997-06-18, TAKE BACK RETURN, REG AIR, ts. quickly bold deposits alongside +290, 5351, 352, 1, 35, 43972.25, 0.01, 0.02, R, F, 1994-04-01, 1994-02-05, 1994-04-27, NONE, MAIL, ove the final foxes detect slyly fluffily +290, 128923, 1436, 2, 2, 3903.84, 0.05, 0.04, A, F, 1994-01-30, 1994-02-13, 1994-02-21, TAKE BACK RETURN, TRUCK, . permanently furious reques +290, 1888, 4389, 3, 5, 8949.40, 0.03, 0.05, A, F, 1994-01-19, 1994-02-24, 1994-01-27, NONE, MAIL, ans integrate. requests sleep. fur +290, 123741, 6254, 4, 23, 40589.02, 0.05, 0.08, R, F, 1994-03-14, 1994-02-21, 1994-04-09, NONE, AIR, refully unusual packages. +291, 122565, 102, 1, 21, 33338.76, 0.05, 0.07, A, F, 1994-05-26, 1994-05-10, 1994-06-23, COLLECT COD, TRUCK, y quickly regular theodolites. final t +291, 137316, 7317, 2, 19, 25712.89, 0.08, 0.02, R, F, 1994-06-14, 1994-04-25, 1994-06-19, NONE, REG AIR, e. ruthlessly final accounts after the +291, 60874, 5887, 3, 30, 55046.10, 0.10, 0.02, R, F, 1994-03-22, 1994-04-30, 1994-03-24, DELIVER IN PERSON, FOB, fluffily regular deposits. quickl +292, 153561, 3562, 1, 8, 12916.48, 0.10, 0.03, R, F, 1992-02-18, 1992-03-30, 1992-03-18, DELIVER IN PERSON, RAIL, sily bold deposits alongside of the ex +292, 99249, 9250, 2, 24, 29957.76, 0.08, 0.04, R, F, 1992-03-24, 1992-03-06, 1992-04-20, COLLECT COD, TRUCK, bold pending theodolites u +293, 8960, 6461, 1, 14, 26165.44, 0.02, 0.05, R, F, 1992-10-19, 1992-12-23, 1992-11-10, DELIVER IN PERSON, SHIP, es. packages above the +293, 186406, 6407, 2, 11, 16416.40, 0.10, 0.04, R, F, 1992-12-24, 1992-12-01, 1993-01-12, COLLECT COD, MAIL, affix carefully quickly special idea +293, 117267, 4801, 3, 13, 16695.38, 0.04, 0.02, A, F, 1992-12-17, 1992-12-26, 1992-12-22, COLLECT COD, RAIL, wake after the quickly even deposits. bli +294, 59620, 7136, 1, 31, 48968.22, 0.00, 0.01, R, F, 1993-08-06, 1993-08-19, 1993-08-13, TAKE BACK RETURN, AIR, le fluffily along the quick +295, 197507, 27, 1, 29, 46530.50, 0.02, 0.07, A, F, 1994-11-09, 1994-12-08, 1994-12-07, COLLECT COD, MAIL, inst the carefully ironic pinto beans. blit +295, 91344, 8872, 2, 26, 34718.84, 0.04, 0.03, R, F, 1994-12-13, 1994-11-30, 1995-01-06, DELIVER IN PERSON, AIR, ts above the slyly regular requests x-ray q +295, 15283, 7785, 3, 8, 9586.24, 0.10, 0.07, R, F, 1995-01-13, 1994-11-17, 1995-01-25, NONE, TRUCK, final instructions h +295, 60621, 3128, 4, 26, 41122.12, 0.10, 0.04, A, F, 1995-01-12, 1994-11-22, 1995-01-22, DELIVER IN PERSON, MAIL, carefully iron +320, 4415, 1916, 1, 30, 39582.30, 0.05, 0.01, N, O, 1997-12-04, 1998-01-21, 1997-12-13, NONE, RAIL, ironic final accounts wake quick de +320, 192158, 4678, 2, 13, 16251.95, 0.03, 0.00, N, O, 1997-12-16, 1997-12-26, 1997-12-17, TAKE BACK RETURN, AIR, he furiously regular pinto beans. car +321, 318, 7819, 1, 21, 25584.51, 0.01, 0.08, A, F, 1993-07-18, 1993-04-24, 1993-08-13, TAKE BACK RETURN, REG AIR, hockey players sleep slyly sl +321, 140433, 5462, 2, 41, 60410.63, 0.08, 0.07, R, F, 1993-06-21, 1993-06-07, 1993-07-09, NONE, REG AIR, special packages shall have to doze blit +322, 152499, 7530, 1, 12, 18617.88, 0.08, 0.07, A, F, 1992-06-29, 1992-05-30, 1992-07-11, NONE, AIR, ular theodolites promise qu +322, 43662, 3663, 2, 48, 77071.68, 0.02, 0.07, A, F, 1992-06-11, 1992-06-16, 1992-06-26, COLLECT COD, RAIL, dolites detect qu +322, 12673, 177, 3, 20, 31713.40, 0.04, 0.01, R, F, 1992-04-26, 1992-05-04, 1992-05-22, DELIVER IN PERSON, MAIL, ckly toward +322, 183246, 5765, 4, 10, 13292.40, 0.06, 0.03, R, F, 1992-04-12, 1992-05-13, 1992-04-14, DELIVER IN PERSON, AIR, deposits grow slyly according to th +322, 11605, 9109, 5, 35, 53081.00, 0.07, 0.06, A, F, 1992-07-17, 1992-05-03, 1992-08-14, TAKE BACK RETURN, RAIL, egular accounts cajole carefully. even d +322, 33310, 8317, 6, 3, 3729.93, 0.08, 0.05, A, F, 1992-07-03, 1992-05-10, 1992-07-28, NONE, AIR, ending ironic deposits along the blith +322, 37435, 4945, 7, 5, 6862.15, 0.01, 0.02, A, F, 1992-04-15, 1992-05-12, 1992-04-26, COLLECT COD, REG AIR, special grouches sleep quickly instructio +323, 163628, 1177, 1, 50, 84581.00, 0.05, 0.04, A, F, 1994-04-20, 1994-04-25, 1994-05-12, DELIVER IN PERSON, REG AIR, cial requests +323, 95136, 7646, 2, 18, 20360.34, 0.06, 0.07, R, F, 1994-04-13, 1994-06-02, 1994-05-10, DELIVER IN PERSON, TRUCK, posits cajole furiously pinto beans. +323, 142725, 2726, 3, 9, 15909.48, 0.07, 0.04, A, F, 1994-06-26, 1994-06-10, 1994-07-13, COLLECT COD, TRUCK, nic accounts. regular regular pack +324, 199475, 4514, 1, 26, 40936.22, 0.07, 0.01, R, F, 1992-04-19, 1992-05-28, 1992-05-12, DELIVER IN PERSON, RAIL, ross the slyly regular s +325, 158791, 6337, 1, 34, 62892.86, 0.09, 0.04, A, F, 1993-10-28, 1993-12-13, 1993-11-17, TAKE BACK RETURN, MAIL, ly bold deposits. always iron +325, 185139, 7658, 2, 5, 6120.65, 0.07, 0.08, A, F, 1994-01-02, 1994-01-05, 1994-01-04, TAKE BACK RETURN, MAIL, theodolites. +325, 18788, 1290, 3, 35, 59737.30, 0.07, 0.07, A, F, 1993-12-06, 1994-01-03, 1993-12-26, DELIVER IN PERSON, REG AIR, packages wa +326, 179094, 4129, 1, 41, 48096.69, 0.06, 0.03, N, O, 1995-08-30, 1995-07-09, 1995-09-12, DELIVER IN PERSON, TRUCK, ily quickly bold ideas. +326, 19480, 1982, 2, 38, 53180.24, 0.02, 0.08, N, O, 1995-09-12, 1995-08-23, 1995-09-14, COLLECT COD, RAIL, es sleep slyly. carefully regular inst +326, 183739, 8776, 3, 25, 45568.25, 0.03, 0.04, N, O, 1995-08-03, 1995-07-27, 1995-08-16, NONE, AIR, ily furiously unusual accounts. +326, 84836, 9853, 4, 5, 9104.15, 0.03, 0.08, N, O, 1995-07-29, 1995-07-13, 1995-08-12, NONE, REG AIR, deas sleep according to the sometimes spe +326, 34543, 9550, 5, 31, 45803.74, 0.04, 0.08, N, O, 1995-09-27, 1995-07-06, 1995-10-22, NONE, TRUCK, cies sleep quick +326, 156712, 4258, 6, 41, 72517.11, 0.02, 0.00, N, O, 1995-07-05, 1995-07-23, 1995-07-20, TAKE BACK RETURN, REG AIR, to beans wake before the furiously re +326, 42134, 4639, 7, 47, 50578.11, 0.04, 0.04, N, O, 1995-09-16, 1995-07-04, 1995-10-04, NONE, REG AIR, special accounts sleep +327, 143503, 1046, 1, 16, 24744.00, 0.03, 0.01, N, O, 1995-07-05, 1995-06-07, 1995-07-09, TAKE BACK RETURN, TRUCK, cial ideas sleep af +327, 41715, 4220, 2, 9, 14910.39, 0.09, 0.05, A, F, 1995-05-24, 1995-07-11, 1995-06-05, NONE, AIR, asymptotes are fu +352, 63762, 3763, 1, 17, 29337.92, 0.07, 0.05, R, F, 1994-06-02, 1994-05-31, 1994-06-29, NONE, FOB, pending deposits sleep furiously +353, 119305, 4328, 1, 41, 54296.30, 0.00, 0.06, A, F, 1994-03-25, 1994-03-31, 1994-03-30, DELIVER IN PERSON, AIR, refully final theodoli +353, 147542, 7543, 2, 29, 46096.66, 0.09, 0.00, A, F, 1994-01-11, 1994-03-19, 1994-02-09, COLLECT COD, FOB, ctions impr +353, 134318, 1858, 3, 12, 16227.72, 0.06, 0.01, R, F, 1994-01-02, 1994-03-26, 1994-01-19, DELIVER IN PERSON, RAIL, g deposits cajole +353, 77071, 2086, 4, 46, 48211.22, 0.00, 0.04, A, F, 1994-04-14, 1994-01-31, 1994-05-05, DELIVER IN PERSON, FOB, ironic dolphins +353, 116803, 1826, 5, 9, 16378.20, 0.02, 0.02, A, F, 1994-03-15, 1994-03-20, 1994-03-18, TAKE BACK RETURN, RAIL, ual accounts! carefu +353, 102699, 2700, 6, 39, 66365.91, 0.02, 0.05, A, F, 1994-01-15, 1994-03-30, 1994-02-01, NONE, MAIL, losely quickly even accounts. c +354, 49480, 1985, 1, 14, 20012.72, 0.08, 0.04, N, O, 1996-04-12, 1996-06-03, 1996-05-08, NONE, SHIP, quickly regular grouches will eat. careful +354, 193864, 1422, 2, 24, 46988.64, 0.01, 0.01, N, O, 1996-05-08, 1996-05-17, 1996-06-07, DELIVER IN PERSON, AIR, y silent requests. regular even accounts +354, 58125, 8126, 3, 50, 54156.00, 0.08, 0.05, N, O, 1996-03-21, 1996-05-20, 1996-04-04, COLLECT COD, TRUCK, to beans s +354, 106672, 4203, 4, 7, 11750.69, 0.06, 0.01, N, O, 1996-05-07, 1996-04-18, 1996-05-24, NONE, MAIL, ously idly ironic accounts-- quickl +354, 30527, 528, 5, 18, 26235.36, 0.04, 0.08, N, O, 1996-03-31, 1996-05-13, 1996-04-27, DELIVER IN PERSON, RAIL, about the carefully unusual +354, 61082, 3589, 6, 36, 37550.88, 0.03, 0.02, N, O, 1996-03-19, 1996-05-29, 1996-03-30, NONE, AIR, onic requests thrash bold g +354, 4660, 9661, 7, 14, 21905.24, 0.01, 0.07, N, O, 1996-07-06, 1996-06-08, 1996-07-10, TAKE BACK RETURN, MAIL, t thinly above the ironic +355, 113959, 8982, 1, 31, 61161.45, 0.09, 0.07, A, F, 1994-07-13, 1994-08-18, 1994-07-18, DELIVER IN PERSON, FOB, y unusual ironic +355, 96030, 3558, 2, 41, 42067.23, 0.05, 0.00, A, F, 1994-08-15, 1994-07-19, 1994-09-06, DELIVER IN PERSON, TRUCK, deposits. carefully r +356, 45214, 5215, 1, 4, 4636.84, 0.10, 0.01, A, F, 1994-07-28, 1994-08-01, 1994-08-04, DELIVER IN PERSON, REG AIR, the dependencies nod unusual final ac +356, 107463, 2484, 2, 48, 70582.08, 0.02, 0.03, R, F, 1994-08-12, 1994-07-31, 1994-08-26, NONE, FOB, unusual packages. furiously +356, 118002, 514, 3, 35, 35700.00, 0.08, 0.07, R, F, 1994-10-14, 1994-07-31, 1994-10-23, COLLECT COD, TRUCK, s. unusual final +356, 55342, 353, 4, 41, 53190.94, 0.07, 0.05, A, F, 1994-09-28, 1994-09-20, 1994-10-07, COLLECT COD, SHIP, according to the express foxes will +356, 124271, 1808, 5, 37, 47924.99, 0.05, 0.03, A, F, 1994-07-15, 1994-08-24, 1994-08-09, DELIVER IN PERSON, FOB, ndencies are since the packag +357, 113143, 3144, 1, 26, 30059.64, 0.06, 0.03, N, O, 1996-12-28, 1996-11-26, 1997-01-13, NONE, FOB, carefully pending accounts use a +357, 185814, 8333, 2, 36, 68393.16, 0.07, 0.06, N, O, 1996-12-28, 1996-11-13, 1997-01-24, DELIVER IN PERSON, AIR, d the carefully even requests. +357, 164807, 9840, 3, 32, 59897.60, 0.05, 0.07, N, O, 1997-01-28, 1996-12-29, 1997-02-14, NONE, MAIL, y above the carefully final accounts +358, 190028, 2548, 1, 41, 45838.82, 0.06, 0.01, A, F, 1993-11-18, 1993-11-14, 1993-11-28, NONE, TRUCK, ely frets. furious deposits sleep +358, 189955, 7510, 2, 32, 65438.40, 0.05, 0.08, A, F, 1993-10-18, 1993-12-12, 1993-10-31, NONE, TRUCK, y final foxes sleep blithely sl +358, 168710, 3743, 3, 40, 71148.40, 0.09, 0.01, A, F, 1993-12-05, 1993-11-04, 1994-01-01, COLLECT COD, MAIL, ng the ironic theo +358, 96557, 1576, 4, 15, 23303.25, 0.08, 0.08, A, F, 1993-10-04, 1993-12-17, 1993-10-23, TAKE BACK RETURN, MAIL, out the blithely ironic deposits slee +358, 28629, 6136, 5, 18, 28037.16, 0.01, 0.02, R, F, 1993-10-07, 1993-11-01, 1993-10-26, COLLECT COD, SHIP, olphins haggle ironic accounts. f +358, 161283, 1284, 6, 32, 43016.96, 0.03, 0.05, R, F, 1993-12-21, 1993-11-06, 1994-01-17, DELIVER IN PERSON, RAIL, lyly express deposits +358, 82916, 7933, 7, 45, 85450.95, 0.05, 0.02, A, F, 1993-12-08, 1993-10-29, 1993-12-30, NONE, REG AIR, to beans. regular unusual deposits sl +359, 165980, 5981, 1, 30, 61379.40, 0.00, 0.08, A, F, 1995-01-06, 1995-02-20, 1995-01-20, TAKE BACK RETURN, AIR, uses detect spec +359, 11158, 6161, 2, 18, 19244.70, 0.00, 0.03, A, F, 1995-01-27, 1995-03-18, 1995-01-31, DELIVER IN PERSON, RAIL, unusual warthogs. ironically sp +359, 131463, 3977, 3, 17, 25405.82, 0.07, 0.06, A, F, 1995-01-31, 1995-03-18, 1995-02-10, COLLECT COD, SHIP, sts according to the blithely +359, 89985, 2494, 4, 38, 75049.24, 0.10, 0.08, R, F, 1995-03-30, 1995-01-20, 1995-04-25, DELIVER IN PERSON, RAIL, g furiously. regular sile +359, 167239, 2272, 5, 11, 14368.53, 0.01, 0.03, A, F, 1995-02-15, 1995-01-27, 1995-02-18, NONE, FOB, rets wake blithely. slyly final dep +359, 182663, 218, 6, 23, 40150.18, 0.04, 0.07, R, F, 1995-01-31, 1995-03-11, 1995-02-16, DELIVER IN PERSON, REG AIR, ic courts snooze quickly furiously final fo +384, 178442, 3477, 1, 38, 57776.72, 0.07, 0.01, R, F, 1992-06-02, 1992-04-18, 1992-06-10, DELIVER IN PERSON, TRUCK, totes cajole blithely against the even +384, 63342, 5849, 2, 49, 63961.66, 0.09, 0.07, A, F, 1992-04-01, 1992-04-25, 1992-04-18, COLLECT COD, AIR, refully carefully ironic instructions. bl +384, 181502, 6539, 3, 11, 17418.50, 0.02, 0.08, A, F, 1992-04-02, 1992-04-21, 1992-04-15, COLLECT COD, MAIL, ash carefully +384, 92053, 7072, 4, 11, 11495.55, 0.00, 0.06, R, F, 1992-06-24, 1992-05-29, 1992-07-22, COLLECT COD, TRUCK, nic excuses are furiously above the blith +384, 131403, 8943, 5, 14, 20081.60, 0.08, 0.06, R, F, 1992-06-14, 1992-05-29, 1992-07-05, DELIVER IN PERSON, TRUCK, ckages are slyly after the slyly specia +385, 166446, 8963, 1, 7, 10587.08, 0.05, 0.06, N, O, 1996-05-23, 1996-05-09, 1996-06-06, DELIVER IN PERSON, REG AIR, special asymptote +385, 53025, 8036, 2, 46, 44988.92, 0.08, 0.07, N, O, 1996-03-29, 1996-05-17, 1996-04-18, NONE, REG AIR, lthily ironic f +386, 152405, 9951, 1, 39, 56838.60, 0.10, 0.07, A, F, 1995-05-10, 1995-02-28, 1995-05-25, NONE, SHIP, hely. carefully regular accounts hag +386, 68123, 5642, 2, 16, 17457.92, 0.06, 0.01, A, F, 1995-04-12, 1995-04-18, 1995-05-11, DELIVER IN PERSON, MAIL, lithely fluffi +386, 130081, 82, 3, 37, 41109.96, 0.09, 0.04, A, F, 1995-05-23, 1995-03-01, 1995-05-25, TAKE BACK RETURN, MAIL, ending pearls breach fluffily. slyly pen +387, 136667, 1694, 1, 1, 1703.66, 0.08, 0.03, N, O, 1997-05-06, 1997-04-23, 1997-05-10, NONE, SHIP, pinto beans wake furiously carefu +387, 152800, 2801, 2, 42, 77817.60, 0.07, 0.05, N, O, 1997-05-25, 1997-02-25, 1997-05-29, DELIVER IN PERSON, RAIL, lithely final theodolites. +387, 96392, 1411, 3, 40, 55535.60, 0.09, 0.02, N, O, 1997-03-08, 1997-04-18, 1997-03-31, COLLECT COD, TRUCK, quickly ironic platelets are slyly. fluff +387, 55927, 5928, 4, 19, 35775.48, 0.08, 0.00, N, O, 1997-03-14, 1997-04-21, 1997-04-04, NONE, REG AIR, gular dependencies +387, 148313, 828, 5, 32, 43561.92, 0.08, 0.06, N, O, 1997-05-02, 1997-04-11, 1997-05-11, DELIVER IN PERSON, TRUCK, gle. silent fur +388, 32590, 100, 1, 42, 63948.78, 0.05, 0.06, R, F, 1993-02-21, 1993-02-26, 1993-03-15, COLLECT COD, FOB, accounts sleep furiously +388, 127808, 7809, 2, 46, 84446.80, 0.07, 0.01, A, F, 1993-03-22, 1993-01-26, 1993-03-24, COLLECT COD, FOB, to beans nag about the careful reque +388, 64486, 9499, 3, 40, 58019.20, 0.06, 0.01, A, F, 1992-12-24, 1993-01-28, 1993-01-19, TAKE BACK RETURN, REG AIR, quests against the carefully unusual epi +389, 189295, 1814, 1, 2, 2768.58, 0.09, 0.00, R, F, 1994-04-13, 1994-04-10, 1994-04-25, TAKE BACK RETURN, RAIL, fts. courts eat blithely even dependenc +390, 106523, 9034, 1, 10, 15295.20, 0.02, 0.05, N, O, 1998-05-26, 1998-07-06, 1998-06-23, TAKE BACK RETURN, SHIP, requests. final accounts x-ray beside the +390, 123353, 890, 2, 17, 23397.95, 0.09, 0.06, N, O, 1998-06-07, 1998-06-14, 1998-07-07, COLLECT COD, SHIP, ending pending pinto beans wake slyl +390, 183266, 8303, 3, 46, 62065.96, 0.07, 0.04, N, O, 1998-06-06, 1998-05-20, 1998-06-14, DELIVER IN PERSON, SHIP, cial excuses. bold pending packages +390, 141937, 1938, 4, 42, 83115.06, 0.01, 0.05, N, O, 1998-06-06, 1998-06-22, 1998-07-05, COLLECT COD, SHIP, counts nag across the sly sil +390, 127657, 170, 5, 13, 21900.45, 0.02, 0.06, N, O, 1998-07-08, 1998-05-10, 1998-07-18, DELIVER IN PERSON, SHIP, sleep carefully idle packages. blithely +390, 124632, 9657, 6, 11, 18222.93, 0.09, 0.06, N, O, 1998-05-05, 1998-05-15, 1998-06-01, DELIVER IN PERSON, SHIP, according to the foxes are furiously +390, 84937, 2462, 7, 24, 46126.32, 0.05, 0.02, N, O, 1998-04-18, 1998-05-19, 1998-04-28, TAKE BACK RETURN, AIR, y. enticingly final depos +391, 121586, 6611, 1, 14, 22506.12, 0.09, 0.02, R, F, 1995-02-11, 1995-02-03, 1995-02-13, TAKE BACK RETURN, TRUCK, escapades sleep furiously about +416, 93563, 6073, 1, 25, 38914.00, 0.00, 0.05, A, F, 1993-10-11, 1993-11-26, 1993-10-21, DELIVER IN PERSON, TRUCK, y final theodolites about +416, 110869, 8403, 2, 22, 41356.92, 0.10, 0.00, R, F, 1993-12-27, 1993-12-17, 1994-01-09, COLLECT COD, RAIL, rint blithely above the pending sentim +416, 174101, 6619, 3, 25, 29377.50, 0.07, 0.01, R, F, 1993-10-16, 1993-12-03, 1993-10-29, NONE, AIR, ses boost after the bold requests. +417, 39560, 9561, 1, 39, 58482.84, 0.01, 0.02, A, F, 1994-05-31, 1994-05-02, 1994-06-06, NONE, SHIP, y regular requests wake along +417, 69212, 4225, 2, 18, 21261.78, 0.00, 0.01, R, F, 1994-03-29, 1994-04-10, 1994-04-26, TAKE BACK RETURN, FOB, - final requests sle +417, 44192, 6697, 3, 41, 46583.79, 0.10, 0.01, R, F, 1994-04-11, 1994-03-08, 1994-05-06, COLLECT COD, RAIL, tes. regular requests across the +417, 131087, 1088, 4, 2, 2236.16, 0.01, 0.03, R, F, 1994-02-13, 1994-04-19, 1994-03-15, DELIVER IN PERSON, SHIP, uriously bol +418, 18552, 1054, 1, 31, 45587.05, 0.00, 0.03, N, F, 1995-06-05, 1995-06-18, 1995-06-26, COLLECT COD, FOB, final theodolites. fluffil +418, 1062, 3563, 2, 1, 963.06, 0.04, 0.07, N, O, 1995-06-23, 1995-06-16, 1995-07-23, DELIVER IN PERSON, AIR, regular silent pinto +418, 34829, 7333, 3, 3, 5291.46, 0.04, 0.06, N, O, 1995-06-29, 1995-07-12, 1995-07-01, COLLECT COD, AIR, ly furiously regular w +419, 152691, 7722, 1, 33, 57541.77, 0.05, 0.02, N, O, 1996-11-06, 1996-12-25, 1996-11-20, TAKE BACK RETURN, TRUCK, y above the bli +419, 64192, 9205, 2, 32, 36998.08, 0.01, 0.06, N, O, 1996-12-04, 1996-12-04, 1996-12-24, COLLECT COD, SHIP, blithely regular requests. special pinto +419, 70495, 3003, 3, 15, 21982.35, 0.07, 0.04, N, O, 1996-12-17, 1996-11-28, 1996-12-19, TAKE BACK RETURN, REG AIR, sleep final regular theodolites. fluffi +419, 8756, 6257, 4, 15, 24971.25, 0.01, 0.02, N, O, 1997-01-09, 1996-12-22, 1997-01-25, COLLECT COD, FOB, of the careful thin theodolites. quickly s +419, 148401, 3430, 5, 17, 24639.80, 0.01, 0.00, N, O, 1997-01-13, 1996-12-20, 1997-02-01, COLLECT COD, REG AIR, lar dependencies: carefully regu +420, 100885, 5906, 1, 5, 9429.40, 0.04, 0.03, N, O, 1995-11-04, 1996-01-02, 1995-11-30, NONE, REG AIR, cajole blit +420, 161079, 8628, 2, 22, 25081.54, 0.05, 0.04, N, O, 1996-01-25, 1995-12-16, 1996-02-03, TAKE BACK RETURN, AIR, ly against the blithely re +420, 47557, 2566, 3, 45, 67704.75, 0.09, 0.08, N, O, 1996-01-14, 1996-01-01, 1996-01-26, COLLECT COD, FOB, final accounts. furiously express forges +420, 74795, 4796, 4, 12, 21237.48, 0.08, 0.08, N, O, 1996-02-05, 1996-01-03, 1996-02-12, TAKE BACK RETURN, REG AIR, c instructions are +420, 72918, 7933, 5, 37, 69963.67, 0.02, 0.00, N, O, 1995-11-16, 1995-12-13, 1995-11-19, DELIVER IN PERSON, SHIP, rbits. bold requests along the quickl +420, 123736, 1273, 6, 40, 70389.20, 0.01, 0.05, N, O, 1995-11-26, 1995-12-26, 1995-12-20, TAKE BACK RETURN, FOB, after the special +420, 15978, 5979, 7, 39, 73864.83, 0.00, 0.08, N, O, 1995-12-09, 1995-12-16, 1995-12-31, DELIVER IN PERSON, REG AIR, s. ironic waters about the car +421, 133070, 3071, 1, 1, 1103.07, 0.02, 0.07, R, F, 1992-05-29, 1992-04-27, 1992-06-09, NONE, TRUCK, oldly busy deposit +422, 151816, 4332, 1, 25, 46695.25, 0.10, 0.07, N, O, 1997-07-01, 1997-08-17, 1997-07-09, DELIVER IN PERSON, SHIP, carefully bold theodolit +422, 170666, 3184, 2, 10, 17366.60, 0.02, 0.03, N, O, 1997-06-15, 1997-08-04, 1997-07-08, TAKE BACK RETURN, AIR, he furiously ironic theodolite +422, 175984, 3536, 3, 46, 94759.08, 0.09, 0.00, N, O, 1997-06-21, 1997-07-14, 1997-06-27, DELIVER IN PERSON, RAIL, ideas. qu +422, 161622, 9171, 4, 25, 42090.50, 0.10, 0.04, N, O, 1997-08-24, 1997-07-09, 1997-09-22, NONE, FOB, ep along the furiousl +423, 131890, 6917, 1, 27, 51891.03, 0.06, 0.03, N, O, 1996-08-20, 1996-08-01, 1996-08-23, TAKE BACK RETURN, SHIP, ccounts. blithely regular pack +448, 125197, 5198, 1, 4, 4888.76, 0.00, 0.04, N, O, 1995-11-25, 1995-10-20, 1995-11-26, TAKE BACK RETURN, MAIL, nts thrash quickly among the b +448, 172359, 9911, 2, 46, 65842.10, 0.05, 0.00, N, O, 1995-08-31, 1995-09-30, 1995-09-09, COLLECT COD, SHIP, to the fluffily ironic packages. +448, 26809, 1814, 3, 35, 60753.00, 0.10, 0.08, N, O, 1995-09-27, 1995-11-19, 1995-10-20, COLLECT COD, REG AIR, ses nag quickly quickly ir +448, 169045, 9046, 4, 8, 8912.32, 0.10, 0.00, N, O, 1995-11-02, 1995-10-16, 1995-11-15, COLLECT COD, TRUCK, ounts wake blithely. furiously pending +448, 137283, 7284, 5, 23, 30366.44, 0.02, 0.05, N, O, 1995-09-26, 1995-11-02, 1995-10-17, NONE, SHIP, ious final gifts +449, 151908, 6939, 1, 12, 23518.80, 0.02, 0.08, N, O, 1995-11-06, 1995-08-25, 1995-11-18, TAKE BACK RETURN, SHIP, ly. blithely ironic +449, 108408, 5939, 2, 4, 5665.60, 0.10, 0.06, N, O, 1995-10-27, 1995-09-14, 1995-11-21, DELIVER IN PERSON, FOB, are fluffily. requests are furiously +449, 9982, 9983, 3, 3, 5675.94, 0.07, 0.08, N, O, 1995-07-28, 1995-09-11, 1995-08-01, NONE, RAIL, bold deposits. express theodolites haggle +449, 157659, 2690, 4, 22, 37766.30, 0.07, 0.00, N, O, 1995-08-17, 1995-09-04, 1995-09-10, COLLECT COD, FOB, furiously final theodolites eat careful +450, 161582, 9131, 1, 42, 69030.36, 0.03, 0.00, N, F, 1995-06-07, 1995-05-29, 1995-06-23, TAKE BACK RETURN, SHIP, y asymptotes. regular depen +450, 106298, 6299, 2, 5, 6521.45, 0.03, 0.02, A, F, 1995-04-02, 1995-05-06, 1995-04-13, TAKE BACK RETURN, TRUCK, the pinto bea +450, 142528, 7557, 3, 32, 50256.64, 0.06, 0.03, N, O, 1995-07-02, 1995-04-25, 1995-07-30, TAKE BACK RETURN, SHIP, accounts nod fluffily even pending +450, 56267, 3783, 4, 40, 48930.40, 0.05, 0.03, R, F, 1995-03-20, 1995-05-25, 1995-04-14, NONE, RAIL, ve. asymptote +450, 78048, 8049, 5, 2, 2052.08, 0.09, 0.00, A, F, 1995-03-11, 1995-05-21, 1995-03-16, COLLECT COD, AIR, y even pinto beans; qui +450, 152726, 5242, 6, 33, 58697.76, 0.08, 0.05, R, F, 1995-05-18, 1995-05-22, 1995-05-23, TAKE BACK RETURN, REG AIR, ily carefully final depo +451, 129532, 4557, 1, 36, 56215.08, 0.02, 0.06, N, O, 1998-06-18, 1998-08-14, 1998-06-20, TAKE BACK RETURN, AIR, rges can haggle carefully ironic dogged +451, 32028, 7035, 2, 42, 40320.84, 0.05, 0.01, N, O, 1998-08-01, 1998-08-05, 1998-08-30, DELIVER IN PERSON, TRUCK, express excuses. blithely ironic pin +451, 86136, 6137, 3, 1, 1122.13, 0.07, 0.05, N, O, 1998-07-13, 1998-07-03, 1998-08-04, DELIVER IN PERSON, AIR, carefully ironic packages solve furiously +451, 76558, 4080, 4, 28, 42967.40, 0.04, 0.05, N, O, 1998-06-16, 1998-07-09, 1998-06-17, DELIVER IN PERSON, SHIP, theodolites. even cou +452, 114639, 4640, 1, 2, 3307.26, 0.04, 0.03, N, O, 1997-12-26, 1998-01-03, 1998-01-12, COLLECT COD, FOB, y express instru +453, 197917, 2956, 1, 45, 90670.95, 0.01, 0.00, N, O, 1997-06-30, 1997-08-20, 1997-07-19, COLLECT COD, REG AIR, ifts wake carefully. +453, 175131, 2683, 2, 38, 45832.94, 0.08, 0.04, N, O, 1997-06-30, 1997-07-08, 1997-07-16, DELIVER IN PERSON, REG AIR, furiously f +453, 13144, 8147, 3, 38, 40171.32, 0.10, 0.01, N, O, 1997-08-10, 1997-07-24, 1997-09-07, NONE, SHIP, sts cajole. furiously un +453, 95748, 5749, 4, 45, 78468.30, 0.10, 0.01, N, O, 1997-09-18, 1997-06-29, 1997-10-14, TAKE BACK RETURN, AIR, ironic foxes. slyly pending depos +453, 25722, 8225, 5, 32, 52727.04, 0.04, 0.01, N, O, 1997-07-15, 1997-06-27, 1997-07-18, NONE, REG AIR, s. fluffily bold packages cajole. unu +453, 94318, 6828, 6, 28, 36744.68, 0.07, 0.07, N, O, 1997-08-16, 1997-08-12, 1997-08-27, NONE, MAIL, final dependencies. slyly special pl +454, 117595, 5129, 1, 24, 38702.16, 0.06, 0.01, N, O, 1996-04-26, 1996-03-23, 1996-05-20, NONE, TRUCK, le. deposits after the ideas nag unusual pa +455, 156485, 4031, 1, 42, 64742.16, 0.10, 0.02, N, O, 1997-01-26, 1997-01-10, 1997-02-22, DELIVER IN PERSON, REG AIR, around the quickly blit +455, 27230, 7231, 2, 44, 50918.12, 0.05, 0.08, N, O, 1997-01-17, 1997-02-22, 1997-02-12, TAKE BACK RETURN, TRUCK, accounts sleep slyly ironic asymptote +455, 48360, 3369, 3, 45, 58876.20, 0.04, 0.06, N, O, 1996-12-20, 1997-01-31, 1997-01-07, TAKE BACK RETURN, SHIP, thrash ironically regular packages. qui +455, 170012, 7564, 4, 11, 11902.11, 0.01, 0.02, N, O, 1997-03-15, 1997-02-14, 1997-03-26, DELIVER IN PERSON, MAIL, g deposits against the slyly idle foxes u +480, 52148, 2149, 1, 22, 24203.08, 0.04, 0.02, A, F, 1993-06-16, 1993-07-28, 1993-07-09, NONE, MAIL, into beans cajole furiously. accounts s +481, 18649, 6153, 1, 17, 26649.88, 0.07, 0.05, A, F, 1992-10-21, 1992-12-09, 1992-11-19, DELIVER IN PERSON, MAIL, . quickly final accounts among the +481, 20646, 647, 2, 19, 29766.16, 0.08, 0.01, R, F, 1993-01-09, 1992-11-27, 1993-01-14, TAKE BACK RETURN, AIR, p blithely after t +481, 185785, 5786, 3, 42, 78572.76, 0.08, 0.08, A, F, 1992-11-27, 1992-11-11, 1992-12-08, COLLECT COD, RAIL, mptotes are furiously among the iron +481, 81009, 6026, 4, 11, 10890.00, 0.05, 0.06, A, F, 1993-01-12, 1992-11-17, 1993-02-05, NONE, FOB, eful attai +481, 111956, 6979, 5, 31, 61006.45, 0.05, 0.01, A, F, 1993-01-15, 1992-12-31, 1993-01-21, DELIVER IN PERSON, AIR, usly final packages believe. quick +482, 137343, 7344, 1, 32, 44170.88, 0.00, 0.02, N, O, 1996-05-22, 1996-05-14, 1996-05-29, NONE, SHIP, usual deposits affix against +482, 121382, 8919, 2, 1, 1403.38, 0.05, 0.08, N, O, 1996-05-29, 1996-05-20, 1996-05-31, COLLECT COD, AIR, es. quickly ironic escapades sleep furious +482, 61141, 6154, 3, 31, 34166.34, 0.04, 0.03, N, O, 1996-06-01, 1996-05-06, 1996-06-17, NONE, MAIL, blithe pin +482, 195826, 5827, 4, 8, 15374.56, 0.02, 0.05, N, O, 1996-04-19, 1996-05-05, 1996-04-21, NONE, TRUCK, tructions near the final regular ideas de +482, 38215, 3222, 5, 46, 53047.66, 0.01, 0.06, N, O, 1996-07-19, 1996-06-05, 1996-08-10, NONE, MAIL, furiously thin realms. final fina +482, 78696, 8697, 6, 19, 31819.11, 0.04, 0.00, N, O, 1996-03-27, 1996-04-25, 1996-04-15, NONE, FOB, ts hinder carefully silent requests +483, 32694, 5198, 1, 8, 13013.52, 0.00, 0.08, N, O, 1995-08-22, 1995-08-23, 1995-09-18, COLLECT COD, RAIL, osits. carefully fin +483, 79758, 9759, 2, 23, 39968.25, 0.04, 0.06, N, O, 1995-07-20, 1995-08-11, 1995-08-04, DELIVER IN PERSON, MAIL, requests was quickly against th +483, 87745, 254, 3, 9, 15594.66, 0.04, 0.03, N, O, 1995-09-10, 1995-09-02, 1995-09-13, NONE, AIR, carefully express ins +484, 30133, 5140, 1, 49, 52093.37, 0.10, 0.02, N, O, 1997-03-06, 1997-02-28, 1997-03-23, COLLECT COD, TRUCK, ven accounts +484, 31950, 9460, 2, 45, 84687.75, 0.06, 0.07, N, O, 1997-04-09, 1997-03-20, 1997-04-19, DELIVER IN PERSON, TRUCK, usly final excuses boost slyly blithe +484, 183351, 5870, 3, 50, 71717.50, 0.06, 0.05, N, O, 1997-01-24, 1997-03-27, 1997-02-22, DELIVER IN PERSON, MAIL, uctions wake. final silent requests haggle +484, 164805, 4806, 4, 22, 41135.60, 0.07, 0.03, N, O, 1997-04-29, 1997-03-26, 1997-05-17, TAKE BACK RETURN, SHIP, es are pending instructions. furiously unu +484, 76308, 1323, 5, 48, 61646.40, 0.00, 0.05, N, O, 1997-03-05, 1997-02-08, 1997-03-22, TAKE BACK RETURN, MAIL, l bold packages? even mult +484, 96871, 9381, 6, 10, 18678.70, 0.01, 0.08, N, O, 1997-04-06, 1997-02-14, 1997-04-16, COLLECT COD, FOB, x fluffily carefully regular +485, 149523, 9524, 1, 50, 78626.00, 0.01, 0.00, N, O, 1997-03-28, 1997-05-26, 1997-04-18, TAKE BACK RETURN, MAIL, iously quick excuses. carefully final f +485, 27973, 2978, 2, 40, 76038.80, 0.08, 0.01, N, O, 1997-04-29, 1997-05-08, 1997-04-30, TAKE BACK RETURN, TRUCK, al escapades +485, 136884, 4424, 3, 22, 42259.36, 0.00, 0.05, N, O, 1997-04-06, 1997-04-27, 1997-05-01, DELIVER IN PERSON, TRUCK, refully final notornis haggle according +486, 75437, 5438, 1, 36, 50847.48, 0.00, 0.01, N, O, 1996-06-25, 1996-05-06, 1996-07-07, COLLECT COD, AIR, deposits around the quickly regular packa +486, 67040, 7041, 2, 40, 40281.60, 0.03, 0.08, N, O, 1996-05-21, 1996-06-06, 1996-06-07, COLLECT COD, SHIP, ts nag quickly among the slyl +486, 135912, 8426, 3, 26, 50645.66, 0.04, 0.03, N, O, 1996-03-16, 1996-05-25, 1996-03-31, NONE, RAIL, forges along the +486, 71865, 6880, 4, 38, 69800.68, 0.08, 0.05, N, O, 1996-05-07, 1996-04-26, 1996-05-26, TAKE BACK RETURN, TRUCK, blithely final pinto +486, 28099, 5606, 5, 3, 3081.27, 0.07, 0.05, N, O, 1996-07-07, 1996-04-20, 1996-07-23, DELIVER IN PERSON, RAIL, ccounts ha +486, 46543, 9048, 6, 46, 68518.84, 0.00, 0.03, N, O, 1996-04-18, 1996-05-02, 1996-04-20, COLLECT COD, AIR, theodolites eat carefully furious +487, 91896, 1897, 1, 47, 88730.83, 0.06, 0.06, R, F, 1992-09-30, 1992-10-08, 1992-10-24, NONE, TRUCK, tions. blithely reg +487, 82099, 2100, 2, 2, 2162.18, 0.02, 0.06, R, F, 1992-10-19, 1992-11-04, 1992-11-11, COLLECT COD, TRUCK, oss the unusual pinto beans. reg +512, 188804, 1323, 1, 19, 35963.20, 0.08, 0.05, N, O, 1995-07-12, 1995-07-11, 1995-08-04, COLLECT COD, MAIL, sleep. requests alongside of the fluff +512, 22847, 7852, 2, 37, 65484.08, 0.01, 0.04, N, O, 1995-06-20, 1995-07-05, 1995-07-16, NONE, RAIL, nic depths cajole? blithely b +512, 179419, 9420, 3, 40, 59936.40, 0.05, 0.02, N, O, 1995-07-06, 1995-07-08, 1995-07-08, COLLECT COD, TRUCK, quests are da +512, 82470, 7487, 4, 10, 14524.70, 0.09, 0.02, N, O, 1995-09-16, 1995-07-29, 1995-10-07, NONE, AIR, xes. pinto beans cajole carefully; +512, 64154, 4155, 5, 6, 6708.90, 0.03, 0.05, R, F, 1995-06-10, 1995-06-21, 1995-06-16, DELIVER IN PERSON, FOB, en ideas haggle +512, 32014, 4518, 6, 12, 11352.12, 0.04, 0.00, R, F, 1995-05-21, 1995-08-03, 1995-06-09, COLLECT COD, FOB, old furiously express deposits. specia +512, 50769, 3275, 7, 2, 3439.52, 0.09, 0.08, N, O, 1995-06-19, 1995-08-13, 1995-06-24, NONE, TRUCK, e slyly silent accounts serve with +513, 61732, 9251, 1, 20, 33874.60, 0.09, 0.07, N, O, 1995-07-12, 1995-05-31, 1995-07-31, NONE, AIR, efully ironic ideas doze slyl +513, 121628, 9165, 2, 44, 72583.28, 0.01, 0.01, N, O, 1995-07-14, 1995-07-14, 1995-08-12, NONE, MAIL, kages sleep boldly ironic theodolites. acco +514, 78713, 1221, 1, 21, 35525.91, 0.06, 0.02, N, O, 1996-06-09, 1996-05-15, 1996-07-07, DELIVER IN PERSON, RAIL, s sleep quickly blithely +514, 117452, 9964, 2, 34, 49961.30, 0.08, 0.02, N, O, 1996-04-14, 1996-06-03, 1996-04-23, COLLECT COD, REG AIR, ily even patterns. bold silent instruc +514, 12812, 5314, 3, 6, 10348.86, 0.06, 0.01, N, O, 1996-05-30, 1996-06-04, 1996-06-28, COLLECT COD, SHIP, as haggle blithely; quickly s +514, 115362, 5363, 4, 43, 59226.48, 0.00, 0.08, N, O, 1996-06-07, 1996-05-14, 1996-07-01, TAKE BACK RETURN, FOB, thely regular +515, 104014, 6525, 1, 10, 10180.10, 0.03, 0.02, A, F, 1993-10-04, 1993-11-03, 1993-10-08, NONE, FOB, ar deposits th +515, 147605, 2634, 2, 38, 62798.80, 0.10, 0.07, A, F, 1993-09-19, 1993-11-12, 1993-10-03, DELIVER IN PERSON, SHIP, ays. furiously express requests haggle furi +515, 182145, 9700, 3, 11, 13498.54, 0.00, 0.02, R, F, 1993-09-04, 1993-10-02, 1993-09-05, DELIVER IN PERSON, FOB, ly pending accounts haggle blithel +515, 108606, 8607, 4, 34, 54896.40, 0.09, 0.03, R, F, 1993-10-03, 1993-10-26, 1993-10-15, DELIVER IN PERSON, REG AIR, ic dependencie +515, 130881, 3395, 5, 32, 61180.16, 0.01, 0.07, R, F, 1993-10-10, 1993-10-08, 1993-11-02, TAKE BACK RETURN, FOB, r sauternes boost. final theodolites wake a +515, 108692, 3713, 6, 25, 42517.25, 0.04, 0.08, R, F, 1993-11-14, 1993-11-07, 1993-12-03, DELIVER IN PERSON, MAIL, e packages engag +516, 24974, 9979, 1, 11, 20888.67, 0.01, 0.06, N, O, 1998-05-02, 1998-05-23, 1998-05-12, DELIVER IN PERSON, FOB, ongside of the blithely final reque +517, 44551, 4552, 1, 28, 41875.40, 0.03, 0.02, N, O, 1997-04-30, 1997-05-18, 1997-05-17, COLLECT COD, MAIL, requests. special fi +517, 155391, 7907, 2, 15, 21695.85, 0.02, 0.00, N, O, 1997-04-09, 1997-06-26, 1997-05-01, NONE, TRUCK, slyly. express requests ar +517, 40932, 3437, 3, 9, 16856.37, 0.04, 0.00, N, O, 1997-05-03, 1997-06-16, 1997-05-24, COLLECT COD, SHIP, slyly stealthily express instructions. +517, 132197, 2198, 4, 11, 13521.09, 0.06, 0.02, N, O, 1997-06-20, 1997-06-01, 1997-06-27, NONE, REG AIR, ly throughout the fu +517, 23349, 8354, 5, 23, 29263.82, 0.00, 0.01, N, O, 1997-04-19, 1997-05-07, 1997-05-12, COLLECT COD, RAIL, kindle. furiously bold requests mus +518, 164711, 4712, 1, 30, 53271.30, 0.07, 0.05, N, O, 1998-02-18, 1998-03-27, 1998-03-16, COLLECT COD, TRUCK, slyly by the packages. carefull +518, 83164, 689, 2, 23, 26384.68, 0.05, 0.07, N, O, 1998-02-20, 1998-05-05, 1998-03-11, COLLECT COD, TRUCK, special requests. fluffily ironic re +518, 133178, 8205, 3, 12, 14534.04, 0.01, 0.06, N, O, 1998-03-08, 1998-03-31, 1998-04-06, NONE, AIR, packages thrash slyly +518, 121990, 1991, 4, 46, 92551.54, 0.07, 0.02, N, O, 1998-04-07, 1998-04-17, 1998-04-29, NONE, MAIL, . blithely even ideas cajole furiously. b +518, 70019, 20, 5, 16, 15824.16, 0.01, 0.01, N, O, 1998-03-15, 1998-03-24, 1998-04-08, NONE, MAIL, use quickly expre +518, 196358, 1397, 6, 39, 56719.65, 0.09, 0.08, N, O, 1998-02-26, 1998-03-17, 1998-03-21, DELIVER IN PERSON, FOB, the bold special deposits are carefully +518, 185956, 8475, 7, 48, 98013.60, 0.03, 0.07, N, O, 1998-03-06, 1998-04-22, 1998-03-14, NONE, FOB, slyly final platelets; quickly even deposi +519, 158970, 4001, 1, 1, 2028.97, 0.07, 0.07, N, O, 1997-12-01, 1998-01-26, 1997-12-23, COLLECT COD, REG AIR, bold requests believe furiou +519, 2946, 2947, 2, 38, 70259.72, 0.05, 0.08, N, O, 1998-02-19, 1997-12-15, 1998-03-19, DELIVER IN PERSON, FOB, gular excuses detect quickly furiously +519, 105900, 921, 3, 19, 36212.10, 0.00, 0.02, N, O, 1998-01-09, 1998-01-03, 1998-02-06, COLLECT COD, AIR, asymptotes. p +519, 46267, 3780, 4, 27, 32758.02, 0.08, 0.06, N, O, 1997-11-20, 1997-12-06, 1997-12-16, DELIVER IN PERSON, REG AIR, le. even final dependencies +519, 9041, 4042, 5, 13, 12350.52, 0.06, 0.08, N, O, 1998-02-06, 1997-12-02, 1998-03-03, TAKE BACK RETURN, TRUCK, c accounts wake along the ironic so +519, 150926, 5957, 6, 3, 5930.76, 0.04, 0.00, N, O, 1998-02-01, 1998-01-25, 1998-02-27, TAKE BACK RETURN, FOB, erve blithely blithely ironic asymp +544, 138474, 8475, 1, 47, 71086.09, 0.08, 0.06, R, F, 1993-03-14, 1993-03-27, 1993-03-27, COLLECT COD, SHIP, ecial pains. deposits grow foxes. +545, 169547, 9548, 1, 4, 6466.16, 0.02, 0.00, N, O, 1996-02-23, 1995-12-16, 1996-03-21, DELIVER IN PERSON, FOB, ironic grouches cajole over +545, 170188, 5223, 2, 18, 22647.24, 0.00, 0.00, N, O, 1996-02-21, 1996-01-17, 1996-02-26, NONE, RAIL, al final packages affix. even a +546, 84585, 2110, 1, 16, 25113.28, 0.08, 0.02, N, O, 1997-02-04, 1996-12-30, 1997-02-25, DELIVER IN PERSON, TRUCK, de of the orbits. sometimes regula +547, 70789, 5804, 1, 44, 77430.32, 0.08, 0.08, N, O, 1996-10-18, 1996-08-17, 1996-10-27, TAKE BACK RETURN, FOB, thely express dependencies. qu +547, 136347, 1374, 2, 48, 66400.32, 0.01, 0.04, N, O, 1996-10-21, 1996-08-04, 1996-11-20, COLLECT COD, SHIP, thely specia +547, 181345, 6382, 3, 3, 4279.02, 0.05, 0.02, N, O, 1996-09-04, 1996-08-01, 1996-09-21, COLLECT COD, SHIP, pinto beans. ironi +548, 196550, 6551, 1, 2, 3293.10, 0.06, 0.05, A, F, 1994-11-26, 1994-11-06, 1994-12-06, COLLECT COD, MAIL, ests haggle quickly eve +548, 4641, 4642, 2, 6, 9273.84, 0.00, 0.08, A, F, 1995-01-18, 1994-12-08, 1995-02-10, NONE, TRUCK, sits wake furiously regular +548, 182, 7683, 3, 21, 22725.78, 0.03, 0.08, A, F, 1995-01-13, 1994-12-18, 1995-01-25, NONE, AIR, ideas. special accounts above the furiou +548, 56720, 4236, 4, 21, 35211.12, 0.08, 0.03, A, F, 1994-10-27, 1994-12-04, 1994-11-21, DELIVER IN PERSON, AIR, engage quickly. regular theo +548, 92995, 523, 5, 19, 37771.81, 0.00, 0.02, A, F, 1994-09-24, 1994-11-24, 1994-10-01, DELIVER IN PERSON, MAIL, courts boost care +548, 152753, 7784, 6, 32, 57784.00, 0.06, 0.04, A, F, 1994-12-16, 1994-11-20, 1994-12-29, NONE, REG AIR, c instruction +549, 195061, 100, 1, 18, 20809.08, 0.07, 0.04, R, F, 1992-10-19, 1992-08-12, 1992-11-13, COLLECT COD, REG AIR, furiously according to the ironic regular +549, 188735, 8736, 2, 38, 69301.74, 0.07, 0.05, A, F, 1992-08-17, 1992-08-28, 1992-09-05, COLLECT COD, RAIL, the regular furious excuses. carefu +549, 65213, 5214, 3, 36, 42415.56, 0.08, 0.04, R, F, 1992-09-11, 1992-10-11, 1992-09-12, DELIVER IN PERSON, AIR, ts against the ironic even theodolites eng +549, 20101, 7608, 4, 18, 18379.80, 0.09, 0.01, A, F, 1992-07-31, 1992-09-11, 1992-08-08, NONE, RAIL, ely regular accounts above the +549, 23480, 987, 5, 38, 53332.24, 0.06, 0.02, R, F, 1992-08-23, 1992-08-12, 1992-08-25, COLLECT COD, REG AIR, eposits. carefully regular depos +550, 190307, 2827, 1, 31, 43316.30, 0.04, 0.02, N, O, 1995-10-24, 1995-09-27, 1995-11-04, COLLECT COD, AIR, thely silent packages. unusual +551, 23786, 6289, 1, 8, 13678.24, 0.08, 0.02, N, O, 1995-07-29, 1995-07-18, 1995-08-02, NONE, REG AIR, wake quickly slyly pending platel +551, 158813, 3844, 2, 20, 37436.20, 0.00, 0.07, N, O, 1995-09-18, 1995-08-25, 1995-10-11, COLLECT COD, TRUCK, r ideas. final even ideas hinder alongside +551, 161089, 6122, 3, 16, 18401.28, 0.07, 0.06, N, O, 1995-07-29, 1995-08-19, 1995-08-10, COLLECT COD, MAIL, y along the carefully ex +576, 86490, 1507, 1, 2, 2952.98, 0.07, 0.01, N, O, 1997-05-15, 1997-06-30, 1997-05-28, NONE, RAIL, ccounts along the ac +576, 33096, 8103, 2, 6, 6174.54, 0.06, 0.05, N, O, 1997-05-15, 1997-07-26, 1997-06-03, DELIVER IN PERSON, TRUCK, al deposits. slyly even sauternes a +576, 36565, 9069, 3, 6, 9009.36, 0.08, 0.07, N, O, 1997-08-28, 1997-06-16, 1997-09-25, DELIVER IN PERSON, FOB, ts. ironic multipliers +576, 137608, 2635, 4, 5, 8228.00, 0.03, 0.07, N, O, 1997-06-11, 1997-06-17, 1997-07-05, NONE, REG AIR, l foxes boost slyly. accounts af +577, 25886, 891, 1, 25, 45297.00, 0.06, 0.01, A, F, 1995-04-09, 1995-02-20, 1995-05-09, TAKE BACK RETURN, AIR, ve slyly of the frets. careful +577, 63233, 8246, 2, 14, 16747.22, 0.08, 0.03, R, F, 1995-03-19, 1995-02-25, 1995-04-09, DELIVER IN PERSON, RAIL, l accounts wake deposits. ironic packa +578, 155542, 5543, 1, 40, 63901.60, 0.02, 0.08, N, O, 1997-02-10, 1997-03-18, 1997-02-11, NONE, SHIP, usly even platel +578, 187025, 2062, 2, 23, 25576.46, 0.05, 0.08, N, O, 1997-03-06, 1997-03-03, 1997-03-20, TAKE BACK RETURN, FOB, nstructions. ironic deposits +579, 150618, 5649, 1, 9, 15017.49, 0.00, 0.05, N, O, 1998-06-20, 1998-04-28, 1998-07-19, DELIVER IN PERSON, RAIL, e ironic express deposits are furiously +579, 32145, 7152, 2, 39, 42008.46, 0.02, 0.01, N, O, 1998-06-21, 1998-06-03, 1998-06-26, COLLECT COD, REG AIR, ncies. furiously final r +579, 59048, 4059, 3, 6, 6042.24, 0.03, 0.00, N, O, 1998-04-24, 1998-05-03, 1998-05-08, TAKE BACK RETURN, TRUCK, ickly final requests-- bold accou +579, 6189, 8690, 4, 41, 44902.38, 0.04, 0.05, N, O, 1998-05-28, 1998-05-01, 1998-06-04, COLLECT COD, REG AIR, bold express requests sublate slyly. blith +579, 12612, 5114, 5, 28, 42689.08, 0.00, 0.03, N, O, 1998-07-10, 1998-05-24, 1998-07-19, NONE, RAIL, ic ideas until th +579, 166717, 9234, 6, 5, 8918.55, 0.05, 0.08, N, O, 1998-05-02, 1998-04-25, 1998-05-05, COLLECT COD, REG AIR, refully silent ideas cajole furious +580, 84916, 2441, 1, 33, 62730.03, 0.03, 0.05, N, O, 1997-10-11, 1997-09-19, 1997-10-16, TAKE BACK RETURN, FOB, y express theodolites cajole carefully +580, 173320, 3321, 2, 31, 43192.92, 0.04, 0.08, N, O, 1997-10-04, 1997-09-08, 1997-10-15, COLLECT COD, FOB, ose alongside of the sl +580, 184444, 6963, 3, 19, 29040.36, 0.04, 0.04, N, O, 1997-07-23, 1997-09-21, 1997-08-15, NONE, FOB, mong the special packag +581, 63384, 8397, 1, 41, 55242.58, 0.09, 0.07, N, O, 1997-05-26, 1997-04-06, 1997-06-10, TAKE BACK RETURN, MAIL, nts. quickly +581, 92527, 5037, 2, 14, 21273.28, 0.06, 0.08, N, O, 1997-05-17, 1997-04-14, 1997-06-08, NONE, MAIL, . deposits s +581, 100106, 5127, 3, 49, 54198.90, 0.10, 0.02, N, O, 1997-02-27, 1997-04-24, 1997-03-10, TAKE BACK RETURN, MAIL, . slyly regular pinto beans acr +581, 74925, 9940, 4, 30, 56997.60, 0.10, 0.08, N, O, 1997-06-19, 1997-05-21, 1997-06-22, TAKE BACK RETURN, TRUCK, regular ideas grow furio +582, 56409, 3925, 1, 7, 9557.80, 0.07, 0.00, N, O, 1997-11-16, 1997-11-29, 1997-12-10, TAKE BACK RETURN, FOB, ithely unusual t +582, 50262, 263, 2, 49, 59400.74, 0.05, 0.03, N, O, 1997-12-17, 1998-01-12, 1997-12-31, COLLECT COD, REG AIR, nts according to the furiously regular pin +582, 140309, 5338, 3, 42, 56670.60, 0.07, 0.00, N, O, 1997-11-15, 1997-12-21, 1997-12-03, COLLECT COD, SHIP, iously beside the silent de +582, 167750, 7751, 4, 36, 65439.00, 0.06, 0.01, N, O, 1997-12-09, 1997-11-27, 1997-12-26, TAKE BACK RETURN, SHIP, lar requests. quickly +583, 144364, 4365, 1, 1, 1408.36, 0.07, 0.07, N, O, 1997-06-17, 1997-04-29, 1997-06-28, NONE, TRUCK, regular regular ideas. even bra +583, 119625, 2137, 2, 47, 77297.14, 0.10, 0.06, N, O, 1997-07-14, 1997-05-12, 1997-08-11, DELIVER IN PERSON, AIR, nts are fluffily. furiously even re +583, 129431, 1944, 3, 34, 49654.62, 0.01, 0.02, N, O, 1997-05-11, 1997-04-24, 1997-06-03, DELIVER IN PERSON, MAIL, express req +583, 141250, 8793, 4, 33, 42611.25, 0.10, 0.01, N, O, 1997-05-28, 1997-04-25, 1997-06-24, NONE, AIR, kages cajole slyly across the +583, 188537, 6092, 5, 13, 21131.89, 0.04, 0.06, N, O, 1997-06-23, 1997-05-29, 1997-07-08, COLLECT COD, TRUCK, y sly theodolites. ironi +608, 153579, 1125, 1, 19, 31018.83, 0.08, 0.06, N, O, 1996-04-19, 1996-05-02, 1996-05-03, DELIVER IN PERSON, RAIL, ideas. the +608, 197310, 2349, 2, 40, 56292.40, 0.03, 0.01, N, O, 1996-05-21, 1996-04-11, 1996-06-02, NONE, AIR, alongside of the regular tithes. sly +609, 65533, 8040, 1, 21, 31469.13, 0.01, 0.05, R, F, 1994-08-24, 1994-08-23, 1994-08-27, DELIVER IN PERSON, FOB, de of the special warthogs. excu +610, 110792, 5815, 1, 49, 88336.71, 0.10, 0.07, N, O, 1995-08-29, 1995-10-26, 1995-09-12, TAKE BACK RETURN, SHIP, ular instruc +610, 67896, 5415, 2, 11, 20502.79, 0.07, 0.08, N, O, 1995-10-31, 1995-10-25, 1995-11-18, NONE, MAIL, blithely final +610, 117617, 7618, 3, 26, 42499.86, 0.09, 0.04, N, O, 1995-11-22, 1995-09-09, 1995-12-04, TAKE BACK RETURN, AIR, cross the furiously even theodolites sl +610, 185206, 7725, 4, 17, 21950.40, 0.03, 0.03, N, O, 1995-11-01, 1995-10-30, 1995-11-04, COLLECT COD, FOB, p quickly instead of the slyly pending foxe +610, 145743, 5744, 5, 39, 69760.86, 0.08, 0.05, N, O, 1995-10-30, 1995-10-21, 1995-11-11, TAKE BACK RETURN, REG AIR, counts. ironic warhorses are +610, 94365, 6875, 6, 5, 6796.80, 0.00, 0.07, N, O, 1995-08-11, 1995-10-22, 1995-08-26, TAKE BACK RETURN, FOB, n pinto beans. iro +610, 189280, 4317, 7, 27, 36970.56, 0.06, 0.03, N, O, 1995-09-02, 1995-09-19, 1995-09-15, NONE, REG AIR, ironic pinto beans haggle. blithe +611, 16855, 4359, 1, 39, 69102.15, 0.05, 0.06, R, F, 1993-05-06, 1993-04-09, 1993-05-22, TAKE BACK RETURN, SHIP, nto beans +611, 80676, 677, 2, 1, 1656.67, 0.08, 0.07, R, F, 1993-05-17, 1993-02-26, 1993-06-15, DELIVER IN PERSON, MAIL, ts. pending platelets aff +611, 119545, 2057, 3, 39, 61017.06, 0.09, 0.02, A, F, 1993-03-10, 1993-03-10, 1993-03-17, TAKE BACK RETURN, TRUCK, the evenly bold requests. furious +612, 184959, 9996, 1, 5, 10219.75, 0.07, 0.00, R, F, 1992-11-08, 1992-11-20, 1992-12-03, TAKE BACK RETURN, RAIL, structions. q +612, 194864, 7384, 2, 28, 54848.08, 0.07, 0.06, R, F, 1993-01-02, 1992-12-11, 1993-01-30, DELIVER IN PERSON, TRUCK, regular instructions affix bl +612, 66130, 1143, 3, 49, 53710.37, 0.00, 0.08, A, F, 1993-01-08, 1992-11-25, 1993-01-17, TAKE BACK RETURN, REG AIR, theodolite +612, 38942, 1446, 4, 28, 52666.32, 0.05, 0.00, A, F, 1992-11-12, 1992-12-05, 1992-12-02, TAKE BACK RETURN, REG AIR, lyly regular asym +612, 87737, 246, 5, 1, 1724.73, 0.08, 0.04, R, F, 1992-12-18, 1992-12-13, 1992-12-20, TAKE BACK RETURN, FOB, requests. +612, 188203, 5758, 6, 33, 42609.60, 0.10, 0.03, R, F, 1992-11-30, 1992-12-01, 1992-12-12, COLLECT COD, MAIL, bove the blithely even ideas. careful +613, 90027, 7555, 1, 17, 17289.34, 0.06, 0.06, N, O, 1995-09-23, 1995-08-04, 1995-10-15, NONE, SHIP, ar dependencie +613, 78348, 5870, 2, 6, 7958.04, 0.05, 0.05, N, O, 1995-08-05, 1995-08-09, 1995-08-08, TAKE BACK RETURN, MAIL, y ironic deposits eat +613, 185016, 7535, 3, 3, 3303.03, 0.03, 0.01, N, O, 1995-09-27, 1995-09-11, 1995-10-05, NONE, TRUCK, ccounts cajole. +613, 158304, 8305, 4, 7, 9536.10, 0.02, 0.04, N, O, 1995-09-07, 1995-08-02, 1995-09-16, DELIVER IN PERSON, MAIL, ously blithely final pinto beans. regula +614, 194109, 9148, 1, 21, 25265.10, 0.00, 0.03, R, F, 1993-03-29, 1993-01-06, 1993-04-16, TAKE BACK RETURN, TRUCK, arefully. slyly express packag +614, 186897, 9416, 2, 48, 95226.72, 0.07, 0.07, A, F, 1993-03-09, 1993-01-19, 1993-03-19, DELIVER IN PERSON, SHIP, riously special excuses haggle along the +614, 166963, 4512, 3, 43, 87288.28, 0.05, 0.00, A, F, 1993-03-07, 1993-02-22, 1993-03-18, DELIVER IN PERSON, SHIP, express accounts wake. slyly ironic ins +614, 146951, 4494, 4, 14, 27971.30, 0.04, 0.06, A, F, 1992-12-03, 1993-02-14, 1992-12-27, DELIVER IN PERSON, SHIP, ular packages haggle about the pack +614, 195308, 7828, 5, 30, 42099.00, 0.08, 0.07, R, F, 1993-01-16, 1993-02-08, 1993-02-12, TAKE BACK RETURN, FOB, tructions are f +614, 136241, 1268, 6, 48, 61307.52, 0.04, 0.08, A, F, 1992-12-14, 1993-01-22, 1993-01-11, NONE, TRUCK, regular platelets cajole quickly eve +615, 104545, 4546, 1, 36, 55783.44, 0.10, 0.01, A, F, 1992-06-01, 1992-07-14, 1992-06-27, NONE, FOB, packages. carefully final pinto bea +640, 92997, 525, 1, 49, 97509.51, 0.09, 0.02, R, F, 1993-03-27, 1993-04-17, 1993-04-15, NONE, RAIL, s haggle slyly +640, 416, 2917, 2, 40, 52656.40, 0.09, 0.05, A, F, 1993-05-11, 1993-04-11, 1993-05-15, COLLECT COD, TRUCK, oach according to the bol +640, 179475, 7027, 3, 22, 34198.34, 0.05, 0.07, A, F, 1993-05-07, 1993-04-14, 1993-05-21, TAKE BACK RETURN, TRUCK, osits across the slyly regular theodo +640, 31474, 1475, 4, 45, 63246.15, 0.07, 0.07, R, F, 1993-04-15, 1993-04-23, 1993-04-21, DELIVER IN PERSON, REG AIR, ong the qui +641, 125192, 2729, 1, 18, 21909.42, 0.01, 0.08, R, F, 1993-10-17, 1993-10-11, 1993-10-29, DELIVER IN PERSON, AIR, p blithely bold packages. quick +641, 99477, 1987, 2, 1, 1476.47, 0.09, 0.02, R, F, 1993-12-03, 1993-10-28, 1993-12-26, TAKE BACK RETURN, RAIL, nag across the regular foxes. +641, 94311, 6821, 3, 40, 52212.40, 0.05, 0.06, R, F, 1993-11-22, 1993-10-20, 1993-12-11, DELIVER IN PERSON, REG AIR, lets. furiously regular requests cajo +641, 70043, 5058, 4, 25, 25326.00, 0.03, 0.02, A, F, 1993-12-04, 1993-11-18, 1993-12-18, TAKE BACK RETURN, FOB, d regular d +641, 3794, 8795, 5, 41, 69609.39, 0.07, 0.04, R, F, 1993-11-29, 1993-10-27, 1993-12-04, TAKE BACK RETURN, FOB, asymptotes are quickly. bol +642, 53624, 3625, 1, 26, 41018.12, 0.10, 0.03, A, F, 1994-04-16, 1994-02-01, 1994-04-27, COLLECT COD, REG AIR, quests according to the unu +643, 12260, 9764, 1, 28, 32823.28, 0.00, 0.08, A, F, 1995-04-13, 1995-05-12, 1995-04-14, TAKE BACK RETURN, TRUCK, ly regular requests nag sly +643, 50168, 169, 2, 48, 53671.68, 0.01, 0.02, N, O, 1995-07-10, 1995-06-07, 1995-08-01, NONE, FOB, ly ironic accounts +643, 162447, 4964, 3, 23, 34717.12, 0.05, 0.03, N, O, 1995-07-09, 1995-05-18, 1995-07-31, COLLECT COD, RAIL, sits are carefully according to the e +643, 44743, 2256, 4, 39, 65821.86, 0.08, 0.04, A, F, 1995-06-08, 1995-06-16, 1995-06-13, COLLECT COD, RAIL, the pains. carefully s +643, 189459, 4496, 5, 47, 72777.15, 0.10, 0.03, R, F, 1995-04-05, 1995-06-14, 1995-04-26, DELIVER IN PERSON, RAIL, y against +644, 133143, 5657, 1, 46, 54102.44, 0.02, 0.01, A, F, 1992-05-20, 1992-06-14, 1992-06-14, DELIVER IN PERSON, RAIL, special requests was sometimes expre +644, 129821, 7358, 2, 11, 20359.02, 0.05, 0.02, A, F, 1992-08-20, 1992-07-21, 1992-09-11, TAKE BACK RETURN, TRUCK, ealthy pinto beans use carefu +644, 100047, 5068, 3, 44, 46069.76, 0.04, 0.04, R, F, 1992-08-17, 1992-07-26, 1992-08-20, COLLECT COD, REG AIR, iously ironic pinto beans. bold packa +644, 79744, 7266, 4, 7, 12066.18, 0.01, 0.02, A, F, 1992-05-18, 1992-07-01, 1992-06-07, COLLECT COD, RAIL, regular requests are blithely. slyly +644, 49295, 9296, 5, 23, 28618.67, 0.02, 0.04, R, F, 1992-07-31, 1992-07-28, 1992-08-13, DELIVER IN PERSON, TRUCK, uctions nag quickly alongside of t +644, 84932, 2457, 6, 33, 63258.69, 0.00, 0.07, R, F, 1992-08-26, 1992-07-27, 1992-08-28, NONE, AIR, ages sleep. bold bo +644, 50239, 2745, 7, 38, 45190.74, 0.08, 0.06, R, F, 1992-05-17, 1992-07-10, 1992-06-06, TAKE BACK RETURN, MAIL, packages. blithely slow accounts nag quic +645, 159694, 2210, 1, 33, 57871.77, 0.01, 0.02, A, F, 1994-12-09, 1995-02-21, 1995-01-03, NONE, TRUCK, heodolites b +645, 169422, 9423, 2, 47, 70096.74, 0.07, 0.05, R, F, 1995-02-16, 1995-02-15, 1995-02-25, COLLECT COD, TRUCK, hely regular instructions alon +645, 69227, 4240, 3, 46, 55026.12, 0.10, 0.01, A, F, 1995-01-04, 1995-02-21, 1995-01-21, COLLECT COD, REG AIR, regular dependencies across the speci +645, 95402, 421, 4, 49, 68472.60, 0.05, 0.03, R, F, 1995-01-24, 1995-01-06, 1995-02-17, NONE, TRUCK, y. slyly iron +645, 4703, 7204, 5, 43, 69131.10, 0.06, 0.02, A, F, 1995-02-12, 1995-02-27, 1995-03-06, TAKE BACK RETURN, REG AIR, furiously accounts. slyly +645, 33631, 8638, 6, 18, 28163.34, 0.10, 0.08, A, F, 1995-03-02, 1995-02-08, 1995-03-03, COLLECT COD, RAIL, ep. slyly even +645, 27031, 7032, 7, 9, 8622.27, 0.03, 0.03, A, F, 1994-12-25, 1995-01-04, 1995-01-15, COLLECT COD, REG AIR, special deposits. regular final th +646, 108975, 6506, 1, 31, 61503.07, 0.00, 0.05, R, F, 1994-12-17, 1995-02-16, 1995-01-04, COLLECT COD, MAIL, ag furiousl +646, 126723, 6724, 2, 1, 1749.72, 0.07, 0.01, A, F, 1994-12-05, 1995-01-07, 1994-12-31, TAKE BACK RETURN, MAIL, t blithely regular deposits. quic +646, 29744, 4749, 3, 24, 40169.76, 0.06, 0.02, A, F, 1995-02-20, 1994-12-30, 1995-03-16, TAKE BACK RETURN, TRUCK, regular accounts haggle dog +646, 98738, 3757, 4, 34, 59048.82, 0.01, 0.00, R, F, 1994-12-28, 1994-12-27, 1994-12-31, COLLECT COD, SHIP, slow accounts. fluffily idle instructions +646, 89173, 4190, 5, 17, 19756.89, 0.04, 0.01, A, F, 1994-12-31, 1994-12-26, 1995-01-01, DELIVER IN PERSON, REG AIR, inal packages haggle carefully +646, 114481, 9504, 6, 40, 59819.20, 0.10, 0.01, R, F, 1995-01-01, 1995-01-13, 1995-01-11, COLLECT COD, TRUCK, ronic packages sleep across th +647, 16310, 8812, 1, 41, 50278.71, 0.08, 0.08, N, O, 1997-11-19, 1997-09-24, 1997-12-15, COLLECT COD, REG AIR, r instructions. quickly unusu +647, 112177, 7200, 2, 5, 5945.85, 0.10, 0.00, N, O, 1997-09-25, 1997-09-22, 1997-10-25, TAKE BACK RETURN, AIR, ly express packages haggle caref +647, 152882, 7913, 3, 15, 29023.20, 0.08, 0.00, N, O, 1997-09-23, 1997-10-09, 1997-10-21, NONE, MAIL, ve the even bold foxes sleep +672, 172190, 2191, 1, 41, 51749.79, 0.06, 0.06, R, F, 1994-06-20, 1994-07-03, 1994-06-22, COLLECT COD, REG AIR, dependencies in +672, 189656, 9657, 2, 9, 15710.85, 0.03, 0.04, R, F, 1994-06-25, 1994-06-06, 1994-07-19, TAKE BACK RETURN, TRUCK, haggle carefully carefully reg +672, 142390, 9933, 3, 35, 50133.65, 0.02, 0.01, R, F, 1994-07-13, 1994-06-04, 1994-07-14, COLLECT COD, RAIL, dependencies haggle quickly. theo +673, 70495, 5510, 1, 22, 32240.78, 0.03, 0.02, R, F, 1994-03-15, 1994-04-27, 1994-03-29, TAKE BACK RETURN, TRUCK, the regular even requests. carefully fin +674, 101366, 3877, 1, 23, 31449.28, 0.06, 0.07, A, F, 1992-10-25, 1992-10-15, 1992-11-03, COLLECT COD, SHIP, ve the quickly even deposits. blithe +674, 58285, 3296, 2, 4, 4973.12, 0.02, 0.07, R, F, 1992-10-05, 1992-11-22, 1992-10-22, NONE, RAIL, ly express pinto beans sleep car +675, 156455, 4001, 1, 1, 1511.45, 0.04, 0.08, N, O, 1997-11-27, 1997-09-30, 1997-12-12, DELIVER IN PERSON, REG AIR, ide of the slyly regular packages. unus +675, 136633, 4173, 2, 35, 58437.05, 0.08, 0.07, N, O, 1997-08-19, 1997-10-16, 1997-09-17, DELIVER IN PERSON, REG AIR, s. furiously expre +675, 175802, 8320, 3, 34, 63845.20, 0.10, 0.04, N, O, 1997-11-17, 1997-10-07, 1997-11-27, NONE, FOB, y final accounts unwind around the +675, 99269, 6797, 4, 15, 19023.90, 0.09, 0.05, N, O, 1997-10-18, 1997-09-28, 1997-11-13, COLLECT COD, TRUCK, posits after the furio +675, 4669, 7170, 5, 46, 72388.36, 0.09, 0.05, N, O, 1997-09-18, 1997-10-14, 1997-10-01, DELIVER IN PERSON, AIR, deposits along the express foxes +676, 50972, 8488, 1, 9, 17306.73, 0.09, 0.02, N, O, 1997-04-03, 1997-02-02, 1997-04-08, COLLECT COD, REG AIR, aintain sl +676, 77668, 5190, 2, 20, 32913.20, 0.07, 0.07, N, O, 1997-02-02, 1997-02-01, 1997-02-11, NONE, REG AIR, riously around the blithely +676, 162330, 2331, 3, 35, 48731.55, 0.05, 0.01, N, O, 1996-12-30, 1997-01-13, 1997-01-19, DELIVER IN PERSON, RAIL, into beans. blithe +676, 72825, 347, 4, 24, 43147.68, 0.01, 0.06, N, O, 1997-02-05, 1997-01-16, 1997-03-07, TAKE BACK RETURN, TRUCK, ress regular dep +676, 165127, 2676, 5, 31, 36955.72, 0.01, 0.06, N, O, 1997-02-06, 1997-02-28, 1997-03-08, COLLECT COD, TRUCK, ial deposits cajo +676, 75930, 5931, 6, 33, 62895.69, 0.09, 0.05, N, O, 1997-03-02, 1997-02-22, 1997-03-19, TAKE BACK RETURN, TRUCK, as wake slyly furiously close pinto b +676, 142123, 7152, 7, 11, 12816.32, 0.07, 0.02, N, O, 1997-03-09, 1997-03-06, 1997-03-31, TAKE BACK RETURN, MAIL, he final acco +677, 58986, 1492, 1, 32, 62239.36, 0.04, 0.08, R, F, 1994-01-06, 1994-01-31, 1994-02-02, NONE, RAIL, slyly final +677, 167361, 7362, 2, 39, 55706.04, 0.00, 0.07, R, F, 1993-12-19, 1994-02-11, 1994-01-05, TAKE BACK RETURN, SHIP, ges. furiously regular packages use +677, 23226, 3227, 3, 46, 52864.12, 0.01, 0.02, R, F, 1993-12-02, 1994-02-12, 1993-12-06, COLLECT COD, RAIL, ng theodolites. furiously unusual theodo +677, 147638, 5181, 4, 1, 1685.63, 0.06, 0.05, R, F, 1993-12-01, 1994-01-14, 1993-12-26, DELIVER IN PERSON, MAIL, ly. regular +677, 149613, 7156, 5, 25, 41565.25, 0.00, 0.05, A, F, 1994-03-12, 1994-02-02, 1994-03-28, DELIVER IN PERSON, AIR, packages integrate blithely +678, 145537, 5538, 1, 20, 31650.60, 0.05, 0.08, R, F, 1993-06-21, 1993-04-07, 1993-07-10, TAKE BACK RETURN, MAIL, furiously express excuses. foxes eat fu +678, 36553, 4063, 2, 22, 32770.10, 0.01, 0.02, A, F, 1993-05-10, 1993-04-29, 1993-06-08, NONE, REG AIR, de of the carefully even requests. bl +678, 142489, 5004, 3, 16, 24503.68, 0.06, 0.02, R, F, 1993-03-20, 1993-04-13, 1993-04-16, DELIVER IN PERSON, REG AIR, equests cajole around the carefully regular +678, 198067, 8068, 4, 48, 55922.88, 0.08, 0.08, R, F, 1993-02-28, 1993-04-04, 1993-03-24, NONE, REG AIR, ithely. slyly express foxes +678, 97451, 7452, 5, 16, 23175.20, 0.06, 0.04, R, F, 1993-03-09, 1993-04-18, 1993-04-07, NONE, AIR, about the +678, 42888, 2889, 6, 11, 20139.68, 0.09, 0.00, R, F, 1993-04-28, 1993-05-16, 1993-05-11, COLLECT COD, TRUCK, ess deposits dazzle f +679, 191759, 1760, 1, 9, 16656.75, 0.09, 0.00, N, O, 1995-12-20, 1996-01-27, 1996-01-07, COLLECT COD, REG AIR, leep slyly. entici +704, 189981, 9982, 1, 40, 82839.20, 0.05, 0.05, N, O, 1997-01-30, 1997-01-10, 1997-02-20, COLLECT COD, AIR, ggle quickly. r +704, 3839, 3840, 2, 14, 24399.62, 0.07, 0.08, N, O, 1997-02-02, 1996-12-26, 1997-02-19, DELIVER IN PERSON, REG AIR, ve the quickly final forges. furiously p +705, 188322, 841, 1, 46, 64874.72, 0.05, 0.06, N, O, 1997-04-18, 1997-05-06, 1997-05-05, DELIVER IN PERSON, SHIP, ss deposits. ironic packa +705, 116218, 3752, 2, 35, 43197.35, 0.10, 0.04, N, O, 1997-03-25, 1997-03-20, 1997-04-23, TAKE BACK RETURN, FOB, carefully ironic accounts +706, 196629, 9149, 1, 23, 39689.26, 0.05, 0.00, N, O, 1995-12-06, 1995-12-02, 1995-12-16, COLLECT COD, SHIP, ckey players. requests above the +707, 154736, 4737, 1, 34, 60884.82, 0.01, 0.02, R, F, 1994-12-08, 1995-01-15, 1995-01-02, NONE, RAIL, dependencies +707, 42642, 5147, 2, 22, 34862.08, 0.00, 0.06, A, F, 1995-01-12, 1994-12-28, 1995-01-16, DELIVER IN PERSON, REG AIR, kindle ironically +708, 123805, 1342, 1, 3, 5486.40, 0.05, 0.02, N, O, 1998-10-09, 1998-09-22, 1998-11-07, COLLECT COD, FOB, e slyly pending foxes. +708, 179124, 9125, 2, 19, 22859.28, 0.06, 0.00, N, O, 1998-10-28, 1998-09-23, 1998-11-25, COLLECT COD, SHIP, requests. even thin ideas +708, 121298, 8835, 3, 33, 43536.57, 0.09, 0.06, N, O, 1998-09-10, 1998-09-20, 1998-09-22, COLLECT COD, RAIL, s boost carefully ruthless theodolites. f +708, 55176, 5177, 4, 5, 5655.85, 0.07, 0.07, N, O, 1998-07-22, 1998-08-15, 1998-07-28, TAKE BACK RETURN, REG AIR, c pinto beans nag after the account +708, 142490, 33, 5, 36, 55169.64, 0.08, 0.01, N, O, 1998-07-16, 1998-09-04, 1998-08-11, NONE, SHIP, ests. even regular hockey p +708, 22352, 9859, 6, 7, 8920.45, 0.10, 0.03, N, O, 1998-08-16, 1998-08-15, 1998-09-10, COLLECT COD, REG AIR, lly express ac +709, 86203, 1220, 1, 7, 8324.40, 0.00, 0.00, N, O, 1998-06-14, 1998-06-08, 1998-06-18, TAKE BACK RETURN, RAIL, special orbits cajole +709, 197250, 9770, 2, 15, 20208.75, 0.08, 0.00, N, O, 1998-07-10, 1998-06-26, 1998-08-09, NONE, RAIL, ily regular deposits. sauternes was accor +709, 168496, 1013, 3, 10, 15644.90, 0.01, 0.02, N, O, 1998-06-04, 1998-06-30, 1998-06-11, NONE, REG AIR, ts cajole boldly +709, 107229, 7230, 4, 40, 49448.80, 0.10, 0.08, N, O, 1998-08-12, 1998-06-20, 1998-08-20, DELIVER IN PERSON, RAIL, ggle fluffily carefully ironic +710, 162111, 9660, 1, 47, 55136.17, 0.06, 0.08, A, F, 1993-01-18, 1993-03-24, 1993-01-24, TAKE BACK RETURN, MAIL, usual ideas into th +710, 192916, 2917, 2, 38, 76338.58, 0.07, 0.02, R, F, 1993-04-18, 1993-03-12, 1993-05-15, COLLECT COD, FOB, sts boost fluffily aft +710, 138984, 6524, 3, 7, 14160.86, 0.04, 0.06, R, F, 1993-01-20, 1993-03-28, 1993-02-15, TAKE BACK RETURN, REG AIR, xpress special ideas. bl +710, 89308, 9309, 4, 25, 32432.50, 0.00, 0.05, R, F, 1993-03-31, 1993-02-05, 1993-04-22, COLLECT COD, FOB, eas detect do +710, 185454, 491, 5, 12, 18473.40, 0.01, 0.02, A, F, 1993-02-18, 1993-02-27, 1993-03-07, DELIVER IN PERSON, MAIL, ions. slyly express theodolites al +710, 113665, 1199, 6, 21, 35251.86, 0.04, 0.06, R, F, 1993-03-22, 1993-03-05, 1993-03-27, DELIVER IN PERSON, SHIP, es. furiously p +710, 159288, 6834, 7, 46, 61974.88, 0.03, 0.07, R, F, 1993-04-16, 1993-03-27, 1993-05-05, COLLECT COD, MAIL, ges use; blithely pending excuses inte +711, 145767, 8282, 1, 2, 3625.52, 0.10, 0.04, R, F, 1993-12-01, 1993-12-09, 1993-12-16, DELIVER IN PERSON, REG AIR, ely across t +711, 102501, 7522, 2, 27, 40594.50, 0.00, 0.08, A, F, 1993-10-02, 1993-10-26, 1993-10-08, DELIVER IN PERSON, MAIL, slyly. ironic asy +711, 127086, 2111, 3, 46, 51201.68, 0.10, 0.00, R, F, 1993-12-26, 1993-11-19, 1994-01-21, TAKE BACK RETURN, MAIL, deposits. permanen +711, 127682, 7683, 4, 20, 34193.60, 0.09, 0.00, A, F, 1994-01-17, 1993-11-10, 1994-01-31, DELIVER IN PERSON, TRUCK, kly regular acco +736, 157380, 7381, 1, 46, 66119.48, 0.05, 0.01, N, O, 1998-07-16, 1998-09-01, 1998-08-09, NONE, AIR, uctions cajole +736, 79423, 9424, 2, 23, 32255.66, 0.02, 0.05, N, O, 1998-10-08, 1998-08-27, 1998-10-19, TAKE BACK RETURN, AIR, k accounts are carefully +736, 56626, 4142, 3, 13, 20574.06, 0.00, 0.03, N, O, 1998-08-16, 1998-07-26, 1998-08-19, DELIVER IN PERSON, FOB, st furiously among the +736, 97851, 5379, 4, 14, 25883.90, 0.06, 0.04, N, O, 1998-10-04, 1998-08-14, 1998-10-16, COLLECT COD, REG AIR, nstructions. +736, 168002, 3035, 5, 32, 34240.00, 0.04, 0.03, N, O, 1998-07-30, 1998-08-22, 1998-08-12, DELIVER IN PERSON, RAIL, iously final accoun +737, 181815, 9370, 1, 12, 22761.72, 0.01, 0.01, R, F, 1992-04-28, 1992-06-30, 1992-05-08, COLLECT COD, RAIL, posits after the slyly bold du +738, 197203, 2242, 1, 34, 44206.80, 0.00, 0.06, R, F, 1993-06-09, 1993-04-15, 1993-07-09, TAKE BACK RETURN, TRUCK, s against the ironic exc +738, 187016, 7017, 2, 4, 4412.04, 0.00, 0.03, A, F, 1993-06-20, 1993-04-08, 1993-07-09, NONE, AIR, ar packages. fluffily bo +738, 169700, 9701, 3, 23, 40703.10, 0.04, 0.08, A, F, 1993-03-17, 1993-04-02, 1993-04-05, TAKE BACK RETURN, SHIP, nic final excuses promise quickly regula +738, 140786, 8329, 4, 12, 21921.36, 0.04, 0.08, A, F, 1993-06-16, 1993-05-05, 1993-06-22, NONE, SHIP, ove the slyly regular p +738, 174837, 9872, 5, 30, 57354.90, 0.02, 0.00, A, F, 1993-06-12, 1993-05-29, 1993-06-25, NONE, AIR, ecial instructions haggle blithely regula +739, 84489, 6998, 1, 28, 41257.44, 0.00, 0.03, N, O, 1998-06-03, 1998-08-04, 1998-06-29, TAKE BACK RETURN, RAIL, elets about the pe +739, 3502, 6003, 2, 50, 70275.00, 0.07, 0.06, N, O, 1998-08-26, 1998-07-16, 1998-09-02, COLLECT COD, MAIL, ndencies. blith +739, 48733, 3742, 3, 12, 20180.76, 0.05, 0.00, N, O, 1998-08-20, 1998-07-24, 1998-08-22, NONE, MAIL, le slyly along the close i +739, 43470, 983, 4, 47, 66433.09, 0.09, 0.07, N, O, 1998-08-12, 1998-07-09, 1998-08-28, NONE, REG AIR, deas according to the theodolites sn +739, 187523, 5078, 5, 30, 48315.60, 0.07, 0.06, N, O, 1998-06-19, 1998-08-26, 1998-07-02, DELIVER IN PERSON, REG AIR, above the even deposits. ironic requests +740, 1206, 8707, 1, 22, 24358.40, 0.10, 0.02, N, O, 1995-07-24, 1995-09-11, 1995-08-11, TAKE BACK RETURN, FOB, odolites cajole ironic pending instruc +740, 65211, 2730, 2, 35, 41167.35, 0.00, 0.00, N, O, 1995-09-06, 1995-08-22, 1995-10-02, NONE, TRUCK, p quickly. fu +740, 198037, 8038, 3, 29, 32915.87, 0.06, 0.05, N, O, 1995-10-26, 1995-09-17, 1995-10-29, DELIVER IN PERSON, FOB, ntly bold pinto beans sleep quickl +741, 186404, 1441, 1, 25, 37260.00, 0.03, 0.06, N, O, 1998-07-15, 1998-08-27, 1998-08-12, DELIVER IN PERSON, MAIL, accounts. blithely bold pa +741, 90396, 5415, 2, 22, 30500.58, 0.09, 0.01, N, O, 1998-09-07, 1998-09-28, 1998-09-12, COLLECT COD, AIR, ven deposits about the regular ironi +742, 101309, 1310, 1, 46, 60273.80, 0.04, 0.08, A, F, 1995-03-12, 1995-03-20, 1995-03-16, TAKE BACK RETURN, SHIP, e slyly bold deposits cajole according to +742, 95395, 7905, 2, 15, 20855.85, 0.08, 0.05, A, F, 1995-02-26, 1995-03-20, 1995-03-03, NONE, SHIP, blithely unusual pinto +742, 101553, 4064, 3, 24, 37309.20, 0.08, 0.08, A, F, 1995-02-12, 1995-03-12, 1995-02-14, DELIVER IN PERSON, SHIP, affix slyly. furiously i +742, 191891, 4411, 4, 16, 31726.24, 0.01, 0.05, A, F, 1995-01-15, 1995-02-25, 1995-01-24, COLLECT COD, AIR, eodolites haggle carefully regul +742, 100006, 2517, 5, 48, 48288.00, 0.09, 0.08, R, F, 1995-03-24, 1995-01-23, 1995-04-08, TAKE BACK RETURN, TRUCK, platelets +742, 191966, 9524, 6, 49, 100840.04, 0.02, 0.07, A, F, 1995-01-13, 1995-02-13, 1995-01-26, TAKE BACK RETURN, RAIL, carefully bold foxes sle +743, 191460, 6499, 1, 21, 32580.66, 0.01, 0.04, N, O, 1996-10-26, 1996-11-05, 1996-11-11, COLLECT COD, MAIL, d requests. packages afte +768, 195813, 5814, 1, 39, 74443.59, 0.06, 0.08, N, O, 1996-09-25, 1996-10-27, 1996-10-20, NONE, SHIP, out the ironic +768, 17862, 7863, 2, 2, 3559.72, 0.00, 0.04, N, O, 1996-11-13, 1996-10-03, 1996-11-25, DELIVER IN PERSON, SHIP, ular courts. slyly dogged accou +768, 5964, 965, 3, 30, 56098.80, 0.06, 0.05, N, O, 1996-09-22, 1996-11-03, 1996-10-13, NONE, MAIL, furiously fluffy pinto beans haggle along +768, 24493, 2000, 4, 37, 52447.13, 0.10, 0.00, N, O, 1996-10-02, 1996-09-23, 1996-10-14, TAKE BACK RETURN, REG AIR, ending requests across the quickly +768, 46500, 1509, 5, 47, 67985.50, 0.06, 0.05, N, O, 1996-11-28, 1996-10-30, 1996-12-12, NONE, TRUCK, foxes. slyly ironic deposits a +768, 111635, 6658, 6, 43, 70805.09, 0.10, 0.06, N, O, 1996-09-22, 1996-11-03, 1996-10-22, TAKE BACK RETURN, AIR, sual ideas wake quickly +768, 48062, 8063, 7, 33, 33331.98, 0.01, 0.04, N, O, 1996-09-06, 1996-09-29, 1996-10-01, COLLECT COD, RAIL, sly ironic instructions. excuses can hagg +769, 175068, 7586, 1, 36, 41150.16, 0.02, 0.02, A, F, 1993-10-01, 1993-08-07, 1993-10-15, NONE, AIR, es. furiously iro +769, 159077, 1593, 2, 4, 4544.28, 0.01, 0.04, R, F, 1993-06-25, 1993-08-12, 1993-07-15, DELIVER IN PERSON, FOB, ideas. even +770, 180119, 2638, 1, 39, 46765.29, 0.09, 0.06, N, O, 1998-07-19, 1998-08-09, 1998-08-04, NONE, REG AIR, osits. foxes cajole +770, 53281, 5787, 2, 25, 30857.00, 0.03, 0.02, N, O, 1998-05-26, 1998-07-23, 1998-06-04, TAKE BACK RETURN, AIR, deposits dazzle fluffily alongside of +771, 6983, 4484, 1, 12, 22679.76, 0.10, 0.08, N, O, 1995-07-18, 1995-08-02, 1995-08-07, COLLECT COD, TRUCK, carefully. pending in +771, 160505, 3022, 2, 38, 59489.00, 0.03, 0.08, N, O, 1995-07-22, 1995-09-10, 1995-07-29, TAKE BACK RETURN, REG AIR, quickly final requests are final packages. +771, 6393, 6394, 3, 14, 18191.46, 0.02, 0.05, N, O, 1995-07-31, 1995-08-13, 1995-08-07, DELIVER IN PERSON, AIR, r final packages are slyly iro +771, 41262, 1263, 4, 7, 8422.82, 0.06, 0.02, N, O, 1995-06-18, 1995-08-31, 1995-06-20, NONE, REG AIR, theodolites after the fluffily express +771, 77402, 4924, 5, 13, 17932.20, 0.09, 0.01, N, O, 1995-08-10, 1995-08-21, 1995-08-30, NONE, FOB, packages affix slyly about the quickly +771, 81921, 9446, 6, 23, 43767.16, 0.08, 0.03, N, O, 1995-06-19, 1995-09-07, 1995-07-09, COLLECT COD, FOB, cajole besides the quickly ironic pin +772, 52698, 214, 1, 35, 57774.15, 0.10, 0.06, R, F, 1993-07-05, 1993-06-05, 1993-08-02, NONE, SHIP, kly thin packages wake slowly +772, 83007, 8024, 2, 10, 9900.00, 0.05, 0.01, R, F, 1993-05-20, 1993-05-19, 1993-06-15, DELIVER IN PERSON, MAIL, deposits cajole carefully instructions. t +772, 85119, 5120, 3, 35, 38643.85, 0.03, 0.04, R, F, 1993-04-18, 1993-06-13, 1993-05-01, COLLECT COD, MAIL, ng ideas. special packages haggle alon +772, 179834, 7386, 4, 10, 19138.30, 0.08, 0.02, A, F, 1993-05-17, 1993-06-09, 1993-05-29, COLLECT COD, AIR, o the furiously final deposits. furi +772, 53649, 3650, 5, 42, 67310.88, 0.02, 0.07, A, F, 1993-06-09, 1993-07-16, 1993-06-12, DELIVER IN PERSON, MAIL, express foxes abo +773, 99317, 9318, 1, 5, 6581.55, 0.06, 0.04, A, F, 1993-11-21, 1993-12-19, 1993-12-21, COLLECT COD, MAIL, ar requests. regular thin packages u +773, 10005, 2507, 2, 31, 28365.00, 0.02, 0.06, A, F, 1993-12-30, 1993-11-02, 1994-01-01, TAKE BACK RETURN, MAIL, e slyly unusual deposit +773, 150082, 7628, 3, 39, 44151.12, 0.06, 0.05, A, F, 1994-01-04, 1993-12-23, 1994-01-26, DELIVER IN PERSON, FOB, quickly eve +773, 28056, 3061, 4, 28, 27553.40, 0.10, 0.06, R, F, 1994-01-19, 1993-11-05, 1994-01-23, NONE, TRUCK, he furiously slow deposits. +773, 133007, 3008, 5, 9, 9360.00, 0.09, 0.02, R, F, 1993-10-09, 1993-12-25, 1993-11-04, TAKE BACK RETURN, FOB, ent orbits haggle fluffily after the +773, 39505, 9506, 6, 43, 62113.50, 0.07, 0.03, A, F, 1993-11-06, 1993-11-20, 1993-11-08, TAKE BACK RETURN, SHIP, furiously bold dependencies. blithel +774, 182652, 2653, 1, 49, 84997.85, 0.08, 0.03, N, O, 1995-12-06, 1996-01-07, 1995-12-14, DELIVER IN PERSON, SHIP, ess accounts are carefully +774, 16038, 1041, 2, 3, 2862.09, 0.10, 0.06, N, O, 1996-02-13, 1996-01-14, 1996-03-04, COLLECT COD, FOB, slyly even courts nag blith +774, 147809, 5352, 3, 34, 63131.20, 0.02, 0.07, N, O, 1996-03-16, 1996-01-03, 1996-03-22, COLLECT COD, FOB, lar excuses are furiously final instr +774, 14993, 4994, 4, 8, 15263.92, 0.00, 0.02, N, O, 1996-01-24, 1996-01-15, 1996-02-13, COLLECT COD, RAIL, ully ironic requests c +774, 176160, 3712, 5, 44, 54391.04, 0.09, 0.07, N, O, 1996-02-29, 1996-01-16, 1996-03-06, NONE, REG AIR, s according to the deposits unwind ca +774, 119813, 9814, 6, 2, 3665.62, 0.07, 0.03, N, O, 1995-12-11, 1996-02-10, 1995-12-14, TAKE BACK RETURN, SHIP, accounts; slyly regular +775, 31640, 6647, 1, 16, 25146.24, 0.10, 0.06, N, F, 1995-05-23, 1995-05-07, 1995-06-19, NONE, TRUCK, un quickly slyly +775, 173244, 796, 2, 21, 27662.04, 0.01, 0.06, R, F, 1995-05-01, 1995-06-02, 1995-05-13, DELIVER IN PERSON, FOB, quickly sile +775, 107093, 4624, 3, 20, 22001.80, 0.01, 0.08, N, F, 1995-06-17, 1995-05-22, 1995-07-13, COLLECT COD, AIR, en dependencies nag slowly +800, 71833, 6848, 1, 38, 68583.54, 0.00, 0.05, N, O, 1998-07-21, 1998-09-25, 1998-08-07, TAKE BACK RETURN, TRUCK, according to the bold final dependencies +800, 84262, 6771, 2, 21, 26171.46, 0.04, 0.05, N, O, 1998-07-23, 1998-10-01, 1998-08-20, TAKE BACK RETURN, RAIL, ckly even requests after the carefully r +800, 175028, 63, 3, 26, 28678.52, 0.01, 0.02, N, O, 1998-07-23, 1998-10-08, 1998-07-25, DELIVER IN PERSON, FOB, bove the pending requests. diff --git a/presto-example-http/src/test/resources/example-data/numbers-1.csv b/presto-example-http/src/test/resources/example-data/numbers-1.csv new file mode 100644 index 00000000..9b3f8dbc --- /dev/null +++ b/presto-example-http/src/test/resources/example-data/numbers-1.csv @@ -0,0 +1,3 @@ +one, 1 +two, 2 +three, 3 diff --git a/presto-example-http/src/test/resources/example-data/numbers-2.csv b/presto-example-http/src/test/resources/example-data/numbers-2.csv new file mode 100644 index 00000000..3153f3c1 --- /dev/null +++ b/presto-example-http/src/test/resources/example-data/numbers-2.csv @@ -0,0 +1,3 @@ +ten, 10 +eleven, 11 +twelve, 12 diff --git a/presto-example-http/src/test/resources/example-data/orders-1.csv b/presto-example-http/src/test/resources/example-data/orders-1.csv new file mode 100644 index 00000000..65a4a523 --- /dev/null +++ b/presto-example-http/src/test/resources/example-data/orders-1.csv @@ -0,0 +1,100 @@ +1, 36901, O, 173665.47, 1996-01-02, 5-LOW, Clerk#000000951, 0, nstructions sleep furiously among , +2, 78002, O, 46929.18, 1996-12-01, 1-URGENT, Clerk#000000880, 0, foxes. pending accounts at the pending, silent asymptot, +3, 123314, F, 193846.25, 1993-10-14, 5-LOW, Clerk#000000955, 0, sly final accounts boost. carefully regular ideas cajole carefully. depos, +4, 136777, O, 32151.78, 1995-10-11, 5-LOW, Clerk#000000124, 0, sits. slyly regular warthogs cajole. regular, regular theodolites acro, +5, 44485, F, 144659.20, 1994-07-30, 5-LOW, Clerk#000000925, 0, quickly. bold deposits sleep slyly. packages use slyly, +6, 55624, F, 58749.59, 1992-02-21, 4-NOT SPECIFIED, Clerk#000000058, 0, ggle. special, final requests are against the furiously specia, +7, 39136, O, 252004.18, 1996-01-10, 2-HIGH, Clerk#000000470, 0, ly special requests , +32, 130057, O, 208660.75, 1995-07-16, 2-HIGH, Clerk#000000616, 0, ise blithely bold, regular requests. quickly unusual dep, +33, 66958, F, 163243.98, 1993-10-27, 3-MEDIUM, Clerk#000000409, 0, uriously. furiously final request, +34, 61001, O, 58949.67, 1998-07-21, 3-MEDIUM, Clerk#000000223, 0, ly final packages. fluffily final deposits wake blithely ideas. spe, +35, 127588, O, 253724.56, 1995-10-23, 4-NOT SPECIFIED, Clerk#000000259, 0, zzle. carefully enticing deposits nag furio, +36, 115252, O, 68289.96, 1995-11-03, 1-URGENT, Clerk#000000358, 0, quick packages are blithely. slyly silent accounts wake qu, +37, 86116, F, 206680.66, 1992-06-03, 3-MEDIUM, Clerk#000000456, 0, kly regular pinto beans. carefully unusual waters cajole never, +38, 124828, O, 82500.05, 1996-08-21, 4-NOT SPECIFIED, Clerk#000000604, 0, haggle blithely. furiously express ideas haggle blithely furiously regular re, +39, 81763, O, 341734.47, 1996-09-20, 3-MEDIUM, Clerk#000000659, 0, ole express, ironic requests: ir, +64, 32113, F, 39414.99, 1994-07-16, 3-MEDIUM, Clerk#000000661, 0, wake fluffily. sometimes ironic pinto beans about the dolphin, +65, 16252, P, 110643.60, 1995-03-18, 1-URGENT, Clerk#000000632, 0, ular requests are blithely pending orbits-- even requests against the deposit, +66, 129200, F, 103740.67, 1994-01-20, 5-LOW, Clerk#000000743, 0, y pending requests integrate, +67, 56614, O, 169405.01, 1996-12-19, 4-NOT SPECIFIED, Clerk#000000547, 0, symptotes haggle slyly around the furiously iron, +68, 28547, O, 330793.52, 1998-04-18, 3-MEDIUM, Clerk#000000440, 0, pinto beans sleep carefully. blithely ironic deposits haggle furiously acro, +69, 84487, F, 197689.49, 1994-06-04, 4-NOT SPECIFIED, Clerk#000000330, 0, depths atop the slyly thin deposits detect among the furiously silent accou, +70, 64340, F, 113534.42, 1993-12-18, 5-LOW, Clerk#000000322, 0, carefully ironic request, +71, 3373, O, 276992.74, 1998-01-24, 4-NOT SPECIFIED, Clerk#000000271, 0, express deposits along the blithely regul, +96, 107779, F, 68989.90, 1994-04-17, 2-HIGH, Clerk#000000395, 0, oost furiously. pinto, +97, 21061, F, 110512.84, 1993-01-29, 3-MEDIUM, Clerk#000000547, 0, hang blithely along the regular accounts. furiously even ideas after the, +98, 104480, F, 69168.33, 1994-09-25, 1-URGENT, Clerk#000000448, 0, c asymptotes. quickly regular packages should have to nag re, +99, 88910, F, 112126.95, 1994-03-13, 4-NOT SPECIFIED, Clerk#000000973, 0, e carefully ironic packages. pending, +100, 147004, O, 187782.63, 1998-02-28, 4-NOT SPECIFIED, Clerk#000000577, 0, heodolites detect slyly alongside of the ent, +101, 27998, O, 124906.11, 1996-03-17, 3-MEDIUM, Clerk#000000419, 0, ding accounts above the slyly final asymptote, +102, 716, O, 164529.10, 1997-05-09, 2-HIGH, Clerk#000000596, 0, slyly according to the asymptotes. carefully final packages integrate furious, +103, 29101, O, 126990.79, 1996-06-20, 4-NOT SPECIFIED, Clerk#000000090, 0, ges. carefully unusual instructions haggle quickly regular f, +128, 73957, F, 66195.16, 1992-06-15, 1-URGENT, Clerk#000000385, 0, ns integrate fluffily. ironic asymptotes after the regular excuses nag around , +129, 71134, F, 261013.14, 1992-11-19, 5-LOW, Clerk#000000859, 0, ing tithes. carefully pending deposits boost about the silently express , +130, 36964, F, 189484.12, 1992-05-08, 2-HIGH, Clerk#000000036, 0, le slyly unusual, regular packages? express deposits det, +131, 92749, F, 130464.09, 1994-06-08, 3-MEDIUM, Clerk#000000625, 0, after the fluffily special foxes integrate s, +132, 26395, F, 144947.21, 1993-06-11, 3-MEDIUM, Clerk#000000488, 0, sits are daringly accounts. carefully regular foxes sleep slyly about the, +133, 44000, O, 114663.57, 1997-11-29, 1-URGENT, Clerk#000000738, 0, usly final asymptotes , +134, 6199, F, 200354.30, 1992-05-01, 4-NOT SPECIFIED, Clerk#000000711, 0, lar theodolites boos, +135, 60481, O, 213713.99, 1995-10-21, 4-NOT SPECIFIED, Clerk#000000804, 0, l platelets use according t, +160, 82495, O, 124132.06, 1996-12-19, 4-NOT SPECIFIED, Clerk#000000342, 0, thely special sauternes wake slyly of t, +161, 16619, F, 34439.93, 1994-08-31, 2-HIGH, Clerk#000000322, 0, carefully! special instructions sin, +162, 14116, O, 2726.45, 1995-05-08, 3-MEDIUM, Clerk#000000378, 0, nts hinder fluffily ironic instructions. express, express excuses , +163, 87760, O, 202379.28, 1997-09-05, 3-MEDIUM, Clerk#000000379, 0, y final packages. final foxes since the quickly even, +164, 779, F, 301925.76, 1992-10-21, 5-LOW, Clerk#000000209, 0, cajole ironic courts. slyly final ideas are slyly. blithely final Tiresias sub, +165, 27238, F, 188023.67, 1993-01-30, 4-NOT SPECIFIED, Clerk#000000292, 0, across the blithely regular accounts. bold, +166, 107812, O, 154451.92, 1995-09-12, 2-HIGH, Clerk#000000440, 0, lets. ironic, bold asymptotes kindle, +167, 119402, F, 71124.35, 1993-01-04, 4-NOT SPECIFIED, Clerk#000000731, 0, s nag furiously bold excuses. fluffily iron, +192, 82570, O, 167668.42, 1997-11-25, 5-LOW, Clerk#000000483, 0, y unusual platelets among the final instructions integrate rut, +193, 79061, F, 88699.17, 1993-08-08, 1-URGENT, Clerk#000000025, 0, the furiously final pin, +194, 61724, F, 176707.84, 1992-04-05, 3-MEDIUM, Clerk#000000352, 0, egular requests haggle slyly regular, regular pinto beans. asymptote, +195, 135421, F, 191542.31, 1993-12-28, 3-MEDIUM, Clerk#000000216, 0, old forges are furiously sheaves. slyly fi, +196, 64825, F, 47095.31, 1993-03-17, 2-HIGH, Clerk#000000988, 0, beans boost at the foxes. silent foxes, +197, 32512, P, 163700.57, 1995-04-07, 2-HIGH, Clerk#000000969, 0, solve quickly about the even braids. carefully express deposits affix care, +198, 110218, O, 176189.31, 1998-01-02, 4-NOT SPECIFIED, Clerk#000000331, 0, its. carefully ironic requests sleep. furiously express fox, +199, 52970, O, 112986.49, 1996-03-07, 2-HIGH, Clerk#000000489, 0, g theodolites. special packag, +224, 2476, F, 259960.36, 1994-06-18, 4-NOT SPECIFIED, Clerk#000000642, 0, r the quickly thin courts. carefully, +225, 33031, P, 220441.09, 1995-05-25, 1-URGENT, Clerk#000000177, 0, s. blithely ironic accounts wake quickly fluffily special acc, +226, 127466, F, 313134.62, 1993-03-10, 2-HIGH, Clerk#000000756, 0, s are carefully at the blithely ironic acc, +227, 9883, O, 54880.58, 1995-11-10, 5-LOW, Clerk#000000919, 0, express instructions. slyly regul, +228, 44078, F, 2749.87, 1993-02-25, 1-URGENT, Clerk#000000562, 0, es was slyly among the regular foxes. blithely regular dependenci, +229, 111728, F, 213076.69, 1993-12-29, 1-URGENT, Clerk#000000628, 0, he fluffily even instructions. furiously i, +230, 102535, F, 171318.37, 1993-10-27, 1-URGENT, Clerk#000000520, 0, odolites. carefully quick requ, +231, 90818, F, 191808.17, 1994-09-29, 2-HIGH, Clerk#000000446, 0, packages haggle slyly after the carefully ironic instruct, +256, 124831, F, 130775.45, 1993-10-19, 4-NOT SPECIFIED, Clerk#000000834, 0, he fluffily final ideas might are final accounts. carefully f, +257, 122693, O, 8649.81, 1998-03-28, 3-MEDIUM, Clerk#000000680, 0, ts against the sly warhorses cajole slyly accounts, +258, 41861, F, 294182.42, 1993-12-29, 1-URGENT, Clerk#000000167, 0, dencies. blithely quick packages cajole. ruthlessly final accounts, +259, 43186, F, 123298.34, 1993-09-29, 4-NOT SPECIFIED, Clerk#000000601, 0, ages doubt blithely against the final foxes. carefully express deposits dazzle, +260, 104728, O, 272353.52, 1996-12-10, 3-MEDIUM, Clerk#000000960, 0, lently regular pinto beans sleep after the slyly e, +261, 46072, F, 319306.86, 1993-06-29, 3-MEDIUM, Clerk#000000310, 0, ully fluffily brave instructions. furiousl, +262, 30352, O, 143146.21, 1995-11-25, 4-NOT SPECIFIED, Clerk#000000551, 0, l packages. blithely final pinto beans use carefu, +263, 116069, F, 149369.68, 1994-05-17, 2-HIGH, Clerk#000000088, 0, pending instructions. blithely un, +288, 7093, O, 266047.60, 1997-02-21, 1-URGENT, Clerk#000000109, 0, uriously final requests. even, final ideas det, +289, 103750, O, 211162.52, 1997-02-10, 3-MEDIUM, Clerk#000000103, 0, sily. slyly special excuse, +290, 117952, F, 99019.42, 1994-01-01, 4-NOT SPECIFIED, Clerk#000000735, 0, efully dogged deposits. furiou, +291, 141050, F, 108550.11, 1994-03-13, 1-URGENT, Clerk#000000923, 0, dolites. carefully regular pinto beans cajol, +292, 22252, F, 40637.14, 1992-01-13, 2-HIGH, Clerk#000000193, 0, g pinto beans will have to sleep f, +293, 29929, F, 58638.09, 1992-10-02, 2-HIGH, Clerk#000000629, 0, re bold, ironic deposits. platelets c, +294, 50498, F, 49457.90, 1993-07-16, 3-MEDIUM, Clerk#000000499, 0, kly according to the frays. final dolphins affix quickly , +295, 18985, F, 130843.69, 1994-09-29, 2-HIGH, Clerk#000000155, 0, unusual pinto beans play. regular ideas haggle, +320, 302, O, 53743.60, 1997-11-21, 2-HIGH, Clerk#000000573, 0, ar foxes nag blithely, +321, 122591, F, 86823.16, 1993-03-21, 3-MEDIUM, Clerk#000000289, 0, equests run. blithely final dependencies after the deposits wake caref, +322, 133546, F, 205623.50, 1992-03-19, 1-URGENT, Clerk#000000158, 0, fully across the slyly bold packages. packages against the quickly regular i, +323, 39133, F, 119432.07, 1994-03-26, 1-URGENT, Clerk#000000959, 0, arefully pending foxes sleep blithely. slyly express accoun, +324, 105155, F, 38451.38, 1992-03-20, 1-URGENT, Clerk#000000352, 0, about the ironic, regular deposits run blithely against the excuses, +325, 40024, F, 125113.94, 1993-10-17, 5-LOW, Clerk#000000844, 0, ly sometimes pending pa, +326, 75986, O, 327413.14, 1995-06-04, 2-HIGH, Clerk#000000466, 0, requests. furiously ironic asymptotes mold carefully alongside of the blit, +327, 144598, P, 38488.56, 1995-04-17, 5-LOW, Clerk#000000992, 0, ng the slyly final courts. slyly even escapades eat , +352, 106456, F, 28648.47, 1994-03-08, 2-HIGH, Clerk#000000932, 0, ke slyly bold pinto beans. blithely regular accounts against the spe, +353, 1777, F, 249710.43, 1993-12-31, 5-LOW, Clerk#000000449, 0, quiet ideas sleep. even instructions cajole slyly. silently spe, +354, 138268, O, 217160.72, 1996-03-14, 2-HIGH, Clerk#000000511, 0, ly regular ideas wake across the slyly silent ideas. final deposits eat b, +355, 70007, F, 99516.75, 1994-06-14, 5-LOW, Clerk#000000532, 0, s. sometimes regular requests cajole. regular, pending accounts a, +356, 146809, F, 209439.04, 1994-06-30, 4-NOT SPECIFIED, Clerk#000000944, 0, as wake along the bold accounts. even, , +357, 60395, O, 157411.61, 1996-10-09, 2-HIGH, Clerk#000000301, 0, e blithely about the express, final accounts. quickl, +358, 2290, F, 354132.39, 1993-09-20, 2-HIGH, Clerk#000000392, 0, l, silent instructions are slyly. silently even de, +359, 77600, F, 239998.53, 1994-12-19, 3-MEDIUM, Clerk#000000934, 0, n dolphins. special courts above the carefully ironic requests use, +384, 113009, F, 166753.71, 1992-03-03, 5-LOW, Clerk#000000206, 0, , even accounts use furiously packages. slyly ironic pla, +385, 32947, O, 54948.26, 1996-03-22, 5-LOW, Clerk#000000600, 0, hless accounts unwind bold pain, +386, 60110, F, 110216.57, 1995-01-25, 2-HIGH, Clerk#000000648, 0, haggle quickly. stealthily bold asymptotes haggle among the furiously even re, +387, 3296, O, 204546.39, 1997-01-26, 4-NOT SPECIFIED, Clerk#000000768, 0, are carefully among the quickly even deposits. furiously silent req, +388, 44668, F, 198800.71, 1992-12-16, 4-NOT SPECIFIED, Clerk#000000356, 0, ar foxes above the furiously ironic deposits nag slyly final reque, diff --git a/presto-example-http/src/test/resources/example-data/orders-2.csv b/presto-example-http/src/test/resources/example-data/orders-2.csv new file mode 100644 index 00000000..18406a1d --- /dev/null +++ b/presto-example-http/src/test/resources/example-data/orders-2.csv @@ -0,0 +1,100 @@ +389, 126973, F, 2519.40, 1994-02-17, 2-HIGH, Clerk#000000062, 0, ing to the regular asymptotes. final pending foxes about the blithely sil +390, 102563, O, 269761.09, 1998-04-07, 5-LOW, Clerk#000000404, 0, xpress asymptotes use among the regular final pinto b +391, 110278, F, 20890.17, 1994-11-17, 2-HIGH, Clerk#000000256, 0, orges thrash fluffil +416, 40130, F, 105675.20, 1993-09-27, 5-LOW, Clerk#000000294, 0, the accounts. fluffily bold depo +417, 54583, F, 125155.22, 1994-02-06, 3-MEDIUM, Clerk#000000468, 0, ironic even packages. thinly unusual accounts sleep along the slyly unusual +418, 94834, P, 53328.48, 1995-04-13, 4-NOT SPECIFIED, Clerk#000000643, 0, . furiously ironic instruc +419, 116261, O, 165454.42, 1996-10-01, 3-MEDIUM, Clerk#000000376, 0, osits. blithely pending theodolites boost carefully +420, 90145, O, 343254.06, 1995-10-31, 4-NOT SPECIFIED, Clerk#000000756, 0, leep carefully final excuses. fluffily pending requests unwind carefully above +421, 39149, F, 1156.67, 1992-02-22, 5-LOW, Clerk#000000405, 0, egular even packages according to the final un +422, 73075, O, 188124.81, 1997-05-31, 4-NOT SPECIFIED, Clerk#000000049, 0, aggle carefully across the accounts. regular accounts eat fluffi +423, 103396, O, 50240.88, 1996-06-01, 1-URGENT, Clerk#000000674, 0, quests. deposits cajole quickly. furiously bold accounts haggle q +448, 149641, O, 165954.35, 1995-08-21, 3-MEDIUM, Clerk#000000597, 0, regular express foxes use blithely. quic +449, 95767, O, 71120.82, 1995-07-20, 2-HIGH, Clerk#000000841, 0, . furiously regular theodolites affix blithely +450, 47380, P, 228518.02, 1995-03-05, 4-NOT SPECIFIED, Clerk#000000293, 0, d theodolites. boldly bold foxes since the pack +451, 98758, O, 141490.92, 1998-05-25, 5-LOW, Clerk#000000048, 0, nic pinto beans. theodolites poach carefully; +452, 59560, O, 3270.20, 1997-10-14, 1-URGENT, Clerk#000000498, 0, t unusual instructions above the blithely bold pint +453, 44030, O, 329149.33, 1997-05-26, 5-LOW, Clerk#000000504, 0, ss foxes. furiously regular ideas sleep according to t +454, 48776, O, 36743.83, 1995-12-27, 5-LOW, Clerk#000000890, 0, dolites sleep carefully blithely regular deposits. quickly regul +455, 12098, O, 183606.42, 1996-12-04, 1-URGENT, Clerk#000000796, 0, about the final platelets. dependen +480, 71383, F, 23699.64, 1993-05-08, 5-LOW, Clerk#000000004, 0, ealthy pinto beans. fluffily regular requests along the special sheaves wake +481, 30352, F, 201254.08, 1992-10-08, 2-HIGH, Clerk#000000230, 0, ly final ideas. packages haggle fluffily +482, 125059, O, 182312.78, 1996-03-26, 1-URGENT, Clerk#000000295, 0, ts. deposits wake: final acco +483, 34820, O, 70146.28, 1995-07-11, 2-HIGH, Clerk#000000025, 0, cross the carefully final e +484, 54244, O, 327889.57, 1997-01-03, 3-MEDIUM, Clerk#000000545, 0, grouches use. furiously bold accounts maintain. bold regular deposits +485, 100561, O, 192867.30, 1997-03-26, 2-HIGH, Clerk#000000105, 0, regular ideas nag thinly furiously s +486, 50861, O, 284644.07, 1996-03-11, 4-NOT SPECIFIED, Clerk#000000803, 0, riously dolphins. fluffily ironic requ +487, 107825, F, 90657.45, 1992-08-18, 1-URGENT, Clerk#000000086, 0, ithely unusual courts eat accordi +512, 63022, P, 194834.40, 1995-05-20, 5-LOW, Clerk#000000814, 0, ding requests. carefully express theodolites was quickly. furious +513, 60569, O, 105559.70, 1995-05-01, 2-HIGH, Clerk#000000522, 0, regular packages. pinto beans cajole carefully against the even +514, 74872, O, 154735.68, 1996-04-04, 2-HIGH, Clerk#000000094, 0, cajole furiously. slyly final excuses cajole. slyly special instructions +515, 141829, F, 244660.33, 1993-08-29, 4-NOT SPECIFIED, Clerk#000000700, 0, eposits are furiously furiously silent pinto beans. pending pack +516, 43903, O, 21920.56, 1998-04-21, 2-HIGH, Clerk#000000305, 0, lar unusual platelets are carefully. even courts sleep bold final pinto bea +517, 9220, O, 121396.01, 1997-04-07, 5-LOW, Clerk#000000359, 0, slyly pending deposits cajole quickly packages. furiou +518, 144382, O, 355180.76, 1998-02-08, 2-HIGH, Clerk#000000768, 0, the carefully bold accounts. quickly regular excuses are +519, 62953, O, 161219.18, 1997-10-31, 1-URGENT, Clerk#000000985, 0, ains doze furiously against the f +544, 93385, F, 69323.15, 1993-02-17, 2-HIGH, Clerk#000000145, 0, the special final accounts. dogged dolphins +545, 63143, O, 28984.07, 1995-11-07, 2-HIGH, Clerk#000000537, 0, as. blithely final hockey players about th +546, 143224, O, 23566.29, 1996-11-01, 2-HIGH, Clerk#000000041, 0, osits sleep. slyly special dolphins about the q +547, 98209, O, 149446.88, 1996-06-22, 3-MEDIUM, Clerk#000000976, 0, ing accounts eat. carefully regular packa +548, 123823, F, 165456.46, 1994-09-21, 1-URGENT, Clerk#000000435, 0, arefully express instru +549, 109921, F, 196410.67, 1992-07-13, 1-URGENT, Clerk#000000196, 0, ideas alongside of +550, 23524, O, 42415.31, 1995-08-02, 1-URGENT, Clerk#000000204, 0, t requests. blithely +551, 89608, O, 71032.36, 1995-05-30, 1-URGENT, Clerk#000000179, 0, xpress accounts boost quic +576, 29518, O, 26276.64, 1997-05-13, 3-MEDIUM, Clerk#000000955, 0, l requests affix regular requests. final account +577, 55126, F, 58874.63, 1994-12-19, 5-LOW, Clerk#000000154, 0, deposits engage stealthil +578, 92582, O, 93874.88, 1997-01-10, 5-LOW, Clerk#000000281, 0, e blithely even packages. slyly pending platelets bes +579, 67057, O, 161591.06, 1998-03-11, 2-HIGH, Clerk#000000862, 0, regular instructions. blithely even p +580, 59221, O, 137666.81, 1997-07-05, 2-HIGH, Clerk#000000314, 0, tegrate fluffily regular accou +581, 68734, O, 180542.57, 1997-02-23, 4-NOT SPECIFIED, Clerk#000000239, 0, requests. even requests use slyly. blithely ironic +582, 49358, O, 181843.80, 1997-10-21, 1-URGENT, Clerk#000000378, 0, n pinto beans print a +583, 47125, O, 185521.56, 1997-03-19, 3-MEDIUM, Clerk#000000792, 0, efully express requests. a +608, 25961, O, 85399.20, 1996-02-28, 3-MEDIUM, Clerk#000000995, 0, nic waters wake slyly slyly expre +609, 125122, F, 32712.15, 1994-06-01, 3-MEDIUM, Clerk#000000348, 0, - ironic gifts believe furiously ca +610, 50665, O, 278270.15, 1995-08-02, 1-URGENT, Clerk#000000610, 0, totes. ironic unusual packag +611, 105497, F, 127852.70, 1993-01-27, 1-URGENT, Clerk#000000401, 0, ounts detect furiously ac +612, 81430, F, 212763.09, 1992-10-21, 3-MEDIUM, Clerk#000000759, 0, boost quickly quickly final excuses. final foxes use bravely afte +613, 138254, O, 38120.35, 1995-06-18, 2-HIGH, Clerk#000000172, 0, ts hinder among the deposits. fluffily ironic depos +614, 133288, F, 337176.45, 1992-12-01, 2-HIGH, Clerk#000000388, 0, deposits! even daring theodol +615, 65407, F, 50707.14, 1992-05-09, 5-LOW, Clerk#000000388, 0, t to promise asymptotes. packages haggle alongside of the fluffil +640, 95138, F, 238520.33, 1993-01-23, 2-HIGH, Clerk#000000433, 0, r unusual accounts boost carefully final ideas. slyly silent theod +641, 132805, F, 169757.60, 1993-08-30, 5-LOW, Clerk#000000175, 0, ents cajole furiously about the quickly silent pac +642, 39907, F, 38023.78, 1993-12-16, 3-MEDIUM, Clerk#000000357, 0, among the requests wake slyly alongside of th +643, 57772, P, 254060.24, 1995-03-25, 2-HIGH, Clerk#000000354, 0, g dependencies. regular accounts +644, 7370, F, 272383.85, 1992-05-01, 1-URGENT, Clerk#000000550, 0, blithely unusual platelets haggle ironic special excuses. excuses unwi +645, 114224, F, 346179.67, 1994-12-03, 2-HIGH, Clerk#000000090, 0, quickly daring theodolites across the regu +646, 50417, F, 236726.73, 1994-11-22, 2-HIGH, Clerk#000000203, 0, carefully even foxes. fina +647, 142930, O, 82009.52, 1997-08-07, 1-URGENT, Clerk#000000270, 0, egular pearls. carefully express asymptotes are. even account +672, 108085, F, 117034.85, 1994-04-14, 5-LOW, Clerk#000000106, 0, egular requests are furiously according to +673, 79148, F, 31899.02, 1994-03-10, 1-URGENT, Clerk#000000448, 0, special pinto beans use quickly furiously even depende +674, 33130, F, 36846.48, 1992-08-29, 5-LOW, Clerk#000000448, 0, ully special deposits. furiously final warhorses affix carefully. fluffily f +675, 11704, O, 206195.97, 1997-07-31, 2-HIGH, Clerk#000000168, 0, ffily between the careful +676, 37930, O, 251888.78, 1996-12-13, 2-HIGH, Clerk#000000248, 0, the final deposits. special pending +677, 123877, F, 222824.61, 1993-11-24, 3-MEDIUM, Clerk#000000824, 0, uriously special pinto beans cajole carefully. fi +678, 130645, F, 185606.97, 1993-02-27, 5-LOW, Clerk#000000530, 0, . blithely final somas about the +679, 48494, O, 15157.64, 1995-12-15, 2-HIGH, Clerk#000000853, 0, tealthy final pinto beans haggle slyly. pending platelets about the special +704, 84352, O, 107139.07, 1996-11-21, 3-MEDIUM, Clerk#000000682, 0, blithely pending platelets wake alongside of the final iron +705, 42706, O, 105761.54, 1997-02-13, 4-NOT SPECIFIED, Clerk#000000294, 0, ithely regular dependencies. express even packages sleep slyly pending t +706, 147367, O, 37704.79, 1995-09-09, 1-URGENT, Clerk#000000448, 0, g the packages. deposits caj +707, 116150, F, 98435.28, 1994-11-20, 3-MEDIUM, Clerk#000000199, 0, ideas about the silent bold deposits nag dolphins +708, 31534, O, 133960.40, 1998-07-03, 3-MEDIUM, Clerk#000000101, 0, lphins cajole about t +709, 36029, O, 90778.89, 1998-04-21, 1-URGENT, Clerk#000000461, 0, ons alongside of the carefully bold pinto bea +710, 131137, F, 295703.61, 1993-01-02, 5-LOW, Clerk#000000026, 0, regular regular requests boost. fluffily re +711, 63673, F, 124433.21, 1993-09-23, 4-NOT SPECIFIED, Clerk#000000856, 0, its. fluffily regular gifts are furi +736, 46354, O, 176984.57, 1998-06-21, 5-LOW, Clerk#000000881, 0, refully of the final pi +737, 119666, F, 22759.44, 1992-04-26, 5-LOW, Clerk#000000233, 0, ake blithely express ironic theodolites. blithely special accounts wa +738, 20752, F, 172540.42, 1993-03-02, 4-NOT SPECIFIED, Clerk#000000669, 0, ly even foxes. furiously regular accounts cajole ca +739, 307, O, 243259.37, 1998-05-31, 5-LOW, Clerk#000000900, 0, against the slyly ironic packages nag slyly ironic +740, 43417, O, 96016.31, 1995-07-16, 3-MEDIUM, Clerk#000000583, 0, courts haggle furiously across the final regul +741, 104491, O, 66343.80, 1998-07-07, 2-HIGH, Clerk#000000295, 0, ic instructions. slyly express instructions solv +742, 102838, F, 305886.71, 1994-12-23, 5-LOW, Clerk#000000543, 0, equests? slyly ironic dolphins boost carefully above the blithely +743, 78322, O, 33545.04, 1996-10-04, 4-NOT SPECIFIED, Clerk#000000933, 0, eans. furiously ironic deposits sleep carefully carefully qui +768, 97054, O, 350817.47, 1996-08-20, 3-MEDIUM, Clerk#000000411, 0, jole slyly ironic packages. slyly even idea +769, 79987, F, 45812.47, 1993-06-02, 3-MEDIUM, Clerk#000000172, 0, ggle furiously. ironic packages haggle slyly. bold platelets affix s +770, 31909, O, 75639.70, 1998-05-23, 5-LOW, Clerk#000000572, 0, heodolites. furiously special pinto beans cajole pac +771, 44542, O, 169115.42, 1995-06-17, 1-URGENT, Clerk#000000105, 0, s. furiously final instructions across the deposit +772, 96496, F, 192141.04, 1993-04-17, 2-HIGH, Clerk#000000430, 0, s boost blithely fluffily idle ideas? fluffily even pin +773, 131834, F, 173949.22, 1993-09-26, 3-MEDIUM, Clerk#000000307, 0, tions are quickly accounts. accounts use bold even pinto beans. gifts ag +774, 79582, O, 221514.77, 1995-12-04, 1-URGENT, Clerk#000000883, 0, tealthily even depths +775, 133180, F, 76542.35, 1995-03-18, 1-URGENT, Clerk#000000191, 0, kly express requests. fluffily silent accounts poach furiously +800, 55940, O, 127353.10, 1998-07-14, 2-HIGH, Clerk#000000213, 0, y alongside of the pending packages? final platelets nag fluffily carefu diff --git a/presto-hive-cdh4/pom.xml b/presto-hive-cdh4/pom.xml new file mode 100644 index 00000000..609b975a --- /dev/null +++ b/presto-hive-cdh4/pom.xml @@ -0,0 +1,91 @@ + + + 4.0.0 + + + com.facebook.presto + presto-root + 0.107 + + + presto-hive-cdh4 + Presto - Hive Connector - CDH 4 + presto-plugin + + + ${project.parent.basedir} + + + + + com.facebook.presto + presto-hive + + + + com.facebook.presto.hadoop + hadoop-cdh4 + runtime + + + + + org.testng + testng + test + + + + io.airlift + testing + test + + + + com.facebook.presto + presto-hive + test-jar + test + + + + com.facebook.presto + presto-main + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + hive,hive-s3 + + + + + + + + test-hive-cdh4 + + + + org.apache.maven.plugins + maven-surefire-plugin + + hive-s3 + + localhost + 9083 + default + + + + + + + + diff --git a/presto-hive-cdh4/src/main/java/com/facebook/presto/hive/HiveCdh4Plugin.java b/presto-hive-cdh4/src/main/java/com/facebook/presto/hive/HiveCdh4Plugin.java new file mode 100644 index 00000000..20ba182f --- /dev/null +++ b/presto-hive-cdh4/src/main/java/com/facebook/presto/hive/HiveCdh4Plugin.java @@ -0,0 +1,23 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +public class HiveCdh4Plugin + extends HivePlugin +{ + public HiveCdh4Plugin() + { + super("hive-cdh4"); + } +} diff --git a/presto-hive-cdh4/src/test/java/com/facebook/presto/hive/TestHiveClient.java b/presto-hive-cdh4/src/test/java/com/facebook/presto/hive/TestHiveClient.java new file mode 100644 index 00000000..8f01eede --- /dev/null +++ b/presto-hive-cdh4/src/test/java/com/facebook/presto/hive/TestHiveClient.java @@ -0,0 +1,31 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +@Test +public class TestHiveClient + extends AbstractTestHiveClient +{ + @Parameters({"hive.cdh4.metastoreHost", "hive.cdh4.metastorePort", "hive.cdh4.databaseName", "hive.cdh4.timeZone"}) + @BeforeClass + @Override + public void setup(String host, int port, String databaseName, String timeZone) + { + super.setup(host, port, databaseName, timeZone); + } +} diff --git a/presto-hive-cdh4/src/test/java/com/facebook/presto/hive/TestHiveClientS3.java b/presto-hive-cdh4/src/test/java/com/facebook/presto/hive/TestHiveClientS3.java new file mode 100644 index 00000000..75123661 --- /dev/null +++ b/presto-hive-cdh4/src/test/java/com/facebook/presto/hive/TestHiveClientS3.java @@ -0,0 +1,38 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +@Test +public class TestHiveClientS3 + extends AbstractTestHiveClientS3 +{ + @Parameters({ + "hive.cdh4.metastoreHost", + "hive.cdh4.metastorePort", + "hive.cdh4.databaseName", + "hive.cdh4.s3.awsAccessKey", + "hive.cdh4.s3.awsSecretKey", + "hive.cdh4.s3.writableBucket", + }) + @BeforeClass + @Override + public void setup(String host, int port, String databaseName, String awsAccessKey, String awsSecretKey, String writableBucket) + { + super.setup(host, port, databaseName, awsAccessKey, awsSecretKey, writableBucket); + } +} diff --git a/presto-hive-cdh4/src/test/java/com/facebook/presto/hive/TestSplitIteratorBackpressure.java b/presto-hive-cdh4/src/test/java/com/facebook/presto/hive/TestSplitIteratorBackpressure.java new file mode 100644 index 00000000..e49c8b07 --- /dev/null +++ b/presto-hive-cdh4/src/test/java/com/facebook/presto/hive/TestSplitIteratorBackpressure.java @@ -0,0 +1,31 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +@Test +public class TestSplitIteratorBackpressure + extends AbstractTestSplitIteratorBackpressure +{ + @Parameters({"hive.cdh4.metastoreHost", "hive.cdh4.metastorePort", "hive.cdh4.databaseName", "hive.cdh4.timeZone"}) + @BeforeClass + @Override + protected void setup(String host, int port, String databaseName, String timeZone) + { + super.setup(host, port, databaseName, timeZone); + } +} diff --git a/presto-hive-cdh5/pom.xml b/presto-hive-cdh5/pom.xml new file mode 100644 index 00000000..afba290a --- /dev/null +++ b/presto-hive-cdh5/pom.xml @@ -0,0 +1,91 @@ + + + 4.0.0 + + + com.facebook.presto + presto-root + 0.107 + + + presto-hive-cdh5 + Presto - Hive Connector - CDH 5 + presto-plugin + + + ${project.parent.basedir} + + + + + com.facebook.presto + presto-hive + + + + com.facebook.presto.hadoop + hadoop-apache2 + runtime + + + + + org.testng + testng + test + + + + io.airlift + testing + test + + + + com.facebook.presto + presto-hive + test-jar + test + + + + com.facebook.presto + presto-main + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + hive,hive-s3 + + + + + + + + test-hive-cdh5 + + + + org.apache.maven.plugins + maven-surefire-plugin + + hive-s3 + + localhost + 9083 + default + + + + + + + + diff --git a/presto-hive-cdh5/src/main/java/com/facebook/presto/hive/HiveCdh5Plugin.java b/presto-hive-cdh5/src/main/java/com/facebook/presto/hive/HiveCdh5Plugin.java new file mode 100644 index 00000000..e0b59d41 --- /dev/null +++ b/presto-hive-cdh5/src/main/java/com/facebook/presto/hive/HiveCdh5Plugin.java @@ -0,0 +1,23 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +public class HiveCdh5Plugin + extends HivePlugin +{ + public HiveCdh5Plugin() + { + super("hive-cdh5"); + } +} diff --git a/presto-hive-cdh5/src/test/java/com/facebook/presto/hive/TestHiveClient.java b/presto-hive-cdh5/src/test/java/com/facebook/presto/hive/TestHiveClient.java new file mode 100644 index 00000000..3f05d450 --- /dev/null +++ b/presto-hive-cdh5/src/test/java/com/facebook/presto/hive/TestHiveClient.java @@ -0,0 +1,31 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +@Test +public class TestHiveClient + extends AbstractTestHiveClient +{ + @Parameters({"hive.cdh5.metastoreHost", "hive.cdh5.metastorePort", "hive.cdh5.databaseName", "hive.cdh5.timeZone"}) + @BeforeClass + @Override + public void setup(String host, int port, String databaseName, String timeZone) + { + super.setup(host, port, databaseName, timeZone); + } +} diff --git a/presto-hive-cdh5/src/test/java/com/facebook/presto/hive/TestHiveClientS3.java b/presto-hive-cdh5/src/test/java/com/facebook/presto/hive/TestHiveClientS3.java new file mode 100644 index 00000000..acfc4e38 --- /dev/null +++ b/presto-hive-cdh5/src/test/java/com/facebook/presto/hive/TestHiveClientS3.java @@ -0,0 +1,38 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +@Test +public class TestHiveClientS3 + extends AbstractTestHiveClientS3 +{ + @Parameters({ + "hive.cdh5.metastoreHost", + "hive.cdh5.metastorePort", + "hive.cdh5.databaseName", + "hive.cdh5.s3.awsAccessKey", + "hive.cdh5.s3.awsSecretKey", + "hive.cdh5.s3.writableBucket", + }) + @BeforeClass + @Override + public void setup(String host, int port, String databaseName, String awsAccessKey, String awsSecretKey, String writableBucket) + { + super.setup(host, port, databaseName, awsAccessKey, awsSecretKey, writableBucket); + } +} diff --git a/presto-hive-cdh5/src/test/java/com/facebook/presto/hive/TestSplitIteratorBackpressure.java b/presto-hive-cdh5/src/test/java/com/facebook/presto/hive/TestSplitIteratorBackpressure.java new file mode 100644 index 00000000..f8878bc4 --- /dev/null +++ b/presto-hive-cdh5/src/test/java/com/facebook/presto/hive/TestSplitIteratorBackpressure.java @@ -0,0 +1,31 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +@Test +public class TestSplitIteratorBackpressure + extends AbstractTestSplitIteratorBackpressure +{ + @Parameters({"hive.cdh5.metastoreHost", "hive.cdh5.metastorePort", "hive.cdh5.databaseName", "hive.cdh5.timeZone"}) + @BeforeClass + @Override + public void setup(String host, int port, String databaseName, String timeZone) + { + super.setup(host, port, databaseName, timeZone); + } +} diff --git a/presto-hive-hadoop1/pom.xml b/presto-hive-hadoop1/pom.xml new file mode 100644 index 00000000..f3e51ef3 --- /dev/null +++ b/presto-hive-hadoop1/pom.xml @@ -0,0 +1,91 @@ + + + 4.0.0 + + + com.facebook.presto + presto-root + 0.107 + + + presto-hive-hadoop1 + Presto - Hive Connector - Apache Hadoop 1.x + presto-plugin + + + ${project.parent.basedir} + + + + + com.facebook.presto + presto-hive + + + + com.facebook.presto.hadoop + hadoop-apache1 + runtime + + + + + org.testng + testng + test + + + + io.airlift + testing + test + + + + com.facebook.presto + presto-hive + test-jar + test + + + + com.facebook.presto + presto-main + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + hive,hive-s3 + + + + + + + + test-hive-hadoop1 + + + + org.apache.maven.plugins + maven-surefire-plugin + + hive-s3 + + localhost + 9083 + default + + + + + + + + diff --git a/presto-hive-hadoop1/src/main/java/com/facebook/presto/hive/HiveHadoop1Plugin.java b/presto-hive-hadoop1/src/main/java/com/facebook/presto/hive/HiveHadoop1Plugin.java new file mode 100644 index 00000000..fa83097a --- /dev/null +++ b/presto-hive-hadoop1/src/main/java/com/facebook/presto/hive/HiveHadoop1Plugin.java @@ -0,0 +1,23 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +public class HiveHadoop1Plugin + extends HivePlugin +{ + public HiveHadoop1Plugin() + { + super("hive-hadoop1"); + } +} diff --git a/presto-hive-hadoop1/src/test/java/com/facebook/presto/hive/TestHiveClient.java b/presto-hive-hadoop1/src/test/java/com/facebook/presto/hive/TestHiveClient.java new file mode 100644 index 00000000..25f3fc87 --- /dev/null +++ b/presto-hive-hadoop1/src/test/java/com/facebook/presto/hive/TestHiveClient.java @@ -0,0 +1,31 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +@Test +public class TestHiveClient + extends AbstractTestHiveClient +{ + @Parameters({"hive.hadoop1.metastoreHost", "hive.hadoop1.metastorePort", "hive.hadoop1.databaseName", "hive.hadoop1.timeZone"}) + @BeforeClass + @Override + public void setup(String host, int port, String databaseName, String timeZone) + { + super.setup(host, port, databaseName, timeZone); + } +} diff --git a/presto-hive-hadoop1/src/test/java/com/facebook/presto/hive/TestHiveClientS3.java b/presto-hive-hadoop1/src/test/java/com/facebook/presto/hive/TestHiveClientS3.java new file mode 100644 index 00000000..c2323290 --- /dev/null +++ b/presto-hive-hadoop1/src/test/java/com/facebook/presto/hive/TestHiveClientS3.java @@ -0,0 +1,38 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +@Test +public class TestHiveClientS3 + extends AbstractTestHiveClientS3 +{ + @Parameters({ + "hive.hadoop1.metastoreHost", + "hive.hadoop1.metastorePort", + "hive.hadoop1.databaseName", + "hive.hadoop1.s3.awsAccessKey", + "hive.hadoop1.s3.awsSecretKey", + "hive.hadoop1.s3.writableBucket", + }) + @BeforeClass + @Override + public void setup(String host, int port, String databaseName, String awsAccessKey, String awsSecretKey, String writableBucket) + { + super.setup(host, port, databaseName, awsAccessKey, awsSecretKey, writableBucket); + } +} diff --git a/presto-hive-hadoop1/src/test/java/com/facebook/presto/hive/TestSplitIteratorBackpressure.java b/presto-hive-hadoop1/src/test/java/com/facebook/presto/hive/TestSplitIteratorBackpressure.java new file mode 100644 index 00000000..1a8a8e5a --- /dev/null +++ b/presto-hive-hadoop1/src/test/java/com/facebook/presto/hive/TestSplitIteratorBackpressure.java @@ -0,0 +1,31 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +@Test +public class TestSplitIteratorBackpressure + extends AbstractTestSplitIteratorBackpressure +{ + @Parameters({"hive.hadoop1.metastoreHost", "hive.hadoop1.metastorePort", "hive.hadoop1.databaseName", "hive.hadoop1.timeZone"}) + @BeforeClass + @Override + public void setup(String host, int port, String databaseName, String timeZone) + { + super.setup(host, port, databaseName, timeZone); + } +} diff --git a/presto-hive-hadoop2/pom.xml b/presto-hive-hadoop2/pom.xml new file mode 100644 index 00000000..8feb5b08 --- /dev/null +++ b/presto-hive-hadoop2/pom.xml @@ -0,0 +1,91 @@ + + + 4.0.0 + + + com.facebook.presto + presto-root + 0.107 + + + presto-hive-hadoop2 + Presto - Hive Connector - Apache Hadoop 2.x + presto-plugin + + + ${project.parent.basedir} + + + + + com.facebook.presto + presto-hive + + + + com.facebook.presto.hadoop + hadoop-apache2 + runtime + + + + + org.testng + testng + test + + + + io.airlift + testing + test + + + + com.facebook.presto + presto-hive + test-jar + test + + + + com.facebook.presto + presto-main + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + hive,hive-s3 + + + + + + + + test-hive-hadoop2 + + + + org.apache.maven.plugins + maven-surefire-plugin + + hive-s3 + + localhost + 9083 + default + + + + + + + + diff --git a/presto-hive-hadoop2/src/main/java/com/facebook/presto/hive/HiveHadoop2Plugin.java b/presto-hive-hadoop2/src/main/java/com/facebook/presto/hive/HiveHadoop2Plugin.java new file mode 100644 index 00000000..d322368e --- /dev/null +++ b/presto-hive-hadoop2/src/main/java/com/facebook/presto/hive/HiveHadoop2Plugin.java @@ -0,0 +1,23 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +public class HiveHadoop2Plugin + extends HivePlugin +{ + public HiveHadoop2Plugin() + { + super("hive-hadoop2"); + } +} diff --git a/presto-hive-hadoop2/src/test/java/com/facebook/presto/hive/TestHiveClient.java b/presto-hive-hadoop2/src/test/java/com/facebook/presto/hive/TestHiveClient.java new file mode 100644 index 00000000..4571940d --- /dev/null +++ b/presto-hive-hadoop2/src/test/java/com/facebook/presto/hive/TestHiveClient.java @@ -0,0 +1,31 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +@Test +public class TestHiveClient + extends AbstractTestHiveClient +{ + @Parameters({"hive.hadoop2.metastoreHost", "hive.hadoop2.metastorePort", "hive.hadoop2.databaseName", "hive.hadoop2.timeZone"}) + @BeforeClass + @Override + public void setup(String host, int port, String databaseName, String timeZone) + { + super.setup(host, port, databaseName, timeZone); + } +} diff --git a/presto-hive-hadoop2/src/test/java/com/facebook/presto/hive/TestHiveClientS3.java b/presto-hive-hadoop2/src/test/java/com/facebook/presto/hive/TestHiveClientS3.java new file mode 100644 index 00000000..abca64bf --- /dev/null +++ b/presto-hive-hadoop2/src/test/java/com/facebook/presto/hive/TestHiveClientS3.java @@ -0,0 +1,38 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +@Test +public class TestHiveClientS3 + extends AbstractTestHiveClientS3 +{ + @Parameters({ + "hive.hadoop2.metastoreHost", + "hive.hadoop2.metastorePort", + "hive.hadoop2.databaseName", + "hive.hadoop2.s3.awsAccessKey", + "hive.hadoop2.s3.awsSecretKey", + "hive.hadoop2.s3.writableBucket", + }) + @BeforeClass + @Override + public void setup(String host, int port, String databaseName, String awsAccessKey, String awsSecretKey, String writableBucket) + { + super.setup(host, port, databaseName, awsAccessKey, awsSecretKey, writableBucket); + } +} diff --git a/presto-hive-hadoop2/src/test/java/com/facebook/presto/hive/TestSplitIteratorBackpressure.java b/presto-hive-hadoop2/src/test/java/com/facebook/presto/hive/TestSplitIteratorBackpressure.java new file mode 100644 index 00000000..c56ae959 --- /dev/null +++ b/presto-hive-hadoop2/src/test/java/com/facebook/presto/hive/TestSplitIteratorBackpressure.java @@ -0,0 +1,31 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +@Test +public class TestSplitIteratorBackpressure + extends AbstractTestSplitIteratorBackpressure +{ + @Parameters({"hive.hadoop2.metastoreHost", "hive.hadoop2.metastorePort", "hive.hadoop2.databaseName", "hive.hadoop2.timeZone"}) + @BeforeClass + @Override + public void setup(String host, int port, String databaseName, String timeZone) + { + super.setup(host, port, databaseName, timeZone); + } +} diff --git a/presto-hive/pom.xml b/presto-hive/pom.xml new file mode 100644 index 00000000..34795d13 --- /dev/null +++ b/presto-hive/pom.xml @@ -0,0 +1,275 @@ + + + 4.0.0 + + + com.facebook.presto + presto-root + 0.107 + + + presto-hive + presto-hive + Presto - Hive Connector + + + ${project.parent.basedir} + + + + + com.facebook.hive + hive-dwrf + + + + com.facebook.hive + hive-dwrf-shims + + + + com.facebook.presto + presto-orc + + + + com.facebook.presto.hadoop + hadoop-cdh4 + provided + + + + com.facebook.presto.hive + hive-apache + + + + org.apache.thrift + libthrift + + + + io.airlift + stats + + + + io.airlift + bootstrap + + + + io.airlift + concurrent + + + + io.airlift + log + + + + io.airlift + json + + + + io.airlift + units + + + + io.airlift + node + + + + io.airlift + configuration + + + + io.airlift + discovery + + + + com.google.guava + guava + + + + com.google.inject + guice + + + + com.google.inject.extensions + guice-multibindings + + + + com.google.code.findbugs + annotations + + + + javax.validation + validation-api + + + + org.weakref + jmxutils + + + + joda-time + joda-time + + + + com.amazonaws + aws-java-sdk + + + joda-time + joda-time + + + + + + org.iq80.snappy + snappy + 0.3 + runtime + + + + org.xerial.snappy + snappy-java + 1.0.5 + runtime + + + + + com.facebook.presto + presto-spi + provided + + + + io.airlift + slice + provided + + + + javax.inject + javax.inject + provided + + + + com.fasterxml.jackson.core + jackson-annotations + provided + + + + com.fasterxml.jackson.core + jackson-core + provided + + + + com.fasterxml.jackson.core + jackson-databind + provided + + + + + com.facebook.presto + presto-main + test + + + + com.facebook.presto + presto-tests + test + + + + com.facebook.presto + presto-tpch + test + + + + io.airlift.tpch + tpch + test + + + + org.jetbrains + annotations + provided + + + + org.testng + testng + test + + + + io.airlift + testing + test + + + + + com.facebook.presto + presto-benchmark + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + **/TestHiveDistributedQueries.java + + + + + + + + + hive-integration + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + + + + + + diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/BackgroundHiveSplitLoader.java b/presto-hive/src/main/java/com/facebook/presto/hive/BackgroundHiveSplitLoader.java new file mode 100644 index 00000000..c6921ce5 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/BackgroundHiveSplitLoader.java @@ -0,0 +1,460 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.hive.util.HiveFileIterator; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.HostAddress; +import com.facebook.presto.spi.TupleDomain; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import io.airlift.units.DataSize; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.BlockLocation; +import org.apache.hadoop.fs.FileStatus; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.LocatedFileStatus; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.hive.metastore.MetaStoreUtils; +import org.apache.hadoop.hive.metastore.api.FieldSchema; +import org.apache.hadoop.hive.metastore.api.Partition; +import org.apache.hadoop.hive.metastore.api.Table; +import org.apache.hadoop.hive.ql.io.SymlinkTextInputFormat; +import org.apache.hadoop.mapred.FileInputFormat; +import org.apache.hadoop.mapred.FileSplit; +import org.apache.hadoop.mapred.InputFormat; +import org.apache.hadoop.mapred.InputSplit; +import org.apache.hadoop.mapred.JobConf; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Properties; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicInteger; + +import static com.facebook.presto.hadoop.HadoopFileStatus.isDirectory; +import static com.facebook.presto.hadoop.HadoopFileStatus.isFile; +import static com.facebook.presto.hive.HiveBucketing.HiveBucket; +import static com.facebook.presto.hive.HiveErrorCode.HIVE_INVALID_METADATA; +import static com.facebook.presto.hive.HiveErrorCode.HIVE_INVALID_PARTITION_VALUE; +import static com.facebook.presto.hive.HiveType.getSupportedHiveType; +import static com.facebook.presto.hive.HiveUtil.checkCondition; +import static com.facebook.presto.hive.HiveUtil.getInputFormat; +import static com.facebook.presto.hive.HiveUtil.isSplittable; +import static com.facebook.presto.hive.UnpartitionedPartition.isUnpartitioned; +import static com.google.common.base.Preconditions.checkState; + +public class BackgroundHiveSplitLoader + implements HiveSplitLoader +{ + private final String connectorId; + private final Table table; + private final Optional bucket; + private final HdfsEnvironment hdfsEnvironment; + private final NamenodeStats namenodeStats; + private final DirectoryLister directoryLister; + private final DataSize maxSplitSize; + private final int maxPartitionBatchSize; + private final DataSize maxInitialSplitSize; + private final boolean recursiveDirWalkerEnabled; + private final boolean forceLocalScheduling; + private final Executor executor; + private final ConnectorSession session; + private final AtomicInteger outstandingTasks = new AtomicInteger(); + private final ConcurrentLazyQueue partitions; + private final Deque fileIterators = new ConcurrentLinkedDeque<>(); + private final AtomicInteger remainingInitialSplits; + + private HiveSplitSource hiveSplitSource; + private volatile boolean stopped; + + public BackgroundHiveSplitLoader( + String connectorId, + Table table, + Iterable partitions, + Optional bucket, + DataSize maxSplitSize, + ConnectorSession session, + HdfsEnvironment hdfsEnvironment, + NamenodeStats namenodeStats, + DirectoryLister directoryLister, + Executor executor, + int maxPartitionBatchSize, + DataSize maxInitialSplitSize, + int maxInitialSplits, + boolean forceLocalScheduling, + boolean recursiveDirWalkerEnabled) + { + this.connectorId = connectorId; + this.table = table; + this.bucket = bucket; + this.maxSplitSize = maxSplitSize; + this.maxPartitionBatchSize = maxPartitionBatchSize; + this.session = session; + this.hdfsEnvironment = hdfsEnvironment; + this.namenodeStats = namenodeStats; + this.directoryLister = directoryLister; + this.maxInitialSplitSize = maxInitialSplitSize; + this.remainingInitialSplits = new AtomicInteger(maxInitialSplits); + this.recursiveDirWalkerEnabled = recursiveDirWalkerEnabled; + this.forceLocalScheduling = forceLocalScheduling; + this.executor = executor; + this.partitions = new ConcurrentLazyQueue<>(partitions); + } + + @Override + public void start(HiveSplitSource splitSource) + { + this.hiveSplitSource = splitSource; + startLoadSplits(); + } + + @Override + public void resume() + { + if (outstandingTasks.get() == 0) { + startLoadSplits(); + } + } + + @Override + public void stop() + { + stopped = true; + } + + private void startLoadSplits() + { + if (stopped || (fileIterators.isEmpty() && partitions.isEmpty())) { + return; + } + if (outstandingTasks.incrementAndGet() > maxPartitionBatchSize) { + outstandingTasks.decrementAndGet(); + return; + } + executor.execute(() -> { + try { + loadSplits(); + if (outstandingTasks.decrementAndGet() == 0) { + if (fileIterators.isEmpty() && partitions.isEmpty()) { + hiveSplitSource.finished(); + return; + } + } + if (!hiveSplitSource.isQueueFull()) { + // Start another task to replace this one + startLoadSplits(); + // Ramp up if we're below the limit and the queue still isn't filled + if (outstandingTasks.get() < maxPartitionBatchSize) { + startLoadSplits(); + } + } + } + catch (Exception e) { + hiveSplitSource.fail(e); + } + }); + } + + private void loadSplits() + throws IOException + { + HiveFileIterator files = fileIterators.poll(); + if (files == null) { + HivePartitionMetadata partition = partitions.poll(); + if (partition != null) { + loadPartition(partition); + } + return; + } + + while (files.hasNext() && !stopped) { + LocatedFileStatus file = files.next(); + if (isDirectory(file)) { + if (recursiveDirWalkerEnabled) { + HiveFileIterator fileIterator = new HiveFileIterator( + file.getPath(), + files.getFileSystem(), + files.getDirectoryLister(), + files.getNamenodeStats(), + files.getPartitionName(), + files.getInputFormat(), + files.getSchema(), + files.getPartitionKeys(), + files.getEffectivePredicate()); + fileIterators.add(fileIterator); + } + } + else { + boolean splittable = isSplittable(files.getInputFormat(), hdfsEnvironment.getFileSystem(file.getPath()), file.getPath()); + + hiveSplitSource.addToQueue(createHiveSplits( + files.getPartitionName(), + file.getPath().toString(), + file.getBlockLocations(), + 0, + file.getLen(), + files.getSchema(), + files.getPartitionKeys(), + splittable, + session, + files.getEffectivePredicate())); + if (hiveSplitSource.isQueueFull()) { + fileIterators.addFirst(files); + return; + } + } + } + + // No need to put the iterator back, since it's either empty or we've stopped + } + + private void loadPartition(HivePartitionMetadata partition) + throws IOException + { + String partitionName = partition.getHivePartition().getPartitionId(); + Properties schema = getPartitionSchema(table, partition.getPartition()); + List partitionKeys = getPartitionKeys(table, partition.getPartition()); + TupleDomain effectivePredicate = partition.getHivePartition().getEffectivePredicate(); + + Path path = new Path(getPartitionLocation(table, partition.getPartition())); + Configuration configuration = hdfsEnvironment.getConfiguration(path); + InputFormat inputFormat = getInputFormat(configuration, schema, false); + + if (inputFormat instanceof SymlinkTextInputFormat) { + JobConf jobConf = new JobConf(configuration); + FileInputFormat.setInputPaths(jobConf, path); + InputSplit[] splits = inputFormat.getSplits(jobConf, 0); + + // TODO: This should use an iterator like the HiveFileIterator + for (InputSplit rawSplit : splits) { + FileSplit split = ((SymlinkTextInputFormat.SymlinkTextInputSplit) rawSplit).getTargetSplit(); + + // get the filesystem for the target path -- it may be a different hdfs instance + FileSystem targetFilesystem = hdfsEnvironment.getFileSystem(split.getPath()); + FileStatus file = targetFilesystem.getFileStatus(split.getPath()); + hiveSplitSource.addToQueue(createHiveSplits( + partitionName, + file.getPath().toString(), + targetFilesystem.getFileBlockLocations(file, split.getStart(), split.getLength()), + split.getStart(), + split.getLength(), + schema, + partitionKeys, + false, + session, + effectivePredicate)); + if (stopped) { + return; + } + } + return; + } + + FileSystem fs = hdfsEnvironment.getFileSystem(path); + if (bucket.isPresent()) { + Optional bucketFile = getBucketFile(bucket.get(), fs, path); + if (bucketFile.isPresent()) { + FileStatus file = bucketFile.get(); + BlockLocation[] blockLocations = fs.getFileBlockLocations(file, 0, file.getLen()); + boolean splittable = isSplittable(inputFormat, fs, file.getPath()); + + hiveSplitSource.addToQueue(createHiveSplits( + partitionName, + file.getPath().toString(), + blockLocations, + 0, + file.getLen(), + schema, + partitionKeys, + splittable, + session, + effectivePredicate)); + return; + } + } + + HiveFileIterator iterator = new HiveFileIterator(path, fs, directoryLister, namenodeStats, partitionName, inputFormat, schema, partitionKeys, effectivePredicate); + fileIterators.addLast(iterator); + } + + private static Optional getBucketFile(HiveBucket bucket, FileSystem fs, Path path) + { + FileStatus[] statuses = listStatus(fs, path); + + if (statuses.length != bucket.getBucketCount()) { + return Optional.empty(); + } + + Map map = new HashMap<>(); + List paths = new ArrayList<>(); + for (FileStatus status : statuses) { + if (!isFile(status)) { + return Optional.empty(); + } + String pathString = status.getPath().toString(); + map.put(pathString, status); + paths.add(pathString); + } + + // Hive sorts the paths as strings lexicographically + Collections.sort(paths); + + String pathString = paths.get(bucket.getBucketNumber()); + return Optional.of(map.get(pathString)); + } + + private static FileStatus[] listStatus(FileSystem fs, Path path) + { + try { + return fs.listStatus(path); + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } + + private List createHiveSplits( + String partitionName, + String path, + BlockLocation[] blockLocations, + long start, + long length, + Properties schema, + List partitionKeys, + boolean splittable, + ConnectorSession session, + TupleDomain effectivePredicate) + throws IOException + { + ImmutableList.Builder builder = ImmutableList.builder(); + + boolean forceLocalScheduling = HiveSessionProperties.getForceLocalScheduling(session, this.forceLocalScheduling); + + if (splittable) { + for (BlockLocation blockLocation : blockLocations) { + // get the addresses for the block + List addresses = toHostAddress(blockLocation.getHosts()); + + long maxBytes = maxSplitSize.toBytes(); + + if (remainingInitialSplits.get() > 0) { + maxBytes = maxInitialSplitSize.toBytes(); + } + + // divide the block into uniform chunks that are smaller than the max split size + int chunks = Math.max(1, (int) (blockLocation.getLength() / maxBytes)); + // when block does not divide evenly into chunks, make the chunk size slightly bigger than necessary + long targetChunkSize = (long) Math.ceil(blockLocation.getLength() * 1.0 / chunks); + + long chunkOffset = 0; + while (chunkOffset < blockLocation.getLength()) { + // adjust the actual chunk size to account for the overrun when chunks are slightly bigger than necessary (see above) + long chunkLength = Math.min(targetChunkSize, blockLocation.getLength() - chunkOffset); + + builder.add(new HiveSplit(connectorId, + table.getDbName(), + table.getTableName(), + partitionName, + path, + blockLocation.getOffset() + chunkOffset, + chunkLength, + schema, + partitionKeys, + addresses, + forceLocalScheduling, + session, + effectivePredicate)); + + chunkOffset += chunkLength; + remainingInitialSplits.decrementAndGet(); + } + checkState(chunkOffset == blockLocation.getLength(), "Error splitting blocks"); + } + } + else { + // not splittable, use the hosts from the first block if it exists + List addresses = ImmutableList.of(); + if (blockLocations.length > 0) { + addresses = toHostAddress(blockLocations[0].getHosts()); + } + + builder.add(new HiveSplit(connectorId, + table.getDbName(), + table.getTableName(), + partitionName, + path, + start, + length, + schema, + partitionKeys, + addresses, + forceLocalScheduling, + session, + effectivePredicate)); + } + return builder.build(); + } + + private static List toHostAddress(String[] hosts) + { + ImmutableList.Builder builder = ImmutableList.builder(); + for (String host : hosts) { + builder.add(HostAddress.fromString(host)); + } + return builder.build(); + } + + private static List getPartitionKeys(Table table, Partition partition) + { + if (isUnpartitioned(partition)) { + return ImmutableList.of(); + } + ImmutableList.Builder partitionKeys = ImmutableList.builder(); + List keys = table.getPartitionKeys(); + List values = partition.getValues(); + checkCondition(keys.size() == values.size(), HIVE_INVALID_METADATA, "Expected %s partition key values, but got %s", keys.size(), values.size()); + for (int i = 0; i < keys.size(); i++) { + String name = keys.get(i).getName(); + HiveType hiveType = getSupportedHiveType(keys.get(i).getType()); + String value = values.get(i); + checkCondition(value != null, HIVE_INVALID_PARTITION_VALUE, "partition key value cannot be null for field: %s", name); + partitionKeys.add(new HivePartitionKey(name, hiveType, value)); + } + return partitionKeys.build(); + } + + private static Properties getPartitionSchema(Table table, Partition partition) + { + if (isUnpartitioned(partition)) { + return MetaStoreUtils.getTableMetadata(table); + } + return MetaStoreUtils.getSchema(partition, table); + } + + private static String getPartitionLocation(Table table, Partition partition) + { + if (isUnpartitioned(partition)) { + return table.getSd().getLocation(); + } + return partition.getSd().getLocation(); + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/ColumnarBinaryHiveRecordCursor.java b/presto-hive/src/main/java/com/facebook/presto/hive/ColumnarBinaryHiveRecordCursor.java new file mode 100644 index 00000000..3ea57352 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/ColumnarBinaryHiveRecordCursor.java @@ -0,0 +1,626 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableSet; +import io.airlift.slice.ByteArrays; +import io.airlift.slice.Slice; +import io.airlift.slice.Slices; +import org.apache.hadoop.hive.serde2.columnar.BytesRefArrayWritable; +import org.apache.hadoop.hive.serde2.columnar.BytesRefWritable; +import org.apache.hadoop.hive.serde2.io.TimestampWritable; +import org.apache.hadoop.hive.serde2.lazy.ByteArrayRef; +import org.apache.hadoop.hive.serde2.lazybinary.LazyBinaryFactory; +import org.apache.hadoop.hive.serde2.lazybinary.LazyBinaryObject; +import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.StructObjectInspector; +import org.apache.hadoop.io.WritableUtils; +import org.apache.hadoop.mapred.RecordReader; +import org.joda.time.DateTimeZone; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import static com.facebook.presto.hive.HiveErrorCode.HIVE_CURSOR_ERROR; +import static com.facebook.presto.hive.HiveType.HIVE_BYTE; +import static com.facebook.presto.hive.HiveType.HIVE_DATE; +import static com.facebook.presto.hive.HiveType.HIVE_DOUBLE; +import static com.facebook.presto.hive.HiveType.HIVE_FLOAT; +import static com.facebook.presto.hive.HiveType.HIVE_INT; +import static com.facebook.presto.hive.HiveType.HIVE_LONG; +import static com.facebook.presto.hive.HiveType.HIVE_SHORT; +import static com.facebook.presto.hive.HiveType.HIVE_TIMESTAMP; +import static com.facebook.presto.hive.HiveUtil.bigintPartitionKey; +import static com.facebook.presto.hive.HiveUtil.booleanPartitionKey; +import static com.facebook.presto.hive.HiveUtil.datePartitionKey; +import static com.facebook.presto.hive.HiveUtil.doublePartitionKey; +import static com.facebook.presto.hive.HiveUtil.getTableObjectInspector; +import static com.facebook.presto.hive.HiveUtil.isStructuralType; +import static com.facebook.presto.hive.HiveUtil.timestampPartitionKey; +import static com.facebook.presto.hive.util.SerDeUtils.getBlockSlice; +import static com.facebook.presto.spi.StandardErrorCode.NOT_SUPPORTED; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.BooleanType.BOOLEAN; +import static com.facebook.presto.spi.type.DateType.DATE; +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; +import static com.facebook.presto.spi.type.TimestampType.TIMESTAMP; +import static com.facebook.presto.spi.type.VarbinaryType.VARBINARY; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.Maps.uniqueIndex; +import static java.lang.Math.max; +import static java.lang.Math.min; +import static java.lang.String.format; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector.Category; + +class ColumnarBinaryHiveRecordCursor + extends HiveRecordCursor +{ + private final RecordReader recordReader; + private final K key; + private final BytesRefArrayWritable value; + + @SuppressWarnings("FieldCanBeLocal") // include names for debugging + private final String[] names; + private final Type[] types; + private final HiveType[] hiveTypes; + + private final ObjectInspector[] fieldInspectors; // DON'T USE THESE UNLESS EXTRACTION WILL BE SLOW ANYWAY + + private final int[] hiveColumnIndexes; + + private final boolean[] isPartitionColumn; + + private final boolean[] loaded; + private final boolean[] booleans; + private final long[] longs; + private final double[] doubles; + private final Slice[] slices; + private final boolean[] nulls; + + private final long totalBytes; + private long completedBytes; + private boolean closed; + + private static final byte HIVE_EMPTY_STRING_BYTE = (byte) 0xbf; + + private static final int SIZE_OF_SHORT = 2; + private static final int SIZE_OF_INT = 4; + private static final int SIZE_OF_LONG = 8; + + private static final Set VALID_HIVE_STRING_TYPES = ImmutableSet.of(HiveType.HIVE_BINARY, HiveType.HIVE_STRING); + private static final Set VALID_HIVE_STRING_CATEGORIES = ImmutableSet.of(Category.LIST, Category.MAP, Category.STRUCT); + + public ColumnarBinaryHiveRecordCursor(RecordReader recordReader, + long totalBytes, + Properties splitSchema, + List partitionKeys, + List columns, + DateTimeZone hiveStorageTimeZone, + TypeManager typeManager) + { + checkNotNull(recordReader, "recordReader is null"); + checkArgument(totalBytes >= 0, "totalBytes is negative"); + checkNotNull(splitSchema, "splitSchema is null"); + checkNotNull(partitionKeys, "partitionKeys is null"); + checkNotNull(columns, "columns is null"); + + this.recordReader = recordReader; + this.totalBytes = totalBytes; + this.key = recordReader.createKey(); + this.value = recordReader.createValue(); + + int size = columns.size(); + + this.names = new String[size]; + this.types = new Type[size]; + this.hiveTypes = new HiveType[size]; + + this.fieldInspectors = new ObjectInspector[size]; + + this.hiveColumnIndexes = new int[size]; + + this.isPartitionColumn = new boolean[size]; + + this.loaded = new boolean[size]; + this.booleans = new boolean[size]; + this.longs = new long[size]; + this.doubles = new double[size]; + this.slices = new Slice[size]; + this.nulls = new boolean[size]; + + // initialize data columns + StructObjectInspector rowInspector = getTableObjectInspector(splitSchema); + + for (int i = 0; i < columns.size(); i++) { + HiveColumnHandle column = columns.get(i); + + names[i] = column.getName(); + types[i] = typeManager.getType(column.getTypeSignature()); + hiveTypes[i] = column.getHiveType(); + + if (!column.isPartitionKey()) { + fieldInspectors[i] = rowInspector.getStructFieldRef(column.getName()).getFieldObjectInspector(); + } + + hiveColumnIndexes[i] = column.getHiveColumnIndex(); + isPartitionColumn[i] = column.isPartitionKey(); + } + + // parse requested partition columns + Map partitionKeysByName = uniqueIndex(partitionKeys, HivePartitionKey::getName); + for (int columnIndex = 0; columnIndex < columns.size(); columnIndex++) { + HiveColumnHandle column = columns.get(columnIndex); + if (column.isPartitionKey()) { + HivePartitionKey partitionKey = partitionKeysByName.get(column.getName()); + checkArgument(partitionKey != null, "Unknown partition key %s", column.getName()); + + byte[] bytes = partitionKey.getValue().getBytes(UTF_8); + + String name = names[columnIndex]; + Type type = types[columnIndex]; + if (HiveUtil.isHiveNull(bytes)) { + nulls[columnIndex] = true; + } + else if (BOOLEAN.equals(type)) { + booleans[columnIndex] = booleanPartitionKey(partitionKey.getValue(), name); + } + else if (BIGINT.equals(type)) { + longs[columnIndex] = bigintPartitionKey(partitionKey.getValue(), name); + } + else if (DOUBLE.equals(type)) { + doubles[columnIndex] = doublePartitionKey(partitionKey.getValue(), name); + } + else if (VARCHAR.equals(type)) { + slices[columnIndex] = Slices.wrappedBuffer(bytes); + } + else if (DATE.equals(type)) { + longs[columnIndex] = datePartitionKey(partitionKey.getValue(), name); + } + else if (TIMESTAMP.equals(type)) { + longs[columnIndex] = timestampPartitionKey(partitionKey.getValue(), hiveStorageTimeZone, name); + } + else { + throw new PrestoException(NOT_SUPPORTED, format("Unsupported column type %s for partition key: %s", type.getDisplayName(), name)); + } + } + } + } + + @Override + public long getTotalBytes() + { + return totalBytes; + } + + @Override + public long getCompletedBytes() + { + if (!closed) { + updateCompletedBytes(); + } + return completedBytes; + } + + private void updateCompletedBytes() + { + try { + long newCompletedBytes = (long) (totalBytes * recordReader.getProgress()); + completedBytes = min(totalBytes, max(completedBytes, newCompletedBytes)); + } + catch (IOException ignored) { + } + } + + @Override + public Type getType(int field) + { + return types[field]; + } + + @Override + public boolean advanceNextPosition() + { + try { + if (closed || !recordReader.next(key, value)) { + close(); + return false; + } + + // reset loaded flags + // partition keys are already loaded, but everything else is not + System.arraycopy(isPartitionColumn, 0, loaded, 0, isPartitionColumn.length); + + return true; + } + catch (IOException | RuntimeException e) { + closeWithSuppression(e); + throw new PrestoException(HIVE_CURSOR_ERROR, e); + } + } + + @Override + public boolean getBoolean(int fieldId) + { + checkState(!closed, "Cursor is closed"); + + validateType(fieldId, BOOLEAN); + if (!loaded[fieldId]) { + parseBooleanColumn(fieldId); + } + return booleans[fieldId]; + } + + private void parseBooleanColumn(int column) + { + // don't include column number in message because it causes boxing which is expensive here + checkArgument(!isPartitionColumn[column], "Column is a partition key"); + + loaded[column] = true; + + if (hiveColumnIndexes[column] >= value.size()) { + // this partition may contain fewer fields than what's declared in the schema + // this happens when additional columns are added to the hive table after a partition has been created + nulls[column] = true; + } + else { + BytesRefWritable fieldData = value.unCheckedGet(hiveColumnIndexes[column]); + + byte[] bytes; + try { + bytes = fieldData.getData(); + } + catch (IOException e) { + throw Throwables.propagate(e); + } + + int start = fieldData.getStart(); + int length = fieldData.getLength(); + + parseBooleanColumn(column, bytes, start, length); + } + } + + private void parseBooleanColumn(int column, byte[] bytes, int start, int length) + { + if (length > 0) { + booleans[column] = bytes[start] != 0; + nulls[column] = false; + } + else { + nulls[column] = true; + } + } + + @Override + public long getLong(int fieldId) + { + checkState(!closed, "Cursor is closed"); + + if (!types[fieldId].equals(BIGINT) && !types[fieldId].equals(DATE) && !types[fieldId].equals(TIMESTAMP)) { + // we don't use Preconditions.checkArgument because it requires boxing fieldId, which affects inner loop performance + throw new IllegalArgumentException(format("Expected field to be %s, %s or %s , actual %s (field %s)", BIGINT, DATE, TIMESTAMP, types[fieldId], fieldId)); + } + if (!loaded[fieldId]) { + parseLongColumn(fieldId); + } + return longs[fieldId]; + } + + private void parseLongColumn(int column) + { + // don't include column number in message because it causes boxing which is expensive here + checkArgument(!isPartitionColumn[column], "Column is a partition key"); + + loaded[column] = true; + + if (hiveColumnIndexes[column] >= value.size()) { + // this partition may contain fewer fields than what's declared in the schema + // this happens when additional columns are added to the hive table after a partition has been created + nulls[column] = true; + } + else { + BytesRefWritable fieldData = value.unCheckedGet(hiveColumnIndexes[column]); + + byte[] bytes; + try { + bytes = fieldData.getData(); + } + catch (IOException e) { + throw Throwables.propagate(e); + } + + int start = fieldData.getStart(); + int length = fieldData.getLength(); + + parseLongColumn(column, bytes, start, length); + } + } + + private void parseLongColumn(int column, byte[] bytes, int start, int length) + { + if (length == 0) { + nulls[column] = true; + return; + } + nulls[column] = false; + if (hiveTypes[column].equals(HIVE_SHORT)) { + // the file format uses big endian + checkState(length == SIZE_OF_SHORT, "Short should be 2 bytes"); + longs[column] = Short.reverseBytes(ByteArrays.getShort(bytes, start)); + } + else if (hiveTypes[column].equals(HIVE_DATE)) { + checkState(length >= 1, "Date should be at least 1 byte"); + long daysSinceEpoch = readVInt(bytes, start, length); + longs[column] = daysSinceEpoch; + } + else if (hiveTypes[column].equals(HIVE_TIMESTAMP)) { + checkState(length >= 1, "Timestamp should be at least 1 byte"); + long seconds = TimestampWritable.getSeconds(bytes, start); + long nanos = (bytes[start] >> 7) != 0 ? TimestampWritable.getNanos(bytes, start + SIZE_OF_INT) : 0; + longs[column] = (seconds * 1000) + (nanos / 1_000_000); + } + else if (hiveTypes[column].equals(HIVE_BYTE)) { + checkState(length == 1, "Byte should be 1 byte"); + longs[column] = bytes[start]; + } + else if (hiveTypes[column].equals(HIVE_INT)) { + checkState(length >= 1, "Int should be at least 1 byte"); + if (length == 1) { + longs[column] = bytes[start]; + } + else { + longs[column] = readVInt(bytes, start, length); + } + } + else if (hiveTypes[column].equals(HIVE_LONG)) { + checkState(length >= 1, "Long should be at least 1 byte"); + if (length == 1) { + longs[column] = bytes[start]; + } + else { + longs[column] = readVInt(bytes, start, length); + } + } + else { + throw new RuntimeException(format("%s is not a valid LONG type", hiveTypes[column])); + } + } + + private static long readVInt(byte[] bytes, int start, int length) + { + long value = 0; + for (int i = 1; i < length; i++) { + value <<= 8; + value |= (bytes[start + i] & 0xFF); + } + return WritableUtils.isNegativeVInt(bytes[start]) ? ~value : value; + } + + @Override + public double getDouble(int fieldId) + { + checkState(!closed, "Cursor is closed"); + + validateType(fieldId, DOUBLE); + if (!loaded[fieldId]) { + parseDoubleColumn(fieldId); + } + return doubles[fieldId]; + } + + private void parseDoubleColumn(int column) + { + // don't include column number in message because it causes boxing which is expensive here + checkArgument(!isPartitionColumn[column], "Column is a partition key"); + + loaded[column] = true; + + if (hiveColumnIndexes[column] >= value.size()) { + // this partition may contain fewer fields than what's declared in the schema + // this happens when additional columns are added to the hive table after a partition has been created + nulls[column] = true; + } + else { + BytesRefWritable fieldData = value.unCheckedGet(hiveColumnIndexes[column]); + + byte[] bytes; + try { + bytes = fieldData.getData(); + } + catch (IOException e) { + throw Throwables.propagate(e); + } + + int start = fieldData.getStart(); + int length = fieldData.getLength(); + + parseDoubleColumn(column, bytes, start, length); + } + } + + private void parseDoubleColumn(int column, byte[] bytes, int start, int length) + { + if (length == 0) { + nulls[column] = true; + } + else { + nulls[column] = false; + if (hiveTypes[column].equals(HIVE_FLOAT)) { + // the file format uses big endian + checkState(length == SIZE_OF_INT, "Float should be 4 bytes"); + int intBits = ByteArrays.getInt(bytes, start); + doubles[column] = Float.intBitsToFloat(Integer.reverseBytes(intBits)); + } + else if (hiveTypes[column].equals(HIVE_DOUBLE)) { + // the file format uses big endian + checkState(length == SIZE_OF_LONG, "Double should be 8 bytes"); + long longBits = ByteArrays.getLong(bytes, start); + doubles[column] = Double.longBitsToDouble(Long.reverseBytes(longBits)); + } + else { + throw new RuntimeException(format("%s is not a valid DOUBLE type", hiveTypes[column])); + } + } + } + + @Override + public Slice getSlice(int fieldId) + { + checkState(!closed, "Cursor is closed"); + + Type type = types[fieldId]; + if (!type.equals(VARCHAR) && !type.equals(VARBINARY) && !isStructuralType(hiveTypes[fieldId])) { + // we don't use Preconditions.checkArgument because it requires boxing fieldId, which affects inner loop performance + throw new IllegalArgumentException(format("Expected field to be VARCHAR or VARBINARY, actual %s (field %s)", type, fieldId)); + } + + if (!loaded[fieldId]) { + parseStringColumn(fieldId); + } + return slices[fieldId]; + } + + private void parseStringColumn(int column) + { + // don't include column number in message because it causes boxing which is expensive here + checkArgument(!isPartitionColumn[column], "Column is a partition key"); + + loaded[column] = true; + + if (hiveColumnIndexes[column] >= value.size()) { + // this partition may contain fewer fields than what's declared in the schema + // this happens when additional columns are added to the hive table after a partition has been created + nulls[column] = true; + } + else { + BytesRefWritable fieldData = value.unCheckedGet(hiveColumnIndexes[column]); + + byte[] bytes; + try { + bytes = fieldData.getData(); + } + catch (IOException e) { + throw Throwables.propagate(e); + } + + int start = fieldData.getStart(); + int length = fieldData.getLength(); + + parseStringColumn(column, bytes, start, length); + } + } + + private void parseStringColumn(int column, byte[] bytes, int start, int length) + { + checkState(VALID_HIVE_STRING_TYPES.contains(hiveTypes[column]) || VALID_HIVE_STRING_CATEGORIES.contains(hiveTypes[column].getCategory()), "%s is not a valid STRING type", hiveTypes[column]); + if (length == 0) { + nulls[column] = true; + } + else { + nulls[column] = false; + if (isStructuralType(hiveTypes[column])) { + LazyBinaryObject lazyObject = LazyBinaryFactory.createLazyBinaryObject(fieldInspectors[column]); + ByteArrayRef byteArrayRef = new ByteArrayRef(); + byteArrayRef.setData(bytes); + lazyObject.init(byteArrayRef, start, length); + slices[column] = getBlockSlice(lazyObject.getObject(), fieldInspectors[column]); + } + else { + // TODO: zero length BINARY is not supported. See https://issues.apache.org/jira/browse/HIVE-2483 + if (hiveTypes[column].equals(HiveType.HIVE_STRING) && (length == 1) && bytes[start] == HIVE_EMPTY_STRING_BYTE) { + slices[column] = Slices.EMPTY_SLICE; + } + else { + slices[column] = Slices.wrappedBuffer(Arrays.copyOfRange(bytes, start, start + length)); + } + } + } + } + + @Override + public boolean isNull(int fieldId) + { + checkState(!closed, "Cursor is closed"); + + if (!loaded[fieldId]) { + parseColumn(fieldId); + } + return nulls[fieldId]; + } + + private void parseColumn(int column) + { + Type type = types[column]; + if (BOOLEAN.equals(type)) { + parseBooleanColumn(column); + } + else if (BIGINT.equals(type)) { + parseLongColumn(column); + } + else if (DOUBLE.equals(type)) { + parseDoubleColumn(column); + } + else if (VARCHAR.equals(type) || VARBINARY.equals(type) || isStructuralType(hiveTypes[column])) { + parseStringColumn(column); + } + else if (DATE.equals(type)) { + parseLongColumn(column); + } + else if (TIMESTAMP.equals(type)) { + parseLongColumn(column); + } + else { + throw new UnsupportedOperationException("Unsupported column type: " + type); + } + } + + private void validateType(int fieldId, Type type) + { + if (!types[fieldId].equals(type)) { + // we don't use Preconditions.checkArgument because it requires boxing fieldId, which affects inner loop performance + throw new IllegalArgumentException(format("Expected field to be %s, actual %s (field %s)", type, types[fieldId], fieldId)); + } + } + + @Override + public void close() + { + // some hive input formats are broken and bad things can happen if you close them multiple times + if (closed) { + return; + } + closed = true; + + updateCompletedBytes(); + + try { + recordReader.close(); + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/ColumnarBinaryHiveRecordCursorProvider.java b/presto-hive/src/main/java/com/facebook/presto/hive/ColumnarBinaryHiveRecordCursorProvider.java new file mode 100644 index 00000000..a74919a2 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/ColumnarBinaryHiveRecordCursorProvider.java @@ -0,0 +1,71 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.TupleDomain; +import com.facebook.presto.spi.type.TypeManager; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.hive.serde2.columnar.BytesRefArrayWritable; +import org.apache.hadoop.hive.serde2.columnar.LazyBinaryColumnarSerDe; +import org.apache.hadoop.mapred.RecordReader; +import org.joda.time.DateTimeZone; + +import java.util.List; +import java.util.Optional; +import java.util.Properties; + +import static com.facebook.presto.hive.HiveUtil.isDeserializerClass; + +public class ColumnarBinaryHiveRecordCursorProvider + implements HiveRecordCursorProvider +{ + @Override + public Optional createHiveRecordCursor( + String clientId, + Configuration configuration, + ConnectorSession session, + Path path, + long start, + long length, + Properties schema, + List columns, + List partitionKeys, + TupleDomain effectivePredicate, + DateTimeZone hiveStorageTimeZone, + TypeManager typeManager) + { + if (!isDeserializerClass(schema, LazyBinaryColumnarSerDe.class)) { + return Optional.empty(); + } + + RecordReader recordReader = HiveUtil.createRecordReader(clientId, configuration, path, start, length, schema, columns, typeManager); + + return Optional.of(new ColumnarBinaryHiveRecordCursor<>( + bytesRecordReader(recordReader), + length, + schema, + partitionKeys, + columns, + hiveStorageTimeZone, + typeManager)); + } + + @SuppressWarnings("unchecked") + private static RecordReader bytesRecordReader(RecordReader recordReader) + { + return (RecordReader) recordReader; + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/ColumnarTextHiveRecordCursor.java b/presto-hive/src/main/java/com/facebook/presto/hive/ColumnarTextHiveRecordCursor.java new file mode 100644 index 00000000..55e45d57 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/ColumnarTextHiveRecordCursor.java @@ -0,0 +1,568 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.google.common.base.Throwables; +import io.airlift.slice.Slice; +import io.airlift.slice.Slices; +import org.apache.hadoop.hive.serde2.columnar.BytesRefArrayWritable; +import org.apache.hadoop.hive.serde2.columnar.BytesRefWritable; +import org.apache.hadoop.hive.serde2.lazy.ByteArrayRef; +import org.apache.hadoop.hive.serde2.lazy.LazyFactory; +import org.apache.hadoop.hive.serde2.lazy.LazyObject; +import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.StructObjectInspector; +import org.apache.hadoop.mapred.RecordReader; +import org.joda.time.DateTimeZone; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import static com.facebook.presto.hive.HiveBooleanParser.isFalse; +import static com.facebook.presto.hive.HiveBooleanParser.isTrue; +import static com.facebook.presto.hive.HiveErrorCode.HIVE_CURSOR_ERROR; +import static com.facebook.presto.hive.HiveUtil.base64Decode; +import static com.facebook.presto.hive.HiveUtil.bigintPartitionKey; +import static com.facebook.presto.hive.HiveUtil.booleanPartitionKey; +import static com.facebook.presto.hive.HiveUtil.datePartitionKey; +import static com.facebook.presto.hive.HiveUtil.doublePartitionKey; +import static com.facebook.presto.hive.HiveUtil.getTableObjectInspector; +import static com.facebook.presto.hive.HiveUtil.isStructuralType; +import static com.facebook.presto.hive.HiveUtil.parseHiveDate; +import static com.facebook.presto.hive.HiveUtil.parseHiveTimestamp; +import static com.facebook.presto.hive.HiveUtil.timestampPartitionKey; +import static com.facebook.presto.hive.NumberParser.parseDouble; +import static com.facebook.presto.hive.NumberParser.parseLong; +import static com.facebook.presto.hive.util.SerDeUtils.getBlockSlice; +import static com.facebook.presto.spi.StandardErrorCode.NOT_SUPPORTED; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.BooleanType.BOOLEAN; +import static com.facebook.presto.spi.type.DateType.DATE; +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; +import static com.facebook.presto.spi.type.TimestampType.TIMESTAMP; +import static com.facebook.presto.spi.type.VarbinaryType.VARBINARY; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.Maps.uniqueIndex; +import static java.lang.Math.max; +import static java.lang.Math.min; +import static java.lang.String.format; +import static java.nio.charset.StandardCharsets.UTF_8; + +class ColumnarTextHiveRecordCursor + extends HiveRecordCursor +{ + private final RecordReader recordReader; + private final K key; + private final BytesRefArrayWritable value; + + @SuppressWarnings("FieldCanBeLocal") // include names for debugging + private final String[] names; + private final Type[] types; + private final HiveType[] hiveTypes; + + private final ObjectInspector[] fieldInspectors; // DON'T USE THESE UNLESS EXTRACTION WILL BE SLOW ANYWAY + + private final int[] hiveColumnIndexes; + + private final boolean[] isPartitionColumn; + + private final boolean[] loaded; + private final boolean[] booleans; + private final long[] longs; + private final double[] doubles; + private final Slice[] slices; + private final boolean[] nulls; + + private final long totalBytes; + private final DateTimeZone hiveStorageTimeZone; + + private long completedBytes; + private boolean closed; + + public ColumnarTextHiveRecordCursor( + RecordReader recordReader, + long totalBytes, + Properties splitSchema, + List partitionKeys, + List columns, + DateTimeZone hiveStorageTimeZone, + TypeManager typeManager) + { + checkNotNull(recordReader, "recordReader is null"); + checkArgument(totalBytes >= 0, "totalBytes is negative"); + checkNotNull(splitSchema, "splitSchema is null"); + checkNotNull(partitionKeys, "partitionKeys is null"); + checkNotNull(columns, "columns is null"); + checkNotNull(hiveStorageTimeZone, "hiveStorageTimeZone is null"); + + this.recordReader = recordReader; + this.totalBytes = totalBytes; + this.key = recordReader.createKey(); + this.value = recordReader.createValue(); + this.hiveStorageTimeZone = hiveStorageTimeZone; + + int size = columns.size(); + + this.names = new String[size]; + this.types = new Type[size]; + this.hiveTypes = new HiveType[size]; + + this.fieldInspectors = new ObjectInspector[size]; + + this.hiveColumnIndexes = new int[size]; + + this.isPartitionColumn = new boolean[size]; + + this.loaded = new boolean[size]; + this.booleans = new boolean[size]; + this.longs = new long[size]; + this.doubles = new double[size]; + this.slices = new Slice[size]; + this.nulls = new boolean[size]; + + // initialize data columns + StructObjectInspector rowInspector = getTableObjectInspector(splitSchema); + + for (int i = 0; i < columns.size(); i++) { + HiveColumnHandle column = columns.get(i); + + names[i] = column.getName(); + types[i] = typeManager.getType(column.getTypeSignature()); + hiveTypes[i] = column.getHiveType(); + + if (!column.isPartitionKey()) { + fieldInspectors[i] = rowInspector.getStructFieldRef(column.getName()).getFieldObjectInspector(); + } + + hiveColumnIndexes[i] = column.getHiveColumnIndex(); + isPartitionColumn[i] = column.isPartitionKey(); + } + + // parse requested partition columns + Map partitionKeysByName = uniqueIndex(partitionKeys, HivePartitionKey::getName); + for (int columnIndex = 0; columnIndex < columns.size(); columnIndex++) { + HiveColumnHandle column = columns.get(columnIndex); + if (column.isPartitionKey()) { + HivePartitionKey partitionKey = partitionKeysByName.get(column.getName()); + checkArgument(partitionKey != null, "Unknown partition key %s", column.getName()); + + byte[] bytes = partitionKey.getValue().getBytes(UTF_8); + + String name = names[columnIndex]; + Type type = types[columnIndex]; + if (HiveUtil.isHiveNull(bytes)) { + nulls[columnIndex] = true; + } + else if (BOOLEAN.equals(type)) { + booleans[columnIndex] = booleanPartitionKey(partitionKey.getValue(), name); + } + else if (BIGINT.equals(type)) { + longs[columnIndex] = bigintPartitionKey(partitionKey.getValue(), name); + } + else if (DOUBLE.equals(type)) { + doubles[columnIndex] = doublePartitionKey(partitionKey.getValue(), name); + } + else if (VARCHAR.equals(type)) { + slices[columnIndex] = Slices.wrappedBuffer(bytes); + } + else if (DATE.equals(type)) { + longs[columnIndex] = datePartitionKey(partitionKey.getValue(), name); + } + else if (TIMESTAMP.equals(type)) { + longs[columnIndex] = timestampPartitionKey(partitionKey.getValue(), hiveStorageTimeZone, name); + } + else { + throw new PrestoException(NOT_SUPPORTED, format("Unsupported column type %s for partition key: %s", type.getDisplayName(), name)); + } + } + } + } + + @Override + public long getTotalBytes() + { + return totalBytes; + } + + @Override + public long getCompletedBytes() + { + if (!closed) { + updateCompletedBytes(); + } + return completedBytes; + } + + private void updateCompletedBytes() + { + try { + long newCompletedBytes = (long) (totalBytes * recordReader.getProgress()); + completedBytes = min(totalBytes, max(completedBytes, newCompletedBytes)); + } + catch (IOException ignored) { + } + } + + @Override + public Type getType(int field) + { + return types[field]; + } + + @Override + public boolean advanceNextPosition() + { + try { + if (closed || !recordReader.next(key, value)) { + close(); + return false; + } + + // reset loaded flags + // partition keys are already loaded, but everything else is not + System.arraycopy(isPartitionColumn, 0, loaded, 0, isPartitionColumn.length); + + return true; + } + catch (IOException | RuntimeException e) { + closeWithSuppression(e); + throw new PrestoException(HIVE_CURSOR_ERROR, e); + } + } + + @Override + public boolean getBoolean(int fieldId) + { + checkState(!closed, "Cursor is closed"); + + validateType(fieldId, boolean.class); + if (!loaded[fieldId]) { + parseBooleanColumn(fieldId); + } + return booleans[fieldId]; + } + + private void parseBooleanColumn(int column) + { + // don't include column number in message because it causes boxing which is expensive here + checkArgument(!isPartitionColumn[column], "Column is a partition key"); + + loaded[column] = true; + + if (hiveColumnIndexes[column] >= value.size()) { + // this partition may contain fewer fields than what's declared in the schema + // this happens when additional columns are added to the hive table after a partition has been created + nulls[column] = true; + } + else { + BytesRefWritable fieldData = value.unCheckedGet(hiveColumnIndexes[column]); + + byte[] bytes; + try { + bytes = fieldData.getData(); + } + catch (IOException e) { + throw Throwables.propagate(e); + } + + int start = fieldData.getStart(); + int length = fieldData.getLength(); + + parseBooleanColumn(column, bytes, start, length); + } + } + + private void parseBooleanColumn(int column, byte[] bytes, int start, int length) + { + boolean wasNull; + if (isTrue(bytes, start, length)) { + booleans[column] = true; + wasNull = false; + } + else if (isFalse(bytes, start, length)) { + booleans[column] = false; + wasNull = false; + } + else { + wasNull = true; + } + nulls[column] = wasNull; + } + + @Override + public long getLong(int fieldId) + { + checkState(!closed, "Cursor is closed"); + + if (!types[fieldId].equals(BIGINT) && !types[fieldId].equals(DATE) && !types[fieldId].equals(TIMESTAMP)) { + // we don't use Preconditions.checkArgument because it requires boxing fieldId, which affects inner loop performance + throw new IllegalArgumentException(String.format("Expected field to be %s, %s or %s , actual %s (field %s)", BIGINT, DATE, TIMESTAMP, types[fieldId], fieldId)); + } + + if (!loaded[fieldId]) { + parseLongColumn(fieldId); + } + return longs[fieldId]; + } + + private void parseLongColumn(int column) + { + // don't include column number in message because it causes boxing which is expensive here + checkArgument(!isPartitionColumn[column], "Column is a partition key"); + + loaded[column] = true; + + if (hiveColumnIndexes[column] >= value.size()) { + // this partition may contain fewer fields than what's declared in the schema + // this happens when additional columns are added to the hive table after a partition has been created + nulls[column] = true; + } + else { + BytesRefWritable fieldData = value.unCheckedGet(hiveColumnIndexes[column]); + + byte[] bytes; + try { + bytes = fieldData.getData(); + } + catch (IOException e) { + throw Throwables.propagate(e); + } + + int start = fieldData.getStart(); + int length = fieldData.getLength(); + + parseLongColumn(column, bytes, start, length); + } + } + + private void parseLongColumn(int column, byte[] bytes, int start, int length) + { + boolean wasNull; + if (length == 0 || (length == "\\N".length() && bytes[start] == '\\' && bytes[start + 1] == 'N')) { + wasNull = true; + } + else if (hiveTypes[column].equals(HiveType.HIVE_DATE)) { + String value = new String(bytes, start, length); + longs[column] = parseHiveDate(value); + wasNull = false; + } + else if (hiveTypes[column].equals(HiveType.HIVE_TIMESTAMP)) { + String value = new String(bytes, start, length); + longs[column] = parseHiveTimestamp(value, hiveStorageTimeZone); + wasNull = false; + } + else { + longs[column] = parseLong(bytes, start, length); + wasNull = false; + } + nulls[column] = wasNull; + } + + @Override + public double getDouble(int fieldId) + { + checkState(!closed, "Cursor is closed"); + + validateType(fieldId, double.class); + if (!loaded[fieldId]) { + parseDoubleColumn(fieldId); + } + return doubles[fieldId]; + } + + private void parseDoubleColumn(int column) + { + // don't include column number in message because it causes boxing which is expensive here + checkArgument(!isPartitionColumn[column], "Column is a partition key"); + + loaded[column] = true; + + if (hiveColumnIndexes[column] >= value.size()) { + // this partition may contain fewer fields than what's declared in the schema + // this happens when additional columns are added to the hive table after a partition has been created + nulls[column] = true; + } + else { + BytesRefWritable fieldData = value.unCheckedGet(hiveColumnIndexes[column]); + + byte[] bytes; + try { + bytes = fieldData.getData(); + } + catch (IOException e) { + throw Throwables.propagate(e); + } + + int start = fieldData.getStart(); + int length = fieldData.getLength(); + + parseDoubleColumn(column, bytes, start, length); + } + } + + private void parseDoubleColumn(int column, byte[] bytes, int start, int length) + { + boolean wasNull; + if (length == 0 || (length == "\\N".length() && bytes[start] == '\\' && bytes[start + 1] == 'N')) { + wasNull = true; + } + else { + doubles[column] = parseDouble(bytes, start, length); + wasNull = false; + } + nulls[column] = wasNull; + } + + @Override + public Slice getSlice(int fieldId) + { + checkState(!closed, "Cursor is closed"); + + validateType(fieldId, Slice.class); + if (!loaded[fieldId]) { + parseStringColumn(fieldId); + } + return slices[fieldId]; + } + + private void parseStringColumn(int column) + { + // don't include column number in message because it causes boxing which is expensive here + checkArgument(!isPartitionColumn[column], "Column is a partition key"); + + loaded[column] = true; + + if (hiveColumnIndexes[column] >= value.size()) { + // this partition may contain fewer fields than what's declared in the schema + // this happens when additional columns are added to the hive table after a partition has been created + nulls[column] = true; + } + else { + BytesRefWritable fieldData = value.unCheckedGet(hiveColumnIndexes[column]); + + byte[] bytes; + try { + bytes = fieldData.getData(); + } + catch (IOException e) { + throw Throwables.propagate(e); + } + + int start = fieldData.getStart(); + int length = fieldData.getLength(); + + parseStringColumn(column, bytes, start, length); + } + } + + private void parseStringColumn(int column, byte[] bytes, int start, int length) + { + boolean wasNull; + if (length == "\\N".length() && bytes[start] == '\\' && bytes[start + 1] == 'N') { + wasNull = true; + } + else if (isStructuralType(hiveTypes[column])) { + LazyObject lazyObject = LazyFactory.createLazyObject(fieldInspectors[column]); + ByteArrayRef byteArrayRef = new ByteArrayRef(); + byteArrayRef.setData(bytes); + lazyObject.init(byteArrayRef, start, length); + slices[column] = getBlockSlice(lazyObject.getObject(), fieldInspectors[column]); + wasNull = false; + } + else { + slices[column] = Slices.wrappedBuffer(Arrays.copyOfRange(bytes, start, start + length)); + + // this is unbelievably stupid but Hive base64 encodes binary data in a binary file format + if (hiveTypes[column].equals(HiveType.HIVE_BINARY)) { + // and yes we end up with an extra copy here because the Base64 only handles whole arrays + slices[column] = base64Decode(slices[column].getBytes()); + } + wasNull = false; + } + nulls[column] = wasNull; + } + + @Override + public boolean isNull(int fieldId) + { + checkState(!closed, "Cursor is closed"); + + if (!loaded[fieldId]) { + parseColumn(fieldId); + } + return nulls[fieldId]; + } + + private void parseColumn(int column) + { + Type type = types[column]; + if (type.equals(BOOLEAN)) { + parseBooleanColumn(column); + } + else if (type.equals(BIGINT)) { + parseLongColumn(column); + } + else if (type.equals(DOUBLE)) { + parseDoubleColumn(column); + } + else if (VARCHAR.equals(type) || VARBINARY.equals(type) || isStructuralType(hiveTypes[column])) { + parseStringColumn(column); + } + else if (type.equals(DATE)) { + parseLongColumn(column); + } + else if (type.equals(TIMESTAMP)) { + parseLongColumn(column); + } + else { + throw new UnsupportedOperationException("Unsupported column type: " + type); + } + } + + private void validateType(int fieldId, Class type) + { + if (!types[fieldId].getJavaType().equals(type)) { + // we don't use Preconditions.checkArgument because it requires boxing fieldId, which affects inner loop performance + throw new IllegalArgumentException(String.format("Expected field to be %s, actual %s (field %s)", type, types[fieldId], fieldId)); + } + } + + @Override + public void close() + { + // some hive input formats are broken and bad things can happen if you close them multiple times + if (closed) { + return; + } + closed = true; + + updateCompletedBytes(); + + try { + recordReader.close(); + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/ColumnarTextHiveRecordCursorProvider.java b/presto-hive/src/main/java/com/facebook/presto/hive/ColumnarTextHiveRecordCursorProvider.java new file mode 100644 index 00000000..807981fc --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/ColumnarTextHiveRecordCursorProvider.java @@ -0,0 +1,71 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.TupleDomain; +import com.facebook.presto.spi.type.TypeManager; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.hive.serde2.columnar.BytesRefArrayWritable; +import org.apache.hadoop.hive.serde2.columnar.ColumnarSerDe; +import org.apache.hadoop.mapred.RecordReader; +import org.joda.time.DateTimeZone; + +import java.util.List; +import java.util.Optional; +import java.util.Properties; + +import static com.facebook.presto.hive.HiveUtil.isDeserializerClass; + +public class ColumnarTextHiveRecordCursorProvider + implements HiveRecordCursorProvider +{ + @Override + public Optional createHiveRecordCursor( + String clientId, + Configuration configuration, + ConnectorSession session, + Path path, + long start, + long length, + Properties schema, + List columns, + List partitionKeys, + TupleDomain effectivePredicate, + DateTimeZone hiveStorageTimeZone, + TypeManager typeManager) + { + if (!isDeserializerClass(schema, ColumnarSerDe.class)) { + return Optional.empty(); + } + + RecordReader recordReader = HiveUtil.createRecordReader(clientId, configuration, path, start, length, schema, columns, typeManager); + + return Optional.of(new ColumnarTextHiveRecordCursor<>( + columnarTextRecordReader(recordReader), + length, + schema, + partitionKeys, + columns, + hiveStorageTimeZone, + typeManager)); + } + + @SuppressWarnings("unchecked") + private static RecordReader columnarTextRecordReader(RecordReader recordReader) + { + return (RecordReader) recordReader; + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/ConcurrentLazyQueue.java b/presto-hive/src/main/java/com/facebook/presto/hive/ConcurrentLazyQueue.java new file mode 100644 index 00000000..05dd95d6 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/ConcurrentLazyQueue.java @@ -0,0 +1,42 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import javax.annotation.concurrent.GuardedBy; + +import java.util.Iterator; + +public class ConcurrentLazyQueue +{ + @GuardedBy("this") + private final Iterator iterator; + + public ConcurrentLazyQueue(Iterable iterable) + { + this.iterator = iterable.iterator(); + } + + public synchronized boolean isEmpty() + { + return !iterator.hasNext(); + } + + public synchronized E poll() + { + if (!iterator.hasNext()) { + return null; + } + return iterator.next(); + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/DirectoryLister.java b/presto-hive/src/main/java/com/facebook/presto/hive/DirectoryLister.java new file mode 100644 index 00000000..1b2af5c0 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/DirectoryLister.java @@ -0,0 +1,27 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.LocatedFileStatus; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.fs.RemoteIterator; + +import java.io.IOException; + +public interface DirectoryLister +{ + RemoteIterator list(FileSystem fs, Path path) + throws IOException; +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/DiscoveryLocatedHiveCluster.java b/presto-hive/src/main/java/com/facebook/presto/hive/DiscoveryLocatedHiveCluster.java new file mode 100644 index 00000000..52bcef24 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/DiscoveryLocatedHiveCluster.java @@ -0,0 +1,77 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.google.common.net.HostAndPort; +import io.airlift.discovery.client.DiscoveryException; +import io.airlift.discovery.client.ServiceDescriptor; +import io.airlift.discovery.client.ServiceSelector; +import io.airlift.discovery.client.ServiceState; +import io.airlift.discovery.client.ServiceType; +import org.apache.thrift.transport.TTransportException; + +import javax.inject.Inject; + +import java.util.Collections; +import java.util.List; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.stream.Collectors.toList; + +public class DiscoveryLocatedHiveCluster + implements HiveCluster +{ + private final ServiceSelector selector; + private final HiveMetastoreClientFactory clientFactory; + + @Inject + public DiscoveryLocatedHiveCluster(@ServiceType("hive-metastore") ServiceSelector selector, HiveMetastoreClientFactory clientFactory) + { + this.selector = checkNotNull(selector, "selector is null"); + this.clientFactory = checkNotNull(clientFactory, "clientFactory is null"); + } + + @Override + public HiveMetastoreClient createMetastoreClient() + { + List descriptors = selector.selectAllServices().stream() + .filter(input -> input.getState() != ServiceState.STOPPED) + .collect(toList()); + if (descriptors.isEmpty()) { + throw new DiscoveryException("No metastore servers available for pool: " + selector.getPool()); + } + + Collections.shuffle(descriptors); + TTransportException lastException = null; + for (ServiceDescriptor descriptor : descriptors) { + String thrift = descriptor.getProperties().get("thrift"); + if (thrift != null) { + try { + HostAndPort metastore = HostAndPort.fromString(thrift); + checkArgument(metastore.hasPort()); + return clientFactory.create(metastore.getHostText(), metastore.getPort()); + } + catch (IllegalArgumentException ignored) { + // Ignore entries with parse issues + } + catch (TTransportException e) { + lastException = e; + } + } + } + + throw new DiscoveryException("Unable to connect to any metastore servers in pool: " + selector.getPool(), lastException); + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/ForHiveClient.java b/presto-hive/src/main/java/com/facebook/presto/hive/ForHiveClient.java new file mode 100644 index 00000000..6297c8ef --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/ForHiveClient.java @@ -0,0 +1,31 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import javax.inject.Qualifier; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention(RUNTIME) +@Target({FIELD, PARAMETER, METHOD}) +@Qualifier +public @interface ForHiveClient +{ +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/ForHiveMetastore.java b/presto-hive/src/main/java/com/facebook/presto/hive/ForHiveMetastore.java new file mode 100644 index 00000000..b8a1b33d --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/ForHiveMetastore.java @@ -0,0 +1,31 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import javax.inject.Qualifier; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention(RUNTIME) +@Target({FIELD, PARAMETER, METHOD}) +@Qualifier +public @interface ForHiveMetastore +{ +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/GenericHiveRecordCursor.java b/presto-hive/src/main/java/com/facebook/presto/hive/GenericHiveRecordCursor.java new file mode 100644 index 00000000..699b967e --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/GenericHiveRecordCursor.java @@ -0,0 +1,487 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.google.common.base.Throwables; +import io.airlift.slice.Slice; +import io.airlift.slice.Slices; +import org.apache.hadoop.hive.serde2.Deserializer; +import org.apache.hadoop.hive.serde2.SerDeException; +import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.PrimitiveObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.StructField; +import org.apache.hadoop.hive.serde2.objectinspector.StructObjectInspector; +import org.apache.hadoop.io.Writable; +import org.apache.hadoop.mapred.RecordReader; +import org.joda.time.DateTimeZone; + +import java.io.IOException; +import java.sql.Date; +import java.sql.Timestamp; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.TimeUnit; + +import static com.facebook.presto.hive.HiveErrorCode.HIVE_CURSOR_ERROR; +import static com.facebook.presto.hive.HiveUtil.bigintPartitionKey; +import static com.facebook.presto.hive.HiveUtil.booleanPartitionKey; +import static com.facebook.presto.hive.HiveUtil.datePartitionKey; +import static com.facebook.presto.hive.HiveUtil.doublePartitionKey; +import static com.facebook.presto.hive.HiveUtil.getDeserializer; +import static com.facebook.presto.hive.HiveUtil.getTableObjectInspector; +import static com.facebook.presto.hive.HiveUtil.isStructuralType; +import static com.facebook.presto.hive.HiveUtil.timestampPartitionKey; +import static com.facebook.presto.hive.util.SerDeUtils.getBlockSlice; +import static com.facebook.presto.spi.StandardErrorCode.NOT_SUPPORTED; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.BooleanType.BOOLEAN; +import static com.facebook.presto.spi.type.DateType.DATE; +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; +import static com.facebook.presto.spi.type.TimestampType.TIMESTAMP; +import static com.facebook.presto.spi.type.VarbinaryType.VARBINARY; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.Maps.uniqueIndex; +import static java.lang.Math.max; +import static java.lang.Math.min; +import static java.lang.String.format; +import static java.nio.charset.StandardCharsets.UTF_8; + +class GenericHiveRecordCursor + extends HiveRecordCursor +{ + private final RecordReader recordReader; + private final K key; + private final V value; + + @SuppressWarnings("deprecation") + private final Deserializer deserializer; + + private final Type[] types; + private final HiveType[] hiveTypes; + + private final StructObjectInspector rowInspector; + private final ObjectInspector[] fieldInspectors; + private final StructField[] structFields; + + private final boolean[] isPartitionColumn; + + private final boolean[] loaded; + private final boolean[] booleans; + private final long[] longs; + private final double[] doubles; + private final Slice[] slices; + private final boolean[] nulls; + + private final long totalBytes; + private final DateTimeZone hiveStorageTimeZone; + + private long completedBytes; + private Object rowData; + private boolean closed; + + public GenericHiveRecordCursor( + RecordReader recordReader, + long totalBytes, + Properties splitSchema, + List partitionKeys, + List columns, + DateTimeZone hiveStorageTimeZone, + TypeManager typeManager) + { + checkNotNull(recordReader, "recordReader is null"); + checkArgument(totalBytes >= 0, "totalBytes is negative"); + checkNotNull(splitSchema, "splitSchema is null"); + checkNotNull(partitionKeys, "partitionKeys is null"); + checkNotNull(columns, "columns is null"); + checkNotNull(hiveStorageTimeZone, "hiveStorageTimeZone is null"); + + this.recordReader = recordReader; + this.totalBytes = totalBytes; + this.key = recordReader.createKey(); + this.value = recordReader.createValue(); + this.hiveStorageTimeZone = hiveStorageTimeZone; + + this.deserializer = getDeserializer(splitSchema); + this.rowInspector = getTableObjectInspector(deserializer); + + int size = columns.size(); + + String[] names = new String[size]; + this.types = new Type[size]; + this.hiveTypes = new HiveType[size]; + + this.structFields = new StructField[size]; + this.fieldInspectors = new ObjectInspector[size]; + + this.isPartitionColumn = new boolean[size]; + + this.loaded = new boolean[size]; + this.booleans = new boolean[size]; + this.longs = new long[size]; + this.doubles = new double[size]; + this.slices = new Slice[size]; + this.nulls = new boolean[size]; + + // initialize data columns + for (int i = 0; i < columns.size(); i++) { + HiveColumnHandle column = columns.get(i); + + names[i] = column.getName(); + types[i] = typeManager.getType(column.getTypeSignature()); + hiveTypes[i] = column.getHiveType(); + + if (!column.isPartitionKey()) { + StructField field = rowInspector.getStructFieldRef(column.getName()); + structFields[i] = field; + fieldInspectors[i] = field.getFieldObjectInspector(); + } + + isPartitionColumn[i] = column.isPartitionKey(); + } + + // parse requested partition columns + Map partitionKeysByName = uniqueIndex(partitionKeys, HivePartitionKey::getName); + for (int columnIndex = 0; columnIndex < columns.size(); columnIndex++) { + HiveColumnHandle column = columns.get(columnIndex); + if (column.isPartitionKey()) { + HivePartitionKey partitionKey = partitionKeysByName.get(column.getName()); + checkArgument(partitionKey != null, "Unknown partition key %s", column.getName()); + + byte[] bytes = partitionKey.getValue().getBytes(UTF_8); + + String name = names[columnIndex]; + Type type = types[columnIndex]; + if (HiveUtil.isHiveNull(bytes)) { + nulls[columnIndex] = true; + } + else if (BOOLEAN.equals(type)) { + booleans[columnIndex] = booleanPartitionKey(partitionKey.getValue(), name); + } + else if (BIGINT.equals(type)) { + longs[columnIndex] = bigintPartitionKey(partitionKey.getValue(), name); + } + else if (DOUBLE.equals(type)) { + doubles[columnIndex] = doublePartitionKey(partitionKey.getValue(), name); + } + else if (VARCHAR.equals(type)) { + slices[columnIndex] = Slices.wrappedBuffer(Arrays.copyOf(bytes, bytes.length)); + } + else if (DATE.equals(type)) { + longs[columnIndex] = datePartitionKey(partitionKey.getValue(), name); + } + else if (TIMESTAMP.equals(type)) { + longs[columnIndex] = timestampPartitionKey(partitionKey.getValue(), hiveStorageTimeZone, name); + } + else { + throw new PrestoException(NOT_SUPPORTED, format("Unsupported column type %s for partition key: %s", type.getDisplayName(), name)); + } + } + } + } + + @Override + public long getTotalBytes() + { + return totalBytes; + } + + @Override + public long getCompletedBytes() + { + if (!closed) { + updateCompletedBytes(); + } + return completedBytes; + } + + private void updateCompletedBytes() + { + try { + long newCompletedBytes = (long) (totalBytes * recordReader.getProgress()); + completedBytes = min(totalBytes, max(completedBytes, newCompletedBytes)); + } + catch (IOException ignored) { + } + } + + @Override + public Type getType(int field) + { + return types[field]; + } + + @Override + public boolean advanceNextPosition() + { + try { + if (closed || !recordReader.next(key, value)) { + close(); + return false; + } + + // reset loaded flags + // partition keys are already loaded, but everything else is not + System.arraycopy(isPartitionColumn, 0, loaded, 0, isPartitionColumn.length); + + // decode value + rowData = deserializer.deserialize(value); + + return true; + } + catch (IOException | SerDeException | RuntimeException e) { + closeWithSuppression(e); + throw new PrestoException(HIVE_CURSOR_ERROR, e); + } + } + + @Override + public boolean getBoolean(int fieldId) + { + checkState(!closed, "Cursor is closed"); + + validateType(fieldId, boolean.class); + if (!loaded[fieldId]) { + parseBooleanColumn(fieldId); + } + return booleans[fieldId]; + } + + private void parseBooleanColumn(int column) + { + // don't include column number in message because it causes boxing which is expensive here + checkArgument(!isPartitionColumn[column], "Column is a partition key"); + + loaded[column] = true; + + Object fieldData = rowInspector.getStructFieldData(rowData, structFields[column]); + + if (fieldData == null) { + nulls[column] = true; + } + else { + Object fieldValue = ((PrimitiveObjectInspector) fieldInspectors[column]).getPrimitiveJavaObject(fieldData); + checkState(fieldValue != null, "fieldValue should not be null"); + booleans[column] = (Boolean) fieldValue; + nulls[column] = false; + } + } + + @Override + public long getLong(int fieldId) + { + checkState(!closed, "Cursor is closed"); + + validateType(fieldId, long.class); + if (!loaded[fieldId]) { + parseLongColumn(fieldId); + } + return longs[fieldId]; + } + + private void parseLongColumn(int column) + { + // don't include column number in message because it causes boxing which is expensive here + checkArgument(!isPartitionColumn[column], "Column is a partition key"); + + loaded[column] = true; + + Object fieldData = rowInspector.getStructFieldData(rowData, structFields[column]); + + if (fieldData == null) { + nulls[column] = true; + } + else { + Object fieldValue = ((PrimitiveObjectInspector) fieldInspectors[column]).getPrimitiveJavaObject(fieldData); + checkState(fieldValue != null, "fieldValue should not be null"); + longs[column] = getLongOrTimestamp(fieldValue, hiveStorageTimeZone); + nulls[column] = false; + } + } + + private static long getLongOrTimestamp(Object value, DateTimeZone hiveTimeZone) + { + if (value instanceof Date) { + long storageTime = ((Date) value).getTime(); + // convert date from VM current time zone to UTC + long utcMillis = storageTime + DateTimeZone.getDefault().getOffset(storageTime); + return TimeUnit.MILLISECONDS.toDays(utcMillis); + } + if (value instanceof Timestamp) { + // The Hive SerDe parses timestamps using the default time zone of + // this JVM, but the data might have been written using a different + // time zone. We need to convert it to the configured time zone. + + // the timestamp that Hive parsed using the JVM time zone + long parsedJvmMillis = ((Timestamp) value).getTime(); + + // remove the JVM time zone correction from the timestamp + DateTimeZone jvmTimeZone = DateTimeZone.getDefault(); + long hiveMillis = jvmTimeZone.convertUTCToLocal(parsedJvmMillis); + + // convert to UTC using the real time zone for the underlying data + long utcMillis = hiveTimeZone.convertLocalToUTC(hiveMillis, false); + + return utcMillis; + } + return ((Number) value).longValue(); + } + + @Override + public double getDouble(int fieldId) + { + checkState(!closed, "Cursor is closed"); + + validateType(fieldId, double.class); + if (!loaded[fieldId]) { + parseDoubleColumn(fieldId); + } + return doubles[fieldId]; + } + + private void parseDoubleColumn(int column) + { + // don't include column number in message because it causes boxing which is expensive here + checkArgument(!isPartitionColumn[column], "Column is a partition key"); + + loaded[column] = true; + + Object fieldData = rowInspector.getStructFieldData(rowData, structFields[column]); + + if (fieldData == null) { + nulls[column] = true; + } + else { + Object fieldValue = ((PrimitiveObjectInspector) fieldInspectors[column]).getPrimitiveJavaObject(fieldData); + checkState(fieldValue != null, "fieldValue should not be null"); + doubles[column] = ((Number) fieldValue).doubleValue(); + nulls[column] = false; + } + } + + @Override + public Slice getSlice(int fieldId) + { + checkState(!closed, "Cursor is closed"); + + validateType(fieldId, Slice.class); + if (!loaded[fieldId]) { + parseStringColumn(fieldId); + } + return slices[fieldId]; + } + + private void parseStringColumn(int column) + { + // don't include column number in message because it causes boxing which is expensive here + checkArgument(!isPartitionColumn[column], "Column is a partition key"); + + loaded[column] = true; + + Object fieldData = rowInspector.getStructFieldData(rowData, structFields[column]); + + if (fieldData == null) { + nulls[column] = true; + } + else if (isStructuralType(hiveTypes[column])) { + slices[column] = getBlockSlice(fieldData, fieldInspectors[column]); + nulls[column] = false; + } + else { + Object fieldValue = ((PrimitiveObjectInspector) fieldInspectors[column]).getPrimitiveJavaObject(fieldData); + checkState(fieldValue != null, "fieldValue should not be null"); + if (fieldValue instanceof String) { + slices[column] = Slices.utf8Slice((String) fieldValue); + } + else if (fieldValue instanceof byte[]) { + slices[column] = Slices.wrappedBuffer((byte[]) fieldValue); + } + else { + throw new IllegalStateException("unsupported string field type: " + fieldValue.getClass().getName()); + } + nulls[column] = false; + } + } + + @Override + public boolean isNull(int fieldId) + { + checkState(!closed, "Cursor is closed"); + + if (!loaded[fieldId]) { + parseColumn(fieldId); + } + return nulls[fieldId]; + } + + private void parseColumn(int column) + { + Type type = types[column]; + if (BOOLEAN.equals(type)) { + parseBooleanColumn(column); + } + else if (BIGINT.equals(type)) { + parseLongColumn(column); + } + else if (DOUBLE.equals(type)) { + parseDoubleColumn(column); + } + else if (VARCHAR.equals(type) || VARBINARY.equals(type) || isStructuralType(hiveTypes[column])) { + parseStringColumn(column); + } + else if (DATE.equals(type)) { + parseLongColumn(column); + } + else if (TIMESTAMP.equals(type)) { + parseLongColumn(column); + } + else { + throw new UnsupportedOperationException("Unsupported column type: " + type); + } + } + + private void validateType(int fieldId, Class type) + { + if (!types[fieldId].getJavaType().equals(type)) { + // we don't use Preconditions.checkArgument because it requires boxing fieldId, which affects inner loop performance + throw new IllegalArgumentException(String.format("Expected field to be %s, actual %s (field %s)", type, types[fieldId], fieldId)); + } + } + + @Override + public void close() + { + // some hive input formats are broken and bad things can happen if you close them multiple times + if (closed) { + return; + } + closed = true; + + updateCompletedBytes(); + + try { + recordReader.close(); + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/GenericHiveRecordCursorProvider.java b/presto-hive/src/main/java/com/facebook/presto/hive/GenericHiveRecordCursorProvider.java new file mode 100644 index 00000000..629ec330 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/GenericHiveRecordCursorProvider.java @@ -0,0 +1,64 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.TupleDomain; +import com.facebook.presto.spi.type.TypeManager; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.io.Writable; +import org.apache.hadoop.mapred.RecordReader; +import org.joda.time.DateTimeZone; + +import java.util.List; +import java.util.Optional; +import java.util.Properties; + +public class GenericHiveRecordCursorProvider + implements HiveRecordCursorProvider +{ + @Override + public Optional createHiveRecordCursor( + String clientId, + Configuration configuration, + ConnectorSession session, + Path path, + long start, + long length, + Properties schema, + List columns, + List partitionKeys, + TupleDomain effectivePredicate, + DateTimeZone hiveStorageTimeZone, + TypeManager typeManager) + { + RecordReader recordReader = HiveUtil.createRecordReader(clientId, configuration, path, start, length, schema, columns, typeManager); + + return Optional.of(new GenericHiveRecordCursor<>( + genericRecordReader(recordReader), + length, + schema, + partitionKeys, + columns, + hiveStorageTimeZone, + typeManager)); + } + + @SuppressWarnings("unchecked") + private static RecordReader genericRecordReader(RecordReader recordReader) + { + return (RecordReader) recordReader; + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/HadoopDirectoryLister.java b/presto-hive/src/main/java/com/facebook/presto/hive/HadoopDirectoryLister.java new file mode 100644 index 00000000..4d55cb22 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/HadoopDirectoryLister.java @@ -0,0 +1,34 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.LocatedFileStatus; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.fs.RemoteIterator; + +import java.io.IOException; + +import static com.facebook.presto.hadoop.HadoopFileSystem.listLocatedStatus; + +public class HadoopDirectoryLister + implements DirectoryLister +{ + @Override + public RemoteIterator list(FileSystem fs, Path path) + throws IOException + { + return listLocatedStatus(fs, path); + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/HdfsConfiguration.java b/presto-hive/src/main/java/com/facebook/presto/hive/HdfsConfiguration.java new file mode 100644 index 00000000..43b6e50b --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/HdfsConfiguration.java @@ -0,0 +1,23 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import org.apache.hadoop.conf.Configuration; + +import java.net.URI; + +public interface HdfsConfiguration +{ + Configuration getConfiguration(URI uri); +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/HdfsConfigurationUpdater.java b/presto-hive/src/main/java/com/facebook/presto/hive/HdfsConfigurationUpdater.java new file mode 100644 index 00000000..0b94d9ab --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/HdfsConfigurationUpdater.java @@ -0,0 +1,164 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.google.common.collect.ImmutableList; +import com.google.common.net.HostAndPort; +import com.google.common.primitives.Ints; +import io.airlift.units.DataSize; +import io.airlift.units.Duration; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.net.DNSToSwitchMapping; +import org.apache.hadoop.net.SocksSocketFactory; + +import javax.inject.Inject; +import javax.net.SocketFactory; + +import java.io.File; +import java.util.List; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.lang.String.format; + +public class HdfsConfigurationUpdater +{ + private final HostAndPort socksProxy; + private final Duration dfsTimeout; + private final Duration dfsConnectTimeout; + private final int dfsConnectMaxRetries; + private final String domainSocketPath; + private final String s3AwsAccessKey; + private final String s3AwsSecretKey; + private final boolean s3UseInstanceCredentials; + private final boolean s3SslEnabled; + private final int s3MaxClientRetries; + private final int s3MaxErrorRetries; + private final Duration s3MaxBackoffTime; + private final Duration s3MaxRetryTime; + private final Duration s3ConnectTimeout; + private final Duration s3SocketTimeout; + private final int s3MaxConnections; + private final DataSize s3MultipartMinFileSize; + private final DataSize s3MultipartMinPartSize; + private final File s3StagingDirectory; + private final List resourcePaths; + + @Inject + public HdfsConfigurationUpdater(HiveClientConfig hiveClientConfig) + { + checkNotNull(hiveClientConfig, "hiveClientConfig is null"); + checkArgument(hiveClientConfig.getDfsTimeout().toMillis() >= 1, "dfsTimeout must be at least 1 ms"); + + this.socksProxy = hiveClientConfig.getMetastoreSocksProxy(); + this.dfsTimeout = hiveClientConfig.getDfsTimeout(); + this.dfsConnectTimeout = hiveClientConfig.getDfsConnectTimeout(); + this.dfsConnectMaxRetries = hiveClientConfig.getDfsConnectMaxRetries(); + this.domainSocketPath = hiveClientConfig.getDomainSocketPath(); + this.s3AwsAccessKey = hiveClientConfig.getS3AwsAccessKey(); + this.s3AwsSecretKey = hiveClientConfig.getS3AwsSecretKey(); + this.s3UseInstanceCredentials = hiveClientConfig.isS3UseInstanceCredentials(); + this.s3SslEnabled = hiveClientConfig.isS3SslEnabled(); + this.s3MaxClientRetries = hiveClientConfig.getS3MaxClientRetries(); + this.s3MaxErrorRetries = hiveClientConfig.getS3MaxErrorRetries(); + this.s3MaxBackoffTime = hiveClientConfig.getS3MaxBackoffTime(); + this.s3MaxRetryTime = hiveClientConfig.getS3MaxRetryTime(); + this.s3ConnectTimeout = hiveClientConfig.getS3ConnectTimeout(); + this.s3SocketTimeout = hiveClientConfig.getS3SocketTimeout(); + this.s3MaxConnections = hiveClientConfig.getS3MaxConnections(); + this.s3MultipartMinFileSize = hiveClientConfig.getS3MultipartMinFileSize(); + this.s3MultipartMinPartSize = hiveClientConfig.getS3MultipartMinPartSize(); + this.s3StagingDirectory = hiveClientConfig.getS3StagingDirectory(); + this.resourcePaths = hiveClientConfig.getResourceConfigFiles(); + } + + public void updateConfiguration(Configuration config) + { + if (resourcePaths != null) { + for (String resourcePath : resourcePaths) { + config.addResource(new Path(resourcePath)); + } + } + + // this is to prevent dfs client from doing reverse DNS lookups to determine whether nodes are rack local + config.setClass("topology.node.switch.mapping.impl", NoOpDNSToSwitchMapping.class, DNSToSwitchMapping.class); + + if (socksProxy != null) { + config.setClass("hadoop.rpc.socket.factory.class.default", SocksSocketFactory.class, SocketFactory.class); + config.set("hadoop.socks.server", socksProxy.toString()); + } + + if (domainSocketPath != null) { + config.setStrings("dfs.domain.socket.path", domainSocketPath); + } + + // only enable short circuit reads if domain socket path is properly configured + if (!config.get("dfs.domain.socket.path", "").trim().isEmpty()) { + config.setBooleanIfUnset("dfs.client.read.shortcircuit", true); + } + + config.setInt("dfs.socket.timeout", Ints.checkedCast(dfsTimeout.toMillis())); + config.setInt("ipc.ping.interval", Ints.checkedCast(dfsTimeout.toMillis())); + config.setInt("ipc.client.connect.timeout", Ints.checkedCast(dfsConnectTimeout.toMillis())); + config.setInt("ipc.client.connect.max.retries", dfsConnectMaxRetries); + + // re-map filesystem schemes to match Amazon Elastic MapReduce + config.set("fs.s3.impl", PrestoS3FileSystem.class.getName()); + config.set("fs.s3a.impl", PrestoS3FileSystem.class.getName()); + config.set("fs.s3n.impl", PrestoS3FileSystem.class.getName()); + config.set("fs.s3bfs.impl", "org.apache.hadoop.fs.s3.S3FileSystem"); + + // set AWS credentials for S3 + for (String scheme : ImmutableList.of("s3", "s3a", "s3bfs", "s3n")) { + if (s3AwsAccessKey != null) { + config.set(format("fs.%s.awsAccessKeyId", scheme), s3AwsAccessKey); + } + if (s3AwsSecretKey != null) { + config.set(format("fs.%s.awsSecretAccessKey", scheme), s3AwsSecretKey); + } + } + + // set config for S3 + config.setBoolean(PrestoS3FileSystem.S3_USE_INSTANCE_CREDENTIALS, s3UseInstanceCredentials); + config.setBoolean(PrestoS3FileSystem.S3_SSL_ENABLED, s3SslEnabled); + config.setInt(PrestoS3FileSystem.S3_MAX_CLIENT_RETRIES, s3MaxClientRetries); + config.setInt(PrestoS3FileSystem.S3_MAX_ERROR_RETRIES, s3MaxErrorRetries); + config.set(PrestoS3FileSystem.S3_MAX_BACKOFF_TIME, s3MaxBackoffTime.toString()); + config.set(PrestoS3FileSystem.S3_MAX_RETRY_TIME, s3MaxRetryTime.toString()); + config.set(PrestoS3FileSystem.S3_CONNECT_TIMEOUT, s3ConnectTimeout.toString()); + config.set(PrestoS3FileSystem.S3_SOCKET_TIMEOUT, s3SocketTimeout.toString()); + config.set(PrestoS3FileSystem.S3_STAGING_DIRECTORY, s3StagingDirectory.toString()); + config.setInt(PrestoS3FileSystem.S3_MAX_CONNECTIONS, s3MaxConnections); + config.setLong(PrestoS3FileSystem.S3_MULTIPART_MIN_FILE_SIZE, s3MultipartMinFileSize.toBytes()); + config.setLong(PrestoS3FileSystem.S3_MULTIPART_MIN_PART_SIZE, s3MultipartMinPartSize.toBytes()); + } + + public static class NoOpDNSToSwitchMapping + implements DNSToSwitchMapping + { + @Override + public List resolve(List names) + { + // dfs client expects an empty list as an indication that the host->switch mapping for the given names are not known + return ImmutableList.of(); + } + + @Override + public void reloadCachedMappings() + { + // no-op + } + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/HdfsEnvironment.java b/presto-hive/src/main/java/com/facebook/presto/hive/HdfsEnvironment.java new file mode 100644 index 00000000..5eec6980 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/HdfsEnvironment.java @@ -0,0 +1,58 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.hadoop.HadoopFileSystemCache; +import com.facebook.presto.hadoop.HadoopNative; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.Path; + +import javax.inject.Inject; + +import java.io.IOException; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class HdfsEnvironment +{ + static { + HadoopNative.requireHadoopNative(); + HadoopFileSystemCache.initialize(); + } + + private final HdfsConfiguration hdfsConfiguration; + private final boolean verifyChecksum; + + @Inject + public HdfsEnvironment(HdfsConfiguration hdfsConfiguration, HiveClientConfig config) + { + this.hdfsConfiguration = checkNotNull(hdfsConfiguration, "hdfsConfiguration is null"); + this.verifyChecksum = checkNotNull(config, "config is null").isVerifyChecksum(); + } + + public Configuration getConfiguration(Path path) + { + return hdfsConfiguration.getConfiguration(path.toUri()); + } + + public FileSystem getFileSystem(Path path) + throws IOException + { + FileSystem fileSystem = path.getFileSystem(getConfiguration(path)); + fileSystem.setVerifyChecksum(verifyChecksum); + + return fileSystem; + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/HiveBooleanParser.java b/presto-hive/src/main/java/com/facebook/presto/hive/HiveBooleanParser.java new file mode 100644 index 00000000..e547cc6c --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/HiveBooleanParser.java @@ -0,0 +1,61 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +public final class HiveBooleanParser +{ + private HiveBooleanParser() {} + + public static Boolean parseHiveBoolean(byte[] bytes, int start, int length) + { + if (isTrue(bytes, start, length)) { + return true; + } + if (isFalse(bytes, start, length)) { + return false; + } + return null; + } + + @SuppressWarnings("PointlessArithmeticExpression") + public static boolean isFalse(byte[] bytes, int start, int length) + { + return (length == 5) && + (toUpperCase(bytes[start + 0]) == 'F') && + (toUpperCase(bytes[start + 1]) == 'A') && + (toUpperCase(bytes[start + 2]) == 'L') && + (toUpperCase(bytes[start + 3]) == 'S') && + (toUpperCase(bytes[start + 4]) == 'E'); + } + + @SuppressWarnings("PointlessArithmeticExpression") + public static boolean isTrue(byte[] bytes, int start, int length) + { + return (length == 4) && + (toUpperCase(bytes[start + 0]) == 'T') && + (toUpperCase(bytes[start + 1]) == 'R') && + (toUpperCase(bytes[start + 2]) == 'U') && + (toUpperCase(bytes[start + 3]) == 'E'); + } + + private static byte toUpperCase(byte b) + { + return isLowerCase(b) ? ((byte) (b - 32)) : b; + } + + private static boolean isLowerCase(byte b) + { + return (b >= 'a') && (b <= 'z'); + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/HiveBucketing.java b/presto-hive/src/main/java/com/facebook/presto/hive/HiveBucketing.java new file mode 100644 index 00000000..fd5d25f3 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/HiveBucketing.java @@ -0,0 +1,226 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.spi.ColumnHandle; +import com.google.common.collect.ImmutableList; +import io.airlift.log.Logger; +import io.airlift.slice.Slice; +import org.apache.hadoop.hive.metastore.api.Table; +import org.apache.hadoop.hive.ql.io.DefaultHivePartitioner; +import org.apache.hadoop.hive.ql.io.HiveKey; +import org.apache.hadoop.hive.ql.metadata.HiveException; +import org.apache.hadoop.hive.ql.udf.generic.GenericUDFHash; +import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.PrimitiveObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.StructField; +import org.apache.hadoop.hive.serde2.objectinspector.primitive.IntObjectInspector; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static com.facebook.presto.hive.HiveUtil.getTableStructFields; +import static com.facebook.presto.hive.util.Types.checkType; +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.collect.Maps.immutableEntry; +import static com.google.common.collect.Sets.immutableEnumSet; +import static java.util.Map.Entry; +import static org.apache.hadoop.hive.ql.udf.generic.GenericUDF.DeferredJavaObject; +import static org.apache.hadoop.hive.ql.udf.generic.GenericUDF.DeferredObject; +import static org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector.Category; +import static org.apache.hadoop.hive.serde2.objectinspector.PrimitiveObjectInspector.PrimitiveCategory; +import static org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory.javaBooleanObjectInspector; +import static org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory.javaByteObjectInspector; +import static org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory.javaIntObjectInspector; +import static org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory.javaLongObjectInspector; +import static org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory.javaShortObjectInspector; +import static org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory.javaStringObjectInspector; + +final class HiveBucketing +{ + private static final Logger log = Logger.get(HiveBucketing.class); + + private static final Set SUPPORTED_TYPES = immutableEnumSet( + PrimitiveCategory.BYTE, + PrimitiveCategory.SHORT, + PrimitiveCategory.INT, + PrimitiveCategory.LONG, + PrimitiveCategory.BOOLEAN, + PrimitiveCategory.STRING); + + private HiveBucketing() {} + + public static Optional getHiveBucket(Table table, Map bindings) + { + if (!table.getSd().isSetBucketCols() || table.getSd().getBucketCols().isEmpty() || + !table.getSd().isSetNumBuckets() || (table.getSd().getNumBuckets() <= 0) || + bindings.isEmpty()) { + return Optional.empty(); + } + + List bucketColumns = table.getSd().getBucketCols(); + Map objectInspectors = new HashMap<>(); + + // Get column name to object inspector mapping + for (StructField field : getTableStructFields(table)) { + objectInspectors.put(field.getFieldName(), field.getFieldObjectInspector()); + } + + // Verify the bucket column types are supported + for (String column : bucketColumns) { + ObjectInspector inspector = objectInspectors.get(column); + if ((inspector == null) || (inspector.getCategory() != Category.PRIMITIVE)) { + return Optional.empty(); + } + if (!SUPPORTED_TYPES.contains(((PrimitiveObjectInspector) inspector).getPrimitiveCategory())) { + return Optional.empty(); + } + } + + // Get bindings for bucket columns + Map bucketBindings = new HashMap<>(); + for (Entry entry : bindings.entrySet()) { + HiveColumnHandle colHandle = (HiveColumnHandle) entry.getKey(); + if (bucketColumns.contains(colHandle.getName())) { + bucketBindings.put(colHandle.getName(), entry.getValue()); + } + } + + // Check that we have bindings for all bucket columns + if (bucketBindings.size() != bucketColumns.size()) { + return Optional.empty(); + } + + // Get bindings of bucket columns + ImmutableList.Builder> columnBindings = ImmutableList.builder(); + for (String column : bucketColumns) { + columnBindings.add(immutableEntry(objectInspectors.get(column), bucketBindings.get(column))); + } + + return getHiveBucket(columnBindings.build(), table.getSd().getNumBuckets()); + } + + public static Optional getHiveBucket(List> columnBindings, int bucketCount) + { + try { + @SuppressWarnings("resource") + GenericUDFHash udf = new GenericUDFHash(); + ObjectInspector[] objectInspectors = new ObjectInspector[columnBindings.size()]; + DeferredObject[] deferredObjects = new DeferredObject[columnBindings.size()]; + + int i = 0; + for (Entry entry : columnBindings) { + objectInspectors[i] = getJavaObjectInspector(entry.getKey()); + deferredObjects[i] = getJavaDeferredObject(entry.getValue(), entry.getKey()); + i++; + } + + ObjectInspector udfInspector = udf.initialize(objectInspectors); + IntObjectInspector inspector = checkType(udfInspector, IntObjectInspector.class, "udfInspector"); + + Object result = udf.evaluate(deferredObjects); + HiveKey hiveKey = new HiveKey(); + hiveKey.setHashCode(inspector.get(result)); + + int bucketNumber = new DefaultHivePartitioner<>().getBucket(hiveKey, null, bucketCount); + + return Optional.of(new HiveBucket(bucketNumber, bucketCount)); + } + catch (HiveException e) { + log.debug(e, "Error evaluating bucket number"); + return Optional.empty(); + } + } + + private static ObjectInspector getJavaObjectInspector(ObjectInspector objectInspector) + { + checkArgument(objectInspector.getCategory() == Category.PRIMITIVE, "Unsupported object inspector category %s", objectInspector.getCategory()); + PrimitiveObjectInspector poi = ((PrimitiveObjectInspector) objectInspector); + switch (poi.getPrimitiveCategory()) { + case BOOLEAN: + return javaBooleanObjectInspector; + case BYTE: + return javaByteObjectInspector; + case SHORT: + return javaShortObjectInspector; + case INT: + return javaIntObjectInspector; + case LONG: + return javaLongObjectInspector; + case STRING: + return javaStringObjectInspector; + } + throw new RuntimeException("Unsupported type: " + poi.getPrimitiveCategory()); + } + + private static DeferredObject getJavaDeferredObject(Object object, ObjectInspector objectInspector) + { + checkArgument(objectInspector.getCategory() == Category.PRIMITIVE, "Unsupported object inspector category %s", objectInspector.getCategory()); + PrimitiveObjectInspector poi = ((PrimitiveObjectInspector) objectInspector); + switch (poi.getPrimitiveCategory()) { + case BOOLEAN: + return new DeferredJavaObject(object); + case BYTE: + return new DeferredJavaObject(((Long) object).byteValue()); + case SHORT: + return new DeferredJavaObject(((Long) object).shortValue()); + case INT: + return new DeferredJavaObject(((Long) object).intValue()); + case LONG: + return new DeferredJavaObject(object); + case STRING: + return new DeferredJavaObject(((Slice) object).toStringUtf8()); + } + throw new RuntimeException("Unsupported type: " + poi.getPrimitiveCategory()); + } + + public static class HiveBucket + { + private final int bucketNumber; + private final int bucketCount; + + public HiveBucket(int bucketNumber, int bucketCount) + { + checkArgument(bucketCount > 0, "bucketCount must be greater than zero"); + checkArgument(bucketNumber >= 0, "bucketCount must be positive"); + checkArgument(bucketNumber < bucketCount, "bucketNumber must be less than bucketCount"); + + this.bucketNumber = bucketNumber; + this.bucketCount = bucketCount; + } + + public int getBucketNumber() + { + return bucketNumber; + } + + public int getBucketCount() + { + return bucketCount; + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("bucketNumber", bucketNumber) + .add("bucketCount", bucketCount) + .toString(); + } + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/HiveClientConfig.java b/presto-hive/src/main/java/com/facebook/presto/hive/HiveClientConfig.java new file mode 100644 index 00000000..8d2f533e --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/HiveClientConfig.java @@ -0,0 +1,702 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.google.common.base.Splitter; +import com.google.common.base.StandardSystemProperty; +import com.google.common.collect.ImmutableList; +import com.google.common.net.HostAndPort; +import io.airlift.configuration.Config; +import io.airlift.configuration.ConfigDescription; +import io.airlift.configuration.DefunctConfig; +import io.airlift.units.DataSize; +import io.airlift.units.Duration; +import io.airlift.units.MinDataSize; +import io.airlift.units.MinDuration; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; + +import java.io.File; +import java.util.List; +import java.util.TimeZone; +import java.util.concurrent.TimeUnit; + +import static io.airlift.units.DataSize.Unit.MEGABYTE; + +@DefunctConfig({ + "hive.file-system-cache-ttl", + "hive.max-global-split-iterator-threads", +}) +public class HiveClientConfig +{ + private static final Splitter SPLITTER = Splitter.on(',').trimResults().omitEmptyStrings(); + + private TimeZone timeZone = TimeZone.getDefault(); + + private DataSize maxSplitSize = new DataSize(64, MEGABYTE); + private int maxOutstandingSplits = 1_000; + private int maxSplitIteratorThreads = 1_000; + private int minPartitionBatchSize = 10; + private int maxPartitionBatchSize = 100; + private int maxInitialSplits = 200; + private DataSize maxInitialSplitSize; + private boolean forceLocalScheduling; + private boolean recursiveDirWalkerEnabled; + private boolean allowDropTable; + private boolean allowRenameTable; + + private boolean allowCorruptWritesForTesting; + + private Duration metastoreCacheTtl = new Duration(1, TimeUnit.HOURS); + private Duration metastoreRefreshInterval = new Duration(1, TimeUnit.SECONDS); + private int maxMetastoreRefreshThreads = 100; + private HostAndPort metastoreSocksProxy; + private Duration metastoreTimeout = new Duration(10, TimeUnit.SECONDS); + + private Duration dfsTimeout = new Duration(10, TimeUnit.SECONDS); + private Duration dfsConnectTimeout = new Duration(500, TimeUnit.MILLISECONDS); + private int dfsConnectMaxRetries = 5; + private boolean verifyChecksum = true; + + private String domainSocketPath; + + private String s3AwsAccessKey; + private String s3AwsSecretKey; + private boolean s3UseInstanceCredentials = true; + private boolean s3SslEnabled = true; + private int s3MaxClientRetries = 3; + private int s3MaxErrorRetries = 10; + private Duration s3MaxBackoffTime = new Duration(10, TimeUnit.MINUTES); + private Duration s3MaxRetryTime = new Duration(10, TimeUnit.MINUTES); + private Duration s3ConnectTimeout = new Duration(5, TimeUnit.SECONDS); + private Duration s3SocketTimeout = new Duration(5, TimeUnit.SECONDS); + private int s3MaxConnections = 500; + private File s3StagingDirectory = new File(StandardSystemProperty.JAVA_IO_TMPDIR.value()); + private DataSize s3MultipartMinFileSize = new DataSize(16, MEGABYTE); + private DataSize s3MultipartMinPartSize = new DataSize(5, MEGABYTE); + private boolean useParquetColumnNames; + + private HiveStorageFormat hiveStorageFormat = HiveStorageFormat.ORC; + + private List resourceConfigFiles; + + private boolean optimizedReaderEnabled = true; + + private boolean assumeCanonicalPartitionKeys; + + private DataSize orcMaxMergeDistance = new DataSize(1, MEGABYTE); + private DataSize orcMaxBufferSize = new DataSize(8, MEGABYTE); + private DataSize orcStreamBufferSize = new DataSize(8, MEGABYTE); + + public int getMaxInitialSplits() + { + return maxInitialSplits; + } + + @Config("hive.max-initial-splits") + public HiveClientConfig setMaxInitialSplits(int maxInitialSplits) + { + this.maxInitialSplits = maxInitialSplits; + return this; + } + + public DataSize getMaxInitialSplitSize() + { + if (maxInitialSplitSize == null) { + return new DataSize(maxSplitSize.getValue() / 2, maxSplitSize.getUnit()); + } + return maxInitialSplitSize; + } + + @Config("hive.max-initial-split-size") + public HiveClientConfig setMaxInitialSplitSize(DataSize maxInitialSplitSize) + { + this.maxInitialSplitSize = maxInitialSplitSize; + return this; + } + + public boolean isForceLocalScheduling() + { + return forceLocalScheduling; + } + + @Config("hive.force-local-scheduling") + public HiveClientConfig setForceLocalScheduling(boolean forceLocalScheduling) + { + this.forceLocalScheduling = forceLocalScheduling; + return this; + } + + @NotNull + public TimeZone getTimeZone() + { + return timeZone; + } + + @Config("hive.recursive-directories") + public HiveClientConfig setRecursiveDirWalkerEnabled(boolean recursiveDirWalkerEnabled) + { + this.recursiveDirWalkerEnabled = recursiveDirWalkerEnabled; + return this; + } + + public boolean getRecursiveDirWalkerEnabled() + { + return recursiveDirWalkerEnabled; + } + + @Config("hive.time-zone") + public HiveClientConfig setTimeZone(String id) + { + this.timeZone = (id == null) ? TimeZone.getDefault() : TimeZone.getTimeZone(id); + return this; + } + + public HiveClientConfig setTimeZone(TimeZone timeZone) + { + this.timeZone = (timeZone == null) ? TimeZone.getDefault() : timeZone; + return this; + } + + @NotNull + public DataSize getMaxSplitSize() + { + return maxSplitSize; + } + + @Config("hive.max-split-size") + public HiveClientConfig setMaxSplitSize(DataSize maxSplitSize) + { + this.maxSplitSize = maxSplitSize; + return this; + } + + @Min(1) + public int getMaxOutstandingSplits() + { + return maxOutstandingSplits; + } + + @Config("hive.max-outstanding-splits") + public HiveClientConfig setMaxOutstandingSplits(int maxOutstandingSplits) + { + this.maxOutstandingSplits = maxOutstandingSplits; + return this; + } + + @Min(1) + public int getMaxSplitIteratorThreads() + { + return maxSplitIteratorThreads; + } + + @Config("hive.max-split-iterator-threads") + public HiveClientConfig setMaxSplitIteratorThreads(int maxSplitIteratorThreads) + { + this.maxSplitIteratorThreads = maxSplitIteratorThreads; + return this; + } + + public boolean getAllowRenameTable() + { + return this.allowRenameTable; + } + + @Config("hive.allow-rename-table") + @ConfigDescription("Allow hive connector to rename table") + public HiveClientConfig setAllowRenameTable(boolean allowRenameTable) + { + this.allowRenameTable = allowRenameTable; + return this; + } + + @Deprecated + public boolean getAllowCorruptWritesForTesting() + { + return allowCorruptWritesForTesting; + } + + @Deprecated + @Config("hive.allow-corrupt-writes-for-testing") + @ConfigDescription("Allow Hive connector to write data even when data will likely be corrupt") + public HiveClientConfig setAllowCorruptWritesForTesting(boolean allowCorruptWritesForTesting) + { + this.allowCorruptWritesForTesting = allowCorruptWritesForTesting; + return this; + } + + public boolean getAllowDropTable() + { + return this.allowDropTable; + } + + @Config("hive.allow-drop-table") + @ConfigDescription("Allow Hive connector to drop table") + public HiveClientConfig setAllowDropTable(boolean allowDropTable) + { + this.allowDropTable = allowDropTable; + return this; + } + + @NotNull + public Duration getMetastoreCacheTtl() + { + return metastoreCacheTtl; + } + + @Config("hive.metastore-cache-ttl") + public HiveClientConfig setMetastoreCacheTtl(Duration metastoreCacheTtl) + { + this.metastoreCacheTtl = metastoreCacheTtl; + return this; + } + + @NotNull + public Duration getMetastoreRefreshInterval() + { + return metastoreRefreshInterval; + } + + @Config("hive.metastore-refresh-interval") + public HiveClientConfig setMetastoreRefreshInterval(Duration metastoreRefreshInterval) + { + this.metastoreRefreshInterval = metastoreRefreshInterval; + return this; + } + + @Min(1) + public int getMaxMetastoreRefreshThreads() + { + return maxMetastoreRefreshThreads; + } + + @Config("hive.metastore-refresh-max-threads") + public HiveClientConfig setMaxMetastoreRefreshThreads(int maxMetastoreRefreshThreads) + { + this.maxMetastoreRefreshThreads = maxMetastoreRefreshThreads; + return this; + } + + public HostAndPort getMetastoreSocksProxy() + { + return metastoreSocksProxy; + } + + @Config("hive.metastore.thrift.client.socks-proxy") + public HiveClientConfig setMetastoreSocksProxy(HostAndPort metastoreSocksProxy) + { + this.metastoreSocksProxy = metastoreSocksProxy; + return this; + } + + @NotNull + public Duration getMetastoreTimeout() + { + return metastoreTimeout; + } + + @Config("hive.metastore-timeout") + public HiveClientConfig setMetastoreTimeout(Duration metastoreTimeout) + { + this.metastoreTimeout = metastoreTimeout; + return this; + } + + @Min(1) + public int getMinPartitionBatchSize() + { + return minPartitionBatchSize; + } + + @Config("hive.metastore.partition-batch-size.min") + public HiveClientConfig setMinPartitionBatchSize(int minPartitionBatchSize) + { + this.minPartitionBatchSize = minPartitionBatchSize; + return this; + } + + @Min(1) + public int getMaxPartitionBatchSize() + { + return maxPartitionBatchSize; + } + + @Config("hive.metastore.partition-batch-size.max") + public HiveClientConfig setMaxPartitionBatchSize(int maxPartitionBatchSize) + { + this.maxPartitionBatchSize = maxPartitionBatchSize; + return this; + } + + public List getResourceConfigFiles() + { + return resourceConfigFiles; + } + + @Config("hive.config.resources") + public HiveClientConfig setResourceConfigFiles(String files) + { + this.resourceConfigFiles = (files == null) ? null : SPLITTER.splitToList(files); + return this; + } + + public HiveClientConfig setResourceConfigFiles(List files) + { + this.resourceConfigFiles = (files == null) ? null : ImmutableList.copyOf(files); + return this; + } + + @NotNull + @MinDuration("1ms") + public Duration getDfsTimeout() + { + return dfsTimeout; + } + + @Config("hive.dfs-timeout") + public HiveClientConfig setDfsTimeout(Duration dfsTimeout) + { + this.dfsTimeout = dfsTimeout; + return this; + } + + @MinDuration("1ms") + @NotNull + public Duration getDfsConnectTimeout() + { + return dfsConnectTimeout; + } + + @Config("hive.dfs.connect.timeout") + public HiveClientConfig setDfsConnectTimeout(Duration dfsConnectTimeout) + { + this.dfsConnectTimeout = dfsConnectTimeout; + return this; + } + + @Min(0) + public int getDfsConnectMaxRetries() + { + return dfsConnectMaxRetries; + } + + @Config("hive.dfs.connect.max-retries") + public HiveClientConfig setDfsConnectMaxRetries(int dfsConnectMaxRetries) + { + this.dfsConnectMaxRetries = dfsConnectMaxRetries; + return this; + } + + public HiveStorageFormat getHiveStorageFormat() + { + return hiveStorageFormat; + } + + @Config("hive.storage-format") + public HiveClientConfig setHiveStorageFormat(HiveStorageFormat hiveStorageFormat) + { + this.hiveStorageFormat = hiveStorageFormat; + return this; + } + + public String getDomainSocketPath() + { + return domainSocketPath; + } + + @Config("dfs.domain-socket-path") + public HiveClientConfig setDomainSocketPath(String domainSocketPath) + { + this.domainSocketPath = domainSocketPath; + return this; + } + + public boolean isVerifyChecksum() + { + return verifyChecksum; + } + + @Config("hive.dfs.verify-checksum") + public HiveClientConfig setVerifyChecksum(boolean verifyChecksum) + { + this.verifyChecksum = verifyChecksum; + return this; + } + + public String getS3AwsAccessKey() + { + return s3AwsAccessKey; + } + + @Config("hive.s3.aws-access-key") + public HiveClientConfig setS3AwsAccessKey(String s3AwsAccessKey) + { + this.s3AwsAccessKey = s3AwsAccessKey; + return this; + } + + public String getS3AwsSecretKey() + { + return s3AwsSecretKey; + } + + @Config("hive.s3.aws-secret-key") + public HiveClientConfig setS3AwsSecretKey(String s3AwsSecretKey) + { + this.s3AwsSecretKey = s3AwsSecretKey; + return this; + } + + public boolean isS3UseInstanceCredentials() + { + return s3UseInstanceCredentials; + } + + @Config("hive.s3.use-instance-credentials") + public HiveClientConfig setS3UseInstanceCredentials(boolean s3UseInstanceCredentials) + { + this.s3UseInstanceCredentials = s3UseInstanceCredentials; + return this; + } + + public boolean isS3SslEnabled() + { + return s3SslEnabled; + } + + @Config("hive.s3.ssl.enabled") + public HiveClientConfig setS3SslEnabled(boolean s3SslEnabled) + { + this.s3SslEnabled = s3SslEnabled; + return this; + } + + @Min(0) + public int getS3MaxClientRetries() + { + return s3MaxClientRetries; + } + + @Config("hive.s3.max-client-retries") + public HiveClientConfig setS3MaxClientRetries(int s3MaxClientRetries) + { + this.s3MaxClientRetries = s3MaxClientRetries; + return this; + } + + @Min(0) + public int getS3MaxErrorRetries() + { + return s3MaxErrorRetries; + } + + @Config("hive.s3.max-error-retries") + public HiveClientConfig setS3MaxErrorRetries(int s3MaxErrorRetries) + { + this.s3MaxErrorRetries = s3MaxErrorRetries; + return this; + } + + @MinDuration("1s") + @NotNull + public Duration getS3MaxBackoffTime() + { + return s3MaxBackoffTime; + } + + @Config("hive.s3.max-backoff-time") + public HiveClientConfig setS3MaxBackoffTime(Duration s3MaxBackoffTime) + { + this.s3MaxBackoffTime = s3MaxBackoffTime; + return this; + } + + @MinDuration("1ms") + @NotNull + public Duration getS3MaxRetryTime() + { + return s3MaxRetryTime; + } + + @Config("hive.s3.max-retry-time") + public HiveClientConfig setS3MaxRetryTime(Duration s3MaxRetryTime) + { + this.s3MaxRetryTime = s3MaxRetryTime; + return this; + } + + @MinDuration("1ms") + @NotNull + public Duration getS3ConnectTimeout() + { + return s3ConnectTimeout; + } + + @Config("hive.s3.connect-timeout") + public HiveClientConfig setS3ConnectTimeout(Duration s3ConnectTimeout) + { + this.s3ConnectTimeout = s3ConnectTimeout; + return this; + } + + @MinDuration("1ms") + @NotNull + public Duration getS3SocketTimeout() + { + return s3SocketTimeout; + } + + @Config("hive.s3.socket-timeout") + public HiveClientConfig setS3SocketTimeout(Duration s3SocketTimeout) + { + this.s3SocketTimeout = s3SocketTimeout; + return this; + } + + @Min(1) + public int getS3MaxConnections() + { + return s3MaxConnections; + } + + @Config("hive.s3.max-connections") + public HiveClientConfig setS3MaxConnections(int s3MaxConnections) + { + this.s3MaxConnections = s3MaxConnections; + return this; + } + + @NotNull + public File getS3StagingDirectory() + { + return s3StagingDirectory; + } + + @Config("hive.s3.staging-directory") + @ConfigDescription("Temporary directory for staging files before uploading to S3") + public HiveClientConfig setS3StagingDirectory(File s3StagingDirectory) + { + this.s3StagingDirectory = s3StagingDirectory; + return this; + } + + @NotNull + @MinDataSize("16MB") + public DataSize getS3MultipartMinFileSize() + { + return s3MultipartMinFileSize; + } + + @Config("hive.s3.multipart.min-file-size") + @ConfigDescription("Minimum file size for an S3 multipart upload") + public HiveClientConfig setS3MultipartMinFileSize(DataSize size) + { + this.s3MultipartMinFileSize = size; + return this; + } + + @NotNull + @MinDataSize("5MB") + public DataSize getS3MultipartMinPartSize() + { + return s3MultipartMinPartSize; + } + + @Config("hive.s3.multipart.min-part-size") + @ConfigDescription("Minimum part size for an S3 multipart upload") + public HiveClientConfig setS3MultipartMinPartSize(DataSize size) + { + this.s3MultipartMinPartSize = size; + return this; + } + + @Deprecated + public boolean isOptimizedReaderEnabled() + { + return optimizedReaderEnabled; + } + + @Deprecated + @Config("hive.optimized-reader.enabled") + public HiveClientConfig setOptimizedReaderEnabled(boolean optimizedReaderEnabled) + { + this.optimizedReaderEnabled = optimizedReaderEnabled; + return this; + } + + @NotNull + public DataSize getOrcMaxMergeDistance() + { + return orcMaxMergeDistance; + } + + @Config("hive.orc.max-merge-distance") + public HiveClientConfig setOrcMaxMergeDistance(DataSize orcMaxMergeDistance) + { + this.orcMaxMergeDistance = orcMaxMergeDistance; + return this; + } + + @NotNull + public DataSize getOrcMaxBufferSize() + { + return orcMaxBufferSize; + } + + @Config("hive.orc.max-buffer-size") + public HiveClientConfig setOrcMaxBufferSize(DataSize orcMaxBufferSize) + { + this.orcMaxBufferSize = orcMaxBufferSize; + return this; + } + + @NotNull + public DataSize getOrcStreamBufferSize() + { + return orcStreamBufferSize; + } + + @Config("hive.orc.stream-buffer-size") + public HiveClientConfig setOrcStreamBufferSize(DataSize orcStreamBufferSize) + { + this.orcStreamBufferSize = orcStreamBufferSize; + return this; + } + + public boolean isAssumeCanonicalPartitionKeys() + { + return assumeCanonicalPartitionKeys; + } + + @Config("hive.assume-canonical-partition-keys") + public HiveClientConfig setAssumeCanonicalPartitionKeys(boolean assumeCanonicalPartitionKeys) + { + this.assumeCanonicalPartitionKeys = assumeCanonicalPartitionKeys; + return this; + } + + public boolean isUseParquetColumnNames() + { + return useParquetColumnNames; + } + + @Config("hive.parquet.use-column-names") + @ConfigDescription("Access Parquet columns using names from the file") + public HiveClientConfig setUseParquetColumnNames(boolean useParquetColumnNames) + { + this.useParquetColumnNames = useParquetColumnNames; + return this; + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/HiveClientModule.java b/presto-hive/src/main/java/com/facebook/presto/hive/HiveClientModule.java new file mode 100644 index 00000000..506a98ac --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/HiveClientModule.java @@ -0,0 +1,156 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.hive.metastore.CachingHiveMetastore; +import com.facebook.presto.hive.metastore.HiveMetastore; +import com.facebook.presto.hive.orc.DwrfPageSourceFactory; +import com.facebook.presto.hive.orc.DwrfRecordCursorProvider; +import com.facebook.presto.hive.orc.OrcPageSourceFactory; +import com.facebook.presto.hive.orc.OrcRecordCursorProvider; +import com.facebook.presto.hive.rcfile.RcFilePageSourceFactory; +import com.facebook.presto.spi.ConnectorHandleResolver; +import com.facebook.presto.spi.ConnectorMetadata; +import com.facebook.presto.spi.ConnectorPageSourceProvider; +import com.facebook.presto.spi.ConnectorRecordSinkProvider; +import com.facebook.presto.spi.ConnectorSplitManager; +import com.facebook.presto.spi.type.TypeManager; +import com.google.common.net.HostAndPort; +import com.google.inject.Binder; +import com.google.inject.Injector; +import com.google.inject.Module; +import com.google.inject.Provides; +import com.google.inject.Scopes; +import com.google.inject.multibindings.Multibinder; + +import javax.inject.Singleton; + +import java.net.URI; +import java.util.concurrent.ExecutorService; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Strings.isNullOrEmpty; +import static io.airlift.concurrent.Threads.daemonThreadsNamed; +import static io.airlift.configuration.ConfigBinder.configBinder; +import static io.airlift.discovery.client.DiscoveryBinder.discoveryBinder; +import static java.util.concurrent.Executors.newCachedThreadPool; +import static java.util.concurrent.Executors.newFixedThreadPool; +import static org.weakref.jmx.ObjectNames.generatedNameOf; +import static org.weakref.jmx.guice.ExportBinder.newExporter; + +public class HiveClientModule + implements Module +{ + private final String connectorId; + private final HiveMetastore metastore; + private final TypeManager typeManager; + + public HiveClientModule(String connectorId, HiveMetastore metastore, TypeManager typeManager) + { + this.connectorId = connectorId; + this.metastore = metastore; + this.typeManager = typeManager; + } + + @Override + public void configure(Binder binder) + { + binder.bind(HiveConnectorId.class).toInstance(new HiveConnectorId(connectorId)); + + binder.bind(HdfsConfigurationUpdater.class).in(Scopes.SINGLETON); + binder.bind(HdfsConfiguration.class).to(HiveHdfsConfiguration.class).in(Scopes.SINGLETON); + binder.bind(HdfsEnvironment.class).in(Scopes.SINGLETON); + binder.bind(DirectoryLister.class).to(HadoopDirectoryLister.class).in(Scopes.SINGLETON); + configBinder(binder).bindConfig(HiveClientConfig.class); + configBinder(binder).bindConfig(HivePluginConfig.class); + + if (metastore != null) { + binder.bind(HiveMetastore.class).toInstance(metastore); + } + else { + binder.bind(HiveMetastore.class).to(CachingHiveMetastore.class).in(Scopes.SINGLETON); + newExporter(binder).export(HiveMetastore.class) + .as(generatedNameOf(CachingHiveMetastore.class, connectorId)); + } + + binder.bind(NamenodeStats.class).in(Scopes.SINGLETON); + newExporter(binder).export(NamenodeStats.class).as(generatedNameOf(NamenodeStats.class)); + + binder.bind(DiscoveryLocatedHiveCluster.class).in(Scopes.SINGLETON); + binder.bind(HiveMetastoreClientFactory.class).in(Scopes.SINGLETON); + discoveryBinder(binder).bindSelector("hive-metastore"); + + binder.bind(TypeManager.class).toInstance(typeManager); + + Multibinder recordCursorProviderBinder = Multibinder.newSetBinder(binder, HiveRecordCursorProvider.class); + recordCursorProviderBinder.addBinding().to(OrcRecordCursorProvider.class).in(Scopes.SINGLETON); + recordCursorProviderBinder.addBinding().to(ParquetRecordCursorProvider.class).in(Scopes.SINGLETON); + recordCursorProviderBinder.addBinding().to(DwrfRecordCursorProvider.class).in(Scopes.SINGLETON); + recordCursorProviderBinder.addBinding().to(ColumnarTextHiveRecordCursorProvider.class).in(Scopes.SINGLETON); + recordCursorProviderBinder.addBinding().to(ColumnarBinaryHiveRecordCursorProvider.class).in(Scopes.SINGLETON); + recordCursorProviderBinder.addBinding().to(GenericHiveRecordCursorProvider.class).in(Scopes.SINGLETON); + + binder.bind(ConnectorMetadata.class).to(HiveMetadata.class).in(Scopes.SINGLETON); + binder.bind(ConnectorSplitManager.class).to(HiveSplitManager.class).in(Scopes.SINGLETON); + binder.bind(ConnectorPageSourceProvider.class).to(HivePageSourceProvider.class).in(Scopes.SINGLETON); + binder.bind(ConnectorRecordSinkProvider.class).to(HiveRecordSinkProvider.class).in(Scopes.SINGLETON); + binder.bind(ConnectorHandleResolver.class).to(HiveHandleResolver.class).in(Scopes.SINGLETON); + + Multibinder pageSourceFactoryBinder = Multibinder.newSetBinder(binder, HivePageSourceFactory.class); + pageSourceFactoryBinder.addBinding().to(RcFilePageSourceFactory.class).in(Scopes.SINGLETON); + pageSourceFactoryBinder.addBinding().to(OrcPageSourceFactory.class).in(Scopes.SINGLETON); + pageSourceFactoryBinder.addBinding().to(DwrfPageSourceFactory.class).in(Scopes.SINGLETON); + + binder.bind(PrestoS3FileSystemStats.class).toInstance(PrestoS3FileSystem.getFileSystemStats()); + newExporter(binder).export(PrestoS3FileSystemStats.class).as(generatedNameOf(PrestoS3FileSystem.class, connectorId)); + } + + @ForHiveClient + @Singleton + @Provides + public ExecutorService createHiveClientExecutor(HiveConnectorId hiveClientId) + { + return newCachedThreadPool(daemonThreadsNamed("hive-" + hiveClientId + "-%s")); + } + + @ForHiveMetastore + @Singleton + @Provides + public ExecutorService createCachingHiveMetastoreExecutor(HiveConnectorId hiveClientId, HiveClientConfig hiveClientConfig) + { + return newFixedThreadPool( + hiveClientConfig.getMaxMetastoreRefreshThreads(), + daemonThreadsNamed("hive-metastore-" + hiveClientId + "-%s")); + } + + @Singleton + @Provides + public HiveCluster createHiveCluster(Injector injector, HivePluginConfig config) + { + URI uri = checkNotNull(config.getMetastoreUri(), "metastoreUri is null"); + String scheme = uri.getScheme(); + checkArgument(!isNullOrEmpty(scheme), "metastoreUri scheme is missing: %s", uri); + switch (scheme) { + case "discovery": + return injector.getInstance(DiscoveryLocatedHiveCluster.class); + case "thrift": + checkArgument(uri.getHost() != null, "metastoreUri host is missing: %s", uri); + checkArgument(uri.getPort() != -1, "metastoreUri port is missing: %s", uri); + HostAndPort address = HostAndPort.fromParts(uri.getHost(), uri.getPort()); + return new StaticHiveCluster(address, injector.getInstance(HiveMetastoreClientFactory.class)); + } + throw new IllegalArgumentException("unsupported metastoreUri: " + uri); + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/HiveCluster.java b/presto-hive/src/main/java/com/facebook/presto/hive/HiveCluster.java new file mode 100644 index 00000000..dcd6e837 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/HiveCluster.java @@ -0,0 +1,31 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +/** + * A Hive cluster is a single logical installation of Hive. It might + * have multiple instances of the metastore service (for scalability + * purposes), but they would all return the same data. + *

+ * This Hive plugin only supports having a single Hive cluster per + * instantiation of the plugin, but a plugin that extends this code + * could support multiple, dynamically located Hive clusters. + */ +public interface HiveCluster +{ + /** + * Create a connected {@link HiveMetastoreClient} to this HiveCluster + */ + HiveMetastoreClient createMetastoreClient(); +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/HiveColumnHandle.java b/presto-hive/src/main/java/com/facebook/presto/hive/HiveColumnHandle.java new file mode 100644 index 00000000..a2a276d2 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/HiveColumnHandle.java @@ -0,0 +1,151 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.spi.ColumnMetadata; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.spi.type.TypeSignature; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +import static com.facebook.presto.hive.util.Types.checkType; +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class HiveColumnHandle + implements ColumnHandle +{ + public static final String SAMPLE_WEIGHT_COLUMN_NAME = "__presto__sample_weight__"; + + private final String clientId; + private final String name; + private final int ordinalPosition; + private final HiveType hiveType; + private final TypeSignature typeName; + private final int hiveColumnIndex; + private final boolean partitionKey; + + @JsonCreator + public HiveColumnHandle( + @JsonProperty("clientId") String clientId, + @JsonProperty("name") String name, + @JsonProperty("ordinalPosition") int ordinalPosition, + @JsonProperty("hiveType") HiveType hiveType, + @JsonProperty("typeSignature") TypeSignature typeSignature, + @JsonProperty("hiveColumnIndex") int hiveColumnIndex, + @JsonProperty("partitionKey") boolean partitionKey) + { + this.clientId = checkNotNull(clientId, "clientId is null"); + this.name = checkNotNull(name, "name is null"); + checkArgument(ordinalPosition >= 0, "ordinalPosition is negative"); + this.ordinalPosition = ordinalPosition; + checkArgument(hiveColumnIndex >= 0 || partitionKey, "hiveColumnIndex is negative"); + this.hiveColumnIndex = hiveColumnIndex; + this.hiveType = checkNotNull(hiveType, "hiveType is null"); + this.typeName = checkNotNull(typeSignature, "type is null"); + this.partitionKey = partitionKey; + } + + @JsonProperty + public String getClientId() + { + return clientId; + } + + @JsonProperty + public String getName() + { + return name; + } + + @JsonProperty + public int getOrdinalPosition() + { + return ordinalPosition; + } + + @JsonProperty + public HiveType getHiveType() + { + return hiveType; + } + + @JsonProperty + public int getHiveColumnIndex() + { + return hiveColumnIndex; + } + + @JsonProperty + public boolean isPartitionKey() + { + return partitionKey; + } + + public ColumnMetadata getColumnMetadata(TypeManager typeManager) + { + return new ColumnMetadata(name, typeManager.getType(typeName), partitionKey); + } + + @JsonProperty + public TypeSignature getTypeSignature() + { + return typeName; + } + + @Override + public int hashCode() + { + return Objects.hash(clientId, name, hiveColumnIndex, hiveType, partitionKey); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + HiveColumnHandle other = (HiveColumnHandle) obj; + return Objects.equals(this.clientId, other.clientId) && + Objects.equals(this.name, other.name) && + Objects.equals(this.hiveColumnIndex, other.hiveColumnIndex) && + Objects.equals(this.hiveType, other.hiveType) && + Objects.equals(this.partitionKey, other.partitionKey); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("clientId", clientId) + .add("name", name) + .add("ordinalPosition", ordinalPosition) + .add("hiveType", hiveType) + .add("hiveColumnIndex", hiveColumnIndex) + .add("partitionKey", partitionKey) + .toString(); + } + + public static HiveColumnHandle toHiveColumnHandle(ColumnHandle columnHandle) + { + return checkType(columnHandle, HiveColumnHandle.class, "columnHandle"); + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/HiveConnector.java b/presto-hive/src/main/java/com/facebook/presto/hive/HiveConnector.java new file mode 100644 index 00000000..2168b397 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/HiveConnector.java @@ -0,0 +1,108 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.spi.Connector; +import com.facebook.presto.spi.ConnectorHandleResolver; +import com.facebook.presto.spi.ConnectorMetadata; +import com.facebook.presto.spi.ConnectorPageSourceProvider; +import com.facebook.presto.spi.ConnectorRecordSinkProvider; +import com.facebook.presto.spi.ConnectorSplitManager; +import com.facebook.presto.spi.SystemTable; +import com.google.common.collect.ImmutableSet; +import io.airlift.bootstrap.LifeCycleManager; +import io.airlift.log.Logger; + +import java.util.Set; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class HiveConnector + implements Connector +{ + private static final Logger log = Logger.get(HiveConnector.class); + + private final LifeCycleManager lifeCycleManager; + private final ConnectorMetadata metadata; + private final ConnectorSplitManager splitManager; + private final ConnectorPageSourceProvider pageSourceProvider; + private final ConnectorRecordSinkProvider recordSinkProvider; + private final ConnectorHandleResolver handleResolver; + private final Set systemTables; + + public HiveConnector( + LifeCycleManager lifeCycleManager, + ConnectorMetadata metadata, + ConnectorSplitManager splitManager, + ConnectorPageSourceProvider pageSourceProvider, + ConnectorRecordSinkProvider recordSinkProvider, + ConnectorHandleResolver handleResolver, + Set systemTables) + { + this.lifeCycleManager = checkNotNull(lifeCycleManager, "lifeCycleManager is null"); + this.metadata = checkNotNull(metadata, "metadata is null"); + this.splitManager = checkNotNull(splitManager, "splitManager is null"); + this.pageSourceProvider = checkNotNull(pageSourceProvider, "pageSourceProvider is null"); + this.recordSinkProvider = checkNotNull(recordSinkProvider, "recordSinkProvider is null"); + this.handleResolver = checkNotNull(handleResolver, "handleResolver is null"); + this.systemTables = ImmutableSet.copyOf(checkNotNull(systemTables, "systemTables is null")); + } + + @Override + public ConnectorMetadata getMetadata() + { + return metadata; + } + + @Override + public ConnectorSplitManager getSplitManager() + { + return splitManager; + } + + @Override + public ConnectorPageSourceProvider getPageSourceProvider() + { + return pageSourceProvider; + } + + @Override + public ConnectorRecordSinkProvider getRecordSinkProvider() + { + return recordSinkProvider; + } + + @Override + public ConnectorHandleResolver getHandleResolver() + { + return handleResolver; + } + + @Override + public Set getSystemTables() + { + return systemTables; + } + + @Override + public final void shutdown() + { + try { + lifeCycleManager.stop(); + } + catch (Exception e) { + log.error(e, "Error shutting down connector"); + } + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/HiveConnectorFactory.java b/presto-hive/src/main/java/com/facebook/presto/hive/HiveConnectorFactory.java new file mode 100644 index 00000000..1d796e9b --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/HiveConnectorFactory.java @@ -0,0 +1,127 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.hive.metastore.HiveMetastore; +import com.facebook.presto.spi.Connector; +import com.facebook.presto.spi.ConnectorFactory; +import com.facebook.presto.spi.ConnectorHandleResolver; +import com.facebook.presto.spi.ConnectorMetadata; +import com.facebook.presto.spi.ConnectorPageSourceProvider; +import com.facebook.presto.spi.ConnectorRecordSinkProvider; +import com.facebook.presto.spi.ConnectorSplitManager; +import com.facebook.presto.spi.classloader.ClassLoaderSafeConnectorHandleResolver; +import com.facebook.presto.spi.classloader.ClassLoaderSafeConnectorMetadata; +import com.facebook.presto.spi.classloader.ClassLoaderSafeConnectorPageSourceProvider; +import com.facebook.presto.spi.classloader.ClassLoaderSafeConnectorRecordSinkProvider; +import com.facebook.presto.spi.classloader.ClassLoaderSafeConnectorSplitManager; +import com.facebook.presto.spi.classloader.ThreadContextClassLoader; +import com.facebook.presto.spi.type.TypeManager; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableSet; +import com.google.inject.Binder; +import com.google.inject.Injector; +import com.google.inject.Module; +import io.airlift.bootstrap.Bootstrap; +import io.airlift.bootstrap.LifeCycleManager; +import io.airlift.discovery.client.DiscoveryModule; +import io.airlift.json.JsonModule; +import io.airlift.node.NodeModule; +import org.weakref.jmx.guice.MBeanModule; + +import javax.management.MBeanServer; + +import java.lang.management.ManagementFactory; +import java.util.Map; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Strings.isNullOrEmpty; + +public class HiveConnectorFactory + implements ConnectorFactory +{ + private final String name; + private final Map optionalConfig; + private final ClassLoader classLoader; + private final HiveMetastore metastore; + private final TypeManager typeManager; + + public HiveConnectorFactory(String name, Map optionalConfig, ClassLoader classLoader, HiveMetastore metastore, TypeManager typeManager) + { + checkArgument(!isNullOrEmpty(name), "name is null or empty"); + this.name = name; + this.optionalConfig = checkNotNull(optionalConfig, "optionalConfig is null"); + this.classLoader = checkNotNull(classLoader, "classLoader is null"); + this.metastore = metastore; + this.typeManager = checkNotNull(typeManager, "typeManager is null"); + } + + @Override + public String getName() + { + return name; + } + + @Override + public Connector create(String connectorId, Map config) + { + checkNotNull(config, "config is null"); + + try (ThreadContextClassLoader ignored = new ThreadContextClassLoader(classLoader)) { + Bootstrap app = new Bootstrap( + new NodeModule(), + new DiscoveryModule(), + new MBeanModule(), + new JsonModule(), + new HiveClientModule(connectorId, metastore, typeManager), + new Module() + { + @Override + public void configure(Binder binder) + { + MBeanServer platformMBeanServer = ManagementFactory.getPlatformMBeanServer(); + binder.bind(MBeanServer.class).toInstance(new RebindSafeMBeanServer(platformMBeanServer)); + } + } + ); + + Injector injector = app + .strictConfig() + .doNotInitializeLogging() + .setRequiredConfigurationProperties(config) + .setOptionalConfigurationProperties(optionalConfig) + .initialize(); + + LifeCycleManager lifeCycleManager = injector.getInstance(LifeCycleManager.class); + ConnectorMetadata metadata = injector.getInstance(ConnectorMetadata.class); + ConnectorSplitManager splitManager = injector.getInstance(ConnectorSplitManager.class); + ConnectorPageSourceProvider connectorPageSource = injector.getInstance(ConnectorPageSourceProvider.class); + ConnectorRecordSinkProvider recordSinkProvider = injector.getInstance(ConnectorRecordSinkProvider.class); + ConnectorHandleResolver handleResolver = injector.getInstance(ConnectorHandleResolver.class); + + return new HiveConnector( + lifeCycleManager, + new ClassLoaderSafeConnectorMetadata(metadata, classLoader), + new ClassLoaderSafeConnectorSplitManager(splitManager, classLoader), + new ClassLoaderSafeConnectorPageSourceProvider(connectorPageSource, classLoader), + new ClassLoaderSafeConnectorRecordSinkProvider(recordSinkProvider, classLoader), + new ClassLoaderSafeConnectorHandleResolver(handleResolver, classLoader), + ImmutableSet.of()); + } + catch (Exception e) { + throw Throwables.propagate(e); + } + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/HiveConnectorId.java b/presto-hive/src/main/java/com/facebook/presto/hive/HiveConnectorId.java new file mode 100644 index 00000000..a7d8815f --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/HiveConnectorId.java @@ -0,0 +1,56 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import java.util.Objects; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class HiveConnectorId +{ + private final String connectorId; + + public HiveConnectorId(String connectorId) + { + checkNotNull(connectorId, "connectorId is null"); + checkArgument(!connectorId.isEmpty(), "connectorId is empty"); + this.connectorId = connectorId; + } + + @Override + public int hashCode() + { + return Objects.hash(connectorId); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + HiveConnectorId other = (HiveConnectorId) obj; + return Objects.equals(this.connectorId, other.connectorId); + } + + @Override + public String toString() + { + return connectorId; + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/HiveErrorCode.java b/presto-hive/src/main/java/com/facebook/presto/hive/HiveErrorCode.java new file mode 100644 index 00000000..699b9e1a --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/HiveErrorCode.java @@ -0,0 +1,54 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.spi.ErrorCode; +import com.facebook.presto.spi.ErrorCodeSupplier; + +public enum HiveErrorCode + implements ErrorCodeSupplier +{ + // Connectors can use error codes starting at EXTERNAL + HIVE_METASTORE_ERROR(0x0100_0000), + HIVE_CURSOR_ERROR(0x0100_0001), + HIVE_TABLE_OFFLINE(0x0100_0002), + HIVE_CANNOT_OPEN_SPLIT(0x0100_0003), + HIVE_FILE_NOT_FOUND(0x0100_0004), + HIVE_UNKNOWN_ERROR(0x0100_0005), + HIVE_PARTITION_OFFLINE(0x0100_0006), + HIVE_BAD_DATA(0x0100_0007), + HIVE_PARTITION_SCHEMA_MISMATCH(0x0100_0008), + HIVE_MISSING_DATA(0x0100_0009), + HIVE_INVALID_PARTITION_VALUE(0x0100_000A), + HIVE_TIMEZONE_MISMATCH(0x0100_000B), + HIVE_INVALID_METADATA(0x0100_000C), + HIVE_INVALID_VIEW_DATA(0x0100_000D), + HIVE_DATABASE_LOCATION_ERROR(0x0100_000E), + HIVE_PATH_ALREADY_EXISTS(0x0100_000F), + HIVE_FILESYSTEM_ERROR(0x0100_0010), + HIVE_WRITER_ERROR(0x0100_0011); + + private final ErrorCode errorCode; + + HiveErrorCode(int code) + { + errorCode = new ErrorCode(code, name()); + } + + @Override + public ErrorCode toErrorCode() + { + return errorCode; + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/HiveHandleResolver.java b/presto-hive/src/main/java/com/facebook/presto/hive/HiveHandleResolver.java new file mode 100644 index 00000000..9557cadc --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/HiveHandleResolver.java @@ -0,0 +1,100 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorHandleResolver; +import com.facebook.presto.spi.ConnectorInsertTableHandle; +import com.facebook.presto.spi.ConnectorOutputTableHandle; +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.ConnectorTableHandle; + +import javax.inject.Inject; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class HiveHandleResolver + implements ConnectorHandleResolver +{ + private final String connectorId; + + @Inject + public HiveHandleResolver(HiveConnectorId connectorId) + { + this.connectorId = checkNotNull(connectorId, "connectorId is null").toString(); + } + + @Override + public boolean canHandle(ConnectorTableHandle tableHandle) + { + return (tableHandle instanceof HiveTableHandle) && ((HiveTableHandle) tableHandle).getClientId().equals(connectorId); + } + + @Override + public boolean canHandle(ColumnHandle columnHandle) + { + return (columnHandle instanceof HiveColumnHandle) && ((HiveColumnHandle) columnHandle).getClientId().equals(connectorId); + } + + @Override + public boolean canHandle(ConnectorSplit split) + { + return (split instanceof HiveSplit) && ((HiveSplit) split).getClientId().equals(connectorId); + } + + @Override + public boolean canHandle(ConnectorOutputTableHandle handle) + { + return (handle instanceof HiveOutputTableHandle) && ((HiveOutputTableHandle) handle).getClientId().equals(connectorId); + } + + @Override + public boolean canHandle(ConnectorInsertTableHandle tableHandle) + { + boolean res = tableHandle instanceof HiveInsertTableHandle + && ((HiveInsertTableHandle) tableHandle).getClientId().equals( + connectorId); + return res; + } + + @Override + public Class getTableHandleClass() + { + return HiveTableHandle.class; + } + + @Override + public Class getColumnHandleClass() + { + return HiveColumnHandle.class; + } + + @Override + public Class getSplitClass() + { + return HiveSplit.class; + } + + @Override + public Class getOutputTableHandleClass() + { + return HiveOutputTableHandle.class; + } + + @Override + public Class getInsertTableHandleClass() + { + return HiveInsertTableHandle.class; + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/HiveHdfsConfiguration.java b/presto-hive/src/main/java/com/facebook/presto/hive/HiveHdfsConfiguration.java new file mode 100644 index 00000000..071e4c7c --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/HiveHdfsConfiguration.java @@ -0,0 +1,53 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import org.apache.hadoop.conf.Configuration; + +import javax.inject.Inject; + +import java.net.URI; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class HiveHdfsConfiguration + implements HdfsConfiguration +{ + @SuppressWarnings("ThreadLocalNotStaticFinal") + private final ThreadLocal hadoopConfiguration = new ThreadLocal() + { + @Override + protected Configuration initialValue() + { + Configuration config = new Configuration(); + updater.updateConfiguration(config); + return config; + } + }; + + private final HdfsConfigurationUpdater updater; + + @Inject + public HiveHdfsConfiguration(HdfsConfigurationUpdater updater) + { + this.updater = checkNotNull(updater, "updater is null"); + } + + @Override + public Configuration getConfiguration(URI uri) + { + // use the same configuration for everything + return hadoopConfiguration.get(); + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/HiveInsertTableHandle.java b/presto-hive/src/main/java/com/facebook/presto/hive/HiveInsertTableHandle.java new file mode 100644 index 00000000..04f74c14 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/HiveInsertTableHandle.java @@ -0,0 +1,240 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import com.facebook.presto.spi.ConnectorInsertTableHandle; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.type.Type; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class HiveInsertTableHandle implements ConnectorInsertTableHandle +{ + private final String clientId; + private final String schemaName; + private final String tableName; + private final List columnNames; + private final List columnTypes; + private final String targetPath; + private final String temporaryPath; + private final String outputFormat; + private final String serdeLib; + private final Map serdeParameters; + private final List partitionBitmap; + private final String filePrefix; + private final ConnectorSession session; + private final PartitionOption partitionOption; + private final boolean overwrite; + @JsonCreator + public HiveInsertTableHandle( + @JsonProperty("clientId") String clientId, + @JsonProperty("schemaName") String schemaName, + @JsonProperty("tableName") String tableName, + @JsonProperty("columnNames") List columnNames, + @JsonProperty("columnTypes") List columnTypes, + @JsonProperty("targetPath") String targetPath, + @JsonProperty("temporaryPath") String temporaryPath, + @JsonProperty("outputFormat") String outputFormat, + @JsonProperty("serdeLib") String serdeLib, + @JsonProperty("serdeParameters") Map serdeParameters, + @JsonProperty("partitionBitmap") List partitionBitmap, + @JsonProperty("filePrefix") String filePrefix, + @JsonProperty("connectorSession") ConnectorSession session, + @JsonProperty("overwrite") boolean overwrite, + @JsonProperty("partitionOption") PartitionOption partitionOption) + { + this.schemaName = schemaName; + this.tableName = tableName; + this.targetPath = checkNotNull(targetPath, "targetPath is null"); + this.temporaryPath = checkNotNull(temporaryPath, "temporaryPath is null"); + this.clientId = checkNotNull(clientId, "clientId is null"); + + checkNotNull(columnNames, "columnNames is null"); + checkNotNull(columnTypes, "columnTypes is null"); + checkArgument(columnNames.size() == columnTypes.size(), "columnNames and columnTypes sizes don't match"); + this.columnNames = ImmutableList.copyOf(columnNames); + this.columnTypes = ImmutableList.copyOf(columnTypes); + + this.outputFormat = outputFormat; + this.serdeLib = serdeLib; + this.serdeParameters = serdeParameters; + this.partitionBitmap = partitionBitmap; + this.filePrefix = filePrefix; + this.session = session; + this.overwrite = overwrite; + this.partitionOption = checkNotNull(partitionOption, "partitionOption is null"); + } + + @JsonProperty + public String getClientId() + { + return clientId; + } + + @JsonProperty + public String getSchemaName() + { + return schemaName; + } + + @JsonProperty + public String getTableName() + { + return tableName; + } + + @JsonProperty + public List getColumnNames() + { + return columnNames; + } + + @JsonProperty + public List getColumnTypes() + { + return columnTypes; + } + + @JsonProperty + public String getTargetPath() + { + return targetPath; + } + + @JsonProperty + public String getTemporaryPath() + { + return temporaryPath; + } + + @JsonProperty + public String getOutputFormat() + { + return outputFormat; + } + + @JsonProperty + public List getPartitionBitmap() + { + return partitionBitmap; + } + + @JsonProperty + public String getFilePrefix() + { + return filePrefix; + } + + @JsonProperty + public String getSerdeLib() + { + return serdeLib; + } + + @JsonProperty + public Map getSerdeParameters() + { + return serdeParameters; + } + + @JsonProperty + public ConnectorSession getConnectorSession() + { + return session; + } + + @Override + public String toString() + { + return "Writing to: " + targetPath + " with Prefix: " + filePrefix; + } + + public boolean hasTemporaryPath() + { + return !temporaryPath.equals(targetPath); + } + + public boolean isOutputTablePartitioned() + { + return partitionBitmap != null; + } + + public List getDataColumnTypes() + { + if (!isOutputTablePartitioned()) { + return ImmutableList.copyOf(columnTypes); + } + + List dataColumnTypes = new ArrayList(); + for (int i = 0; i < columnTypes.size(); i++) { + if (!partitionBitmap.get(i)) { + dataColumnTypes.add(columnTypes.get(i)); + } + } + + return dataColumnTypes; + } + + public List getDataColumnNames() + { + if (!isOutputTablePartitioned()) { + return columnNames; + } + + List dataColumnNames = new ArrayList(); + for (int i = 0; i < columnNames.size(); i++) { + if (!partitionBitmap.get(i)) { + dataColumnNames.add(columnNames.get(i)); + } + } + + return dataColumnNames; + } + + public List getPartitionColumnNames() + { + if (!isOutputTablePartitioned()) { + return null; + } + + List partitionColumnNames = new ArrayList(); + for (int i = 0; i < columnNames.size(); i++) { + if (partitionBitmap.get(i)) { + partitionColumnNames.add(columnNames.get(i)); + } + } + + return partitionColumnNames; + } + + @JsonProperty + public boolean isOverwrite() + { + return overwrite; + } + + @JsonProperty + public PartitionOption getPartitionOption() + { + return partitionOption; + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/HiveMetadata.java b/presto-hive/src/main/java/com/facebook/presto/hive/HiveMetadata.java new file mode 100644 index 00000000..2eb8670d --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/HiveMetadata.java @@ -0,0 +1,1191 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.hive.metastore.HiveMetastore; +import com.facebook.presto.spi.ColumnMetadata; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorInsertTableHandle; +import com.facebook.presto.spi.ConnectorMetadata; +import com.facebook.presto.spi.ConnectorOutputTableHandle; +import com.facebook.presto.spi.ConnectorPartition; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.ConnectorSplitManager; +import com.facebook.presto.spi.ConnectorTableHandle; +import com.facebook.presto.spi.ConnectorTableMetadata; +import com.facebook.presto.spi.InsertOption; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.SchemaNotFoundException; +import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.spi.SchemaTablePrefix; +import com.facebook.presto.spi.TableNotFoundException; +import com.facebook.presto.spi.TupleDomain; +import com.facebook.presto.spi.ViewNotFoundException; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Function; +import com.google.common.base.Splitter; +import com.google.common.base.StandardSystemProperty; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import io.airlift.log.Logger; +import io.airlift.slice.Slice; +import org.apache.hadoop.fs.FileStatus; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.hive.common.FileUtils; +import org.apache.hadoop.hive.metastore.TableType; +import org.apache.hadoop.hive.metastore.api.Database; +import org.apache.hadoop.hive.metastore.api.FieldSchema; +import org.apache.hadoop.hive.metastore.api.NoSuchObjectException; +import org.apache.hadoop.hive.metastore.api.Partition; +import org.apache.hadoop.hive.metastore.api.SerDeInfo; +import org.apache.hadoop.hive.metastore.api.StorageDescriptor; +import org.apache.hadoop.hive.metastore.api.Table; +import org.apache.thrift.TException; +import org.joda.time.DateTimeZone; + +import javax.inject.Inject; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static com.facebook.presto.hive.HiveColumnHandle.SAMPLE_WEIGHT_COLUMN_NAME; +import static com.facebook.presto.hive.HiveErrorCode.HIVE_DATABASE_LOCATION_ERROR; +import static com.facebook.presto.hive.HiveErrorCode.HIVE_FILESYSTEM_ERROR; +import static com.facebook.presto.hive.HiveErrorCode.HIVE_PATH_ALREADY_EXISTS; +import static com.facebook.presto.hive.HiveErrorCode.HIVE_TIMEZONE_MISMATCH; +import static com.facebook.presto.hive.HiveSessionProperties.getHiveStorageFormat; +import static com.facebook.presto.hive.HiveUtil.PRESTO_VIEW_FLAG; +import static com.facebook.presto.hive.HiveUtil.decodeViewData; +import static com.facebook.presto.hive.HiveUtil.encodeViewData; +import static com.facebook.presto.hive.HiveUtil.hiveColumnHandles; +import static com.facebook.presto.hive.HiveUtil.schemaTableName; +import static com.facebook.presto.hive.util.Types.checkType; +import static com.facebook.presto.spi.StandardErrorCode.EXTERNAL; +import static com.facebook.presto.spi.StandardErrorCode.NOT_SUPPORTED; +import static com.facebook.presto.spi.StandardErrorCode.PERMISSION_DENIED; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Strings.isNullOrEmpty; +import static com.google.common.collect.Iterables.concat; +import static com.google.common.collect.Iterables.transform; +import static java.lang.String.format; +import static java.util.UUID.randomUUID; +import static java.util.stream.Collectors.toList; +import static org.apache.hadoop.hive.serde.serdeConstants.STRING_TYPE_NAME; + +public class HiveMetadata + implements ConnectorMetadata +{ + private static final Logger log = Logger.get(HiveMetadata.class); + public static final String PRESTO_OFFLINE = "presto_offline"; + private final String connectorId; + private final boolean allowDropTable; + private final boolean allowRenameTable; + private final boolean allowCorruptWritesForTesting; + private final HiveMetastore metastore; + private final HdfsEnvironment hdfsEnvironment; + private final DateTimeZone timeZone; + private final HiveStorageFormat hiveStorageFormat; + private final TypeManager typeManager; + + private final HiveSplitManager splitManager; + private static final int renameThreadPoolSize = 20; + private static final int partionCommitBatchSize = 8; + + @Inject + @SuppressWarnings("deprecation") + public HiveMetadata( + HiveConnectorId connectorId, + HiveClientConfig hiveClientConfig, + HiveMetastore metastore, + HdfsEnvironment hdfsEnvironment, + @ForHiveClient ExecutorService executorService, + TypeManager typeManager, + ConnectorSplitManager splitManager) + { + this(connectorId, + metastore, + hdfsEnvironment, + DateTimeZone.forTimeZone(hiveClientConfig.getTimeZone()), + hiveClientConfig.getAllowDropTable(), + hiveClientConfig.getAllowRenameTable(), + hiveClientConfig.getAllowCorruptWritesForTesting(), + hiveClientConfig.getHiveStorageFormat(), + typeManager, + splitManager); + } + + public HiveMetadata( + HiveConnectorId connectorId, + HiveMetastore metastore, + HdfsEnvironment hdfsEnvironment, + DateTimeZone timeZone, + boolean allowDropTable, + boolean allowRenameTable, + boolean allowCorruptWritesForTesting, + HiveStorageFormat hiveStorageFormat, + TypeManager typeManager, + ConnectorSplitManager splitManager) + { + this.connectorId = checkNotNull(connectorId, "connectorId is null").toString(); + + this.allowDropTable = allowDropTable; + this.allowRenameTable = allowRenameTable; + this.allowCorruptWritesForTesting = allowCorruptWritesForTesting; + + this.metastore = checkNotNull(metastore, "metastore is null"); + this.hdfsEnvironment = checkNotNull(hdfsEnvironment, "hdfsEnvironment is null"); + this.timeZone = checkNotNull(timeZone, "timeZone is null"); + this.hiveStorageFormat = hiveStorageFormat; + this.typeManager = checkNotNull(typeManager, "typeManager is null"); + + if (!allowCorruptWritesForTesting && !timeZone.equals(DateTimeZone.getDefault())) { + log.warn("Hive writes are disabled. " + + "To write data to Hive, your JVM timezone must match the Hive storage timezone. " + + "Add -Duser.timezone=%s to your JVM arguments", + timeZone.getID()); + } + this.splitManager = checkType(splitManager, HiveSplitManager.class, "splitManager"); + } + + public HiveMetastore getMetastore() + { + return metastore; + } + + @Override + public List listSchemaNames(ConnectorSession session) + { + return metastore.getAllDatabases(); + } + + @Override + public HiveTableHandle getTableHandle(ConnectorSession session, SchemaTableName tableName) + { + checkNotNull(tableName, "tableName is null"); + try { + metastore.getTable(tableName.getSchemaName(), tableName.getTableName()); + return new HiveTableHandle(connectorId, tableName.getSchemaName(), tableName.getTableName(), session); + } + catch (NoSuchObjectException e) { + // table was not found + return null; + } + } + + @Override + public ConnectorTableMetadata getTableMetadata(ConnectorTableHandle tableHandle) + { + checkNotNull(tableHandle, "tableHandle is null"); + SchemaTableName tableName = schemaTableName(tableHandle); + return getTableMetadata(tableName); + } + + private ConnectorTableMetadata getTableMetadata(SchemaTableName tableName) + { + try { + Table table = metastore.getTable(tableName.getSchemaName(), tableName.getTableName()); + if (table.getTableType().equals(TableType.VIRTUAL_VIEW.name())) { + throw new TableNotFoundException(tableName); + } + List handles = hiveColumnHandles(typeManager, connectorId, table, false); + List columns = ImmutableList.copyOf(transform(handles, columnMetadataGetter(table, typeManager))); + return new ConnectorTableMetadata(tableName, columns, table.getOwner()); + } + catch (NoSuchObjectException e) { + throw new TableNotFoundException(tableName); + } + } + + @Override + public List listTables(ConnectorSession session, String schemaNameOrNull) + { + ImmutableList.Builder tableNames = ImmutableList.builder(); + for (String schemaName : listSchemas(session, schemaNameOrNull)) { + try { + for (String tableName : metastore.getAllTables(schemaName)) { + tableNames.add(new SchemaTableName(schemaName, tableName)); + } + } + catch (NoSuchObjectException e) { + // schema disappeared during listing operation + } + } + return tableNames.build(); + } + + private List listSchemas(ConnectorSession session, String schemaNameOrNull) + { + if (schemaNameOrNull == null) { + return listSchemaNames(session); + } + return ImmutableList.of(schemaNameOrNull); + } + + @Override + public ColumnHandle getSampleWeightColumnHandle(ConnectorTableHandle tableHandle) + { + SchemaTableName tableName = schemaTableName(tableHandle); + try { + Table table = metastore.getTable(tableName.getSchemaName(), tableName.getTableName()); + for (HiveColumnHandle columnHandle : hiveColumnHandles(typeManager, connectorId, table, true)) { + if (columnHandle.getName().equals(SAMPLE_WEIGHT_COLUMN_NAME)) { + return columnHandle; + } + } + return null; + } + catch (NoSuchObjectException e) { + throw new TableNotFoundException(tableName); + } + } + + @Override + public boolean canCreateSampledTables(ConnectorSession session) + { + return true; + } + + @Override + public Map getColumnHandles(ConnectorTableHandle tableHandle) + { + SchemaTableName tableName = schemaTableName(tableHandle); + try { + Table table = metastore.getTable(tableName.getSchemaName(), tableName.getTableName()); + ImmutableMap.Builder columnHandles = ImmutableMap.builder(); + for (HiveColumnHandle columnHandle : hiveColumnHandles(typeManager, connectorId, table, false)) { + columnHandles.put(columnHandle.getName(), columnHandle); + } + return columnHandles.build(); + } + catch (NoSuchObjectException e) { + throw new TableNotFoundException(tableName); + } + } + + @Override + public Map> listTableColumns(ConnectorSession session, SchemaTablePrefix prefix) + { + checkNotNull(prefix, "prefix is null"); + ImmutableMap.Builder> columns = ImmutableMap.builder(); + for (SchemaTableName tableName : listTables(session, prefix)) { + try { + columns.put(tableName, getTableMetadata(tableName).getColumns()); + } + catch (HiveViewNotSupportedException e) { + // view is not supported + } + catch (TableNotFoundException e) { + // table disappeared during listing operation + } + } + return columns.build(); + } + + private List listTables(ConnectorSession session, SchemaTablePrefix prefix) + { + if (prefix.getSchemaName() == null || prefix.getTableName() == null) { + return listTables(session, prefix.getSchemaName()); + } + return ImmutableList.of(new SchemaTableName(prefix.getSchemaName(), prefix.getTableName())); + } + + /** + * NOTE: This method does not return column comment + */ + @Override + public ColumnMetadata getColumnMetadata(ConnectorTableHandle tableHandle, ColumnHandle columnHandle) + { + checkType(tableHandle, HiveTableHandle.class, "tableHandle"); + return checkType(columnHandle, HiveColumnHandle.class, "columnHandle").getColumnMetadata(typeManager); + } + + @Override + public void createTable(ConnectorSession session, ConnectorTableMetadata tableMetadata) + { + checkArgument(!isNullOrEmpty(tableMetadata.getOwner()), "Table owner is null or empty"); + + SchemaTableName schemaTableName = tableMetadata.getTable(); + String schemaName = schemaTableName.getSchemaName(); + String tableName = schemaTableName.getTableName(); + + ImmutableList.Builder columnNames = ImmutableList.builder(); + ImmutableList.Builder columnTypes = ImmutableList.builder(); + + buildColumnInfo(tableMetadata, columnNames, columnTypes); + + ImmutableList.Builder partitionKeys = ImmutableList.builder(); + ImmutableList.Builder columns = ImmutableList.builder(); + + List names = columnNames.build(); + List typeNames = columnTypes.build().stream() + .map(HiveType::toHiveType) + .map(HiveType::getHiveTypeName) + .collect(toList()); + + for (int i = 0; i < names.size(); i++) { + if (tableMetadata.getColumns().get(i).isPartitionKey()) { + partitionKeys.add(new FieldSchema(names.get(i), typeNames.get(i), tableMetadata.getColumns().get(i).getComment())); + } + else { + columns.add(new FieldSchema(names.get(i), typeNames.get(i), tableMetadata.getColumns().get(i).getComment())); + } + } + + Path targetPath = getTargetPath(schemaName, tableName, schemaTableName); + + HiveStorageFormat hiveStorageFormat = getHiveStorageFormat(session, this.hiveStorageFormat); + SerDeInfo serdeInfo = new SerDeInfo(); + serdeInfo.setName(tableName); + serdeInfo.setSerializationLib(hiveStorageFormat.getSerDe()); + + StorageDescriptor sd = new StorageDescriptor(); + sd.setLocation(targetPath.toString()); + + sd.setCols(columns.build()); + sd.setSerdeInfo(serdeInfo); + sd.setInputFormat(hiveStorageFormat.getInputFormat()); + sd.setOutputFormat(hiveStorageFormat.getOutputFormat()); + + Table table = new Table(); + table.setDbName(schemaName); + table.setTableName(tableName); + table.setOwner(tableMetadata.getOwner()); + table.setTableType(TableType.MANAGED_TABLE.toString()); + String tableComment = "Created by Presto"; + table.setParameters(ImmutableMap.of("comment", tableComment)); + table.setPartitionKeys(partitionKeys.build()); + table.setSd(sd); + + metastore.createTable(table); + } + + @Override + public void renameTable(ConnectorTableHandle tableHandle, SchemaTableName newTableName) + { + if (!allowRenameTable) { + throw new PrestoException(PERMISSION_DENIED, "Renaming tables is disabled in this Hive catalog"); + } + + HiveTableHandle handle = checkType(tableHandle, HiveTableHandle.class, "tableHandle"); + metastore.renameTable(handle.getSchemaName(), handle.getTableName(), newTableName.getSchemaName(), newTableName.getTableName()); + } + + @Override + public void dropTable(ConnectorTableHandle tableHandle) + { + HiveTableHandle handle = checkType(tableHandle, HiveTableHandle.class, "tableHandle"); + SchemaTableName tableName = schemaTableName(tableHandle); + + if (!allowDropTable) { + throw new PrestoException(PERMISSION_DENIED, "DROP TABLE is disabled in this Hive catalog"); + } + + try { + Table table = metastore.getTable(handle.getSchemaName(), handle.getTableName()); + if (!handle.getSession().getUser().equals(table.getOwner())) { + throw new PrestoException(PERMISSION_DENIED, format("Unable to drop table '%s': owner of the table is different from session user", table)); + } + metastore.dropTable(handle.getSchemaName(), handle.getTableName()); + } + catch (NoSuchObjectException e) { + throw new TableNotFoundException(tableName); + } + } + + @Override + public HiveOutputTableHandle beginCreateTable(ConnectorSession session, ConnectorTableMetadata tableMetadata) + { + verifyJvmTimeZone(); + + checkArgument(!isNullOrEmpty(tableMetadata.getOwner()), "Table owner is null or empty"); + + HiveStorageFormat hiveStorageFormat = getHiveStorageFormat(session, this.hiveStorageFormat); + + ImmutableList.Builder columnNames = ImmutableList.builder(); + ImmutableList.Builder columnTypes = ImmutableList.builder(); + List columnPartitions = tableMetadata.getColumns().stream() + .map(ColumnMetadata::isPartitionKey) + .collect(toList()); + + // get the root directory for the database + SchemaTableName schemaTableName = tableMetadata.getTable(); + String schemaName = schemaTableName.getSchemaName(); + String tableName = schemaTableName.getTableName(); + + buildColumnInfo(tableMetadata, columnNames, columnTypes); + + Path targetPath = getTargetPath(schemaName, tableName, schemaTableName); + + if (!useTemporaryDirectory(targetPath)) { + return new HiveOutputTableHandle( + connectorId, + schemaName, + tableName, + columnNames.build(), + columnTypes.build(), + columnPartitions, + tableMetadata.getOwner(), + targetPath.toString(), + targetPath.toString(), + session, + hiveStorageFormat); + } + + // use a per-user temporary directory to avoid permission problems + // TODO: this should use Hadoop UserGroupInformation + String temporaryPrefix = "/tmp/presto-" + StandardSystemProperty.USER_NAME.value(); + + // create a temporary directory on the same filesystem + Path temporaryRoot = new Path(targetPath, temporaryPrefix); + Path temporaryPath = new Path(temporaryRoot, randomUUID().toString()); + createDirectories(temporaryPath); + + return new HiveOutputTableHandle( + connectorId, + schemaName, + tableName, + columnNames.build(), + columnTypes.build(), + columnPartitions, + tableMetadata.getOwner(), + targetPath.toString(), + temporaryPath.toString(), + session, + hiveStorageFormat); + } + + @Override + public void commitCreateTable(ConnectorOutputTableHandle tableHandle, Collection fragments) + { + HiveOutputTableHandle handle = checkType(tableHandle, HiveOutputTableHandle.class, "tableHandle"); + + // verify no one raced us to create the target directory + Path targetPath = new Path(handle.getTargetPath()); + + // rename if using a temporary directory + if (handle.hasTemporaryPath()) { + if (pathExists(targetPath)) { + SchemaTableName table = new SchemaTableName(handle.getSchemaName(), handle.getTableName()); + throw new PrestoException(HIVE_PATH_ALREADY_EXISTS, format("Unable to commit creation of table '%s': target directory already exists: %s", table, targetPath)); + } + // rename the temporary directory to the target + rename(new Path(handle.getTemporaryPath()), targetPath); + } + + // create the table in the metastore + List types = handle.getColumnTypes().stream() + .map(HiveType::toHiveType) + .map(HiveType::getHiveTypeName) + .collect(toList()); + + boolean sampled = false; + ImmutableList.Builder columns = ImmutableList.builder(); + ImmutableList.Builder partitionedColumns = ImmutableList.builder(); + for (int i = 0; i < handle.getColumnNames().size(); i++) { + String name = handle.getColumnNames().get(i); + String type = types.get(i); + if (name.equals(SAMPLE_WEIGHT_COLUMN_NAME)) { + if (handle.getColumnPartitioned().get(i)) { + partitionedColumns.add(new FieldSchema(name, type, "Presto sample weight column")); + } + else { + columns.add(new FieldSchema(name, type, "Presto sample weight column")); + sampled = true; + } + } + else { + // specify the output table partition columns + if (handle.getColumnPartitioned().get(i)) { + partitionedColumns.add(new FieldSchema(name, type, null)); + } + else { + columns.add(new FieldSchema(name, type, null)); + } + } + } + + HiveStorageFormat hiveStorageFormat = handle.getHiveStorageFormat(); + + SerDeInfo serdeInfo = new SerDeInfo(); + serdeInfo.setName(handle.getTableName()); + serdeInfo.setSerializationLib(hiveStorageFormat.getSerDe()); + serdeInfo.setParameters(ImmutableMap.of()); + + StorageDescriptor sd = new StorageDescriptor(); + sd.setLocation(targetPath.toString()); + sd.setCols(columns.build()); + sd.setSerdeInfo(serdeInfo); + sd.setInputFormat(hiveStorageFormat.getInputFormat()); + sd.setOutputFormat(hiveStorageFormat.getOutputFormat()); + sd.setParameters(ImmutableMap.of()); + + Table table = new Table(); + table.setDbName(handle.getSchemaName()); + table.setTableName(handle.getTableName()); + table.setOwner(handle.getTableOwner()); + table.setTableType(TableType.MANAGED_TABLE.toString()); + String tableComment = "Created by Presto"; + if (sampled) { + tableComment = "Sampled table created by Presto. Only query this table from Hive if you understand how Presto implements sampling."; + } + table.setParameters(ImmutableMap.of("comment", tableComment)); + table.setPartitionKeys(partitionedColumns.build()); + table.setSd(sd); + + metastore.createTable(table); + + // if output table is partitioned, create partition in hive metastore + if (handle.isOutputTablePartitioned()) { + Map> filesWritten = new HashMap>(); + try { + filesWritten = getFilesWritten(fragments, handle.getTableName()); + addDynamicPartitions(filesWritten.keySet(), + null, + handle.getTargetPath(), + handle.getSchemaName(), + handle.getTableName()); + } + catch (Exception e) { + String filePrefix = handle.getTemporaryPath().replace("/tmp/presto-" + StandardSystemProperty.USER_NAME.value(), ""); + rollbackInsertCreateChanges(handle.getTargetPath(), handle.isOutputTablePartitioned(), filePrefix, filesWritten); + e.printStackTrace(); + } + } + } + + private Path getTargetPath(String schemaName, String tableName, SchemaTableName schemaTableName) + { + String location = getDatabase(schemaName).getLocationUri(); + if (isNullOrEmpty(location)) { + throw new PrestoException(HIVE_DATABASE_LOCATION_ERROR, format("Database '%s' location is not set", schemaName)); + } + + Path databasePath = new Path(location); + if (!pathExists(databasePath)) { + throw new PrestoException(HIVE_DATABASE_LOCATION_ERROR, format("Database '%s' location does not exist: %s", schemaName, databasePath)); + } + if (!isDirectory(databasePath)) { + throw new PrestoException(HIVE_DATABASE_LOCATION_ERROR, format("Database '%s' location is not a directory: %s", schemaName, databasePath)); + } + + // verify the target directory for the table + Path targetPath = new Path(databasePath, tableName); + if (pathExists(targetPath)) { + throw new PrestoException(HIVE_PATH_ALREADY_EXISTS, format("Target directory for table '%s' already exists: %s", schemaTableName, targetPath)); + } + return targetPath; + } + + private Database getDatabase(String database) + { + try { + return metastore.getDatabase(database); + } + catch (NoSuchObjectException e) { + throw new SchemaNotFoundException(database); + } + } + + private boolean useTemporaryDirectory(Path path) + { + try { + // skip using temporary directory for S3 + return !(hdfsEnvironment.getFileSystem(path) instanceof PrestoS3FileSystem); + } + catch (IOException e) { + throw new PrestoException(HIVE_FILESYSTEM_ERROR, "Failed checking path: " + path, e); + } + } + + private boolean pathExists(Path path) + { + try { + return hdfsEnvironment.getFileSystem(path).exists(path); + } + catch (IOException e) { + throw new PrestoException(HIVE_FILESYSTEM_ERROR, "Failed checking path: " + path, e); + } + } + + private boolean isDirectory(Path path) + { + try { + return hdfsEnvironment.getFileSystem(path).isDirectory(path); + } + catch (IOException e) { + throw new PrestoException(HIVE_FILESYSTEM_ERROR, "Failed checking path: " + path, e); + } + } + + private void createDirectories(Path path) + { + try { + if (!hdfsEnvironment.getFileSystem(path).mkdirs(path)) { + throw new IOException("mkdirs returned false"); + } + } + catch (IOException e) { + throw new PrestoException(HIVE_FILESYSTEM_ERROR, "Failed to create directory: " + path, e); + } + } + + private void rename(Path source, Path target) + { + try { + if (!hdfsEnvironment.getFileSystem(source).rename(source, target)) { + throw new IOException("rename returned false"); + } + } + catch (IOException e) { + throw new PrestoException(HIVE_FILESYSTEM_ERROR, format("Failed to rename %s to %s", source, target), e); + } + } + + @Override + public void createView(ConnectorSession session, SchemaTableName viewName, String viewData, boolean replace) + { + if (replace) { + try { + dropView(session, viewName); + } + catch (ViewNotFoundException ignored) { + } + } + + Map properties = ImmutableMap.builder() + .put("comment", "Presto View") + .put(PRESTO_VIEW_FLAG, "true") + .build(); + + FieldSchema dummyColumn = new FieldSchema("dummy", STRING_TYPE_NAME, null); + + StorageDescriptor sd = new StorageDescriptor(); + sd.setCols(ImmutableList.of(dummyColumn)); + sd.setSerdeInfo(new SerDeInfo()); + + Table table = new Table(); + table.setDbName(viewName.getSchemaName()); + table.setTableName(viewName.getTableName()); + table.setOwner(session.getUser()); + table.setTableType(TableType.VIRTUAL_VIEW.name()); + table.setParameters(properties); + table.setViewOriginalText(encodeViewData(viewData)); + table.setViewExpandedText("/* Presto View */"); + table.setSd(sd); + + try { + metastore.createTable(table); + } + catch (TableAlreadyExistsException e) { + throw new ViewAlreadyExistsException(e.getTableName()); + } + } + + @Override + public void dropView(ConnectorSession session, SchemaTableName viewName) + { + String view = getViews(session, viewName.toSchemaTablePrefix()).get(viewName); + if (view == null) { + throw new ViewNotFoundException(viewName); + } + + try { + metastore.dropTable(viewName.getSchemaName(), viewName.getTableName()); + } + catch (TableNotFoundException e) { + throw new ViewNotFoundException(e.getTableName()); + } + } + + @Override + public List listViews(ConnectorSession session, String schemaNameOrNull) + { + ImmutableList.Builder tableNames = ImmutableList.builder(); + for (String schemaName : listSchemas(session, schemaNameOrNull)) { + try { + for (String tableName : metastore.getAllViews(schemaName)) { + tableNames.add(new SchemaTableName(schemaName, tableName)); + } + } + catch (NoSuchObjectException e) { + // schema disappeared during listing operation + } + } + return tableNames.build(); + } + + @Override + public Map getViews(ConnectorSession session, SchemaTablePrefix prefix) + { + ImmutableMap.Builder views = ImmutableMap.builder(); + List tableNames; + if (prefix.getTableName() != null) { + tableNames = ImmutableList.of(new SchemaTableName(prefix.getSchemaName(), prefix.getTableName())); + } + else { + tableNames = listViews(session, prefix.getSchemaName()); + } + + for (SchemaTableName schemaTableName : tableNames) { + try { + Table table = metastore.getTable(schemaTableName.getSchemaName(), schemaTableName.getTableName()); + if (HiveUtil.isPrestoView(table)) { + views.put(schemaTableName, decodeViewData(table.getViewOriginalText())); + } + } + catch (NoSuchObjectException ignored) { + } + } + + return views.build(); + } + + @Override + public ConnectorInsertTableHandle beginInsert(ConnectorSession session, ConnectorTableHandle tableHandle, InsertOption insertOption) + { + verifyJvmTimeZone(); + + List partitionBitmap = null; + ImmutableList.Builder columnNames = ImmutableList.builder(); + ImmutableList.Builder columnTypes = ImmutableList.builder(); + + // call metastore to get table location, outputFormat, serde, partitions indices + Table table = null; + HiveTableHandle hiveTableHandle = checkType(tableHandle, HiveTableHandle.class, "tableHandle"); + SchemaTableName tableSchemaName = hiveTableHandle.getSchemaTableName(); + try { + table = metastore.getTable(tableSchemaName.getSchemaName(), tableSchemaName.getTableName()); + } + catch (NoSuchObjectException e) { + table = null; + } + + checkNotNull(table, "Table %s does not exist", tableSchemaName.getTableName()); + if (table.getSd().getNumBuckets() > 0) { + throw new UnsupportedOperationException("Insert not supported with Bucketed Tables"); + } + + String outputFormat = table.getSd().getOutputFormat(); + SerDeInfo serdeInfo = table.getSd().getSerdeInfo(); + String serdeLib = serdeInfo.getSerializationLib(); + Map serdeParameters = serdeInfo.getParameters(); + + String location = table.getSd().getLocation(); + ConnectorTableMetadata tableMetadata = getTableMetadata(tableHandle); + if (table.getPartitionKeysSize() != 0) { + partitionBitmap = new ArrayList(tableMetadata.getColumns().size()); + for (ColumnMetadata column : tableMetadata.getColumns()) { + partitionBitmap.add(column.isPartitionKey()); + } + } + if (tableMetadata.isSampled()) { + columnNames.add(SAMPLE_WEIGHT_COLUMN_NAME); + columnTypes.add(BIGINT); + } + for (ColumnMetadata column : tableMetadata.getColumns()) { + columnNames.add(column.getName()); + columnTypes.add(column.getType()); + } + + return buildInsert( + tableSchemaName.getSchemaName(), + tableSchemaName.getTableName(), + location, + columnNames.build(), + columnTypes.build(), + outputFormat, + serdeLib, + serdeParameters, + partitionBitmap, + insertOption, + session); + } + + private ConnectorInsertTableHandle buildInsert(String schemaName, + String tableName, + String location, + List columnNames, + List columnTypes, + String outputFormat, + String serdeLib, + Map serdeParameters, + List partitionBitmap, + InsertOption insertOption, + ConnectorSession session) + { + Path targetPath = new Path(location); + if (!pathExists(targetPath)) { + createDirectories(targetPath); + } + + String tempPath; + String filePrefix = randomUUID().toString(); + + log.info(String.format("Using '%s' as file prefix for insert", filePrefix)); + + if ((useTemporaryDirectory(targetPath))) { + tempPath = createTemporaryPath(targetPath); + } + else { + tempPath = targetPath.toString(); + } + + Map partitionValueList = insertOption.getPartitions(); + String pathName = FileUtils.makePartName(new LinkedList(partitionValueList.keySet()), + new LinkedList(partitionValueList.values())); + PartitionOption partitionOption = new PartitionOption(insertOption.isDynamicPartition(), + insertOption.getPartitions(), + pathName); + return new HiveInsertTableHandle(connectorId, + schemaName, + tableName, + columnNames, + columnTypes, + targetPath.toString(), + tempPath, + outputFormat, + serdeLib, + serdeParameters, + partitionBitmap, + filePrefix, + session, + insertOption.isOverwrite(), + partitionOption); + } + + @Override + public void commitInsert(ConnectorInsertTableHandle insertHandle, Collection fragments) + { + HiveInsertTableHandle handle = checkType(insertHandle, HiveInsertTableHandle.class, "invalid insertHandle"); + Map> filesWritten = getFilesWritten(fragments, handle.getTableName()); + Path targetLocation = new Path(handle.getTargetPath()); + // Move data from temp locations + try { + if (handle.hasTemporaryPath()) { + moveInsertIntoData(handle, filesWritten); + } + if (handle.isOutputTablePartitioned()) { + Table table = metastore.getTable(handle.getSchemaName(), handle.getTableName()); + findAndRecoverPartitions(targetLocation, + filesWritten.keySet(), + getPartitionsKnown(handle), + table, + handle); + } + } + catch (Exception e) { + rollbackInsertCreateChanges(handle.getTargetPath(), handle.isOutputTablePartitioned(), handle.getFilePrefix(), filesWritten); + e.printStackTrace(); + throw new PrestoException(EXTERNAL, "HiveMetastore Error"); + } + } + + private Map> getFilesWritten(Collection fragments, String tableName) + { + Map> filesWritten = new HashMap>(); + ObjectMapper mapper = new ObjectMapper(); + try { + for (Slice fragment : fragments) { + if (fragment.length() == 0) { + log.warn("Empty fragment for Insert on table " + tableName); + continue; + } + Map> filesWrittenFragment = new HashMap>(); + filesWrittenFragment = mapper.readValue(new String(fragment.getBytes()), filesWrittenFragment.getClass()); + for (String partition : filesWrittenFragment.keySet()) { + if (!filesWritten.containsKey(partition)) { + filesWritten.put(partition, new ArrayList()); + } + filesWritten.get(partition).addAll(filesWrittenFragment.get(partition)); + } + } + } + catch (Exception e) { + throw Throwables.propagate(e); + } + return filesWritten; + } + + private void findAndRecoverPartitions(Path tableLocation, + Set partitionsWritten, + List partitionsKnown, + final Table table, + HiveInsertTableHandle handle) throws TException + { + List commitBatch = new ArrayList(); + PartitionOption partitionOption = handle.getPartitionOption(); + + if (!partitionOption.isDynamicPartition()) { + String partition = partitionOption.getPartitionSuffix(); + if (!partitionsKnown.contains(partition)) { + List partitionLists = new LinkedList(); + Path partLocation = new Path(tableLocation, partition); + partitionLists.addAll(partitionOption.getPartitionElements().values()); + + Partition tpart = metastore.createPartition(table.getDbName(), table.getTableName(), partitionLists, null, table, partLocation.toString()); + commitBatch.add(tpart); + metastore.addPartitions(commitBatch, table.getDbName(), table.getTableName()); + commitBatch.clear(); + } + } + else { + addDynamicPartitions(partitionsWritten, partitionsKnown, handle.getTargetPath(), handle.getSchemaName(), handle.getTableName()); + } + } + + private void moveInsertIntoData(HiveInsertTableHandle handle, + Map> filesWritten) + { + ExecutorService executor = Executors.newFixedThreadPool( + renameThreadPoolSize, new ThreadFactoryBuilder().setNameFormat( + "hive-client-rename-" + "-%d").build()); + final PartitionOption partitionOption = handle.getPartitionOption(); + for (String partition : filesWritten.keySet()) { + Path srcPartition; + Path destPartition; + + if (handle.isOutputTablePartitioned()) { + srcPartition = new Path(handle.getTemporaryPath(), partition); + if (!partitionOption.isDynamicPartition()) { + destPartition = new Path(handle.getTargetPath(), partitionOption.getPartitionSuffix()); + } + else { + destPartition = new Path(handle.getTargetPath(), partition); + } + } + else { + srcPartition = new Path(handle.getTemporaryPath()); + destPartition = new Path(handle.getTargetPath()); + } + if (!pathExists(destPartition)) { + createDirectories(destPartition); + } + if (handle.isOverwrite()) { + deleteHdfs(destPartition); + } + for (String file : filesWritten.get(partition)) { + final Path srcPath = new Path(srcPartition, file); + final Path destPath = destPartition; + + executor.execute(new Runnable() { + @Override + public void run() + { + rename(srcPath, destPath); + } + }); + + } + } + + // delete data or partitions if no data written and overwrite is specified + if (filesWritten.keySet().size() == 0 && handle.isOverwrite() && !partitionOption.isDynamicPartition()) { + Path destPartition = null; + if (handle.isOutputTablePartitioned()) { + destPartition = new Path(handle.getTargetPath(), partitionOption.getPartitionSuffix()); + } + else { + destPartition = new Path(handle.getTargetPath()); + } + if (!pathExists(destPartition)) { + createDirectories(destPartition); + } + if (handle.isOverwrite()) { + deleteHdfs(destPartition); + } + } + + executor.shutdown(); + while (!executor.isTerminated()) { + try { + Thread.sleep(1000); + } + catch (InterruptedException e) { + // ignore + } + } + } + + private List getPartitionsKnown(HiveInsertTableHandle handle) + { + HiveTableHandle hiveTableHandle = new HiveTableHandle(handle.getClientId(), + handle.getSchemaName(), + handle.getTableName(), + handle.getConnectorSession()); + List partitions = splitManager.getPartitions(hiveTableHandle, TupleDomain.all()).getPartitions(); + + return partitions.stream() + .map(ConnectorPartition::getPartitionId) + .collect(toList()); + } + + /** + * add dynamic partition based on files written + * @param filesWritten + * @param partitionsKnown + * @param targetPath + * @param schemaName + * @param tableName + * @throws TException + */ + private void addDynamicPartitions(Set filesWritten, List partitionsKnown, String targetPath, String schemaName, String tableName) throws TException + { + List commitBatch = new ArrayList(); + int recovered = 0; + Table table = metastore.getTable(schemaName, tableName); + for (String partition : filesWritten) { + if (partitionsKnown == null || !partitionsKnown.contains(partition)) { + List lli = new LinkedList(); + Path partLocation = new Path(targetPath, partition); + Splitter.MapSplitter mapSplitter = Splitter.on("/").withKeyValueSeparator("="); + Map splitMap = mapSplitter.split(partition.substring(partition.indexOf(table.getTableName()) + 1)); + lli.addAll(splitMap.values()); + Partition tpart = getMetastore().createPartition(table.getDbName(), table.getTableName(), lli, null, table, partLocation.toString()); + commitBatch.add(tpart); + recovered++; + if (commitBatch.size() >= partionCommitBatchSize) { + getMetastore().addPartitions(commitBatch, table.getDbName(), table.getTableName()); + commitBatch.clear(); + } + } + } + + if (commitBatch.size() > 0) { + getMetastore().addPartitions(commitBatch, table.getDbName(), table.getTableName()); + commitBatch.clear(); + } + log.info("Recovered " + recovered + " partitions"); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("clientId", connectorId) + .toString(); + } + + private void verifyJvmTimeZone() + { + if (!allowCorruptWritesForTesting && !timeZone.equals(DateTimeZone.getDefault())) { + throw new PrestoException(HIVE_TIMEZONE_MISMATCH, format( + "To write Hive data, your JVM timezone must match the Hive storage timezone. Add -Duser.timezone=%s to your JVM arguments.", + timeZone.getID())); + } + } + + private static void buildColumnInfo(ConnectorTableMetadata tableMetadata, ImmutableList.Builder names, ImmutableList.Builder types) + { + for (ColumnMetadata column : tableMetadata.getColumns()) { + // TODO: also verify that the OutputFormat supports the type + if (!HiveRecordSink.isTypeSupported(column.getType())) { + throw new PrestoException(NOT_SUPPORTED, format("Cannot create table with unsupported type: %s", column.getType().getDisplayName())); + } + names.add(column.getName()); + types.add(column.getType()); + } + + if (tableMetadata.isSampled()) { + names.add(SAMPLE_WEIGHT_COLUMN_NAME); + types.add(BIGINT); + } + } + + private static Function columnMetadataGetter(Table table, final TypeManager typeManager) + { + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (FieldSchema field : concat(table.getSd().getCols(), table.getPartitionKeys())) { + if (field.getComment() != null) { + builder.put(field.getName(), field.getComment()); + } + } + final Map columnComment = builder.build(); + + return input -> new ColumnMetadata( + input.getName(), + typeManager.getType(input.getTypeSignature()), + input.isPartitionKey(), + columnComment.get(input.getName()), + false); + } + + private synchronized String createTemporaryPath(Path targetPath) + { + // use a per-user temporary directory to avoid permission problems + // TODO: this should use Hadoop UserGroupInformation + String temporaryPrefix = "/tmp/presto-" + StandardSystemProperty.USER_NAME.value(); + + // create a temporary directory on the same filesystem + Path temporaryRoot = new Path(targetPath, temporaryPrefix); + Path temporaryPath = new Path(temporaryRoot, randomUUID().toString() + System.nanoTime()); + createDirectories(temporaryPath); + + return temporaryPath.toString(); + } + + private void rollbackInsertCreateChanges(String targetPath, boolean isOutputPartitioned, String filePrefix, + Map> filesWritten) + { + Path tableLocation = new Path(targetPath); + for (String partition : filesWritten.keySet()) { + Path partitionPath; + if (isOutputPartitioned) { + partitionPath = new Path(tableLocation, partition); + } + else { + partitionPath = tableLocation; + } + for (String file : filesWritten.get(partition)) { + deleteHdfs(new Path(partitionPath, file)); + } + } + } + + private void deleteHdfs(Path target) + { + log.debug("target path uri : " + target.toString()); + try { + FileSystem fileSystem = hdfsEnvironment.getFileSystem(target); + if (!fileSystem.exists(target)) { + return; + } + FileStatus[] listStatus = fileSystem.listStatus(target); + if (listStatus == null || listStatus.length == 0) { + return; + } + for (FileStatus file : listStatus) { + fileSystem.delete(file.getPath(), false); + } + } + catch (IOException e) { + throw new RuntimeException(format("Failed to delete %s", target), e); + } + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/HiveMetastoreClient.java b/presto-hive/src/main/java/com/facebook/presto/hive/HiveMetastoreClient.java new file mode 100644 index 00000000..344e8209 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/HiveMetastoreClient.java @@ -0,0 +1,46 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import org.apache.hadoop.hive.metastore.api.ThriftHiveMetastore; +import org.apache.thrift.protocol.TBinaryProtocol; +import org.apache.thrift.protocol.TProtocol; +import org.apache.thrift.transport.TTransport; + +import java.io.Closeable; + +public class HiveMetastoreClient + extends ThriftHiveMetastore.Client + implements Closeable +{ + private final TTransport transport; + + public HiveMetastoreClient(TTransport transport) + { + super(new TBinaryProtocol(transport)); + this.transport = transport; + } + + public HiveMetastoreClient(TProtocol protocol) + { + super(protocol); + this.transport = protocol.getTransport(); + } + + @Override + public void close() + { + transport.close(); + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/HiveMetastoreClientFactory.java b/presto-hive/src/main/java/com/facebook/presto/hive/HiveMetastoreClientFactory.java new file mode 100644 index 00000000..145f5f73 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/HiveMetastoreClientFactory.java @@ -0,0 +1,219 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.google.common.net.HostAndPort; +import com.google.common.primitives.Ints; +import io.airlift.units.Duration; +import org.apache.thrift.transport.TSocket; +import org.apache.thrift.transport.TTransport; +import org.apache.thrift.transport.TTransportException; + +import javax.annotation.Nullable; +import javax.inject.Inject; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.Socket; +import java.net.SocketAddress; + +public class HiveMetastoreClientFactory +{ + private final HostAndPort socksProxy; + private final int timeoutMillis; + + public HiveMetastoreClientFactory(@Nullable HostAndPort socksProxy, Duration timeout) + { + this.socksProxy = socksProxy; + this.timeoutMillis = Ints.checkedCast(timeout.toMillis()); + } + + @Inject + public HiveMetastoreClientFactory(HiveClientConfig config) + { + this(config.getMetastoreSocksProxy(), config.getMetastoreTimeout()); + } + + private static Socket createSocksSocket(HostAndPort proxy) + { + SocketAddress address = InetSocketAddress.createUnresolved(proxy.getHostText(), proxy.getPort()); + return new Socket(new Proxy(Proxy.Type.SOCKS, address)); + } + + private static TTransportException rewriteException(TTransportException e, String host) + { + return new TTransportException(e.getType(), String.format("%s: %s", host, e.getMessage()), e.getCause()); + } + + public HiveMetastoreClient create(String host, int port) + throws TTransportException + { + return new HiveMetastoreClient(createTransport(host, port)); + } + + protected TTransport createTransport(String host, int port) + throws TTransportException + { + TTransport transport; + if (socksProxy == null) { + transport = new TTransportWrapper(new TSocket(host, port, timeoutMillis), host); + transport.open(); + } + else { + Socket socks = createSocksSocket(socksProxy); + try { + socks.connect(InetSocketAddress.createUnresolved(host, port), timeoutMillis); + socks.setSoTimeout(timeoutMillis); + } + catch (IOException e) { + throw rewriteException(new TTransportException(e), host); + } + try { + transport = new TTransportWrapper(new TSocket(socks), host); + } + catch (TTransportException e) { + throw rewriteException(e, host); + } + } + return transport; + } + + private static class TTransportWrapper + extends TTransport + { + private final TTransport transport; + private final String host; + + TTransportWrapper(TTransport transport, String host) + { + this.transport = transport; + this.host = host; + } + + @Override + public boolean isOpen() + { + return transport.isOpen(); + } + + @Override + public boolean peek() + { + return transport.peek(); + } + + @Override + public byte[] getBuffer() + { + return transport.getBuffer(); + } + + @Override + public int getBufferPosition() + { + return transport.getBufferPosition(); + } + + @Override + public int getBytesRemainingInBuffer() + { + return transport.getBytesRemainingInBuffer(); + } + + @Override + public void consumeBuffer(int len) + { + transport.consumeBuffer(len); + } + + @Override + public void close() + { + transport.close(); + } + + @Override + public void open() + throws TTransportException + { + try { + transport.open(); + } + catch (TTransportException e) { + throw rewriteException(e, host); + } + } + + @Override + public int readAll(byte[] bytes, int off, int len) + throws TTransportException + { + try { + return transport.readAll(bytes, off, len); + } + catch (TTransportException e) { + throw rewriteException(e, host); + } + } + + @Override + public int read(byte[] bytes, int off, int len) + throws TTransportException + { + try { + return transport.read(bytes, off, len); + } + catch (TTransportException e) { + throw rewriteException(e, host); + } + } + + @Override + public void write(byte[] bytes) + throws TTransportException + { + try { + transport.write(bytes); + } + catch (TTransportException e) { + throw rewriteException(e, host); + } + } + + @Override + public void write(byte[] bytes, int off, int len) + throws TTransportException + { + try { + transport.write(bytes, off, len); + } + catch (TTransportException e) { + throw rewriteException(e, host); + } + } + + @Override + public void flush() + throws TTransportException + { + try { + transport.flush(); + } + catch (TTransportException e) { + throw rewriteException(e, host); + } + } + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/HiveOutputTableHandle.java b/presto-hive/src/main/java/com/facebook/presto/hive/HiveOutputTableHandle.java new file mode 100644 index 00000000..6ee9b2b3 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/HiveOutputTableHandle.java @@ -0,0 +1,204 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.spi.ConnectorOutputTableHandle; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.type.Type; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; + +import java.util.ArrayList; +import java.util.List; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class HiveOutputTableHandle + implements ConnectorOutputTableHandle +{ + private final String clientId; + private final String schemaName; + private final String tableName; + private final List columnNames; + private final List columnTypes; + private final List columnPartitioned; + private final String tableOwner; + private final String targetPath; + private final String temporaryPath; + private final ConnectorSession connectorSession; + private final HiveStorageFormat hiveStorageFormat; + + @JsonCreator + public HiveOutputTableHandle( + @JsonProperty("clientId") String clientId, + @JsonProperty("schemaName") String schemaName, + @JsonProperty("tableName") String tableName, + @JsonProperty("columnNames") List columnNames, + @JsonProperty("columnTypes") List columnTypes, + @JsonProperty("columnPartitioned") List columnPartitioned, + @JsonProperty("tableOwner") String tableOwner, + @JsonProperty("targetPath") String targetPath, + @JsonProperty("temporaryPath") String temporaryPath, + @JsonProperty("connectorSession") ConnectorSession connectorSession, + @JsonProperty("hiveStorageFormat") HiveStorageFormat hiveStorageFormat) + { + this.clientId = checkNotNull(clientId, "clientId is null"); + this.schemaName = checkNotNull(schemaName, "schemaName is null"); + this.tableName = checkNotNull(tableName, "tableName is null"); + this.tableOwner = checkNotNull(tableOwner, "tableOwner is null"); + this.targetPath = checkNotNull(targetPath, "targetPath is null"); + this.temporaryPath = checkNotNull(temporaryPath, "temporaryPath is null"); + this.connectorSession = checkNotNull(connectorSession, "session is null"); + this.hiveStorageFormat = checkNotNull(hiveStorageFormat, "hiveStorageFormat is null"); + + checkNotNull(columnNames, "columnNames is null"); + checkNotNull(columnTypes, "columnTypes is null"); + checkArgument(columnNames.size() == columnTypes.size(), "columnNames and columnTypes sizes don't match"); + this.columnNames = ImmutableList.copyOf(columnNames); + this.columnTypes = ImmutableList.copyOf(columnTypes); + this.columnPartitioned = columnPartitioned; + } + + @JsonProperty + public String getClientId() + { + return clientId; + } + + @JsonProperty + public String getSchemaName() + { + return schemaName; + } + + @JsonProperty + public String getTableName() + { + return tableName; + } + + @JsonProperty + public List getColumnNames() + { + return columnNames; + } + + public List getDataColumnNames() + { + if (!isOutputTablePartitioned()) { + return columnNames; + } + + List dataColumnNames = new ArrayList(); + for (int i = 0; i < columnNames.size(); i++) { + if (!columnPartitioned.get(i)) { + dataColumnNames.add(columnNames.get(i)); + } + } + + return dataColumnNames; + } + + @JsonProperty + public List getColumnTypes() + { + return columnTypes; + } + + public List getDataColumnTypes() + { + if (!isOutputTablePartitioned()) { + return ImmutableList.copyOf(columnTypes); + } + + List dataColumnTypes = new ArrayList(); + for (int i = 0; i < columnTypes.size(); i++) { + if (!columnPartitioned.get(i)) { + dataColumnTypes.add(columnTypes.get(i)); + } + } + + return dataColumnTypes; + } + + @JsonProperty + public List getColumnPartitioned() + { + return columnPartitioned; + } + + public boolean isOutputTablePartitioned() + { + return columnPartitioned != null && columnPartitioned.contains(true); + } + + public List getPartitionColumnNames() + { + if (!isOutputTablePartitioned()) { + return null; + } + + List partitionColumnNames = new ArrayList(); + for (int i = 0; i < columnNames.size(); i++) { + if (columnPartitioned.get(i)) { + partitionColumnNames.add(columnNames.get(i)); + } + } + + return partitionColumnNames; + } + + @JsonProperty + public String getTableOwner() + { + return tableOwner; + } + + @JsonProperty + public String getTargetPath() + { + return targetPath; + } + + @JsonProperty + public String getTemporaryPath() + { + return temporaryPath; + } + + @JsonProperty + public ConnectorSession getConnectorSession() + { + return connectorSession; + } + + @JsonProperty + public HiveStorageFormat getHiveStorageFormat() + { + return hiveStorageFormat; + } + + @Override + public String toString() + { + return "hive:" + schemaName + "." + tableName; + } + + public boolean hasTemporaryPath() + { + return !temporaryPath.equals(targetPath); + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/HivePageSourceFactory.java b/presto-hive/src/main/java/com/facebook/presto/hive/HivePageSourceFactory.java new file mode 100644 index 00000000..84d3e70e --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/HivePageSourceFactory.java @@ -0,0 +1,40 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.spi.ConnectorPageSource; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.TupleDomain; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.Path; +import org.joda.time.DateTimeZone; + +import java.util.List; +import java.util.Optional; +import java.util.Properties; + +public interface HivePageSourceFactory +{ + Optional createPageSource( + Configuration configuration, + ConnectorSession session, + Path path, + long start, + long length, + Properties schema, + List columns, + List partitionKeys, + TupleDomain effectivePredicate, + DateTimeZone hiveStorageTimeZone); +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/HivePageSourceProvider.java b/presto-hive/src/main/java/com/facebook/presto/hive/HivePageSourceProvider.java new file mode 100644 index 00000000..75acf1a0 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/HivePageSourceProvider.java @@ -0,0 +1,148 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorPageSource; +import com.facebook.presto.spi.ConnectorPageSourceProvider; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.RecordPageSource; +import com.facebook.presto.spi.TupleDomain; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.Path; +import org.joda.time.DateTimeZone; + +import javax.inject.Inject; + +import java.util.List; +import java.util.Optional; +import java.util.Properties; +import java.util.Set; + +import static com.facebook.presto.hive.util.Types.checkType; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.Iterables.transform; + +public class HivePageSourceProvider + implements ConnectorPageSourceProvider +{ + private final DateTimeZone hiveStorageTimeZone; + private final HdfsEnvironment hdfsEnvironment; + private final Set cursorProviders; + private final TypeManager typeManager; + + private final Set pageSourceFactories; + + @Inject + public HivePageSourceProvider( + HiveClientConfig hiveClientConfig, + HdfsEnvironment hdfsEnvironment, + Set cursorProviders, + Set pageSourceFactories, + TypeManager typeManager) + { + checkNotNull(hiveClientConfig, "hiveClientConfig is null"); + this.hiveStorageTimeZone = DateTimeZone.forTimeZone(hiveClientConfig.getTimeZone()); + this.hdfsEnvironment = checkNotNull(hdfsEnvironment, "hdfsEnvironment is null"); + this.cursorProviders = ImmutableSet.copyOf(checkNotNull(cursorProviders, "cursorProviders is null")); + this.pageSourceFactories = ImmutableSet.copyOf(checkNotNull(pageSourceFactories, "pageSourceFactories is null")); + this.typeManager = checkNotNull(typeManager, "typeManager is null"); + } + + @Override + public ConnectorPageSource createPageSource(ConnectorSplit split, List columns) + { + HiveSplit hiveSplit = checkType(split, HiveSplit.class, "split"); + + String clientId = hiveSplit.getClientId(); + ConnectorSession session = hiveSplit.getSession(); + + Path path = new Path(hiveSplit.getPath()); + long start = hiveSplit.getStart(); + long length = hiveSplit.getLength(); + + Configuration configuration = hdfsEnvironment.getConfiguration(path); + + TupleDomain effectivePredicate = hiveSplit.getEffectivePredicate(); + + Properties schema = hiveSplit.getSchema(); + + List partitionKeys = hiveSplit.getPartitionKeys(); + List hiveColumns = ImmutableList.copyOf(transform(columns, HiveColumnHandle::toHiveColumnHandle)); + + for (HivePageSourceFactory pageSourceFactory : pageSourceFactories) { + Optional pageSource = pageSourceFactory.createPageSource( + configuration, + session, + path, + start, + length, + schema, + hiveColumns, + partitionKeys, + effectivePredicate, + hiveStorageTimeZone + ); + if (pageSource.isPresent()) { + return pageSource.get(); + } + } + + HiveRecordCursor recordCursor = getHiveRecordCursor(clientId, session, configuration, path, start, length, schema, effectivePredicate, partitionKeys, hiveColumns); + if (recordCursor != null) { + List columnTypes = ImmutableList.copyOf(transform(hiveColumns, input -> typeManager.getType(input.getTypeSignature()))); + return new RecordPageSource(columnTypes, recordCursor); + } + + throw new RuntimeException("Could not find a file reader for split " + hiveSplit); + } + + protected HiveRecordCursor getHiveRecordCursor( + String clientId, + ConnectorSession session, + Configuration configuration, + Path path, + long start, + long length, + Properties schema, + TupleDomain effectivePredicate, + List partitionKeys, + List hiveColumns) + { + for (HiveRecordCursorProvider provider : cursorProviders) { + Optional cursor = provider.createHiveRecordCursor( + clientId, + configuration, + session, + path, + start, + length, + schema, + hiveColumns, + partitionKeys, + effectivePredicate, + hiveStorageTimeZone, + typeManager); + if (cursor.isPresent()) { + return cursor.get(); + } + } + return null; + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/HivePartition.java b/presto-hive/src/main/java/com/facebook/presto/hive/HivePartition.java new file mode 100644 index 00000000..2d227704 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/HivePartition.java @@ -0,0 +1,124 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorPartition; +import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.spi.SerializableNativeValue; +import com.facebook.presto.spi.TupleDomain; +import com.google.common.collect.ImmutableMap; + +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +import static com.facebook.presto.hive.HiveBucketing.HiveBucket; +import static com.google.common.base.Preconditions.checkNotNull; + +public class HivePartition + implements ConnectorPartition +{ + public static final String UNPARTITIONED_ID = ""; + + private final SchemaTableName tableName; + private final TupleDomain effectivePredicate; + private final String partitionId; + private final Map keys; + private final Optional bucket; + + public HivePartition(SchemaTableName tableName, TupleDomain effectivePredicate) + { + this.tableName = checkNotNull(tableName, "tableName is null"); + this.effectivePredicate = checkNotNull(effectivePredicate, "effectivePredicate is null"); + this.partitionId = UNPARTITIONED_ID; + this.keys = ImmutableMap.of(); + this.bucket = Optional.empty(); + } + + public HivePartition(SchemaTableName tableName, TupleDomain effectivePredicate, Optional bucket) + { + this(tableName, effectivePredicate, UNPARTITIONED_ID, ImmutableMap.of(), bucket); + } + + public HivePartition(SchemaTableName tableName, + TupleDomain effectivePredicate, + String partitionId, + Map keys, + Optional bucket) + { + this.tableName = checkNotNull(tableName, "tableName is null"); + this.effectivePredicate = checkNotNull(effectivePredicate, "effectivePredicate is null"); + this.partitionId = checkNotNull(partitionId, "partitionId is null"); + this.keys = ImmutableMap.copyOf(checkNotNull(keys, "keys is null")); + this.bucket = checkNotNull(bucket, "bucket number is null"); + } + + public SchemaTableName getTableName() + { + return tableName; + } + + public TupleDomain getEffectivePredicate() + { + return effectivePredicate; + } + + @Override + public String getPartitionId() + { + return partitionId; + } + + @Override + public TupleDomain getTupleDomain() + { + return TupleDomain.withNullableFixedValues(keys); + } + + public Map getKeys() + { + return keys; + } + + public Optional getBucket() + { + return bucket; + } + + @Override + public int hashCode() + { + return Objects.hash(partitionId); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + HivePartition other = (HivePartition) obj; + return Objects.equals(this.partitionId, other.partitionId); + } + + @Override + public String toString() + { + return tableName + ":" + partitionId; + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/HivePartitionKey.java b/presto-hive/src/main/java/com/facebook/presto/hive/HivePartitionKey.java new file mode 100644 index 00000000..5fe4dfb9 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/HivePartitionKey.java @@ -0,0 +1,94 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkNotNull; + +public final class HivePartitionKey +{ + public static final String HIVE_DEFAULT_DYNAMIC_PARTITION = "__HIVE_DEFAULT_PARTITION__"; + private final String name; + private final HiveType hiveType; + private final String value; + + @JsonCreator + public HivePartitionKey( + @JsonProperty("name") String name, + @JsonProperty("hiveType") HiveType hiveType, + @JsonProperty("value") String value) + { + checkNotNull(name, "name is null"); + checkNotNull(hiveType, "hiveType is null"); + checkNotNull(value, "value is null"); + + this.name = name; + this.hiveType = hiveType; + this.value = value.equals(HIVE_DEFAULT_DYNAMIC_PARTITION) ? "\\N" : value; + } + + @JsonProperty + public String getName() + { + return name; + } + + @JsonProperty + public HiveType getHiveType() + { + return hiveType; + } + + @JsonProperty + public String getValue() + { + return value; + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("name", name) + .add("hiveType", hiveType) + .add("value", value) + .toString(); + } + + @Override + public int hashCode() + { + return Objects.hash(name, hiveType, value); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + HivePartitionKey other = (HivePartitionKey) obj; + return Objects.equals(this.name, other.name) && + Objects.equals(this.hiveType, other.hiveType) && + Objects.equals(this.value, other.value); + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/HivePartitionMetadata.java b/presto-hive/src/main/java/com/facebook/presto/hive/HivePartitionMetadata.java new file mode 100644 index 00000000..07d33f79 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/HivePartitionMetadata.java @@ -0,0 +1,40 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import org.apache.hadoop.hive.metastore.api.Partition; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class HivePartitionMetadata +{ + private final Partition partition; + private final HivePartition hivePartition; + + HivePartitionMetadata(HivePartition hivePartition, Partition partition) + { + this.partition = checkNotNull(partition, "partition is null"); + this.hivePartition = checkNotNull(hivePartition, "hivePartition is null"); + } + + public HivePartition getHivePartition() + { + return hivePartition; + } + + public Partition getPartition() + { + return partition; + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/HivePlugin.java b/presto-hive/src/main/java/com/facebook/presto/hive/HivePlugin.java new file mode 100644 index 00000000..c30ff2ae --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/HivePlugin.java @@ -0,0 +1,81 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.hive.metastore.HiveMetastore; +import com.facebook.presto.spi.ConnectorFactory; +import com.facebook.presto.spi.Plugin; +import com.facebook.presto.spi.type.TypeManager; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +import javax.inject.Inject; + +import java.util.List; +import java.util.Map; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Strings.isNullOrEmpty; + +public class HivePlugin + implements Plugin +{ + private final String name; + private Map optionalConfig = ImmutableMap.of(); + private HiveMetastore metastore; + private TypeManager typeManager; + + public HivePlugin(String name) + { + this(name, null); + } + + public HivePlugin(String name, HiveMetastore metastore) + { + checkArgument(!isNullOrEmpty(name), "name is null or empty"); + this.name = name; + this.metastore = metastore; + } + + @Inject + public void setTypeManager(TypeManager typeManager) + { + this.typeManager = checkNotNull(typeManager, "typeManager is null"); + } + + @Override + public void setOptionalConfig(Map optionalConfig) + { + this.optionalConfig = ImmutableMap.copyOf(checkNotNull(optionalConfig, "optionalConfig is null")); + } + + @Override + public List getServices(Class type) + { + if (type == ConnectorFactory.class) { + return ImmutableList.of(type.cast(new HiveConnectorFactory(name, optionalConfig, getClassLoader(), metastore, typeManager))); + } + return ImmutableList.of(); + } + + private static ClassLoader getClassLoader() + { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + if (classLoader == null) { + classLoader = HivePlugin.class.getClassLoader(); + } + return classLoader; + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/HivePluginConfig.java b/presto-hive/src/main/java/com/facebook/presto/hive/HivePluginConfig.java new file mode 100644 index 00000000..cc39fb6c --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/HivePluginConfig.java @@ -0,0 +1,38 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import io.airlift.configuration.Config; + +import javax.validation.constraints.NotNull; + +import java.net.URI; + +public class HivePluginConfig +{ + private URI metastoreUri; + + @NotNull + public URI getMetastoreUri() + { + return metastoreUri; + } + + @Config("hive.metastore.uri") + public HivePluginConfig setMetastoreUri(URI metastoreUri) + { + this.metastoreUri = metastoreUri; + return this; + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/HiveRecordCursor.java b/presto-hive/src/main/java/com/facebook/presto/hive/HiveRecordCursor.java new file mode 100644 index 00000000..26f55940 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/HiveRecordCursor.java @@ -0,0 +1,50 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.spi.RecordCursor; + +import static com.google.common.base.Preconditions.checkNotNull; + +public abstract class HiveRecordCursor + implements RecordCursor +{ + private long readTime; + + @Override + public long getReadTimeNanos() + { + return readTime; + } + + // For use with HDFS clients that can report the remote read time + public void addReadTime(long nanos) + { + readTime += nanos; + } + + protected void closeWithSuppression(Throwable throwable) + { + checkNotNull(throwable, "throwable is null"); + try { + close(); + } + catch (RuntimeException e) { + // Self-suppression not permitted + if (throwable != e) { + throwable.addSuppressed(e); + } + } + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/HiveRecordCursorProvider.java b/presto-hive/src/main/java/com/facebook/presto/hive/HiveRecordCursorProvider.java new file mode 100644 index 00000000..b98d96cd --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/HiveRecordCursorProvider.java @@ -0,0 +1,42 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.TupleDomain; +import com.facebook.presto.spi.type.TypeManager; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.Path; +import org.joda.time.DateTimeZone; + +import java.util.List; +import java.util.Optional; +import java.util.Properties; + +public interface HiveRecordCursorProvider +{ + Optional createHiveRecordCursor( + String clientId, + Configuration configuration, + ConnectorSession session, + Path path, + long start, + long length, + Properties schema, + List columns, + List partitionKeys, + TupleDomain effectivePredicate, + DateTimeZone hiveStorageTimeZone, + TypeManager typeManager); +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/HiveRecordSink.java b/presto-hive/src/main/java/com/facebook/presto/hive/HiveRecordSink.java new file mode 100644 index 00000000..93b153dc --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/HiveRecordSink.java @@ -0,0 +1,558 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.RecordSink; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.block.BlockBuilderStatus; +import com.facebook.presto.spi.classloader.ThreadContextClassLoader; +import com.facebook.presto.spi.type.BigintType; +import com.facebook.presto.spi.type.BooleanType; +import com.facebook.presto.spi.type.DateType; +import com.facebook.presto.spi.type.DoubleType; +import com.facebook.presto.spi.type.SqlDate; +import com.facebook.presto.spi.type.SqlTimestamp; +import com.facebook.presto.spi.type.TimestampType; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.VarbinaryType; +import com.facebook.presto.spi.type.VarcharType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Joiner; +import com.google.common.base.Throwables; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.ImmutableList; + +import io.airlift.slice.Slice; +import io.airlift.slice.Slices; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.hive.common.FileUtils; +import org.apache.hadoop.hive.ql.io.HiveOutputFormat; +import org.apache.hadoop.hive.serde2.SerDeException; +import org.apache.hadoop.hive.serde2.Serializer; +import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspectorFactory; +import org.apache.hadoop.hive.serde2.objectinspector.SettableStructObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.StructField; +import org.apache.hadoop.io.Text; +import org.apache.hadoop.mapred.JobConf; +import org.apache.hadoop.mapred.Reporter; + +import java.io.IOException; +import java.sql.Date; +import java.sql.Timestamp; +import java.util.Collection; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import static com.facebook.presto.hive.HiveColumnHandle.SAMPLE_WEIGHT_COLUMN_NAME; +import static com.facebook.presto.hive.HiveErrorCode.HIVE_WRITER_ERROR; +import static com.facebook.presto.hive.HivePartition.UNPARTITIONED_ID; +import static com.facebook.presto.hive.HivePartitionKey.HIVE_DEFAULT_DYNAMIC_PARTITION; +import static com.facebook.presto.hive.HiveUtil.isArrayType; +import static com.facebook.presto.hive.HiveUtil.isMapType; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.Iterables.transform; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.stream.Collectors.toList; +import static java.util.UUID.randomUUID; +import static org.apache.hadoop.hive.metastore.api.hive_metastoreConstants.META_TABLE_COLUMNS; +import static org.apache.hadoop.hive.metastore.api.hive_metastoreConstants.META_TABLE_COLUMN_TYPES; +import static org.apache.hadoop.hive.ql.exec.FileSinkOperator.RecordWriter; +import static org.apache.hadoop.hive.serde2.objectinspector.ObjectInspectorFactory.getStandardStructObjectInspector; +import static org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory.javaBooleanObjectInspector; +import static org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory.javaByteArrayObjectInspector; +import static org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory.javaDateObjectInspector; +import static org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory.javaDoubleObjectInspector; +import static org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory.javaLongObjectInspector; +import static org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory.javaStringObjectInspector; +import static org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory.javaTimestampObjectInspector; + +public class HiveRecordSink + implements RecordSink +{ + private final int fieldCount; + @SuppressWarnings("deprecation") + private final Serializer serializer; + private RecordWriter recordWriter; + private final SettableStructObjectInspector tableInspector; + private final List structFields; + private final List columnTypes; + private final List hasDateTimeTypes; + private final Object row; + private final int sampleWeightField; + private final ConnectorSession connectorSession; + + private int field = -1; + + private final boolean isPartitioned; + private List partitionColNames; + private List partitionValues; + private final JobConf conf; + private final int dataFieldsCount; + private final String outputFormat; + + private String fileName; + private Path basePath; + + private Map> filesWritten; // filesWritten for each partition + private LoadingCache recordWriters; + private final Properties properties; + + private final ClassLoader contextClassLoader; + + public HiveRecordSink(HiveOutputTableHandle handle, Path target, JobConf conf) + { + this(target, + conf, + handle.getColumnNames(), + handle.getDataColumnNames(), + handle.getColumnTypes(), + handle.getDataColumnTypes(), + handle.getHiveStorageFormat().getOutputFormat(), + handle.getHiveStorageFormat().getSerDe(), + null, + "", + handle.isOutputTablePartitioned(), + handle.getConnectorSession()); + + if (isPartitioned) { + partitionColNames = handle.getPartitionColumnNames(); + partitionValues = new ArrayList(partitionColNames.size()); + } + } + + public HiveRecordSink(HiveInsertTableHandle handle, Path target, JobConf conf) + { + this(target, + conf, + handle.getColumnNames(), + handle.getDataColumnNames(), + handle.getColumnTypes(), + handle.getDataColumnTypes(), + handle.getOutputFormat(), + handle.getSerdeLib(), + handle.getSerdeParameters(), + handle.getFilePrefix(), + handle.isOutputTablePartitioned(), + handle.getConnectorSession()); + + if (isPartitioned) { + partitionColNames = handle.getPartitionColumnNames(); + partitionValues = new ArrayList(partitionColNames.size()); + } + } + + private HiveRecordSink(Path target, + JobConf conf, + List columnNames, + List dataColumnNames, + List columnTypes, + List dataColumnTypes, + String outputFormat, + String serdeLib, + Map serdeParameters, + String filePrefix, + boolean isPartitioned, + ConnectorSession connectorSession + ) + { + checkNotNull(target, "Base path for table data not set"); + + basePath = target; + fieldCount = columnNames.size(); + dataFieldsCount = dataColumnNames.size(); + + this.conf = conf; + this.isPartitioned = isPartitioned; + + sampleWeightField = columnNames.indexOf(SAMPLE_WEIGHT_COLUMN_NAME); + this.columnTypes = columnTypes; + this.connectorSession = connectorSession; + hasDateTimeTypes = columnTypes.stream().map(this::containsDateTime).collect(toList()); + + Iterable hiveTypeNames = transform(transform(dataColumnTypes, HiveType::toHiveType), HiveType::getHiveTypeName); + + this.outputFormat = outputFormat; + properties = new Properties(); + properties.setProperty(META_TABLE_COLUMNS, Joiner.on(',').join(dataColumnNames)); + properties.setProperty(META_TABLE_COLUMN_TYPES, Joiner.on(':').join(hiveTypeNames)); + + if (serdeParameters != null) { + for (String key : serdeParameters.keySet()) { + properties.setProperty(key, serdeParameters.get(key)); + } + } + + serializer = initializeSerializer(conf, properties, serdeLib); + + filePrefix = (filePrefix.length() > 0) ? filePrefix + "_" : filePrefix; + fileName = filePrefix + randomUUID().toString(); + + filesWritten = new HashMap>(); + contextClassLoader = Thread.currentThread().getContextClassLoader(); + if (isPartitioned) { + recordWriters = CacheBuilder.newBuilder().build( + new CacheLoader() { + @Override + public RecordWriter load(String path) + { + try (ThreadContextClassLoader ignored = new ThreadContextClassLoader(contextClassLoader)) { + return getRecordWriter(path); + } + } + }); + recordWriter = null; + } + else { + createNonPartitionedRecordReader(); + } + + tableInspector = getStandardStructObjectInspector(dataColumnNames, getJavaObjectInspectors(columnTypes)); + structFields = ImmutableList.copyOf(tableInspector.getAllStructFieldRefs()); + row = tableInspector.create(); + } + + private RecordWriter createNonPartitionedRecordReader() + { + Path filePath = new Path(basePath, fileName); + if (!filesWritten.containsKey(UNPARTITIONED_ID)) { + filesWritten.put(UNPARTITIONED_ID, new ArrayList()); + } + + filesWritten.get(UNPARTITIONED_ID).add(fileName); + + recordWriter = createRecordWriter(filePath, conf, properties, outputFormat); + return recordWriter; + } + + private RecordWriter getRecordWriter(String pathName) + { + String partitionId = pathName.startsWith("/") ? pathName.substring(1) : pathName; + Path partitionPath = new Path(basePath, partitionId); + Path filePath = new Path(partitionPath, fileName); + if (!filesWritten.containsKey(partitionId)) { + filesWritten.put(partitionId, new ArrayList()); + } + + filesWritten.get(partitionId).add(fileName); + + return createRecordWriter(filePath, conf, properties, outputFormat); + } + + @Override + public void beginRecord(long sampleWeight) + { + checkState(field == -1, "already in record"); + if (sampleWeightField >= 0) { + tableInspector.setStructFieldData(row, structFields.get(sampleWeightField), sampleWeight); + } + field = 0; + if (sampleWeightField == 0) { + field++; + } + } + + @Override + public void finishRecord() + { + checkState(field != -1, "not in record"); + checkState(field == fieldCount, "not all fields set"); + field = -1; + + RecordWriter rw = recordWriter; + + if (isPartitioned) { + String pathName = FileUtils.makePartName(partitionColNames, partitionValues); + try { + rw = recordWriters.get(pathName); + } + catch (ExecutionException e) { + throw Throwables.propagate(e); + } + partitionValues.clear(); + } + + try { + rw.write(serializer.serialize(row, tableInspector)); + } + catch (SerDeException | IOException e) { + throw new PrestoException(HIVE_WRITER_ERROR, e); + } + } + + @Override + public void appendNull() + { + append(null); + } + + @Override + public void appendBoolean(boolean value) + { + append(value); + } + + @Override + public void appendLong(long value) + { + Type type = columnTypes.get(field); + if (type.equals(DateType.DATE)) { + // todo should this be adjusted to midnight in JVM timezone? + append(new Date(TimeUnit.DAYS.toMillis(value))); + } + else if (type.equals(TimestampType.TIMESTAMP)) { + append(new Timestamp(value)); + } + else { + append(value); + } + } + + @Override + public void appendDouble(double value) + { + append(value); + } + + @Override + public void appendString(byte[] value) + { + Type type = columnTypes.get(field); + if (type.equals(VarbinaryType.VARBINARY)) { + append(value); + } + else if (isMapType(type) || isArrayType(type)) { + // Hive expects a List<>/Map<> to write, so decode the value + BlockBuilder blockBuilder = type.createBlockBuilder(new BlockBuilderStatus(), 1, value.length); + type.writeSlice(blockBuilder, Slices.wrappedBuffer(value)); + Object complexValue = type.getObjectValue(connectorSession, blockBuilder.build(), 0); + if (hasDateTimeTypes.get(field)) { + complexValue = translateDateTime(type, complexValue); + } + append(complexValue); + } + else { + append(new String(value, UTF_8)); + } + } + + @Override + public Collection commit() + { + checkState(field == -1, "record not finished"); + String partitionsJson = ""; + try { + if (isPartitioned) { + for (String path : recordWriters.asMap().keySet()) { + recordWriters.get(path).close(false); + } + } + else { + recordWriter.close(false); + } + ObjectMapper mapper = new ObjectMapper(); + partitionsJson = mapper.writeValueAsString(filesWritten); + } + catch (IOException | ExecutionException e) { + throw Throwables.propagate(e); + } + // the committer can list the directory + // partition list will be used in commit of insert to add partitions + return ImmutableList.of(Slices.utf8Slice(partitionsJson)); + } + + @Override + public void rollback() + { + try { + if (isPartitioned) { + for (String path : recordWriters.asMap().keySet()) { + recordWriters.get(path).close(true); + } + } + else { + recordWriter.close(true); + } + } + catch (IOException | ExecutionException e) { + throw Throwables.propagate(e); + } + } + + @Override + public List getColumnTypes() + { + return columnTypes; + } + + private void append(Object value) + { + checkState(field != -1, "not in record"); + checkState(field < fieldCount, "all fields already set"); + + if (field < dataFieldsCount) { + tableInspector.setStructFieldData(row, structFields.get(field), value); + field++; + if (field == sampleWeightField) { + field++; + } + } + else { + // into partition columns now + if (value != null) { + partitionValues.add(value.toString()); + } + else { + partitionValues.add(HIVE_DEFAULT_DYNAMIC_PARTITION); + } + field++; + checkState(field != sampleWeightField, "Partition columns not at the end"); + } + } + + @SuppressWarnings("deprecation") + private static Serializer initializeSerializer(Configuration conf, Properties properties, String serializerName) + { + try { + Serializer result = (Serializer) Class.forName(serializerName).getConstructor().newInstance(); + result.initialize(conf, properties); + return result; + } + catch (SerDeException | ReflectiveOperationException e) { + throw Throwables.propagate(e); + } + } + + private static RecordWriter createRecordWriter(Path target, JobConf conf, Properties properties, String outputFormatName) + { + try { + Object writer = Class.forName(outputFormatName).getConstructor().newInstance(); + return ((HiveOutputFormat) writer).getHiveRecordWriter(conf, target, Text.class, false, properties, Reporter.NULL); + } + catch (IOException | ReflectiveOperationException e) { + throw Throwables.propagate(e); + } + } + + private static List getJavaObjectInspectors(Iterable types) + { + ImmutableList.Builder list = ImmutableList.builder(); + for (Type type : types) { + list.add(getJavaObjectInspector(type)); + } + return list.build(); + } + + private static ObjectInspector getJavaObjectInspector(Type type) + { + if (type.equals(BooleanType.BOOLEAN)) { + return javaBooleanObjectInspector; + } + else if (type.equals(BigintType.BIGINT)) { + return javaLongObjectInspector; + } + else if (type.equals(DoubleType.DOUBLE)) { + return javaDoubleObjectInspector; + } + else if (type.equals(VarcharType.VARCHAR)) { + return javaStringObjectInspector; + } + else if (type.equals(VarbinaryType.VARBINARY)) { + return javaByteArrayObjectInspector; + } + else if (type.equals(DateType.DATE)) { + return javaDateObjectInspector; + } + else if (type.equals(TimestampType.TIMESTAMP)) { + return javaTimestampObjectInspector; + } + else if (isArrayType(type)) { + return ObjectInspectorFactory.getStandardListObjectInspector(getJavaObjectInspector(type.getTypeParameters().get(0))); + } + else if (isMapType(type)) { + ObjectInspector keyObjectInspector = getJavaObjectInspector(type.getTypeParameters().get(0)); + ObjectInspector valueObjectInspector = getJavaObjectInspector(type.getTypeParameters().get(1)); + return ObjectInspectorFactory.getStandardMapObjectInspector(keyObjectInspector, valueObjectInspector); + } + throw new IllegalArgumentException("unsupported type: " + type); + } + + public static boolean isTypeSupported(Type type) + { + try { + getJavaObjectInspector(type); + return true; + } + catch (IllegalArgumentException e) { + return false; + } + } + + private boolean containsDateTime(Type type) + { + if (isArrayType(type)) { + return containsDateTime(type.getTypeParameters().get(0)); + } + if (isMapType(type)) { + return containsDateTime(type.getTypeParameters().get(0)) || containsDateTime(type.getTypeParameters().get(1)); + } + return type.equals(DateType.DATE) || type.equals(TimestampType.TIMESTAMP); + } + + private Object translateDateTime(Type type, Object value) + { + if (value == null) { + return null; + } + if (isArrayType(type)) { + List newValue = new ArrayList<>(); + Type elementType = type.getTypeParameters().get(0); + for (Object val : (List) value) { + newValue.add(translateDateTime(elementType, val)); + } + return newValue; + } + if (isMapType(type)) { + Map newValue = new HashMap<>(); + Type keyType = type.getTypeParameters().get(0); + Type valueType = type.getTypeParameters().get(1); + for (Map.Entry entry : ((Map) value).entrySet()) { + newValue.put( + translateDateTime(keyType, entry.getKey()), + translateDateTime(valueType, entry.getValue())); + } + return newValue; + } + if (value instanceof SqlDate) { + return new Date(TimeUnit.DAYS.toMillis(((SqlDate) value).getDays())); + } + if (value instanceof SqlTimestamp) { + return new Timestamp(((SqlTimestamp) value).getMillisUtc()); + } + + return value; + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/HiveRecordSinkProvider.java b/presto-hive/src/main/java/com/facebook/presto/hive/HiveRecordSinkProvider.java new file mode 100644 index 00000000..e6ac8f40 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/HiveRecordSinkProvider.java @@ -0,0 +1,57 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.spi.ConnectorInsertTableHandle; +import com.facebook.presto.spi.ConnectorOutputTableHandle; +import com.facebook.presto.spi.ConnectorRecordSinkProvider; +import com.facebook.presto.spi.RecordSink; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.mapred.JobConf; + +import javax.inject.Inject; + +import static com.facebook.presto.hive.util.Types.checkType; +import static com.google.common.base.Preconditions.checkNotNull; +public class HiveRecordSinkProvider + implements ConnectorRecordSinkProvider +{ + private final HdfsEnvironment hdfsEnvironment; + + @Inject + public HiveRecordSinkProvider(HdfsEnvironment hdfsEnvironment) + { + this.hdfsEnvironment = checkNotNull(hdfsEnvironment, "hdfsEnvironment is null"); + } + + @Override + public RecordSink getRecordSink(ConnectorOutputTableHandle tableHandle) + { + HiveOutputTableHandle handle = checkType(tableHandle, HiveOutputTableHandle.class, "tableHandle"); + Path target = new Path(handle.getTemporaryPath()); + JobConf conf = new JobConf(hdfsEnvironment.getConfiguration(target)); + + return new HiveRecordSink(handle, target, conf); + } + + @Override + public RecordSink getRecordSink(ConnectorInsertTableHandle tableHandle) + { + HiveInsertTableHandle handle = checkType(tableHandle, HiveInsertTableHandle.class, "tableHandle"); + Path target = new Path(handle.getTemporaryPath()); + JobConf conf = new JobConf(hdfsEnvironment.getConfiguration(target)); + + return new HiveRecordSink(handle, target, conf); + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/HiveSessionProperties.java b/presto-hive/src/main/java/com/facebook/presto/hive/HiveSessionProperties.java new file mode 100644 index 00000000..d86d64b7 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/HiveSessionProperties.java @@ -0,0 +1,125 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.PrestoException; +import io.airlift.units.DataSize; + +import static com.facebook.presto.spi.StandardErrorCode.INVALID_SESSION_PROPERTY; +import static com.facebook.presto.spi.StandardErrorCode.NOT_SUPPORTED; + +public final class HiveSessionProperties +{ + public static final String STORAGE_FORMAT_PROPERTY = "storage_format"; + private static final String FORCE_LOCAL_SCHEDULING = "force_local_scheduling"; + private static final String OPTIMIZED_READER_ENABLED = "optimized_reader_enabled"; + private static final String ORC_MAX_MERGE_DISTANCE = "orc_max_merge_distance"; + private static final String ORC_MAX_BUFFER_SIZE = "orc_max_buffer_size"; + private static final String ORC_STREAM_BUFFER_SIZE = "orc_stream_buffer_size"; + + private HiveSessionProperties() + { + } + + public static HiveStorageFormat getHiveStorageFormat(ConnectorSession session, HiveStorageFormat defaultValue) + { + String storageFormatString = session.getProperties().get(STORAGE_FORMAT_PROPERTY); + if (storageFormatString == null) { + return defaultValue; + } + + try { + return HiveStorageFormat.valueOf(storageFormatString.toUpperCase()); + } + catch (IllegalArgumentException e) { + throw new PrestoException(INVALID_SESSION_PROPERTY, "Hive storage-format is invalid: " + storageFormatString); + } + } + + public static boolean isOptimizedReaderEnabled(ConnectorSession session, boolean defaultValue) + { + return isEnabled(OPTIMIZED_READER_ENABLED, session, defaultValue); + } + + public static DataSize getOrcMaxMergeDistance(ConnectorSession session, DataSize defaultValue) + { + String maxMergeDistanceString = session.getProperties().get(ORC_MAX_MERGE_DISTANCE); + if (maxMergeDistanceString == null) { + return defaultValue; + } + + try { + return DataSize.valueOf(maxMergeDistanceString); + } + catch (IllegalArgumentException e) { + throw new PrestoException(INVALID_SESSION_PROPERTY, ORC_MAX_MERGE_DISTANCE + " is invalid: " + maxMergeDistanceString); + } + } + + public static DataSize getOrcMaxBufferSize(ConnectorSession session, DataSize defaultValue) + { + String maxBufferSizeString = session.getProperties().get(ORC_MAX_BUFFER_SIZE); + if (maxBufferSizeString == null) { + return defaultValue; + } + + try { + return DataSize.valueOf(maxBufferSizeString); + } + catch (IllegalArgumentException e) { + throw new PrestoException(INVALID_SESSION_PROPERTY, ORC_MAX_BUFFER_SIZE + " is invalid: " + maxBufferSizeString); + } + } + + public static DataSize getOrcStreamBufferSize(ConnectorSession session, DataSize defaultValue) + { + String streamBufferSizeString = session.getProperties().get(ORC_STREAM_BUFFER_SIZE); + if (streamBufferSizeString == null) { + return defaultValue; + } + + try { + return DataSize.valueOf(streamBufferSizeString); + } + catch (IllegalArgumentException e) { + throw new PrestoException(INVALID_SESSION_PROPERTY, ORC_STREAM_BUFFER_SIZE + " is invalid: " + streamBufferSizeString); + } + } + + private static boolean isEnabled(String propertyName, ConnectorSession session, boolean defaultValue) + { + String enabled = session.getProperties().get(propertyName); + if (enabled == null) { + return defaultValue; + } + + return Boolean.valueOf(enabled); + } + + public static boolean getForceLocalScheduling(ConnectorSession session, boolean defaultValue) + { + String forceLocalScheduling = session.getProperties().get(FORCE_LOCAL_SCHEDULING); + if (forceLocalScheduling == null) { + return defaultValue; + } + + try { + return Boolean.valueOf(forceLocalScheduling); + } + catch (IllegalArgumentException e) { + throw new PrestoException(NOT_SUPPORTED, "Invalid Hive session property '" + FORCE_LOCAL_SCHEDULING + "=" + forceLocalScheduling + "'"); + } + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/HiveSplit.java b/presto-hive/src/main/java/com/facebook/presto/hive/HiveSplit.java new file mode 100644 index 00000000..e9ff162c --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/HiveSplit.java @@ -0,0 +1,203 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.HostAddress; +import com.facebook.presto.spi.TupleDomain; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +import java.util.List; +import java.util.Properties; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class HiveSplit + implements ConnectorSplit +{ + private final String clientId; + private final String path; + private final long start; + private final long length; + private final Properties schema; + private final List partitionKeys; + private final List addresses; + private final String database; + private final String table; + private final String partitionName; + private final ConnectorSession session; + private final TupleDomain effectivePredicate; + private final boolean forceLocalScheduling; + + @JsonCreator + public HiveSplit( + @JsonProperty("clientId") String clientId, + @JsonProperty("database") String database, + @JsonProperty("table") String table, + @JsonProperty("partitionName") String partitionName, + @JsonProperty("path") String path, + @JsonProperty("start") long start, + @JsonProperty("length") long length, + @JsonProperty("schema") Properties schema, + @JsonProperty("partitionKeys") List partitionKeys, + @JsonProperty("addresses") List addresses, + @JsonProperty("forceLocalScheduling") boolean forceLocalScheduling, + @JsonProperty("session") ConnectorSession session, + @JsonProperty("effectivePredicate") TupleDomain effectivePredicate) + { + checkNotNull(clientId, "clientId is null"); + checkArgument(start >= 0, "start must be positive"); + checkArgument(length >= 0, "length must be positive"); + checkNotNull(database, "database is null"); + checkNotNull(table, "table is null"); + checkNotNull(partitionName, "partitionName is null"); + checkNotNull(path, "path is null"); + checkNotNull(schema, "schema is null"); + checkNotNull(partitionKeys, "partitionKeys is null"); + checkNotNull(addresses, "addresses is null"); + checkNotNull(effectivePredicate, "tupleDomain is null"); + + this.clientId = clientId; + this.database = database; + this.table = table; + this.partitionName = partitionName; + this.path = path; + this.start = start; + this.length = length; + this.schema = schema; + this.partitionKeys = ImmutableList.copyOf(partitionKeys); + this.addresses = ImmutableList.copyOf(addresses); + this.forceLocalScheduling = forceLocalScheduling; + this.session = session; + this.effectivePredicate = effectivePredicate; + } + + @JsonProperty + public String getClientId() + { + return clientId; + } + + @JsonProperty + public String getDatabase() + { + return database; + } + + @JsonProperty + public String getTable() + { + return table; + } + + @JsonProperty + public String getPartitionName() + { + return partitionName; + } + + @JsonProperty + public String getPath() + { + return path; + } + + @JsonProperty + public long getStart() + { + return start; + } + + @JsonProperty + public long getLength() + { + return length; + } + + @JsonProperty + public Properties getSchema() + { + return schema; + } + + @JsonProperty + public List getPartitionKeys() + { + return partitionKeys; + } + + @JsonProperty + @Override + public List getAddresses() + { + return addresses; + } + + @JsonProperty + public ConnectorSession getSession() + { + return session; + } + + @JsonProperty + public TupleDomain getEffectivePredicate() + { + return effectivePredicate; + } + + @JsonProperty + public boolean isForceLocalScheduling() + { + return forceLocalScheduling; + } + + @Override + public boolean isRemotelyAccessible() + { + return !forceLocalScheduling; + } + + @Override + public Object getInfo() + { + return ImmutableMap.builder() + .put("path", path) + .put("start", start) + .put("length", length) + .put("hosts", addresses) + .put("database", database) + .put("table", table) + .put("forceLocalScheduling", forceLocalScheduling) + .put("partitionName", partitionName) + .put("effectivePredicate", effectivePredicate) + .build(); + } + + @Override + public String toString() + { + return toStringHelper(this) + .addValue(path) + .addValue(start) + .addValue(length) + .addValue(effectivePredicate) + .toString(); + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/HiveSplitLoader.java b/presto-hive/src/main/java/com/facebook/presto/hive/HiveSplitLoader.java new file mode 100644 index 00000000..daf85741 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/HiveSplitLoader.java @@ -0,0 +1,23 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +interface HiveSplitLoader +{ + void start(HiveSplitSource splitSource); + + void resume(); + + void stop(); +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/HiveSplitManager.java b/presto-hive/src/main/java/com/facebook/presto/hive/HiveSplitManager.java new file mode 100644 index 00000000..cae5f12f --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/HiveSplitManager.java @@ -0,0 +1,547 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.hive.metastore.HiveMetastore; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorPartition; +import com.facebook.presto.spi.ConnectorPartitionResult; +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.ConnectorSplitManager; +import com.facebook.presto.spi.ConnectorSplitSource; +import com.facebook.presto.spi.ConnectorTableHandle; +import com.facebook.presto.spi.Domain; +import com.facebook.presto.spi.FixedSplitSource; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.spi.SerializableNativeValue; +import com.facebook.presto.spi.SortedRangeSet; +import com.facebook.presto.spi.TableNotFoundException; +import com.facebook.presto.spi.TupleDomain; +import com.google.common.base.Function; +import com.google.common.base.Predicates; +import com.google.common.base.Throwables; +import com.google.common.collect.AbstractIterator; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Ordering; +import io.airlift.concurrent.BoundedExecutor; +import io.airlift.log.Logger; +import io.airlift.slice.Slice; +import io.airlift.units.DataSize; +import org.apache.hadoop.hive.common.FileUtils; +import org.apache.hadoop.hive.metastore.ProtectMode; +import org.apache.hadoop.hive.metastore.api.FieldSchema; +import org.apache.hadoop.hive.metastore.api.MetaException; +import org.apache.hadoop.hive.metastore.api.NoSuchObjectException; +import org.apache.hadoop.hive.metastore.api.Partition; +import org.apache.hadoop.hive.metastore.api.Table; +import org.joda.time.DateTimeZone; + +import javax.inject.Inject; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; + +import static com.facebook.presto.hive.HiveBucketing.getHiveBucket; +import static com.facebook.presto.hive.HiveErrorCode.HIVE_INVALID_METADATA; +import static com.facebook.presto.hive.HiveErrorCode.HIVE_PARTITION_SCHEMA_MISMATCH; +import static com.facebook.presto.hive.HivePartition.UNPARTITIONED_ID; +import static com.facebook.presto.hive.HiveUtil.getPartitionKeyColumnHandles; +import static com.facebook.presto.hive.HiveUtil.parsePartitionValue; +import static com.facebook.presto.hive.HiveUtil.schemaTableName; +import static com.facebook.presto.hive.UnpartitionedPartition.UNPARTITIONED_PARTITION; +import static com.facebook.presto.hive.util.Types.checkType; +import static com.facebook.presto.spi.StandardErrorCode.NOT_SUPPORTED; +import static com.facebook.presto.spi.StandardErrorCode.SERVER_SHUTTING_DOWN; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Predicates.not; +import static com.google.common.base.Strings.isNullOrEmpty; +import static com.google.common.collect.Iterables.concat; +import static com.google.common.collect.Iterables.getOnlyElement; +import static com.google.common.collect.Iterables.transform; +import static java.lang.Math.min; +import static java.lang.String.format; +import static org.apache.hadoop.hive.metastore.ProtectMode.getProtectModeFromString; +import static org.apache.hadoop.hive.metastore.Warehouse.makePartName; + +public class HiveSplitManager + implements ConnectorSplitManager +{ + public static final String PRESTO_OFFLINE = "presto_offline"; + private static final String PARTITION_VALUE_WILDCARD = ""; + + private static final Logger log = Logger.get(HiveSplitManager.class); + + private final String connectorId; + private final HiveMetastore metastore; + private final NamenodeStats namenodeStats; + private final HdfsEnvironment hdfsEnvironment; + private final DirectoryLister directoryLister; + private final DateTimeZone timeZone; + private final Executor executor; + private final int maxOutstandingSplits; + private final int minPartitionBatchSize; + private final int maxPartitionBatchSize; + private final DataSize maxSplitSize; + private final DataSize maxInitialSplitSize; + private final int maxInitialSplits; + private final boolean forceLocalScheduling; + private final boolean recursiveDfsWalkerEnabled; + private final boolean assumeCanonicalPartitionKeys; + + @Inject + public HiveSplitManager( + HiveConnectorId connectorId, + HiveClientConfig hiveClientConfig, + HiveMetastore metastore, + NamenodeStats namenodeStats, + HdfsEnvironment hdfsEnvironment, + DirectoryLister directoryLister, + @ForHiveClient ExecutorService executorService) + { + this(connectorId, + metastore, + namenodeStats, + hdfsEnvironment, + directoryLister, + DateTimeZone.forTimeZone(hiveClientConfig.getTimeZone()), + new BoundedExecutor(executorService, hiveClientConfig.getMaxSplitIteratorThreads()), + hiveClientConfig.getMaxOutstandingSplits(), + hiveClientConfig.getMinPartitionBatchSize(), + hiveClientConfig.getMaxPartitionBatchSize(), + hiveClientConfig.getMaxSplitSize(), + hiveClientConfig.getMaxInitialSplitSize(), + hiveClientConfig.getMaxInitialSplits(), + hiveClientConfig.isForceLocalScheduling(), + hiveClientConfig.isAssumeCanonicalPartitionKeys(), + hiveClientConfig.getRecursiveDirWalkerEnabled()); + } + + public HiveSplitManager( + HiveConnectorId connectorId, + HiveMetastore metastore, + NamenodeStats namenodeStats, + HdfsEnvironment hdfsEnvironment, + DirectoryLister directoryLister, + DateTimeZone timeZone, + Executor executor, + int maxOutstandingSplits, + int minPartitionBatchSize, + int maxPartitionBatchSize, + DataSize maxSplitSize, + DataSize maxInitialSplitSize, + int maxInitialSplits, + boolean forceLocalScheduling, + boolean assumeCanonicalPartitionKeys, + boolean recursiveDfsWalkerEnabled) + { + this.connectorId = checkNotNull(connectorId, "connectorId is null").toString(); + this.metastore = checkNotNull(metastore, "metastore is null"); + this.namenodeStats = checkNotNull(namenodeStats, "namenodeStats is null"); + this.hdfsEnvironment = checkNotNull(hdfsEnvironment, "hdfsEnvironment is null"); + this.directoryLister = checkNotNull(directoryLister, "directoryLister is null"); + this.timeZone = checkNotNull(timeZone, "timeZone is null"); + this.executor = new ErrorCodedExecutor(executor); + checkArgument(maxOutstandingSplits >= 1, "maxOutstandingSplits must be at least 1"); + this.maxOutstandingSplits = maxOutstandingSplits; + this.minPartitionBatchSize = minPartitionBatchSize; + this.maxPartitionBatchSize = maxPartitionBatchSize; + this.maxSplitSize = checkNotNull(maxSplitSize, "maxSplitSize is null"); + this.maxInitialSplitSize = checkNotNull(maxInitialSplitSize, "maxInitialSplitSize is null"); + this.maxInitialSplits = maxInitialSplits; + this.forceLocalScheduling = forceLocalScheduling; + this.recursiveDfsWalkerEnabled = recursiveDfsWalkerEnabled; + this.assumeCanonicalPartitionKeys = assumeCanonicalPartitionKeys; + } + + @Override + public ConnectorPartitionResult getPartitions(ConnectorTableHandle tableHandle, TupleDomain effectivePredicate) + { + checkNotNull(tableHandle, "tableHandle is null"); + checkNotNull(effectivePredicate, "effectivePredicate is null"); + + if (effectivePredicate.isNone()) { + return new ConnectorPartitionResult(ImmutableList.of(), TupleDomain.none()); + } + + SchemaTableName tableName = schemaTableName(tableHandle); + Table table = getTable(tableName); + Optional bucket = getHiveBucket(table, effectivePredicate.extractFixedValues()); + + TupleDomain compactEffectivePredicate = toCompactTupleDomain(effectivePredicate); + + if (table.getPartitionKeys().isEmpty()) { + return new ConnectorPartitionResult(ImmutableList.of(new HivePartition(tableName, compactEffectivePredicate, bucket)), effectivePredicate); + } + + List partitionColumns = getPartitionKeyColumnHandles(connectorId, table, 0); + List partitionNames = getFilteredPartitionNames(tableName, partitionColumns, effectivePredicate); + + // do a final pass to filter based on fields that could not be used to filter the partitions + ImmutableList.Builder partitions = ImmutableList.builder(); + for (String partitionName : partitionNames) { + Optional> values = parseValuesAndFilterPartition(partitionName, partitionColumns, effectivePredicate); + + if (values.isPresent()) { + partitions.add(new HivePartition(tableName, compactEffectivePredicate, partitionName, values.get(), bucket)); + } + } + + // All partition key domains will be fully evaluated, so we don't need to include those + TupleDomain remainingTupleDomain = TupleDomain.withColumnDomains(Maps.filterKeys(effectivePredicate.getDomains(), not(Predicates.in(partitionColumns)))); + return new ConnectorPartitionResult(partitions.build(), remainingTupleDomain); + } + + private static TupleDomain toCompactTupleDomain(TupleDomain effectivePredicate) + { + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (Map.Entry entry : effectivePredicate.getDomains().entrySet()) { + HiveColumnHandle hiveColumnHandle = checkType(entry.getKey(), HiveColumnHandle.class, "ConnectorColumnHandle"); + + SortedRangeSet ranges = entry.getValue().getRanges(); + if (!ranges.isNone()) { + // compact the range to a single span + ranges = SortedRangeSet.of(entry.getValue().getRanges().getSpan()); + } + + builder.put(hiveColumnHandle, new Domain(ranges, entry.getValue().isNullAllowed())); + } + return TupleDomain.withColumnDomains(builder.build()); + } + + private Optional> parseValuesAndFilterPartition(String partitionName, List partitionColumns, TupleDomain predicate) + { + List partitionValues = extractPartitionKeyValues(partitionName); + + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (int i = 0; i < partitionColumns.size(); i++) { + HiveColumnHandle column = partitionColumns.get(i); + SerializableNativeValue parsedValue = parsePartitionValue(partitionName, partitionValues.get(i), column.getHiveType(), timeZone); + + Domain allowedDomain = predicate.getDomains().get(column); + if (allowedDomain != null && !allowedDomain.includesValue(parsedValue.getValue())) { + return Optional.empty(); + } + builder.put(column, parsedValue); + } + + return Optional.of(builder.build()); + } + + private Table getTable(SchemaTableName tableName) + { + try { + Table table = metastore.getTable(tableName.getSchemaName(), tableName.getTableName()); + + String protectMode = table.getParameters().get(ProtectMode.PARAMETER_NAME); + if (protectMode != null && getProtectModeFromString(protectMode).offline) { + throw new TableOfflineException(tableName); + } + + String prestoOffline = table.getParameters().get(PRESTO_OFFLINE); + if (!isNullOrEmpty(prestoOffline)) { + throw new TableOfflineException(tableName, format("Table '%s' is offline for Presto: %s", tableName, prestoOffline)); + } + + return table; + } + catch (NoSuchObjectException e) { + throw new TableNotFoundException(tableName); + } + } + + private List getFilteredPartitionNames(SchemaTableName tableName, List partitionKeys, TupleDomain effectivePredicate) + { + List filter = new ArrayList<>(); + for (HiveColumnHandle partitionKey : partitionKeys) { + Domain domain = effectivePredicate.getDomains().get(partitionKey); + if (domain != null && domain.isNullableSingleValue()) { + Comparable value = domain.getNullableSingleValue(); + if (value == null) { + filter.add(HivePartitionKey.HIVE_DEFAULT_DYNAMIC_PARTITION); + } + else if (value instanceof Slice) { + filter.add(((Slice) value).toStringUtf8()); + } + else if ((value instanceof Boolean) || (value instanceof Double) || (value instanceof Long)) { + if (assumeCanonicalPartitionKeys) { + filter.add(value.toString()); + } + else { + // Hive treats '0', 'false', and 'False' the same. However, the metastore differentiates between these. + filter.add(PARTITION_VALUE_WILDCARD); + } + } + else { + throw new PrestoException(NOT_SUPPORTED, "Only Boolean, Double and Long partition keys are supported"); + } + } + else { + filter.add(PARTITION_VALUE_WILDCARD); + } + } + + try { + // fetch the partition names + return metastore.getPartitionNamesByParts(tableName.getSchemaName(), tableName.getTableName(), filter); + } + catch (NoSuchObjectException e) { + throw new TableNotFoundException(tableName); + } + } + + private static List extractPartitionKeyValues(String partitionName) + { + ImmutableList.Builder values = ImmutableList.builder(); + + boolean inKey = true; + int valueStart = -1; + for (int i = 0; i < partitionName.length(); i++) { + char current = partitionName.charAt(i); + if (inKey) { + checkArgument(current != '/', "Invalid partition spec: %s", partitionName); + if (current == '=') { + inKey = false; + valueStart = i + 1; + } + } + else if (current == '/') { + checkArgument(valueStart != -1, "Invalid partition spec: %s", partitionName); + values.add(FileUtils.unescapePathName(partitionName.substring(valueStart, i))); + inKey = true; + valueStart = -1; + } + } + checkArgument(!inKey, "Invalid partition spec: %s", partitionName); + values.add(FileUtils.unescapePathName(partitionName.substring(valueStart, partitionName.length()))); + + return values.build(); + } + + @Override + public ConnectorSplitSource getPartitionSplits(ConnectorTableHandle tableHandle, List connectorPartitions) + { + HiveTableHandle hiveTableHandle = checkType(tableHandle, HiveTableHandle.class, "tableHandle"); + + checkNotNull(connectorPartitions, "connectorPartitions is null"); + List partitions = Lists.transform(connectorPartitions, partition -> checkType(partition, HivePartition.class, "partition")); + + HivePartition partition = Iterables.getFirst(partitions, null); + if (partition == null) { + return new FixedSplitSource(connectorId, ImmutableList.of()); + } + SchemaTableName tableName = partition.getTableName(); + Optional bucket = partition.getBucket(); + + // sort partitions + partitions = Ordering.natural().onResultOf(ConnectorPartition::getPartitionId).reverse().sortedCopy(partitions); + + Table table; + Iterable hivePartitions; + try { + table = metastore.getTable(tableName.getSchemaName(), tableName.getTableName()); + hivePartitions = getPartitionMetadata(table, tableName, partitions); + } + catch (NoSuchObjectException e) { + throw new TableNotFoundException(tableName); + } + + HiveSplitLoader hiveSplitLoader = new BackgroundHiveSplitLoader( + connectorId, + table, + hivePartitions, + bucket, + maxSplitSize, + hiveTableHandle.getSession(), + hdfsEnvironment, + namenodeStats, + directoryLister, + executor, + maxPartitionBatchSize, + maxInitialSplitSize, + maxInitialSplits, + forceLocalScheduling, + recursiveDfsWalkerEnabled); + + HiveSplitSource splitSource = new HiveSplitSource(connectorId, maxOutstandingSplits, hiveSplitLoader); + hiveSplitLoader.start(splitSource); + + return splitSource; + } + + private Iterable getPartitionMetadata(Table table, SchemaTableName tableName, List partitions) + throws NoSuchObjectException + { + if (partitions.isEmpty()) { + return ImmutableList.of(); + } + + if (partitions.size() == 1) { + HivePartition firstPartition = getOnlyElement(partitions); + if (firstPartition.getPartitionId().equals(UNPARTITIONED_ID)) { + return ImmutableList.of(new HivePartitionMetadata(firstPartition, UNPARTITIONED_PARTITION)); + } + } + + Iterable> partitionNameBatches = partitionExponentially(partitions, minPartitionBatchSize, maxPartitionBatchSize); + Iterable> partitionBatches = transform(partitionNameBatches, new Function, List>() + { + @Override + public List apply(List partitionBatch) + { + Exception exception = null; + for (int attempt = 0; attempt < 10; attempt++) { + try { + Map partitions = metastore.getPartitionsByNames( + tableName.getSchemaName(), + tableName.getTableName(), + Lists.transform(partitionBatch, ConnectorPartition::getPartitionId)); + checkState(partitionBatch.size() == partitions.size(), "expected %s partitions but found %s", partitionBatch.size(), partitions.size()); + + ImmutableList.Builder results = ImmutableList.builder(); + for (HivePartition hivePartition : partitionBatch) { + Partition partition = partitions.get(hivePartition.getPartitionId()); + checkState(partition != null, "Partition %s was not loaded", hivePartition.getPartitionId()); + + // verify all partition is online + String protectMode = partition.getParameters().get(ProtectMode.PARAMETER_NAME); + String partName = makePartName(table.getPartitionKeys(), partition.getValues()); + if (protectMode != null && getProtectModeFromString(protectMode).offline) { + throw new PartitionOfflineException(tableName, partName); + } + String prestoOffline = partition.getParameters().get(PRESTO_OFFLINE); + if (!isNullOrEmpty(prestoOffline)) { + throw new PartitionOfflineException(tableName, partName, format("Partition '%s' is offline for Presto: %s", partName, prestoOffline)); + } + + // Verify that the partition schema matches the table schema. + // Either adding or dropping columns from the end of the table + // without modifying existing partitions is allowed, but every + // column that exists in both the table and partition must have + // the same type. + List tableColumns = table.getSd().getCols(); + List partitionColumns = partition.getSd().getCols(); + if ((tableColumns == null) || (partitionColumns == null)) { + throw new PrestoException(HIVE_INVALID_METADATA, format("Table '%s' or partition '%s' has null columns", tableName, partName)); + } + for (int i = 0; i < min(partitionColumns.size(), tableColumns.size()); i++) { + String tableType = tableColumns.get(i).getType(); + String partitionType = partitionColumns.get(i).getType(); + if (!tableType.equals(partitionType)) { + throw new PrestoException(HIVE_PARTITION_SCHEMA_MISMATCH, format("" + + "There is a mismatch between the table and partition schemas. " + + "The column '%s' in table '%s' is declared as type '%s', " + + "but partition '%s' declared column '%s' as type '%s'.", + tableColumns.get(i).getName(), + tableName, + tableType, + partName, + partitionColumns.get(i).getName(), + partitionType)); + } + } + + results.add(new HivePartitionMetadata(hivePartition, partition)); + } + + return results.build(); + } + catch (PrestoException | NoSuchObjectException | NullPointerException | IllegalStateException | IllegalArgumentException e) { + throw Throwables.propagate(e); + } + catch (MetaException | RuntimeException e) { + exception = e; + log.debug("getPartitions attempt %s failed, will retry. Exception: %s", attempt, e.getMessage()); + } + + try { + TimeUnit.SECONDS.sleep(1); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw Throwables.propagate(e); + } + } + assert exception != null; // impossible + throw Throwables.propagate(exception); + } + }); + return concat(partitionBatches); + } + + /** + * Partition the given list in exponentially (power of 2) increasing batch sizes starting at 1 up to maxBatchSize + */ + private static Iterable> partitionExponentially(List values, int minBatchSize, int maxBatchSize) + { + return () -> new AbstractIterator>() + { + private int currentSize = minBatchSize; + private final Iterator iterator = values.iterator(); + + @Override + protected List computeNext() + { + if (!iterator.hasNext()) { + return endOfData(); + } + + int count = 0; + ImmutableList.Builder builder = ImmutableList.builder(); + while (iterator.hasNext() && count < currentSize) { + builder.add(iterator.next()); + ++count; + } + + currentSize = min(maxBatchSize, currentSize * 2); + return builder.build(); + } + }; + } + + private static class ErrorCodedExecutor + implements Executor + { + private final Executor delegate; + + private ErrorCodedExecutor(Executor delegate) + { + this.delegate = checkNotNull(delegate, "delegate is null"); + } + + @Override + public void execute(Runnable command) + { + try { + delegate.execute(command); + } + catch (RejectedExecutionException e) { + throw new PrestoException(SERVER_SHUTTING_DOWN, "Server is shutting down", e); + } + } + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/HiveSplitSource.java b/presto-hive/src/main/java/com/facebook/presto/hive/HiveSplitSource.java new file mode 100644 index 00000000..3293b524 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/HiveSplitSource.java @@ -0,0 +1,158 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.hive.util.AsyncQueue; +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.ConnectorSplitSource; +import com.facebook.presto.spi.PrestoException; + +import java.io.FileNotFoundException; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import static com.facebook.presto.hive.HiveErrorCode.HIVE_FILE_NOT_FOUND; +import static com.facebook.presto.hive.HiveErrorCode.HIVE_UNKNOWN_ERROR; +import static com.google.common.base.Preconditions.checkState; +import static io.airlift.concurrent.MoreFutures.failedFuture; + +class HiveSplitSource + implements ConnectorSplitSource +{ + private final String connectorId; + private final AsyncQueue queue = new AsyncQueue<>(); + private final AtomicInteger outstandingSplitCount = new AtomicInteger(); + private final AtomicReference throwable = new AtomicReference<>(); + private final int maxOutstandingSplits; + private final HiveSplitLoader splitLoader; + private volatile boolean closed; + + HiveSplitSource(String connectorId, int maxOutstandingSplits, HiveSplitLoader splitLoader) + { + this.connectorId = connectorId; + this.maxOutstandingSplits = maxOutstandingSplits; + this.splitLoader = splitLoader; + } + + int getOutstandingSplitCount() + { + return outstandingSplitCount.get(); + } + + void addToQueue(Iterable splits) + { + for (ConnectorSplit split : splits) { + addToQueue(split); + } + } + + void addToQueue(ConnectorSplit split) + { + if (throwable.get() == null) { + outstandingSplitCount.incrementAndGet(); + queue.add(split); + } + } + + boolean isQueueFull() + { + return outstandingSplitCount.get() >= maxOutstandingSplits; + } + + void finished() + { + if (throwable.get() == null) { + queue.finish(); + splitLoader.stop(); + } + } + + void fail(Throwable e) + { + // only record the first error message + if (throwable.compareAndSet(null, e)) { + // add finish the queue + queue.finish(); + + // no need to process any more jobs + splitLoader.stop(); + } + } + + @Override + public String getDataSourceName() + { + return connectorId; + } + + @Override + public CompletableFuture> getNextBatch(int maxSize) + { + checkState(!closed, "Provider is already closed"); + + CompletableFuture> future = queue.getBatchAsync(maxSize); + + // Before returning, check if there is a registered failure. + // If so, we want to throw the error, instead of returning because the scheduler can block + // while scheduling splits and wait for work to finish before continuing. In this case, + // we want to end the query as soon as possible and abort the work + if (throwable.get() != null) { + return failedFuture(throwable.get()); + } + + // when future completes, decrement the outstanding split count by the number of splits we took + future.thenAccept(splits -> { + if (outstandingSplitCount.addAndGet(-splits.size()) < maxOutstandingSplits) { + // we are below the low water mark (and there isn't a failure) so resume scanning hdfs + splitLoader.resume(); + } + }); + + return future; + } + + @Override + public boolean isFinished() + { + // the finished marker must be checked before checking the throwable + // to avoid a race with the fail method + boolean isFinished = queue.isFinished(); + if (throwable.get() != null) { + throw propagatePrestoException(throwable.get()); + } + return isFinished; + } + + @Override + public void close() + { + queue.finish(); + splitLoader.stop(); + + closed = true; + } + + private static RuntimeException propagatePrestoException(Throwable throwable) + { + if (throwable instanceof PrestoException) { + throw (PrestoException) throwable; + } + if (throwable instanceof FileNotFoundException) { + throw new PrestoException(HIVE_FILE_NOT_FOUND, throwable); + } + throw new PrestoException(HIVE_UNKNOWN_ERROR, throwable); + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/HiveStorageFormat.java b/presto-hive/src/main/java/com/facebook/presto/hive/HiveStorageFormat.java new file mode 100644 index 00000000..3000475c --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/HiveStorageFormat.java @@ -0,0 +1,83 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat; +import org.apache.hadoop.hive.ql.io.HiveSequenceFileOutputFormat; +import org.apache.hadoop.hive.ql.io.RCFileInputFormat; +import org.apache.hadoop.hive.ql.io.RCFileOutputFormat; +import org.apache.hadoop.hive.ql.io.orc.OrcInputFormat; +import org.apache.hadoop.hive.ql.io.orc.OrcOutputFormat; +import org.apache.hadoop.hive.ql.io.orc.OrcSerde; +import org.apache.hadoop.hive.ql.io.parquet.MapredParquetInputFormat; +import org.apache.hadoop.hive.ql.io.parquet.MapredParquetOutputFormat; +import org.apache.hadoop.hive.ql.io.parquet.serde.ParquetHiveSerDe; +import org.apache.hadoop.hive.serde2.columnar.ColumnarSerDe; +import org.apache.hadoop.hive.serde2.columnar.LazyBinaryColumnarSerDe; +import org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe; +import org.apache.hadoop.mapred.SequenceFileInputFormat; +import org.apache.hadoop.mapred.TextInputFormat; + +import static com.google.common.base.Preconditions.checkNotNull; + +public enum HiveStorageFormat +{ + ORC(OrcSerde.class.getName(), + OrcInputFormat.class.getName(), + OrcOutputFormat.class.getName()), + DWRF(com.facebook.hive.orc.OrcSerde.class.getName(), + com.facebook.hive.orc.OrcInputFormat.class.getName(), + com.facebook.hive.orc.OrcOutputFormat.class.getName()), + PARQUET(ParquetHiveSerDe.class.getName(), + MapredParquetInputFormat.class.getName(), + MapredParquetOutputFormat.class.getName()), + RCBINARY(LazyBinaryColumnarSerDe.class.getName(), + RCFileInputFormat.class.getName(), + RCFileOutputFormat.class.getName()), + RCTEXT(ColumnarSerDe.class.getName(), + RCFileInputFormat.class.getName(), + RCFileOutputFormat.class.getName()), + SEQUENCEFILE(LazySimpleSerDe.class.getName(), + SequenceFileInputFormat.class.getName(), + HiveSequenceFileOutputFormat.class.getName()), + TEXTFILE(LazySimpleSerDe.class.getName(), + TextInputFormat.class.getName(), + HiveIgnoreKeyTextOutputFormat.class.getName()); + + private final String serde; + private final String inputFormat; + private final String outputFormat; + + HiveStorageFormat(String serde, String inputFormat, String outputFormat) + { + this.serde = checkNotNull(serde, "serde is null"); + this.inputFormat = checkNotNull(inputFormat, "inputFormat is null"); + this.outputFormat = checkNotNull(outputFormat, "outputFormat is null"); + } + + public String getSerDe() + { + return serde; + } + + public String getInputFormat() + { + return inputFormat; + } + + public String getOutputFormat() + { + return outputFormat; + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/HiveTableHandle.java b/presto-hive/src/main/java/com/facebook/presto/hive/HiveTableHandle.java new file mode 100644 index 00000000..d3b84886 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/HiveTableHandle.java @@ -0,0 +1,102 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.ConnectorTableHandle; +import com.facebook.presto.spi.SchemaTableName; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class HiveTableHandle + implements ConnectorTableHandle +{ + private final String clientId; + private final String schemaName; + private final String tableName; + private final ConnectorSession session; + + @JsonCreator + public HiveTableHandle( + @JsonProperty("clientId") String clientId, + @JsonProperty("schemaName") String schemaName, + @JsonProperty("tableName") String tableName, + @JsonProperty("session") ConnectorSession session) + { + this.clientId = checkNotNull(clientId, "clientId is null"); + this.schemaName = checkNotNull(schemaName, "schemaName is null"); + this.tableName = checkNotNull(tableName, "tableName is null"); + this.session = checkNotNull(session, "session is null"); + } + + @JsonProperty + public String getClientId() + { + return clientId; + } + + @JsonProperty + public String getSchemaName() + { + return schemaName; + } + + @JsonProperty + public ConnectorSession getSession() + { + return session; + } + + @JsonProperty + public String getTableName() + { + return tableName; + } + + public SchemaTableName getSchemaTableName() + { + return new SchemaTableName(schemaName, tableName); + } + + @Override + public int hashCode() + { + return Objects.hash(clientId, schemaName, tableName); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + HiveTableHandle other = (HiveTableHandle) obj; + return Objects.equals(this.clientId, other.clientId) && + Objects.equals(this.schemaName, other.schemaName) && + Objects.equals(this.tableName, other.tableName); + } + + @Override + public String toString() + { + return clientId + ":" + schemaName + ":" + tableName; + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/HiveType.java b/presto-hive/src/main/java/com/facebook/presto/hive/HiveType.java new file mode 100644 index 00000000..3740ba1a --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/HiveType.java @@ -0,0 +1,312 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.type.BigintType; +import com.facebook.presto.spi.type.BooleanType; +import com.facebook.presto.spi.type.DateType; +import com.facebook.presto.spi.type.DoubleType; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.TimestampType; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.spi.type.TypeSignature; +import com.facebook.presto.spi.type.VarbinaryType; +import com.facebook.presto.spi.type.VarcharType; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import org.apache.hadoop.hive.serde2.objectinspector.ListObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.MapObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.PrimitiveObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.StructField; +import org.apache.hadoop.hive.serde2.objectinspector.StructObjectInspector; +import org.apache.hadoop.hive.serde2.typeinfo.TypeInfoUtils; + +import javax.annotation.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import static com.facebook.presto.hive.HiveUtil.isArrayType; +import static com.facebook.presto.hive.HiveUtil.isMapType; +import static com.facebook.presto.hive.HiveUtil.isStructuralType; +import static com.facebook.presto.hive.util.Types.checkType; +import static com.facebook.presto.spi.StandardErrorCode.NOT_SUPPORTED; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.BooleanType.BOOLEAN; +import static com.facebook.presto.spi.type.DateType.DATE; +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; +import static com.facebook.presto.spi.type.TimestampType.TIMESTAMP; +import static com.facebook.presto.spi.type.VarbinaryType.VARBINARY; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.lang.String.format; +import static org.apache.hadoop.hive.serde.Constants.BIGINT_TYPE_NAME; +import static org.apache.hadoop.hive.serde.Constants.BINARY_TYPE_NAME; +import static org.apache.hadoop.hive.serde.Constants.BOOLEAN_TYPE_NAME; +import static org.apache.hadoop.hive.serde.Constants.DOUBLE_TYPE_NAME; +import static org.apache.hadoop.hive.serde.Constants.FLOAT_TYPE_NAME; +import static org.apache.hadoop.hive.serde.Constants.INT_TYPE_NAME; +import static org.apache.hadoop.hive.serde.Constants.SMALLINT_TYPE_NAME; +import static org.apache.hadoop.hive.serde.Constants.STRING_TYPE_NAME; +import static org.apache.hadoop.hive.serde.Constants.TIMESTAMP_TYPE_NAME; +import static org.apache.hadoop.hive.serde.Constants.TINYINT_TYPE_NAME; +import static org.apache.hadoop.hive.serde.serdeConstants.DATE_TYPE_NAME; +import static org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector.Category; + +public final class HiveType +{ + public static final HiveType HIVE_BOOLEAN = new HiveType(BOOLEAN_TYPE_NAME); + public static final HiveType HIVE_BYTE = new HiveType(TINYINT_TYPE_NAME); + public static final HiveType HIVE_SHORT = new HiveType(SMALLINT_TYPE_NAME); + public static final HiveType HIVE_INT = new HiveType(INT_TYPE_NAME); + public static final HiveType HIVE_LONG = new HiveType(BIGINT_TYPE_NAME); + public static final HiveType HIVE_FLOAT = new HiveType(FLOAT_TYPE_NAME); + public static final HiveType HIVE_DOUBLE = new HiveType(DOUBLE_TYPE_NAME); + public static final HiveType HIVE_STRING = new HiveType(STRING_TYPE_NAME); + public static final HiveType HIVE_TIMESTAMP = new HiveType(TIMESTAMP_TYPE_NAME); + public static final HiveType HIVE_DATE = new HiveType(DATE_TYPE_NAME); + public static final HiveType HIVE_BINARY = new HiveType(BINARY_TYPE_NAME); + + private static final Set SUPPORTED_HIVE_TYPES = ImmutableSet.of( + HIVE_BOOLEAN, + HIVE_BYTE, + HIVE_SHORT, + HIVE_INT, + HIVE_LONG, + HIVE_FLOAT, + HIVE_DOUBLE, + HIVE_STRING, + HIVE_TIMESTAMP, + HIVE_DATE, + HIVE_BINARY); + + private final String hiveTypeName; + private final Category category; + + private HiveType(String hiveTypeName) + { + this.hiveTypeName = checkNotNull(hiveTypeName, "hiveTypeName is null"); + this.category = TypeInfoUtils.getTypeInfoFromTypeString(hiveTypeName).getCategory(); + } + + @JsonValue + public String getHiveTypeName() + { + return hiveTypeName; + } + + public Category getCategory() + { + return category; + } + + public static HiveType getSupportedHiveType(String hiveTypeName) + { + HiveType hiveType = getHiveType(hiveTypeName); + checkArgument(hiveType != null, "Unknown Hive type: " + hiveTypeName); + return hiveType; + } + + @JsonCreator + @Nullable + public static HiveType getHiveType(String hiveTypeName) + { + HiveType hiveType = new HiveType(hiveTypeName); + if (!isStructuralType(hiveType) && !SUPPORTED_HIVE_TYPES.contains(hiveType)) { + return null; + } + return hiveType; + } + + public static HiveType getSupportedHiveType(ObjectInspector fieldInspector) + { + HiveType hiveType = getHiveType(fieldInspector); + checkArgument(hiveType != null, "Unknown Hive category: " + fieldInspector.getCategory()); + return hiveType; + } + + public static HiveType getHiveType(ObjectInspector fieldInspector) + { + return getHiveType(fieldInspector.getTypeName()); + } + + public static HiveType toHiveType(Type type) + { + if (BooleanType.BOOLEAN.equals(type)) { + return HIVE_BOOLEAN; + } + if (BigintType.BIGINT.equals(type)) { + return HIVE_LONG; + } + if (DoubleType.DOUBLE.equals(type)) { + return HIVE_DOUBLE; + } + if (VarcharType.VARCHAR.equals(type)) { + return HIVE_STRING; + } + if (VarbinaryType.VARBINARY.equals(type)) { + return HIVE_BINARY; + } + if (DateType.DATE.equals(type)) { + return HIVE_DATE; + } + if (TimestampType.TIMESTAMP.equals(type)) { + return HIVE_TIMESTAMP; + } + if (isArrayType(type)) { + HiveType hiveElementType = toHiveType(type.getTypeParameters().get(0)); + return new HiveType(format("array<%s>", hiveElementType.getHiveTypeName())); + } + if (isMapType(type)) { + HiveType hiveKeyType = toHiveType(type.getTypeParameters().get(0)); + HiveType hiveValueType = toHiveType(type.getTypeParameters().get(1)); + return new HiveType(format("map<%s,%s>", hiveKeyType.getHiveTypeName(), hiveValueType.getHiveTypeName())); + } + throw new PrestoException(NOT_SUPPORTED, "unsupported type: " + type); + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + HiveType hiveType = (HiveType) o; + + if (!hiveTypeName.equals(hiveType.hiveTypeName)) { + return false; + } + + return true; + } + + @Override + public int hashCode() + { + return hiveTypeName.hashCode(); + } + + @Override + public String toString() + { + return hiveTypeName; + } + + public static Type getType(String hiveType) + { + switch (hiveType) { + case BOOLEAN_TYPE_NAME: + return BOOLEAN; + case TINYINT_TYPE_NAME: + case SMALLINT_TYPE_NAME: + case INT_TYPE_NAME: + case BIGINT_TYPE_NAME: + return BIGINT; + case FLOAT_TYPE_NAME: + case DOUBLE_TYPE_NAME: + return DOUBLE; + case STRING_TYPE_NAME: + return VARCHAR; + case DATE_TYPE_NAME: + return DATE; + case TIMESTAMP_TYPE_NAME: + return TIMESTAMP; + case BINARY_TYPE_NAME: + return VARBINARY; + default: + throw new IllegalArgumentException("Unsupported hive type " + hiveType); + } + } + + @Nullable + public static Type getType(ObjectInspector fieldInspector, TypeManager typeManager) + { + switch (fieldInspector.getCategory()) { + case PRIMITIVE: + PrimitiveObjectInspector.PrimitiveCategory primitiveCategory = ((PrimitiveObjectInspector) fieldInspector).getPrimitiveCategory(); + return getPrimitiveType(primitiveCategory); + case MAP: + MapObjectInspector mapObjectInspector = checkType(fieldInspector, MapObjectInspector.class, "fieldInspector"); + Type keyType = getType(mapObjectInspector.getMapKeyObjectInspector(), typeManager); + Type valueType = getType(mapObjectInspector.getMapValueObjectInspector(), typeManager); + if (keyType == null || valueType == null) { + return null; + } + return typeManager.getParameterizedType(StandardTypes.MAP, ImmutableList.of(keyType.getTypeSignature(), valueType.getTypeSignature()), ImmutableList.of()); + case LIST: + ListObjectInspector listObjectInspector = checkType(fieldInspector, ListObjectInspector.class, "fieldInspector"); + Type elementType = getType(listObjectInspector.getListElementObjectInspector(), typeManager); + if (elementType == null) { + return null; + } + return typeManager.getParameterizedType(StandardTypes.ARRAY, ImmutableList.of(elementType.getTypeSignature()), ImmutableList.of()); + case STRUCT: + StructObjectInspector structObjectInspector = checkType(fieldInspector, StructObjectInspector.class, "fieldInspector"); + List fieldTypes = new ArrayList<>(); + List fieldNames = new ArrayList<>(); + for (StructField field : structObjectInspector.getAllStructFieldRefs()) { + fieldNames.add(field.getFieldName()); + Type fieldType = getType(field.getFieldObjectInspector(), typeManager); + if (fieldType == null) { + return null; + } + fieldTypes.add(fieldType.getTypeSignature()); + } + return typeManager.getParameterizedType(StandardTypes.ROW, fieldTypes, fieldNames); + default: + throw new IllegalArgumentException("Unsupported hive type " + fieldInspector.getTypeName()); + } + } + + private static Type getPrimitiveType(PrimitiveObjectInspector.PrimitiveCategory primitiveCategory) + { + switch (primitiveCategory) { + case BOOLEAN: + return BOOLEAN; + case BYTE: + return BIGINT; + case SHORT: + return BIGINT; + case INT: + return BIGINT; + case LONG: + return BIGINT; + case FLOAT: + return DOUBLE; + case DOUBLE: + return DOUBLE; + case STRING: + return VARCHAR; + case DATE: + return DATE; + case TIMESTAMP: + return TIMESTAMP; + case BINARY: + return VARBINARY; + default: + return null; + } + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/HiveUtil.java b/presto-hive/src/main/java/com/facebook/presto/hive/HiveUtil.java new file mode 100644 index 00000000..424ab99e --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/HiveUtil.java @@ -0,0 +1,555 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.spi.ConnectorTableHandle; +import com.facebook.presto.spi.ErrorCodeSupplier; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.spi.SerializableNativeValue; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.google.common.base.Joiner; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; +import io.airlift.slice.Slices; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.hive.common.JavaUtils; +import org.apache.hadoop.hive.metastore.api.FieldSchema; +import org.apache.hadoop.hive.metastore.api.Table; +import org.apache.hadoop.hive.ql.io.SymlinkTextInputFormat; +import org.apache.hadoop.hive.ql.io.parquet.MapredParquetInputFormat; +import org.apache.hadoop.hive.ql.io.parquet.serde.ParquetHiveSerDe; +import org.apache.hadoop.hive.serde2.Deserializer; +import org.apache.hadoop.hive.serde2.SerDeException; +import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.StructField; +import org.apache.hadoop.hive.serde2.objectinspector.StructObjectInspector; +import org.apache.hadoop.mapred.FileSplit; +import org.apache.hadoop.mapred.InputFormat; +import org.apache.hadoop.mapred.JobConf; +import org.apache.hadoop.mapred.RecordReader; +import org.apache.hadoop.mapred.Reporter; +import org.apache.hadoop.mapred.TextInputFormat; +import org.apache.hadoop.util.ReflectionUtils; +import org.joda.time.DateTimeZone; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; +import org.joda.time.format.DateTimeFormatterBuilder; +import org.joda.time.format.DateTimeParser; +import org.joda.time.format.DateTimePrinter; +import org.joda.time.format.ISODateTimeFormat; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Base64; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.TimeUnit; + +import static com.facebook.presto.hive.HiveColumnHandle.SAMPLE_WEIGHT_COLUMN_NAME; +import static com.facebook.presto.hive.HiveErrorCode.HIVE_CANNOT_OPEN_SPLIT; +import static com.facebook.presto.hive.HiveErrorCode.HIVE_INVALID_METADATA; +import static com.facebook.presto.hive.HiveErrorCode.HIVE_INVALID_PARTITION_VALUE; +import static com.facebook.presto.hive.HiveErrorCode.HIVE_INVALID_VIEW_DATA; +import static com.facebook.presto.hive.HivePartitionKey.HIVE_DEFAULT_DYNAMIC_PARTITION; +import static com.facebook.presto.hive.HiveType.HIVE_BOOLEAN; +import static com.facebook.presto.hive.HiveType.HIVE_BYTE; +import static com.facebook.presto.hive.HiveType.HIVE_DATE; +import static com.facebook.presto.hive.HiveType.HIVE_DOUBLE; +import static com.facebook.presto.hive.HiveType.HIVE_FLOAT; +import static com.facebook.presto.hive.HiveType.HIVE_INT; +import static com.facebook.presto.hive.HiveType.HIVE_LONG; +import static com.facebook.presto.hive.HiveType.HIVE_SHORT; +import static com.facebook.presto.hive.HiveType.HIVE_STRING; +import static com.facebook.presto.hive.HiveType.HIVE_TIMESTAMP; +import static com.facebook.presto.hive.HiveType.getHiveType; +import static com.facebook.presto.hive.HiveType.getSupportedHiveType; +import static com.facebook.presto.hive.HiveType.getType; +import static com.facebook.presto.hive.RetryDriver.retry; +import static com.facebook.presto.hive.util.Types.checkType; +import static com.facebook.presto.spi.StandardErrorCode.NOT_SUPPORTED; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Predicates.not; +import static com.google.common.collect.Iterables.filter; +import static com.google.common.collect.Lists.transform; +import static io.airlift.slice.Slices.utf8Slice; +import static java.lang.Boolean.parseBoolean; +import static java.lang.Double.parseDouble; +import static java.lang.Long.parseLong; +import static java.lang.String.format; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.apache.hadoop.hive.metastore.MetaStoreUtils.getTableMetadata; +import static org.apache.hadoop.hive.metastore.api.hive_metastoreConstants.FILE_INPUT_FORMAT; +import static org.apache.hadoop.hive.serde.serdeConstants.SERIALIZATION_LIB; +import static org.apache.hadoop.hive.serde2.ColumnProjectionUtils.READ_ALL_COLUMNS; +import static org.apache.hadoop.hive.serde2.ColumnProjectionUtils.READ_COLUMN_IDS_CONF_STR; +import static org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector.Category; + +public final class HiveUtil +{ + public static final String PRESTO_VIEW_FLAG = "presto_view"; + + private static final String VIEW_PREFIX = "/* Presto View: "; + private static final String VIEW_SUFFIX = " */"; + + private static final DateTimeFormatter HIVE_DATE_PARSER = ISODateTimeFormat.date().withZoneUTC(); + private static final DateTimeFormatter HIVE_TIMESTAMP_PARSER; + + static { + DateTimeParser[] timestampWithoutTimeZoneParser = { + DateTimeFormat.forPattern("yyyy-M-d").getParser(), + DateTimeFormat.forPattern("yyyy-M-d H:m").getParser(), + DateTimeFormat.forPattern("yyyy-M-d H:m:s").getParser(), + DateTimeFormat.forPattern("yyyy-M-d H:m:s.SSS").getParser(), + DateTimeFormat.forPattern("yyyy-M-d H:m:s.SSSSSSS").getParser(), + DateTimeFormat.forPattern("yyyy-M-d H:m:s.SSSSSSSSS").getParser(), + }; + DateTimePrinter timestampWithoutTimeZonePrinter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss.SSSSSSSSS").getPrinter(); + HIVE_TIMESTAMP_PARSER = new DateTimeFormatterBuilder().append(timestampWithoutTimeZonePrinter, timestampWithoutTimeZoneParser).toFormatter().withZoneUTC(); + } + + private HiveUtil() + { + } + + public static RecordReader createRecordReader(String clientId, Configuration configuration, Path path, long start, long length, Properties schema, List columns, TypeManager typeManager) + { + // determine which hive columns we will read + List readColumns = ImmutableList.copyOf(filter(columns, not(HiveColumnHandle::isPartitionKey))); + List readHiveColumnIndexes = ImmutableList.copyOf(transform(readColumns, HiveColumnHandle::getHiveColumnIndex)); + + // Tell hive the columns we would like to read, this lets hive optimize reading column oriented files + setReadColumns(configuration, readHiveColumnIndexes); + + final InputFormat inputFormat = getInputFormat(configuration, schema, true); + final JobConf jobConf = new JobConf(configuration); + final FileSplit fileSplit = new FileSplit(path, start, length, (String[]) null); + + // propagate serialization configuration to getRecordReader + for (String name : schema.stringPropertyNames()) { + if (name.startsWith("serialization.")) { + jobConf.set(name, schema.getProperty(name)); + } + } + + try { + return retry() + .stopOnIllegalExceptions() + .run("createRecordReader", () -> inputFormat.getRecordReader(fileSplit, jobConf, Reporter.NULL)); + } + catch (Exception e) { + throw new PrestoException(HIVE_CANNOT_OPEN_SPLIT, format("Error opening Hive split %s (offset=%s, length=%s) using %s: %s", + path, + start, + length, + getInputFormatName(schema), + e.getMessage()), + e); + } + } + + public static void setReadColumns(Configuration configuration, List readHiveColumnIndexes) + { + configuration.set(READ_COLUMN_IDS_CONF_STR, Joiner.on(',').join(readHiveColumnIndexes)); + configuration.setBoolean(READ_ALL_COLUMNS, false); + } + + static InputFormat getInputFormat(Configuration configuration, Properties schema, boolean symlinkTarget) + { + String inputFormatName = getInputFormatName(schema); + try { + JobConf jobConf = new JobConf(configuration); + + Class> inputFormatClass = getInputFormatClass(jobConf, inputFormatName); + if (symlinkTarget && (inputFormatClass == SymlinkTextInputFormat.class)) { + // symlink targets are always TextInputFormat + inputFormatClass = TextInputFormat.class; + } + + return ReflectionUtils.newInstance(inputFormatClass, jobConf); + } + catch (ClassNotFoundException | RuntimeException e) { + throw new RuntimeException("Unable to create input format " + inputFormatName, e); + } + } + + @SuppressWarnings({"unchecked", "RedundantCast"}) + private static Class> getInputFormatClass(JobConf conf, String inputFormatName) + throws ClassNotFoundException + { + // CDH uses different names for Parquet + if ("parquet.hive.DeprecatedParquetInputFormat".equals(inputFormatName) || + "parquet.hive.MapredParquetInputFormat".equals(inputFormatName)) { + return MapredParquetInputFormat.class; + } + + Class clazz = conf.getClassByName(inputFormatName); + // TODO: remove redundant cast to Object after IDEA-118533 is fixed + return (Class>) (Object) clazz.asSubclass(InputFormat.class); + } + + static String getInputFormatName(Properties schema) + { + String name = schema.getProperty(FILE_INPUT_FORMAT); + checkCondition(name != null, HIVE_INVALID_METADATA, "Table or partition is missing Hive input format property: %s", FILE_INPUT_FORMAT); + return name; + } + + public static long parseHiveDate(String value) + { + long millis = HIVE_DATE_PARSER.parseMillis(value); + return TimeUnit.MILLISECONDS.toDays(millis); + } + + public static long parseHiveTimestamp(String value, DateTimeZone timeZone) + { + return HIVE_TIMESTAMP_PARSER.withZone(timeZone).parseMillis(value); + } + + static boolean isSplittable(InputFormat inputFormat, FileSystem fileSystem, Path path) + { + // ORC uses a custom InputFormat but is always splittable + if (inputFormat.getClass().getSimpleName().equals("OrcInputFormat")) { + return true; + } + + // use reflection to get isSplittable method on FileInputFormat + Method method = null; + for (Class clazz = inputFormat.getClass(); clazz != null; clazz = clazz.getSuperclass()) { + try { + method = clazz.getDeclaredMethod("isSplitable", FileSystem.class, Path.class); + break; + } + catch (NoSuchMethodException ignored) { + } + } + + if (method == null) { + return false; + } + try { + method.setAccessible(true); + return (boolean) method.invoke(inputFormat, fileSystem, path); + } + catch (InvocationTargetException | IllegalAccessException e) { + throw Throwables.propagate(e); + } + } + + public static StructObjectInspector getTableObjectInspector(Properties schema) + { + return getTableObjectInspector(getDeserializer(schema)); + } + + public static StructObjectInspector getTableObjectInspector(Deserializer deserializer) + { + try { + ObjectInspector inspector = deserializer.getObjectInspector(); + checkArgument(inspector.getCategory() == Category.STRUCT, "expected STRUCT: %s", inspector.getCategory()); + return (StructObjectInspector) inspector; + } + catch (SerDeException e) { + throw Throwables.propagate(e); + } + } + + public static List getTableStructFields(Table table) + { + return getTableObjectInspector(getTableMetadata(table)).getAllStructFieldRefs(); + } + + public static boolean isDeserializerClass(Properties schema, Class deserializerClass) + { + return getDeserializerClassName(schema).equals(deserializerClass.getName()); + } + + public static String getDeserializerClassName(Properties schema) + { + String name = schema.getProperty(SERIALIZATION_LIB); + checkCondition(name != null, HIVE_INVALID_METADATA, "Table or partition is missing Hive deserializer property: %s", SERIALIZATION_LIB); + return name; + } + + @SuppressWarnings("deprecation") + public static Deserializer getDeserializer(Properties schema) + { + String name = getDeserializerClassName(schema); + + Deserializer deserializer = createDeserializer(getDeserializerClass(name)); + initializeDeserializer(deserializer, schema); + return deserializer; + } + + @SuppressWarnings("deprecation") + private static Class getDeserializerClass(String name) + { + // CDH uses different names for Parquet + if ("parquet.hive.serde.ParquetHiveSerDe".equals(name)) { + return ParquetHiveSerDe.class; + } + + try { + return Class.forName(name, true, JavaUtils.getClassLoader()).asSubclass(Deserializer.class); + } + catch (ClassNotFoundException e) { + throw new RuntimeException("deserializer does not exist: " + name); + } + catch (ClassCastException e) { + throw new RuntimeException("invalid deserializer class: " + name); + } + } + + @SuppressWarnings("deprecation") + private static Deserializer createDeserializer(Class clazz) + { + try { + return clazz.getConstructor().newInstance(); + } + catch (ReflectiveOperationException e) { + throw new RuntimeException("error creating deserializer: " + clazz.getName(), e); + } + } + + @SuppressWarnings("deprecation") + private static void initializeDeserializer(Deserializer deserializer, Properties schema) + { + try { + deserializer.initialize(null, schema); + } + catch (SerDeException e) { + throw new RuntimeException("error initializing deserializer: " + deserializer.getClass().getName()); + } + } + + public static boolean isHiveNull(byte[] bytes) + { + return bytes.length == 2 && bytes[0] == '\\' && bytes[1] == 'N'; + } + + public static SerializableNativeValue parsePartitionValue(String partitionName, String value, HiveType hiveType, DateTimeZone timeZone) + { + try { + boolean isNull = HIVE_DEFAULT_DYNAMIC_PARTITION.equals(value); + + if (HIVE_BOOLEAN.equals(hiveType)) { + if (isNull) { + return new SerializableNativeValue(Boolean.class, null); + } + if (value.isEmpty()) { + return new SerializableNativeValue(Boolean.class, false); + } + return new SerializableNativeValue(Boolean.class, parseBoolean(value)); + } + + if (HIVE_BYTE.equals(hiveType) || HIVE_SHORT.equals(hiveType) || HIVE_INT.equals(hiveType) || HIVE_LONG.equals(hiveType)) { + if (isNull) { + return new SerializableNativeValue(Long.class, null); + } + if (value.isEmpty()) { + return new SerializableNativeValue(Long.class, 0L); + } + return new SerializableNativeValue(Long.class, parseLong(value)); + } + + if (HIVE_DATE.equals(hiveType)) { + if (isNull) { + return new SerializableNativeValue(Long.class, null); + } + long dateInMillis = parseHiveDate(value); + return new SerializableNativeValue(Long.class, dateInMillis); + } + + if (HIVE_TIMESTAMP.equals(hiveType)) { + if (isNull) { + return new SerializableNativeValue(Long.class, null); + } + return new SerializableNativeValue(Long.class, parseHiveTimestamp(value, timeZone)); + } + + if (HIVE_FLOAT.equals(hiveType) || HIVE_DOUBLE.equals(hiveType)) { + if (isNull) { + return new SerializableNativeValue(Double.class, null); + } + if (value.isEmpty()) { + return new SerializableNativeValue(Double.class, 0.0); + } + return new SerializableNativeValue(Double.class, parseDouble(value)); + } + + if (HIVE_STRING.equals(hiveType)) { + if (isNull) { + return new SerializableNativeValue(Slice.class, null); + } + return new SerializableNativeValue(Slice.class, utf8Slice(value)); + } + } + catch (RuntimeException e) { + throw new PrestoException(HIVE_INVALID_PARTITION_VALUE, format( + "Invalid partition value '%s' for Hive type [%s] partition key: %s", value, hiveType, partitionName)); + } + + throw new PrestoException(NOT_SUPPORTED, format("Unsupported Hive type [%s] for partition: %s", hiveType, partitionName)); + } + + public static boolean isPrestoView(Table table) + { + return "true".equals(table.getParameters().get(PRESTO_VIEW_FLAG)); + } + + public static String encodeViewData(String data) + { + return VIEW_PREFIX + Base64.getEncoder().encodeToString(data.getBytes(UTF_8)) + VIEW_SUFFIX; + } + + public static String decodeViewData(String data) + { + checkCondition(data.startsWith(VIEW_PREFIX), HIVE_INVALID_VIEW_DATA, "View data missing prefix: %s", data); + checkCondition(data.endsWith(VIEW_SUFFIX), HIVE_INVALID_VIEW_DATA, "View data missing suffix: %s", data); + data = data.substring(VIEW_PREFIX.length()); + data = data.substring(0, data.length() - VIEW_SUFFIX.length()); + return new String(Base64.getDecoder().decode(data), UTF_8); + } + + public static boolean isArrayType(Type type) + { + return type.getTypeSignature().getBase().equals(StandardTypes.ARRAY); + } + + public static boolean isMapType(Type type) + { + return type.getTypeSignature().getBase().equals(StandardTypes.MAP); + } + + public static boolean isStructuralType(Type type) + { + String baseName = type.getTypeSignature().getBase(); + return baseName.equals(StandardTypes.MAP) || baseName.equals(StandardTypes.ARRAY) || baseName.equals(StandardTypes.ROW); + } + + public static boolean isStructuralType(HiveType hiveType) + { + return hiveType.getCategory() == Category.LIST || hiveType.getCategory() == Category.MAP || hiveType.getCategory() == Category.STRUCT; + } + + public static boolean booleanPartitionKey(String value, String name) + { + if (value.equalsIgnoreCase("true")) { + return true; + } + if (value.equalsIgnoreCase("false")) { + return false; + } + throw new PrestoException(HIVE_INVALID_PARTITION_VALUE, format("Invalid partition value '%s' for BOOLEAN partition key: %s", value, name)); + } + + public static long bigintPartitionKey(String value, String name) + { + try { + return parseLong(value); + } + catch (NumberFormatException e) { + throw new PrestoException(HIVE_INVALID_PARTITION_VALUE, format("Invalid partition value '%s' for BIGINT partition key: %s", value, name)); + } + } + + public static double doublePartitionKey(String value, String name) + { + try { + return parseDouble(value); + } + catch (NumberFormatException e) { + throw new PrestoException(HIVE_INVALID_PARTITION_VALUE, format("Invalid partition value '%s' for DOUBLE partition key: %s", value, name)); + } + } + + public static long datePartitionKey(String value, String name) + { + try { + return parseHiveDate(value); + } + catch (IllegalArgumentException e) { + throw new PrestoException(HIVE_INVALID_PARTITION_VALUE, format("Invalid partition value '%s' for DATE partition key: %s", value, name)); + } + } + + public static long timestampPartitionKey(String value, DateTimeZone zone, String name) + { + try { + return parseHiveTimestamp(value, zone); + } + catch (IllegalArgumentException e) { + throw new PrestoException(HIVE_INVALID_PARTITION_VALUE, format("Invalid partition value '%s' for TIMESTAMP partition key: %s", value, name)); + } + } + + public static SchemaTableName schemaTableName(ConnectorTableHandle tableHandle) + { + return checkType(tableHandle, HiveTableHandle.class, "tableHandle").getSchemaTableName(); + } + + public static List hiveColumnHandles(TypeManager typeManager, String connectorId, Table table, boolean includeSampleWeight) + { + ImmutableList.Builder columns = ImmutableList.builder(); + + // add the data fields first + int hiveColumnIndex = 0; + for (StructField field : getTableStructFields(table)) { + // ignore unsupported types rather than failing + HiveType hiveType = getHiveType(field.getFieldObjectInspector()); + if (hiveType != null && (includeSampleWeight || !field.getFieldName().equals(SAMPLE_WEIGHT_COLUMN_NAME))) { + Type type = getType(field.getFieldObjectInspector(), typeManager); + checkCondition(type != null, NOT_SUPPORTED, "Unsupported Hive type: %s", field.getFieldObjectInspector().getTypeName()); + columns.add(new HiveColumnHandle(connectorId, field.getFieldName(), hiveColumnIndex, hiveType, type.getTypeSignature(), hiveColumnIndex, false)); + } + hiveColumnIndex++; + } + + // add the partition keys last (like Hive does) + columns.addAll(getPartitionKeyColumnHandles(connectorId, table, hiveColumnIndex)); + + return columns.build(); + } + + public static List getPartitionKeyColumnHandles(String connectorId, Table table, int startOrdinal) + { + ImmutableList.Builder columns = ImmutableList.builder(); + + List partitionKeys = table.getPartitionKeys(); + for (int i = 0; i < partitionKeys.size(); i++) { + FieldSchema field = partitionKeys.get(i); + + HiveType hiveType = getSupportedHiveType(field.getType()); + columns.add(new HiveColumnHandle(connectorId, field.getName(), startOrdinal + i, hiveType, getType(field.getType()).getTypeSignature(), -1, true)); + } + + return columns.build(); + } + + public static Slice base64Decode(byte[] bytes) + { + return Slices.wrappedBuffer(Base64.getDecoder().decode(bytes)); + } + + public static void checkCondition(boolean condition, ErrorCodeSupplier errorCode, String formatString, Object... args) + { + if (!condition) { + throw new PrestoException(errorCode, format(formatString, args)); + } + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/HiveViewNotSupportedException.java b/presto-hive/src/main/java/com/facebook/presto/hive/HiveViewNotSupportedException.java new file mode 100644 index 00000000..429d6d58 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/HiveViewNotSupportedException.java @@ -0,0 +1,41 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.spi.NotFoundException; +import com.facebook.presto.spi.SchemaTableName; + +import static java.lang.String.format; + +public class HiveViewNotSupportedException + extends NotFoundException +{ + private final SchemaTableName tableName; + + public HiveViewNotSupportedException(SchemaTableName tableName) + { + this(tableName, format("Hive views are not supported: '%s'", tableName)); + } + + public HiveViewNotSupportedException(SchemaTableName tableName, String message) + { + super(message); + this.tableName = tableName; + } + + public SchemaTableName getTableName() + { + return tableName; + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/NamenodeStats.java b/presto-hive/src/main/java/com/facebook/presto/hive/NamenodeStats.java new file mode 100644 index 00000000..9977f91f --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/NamenodeStats.java @@ -0,0 +1,82 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import io.airlift.stats.CounterStat; +import io.airlift.stats.TimeStat; +import org.weakref.jmx.Managed; +import org.weakref.jmx.Nested; + +import java.io.IOException; + +public class NamenodeStats +{ + private final CallStats listLocatedStatus = new CallStats(); + private final CallStats remoteIteratorNext = new CallStats(); + + @Managed + @Nested + public CallStats getListLocatedStatus() + { + return listLocatedStatus; + } + + @Managed + @Nested + public CallStats getRemoteIteratorNext() + { + return remoteIteratorNext; + } + + public static class CallStats + { + private final TimeStat time = new TimeStat(); + private final CounterStat totalFailures = new CounterStat(); + private final CounterStat ioExceptions = new CounterStat(); + + public TimeStat.BlockTimer time() + { + return time.time(); + } + + public void recordException(Exception exception) + { + if (exception instanceof IOException) { + ioExceptions.update(1); + } + totalFailures.update(1); + } + + @Managed + @Nested + public CounterStat getTotalFailures() + { + return totalFailures; + } + + @Managed + @Nested + public CounterStat getIoExceptions() + { + return ioExceptions; + } + + @Managed + @Nested + public TimeStat getTime() + { + return time; + } + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/NumberParser.java b/presto-hive/src/main/java/com/facebook/presto/hive/NumberParser.java new file mode 100644 index 00000000..3459ee89 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/NumberParser.java @@ -0,0 +1,58 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.spi.PrestoException; + +import static com.facebook.presto.hive.HiveErrorCode.HIVE_BAD_DATA; + +public final class NumberParser +{ + private NumberParser() {} + + public static long parseLong(byte[] bytes, int start, int length) + { + int limit = start + length; + + int sign = bytes[start] == '-' ? -1 : 1; + + if (sign == -1 || bytes[start] == '+') { + start++; + } + + long value = bytes[start] - ((int) '0'); + start++; + while (start < limit) { + value = value * 10 + (bytes[start] - ((int) '0')); + start++; + } + + return value * sign; + } + + public static double parseDouble(byte[] bytes, int start, int length) + { + char[] chars = new char[length]; + for (int pos = 0; pos < length; pos++) { + chars[pos] = (char) bytes[start + pos]; + } + String string = new String(chars); + try { + return Double.parseDouble(string); + } + catch (NumberFormatException e) { + throw new PrestoException(HIVE_BAD_DATA, e); + } + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/ParquetHiveRecordCursor.java b/presto-hive/src/main/java/com/facebook/presto/hive/ParquetHiveRecordCursor.java new file mode 100644 index 00000000..b50f4825 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/ParquetHiveRecordCursor.java @@ -0,0 +1,1135 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.block.BlockBuilderStatus; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.DynamicSliceOutput; +import io.airlift.slice.Slice; +import io.airlift.slice.Slices; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.mapreduce.TaskAttemptContext; +import org.apache.hadoop.mapreduce.TaskAttemptID; +import parquet.column.Dictionary; +import parquet.hadoop.ParquetFileReader; +import parquet.hadoop.ParquetInputSplit; +import parquet.hadoop.ParquetRecordReader; +import parquet.hadoop.api.ReadSupport; +import parquet.hadoop.api.ReadSupport.ReadContext; +import parquet.hadoop.metadata.BlockMetaData; +import parquet.hadoop.metadata.FileMetaData; +import parquet.hadoop.metadata.ParquetMetadata; +import parquet.hadoop.util.ContextUtil; +import parquet.io.api.Binary; +import parquet.io.api.Converter; +import parquet.io.api.GroupConverter; +import parquet.io.api.PrimitiveConverter; +import parquet.io.api.RecordMaterializer; +import parquet.schema.GroupType; +import parquet.schema.MessageType; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Properties; + +import static com.facebook.presto.hive.HiveErrorCode.HIVE_CURSOR_ERROR; +import static com.facebook.presto.hive.HiveUtil.bigintPartitionKey; +import static com.facebook.presto.hive.HiveUtil.booleanPartitionKey; +import static com.facebook.presto.hive.HiveUtil.doublePartitionKey; +import static com.facebook.presto.spi.StandardErrorCode.NOT_SUPPORTED; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.BooleanType.BOOLEAN; +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; +import static com.facebook.presto.spi.type.VarbinaryType.VARBINARY; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.Maps.uniqueIndex; +import static java.lang.Math.max; +import static java.lang.Math.min; +import static java.lang.String.format; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.stream.Collectors.toList; +import static parquet.schema.OriginalType.LIST; +import static parquet.schema.OriginalType.MAP; +import static parquet.schema.OriginalType.MAP_KEY_VALUE; + +class ParquetHiveRecordCursor + extends HiveRecordCursor +{ + private final ParquetRecordReader recordReader; + + @SuppressWarnings("FieldCanBeLocal") // include names for debugging + private final String[] names; + private final Type[] types; + + private final boolean[] isPartitionColumn; + + private final boolean[] booleans; + private final long[] longs; + private final double[] doubles; + private final Slice[] slices; + private final boolean[] nulls; + private final boolean[] nullsRowDefault; + + private final long totalBytes; + private long completedBytes; + private boolean closed; + + public ParquetHiveRecordCursor( + Configuration configuration, + Path path, + long start, + long length, + Properties splitSchema, + List partitionKeys, + List columns, + boolean useParquetColumnNames, + TypeManager typeManager) + { + checkNotNull(path, "path is null"); + checkArgument(length >= 0, "totalBytes is negative"); + checkNotNull(splitSchema, "splitSchema is null"); + checkNotNull(partitionKeys, "partitionKeys is null"); + checkNotNull(columns, "columns is null"); + + this.recordReader = createParquetRecordReader(configuration, path, start, length, columns, useParquetColumnNames); + this.totalBytes = length; + + int size = columns.size(); + + this.names = new String[size]; + this.types = new Type[size]; + + this.isPartitionColumn = new boolean[size]; + + this.booleans = new boolean[size]; + this.longs = new long[size]; + this.doubles = new double[size]; + this.slices = new Slice[size]; + this.nulls = new boolean[size]; + this.nullsRowDefault = new boolean[size]; + + for (int i = 0; i < columns.size(); i++) { + HiveColumnHandle column = columns.get(i); + + names[i] = column.getName(); + types[i] = typeManager.getType(column.getTypeSignature()); + + isPartitionColumn[i] = column.isPartitionKey(); + nullsRowDefault[i] = !column.isPartitionKey(); + } + + // parse requested partition columns + Map partitionKeysByName = uniqueIndex(partitionKeys, HivePartitionKey::getName); + for (int columnIndex = 0; columnIndex < columns.size(); columnIndex++) { + HiveColumnHandle column = columns.get(columnIndex); + if (column.isPartitionKey()) { + HivePartitionKey partitionKey = partitionKeysByName.get(column.getName()); + checkArgument(partitionKey != null, "Unknown partition key %s", column.getName()); + + byte[] bytes = partitionKey.getValue().getBytes(UTF_8); + + String name = names[columnIndex]; + Type type = types[columnIndex]; + if (HiveUtil.isHiveNull(bytes)) { + nullsRowDefault[columnIndex] = true; + } + else if (type.equals(BOOLEAN)) { + booleans[columnIndex] = booleanPartitionKey(partitionKey.getValue(), name); + } + else if (type.equals(BIGINT)) { + longs[columnIndex] = bigintPartitionKey(partitionKey.getValue(), name); + } + else if (type.equals(DOUBLE)) { + doubles[columnIndex] = doublePartitionKey(partitionKey.getValue(), name); + } + else if (type.equals(VARCHAR)) { + slices[columnIndex] = Slices.wrappedBuffer(bytes); + } + else { + throw new PrestoException(NOT_SUPPORTED, format("Unsupported column type %s for partition key: %s", type.getDisplayName(), name)); + } + } + } + } + + @Override + public long getTotalBytes() + { + return totalBytes; + } + + @Override + public long getCompletedBytes() + { + if (!closed) { + updateCompletedBytes(); + } + return completedBytes; + } + + private void updateCompletedBytes() + { + try { + long newCompletedBytes = (long) (totalBytes * recordReader.getProgress()); + completedBytes = min(totalBytes, max(completedBytes, newCompletedBytes)); + } + catch (IOException ignored) { + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + @Override + public Type getType(int field) + { + return types[field]; + } + + @Override + public boolean advanceNextPosition() + { + try { + // reset null flags + System.arraycopy(nullsRowDefault, 0, nulls, 0, isPartitionColumn.length); + + if (closed || !recordReader.nextKeyValue()) { + close(); + return false; + } + + return true; + } + catch (IOException | RuntimeException | InterruptedException e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + + closeWithSuppression(e); + throw new PrestoException(HIVE_CURSOR_ERROR, e); + } + } + + @Override + public boolean getBoolean(int fieldId) + { + checkState(!closed, "Cursor is closed"); + + validateType(fieldId, boolean.class); + return booleans[fieldId]; + } + + @Override + public long getLong(int fieldId) + { + checkState(!closed, "Cursor is closed"); + + validateType(fieldId, long.class); + return longs[fieldId]; + } + + @Override + public double getDouble(int fieldId) + { + checkState(!closed, "Cursor is closed"); + + validateType(fieldId, double.class); + return doubles[fieldId]; + } + + @Override + public Slice getSlice(int fieldId) + { + checkState(!closed, "Cursor is closed"); + + validateType(fieldId, Slice.class); + return slices[fieldId]; + } + + @Override + public boolean isNull(int fieldId) + { + checkState(!closed, "Cursor is closed"); + return nulls[fieldId]; + } + + private void validateType(int fieldId, Class javaType) + { + if (types[fieldId].getJavaType() != javaType) { + // we don't use Preconditions.checkArgument because it requires boxing fieldId, which affects inner loop performance + throw new IllegalArgumentException(String.format("Expected field to be %s, actual %s (field %s)", javaType.getName(), types[fieldId].getJavaType().getName(), fieldId)); + } + } + + @Override + public void close() + { + // some hive input formats are broken and bad things can happen if you close them multiple times + if (closed) { + return; + } + closed = true; + + updateCompletedBytes(); + + try { + recordReader.close(); + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } + + private ParquetRecordReader createParquetRecordReader( + Configuration configuration, + Path path, + long start, + long length, + List columns, + boolean useParquetColumnNames) + { + try { + ParquetMetadata parquetMetadata = ParquetFileReader.readFooter(configuration, path); + List blocks = parquetMetadata.getBlocks(); + FileMetaData fileMetaData = parquetMetadata.getFileMetaData(); + + PrestoReadSupport readSupport = new PrestoReadSupport(useParquetColumnNames, columns, fileMetaData.getSchema()); + ReadContext readContext = readSupport.init(configuration, fileMetaData.getKeyValueMetaData(), fileMetaData.getSchema()); + + List splitGroup = new ArrayList<>(); + long splitStart = start; + long splitLength = length; + for (BlockMetaData block : blocks) { + long firstDataPage = block.getColumns().get(0).getFirstDataPageOffset(); + if (firstDataPage >= splitStart && firstDataPage < splitStart + splitLength) { + splitGroup.add(block); + } + } + + ParquetInputSplit split; + + split = new ParquetInputSplit(path, + splitStart, + splitLength, + null, + splitGroup, + readContext.getRequestedSchema().toString(), + fileMetaData.getSchema().toString(), + fileMetaData.getKeyValueMetaData(), + readContext.getReadSupportMetadata()); + + TaskAttemptContext taskContext = ContextUtil.newTaskAttemptContext(configuration, new TaskAttemptID()); + ParquetRecordReader realReader = new PrestoParquetRecordReader(readSupport); + realReader.initialize(split, taskContext); + return realReader; + } + catch (IOException e) { + throw Throwables.propagate(e); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw Throwables.propagate(e); + } + } + + public class PrestoParquetRecordReader + extends ParquetRecordReader + { + public PrestoParquetRecordReader(PrestoReadSupport readSupport) + { + super(readSupport); + } + } + + public final class PrestoReadSupport + extends ReadSupport + { + private final boolean useParquetColumnNames; + private final List columns; + private final List converters; + + public PrestoReadSupport(boolean useParquetColumnNames, List columns, MessageType messageType) + { + this.columns = columns; + this.useParquetColumnNames = useParquetColumnNames; + + ImmutableList.Builder converters = ImmutableList.builder(); + for (int i = 0; i < columns.size(); i++) { + HiveColumnHandle column = columns.get(i); + if (!column.isPartitionKey()) { + parquet.schema.Type parquetType = getParquetType(column, messageType); + if (parquetType == null) { + continue; + } + if (parquetType.isPrimitive()) { + converters.add(new ParquetPrimitiveColumnConverter(i)); + } + else { + GroupType groupType = parquetType.asGroupType(); + switch (column.getTypeSignature().getBase()) { + case StandardTypes.ARRAY: + ParquetColumnConverter listConverter = new ParquetColumnConverter(new ParquetListConverter(groupType.getName(), groupType), i); + converters.add(listConverter); + break; + case StandardTypes.MAP: + ParquetColumnConverter mapConverter = new ParquetColumnConverter(new ParquetMapConverter(groupType.getName(), groupType), i); + converters.add(mapConverter); + break; + case StandardTypes.ROW: + ParquetColumnConverter rowConverter = new ParquetColumnConverter(new ParquetStructConverter(groupType.getName(), groupType), i); + converters.add(rowConverter); + break; + default: + throw new IllegalArgumentException("Group column " + groupType.getName() + " type " + groupType.getOriginalType() + " not supported"); + } + } + } + } + this.converters = converters.build(); + } + + @Override + @SuppressWarnings("deprecation") + public ReadContext init( + Configuration configuration, + Map keyValueMetaData, + MessageType messageType) + { + List fields = columns.stream() + .filter(column -> !column.isPartitionKey()) + .map(column -> getParquetType(column, messageType)) + .filter(Objects::nonNull) + .collect(toList()); + MessageType requestedProjection = new MessageType(messageType.getName(), fields); + return new ReadContext(requestedProjection); + } + + @Override + public RecordMaterializer prepareForRead( + Configuration configuration, + Map keyValueMetaData, + MessageType fileSchema, + ReadContext readContext) + { + return new ParquetRecordConverter(converters); + } + + private parquet.schema.Type getParquetType(HiveColumnHandle column, MessageType messageType) + { + if (useParquetColumnNames) { + if (messageType.containsField(column.getName())) { + return messageType.getType(column.getName()); + } + return null; + } + + if (column.getHiveColumnIndex() < messageType.getFieldCount()) { + return messageType.getType(column.getHiveColumnIndex()); + } + return null; + } + } + + private static class ParquetRecordConverter + extends RecordMaterializer + { + private final ParquetGroupConverter groupConverter; + + public ParquetRecordConverter(List converters) + { + groupConverter = new ParquetGroupConverter(converters); + } + + @Override + public FakeParquetRecord getCurrentRecord() + { + // Parquet skips the record if it is null, so we need non-null record + return FakeParquetRecord.MATERIALIZE_RECORD; + } + + @Override + public GroupConverter getRootConverter() + { + return groupConverter; + } + } + + private enum FakeParquetRecord + { + MATERIALIZE_RECORD + } + + public static class ParquetGroupConverter + extends GroupConverter + { + private final List converters; + + public ParquetGroupConverter(List converters) + { + this.converters = converters; + } + + @Override + public Converter getConverter(int fieldIndex) + { + return converters.get(fieldIndex); + } + + @Override + public void start() + { + } + + @Override + public void end() + { + } + } + + @SuppressWarnings("AccessingNonPublicFieldOfAnotherObject") + private class ParquetPrimitiveColumnConverter + extends PrimitiveConverter + { + private final int fieldIndex; + + private ParquetPrimitiveColumnConverter(int fieldIndex) + { + this.fieldIndex = fieldIndex; + } + + @Override + public boolean isPrimitive() + { + return true; + } + + @Override + public PrimitiveConverter asPrimitiveConverter() + { + return this; + } + + @Override + public boolean hasDictionarySupport() + { + return false; + } + + @Override + public void setDictionary(Dictionary dictionary) + { + } + + @Override + public void addValueFromDictionary(int dictionaryId) + { + } + + @Override + public void addBoolean(boolean value) + { + nulls[fieldIndex] = false; + booleans[fieldIndex] = value; + } + + @Override + public void addDouble(double value) + { + nulls[fieldIndex] = false; + doubles[fieldIndex] = value; + } + + @Override + public void addLong(long value) + { + nulls[fieldIndex] = false; + longs[fieldIndex] = value; + } + + @Override + public void addBinary(Binary value) + { + nulls[fieldIndex] = false; + slices[fieldIndex] = Slices.wrappedBuffer(value.getBytes()); + } + + @Override + public void addFloat(float value) + { + nulls[fieldIndex] = false; + doubles[fieldIndex] = value; + } + + @Override + public void addInt(int value) + { + nulls[fieldIndex] = false; + longs[fieldIndex] = value; + } + } + + public class ParquetColumnConverter + extends GroupConverter + { + private final GroupedConverter groupedConverter; + private final int fieldIndex; + + public ParquetColumnConverter(GroupedConverter groupedConverter, int fieldIndex) + { + this.groupedConverter = groupedConverter; + this.fieldIndex = fieldIndex; + } + + @Override + public Converter getConverter(int fieldIndex) + { + return groupedConverter.getConverter(fieldIndex); + } + + @Override + public void start() + { + groupedConverter.beforeValue(null); + groupedConverter.start(); + } + + @Override + public void end() + { + groupedConverter.afterValue(); + groupedConverter.end(); + + nulls[fieldIndex] = false; + + slices[fieldIndex] = groupedConverter.toSlice(); + } + } + + private interface BlockConverter + { + void beforeValue(BlockBuilder builder); + + void afterValue(); + } + + private abstract static class GroupedConverter + extends GroupConverter + implements BlockConverter + { + public abstract Slice toSlice(); + } + + private static BlockConverter createConverter(String columnName, parquet.schema.Type type) + { + if (type.isPrimitive()) { + return new ParquetPrimitiveConverter(); + } + else if (type.getOriginalType() == LIST) { + return new ParquetListConverter(columnName, type.asGroupType()); + } + else if (type.getOriginalType() == MAP) { + return new ParquetMapConverter(columnName, type.asGroupType()); + } + else if (type.getOriginalType() == null) { + // struct does not have an original type + return new ParquetStructConverter(columnName, type.asGroupType()); + } + throw new IllegalArgumentException("Unsupported type " + type); + } + + private static class ParquetStructConverter + extends GroupedConverter + { + private final DynamicSliceOutput out = new DynamicSliceOutput(1024); + + private final List converters; + private BlockBuilder builder; + private BlockBuilder currentBuilder; + + public ParquetStructConverter(String columnName, GroupType entryType) + { + ImmutableList.Builder converters = ImmutableList.builder(); + for (parquet.schema.Type fieldType : entryType.getFields()) { + converters.add(createConverter(columnName + "." + fieldType.getName(), fieldType)); + } + this.converters = converters.build(); + } + + @Override + public Converter getConverter(int fieldIndex) + { + return (Converter) converters.get(fieldIndex); + } + + @Override + public void beforeValue(BlockBuilder builder) + { + this.builder = builder; + } + + @Override + public void start() + { + out.reset(); + this.currentBuilder = createBlockBuilder(); + for (BlockConverter converter : converters) { + converter.beforeValue(currentBuilder); + } + } + + @Override + public void end() + { + for (BlockConverter converter : converters) { + converter.afterValue(); + } + currentBuilder.getEncoding().writeBlock(out, currentBuilder.build()); + + if (builder != null) { + VARBINARY.writeSlice(builder, out.copySlice()); + } + } + + @Override + public void afterValue() + { + } + + @Override + public Slice toSlice() + { + return out.copySlice(); + } + } + + private static class ParquetListConverter + extends GroupedConverter + { + private final DynamicSliceOutput out = new DynamicSliceOutput(1024); + + private final BlockConverter elementConverter; + private BlockBuilder builder; + private BlockBuilder currentBuilder; + + public ParquetListConverter(String columnName, GroupType listType) + { + checkArgument(listType.getFieldCount() == 1, + "Expected LIST column '%s' to only have one field, but has %s fields", + columnName, + listType.getFieldCount()); + + // The Parquet specification requires that the element value of a + // LIST type be wrapped in an inner repeated group, like so: + // + // optional group listField (LIST) { + // repeated group list { + // optional int element + // } + // } + // + // However, some parquet libraries don't follow this spec. The + // compatibility rules used here are specified in the Parquet + // documentation at http://git.io/vf3wG. + parquet.schema.Type elementType = listType.getType(0); + if (elementType.isPrimitive() || + elementType.asGroupType().getFieldCount() > 1 || + elementType.getName().equals("array") || + elementType.getName().equals(listType.getName() + "_tuple")) { + elementConverter = createConverter(columnName + ".element", elementType); + } + else { + elementConverter = new ParquetListEntryConverter(columnName, elementType.asGroupType()); + } + } + + @Override + public void beforeValue(BlockBuilder builder) + { + this.builder = builder; + } + + @Override + public Converter getConverter(int fieldIndex) + { + if (fieldIndex == 0) { + return (Converter) elementConverter; + } + throw new IllegalArgumentException("LIST field must be 0 not " + fieldIndex); + } + + @Override + public void start() + { + out.reset(); + this.currentBuilder = createBlockBuilder(); + elementConverter.beforeValue(currentBuilder); + } + + @Override + public void end() + { + elementConverter.afterValue(); + currentBuilder.getEncoding().writeBlock(out, currentBuilder.build()); + + if (builder != null) { + VARBINARY.writeSlice(builder, out.copySlice()); + } + } + + @Override + public void afterValue() + { + } + + @Override + public Slice toSlice() + { + return out.copySlice(); + } + } + + private static class ParquetListEntryConverter + extends GroupConverter + implements BlockConverter + { + private final BlockConverter elementConverter; + + private BlockBuilder builder; + + public ParquetListEntryConverter(String columnName, GroupType elementType) + { + checkArgument(elementType.getOriginalType() == null, + "Expected LIST column '%s' field to be type STRUCT, but is %s", + columnName, + elementType); + + checkArgument(elementType.getFieldCount() == 1, + "Expected LIST column '%s' element to have one field, but has %s fields", + columnName, + elementType.getFieldCount()); + + checkArgument(elementType.getFieldName(0).equals("array_element"), + "Expected LIST column '%s' entry field 0 to be named 'array_element', but is named %s", + columnName, + elementType.getFieldName(0)); + + elementConverter = createConverter(columnName + ".element", elementType.getType(0)); + } + + @Override + public Converter getConverter(int fieldIndex) + { + if (fieldIndex == 0) { + return (Converter) elementConverter; + } + throw new IllegalArgumentException("LIST entry field must be 0 not " + fieldIndex); + } + + @Override + public void beforeValue(BlockBuilder builder) + { + this.builder = builder; + } + + @Override + public void start() + { + elementConverter.beforeValue(builder); + } + + @Override + public void end() + { + elementConverter.afterValue(); + } + + @Override + public void afterValue() + { + } + } + + private static class ParquetMapConverter + extends GroupedConverter + { + private final DynamicSliceOutput out = new DynamicSliceOutput(1024); + + private final ParquetMapEntryConverter entryConverter; + private BlockBuilder builder; + private BlockBuilder currentBuilder; + + public ParquetMapConverter(String columnName, GroupType mapType) + { + checkArgument(mapType.getFieldCount() == 1, + "Expected MAP column '%s' to only have one field, but has %s fields", + mapType.getName(), + mapType.getFieldCount()); + + parquet.schema.Type entryType = mapType.getFields().get(0); + + // original versions of parquet had map end entry swapped + if (mapType.getOriginalType() != MAP_KEY_VALUE) { + checkArgument(entryType.getOriginalType() == MAP_KEY_VALUE, + "Expected MAP column '%s' field to be type %s, but is %s", + mapType.getName(), + MAP_KEY_VALUE, + entryType); + } + + entryConverter = new ParquetMapEntryConverter(columnName + ".entry", entryType.asGroupType()); + } + + @Override + public void beforeValue(BlockBuilder builder) + { + this.builder = builder; + } + + @Override + public Converter getConverter(int fieldIndex) + { + if (fieldIndex == 0) { + return entryConverter; + } + throw new IllegalArgumentException("Map field must be 0 not " + fieldIndex); + } + + @Override + public void start() + { + out.reset(); + this.currentBuilder = createBlockBuilder(); + entryConverter.beforeValue(currentBuilder); + } + + @Override + public void end() + { + entryConverter.afterValue(); + currentBuilder.getEncoding().writeBlock(out, currentBuilder.build()); + + if (builder != null) { + VARBINARY.writeSlice(builder, out.copySlice()); + } + } + + @Override + public void afterValue() + { + } + + @Override + public Slice toSlice() + { + return out.copySlice(); + } + } + + private static class ParquetMapEntryConverter + extends GroupConverter + implements BlockConverter + { + private final BlockConverter keyConverter; + private final BlockConverter valueConverter; + + private BlockBuilder builder; + + public ParquetMapEntryConverter(String columnName, GroupType entryType) + { + // original version of parquet used null for entry due to a bug + if (entryType.getOriginalType() != null) { + checkArgument(entryType.getOriginalType() == MAP_KEY_VALUE, + "Expected MAP column '%s' field to be type %s, but is %s", + columnName, + MAP_KEY_VALUE, + entryType); + } + + GroupType entryGroupType = entryType.asGroupType(); + checkArgument(entryGroupType.getFieldCount() == 2, + "Expected MAP column '%s' entry to have two fields, but has %s fields", + columnName, + entryGroupType.getFieldCount()); + checkArgument(entryGroupType.getFieldName(0).equals("key"), + "Expected MAP column '%s' entry field 0 to be named 'key', but is named %s", + columnName, + entryGroupType.getFieldName(0)); + checkArgument(entryGroupType.getFieldName(1).equals("value"), + "Expected MAP column '%s' entry field 1 to be named 'value', but is named %s", + columnName, + entryGroupType.getFieldName(1)); + checkArgument(entryGroupType.getType(0).isPrimitive(), + "Expected MAP column '%s' entry field 0 to be primitive, but is named %s", + columnName, + entryGroupType.getType(0)); + + keyConverter = createConverter(columnName + ".key", entryGroupType.getFields().get(0)); + valueConverter = createConverter(columnName + ".value", entryGroupType.getFields().get(1)); + } + + @Override + public Converter getConverter(int fieldIndex) + { + if (fieldIndex == 0) { + return (Converter) keyConverter; + } + if (fieldIndex == 1) { + return (Converter) valueConverter; + } + throw new IllegalArgumentException("Map entry field must be 0 or 1 not " + fieldIndex); + } + + @Override + public void beforeValue(BlockBuilder builder) + { + this.builder = builder; + } + + @Override + public void start() + { + keyConverter.beforeValue(builder); + valueConverter.beforeValue(builder); + } + + @Override + public void end() + { + keyConverter.afterValue(); + valueConverter.afterValue(); + } + + @Override + public void afterValue() + { + } + } + + private static class ParquetPrimitiveConverter + extends PrimitiveConverter + implements BlockConverter + { + private BlockBuilder builder; + private boolean wroteValue; + + public ParquetPrimitiveConverter() + { + } + + @Override + public void beforeValue(BlockBuilder builder) + { + this.builder = checkNotNull(builder, "parent builder is null"); + wroteValue = false; + } + + @Override + public void afterValue() + { + if (wroteValue) { + return; + } + + builder.appendNull(); + } + + @Override + public boolean isPrimitive() + { + return true; + } + + @Override + public PrimitiveConverter asPrimitiveConverter() + { + return this; + } + + @Override + public boolean hasDictionarySupport() + { + return false; + } + + @Override + public void setDictionary(Dictionary dictionary) + { + } + + @Override + public void addValueFromDictionary(int dictionaryId) + { + } + + @Override + public void addBoolean(boolean value) + { + BOOLEAN.writeBoolean(builder, value); + wroteValue = true; + } + + @Override + public void addDouble(double value) + { + DOUBLE.writeDouble(builder, value); + wroteValue = true; + } + + @Override + public void addLong(long value) + { + BIGINT.writeLong(builder, value); + wroteValue = true; + } + + @Override + public void addBinary(Binary value) + { + VARBINARY.writeSlice(builder, Slices.wrappedBuffer(value.getBytes())); + wroteValue = true; + } + + @Override + public void addFloat(float value) + { + DOUBLE.writeDouble(builder, value); + wroteValue = true; + } + + @Override + public void addInt(int value) + { + BIGINT.writeLong(builder, value); + wroteValue = true; + } + } + + private static BlockBuilder createBlockBuilder() + { + return VARBINARY.createBlockBuilder(new BlockBuilderStatus(), 100); + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/ParquetRecordCursorProvider.java b/presto-hive/src/main/java/com/facebook/presto/hive/ParquetRecordCursorProvider.java new file mode 100644 index 00000000..71d68bb6 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/ParquetRecordCursorProvider.java @@ -0,0 +1,105 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.TupleDomain; +import com.facebook.presto.spi.type.TypeManager; +import com.google.common.base.Predicate; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.Path; +import org.joda.time.DateTimeZone; + +import javax.inject.Inject; + +import java.util.List; +import java.util.Optional; +import java.util.Properties; +import java.util.Set; + +import static com.facebook.presto.hive.HiveUtil.getDeserializerClassName; +import static com.google.common.base.Predicates.not; +import static com.google.common.collect.Iterables.filter; +import static java.util.Objects.requireNonNull; + +public class ParquetRecordCursorProvider + implements HiveRecordCursorProvider +{ + private static final Set PARQUET_SERDE_CLASS_NAMES = ImmutableSet.builder() + .add("org.apache.hadoop.hive.ql.io.parquet.serde.ParquetHiveSerDe") + .add("parquet.hive.serde.ParquetHiveSerDe") + .build(); + + private final boolean useParquetColumnNames; + + @Inject + public ParquetRecordCursorProvider(HiveClientConfig hiveClientConfig) + { + this(requireNonNull(hiveClientConfig, "hiveClientConfig is null").isUseParquetColumnNames()); + } + + public ParquetRecordCursorProvider(boolean useParquetColumnNames) + { + this.useParquetColumnNames = useParquetColumnNames; + } + + @Override + public Optional createHiveRecordCursor( + String clientId, + Configuration configuration, + ConnectorSession session, + Path path, + long start, + long length, + Properties schema, + List columns, + List partitionKeys, + TupleDomain effectivePredicate, + DateTimeZone hiveStorageTimeZone, + TypeManager typeManager) + { + if (!PARQUET_SERDE_CLASS_NAMES.contains(getDeserializerClassName(schema))) { + return Optional.empty(); + } + + // are all columns supported by Parquet code + List unsupportedColumns = ImmutableList.copyOf(filter(columns, not(isParquetSupportedType()))); + if (!unsupportedColumns.isEmpty()) { + throw new IllegalArgumentException("Can not read Parquet column: " + unsupportedColumns); + } + + return Optional.of(new ParquetHiveRecordCursor( + configuration, + path, + start, + length, + schema, + partitionKeys, + columns, + useParquetColumnNames, + typeManager)); + } + + private static Predicate isParquetSupportedType() + { + return columnHandle -> { + HiveType hiveType = columnHandle.getHiveType(); + return !hiveType.equals(HiveType.HIVE_TIMESTAMP) && + !hiveType.equals(HiveType.HIVE_DATE) && + !hiveType.equals(HiveType.HIVE_BINARY); + }; + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/PartitionOfflineException.java b/presto-hive/src/main/java/com/facebook/presto/hive/PartitionOfflineException.java new file mode 100644 index 00000000..fc168ce0 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/PartitionOfflineException.java @@ -0,0 +1,54 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.SchemaTableName; + +import static com.facebook.presto.hive.HiveErrorCode.HIVE_PARTITION_OFFLINE; +import static com.google.common.base.Preconditions.checkNotNull; + +public class PartitionOfflineException + extends PrestoException +{ + private final SchemaTableName tableName; + private final String partition; + + public PartitionOfflineException(SchemaTableName tableName, String partition) + { + this(tableName, partition, String.format("Table '%s' partition '%s' is offline", tableName, partition)); + } + + public PartitionOfflineException(SchemaTableName tableName, + String partition, + String message) + { + super(HIVE_PARTITION_OFFLINE, message); + if (tableName == null) { + throw new NullPointerException("tableName is null"); + } + this.tableName = checkNotNull(tableName, "tableName is null"); + this.partition = checkNotNull(partition, "partition is null"); + } + + public SchemaTableName getTableName() + { + return tableName; + } + + public String getPartition() + { + return partition; + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/PartitionOption.java b/presto-hive/src/main/java/com/facebook/presto/hive/PartitionOption.java new file mode 100644 index 00000000..99ae178d --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/PartitionOption.java @@ -0,0 +1,55 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import java.util.LinkedHashMap; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class PartitionOption +{ + private final boolean dynamicPartition; + private final LinkedHashMap partitionElements; + private final String partitionSuffix; + + @JsonCreator + public PartitionOption( + @JsonProperty("dynamicPartition") boolean dynamicPartition, + @JsonProperty("partitionElements") LinkedHashMap partitionElements, + @JsonProperty("partitionSuffix") String partitionSuffix) + { + this.dynamicPartition = dynamicPartition; + this.partitionElements = new LinkedHashMap(partitionElements); + this.partitionSuffix = partitionSuffix; + } + + @JsonProperty + public String getPartitionSuffix() + { + return partitionSuffix; + } + + @JsonProperty + public LinkedHashMap getPartitionElements() + { + return partitionElements; + } + + @JsonProperty + public boolean isDynamicPartition() + { + return dynamicPartition; + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/PrestoS3FileSystem.java b/presto-hive/src/main/java/com/facebook/presto/hive/PrestoS3FileSystem.java new file mode 100644 index 00000000..07438755 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/PrestoS3FileSystem.java @@ -0,0 +1,886 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.amazonaws.AbortedException; +import com.amazonaws.AmazonClientException; +import com.amazonaws.ClientConfiguration; +import com.amazonaws.Protocol; +import com.amazonaws.auth.AWSCredentials; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.auth.InstanceProfileCredentialsProvider; +import com.amazonaws.event.ProgressEvent; +import com.amazonaws.event.ProgressEventType; +import com.amazonaws.event.ProgressListener; +import com.amazonaws.internal.StaticCredentialsProvider; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.AmazonS3Exception; +import com.amazonaws.services.s3.model.GetObjectRequest; +import com.amazonaws.services.s3.model.ListObjectsRequest; +import com.amazonaws.services.s3.model.ObjectListing; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.S3ObjectInputStream; +import com.amazonaws.services.s3.model.S3ObjectSummary; +import com.amazonaws.services.s3.transfer.Transfer; +import com.amazonaws.services.s3.transfer.TransferManager; +import com.amazonaws.services.s3.transfer.TransferManagerConfiguration; +import com.amazonaws.services.s3.transfer.Upload; +import com.facebook.presto.hadoop.HadoopFileStatus; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Throwables; +import com.google.common.collect.AbstractSequentialIterator; +import com.google.common.collect.Iterators; +import com.google.common.primitives.Ints; +import io.airlift.log.Logger; +import io.airlift.units.DataSize; +import io.airlift.units.Duration; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.BlockLocation; +import org.apache.hadoop.fs.BufferedFSInputStream; +import org.apache.hadoop.fs.FSDataInputStream; +import org.apache.hadoop.fs.FSDataOutputStream; +import org.apache.hadoop.fs.FSInputStream; +import org.apache.hadoop.fs.FileStatus; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.LocatedFileStatus; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.fs.RemoteIterator; +import org.apache.hadoop.fs.permission.FsPermission; +import org.apache.hadoop.fs.s3.S3Credentials; +import org.apache.hadoop.util.Progressable; + +import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InterruptedIOException; +import java.net.URI; +import java.util.ArrayList; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static com.facebook.presto.hive.RetryDriver.retry; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Strings.nullToEmpty; +import static com.google.common.collect.Iterables.toArray; +import static io.airlift.units.DataSize.Unit.MEGABYTE; +import static java.lang.Math.max; +import static java.nio.file.Files.createDirectories; +import static java.nio.file.Files.createTempFile; +import static org.apache.http.HttpStatus.SC_FORBIDDEN; +import static org.apache.http.HttpStatus.SC_NOT_FOUND; +import static org.apache.http.HttpStatus.SC_REQUESTED_RANGE_NOT_SATISFIABLE; + +public class PrestoS3FileSystem + extends FileSystem +{ + private static final Logger log = Logger.get(PrestoS3FileSystem.class); + + private static final PrestoS3FileSystemStats STATS = new PrestoS3FileSystemStats(); + private static final PrestoS3FileSystemMetricCollector METRIC_COLLECTOR = new PrestoS3FileSystemMetricCollector(STATS); + + public static PrestoS3FileSystemStats getFileSystemStats() + { + return STATS; + } + + private static final String DIRECTORY_SUFFIX = "_$folder$"; + + public static final String S3_SSL_ENABLED = "presto.s3.ssl.enabled"; + public static final String S3_MAX_ERROR_RETRIES = "presto.s3.max-error-retries"; + public static final String S3_MAX_CLIENT_RETRIES = "presto.s3.max-client-retries"; + public static final String S3_MAX_BACKOFF_TIME = "presto.s3.max-backoff-time"; + public static final String S3_MAX_RETRY_TIME = "presto.s3.max-retry-time"; + public static final String S3_CONNECT_TIMEOUT = "presto.s3.connect-timeout"; + public static final String S3_SOCKET_TIMEOUT = "presto.s3.socket-timeout"; + public static final String S3_MAX_CONNECTIONS = "presto.s3.max-connections"; + public static final String S3_STAGING_DIRECTORY = "presto.s3.staging-directory"; + public static final String S3_MULTIPART_MIN_FILE_SIZE = "presto.s3.multipart.min-file-size"; + public static final String S3_MULTIPART_MIN_PART_SIZE = "presto.s3.multipart.min-part-size"; + public static final String S3_USE_INSTANCE_CREDENTIALS = "presto.s3.use-instance-credentials"; + + private static final DataSize BLOCK_SIZE = new DataSize(32, MEGABYTE); + private static final DataSize MAX_SKIP_SIZE = new DataSize(1, MEGABYTE); + + private final TransferManagerConfiguration transferConfig = new TransferManagerConfiguration(); + + private URI uri; + private Path workingDirectory; + private AmazonS3 s3; + private File stagingDirectory; + private int maxAttempts; + private Duration maxBackoffTime; + private Duration maxRetryTime; + private boolean useInstanceCredentials; + + @Override + public void initialize(URI uri, Configuration conf) + throws IOException + { + checkNotNull(uri, "uri is null"); + checkNotNull(conf, "conf is null"); + super.initialize(uri, conf); + setConf(conf); + + this.uri = URI.create(uri.getScheme() + "://" + uri.getAuthority()); + this.workingDirectory = new Path("/").makeQualified(this.uri, new Path("/")); + + HiveClientConfig defaults = new HiveClientConfig(); + this.stagingDirectory = new File(conf.get(S3_STAGING_DIRECTORY, defaults.getS3StagingDirectory().toString())); + this.maxAttempts = conf.getInt(S3_MAX_CLIENT_RETRIES, defaults.getS3MaxClientRetries()) + 1; + this.maxBackoffTime = Duration.valueOf(conf.get(S3_MAX_BACKOFF_TIME, defaults.getS3MaxBackoffTime().toString())); + this.maxRetryTime = Duration.valueOf(conf.get(S3_MAX_RETRY_TIME, defaults.getS3MaxRetryTime().toString())); + int maxErrorRetries = conf.getInt(S3_MAX_ERROR_RETRIES, defaults.getS3MaxErrorRetries()); + boolean sslEnabled = conf.getBoolean(S3_SSL_ENABLED, defaults.isS3SslEnabled()); + Duration connectTimeout = Duration.valueOf(conf.get(S3_CONNECT_TIMEOUT, defaults.getS3ConnectTimeout().toString())); + Duration socketTimeout = Duration.valueOf(conf.get(S3_SOCKET_TIMEOUT, defaults.getS3SocketTimeout().toString())); + int maxConnections = conf.getInt(S3_MAX_CONNECTIONS, defaults.getS3MaxConnections()); + long minFileSize = conf.getLong(S3_MULTIPART_MIN_FILE_SIZE, defaults.getS3MultipartMinFileSize().toBytes()); + long minPartSize = conf.getLong(S3_MULTIPART_MIN_PART_SIZE, defaults.getS3MultipartMinPartSize().toBytes()); + this.useInstanceCredentials = conf.getBoolean(S3_USE_INSTANCE_CREDENTIALS, defaults.isS3UseInstanceCredentials()); + + ClientConfiguration configuration = new ClientConfiguration() + .withMaxErrorRetry(maxErrorRetries) + .withProtocol(sslEnabled ? Protocol.HTTPS : Protocol.HTTP) + .withConnectionTimeout(Ints.checkedCast(connectTimeout.toMillis())) + .withSocketTimeout(Ints.checkedCast(socketTimeout.toMillis())) + .withMaxConnections(maxConnections); + + this.s3 = createAmazonS3Client(uri, conf, configuration); + + transferConfig.setMultipartUploadThreshold(minFileSize); + transferConfig.setMinimumUploadPartSize(minPartSize); + } + + @Override + public URI getUri() + { + return uri; + } + + @Override + public Path getWorkingDirectory() + { + return workingDirectory; + } + + @Override + public void setWorkingDirectory(Path path) + { + workingDirectory = path; + } + + @Override + public FileStatus[] listStatus(Path path) + throws IOException + { + STATS.newListStatusCall(); + List list = new ArrayList<>(); + RemoteIterator iterator = listLocatedStatus(path); + while (iterator.hasNext()) { + list.add(iterator.next()); + } + return toArray(list, LocatedFileStatus.class); + } + + @Override + public RemoteIterator listLocatedStatus(Path path) + { + STATS.newListLocatedStatusCall(); + return new RemoteIterator() + { + private final Iterator iterator = listPrefix(path); + + @Override + public boolean hasNext() + throws IOException + { + try { + return iterator.hasNext(); + } + catch (AmazonClientException e) { + throw new IOException(e); + } + } + + @Override + public LocatedFileStatus next() + throws IOException + { + try { + return iterator.next(); + } + catch (AmazonClientException e) { + throw new IOException(e); + } + } + }; + } + + @Override + public FileStatus getFileStatus(Path path) + throws IOException + { + if (path.getName().isEmpty()) { + // the bucket root requires special handling + if (getS3ObjectMetadata(path) != null) { + return new FileStatus(0, true, 1, 0, 0, qualifiedPath(path)); + } + throw new FileNotFoundException("File does not exist: " + path); + } + + ObjectMetadata metadata = getS3ObjectMetadata(path); + + if (metadata == null) { + // check if this path is a directory + Iterator iterator = listPrefix(path); + if (iterator.hasNext()) { + return new FileStatus(0, true, 1, 0, 0, qualifiedPath(path)); + } + throw new FileNotFoundException("File does not exist: " + path); + } + + return new FileStatus( + metadata.getContentLength(), + false, + 1, + BLOCK_SIZE.toBytes(), + lastModifiedTime(metadata), + qualifiedPath(path)); + } + + @Override + public FSDataInputStream open(Path path, int bufferSize) + throws IOException + { + return new FSDataInputStream( + new BufferedFSInputStream( + new PrestoS3InputStream(s3, uri.getHost(), path, maxAttempts, maxBackoffTime, maxRetryTime), + bufferSize)); + } + + @Override + public FSDataOutputStream create(Path path, FsPermission permission, boolean overwrite, int bufferSize, short replication, long blockSize, Progressable progress) + throws IOException + { + if ((!overwrite) && exists(path)) { + throw new IOException("File already exists:" + path); + } + + createDirectories(stagingDirectory.toPath()); + File tempFile = createTempFile(stagingDirectory.toPath(), "presto-s3-", ".tmp").toFile(); + + String key = keyFromPath(qualifiedPath(path)); + return new FSDataOutputStream( + new PrestoS3OutputStream(s3, transferConfig, uri.getHost(), key, tempFile), + statistics); + } + + @Override + public FSDataOutputStream append(Path f, int bufferSize, Progressable progress) + { + throw new UnsupportedOperationException("append"); + } + + @Override + public boolean rename(Path src, Path dst) + throws IOException + { + boolean srcDirectory; + try { + srcDirectory = directory(src); + } + catch (FileNotFoundException e) { + return false; + } + + try { + if (!directory(dst)) { + // cannot copy a file to an existing file + return keysEqual(src, dst); + } + // move source under destination directory + dst = new Path(dst, src.getName()); + } + catch (FileNotFoundException e) { + // destination does not exist + } + + if (keysEqual(src, dst)) { + return true; + } + + if (srcDirectory) { + for (FileStatus file : listStatus(src)) { + rename(file.getPath(), new Path(dst, file.getPath().getName())); + } + deleteObject(keyFromPath(src) + DIRECTORY_SUFFIX); + } + else { + s3.copyObject(uri.getHost(), keyFromPath(src), uri.getHost(), keyFromPath(dst)); + delete(src, true); + } + + return true; + } + + @Override + public boolean delete(Path path, boolean recursive) + throws IOException + { + try { + if (!directory(path)) { + return deleteObject(keyFromPath(path)); + } + } + catch (FileNotFoundException e) { + return false; + } + + if (!recursive) { + throw new IOException("Directory " + path + " is not empty"); + } + + for (FileStatus file : listStatus(path)) { + delete(file.getPath(), true); + } + deleteObject(keyFromPath(path) + DIRECTORY_SUFFIX); + + return true; + } + + private boolean directory(Path path) + throws IOException + { + return HadoopFileStatus.isDirectory(getFileStatus(path)); + } + + private boolean deleteObject(String key) + { + try { + s3.deleteObject(uri.getHost(), key); + return true; + } + catch (AmazonClientException e) { + return false; + } + } + + @Override + public boolean mkdirs(Path f, FsPermission permission) + { + // no need to do anything for S3 + return true; + } + + private Iterator listPrefix(Path path) + { + String key = keyFromPath(path); + if (!key.isEmpty()) { + key += "/"; + } + + ListObjectsRequest request = new ListObjectsRequest() + .withBucketName(uri.getHost()) + .withPrefix(key) + .withDelimiter("/"); + + STATS.newListObjectsCall(); + Iterator listings = new AbstractSequentialIterator(s3.listObjects(request)) + { + @Override + protected ObjectListing computeNext(ObjectListing previous) + { + if (!previous.isTruncated()) { + return null; + } + return s3.listNextBatchOfObjects(previous); + } + }; + + return Iterators.concat(Iterators.transform(listings, this::statusFromListing)); + } + + private Iterator statusFromListing(ObjectListing listing) + { + return Iterators.concat( + statusFromPrefixes(listing.getCommonPrefixes()), + statusFromObjects(listing.getObjectSummaries())); + } + + private Iterator statusFromPrefixes(List prefixes) + { + List list = new ArrayList<>(); + for (String prefix : prefixes) { + Path path = qualifiedPath(new Path("/" + prefix)); + FileStatus status = new FileStatus(0, true, 1, 0, 0, path); + list.add(createLocatedFileStatus(status)); + } + return list.iterator(); + } + + private Iterator statusFromObjects(List objects) + { + return objects.stream() + .filter(object -> !object.getKey().endsWith("/")) + .map(object -> new FileStatus( + object.getSize(), + false, + 1, + BLOCK_SIZE.toBytes(), + object.getLastModified().getTime(), + qualifiedPath(new Path("/" + object.getKey())))) + .map(this::createLocatedFileStatus) + .iterator(); + } + + /** + * This exception is for stopping retries for S3 calls that shouldn't be retried. + * For example, "Caused by: com.amazonaws.services.s3.model.AmazonS3Exception: Forbidden (Service: Amazon S3; Status Code: 403 ..." + */ + private static class UnrecoverableS3OperationException + extends Exception + { + public UnrecoverableS3OperationException(Throwable cause) + { + super(cause); + } + } + + @VisibleForTesting + ObjectMetadata getS3ObjectMetadata(Path path) + throws IOException + { + try { + return retry() + .maxAttempts(maxAttempts) + .exponentialBackoff(new Duration(1, TimeUnit.SECONDS), maxBackoffTime, maxRetryTime, 2.0) + .stopOn(InterruptedException.class, UnrecoverableS3OperationException.class) + .onRetry(STATS::newGetMetadataRetry) + .run("getS3ObjectMetadata", () -> { + try { + STATS.newMetadataCall(); + return s3.getObjectMetadata(uri.getHost(), keyFromPath(path)); + } + catch (RuntimeException e) { + STATS.newGetMetadataError(); + if (e instanceof AmazonS3Exception) { + switch (((AmazonS3Exception) e).getStatusCode()) { + case SC_NOT_FOUND: + return null; + case SC_FORBIDDEN: + throw new UnrecoverableS3OperationException(e); + } + } + throw Throwables.propagate(e); + } + }); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw Throwables.propagate(e); + } + catch (Exception e) { + Throwables.propagateIfInstanceOf(e, IOException.class); + throw Throwables.propagate(e); + } + } + + private Path qualifiedPath(Path path) + { + return path.makeQualified(this.uri, getWorkingDirectory()); + } + + private LocatedFileStatus createLocatedFileStatus(FileStatus status) + { + try { + BlockLocation[] fakeLocation = getFileBlockLocations(status, 0, status.getLen()); + return new LocatedFileStatus(status, fakeLocation); + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } + + private static long lastModifiedTime(ObjectMetadata metadata) + { + Date date = metadata.getLastModified(); + return (date != null) ? date.getTime() : 0; + } + + private static boolean keysEqual(Path p1, Path p2) + { + return keyFromPath(p1).equals(keyFromPath(p2)); + } + + private static String keyFromPath(Path path) + { + checkArgument(path.isAbsolute(), "Path is not absolute: %s", path); + String key = nullToEmpty(path.toUri().getPath()); + if (key.startsWith("/")) { + key = key.substring(1); + } + if (key.endsWith("/")) { + key = key.substring(0, key.length() - 1); + } + return key; + } + + private AmazonS3Client createAmazonS3Client(URI uri, Configuration hadoopConfig, ClientConfiguration clientConfig) + { + // first try credentials from URI or static properties + try { + return new AmazonS3Client(new StaticCredentialsProvider(getAwsCredentials(uri, hadoopConfig)), clientConfig, METRIC_COLLECTOR); + } + catch (IllegalArgumentException ignored) { + } + + if (useInstanceCredentials) { + return new AmazonS3Client(new InstanceProfileCredentialsProvider(), clientConfig, METRIC_COLLECTOR); + } + + throw new RuntimeException("S3 credentials not configured"); + } + + private static AWSCredentials getAwsCredentials(URI uri, Configuration conf) + { + S3Credentials credentials = new S3Credentials(); + credentials.initialize(uri, conf); + return new BasicAWSCredentials(credentials.getAccessKey(), credentials.getSecretAccessKey()); + } + + private static class PrestoS3InputStream + extends FSInputStream + { + private final AmazonS3 s3; + private final String host; + private final Path path; + private final int maxAttempts; + private final Duration maxBackoffTime; + private final Duration maxRetryTime; + + private boolean closed; + private InputStream in; + private long streamPosition; + private long nextReadPosition; + + public PrestoS3InputStream(AmazonS3 s3, String host, Path path, int maxAttempts, Duration maxBackoffTime, Duration maxRetryTime) + { + this.s3 = checkNotNull(s3, "s3 is null"); + this.host = checkNotNull(host, "host is null"); + this.path = checkNotNull(path, "path is null"); + + checkArgument(maxAttempts >= 0, "maxAttempts cannot be negative"); + this.maxAttempts = maxAttempts; + this.maxBackoffTime = checkNotNull(maxBackoffTime, "maxBackoffTime is null"); + this.maxRetryTime = checkNotNull(maxRetryTime, "maxRetryTime is null"); + } + + @Override + public void close() + { + closed = true; + closeStream(); + } + + @Override + public void seek(long pos) + { + checkState(!closed, "already closed"); + checkArgument(pos >= 0, "position is negative: %s", pos); + + // this allows a seek beyond the end of the stream but the next read will fail + nextReadPosition = pos; + } + + @Override + public long getPos() + { + return nextReadPosition; + } + + @Override + public int read() + { + // This stream is wrapped with BufferedInputStream, so this method should never be called + throw new UnsupportedOperationException(); + } + + @Override + public int read(byte[] buffer, int offset, int length) + throws IOException + { + try { + int bytesRead = retry() + .maxAttempts(maxAttempts) + .exponentialBackoff(new Duration(1, TimeUnit.SECONDS), maxBackoffTime, maxRetryTime, 2.0) + .stopOn(InterruptedException.class, UnrecoverableS3OperationException.class) + .onRetry(STATS::newReadRetry) + .run("readStream", () -> { + seekStream(); + try { + return in.read(buffer, offset, length); + } + catch (Exception e) { + STATS.newReadError(e); + closeStream(); + throw e; + } + }); + + if (bytesRead != -1) { + streamPosition += bytesRead; + nextReadPosition += bytesRead; + } + return bytesRead; + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw Throwables.propagate(e); + } + catch (Exception e) { + Throwables.propagateIfInstanceOf(e, IOException.class); + throw Throwables.propagate(e); + } + } + + @Override + public boolean seekToNewSource(long targetPos) + { + return false; + } + + private void seekStream() + throws IOException, UnrecoverableS3OperationException + { + if ((in != null) && (nextReadPosition == streamPosition)) { + // already at specified position + return; + } + + if ((in != null) && (nextReadPosition > streamPosition)) { + // seeking forwards + long skip = nextReadPosition - streamPosition; + if (skip <= max(in.available(), MAX_SKIP_SIZE.toBytes())) { + // already buffered or seek is small enough + try { + if (in.skip(skip) == skip) { + streamPosition = nextReadPosition; + return; + } + } + catch (IOException ignored) { + // will retry by re-opening the stream + } + } + } + + // close the stream and open at desired position + streamPosition = nextReadPosition; + closeStream(); + openStream(); + } + + private void openStream() + throws IOException, UnrecoverableS3OperationException + { + if (in == null) { + in = openStream(path, nextReadPosition); + streamPosition = nextReadPosition; + STATS.connectionOpened(); + } + } + + private InputStream openStream(Path path, long start) + throws IOException, UnrecoverableS3OperationException + { + try { + return retry() + .maxAttempts(maxAttempts) + .exponentialBackoff(new Duration(1, TimeUnit.SECONDS), maxBackoffTime, maxRetryTime, 2.0) + .stopOn(InterruptedException.class, UnrecoverableS3OperationException.class) + .onRetry(STATS::newGetObjectRetry) + .run("getS3Object", () -> { + try { + GetObjectRequest request = new GetObjectRequest(host, keyFromPath(path)).withRange(start, Long.MAX_VALUE); + return s3.getObject(request).getObjectContent(); + } + catch (RuntimeException e) { + STATS.newGetObjectError(); + if (e instanceof AmazonS3Exception) { + switch (((AmazonS3Exception) e).getStatusCode()) { + case SC_REQUESTED_RANGE_NOT_SATISFIABLE: + // ignore request for start past end of object + return new ByteArrayInputStream(new byte[0]); + case SC_FORBIDDEN: + case SC_NOT_FOUND: + throw new UnrecoverableS3OperationException(e); + } + } + throw Throwables.propagate(e); + } + }); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw Throwables.propagate(e); + } + catch (Exception e) { + Throwables.propagateIfInstanceOf(e, IOException.class); + Throwables.propagateIfInstanceOf(e, UnrecoverableS3OperationException.class); + throw Throwables.propagate(e); + } + } + + private void closeStream() + { + if (in != null) { + try { + if (in instanceof S3ObjectInputStream) { + ((S3ObjectInputStream) in).abort(); + } + else { + in.close(); + } + } + catch (IOException | AbortedException ignored) { + // thrown if the current thread is in the interrupted state + } + in = null; + STATS.connectionReleased(); + } + } + } + + private static class PrestoS3OutputStream + extends FilterOutputStream + { + private final TransferManager transferManager; + private final String host; + private final String key; + private final File tempFile; + + private boolean closed; + + public PrestoS3OutputStream(AmazonS3 s3, TransferManagerConfiguration config, String host, String key, File tempFile) + throws IOException + { + super(new BufferedOutputStream(new FileOutputStream(checkNotNull(tempFile, "tempFile is null")))); + + transferManager = new TransferManager(checkNotNull(s3, "s3 is null")); + transferManager.setConfiguration(checkNotNull(config, "config is null")); + + this.host = checkNotNull(host, "host is null"); + this.key = checkNotNull(key, "key is null"); + this.tempFile = tempFile; + + log.debug("OutputStream for key '%s' using file: %s", key, tempFile); + } + + @Override + public void close() + throws IOException + { + if (closed) { + return; + } + closed = true; + + try { + super.close(); + uploadObject(); + } + finally { + if (!tempFile.delete()) { + log.warn("Could not delete temporary file: %s", tempFile); + } + // close transfer manager but keep underlying S3 client open + transferManager.shutdownNow(false); + } + } + + private void uploadObject() + throws IOException + { + try { + log.debug("Starting upload for host: %s, key: %s, file: %s, size: %s", host, key, tempFile, tempFile.length()); + STATS.uploadStarted(); + Upload upload = transferManager.upload(host, key, tempFile); + + if (log.isDebugEnabled()) { + upload.addProgressListener(createProgressListener(upload)); + } + + upload.waitForCompletion(); + STATS.uploadSuccessful(); + log.debug("Completed upload for host: %s, key: %s", host, key); + } + catch (AmazonClientException e) { + STATS.uploadFailed(); + throw new IOException(e); + } + catch (InterruptedException e) { + STATS.uploadFailed(); + Thread.currentThread().interrupt(); + throw new InterruptedIOException(); + } + } + + private ProgressListener createProgressListener(Transfer transfer) + { + return new ProgressListener() + { + private ProgressEventType previousType; + private double previousTransferred; + + @Override + public synchronized void progressChanged(ProgressEvent progressEvent) + { + ProgressEventType eventType = progressEvent.getEventType(); + if (previousType != eventType) { + log.debug("Upload progress event (%s/%s): %s", host, key, eventType); + previousType = eventType; + } + + double transferred = transfer.getProgress().getPercentTransferred(); + if (transferred >= (previousTransferred + 10.0)) { + log.debug("Upload percentage (%s/%s): %.0f%%", host, key, transferred); + previousTransferred = transferred; + } + } + }; + } + } + + @VisibleForTesting + AmazonS3 getS3Client() + { + return s3; + } + + @VisibleForTesting + void setS3Client(AmazonS3 client) + { + s3 = client; + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/PrestoS3FileSystemMetricCollector.java b/presto-hive/src/main/java/com/facebook/presto/hive/PrestoS3FileSystemMetricCollector.java new file mode 100644 index 00000000..dd34efbf --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/PrestoS3FileSystemMetricCollector.java @@ -0,0 +1,64 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.amazonaws.Request; +import com.amazonaws.Response; +import com.amazonaws.metrics.RequestMetricCollector; +import com.amazonaws.util.AWSRequestMetrics; +import com.amazonaws.util.TimingInfo; +import io.airlift.units.Duration; + +import static com.amazonaws.util.AWSRequestMetrics.Field; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +public class PrestoS3FileSystemMetricCollector + extends RequestMetricCollector +{ + private final PrestoS3FileSystemStats stats; + + public PrestoS3FileSystemMetricCollector(PrestoS3FileSystemStats stats) + { + this.stats = checkNotNull(stats, "stats is null"); + } + + @Override + public void collectMetrics(Request request, Response response) + { + AWSRequestMetrics metrics = request.getAWSRequestMetrics(); + + TimingInfo timingInfo = metrics.getTimingInfo(); + Number requestCounts = timingInfo.getCounter(Field.RequestCount.name()); + Number retryCounts = timingInfo.getCounter(Field.HttpClientRetryCount.name()); + Number throttleExceptions = timingInfo.getCounter(Field.ThrottleException.name()); + TimingInfo requestTime = timingInfo.getSubMeasurement(Field.HttpRequestTime.name()); + + if (requestCounts != null) { + stats.updateAwsRequestCount(requestCounts.longValue()); + } + + if (retryCounts != null) { + stats.updateAwsRetryCount(retryCounts.longValue()); + } + + if (throttleExceptions != null) { + stats.updateAwsThrottleExceptionsCount(throttleExceptions.longValue()); + } + + if (requestTime != null && requestTime.getTimeTakenMillisIfKnown() != null) { + stats.addAwsRequestTime(new Duration(requestTime.getTimeTakenMillisIfKnown(), MILLISECONDS)); + } + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/PrestoS3FileSystemStats.java b/presto-hive/src/main/java/com/facebook/presto/hive/PrestoS3FileSystemStats.java new file mode 100644 index 00000000..ce4e67f3 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/PrestoS3FileSystemStats.java @@ -0,0 +1,306 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.amazonaws.AbortedException; +import io.airlift.stats.CounterStat; +import io.airlift.stats.TimeStat; +import io.airlift.units.Duration; +import org.weakref.jmx.Managed; +import org.weakref.jmx.Nested; + +import java.net.SocketException; +import java.net.SocketTimeoutException; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +public class PrestoS3FileSystemStats +{ + private final CounterStat activeConnections = new CounterStat(); + private final CounterStat startedUploads = new CounterStat(); + private final CounterStat failedUploads = new CounterStat(); + private final CounterStat successfulUploads = new CounterStat(); + private final CounterStat metadataCalls = new CounterStat(); + private final CounterStat listStatusCalls = new CounterStat(); + private final CounterStat listLocatedStatusCalls = new CounterStat(); + private final CounterStat listObjectsCalls = new CounterStat(); + private final CounterStat otherReadErrors = new CounterStat(); + private final CounterStat awsAbortedExceptions = new CounterStat(); + private final CounterStat socketExceptions = new CounterStat(); + private final CounterStat socketTimeoutExceptions = new CounterStat(); + private final CounterStat getObjectErrors = new CounterStat(); + private final CounterStat getMetadataErrors = new CounterStat(); + private final CounterStat getObjectRetries = new CounterStat(); + private final CounterStat getMetadataRetries = new CounterStat(); + private final CounterStat readRetries = new CounterStat(); + + // see AWSRequestMetrics + private final CounterStat awsRequestCount = new CounterStat(); + private final CounterStat awsRetryCount = new CounterStat(); + private final CounterStat awsThrottleExceptions = new CounterStat(); + private final TimeStat awsRequestTime = new TimeStat(MILLISECONDS); + + @Managed + @Nested + public CounterStat getActiveConnections() + { + return activeConnections; + } + + @Managed + @Nested + public CounterStat getStartedUploads() + { + return startedUploads; + } + + @Managed + @Nested + public CounterStat getFailedUploads() + { + return failedUploads; + } + + @Managed + @Nested + public CounterStat getSuccessfulUploads() + { + return successfulUploads; + } + + @Managed + @Nested + public CounterStat getMetadataCalls() + { + return metadataCalls; + } + + @Managed + @Nested + public CounterStat getListStatusCalls() + { + return listStatusCalls; + } + + @Managed + @Nested + public CounterStat getListLocatedStatusCalls() + { + return listLocatedStatusCalls; + } + + @Managed + @Nested + public CounterStat getListObjectsCalls() + { + return listObjectsCalls; + } + + @Managed + @Nested + public CounterStat getGetObjectErrors() + { + return getObjectErrors; + } + + @Managed + @Nested + public CounterStat getGetMetadataErrors() + { + return getMetadataErrors; + } + + @Managed + @Nested + public CounterStat getOtherReadErrors() + { + return otherReadErrors; + } + + @Managed + @Nested + public CounterStat getSocketExceptions() + { + return socketExceptions; + } + + @Managed + @Nested + public CounterStat getSocketTimeoutExceptions() + { + return socketTimeoutExceptions; + } + + @Managed + @Nested + public CounterStat getAwsAbortedExceptions() + { + return awsAbortedExceptions; + } + + @Managed + @Nested + public CounterStat getAwsRequestCount() + { + return awsRequestCount; + } + + @Managed + @Nested + public CounterStat getAwsRetryCount() + { + return awsRetryCount; + } + + @Managed + @Nested + public CounterStat getAwsThrottleExceptions() + { + return awsThrottleExceptions; + } + + @Managed + @Nested + public TimeStat getAwsRequestTime() + { + return awsRequestTime; + } + + @Managed + @Nested + public CounterStat getGetObjectRetries() + { + return getObjectRetries; + } + + @Managed + @Nested + public CounterStat getGetMetadataRetries() + { + return getMetadataRetries; + } + + @Managed + @Nested + public CounterStat getReadRetries() + { + return readRetries; + } + + public void connectionOpened() + { + activeConnections.update(1); + } + + public void connectionReleased() + { + activeConnections.update(-1); + } + + public void uploadStarted() + { + startedUploads.update(1); + } + + public void uploadFailed() + { + failedUploads.update(1); + } + + public void uploadSuccessful() + { + successfulUploads.update(1); + } + + public void newMetadataCall() + { + metadataCalls.update(1); + } + + public void newListStatusCall() + { + listStatusCalls.update(1); + } + + public void newListLocatedStatusCall() + { + listLocatedStatusCalls.update(1); + } + + public void newListObjectsCall() + { + listObjectsCalls.update(1); + } + + public void newReadError(Exception e) + { + if (e instanceof SocketException) { + socketExceptions.update(1); + } + else if (e instanceof SocketTimeoutException) { + socketTimeoutExceptions.update(1); + } + else if (e instanceof AbortedException) { + awsAbortedExceptions.update(1); + } + else { + otherReadErrors.update(1); + } + } + + public void newGetObjectError() + { + getObjectErrors.update(1); + } + + public void newGetMetadataError() + { + getMetadataErrors.update(1); + } + + public void updateAwsRequestCount(long requestCount) + { + awsRequestCount.update(requestCount); + } + + public void updateAwsRetryCount(long retryCount) + { + awsRetryCount.update(retryCount); + } + + public void updateAwsThrottleExceptionsCount(long throttleExceptionsCount) + { + awsThrottleExceptions.update(throttleExceptionsCount); + } + + public void addAwsRequestTime(Duration duration) + { + awsRequestTime.add(duration); + } + + public void newGetObjectRetry() + { + getObjectRetries.update(1); + } + + public void newGetMetadataRetry() + { + getMetadataRetries.update(1); + } + + public void newReadRetry() + { + readRetries.update(1); + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/RebindSafeMBeanServer.java b/presto-hive/src/main/java/com/facebook/presto/hive/RebindSafeMBeanServer.java new file mode 100644 index 00000000..79bce17a --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/RebindSafeMBeanServer.java @@ -0,0 +1,333 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import io.airlift.log.Logger; + +import javax.annotation.concurrent.ThreadSafe; +import javax.management.Attribute; +import javax.management.AttributeList; +import javax.management.AttributeNotFoundException; +import javax.management.InstanceAlreadyExistsException; +import javax.management.InstanceNotFoundException; +import javax.management.IntrospectionException; +import javax.management.InvalidAttributeValueException; +import javax.management.ListenerNotFoundException; +import javax.management.MBeanException; +import javax.management.MBeanInfo; +import javax.management.MBeanRegistrationException; +import javax.management.MBeanServer; +import javax.management.NotCompliantMBeanException; +import javax.management.NotificationFilter; +import javax.management.NotificationListener; +import javax.management.ObjectInstance; +import javax.management.ObjectName; +import javax.management.OperationsException; +import javax.management.QueryExp; +import javax.management.ReflectionException; +import javax.management.loading.ClassLoaderRepository; + +import java.io.ObjectInputStream; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * MBeanServer wrapper that a ignores calls to registerMBean when there is already + * a MBean registered with the specified object name. + */ +@SuppressWarnings("deprecation") +@ThreadSafe +public class RebindSafeMBeanServer + implements MBeanServer +{ + private static final Logger log = Logger.get(RebindSafeMBeanServer.class); + + private final MBeanServer mbeanServer; + + public RebindSafeMBeanServer(MBeanServer mbeanServer) + { + this.mbeanServer = checkNotNull(mbeanServer, "mbeanServer is null"); + } + + /** + * Delegates to the wrapped mbean server, but if a mbean is already registered + * with the specified name, the existing instance is returned. + */ + @Override + public ObjectInstance registerMBean(Object object, ObjectName name) + throws MBeanRegistrationException, NotCompliantMBeanException + { + while (true) { + try { + // try to register the mbean + return mbeanServer.registerMBean(object, name); + } + catch (InstanceAlreadyExistsException ignored) { + } + + try { + // a mbean is already installed, try to return the already registered instance + ObjectInstance objectInstance = mbeanServer.getObjectInstance(name); + log.debug("%s already bound to %s", name, objectInstance); + return objectInstance; + } + catch (InstanceNotFoundException ignored) { + // the mbean was removed before we could get the reference + // start the whole process over again + } + } + } + + @Override + public void unregisterMBean(ObjectName name) + throws InstanceNotFoundException, MBeanRegistrationException + { + mbeanServer.unregisterMBean(name); + } + + @Override + public ObjectInstance getObjectInstance(ObjectName name) + throws InstanceNotFoundException + { + return mbeanServer.getObjectInstance(name); + } + + @Override + public Set queryMBeans(ObjectName name, QueryExp query) + { + return mbeanServer.queryMBeans(name, query); + } + + @Override + public Set queryNames(ObjectName name, QueryExp query) + { + return mbeanServer.queryNames(name, query); + } + + @Override + public boolean isRegistered(ObjectName name) + { + return mbeanServer.isRegistered(name); + } + + @Override + public Integer getMBeanCount() + { + return mbeanServer.getMBeanCount(); + } + + @Override + public Object getAttribute(ObjectName name, String attribute) + throws MBeanException, AttributeNotFoundException, InstanceNotFoundException, ReflectionException + { + return mbeanServer.getAttribute(name, attribute); + } + + @Override + public AttributeList getAttributes(ObjectName name, String[] attributes) + throws InstanceNotFoundException, ReflectionException + { + return mbeanServer.getAttributes(name, attributes); + } + + @Override + public void setAttribute(ObjectName name, Attribute attribute) + throws InstanceNotFoundException, AttributeNotFoundException, InvalidAttributeValueException, MBeanException, ReflectionException + { + mbeanServer.setAttribute(name, attribute); + } + + @Override + public AttributeList setAttributes(ObjectName name, AttributeList attributes) + throws InstanceNotFoundException, ReflectionException + { + return mbeanServer.setAttributes(name, attributes); + } + + @Override + public Object invoke(ObjectName name, String operationName, Object[] params, String[] signature) + throws InstanceNotFoundException, MBeanException, ReflectionException + { + return mbeanServer.invoke(name, operationName, params, signature); + } + + @Override + public String getDefaultDomain() + { + return mbeanServer.getDefaultDomain(); + } + + @Override + public String[] getDomains() + { + return mbeanServer.getDomains(); + } + + @Override + public void addNotificationListener(ObjectName name, NotificationListener listener, NotificationFilter filter, Object context) + throws InstanceNotFoundException + { + mbeanServer.addNotificationListener(name, listener, filter, context); + } + + @Override + public void addNotificationListener(ObjectName name, ObjectName listener, NotificationFilter filter, Object context) + throws InstanceNotFoundException + { + mbeanServer.addNotificationListener(name, listener, filter, context); + } + + @Override + public void removeNotificationListener(ObjectName name, ObjectName listener) + throws InstanceNotFoundException, ListenerNotFoundException + { + mbeanServer.removeNotificationListener(name, listener); + } + + @Override + public void removeNotificationListener(ObjectName name, ObjectName listener, NotificationFilter filter, Object context) + throws InstanceNotFoundException, ListenerNotFoundException + { + mbeanServer.removeNotificationListener(name, listener, filter, context); + } + + @Override + public void removeNotificationListener(ObjectName name, NotificationListener listener) + throws InstanceNotFoundException, ListenerNotFoundException + { + mbeanServer.removeNotificationListener(name, listener); + } + + @Override + public void removeNotificationListener(ObjectName name, NotificationListener listener, NotificationFilter filter, Object context) + throws InstanceNotFoundException, ListenerNotFoundException + { + mbeanServer.removeNotificationListener(name, listener, filter, context); + } + + @Override + public MBeanInfo getMBeanInfo(ObjectName name) + throws InstanceNotFoundException, IntrospectionException, ReflectionException + { + return mbeanServer.getMBeanInfo(name); + } + + @Override + public boolean isInstanceOf(ObjectName name, String className) + throws InstanceNotFoundException + { + return mbeanServer.isInstanceOf(name, className); + } + + @Override + public Object instantiate(String className) + throws ReflectionException, MBeanException + { + return mbeanServer.instantiate(className); + } + + @Override + public Object instantiate(String className, ObjectName loaderName) + throws ReflectionException, MBeanException, InstanceNotFoundException + { + return mbeanServer.instantiate(className, loaderName); + } + + @Override + public Object instantiate(String className, Object[] params, String[] signature) + throws ReflectionException, MBeanException + { + return mbeanServer.instantiate(className, params, signature); + } + + @Override + public Object instantiate(String className, ObjectName loaderName, Object[] params, String[] signature) + throws ReflectionException, MBeanException, InstanceNotFoundException + { + return mbeanServer.instantiate(className, loaderName, params, signature); + } + + @Override + @Deprecated + public ObjectInputStream deserialize(ObjectName name, byte[] data) + throws OperationsException + { + return mbeanServer.deserialize(name, data); + } + + @Override + @Deprecated + public ObjectInputStream deserialize(String className, byte[] data) + throws OperationsException, ReflectionException + { + return mbeanServer.deserialize(className, data); + } + + @Override + @Deprecated + public ObjectInputStream deserialize(String className, ObjectName loaderName, byte[] data) + throws OperationsException, ReflectionException + { + return mbeanServer.deserialize(className, loaderName, data); + } + + @Override + public ClassLoader getClassLoaderFor(ObjectName mbeanName) + throws InstanceNotFoundException + { + return mbeanServer.getClassLoaderFor(mbeanName); + } + + @Override + public ClassLoader getClassLoader(ObjectName loaderName) + throws InstanceNotFoundException + { + return mbeanServer.getClassLoader(loaderName); + } + + @Override + public ClassLoaderRepository getClassLoaderRepository() + { + return mbeanServer.getClassLoaderRepository(); + } + + @Override + public ObjectInstance createMBean(String className, ObjectName name) + throws ReflectionException, InstanceAlreadyExistsException, MBeanException, NotCompliantMBeanException + { + return mbeanServer.createMBean(className, name); + } + + @Override + public ObjectInstance createMBean(String className, ObjectName name, ObjectName loaderName) + throws ReflectionException, InstanceAlreadyExistsException, MBeanException, NotCompliantMBeanException, InstanceNotFoundException + { + return mbeanServer.createMBean(className, name, loaderName); + } + + @Override + public ObjectInstance createMBean(String className, ObjectName name, Object[] params, String[] signature) + throws ReflectionException, InstanceAlreadyExistsException, MBeanException, NotCompliantMBeanException + { + return mbeanServer.createMBean(className, name, params, signature); + } + + @Override + public ObjectInstance createMBean(String className, ObjectName name, ObjectName loaderName, Object[] params, String[] signature) + throws ReflectionException, InstanceAlreadyExistsException, MBeanException, NotCompliantMBeanException, InstanceNotFoundException + { + return mbeanServer.createMBean(className, name, loaderName, params, signature); + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/RetryDriver.java b/presto-hive/src/main/java/com/facebook/presto/hive/RetryDriver.java new file mode 100644 index 00000000..655c496c --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/RetryDriver.java @@ -0,0 +1,144 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.google.common.collect.ImmutableList; +import io.airlift.log.Logger; +import io.airlift.units.Duration; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class RetryDriver +{ + private static final Logger log = Logger.get(RetryDriver.class); + private static final int DEFAULT_RETRY_ATTEMPTS = 10; + private static final Duration DEFAULT_SLEEP_TIME = Duration.valueOf("1s"); + private static final Duration DEFAULT_MAX_RETRY_TIME = Duration.valueOf("30s"); + private static final double DEFAULT_SCALE_FACTOR = 2.0; + + private final int maxAttempts; + private final Duration minSleepTime; + private final Duration maxSleepTime; + private final double scaleFactor; + private final Duration maxRetryTime; + private final List> exceptionWhiteList; + private final Optional retryRunnable; + + private RetryDriver( + int maxAttempts, + Duration minSleepTime, + Duration maxSleepTime, + double scaleFactor, + Duration maxRetryTime, + List> exceptionWhiteList, + Optional retryRunnable) + { + this.maxAttempts = maxAttempts; + this.minSleepTime = minSleepTime; + this.maxSleepTime = maxSleepTime; + this.scaleFactor = scaleFactor; + this.maxRetryTime = maxRetryTime; + this.exceptionWhiteList = exceptionWhiteList; + this.retryRunnable = retryRunnable; + } + + private RetryDriver() + { + this(DEFAULT_RETRY_ATTEMPTS, + DEFAULT_SLEEP_TIME, + DEFAULT_SLEEP_TIME, + DEFAULT_SCALE_FACTOR, + DEFAULT_MAX_RETRY_TIME, + ImmutableList.of(), + Optional.empty()); + } + + public static RetryDriver retry() + { + return new RetryDriver(); + } + + public final RetryDriver maxAttempts(int maxAttempts) + { + return new RetryDriver(maxAttempts, minSleepTime, maxSleepTime, scaleFactor, maxRetryTime, exceptionWhiteList, retryRunnable); + } + + public final RetryDriver exponentialBackoff(Duration minSleepTime, Duration maxSleepTime, Duration maxRetryTime, double scaleFactor) + { + return new RetryDriver(maxAttempts, minSleepTime, maxSleepTime, scaleFactor, maxRetryTime, exceptionWhiteList, retryRunnable); + } + + public final RetryDriver onRetry(Runnable retryRunnable) + { + return new RetryDriver(maxAttempts, minSleepTime, maxSleepTime, scaleFactor, maxRetryTime, exceptionWhiteList, Optional.ofNullable(retryRunnable)); + } + + @SafeVarargs + public final RetryDriver stopOn(Class... classes) + { + checkNotNull(classes, "classes is null"); + List> exceptions = ImmutableList.>builder() + .addAll(exceptionWhiteList) + .addAll(Arrays.asList(classes)) + .build(); + + return new RetryDriver(maxAttempts, minSleepTime, maxSleepTime, scaleFactor, maxRetryTime, exceptions, retryRunnable); + } + + public RetryDriver stopOnIllegalExceptions() + { + return stopOn(NullPointerException.class, IllegalStateException.class, IllegalArgumentException.class); + } + + public V run(String callableName, Callable callable) + throws Exception + { + checkNotNull(callableName, "callableName is null"); + checkNotNull(callable, "callable is null"); + + long startTime = System.nanoTime(); + int attempt = 0; + while (true) { + attempt++; + + if (attempt > 1) { + retryRunnable.ifPresent(Runnable::run); + } + + try { + return callable.call(); + } + catch (Exception e) { + for (Class clazz : exceptionWhiteList) { + if (clazz.isInstance(e)) { + throw e; + } + } + if (attempt >= maxAttempts || Duration.nanosSince(startTime).compareTo(maxRetryTime) >= 0) { + throw e; + } + log.debug("Failed on executing %s with attempt %d, will retry. Exception: %s", callableName, attempt, e.getMessage()); + + int delayInMs = (int) Math.min(minSleepTime.toMillis() * Math.pow(scaleFactor, attempt - 1), maxSleepTime.toMillis()); + TimeUnit.MILLISECONDS.sleep(delayInMs); + } + } + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/StaticHiveCluster.java b/presto-hive/src/main/java/com/facebook/presto/hive/StaticHiveCluster.java new file mode 100644 index 00000000..e68970c2 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/StaticHiveCluster.java @@ -0,0 +1,48 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.google.common.net.HostAndPort; +import org.apache.thrift.transport.TTransportException; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class StaticHiveCluster + implements HiveCluster +{ + private final HostAndPort address; + private final HiveMetastoreClientFactory clientFactory; + + public StaticHiveCluster(HostAndPort address, HiveMetastoreClientFactory clientFactory) + { + checkNotNull(address, "address is null"); + checkArgument(address.hasPort(), "address does not have a port"); + checkNotNull(clientFactory, "clientFactory is null"); + + this.address = address; + this.clientFactory = clientFactory; + } + + @Override + public HiveMetastoreClient createMetastoreClient() + { + try { + return clientFactory.create(address.getHostText(), address.getPort()); + } + catch (TTransportException e) { + throw new RuntimeException("Failed connecting to Hive metastore: " + address, e); + } + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/TableAlreadyExistsException.java b/presto-hive/src/main/java/com/facebook/presto/hive/TableAlreadyExistsException.java new file mode 100644 index 00000000..2920ae65 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/TableAlreadyExistsException.java @@ -0,0 +1,41 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.spi.NotFoundException; +import com.facebook.presto.spi.SchemaTableName; + +import static java.lang.String.format; + +public class TableAlreadyExistsException + extends NotFoundException +{ + private final SchemaTableName tableName; + + public TableAlreadyExistsException(SchemaTableName tableName) + { + this(tableName, format("Table already exists: '%s'", tableName)); + } + + public TableAlreadyExistsException(SchemaTableName tableName, String message) + { + super(message); + this.tableName = tableName; + } + + public SchemaTableName getTableName() + { + return tableName; + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/TableOfflineException.java b/presto-hive/src/main/java/com/facebook/presto/hive/TableOfflineException.java new file mode 100644 index 00000000..777d9508 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/TableOfflineException.java @@ -0,0 +1,42 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.SchemaTableName; + +import static com.facebook.presto.hive.HiveErrorCode.HIVE_TABLE_OFFLINE; +import static com.google.common.base.Preconditions.checkNotNull; + +public class TableOfflineException + extends PrestoException +{ + private final SchemaTableName tableName; + + public TableOfflineException(SchemaTableName tableName) + { + this(tableName, String.format("Table '%s' is offline", tableName)); + } + + public TableOfflineException(SchemaTableName tableName, String message) + { + super(HIVE_TABLE_OFFLINE, message); + this.tableName = checkNotNull(tableName, "tableName is null"); + } + + public SchemaTableName getTableName() + { + return tableName; + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/UnpartitionedPartition.java b/presto-hive/src/main/java/com/facebook/presto/hive/UnpartitionedPartition.java new file mode 100644 index 00000000..b6fe1d9a --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/UnpartitionedPartition.java @@ -0,0 +1,39 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import org.apache.hadoop.hive.metastore.api.Partition; + +final class UnpartitionedPartition + extends Partition +{ + static final Partition UNPARTITIONED_PARTITION = new UnpartitionedPartition(); + + private UnpartitionedPartition() + { + } + + @Override + public UnpartitionedPartition clone() + throws CloneNotSupportedException + { + throw new CloneNotSupportedException(); + } + + @SuppressWarnings("ObjectEquality") + static boolean isUnpartitioned(Partition partition) + { + return partition == UNPARTITIONED_PARTITION; + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/ViewAlreadyExistsException.java b/presto-hive/src/main/java/com/facebook/presto/hive/ViewAlreadyExistsException.java new file mode 100644 index 00000000..8819a7c8 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/ViewAlreadyExistsException.java @@ -0,0 +1,41 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.spi.NotFoundException; +import com.facebook.presto.spi.SchemaTableName; + +import static java.lang.String.format; + +public class ViewAlreadyExistsException + extends NotFoundException +{ + private final SchemaTableName viewName; + + public ViewAlreadyExistsException(SchemaTableName viewName) + { + this(viewName, format("View already exists: '%s'", viewName)); + } + + public ViewAlreadyExistsException(SchemaTableName viewName, String message) + { + super(message); + this.viewName = viewName; + } + + public SchemaTableName getViewName() + { + return viewName; + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/metastore/CachingHiveMetastore.java b/presto-hive/src/main/java/com/facebook/presto/hive/metastore/CachingHiveMetastore.java new file mode 100644 index 00000000..dd079e62 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/metastore/CachingHiveMetastore.java @@ -0,0 +1,903 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive.metastore; + +import com.facebook.presto.hive.ForHiveMetastore; +import com.facebook.presto.hive.HiveClientConfig; +import com.facebook.presto.hive.HiveCluster; +import com.facebook.presto.hive.HiveMetastoreClient; +import com.facebook.presto.hive.HiveViewNotSupportedException; +import com.facebook.presto.hive.TableAlreadyExistsException; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.spi.TableNotFoundException; +import com.google.common.base.Throwables; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; +import com.google.common.util.concurrent.UncheckedExecutionException; +import io.airlift.units.Duration; +import org.apache.hadoop.hive.common.FileUtils; +import org.apache.hadoop.hive.metastore.TableType; +import org.apache.hadoop.hive.metastore.Warehouse; +import org.apache.hadoop.hive.metastore.api.AlreadyExistsException; +import org.apache.hadoop.hive.metastore.api.Database; +import org.apache.hadoop.hive.metastore.api.InvalidObjectException; +import org.apache.hadoop.hive.metastore.api.InvalidOperationException; +import org.apache.hadoop.hive.metastore.api.MetaException; +import org.apache.hadoop.hive.metastore.api.NoSuchObjectException; +import org.apache.hadoop.hive.metastore.api.Partition; +import org.apache.hadoop.hive.metastore.api.StorageDescriptor; +import org.apache.hadoop.hive.metastore.api.Table; +import org.apache.hadoop.hive.metastore.api.UnknownDBException; +import org.apache.thrift.TException; +import org.apache.thrift.protocol.TBinaryProtocol; +import org.apache.thrift.transport.TMemoryBuffer; +import org.weakref.jmx.Flatten; +import org.weakref.jmx.Managed; + +import javax.annotation.concurrent.ThreadSafe; +import javax.inject.Inject; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; + +import static com.facebook.presto.hive.HiveErrorCode.HIVE_METASTORE_ERROR; +import static com.facebook.presto.hive.HiveUtil.PRESTO_VIEW_FLAG; +import static com.facebook.presto.hive.HiveUtil.isPrestoView; +import static com.facebook.presto.hive.RetryDriver.retry; +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.cache.CacheLoader.asyncReloading; +import static com.google.common.collect.Iterables.transform; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.apache.hadoop.hive.metastore.api.hive_metastoreConstants.HIVE_FILTER_FIELD_PARAMS; + +/** + * Hive Metastore Cache + */ +@ThreadSafe +public class CachingHiveMetastore + implements HiveMetastore +{ + private final CachingHiveMetastoreStats stats = new CachingHiveMetastoreStats(); + protected final HiveCluster clientProvider; + private final LoadingCache> databaseNamesCache; + private final LoadingCache databaseCache; + private final LoadingCache> tableNamesCache; + private final LoadingCache> viewNamesCache; + private final LoadingCache> partitionNamesCache; + private final LoadingCache tableCache; + private final LoadingCache partitionCache; + private final LoadingCache> partitionFilterCache; + + @Inject + public CachingHiveMetastore(HiveCluster hiveCluster, @ForHiveMetastore ExecutorService executor, HiveClientConfig hiveClientConfig) + { + this(checkNotNull(hiveCluster, "hiveCluster is null"), + checkNotNull(executor, "executor is null"), + checkNotNull(hiveClientConfig, "hiveClientConfig is null").getMetastoreCacheTtl(), + hiveClientConfig.getMetastoreRefreshInterval()); + } + + public CachingHiveMetastore(HiveCluster hiveCluster, ExecutorService executor, Duration cacheTtl, Duration refreshInterval) + { + this.clientProvider = checkNotNull(hiveCluster, "hiveCluster is null"); + + long expiresAfterWriteMillis = checkNotNull(cacheTtl, "cacheTtl is null").toMillis(); + long refreshMills = checkNotNull(refreshInterval, "refreshInterval is null").toMillis(); + + databaseNamesCache = CacheBuilder.newBuilder() + .expireAfterWrite(expiresAfterWriteMillis, MILLISECONDS) + .refreshAfterWrite(refreshMills, MILLISECONDS) + .build(asyncReloading(new CacheLoader>() + { + @Override + public List load(String key) + throws Exception + { + return loadAllDatabases(); + } + }, executor)); + + databaseCache = CacheBuilder.newBuilder() + .expireAfterWrite(expiresAfterWriteMillis, MILLISECONDS) + .refreshAfterWrite(refreshMills, MILLISECONDS) + .build(asyncReloading(new CacheLoader() + { + @Override + public Database load(String databaseName) + throws Exception + { + return loadDatabase(databaseName); + } + }, executor)); + + tableNamesCache = CacheBuilder.newBuilder() + .expireAfterWrite(expiresAfterWriteMillis, MILLISECONDS) + .refreshAfterWrite(refreshMills, MILLISECONDS) + .build(asyncReloading(new CacheLoader>() + { + @Override + public List load(String databaseName) + throws Exception + { + return loadAllTables(databaseName); + } + }, executor)); + + tableCache = CacheBuilder.newBuilder() + .expireAfterWrite(expiresAfterWriteMillis, MILLISECONDS) + .refreshAfterWrite(refreshMills, MILLISECONDS) + .build(asyncReloading(new CacheLoader() + { + @Override + public Table load(HiveTableName hiveTableName) + throws Exception + { + return loadTable(hiveTableName); + } + }, executor)); + + viewNamesCache = CacheBuilder.newBuilder() + .expireAfterWrite(expiresAfterWriteMillis, MILLISECONDS) + .refreshAfterWrite(refreshMills, MILLISECONDS) + .build(asyncReloading(new CacheLoader>() + { + @Override + public List load(String databaseName) + throws Exception + { + return loadAllViews(databaseName); + } + }, executor)); + + partitionNamesCache = CacheBuilder.newBuilder() + .expireAfterWrite(expiresAfterWriteMillis, MILLISECONDS) + .refreshAfterWrite(refreshMills, MILLISECONDS) + .build(asyncReloading(new CacheLoader>() + { + @Override + public List load(HiveTableName hiveTableName) + throws Exception + { + return loadPartitionNames(hiveTableName); + } + }, executor)); + + partitionFilterCache = CacheBuilder.newBuilder() + .expireAfterWrite(expiresAfterWriteMillis, MILLISECONDS) + .refreshAfterWrite(refreshMills, MILLISECONDS) + .build(asyncReloading(new CacheLoader>() + { + @Override + public List load(PartitionFilter partitionFilter) + throws Exception + { + return loadPartitionNamesByParts(partitionFilter); + } + }, executor)); + + partitionCache = CacheBuilder.newBuilder() + .expireAfterWrite(expiresAfterWriteMillis, MILLISECONDS) + .refreshAfterWrite(refreshMills, MILLISECONDS) + .build(asyncReloading(new CacheLoader() + { + @Override + public Partition load(HivePartitionName partitionName) + throws Exception + { + return loadPartitionByName(partitionName); + } + + @Override + public Map loadAll(Iterable partitionNames) + throws Exception + { + return loadPartitionsByNames(partitionNames); + } + }, executor)); + } + + @Managed + @Flatten + public CachingHiveMetastoreStats getStats() + { + return stats; + } + + @Override + @Managed + public void flushCache() + { + databaseNamesCache.invalidateAll(); + tableNamesCache.invalidateAll(); + viewNamesCache.invalidateAll(); + partitionNamesCache.invalidateAll(); + databaseCache.invalidateAll(); + tableCache.invalidateAll(); + partitionCache.invalidateAll(); + partitionFilterCache.invalidateAll(); + } + + private static V get(LoadingCache cache, K key, Class exceptionClass) + throws E + { + try { + return cache.get(key); + } + catch (ExecutionException | UncheckedExecutionException e) { + Throwable t = e.getCause(); + Throwables.propagateIfInstanceOf(t, exceptionClass); + throw Throwables.propagate(t); + } + } + + private static Map getAll(LoadingCache cache, Iterable keys, Class exceptionClass) + throws E + { + try { + return cache.getAll(keys); + } + catch (ExecutionException | UncheckedExecutionException e) { + Throwable t = e.getCause(); + Throwables.propagateIfInstanceOf(t, exceptionClass); + throw Throwables.propagate(t); + } + } + + @Override + public List getAllDatabases() + { + return get(databaseNamesCache, "", RuntimeException.class); + } + + private List loadAllDatabases() + throws Exception + { + try { + return retry() + .stopOnIllegalExceptions() + .run("getAllDatabases", stats.getGetAllDatabases().wrap(() -> { + try (HiveMetastoreClient client = clientProvider.createMetastoreClient()) { + return client.get_all_databases(); + } + })); + } + catch (TException e) { + throw new PrestoException(HIVE_METASTORE_ERROR, e); + } + } + + @Override + public Database getDatabase(String databaseName) + throws NoSuchObjectException + { + return get(databaseCache, databaseName, NoSuchObjectException.class); + } + + private Database loadDatabase(final String databaseName) + throws Exception + { + try { + return retry() + .stopOn(NoSuchObjectException.class) + .stopOnIllegalExceptions() + .run("getDatabase", stats.getGetDatabase().wrap(() -> { + try (HiveMetastoreClient client = clientProvider.createMetastoreClient()) { + return client.get_database(databaseName); + } + })); + } + catch (NoSuchObjectException e) { + throw e; + } + catch (TException e) { + throw new PrestoException(HIVE_METASTORE_ERROR, e); + } + } + + @Override + public List getAllTables(String databaseName) + throws NoSuchObjectException + { + return get(tableNamesCache, databaseName, NoSuchObjectException.class); + } + + private List loadAllTables(final String databaseName) + throws Exception + { + final Callable> getAllTables = stats.getGetAllTables().wrap(() -> { + try (HiveMetastoreClient client = clientProvider.createMetastoreClient()) { + return client.get_all_tables(databaseName); + } + }); + + final Callable getDatabase = stats.getGetDatabase().wrap(() -> { + try (HiveMetastoreClient client = clientProvider.createMetastoreClient()) { + client.get_database(databaseName); + return null; + } + }); + + try { + return retry() + .stopOn(NoSuchObjectException.class) + .stopOnIllegalExceptions() + .run("getAllTables", () -> { + List tables = getAllTables.call(); + if (tables.isEmpty()) { + // Check to see if the database exists + getDatabase.call(); + } + return tables; + }); + } + catch (NoSuchObjectException e) { + throw e; + } + catch (TException e) { + throw new PrestoException(HIVE_METASTORE_ERROR, e); + } + } + + @Override + public Table getTable(String databaseName, String tableName) + throws NoSuchObjectException + { + return get(tableCache, HiveTableName.table(databaseName, tableName), NoSuchObjectException.class); + } + + @Override + public List getAllViews(String databaseName) + throws NoSuchObjectException + { + return get(viewNamesCache, databaseName, NoSuchObjectException.class); + } + + private List loadAllViews(final String databaseName) + throws Exception + { + try { + return retry() + .stopOn(UnknownDBException.class) + .stopOnIllegalExceptions() + .run("getAllViews", stats.getAllViews().wrap(() -> { + try (HiveMetastoreClient client = clientProvider.createMetastoreClient()) { + String filter = HIVE_FILTER_FIELD_PARAMS + PRESTO_VIEW_FLAG + " = \"true\""; + return client.get_table_names_by_filter(databaseName, filter, (short) -1); + } + })); + } + catch (UnknownDBException e) { + throw new NoSuchObjectException(e.getMessage()); + } + catch (TException e) { + throw new PrestoException(HIVE_METASTORE_ERROR, e); + } + } + + @Override + public void createTable(final Table table) + { + try { + retry() + .stopOn(AlreadyExistsException.class, InvalidObjectException.class, MetaException.class, NoSuchObjectException.class) + .stopOnIllegalExceptions() + .run("createTable", stats.getCreateTable().wrap(() -> { + try (HiveMetastoreClient client = clientProvider.createMetastoreClient()) { + client.create_table(table); + } + return null; + })); + } + catch (AlreadyExistsException e) { + throw new TableAlreadyExistsException(new SchemaTableName(table.getDbName(), table.getTableName())); + } + catch (InvalidObjectException | NoSuchObjectException | MetaException e) { + throw Throwables.propagate(e); + } + catch (TException e) { + throw new PrestoException(HIVE_METASTORE_ERROR, e); + } + catch (Exception e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + throw Throwables.propagate(e); + } + finally { + tableNamesCache.invalidate(table.getDbName()); + viewNamesCache.invalidate(table.getDbName()); + } + } + + @Override + public void dropTable(final String databaseName, final String tableName) + { + try { + retry() + .stopOn(NoSuchObjectException.class) + .stopOnIllegalExceptions() + .run("dropTable", stats.getDropTable().wrap(() -> { + try (HiveMetastoreClient client = clientProvider.createMetastoreClient()) { + client.drop_table(databaseName, tableName, true); + } + return null; + })); + } + catch (NoSuchObjectException e) { + throw new TableNotFoundException(new SchemaTableName(databaseName, tableName)); + } + catch (TException e) { + throw new PrestoException(HIVE_METASTORE_ERROR, e); + } + catch (Exception e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + throw Throwables.propagate(e); + } + finally { + invalidatePartitionCaches(databaseName, tableName); + tableCache.invalidate(new HiveTableName(databaseName, tableName)); + tableNamesCache.invalidate(databaseName); + viewNamesCache.invalidate(databaseName); + } + } + + @Override + public void renameTable(final String databaseName, final String tableName, final String newDatabaseName, final String newTableName) + { + try { + retry() + .stopOn(InvalidOperationException.class, MetaException.class) + .stopOnIllegalExceptions() + .run("renameTable", stats.getRenameTable().wrap(() -> { + try (HiveMetastoreClient client = clientProvider.createMetastoreClient()) { + Table table = new Table(loadTable(new HiveTableName(databaseName, tableName))); + table.setDbName(newDatabaseName); + table.setTableName(newTableName); + client.alter_table(databaseName, tableName, table); + } + return null; + })); + } + catch (InvalidOperationException | MetaException e) { + throw Throwables.propagate(e); + } + catch (TException e) { + throw new PrestoException(HIVE_METASTORE_ERROR, e); + } + catch (Exception e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + throw Throwables.propagate(e); + } + finally { + tableCache.invalidate(new HiveTableName(databaseName, tableName)); + tableNamesCache.invalidate(databaseName); + viewNamesCache.invalidate(databaseName); + } + } + + private Table loadTable(final HiveTableName hiveTableName) + throws Exception + { + try { + return retry() + .stopOn(NoSuchObjectException.class, HiveViewNotSupportedException.class) + .stopOnIllegalExceptions() + .run("getTable", stats.getGetTable().wrap(() -> { + try (HiveMetastoreClient client = clientProvider.createMetastoreClient()) { + Table table = client.get_table(hiveTableName.getDatabaseName(), hiveTableName.getTableName()); + if (table.getTableType().equals(TableType.VIRTUAL_VIEW.name()) && (!isPrestoView(table))) { + throw new HiveViewNotSupportedException(new SchemaTableName(hiveTableName.getDatabaseName(), hiveTableName.getTableName())); + } + return table; + } + })); + } + catch (NoSuchObjectException | HiveViewNotSupportedException e) { + throw e; + } + catch (TException e) { + throw new PrestoException(HIVE_METASTORE_ERROR, e); + } + } + + @Override + public List getPartitionNames(String databaseName, String tableName) + throws NoSuchObjectException + { + return get(partitionNamesCache, HiveTableName.table(databaseName, tableName), NoSuchObjectException.class); + } + + private List loadPartitionNames(final HiveTableName hiveTableName) + throws Exception + { + try { + return retry() + .stopOn(NoSuchObjectException.class) + .stopOnIllegalExceptions() + .run("getPartitionNames", stats.getGetPartitionNames().wrap(() -> { + try (HiveMetastoreClient client = clientProvider.createMetastoreClient()) { + return client.get_partition_names(hiveTableName.getDatabaseName(), hiveTableName.getTableName(), (short) 0); + } + })); + } + catch (NoSuchObjectException e) { + throw e; + } + catch (TException e) { + throw new PrestoException(HIVE_METASTORE_ERROR, e); + } + } + + @Override + public List getPartitionNamesByParts(String databaseName, String tableName, List parts) + throws NoSuchObjectException + { + return get(partitionFilterCache, PartitionFilter.partitionFilter(databaseName, tableName, parts), NoSuchObjectException.class); + } + + private List loadPartitionNamesByParts(final PartitionFilter partitionFilter) + throws Exception + { + try { + return retry() + .stopOn(NoSuchObjectException.class) + .stopOnIllegalExceptions() + .run("getPartitionNamesByParts", stats.getGetPartitionNamesPs().wrap(() -> { + try (HiveMetastoreClient client = clientProvider.createMetastoreClient()) { + return client.get_partition_names_ps(partitionFilter.getHiveTableName().getDatabaseName(), + partitionFilter.getHiveTableName().getTableName(), + partitionFilter.getParts(), + (short) -1); + } + })); + } + catch (NoSuchObjectException e) { + throw e; + } + catch (TException e) { + throw new PrestoException(HIVE_METASTORE_ERROR, e); + } + } + + public Map getPartitionsByNames(String databaseName, String tableName, List partitionNames) + throws NoSuchObjectException + { + Iterable names = transform(partitionNames, name -> HivePartitionName.partition(databaseName, tableName, name)); + + ImmutableMap.Builder partitionsByName = ImmutableMap.builder(); + Map all = getAll(partitionCache, names, NoSuchObjectException.class); + for (Entry entry : all.entrySet()) { + partitionsByName.put(entry.getKey().getPartitionName(), entry.getValue()); + } + return partitionsByName.build(); + } + + private Partition loadPartitionByName(final HivePartitionName partitionName) + throws Exception + { + checkNotNull(partitionName, "partitionName is null"); + try { + return retry() + .stopOn(NoSuchObjectException.class) + .stopOnIllegalExceptions() + .run("getPartitionsByNames", stats.getGetPartitionByName().wrap(() -> { + try (HiveMetastoreClient client = clientProvider.createMetastoreClient()) { + return client.get_partition_by_name(partitionName.getHiveTableName().getDatabaseName(), + partitionName.getHiveTableName().getTableName(), + partitionName.getPartitionName()); + } + })); + } + catch (NoSuchObjectException e) { + throw e; + } + catch (TException e) { + throw new PrestoException(HIVE_METASTORE_ERROR, e); + } + } + + private Map loadPartitionsByNames(Iterable partitionNames) + throws Exception + { + checkNotNull(partitionNames, "partitionNames is null"); + checkArgument(!Iterables.isEmpty(partitionNames), "partitionNames is empty"); + + HivePartitionName firstPartition = Iterables.get(partitionNames, 0); + + HiveTableName hiveTableName = firstPartition.getHiveTableName(); + final String databaseName = hiveTableName.getDatabaseName(); + final String tableName = hiveTableName.getTableName(); + + final List partitionsToFetch = new ArrayList<>(); + for (HivePartitionName partitionName : partitionNames) { + checkArgument(partitionName.getHiveTableName().equals(hiveTableName), "Expected table name %s but got %s", hiveTableName, partitionName.getHiveTableName()); + partitionsToFetch.add(partitionName.getPartitionName()); + } + + final List partitionColumnNames = ImmutableList.copyOf(Warehouse.makeSpecFromName(firstPartition.getPartitionName()).keySet()); + + try { + return retry() + .stopOn(NoSuchObjectException.class) + .stopOnIllegalExceptions() + .run("getPartitionsByNames", stats.getGetPartitionsByNames().wrap(() -> { + try (HiveMetastoreClient client = clientProvider.createMetastoreClient()) { + ImmutableMap.Builder partitions = ImmutableMap.builder(); + for (Partition partition : client.get_partitions_by_names(databaseName, tableName, partitionsToFetch)) { + String partitionId = FileUtils.makePartName(partitionColumnNames, partition.getValues(), null); + partitions.put(HivePartitionName.partition(databaseName, tableName, partitionId), partition); + } + return partitions.build(); + } + })); + } + catch (NoSuchObjectException e) { + throw e; + } + catch (TException e) { + throw new PrestoException(HIVE_METASTORE_ERROR, e); + } + } + + private static class HiveTableName + { + private final String databaseName; + private final String tableName; + + private HiveTableName(String databaseName, String tableName) + { + this.databaseName = databaseName; + this.tableName = tableName; + } + + public static HiveTableName table(String databaseName, String tableName) + { + return new HiveTableName(databaseName, tableName); + } + + public String getDatabaseName() + { + return databaseName; + } + + public String getTableName() + { + return tableName; + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("databaseName", databaseName) + .add("tableName", tableName) + .toString(); + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + HiveTableName other = (HiveTableName) o; + return Objects.equals(databaseName, other.databaseName) && + Objects.equals(tableName, other.tableName); + } + + @Override + public int hashCode() + { + return Objects.hash(databaseName, tableName); + } + } + + private static class HivePartitionName + { + private final HiveTableName hiveTableName; + private final String partitionName; + + private HivePartitionName(HiveTableName hiveTableName, String partitionName) + { + this.hiveTableName = hiveTableName; + this.partitionName = partitionName; + } + + public static HivePartitionName partition(String databaseName, String tableName, String partitionName) + { + return new HivePartitionName(HiveTableName.table(databaseName, tableName), partitionName); + } + + public HiveTableName getHiveTableName() + { + return hiveTableName; + } + + public String getPartitionName() + { + return partitionName; + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("hiveTableName", hiveTableName) + .add("partitionName", partitionName) + .toString(); + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + HivePartitionName other = (HivePartitionName) o; + return Objects.equals(hiveTableName, other.hiveTableName) && + Objects.equals(partitionName, other.partitionName); + } + + @Override + public int hashCode() + { + return Objects.hash(hiveTableName, partitionName); + } + } + + private static class PartitionFilter + { + private final HiveTableName hiveTableName; + private final List parts; + + private PartitionFilter(HiveTableName hiveTableName, List parts) + { + this.hiveTableName = hiveTableName; + this.parts = ImmutableList.copyOf(parts); + } + + public static PartitionFilter partitionFilter(String databaseName, String tableName, List parts) + { + return new PartitionFilter(HiveTableName.table(databaseName, tableName), parts); + } + + public HiveTableName getHiveTableName() + { + return hiveTableName; + } + + public List getParts() + { + return parts; + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("hiveTableName", hiveTableName) + .add("parts", parts) + .toString(); + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + PartitionFilter other = (PartitionFilter) o; + return Objects.equals(hiveTableName, other.hiveTableName) && + Objects.equals(parts, other.parts); + } + + @Override + public int hashCode() + { + return Objects.hash(hiveTableName, parts); + } + } + + @Override + public Partition createPartition(String dbName, String tableName, List values, List pCols, Table table, String location) + { + Partition tpart = new Partition(); + tpart.setTableName(tableName); + tpart.setDbName(dbName); + tpart.setValues(values); + StorageDescriptor sd = new StorageDescriptor(); + TMemoryBuffer buffer = new TMemoryBuffer(1024); + TBinaryProtocol prot = new TBinaryProtocol(buffer); + try { + table.getSd().write(prot); + sd.read(prot); + } + catch (TException e) { + throw new PrestoException(HIVE_METASTORE_ERROR, e); + } + tpart.setSd(sd); + tpart.getSd().setLocation(location); + return tpart; + } + + @Override + public int addPartitions(List partitions, String dbName, String tblName) + { + HiveMetastoreClient client = clientProvider.createMetastoreClient(); + int ret; + try { + ret = client.add_partitions(partitions); + } + catch (AlreadyExistsException | InvalidObjectException | MetaException e) { + throw Throwables.propagate(e); + } + catch (TException e) { + throw new PrestoException(HIVE_METASTORE_ERROR, e); + } + if (ret == partitions.size()) { + invalidatePartitionCaches(dbName, tblName); + } + return ret; + } + + private void invalidatePartitionCaches(String dbName, String tblName) + { + // invalidate related partitionNamesCache + HiveTableName key = HiveTableName.table(dbName, tblName); + this.partitionNamesCache.invalidate(key); + + // invalidate related partitionCache + for (HivePartitionName pnKey : partitionCache.asMap().keySet()) { + if (pnKey.getHiveTableName().equals(key)) { + partitionCache.invalidate(pnKey); + } + } + + // invalidate related partitionFileterCache + for (PartitionFilter pfKey : partitionFilterCache.asMap().keySet()) { + if (pfKey.getHiveTableName().equals(key)) { + partitionFilterCache.invalidate(pfKey); + } + } + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/metastore/CachingHiveMetastoreStats.java b/presto-hive/src/main/java/com/facebook/presto/hive/metastore/CachingHiveMetastoreStats.java new file mode 100644 index 00000000..70f88687 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/metastore/CachingHiveMetastoreStats.java @@ -0,0 +1,117 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive.metastore; + +import org.weakref.jmx.Managed; +import org.weakref.jmx.Nested; + +public class CachingHiveMetastoreStats +{ + private final HiveMetastoreApiStats getAllDatabases = new HiveMetastoreApiStats(); + private final HiveMetastoreApiStats getDatabase = new HiveMetastoreApiStats(); + private final HiveMetastoreApiStats getAllTables = new HiveMetastoreApiStats(); + private final HiveMetastoreApiStats getAllViews = new HiveMetastoreApiStats(); + private final HiveMetastoreApiStats getTable = new HiveMetastoreApiStats(); + private final HiveMetastoreApiStats getPartitionNames = new HiveMetastoreApiStats(); + private final HiveMetastoreApiStats getPartitionNamesPs = new HiveMetastoreApiStats(); + private final HiveMetastoreApiStats getPartitionByName = new HiveMetastoreApiStats(); + private final HiveMetastoreApiStats getPartitionsByNames = new HiveMetastoreApiStats(); + private final HiveMetastoreApiStats createTable = new HiveMetastoreApiStats(); + private final HiveMetastoreApiStats dropTable = new HiveMetastoreApiStats(); + private final HiveMetastoreApiStats renameTable = new HiveMetastoreApiStats(); + + @Managed + @Nested + public HiveMetastoreApiStats getGetAllDatabases() + { + return getAllDatabases; + } + + @Managed + @Nested + public HiveMetastoreApiStats getGetDatabase() + { + return getDatabase; + } + + @Managed + @Nested + public HiveMetastoreApiStats getGetAllTables() + { + return getAllTables; + } + + @Managed + @Nested + public HiveMetastoreApiStats getAllViews() + { + return getAllViews; + } + + @Managed + @Nested + public HiveMetastoreApiStats getGetTable() + { + return getTable; + } + + @Managed + @Nested + public HiveMetastoreApiStats getGetPartitionNames() + { + return getPartitionNames; + } + + @Managed + @Nested + public HiveMetastoreApiStats getGetPartitionNamesPs() + { + return getPartitionNamesPs; + } + + @Managed + @Nested + public HiveMetastoreApiStats getGetPartitionByName() + { + return getPartitionByName; + } + + @Managed + @Nested + public HiveMetastoreApiStats getGetPartitionsByNames() + { + return getPartitionsByNames; + } + + @Managed + @Nested + public HiveMetastoreApiStats getCreateTable() + { + return createTable; + } + + @Managed + @Nested + public HiveMetastoreApiStats getDropTable() + { + return dropTable; + } + + @Managed + @Nested + public HiveMetastoreApiStats getRenameTable() + { + return renameTable; + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/metastore/HiveMetastore.java b/presto-hive/src/main/java/com/facebook/presto/hive/metastore/HiveMetastore.java new file mode 100644 index 00000000..e2f4320a --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/metastore/HiveMetastore.java @@ -0,0 +1,62 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive.metastore; + +import org.apache.hadoop.hive.metastore.api.Database; +import org.apache.hadoop.hive.metastore.api.NoSuchObjectException; +import org.apache.hadoop.hive.metastore.api.Partition; +import org.apache.hadoop.hive.metastore.api.Table; +import org.weakref.jmx.Managed; + +import java.util.List; +import java.util.Map; + +public interface HiveMetastore +{ + void createTable(Table table); + + void dropTable(String databaseName, String tableName); + + void renameTable(String databaseName, String tableName, String newDatabaseName, String newTableName); + + @Managed + void flushCache(); + + List getAllDatabases(); + + List getAllTables(String databaseName) + throws NoSuchObjectException; + + List getAllViews(String databaseName) + throws NoSuchObjectException; + + Database getDatabase(String databaseName) + throws NoSuchObjectException; + + List getPartitionNames(String databaseName, String tableName) + throws NoSuchObjectException; + + List getPartitionNamesByParts(String databaseName, String tableName, List parts) + throws NoSuchObjectException; + + Map getPartitionsByNames(String databaseName, String tableName, List partitionNames) + throws NoSuchObjectException; + + Table getTable(String databaseName, String tableName) + throws NoSuchObjectException; + + Partition createPartition(String dbName, String tableName, List values, List pCols, Table table, String location); + + int addPartitions(List partitions, String dbName, String tblName); +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/metastore/HiveMetastoreApiStats.java b/presto-hive/src/main/java/com/facebook/presto/hive/metastore/HiveMetastoreApiStats.java new file mode 100644 index 00000000..6db7cdef --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/metastore/HiveMetastoreApiStats.java @@ -0,0 +1,94 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive.metastore; + +import io.airlift.stats.CounterStat; +import io.airlift.stats.TimeStat; +import org.apache.hadoop.hive.metastore.api.MetaException; +import org.apache.thrift.TException; +import org.weakref.jmx.Managed; +import org.weakref.jmx.Nested; + +import javax.annotation.concurrent.ThreadSafe; + +import java.util.concurrent.Callable; + +@ThreadSafe +public class HiveMetastoreApiStats +{ + private final TimeStat time = new TimeStat(); + private final CounterStat totalFailures = new CounterStat(); + private final CounterStat metastoreExceptions = new CounterStat(); + private final CounterStat thriftExceptions = new CounterStat(); + + public Callable wrap(final Callable callable) + { + return new Callable() + { + @Override + public V call() + throws Exception + { + try (TimeStat.BlockTimer timer = time.time()) { + return callable.call(); + } + catch (Exception e) { + if (e instanceof MetaException) { + metastoreExceptions.update(1); + // Need to throw here instead of falling through due to JDK-8059299 + totalFailures.update(1); + throw e; + } + else if (e instanceof TException) { + thriftExceptions.update(1); + // Need to throw here instead of falling through due to JDK-8059299 + totalFailures.update(1); + throw e; + } + totalFailures.update(1); + + throw e; + } + } + }; + } + + @Managed + @Nested + public TimeStat getTime() + { + return time; + } + + @Managed + @Nested + public CounterStat getTotalFailures() + { + return totalFailures; + } + + @Managed + @Nested + public CounterStat getThriftExceptions() + { + return thriftExceptions; + } + + @Managed + @Nested + public CounterStat getMetastoreExceptions() + { + return metastoreExceptions; + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/metastore/InMemoryHiveMetastore.java b/presto-hive/src/main/java/com/facebook/presto/hive/metastore/InMemoryHiveMetastore.java new file mode 100644 index 00000000..185598bb --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/metastore/InMemoryHiveMetastore.java @@ -0,0 +1,191 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive.metastore; + +import com.facebook.presto.hive.TableAlreadyExistsException; +import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.spi.TableNotFoundException; +import com.google.common.collect.ImmutableList; +import org.apache.hadoop.hive.metastore.TableType; +import org.apache.hadoop.hive.metastore.api.Database; +import org.apache.hadoop.hive.metastore.api.NoSuchObjectException; +import org.apache.hadoop.hive.metastore.api.Partition; +import org.apache.hadoop.hive.metastore.api.StorageDescriptor; +import org.apache.hadoop.hive.metastore.api.Table; + +import java.io.File; +import java.net.URI; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class InMemoryHiveMetastore + implements HiveMetastore +{ + private final ConcurrentHashMap databases = new ConcurrentHashMap<>(); + private final ConcurrentHashMap relations = new ConcurrentHashMap<>(); + private final ConcurrentHashMap views = new ConcurrentHashMap<>(); + + public void createDatabase(Database database) + { + checkNotNull(database, "database is null"); + + File file = new File(URI.create(database.getLocationUri())); + file.mkdirs(); + + if (databases.putIfAbsent(database.getName(), database) != null) { + throw new IllegalArgumentException("Database " + database.getName() + " already exists"); + } + } + + @Override + public List getAllDatabases() + { + return ImmutableList.copyOf(databases.keySet()); + } + + @Override + public void createTable(Table table) + { + SchemaTableName schemaTableName = new SchemaTableName(table.getDbName(), table.getTableName()); + Table tableCopy = table.deepCopy(); + if (tableCopy.getSd() == null) { + tableCopy.setSd(new StorageDescriptor()); + } + + if (relations.putIfAbsent(schemaTableName, tableCopy) != null) { + throw new TableAlreadyExistsException(schemaTableName); + } + + if (tableCopy.getTableType().equals(TableType.VIRTUAL_VIEW.name())) { + views.put(schemaTableName, tableCopy); + } + } + + @Override + public void dropTable(String databaseName, String tableName) + { + SchemaTableName schemaTableName = new SchemaTableName(databaseName, tableName); + if (relations.remove(schemaTableName) == null) { + throw new TableNotFoundException(schemaTableName); + } + views.remove(schemaTableName); + } + + @Override + public void renameTable(String databaseName, String tableName, String newDatabaseName, String newTableName) + { + // TODO: use locking to do this properly + SchemaTableName oldTable = new SchemaTableName(databaseName, tableName); + Table table = relations.get(oldTable); + if (table == null) { + throw new TableNotFoundException(oldTable); + } + + SchemaTableName newTable = new SchemaTableName(newDatabaseName, newTableName); + if (relations.putIfAbsent(newTable, table) != null) { + throw new TableAlreadyExistsException(newTable); + } + relations.remove(oldTable); + } + + @Override + public List getAllTables(String databaseName) + throws NoSuchObjectException + { + ImmutableList.Builder tables = ImmutableList.builder(); + for (SchemaTableName schemaTableName : this.relations.keySet()) { + if (schemaTableName.getSchemaName().equals(databaseName)) { + tables.add(schemaTableName.getTableName()); + } + } + return tables.build(); + } + + @Override + public List getAllViews(String databaseName) + throws NoSuchObjectException + { + ImmutableList.Builder tables = ImmutableList.builder(); + for (SchemaTableName schemaTableName : this.views.keySet()) { + if (schemaTableName.getSchemaName().equals(databaseName)) { + tables.add(schemaTableName.getTableName()); + } + } + return tables.build(); + } + + @Override + public Database getDatabase(String databaseName) + throws NoSuchObjectException + { + Database database = databases.get(databaseName); + if (database == null) { + throw new NoSuchObjectException(); + } + return database; + } + + @Override + public List getPartitionNames(String databaseName, String tableName) + throws NoSuchObjectException + { + throw new UnsupportedOperationException(); + } + + @Override + public List getPartitionNamesByParts(String databaseName, String tableName, List parts) + throws NoSuchObjectException + { + throw new UnsupportedOperationException(); + } + + @Override + public Map getPartitionsByNames(String databaseName, String tableName, List partitionNames) + throws NoSuchObjectException + { + throw new UnsupportedOperationException(); + } + + @Override + public Table getTable(String databaseName, String tableName) + throws NoSuchObjectException + { + SchemaTableName schemaTableName = new SchemaTableName(databaseName, tableName); + Table table = relations.get(schemaTableName); + if (table == null) { + throw new NoSuchObjectException(); + } + return table; + } + + @Override + public void flushCache() + { + } + + @Override + public Partition createPartition(String dbName, String tableName, List values, List pCols, Table table, String location) + { + throw new UnsupportedOperationException(); + } + + @Override + public int addPartitions(List partitions, String dbName, String tblName) + { + throw new UnsupportedOperationException(); + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/orc/DwrfHiveRecordCursor.java b/presto-hive/src/main/java/com/facebook/presto/hive/orc/DwrfHiveRecordCursor.java new file mode 100644 index 00000000..21047f97 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/orc/DwrfHiveRecordCursor.java @@ -0,0 +1,532 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive.orc; + +import com.facebook.hive.orc.RecordReader; +import com.facebook.hive.orc.lazy.OrcLazyObject; +import com.facebook.hive.orc.lazy.OrcLazyRow; +import com.facebook.presto.hive.HiveColumnHandle; +import com.facebook.presto.hive.HivePartitionKey; +import com.facebook.presto.hive.HiveRecordCursor; +import com.facebook.presto.hive.HiveType; +import com.facebook.presto.hive.HiveUtil; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.google.common.base.Throwables; +import io.airlift.slice.Slice; +import io.airlift.slice.Slices; +import org.apache.hadoop.hive.serde2.io.ByteWritable; +import org.apache.hadoop.hive.serde2.io.DoubleWritable; +import org.apache.hadoop.hive.serde2.io.ShortWritable; +import org.apache.hadoop.hive.serde2.io.TimestampWritable; +import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.StructObjectInspector; +import org.apache.hadoop.io.BooleanWritable; +import org.apache.hadoop.io.BytesWritable; +import org.apache.hadoop.io.FloatWritable; +import org.apache.hadoop.io.IntWritable; +import org.apache.hadoop.io.LongWritable; +import org.apache.hadoop.io.Text; +import org.apache.hadoop.io.Writable; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import static com.facebook.presto.hive.HiveErrorCode.HIVE_BAD_DATA; +import static com.facebook.presto.hive.HiveErrorCode.HIVE_CURSOR_ERROR; +import static com.facebook.presto.hive.HiveType.HIVE_BINARY; +import static com.facebook.presto.hive.HiveType.HIVE_BYTE; +import static com.facebook.presto.hive.HiveType.HIVE_DOUBLE; +import static com.facebook.presto.hive.HiveType.HIVE_FLOAT; +import static com.facebook.presto.hive.HiveType.HIVE_INT; +import static com.facebook.presto.hive.HiveType.HIVE_LONG; +import static com.facebook.presto.hive.HiveType.HIVE_SHORT; +import static com.facebook.presto.hive.HiveType.HIVE_STRING; +import static com.facebook.presto.hive.HiveType.HIVE_TIMESTAMP; +import static com.facebook.presto.hive.HiveUtil.bigintPartitionKey; +import static com.facebook.presto.hive.HiveUtil.booleanPartitionKey; +import static com.facebook.presto.hive.HiveUtil.datePartitionKey; +import static com.facebook.presto.hive.HiveUtil.doublePartitionKey; +import static com.facebook.presto.hive.HiveUtil.getTableObjectInspector; +import static com.facebook.presto.hive.HiveUtil.isStructuralType; +import static com.facebook.presto.hive.HiveUtil.timestampPartitionKey; +import static com.facebook.presto.hive.util.SerDeUtils.getBlockSlice; +import static com.facebook.presto.hive.util.Types.checkType; +import static com.facebook.presto.spi.StandardErrorCode.NOT_SUPPORTED; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.BooleanType.BOOLEAN; +import static com.facebook.presto.spi.type.DateType.DATE; +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; +import static com.facebook.presto.spi.type.TimestampType.TIMESTAMP; +import static com.facebook.presto.spi.type.VarbinaryType.VARBINARY; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.Maps.uniqueIndex; +import static java.lang.Math.max; +import static java.lang.Math.min; +import static java.lang.String.format; +import static java.nio.charset.StandardCharsets.UTF_8; + +public class DwrfHiveRecordCursor + extends HiveRecordCursor +{ + private final RecordReader recordReader; + + @SuppressWarnings("FieldCanBeLocal") // include names for debugging + private final String[] names; + private final Type[] types; + private final HiveType[] hiveTypes; + + private final ObjectInspector[] fieldInspectors; // only used for structured types + + private final int[] hiveColumnIndexes; + + private final boolean[] isPartitionColumn; + + private OrcLazyRow value; + + private final boolean[] loaded; + private final boolean[] booleans; + private final long[] longs; + private final double[] doubles; + private final Slice[] slices; + private final boolean[] nulls; + + private final long totalBytes; + private long completedBytes; + private boolean closed; + private final long timeZoneCorrection; + + public DwrfHiveRecordCursor(RecordReader recordReader, + long totalBytes, + Properties splitSchema, + List partitionKeys, + List columns, + DateTimeZone hiveStorageTimeZone, + TypeManager typeManager) + { + checkNotNull(recordReader, "recordReader is null"); + checkArgument(totalBytes >= 0, "totalBytes is negative"); + checkNotNull(splitSchema, "splitSchema is null"); + checkNotNull(partitionKeys, "partitionKeys is null"); + checkNotNull(columns, "columns is null"); + checkNotNull(hiveStorageTimeZone, "hiveStorageTimeZone is null"); + + this.recordReader = recordReader; + this.totalBytes = totalBytes; + + int size = columns.size(); + + this.names = new String[size]; + this.types = new Type[size]; + this.hiveTypes = new HiveType[size]; + + this.fieldInspectors = new ObjectInspector[size]; + + this.hiveColumnIndexes = new int[size]; + + this.isPartitionColumn = new boolean[size]; + + this.loaded = new boolean[size]; + this.booleans = new boolean[size]; + this.longs = new long[size]; + this.doubles = new double[size]; + this.slices = new Slice[size]; + this.nulls = new boolean[size]; + + // DWRF uses an epoch sensitive to the JVM default timezone, so we need to correct for this + long hiveStorageCorrection = new DateTime(2015, 1, 1, 0, 0, hiveStorageTimeZone).getMillis() - new DateTime(2015, 1, 1, 0, 0, DateTimeZone.UTC).getMillis(); + long jvmCorrection = new DateTime(2015, 1, 1, 0, 0).getMillis() - new DateTime(2015, 1, 1, 0, 0, DateTimeZone.UTC).getMillis(); + timeZoneCorrection = hiveStorageCorrection - jvmCorrection; + + // initialize data columns + StructObjectInspector rowInspector = getTableObjectInspector(splitSchema); + + for (int i = 0; i < columns.size(); i++) { + HiveColumnHandle column = columns.get(i); + + names[i] = column.getName(); + types[i] = typeManager.getType(column.getTypeSignature()); + hiveTypes[i] = column.getHiveType(); + + if (!column.isPartitionKey()) { + fieldInspectors[i] = rowInspector.getStructFieldRef(column.getName()).getFieldObjectInspector(); + } + + hiveColumnIndexes[i] = column.getHiveColumnIndex(); + isPartitionColumn[i] = column.isPartitionKey(); + } + + // parse requested partition columns + Map partitionKeysByName = uniqueIndex(partitionKeys, HivePartitionKey::getName); + for (int columnIndex = 0; columnIndex < columns.size(); columnIndex++) { + HiveColumnHandle column = columns.get(columnIndex); + if (column.isPartitionKey()) { + HivePartitionKey partitionKey = partitionKeysByName.get(column.getName()); + checkArgument(partitionKey != null, "Unknown partition key %s", column.getName()); + + byte[] bytes = partitionKey.getValue().getBytes(UTF_8); + + String name = names[columnIndex]; + Type type = types[columnIndex]; + if (HiveUtil.isHiveNull(bytes)) { + nulls[columnIndex] = true; + } + else if (BOOLEAN.equals(type)) { + booleans[columnIndex] = booleanPartitionKey(partitionKey.getValue(), name); + } + else if (BIGINT.equals(type)) { + longs[columnIndex] = bigintPartitionKey(partitionKey.getValue(), name); + } + else if (DOUBLE.equals(type)) { + doubles[columnIndex] = doublePartitionKey(partitionKey.getValue(), name); + } + else if (VARCHAR.equals(type)) { + slices[columnIndex] = Slices.wrappedBuffer(Arrays.copyOf(bytes, bytes.length)); + } + else if (DATE.equals(type)) { + longs[columnIndex] = datePartitionKey(partitionKey.getValue(), name); + } + else if (TIMESTAMP.equals(type)) { + longs[columnIndex] = timestampPartitionKey(partitionKey.getValue(), hiveStorageTimeZone, name); + } + else { + throw new PrestoException(NOT_SUPPORTED, format("Unsupported column type %s for partition key: %s", type.getDisplayName(), name)); + } + } + } + } + + @Override + public long getTotalBytes() + { + return totalBytes; + } + + @Override + public long getCompletedBytes() + { + if (!closed) { + updateCompletedBytes(); + } + return completedBytes; + } + + private void updateCompletedBytes() + { + try { + long newCompletedBytes = (long) (totalBytes * recordReader.getProgress()); + completedBytes = min(totalBytes, max(completedBytes, newCompletedBytes)); + } + catch (IOException ignored) { + } + } + + @Override + public Type getType(int field) + { + return types[field]; + } + + @Override + public boolean advanceNextPosition() + { + try { + if (closed || !recordReader.hasNext()) { + close(); + return false; + } + + value = (OrcLazyRow) recordReader.next(value); + + // reset loaded flags + // partition keys are already loaded, but everything else is not + System.arraycopy(isPartitionColumn, 0, loaded, 0, isPartitionColumn.length); + + return true; + } + catch (IOException | RuntimeException e) { + closeWithSuppression(e); + throw new PrestoException(HIVE_CURSOR_ERROR, e); + } + } + + @Override + public boolean getBoolean(int fieldId) + { + checkState(!closed, "Cursor is closed"); + + validateType(fieldId, boolean.class); + if (!loaded[fieldId]) { + parseBooleanColumn(fieldId); + } + return booleans[fieldId]; + } + + private void parseBooleanColumn(int column) + { + // don't include column number in message because it causes boxing which is expensive here + checkArgument(!isPartitionColumn[column], "Column is a partition key"); + + loaded[column] = true; + + Object object = getMaterializedValue(column); + + if (object == null) { + nulls[column] = true; + } + else { + nulls[column] = false; + BooleanWritable booleanWritable = checkWritable(object, BooleanWritable.class); + booleans[column] = booleanWritable.get(); + } + } + + @Override + public long getLong(int fieldId) + { + checkState(!closed, "Cursor is closed"); + + validateType(fieldId, long.class); + if (!loaded[fieldId]) { + parseLongColumn(fieldId); + } + return longs[fieldId]; + } + + private void parseLongColumn(int column) + { + // don't include column number in message because it causes boxing which is expensive here + checkArgument(!isPartitionColumn[column], "Column is a partition key"); + + loaded[column] = true; + Object object = getMaterializedValue(column); + if (object == null) { + nulls[column] = true; + } + else { + nulls[column] = false; + + HiveType type = hiveTypes[column]; + if (hiveTypes[column].equals(HIVE_SHORT)) { + ShortWritable shortWritable = checkWritable(object, ShortWritable.class); + longs[column] = shortWritable.get(); + } + else if (hiveTypes[column].equals(HIVE_TIMESTAMP)) { + TimestampWritable timestampWritable = (TimestampWritable) object; + long seconds = timestampWritable.getSeconds(); + int nanos = timestampWritable.getNanos(); + longs[column] = (seconds * 1000) + (nanos / 1_000_000) + timeZoneCorrection; + } + else if (hiveTypes[column].equals(HIVE_BYTE)) { + ByteWritable byteWritable = checkWritable(object, ByteWritable.class); + longs[column] = byteWritable.get(); + } + else if (hiveTypes[column].equals(HIVE_INT)) { + IntWritable intWritable = checkWritable(object, IntWritable.class); + longs[column] = intWritable.get(); + } + else if (hiveTypes[column].equals(HIVE_LONG)) { + LongWritable longWritable = checkWritable(object, LongWritable.class); + longs[column] = longWritable.get(); + } + else { + throw new RuntimeException(String.format("%s is not a valid LONG type", type)); + } + } + } + + @Override + public double getDouble(int fieldId) + { + checkState(!closed, "Cursor is closed"); + + validateType(fieldId, double.class); + if (!loaded[fieldId]) { + parseDoubleColumn(fieldId); + } + return doubles[fieldId]; + } + + private void parseDoubleColumn(int column) + { + // don't include column number in message because it causes boxing which is expensive here + checkArgument(!isPartitionColumn[column], "Column is a partition key"); + + loaded[column] = true; + Object object = getMaterializedValue(column); + if (object == null) { + nulls[column] = true; + } + else { + nulls[column] = false; + + HiveType type = hiveTypes[column]; + if (hiveTypes[column].equals(HIVE_FLOAT)) { + FloatWritable floatWritable = checkWritable(object, FloatWritable.class); + doubles[column] = floatWritable.get(); + } + else if (hiveTypes[column].equals(HIVE_DOUBLE)) { + DoubleWritable doubleWritable = checkWritable(object, DoubleWritable.class); + doubles[column] = doubleWritable.get(); + } + else { + throw new RuntimeException(String.format("%s is not a valid DOUBLE type", type)); + } + } + } + + @Override + public Slice getSlice(int fieldId) + { + checkState(!closed, "Cursor is closed"); + + validateType(fieldId, Slice.class); + if (!loaded[fieldId]) { + parseStringColumn(fieldId); + } + return slices[fieldId]; + } + + private void parseStringColumn(int column) + { + // don't include column number in message because it causes boxing which is expensive here + checkArgument(!isPartitionColumn[column], "Column is a partition key"); + + loaded[column] = true; + nulls[column] = false; + + OrcLazyObject lazyObject = getRawValue(column); + if (lazyObject == null) { + nulls[column] = true; + return; + } + + Object value = materializeValue(lazyObject); + if (value == null) { + nulls[column] = true; + return; + } + + HiveType type = hiveTypes[column]; + if (isStructuralType(type)) { + slices[column] = getBlockSlice(lazyObject, fieldInspectors[column]); + } + else if (type.equals(HIVE_STRING)) { + Text text = checkWritable(value, Text.class); + slices[column] = Slices.copyOf(Slices.wrappedBuffer(text.getBytes()), 0, text.getLength()); + } + else if (type.equals(HIVE_BINARY)) { + BytesWritable bytesWritable = checkWritable(value, BytesWritable.class); + slices[column] = Slices.copyOf(Slices.wrappedBuffer(bytesWritable.getBytes()), 0, bytesWritable.getLength()); + } + else { + throw new RuntimeException(String.format("%s is not a valid STRING type", type)); + } + } + + @Override + public boolean isNull(int fieldId) + { + checkState(!closed, "Cursor is closed"); + + if (!loaded[fieldId]) { + parseColumn(fieldId); + } + return nulls[fieldId]; + } + + private void parseColumn(int column) + { + if (types[column].equals(BOOLEAN)) { + parseBooleanColumn(column); + } + else if (types[column].equals(BIGINT)) { + parseLongColumn(column); + } + else if (types[column].equals(DOUBLE)) { + parseDoubleColumn(column); + } + else if (types[column].equals(VARCHAR) || types[column].equals(VARBINARY) || isStructuralType(hiveTypes[column])) { + parseStringColumn(column); + } + else if (types[column].equals(TIMESTAMP)) { + parseLongColumn(column); + } + else { + throw new UnsupportedOperationException("Unsupported column type: " + types[column]); + } + } + + private void validateType(int fieldId, Class javaType) + { + if (types[fieldId].getJavaType() != javaType) { + // we don't use Preconditions.checkArgument because it requires boxing fieldId, which affects inner loop performance + throw new IllegalArgumentException(String.format("Expected field to be %s, actual %s (field %s)", javaType.getName(), types[fieldId].getJavaType().getName(), fieldId)); + } + } + + private static Object materializeValue(OrcLazyObject object) + { + try { + return object.materialize(); + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } + + private OrcLazyObject getRawValue(int column) + { + return this.value.getFieldValue(hiveColumnIndexes[column]); + } + + private Object getMaterializedValue(int column) + { + OrcLazyObject value = getRawValue(column); + return (value == null) ? null : materializeValue(value); + } + + @Override + public void close() + { + // some hive input formats are broken and bad things can happen if you close them multiple times + if (closed) { + return; + } + closed = true; + + updateCompletedBytes(); + + try { + recordReader.close(); + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } + + private static T checkWritable(Object object, Class clazz) + { + return checkType(object, clazz, HIVE_BAD_DATA, "materialized object"); + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/orc/DwrfPageSourceFactory.java b/presto-hive/src/main/java/com/facebook/presto/hive/orc/DwrfPageSourceFactory.java new file mode 100644 index 00000000..f0b2b47a --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/orc/DwrfPageSourceFactory.java @@ -0,0 +1,111 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive.orc; + +import com.facebook.hive.orc.OrcSerde; +import com.facebook.presto.hive.HiveClientConfig; +import com.facebook.presto.hive.HiveColumnHandle; +import com.facebook.presto.hive.HivePageSourceFactory; +import com.facebook.presto.hive.HivePartitionKey; +import com.facebook.presto.orc.metadata.DwrfMetadataReader; +import com.facebook.presto.spi.ConnectorPageSource; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.TupleDomain; +import com.facebook.presto.spi.type.TypeManager; +import io.airlift.units.DataSize; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.Path; +import org.joda.time.DateTimeZone; + +import javax.inject.Inject; + +import java.util.List; +import java.util.Optional; +import java.util.Properties; + +import static com.facebook.presto.hive.HiveSessionProperties.getOrcMaxBufferSize; +import static com.facebook.presto.hive.HiveSessionProperties.getOrcMaxMergeDistance; +import static com.facebook.presto.hive.HiveSessionProperties.getOrcStreamBufferSize; +import static com.facebook.presto.hive.HiveSessionProperties.isOptimizedReaderEnabled; +import static com.facebook.presto.hive.HiveUtil.isDeserializerClass; +import static com.facebook.presto.hive.orc.OrcPageSourceFactory.createOrcPageSource; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.airlift.units.DataSize.Unit.MEGABYTE; + +public class DwrfPageSourceFactory + implements HivePageSourceFactory +{ + private final TypeManager typeManager; + private final boolean enabled; + private final DataSize orcMaxMergeDistance; + private final DataSize orcMaxBufferSize; + private final DataSize orcStreamBufferSize; + + @Inject + public DwrfPageSourceFactory(TypeManager typeManager, HiveClientConfig config) + { + //noinspection deprecation + this(typeManager, config.isOptimizedReaderEnabled(), config.getOrcMaxMergeDistance(), config.getOrcMaxBufferSize(), config.getOrcStreamBufferSize()); + } + + public DwrfPageSourceFactory(TypeManager typeManager) + { + this(typeManager, true, new DataSize(1, MEGABYTE), new DataSize(8, MEGABYTE), new DataSize(8, MEGABYTE)); + } + + public DwrfPageSourceFactory(TypeManager typeManager, boolean enabled, DataSize orcMaxMergeDistance, DataSize orcMaxBufferSize, DataSize orcStreamBufferSize) + { + this.typeManager = checkNotNull(typeManager, "typeManager is null"); + this.enabled = enabled; + this.orcMaxMergeDistance = checkNotNull(orcMaxMergeDistance, "orcMaxMergeDistance is null"); + this.orcMaxBufferSize = checkNotNull(orcMaxBufferSize, "orcMaxBufferSize is null"); + this.orcStreamBufferSize = checkNotNull(orcStreamBufferSize, "orcStreamBufferSize is null"); + } + + @Override + public Optional createPageSource(Configuration configuration, + ConnectorSession session, + Path path, + long start, + long length, + Properties schema, + List columns, + List partitionKeys, + TupleDomain effectivePredicate, + DateTimeZone hiveStorageTimeZone) + { + if (!isOptimizedReaderEnabled(session, enabled)) { + return Optional.empty(); + } + + if (!isDeserializerClass(schema, OrcSerde.class)) { + return Optional.empty(); + } + + return Optional.of(createOrcPageSource( + new DwrfMetadataReader(), + configuration, + path, + start, + length, + columns, + partitionKeys, + effectivePredicate, + hiveStorageTimeZone, + typeManager, + getOrcMaxMergeDistance(session, orcMaxMergeDistance), + getOrcMaxBufferSize(session, orcMaxBufferSize), + getOrcStreamBufferSize(session, orcStreamBufferSize))); + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/orc/DwrfRecordCursorProvider.java b/presto-hive/src/main/java/com/facebook/presto/hive/orc/DwrfRecordCursorProvider.java new file mode 100644 index 00000000..543fa7b1 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/orc/DwrfRecordCursorProvider.java @@ -0,0 +1,173 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive.orc; + +import com.facebook.hive.orc.OrcFile; +import com.facebook.hive.orc.OrcProto; +import com.facebook.hive.orc.OrcProto.Type; +import com.facebook.hive.orc.OrcSerde; +import com.facebook.hive.orc.Reader; +import com.facebook.hive.orc.RecordReader; +import com.facebook.presto.hive.HiveColumnHandle; +import com.facebook.presto.hive.HivePartitionKey; +import com.facebook.presto.hive.HiveRecordCursor; +import com.facebook.presto.hive.HiveRecordCursorProvider; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.TupleDomain; +import com.facebook.presto.spi.type.TypeManager; +import com.google.common.base.Predicate; +import com.google.common.base.Throwables; +import com.google.common.collect.Lists; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.hive.serde2.ReaderWriterProfiler; +import org.apache.hadoop.hive.serde2.objectinspector.ListObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.MapObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.PrimitiveObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.PrimitiveObjectInspector.PrimitiveCategory; +import org.apache.hadoop.hive.serde2.objectinspector.StructField; +import org.apache.hadoop.hive.serde2.objectinspector.StructObjectInspector; +import org.apache.hadoop.mapred.JobConf; +import org.joda.time.DateTimeZone; + +import java.util.List; +import java.util.Optional; +import java.util.Properties; + +import static com.facebook.presto.hive.HiveUtil.getTableObjectInspector; +import static com.facebook.presto.hive.HiveUtil.isDeserializerClass; +import static com.google.common.collect.Iterables.all; + +public class DwrfRecordCursorProvider + implements HiveRecordCursorProvider +{ + @Override + public Optional createHiveRecordCursor( + String clientId, + Configuration configuration, + ConnectorSession session, + Path path, + long start, + long length, + Properties schema, + List columns, + List partitionKeys, + TupleDomain effectivePredicate, + DateTimeZone hiveStorageTimeZone, + TypeManager typeManager) + { + if (!isDeserializerClass(schema, OrcSerde.class)) { + return Optional.empty(); + } + + StructObjectInspector rowInspector = getTableObjectInspector(schema); + if (!all(rowInspector.getAllStructFieldRefs(), isSupportedDwrfType())) { + throw new IllegalArgumentException("DWRF does not support DATE type"); + } + + ReaderWriterProfiler.setProfilerOptions(configuration); + + RecordReader recordReader; + try { + FileSystem fileSystem = path.getFileSystem(configuration); + Reader reader = OrcFile.createReader(fileSystem, path, new JobConf(configuration)); + boolean[] include = findIncludedColumns(reader.getTypes(), columns); + recordReader = reader.rows(start, length, include); + } + catch (Exception e) { + throw Throwables.propagate(e); + } + + return Optional.of(new DwrfHiveRecordCursor( + recordReader, + length, + schema, + partitionKeys, + columns, + hiveStorageTimeZone, + typeManager)); + } + + private static Predicate isSupportedDwrfType() + { + return new Predicate() + { + @Override + public boolean apply(StructField hiveColumnHandle) + { + return !hasDateType(hiveColumnHandle.getFieldObjectInspector()); + } + }; + } + + private static boolean[] findIncludedColumns(List types, List columns) + { + boolean[] includes = new boolean[types.size()]; + includes[0] = true; + + OrcProto.Type root = types.get(0); + List included = Lists.transform(columns, HiveColumnHandle::getHiveColumnIndex); + for (int i = 0; i < root.getSubtypesCount(); ++i) { + if (included.contains(i)) { + includeColumnRecursive(types, includes, root.getSubtypes(i)); + } + } + + // if we are filtering at least one column, return the boolean array + for (boolean include : includes) { + if (!include) { + return includes; + } + } + return null; + } + + private static void includeColumnRecursive(List types, boolean[] result, int typeId) + { + result[typeId] = true; + OrcProto.Type type = types.get(typeId); + int children = type.getSubtypesCount(); + for (int i = 0; i < children; ++i) { + includeColumnRecursive(types, result, type.getSubtypes(i)); + } + } + + static boolean hasDateType(ObjectInspector objectInspector) + { + if (objectInspector instanceof PrimitiveObjectInspector) { + PrimitiveObjectInspector primitiveInspector = (PrimitiveObjectInspector) objectInspector; + return primitiveInspector.getPrimitiveCategory() == PrimitiveCategory.DATE; + } + if (objectInspector instanceof ListObjectInspector) { + ListObjectInspector listInspector = (ListObjectInspector) objectInspector; + return hasDateType(listInspector.getListElementObjectInspector()); + } + if (objectInspector instanceof MapObjectInspector) { + MapObjectInspector mapInspector = (MapObjectInspector) objectInspector; + return hasDateType(mapInspector.getMapKeyObjectInspector()) || + hasDateType(mapInspector.getMapValueObjectInspector()); + } + if (objectInspector instanceof StructObjectInspector) { + for (StructField field : ((StructObjectInspector) objectInspector).getAllStructFieldRefs()) { + if (hasDateType(field.getFieldObjectInspector())) { + return true; + } + } + return false; + } + throw new IllegalArgumentException("Unknown object inspector type " + objectInspector); + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/orc/HdfsOrcDataSource.java b/presto-hive/src/main/java/com/facebook/presto/hive/orc/HdfsOrcDataSource.java new file mode 100644 index 00000000..5e411a1d --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/orc/HdfsOrcDataSource.java @@ -0,0 +1,46 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive.orc; + +import com.facebook.presto.orc.AbstractOrcDataSource; +import io.airlift.units.DataSize; +import org.apache.hadoop.fs.FSDataInputStream; + +import java.io.IOException; + +public class HdfsOrcDataSource + extends AbstractOrcDataSource +{ + private final FSDataInputStream inputStream; + + public HdfsOrcDataSource(String name, long size, DataSize maxMergeDistance, DataSize maxReadSize, DataSize streamBufferSize, FSDataInputStream inputStream) + { + super(name, size, maxMergeDistance, maxReadSize, streamBufferSize); + this.inputStream = inputStream; + } + + @Override + public void close() + throws IOException + { + inputStream.close(); + } + + @Override + protected void readInternal(long position, byte[] buffer, int bufferOffset, int bufferLength) + throws IOException + { + inputStream.readFully(position, buffer, bufferOffset, bufferLength); + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/orc/OrcHiveRecordCursor.java b/presto-hive/src/main/java/com/facebook/presto/hive/orc/OrcHiveRecordCursor.java new file mode 100644 index 00000000..913298e8 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/orc/OrcHiveRecordCursor.java @@ -0,0 +1,507 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive.orc; + +import com.facebook.presto.hive.HiveColumnHandle; +import com.facebook.presto.hive.HivePartitionKey; +import com.facebook.presto.hive.HiveRecordCursor; +import com.facebook.presto.hive.HiveType; +import com.facebook.presto.hive.HiveUtil; +import com.facebook.presto.hive.util.Types; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.google.common.base.Throwables; +import io.airlift.slice.Slice; +import io.airlift.slice.Slices; +import org.apache.hadoop.hive.ql.io.orc.OrcStruct; +import org.apache.hadoop.hive.ql.io.orc.RecordReader; +import org.apache.hadoop.hive.serde2.io.ByteWritable; +import org.apache.hadoop.hive.serde2.io.DateWritable; +import org.apache.hadoop.hive.serde2.io.DoubleWritable; +import org.apache.hadoop.hive.serde2.io.ShortWritable; +import org.apache.hadoop.hive.serde2.io.TimestampWritable; +import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.StructObjectInspector; +import org.apache.hadoop.io.BooleanWritable; +import org.apache.hadoop.io.BytesWritable; +import org.apache.hadoop.io.FloatWritable; +import org.apache.hadoop.io.IntWritable; +import org.apache.hadoop.io.LongWritable; +import org.apache.hadoop.io.Text; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import static com.facebook.presto.hive.HiveErrorCode.HIVE_CURSOR_ERROR; +import static com.facebook.presto.hive.HiveType.HIVE_BINARY; +import static com.facebook.presto.hive.HiveType.HIVE_BYTE; +import static com.facebook.presto.hive.HiveType.HIVE_DATE; +import static com.facebook.presto.hive.HiveType.HIVE_DOUBLE; +import static com.facebook.presto.hive.HiveType.HIVE_FLOAT; +import static com.facebook.presto.hive.HiveType.HIVE_INT; +import static com.facebook.presto.hive.HiveType.HIVE_LONG; +import static com.facebook.presto.hive.HiveType.HIVE_SHORT; +import static com.facebook.presto.hive.HiveType.HIVE_STRING; +import static com.facebook.presto.hive.HiveType.HIVE_TIMESTAMP; +import static com.facebook.presto.hive.HiveUtil.bigintPartitionKey; +import static com.facebook.presto.hive.HiveUtil.booleanPartitionKey; +import static com.facebook.presto.hive.HiveUtil.datePartitionKey; +import static com.facebook.presto.hive.HiveUtil.doublePartitionKey; +import static com.facebook.presto.hive.HiveUtil.getTableObjectInspector; +import static com.facebook.presto.hive.HiveUtil.isStructuralType; +import static com.facebook.presto.hive.HiveUtil.timestampPartitionKey; +import static com.facebook.presto.spi.StandardErrorCode.NOT_SUPPORTED; +import static com.facebook.presto.hive.util.SerDeUtils.getBlockSlice; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.BooleanType.BOOLEAN; +import static com.facebook.presto.spi.type.DateType.DATE; +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; +import static com.facebook.presto.spi.type.TimestampType.TIMESTAMP; +import static com.facebook.presto.spi.type.VarbinaryType.VARBINARY; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.Maps.uniqueIndex; +import static java.lang.Math.max; +import static java.lang.Math.min; +import static java.lang.String.format; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.apache.hadoop.hive.ql.io.orc.OrcUtil.getFieldValue; + +public class OrcHiveRecordCursor + extends HiveRecordCursor +{ + private final RecordReader recordReader; + + @SuppressWarnings("FieldCanBeLocal") // include names for debugging + private final String[] names; + private final Type[] types; + private final HiveType[] hiveTypes; + + private final ObjectInspector[] fieldInspectors; // only used for structured types + + private final int[] hiveColumnIndexes; + + private final boolean[] isPartitionColumn; + + private OrcStruct row; + + private final boolean[] loaded; + private final boolean[] booleans; + private final long[] longs; + private final double[] doubles; + private final Slice[] slices; + private final boolean[] nulls; + + private final long totalBytes; + private long completedBytes; + private boolean closed; + private final long timeZoneCorrection; + + public OrcHiveRecordCursor(RecordReader recordReader, + long totalBytes, + Properties splitSchema, + List partitionKeys, + List columns, + DateTimeZone hiveStorageTimeZone, + TypeManager typeManager) + { + checkNotNull(recordReader, "recordReader is null"); + checkArgument(totalBytes >= 0, "totalBytes is negative"); + checkNotNull(splitSchema, "splitSchema is null"); + checkNotNull(partitionKeys, "partitionKeys is null"); + checkNotNull(columns, "columns is null"); + checkNotNull(hiveStorageTimeZone, "hiveStorageTimeZone is null"); + + this.recordReader = recordReader; + this.totalBytes = totalBytes; + + int size = columns.size(); + + this.names = new String[size]; + this.types = new Type[size]; + this.hiveTypes = new HiveType[size]; + + this.fieldInspectors = new ObjectInspector[size]; + + this.hiveColumnIndexes = new int[size]; + + this.isPartitionColumn = new boolean[size]; + + this.loaded = new boolean[size]; + this.booleans = new boolean[size]; + this.longs = new long[size]; + this.doubles = new double[size]; + this.slices = new Slice[size]; + this.nulls = new boolean[size]; + + // ORC stores timestamps relative to 2015-01-01 00:00:00 but in the timezone of the writer + // When reading back a timestamp the Hive ORC reader will an epoch in this machine's timezone + // We must correct for the difference between the writer's timezone and this machine's + // timezone (on 2015-01-01) + long hiveStorageCorrection = new DateTime(2015, 1, 1, 0, 0, hiveStorageTimeZone).getMillis() - new DateTime(2015, 1, 1, 0, 0, DateTimeZone.UTC).getMillis(); + long jvmCorrection = new DateTime(2015, 1, 1, 0, 0).getMillis() - new DateTime(2015, 1, 1, 0, 0, DateTimeZone.UTC).getMillis(); + timeZoneCorrection = hiveStorageCorrection - jvmCorrection; + + // initialize data columns + StructObjectInspector rowInspector = getTableObjectInspector(splitSchema); + + for (int i = 0; i < columns.size(); i++) { + HiveColumnHandle column = columns.get(i); + + names[i] = column.getName(); + types[i] = typeManager.getType(column.getTypeSignature()); + hiveTypes[i] = column.getHiveType(); + + if (!column.isPartitionKey()) { + fieldInspectors[i] = rowInspector.getStructFieldRef(column.getName()).getFieldObjectInspector(); + } + + hiveColumnIndexes[i] = column.getHiveColumnIndex(); + isPartitionColumn[i] = column.isPartitionKey(); + } + + // parse requested partition columns + Map partitionKeysByName = uniqueIndex(partitionKeys, HivePartitionKey::getName); + for (int columnIndex = 0; columnIndex < columns.size(); columnIndex++) { + HiveColumnHandle column = columns.get(columnIndex); + if (column.isPartitionKey()) { + HivePartitionKey partitionKey = partitionKeysByName.get(column.getName()); + checkArgument(partitionKey != null, "Unknown partition key %s", column.getName()); + + byte[] bytes = partitionKey.getValue().getBytes(UTF_8); + + String name = names[columnIndex]; + Type type = types[columnIndex]; + if (HiveUtil.isHiveNull(bytes)) { + nulls[columnIndex] = true; + } + else if (type.equals(BOOLEAN)) { + booleans[columnIndex] = booleanPartitionKey(partitionKey.getValue(), name); + } + else if (type.equals(BIGINT)) { + longs[columnIndex] = bigintPartitionKey(partitionKey.getValue(), name); + } + else if (type.equals(DOUBLE)) { + doubles[columnIndex] = doublePartitionKey(partitionKey.getValue(), name); + } + else if (type.equals(VARCHAR)) { + slices[columnIndex] = Slices.wrappedBuffer(bytes); + } + else if (type.equals(DATE)) { + longs[columnIndex] = datePartitionKey(partitionKey.getValue(), name); + } + else if (type.equals(TIMESTAMP)) { + longs[columnIndex] = timestampPartitionKey(partitionKey.getValue(), hiveStorageTimeZone, name); + } + else { + throw new PrestoException(NOT_SUPPORTED, format("Unsupported column type %s for partition key: %s", type.getDisplayName(), name)); + } + } + } + } + + @Override + public long getTotalBytes() + { + return totalBytes; + } + + @Override + public long getCompletedBytes() + { + if (!closed) { + updateCompletedBytes(); + } + return completedBytes; + } + + private void updateCompletedBytes() + { + try { + long newCompletedBytes = (long) (totalBytes * recordReader.getProgress()); + completedBytes = min(totalBytes, max(completedBytes, newCompletedBytes)); + } + catch (IOException ignored) { + } + } + + @Override + public Type getType(int field) + { + return types[field]; + } + + @Override + public boolean advanceNextPosition() + { + try { + if (closed || !recordReader.hasNext()) { + close(); + return false; + } + + row = (OrcStruct) recordReader.next(row); + + // reset loaded flags + // partition keys are already loaded, but everything else is not + System.arraycopy(isPartitionColumn, 0, loaded, 0, isPartitionColumn.length); + + return true; + } + catch (IOException | RuntimeException e) { + closeWithSuppression(e); + throw new PrestoException(HIVE_CURSOR_ERROR, e); + } + } + + @Override + public boolean getBoolean(int fieldId) + { + checkState(!closed, "Cursor is closed"); + + validateType(fieldId, boolean.class); + if (!loaded[fieldId]) { + parseBooleanColumn(fieldId); + } + return booleans[fieldId]; + } + + private void parseBooleanColumn(int column) + { + // don't include column number in message because it causes boxing which is expensive here + checkArgument(!isPartitionColumn[column], "Column is a partition key"); + + loaded[column] = true; + + Object object = getFieldValue(row, hiveColumnIndexes[column]); + + if (object == null) { + nulls[column] = true; + } + else { + nulls[column] = false; + booleans[column] = ((BooleanWritable) object).get(); + } + } + + @Override + public long getLong(int fieldId) + { + checkState(!closed, "Cursor is closed"); + + validateType(fieldId, long.class); + if (!loaded[fieldId]) { + parseLongColumn(fieldId); + } + return longs[fieldId]; + } + + private void parseLongColumn(int column) + { + // don't include column number in message because it causes boxing which is expensive here + checkArgument(!isPartitionColumn[column], "Column is a partition key"); + + loaded[column] = true; + Object object = getFieldValue(row, hiveColumnIndexes[column]); + if (object == null) { + nulls[column] = true; + } + else { + nulls[column] = false; + + HiveType type = hiveTypes[column]; + if (hiveTypes[column].equals(HIVE_SHORT)) { + ShortWritable shortWritable = (ShortWritable) object; + longs[column] = shortWritable.get(); + } + else if (hiveTypes[column].equals(HIVE_DATE)) { + longs[column] = ((DateWritable) object).getDays(); + } + else if (hiveTypes[column].equals(HIVE_TIMESTAMP)) { + TimestampWritable timestampWritable = (TimestampWritable) object; + long seconds = timestampWritable.getSeconds(); + int nanos = timestampWritable.getNanos(); + longs[column] = (seconds * 1000) + (nanos / 1_000_000) + timeZoneCorrection; + } + else if (hiveTypes[column].equals(HIVE_BYTE)) { + ByteWritable byteWritable = (ByteWritable) object; + longs[column] = byteWritable.get(); + } + else if (hiveTypes[column].equals(HIVE_INT)) { + IntWritable intWritable = (IntWritable) object; + longs[column] = intWritable.get(); + } + else if (hiveTypes[column].equals(HIVE_LONG)) { + LongWritable longWritable = (LongWritable) object; + longs[column] = longWritable.get(); + } + else { + throw new RuntimeException(String.format("%s is not a valid LONG type", type)); + } + } + } + + @Override + public double getDouble(int fieldId) + { + checkState(!closed, "Cursor is closed"); + + validateType(fieldId, double.class); + if (!loaded[fieldId]) { + parseDoubleColumn(fieldId); + } + return doubles[fieldId]; + } + + private void parseDoubleColumn(int column) + { + // don't include column number in message because it causes boxing which is expensive here + checkArgument(!isPartitionColumn[column], "Column is a partition key"); + + loaded[column] = true; + Object object = getFieldValue(row, hiveColumnIndexes[column]); + if (object == null) { + nulls[column] = true; + } + else { + nulls[column] = false; + + HiveType type = hiveTypes[column]; + if (hiveTypes[column].equals(HIVE_FLOAT)) { + FloatWritable floatWritable = (FloatWritable) object; + doubles[column] = floatWritable.get(); + } + else if (hiveTypes[column].equals(HIVE_DOUBLE)) { + DoubleWritable doubleWritable = (DoubleWritable) object; + doubles[column] = doubleWritable.get(); + } + else { + throw new RuntimeException(String.format("%s is not a valid DOUBLE type", type)); + } + } + } + + @Override + public Slice getSlice(int fieldId) + { + checkState(!closed, "Cursor is closed"); + + validateType(fieldId, Slice.class); + if (!loaded[fieldId]) { + parseStringColumn(fieldId); + } + return slices[fieldId]; + } + + private void parseStringColumn(int column) + { + // don't include column number in message because it causes boxing which is expensive here + checkArgument(!isPartitionColumn[column], "Column is a partition key"); + + loaded[column] = true; + nulls[column] = false; + + Object object = getFieldValue(row, hiveColumnIndexes[column]); + if (object == null) { + nulls[column] = true; + return; + } + + HiveType type = hiveTypes[column]; + if (isStructuralType(type)) { + slices[column] = getBlockSlice(object, fieldInspectors[column]); + } + else if (type.equals(HIVE_STRING)) { + Text text = Types.checkType(object, Text.class, "materialized string value"); + slices[column] = Slices.copyOf(Slices.wrappedBuffer(text.getBytes()), 0, text.getLength()); + } + else if (type.equals(HIVE_BINARY)) { + BytesWritable bytesWritable = Types.checkType(object, BytesWritable.class, "materialized binary value"); + slices[column] = Slices.copyOf(Slices.wrappedBuffer(bytesWritable.getBytes()), 0, bytesWritable.getLength()); + } + else { + throw new RuntimeException(String.format("%s is not a valid STRING type", type)); + } + } + + @Override + public boolean isNull(int fieldId) + { + checkState(!closed, "Cursor is closed"); + + if (!loaded[fieldId]) { + parseColumn(fieldId); + } + return nulls[fieldId]; + } + + private void parseColumn(int column) + { + if (types[column].equals(BOOLEAN)) { + parseBooleanColumn(column); + } + else if (types[column].equals(BIGINT)) { + parseLongColumn(column); + } + else if (types[column].equals(DOUBLE)) { + parseDoubleColumn(column); + } + else if (types[column].equals(VARCHAR) || types[column].equals(VARBINARY) || isStructuralType(hiveTypes[column])) { + parseStringColumn(column); + } + else if (types[column].equals(TIMESTAMP)) { + parseLongColumn(column); + } + else if (types[column].equals(DATE)) { + parseLongColumn(column); + } + else { + throw new UnsupportedOperationException("Unsupported column type: " + types[column]); + } + } + + private void validateType(int fieldId, Class javaType) + { + if (types[fieldId].getJavaType() != javaType) { + // we don't use Preconditions.checkArgument because it requires boxing fieldId, which affects inner loop performance + throw new IllegalArgumentException(String.format("Expected field to be %s, actual %s (field %s)", javaType.getName(), types[fieldId].getJavaType().getName(), fieldId)); + } + } + + @Override + public void close() + { + // some hive input formats are broken and bad things can happen if you close them multiple times + if (closed) { + return; + } + closed = true; + + updateCompletedBytes(); + + try { + recordReader.close(); + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/orc/OrcPageSource.java b/presto-hive/src/main/java/com/facebook/presto/hive/orc/OrcPageSource.java new file mode 100644 index 00000000..2fe20eec --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/orc/OrcPageSource.java @@ -0,0 +1,484 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive.orc; + +import com.facebook.presto.hive.HiveColumnHandle; +import com.facebook.presto.hive.HivePartitionKey; +import com.facebook.presto.hive.HiveUtil; +import com.facebook.presto.orc.BooleanVector; +import com.facebook.presto.orc.DoubleVector; +import com.facebook.presto.orc.LongVector; +import com.facebook.presto.orc.OrcCorruptionException; +import com.facebook.presto.orc.OrcDataSource; +import com.facebook.presto.orc.OrcRecordReader; +import com.facebook.presto.orc.SliceVector; +import com.facebook.presto.spi.ConnectorPageSource; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.block.BlockBuilderStatus; +import com.facebook.presto.spi.block.LazyBlockLoader; +import com.facebook.presto.spi.block.LazyFixedWidthBlock; +import com.facebook.presto.spi.block.LazySliceArrayBlock; +import com.facebook.presto.spi.type.FixedWidthType; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; +import io.airlift.slice.Slices; +import org.joda.time.DateTimeZone; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import static com.facebook.presto.hive.HiveErrorCode.HIVE_BAD_DATA; +import static com.facebook.presto.hive.HiveErrorCode.HIVE_CURSOR_ERROR; +import static com.facebook.presto.hive.HiveUtil.bigintPartitionKey; +import static com.facebook.presto.hive.HiveUtil.booleanPartitionKey; +import static com.facebook.presto.hive.HiveUtil.datePartitionKey; +import static com.facebook.presto.hive.HiveUtil.doublePartitionKey; +import static com.facebook.presto.hive.HiveUtil.timestampPartitionKey; +import static com.facebook.presto.orc.Vector.MAX_VECTOR_LENGTH; +import static com.facebook.presto.spi.StandardErrorCode.NOT_SUPPORTED; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.BooleanType.BOOLEAN; +import static com.facebook.presto.spi.type.DateType.DATE; +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; +import static com.facebook.presto.spi.type.StandardTypes.ARRAY; +import static com.facebook.presto.spi.type.StandardTypes.MAP; +import static com.facebook.presto.spi.type.StandardTypes.ROW; +import static com.facebook.presto.spi.type.TimestampType.TIMESTAMP; +import static com.facebook.presto.spi.type.VarbinaryType.VARBINARY; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.Maps.uniqueIndex; +import static io.airlift.slice.Slices.wrappedBooleanArray; +import static io.airlift.slice.Slices.wrappedDoubleArray; +import static io.airlift.slice.Slices.wrappedIntArray; +import static io.airlift.slice.Slices.wrappedLongArray; +import static java.lang.Math.max; +import static java.lang.Math.min; +import static java.lang.String.format; +import static java.nio.charset.StandardCharsets.UTF_8; + +public class OrcPageSource + implements ConnectorPageSource +{ + private static final int NULL_ENTRY_SIZE = 0; + private final OrcRecordReader recordReader; + private final OrcDataSource orcDataSource; + + private final List columnNames; + private final List types; + private final boolean[] isStructuralType; + + private final Block[] constantBlocks; + private final int[] hiveColumnIndexes; + + private long completedBytes; + + private int batchId; + private boolean closed; + + public OrcPageSource( + OrcRecordReader recordReader, + OrcDataSource orcDataSource, + List partitionKeys, + List columns, + DateTimeZone hiveStorageTimeZone, + TypeManager typeManager) + { + this.recordReader = checkNotNull(recordReader, "recordReader is null"); + this.orcDataSource = checkNotNull(orcDataSource, "orcDataSource is null"); + + Map partitionKeysByName = uniqueIndex(checkNotNull(partitionKeys, "partitionKeys is null"), HivePartitionKey::getName); + + int size = checkNotNull(columns, "columns is null").size(); + + this.isStructuralType = new boolean[size]; + + this.constantBlocks = new Block[size]; + this.hiveColumnIndexes = new int[size]; + + ImmutableList.Builder namesBuilder = ImmutableList.builder(); + ImmutableList.Builder typesBuilder = ImmutableList.builder(); + for (int columnIndex = 0; columnIndex < columns.size(); columnIndex++) { + HiveColumnHandle column = columns.get(columnIndex); + + String name = column.getName(); + Type type = typeManager.getType(column.getTypeSignature()); + + namesBuilder.add(name); + typesBuilder.add(type); + + String typeBase = column.getTypeSignature().getBase(); + isStructuralType[columnIndex] = ARRAY.equals(typeBase) || MAP.equals(typeBase) || ROW.equals(typeBase); + + hiveColumnIndexes[columnIndex] = column.getHiveColumnIndex(); + + if (column.isPartitionKey()) { + HivePartitionKey partitionKey = partitionKeysByName.get(name); + checkArgument(partitionKey != null, "No value provided for partition key %s", name); + + byte[] bytes = partitionKey.getValue().getBytes(UTF_8); + + BlockBuilder blockBuilder; + if (type instanceof FixedWidthType) { + blockBuilder = type.createBlockBuilder(new BlockBuilderStatus(), MAX_VECTOR_LENGTH); + } + else { + blockBuilder = type.createBlockBuilder(new BlockBuilderStatus(), MAX_VECTOR_LENGTH, bytes.length); + } + + if (HiveUtil.isHiveNull(bytes)) { + for (int i = 0; i < MAX_VECTOR_LENGTH; i++) { + blockBuilder.appendNull(); + } + } + else if (type.equals(BOOLEAN)) { + boolean value = booleanPartitionKey(partitionKey.getValue(), name); + for (int i = 0; i < MAX_VECTOR_LENGTH; i++) { + BOOLEAN.writeBoolean(blockBuilder, value); + } + } + else if (type.equals(BIGINT)) { + long value = bigintPartitionKey(partitionKey.getValue(), name); + for (int i = 0; i < MAX_VECTOR_LENGTH; i++) { + BIGINT.writeLong(blockBuilder, value); + } + } + else if (type.equals(DOUBLE)) { + double value = doublePartitionKey(partitionKey.getValue(), name); + for (int i = 0; i < MAX_VECTOR_LENGTH; i++) { + DOUBLE.writeDouble(blockBuilder, value); + } + } + else if (type.equals(VARCHAR)) { + Slice value = Slices.wrappedBuffer(bytes); + for (int i = 0; i < MAX_VECTOR_LENGTH; i++) { + VARCHAR.writeSlice(blockBuilder, value); + } + } + else if (type.equals(DATE)) { + long value = datePartitionKey(partitionKey.getValue(), name); + for (int i = 0; i < MAX_VECTOR_LENGTH; i++) { + DATE.writeLong(blockBuilder, value); + } + } + else if (type.equals(TIMESTAMP)) { + long value = timestampPartitionKey(partitionKey.getValue(), hiveStorageTimeZone, name); + for (int i = 0; i < MAX_VECTOR_LENGTH; i++) { + TIMESTAMP.writeLong(blockBuilder, value); + } + } + else { + throw new PrestoException(NOT_SUPPORTED, format("Unsupported column type %s for partition key: %s", type.getDisplayName(), name)); + } + + constantBlocks[columnIndex] = blockBuilder.build(); + } + else if (!recordReader.isColumnPresent(column.getHiveColumnIndex())) { + BlockBuilder blockBuilder = type.createBlockBuilder(new BlockBuilderStatus(), MAX_VECTOR_LENGTH, NULL_ENTRY_SIZE); + for (int i = 0; i < MAX_VECTOR_LENGTH; i++) { + blockBuilder.appendNull(); + } + constantBlocks[columnIndex] = blockBuilder.build(); + } + } + types = typesBuilder.build(); + columnNames = namesBuilder.build(); + } + + @Override + public long getTotalBytes() + { + return recordReader.getSplitLength(); + } + + @Override + public long getCompletedBytes() + { + return completedBytes; + } + + @Override + public long getReadTimeNanos() + { + return orcDataSource.getReadTimeNanos(); + } + + @Override + public boolean isFinished() + { + return closed; + } + + @Override + public Page getNextPage() + { + try { + batchId++; + int batchSize = recordReader.nextBatch(); + if (batchSize <= 0) { + close(); + return null; + } + + Block[] blocks = new Block[hiveColumnIndexes.length]; + for (int fieldId = 0; fieldId < blocks.length; fieldId++) { + Type type = types.get(fieldId); + if (constantBlocks[fieldId] != null) { + blocks[fieldId] = constantBlocks[fieldId].getRegion(0, batchSize); + } + else if (BOOLEAN.equals(type)) { + blocks[fieldId] = new LazyFixedWidthBlock(BOOLEAN.getFixedSize(), batchSize, new LazyBooleanBlockLoader(hiveColumnIndexes[fieldId], batchSize)); + } + else if (DATE.equals(type)) { + blocks[fieldId] = new LazyFixedWidthBlock(DATE.getFixedSize(), batchSize, new LazyDateBlockLoader(hiveColumnIndexes[fieldId], batchSize)); + } + else if (BIGINT.equals(type) || TIMESTAMP.equals(type)) { + blocks[fieldId] = new LazyFixedWidthBlock(((FixedWidthType) type).getFixedSize(), batchSize, new LazyLongBlockLoader(hiveColumnIndexes[fieldId], batchSize)); + } + else if (DOUBLE.equals(type)) { + blocks[fieldId] = new LazyFixedWidthBlock(DOUBLE.getFixedSize(), batchSize, new LazyDoubleBlockLoader(hiveColumnIndexes[fieldId], batchSize)); + } + else if (VARCHAR.equals(type) || VARBINARY.equals(type) || isStructuralType[fieldId]) { + blocks[fieldId] = new LazySliceArrayBlock(batchSize, new LazySliceBlockLoader(hiveColumnIndexes[fieldId], batchSize)); + } + else { + throw new PrestoException(NOT_SUPPORTED, "Unsupported column type: " + type); + } + } + Page page = new Page(batchSize, blocks); + + long newCompletedBytes = (long) (recordReader.getSplitLength() * recordReader.getProgress()); + completedBytes = min(recordReader.getSplitLength(), max(completedBytes, newCompletedBytes)); + + return page; + } + catch (PrestoException e) { + closeWithSuppression(e); + throw e; + } + catch (IOException | RuntimeException e) { + closeWithSuppression(e); + throw new PrestoException(HIVE_CURSOR_ERROR, e); + } + } + + @Override + public void close() + { + // some hive input formats are broken and bad things can happen if you close them multiple times + if (closed) { + return; + } + closed = true; + + try { + recordReader.close(); + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("columnNames", columnNames) + .add("types", types) + .toString(); + } + + protected void closeWithSuppression(Throwable throwable) + { + checkNotNull(throwable, "throwable is null"); + try { + close(); + } + catch (RuntimeException e) { + throwable.addSuppressed(e); + } + } + + private final class LazyBooleanBlockLoader + implements LazyBlockLoader + { + private final int expectedBatchId = batchId; + private final int batchSize; + private final int hiveColumnIndex; + + public LazyBooleanBlockLoader(int hiveColumnIndex, int batchSize) + { + this.batchSize = batchSize; + this.hiveColumnIndex = hiveColumnIndex; + } + + @Override + public void load(LazyFixedWidthBlock block) + { + checkState(batchId == expectedBatchId); + try { + BooleanVector vector = new BooleanVector(batchSize); + recordReader.readVector(hiveColumnIndex, vector); + block.setNullVector(vector.isNull); + block.setRawSlice(wrappedBooleanArray(vector.vector, 0, batchSize)); + } + catch (IOException e) { + throw propagateException(e); + } + } + } + + private final class LazyDateBlockLoader + implements LazyBlockLoader + { + private final int expectedBatchId = batchId; + private final int batchSize; + private final int hiveColumnIndex; + + public LazyDateBlockLoader(int hiveColumnIndex, int batchSize) + { + this.batchSize = batchSize; + this.hiveColumnIndex = hiveColumnIndex; + } + + @Override + public void load(LazyFixedWidthBlock block) + { + checkState(batchId == expectedBatchId); + try { + LongVector vector = new LongVector(batchSize); + recordReader.readVector(hiveColumnIndex, vector); + block.setNullVector(vector.isNull); + + // Presto stores dates as ints in memory, so convert to int array + // TODO to add an ORC int vector + int[] days = new int[batchSize]; + for (int i = 0; i < batchSize; i++) { + days[i] = (int) vector.vector[i]; + } + + block.setRawSlice(wrappedIntArray(days, 0, batchSize)); + } + catch (IOException e) { + throw propagateException(e); + } + } + } + + private final class LazyLongBlockLoader + implements LazyBlockLoader + { + private final int expectedBatchId = batchId; + private final int batchSize; + private final int hiveColumnIndex; + + public LazyLongBlockLoader(int hiveColumnIndex, int batchSize) + { + this.batchSize = batchSize; + this.hiveColumnIndex = hiveColumnIndex; + } + + @Override + public void load(LazyFixedWidthBlock block) + { + checkState(batchId == expectedBatchId); + try { + LongVector vector = new LongVector(batchSize); + recordReader.readVector(hiveColumnIndex, vector); + block.setNullVector(vector.isNull); + block.setRawSlice(wrappedLongArray(vector.vector, 0, batchSize)); + } + catch (IOException e) { + throw propagateException(e); + } + } + } + + private final class LazyDoubleBlockLoader + implements LazyBlockLoader + { + private final int expectedBatchId = batchId; + + private final int batchSize; + private final int hiveColumnIndex; + + public LazyDoubleBlockLoader(int hiveColumnIndex, int batchSize) + { + this.batchSize = batchSize; + this.hiveColumnIndex = hiveColumnIndex; + } + + @Override + public void load(LazyFixedWidthBlock block) + { + checkState(batchId == expectedBatchId); + try { + DoubleVector vector = new DoubleVector(batchSize); + recordReader.readVector(hiveColumnIndex, vector); + block.setNullVector(vector.isNull); + block.setRawSlice(wrappedDoubleArray(vector.vector, 0, batchSize)); + } + catch (IOException e) { + throw propagateException(e); + } + } + } + + private final class LazySliceBlockLoader + implements LazyBlockLoader + { + private final int expectedBatchId = batchId; + + private final int batchSize; + private final int hiveColumnIndex; + + public LazySliceBlockLoader(int hiveColumnIndex, int batchSize) + { + this.batchSize = batchSize; + this.hiveColumnIndex = hiveColumnIndex; + } + + @Override + public void load(LazySliceArrayBlock block) + { + checkState(batchId == expectedBatchId); + try { + SliceVector vector = new SliceVector(batchSize); + recordReader.readVector(hiveColumnIndex, vector); + block.setValues(vector.vector); + } + catch (IOException e) { + throw propagateException(e); + } + } + } + + private static RuntimeException propagateException(IOException e) + { + if (e instanceof OrcCorruptionException) { + throw new PrestoException(HIVE_BAD_DATA, e); + } + throw new PrestoException(HIVE_CURSOR_ERROR, e); + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/orc/OrcPageSourceFactory.java b/presto-hive/src/main/java/com/facebook/presto/hive/orc/OrcPageSourceFactory.java new file mode 100644 index 00000000..90451591 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/orc/OrcPageSourceFactory.java @@ -0,0 +1,210 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive.orc; + +import com.facebook.presto.hive.HiveClientConfig; +import com.facebook.presto.hive.HiveColumnHandle; +import com.facebook.presto.hive.HivePageSourceFactory; +import com.facebook.presto.hive.HivePartitionKey; +import com.facebook.presto.orc.OrcDataSource; +import com.facebook.presto.orc.OrcPredicate; +import com.facebook.presto.orc.OrcReader; +import com.facebook.presto.orc.OrcRecordReader; +import com.facebook.presto.orc.TupleDomainOrcPredicate; +import com.facebook.presto.orc.TupleDomainOrcPredicate.ColumnReference; +import com.facebook.presto.orc.metadata.MetadataReader; +import com.facebook.presto.orc.metadata.OrcMetadataReader; +import com.facebook.presto.spi.ConnectorPageSource; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.TupleDomain; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.airlift.units.DataSize; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.FSDataInputStream; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.hive.ql.io.orc.OrcSerde; +import org.joda.time.DateTimeZone; + +import javax.inject.Inject; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.List; +import java.util.Optional; +import java.util.Properties; + +import static com.facebook.presto.hive.HiveErrorCode.HIVE_CANNOT_OPEN_SPLIT; +import static com.facebook.presto.hive.HiveErrorCode.HIVE_MISSING_DATA; +import static com.facebook.presto.hive.HiveSessionProperties.getOrcMaxBufferSize; +import static com.facebook.presto.hive.HiveSessionProperties.getOrcMaxMergeDistance; +import static com.facebook.presto.hive.HiveSessionProperties.getOrcStreamBufferSize; +import static com.facebook.presto.hive.HiveSessionProperties.isOptimizedReaderEnabled; +import static com.facebook.presto.hive.HiveUtil.isDeserializerClass; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Strings.nullToEmpty; +import static io.airlift.units.DataSize.Unit.MEGABYTE; +import static java.lang.String.format; + +public class OrcPageSourceFactory + implements HivePageSourceFactory +{ + private final TypeManager typeManager; + private final boolean enabled; + private final DataSize orcMaxMergeDistance; + private final DataSize orcMaxBufferSize; + private final DataSize orcStreamBufferSize; + + @Inject + public OrcPageSourceFactory(TypeManager typeManager, HiveClientConfig config) + { + //noinspection deprecation + this(typeManager, config.isOptimizedReaderEnabled(), config.getOrcMaxMergeDistance(), config.getOrcMaxBufferSize(), config.getOrcStreamBufferSize()); + } + + public OrcPageSourceFactory(TypeManager typeManager) + { + this(typeManager, true, new DataSize(1, MEGABYTE), new DataSize(8, MEGABYTE), new DataSize(8, MEGABYTE)); + } + + public OrcPageSourceFactory(TypeManager typeManager, boolean enabled, DataSize orcMaxMergeDistance, DataSize orcMaxBufferSize, DataSize orcStreamBufferSize) + { + this.typeManager = checkNotNull(typeManager, "typeManager is null"); + this.enabled = enabled; + this.orcMaxMergeDistance = checkNotNull(orcMaxMergeDistance, "orcMaxMergeDistance is null"); + this.orcMaxBufferSize = checkNotNull(orcMaxBufferSize, "orcMaxBufferSize is null"); + this.orcStreamBufferSize = checkNotNull(orcStreamBufferSize, "orcStreamBufferSize is null"); + } + + @Override + public Optional createPageSource( + Configuration configuration, + ConnectorSession session, + Path path, + long start, + long length, + Properties schema, + List columns, + List partitionKeys, + TupleDomain effectivePredicate, + DateTimeZone hiveStorageTimeZone) + { + if (!isOptimizedReaderEnabled(session, enabled)) { + return Optional.empty(); + } + + if (!isDeserializerClass(schema, OrcSerde.class)) { + return Optional.empty(); + } + + return Optional.of(createOrcPageSource( + new OrcMetadataReader(), + configuration, + path, + start, + length, + columns, + partitionKeys, + effectivePredicate, + hiveStorageTimeZone, + typeManager, + getOrcMaxMergeDistance(session, orcMaxMergeDistance), + getOrcMaxBufferSize(session, orcMaxBufferSize), + getOrcStreamBufferSize(session, orcStreamBufferSize))); + } + + public static OrcPageSource createOrcPageSource(MetadataReader metadataReader, + Configuration configuration, + Path path, + long start, + long length, + List columns, + List partitionKeys, + TupleDomain effectivePredicate, + DateTimeZone hiveStorageTimeZone, + TypeManager typeManager, + DataSize maxMergeDistance, + DataSize maxBufferSize, + DataSize streamBufferSize) + { + OrcDataSource orcDataSource; + try { + FileSystem fileSystem = path.getFileSystem(configuration); + long size = fileSystem.getFileStatus(path).getLen(); + FSDataInputStream inputStream = fileSystem.open(path); + orcDataSource = new HdfsOrcDataSource(path.toString(), size, maxMergeDistance, maxBufferSize, streamBufferSize, inputStream); + } + catch (Exception e) { + if (nullToEmpty(e.getMessage()).trim().equals("Filesystem closed") || + e instanceof FileNotFoundException) { + throw new PrestoException(HIVE_CANNOT_OPEN_SPLIT, e); + } + throw new PrestoException(HIVE_CANNOT_OPEN_SPLIT, splitError(e, path, start, length), e); + } + + ImmutableMap.Builder includedColumns = ImmutableMap.builder(); + ImmutableList.Builder> columnReferences = ImmutableList.builder(); + for (HiveColumnHandle column : columns) { + if (!column.isPartitionKey()) { + Type type = typeManager.getType(column.getTypeSignature()); + includedColumns.put(column.getHiveColumnIndex(), type); + columnReferences.add(new ColumnReference<>(column, column.getHiveColumnIndex(), type)); + } + } + + OrcPredicate predicate = new TupleDomainOrcPredicate<>(effectivePredicate, columnReferences.build()); + + try { + OrcReader reader = new OrcReader(orcDataSource, metadataReader); + OrcRecordReader recordReader = reader.createRecordReader( + includedColumns.build(), + predicate, + start, + length, + hiveStorageTimeZone); + + return new OrcPageSource( + recordReader, + orcDataSource, + partitionKeys, + columns, + hiveStorageTimeZone, + typeManager); + } + catch (Exception e) { + try { + orcDataSource.close(); + } + catch (IOException ignored) { + } + if (e instanceof PrestoException) { + throw (PrestoException) e; + } + String message = splitError(e, path, start, length); + if (e.getClass().getSimpleName().equals("BlockMissingException")) { + throw new PrestoException(HIVE_MISSING_DATA, message, e); + } + throw new PrestoException(HIVE_CANNOT_OPEN_SPLIT, message, e); + } + } + + private static String splitError(Throwable t, Path path, long start, long length) + { + return format("Error opening Hive split %s (offset=%s, length=%s): %s", path, start, length, t.getMessage()); + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/orc/OrcRecordCursorProvider.java b/presto-hive/src/main/java/com/facebook/presto/hive/orc/OrcRecordCursorProvider.java new file mode 100644 index 00000000..5e8cec72 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/orc/OrcRecordCursorProvider.java @@ -0,0 +1,146 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive.orc; + +import com.facebook.presto.hive.HiveClientConfig; +import com.facebook.presto.hive.HiveColumnHandle; +import com.facebook.presto.hive.HivePartitionKey; +import com.facebook.presto.hive.HiveRecordCursor; +import com.facebook.presto.hive.HiveRecordCursorProvider; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.TupleDomain; +import com.facebook.presto.spi.type.TypeManager; +import com.google.common.base.Throwables; +import com.google.common.collect.Lists; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.hive.ql.io.orc.OrcFile; +import org.apache.hadoop.hive.ql.io.orc.OrcProto.Type; +import org.apache.hadoop.hive.ql.io.orc.OrcSerde; +import org.apache.hadoop.hive.ql.io.orc.Reader; +import org.apache.hadoop.hive.ql.io.orc.RecordReader; +import org.joda.time.DateTimeZone; + +import javax.inject.Inject; + +import java.util.List; +import java.util.Optional; +import java.util.Properties; + +import static com.facebook.presto.hive.HiveUtil.isDeserializerClass; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class OrcRecordCursorProvider + implements HiveRecordCursorProvider +{ + private final boolean enabled; + + @Inject + public OrcRecordCursorProvider(HiveClientConfig config) + { + //noinspection deprecation + this(!config.isOptimizedReaderEnabled()); + } + + public OrcRecordCursorProvider() + { + this(true); + } + + public OrcRecordCursorProvider(boolean enabled) + { + this.enabled = enabled; + } + + @Override + public Optional createHiveRecordCursor( + String clientId, + Configuration configuration, + ConnectorSession session, + Path path, + long start, + long length, + Properties schema, + List columns, + List partitionKeys, + TupleDomain effectivePredicate, + DateTimeZone hiveStorageTimeZone, + TypeManager typeManager) + { + if (!enabled) { + return Optional.empty(); + } + + if (!isDeserializerClass(schema, OrcSerde.class)) { + return Optional.empty(); + } + + RecordReader recordReader; + try { + FileSystem fileSystem = path.getFileSystem(configuration); + Reader reader = OrcFile.createReader(fileSystem, path); + boolean[] include = findIncludedColumns(reader.getTypes(), columns); + recordReader = reader.rows(start, length, include); + } + catch (Exception e) { + throw Throwables.propagate(e); + } + + return Optional.of(new OrcHiveRecordCursor( + recordReader, + length, + schema, + partitionKeys, + columns, + hiveStorageTimeZone, + typeManager)); + } + + private static boolean[] findIncludedColumns(List types, List columns) + { + checkNotNull(types, "types is null"); + checkArgument(!types.isEmpty(), "types is empty"); + + boolean[] includes = new boolean[types.size()]; + includes[0] = true; + + Type root = types.get(0); + List included = Lists.transform(columns, HiveColumnHandle::getHiveColumnIndex); + for (int i = 0; i < root.getSubtypesCount(); ++i) { + if (included.contains(i)) { + includeColumnRecursive(types, includes, root.getSubtypes(i)); + } + } + + // if we are filtering at least one column, return the boolean array + for (boolean include : includes) { + if (!include) { + return includes; + } + } + return null; + } + + private static void includeColumnRecursive(List types, boolean[] result, int typeId) + { + result[typeId] = true; + Type type = types.get(typeId); + int children = type.getSubtypesCount(); + for (int i = 0; i < children; ++i) { + includeColumnRecursive(types, result, type.getSubtypes(i)); + } + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/rcfile/RcBinaryBlockLoader.java b/presto-hive/src/main/java/com/facebook/presto/hive/rcfile/RcBinaryBlockLoader.java new file mode 100644 index 00000000..bbde5253 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/rcfile/RcBinaryBlockLoader.java @@ -0,0 +1,727 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive.rcfile; + +import com.facebook.presto.hive.HiveType; +import com.facebook.presto.hive.rcfile.RcFilePageSource.RcFileColumnsBatch; +import com.facebook.presto.spi.block.LazyBlockLoader; +import com.facebook.presto.spi.block.LazyFixedWidthBlock; +import com.facebook.presto.spi.block.LazySliceArrayBlock; +import com.google.common.base.Throwables; +import io.airlift.slice.ByteArrays; +import io.airlift.slice.Slice; +import io.airlift.slice.Slices; +import org.apache.hadoop.hive.serde2.columnar.BytesRefArrayWritable; +import org.apache.hadoop.hive.serde2.columnar.BytesRefWritable; +import org.apache.hadoop.hive.serde2.io.TimestampWritable; +import org.apache.hadoop.hive.serde2.lazy.ByteArrayRef; +import org.apache.hadoop.hive.serde2.lazybinary.LazyBinaryFactory; +import org.apache.hadoop.hive.serde2.lazybinary.LazyBinaryObject; +import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector; +import org.apache.hadoop.io.WritableUtils; + +import java.io.IOException; +import java.util.Arrays; + +import static com.facebook.presto.hive.HiveType.HIVE_BINARY; +import static com.facebook.presto.hive.HiveType.HIVE_BOOLEAN; +import static com.facebook.presto.hive.HiveType.HIVE_BYTE; +import static com.facebook.presto.hive.HiveType.HIVE_DATE; +import static com.facebook.presto.hive.HiveType.HIVE_DOUBLE; +import static com.facebook.presto.hive.HiveType.HIVE_FLOAT; +import static com.facebook.presto.hive.HiveType.HIVE_INT; +import static com.facebook.presto.hive.HiveType.HIVE_LONG; +import static com.facebook.presto.hive.HiveType.HIVE_SHORT; +import static com.facebook.presto.hive.HiveType.HIVE_STRING; +import static com.facebook.presto.hive.HiveType.HIVE_TIMESTAMP; +import static com.facebook.presto.hive.HiveUtil.isStructuralType; +import static com.facebook.presto.hive.util.SerDeUtils.getBlockSlice; +import static com.google.common.base.Preconditions.checkState; +import static io.airlift.slice.SizeOf.SIZE_OF_INT; +import static io.airlift.slice.SizeOf.SIZE_OF_LONG; +import static io.airlift.slice.SizeOf.SIZE_OF_SHORT; +import static io.airlift.slice.Slices.wrappedBooleanArray; +import static io.airlift.slice.Slices.wrappedDoubleArray; +import static io.airlift.slice.Slices.wrappedLongArray; + +public class RcBinaryBlockLoader + implements RcFileBlockLoader +{ + private static final byte HIVE_EMPTY_STRING_BYTE = (byte) 0xbf; + + @Override + public LazyBlockLoader fixedWidthBlockLoader(RcFileColumnsBatch batch, int fieldId, HiveType hiveType) + { + if (HIVE_BOOLEAN.equals(hiveType)) { + return new LazyBooleanBlockLoader(batch, fieldId); + } + if (HIVE_BYTE.equals(hiveType)) { + return new LazyByteBlockLoader(batch, fieldId); + } + if (HIVE_SHORT.equals(hiveType)) { + return new LazyShortBlockLoader(batch, fieldId); + } + if (HIVE_INT.equals(hiveType)) { + return new LazyIntBlockLoader(batch, fieldId); + } + if (HIVE_LONG.equals(hiveType)) { + return new LazyLongBlockLoader(batch, fieldId); + } + if (HIVE_DATE.equals(hiveType)) { + return new LazyDateBlockLoader(batch, fieldId); + } + if (HIVE_TIMESTAMP.equals(hiveType)) { + return new LazyTimestampBlockLoader(batch, fieldId); + } + if (HIVE_FLOAT.equals(hiveType)) { + return new LazyFloatBlockLoader(batch, fieldId); + } + if (HIVE_DOUBLE.equals(hiveType)) { + return new LazyDoubleBlockLoader(batch, fieldId); + } + throw new UnsupportedOperationException("Unsupported column type: " + hiveType); + } + + @Override + public LazyBlockLoader variableWidthBlockLoader(RcFileColumnsBatch batch, int fieldId, HiveType hiveType, ObjectInspector fieldInspector) + { + if (HIVE_STRING.equals(hiveType) || HIVE_BINARY.equals(hiveType)) { + return new LazySliceBlockLoader(batch, fieldId); + } + if (isStructuralType(hiveType)) { + return new LazyJsonSliceBlockLoader(batch, fieldId, fieldInspector); + } + throw new UnsupportedOperationException("Unsupported column type: " + hiveType); + } + + private static final class LazyBooleanBlockLoader + implements LazyBlockLoader + { + private final RcFileColumnsBatch batch; + private final int fieldId; + private boolean loaded; + + public LazyBooleanBlockLoader(RcFileColumnsBatch batch, int fieldId) + { + this.batch = batch; + this.fieldId = fieldId; + } + + @Override + public void load(LazyFixedWidthBlock block) + { + if (loaded) { + return; + } + + try { + BytesRefArrayWritable columnBatch = batch.getColumn(fieldId); + int positionInBatch = batch.getPositionInBatch(); + + int positionCount = block.getPositionCount(); + boolean[] isNull = new boolean[positionCount]; + boolean[] vector = new boolean[positionCount]; + + for (int i = 0; i < positionCount; i++) { + BytesRefWritable writable = columnBatch.unCheckedGet(i + positionInBatch); + + int length = writable.getLength(); + if (length != 0) { + byte[] bytes = writable.getData(); + int start = writable.getStart(); + vector[i] = bytes[start] != 0; + } + else { + isNull[i] = true; + } + } + + block.setNullVector(isNull); + block.setRawSlice(wrappedBooleanArray(vector, 0, positionCount)); + + loaded = true; + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } + } + + private static final class LazyByteBlockLoader + implements LazyBlockLoader + { + private final RcFileColumnsBatch batch; + private final int fieldId; + private boolean loaded; + + private LazyByteBlockLoader(RcFileColumnsBatch batch, int fieldId) + { + this.batch = batch; + this.fieldId = fieldId; + } + + @Override + public void load(LazyFixedWidthBlock block) + { + if (loaded) { + return; + } + + try { + BytesRefArrayWritable columnBatch = batch.getColumn(fieldId); + int positionInBatch = batch.getPositionInBatch(); + + int batchSize = block.getPositionCount(); + boolean[] isNull = new boolean[batchSize]; + long[] vector = new long[batchSize]; + + for (int i = 0; i < batchSize; i++) { + BytesRefWritable writable = columnBatch.unCheckedGet(i + positionInBatch); + + int length = writable.getLength(); + if (length != 0) { + byte[] bytes = writable.getData(); + int start = writable.getStart(); + vector[i] = bytes[start]; + } + else { + isNull[i] = true; + } + } + + block.setNullVector(isNull); + block.setRawSlice(wrappedLongArray(vector)); + + loaded = true; + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } + } + + private static final class LazyShortBlockLoader + implements LazyBlockLoader + { + private final RcFileColumnsBatch batch; + private final int fieldId; + private boolean loaded; + + private LazyShortBlockLoader(RcFileColumnsBatch batch, int fieldId) + { + this.batch = batch; + this.fieldId = fieldId; + } + + @Override + public void load(LazyFixedWidthBlock block) + { + if (loaded) { + return; + } + + try { + BytesRefArrayWritable columnBatch = batch.getColumn(fieldId); + int positionInBatch = batch.getPositionInBatch(); + + int batchSize = block.getPositionCount(); + boolean[] isNull = new boolean[batchSize]; + long[] vector = new long[batchSize]; + + for (int i = 0; i < batchSize; i++) { + BytesRefWritable writable = columnBatch.unCheckedGet(i + positionInBatch); + + int length = writable.getLength(); + if (length != 0) { + checkState(length == SIZE_OF_SHORT, "Short should be 2 bytes"); + + // the file format uses big endian + byte[] bytes = writable.getData(); + int start = writable.getStart(); + vector[i] = (long) Short.reverseBytes(ByteArrays.getShort(bytes, start)); + } + else { + isNull[i] = true; + } + } + + block.setNullVector(isNull); + block.setRawSlice(wrappedLongArray(vector)); + + loaded = true; + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } + } + + private static final class LazyIntBlockLoader + implements LazyBlockLoader + { + private final RcFileColumnsBatch batch; + private final int fieldId; + private boolean loaded; + + private LazyIntBlockLoader(RcFileColumnsBatch batch, int fieldId) + { + this.batch = batch; + this.fieldId = fieldId; + } + + @Override + public void load(LazyFixedWidthBlock block) + { + if (loaded) { + return; + } + + try { + BytesRefArrayWritable columnBatch = batch.getColumn(fieldId); + int positionInBatch = batch.getPositionInBatch(); + + int batchSize = block.getPositionCount(); + boolean[] isNull = new boolean[batchSize]; + long[] vector = new long[batchSize]; + + for (int i = 0; i < batchSize; i++) { + BytesRefWritable writable = columnBatch.unCheckedGet(i + positionInBatch); + + byte[] bytes = writable.getData(); + int start = writable.getStart(); + int length = writable.getLength(); + if (length == 0) { + isNull[i] = true; + } + else if (length == 1) { + vector[i] = bytes[start]; + } + else { + vector[i] = readVInt(bytes, start, length); + } + } + + block.setNullVector(isNull); + block.setRawSlice(wrappedLongArray(vector)); + + loaded = true; + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } + } + + private static final class LazyLongBlockLoader + implements LazyBlockLoader + { + private final RcFileColumnsBatch batch; + private final int fieldId; + private boolean loaded; + + private LazyLongBlockLoader(RcFileColumnsBatch batch, int fieldId) + { + this.batch = batch; + this.fieldId = fieldId; + } + + @Override + public void load(LazyFixedWidthBlock block) + { + if (loaded) { + return; + } + + try { + BytesRefArrayWritable columnBatch = batch.getColumn(fieldId); + int positionInBatch = batch.getPositionInBatch(); + + int batchSize = block.getPositionCount(); + boolean[] isNull = new boolean[batchSize]; + long[] vector = new long[batchSize]; + + for (int i = 0; i < batchSize; i++) { + BytesRefWritable writable = columnBatch.unCheckedGet(i + positionInBatch); + + byte[] bytes = writable.getData(); + int start = writable.getStart(); + int length = writable.getLength(); + if (length == 0) { + isNull[i] = true; + } + else if (length == 1) { + vector[i] = bytes[start]; + } + else { + vector[i] = readVInt(bytes, start, length); + } + } + + block.setNullVector(isNull); + block.setRawSlice(wrappedLongArray(vector)); + + loaded = true; + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } + + } + + private static final class LazyDateBlockLoader + implements LazyBlockLoader + { + private final RcFileColumnsBatch batch; + private final int fieldId; + private boolean loaded; + + private LazyDateBlockLoader(RcFileColumnsBatch batch, int fieldId) + { + this.batch = batch; + this.fieldId = fieldId; + } + + @Override + public void load(LazyFixedWidthBlock block) + { + if (loaded) { + return; + } + + try { + BytesRefArrayWritable columnBatch = batch.getColumn(fieldId); + int positionInBatch = batch.getPositionInBatch(); + + int positionCount = block.getPositionCount(); + boolean[] isNull = new boolean[positionCount]; + long[] vector = new long[positionCount]; + + for (int i = 0; i < positionCount; i++) { + BytesRefWritable writable = columnBatch.unCheckedGet(i + positionInBatch); + + int length = writable.getLength(); + if (length != 0) { + byte[] bytes = writable.getData(); + int start = writable.getStart(); + long daysSinceEpoch = readVInt(bytes, start, length); + vector[i] = daysSinceEpoch; + } + else { + isNull[i] = true; + } + } + + block.setNullVector(isNull); + block.setRawSlice(wrappedLongArray(vector)); + + loaded = true; + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } + + } + + private static final class LazyTimestampBlockLoader + implements LazyBlockLoader + { + private final RcFileColumnsBatch batch; + private final int fieldId; + private boolean loaded; + + private LazyTimestampBlockLoader(RcFileColumnsBatch batch, int fieldId) + { + this.batch = batch; + this.fieldId = fieldId; + } + + @Override + public void load(LazyFixedWidthBlock block) + { + if (loaded) { + return; + } + + try { + BytesRefArrayWritable columnBatch = batch.getColumn(fieldId); + int positionInBatch = batch.getPositionInBatch(); + + int batchSize = block.getPositionCount(); + boolean[] isNull = new boolean[batchSize]; + long[] vector = new long[batchSize]; + + for (int i = 0; i < batchSize; i++) { + BytesRefWritable writable = columnBatch.unCheckedGet(i + positionInBatch); + + int length = writable.getLength(); + if (length != 0) { + byte[] bytes = writable.getData(); + int start = writable.getStart(); + + long seconds = TimestampWritable.getSeconds(bytes, start); + long nanos = TimestampWritable.getNanos(bytes, start + SIZE_OF_INT); + vector[i] = (seconds * 1000) + (nanos / 1_000_000); + } + else { + isNull[i] = true; + } + } + + block.setNullVector(isNull); + block.setRawSlice(wrappedLongArray(vector)); + + loaded = true; + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } + } + + private static final class LazyFloatBlockLoader + implements LazyBlockLoader + { + private final RcFileColumnsBatch batch; + private final int fieldId; + private boolean loaded; + + private LazyFloatBlockLoader(RcFileColumnsBatch batch, int fieldId) + { + this.batch = batch; + this.fieldId = fieldId; + } + + @Override + public void load(LazyFixedWidthBlock block) + { + if (loaded) { + return; + } + + try { + BytesRefArrayWritable columnBatch = batch.getColumn(fieldId); + int positionInBatch = batch.getPositionInBatch(); + + int batchSize = block.getPositionCount(); + boolean[] isNull = new boolean[batchSize]; + double[] vector = new double[batchSize]; + + for (int i = 0; i < batchSize; i++) { + BytesRefWritable writable = columnBatch.unCheckedGet(i + positionInBatch); + + int length = writable.getLength(); + if (length != 0) { + checkState(length == SIZE_OF_INT, "Float should be 4 bytes"); + + byte[] bytes = writable.getData(); + int start = writable.getStart(); + int intBits = ByteArrays.getInt(bytes, start); + + // the file format uses big endian + vector[i] = (double) Float.intBitsToFloat(Integer.reverseBytes(intBits)); + } + else { + isNull[i] = true; + } + } + + block.setNullVector(isNull); + block.setRawSlice(wrappedDoubleArray(vector)); + + loaded = true; + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } + } + + private static final class LazyDoubleBlockLoader + implements LazyBlockLoader + { + private final RcFileColumnsBatch batch; + private final int fieldId; + private boolean loaded; + + private LazyDoubleBlockLoader(RcFileColumnsBatch batch, int fieldId) + { + this.batch = batch; + this.fieldId = fieldId; + } + + @Override + public void load(LazyFixedWidthBlock block) + { + if (loaded) { + return; + } + + try { + BytesRefArrayWritable columnBatch = batch.getColumn(fieldId); + int positionInBatch = batch.getPositionInBatch(); + + int batchSize = block.getPositionCount(); + boolean[] isNull = new boolean[batchSize]; + double[] vector = new double[batchSize]; + + for (int i = 0; i < batchSize; i++) { + BytesRefWritable writable = columnBatch.unCheckedGet(i + positionInBatch); + + int length = writable.getLength(); + if (length != 0) { + checkState(length == SIZE_OF_LONG, "Double should be 8 bytes"); + + byte[] bytes = writable.getData(); + int start = writable.getStart(); + long longBits = ByteArrays.getLong(bytes, start); + + // the file format uses big endian + vector[i] = Double.longBitsToDouble(Long.reverseBytes(longBits)); + } + else { + isNull[i] = true; + } + } + + block.setNullVector(isNull); + block.setRawSlice(wrappedDoubleArray(vector)); + + loaded = true; + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } + } + + private static final class LazySliceBlockLoader + implements LazyBlockLoader + { + private final RcFileColumnsBatch batch; + private final int fieldId; + private boolean loaded; + + private LazySliceBlockLoader(RcFileColumnsBatch batch, int fieldId) + { + this.batch = batch; + this.fieldId = fieldId; + } + + @Override + public void load(LazySliceArrayBlock block) + { + if (loaded) { + return; + } + + try { + BytesRefArrayWritable columnBatch = batch.getColumn(fieldId); + int positionInBatch = batch.getPositionInBatch(); + + int batchSize = block.getPositionCount(); + Slice[] vector = new Slice[batchSize]; + + for (int i = 0; i < batchSize; i++) { + BytesRefWritable writable = columnBatch.unCheckedGet(i + positionInBatch); + + int length = writable.getLength(); + if (length > 0) { + byte[] bytes = writable.getData(); + int start = writable.getStart(); + if ((length == 1) && bytes[start] == HIVE_EMPTY_STRING_BYTE) { + vector[i] = Slices.EMPTY_SLICE; + } + else { + vector[i] = Slices.wrappedBuffer(Arrays.copyOfRange(bytes, start, start + length)); + } + } + } + + block.setValues(vector); + + loaded = true; + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } + } + + private static final class LazyJsonSliceBlockLoader + implements LazyBlockLoader + { + private final RcFileColumnsBatch batch; + private final int fieldId; + private final ObjectInspector fieldInspector; + private boolean loaded; + + private LazyJsonSliceBlockLoader(RcFileColumnsBatch batch, int fieldId, ObjectInspector fieldInspector) + { + this.batch = batch; + this.fieldId = fieldId; + this.fieldInspector = fieldInspector; + } + + @Override + public void load(LazySliceArrayBlock block) + { + if (loaded) { + return; + } + + try { + BytesRefArrayWritable columnBatch = batch.getColumn(fieldId); + int positionInBatch = batch.getPositionInBatch(); + + int batchSize = block.getPositionCount(); + Slice[] vector = new Slice[batchSize]; + + for (int i = 0; i < batchSize; i++) { + BytesRefWritable writable = columnBatch.unCheckedGet(i + positionInBatch); + + int length = writable.getLength(); + if (length > 0) { + byte[] bytes = writable.getData(); + int start = writable.getStart(); + LazyBinaryObject lazyObject = LazyBinaryFactory.createLazyBinaryObject(fieldInspector); + ByteArrayRef byteArrayRef = new ByteArrayRef(); + byteArrayRef.setData(bytes); + lazyObject.init(byteArrayRef, start, length); + vector[i] = getBlockSlice(lazyObject.getObject(), fieldInspector); + } + } + + block.setValues(vector); + + loaded = true; + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } + } + + // faster version of org.apache.hadoop.io.WritableUtils.readVLong + private static long readVInt(byte[] bytes, int start, int length) + { + long value = 0; + for (int i = 1; i < length; i++) { + value <<= 8; + value |= (bytes[start + i] & 0xFF); + } + return WritableUtils.isNegativeVInt(bytes[start]) ? ~value : value; + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/rcfile/RcFileBlockLoader.java b/presto-hive/src/main/java/com/facebook/presto/hive/rcfile/RcFileBlockLoader.java new file mode 100644 index 00000000..bdfc56d8 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/rcfile/RcFileBlockLoader.java @@ -0,0 +1,28 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive.rcfile; + +import com.facebook.presto.hive.HiveType; +import com.facebook.presto.hive.rcfile.RcFilePageSource.RcFileColumnsBatch; +import com.facebook.presto.spi.block.LazyBlockLoader; +import com.facebook.presto.spi.block.LazyFixedWidthBlock; +import com.facebook.presto.spi.block.LazySliceArrayBlock; +import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector; + +public interface RcFileBlockLoader +{ + LazyBlockLoader fixedWidthBlockLoader(RcFileColumnsBatch batch, int fieldId, HiveType hiveType); + + LazyBlockLoader variableWidthBlockLoader(RcFileColumnsBatch batch, int fieldId, HiveType hiveType, ObjectInspector fieldInspector); +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/rcfile/RcFilePageSource.java b/presto-hive/src/main/java/com/facebook/presto/hive/rcfile/RcFilePageSource.java new file mode 100644 index 00000000..0f02feba --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/rcfile/RcFilePageSource.java @@ -0,0 +1,388 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive.rcfile; + +import com.facebook.presto.hive.HiveColumnHandle; +import com.facebook.presto.hive.HivePartitionKey; +import com.facebook.presto.hive.HiveType; +import com.facebook.presto.hive.HiveUtil; +import com.facebook.presto.spi.ConnectorPageSource; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.block.BlockBuilderStatus; +import com.facebook.presto.spi.block.LazyBlockLoader; +import com.facebook.presto.spi.block.LazyFixedWidthBlock; +import com.facebook.presto.spi.block.LazySliceArrayBlock; +import com.facebook.presto.spi.type.FixedWidthType; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.common.primitives.Ints; +import io.airlift.slice.Slice; +import io.airlift.slice.Slices; +import org.apache.hadoop.hive.ql.io.RCFile; +import org.apache.hadoop.hive.ql.io.RCFile.Reader; +import org.apache.hadoop.hive.serde2.columnar.BytesRefArrayWritable; +import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.StructObjectInspector; +import org.joda.time.DateTimeZone; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import static com.facebook.presto.hive.HiveErrorCode.HIVE_CURSOR_ERROR; +import static com.facebook.presto.hive.HiveUtil.bigintPartitionKey; +import static com.facebook.presto.hive.HiveUtil.booleanPartitionKey; +import static com.facebook.presto.hive.HiveUtil.datePartitionKey; +import static com.facebook.presto.hive.HiveUtil.doublePartitionKey; +import static com.facebook.presto.hive.HiveUtil.getTableObjectInspector; +import static com.facebook.presto.hive.HiveUtil.timestampPartitionKey; +import static com.facebook.presto.spi.StandardErrorCode.NOT_SUPPORTED; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.BooleanType.BOOLEAN; +import static com.facebook.presto.spi.type.DateType.DATE; +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; +import static com.facebook.presto.spi.type.TimestampType.TIMESTAMP; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.Maps.uniqueIndex; +import static java.lang.String.format; +import static java.nio.charset.StandardCharsets.UTF_8; + +public class RcFilePageSource + implements ConnectorPageSource +{ + private static final int MAX_PAGE_SIZE = 1024; + public static final int MAX_FIXED_WIDTH_SIZE = 8; + public static final int NULL_ENTRY_SIZE = 0; + + private final RCFile.Reader recordReader; + private final RcFileBlockLoader blockLoader; + private final long startFilePosition; + private final long endFilePosition; + + private final List columnNames; + private final List types; + private final List hiveTypes; + + private final ObjectInspector[] fieldInspectors; // DON'T USE THESE UNLESS EXTRACTION WILL BE SLOW ANYWAY + + private final Block[] constantBlocks; + private final int[] hiveColumnIndexes; + + private int pageId; + private int currentBatchSize; + private int positionInBatch; + private int currentPageSize; + + private boolean closed; + + private final BytesRefArrayWritable[] columnBatch; + private final boolean[] columnBatchLoaded; + + private long completedBytes; + + public RcFilePageSource( + Reader recordReader, + long offset, + long length, + RcFileBlockLoader blockLoader, + Properties splitSchema, + List partitionKeys, + List columns, + DateTimeZone hiveStorageTimeZone, + TypeManager typeManager) + { + this.recordReader = checkNotNull(recordReader, "recordReader is null"); + this.blockLoader = checkNotNull(blockLoader, "blockLoader is null"); + checkNotNull(splitSchema, "splitSchema is null"); + checkNotNull(partitionKeys, "partitionKeys is null"); + checkNotNull(columns, "columns is null"); + checkNotNull(hiveStorageTimeZone, "hiveStorageTimeZone is null"); + checkNotNull(typeManager, "typeManager is null"); + + // seek to start + try { + if (offset > recordReader.getPosition()) { + recordReader.sync(offset); + } + + this.startFilePosition = recordReader.getPosition(); + } + catch (IOException e) { + throw Throwables.propagate(e); + } + + this.endFilePosition = offset + length; + + Map partitionKeysByName = uniqueIndex(partitionKeys, HivePartitionKey::getName); + + int size = columns.size(); + + this.constantBlocks = new Block[size]; + this.hiveColumnIndexes = new int[size]; + this.fieldInspectors = new ObjectInspector[size]; + + StructObjectInspector rowInspector = getTableObjectInspector(splitSchema); + + ImmutableList.Builder namesBuilder = ImmutableList.builder(); + ImmutableList.Builder typesBuilder = ImmutableList.builder(); + ImmutableList.Builder hiveTypesBuilder = ImmutableList.builder(); + for (int columnIndex = 0; columnIndex < columns.size(); columnIndex++) { + HiveColumnHandle column = columns.get(columnIndex); + + String name = column.getName(); + Type type = typeManager.getType(column.getTypeSignature()); + + namesBuilder.add(name); + typesBuilder.add(type); + hiveTypesBuilder.add(column.getHiveType()); + + hiveColumnIndexes[columnIndex] = column.getHiveColumnIndex(); + + if (!column.isPartitionKey()) { + fieldInspectors[columnIndex] = rowInspector.getStructFieldRef(column.getName()).getFieldObjectInspector(); + } + + if (column.isPartitionKey()) { + HivePartitionKey partitionKey = partitionKeysByName.get(name); + checkArgument(partitionKey != null, "No value provided for partition key %s", name); + + byte[] bytes = partitionKey.getValue().getBytes(UTF_8); + + BlockBuilder blockBuilder = type.createBlockBuilder(new BlockBuilderStatus(), MAX_PAGE_SIZE, Math.max(MAX_FIXED_WIDTH_SIZE, bytes.length)); + + if (HiveUtil.isHiveNull(bytes)) { + for (int i = 0; i < MAX_PAGE_SIZE; i++) { + blockBuilder.appendNull(); + } + } + else if (type.equals(BOOLEAN)) { + boolean value = booleanPartitionKey(partitionKey.getValue(), name); + for (int i = 0; i < MAX_PAGE_SIZE; i++) { + BOOLEAN.writeBoolean(blockBuilder, value); + } + } + else if (type.equals(BIGINT)) { + long value = bigintPartitionKey(partitionKey.getValue(), name); + for (int i = 0; i < MAX_PAGE_SIZE; i++) { + BIGINT.writeLong(blockBuilder, value); + } + } + else if (type.equals(DOUBLE)) { + double value = doublePartitionKey(partitionKey.getValue(), name); + for (int i = 0; i < MAX_PAGE_SIZE; i++) { + DOUBLE.writeDouble(blockBuilder, value); + } + } + else if (type.equals(VARCHAR)) { + Slice value = Slices.wrappedBuffer(bytes); + for (int i = 0; i < MAX_PAGE_SIZE; i++) { + VARCHAR.writeSlice(blockBuilder, value); + } + } + else if (type.equals(DATE)) { + long value = datePartitionKey(partitionKey.getValue(), name); + for (int i = 0; i < MAX_PAGE_SIZE; i++) { + DATE.writeLong(blockBuilder, value); + } + } + else if (TIMESTAMP.equals(type)) { + long value = timestampPartitionKey(partitionKey.getValue(), hiveStorageTimeZone, name); + for (int i = 0; i < MAX_PAGE_SIZE; i++) { + TIMESTAMP.writeLong(blockBuilder, value); + } + } + else { + throw new PrestoException(NOT_SUPPORTED, format("Unsupported column type %s for partition key: %s", type.getDisplayName(), name)); + } + + constantBlocks[columnIndex] = blockBuilder.build(); + } + else if (hiveColumnIndexes[columnIndex] >= recordReader.getCurrentKeyBufferObj().getColumnNumber()) { + // this partition may contain fewer fields than what's declared in the schema + // this happens when additional columns are added to the hive table after a partition has been created + BlockBuilder blockBuilder = type.createBlockBuilder(new BlockBuilderStatus(), MAX_PAGE_SIZE, NULL_ENTRY_SIZE); + for (int i = 0; i < MAX_PAGE_SIZE; i++) { + blockBuilder.appendNull(); + } + constantBlocks[columnIndex] = blockBuilder.build(); + } + } + types = typesBuilder.build(); + hiveTypes = hiveTypesBuilder.build(); + columnNames = namesBuilder.build(); + + columnBatch = new BytesRefArrayWritable[hiveColumnIndexes.length]; + columnBatchLoaded = new boolean[hiveColumnIndexes.length]; + } + + @Override + public long getTotalBytes() + { + return 0; + } + + @Override + public long getCompletedBytes() + { + return completedBytes; + } + + @Override + public long getReadTimeNanos() + { + return 0; + } + + @Override + public boolean isFinished() + { + return closed; + } + + @Override + public Page getNextPage() + { + try { + // advance in the current batch + pageId++; + positionInBatch += currentPageSize; + + // if the batch has been consumed, read the next batch + if (positionInBatch >= currentBatchSize) { + //noinspection deprecation + if (!recordReader.nextColumnsBatch() || recordReader.lastSeenSyncPos() >= endFilePosition) { + close(); + return null; + } + + currentBatchSize = recordReader.getCurrentKeyBufferObj().getNumberRows(); + positionInBatch = 0; + + Arrays.fill(columnBatchLoaded, false); + } + + currentPageSize = Ints.checkedCast(Math.min(currentBatchSize - positionInBatch, MAX_PAGE_SIZE)); + + RcFileColumnsBatch rcFileColumnsBatch = new RcFileColumnsBatch(pageId, positionInBatch); + + Block[] blocks = new Block[hiveColumnIndexes.length]; + for (int fieldId = 0; fieldId < blocks.length; fieldId++) { + Type type = types.get(fieldId); + if (constantBlocks[fieldId] != null) { + blocks[fieldId] = constantBlocks[fieldId].getRegion(0, currentPageSize); + } + else if (type instanceof FixedWidthType) { + LazyBlockLoader loader = blockLoader.fixedWidthBlockLoader(rcFileColumnsBatch, fieldId, hiveTypes.get(fieldId)); + blocks[fieldId] = new LazyFixedWidthBlock(((FixedWidthType) type).getFixedSize(), currentPageSize, loader); + } + else { + LazyBlockLoader loader = blockLoader.variableWidthBlockLoader(rcFileColumnsBatch, + fieldId, + hiveTypes.get(fieldId), + fieldInspectors[fieldId]); + blocks[fieldId] = new LazySliceArrayBlock(currentPageSize, loader); + } + } + + Page page = new Page(currentPageSize, blocks); + completedBytes = recordReader.getPosition() - startFilePosition; + + return page; + } + catch (IOException | RuntimeException e) { + closeWithSuppression(e); + throw new PrestoException(HIVE_CURSOR_ERROR, e); + } + } + + @Override + public void close() + { + // some hive input formats are broken and bad things can happen if you close them multiple times + if (closed) { + return; + } + closed = true; + + recordReader.close(); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("columnNames", columnNames) + .add("types", types) + .toString(); + } + + private void closeWithSuppression(Throwable throwable) + { + checkNotNull(throwable, "throwable is null"); + try { + close(); + } + catch (RuntimeException e) { + throwable.addSuppressed(e); + } + } + + public final class RcFileColumnsBatch + { + private final int expectedBatchId; + private final int positionInBatch; + + private RcFileColumnsBatch(int expectedBatchId, int positionInBatch) + { + this.expectedBatchId = expectedBatchId; + this.positionInBatch = positionInBatch; + } + + public BytesRefArrayWritable getColumn(int fieldId) + throws IOException + { + checkState(pageId == expectedBatchId); + if (!columnBatchLoaded[fieldId]) { + columnBatch[fieldId] = recordReader.getColumn(hiveColumnIndexes[fieldId], columnBatch[fieldId]); + columnBatchLoaded[fieldId] = true; + } + return columnBatch[fieldId]; + } + + public int getPositionInBatch() + { + return positionInBatch; + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("expectedBatchId", expectedBatchId) + .add("positionInBatch", positionInBatch) + .toString(); + } + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/rcfile/RcFilePageSourceFactory.java b/presto-hive/src/main/java/com/facebook/presto/hive/rcfile/RcFilePageSourceFactory.java new file mode 100644 index 00000000..5b0f25e1 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/rcfile/RcFilePageSourceFactory.java @@ -0,0 +1,147 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive.rcfile; + +import com.facebook.presto.hive.HiveClientConfig; +import com.facebook.presto.hive.HiveColumnHandle; +import com.facebook.presto.hive.HivePageSourceFactory; +import com.facebook.presto.hive.HivePartitionKey; +import com.facebook.presto.spi.ConnectorPageSource; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.TupleDomain; +import com.facebook.presto.spi.type.TypeManager; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.hive.ql.io.RCFile; +import org.apache.hadoop.hive.serde2.columnar.ColumnarSerDe; +import org.apache.hadoop.hive.serde2.columnar.LazyBinaryColumnarSerDe; +import org.joda.time.DateTimeZone; + +import javax.inject.Inject; + +import java.util.List; +import java.util.Optional; +import java.util.Properties; + +import static com.facebook.presto.hive.HiveSessionProperties.isOptimizedReaderEnabled; +import static com.facebook.presto.hive.HiveUtil.getDeserializerClassName; +import static com.facebook.presto.hive.HiveUtil.setReadColumns; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Predicates.not; +import static com.google.common.collect.Iterables.filter; +import static com.google.common.collect.Lists.transform; + +public class RcFilePageSourceFactory + implements HivePageSourceFactory +{ + private final TypeManager typeManager; + private final boolean enabled; + + @Inject + public RcFilePageSourceFactory(TypeManager typeManager, HiveClientConfig config) + { + //noinspection deprecation + this(typeManager, config.isOptimizedReaderEnabled()); + } + + public RcFilePageSourceFactory(TypeManager typeManager) + { + this(typeManager, true); + } + + public RcFilePageSourceFactory(TypeManager typeManager, boolean enabled) + { + this.typeManager = checkNotNull(typeManager, "typeManager is null"); + this.enabled = enabled; + } + + @Override + public Optional createPageSource( + Configuration configuration, + ConnectorSession session, + Path path, + long start, + long length, + Properties schema, + List columns, + List partitionKeys, + TupleDomain effectivePredicate, + DateTimeZone hiveStorageTimeZone) + { + // todo remove this when GC issues are resolved + if (true || !isOptimizedReaderEnabled(session, enabled)) { + return Optional.empty(); + } + + String deserializerClassName = getDeserializerClassName(schema); + + RcFileBlockLoader blockLoader; + if (deserializerClassName.equals(LazyBinaryColumnarSerDe.class.getName())) { + blockLoader = new RcBinaryBlockLoader(); + } + else if (deserializerClassName.equals(ColumnarSerDe.class.getName())) { + blockLoader = new RcTextBlockLoader(hiveStorageTimeZone); + } + else { + return Optional.empty(); + } + + // determine which hive columns we will read + List readColumns = ImmutableList.copyOf(filter(columns, not(HiveColumnHandle::isPartitionKey))); + List readHiveColumnIndexes = ImmutableList.copyOf(transform(readColumns, HiveColumnHandle::getHiveColumnIndex)); + + // Tell hive the columns we would like to read, this lets hive optimize reading column oriented files + setReadColumns(configuration, readHiveColumnIndexes); + + // propagate serialization configuration to getRecordReader + for (String name : schema.stringPropertyNames()) { + if (name.startsWith("serialization.")) { + configuration.set(name, schema.getProperty(name)); + } + } + + RCFile.Reader recordReader; + try { + FileSystem fileSystem = path.getFileSystem(configuration); + recordReader = new RCFile.Reader(fileSystem, path, configuration); + } + catch (Exception e) { + throw Throwables.propagate(e); + } + + try { + return Optional.of(new RcFilePageSource( + recordReader, + start, + length, + blockLoader, + schema, + partitionKeys, + columns, + hiveStorageTimeZone, + typeManager)); + } + catch (Exception e) { + try { + recordReader.close(); + } + catch (Exception ignored) { + } + throw Throwables.propagate(e); + } + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/rcfile/RcTextBlockLoader.java b/presto-hive/src/main/java/com/facebook/presto/hive/rcfile/RcTextBlockLoader.java new file mode 100644 index 00000000..1c384567 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/rcfile/RcTextBlockLoader.java @@ -0,0 +1,534 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive.rcfile; + +import com.facebook.presto.hive.HiveType; +import com.facebook.presto.hive.rcfile.RcFilePageSource.RcFileColumnsBatch; +import com.facebook.presto.spi.block.LazyBlockLoader; +import com.facebook.presto.spi.block.LazyFixedWidthBlock; +import com.facebook.presto.spi.block.LazySliceArrayBlock; +import com.google.common.base.Throwables; +import io.airlift.slice.Slice; +import io.airlift.slice.Slices; +import org.apache.hadoop.hive.serde2.columnar.BytesRefArrayWritable; +import org.apache.hadoop.hive.serde2.columnar.BytesRefWritable; +import org.apache.hadoop.hive.serde2.lazy.ByteArrayRef; +import org.apache.hadoop.hive.serde2.lazy.LazyFactory; +import org.apache.hadoop.hive.serde2.lazy.LazyObject; +import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector; +import org.joda.time.DateTimeZone; + +import java.io.IOException; +import java.util.Arrays; + +import static com.facebook.presto.hive.HiveBooleanParser.isFalse; +import static com.facebook.presto.hive.HiveBooleanParser.isTrue; +import static com.facebook.presto.hive.HiveType.HIVE_BINARY; +import static com.facebook.presto.hive.HiveType.HIVE_BOOLEAN; +import static com.facebook.presto.hive.HiveType.HIVE_BYTE; +import static com.facebook.presto.hive.HiveType.HIVE_DATE; +import static com.facebook.presto.hive.HiveType.HIVE_DOUBLE; +import static com.facebook.presto.hive.HiveType.HIVE_FLOAT; +import static com.facebook.presto.hive.HiveType.HIVE_INT; +import static com.facebook.presto.hive.HiveType.HIVE_LONG; +import static com.facebook.presto.hive.HiveType.HIVE_SHORT; +import static com.facebook.presto.hive.HiveType.HIVE_STRING; +import static com.facebook.presto.hive.HiveType.HIVE_TIMESTAMP; +import static com.facebook.presto.hive.HiveUtil.base64Decode; +import static com.facebook.presto.hive.HiveUtil.isStructuralType; +import static com.facebook.presto.hive.HiveUtil.parseHiveDate; +import static com.facebook.presto.hive.HiveUtil.parseHiveTimestamp; +import static com.facebook.presto.hive.NumberParser.parseDouble; +import static com.facebook.presto.hive.NumberParser.parseLong; +import static com.facebook.presto.hive.util.SerDeUtils.getBlockSlice; +import static io.airlift.slice.Slices.wrappedBooleanArray; +import static io.airlift.slice.Slices.wrappedDoubleArray; +import static io.airlift.slice.Slices.wrappedLongArray; + +public class RcTextBlockLoader + implements RcFileBlockLoader +{ + private final DateTimeZone hiveStorageTimeZone; + + public RcTextBlockLoader(DateTimeZone hiveStorageTimeZone) + { + this.hiveStorageTimeZone = hiveStorageTimeZone; + } + + @Override + public LazyBlockLoader fixedWidthBlockLoader(RcFileColumnsBatch batch, int fieldId, HiveType hiveType) + { + if (HIVE_BOOLEAN.equals(hiveType)) { + return new LazyBooleanBlockLoader(batch, fieldId); + } + if (HIVE_BYTE.equals(hiveType) || HIVE_SHORT.equals(hiveType) || HIVE_INT.equals(hiveType) || HIVE_LONG.equals(hiveType)) { + return new LazyLongBlockLoader(batch, fieldId); + } + if (HIVE_DATE.equals(hiveType)) { + return new LazyDateBlockLoader(batch, fieldId); + } + if (HIVE_TIMESTAMP.equals(hiveType)) { + return new LazyTimestampBlockLoader(batch, fieldId, hiveStorageTimeZone); + } + if (HIVE_FLOAT.equals(hiveType) || HIVE_DOUBLE.equals(hiveType)) { + return new LazyDoubleBlockLoader(batch, fieldId); + } + throw new UnsupportedOperationException("Unsupported column type: " + hiveType); + } + + @Override + public LazyBlockLoader variableWidthBlockLoader(RcFileColumnsBatch batch, int fieldId, HiveType hiveType, ObjectInspector fieldInspector) + { + if (HIVE_STRING.equals(hiveType)) { + return new LazyStringBlockLoader(batch, fieldId); + } + if (HIVE_BINARY.equals(hiveType)) { + return new LazyBinaryBlockLoader(batch, fieldId); + } + if (isStructuralType(hiveType)) { + return new LazyJsonSliceBlockLoader(batch, fieldId, fieldInspector); + } + throw new UnsupportedOperationException("Unsupported column type: " + hiveType); + } + + private static boolean isNull(byte[] bytes, int start, int length) + { + return length == "\\N".length() && bytes[start] == '\\' && bytes[start + 1] == 'N'; + } + + private static final class LazyBooleanBlockLoader + implements LazyBlockLoader + { + private final RcFileColumnsBatch batch; + private final int fieldId; + private boolean loaded; + + public LazyBooleanBlockLoader(RcFileColumnsBatch batch, int fieldId) + { + this.batch = batch; + this.fieldId = fieldId; + } + + @Override + public void load(LazyFixedWidthBlock block) + { + if (loaded) { + return; + } + + try { + BytesRefArrayWritable columnBatch = batch.getColumn(fieldId); + int positionInBatch = batch.getPositionInBatch(); + + int positionCount = block.getPositionCount(); + boolean[] isNull = new boolean[positionCount]; + boolean[] vector = new boolean[positionCount]; + + for (int i = 0; i < positionCount; i++) { + BytesRefWritable writable = columnBatch.unCheckedGet(i + positionInBatch); + + byte[] bytes = writable.getData(); + int start = writable.getStart(); + int length = writable.getLength(); + if (isTrue(bytes, start, length)) { + vector[i] = true; + } + else if (isFalse(bytes, start, length)) { + vector[i] = false; + } + else { + isNull[i] = true; + } + } + + block.setNullVector(isNull); + block.setRawSlice(wrappedBooleanArray(vector, 0, positionCount)); + + loaded = true; + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } + } + + private static final class LazyLongBlockLoader + implements LazyBlockLoader + { + private final RcFileColumnsBatch batch; + private final int fieldId; + private boolean loaded; + + private LazyLongBlockLoader(RcFileColumnsBatch batch, int fieldId) + { + this.batch = batch; + this.fieldId = fieldId; + } + + @Override + public void load(LazyFixedWidthBlock block) + { + if (loaded) { + return; + } + + try { + BytesRefArrayWritable columnBatch = batch.getColumn(fieldId); + int positionInBatch = batch.getPositionInBatch(); + + int batchSize = block.getPositionCount(); + boolean[] isNull = new boolean[batchSize]; + long[] vector = new long[batchSize]; + + for (int i = 0; i < batchSize; i++) { + BytesRefWritable writable = columnBatch.unCheckedGet(i + positionInBatch); + + byte[] bytes = writable.getData(); + int start = writable.getStart(); + int length = writable.getLength(); + if (length == 0 || isNull(bytes, start, length)) { + isNull[i] = true; + } + else { + vector[i] = parseLong(bytes, start, length); + } + } + + block.setNullVector(isNull); + block.setRawSlice(wrappedLongArray(vector)); + + loaded = true; + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } + } + + private static final class LazyDateBlockLoader + implements LazyBlockLoader + { + private final RcFileColumnsBatch batch; + private final int fieldId; + private boolean loaded; + + private LazyDateBlockLoader(RcFileColumnsBatch batch, int fieldId) + { + this.batch = batch; + this.fieldId = fieldId; + } + + @Override + public void load(LazyFixedWidthBlock block) + { + if (loaded) { + return; + } + + try { + BytesRefArrayWritable columnBatch = batch.getColumn(fieldId); + int positionInBatch = batch.getPositionInBatch(); + + int positionCount = block.getPositionCount(); + boolean[] isNull = new boolean[positionCount]; + long[] vector = new long[positionCount]; + + for (int i = 0; i < positionCount; i++) { + BytesRefWritable writable = columnBatch.unCheckedGet(i + positionInBatch); + + byte[] bytes = writable.getData(); + int start = writable.getStart(); + int length = writable.getLength(); + if (length == 0 || isNull(bytes, start, length)) { + isNull[i] = true; + } + else { + String value = new String(bytes, start, length); + vector[i] = parseHiveDate(value); + } + } + + block.setNullVector(isNull); + block.setRawSlice(wrappedLongArray(vector)); + + loaded = true; + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } + + } + + private static final class LazyTimestampBlockLoader + implements LazyBlockLoader + { + private final RcFileColumnsBatch batch; + private final int fieldId; + private final DateTimeZone hiveStorageTimeZone; + private boolean loaded; + + private LazyTimestampBlockLoader(RcFileColumnsBatch batch, int fieldId, DateTimeZone hiveStorageTimeZone) + { + this.batch = batch; + this.fieldId = fieldId; + this.hiveStorageTimeZone = hiveStorageTimeZone; + } + + @Override + public void load(LazyFixedWidthBlock block) + { + if (loaded) { + return; + } + + try { + BytesRefArrayWritable columnBatch = batch.getColumn(fieldId); + int positionInBatch = batch.getPositionInBatch(); + + int batchSize = block.getPositionCount(); + boolean[] isNull = new boolean[batchSize]; + long[] vector = new long[batchSize]; + + for (int i = 0; i < batchSize; i++) { + BytesRefWritable writable = columnBatch.unCheckedGet(i + positionInBatch); + + byte[] bytes = writable.getData(); + int start = writable.getStart(); + int length = writable.getLength(); + if (length == 0 || isNull(bytes, start, length)) { + isNull[i] = true; + } + else { + String value = new String(bytes, start, length); + vector[i] = parseHiveTimestamp(value, hiveStorageTimeZone); + } + } + + block.setNullVector(isNull); + block.setRawSlice(wrappedLongArray(vector)); + + loaded = true; + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } + } + + private static final class LazyDoubleBlockLoader + implements LazyBlockLoader + { + private final RcFileColumnsBatch batch; + private final int fieldId; + private boolean loaded; + + private LazyDoubleBlockLoader(RcFileColumnsBatch batch, int fieldId) + { + this.batch = batch; + this.fieldId = fieldId; + } + + @Override + public void load(LazyFixedWidthBlock block) + { + if (loaded) { + return; + } + + try { + BytesRefArrayWritable columnBatch = batch.getColumn(fieldId); + int positionInBatch = batch.getPositionInBatch(); + + int batchSize = block.getPositionCount(); + boolean[] isNull = new boolean[batchSize]; + double[] vector = new double[batchSize]; + + for (int i = 0; i < batchSize; i++) { + BytesRefWritable writable = columnBatch.unCheckedGet(i + positionInBatch); + + byte[] bytes = writable.getData(); + int start = writable.getStart(); + int length = writable.getLength(); + if (length == 0 || isNull(bytes, start, length)) { + isNull[i] = true; + } + else { + vector[i] = parseDouble(bytes, start, length); + } + } + + block.setNullVector(isNull); + block.setRawSlice(wrappedDoubleArray(vector)); + + loaded = true; + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } + } + + private static final class LazyStringBlockLoader + implements LazyBlockLoader + { + private final RcFileColumnsBatch batch; + private final int fieldId; + private boolean loaded; + + private LazyStringBlockLoader(RcFileColumnsBatch batch, int fieldId) + { + this.batch = batch; + this.fieldId = fieldId; + } + + @Override + public void load(LazySliceArrayBlock block) + { + if (loaded) { + return; + } + + try { + BytesRefArrayWritable columnBatch = batch.getColumn(fieldId); + int positionInBatch = batch.getPositionInBatch(); + + int batchSize = block.getPositionCount(); + Slice[] vector = new Slice[batchSize]; + + for (int i = 0; i < batchSize; i++) { + BytesRefWritable writable = columnBatch.unCheckedGet(i + positionInBatch); + + byte[] bytes = writable.getData(); + int start = writable.getStart(); + int length = writable.getLength(); + if (!isNull(bytes, start, length)) { + vector[i] = Slices.wrappedBuffer(Arrays.copyOfRange(bytes, start, start + length)); + } + } + + block.setValues(vector); + + loaded = true; + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } + } + + private static final class LazyBinaryBlockLoader + implements LazyBlockLoader + { + private final RcFileColumnsBatch batch; + private final int fieldId; + private boolean loaded; + + private LazyBinaryBlockLoader(RcFileColumnsBatch batch, int fieldId) + { + this.batch = batch; + this.fieldId = fieldId; + } + + @Override + public void load(LazySliceArrayBlock block) + { + if (loaded) { + return; + } + + try { + BytesRefArrayWritable columnBatch = batch.getColumn(fieldId); + int positionInBatch = batch.getPositionInBatch(); + + int batchSize = block.getPositionCount(); + Slice[] vector = new Slice[batchSize]; + + for (int i = 0; i < batchSize; i++) { + BytesRefWritable writable = columnBatch.unCheckedGet(i + positionInBatch); + + byte[] bytes = writable.getData(); + int start = writable.getStart(); + int length = writable.getLength(); + if (!isNull(bytes, start, length)) { + // yes we end up with an extra copy here because the Base64 only handles whole arrays + byte[] data = Arrays.copyOfRange(bytes, start, start + length); + vector[i] = base64Decode(data); + } + } + + block.setValues(vector); + + loaded = true; + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } + } + + private static final class LazyJsonSliceBlockLoader + implements LazyBlockLoader + { + private final RcFileColumnsBatch batch; + private final int fieldId; + private final ObjectInspector fieldInspector; + private boolean loaded; + + private LazyJsonSliceBlockLoader(RcFileColumnsBatch batch, int fieldId, ObjectInspector fieldInspector) + { + this.batch = batch; + this.fieldId = fieldId; + this.fieldInspector = fieldInspector; + } + + @Override + public void load(LazySliceArrayBlock block) + { + if (loaded) { + return; + } + + try { + BytesRefArrayWritable columnBatch = batch.getColumn(fieldId); + int positionInBatch = batch.getPositionInBatch(); + + int batchSize = block.getPositionCount(); + Slice[] vector = new Slice[batchSize]; + + for (int i = 0; i < batchSize; i++) { + BytesRefWritable writable = columnBatch.unCheckedGet(i + positionInBatch); + + byte[] bytes = writable.getData(); + int start = writable.getStart(); + int length = writable.getLength(); + if (!isNull(bytes, start, length)) { + LazyObject lazyObject = LazyFactory.createLazyObject(fieldInspector); + ByteArrayRef byteArrayRef = new ByteArrayRef(); + byteArrayRef.setData(bytes); + lazyObject.init(byteArrayRef, start, length); + vector[i] = getBlockSlice(lazyObject.getObject(), fieldInspector); + } + } + + block.setValues(vector); + + loaded = true; + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/util/AsyncQueue.java b/presto-hive/src/main/java/com/facebook/presto/hive/util/AsyncQueue.java new file mode 100644 index 00000000..20d563d0 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/util/AsyncQueue.java @@ -0,0 +1,115 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive.util; + +import com.google.common.collect.ImmutableList; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import static java.util.Objects.requireNonNull; + +/** + * A simple unbounded queue that can fetch batches of elements asynchronously. + *

+ * This class is designed for multiple writers and multiple readers. + */ +public class AsyncQueue +{ + private final LinkedBlockingQueue queue = new LinkedBlockingQueue<>(); + private final AtomicBoolean finished = new AtomicBoolean(); + private final AtomicReference> notEmptyFuture = new AtomicReference<>(new CompletableFuture<>()); + + /** + * Adds an element to the queue. Elements added after the queue is finished + * are normally ignored (without error), but if someone calls finish while + * {@code add} is running the element may still be added. + * + * @throws NullPointerException if the element is null + */ + public void add(T element) + { + requireNonNull(element, "element is null"); + if (finished.get()) { + return; + } + queue.add(element); + notEmptyFuture.get().complete(null); + } + + /** + * Gets a batch of elements from the queue. If queue is not empty, a + * completed future containing the queued elements is returned. If the + * queue is empty, an incomplete future is returned that completes when + * an element is added. + *

+ * It is possible that an empty list will be returned as another reader + * may have consumed all of the data. A caller should read until, the + * {@code isFinished} method returns {@code true}. + * + * @param maxSize the maximum number of elements to return. + */ + public CompletableFuture> getBatchAsync(int maxSize) + { + return notEmptyFuture.get().thenApply(x -> getBatch(maxSize)); + } + + private List getBatch(int maxSize) + { + // take up to maxSize elements from the queue + List elements = new ArrayList<>(maxSize); + queue.drainTo(elements, maxSize); + + // if the queue is empty and the current future is finished, create a new one so + // a new readers can be notified when the queue has elements to read + if (queue.isEmpty() && !finished.get()) { + CompletableFuture future = notEmptyFuture.get(); + if (future.isDone()) { + notEmptyFuture.compareAndSet(future, new CompletableFuture<>()); + } + } + + // if someone added an element while we were updating the state, complete the current future + if (!queue.isEmpty() || finished.get()) { + notEmptyFuture.get().complete(null); + } + + return ImmutableList.copyOf((Collection) elements); + } + + /** + * Finishes the queue. Elements added after the queue is finished + * are ignored without error. + */ + public void finish() + { + finished.set(true); + if (queue.isEmpty()) { + notEmptyFuture.get().complete(null); + } + } + + /** + * Is the queue finished and no more elements can be fetched? + */ + public boolean isFinished() + { + return finished.get() && queue.isEmpty(); + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/util/HiveFileIterator.java b/presto-hive/src/main/java/com/facebook/presto/hive/util/HiveFileIterator.java new file mode 100644 index 00000000..db84237a --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/util/HiveFileIterator.java @@ -0,0 +1,167 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive.util; + +import com.facebook.presto.hive.DirectoryLister; +import com.facebook.presto.hive.HiveColumnHandle; +import com.facebook.presto.hive.HivePartitionKey; +import com.facebook.presto.hive.NamenodeStats; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.TupleDomain; +import com.google.common.collect.AbstractIterator; +import io.airlift.stats.TimeStat; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.LocatedFileStatus; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.fs.RemoteIterator; +import org.apache.hadoop.mapred.InputFormat; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.List; +import java.util.Properties; + +import static com.facebook.presto.hive.HiveErrorCode.HIVE_FILESYSTEM_ERROR; +import static com.facebook.presto.hive.HiveErrorCode.HIVE_FILE_NOT_FOUND; +import static com.google.common.base.Preconditions.checkNotNull; + +public class HiveFileIterator + extends AbstractIterator +{ + private final FileSystem fileSystem; + private final DirectoryLister directoryLister; + private final NamenodeStats namenodeStats; + private final Path path; + private final String partitionName; + private final InputFormat inputFormat; + private final Properties schema; + private final List partitionKeys; + private final TupleDomain effectivePredicate; + + private RemoteIterator remoteIterator; + + public HiveFileIterator( + Path path, + FileSystem fileSystem, + DirectoryLister directoryLister, + NamenodeStats namenodeStats, + String partitionName, + InputFormat inputFormat, + Properties schema, + List partitionKeys, + TupleDomain effectivePredicate) + { + this.partitionName = checkNotNull(partitionName, "partitionName is null"); + this.inputFormat = checkNotNull(inputFormat, "inputFormat is null"); + this.schema = checkNotNull(schema, "schema is null"); + this.partitionKeys = checkNotNull(partitionKeys, "partitionKeys is null"); + this.effectivePredicate = checkNotNull(effectivePredicate, "effectivePredicate is null"); + this.path = checkNotNull(path, "path is null"); + this.fileSystem = checkNotNull(fileSystem, "fileSystem is null"); + this.directoryLister = checkNotNull(directoryLister, "directoryLister is null"); + this.namenodeStats = checkNotNull(namenodeStats, "namenodeStats is null"); + } + + @Override + protected LocatedFileStatus computeNext() + { + try { + if (remoteIterator == null) { + remoteIterator = getLocatedFileStatusRemoteIterator(path); + } + + while (remoteIterator.hasNext()) { + LocatedFileStatus status = getLocatedFileStatus(remoteIterator); + + // ignore hidden files. Hive ignores files starting with _ and . as well. + String fileName = status.getPath().getName(); + if (fileName.startsWith("_") || fileName.startsWith(".")) { + continue; + } + return status; + } + return endOfData(); + } + catch (FileNotFoundException e) { + throw new PrestoException(HIVE_FILE_NOT_FOUND, "Partition location does not exist: " + path); + } + catch (IOException e) { + throw new PrestoException(HIVE_FILESYSTEM_ERROR, e); + } + } + + private RemoteIterator getLocatedFileStatusRemoteIterator(Path path) + throws IOException + { + try (TimeStat.BlockTimer ignored = namenodeStats.getListLocatedStatus().time()) { + return directoryLister.list(fileSystem, path); + } + catch (IOException | RuntimeException e) { + namenodeStats.getListLocatedStatus().recordException(e); + throw e; + } + } + + private LocatedFileStatus getLocatedFileStatus(RemoteIterator iterator) + throws IOException + { + try (TimeStat.BlockTimer ignored = namenodeStats.getRemoteIteratorNext().time()) { + return iterator.next(); + } + catch (IOException | RuntimeException e) { + namenodeStats.getRemoteIteratorNext().recordException(e); + throw e; + } + } + + public FileSystem getFileSystem() + { + return fileSystem; + } + + public DirectoryLister getDirectoryLister() + { + return directoryLister; + } + + public NamenodeStats getNamenodeStats() + { + return namenodeStats; + } + + public String getPartitionName() + { + return partitionName; + } + + public InputFormat getInputFormat() + { + return inputFormat; + } + + public Properties getSchema() + { + return schema; + } + + public List getPartitionKeys() + { + return partitionKeys; + } + + public TupleDomain getEffectivePredicate() + { + return effectivePredicate; + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/util/SerDeUtils.java b/presto-hive/src/main/java/com/facebook/presto/hive/util/SerDeUtils.java new file mode 100644 index 00000000..9b0eb315 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/util/SerDeUtils.java @@ -0,0 +1,246 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive.util; + +import com.facebook.presto.spi.block.BlockBuilderStatus; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.BigintType; +import com.facebook.presto.spi.type.BooleanType; +import com.facebook.presto.spi.type.DateType; +import com.facebook.presto.spi.type.DoubleType; +import com.facebook.presto.spi.type.TimestampType; +import com.facebook.presto.spi.type.VarcharType; +import com.google.common.annotations.VisibleForTesting; +import io.airlift.slice.DynamicSliceOutput; +import io.airlift.slice.Slice; +import io.airlift.slice.SliceOutput; +import io.airlift.slice.Slices; +import org.apache.hadoop.hive.serde2.io.DateWritable; +import org.apache.hadoop.hive.serde2.io.TimestampWritable; +import org.apache.hadoop.hive.serde2.lazy.LazyDate; +import org.apache.hadoop.hive.serde2.objectinspector.ListObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.MapObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.PrimitiveObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.StructField; +import org.apache.hadoop.hive.serde2.objectinspector.StructObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.primitive.BinaryObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.primitive.BooleanObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.primitive.ByteObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.primitive.DateObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.primitive.DoubleObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.primitive.FloatObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.primitive.IntObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.primitive.LongObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.primitive.ShortObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.primitive.StringObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.primitive.TimestampObjectInspector; +import org.joda.time.DateTimeZone; + +import java.sql.Timestamp; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static com.facebook.presto.spi.type.VarbinaryType.VARBINARY; +import static com.google.common.base.Preconditions.checkNotNull; + +public final class SerDeUtils +{ + private SerDeUtils() {} + + public static Slice getBlockSlice(Object object, ObjectInspector objectInspector) + { + return checkNotNull(serializeObject(null, object, objectInspector), "serialized result is null"); + } + + @VisibleForTesting + static Slice serializeObject(BlockBuilder builder, Object object, ObjectInspector inspector) + { + switch (inspector.getCategory()) { + case PRIMITIVE: + serializePrimitive(builder, object, (PrimitiveObjectInspector) inspector); + return null; + case LIST: + return serializeList(builder, object, (ListObjectInspector) inspector); + case MAP: + return serializeMap(builder, object, (MapObjectInspector) inspector); + case STRUCT: + return serializeStruct(builder, object, (StructObjectInspector) inspector); + } + throw new RuntimeException("Unknown object inspector category: " + inspector.getCategory()); + } + + private static void serializePrimitive(BlockBuilder builder, Object object, PrimitiveObjectInspector inspector) + { + checkNotNull(builder, "parent builder is null"); + + if (object == null) { + builder.appendNull(); + return; + } + + switch (inspector.getPrimitiveCategory()) { + case BOOLEAN: + BooleanType.BOOLEAN.writeBoolean(builder, ((BooleanObjectInspector) inspector).get(object)); + return; + case BYTE: + BigintType.BIGINT.writeLong(builder, ((ByteObjectInspector) inspector).get(object)); + return; + case SHORT: + BigintType.BIGINT.writeLong(builder, ((ShortObjectInspector) inspector).get(object)); + return; + case INT: + BigintType.BIGINT.writeLong(builder, ((IntObjectInspector) inspector).get(object)); + return; + case LONG: + BigintType.BIGINT.writeLong(builder, ((LongObjectInspector) inspector).get(object)); + return; + case FLOAT: + DoubleType.DOUBLE.writeDouble(builder, ((FloatObjectInspector) inspector).get(object)); + return; + case DOUBLE: + DoubleType.DOUBLE.writeDouble(builder, ((DoubleObjectInspector) inspector).get(object)); + return; + case STRING: + VarcharType.VARCHAR.writeSlice(builder, Slices.utf8Slice(((StringObjectInspector) inspector).getPrimitiveJavaObject(object))); + return; + case DATE: + DateType.DATE.writeLong(builder, formatDateAsLong(object, (DateObjectInspector) inspector)); + return; + case TIMESTAMP: + TimestampType.TIMESTAMP.writeLong(builder, formatTimestampAsLong(object, (TimestampObjectInspector) inspector)); + return; + case BINARY: + VARBINARY.writeSlice(builder, Slices.wrappedBuffer(((BinaryObjectInspector) inspector).getPrimitiveJavaObject(object))); + return; + } + throw new RuntimeException("Unknown primitive type: " + inspector.getPrimitiveCategory()); + } + + private static Slice serializeList(BlockBuilder builder, Object object, ListObjectInspector inspector) + { + List list = inspector.getList(object); + if (list == null) { + checkNotNull(builder, "parent builder is null").appendNull(); + return null; + } + + ObjectInspector elementInspector = inspector.getListElementObjectInspector(); + BlockBuilder currentBuilder = createBlockBuilder(); + + for (Object element : list) { + serializeObject(currentBuilder, element, elementInspector); + } + + SliceOutput out = new DynamicSliceOutput(1024); + currentBuilder.getEncoding().writeBlock(out, currentBuilder.build()); + + if (builder != null) { + VARBINARY.writeSlice(builder, out.slice()); + } + + return out.slice(); + } + + private static Slice serializeMap(BlockBuilder builder, Object object, MapObjectInspector inspector) + { + Map map = inspector.getMap(object); + if (map == null) { + checkNotNull(builder, "parent builder is null").appendNull(); + return null; + } + + ObjectInspector keyInspector = inspector.getMapKeyObjectInspector(); + ObjectInspector valueInspector = inspector.getMapValueObjectInspector(); + BlockBuilder currentBuilder = createBlockBuilder(); + + for (Map.Entry entry : map.entrySet()) { + // Hive skips map entries with null keys + if (entry.getKey() != null) { + serializeObject(currentBuilder, entry.getKey(), keyInspector); + serializeObject(currentBuilder, entry.getValue(), valueInspector); + } + } + + SliceOutput out = new DynamicSliceOutput(1024); + currentBuilder.getEncoding().writeBlock(out, currentBuilder.build()); + + if (builder != null) { + VARBINARY.writeSlice(builder, out.slice()); + } + + return out.slice(); + } + + private static Slice serializeStruct(BlockBuilder builder, Object object, StructObjectInspector inspector) + { + if (object == null) { + checkNotNull(builder, "parent builder is null").appendNull(); + return null; + } + + BlockBuilder currentBuilder = createBlockBuilder(); + for (StructField field : inspector.getAllStructFieldRefs()) { + serializeObject(currentBuilder, inspector.getStructFieldData(object, field), field.getFieldObjectInspector()); + } + + SliceOutput out = new DynamicSliceOutput(1024); + currentBuilder.getEncoding().writeBlock(out, currentBuilder.build()); + + if (builder != null) { + VARBINARY.writeSlice(builder, out.slice()); + } + + return out.slice(); + } + + private static BlockBuilder createBlockBuilder() + { + // default BlockBuilderStatus could cause OOM at unit tests + return VARBINARY.createBlockBuilder(new BlockBuilderStatus(), 100); + } + + private static long formatDateAsLong(Object object, DateObjectInspector inspector) + { + if (object instanceof LazyDate) { + return ((LazyDate) object).getWritableObject().getDays(); + } + if (object instanceof DateWritable) { + return ((DateWritable) object).getDays(); + } + + // Hive will return java.sql.Date at midnight in JVM time zone + long millisLocal = inspector.getPrimitiveJavaObject(object).getTime(); + // Convert it to midnight in UTC + long millisUtc = DateTimeZone.getDefault().getMillisKeepLocal(DateTimeZone.UTC, millisLocal); + // Convert midnight UTC to days + return TimeUnit.MILLISECONDS.toDays(millisUtc); + } + + private static long formatTimestampAsLong(Object object, TimestampObjectInspector inspector) + { + Timestamp timestamp = getTimestamp(object, inspector); + return timestamp.getTime(); + } + + private static Timestamp getTimestamp(Object object, TimestampObjectInspector inspector) + { + // handle broken ObjectInspectors + if (object instanceof TimestampWritable) { + return ((TimestampWritable) object).getTimestamp(); + } + return inspector.getPrimitiveJavaObject(object); + } +} diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/util/Types.java b/presto-hive/src/main/java/com/facebook/presto/hive/util/Types.java new file mode 100644 index 00000000..39732749 --- /dev/null +++ b/presto-hive/src/main/java/com/facebook/presto/hive/util/Types.java @@ -0,0 +1,49 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive.util; + +import com.facebook.presto.spi.ErrorCodeSupplier; +import com.facebook.presto.spi.PrestoException; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public final class Types +{ + private Types() {} + + public static B checkType(A value, Class target, String name) + { + checkNotNull(value, "%s is null", name); + checkArgument(target.isInstance(value), + "%s must be of type %s, not %s", + name, + target.getName(), + value.getClass().getName()); + return target.cast(value); + } + + public static B checkType(A value, Class target, ErrorCodeSupplier errorCode, String name) + { + checkNotNull(value, "%s is null", name); + if (!target.isInstance(value)) { + throw new PrestoException(errorCode, String.format( + "%s must be of type %s, not %s", + name, + target.getName(), + value.getClass().getName())); + } + return target.cast(value); + } +} diff --git a/presto-hive/src/test/java/com/facebook/presto/hive/AbstractTestHiveClient.java b/presto-hive/src/test/java/com/facebook/presto/hive/AbstractTestHiveClient.java new file mode 100644 index 00000000..68e81e22 --- /dev/null +++ b/presto-hive/src/test/java/com/facebook/presto/hive/AbstractTestHiveClient.java @@ -0,0 +1,1854 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.hive.metastore.CachingHiveMetastore; +import com.facebook.presto.hive.metastore.HiveMetastore; +import com.facebook.presto.hive.orc.DwrfHiveRecordCursor; +import com.facebook.presto.hive.orc.DwrfRecordCursorProvider; +import com.facebook.presto.hive.orc.OrcHiveRecordCursor; +import com.facebook.presto.hive.orc.OrcPageSource; +import com.facebook.presto.hive.orc.OrcRecordCursorProvider; +import com.facebook.presto.hive.rcfile.RcFilePageSource; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ColumnMetadata; +import com.facebook.presto.spi.ConnectorMetadata; +import com.facebook.presto.spi.ConnectorOutputTableHandle; +import com.facebook.presto.spi.ConnectorPageSource; +import com.facebook.presto.spi.ConnectorPageSourceProvider; +import com.facebook.presto.spi.ConnectorPartition; +import com.facebook.presto.spi.ConnectorPartitionResult; +import com.facebook.presto.spi.ConnectorRecordSinkProvider; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.ConnectorSplitManager; +import com.facebook.presto.spi.ConnectorSplitSource; +import com.facebook.presto.spi.ConnectorTableHandle; +import com.facebook.presto.spi.ConnectorTableMetadata; +import com.facebook.presto.spi.Domain; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.RecordPageSource; +import com.facebook.presto.spi.RecordSink; +import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.spi.SchemaTablePrefix; +import com.facebook.presto.spi.SerializableNativeValue; +import com.facebook.presto.spi.TableNotFoundException; +import com.facebook.presto.spi.TupleDomain; +import com.facebook.presto.spi.ViewNotFoundException; +import com.facebook.presto.spi.type.SqlDate; +import com.facebook.presto.spi.type.SqlTimestamp; +import com.facebook.presto.spi.type.SqlVarbinary; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.testing.MaterializedResult; +import com.facebook.presto.testing.MaterializedRow; +import com.facebook.presto.type.ArrayType; +import com.facebook.presto.type.MapType; +import com.facebook.presto.type.TypeRegistry; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.net.HostAndPort; +import com.google.common.primitives.Ints; +import io.airlift.log.Logger; +import io.airlift.slice.Slice; +import io.airlift.units.Duration; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.hive.serde2.ReaderWriterProfiler; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.TimeZone; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; + +import static com.facebook.presto.hive.HiveErrorCode.HIVE_INVALID_PARTITION_VALUE; +import static com.facebook.presto.hive.HiveSessionProperties.STORAGE_FORMAT_PROPERTY; +import static com.facebook.presto.hive.HiveStorageFormat.DWRF; +import static com.facebook.presto.hive.HiveStorageFormat.ORC; +import static com.facebook.presto.hive.HiveStorageFormat.PARQUET; +import static com.facebook.presto.hive.HiveStorageFormat.RCBINARY; +import static com.facebook.presto.hive.HiveStorageFormat.RCTEXT; +import static com.facebook.presto.hive.HiveStorageFormat.SEQUENCEFILE; +import static com.facebook.presto.hive.HiveStorageFormat.TEXTFILE; +import static com.facebook.presto.hive.HiveTestUtils.DEFAULT_HIVE_DATA_STREAM_FACTORIES; +import static com.facebook.presto.hive.HiveTestUtils.DEFAULT_HIVE_RECORD_CURSOR_PROVIDER; +import static com.facebook.presto.hive.HiveTestUtils.TYPE_MANAGER; +import static com.facebook.presto.hive.HiveTestUtils.getTypes; +import static com.facebook.presto.hive.HiveType.HIVE_INT; +import static com.facebook.presto.hive.HiveType.HIVE_STRING; +import static com.facebook.presto.hive.util.Types.checkType; +import static com.facebook.presto.spi.StandardErrorCode.NOT_SUPPORTED; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.BooleanType.BOOLEAN; +import static com.facebook.presto.spi.type.DateType.DATE; +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; +import static com.facebook.presto.spi.type.HyperLogLogType.HYPER_LOG_LOG; +import static com.facebook.presto.spi.type.TimeZoneKey.UTC_KEY; +import static com.facebook.presto.spi.type.TimestampType.TIMESTAMP; +import static com.facebook.presto.spi.type.TypeSignature.parseTypeSignature; +import static com.facebook.presto.spi.type.VarbinaryType.VARBINARY; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static com.facebook.presto.testing.MaterializedResult.materializeSourceDataStream; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.collect.Iterables.getOnlyElement; +import static com.google.common.collect.Maps.uniqueIndex; +import static com.google.common.util.concurrent.MoreExecutors.newDirectExecutorService; +import static io.airlift.concurrent.MoreFutures.getFutureValue; +import static io.airlift.concurrent.Threads.daemonThreadsNamed; +import static io.airlift.slice.Slices.utf8Slice; +import static io.airlift.testing.Assertions.assertInstanceOf; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Locale.ENGLISH; +import static java.util.Objects.requireNonNull; +import static java.util.concurrent.Executors.newCachedThreadPool; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.fail; + +@Test(groups = "hive") +public abstract class AbstractTestHiveClient +{ + private static final ConnectorSession SESSION = new ConnectorSession("presto_test", UTC_KEY, ENGLISH, System.currentTimeMillis(), null); + + protected static final String INVALID_DATABASE = "totally_invalid_database_name"; + protected static final String INVALID_TABLE = "totally_invalid_table_name"; + protected static final String INVALID_COLUMN = "totally_invalid_column_name"; + + protected Set createTableFormats = ImmutableSet.copyOf(HiveStorageFormat.values()); + + protected String database; + protected SchemaTableName tablePartitionFormat; + protected SchemaTableName tableUnpartitioned; + protected SchemaTableName tableOffline; + protected SchemaTableName tableOfflinePartition; + protected SchemaTableName view; + protected SchemaTableName invalidTable; + protected SchemaTableName tableBucketedStringInt; + protected SchemaTableName tableBucketedBigintBoolean; + protected SchemaTableName tableBucketedDoubleFloat; + protected SchemaTableName tablePartitionSchemaChange; + protected SchemaTableName tablePartitionSchemaChangeNonCanonical; + + protected SchemaTableName temporaryCreateTable; + protected SchemaTableName temporaryCreateSampledTable; + protected SchemaTableName temporaryCreateEmptyTable; + protected SchemaTableName temporaryRenameTableOld; + protected SchemaTableName temporaryRenameTableNew; + protected SchemaTableName temporaryCreateView; + + protected ConnectorTableHandle invalidTableHandle; + + protected ColumnHandle dsColumn; + protected ColumnHandle fileFormatColumn; + protected ColumnHandle dummyColumn; + protected ColumnHandle intColumn; + protected ColumnHandle invalidColumnHandle; + + protected Set partitions; + protected Set unpartitionedPartitions; + protected ConnectorPartition invalidPartition; + + protected DateTimeZone timeZone; + + protected HdfsEnvironment hdfsEnvironment; + + protected ConnectorMetadata metadata; + protected ConnectorSplitManager splitManager; + protected ConnectorPageSourceProvider pageSourceProvider; + protected ConnectorRecordSinkProvider recordSinkProvider; + protected ExecutorService executor; + + @BeforeClass + public void setUp() + throws Exception + { + executor = newCachedThreadPool(daemonThreadsNamed("hive-%s")); + } + + @AfterClass + public void tearDown() + throws Exception + { + if (executor != null) { + executor.shutdownNow(); + executor = null; + } + } + + protected void setupHive(String connectorId, String databaseName, String timeZoneId) + { + database = databaseName; + tablePartitionFormat = new SchemaTableName(database, "presto_test_partition_format"); + tableUnpartitioned = new SchemaTableName(database, "presto_test_unpartitioned"); + tableOffline = new SchemaTableName(database, "presto_test_offline"); + tableOfflinePartition = new SchemaTableName(database, "presto_test_offline_partition"); + view = new SchemaTableName(database, "presto_test_view"); + invalidTable = new SchemaTableName(database, INVALID_TABLE); + tableBucketedStringInt = new SchemaTableName(database, "presto_test_bucketed_by_string_int"); + tableBucketedBigintBoolean = new SchemaTableName(database, "presto_test_bucketed_by_bigint_boolean"); + tableBucketedDoubleFloat = new SchemaTableName(database, "presto_test_bucketed_by_double_float"); + tablePartitionSchemaChange = new SchemaTableName(database, "presto_test_partition_schema_change"); + tablePartitionSchemaChangeNonCanonical = new SchemaTableName(database, "presto_test_partition_schema_change_non_canonical"); + + temporaryCreateTable = new SchemaTableName(database, "tmp_presto_test_create_" + randomName()); + temporaryCreateSampledTable = new SchemaTableName(database, "tmp_presto_test_create_" + randomName()); + temporaryCreateEmptyTable = new SchemaTableName(database, "tmp_presto_test_create_" + randomName()); + temporaryRenameTableOld = new SchemaTableName(database, "tmp_presto_test_rename_" + randomName()); + temporaryRenameTableNew = new SchemaTableName(database, "tmp_presto_test_rename_" + randomName()); + temporaryCreateView = new SchemaTableName(database, "tmp_presto_test_create_" + randomName()); + + invalidTableHandle = new HiveTableHandle("hive", database, INVALID_TABLE, SESSION); + + dsColumn = new HiveColumnHandle(connectorId, "ds", 0, HIVE_STRING, parseTypeSignature(StandardTypes.VARCHAR), -1, true); + fileFormatColumn = new HiveColumnHandle(connectorId, "file_format", 1, HIVE_STRING, parseTypeSignature(StandardTypes.VARCHAR), -1, true); + dummyColumn = new HiveColumnHandle(connectorId, "dummy", 2, HIVE_INT, parseTypeSignature(StandardTypes.BIGINT), -1, true); + intColumn = new HiveColumnHandle(connectorId, "t_int", 0, HIVE_INT, parseTypeSignature(StandardTypes.BIGINT), -1, true); + invalidColumnHandle = new HiveColumnHandle(connectorId, INVALID_COLUMN, 0, HIVE_STRING, parseTypeSignature(StandardTypes.VARCHAR), 0, false); + + partitions = ImmutableSet.builder() + .add(new HivePartition(tablePartitionFormat, + TupleDomain.all(), + "ds=2012-12-29/file_format=textfile/dummy=1", + ImmutableMap.builder() + .put(dsColumn, new SerializableNativeValue(Slice.class, utf8Slice("2012-12-29"))) + .put(fileFormatColumn, new SerializableNativeValue(Slice.class, utf8Slice("textfile"))) + .put(dummyColumn, new SerializableNativeValue(Long.class, 1L)) + .build(), + Optional.empty())) + .add(new HivePartition(tablePartitionFormat, + TupleDomain.all(), + "ds=2012-12-29/file_format=sequencefile/dummy=2", + ImmutableMap.builder() + .put(dsColumn, new SerializableNativeValue(Slice.class, utf8Slice("2012-12-29"))) + .put(fileFormatColumn, new SerializableNativeValue(Slice.class, utf8Slice("sequencefile"))) + .put(dummyColumn, new SerializableNativeValue(Long.class, 2L)) + .build(), + Optional.empty())) + .add(new HivePartition(tablePartitionFormat, + TupleDomain.all(), + "ds=2012-12-29/file_format=rctext/dummy=3", + ImmutableMap.builder() + .put(dsColumn, new SerializableNativeValue(Slice.class, utf8Slice("2012-12-29"))) + .put(fileFormatColumn, new SerializableNativeValue(Slice.class, utf8Slice("rctext"))) + .put(dummyColumn, new SerializableNativeValue(Long.class, 3L)) + .build(), + Optional.empty())) + .add(new HivePartition(tablePartitionFormat, + TupleDomain.all(), + "ds=2012-12-29/file_format=rcbinary/dummy=4", + ImmutableMap.builder() + .put(dsColumn, new SerializableNativeValue(Slice.class, utf8Slice("2012-12-29"))) + .put(fileFormatColumn, new SerializableNativeValue(Slice.class, utf8Slice("rcbinary"))) + .put(dummyColumn, new SerializableNativeValue(Long.class, 4L)) + .build(), + Optional.empty())) + .build(); + unpartitionedPartitions = ImmutableSet.of(new HivePartition(tableUnpartitioned, TupleDomain.all())); + invalidPartition = new HivePartition(invalidTable, TupleDomain.all(), "unknown", ImmutableMap.of(), Optional.empty()); + timeZone = DateTimeZone.forTimeZone(TimeZone.getTimeZone(timeZoneId)); + } + + protected void setup(String host, int port, String databaseName, String timeZone) + { + setup(host, port, databaseName, timeZone, "hive-test", 100, 50); + } + + protected void setup(String host, int port, String databaseName, String timeZoneId, String connectorName, int maxOutstandingSplits, int maxThreads) + { + setupHive(connectorName, databaseName, timeZoneId); + + HiveClientConfig hiveClientConfig = new HiveClientConfig(); + hiveClientConfig.setTimeZone(timeZoneId); + String proxy = System.getProperty("hive.metastore.thrift.client.socks-proxy"); + if (proxy != null) { + hiveClientConfig.setMetastoreSocksProxy(HostAndPort.fromString(proxy)); + } + + HiveCluster hiveCluster = new TestingHiveCluster(hiveClientConfig, host, port); + HiveMetastore metastoreClient = new CachingHiveMetastore(hiveCluster, executor, Duration.valueOf("1m"), Duration.valueOf("15s")); + HiveConnectorId connectorId = new HiveConnectorId(connectorName); + HdfsConfiguration hdfsConfiguration = new HiveHdfsConfiguration(new HdfsConfigurationUpdater(hiveClientConfig)); + + hdfsEnvironment = new HdfsEnvironment(hdfsConfiguration, hiveClientConfig); + splitManager = new HiveSplitManager( + connectorId, + metastoreClient, + new NamenodeStats(), + hdfsEnvironment, + new HadoopDirectoryLister(), + timeZone, + newDirectExecutorService(), + maxOutstandingSplits, + hiveClientConfig.getMinPartitionBatchSize(), + hiveClientConfig.getMaxPartitionBatchSize(), + hiveClientConfig.getMaxSplitSize(), + hiveClientConfig.getMaxInitialSplitSize(), + hiveClientConfig.getMaxInitialSplits(), + false, + false, + false); + metadata = new HiveMetadata( + connectorId, + metastoreClient, + hdfsEnvironment, + timeZone, + true, + true, + true, + hiveClientConfig.getHiveStorageFormat(), + new TypeRegistry(), + splitManager); + recordSinkProvider = new HiveRecordSinkProvider(hdfsEnvironment); + pageSourceProvider = new HivePageSourceProvider(hiveClientConfig, hdfsEnvironment, DEFAULT_HIVE_RECORD_CURSOR_PROVIDER, DEFAULT_HIVE_DATA_STREAM_FACTORIES, TYPE_MANAGER); + } + + @Test + public void testGetDatabaseNames() + throws Exception + { + List databases = metadata.listSchemaNames(SESSION); + assertTrue(databases.contains(database)); + } + + @Test + public void testGetTableNames() + throws Exception + { + List tables = metadata.listTables(SESSION, database); + assertTrue(tables.contains(tablePartitionFormat)); + assertTrue(tables.contains(tableUnpartitioned)); + } + + @Test + public void testGetAllTableNames() + throws Exception + { + List tables = metadata.listTables(SESSION, null); + assertTrue(tables.contains(tablePartitionFormat)); + assertTrue(tables.contains(tableUnpartitioned)); + } + + @Test + public void testGetAllTableColumns() + { + Map> allColumns = metadata.listTableColumns(SESSION, new SchemaTablePrefix()); + assertTrue(allColumns.containsKey(tablePartitionFormat)); + assertTrue(allColumns.containsKey(tableUnpartitioned)); + } + + @Test + public void testGetAllTableColumnsInSchema() + { + Map> allColumns = metadata.listTableColumns(SESSION, new SchemaTablePrefix(database)); + assertTrue(allColumns.containsKey(tablePartitionFormat)); + assertTrue(allColumns.containsKey(tableUnpartitioned)); + } + + @Test + public void testListUnknownSchema() + { + assertNull(metadata.getTableHandle(SESSION, new SchemaTableName(INVALID_DATABASE, INVALID_TABLE))); + assertEquals(metadata.listTables(SESSION, INVALID_DATABASE), ImmutableList.of()); + assertEquals(metadata.listTableColumns(SESSION, new SchemaTablePrefix(INVALID_DATABASE, INVALID_TABLE)), ImmutableMap.of()); + assertEquals(metadata.listViews(SESSION, INVALID_DATABASE), ImmutableList.of()); + assertEquals(metadata.getViews(SESSION, new SchemaTablePrefix(INVALID_DATABASE, INVALID_TABLE)), ImmutableMap.of()); + } + + @Test + public void testGetPartitions() + throws Exception + { + ConnectorTableHandle tableHandle = getTableHandle(tablePartitionFormat); + ConnectorPartitionResult partitionResult = splitManager.getPartitions(tableHandle, TupleDomain.all()); + assertExpectedPartitions(partitionResult.getPartitions(), partitions); + } + + @Test + public void testGetPartitionsWithBindings() + throws Exception + { + ConnectorTableHandle tableHandle = getTableHandle(tablePartitionFormat); + ConnectorPartitionResult partitionResult = splitManager.getPartitions(tableHandle, TupleDomain.withColumnDomains(ImmutableMap.of(intColumn, Domain.singleValue(5L)))); + assertExpectedPartitions(partitionResult.getPartitions(), partitions); + } + + @Test(expectedExceptions = TableNotFoundException.class) + public void testGetPartitionsException() + throws Exception + { + splitManager.getPartitions(invalidTableHandle, TupleDomain.all()); + } + + @Test + public void testGetPartitionNames() + throws Exception + { + ConnectorTableHandle tableHandle = getTableHandle(tablePartitionFormat); + ConnectorPartitionResult partitionResult = splitManager.getPartitions(tableHandle, TupleDomain.all()); + assertExpectedPartitions(partitionResult.getPartitions(), partitions); + } + + protected void assertExpectedPartitions(List actualPartitions, Iterable expectedPartitions) + { + Map actualById = uniqueIndex(actualPartitions, ConnectorPartition::getPartitionId); + for (ConnectorPartition expected : expectedPartitions) { + assertInstanceOf(expected, HivePartition.class); + HivePartition expectedPartition = (HivePartition) expected; + + ConnectorPartition actual = actualById.get(expectedPartition.getPartitionId()); + assertEquals(actual, expected); + assertInstanceOf(actual, HivePartition.class); + HivePartition actualPartition = (HivePartition) actual; + + assertNotNull(actualPartition, "partition " + expectedPartition.getPartitionId()); + assertEquals(actualPartition.getPartitionId(), expectedPartition.getPartitionId()); + assertEquals(actualPartition.getKeys(), expectedPartition.getKeys()); + assertEquals(actualPartition.getTableName(), expectedPartition.getTableName()); + assertEquals(actualPartition.getBucket(), expectedPartition.getBucket()); + assertEquals(actualPartition.getTupleDomain(), expectedPartition.getTupleDomain()); + } + } + + @Test + public void testGetPartitionNamesUnpartitioned() + throws Exception + { + ConnectorTableHandle tableHandle = getTableHandle(tableUnpartitioned); + ConnectorPartitionResult partitionResult = splitManager.getPartitions(tableHandle, TupleDomain.all()); + assertEquals(partitionResult.getPartitions().size(), 1); + assertEquals(partitionResult.getPartitions(), unpartitionedPartitions); + } + + @Test(expectedExceptions = TableNotFoundException.class) + public void testGetPartitionNamesException() + throws Exception + { + splitManager.getPartitions(invalidTableHandle, TupleDomain.all()); + } + + @SuppressWarnings({"ValueOfIncrementOrDecrementUsed", "UnusedAssignment"}) + @Test + public void testGetTableSchemaPartitionFormat() + throws Exception + { + ConnectorTableMetadata tableMetadata = metadata.getTableMetadata(getTableHandle(tablePartitionFormat)); + Map map = uniqueIndex(tableMetadata.getColumns(), ColumnMetadata::getName); + + int i = 0; + assertPrimitiveField(map, i++, "t_string", VARCHAR, false); + assertPrimitiveField(map, i++, "t_tinyint", BIGINT, false); + assertPrimitiveField(map, i++, "t_smallint", BIGINT, false); + assertPrimitiveField(map, i++, "t_int", BIGINT, false); + assertPrimitiveField(map, i++, "t_bigint", BIGINT, false); + assertPrimitiveField(map, i++, "t_float", DOUBLE, false); + assertPrimitiveField(map, i++, "t_double", DOUBLE, false); + assertPrimitiveField(map, i++, "t_boolean", BOOLEAN, false); + assertPrimitiveField(map, i++, "ds", VARCHAR, true); + assertPrimitiveField(map, i++, "file_format", VARCHAR, true); + assertPrimitiveField(map, i++, "dummy", BIGINT, true); + } + + @Test + public void testGetTableSchemaUnpartitioned() + throws Exception + { + ConnectorTableHandle tableHandle = getTableHandle(tableUnpartitioned); + ConnectorTableMetadata tableMetadata = metadata.getTableMetadata(tableHandle); + Map map = uniqueIndex(tableMetadata.getColumns(), ColumnMetadata::getName); + + assertPrimitiveField(map, 0, "t_string", VARCHAR, false); + assertPrimitiveField(map, 1, "t_tinyint", BIGINT, false); + } + + @Test + public void testGetTableSchemaOffline() + throws Exception + { + ConnectorTableHandle tableHandle = getTableHandle(tableOffline); + ConnectorTableMetadata tableMetadata = metadata.getTableMetadata(tableHandle); + Map map = uniqueIndex(tableMetadata.getColumns(), ColumnMetadata::getName); + + assertPrimitiveField(map, 0, "t_string", VARCHAR, false); + } + + @Test + public void testGetTableSchemaOfflinePartition() + throws Exception + { + ConnectorTableHandle tableHandle = getTableHandle(tableOfflinePartition); + ConnectorTableMetadata tableMetadata = metadata.getTableMetadata(tableHandle); + Map map = uniqueIndex(tableMetadata.getColumns(), ColumnMetadata::getName); + + assertPrimitiveField(map, 0, "t_string", VARCHAR, false); + } + + @Test + public void testGetTableSchemaException() + throws Exception + { + assertNull(metadata.getTableHandle(SESSION, invalidTable)); + } + + @Test + public void testGetPartitionSplitsBatch() + throws Exception + { + ConnectorTableHandle tableHandle = getTableHandle(tablePartitionFormat); + ConnectorPartitionResult partitionResult = splitManager.getPartitions(tableHandle, TupleDomain.all()); + ConnectorSplitSource splitSource = splitManager.getPartitionSplits(tableHandle, partitionResult.getPartitions()); + + assertEquals(getSplitCount(splitSource), partitions.size()); + } + + @Test + public void testGetPartitionSplitsBatchUnpartitioned() + throws Exception + { + ConnectorTableHandle tableHandle = getTableHandle(tableUnpartitioned); + ConnectorPartitionResult partitionResult = splitManager.getPartitions(tableHandle, TupleDomain.all()); + ConnectorSplitSource splitSource = splitManager.getPartitionSplits(tableHandle, partitionResult.getPartitions()); + + assertEquals(getSplitCount(splitSource), 1); + } + + @Test(expectedExceptions = TableNotFoundException.class) + public void testGetPartitionSplitsBatchInvalidTable() + throws Exception + { + splitManager.getPartitionSplits(invalidTableHandle, ImmutableList.of(invalidPartition)); + } + + @Test + public void testGetPartitionSplitsEmpty() + throws Exception + { + ConnectorSplitSource splitSource = splitManager.getPartitionSplits(invalidTableHandle, ImmutableList.of()); + // fetch full list + getSplitCount(splitSource); + } + + @Test + public void testGetPartitionTableOffline() + throws Exception + { + ConnectorTableHandle tableHandle = getTableHandle(tableOffline); + try { + splitManager.getPartitions(tableHandle, TupleDomain.all()); + fail("expected TableOfflineException"); + } + catch (TableOfflineException e) { + assertEquals(e.getTableName(), tableOffline); + } + } + + @Test + public void testGetPartitionSplitsTableOfflinePartition() + throws Exception + { + ConnectorTableHandle tableHandle = getTableHandle(tableOfflinePartition); + assertNotNull(tableHandle); + + ColumnHandle dsColumn = metadata.getColumnHandles(tableHandle).get("ds"); + assertNotNull(dsColumn); + + Domain domain = Domain.singleValue(utf8Slice("2012-12-30")); + TupleDomain tupleDomain = TupleDomain.withColumnDomains(ImmutableMap.of(dsColumn, domain)); + ConnectorPartitionResult partitionResult = splitManager.getPartitions(tableHandle, tupleDomain); + for (ConnectorPartition partition : partitionResult.getPartitions()) { + if (domain.equals(partition.getTupleDomain().getDomains().get(dsColumn))) { + try { + getSplitCount(splitManager.getPartitionSplits(tableHandle, ImmutableList.of(partition))); + fail("Expected PartitionOfflineException"); + } + catch (PartitionOfflineException e) { + assertEquals(e.getTableName(), tableOfflinePartition); + assertEquals(e.getPartition(), "ds=2012-12-30"); + } + } + else { + getSplitCount(splitManager.getPartitionSplits(tableHandle, ImmutableList.of(partition))); + } + } + } + + @Test + public void testBucketedTableStringInt() + throws Exception + { + ConnectorTableHandle tableHandle = getTableHandle(tableBucketedStringInt); + List columnHandles = ImmutableList.copyOf(metadata.getColumnHandles(tableHandle).values()); + Map columnIndex = indexColumns(columnHandles); + + assertTableIsBucketed(tableHandle); + + String testString = "test"; + Long testInt = 13L; + Long testSmallint = 12L; + + // Reverse the order of bindings as compared to bucketing order + ImmutableMap bindings = ImmutableMap.builder() + .put(columnHandles.get(columnIndex.get("t_int")), new SerializableNativeValue(Long.class, testInt)) + .put(columnHandles.get(columnIndex.get("t_string")), new SerializableNativeValue(Slice.class, utf8Slice(testString))) + .put(columnHandles.get(columnIndex.get("t_smallint")), new SerializableNativeValue(Long.class, testSmallint)) + .build(); + + ConnectorPartitionResult partitionResult = splitManager.getPartitions(tableHandle, TupleDomain.withNullableFixedValues(bindings)); + List splits = getAllSplits(splitManager.getPartitionSplits(tableHandle, partitionResult.getPartitions())); + assertEquals(splits.size(), 1); + + try (ConnectorPageSource pageSource = pageSourceProvider.createPageSource(splits.get(0), columnHandles)) { + MaterializedResult result = materializeSourceDataStream(SESSION, pageSource, getTypes(columnHandles)); + + boolean rowFound = false; + for (MaterializedRow row : result) { + if (testString.equals(row.getField(columnIndex.get("t_string"))) && + testInt.equals(row.getField(columnIndex.get("t_int"))) && + testSmallint.equals(row.getField(columnIndex.get("t_smallint")))) { + rowFound = true; + } + } + assertTrue(rowFound); + } + } + + @SuppressWarnings("ConstantConditions") + @Test + public void testBucketedTableBigintBoolean() + throws Exception + { + ConnectorTableHandle tableHandle = getTableHandle(tableBucketedBigintBoolean); + List columnHandles = ImmutableList.copyOf(metadata.getColumnHandles(tableHandle).values()); + Map columnIndex = indexColumns(columnHandles); + + assertTableIsBucketed(tableHandle); + + String testString = "test"; + Long testBigint = 89L; + Boolean testBoolean = true; + + ImmutableMap bindings = ImmutableMap.builder() + .put(columnHandles.get(columnIndex.get("t_string")), new SerializableNativeValue(Slice.class, utf8Slice(testString))) + .put(columnHandles.get(columnIndex.get("t_bigint")), new SerializableNativeValue(Long.class, testBigint)) + .put(columnHandles.get(columnIndex.get("t_boolean")), new SerializableNativeValue(Boolean.class, testBoolean)) + .build(); + + ConnectorPartitionResult partitionResult = splitManager.getPartitions(tableHandle, TupleDomain.withNullableFixedValues(bindings)); + List splits = getAllSplits(splitManager.getPartitionSplits(tableHandle, partitionResult.getPartitions())); + assertEquals(splits.size(), 1); + + try (ConnectorPageSource pageSource = pageSourceProvider.createPageSource(splits.get(0), columnHandles)) { + MaterializedResult result = materializeSourceDataStream(SESSION, pageSource, getTypes(columnHandles)); + + boolean rowFound = false; + for (MaterializedRow row : result) { + if (testString.equals(row.getField(columnIndex.get("t_string"))) && + testBigint.equals(row.getField(columnIndex.get("t_bigint"))) && + testBoolean.equals(row.getField(columnIndex.get("t_boolean")))) { + rowFound = true; + break; + } + } + assertTrue(rowFound); + } + } + + @Test + public void testBucketedTableDoubleFloat() + throws Exception + { + ConnectorTableHandle tableHandle = getTableHandle(tableBucketedDoubleFloat); + List columnHandles = ImmutableList.copyOf(metadata.getColumnHandles(tableHandle).values()); + Map columnIndex = indexColumns(columnHandles); + + assertTableIsBucketed(tableHandle); + + ImmutableMap bindings = ImmutableMap.builder() + .put(columnHandles.get(columnIndex.get("t_float")), new SerializableNativeValue(Double.class, 87.1)) + .put(columnHandles.get(columnIndex.get("t_double")), new SerializableNativeValue(Double.class, 88.2)) + .build(); + + // floats and doubles are not supported, so we should see all splits + ConnectorPartitionResult partitionResult = splitManager.getPartitions(tableHandle, TupleDomain.withNullableFixedValues(bindings)); + List splits = getAllSplits(splitManager.getPartitionSplits(tableHandle, partitionResult.getPartitions())); + assertEquals(splits.size(), 32); + + int count = 0; + for (ConnectorSplit split : splits) { + try (ConnectorPageSource pageSource = pageSourceProvider.createPageSource(split, columnHandles)) { + MaterializedResult result = materializeSourceDataStream(SESSION, pageSource, getTypes(columnHandles)); + count += result.getRowCount(); + } + } + assertEquals(count, 100); + } + + private void assertTableIsBucketed(ConnectorTableHandle tableHandle) + throws Exception + { + // the bucketed test tables should have exactly 32 splits + ConnectorPartitionResult partitionResult = splitManager.getPartitions(tableHandle, TupleDomain.all()); + List splits = getAllSplits(splitManager.getPartitionSplits(tableHandle, partitionResult.getPartitions())); + assertEquals(splits.size(), 32); + + // verify all paths are unique + Set paths = new HashSet<>(); + for (ConnectorSplit split : splits) { + assertTrue(paths.add(((HiveSplit) split).getPath())); + } + } + + @Test + public void testGetRecords() + throws Exception + { + ConnectorTableHandle tableHandle = getTableHandle(tablePartitionFormat); + ConnectorTableMetadata tableMetadata = metadata.getTableMetadata(tableHandle); + List columnHandles = ImmutableList.copyOf(metadata.getColumnHandles(tableHandle).values()); + Map columnIndex = indexColumns(columnHandles); + + ConnectorPartitionResult partitionResult = splitManager.getPartitions(tableHandle, TupleDomain.all()); + List splits = getAllSplits(splitManager.getPartitionSplits(tableHandle, partitionResult.getPartitions())); + assertEquals(splits.size(), this.partitions.size()); + for (ConnectorSplit split : splits) { + HiveSplit hiveSplit = (HiveSplit) split; + + List partitionKeys = hiveSplit.getPartitionKeys(); + String ds = partitionKeys.get(0).getValue(); + String fileFormat = partitionKeys.get(1).getValue(); + HiveStorageFormat fileType = HiveStorageFormat.valueOf(fileFormat.toUpperCase()); + long dummyPartition = Long.parseLong(partitionKeys.get(2).getValue()); + + long rowNumber = 0; + long completedBytes = 0; + try (ConnectorPageSource pageSource = pageSourceProvider.createPageSource(hiveSplit, columnHandles)) { + MaterializedResult result = materializeSourceDataStream(SESSION, pageSource, getTypes(columnHandles)); + + assertPageSourceType(pageSource, fileType); + + for (MaterializedRow row : result) { + try { + assertValueTypes(row, tableMetadata.getColumns()); + } + catch (RuntimeException e) { + throw new RuntimeException("row " + rowNumber, e); + } + + rowNumber++; + + if (rowNumber % 19 == 0) { + assertNull(row.getField(columnIndex.get("t_string"))); + } + else if (rowNumber % 19 == 1) { + assertEquals(row.getField(columnIndex.get("t_string")), ""); + } + else { + assertEquals(row.getField(columnIndex.get("t_string")), "test"); + } + + assertEquals(row.getField(columnIndex.get("t_tinyint")), 1 + rowNumber); + assertEquals(row.getField(columnIndex.get("t_smallint")), 2 + rowNumber); + assertEquals(row.getField(columnIndex.get("t_int")), 3 + rowNumber); + + if (rowNumber % 13 == 0) { + assertNull(row.getField(columnIndex.get("t_bigint"))); + } + else { + assertEquals(row.getField(columnIndex.get("t_bigint")), 4 + rowNumber); + } + + assertEquals((Double) row.getField(columnIndex.get("t_float")), 5.1 + rowNumber, 0.001); + assertEquals(row.getField(columnIndex.get("t_double")), 6.2 + rowNumber); + + if (rowNumber % 3 == 2) { + assertNull(row.getField(columnIndex.get("t_boolean"))); + } + else { + assertEquals(row.getField(columnIndex.get("t_boolean")), rowNumber % 3 != 0); + } + + assertEquals(row.getField(columnIndex.get("ds")), ds); + assertEquals(row.getField(columnIndex.get("file_format")), fileFormat); + assertEquals(row.getField(columnIndex.get("dummy")), dummyPartition); + + long newCompletedBytes = pageSource.getCompletedBytes(); + assertTrue(newCompletedBytes >= completedBytes); + assertTrue(newCompletedBytes <= hiveSplit.getLength()); + completedBytes = newCompletedBytes; + } + + assertTrue(completedBytes <= hiveSplit.getLength()); + assertEquals(rowNumber, 100); + } + } + } + + @Test + public void testGetPartialRecords() + throws Exception + { + ConnectorTableHandle tableHandle = getTableHandle(tablePartitionFormat); + List columnHandles = ImmutableList.copyOf(metadata.getColumnHandles(tableHandle).values()); + Map columnIndex = indexColumns(columnHandles); + + ConnectorPartitionResult partitionResult = splitManager.getPartitions(tableHandle, TupleDomain.all()); + List splits = getAllSplits(splitManager.getPartitionSplits(tableHandle, partitionResult.getPartitions())); + assertEquals(splits.size(), this.partitions.size()); + for (ConnectorSplit split : splits) { + HiveSplit hiveSplit = (HiveSplit) split; + + List partitionKeys = hiveSplit.getPartitionKeys(); + String ds = partitionKeys.get(0).getValue(); + String fileFormat = partitionKeys.get(1).getValue(); + HiveStorageFormat fileType = HiveStorageFormat.valueOf(fileFormat.toUpperCase()); + long dummyPartition = Long.parseLong(partitionKeys.get(2).getValue()); + + long rowNumber = 0; + try (ConnectorPageSource pageSource = pageSourceProvider.createPageSource(hiveSplit, columnHandles)) { + assertPageSourceType(pageSource, fileType); + MaterializedResult result = materializeSourceDataStream(SESSION, pageSource, getTypes(columnHandles)); + for (MaterializedRow row : result) { + rowNumber++; + + assertEquals(row.getField(columnIndex.get("t_double")), 6.2 + rowNumber); + assertEquals(row.getField(columnIndex.get("ds")), ds); + assertEquals(row.getField(columnIndex.get("file_format")), fileFormat); + assertEquals(row.getField(columnIndex.get("dummy")), dummyPartition); + } + } + assertEquals(rowNumber, 100); + } + } + + @Test + public void testGetRecordsUnpartitioned() + throws Exception + { + ConnectorTableHandle tableHandle = getTableHandle(tableUnpartitioned); + List columnHandles = ImmutableList.copyOf(metadata.getColumnHandles(tableHandle).values()); + Map columnIndex = indexColumns(columnHandles); + + ConnectorPartitionResult partitionResult = splitManager.getPartitions(tableHandle, TupleDomain.all()); + List splits = getAllSplits(splitManager.getPartitionSplits(tableHandle, partitionResult.getPartitions())); + assertEquals(splits.size(), 1); + + for (ConnectorSplit split : splits) { + HiveSplit hiveSplit = (HiveSplit) split; + + assertEquals(hiveSplit.getPartitionKeys(), ImmutableList.of()); + + long rowNumber = 0; + try (ConnectorPageSource pageSource = pageSourceProvider.createPageSource(split, columnHandles)) { + assertPageSourceType(pageSource, TEXTFILE); + MaterializedResult result = materializeSourceDataStream(SESSION, pageSource, getTypes(columnHandles)); + + assertEquals(pageSource.getTotalBytes(), hiveSplit.getLength()); + for (MaterializedRow row : result) { + rowNumber++; + + if (rowNumber % 19 == 0) { + assertNull(row.getField(columnIndex.get("t_string"))); + } + else if (rowNumber % 19 == 1) { + assertEquals(row.getField(columnIndex.get("t_string")), ""); + } + else { + assertEquals(row.getField(columnIndex.get("t_string")), "unpartitioned"); + } + + assertEquals(row.getField(columnIndex.get("t_tinyint")), 1 + rowNumber); + } + } + assertEquals(rowNumber, 100); + } + } + + @Test(expectedExceptions = RuntimeException.class, expectedExceptionsMessageRegExp = ".*" + INVALID_COLUMN + ".*") + public void testGetRecordsInvalidColumn() + throws Exception + { + ConnectorTableHandle table = getTableHandle(tableUnpartitioned); + ConnectorPartitionResult partitionResult = splitManager.getPartitions(table, TupleDomain.all()); + ConnectorSplit split = Iterables.getFirst(getAllSplits(splitManager.getPartitionSplits(table, partitionResult.getPartitions())), null); + pageSourceProvider.createPageSource(split, ImmutableList.of(invalidColumnHandle)); + } + + @Test(expectedExceptions = PrestoException.class, expectedExceptionsMessageRegExp = ".*The column 't_data' in table '.*\\.presto_test_partition_schema_change' is declared as type 'bigint', but partition 'ds=2012-12-29' declared column 't_data' as type 'string'.") + public void testPartitionSchemaMismatch() + throws Exception + { + ConnectorTableHandle table = getTableHandle(tablePartitionSchemaChange); + ConnectorPartitionResult partitionResult = splitManager.getPartitions(table, TupleDomain.all()); + getAllSplits(splitManager.getPartitionSplits(table, partitionResult.getPartitions())); + } + + @Test + public void testPartitionSchemaNonCanonical() + throws Exception + { + ConnectorTableHandle table = getTableHandle(tablePartitionSchemaChangeNonCanonical); + ColumnHandle column = metadata.getColumnHandles(table).get("t_boolean"); + assertNotNull(column); + ConnectorPartitionResult partitionResult = splitManager.getPartitions(table, TupleDomain.withFixedValues(ImmutableMap.of(column, false))); + assertEquals(partitionResult.getPartitions().size(), 1); + assertEquals(partitionResult.getPartitions().get(0).getPartitionId(), "t_boolean=0"); + + ConnectorSplitSource splitSource = splitManager.getPartitionSplits(table, partitionResult.getPartitions()); + ConnectorSplit split = getOnlyElement(getAllSplits(splitSource)); + + ImmutableList columnHandles = ImmutableList.of(column); + try (ConnectorPageSource pageSource = pageSourceProvider.createPageSource(split, columnHandles)) { + // TODO coercion of non-canonical values should be supported + fail("expected exception"); + } + catch (PrestoException e) { + assertEquals(e.getErrorCode(), HIVE_INVALID_PARTITION_VALUE.toErrorCode()); + } + } + + @Test + public void testTypesTextFile() + throws Exception + { + assertGetRecords("presto_test_types_textfile", TEXTFILE); + } + + @Test + public void testTypesSequenceFile() + throws Exception + { + assertGetRecords("presto_test_types_sequencefile", SEQUENCEFILE); + } + + @Test + public void testTypesRcText() + throws Exception + { + assertGetRecords("presto_test_types_rctext", RCTEXT); + } + + @Test + public void testTypesRcTextRecordCursor() + throws Exception + { + if (metadata.getTableHandle(SESSION, new SchemaTableName(database, "presto_test_types_rctext")) == null) { + return; + } + + ConnectorTableHandle tableHandle = getTableHandle(new SchemaTableName(database, "presto_test_types_rctext")); + ConnectorTableMetadata tableMetadata = metadata.getTableMetadata(tableHandle); + HiveSplit hiveSplit = getHiveSplit(tableHandle); + List columnHandles = ImmutableList.copyOf(metadata.getColumnHandles(tableHandle).values()); + + ConnectorPageSourceProvider pageSourceProvider = new HivePageSourceProvider( + new HiveClientConfig().setTimeZone(timeZone.getID()), + hdfsEnvironment, + ImmutableSet.of(new ColumnarTextHiveRecordCursorProvider()), + ImmutableSet.of(), + TYPE_MANAGER); + + ConnectorPageSource pageSource = pageSourceProvider.createPageSource(hiveSplit, columnHandles); + assertGetRecords(RCTEXT, tableMetadata, hiveSplit, pageSource, columnHandles); + } + + @Test + public void testTypesRcBinary() + throws Exception + { + assertGetRecords("presto_test_types_rcbinary", RCBINARY); + } + + @Test + public void testTypesRcBinaryRecordCursor() + throws Exception + { + if (metadata.getTableHandle(SESSION, new SchemaTableName(database, "presto_test_types_rcbinary")) == null) { + return; + } + + ConnectorTableHandle tableHandle = getTableHandle(new SchemaTableName(database, "presto_test_types_rcbinary")); + ConnectorTableMetadata tableMetadata = metadata.getTableMetadata(tableHandle); + HiveSplit hiveSplit = getHiveSplit(tableHandle); + List columnHandles = ImmutableList.copyOf(metadata.getColumnHandles(tableHandle).values()); + + ConnectorPageSourceProvider pageSourceProvider = new HivePageSourceProvider( + new HiveClientConfig().setTimeZone(timeZone.getID()), + hdfsEnvironment, + ImmutableSet.of(new ColumnarBinaryHiveRecordCursorProvider()), + ImmutableSet.of(), + TYPE_MANAGER); + + ConnectorPageSource pageSource = pageSourceProvider.createPageSource(hiveSplit, columnHandles); + assertGetRecords(RCBINARY, tableMetadata, hiveSplit, pageSource, columnHandles); + } + + @Test + public void testTypesOrc() + throws Exception + { + assertGetRecordsOptional("presto_test_types_orc", ORC); + } + + @Test + public void testTypesOrcRecordCursor() + throws Exception + { + if (metadata.getTableHandle(SESSION, new SchemaTableName(database, "presto_test_types_orc")) == null) { + return; + } + + ConnectorTableHandle tableHandle = getTableHandle(new SchemaTableName(database, "presto_test_types_orc")); + ConnectorTableMetadata tableMetadata = metadata.getTableMetadata(tableHandle); + HiveSplit hiveSplit = getHiveSplit(tableHandle); + List columnHandles = ImmutableList.copyOf(metadata.getColumnHandles(tableHandle).values()); + + ConnectorPageSourceProvider pageSourceProvider = new HivePageSourceProvider( + new HiveClientConfig().setTimeZone(timeZone.getID()), + hdfsEnvironment, + ImmutableSet.of(new OrcRecordCursorProvider()), + ImmutableSet.of(), + TYPE_MANAGER); + + ConnectorPageSource pageSource = pageSourceProvider.createPageSource(hiveSplit, columnHandles); + assertGetRecords(ORC, tableMetadata, hiveSplit, pageSource, columnHandles); + } + + @Test + public void testTypesParquet() + throws Exception + { + assertGetRecordsOptional("presto_test_types_parquet", PARQUET); + } + + @Test + public void testTypesDwrf() + throws Exception + { + assertGetRecordsOptional("presto_test_types_dwrf", DWRF); + } + + @Test + public void testTypesDwrfRecordCursor() + throws Exception + { + if (metadata.getTableHandle(SESSION, new SchemaTableName(database, "presto_test_types_dwrf")) == null) { + return; + } + + ConnectorTableHandle tableHandle = getTableHandle(new SchemaTableName(database, "presto_test_types_dwrf")); + ConnectorTableMetadata tableMetadata = metadata.getTableMetadata(tableHandle); + HiveSplit hiveSplit = getHiveSplit(tableHandle); + List columnHandles = ImmutableList.copyOf(metadata.getColumnHandles(tableHandle).values()); + + ReaderWriterProfiler.setProfilerOptions(new Configuration()); + + ConnectorPageSourceProvider pageSourceProvider = new HivePageSourceProvider( + new HiveClientConfig().setTimeZone(timeZone.getID()), + hdfsEnvironment, + ImmutableSet.of(new DwrfRecordCursorProvider()), + ImmutableSet.of(), + TYPE_MANAGER); + + ConnectorPageSource pageSource = pageSourceProvider.createPageSource(hiveSplit, columnHandles); + assertGetRecords(DWRF, tableMetadata, hiveSplit, pageSource, columnHandles); + } + + @Test + public void testHiveViewsAreNotSupported() + throws Exception + { + try { + getTableHandle(view); + fail("Expected HiveViewNotSupportedException"); + } + catch (HiveViewNotSupportedException e) { + assertEquals(e.getTableName(), view); + } + } + + @Test + public void testHiveViewsHaveNoColumns() + throws Exception + { + assertEquals(metadata.listTableColumns(SESSION, new SchemaTablePrefix(view.getSchemaName(), view.getTableName())), ImmutableMap.of()); + } + + @Test + public void testRenameTable() + { + try { + createDummyTable(temporaryRenameTableOld); + + metadata.renameTable(getTableHandle(temporaryRenameTableOld), temporaryRenameTableNew); + + assertNull(metadata.getTableHandle(SESSION, temporaryRenameTableOld)); + assertNotNull(metadata.getTableHandle(SESSION, temporaryRenameTableNew)); + } + finally { + dropTable(temporaryRenameTableOld); + dropTable(temporaryRenameTableNew); + } + } + + @Test + public void testTableCreation() + throws Exception + { + for (HiveStorageFormat storageFormat : createTableFormats) { + try { + doCreateTable(storageFormat); + } + finally { + dropTable(temporaryCreateTable); + } + } + } + + @Test + public void testSampledTableCreation() + throws Exception + { + try { + doCreateSampledTable(); + } + finally { + dropTable(temporaryCreateSampledTable); + } + } + + @Test + public void testEmptyTableCreation() + throws Exception + { + for (HiveStorageFormat storageFormat : createTableFormats) { + try { + doCreateEmptyTable(storageFormat); + } + finally { + dropTable(temporaryCreateEmptyTable); + } + } + } + + @Test + public void testViewCreation() + { + try { + verifyViewCreation(); + } + finally { + try { + metadata.dropView(SESSION, temporaryCreateView); + } + catch (RuntimeException e) { + // this usually occurs because the view was not created + } + } + } + + @Test + public void testCreateTableUnsupportedType() + { + for (HiveStorageFormat storageFormat : createTableFormats) { + try { + ConnectorSession session = createSession(storageFormat); + List columns = ImmutableList.of(new ColumnMetadata("dummy", HYPER_LOG_LOG, false)); + ConnectorTableMetadata tableMetadata = new ConnectorTableMetadata(invalidTable, columns, session.getUser()); + metadata.beginCreateTable(session, tableMetadata); + fail("create table with unsupported type should fail for storage format " + storageFormat); + } + catch (PrestoException e) { + assertEquals(e.getErrorCode(), NOT_SUPPORTED.toErrorCode()); + } + } + } + + private void createDummyTable(SchemaTableName tableName) + { + List columns = ImmutableList.of(new ColumnMetadata("dummy", VARCHAR, false)); + ConnectorTableMetadata tableMetadata = new ConnectorTableMetadata(tableName, columns, SESSION.getUser()); + ConnectorOutputTableHandle handle = metadata.beginCreateTable(SESSION, tableMetadata); + metadata.commitCreateTable(handle, ImmutableList.of()); + } + + private void verifyViewCreation() + { + // replace works for new view + doCreateView(temporaryCreateView, true); + + // replace works for existing view + doCreateView(temporaryCreateView, true); + + // create fails for existing view + try { + doCreateView(temporaryCreateView, false); + fail("create existing should fail"); + } + catch (ViewAlreadyExistsException e) { + assertEquals(e.getViewName(), temporaryCreateView); + } + + // drop works when view exists + metadata.dropView(SESSION, temporaryCreateView); + assertEquals(metadata.getViews(SESSION, temporaryCreateView.toSchemaTablePrefix()).size(), 0); + assertFalse(metadata.listViews(SESSION, temporaryCreateView.getSchemaName()).contains(temporaryCreateView)); + + // drop fails when view does not exist + try { + metadata.dropView(SESSION, temporaryCreateView); + fail("drop non-existing should fail"); + } + catch (ViewNotFoundException e) { + assertEquals(e.getViewName(), temporaryCreateView); + } + + // create works for new view + doCreateView(temporaryCreateView, false); + } + + private void doCreateView(SchemaTableName viewName, boolean replace) + { + String viewData = "test data"; + + metadata.createView(SESSION, viewName, viewData, replace); + + Map views = metadata.getViews(SESSION, viewName.toSchemaTablePrefix()); + assertEquals(views.size(), 1); + assertEquals(views.get(viewName), viewData); + + assertTrue(metadata.listViews(SESSION, viewName.getSchemaName()).contains(viewName)); + } + + private void doCreateSampledTable() + throws Exception + { + // begin creating the table + List columns = ImmutableList.builder() + .add(new ColumnMetadata("sales", BIGINT, false)) + .build(); + + ConnectorTableMetadata tableMetadata = new ConnectorTableMetadata(temporaryCreateSampledTable, columns, SESSION.getUser(), true); + ConnectorOutputTableHandle outputHandle = metadata.beginCreateTable(SESSION, tableMetadata); + + // write the records + RecordSink sink = recordSinkProvider.getRecordSink(outputHandle); + + sink.beginRecord(8); + sink.appendLong(2); + sink.finishRecord(); + + sink.beginRecord(5); + sink.appendLong(3); + sink.finishRecord(); + + sink.beginRecord(7); + sink.appendLong(4); + sink.finishRecord(); + + Collection fragments = sink.commit(); + + // commit the table + metadata.commitCreateTable(outputHandle, fragments); + + // load the new table + ConnectorTableHandle tableHandle = getTableHandle(temporaryCreateSampledTable); + List columnHandles = ImmutableList.builder() + .addAll(metadata.getColumnHandles(tableHandle).values()) + .add(metadata.getSampleWeightColumnHandle(tableHandle)) + .build(); + assertEquals(columnHandles.size(), 2); + + // verify the metadata + tableMetadata = metadata.getTableMetadata(getTableHandle(temporaryCreateSampledTable)); + assertEquals(tableMetadata.getOwner(), SESSION.getUser()); + + Map columnMap = uniqueIndex(tableMetadata.getColumns(), ColumnMetadata::getName); + assertEquals(columnMap.size(), 1); + + assertPrimitiveField(columnMap, 0, "sales", BIGINT, false); + + // verify the data + ConnectorPartitionResult partitionResult = splitManager.getPartitions(tableHandle, TupleDomain.all()); + assertEquals(partitionResult.getPartitions().size(), 1); + ConnectorSplitSource splitSource = splitManager.getPartitionSplits(tableHandle, partitionResult.getPartitions()); + ConnectorSplit split = getOnlyElement(getAllSplits(splitSource)); + + try (ConnectorPageSource pageSource = pageSourceProvider.createPageSource(split, columnHandles)) { + assertPageSourceType(pageSource, RCBINARY); + MaterializedResult result = materializeSourceDataStream(SESSION, pageSource, getTypes(columnHandles)); + assertEquals(result.getRowCount(), 3); + + MaterializedRow row; + + row = result.getMaterializedRows().get(0); + assertEquals(row.getField(0), 2L); + assertEquals(row.getField(1), 8L); + + row = result.getMaterializedRows().get(1); + assertEquals(row.getField(0), 3L); + assertEquals(row.getField(1), 5L); + + row = result.getMaterializedRows().get(2); + assertEquals(row.getField(0), 4L); + assertEquals(row.getField(1), 7L); + } + } + + private void doCreateTable(HiveStorageFormat storageFormat) + throws Exception + { + // begin creating the table + List columns = ImmutableList.builder() + .add(new ColumnMetadata("id", BIGINT, false)) + .add(new ColumnMetadata("t_string", VARCHAR, false)) + .add(new ColumnMetadata("t_bigint", BIGINT, false)) + .add(new ColumnMetadata("t_double", DOUBLE, false)) + .add(new ColumnMetadata("t_boolean", BOOLEAN, false)) + .build(); + + ConnectorTableMetadata tableMetadata = new ConnectorTableMetadata(temporaryCreateTable, columns, SESSION.getUser()); + + ConnectorSession session = createSession(storageFormat); + + ConnectorOutputTableHandle outputHandle = metadata.beginCreateTable(session, tableMetadata); + + // write the records + RecordSink sink = recordSinkProvider.getRecordSink(outputHandle); + + sink.beginRecord(1); + sink.appendLong(1); + sink.appendString("hello".getBytes(UTF_8)); + sink.appendLong(123); + sink.appendDouble(43.5); + sink.appendBoolean(true); + sink.finishRecord(); + + sink.beginRecord(1); + sink.appendLong(2); + sink.appendNull(); + sink.appendNull(); + sink.appendNull(); + sink.appendNull(); + sink.finishRecord(); + + sink.beginRecord(1); + sink.appendLong(3); + sink.appendString("bye".getBytes(UTF_8)); + sink.appendLong(456); + sink.appendDouble(98.1); + sink.appendBoolean(false); + sink.finishRecord(); + + Collection fragments = sink.commit(); + + // commit the table + metadata.commitCreateTable(outputHandle, fragments); + + // load the new table + ConnectorTableHandle tableHandle = getTableHandle(temporaryCreateTable); + List columnHandles = ImmutableList.copyOf(metadata.getColumnHandles(tableHandle).values()); + + // verify the metadata + tableMetadata = metadata.getTableMetadata(getTableHandle(temporaryCreateTable)); + assertEquals(tableMetadata.getOwner(), session.getUser()); + + Map columnMap = uniqueIndex(tableMetadata.getColumns(), ColumnMetadata::getName); + + assertPrimitiveField(columnMap, 0, "id", BIGINT, false); + assertPrimitiveField(columnMap, 1, "t_string", VARCHAR, false); + assertPrimitiveField(columnMap, 2, "t_bigint", BIGINT, false); + assertPrimitiveField(columnMap, 3, "t_double", DOUBLE, false); + assertPrimitiveField(columnMap, 4, "t_boolean", BOOLEAN, false); + + // verify the data + ConnectorPartitionResult partitionResult = splitManager.getPartitions(tableHandle, TupleDomain.all()); + assertEquals(partitionResult.getPartitions().size(), 1); + ConnectorSplitSource splitSource = splitManager.getPartitionSplits(tableHandle, partitionResult.getPartitions()); + ConnectorSplit split = getOnlyElement(getAllSplits(splitSource)); + + try (ConnectorPageSource pageSource = pageSourceProvider.createPageSource(split, columnHandles)) { + assertPageSourceType(pageSource, storageFormat); + MaterializedResult result = materializeSourceDataStream(session, pageSource, getTypes(columnHandles)); + assertEquals(result.getRowCount(), 3); + + MaterializedRow row; + + row = result.getMaterializedRows().get(0); + assertEquals(row.getField(0), 1L); + assertEquals(row.getField(1), "hello"); + assertEquals(row.getField(2), 123L); + assertEquals(row.getField(3), 43.5); + assertEquals(row.getField(4), true); + + row = result.getMaterializedRows().get(1); + assertEquals(row.getField(0), 2L); + assertNull(row.getField(1)); + assertNull(row.getField(2)); + assertNull(row.getField(3)); + assertNull(row.getField(4)); + + row = result.getMaterializedRows().get(2); + assertEquals(row.getField(0), 3L); + assertEquals(row.getField(1), "bye"); + assertEquals(row.getField(2), 456L); + assertEquals(row.getField(3), 98.1); + assertEquals(row.getField(4), false); + } + } + + private void doCreateEmptyTable(HiveStorageFormat storageFormat) + throws Exception + { + // create the table + Type arrayStringType = requireNonNull(TYPE_MANAGER.getType(parseTypeSignature("array"))); + List columns = ImmutableList.builder() + .add(new ColumnMetadata("id", BIGINT, false)) + .add(new ColumnMetadata("t_string", VARCHAR, false)) + .add(new ColumnMetadata("t_bigint", BIGINT, false)) + .add(new ColumnMetadata("t_double", DOUBLE, false)) + .add(new ColumnMetadata("t_boolean", BOOLEAN, false)) + .add(new ColumnMetadata("t_array_string", arrayStringType, false)) + .build(); + + ConnectorTableMetadata tableMetadata = new ConnectorTableMetadata(temporaryCreateEmptyTable, columns, SESSION.getUser()); + + ConnectorSession session = createSession(storageFormat); + + metadata.createTable(session, tableMetadata); + + // load the new table + ConnectorTableHandle tableHandle = getTableHandle(temporaryCreateEmptyTable); + + // verify the metadata + tableMetadata = metadata.getTableMetadata(getTableHandle(temporaryCreateEmptyTable)); + assertEquals(tableMetadata.getOwner(), session.getUser()); + + Map columnMap = uniqueIndex(tableMetadata.getColumns(), ColumnMetadata::getName); + + assertPrimitiveField(columnMap, 0, "id", BIGINT, false); + assertPrimitiveField(columnMap, 1, "t_string", VARCHAR, false); + assertPrimitiveField(columnMap, 2, "t_bigint", BIGINT, false); + assertPrimitiveField(columnMap, 3, "t_double", DOUBLE, false); + assertPrimitiveField(columnMap, 4, "t_boolean", BOOLEAN, false); + assertPrimitiveField(columnMap, 5, "t_array_string", arrayStringType, false); + + // verify the table is empty + ConnectorPartitionResult partitionResult = splitManager.getPartitions(tableHandle, TupleDomain.all()); + assertEquals(partitionResult.getPartitions().size(), 1); + ConnectorSplitSource splitSource = splitManager.getPartitionSplits(tableHandle, partitionResult.getPartitions()); + assertEquals(getAllSplits(splitSource).size(), 0); + } + + protected void assertGetRecordsOptional(String tableName, HiveStorageFormat hiveStorageFormat) + throws Exception + { + if (metadata.getTableHandle(SESSION, new SchemaTableName(database, tableName)) != null) { + assertGetRecords(tableName, hiveStorageFormat); + } + } + + protected void assertGetRecords(String tableName, HiveStorageFormat hiveStorageFormat) + throws Exception + { + ConnectorTableHandle tableHandle = getTableHandle(new SchemaTableName(database, tableName)); + ConnectorTableMetadata tableMetadata = metadata.getTableMetadata(tableHandle); + HiveSplit hiveSplit = getHiveSplit(tableHandle); + + List columnHandles = ImmutableList.copyOf(metadata.getColumnHandles(tableHandle).values()); + + ConnectorPageSource pageSource = pageSourceProvider.createPageSource(hiveSplit, columnHandles); + assertGetRecords(hiveStorageFormat, tableMetadata, hiveSplit, pageSource, columnHandles); + } + + protected HiveSplit getHiveSplit(ConnectorTableHandle tableHandle) + throws InterruptedException + { + ConnectorPartitionResult partitionResult = splitManager.getPartitions(tableHandle, TupleDomain.all()); + List splits = getAllSplits(splitManager.getPartitionSplits(tableHandle, partitionResult.getPartitions())); + assertEquals(splits.size(), 1); + return checkType(getOnlyElement(splits), HiveSplit.class, "split"); + } + + protected void assertGetRecords( + HiveStorageFormat hiveStorageFormat, + ConnectorTableMetadata tableMetadata, + HiveSplit hiveSplit, + ConnectorPageSource pageSource, + List columnHandles) + throws IOException + { + try { + MaterializedResult result = materializeSourceDataStream(SESSION, pageSource, getTypes(columnHandles)); + + assertPageSourceType(pageSource, hiveStorageFormat); + + ImmutableMap columnIndex = indexColumns(tableMetadata); + + long rowNumber = 0; + long completedBytes = 0; + for (MaterializedRow row : result) { + try { + assertValueTypes(row, tableMetadata.getColumns()); + } + catch (RuntimeException e) { + throw new RuntimeException("row " + rowNumber, e); + } + + rowNumber++; + Integer index; + + // STRING + index = columnIndex.get("t_string"); + if ((rowNumber % 19) == 0) { + assertNull(row.getField(index)); + } + else { + assertEquals(row.getField(index), ((rowNumber % 19) == 1) ? "" : "test"); + } + + // NUMBERS + assertEquals(row.getField(columnIndex.get("t_tinyint")), 1 + rowNumber); + assertEquals(row.getField(columnIndex.get("t_smallint")), 2 + rowNumber); + assertEquals(row.getField(columnIndex.get("t_int")), 3 + rowNumber); + + index = columnIndex.get("t_bigint"); + if ((rowNumber % 13) == 0) { + assertNull(row.getField(index)); + } + else { + assertEquals(row.getField(index), 4 + rowNumber); + } + + assertEquals((Double) row.getField(columnIndex.get("t_float")), 5.1 + rowNumber, 0.001); + assertEquals(row.getField(columnIndex.get("t_double")), 6.2 + rowNumber); + + // BOOLEAN + index = columnIndex.get("t_boolean"); + if ((rowNumber % 3) == 2) { + assertNull(row.getField(index)); + } + else { + assertEquals(row.getField(index), (rowNumber % 3) != 0); + } + + // TIMESTAMP + index = columnIndex.get("t_timestamp"); + if (index != null) { + if ((rowNumber % 17) == 0) { + assertNull(row.getField(index)); + } + else { + SqlTimestamp expected = new SqlTimestamp(new DateTime(2011, 5, 6, 7, 8, 9, 123, timeZone).getMillis(), UTC_KEY); + assertEquals(row.getField(index), expected); + } + } + + // BINARY + index = columnIndex.get("t_binary"); + if (index != null) { + if ((rowNumber % 23) == 0) { + assertNull(row.getField(index)); + } + else { + assertEquals(row.getField(index), new SqlVarbinary("test binary".getBytes(UTF_8))); + } + } + + // DATE + index = columnIndex.get("t_date"); + if (index != null) { + if ((rowNumber % 37) == 0) { + assertNull(row.getField(index)); + } + else { + SqlDate expected = new SqlDate(Ints.checkedCast(TimeUnit.MILLISECONDS.toDays(new DateTime(2013, 8, 9, 0, 0, 0, DateTimeZone.UTC).getMillis()))); + assertEquals(row.getField(index), expected); + } + } + + /* TODO: enable these tests when the types are supported + // VARCHAR(50) + index = columnIndex.get("t_varchar"); + if (index != null) { + if ((rowNumber % 39) == 0) { + assertTrue(cursor.isNull(index)); + } + else { + String stringValue = cursor.getSlice(index).toStringUtf8(); + assertEquals(stringValue, ((rowNumber % 39) == 1) ? "" : "test varchar"); + } + } + + // CHAR(25) + index = columnIndex.get("t_char"); + if (index != null) { + if ((rowNumber % 41) == 0) { + assertTrue(cursor.isNull(index)); + } + else { + String stringValue = cursor.getSlice(index).toStringUtf8(); + assertEquals(stringValue, ((rowNumber % 41) == 1) ? "" : "test char"); + } + } + */ + + // MAP + index = columnIndex.get("t_map"); + if (index != null) { + if ((rowNumber % 27) == 0) { + assertNull(row.getField(index)); + } + else { + assertEquals(row.getField(index), ImmutableMap.of("test key", "test value")); + } + } + + // ARRAY + index = columnIndex.get("t_array_string"); + if (index != null) { + if ((rowNumber % 29) == 0) { + assertNull(row.getField(index)); + } + else { + assertEquals(row.getField(index), ImmutableList.of("abc", "xyz", "data")); + } + } + + // ARRAY> + index = columnIndex.get("t_array_struct"); + if (index != null) { + if ((rowNumber % 31) == 0) { + assertNull(row.getField(index)); + } + else { + List expected1 = ImmutableList.of("test abc", 0.1); + List expected2 = ImmutableList.of("test xyz", 0.2); + assertEquals(row.getField(index), ImmutableList.of(expected1, expected2)); + } + } + + // MAP>> + index = columnIndex.get("t_complex"); + if (index != null) { + if ((rowNumber % 33) == 0) { + assertNull(row.getField(index)); + } + else { + List expected1 = ImmutableList.of("test abc", 0.1); + List expected2 = ImmutableList.of("test xyz", 0.2); + assertEquals(row.getField(index), ImmutableMap.of(1L, ImmutableList.of(expected1, expected2))); + } + } + + // NEW COLUMN + assertNull(row.getField(columnIndex.get("new_column"))); + + long newCompletedBytes = pageSource.getCompletedBytes(); + assertTrue(newCompletedBytes >= completedBytes); + assertTrue(newCompletedBytes <= hiveSplit.getLength()); + completedBytes = newCompletedBytes; + } + + assertTrue(completedBytes <= hiveSplit.getLength()); + assertEquals(rowNumber, 100); + } + finally { + pageSource.close(); + } + } + + private void dropTable(SchemaTableName table) + { + try { + ConnectorTableHandle handle = metadata.getTableHandle(SESSION, table); + if (handle != null) { + metadata.dropTable(handle); + } + } + catch (RuntimeException e) { + Logger.get(getClass()).warn(e, "failed to drop table"); + } + } + + protected ConnectorTableHandle getTableHandle(SchemaTableName tableName) + { + ConnectorTableHandle handle = metadata.getTableHandle(SESSION, tableName); + checkArgument(handle != null, "table not found: %s", tableName); + return handle; + } + + protected static int getSplitCount(ConnectorSplitSource splitSource) + throws InterruptedException + { + int splitCount = 0; + while (!splitSource.isFinished()) { + List batch = getFutureValue(splitSource.getNextBatch(1000)); + splitCount += batch.size(); + } + return splitCount; + } + + protected static List getAllSplits(ConnectorSplitSource splitSource) + throws InterruptedException + { + ImmutableList.Builder splits = ImmutableList.builder(); + while (!splitSource.isFinished()) { + List batch = getFutureValue(splitSource.getNextBatch(1000)); + splits.addAll(batch); + } + return splits.build(); + } + + protected static void assertPageSourceType(ConnectorPageSource pageSource, HiveStorageFormat hiveStorageFormat) + { + if (pageSource instanceof RecordPageSource) { + assertInstanceOf(((RecordPageSource) pageSource).getCursor(), recordCursorType(hiveStorageFormat), hiveStorageFormat.name()); + } + else { + assertInstanceOf(pageSource, pageSourceType(hiveStorageFormat), hiveStorageFormat.name()); + } + } + + private static Class recordCursorType(HiveStorageFormat hiveStorageFormat) + { + switch (hiveStorageFormat) { + case RCTEXT: + return ColumnarTextHiveRecordCursor.class; + case RCBINARY: + return ColumnarBinaryHiveRecordCursor.class; + case ORC: + return OrcHiveRecordCursor.class; + case PARQUET: + return ParquetHiveRecordCursor.class; + case DWRF: + return DwrfHiveRecordCursor.class; + } + return GenericHiveRecordCursor.class; + } + + private static Class pageSourceType(HiveStorageFormat hiveStorageFormat) + { + switch (hiveStorageFormat) { + case RCTEXT: + case RCBINARY: + return RcFilePageSource.class; + case ORC: + case DWRF: + return OrcPageSource.class; + default: + throw new AssertionError("Filed type " + hiveStorageFormat + " does not use a page source"); + } + } + + private static void assertValueTypes(MaterializedRow row, List schema) + { + for (int columnIndex = 0; columnIndex < schema.size(); columnIndex++) { + ColumnMetadata column = schema.get(columnIndex); + Object value = row.getField(columnIndex); + if (value != null) { + if (BOOLEAN.equals(column.getType())) { + assertInstanceOf(value, Boolean.class); + } + else if (BIGINT.equals(column.getType())) { + assertInstanceOf(value, Long.class); + } + else if (DOUBLE.equals(column.getType())) { + assertInstanceOf(value, Double.class); + } + else if (VARCHAR.equals(column.getType())) { + assertInstanceOf(value, String.class); + } + else if (VARBINARY.equals(column.getType())) { + assertInstanceOf(value, SqlVarbinary.class); + } + else if (TIMESTAMP.equals(column.getType())) { + assertInstanceOf(value, SqlTimestamp.class); + } + else if (DATE.equals(column.getType())) { + assertInstanceOf(value, SqlDate.class); + } + else if (column.getType() instanceof ArrayType) { + assertInstanceOf(value, List.class); + } + else if (column.getType() instanceof MapType) { + assertInstanceOf(value, Map.class); + } + else { + fail("Unknown primitive type " + columnIndex); + } + } + } + } + + private static void assertPrimitiveField(Map map, int position, String name, Type type, boolean partitionKey) + { + assertTrue(map.containsKey(name)); + ColumnMetadata column = map.get(name); + assertEquals(column.getType(), type, name); + assertEquals(column.isPartitionKey(), partitionKey, name); + } + + protected static ImmutableMap indexColumns(List columnHandles) + { + ImmutableMap.Builder index = ImmutableMap.builder(); + int i = 0; + for (ColumnHandle columnHandle : columnHandles) { + HiveColumnHandle hiveColumnHandle = checkType(columnHandle, HiveColumnHandle.class, "columnHandle"); + index.put(hiveColumnHandle.getName(), i); + i++; + } + return index.build(); + } + + protected static ImmutableMap indexColumns(ConnectorTableMetadata tableMetadata) + { + ImmutableMap.Builder index = ImmutableMap.builder(); + int i = 0; + for (ColumnMetadata columnMetadata : tableMetadata.getColumns()) { + index.put(columnMetadata.getName(), i); + i++; + } + return index.build(); + } + + private static ConnectorSession createSession(HiveStorageFormat storageFormat) + { + return new ConnectorSession( + SESSION.getUser(), + SESSION.getTimeZoneKey(), + SESSION.getLocale(), + SESSION.getStartTime(), + ImmutableMap.of(STORAGE_FORMAT_PROPERTY, storageFormat.name().toLowerCase())); + } + + private static String randomName() + { + return UUID.randomUUID().toString().toLowerCase(ENGLISH).replace("-", ""); + } +} diff --git a/presto-hive/src/test/java/com/facebook/presto/hive/AbstractTestHiveClientS3.java b/presto-hive/src/test/java/com/facebook/presto/hive/AbstractTestHiveClientS3.java new file mode 100644 index 00000000..4cb99873 --- /dev/null +++ b/presto-hive/src/test/java/com/facebook/presto/hive/AbstractTestHiveClientS3.java @@ -0,0 +1,445 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.hive.metastore.CachingHiveMetastore; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ColumnMetadata; +import com.facebook.presto.spi.ConnectorPageSource; +import com.facebook.presto.spi.ConnectorPageSourceProvider; +import com.facebook.presto.spi.ConnectorPartitionResult; +import com.facebook.presto.spi.ConnectorRecordSinkProvider; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.ConnectorSplitManager; +import com.facebook.presto.spi.ConnectorSplitSource; +import com.facebook.presto.spi.ConnectorTableHandle; +import com.facebook.presto.spi.ConnectorTableMetadata; +import com.facebook.presto.spi.RecordSink; +import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.spi.TupleDomain; +import com.facebook.presto.testing.MaterializedResult; +import com.facebook.presto.testing.MaterializedRow; +import com.facebook.presto.type.TypeRegistry; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.net.HostAndPort; +import io.airlift.slice.Slice; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.hive.metastore.api.Database; +import org.apache.hadoop.hive.metastore.api.NoSuchObjectException; +import org.apache.hadoop.hive.metastore.api.Table; +import org.apache.thrift.TException; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ExecutorService; + +import static com.facebook.presto.hadoop.HadoopFileStatus.isDirectory; +import static com.facebook.presto.hive.HiveTestUtils.DEFAULT_HIVE_DATA_STREAM_FACTORIES; +import static com.facebook.presto.hive.HiveTestUtils.DEFAULT_HIVE_RECORD_CURSOR_PROVIDER; +import static com.facebook.presto.hive.HiveTestUtils.TYPE_MANAGER; +import static com.facebook.presto.hive.HiveTestUtils.getTypes; +import static com.facebook.presto.hive.util.Types.checkType; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.TimeZoneKey.UTC_KEY; +import static com.facebook.presto.testing.MaterializedResult.materializeSourceDataStream; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.collect.Iterables.getOnlyElement; +import static com.google.common.util.concurrent.MoreExecutors.newDirectExecutorService; +import static io.airlift.concurrent.MoreFutures.getFutureValue; +import static io.airlift.concurrent.Threads.daemonThreadsNamed; +import static java.lang.String.format; +import static java.util.Locale.ENGLISH; +import static java.util.concurrent.Executors.newCachedThreadPool; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +@Test(groups = "hive-s3") +public abstract class AbstractTestHiveClientS3 +{ + private static final ConnectorSession SESSION = new ConnectorSession("user", UTC_KEY, ENGLISH, System.currentTimeMillis(), null); + + protected String writableBucket; + + protected String database; + protected SchemaTableName tableS3; + protected SchemaTableName temporaryCreateTable; + + protected HdfsEnvironment hdfsEnvironment; + protected TestingHiveMetastore metastoreClient; + protected HiveMetadata metadata; + protected ConnectorSplitManager splitManager; + protected ConnectorRecordSinkProvider recordSinkProvider; + protected ConnectorPageSourceProvider pageSourceProvider; + + private ExecutorService executor; + + @BeforeClass + public void setUp() + throws Exception + { + executor = newCachedThreadPool(daemonThreadsNamed("hive-%s")); + } + + @AfterClass + public void tearDown() + throws Exception + { + if (executor != null) { + executor.shutdownNow(); + executor = null; + } + } + + protected void setupHive(String databaseName) + { + database = databaseName; + tableS3 = new SchemaTableName(database, "presto_test_s3"); + + String random = UUID.randomUUID().toString().toLowerCase(ENGLISH).replace("-", ""); + temporaryCreateTable = new SchemaTableName(database, "tmp_presto_test_create_s3_" + random); + } + + protected void setup(String host, int port, String databaseName, String awsAccessKey, String awsSecretKey, String writableBucket) + { + this.writableBucket = writableBucket; + + setupHive(databaseName); + + HiveClientConfig hiveClientConfig = new HiveClientConfig() + .setS3AwsAccessKey(awsAccessKey) + .setS3AwsSecretKey(awsSecretKey); + + String proxy = System.getProperty("hive.metastore.thrift.client.socks-proxy"); + if (proxy != null) { + hiveClientConfig.setMetastoreSocksProxy(HostAndPort.fromString(proxy)); + } + + HiveConnectorId connectorId = new HiveConnectorId("hive-test"); + HiveCluster hiveCluster = new TestingHiveCluster(hiveClientConfig, host, port); + ExecutorService executor = newCachedThreadPool(daemonThreadsNamed("hive-s3-%s")); + HdfsConfiguration hdfsConfiguration = new HiveHdfsConfiguration(new HdfsConfigurationUpdater(hiveClientConfig)); + + hdfsEnvironment = new HdfsEnvironment(hdfsConfiguration, hiveClientConfig); + metastoreClient = new TestingHiveMetastore(hiveCluster, executor, hiveClientConfig, writableBucket); + splitManager = new HiveSplitManager( + connectorId, + hiveClientConfig, + metastoreClient, + new NamenodeStats(), + hdfsEnvironment, + new HadoopDirectoryLister(), + executor); + metadata = new HiveMetadata( + connectorId, + hiveClientConfig, + metastoreClient, + hdfsEnvironment, + newDirectExecutorService(), + new TypeRegistry(), + splitManager); + recordSinkProvider = new HiveRecordSinkProvider(hdfsEnvironment); + pageSourceProvider = new HivePageSourceProvider(hiveClientConfig, hdfsEnvironment, DEFAULT_HIVE_RECORD_CURSOR_PROVIDER, DEFAULT_HIVE_DATA_STREAM_FACTORIES, TYPE_MANAGER); + } + + @Test + public void testGetRecordsS3() + throws Exception + { + ConnectorTableHandle table = getTableHandle(tableS3); + List columnHandles = ImmutableList.copyOf(metadata.getColumnHandles(table).values()); + Map columnIndex = indexColumns(columnHandles); + + ConnectorPartitionResult partitionResult = splitManager.getPartitions(table, TupleDomain.all()); + assertEquals(partitionResult.getPartitions().size(), 1); + ConnectorSplitSource splitSource = splitManager.getPartitionSplits(table, partitionResult.getPartitions()); + + long sum = 0; + + for (ConnectorSplit split : getAllSplits(splitSource)) { + try (ConnectorPageSource pageSource = pageSourceProvider.createPageSource(split, columnHandles)) { + MaterializedResult result = materializeSourceDataStream(SESSION, pageSource, getTypes(columnHandles)); + + for (MaterializedRow row : result) { + sum += (Long) row.getField(columnIndex.get("t_bigint")); + } + } + } + assertEquals(sum, 78300); + } + + @Test + public void testGetFileStatus() + throws Exception + { + Path basePath = new Path("s3://presto-test-hive/"); + Path tablePath = new Path(basePath, "presto_test_s3"); + Path filePath = new Path(tablePath, "test1.csv"); + FileSystem fs = hdfsEnvironment.getFileSystem(basePath); + + assertTrue(isDirectory(fs.getFileStatus(basePath))); + assertTrue(isDirectory(fs.getFileStatus(tablePath))); + assertFalse(isDirectory(fs.getFileStatus(filePath))); + assertFalse(fs.exists(new Path(basePath, "foo"))); + } + + @Test + public void testRename() + throws Exception + { + Path basePath = new Path(format("s3://%s/rename/%s/", writableBucket, UUID.randomUUID())); + FileSystem fs = hdfsEnvironment.getFileSystem(basePath); + assertFalse(fs.exists(basePath)); + + // create file foo.txt + Path path = new Path(basePath, "foo.txt"); + assertTrue(fs.createNewFile(path)); + assertTrue(fs.exists(path)); + + // rename foo.txt to bar.txt + Path newPath = new Path(basePath, "bar.txt"); + assertFalse(fs.exists(newPath)); + assertTrue(fs.rename(path, newPath)); + assertFalse(fs.exists(path)); + assertTrue(fs.exists(newPath)); + + // create file foo.txt and rename to bar.txt + assertTrue(fs.createNewFile(path)); + assertFalse(fs.rename(path, newPath)); + assertTrue(fs.exists(path)); + + // rename foo.txt to foo.txt + assertTrue(fs.rename(path, path)); + assertTrue(fs.exists(path)); + + // delete foo.txt + assertTrue(fs.delete(path, false)); + assertFalse(fs.exists(path)); + + // create directory source with file + Path source = new Path(basePath, "source"); + assertTrue(fs.createNewFile(new Path(source, "test.txt"))); + + // rename source to non-existing target + Path target = new Path(basePath, "target"); + assertFalse(fs.exists(target)); + assertTrue(fs.rename(source, target)); + assertFalse(fs.exists(source)); + assertTrue(fs.exists(target)); + + // create directory source with file + assertTrue(fs.createNewFile(new Path(source, "test.txt"))); + + // rename source to existing target + assertTrue(fs.rename(source, target)); + assertFalse(fs.exists(source)); + target = new Path(target, "source"); + assertTrue(fs.exists(target)); + assertTrue(fs.exists(new Path(target, "test.txt"))); + + // delete target + target = new Path(basePath, "target"); + assertTrue(fs.exists(target)); + assertTrue(fs.delete(target, true)); + assertFalse(fs.exists(target)); + + // cleanup + fs.delete(basePath, true); + } + + @Test + public void testTableCreation() + throws Exception + { + try { + doCreateTable(temporaryCreateTable, "presto_test"); + } + finally { + dropTable(temporaryCreateTable); + } + } + + private void doCreateTable(SchemaTableName tableName, String tableOwner) + throws Exception + { + // begin creating the table + List columns = ImmutableList.builder() + .add(new ColumnMetadata("id", BIGINT, false)) + .build(); + + ConnectorTableMetadata tableMetadata = new ConnectorTableMetadata(tableName, columns, tableOwner); + HiveOutputTableHandle outputHandle = metadata.beginCreateTable(SESSION, tableMetadata); + + // write the records + RecordSink sink = recordSinkProvider.getRecordSink(outputHandle); + + sink.beginRecord(1); + sink.appendLong(1); + sink.finishRecord(); + + sink.beginRecord(1); + sink.appendLong(3); + sink.finishRecord(); + + sink.beginRecord(1); + sink.appendLong(2); + sink.finishRecord(); + + Collection fragments = sink.commit(); + + // commit the table + metadata.commitCreateTable(outputHandle, fragments); + + // Hack to work around the metastore not being configured for S3. + // The metastore tries to validate the location when creating the + // table, which fails without explicit configuration for S3. + // We work around that by using a dummy location when creating the + // table and update it here to the correct S3 location. + metastoreClient.updateTableLocation(database, tableName.getTableName(), outputHandle.getTargetPath()); + + // load the new table + ConnectorTableHandle tableHandle = getTableHandle(tableName); + List columnHandles = ImmutableList.copyOf(metadata.getColumnHandles(tableHandle).values()); + + // verify the data + ConnectorPartitionResult partitionResult = splitManager.getPartitions(tableHandle, TupleDomain.all()); + assertEquals(partitionResult.getPartitions().size(), 1); + ConnectorSplitSource splitSource = splitManager.getPartitionSplits(tableHandle, partitionResult.getPartitions()); + ConnectorSplit split = getOnlyElement(getAllSplits(splitSource)); + + try (ConnectorPageSource pageSource = pageSourceProvider.createPageSource(split, columnHandles)) { + MaterializedResult result = materializeSourceDataStream(SESSION, pageSource, getTypes(columnHandles)); + assertEquals(result.getRowCount(), 3); + + MaterializedRow row; + + row = result.getMaterializedRows().get(0); + assertEquals(row.getField(0), 1L); + + row = result.getMaterializedRows().get(1); + assertEquals(row.getField(0), 3L); + + row = result.getMaterializedRows().get(2); + assertEquals(row.getField(0), 2L); + } + } + + private void dropTable(SchemaTableName table) + { + try { + metastoreClient.dropTable(table.getSchemaName(), table.getTableName()); + } + catch (RuntimeException e) { + // this usually occurs because the table was not created + } + } + + private ConnectorTableHandle getTableHandle(SchemaTableName tableName) + { + ConnectorTableHandle handle = metadata.getTableHandle(SESSION, tableName); + checkArgument(handle != null, "table not found: %s", tableName); + return handle; + } + + private static List getAllSplits(ConnectorSplitSource source) + throws InterruptedException + { + ImmutableList.Builder splits = ImmutableList.builder(); + while (!source.isFinished()) { + splits.addAll(getFutureValue(source.getNextBatch(1000))); + } + return splits.build(); + } + + private static ImmutableMap indexColumns(List columnHandles) + { + ImmutableMap.Builder index = ImmutableMap.builder(); + int i = 0; + for (ColumnHandle columnHandle : columnHandles) { + HiveColumnHandle hiveColumnHandle = checkType(columnHandle, HiveColumnHandle.class, "columnHandle"); + index.put(hiveColumnHandle.getName(), i); + i++; + } + return index.build(); + } + + private static class TestingHiveMetastore + extends CachingHiveMetastore + { + private final String writableBucket; + + public TestingHiveMetastore(HiveCluster hiveCluster, ExecutorService executor, HiveClientConfig hiveClientConfig, String writableBucket) + { + super(hiveCluster, executor, hiveClientConfig); + this.writableBucket = writableBucket; + } + + @Override + public Database getDatabase(String databaseName) + throws NoSuchObjectException + { + Database database = super.getDatabase(databaseName); + database.setLocationUri("s3://" + writableBucket + "/"); + return database; + } + + @Override + public void createTable(Table table) + { + // hack to work around the metastore not being configured for S3 + table.getSd().setLocation("/"); + super.createTable(table); + } + + @Override + public void dropTable(String databaseName, String tableName) + { + try { + // hack to work around the metastore not being configured for S3 + Table table = getTable(databaseName, tableName); + table.getSd().setLocation("/"); + try (HiveMetastoreClient client = clientProvider.createMetastoreClient()) { + client.alter_table(databaseName, tableName, table); + client.drop_table(databaseName, tableName, false); + } + } + catch (TException e) { + throw Throwables.propagate(e); + } + } + + public void updateTableLocation(String databaseName, String tableName, String location) + { + try { + Table table = getTable(databaseName, tableName); + table.getSd().setLocation(location); + try (HiveMetastoreClient client = clientProvider.createMetastoreClient()) { + client.alter_table(databaseName, tableName, table); + } + } + catch (TException e) { + throw Throwables.propagate(e); + } + } + } +} diff --git a/presto-hive/src/test/java/com/facebook/presto/hive/AbstractTestHiveFileFormats.java b/presto-hive/src/test/java/com/facebook/presto/hive/AbstractTestHiveFileFormats.java new file mode 100644 index 00000000..0bc7baa5 --- /dev/null +++ b/presto-hive/src/test/java/com/facebook/presto/hive/AbstractTestHiveFileFormats.java @@ -0,0 +1,515 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.spi.ConnectorPageSource; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.RecordCursor; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.block.BlockBuilderStatus; +import com.facebook.presto.spi.type.DateType; +import com.facebook.presto.spi.type.SqlDate; +import com.facebook.presto.spi.type.SqlTimestamp; +import com.facebook.presto.spi.type.SqlVarbinary; +import com.facebook.presto.spi.type.TimestampType; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.testing.MaterializedResult; +import com.facebook.presto.testing.MaterializedRow; +import com.facebook.presto.type.ArrayType; +import com.facebook.presto.type.TypeRegistry; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.airlift.slice.Slice; +import io.airlift.slice.Slices; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.hive.ql.exec.FileSinkOperator.RecordWriter; +import org.apache.hadoop.hive.ql.io.HiveOutputFormat; +import org.apache.hadoop.hive.serde2.ReaderWriterProfiler; +import org.apache.hadoop.hive.serde2.SerDe; +import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector.Category; +import org.apache.hadoop.hive.serde2.objectinspector.SettableStructObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.StructField; +import org.apache.hadoop.io.SequenceFile; +import org.apache.hadoop.io.Text; +import org.apache.hadoop.io.Writable; +import org.apache.hadoop.io.compress.CompressionCodec; +import org.apache.hadoop.io.compress.CompressionCodecFactory; +import org.apache.hadoop.mapred.FileSplit; +import org.apache.hadoop.mapred.JobConf; +import org.apache.hadoop.util.Progressable; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.joda.time.format.DateTimeFormat; +import org.testng.annotations.Test; + +import java.io.File; +import java.io.IOException; +import java.sql.Date; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.TimeUnit; + +import static com.facebook.presto.hive.HivePartitionKey.HIVE_DEFAULT_DYNAMIC_PARTITION; +import static com.facebook.presto.hive.HiveTestUtils.arraySliceOf; +import static com.facebook.presto.hive.HiveTestUtils.mapSliceOf; +import static com.facebook.presto.hive.HiveTestUtils.rowSliceOf; +import static com.facebook.presto.hive.HiveType.getType; +import static com.facebook.presto.hive.HiveUtil.isStructuralType; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.BooleanType.BOOLEAN; +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; +import static com.facebook.presto.spi.type.TimeZoneKey.UTC_KEY; +import static com.facebook.presto.spi.type.VarbinaryType.VARBINARY; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static com.facebook.presto.testing.MaterializedResult.materializeSourceDataStream; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Predicates.not; +import static com.google.common.collect.Iterables.filter; +import static com.google.common.collect.Iterables.transform; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Locale.ENGLISH; +import static org.apache.hadoop.hive.serde2.objectinspector.ObjectInspectorFactory.getStandardListObjectInspector; +import static org.apache.hadoop.hive.serde2.objectinspector.ObjectInspectorFactory.getStandardMapObjectInspector; +import static org.apache.hadoop.hive.serde2.objectinspector.ObjectInspectorFactory.getStandardStructObjectInspector; +import static org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory.javaBooleanObjectInspector; +import static org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory.javaByteArrayObjectInspector; +import static org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory.javaByteObjectInspector; +import static org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory.javaDateObjectInspector; +import static org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory.javaDoubleObjectInspector; +import static org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory.javaFloatObjectInspector; +import static org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory.javaIntObjectInspector; +import static org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory.javaLongObjectInspector; +import static org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory.javaShortObjectInspector; +import static org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory.javaStringObjectInspector; +import static org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory.javaTimestampObjectInspector; +import static org.apache.hadoop.mapreduce.lib.output.FileOutputFormat.COMPRESS_CODEC; +import static org.apache.hadoop.mapreduce.lib.output.FileOutputFormat.COMPRESS_TYPE; +import static org.joda.time.DateTimeZone.UTC; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +@Test(groups = "hive") +public abstract class AbstractTestHiveFileFormats +{ + private static final ConnectorSession SESSION = new ConnectorSession("user", UTC_KEY, ENGLISH, System.currentTimeMillis(), null); + + private static final double EPSILON = 0.001; + private static final TypeManager TYPE_MANAGER = new TypeRegistry(); + + private static final long DATE_MILLIS_UTC = new DateTime(2011, 5, 6, 0, 0, UTC).getMillis(); + private static final long DATE_DAYS = TimeUnit.MILLISECONDS.toDays(DATE_MILLIS_UTC); + private static final String DATE_STRING = DateTimeFormat.forPattern("yyyy-MM-dd").withZoneUTC().print(DATE_MILLIS_UTC); + private static final Date SQL_DATE = new Date(UTC.getMillisKeepLocal(DateTimeZone.getDefault(), DATE_MILLIS_UTC)); + + public static final long TIMESTAMP = new DateTime(2011, 5, 6, 7, 8, 9, 123).getMillis(); + public static final String TIMESTAMP_STRING = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss.SSS").print(TIMESTAMP); + + // TODO: support null values and determine if timestamp and binary are allowed as partition keys + public static final int NUM_ROWS = 1000; + public static final List TEST_COLUMNS = ImmutableList.builder() + .add(new TestColumn("p_empty_string", javaStringObjectInspector, "", Slices.EMPTY_SLICE, true)) + .add(new TestColumn("p_string", javaStringObjectInspector, "test", Slices.utf8Slice("test"), true)) + .add(new TestColumn("p_tinyint", javaByteObjectInspector, "1", 1L, true)) + .add(new TestColumn("p_smallint", javaShortObjectInspector, "2", 2L, true)) + .add(new TestColumn("p_int", javaIntObjectInspector, "3", 3L, true)) + .add(new TestColumn("p_bigint", javaLongObjectInspector, "4", 4L, true)) + .add(new TestColumn("p_float", javaFloatObjectInspector, "5.1", 5.1, true)) + .add(new TestColumn("p_double", javaDoubleObjectInspector, "6.2", 6.2, true)) + .add(new TestColumn("p_boolean", javaBooleanObjectInspector, "true", true, true)) + .add(new TestColumn("p_date", javaDateObjectInspector, DATE_STRING, DATE_DAYS, true)) + .add(new TestColumn("p_timestamp", javaTimestampObjectInspector, TIMESTAMP_STRING, TIMESTAMP, true)) +// .add(new TestColumn("p_binary", javaByteArrayObjectInspector, "test2", Slices.utf8Slice("test2"), true)) + .add(new TestColumn("p_null_string", javaStringObjectInspector, HIVE_DEFAULT_DYNAMIC_PARTITION, null, true)) + .add(new TestColumn("p_null_tinyint", javaByteObjectInspector, HIVE_DEFAULT_DYNAMIC_PARTITION, null, true)) + .add(new TestColumn("p_null_smallint", javaShortObjectInspector, HIVE_DEFAULT_DYNAMIC_PARTITION, null, true)) + .add(new TestColumn("p_null_int", javaIntObjectInspector, HIVE_DEFAULT_DYNAMIC_PARTITION, null, true)) + .add(new TestColumn("p_null_bigint", javaLongObjectInspector, HIVE_DEFAULT_DYNAMIC_PARTITION, null, true)) + .add(new TestColumn("p_null_float", javaFloatObjectInspector, HIVE_DEFAULT_DYNAMIC_PARTITION, null, true)) + .add(new TestColumn("p_null_double", javaDoubleObjectInspector, HIVE_DEFAULT_DYNAMIC_PARTITION, null, true)) + .add(new TestColumn("p_null_boolean", javaBooleanObjectInspector, HIVE_DEFAULT_DYNAMIC_PARTITION, null, true)) + .add(new TestColumn("p_null_date", javaDateObjectInspector, HIVE_DEFAULT_DYNAMIC_PARTITION, null, true)) + .add(new TestColumn("p_null_timestamp", javaTimestampObjectInspector, HIVE_DEFAULT_DYNAMIC_PARTITION, null, true)) +// .add(new TestColumn("p_null_binary", javaByteArrayObjectInspector, HIVE_DEFAULT_DYNAMIC_PARTITION, null, true)) + .add(new TestColumn("t_null_string", javaStringObjectInspector, null, null)) + .add(new TestColumn("t_null_array_int", getStandardListObjectInspector(javaIntObjectInspector), null, null)) + .add(new TestColumn("t_empty_string", javaStringObjectInspector, "", Slices.EMPTY_SLICE)) + .add(new TestColumn("t_string", javaStringObjectInspector, "test", Slices.utf8Slice("test"))) + .add(new TestColumn("t_tinyint", javaByteObjectInspector, (byte) 1, 1L)) + .add(new TestColumn("t_smallint", javaShortObjectInspector, (short) 2, 2L)) + .add(new TestColumn("t_int", javaIntObjectInspector, 3, 3L)) + .add(new TestColumn("t_bigint", javaLongObjectInspector, 4L, 4L)) + .add(new TestColumn("t_float", javaFloatObjectInspector, 5.1f, 5.1)) + .add(new TestColumn("t_double", javaDoubleObjectInspector, 6.2, 6.2)) + .add(new TestColumn("t_boolean_true", javaBooleanObjectInspector, true, true)) + .add(new TestColumn("t_boolean_false", javaBooleanObjectInspector, false, false)) + .add(new TestColumn("t_date", javaDateObjectInspector, SQL_DATE, DATE_DAYS)) + .add(new TestColumn("t_timestamp", javaTimestampObjectInspector, new Timestamp(TIMESTAMP), TIMESTAMP)) + .add(new TestColumn("t_binary", javaByteArrayObjectInspector, Slices.utf8Slice("test2"), Slices.utf8Slice("test2"))) + .add(new TestColumn("t_map_string", + getStandardMapObjectInspector(javaStringObjectInspector, javaStringObjectInspector), + ImmutableMap.of("test", "test"), + mapSliceOf(VARCHAR, VARCHAR, "test", "test"))) + .add(new TestColumn("t_map_tinyint", getStandardMapObjectInspector(javaByteObjectInspector, javaByteObjectInspector), ImmutableMap.of((byte) 1, (byte) 1), mapSliceOf(BIGINT, BIGINT, 1, 1))) + .add(new TestColumn("t_map_smallint", + getStandardMapObjectInspector(javaShortObjectInspector, javaShortObjectInspector), + ImmutableMap.of((short) 2, (short) 2), + mapSliceOf(BIGINT, BIGINT, 2, 2))) + .add(new TestColumn("t_map_null_key", getStandardMapObjectInspector(javaIntObjectInspector, javaIntObjectInspector), mapWithNullKey(), mapSliceOf(BIGINT, BIGINT, 2, 3))) + .add(new TestColumn("t_map_int", getStandardMapObjectInspector(javaIntObjectInspector, javaIntObjectInspector), ImmutableMap.of(3, 3), mapSliceOf(BIGINT, BIGINT, 3, 3))) + .add(new TestColumn("t_map_bigint", getStandardMapObjectInspector(javaLongObjectInspector, javaLongObjectInspector), ImmutableMap.of(4L, 4L), mapSliceOf(BIGINT, BIGINT, 4L, 4L))) + .add(new TestColumn("t_map_float", getStandardMapObjectInspector(javaFloatObjectInspector, javaFloatObjectInspector), ImmutableMap.of(5.0f, 5.0f), mapSliceOf(DOUBLE, DOUBLE, 5.0f, 5.0f))) + .add(new TestColumn("t_map_double", getStandardMapObjectInspector(javaDoubleObjectInspector, javaDoubleObjectInspector), ImmutableMap.of(6.0, 6.0), mapSliceOf(DOUBLE, DOUBLE, 6.0, 6.0))) + .add(new TestColumn("t_map_boolean", + getStandardMapObjectInspector(javaBooleanObjectInspector, javaBooleanObjectInspector), + ImmutableMap.of(true, true), + mapSliceOf(BOOLEAN, BOOLEAN, true, true))) + .add(new TestColumn("t_map_date", + getStandardMapObjectInspector(javaDateObjectInspector, javaDateObjectInspector), + ImmutableMap.of(SQL_DATE, SQL_DATE), + mapSliceOf(DateType.DATE, DateType.DATE, DATE_DAYS, DATE_DAYS))) + .add(new TestColumn("t_map_timestamp", + getStandardMapObjectInspector(javaTimestampObjectInspector, javaTimestampObjectInspector), + ImmutableMap.of(new Timestamp(TIMESTAMP), new Timestamp(TIMESTAMP)), + mapSliceOf(TimestampType.TIMESTAMP, TimestampType.TIMESTAMP, TIMESTAMP, TIMESTAMP))) + .add(new TestColumn("t_array_empty", getStandardListObjectInspector(javaStringObjectInspector), ImmutableList.of(), arraySliceOf(VARCHAR))) + .add(new TestColumn("t_array_string", getStandardListObjectInspector(javaStringObjectInspector), ImmutableList.of("test"), arraySliceOf(VARCHAR, "test"))) + .add(new TestColumn("t_array_tinyint", getStandardListObjectInspector(javaByteObjectInspector), ImmutableList.of((byte) 1), arraySliceOf(BIGINT, 1))) + .add(new TestColumn("t_array_smallint", getStandardListObjectInspector(javaShortObjectInspector), ImmutableList.of((short) 2), arraySliceOf(BIGINT, 2))) + .add(new TestColumn("t_array_int", getStandardListObjectInspector(javaIntObjectInspector), ImmutableList.of(3), arraySliceOf(BIGINT, 3))) + .add(new TestColumn("t_array_bigint", getStandardListObjectInspector(javaLongObjectInspector), ImmutableList.of(4L), arraySliceOf(BIGINT, 4L))) + .add(new TestColumn("t_array_float", getStandardListObjectInspector(javaFloatObjectInspector), ImmutableList.of(5.0f), arraySliceOf(DOUBLE, 5.0f))) + .add(new TestColumn("t_array_double", getStandardListObjectInspector(javaDoubleObjectInspector), ImmutableList.of(6.0), arraySliceOf(DOUBLE, 6.0))) + .add(new TestColumn("t_array_boolean", getStandardListObjectInspector(javaBooleanObjectInspector), ImmutableList.of(true), arraySliceOf(BOOLEAN, true))) + .add(new TestColumn("t_array_date", + getStandardListObjectInspector(javaDateObjectInspector), + ImmutableList.of(SQL_DATE), + arraySliceOf(DateType.DATE, DATE_DAYS))) + .add(new TestColumn("t_array_timestamp", + getStandardListObjectInspector(javaTimestampObjectInspector), + ImmutableList.of(new Timestamp(TIMESTAMP)), + arraySliceOf(TimestampType.TIMESTAMP, TIMESTAMP))) + .add(new TestColumn("t_struct_bigint", + getStandardStructObjectInspector(ImmutableList.of("s_bigint"), ImmutableList.of(javaLongObjectInspector)), + new Long[] {1L}, + arraySliceOf(BIGINT, 1))) + .add(new TestColumn("t_complex", + getStandardMapObjectInspector( + javaStringObjectInspector, + getStandardListObjectInspector( + getStandardStructObjectInspector( + ImmutableList.of("s_int"), + ImmutableList.of(javaIntObjectInspector) + ) + ) + ), + ImmutableMap.of("test", ImmutableList.of(new Integer[] {1})), + mapSliceOf(VARCHAR, new ArrayType(new ArrayType(BIGINT)), "test", arraySliceOf(new ArrayType(BIGINT), arraySliceOf(BIGINT, 1))) + )) + .add(new TestColumn("t_struct_nested", getStandardStructObjectInspector(ImmutableList.of("struct_field"), + ImmutableList.of(getStandardListObjectInspector(javaStringObjectInspector))), ImmutableList.of(ImmutableList.of("1", "2", "3")) , rowSliceOf(ImmutableList.of(new ArrayType(VARCHAR)), arraySliceOf(VARCHAR, "1", "2", "3")))) + .add(new TestColumn("t_struct_null", getStandardStructObjectInspector(ImmutableList.of("struct_field", "struct_field2"), + ImmutableList.of(javaStringObjectInspector, javaStringObjectInspector)), Arrays.asList(null, null), rowSliceOf(ImmutableList.of(VARCHAR, VARCHAR), null, null))) + .build(); + + private static Map mapWithNullKey() + { + Map map = new HashMap<>(); + map.put(null, 0); + map.put(2, 3); + return map; + } + + protected List getColumnHandles(List testColumns) + { + List columns = new ArrayList<>(); + int nextHiveColumnIndex = 0; + for (int i = 0; i < testColumns.size(); i++) { + TestColumn testColumn = testColumns.get(i); + int columnIndex = testColumn.isPartitionKey() ? -1 : nextHiveColumnIndex++; + + ObjectInspector inspector = testColumn.getObjectInspector(); + HiveType hiveType = HiveType.getHiveType(inspector); + Type type = getType(inspector, TYPE_MANAGER); + + columns.add(new HiveColumnHandle("client_id", testColumn.getName(), i, hiveType, type.getTypeSignature(), columnIndex, testColumn.isPartitionKey())); + } + return columns; + } + + public FileSplit createTestFile(String filePath, + HiveOutputFormat outputFormat, + @SuppressWarnings("deprecation") SerDe serDe, + String compressionCodec, + List testColumns, + int numRows) + throws Exception + { + // filter out partition keys, which are not written to the file + testColumns = ImmutableList.copyOf(filter(testColumns, not(TestColumn::isPartitionKey))); + + JobConf jobConf = new JobConf(); + ReaderWriterProfiler.setProfilerOptions(jobConf); + + Properties tableProperties = new Properties(); + tableProperties.setProperty("columns", Joiner.on(',').join(transform(testColumns, TestColumn::getName))); + tableProperties.setProperty("columns.types", Joiner.on(',').join(transform(testColumns, TestColumn::getType))); + serDe.initialize(new Configuration(), tableProperties); + + if (compressionCodec != null) { + CompressionCodec codec = new CompressionCodecFactory(new Configuration()).getCodecByName(compressionCodec); + jobConf.set(COMPRESS_CODEC, codec.getClass().getName()); + jobConf.set(COMPRESS_TYPE, SequenceFile.CompressionType.BLOCK.toString()); + jobConf.set("parquet.compression", compressionCodec); + jobConf.set("parquet.enable.dictionary", "true"); + } + + RecordWriter recordWriter = outputFormat.getHiveRecordWriter( + jobConf, + new Path(filePath), + Text.class, + compressionCodec != null, + tableProperties, + new Progressable() + { + @Override + public void progress() + { + } + } + ); + + try { + serDe.initialize(new Configuration(), tableProperties); + + SettableStructObjectInspector objectInspector = getStandardStructObjectInspector( + ImmutableList.copyOf(transform(testColumns, TestColumn::getName)), + ImmutableList.copyOf(transform(testColumns, TestColumn::getObjectInspector))); + + Object row = objectInspector.create(); + + List fields = ImmutableList.copyOf(objectInspector.getAllStructFieldRefs()); + + for (int rowNumber = 0; rowNumber < numRows; rowNumber++) { + for (int i = 0; i < testColumns.size(); i++) { + Object writeValue = testColumns.get(i).getWriteValue(); + if (writeValue instanceof Slice) { + writeValue = ((Slice) writeValue).getBytes(); + } + objectInspector.setStructFieldData(row, fields.get(i), writeValue); + } + + Writable record = serDe.serialize(row, objectInspector); + recordWriter.write(record); + } + } + finally { + recordWriter.close(false); + } + + Path path = new Path(filePath); + path.getFileSystem(new Configuration()).setVerifyChecksum(true); + File file = new File(filePath); + return new FileSplit(path, 0, file.length(), new String[0]); + } + + protected void checkCursor(RecordCursor cursor, List testColumns, int numRows) + throws IOException + { + for (int row = 0; row < numRows; row++) { + assertTrue(cursor.advanceNextPosition()); + for (int i = 0, testColumnsSize = testColumns.size(); i < testColumnsSize; i++) { + TestColumn testColumn = testColumns.get(i); + + Object fieldFromCursor; + Type type = getType(testColumn.getObjectInspector(), TYPE_MANAGER); + if (cursor.isNull(i)) { + fieldFromCursor = null; + } + else if (BOOLEAN.equals(type)) { + fieldFromCursor = cursor.getBoolean(i); + } + else if (BIGINT.equals(type)) { + fieldFromCursor = cursor.getLong(i); + } + else if (DOUBLE.equals(type)) { + fieldFromCursor = cursor.getDouble(i); + } + else if (VARCHAR.equals(type)) { + fieldFromCursor = cursor.getSlice(i); + } + else if (VARBINARY.equals(type)) { + fieldFromCursor = cursor.getSlice(i); + } + else if (DateType.DATE.equals(type)) { + fieldFromCursor = cursor.getLong(i); + } + else if (TimestampType.TIMESTAMP.equals(type)) { + fieldFromCursor = cursor.getLong(i); + } + else if (isStructuralType(type)) { + fieldFromCursor = cursor.getSlice(i); + } + else { + throw new RuntimeException("unknown type"); + } + + if (fieldFromCursor == null) { + assertEquals(null, testColumn.getExpectedValue(), String.format("Expected null for column %s", testColumn.getName())); + } + else if (testColumn.getObjectInspector().getTypeName().equals("float") || + testColumn.getObjectInspector().getTypeName().equals("double")) { + assertEquals((double) fieldFromCursor, (double) testColumn.getExpectedValue(), EPSILON); + } + else if (testColumn.getObjectInspector().getCategory() == Category.PRIMITIVE) { + assertEquals(fieldFromCursor, testColumn.getExpectedValue(), String.format("Wrong value for column %s", testColumn.getName())); + } + else { + Slice expected = (Slice) testColumn.getExpectedValue(); + Slice actual = (Slice) fieldFromCursor; + assertEquals(actual, expected, String.format("Wrong value for column %s", testColumn.getName())); + } + } + } + } + + protected void checkPageSource(ConnectorPageSource pageSource, List testColumns, List types) + throws IOException + { + try { + MaterializedResult result = materializeSourceDataStream(SESSION, pageSource, types); + + for (MaterializedRow row : result) { + for (int i = 0, testColumnsSize = testColumns.size(); i < testColumnsSize; i++) { + TestColumn testColumn = testColumns.get(i); + Type type = types.get(i); + + Object actualValue = row.getField(i); + Object expectedValue = testColumn.getExpectedValue(); + if (actualValue == null) { + assertEquals(null, expectedValue, String.format("Expected null for column %d", i)); + } + else if (testColumn.getObjectInspector().getTypeName().equals("float") || + testColumn.getObjectInspector().getTypeName().equals("double")) { + assertEquals((double) actualValue, (double) expectedValue, EPSILON); + } + else if (testColumn.getObjectInspector().getTypeName().equals("date")) { + SqlDate expectedDate = new SqlDate(((Long) expectedValue).intValue()); + assertEquals(actualValue, expectedDate); + } + else if (testColumn.getObjectInspector().getTypeName().equals("timestamp")) { + SqlTimestamp expectedTimestamp = new SqlTimestamp((Long) expectedValue, SESSION.getTimeZoneKey()); + assertEquals(actualValue, expectedTimestamp); + } + else if (testColumn.getObjectInspector().getCategory() == Category.PRIMITIVE) { + if (expectedValue instanceof Slice) { + expectedValue = ((Slice) expectedValue).toStringUtf8(); + } + + if (actualValue instanceof Slice) { + actualValue = ((Slice) actualValue).toStringUtf8(); + } + if (actualValue instanceof SqlVarbinary) { + actualValue = new String(((SqlVarbinary) actualValue).getBytes(), UTF_8); + } + assertEquals(actualValue, expectedValue, String.format("Wrong value for column %d", i)); + } + else { + BlockBuilder builder = type.createBlockBuilder(new BlockBuilderStatus(), 1, ((Slice) expectedValue).length()); + type.writeSlice(builder, (Slice) expectedValue); + expectedValue = type.getObjectValue(SESSION, builder.build(), 0); + assertEquals(actualValue, expectedValue, String.format("Wrong value for column %s", testColumn.getName())); + } + } + } + } + finally { + pageSource.close(); + } + } + + public static final class TestColumn + { + private final String name; + private final ObjectInspector objectInspector; + private final Object writeValue; + private final Object expectedValue; + private final boolean partitionKey; + + public TestColumn(String name, ObjectInspector objectInspector, Object writeValue, Object expectedValue) + { + this(name, objectInspector, writeValue, expectedValue, false); + } + + public TestColumn(String name, ObjectInspector objectInspector, Object writeValue, Object expectedValue, boolean partitionKey) + { + this.name = checkNotNull(name, "name is null"); + this.objectInspector = checkNotNull(objectInspector, "objectInspector is null"); + this.writeValue = writeValue; + this.expectedValue = expectedValue; + this.partitionKey = partitionKey; + } + + public String getName() + { + return name; + } + + public String getType() + { + return objectInspector.getTypeName(); + } + + public ObjectInspector getObjectInspector() + { + return objectInspector; + } + + public Object getWriteValue() + { + return writeValue; + } + + public Object getExpectedValue() + { + return expectedValue; + } + + public boolean isPartitionKey() + { + return partitionKey; + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder("TestColumn{"); + sb.append("name='").append(name).append('\''); + sb.append(", objectInspector=").append(objectInspector); + sb.append(", writeValue=").append(writeValue); + sb.append(", expectedValue=").append(expectedValue); + sb.append(", partitionKey=").append(partitionKey); + sb.append('}'); + return sb.toString(); + } + } +} diff --git a/presto-hive/src/test/java/com/facebook/presto/hive/AbstractTestSplitIteratorBackpressure.java b/presto-hive/src/test/java/com/facebook/presto/hive/AbstractTestSplitIteratorBackpressure.java new file mode 100644 index 00000000..5726d6f8 --- /dev/null +++ b/presto-hive/src/test/java/com/facebook/presto/hive/AbstractTestSplitIteratorBackpressure.java @@ -0,0 +1,25 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +public abstract class AbstractTestSplitIteratorBackpressure + extends AbstractTestHiveClient +{ + @Override + protected void setup(String host, int port, String databaseName, String timeZone) + { + // Restrict the outstanding splits to 1 and only use 2 threads per iterator + setup(host, port, databaseName, timeZone, "hive-push-backtest", 1, 2); + } +} diff --git a/presto-hive/src/test/java/com/facebook/presto/hive/BenchmarkHiveFileFormats.java b/presto-hive/src/test/java/com/facebook/presto/hive/BenchmarkHiveFileFormats.java new file mode 100644 index 00000000..036b1113 --- /dev/null +++ b/presto-hive/src/test/java/com/facebook/presto/hive/BenchmarkHiveFileFormats.java @@ -0,0 +1,1850 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.hadoop.HadoopNative; +import com.facebook.presto.hive.orc.DwrfPageSourceFactory; +import com.facebook.presto.hive.orc.DwrfRecordCursorProvider; +import com.facebook.presto.hive.orc.OrcPageSourceFactory; +import com.facebook.presto.hive.orc.OrcRecordCursorProvider; +import com.facebook.presto.hive.rcfile.RcFilePageSourceFactory; +import com.facebook.presto.spi.ConnectorPageSource; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.HostAddress; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.TupleDomain; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.type.TypeRegistry; +import com.google.common.base.Joiner; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; +import io.airlift.tpch.LineItem; +import io.airlift.tpch.LineItemColumn; +import io.airlift.tpch.LineItemGenerator; +import io.airlift.tpch.TpchColumn; +import io.airlift.units.DataSize; +import io.airlift.units.DataSize.Unit; +import io.airlift.units.Duration; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.hive.ql.exec.FileSinkOperator.RecordWriter; +import org.apache.hadoop.hive.ql.io.HiveOutputFormat; +import org.apache.hadoop.hive.ql.io.RCFileInputFormat; +import org.apache.hadoop.hive.ql.io.RCFileOutputFormat; +import org.apache.hadoop.hive.ql.io.parquet.MapredParquetInputFormat; +import org.apache.hadoop.hive.ql.io.parquet.MapredParquetOutputFormat; +import org.apache.hadoop.hive.ql.io.parquet.serde.ParquetHiveSerDe; +import org.apache.hadoop.hive.serde2.ReaderWriterProfiler; +import org.apache.hadoop.hive.serde2.SerDe; +import org.apache.hadoop.hive.serde2.Serializer; +import org.apache.hadoop.hive.serde2.columnar.ColumnarSerDe; +import org.apache.hadoop.hive.serde2.columnar.LazyBinaryColumnarSerDe; +import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.SettableStructObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.StructField; +import org.apache.hadoop.io.Text; +import org.apache.hadoop.io.Writable; +import org.apache.hadoop.io.compress.CompressionCodec; +import org.apache.hadoop.io.compress.CompressionCodecFactory; +import org.apache.hadoop.mapred.FileSplit; +import org.apache.hadoop.mapred.InputFormat; +import org.apache.hadoop.mapred.JobConf; +import org.apache.hadoop.util.Progressable; +import org.joda.time.DateTimeZone; +import parquet.Log; + +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; +import java.util.List; +import java.util.Properties; + +import static com.facebook.presto.hive.HiveType.getType; +import static com.facebook.presto.hive.HiveUtil.setReadColumns; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; +import static com.facebook.presto.spi.type.TimeZoneKey.UTC_KEY; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static com.google.common.collect.Lists.transform; +import static com.google.common.io.ByteStreams.nullOutputStream; +import static io.airlift.tpch.LineItemColumn.DISCOUNT; +import static io.airlift.tpch.LineItemColumn.EXTENDED_PRICE; +import static io.airlift.tpch.LineItemColumn.ORDER_KEY; +import static io.airlift.tpch.LineItemColumn.QUANTITY; +import static io.airlift.tpch.LineItemColumn.RETURN_FLAG; +import static io.airlift.tpch.LineItemColumn.SHIP_DATE; +import static io.airlift.tpch.LineItemColumn.SHIP_INSTRUCTIONS; +import static io.airlift.tpch.LineItemColumn.STATUS; +import static io.airlift.tpch.LineItemColumn.TAX; +import static java.util.Locale.ENGLISH; +import static java.util.concurrent.TimeUnit.NANOSECONDS; +import static org.apache.hadoop.hive.metastore.api.hive_metastoreConstants.FILE_INPUT_FORMAT; +import static org.apache.hadoop.hive.serde.serdeConstants.SERIALIZATION_LIB; +import static org.apache.hadoop.hive.serde2.objectinspector.ObjectInspectorFactory.getStandardStructObjectInspector; +import static org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory.javaDateObjectInspector; +import static org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory.javaDoubleObjectInspector; +import static org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory.javaLongObjectInspector; +import static org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory.javaStringObjectInspector; +import static org.apache.hadoop.mapreduce.lib.output.FileOutputFormat.COMPRESS_CODEC; +import static org.apache.hadoop.mapreduce.lib.output.FileOutputFormat.COMPRESS_TYPE; + +@SuppressWarnings("UseOfSystemOutOrSystemErr") +public final class BenchmarkHiveFileFormats +{ + private static final ConnectorSession SESSION = new ConnectorSession("user", UTC_KEY, ENGLISH, System.currentTimeMillis(), null); + + private static final List ENABLED_COMPRESSION = ImmutableList.builder() + .add(CompressionType.none) + .add(CompressionType.snappy) + .add(CompressionType.gzip) + .build(); + + private enum CompressionType + { + none(""), + snappy(".snappy"), + gzip(".gz"); + + private final String fileExtension; + + CompressionType(String fileExtension) + { + this.fileExtension = fileExtension; + } + + public String getFileExtension() + { + return fileExtension; + } + } + + private static final File DATA_DIR = new File("target"); + private static final JobConf JOB_CONF = new JobConf(); + private static final ImmutableList> COLUMNS = ImmutableList.copyOf(LineItemColumn.values()); + + private static final long FILTER_ORDER_KEY_ID = 300_000L; + private static final List BIGINT_COLUMN = getHiveColumnHandles(ORDER_KEY); + private static final List BIGINT_COLUMN_INDEX = ImmutableList.copyOf(transform(BIGINT_COLUMN, HiveColumnHandle::getHiveColumnIndex)); + + private static final List DOUBLE_COLUMN = getHiveColumnHandles(EXTENDED_PRICE); + private static final List DOUBLE_COLUMN_INDEX = ImmutableList.copyOf(transform(DOUBLE_COLUMN, HiveColumnHandle::getHiveColumnIndex)); + + private static final List VARCHAR_COLUMN = getHiveColumnHandles(SHIP_INSTRUCTIONS); + private static final List VARCHAR_COLUMN_INDEX = ImmutableList.copyOf(transform(VARCHAR_COLUMN, HiveColumnHandle::getHiveColumnIndex)); + + private static final List TPCH_6_COLUMNS = getHiveColumnHandles(QUANTITY, EXTENDED_PRICE, DISCOUNT, SHIP_DATE); + private static final List TPCH_6_COLUMN_INDEXES = ImmutableList.copyOf(transform(TPCH_6_COLUMNS, HiveColumnHandle::getHiveColumnIndex)); + + private static final List TPCH_1_COLUMNS = getHiveColumnHandles(QUANTITY, EXTENDED_PRICE, DISCOUNT, TAX, RETURN_FLAG, STATUS, SHIP_DATE); + private static final List TPCH_1_COLUMN_INDEXES = ImmutableList.copyOf(transform(TPCH_1_COLUMNS, HiveColumnHandle::getHiveColumnIndex)); + + private static final List ALL_COLUMNS = getHiveColumnHandles(LineItemColumn.values()); + private static final List ALL_COLUMN_INDEXES = ImmutableList.copyOf(transform(ALL_COLUMNS, HiveColumnHandle::getHiveColumnIndex)); + + private static final int LOOPS = 1; + private static final TypeRegistry TYPE_MANAGER = new TypeRegistry(); + + private BenchmarkHiveFileFormats() + { + } + + public static void main(String[] args) + throws Exception + { + workAroundParquetBrokenLoggingSetup(); + run(true, false); + } + + private static void run(boolean benchmarkReadSpeed, boolean benchmarkWriteSpeed) + throws Exception + { + HadoopNative.requireHadoopNative(); + ReaderWriterProfiler.setProfilerOptions(JOB_CONF); + DATA_DIR.mkdirs(); + + List benchmarkFiles = ImmutableList.builder() + .add(new BenchmarkFile( + "rc-binary", + new RCFileInputFormat<>(), + new RCFileOutputFormat(), + new LazyBinaryColumnarSerDe(), + ImmutableList.builder() + .add(new ColumnarBinaryHiveRecordCursorProvider()) + .build(), + ImmutableList.builder() + .add(new RcFilePageSourceFactory(TYPE_MANAGER)) + .build())) + + .add(new BenchmarkFile( + "rc-text", + new RCFileInputFormat<>(), + new RCFileOutputFormat(), + new ColumnarSerDe(), + ImmutableList.builder() + .add(new ColumnarTextHiveRecordCursorProvider()) + .build(), + ImmutableList.builder() + .add(new RcFilePageSourceFactory(TYPE_MANAGER)) + .build())) + + .add(new BenchmarkFile( + "parquet", + new MapredParquetInputFormat(), + new MapredParquetOutputFormat(), + new ParquetHiveSerDe(), + ImmutableList.builder() + .add(new ParquetRecordCursorProvider(false)) + .build(), + ImmutableList.builder() + .build())) + + .add(new BenchmarkFile( + "dwrf", + new com.facebook.hive.orc.OrcInputFormat(), + new com.facebook.hive.orc.OrcOutputFormat(), + new com.facebook.hive.orc.OrcSerde(), + ImmutableList.builder() + .add(new DwrfRecordCursorProvider()) + .build(), + ImmutableList.builder() + .add(new DwrfPageSourceFactory(TYPE_MANAGER)) + .build())) + + .add(new BenchmarkFile( + "orc", + new org.apache.hadoop.hive.ql.io.orc.OrcInputFormat(), + new org.apache.hadoop.hive.ql.io.orc.OrcOutputFormat(), + new org.apache.hadoop.hive.ql.io.orc.OrcSerde(), + ImmutableList.builder() + .add(new OrcRecordCursorProvider()) + .build(), + ImmutableList.builder() + .add(new OrcPageSourceFactory(TYPE_MANAGER)) + .build())) + .build(); + + if (!benchmarkWriteSpeed) { + for (BenchmarkFile benchmarkFile : benchmarkFiles) { + for (CompressionType compressionType : ENABLED_COMPRESSION) { + if (!benchmarkFile.getFile(compressionType).exists()) { + writeLineItems(benchmarkFile.getFile(compressionType), benchmarkFile.getOutputFormat(), benchmarkFile.getSerDe(), compressionType, COLUMNS); + } + } + } + } + + for (int run = 0; run < 2; run++) { + System.out.println("==== Run " + run + " ===="); + if (benchmarkWriteSpeed) { + benchmarkWrite(benchmarkFiles, 2, ENABLED_COMPRESSION); + } + if (benchmarkReadSpeed) { + benchmarkRead(benchmarkFiles, 2 + (run > 0 ? 3 : 0), ENABLED_COMPRESSION); + } + } + } + + private static void benchmarkWrite(List benchmarkFiles, int loopCount, List compressionTypes) + throws Exception + { + long start; + System.out.println("write"); + for (BenchmarkFile benchmarkFile : benchmarkFiles) { + for (CompressionType compressionType : compressionTypes) { + DataSize dataSize = null; + start = System.nanoTime(); + for (int loop = 0; loop < loopCount; loop++) { + dataSize = writeLineItems(benchmarkFile.getFile(compressionType), + benchmarkFile.getOutputFormat(), + benchmarkFile.getSerDe(), + compressionType, + COLUMNS); + } + logDuration("none", benchmarkFile.getName(), "write", compressionType, start, loopCount, dataSize); + } + } + System.out.println(); + } + + private static void benchmarkRead(List benchmarkFiles, int loopCount, List compressionTypes) + throws Exception + { + long start; + + setReadColumns(JOB_CONF, ImmutableList.of()); + for (BenchmarkFile benchmarkFile : benchmarkFiles) { + for (HiveRecordCursorProvider recordCursorProvider : benchmarkFile.getRecordCursorProviders()) { + for (CompressionType compressionType : compressionTypes) { + long result = 0; + start = System.nanoTime(); + for (int loop = 0; loop < loopCount; loop++) { + result = benchmarkReadNone( + createFileSplit(benchmarkFile.getFile(compressionType)), + createPartitionProperties(benchmarkFile), + recordCursorProvider + ); + } + logDuration("none", benchmarkFile.getName(), getCursorType(recordCursorProvider), compressionType, start, loopCount, result); + } + } + + for (HivePageSourceFactory pageSourceFactory : benchmarkFile.getPageSourceFactory()) { + for (CompressionType compressionType : compressionTypes) { + long result = 0; + start = System.nanoTime(); + for (int loop = 0; loop < loopCount; loop++) { + result = benchmarkReadNone( + createFileSplit(benchmarkFile.getFile(compressionType)), + createPartitionProperties(benchmarkFile), + pageSourceFactory + ); + } + logDuration("none", benchmarkFile.getName(), "page", compressionType, start, loopCount, result); + } + } + } + + setReadColumns(JOB_CONF, ImmutableList.of()); + for (BenchmarkFile benchmarkFile : benchmarkFiles) { + for (HiveRecordCursorProvider recordCursorProvider : benchmarkFile.getRecordCursorProviders()) { + for (CompressionType compressionType : compressionTypes) { + long result = 0; + start = System.nanoTime(); + for (int loop = 0; loop < loopCount; loop++) { + result = benchmarkReadBigint( + createFileSplit(benchmarkFile.getFile(compressionType)), + createPartitionProperties(benchmarkFile), + recordCursorProvider + ); + } + logDuration("bigint", benchmarkFile.getName(), getCursorType(recordCursorProvider), compressionType, start, loopCount, result); + } + } + + for (HivePageSourceFactory pageSourceFactory : benchmarkFile.getPageSourceFactory()) { + for (CompressionType compressionType : compressionTypes) { + long result = 0; + start = System.nanoTime(); + for (int loop = 0; loop < loopCount; loop++) { + result = benchmarkReadBigint( + createFileSplit(benchmarkFile.getFile(compressionType)), + createPartitionProperties(benchmarkFile), + pageSourceFactory + ); + } + logDuration("bigint", benchmarkFile.getName(), "page", compressionType, start, loopCount, result); + } + } + } + + setReadColumns(JOB_CONF, DOUBLE_COLUMN_INDEX); + for (BenchmarkFile benchmarkFile : benchmarkFiles) { + for (HiveRecordCursorProvider recordCursorProvider : benchmarkFile.getRecordCursorProviders()) { + for (CompressionType compressionType : compressionTypes) { + double result = 0; + start = System.nanoTime(); + for (int loop = 0; loop < loopCount; loop++) { + result = benchmarkReadDouble( + createFileSplit(benchmarkFile.getFile(compressionType)), + createPartitionProperties(benchmarkFile), + recordCursorProvider + ); + } + logDuration("double", benchmarkFile.getName(), getCursorType(recordCursorProvider), compressionType, start, loopCount, result); + } + } + + for (HivePageSourceFactory pageSourceFactory : benchmarkFile.getPageSourceFactory()) { + for (CompressionType compressionType : compressionTypes) { + double result = 0; + start = System.nanoTime(); + for (int loop = 0; loop < loopCount; loop++) { + result = benchmarkReadDouble( + createFileSplit(benchmarkFile.getFile(compressionType)), + createPartitionProperties(benchmarkFile), + pageSourceFactory + ); + } + logDuration("double", benchmarkFile.getName(), "page", compressionType, start, loopCount, result); + } + } + } + + setReadColumns(JOB_CONF, VARCHAR_COLUMN_INDEX); + for (BenchmarkFile benchmarkFile : benchmarkFiles) { + for (HiveRecordCursorProvider recordCursorProvider : benchmarkFile.getRecordCursorProviders()) { + for (CompressionType compressionType : compressionTypes) { + long result = 0; + start = System.nanoTime(); + for (int loop = 0; loop < loopCount; loop++) { + result = benchmarkReadVarchar( + createFileSplit(benchmarkFile.getFile(compressionType)), + createPartitionProperties(benchmarkFile), + recordCursorProvider + ); + } + logDuration("varchar", benchmarkFile.getName(), getCursorType(recordCursorProvider), compressionType, start, loopCount, result); + } + } + for (HivePageSourceFactory pageSourceFactory : benchmarkFile.getPageSourceFactory()) { + for (CompressionType compressionType : compressionTypes) { + long result = 0; + start = System.nanoTime(); + for (int loop = 0; loop < loopCount; loop++) { + result = benchmarkReadVarchar( + createFileSplit(benchmarkFile.getFile(compressionType)), + createPartitionProperties(benchmarkFile), + pageSourceFactory + ); + } + logDuration("varchar", benchmarkFile.getName(), "page", compressionType, start, loopCount, result); + } + } + } + + setReadColumns(JOB_CONF, TPCH_6_COLUMN_INDEXES); + for (BenchmarkFile benchmarkFile : benchmarkFiles) { + for (HiveRecordCursorProvider recordCursorProvider : benchmarkFile.getRecordCursorProviders()) { + for (CompressionType compressionType : compressionTypes) { + double result = 0; + start = System.nanoTime(); + for (int loop = 0; loop < loopCount; loop++) { + result = benchmarkReadTpch6( + createFileSplit(benchmarkFile.getFile(compressionType)), + createPartitionProperties(benchmarkFile), + recordCursorProvider + ); + } + logDuration("tpch6", benchmarkFile.getName(), getCursorType(recordCursorProvider), compressionType, start, loopCount, result); + } + } + + for (HivePageSourceFactory pageSourceFactory : benchmarkFile.getPageSourceFactory()) { + for (CompressionType compressionType : compressionTypes) { + double result = 0; + start = System.nanoTime(); + for (int loop = 0; loop < loopCount; loop++) { + result = benchmarkReadTpch6( + createFileSplit(benchmarkFile.getFile(compressionType)), + createPartitionProperties(benchmarkFile), + pageSourceFactory + ); + } + logDuration("tpch6", benchmarkFile.getName(), "page", compressionType, start, loopCount, result); + } + } + } + + setReadColumns(JOB_CONF, TPCH_1_COLUMN_INDEXES); + for (BenchmarkFile benchmarkFile : benchmarkFiles) { + for (HiveRecordCursorProvider recordCursorProvider : benchmarkFile.getRecordCursorProviders()) { + for (CompressionType compressionType : compressionTypes) { + double result = 0; + start = System.nanoTime(); + for (int loop = 0; loop < loopCount; loop++) { + result = benchmarkReadTpch1( + createFileSplit(benchmarkFile.getFile(compressionType)), + createPartitionProperties(benchmarkFile), + recordCursorProvider + ); + } + logDuration("tpch1", benchmarkFile.getName(), getCursorType(recordCursorProvider), compressionType, start, loopCount, result); + } + } + + for (HivePageSourceFactory pageSourceFactory : benchmarkFile.getPageSourceFactory()) { + for (CompressionType compressionType : compressionTypes) { + double result = 0; + start = System.nanoTime(); + for (int loop = 0; loop < loopCount; loop++) { + result = benchmarkReadTpch1( + createFileSplit(benchmarkFile.getFile(compressionType)), + createPartitionProperties(benchmarkFile), + pageSourceFactory + ); + } + logDuration("tpch1", benchmarkFile.getName(), "page", compressionType, start, loopCount, result); + } + } + } + + setReadColumns(JOB_CONF, ALL_COLUMN_INDEXES); + for (BenchmarkFile benchmarkFile : benchmarkFiles) { + for (HiveRecordCursorProvider recordCursorProvider : benchmarkFile.getRecordCursorProviders()) { + for (CompressionType compressionType : compressionTypes) { + double result = 0; + start = System.nanoTime(); + for (int loop = 0; loop < loopCount; loop++) { + result = benchmarkReadAll( + createFileSplit(benchmarkFile.getFile(compressionType)), + createPartitionProperties(benchmarkFile), + recordCursorProvider + ); + } + logDuration("all", benchmarkFile.getName(), getCursorType(recordCursorProvider), compressionType, start, loopCount, result); + } + } + + for (HivePageSourceFactory pageSourceFactory : benchmarkFile.getPageSourceFactory()) { + for (CompressionType compressionType : compressionTypes) { + double result = 0; + start = System.nanoTime(); + for (int loop = 0; loop < loopCount; loop++) { + result = benchmarkReadAll( + createFileSplit(benchmarkFile.getFile(compressionType)), + createPartitionProperties(benchmarkFile), + pageSourceFactory + ); + } + logDuration("all", benchmarkFile.getName(), "page", compressionType, start, loopCount, result); + } + } + } + + setReadColumns(JOB_CONF, ALL_COLUMN_INDEXES); + for (BenchmarkFile benchmarkFile : benchmarkFiles) { + for (HiveRecordCursorProvider recordCursorProvider : benchmarkFile.getRecordCursorProviders()) { + for (CompressionType compressionType : compressionTypes) { + double result = 0; + start = System.nanoTime(); + for (int loop = 0; loop < loopCount; loop++) { + result = benchmarkLoadAllReadOne( + createFileSplit(benchmarkFile.getFile(compressionType)), + createPartitionProperties(benchmarkFile), + recordCursorProvider + ); + } + logDuration("lazy", benchmarkFile.getName(), getCursorType(recordCursorProvider), compressionType, start, loopCount, result); + } + } + for (HivePageSourceFactory pageSourceFactory : benchmarkFile.getPageSourceFactory()) { + for (CompressionType compressionType : compressionTypes) { + double result = 0; + start = System.nanoTime(); + for (int loop = 0; loop < loopCount; loop++) { + result = benchmarkLoadAllReadOne( + createFileSplit(benchmarkFile.getFile(compressionType)), + createPartitionProperties(benchmarkFile), + pageSourceFactory + ); + } + logDuration("lazy", benchmarkFile.getName(), "page", compressionType, start, loopCount, result); + } + } + } + + setReadColumns(JOB_CONF, ALL_COLUMN_INDEXES); + for (BenchmarkFile benchmarkFile : benchmarkFiles) { + for (HiveRecordCursorProvider recordCursorProvider : benchmarkFile.getRecordCursorProviders()) { + for (CompressionType compressionType : compressionTypes) { + double result = 0; + start = System.nanoTime(); + for (int loop = 0; loop < loopCount; loop++) { + // cursor interface doesn't support predicate pushdown + result = benchmarkReadAll( + createFileSplit(benchmarkFile.getFile(compressionType)), + createPartitionProperties(benchmarkFile), + recordCursorProvider + ); + } + logDuration("pushdown", benchmarkFile.getName(), getCursorType(recordCursorProvider), compressionType, start, loopCount, result); + } + } + + for (HivePageSourceFactory pageSourceFactory : benchmarkFile.getPageSourceFactory()) { + for (CompressionType compressionType : compressionTypes) { + double result = 0; + start = System.nanoTime(); + for (int loop = 0; loop < loopCount; loop++) { + result = benchmarkPredicatePushDown( + createFileSplit(benchmarkFile.getFile(compressionType)), + createPartitionProperties(benchmarkFile), + pageSourceFactory + ); + } + logDuration("pushdown", benchmarkFile.getName(), "page", compressionType, start, loopCount, result); + } + } + } + System.out.println(); + + } + + private static long benchmarkReadNone( + FileSplit fileSplit, + Properties partitionProperties, + HiveRecordCursorProvider hiveRecordCursorProvider) + throws Exception + { + HiveSplit split = createHiveSplit(fileSplit, partitionProperties); + + long count = 0; + for (int i = 0; i < LOOPS; i++) { + count = 0; + + HiveRecordCursor recordCursor = hiveRecordCursorProvider.createHiveRecordCursor( + split.getClientId(), + new Configuration(), + split.getSession(), + new Path(split.getPath()), + split.getStart(), + split.getLength(), + split.getSchema(), + ImmutableList.of(), + split.getPartitionKeys(), + TupleDomain.all(), + DateTimeZone.UTC, + TYPE_MANAGER).get(); + + while (recordCursor.advanceNextPosition()) { + count++; + } + recordCursor.close(); + } + return count; + } + + private static long benchmarkReadNone( + FileSplit fileSplit, + Properties partitionProperties, + HivePageSourceFactory pageSourceFactory) + throws Exception + { + HiveSplit split = createHiveSplit(fileSplit, partitionProperties); + + long count = 0; + for (int i = 0; i < LOOPS; i++) { + count = 0; + + ConnectorPageSource pageSource = pageSourceFactory.createPageSource( + new Configuration(), + split.getSession(), + new Path(split.getPath()), + split.getStart(), + split.getLength(), + split.getSchema(), + ImmutableList.of(), + split.getPartitionKeys(), + TupleDomain.all(), + DateTimeZone.UTC).get(); + + while (!pageSource.isFinished()) { + Page page = pageSource.getNextPage(); + if (page == null) { + continue; + } + count += page.getPositionCount(); + } + pageSource.close(); + } + return count; + } + + private static long benchmarkReadBigint( + FileSplit fileSplit, + Properties partitionProperties, + HiveRecordCursorProvider hiveRecordCursorProvider) + throws Exception + { + HiveSplit split = createHiveSplit(fileSplit, partitionProperties); + + long sum = 0; + for (int i = 0; i < LOOPS; i++) { + sum = 0; + + HiveRecordCursor recordCursor = hiveRecordCursorProvider.createHiveRecordCursor( + split.getClientId(), + new Configuration(), + split.getSession(), + new Path(split.getPath()), + split.getStart(), + split.getLength(), + split.getSchema(), + BIGINT_COLUMN, + split.getPartitionKeys(), + TupleDomain.all(), + DateTimeZone.UTC, + TYPE_MANAGER).get(); + + while (recordCursor.advanceNextPosition()) { + if (!recordCursor.isNull(0)) { + sum += recordCursor.getLong(0); + } + } + recordCursor.close(); + } + return sum; + } + + private static long benchmarkReadBigint( + FileSplit fileSplit, + Properties partitionProperties, + HivePageSourceFactory pageSourceFactory) + throws Exception + { + HiveSplit split = createHiveSplit(fileSplit, partitionProperties); + + long sum = 0; + for (int i = 0; i < LOOPS; i++) { + sum = 0; + + ConnectorPageSource pageSource = pageSourceFactory.createPageSource( + new Configuration(), + split.getSession(), + new Path(split.getPath()), + split.getStart(), + split.getLength(), + split.getSchema(), + BIGINT_COLUMN, + split.getPartitionKeys(), + TupleDomain.all(), + DateTimeZone.UTC).get(); + + while (!pageSource.isFinished()) { + Page page = pageSource.getNextPage(); + if (page == null) { + continue; + } + Block block = page.getBlock(0); + for (int position = 0; position < block.getPositionCount(); position++) { + if (!block.isNull(position)) { + sum += BIGINT.getLong(block, position); + } + } + } + pageSource.close(); + } + return sum; + } + + private static double benchmarkReadDouble( + FileSplit fileSplit, + Properties partitionProperties, + HiveRecordCursorProvider hiveRecordCursorProvider) + throws Exception + { + HiveSplit split = createHiveSplit(fileSplit, partitionProperties); + + double sum = 0; + for (int i = 0; i < LOOPS; i++) { + sum = 0; + + HiveRecordCursor recordCursor = hiveRecordCursorProvider.createHiveRecordCursor( + split.getClientId(), + new Configuration(), + split.getSession(), + new Path(split.getPath()), + split.getStart(), + split.getLength(), + split.getSchema(), + DOUBLE_COLUMN, + split.getPartitionKeys(), + TupleDomain.all(), + DateTimeZone.UTC, + TYPE_MANAGER).get(); + + while (recordCursor.advanceNextPosition()) { + if (!recordCursor.isNull(0)) { + sum += recordCursor.getDouble(0); + } + } + recordCursor.close(); + } + return sum; + } + + private static double benchmarkReadDouble( + FileSplit fileSplit, + Properties partitionProperties, + HivePageSourceFactory pageSourceFactory) + throws Exception + { + HiveSplit split = createHiveSplit(fileSplit, partitionProperties); + + double sum = 0; + for (int i = 0; i < LOOPS; i++) { + sum = 0; + + ConnectorPageSource pageSource = pageSourceFactory.createPageSource( + new Configuration(), + split.getSession(), + new Path(split.getPath()), + split.getStart(), + split.getLength(), + split.getSchema(), + DOUBLE_COLUMN, + split.getPartitionKeys(), + TupleDomain.all(), + DateTimeZone.UTC).get(); + + while (!pageSource.isFinished()) { + Page page = pageSource.getNextPage(); + if (page == null) { + continue; + } + Block block = page.getBlock(0); + for (int position = 0; position < block.getPositionCount(); position++) { + if (!block.isNull(position)) { + sum += DOUBLE.getDouble(block, position); + } + } + } + pageSource.close(); + } + return sum; + } + + private static long benchmarkReadVarchar( + FileSplit fileSplit, + Properties partitionProperties, + HiveRecordCursorProvider hiveRecordCursorProvider) + throws Exception + { + HiveSplit split = createHiveSplit(fileSplit, partitionProperties); + + long sum = 0; + for (int i = 0; i < LOOPS; i++) { + sum = 0; + + HiveRecordCursor recordCursor = hiveRecordCursorProvider.createHiveRecordCursor( + split.getClientId(), + new Configuration(), + split.getSession(), + new Path(split.getPath()), + split.getStart(), + split.getLength(), + split.getSchema(), + VARCHAR_COLUMN, + split.getPartitionKeys(), + TupleDomain.all(), + DateTimeZone.UTC, + TYPE_MANAGER).get(); + + while (recordCursor.advanceNextPosition()) { + if (!recordCursor.isNull(0)) { + sum += recordCursor.getSlice(0).length(); + } + } + recordCursor.close(); + } + return sum; + } + + private static long benchmarkReadVarchar( + FileSplit fileSplit, + Properties partitionProperties, + HivePageSourceFactory pageSourceFactory) + throws Exception + { + HiveSplit split = createHiveSplit(fileSplit, partitionProperties); + + long sum = 0; + for (int i = 0; i < LOOPS; i++) { + sum = 0; + + ConnectorPageSource pageSource = pageSourceFactory.createPageSource( + new Configuration(), + split.getSession(), + new Path(split.getPath()), + split.getStart(), + split.getLength(), + split.getSchema(), + VARCHAR_COLUMN, + split.getPartitionKeys(), + TupleDomain.all(), + DateTimeZone.UTC).get(); + + while (!pageSource.isFinished()) { + Page page = pageSource.getNextPage(); + if (page == null) { + continue; + } + Block block = page.getBlock(0); + for (int position = 0; position < block.getPositionCount(); position++) { + if (!block.isNull(position)) { + sum += VARCHAR.getSlice(block, position).length(); + } + } + } + pageSource.close(); + } + return sum; + } + + private static double benchmarkReadTpch6( + FileSplit fileSplit, + Properties partitionProperties, + HiveRecordCursorProvider hiveRecordCursorProvider) + throws IOException + { + HiveSplit split = createHiveSplit(fileSplit, partitionProperties); + + double sum = 0; + for (int i = 0; i < LOOPS; i++) { + sum = 0; + + HiveRecordCursor recordCursor = hiveRecordCursorProvider.createHiveRecordCursor( + split.getClientId(), + new Configuration(), + split.getSession(), + new Path(split.getPath()), + split.getStart(), + split.getLength(), + split.getSchema(), + TPCH_6_COLUMNS, + split.getPartitionKeys(), + TupleDomain.all(), + DateTimeZone.UTC, + TYPE_MANAGER).get(); + + while (recordCursor.advanceNextPosition()) { + if (!recordCursor.isNull(0)) { + sum += recordCursor.getLong(0); + } + if (!recordCursor.isNull(1)) { + sum += recordCursor.getDouble(1); + } + if (!recordCursor.isNull(2)) { + sum += recordCursor.getDouble(2); + } + if (!recordCursor.isNull(3)) { + sum += recordCursor.getSlice(3).length(); + } + } + recordCursor.close(); + } + return sum; + } + + private static double benchmarkReadTpch6( + FileSplit fileSplit, + Properties partitionProperties, + HivePageSourceFactory pageSourceFactory) + throws IOException + { + HiveSplit split = createHiveSplit(fileSplit, partitionProperties); + + double sum = 0; + for (int i = 0; i < LOOPS; i++) { + sum = 0; + + ConnectorPageSource pageSource = pageSourceFactory.createPageSource( + new Configuration(), + split.getSession(), + new Path(split.getPath()), + split.getStart(), + split.getLength(), + split.getSchema(), + TPCH_6_COLUMNS, + split.getPartitionKeys(), + TupleDomain.all(), + DateTimeZone.UTC).get(); + + while (!pageSource.isFinished()) { + Page page = pageSource.getNextPage(); + if (page == null) { + continue; + } + Block block0 = page.getBlock(0); + Block block1 = page.getBlock(1); + Block block2 = page.getBlock(2); + Block block3 = page.getBlock(3); + for (int position = 0; position < page.getPositionCount(); position++) { + if (!block0.isNull(position)) { + sum += BIGINT.getLong(block0, position); + } + if (!block1.isNull(position)) { + sum += DOUBLE.getDouble(block1, position); + } + if (!block2.isNull(position)) { + sum += DOUBLE.getDouble(block2, position); + } + if (!block3.isNull(position)) { + sum += VARCHAR.getSlice(block3, position).length(); + } + } + } + pageSource.close(); + } + return sum; + } + + private static double benchmarkReadTpch1( + FileSplit fileSplit, + Properties partitionProperties, + HiveRecordCursorProvider hiveRecordCursorProvider) + throws IOException + { + HiveSplit split = createHiveSplit(fileSplit, partitionProperties); + + double sum = 0; + for (int i = 0; i < LOOPS; i++) { + sum = 0; + + HiveRecordCursor recordCursor = hiveRecordCursorProvider.createHiveRecordCursor( + split.getClientId(), + new Configuration(), + split.getSession(), + new Path(split.getPath()), + split.getStart(), + split.getLength(), + split.getSchema(), + TPCH_1_COLUMNS, + split.getPartitionKeys(), + TupleDomain.all(), + DateTimeZone.UTC, + TYPE_MANAGER).get(); + + while (recordCursor.advanceNextPosition()) { + if (!recordCursor.isNull(0)) { + sum += recordCursor.getLong(0); + } + if (!recordCursor.isNull(1)) { + sum += recordCursor.getDouble(1); + } + if (!recordCursor.isNull(2)) { + sum += recordCursor.getDouble(2); + } + if (!recordCursor.isNull(3)) { + sum += recordCursor.getDouble(3); + } + if (!recordCursor.isNull(4)) { + sum += recordCursor.getSlice(4).length(); + } + if (!recordCursor.isNull(5)) { + sum += recordCursor.getSlice(5).length(); + } + if (!recordCursor.isNull(6)) { + sum += recordCursor.getSlice(6).length(); + } + } + recordCursor.close(); + } + return sum; + } + + private static double benchmarkReadTpch1( + FileSplit fileSplit, + Properties partitionProperties, + HivePageSourceFactory pageSourceFactory) + throws IOException + { + HiveSplit split = createHiveSplit(fileSplit, partitionProperties); + + double sum = 0; + for (int i = 0; i < LOOPS; i++) { + sum = 0; + + ConnectorPageSource pageSource = pageSourceFactory.createPageSource( + new Configuration(), + split.getSession(), + new Path(split.getPath()), + split.getStart(), + split.getLength(), + split.getSchema(), + TPCH_1_COLUMNS, + split.getPartitionKeys(), + TupleDomain.all(), + DateTimeZone.UTC).get(); + + while (!pageSource.isFinished()) { + Page page = pageSource.getNextPage(); + if (page == null) { + continue; + } + + Block block0 = page.getBlock(0); + Block block1 = page.getBlock(1); + Block block2 = page.getBlock(2); + Block block3 = page.getBlock(3); + Block block4 = page.getBlock(4); + Block block5 = page.getBlock(5); + Block block6 = page.getBlock(6); + + for (int position = 0; position < page.getPositionCount(); position++) { + if (!block0.isNull(position)) { + sum += BIGINT.getLong(block0, position); + } + if (!block1.isNull(position)) { + sum += DOUBLE.getDouble(block1, position); + } + if (!block2.isNull(position)) { + sum += DOUBLE.getDouble(block2, position); + } + if (!block3.isNull(position)) { + sum += DOUBLE.getDouble(block3, position); + } + if (!block4.isNull(position)) { + sum += VARCHAR.getSlice(block4, position).length(); + } + if (!block5.isNull(position)) { + sum += VARCHAR.getSlice(block5, position).length(); + } + if (!block6.isNull(position)) { + sum += VARCHAR.getSlice(block6, position).length(); + } + } + } + pageSource.close(); + } + return sum; + } + + private static double benchmarkReadAll( + FileSplit fileSplit, + Properties partitionProperties, + HiveRecordCursorProvider hiveRecordCursorProvider) + throws IOException + { + HiveSplit split = createHiveSplit(fileSplit, partitionProperties); + + double sum = 0; + for (int i = 0; i < LOOPS; i++) { + sum = 0; + + HiveRecordCursor recordCursor = hiveRecordCursorProvider.createHiveRecordCursor( + split.getClientId(), + new Configuration(), + split.getSession(), + new Path(split.getPath()), + split.getStart(), + split.getLength(), + split.getSchema(), + ALL_COLUMNS, + split.getPartitionKeys(), + TupleDomain.all(), + DateTimeZone.UTC, + TYPE_MANAGER).get(); + + while (recordCursor.advanceNextPosition()) { + if (!recordCursor.isNull(0)) { + sum += recordCursor.getLong(0); + } + if (!recordCursor.isNull(1)) { + sum += recordCursor.getLong(1); + } + if (!recordCursor.isNull(2)) { + sum += recordCursor.getLong(2); + } + if (!recordCursor.isNull(3)) { + sum += recordCursor.getLong(3); + } + if (!recordCursor.isNull(4)) { + sum += recordCursor.getLong(4); + } + if (!recordCursor.isNull(5)) { + sum += recordCursor.getDouble(5); + } + if (!recordCursor.isNull(6)) { + sum += recordCursor.getDouble(6); + } + if (!recordCursor.isNull(7)) { + sum += recordCursor.getDouble(7); + } + if (!recordCursor.isNull(8)) { + sum += recordCursor.getSlice(8).length(); + } + if (!recordCursor.isNull(9)) { + sum += recordCursor.getSlice(9).length(); + } + if (!recordCursor.isNull(10)) { + sum += recordCursor.getSlice(10).length(); + } + if (!recordCursor.isNull(11)) { + sum += recordCursor.getSlice(11).length(); + } + if (!recordCursor.isNull(12)) { + sum += recordCursor.getSlice(12).length(); + } + if (!recordCursor.isNull(13)) { + sum += recordCursor.getSlice(13).length(); + } + if (!recordCursor.isNull(14)) { + sum += recordCursor.getSlice(14).length(); + } + if (!recordCursor.isNull(15)) { + sum += recordCursor.getSlice(15).length(); + } + } + recordCursor.close(); + } + return sum; + } + + private static double benchmarkReadAll( + FileSplit fileSplit, + Properties partitionProperties, + HivePageSourceFactory pageSourceFactory) + throws IOException + { + HiveSplit split = createHiveSplit(fileSplit, partitionProperties); + + double sum = 0; + for (int i = 0; i < LOOPS; i++) { + sum = 0; + + ConnectorPageSource pageSource = pageSourceFactory.createPageSource( + new Configuration(), + split.getSession(), + new Path(split.getPath()), + split.getStart(), + split.getLength(), + split.getSchema(), + ALL_COLUMNS, + split.getPartitionKeys(), + TupleDomain.all(), + DateTimeZone.UTC).get(); + + while (!pageSource.isFinished()) { + Page page = pageSource.getNextPage(); + if (page == null) { + continue; + } + + Block block0 = page.getBlock(0); + Block block1 = page.getBlock(1); + Block block2 = page.getBlock(2); + Block block3 = page.getBlock(3); + Block block4 = page.getBlock(4); + Block block5 = page.getBlock(5); + Block block6 = page.getBlock(6); + Block block7 = page.getBlock(7); + Block block8 = page.getBlock(8); + Block block9 = page.getBlock(9); + Block block10 = page.getBlock(10); + Block block11 = page.getBlock(11); + Block block12 = page.getBlock(12); + Block block13 = page.getBlock(13); + Block block14 = page.getBlock(14); + Block block15 = page.getBlock(15); + + for (int position = 0; position < page.getPositionCount(); position++) { + if (!block0.isNull(position)) { + sum += BIGINT.getLong(block0, position); + } + if (!block1.isNull(position)) { + sum += BIGINT.getLong(block1, position); + } + if (!block2.isNull(position)) { + sum += BIGINT.getLong(block2, position); + } + if (!block3.isNull(position)) { + sum += BIGINT.getLong(block3, position); + } + if (!block4.isNull(position)) { + sum += BIGINT.getLong(block4, position); + } + if (!block5.isNull(position)) { + sum += DOUBLE.getDouble(block5, position); + } + if (!block6.isNull(position)) { + sum += DOUBLE.getDouble(block6, position); + } + if (!block7.isNull(position)) { + sum += DOUBLE.getDouble(block7, position); + } + if (!block8.isNull(position)) { + sum += VARCHAR.getSlice(block8, position).length(); + } + if (!block9.isNull(position)) { + sum += VARCHAR.getSlice(block9, position).length(); + } + if (!block10.isNull(position)) { + sum += VARCHAR.getSlice(block10, position).length(); + } + if (!block11.isNull(position)) { + sum += VARCHAR.getSlice(block11, position).length(); + } + if (!block12.isNull(position)) { + sum += VARCHAR.getSlice(block12, position).length(); + } + if (!block13.isNull(position)) { + sum += VARCHAR.getSlice(block13, position).length(); + } + if (!block14.isNull(position)) { + sum += VARCHAR.getSlice(block14, position).length(); + } + if (!block15.isNull(position)) { + sum += VARCHAR.getSlice(block15, position).length(); + } + } + } + pageSource.close(); + } + return sum; + } + + private static double benchmarkLoadAllReadOne( + FileSplit fileSplit, + Properties partitionProperties, + HiveRecordCursorProvider hiveRecordCursorProvider) + throws IOException + { + HiveSplit split = createHiveSplit(fileSplit, partitionProperties); + + double sum = 0; + for (int i = 0; i < LOOPS; i++) { + sum = 0; + + HiveRecordCursor recordCursor = hiveRecordCursorProvider.createHiveRecordCursor( + split.getClientId(), + new Configuration(), + split.getSession(), + new Path(split.getPath()), + split.getStart(), + split.getLength(), + split.getSchema(), + ALL_COLUMNS, + split.getPartitionKeys(), + TupleDomain.all(), + DateTimeZone.UTC, + TYPE_MANAGER).get(); + + while (recordCursor.advanceNextPosition()) { + if (!recordCursor.isNull(0)) { + sum += recordCursor.getLong(0); + } + } + recordCursor.close(); + } + return sum; + } + + private static double benchmarkLoadAllReadOne( + FileSplit fileSplit, + Properties partitionProperties, + HivePageSourceFactory pageSourceFactory) + throws IOException + { + HiveSplit split = createHiveSplit(fileSplit, partitionProperties); + + double sum = 0; + for (int i = 0; i < LOOPS; i++) { + sum = 0; + + ConnectorPageSource pageSource = pageSourceFactory.createPageSource( + new Configuration(), + split.getSession(), + new Path(split.getPath()), + split.getStart(), + split.getLength(), + split.getSchema(), + ALL_COLUMNS, + split.getPartitionKeys(), + TupleDomain.all(), + DateTimeZone.UTC).get(); + + while (!pageSource.isFinished()) { + Page page = pageSource.getNextPage(); + if (page == null) { + continue; + } + Block block = page.getBlock(0); + for (int position = 0; position < block.getPositionCount(); position++) { + if (!block.isNull(position)) { + sum += BIGINT.getLong(block, position); + } + } + } + pageSource.close(); + } + return sum; + } + + private static double benchmarkPredicatePushDown( + FileSplit fileSplit, + Properties partitionProperties, + HiveRecordCursorProvider hiveRecordCursorProvider) + throws IOException + { + HiveSplit split = createHiveSplit(fileSplit, partitionProperties); + + double sum = 0; + for (int i = 0; i < LOOPS; i++) { + sum = 0; + + HiveRecordCursor recordCursor = hiveRecordCursorProvider.createHiveRecordCursor( + split.getClientId(), + new Configuration(), + split.getSession(), + new Path(split.getPath()), + split.getStart(), + split.getLength(), + split.getSchema(), + ALL_COLUMNS, + split.getPartitionKeys(), + TupleDomain.all(), + DateTimeZone.UTC, + TYPE_MANAGER).get(); + + while (recordCursor.advanceNextPosition()) { + if (!recordCursor.isNull(0)) { + long orderKey = recordCursor.getLong(0); + if (orderKey != FILTER_ORDER_KEY_ID) { + continue; + } + sum += orderKey; + } + if (!recordCursor.isNull(1)) { + sum += recordCursor.getLong(1); + } + if (!recordCursor.isNull(2)) { + sum += recordCursor.getLong(2); + } + if (!recordCursor.isNull(3)) { + sum += recordCursor.getLong(3); + } + if (!recordCursor.isNull(4)) { + sum += recordCursor.getLong(4); + } + if (!recordCursor.isNull(5)) { + sum += recordCursor.getDouble(5); + } + if (!recordCursor.isNull(6)) { + sum += recordCursor.getDouble(6); + } + if (!recordCursor.isNull(7)) { + sum += recordCursor.getDouble(7); + } + if (!recordCursor.isNull(8)) { + sum += recordCursor.getSlice(8).length(); + } + if (!recordCursor.isNull(9)) { + sum += recordCursor.getSlice(9).length(); + } + if (!recordCursor.isNull(10)) { + sum += recordCursor.getSlice(10).length(); + } + if (!recordCursor.isNull(11)) { + sum += recordCursor.getSlice(11).length(); + } + if (!recordCursor.isNull(12)) { + sum += recordCursor.getSlice(12).length(); + } + if (!recordCursor.isNull(13)) { + sum += recordCursor.getSlice(13).length(); + } + if (!recordCursor.isNull(14)) { + sum += recordCursor.getSlice(14).length(); + } + if (!recordCursor.isNull(15)) { + sum += recordCursor.getSlice(15).length(); + } + } + recordCursor.close(); + } + return sum; + } + + private static double benchmarkPredicatePushDown( + FileSplit fileSplit, + Properties partitionProperties, + HivePageSourceFactory pageSourceFactory) + throws IOException + { + HiveSplit split = createHiveSplit(fileSplit, partitionProperties); + + double sum = 0; + for (int i = 0; i < LOOPS; i++) { + sum = 0; + + ConnectorPageSource pageSource = pageSourceFactory.createPageSource( + new Configuration(), + split.getSession(), + new Path(split.getPath()), + split.getStart(), + split.getLength(), + split.getSchema(), + ALL_COLUMNS, + split.getPartitionKeys(), + TupleDomain.withFixedValues(ImmutableMap.>of(Iterables.getOnlyElement(getHiveColumnHandles(ORDER_KEY)), FILTER_ORDER_KEY_ID)), + DateTimeZone.UTC).get(); + + while (!pageSource.isFinished()) { + Page page = pageSource.getNextPage(); + if (page == null) { + continue; + } + + Block block0 = page.getBlock(0); + Block block1 = page.getBlock(1); + Block block2 = page.getBlock(2); + Block block3 = page.getBlock(3); + Block block4 = page.getBlock(4); + Block block5 = page.getBlock(5); + Block block6 = page.getBlock(6); + Block block7 = page.getBlock(7); + Block block8 = page.getBlock(8); + Block block9 = page.getBlock(9); + Block block10 = page.getBlock(10); + Block block11 = page.getBlock(11); + Block block12 = page.getBlock(12); + Block block13 = page.getBlock(13); + Block block14 = page.getBlock(14); + Block block15 = page.getBlock(15); + + for (int position = 0; position < page.getPositionCount(); position++) { + if (!block0.isNull(position)) { + long orderKey = BIGINT.getLong(block0, position); + if (orderKey != FILTER_ORDER_KEY_ID) { + continue; + } + sum += orderKey; + } + if (!block1.isNull(position)) { + sum += BIGINT.getLong(block1, position); + } + if (!block2.isNull(position)) { + sum += BIGINT.getLong(block2, position); + } + if (!block3.isNull(position)) { + sum += BIGINT.getLong(block3, position); + } + if (!block4.isNull(position)) { + sum += BIGINT.getLong(block4, position); + } + if (!block5.isNull(position)) { + sum += DOUBLE.getDouble(block5, position); + } + if (!block6.isNull(position)) { + sum += DOUBLE.getDouble(block6, position); + } + if (!block7.isNull(position)) { + sum += DOUBLE.getDouble(block7, position); + } + if (!block8.isNull(position)) { + sum += VARCHAR.getSlice(block8, position).length(); + } + if (!block9.isNull(position)) { + sum += VARCHAR.getSlice(block9, position).length(); + } + if (!block10.isNull(position)) { + sum += VARCHAR.getSlice(block10, position).length(); + } + if (!block11.isNull(position)) { + sum += VARCHAR.getSlice(block11, position).length(); + } + if (!block12.isNull(position)) { + sum += VARCHAR.getSlice(block12, position).length(); + } + if (!block13.isNull(position)) { + sum += VARCHAR.getSlice(block13, position).length(); + } + if (!block14.isNull(position)) { + sum += VARCHAR.getSlice(block14, position).length(); + } + if (!block15.isNull(position)) { + sum += VARCHAR.getSlice(block15, position).length(); + } + } + } + pageSource.close(); + } + return sum; + } + + public static RecordWriter createRecordWriter(List> columns, File outputFile, HiveOutputFormat outputFormat, CompressionType compressionCodec) + throws Exception + { + JobConf jobConf = new JobConf(); + ReaderWriterProfiler.setProfilerOptions(jobConf); + if (compressionCodec != CompressionType.none) { + CompressionCodec codec = new CompressionCodecFactory(new Configuration()).getCodecByName(compressionCodec.toString()); + jobConf.set(COMPRESS_CODEC, codec.getClass().getName()); + jobConf.set(COMPRESS_TYPE, org.apache.hadoop.io.SequenceFile.CompressionType.BLOCK.toString()); + jobConf.set("parquet.compression", compressionCodec.toString()); + jobConf.set("parquet.enable.dictionary", "true"); + switch (compressionCodec) { + case gzip: + jobConf.set("hive.exec.orc.default.compress", "ZLIB"); + jobConf.set("hive.exec.orc.compress", "ZLIB"); + break; + case snappy: + jobConf.set("hive.exec.orc.default.compress", "SNAPPY"); + jobConf.set("hive.exec.orc.compress", "SNAPPY"); + break; + default: + throw new IllegalArgumentException("Unsupported compression codec: " + compressionCodec); + } + } + else { + jobConf.set("parquet.enable.dictionary", "true"); + jobConf.set("hive.exec.orc.default.compress", "NONE"); + jobConf.set("hive.exec.orc.compress", "NONE"); + } + + RecordWriter recordWriter = outputFormat.getHiveRecordWriter( + jobConf, + new Path(outputFile.toURI()), + Text.class, + compressionCodec != CompressionType.none, + createTableProperties(columns), + new Progressable() + { + @Override + public void progress() + { + } + } + ); + + return recordWriter; + } + + public static DataSize writeLineItems( + File outputFile, + HiveOutputFormat outputFormat, + @SuppressWarnings("deprecation") Serializer serializer, + CompressionType compressionType, + List> columns) + throws Exception + { + RecordWriter recordWriter = createRecordWriter(columns, outputFile, outputFormat, compressionType); + + SettableStructObjectInspector objectInspector = getStandardStructObjectInspector(transform(columns, input -> input.getColumnName()), transform(columns, input -> getObjectInspector(input))); + + Object row = objectInspector.create(); + + List fields = ImmutableList.copyOf(objectInspector.getAllStructFieldRefs()); + + for (LineItem lineItem : new LineItemGenerator(1, 1, 1)) { + objectInspector.setStructFieldData(row, fields.get(0), lineItem.getOrderKey()); + objectInspector.setStructFieldData(row, fields.get(1), lineItem.getPartKey()); + objectInspector.setStructFieldData(row, fields.get(2), lineItem.getSupplierKey()); + objectInspector.setStructFieldData(row, fields.get(3), lineItem.getLineNumber()); + objectInspector.setStructFieldData(row, fields.get(4), lineItem.getQuantity()); + objectInspector.setStructFieldData(row, fields.get(5), lineItem.getExtendedPrice()); + objectInspector.setStructFieldData(row, fields.get(6), lineItem.getDiscount()); + objectInspector.setStructFieldData(row, fields.get(7), lineItem.getTax()); + objectInspector.setStructFieldData(row, fields.get(8), lineItem.getReturnFlag()); + objectInspector.setStructFieldData(row, fields.get(9), lineItem.getStatus()); + objectInspector.setStructFieldData(row, fields.get(10), lineItem.getShipDate()); + objectInspector.setStructFieldData(row, fields.get(11), lineItem.getCommitDate()); + objectInspector.setStructFieldData(row, fields.get(12), lineItem.getReceiptDate()); + objectInspector.setStructFieldData(row, fields.get(13), lineItem.getShipInstructions()); + objectInspector.setStructFieldData(row, fields.get(14), lineItem.getShipMode()); + objectInspector.setStructFieldData(row, fields.get(15), lineItem.getComment()); + + Writable record = serializer.serialize(row, objectInspector); + recordWriter.write(record); + } + + recordWriter.close(false); + return getFileSize(outputFile); + } + + public static Properties createTableProperties(List> columns) + { + Properties orderTableProperties = new Properties(); + orderTableProperties.setProperty( + "columns", + Joiner.on(',').join(transform(columns, input -> input.getColumnName()))); + orderTableProperties.setProperty( + "columns.types", + Joiner.on(':').join(transform(columns, BenchmarkHiveFileFormats::getColumnType))); + return orderTableProperties; + } + + private static Properties createPartitionProperties(BenchmarkFile benchmarkFile) + { + Properties schema = createTableProperties(ImmutableList.copyOf(LineItemColumn.values())); + schema.setProperty(FILE_INPUT_FORMAT, benchmarkFile.getInputFormat().getClass().getName()); + schema.setProperty(SERIALIZATION_LIB, benchmarkFile.getSerDe().getClass().getName()); + return schema; + } + + private static HiveSplit createHiveSplit(FileSplit fileSplit, Properties partitionProperties) + { + return new HiveSplit("test", + "test", + "lineitem", + "unpartitioned", + fileSplit.getPath().toString(), + fileSplit.getStart(), + fileSplit.getLength(), + partitionProperties, + ImmutableList.of(), + ImmutableList.of(), + false, + SESSION, + TupleDomain.all()); + } + + private static List getHiveColumnHandles(TpchColumn... tpchColumns) + { + ImmutableList.Builder columns = ImmutableList.builder(); + for (TpchColumn column : tpchColumns) { + int ordinal = COLUMNS.indexOf(column); + ObjectInspector inspector = getObjectInspector(column); + columns.add(new HiveColumnHandle("test", + column.getColumnName(), + ordinal, + HiveType.getHiveType(inspector), + getType(inspector, TYPE_MANAGER).getTypeSignature(), + ordinal, + false)); + } + + return columns.build(); + } + + private static void logDuration(String test, String formatName, String readerType, CompressionType compressionType, long start, int loopCount, Object value) + { + long end = System.nanoTime(); + long nanos = end - start; + Duration duration = new Duration(1.0 * nanos / loopCount, NANOSECONDS); + System.out.printf("%s\t%s\t%s\t%s\t%s\t%s\n", test, formatName, readerType, compressionType, duration.toMillis(), value); + } + + private static DataSize getFileSize(File outputFile) + { + return new DataSize(outputFile.length(), Unit.BYTE).convertToMostSuccinctDataSize(); + } + + private static String getColumnType(TpchColumn input) + { + switch (input.getType()) { + case BIGINT: + return "bigint"; + case DATE: + return "date"; + case DOUBLE: + return "double"; + case VARCHAR: + return "string"; + } + throw new IllegalArgumentException("Unsupported type " + input.getType()); + } + + private static ObjectInspector getObjectInspector(TpchColumn input) + { + switch (input.getType()) { + case BIGINT: + return javaLongObjectInspector; + case DATE: + return javaDateObjectInspector; + case DOUBLE: + return javaDoubleObjectInspector; + case VARCHAR: + return javaStringObjectInspector; + } + throw new IllegalArgumentException("Unsupported type " + input.getType()); + } + + private static String getCursorType(HiveRecordCursorProvider recordCursorProvider) + { + if (recordCursorProvider instanceof GenericHiveRecordCursorProvider) { + return "generic"; + } + return "cursor"; + } + + private static FileSplit createFileSplit(File file) + { + try { + Path lineitemPath = new Path(file.toURI()); + lineitemPath.getFileSystem(new Configuration()).setVerifyChecksum(false); + return new FileSplit(lineitemPath, 0, file.length(), new String[0]); + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } + + private static class BenchmarkFile + { + private final String name; + private final InputFormat inputFormat; + private final HiveOutputFormat outputFormat; + @SuppressWarnings("deprecation") + private final SerDe serDe; + private final List recordCursorProviders; + private final List pageSourceFactories; + + private BenchmarkFile( + String name, + InputFormat inputFormat, + HiveOutputFormat outputFormat, + @SuppressWarnings("deprecation") SerDe serDe, + Iterable recordCursorProviders, + Iterable pageSourceFactories) + throws Exception + { + this.name = name; + this.inputFormat = inputFormat; + this.outputFormat = outputFormat; + this.serDe = serDe; + this.recordCursorProviders = ImmutableList.copyOf(recordCursorProviders); + this.pageSourceFactories = ImmutableList.copyOf(pageSourceFactories); + + serDe.initialize(new Configuration(), createTableProperties(COLUMNS)); + } + + public String getName() + { + return name; + } + + public InputFormat getInputFormat() + { + return inputFormat; + } + + public HiveOutputFormat getOutputFormat() + { + return outputFormat; + } + + @SuppressWarnings("deprecation") + public SerDe getSerDe() + { + return serDe; + } + + public File getFile(CompressionType compressionType) + { + return new File(DATA_DIR, "line_item." + getName() + compressionType.getFileExtension()); + } + + public List getRecordCursorProviders() + { + return recordCursorProviders; + } + + public List getPageSourceFactory() + { + return pageSourceFactories; + } + } + + private static void workAroundParquetBrokenLoggingSetup() + throws IOException + { + // unhook out and err while initializing logging or logger will print to them + PrintStream out = System.out; + PrintStream err = System.err; + try { + System.setOut(new PrintStream(nullOutputStream())); + System.setErr(new PrintStream(nullOutputStream())); + + Log.getLog(Object.class); + } + finally { + System.setOut(out); + System.setErr(err); + } + } +} diff --git a/presto-hive/src/test/java/com/facebook/presto/hive/HiveBenchmarkQueryRunner.java b/presto-hive/src/test/java/com/facebook/presto/hive/HiveBenchmarkQueryRunner.java new file mode 100644 index 00000000..309f7a45 --- /dev/null +++ b/presto-hive/src/test/java/com/facebook/presto/hive/HiveBenchmarkQueryRunner.java @@ -0,0 +1,96 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.Session; +import com.facebook.presto.benchmark.BenchmarkSuite; +import com.facebook.presto.hive.metastore.InMemoryHiveMetastore; +import com.facebook.presto.metadata.InMemoryNodeManager; +import com.facebook.presto.testing.LocalQueryRunner; +import com.facebook.presto.tpch.TpchConnectorFactory; +import com.facebook.presto.type.TypeRegistry; +import com.google.common.collect.ImmutableMap; +import com.google.common.io.Files; +import io.airlift.testing.FileUtils; +import org.apache.hadoop.hive.metastore.api.Database; + +import java.io.File; +import java.io.IOException; +import java.util.Map; + +import static com.facebook.presto.spi.type.TimeZoneKey.UTC_KEY; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.Locale.ENGLISH; + +public final class HiveBenchmarkQueryRunner +{ + private HiveBenchmarkQueryRunner() + { + } + + public static void main(String[] args) + throws IOException + { + String outputDirectory = checkNotNull(System.getProperty("outputDirectory"), "Must specify -DoutputDirectory=..."); + File tempDir = Files.createTempDir(); + try (LocalQueryRunner localQueryRunner = createLocalQueryRunner(tempDir)) { + new BenchmarkSuite(localQueryRunner, outputDirectory).runAllBenchmarks(); + } + finally { + FileUtils.deleteRecursively(tempDir); + } + } + + public static LocalQueryRunner createLocalQueryRunner(File tempDir) + { + Session session = Session.builder() + .setUser("user") + .setSource("test") + .setCatalog("hive") + .setSchema("tpch") + .setTimeZoneKey(UTC_KEY) + .setLocale(ENGLISH) + .build(); + + LocalQueryRunner localQueryRunner = new LocalQueryRunner(session); + + // add tpch + InMemoryNodeManager nodeManager = localQueryRunner.getNodeManager(); + localQueryRunner.createCatalog("tpch", new TpchConnectorFactory(nodeManager, 1), ImmutableMap.of()); + + // add hive + InMemoryHiveMetastore metastore = new InMemoryHiveMetastore(); + File tpchDataDir = new File(tempDir, "tpch"); + tpchDataDir.mkdir(); + metastore.createDatabase(new Database("tpch", null, tpchDataDir.toURI().toString(), null)); + + HiveConnectorFactory hiveConnectorFactory = new HiveConnectorFactory( + "hive", + ImmutableMap.of("node.environment", "test"), + HiveBenchmarkQueryRunner.class.getClassLoader(), + metastore, + new TypeRegistry()); + + Map hiveCatalogConfig = ImmutableMap.builder() + .put("hive.metastore.uri", "thrift://none.invalid:0") + .put("hive.max-split-size", "10GB") + .build(); + + localQueryRunner.createCatalog("hive", hiveConnectorFactory, hiveCatalogConfig); + + localQueryRunner.execute("CREATE TABLE orders AS SELECT * FROM tpch.sf1.orders"); + localQueryRunner.execute("CREATE TABLE lineitem AS SELECT * FROM tpch.sf1.lineitem"); + return localQueryRunner; + } +} diff --git a/presto-hive/src/test/java/com/facebook/presto/hive/HiveQueryRunner.java b/presto-hive/src/test/java/com/facebook/presto/hive/HiveQueryRunner.java new file mode 100644 index 00000000..43a6f5d8 --- /dev/null +++ b/presto-hive/src/test/java/com/facebook/presto/hive/HiveQueryRunner.java @@ -0,0 +1,113 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.Session; +import com.facebook.presto.hive.metastore.InMemoryHiveMetastore; +import com.facebook.presto.testing.QueryRunner; +import com.facebook.presto.tests.DistributedQueryRunner; +import com.facebook.presto.tpch.TpchPlugin; +import com.facebook.presto.tpch.testing.SampledTpchPlugin; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.airlift.tpch.TpchTable; +import org.apache.hadoop.hive.metastore.api.Database; +import org.joda.time.DateTimeZone; + +import java.io.File; +import java.util.Map; + +import static com.facebook.presto.spi.type.TimeZoneKey.UTC_KEY; +import static com.facebook.presto.tests.QueryAssertions.copyTpchTables; +import static com.facebook.presto.tpch.TpchMetadata.TINY_SCHEMA_NAME; +import static java.util.Locale.ENGLISH; +import static org.testng.Assert.assertEquals; + +public final class HiveQueryRunner +{ + private HiveQueryRunner() + { + } + + private static final String TPCH_SCHEMA = "tpch"; + private static final String TPCH_SAMPLED_SCHEMA = "tpch_sampled"; + private static final DateTimeZone TIME_ZONE = DateTimeZone.forID("Asia/Kathmandu"); + + public static QueryRunner createQueryRunner(TpchTable... tables) + throws Exception + { + return createQueryRunner(ImmutableList.copyOf(tables)); + } + + public static QueryRunner createQueryRunner(Iterable> tables) + throws Exception + { + assertEquals(DateTimeZone.getDefault(), TIME_ZONE, "Timezone not configured correctly. Add -Duser.timezone=Asia/Katmandu to your JVM arguments"); + + DistributedQueryRunner queryRunner = new DistributedQueryRunner(createSession(), 4); + + try { + queryRunner.installPlugin(new TpchPlugin()); + queryRunner.createCatalog("tpch", "tpch"); + + queryRunner.installPlugin(new SampledTpchPlugin()); + queryRunner.createCatalog("tpch_sampled", "tpch_sampled"); + + File baseDir = queryRunner.getCoordinator().getBaseDataDir().toFile(); + InMemoryHiveMetastore metastore = new InMemoryHiveMetastore(); + metastore.createDatabase(new Database("tpch", null, new File(baseDir, "tpch").toURI().toString(), null)); + metastore.createDatabase(new Database("tpch_sampled", null, new File(baseDir, "tpch_sampled").toURI().toString(), null)); + + queryRunner.installPlugin(new HivePlugin("hive", metastore)); + Map hiveProperties = ImmutableMap.builder() + .put("hive.metastore.uri", "thrift://localhost:8080") + .put("hive.allow-drop-table", "true") + .put("hive.allow-rename-table", "true") + .put("hive.time-zone", TIME_ZONE.getID()) + .build(); + queryRunner.createCatalog("hive", "hive", hiveProperties); + + copyTpchTables(queryRunner, "tpch", TINY_SCHEMA_NAME, createSession(), tables); + copyTpchTables(queryRunner, "tpch_sampled", TINY_SCHEMA_NAME, createSampledSession(), tables); + + return queryRunner; + } + catch (Exception e) { + queryRunner.close(); + throw e; + } + } + + public static Session createSession() + { + return createHiveSession(TPCH_SCHEMA); + } + + public static Session createSampledSession() + { + return createHiveSession(TPCH_SAMPLED_SCHEMA); + } + + private static Session createHiveSession(String schema) + { + return Session.builder() + .setUser("user") + .setSource("test") + .setCatalog("hive") + .setSchema(schema) + .setTimeZoneKey(UTC_KEY) + .setLocale(ENGLISH) + .build(); + } +} diff --git a/presto-hive/src/test/java/com/facebook/presto/hive/HiveTestUtils.java b/presto-hive/src/test/java/com/facebook/presto/hive/HiveTestUtils.java new file mode 100644 index 00000000..0bf5688c --- /dev/null +++ b/presto-hive/src/test/java/com/facebook/presto/hive/HiveTestUtils.java @@ -0,0 +1,93 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.hive.orc.DwrfPageSourceFactory; +import com.facebook.presto.hive.orc.DwrfRecordCursorProvider; +import com.facebook.presto.hive.orc.OrcPageSourceFactory; +import com.facebook.presto.hive.orc.OrcRecordCursorProvider; +import com.facebook.presto.hive.rcfile.RcFilePageSourceFactory; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.block.BlockBuilderStatus; +import com.facebook.presto.spi.block.VariableWidthBlockBuilder; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.type.TypeRegistry; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import io.airlift.slice.Slice; + +import java.util.List; + +import static com.facebook.presto.type.TypeUtils.appendToBlockBuilder; +import static com.facebook.presto.type.TypeUtils.buildStructuralSlice; + +public final class HiveTestUtils +{ + private HiveTestUtils() + { + } + + public static final TypeRegistry TYPE_MANAGER = new TypeRegistry(); + + public static final ImmutableSet DEFAULT_HIVE_DATA_STREAM_FACTORIES = ImmutableSet.builder() + .add(new RcFilePageSourceFactory(TYPE_MANAGER)) + .add(new OrcPageSourceFactory(TYPE_MANAGER)) + .add(new DwrfPageSourceFactory(TYPE_MANAGER)) + .build(); + + public static final ImmutableSet DEFAULT_HIVE_RECORD_CURSOR_PROVIDER = ImmutableSet.builder() + .add(new OrcRecordCursorProvider()) + .add(new ParquetRecordCursorProvider(false)) + .add(new DwrfRecordCursorProvider()) + .add(new ColumnarTextHiveRecordCursorProvider()) + .add(new ColumnarBinaryHiveRecordCursorProvider()) + .add(new GenericHiveRecordCursorProvider()) + .build(); + + public static List getTypes(List columnHandles) + { + ImmutableList.Builder types = ImmutableList.builder(); + for (ColumnHandle columnHandle : columnHandles) { + types.add(TYPE_MANAGER.getType(((HiveColumnHandle) columnHandle).getTypeSignature())); + } + return types.build(); + } + + public static Slice arraySliceOf(Type elementType, Object... values) + { + BlockBuilder blockBuilder = new VariableWidthBlockBuilder(new BlockBuilderStatus(), 1024); + for (Object value : values) { + appendToBlockBuilder(elementType, value, blockBuilder); + } + return buildStructuralSlice(blockBuilder); + } + + public static Slice mapSliceOf(Type keyType, Type valueType, Object key, Object value) + { + BlockBuilder blockBuilder = new VariableWidthBlockBuilder(new BlockBuilderStatus(), 1024); + appendToBlockBuilder(keyType, key, blockBuilder); + appendToBlockBuilder(valueType, value, blockBuilder); + return buildStructuralSlice(blockBuilder); + } + + public static Slice rowSliceOf(List parameterTypes, Object... values) + { + BlockBuilder blockBuilder = new VariableWidthBlockBuilder(new BlockBuilderStatus(), 1024); + for (int i = 0; i < values.length; i++) { + appendToBlockBuilder(parameterTypes.get(i), values[i], blockBuilder); + } + return buildStructuralSlice(blockBuilder); + } +} diff --git a/presto-hive/src/test/java/com/facebook/presto/hive/MockAmazonS3.java b/presto-hive/src/test/java/com/facebook/presto/hive/MockAmazonS3.java new file mode 100644 index 00000000..c02f0f40 --- /dev/null +++ b/presto-hive/src/test/java/com/facebook/presto/hive/MockAmazonS3.java @@ -0,0 +1,766 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.amazonaws.AmazonClientException; +import com.amazonaws.AmazonServiceException; +import com.amazonaws.AmazonWebServiceRequest; +import com.amazonaws.HttpMethod; +import com.amazonaws.regions.Region; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.S3ClientOptions; +import com.amazonaws.services.s3.S3ResponseMetadata; +import com.amazonaws.services.s3.model.AbortMultipartUploadRequest; +import com.amazonaws.services.s3.model.AccessControlList; +import com.amazonaws.services.s3.model.AmazonS3Exception; +import com.amazonaws.services.s3.model.Bucket; +import com.amazonaws.services.s3.model.BucketCrossOriginConfiguration; +import com.amazonaws.services.s3.model.BucketLifecycleConfiguration; +import com.amazonaws.services.s3.model.BucketLoggingConfiguration; +import com.amazonaws.services.s3.model.BucketNotificationConfiguration; +import com.amazonaws.services.s3.model.BucketPolicy; +import com.amazonaws.services.s3.model.BucketTaggingConfiguration; +import com.amazonaws.services.s3.model.BucketVersioningConfiguration; +import com.amazonaws.services.s3.model.BucketWebsiteConfiguration; +import com.amazonaws.services.s3.model.CannedAccessControlList; +import com.amazonaws.services.s3.model.CompleteMultipartUploadRequest; +import com.amazonaws.services.s3.model.CompleteMultipartUploadResult; +import com.amazonaws.services.s3.model.CopyObjectRequest; +import com.amazonaws.services.s3.model.CopyObjectResult; +import com.amazonaws.services.s3.model.CopyPartRequest; +import com.amazonaws.services.s3.model.CopyPartResult; +import com.amazonaws.services.s3.model.CreateBucketRequest; +import com.amazonaws.services.s3.model.DeleteBucketCrossOriginConfigurationRequest; +import com.amazonaws.services.s3.model.DeleteBucketLifecycleConfigurationRequest; +import com.amazonaws.services.s3.model.DeleteBucketPolicyRequest; +import com.amazonaws.services.s3.model.DeleteBucketRequest; +import com.amazonaws.services.s3.model.DeleteBucketTaggingConfigurationRequest; +import com.amazonaws.services.s3.model.DeleteBucketWebsiteConfigurationRequest; +import com.amazonaws.services.s3.model.DeleteObjectRequest; +import com.amazonaws.services.s3.model.DeleteObjectsRequest; +import com.amazonaws.services.s3.model.DeleteObjectsResult; +import com.amazonaws.services.s3.model.DeleteVersionRequest; +import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest; +import com.amazonaws.services.s3.model.GetBucketAclRequest; +import com.amazonaws.services.s3.model.GetBucketLocationRequest; +import com.amazonaws.services.s3.model.GetBucketPolicyRequest; +import com.amazonaws.services.s3.model.GetBucketWebsiteConfigurationRequest; +import com.amazonaws.services.s3.model.GetObjectMetadataRequest; +import com.amazonaws.services.s3.model.GetObjectRequest; +import com.amazonaws.services.s3.model.InitiateMultipartUploadRequest; +import com.amazonaws.services.s3.model.InitiateMultipartUploadResult; +import com.amazonaws.services.s3.model.ListBucketsRequest; +import com.amazonaws.services.s3.model.ListMultipartUploadsRequest; +import com.amazonaws.services.s3.model.ListObjectsRequest; +import com.amazonaws.services.s3.model.ListPartsRequest; +import com.amazonaws.services.s3.model.ListVersionsRequest; +import com.amazonaws.services.s3.model.MultipartUploadListing; +import com.amazonaws.services.s3.model.ObjectListing; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.Owner; +import com.amazonaws.services.s3.model.PartListing; +import com.amazonaws.services.s3.model.PutObjectRequest; +import com.amazonaws.services.s3.model.PutObjectResult; +import com.amazonaws.services.s3.model.RestoreObjectRequest; +import com.amazonaws.services.s3.model.S3Object; +import com.amazonaws.services.s3.model.SetBucketAclRequest; +import com.amazonaws.services.s3.model.SetBucketCrossOriginConfigurationRequest; +import com.amazonaws.services.s3.model.SetBucketLifecycleConfigurationRequest; +import com.amazonaws.services.s3.model.SetBucketLoggingConfigurationRequest; +import com.amazonaws.services.s3.model.SetBucketNotificationConfigurationRequest; +import com.amazonaws.services.s3.model.SetBucketPolicyRequest; +import com.amazonaws.services.s3.model.SetBucketTaggingConfigurationRequest; +import com.amazonaws.services.s3.model.SetBucketVersioningConfigurationRequest; +import com.amazonaws.services.s3.model.SetBucketWebsiteConfigurationRequest; +import com.amazonaws.services.s3.model.StorageClass; +import com.amazonaws.services.s3.model.UploadPartRequest; +import com.amazonaws.services.s3.model.UploadPartResult; +import com.amazonaws.services.s3.model.VersionListing; + +import java.io.File; +import java.io.InputStream; +import java.net.URL; +import java.util.Date; +import java.util.List; + +import static org.apache.http.HttpStatus.SC_OK; + +public class MockAmazonS3 + implements AmazonS3 +{ + private int getObjectHttpCode = SC_OK; + private int getObjectMetadataHttpCode = SC_OK; + + public void setGetObjectHttpErrorCode(int getObjectHttpErrorCode) + { + this.getObjectHttpCode = getObjectHttpErrorCode; + } + + public void setGetObjectMetadataHttpCode(int getObjectMetadataHttpCode) + { + this.getObjectMetadataHttpCode = getObjectMetadataHttpCode; + } + + @Override + public void setEndpoint(String endpoint) + { + } + + @Override + public void setRegion(Region region) + throws IllegalArgumentException + { + } + + @Override + public void setS3ClientOptions(S3ClientOptions clientOptions) + { + } + + @Override + public void changeObjectStorageClass(String bucketName, String key, StorageClass newStorageClass) + throws AmazonClientException + { + } + + @Override + public void setObjectRedirectLocation(String bucketName, String key, String newRedirectLocation) + throws AmazonClientException + { + } + + @Override + public ObjectListing listObjects(String bucketName) + throws AmazonClientException + { + return null; + } + + @Override + public ObjectListing listObjects(String bucketName, String prefix) + throws AmazonClientException + { + return null; + } + + @Override + public ObjectListing listObjects(ListObjectsRequest listObjectsRequest) + throws AmazonClientException + { + return null; + } + + @Override + public ObjectListing listNextBatchOfObjects(ObjectListing previousObjectListing) + throws AmazonClientException + { + return null; + } + + @Override + public VersionListing listVersions(String bucketName, String prefix) + throws AmazonClientException + { + return null; + } + + @Override + public VersionListing listNextBatchOfVersions(VersionListing previousVersionListing) + throws AmazonClientException + { + return null; + } + + @Override + public VersionListing listVersions(String bucketName, String prefix, String keyMarker, String versionIdMarker, String delimiter, Integer maxResults) + throws AmazonClientException + { + return null; + } + + @Override + public VersionListing listVersions(ListVersionsRequest listVersionsRequest) + throws AmazonClientException + { + return null; + } + + @Override + public Owner getS3AccountOwner() + throws AmazonClientException + { + return null; + } + + @Override + public boolean doesBucketExist(String bucketName) + throws AmazonClientException + { + return false; + } + + @Override + public List listBuckets() + throws AmazonClientException + { + return null; + } + + @Override + public List listBuckets(ListBucketsRequest listBucketsRequest) + throws AmazonClientException + { + return null; + } + + @Override + public String getBucketLocation(String bucketName) + throws AmazonClientException + { + return null; + } + + @Override + public String getBucketLocation(GetBucketLocationRequest getBucketLocationRequest) + throws AmazonClientException + { + return null; + } + + @Override + public Bucket createBucket(CreateBucketRequest createBucketRequest) + throws AmazonClientException + { + return null; + } + + @Override + public Bucket createBucket(String bucketName) + throws AmazonClientException + { + return null; + } + + @Override + public Bucket createBucket(String bucketName, com.amazonaws.services.s3.model.Region region) + throws AmazonClientException + { + return null; + } + + @Override + public Bucket createBucket(String bucketName, String region) + throws AmazonClientException + { + return null; + } + + @Override + public AccessControlList getObjectAcl(String bucketName, String key) + throws AmazonClientException + { + return null; + } + + @Override + public AccessControlList getObjectAcl(String bucketName, String key, String versionId) + throws AmazonClientException + { + return null; + } + + @Override + public void setObjectAcl(String bucketName, String key, AccessControlList acl) + throws AmazonClientException + { + } + + @Override + public void setObjectAcl(String bucketName, String key, CannedAccessControlList acl) + throws AmazonClientException + { + } + + @Override + public void setObjectAcl(String bucketName, String key, String versionId, AccessControlList acl) + throws AmazonClientException + { + } + + @Override + public void setObjectAcl(String bucketName, String key, String versionId, CannedAccessControlList acl) + throws AmazonClientException + { + } + + @Override + public AccessControlList getBucketAcl(String bucketName) + throws AmazonClientException + { + return null; + } + + @Override + public void setBucketAcl(SetBucketAclRequest setBucketAclRequest) + throws AmazonClientException + { + } + + @Override + public AccessControlList getBucketAcl(GetBucketAclRequest getBucketAclRequest) + throws AmazonClientException + { + return null; + } + + @Override + public void setBucketAcl(String bucketName, AccessControlList acl) + throws AmazonClientException + { + } + + @Override + public void setBucketAcl(String bucketName, CannedAccessControlList acl) + throws AmazonClientException + { + } + + @Override + public ObjectMetadata getObjectMetadata(String bucketName, String key) + throws AmazonClientException + { + if (getObjectMetadataHttpCode != SC_OK) { + AmazonS3Exception exception = new AmazonS3Exception("Failing getObjectMetadata call with " + getObjectMetadataHttpCode); + exception.setStatusCode(getObjectMetadataHttpCode); + throw exception; + } + return null; + } + + @Override + public ObjectMetadata getObjectMetadata(GetObjectMetadataRequest getObjectMetadataRequest) + throws AmazonClientException + { + return null; + } + + @Override + public S3Object getObject(String bucketName, String key) + throws AmazonClientException + { + return null; + } + + @Override + public S3Object getObject(GetObjectRequest getObjectRequest) + throws AmazonClientException + { + if (getObjectHttpCode != SC_OK) { + AmazonS3Exception exception = new AmazonS3Exception("Failing getObject call with " + getObjectHttpCode); + exception.setStatusCode(getObjectHttpCode); + throw exception; + } + return null; + } + + @Override + public ObjectMetadata getObject(GetObjectRequest getObjectRequest, File destinationFile) + throws AmazonClientException + { + return null; + } + + @Override + public void deleteBucket(DeleteBucketRequest deleteBucketRequest) + throws AmazonClientException + { + } + + @Override + public void deleteBucket(String bucketName) + throws AmazonClientException + { + } + + @Override + public PutObjectResult putObject(PutObjectRequest putObjectRequest) + throws AmazonClientException + { + return null; + } + + @Override + public PutObjectResult putObject(String bucketName, String key, File file) + throws AmazonClientException + { + return null; + } + + @Override + public PutObjectResult putObject(String bucketName, String key, InputStream input, ObjectMetadata metadata) + throws AmazonClientException + { + return null; + } + + @Override + public CopyObjectResult copyObject(String sourceBucketName, String sourceKey, String destinationBucketName, String destinationKey) + throws AmazonClientException + { + return null; + } + + @Override + public CopyObjectResult copyObject(CopyObjectRequest copyObjectRequest) + throws AmazonClientException + { + return null; + } + + @Override + public CopyPartResult copyPart(CopyPartRequest copyPartRequest) + throws AmazonClientException + { + return null; + } + + @Override + public void deleteObject(String bucketName, String key) + throws AmazonClientException + { + } + + @Override + public void deleteObject(DeleteObjectRequest deleteObjectRequest) + throws AmazonClientException + { + } + + @Override + public DeleteObjectsResult deleteObjects(DeleteObjectsRequest deleteObjectsRequest) + throws AmazonClientException + { + return null; + } + + @Override + public void deleteVersion(String bucketName, String key, String versionId) + throws AmazonClientException + { + } + + @Override + public void deleteVersion(DeleteVersionRequest deleteVersionRequest) + throws AmazonClientException + { + } + + @Override + public BucketLoggingConfiguration getBucketLoggingConfiguration(String bucketName) + throws AmazonClientException + { + return null; + } + + @Override + public void setBucketLoggingConfiguration(SetBucketLoggingConfigurationRequest setBucketLoggingConfigurationRequest) + throws AmazonClientException + { + } + + @Override + public BucketVersioningConfiguration getBucketVersioningConfiguration(String bucketName) + throws AmazonClientException + { + return null; + } + + @Override + public void setBucketVersioningConfiguration(SetBucketVersioningConfigurationRequest setBucketVersioningConfigurationRequest) + throws AmazonClientException + { + } + + @Override + public BucketLifecycleConfiguration getBucketLifecycleConfiguration(String bucketName) + { + return null; + } + + @Override + public void setBucketLifecycleConfiguration(String bucketName, BucketLifecycleConfiguration bucketLifecycleConfiguration) + { + } + + @Override + public void setBucketLifecycleConfiguration(SetBucketLifecycleConfigurationRequest setBucketLifecycleConfigurationRequest) + { + } + + @Override + public void deleteBucketLifecycleConfiguration(String bucketName) + { + } + + @Override + public void deleteBucketLifecycleConfiguration(DeleteBucketLifecycleConfigurationRequest deleteBucketLifecycleConfigurationRequest) + { + } + + @Override + public BucketCrossOriginConfiguration getBucketCrossOriginConfiguration(String bucketName) + { + return null; + } + + @Override + public void setBucketCrossOriginConfiguration(String bucketName, BucketCrossOriginConfiguration bucketCrossOriginConfiguration) + { + } + + @Override + public void setBucketCrossOriginConfiguration(SetBucketCrossOriginConfigurationRequest setBucketCrossOriginConfigurationRequest) + { + } + + @Override + public void deleteBucketCrossOriginConfiguration(String bucketName) + { + } + + @Override + public void deleteBucketCrossOriginConfiguration(DeleteBucketCrossOriginConfigurationRequest deleteBucketCrossOriginConfigurationRequest) + { + } + + @Override + public BucketTaggingConfiguration getBucketTaggingConfiguration(String bucketName) + { + return null; + } + + @Override + public void setBucketTaggingConfiguration(String bucketName, BucketTaggingConfiguration bucketTaggingConfiguration) + { + } + + @Override + public void setBucketTaggingConfiguration(SetBucketTaggingConfigurationRequest setBucketTaggingConfigurationRequest) + { + } + + @Override + public void deleteBucketTaggingConfiguration(String bucketName) + { + } + + @Override + public void deleteBucketTaggingConfiguration(DeleteBucketTaggingConfigurationRequest deleteBucketTaggingConfigurationRequest) + { + } + + @Override + public BucketNotificationConfiguration getBucketNotificationConfiguration(String bucketName) + throws AmazonClientException + { + return null; + } + + @Override + public void setBucketNotificationConfiguration(SetBucketNotificationConfigurationRequest setBucketNotificationConfigurationRequest) + throws AmazonClientException + { + } + + @Override + public void setBucketNotificationConfiguration(String bucketName, BucketNotificationConfiguration bucketNotificationConfiguration) + throws AmazonClientException + { + } + + @Override + public BucketWebsiteConfiguration getBucketWebsiteConfiguration(String bucketName) + throws AmazonClientException + { + return null; + } + + @Override + public BucketWebsiteConfiguration getBucketWebsiteConfiguration(GetBucketWebsiteConfigurationRequest getBucketWebsiteConfigurationRequest) + throws AmazonClientException + { + return null; + } + + @Override + public void setBucketWebsiteConfiguration(String bucketName, BucketWebsiteConfiguration configuration) + throws AmazonClientException + { + } + + @Override + public void setBucketWebsiteConfiguration(SetBucketWebsiteConfigurationRequest setBucketWebsiteConfigurationRequest) + throws AmazonClientException + { + } + + @Override + public void deleteBucketWebsiteConfiguration(String bucketName) + throws AmazonClientException + { + } + + @Override + public void deleteBucketWebsiteConfiguration(DeleteBucketWebsiteConfigurationRequest deleteBucketWebsiteConfigurationRequest) + throws AmazonClientException + { + } + + @Override + public BucketPolicy getBucketPolicy(String bucketName) + throws AmazonClientException + { + return null; + } + + @Override + public BucketPolicy getBucketPolicy(GetBucketPolicyRequest getBucketPolicyRequest) + throws AmazonClientException + { + return null; + } + + @Override + public void setBucketPolicy(String bucketName, String policyText) + throws AmazonClientException + { + } + + @Override + public void setBucketPolicy(SetBucketPolicyRequest setBucketPolicyRequest) + throws AmazonClientException + { + } + + @Override + public void deleteBucketPolicy(String bucketName) + throws AmazonClientException + { + } + + @Override + public void deleteBucketPolicy(DeleteBucketPolicyRequest deleteBucketPolicyRequest) + throws AmazonClientException + { + } + + @Override + public URL generatePresignedUrl(String bucketName, String key, Date expiration) + throws AmazonClientException + { + return null; + } + + @Override + public URL generatePresignedUrl(String bucketName, String key, Date expiration, HttpMethod method) + throws AmazonClientException + { + return null; + } + + @Override + public URL generatePresignedUrl(GeneratePresignedUrlRequest generatePresignedUrlRequest) + throws AmazonClientException + { + return null; + } + + @Override + public InitiateMultipartUploadResult initiateMultipartUpload(InitiateMultipartUploadRequest request) + throws AmazonClientException + { + return null; + } + + @Override + public UploadPartResult uploadPart(UploadPartRequest request) + throws AmazonClientException + { + return null; + } + + @Override + public PartListing listParts(ListPartsRequest request) + throws AmazonClientException + { + return null; + } + + @Override + public void abortMultipartUpload(AbortMultipartUploadRequest request) + throws AmazonClientException + { + } + + @Override + public CompleteMultipartUploadResult completeMultipartUpload(CompleteMultipartUploadRequest request) + throws AmazonClientException + { + return null; + } + + @Override + public MultipartUploadListing listMultipartUploads(ListMultipartUploadsRequest request) + throws AmazonClientException + { + return null; + } + + @Override + public S3ResponseMetadata getCachedResponseMetadata(AmazonWebServiceRequest request) + { + return null; + } + + @Override + public void restoreObject(RestoreObjectRequest request) + throws AmazonServiceException + { + } + + @Override + public void restoreObject(String bucketName, String key, int expirationInDays) + throws AmazonServiceException + { + } + + @Override + public void enableRequesterPays(String bucketName) + throws AmazonClientException + { + } + + @Override + public void disableRequesterPays(String bucketName) + throws AmazonClientException + { + } + + @Override + public boolean isRequesterPaysEnabled(String bucketName) + throws AmazonClientException + { + return false; + } +} diff --git a/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveBooleanParser.java b/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveBooleanParser.java new file mode 100644 index 00000000..15cf60bd --- /dev/null +++ b/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveBooleanParser.java @@ -0,0 +1,51 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import org.testng.annotations.Test; + +import static com.facebook.presto.hive.HiveBooleanParser.parseHiveBoolean; +import static java.nio.charset.StandardCharsets.US_ASCII; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; + +public class TestHiveBooleanParser +{ + @Test + public void testParse() + { + assertTrue(parseBoolean("true")); + assertTrue(parseBoolean("TRUE")); + assertTrue(parseBoolean("tRuE")); + + assertFalse(parseBoolean("false")); + assertFalse(parseBoolean("FALSE")); + assertFalse(parseBoolean("fAlSe")); + + assertNull(parseBoolean("true ")); + assertNull(parseBoolean(" true")); + assertNull(parseBoolean("false ")); + assertNull(parseBoolean(" false")); + assertNull(parseBoolean("t")); + assertNull(parseBoolean("f")); + assertNull(parseBoolean("")); + assertNull(parseBoolean("blah")); + } + + private static Boolean parseBoolean(String s) + { + return parseHiveBoolean(s.getBytes(US_ASCII), 0, s.length()); + } +} diff --git a/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveBucketing.java b/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveBucketing.java new file mode 100644 index 00000000..57ada3cf --- /dev/null +++ b/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveBucketing.java @@ -0,0 +1,68 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.google.common.collect.ImmutableList; +import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector; +import org.testng.annotations.Test; + +import java.util.List; +import java.util.Optional; + +import static com.facebook.presto.hive.HiveBucketing.HiveBucket; +import static com.google.common.collect.Maps.immutableEntry; +import static io.airlift.slice.Slices.utf8Slice; +import static java.util.Map.Entry; +import static org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory.javaBooleanObjectInspector; +import static org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory.javaLongObjectInspector; +import static org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory.javaStringObjectInspector; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +public class TestHiveBucketing +{ + @Test + public void testHashingBooleanLong() + throws Exception + { + List> bindings = ImmutableList.>builder() + .add(entry(javaBooleanObjectInspector, true)) + .add(entry(javaLongObjectInspector, 123L)) + .build(); + + Optional bucket = HiveBucketing.getHiveBucket(bindings, 32); + assertTrue(bucket.isPresent()); + assertEquals(bucket.get().getBucketCount(), 32); + assertEquals(bucket.get().getBucketNumber(), 26); + } + + @Test + public void testHashingString() + throws Exception + { + List> bindings = ImmutableList.>builder() + .add(entry(javaStringObjectInspector, utf8Slice("sequencefile test"))) + .build(); + + Optional bucket = HiveBucketing.getHiveBucket(bindings, 32); + assertTrue(bucket.isPresent()); + assertEquals(bucket.get().getBucketCount(), 32); + assertEquals(bucket.get().getBucketNumber(), 21); + } + + private static Entry entry(ObjectInspector inspector, Object value) + { + return immutableEntry(inspector, value); + } +} diff --git a/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveClientConfig.java b/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveClientConfig.java new file mode 100644 index 00000000..4b79ccd6 --- /dev/null +++ b/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveClientConfig.java @@ -0,0 +1,186 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.google.common.base.StandardSystemProperty; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.net.HostAndPort; +import io.airlift.configuration.testing.ConfigAssertions; +import io.airlift.units.DataSize; +import io.airlift.units.DataSize.Unit; +import io.airlift.units.Duration; +import org.testng.annotations.Test; + +import java.io.File; +import java.util.Map; +import java.util.TimeZone; +import java.util.concurrent.TimeUnit; + +import static com.facebook.presto.hive.TestHiveUtil.nonDefaultTimeZone; + +public class TestHiveClientConfig +{ + @Test + public void testDefaults() + { + ConfigAssertions.assertRecordedDefaults(ConfigAssertions.recordDefaults(HiveClientConfig.class) + .setTimeZone(TimeZone.getDefault().getID()) + .setMaxSplitSize(new DataSize(64, Unit.MEGABYTE)) + .setMaxOutstandingSplits(1_000) + .setMaxSplitIteratorThreads(1_000) + .setAllowDropTable(false) + .setAllowRenameTable(false) + .setAllowCorruptWritesForTesting(false) + .setMetastoreCacheTtl(new Duration(1, TimeUnit.HOURS)) + .setMetastoreRefreshInterval(new Duration(1, TimeUnit.SECONDS)) + .setMaxMetastoreRefreshThreads(100) + .setMetastoreSocksProxy(null) + .setMetastoreTimeout(new Duration(10, TimeUnit.SECONDS)) + .setMinPartitionBatchSize(10) + .setMaxPartitionBatchSize(100) + .setMaxInitialSplits(200) + .setMaxInitialSplitSize(new DataSize(32, Unit.MEGABYTE)) + .setForceLocalScheduling(false) + .setRecursiveDirWalkerEnabled(false) + .setDfsTimeout(new Duration(10, TimeUnit.SECONDS)) + .setDfsConnectTimeout(new Duration(500, TimeUnit.MILLISECONDS)) + .setDfsConnectMaxRetries(5) + .setVerifyChecksum(true) + .setResourceConfigFiles((String) null) + .setHiveStorageFormat(HiveStorageFormat.ORC) + .setDomainSocketPath(null) + .setUseParquetColumnNames(false) + .setS3AwsAccessKey(null) + .setS3AwsSecretKey(null) + .setS3UseInstanceCredentials(true) + .setS3SslEnabled(true) + .setS3MaxClientRetries(3) + .setS3MaxErrorRetries(10) + .setS3MaxBackoffTime(new Duration(10, TimeUnit.MINUTES)) + .setS3MaxRetryTime(new Duration(10, TimeUnit.MINUTES)) + .setS3ConnectTimeout(new Duration(5, TimeUnit.SECONDS)) + .setS3SocketTimeout(new Duration(5, TimeUnit.SECONDS)) + .setS3MultipartMinFileSize(new DataSize(16, Unit.MEGABYTE)) + .setS3MultipartMinPartSize(new DataSize(5, Unit.MEGABYTE)) + .setS3MaxConnections(500) + .setS3StagingDirectory(new File(StandardSystemProperty.JAVA_IO_TMPDIR.value())) + .setOptimizedReaderEnabled(true) + .setAssumeCanonicalPartitionKeys(false) + .setOrcMaxMergeDistance(new DataSize(1, Unit.MEGABYTE)) + .setOrcMaxBufferSize(new DataSize(8, Unit.MEGABYTE)) + .setOrcStreamBufferSize(new DataSize(8, Unit.MEGABYTE))); + } + + @Test + public void testExplicitPropertyMappings() + { + Map properties = new ImmutableMap.Builder() + .put("hive.time-zone", nonDefaultTimeZone().getID()) + .put("hive.max-split-size", "256MB") + .put("hive.max-outstanding-splits", "10") + .put("hive.max-split-iterator-threads", "10") + .put("hive.allow-drop-table", "true") + .put("hive.allow-rename-table", "true") + .put("hive.allow-corrupt-writes-for-testing", "true") + .put("hive.metastore-cache-ttl", "2h") + .put("hive.metastore-refresh-interval", "30m") + .put("hive.metastore-refresh-max-threads", "2500") + .put("hive.metastore.thrift.client.socks-proxy", "localhost:1080") + .put("hive.metastore-timeout", "20s") + .put("hive.metastore.partition-batch-size.min", "1") + .put("hive.metastore.partition-batch-size.max", "1000") + .put("hive.dfs-timeout", "33s") + .put("hive.dfs.connect.timeout", "20s") + .put("hive.dfs.connect.max-retries", "10") + .put("hive.dfs.verify-checksum", "false") + .put("hive.config.resources", "/foo.xml,/bar.xml") + .put("hive.max-initial-splits", "10") + .put("hive.max-initial-split-size", "16MB") + .put("hive.recursive-directories", "true") + .put("hive.storage-format", "SEQUENCEFILE") + .put("hive.force-local-scheduling", "true") + .put("hive.assume-canonical-partition-keys", "true") + .put("dfs.domain-socket-path", "/foo") + .put("hive.parquet.use-column-names", "true") + .put("hive.s3.aws-access-key", "abc123") + .put("hive.s3.aws-secret-key", "secret") + .put("hive.s3.use-instance-credentials", "false") + .put("hive.s3.ssl.enabled", "false") + .put("hive.s3.max-client-retries", "9") + .put("hive.s3.max-error-retries", "8") + .put("hive.s3.max-backoff-time", "4m") + .put("hive.s3.max-retry-time", "20m") + .put("hive.s3.connect-timeout", "8s") + .put("hive.s3.socket-timeout", "4m") + .put("hive.s3.multipart.min-file-size", "32MB") + .put("hive.s3.multipart.min-part-size", "15MB") + .put("hive.s3.max-connections", "77") + .put("hive.s3.staging-directory", "/s3-staging") + .put("hive.optimized-reader.enabled", "false") + .put("hive.orc.max-merge-distance", "22kB") + .put("hive.orc.max-buffer-size", "44kB") + .put("hive.orc.stream-buffer-size", "55kB") + .build(); + + HiveClientConfig expected = new HiveClientConfig() + .setTimeZone(nonDefaultTimeZone().toTimeZone()) + .setMaxSplitSize(new DataSize(256, Unit.MEGABYTE)) + .setMaxOutstandingSplits(10) + .setMaxSplitIteratorThreads(10) + .setAllowDropTable(true) + .setAllowRenameTable(true) + .setAllowCorruptWritesForTesting(true) + .setMetastoreCacheTtl(new Duration(2, TimeUnit.HOURS)) + .setMetastoreRefreshInterval(new Duration(30, TimeUnit.MINUTES)) + .setMaxMetastoreRefreshThreads(2500) + .setMetastoreSocksProxy(HostAndPort.fromParts("localhost", 1080)) + .setMetastoreTimeout(new Duration(20, TimeUnit.SECONDS)) + .setMinPartitionBatchSize(1) + .setMaxPartitionBatchSize(1000) + .setMaxInitialSplits(10) + .setMaxInitialSplitSize(new DataSize(16, Unit.MEGABYTE)) + .setForceLocalScheduling(true) + .setRecursiveDirWalkerEnabled(true) + .setDfsTimeout(new Duration(33, TimeUnit.SECONDS)) + .setDfsConnectTimeout(new Duration(20, TimeUnit.SECONDS)) + .setDfsConnectMaxRetries(10) + .setVerifyChecksum(false) + .setResourceConfigFiles(ImmutableList.of("/foo.xml", "/bar.xml")) + .setHiveStorageFormat(HiveStorageFormat.SEQUENCEFILE) + .setDomainSocketPath("/foo") + .setUseParquetColumnNames(true) + .setS3AwsAccessKey("abc123") + .setS3AwsSecretKey("secret") + .setS3UseInstanceCredentials(false) + .setS3SslEnabled(false) + .setS3MaxClientRetries(9) + .setS3MaxErrorRetries(8) + .setS3MaxBackoffTime(new Duration(4, TimeUnit.MINUTES)) + .setS3MaxRetryTime(new Duration(20, TimeUnit.MINUTES)) + .setS3ConnectTimeout(new Duration(8, TimeUnit.SECONDS)) + .setS3SocketTimeout(new Duration(4, TimeUnit.MINUTES)) + .setS3MultipartMinFileSize(new DataSize(32, Unit.MEGABYTE)) + .setS3MultipartMinPartSize(new DataSize(15, Unit.MEGABYTE)) + .setS3MaxConnections(77) + .setS3StagingDirectory(new File("/s3-staging")) + .setOptimizedReaderEnabled(false) + .setAssumeCanonicalPartitionKeys(true) + .setOrcMaxMergeDistance(new DataSize(22, Unit.KILOBYTE)) + .setOrcMaxBufferSize(new DataSize(44, Unit.KILOBYTE)) + .setOrcStreamBufferSize(new DataSize(55, Unit.KILOBYTE)); + + ConfigAssertions.assertFullMapping(properties, expected); + } +} diff --git a/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveColumnHandle.java b/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveColumnHandle.java new file mode 100644 index 00000000..0c40fc0e --- /dev/null +++ b/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveColumnHandle.java @@ -0,0 +1,42 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.spi.type.StandardTypes; +import io.airlift.json.JsonCodec; +import org.testng.annotations.Test; + +import static com.facebook.presto.spi.type.TypeSignature.parseTypeSignature; +import static org.testng.Assert.assertEquals; + +public class TestHiveColumnHandle +{ + private final JsonCodec codec = JsonCodec.jsonCodec(HiveColumnHandle.class); + + @Test + public void testRoundTrip() + { + HiveColumnHandle expected = new HiveColumnHandle("client", "name", 42, HiveType.HIVE_FLOAT, parseTypeSignature(StandardTypes.DOUBLE), 88, true); + + String json = codec.toJson(expected); + HiveColumnHandle actual = codec.fromJson(json); + + assertEquals(actual.getClientId(), expected.getClientId()); + assertEquals(actual.getName(), expected.getName()); + assertEquals(actual.getOrdinalPosition(), expected.getOrdinalPosition()); + assertEquals(actual.getHiveType(), expected.getHiveType()); + assertEquals(actual.getHiveColumnIndex(), expected.getHiveColumnIndex()); + assertEquals(actual.isPartitionKey(), expected.isPartitionKey()); + } +} diff --git a/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveConnectorFactory.java b/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveConnectorFactory.java new file mode 100644 index 00000000..2b5a36c8 --- /dev/null +++ b/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveConnectorFactory.java @@ -0,0 +1,54 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.spi.Connector; +import com.facebook.presto.spi.ConnectorPageSourceProvider; +import com.facebook.presto.spi.classloader.ClassLoaderSafeConnectorHandleResolver; +import com.facebook.presto.spi.classloader.ClassLoaderSafeConnectorMetadata; +import com.facebook.presto.spi.classloader.ClassLoaderSafeConnectorSplitManager; +import com.facebook.presto.type.TypeRegistry; +import com.google.common.collect.ImmutableMap; +import org.testng.annotations.Test; + +import static io.airlift.testing.Assertions.assertInstanceOf; + +public class TestHiveConnectorFactory +{ + @Test + public void testGetClient() + { + assertCreateConnector("thrift://localhost:1234"); + assertCreateConnector("discovery::"); + } + + private static void assertCreateConnector(String metastoreUri) + { + HiveConnectorFactory connectorFactory = new HiveConnectorFactory( + "hive-test", + ImmutableMap.builder() + .put("node.environment", "test") + .put("hive.metastore.uri", metastoreUri) + .build(), + HiveConnector.class.getClassLoader(), + null, + new TypeRegistry()); + + Connector connector = connectorFactory.create("hive-test", ImmutableMap.of()); + assertInstanceOf(connector.getMetadata(), ClassLoaderSafeConnectorMetadata.class); + assertInstanceOf(connector.getSplitManager(), ClassLoaderSafeConnectorSplitManager.class); + assertInstanceOf(connector.getPageSourceProvider(), ConnectorPageSourceProvider.class); + assertInstanceOf(connector.getHandleResolver(), ClassLoaderSafeConnectorHandleResolver.class); + } +} diff --git a/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveDistributedQueries.java b/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveDistributedQueries.java new file mode 100644 index 00000000..f90a7f23 --- /dev/null +++ b/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveDistributedQueries.java @@ -0,0 +1,51 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.tests.AbstractTestDistributedQueries; + +import static com.facebook.presto.hive.HiveQueryRunner.createQueryRunner; +import static com.facebook.presto.hive.HiveQueryRunner.createSampledSession; +import static io.airlift.tpch.TpchTable.getTables; + +public class TestHiveDistributedQueries + extends AbstractTestDistributedQueries +{ + public TestHiveDistributedQueries() + throws Exception + { + super(createQueryRunner(getTables()), createSampledSession()); + } + + @Override + public void testInsert() + throws Exception + { + // Hive connector currently does not support insert + } + + @Override + public void testRenameColumn() + throws Exception + { + // Hive connector currently does not support rename column + } + + @Override + public void testDelete() + throws Exception + { + // Hive connector currently does not support delete + } +} diff --git a/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveFileFormats.java b/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveFileFormats.java new file mode 100644 index 00000000..421ff2f6 --- /dev/null +++ b/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveFileFormats.java @@ -0,0 +1,478 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.hive.orc.DwrfPageSourceFactory; +import com.facebook.presto.hive.orc.DwrfRecordCursorProvider; +import com.facebook.presto.hive.orc.OrcPageSourceFactory; +import com.facebook.presto.hive.orc.OrcRecordCursorProvider; +import com.facebook.presto.hive.rcfile.RcFilePageSourceFactory; +import com.facebook.presto.spi.ConnectorPageSource; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.TupleDomain; +import com.facebook.presto.spi.type.TimeZoneKey; +import com.facebook.presto.type.ArrayType; +import com.facebook.presto.type.RowType; +import com.facebook.presto.type.TypeRegistry; +import com.google.common.base.Joiner; +import com.google.common.base.Predicate; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.hive.ql.io.HiveOutputFormat; +import org.apache.hadoop.hive.ql.io.RCFileInputFormat; +import org.apache.hadoop.hive.ql.io.RCFileOutputFormat; +import org.apache.hadoop.hive.ql.io.parquet.MapredParquetInputFormat; +import org.apache.hadoop.hive.ql.io.parquet.MapredParquetOutputFormat; +import org.apache.hadoop.hive.ql.io.parquet.serde.ParquetHiveSerDe; +import org.apache.hadoop.hive.serde2.SerDe; +import org.apache.hadoop.hive.serde2.columnar.ColumnarSerDe; +import org.apache.hadoop.hive.serde2.columnar.LazyBinaryColumnarSerDe; +import org.apache.hadoop.hive.serde2.objectinspector.ListObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.MapObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.PrimitiveObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.PrimitiveObjectInspector.PrimitiveCategory; +import org.apache.hadoop.hive.serde2.objectinspector.StructField; +import org.apache.hadoop.hive.serde2.objectinspector.StructObjectInspector; +import org.apache.hadoop.mapred.FileSplit; +import org.apache.hadoop.mapred.InputFormat; +import org.joda.time.DateTimeZone; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.Optional; +import java.util.Properties; +import java.util.TimeZone; + +import static com.facebook.presto.hive.HiveTestUtils.getTypes; +import static com.facebook.presto.hive.HiveTestUtils.arraySliceOf; +import static com.facebook.presto.hive.HiveTestUtils.rowSliceOf; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static com.google.common.base.Predicates.not; +import static com.google.common.collect.Iterables.filter; +import static com.google.common.collect.Iterables.transform; +import static java.util.Locale.ENGLISH; +import static java.util.stream.Collectors.toList; +import static org.apache.hadoop.hive.metastore.api.hive_metastoreConstants.FILE_INPUT_FORMAT; +import static org.apache.hadoop.hive.serde.serdeConstants.SERIALIZATION_LIB; +import static org.apache.hadoop.hive.serde2.objectinspector.ObjectInspectorFactory.getStandardListObjectInspector; +import static org.apache.hadoop.hive.serde2.objectinspector.ObjectInspectorFactory.getStandardStructObjectInspector; +import static org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory.javaStringObjectInspector; +import static org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory.javaIntObjectInspector; +import static org.testng.Assert.assertEquals; + +public class TestHiveFileFormats + extends AbstractTestHiveFileFormats +{ + private static final TimeZoneKey TIME_ZONE_KEY = TimeZoneKey.getTimeZoneKey(DateTimeZone.getDefault().getID()); + private static final ConnectorSession SESSION = new ConnectorSession("user", TIME_ZONE_KEY, ENGLISH, System.currentTimeMillis(), null); + private static final TypeRegistry TYPE_MANAGER = new TypeRegistry(); + + @BeforeClass(alwaysRun = true) + public void setUp() + throws Exception + { + // ensure the expected timezone is configured for this VM + assertEquals(TimeZone.getDefault().getID(), + "Asia/Katmandu", + "Timezone not configured correctly. Add -Duser.timezone=Asia/Katmandu to your JVM arguments"); + } + + @Test + public void testRCText() + throws Exception + { + List testColumns = ImmutableList.copyOf(filter(TEST_COLUMNS, new Predicate() + { + @Override + public boolean apply(TestColumn testColumn) + { + // TODO: This is a bug in the RC text reader + return !testColumn.getName().equals("t_struct_null"); + } + })); + + HiveOutputFormat outputFormat = new RCFileOutputFormat(); + InputFormat inputFormat = new RCFileInputFormat<>(); + @SuppressWarnings("deprecation") + SerDe serde = new ColumnarSerDe(); + File file = File.createTempFile("presto_test", "rc-text"); + try { + FileSplit split = createTestFile(file.getAbsolutePath(), outputFormat, serde, null, testColumns, NUM_ROWS); + testCursorProvider(new ColumnarTextHiveRecordCursorProvider(), split, inputFormat, serde, testColumns, NUM_ROWS); + testCursorProvider(new GenericHiveRecordCursorProvider(), split, inputFormat, serde, testColumns, NUM_ROWS); + } + finally { + //noinspection ResultOfMethodCallIgnored + file.delete(); + } + } + + @Test(enabled = false) + public void testRcTextPageSource() + throws Exception + { + HiveOutputFormat outputFormat = new RCFileOutputFormat(); + InputFormat inputFormat = new RCFileInputFormat<>(); + @SuppressWarnings("deprecation") + SerDe serde = new ColumnarSerDe(); + File file = File.createTempFile("presto_test", "rc-binary"); + file.delete(); + try { + FileSplit split = createTestFile(file.getAbsolutePath(), outputFormat, serde, null, TEST_COLUMNS, NUM_ROWS); + testPageSourceFactory(new RcFilePageSourceFactory(TYPE_MANAGER), split, inputFormat, serde, TEST_COLUMNS); + } + finally { + //noinspection ResultOfMethodCallIgnored + file.delete(); + } + } + + @Test + public void testRCBinary() + throws Exception + { + HiveOutputFormat outputFormat = new RCFileOutputFormat(); + InputFormat inputFormat = new RCFileInputFormat<>(); + @SuppressWarnings("deprecation") + SerDe serde = new LazyBinaryColumnarSerDe(); + File file = File.createTempFile("presto_test", "rc-binary"); + try { + FileSplit split = createTestFile(file.getAbsolutePath(), outputFormat, serde, null, TEST_COLUMNS, NUM_ROWS); + testCursorProvider(new ColumnarBinaryHiveRecordCursorProvider(), split, inputFormat, serde, TEST_COLUMNS, NUM_ROWS); + testCursorProvider(new GenericHiveRecordCursorProvider(), split, inputFormat, serde, TEST_COLUMNS, NUM_ROWS); + } + finally { + //noinspection ResultOfMethodCallIgnored + file.delete(); + } + } + + @Test(enabled = false) + public void testRcBinaryPageSource() + throws Exception + { + HiveOutputFormat outputFormat = new RCFileOutputFormat(); + InputFormat inputFormat = new RCFileInputFormat<>(); + @SuppressWarnings("deprecation") + SerDe serde = new LazyBinaryColumnarSerDe(); + File file = File.createTempFile("presto_test", "rc-binary"); + file.delete(); + try { + FileSplit split = createTestFile(file.getAbsolutePath(), outputFormat, serde, null, TEST_COLUMNS, NUM_ROWS); + testPageSourceFactory(new RcFilePageSourceFactory(TYPE_MANAGER), split, inputFormat, serde, TEST_COLUMNS); + } + finally { + //noinspection ResultOfMethodCallIgnored + file.delete(); + } + } + + @Test + public void testOrc() + throws Exception + { + HiveOutputFormat outputFormat = new org.apache.hadoop.hive.ql.io.orc.OrcOutputFormat(); + InputFormat inputFormat = new org.apache.hadoop.hive.ql.io.orc.OrcInputFormat(); + @SuppressWarnings("deprecation") + SerDe serde = new org.apache.hadoop.hive.ql.io.orc.OrcSerde(); + File file = File.createTempFile("presto_test", "orc"); + file.delete(); + try { + FileSplit split = createTestFile(file.getAbsolutePath(), outputFormat, serde, null, TEST_COLUMNS, NUM_ROWS); + testCursorProvider(new OrcRecordCursorProvider(), split, inputFormat, serde, TEST_COLUMNS, NUM_ROWS); + } + finally { + //noinspection ResultOfMethodCallIgnored + file.delete(); + } + } + + @Test + public void testOrcDataStream() + throws Exception + { + HiveOutputFormat outputFormat = new org.apache.hadoop.hive.ql.io.orc.OrcOutputFormat(); + InputFormat inputFormat = new org.apache.hadoop.hive.ql.io.orc.OrcInputFormat(); + @SuppressWarnings("deprecation") + SerDe serde = new org.apache.hadoop.hive.ql.io.orc.OrcSerde(); + File file = File.createTempFile("presto_test", "orc"); + file.delete(); + try { + FileSplit split = createTestFile(file.getAbsolutePath(), outputFormat, serde, null, TEST_COLUMNS, NUM_ROWS); + testPageSourceFactory(new OrcPageSourceFactory(TYPE_MANAGER), split, inputFormat, serde, TEST_COLUMNS); + } + finally { + //noinspection ResultOfMethodCallIgnored + file.delete(); + } + } + + @Test + public void testParquet() + throws Exception + { + List testColumns = ImmutableList.copyOf(filter(TEST_COLUMNS, new Predicate() + { + @Override + public boolean apply(TestColumn testColumn) + { + // Write of complex hive data to Parquet is broken + // TODO: empty arrays or maps with null keys don't seem to work + if (ImmutableSet.of("t_complex", "t_array_empty", "t_map_null_key").contains(testColumn.getName())) { + return false; + } + + // Parquet does not support DATE, TIMESTAMP, or BINARY + ObjectInspector objectInspector = testColumn.getObjectInspector(); + return !hasType(objectInspector, PrimitiveCategory.DATE, PrimitiveCategory.TIMESTAMP, PrimitiveCategory.BINARY); + } + })); + + HiveOutputFormat outputFormat = new MapredParquetOutputFormat(); + InputFormat inputFormat = new MapredParquetInputFormat(); + @SuppressWarnings("deprecation") + SerDe serde = new ParquetHiveSerDe(); + File file = File.createTempFile("presto_test", "parquet"); + file.delete(); + try { + FileSplit split = createTestFile(file.getAbsolutePath(), outputFormat, serde, null, testColumns, NUM_ROWS); + HiveRecordCursorProvider cursorProvider = new ParquetRecordCursorProvider(false); + testCursorProvider(cursorProvider, split, inputFormat, serde, testColumns, NUM_ROWS); + } + finally { + //noinspection ResultOfMethodCallIgnored + file.delete(); + } + } + + @Test + public void testParquetThrift() + throws Exception + { + RowType nameType = new RowType(ImmutableList.of(VARCHAR, VARCHAR), Optional.empty()); + RowType phoneType = new RowType(ImmutableList.of(VARCHAR, VARCHAR), Optional.empty()); + RowType personType = new RowType(ImmutableList.of(nameType, BIGINT, VARCHAR, new ArrayType(phoneType)), Optional.empty()); + + List testColumns = ImmutableList.of( + new TestColumn( + "persons", + getStandardListObjectInspector( + getStandardStructObjectInspector( + ImmutableList.of("name", "id", "email", "phones"), + ImmutableList.of( + getStandardStructObjectInspector( + ImmutableList.of("first_name", "last_name"), + ImmutableList.of(javaStringObjectInspector, javaStringObjectInspector) + ), + javaIntObjectInspector, + javaStringObjectInspector, + getStandardListObjectInspector( + getStandardStructObjectInspector( + ImmutableList.of("number", "type"), + ImmutableList.of(javaStringObjectInspector, javaStringObjectInspector) + ) + ) + ) + ) + ), + null, + arraySliceOf(personType, + rowSliceOf(ImmutableList.of(nameType, BIGINT, VARCHAR, new ArrayType(phoneType)), + rowSliceOf(ImmutableList.of(VARCHAR, VARCHAR), "Bob", "Roberts"), + 0, + "bob.roberts@example.com", + arraySliceOf(phoneType, rowSliceOf(ImmutableList.of(VARCHAR, VARCHAR), "1234567890", null)) + ) + ) + ) + ); + + InputFormat inputFormat = new MapredParquetInputFormat(); + @SuppressWarnings("deprecation") + SerDe serde = new ParquetHiveSerDe(); + File file = new File(this.getClass().getClassLoader().getResource("addressbook.parquet").getPath()); + FileSplit split = new FileSplit(new Path(file.getAbsolutePath()), 0, file.length(), new String[0]); + HiveRecordCursorProvider cursorProvider = new ParquetRecordCursorProvider(false); + testCursorProvider(cursorProvider, split, inputFormat, serde, testColumns, 1); + } + + @Test + public void testDwrf() + throws Exception + { + List testColumns = ImmutableList.copyOf(filter(TEST_COLUMNS, new Predicate() + { + @Override + public boolean apply(TestColumn testColumn) + { + ObjectInspector objectInspector = testColumn.getObjectInspector(); + return !hasType(objectInspector, PrimitiveCategory.DATE); + } + + })); + + HiveOutputFormat outputFormat = new com.facebook.hive.orc.OrcOutputFormat(); + InputFormat inputFormat = new com.facebook.hive.orc.OrcInputFormat(); + @SuppressWarnings("deprecation") + SerDe serde = new com.facebook.hive.orc.OrcSerde(); + File file = File.createTempFile("presto_test", "dwrf"); + file.delete(); + try { + FileSplit split = createTestFile(file.getAbsolutePath(), outputFormat, serde, null, testColumns, NUM_ROWS); + testCursorProvider(new DwrfRecordCursorProvider(), split, inputFormat, serde, testColumns, NUM_ROWS); + } + finally { + //noinspection ResultOfMethodCallIgnored + file.delete(); + } + } + + @Test + public void testDwrfDataStream() + throws Exception + { + List testColumns = ImmutableList.copyOf(filter(TEST_COLUMNS, new Predicate() + { + @Override + public boolean apply(TestColumn testColumn) + { + ObjectInspector objectInspector = testColumn.getObjectInspector(); + return !hasType(objectInspector, PrimitiveCategory.DATE); + } + + })); + + HiveOutputFormat outputFormat = new com.facebook.hive.orc.OrcOutputFormat(); + InputFormat inputFormat = new com.facebook.hive.orc.OrcInputFormat(); + @SuppressWarnings("deprecation") + SerDe serde = new com.facebook.hive.orc.OrcSerde(); + File file = File.createTempFile("presto_test", "dwrf"); + file.delete(); + try { + FileSplit split = createTestFile(file.getAbsolutePath(), outputFormat, serde, null, testColumns, NUM_ROWS); + testPageSourceFactory(new DwrfPageSourceFactory(TYPE_MANAGER), split, inputFormat, serde, testColumns); + } + finally { + //noinspection ResultOfMethodCallIgnored + file.delete(); + } + } + + private void testCursorProvider(HiveRecordCursorProvider cursorProvider, + FileSplit split, + InputFormat inputFormat, + @SuppressWarnings("deprecation") SerDe serde, + List testColumns, + int numRows) + throws IOException + { + Properties splitProperties = new Properties(); + splitProperties.setProperty(FILE_INPUT_FORMAT, inputFormat.getClass().getName()); + splitProperties.setProperty(SERIALIZATION_LIB, serde.getClass().getName()); + splitProperties.setProperty("columns", Joiner.on(',').join(transform(filter(testColumns, not(TestColumn::isPartitionKey)), TestColumn::getName))); + splitProperties.setProperty("columns.types", Joiner.on(',').join(transform(filter(testColumns, not(TestColumn::isPartitionKey)), TestColumn::getType))); + + List partitionKeys = testColumns.stream() + .filter(TestColumn::isPartitionKey) + .map(input -> new HivePartitionKey(input.getName(), HiveType.getHiveType(input.getObjectInspector()), (String) input.getWriteValue())) + .collect(toList()); + + HiveRecordCursor cursor = cursorProvider.createHiveRecordCursor( + "test", + new Configuration(), + SESSION, + split.getPath(), + split.getStart(), + split.getLength(), + splitProperties, + getColumnHandles(testColumns), + partitionKeys, + TupleDomain.all(), + DateTimeZone.getDefault(), + TYPE_MANAGER).get(); + + checkCursor(cursor, testColumns, numRows); + } + + private void testPageSourceFactory(HivePageSourceFactory sourceFactory, FileSplit split, InputFormat inputFormat, SerDe serde, List testColumns) + throws IOException + { + Properties splitProperties = new Properties(); + splitProperties.setProperty(FILE_INPUT_FORMAT, inputFormat.getClass().getName()); + splitProperties.setProperty(SERIALIZATION_LIB, serde.getClass().getName()); + splitProperties.setProperty("columns", Joiner.on(',').join(transform(filter(testColumns, not(TestColumn::isPartitionKey)), TestColumn::getName))); + splitProperties.setProperty("columns.types", Joiner.on(',').join(transform(filter(testColumns, not(TestColumn::isPartitionKey)), TestColumn::getType))); + + List partitionKeys = testColumns.stream() + .filter(TestColumn::isPartitionKey) + .map(input -> new HivePartitionKey(input.getName(), HiveType.getHiveType(input.getObjectInspector()), (String) input.getWriteValue())) + .collect(toList()); + + List columnHandles = getColumnHandles(testColumns); + + ConnectorPageSource pageSource = sourceFactory.createPageSource( + new Configuration(), + SESSION, + split.getPath(), + split.getStart(), + split.getLength(), + splitProperties, + columnHandles, + partitionKeys, + TupleDomain.all(), + DateTimeZone.getDefault() + ).get(); + + checkPageSource(pageSource, testColumns, getTypes(columnHandles)); + } + + public static boolean hasType(ObjectInspector objectInspector, PrimitiveCategory... types) + { + if (objectInspector instanceof PrimitiveObjectInspector) { + PrimitiveObjectInspector primitiveInspector = (PrimitiveObjectInspector) objectInspector; + PrimitiveCategory primitiveCategory = primitiveInspector.getPrimitiveCategory(); + for (PrimitiveCategory type : types) { + if (primitiveCategory == type) { + return true; + } + } + return false; + } + if (objectInspector instanceof ListObjectInspector) { + ListObjectInspector listInspector = (ListObjectInspector) objectInspector; + return hasType(listInspector.getListElementObjectInspector(), types); + } + if (objectInspector instanceof MapObjectInspector) { + MapObjectInspector mapInspector = (MapObjectInspector) objectInspector; + return hasType(mapInspector.getMapKeyObjectInspector(), types) || + hasType(mapInspector.getMapValueObjectInspector(), types); + } + if (objectInspector instanceof StructObjectInspector) { + for (StructField field : ((StructObjectInspector) objectInspector).getAllStructFieldRefs()) { + if (hasType(field.getFieldObjectInspector(), types)) { + return true; + } + } + return false; + } + throw new IllegalArgumentException("Unknown object inspector type " + objectInspector); + } +} diff --git a/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveIntegrationSmokeTest.java b/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveIntegrationSmokeTest.java new file mode 100644 index 00000000..a00b0eb7 --- /dev/null +++ b/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveIntegrationSmokeTest.java @@ -0,0 +1,181 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.testing.MaterializedResult; +import com.facebook.presto.testing.MaterializedRow; +import com.facebook.presto.tests.AbstractTestIntegrationSmokeTest; +import org.intellij.lang.annotations.Language; +import org.joda.time.DateTime; +import org.testng.annotations.Test; + +import java.sql.Date; +import java.sql.Timestamp; + +import static com.facebook.presto.hive.HiveQueryRunner.createQueryRunner; +import static com.facebook.presto.hive.HiveQueryRunner.createSampledSession; +import static io.airlift.tpch.TpchTable.ORDERS; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.joda.time.DateTimeZone.UTC; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotNull; + +public class TestHiveIntegrationSmokeTest + extends AbstractTestIntegrationSmokeTest +{ + public TestHiveIntegrationSmokeTest() + throws Exception + { + super(createQueryRunner(ORDERS), createSampledSession()); + } + + @Test + public void testInformationSchemaTablesWithoutEqualityConstraint() + throws Exception + { + @Language("SQL") String actual = "" + + "SELECT lower(table_name) " + + "FROM information_schema.tables " + + "WHERE table_catalog = 'hive' AND table_schema LIKE 'tpch' AND table_name LIKE '%orders'"; + + @Language("SQL") String expected = "" + + "SELECT lower(table_name) " + + "FROM information_schema.tables " + + "WHERE table_name LIKE '%ORDERS'"; + + assertQuery(actual, expected); + } + + @Test + public void testInformationSchemaColumnsWithoutEqualityConstraint() + throws Exception + { + @Language("SQL") String actual = "" + + "SELECT lower(table_name), lower(column_name) " + + "FROM information_schema.columns " + + "WHERE table_catalog = 'hive' AND table_schema = 'tpch' AND table_name LIKE '%orders%'"; + + @Language("SQL") String expected = "" + + "SELECT lower(table_name), lower(column_name) " + + "FROM information_schema.columns " + + "WHERE table_name LIKE '%ORDERS%'"; + + assertQuery(actual, expected); + } + + @Test + public void createTableWithEveryType() + throws Exception + { + String query = "" + + "CREATE TABLE test_types_table AS " + + "SELECT" + + " 'foo' _varchar" + + ", cast('bar' as varbinary) _varbinary" + + ", 1 _bigint" + + ", 3.14 _double" + + ", true _boolean" + + ", DATE '1980-05-07' _date" + + ", TIMESTAMP '1980-05-07 11:22:33.456' _timestamp"; + + assertQuery(query, "SELECT 1"); + + MaterializedResult results = queryRunner.execute(getSession(), "SELECT * FROM test_types_table").toJdbcTypes(); + assertEquals(results.getRowCount(), 1); + MaterializedRow row = results.getMaterializedRows().get(0); + assertEquals(row.getField(0), "foo"); + assertEquals(row.getField(1), "bar".getBytes(UTF_8)); + assertEquals(row.getField(2), 1L); + assertEquals(row.getField(3), 3.14); + assertEquals(row.getField(4), true); + assertEquals(row.getField(5), new Date(new DateTime(1980, 5, 7, 0, 0, 0, UTC).getMillis())); + assertEquals(row.getField(6), new Timestamp(new DateTime(1980, 5, 7, 11, 22, 33, 456, UTC).getMillis())); + assertQueryTrue("DROP TABLE test_types_table"); + + assertFalse(queryRunner.tableExists(getSession(), "test_types_table")); + } + + // TODO: These should be moved to another class, when more connectors support arrays + @Test + public void testArrays() + throws Exception + { + assertQuery("CREATE TABLE tmp_array1 AS SELECT ARRAY[1, 2, NULL] AS col", "SELECT 1"); + assertQuery("SELECT col[2] FROM tmp_array1", "SELECT 2"); + assertQuery("SELECT col[3] FROM tmp_array1", "SELECT NULL"); + + assertQuery("CREATE TABLE tmp_array2 AS SELECT ARRAY[1.0, 2.5, 3.5] AS col", "SELECT 1"); + assertQuery("SELECT col[2] FROM tmp_array2", "SELECT 2.5"); + + assertQuery("CREATE TABLE tmp_array3 AS SELECT ARRAY['puppies', 'kittens', NULL] AS col", "SELECT 1"); + assertQuery("SELECT col[2] FROM tmp_array3", "SELECT 'kittens'"); + assertQuery("SELECT col[3] FROM tmp_array3", "SELECT NULL"); + + assertQuery("CREATE TABLE tmp_array4 AS SELECT ARRAY[TRUE, NULL] AS col", "SELECT 1"); + assertQuery("SELECT col[1] FROM tmp_array4", "SELECT TRUE"); + assertQuery("SELECT col[2] FROM tmp_array4", "SELECT NULL"); + + assertQuery("CREATE TABLE tmp_array5 AS SELECT ARRAY[ARRAY[1, 2], NULL, ARRAY[3, 4]] AS col", "SELECT 1"); + assertQuery("SELECT col[1][2] FROM tmp_array5", "SELECT 2"); + + assertQuery("CREATE TABLE tmp_array6 AS SELECT ARRAY[ARRAY['\"hi\"'], NULL, ARRAY['puppies']] AS col", "SELECT 1"); + assertQuery("SELECT col[1][1] FROM tmp_array6", "SELECT '\"hi\"'"); + assertQuery("SELECT col[3][1] FROM tmp_array6", "SELECT 'puppies'"); + } + + @Test + public void testTemporalArrays() + throws Exception + { + assertQuery("CREATE TABLE tmp_array7 AS SELECT ARRAY[DATE '2014-09-30'] AS col", "SELECT 1"); + assertOneNotNullResult("SELECT col[1] FROM tmp_array7"); + assertQuery("CREATE TABLE tmp_array8 AS SELECT ARRAY[TIMESTAMP '2001-08-22 03:04:05.321'] AS col", "SELECT 1"); + assertOneNotNullResult("SELECT col[1] FROM tmp_array8"); + } + + @Test + public void testMaps() + throws Exception + { + assertQuery("CREATE TABLE tmp_map1 AS SELECT MAP(ARRAY[0,1], ARRAY[2,NULL]) AS col", "SELECT 1"); + assertQuery("SELECT col[0] FROM tmp_map1", "SELECT 2"); + assertQuery("SELECT col[1] FROM tmp_map1", "SELECT NULL"); + + assertQuery("CREATE TABLE tmp_map2 AS SELECT MAP(ARRAY[1.0], ARRAY[2.5]) AS col", "SELECT 1"); + assertQuery("SELECT col[1.0] FROM tmp_map2", "SELECT 2.5"); + + assertQuery("CREATE TABLE tmp_map3 AS SELECT MAP(ARRAY['puppies'], ARRAY['kittens']) AS col", "SELECT 1"); + assertQuery("SELECT col['puppies'] FROM tmp_map3", "SELECT 'kittens'"); + + assertQuery("CREATE TABLE tmp_map4 AS SELECT MAP(ARRAY[TRUE], ARRAY[FALSE]) AS col", "SELECT 1"); + assertQuery("SELECT col[TRUE] FROM tmp_map4", "SELECT FALSE"); + + assertQuery("CREATE TABLE tmp_map5 AS SELECT MAP(ARRAY[1.0], ARRAY[ARRAY[1, 2]]) AS col", "SELECT 1"); + assertQuery("SELECT col[1.0][2] FROM tmp_map5", "SELECT 2"); + + assertQuery("CREATE TABLE tmp_map6 AS SELECT MAP(ARRAY[DATE '2014-09-30'], ARRAY[DATE '2014-09-29']) AS col", "SELECT 1"); + assertOneNotNullResult("SELECT col[DATE '2014-09-30'] FROM tmp_map6"); + assertQuery("CREATE TABLE tmp_map7 AS SELECT MAP(ARRAY[TIMESTAMP '2001-08-22 03:04:05.321'], ARRAY[TIMESTAMP '2001-08-22 03:04:05.321']) AS col", "SELECT 1"); + assertOneNotNullResult("SELECT col[TIMESTAMP '2001-08-22 03:04:05.321'] FROM tmp_map7"); + } + + private void assertOneNotNullResult(@Language("SQL") String query) + { + MaterializedResult results = queryRunner.execute(getSession(), query).toJdbcTypes(); + assertEquals(results.getRowCount(), 1); + assertEquals(results.getMaterializedRows().get(0).getFieldCount(), 1); + assertNotNull(results.getMaterializedRows().get(0).getField(0)); + } +} diff --git a/presto-hive/src/test/java/com/facebook/presto/hive/TestHivePluginConfig.java b/presto-hive/src/test/java/com/facebook/presto/hive/TestHivePluginConfig.java new file mode 100644 index 00000000..efc5bf6b --- /dev/null +++ b/presto-hive/src/test/java/com/facebook/presto/hive/TestHivePluginConfig.java @@ -0,0 +1,47 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.google.common.collect.ImmutableMap; +import org.testng.annotations.Test; + +import java.net.URI; +import java.util.Map; + +import static io.airlift.configuration.testing.ConfigAssertions.assertFullMapping; +import static io.airlift.configuration.testing.ConfigAssertions.assertRecordedDefaults; +import static io.airlift.configuration.testing.ConfigAssertions.recordDefaults; + +public class TestHivePluginConfig +{ + @Test + public void testDefaults() + { + assertRecordedDefaults(recordDefaults(HivePluginConfig.class) + .setMetastoreUri(null)); + } + + @Test + public void testExplicitPropertyMappings() + { + Map properties = new ImmutableMap.Builder() + .put("hive.metastore.uri", "thrift://localhost:9083") + .build(); + + HivePluginConfig expected = new HivePluginConfig() + .setMetastoreUri(URI.create("thrift://localhost:9083")); + + assertFullMapping(properties, expected); + } +} diff --git a/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveSplit.java b/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveSplit.java new file mode 100644 index 00000000..99834b6a --- /dev/null +++ b/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveSplit.java @@ -0,0 +1,77 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.HostAddress; +import com.facebook.presto.spi.TupleDomain; +import com.google.common.collect.ImmutableList; +import io.airlift.json.JsonCodec; +import org.testng.annotations.Test; + +import java.util.Properties; + +import static com.facebook.presto.spi.type.TimeZoneKey.UTC_KEY; +import static java.util.Locale.ENGLISH; +import static org.testng.Assert.assertEquals; + +public class TestHiveSplit +{ + private static final ConnectorSession SESSION = new ConnectorSession("user", UTC_KEY, ENGLISH, System.currentTimeMillis(), null); + private final JsonCodec codec = JsonCodec.jsonCodec(HiveSplit.class); + + @Test + public void testJsonRoundTrip() + { + Properties schema = new Properties(); + schema.setProperty("foo", "bar"); + schema.setProperty("bar", "baz"); + + ImmutableList partitionKeys = ImmutableList.of(new HivePartitionKey("a", HiveType.HIVE_STRING, "apple"), new HivePartitionKey("b", HiveType.HIVE_LONG, "42")); + ImmutableList addresses = ImmutableList.of(HostAddress.fromParts("127.0.0.1", 44), HostAddress.fromParts("127.0.0.1", 45)); + HiveSplit expected = new HiveSplit( + "clientId", + "db", + "table", + "partitionId", + "path", + 42, + 88, + schema, + partitionKeys, + addresses, + true, + SESSION, + TupleDomain.all()); + + String json = codec.toJson(expected); + HiveSplit actual = codec.fromJson(json); + + assertEquals(actual.getClientId(), expected.getClientId()); + assertEquals(actual.getDatabase(), expected.getDatabase()); + assertEquals(actual.getTable(), expected.getTable()); + assertEquals(actual.getPartitionName(), expected.getPartitionName()); + assertEquals(actual.getPath(), expected.getPath()); + assertEquals(actual.getStart(), expected.getStart()); + assertEquals(actual.getLength(), expected.getLength()); + assertEquals(actual.getSchema(), expected.getSchema()); + assertEquals(actual.getPartitionKeys(), expected.getPartitionKeys()); + assertEquals(actual.getAddresses(), expected.getAddresses()); + assertEquals(actual.getSession().getUser(), expected.getSession().getUser()); + assertEquals(actual.getSession().getLocale(), expected.getSession().getLocale()); + assertEquals(actual.getSession().getTimeZoneKey(), expected.getSession().getTimeZoneKey()); + assertEquals(actual.getSession().getStartTime(), expected.getSession().getStartTime()); + assertEquals(actual.isForceLocalScheduling(), expected.isForceLocalScheduling()); + } +} diff --git a/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveSplitSource.java b/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveSplitSource.java new file mode 100644 index 00000000..6f4d1b53 --- /dev/null +++ b/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveSplitSource.java @@ -0,0 +1,236 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.HostAddress; +import com.google.common.util.concurrent.SettableFuture; +import org.testng.annotations.Test; + +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static io.airlift.concurrent.MoreFutures.getFutureValue; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertSame; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.fail; + +public class TestHiveSplitSource +{ + @Test + public void testOutstandingSplitCount() + throws Exception + { + HiveSplitSource hiveSplitSource = new HiveSplitSource("test", 10, new TestingHiveSplitLoader()); + + // add 10 splits + for (int i = 0; i < 10; i++) { + hiveSplitSource.addToQueue(new TestSplit(i)); + assertEquals(hiveSplitSource.getOutstandingSplitCount(), i + 1); + } + + // remove 1 split + assertEquals(getFutureValue(hiveSplitSource.getNextBatch(1)).size(), 1); + assertEquals(hiveSplitSource.getOutstandingSplitCount(), 9); + + // remove 4 splits + assertEquals(getFutureValue(hiveSplitSource.getNextBatch(4)).size(), 4); + assertEquals(hiveSplitSource.getOutstandingSplitCount(), 5); + + // try to remove 20 splits, and verify we only got 5 + assertEquals(getFutureValue(hiveSplitSource.getNextBatch(20)).size(), 5); + assertEquals(hiveSplitSource.getOutstandingSplitCount(), 0); + } + + @Test + public void testSuspendResume() + throws Exception + { + TestingHiveSplitLoader splitLoader = new TestingHiveSplitLoader(); + HiveSplitSource hiveSplitSource = new HiveSplitSource("test", 10, splitLoader); + + // almost fill the source + for (int i = 0; i < 9; i++) { + hiveSplitSource.addToQueue(new TestSplit(i)); + assertEquals(hiveSplitSource.getOutstandingSplitCount(), i + 1); + assertFalse(splitLoader.isResumed()); + } + + // add one more split so the source is now full + hiveSplitSource.addToQueue(new TestSplit(10)); + assertEquals(hiveSplitSource.getOutstandingSplitCount(), 10); + assertFalse(splitLoader.isResumed()); + + // remove one split so the source is no longer full and verify the loader is resumed + assertEquals(getFutureValue(hiveSplitSource.getNextBatch(1)).size(), 1); + assertEquals(hiveSplitSource.getOutstandingSplitCount(), 9); + assertTrue(splitLoader.isResumed()); + } + + @Test + public void testFail() + throws Exception + { + HiveSplitSource hiveSplitSource = new HiveSplitSource("test", 10, new TestingHiveSplitLoader()); + + // add some splits + for (int i = 0; i < 5; i++) { + hiveSplitSource.addToQueue(new TestSplit(i)); + assertEquals(hiveSplitSource.getOutstandingSplitCount(), i + 1); + } + + // remove a split and verify + assertEquals(getFutureValue(hiveSplitSource.getNextBatch(1)).size(), 1); + assertEquals(hiveSplitSource.getOutstandingSplitCount(), 4); + + // fail source + hiveSplitSource.fail(new RuntimeException("test")); + assertEquals(hiveSplitSource.getOutstandingSplitCount(), 4); + + // try to remove a split and verify we got the expected exception + try { + getFutureValue(hiveSplitSource.getNextBatch(1)); + fail("expected RuntimeException"); + } + catch (RuntimeException e) { + assertEquals(e.getMessage(), "test"); + } + assertEquals(hiveSplitSource.getOutstandingSplitCount(), 4); + + // attempt to add another split and verify it does not work + hiveSplitSource.addToQueue(new TestSplit(99)); + assertEquals(hiveSplitSource.getOutstandingSplitCount(), 4); + + // fail source again + hiveSplitSource.fail(new RuntimeException("another failure")); + assertEquals(hiveSplitSource.getOutstandingSplitCount(), 4); + + // try to remove a split and verify we got the first exception + try { + getFutureValue(hiveSplitSource.getNextBatch(1)); + fail("expected RuntimeException"); + } + catch (RuntimeException e) { + assertEquals(e.getMessage(), "test"); + } + } + + @Test + public void testReaderWaitsForSplits() + throws Exception + { + final HiveSplitSource hiveSplitSource = new HiveSplitSource("test", 10, new TestingHiveSplitLoader()); + + final SettableFuture splits = SettableFuture.create(); + + // create a thread that will get a split + final CountDownLatch started = new CountDownLatch(1); + Thread getterThread = new Thread(new Runnable() + { + @Override + public void run() + { + try { + started.countDown(); + List batch = getFutureValue(hiveSplitSource.getNextBatch(1)); + assertEquals(batch.size(), 1); + splits.set(batch.get(0)); + } + catch (Throwable e) { + splits.setException(e); + } + } + }); + getterThread.start(); + + try { + // wait for the thread to be started + assertTrue(started.await(1, TimeUnit.SECONDS)); + + // sleep for a bit, and assure the thread is blocked + TimeUnit.MILLISECONDS.sleep(200); + assertTrue(!splits.isDone()); + + // add a split + hiveSplitSource.addToQueue(new TestSplit(33)); + + // wait for thread to get the split + ConnectorSplit split = splits.get(200, TimeUnit.MILLISECONDS); + assertSame(split.getInfo(), 33); + } + finally { + // make sure the thread exits + getterThread.interrupt(); + } + } + + private static class TestingHiveSplitLoader + implements HiveSplitLoader + { + private boolean resumed; + + @Override + public void start(HiveSplitSource splitSource) + { + } + + @Override + public void resume() + { + resumed = true; + } + + @Override + public void stop() + { + } + + public boolean isResumed() + { + return resumed; + } + } + + private static class TestSplit + implements ConnectorSplit + { + private final int id; + + private TestSplit(int id) + { + this.id = id; + } + + @Override + public boolean isRemotelyAccessible() + { + throw new UnsupportedOperationException(); + } + + @Override + public List getAddresses() + { + throw new UnsupportedOperationException(); + } + + @Override + public Object getInfo() + { + return id; + } + } +} diff --git a/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveTableHandle.java b/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveTableHandle.java new file mode 100644 index 00000000..a3aa6273 --- /dev/null +++ b/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveTableHandle.java @@ -0,0 +1,40 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.spi.ConnectorSession; +import io.airlift.json.JsonCodec; +import org.testng.annotations.Test; + +import static com.facebook.presto.spi.type.TimeZoneKey.UTC_KEY; +import static java.util.Locale.ENGLISH; +import static org.testng.Assert.assertEquals; + +public class TestHiveTableHandle +{ + private static final ConnectorSession SESSION = new ConnectorSession("user", UTC_KEY, ENGLISH, System.currentTimeMillis(), null); + private final JsonCodec codec = JsonCodec.jsonCodec(HiveTableHandle.class); + + @Test + public void testRoundTrip() + { + HiveTableHandle expected = new HiveTableHandle("client", "schema", "table", SESSION); + + String json = codec.toJson(expected); + HiveTableHandle actual = codec.fromJson(json); + + assertEquals(actual.getClientId(), expected.getClientId()); + assertEquals(actual.getSchemaTableName(), expected.getSchemaTableName()); + } +} diff --git a/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveUtil.java b/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveUtil.java new file mode 100644 index 00000000..0d001798 --- /dev/null +++ b/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveUtil.java @@ -0,0 +1,61 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.joda.time.format.DateTimeFormat; +import org.testng.annotations.Test; + +import static com.facebook.presto.hive.HiveUtil.parseHiveTimestamp; +import static org.testng.Assert.assertEquals; + +public class TestHiveUtil +{ + @Test + public void testParseHiveTimestamp() + { + DateTime time = new DateTime(2011, 5, 6, 7, 8, 9, 123, nonDefaultTimeZone()); + assertEquals(parse(time, "yyyy-MM-dd HH:mm:ss"), unixTime(time, 0)); + assertEquals(parse(time, "yyyy-MM-dd HH:mm:ss.S"), unixTime(time, 1)); + assertEquals(parse(time, "yyyy-MM-dd HH:mm:ss.SSS"), unixTime(time, 3)); + assertEquals(parse(time, "yyyy-MM-dd HH:mm:ss.SSSSSSS"), unixTime(time, 6)); + assertEquals(parse(time, "yyyy-MM-dd HH:mm:ss.SSSSSSSSS"), unixTime(time, 7)); + } + + private static long parse(DateTime time, String pattern) + { + return parseHiveTimestamp(DateTimeFormat.forPattern(pattern).print(time), nonDefaultTimeZone()); + } + + private static long unixTime(DateTime time, int factionalDigits) + { + int factor = (int) Math.pow(10, Math.max(0, 3 - factionalDigits)); + return (time.getMillis() / factor) * factor; + } + + static DateTimeZone nonDefaultTimeZone() + { + String defaultId = DateTimeZone.getDefault().getID(); + for (String id : DateTimeZone.getAvailableIDs()) { + if (!id.equals(defaultId)) { + DateTimeZone zone = DateTimeZone.forID(id); + if (zone.getStandardOffset(0) != 0) { + return zone; + } + } + } + throw new IllegalStateException("no non-default timezone"); + } +} diff --git a/presto-hive/src/test/java/com/facebook/presto/hive/TestJsonHiveHandles.java b/presto-hive/src/test/java/com/facebook/presto/hive/TestJsonHiveHandles.java new file mode 100644 index 00000000..dbd9e83e --- /dev/null +++ b/presto-hive/src/test/java/com/facebook/presto/hive/TestJsonHiveHandles.java @@ -0,0 +1,120 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.spi.type.StandardTypes; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableMap; +import io.airlift.json.ObjectMapperProvider; +import org.testng.annotations.Test; + +import java.util.Map; + +import static com.facebook.presto.spi.type.TimeZoneKey.UTC_KEY; +import static com.facebook.presto.spi.type.TypeSignature.parseTypeSignature; +import static io.airlift.testing.Assertions.assertEqualsIgnoreOrder; +import static java.util.Locale.ENGLISH; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +@Test +public class TestJsonHiveHandles +{ + private static final ConnectorSession SESSION = new ConnectorSession("user", UTC_KEY, ENGLISH, System.currentTimeMillis(), null); + + private static final Map TABLE_HANDLE_AS_MAP = ImmutableMap.of( + "clientId", "hive", + "schemaName", "hive_schema", + "tableName", "hive_table", + "session", ImmutableMap.builder() + .put("user", SESSION.getUser()) + .put("timeZoneKey", (int) SESSION.getTimeZoneKey().getKey()) + .put("locale", SESSION.getLocale().toString()) + .put("startTime", SESSION.getStartTime()) + .put("properties", ImmutableMap.of()) + .build()); + + private static final Map COLUMN_HANDLE_AS_MAP = ImmutableMap.builder() + .put("clientId", "hive") + .put("name", "column") + .put("ordinalPosition", 42) + .put("hiveType", "float") + .put("typeSignature", "double") + .put("hiveColumnIndex", -1) + .put("partitionKey", true) + .build(); + + private final ObjectMapper objectMapper = new ObjectMapperProvider().get(); + + @Test + public void testTableHandleSerialize() + throws Exception + { + HiveTableHandle tableHandle = new HiveTableHandle("hive", "hive_schema", "hive_table", SESSION); + + assertTrue(objectMapper.canSerialize(HiveTableHandle.class)); + String json = objectMapper.writeValueAsString(tableHandle); + testJsonEquals(json, TABLE_HANDLE_AS_MAP); + } + + @Test + public void testTableHandleDeserialize() + throws Exception + { + String json = objectMapper.writeValueAsString(TABLE_HANDLE_AS_MAP); + + HiveTableHandle tableHandle = objectMapper.readValue(json, HiveTableHandle.class); + + assertEquals(tableHandle.getClientId(), "hive"); + assertEquals(tableHandle.getSchemaName(), "hive_schema"); + assertEquals(tableHandle.getTableName(), "hive_table"); + assertEquals(tableHandle.getSchemaTableName(), new SchemaTableName("hive_schema", "hive_table")); + } + + @Test + public void testColumnHandleSerialize() + throws Exception + { + HiveColumnHandle columnHandle = new HiveColumnHandle("hive", "column", 42, HiveType.HIVE_FLOAT, parseTypeSignature(StandardTypes.DOUBLE), -1, true); + + assertTrue(objectMapper.canSerialize(HiveColumnHandle.class)); + String json = objectMapper.writeValueAsString(columnHandle); + testJsonEquals(json, COLUMN_HANDLE_AS_MAP); + } + + @Test + public void testColumnHandleDeserialize() + throws Exception + { + String json = objectMapper.writeValueAsString(COLUMN_HANDLE_AS_MAP); + + HiveColumnHandle columnHandle = objectMapper.readValue(json, HiveColumnHandle.class); + + assertEquals(columnHandle.getName(), "column"); + assertEquals(columnHandle.getOrdinalPosition(), 42); + assertEquals(columnHandle.getHiveType(), HiveType.HIVE_FLOAT); + assertEquals(columnHandle.getHiveColumnIndex(), -1); + assertEquals(columnHandle.isPartitionKey(), true); + } + + private void testJsonEquals(String json, Map expectedMap) + throws Exception + { + Map jsonMap = objectMapper.readValue(json, new TypeReference>() {}); + assertEqualsIgnoreOrder(jsonMap.entrySet(), expectedMap.entrySet()); + } +} diff --git a/presto-hive/src/test/java/com/facebook/presto/hive/TestNumberParser.java b/presto-hive/src/test/java/com/facebook/presto/hive/TestNumberParser.java new file mode 100644 index 00000000..14023b01 --- /dev/null +++ b/presto-hive/src/test/java/com/facebook/presto/hive/TestNumberParser.java @@ -0,0 +1,109 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import org.testng.annotations.Test; + +import static com.facebook.presto.hive.NumberParser.parseDouble; +import static com.facebook.presto.hive.NumberParser.parseLong; +import static java.nio.charset.StandardCharsets.US_ASCII; +import static org.testng.Assert.assertEquals; + +public class TestNumberParser +{ + @Test + public void testLong() + throws Exception + { + assertParseLong("1"); + assertParseLong("12"); + assertParseLong("123"); + assertParseLong("-1"); + assertParseLong("-12"); + assertParseLong("-123"); + assertParseLong("+1"); + assertParseLong("+12"); + assertParseLong("+123"); + assertParseLong("0"); + assertParseLong("-0"); + assertParseLong("+0"); + assertParseLong(Long.toString(Long.MAX_VALUE)); + assertParseLong(Long.toString(Long.MIN_VALUE)); + } + + @Test + public void testDouble() + throws Exception + { + assertParseDouble("123"); + assertParseDouble("123.0"); + assertParseDouble("123.456"); + assertParseDouble("123.456e5"); + assertParseDouble("123.456e-5"); + assertParseDouble("123e5"); + assertParseDouble("123e-5"); + assertParseDouble("0"); + assertParseDouble("0.0"); + assertParseDouble("0.456"); + assertParseDouble("-0"); + assertParseDouble("-0.0"); + assertParseDouble("-0.456"); + assertParseDouble("-123"); + assertParseDouble("-123.0"); + assertParseDouble("-123.456"); + assertParseDouble("-123.456e-5"); + assertParseDouble("-123e5"); + assertParseDouble("-123e-5"); + assertParseDouble("+123"); + assertParseDouble("+123.0"); + assertParseDouble("+123.456"); + assertParseDouble("+123.456e5"); + assertParseDouble("+123.456e-5"); + assertParseDouble("+123e5"); + assertParseDouble("+123e-5"); + assertParseDouble("+0"); + assertParseDouble("+0.0"); + assertParseDouble("+0.456"); + + assertParseDouble("NaN"); + assertParseDouble("-Infinity"); + assertParseDouble("Infinity"); + assertParseDouble("+Infinity"); + + assertParseDouble(Double.toString(Double.MAX_VALUE)); + assertParseDouble(Double.toString(-Double.MAX_VALUE)); + assertParseDouble(Double.toString(Double.MIN_VALUE)); + assertParseDouble(Double.toString(-Double.MIN_VALUE)); + } + + private static void assertParseLong(String string) + { + assertEquals(parseLong(string.getBytes(US_ASCII), 0, string.length()), Long.parseLong(string)); + + // verify we can parse using a non-zero offset + String padding = "9999"; + String padded = padding + string + padding; + assertEquals(parseLong(padded.getBytes(US_ASCII), padding.length(), string.length()), Long.parseLong(string)); + } + + private static void assertParseDouble(String string) + { + assertEquals(parseDouble(string.getBytes(US_ASCII), 0, string.length()), Double.parseDouble(string)); + + // verify we can parse using a non-zero offset + String padding = "9999"; + String padded = padding + string + padding; + assertEquals(parseDouble(padded.getBytes(US_ASCII), padding.length(), string.length()), Double.parseDouble(string)); + } +} diff --git a/presto-hive/src/test/java/com/facebook/presto/hive/TestPrestoS3FileSystem.java b/presto-hive/src/test/java/com/facebook/presto/hive/TestPrestoS3FileSystem.java new file mode 100644 index 00000000..efc09faf --- /dev/null +++ b/presto-hive/src/test/java/com/facebook/presto/hive/TestPrestoS3FileSystem.java @@ -0,0 +1,223 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.amazonaws.auth.AWSCredentialsProvider; +import com.amazonaws.auth.InstanceProfileCredentialsProvider; +import com.amazonaws.internal.StaticCredentialsProvider; +import com.amazonaws.services.s3.model.AmazonS3Exception; +import com.google.common.base.Throwables; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.FSDataInputStream; +import org.apache.hadoop.fs.Path; +import org.testng.annotations.Test; + +import java.lang.reflect.Field; +import java.net.URI; + +import static com.facebook.presto.hive.PrestoS3FileSystem.S3_MAX_BACKOFF_TIME; +import static com.facebook.presto.hive.PrestoS3FileSystem.S3_MAX_CLIENT_RETRIES; +import static com.facebook.presto.hive.PrestoS3FileSystem.S3_MAX_RETRY_TIME; +import static com.google.common.base.Preconditions.checkArgument; +import static io.airlift.testing.Assertions.assertInstanceOf; +import static org.apache.http.HttpStatus.SC_FORBIDDEN; +import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR; +import static org.apache.http.HttpStatus.SC_NOT_FOUND; +import static org.apache.http.HttpStatus.SC_REQUESTED_RANGE_NOT_SATISFIABLE; +import static org.testng.Assert.assertEquals; + +public class TestPrestoS3FileSystem +{ + @Test + public void testStaticCredentials() + throws Exception + { + Configuration config = new Configuration(); + config.set("fs.s3n.awsSecretAccessKey", "test_secret_access_key"); + config.set("fs.s3n.awsAccessKeyId", "test_access_key_id"); + // the static credentials should be preferred + + try (PrestoS3FileSystem fs = new PrestoS3FileSystem()) { + fs.initialize(new URI("s3n://test-bucket/"), config); + assertInstanceOf(getAwsCredentialsProvider(fs), StaticCredentialsProvider.class); + } + } + + @Test + public void testInstanceCredentialsEnabled() + throws Exception + { + Configuration config = new Configuration(); + // instance credentials are enabled by default + + try (PrestoS3FileSystem fs = new PrestoS3FileSystem()) { + fs.initialize(new URI("s3n://test-bucket/"), config); + assertInstanceOf(getAwsCredentialsProvider(fs), InstanceProfileCredentialsProvider.class); + } + } + + @Test(expectedExceptions = RuntimeException.class, expectedExceptionsMessageRegExp = "S3 credentials not configured") + public void testInstanceCredentialsDisabled() + throws Exception + { + Configuration config = new Configuration(); + config.setBoolean(PrestoS3FileSystem.S3_USE_INSTANCE_CREDENTIALS, false); + + try (PrestoS3FileSystem fs = new PrestoS3FileSystem()) { + fs.initialize(new URI("s3n://test-bucket/"), config); + } + } + + @SuppressWarnings({"ResultOfMethodCallIgnored", "OverlyStrongTypeCast", "ConstantConditions"}) + @Test + public void testReadRetryCounters() + throws Exception + { + try (PrestoS3FileSystem fs = new PrestoS3FileSystem()) { + int maxRetries = 2; + MockAmazonS3 s3 = new MockAmazonS3(); + s3.setGetObjectHttpErrorCode(SC_INTERNAL_SERVER_ERROR); + Configuration configuration = new Configuration(); + configuration.set(S3_MAX_BACKOFF_TIME, "1ms"); + configuration.set(S3_MAX_RETRY_TIME, "5s"); + configuration.setInt(S3_MAX_CLIENT_RETRIES, maxRetries); + fs.initialize(new URI("s3n://test-bucket/"), configuration); + fs.setS3Client(s3); + try (FSDataInputStream inputStream = fs.open(new Path("s3n://test-bucket/test"))) { + inputStream.read(); + } + catch (Throwable expected) { + assertInstanceOf(expected, AmazonS3Exception.class); + assertEquals(((AmazonS3Exception) expected).getStatusCode(), SC_INTERNAL_SERVER_ERROR); + assertEquals(PrestoS3FileSystem.getFileSystemStats().getReadRetries().getTotalCount(), maxRetries); + assertEquals(PrestoS3FileSystem.getFileSystemStats().getGetObjectRetries().getTotalCount(), (maxRetries + 1L) * maxRetries); + } + } + } + + @SuppressWarnings({"OverlyStrongTypeCast", "ConstantConditions"}) + @Test + public void testGetMetadataRetryCounter() + { + int maxRetries = 2; + try (PrestoS3FileSystem fs = new PrestoS3FileSystem()) { + MockAmazonS3 s3 = new MockAmazonS3(); + s3.setGetObjectMetadataHttpCode(SC_INTERNAL_SERVER_ERROR); + Configuration configuration = new Configuration(); + configuration.set(S3_MAX_BACKOFF_TIME, "1ms"); + configuration.set(S3_MAX_RETRY_TIME, "5s"); + configuration.setInt(S3_MAX_CLIENT_RETRIES, maxRetries); + fs.initialize(new URI("s3n://test-bucket/"), configuration); + fs.setS3Client(s3); + fs.getS3ObjectMetadata(new Path("s3n://test-bucket/test")); + } + catch (Throwable expected) { + assertInstanceOf(expected, AmazonS3Exception.class); + assertEquals(((AmazonS3Exception) expected).getStatusCode(), SC_INTERNAL_SERVER_ERROR); + assertEquals(PrestoS3FileSystem.getFileSystemStats().getGetMetadataRetries().getTotalCount(), maxRetries); + } + } + + @SuppressWarnings("ResultOfMethodCallIgnored") + @Test(expectedExceptions = RuntimeException.class, expectedExceptionsMessageRegExp = ".*Failing getObject call with " + SC_NOT_FOUND + ".*") + public void testReadNotFound() + throws Exception + { + try (PrestoS3FileSystem fs = new PrestoS3FileSystem()) { + MockAmazonS3 s3 = new MockAmazonS3(); + s3.setGetObjectHttpErrorCode(SC_NOT_FOUND); + fs.initialize(new URI("s3n://test-bucket/"), new Configuration()); + fs.setS3Client(s3); + try (FSDataInputStream inputStream = fs.open(new Path("s3n://test-bucket/test"))) { + inputStream.read(); + } + } + } + + @SuppressWarnings("ResultOfMethodCallIgnored") + @Test(expectedExceptions = RuntimeException.class, expectedExceptionsMessageRegExp = ".*Failing getObject call with " + SC_FORBIDDEN + ".*") + public void testReadForbidden() + throws Exception + { + try (PrestoS3FileSystem fs = new PrestoS3FileSystem()) { + MockAmazonS3 s3 = new MockAmazonS3(); + s3.setGetObjectHttpErrorCode(SC_FORBIDDEN); + fs.initialize(new URI("s3n://test-bucket/"), new Configuration()); + fs.setS3Client(s3); + try (FSDataInputStream inputStream = fs.open(new Path("s3n://test-bucket/test"))) { + inputStream.read(); + } + } + } + + @Test + public void testReadRequestRangeNotSatisfiable() + throws Exception + { + try (PrestoS3FileSystem fs = new PrestoS3FileSystem()) { + MockAmazonS3 s3 = new MockAmazonS3(); + s3.setGetObjectHttpErrorCode(SC_REQUESTED_RANGE_NOT_SATISFIABLE); + fs.initialize(new URI("s3n://test-bucket/"), new Configuration()); + fs.setS3Client(s3); + try (FSDataInputStream inputStream = fs.open(new Path("s3n://test-bucket/test"))) { + assertEquals(inputStream.read(), -1); + } + } + } + + @Test(expectedExceptions = RuntimeException.class, expectedExceptionsMessageRegExp = ".*Failing getObjectMetadata call with " + SC_FORBIDDEN + ".*") + public void testGetMetadataForbidden() + throws Exception + { + try (PrestoS3FileSystem fs = new PrestoS3FileSystem()) { + MockAmazonS3 s3 = new MockAmazonS3(); + s3.setGetObjectMetadataHttpCode(SC_FORBIDDEN); + fs.initialize(new URI("s3n://test-bucket/"), new Configuration()); + fs.setS3Client(s3); + fs.getS3ObjectMetadata(new Path("s3n://test-bucket/test")); + } + } + + @Test + public void testGetMetadataNotFound() + throws Exception + { + try (PrestoS3FileSystem fs = new PrestoS3FileSystem()) { + MockAmazonS3 s3 = new MockAmazonS3(); + s3.setGetObjectMetadataHttpCode(SC_NOT_FOUND); + fs.initialize(new URI("s3n://test-bucket/"), new Configuration()); + fs.setS3Client(s3); + assertEquals(fs.getS3ObjectMetadata(new Path("s3n://test-bucket/test")), null); + } + } + + private static AWSCredentialsProvider getAwsCredentialsProvider(PrestoS3FileSystem fs) + { + return getFieldValue(fs.getS3Client(), "awsCredentialsProvider", AWSCredentialsProvider.class); + } + + @SuppressWarnings("unchecked") + private static T getFieldValue(Object instance, String name, Class type) + { + try { + Field field = instance.getClass().getDeclaredField(name); + checkArgument(field.getType() == type, "expected %s but found %s", type, field.getType()); + field.setAccessible(true); + return (T) field.get(instance); + } + catch (ReflectiveOperationException e) { + throw Throwables.propagate(e); + } + } +} diff --git a/presto-hive/src/test/java/com/facebook/presto/hive/TestingHiveCluster.java b/presto-hive/src/test/java/com/facebook/presto/hive/TestingHiveCluster.java new file mode 100644 index 00000000..652f8c8d --- /dev/null +++ b/presto-hive/src/test/java/com/facebook/presto/hive/TestingHiveCluster.java @@ -0,0 +1,68 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive; + +import com.google.common.base.Throwables; +import org.apache.thrift.transport.TTransportException; + +import java.util.Objects; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class TestingHiveCluster + implements HiveCluster +{ + private final HiveClientConfig config; + private final String host; + private final int port; + + public TestingHiveCluster(HiveClientConfig config, String host, int port) + { + this.config = checkNotNull(config, "config is null"); + this.host = checkNotNull(host, "host is null"); + this.port = port; + } + + @Override + public HiveMetastoreClient createMetastoreClient() + { + try { + return new HiveMetastoreClientFactory(config).create(host, port); + } + catch (TTransportException e) { + throw Throwables.propagate(e); + } + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if ((obj == null) || (getClass() != obj.getClass())) { + return false; + } + TestingHiveCluster o = (TestingHiveCluster) obj; + + return Objects.equals(this.host, o.host) && + Objects.equals(this.port, o.port); + } + + @Override + public int hashCode() + { + return Objects.hash(host, port); + } +} diff --git a/presto-hive/src/test/java/com/facebook/presto/hive/metastore/MockHiveMetastoreClient.java b/presto-hive/src/test/java/com/facebook/presto/hive/metastore/MockHiveMetastoreClient.java new file mode 100644 index 00000000..2bcad924 --- /dev/null +++ b/presto-hive/src/test/java/com/facebook/presto/hive/metastore/MockHiveMetastoreClient.java @@ -0,0 +1,182 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive.metastore; + +import com.facebook.presto.hive.HiveMetastoreClient; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import org.apache.hadoop.hive.metastore.Warehouse; +import org.apache.hadoop.hive.metastore.api.Database; +import org.apache.hadoop.hive.metastore.api.FieldSchema; +import org.apache.hadoop.hive.metastore.api.MetaException; +import org.apache.hadoop.hive.metastore.api.NoSuchObjectException; +import org.apache.hadoop.hive.metastore.api.Partition; +import org.apache.hadoop.hive.metastore.api.Table; +import org.apache.thrift.TException; +import org.apache.thrift.transport.TTransport; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +public class MockHiveMetastoreClient + extends HiveMetastoreClient +{ + static final String TEST_DATABASE = "testdb"; + static final String BAD_DATABASE = "baddb"; + static final String TEST_TABLE = "testtbl"; + static final String TEST_PARTITION1 = "key=testpartition1"; + static final String TEST_PARTITION2 = "key=testpartition2"; + + private final AtomicInteger accessCount = new AtomicInteger(); + private boolean throwException; + + MockHiveMetastoreClient() + { + super((TTransport) null); + } + + public void setThrowException(boolean throwException) + { + this.throwException = throwException; + } + + public int getAccessCount() + { + return accessCount.get(); + } + + @Override + public List get_all_databases() + throws TException + { + accessCount.incrementAndGet(); + if (throwException) { + throw new IllegalStateException(); + } + return ImmutableList.of(TEST_DATABASE); + } + + @Override + public List get_all_tables(String dbName) + throws TException + { + accessCount.incrementAndGet(); + if (throwException) { + throw new RuntimeException(); + } + if (!dbName.equals(TEST_DATABASE)) { + return ImmutableList.of(); // As specified by Hive specification + } + return ImmutableList.of(TEST_TABLE); + } + + @Override + public Database get_database(String name) + throws TException + { + accessCount.incrementAndGet(); + if (throwException) { + throw new RuntimeException(); + } + if (!name.equals(TEST_DATABASE)) { + throw new NoSuchObjectException(); + } + return new Database(TEST_DATABASE, null, null, null); + } + + @Override + public Table get_table(String dbName, String tableName) + throws TException + { + accessCount.incrementAndGet(); + if (throwException) { + throw new RuntimeException(); + } + if (!dbName.equals(TEST_DATABASE) || !tableName.equals(TEST_TABLE)) { + throw new NoSuchObjectException(); + } + return new Table(TEST_TABLE, TEST_DATABASE, "", 0, 0, 0, null, ImmutableList.of(new FieldSchema("key", "String", null)), null, "", "", ""); + } + + @Override + public List get_partition_names(String dbName, String tableName, short maxParts) + throws TException + { + accessCount.incrementAndGet(); + if (throwException) { + throw new RuntimeException(); + } + if (!dbName.equals(TEST_DATABASE) || !tableName.equals(TEST_TABLE)) { + return ImmutableList.of(); + } + return ImmutableList.of(TEST_PARTITION1, TEST_PARTITION2); + } + + @Override + public List get_partition_names_ps(String dbName, String tableName, List partValues, short maxParts) + throws TException + { + accessCount.incrementAndGet(); + if (throwException) { + throw new RuntimeException(); + } + if (!dbName.equals(TEST_DATABASE) || !tableName.equals(TEST_TABLE)) { + throw new NoSuchObjectException(); + } + return ImmutableList.of(TEST_PARTITION1, TEST_PARTITION2); + } + + @Override + public Partition get_partition_by_name(String dbName, String tableName, String partName) + throws TException + { + accessCount.incrementAndGet(); + if (throwException) { + throw new RuntimeException(); + } + if (!dbName.equals(TEST_DATABASE) || !tableName.equals(TEST_TABLE) || !ImmutableSet.of(TEST_PARTITION1, TEST_PARTITION2).contains(partName)) { + throw new NoSuchObjectException(); + } + return new Partition(null, TEST_DATABASE, TEST_TABLE, 0, 0, null, null); + } + + @Override + public List get_partitions_by_names(String dbName, String tableName, List names) + throws TException + { + accessCount.incrementAndGet(); + if (throwException) { + throw new RuntimeException(); + } + if (!dbName.equals(TEST_DATABASE) || !tableName.equals(TEST_TABLE) || !ImmutableSet.of(TEST_PARTITION1, TEST_PARTITION2).containsAll(names)) { + throw new NoSuchObjectException(); + } + return Lists.transform(names, name -> { + try { + return new Partition(ImmutableList.copyOf(Warehouse.getPartValuesFromPartName(name)), TEST_DATABASE, TEST_TABLE, 0, 0, null, null); + } + catch (MetaException e) { + throw Throwables.propagate(e); + } + }); + } + + @Override + public void close() + { + // No-op + } +} diff --git a/presto-hive/src/test/java/com/facebook/presto/hive/metastore/TestCachingHiveMetastore.java b/presto-hive/src/test/java/com/facebook/presto/hive/metastore/TestCachingHiveMetastore.java new file mode 100644 index 00000000..06948251 --- /dev/null +++ b/presto-hive/src/test/java/com/facebook/presto/hive/metastore/TestCachingHiveMetastore.java @@ -0,0 +1,242 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive.metastore; + +import com.facebook.presto.hive.HiveCluster; +import com.facebook.presto.hive.HiveMetastoreClient; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.ListeningExecutorService; +import io.airlift.units.Duration; +import org.apache.hadoop.hive.metastore.api.NoSuchObjectException; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.util.concurrent.TimeUnit; + +import static com.facebook.presto.hive.metastore.MockHiveMetastoreClient.BAD_DATABASE; +import static com.facebook.presto.hive.metastore.MockHiveMetastoreClient.TEST_DATABASE; +import static com.facebook.presto.hive.metastore.MockHiveMetastoreClient.TEST_PARTITION1; +import static com.facebook.presto.hive.metastore.MockHiveMetastoreClient.TEST_PARTITION2; +import static com.facebook.presto.hive.metastore.MockHiveMetastoreClient.TEST_TABLE; +import static com.google.common.util.concurrent.MoreExecutors.listeningDecorator; +import static io.airlift.concurrent.Threads.daemonThreadsNamed; +import static java.util.concurrent.Executors.newCachedThreadPool; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +@Test(singleThreaded = true) +public class TestCachingHiveMetastore +{ + private MockHiveMetastoreClient mockClient; + private HiveMetastore metastore; + + @BeforeMethod + public void setUp() + throws Exception + { + mockClient = new MockHiveMetastoreClient(); + MockHiveCluster mockHiveCluster = new MockHiveCluster(mockClient); + ListeningExecutorService executor = listeningDecorator(newCachedThreadPool(daemonThreadsNamed("test-%s"))); + metastore = new CachingHiveMetastore(mockHiveCluster, executor, new Duration(5, TimeUnit.MINUTES), new Duration(1, TimeUnit.MINUTES)); + } + + @Test + public void testGetAllDatabases() + throws Exception + { + assertEquals(mockClient.getAccessCount(), 0); + assertEquals(metastore.getAllDatabases(), ImmutableList.of(TEST_DATABASE)); + assertEquals(mockClient.getAccessCount(), 1); + assertEquals(metastore.getAllDatabases(), ImmutableList.of(TEST_DATABASE)); + assertEquals(mockClient.getAccessCount(), 1); + + metastore.flushCache(); + + assertEquals(metastore.getAllDatabases(), ImmutableList.of(TEST_DATABASE)); + assertEquals(mockClient.getAccessCount(), 2); + } + + @Test + public void testGetAllTable() + throws Exception + { + assertEquals(mockClient.getAccessCount(), 0); + assertEquals(metastore.getAllTables(TEST_DATABASE), ImmutableList.of(TEST_TABLE)); + assertEquals(mockClient.getAccessCount(), 1); + assertEquals(metastore.getAllTables(TEST_DATABASE), ImmutableList.of(TEST_TABLE)); + assertEquals(mockClient.getAccessCount(), 1); + + metastore.flushCache(); + + assertEquals(metastore.getAllTables(TEST_DATABASE), ImmutableList.of(TEST_TABLE)); + assertEquals(mockClient.getAccessCount(), 2); + } + + @Test(expectedExceptions = NoSuchObjectException.class) + public void testInvalidDbGetAllTAbles() + throws Exception + { + metastore.getAllTables(BAD_DATABASE); + } + + @Test + public void testGetTable() + throws Exception + { + assertEquals(mockClient.getAccessCount(), 0); + assertNotNull(metastore.getTable(TEST_DATABASE, TEST_TABLE)); + assertEquals(mockClient.getAccessCount(), 1); + assertNotNull(metastore.getTable(TEST_DATABASE, TEST_TABLE)); + assertEquals(mockClient.getAccessCount(), 1); + + metastore.flushCache(); + + assertNotNull(metastore.getTable(TEST_DATABASE, TEST_TABLE)); + assertEquals(mockClient.getAccessCount(), 2); + } + + @Test(expectedExceptions = NoSuchObjectException.class) + public void testInvalidDbGetTable() + throws Exception + { + metastore.getTable(BAD_DATABASE, TEST_TABLE); + } + + @Test + public void testGetPartitionNames() + throws Exception + { + ImmutableList expectedPartitions = ImmutableList.of(TEST_PARTITION1, TEST_PARTITION2); + assertEquals(mockClient.getAccessCount(), 0); + assertEquals(metastore.getPartitionNames(TEST_DATABASE, TEST_TABLE), expectedPartitions); + assertEquals(mockClient.getAccessCount(), 1); + assertEquals(metastore.getPartitionNames(TEST_DATABASE, TEST_TABLE), expectedPartitions); + assertEquals(mockClient.getAccessCount(), 1); + + metastore.flushCache(); + + assertEquals(metastore.getPartitionNames(TEST_DATABASE, TEST_TABLE), expectedPartitions); + assertEquals(mockClient.getAccessCount(), 2); + } + + @Test + public void testInvalidGetPartitionNames() + throws Exception + { + assertEquals(metastore.getPartitionNames(BAD_DATABASE, TEST_TABLE), ImmutableList.of()); + } + + @Test + public void testGetPartitionNamesByParts() + throws Exception + { + ImmutableList parts = ImmutableList.of(); + ImmutableList expectedPartitions = ImmutableList.of(TEST_PARTITION1, TEST_PARTITION2); + + assertEquals(mockClient.getAccessCount(), 0); + assertEquals(metastore.getPartitionNamesByParts(TEST_DATABASE, TEST_TABLE, parts), expectedPartitions); + assertEquals(mockClient.getAccessCount(), 1); + assertEquals(metastore.getPartitionNamesByParts(TEST_DATABASE, TEST_TABLE, parts), expectedPartitions); + assertEquals(mockClient.getAccessCount(), 1); + + metastore.flushCache(); + + assertEquals(metastore.getPartitionNamesByParts(TEST_DATABASE, TEST_TABLE, parts), expectedPartitions); + assertEquals(mockClient.getAccessCount(), 2); + } + + @Test(expectedExceptions = NoSuchObjectException.class) + public void testInvalidGetPartitionNamesByParts() + throws Exception + { + ImmutableList parts = ImmutableList.of(); + metastore.getPartitionNamesByParts(BAD_DATABASE, TEST_TABLE, parts); + } + + @Test + public void testGetPartitionsByNames() + throws Exception + { + assertEquals(mockClient.getAccessCount(), 0); + metastore.getTable(TEST_DATABASE, TEST_TABLE); + assertEquals(mockClient.getAccessCount(), 1); + + // Select half of the available partitions and load them into the cache + assertEquals(metastore.getPartitionsByNames(TEST_DATABASE, TEST_TABLE, ImmutableList.of(TEST_PARTITION1)).size(), 1); + assertEquals(mockClient.getAccessCount(), 2); + + // Now select all of the partitions + assertEquals(metastore.getPartitionsByNames(TEST_DATABASE, TEST_TABLE, ImmutableList.of(TEST_PARTITION1, TEST_PARTITION2)).size(), 2); + // There should be one more access to fetch the remaining partition + assertEquals(mockClient.getAccessCount(), 3); + + // Now if we fetch any or both of them, they should not hit the client + assertEquals(metastore.getPartitionsByNames(TEST_DATABASE, TEST_TABLE, ImmutableList.of(TEST_PARTITION1)).size(), 1); + assertEquals(metastore.getPartitionsByNames(TEST_DATABASE, TEST_TABLE, ImmutableList.of(TEST_PARTITION2)).size(), 1); + assertEquals(metastore.getPartitionsByNames(TEST_DATABASE, TEST_TABLE, ImmutableList.of(TEST_PARTITION1, TEST_PARTITION2)).size(), 2); + assertEquals(mockClient.getAccessCount(), 3); + + metastore.flushCache(); + + // Fetching both should only result in one batched access + assertEquals(metastore.getPartitionsByNames(TEST_DATABASE, TEST_TABLE, ImmutableList.of(TEST_PARTITION1, TEST_PARTITION2)).size(), 2); + assertEquals(mockClient.getAccessCount(), 4); + } + + @Test(expectedExceptions = NoSuchObjectException.class) + public void testInvalidGetPartitionsByNames() + throws Exception + { + metastore.getPartitionsByNames(BAD_DATABASE, TEST_TABLE, ImmutableList.of(TEST_PARTITION1)); + } + + @Test + public void testNoCacheExceptions() + throws Exception + { + // Throw exceptions on usage + mockClient.setThrowException(true); + try { + metastore.getAllDatabases(); + } + catch (RuntimeException ignored) { + } + assertEquals(mockClient.getAccessCount(), 1); + + // Second try should hit the client again + try { + metastore.getAllDatabases(); + } + catch (RuntimeException ignored) { + } + assertEquals(mockClient.getAccessCount(), 2); + } + + private static class MockHiveCluster + implements HiveCluster + { + private final HiveMetastoreClient client; + + private MockHiveCluster(HiveMetastoreClient client) + { + this.client = client; + } + + @Override + public HiveMetastoreClient createMetastoreClient() + { + return client; + } + } +} diff --git a/presto-hive/src/test/java/com/facebook/presto/hive/util/TestLazyMap.java b/presto-hive/src/test/java/com/facebook/presto/hive/util/TestLazyMap.java new file mode 100644 index 00000000..6806b9fe --- /dev/null +++ b/presto-hive/src/test/java/com/facebook/presto/hive/util/TestLazyMap.java @@ -0,0 +1,84 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive.util; + +import com.google.common.collect.ImmutableMap; +import org.apache.hadoop.hive.serde2.lazy.ByteArrayRef; +import org.apache.hadoop.hive.serde2.lazy.LazyMap; +import org.apache.hadoop.hive.serde2.lazy.LazyString; +import org.apache.hadoop.hive.serde2.lazy.objectinspector.primitive.LazyStringObjectInspector; +import org.apache.hadoop.io.Text; +import org.testng.annotations.Test; + +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +import static org.apache.hadoop.hive.serde2.lazy.LazyFactory.createLazyObject; +import static org.apache.hadoop.hive.serde2.lazy.objectinspector.LazyObjectInspectorFactory.getLazySimpleMapObjectInspector; +import static org.apache.hadoop.hive.serde2.lazy.objectinspector.primitive.LazyPrimitiveObjectInspectorFactory.getLazyStringObjectInspector; +import static org.testng.Assert.assertEquals; + +public class TestLazyMap +{ + private static final LazyStringObjectInspector LAZY_STRING_OBJECT_INSPECTOR = getLazyStringObjectInspector(false, (byte) 0); + + @Test + public void test() + throws Exception + { + assertMapDecode("\\N\u0003ignored", ImmutableMap.of()); + assertMapDecode("\\N\u0003ignored\u0002alice\u0003apple", ImmutableMap.of(lazyString("alice"), lazyString("apple"))); + assertMapDecode("alice\u0003apple\u0002\\N\u0003ignored", ImmutableMap.of(lazyString("alice"), lazyString("apple"))); + assertMapDecode("alice\u0003apple\u0002\\N\u0003ignored\u0002bob\u0003banana", + ImmutableMap.of(lazyString("alice"), lazyString("apple"), lazyString("bob"), lazyString("banana"))); + assertMapDecode("\\N\u0003ignored\u0002\u0003", ImmutableMap.of(lazyString(""), lazyString(""))); + + HashMap expectedMap = new HashMap<>(); + expectedMap.put("null", null); + assertMapDecode("\\N\u0003ignored\u0002null\u0003\\N", expectedMap); + } + + public static void assertMapDecode(String encodedMap, Map expectedMap) + { + LazyMap lazyMap = (LazyMap) createLazyObject(getLazySimpleMapObjectInspector( + LAZY_STRING_OBJECT_INSPECTOR, + getLazyStringObjectInspector(false, (byte) 0), + (byte) 2, + (byte) 3, + new Text("\\N"), + false, + (byte) 0 + )); + + lazyMap.init(newByteArrayRef(encodedMap), 0, encodedMap.length()); + + Map map = lazyMap.getMap(); + assertEquals(map, expectedMap); + } + + private static LazyString lazyString(String string) + { + LazyString lazyString = new LazyString(LAZY_STRING_OBJECT_INSPECTOR); + lazyString.init(newByteArrayRef(string), 0, string.length()); + return lazyString; + } + + public static ByteArrayRef newByteArrayRef(String encodedMap) + { + ByteArrayRef bytes = new ByteArrayRef(); + bytes.setData(encodedMap.getBytes(StandardCharsets.US_ASCII)); + return bytes; + } +} diff --git a/presto-hive/src/test/java/com/facebook/presto/hive/util/TestSerDeUtils.java b/presto-hive/src/test/java/com/facebook/presto/hive/util/TestSerDeUtils.java new file mode 100644 index 00000000..611cc58a --- /dev/null +++ b/presto-hive/src/test/java/com/facebook/presto/hive/util/TestSerDeUtils.java @@ -0,0 +1,294 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.hive.util; + +import com.facebook.presto.hadoop.shaded.com.google.common.collect.ImmutableList; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.block.BlockBuilderStatus; +import com.facebook.presto.spi.block.VariableWidthBlockBuilder; +import com.facebook.presto.spi.type.VarbinaryType; +import com.facebook.presto.type.ArrayType; +import com.facebook.presto.type.MapType; +import com.facebook.presto.type.RowType; +import com.google.common.collect.ImmutableMap; +import com.google.common.reflect.TypeToken; +import io.airlift.slice.DynamicSliceOutput; +import io.airlift.slice.Slice; +import io.airlift.slice.Slices; +import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector; +import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector.Category; +import org.apache.hadoop.io.BytesWritable; +import org.joda.time.DateTime; +import org.testng.annotations.Test; + +import java.lang.reflect.Type; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.TreeMap; + +import static com.facebook.presto.hive.HiveTestUtils.arraySliceOf; +import static com.facebook.presto.hive.HiveTestUtils.mapSliceOf; +import static com.facebook.presto.hive.HiveTestUtils.rowSliceOf; +import static com.facebook.presto.hive.util.SerDeUtils.getBlockSlice; +import static com.facebook.presto.hive.util.SerDeUtils.serializeObject; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static com.facebook.presto.type.TypeUtils.buildStructuralSlice; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.apache.hadoop.hive.serde2.objectinspector.ObjectInspectorFactory.ObjectInspectorOptions; +import static org.apache.hadoop.hive.serde2.objectinspector.ObjectInspectorFactory.getReflectionObjectInspector; +import static org.testng.Assert.assertEquals; + +@SuppressWarnings("PackageVisibleField") +public class TestSerDeUtils +{ + private static class ListHolder + { + List array; + } + + private static class InnerStruct + { + public InnerStruct(Integer intVal, Long longVal) + { + this.intVal = intVal; + this.longVal = longVal; + } + + Integer intVal; + Long longVal; + } + + private static class OuterStruct + { + Byte byteVal; + Short shortVal; + Integer intVal; + Long longVal; + Float floatVal; + Double doubleVal; + String stringVal; + byte[] byteArray; + List structArray; + Map map; + InnerStruct innerStruct; + } + + private static ObjectInspector getInspector(Type type) + { + return getReflectionObjectInspector(type, ObjectInspectorOptions.JAVA); + } + + @Test + public void testPrimitiveSlice() + { + // boolean + Slice expectedBoolean = new DynamicSliceOutput(10).appendByte(1).slice(); + Slice actualBoolean = toBinarySlice(true, getInspector(Boolean.class)); + assertEquals(actualBoolean, expectedBoolean); + + // byte + Slice expectedByte = new DynamicSliceOutput(10).appendLong(5).slice(); + Slice actualByte = toBinarySlice((byte) 5, getInspector(Byte.class)); + assertEquals(actualByte, expectedByte); + // short + Slice expectedShort = new DynamicSliceOutput(10).appendLong(2).slice(); + Slice actualShort = toBinarySlice((short) 2, getInspector(Short.class)); + assertEquals(actualShort, expectedShort); + + // int + Slice expectedInt = new DynamicSliceOutput(10).appendLong(1).slice(); + Slice actualInt = toBinarySlice(1, getInspector(Integer.class)); + assertEquals(actualInt, expectedInt); + + // long + Slice expectedLong = new DynamicSliceOutput(10).appendLong(10).slice(); + Slice actualLong = toBinarySlice(10L, getInspector(Long.class)); + assertEquals(actualLong, expectedLong); + + // float + Slice expectedFloat = new DynamicSliceOutput(10).appendDouble(20.0).slice(); + Slice actualFloat = toBinarySlice(20.0f, getInspector(Float.class)); + assertEquals(actualFloat, expectedFloat); + + // double + Slice expectedDouble = new DynamicSliceOutput(10).appendDouble(30.12).slice(); + Slice actualDouble = toBinarySlice(30.12d, getInspector(Double.class)); + assertEquals(actualDouble, expectedDouble); + + // string + Slice expectedString = Slices.utf8Slice("abdd"); + Slice actualString = toBinarySlice("abdd", getInspector(String.class)); + assertEquals(actualString, expectedString); + + // timestamp + DateTime dateTime = new DateTime(2008, 10, 28, 16, 7, 15, 0); + Slice expectedTimestamp = new DynamicSliceOutput(10).appendLong(dateTime.getMillis()).slice(); + Slice actualTimestamp = toBinarySlice(new Timestamp(dateTime.getMillis()), getInspector(Timestamp.class)); + assertEquals(actualTimestamp, expectedTimestamp); + + // binary + byte[] byteArray = {81, 82, 84, 85}; + Slice expectedBinary = Slices.wrappedBuffer(byteArray); + Slice actualBinary = toBinarySlice(byteArray, getInspector(byte[].class)); + assertEquals(actualBinary, expectedBinary); + } + + @Test + public void testListBlock() + { + List array = new ArrayList<>(2); + array.add(new InnerStruct(8, 9L)); + array.add(new InnerStruct(10, 11L)); + ListHolder listHolder = new ListHolder(); + listHolder.array = array; + + Slice actual = toBinarySlice(listHolder, getInspector(ListHolder.class)); + + com.facebook.presto.spi.type.Type rowType = new RowType(ImmutableList.of(BIGINT, BIGINT), Optional.empty()); + BlockBuilder blockBuilder = new VariableWidthBlockBuilder(new BlockBuilderStatus(), 1024); + rowType.writeSlice(blockBuilder, rowSliceOf(ImmutableList.of(BIGINT, BIGINT), 8, 9L)); + rowType.writeSlice(blockBuilder, rowSliceOf(ImmutableList.of(BIGINT, BIGINT), 10, 11L)); + Slice expected = rowSliceOf(ImmutableList.of(new ArrayType(rowType)), buildStructuralSlice(blockBuilder)); + + assertEquals(actual, expected); + } + + private static class MapHolder + { + Map map; + } + + @Test + public void testMapBlock() + { + MapHolder holder = new MapHolder(); + holder.map = new TreeMap<>(); + holder.map.put("twelve", new InnerStruct(13, 14L)); + holder.map.put("fifteen", new InnerStruct(16, 17L)); + + Slice actual = toBinarySlice(holder, getInspector(MapHolder.class)); + + com.facebook.presto.spi.type.Type rowType = new RowType(ImmutableList.of(BIGINT, BIGINT), Optional.empty()); + BlockBuilder blockBuilder = new VariableWidthBlockBuilder(new BlockBuilderStatus(), 1024); + VARCHAR.writeString(blockBuilder, "fifteen"); + rowType.writeSlice(blockBuilder, rowSliceOf(ImmutableList.of(BIGINT, BIGINT), 16, 17L)); + VARCHAR.writeString(blockBuilder, "twelve"); + rowType.writeSlice(blockBuilder, rowSliceOf(ImmutableList.of(BIGINT, BIGINT), 13, 14L)); + Slice expected = rowSliceOf(ImmutableList.of(new MapType(VARCHAR, rowType)), buildStructuralSlice(blockBuilder)); + + assertEquals(actual, expected); + } + + @Test + public void testStructBlock() + { + // test simple structs + InnerStruct innerStruct = new InnerStruct(13, 14L); + Slice actual = toBinarySlice(innerStruct, getInspector(InnerStruct.class)); + Slice expected = rowSliceOf(ImmutableList.of(BIGINT, BIGINT), 13, 14L); + assertEquals(actual, expected); + + // test complex structs + OuterStruct outerStruct = new OuterStruct(); + outerStruct.byteVal = 1; + outerStruct.shortVal = 2; + outerStruct.intVal = 3; + outerStruct.longVal = 4L; + outerStruct.floatVal = 5.01f; + outerStruct.doubleVal = 6.001d; + outerStruct.stringVal = "seven"; + outerStruct.byteArray = new byte[] {'2'}; + InnerStruct is1 = new InnerStruct(2, -5L); + InnerStruct is2 = new InnerStruct(-10, 0L); + outerStruct.structArray = new ArrayList<>(2); + outerStruct.structArray.add(is1); + outerStruct.structArray.add(is2); + outerStruct.map = new TreeMap<>(); + outerStruct.map.put("twelve", new InnerStruct(0, 5L)); + outerStruct.map.put("fifteen", new InnerStruct(-5, -10L)); + outerStruct.innerStruct = new InnerStruct(18, 19L); + + actual = toBinarySlice(outerStruct, getInspector(OuterStruct.class)); + com.facebook.presto.spi.type.Type innerRowType = new RowType(ImmutableList.of(BIGINT, BIGINT), Optional.empty()); + com.facebook.presto.spi.type.Type arrayOfInnerRowType = new ArrayType(innerRowType); + com.facebook.presto.spi.type.Type mapOfInnerRowType = new MapType(VARCHAR, innerRowType); + List outerRowParameterTypes = ImmutableList.of(BIGINT, BIGINT, BIGINT, BIGINT, DOUBLE, DOUBLE, VARCHAR, VARCHAR, arrayOfInnerRowType, mapOfInnerRowType, innerRowType); + + ImmutableList.Builder outerRowValues = ImmutableList.builder(); + outerRowValues.add(1); + outerRowValues.add(2); + outerRowValues.add(3); + outerRowValues.add(4L); + outerRowValues.add(5.01f); + outerRowValues.add(6.001d); + outerRowValues.add("seven"); + outerRowValues.add(new byte[] {'2'}); + outerRowValues.add(arraySliceOf(innerRowType, rowSliceOf(ImmutableList.of(BIGINT, BIGINT), 2, -5L), rowSliceOf(ImmutableList.of(BIGINT, BIGINT), -10, 0))); + BlockBuilder blockBuilder = new VariableWidthBlockBuilder(new BlockBuilderStatus(), 1024); + VARCHAR.writeString(blockBuilder, "fifteen"); + innerRowType.writeSlice(blockBuilder, rowSliceOf(ImmutableList.of(BIGINT, BIGINT), -5, -10L)); + VARCHAR.writeString(blockBuilder, "twelve"); + innerRowType.writeSlice(blockBuilder, rowSliceOf(ImmutableList.of(BIGINT, BIGINT), 0, 5L)); + outerRowValues.add(buildStructuralSlice(blockBuilder)); + outerRowValues.add(rowSliceOf(ImmutableList.of(BIGINT, BIGINT), 18, 19L)); + + assertEquals(actual, rowSliceOf(outerRowParameterTypes, outerRowValues.build().toArray())); + } + + @Test + public void testReuse() + throws Exception + { + BytesWritable value = new BytesWritable(); + + byte[] first = "hello world".getBytes(UTF_8); + value.set(first, 0, first.length); + + byte[] second = "bye".getBytes(UTF_8); + value.set(second, 0, second.length); + + Type type = new TypeToken>() {}.getType(); + ObjectInspector inspector = getReflectionObjectInspector(type, ObjectInspectorOptions.JAVA); + + Slice actual = getBlockSlice(ImmutableMap.of(value, 0), inspector); + Slice expected = mapSliceOf(VARCHAR, BIGINT, "bye", 0); + + assertEquals(actual, expected); + } + + private static Slice toBinarySlice(Object object, ObjectInspector inspector) + { + if (inspector.getCategory() == Category.PRIMITIVE) { + return getPrimitiveSlice(object, inspector); + } + return getBlockSlice(object, inspector); + } + + private static Slice getPrimitiveSlice(Object object, ObjectInspector inspector) + { + BlockBuilder builder = VarbinaryType.VARBINARY.createBlockBuilder(new BlockBuilderStatus(), 1); + serializeObject(builder, object, inspector); + if (builder.isNull(0)) { + return Slices.EMPTY_SLICE; + } + Block block = builder.build(); + return VarbinaryType.VARBINARY.getSlice(block, 0); + } +} diff --git a/presto-hive/src/test/resources/addressbook.parquet b/presto-hive/src/test/resources/addressbook.parquet new file mode 100644 index 0000000000000000000000000000000000000000..0f5b8e0c07dc8cdd6b7f92c5cd6c72da1c52ce7a GIT binary patch literal 3956 zcmc&%-EQMV6rQ+gTB7y>340x>T3L}AwNkM9vxOzfMO!w|h_(r7E07Rcxv@hm5<6=< zzzSubfXlt-4fY}SnhTy_UjUwfE6$v;$79ECED$19{d0c4^PL}$$NjUxp-kjGd9Nu; za8Z#fJL=lGdyfZ&z)ynzHTZ)gJAoD~599;EWnBh`UNU+xPYkM7-j-D5#dt)GnPY+~s{cdF(omT?~eZVn2BYsoDkT=nqi_{qsf zA3y!%KozbL6iY)0WxuvgNEObpH6T5fA9EyHO8iG_ILK1%w~Ot*;k}q!!Od`NO$}Kh zCz84`vpnB*d}vFBoRrn=@cwx)hg9WFasmZ&jHxA;iM+KdsnwC~`N4B4+67!HdT(L~ z5(y-dZ8Ft51hOY!2%##ovFlho(iXc71BK`nXFi1vN66*i<;>dMB#KnqsZAQ2JMva- zmsDyO5$z;hBq|V;!BH70qsC@CHEj*22vnRHBE3>xHJmn|J$`}hY5ZB*fhtpWPqW!U zQ4q#dOfYV9%p&fE2W5C{8?QBIZ9##qelYgzQJ|ZaKlJQb;Ckw> zAN*X^w7PB9wU$=DxgFendRuQ&(Qs@}OwV$t;v0=xfsl?aT85*JrXU~L*2L^0$RYIx zk!QV_+nzPGoB%zZbv`@0yJX;?rqfB~3>X9cMLQ7Ykie+RL<*2W zlQba|F4u*M=`XWBOeIo8uB;+lnavSF(?uI}zP#%WI?uv7bBHXm2#`-0)mcrAK+%BL zHeRYyvC%9)1yrZgNcKJvFfS9tiL>cme_qGA5}g+*$)d@}DK?A8g)g-$GCE533z__1 z8EM}pauhRO$~Mjv>Xytwd_Ys_t$0wQLM}b9ToE>@dwi7aP(iNvmV1EiaVnEyomfd1 zmb#^xD#)0^3vfxvzAD%2K*VW@7Orf}xW$#f^lOIQ3w2 zlXRlJ{%yB+eRi2wyP}zPuJ5jLFo1nLm(lJaA|#%}*Zx6vcGNOn)vkKy-OEmLk%_T# z>xdw%IT-^U0mNZ0dRPA{NKPA&2A|%qdV?==Gf6G_5yi~u$*@h^CGW+%h2R~@*_j@x zc0ttY%>GXI@w-R;r4(-bVB;3Fv^|;+oZ`(wOB;?2&+t6sWs|>e1>t_vHd~tQ1WjD` z1x=GJQ4uhH%ZO?^mBRi0Vjs9|g*)0I#PZc6_`1}CAMGHpJl!+`L!WtW;OfB~wz{@4 g(L?Jq6E?QKf9|^9)AZPv-G@_;{IN|)AHKK$22x{7AOHXW literal 0 HcmV?d00001 diff --git a/presto-hive/src/test/sql/create-test-cdh4.sql b/presto-hive/src/test/sql/create-test-cdh4.sql new file mode 100644 index 00000000..e4a0b64f --- /dev/null +++ b/presto-hive/src/test/sql/create-test-cdh4.sql @@ -0,0 +1,113 @@ +CREATE TABLE presto_test_types_textfile ( + t_string STRING +, t_tinyint TINYINT +, t_smallint SMALLINT +, t_int INT +, t_bigint BIGINT +, t_float FLOAT +, t_double DOUBLE +, t_boolean BOOLEAN +, t_timestamp TIMESTAMP +, t_binary BINARY +, t_map MAP +, t_array_string ARRAY +, t_array_struct ARRAY> +, t_complex MAP>> +) +STORED AS TEXTFILE +; + +INSERT INTO TABLE presto_test_types_textfile +SELECT + CASE n % 19 WHEN 0 THEN NULL WHEN 1 THEN '' ELSE 'test' END +, 1 + n +, 2 + n +, 3 + n +, 4 + n + CASE WHEN n % 13 = 0 THEN NULL ELSE 0 END +, 5.1 + n +, 6.2 + n +, CASE n % 3 WHEN 0 THEN false WHEN 1 THEN true ELSE NULL END +, CASE WHEN n % 17 = 0 THEN NULL ELSE '2011-05-06 07:08:09.1234567' END +, CASE WHEN n % 23 = 0 THEN NULL ELSE CAST('test binary' AS BINARY) END +, CASE WHEN n % 27 = 0 THEN NULL ELSE map('test key', 'test value') END +, CASE WHEN n % 29 = 0 THEN NULL ELSE array('abc', 'xyz', 'data') END +, CASE WHEN n % 31 = 0 THEN NULL ELSE + array(named_struct('s_string', 'test abc', 's_double', 0.1), + named_struct('s_string' , 'test xyz', 's_double', 0.2)) END +, CASE WHEN n % 33 = 0 THEN NULL ELSE + map(1, array(named_struct('s_string', 'test abc', 's_double', 0.1), + named_struct('s_string' , 'test xyz', 's_double', 0.2))) END +FROM presto_test_sequence +LIMIT 100 +; + + +CREATE TABLE presto_test_types_sequencefile LIKE presto_test_types_textfile; +ALTER TABLE presto_test_types_sequencefile SET FILEFORMAT SEQUENCEFILE; + +INSERT INTO TABLE presto_test_types_sequencefile +SELECT * FROM presto_test_types_textfile +; + + +CREATE TABLE presto_test_types_rctext LIKE presto_test_types_textfile; +ALTER TABLE presto_test_types_rctext SET FILEFORMAT RCFILE; +ALTER TABLE presto_test_types_rctext SET SERDE 'org.apache.hadoop.hive.serde2.columnar.ColumnarSerDe'; + +INSERT INTO TABLE presto_test_types_rctext +SELECT * FROM presto_test_types_textfile +; + + +CREATE TABLE presto_test_types_rcbinary LIKE presto_test_types_textfile; +ALTER TABLE presto_test_types_rcbinary SET FILEFORMAT RCFILE; +ALTER TABLE presto_test_types_rcbinary SET SERDE 'org.apache.hadoop.hive.serde2.columnar.LazyBinaryColumnarSerDe'; + +INSERT INTO TABLE presto_test_types_rcbinary +SELECT * FROM presto_test_types_textfile +; + + +-- Parquet is missing TIMESTAMP and BINARY, and for some reason +-- fails when trying to use complex nested types. +CREATE TABLE presto_test_types_parquet ( + t_string STRING +, t_tinyint TINYINT +, t_smallint SMALLINT +, t_int INT +, t_bigint BIGINT +, t_float FLOAT +, t_double DOUBLE +, t_boolean BOOLEAN +, t_map MAP +, t_array_string ARRAY +, t_array_struct ARRAY> +) +ROW FORMAT SERDE 'parquet.hive.serde.ParquetHiveSerDe' +STORED AS + INPUTFORMAT 'parquet.hive.DeprecatedParquetInputFormat' + OUTPUTFORMAT 'parquet.hive.DeprecatedParquetOutputFormat' +; + +INSERT INTO TABLE presto_test_types_parquet +SELECT + t_string +, t_tinyint +, t_smallint +, t_int +, t_bigint +, t_float +, t_double +, t_boolean +, t_map +, t_array_string +, t_array_struct +FROM presto_test_types_textfile +; + + +ALTER TABLE presto_test_types_textfile ADD COLUMNS (new_column INT); +ALTER TABLE presto_test_types_sequencefile ADD COLUMNS (new_column INT); +ALTER TABLE presto_test_types_rctext ADD COLUMNS (new_column INT); +ALTER TABLE presto_test_types_rcbinary ADD COLUMNS (new_column INT); +ALTER TABLE presto_test_types_parquet ADD COLUMNS (new_column INT); diff --git a/presto-hive/src/test/sql/create-test-hive12.sql b/presto-hive/src/test/sql/create-test-hive12.sql new file mode 100644 index 00000000..3cc14179 --- /dev/null +++ b/presto-hive/src/test/sql/create-test-hive12.sql @@ -0,0 +1,83 @@ +CREATE TABLE presto_test_types_textfile ( + t_string STRING +, t_tinyint TINYINT +, t_smallint SMALLINT +, t_int INT +, t_bigint BIGINT +, t_float FLOAT +, t_double DOUBLE +, t_boolean BOOLEAN +, t_timestamp TIMESTAMP +, t_binary BINARY +, t_map MAP +, t_array_string ARRAY +, t_array_struct ARRAY> +, t_complex MAP>> +) +STORED AS TEXTFILE +; + +INSERT INTO TABLE presto_test_types_textfile +SELECT + CASE n % 19 WHEN 0 THEN NULL WHEN 1 THEN '' ELSE 'test' END +, 1 + n +, 2 + n +, 3 + n +, 4 + n + CASE WHEN n % 13 = 0 THEN NULL ELSE 0 END +, 5.1 + n +, 6.2 + n +, CASE n % 3 WHEN 0 THEN false WHEN 1 THEN true ELSE NULL END +, CASE WHEN n % 17 = 0 THEN NULL ELSE '2011-05-06 07:08:09.1234567' END +, CASE WHEN n % 23 = 0 THEN NULL ELSE CAST('test binary' AS BINARY) END +, CASE WHEN n % 27 = 0 THEN NULL ELSE map('test key', 'test value') END +, CASE WHEN n % 29 = 0 THEN NULL ELSE array('abc', 'xyz', 'data') END +, CASE WHEN n % 31 = 0 THEN NULL ELSE + array(named_struct('s_string', 'test abc', 's_double', 0.1), + named_struct('s_string' , 'test xyz', 's_double', 0.2)) END +, CASE WHEN n % 33 = 0 THEN NULL ELSE + map(1, array(named_struct('s_string', 'test abc', 's_double', 0.1), + named_struct('s_string' , 'test xyz', 's_double', 0.2))) END +FROM presto_test_sequence +LIMIT 100 +; + + +CREATE TABLE presto_test_types_sequencefile LIKE presto_test_types_textfile; +ALTER TABLE presto_test_types_sequencefile SET FILEFORMAT SEQUENCEFILE; + +INSERT INTO TABLE presto_test_types_sequencefile +SELECT * FROM presto_test_types_textfile +; + + +CREATE TABLE presto_test_types_rctext LIKE presto_test_types_textfile; +ALTER TABLE presto_test_types_rctext SET FILEFORMAT RCFILE; +ALTER TABLE presto_test_types_rctext SET SERDE 'org.apache.hadoop.hive.serde2.columnar.ColumnarSerDe'; + +INSERT INTO TABLE presto_test_types_rctext +SELECT * FROM presto_test_types_textfile +; + + +CREATE TABLE presto_test_types_rcbinary LIKE presto_test_types_textfile; +ALTER TABLE presto_test_types_rcbinary SET FILEFORMAT RCFILE; +ALTER TABLE presto_test_types_rcbinary SET SERDE 'org.apache.hadoop.hive.serde2.columnar.LazyBinaryColumnarSerDe'; + +INSERT INTO TABLE presto_test_types_rcbinary +SELECT * FROM presto_test_types_textfile +; + + +CREATE TABLE presto_test_types_orc LIKE presto_test_types_textfile; +ALTER TABLE presto_test_types_orc SET FILEFORMAT ORC; + +INSERT INTO TABLE presto_test_types_orc +SELECT * FROM presto_test_types_textfile +; + + +ALTER TABLE presto_test_types_textfile ADD COLUMNS (new_column INT); +ALTER TABLE presto_test_types_sequencefile ADD COLUMNS (new_column INT); +ALTER TABLE presto_test_types_rctext ADD COLUMNS (new_column INT); +ALTER TABLE presto_test_types_rcbinary ADD COLUMNS (new_column INT); +ALTER TABLE presto_test_types_orc ADD COLUMNS (new_column INT); diff --git a/presto-hive/src/test/sql/create-test-hive13.sql b/presto-hive/src/test/sql/create-test-hive13.sql new file mode 100644 index 00000000..8720bbe2 --- /dev/null +++ b/presto-hive/src/test/sql/create-test-hive13.sql @@ -0,0 +1,125 @@ +CREATE TABLE presto_test_types_textfile ( + t_string STRING +, t_tinyint TINYINT +, t_smallint SMALLINT +, t_int INT +, t_bigint BIGINT +, t_float FLOAT +, t_double DOUBLE +, t_boolean BOOLEAN +, t_timestamp TIMESTAMP +, t_binary BINARY +, t_date DATE +, t_varchar VARCHAR(50) +, t_char CHAR(25) +, t_map MAP +, t_array_string ARRAY +, t_array_struct ARRAY> +, t_complex MAP>> +) +STORED AS TEXTFILE +; + +INSERT INTO TABLE presto_test_types_textfile +SELECT + CASE n % 19 WHEN 0 THEN NULL WHEN 1 THEN '' ELSE 'test' END +, 1 + n +, 2 + n +, 3 + n +, 4 + n + CASE WHEN n % 13 = 0 THEN NULL ELSE 0 END +, 5.1 + n +, 6.2 + n +, CASE n % 3 WHEN 0 THEN false WHEN 1 THEN true ELSE NULL END +, CASE WHEN n % 17 = 0 THEN NULL ELSE '2011-05-06 07:08:09.1234567' END +, CASE WHEN n % 23 = 0 THEN NULL ELSE CAST('test binary' AS BINARY) END +, CASE WHEN n % 37 = 0 THEN NULL ELSE '2013-08-09' END +, CASE n % 39 WHEN 0 THEN NULL WHEN 1 THEN '' ELSE 'test varchar' END +, CASE n % 41 WHEN 0 THEN NULL WHEN 1 THEN '' ELSE 'test char' END +, CASE WHEN n % 27 = 0 THEN NULL ELSE map('test key', 'test value') END +, CASE WHEN n % 29 = 0 THEN NULL ELSE array('abc', 'xyz', 'data') END +, CASE WHEN n % 31 = 0 THEN NULL ELSE + array(named_struct('s_string', 'test abc', 's_double', 0.1), + named_struct('s_string' , 'test xyz', 's_double', 0.2)) END +, CASE WHEN n % 33 = 0 THEN NULL ELSE + map(1, array(named_struct('s_string', 'test abc', 's_double', 0.1), + named_struct('s_string' , 'test xyz', 's_double', 0.2))) END +FROM presto_test_sequence +LIMIT 100 +; + + +CREATE TABLE presto_test_types_sequencefile LIKE presto_test_types_textfile; +ALTER TABLE presto_test_types_sequencefile SET FILEFORMAT SEQUENCEFILE; + +INSERT INTO TABLE presto_test_types_sequencefile +SELECT * FROM presto_test_types_textfile +; + + +CREATE TABLE presto_test_types_rctext LIKE presto_test_types_textfile; +ALTER TABLE presto_test_types_rctext SET FILEFORMAT RCFILE; +ALTER TABLE presto_test_types_rctext SET SERDE 'org.apache.hadoop.hive.serde2.columnar.ColumnarSerDe'; + +INSERT INTO TABLE presto_test_types_rctext +SELECT * FROM presto_test_types_textfile +; + + +CREATE TABLE presto_test_types_rcbinary LIKE presto_test_types_textfile; +ALTER TABLE presto_test_types_rcbinary SET FILEFORMAT RCFILE; +ALTER TABLE presto_test_types_rcbinary SET SERDE 'org.apache.hadoop.hive.serde2.columnar.LazyBinaryColumnarSerDe'; + +INSERT INTO TABLE presto_test_types_rcbinary +SELECT * FROM presto_test_types_textfile +; + + +CREATE TABLE presto_test_types_orc LIKE presto_test_types_textfile; +ALTER TABLE presto_test_types_orc SET FILEFORMAT ORC; + +INSERT INTO TABLE presto_test_types_orc +SELECT * FROM presto_test_types_textfile +; + + +-- Parquet is missing TIMESTAMP and BINARY, and for some reason +-- fails when trying to use complex nested types. +CREATE TABLE presto_test_types_parquet ( + t_string STRING +, t_tinyint TINYINT +, t_smallint SMALLINT +, t_int INT +, t_bigint BIGINT +, t_float FLOAT +, t_double DOUBLE +, t_boolean BOOLEAN +, t_map MAP +, t_array_string ARRAY +, t_array_struct ARRAY> +) +STORED AS PARQUET +; + +INSERT INTO TABLE presto_test_types_parquet +SELECT + t_string +, t_tinyint +, t_smallint +, t_int +, t_bigint +, t_float +, t_double +, t_boolean +, t_map +, t_array_string +, t_array_struct +FROM presto_test_types_textfile +; + + +ALTER TABLE presto_test_types_textfile ADD COLUMNS (new_column INT); +ALTER TABLE presto_test_types_sequencefile ADD COLUMNS (new_column INT); +ALTER TABLE presto_test_types_rctext ADD COLUMNS (new_column INT); +ALTER TABLE presto_test_types_rcbinary ADD COLUMNS (new_column INT); +ALTER TABLE presto_test_types_orc ADD COLUMNS (new_column INT); +ALTER TABLE presto_test_types_parquet ADD COLUMNS (new_column INT); diff --git a/presto-hive/src/test/sql/create-test-s3.sql b/presto-hive/src/test/sql/create-test-s3.sql new file mode 100644 index 00000000..93a19845 --- /dev/null +++ b/presto-hive/src/test/sql/create-test-s3.sql @@ -0,0 +1,13 @@ +CREATE EXTERNAL TABLE presto_test_s3 ( + t_bigint BIGINT, + t_string STRING +) +COMMENT 'Presto test S3 table' +ROW FORMAT DELIMITED FIELDS TERMINATED BY ',' +STORED AS TEXTFILE +TBLPROPERTIES ('RETENTION'='-1') +; + +ALTER TABLE presto_test_s3 +SET LOCATION 's3://presto-test-hive/presto_test_s3' +; diff --git a/presto-hive/src/test/sql/create-test.sql b/presto-hive/src/test/sql/create-test.sql new file mode 100644 index 00000000..77f73a63 --- /dev/null +++ b/presto-hive/src/test/sql/create-test.sql @@ -0,0 +1,234 @@ +CREATE TABLE presto_test_sequence ( + n INT +) +COMMENT 'Presto test data' +TBLPROPERTIES ('RETENTION'='-1') +; + +CREATE TABLE presto_test_partition_format ( + t_string STRING, + t_tinyint TINYINT, + t_smallint SMALLINT, + t_int INT, + t_bigint BIGINT, + t_float FLOAT, + t_double DOUBLE, + t_boolean BOOLEAN +) +COMMENT 'Presto test data' +PARTITIONED BY (ds STRING, file_format STRING, dummy INT) +TBLPROPERTIES ('RETENTION'='-1') +; + +CREATE TABLE presto_test_unpartitioned ( + t_string STRING, + t_tinyint TINYINT +) +COMMENT 'Presto test data' +STORED AS TEXTFILE +TBLPROPERTIES ('RETENTION'='-1') +; + +CREATE TABLE presto_test_offline ( + t_string STRING +) +COMMENT 'Presto test data' +PARTITIONED BY (ds STRING) +TBLPROPERTIES ('RETENTION'='-1', 'PROTECT_MODE'='OFFLINE') +; + +CREATE TABLE presto_test_offline_partition ( + t_string STRING +) +COMMENT 'Presto test data' +PARTITIONED BY (ds STRING) +TBLPROPERTIES ('RETENTION'='-1') +; + +CREATE TABLE presto_test_bucketed_by_string_int ( + t_string STRING, + t_tinyint TINYINT, + t_smallint SMALLINT, + t_int INT, + t_bigint BIGINT, + t_float FLOAT, + t_double DOUBLE, + t_boolean BOOLEAN +) +COMMENT 'Presto test bucketed table' +PARTITIONED BY (ds STRING) +CLUSTERED BY (t_string, t_int) INTO 32 BUCKETS +STORED AS RCFILE +TBLPROPERTIES ('RETENTION'='-1') +; + +CREATE TABLE presto_test_bucketed_by_bigint_boolean ( + t_string STRING, + t_tinyint TINYINT, + t_smallint SMALLINT, + t_int INT, + t_bigint BIGINT, + t_float FLOAT, + t_double DOUBLE, + t_boolean BOOLEAN +) +COMMENT 'Presto test bucketed table' +PARTITIONED BY (ds STRING) +CLUSTERED BY (t_bigint, t_boolean) INTO 32 BUCKETS +STORED AS RCFILE +TBLPROPERTIES ('RETENTION'='-1') +; + +CREATE TABLE presto_test_bucketed_by_double_float ( + t_string STRING, + t_tinyint TINYINT, + t_smallint SMALLINT, + t_int INT, + t_bigint BIGINT, + t_float FLOAT, + t_double DOUBLE, + t_boolean BOOLEAN +) +COMMENT 'Presto test bucketed table' +PARTITIONED BY (ds STRING) +CLUSTERED BY (t_double, t_float) INTO 32 BUCKETS +STORED AS RCFILE +TBLPROPERTIES ('RETENTION'='-1') +; + +CREATE TABLE presto_test_partition_schema_change ( + t_data STRING, + t_extra STRING +) +COMMENT 'Presto test partition schema change' +PARTITIONED BY (ds STRING) +STORED AS TEXTFILE +TBLPROPERTIES ('RETENTION'='-1') +; + +CREATE TABLE presto_test_partition_schema_change_non_canonical ( + t_data STRING +) +COMMENT 'Presto test non-canonical boolean partition table' +PARTITIONED BY (t_boolean BOOLEAN) +TBLPROPERTIES ('RETENTION'='-1') +; + +CREATE VIEW presto_test_view +COMMENT 'Presto test view' +TBLPROPERTIES ('RETENTION'='-1') +AS SELECT * FROM presto_test_unpartitioned +; + +DROP TABLE IF EXISTS tmp_presto_test_load; +CREATE TABLE tmp_presto_test_load (word STRING) STORED AS TEXTFILE; +LOAD DATA LOCAL INPATH '/usr/share/dict/words' +INTO TABLE tmp_presto_test_load +; + +INSERT OVERWRITE TABLE presto_test_sequence +SELECT TRANSFORM(word) +USING 'awk "BEGIN { n = 0 } { print ++n }"' AS n +FROM tmp_presto_test_load +LIMIT 100 +; + +DROP TABLE tmp_presto_test_load; + +DROP TABLE IF EXISTS tmp_presto_test; +CREATE TABLE tmp_presto_test ( + t_string STRING, + t_tinyint TINYINT, + t_smallint SMALLINT, + t_int INT, + t_bigint BIGINT, + t_float FLOAT, + t_double DOUBLE, + t_boolean BOOLEAN +) +; +INSERT INTO TABLE tmp_presto_test +SELECT + CASE n % 19 WHEN 0 THEN NULL WHEN 1 THEN '' ELSE 'test' END +, 1 + n +, 2 + n +, 3 + n +, 4 + n + CASE WHEN n % 13 = 0 THEN NULL ELSE 0 END +, 5.1 + n +, 6.2 + n +, CASE n % 3 WHEN 0 THEN false WHEN 1 THEN true ELSE NULL END +FROM presto_test_sequence +LIMIT 100 +; + +ALTER TABLE presto_test_partition_format SET FILEFORMAT TEXTFILE; +ALTER TABLE presto_test_partition_format SET SERDE 'org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe'; +ALTER TABLE presto_test_partition_format ADD PARTITION (ds='2012-12-29', file_format='textfile', dummy=1); +INSERT INTO TABLE presto_test_partition_format PARTITION (ds='2012-12-29', file_format='textfile', dummy=1) +SELECT * FROM tmp_presto_test +; + +ALTER TABLE presto_test_partition_format SET FILEFORMAT SEQUENCEFILE; +ALTER TABLE presto_test_partition_format SET SERDE 'org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe'; +ALTER TABLE presto_test_partition_format ADD PARTITION (ds='2012-12-29', file_format='sequencefile', dummy=2); +INSERT INTO TABLE presto_test_partition_format PARTITION (ds='2012-12-29', file_format='sequencefile', dummy=2) +SELECT * FROM tmp_presto_test +; + +ALTER TABLE presto_test_partition_format SET FILEFORMAT RCFILE; +ALTER TABLE presto_test_partition_format SET SERDE 'org.apache.hadoop.hive.serde2.columnar.ColumnarSerDe'; +ALTER TABLE presto_test_partition_format ADD PARTITION (ds='2012-12-29', file_format='rctext', dummy=3); +INSERT INTO TABLE presto_test_partition_format PARTITION (ds='2012-12-29', file_format='rctext', dummy=3) +SELECT * FROM tmp_presto_test +; + +ALTER TABLE presto_test_partition_format SET FILEFORMAT RCFILE; +ALTER TABLE presto_test_partition_format SET SERDE 'org.apache.hadoop.hive.serde2.columnar.LazyBinaryColumnarSerDe'; +ALTER TABLE presto_test_partition_format ADD PARTITION (ds='2012-12-29', file_format='rcbinary', dummy=4); +INSERT INTO TABLE presto_test_partition_format PARTITION (ds='2012-12-29', file_format='rcbinary', dummy=4) +SELECT * FROM tmp_presto_test +; + +INSERT INTO TABLE presto_test_unpartitioned +SELECT + CASE n % 19 WHEN 0 THEN NULL WHEN 1 THEN '' ELSE 'unpartitioned' END +, 1 + n +FROM presto_test_sequence LIMIT 100; + +INSERT INTO TABLE presto_test_offline_partition PARTITION (ds='2012-12-29') +SELECT 'test' FROM presto_test_sequence LIMIT 100; + +INSERT INTO TABLE presto_test_offline_partition PARTITION (ds='2012-12-30') +SELECT 'test' FROM presto_test_sequence LIMIT 100; + +ALTER TABLE presto_test_offline_partition PARTITION (ds='2012-12-30') ENABLE OFFLINE; + +SET hive.enforce.bucketing = true; + +INSERT OVERWRITE TABLE presto_test_bucketed_by_string_int +PARTITION (ds='2012-12-29') +SELECT t_string, t_tinyint, t_smallint, t_int, t_bigint, t_float, t_double, t_boolean +FROM tmp_presto_test +; + +INSERT OVERWRITE TABLE presto_test_bucketed_by_bigint_boolean +PARTITION (ds='2012-12-29') +SELECT t_string, t_tinyint, t_smallint, t_int, t_bigint, t_float, t_double, t_boolean +FROM tmp_presto_test +; + +INSERT OVERWRITE TABLE presto_test_bucketed_by_double_float +PARTITION (ds='2012-12-29') +SELECT t_string, t_tinyint, t_smallint, t_int, t_bigint, t_float, t_double, t_boolean +FROM tmp_presto_test +; + +DROP TABLE tmp_presto_test; + +ALTER TABLE presto_test_partition_schema_change ADD PARTITION (ds='2012-12-29'); +INSERT OVERWRITE TABLE presto_test_partition_schema_change PARTITION (ds='2012-12-29') +SELECT '123', '456' FROM presto_test_sequence; +ALTER TABLE presto_test_partition_schema_change REPLACE COLUMNS (t_data BIGINT); + +INSERT OVERWRITE TABLE presto_test_partition_schema_change_non_canonical PARTITION (t_boolean='0') +SELECT 'test' FROM presto_test_sequence LIMIT 100; diff --git a/presto-hive/src/test/sql/drop-test-s3.sql b/presto-hive/src/test/sql/drop-test-s3.sql new file mode 100644 index 00000000..b126a4f3 --- /dev/null +++ b/presto-hive/src/test/sql/drop-test-s3.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS presto_test_s3; diff --git a/presto-hive/src/test/sql/drop-test.sql b/presto-hive/src/test/sql/drop-test.sql new file mode 100644 index 00000000..53917f4c --- /dev/null +++ b/presto-hive/src/test/sql/drop-test.sql @@ -0,0 +1,33 @@ +DROP TABLE IF EXISTS presto_test_sequence; + +DROP TABLE IF EXISTS presto_test; + +DROP TABLE IF EXISTS presto_test_partition_format; + +DROP TABLE IF EXISTS presto_test_unpartitioned; + +CREATE TABLE IF NOT EXISTS presto_test_offline (x INT); +ALTER TABLE presto_test_offline DISABLE OFFLINE; +DROP TABLE IF EXISTS presto_test_offline; + +CREATE TABLE IF NOT EXISTS presto_test_offline_partition (x INT) PARTITIONED BY (ds STRING); +ALTER TABLE presto_test_offline_partition ADD IF NOT EXISTS PARTITION (ds='2012-12-30'); +ALTER TABLE presto_test_offline_partition PARTITION (ds='2012-12-30') DISABLE OFFLINE; +DROP TABLE IF EXISTS presto_test_offline_partition; + +DROP TABLE IF EXISTS presto_test_bucketed_by_string_int; +DROP TABLE IF EXISTS presto_test_bucketed_by_bigint_boolean; +DROP TABLE IF EXISTS presto_test_bucketed_by_double_float; + +DROP TABLE IF EXISTS presto_test_partition_schema_change; +DROP TABLE IF EXISTS presto_test_partition_schema_change_non_canonical; + +DROP VIEW IF EXISTS presto_test_view; + +DROP TABLE IF EXISTS presto_test_types_textfile; +DROP TABLE IF EXISTS presto_test_types_sequencefile; +DROP TABLE IF EXISTS presto_test_types_rctext; +DROP TABLE IF EXISTS presto_test_types_rcbinary; +DROP TABLE IF EXISTS presto_test_types_orc; +DROP TABLE IF EXISTS presto_test_types_parquet; +DROP TABLE IF EXISTS presto_test_types_dwrf; diff --git a/presto-jdbc/pom.xml b/presto-jdbc/pom.xml new file mode 100644 index 00000000..616e1b93 --- /dev/null +++ b/presto-jdbc/pom.xml @@ -0,0 +1,252 @@ + + + 4.0.0 + + + com.facebook.presto + presto-root + 0.107 + + + presto-jdbc + presto-jdbc + + + ${project.parent.basedir} + com.facebook.presto.jdbc.internal + + + + + com.facebook.presto + presto-client + + + + com.facebook.presto + presto-spi + + + + io.airlift + http-client + + + + io.airlift + configuration + + + + io.airlift + trace-token + + + + com.google.inject + guice + + + com.google.inject.extensions + guice-multibindings + + + + org.weakref + jmxutils + + + + org.eclipse.jetty + jetty-servlet + + + + + + io.airlift + units + + + + io.airlift + json + + + javax.inject + javax.inject + + + com.google.inject + guice + + + com.google.inject.extensions + guice-multibindings + + + + + + joda-time + joda-time + + + + com.google.guava + guava + + + + com.google.code.findbugs + annotations + + + + + org.eclipse.jetty + jetty-util + + + + + com.facebook.presto + presto-main + test + + + + com.facebook.presto + presto-tpch + test + + + + org.testng + testng + test + + + + io.airlift + testing + test + + + + io.airlift + log-manager + test + + + + + + + org.apache.maven.plugins + maven-shade-plugin + + + package + + shade + + + true + true + ${project.build.directory}/pom.xml + + + com.facebook.presto.client + ${shadeBase}.client + + + com.facebook.presto.spi + ${shadeBase}.spi + + + com.fasterxml.jackson + ${shadeBase}.jackson + + + com.google.common + ${shadeBase}.guava + + + com.google.thirdparty + ${shadeBase}.guava + + + io.airlift + ${shadeBase}.airlift + + + javax.inject + ${shadeBase}.inject + + + org.openjdk.jol + ${shadeBase}.jol + + + org.joda.time + ${shadeBase}.joda.time + + + org.eclipse.jetty + ${shadeBase}.jetty + + + + + *:* + + META-INF/maven/** + META-INF/*.xml + LICENSE + + + + com.fasterxml.jackson.core:* + + META-INF/services/** + + + + com.google.code.findbugs:annotations + + ** + + + + javax.validation:validation-api + + ** + + + + io.airlift + + jetty-logging.properties + + + + org.eclipse.jetty:jetty-util + + jetty-dir.css + + + + org.eclipse.jetty:* + + about.html + + + + + + + + + + diff --git a/presto-jdbc/src/main/java/com/facebook/presto/jdbc/ColumnInfo.java b/presto-jdbc/src/main/java/com/facebook/presto/jdbc/ColumnInfo.java new file mode 100644 index 00000000..83fd452d --- /dev/null +++ b/presto-jdbc/src/main/java/com/facebook/presto/jdbc/ColumnInfo.java @@ -0,0 +1,386 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.jdbc; + +import com.facebook.presto.spi.type.TypeSignature; +import com.google.common.collect.ImmutableList; + +import java.sql.Types; +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; + +class ColumnInfo +{ + private static final int VARCHAR_MAX = 1024 * 1024 * 1024; + private static final int VARBINARY_MAX = 1024 * 1024 * 1024; + private static final int TIME_ZONE_MAX = 40; // current longest time zone is 32 + private static final int TIME_MAX = "HH:mm:ss.SSS".length(); + private static final int TIME_WITH_TIME_ZONE_MAX = TIME_MAX + TIME_ZONE_MAX; + private static final int TIMESTAMP_MAX = "yyyy-MM-dd HH:mm:ss.SSS".length(); + private static final int TIMESTAMP_WITH_TIME_ZONE_MAX = TIMESTAMP_MAX + TIME_ZONE_MAX; + private static final int DATE_MAX = "yyyy-MM-dd".length(); + + private final int columnType; + private final List columnParameterTypes; + private final TypeSignature columnTypeSignature; + private final Nullable nullable; + private final boolean currency; + private final boolean signed; + private final int precision; + private final int scale; + private final int columnDisplaySize; + private final String columnLabel; + private final String columnName; + private final String tableName; + private final String schemaName; + private final String catalogName; + + public enum Nullable + { + NO_NULLS, NULLABLE, UNKNOWN + } + + public ColumnInfo( + int columnType, + List columnParameterTypes, + TypeSignature columnTypeSignature, + Nullable nullable, + boolean currency, + boolean signed, + int precision, + int scale, + int columnDisplaySize, + String columnLabel, + String columnName, + String tableName, + String schemaName, + String catalogName) + { + this.columnType = columnType; + this.columnParameterTypes = ImmutableList.copyOf(checkNotNull(columnParameterTypes, "columnParameterTypes is null")); + this.columnTypeSignature = checkNotNull(columnTypeSignature, "columnTypeName is null"); + this.nullable = checkNotNull(nullable, "nullable is null"); + this.currency = currency; + this.signed = signed; + this.precision = precision; + this.scale = scale; + this.columnDisplaySize = columnDisplaySize; + this.columnLabel = checkNotNull(columnLabel, "columnLabel is null"); + this.columnName = checkNotNull(columnName, "columnName is null"); + this.tableName = checkNotNull(tableName, "tableName is null"); + this.schemaName = checkNotNull(schemaName, "schemaName is null"); + this.catalogName = checkNotNull(catalogName, "catalogName is null"); + } + + public static void setTypeInfo(Builder builder, TypeSignature type) + { + builder.setColumnType(getType(type)); + ImmutableList.Builder parameterTypes = ImmutableList.builder(); + for (TypeSignature parameter : type.getParameters()) { + parameterTypes.add(getType(parameter)); + } + builder.setColumnParameterTypes(parameterTypes.build()); + switch (type.toString()) { + case "boolean": + builder.setColumnDisplaySize(5); + break; + case "bigint": + builder.setSigned(true); + builder.setPrecision(19); + builder.setScale(0); + builder.setColumnDisplaySize(20); + break; + case "double": + builder.setSigned(true); + builder.setPrecision(17); + builder.setScale(0); + builder.setColumnDisplaySize(24); + break; + case "varchar": + builder.setSigned(true); + builder.setPrecision(VARCHAR_MAX); + builder.setScale(0); + builder.setColumnDisplaySize(VARCHAR_MAX); + break; + case "varbinary": + builder.setSigned(true); + builder.setPrecision(VARBINARY_MAX); + builder.setScale(0); + builder.setColumnDisplaySize(VARBINARY_MAX); + break; + case "time": + builder.setSigned(true); + builder.setPrecision(3); + builder.setScale(0); + builder.setColumnDisplaySize(TIME_MAX); + break; + case "time with time zone": + builder.setSigned(true); + builder.setPrecision(3); + builder.setScale(0); + builder.setColumnDisplaySize(TIME_WITH_TIME_ZONE_MAX); + break; + case "timestamp": + builder.setSigned(true); + builder.setPrecision(3); + builder.setScale(0); + builder.setColumnDisplaySize(TIMESTAMP_MAX); + break; + case "timestamp with time zone": + builder.setSigned(true); + builder.setPrecision(3); + builder.setScale(0); + builder.setColumnDisplaySize(TIMESTAMP_WITH_TIME_ZONE_MAX); + break; + case "date": + builder.setSigned(true); + builder.setScale(0); + builder.setColumnDisplaySize(DATE_MAX); + break; + case "interval year to month": + builder.setColumnDisplaySize(TIMESTAMP_MAX); + break; + case "interval day to second": + builder.setColumnDisplaySize(TIMESTAMP_MAX); + break; + } + } + + private static int getType(TypeSignature type) + { + if (type.getBase().equals("array")) { + return Types.ARRAY; + } + switch (type.toString()) { + case "boolean": + return Types.BOOLEAN; + case "bigint": + return Types.BIGINT; + case "double": + return Types.DOUBLE; + case "varchar": + return Types.LONGNVARCHAR; + case "varbinary": + return Types.LONGVARBINARY; + case "time": + return Types.TIME; + case "time with time zone": + return Types.TIME; + case "timestamp": + return Types.TIMESTAMP; + case "timestamp with time zone": + return Types.TIMESTAMP; + case "date": + return Types.DATE; + default: + return Types.JAVA_OBJECT; + } + } + + public int getColumnType() + { + return columnType; + } + + public List getColumnParameterTypes() + { + return columnParameterTypes; + } + + public String getColumnTypeName() + { + return columnTypeSignature.toString(); + } + + public TypeSignature getColumnTypeSignature() + { + return columnTypeSignature; + } + + public Nullable getNullable() + { + return nullable; + } + + public boolean isCurrency() + { + return currency; + } + + public boolean isSigned() + { + return signed; + } + + public int getPrecision() + { + return precision; + } + + public int getScale() + { + return scale; + } + + public int getColumnDisplaySize() + { + return columnDisplaySize; + } + + public String getColumnLabel() + { + return columnLabel; + } + + public String getColumnName() + { + return columnName; + } + + public String getTableName() + { + return tableName; + } + + public String getSchemaName() + { + return schemaName; + } + + public String getCatalogName() + { + return catalogName; + } + + static class Builder + { + private int columnType; + private List columnParameterTypes; + private TypeSignature columnTypeSignature; + private Nullable nullable; + private boolean currency; + private boolean signed; + private int precision; + private int scale; + private int columnDisplaySize; + private String columnLabel; + private String columnName; + private String tableName; + private String schemaName; + private String catalogName; + + public Builder setColumnType(int columnType) + { + this.columnType = columnType; + return this; + } + + public void setColumnParameterTypes(List columnParameterTypes) + { + this.columnParameterTypes = ImmutableList.copyOf(checkNotNull(columnParameterTypes, "columnParameterTypes is null")); + } + + public Builder setColumnTypeSignature(TypeSignature columnTypeSignature) + { + this.columnTypeSignature = columnTypeSignature; + return this; + } + + public Builder setNullable(Nullable nullable) + { + this.nullable = nullable; + return this; + } + + public Builder setCurrency(boolean currency) + { + this.currency = currency; + return this; + } + + public Builder setSigned(boolean signed) + { + this.signed = signed; + return this; + } + + public Builder setPrecision(int precision) + { + this.precision = precision; + return this; + } + + public Builder setScale(int scale) + { + this.scale = scale; + return this; + } + + public Builder setColumnDisplaySize(int columnDisplaySize) + { + this.columnDisplaySize = columnDisplaySize; + return this; + } + + public Builder setColumnLabel(String columnLabel) + { + this.columnLabel = columnLabel; + return this; + } + + public Builder setColumnName(String columnName) + { + this.columnName = columnName; + return this; + } + + public Builder setTableName(String tableName) + { + this.tableName = tableName; + return this; + } + + public Builder setSchemaName(String schemaName) + { + this.schemaName = schemaName; + return this; + } + + public Builder setCatalogName(String catalogName) + { + this.catalogName = catalogName; + return this; + } + + public ColumnInfo build() + { + return new ColumnInfo( + columnType, + columnParameterTypes, + columnTypeSignature, + nullable, + currency, + signed, + precision, + scale, + columnDisplaySize, + columnLabel, + columnName, + tableName, + schemaName, + catalogName); + } + } +} diff --git a/presto-jdbc/src/main/java/com/facebook/presto/jdbc/JettyLogging.java b/presto-jdbc/src/main/java/com/facebook/presto/jdbc/JettyLogging.java new file mode 100644 index 00000000..b796465d --- /dev/null +++ b/presto-jdbc/src/main/java/com/facebook/presto/jdbc/JettyLogging.java @@ -0,0 +1,32 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.jdbc; + +import org.eclipse.jetty.util.log.JavaUtilLog; +import org.eclipse.jetty.util.log.Log; + +// TODO: fix this in Airlift +final class JettyLogging +{ + private JettyLogging() {} + + /** + * Force Jetty to use java.util.logging instead of SLF4J + */ + public static void useJavaUtilLogging() + { + Log.__logClass = JavaUtilLog.class.getName(); + Log.initialized(); + } +} diff --git a/presto-jdbc/src/main/java/com/facebook/presto/jdbc/NotImplementedException.java b/presto-jdbc/src/main/java/com/facebook/presto/jdbc/NotImplementedException.java new file mode 100644 index 00000000..eca77d9d --- /dev/null +++ b/presto-jdbc/src/main/java/com/facebook/presto/jdbc/NotImplementedException.java @@ -0,0 +1,35 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.jdbc; + +import java.sql.SQLNonTransientException; + +import static java.lang.String.format; + +/** + * Thrown when a required JDBC method is not yet implemented. + */ +public class NotImplementedException + extends SQLNonTransientException +{ + public NotImplementedException(String reason) + { + super(reason); + } + + public NotImplementedException(String clazz, String method) + { + this(format("Method %s.%s is not yet implemented", clazz, method)); + } +} diff --git a/presto-jdbc/src/main/java/com/facebook/presto/jdbc/PrestoArray.java b/presto-jdbc/src/main/java/com/facebook/presto/jdbc/PrestoArray.java new file mode 100644 index 00000000..81b01b77 --- /dev/null +++ b/presto-jdbc/src/main/java/com/facebook/presto/jdbc/PrestoArray.java @@ -0,0 +1,118 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.jdbc; + +import com.google.common.primitives.Ints; + +import java.sql.Array; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class PrestoArray + implements Array +{ + private final String elementTypeName; + private final int elementType; + private final Object[] array; + + PrestoArray(String elementTypeName, int elementType, List array) + { + this.elementTypeName = checkNotNull(elementTypeName, "elementType is null"); + this.elementType = elementType; + this.array = checkNotNull(array, "array is null").toArray(); + } + + @Override + public String getBaseTypeName() + { + return elementTypeName; + } + + @Override + public int getBaseType() + { + return elementType; + } + + @Override + public Object getArray() + { + return array.clone(); + } + + @Override + public Object getArray(Map> map) + throws SQLException + { + throw new SQLFeatureNotSupportedException("getArray not supported"); + } + + @Override + public Object getArray(long index, int count) + throws SQLException + { + int arrayOffset = Ints.saturatedCast(index - 1); + if (index < 1 || count < 0 || (arrayOffset + count) > array.length) { + throw new SQLException("Index out of bounds"); + } + return Arrays.copyOfRange(array, arrayOffset, arrayOffset + count); + } + + @Override + public Object getArray(long index, int count, Map> map) + throws SQLException + { + throw new SQLFeatureNotSupportedException("getArray not supported"); + } + + @Override + public ResultSet getResultSet() + throws SQLException + { + throw new SQLFeatureNotSupportedException("getResultSet not supported"); + } + + @Override + public ResultSet getResultSet(Map> map) + throws SQLException + { + throw new SQLFeatureNotSupportedException("getResultSet not supported"); + } + + @Override + public ResultSet getResultSet(long index, int count) + throws SQLException + { + throw new SQLFeatureNotSupportedException("getResultSet not supported"); + } + + @Override + public ResultSet getResultSet(long index, int count, Map> map) + throws SQLException + { + throw new SQLFeatureNotSupportedException("getResultSet not supported"); + } + + @Override + public void free() + { + // no-op + } +} diff --git a/presto-jdbc/src/main/java/com/facebook/presto/jdbc/PrestoConnection.java b/presto-jdbc/src/main/java/com/facebook/presto/jdbc/PrestoConnection.java new file mode 100644 index 00000000..5c4713be --- /dev/null +++ b/presto-jdbc/src/main/java/com/facebook/presto/jdbc/PrestoConnection.java @@ -0,0 +1,666 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.jdbc; + +import com.facebook.presto.client.ClientSession; +import com.facebook.presto.client.StatementClient; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableMap; +import com.google.common.net.HostAndPort; + +import java.net.URI; +import java.nio.charset.CharsetEncoder; +import java.sql.Array; +import java.sql.Blob; +import java.sql.CallableStatement; +import java.sql.Clob; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.NClob; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLClientInfoException; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.sql.SQLWarning; +import java.sql.SQLXML; +import java.sql.Savepoint; +import java.sql.Statement; +import java.sql.Struct; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.TimeZone; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import static com.google.common.base.MoreObjects.firstNonNull; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Strings.isNullOrEmpty; +import static com.google.common.collect.Maps.fromProperties; +import static io.airlift.http.client.HttpUriBuilder.uriBuilder; +import static java.nio.charset.StandardCharsets.US_ASCII; + +public class PrestoConnection + implements Connection +{ + private final AtomicBoolean closed = new AtomicBoolean(); + private final AtomicReference catalog = new AtomicReference<>(); + private final AtomicReference schema = new AtomicReference<>(); + private final AtomicReference timeZoneId = new AtomicReference<>(); + private final AtomicReference locale = new AtomicReference<>(); + private final URI uri; + private final HostAndPort address; + private final String user; + private final Map clientInfo = new ConcurrentHashMap<>(); + private final Map sessionProperties = new ConcurrentHashMap<>(); + private final QueryExecutor queryExecutor; + + PrestoConnection(URI uri, String user, QueryExecutor queryExecutor) + throws SQLException + { + this.uri = checkNotNull(uri, "uri is null"); + this.address = HostAndPort.fromParts(uri.getHost(), uri.getPort()); + this.user = checkNotNull(user, "user is null"); + this.queryExecutor = checkNotNull(queryExecutor, "queryExecutor is null"); + catalog.set("default"); + schema.set("default"); + timeZoneId.set(TimeZone.getDefault().getID()); + locale.set(Locale.getDefault()); + + if (!isNullOrEmpty(uri.getPath())) { + setCatalogAndSchema(); + } + } + + @Override + public Statement createStatement() + throws SQLException + { + checkOpen(); + return new PrestoStatement(this); + } + + @Override + public PreparedStatement prepareStatement(String sql) + throws SQLException + { + checkOpen(); + throw new NotImplementedException("Connection", "prepareStatement"); + } + + @Override + public CallableStatement prepareCall(String sql) + throws SQLException + { + throw new NotImplementedException("Connection", "prepareCall"); + } + + @Override + public String nativeSQL(String sql) + throws SQLException + { + checkOpen(); + return sql; + } + + @Override + public void setAutoCommit(boolean autoCommit) + throws SQLException + { + checkOpen(); + if (!autoCommit) { + throw new SQLFeatureNotSupportedException("Disabling auto-commit mode not supported"); + } + } + + @Override + public boolean getAutoCommit() + throws SQLException + { + checkOpen(); + return true; + } + + @Override + public void commit() + throws SQLException + { + checkOpen(); + if (getAutoCommit()) { + throw new SQLException("Connection is in auto-commit mode"); + } + throw new NotImplementedException("Connection", "commit"); + } + + @Override + public void rollback() + throws SQLException + { + checkOpen(); + if (getAutoCommit()) { + throw new SQLException("Connection is in auto-commit mode"); + } + throw new NotImplementedException("Connection", "rollback"); + } + + @Override + public void close() + throws SQLException + { + closed.set(true); + } + + @Override + public boolean isClosed() + throws SQLException + { + return closed.get(); + } + + @Override + public DatabaseMetaData getMetaData() + throws SQLException + { + return new PrestoDatabaseMetaData(this); + } + + @Override + public void setReadOnly(boolean readOnly) + throws SQLException + { + checkOpen(); + if (!readOnly) { + throw new SQLFeatureNotSupportedException("Disabling read-only mode not supported"); + } + } + + @Override + public boolean isReadOnly() + throws SQLException + { + checkOpen(); + return true; + } + + @Override + public void setCatalog(String catalog) + throws SQLException + { + checkOpen(); + this.catalog.set(catalog); + } + + @Override + public String getCatalog() + throws SQLException + { + checkOpen(); + return catalog.get(); + } + + @Override + public void setTransactionIsolation(int level) + throws SQLException + { + checkOpen(); + throw new SQLFeatureNotSupportedException("Transactions are not yet supported"); + } + + @Override + public int getTransactionIsolation() + throws SQLException + { + checkOpen(); + return TRANSACTION_NONE; + } + + @Override + public SQLWarning getWarnings() + throws SQLException + { + checkOpen(); + return null; + } + + @Override + public void clearWarnings() + throws SQLException + { + checkOpen(); + } + + @Override + public Statement createStatement(int resultSetType, int resultSetConcurrency) + throws SQLException + { + checkResultSet(resultSetType, resultSetConcurrency); + return createStatement(); + } + + @Override + public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) + throws SQLException + { + checkResultSet(resultSetType, resultSetConcurrency); + return prepareStatement(sql); + } + + @Override + public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) + throws SQLException + { + checkResultSet(resultSetType, resultSetConcurrency); + throw new SQLFeatureNotSupportedException("prepareCall"); + } + + @Override + public Map> getTypeMap() + throws SQLException + { + throw new SQLFeatureNotSupportedException("getTypeMap"); + } + + @Override + public void setTypeMap(Map> map) + throws SQLException + { + throw new SQLFeatureNotSupportedException("setTypeMap"); + } + + @Override + public void setHoldability(int holdability) + throws SQLException + { + checkOpen(); + if (holdability != ResultSet.HOLD_CURSORS_OVER_COMMIT) { + throw new SQLFeatureNotSupportedException("Changing holdability not supported"); + } + } + + @Override + public int getHoldability() + throws SQLException + { + checkOpen(); + return ResultSet.HOLD_CURSORS_OVER_COMMIT; + } + + @Override + public Savepoint setSavepoint() + throws SQLException + { + throw new SQLFeatureNotSupportedException("setSavepoint"); + } + + @Override + public Savepoint setSavepoint(String name) + throws SQLException + { + throw new SQLFeatureNotSupportedException("setSavepoint"); + } + + @Override + public void rollback(Savepoint savepoint) + throws SQLException + { + throw new SQLFeatureNotSupportedException("rollback"); + } + + @Override + public void releaseSavepoint(Savepoint savepoint) + throws SQLException + { + throw new SQLFeatureNotSupportedException("releaseSavepoint"); + } + + @Override + public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) + throws SQLException + { + checkHoldability(resultSetHoldability); + return createStatement(resultSetType, resultSetConcurrency); + } + + @Override + public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) + throws SQLException + { + checkHoldability(resultSetHoldability); + return prepareStatement(sql, resultSetType, resultSetConcurrency); + } + + @Override + public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) + throws SQLException + { + checkHoldability(resultSetHoldability); + return prepareCall(sql, resultSetType, resultSetConcurrency); + } + + @Override + public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) + throws SQLException + { + if (autoGeneratedKeys != Statement.RETURN_GENERATED_KEYS) { + throw new SQLFeatureNotSupportedException("Auto generated keys must be NO_GENERATED_KEYS"); + } + return prepareStatement(sql); + } + + @Override + public PreparedStatement prepareStatement(String sql, int[] columnIndexes) + throws SQLException + { + throw new SQLFeatureNotSupportedException("prepareStatement"); + } + + @Override + public PreparedStatement prepareStatement(String sql, String[] columnNames) + throws SQLException + { + throw new SQLFeatureNotSupportedException("prepareStatement"); + } + + @Override + public Clob createClob() + throws SQLException + { + throw new SQLFeatureNotSupportedException("createClob"); + } + + @Override + public Blob createBlob() + throws SQLException + { + throw new SQLFeatureNotSupportedException("createBlob"); + } + + @Override + public NClob createNClob() + throws SQLException + { + throw new SQLFeatureNotSupportedException("createNClob"); + } + + @Override + public SQLXML createSQLXML() + throws SQLException + { + throw new SQLFeatureNotSupportedException("createSQLXML"); + } + + @Override + public boolean isValid(int timeout) + throws SQLException + { + if (timeout < 0) { + throw new SQLException("Timeout is negative"); + } + return !isClosed(); + } + + @Override + public void setClientInfo(String name, String value) + throws SQLClientInfoException + { + checkNotNull(name, "name is null"); + if (value != null) { + clientInfo.put(name, value); + } + else { + clientInfo.remove(name); + } + } + + @Override + public void setClientInfo(Properties properties) + throws SQLClientInfoException + { + clientInfo.putAll(fromProperties(properties)); + } + + @Override + public String getClientInfo(String name) + throws SQLException + { + return clientInfo.get(name); + } + + @Override + public Properties getClientInfo() + throws SQLException + { + Properties properties = new Properties(); + for (Map.Entry entry : clientInfo.entrySet()) { + properties.setProperty(entry.getKey(), entry.getValue()); + } + return properties; + } + + @Override + public Array createArrayOf(String typeName, Object[] elements) + throws SQLException + { + throw new SQLFeatureNotSupportedException("createArrayOf"); + } + + @Override + public Struct createStruct(String typeName, Object[] attributes) + throws SQLException + { + throw new SQLFeatureNotSupportedException("createStruct"); + } + + @Override + public void setSchema(String schema) + throws SQLException + { + checkOpen(); + this.schema.set(schema); + } + + @Override + public String getSchema() + throws SQLException + { + checkOpen(); + return schema.get(); + } + + public String getTimeZoneId() + { + return timeZoneId.get(); + } + + public void setTimeZoneId(String timeZoneId) + { + checkNotNull(timeZoneId, "timeZoneId is null"); + this.timeZoneId.set(timeZoneId); + } + + public Locale getLocale() + { + return locale.get(); + } + + public void setLocale(Locale locale) + { + this.locale.set(locale); + } + + /** + * Adds a session property (experimental). + */ + public void setSessionProperty(String name, String value) + { + checkNotNull(name, "name is null"); + checkNotNull(value, "value is null"); + checkArgument(!name.isEmpty(), "name is empty"); + + CharsetEncoder charsetEncoder = US_ASCII.newEncoder(); + checkArgument(name.indexOf('=') < 0, "Session property name must not contain '=': %s", name); + checkArgument(charsetEncoder.canEncode(name), "Session property name is not US_ASCII: %s", name); + checkArgument(charsetEncoder.canEncode(value), "Session property value is not US_ASCII: %s", value); + + sessionProperties.put(name, value); + } + + @Override + public void abort(Executor executor) + throws SQLException + { + close(); + } + + @Override + public void setNetworkTimeout(Executor executor, int milliseconds) + throws SQLException + { + throw new SQLFeatureNotSupportedException("setNetworkTimeout"); + } + + @Override + public int getNetworkTimeout() + throws SQLException + { + throw new SQLFeatureNotSupportedException("getNetworkTimeout"); + } + + @SuppressWarnings("unchecked") + @Override + public T unwrap(Class iface) + throws SQLException + { + if (isWrapperFor(iface)) { + return (T) this; + } + throw new SQLException("No wrapper for " + iface); + } + + @Override + public boolean isWrapperFor(Class iface) + throws SQLException + { + return iface.isInstance(this); + } + + URI getURI() + { + return uri; + } + + String getUser() + { + return user; + } + + StatementClient startQuery(String sql) + { + URI uri = createHttpUri(address); + + String source = firstNonNull(clientInfo.get("ApplicationName"), "presto-jdbc"); + + ClientSession session = new ClientSession( + uri, + user, + source, + catalog.get(), + schema.get(), + timeZoneId.get(), + locale.get(), + ImmutableMap.copyOf(sessionProperties), + false); + + return queryExecutor.startQuery(session, sql); + } + + private void checkOpen() + throws SQLException + { + if (isClosed()) { + throw new SQLException("Connection is closed"); + } + } + + private void setCatalogAndSchema() + throws SQLException + { + String path = uri.getPath(); + if (path.equals("/")) { + return; + } + + // remove first slash + if (!path.startsWith("/")) { + throw new SQLException("Path does not start with a slash: " + uri); + } + path = path.substring(1); + + List parts = Splitter.on("/").splitToList(path); + + // remove last item due to a trailing slash + if (parts.get(parts.size() - 1).isEmpty()) { + parts = parts.subList(0, parts.size() - 1); + } + + if (parts.size() > 2) { + throw new SQLException("Invalid path segments in URL: " + uri); + } + + if (parts.get(0).isEmpty()) { + throw new SQLException("Catalog name is empty: " + uri); + } + catalog.set(parts.get(0)); + + if (parts.size() > 1) { + if (parts.get(1).isEmpty()) { + throw new SQLException("Schema name is empty: " + uri); + } + schema.set(parts.get(1)); + } + } + + private static URI createHttpUri(HostAndPort address) + { + return uriBuilder() + .scheme("http") + .host(address.getHostText()) + .port(address.getPort()) + .build(); + } + + private static void checkResultSet(int resultSetType, int resultSetConcurrency) + throws SQLFeatureNotSupportedException + { + if (resultSetType != ResultSet.TYPE_FORWARD_ONLY) { + throw new SQLFeatureNotSupportedException("Result set type must be TYPE_FORWARD_ONLY"); + } + if (resultSetConcurrency != ResultSet.CONCUR_READ_ONLY) { + throw new SQLFeatureNotSupportedException("Result set concurrency must be CONCUR_READ_ONLY"); + } + } + + private static void checkHoldability(int resultSetHoldability) + throws SQLFeatureNotSupportedException + { + if (resultSetHoldability != ResultSet.HOLD_CURSORS_OVER_COMMIT) { + throw new SQLFeatureNotSupportedException("Result set holdability must be HOLD_CURSORS_OVER_COMMIT"); + } + } +} diff --git a/presto-jdbc/src/main/java/com/facebook/presto/jdbc/PrestoDatabaseMetaData.java b/presto-jdbc/src/main/java/com/facebook/presto/jdbc/PrestoDatabaseMetaData.java new file mode 100644 index 00000000..73662aa1 --- /dev/null +++ b/presto-jdbc/src/main/java/com/facebook/presto/jdbc/PrestoDatabaseMetaData.java @@ -0,0 +1,1528 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.jdbc; + +import com.google.common.base.Joiner; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.RowIdLifetime; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.sql.Statement; +import java.sql.Types; +import java.util.ArrayList; +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class PrestoDatabaseMetaData + implements DatabaseMetaData +{ + private final PrestoConnection connection; + + PrestoDatabaseMetaData(PrestoConnection connection) + { + this.connection = checkNotNull(connection, "connection is null"); + } + + @Override + public boolean allProceduresAreCallable() + throws SQLException + { + return true; + } + + @Override + public boolean allTablesAreSelectable() + throws SQLException + { + return true; + } + + @Override + public String getURL() + throws SQLException + { + return connection.getURI().toString(); + } + + @Override + public String getUserName() + throws SQLException + { + return connection.getUser(); + } + + @Override + public boolean isReadOnly() + throws SQLException + { + return connection.isReadOnly(); + } + + @Override + public boolean nullsAreSortedHigh() + throws SQLException + { + // TODO: determine null sort order + throw new NotImplementedException("DatabaseMetaData", "nullsAreSortedHigh"); + } + + @Override + public boolean nullsAreSortedLow() + throws SQLException + { + // TODO: determine null sort order + throw new NotImplementedException("DatabaseMetaData", "nullsAreSortedLow"); + } + + @Override + public boolean nullsAreSortedAtStart() + throws SQLException + { + // TODO: determine null sort order + throw new NotImplementedException("DatabaseMetaData", "nullsAreSortedAtStart"); + } + + @Override + public boolean nullsAreSortedAtEnd() + throws SQLException + { + // TODO: determine null sort order + throw new NotImplementedException("DatabaseMetaData", "nullsAreSortedAtEnd"); + } + + @Override + public String getDatabaseProductName() + throws SQLException + { + return "Presto"; + } + + @Override + public String getDatabaseProductVersion() + throws SQLException + { + // TODO: get version from server + return "UNKNOWN"; + } + + @Override + public String getDriverName() + throws SQLException + { + return PrestoDriver.DRIVER_NAME; + } + + @Override + public String getDriverVersion() + throws SQLException + { + return PrestoDriver.DRIVER_VERSION; + } + + @Override + public int getDriverMajorVersion() + { + return PrestoDriver.VERSION_MAJOR; + } + + @Override + public int getDriverMinorVersion() + { + return PrestoDriver.VERSION_MINOR; + } + + @Override + public boolean usesLocalFiles() + throws SQLException + { + return false; + } + + @Override + public boolean usesLocalFilePerTable() + throws SQLException + { + return false; + } + + @Override + public boolean supportsMixedCaseIdentifiers() + throws SQLException + { + return false; + } + + @Override + public boolean storesUpperCaseIdentifiers() + throws SQLException + { + return false; + } + + @Override + public boolean storesLowerCaseIdentifiers() + throws SQLException + { + return true; + } + + @Override + public boolean storesMixedCaseIdentifiers() + throws SQLException + { + return false; + } + + @Override + public boolean supportsMixedCaseQuotedIdentifiers() + throws SQLException + { + return true; + } + + @Override + public boolean storesUpperCaseQuotedIdentifiers() + throws SQLException + { + return false; + } + + @Override + public boolean storesLowerCaseQuotedIdentifiers() + throws SQLException + { + return false; + } + + @Override + public boolean storesMixedCaseQuotedIdentifiers() + throws SQLException + { + return true; + } + + @Override + public String getIdentifierQuoteString() + throws SQLException + { + return "\""; + } + + @Override + public String getSQLKeywords() + throws SQLException + { + return "LIMIT"; + } + + @Override + public String getNumericFunctions() + throws SQLException + { + return ""; + } + + @Override + public String getStringFunctions() + throws SQLException + { + return ""; + } + + @Override + public String getSystemFunctions() + throws SQLException + { + return ""; + } + + @Override + public String getTimeDateFunctions() + throws SQLException + { + return ""; + } + + @Override + public String getSearchStringEscape() + throws SQLException + { + return "\\"; + } + + @Override + public String getExtraNameCharacters() + throws SQLException + { + return ""; + } + + @Override + public boolean supportsAlterTableWithAddColumn() + throws SQLException + { + return false; + } + + @Override + public boolean supportsAlterTableWithDropColumn() + throws SQLException + { + return false; + } + + @Override + public boolean supportsColumnAliasing() + throws SQLException + { + return true; + } + + @Override + public boolean nullPlusNonNullIsNull() + throws SQLException + { + return true; + } + + @Override + public boolean supportsConvert() + throws SQLException + { + // TODO: support convert + return false; + } + + @Override + public boolean supportsConvert(int fromType, int toType) + throws SQLException + { + // TODO: support convert + return false; + } + + @Override + public boolean supportsTableCorrelationNames() + throws SQLException + { + return true; + } + + @Override + public boolean supportsDifferentTableCorrelationNames() + throws SQLException + { + // TODO: verify this + return false; + } + + @Override + public boolean supportsExpressionsInOrderBy() + throws SQLException + { + return true; + } + + @Override + public boolean supportsOrderByUnrelated() + throws SQLException + { + // TODO: verify this + return true; + } + + @Override + public boolean supportsGroupBy() + throws SQLException + { + return true; + } + + @Override + public boolean supportsGroupByUnrelated() + throws SQLException + { + return true; + } + + @Override + public boolean supportsGroupByBeyondSelect() + throws SQLException + { + return true; + } + + @Override + public boolean supportsLikeEscapeClause() + throws SQLException + { + return true; + } + + @Override + public boolean supportsMultipleResultSets() + throws SQLException + { + return false; + } + + @Override + public boolean supportsMultipleTransactions() + throws SQLException + { + return true; + } + + @Override + public boolean supportsNonNullableColumns() + throws SQLException + { + return true; + } + + @Override + public boolean supportsMinimumSQLGrammar() + throws SQLException + { + return true; + } + + @Override + public boolean supportsCoreSQLGrammar() + throws SQLException + { + // TODO: support this + return false; + } + + @Override + public boolean supportsExtendedSQLGrammar() + throws SQLException + { + // TODO: support this + return false; + } + + @Override + public boolean supportsANSI92EntryLevelSQL() + throws SQLException + { + // TODO: verify this + return true; + } + + @Override + public boolean supportsANSI92IntermediateSQL() + throws SQLException + { + // TODO: support this + return false; + } + + @Override + public boolean supportsANSI92FullSQL() + throws SQLException + { + // TODO: support this + return false; + } + + @Override + public boolean supportsIntegrityEnhancementFacility() + throws SQLException + { + return false; + } + + @Override + public boolean supportsOuterJoins() + throws SQLException + { + return true; + } + + @Override + public boolean supportsFullOuterJoins() + throws SQLException + { + // TODO: support full outer joins + return false; + } + + @Override + public boolean supportsLimitedOuterJoins() + throws SQLException + { + return true; + } + + @Override + public String getSchemaTerm() + throws SQLException + { + return "schema"; + } + + @Override + public String getProcedureTerm() + throws SQLException + { + return "procedure"; + } + + @Override + public String getCatalogTerm() + throws SQLException + { + return "catalog"; + } + + @Override + public boolean isCatalogAtStart() + throws SQLException + { + return true; + } + + @Override + public String getCatalogSeparator() + throws SQLException + { + return "."; + } + + @Override + public boolean supportsSchemasInDataManipulation() + throws SQLException + { + return true; + } + + @Override + public boolean supportsSchemasInProcedureCalls() + throws SQLException + { + return true; + } + + @Override + public boolean supportsSchemasInTableDefinitions() + throws SQLException + { + return true; + } + + @Override + public boolean supportsSchemasInIndexDefinitions() + throws SQLException + { + return true; + } + + @Override + public boolean supportsSchemasInPrivilegeDefinitions() + throws SQLException + { + return true; + } + + @Override + public boolean supportsCatalogsInDataManipulation() + throws SQLException + { + return true; + } + + @Override + public boolean supportsCatalogsInProcedureCalls() + throws SQLException + { + return true; + } + + @Override + public boolean supportsCatalogsInTableDefinitions() + throws SQLException + { + return true; + } + + @Override + public boolean supportsCatalogsInIndexDefinitions() + throws SQLException + { + return true; + } + + @Override + public boolean supportsCatalogsInPrivilegeDefinitions() + throws SQLException + { + return true; + } + + @Override + public boolean supportsPositionedDelete() + throws SQLException + { + return false; + } + + @Override + public boolean supportsPositionedUpdate() + throws SQLException + { + return false; + } + + @Override + public boolean supportsSelectForUpdate() + throws SQLException + { + return false; + } + + @Override + public boolean supportsStoredProcedures() + throws SQLException + { + // TODO: support stored procedures + return false; + } + + @Override + public boolean supportsSubqueriesInComparisons() + throws SQLException + { + // TODO: support subqueries in comparisons + return false; + } + + @Override + public boolean supportsSubqueriesInExists() + throws SQLException + { + // TODO: support EXISTS + return false; + } + + @Override + public boolean supportsSubqueriesInIns() + throws SQLException + { + // TODO: support subqueries in IN clauses + return false; + } + + @Override + public boolean supportsSubqueriesInQuantifieds() + throws SQLException + { + // TODO: support subqueries in ANY/SOME/ALL predicates + return false; + } + + @Override + public boolean supportsCorrelatedSubqueries() + throws SQLException + { + // TODO: support correlated subqueries + return false; + } + + @Override + public boolean supportsUnion() + throws SQLException + { + // TODO: support UNION + return false; + } + + @Override + public boolean supportsUnionAll() + throws SQLException + { + // TODO: support UNION ALL + return false; + } + + @Override + public boolean supportsOpenCursorsAcrossCommit() + throws SQLException + { + return true; + } + + @Override + public boolean supportsOpenCursorsAcrossRollback() + throws SQLException + { + return false; + } + + @Override + public boolean supportsOpenStatementsAcrossCommit() + throws SQLException + { + return true; + } + + @Override + public boolean supportsOpenStatementsAcrossRollback() + throws SQLException + { + return true; + } + + @Override + public int getMaxBinaryLiteralLength() + throws SQLException + { + return 0; + } + + @Override + public int getMaxCharLiteralLength() + throws SQLException + { + return 0; + } + + @Override + public int getMaxColumnNameLength() + throws SQLException + { + // TODO: define max identifier length + return 0; + } + + @Override + public int getMaxColumnsInGroupBy() + throws SQLException + { + return 0; + } + + @Override + public int getMaxColumnsInIndex() + throws SQLException + { + return 0; + } + + @Override + public int getMaxColumnsInOrderBy() + throws SQLException + { + return 0; + } + + @Override + public int getMaxColumnsInSelect() + throws SQLException + { + return 0; + } + + @Override + public int getMaxColumnsInTable() + throws SQLException + { + return 0; + } + + @Override + public int getMaxConnections() + throws SQLException + { + return 0; + } + + @Override + public int getMaxCursorNameLength() + throws SQLException + { + return 0; + } + + @Override + public int getMaxIndexLength() + throws SQLException + { + return 0; + } + + @Override + public int getMaxSchemaNameLength() + throws SQLException + { + // TODO: define max identifier length + return 0; + } + + @Override + public int getMaxProcedureNameLength() + throws SQLException + { + // TODO: define max identifier length + return 0; + } + + @Override + public int getMaxCatalogNameLength() + throws SQLException + { + // TODO: define max identifier length + return 0; + } + + @Override + public int getMaxRowSize() + throws SQLException + { + return 0; + } + + @Override + public boolean doesMaxRowSizeIncludeBlobs() + throws SQLException + { + return true; + } + + @Override + public int getMaxStatementLength() + throws SQLException + { + return 0; + } + + @Override + public int getMaxStatements() + throws SQLException + { + return 0; + } + + @Override + public int getMaxTableNameLength() + throws SQLException + { + // TODO: define max identifier length + return 0; + } + + @Override + public int getMaxTablesInSelect() + throws SQLException + { + return 0; + } + + @Override + public int getMaxUserNameLength() + throws SQLException + { + // TODO: define max identifier length + return 0; + } + + @Override + public int getDefaultTransactionIsolation() + throws SQLException + { + // TODO: support transactions + return Connection.TRANSACTION_NONE; + } + + @Override + public boolean supportsTransactions() + throws SQLException + { + // TODO: support transactions + return false; + } + + @Override + public boolean supportsTransactionIsolationLevel(int level) + throws SQLException + { + return level == Connection.TRANSACTION_NONE; + } + + @Override + public boolean supportsDataDefinitionAndDataManipulationTransactions() + throws SQLException + { + return true; + } + + @Override + public boolean supportsDataManipulationTransactionsOnly() + throws SQLException + { + return false; + } + + @Override + public boolean dataDefinitionCausesTransactionCommit() + throws SQLException + { + return false; + } + + @Override + public boolean dataDefinitionIgnoredInTransactions() + throws SQLException + { + return false; + } + + @Override + public ResultSet getProcedures(String catalog, String schemaPattern, String procedureNamePattern) + throws SQLException + { + // TODO: support stored procedures + throw new SQLFeatureNotSupportedException("stored procedures not supported"); + } + + @Override + public ResultSet getProcedureColumns(String catalog, String schemaPattern, String procedureNamePattern, String columnNamePattern) + throws SQLException + { + // TODO: support stored procedures + throw new SQLFeatureNotSupportedException("stored procedures not supported"); + } + + @Override + public ResultSet getTables(String catalog, String schemaPattern, String tableNamePattern, String[] types) + throws SQLException + { + StringBuilder query = new StringBuilder(1024); + query.append("SELECT"); + query.append(" table_catalog AS TABLE_CAT"); + query.append(", table_schema AS TABLE_SCHEM"); + query.append(", table_name AS TABLE_NAME"); + query.append(", table_type AS TABLE_TYPE"); + query.append(", '' AS REMARKS"); + query.append(", '' AS TYPE_CAT"); + query.append(", '' AS TYPE_SCHEM"); + query.append(", '' AS TYPE_NAME"); + query.append(", '' AS SELF_REFERENCING_COL_NAME"); + query.append(", '' AS REF_GENERATION"); + query.append(" FROM information_schema.tables "); + + List filters = new ArrayList<>(4); + if (catalog != null) { + if (catalog.isEmpty()) { + filters.add("table_catalog IS NULL"); + } + else { + filters.add(stringColumnEquals("table_catalog", catalog)); + } + } + + if (schemaPattern != null) { + if (schemaPattern.isEmpty()) { + filters.add("table_schema IS NULL"); + } + else { + filters.add(stringColumnLike("table_schema", schemaPattern)); + } + } + + if (tableNamePattern != null) { + filters.add(stringColumnLike("table_name", tableNamePattern)); + } + + if (types != null && types.length > 0) { + StringBuilder filter = new StringBuilder(); + filter.append("table_type in ("); + + for (int i = 0; i < types.length; i++) { + String type = types[i]; + + if (i > 0) { + filter.append(" ,"); + } + + quoteStringLiteral(filter, type); + } + filter.append(")"); + filters.add(filter.toString()); + } + + if (!filters.isEmpty()) { + query.append(" WHERE "); + Joiner.on(" AND ").appendTo(query, filters); + } + + query.append(" ORDER BY TABLE_TYPE, TABLE_CAT, TABLE_SCHEM, TABLE_NAME"); + + return select(query.toString()); + } + + @Override + public ResultSet getSchemas() + throws SQLException + { + return select("" + + "SELECT schema_name AS TABLE_SCHEM, catalog_name TABLE_CATALOG " + + "FROM information_schema.schemata " + + "ORDER BY TABLE_CATALOG, TABLE_SCHEM"); + } + + @Override + public ResultSet getCatalogs() + throws SQLException + { + return select("" + + "SELECT DISTINCT catalog_name AS TABLE_CAT " + + "FROM information_schema.schemata " + + "ORDER BY TABLE_CAT"); + } + + @Override + public ResultSet getTableTypes() + throws SQLException + { + return select("" + + "SELECT DISTINCT table_type AS TABLE_TYPE " + + "FROM information_schema.tables " + + "ORDER BY TABLE_TYPE"); + } + + @Override + public ResultSet getColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) + throws SQLException + { + StringBuilder query = new StringBuilder("" + + "SELECT " + + " table_catalog TABLE_CAT " + + ", table_schema TABLE_SCHEM " + + ", table_name TABLE_NAME " + + ", column_name COLUMN_NAME " + + ", CASE data_type " + + " WHEN 'bigint' THEN " + Types.BIGINT + " " + + " WHEN 'double' THEN " + Types.DOUBLE + " " + + " WHEN 'varchar' THEN " + Types.LONGNVARCHAR + " " + + " WHEN 'boolean' THEN " + Types.BOOLEAN + " " + + " ELSE " + Types.OTHER + " " + + " END DATA_TYPE " + + ", data_type TYPE_NAME " + + ", 0 COLUMN_SIZE " + + ", 0 BUFFER_LENGTH " + + ", CASE data_type " + + " WHEN 'bigint' THEN 0 " + + " END DECIMAL_DIGITS " + + ", CASE data_type " + + " WHEN 'bigint' THEN 10 " + + " WHEN 'double' THEN 10 " + + " ELSE 0 " + + " END AS NUM_PREC_RADIX " + + ", CASE is_nullable " + + " WHEN 'NO' THEN " + columnNoNulls + " " + + " WHEN 'YES' THEN 1" + columnNullable + " " + + " ELSE 2" + columnNullableUnknown + " " + + " END NULLABLE " + + ", CAST(NULL AS varchar) REMARKS " + + ", column_default AS COLUMN_DEF " + + ", CAST(NULL AS bigint) AS SQL_DATA_TYPE " + + ", CAST(NULL AS bigint) AS SQL_DATETIME_SUB " + + ", 0 AS CHAR_OCTET_LENGTH " + + ", ordinal_position ORDINAL_POSITION " + + ", is_nullable IS_NULLABLE " + + ", CAST(NULL AS varchar) SCOPE_CATALOG " + + ", CAST(NULL AS varchar) SCOPE_SCHEMA " + + ", CAST(NULL AS varchar) SCOPE_TABLE " + + ", CAST(NULL AS bigint) SOURCE_DATA_TYPE " + + ", '' IS_AUTOINCREMENT " + + ", '' IS_GENERATEDCOLUMN " + + "FROM information_schema.columns "); + + List filters = new ArrayList<>(4); + if (catalog != null) { + if (catalog.isEmpty()) { + filters.add("table_catalog IS NULL"); + } + else { + filters.add(stringColumnEquals("table_catalog", catalog)); + } + } + + if (schemaPattern != null) { + if (schemaPattern.isEmpty()) { + filters.add("table_schema IS NULL"); + } + else { + filters.add(stringColumnLike("table_schema", schemaPattern)); + } + } + + if (tableNamePattern != null) { + filters.add(stringColumnLike("table_name", tableNamePattern)); + } + + if (columnNamePattern != null) { + filters.add(stringColumnLike("column_name", columnNamePattern)); + } + + if (!filters.isEmpty()) { + query.append(" WHERE "); + Joiner.on(" AND ").appendTo(query, filters); + } + + query.append(" ORDER BY table_cat, table_schem, table_name, ordinal_position"); + + return select(query.toString()); + } + + @Override + public ResultSet getColumnPrivileges(String catalog, String schema, String table, String columnNamePattern) + throws SQLException + { + throw new SQLFeatureNotSupportedException("privileges not supported"); + } + + @Override + public ResultSet getTablePrivileges(String catalog, String schemaPattern, String tableNamePattern) + throws SQLException + { + throw new SQLFeatureNotSupportedException("privileges not supported"); + } + + @Override + public ResultSet getBestRowIdentifier(String catalog, String schema, String table, int scope, boolean nullable) + throws SQLException + { + throw new SQLFeatureNotSupportedException("row identifiers not supported"); + } + + @Override + public ResultSet getVersionColumns(String catalog, String schema, String table) + throws SQLException + { + throw new SQLFeatureNotSupportedException("version columns not supported"); + } + + @Override + public ResultSet getPrimaryKeys(String catalog, String schema, String table) + throws SQLException + { + throw new SQLFeatureNotSupportedException("primary keys not supported"); + } + + @Override + public ResultSet getImportedKeys(String catalog, String schema, String table) + throws SQLException + { + throw new SQLFeatureNotSupportedException("imported keys not supported"); + } + + @Override + public ResultSet getExportedKeys(String catalog, String schema, String table) + throws SQLException + { + throw new SQLFeatureNotSupportedException("exported keys not supported"); + } + + @Override + public ResultSet getCrossReference(String parentCatalog, String parentSchema, String parentTable, String foreignCatalog, String foreignSchema, String foreignTable) + throws SQLException + { + throw new SQLFeatureNotSupportedException("cross reference not supported"); + } + + @Override + public ResultSet getTypeInfo() + throws SQLException + { + // TODO: implement this + throw new NotImplementedException("DatabaseMetaData", "getTypeInfo"); + } + + @Override + public ResultSet getIndexInfo(String catalog, String schema, String table, boolean unique, boolean approximate) + throws SQLException + { + throw new SQLFeatureNotSupportedException("indexes not supported"); + } + + @Override + public boolean supportsResultSetType(int type) + throws SQLException + { + return type == ResultSet.TYPE_FORWARD_ONLY; + } + + @Override + public boolean supportsResultSetConcurrency(int type, int concurrency) + throws SQLException + { + return (type == ResultSet.TYPE_FORWARD_ONLY) && + (concurrency == ResultSet.CONCUR_READ_ONLY); + } + + @Override + public boolean ownUpdatesAreVisible(int type) + throws SQLException + { + return false; + } + + @Override + public boolean ownDeletesAreVisible(int type) + throws SQLException + { + return false; + } + + @Override + public boolean ownInsertsAreVisible(int type) + throws SQLException + { + return false; + } + + @Override + public boolean othersUpdatesAreVisible(int type) + throws SQLException + { + return false; + } + + @Override + public boolean othersDeletesAreVisible(int type) + throws SQLException + { + return false; + } + + @Override + public boolean othersInsertsAreVisible(int type) + throws SQLException + { + return false; + } + + @Override + public boolean updatesAreDetected(int type) + throws SQLException + { + return false; + } + + @Override + public boolean deletesAreDetected(int type) + throws SQLException + { + return false; + } + + @Override + public boolean insertsAreDetected(int type) + throws SQLException + { + return false; + } + + @Override + public boolean supportsBatchUpdates() + throws SQLException + { + // TODO: support batch updates + return false; + } + + @Override + public ResultSet getUDTs(String catalog, String schemaPattern, String typeNamePattern, int[] types) + throws SQLException + { + throw new SQLFeatureNotSupportedException("user-defined types not supported"); + } + + @Override + public Connection getConnection() + throws SQLException + { + return connection; + } + + @Override + public boolean supportsSavepoints() + throws SQLException + { + return false; + } + + @Override + public boolean supportsNamedParameters() + throws SQLException + { + return true; + } + + @Override + public boolean supportsMultipleOpenResults() + throws SQLException + { + return false; + } + + @Override + public boolean supportsGetGeneratedKeys() + throws SQLException + { + return false; + } + + @Override + public ResultSet getSuperTypes(String catalog, String schemaPattern, String typeNamePattern) + throws SQLException + { + throw new SQLFeatureNotSupportedException("type hierarchies not supported"); + } + + @Override + public ResultSet getSuperTables(String catalog, String schemaPattern, String tableNamePattern) + throws SQLException + { + throw new SQLFeatureNotSupportedException("type hierarchies not supported"); + } + + @Override + public ResultSet getAttributes(String catalog, String schemaPattern, String typeNamePattern, String attributeNamePattern) + throws SQLException + { + throw new SQLFeatureNotSupportedException("user-defined types not supported"); + } + + @Override + public boolean supportsResultSetHoldability(int holdability) + throws SQLException + { + return holdability == ResultSet.HOLD_CURSORS_OVER_COMMIT; + } + + @Override + public int getResultSetHoldability() + throws SQLException + { + return ResultSet.HOLD_CURSORS_OVER_COMMIT; + } + + @Override + public int getDatabaseMajorVersion() + throws SQLException + { + // TODO: get version from server + return PrestoDriver.VERSION_MAJOR; + } + + @Override + public int getDatabaseMinorVersion() + throws SQLException + { + return PrestoDriver.VERSION_MINOR; + } + + @Override + public int getJDBCMajorVersion() + throws SQLException + { + return PrestoDriver.JDBC_VERSION_MAJOR; + } + + @Override + public int getJDBCMinorVersion() + throws SQLException + { + return PrestoDriver.JDBC_VERSION_MINOR; + } + + @Override + public int getSQLStateType() + throws SQLException + { + return DatabaseMetaData.sqlStateSQL; + } + + @Override + public boolean locatorsUpdateCopy() + throws SQLException + { + return true; + } + + @Override + public boolean supportsStatementPooling() + throws SQLException + { + return false; + } + + @Override + public RowIdLifetime getRowIdLifetime() + throws SQLException + { + return RowIdLifetime.ROWID_UNSUPPORTED; + } + + @Override + public ResultSet getSchemas(String catalog, String schemaPattern) + throws SQLException + { + // The schema columns are: + // TABLE_SCHEM String => schema name + // TABLE_CATALOG String => catalog name (may be null) + StringBuilder query = new StringBuilder(512); + query.append("SELECT DISTINCT schema_name TABLE_SCHEM, catalog_name TABLE_CATALOG "); + query.append(" FROM information_schema.schemata"); + + List filters = new ArrayList<>(4); + if (catalog != null) { + if (catalog.isEmpty()) { + filters.add("catalog_name IS NULL"); + } + else { + filters.add(stringColumnEquals("catalog_name", catalog)); + } + } + + if (schemaPattern != null) { + filters.add(stringColumnLike("schema_name", schemaPattern)); + } + + if (!filters.isEmpty()) { + query.append(" WHERE "); + Joiner.on(" AND ").appendTo(query, filters); + } + + query.append(" ORDER BY TABLE_CATALOG, TABLE_SCHEM"); + + return select(query.toString()); + } + + @Override + public boolean supportsStoredFunctionsUsingCallSyntax() + throws SQLException + { + return false; + } + + @Override + public boolean autoCommitFailureClosesAllResultSets() + throws SQLException + { + return false; + } + + @Override + public ResultSet getClientInfoProperties() + throws SQLException + { + // TODO: implement this + throw new NotImplementedException("DatabaseMetaData", "getClientInfoProperties"); + } + + @Override + public ResultSet getFunctions(String catalog, String schemaPattern, String functionNamePattern) + throws SQLException + { + // TODO: implement this + throw new NotImplementedException("DatabaseMetaData", "getFunctions"); + } + + @Override + public ResultSet getFunctionColumns(String catalog, String schemaPattern, String functionNamePattern, String columnNamePattern) + throws SQLException + { + // TODO: implement this + throw new NotImplementedException("DatabaseMetaData", "getFunctionColumns"); + } + + @Override + public ResultSet getPseudoColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) + throws SQLException + { + // TODO: implement this + throw new NotImplementedException("DatabaseMetaData", "getPseudoColumns"); + } + + @Override + public boolean generatedKeyAlwaysReturned() + throws SQLException + { + return false; + } + + @SuppressWarnings("unchecked") + @Override + public T unwrap(Class iface) + throws SQLException + { + if (isWrapperFor(iface)) { + return (T) this; + } + throw new SQLException("No wrapper for " + iface); + } + + @Override + public boolean isWrapperFor(Class iface) + throws SQLException + { + return iface.isInstance(this); + } + + private ResultSet select(String sql) + throws SQLException + { + try (Statement statement = getConnection().createStatement()) { + return statement.executeQuery(sql); + } + } + + private static String stringColumnEquals(String columnName, String value) + { + StringBuilder filter = new StringBuilder(); + filter.append(columnName).append(" = "); + quoteStringLiteral(filter, value); + return filter.toString(); + } + + private static String stringColumnLike(String columnName, String pattern) + { + StringBuilder filter = new StringBuilder(); + filter.append(columnName).append(" LIKE "); + quoteStringLiteral(filter, pattern); + return filter.toString(); + } + + private static void quoteStringLiteral(StringBuilder out, String value) + { + out.append('\''); + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + out.append(c); + if (c == '\'') { + out.append('\''); + } + } + out.append('\''); + } +} diff --git a/presto-jdbc/src/main/java/com/facebook/presto/jdbc/PrestoDriver.java b/presto-jdbc/src/main/java/com/facebook/presto/jdbc/PrestoDriver.java new file mode 100644 index 00000000..c054e48a --- /dev/null +++ b/presto-jdbc/src/main/java/com/facebook/presto/jdbc/PrestoDriver.java @@ -0,0 +1,154 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.jdbc; + +import com.google.common.base.Throwables; + +import java.io.Closeable; +import java.net.URI; +import java.net.URISyntaxException; +import java.sql.Connection; +import java.sql.Driver; +import java.sql.DriverManager; +import java.sql.DriverPropertyInfo; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.util.Properties; +import java.util.logging.Logger; + +import static com.google.common.base.Strings.isNullOrEmpty; +import static java.lang.String.format; + +public class PrestoDriver + implements Driver, Closeable +{ + static final int VERSION_MAJOR = 1; + static final int VERSION_MINOR = 0; + + static final int JDBC_VERSION_MAJOR = 4; + static final int JDBC_VERSION_MINOR = 1; + + static final String DRIVER_NAME = "Presto JDBC Driver"; + static final String DRIVER_VERSION = VERSION_MAJOR + "." + VERSION_MINOR; + + private static final DriverPropertyInfo[] DRIVER_PROPERTY_INFOS = {}; + + private static final String JDBC_URL_START = "jdbc:"; + private static final String DRIVER_URL_START = "jdbc:presto:"; + + private static final String USER_PROPERTY = "user"; + + private final QueryExecutor queryExecutor; + + static { + JettyLogging.useJavaUtilLogging(); + + try { + DriverManager.registerDriver(new PrestoDriver()); + } + catch (SQLException e) { + throw Throwables.propagate(e); + } + } + + public PrestoDriver() + { + this.queryExecutor = QueryExecutor.create(DRIVER_NAME + "/" + DRIVER_VERSION); + } + + @Override + public void close() + { + queryExecutor.close(); + } + + @Override + public Connection connect(String url, Properties info) + throws SQLException + { + if (!acceptsURL(url)) { + return null; + } + + String user = info.getProperty(USER_PROPERTY); + if (isNullOrEmpty(user)) { + throw new SQLException(format("Username property (%s) must be set", USER_PROPERTY)); + } + + return new PrestoConnection(parseDriverUrl(url), user, queryExecutor); + } + + @Override + public boolean acceptsURL(String url) + throws SQLException + { + return url.startsWith(DRIVER_URL_START); + } + + @Override + public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) + throws SQLException + { + return DRIVER_PROPERTY_INFOS; + } + + @Override + public int getMajorVersion() + { + return VERSION_MAJOR; + } + + @Override + public int getMinorVersion() + { + return VERSION_MINOR; + } + + @Override + public boolean jdbcCompliant() + { + // TODO: pass compliance tests + return false; + } + + @Override + public Logger getParentLogger() + throws SQLFeatureNotSupportedException + { + // TODO: support java.util.Logging + throw new SQLFeatureNotSupportedException(); + } + + private static URI parseDriverUrl(String url) + throws SQLException + { + URI uri; + try { + uri = new URI(url.substring(JDBC_URL_START.length())); + } + catch (URISyntaxException e) { + throw new SQLException("Invalid JDBC URL: " + url, e); + } + if (isNullOrEmpty(uri.getHost())) { + throw new SQLException("No host specified: " + url); + } + if (uri.getPort() == -1) { + throw new SQLException("No port number specified: " + url); + } + if ((uri.getPort() < 1) || (uri.getPort() > 65535)) { + throw new SQLException("Invalid port number: " + url); + } + return uri; + } +} diff --git a/presto-jdbc/src/main/java/com/facebook/presto/jdbc/PrestoIntervalDayTime.java b/presto-jdbc/src/main/java/com/facebook/presto/jdbc/PrestoIntervalDayTime.java new file mode 100644 index 00000000..d46d2c26 --- /dev/null +++ b/presto-jdbc/src/main/java/com/facebook/presto/jdbc/PrestoIntervalDayTime.java @@ -0,0 +1,89 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.jdbc; + +import java.util.Objects; + +public class PrestoIntervalDayTime +{ + private static final long MILLIS_IN_SECOND = 1000; + private static final long MILLIS_IN_MINUTE = 60 * MILLIS_IN_SECOND; + private static final long MILLIS_IN_HOUR = 60 * MILLIS_IN_MINUTE; + private static final long MILLIS_IN_DAY = 24 * MILLIS_IN_HOUR; + + private final long milliSeconds; + + public PrestoIntervalDayTime(long milliSeconds) + { + this.milliSeconds = milliSeconds; + } + + public PrestoIntervalDayTime(int day, int hour, int minute, int second, int millis) + { + milliSeconds = toMillis(day, hour, minute, second, millis); + } + + public long getMilliSeconds() + { + return milliSeconds; + } + + @Override + public int hashCode() + { + return Objects.hash(milliSeconds); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + PrestoIntervalDayTime other = (PrestoIntervalDayTime) obj; + return Objects.equals(this.milliSeconds, other.milliSeconds); + } + + @Override + public String toString() + { + return formatMillis(milliSeconds); + } + + private static long toMillis(int day, int hour, int minute, int second, int millis) + { + return (day * MILLIS_IN_DAY) + + (hour * MILLIS_IN_HOUR) + + (minute * MILLIS_IN_MINUTE) + + (second * MILLIS_IN_SECOND) + + millis; + } + + private static String formatMillis(long millis) + { + long day = millis / MILLIS_IN_DAY; + millis %= MILLIS_IN_DAY; + long hour = millis / MILLIS_IN_HOUR; + millis %= MILLIS_IN_HOUR; + long minute = millis / MILLIS_IN_MINUTE; + millis %= MILLIS_IN_MINUTE; + long second = millis / MILLIS_IN_SECOND; + millis %= MILLIS_IN_SECOND; + + return String.format("%d %02d:%02d:%02d.%03d", day, hour, minute, second, millis); + } +} diff --git a/presto-jdbc/src/main/java/com/facebook/presto/jdbc/PrestoIntervalYearMonth.java b/presto-jdbc/src/main/java/com/facebook/presto/jdbc/PrestoIntervalYearMonth.java new file mode 100644 index 00000000..5257708c --- /dev/null +++ b/presto-jdbc/src/main/java/com/facebook/presto/jdbc/PrestoIntervalYearMonth.java @@ -0,0 +1,66 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.jdbc; + +import java.util.Objects; + +public class PrestoIntervalYearMonth +{ + private final long months; + + public PrestoIntervalYearMonth(long months) + { + this.months = months; + } + + public PrestoIntervalYearMonth(int year, int months) + { + this.months = (12L * year) + months; + } + + public long getMonths() + { + return months; + } + + @Override + public int hashCode() + { + return Objects.hash(months); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + PrestoIntervalYearMonth other = (PrestoIntervalYearMonth) obj; + return Objects.equals(this.months, other.months); + } + + @Override + public String toString() + { + return formatMonths(months); + } + + private static String formatMonths(long months) + { + return (months / 12) + "-" + (months % 12); + } +} diff --git a/presto-jdbc/src/main/java/com/facebook/presto/jdbc/PrestoPreparedStatement.java b/presto-jdbc/src/main/java/com/facebook/presto/jdbc/PrestoPreparedStatement.java new file mode 100644 index 00000000..ead74307 --- /dev/null +++ b/presto-jdbc/src/main/java/com/facebook/presto/jdbc/PrestoPreparedStatement.java @@ -0,0 +1,439 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.jdbc; + +import java.io.InputStream; +import java.io.Reader; +import java.math.BigDecimal; +import java.net.URL; +import java.sql.Array; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.Date; +import java.sql.NClob; +import java.sql.ParameterMetaData; +import java.sql.PreparedStatement; +import java.sql.Ref; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.RowId; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.sql.SQLXML; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.Calendar; + +public class PrestoPreparedStatement + extends PrestoStatement + implements PreparedStatement +{ + PrestoPreparedStatement(PrestoConnection connection, String sql) + throws SQLException + { + super(connection); + } + + @Override + public ResultSet executeQuery() + throws SQLException + { + throw new NotImplementedException("PreparedStatement", "executeQuery"); + } + + @Override + public int executeUpdate() + throws SQLException + { + throw new NotImplementedException("PreparedStatement", "executeUpdate"); + } + + @Override + public void setNull(int parameterIndex, int sqlType) + throws SQLException + { + throw new NotImplementedException("PreparedStatement", "setNull"); + } + + @Override + public void setBoolean(int parameterIndex, boolean x) + throws SQLException + { + throw new NotImplementedException("PreparedStatement", "setBoolean"); + } + + @Override + public void setByte(int parameterIndex, byte x) + throws SQLException + { + throw new NotImplementedException("PreparedStatement", "setByte"); + } + + @Override + public void setShort(int parameterIndex, short x) + throws SQLException + { + throw new NotImplementedException("PreparedStatement", "setShort"); + } + + @Override + public void setInt(int parameterIndex, int x) + throws SQLException + { + throw new NotImplementedException("PreparedStatement", "setInt"); + } + + @Override + public void setLong(int parameterIndex, long x) + throws SQLException + { + throw new NotImplementedException("PreparedStatement", "setLong"); + } + + @Override + public void setFloat(int parameterIndex, float x) + throws SQLException + { + throw new NotImplementedException("PreparedStatement", "setFloat"); + } + + @Override + public void setDouble(int parameterIndex, double x) + throws SQLException + { + throw new NotImplementedException("PreparedStatement", "setDouble"); + } + + @Override + public void setBigDecimal(int parameterIndex, BigDecimal x) + throws SQLException + { + throw new NotImplementedException("PreparedStatement", "setBigDecimal"); + } + + @Override + public void setString(int parameterIndex, String x) + throws SQLException + { + throw new NotImplementedException("PreparedStatement", "setString"); + } + + @Override + public void setBytes(int parameterIndex, byte[] x) + throws SQLException + { + throw new NotImplementedException("PreparedStatement", "setBytes"); + } + + @Override + public void setDate(int parameterIndex, Date x) + throws SQLException + { + throw new NotImplementedException("PreparedStatement", "setDate"); + } + + @Override + public void setTime(int parameterIndex, Time x) + throws SQLException + { + throw new NotImplementedException("PreparedStatement", "setTime"); + } + + @Override + public void setTimestamp(int parameterIndex, Timestamp x) + throws SQLException + { + throw new NotImplementedException("PreparedStatement", "setTimestamp"); + } + + @Override + public void setAsciiStream(int parameterIndex, InputStream x, int length) + throws SQLException + { + throw new NotImplementedException("PreparedStatement", "setAsciiStream"); + } + + @Override + public void setUnicodeStream(int parameterIndex, InputStream x, int length) + throws SQLException + { + throw new SQLFeatureNotSupportedException("setUnicodeStream"); + } + + @Override + public void setBinaryStream(int parameterIndex, InputStream x, int length) + throws SQLException + { + throw new NotImplementedException("PreparedStatement", "setBinaryStream"); + } + + @Override + public void clearParameters() + throws SQLException + { + throw new NotImplementedException("PreparedStatement", "clearParameters"); + } + + @Override + public void setObject(int parameterIndex, Object x, int targetSqlType) + throws SQLException + { + throw new NotImplementedException("PreparedStatement", "setObject"); + } + + @Override + public void setObject(int parameterIndex, Object x) + throws SQLException + { + throw new NotImplementedException("PreparedStatement", "setObject"); + } + + @Override + public boolean execute() + throws SQLException + { + throw new NotImplementedException("PreparedStatement", "execute"); + } + + @Override + public void addBatch() + throws SQLException + { + throw new NotImplementedException("PreparedStatement", "addBatch"); + } + + @Override + public void setCharacterStream(int parameterIndex, Reader reader, int length) + throws SQLException + { + throw new NotImplementedException("PreparedStatement", "setCharacterStream"); + } + + @Override + public void setRef(int parameterIndex, Ref x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("setRef"); + } + + @Override + public void setBlob(int parameterIndex, Blob x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("setBlob"); + } + + @Override + public void setClob(int parameterIndex, Clob x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("setClob"); + } + + @Override + public void setArray(int parameterIndex, Array x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("setArray"); + } + + @Override + public ResultSetMetaData getMetaData() + throws SQLException + { + throw new SQLFeatureNotSupportedException("getMetaData"); + } + + @Override + public void setDate(int parameterIndex, Date x, Calendar cal) + throws SQLException + { + throw new NotImplementedException("PreparedStatement", "setDate"); + } + + @Override + public void setTime(int parameterIndex, Time x, Calendar cal) + throws SQLException + { + throw new NotImplementedException("PreparedStatement", "setTime"); + } + + @Override + public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) + throws SQLException + { + throw new NotImplementedException("PreparedStatement", "setTimestamp"); + } + + @Override + public void setNull(int parameterIndex, int sqlType, String typeName) + throws SQLException + { + throw new NotImplementedException("PreparedStatement", "setNull"); + } + + @Override + public void setURL(int parameterIndex, URL x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("setURL"); + } + + @Override + public ParameterMetaData getParameterMetaData() + throws SQLException + { + throw new NotImplementedException("PreparedStatement", "getParameterMetaData"); + } + + @Override + public void setRowId(int parameterIndex, RowId x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("setRowId"); + } + + @Override + public void setNString(int parameterIndex, String value) + throws SQLException + { + throw new SQLFeatureNotSupportedException("setNString"); + } + + @Override + public void setNCharacterStream(int parameterIndex, Reader value, long length) + throws SQLException + { + throw new SQLFeatureNotSupportedException("setNCharacterStream"); + } + + @Override + public void setNClob(int parameterIndex, NClob value) + throws SQLException + { + throw new SQLFeatureNotSupportedException("setNClob"); + } + + @Override + public void setClob(int parameterIndex, Reader reader, long length) + throws SQLException + { + throw new SQLFeatureNotSupportedException("setClob"); + } + + @Override + public void setBlob(int parameterIndex, InputStream inputStream, long length) + throws SQLException + { + throw new SQLFeatureNotSupportedException("setBlob"); + } + + @Override + public void setNClob(int parameterIndex, Reader reader, long length) + throws SQLException + { + throw new SQLFeatureNotSupportedException("setNClob"); + } + + @Override + public void setSQLXML(int parameterIndex, SQLXML xmlObject) + throws SQLException + { + throw new SQLFeatureNotSupportedException("setSQLXML"); + } + + @Override + public void setObject(int parameterIndex, Object x, int targetSqlType, int scaleOrLength) + throws SQLException + { + throw new SQLFeatureNotSupportedException("setObject"); + } + + @Override + public void setAsciiStream(int parameterIndex, InputStream x, long length) + throws SQLException + { + throw new NotImplementedException("PreparedStatement", "setAsciiStream"); + } + + @Override + public void setBinaryStream(int parameterIndex, InputStream x, long length) + throws SQLException + { + throw new NotImplementedException("PreparedStatement", "setBinaryStream"); + } + + @Override + public void setCharacterStream(int parameterIndex, Reader reader, long length) + throws SQLException + { + throw new NotImplementedException("PreparedStatement", "setCharacterStream"); + } + + @Override + public void setAsciiStream(int parameterIndex, InputStream x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("setAsciiStream"); + } + + @Override + public void setBinaryStream(int parameterIndex, InputStream x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("setBinaryStream"); + } + + @Override + public void setCharacterStream(int parameterIndex, Reader reader) + throws SQLException + { + throw new SQLFeatureNotSupportedException("setCharacterStream"); + } + + @Override + public void setNCharacterStream(int parameterIndex, Reader value) + throws SQLException + { + throw new SQLFeatureNotSupportedException("setNCharacterStream"); + } + + @Override + public void setClob(int parameterIndex, Reader reader) + throws SQLException + { + throw new SQLFeatureNotSupportedException("setClob"); + } + + @Override + public void setBlob(int parameterIndex, InputStream inputStream) + throws SQLException + { + throw new SQLFeatureNotSupportedException("setBlob"); + } + + @Override + public void setNClob(int parameterIndex, Reader reader) + throws SQLException + { + throw new SQLFeatureNotSupportedException("setNClob"); + } + + @Override + public void addBatch(String sql) + throws SQLException + { + throw new SQLException("This method cannot be called on PreparedStatement"); + } +} diff --git a/presto-jdbc/src/main/java/com/facebook/presto/jdbc/PrestoResultSet.java b/presto-jdbc/src/main/java/com/facebook/presto/jdbc/PrestoResultSet.java new file mode 100644 index 00000000..7273ddde --- /dev/null +++ b/presto-jdbc/src/main/java/com/facebook/presto/jdbc/PrestoResultSet.java @@ -0,0 +1,1832 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.jdbc; + +import com.facebook.presto.client.Column; +import com.facebook.presto.client.QueryError; +import com.facebook.presto.client.QueryResults; +import com.facebook.presto.client.StatementClient; +import com.facebook.presto.jdbc.ColumnInfo.Nullable; +import com.google.common.collect.AbstractIterator; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import org.joda.time.DateTimeZone; +import org.joda.time.Period; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; +import org.joda.time.format.DateTimeFormatterBuilder; +import org.joda.time.format.DateTimeParser; +import org.joda.time.format.ISODateTimeFormat; +import org.joda.time.format.PeriodFormatter; +import org.joda.time.format.PeriodFormatterBuilder; + +import java.io.InputStream; +import java.io.Reader; +import java.math.BigDecimal; +import java.net.URL; +import java.sql.Array; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.Date; +import java.sql.NClob; +import java.sql.Ref; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.RowId; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.sql.SQLWarning; +import java.sql.SQLXML; +import java.sql.Statement; +import java.sql.Time; +import java.sql.Timestamp; +import java.sql.Types; +import java.util.Calendar; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import static com.facebook.presto.jdbc.ColumnInfo.setTypeInfo; +import static com.facebook.presto.spi.type.TypeSignature.parseTypeSignature; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Throwables.propagate; +import static com.google.common.collect.Iterables.getOnlyElement; +import static com.google.common.collect.Iterators.concat; +import static com.google.common.collect.Iterators.transform; +import static java.lang.String.format; +import static java.util.Locale.ENGLISH; +import static java.util.Objects.requireNonNull; + +public class PrestoResultSet + implements ResultSet +{ + private static final DateTimeFormatter DATE_FORMATTER = ISODateTimeFormat.date(); + private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormat.forPattern("HH:mm:ss.SSS"); + private static final DateTimeFormatter TIME_WITH_TIME_ZONE_FORMATTER = new DateTimeFormatterBuilder() + .append(DateTimeFormat.forPattern("HH:mm:ss.SSS ZZZ").getPrinter(), + new DateTimeParser[] { + DateTimeFormat.forPattern("HH:mm:ss.SSS Z").getParser(), + DateTimeFormat.forPattern("HH:mm:ss.SSS ZZZ").getParser(), + }) + .toFormatter() + .withOffsetParsed(); + + private static final DateTimeFormatter TIMESTAMP_FORMATTER = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss.SSS"); + private static final DateTimeFormatter TIMESTAMP_WITH_TIME_ZONE_FORMATTER = new DateTimeFormatterBuilder() + .append(DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss.SSS ZZZ").getPrinter(), + new DateTimeParser[] { + DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss.SSS Z").getParser(), + DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss.SSS ZZZ").getParser(), + }) + .toFormatter() + .withOffsetParsed(); + + private static final PeriodFormatter INTERVAL_YEAR_TO_MONTH_FORMATTER = new PeriodFormatterBuilder() + .appendYears() + .appendLiteral("-") + .appendMonths() + .toFormatter(); + + private static final PeriodFormatter INTERVAL_DAY_TO_SECOND_FORMATTER = new PeriodFormatterBuilder() + .appendDays() + .appendLiteral(" ") + .appendHours() + .appendLiteral(":") + .appendMinutes() + .appendLiteral(":") + .appendSecondsWithOptionalMillis() + .toFormatter(); + + private static final int YEAR_FIELD = 0; + private static final int MONTH_FIELD = 1; + private static final int DAY_FIELD = 3; + private static final int HOUR_FIELD = 4; + private static final int MINUTE_FIELD = 5; + private static final int SECOND_FIELD = 6; + private static final int MILLIS_FIELD = 7; + + private final StatementClient client; + private final DateTimeZone sessionTimeZone; + private final String queryId; + private final Iterator> results; + private final Map fieldMap; + private final List columnInfoList; + private final ResultSetMetaData resultSetMetaData; + private final AtomicReference> row = new AtomicReference<>(); + private final AtomicBoolean wasNull = new AtomicBoolean(); + + PrestoResultSet(StatementClient client) + throws SQLException + { + this.client = checkNotNull(client, "client is null"); + this.sessionTimeZone = DateTimeZone.forID(client.getTimeZoneId()); + this.queryId = client.current().getId(); + + List columns = getColumns(client); + this.fieldMap = getFieldMap(columns); + this.columnInfoList = getColumnInfo(columns); + this.resultSetMetaData = new PrestoResultSetMetaData(columnInfoList); + + this.results = flatten(new ResultsPageIterator(client)); + } + + public String getQueryId() + { + return queryId; + } + + @Override + public boolean next() + throws SQLException + { + checkOpen(); + try { + if (!results.hasNext()) { + row.set(null); + return false; + } + row.set(results.next()); + return true; + } + catch (RuntimeException e) { + if (e.getCause() instanceof SQLException) { + throw (SQLException) e.getCause(); + } + throw new SQLException("Error fetching results", e); + } + } + + @Override + public void close() + throws SQLException + { + client.close(); + } + + @Override + public boolean wasNull() + throws SQLException + { + return wasNull.get(); + } + + @Override + public String getString(int columnIndex) + throws SQLException + { + Object value = column(columnIndex); + return (value != null) ? value.toString() : null; + } + + @Override + public boolean getBoolean(int columnIndex) + throws SQLException + { + Object value = column(columnIndex); + return (value != null) ? (Boolean) value : false; + } + + @Override + public byte getByte(int columnIndex) + throws SQLException + { + return toNumber(column(columnIndex)).byteValue(); + } + + @Override + public short getShort(int columnIndex) + throws SQLException + { + return toNumber(column(columnIndex)).shortValue(); + } + + @Override + public int getInt(int columnIndex) + throws SQLException + { + return toNumber(column(columnIndex)).intValue(); + } + + @Override + public long getLong(int columnIndex) + throws SQLException + { + return toNumber(column(columnIndex)).longValue(); + } + + @Override + public float getFloat(int columnIndex) + throws SQLException + { + return toNumber(column(columnIndex)).floatValue(); + } + + @Override + public double getDouble(int columnIndex) + throws SQLException + { + return toNumber(column(columnIndex)).doubleValue(); + } + + @Override + public BigDecimal getBigDecimal(int columnIndex, int scale) + throws SQLException + { + throw new SQLFeatureNotSupportedException("getBigDecimal"); + } + + @Override + public byte[] getBytes(int columnIndex) + throws SQLException + { + return (byte[]) column(columnIndex); + } + + @Override + public Date getDate(int columnIndex) + throws SQLException + { + return getDate(columnIndex, sessionTimeZone); + } + + private Date getDate(int columnIndex, DateTimeZone localTimeZone) + throws SQLException + { + Object value = column(columnIndex); + if (value == null) { + return null; + } + + try { + return new Date(DATE_FORMATTER.withZone(localTimeZone).parseMillis(String.valueOf(value))); + } + catch (IllegalArgumentException e) { + throw new SQLException("Invalid date from server: " + value, e); + } + } + + @Override + public Time getTime(int columnIndex) + throws SQLException + { + return getTime(columnIndex, sessionTimeZone); + } + + private Time getTime(int columnIndex, DateTimeZone localTimeZone) + throws SQLException + { + Object value = column(columnIndex); + if (value == null) { + return null; + } + + ColumnInfo columnInfo = columnInfo(columnIndex); + if (columnInfo.getColumnTypeName().equalsIgnoreCase("time")) { + try { + return new Time(TIME_FORMATTER.withZone(localTimeZone).parseMillis(String.valueOf(value))); + } + catch (IllegalArgumentException e) { + throw new SQLException("Invalid time from server: " + value, e); + } + } + + if (columnInfo.getColumnTypeName().equalsIgnoreCase("time with time zone")) { + try { + return new Time(TIME_WITH_TIME_ZONE_FORMATTER.parseMillis(String.valueOf(value))); + } + catch (IllegalArgumentException e) { + throw new SQLException("Invalid time from server: " + value, e); + } + } + + throw new IllegalArgumentException("Expected column to be a time type but is " + columnInfo.getColumnTypeName()); + } + + @Override + public Timestamp getTimestamp(int columnIndex) + throws SQLException + { + return getTimestamp(columnIndex, sessionTimeZone); + } + + private Timestamp getTimestamp(int columnIndex, DateTimeZone localTimeZone) + throws SQLException + { + Object value = column(columnIndex); + if (value == null) { + return null; + } + + ColumnInfo columnInfo = columnInfo(columnIndex); + if (columnInfo.getColumnTypeName().equalsIgnoreCase("timestamp")) { + try { + return new Timestamp(TIMESTAMP_FORMATTER.withZone(localTimeZone).parseMillis(String.valueOf(value))); + } + catch (IllegalArgumentException e) { + throw new SQLException("Invalid timestamp from server: " + value, e); + } + } + + if (columnInfo.getColumnTypeName().equalsIgnoreCase("timestamp with time zone")) { + try { + return new Timestamp(TIMESTAMP_WITH_TIME_ZONE_FORMATTER.parseMillis(String.valueOf(value))); + } + catch (IllegalArgumentException e) { + throw new SQLException("Invalid timestamp from server: " + value, e); + } + } + + throw new IllegalArgumentException("Expected column to be a timestamp type but is " + columnInfo.getColumnTypeName()); + } + + @Override + public InputStream getAsciiStream(int columnIndex) + throws SQLException + { + throw new NotImplementedException("ResultSet", "getAsciiStream"); + } + + @Override + public InputStream getUnicodeStream(int columnIndex) + throws SQLException + { + throw new SQLFeatureNotSupportedException("getUnicodeStream"); + } + + @Override + public InputStream getBinaryStream(int columnIndex) + throws SQLException + { + throw new NotImplementedException("ResultSet", "getBinaryStream"); + } + + @Override + public String getString(String columnLabel) + throws SQLException + { + Object value = column(columnLabel); + return (value != null) ? value.toString() : null; + } + + @Override + public boolean getBoolean(String columnLabel) + throws SQLException + { + Object value = column(columnLabel); + return (value != null) ? (Boolean) value : false; + } + + @Override + public byte getByte(String columnLabel) + throws SQLException + { + return toNumber(column(columnLabel)).byteValue(); + } + + @Override + public short getShort(String columnLabel) + throws SQLException + { + return toNumber(column(columnLabel)).shortValue(); + } + + @Override + public int getInt(String columnLabel) + throws SQLException + { + return toNumber(column(columnLabel)).intValue(); + } + + @Override + public long getLong(String columnLabel) + throws SQLException + { + return toNumber(column(columnLabel)).longValue(); + } + + @Override + public float getFloat(String columnLabel) + throws SQLException + { + return toNumber(column(columnLabel)).floatValue(); + } + + @Override + public double getDouble(String columnLabel) + throws SQLException + { + return toNumber(column(columnLabel)).doubleValue(); + } + + @Override + public BigDecimal getBigDecimal(String columnLabel, int scale) + throws SQLException + { + throw new SQLFeatureNotSupportedException("getBigDecimal"); + } + + @Override + public byte[] getBytes(String columnLabel) + throws SQLException + { + return (byte[]) column(columnLabel); + } + + @Override + public Date getDate(String columnLabel) + throws SQLException + { + return getDate(columnIndex(columnLabel)); + } + + @Override + public Time getTime(String columnLabel) + throws SQLException + { + return getTime(columnIndex(columnLabel)); + } + + @Override + public Timestamp getTimestamp(String columnLabel) + throws SQLException + { + return getTimestamp(columnIndex(columnLabel)); + } + + @Override + public InputStream getAsciiStream(String columnLabel) + throws SQLException + { + throw new NotImplementedException("ResultSet", "getAsciiStream"); + } + + @Override + public InputStream getUnicodeStream(String columnLabel) + throws SQLException + { + throw new SQLFeatureNotSupportedException("getUnicodeStream"); + } + + @Override + public InputStream getBinaryStream(String columnLabel) + throws SQLException + { + throw new NotImplementedException("ResultSet", "getBinaryStream"); + } + + @Override + public SQLWarning getWarnings() + throws SQLException + { + checkOpen(); + return null; + } + + @Override + public void clearWarnings() + throws SQLException + { + checkOpen(); + } + + @Override + public String getCursorName() + throws SQLException + { + throw new SQLFeatureNotSupportedException("getCursorName"); + } + + @Override + public ResultSetMetaData getMetaData() + throws SQLException + { + return resultSetMetaData; + } + + @Override + public Object getObject(int columnIndex) + throws SQLException + { + ColumnInfo columnInfo = columnInfo(columnIndex); + switch (columnInfo.getColumnType()) { + case Types.DATE: + return getDate(columnIndex); + case Types.TIME: + return getTime(columnIndex); + case Types.TIMESTAMP: + return getTimestamp(columnIndex); + case Types.ARRAY: + return getArray(columnIndex); + case Types.JAVA_OBJECT: + if (columnInfo.getColumnTypeName().equalsIgnoreCase("interval year to month")) { + return getIntervalYearMonth(columnIndex); + } + if (columnInfo.getColumnTypeName().equalsIgnoreCase("interval day to second")) { + return getIntervalDayTime(columnIndex); + } + } + return column(columnIndex); + } + + private PrestoIntervalYearMonth getIntervalYearMonth(int columnIndex) + throws SQLException + { + Object value = column(columnIndex); + if (value == null) { + return null; + } + + Period period = INTERVAL_YEAR_TO_MONTH_FORMATTER.parsePeriod(String.valueOf(value)); + return new PrestoIntervalYearMonth( + period.getValue(YEAR_FIELD), + period.getValue(MONTH_FIELD)); + } + + private PrestoIntervalDayTime getIntervalDayTime(int columnIndex) + throws SQLException + { + Object value = column(columnIndex); + if (value == null) { + return null; + } + + Period period = INTERVAL_DAY_TO_SECOND_FORMATTER.parsePeriod(String.valueOf(value)); + return new PrestoIntervalDayTime( + period.getValue(DAY_FIELD), + period.getValue(HOUR_FIELD), + period.getValue(MINUTE_FIELD), + period.getValue(SECOND_FIELD), + period.getValue(MILLIS_FIELD)); + } + + @Override + public Object getObject(String columnLabel) + throws SQLException + { + return getObject(columnIndex(columnLabel)); + } + + @Override + public int findColumn(String columnLabel) + throws SQLException + { + checkOpen(); + return columnIndex(columnLabel); + } + + @Override + public Reader getCharacterStream(int columnIndex) + throws SQLException + { + throw new NotImplementedException("ResultSet", "getCharacterStream"); + } + + @Override + public Reader getCharacterStream(String columnLabel) + throws SQLException + { + throw new NotImplementedException("ResultSet", "getCharacterStream"); + } + + @Override + public BigDecimal getBigDecimal(int columnIndex) + throws SQLException + { + throw new NotImplementedException("ResultSet", "getBigDecimal"); + } + + @Override + public BigDecimal getBigDecimal(String columnLabel) + throws SQLException + { + throw new NotImplementedException("ResultSet", "getBigDecimal"); + } + + @Override + public boolean isBeforeFirst() + throws SQLException + { + throw new SQLFeatureNotSupportedException("isBeforeFirst"); + } + + @Override + public boolean isAfterLast() + throws SQLException + { + throw new SQLFeatureNotSupportedException("isAfterLast"); + } + + @Override + public boolean isFirst() + throws SQLException + { + throw new SQLFeatureNotSupportedException("isFirst"); + } + + @Override + public boolean isLast() + throws SQLException + { + throw new SQLFeatureNotSupportedException("isLast"); + } + + @Override + public void beforeFirst() + throws SQLException + { + throw new SQLFeatureNotSupportedException("beforeFirst"); + } + + @Override + public void afterLast() + throws SQLException + { + throw new SQLFeatureNotSupportedException("afterLast"); + } + + @Override + public boolean first() + throws SQLException + { + throw new SQLFeatureNotSupportedException("first"); + } + + @Override + public boolean last() + throws SQLException + { + throw new SQLFeatureNotSupportedException("last"); + } + + @Override + public int getRow() + throws SQLException + { + throw new SQLFeatureNotSupportedException("getRow"); + } + + @Override + public boolean absolute(int row) + throws SQLException + { + throw new SQLFeatureNotSupportedException("absolute"); + } + + @Override + public boolean relative(int rows) + throws SQLException + { + throw new SQLFeatureNotSupportedException("relative"); + } + + @Override + public boolean previous() + throws SQLException + { + throw new SQLFeatureNotSupportedException("previous"); + } + + @Override + public void setFetchDirection(int direction) + throws SQLException + { + checkOpen(); + if (direction != FETCH_FORWARD) { + throw new SQLException("Fetch direction must be FETCH_FORWARD"); + } + } + + @Override + public int getFetchDirection() + throws SQLException + { + checkOpen(); + return FETCH_FORWARD; + } + + @Override + public void setFetchSize(int rows) + throws SQLException + { + checkOpen(); + if (rows < 0) { + throw new SQLException("Rows is negative"); + } + // fetch size is ignored + } + + @Override + public int getFetchSize() + throws SQLException + { + checkOpen(); + // fetch size is ignored + return 0; + } + + @Override + public int getType() + throws SQLException + { + checkOpen(); + return TYPE_FORWARD_ONLY; + } + + @Override + public int getConcurrency() + throws SQLException + { + checkOpen(); + return CONCUR_READ_ONLY; + } + + @Override + public boolean rowUpdated() + throws SQLException + { + throw new SQLFeatureNotSupportedException("rowUpdated"); + } + + @Override + public boolean rowInserted() + throws SQLException + { + throw new SQLFeatureNotSupportedException("rowInserted"); + } + + @Override + public boolean rowDeleted() + throws SQLException + { + throw new SQLFeatureNotSupportedException("rowDeleted"); + } + + @Override + public void updateNull(int columnIndex) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateNull"); + } + + @Override + public void updateBoolean(int columnIndex, boolean x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateBoolean"); + } + + @Override + public void updateByte(int columnIndex, byte x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateByte"); + } + + @Override + public void updateShort(int columnIndex, short x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateShort"); + } + + @Override + public void updateInt(int columnIndex, int x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateInt"); + } + + @Override + public void updateLong(int columnIndex, long x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateLong"); + } + + @Override + public void updateFloat(int columnIndex, float x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateFloat"); + } + + @Override + public void updateDouble(int columnIndex, double x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateDouble"); + } + + @Override + public void updateBigDecimal(int columnIndex, BigDecimal x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateBigDecimal"); + } + + @Override + public void updateString(int columnIndex, String x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateString"); + } + + @Override + public void updateBytes(int columnIndex, byte[] x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateBytes"); + } + + @Override + public void updateDate(int columnIndex, Date x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateDate"); + } + + @Override + public void updateTime(int columnIndex, Time x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateTime"); + } + + @Override + public void updateTimestamp(int columnIndex, Timestamp x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateTimestamp"); + } + + @Override + public void updateAsciiStream(int columnIndex, InputStream x, int length) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateAsciiStream"); + } + + @Override + public void updateBinaryStream(int columnIndex, InputStream x, int length) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateBinaryStream"); + } + + @Override + public void updateCharacterStream(int columnIndex, Reader x, int length) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateCharacterStream"); + } + + @Override + public void updateObject(int columnIndex, Object x, int scaleOrLength) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateObject"); + } + + @Override + public void updateObject(int columnIndex, Object x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateObject"); + } + + @Override + public void updateNull(String columnLabel) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateNull"); + } + + @Override + public void updateBoolean(String columnLabel, boolean x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateBoolean"); + } + + @Override + public void updateByte(String columnLabel, byte x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateByte"); + } + + @Override + public void updateShort(String columnLabel, short x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateShort"); + } + + @Override + public void updateInt(String columnLabel, int x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateInt"); + } + + @Override + public void updateLong(String columnLabel, long x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateLong"); + } + + @Override + public void updateFloat(String columnLabel, float x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateFloat"); + } + + @Override + public void updateDouble(String columnLabel, double x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateDouble"); + } + + @Override + public void updateBigDecimal(String columnLabel, BigDecimal x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateBigDecimal"); + } + + @Override + public void updateString(String columnLabel, String x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateString"); + } + + @Override + public void updateBytes(String columnLabel, byte[] x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateBytes"); + } + + @Override + public void updateDate(String columnLabel, Date x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateDate"); + } + + @Override + public void updateTime(String columnLabel, Time x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateTime"); + } + + @Override + public void updateTimestamp(String columnLabel, Timestamp x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateTimestamp"); + } + + @Override + public void updateAsciiStream(String columnLabel, InputStream x, int length) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateAsciiStream"); + } + + @Override + public void updateBinaryStream(String columnLabel, InputStream x, int length) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateBinaryStream"); + } + + @Override + public void updateCharacterStream(String columnLabel, Reader reader, int length) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateCharacterStream"); + } + + @Override + public void updateObject(String columnLabel, Object x, int scaleOrLength) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateObject"); + } + + @Override + public void updateObject(String columnLabel, Object x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateObject"); + } + + @Override + public void insertRow() + throws SQLException + { + throw new SQLFeatureNotSupportedException("insertRow"); + } + + @Override + public void updateRow() + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateRow"); + } + + @Override + public void deleteRow() + throws SQLException + { + throw new SQLFeatureNotSupportedException("deleteRow"); + } + + @Override + public void refreshRow() + throws SQLException + { + throw new SQLFeatureNotSupportedException("refreshRow"); + } + + @Override + public void cancelRowUpdates() + throws SQLException + { + throw new SQLFeatureNotSupportedException("cancelRowUpdates"); + } + + @Override + public void moveToInsertRow() + throws SQLException + { + throw new SQLFeatureNotSupportedException("moveToInsertRow"); + } + + @Override + public void moveToCurrentRow() + throws SQLException + { + throw new SQLFeatureNotSupportedException("moveToCurrentRow"); + } + + @Override + public Statement getStatement() + throws SQLException + { + throw new NotImplementedException("ResultSet", "getStatement"); + } + + @Override + public Object getObject(int columnIndex, Map> map) + throws SQLException + { + throw new SQLFeatureNotSupportedException("getObject"); + } + + @Override + public Ref getRef(int columnIndex) + throws SQLException + { + throw new SQLFeatureNotSupportedException("getRef"); + } + + @Override + public Blob getBlob(int columnIndex) + throws SQLException + { + throw new SQLFeatureNotSupportedException("getBlob"); + } + + @Override + public Clob getClob(int columnIndex) + throws SQLException + { + throw new SQLFeatureNotSupportedException("getClob"); + } + + @Override + public Array getArray(int columnIndex) + throws SQLException + { + Object value = column(columnIndex); + if (value == null) { + return null; + } + + ColumnInfo columnInfo = columnInfo(columnIndex); + String elementTypeName = getOnlyElement(columnInfo.getColumnTypeSignature().getParameters()).toString(); + int elementType = getOnlyElement(columnInfo.getColumnParameterTypes()); + return new PrestoArray(elementTypeName, elementType, (List) value); + } + + @Override + public Object getObject(String columnLabel, Map> map) + throws SQLException + { + throw new SQLFeatureNotSupportedException("getObject"); + } + + @Override + public Ref getRef(String columnLabel) + throws SQLException + { + throw new SQLFeatureNotSupportedException("getRef"); + } + + @Override + public Blob getBlob(String columnLabel) + throws SQLException + { + throw new SQLFeatureNotSupportedException("getBlob"); + } + + @Override + public Clob getClob(String columnLabel) + throws SQLException + { + throw new SQLFeatureNotSupportedException("getClob"); + } + + @Override + public Array getArray(String columnLabel) + throws SQLException + { + return getArray(columnIndex(columnLabel)); + } + + @Override + public Date getDate(int columnIndex, Calendar cal) + throws SQLException + { + return getDate(columnIndex, DateTimeZone.forTimeZone(cal.getTimeZone())); + } + + @Override + public Date getDate(String columnLabel, Calendar cal) + throws SQLException + { + return getDate(columnIndex(columnLabel), cal); + } + + @Override + public Time getTime(int columnIndex, Calendar cal) + throws SQLException + { + return getTime(columnIndex, DateTimeZone.forTimeZone(cal.getTimeZone())); + } + + @Override + public Time getTime(String columnLabel, Calendar cal) + throws SQLException + { + return getTime(columnIndex(columnLabel), cal); + } + + @Override + public Timestamp getTimestamp(int columnIndex, Calendar cal) + throws SQLException + { + return getTimestamp(columnIndex, DateTimeZone.forTimeZone(cal.getTimeZone())); + } + + @Override + public Timestamp getTimestamp(String columnLabel, Calendar cal) + throws SQLException + { + return getTimestamp(columnIndex(columnLabel), cal); + } + + @Override + public URL getURL(int columnIndex) + throws SQLException + { + throw new SQLFeatureNotSupportedException("getURL"); + } + + @Override + public URL getURL(String columnLabel) + throws SQLException + { + throw new SQLFeatureNotSupportedException("getURL"); + } + + @Override + public void updateRef(int columnIndex, Ref x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateRef"); + } + + @Override + public void updateRef(String columnLabel, Ref x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateRef"); + } + + @Override + public void updateBlob(int columnIndex, Blob x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateBlob"); + } + + @Override + public void updateBlob(String columnLabel, Blob x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateBlob"); + } + + @Override + public void updateClob(int columnIndex, Clob x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateClob"); + } + + @Override + public void updateClob(String columnLabel, Clob x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateClob"); + } + + @Override + public void updateArray(int columnIndex, Array x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateArray"); + } + + @Override + public void updateArray(String columnLabel, Array x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateArray"); + } + + @Override + public RowId getRowId(int columnIndex) + throws SQLException + { + throw new SQLFeatureNotSupportedException("getRowId"); + } + + @Override + public RowId getRowId(String columnLabel) + throws SQLException + { + throw new SQLFeatureNotSupportedException("getRowId"); + } + + @Override + public void updateRowId(int columnIndex, RowId x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateRowId"); + } + + @Override + public void updateRowId(String columnLabel, RowId x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateRowId"); + } + + @Override + public int getHoldability() + throws SQLException + { + checkOpen(); + return ResultSet.HOLD_CURSORS_OVER_COMMIT; + } + + @Override + public boolean isClosed() + throws SQLException + { + return client.isClosed(); + } + + @Override + public void updateNString(int columnIndex, String nString) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateNString"); + } + + @Override + public void updateNString(String columnLabel, String nString) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateNString"); + } + + @Override + public void updateNClob(int columnIndex, NClob nClob) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateNClob"); + } + + @Override + public void updateNClob(String columnLabel, NClob nClob) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateNClob"); + } + + @Override + public NClob getNClob(int columnIndex) + throws SQLException + { + throw new SQLFeatureNotSupportedException("getNClob"); + } + + @Override + public NClob getNClob(String columnLabel) + throws SQLException + { + throw new SQLFeatureNotSupportedException("getNClob"); + } + + @Override + public SQLXML getSQLXML(int columnIndex) + throws SQLException + { + throw new SQLFeatureNotSupportedException("getSQLXML"); + } + + @Override + public SQLXML getSQLXML(String columnLabel) + throws SQLException + { + throw new SQLFeatureNotSupportedException("getSQLXML"); + } + + @Override + public void updateSQLXML(int columnIndex, SQLXML xmlObject) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateSQLXML"); + } + + @Override + public void updateSQLXML(String columnLabel, SQLXML xmlObject) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateSQLXML"); + } + + @Override + public String getNString(int columnIndex) + throws SQLException + { + throw new SQLFeatureNotSupportedException("getNString"); + } + + @Override + public String getNString(String columnLabel) + throws SQLException + { + throw new SQLFeatureNotSupportedException("getNString"); + } + + @Override + public Reader getNCharacterStream(int columnIndex) + throws SQLException + { + throw new SQLFeatureNotSupportedException("getNCharacterStream"); + } + + @Override + public Reader getNCharacterStream(String columnLabel) + throws SQLException + { + throw new SQLFeatureNotSupportedException("getNCharacterStream"); + } + + @Override + public void updateNCharacterStream(int columnIndex, Reader x, long length) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateNCharacterStream"); + } + + @Override + public void updateNCharacterStream(String columnLabel, Reader reader, long length) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateNCharacterStream"); + } + + @Override + public void updateAsciiStream(int columnIndex, InputStream x, long length) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateAsciiStream"); + } + + @Override + public void updateBinaryStream(int columnIndex, InputStream x, long length) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateBinaryStream"); + } + + @Override + public void updateCharacterStream(int columnIndex, Reader x, long length) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateCharacterStream"); + } + + @Override + public void updateAsciiStream(String columnLabel, InputStream x, long length) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateAsciiStream"); + } + + @Override + public void updateBinaryStream(String columnLabel, InputStream x, long length) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateBinaryStream"); + } + + @Override + public void updateCharacterStream(String columnLabel, Reader reader, long length) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateCharacterStream"); + } + + @Override + public void updateBlob(int columnIndex, InputStream inputStream, long length) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateBlob"); + } + + @Override + public void updateBlob(String columnLabel, InputStream inputStream, long length) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateBlob"); + } + + @Override + public void updateClob(int columnIndex, Reader reader, long length) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateClob"); + } + + @Override + public void updateClob(String columnLabel, Reader reader, long length) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateClob"); + } + + @Override + public void updateNClob(int columnIndex, Reader reader, long length) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateNClob"); + } + + @Override + public void updateNClob(String columnLabel, Reader reader, long length) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateNClob"); + } + + @Override + public void updateNCharacterStream(int columnIndex, Reader x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateNCharacterStream"); + } + + @Override + public void updateNCharacterStream(String columnLabel, Reader reader) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateNCharacterStream"); + } + + @Override + public void updateAsciiStream(int columnIndex, InputStream x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateAsciiStream"); + } + + @Override + public void updateBinaryStream(int columnIndex, InputStream x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateBinaryStream"); + } + + @Override + public void updateCharacterStream(int columnIndex, Reader x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateCharacterStream"); + } + + @Override + public void updateAsciiStream(String columnLabel, InputStream x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateAsciiStream"); + } + + @Override + public void updateBinaryStream(String columnLabel, InputStream x) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateBinaryStream"); + } + + @Override + public void updateCharacterStream(String columnLabel, Reader reader) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateCharacterStream"); + } + + @Override + public void updateBlob(int columnIndex, InputStream inputStream) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateBlob"); + } + + @Override + public void updateBlob(String columnLabel, InputStream inputStream) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateBlob"); + } + + @Override + public void updateClob(int columnIndex, Reader reader) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateClob"); + } + + @Override + public void updateClob(String columnLabel, Reader reader) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateClob"); + } + + @Override + public void updateNClob(int columnIndex, Reader reader) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateNClob"); + } + + @Override + public void updateNClob(String columnLabel, Reader reader) + throws SQLException + { + throw new SQLFeatureNotSupportedException("updateNClob"); + } + + @Override + public T getObject(int columnIndex, Class type) + throws SQLException + { + throw new SQLFeatureNotSupportedException("getObject"); + } + + @Override + public T getObject(String columnLabel, Class type) + throws SQLException + { + throw new SQLFeatureNotSupportedException("getObject"); + } + + @SuppressWarnings("unchecked") + @Override + public T unwrap(Class iface) + throws SQLException + { + if (isWrapperFor(iface)) { + return (T) this; + } + throw new SQLException("No wrapper for " + iface); + } + + @Override + public boolean isWrapperFor(Class iface) + throws SQLException + { + return iface.isInstance(this); + } + + private void checkOpen() + throws SQLException + { + if (isClosed()) { + throw new SQLException("ResultSet is closed"); + } + } + + private void checkValidRow() + throws SQLException + { + if (row.get() == null) { + throw new SQLException("Not on a valid row"); + } + } + + private Object column(int index) + throws SQLException + { + checkOpen(); + checkValidRow(); + if ((index <= 0) || (index > resultSetMetaData.getColumnCount())) { + throw new SQLException("Invalid column index: " + index); + } + Object value = row.get().get(index - 1); + wasNull.set(value == null); + return value; + } + + private ColumnInfo columnInfo(int index) + throws SQLException + { + checkOpen(); + checkValidRow(); + if ((index <= 0) || (index > columnInfoList.size())) { + throw new SQLException("Invalid column index: " + index); + } + return columnInfoList.get(index - 1); + } + + private Object column(String label) + throws SQLException + { + checkOpen(); + checkValidRow(); + Object value = row.get().get(columnIndex(label) - 1); + wasNull.set(value == null); + return value; + } + + private int columnIndex(String label) + throws SQLException + { + if (label == null) { + throw new SQLException("Column label is null"); + } + Integer index = fieldMap.get(label.toLowerCase(ENGLISH)); + if (index == null) { + throw new SQLException("Invalid column label: " + label); + } + return index; + } + + private static Number toNumber(Object value) + throws SQLException + { + if (value == null) { + return 0; + } + if (value instanceof Number) { + return (Number) value; + } + if (value instanceof Boolean) { + return ((Boolean) value) ? 1 : 0; + } + throw new SQLException("Value is not a number: " + value.getClass().getCanonicalName()); + } + + private static List getColumns(StatementClient client) + throws SQLException + { + while (client.isValid()) { + List columns = client.current().getColumns(); + if (columns != null) { + return columns; + } + client.advance(); + } + + QueryResults results = client.finalResults(); + if (!client.isFailed()) { + throw new SQLException(format("Query has no columns (#%s)", results.getId())); + } + throw resultsException(results); + } + + private static Iterator flatten(Iterator> iterator) + { + return concat(transform(iterator, Iterable::iterator)); + } + + private static class ResultsPageIterator + extends AbstractIterator>> + { + private final StatementClient client; + + private ResultsPageIterator(StatementClient client) + { + this.client = checkNotNull(client, "client is null"); + } + + @Override + protected Iterable> computeNext() + { + while (client.isValid()) { + if (Thread.currentThread().isInterrupted()) { + client.close(); + throw propagate(new SQLException("ResultSet thread was interrupted")); + } + + Iterable> data = client.current().getData(); + client.advance(); + if (data != null) { + return data; + } + } + + if (client.isFailed()) { + throw propagate(resultsException(client.finalResults())); + } + + return endOfData(); + } + } + + private static SQLException resultsException(QueryResults results) + { + QueryError error = requireNonNull(results.getError()); + String message = format("Query failed (#%s): %s", results.getId(), error.getMessage()); + Throwable cause = (error.getFailureInfo() == null) ? null : error.getFailureInfo().toException(); + return new SQLException(message, error.getSqlState(), error.getErrorCode(), cause); + } + + private static Map getFieldMap(List columns) + { + Map map = new HashMap<>(); + for (int i = 0; i < columns.size(); i++) { + String name = columns.get(i).getName().toLowerCase(ENGLISH); + if (!map.containsKey(name)) { + map.put(name, i + 1); + } + } + return ImmutableMap.copyOf(map); + } + + private static List getColumnInfo(List columns) + { + ImmutableList.Builder list = ImmutableList.builder(); + for (Column column : columns) { + ColumnInfo.Builder builder = new ColumnInfo.Builder() + .setCatalogName("") // TODO + .setSchemaName("") // TODO + .setTableName("") // TODO + .setColumnLabel(column.getName()) + .setColumnName(column.getName()) // TODO + .setColumnTypeSignature(parseTypeSignature(column.getType().toUpperCase(ENGLISH))) + .setNullable(Nullable.UNKNOWN) + .setCurrency(false); + setTypeInfo(builder, parseTypeSignature(column.getType())); + list.add(builder.build()); + } + return list.build(); + } +} diff --git a/presto-jdbc/src/main/java/com/facebook/presto/jdbc/PrestoResultSetMetaData.java b/presto-jdbc/src/main/java/com/facebook/presto/jdbc/PrestoResultSetMetaData.java new file mode 100644 index 00000000..fb184c07 --- /dev/null +++ b/presto-jdbc/src/main/java/com/facebook/presto/jdbc/PrestoResultSetMetaData.java @@ -0,0 +1,259 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.jdbc; + +import com.google.common.collect.ImmutableList; + +import java.math.BigDecimal; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.Date; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Time; +import java.sql.Timestamp; +import java.sql.Types; +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class PrestoResultSetMetaData + implements ResultSetMetaData +{ + private final List columnInfo; + + PrestoResultSetMetaData(List columnInfo) + { + this.columnInfo = ImmutableList.copyOf(checkNotNull(columnInfo, "columnInfo is null")); + } + + @Override + public int getColumnCount() + throws SQLException + { + return columnInfo.size(); + } + + @Override + public boolean isAutoIncrement(int column) + throws SQLException + { + return false; + } + + @Override + public boolean isCaseSensitive(int column) + throws SQLException + { + return false; + } + + @Override + public boolean isSearchable(int column) + throws SQLException + { + return true; + } + + @Override + public boolean isCurrency(int column) + throws SQLException + { + return column(column).isCurrency(); + } + + @Override + public int isNullable(int column) + throws SQLException + { + ColumnInfo.Nullable nullable = column(column).getNullable(); + switch (nullable) { + case NO_NULLS: + return columnNoNulls; + case NULLABLE: + return columnNullable; + case UNKNOWN: + return columnNullableUnknown; + } + throw new SQLException("Unhandled nullable type: " + nullable); + } + + @Override + public boolean isSigned(int column) + throws SQLException + { + return column(column).isSigned(); + } + + @Override + public int getColumnDisplaySize(int column) + throws SQLException + { + return column(column).getColumnDisplaySize(); + } + + @Override + public String getColumnLabel(int column) + throws SQLException + { + return column(column).getColumnLabel(); + } + + @Override + public String getColumnName(int column) + throws SQLException + { + return column(column).getColumnName(); + } + + @Override + public int getPrecision(int column) + throws SQLException + { + return column(column).getPrecision(); + } + + @Override + public int getScale(int column) + throws SQLException + { + return column(column).getScale(); + } + + @Override + public String getTableName(int column) + throws SQLException + { + return column(column).getTableName(); + } + + @Override + public String getSchemaName(int column) + throws SQLException + { + return column(column).getSchemaName(); + } + + @Override + public String getCatalogName(int column) + throws SQLException + { + return column(column).getCatalogName(); + } + + @Override + public int getColumnType(int column) + throws SQLException + { + return column(column).getColumnType(); + } + + @Override + public String getColumnTypeName(int column) + throws SQLException + { + return column(column).getColumnTypeName(); + } + + @Override + public boolean isReadOnly(int column) + throws SQLException + { + return true; + } + + @Override + public boolean isWritable(int column) + throws SQLException + { + return false; + } + + @Override + public boolean isDefinitelyWritable(int column) + throws SQLException + { + return false; + } + + @Override + public String getColumnClassName(int column) + throws SQLException + { + // see javax.sql.rowset.RowSetMetaDataImpl + switch (column(column).getColumnType()) { + case Types.NUMERIC: + case Types.DECIMAL: + return BigDecimal.class.getName(); + case Types.BOOLEAN: + case Types.BIT: + return Boolean.class.getName(); + case Types.TINYINT: + return Byte.class.getName(); + case Types.SMALLINT: + return Short.class.getName(); + case Types.INTEGER: + return Integer.class.getName(); + case Types.BIGINT: + return Long.class.getName(); + case Types.REAL: + return Float.class.getName(); + case Types.FLOAT: + case Types.DOUBLE: + return Double.class.getName(); + case Types.BINARY: + case Types.VARBINARY: + case Types.LONGVARBINARY: + return "byte[]"; + case Types.DATE: + return Date.class.getName(); + case Types.TIME: + return Time.class.getName(); + case Types.TIMESTAMP: + return Timestamp.class.getName(); + case Types.BLOB: + return Blob.class.getName(); + case Types.CLOB: + return Clob.class.getName(); + } + return String.class.getName(); + } + + @SuppressWarnings("unchecked") + @Override + public T unwrap(Class iface) + throws SQLException + { + if (isWrapperFor(iface)) { + return (T) this; + } + throw new SQLException("No wrapper for " + iface); + } + + @Override + public boolean isWrapperFor(Class iface) + throws SQLException + { + return iface.isInstance(this); + } + + private ColumnInfo column(int column) + throws SQLException + { + if ((column <= 0) || (column > columnInfo.size())) { + throw new SQLException("Invalid column index: " + column); + } + return columnInfo.get(column - 1); + } +} diff --git a/presto-jdbc/src/main/java/com/facebook/presto/jdbc/PrestoStatement.java b/presto-jdbc/src/main/java/com/facebook/presto/jdbc/PrestoStatement.java new file mode 100644 index 00000000..13a4119b --- /dev/null +++ b/presto-jdbc/src/main/java/com/facebook/presto/jdbc/PrestoStatement.java @@ -0,0 +1,446 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.jdbc; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.sql.SQLWarning; +import java.sql.Statement; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class PrestoStatement + implements Statement +{ + private final AtomicInteger maxRows = new AtomicInteger(); + private final AtomicInteger queryTimeoutSeconds = new AtomicInteger(); + private final AtomicInteger fetchSize = new AtomicInteger(); + private final AtomicBoolean escapeProcessing = new AtomicBoolean(true); + private final AtomicBoolean closeOnCompletion = new AtomicBoolean(); + private final AtomicReference connection; + private final AtomicReference currentResult = new AtomicReference<>(); + + PrestoStatement(PrestoConnection connection) + { + this.connection = new AtomicReference<>(checkNotNull(connection, "connection is null")); + } + + @Override + public ResultSet executeQuery(String sql) + throws SQLException + { + try { + ResultSet result = new PrestoResultSet(connection().startQuery(sql)); + currentResult.set(result); + return result; + } + catch (RuntimeException e) { + throw new SQLException("Error executing query", e); + } + } + + @Override + public int executeUpdate(String sql) + throws SQLException + { + throw new NotImplementedException("Statement", "executeUpdate"); + } + + @Override + public void close() + throws SQLException + { + if (connection.getAndSet(null) != null) { + // TODO + } + } + + @Override + public int getMaxFieldSize() + throws SQLException + { + checkOpen(); + return 0; + } + + @Override + public void setMaxFieldSize(int max) + throws SQLException + { + checkOpen(); + if (max < 0) { + throw new SQLException("Max field size must be positive"); + } + // ignore: full values are always returned + } + + @Override + public int getMaxRows() + throws SQLException + { + checkOpen(); + return maxRows.get(); + } + + @Override + public void setMaxRows(int max) + throws SQLException + { + checkOpen(); + if (max < 0) { + throw new SQLException("Max rows must be positive"); + } + maxRows.set(max); + } + + @Override + public void setEscapeProcessing(boolean enable) + throws SQLException + { + checkOpen(); + escapeProcessing.set(enable); + } + + @Override + public int getQueryTimeout() + throws SQLException + { + checkOpen(); + return queryTimeoutSeconds.get(); + } + + @Override + public void setQueryTimeout(int seconds) + throws SQLException + { + checkOpen(); + if (seconds < 0) { + throw new SQLException("Query timeout seconds must be positive"); + } + queryTimeoutSeconds.set(seconds); + } + + @Override + public void cancel() + throws SQLException + { + throw new SQLFeatureNotSupportedException("cancel"); + } + + @Override + public SQLWarning getWarnings() + throws SQLException + { + checkOpen(); + return null; + } + + @Override + public void clearWarnings() + throws SQLException + { + checkOpen(); + } + + @Override + public void setCursorName(String name) + throws SQLException + { + checkOpen(); + // ignore: positioned modifications not supported + } + + @Override + public boolean execute(String sql) + throws SQLException + { + checkOpen(); + // Only support returning a single result set + currentResult.set(executeQuery(sql)); + return true; + } + + @Override + public ResultSet getResultSet() + throws SQLException + { + checkOpen(); + return currentResult.get(); + } + + @Override + public int getUpdateCount() + throws SQLException + { + checkOpen(); + // Updates are not allowed yet so return -1 + return -1; + } + + @Override + public boolean getMoreResults() + throws SQLException + { + checkOpen(); + currentResult.get().close(); + return false; + } + + @Override + public void setFetchDirection(int direction) + throws SQLException + { + checkOpen(); + if (!validFetchDirection(direction)) { + throw new SQLException("Invalid fetch direction"); + } + // ignore: fetch direction is always forward + } + + @Override + public int getFetchDirection() + throws SQLException + { + checkOpen(); + return ResultSet.FETCH_FORWARD; + } + + @Override + public void setFetchSize(int rows) + throws SQLException + { + checkOpen(); + if (rows < 0) { + throw new SQLException("Fetch size must be positive"); + } + fetchSize.set(rows); + } + + @Override + public int getFetchSize() + throws SQLException + { + checkOpen(); + return fetchSize.get(); + } + + @Override + public int getResultSetConcurrency() + throws SQLException + { + checkOpen(); + return ResultSet.CONCUR_READ_ONLY; + } + + @Override + public int getResultSetType() + throws SQLException + { + checkOpen(); + return ResultSet.TYPE_FORWARD_ONLY; + } + + @Override + public void addBatch(String sql) + throws SQLException + { + checkOpen(); + throw new SQLFeatureNotSupportedException("Batches not supported"); + } + + @Override + public void clearBatch() + throws SQLException + { + checkOpen(); + throw new SQLFeatureNotSupportedException("Batches not supported"); + } + + @Override + public int[] executeBatch() + throws SQLException + { + checkOpen(); + throw new SQLFeatureNotSupportedException("Batches not supported"); + } + + @Override + public Connection getConnection() + throws SQLException + { + return connection(); + } + + @Override + public boolean getMoreResults(int current) + throws SQLException + { + checkOpen(); + + if (current == CLOSE_CURRENT_RESULT) { + currentResult.get().close(); + return false; + } + + if (current != KEEP_CURRENT_RESULT && current != CLOSE_ALL_RESULTS) { + throw new SQLException("Invalid argument: " + current); + } + + throw new SQLFeatureNotSupportedException("Multiple open results not supported"); + } + + @Override + public ResultSet getGeneratedKeys() + throws SQLException + { + throw new SQLFeatureNotSupportedException("getGeneratedKeys"); + } + + @Override + public int executeUpdate(String sql, int autoGeneratedKeys) + throws SQLException + { + throw new SQLFeatureNotSupportedException("executeUpdate"); + } + + @Override + public int executeUpdate(String sql, int[] columnIndexes) + throws SQLException + { + throw new SQLFeatureNotSupportedException("executeUpdate"); + } + + @Override + public int executeUpdate(String sql, String[] columnNames) + throws SQLException + { + throw new SQLFeatureNotSupportedException("executeUpdate"); + } + + @Override + public boolean execute(String sql, int autoGeneratedKeys) + throws SQLException + { + throw new SQLFeatureNotSupportedException("execute"); + } + + @Override + public boolean execute(String sql, int[] columnIndexes) + throws SQLException + { + throw new SQLFeatureNotSupportedException("execute"); + } + + @Override + public boolean execute(String sql, String[] columnNames) + throws SQLException + { + throw new SQLFeatureNotSupportedException("execute"); + } + + @Override + public int getResultSetHoldability() + throws SQLException + { + return ResultSet.CLOSE_CURSORS_AT_COMMIT; + } + + @Override + public boolean isClosed() + throws SQLException + { + return connection.get() == null; + } + + @Override + public void setPoolable(boolean poolable) + throws SQLException + { + checkOpen(); + // ignore: statement pooling not supported + } + + @Override + public boolean isPoolable() + throws SQLException + { + checkOpen(); + return false; + } + + @Override + public void closeOnCompletion() + throws SQLException + { + checkOpen(); + closeOnCompletion.set(true); + } + + @Override + public boolean isCloseOnCompletion() + throws SQLException + { + checkOpen(); + return closeOnCompletion.get(); + } + + @SuppressWarnings("unchecked") + @Override + public T unwrap(Class iface) + throws SQLException + { + if (isWrapperFor(iface)) { + return (T) this; + } + throw new SQLException("No wrapper for " + iface); + } + + @Override + public boolean isWrapperFor(Class iface) + throws SQLException + { + return iface.isInstance(this); + } + + private void checkOpen() + throws SQLException + { + connection(); + } + + private PrestoConnection connection() + throws SQLException + { + PrestoConnection connection = this.connection.get(); + if (connection == null) { + throw new SQLException("Statement is closed"); + } + if (connection.isClosed()) { + throw new SQLException("Connection is closed"); + } + return connection; + } + + private static boolean validFetchDirection(int direction) + { + return (direction == ResultSet.FETCH_FORWARD) || + (direction == ResultSet.FETCH_REVERSE) || + (direction == ResultSet.FETCH_UNKNOWN); + } +} diff --git a/presto-jdbc/src/main/java/com/facebook/presto/jdbc/QueryExecutor.java b/presto-jdbc/src/main/java/com/facebook/presto/jdbc/QueryExecutor.java new file mode 100644 index 00000000..091e110b --- /dev/null +++ b/presto-jdbc/src/main/java/com/facebook/presto/jdbc/QueryExecutor.java @@ -0,0 +1,99 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.jdbc; + +import com.facebook.presto.client.ClientSession; +import com.facebook.presto.client.QueryResults; +import com.facebook.presto.client.StatementClient; +import com.google.common.collect.ImmutableSet; +import com.google.common.net.HostAndPort; +import io.airlift.http.client.HttpClient; +import io.airlift.http.client.HttpClientConfig; +import io.airlift.http.client.jetty.JettyHttpClient; +import io.airlift.http.client.jetty.JettyIoPool; +import io.airlift.http.client.jetty.JettyIoPoolConfig; +import io.airlift.json.JsonCodec; +import io.airlift.units.Duration; + +import javax.annotation.Nullable; + +import java.io.Closeable; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.ProxySelector; +import java.net.URI; +import java.util.concurrent.TimeUnit; + +import static com.google.common.base.Preconditions.checkNotNull; +import static io.airlift.json.JsonCodec.jsonCodec; + +class QueryExecutor + implements Closeable +{ + private final JsonCodec queryInfoCodec; + private final HttpClient httpClient; + + private QueryExecutor(String userAgent, JsonCodec queryResultsCodec, HostAndPort socksProxy) + { + checkNotNull(userAgent, "userAgent is null"); + checkNotNull(queryResultsCodec, "queryResultsCodec is null"); + + this.queryInfoCodec = queryResultsCodec; + this.httpClient = new JettyHttpClient( + new HttpClientConfig() + .setConnectTimeout(new Duration(10, TimeUnit.SECONDS)) + .setSocksProxy(socksProxy), + new JettyIoPool("presto-jdbc", new JettyIoPoolConfig()), + ImmutableSet.of(new UserAgentRequestFilter(userAgent))); + } + + public StatementClient startQuery(ClientSession session, String query) + { + return new StatementClient(httpClient, queryInfoCodec, session, query); + } + + @Override + public void close() + { + httpClient.close(); + } + + // TODO: replace this with a phantom reference + @SuppressWarnings("FinalizeDeclaration") + @Override + protected void finalize() + { + close(); + } + + static QueryExecutor create(String userAgent) + { + return new QueryExecutor(userAgent, jsonCodec(QueryResults.class), getSystemSocksProxy()); + } + + @Nullable + private static HostAndPort getSystemSocksProxy() + { + URI uri = URI.create("socket://0.0.0.0:80"); + for (Proxy proxy : ProxySelector.getDefault().select(uri)) { + if (proxy.type() == Proxy.Type.SOCKS) { + if (proxy.address() instanceof InetSocketAddress) { + InetSocketAddress address = (InetSocketAddress) proxy.address(); + return HostAndPort.fromParts(address.getHostString(), address.getPort()); + } + } + } + return null; + } +} diff --git a/presto-jdbc/src/main/java/com/facebook/presto/jdbc/UserAgentRequestFilter.java b/presto-jdbc/src/main/java/com/facebook/presto/jdbc/UserAgentRequestFilter.java new file mode 100644 index 00000000..2b0173d7 --- /dev/null +++ b/presto-jdbc/src/main/java/com/facebook/presto/jdbc/UserAgentRequestFilter.java @@ -0,0 +1,40 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.jdbc; + +import com.google.common.net.HttpHeaders; +import io.airlift.http.client.HttpRequestFilter; +import io.airlift.http.client.Request; + +import static com.google.common.base.Preconditions.checkNotNull; +import static io.airlift.http.client.Request.Builder.fromRequest; + +class UserAgentRequestFilter + implements HttpRequestFilter +{ + private final String userAgent; + + public UserAgentRequestFilter(String userAgent) + { + this.userAgent = checkNotNull(userAgent, "userAgent is null"); + } + + @Override + public Request filterRequest(Request request) + { + return fromRequest(request) + .addHeader(HttpHeaders.USER_AGENT, userAgent) + .build(); + } +} diff --git a/presto-jdbc/src/main/resources/META-INF/license/LICENSE.jol.txt b/presto-jdbc/src/main/resources/META-INF/license/LICENSE.jol.txt new file mode 100644 index 00000000..b40a0f45 --- /dev/null +++ b/presto-jdbc/src/main/resources/META-INF/license/LICENSE.jol.txt @@ -0,0 +1,347 @@ +The GNU General Public License (GPL) + +Version 2, June 1991 + +Copyright (C) 1989, 1991 Free Software Foundation, Inc. +59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +Preamble + +The licenses for most software are designed to take away your freedom to share +and change it. By contrast, the GNU General Public License is intended to +guarantee your freedom to share and change free software--to make sure the +software is free for all its users. This General Public License applies to +most of the Free Software Foundation's software and to any other program whose +authors commit to using it. (Some other Free Software Foundation software is +covered by the GNU Library General Public License instead.) You can apply it to +your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our +General Public Licenses are designed to make sure that you have the freedom to +distribute copies of free software (and charge for this service if you wish), +that you receive source code or can get it if you want it, that you can change +the software or use pieces of it in new free programs; and that you know you +can do these things. + +To protect your rights, we need to make restrictions that forbid anyone to deny +you these rights or to ask you to surrender the rights. These restrictions +translate to certain responsibilities for you if you distribute copies of the +software, or if you modify it. + +For example, if you distribute copies of such a program, whether gratis or for +a fee, you must give the recipients all the rights that you have. You must +make sure that they, too, receive or can get the source code. And you must +show them these terms so they know their rights. + +We protect your rights with two steps: (1) copyright the software, and (2) +offer you this license which gives you legal permission to copy, distribute +and/or modify the software. + +Also, for each author's protection and ours, we want to make certain that +everyone understands that there is no warranty for this free software. If the +software is modified by someone else and passed on, we want its recipients to +know that what they have is not the original, so that any problems introduced +by others will not reflect on the original authors' reputations. + +Finally, any free program is threatened constantly by software patents. We +wish to avoid the danger that redistributors of a free program will +individually obtain patent licenses, in effect making the program proprietary. +To prevent this, we have made it clear that any patent must be licensed for +everyone's free use or not licensed at all. + +The precise terms and conditions for copying, distribution and modification +follow. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License applies to any program or other work which contains a notice +placed by the copyright holder saying it may be distributed under the terms of +this General Public License. The "Program", below, refers to any such program +or work, and a "work based on the Program" means either the Program or any +derivative work under copyright law: that is to say, a work containing the +Program or a portion of it, either verbatim or with modifications and/or +translated into another language. (Hereinafter, translation is included +without limitation in the term "modification".) Each licensee is addressed as +"you". + +Activities other than copying, distribution and modification are not covered by +this License; they are outside its scope. The act of running the Program is +not restricted, and the output from the Program is covered only if its contents +constitute a work based on the Program (independent of having been made by +running the Program). Whether that is true depends on what the Program does. + +1. You may copy and distribute verbatim copies of the Program's source code as +you receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice and +disclaimer of warranty; keep intact all the notices that refer to this License +and to the absence of any warranty; and give any other recipients of the +Program a copy of this License along with the Program. + +You may charge a fee for the physical act of transferring a copy, and you may +at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Program or any portion of it, thus +forming a work based on the Program, and copy and distribute such modifications +or work under the terms of Section 1 above, provided that you also meet all of +these conditions: + + a) You must cause the modified files to carry prominent notices stating + that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in whole or + in part contains or is derived from the Program or any part thereof, to be + licensed as a whole at no charge to all third parties under the terms of + this License. + + c) If the modified program normally reads commands interactively when run, + you must cause it, when started running for such interactive use in the + most ordinary way, to print or display an announcement including an + appropriate copyright notice and a notice that there is no warranty (or + else, saying that you provide a warranty) and that users may redistribute + the program under these conditions, and telling the user how to view a copy + of this License. (Exception: if the Program itself is interactive but does + not normally print such an announcement, your work based on the Program is + not required to print an announcement.) + +These requirements apply to the modified work as a whole. If identifiable +sections of that work are not derived from the Program, and can be reasonably +considered independent and separate works in themselves, then this License, and +its terms, do not apply to those sections when you distribute them as separate +works. But when you distribute the same sections as part of a whole which is a +work based on the Program, the distribution of the whole must be on the terms +of this License, whose permissions for other licensees extend to the entire +whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest your +rights to work written entirely by you; rather, the intent is to exercise the +right to control the distribution of derivative or collective works based on +the Program. + +In addition, mere aggregation of another work not based on the Program with the +Program (or with a work based on the Program) on a volume of a storage or +distribution medium does not bring the other work under the scope of this +License. + +3. You may copy and distribute the Program (or a work based on it, under +Section 2) in object code or executable form under the terms of Sections 1 and +2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable source + code, which must be distributed under the terms of Sections 1 and 2 above + on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three years, to + give any third party, for a charge no more than your cost of physically + performing source distribution, a complete machine-readable copy of the + corresponding source code, to be distributed under the terms of Sections 1 + and 2 above on a medium customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer to + distribute corresponding source code. (This alternative is allowed only + for noncommercial distribution and only if you received the program in + object code or executable form with such an offer, in accord with + Subsection b above.) + +The source code for a work means the preferred form of the work for making +modifications to it. For an executable work, complete source code means all +the source code for all modules it contains, plus any associated interface +definition files, plus the scripts used to control compilation and installation +of the executable. However, as a special exception, the source code +distributed need not include anything that is normally distributed (in either +source or binary form) with the major components (compiler, kernel, and so on) +of the operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the source +code from the same place counts as distribution of the source code, even though +third parties are not compelled to copy the source along with the object code. + +4. You may not copy, modify, sublicense, or distribute the Program except as +expressly provided under this License. Any attempt otherwise to copy, modify, +sublicense or distribute the Program is void, and will automatically terminate +your rights under this License. However, parties who have received copies, or +rights, from you under this License will not have their licenses terminated so +long as such parties remain in full compliance. + +5. You are not required to accept this License, since you have not signed it. +However, nothing else grants you permission to modify or distribute the Program +or its derivative works. These actions are prohibited by law if you do not +accept this License. Therefore, by modifying or distributing the Program (or +any work based on the Program), you indicate your acceptance of this License to +do so, and all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + +6. Each time you redistribute the Program (or any work based on the Program), +the recipient automatically receives a license from the original licensor to +copy, distribute or modify the Program subject to these terms and conditions. +You may not impose any further restrictions on the recipients' exercise of the +rights granted herein. You are not responsible for enforcing compliance by +third parties to this License. + +7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), conditions +are imposed on you (whether by court order, agreement or otherwise) that +contradict the conditions of this License, they do not excuse you from the +conditions of this License. If you cannot distribute so as to satisfy +simultaneously your obligations under this License and any other pertinent +obligations, then as a consequence you may not distribute the Program at all. +For example, if a patent license would not permit royalty-free redistribution +of the Program by all those who receive copies directly or indirectly through +you, then the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply and +the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents or +other property right claims or to contest validity of any such claims; this +section has the sole purpose of protecting the integrity of the free software +distribution system, which is implemented by public license practices. Many +people have made generous contributions to the wide range of software +distributed through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing to +distribute software through any other system and a licensee cannot impose that +choice. + +This section is intended to make thoroughly clear what is believed to be a +consequence of the rest of this License. + +8. If the distribution and/or use of the Program is restricted in certain +countries either by patents or by copyrighted interfaces, the original +copyright holder who places the Program under this License may add an explicit +geographical distribution limitation excluding those countries, so that +distribution is permitted only in or among countries not thus excluded. In +such case, this License incorporates the limitation as if written in the body +of this License. + +9. The Free Software Foundation may publish revised and/or new versions of the +General Public License from time to time. Such new versions will be similar in +spirit to the present version, but may differ in detail to address new problems +or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any later +version", you have the option of following the terms and conditions either of +that version or of any later version published by the Free Software Foundation. +If the Program does not specify a version number of this License, you may +choose any version ever published by the Free Software Foundation. + +10. If you wish to incorporate parts of the Program into other free programs +whose distribution conditions are different, write to the author to ask for +permission. For software which is copyrighted by the Free Software Foundation, +write to the Free Software Foundation; we sometimes make exceptions for this. +Our decision will be guided by the two goals of preserving the free status of +all derivatives of our free software and of promoting the sharing and reuse of +software generally. + +NO WARRANTY + +11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR +THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE +STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE +PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND +PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, +YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL +ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE +PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR +INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA +BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER +OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible +use to the public, the best way to achieve this is to make it free software +which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach +them to the start of each source file to most effectively convey the exclusion +of warranty; and each file should have at least the "copyright" line and a +pointer to where the full notice is found. + + One line to give the program's name and a brief idea of what it does. + + Copyright (C) + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by the Free + Software Foundation; either version 2 of the License, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., 59 + Temple Place, Suite 330, Boston, MA 02111-1307 USA + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this when it +starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author Gnomovision comes + with ABSOLUTELY NO WARRANTY; for details type 'show w'. This is free + software, and you are welcome to redistribute it under certain conditions; + type 'show c' for details. + +The hypothetical commands 'show w' and 'show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may be +called something other than 'show w' and 'show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your school, +if any, to sign a "copyright disclaimer" for the program, if necessary. Here +is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + 'Gnomovision' (which makes passes at compilers) written by James Hacker. + + signature of Ty Coon, 1 April 1989 + + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General Public +License instead of this License. + + +"CLASSPATH" EXCEPTION TO THE GPL + +Certain source files distributed by Oracle America and/or its affiliates are +subject to the following clarification and special exception to the GPL, but +only where Oracle has expressly included in the particular source file's header +the words "Oracle designates this particular file as subject to the "Classpath" +exception as provided by Oracle in the LICENSE file that accompanied this code." + + Linking this library statically or dynamically with other modules is making + a combined work based on this library. Thus, the terms and conditions of + the GNU General Public License cover the whole combination. + + As a special exception, the copyright holders of this library give you + permission to link this library with independent modules to produce an + executable, regardless of the license terms of these independent modules, + and to copy and distribute the resulting executable under terms of your + choice, provided that you also meet, for each linked independent module, + the terms and conditions of the license of that module. An independent + module is a module which is not derived from or based on this library. If + you modify this library, you may extend this exception to your version of + the library, but you are not obligated to do so. If you do not wish to do + so, delete this exception statement from your version. diff --git a/presto-jdbc/src/main/resources/META-INF/services/java.sql.Driver b/presto-jdbc/src/main/resources/META-INF/services/java.sql.Driver new file mode 100644 index 00000000..c56d8077 --- /dev/null +++ b/presto-jdbc/src/main/resources/META-INF/services/java.sql.Driver @@ -0,0 +1 @@ +com.facebook.presto.jdbc.PrestoDriver diff --git a/presto-jdbc/src/test/java/com/facebook/presto/jdbc/TestDriver.java b/presto-jdbc/src/test/java/com/facebook/presto/jdbc/TestDriver.java new file mode 100644 index 00000000..6dc64f44 --- /dev/null +++ b/presto-jdbc/src/test/java/com/facebook/presto/jdbc/TestDriver.java @@ -0,0 +1,993 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.jdbc; + +import com.facebook.presto.server.testing.TestingPrestoServer; +import com.facebook.presto.tpch.TpchPlugin; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import io.airlift.log.Logging; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import java.sql.Connection; +import java.sql.Date; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.sql.Statement; +import java.sql.Time; +import java.sql.Timestamp; +import java.sql.Types; +import java.util.ArrayList; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.Set; + +import static com.facebook.presto.server.testing.TestingPrestoServer.TEST_CATALOG; +import static io.airlift.testing.Assertions.assertInstanceOf; +import static java.lang.String.format; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.fail; + +public class TestDriver +{ + private static final DateTimeZone ASIA_ORAL_ZONE = DateTimeZone.forID("Asia/Oral"); + private static final GregorianCalendar ASIA_ORAL_CALENDAR = new GregorianCalendar(ASIA_ORAL_ZONE.toTimeZone()); + + private TestingPrestoServer server; + + @BeforeClass + public void setup() + throws Exception + { + Logging.initialize(); + server = new TestingPrestoServer(); + server.installPlugin(new TpchPlugin()); + server.createCatalog("default", "tpch"); // TODO: change catalog name + } + + @AfterClass + public void teardown() + { + closeQuietly(server); + } + + @Test + public void testDriverManager() + throws Exception + { + try (Connection connection = createConnection()) { + try (Statement statement = connection.createStatement()) { + try (ResultSet rs = statement.executeQuery("" + + "SELECT " + + " 123 _bigint" + + ", 'foo' _varchar" + + ", 0.1 _double" + + ", true _boolean" + + ", cast('hello' as varbinary) _varbinary" + + ", approx_set(42) _hll")) { + ResultSetMetaData metadata = rs.getMetaData(); + + assertEquals(metadata.getColumnCount(), 6); + + assertEquals(metadata.getColumnLabel(1), "_bigint"); + assertEquals(metadata.getColumnType(1), Types.BIGINT); + + assertEquals(metadata.getColumnLabel(2), "_varchar"); + assertEquals(metadata.getColumnType(2), Types.LONGNVARCHAR); + + assertEquals(metadata.getColumnLabel(3), "_double"); + assertEquals(metadata.getColumnType(3), Types.DOUBLE); + + assertEquals(metadata.getColumnLabel(4), "_boolean"); + assertEquals(metadata.getColumnType(4), Types.BOOLEAN); + + assertEquals(metadata.getColumnLabel(5), "_varbinary"); + assertEquals(metadata.getColumnType(5), Types.LONGVARBINARY); + + assertEquals(metadata.getColumnLabel(6), "_hll"); + assertEquals(metadata.getColumnType(6), Types.JAVA_OBJECT); + + assertTrue(rs.next()); + + assertEquals(rs.getObject(1), 123L); + assertEquals(rs.getObject("_bigint"), 123L); + assertEquals(rs.getLong(1), 123); + assertEquals(rs.getLong("_bigint"), 123); + + assertEquals(rs.getObject(2), "foo"); + assertEquals(rs.getObject("_varchar"), "foo"); + assertEquals(rs.getString(2), "foo"); + assertEquals(rs.getString("_varchar"), "foo"); + + assertEquals(rs.getObject(3), 0.1); + assertEquals(rs.getObject("_double"), 0.1); + assertEquals(rs.getDouble(3), 0.1); + assertEquals(rs.getDouble("_double"), 0.1); + + assertEquals(rs.getObject(4), true); + assertEquals(rs.getObject("_boolean"), true); + assertEquals(rs.getBoolean(4), true); + assertEquals(rs.getBoolean("_boolean"), true); + + assertEquals(rs.getObject(5), "hello".getBytes(UTF_8)); + assertEquals(rs.getObject("_varbinary"), "hello".getBytes(UTF_8)); + assertEquals(rs.getBytes(5), "hello".getBytes(UTF_8)); + assertEquals(rs.getBytes("_varbinary"), "hello".getBytes(UTF_8)); + + assertInstanceOf(rs.getObject(6), byte[].class); + assertInstanceOf(rs.getObject("_hll"), byte[].class); + assertInstanceOf(rs.getBytes(6), byte[].class); + assertInstanceOf(rs.getBytes("_hll"), byte[].class); + + assertFalse(rs.next()); + } + } + } + } + + @Test + public void testTypes() + throws Exception + { + try (Connection connection = createConnection()) { + try (Statement statement = connection.createStatement()) { + try (ResultSet rs = statement.executeQuery("SELECT " + + " TIME '3:04:05' as a" + + ", TIME '6:07:08 +06:17' as b" + + ", TIME '9:10:11 Europe/Berlin' as c" + + ", TIMESTAMP '2001-02-03 3:04:05' as d" + + ", TIMESTAMP '2004-05-06 6:07:08 +06:17' as e" + + ", TIMESTAMP '2007-08-09 9:10:11 Europe/Berlin' as f" + + ", DATE '2013-03-22' as g" + + ", INTERVAL '123-11' YEAR TO MONTH as h" + + ", INTERVAL '11 22:33:44.555' DAY TO SECOND as i" + + "")) { + assertTrue(rs.next()); + + assertEquals(rs.getTime(1), new Time(new DateTime(1970, 1, 1, 3, 4, 5).getMillis())); + assertEquals(rs.getTime(1, ASIA_ORAL_CALENDAR), new Time(new DateTime(1970, 1, 1, 3, 4, 5, ASIA_ORAL_ZONE).getMillis())); + assertEquals(rs.getObject(1), new Time(new DateTime(1970, 1, 1, 3, 4, 5).getMillis())); + assertEquals(rs.getTime("a"), new Time(new DateTime(1970, 1, 1, 3, 4, 5).getMillis())); + assertEquals(rs.getTime("a", ASIA_ORAL_CALENDAR), new Time(new DateTime(1970, 1, 1, 3, 4, 5, ASIA_ORAL_ZONE).getMillis())); + assertEquals(rs.getObject("a"), new Time(new DateTime(1970, 1, 1, 3, 4, 5).getMillis())); + + assertEquals(rs.getTime(2), new Time(new DateTime(1970, 1, 1, 6, 7, 8, DateTimeZone.forOffsetHoursMinutes(6, 17)).getMillis())); + assertEquals(rs.getTime(2, ASIA_ORAL_CALENDAR), new Time(new DateTime(1970, 1, 1, 6, 7, 8, DateTimeZone.forOffsetHoursMinutes(6, 17)).getMillis())); + assertEquals(rs.getObject(2), new Time(new DateTime(1970, 1, 1, 6, 7, 8, DateTimeZone.forOffsetHoursMinutes(6, 17)).getMillis())); + assertEquals(rs.getTime("b"), new Time(new DateTime(1970, 1, 1, 6, 7, 8, DateTimeZone.forOffsetHoursMinutes(6, 17)).getMillis())); + assertEquals(rs.getTime("b", ASIA_ORAL_CALENDAR), new Time(new DateTime(1970, 1, 1, 6, 7, 8, DateTimeZone.forOffsetHoursMinutes(6, 17)).getMillis())); + assertEquals(rs.getObject("b"), new Time(new DateTime(1970, 1, 1, 6, 7, 8, DateTimeZone.forOffsetHoursMinutes(6, 17)).getMillis())); + + assertEquals(rs.getTime(3), new Time(new DateTime(1970, 1, 1, 9, 10, 11, DateTimeZone.forID("Europe/Berlin")).getMillis())); + assertEquals(rs.getTime(3, ASIA_ORAL_CALENDAR), new Time(new DateTime(1970, 1, 1, 9, 10, 11, DateTimeZone.forID("Europe/Berlin")).getMillis())); + assertEquals(rs.getObject(3), new Time(new DateTime(1970, 1, 1, 9, 10, 11, DateTimeZone.forID("Europe/Berlin")).getMillis())); + assertEquals(rs.getTime("c"), new Time(new DateTime(1970, 1, 1, 9, 10, 11, DateTimeZone.forID("Europe/Berlin")).getMillis())); + assertEquals(rs.getTime("c", ASIA_ORAL_CALENDAR), new Time(new DateTime(1970, 1, 1, 9, 10, 11, DateTimeZone.forID("Europe/Berlin")).getMillis())); + assertEquals(rs.getObject("c"), new Time(new DateTime(1970, 1, 1, 9, 10, 11, DateTimeZone.forID("Europe/Berlin")).getMillis())); + + assertEquals(rs.getTimestamp(4), new Timestamp(new DateTime(2001, 2, 3, 3, 4, 5).getMillis())); + assertEquals(rs.getTimestamp(4, ASIA_ORAL_CALENDAR), new Timestamp(new DateTime(2001, 2, 3, 3, 4, 5, ASIA_ORAL_ZONE).getMillis())); + assertEquals(rs.getObject(4), new Timestamp(new DateTime(2001, 2, 3, 3, 4, 5).getMillis())); + assertEquals(rs.getTimestamp("d"), new Timestamp(new DateTime(2001, 2, 3, 3, 4, 5).getMillis())); + assertEquals(rs.getTimestamp("d", ASIA_ORAL_CALENDAR), new Timestamp(new DateTime(2001, 2, 3, 3, 4, 5, ASIA_ORAL_ZONE).getMillis())); + assertEquals(rs.getObject("d"), new Timestamp(new DateTime(2001, 2, 3, 3, 4, 5).getMillis())); + + assertEquals(rs.getTimestamp(5), new Timestamp(new DateTime(2004, 5, 6, 6, 7, 8, DateTimeZone.forOffsetHoursMinutes(6, 17)).getMillis())); + assertEquals(rs.getTimestamp(5, ASIA_ORAL_CALENDAR), new Timestamp(new DateTime(2004, 5, 6, 6, 7, 8, DateTimeZone.forOffsetHoursMinutes(6, 17)).getMillis())); + assertEquals(rs.getObject(5), new Timestamp(new DateTime(2004, 5, 6, 6, 7, 8, DateTimeZone.forOffsetHoursMinutes(6, 17)).getMillis())); + assertEquals(rs.getTimestamp("e"), new Timestamp(new DateTime(2004, 5, 6, 6, 7, 8, DateTimeZone.forOffsetHoursMinutes(6, 17)).getMillis())); + assertEquals(rs.getTimestamp("e", ASIA_ORAL_CALENDAR), new Timestamp(new DateTime(2004, 5, 6, 6, 7, 8, DateTimeZone.forOffsetHoursMinutes(6, 17)).getMillis())); + assertEquals(rs.getObject("e"), new Timestamp(new DateTime(2004, 5, 6, 6, 7, 8, DateTimeZone.forOffsetHoursMinutes(6, 17)).getMillis())); + + assertEquals(rs.getTimestamp(6), new Timestamp(new DateTime(2007, 8, 9, 9, 10, 11, DateTimeZone.forID("Europe/Berlin")).getMillis())); + assertEquals(rs.getTimestamp(6, ASIA_ORAL_CALENDAR), new Timestamp(new DateTime(2007, 8, 9, 9, 10, 11, DateTimeZone.forID("Europe/Berlin")).getMillis())); + assertEquals(rs.getObject(6), new Timestamp(new DateTime(2007, 8, 9, 9, 10, 11, DateTimeZone.forID("Europe/Berlin")).getMillis())); + assertEquals(rs.getTimestamp("f"), new Timestamp(new DateTime(2007, 8, 9, 9, 10, 11, DateTimeZone.forID("Europe/Berlin")).getMillis())); + assertEquals(rs.getTimestamp("f", ASIA_ORAL_CALENDAR), new Timestamp(new DateTime(2007, 8, 9, 9, 10, 11, DateTimeZone.forID("Europe/Berlin")).getMillis())); + assertEquals(rs.getObject("f"), new Timestamp(new DateTime(2007, 8, 9, 9, 10, 11, DateTimeZone.forID("Europe/Berlin")).getMillis())); + + assertEquals(rs.getDate(7), new Date(new DateTime(2013, 3, 22, 0, 0).getMillis())); + assertEquals(rs.getDate(7, ASIA_ORAL_CALENDAR), new Date(new DateTime(2013, 3, 22, 0, 0, ASIA_ORAL_ZONE).getMillis())); + assertEquals(rs.getObject(7), new Date(new DateTime(2013, 3, 22, 0, 0).getMillis())); + assertEquals(rs.getDate("g"), new Date(new DateTime(2013, 3, 22, 0, 0).getMillis())); + assertEquals(rs.getDate("g", ASIA_ORAL_CALENDAR), new Date(new DateTime(2013, 3, 22, 0, 0, ASIA_ORAL_ZONE).getMillis())); + assertEquals(rs.getObject("g"), new Date(new DateTime(2013, 3, 22, 0, 0).getMillis())); + + assertEquals(rs.getObject(8), new PrestoIntervalYearMonth(123, 11)); + assertEquals(rs.getObject("h"), new PrestoIntervalYearMonth(123, 11)); + assertEquals(rs.getObject(9), new PrestoIntervalDayTime(11, 22, 33, 44, 555)); + assertEquals(rs.getObject("i"), new PrestoIntervalDayTime(11, 22, 33, 44, 555)); + + assertFalse(rs.next()); + } + } + } + } + + @Test + public void testGetCatalogs() + throws Exception + { + try (Connection connection = createConnection()) { + try (ResultSet rs = connection.getMetaData().getCatalogs()) { + assertRowCount(rs, 1); + ResultSetMetaData metadata = rs.getMetaData(); + assertEquals(metadata.getColumnCount(), 1); + assertEquals(metadata.getColumnLabel(1), "TABLE_CAT"); + assertEquals(metadata.getColumnType(1), Types.LONGNVARCHAR); + } + } + } + + @Test + public void testGetSchemas() + throws Exception + { + try (Connection connection = createConnection()) { + try (ResultSet rs = connection.getMetaData().getSchemas()) { + assertGetSchemasResult(rs, 10); + } + + try (ResultSet rs = connection.getMetaData().getSchemas(null, null)) { + assertGetSchemasResult(rs, 10); + } + + try (ResultSet rs = connection.getMetaData().getSchemas(TEST_CATALOG, null)) { + assertGetSchemasResult(rs, 10); + } + + try (ResultSet rs = connection.getMetaData().getSchemas("", null)) { + // all schemas in presto have a catalog name + assertGetSchemasResult(rs, 0); + } + + try (ResultSet rs = connection.getMetaData().getSchemas(TEST_CATALOG, "information_schema")) { + assertGetSchemasResult(rs, 1); + } + + try (ResultSet rs = connection.getMetaData().getSchemas(null, "information_schema")) { + assertGetSchemasResult(rs, 1); + } + + try (ResultSet rs = connection.getMetaData().getSchemas(null, "sf_")) { + assertGetSchemasResult(rs, 1); + } + + try (ResultSet rs = connection.getMetaData().getSchemas(null, "sf%")) { + assertGetSchemasResult(rs, 8); + } + + try (ResultSet rs = connection.getMetaData().getSchemas("unknown", null)) { + assertGetSchemasResult(rs, 0); + } + + try (ResultSet rs = connection.getMetaData().getSchemas(null, "unknown")) { + assertGetSchemasResult(rs, 0); + } + + try (ResultSet rs = connection.getMetaData().getSchemas(TEST_CATALOG, "unknown")) { + assertGetSchemasResult(rs, 0); + } + + try (ResultSet rs = connection.getMetaData().getSchemas("unknown", "unknown")) { + assertGetSchemasResult(rs, 0); + } + } + } + + private static void assertGetSchemasResult(ResultSet rs, int expectedRows) + throws SQLException + { + assertRowCount(rs, expectedRows); + + ResultSetMetaData metadata = rs.getMetaData(); + assertEquals(metadata.getColumnCount(), 2); + + assertEquals(metadata.getColumnLabel(1), "TABLE_SCHEM"); + assertEquals(metadata.getColumnType(1), Types.LONGNVARCHAR); + + assertEquals(metadata.getColumnLabel(2), "TABLE_CATALOG"); + assertEquals(metadata.getColumnType(2), Types.LONGNVARCHAR); + } + + @Test + public void testGetTables() + throws Exception + { + try (Connection connection = createConnection()) { + try (ResultSet rs = connection.getMetaData().getTables(null, null, null, null)) { + assertTableMetadata(rs); + + Set> rows = ImmutableSet.copyOf(readRows(rs)); + assertTrue(rows.contains(getTablesRow("information_schema", "tables"))); + assertTrue(rows.contains(getTablesRow("information_schema", "schemata"))); + } + } + + try (Connection connection = createConnection()) { + try (ResultSet rs = connection.getMetaData().getTables(TEST_CATALOG, null, null, null)) { + assertTableMetadata(rs); + + Set> rows = ImmutableSet.copyOf(readRows(rs)); + assertTrue(rows.contains(getTablesRow("information_schema", "tables"))); + assertTrue(rows.contains(getTablesRow("information_schema", "schemata"))); + } + } + + try (Connection connection = createConnection()) { + try (ResultSet rs = connection.getMetaData().getTables("", null, null, null)) { + assertTableMetadata(rs); + + // all tables in presto have a catalog name + Set> rows = ImmutableSet.copyOf(readRows(rs)); + assertEquals(rows.size(), 0); + } + } + + try (Connection connection = createConnection()) { + try (ResultSet rs = connection.getMetaData().getTables(TEST_CATALOG, "information_schema", null, null)) { + assertTableMetadata(rs); + + Set> rows = ImmutableSet.copyOf(readRows(rs)); + assertTrue(rows.contains(getTablesRow("information_schema", "tables"))); + assertTrue(rows.contains(getTablesRow("information_schema", "schemata"))); + } + } + + try (Connection connection = createConnection()) { + try (ResultSet rs = connection.getMetaData().getTables(TEST_CATALOG, "", null, null)) { + assertTableMetadata(rs); + + Set> rows = ImmutableSet.copyOf(readRows(rs)); + assertEquals(rows.size(), 0); + } + } + + try (Connection connection = createConnection()) { + try (ResultSet rs = connection.getMetaData().getTables(TEST_CATALOG, "information_schema", "tables", null)) { + assertTableMetadata(rs); + + Set> rows = ImmutableSet.copyOf(readRows(rs)); + assertTrue(rows.contains(getTablesRow("information_schema", "tables"))); + assertFalse(rows.contains(getTablesRow("information_schema", "schemata"))); + } + } + + try (Connection connection = createConnection()) { + try (ResultSet rs = connection.getMetaData().getTables(TEST_CATALOG, "information_schema", "tables", new String[] {"BASE TABLE"})) { + assertTableMetadata(rs); + + Set> rows = ImmutableSet.copyOf(readRows(rs)); + assertTrue(rows.contains(getTablesRow("information_schema", "tables"))); + assertFalse(rows.contains(getTablesRow("information_schema", "schemata"))); + } + } + + try (Connection connection = createConnection()) { + try (ResultSet rs = connection.getMetaData().getTables(null, "information_schema", null, null)) { + assertTableMetadata(rs); + + Set> rows = ImmutableSet.copyOf(readRows(rs)); + assertTrue(rows.contains(getTablesRow("information_schema", "tables"))); + assertTrue(rows.contains(getTablesRow("information_schema", "schemata"))); + } + } + + try (Connection connection = createConnection()) { + try (ResultSet rs = connection.getMetaData().getTables(null, null, "tables", null)) { + assertTableMetadata(rs); + + Set> rows = ImmutableSet.copyOf(readRows(rs)); + assertTrue(rows.contains(getTablesRow("information_schema", "tables"))); + assertFalse(rows.contains(getTablesRow("information_schema", "schemata"))); + } + } + + try (Connection connection = createConnection()) { + try (ResultSet rs = connection.getMetaData().getTables(null, null, null, new String[] {"BASE TABLE"})) { + assertTableMetadata(rs); + + Set> rows = ImmutableSet.copyOf(readRows(rs)); + assertTrue(rows.contains(getTablesRow("information_schema", "tables"))); + assertTrue(rows.contains(getTablesRow("information_schema", "schemata"))); + } + } + + try (Connection connection = createConnection()) { + try (ResultSet rs = connection.getMetaData().getTables(TEST_CATALOG, "inf%", "tables", null)) { + assertTableMetadata(rs); + + Set> rows = ImmutableSet.copyOf(readRows(rs)); + assertTrue(rows.contains(getTablesRow("information_schema", "tables"))); + assertFalse(rows.contains(getTablesRow("information_schema", "schemata"))); + } + } + + try (Connection connection = createConnection()) { + try (ResultSet rs = connection.getMetaData().getTables(TEST_CATALOG, "information_schema", "tab%", null)) { + assertTableMetadata(rs); + + Set> rows = ImmutableSet.copyOf(readRows(rs)); + assertTrue(rows.contains(getTablesRow("information_schema", "tables"))); + assertFalse(rows.contains(getTablesRow("information_schema", "schemata"))); + } + } + + try (Connection connection = createConnection()) { + try (ResultSet rs = connection.getMetaData().getTables("unknown", "information_schema", "tables", new String[] {"BASE TABLE"})) { + assertTableMetadata(rs); + + Set> rows = ImmutableSet.copyOf(readRows(rs)); + assertFalse(rows.contains(getTablesRow("information_schema", "tables"))); + assertFalse(rows.contains(getTablesRow("information_schema", "schemata"))); + } + } + + // todo why does Presto require that the schema name be lower case + try (Connection connection = createConnection()) { + try (ResultSet rs = connection.getMetaData().getTables(TEST_CATALOG, "unknown", "tables", new String[] {"BASE TABLE"})) { + assertTableMetadata(rs); + + Set> rows = ImmutableSet.copyOf(readRows(rs)); + assertFalse(rows.contains(getTablesRow("information_schema", "tables"))); + assertFalse(rows.contains(getTablesRow("information_schema", "schemata"))); + } + } + + try (Connection connection = createConnection()) { + try (ResultSet rs = connection.getMetaData().getTables(TEST_CATALOG, "information_schema", "unknown", new String[] {"BASE TABLE"})) { + assertTableMetadata(rs); + + Set> rows = ImmutableSet.copyOf(readRows(rs)); + assertFalse(rows.contains(getTablesRow("information_schema", "tables"))); + assertFalse(rows.contains(getTablesRow("information_schema", "schemata"))); + } + } + + try (Connection connection = createConnection()) { + try (ResultSet rs = connection.getMetaData().getTables(TEST_CATALOG, "information_schema", "tables", new String[] {"unknown"})) { + assertTableMetadata(rs); + + Set> rows = ImmutableSet.copyOf(readRows(rs)); + assertFalse(rows.contains(getTablesRow("information_schema", "tables"))); + assertFalse(rows.contains(getTablesRow("information_schema", "schemata"))); + } + } + + try (Connection connection = createConnection()) { + try (ResultSet rs = connection.getMetaData().getTables(TEST_CATALOG, "information_schema", "tables", new String[] {"unknown", "BASE TABLE"})) { + assertTableMetadata(rs); + + Set> rows = ImmutableSet.copyOf(readRows(rs)); + assertTrue(rows.contains(getTablesRow("information_schema", "tables"))); + assertFalse(rows.contains(getTablesRow("information_schema", "schemata"))); + } + } + + try (Connection connection = createConnection()) { + try (ResultSet rs = connection.getMetaData().getTables(TEST_CATALOG, "information_schema", "tables", new String[] {})) { + assertTableMetadata(rs); + + Set> rows = ImmutableSet.copyOf(readRows(rs)); + assertTrue(rows.contains(getTablesRow("information_schema", "tables"))); + assertFalse(rows.contains(getTablesRow("information_schema", "schemata"))); + } + } + } + + private static List getTablesRow(String schema, String table) + { + return ImmutableList.of(TEST_CATALOG, schema, table, "BASE TABLE", "", "", "", "", "", ""); + } + + private static void assertTableMetadata(ResultSet rs) + throws SQLException + { + ResultSetMetaData metadata = rs.getMetaData(); + assertEquals(metadata.getColumnCount(), 10); + + assertEquals(metadata.getColumnLabel(1), "TABLE_CAT"); + assertEquals(metadata.getColumnType(1), Types.LONGNVARCHAR); + + assertEquals(metadata.getColumnLabel(2), "TABLE_SCHEM"); + assertEquals(metadata.getColumnType(2), Types.LONGNVARCHAR); + + assertEquals(metadata.getColumnLabel(3), "TABLE_NAME"); + assertEquals(metadata.getColumnType(3), Types.LONGNVARCHAR); + + assertEquals(metadata.getColumnLabel(4), "TABLE_TYPE"); + assertEquals(metadata.getColumnType(4), Types.LONGNVARCHAR); + + assertEquals(metadata.getColumnLabel(5), "REMARKS"); + assertEquals(metadata.getColumnType(5), Types.LONGNVARCHAR); + + assertEquals(metadata.getColumnLabel(6), "TYPE_CAT"); + assertEquals(metadata.getColumnType(6), Types.LONGNVARCHAR); + + assertEquals(metadata.getColumnLabel(7), "TYPE_SCHEM"); + assertEquals(metadata.getColumnType(7), Types.LONGNVARCHAR); + + assertEquals(metadata.getColumnLabel(8), "TYPE_NAME"); + assertEquals(metadata.getColumnType(8), Types.LONGNVARCHAR); + + assertEquals(metadata.getColumnLabel(9), "SELF_REFERENCING_COL_NAME"); + assertEquals(metadata.getColumnType(9), Types.LONGNVARCHAR); + + assertEquals(metadata.getColumnLabel(10), "REF_GENERATION"); + assertEquals(metadata.getColumnType(10), Types.LONGNVARCHAR); + } + + @Test + public void testGetTableTypes() + throws Exception + { + try (Connection connection = createConnection()) { + try (ResultSet tableTypes = connection.getMetaData().getTableTypes()) { + List> data = readRows(tableTypes); + assertEquals(data.size(), 1); + assertEquals(data.get(0).get(0), "BASE TABLE"); + + ResultSetMetaData metadata = tableTypes.getMetaData(); + assertEquals(metadata.getColumnCount(), 1); + + assertEquals(metadata.getColumnLabel(1), "TABLE_TYPE"); + assertEquals(metadata.getColumnType(1), Types.LONGNVARCHAR); + } + } + } + + @Test + public void testGetColumns() + throws Exception + { + try (Connection connection = createConnection()) { + try (ResultSet rs = connection.getMetaData().getColumns(null, null, "tables", "column_name")) { + assertColumnMetadata(rs); + } + } + + try (Connection connection = createConnection()) { + try (ResultSet rs = connection.getMetaData().getColumns(TEST_CATALOG, null, "tables", "column_name")) { + assertColumnMetadata(rs); + } + } + + try (Connection connection = createConnection()) { + try (ResultSet rs = connection.getMetaData().getColumns(null, "information_schema", "tables", "column_name")) { + assertColumnMetadata(rs); + } + } + + try (Connection connection = createConnection()) { + try (ResultSet rs = connection.getMetaData().getColumns(TEST_CATALOG, "information_schema", "tables", "column_name")) { + assertColumnMetadata(rs); + } + } + + try (Connection connection = createConnection()) { + try (ResultSet rs = connection.getMetaData().getColumns(TEST_CATALOG, "inf%", "tables", "column_name")) { + assertColumnMetadata(rs); + } + } + + try (Connection connection = createConnection()) { + try (ResultSet rs = connection.getMetaData().getColumns(TEST_CATALOG, "information_schema", "tab%", "column_name")) { + assertColumnMetadata(rs); + } + } + + try (Connection connection = createConnection()) { + try (ResultSet rs = connection.getMetaData().getColumns(TEST_CATALOG, "information_schema", "tables", "col%")) { + assertColumnMetadata(rs); + } + } + } + + private static void assertColumnMetadata(ResultSet rs) + throws SQLException + { + ResultSetMetaData metadata = rs.getMetaData(); + assertEquals(metadata.getColumnCount(), 24); + + assertEquals(metadata.getColumnLabel(1), "TABLE_CAT"); + assertEquals(metadata.getColumnType(1), Types.LONGNVARCHAR); + + assertEquals(metadata.getColumnLabel(2), "TABLE_SCHEM"); + assertEquals(metadata.getColumnType(2), Types.LONGNVARCHAR); + + assertEquals(metadata.getColumnLabel(3), "TABLE_NAME"); + assertEquals(metadata.getColumnType(3), Types.LONGNVARCHAR); + + assertEquals(metadata.getColumnLabel(4), "COLUMN_NAME"); + assertEquals(metadata.getColumnType(4), Types.LONGNVARCHAR); + + assertEquals(metadata.getColumnLabel(5), "DATA_TYPE"); + assertEquals(metadata.getColumnType(5), Types.BIGINT); + + assertEquals(metadata.getColumnLabel(6), "TYPE_NAME"); + assertEquals(metadata.getColumnType(6), Types.LONGNVARCHAR); + + assertEquals(metadata.getColumnLabel(7), "COLUMN_SIZE"); + assertEquals(metadata.getColumnType(7), Types.BIGINT); + + assertEquals(metadata.getColumnLabel(8), "BUFFER_LENGTH"); + assertEquals(metadata.getColumnType(8), Types.BIGINT); + + assertEquals(metadata.getColumnLabel(9), "DECIMAL_DIGITS"); + assertEquals(metadata.getColumnType(9), Types.BIGINT); + + assertEquals(metadata.getColumnLabel(10), "NUM_PREC_RADIX"); + assertEquals(metadata.getColumnType(10), Types.BIGINT); + + assertEquals(metadata.getColumnLabel(11), "NULLABLE"); + assertEquals(metadata.getColumnType(11), Types.BIGINT); + + assertEquals(metadata.getColumnLabel(12), "REMARKS"); + assertEquals(metadata.getColumnType(12), Types.LONGNVARCHAR); + + assertEquals(metadata.getColumnLabel(13), "COLUMN_DEF"); + assertEquals(metadata.getColumnType(13), Types.LONGNVARCHAR); + + assertEquals(metadata.getColumnLabel(14), "SQL_DATA_TYPE"); + assertEquals(metadata.getColumnType(14), Types.BIGINT); + + assertEquals(metadata.getColumnLabel(15), "SQL_DATETIME_SUB"); + assertEquals(metadata.getColumnType(15), Types.BIGINT); + + assertEquals(metadata.getColumnLabel(16), "CHAR_OCTET_LENGTH"); + assertEquals(metadata.getColumnType(16), Types.BIGINT); + + assertEquals(metadata.getColumnLabel(17), "ORDINAL_POSITION"); + assertEquals(metadata.getColumnType(17), Types.BIGINT); + + assertEquals(metadata.getColumnLabel(18), "IS_NULLABLE"); + assertEquals(metadata.getColumnType(18), Types.LONGNVARCHAR); + + assertEquals(metadata.getColumnLabel(19), "SCOPE_CATALOG"); + assertEquals(metadata.getColumnType(19), Types.LONGNVARCHAR); + + assertEquals(metadata.getColumnLabel(20), "SCOPE_SCHEMA"); + assertEquals(metadata.getColumnType(20), Types.LONGNVARCHAR); + + assertEquals(metadata.getColumnLabel(21), "SCOPE_TABLE"); + assertEquals(metadata.getColumnType(21), Types.LONGNVARCHAR); + + assertEquals(metadata.getColumnLabel(22), "SOURCE_DATA_TYPE"); + assertEquals(metadata.getColumnType(22), Types.BIGINT); + + assertEquals(metadata.getColumnLabel(23), "IS_AUTOINCREMENT"); + assertEquals(metadata.getColumnType(23), Types.LONGNVARCHAR); + + assertEquals(metadata.getColumnLabel(24), "IS_GENERATEDCOLUMN"); + assertEquals(metadata.getColumnType(24), Types.LONGNVARCHAR); + } + + @Test + public void testExecute() + throws Exception + { + try (Connection connection = createConnection()) { + try (Statement statement = connection.createStatement()) { + assertTrue(statement.execute("SELECT 123 x, 'foo' y, CAST(NULL AS bigint) z")); + ResultSet rs = statement.getResultSet(); + assertTrue(rs.next()); + + assertEquals(rs.getLong(1), 123); + assertFalse(rs.wasNull()); + assertEquals(rs.getLong("x"), 123); + assertFalse(rs.wasNull()); + + assertEquals(rs.getLong(3), 0); + assertTrue(rs.wasNull()); + assertEquals(rs.getLong("z"), 0); + assertTrue(rs.wasNull()); + assertNull(rs.getObject("z")); + assertTrue(rs.wasNull()); + + assertEquals(rs.getString(2), "foo"); + assertFalse(rs.wasNull()); + assertEquals(rs.getString("y"), "foo"); + assertFalse(rs.wasNull()); + + assertFalse(rs.next()); + } + } + } + + @Test + public void testGetUpdateCount() + throws Exception + { + try (Connection connection = createConnection()) { + try (Statement statement = connection.createStatement()) { + assertTrue(statement.execute("SELECT 123 x, 'foo' y")); + assertEquals(statement.getUpdateCount(), -1); + } + } + } + + @Test + public void testResultSetClose() + throws Exception + { + try (Connection connection = createConnection()) { + try (Statement statement = connection.createStatement()) { + assertTrue(statement.execute("SELECT 123 x, 'foo' y")); + ResultSet result = statement.getResultSet(); + assertFalse(result.isClosed()); + result.close(); + assertTrue(result.isClosed()); + } + } + } + + @Test + public void testGetResultSet() + throws Exception + { + try (Connection connection = createConnection()) { + try (Statement statement = connection.createStatement()) { + assertTrue(statement.execute("SELECT 123 x, 'foo' y")); + ResultSet result = statement.getResultSet(); + assertNotNull(result); + assertFalse(result.isClosed()); + statement.getMoreResults(); + assertTrue(result.isClosed()); + + assertTrue(statement.execute("SELECT 123 x, 'foo' y")); + result = statement.getResultSet(); + assertNotNull(result); + assertFalse(result.isClosed()); + + assertTrue(statement.execute("SELECT 123 x, 'foo' y")); + assertFalse(statement.getMoreResults(Statement.CLOSE_CURRENT_RESULT)); + } + } + } + + @Test(expectedExceptions = SQLFeatureNotSupportedException.class, expectedExceptionsMessageRegExp = "Multiple open results not supported") + public void testGetMoreResultsException() + throws Exception + { + try (Connection connection = createConnection()) { + try (Statement statement = connection.createStatement()) { + assertTrue(statement.execute("SELECT 123 x, 'foo' y")); + statement.getMoreResults(Statement.KEEP_CURRENT_RESULT); + } + } + } + + @Test + public void testConnectionStringWithCatalogAndSchema() + throws Exception + { + String prefix = format("jdbc:presto://%s", server.getAddress()); + + Connection connection; + connection = DriverManager.getConnection(prefix + "/a/b/", "test", null); + assertEquals(connection.getCatalog(), "a"); + assertEquals(connection.getSchema(), "b"); + + connection = DriverManager.getConnection(prefix + "/a/b", "test", null); + assertEquals(connection.getCatalog(), "a"); + assertEquals(connection.getSchema(), "b"); + + connection = DriverManager.getConnection(prefix + "/a/", "test", null); + assertEquals(connection.getCatalog(), "a"); + assertEquals(connection.getSchema(), TEST_CATALOG); + + connection = DriverManager.getConnection(prefix + "/a", "test", null); + assertEquals(connection.getCatalog(), "a"); + assertEquals(connection.getSchema(), TEST_CATALOG); + + connection = DriverManager.getConnection(prefix + "/", "test", null); + assertEquals(connection.getCatalog(), TEST_CATALOG); + assertEquals(connection.getSchema(), TEST_CATALOG); + + connection = DriverManager.getConnection(prefix, "test", null); + assertEquals(connection.getCatalog(), TEST_CATALOG); + assertEquals(connection.getSchema(), TEST_CATALOG); + } + + @Test + public void testConnectionWithCatalogAndSchema() + throws Exception + { + try (Connection connection = createConnection(TEST_CATALOG, "information_schema")) { + try (Statement statement = connection.createStatement()) { + try (ResultSet rs = statement.executeQuery("" + + "SELECT table_catalog, table_schema " + + "FROM tables " + + "WHERE table_schema = 'information_schema' " + + " AND table_name = 'tables'")) { + ResultSetMetaData metadata = rs.getMetaData(); + assertEquals(metadata.getColumnCount(), 2); + assertEquals(metadata.getColumnLabel(1), "table_catalog"); + assertEquals(metadata.getColumnLabel(2), "table_schema"); + assertTrue(rs.next()); + assertEquals(rs.getString("table_catalog"), TEST_CATALOG); + } + } + } + } + + @Test + public void testConnectionWithCatalog() + throws Exception + { + try (Connection connection = createConnection(TEST_CATALOG)) { + try (Statement statement = connection.createStatement()) { + try (ResultSet rs = statement.executeQuery("" + + "SELECT table_catalog, table_schema " + + "FROM information_schema.tables " + + "WHERE table_schema = 'information_schema' " + + " AND table_name = 'tables'")) { + ResultSetMetaData metadata = rs.getMetaData(); + assertEquals(metadata.getColumnCount(), 2); + assertEquals(metadata.getColumnLabel(1), "table_catalog"); + assertEquals(metadata.getColumnLabel(2), "table_schema"); + assertTrue(rs.next()); + assertEquals(rs.getString("table_catalog"), TEST_CATALOG); + } + } + } + } + + @Test + public void testConnectionResourceHandling() + throws Exception + { + List connections = new ArrayList<>(); + + for (int i = 0; i < 100; i++) { + Connection connection = createConnection(); + connections.add(connection); + + try (Statement statement = connection.createStatement(); + ResultSet rs = statement.executeQuery("SELECT 123")) { + assertTrue(rs.next()); + } + } + + for (Connection connection : connections) { + connection.close(); + } + } + + @Test(expectedExceptions = SQLException.class, expectedExceptionsMessageRegExp = ".* does not exist") + public void testBadQuery() + throws Exception + { + try (Connection connection = createConnection()) { + try (Statement statement = connection.createStatement()) { + try (ResultSet ignored = statement.executeQuery("SELECT * FROM bad_table")) { + fail("expected exception"); + } + } + } + } + + @Test(expectedExceptions = SQLException.class, expectedExceptionsMessageRegExp = "Username property \\(user\\) must be set") + public void testUserIsRequired() + throws Exception + { + try (Connection ignored = DriverManager.getConnection("jdbc:presto://test.invalid/")) { + fail("expected exception"); + } + } + + @Test(expectedExceptions = SQLException.class, expectedExceptionsMessageRegExp = "Invalid path segments in URL: .*") + public void testBadUrlExtraPathSegments() + throws Exception + { + String url = format("jdbc:presto://%s/hive/default/bad_string", server.getAddress()); + try (Connection ignored = DriverManager.getConnection(url, "test", null)) { + fail("expected exception"); + } + } + + @Test(expectedExceptions = SQLException.class, expectedExceptionsMessageRegExp = "Catalog name is empty: .*") + public void testBadUrlMissingCatalog() + throws Exception + { + String url = format("jdbc:presto://%s//default", server.getAddress()); + try (Connection ignored = DriverManager.getConnection(url, "test", null)) { + fail("expected exception"); + } + } + + @Test(expectedExceptions = SQLException.class, expectedExceptionsMessageRegExp = "Catalog name is empty: .*") + public void testBadUrlEndsInSlashes() + throws Exception + { + String url = format("jdbc:presto://%s//", server.getAddress()); + try (Connection ignored = DriverManager.getConnection(url, "test", null)) { + fail("expected exception"); + } + } + + @Test(expectedExceptions = SQLException.class, expectedExceptionsMessageRegExp = "Schema name is empty: .*") + public void testBadUrlMissingSchema() + throws Exception + { + String url = format("jdbc:presto://%s/a//", server.getAddress()); + try (Connection ignored = DriverManager.getConnection(url, "test", null)) { + fail("expected exception"); + } + } + + private Connection createConnection() + throws SQLException + { + String url = format("jdbc:presto://%s", server.getAddress()); + return DriverManager.getConnection(url, "test", null); + } + + private Connection createConnection(String catalog) + throws SQLException + { + String url = format("jdbc:presto://%s/%s", server.getAddress(), catalog); + return DriverManager.getConnection(url, "test", null); + } + + private Connection createConnection(String catalog, String schema) + throws SQLException + { + String url = format("jdbc:presto://%s/%s/%s", server.getAddress(), catalog, schema); + return DriverManager.getConnection(url, "test", null); + } + + private static void assertRowCount(ResultSet rs, int expected) + throws SQLException + { + List> data = readRows(rs); + assertEquals(data.size(), expected); + } + + private static List> readRows(ResultSet rs) + throws SQLException + { + ImmutableList.Builder> rows = ImmutableList.builder(); + int columnCount = rs.getMetaData().getColumnCount(); + while (rs.next()) { + ImmutableList.Builder row = ImmutableList.builder(); + for (int i = 0; i < columnCount; i++) { + row.add(rs.getObject(i + 1)); + } + rows.add(row.build()); + } + return rows.build(); + } + + static void closeQuietly(AutoCloseable closeable) + { + try { + closeable.close(); + } + catch (Exception ignored) { + } + } +} diff --git a/presto-jdbc/src/test/java/com/facebook/presto/jdbc/TestJdbcResultSet.java b/presto-jdbc/src/test/java/com/facebook/presto/jdbc/TestJdbcResultSet.java new file mode 100644 index 00000000..832d7d0a --- /dev/null +++ b/presto-jdbc/src/test/java/com/facebook/presto/jdbc/TestJdbcResultSet.java @@ -0,0 +1,126 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.jdbc; + +import com.facebook.presto.server.testing.TestingPrestoServer; +import io.airlift.log.Logging; +import org.testng.annotations.AfterClass; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Types; + +import static com.facebook.presto.jdbc.TestDriver.closeQuietly; +import static java.lang.String.format; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +@Test(singleThreaded = true) +public class TestJdbcResultSet +{ + private TestingPrestoServer server; + + private Connection connection; + private Statement statement; + + @BeforeClass + public void setupServer() + throws Exception + { + Logging.initialize(); + server = new TestingPrestoServer(); + } + + @AfterClass + public void teardownServer() + { + closeQuietly(server); + } + + @SuppressWarnings("JDBCResourceOpenedButNotSafelyClosed") + @BeforeMethod + public void setup() + throws Exception + { + connection = createConnection(); + statement = connection.createStatement(); + } + + @AfterMethod + public void teardown() + { + closeQuietly(statement); + closeQuietly(connection); + } + + @Test + public void testDuplicateColumnLabels() + throws Exception + { + try (ResultSet rs = statement.executeQuery("SELECT 123 x, 456 x")) { + ResultSetMetaData metadata = rs.getMetaData(); + assertEquals(metadata.getColumnCount(), 2); + assertEquals(metadata.getColumnName(1), "x"); + assertEquals(metadata.getColumnName(2), "x"); + + assertTrue(rs.next()); + assertEquals(rs.getLong(1), 123L); + assertEquals(rs.getLong(2), 456L); + assertEquals(rs.getLong("x"), 123L); + } + } + + @SuppressWarnings("UnnecessaryBoxing") + @Test + public void testObjectTypes() + throws Exception + { + String sql = "SELECT 123, 0.1, true, 'hello', 1.0 / 0.0, 0.0 / 0.0, ARRAY[1, 2]"; + try (ResultSet rs = statement.executeQuery(sql)) { + ResultSetMetaData metadata = rs.getMetaData(); + assertEquals(metadata.getColumnCount(), 7); + assertEquals(metadata.getColumnType(1), Types.BIGINT); + assertEquals(metadata.getColumnType(2), Types.DOUBLE); + assertEquals(metadata.getColumnType(3), Types.BOOLEAN); + assertEquals(metadata.getColumnType(4), Types.LONGNVARCHAR); + assertEquals(metadata.getColumnType(5), Types.DOUBLE); + assertEquals(metadata.getColumnType(6), Types.DOUBLE); + assertEquals(metadata.getColumnType(7), Types.ARRAY); + + assertTrue(rs.next()); + assertEquals(rs.getObject(1), 123L); + assertEquals(rs.getObject(2), 0.1d); + assertEquals(rs.getObject(3), true); + assertEquals(rs.getObject(4), "hello"); + assertEquals(rs.getObject(5), Double.POSITIVE_INFINITY); + assertEquals(rs.getObject(6), Double.NaN); + assertEquals(rs.getArray(7).getArray(), new long[] {1L, 2L}); + } + } + + private Connection createConnection() + throws SQLException + { + String url = format("jdbc:presto://%s", server.getAddress()); + return DriverManager.getConnection(url, "test", null); + } +} diff --git a/presto-kafka/pom.xml b/presto-kafka/pom.xml new file mode 100644 index 00000000..86a79c14 --- /dev/null +++ b/presto-kafka/pom.xml @@ -0,0 +1,214 @@ + + + 4.0.0 + + + com.facebook.presto + presto-root + 0.107 + + + presto-kafka + Presto - Kafka Connector + presto-plugin + + + ${project.parent.basedir} + + + + + io.airlift + bootstrap + + + + io.airlift + json + + + + io.airlift + log + + + + io.airlift + configuration + + + + io.airlift + units + + + + com.google.guava + guava + + + + com.google.inject + guice + + + + com.google.inject.extensions + guice-multibindings + + + + javax.validation + validation-api + + + + com.google.code.findbugs + annotations + + + + org.apache.kafka + kafka_2.10 + + + + net.sf.opencsv + opencsv + + + + joda-time + joda-time + + + + javax.annotation + javax.annotation-api + + + + + com.facebook.presto + presto-spi + provided + + + + io.airlift + slice + provided + + + + javax.inject + javax.inject + provided + + + + com.fasterxml.jackson.core + jackson-annotations + provided + + + + com.fasterxml.jackson.core + jackson-core + provided + + + + com.fasterxml.jackson.core + jackson-databind + provided + + + + + org.testng + testng + test + + + + io.airlift + testing + test + + + + com.facebook.presto + presto-main + test + + + + com.facebook.presto + presto-tpch + test + + + + com.facebook.presto + presto-client + test + + + + com.facebook.presto + presto-tests + test + + + + io.airlift.tpch + tpch + test + + + + org.apache.zookeeper + zookeeper + test + + + + com.101tec + zkclient + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + **/TestKafkaDistributed.java + + + + + + + + + ci + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + + + + + + diff --git a/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaColumnHandle.java b/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaColumnHandle.java new file mode 100644 index 00000000..9acf25d6 --- /dev/null +++ b/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaColumnHandle.java @@ -0,0 +1,219 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka; + +import com.facebook.presto.spi.ColumnMetadata; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.type.Type; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.primitives.Ints; + +import java.util.Objects; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Kafka specific connector column handle. + */ +public final class KafkaColumnHandle + implements ColumnHandle, Comparable +{ + private final String connectorId; + private final int ordinalPosition; + + /** + * Column Name + */ + private final String name; + + /** + * Column type + */ + private final Type type; + + /** + * Mapping hint for the decoder. Can be null. + */ + private final String mapping; + + /** + * Data format to use (selects the decoder). Can be null. + */ + private final String dataFormat; + + /** + * Additional format hint for the selected decoder. Selects a decoder subtype (e.g. which timestamp decoder). + */ + private final String formatHint; + + /** + * True if the key decoder should be used, false if the message decoder should be used. + */ + private final boolean keyDecoder; + + /** + * True if the column should be hidden. + */ + private final boolean hidden; + + /** + * True if the column is internal to the connector and not defined by a topic definition. + */ + private final boolean internal; + + @JsonCreator + public KafkaColumnHandle( + @JsonProperty("connectorId") String connectorId, + @JsonProperty("ordinalPosition") int ordinalPosition, + @JsonProperty("name") String name, + @JsonProperty("type") Type type, + @JsonProperty("mapping") String mapping, + @JsonProperty("dataFormat") String dataFormat, + @JsonProperty("formatHint") String formatHint, + @JsonProperty("keyDecoder") boolean keyDecoder, + @JsonProperty("hidden") boolean hidden, + @JsonProperty("internal") boolean internal) + + { + this.connectorId = checkNotNull(connectorId, "connectorId is null"); + this.ordinalPosition = ordinalPosition; + this.name = checkNotNull(name, "name is null"); + this.type = checkNotNull(type, "type is null"); + this.mapping = mapping; + this.dataFormat = dataFormat; + this.formatHint = formatHint; + this.keyDecoder = keyDecoder; + this.hidden = hidden; + this.internal = internal; + } + + @JsonProperty + public String getConnectorId() + { + return connectorId; + } + + @JsonProperty + public int getOrdinalPosition() + { + return ordinalPosition; + } + + @JsonProperty + public String getName() + { + return name; + } + + @JsonProperty + public Type getType() + { + return type; + } + + @JsonProperty + public String getMapping() + { + return mapping; + } + + @JsonProperty + public String getDataFormat() + { + return dataFormat; + } + + @JsonProperty + public String getFormatHint() + { + return formatHint; + } + + @JsonProperty + public boolean isKeyDecoder() + { + return keyDecoder; + } + + @JsonProperty + public boolean isHidden() + { + return hidden; + } + + @JsonProperty + public boolean isInternal() + { + return internal; + } + + ColumnMetadata getColumnMetadata() + { + return new ColumnMetadata(name, type, false, null, hidden); + } + + @Override + public int hashCode() + { + return Objects.hash(connectorId, ordinalPosition, name, type, mapping, dataFormat, formatHint, keyDecoder, hidden, internal); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + KafkaColumnHandle other = (KafkaColumnHandle) obj; + return Objects.equals(this.connectorId, other.connectorId) && + Objects.equals(this.ordinalPosition, other.ordinalPosition) && + Objects.equals(this.name, other.name) && + Objects.equals(this.type, other.type) && + Objects.equals(this.mapping, other.mapping) && + Objects.equals(this.dataFormat, other.dataFormat) && + Objects.equals(this.formatHint, other.formatHint) && + Objects.equals(this.keyDecoder, other.keyDecoder) && + Objects.equals(this.hidden, other.hidden) && + Objects.equals(this.internal, other.internal); + } + + @Override + public int compareTo(KafkaColumnHandle otherHandle) + { + return Ints.compare(this.getOrdinalPosition(), otherHandle.getOrdinalPosition()); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("connectorId", connectorId) + .add("ordinalPosition", ordinalPosition) + .add("name", name) + .add("type", type) + .add("mapping", mapping) + .add("dataFormat", dataFormat) + .add("formatHint", formatHint) + .add("keyDecoder", keyDecoder) + .add("hidden", hidden) + .add("internal", internal) + .toString(); + } +} diff --git a/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaConnector.java b/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaConnector.java new file mode 100644 index 00000000..574ac831 --- /dev/null +++ b/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaConnector.java @@ -0,0 +1,91 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka; + +import com.facebook.presto.spi.Connector; +import com.facebook.presto.spi.ConnectorHandleResolver; +import com.facebook.presto.spi.ConnectorMetadata; +import com.facebook.presto.spi.ConnectorRecordSetProvider; +import com.facebook.presto.spi.ConnectorSplitManager; +import io.airlift.bootstrap.LifeCycleManager; +import io.airlift.log.Logger; + +import javax.inject.Inject; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Kafka specific implementation of the Presto Connector SPI. This is a read only connector. + */ +public class KafkaConnector + implements Connector +{ + private static final Logger log = Logger.get(KafkaConnector.class); + + private final LifeCycleManager lifeCycleManager; + private final KafkaMetadata metadata; + private final KafkaSplitManager splitManager; + private final KafkaRecordSetProvider recordSetProvider; + private final KafkaHandleResolver handleResolver; + + @Inject + public KafkaConnector( + LifeCycleManager lifeCycleManager, + KafkaHandleResolver handleResolver, + KafkaMetadata metadata, + KafkaSplitManager splitManager, + KafkaRecordSetProvider recordSetProvider) + { + this.lifeCycleManager = checkNotNull(lifeCycleManager, "lifeCycleManager is null"); + this.handleResolver = checkNotNull(handleResolver, "handleResolver is null"); + this.metadata = checkNotNull(metadata, "metadata is null"); + this.splitManager = checkNotNull(splitManager, "splitManager is null"); + this.recordSetProvider = checkNotNull(recordSetProvider, "recordSetProvider is null"); + } + + @Override + public ConnectorHandleResolver getHandleResolver() + { + return handleResolver; + } + + @Override + public ConnectorMetadata getMetadata() + { + return metadata; + } + + @Override + public ConnectorSplitManager getSplitManager() + { + return splitManager; + } + + @Override + public ConnectorRecordSetProvider getRecordSetProvider() + { + return recordSetProvider; + } + + @Override + public final void shutdown() + { + try { + lifeCycleManager.stop(); + } + catch (Exception e) { + log.error(e, "Error shutting down connector"); + } + } +} diff --git a/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaConnectorConfig.java b/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaConnectorConfig.java new file mode 100644 index 00000000..ae5671f6 --- /dev/null +++ b/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaConnectorConfig.java @@ -0,0 +1,171 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka; + +import com.facebook.presto.spi.HostAddress; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableSet; +import io.airlift.configuration.Config; +import io.airlift.units.DataSize; +import io.airlift.units.DataSize.Unit; +import io.airlift.units.Duration; +import io.airlift.units.MinDuration; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +import java.io.File; +import java.util.Set; + +import static com.google.common.collect.Iterables.transform; + +public class KafkaConnectorConfig +{ + private static final int KAFKA_DEFAULT_PORT = 9092; + + /** + * Seed nodes for Kafka cluster. At least one must exist. + */ + private Set nodes = ImmutableSet.of(); + + /** + * Timeout to connect to Kafka. + */ + private Duration kafkaConnectTimeout = Duration.valueOf("10s"); + + /** + * Buffer size for connecting to Kafka. + */ + private DataSize kafkaBufferSize = new DataSize(64, Unit.KILOBYTE); + + /** + * The schema name to use in the connector. + */ + private String defaultSchema = "default"; + + /** + * Set of tables known to this connector. For each table, a description file may be present in the catalog folder which describes columns for the given topic. + */ + private Set tableNames = ImmutableSet.of(); + + /** + * Folder holding the JSON description files for Kafka topics. + */ + private File tableDescriptionDir = new File("etc/kafka/"); + + /** + * Whether internal columns are shown in table metadata or not. Default is no. + */ + private boolean hideInternalColumns = true; + + @NotNull + public File getTableDescriptionDir() + { + return tableDescriptionDir; + } + + @Config("kafka.table-description-dir") + public KafkaConnectorConfig setTableDescriptionDir(File tableDescriptionDir) + { + this.tableDescriptionDir = tableDescriptionDir; + return this; + } + + @NotNull + public Set getTableNames() + { + return tableNames; + } + + @Config("kafka.table-names") + public KafkaConnectorConfig setTableNames(String tableNames) + { + this.tableNames = ImmutableSet.copyOf(Splitter.on(',').omitEmptyStrings().trimResults().split(tableNames)); + return this; + } + + @NotNull + public String getDefaultSchema() + { + return defaultSchema; + } + + @Config("kafka.default-schema") + public KafkaConnectorConfig setDefaultSchema(String defaultSchema) + { + this.defaultSchema = defaultSchema; + return this; + } + + @Size(min = 1) + public Set getNodes() + { + return nodes; + } + + @Config("kafka.nodes") + public KafkaConnectorConfig setNodes(String nodes) + { + this.nodes = (nodes == null) ? null : parseNodes(nodes); + return this; + } + + @MinDuration("1s") + public Duration getKafkaConnectTimeout() + { + return kafkaConnectTimeout; + } + + @Config("kafka.connect-timeout") + public KafkaConnectorConfig setKafkaConnectTimeout(String kafkaConnectTimeout) + { + this.kafkaConnectTimeout = Duration.valueOf(kafkaConnectTimeout); + return this; + } + + public DataSize getKafkaBufferSize() + { + return kafkaBufferSize; + } + + @Config("kafka.buffer-size") + public KafkaConnectorConfig setKafkaBufferSize(String kafkaBufferSize) + { + this.kafkaBufferSize = DataSize.valueOf(kafkaBufferSize); + return this; + } + + public boolean isHideInternalColumns() + { + return hideInternalColumns; + } + + @Config("kafka.hide-internal-columns") + public KafkaConnectorConfig setHideInternalColumns(boolean hideInternalColumns) + { + this.hideInternalColumns = hideInternalColumns; + return this; + } + + public static ImmutableSet parseNodes(String nodes) + { + Splitter splitter = Splitter.on(',').omitEmptyStrings().trimResults(); + return ImmutableSet.copyOf(transform(splitter.split(nodes), KafkaConnectorConfig::toHostAddress)); + } + + private static HostAddress toHostAddress(String value) + { + return HostAddress.fromString(value).withDefaultPort(KAFKA_DEFAULT_PORT); + } +} diff --git a/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaConnectorFactory.java b/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaConnectorFactory.java new file mode 100644 index 00000000..28c4d9fa --- /dev/null +++ b/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaConnectorFactory.java @@ -0,0 +1,106 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka; + +import com.facebook.presto.spi.Connector; +import com.facebook.presto.spi.ConnectorFactory; +import com.facebook.presto.spi.NodeManager; +import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.spi.type.TypeManager; +import com.google.common.base.Supplier; +import com.google.common.base.Throwables; +import com.google.inject.Binder; +import com.google.inject.Injector; +import com.google.inject.Module; +import com.google.inject.Scopes; +import com.google.inject.TypeLiteral; +import com.google.inject.name.Names; +import io.airlift.bootstrap.Bootstrap; +import io.airlift.json.JsonModule; + +import java.util.Map; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Creates Kafka Connectors based off connectorId and specific configuration. + */ +public class KafkaConnectorFactory + implements ConnectorFactory +{ + private final TypeManager typeManager; + private final NodeManager nodeManager; + private final Optional>> tableDescriptionSupplier; + private final Map optionalConfig; + + KafkaConnectorFactory(TypeManager typeManager, + NodeManager nodeManager, + Optional>> tableDescriptionSupplier, + Map optionalConfig) + { + this.typeManager = checkNotNull(typeManager, "typeManager is null"); + this.nodeManager = checkNotNull(nodeManager, "nodeManager is null"); + this.optionalConfig = checkNotNull(optionalConfig, "optionalConfig is null"); + this.tableDescriptionSupplier = checkNotNull(tableDescriptionSupplier, "tableDescriptionSupplier is null"); + } + + @Override + public String getName() + { + return "kafka"; + } + + @Override + public Connector create(final String connectorId, Map config) + { + checkNotNull(connectorId, "connectorId is null"); + checkNotNull(config, "config is null"); + + try { + Bootstrap app = new Bootstrap( + new JsonModule(), + new KafkaConnectorModule(), + new Module() + { + @Override + public void configure(Binder binder) + { + binder.bindConstant().annotatedWith(Names.named("connectorId")).to(connectorId); + binder.bind(TypeManager.class).toInstance(typeManager); + binder.bind(NodeManager.class).toInstance(nodeManager); + + if (tableDescriptionSupplier.isPresent()) { + binder.bind(new TypeLiteral>>() {}).toInstance(tableDescriptionSupplier.get()); + } + else { + binder.bind(new TypeLiteral>>() {}).to(KafkaTableDescriptionSupplier.class).in(Scopes.SINGLETON); + } + } + } + ); + + Injector injector = app.strictConfig() + .doNotInitializeLogging() + .setRequiredConfigurationProperties(config) + .setOptionalConfigurationProperties(optionalConfig) + .initialize(); + + return injector.getInstance(KafkaConnector.class); + } + catch (Exception e) { + throw Throwables.propagate(e); + } + } +} diff --git a/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaConnectorModule.java b/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaConnectorModule.java new file mode 100644 index 00000000..8d19c51d --- /dev/null +++ b/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaConnectorModule.java @@ -0,0 +1,93 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka; + +import com.facebook.presto.kafka.decoder.KafkaDecoderModule; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.deser.std.FromStringDeserializer; +import com.google.inject.Binder; +import com.google.inject.Module; +import com.google.inject.Scopes; +import com.google.inject.multibindings.Multibinder; + +import javax.inject.Inject; + +import static com.facebook.presto.spi.type.TypeSignature.parseTypeSignature; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.airlift.configuration.ConfigBinder.configBinder; +import static io.airlift.json.JsonBinder.jsonBinder; +import static io.airlift.json.JsonCodecBinder.jsonCodecBinder; + +/** + * Guice module for the Apache Kafka connector. + */ +public class KafkaConnectorModule + implements Module +{ + @Override + public void configure(Binder binder) + { + binder.bind(KafkaConnector.class).in(Scopes.SINGLETON); + + binder.bind(KafkaHandleResolver.class).in(Scopes.SINGLETON); + binder.bind(KafkaMetadata.class).in(Scopes.SINGLETON); + binder.bind(KafkaSplitManager.class).in(Scopes.SINGLETON); + binder.bind(KafkaRecordSetProvider.class).in(Scopes.SINGLETON); + + binder.bind(KafkaSimpleConsumerManager.class).in(Scopes.SINGLETON); + + configBinder(binder).bindConfig(KafkaConnectorConfig.class); + + jsonBinder(binder).addDeserializerBinding(Type.class).to(TypeDeserializer.class); + jsonCodecBinder(binder).bindJsonCodec(KafkaTopicDescription.class); + + binder.install(new KafkaDecoderModule()); + + for (KafkaInternalFieldDescription internalFieldDescription : KafkaInternalFieldDescription.getInternalFields()) { + bindInternalColumn(binder, internalFieldDescription); + } + } + + private static void bindInternalColumn(Binder binder, KafkaInternalFieldDescription fieldDescription) + { + Multibinder fieldDescriptionBinder = Multibinder.newSetBinder(binder, KafkaInternalFieldDescription.class); + fieldDescriptionBinder.addBinding().toInstance(fieldDescription); + } + + public static final class TypeDeserializer + extends FromStringDeserializer + { + private static final long serialVersionUID = 1L; + + private final TypeManager typeManager; + + @Inject + public TypeDeserializer(TypeManager typeManager) + { + super(Type.class); + this.typeManager = checkNotNull(typeManager, "typeManager is null"); + } + + @Override + protected Type _deserialize(String value, DeserializationContext context) + { + Type type = typeManager.getType(parseTypeSignature(value)); + checkArgument(type != null, "Unknown type %s", value); + return type; + } + } +} diff --git a/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaErrorCode.java b/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaErrorCode.java new file mode 100644 index 00000000..ac1c4eca --- /dev/null +++ b/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaErrorCode.java @@ -0,0 +1,45 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka; + +import com.facebook.presto.spi.ErrorCode; +import com.facebook.presto.spi.ErrorCodeSupplier; + +/** + * Kafka connector specific error codes. + */ +public enum KafkaErrorCode + implements ErrorCodeSupplier +{ + // Connectors can use error codes starting at EXTERNAL + + /** + * A requested data conversion is not supported. + */ + KAFKA_CONVERSION_NOT_SUPPORTED(0x0200_0000), + KAFKA_SPLIT_ERROR(0x0200_0001); + + private final ErrorCode errorCode; + + KafkaErrorCode(int code) + { + errorCode = new ErrorCode(code, name()); + } + + @Override + public ErrorCode toErrorCode() + { + return errorCode; + } +} diff --git a/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaFieldValueProvider.java b/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaFieldValueProvider.java new file mode 100644 index 00000000..6ce7f469 --- /dev/null +++ b/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaFieldValueProvider.java @@ -0,0 +1,49 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka; + +import com.facebook.presto.spi.PrestoException; +import io.airlift.slice.Slice; + +import static com.facebook.presto.kafka.KafkaErrorCode.KAFKA_CONVERSION_NOT_SUPPORTED; + +/** + * Base class for all providers that return values for a selected column. + */ +public abstract class KafkaFieldValueProvider +{ + public abstract boolean accept(KafkaColumnHandle columnHandle); + + public boolean getBoolean() + { + throw new PrestoException(KAFKA_CONVERSION_NOT_SUPPORTED, "conversion to boolean not supported"); + } + + public long getLong() + { + throw new PrestoException(KAFKA_CONVERSION_NOT_SUPPORTED, "conversion to long not supported"); + } + + public double getDouble() + { + throw new PrestoException(KAFKA_CONVERSION_NOT_SUPPORTED, "conversion to double not supported"); + } + + public Slice getSlice() + { + throw new PrestoException(KAFKA_CONVERSION_NOT_SUPPORTED, "conversion to Slice not supported"); + } + + public abstract boolean isNull(); +} diff --git a/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaHandleResolver.java b/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaHandleResolver.java new file mode 100644 index 00000000..f3a5ca59 --- /dev/null +++ b/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaHandleResolver.java @@ -0,0 +1,106 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka; + +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorHandleResolver; +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.ConnectorTableHandle; +import com.google.inject.name.Named; + +import javax.inject.Inject; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Kafka specific {@link com.facebook.presto.spi.ConnectorHandleResolver} implementation. + */ +public class KafkaHandleResolver + implements ConnectorHandleResolver +{ + private final String connectorId; + + @Inject + KafkaHandleResolver(@Named("connectorId") String connectorId, + KafkaConnectorConfig kafkaConnectorConfig) + { + this.connectorId = checkNotNull(connectorId, "connectorId is null"); + checkNotNull(kafkaConnectorConfig, "kafkaConfig is null"); + } + + @Override + public boolean canHandle(ConnectorTableHandle tableHandle) + { + return tableHandle != null && tableHandle instanceof KafkaTableHandle && connectorId.equals(((KafkaTableHandle) tableHandle).getConnectorId()); + } + + @Override + public boolean canHandle(ColumnHandle columnHandle) + { + return columnHandle != null && columnHandle instanceof KafkaColumnHandle && connectorId.equals(((KafkaColumnHandle) columnHandle).getConnectorId()); + } + + @Override + public boolean canHandle(ConnectorSplit split) + { + return split != null && split instanceof KafkaSplit && connectorId.equals(((KafkaSplit) split).getConnectorId()); + } + + @Override + public Class getTableHandleClass() + { + return KafkaTableHandle.class; + } + + @Override + public Class getColumnHandleClass() + { + return KafkaColumnHandle.class; + } + + @Override + public Class getSplitClass() + { + return KafkaSplit.class; + } + + KafkaTableHandle convertTableHandle(ConnectorTableHandle tableHandle) + { + checkNotNull(tableHandle, "tableHandle is null"); + checkArgument(tableHandle instanceof KafkaTableHandle, "tableHandle is not an instance of KafkaTableHandle"); + KafkaTableHandle kafkaTableHandle = (KafkaTableHandle) tableHandle; + checkArgument(kafkaTableHandle.getConnectorId().equals(connectorId), "tableHandle is not for this connector"); + + return kafkaTableHandle; + } + + KafkaColumnHandle convertColumnHandle(ColumnHandle columnHandle) + { + checkNotNull(columnHandle, "columnHandle is null"); + checkArgument(columnHandle instanceof KafkaColumnHandle, "columnHandle is not an instance of KafkaColumnHandle"); + KafkaColumnHandle kafkaColumnHandle = (KafkaColumnHandle) columnHandle; + checkArgument(kafkaColumnHandle.getConnectorId().equals(connectorId), "columnHandle is not for this connector"); + return kafkaColumnHandle; + } + + KafkaSplit convertSplit(ConnectorSplit split) + { + checkNotNull(split, "split is null"); + checkArgument(split instanceof KafkaSplit, "split is not an instance of KafkaSplit"); + KafkaSplit kafkaSplit = (KafkaSplit) split; + checkArgument(kafkaSplit.getConnectorId().equals(connectorId), "split is not for this connector"); + return kafkaSplit; + } +} diff --git a/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaInternalFieldDescription.java b/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaInternalFieldDescription.java new file mode 100644 index 00000000..7aac8875 --- /dev/null +++ b/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaInternalFieldDescription.java @@ -0,0 +1,279 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka; + +import com.facebook.presto.spi.ColumnMetadata; +import com.facebook.presto.spi.type.BigintType; +import com.facebook.presto.spi.type.BooleanType; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.VarcharType; +import com.google.common.collect.ImmutableSet; +import io.airlift.slice.Slice; +import io.airlift.slice.Slices; + +import java.util.Objects; +import java.util.Set; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Strings.isNullOrEmpty; + +/** + * Describes an internal (managed by the connector) field which is added to each table row. The definition itself makes the row + * show up in the tables (the columns are hidden by default, so they must be explicitly selected) but unless the field is hooked in using the + * forBooleanValue/forLongValue/forBytesValue methods and the resulting FieldValueProvider is then passed into the appropriate row decoder, the fields + * will be null. Most values are assigned in the {@link com.facebook.presto.kafka.KafkaRecordSet}. + */ +public class KafkaInternalFieldDescription +{ + /** + * _partition_id - Kafka partition id. + */ + public static final KafkaInternalFieldDescription PARTITION_ID_FIELD = new KafkaInternalFieldDescription("_partition_id", BigintType.BIGINT, "Partition Id"); + + /** + * _partition_offset - The current offset of the message in the partition. + */ + public static final KafkaInternalFieldDescription PARTITION_OFFSET_FIELD = new KafkaInternalFieldDescription("_partition_offset", BigintType.BIGINT, "Offset for the message within the partition"); + + /** + * _segment_start - Kafka start offset for the segment which contains the current message. This is per-partition. + */ + public static final KafkaInternalFieldDescription SEGMENT_START_FIELD = new KafkaInternalFieldDescription("_segment_start", BigintType.BIGINT, "Segment start offset"); + + /** + * _segment_end - Kafka end offset for the segment which contains the current message. This is per-partition. The end offset is the first offset that is *not* in the segment. + */ + public static final KafkaInternalFieldDescription SEGMENT_END_FIELD = new KafkaInternalFieldDescription("_segment_end", BigintType.BIGINT, "Segment end offset"); + + /** + * _segment_count - Running count of messages in a segment. + */ + public static final KafkaInternalFieldDescription SEGMENT_COUNT_FIELD = new KafkaInternalFieldDescription("_segment_count", BigintType.BIGINT, "Running message count per segment"); + + /** + * _message_corrupt - True if the row converter could not read the a message. May be null if the row converter does not set a value (e.g. the dummy row converter does not). + */ + public static final KafkaInternalFieldDescription MESSAGE_CORRUPT_FIELD = new KafkaInternalFieldDescription("_message_corrupt", BooleanType.BOOLEAN, "Message data is corrupt"); + + /** + * _message - Represents the full topic as a text column. Format is UTF-8 which may be wrong for some topics. TODO: make charset configurable. + */ + public static final KafkaInternalFieldDescription MESSAGE_FIELD = new KafkaInternalFieldDescription("_message", VarcharType.VARCHAR, "Message text"); + + /** + * _message_length - length in bytes of the message. + */ + public static final KafkaInternalFieldDescription MESSAGE_LENGTH_FIELD = new KafkaInternalFieldDescription("_message_length", BigintType.BIGINT, "Total number of message bytes"); + + /** + * _key_corrupt - True if the row converter could not read the a key. May be null if the row converter does not set a value (e.g. the dummy row converter does not). + */ + public static final KafkaInternalFieldDescription KEY_CORRUPT_FIELD = new KafkaInternalFieldDescription("_key_corrupt", BooleanType.BOOLEAN, "Key data is corrupt"); + + /** + * _key - Represents the key as a text column. Format is UTF-8 which may be wrong for topics. TODO: make charset configurable. + */ + public static final KafkaInternalFieldDescription KEY_FIELD = new KafkaInternalFieldDescription("_key", VarcharType.VARCHAR, "Key text"); + + /** + * _key_length - length in bytes of the key. + */ + public static final KafkaInternalFieldDescription KEY_LENGTH_FIELD = new KafkaInternalFieldDescription("_key_length", BigintType.BIGINT, "Total number of key bytes"); + + public static Set getInternalFields() + { + return ImmutableSet.of(PARTITION_ID_FIELD, PARTITION_OFFSET_FIELD, + SEGMENT_START_FIELD, SEGMENT_END_FIELD, SEGMENT_COUNT_FIELD, + KEY_FIELD, KEY_CORRUPT_FIELD, KEY_LENGTH_FIELD, + MESSAGE_FIELD, MESSAGE_CORRUPT_FIELD, MESSAGE_LENGTH_FIELD); + } + + private final String name; + private final Type type; + private final String comment; + + KafkaInternalFieldDescription( + String name, + Type type, + String comment) + { + checkArgument(!isNullOrEmpty(name), "name is null or is empty"); + this.name = name; + this.type = checkNotNull(type, "type is null"); + this.comment = checkNotNull(comment, "comment is null"); + } + + public String getName() + { + return name; + } + + public Type getType() + { + return type; + } + + KafkaColumnHandle getColumnHandle(String connectorId, int index, boolean hidden) + { + return new KafkaColumnHandle(connectorId, + index, + getName(), + getType(), + null, + null, + null, + false, + hidden, + true); + } + + ColumnMetadata getColumnMetadata(boolean hidden) + { + return new ColumnMetadata(name, type, false, comment, hidden); + } + + public KafkaFieldValueProvider forBooleanValue(boolean value) + { + return new BooleanKafkaFieldValueProvider(value); + } + + public KafkaFieldValueProvider forLongValue(long value) + { + return new LongKafkaFieldValueProvider(value); + } + + public KafkaFieldValueProvider forByteValue(byte[] value) + { + return new BytesKafkaFieldValueProvider(value); + } + + @Override + public int hashCode() + { + return Objects.hash(name, type); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + KafkaInternalFieldDescription other = (KafkaInternalFieldDescription) obj; + return Objects.equals(this.name, other.name) && + Objects.equals(this.type, other.type); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("name", name) + .add("type", type) + .toString(); + } + + public class BooleanKafkaFieldValueProvider + extends KafkaFieldValueProvider + { + private final boolean value; + + private BooleanKafkaFieldValueProvider(boolean value) + { + this.value = value; + } + + @Override + public boolean accept(KafkaColumnHandle columnHandle) + { + return columnHandle.getName().equals(name); + } + + @Override + public boolean getBoolean() + { + return value; + } + + @Override + public boolean isNull() + { + return false; + } + } + + public class LongKafkaFieldValueProvider + extends KafkaFieldValueProvider + { + private final long value; + + private LongKafkaFieldValueProvider(long value) + { + this.value = value; + } + + @Override + public boolean accept(KafkaColumnHandle columnHandle) + { + return columnHandle.getName().equals(name); + } + + @Override + public long getLong() + { + return value; + } + + @Override + public boolean isNull() + { + return false; + } + } + + public class BytesKafkaFieldValueProvider + extends KafkaFieldValueProvider + { + private final byte[] value; + + private BytesKafkaFieldValueProvider(byte[] value) + { + this.value = value; + } + + @Override + public boolean accept(KafkaColumnHandle columnHandle) + { + return columnHandle.getName().equals(name); + } + + @Override + public Slice getSlice() + { + return isNull() ? Slices.EMPTY_SLICE : Slices.wrappedBuffer(value); + } + + @Override + public boolean isNull() + { + return value == null || value.length == 0; + } + } +} diff --git a/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaMetadata.java b/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaMetadata.java new file mode 100644 index 00000000..183cb6fb --- /dev/null +++ b/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaMetadata.java @@ -0,0 +1,245 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka; + +import com.facebook.presto.kafka.decoder.dummy.DummyKafkaRowDecoder; +import com.facebook.presto.spi.ColumnMetadata; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.ConnectorTableHandle; +import com.facebook.presto.spi.ConnectorTableMetadata; +import com.facebook.presto.spi.ReadOnlyConnectorMetadata; +import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.spi.SchemaTablePrefix; +import com.facebook.presto.spi.TableNotFoundException; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.inject.name.Named; +import io.airlift.log.Logger; + +import javax.inject.Inject; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Manages the Kafka connector specific metadata information. The Connector provides an additional set of columns + * for each table that are created as hidden columns. See {@link KafkaInternalFieldDescription} for a list + * of per-topic additional columns. + */ +public class KafkaMetadata + extends ReadOnlyConnectorMetadata +{ + private static final Logger log = Logger.get(KafkaMetadata.class); + + private final String connectorId; + private final KafkaConnectorConfig kafkaConnectorConfig; + private final KafkaHandleResolver handleResolver; + + private final Supplier> kafkaTableDescriptionSupplier; + private final Set internalFieldDescriptions; + + @Inject + KafkaMetadata(@Named("connectorId") String connectorId, + KafkaConnectorConfig kafkaConnectorConfig, + KafkaHandleResolver handleResolver, + Supplier> kafkaTableDescriptionSupplier, + Set internalFieldDescriptions) + { + this.connectorId = checkNotNull(connectorId, "connectorId is null"); + this.kafkaConnectorConfig = checkNotNull(kafkaConnectorConfig, "kafkaConfig is null"); + this.handleResolver = checkNotNull(handleResolver, "handleResolver is null"); + + log.debug("Loading kafka table definitions from %s", kafkaConnectorConfig.getTableDescriptionDir().getAbsolutePath()); + + this.kafkaTableDescriptionSupplier = Suppliers.memoize(kafkaTableDescriptionSupplier); + this.internalFieldDescriptions = checkNotNull(internalFieldDescriptions, "internalFieldDescriptions is null"); + } + + @Override + public List listSchemaNames(ConnectorSession session) + { + ImmutableSet.Builder builder = ImmutableSet.builder(); + for (SchemaTableName tableName : getDefinedTables().keySet()) { + builder.add(tableName.getSchemaName()); + } + return ImmutableList.copyOf(builder.build()); + } + + @Override + public KafkaTableHandle getTableHandle(ConnectorSession session, SchemaTableName schemaTableName) + { + KafkaTopicDescription table = getDefinedTables().get(schemaTableName); + if (table == null) { + return null; + } + + return new KafkaTableHandle(connectorId, + schemaTableName.getSchemaName(), + schemaTableName.getTableName(), + table.getTopicName(), + getDataFormat(table.getKey()), + getDataFormat(table.getMessage())); + } + + private static String getDataFormat(KafkaTopicFieldGroup fieldGroup) + { + return (fieldGroup == null) ? DummyKafkaRowDecoder.NAME : fieldGroup.getDataFormat(); + } + + @Override + public ConnectorTableMetadata getTableMetadata(ConnectorTableHandle tableHandle) + { + KafkaTableHandle kafkaTableHandle = handleResolver.convertTableHandle(tableHandle); + return getTableMetadata(kafkaTableHandle.toSchemaTableName()); + } + + @Override + public List listTables(ConnectorSession session, String schemaNameOrNull) + { + ImmutableList.Builder builder = ImmutableList.builder(); + for (SchemaTableName tableName : getDefinedTables().keySet()) { + if (schemaNameOrNull == null || tableName.getSchemaName().equals(schemaNameOrNull)) { + builder.add(tableName); + } + } + + return builder.build(); + } + + @Override + public ColumnHandle getSampleWeightColumnHandle(ConnectorTableHandle tableHandle) + { + return null; + } + + @SuppressWarnings("ValueOfIncrementOrDecrementUsed") + @Override + public Map getColumnHandles(ConnectorTableHandle tableHandle) + { + KafkaTableHandle kafkaTableHandle = handleResolver.convertTableHandle(tableHandle); + + KafkaTopicDescription kafkaTopicDescription = getDefinedTables().get(kafkaTableHandle.toSchemaTableName()); + if (kafkaTopicDescription == null) { + throw new TableNotFoundException(kafkaTableHandle.toSchemaTableName()); + } + + ImmutableMap.Builder columnHandles = ImmutableMap.builder(); + + int index = 0; + KafkaTopicFieldGroup key = kafkaTopicDescription.getKey(); + if (key != null) { + List fields = key.getFields(); + if (fields != null) { + for (KafkaTopicFieldDescription kafkaTopicFieldDescription : fields) { + columnHandles.put(kafkaTopicFieldDescription.getName(), kafkaTopicFieldDescription.getColumnHandle(connectorId, true, index++)); + } + } + } + + KafkaTopicFieldGroup message = kafkaTopicDescription.getMessage(); + if (message != null) { + List fields = message.getFields(); + if (fields != null) { + for (KafkaTopicFieldDescription kafkaTopicFieldDescription : fields) { + columnHandles.put(kafkaTopicFieldDescription.getName(), kafkaTopicFieldDescription.getColumnHandle(connectorId, false, index++)); + } + } + } + + for (KafkaInternalFieldDescription kafkaInternalFieldDescription : internalFieldDescriptions) { + columnHandles.put(kafkaInternalFieldDescription.getName(), kafkaInternalFieldDescription.getColumnHandle(connectorId, index++, kafkaConnectorConfig.isHideInternalColumns())); + } + + return columnHandles.build(); + } + + @Override + public Map> listTableColumns(ConnectorSession session, SchemaTablePrefix prefix) + { + checkNotNull(prefix, "prefix is null"); + + ImmutableMap.Builder> columns = ImmutableMap.builder(); + + List tableNames = prefix.getSchemaName() == null ? listTables(session, null) : ImmutableList.of(new SchemaTableName(prefix.getSchemaName(), prefix.getTableName())); + + for (SchemaTableName tableName : tableNames) { + ConnectorTableMetadata tableMetadata = getTableMetadata(tableName); + // table can disappear during listing operation + if (tableMetadata != null) { + columns.put(tableName, tableMetadata.getColumns()); + } + } + return columns.build(); + } + + @Override + public ColumnMetadata getColumnMetadata(ConnectorTableHandle tableHandle, ColumnHandle columnHandle) + { + handleResolver.convertTableHandle(tableHandle); + KafkaColumnHandle kafkaColumnHandle = handleResolver.convertColumnHandle(columnHandle); + + return kafkaColumnHandle.getColumnMetadata(); + } + + @VisibleForTesting + Map getDefinedTables() + { + return kafkaTableDescriptionSupplier.get(); + } + + @SuppressWarnings("ValueOfIncrementOrDecrementUsed") + private ConnectorTableMetadata getTableMetadata(SchemaTableName schemaTableName) + { + KafkaTopicDescription table = getDefinedTables().get(schemaTableName); + if (table == null) { + throw new TableNotFoundException(schemaTableName); + } + + ImmutableList.Builder builder = ImmutableList.builder(); + + KafkaTopicFieldGroup key = table.getKey(); + if (key != null) { + List fields = key.getFields(); + if (fields != null) { + for (KafkaTopicFieldDescription fieldDescription : fields) { + builder.add(fieldDescription.getColumnMetadata()); + } + } + } + + KafkaTopicFieldGroup message = table.getMessage(); + if (message != null) { + List fields = message.getFields(); + if (fields != null) { + for (KafkaTopicFieldDescription fieldDescription : fields) { + builder.add(fieldDescription.getColumnMetadata()); + } + } + } + + for (KafkaInternalFieldDescription fieldDescription : internalFieldDescriptions) { + builder.add(fieldDescription.getColumnMetadata(kafkaConnectorConfig.isHideInternalColumns())); + } + + return new ConnectorTableMetadata(schemaTableName, builder.build()); + } +} diff --git a/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaPartition.java b/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaPartition.java new file mode 100644 index 00000000..edeb8eb7 --- /dev/null +++ b/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaPartition.java @@ -0,0 +1,91 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka; + +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorPartition; +import com.facebook.presto.spi.HostAddress; +import com.facebook.presto.spi.TupleDomain; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Kafka specific partition representation. Each partition maps to a topic partition and is split along segment boundaries. + */ +public class KafkaPartition + implements ConnectorPartition +{ + private final String topicName; + private final int partitionId; + private final HostAddress partitionLeader; + private final List partitionNodes; + + public KafkaPartition(String topicName, + int partitionId, + HostAddress partitionLeader, + List partitionNodes) + { + this.topicName = checkNotNull(topicName, "schema name is null"); + this.partitionId = partitionId; + this.partitionLeader = checkNotNull(partitionLeader, "partitionLeader is null"); + this.partitionNodes = ImmutableList.copyOf(checkNotNull(partitionNodes, "partitionNodes is null")); + } + + @Override + public String getPartitionId() + { + return Integer.toString(partitionId); + } + + public String getTopicName() + { + return topicName; + } + + public int getPartitionIdAsInt() + { + return partitionId; + } + + public HostAddress getPartitionLeader() + { + return partitionLeader; + } + + public List getPartitionNodes() + { + return partitionNodes; + } + + @Override + public TupleDomain getTupleDomain() + { + return TupleDomain.all(); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("topicName", topicName) + .add("partitionId", partitionId) + .add("partitionLeader", partitionLeader) + .add("partitionNodes", partitionNodes) + .toString(); + } +} diff --git a/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaPlugin.java b/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaPlugin.java new file mode 100644 index 00000000..b0a97d28 --- /dev/null +++ b/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaPlugin.java @@ -0,0 +1,77 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka; + +import com.facebook.presto.spi.ConnectorFactory; +import com.facebook.presto.spi.NodeManager; +import com.facebook.presto.spi.Plugin; +import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.spi.type.TypeManager; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Supplier; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +import javax.inject.Inject; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Presto plugin to use Apache Kafka as a data source. + */ +public class KafkaPlugin + implements Plugin +{ + private TypeManager typeManager; + private NodeManager nodeManager; + private Optional>> tableDescriptionSupplier = Optional.empty(); + private Map optionalConfig = ImmutableMap.of(); + + @Override + public synchronized void setOptionalConfig(Map optionalConfig) + { + this.optionalConfig = ImmutableMap.copyOf(checkNotNull(optionalConfig, "optionalConfig is null")); + } + + @Inject + public synchronized void setTypeManager(TypeManager typeManager) + { + this.typeManager = checkNotNull(typeManager, "typeManager is null"); + } + + @Inject + public synchronized void setNodeManager(NodeManager nodeManager) + { + this.nodeManager = checkNotNull(nodeManager, "node is null"); + } + + @VisibleForTesting + public synchronized void setTableDescriptionSupplier(Supplier> tableDescriptionSupplier) + { + this.tableDescriptionSupplier = Optional.of(checkNotNull(tableDescriptionSupplier, "tableDescriptionSupplier is null")); + } + + @Override + public synchronized List getServices(Class type) + { + if (type == ConnectorFactory.class) { + return ImmutableList.of(type.cast(new KafkaConnectorFactory(typeManager, nodeManager, tableDescriptionSupplier, optionalConfig))); + } + return ImmutableList.of(); + } +} diff --git a/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaRecordSet.java b/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaRecordSet.java new file mode 100644 index 00000000..65079732 --- /dev/null +++ b/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaRecordSet.java @@ -0,0 +1,320 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka; + +import com.facebook.presto.kafka.decoder.KafkaFieldDecoder; +import com.facebook.presto.kafka.decoder.KafkaRowDecoder; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.RecordCursor; +import com.facebook.presto.spi.RecordSet; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import io.airlift.log.Logger; +import io.airlift.slice.Slice; +import io.airlift.slice.Slices; +import kafka.api.FetchRequest; +import kafka.api.FetchRequestBuilder; +import kafka.javaapi.FetchResponse; +import kafka.javaapi.consumer.SimpleConsumer; +import kafka.message.MessageAndOffset; + +import java.nio.ByteBuffer; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +import static com.facebook.presto.kafka.KafkaErrorCode.KAFKA_SPLIT_ERROR; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Kafka specific record set. Returns a cursor for a topic which iterates over a Kafka partition segment. + */ +public class KafkaRecordSet + implements RecordSet +{ + private static final Logger log = Logger.get(KafkaRecordSet.class); + + private static final int KAFKA_READ_BUFFER_SIZE = 100_000; + private static final byte [] EMPTY_BYTE_ARRAY = new byte [0]; + + private final KafkaSplit split; + private final KafkaSimpleConsumerManager consumerManager; + + private final KafkaRowDecoder keyDecoder; + private final KafkaRowDecoder messageDecoder; + private final Map> keyFieldDecoders; + private final Map> messageFieldDecoders; + + private final List columnHandles; + private final List columnTypes; + + private final Set globalInternalFieldValueProviders; + + KafkaRecordSet(KafkaSplit split, + KafkaSimpleConsumerManager consumerManager, + List columnHandles, + KafkaRowDecoder keyDecoder, + KafkaRowDecoder messageDecoder, + Map> keyFieldDecoders, + Map> messageFieldDecoders) + { + this.split = checkNotNull(split, "split is null"); + + this.globalInternalFieldValueProviders = ImmutableSet.of( + KafkaInternalFieldDescription.PARTITION_ID_FIELD.forLongValue(split.getPartitionId()), + KafkaInternalFieldDescription.SEGMENT_START_FIELD.forLongValue(split.getStart()), + KafkaInternalFieldDescription.SEGMENT_END_FIELD.forLongValue(split.getEnd())); + + this.consumerManager = checkNotNull(consumerManager, "consumerManager is null"); + + this.keyDecoder = checkNotNull(keyDecoder, "rowDecoder is null"); + this.messageDecoder = checkNotNull(messageDecoder, "rowDecoder is null"); + this.keyFieldDecoders = checkNotNull(keyFieldDecoders, "keyFieldDecoders is null"); + this.messageFieldDecoders = checkNotNull(messageFieldDecoders, "messageFieldDecoders is null"); + + this.columnHandles = checkNotNull(columnHandles, "columnHandles is null"); + + ImmutableList.Builder typeBuilder = ImmutableList.builder(); + + for (KafkaColumnHandle handle : columnHandles) { + typeBuilder.add(handle.getType()); + } + + this.columnTypes = typeBuilder.build(); + } + + @Override + public List getColumnTypes() + { + return columnTypes; + } + + @Override + public RecordCursor cursor() + { + return new KafkaRecordCursor(); + } + + public class KafkaRecordCursor + implements RecordCursor + { + private long totalBytes; + private long totalMessages; + private long cursorOffset = split.getStart(); + private Iterator messageAndOffsetIterator; + private final AtomicBoolean reported = new AtomicBoolean(); + + private KafkaFieldValueProvider[] fieldValueProviders; + + KafkaRecordCursor() + { + } + + @Override + public long getTotalBytes() + { + return totalBytes; + } + + @Override + public long getCompletedBytes() + { + return totalBytes; + } + + @Override + public long getReadTimeNanos() + { + return 0; + } + + @Override + public Type getType(int field) + { + checkArgument(field < columnHandles.size(), "Invalid field index"); + return columnHandles.get(field).getType(); + } + + @Override + public boolean advanceNextPosition() + { + while (true) { + if (cursorOffset >= split.getEnd()) { + return endOfData(); // Split end is exclusive. + } + // Create a fetch request + openFetchRequest(); + + while (messageAndOffsetIterator.hasNext()) { + MessageAndOffset currentMessageAndOffset = messageAndOffsetIterator.next(); + long messageOffset = currentMessageAndOffset.offset(); + + if (messageOffset >= split.getEnd()) { + return endOfData(); // Past our split end. Bail. + } + + if (messageOffset >= cursorOffset) { + return nextRow(currentMessageAndOffset); + } + } + messageAndOffsetIterator = null; + } + } + + private boolean endOfData() + { + if (!reported.getAndSet(true)) { + log.debug("Found a total of %d messages with %d bytes (%d messages expected). Last Offset: %d (%d, %d)", + totalMessages, totalBytes, split.getEnd() - split.getStart(), + cursorOffset, split.getStart(), split.getEnd()); + } + return false; + } + + private boolean nextRow(MessageAndOffset messageAndOffset) + { + cursorOffset = messageAndOffset.offset() + 1; // Cursor now points to the next message. + totalBytes += messageAndOffset.message().payloadSize(); + totalMessages++; + + byte[] keyData = EMPTY_BYTE_ARRAY; + byte[] messageData = EMPTY_BYTE_ARRAY; + ByteBuffer key = messageAndOffset.message().key(); + if (key != null) { + keyData = new byte[key.remaining()]; + key.get(keyData); + } + + ByteBuffer message = messageAndOffset.message().payload(); + if (message != null) { + messageData = new byte[message.remaining()]; + message.get(messageData); + } + + Set fieldValueProviders = new HashSet<>(); + + fieldValueProviders.addAll(globalInternalFieldValueProviders); + fieldValueProviders.add(KafkaInternalFieldDescription.SEGMENT_COUNT_FIELD.forLongValue(totalMessages)); + fieldValueProviders.add(KafkaInternalFieldDescription.PARTITION_OFFSET_FIELD.forLongValue(messageAndOffset.offset())); + fieldValueProviders.add(KafkaInternalFieldDescription.MESSAGE_FIELD.forByteValue(messageData)); + fieldValueProviders.add(KafkaInternalFieldDescription.MESSAGE_LENGTH_FIELD.forLongValue(messageData.length)); + fieldValueProviders.add(KafkaInternalFieldDescription.KEY_FIELD.forByteValue(keyData)); + fieldValueProviders.add(KafkaInternalFieldDescription.KEY_LENGTH_FIELD.forLongValue(keyData.length)); + fieldValueProviders.add(KafkaInternalFieldDescription.KEY_CORRUPT_FIELD.forBooleanValue(keyDecoder.decodeRow(keyData, fieldValueProviders, columnHandles, keyFieldDecoders))); + fieldValueProviders.add(KafkaInternalFieldDescription.MESSAGE_CORRUPT_FIELD.forBooleanValue(messageDecoder.decodeRow(messageData, fieldValueProviders, columnHandles, messageFieldDecoders))); + + this.fieldValueProviders = new KafkaFieldValueProvider[columnHandles.size()]; + + // If a value provider for a requested internal column is present, assign the + // value to the internal cache. It is possible that an internal column is present + // where no value provider exists (e.g. the '_corrupt' column with the DummyRowDecoder). + // In that case, the cache is null (and the column is reported as null). + for (int i = 0; i < columnHandles.size(); i++) { + for (KafkaFieldValueProvider fieldValueProvider : fieldValueProviders) { + if (fieldValueProvider.accept(columnHandles.get(i))) { + this.fieldValueProviders[i] = fieldValueProvider; + break; // for(InternalColumnProvider... + } + } + } + + return true; // Advanced successfully. + } + + @SuppressWarnings("SimplifiableConditionalExpression") + @Override + public boolean getBoolean(int field) + { + checkArgument(field < columnHandles.size(), "Invalid field index"); + + checkFieldType(field, boolean.class); + return isNull(field) ? false : fieldValueProviders[field].getBoolean(); + } + + @Override + public long getLong(int field) + { + checkArgument(field < columnHandles.size(), "Invalid field index"); + + checkFieldType(field, long.class); + return isNull(field) ? 0L : fieldValueProviders[field].getLong(); + } + + @Override + public double getDouble(int field) + { + checkArgument(field < columnHandles.size(), "Invalid field index"); + + checkFieldType(field, double.class); + return isNull(field) ? 0.0d : fieldValueProviders[field].getDouble(); + } + + @Override + public Slice getSlice(int field) + { + checkArgument(field < columnHandles.size(), "Invalid field index"); + + checkFieldType(field, Slice.class); + return isNull(field) ? Slices.EMPTY_SLICE : fieldValueProviders[field].getSlice(); + } + + @Override + public boolean isNull(int field) + { + checkArgument(field < columnHandles.size(), "Invalid field index"); + + return fieldValueProviders[field] == null || fieldValueProviders[field].isNull(); + } + + private void checkFieldType(int field, Class expected) + { + Class actual = getType(field).getJavaType(); + checkArgument(actual == expected, "Expected field %s to be type %s but is %s", field, expected, actual); + } + + @Override + public void close() + { + } + + private void openFetchRequest() + { + if (messageAndOffsetIterator == null) { + log.debug("Fetching %d bytes from offset %d (%d - %d). %d messages read so far", KAFKA_READ_BUFFER_SIZE, cursorOffset, split.getStart(), split.getEnd(), totalMessages); + FetchRequest req = new FetchRequestBuilder() + .clientId("presto-worker-" + Thread.currentThread().getName()) + .addFetch(split.getTopicName(), split.getPartitionId(), cursorOffset, KAFKA_READ_BUFFER_SIZE) + .build(); + + // TODO - this should look at the actual node this is running on and prefer + // that copy if running locally. - look into NodeInfo + SimpleConsumer consumer = consumerManager.getConsumer(split.getNodes().get(0)); + + FetchResponse fetchResponse = consumer.fetch(req); + if (fetchResponse.hasError()) { + short errorCode = fetchResponse.errorCode(split.getTopicName(), split.getPartitionId()); + log.warn("Fetch response has error: %d", errorCode); + throw new PrestoException(KAFKA_SPLIT_ERROR, "could not fetch data from Kafka, error code is '" + errorCode + "'"); + } + + messageAndOffsetIterator = fetchResponse.messageSet(split.getTopicName(), split.getPartitionId()).iterator(); + } + } + } +} diff --git a/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaRecordSetProvider.java b/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaRecordSetProvider.java new file mode 100644 index 00000000..ee101f76 --- /dev/null +++ b/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaRecordSetProvider.java @@ -0,0 +1,95 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka; + +import com.facebook.presto.kafka.decoder.KafkaDecoderRegistry; +import com.facebook.presto.kafka.decoder.KafkaFieldDecoder; +import com.facebook.presto.kafka.decoder.KafkaRowDecoder; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorRecordSetProvider; +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.RecordSet; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +import javax.inject.Inject; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Factory for Kafka specific {@link RecordSet} instances. + */ +public class KafkaRecordSetProvider + implements ConnectorRecordSetProvider +{ + private final KafkaHandleResolver handleResolver; + private final KafkaSimpleConsumerManager consumerManager; + private final KafkaDecoderRegistry registry; + + @Inject + public KafkaRecordSetProvider( + KafkaDecoderRegistry registry, + KafkaHandleResolver handleResolver, + KafkaSimpleConsumerManager consumerManager) + { + this.registry = checkNotNull(registry, "registry is null"); + this.handleResolver = checkNotNull(handleResolver, "handleResolver is null"); + this.consumerManager = checkNotNull(consumerManager, "consumerManager is null"); + } + + @Override + public RecordSet getRecordSet(ConnectorSplit split, List columns) + { + KafkaSplit kafkaSplit = handleResolver.convertSplit(split); + + ImmutableList.Builder handleBuilder = ImmutableList.builder(); + ImmutableMap.Builder> keyFieldDecoderBuilder = ImmutableMap.builder(); + ImmutableMap.Builder> messageFieldDecoderBuilder = ImmutableMap.builder(); + + KafkaRowDecoder keyDecoder = registry.getRowDecoder(kafkaSplit.getKeyDataFormat()); + KafkaRowDecoder messageDecoder = registry.getRowDecoder(kafkaSplit.getMessageDataFormat()); + + for (ColumnHandle handle : columns) { + KafkaColumnHandle columnHandle = handleResolver.convertColumnHandle(handle); + handleBuilder.add(columnHandle); + + if (!columnHandle.isInternal()) { + if (columnHandle.isKeyDecoder()) { + KafkaFieldDecoder fieldDecoder = registry.getFieldDecoder( + kafkaSplit.getKeyDataFormat(), + columnHandle.getType().getJavaType(), + columnHandle.getDataFormat()); + + keyFieldDecoderBuilder.put(columnHandle, fieldDecoder); + } + else { + KafkaFieldDecoder fieldDecoder = registry.getFieldDecoder( + kafkaSplit.getMessageDataFormat(), + columnHandle.getType().getJavaType(), + columnHandle.getDataFormat()); + + messageFieldDecoderBuilder.put(columnHandle, fieldDecoder); + } + } + } + + ImmutableList handles = handleBuilder.build(); + ImmutableMap> keyFieldDecoders = keyFieldDecoderBuilder.build(); + ImmutableMap> messageFieldDecoders = messageFieldDecoderBuilder.build(); + + return new KafkaRecordSet(kafkaSplit, consumerManager, handles, keyDecoder, messageDecoder, keyFieldDecoders, messageFieldDecoders); + } +} diff --git a/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaSimpleConsumerManager.java b/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaSimpleConsumerManager.java new file mode 100644 index 00000000..2f47d003 --- /dev/null +++ b/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaSimpleConsumerManager.java @@ -0,0 +1,101 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka; + +import com.facebook.presto.spi.HostAddress; +import com.facebook.presto.spi.NodeManager; +import com.google.common.base.Throwables; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.primitives.Ints; +import io.airlift.log.Logger; +import kafka.javaapi.consumer.SimpleConsumer; + +import javax.annotation.PreDestroy; +import javax.inject.Inject; +import javax.inject.Named; + +import java.util.Map; +import java.util.concurrent.ExecutionException; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.lang.String.format; + +/** + * Manages connections to the Kafka nodes. A worker may connect to multiple Kafka nodes depending on the segments and partitions + * it needs to process. According to the Kafka source code, a Kafka {@link kafka.javaapi.consumer.SimpleConsumer} is thread-safe. + */ +public class KafkaSimpleConsumerManager +{ + private static final Logger log = Logger.get(KafkaSimpleConsumerManager.class); + + private final LoadingCache consumerCache; + + private final String connectorId; + private final KafkaConnectorConfig kafkaConnectorConfig; + private final NodeManager nodeManager; + + @Inject + KafkaSimpleConsumerManager(@Named("connectorId") String connectorId, + KafkaConnectorConfig kafkaConnectorConfig, + NodeManager nodeManager) + { + this.connectorId = checkNotNull(connectorId, "connectorId is null"); + this.kafkaConnectorConfig = checkNotNull(kafkaConnectorConfig, "kafkaConfig is null"); + this.nodeManager = checkNotNull(nodeManager, "nodeManager is null"); + + this.consumerCache = CacheBuilder.newBuilder().build(new SimpleConsumerCacheLoader()); + } + + @PreDestroy + public void tearDown() + { + for (Map.Entry entry : consumerCache.asMap().entrySet()) { + try { + entry.getValue().close(); + } + catch (Exception e) { + log.warn(e, "While closing consumer %s:", entry.getKey()); + } + } + } + + public SimpleConsumer getConsumer(HostAddress host) + { + checkNotNull(host, "host is null"); + try { + return consumerCache.get(host); + } + catch (ExecutionException e) { + throw Throwables.propagate(e.getCause()); + } + } + + private class SimpleConsumerCacheLoader + extends CacheLoader + { + @Override + public SimpleConsumer load(HostAddress host) + throws Exception + { + log.info("Creating new Consumer for %s", host); + return new SimpleConsumer(host.getHostText(), + host.getPort(), + Ints.checkedCast(kafkaConnectorConfig.getKafkaConnectTimeout().toMillis()), + Ints.checkedCast(kafkaConnectorConfig.getKafkaBufferSize().toBytes()), + format("presto-kafka-%s-%s", connectorId, nodeManager.getCurrentNode().getNodeIdentifier())); + } + } +} diff --git a/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaSplit.java b/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaSplit.java new file mode 100644 index 00000000..9d5a62b5 --- /dev/null +++ b/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaSplit.java @@ -0,0 +1,148 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka; + +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.HostAddress; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Represents a kafka specific {@link ConnectorSplit}. Each split is mapped to a segment file on disk (based off the segment offset start() and end() values) so that + * a partition can be processed by reading segment files off different Kafka nodes (in case of replication) and by different workers. Otherwise, a Kafka topic could only + * be processed along partition boundaries. + *

+ * When planning to process a Kafka topic with Presto, using smaller than the recommended segment size (default is 1G) allows Presto to optimize early and process a topic + * with more workers in parallel. + */ +public class KafkaSplit + implements ConnectorSplit +{ + private final String connectorId; + private final String topicName; + private final String keyDataFormat; + private final String messageDataFormat; + private final int partitionId; + private final long start; + private final long end; + private final List nodes; + + @JsonCreator + public KafkaSplit( + @JsonProperty("connectorId") String connectorId, + @JsonProperty("topicName") String topicName, + @JsonProperty("keyDataFormat") String keyDataFormat, + @JsonProperty("messageDataFormat") String messageDataFormat, + @JsonProperty("partitionId") int partitionId, + @JsonProperty("start") long start, + @JsonProperty("end") long end, + @JsonProperty("nodes") List nodes) + { + this.connectorId = checkNotNull(connectorId, "connector id is null"); + this.topicName = checkNotNull(topicName, "topicName is null"); + this.keyDataFormat = checkNotNull(keyDataFormat, "dataFormat is null"); + this.messageDataFormat = checkNotNull(messageDataFormat, "messageDataFormat is null"); + this.partitionId = partitionId; + this.start = start; + this.end = end; + this.nodes = ImmutableList.copyOf(checkNotNull(nodes, "addresses is null")); + } + + @JsonProperty + public String getConnectorId() + { + return connectorId; + } + + @JsonProperty + public long getStart() + { + return start; + } + + @JsonProperty + public long getEnd() + { + return end; + } + + @JsonProperty + public String getTopicName() + { + return topicName; + } + + @JsonProperty + public String getKeyDataFormat() + { + return keyDataFormat; + } + + @JsonProperty + public String getMessageDataFormat() + { + return messageDataFormat; + } + + @JsonProperty + public int getPartitionId() + { + return partitionId; + } + + @JsonProperty + public List getNodes() + { + return nodes; + } + + @Override + public boolean isRemotelyAccessible() + { + return true; + } + + @Override + public List getAddresses() + { + return nodes; + } + + @Override + public Object getInfo() + { + return this; + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("connectorId", connectorId) + .add("topicName", topicName) + .add("keyDataFormat", keyDataFormat) + .add("messageDataFormat", messageDataFormat) + .add("partitionId", partitionId) + .add("start", start) + .add("end", end) + .add("nodes", nodes) + .toString(); + } +} diff --git a/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaSplitManager.java b/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaSplitManager.java new file mode 100644 index 00000000..5c15ffc4 --- /dev/null +++ b/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaSplitManager.java @@ -0,0 +1,193 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka; + +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorPartition; +import com.facebook.presto.spi.ConnectorPartitionResult; +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.ConnectorSplitManager; +import com.facebook.presto.spi.ConnectorSplitSource; +import com.facebook.presto.spi.ConnectorTableHandle; +import com.facebook.presto.spi.FixedSplitSource; +import com.facebook.presto.spi.HostAddress; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.TableNotFoundException; +import com.facebook.presto.spi.TupleDomain; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.airlift.log.Logger; +import kafka.api.PartitionOffsetRequestInfo; +import kafka.cluster.Broker; +import kafka.common.TopicAndPartition; +import kafka.javaapi.OffsetRequest; +import kafka.javaapi.OffsetResponse; +import kafka.javaapi.PartitionMetadata; +import kafka.javaapi.TopicMetadata; +import kafka.javaapi.TopicMetadataRequest; +import kafka.javaapi.TopicMetadataResponse; +import kafka.javaapi.consumer.SimpleConsumer; + +import javax.inject.Inject; +import javax.inject.Named; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static com.facebook.presto.kafka.KafkaErrorCode.KAFKA_SPLIT_ERROR; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +/** + * Kafka specific implementation of {@link ConnectorSplitManager}. + */ +public class KafkaSplitManager + implements ConnectorSplitManager +{ + private static final Logger log = Logger.get(KafkaSplitManager.class); + + private final String connectorId; + private final KafkaConnectorConfig kafkaConnectorConfig; + private final KafkaHandleResolver handleResolver; + private final KafkaSimpleConsumerManager consumerManager; + + @Inject + public KafkaSplitManager(@Named("connectorId") String connectorId, + KafkaConnectorConfig kafkaConnectorConfig, + KafkaHandleResolver handleResolver, + KafkaSimpleConsumerManager consumerManager) + { + this.connectorId = checkNotNull(connectorId, "connectorId is null"); + this.kafkaConnectorConfig = checkNotNull(kafkaConnectorConfig, "kafkaConfig is null"); + this.handleResolver = checkNotNull(handleResolver, "handleResolver is null"); + this.consumerManager = checkNotNull(consumerManager, "consumerManager is null"); + } + + @Override + public ConnectorPartitionResult getPartitions(ConnectorTableHandle tableHandle, TupleDomain tupleDomain) + { + KafkaTableHandle kafkaTableHandle = handleResolver.convertTableHandle(tableHandle); + + List nodes = new ArrayList<>(kafkaConnectorConfig.getNodes()); + Collections.shuffle(nodes); + + SimpleConsumer simpleConsumer = consumerManager.getConsumer(nodes.get(0)); + + try { + TopicMetadataRequest topicMetadataRequest = new TopicMetadataRequest(ImmutableList.of(kafkaTableHandle.getTopicName())); + TopicMetadataResponse topicMetadataResponse = simpleConsumer.send(topicMetadataRequest); + + ImmutableList.Builder builder = ImmutableList.builder(); + + for (TopicMetadata metadata : topicMetadataResponse.topicsMetadata()) { + for (PartitionMetadata part : metadata.partitionsMetadata()) { + log.debug("Adding Partition %s/%s", metadata.topic(), part.partitionId()); + Broker leader = part.leader(); + if (leader == null) { // Leader election going on... + log.warn("No leader for partition %s/%s found!", metadata.topic(), part.partitionId()); + } + else { + // make sure the first broker is the partition leader + ImmutableList.Builder partitionNodesBuilder = ImmutableList.builder(); + HostAddress partitionLeader = brokerToHostAddress(leader); + + boolean isLeaderFirst = true; + if (part.isr().get(0).id() != leader.id()) { + isLeaderFirst = false; + } + + if (!isLeaderFirst) { + partitionNodesBuilder.add(partitionLeader); + } + for (Broker broker : part.isr()) { + if (isLeaderFirst || broker.id() != leader.id()) { + partitionNodesBuilder.add(brokerToHostAddress(broker)); + } + } + + builder.add(new KafkaPartition(metadata.topic(), + part.partitionId(), + partitionLeader, + partitionNodesBuilder.build())); + } + } + } + + return new ConnectorPartitionResult(builder.build(), tupleDomain); + } + catch (Exception e) { + throw new TableNotFoundException(kafkaTableHandle.toSchemaTableName(), e); + } + } + + @Override + public ConnectorSplitSource getPartitionSplits(ConnectorTableHandle tableHandle, List partitions) + { + KafkaTableHandle kafkaTableHandle = handleResolver.convertTableHandle(tableHandle); + + ImmutableList.Builder builder = ImmutableList.builder(); + + for (ConnectorPartition cp : partitions) { + checkState(cp instanceof KafkaPartition, "Found an unknown partition type: %s", cp.getClass().getSimpleName()); + KafkaPartition partition = (KafkaPartition) cp; + + SimpleConsumer leaderConsumer = consumerManager.getConsumer(partition.getPartitionLeader()); + // Kafka contains a reverse list of "end - start" pairs for the splits + + long[] offsets = findAllOffsets(leaderConsumer, partition); + + for (int i = offsets.length - 1; i > 0; i--) { + KafkaSplit split = new KafkaSplit(connectorId, + partition.getTopicName(), + kafkaTableHandle.getKeyDataFormat(), + kafkaTableHandle.getMessageDataFormat(), + partition.getPartitionIdAsInt(), + offsets[i], + offsets[i - 1], + partition.getPartitionNodes()); + builder.add(split); + } + } + + return new FixedSplitSource(connectorId, builder.build()); + } + + private static long[] findAllOffsets(SimpleConsumer consumer, KafkaPartition partition) + { + TopicAndPartition topicAndPartition = new TopicAndPartition(partition.getTopicName(), partition.getPartitionIdAsInt()); + + // The API implies that this will always return all of the offsets. So it seems a partition can not have + // more than Integer.MAX_VALUE-1 segments. + // + // This also assumes that the lowest value returned will be the first segment available. So if segments have been dropped off, this value + // should not be 0. + PartitionOffsetRequestInfo partitionOffsetRequestInfo = new PartitionOffsetRequestInfo(kafka.api.OffsetRequest.LatestTime(), Integer.MAX_VALUE); + OffsetRequest offsetRequest = new OffsetRequest(ImmutableMap.of(topicAndPartition, partitionOffsetRequestInfo), kafka.api.OffsetRequest.CurrentVersion(), consumer.clientId()); + OffsetResponse offsetResponse = consumer.getOffsetsBefore(offsetRequest); + + if (offsetResponse.hasError()) { + short errorCode = offsetResponse.errorCode(partition.getTopicName(), partition.getPartitionIdAsInt()); + log.warn("Offset response has error: %d", errorCode); + throw new PrestoException(KAFKA_SPLIT_ERROR, "could not fetch data from Kafka, error code is '" + errorCode + "'"); + } + + return offsetResponse.offsets(partition.getTopicName(), partition.getPartitionIdAsInt()); + } + + private HostAddress brokerToHostAddress(Broker broker) + { + return HostAddress.fromParts(broker.host(), broker.port()); + } +} diff --git a/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaTableDescriptionSupplier.java b/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaTableDescriptionSupplier.java new file mode 100644 index 00000000..c39389b4 --- /dev/null +++ b/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaTableDescriptionSupplier.java @@ -0,0 +1,117 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka; + +import com.facebook.presto.kafka.decoder.dummy.DummyKafkaRowDecoder; +import com.facebook.presto.spi.SchemaTableName; +import com.google.common.base.Supplier; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.io.Files; +import io.airlift.json.JsonCodec; +import io.airlift.log.Logger; + +import javax.inject.Inject; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import static com.google.common.base.MoreObjects.firstNonNull; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.Arrays.asList; + +public class KafkaTableDescriptionSupplier + implements Supplier> +{ + private static final Logger log = Logger.get(KafkaTableDescriptionSupplier.class); + + private final KafkaConnectorConfig kafkaConnectorConfig; + private final JsonCodec topicDescriptionCodec; + + @Inject + KafkaTableDescriptionSupplier(KafkaConnectorConfig kafkaConnectorConfig, + JsonCodec topicDescriptionCodec) + { + this.kafkaConnectorConfig = checkNotNull(kafkaConnectorConfig, "kafkaConnectorConfig is null"); + this.topicDescriptionCodec = checkNotNull(topicDescriptionCodec, "topicDescriptionCodec is null"); + } + + @Override + public Map get() + { + ImmutableMap.Builder builder = ImmutableMap.builder(); + + try { + for (File file : listFiles(kafkaConnectorConfig.getTableDescriptionDir())) { + if (file.isFile() && file.getName().endsWith(".json")) { + KafkaTopicDescription table = topicDescriptionCodec.fromJson(Files.toByteArray(file)); + String schemaName = firstNonNull(table.getSchemaName(), kafkaConnectorConfig.getDefaultSchema()); + log.debug("Kafka table %s.%s: %s", schemaName, table.getTableName(), table); + builder.put(new SchemaTableName(schemaName, table.getTableName()), table); + } + } + + Map tableDefinitions = builder.build(); + + log.debug("Loaded Table definitions: %s", tableDefinitions.keySet()); + + builder = ImmutableMap.builder(); + for (String definedTable : kafkaConnectorConfig.getTableNames()) { + SchemaTableName tableName; + try { + tableName = SchemaTableName.valueOf(definedTable); + } + catch (IllegalArgumentException iae) { + tableName = new SchemaTableName(kafkaConnectorConfig.getDefaultSchema(), definedTable); + } + + if (tableDefinitions.containsKey(tableName)) { + KafkaTopicDescription kafkaTable = tableDefinitions.get(tableName); + log.debug("Found Table definition for %s: %s", tableName, kafkaTable); + builder.put(tableName, kafkaTable); + } + else { + // A dummy table definition only supports the internal columns. + log.debug("Created dummy Table definition for %s", tableName); + builder.put(tableName, new KafkaTopicDescription(tableName.getTableName(), + tableName.getSchemaName(), + definedTable, + new KafkaTopicFieldGroup(DummyKafkaRowDecoder.NAME, ImmutableList.of()), + new KafkaTopicFieldGroup(DummyKafkaRowDecoder.NAME, ImmutableList.of()))); + } + } + + return builder.build(); + } + catch (IOException e) { + log.warn(e, "Error: "); + throw Throwables.propagate(e); + } + } + + private static List listFiles(File dir) + { + if ((dir != null) && dir.isDirectory()) { + File[] files = dir.listFiles(); + if (files != null) { + log.debug("Considering files: %s", asList(files)); + return ImmutableList.copyOf(files); + } + } + return ImmutableList.of(); + } +} diff --git a/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaTableHandle.java b/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaTableHandle.java new file mode 100644 index 00000000..d85c88d6 --- /dev/null +++ b/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaTableHandle.java @@ -0,0 +1,151 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka; + +import com.facebook.presto.spi.ConnectorTableHandle; +import com.facebook.presto.spi.SchemaTableName; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Kafka specific {@link ConnectorTableHandle}. + */ +public final class KafkaTableHandle + implements ConnectorTableHandle +{ + /** + * connector id + */ + private final String connectorId; + + /** + * The schema name for this table. Is set through configuration and read + * using {@link KafkaConnectorConfig#getDefaultSchema()}. Usually 'default'. + */ + private final String schemaName; + + /** + * The table name used by presto. + */ + private final String tableName; + + /** + * The topic name that is read from Kafka. + */ + private final String topicName; + + private final String keyDataFormat; + private final String messageDataFormat; + + @JsonCreator + public KafkaTableHandle( + @JsonProperty("connectorId") String connectorId, + @JsonProperty("schemaName") String schemaName, + @JsonProperty("tableName") String tableName, + @JsonProperty("topicName") String topicName, + @JsonProperty("keyDataFormat") String keyDataFormat, + @JsonProperty("messageDataFormat") String messageDataFormat) + { + this.connectorId = checkNotNull(connectorId, "connectorId is null"); + this.schemaName = checkNotNull(schemaName, "schemaName is null"); + this.tableName = checkNotNull(tableName, "tableName is null"); + this.topicName = checkNotNull(topicName, "topicName is null"); + this.keyDataFormat = checkNotNull(keyDataFormat, "keyDataFormat is null"); + this.messageDataFormat = checkNotNull(messageDataFormat, "messageDataFormat is null"); + } + + @JsonProperty + public String getConnectorId() + { + return connectorId; + } + + @JsonProperty + public String getSchemaName() + { + return schemaName; + } + + @JsonProperty + public String getTableName() + { + return tableName; + } + + @JsonProperty + public String getTopicName() + { + return topicName; + } + + @JsonProperty + public String getKeyDataFormat() + { + return keyDataFormat; + } + + @JsonProperty + public String getMessageDataFormat() + { + return messageDataFormat; + } + + public SchemaTableName toSchemaTableName() + { + return new SchemaTableName(schemaName, tableName); + } + + @Override + public int hashCode() + { + return Objects.hash(connectorId, schemaName, tableName, topicName, keyDataFormat, messageDataFormat); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + KafkaTableHandle other = (KafkaTableHandle) obj; + return Objects.equals(this.connectorId, other.connectorId) + && Objects.equals(this.schemaName, other.schemaName) + && Objects.equals(this.tableName, other.tableName) + && Objects.equals(this.topicName, other.topicName) + && Objects.equals(this.keyDataFormat, other.keyDataFormat) + && Objects.equals(this.messageDataFormat, other.messageDataFormat); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("connectorId", connectorId) + .add("schemaName", schemaName) + .add("tableName", tableName) + .add("topicName", topicName) + .add("keyDataFormat", keyDataFormat) + .add("messageDataFormat", messageDataFormat) + .toString(); + } +} diff --git a/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaTopicDescription.java b/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaTopicDescription.java new file mode 100644 index 00000000..71ff3d4e --- /dev/null +++ b/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaTopicDescription.java @@ -0,0 +1,92 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Strings.isNullOrEmpty; + +/** + * Json description to parse a row on a Kafka topic. A row contains a message and an optional key. See the documentation for the exact JSON syntax. + */ +public class KafkaTopicDescription +{ + private final String tableName; + private final String topicName; + private final String schemaName; + private final KafkaTopicFieldGroup key; + private final KafkaTopicFieldGroup message; + + @JsonCreator + public KafkaTopicDescription( + @JsonProperty("tableName") String tableName, + @JsonProperty("schemaName") String schemaName, + @JsonProperty("topicName") String topicName, + @JsonProperty("key") KafkaTopicFieldGroup key, + @JsonProperty("message") KafkaTopicFieldGroup message) + { + checkArgument(!isNullOrEmpty(tableName), "tableName is null or is empty"); + this.tableName = tableName; + this.topicName = checkNotNull(topicName, "topicName is null"); + this.schemaName = schemaName; + this.key = key; + this.message = message; + } + + @JsonProperty + public String getTableName() + { + return tableName; + } + + @JsonProperty + public String getTopicName() + { + return topicName; + } + + @JsonProperty + public String getSchemaName() + { + return schemaName; + } + + @JsonProperty + public KafkaTopicFieldGroup getKey() + { + return key; + } + + @JsonProperty + public KafkaTopicFieldGroup getMessage() + { + return message; + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("tableName", tableName) + .add("topicName", topicName) + .add("schemaName", schemaName) + .add("key", key) + .add("message", message) + .toString(); + } +} diff --git a/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaTopicFieldDescription.java b/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaTopicFieldDescription.java new file mode 100644 index 00000000..f4cc38cb --- /dev/null +++ b/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaTopicFieldDescription.java @@ -0,0 +1,159 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka; + +import com.facebook.presto.spi.ColumnMetadata; +import com.facebook.presto.spi.type.Type; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Strings.isNullOrEmpty; + +/** + * Json description to parse a single field from a Kafka topic message. See {@link com.facebook.presto.kafka.KafkaTopicDescription} for more details. + */ +public final class KafkaTopicFieldDescription +{ + private final String name; + private final Type type; + private final String mapping; + private final String comment; + private final String dataFormat; + private final String formatHint; + private final boolean hidden; + + @JsonCreator + public KafkaTopicFieldDescription( + @JsonProperty("name") String name, + @JsonProperty("type") Type type, + @JsonProperty("mapping") String mapping, + @JsonProperty("comment") String comment, + @JsonProperty("dataFormat") String dataFormat, + @JsonProperty("formatHint") String formatHint, + @JsonProperty("hidden") boolean hidden) + { + checkArgument(!isNullOrEmpty(name), "name is null or is empty"); + this.name = name; + this.type = checkNotNull(type, "type is null"); + this.mapping = mapping; + this.comment = comment; + this.dataFormat = dataFormat; + this.formatHint = formatHint; + this.hidden = hidden; + } + + @JsonProperty + public String getName() + { + return name; + } + + @JsonProperty + public Type getType() + { + return type; + } + + @JsonProperty + public String getMapping() + { + return mapping; + } + + @JsonProperty + public String getComment() + { + return comment; + } + + @JsonProperty + public String getDataFormat() + { + return dataFormat; + } + + @JsonProperty + public String getFormatHint() + { + return formatHint; + } + + @JsonProperty + public boolean isHidden() + { + return hidden; + } + + KafkaColumnHandle getColumnHandle(String connectorId, boolean keyDecoder, int index) + { + return new KafkaColumnHandle(connectorId, + index, + getName(), + getType(), + getMapping(), + getDataFormat(), + getFormatHint(), + keyDecoder, + isHidden(), + false); + } + + ColumnMetadata getColumnMetadata() + { + return new ColumnMetadata(getName(), getType(), false, getComment(), isHidden()); + } + + @Override + public int hashCode() + { + return Objects.hash(name, type, mapping, dataFormat, formatHint, hidden); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + KafkaTopicFieldDescription other = (KafkaTopicFieldDescription) obj; + return Objects.equals(this.name, other.name) && + Objects.equals(this.type, other.type) && + Objects.equals(this.mapping, other.mapping) && + Objects.equals(this.dataFormat, other.dataFormat) && + Objects.equals(this.formatHint, other.formatHint) && + Objects.equals(this.hidden, other.hidden); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("name", name) + .add("type", type) + .add("mapping", mapping) + .add("dataFormat", dataFormat) + .add("formatHint", formatHint) + .add("hidden", hidden) + .toString(); + } +} diff --git a/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaTopicFieldGroup.java b/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaTopicFieldGroup.java new file mode 100644 index 00000000..8ff1c503 --- /dev/null +++ b/presto-kafka/src/main/java/com/facebook/presto/kafka/KafkaTopicFieldGroup.java @@ -0,0 +1,62 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Groups the field descriptions for message or key. + */ +public class KafkaTopicFieldGroup +{ + private final String dataFormat; + private final List fields; + + @JsonCreator + public KafkaTopicFieldGroup( + @JsonProperty("dataFormat") String dataFormat, + @JsonProperty("fields") List fields) + { + this.dataFormat = checkNotNull(dataFormat, "dataFormat is null"); + this.fields = ImmutableList.copyOf(checkNotNull(fields, "fields is null")); + } + + @JsonProperty + public String getDataFormat() + { + return dataFormat; + } + + @JsonProperty + public List getFields() + { + return fields; + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("dataFormat", dataFormat) + .add("fields", fields) + .toString(); + } +} diff --git a/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/KafkaDecoderModule.java b/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/KafkaDecoderModule.java new file mode 100644 index 00000000..540ed7db --- /dev/null +++ b/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/KafkaDecoderModule.java @@ -0,0 +1,54 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka.decoder; + +import com.facebook.presto.kafka.decoder.csv.CsvKafkaDecoderModule; +import com.facebook.presto.kafka.decoder.dummy.DummyKafkaDecoderModule; +import com.facebook.presto.kafka.decoder.json.JsonKafkaDecoderModule; +import com.facebook.presto.kafka.decoder.raw.RawKafkaDecoderModule; +import com.google.inject.Binder; +import com.google.inject.Module; +import com.google.inject.Scopes; +import com.google.inject.TypeLiteral; +import com.google.inject.multibindings.Multibinder; + +/** + * Kafka decoder specific module. Installs the registry and all known decoder submodules. + */ +public class KafkaDecoderModule + implements Module +{ + @Override + public void configure(Binder binder) + { + binder.bind(KafkaDecoderRegistry.class).in(Scopes.SINGLETON); + + binder.install(new DummyKafkaDecoderModule()); + binder.install(new CsvKafkaDecoderModule()); + binder.install(new JsonKafkaDecoderModule()); + binder.install(new RawKafkaDecoderModule()); + } + + public static void bindRowDecoder(Binder binder, Class decoderClass) + { + Multibinder rowDecoderBinder = Multibinder.newSetBinder(binder, KafkaRowDecoder.class); + rowDecoderBinder.addBinding().to(decoderClass).in(Scopes.SINGLETON); + } + + public static void bindFieldDecoder(Binder binder, Class> decoderClass) + { + Multibinder> fieldDecoderBinder = Multibinder.newSetBinder(binder, new TypeLiteral>() {}); + fieldDecoderBinder.addBinding().to(decoderClass).in(Scopes.SINGLETON); + } +} diff --git a/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/KafkaDecoderRegistry.java b/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/KafkaDecoderRegistry.java new file mode 100644 index 00000000..ed970874 --- /dev/null +++ b/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/KafkaDecoderRegistry.java @@ -0,0 +1,116 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka.decoder; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSetMultimap; +import com.google.common.collect.SetMultimap; +import io.airlift.log.Logger; + +import javax.annotation.Nullable; +import javax.inject.Inject; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import static com.facebook.presto.kafka.decoder.KafkaFieldDecoder.DEFAULT_FIELD_DECODER_NAME; +import static com.google.common.base.MoreObjects.firstNonNull; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static java.lang.String.format; + +/** + * Manages Row and Field decoders for the various dataFormat values. + */ +public class KafkaDecoderRegistry +{ + private static final Logger log = Logger.get(KafkaDecoderRegistry.class); + + private final Map rowDecoders; + private final Map, KafkaFieldDecoder>> fieldDecoders; + + @Inject + KafkaDecoderRegistry(Set rowDecoders, + Set> fieldDecoders) + { + checkNotNull(rowDecoders, "rowDecoders is null"); + + ImmutableMap.Builder rowBuilder = ImmutableMap.builder(); + for (KafkaRowDecoder rowDecoder : rowDecoders) { + rowBuilder.put(rowDecoder.getName(), rowDecoder); + } + + this.rowDecoders = rowBuilder.build(); + + Map, KafkaFieldDecoder>> fieldDecoderBuilders = new HashMap<>(); + + for (KafkaFieldDecoder fieldDecoder : fieldDecoders) { + ImmutableSetMultimap.Builder, KafkaFieldDecoder> fieldDecoderBuilder = fieldDecoderBuilders.get(fieldDecoder.getRowDecoderName()); + if (fieldDecoderBuilder == null) { + fieldDecoderBuilder = ImmutableSetMultimap.builder(); + fieldDecoderBuilders.put(fieldDecoder.getRowDecoderName(), fieldDecoderBuilder); + } + + for (Class clazz : fieldDecoder.getJavaTypes()) { + fieldDecoderBuilder.put(clazz, fieldDecoder); + } + } + + ImmutableMap.Builder, KafkaFieldDecoder>> fieldDecoderBuilder = ImmutableMap.builder(); + for (Map.Entry, KafkaFieldDecoder>> entry : fieldDecoderBuilders.entrySet()) { + fieldDecoderBuilder.put(entry.getKey(), entry.getValue().build()); + } + + this.fieldDecoders = fieldDecoderBuilder.build(); + log.debug("Field decoders found: %s", this.fieldDecoders); + } + + /** + * Return the specific row decoder for a given data format. + */ + public KafkaRowDecoder getRowDecoder(String dataFormat) + { + checkState(rowDecoders.containsKey(dataFormat), "no row decoder for '%s' found", dataFormat); + return rowDecoders.get(dataFormat); + } + + /** + * Return the best matching field decoder for a given row data format, field type and a possible field data format name. If no + * name was given or an unknown field data type was given, fall back to the default decoder. + */ + public KafkaFieldDecoder getFieldDecoder(String rowDataFormat, Class fieldType, @Nullable String fieldDataFormat) + { + checkNotNull(rowDataFormat, "rowDataFormat is null"); + checkNotNull(fieldType, "fieldType is null"); + + checkState(fieldDecoders.containsKey(rowDataFormat), "no field decoders for '%s' found", rowDataFormat); + Set> decoders = fieldDecoders.get(rowDataFormat).get(fieldType); + + ImmutableSet fieldNames = ImmutableSet.of( + firstNonNull(fieldDataFormat, DEFAULT_FIELD_DECODER_NAME), + DEFAULT_FIELD_DECODER_NAME); + + for (String fieldName : fieldNames) { + for (KafkaFieldDecoder decoder : decoders) { + if (fieldName.equals(decoder.getFieldDecoderName())) { + return decoder; + } + } + } + + throw new IllegalStateException(format("No field decoder for %s/%s found!", rowDataFormat, fieldType)); + } +} diff --git a/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/KafkaFieldDecoder.java b/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/KafkaFieldDecoder.java new file mode 100644 index 00000000..851a9183 --- /dev/null +++ b/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/KafkaFieldDecoder.java @@ -0,0 +1,54 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka.decoder; + +import com.facebook.presto.kafka.KafkaColumnHandle; +import com.facebook.presto.kafka.KafkaFieldValueProvider; + +import java.util.Set; + +/** + * Format specific field decoder description. + */ +public interface KafkaFieldDecoder +{ + /** + * Default name. Each decoder type *must* have a default decoder as fallback. + */ + String DEFAULT_FIELD_DECODER_NAME = "_default"; + + /** + * Returns the types which the field decoder can process. + */ + Set> getJavaTypes(); + + /** + * Returns the name of the row decoder to which this field decoder belongs. + */ + String getRowDecoderName(); + + /** + * Returns the field decoder specific name. This name will be selected with the {@link com.facebook.presto.kafka.KafkaTopicFieldDescription#dataFormat} value. + */ + String getFieldDecoderName(); + + /** + * Decode a value for the given column handle. + * + * @param value The raw value as generated by the row decoder. + * @param columnHandle The column for which the value is decoded. + * @return A {@link KafkaFieldValueProvider} instance which returns a captured value for this specific column. + */ + KafkaFieldValueProvider decode(T value, KafkaColumnHandle columnHandle); +} diff --git a/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/KafkaRowDecoder.java b/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/KafkaRowDecoder.java new file mode 100644 index 00000000..56a5d7ea --- /dev/null +++ b/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/KafkaRowDecoder.java @@ -0,0 +1,47 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka.decoder; + +import com.facebook.presto.kafka.KafkaColumnHandle; +import com.facebook.presto.kafka.KafkaFieldValueProvider; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Implementations decode a Kafka message from bytes and add field value providers for all decodable columns. + */ +public interface KafkaRowDecoder +{ + /** + * Returns the row decoder specific name. + */ + String getName(); + + /** + * Decodes a given set of bytes into field values. + * + * @param data The row data (Kafka message) to decode. + * @param fieldValueProviders Must be a mutable set. Any field value provider created by this row decoder is put into this set. + * @param columnHandles List of column handles for which field values are required. + * @param fieldDecoders Map from column handles to decoders. This map should be used to look up the field decoder that generates the field value provider for a given column handle. + * @return false if the row was decoded successfully, true if it could not be decoded (was corrupt). TODO - reverse this boolean. + */ + boolean decodeRow( + byte[] data, + Set fieldValueProviders, + List columnHandles, + Map> fieldDecoders); +} diff --git a/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/csv/CsvKafkaDecoderModule.java b/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/csv/CsvKafkaDecoderModule.java new file mode 100644 index 00000000..99e58458 --- /dev/null +++ b/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/csv/CsvKafkaDecoderModule.java @@ -0,0 +1,35 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka.decoder.csv; + +import com.google.inject.Binder; +import com.google.inject.Module; + +import static com.facebook.presto.kafka.decoder.KafkaDecoderModule.bindFieldDecoder; +import static com.facebook.presto.kafka.decoder.KafkaDecoderModule.bindRowDecoder; + +/** + * Guice module for the CSV decoder. + */ +public class CsvKafkaDecoderModule + implements Module +{ + @Override + public void configure(Binder binder) + { + bindRowDecoder(binder, CsvKafkaRowDecoder.class); + + bindFieldDecoder(binder, CsvKafkaFieldDecoder.class); + } +} diff --git a/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/csv/CsvKafkaFieldDecoder.java b/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/csv/CsvKafkaFieldDecoder.java new file mode 100644 index 00000000..6e24672d --- /dev/null +++ b/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/csv/CsvKafkaFieldDecoder.java @@ -0,0 +1,105 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka.decoder.csv; + +import com.facebook.presto.kafka.KafkaColumnHandle; +import com.facebook.presto.kafka.KafkaFieldValueProvider; +import com.facebook.presto.kafka.decoder.KafkaFieldDecoder; +import com.google.common.collect.ImmutableSet; +import io.airlift.slice.Slice; + +import java.util.Set; + +import static com.google.common.base.Preconditions.checkNotNull; +import static io.airlift.slice.Slices.EMPTY_SLICE; +import static io.airlift.slice.Slices.utf8Slice; +import static java.lang.String.format; + +/** + * Default field decoder for the CSV format. Very simple string based conversion of field values. May + * not work for every CSV topic. + */ +public class CsvKafkaFieldDecoder + implements KafkaFieldDecoder +{ + @Override + public Set> getJavaTypes() + { + return ImmutableSet.>of(boolean.class, long.class, double.class, Slice.class); + } + + @Override + public String getRowDecoderName() + { + return CsvKafkaRowDecoder.NAME; + } + + @Override + public String getFieldDecoderName() + { + return KafkaFieldDecoder.DEFAULT_FIELD_DECODER_NAME; + } + + @Override + public KafkaFieldValueProvider decode(final String value, final KafkaColumnHandle columnHandle) + { + checkNotNull(columnHandle, "columnHandle is null"); + + return new KafkaFieldValueProvider() + { + @Override + public boolean accept(KafkaColumnHandle handle) + { + return columnHandle.equals(handle); + } + + @Override + public boolean isNull() + { + return (value == null) || value.isEmpty(); + } + + @SuppressWarnings("SimplifiableConditionalExpression") + @Override + public boolean getBoolean() + { + return isNull() ? false : Boolean.parseBoolean(value.trim()); + } + + @Override + public long getLong() + { + return isNull() ? 0L : Long.parseLong(value.trim()); + } + + @Override + public double getDouble() + { + return isNull() ? 0.0d : Double.parseDouble(value.trim()); + } + + @Override + public Slice getSlice() + { + return isNull() ? EMPTY_SLICE : utf8Slice(value); + } + }; + } + + @Override + public String toString() + { + return format("FieldDecoder[%s/%s]", getRowDecoderName(), getFieldDecoderName()); + } +} diff --git a/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/csv/CsvKafkaRowDecoder.java b/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/csv/CsvKafkaRowDecoder.java new file mode 100644 index 00000000..e9c43d20 --- /dev/null +++ b/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/csv/CsvKafkaRowDecoder.java @@ -0,0 +1,88 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka.decoder.csv; + +import au.com.bytecode.opencsv.CSVParser; +import com.facebook.presto.kafka.KafkaColumnHandle; +import com.facebook.presto.kafka.KafkaFieldValueProvider; +import com.facebook.presto.kafka.decoder.KafkaFieldDecoder; +import com.facebook.presto.kafka.decoder.KafkaRowDecoder; + +import javax.inject.Inject; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkState; + +/** + * Decode a Kafka message as CSV. This is an extremely primitive CSV decoder using {@link au.com.bytecode.opencsv.CSVParser]}. + */ +public class CsvKafkaRowDecoder + implements KafkaRowDecoder +{ + public static final String NAME = "csv"; + + private final CSVParser parser = new CSVParser(); + + @Inject + CsvKafkaRowDecoder() + { + } + + @Override + public String getName() + { + return NAME; + } + + @Override + public boolean decodeRow(byte[] data, Set fieldValueProviders, List columnHandles, Map> fieldDecoders) + { + String[] fields; + try { + // TODO - There is no reason why the row can't have a formatHint and it could be used + // to set the charset here. + String line = new String(data, StandardCharsets.UTF_8); + fields = parser.parseLine(line); + } + catch (Exception e) { + return true; + } + + for (KafkaColumnHandle columnHandle : columnHandles) { + if (columnHandle.isInternal()) { + continue; + } + + String mapping = columnHandle.getMapping(); + checkState(mapping != null, "No mapping for column handle %s!", columnHandle); + int columnIndex = Integer.parseInt(mapping); + + if (columnIndex >= fields.length) { + continue; + } + + @SuppressWarnings("unchecked") + KafkaFieldDecoder decoder = (KafkaFieldDecoder) fieldDecoders.get(columnHandle); + + if (decoder != null) { + fieldValueProviders.add(decoder.decode(fields[columnIndex], columnHandle)); + } + } + return false; + } +} diff --git a/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/dummy/DummyKafkaDecoderModule.java b/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/dummy/DummyKafkaDecoderModule.java new file mode 100644 index 00000000..5fcd9b24 --- /dev/null +++ b/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/dummy/DummyKafkaDecoderModule.java @@ -0,0 +1,35 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka.decoder.dummy; + +import com.google.inject.Binder; +import com.google.inject.Module; + +import static com.facebook.presto.kafka.decoder.KafkaDecoderModule.bindFieldDecoder; +import static com.facebook.presto.kafka.decoder.KafkaDecoderModule.bindRowDecoder; + +/** + * Guice module for the 'dummy' decoder. See {@link com.facebook.presto.kafka.decoder.dummy.DummyKafkaRowDecoder} for an explanation. + */ +public class DummyKafkaDecoderModule + implements Module +{ + @Override + public void configure(Binder binder) + { + bindRowDecoder(binder, DummyKafkaRowDecoder.class); + + bindFieldDecoder(binder, DummyKafkaFieldDecoder.class); + } +} diff --git a/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/dummy/DummyKafkaFieldDecoder.java b/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/dummy/DummyKafkaFieldDecoder.java new file mode 100644 index 00000000..b5738c44 --- /dev/null +++ b/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/dummy/DummyKafkaFieldDecoder.java @@ -0,0 +1,79 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka.decoder.dummy; + +import com.facebook.presto.kafka.KafkaColumnHandle; +import com.facebook.presto.kafka.KafkaFieldValueProvider; +import com.facebook.presto.kafka.decoder.KafkaFieldDecoder; +import com.facebook.presto.spi.PrestoException; +import com.google.common.collect.ImmutableSet; +import io.airlift.slice.Slice; + +import java.util.Set; + +import static com.facebook.presto.kafka.KafkaErrorCode.KAFKA_CONVERSION_NOT_SUPPORTED; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.lang.String.format; + +/** + * Default 'decoder' for the dummy format. Can not decode anything. This is intentional. + */ +public class DummyKafkaFieldDecoder + implements KafkaFieldDecoder +{ + @Override + public Set> getJavaTypes() + { + return ImmutableSet.>of(boolean.class, long.class, double.class, Slice.class); + } + + @Override + public final String getRowDecoderName() + { + return DummyKafkaRowDecoder.NAME; + } + + @Override + public String getFieldDecoderName() + { + return KafkaFieldDecoder.DEFAULT_FIELD_DECODER_NAME; + } + + @Override + public KafkaFieldValueProvider decode(Void value, KafkaColumnHandle columnHandle) + { + checkNotNull(columnHandle, "columnHandle is null"); + + return new KafkaFieldValueProvider() + { + @Override + public boolean accept(KafkaColumnHandle handle) + { + return false; + } + + @Override + public boolean isNull() + { + throw new PrestoException(KAFKA_CONVERSION_NOT_SUPPORTED, "is null check not supported"); + } + }; + } + + @Override + public String toString() + { + return format("FieldDecoder[%s/%s]", getRowDecoderName(), getFieldDecoderName()); + } +} diff --git a/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/dummy/DummyKafkaRowDecoder.java b/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/dummy/DummyKafkaRowDecoder.java new file mode 100644 index 00000000..e317367b --- /dev/null +++ b/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/dummy/DummyKafkaRowDecoder.java @@ -0,0 +1,48 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka.decoder.dummy; + +import com.facebook.presto.kafka.KafkaColumnHandle; +import com.facebook.presto.kafka.KafkaFieldValueProvider; +import com.facebook.presto.kafka.decoder.KafkaFieldDecoder; +import com.facebook.presto.kafka.decoder.KafkaRowDecoder; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * The row decoder for the 'dummy' format. As Kafka is an unstructured message format (bag of bytes), a specific decoder for a topic must exist. To start developing such a decoder, + * it is beneficial to be able to configure an arbitrary topic to be available through presto without any decoding at all (not even line parsing) and examine the internal rows + * that are exposed by Presto (see {@link com.facebook.presto.kafka.KafkaInternalFieldDescription} for a list of internal columns that are available to each configured topic). + * By adding a topic name to the catalog configuration file and not having and specific topic description JSON file (or by omitting the 'dataFormat' field in the topic description file), + * this decoder is selected, which intentionally does not do *anything* with the messages read from Kafka. + */ +public class DummyKafkaRowDecoder + implements KafkaRowDecoder +{ + public static final String NAME = "dummy"; + + @Override + public String getName() + { + return NAME; + } + + @Override + public boolean decodeRow(byte[] data, Set fieldValueProviders, List columnHandles, Map> fieldDecoders) + { + return false; + } +} diff --git a/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/json/CustomDateTimeJsonKafkaFieldDecoder.java b/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/json/CustomDateTimeJsonKafkaFieldDecoder.java new file mode 100644 index 00000000..4ca82cc0 --- /dev/null +++ b/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/json/CustomDateTimeJsonKafkaFieldDecoder.java @@ -0,0 +1,86 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka.decoder.json; + +import com.facebook.presto.kafka.KafkaColumnHandle; +import com.facebook.presto.kafka.KafkaFieldValueProvider; +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.collect.ImmutableSet; +import io.airlift.slice.Slice; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; + +import java.util.Locale; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Custom date format decoder. + *

+ * formatHint uses {@link org.joda.time.format.DateTimeFormatter} format. + *

+ * Uses hardcoded UTC timezone and english locale. + */ +public class CustomDateTimeJsonKafkaFieldDecoder + extends JsonKafkaFieldDecoder +{ + @Override + public Set> getJavaTypes() + { + return ImmutableSet.>of(long.class, Slice.class); + } + + @Override + public String getFieldDecoderName() + { + return "custom-date-time"; + } + + @Override + public KafkaFieldValueProvider decode(JsonNode value, KafkaColumnHandle columnHandle) + { + checkNotNull(columnHandle, "columnHandle is null"); + checkNotNull(value, "value is null"); + + return new CustomDateTimeJsonKafkaValueProvider(value, columnHandle); + } + + public static class CustomDateTimeJsonKafkaValueProvider + extends DateTimeJsonKafkaValueProvider + { + public CustomDateTimeJsonKafkaValueProvider(JsonNode value, KafkaColumnHandle columnHandle) + { + super(value, columnHandle); + } + + @Override + protected long getMillis() + { + if (isNull()) { + return 0L; + } + + if (value.canConvertToLong()) { + return value.asLong(); + } + + checkNotNull(columnHandle.getFormatHint(), "formatHint is null"); + String textValue = value.isValueNode() ? value.asText() : value.toString(); + + DateTimeFormatter formatter = DateTimeFormat.forPattern(columnHandle.getFormatHint()).withLocale(Locale.ENGLISH).withZoneUTC(); + return formatter.parseMillis(textValue); + } + } +} diff --git a/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/json/ISO8601JsonKafkaFieldDecoder.java b/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/json/ISO8601JsonKafkaFieldDecoder.java new file mode 100644 index 00000000..5c757548 --- /dev/null +++ b/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/json/ISO8601JsonKafkaFieldDecoder.java @@ -0,0 +1,90 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka.decoder.json; + +import com.facebook.presto.kafka.KafkaColumnHandle; +import com.facebook.presto.kafka.KafkaFieldValueProvider; +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableSet; +import io.airlift.slice.Slice; +import org.joda.time.format.DateTimeFormatter; +import org.joda.time.format.ISODateTimeFormat; + +import java.util.Locale; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * ISO 8601 date format decoder. + *

+ * Uses hardcoded UTC timezone and english locale. + */ +public class ISO8601JsonKafkaFieldDecoder + extends JsonKafkaFieldDecoder +{ + @VisibleForTesting + static final String NAME = "iso8601"; + + /** + * TODO: configurable time zones and locales + */ + private static final DateTimeFormatter FORMATTER = ISODateTimeFormat.dateTimeParser().withLocale(Locale.ENGLISH).withZoneUTC(); + + @Override + public Set> getJavaTypes() + { + return ImmutableSet.>of(long.class, Slice.class); + } + + @Override + public String getFieldDecoderName() + { + return NAME; + } + + @Override + public KafkaFieldValueProvider decode(JsonNode value, KafkaColumnHandle columnHandle) + { + checkNotNull(columnHandle, "columnHandle is null"); + checkNotNull(value, "value is null"); + + return new ISO8601JsonKafkaValueProvider(value, columnHandle); + } + + public static class ISO8601JsonKafkaValueProvider + extends DateTimeJsonKafkaValueProvider + { + public ISO8601JsonKafkaValueProvider(JsonNode value, KafkaColumnHandle columnHandle) + { + super(value, columnHandle); + } + + @Override + protected long getMillis() + { + if (isNull()) { + return 0L; + } + + if (value.canConvertToLong()) { + return value.asLong(); + } + + String textValue = value.isValueNode() ? value.asText() : value.toString(); + return FORMATTER.parseMillis(textValue); + } + } +} diff --git a/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/json/JsonKafkaDecoderModule.java b/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/json/JsonKafkaDecoderModule.java new file mode 100644 index 00000000..f2112a47 --- /dev/null +++ b/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/json/JsonKafkaDecoderModule.java @@ -0,0 +1,53 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka.decoder.json; + +import com.google.inject.Binder; +import com.google.inject.Module; + +import static com.facebook.presto.kafka.decoder.KafkaDecoderModule.bindFieldDecoder; +import static com.facebook.presto.kafka.decoder.KafkaDecoderModule.bindRowDecoder; + +/** + * Guice module for the Json decoder module. This is the most mature (best tested) topic decoder. + *

+ * Besides the default field decoder for all the values, it also supports a number of decoders for + * timestamp specific information. These decoders can be selected with the dataFormat field. + *

+ *

    + *
  • iso8601 - decode the value of a json string field as an ISO8601 timestamp; returns a long value which can be mapped to a presto TIMESTAMP.
  • + *
  • rfc2822 - decode the value of a json string field as an RFC 2822 compliant timestamp; returns a long value which can be mapped to a presto TIMESTAMP + * (the twitter sample feed contains timestamps in this format).
  • + *
  • milliseconds-since-epoch - Interpret the value of a json string or number field as a long containing milliseconds since the beginning of the epoch.
  • + *
  • seconds-since-epoch - Interpret the value of a json string or number field as a long containing seconds since the beginning of the epoch.
  • + *
  • custom-date-time - Interpret the value of a json string field according to the {@link org.joda.time.format.DateTimeFormatter} formatting rules + * given using the formatHint field.
  • + *
+ */ +public class JsonKafkaDecoderModule + implements Module +{ + @Override + public void configure(Binder binder) + { + bindRowDecoder(binder, JsonKafkaRowDecoder.class); + + bindFieldDecoder(binder, JsonKafkaFieldDecoder.class); + bindFieldDecoder(binder, ISO8601JsonKafkaFieldDecoder.class); + bindFieldDecoder(binder, RFC2822JsonKafkaFieldDecoder.class); + bindFieldDecoder(binder, SecondsSinceEpochJsonKafkaFieldDecoder.class); + bindFieldDecoder(binder, MillisecondsSinceEpochJsonKafkaFieldDecoder.class); + bindFieldDecoder(binder, CustomDateTimeJsonKafkaFieldDecoder.class); + } +} diff --git a/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/json/JsonKafkaFieldDecoder.java b/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/json/JsonKafkaFieldDecoder.java new file mode 100644 index 00000000..e143fe42 --- /dev/null +++ b/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/json/JsonKafkaFieldDecoder.java @@ -0,0 +1,173 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka.decoder.json; + +import com.facebook.presto.kafka.KafkaColumnHandle; +import com.facebook.presto.kafka.KafkaFieldValueProvider; +import com.facebook.presto.kafka.decoder.KafkaFieldDecoder; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.type.Type; +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.collect.ImmutableSet; +import io.airlift.slice.Slice; + +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import static com.facebook.presto.kafka.KafkaErrorCode.KAFKA_CONVERSION_NOT_SUPPORTED; +import static com.facebook.presto.spi.type.DateTimeEncoding.packDateTimeWithZone; +import static com.facebook.presto.spi.type.DateType.DATE; +import static com.facebook.presto.spi.type.TimeType.TIME; +import static com.facebook.presto.spi.type.TimeWithTimeZoneType.TIME_WITH_TIME_ZONE; +import static com.facebook.presto.spi.type.TimestampType.TIMESTAMP; +import static com.facebook.presto.spi.type.TimestampWithTimeZoneType.TIMESTAMP_WITH_TIME_ZONE; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.airlift.slice.Slices.EMPTY_SLICE; +import static io.airlift.slice.Slices.utf8Slice; +import static java.lang.String.format; + +/** + * Default field decoder for the JSON format. Supports json format coercions to implicitly convert e.g. string to long values. + */ +public class JsonKafkaFieldDecoder + implements KafkaFieldDecoder +{ + @Override + public Set> getJavaTypes() + { + return ImmutableSet.>of(boolean.class, long.class, double.class, Slice.class); + } + + @Override + public final String getRowDecoderName() + { + return JsonKafkaRowDecoder.NAME; + } + + @Override + public String getFieldDecoderName() + { + return KafkaFieldDecoder.DEFAULT_FIELD_DECODER_NAME; + } + + @Override + public KafkaFieldValueProvider decode(JsonNode value, KafkaColumnHandle columnHandle) + { + checkNotNull(columnHandle, "columnHandle is null"); + checkNotNull(value, "value is null"); + + return new JsonKafkaValueProvider(value, columnHandle); + } + + @Override + public String toString() + { + return format("FieldDecoder[%s/%s]", getRowDecoderName(), getFieldDecoderName()); + } + + public static class JsonKafkaValueProvider + extends KafkaFieldValueProvider + { + protected final JsonNode value; + protected final KafkaColumnHandle columnHandle; + + public JsonKafkaValueProvider(JsonNode value, KafkaColumnHandle columnHandle) + { + this.value = value; + this.columnHandle = columnHandle; + } + + @Override + public final boolean accept(KafkaColumnHandle columnHandle) + { + return this.columnHandle.equals(columnHandle); + } + + @Override + public final boolean isNull() + { + return value.isMissingNode() || value.isNull(); + } + + @Override + public boolean getBoolean() + { + return value.asBoolean(); + } + + @Override + public long getLong() + { + return value.asLong(); + } + + @Override + public double getDouble() + { + return value.asDouble(); + } + + @Override + public Slice getSlice() + { + String textValue = value.isValueNode() ? value.asText() : value.toString(); + return isNull() ? EMPTY_SLICE : utf8Slice(textValue); + } + } + + public abstract static class DateTimeJsonKafkaValueProvider + extends JsonKafkaValueProvider + { + protected DateTimeJsonKafkaValueProvider(JsonNode value, KafkaColumnHandle columnHandle) + { + super(value, columnHandle); + } + + @Override + public boolean getBoolean() + { + throw new PrestoException(KAFKA_CONVERSION_NOT_SUPPORTED, "conversion to boolean not supported"); + } + + @Override + public double getDouble() + { + throw new PrestoException(KAFKA_CONVERSION_NOT_SUPPORTED, "conversion to double not supported"); + } + + @Override + public final long getLong() + { + long millis = getMillis(); + + Type type = columnHandle.getType(); + if (type.equals(DATE)) { + return TimeUnit.MILLISECONDS.toDays(millis); + } + if (type.equals(TIMESTAMP) || type.equals(TIME)) { + return millis; + } + if (type.equals(TIMESTAMP_WITH_TIME_ZONE) || type.equals(TIME_WITH_TIME_ZONE)) { + return packDateTimeWithZone(millis, 0); + } + + return millis; + } + + /** + * @return epoch milliseconds in UTC + */ + protected abstract long getMillis(); + } +} diff --git a/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/json/JsonKafkaRowDecoder.java b/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/json/JsonKafkaRowDecoder.java new file mode 100644 index 00000000..4d4c9b4b --- /dev/null +++ b/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/json/JsonKafkaRowDecoder.java @@ -0,0 +1,97 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka.decoder.json; + +import com.facebook.presto.kafka.KafkaColumnHandle; +import com.facebook.presto.kafka.KafkaFieldValueProvider; +import com.facebook.presto.kafka.decoder.KafkaFieldDecoder; +import com.facebook.presto.kafka.decoder.KafkaRowDecoder; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.MissingNode; +import com.google.common.base.Splitter; + +import javax.inject.Inject; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkState; + +/** + * JSON specific Kafka row decoder. + */ +public class JsonKafkaRowDecoder + implements KafkaRowDecoder +{ + public static final String NAME = "json"; + + private final ObjectMapper objectMapper; + + @Inject + JsonKafkaRowDecoder(ObjectMapper objectMapper) + { + this.objectMapper = objectMapper; + } + + @Override + public String getName() + { + return NAME; + } + + @Override + public boolean decodeRow(byte[] data, Set fieldValueProviders, List columnHandles, Map> fieldDecoders) + { + JsonNode tree; + + try { + tree = objectMapper.readTree(data); + } + catch (Exception e) { + return true; + } + + for (KafkaColumnHandle columnHandle : columnHandles) { + if (columnHandle.isInternal()) { + continue; + } + @SuppressWarnings("unchecked") + KafkaFieldDecoder decoder = (KafkaFieldDecoder) fieldDecoders.get(columnHandle); + + if (decoder != null) { + JsonNode node = locateNode(tree, columnHandle); + fieldValueProviders.add(decoder.decode(node, columnHandle)); + } + } + + return false; + } + + private static JsonNode locateNode(JsonNode tree, KafkaColumnHandle columnHandle) + { + String mapping = columnHandle.getMapping(); + checkState(mapping != null, "No mapping for %s", columnHandle.getName()); + + JsonNode currentNode = tree; + for (String pathElement : Splitter.on('/').omitEmptyStrings().split(mapping)) { + if (!currentNode.has(pathElement)) { + return MissingNode.getInstance(); + } + currentNode = currentNode.path(pathElement); + } + return currentNode; + } +} diff --git a/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/json/MillisecondsSinceEpochJsonKafkaFieldDecoder.java b/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/json/MillisecondsSinceEpochJsonKafkaFieldDecoder.java new file mode 100644 index 00000000..220485f0 --- /dev/null +++ b/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/json/MillisecondsSinceEpochJsonKafkaFieldDecoder.java @@ -0,0 +1,90 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka.decoder.json; + +import com.facebook.presto.kafka.KafkaColumnHandle; +import com.facebook.presto.kafka.KafkaFieldValueProvider; +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableSet; +import io.airlift.slice.Slice; +import org.joda.time.format.DateTimeFormatter; +import org.joda.time.format.ISODateTimeFormat; + +import java.util.Locale; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkNotNull; +import static io.airlift.slice.Slices.EMPTY_SLICE; +import static io.airlift.slice.Slices.utf8Slice; + +/** + * Milliseconds since the epoch date format decoder. + *

+ * Uses hardcoded UTC timezone and english locale. + */ +public class MillisecondsSinceEpochJsonKafkaFieldDecoder + extends JsonKafkaFieldDecoder +{ + @VisibleForTesting + static final String NAME = "milliseconds-since-epoch"; + + /** + * Todo - configurable time zones and locales. + */ + @VisibleForTesting + static final DateTimeFormatter FORMATTER = ISODateTimeFormat.dateTime().withLocale(Locale.ENGLISH).withZoneUTC(); + + @Override + public Set> getJavaTypes() + { + return ImmutableSet.>of(long.class, Slice.class); + } + + @Override + public String getFieldDecoderName() + { + return NAME; + } + + @Override + public KafkaFieldValueProvider decode(JsonNode value, KafkaColumnHandle columnHandle) + { + checkNotNull(columnHandle, "columnHandle is null"); + checkNotNull(value, "value is null"); + + return new MillisecondsSinceEpochJsonKafkaValueProvider(value, columnHandle); + } + + public static class MillisecondsSinceEpochJsonKafkaValueProvider + extends DateTimeJsonKafkaValueProvider + { + public MillisecondsSinceEpochJsonKafkaValueProvider(JsonNode value, KafkaColumnHandle columnHandle) + { + super(value, columnHandle); + } + + @Override + protected long getMillis() + { + return isNull() ? 0L : value.asLong(); + } + + @Override + public Slice getSlice() + { + return isNull() ? EMPTY_SLICE : utf8Slice(FORMATTER.print(value.asLong())); + } + } +} diff --git a/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/json/RFC2822JsonKafkaFieldDecoder.java b/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/json/RFC2822JsonKafkaFieldDecoder.java new file mode 100644 index 00000000..21144a5e --- /dev/null +++ b/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/json/RFC2822JsonKafkaFieldDecoder.java @@ -0,0 +1,91 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka.decoder.json; + +import com.facebook.presto.kafka.KafkaColumnHandle; +import com.facebook.presto.kafka.KafkaFieldValueProvider; +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableSet; +import io.airlift.slice.Slice; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; + +import java.util.Locale; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * RFC 2822 date format decoder. + *

+ * Uses hardcoded UTC timezone and english locale. + */ +public class RFC2822JsonKafkaFieldDecoder + extends JsonKafkaFieldDecoder +{ + @VisibleForTesting + static final String NAME = "rfc2822"; + + /** + * Todo - configurable time zones and locales. + */ + @VisibleForTesting + static final DateTimeFormatter FORMATTER = DateTimeFormat.forPattern("EEE MMM dd HH:mm:ss Z yyyy").withLocale(Locale.ENGLISH).withZoneUTC(); + + @Override + public Set> getJavaTypes() + { + return ImmutableSet.>of(long.class, Slice.class); + } + + @Override + public String getFieldDecoderName() + { + return NAME; + } + + @Override + public KafkaFieldValueProvider decode(JsonNode value, KafkaColumnHandle columnHandle) + { + checkNotNull(columnHandle, "columnHandle is null"); + checkNotNull(value, "value is null"); + + return new RFC2822JsonKafkaValueProvider(value, columnHandle); + } + + public static class RFC2822JsonKafkaValueProvider + extends DateTimeJsonKafkaValueProvider + { + public RFC2822JsonKafkaValueProvider(JsonNode value, KafkaColumnHandle columnHandle) + { + super(value, columnHandle); + } + + @Override + protected long getMillis() + { + if (isNull()) { + return 0L; + } + + if (value.canConvertToLong()) { + return value.asLong(); + } + + String textValue = value.isValueNode() ? value.asText() : value.toString(); + return FORMATTER.parseMillis(textValue); + } + } +} diff --git a/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/json/SecondsSinceEpochJsonKafkaFieldDecoder.java b/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/json/SecondsSinceEpochJsonKafkaFieldDecoder.java new file mode 100644 index 00000000..d45705d3 --- /dev/null +++ b/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/json/SecondsSinceEpochJsonKafkaFieldDecoder.java @@ -0,0 +1,90 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka.decoder.json; + +import com.facebook.presto.kafka.KafkaColumnHandle; +import com.facebook.presto.kafka.KafkaFieldValueProvider; +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableSet; +import io.airlift.slice.Slice; +import io.airlift.slice.Slices; +import org.joda.time.format.DateTimeFormatter; +import org.joda.time.format.ISODateTimeFormat; + +import java.util.Locale; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkNotNull; +import static io.airlift.slice.Slices.utf8Slice; + +/** + * Seconds since the epoch date format decoder. + *

+ * Uses hardcoded UTC timezone and english locale. + */ +public class SecondsSinceEpochJsonKafkaFieldDecoder + extends JsonKafkaFieldDecoder +{ + @VisibleForTesting + static final String NAME = "seconds-since-epoch"; + + /** + * Todo - configurable time zones and locales. + */ + @VisibleForTesting + static final DateTimeFormatter FORMATTER = ISODateTimeFormat.dateTimeNoMillis().withLocale(Locale.ENGLISH).withZoneUTC(); + + @Override + public Set> getJavaTypes() + { + return ImmutableSet.>of(long.class, Slice.class); + } + + @Override + public String getFieldDecoderName() + { + return NAME; + } + + @Override + public KafkaFieldValueProvider decode(JsonNode value, KafkaColumnHandle columnHandle) + { + checkNotNull(columnHandle, "columnHandle is null"); + checkNotNull(value, "value is null"); + + return new SecondsSinceEpochJsonKafkaValueProvider(value, columnHandle); + } + + public static class SecondsSinceEpochJsonKafkaValueProvider + extends DateTimeJsonKafkaValueProvider + { + public SecondsSinceEpochJsonKafkaValueProvider(JsonNode value, KafkaColumnHandle columnHandle) + { + super(value, columnHandle); + } + + @Override + protected long getMillis() + { + return isNull() ? 0L : value.asLong() * 1000L; + } + + @Override + public Slice getSlice() + { + return isNull() ? Slices.EMPTY_SLICE : utf8Slice(FORMATTER.print(value.asLong() * 1000L)); + } + } +} diff --git a/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/raw/RawKafkaDecoderModule.java b/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/raw/RawKafkaDecoderModule.java new file mode 100644 index 00000000..1e67e346 --- /dev/null +++ b/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/raw/RawKafkaDecoderModule.java @@ -0,0 +1,34 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka.decoder.raw; + +import com.google.inject.Binder; +import com.google.inject.Module; + +import static com.facebook.presto.kafka.decoder.KafkaDecoderModule.bindFieldDecoder; +import static com.facebook.presto.kafka.decoder.KafkaDecoderModule.bindRowDecoder; + +/** + * Raw decoder guice module. + */ +public class RawKafkaDecoderModule + implements Module +{ + @Override + public void configure(Binder binder) + { + bindRowDecoder(binder, RawKafkaRowDecoder.class); + bindFieldDecoder(binder, RawKafkaFieldDecoder.class); + } +} diff --git a/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/raw/RawKafkaFieldDecoder.java b/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/raw/RawKafkaFieldDecoder.java new file mode 100644 index 00000000..942615af --- /dev/null +++ b/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/raw/RawKafkaFieldDecoder.java @@ -0,0 +1,229 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka.decoder.raw; + +import com.facebook.presto.kafka.KafkaColumnHandle; +import com.facebook.presto.kafka.KafkaFieldValueProvider; +import com.facebook.presto.kafka.decoder.KafkaFieldDecoder; +import com.facebook.presto.spi.PrestoException; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import io.airlift.slice.Slice; +import io.airlift.slice.Slices; + +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +import static com.facebook.presto.kafka.KafkaErrorCode.KAFKA_CONVERSION_NOT_SUPPORTED; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static java.lang.String.format; + +/** + * Default field decoder for raw (byte) columns. + */ +public class RawKafkaFieldDecoder + implements KafkaFieldDecoder +{ + public enum FieldType + { + BYTE(Byte.SIZE), + SHORT(Short.SIZE), + INT(Integer.SIZE), + LONG(Long.SIZE), + FLOAT(Float.SIZE), + DOUBLE(Double.SIZE); + + private final int size; + + FieldType(int bitSize) + { + this.size = bitSize / 8; + } + + public int getSize() + { + return size; + } + + static FieldType forString(String value) + { + if (value != null) { + for (FieldType fieldType : values()) { + if (value.toUpperCase(Locale.ENGLISH).equals(fieldType.name())) { + return fieldType; + } + } + } + + return null; + } + } + + @Override + public Set> getJavaTypes() + { + return ImmutableSet.>of(boolean.class, long.class, double.class, Slice.class); + } + + @Override + public final String getRowDecoderName() + { + return RawKafkaRowDecoder.NAME; + } + + @Override + public String getFieldDecoderName() + { + return KafkaFieldDecoder.DEFAULT_FIELD_DECODER_NAME; + } + + @Override + public KafkaFieldValueProvider decode(byte[] value, KafkaColumnHandle columnHandle) + { + checkNotNull(columnHandle, "columnHandle is null"); + checkNotNull(value, "value is null"); + + String mapping = columnHandle.getMapping(); + FieldType fieldType = columnHandle.getDataFormat() == null ? FieldType.BYTE : FieldType.forString(columnHandle.getDataFormat()); + + int start = 0; + int end = value.length; + + if (mapping != null) { + List fields = ImmutableList.copyOf(Splitter.on(':').limit(2).split(mapping)); + if (!fields.isEmpty()) { + start = Integer.parseInt(fields.get(0)); + checkState(start >= 0 && start < value.length, "Found start %s, but only 0..%s is legal", start, value.length); + if (fields.size() > 1) { + end = Integer.parseInt(fields.get(1)); + checkState(end > 0 && end <= value.length, "Found end %s, but only 1..%s is legal", end, value.length); + } + } + } + + checkState(start <= end, "Found start %s and end %s. start must be smaller than end", start, end); + + return new RawKafkaValueProvider(ByteBuffer.wrap(value, start, end - start), columnHandle, fieldType); + } + + @Override + public String toString() + { + return format("FieldDecoder[%s/%s]", getRowDecoderName(), getFieldDecoderName()); + } + + public static class RawKafkaValueProvider + extends KafkaFieldValueProvider + { + protected final ByteBuffer value; + protected final KafkaColumnHandle columnHandle; + protected final FieldType fieldType; + protected final int size; + + public RawKafkaValueProvider(ByteBuffer value, KafkaColumnHandle columnHandle, FieldType fieldType) + { + this.columnHandle = checkNotNull(columnHandle, "columnHandle is null"); + this.fieldType = checkNotNull(fieldType, "fieldType is null"); + this.size = value.limit() - value.position(); + checkState(size >= fieldType.getSize(), "minimum byte size is %s, found %s,", fieldType.getSize(), size); + this.value = value; + } + + @Override + public final boolean accept(KafkaColumnHandle columnHandle) + { + return this.columnHandle.equals(columnHandle); + } + + @Override + public final boolean isNull() + { + return size == 0; + } + + @Override + public boolean getBoolean() + { + if (isNull()) { + return false; + } + switch (fieldType) { + case BYTE: + return value.get() != 0; + case SHORT: + return value.getShort() != 0; + case INT: + return value.getInt() != 0; + case LONG: + return value.getLong() != 0; + default: + throw new PrestoException(KAFKA_CONVERSION_NOT_SUPPORTED, format("conversion %s to boolean not supported", fieldType)); + } + } + + @Override + public long getLong() + { + if (isNull()) { + return 0L; + } + switch (fieldType) { + case BYTE: + return value.get(); + case SHORT: + return value.getShort(); + case INT: + return value.getInt(); + case LONG: + return value.getLong(); + default: + throw new PrestoException(KAFKA_CONVERSION_NOT_SUPPORTED, format("conversion %s to long not supported", fieldType)); + } + } + + @Override + public double getDouble() + { + if (isNull()) { + return 0.0d; + } + switch (fieldType) { + case FLOAT: + return value.getFloat(); + case DOUBLE: + return value.getDouble(); + default: + throw new PrestoException(KAFKA_CONVERSION_NOT_SUPPORTED, format("conversion %s to double not supported", fieldType)); + } + } + + @Override + public Slice getSlice() + { + if (isNull()) { + return Slices.EMPTY_SLICE; + } + + if (fieldType == FieldType.BYTE) { + return Slices.wrappedBuffer(value.slice()); + } + + throw new PrestoException(KAFKA_CONVERSION_NOT_SUPPORTED, format("conversion %s to Slice not supported", fieldType)); + } + } +} diff --git a/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/raw/RawKafkaRowDecoder.java b/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/raw/RawKafkaRowDecoder.java new file mode 100644 index 00000000..796218f8 --- /dev/null +++ b/presto-kafka/src/main/java/com/facebook/presto/kafka/decoder/raw/RawKafkaRowDecoder.java @@ -0,0 +1,57 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka.decoder.raw; + +import com.facebook.presto.kafka.KafkaColumnHandle; +import com.facebook.presto.kafka.KafkaFieldValueProvider; +import com.facebook.presto.kafka.decoder.KafkaFieldDecoder; +import com.facebook.presto.kafka.decoder.KafkaRowDecoder; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Decoder for raw (direct byte) rows. All field decoders map bytes directly to Presto columns. + */ +public class RawKafkaRowDecoder + implements KafkaRowDecoder +{ + public static final String NAME = "raw"; + + @Override + public String getName() + { + return NAME; + } + + @Override + public boolean decodeRow(byte[] data, Set fieldValueProviders, List columnHandles, Map> fieldDecoders) + { + for (KafkaColumnHandle columnHandle : columnHandles) { + if (columnHandle.isInternal()) { + continue; + } + + @SuppressWarnings("unchecked") + KafkaFieldDecoder decoder = (KafkaFieldDecoder) fieldDecoders.get(columnHandle); + + if (decoder != null) { + fieldValueProviders.add(decoder.decode(data, columnHandle)); + } + } + + return false; + } +} diff --git a/presto-kafka/src/test/java/com/facebook/presto/kafka/KafkaQueryRunner.java b/presto-kafka/src/test/java/com/facebook/presto/kafka/KafkaQueryRunner.java new file mode 100644 index 00000000..8da5fb1b --- /dev/null +++ b/presto-kafka/src/test/java/com/facebook/presto/kafka/KafkaQueryRunner.java @@ -0,0 +1,134 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka; + +import com.facebook.presto.Session; +import com.facebook.presto.kafka.util.CodecSupplier; +import com.facebook.presto.kafka.util.EmbeddedKafka; +import com.facebook.presto.kafka.util.TestUtils; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.metadata.QualifiedTableName; +import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.tests.DistributedQueryRunner; +import com.facebook.presto.tests.TestingPrestoClient; +import com.facebook.presto.tpch.TpchPlugin; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.airlift.json.JsonCodec; +import io.airlift.log.Logger; +import io.airlift.tpch.TpchTable; + +import java.util.Map; + +import static com.facebook.presto.kafka.util.TestUtils.installKafkaPlugin; +import static com.facebook.presto.kafka.util.TestUtils.loadTpchTopicDescription; +import static com.facebook.presto.spi.type.TimeZoneKey.UTC_KEY; +import static com.facebook.presto.tpch.TpchMetadata.TINY_SCHEMA_NAME; +import static io.airlift.testing.Closeables.closeAllSuppress; +import static io.airlift.units.Duration.nanosSince; +import static java.util.Locale.ENGLISH; +import static java.util.concurrent.TimeUnit.SECONDS; + +public final class KafkaQueryRunner +{ + private KafkaQueryRunner() + { + } + + private static final Logger log = Logger.get("TestQueries"); + private static final String TPCH_SCHEMA = "tpch"; + + public static DistributedQueryRunner createKafkaQueryRunner(EmbeddedKafka embeddedKafka, TpchTable... tables) + throws Exception + { + return createKafkaQueryRunner(embeddedKafka, ImmutableList.copyOf(tables)); + } + + public static DistributedQueryRunner createKafkaQueryRunner(EmbeddedKafka embeddedKafka, Iterable> tables) + throws Exception + { + DistributedQueryRunner queryRunner = null; + try { + queryRunner = new DistributedQueryRunner(createSession(), 2); + + queryRunner.installPlugin(new TpchPlugin()); + queryRunner.createCatalog("tpch", "tpch"); + + embeddedKafka.start(); + + for (TpchTable table : tables) { + embeddedKafka.createTopics(kafkaTopicName(table)); + } + + Map topicDescriptions = createTpchTopicDescriptions(queryRunner.getCoordinator().getMetadata(), tables); + + installKafkaPlugin(embeddedKafka, queryRunner, topicDescriptions); + + TestingPrestoClient prestoClient = queryRunner.getClient(); + + log.info("Loading data..."); + long startTime = System.nanoTime(); + for (TpchTable table : tables) { + loadTpchTopic(embeddedKafka, prestoClient, table); + } + log.info("Loading complete in %s", nanosSince(startTime).toString(SECONDS)); + + return queryRunner; + } + catch (Throwable e) { + closeAllSuppress(e, queryRunner, embeddedKafka); + throw e; + } + } + + private static void loadTpchTopic(EmbeddedKafka embeddedKafka, TestingPrestoClient prestoClient, TpchTable table) + { + long start = System.nanoTime(); + log.info("Running import for %s", table.getTableName()); + TestUtils.loadTpchTopic(embeddedKafka, prestoClient, kafkaTopicName(table), new QualifiedTableName("tpch", TINY_SCHEMA_NAME, table.getTableName().toLowerCase(ENGLISH))); + log.info("Imported %s in %s", 0, table.getTableName(), nanosSince(start).convertToMostSuccinctTimeUnit()); + } + + private static String kafkaTopicName(TpchTable table) + { + return TPCH_SCHEMA + "." + table.getTableName().toLowerCase(ENGLISH); + } + + private static Map createTpchTopicDescriptions(Metadata metadata, Iterable> tables) + throws Exception + { + JsonCodec topicDescriptionJsonCodec = new CodecSupplier<>(KafkaTopicDescription.class, metadata).get(); + + ImmutableMap.Builder topicDescriptions = ImmutableMap.builder(); + for (TpchTable table : tables) { + String tableName = table.getTableName(); + SchemaTableName tpchTable = new SchemaTableName(TPCH_SCHEMA, tableName); + + topicDescriptions.put(loadTpchTopicDescription(topicDescriptionJsonCodec, tpchTable.toString(), tpchTable)); + } + return topicDescriptions.build(); + } + + public static Session createSession() + { + return Session.builder() + .setUser("user") + .setSource("test") + .setCatalog("kafka") + .setSchema(TPCH_SCHEMA) + .setTimeZoneKey(UTC_KEY) + .setLocale(ENGLISH) + .build(); + } +} diff --git a/presto-kafka/src/test/java/com/facebook/presto/kafka/TestKafkaConnectorConfig.java b/presto-kafka/src/test/java/com/facebook/presto/kafka/TestKafkaConnectorConfig.java new file mode 100644 index 00000000..92bc30a9 --- /dev/null +++ b/presto-kafka/src/test/java/com/facebook/presto/kafka/TestKafkaConnectorConfig.java @@ -0,0 +1,62 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka; + +import com.google.common.collect.ImmutableMap; +import io.airlift.configuration.testing.ConfigAssertions; +import org.testng.annotations.Test; + +import java.io.File; +import java.util.Map; + +public class TestKafkaConnectorConfig +{ + @Test + public void testDefaults() + { + ConfigAssertions.assertRecordedDefaults(ConfigAssertions.recordDefaults(KafkaConnectorConfig.class) + .setNodes("") + .setKafkaConnectTimeout("10s") + .setKafkaBufferSize("64kB") + .setDefaultSchema("default") + .setTableNames("") + .setTableDescriptionDir(new File("etc/kafka/")) + .setHideInternalColumns(true)); + } + + @Test + public void testExplicitPropertyMappings() + { + Map properties = new ImmutableMap.Builder() + .put("kafka.table-description-dir", "/var/lib/kafka") + .put("kafka.table-names", "table1, table2, table3") + .put("kafka.default-schema", "kafka") + .put("kafka.nodes", "localhost:12345,localhost:23456") + .put("kafka.connect-timeout", "1h") + .put("kafka.buffer-size", "1MB") + .put("kafka.hide-internal-columns", "false") + .build(); + + KafkaConnectorConfig expected = new KafkaConnectorConfig() + .setTableDescriptionDir(new File("/var/lib/kafka")) + .setTableNames("table1, table2, table3") + .setDefaultSchema("kafka") + .setNodes("localhost:12345, localhost:23456") + .setKafkaConnectTimeout("1h") + .setKafkaBufferSize("1MB") + .setHideInternalColumns(false); + + ConfigAssertions.assertFullMapping(properties, expected); + } +} diff --git a/presto-kafka/src/test/java/com/facebook/presto/kafka/TestKafkaDistributed.java b/presto-kafka/src/test/java/com/facebook/presto/kafka/TestKafkaDistributed.java new file mode 100644 index 00000000..398502ef --- /dev/null +++ b/presto-kafka/src/test/java/com/facebook/presto/kafka/TestKafkaDistributed.java @@ -0,0 +1,155 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka; + +import com.facebook.presto.kafka.util.EmbeddedKafka; +import com.facebook.presto.tests.AbstractTestDistributedQueries; +import io.airlift.tpch.TpchTable; +import org.testng.annotations.AfterClass; +import org.testng.annotations.Test; + +import java.io.IOException; + +import static com.facebook.presto.kafka.KafkaQueryRunner.createKafkaQueryRunner; +import static com.facebook.presto.kafka.util.EmbeddedKafka.createEmbeddedKafka; +import static io.airlift.testing.Closeables.closeAllRuntimeException; + +@Test +public class TestKafkaDistributed + extends AbstractTestDistributedQueries +{ + private final EmbeddedKafka embeddedKafka; + + public TestKafkaDistributed() + throws Exception + { + this(createEmbeddedKafka()); + } + + public TestKafkaDistributed(EmbeddedKafka embeddedKafka) + throws Exception + { + super(createKafkaQueryRunner(embeddedKafka, TpchTable.getTables())); + this.embeddedKafka = embeddedKafka; + } + + @AfterClass(alwaysRun = true) + public void destroy() + throws IOException + { + closeAllRuntimeException(queryRunner, embeddedKafka); + } + + // + // Kafka connector does not support table creation. + // + + @Override + public void testCreateTable() + throws Exception + { + } + + @Override + public void testCreateSampledTableAsSelectLimit() + throws Exception + { + } + + @Override + public void testCreateTableAsSelect() + throws Exception + { + } + + @Override + public void testCreateTableAsSelectGroupBy() + throws Exception + { + } + + @Override + public void testCreateTableAsSelectJoin() + throws Exception + { + } + + @Override + public void testCreateTableAsSelectLimit() + throws Exception + { + } + + @Override + public void testSymbolAliasing() + throws Exception + { + } + + // + // Kafka connector does not support views. + // + + @Override + public void testView() + throws Exception + { + } + + @Override + public void testViewMetadata() + throws Exception + { + } + + // + // Kafka connector does not insert. + // + + @Override + public void testInsert() + throws Exception + { + } + + // + // Kafka connector does not delete. + // + + @Override + public void testDelete() + throws Exception + { + } + + // + // Kafka connector does not table rename. + // + + @Override + public void testRenameTable() + throws Exception + { + } + + // + // Kafka connector does not table column. + // + + @Override + public void testRenameColumn() + throws Exception + { + } +} diff --git a/presto-kafka/src/test/java/com/facebook/presto/kafka/TestKafkaIntegrationSmokeTest.java b/presto-kafka/src/test/java/com/facebook/presto/kafka/TestKafkaIntegrationSmokeTest.java new file mode 100644 index 00000000..a73ccf06 --- /dev/null +++ b/presto-kafka/src/test/java/com/facebook/presto/kafka/TestKafkaIntegrationSmokeTest.java @@ -0,0 +1,53 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka; + +import com.facebook.presto.kafka.util.EmbeddedKafka; +import com.facebook.presto.tests.AbstractTestIntegrationSmokeTest; +import org.testng.annotations.AfterClass; +import org.testng.annotations.Test; + +import java.io.IOException; + +import static com.facebook.presto.kafka.KafkaQueryRunner.createKafkaQueryRunner; +import static com.facebook.presto.kafka.util.EmbeddedKafka.createEmbeddedKafka; +import static io.airlift.testing.Closeables.closeAllRuntimeException; +import static io.airlift.tpch.TpchTable.ORDERS; + +@Test +public class TestKafkaIntegrationSmokeTest + extends AbstractTestIntegrationSmokeTest +{ + private final EmbeddedKafka embeddedKafka; + + public TestKafkaIntegrationSmokeTest() + throws Exception + { + this(createEmbeddedKafka()); + } + + public TestKafkaIntegrationSmokeTest(EmbeddedKafka embeddedKafka) + throws Exception + { + super(createKafkaQueryRunner(embeddedKafka, ORDERS)); + this.embeddedKafka = embeddedKafka; + } + + @AfterClass(alwaysRun = true) + public void destroy() + throws IOException + { + closeAllRuntimeException(queryRunner, embeddedKafka); + } +} diff --git a/presto-kafka/src/test/java/com/facebook/presto/kafka/TestKafkaPlugin.java b/presto-kafka/src/test/java/com/facebook/presto/kafka/TestKafkaPlugin.java new file mode 100644 index 00000000..fc609248 --- /dev/null +++ b/presto-kafka/src/test/java/com/facebook/presto/kafka/TestKafkaPlugin.java @@ -0,0 +1,139 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka; + +import com.facebook.presto.spi.Connector; +import com.facebook.presto.spi.ConnectorFactory; +import com.facebook.presto.spi.HostAddress; +import com.facebook.presto.spi.Node; +import com.facebook.presto.spi.NodeManager; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.spi.type.TypeSignature; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import org.testng.annotations.Test; + +import java.net.URI; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +@Test +public class TestKafkaPlugin +{ + @Test + public ConnectorFactory testConnectorExists() + { + KafkaPlugin plugin = new KafkaPlugin(); + plugin.setTypeManager(new TestingTypeManager()); + plugin.setNodeManager(new TestingNodeManager()); + + List factories = plugin.getServices(ConnectorFactory.class); + assertNotNull(factories); + assertEquals(factories.size(), 1); + ConnectorFactory factory = factories.get(0); + assertNotNull(factory); + return factory; + } + + @Test + public void testSpinup() + { + ConnectorFactory factory = testConnectorExists(); + Connector c = factory.create("test-connector", ImmutableMap.builder() + .put("kafka.table-names", "test") + .put("kafka.nodes", "localhost:9092") + .build()); + assertNotNull(c); + } + + private static class TestingTypeManager + implements TypeManager + { + @Override + public Type getType(TypeSignature signature) + { + return null; + } + + @Override + public Type getParameterizedType(String baseTypeName, List typeParameters, List literalParameters) + { + return null; + } + + @Override + public List getTypes() + { + return ImmutableList.of(); + } + } + + private static class TestingNodeManager + implements NodeManager + { + private static final Node LOCAL_NODE = new TestingNode(); + + @Override + public Set getActiveNodes() + { + return ImmutableSet.of(LOCAL_NODE); + } + + @Override + public Set getActiveDatasourceNodes(String datasourceName) + { + return ImmutableSet.of(LOCAL_NODE); + } + + @Override + public Node getCurrentNode() + { + return LOCAL_NODE; + } + + @Override + public Set getCoordinators() + { + return ImmutableSet.of(LOCAL_NODE); + } + } + + private static class TestingNode + implements Node + { + @Override + public HostAddress getHostAndPort() + { + return HostAddress.fromParts("localhost", 8080); + } + + @Override + public URI getHttpUri() + { + return URI.create("http://localhost:8080/"); + } + + @Override + public String getNodeIdentifier() + { + return UUID.randomUUID().toString(); + } + } +} diff --git a/presto-kafka/src/test/java/com/facebook/presto/kafka/TestManySegments.java b/presto-kafka/src/test/java/com/facebook/presto/kafka/TestManySegments.java new file mode 100644 index 00000000..9ae8b7de --- /dev/null +++ b/presto-kafka/src/test/java/com/facebook/presto/kafka/TestManySegments.java @@ -0,0 +1,122 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka; + +import com.facebook.presto.Session; +import com.facebook.presto.kafka.util.EmbeddedKafka; +import com.facebook.presto.kafka.util.TestUtils; +import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.spi.type.BigintType; +import com.facebook.presto.testing.MaterializedResult; +import com.facebook.presto.tests.StandaloneQueryRunner; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import kafka.producer.KeyedMessage; +import org.testng.annotations.AfterClass; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.util.Properties; +import java.util.UUID; + +import static com.facebook.presto.kafka.util.EmbeddedKafka.CloseableProducer; +import static com.facebook.presto.kafka.util.TestUtils.createEmptyTopicDescription; +import static com.facebook.presto.spi.type.TimeZoneKey.UTC_KEY; +import static java.util.Locale.ENGLISH; +import static org.testng.Assert.assertEquals; + +@Test(singleThreaded = true) +public class TestManySegments +{ + private static final Session SESSION = Session.builder() + .setUser("user") + .setSource("source") + .setCatalog("kafka") + .setSchema("default") + .setTimeZoneKey(UTC_KEY) + .setLocale(ENGLISH) + .build(); + + private EmbeddedKafka embeddedKafka; + private String topicName; + private StandaloneQueryRunner queryRunner; + + @BeforeClass + public void startKafka() + throws Exception + { + embeddedKafka = EmbeddedKafka.createEmbeddedKafka(); + embeddedKafka.start(); + + topicName = "test_" + UUID.randomUUID().toString().replaceAll("-", "_"); + + Properties topicProperties = new Properties(); + topicProperties.setProperty("segment.bytes", "256"); + + embeddedKafka.createTopics(1, 1, topicProperties, topicName); + + try (CloseableProducer producer = embeddedKafka.createProducer()) { + int jMax = 10_000; + int iMax = 100_000 / jMax; + for (long i = 0; i < iMax; i++) { + ImmutableList.Builder> builder = ImmutableList.builder(); + for (long j = 0; j < jMax; j++) { + builder.add(new KeyedMessage(topicName, i, ImmutableMap.of("id", Long.toString(i * iMax + j), "value", UUID.randomUUID().toString()))); + } + producer.send(builder.build()); + } + } + } + + @AfterClass(alwaysRun = true) + public void stopKafka() + throws Exception + { + embeddedKafka.close(); + } + + @BeforeMethod + public void spinUp() + throws Exception + { + this.queryRunner = new StandaloneQueryRunner(SESSION); + + TestUtils.installKafkaPlugin(embeddedKafka, queryRunner, + ImmutableMap.builder() + .put(createEmptyTopicDescription(topicName, new SchemaTableName("default", topicName))) + .build()); + } + + @AfterMethod(alwaysRun = true) + public void tearDown() + throws Exception + { + queryRunner.close(); + } + + @Test + public void testManySegments() + throws Exception + { + MaterializedResult result = queryRunner.execute("SELECT count(_message) from " + topicName); + + MaterializedResult expected = MaterializedResult.resultBuilder(SESSION, BigintType.BIGINT) + .row(100000) + .build(); + + assertEquals(result, expected); + } +} diff --git a/presto-kafka/src/test/java/com/facebook/presto/kafka/TestMinimalFunctionality.java b/presto-kafka/src/test/java/com/facebook/presto/kafka/TestMinimalFunctionality.java new file mode 100644 index 00000000..88728ea4 --- /dev/null +++ b/presto-kafka/src/test/java/com/facebook/presto/kafka/TestMinimalFunctionality.java @@ -0,0 +1,141 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka; + +import com.facebook.presto.Session; +import com.facebook.presto.kafka.util.EmbeddedKafka; +import com.facebook.presto.kafka.util.TestUtils; +import com.facebook.presto.metadata.QualifiedTableName; +import com.facebook.presto.metadata.TableHandle; +import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.spi.type.BigintType; +import com.facebook.presto.testing.MaterializedResult; +import com.facebook.presto.tests.StandaloneQueryRunner; +import com.google.common.collect.ImmutableMap; +import kafka.producer.KeyedMessage; +import org.testng.annotations.AfterClass; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.util.Optional; +import java.util.Properties; +import java.util.UUID; + +import static com.facebook.presto.kafka.util.EmbeddedKafka.CloseableProducer; +import static com.facebook.presto.kafka.util.TestUtils.createEmptyTopicDescription; +import static com.facebook.presto.spi.type.TimeZoneKey.UTC_KEY; +import static java.util.Locale.ENGLISH; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +@Test(singleThreaded = true) +public class TestMinimalFunctionality +{ + private static final Session SESSION = Session.builder() + .setUser("user") + .setSource("source") + .setCatalog("kafka") + .setSchema("default") + .setTimeZoneKey(UTC_KEY) + .setLocale(ENGLISH) + .build(); + + private EmbeddedKafka embeddedKafka; + private String topicName; + private StandaloneQueryRunner queryRunner; + + @BeforeClass + public void startKafka() + throws Exception + { + embeddedKafka = EmbeddedKafka.createEmbeddedKafka(); + embeddedKafka.start(); + } + + @AfterClass + public void stopKafka() + throws Exception + { + embeddedKafka.close(); + } + + @BeforeMethod + public void spinUp() + throws Exception + { + this.topicName = "test_" + UUID.randomUUID().toString().replaceAll("-", "_"); + + Properties topicProperties = new Properties(); + embeddedKafka.createTopics(2, 1, topicProperties, topicName); + + this.queryRunner = new StandaloneQueryRunner(SESSION); + + TestUtils.installKafkaPlugin(embeddedKafka, queryRunner, + ImmutableMap.builder() + .put(createEmptyTopicDescription(topicName, new SchemaTableName("default", topicName))) + .build()); + } + + @AfterMethod + public void tearDown() + throws Exception + { + queryRunner.close(); + } + + private void createMessages(String topicName, int count) + { + try (CloseableProducer producer = embeddedKafka.createProducer()) { + for (long i = 0; i < count; i++) { + Object message = ImmutableMap.of("id", Long.toString(i), "value", UUID.randomUUID().toString()); + producer.send(new KeyedMessage<>(topicName, i, message)); + } + } + } + + @Test + public void testTopicExists() + throws Exception + { + QualifiedTableName name = new QualifiedTableName("kafka", "default", topicName); + Optional handle = queryRunner.getServer().getMetadata().getTableHandle(SESSION, name); + assertTrue(handle.isPresent()); + } + + @Test + public void testTopicHasData() + throws Exception + { + MaterializedResult result = queryRunner.execute("SELECT count(1) from " + topicName); + + MaterializedResult expected = MaterializedResult.resultBuilder(SESSION, BigintType.BIGINT) + .row(0) + .build(); + + assertEquals(result, expected); + + int count = 1000; + createMessages(topicName, count); + + result = queryRunner.execute("SELECT count(1) from " + topicName); + + expected = MaterializedResult.resultBuilder(SESSION, BigintType.BIGINT) + .row(count) + .build(); + + assertEquals(result, expected); + } +} diff --git a/presto-kafka/src/test/java/com/facebook/presto/kafka/decoder/csv/TestCsvDecoder.java b/presto-kafka/src/test/java/com/facebook/presto/kafka/decoder/csv/TestCsvDecoder.java new file mode 100644 index 00000000..f7b62f9f --- /dev/null +++ b/presto-kafka/src/test/java/com/facebook/presto/kafka/decoder/csv/TestCsvDecoder.java @@ -0,0 +1,142 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka.decoder.csv; + +import com.facebook.presto.kafka.KafkaColumnHandle; +import com.facebook.presto.kafka.KafkaFieldValueProvider; +import com.facebook.presto.kafka.decoder.KafkaFieldDecoder; +import com.facebook.presto.spi.type.BigintType; +import com.facebook.presto.spi.type.BooleanType; +import com.facebook.presto.spi.type.DoubleType; +import com.facebook.presto.spi.type.VarcharType; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import org.testng.annotations.Test; + +import java.nio.charset.StandardCharsets; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.facebook.presto.kafka.decoder.util.DecoderTestUtil.checkValue; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; + +public class TestCsvDecoder +{ + private static final CsvKafkaFieldDecoder DEFAULT_FIELD_DECODER = new CsvKafkaFieldDecoder(); + + private static Map> buildMap(List columns) + { + ImmutableMap.Builder> map = ImmutableMap.builder(); + for (KafkaColumnHandle column : columns) { + map.put(column, DEFAULT_FIELD_DECODER); + } + return map.build(); + } + + @Test + public void testSimple() + { + String csv = "\"row 1\",row2,\"row3\",100,\"200\",300,4.5"; + + CsvKafkaRowDecoder rowDecoder = new CsvKafkaRowDecoder(); + KafkaColumnHandle row1 = new KafkaColumnHandle("", 0, "row1", VarcharType.VARCHAR, "0", null, null, false, false, false); + KafkaColumnHandle row2 = new KafkaColumnHandle("", 1, "row2", VarcharType.VARCHAR, "1", null, null, false, false, false); + KafkaColumnHandle row3 = new KafkaColumnHandle("", 2, "row3", VarcharType.VARCHAR, "2", null, null, false, false, false); + KafkaColumnHandle row4 = new KafkaColumnHandle("", 3, "row4", BigintType.BIGINT, "3", null, null, false, false, false); + KafkaColumnHandle row5 = new KafkaColumnHandle("", 4, "row5", BigintType.BIGINT, "4", null, null, false, false, false); + KafkaColumnHandle row6 = new KafkaColumnHandle("", 5, "row6", BigintType.BIGINT, "5", null, null, false, false, false); + KafkaColumnHandle row7 = new KafkaColumnHandle("", 6, "row7", DoubleType.DOUBLE, "6", null, null, false, false, false); + + List columns = ImmutableList.of(row1, row2, row3, row4, row5, row6, row7); + Set providers = new HashSet<>(); + + boolean corrupt = rowDecoder.decodeRow(csv.getBytes(StandardCharsets.UTF_8), providers, columns, buildMap(columns)); + assertFalse(corrupt); + + assertEquals(providers.size(), columns.size()); + + checkValue(providers, row1, "row 1"); + checkValue(providers, row2, "row2"); + checkValue(providers, row3, "row3"); + checkValue(providers, row4, 100); + checkValue(providers, row5, 200); + checkValue(providers, row6, 300); + checkValue(providers, row7, 4.5d); + } + + @Test + public void testBoolean() + { + String csv = "True,False,0,1,\"0\",\"1\",\"true\",\"false\""; + + CsvKafkaRowDecoder rowDecoder = new CsvKafkaRowDecoder(); + + KafkaColumnHandle row1 = new KafkaColumnHandle("", 0, "row1", BooleanType.BOOLEAN, "0", null, null, false, false, false); + KafkaColumnHandle row2 = new KafkaColumnHandle("", 1, "row2", BooleanType.BOOLEAN, "1", null, null, false, false, false); + KafkaColumnHandle row3 = new KafkaColumnHandle("", 2, "row3", BooleanType.BOOLEAN, "2", null, null, false, false, false); + KafkaColumnHandle row4 = new KafkaColumnHandle("", 3, "row4", BooleanType.BOOLEAN, "3", null, null, false, false, false); + KafkaColumnHandle row5 = new KafkaColumnHandle("", 4, "row5", BooleanType.BOOLEAN, "4", null, null, false, false, false); + KafkaColumnHandle row6 = new KafkaColumnHandle("", 5, "row6", BooleanType.BOOLEAN, "5", null, null, false, false, false); + KafkaColumnHandle row7 = new KafkaColumnHandle("", 6, "row7", BooleanType.BOOLEAN, "6", null, null, false, false, false); + KafkaColumnHandle row8 = new KafkaColumnHandle("", 7, "row8", BooleanType.BOOLEAN, "7", null, null, false, false, false); + + List columns = ImmutableList.of(row1, row2, row3, row4, row5, row6, row7, row8); + + Set providers = new HashSet<>(); + + boolean corrupt = rowDecoder.decodeRow(csv.getBytes(StandardCharsets.UTF_8), providers, columns, buildMap(columns)); + assertFalse(corrupt); + + assertEquals(providers.size(), columns.size()); + + checkValue(providers, row1, true); + checkValue(providers, row2, false); + checkValue(providers, row3, false); + checkValue(providers, row4, false); + checkValue(providers, row5, false); + checkValue(providers, row6, false); + checkValue(providers, row7, true); + checkValue(providers, row8, false); + } + + @Test + public void testNulls() + { + String csv = ",,,"; + + CsvKafkaRowDecoder rowDecoder = new CsvKafkaRowDecoder(); + + KafkaColumnHandle row1 = new KafkaColumnHandle("", 0, "row1", VarcharType.VARCHAR, "0", null, null, false, false, false); + KafkaColumnHandle row2 = new KafkaColumnHandle("", 1, "row2", BigintType.BIGINT, "1", null, null, false, false, false); + KafkaColumnHandle row3 = new KafkaColumnHandle("", 2, "row3", DoubleType.DOUBLE, "2", null, null, false, false, false); + KafkaColumnHandle row4 = new KafkaColumnHandle("", 3, "row4", BooleanType.BOOLEAN, "3", null, null, false, false, false); + + List columns = ImmutableList.of(row1, row2, row3, row4); + + Set providers = new HashSet<>(); + + boolean corrupt = rowDecoder.decodeRow(csv.getBytes(StandardCharsets.UTF_8), providers, columns, buildMap(columns)); + assertFalse(corrupt); + + assertEquals(providers.size(), columns.size()); + + checkValue(providers, row1, ""); + checkValue(providers, row2, 0); + checkValue(providers, row3, 0.0d); + checkValue(providers, row4, false); + } +} diff --git a/presto-kafka/src/test/java/com/facebook/presto/kafka/decoder/json/TestISO8601JsonKafkaFieldDecoder.java b/presto-kafka/src/test/java/com/facebook/presto/kafka/decoder/json/TestISO8601JsonKafkaFieldDecoder.java new file mode 100644 index 00000000..623eb6c9 --- /dev/null +++ b/presto-kafka/src/test/java/com/facebook/presto/kafka/decoder/json/TestISO8601JsonKafkaFieldDecoder.java @@ -0,0 +1,135 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka.decoder.json; + +import com.facebook.presto.kafka.KafkaColumnHandle; +import com.facebook.presto.kafka.KafkaFieldValueProvider; +import com.facebook.presto.kafka.decoder.KafkaFieldDecoder; +import com.facebook.presto.spi.type.BigintType; +import com.facebook.presto.spi.type.VarcharType; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.airlift.json.ObjectMapperProvider; +import org.joda.time.format.DateTimeFormatter; +import org.joda.time.format.ISODateTimeFormat; +import org.testng.annotations.Test; + +import java.nio.charset.StandardCharsets; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import static com.facebook.presto.kafka.decoder.KafkaFieldDecoder.DEFAULT_FIELD_DECODER_NAME; +import static com.facebook.presto.kafka.decoder.util.DecoderTestUtil.checkIsNull; +import static com.facebook.presto.kafka.decoder.util.DecoderTestUtil.checkValue; +import static java.lang.String.format; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; + +public class TestISO8601JsonKafkaFieldDecoder +{ + private static final Map DECODERS = ImmutableMap.of(DEFAULT_FIELD_DECODER_NAME, new JsonKafkaFieldDecoder(), + ISO8601JsonKafkaFieldDecoder.NAME, new ISO8601JsonKafkaFieldDecoder()); + + private static final ObjectMapperProvider PROVIDER = new ObjectMapperProvider(); + + private static final DateTimeFormatter PRINTER = ISODateTimeFormat.dateTime().withLocale(Locale.ENGLISH).withZoneUTC(); + + private static Map> buildMap(List columns) + { + ImmutableMap.Builder> map = ImmutableMap.builder(); + for (KafkaColumnHandle column : columns) { + map.put(column, DECODERS.get(column.getDataFormat())); + } + return map.build(); + } + + @Test + public void testBasicFormatting() + throws Exception + { + long now = System.currentTimeMillis(); + String nowString = PRINTER.print(now); + + byte[] json = format("{\"a_number\":%d,\"a_string\":\"%s\"}", now, nowString).getBytes(StandardCharsets.UTF_8); + + JsonKafkaRowDecoder rowDecoder = new JsonKafkaRowDecoder(PROVIDER.get()); + KafkaColumnHandle row1 = new KafkaColumnHandle("", 0, "row1", BigintType.BIGINT, "a_number", DEFAULT_FIELD_DECODER_NAME, null, false, false, false); + KafkaColumnHandle row2 = new KafkaColumnHandle("", 1, "row2", VarcharType.VARCHAR, "a_string", DEFAULT_FIELD_DECODER_NAME, null, false, false, false); + + KafkaColumnHandle row3 = new KafkaColumnHandle("", 2, "row3", BigintType.BIGINT, "a_number", ISO8601JsonKafkaFieldDecoder.NAME, null, false, false, false); + KafkaColumnHandle row4 = new KafkaColumnHandle("", 3, "row4", BigintType.BIGINT, "a_string", ISO8601JsonKafkaFieldDecoder.NAME, null, false, false, false); + + KafkaColumnHandle row5 = new KafkaColumnHandle("", 4, "row5", VarcharType.VARCHAR, "a_number", ISO8601JsonKafkaFieldDecoder.NAME, null, false, false, false); + KafkaColumnHandle row6 = new KafkaColumnHandle("", 5, "row6", VarcharType.VARCHAR, "a_string", ISO8601JsonKafkaFieldDecoder.NAME, null, false, false, false); + + List columns = ImmutableList.of(row1, row2, row3, row4, row5, row6); + Set providers = new HashSet<>(); + + boolean corrupt = rowDecoder.decodeRow(json, providers, columns, buildMap(columns)); + assertFalse(corrupt); + + assertEquals(providers.size(), columns.size()); + + // sanity checks + checkValue(providers, row1, now); + checkValue(providers, row2, nowString); + + // number parsed as number --> as is + checkValue(providers, row3, now); + // string parsed as number --> parse text, convert to timestamp + checkValue(providers, row4, now); + + // number parsed as string --> parse text, convert to timestamp, turn into string + checkValue(providers, row5, Long.toString(now)); + + // string parsed as string --> as is + checkValue(providers, row6, nowString); + } + + @Test + public void testNullValues() + throws Exception + { + byte[] json = "{}".getBytes(StandardCharsets.UTF_8); + + JsonKafkaRowDecoder rowDecoder = new JsonKafkaRowDecoder(PROVIDER.get()); + KafkaColumnHandle row1 = new KafkaColumnHandle("", 0, "row1", BigintType.BIGINT, "a_number", DEFAULT_FIELD_DECODER_NAME, null, false, false, false); + KafkaColumnHandle row2 = new KafkaColumnHandle("", 1, "row2", VarcharType.VARCHAR, "a_string", DEFAULT_FIELD_DECODER_NAME, null, false, false, false); + + KafkaColumnHandle row3 = new KafkaColumnHandle("", 2, "row3", BigintType.BIGINT, "a_number", ISO8601JsonKafkaFieldDecoder.NAME, null, false, false, false); + KafkaColumnHandle row4 = new KafkaColumnHandle("", 3, "row4", BigintType.BIGINT, "a_string", ISO8601JsonKafkaFieldDecoder.NAME, null, false, false, false); + + KafkaColumnHandle row5 = new KafkaColumnHandle("", 4, "row5", VarcharType.VARCHAR, "a_number", ISO8601JsonKafkaFieldDecoder.NAME, null, false, false, false); + KafkaColumnHandle row6 = new KafkaColumnHandle("", 5, "row6", VarcharType.VARCHAR, "a_string", ISO8601JsonKafkaFieldDecoder.NAME, null, false, false, false); + + List columns = ImmutableList.of(row1, row2, row3, row4, row5, row6); + Set providers = new HashSet<>(); + + boolean corrupt = rowDecoder.decodeRow(json, providers, columns, buildMap(columns)); + assertFalse(corrupt); + + assertEquals(providers.size(), columns.size()); + + // sanity checks + checkIsNull(providers, row1); + checkIsNull(providers, row2); + checkIsNull(providers, row3); + checkIsNull(providers, row4); + checkIsNull(providers, row5); + checkIsNull(providers, row6); + } +} diff --git a/presto-kafka/src/test/java/com/facebook/presto/kafka/decoder/json/TestJsonDecoder.java b/presto-kafka/src/test/java/com/facebook/presto/kafka/decoder/json/TestJsonDecoder.java new file mode 100644 index 00000000..503f052a --- /dev/null +++ b/presto-kafka/src/test/java/com/facebook/presto/kafka/decoder/json/TestJsonDecoder.java @@ -0,0 +1,133 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka.decoder.json; + +import com.facebook.presto.kafka.KafkaColumnHandle; +import com.facebook.presto.kafka.KafkaFieldValueProvider; +import com.facebook.presto.kafka.decoder.KafkaFieldDecoder; +import com.facebook.presto.spi.type.BigintType; +import com.facebook.presto.spi.type.BooleanType; +import com.facebook.presto.spi.type.DoubleType; +import com.facebook.presto.spi.type.VarcharType; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.io.ByteStreams; +import io.airlift.json.ObjectMapperProvider; +import org.testng.annotations.Test; + +import java.nio.charset.StandardCharsets; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.facebook.presto.kafka.decoder.util.DecoderTestUtil.checkIsNull; +import static com.facebook.presto.kafka.decoder.util.DecoderTestUtil.checkValue; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; + +public class TestJsonDecoder +{ + private static final JsonKafkaFieldDecoder DEFAULT_FIELD_DECODER = new JsonKafkaFieldDecoder(); + private static final ObjectMapperProvider PROVIDER = new ObjectMapperProvider(); + + private static Map> buildMap(List columns) + { + ImmutableMap.Builder> map = ImmutableMap.builder(); + for (KafkaColumnHandle column : columns) { + map.put(column, DEFAULT_FIELD_DECODER); + } + return map.build(); + } + + @Test + public void testSimple() + throws Exception + { + byte[] json = ByteStreams.toByteArray(TestJsonDecoder.class.getResourceAsStream("/decoder/json/message.json")); + + JsonKafkaRowDecoder rowDecoder = new JsonKafkaRowDecoder(PROVIDER.get()); + KafkaColumnHandle row1 = new KafkaColumnHandle("", 0, "row1", VarcharType.VARCHAR, "source", null, null, false, false, false); + KafkaColumnHandle row2 = new KafkaColumnHandle("", 1, "row2", VarcharType.VARCHAR, "user/screen_name", null, null, false, false, false); + KafkaColumnHandle row3 = new KafkaColumnHandle("", 2, "row3", BigintType.BIGINT, "id", null, null, false, false, false); + KafkaColumnHandle row4 = new KafkaColumnHandle("", 3, "row4", BigintType.BIGINT, "user/statuses_count", null, null, false, false, false); + KafkaColumnHandle row5 = new KafkaColumnHandle("", 4, "row5", BooleanType.BOOLEAN, "user/geo_enabled", null, null, false, false, false); + + List columns = ImmutableList.of(row1, row2, row3, row4, row5); + Set providers = new HashSet<>(); + + boolean corrupt = rowDecoder.decodeRow(json, providers, columns, buildMap(columns)); + assertFalse(corrupt); + + assertEquals(providers.size(), columns.size()); + + checkValue(providers, row1, "twitterfeed"); + checkValue(providers, row2, "EKentuckyNews"); + checkValue(providers, row3, 493857959588286460L); + checkValue(providers, row4, 7630); + checkValue(providers, row5, true); + } + + @Test + public void testNonExistent() + throws Exception + { + byte[] json = "{}".getBytes(StandardCharsets.UTF_8); + + JsonKafkaRowDecoder rowDecoder = new JsonKafkaRowDecoder(PROVIDER.get()); + KafkaColumnHandle row1 = new KafkaColumnHandle("", 0, "row1", VarcharType.VARCHAR, "very/deep/varchar", null, null, false, false, false); + KafkaColumnHandle row2 = new KafkaColumnHandle("", 1, "row2", BigintType.BIGINT, "no_bigint", null, null, false, false, false); + KafkaColumnHandle row3 = new KafkaColumnHandle("", 2, "row3", DoubleType.DOUBLE, "double/is_missing", null, null, false, false, false); + KafkaColumnHandle row4 = new KafkaColumnHandle("", 3, "row4", BooleanType.BOOLEAN, "hello", null, null, false, false, false); + + List columns = ImmutableList.of(row1, row2, row3, row4); + Set providers = new HashSet<>(); + + boolean corrupt = rowDecoder.decodeRow(json, providers, columns, buildMap(columns)); + assertFalse(corrupt); + + assertEquals(providers.size(), columns.size()); + + checkIsNull(providers, row1); + checkIsNull(providers, row2); + checkIsNull(providers, row3); + checkIsNull(providers, row4); + } + + @Test + public void testStringNumber() + throws Exception + { + byte[] json = "{\"a_number\":481516,\"a_string\":\"2342\"}".getBytes(StandardCharsets.UTF_8); + + JsonKafkaRowDecoder rowDecoder = new JsonKafkaRowDecoder(PROVIDER.get()); + KafkaColumnHandle row1 = new KafkaColumnHandle("", 0, "row1", VarcharType.VARCHAR, "a_number", null, null, false, false, false); + KafkaColumnHandle row2 = new KafkaColumnHandle("", 1, "row2", BigintType.BIGINT, "a_number", null, null, false, false, false); + KafkaColumnHandle row3 = new KafkaColumnHandle("", 2, "row3", VarcharType.VARCHAR, "a_string", null, null, false, false, false); + KafkaColumnHandle row4 = new KafkaColumnHandle("", 3, "row4", BigintType.BIGINT, "a_string", null, null, false, false, false); + + List columns = ImmutableList.of(row1, row2, row3, row4); + Set providers = new HashSet<>(); + + boolean corrupt = rowDecoder.decodeRow(json, providers, columns, buildMap(columns)); + assertFalse(corrupt); + + assertEquals(providers.size(), columns.size()); + + checkValue(providers, row1, "481516"); + checkValue(providers, row2, 481516); + checkValue(providers, row3, "2342"); + checkValue(providers, row4, 2342); + } +} diff --git a/presto-kafka/src/test/java/com/facebook/presto/kafka/decoder/json/TestMillisecondsSinceEpochJsonKafkaFieldDecoder.java b/presto-kafka/src/test/java/com/facebook/presto/kafka/decoder/json/TestMillisecondsSinceEpochJsonKafkaFieldDecoder.java new file mode 100644 index 00000000..0c61bd54 --- /dev/null +++ b/presto-kafka/src/test/java/com/facebook/presto/kafka/decoder/json/TestMillisecondsSinceEpochJsonKafkaFieldDecoder.java @@ -0,0 +1,130 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka.decoder.json; + +import com.facebook.presto.kafka.KafkaColumnHandle; +import com.facebook.presto.kafka.KafkaFieldValueProvider; +import com.facebook.presto.kafka.decoder.KafkaFieldDecoder; +import com.facebook.presto.spi.type.BigintType; +import com.facebook.presto.spi.type.VarcharType; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.airlift.json.ObjectMapperProvider; +import org.testng.annotations.Test; + +import java.nio.charset.StandardCharsets; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.facebook.presto.kafka.decoder.KafkaFieldDecoder.DEFAULT_FIELD_DECODER_NAME; +import static com.facebook.presto.kafka.decoder.util.DecoderTestUtil.checkIsNull; +import static com.facebook.presto.kafka.decoder.util.DecoderTestUtil.checkValue; +import static java.lang.String.format; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; + +public class TestMillisecondsSinceEpochJsonKafkaFieldDecoder +{ + private static final Map DECODERS = ImmutableMap.of(DEFAULT_FIELD_DECODER_NAME, new JsonKafkaFieldDecoder(), + MillisecondsSinceEpochJsonKafkaFieldDecoder.NAME, new MillisecondsSinceEpochJsonKafkaFieldDecoder()); + + private static final ObjectMapperProvider PROVIDER = new ObjectMapperProvider(); + + private static Map> buildMap(List columns) + { + ImmutableMap.Builder> map = ImmutableMap.builder(); + for (KafkaColumnHandle column : columns) { + map.put(column, DECODERS.get(column.getDataFormat())); + } + return map.build(); + } + + @Test + public void testBasicFormatting() + throws Exception + { + long now = System.currentTimeMillis(); + String nowString = MillisecondsSinceEpochJsonKafkaFieldDecoder.FORMATTER.print(now); + + byte[] json = format("{\"a_number\":%d,\"a_string\":\"%d\"}", now, now).getBytes(StandardCharsets.UTF_8); + + JsonKafkaRowDecoder rowDecoder = new JsonKafkaRowDecoder(PROVIDER.get()); + KafkaColumnHandle row1 = new KafkaColumnHandle("", 0, "row1", BigintType.BIGINT, "a_number", DEFAULT_FIELD_DECODER_NAME, null, false, false, false); + KafkaColumnHandle row2 = new KafkaColumnHandle("", 1, "row2", VarcharType.VARCHAR, "a_string", DEFAULT_FIELD_DECODER_NAME, null, false, false, false); + + KafkaColumnHandle row3 = new KafkaColumnHandle("", 2, "row3", BigintType.BIGINT, "a_number", MillisecondsSinceEpochJsonKafkaFieldDecoder.NAME, null, false, false, false); + KafkaColumnHandle row4 = new KafkaColumnHandle("", 3, "row4", BigintType.BIGINT, "a_string", MillisecondsSinceEpochJsonKafkaFieldDecoder.NAME, null, false, false, false); + + KafkaColumnHandle row5 = new KafkaColumnHandle("", 4, "row5", VarcharType.VARCHAR, "a_number", MillisecondsSinceEpochJsonKafkaFieldDecoder.NAME, null, false, false, false); + KafkaColumnHandle row6 = new KafkaColumnHandle("", 5, "row6", VarcharType.VARCHAR, "a_string", MillisecondsSinceEpochJsonKafkaFieldDecoder.NAME, null, false, false, false); + + List columns = ImmutableList.of(row1, row2, row3, row4, row5, row6); + Set providers = new HashSet<>(); + + boolean corrupt = rowDecoder.decodeRow(json, providers, columns, buildMap(columns)); + assertFalse(corrupt); + + assertEquals(providers.size(), columns.size()); + + // sanity checks + checkValue(providers, row1, now); + checkValue(providers, row2, Long.toString(now)); + + // number parsed as number --> return as time stamp (millis) + checkValue(providers, row3, now); + // string parsed as number --> parse text, convert to timestamp + checkValue(providers, row4, now); + + // number parsed as string --> parse text, convert to timestamp, turn into string + checkValue(providers, row5, nowString); + + // string parsed as string --> parse text, convert to timestamp, turn into string + checkValue(providers, row6, nowString); + } + + @Test + public void testNullValues() + throws Exception + { + byte[] json = "{}".getBytes(StandardCharsets.UTF_8); + + JsonKafkaRowDecoder rowDecoder = new JsonKafkaRowDecoder(PROVIDER.get()); + KafkaColumnHandle row1 = new KafkaColumnHandle("", 0, "row1", BigintType.BIGINT, "a_number", DEFAULT_FIELD_DECODER_NAME, null, false, false, false); + KafkaColumnHandle row2 = new KafkaColumnHandle("", 1, "row2", VarcharType.VARCHAR, "a_string", DEFAULT_FIELD_DECODER_NAME, null, false, false, false); + + KafkaColumnHandle row3 = new KafkaColumnHandle("", 2, "row3", BigintType.BIGINT, "a_number", MillisecondsSinceEpochJsonKafkaFieldDecoder.NAME, null, false, false, false); + KafkaColumnHandle row4 = new KafkaColumnHandle("", 3, "row4", BigintType.BIGINT, "a_string", MillisecondsSinceEpochJsonKafkaFieldDecoder.NAME, null, false, false, false); + + KafkaColumnHandle row5 = new KafkaColumnHandle("", 4, "row5", VarcharType.VARCHAR, "a_number", MillisecondsSinceEpochJsonKafkaFieldDecoder.NAME, null, false, false, false); + KafkaColumnHandle row6 = new KafkaColumnHandle("", 5, "row6", VarcharType.VARCHAR, "a_string", MillisecondsSinceEpochJsonKafkaFieldDecoder.NAME, null, false, false, false); + + List columns = ImmutableList.of(row1, row2, row3, row4, row5, row6); + Set providers = new HashSet<>(); + + boolean corrupt = rowDecoder.decodeRow(json, providers, columns, buildMap(columns)); + assertFalse(corrupt); + + assertEquals(providers.size(), columns.size()); + + // sanity checks + checkIsNull(providers, row1); + checkIsNull(providers, row2); + checkIsNull(providers, row3); + checkIsNull(providers, row4); + checkIsNull(providers, row5); + checkIsNull(providers, row6); + } +} diff --git a/presto-kafka/src/test/java/com/facebook/presto/kafka/decoder/json/TestRFC2822JsonKafkaFieldDecoder.java b/presto-kafka/src/test/java/com/facebook/presto/kafka/decoder/json/TestRFC2822JsonKafkaFieldDecoder.java new file mode 100644 index 00000000..45b831ee --- /dev/null +++ b/presto-kafka/src/test/java/com/facebook/presto/kafka/decoder/json/TestRFC2822JsonKafkaFieldDecoder.java @@ -0,0 +1,131 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka.decoder.json; + +import com.facebook.presto.kafka.KafkaColumnHandle; +import com.facebook.presto.kafka.KafkaFieldValueProvider; +import com.facebook.presto.kafka.decoder.KafkaFieldDecoder; +import com.facebook.presto.spi.type.BigintType; +import com.facebook.presto.spi.type.VarcharType; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.airlift.json.ObjectMapperProvider; +import org.testng.annotations.Test; + +import java.nio.charset.StandardCharsets; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.facebook.presto.kafka.decoder.KafkaFieldDecoder.DEFAULT_FIELD_DECODER_NAME; +import static com.facebook.presto.kafka.decoder.json.RFC2822JsonKafkaFieldDecoder.FORMATTER; +import static com.facebook.presto.kafka.decoder.util.DecoderTestUtil.checkIsNull; +import static com.facebook.presto.kafka.decoder.util.DecoderTestUtil.checkValue; +import static java.lang.String.format; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; + +public class TestRFC2822JsonKafkaFieldDecoder +{ + private static final Map DECODERS = ImmutableMap.of(DEFAULT_FIELD_DECODER_NAME, new JsonKafkaFieldDecoder(), + RFC2822JsonKafkaFieldDecoder.NAME, new RFC2822JsonKafkaFieldDecoder()); + + private static final ObjectMapperProvider PROVIDER = new ObjectMapperProvider(); + + private static Map> map(List columns) + { + ImmutableMap.Builder> map = ImmutableMap.builder(); + for (KafkaColumnHandle column : columns) { + map.put(column, DECODERS.get(column.getDataFormat())); + } + return map.build(); + } + + @Test + public void testBasicFormatting() + throws Exception + { + long now = (System.currentTimeMillis() / 1000) * 1000; // rfc2822 is second granularity + String nowString = FORMATTER.print(now); + + byte[] json = format("{\"a_number\":%d,\"a_string\":\"%s\"}", now, nowString).getBytes(StandardCharsets.UTF_8); + + JsonKafkaRowDecoder rowDecoder = new JsonKafkaRowDecoder(PROVIDER.get()); + KafkaColumnHandle row1 = new KafkaColumnHandle("", 0, "row1", BigintType.BIGINT, "a_number", DEFAULT_FIELD_DECODER_NAME, null, false, false, false); + KafkaColumnHandle row2 = new KafkaColumnHandle("", 1, "row2", VarcharType.VARCHAR, "a_string", DEFAULT_FIELD_DECODER_NAME, null, false, false, false); + + KafkaColumnHandle row3 = new KafkaColumnHandle("", 2, "row3", BigintType.BIGINT, "a_number", RFC2822JsonKafkaFieldDecoder.NAME, null, false, false, false); + KafkaColumnHandle row4 = new KafkaColumnHandle("", 3, "row4", BigintType.BIGINT, "a_string", RFC2822JsonKafkaFieldDecoder.NAME, null, false, false, false); + + KafkaColumnHandle row5 = new KafkaColumnHandle("", 4, "row5", VarcharType.VARCHAR, "a_number", RFC2822JsonKafkaFieldDecoder.NAME, null, false, false, false); + KafkaColumnHandle row6 = new KafkaColumnHandle("", 5, "row6", VarcharType.VARCHAR, "a_string", RFC2822JsonKafkaFieldDecoder.NAME, null, false, false, false); + + List columns = ImmutableList.of(row1, row2, row3, row4, row5, row6); + Set providers = new HashSet<>(); + + boolean corrupt = rowDecoder.decodeRow(json, providers, columns, map(columns)); + assertFalse(corrupt); + + assertEquals(providers.size(), columns.size()); + + // sanity checks + checkValue(providers, row1, now); + checkValue(providers, row2, nowString); + + // number parsed as number --> as is + checkValue(providers, row3, now); + // string parsed as number --> parse text, convert to timestamp + checkValue(providers, row4, now); + + // number parsed as string --> parse text, convert to timestamp, turn into string + checkValue(providers, row5, Long.toString(now)); + + // string parsed as string --> as is + checkValue(providers, row6, nowString); + } + + @Test + public void testNullValues() + throws Exception + { + byte[] json = "{}".getBytes(StandardCharsets.UTF_8); + + JsonKafkaRowDecoder rowDecoder = new JsonKafkaRowDecoder(PROVIDER.get()); + KafkaColumnHandle row1 = new KafkaColumnHandle("", 0, "row1", BigintType.BIGINT, "a_number", DEFAULT_FIELD_DECODER_NAME, null, false, false, false); + KafkaColumnHandle row2 = new KafkaColumnHandle("", 1, "row2", VarcharType.VARCHAR, "a_string", DEFAULT_FIELD_DECODER_NAME, null, false, false, false); + + KafkaColumnHandle row3 = new KafkaColumnHandle("", 2, "row3", BigintType.BIGINT, "a_number", RFC2822JsonKafkaFieldDecoder.NAME, null, false, false, false); + KafkaColumnHandle row4 = new KafkaColumnHandle("", 3, "row4", BigintType.BIGINT, "a_string", RFC2822JsonKafkaFieldDecoder.NAME, null, false, false, false); + + KafkaColumnHandle row5 = new KafkaColumnHandle("", 4, "row5", VarcharType.VARCHAR, "a_number", RFC2822JsonKafkaFieldDecoder.NAME, null, false, false, false); + KafkaColumnHandle row6 = new KafkaColumnHandle("", 5, "row6", VarcharType.VARCHAR, "a_string", RFC2822JsonKafkaFieldDecoder.NAME, null, false, false, false); + + List columns = ImmutableList.of(row1, row2, row3, row4, row5, row6); + Set providers = new HashSet<>(); + + boolean corrupt = rowDecoder.decodeRow(json, providers, columns, map(columns)); + assertFalse(corrupt); + + assertEquals(providers.size(), columns.size()); + + // sanity checks + checkIsNull(providers, row1); + checkIsNull(providers, row2); + checkIsNull(providers, row3); + checkIsNull(providers, row4); + checkIsNull(providers, row5); + checkIsNull(providers, row6); + } +} diff --git a/presto-kafka/src/test/java/com/facebook/presto/kafka/decoder/json/TestSecondsSinceEpochJsonKafkaFieldDecoder.java b/presto-kafka/src/test/java/com/facebook/presto/kafka/decoder/json/TestSecondsSinceEpochJsonKafkaFieldDecoder.java new file mode 100644 index 00000000..38d60618 --- /dev/null +++ b/presto-kafka/src/test/java/com/facebook/presto/kafka/decoder/json/TestSecondsSinceEpochJsonKafkaFieldDecoder.java @@ -0,0 +1,131 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka.decoder.json; + +import com.facebook.presto.kafka.KafkaColumnHandle; +import com.facebook.presto.kafka.KafkaFieldValueProvider; +import com.facebook.presto.kafka.decoder.KafkaFieldDecoder; +import com.facebook.presto.spi.type.BigintType; +import com.facebook.presto.spi.type.VarcharType; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.airlift.json.ObjectMapperProvider; +import org.testng.annotations.Test; + +import java.nio.charset.StandardCharsets; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.facebook.presto.kafka.decoder.KafkaFieldDecoder.DEFAULT_FIELD_DECODER_NAME; +import static com.facebook.presto.kafka.decoder.json.SecondsSinceEpochJsonKafkaFieldDecoder.FORMATTER; +import static com.facebook.presto.kafka.decoder.util.DecoderTestUtil.checkIsNull; +import static com.facebook.presto.kafka.decoder.util.DecoderTestUtil.checkValue; +import static java.lang.String.format; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; + +public class TestSecondsSinceEpochJsonKafkaFieldDecoder +{ + private static final Map DECODERS = ImmutableMap.of(DEFAULT_FIELD_DECODER_NAME, new JsonKafkaFieldDecoder(), + SecondsSinceEpochJsonKafkaFieldDecoder.NAME, new SecondsSinceEpochJsonKafkaFieldDecoder()); + + private static final ObjectMapperProvider PROVIDER = new ObjectMapperProvider(); + + private static Map> buildMap(List columns) + { + ImmutableMap.Builder> map = ImmutableMap.builder(); + for (KafkaColumnHandle column : columns) { + map.put(column, DECODERS.get(column.getDataFormat())); + } + return map.build(); + } + + @Test + public void testBasicFormatting() + throws Exception + { + long now = System.currentTimeMillis() / 1000; // SecondsSinceEpoch is second granularity + String nowString = FORMATTER.print(now * 1000); + + byte[] json = format("{\"a_number\":%d,\"a_string\":\"%d\"}", now, now).getBytes(StandardCharsets.UTF_8); + + JsonKafkaRowDecoder rowDecoder = new JsonKafkaRowDecoder(PROVIDER.get()); + KafkaColumnHandle row1 = new KafkaColumnHandle("", 0, "row1", BigintType.BIGINT, "a_number", DEFAULT_FIELD_DECODER_NAME, null, false, false, false); + KafkaColumnHandle row2 = new KafkaColumnHandle("", 1, "row2", VarcharType.VARCHAR, "a_string", DEFAULT_FIELD_DECODER_NAME, null, false, false, false); + + KafkaColumnHandle row3 = new KafkaColumnHandle("", 2, "row3", BigintType.BIGINT, "a_number", SecondsSinceEpochJsonKafkaFieldDecoder.NAME, null, false, false, false); + KafkaColumnHandle row4 = new KafkaColumnHandle("", 3, "row4", BigintType.BIGINT, "a_string", SecondsSinceEpochJsonKafkaFieldDecoder.NAME, null, false, false, false); + + KafkaColumnHandle row5 = new KafkaColumnHandle("", 4, "row5", VarcharType.VARCHAR, "a_number", SecondsSinceEpochJsonKafkaFieldDecoder.NAME, null, false, false, false); + KafkaColumnHandle row6 = new KafkaColumnHandle("", 5, "row6", VarcharType.VARCHAR, "a_string", SecondsSinceEpochJsonKafkaFieldDecoder.NAME, null, false, false, false); + + List columns = ImmutableList.of(row1, row2, row3, row4, row5, row6); + Set providers = new HashSet<>(); + + boolean corrupt = rowDecoder.decodeRow(json, providers, columns, buildMap(columns)); + assertFalse(corrupt); + + assertEquals(providers.size(), columns.size()); + + // sanity checks + checkValue(providers, row1, now); + checkValue(providers, row2, Long.toString(now)); + + // number parsed as number --> return as time stamp (millis) + checkValue(providers, row3, now * 1000); + // string parsed as number --> parse text, convert to timestamp + checkValue(providers, row4, now * 1000); + + // number parsed as string --> parse text, convert to timestamp, turn into string + checkValue(providers, row5, nowString); + + // string parsed as string --> parse text, convert to timestamp, turn into string + checkValue(providers, row6, nowString); + } + + @Test + public void testNullValues() + throws Exception + { + byte[] json = "{}".getBytes(StandardCharsets.UTF_8); + + JsonKafkaRowDecoder rowDecoder = new JsonKafkaRowDecoder(PROVIDER.get()); + KafkaColumnHandle row1 = new KafkaColumnHandle("", 0, "row1", BigintType.BIGINT, "a_number", DEFAULT_FIELD_DECODER_NAME, null, false, false, false); + KafkaColumnHandle row2 = new KafkaColumnHandle("", 1, "row2", VarcharType.VARCHAR, "a_string", DEFAULT_FIELD_DECODER_NAME, null, false, false, false); + + KafkaColumnHandle row3 = new KafkaColumnHandle("", 2, "row3", BigintType.BIGINT, "a_number", SecondsSinceEpochJsonKafkaFieldDecoder.NAME, null, false, false, false); + KafkaColumnHandle row4 = new KafkaColumnHandle("", 3, "row4", BigintType.BIGINT, "a_string", SecondsSinceEpochJsonKafkaFieldDecoder.NAME, null, false, false, false); + + KafkaColumnHandle row5 = new KafkaColumnHandle("", 4, "row5", VarcharType.VARCHAR, "a_number", SecondsSinceEpochJsonKafkaFieldDecoder.NAME, null, false, false, false); + KafkaColumnHandle row6 = new KafkaColumnHandle("", 5, "row6", VarcharType.VARCHAR, "a_string", SecondsSinceEpochJsonKafkaFieldDecoder.NAME, null, false, false, false); + + List columns = ImmutableList.of(row1, row2, row3, row4, row5, row6); + Set providers = new HashSet<>(); + + boolean corrupt = rowDecoder.decodeRow(json, providers, columns, buildMap(columns)); + assertFalse(corrupt); + + assertEquals(providers.size(), columns.size()); + + // sanity checks + checkIsNull(providers, row1); + checkIsNull(providers, row2); + checkIsNull(providers, row3); + checkIsNull(providers, row4); + checkIsNull(providers, row5); + checkIsNull(providers, row6); + } +} diff --git a/presto-kafka/src/test/java/com/facebook/presto/kafka/decoder/raw/TestRawDecoder.java b/presto-kafka/src/test/java/com/facebook/presto/kafka/decoder/raw/TestRawDecoder.java new file mode 100644 index 00000000..3212b52b --- /dev/null +++ b/presto-kafka/src/test/java/com/facebook/presto/kafka/decoder/raw/TestRawDecoder.java @@ -0,0 +1,232 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka.decoder.raw; + +import com.facebook.presto.kafka.KafkaColumnHandle; +import com.facebook.presto.kafka.KafkaFieldValueProvider; +import com.facebook.presto.kafka.decoder.KafkaFieldDecoder; +import com.facebook.presto.spi.type.BigintType; +import com.facebook.presto.spi.type.BooleanType; +import com.facebook.presto.spi.type.VarcharType; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import org.testng.annotations.Test; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.facebook.presto.kafka.decoder.util.DecoderTestUtil.checkValue; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; + +public class TestRawDecoder +{ + private static final RawKafkaFieldDecoder DEFAULT_FIELD_DECODER = new RawKafkaFieldDecoder(); + + private static Map> buildMap(List columns) + { + ImmutableMap.Builder> map = ImmutableMap.builder(); + for (KafkaColumnHandle column : columns) { + map.put(column, DEFAULT_FIELD_DECODER); + } + return map.build(); + } + + @Test + public void testSimple() + { + ByteBuffer buf = ByteBuffer.allocate(100); + buf.putLong(4815162342L); // 0 - 7 + buf.putInt(12345678); // 8 - 11 + buf.putShort((short) 4567); // 12 - 13 + buf.put((byte) 123); // 14 + buf.put("Ich bin zwei Oeltanks".getBytes(StandardCharsets.UTF_8)); // 15+ + + byte[] row = new byte[buf.position()]; + System.arraycopy(buf.array(), 0, row, 0, buf.position()); + + RawKafkaRowDecoder rowDecoder = new RawKafkaRowDecoder(); + KafkaColumnHandle row1 = new KafkaColumnHandle("", 0, "row1", BigintType.BIGINT, "0", "LONG", null, false, false, false); + KafkaColumnHandle row2 = new KafkaColumnHandle("", 1, "row2", BigintType.BIGINT, "8", "INT", null, false, false, false); + KafkaColumnHandle row3 = new KafkaColumnHandle("", 2, "row3", BigintType.BIGINT, "12", "SHORT", null, false, false, false); + KafkaColumnHandle row4 = new KafkaColumnHandle("", 3, "row4", BigintType.BIGINT, "14", "BYTE", null, false, false, false); + KafkaColumnHandle row5 = new KafkaColumnHandle("", 4, "row5", VarcharType.VARCHAR, "15", null, null, false, false, false); + + List columns = ImmutableList.of(row1, row2, row3, row4, row5); + Set providers = new HashSet<>(); + + boolean corrupt = rowDecoder.decodeRow(row, providers, columns, buildMap(columns)); + assertFalse(corrupt); + + assertEquals(providers.size(), columns.size()); + + checkValue(providers, row1, 4815162342L); + checkValue(providers, row2, 12345678); + checkValue(providers, row3, 4567); + checkValue(providers, row4, 123); + checkValue(providers, row5, "Ich bin zwei Oeltanks"); + } + + @Test + public void testFixedWithString() + { + String str = "Ich bin zwei Oeltanks"; + byte[] row = str.getBytes(StandardCharsets.UTF_8); + + RawKafkaRowDecoder rowDecoder = new RawKafkaRowDecoder(); + KafkaColumnHandle row1 = new KafkaColumnHandle("", 0, "row1", VarcharType.VARCHAR, null, null, null, false, false, false); + KafkaColumnHandle row2 = new KafkaColumnHandle("", 1, "row2", VarcharType.VARCHAR, "0", null, null, false, false, false); + KafkaColumnHandle row3 = new KafkaColumnHandle("", 2, "row3", VarcharType.VARCHAR, "0:4", null, null, false, false, false); + KafkaColumnHandle row4 = new KafkaColumnHandle("", 3, "row4", VarcharType.VARCHAR, "5:8", null, null, false, false, false); + + List columns = ImmutableList.of(row1, row2, row3, row4); + Set providers = new HashSet<>(); + + boolean corrupt = rowDecoder.decodeRow(row, providers, columns, buildMap(columns)); + assertFalse(corrupt); + + assertEquals(providers.size(), columns.size()); + + checkValue(providers, row1, str); + checkValue(providers, row2, str); + // these only work for single byte encodings... + checkValue(providers, row3, str.substring(0, 4)); + checkValue(providers, row4, str.substring(5, 8)); + } + + @SuppressWarnings("NumericCastThatLosesPrecision") + @Test + public void testFloatStuff() + { + ByteBuffer buf = ByteBuffer.allocate(100); + buf.putDouble(Math.PI); + buf.putFloat((float) Math.E); + buf.putDouble(Math.E); + + byte[] row = new byte[buf.position()]; + System.arraycopy(buf.array(), 0, row, 0, buf.position()); + + RawKafkaRowDecoder rowDecoder = new RawKafkaRowDecoder(); + KafkaColumnHandle row1 = new KafkaColumnHandle("", 0, "row1", VarcharType.VARCHAR, null, "DOUBLE", null, false, false, false); + KafkaColumnHandle row2 = new KafkaColumnHandle("", 1, "row2", VarcharType.VARCHAR, "8", "FLOAT", null, false, false, false); + + List columns = ImmutableList.of(row1, row2); + Set providers = new HashSet<>(); + + boolean corrupt = rowDecoder.decodeRow(row, providers, columns, buildMap(columns)); + assertFalse(corrupt); + + assertEquals(providers.size(), columns.size()); + + checkValue(providers, row1, Math.PI); + checkValue(providers, row2, Math.E); + } + + @Test + public void testBooleanStuff() + { + ByteBuffer buf = ByteBuffer.allocate(100); + buf.put((byte) 127); // offset 0 + buf.putLong(0); // offset 1 + buf.put((byte) 126); // offset 9 + buf.putLong(1); // offset 10 + + buf.put((byte) 125); // offset 18 + buf.putInt(0); // offset 19 + buf.put((byte) 124); // offset 23 + buf.putInt(1); // offset 24 + + buf.put((byte) 123); // offset 28 + buf.putShort((short) 0); // offset 29 + buf.put((byte) 122); // offset 31 + buf.putShort((short) 1); // offset 32 + + buf.put((byte) 121); // offset 34 + buf.put((byte) 0); // offset 35 + buf.put((byte) 120); // offset 36 + buf.put((byte) 1); // offset 37 + + byte[] row = new byte[buf.position()]; + System.arraycopy(buf.array(), 0, row, 0, buf.position()); + + RawKafkaRowDecoder rowDecoder = new RawKafkaRowDecoder(); + KafkaColumnHandle row01 = new KafkaColumnHandle("", 0, "row01", BigintType.BIGINT, "0", "BYTE", null, false, false, false); + KafkaColumnHandle row02 = new KafkaColumnHandle("", 1, "row02", BooleanType.BOOLEAN, "1", "LONG", null, false, false, false); + KafkaColumnHandle row03 = new KafkaColumnHandle("", 2, "row03", BigintType.BIGINT, "9", "BYTE", null, false, false, false); + KafkaColumnHandle row04 = new KafkaColumnHandle("", 3, "row04", BooleanType.BOOLEAN, "10", "LONG", null, false, false, false); + + KafkaColumnHandle row11 = new KafkaColumnHandle("", 4, "row11", BigintType.BIGINT, "18", "BYTE", null, false, false, false); + KafkaColumnHandle row12 = new KafkaColumnHandle("", 5, "row12", BooleanType.BOOLEAN, "19", "INT", null, false, false, false); + KafkaColumnHandle row13 = new KafkaColumnHandle("", 6, "row13", BigintType.BIGINT, "23", "BYTE", null, false, false, false); + KafkaColumnHandle row14 = new KafkaColumnHandle("", 7, "row14", BooleanType.BOOLEAN, "24", "INT", null, false, false, false); + + KafkaColumnHandle row21 = new KafkaColumnHandle("", 8, "row21", BigintType.BIGINT, "28", "BYTE", null, false, false, false); + KafkaColumnHandle row22 = new KafkaColumnHandle("", 9, "row22", BooleanType.BOOLEAN, "29", "SHORT", null, false, false, false); + KafkaColumnHandle row23 = new KafkaColumnHandle("", 10, "row23", BigintType.BIGINT, "31", "BYTE", null, false, false, false); + KafkaColumnHandle row24 = new KafkaColumnHandle("", 11, "row24", BooleanType.BOOLEAN, "32", "SHORT", null, false, false, false); + + KafkaColumnHandle row31 = new KafkaColumnHandle("", 12, "row31", BigintType.BIGINT, "34", "BYTE", null, false, false, false); + KafkaColumnHandle row32 = new KafkaColumnHandle("", 13, "row32", BooleanType.BOOLEAN, "35", "BYTE", null, false, false, false); + KafkaColumnHandle row33 = new KafkaColumnHandle("", 14, "row33", BigintType.BIGINT, "36", "BYTE", null, false, false, false); + KafkaColumnHandle row34 = new KafkaColumnHandle("", 15, "row34", BooleanType.BOOLEAN, "37", "BYTE", null, false, false, false); + + List columns = ImmutableList.of(row01, + row02, + row03, + row04, + row11, + row12, + row13, + row14, + row21, + row22, + row23, + row24, + row31, + row32, + row33, + row34); + + Set providers = new HashSet<>(); + + boolean corrupt = rowDecoder.decodeRow(row, providers, columns, buildMap(columns)); + assertFalse(corrupt); + + assertEquals(providers.size(), columns.size()); + + checkValue(providers, row01, 127); + checkValue(providers, row02, false); + checkValue(providers, row03, 126); + checkValue(providers, row04, true); + + checkValue(providers, row11, 125); + checkValue(providers, row12, false); + checkValue(providers, row13, 124); + checkValue(providers, row14, true); + + checkValue(providers, row21, 123); + checkValue(providers, row22, false); + checkValue(providers, row23, 122); + checkValue(providers, row24, true); + + checkValue(providers, row31, 121); + checkValue(providers, row32, false); + checkValue(providers, row33, 120); + checkValue(providers, row34, true); + } +} diff --git a/presto-kafka/src/test/java/com/facebook/presto/kafka/decoder/util/DecoderTestUtil.java b/presto-kafka/src/test/java/com/facebook/presto/kafka/decoder/util/DecoderTestUtil.java new file mode 100644 index 00000000..181e5cd0 --- /dev/null +++ b/presto-kafka/src/test/java/com/facebook/presto/kafka/decoder/util/DecoderTestUtil.java @@ -0,0 +1,74 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka.decoder.util; + +import com.facebook.presto.kafka.KafkaColumnHandle; +import com.facebook.presto.kafka.KafkaFieldValueProvider; + +import java.nio.charset.StandardCharsets; +import java.util.Set; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +public final class DecoderTestUtil +{ + private DecoderTestUtil() {} + + private static KafkaFieldValueProvider findValueProvider(Set providers, KafkaColumnHandle handle) + { + for (KafkaFieldValueProvider provider : providers) { + if (provider.accept(handle)) { + return provider; + } + } + return null; + } + + public static void checkValue(Set providers, KafkaColumnHandle handle, String value) + { + KafkaFieldValueProvider provider = findValueProvider(providers, handle); + assertNotNull(provider); + assertEquals(new String(provider.getSlice().getBytes(), StandardCharsets.UTF_8), value); + } + + public static void checkValue(Set providers, KafkaColumnHandle handle, long value) + { + KafkaFieldValueProvider provider = findValueProvider(providers, handle); + assertNotNull(provider); + assertEquals(provider.getLong(), value); + } + + public static void checkValue(Set providers, KafkaColumnHandle handle, double value) + { + KafkaFieldValueProvider provider = findValueProvider(providers, handle); + assertNotNull(provider); + assertEquals(provider.getDouble(), value, 0.0001); + } + + public static void checkValue(Set providers, KafkaColumnHandle handle, boolean value) + { + KafkaFieldValueProvider provider = findValueProvider(providers, handle); + assertNotNull(provider); + assertEquals(provider.getBoolean(), value); + } + + public static void checkIsNull(Set providers, KafkaColumnHandle handle) + { + KafkaFieldValueProvider provider = findValueProvider(providers, handle); + assertNotNull(provider); + assertTrue(provider.isNull()); + } +} diff --git a/presto-kafka/src/test/java/com/facebook/presto/kafka/util/CodecSupplier.java b/presto-kafka/src/test/java/com/facebook/presto/kafka/util/CodecSupplier.java new file mode 100644 index 00000000..b0178035 --- /dev/null +++ b/presto-kafka/src/test/java/com/facebook/presto/kafka/util/CodecSupplier.java @@ -0,0 +1,71 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka.util; + +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.spi.type.Type; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.deser.std.FromStringDeserializer; +import com.google.common.base.Supplier; +import com.google.common.collect.ImmutableMap; +import io.airlift.json.JsonCodec; +import io.airlift.json.JsonCodecFactory; +import io.airlift.json.ObjectMapperProvider; + +import static com.facebook.presto.spi.type.TypeSignature.parseTypeSignature; + +public final class CodecSupplier + implements Supplier> +{ + private final Metadata metadata; + private final JsonCodecFactory codecFactory; + private final Class clazz; + + public CodecSupplier(Class clazz, Metadata metadata) + { + this.clazz = clazz; + this.metadata = metadata; + ObjectMapperProvider objectMapperProvider = new ObjectMapperProvider(); + objectMapperProvider.setJsonDeserializers(ImmutableMap., JsonDeserializer>of(Type.class, new TypeDeserializer())); + this.codecFactory = new JsonCodecFactory(objectMapperProvider); + } + + @Override + public JsonCodec get() + { + return codecFactory.jsonCodec(clazz); + } + + private class TypeDeserializer + extends FromStringDeserializer + { + private static final long serialVersionUID = 1L; + + public TypeDeserializer() + { + super(Type.class); + } + + @Override + protected Type _deserialize(String value, DeserializationContext context) + { + Type type = metadata.getType(parseTypeSignature(value)); + if (type == null) { + throw new IllegalArgumentException(String.valueOf("Unknown type " + value)); + } + return type; + } + } +} diff --git a/presto-kafka/src/test/java/com/facebook/presto/kafka/util/EmbeddedKafka.java b/presto-kafka/src/test/java/com/facebook/presto/kafka/util/EmbeddedKafka.java new file mode 100644 index 00000000..dec5d667 --- /dev/null +++ b/presto-kafka/src/test/java/com/facebook/presto/kafka/util/EmbeddedKafka.java @@ -0,0 +1,175 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka.util; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import com.google.common.io.Files; +import kafka.admin.AdminUtils; +import kafka.javaapi.producer.Producer; +import kafka.producer.ProducerConfig; +import kafka.server.KafkaConfig; +import kafka.server.KafkaServerStartable; +import kafka.utils.ZKStringSerializer$; +import org.I0Itec.zkclient.ZkClient; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicBoolean; + +import static com.facebook.presto.kafka.util.TestUtils.findUnusedPort; +import static com.facebook.presto.kafka.util.TestUtils.toProperties; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static io.airlift.testing.FileUtils.deleteRecursively; + +public class EmbeddedKafka + implements Closeable +{ + private final EmbeddedZookeeper zookeeper; + private final int port; + private final File kafkaDataDir; + private final KafkaServerStartable kafka; + + private final AtomicBoolean started = new AtomicBoolean(); + private final AtomicBoolean stopped = new AtomicBoolean(); + + public static EmbeddedKafka createEmbeddedKafka() + throws IOException + { + return new EmbeddedKafka(new EmbeddedZookeeper(), new Properties()); + } + + public static EmbeddedKafka createEmbeddedKafka(Properties overrideProperties) + throws IOException + { + return new EmbeddedKafka(new EmbeddedZookeeper(), overrideProperties); + } + + EmbeddedKafka(EmbeddedZookeeper zookeeper, Properties overrideProperties) + throws IOException + { + this.zookeeper = checkNotNull(zookeeper, "zookeeper is null"); + checkNotNull(overrideProperties, "overrideProperties is null"); + + this.port = findUnusedPort(); + this.kafkaDataDir = Files.createTempDir(); + + Map properties = ImmutableMap.builder() + .put("broker.id", "0") + .put("host.name", "localhost") + .put("num.partitions", "2") + .put("log.flush.interval.messages", "10000") + .put("log.flush.interval.ms", "1000") + .put("log.retention.minutes", "60") + .put("log.segment.bytes", "1048576") + .put("auto.create.topics.enable", "false") + .put("zookeeper.connection.timeout.ms", "1000000") + .put("port", Integer.toString(port)) + .put("log.dirs", kafkaDataDir.getAbsolutePath()) + .put("zookeeper.connect", zookeeper.getConnectString()) + .putAll(Maps.fromProperties(overrideProperties)) + .build(); + + KafkaConfig config = new KafkaConfig(toProperties(properties)); + this.kafka = new KafkaServerStartable(config); + } + + public void start() + throws InterruptedException, IOException + { + if (!started.getAndSet(true)) { + zookeeper.start(); + kafka.startup(); + } + } + + @Override + public void close() + { + if (started.get() && !stopped.getAndSet(true)) { + kafka.shutdown(); + kafka.awaitShutdown(); + zookeeper.close(); + deleteRecursively(kafkaDataDir); + } + } + + public void createTopics(String... topics) + { + createTopics(2, 1, new Properties(), topics); + } + + public void createTopics(int partitions, int replication, Properties topicProperties, String... topics) + { + checkState(started.get() && !stopped.get(), "not started!"); + + ZkClient zkClient = new ZkClient(getZookeeperConnectString(), 30_000, 30_000, ZKStringSerializer$.MODULE$); + try { + for (String topic : topics) { + AdminUtils.createTopic(zkClient, topic, partitions, replication, topicProperties); + } + } + finally { + zkClient.close(); + } + } + + public CloseableProducer createProducer() + { + Map properties = ImmutableMap.builder() + .put("metadata.broker.list", getConnectString()) + .put("serializer.class", JsonEncoder.class.getName()) + .put("key.serializer.class", NumberEncoder.class.getName()) + .put("partitioner.class", NumberPartitioner.class.getName()) + .put("request.required.acks", "1") + .build(); + + ProducerConfig producerConfig = new ProducerConfig(toProperties(properties)); + return new CloseableProducer<>(producerConfig); + } + + public static class CloseableProducer + extends Producer + implements AutoCloseable + { + public CloseableProducer(ProducerConfig config) + { + super(config); + } + } + + public int getZookeeperPort() + { + return zookeeper.getPort(); + } + + public int getPort() + { + return port; + } + + public String getConnectString() + { + return "localhost:" + Integer.toString(port); + } + + public String getZookeeperConnectString() + { + return zookeeper.getConnectString(); + } +} diff --git a/presto-kafka/src/test/java/com/facebook/presto/kafka/util/EmbeddedZookeeper.java b/presto-kafka/src/test/java/com/facebook/presto/kafka/util/EmbeddedZookeeper.java new file mode 100644 index 00000000..fddf5c84 --- /dev/null +++ b/presto-kafka/src/test/java/com/facebook/presto/kafka/util/EmbeddedZookeeper.java @@ -0,0 +1,96 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka.util; + +import com.google.common.io.Files; +import org.apache.zookeeper.server.NIOServerCnxn; +import org.apache.zookeeper.server.ZooKeeperServer; +import org.apache.zookeeper.server.persistence.FileTxnSnapLog; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.concurrent.atomic.AtomicBoolean; + +import static io.airlift.testing.FileUtils.deleteRecursively; + +public class EmbeddedZookeeper + implements Closeable +{ + private final int port; + private final File zkDataDir; + private final ZooKeeperServer zkServer; + private final NIOServerCnxn.Factory cnxnFactory; + + private final AtomicBoolean started = new AtomicBoolean(); + private final AtomicBoolean stopped = new AtomicBoolean(); + + public EmbeddedZookeeper() + throws IOException + { + this(TestUtils.findUnusedPort()); + } + + public EmbeddedZookeeper(int port) + throws IOException + { + this.port = port; + zkDataDir = Files.createTempDir(); + zkServer = new ZooKeeperServer(); + + FileTxnSnapLog ftxn = new FileTxnSnapLog(zkDataDir, zkDataDir); + zkServer.setTxnLogFactory(ftxn); + + cnxnFactory = new NIOServerCnxn.Factory(new InetSocketAddress(this.port), 0); + } + + public void start() + throws InterruptedException, IOException + { + if (!started.getAndSet(true)) { + cnxnFactory.startup(zkServer); + } + } + + @Override + public void close() + { + if (started.get() && !stopped.getAndSet(true)) { + cnxnFactory.shutdown(); + try { + cnxnFactory.join(); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + if (zkServer.isRunning()) { + zkServer.shutdown(); + } + + deleteRecursively(zkDataDir); + } + } + + public String getConnectString() + { + return "127.0.0.1:" + Integer.toString(port); + } + + public int getPort() + { + return port; + } +} diff --git a/presto-kafka/src/test/java/com/facebook/presto/kafka/util/JsonEncoder.java b/presto-kafka/src/test/java/com/facebook/presto/kafka/util/JsonEncoder.java new file mode 100644 index 00000000..292b184d --- /dev/null +++ b/presto-kafka/src/test/java/com/facebook/presto/kafka/util/JsonEncoder.java @@ -0,0 +1,44 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka.util; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Throwables; +import kafka.serializer.Encoder; +import kafka.utils.VerifiableProperties; + +import java.io.IOException; + +public class JsonEncoder + implements Encoder +{ + private final ObjectMapper objectMapper = new ObjectMapper(); + + @SuppressWarnings("UnusedParameters") + public JsonEncoder(VerifiableProperties properties) + { + // constructor required by Kafka + } + + @Override + public byte[] toBytes(Object o) + { + try { + return objectMapper.writeValueAsBytes(o); + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } +} diff --git a/presto-kafka/src/test/java/com/facebook/presto/kafka/util/KafkaLoader.java b/presto-kafka/src/test/java/com/facebook/presto/kafka/util/KafkaLoader.java new file mode 100644 index 00000000..f08bc3f8 --- /dev/null +++ b/presto-kafka/src/test/java/com/facebook/presto/kafka/util/KafkaLoader.java @@ -0,0 +1,152 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka.util; + +import com.facebook.presto.Session; +import com.facebook.presto.client.Column; +import com.facebook.presto.client.QueryResults; +import com.facebook.presto.server.testing.TestingPrestoServer; +import com.facebook.presto.spi.type.TimeZoneKey; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.tests.AbstractTestingPrestoClient; +import com.facebook.presto.tests.ResultsSession; +import com.google.common.collect.ImmutableMap; +import kafka.javaapi.producer.Producer; +import kafka.producer.KeyedMessage; +import org.joda.time.format.DateTimeFormatter; +import org.joda.time.format.ISODateTimeFormat; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.BooleanType.BOOLEAN; +import static com.facebook.presto.spi.type.DateTimeEncoding.unpackMillisUtc; +import static com.facebook.presto.spi.type.DateType.DATE; +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; +import static com.facebook.presto.spi.type.TimeType.TIME; +import static com.facebook.presto.spi.type.TimeWithTimeZoneType.TIME_WITH_TIME_ZONE; +import static com.facebook.presto.spi.type.TimestampType.TIMESTAMP; +import static com.facebook.presto.spi.type.TimestampWithTimeZoneType.TIMESTAMP_WITH_TIME_ZONE; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static com.facebook.presto.util.DateTimeUtils.parseTime; +import static com.facebook.presto.util.DateTimeUtils.parseTimestamp; +import static com.facebook.presto.util.DateTimeUtils.parseTimestampWithTimeZone; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +public class KafkaLoader + extends AbstractTestingPrestoClient +{ + private static final DateTimeFormatter ISO8601_FORMATTER = ISODateTimeFormat.dateTime(); + + private final String topicName; + private final Producer producer; + private final AtomicLong count = new AtomicLong(); + + public KafkaLoader(Producer producer, + String topicName, + TestingPrestoServer prestoServer, + Session defaultSession) + { + super(prestoServer, defaultSession); + + this.topicName = topicName; + this.producer = producer; + } + + @Override + public ResultsSession getResultSession(Session session) + { + checkNotNull(session, "session is null"); + return new KafkaLoadingSession(session); + } + + private class KafkaLoadingSession + implements ResultsSession + { + private final AtomicReference> types = new AtomicReference<>(); + + private final TimeZoneKey timeZoneKey; + + private KafkaLoadingSession(Session session) + { + this.timeZoneKey = session.getTimeZoneKey(); + } + + @Override + public void addResults(QueryResults results) + { + if (types.get() == null && results.getColumns() != null) { + types.set(getTypes(results.getColumns())); + } + + if (results.getData() != null) { + checkState(types.get() != null, "Data without types received!"); + List columns = results.getColumns(); + for (List fields : results.getData()) { + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (int i = 0; i < fields.size(); i++) { + Type type = types.get().get(i); + Object value = convertValue(fields.get(i), type); + if (value != null) { + builder.put(columns.get(i).getName(), value); + } + } + + producer.send(new KeyedMessage<>(topicName, count.getAndIncrement(), builder.build())); + } + } + } + + @Override + public Void build(Map setSessionProperties, Set resetSessionProperties) + { + return null; + } + + private Object convertValue(Object value, Type type) + { + if (value == null) { + return null; + } + + if (BOOLEAN.equals(type) || VARCHAR.equals(type)) { + return value; + } + if (BIGINT.equals(type)) { + return ((Number) value).longValue(); + } + if (DOUBLE.equals(type)) { + return ((Number) value).doubleValue(); + } + if (DATE.equals(type)) { + return value; + } + if (TIME.equals(type)) { + return ISO8601_FORMATTER.print(parseTime(timeZoneKey, (String) value)); + } + if (TIMESTAMP.equals(type)) { + return ISO8601_FORMATTER.print(parseTimestamp(timeZoneKey, (String) value)); + } + if (TIME_WITH_TIME_ZONE.equals(type) || TIMESTAMP_WITH_TIME_ZONE.equals(type)) { + return ISO8601_FORMATTER.print(unpackMillisUtc(parseTimestampWithTimeZone((String) value))); + } + throw new AssertionError("unhandled type: " + type); + } + } +} diff --git a/presto-kafka/src/test/java/com/facebook/presto/kafka/util/NumberEncoder.java b/presto-kafka/src/test/java/com/facebook/presto/kafka/util/NumberEncoder.java new file mode 100644 index 00000000..e0708a5c --- /dev/null +++ b/presto-kafka/src/test/java/com/facebook/presto/kafka/util/NumberEncoder.java @@ -0,0 +1,37 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka.util; + +import kafka.serializer.Encoder; +import kafka.utils.VerifiableProperties; + +import java.nio.ByteBuffer; + +public class NumberEncoder + implements Encoder +{ + @SuppressWarnings("UnusedParameters") + public NumberEncoder(VerifiableProperties properties) + { + // constructor required by Kafka + } + + @Override + public byte[] toBytes(Number value) + { + ByteBuffer buf = ByteBuffer.allocate(8); + buf.putLong(value == null ? 0L : value.longValue()); + return buf.array(); + } +} diff --git a/presto-kafka/src/test/java/com/facebook/presto/kafka/util/NumberPartitioner.java b/presto-kafka/src/test/java/com/facebook/presto/kafka/util/NumberPartitioner.java new file mode 100644 index 00000000..b4899a6a --- /dev/null +++ b/presto-kafka/src/test/java/com/facebook/presto/kafka/util/NumberPartitioner.java @@ -0,0 +1,37 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka.util; + +import com.google.common.primitives.Ints; +import kafka.producer.Partitioner; +import kafka.utils.VerifiableProperties; + +public class NumberPartitioner + implements Partitioner +{ + @SuppressWarnings("UnusedParameters") + public NumberPartitioner(VerifiableProperties properties) + { + // constructor required by Kafka + } + + @Override + public int partition(Object key, int numPartitions) + { + if (key instanceof Number) { + return Ints.checkedCast(((Number) key).longValue() % numPartitions); + } + return 0; + } +} diff --git a/presto-kafka/src/test/java/com/facebook/presto/kafka/util/TestUtils.java b/presto-kafka/src/test/java/com/facebook/presto/kafka/util/TestUtils.java new file mode 100644 index 00000000..a37fd9da --- /dev/null +++ b/presto-kafka/src/test/java/com/facebook/presto/kafka/util/TestUtils.java @@ -0,0 +1,96 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.kafka.util; + +import com.facebook.presto.kafka.KafkaPlugin; +import com.facebook.presto.kafka.KafkaTopicDescription; +import com.facebook.presto.metadata.QualifiedTableName; +import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.testing.QueryRunner; +import com.facebook.presto.tests.TestingPrestoClient; +import com.google.common.base.Joiner; +import com.google.common.base.Suppliers; +import com.google.common.collect.ImmutableMap; +import com.google.common.io.ByteStreams; +import io.airlift.json.JsonCodec; + +import java.io.IOException; +import java.net.ServerSocket; +import java.util.AbstractMap; +import java.util.Map; +import java.util.Properties; + +import static com.facebook.presto.kafka.util.EmbeddedKafka.CloseableProducer; +import static java.lang.String.format; + +public final class TestUtils +{ + private TestUtils() {} + + public static int findUnusedPort() + throws IOException + { + try (ServerSocket socket = new ServerSocket(0)) { + return socket.getLocalPort(); + } + } + + public static Properties toProperties(Map map) + { + Properties properties = new Properties(); + for (Map.Entry entry : map.entrySet()) { + properties.setProperty(entry.getKey(), entry.getValue()); + } + return properties; + } + + public static void installKafkaPlugin(EmbeddedKafka embeddedKafka, QueryRunner queryRunner, Map topicDescriptions) + { + KafkaPlugin kafkaPlugin = new KafkaPlugin(); + kafkaPlugin.setTableDescriptionSupplier(Suppliers.ofInstance(topicDescriptions)); + queryRunner.installPlugin(kafkaPlugin); + + Map kafkaConfig = ImmutableMap.of( + "kafka.nodes", embeddedKafka.getConnectString(), + "kafka.table-names", Joiner.on(",").join(topicDescriptions.keySet()), + "kafka.connect-timeout", "120s", + "kafka.default-schema", "default"); + queryRunner.createCatalog("kafka", "kafka", kafkaConfig); + } + + public static void loadTpchTopic(EmbeddedKafka embeddedKafka, TestingPrestoClient prestoClient, String topicName, QualifiedTableName tpchTableName) + { + try (CloseableProducer producer = embeddedKafka.createProducer(); + KafkaLoader tpchLoader = new KafkaLoader(producer, topicName, prestoClient.getServer(), prestoClient.getDefaultSession())) { + tpchLoader.execute(format("SELECT * from %s", tpchTableName)); + } + } + + public static Map.Entry loadTpchTopicDescription(JsonCodec topicDescriptionJsonCodec, String topicName, SchemaTableName schemaTableName) + throws IOException + { + KafkaTopicDescription tpchTemplate = topicDescriptionJsonCodec.fromJson(ByteStreams.toByteArray(TestUtils.class.getResourceAsStream(format("/tpch/%s.json", schemaTableName.getTableName())))); + + return new AbstractMap.SimpleImmutableEntry<>( + schemaTableName, + new KafkaTopicDescription(schemaTableName.getTableName(), schemaTableName.getSchemaName(), topicName, tpchTemplate.getKey(), tpchTemplate.getMessage())); + } + + public static Map.Entry createEmptyTopicDescription(String topicName, SchemaTableName schemaTableName) + { + return new AbstractMap.SimpleImmutableEntry<>( + schemaTableName, + new KafkaTopicDescription(schemaTableName.getTableName(), schemaTableName.getSchemaName(), topicName, null, null)); + } +} diff --git a/presto-kafka/src/test/resources/decoder/json/message.json b/presto-kafka/src/test/resources/decoder/json/message.json new file mode 100644 index 00000000..9a074395 --- /dev/null +++ b/presto-kafka/src/test/resources/decoder/json/message.json @@ -0,0 +1,80 @@ +{ + "created_at": "Mon Jul 28 20:38:07 +0000 2014", + "id": 493857959588286460, + "id_str": "493857959588286465", + "text": "Lots of Important Preseason Football Dates on the Horizon - EKU Sports: Lots of Important Preseason Football D... http://t.co/F7iz6APFTW", + "source": "twitterfeed", + "truncated": false, + "in_reply_to_status_id": null, + "in_reply_to_status_id_str": null, + "in_reply_to_user_id": null, + "in_reply_to_user_id_str": null, + "in_reply_to_screen_name": null, + "user": { + "id": 98247748, + "id_str": "98247748", + "name": "Eastern KY News", + "screen_name": "EKentuckyNews", + "location": "Eastern Kentucky", + "url": null, + "description": "Your Eastern Kentucky News Source.", + "protected": false, + "verified": false, + "followers_count": 305, + "friends_count": 186, + "listed_count": 8, + "favourites_count": 0, + "statuses_count": 7630, + "created_at": "Mon Dec 21 01:17:22 +0000 2009", + "utc_offset": -14400, + "time_zone": "Eastern Time (US & Canada)", + "geo_enabled": true, + "lang": "en", + "contributors_enabled": false, + "is_translator": false, + "profile_background_color": "C6E2EE", + "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", + "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", + "profile_background_tile": false, + "profile_link_color": "1F98C7", + "profile_sidebar_border_color": "C6E2EE", + "profile_sidebar_fill_color": "DAECF4", + "profile_text_color": "8F433C", + "profile_use_background_image": true, + "profile_image_url": "http://pbs.twimg.com/profile_images/1297233295/Kentucky_at_Work_logo_normal.jpeg", + "profile_image_url_https": "https://pbs.twimg.com/profile_images/1297233295/Kentucky_at_Work_logo_normal.jpeg", + "default_profile": false, + "default_profile_image": false, + "following": null, + "follow_request_sent": null, + "notifications": null + }, + "geo": null, + "coordinates": null, + "place": null, + "contributors": null, + "retweet_count": 0, + "favorite_count": 0, + "entities": { + "hashtags": [], + "trends": [], + "urls": [ + { + "url": "http://t.co/F7iz6APFTW", + "expanded_url": "http://bit.ly/1rTEYRM", + "display_url": "bit.ly/1rTEYRM", + "indices": [ + 114, + 136 + ] + } + ], + "user_mentions": [], + "symbols": [] + }, + "favorited": false, + "retweeted": false, + "possibly_sensitive": false, + "filter_level": "medium", + "lang": "en" +} diff --git a/presto-kafka/src/test/resources/tpch/customer.json b/presto-kafka/src/test/resources/tpch/customer.json new file mode 100644 index 00000000..9d8225a3 --- /dev/null +++ b/presto-kafka/src/test/resources/tpch/customer.json @@ -0,0 +1,61 @@ +{ + "tableName": "customer", + "schemaName": "tpch", + "topicName": "tpch.customer", + "key": { + "dataFormat": "raw", + "fields": [ + { + "name": "kafka_key", + "dataFormat": "LONG", + "type": "BIGINT", + "hidden": "true" + } + ] + }, + "message": { + "dataFormat": "json", + "fields": [ + { + "name": "custkey", + "mapping": "custkey", + "type": "BIGINT" + }, + { + "name": "name", + "mapping": "name", + "type": "VARCHAR" + }, + { + "name": "address", + "mapping": "address", + "type": "VARCHAR" + }, + { + "name": "nationkey", + "mapping": "nationkey", + "type": "BIGINT" + }, + { + "name": "phone", + "mapping": "phone", + "type": "VARCHAR" + }, + { + "name": "acctbal", + "mapping": "acctbal", + "type": "DOUBLE" + }, + { + "name": "mktsegment", + "mapping": "mktsegment", + "type": "VARCHAR" + }, + { + "name": "comment", + "mapping": "comment", + "type": "VARCHAR" + } + ] + } +} diff --git a/presto-kafka/src/test/resources/tpch/lineitem.json b/presto-kafka/src/test/resources/tpch/lineitem.json new file mode 100644 index 00000000..35cc8d41 --- /dev/null +++ b/presto-kafka/src/test/resources/tpch/lineitem.json @@ -0,0 +1,104 @@ +{ + "tableName": "lineitem", + "schemaName": "tpch", + "topicName": "tpch.lineitem", + "key": { + "dataFormat": "raw", + "fields": [ + { + "name": "kafka_key", + "dataFormat": "LONG", + "type": "BIGINT", + "hidden": "true" + } + ] + }, + "message": { + "dataFormat": "json", + "fields": [ + { + "name": "orderkey", + "mapping": "orderkey", + "type": "BIGINT" + }, + { + "name": "partkey", + "mapping": "partkey", + "type": "BIGINT" + }, + { + "name": "suppkey", + "mapping": "suppkey", + "type": "BIGINT" + }, + { + "name": "linenumber", + "mapping": "linenumber", + "type": "BIGINT" + }, + { + "name": "quantity", + "mapping": "quantity", + "type": "BIGINT" + }, + { + "name": "extendedprice", + "mapping": "extendedprice", + "type": "DOUBLE" + }, + { + "name": "discount", + "mapping": "discount", + "type": "DOUBLE" + }, + { + "name": "tax", + "mapping": "tax", + "type": "DOUBLE" + }, + { + "name": "returnflag", + "mapping": "returnflag", + "type": "VARCHAR" + }, + { + "name": "linestatus", + "mapping": "linestatus", + "type": "VARCHAR" + }, + { + "name": "shipdate", + "mapping": "shipdate", + "type": "DATE", + "dataFormat": "iso8601" + }, + { + "name": "commitdate", + "mapping": "commitdate", + "type": "DATE", + "dataFormat": "iso8601" + }, + { + "name": "receiptdate", + "mapping": "receiptdate", + "type": "DATE", + "dataFormat": "iso8601" + }, + { + "name": "shipinstruct", + "mapping": "shipinstruct", + "type": "VARCHAR" + }, + { + "name": "shipmode", + "mapping": "shipmode", + "type": "VARCHAR" + }, + { + "name": "comment", + "mapping": "comment", + "type": "VARCHAR" + } + ] + } +} diff --git a/presto-kafka/src/test/resources/tpch/nation.json b/presto-kafka/src/test/resources/tpch/nation.json new file mode 100644 index 00000000..6b7a0fef --- /dev/null +++ b/presto-kafka/src/test/resources/tpch/nation.json @@ -0,0 +1,41 @@ +{ + "tableName": "nation", + "schemaName": "tpch", + "topicName": "tpch.nation", + "key": { + "dataFormat": "raw", + "fields": [ + { + "name": "kafka_key", + "dataFormat": "LONG", + "type": "BIGINT", + "hidden": "true" + } + ] + }, + "message": { + "dataFormat": "json", + "fields": [ + { + "name": "nationkey", + "mapping": "nationkey", + "type": "BIGINT" + }, + { + "name": "name", + "mapping": "name", + "type": "VARCHAR" + }, + { + "name": "regionkey", + "mapping": "regionkey", + "type": "BIGINT" + }, + { + "name": "comment", + "mapping": "comment", + "type": "VARCHAR" + } + ] + } +} diff --git a/presto-kafka/src/test/resources/tpch/orders.json b/presto-kafka/src/test/resources/tpch/orders.json new file mode 100644 index 00000000..ab6b9db3 --- /dev/null +++ b/presto-kafka/src/test/resources/tpch/orders.json @@ -0,0 +1,67 @@ +{ + "tableName": "orders", + "schemaName": "tpch", + "topicName": "tpch.orders", + "key": { + "dataFormat": "raw", + "fields": [ + { + "name": "kafka_key", + "dataFormat": "LONG", + "type": "BIGINT", + "hidden": "true" + } + ] + }, + "message": { + "dataFormat": "json", + "fields": [ + { + "name": "orderkey", + "mapping": "orderkey", + "type": "BIGINT" + }, + { + "name": "custkey", + "mapping": "custkey", + "type": "BIGINT" + }, + { + "name": "orderstatus", + "mapping": "orderstatus", + "type": "VARCHAR" + }, + { + "name": "totalprice", + "mapping": "totalprice", + "type": "DOUBLE" + }, + { + "name": "orderdate", + "mapping": "orderdate", + "type": "DATE", + "dataFormat": "iso8601" + }, + { + "name": "orderpriority", + "mapping": "orderpriority", + "type": "VARCHAR" + }, + { + "name": "clerk", + "mapping": "clerk", + "type": "VARCHAR" + }, + { + "name": "shippriority", + "mapping": "shippriority", + "type": "BIGINT" + }, + { + "name": "comment", + "mapping": "comment", + "type": "VARCHAR" + } + ] + } +} diff --git a/presto-kafka/src/test/resources/tpch/part.json b/presto-kafka/src/test/resources/tpch/part.json new file mode 100644 index 00000000..3fd25b73 --- /dev/null +++ b/presto-kafka/src/test/resources/tpch/part.json @@ -0,0 +1,66 @@ +{ + "tableName": "part", + "schemaName": "tpch", + "topicName": "tpch.part", + "key": { + "dataFormat": "raw", + "fields": [ + { + "name": "kafka_key", + "dataFormat": "LONG", + "type": "BIGINT", + "hidden": "true" + } + ] + }, + "message": { + "dataFormat": "json", + "fields": [ + { + "name": "partkey", + "mapping": "partkey", + "type": "BIGINT" + }, + { + "name": "name", + "mapping": "name", + "type": "VARCHAR" + }, + { + "name": "mfgr", + "mapping": "mfgr", + "type": "VARCHAR" + }, + { + "name": "brand", + "mapping": "brand", + "type": "VARCHAR" + }, + { + "name": "type", + "mapping": "type", + "type": "VARCHAR" + }, + { + "name": "size", + "mapping": "size", + "type": "BIGINT" + }, + { + "name": "container", + "mapping": "container", + "type": "VARCHAR" + }, + { + "name": "retailprice", + "mapping": "retailprice", + "type": "DOUBLE" + }, + { + "name": "comment", + "mapping": "comment", + "type": "VARCHAR" + } + ] + } +} diff --git a/presto-kafka/src/test/resources/tpch/partsupp.json b/presto-kafka/src/test/resources/tpch/partsupp.json new file mode 100644 index 00000000..c5f0b3df --- /dev/null +++ b/presto-kafka/src/test/resources/tpch/partsupp.json @@ -0,0 +1,46 @@ +{ + "tableName": "partsupp", + "schemaName": "tpch", + "topicName": "tpch.partsupp", + "key": { + "dataFormat": "raw", + "fields": [ + { + "name": "kafka_key", + "dataFormat": "LONG", + "type": "BIGINT", + "hidden": "true" + } + ] + }, + "message": { + "dataFormat": "json", + "fields": [ + { + "name": "partkey", + "mapping": "partkey", + "type": "BIGINT" + }, + { + "name": "suppkey", + "mapping": "suppkey", + "type": "BIGINT" + }, + { + "name": "availqty", + "mapping": "availqty", + "type": "BIGINT" + }, + { + "name": "supplycost", + "mapping": "supplycost", + "type": "DOUBLE" + }, + { + "name": "comment", + "mapping": "comment", + "type": "VARCHAR" + } + ] + } +} diff --git a/presto-kafka/src/test/resources/tpch/region.json b/presto-kafka/src/test/resources/tpch/region.json new file mode 100644 index 00000000..ca629737 --- /dev/null +++ b/presto-kafka/src/test/resources/tpch/region.json @@ -0,0 +1,36 @@ +{ + "tableName": "region", + "schemaName": "tpch", + "topicName": "tpch.region", + "key": { + "dataFormat": "raw", + "fields": [ + { + "name": "kafka_key", + "dataFormat": "LONG", + "type": "BIGINT", + "hidden": "true" + } + ] + }, + "message": { + "dataFormat": "json", + "fields": [ + { + "name": "regionkey", + "mapping": "regionkey", + "type": "BIGINT" + }, + { + "name": "name", + "mapping": "name", + "type": "VARCHAR" + }, + { + "name": "comment", + "mapping": "comment", + "type": "VARCHAR" + } + ] + } +} diff --git a/presto-kafka/src/test/resources/tpch/supplier.json b/presto-kafka/src/test/resources/tpch/supplier.json new file mode 100644 index 00000000..0957105f --- /dev/null +++ b/presto-kafka/src/test/resources/tpch/supplier.json @@ -0,0 +1,56 @@ +{ + "tableName": "supplier", + "schemaName": "tpch", + "topicName": "tpch.supplier", + "key": { + "dataFormat": "raw", + "fields": [ + { + "name": "kafka_key", + "dataFormat": "LONG", + "type": "BIGINT", + "hidden": "true" + } + ] + }, + "message": { + "dataFormat": "json", + "fields": [ + { + "name": "suppkey", + "mapping": "suppkey", + "type": "BIGINT" + }, + { + "name": "name", + "mapping": "name", + "type": "VARCHAR" + }, + { + "name": "address", + "mapping": "address", + "type": "VARCHAR" + }, + { + "name": "nationkey", + "mapping": "nationkey", + "type": "BIGINT" + }, + { + "name": "phone", + "mapping": "phone", + "type": "VARCHAR" + }, + { + "name": "acctbal", + "mapping": "acctbal", + "type": "DOUBLE" + }, + { + "name": "comment", + "mapping": "comment", + "type": "VARCHAR" + } + ] + } +} diff --git a/presto-main/etc/catalog/default.properties b/presto-main/etc/catalog/default.properties new file mode 100644 index 00000000..546d6fcc --- /dev/null +++ b/presto-main/etc/catalog/default.properties @@ -0,0 +1,6 @@ +connector.name=raptor +metadata.db.type=h2 +metadata.db.filename=var/data/db/MetaStore +#metadata.db.type=mysql +#metadata.db.connections.max=500 +storage.data-directory=var/data diff --git a/presto-main/etc/catalog/example.properties b/presto-main/etc/catalog/example.properties new file mode 100644 index 00000000..88aeedd8 --- /dev/null +++ b/presto-main/etc/catalog/example.properties @@ -0,0 +1,2 @@ +connector.name=example-http +metadata-uri=http://s3.amazonaws.com/presto-example/v2/example-metadata.json diff --git a/presto-main/etc/catalog/hive.properties b/presto-main/etc/catalog/hive.properties new file mode 100644 index 00000000..c43a6837 --- /dev/null +++ b/presto-main/etc/catalog/hive.properties @@ -0,0 +1,2 @@ +connector.name=hive-cdh4 +hive.metastore.uri=thrift://localhost:9083 diff --git a/presto-main/etc/catalog/jmx.properties b/presto-main/etc/catalog/jmx.properties new file mode 100644 index 00000000..b6e0372b --- /dev/null +++ b/presto-main/etc/catalog/jmx.properties @@ -0,0 +1 @@ +connector.name=jmx diff --git a/presto-main/etc/catalog/tpch.properties b/presto-main/etc/catalog/tpch.properties new file mode 100644 index 00000000..599f5ec6 --- /dev/null +++ b/presto-main/etc/catalog/tpch.properties @@ -0,0 +1,2 @@ +connector.name=tpch +tpch.splits-per-node=4 diff --git a/presto-main/etc/config.properties b/presto-main/etc/config.properties new file mode 100644 index 00000000..00856c4a --- /dev/null +++ b/presto-main/etc/config.properties @@ -0,0 +1,31 @@ +# sample nodeId to provide consistency across test runs +node.id=ffffffff-ffff-ffff-ffff-ffffffffffff +node.environment=test +http-server.http.port=8080 + +discovery-server.enabled=true +discovery.uri=http://localhost:8080 + +exchange.http-client.max-connections=1000 +exchange.http-client.max-connections-per-server=1000 +exchange.http-client.connect-timeout=1m +exchange.http-client.read-timeout=1m + +scheduler.http-client.max-connections=1000 +scheduler.http-client.max-connections-per-server=1000 +scheduler.http-client.connect-timeout=1m +scheduler.http-client.read-timeout=1m + +query.client.timeout=5m +query.max-age=30m + +plugin.bundles=\ + ../presto-raptor/pom.xml,\ + ../presto-hive-cdh4/pom.xml,\ + ../presto-example-http/pom.xml,\ + ../presto-kafka/pom.xml, \ + ../presto-tpch/pom.xml + +presto.version=testversion +experimental-syntax-enabled=true +distributed-joins-enabled=true diff --git a/presto-main/etc/jvm.config b/presto-main/etc/jvm.config new file mode 100644 index 00000000..e69de29b diff --git a/presto-main/etc/log.properties b/presto-main/etc/log.properties new file mode 100644 index 00000000..a192b8b2 --- /dev/null +++ b/presto-main/etc/log.properties @@ -0,0 +1,4 @@ +com.facebook.presto=INFO +com.sun.jersey.guice.spi.container.GuiceComponentProviderFactory=WARN +com.ning.http.client=WARN +com.facebook.presto.server.PluginManager=DEBUG diff --git a/presto-main/pom.xml b/presto-main/pom.xml new file mode 100644 index 00000000..ebdfe5d6 --- /dev/null +++ b/presto-main/pom.xml @@ -0,0 +1,365 @@ + + + 4.0.0 + + + com.facebook.presto + presto-root + 0.107 + + + presto-main + presto-main + + + ${project.parent.basedir} + 0.9.5 + + + + + it.unimi.dsi + fastutil + + + + org.apache.commons + commons-math3 + + + + com.facebook.presto + presto-spi + + + + com.facebook.presto + presto-client + + + + com.facebook.presto + presto-parser + + + + com.google.code.findbugs + annotations + + + + io.airlift + bootstrap + + + + io.airlift + slice + + + + io.airlift + concurrent + + + + io.airlift + node + + + + io.airlift + json + + + + io.airlift + configuration + + + + io.airlift + discovery + + + + io.airlift + event + + + + io.airlift + http-server + + + + io.airlift + jaxrs + + + + io.airlift + jmx + + + + io.airlift + jmx-http + + + + io.airlift + log + + + + io.airlift + log-manager + + + + io.airlift.resolver + resolver + + + + io.airlift + stats + + + + io.airlift + trace-token + + + + io.airlift + units + + + + io.airlift + joni + + + + io.airlift.discovery + discovery-server + + + + org.openjdk.jol + jol-core + + + + javax.servlet + javax.servlet-api + + + + javax.annotation + javax.annotation-api + + + + javax.ws.rs + javax.ws.rs-api + + + + com.fasterxml.jackson.core + jackson-annotations + + + + com.fasterxml.jackson.core + jackson-core + + + + com.fasterxml.jackson.core + jackson-databind + + + + joda-time + joda-time + + + + org.weakref + jmxutils + + + + javax.validation + validation-api + + + + javax.inject + javax.inject + + + + com.google.inject + guice + + + + com.google.inject.extensions + guice-multibindings + + + + com.google.guava + guava + + + + io.airlift + http-client + + + + org.jetbrains + annotations + provided + + + + org.sonatype.aether + aether-api + + + + org.ow2.asm + asm-all + + + + org.jgrapht + jgrapht-core + + + + mysql + mysql-connector-java + + + + commons-dbutils + commons-dbutils + 1.6 + + + + + org.testng + testng + test + + + + io.airlift + testing + test + + + + com.h2database + h2 + test + + + + com.facebook.presto + presto-tpch + test + + + + com.facebook.presto + presto-parser + test-jar + test + + + + org.openjdk.jmh + jmh-core + ${dep.jmh.version} + test + + + + org.openjdk.jmh + jmh-generator-annprocess + ${dep.jmh.version} + test + + + + io.airlift.tpch + tpch + test + + + + + + + com.mycila + license-maven-plugin + + + src/main/java/com/facebook/presto/byteCode/ClassInfo.java + src/main/java/com/facebook/presto/byteCode/ClassInfoLoader.java + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + hive + + + + + + + + benchmarks + + + + org.codehaus.mojo + exec-maven-plugin + + + benchmarks + + exec + + + + + ${java.home}/bin/java + + -DoutputDirectory=benchmark_outputs + -Dmyproperty=myvalue + -classpath + + com.facebook.presto.benchmark.BenchmarkSuite + + test + + + + + + + diff --git a/presto-main/src/main/java/com/facebook/presto/ExceededMemoryLimitException.java b/presto-main/src/main/java/com/facebook/presto/ExceededMemoryLimitException.java new file mode 100644 index 00000000..9d98bd57 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/ExceededMemoryLimitException.java @@ -0,0 +1,41 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto; + +import com.facebook.presto.spi.PrestoException; +import io.airlift.units.DataSize; + +import static com.facebook.presto.spi.StandardErrorCode.EXCEEDED_MEMORY_LIMIT; + +public class ExceededMemoryLimitException + extends PrestoException +{ + private final DataSize maxMemory; + + public ExceededMemoryLimitException(DataSize maxMemory) + { + this("Task", maxMemory); + } + + public ExceededMemoryLimitException(String entity, DataSize maxMemory) + { + super(EXCEEDED_MEMORY_LIMIT, String.format("%s exceeded max memory size of %s", entity, maxMemory)); + this.maxMemory = maxMemory; + } + + public DataSize getMaxMemory() + { + return maxMemory; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/HashPagePartitionFunction.java b/presto-main/src/main/java/com/facebook/presto/HashPagePartitionFunction.java new file mode 100644 index 00000000..dcb642c5 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/HashPagePartitionFunction.java @@ -0,0 +1,162 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto; + +import com.facebook.presto.operator.HashGenerator; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.type.Type; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import static com.facebook.presto.operator.HashGenerator.createHashGenerator; +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public final class HashPagePartitionFunction + implements PagePartitionFunction +{ + private final int partition; + private final int partitionCount; + private final List partitioningChannels; + private final List types; + private final HashGenerator hashGenerator; + private final Optional hashChannel; + + @JsonCreator + public HashPagePartitionFunction( + @JsonProperty("partition") int partition, + @JsonProperty("partitionCount") int partitionCount, + @JsonProperty("partitioningChannels") List partitioningChannels, + @JsonProperty("hashChannel") Optional hashChannel, + @JsonProperty("types") List types) + { + checkNotNull(partitioningChannels, "partitioningChannels is null"); + checkArgument(!partitioningChannels.isEmpty(), "partitioningChannels is empty"); + this.hashChannel = checkNotNull(hashChannel, "hashChannel is null"); + checkArgument(!hashChannel. isPresent() || hashChannel.get() < types.size(), "invalid hashChannel"); + + this.partition = partition; + this.partitionCount = partitionCount; + this.partitioningChannels = ImmutableList.copyOf(partitioningChannels); + this.hashGenerator = createHashGenerator(hashChannel, partitioningChannels, types); + this.types = ImmutableList.copyOf(types); + } + + @JsonProperty + public int getPartition() + { + return partition; + } + + @JsonProperty + public int getPartitionCount() + { + return partitionCount; + } + + @JsonProperty + public List getPartitioningChannels() + { + return partitioningChannels; + } + + @JsonProperty + public List getTypes() + { + return types; + } + + @JsonProperty + public Optional getHashChannel() + { + return hashChannel; + } + + @Override + public List partition(List pages) + { + if (pages.isEmpty()) { + return pages; + } + PageBuilder pageBuilder = new PageBuilder(types); + + ImmutableList.Builder partitionedPages = ImmutableList.builder(); + for (Page page : pages) { + for (int position = 0; position < page.getPositionCount(); position++) { + // if hash is not in range skip + int partitionHashBucket = hashGenerator.getPartitionHashBucket(partitionCount, position, page); + if (partitionHashBucket != partition) { + continue; + } + + pageBuilder.declarePosition(); + for (int channel = 0; channel < types.size(); channel++) { + Type type = types.get(channel); + type.appendTo(page.getBlock(channel), position, pageBuilder.getBlockBuilder(channel)); + } + + // if page is full, flush + if (pageBuilder.isFull()) { + partitionedPages.add(pageBuilder.build()); + pageBuilder.reset(); + } + } + } + if (!pageBuilder.isEmpty()) { + partitionedPages.add(pageBuilder.build()); + } + + return partitionedPages.build(); + } + + @Override + public int hashCode() + { + return Objects.hash(partition, partitionCount, partitioningChannels, hashGenerator); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + HashPagePartitionFunction other = (HashPagePartitionFunction) obj; + return Objects.equals(this.partition, other.partition) && + Objects.equals(this.partitionCount, other.partitionCount) && + Objects.equals(this.partitioningChannels, other.partitioningChannels) && + Objects.equals(hashChannel, other.hashChannel); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("partition", partition) + .add("partitionCount", partitionCount) + .add("partitioningChannels", partitioningChannels) + .add("hashChannel", hashChannel) + .toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/OutputBuffers.java b/presto-main/src/main/java/com/facebook/presto/OutputBuffers.java new file mode 100644 index 00000000..d3c9d2d0 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/OutputBuffers.java @@ -0,0 +1,170 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto; + +import com.facebook.presto.execution.TaskId; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableMap; + +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +public final class OutputBuffers +{ + public static final OutputBuffers INITIAL_EMPTY_OUTPUT_BUFFERS = new OutputBuffers(0, false, ImmutableMap.of()); + + private final long version; + private final boolean noMoreBufferIds; + private final Map buffers; + + // Visible only for Jackson... Use the "with" methods instead + @JsonCreator + public OutputBuffers( + @JsonProperty("version") long version, + @JsonProperty("noMoreBufferIds") boolean noMoreBufferIds, + @JsonProperty("buffers") Map buffers) + { + this.version = version; + this.buffers = ImmutableMap.copyOf(checkNotNull(buffers, "buffers is null")); + this.noMoreBufferIds = noMoreBufferIds; + } + + @JsonProperty + public long getVersion() + { + return version; + } + + @JsonProperty + public boolean isNoMoreBufferIds() + { + return noMoreBufferIds; + } + + @JsonProperty + public Map getBuffers() + { + return buffers; + } + + @Override + public int hashCode() + { + return Objects.hash(version, noMoreBufferIds, buffers); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final OutputBuffers other = (OutputBuffers) obj; + return Objects.equals(this.version, other.version) && + Objects.equals(this.noMoreBufferIds, other.noMoreBufferIds) && + Objects.equals(this.buffers, other.buffers); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("version", version) + .add("noMoreBufferIds", noMoreBufferIds) + .add("bufferIds", buffers) + .toString(); + } + + public OutputBuffers withBuffer(TaskId bufferId, PagePartitionFunction pagePartitionFunction) + { + checkNotNull(bufferId, "bufferId is null"); + + if (buffers.containsKey(bufferId)) { + checkHasBuffer(bufferId, pagePartitionFunction); + return this; + } + + // verify no new buffers is not set + checkState(!noMoreBufferIds, "No more buffer ids already set"); + + return new OutputBuffers( + version + 1, + false, + ImmutableMap.builder() + .putAll(buffers) + .put(bufferId, pagePartitionFunction) + .build()); + } + + public OutputBuffers withBuffers(Map buffers) + { + checkNotNull(buffers, "buffers is null"); + + Map newBuffers = new HashMap<>(); + for (Entry entry : buffers.entrySet()) { + TaskId bufferId = entry.getKey(); + PagePartitionFunction pagePartitionFunction = entry.getValue(); + + // it is ok to have a duplicate buffer declaration but it must have the same page partition function + if (this.buffers.containsKey(bufferId)) { + checkHasBuffer(bufferId, pagePartitionFunction); + continue; + } + + newBuffers.put(bufferId, pagePartitionFunction); + } + + // if we don't have new buffers, don't update + if (newBuffers.isEmpty()) { + return this; + } + + // verify no new buffers is not set + checkState(!noMoreBufferIds, "No more buffer ids already set"); + + // add the existing buffers + newBuffers.putAll(this.buffers); + + return new OutputBuffers(version + 1, false, newBuffers); + } + + public OutputBuffers withNoMoreBufferIds() + { + checkNotNull(this, "this is null"); + if (noMoreBufferIds) { + return this; + } + + return new OutputBuffers(version + 1, true, buffers); + } + + private void checkHasBuffer(TaskId bufferId, PagePartitionFunction pagePartitionFunction) + { + checkState(getBuffers().get(bufferId).equals(pagePartitionFunction), + "outputBuffers already contains buffer %s, but partition function is %s not %s", + bufferId, + buffers.get(bufferId), + pagePartitionFunction); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/PagePartitionFunction.java b/presto-main/src/main/java/com/facebook/presto/PagePartitionFunction.java new file mode 100644 index 00000000..947cd694 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/PagePartitionFunction.java @@ -0,0 +1,33 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto; + +import com.facebook.presto.spi.Page; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import java.util.List; + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = UnpartitionedPagePartitionFunction.class, name = "unpartitioned"), + @JsonSubTypes.Type(value = HashPagePartitionFunction.class, name = "hash") +}) +public interface PagePartitionFunction +{ + List partition(List pages); +} diff --git a/presto-main/src/main/java/com/facebook/presto/PagesIndexPageSorter.java b/presto-main/src/main/java/com/facebook/presto/PagesIndexPageSorter.java new file mode 100644 index 00000000..425f6953 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/PagesIndexPageSorter.java @@ -0,0 +1,51 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto; + +import com.facebook.presto.operator.PagesIndex; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PageSorter; +import com.facebook.presto.spi.block.SortOrder; +import com.facebook.presto.spi.type.Type; + +import java.util.List; + +import static com.facebook.presto.operator.SyntheticAddress.decodePosition; +import static com.facebook.presto.operator.SyntheticAddress.decodeSliceIndex; + +public class PagesIndexPageSorter + implements PageSorter +{ + @Override + public long[] sort(List types, List pages, List sortChannels, List sortOrders, int expectedPositions) + { + PagesIndex pagesIndex = new PagesIndex(types, expectedPositions); + pages.forEach(pagesIndex::addPage); + pagesIndex.sort(sortChannels, sortOrders); + + return pagesIndex.getValueAddresses().toLongArray(null); + } + + @Override + public int decodePageIndex(long address) + { + return decodeSliceIndex(address); + } + + @Override + public int decodePositionIndex(long address) + { + return decodePosition(address); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/PrestoMediaTypes.java b/presto-main/src/main/java/com/facebook/presto/PrestoMediaTypes.java new file mode 100644 index 00000000..a61aaf8c --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/PrestoMediaTypes.java @@ -0,0 +1,26 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto; + +import com.google.common.net.MediaType; + +public final class PrestoMediaTypes +{ + public static final String PRESTO_PAGES = "application/X-presto-pages"; + public static final MediaType PRESTO_PAGES_TYPE = MediaType.create("application", "X-presto-pages"); + + private PrestoMediaTypes() + { + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/ScheduledSplit.java b/presto-main/src/main/java/com/facebook/presto/ScheduledSplit.java new file mode 100644 index 00000000..3ec00c2b --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/ScheduledSplit.java @@ -0,0 +1,75 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto; + +import com.facebook.presto.metadata.Split; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.primitives.Longs; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkNotNull; + +public class ScheduledSplit +{ + private final long sequenceId; + private final Split split; + + @JsonCreator + public ScheduledSplit(@JsonProperty("sequenceId") long sequenceId, @JsonProperty("split") Split split) + { + this.sequenceId = sequenceId; + this.split = checkNotNull(split, "split is null"); + } + + @JsonProperty + public long getSequenceId() + { + return sequenceId; + } + + @JsonProperty + public Split getSplit() + { + return split; + } + + @Override + public int hashCode() + { + return Longs.hashCode(sequenceId); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final ScheduledSplit other = (ScheduledSplit) obj; + return this.sequenceId == other.sequenceId; + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("sequenceId", sequenceId) + .add("split", split) + .toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/Session.java b/presto-main/src/main/java/com/facebook/presto/Session.java new file mode 100644 index 00000000..cec6a7f1 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/Session.java @@ -0,0 +1,364 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto; + +import com.facebook.presto.client.ClientSession; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.type.TimeZoneKey; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; + +import javax.annotation.Nullable; + +import java.net.URI; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.TimeZone; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.Objects.requireNonNull; + +public final class Session +{ + private final String user; + @Nullable + private final String source; + private final String catalog; + private final String schema; + private final TimeZoneKey timeZoneKey; + private final Locale locale; + @Nullable + private final String remoteUserAddress; + @Nullable + private final String userAgent; + private final long startTime; + private final Map systemProperties; + private final Map> catalogProperties; + + @JsonCreator + public Session( + @JsonProperty("user") String user, + @JsonProperty("source") @Nullable String source, + @JsonProperty("catalog") String catalog, + @JsonProperty("schema") String schema, + @JsonProperty("timeZoneKey") TimeZoneKey timeZoneKey, + @JsonProperty("locale") Locale locale, + @JsonProperty("remoteUserAddress") @Nullable String remoteUserAddress, + @JsonProperty("userAgent") @Nullable String userAgent, + @JsonProperty("startTime") long startTime, + @JsonProperty("systemProperties") Map systemProperties, + @JsonProperty("catalogProperties") Map> catalogProperties) + { + this.user = requireNonNull(user, "user is null"); + this.source = source; + this.catalog = requireNonNull(catalog, "catalog is null"); + this.schema = requireNonNull(schema, "schema is null"); + this.timeZoneKey = requireNonNull(timeZoneKey, "timeZoneKey is null"); + this.locale = requireNonNull(locale, "locale is null"); + this.remoteUserAddress = remoteUserAddress; + this.userAgent = userAgent; + this.startTime = startTime; + this.systemProperties = ImmutableMap.copyOf(systemProperties); + + ImmutableMap.Builder> catalogPropertiesBuilder = ImmutableMap.>builder(); + catalogProperties.entrySet().stream() + .map(entry -> Maps.immutableEntry(entry.getKey(), ImmutableMap.copyOf(entry.getValue()))) + .forEach(catalogPropertiesBuilder::put); + this.catalogProperties = catalogPropertiesBuilder.build(); + } + + @JsonProperty + public String getUser() + { + return user; + } + + @Nullable + @JsonProperty + public String getSource() + { + return source; + } + + @JsonProperty + public String getCatalog() + { + return catalog; + } + + @JsonProperty + public String getSchema() + { + return schema; + } + + @JsonProperty + public TimeZoneKey getTimeZoneKey() + { + return timeZoneKey; + } + + @JsonProperty + public Locale getLocale() + { + return locale; + } + + @Nullable + @JsonProperty + public String getRemoteUserAddress() + { + return remoteUserAddress; + } + + @Nullable + @JsonProperty + public String getUserAgent() + { + return userAgent; + } + + @JsonProperty + public long getStartTime() + { + return startTime; + } + + @JsonProperty + public Map getSystemProperties() + { + return systemProperties; + } + + @JsonProperty + public Map> getCatalogProperties() + { + return catalogProperties; + } + + public Session withSystemProperty(String key, String value) + { + checkNotNull(key, "key is null"); + checkNotNull(value, "value is null"); + + Map systemProperties = new LinkedHashMap<>(this.systemProperties); + systemProperties.put(key, value); + + return new Session( + user, + source, + catalog, + schema, + timeZoneKey, + locale, + remoteUserAddress, + userAgent, + startTime, + systemProperties, + catalogProperties); + } + + public Session withCatalogProperty(String catalog, String key, String value) + { + checkNotNull(catalog, "catalog is null"); + checkNotNull(key, "key is null"); + checkNotNull(value, "value is null"); + + Map> catalogProperties = new LinkedHashMap<>(this.catalogProperties); + Map properties = catalogProperties.get(catalog); + if (properties == null) { + properties = new LinkedHashMap<>(); + } + else { + properties = new LinkedHashMap<>(properties); + } + properties.put(key, value); + catalogProperties.put(catalog, properties); + + return new Session( + user, + source, + catalog, + schema, + timeZoneKey, + locale, + remoteUserAddress, + userAgent, + startTime, + systemProperties, + catalogProperties); + } + + public ConnectorSession toConnectorSession() + { + return new ConnectorSession(user, timeZoneKey, locale, startTime, null); + } + + public ConnectorSession toConnectorSession(String catalog) + { + return new ConnectorSession(user, timeZoneKey, locale, startTime, catalogProperties.get(checkNotNull(catalog, "catalog is null"))); + } + + public ClientSession toClientSession(URI server, boolean debug) + { + ImmutableMap.Builder properties = ImmutableMap.builder(); + properties.putAll(systemProperties); + for (Entry> catalogProperties : this.catalogProperties.entrySet()) { + String catalog = catalogProperties.getKey(); + for (Entry entry : catalogProperties.getValue().entrySet()) { + properties.put(catalog + "." + entry.getKey(), entry.getValue()); + } + } + + return new ClientSession( + checkNotNull(server, "server is null"), + user, + source, + catalog, + schema, + timeZoneKey.getId(), + locale, + properties.build(), + debug); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("user", user) + .add("source", source) + .add("catalog", catalog) + .add("schema", schema) + .add("timeZoneKey", timeZoneKey) + .add("locale", locale) + .add("remoteUserAddress", remoteUserAddress) + .add("userAgent", userAgent) + .add("startTime", startTime) + .toString(); + } + + public static SessionBuilder builder() + { + return new SessionBuilder(); + } + + public static class SessionBuilder + { + private String user; + private String source; + private String catalog; + private String schema; + private TimeZoneKey timeZoneKey = TimeZoneKey.getTimeZoneKey(TimeZone.getDefault().getID()); + private Locale locale = Locale.getDefault(); + private String remoteUserAddress; + private String userAgent; + private long startTime = System.currentTimeMillis(); + private Map systemProperties = ImmutableMap.of(); + private final Map> catalogProperties = new HashMap<>(); + + private SessionBuilder() + { + } + + public SessionBuilder setCatalog(String catalog) + { + this.catalog = catalog; + return this; + } + + public SessionBuilder setLocale(Locale locale) + { + this.locale = locale; + return this; + } + + public SessionBuilder setRemoteUserAddress(String remoteUserAddress) + { + this.remoteUserAddress = remoteUserAddress; + return this; + } + + public SessionBuilder setSchema(String schema) + { + this.schema = schema; + return this; + } + + public SessionBuilder setSource(String source) + { + this.source = source; + return this; + } + + public SessionBuilder setStartTime(long startTime) + { + this.startTime = startTime; + return this; + } + + public SessionBuilder setTimeZoneKey(TimeZoneKey timeZoneKey) + { + this.timeZoneKey = timeZoneKey; + return this; + } + + public SessionBuilder setUser(String user) + { + this.user = user; + return this; + } + + public SessionBuilder setUserAgent(String userAgent) + { + this.userAgent = userAgent; + return this; + } + + /** + * Sets the system properties for the session. The property names and + * values must only contain characters from US-ASCII and must not be for '='. + */ + public SessionBuilder setSystemProperties(Map systemProperties) + { + this.systemProperties = ImmutableMap.copyOf(systemProperties); + return this; + } + + /** + * Sets the properties for a catalog. The catalog name, property names, and + * values must only contain characters from US-ASCII and must not be for '='. + */ + public SessionBuilder setCatalogProperties(String catalog, Map properties) + { + checkNotNull(catalog, "catalog is null"); + checkArgument(!catalog.isEmpty(), "catalog is empty"); + + catalogProperties.put(catalog, ImmutableMap.copyOf(properties)); + return this; + } + + public Session build() + { + return new Session(user, source, catalog, schema, timeZoneKey, locale, remoteUserAddress, userAgent, startTime, systemProperties, catalogProperties); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/SystemSessionProperties.java b/presto-main/src/main/java/com/facebook/presto/SystemSessionProperties.java new file mode 100644 index 00000000..f58a7215 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/SystemSessionProperties.java @@ -0,0 +1,126 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto; + +import io.airlift.units.DataSize; + +public final class SystemSessionProperties +{ + public static final String BIG_QUERY = "experimental_big_query"; + private static final String OPTIMIZE_HASH_GENERATION = "optimize_hash_generation"; + private static final String DISTRIBUTED_JOIN = "distributed_join"; + private static final String HASH_PARTITION_COUNT = "hash_partition_count"; + private static final String PREFER_STREAMING_OPERATORS = "prefer_streaming_operators"; + private static final String TASK_WRITER_COUNT = "task_writer_count"; + private static final String TASK_DEFAULT_CONCURRENCY = "task_default_concurrency"; + private static final String TASK_JOIN_CONCURRENCY = "task_join_concurrency"; + private static final String TASK_HASH_BUILD_CONCURRENCY = "task_hash_build_concurrency"; + private static final String TASK_AGGREGATION_CONCURRENCY = "task_aggregation_concurrency"; + private static final String QUERY_MAX_MEMORY = "query_max_memory"; + + private SystemSessionProperties() {} + + public static boolean isBigQueryEnabled(Session session, boolean defaultValue) + { + return isEnabled(BIG_QUERY, session, defaultValue); + } + + private static boolean isEnabled(String propertyName, Session session, boolean defaultValue) + { + String enabled = session.getSystemProperties().get(propertyName); + if (enabled == null) { + return defaultValue; + } + + return Boolean.valueOf(enabled); + } + + private static int getNumber(String propertyName, Session session, int defaultValue) + { + String count = session.getSystemProperties().get(propertyName); + if (count != null) { + try { + return Integer.parseInt(count); + } + catch (NumberFormatException ignored) { + } + } + + return defaultValue; + } + + private static DataSize getDataSize(String propertyName, Session session, DataSize defaultValue) + { + String size = session.getSystemProperties().get(propertyName); + if (size != null) { + try { + return DataSize.valueOf(size); + } + catch (IllegalArgumentException ignored) { + } + } + + return defaultValue; + } + + public static boolean isOptimizeHashGenerationEnabled(Session session, boolean defaultValue) + { + return isEnabled(OPTIMIZE_HASH_GENERATION, session, defaultValue); + } + + public static boolean isDistributedJoinEnabled(Session session, boolean defaultValue) + { + return isEnabled(DISTRIBUTED_JOIN, session, defaultValue); + } + + public static int getHashPartitionCount(Session session, int defaultValue) + { + return getNumber(HASH_PARTITION_COUNT, session, defaultValue); + } + + public static boolean preferStreamingOperators(Session session, boolean defaultValue) + { + return isEnabled(PREFER_STREAMING_OPERATORS, session, defaultValue); + } + + public static int getTaskWriterCount(Session session, int defaultValue) + { + return getNumber(TASK_WRITER_COUNT, session, defaultValue); + } + + public static int getTaskDefaultConcurrency(Session session, int defaultValue) + { + return getNumber(TASK_DEFAULT_CONCURRENCY, session, defaultValue); + } + + public static int getTaskJoinConcurrency(Session session, int defaultValue) + { + return getNumber(TASK_JOIN_CONCURRENCY, session, getTaskDefaultConcurrency(session, defaultValue)); + } + + public static int getTaskHashBuildConcurrency(Session session, int defaultValue) + { + return getNumber(TASK_HASH_BUILD_CONCURRENCY, session, getTaskDefaultConcurrency(session, defaultValue)); + } + + public static int getTaskAggregationConcurrency(Session session, int defaultValue) + { + return getNumber(TASK_AGGREGATION_CONCURRENCY, session, getTaskDefaultConcurrency(session, defaultValue)); + } + + public static DataSize getQueryMaxMemory(Session session, DataSize defaultValue) + { + return getDataSize(QUERY_MAX_MEMORY, session, defaultValue); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/TaskSource.java b/presto-main/src/main/java/com/facebook/presto/TaskSource.java new file mode 100644 index 00000000..57ea4ec1 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/TaskSource.java @@ -0,0 +1,103 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto; + +import com.facebook.presto.sql.planner.plan.PlanNodeId; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableSet; + +import java.util.Set; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class TaskSource +{ + private final PlanNodeId planNodeId; + private final Set splits; + private final boolean noMoreSplits; + + @JsonCreator + public TaskSource( + @JsonProperty("planNodeId") PlanNodeId planNodeId, + @JsonProperty("splits") Set splits, + @JsonProperty("noMoreSplits") boolean noMoreSplits) + { + this.planNodeId = checkNotNull(planNodeId, "planNodeId is null"); + this.splits = ImmutableSet.copyOf(checkNotNull(splits, "splits is null")); + this.noMoreSplits = noMoreSplits; + } + + @JsonProperty + public PlanNodeId getPlanNodeId() + { + return planNodeId; + } + + @JsonProperty + public Set getSplits() + { + return splits; + } + + @JsonProperty + public boolean isNoMoreSplits() + { + return noMoreSplits; + } + + public TaskSource update(TaskSource source) + { + checkArgument(planNodeId.equals(source.getPlanNodeId()), "Expected source %s, but got source %s", planNodeId, source.getPlanNodeId()); + + if (isNewer(source)) { + // assure the new source is properly formed + // we know that either the new source one has new splits and/or it is marking the source as closed + checkArgument(!noMoreSplits || source.isNoMoreSplits(), "Source %s has new splits, but no more splits already set", planNodeId); + + Set newSplits = ImmutableSet.builder() + .addAll(splits) + .addAll(source.getSplits()) + .build(); + + return new TaskSource(planNodeId, + newSplits, + source.isNoMoreSplits()); + } + else { + // the specified source is older than this one + return this; + } + } + + private boolean isNewer(TaskSource source) + { + // the specified source is newer if it changes the no more + // splits flag or if it contains new splits + return (!noMoreSplits && source.isNoMoreSplits()) || + (!splits.containsAll(source.getSplits())); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("planNodeId", planNodeId) + .add("splits", splits) + .add("noMoreSplits", noMoreSplits) + .toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/UnpartitionedPagePartitionFunction.java b/presto-main/src/main/java/com/facebook/presto/UnpartitionedPagePartitionFunction.java new file mode 100644 index 00000000..ab0a1f67 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/UnpartitionedPagePartitionFunction.java @@ -0,0 +1,60 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto; + +import com.facebook.presto.spi.Page; +import com.fasterxml.jackson.annotation.JsonCreator; + +import java.util.List; +import java.util.Objects; + +public final class UnpartitionedPagePartitionFunction + implements PagePartitionFunction +{ + @JsonCreator + public UnpartitionedPagePartitionFunction() + { + } + + @Override + public List partition(List pages) + { + return pages; + } + + @Override + public int hashCode() + { + return Objects.hash(getClass()); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final UnpartitionedPagePartitionFunction other = (UnpartitionedPagePartitionFunction) obj; + return Objects.equals(this.getClass(), other.getClass()); + } + + @Override + public String toString() + { + return "unpartitioned"; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/block/BlockEncodingManager.java b/presto-main/src/main/java/com/facebook/presto/block/BlockEncodingManager.java new file mode 100644 index 00000000..09779a81 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/block/BlockEncodingManager.java @@ -0,0 +1,149 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.block; + +import com.facebook.presto.spi.block.BlockEncoding; +import com.facebook.presto.spi.block.BlockEncodingFactory; +import com.facebook.presto.spi.block.BlockEncodingSerde; +import com.facebook.presto.spi.block.FixedWidthBlockEncoding; +import com.facebook.presto.spi.block.LazySliceArrayBlockEncoding; +import com.facebook.presto.spi.block.SliceArrayBlockEncoding; +import com.facebook.presto.spi.block.VariableWidthBlockEncoding; +import com.facebook.presto.spi.type.TypeManager; +import com.google.common.collect.ImmutableSet; +import io.airlift.slice.SliceInput; +import io.airlift.slice.SliceOutput; + +import javax.inject.Inject; + +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.nio.charset.StandardCharsets.UTF_8; + +public final class BlockEncodingManager + implements BlockEncodingSerde +{ + private final TypeManager typeManager; + private final ConcurrentMap> blockEncodings = new ConcurrentHashMap<>(); + + public BlockEncodingManager(TypeManager typeManager, BlockEncodingFactory... blockEncodingFactories) + { + this(typeManager, ImmutableSet.copyOf(blockEncodingFactories)); + } + + @Inject + public BlockEncodingManager(TypeManager typeManager, Set> blockEncodingFactories) + { + // This function should be called from Guice and tests only + + this.typeManager = checkNotNull(typeManager, "typeManager is null"); + + // always add the built-in BlockEncodingFactories + addBlockEncodingFactory(VariableWidthBlockEncoding.FACTORY); + addBlockEncodingFactory(FixedWidthBlockEncoding.FACTORY); + addBlockEncodingFactory(SliceArrayBlockEncoding.FACTORY); + addBlockEncodingFactory(LazySliceArrayBlockEncoding.FACTORY); + + for (BlockEncodingFactory factory : checkNotNull(blockEncodingFactories, "blockEncodingFactories is null")) { + addBlockEncodingFactory(factory); + } + } + + public void addBlockEncodingFactory(BlockEncodingFactory blockEncoding) + { + checkNotNull(blockEncoding, "blockEncoding is null"); + BlockEncodingFactory existingEntry = blockEncodings.putIfAbsent(blockEncoding.getName(), blockEncoding); + checkArgument(existingEntry == null, "Encoding %s is already registered", blockEncoding.getName()); + } + + @Override + public BlockEncoding readBlockEncoding(SliceInput input) + { + // read the encoding name + String encodingName = readLengthPrefixedString(input); + + // look up the encoding factory + BlockEncodingFactory blockEncoding = blockEncodings.get(encodingName); + checkArgument(blockEncoding != null, "Unknown block encoding %s", encodingName); + + // load read the encoding factory from the output stream + return blockEncoding.readEncoding(typeManager, this, input); + } + + @Override + public void writeBlockEncoding(SliceOutput output, BlockEncoding encoding) + { + writeBlockEncodingInternal(output, encoding); + } + + /** + * This method enables internal implementations to serialize data without holding a BlockEncodingManager. + * For example, LiteralInterpreter.toExpression serializes data to produce literals. + */ + public static void writeBlockEncodingInternal(SliceOutput output, BlockEncoding encoding) + { + WriteOnlyBlockEncodingManager.INSTANCE.writeBlockEncoding(output, encoding); + } + + private static class WriteOnlyBlockEncodingManager + implements BlockEncodingSerde + { + static final WriteOnlyBlockEncodingManager INSTANCE = new WriteOnlyBlockEncodingManager(); + + private WriteOnlyBlockEncodingManager() + { + } + + @Override + public BlockEncoding readBlockEncoding(SliceInput input) + { + throw new UnsupportedOperationException(); + } + + @Override + public void writeBlockEncoding(SliceOutput output, BlockEncoding encoding) + { + // get the encoding name + String encodingName = encoding.getName(); + + // look up the encoding factory + BlockEncodingFactory blockEncoding = encoding.getFactory(); + + // write the name to the output + writeLengthPrefixedString(output, encodingName); + + // write the encoding to the output + blockEncoding.writeEncoding(this, output, encoding); + } + } + + private static String readLengthPrefixedString(SliceInput input) + { + int length = input.readInt(); + byte[] bytes = new byte[length]; + input.readBytes(bytes); + return new String(bytes, UTF_8); + } + + private static void writeLengthPrefixedString(SliceOutput output, String value) + { + byte[] bytes = value.getBytes(UTF_8); + output.writeInt(bytes.length); + output.writeBytes(bytes); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/block/BlockUtils.java b/presto-main/src/main/java/com/facebook/presto/block/BlockUtils.java new file mode 100644 index 00000000..e1234a65 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/block/BlockUtils.java @@ -0,0 +1,58 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.block; + +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; +import io.airlift.slice.Slice; +import io.airlift.slice.Slices; + +public final class BlockUtils +{ + private BlockUtils() + { + } + + public static void appendObject(Type type, BlockBuilder blockBuilder, Object value) + { + if (value == null) { + blockBuilder.appendNull(); + } + else if (type.getJavaType() == boolean.class) { + type.writeBoolean(blockBuilder, (Boolean) value); + } + else if (type.getJavaType() == double.class) { + type.writeDouble(blockBuilder, ((Number) value).doubleValue()); + } + else if (type.getJavaType() == long.class) { + type.writeLong(blockBuilder, ((Number) value).longValue()); + } + else if (type.getJavaType() == Slice.class) { + Slice slice; + if (value instanceof byte[]) { + slice = Slices.wrappedBuffer((byte[]) value); + } + else if (value instanceof String) { + slice = Slices.utf8Slice((String) value); + } + else { + slice = (Slice) value; + } + type.writeSlice(blockBuilder, slice, 0, slice.length()); + } + else { + throw new IllegalArgumentException("Unsupported type: " + value.getClass().getName()); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/block/PagesSerde.java b/presto-main/src/main/java/com/facebook/presto/block/PagesSerde.java new file mode 100644 index 00000000..3660c2d5 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/block/PagesSerde.java @@ -0,0 +1,123 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.block; + +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockEncoding; +import com.facebook.presto.spi.block.BlockEncodingSerde; +import com.google.common.collect.AbstractIterator; +import io.airlift.slice.SliceInput; +import io.airlift.slice.SliceOutput; + +import java.util.Iterator; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.Arrays.asList; + +// layout is: +// - position count (int) +// - number of blocks (int) +// - sequence of: +// - block encoding +// - block +public final class PagesSerde +{ + private PagesSerde() {} + + public static void writePages(BlockEncodingSerde blockEncodingSerde, SliceOutput sliceOutput, Page... pages) + { + writePages(blockEncodingSerde, sliceOutput, asList(pages).iterator()); + } + + public static void writePages(BlockEncodingSerde blockEncodingSerde, SliceOutput sliceOutput, Iterable pages) + { + writePages(blockEncodingSerde, sliceOutput, pages.iterator()); + } + + public static void writePages(BlockEncodingSerde blockEncodingSerde, SliceOutput sliceOutput, Iterator pages) + { + PagesWriter pagesWriter = new PagesWriter(blockEncodingSerde, sliceOutput); + while (pages.hasNext()) { + pagesWriter.append(pages.next()); + } + } + + public static Iterator readPages(BlockEncodingSerde blockEncodingSerde, SliceInput sliceInput) + { + return new PagesReader(blockEncodingSerde, sliceInput); + } + + private static class PagesWriter + { + private final BlockEncodingSerde serde; + private final SliceOutput output; + + private PagesWriter(BlockEncodingSerde serde, SliceOutput output) + { + this.serde = checkNotNull(serde, "serde is null"); + this.output = checkNotNull(output, "output is null"); + } + + public PagesWriter append(Page page) + { + checkNotNull(page, "page is null"); + + Block[] blocks = page.getBlocks(); + + output.writeInt(page.getPositionCount()); + output.writeInt(blocks.length); + for (int i = 0; i < blocks.length; i++) { + BlockEncoding encoding = blocks[i].getEncoding(); + serde.writeBlockEncoding(output, encoding); + encoding.writeBlock(output, blocks[i]); + } + + return this; + } + } + + private static class PagesReader + extends AbstractIterator + { + private final BlockEncodingSerde serde; + private final SliceInput input; + + public PagesReader(BlockEncodingSerde serde, SliceInput input) + { + this.serde = checkNotNull(serde, "serde is null"); + this.input = checkNotNull(input, "input is null"); + } + + @Override + protected Page computeNext() + { + if (!input.isReadable()) { + return endOfData(); + } + + int positions = input.readInt(); + int numberOfBlocks = input.readInt(); + Block[] blocks = new Block[numberOfBlocks]; + for (int i = 0; i < blocks.length; i++) { + BlockEncoding encoding = serde.readBlockEncoding(input); + blocks[i] = encoding.readBlock(input); + } + + @SuppressWarnings("UnnecessaryLocalVariable") + Page page = new Page(positions, blocks); + return page; + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/Access.java b/presto-main/src/main/java/com/facebook/presto/byteCode/Access.java new file mode 100644 index 00000000..4a9c3eca --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/Access.java @@ -0,0 +1,92 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode; + +import com.google.common.collect.ImmutableList; + +import java.util.EnumSet; + +import static java.util.Locale.ENGLISH; +import static org.objectweb.asm.Opcodes.ACC_ABSTRACT; +import static org.objectweb.asm.Opcodes.ACC_ANNOTATION; +import static org.objectweb.asm.Opcodes.ACC_BRIDGE; +import static org.objectweb.asm.Opcodes.ACC_ENUM; +import static org.objectweb.asm.Opcodes.ACC_FINAL; +import static org.objectweb.asm.Opcodes.ACC_INTERFACE; +import static org.objectweb.asm.Opcodes.ACC_NATIVE; +import static org.objectweb.asm.Opcodes.ACC_PRIVATE; +import static org.objectweb.asm.Opcodes.ACC_PROTECTED; +import static org.objectweb.asm.Opcodes.ACC_PUBLIC; +import static org.objectweb.asm.Opcodes.ACC_STATIC; +import static org.objectweb.asm.Opcodes.ACC_STRICT; +import static org.objectweb.asm.Opcodes.ACC_SUPER; +import static org.objectweb.asm.Opcodes.ACC_SYNCHRONIZED; +import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC; +import static org.objectweb.asm.Opcodes.ACC_TRANSIENT; +import static org.objectweb.asm.Opcodes.ACC_VARARGS; +import static org.objectweb.asm.Opcodes.ACC_VOLATILE; + +public enum Access +{ + PUBLIC(ACC_PUBLIC), + PRIVATE(ACC_PRIVATE), + PROTECTED(ACC_PROTECTED), + STATIC(ACC_STATIC), + FINAL(ACC_FINAL), + SUPER(ACC_SUPER), + SYNCHRONIZED(ACC_SYNCHRONIZED), + VOLATILE(ACC_VOLATILE), + BRIDGE(ACC_BRIDGE), + VARARGS(ACC_VARARGS), + TRANSIENT(ACC_TRANSIENT), + NATIVE(ACC_NATIVE), + INTERFACE(ACC_INTERFACE), + ABSTRACT(ACC_ABSTRACT), + STRICT(ACC_STRICT), + SYNTHETIC(ACC_SYNTHETIC), + ANNOTATION(ACC_ANNOTATION), + ENUM(ACC_ENUM); + + private final int modifier; + + Access(int modifier) + { + this.modifier = modifier; + } + + public int getModifier() + { + return modifier; + } + + @Override + public String toString() + { + return name().toLowerCase(ENGLISH); + } + + public static EnumSet a(Access... access) + { + return EnumSet.copyOf(ImmutableList.copyOf(access)); + } + + public static int toAccessModifier(Iterable accesses) + { + int modifier = 0; + for (Access access : accesses) { + modifier += access.getModifier(); + } + return modifier; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/AnnotationDefinition.java b/presto-main/src/main/java/com/facebook/presto/byteCode/AnnotationDefinition.java new file mode 100644 index 00000000..e0e4db3c --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/AnnotationDefinition.java @@ -0,0 +1,232 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableSet; +import com.google.common.primitives.Primitives; +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Type; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import static com.facebook.presto.byteCode.ParameterizedType.type; + +public class AnnotationDefinition +{ + private static final Set> ALLOWED_TYPES = ImmutableSet.>builder() + .addAll(Primitives.allWrapperTypes()) + .add(String.class) + .add(Class.class) + .add(ParameterizedType.class) + .add(AnnotationDefinition.class) + .add(Enum.class) + .build(); + + private final ParameterizedType type; + private final Map values = new LinkedHashMap<>(); + + public AnnotationDefinition(Class type) + { + this.type = type(type); + } + + public AnnotationDefinition(ParameterizedType type) + { + this.type = type; + } + + public AnnotationDefinition setValue(String name, Byte value) + { + return setValueInternal(name, value); + } + + public AnnotationDefinition setValue(String name, Boolean value) + { + return setValueInternal(name, value); + } + + public AnnotationDefinition setValue(String name, Character value) + { + return setValueInternal(name, value); + } + + public AnnotationDefinition setValue(String name, Short value) + { + return setValueInternal(name, value); + } + + public AnnotationDefinition setValue(String name, Integer value) + { + return setValueInternal(name, value); + } + + public AnnotationDefinition setValue(String name, Long value) + { + return setValueInternal(name, value); + } + + public AnnotationDefinition setValue(String name, Float value) + { + return setValueInternal(name, value); + } + + public AnnotationDefinition setValue(String name, Double value) + { + return setValueInternal(name, value); + } + + public AnnotationDefinition setValue(String name, String value) + { + return setValueInternal(name, value); + } + + public AnnotationDefinition setValue(String name, Class value) + { + return setValueInternal(name, value); + } + + public AnnotationDefinition setValue(String name, ParameterizedType value) + { + return setValueInternal(name, value); + } + + public AnnotationDefinition setValue(String name, AnnotationDefinition value) + { + return setValueInternal(name, value); + } + + public AnnotationDefinition setValue(String name, Enum value) + { + return setValueInternal(name, value); + } + + public AnnotationDefinition setValue(String name, List value) + { + return setValueInternal(name, value); + } + + private AnnotationDefinition setValueInternal(String name, Object value) + { + Preconditions.checkNotNull(name, "name is null"); + Preconditions.checkNotNull(value, "value is null"); + + isValidType(value); + + values.put(name, value); + return this; + } + + public ParameterizedType getType() + { + return type; + } + + public Map getValues() + { + // todo we need an unmodifiable view + return values; + } + + private static void isValidType(Object value) + { + if (value instanceof List) { + // todo verify list contains single type + for (Object v : (List) value) { + Preconditions.checkArgument(ALLOWED_TYPES.contains(v.getClass()), "List contains invalid type %s", v.getClass()); + if (v instanceof List) { + isValidType(value); + } + } + } + else { + Preconditions.checkArgument(ALLOWED_TYPES.contains(value.getClass()), "Invalid value type %s", value.getClass()); + } + } + + public void visitClassAnnotation(ClassVisitor visitor) + { + AnnotationVisitor annotationVisitor = visitor.visitAnnotation(type.getType(), true); + visit(annotationVisitor); + annotationVisitor.visitEnd(); + } + + public void visitFieldAnnotation(FieldVisitor visitor) + { + AnnotationVisitor annotationVisitor = visitor.visitAnnotation(type.getType(), true); + visit(annotationVisitor); + annotationVisitor.visitEnd(); + } + + public void visitMethodAnnotation(MethodVisitor visitor) + { + AnnotationVisitor annotationVisitor = visitor.visitAnnotation(type.getType(), true); + visit(annotationVisitor); + annotationVisitor.visitEnd(); + } + + public void visitParameterAnnotation(int parameterIndex, MethodVisitor visitor) + { + AnnotationVisitor annotationVisitor = visitor.visitParameterAnnotation(parameterIndex, type.getType(), true); + visit(annotationVisitor); + annotationVisitor.visitEnd(); + } + + private void visit(AnnotationVisitor visitor) + { + for (Entry entry : values.entrySet()) { + String name = entry.getKey(); + Object value = entry.getValue(); + visit(visitor, name, value); + } + } + + private static void visit(AnnotationVisitor visitor, String name, Object value) + { + if (value instanceof AnnotationDefinition) { + AnnotationDefinition annotation = (AnnotationDefinition) value; + AnnotationVisitor annotationVisitor = visitor.visitAnnotation(name, annotation.type.getType()); + annotation.visit(annotationVisitor); + } + else if (value instanceof Enum) { + Enum enumConstant = (Enum) value; + visitor.visitEnum(name, type(enumConstant.getDeclaringClass()).getClassName(), enumConstant.name()); + } + else if (value instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) value; + visitor.visit(name, Type.getType(parameterizedType.getType())); + } + else if (value instanceof Class) { + Class clazz = (Class) value; + visitor.visit(name, Type.getType(clazz)); + } + else if (value instanceof List) { + AnnotationVisitor arrayVisitor = visitor.visitArray(name); + for (Object element : (List) value) { + visit(arrayVisitor, null, element); + } + arrayVisitor.visitEnd(); + } + else { + visitor.visit(name, value); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/Block.java b/presto-main/src/main/java/com/facebook/presto/byteCode/Block.java new file mode 100644 index 00000000..c4f722ed --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/Block.java @@ -0,0 +1,951 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode; + +import com.facebook.presto.byteCode.debug.LineNumberNode; +import com.facebook.presto.byteCode.instruction.Constant; +import com.facebook.presto.byteCode.instruction.InvokeInstruction; +import com.facebook.presto.byteCode.instruction.JumpInstruction; +import com.facebook.presto.byteCode.instruction.LabelNode; +import com.facebook.presto.byteCode.instruction.TypeInstruction; +import com.facebook.presto.byteCode.instruction.VariableInstruction; +import com.google.common.collect.ImmutableList; +import org.objectweb.asm.MethodVisitor; + +import javax.annotation.concurrent.NotThreadSafe; + +import java.lang.invoke.MethodType; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; + +import static com.facebook.presto.byteCode.Access.STATIC; +import static com.facebook.presto.byteCode.ParameterizedType.type; +import static com.facebook.presto.byteCode.instruction.Constant.loadBoolean; +import static com.facebook.presto.byteCode.instruction.Constant.loadClass; +import static com.facebook.presto.byteCode.instruction.Constant.loadDouble; +import static com.facebook.presto.byteCode.instruction.Constant.loadFloat; +import static com.facebook.presto.byteCode.instruction.Constant.loadInt; +import static com.facebook.presto.byteCode.instruction.Constant.loadLong; +import static com.facebook.presto.byteCode.instruction.Constant.loadNumber; +import static com.facebook.presto.byteCode.instruction.FieldInstruction.getFieldInstruction; +import static com.facebook.presto.byteCode.instruction.FieldInstruction.getStaticInstruction; +import static com.facebook.presto.byteCode.instruction.FieldInstruction.putFieldInstruction; +import static com.facebook.presto.byteCode.instruction.FieldInstruction.putStaticInstruction; +import static com.facebook.presto.byteCode.instruction.TypeInstruction.cast; +import static com.facebook.presto.byteCode.instruction.TypeInstruction.instanceOf; +import static com.facebook.presto.byteCode.instruction.VariableInstruction.loadVariable; +import static com.facebook.presto.byteCode.instruction.VariableInstruction.storeVariable; +import static com.google.common.base.Preconditions.checkArgument; + +@SuppressWarnings("UnusedDeclaration") +@NotThreadSafe +public class Block + implements ByteCodeNode +{ + private final List nodes = new ArrayList<>(); + + private String description; + private int currentLineNumber = -1; + + public Block() + { + } + + public String getDescription() + { + return description; + } + + public Block setDescription(String description) + { + this.description = description; + return this; + } + + @Override + public List getChildNodes() + { + return ImmutableList.copyOf(nodes); + } + + public Block append(ByteCodeNode node) + { + nodes.add(node); + return this; + } + + public Block comment(String comment) + { + nodes.add(new Comment(comment)); + return this; + } + + public Block comment(String comment, Object... args) + { + nodes.add(new Comment(String.format(comment, args))); + return this; + } + + public boolean isEmpty() + { + return nodes.isEmpty(); + } + + public Block visitLabel(LabelNode label) + { + nodes.add(label); + return this; + } + + public Block gotoLabel(LabelNode label) + { + nodes.add(JumpInstruction.jump(label)); + return this; + } + + public Block ifFalseGoto(LabelNode label) + { + return ifZeroGoto(label); + } + + public Block ifTrueGoto(LabelNode label) + { + return ifNotZeroGoto(label); + } + + public Block ifZeroGoto(LabelNode label) + { + nodes.add(JumpInstruction.jumpIfEqualZero(label)); + return this; + } + + public Block ifNotZeroGoto(LabelNode label) + { + nodes.add(JumpInstruction.jumpIfNotEqualZero(label)); + return this; + } + + public Block ifNullGoto(LabelNode label) + { + nodes.add(JumpInstruction.jumpIfNull(label)); + return this; + } + + public Block ifNotNullGoto(LabelNode label) + { + nodes.add(JumpInstruction.jumpIfNotNull(label)); + return this; + } + + public Block intAdd() + { + nodes.add(OpCode.IADD); + return this; + } + + public Block longAdd() + { + nodes.add(OpCode.LADD); + return this; + } + + public Block longCompare() + { + nodes.add(OpCode.LCMP); + return this; + } + + /** + * Compare two doubles. If either is NaN comparison is -1. + */ + public Block doubleCompareNanLess() + { + nodes.add(OpCode.DCMPL); + return this; + } + + /** + * Compare two doubles. If either is NaN comparison is 1. + */ + public Block doubleCompareNanGreater() + { + nodes.add(OpCode.DCMPG); + return this; + } + + public Block intLeftShift() + { + nodes.add(OpCode.ISHL); + return this; + } + + public Block intRightShift() + { + nodes.add(OpCode.ISHR); + return this; + } + + public Block longLeftShift() + { + nodes.add(OpCode.LSHL); + return this; + } + + public Block longRightShift() + { + nodes.add(OpCode.LSHR); + return this; + } + + public Block unsignedIntRightShift() + { + nodes.add(OpCode.IUSHR); + return this; + } + + public Block unsignedLongRightShift() + { + nodes.add(OpCode.LUSHR); + return this; + } + + public Block intBitAnd() + { + nodes.add(OpCode.IAND); + return this; + } + + public Block intBitOr() + { + nodes.add(OpCode.IOR); + return this; + } + + public Block intBitXor() + { + nodes.add(OpCode.IXOR); + return this; + } + + public Block longBitAnd() + { + nodes.add(OpCode.LAND); + return this; + } + + public Block longBitOr() + { + nodes.add(OpCode.LOR); + return this; + } + + public Block longBitXor() + { + nodes.add(OpCode.LXOR); + return this; + } + + public Block intNegate() + { + nodes.add(OpCode.INEG); + return this; + } + + public Block intToLong() + { + nodes.add(OpCode.I2L); + return this; + } + + public Block longNegate() + { + nodes.add(OpCode.LNEG); + return this; + } + + public Block longToInt() + { + nodes.add(OpCode.L2I); + return this; + } + + public Block isInstanceOf(Class type) + { + nodes.add(instanceOf(type)); + return this; + } + + public Block isInstanceOf(ParameterizedType type) + { + nodes.add(instanceOf(type)); + return this; + } + + public Block checkCast(Class type) + { + nodes.add(cast(type)); + return this; + } + + public Block checkCast(ParameterizedType type) + { + nodes.add(cast(type)); + return this; + } + + public Block invokeStatic(Method method) + { + nodes.add(InvokeInstruction.invokeStatic(method)); + return this; + } + + public Block invokeStatic(MethodDefinition method) + { + nodes.add(InvokeInstruction.invokeStatic(method)); + return this; + } + + public Block invokeStatic(Class type, String name, Class returnType, Class... parameterTypes) + { + nodes.add(InvokeInstruction.invokeStatic(type, name, returnType, parameterTypes)); + return this; + } + + public Block invokeStatic(Class type, String name, Class returnType, Iterable> parameterTypes) + { + nodes.add(InvokeInstruction.invokeStatic(type, name, returnType, parameterTypes)); + return this; + } + + public Block invokeStatic(ParameterizedType type, String name, ParameterizedType returnType, ParameterizedType... parameterTypes) + { + nodes.add(InvokeInstruction.invokeStatic(type, name, returnType, parameterTypes)); + return this; + } + + public Block invokeStatic(ParameterizedType type, String name, ParameterizedType returnType, Iterable parameterTypes) + { + nodes.add(InvokeInstruction.invokeStatic(type, name, returnType, parameterTypes)); + return this; + } + + public Block invokeVirtual(Method method) + { + nodes.add(InvokeInstruction.invokeVirtual(method)); + return this; + } + + public Block invokeVirtual(MethodDefinition method) + { + nodes.add(InvokeInstruction.invokeVirtual(method)); + return this; + } + + public Block invokeVirtual(Class type, String name, Class returnType, Class... parameterTypes) + { + nodes.add(InvokeInstruction.invokeVirtual(type, name, returnType, parameterTypes)); + return this; + } + + public Block invokeVirtual(Class type, String name, Class returnType, Iterable> parameterTypes) + { + nodes.add(InvokeInstruction.invokeVirtual(type, name, returnType, parameterTypes)); + return this; + } + + public Block invokeVirtual(ParameterizedType type, String name, ParameterizedType returnType, ParameterizedType... parameterTypes) + { + nodes.add(InvokeInstruction.invokeVirtual(type, name, returnType, parameterTypes)); + return this; + } + + public Block invokeVirtual(ParameterizedType type, String name, ParameterizedType returnType, Iterable parameterTypes) + { + nodes.add(InvokeInstruction.invokeVirtual(type, name, returnType, parameterTypes)); + return this; + } + + public Block invokeInterface(Method method) + { + nodes.add(InvokeInstruction.invokeInterface(method)); + return this; + } + + public Block invokeInterface(MethodDefinition method) + { + nodes.add(InvokeInstruction.invokeInterface(method)); + return this; + } + + public Block invokeInterface(Class type, String name, Class returnType, Class... parameterTypes) + { + nodes.add(InvokeInstruction.invokeInterface(type, name, returnType, parameterTypes)); + return this; + } + + public Block invokeInterface(Class type, String name, Class returnType, Iterable> parameterTypes) + { + nodes.add(InvokeInstruction.invokeInterface(type, name, returnType, parameterTypes)); + return this; + } + + public Block invokeInterface(ParameterizedType type, String name, ParameterizedType returnType, ParameterizedType... parameterTypes) + { + nodes.add(InvokeInstruction.invokeInterface(type, name, returnType, parameterTypes)); + return this; + } + + public Block invokeInterface(ParameterizedType type, String name, ParameterizedType returnType, Iterable parameterTypes) + { + nodes.add(InvokeInstruction.invokeInterface(type, name, returnType, parameterTypes)); + return this; + } + + public Block invokeConstructor(Constructor constructor) + { + nodes.add(InvokeInstruction.invokeConstructor(constructor)); + return this; + } + + public Block invokeConstructor(Class type, Class... parameterTypes) + { + nodes.add(InvokeInstruction.invokeConstructor(type, parameterTypes)); + return this; + } + + public Block invokeConstructor(Class type, Iterable> parameterTypes) + { + nodes.add(InvokeInstruction.invokeConstructor(type, parameterTypes)); + return this; + } + + public Block invokeConstructor(ParameterizedType type, ParameterizedType... parameterTypes) + { + nodes.add(InvokeInstruction.invokeConstructor(type, parameterTypes)); + return this; + } + + public Block invokeConstructor(ParameterizedType type, Iterable parameterTypes) + { + nodes.add(InvokeInstruction.invokeConstructor(type, parameterTypes)); + return this; + } + + public Block invokeSpecial(Method method) + { + nodes.add(InvokeInstruction.invokeSpecial(method)); + return this; + } + + public Block invokeSpecial(MethodDefinition method) + { + nodes.add(InvokeInstruction.invokeSpecial(method)); + return this; + } + + public Block invokeSpecial(Class type, String name, Class returnType, Class... parameterTypes) + { + nodes.add(InvokeInstruction.invokeSpecial(type, name, returnType, parameterTypes)); + return this; + } + + public Block invokeSpecial(Class type, String name, Class returnType, Iterable> parameterTypes) + { + nodes.add(InvokeInstruction.invokeSpecial(type, name, returnType, parameterTypes)); + return this; + } + + public Block invokeSpecial(ParameterizedType type, String name, ParameterizedType returnType, ParameterizedType... parameterTypes) + { + nodes.add(InvokeInstruction.invokeSpecial(type, name, returnType, parameterTypes)); + return this; + } + + public Block invokeSpecial(ParameterizedType type, String name, ParameterizedType returnType, Iterable parameterTypes) + { + nodes.add(InvokeInstruction.invokeSpecial(type, name, returnType, parameterTypes)); + return this; + } + + public Block invokeDynamic(String name, MethodType methodType, Method bootstrapMethod, Object... defaultBootstrapArguments) + { + nodes.add(InvokeInstruction.invokeDynamic(name, methodType, bootstrapMethod, defaultBootstrapArguments)); + return this; + } + + public ByteCodeNode invokeDynamic(String name, + ParameterizedType returnType, + Iterable parameterTypes, + Method bootstrapMethod, + List bootstrapArgs) + { + nodes.add(InvokeInstruction.invokeDynamic(name, returnType, parameterTypes, bootstrapMethod, bootstrapArgs)); + return this; + } + + public Block ret(Class type) + { + if (type == long.class) { + retLong(); + } + else if (type == boolean.class) { + retBoolean(); + } + else if (type == int.class || type == byte.class || type == char.class || type == short.class) { + retInt(); + } + else if (type == float.class) { + retFloat(); + } + else if (type == double.class) { + retDouble(); + } + else if (type == void.class) { + ret(); + } + else if (!type.isPrimitive()) { + retObject(); + } + else { + throw new IllegalArgumentException("Unsupported type: " + type.getName()); + } + + return this; + } + + public Block ret() + { + nodes.add(OpCode.RETURN); + return this; + } + + public Block retObject() + { + nodes.add(OpCode.ARETURN); + return this; + } + + public Block retFloat() + { + nodes.add(OpCode.FRETURN); + return this; + } + + public Block retDouble() + { + nodes.add(OpCode.DRETURN); + return this; + } + + public Block retBoolean() + { + nodes.add(OpCode.IRETURN); + return this; + } + + public Block retLong() + { + nodes.add(OpCode.LRETURN); + return this; + } + + public Block retInt() + { + nodes.add(OpCode.IRETURN); + return this; + } + + public Block throwObject() + { + nodes.add(OpCode.ATHROW); + return this; + } + + public Block newObject(Class type) + { + nodes.add(TypeInstruction.newObject(type)); + return this; + } + + public Block newObject(ParameterizedType type) + { + nodes.add(TypeInstruction.newObject(type)); + return this; + } + + public Block newArray(Class type) + { + nodes.add(TypeInstruction.newObjectArray(type)); + return this; + } + + public Block dup() + { + nodes.add(OpCode.DUP); + return this; + } + + public Block dup(Class type) + { + if (type == long.class || type == double.class) { + nodes.add(OpCode.DUP2); + } + else { + nodes.add(OpCode.DUP); + } + return this; + } + + public Block pop() + { + nodes.add(OpCode.POP); + return this; + } + + public Block pop(Class type) + { + if (type == long.class || type == double.class) { + nodes.add(OpCode.POP2); + } + else if (type != void.class) { + nodes.add(OpCode.POP); + } + return this; + } + + public Block pop(ParameterizedType type) + { + Class primitiveType = type.getPrimitiveType(); + if (primitiveType == long.class || primitiveType == double.class) { + nodes.add(OpCode.POP2); + } + else if (primitiveType != void.class) { + nodes.add(OpCode.POP); + } + return this; + } + + public Block swap() + { + nodes.add(OpCode.SWAP); + return this; + } + + // + // Fields (non-static) + // + + public Block getField(Field field) + { + return getField(field.getDeclaringClass(), field.getName(), field.getType()); + } + + public Block getField(FieldDefinition field) + { + getField(field.getDeclaringClass().getType(), field.getName(), field.getType()); + return this; + } + + public Block getField(Class target, String fieldName, Class fieldType) + { + getField(type(target), fieldName, type(fieldType)); + return this; + } + + public Block getField(ParameterizedType target, String fieldName, ParameterizedType fieldType) + { + nodes.add(getFieldInstruction(target, fieldName, fieldType)); + return this; + } + + public Block putField(Field field) + { + return putField(field.getDeclaringClass(), field.getName(), field.getType()); + } + + public Block putField(Class target, String fieldName, Class fieldType) + { + putField(type(target), fieldName, type(fieldType)); + return this; + } + + public Block putField(FieldDefinition field) + { + checkArgument(!field.getAccess().contains(STATIC), "Field is static: %s", field); + putField(field.getDeclaringClass().getType(), field.getName(), field.getType()); + return this; + } + + public Block putField(ParameterizedType target, String fieldName, ParameterizedType fieldType) + { + nodes.add(putFieldInstruction(target, fieldName, fieldType)); + return this; + } + + // + // Static fields + // + + public Block getStaticField(FieldDefinition field) + { + getStaticField(field.getDeclaringClass().getType(), field.getName(), field.getType()); + return this; + } + + public Block getStaticField(Field field) + { + checkArgument(Modifier.isStatic(field.getModifiers()), "Field is not static: %s", field); + getStaticField(type(field.getDeclaringClass()), field.getName(), type(field.getType())); + return this; + } + + public Block getStaticField(Class target, String fieldName, Class fieldType) + { + nodes.add(getStaticInstruction(target, fieldName, fieldType)); + return this; + } + + public Block getStaticField(ParameterizedType target, String fieldName, ParameterizedType fieldType) + { + nodes.add(getStaticInstruction(target, fieldName, fieldType)); + return this; + } + + public Block getStaticField(ParameterizedType target, FieldDefinition field) + { + nodes.add(getStaticInstruction(target, field.getName(), field.getType())); + return this; + } + + public Block putStaticField(FieldDefinition field) + { + putStaticField(field.getDeclaringClass().getType(), field.getName(), field.getType()); + return this; + } + + public Block putStaticField(ParameterizedType target, FieldDefinition field) + { + checkArgument(field.getAccess().contains(STATIC), "Field is not static: %s", field); + putStaticField(target, field.getName(), field.getType()); + return this; + } + + public Block putStaticField(ParameterizedType target, String fieldName, ParameterizedType fieldType) + { + nodes.add(putStaticInstruction(target, fieldName, fieldType)); + return this; + } + + // + // Load constants + // + + public Block pushNull() + { + nodes.add(OpCode.ACONST_NULL); + return this; + } + + public Block push(Class type) + { + nodes.add(loadClass(type)); + return this; + } + + public Block push(ParameterizedType type) + { + nodes.add(loadClass(type)); + return this; + } + + public Block push(String value) + { + nodes.add(Constant.loadString(value)); + return this; + } + + public Block push(Number value) + { + nodes.add(loadNumber(value)); + return this; + } + + public Block push(int value) + { + nodes.add(loadInt(value)); + return this; + } + + public Block push(boolean value) + { + nodes.add(loadBoolean(value)); + return this; + } + + public Block pushJavaDefault(Class type) + { + if (type == void.class) { + return this; + } + if (type == boolean.class || type == byte.class || type == char.class || type == short.class || type == int.class) { + return push(0); + } + if (type == long.class) { + return push(0L); + } + if (type == float.class) { + return push(0.0f); + } + if (type == double.class) { + return push(0.0d); + } + return pushNull(); + } + + public Block initializeVariable(Variable variable) + { + ParameterizedType type = variable.getType(); + if (type.getType().length() == 1) { + switch (type.getType().charAt(0)) { + case 'B': + case 'Z': + case 'S': + case 'C': + case 'I': + nodes.add(loadInt(0)); + break; + case 'F': + nodes.add(loadFloat(0)); + break; + case 'D': + nodes.add(loadDouble(0)); + break; + case 'J': + nodes.add(loadLong(0)); + break; + default: + checkArgument(false, "Unknown type '%s'", variable.getType()); + } + } + else { + nodes.add(Constant.loadNull()); + } + + nodes.add(storeVariable(variable)); + + return this; + } + + public Block getVariable(Variable variable) + { + nodes.add(loadVariable(variable)); + return this; + } + + public Block putVariable(Variable variable) + { + nodes.add(storeVariable(variable)); + return this; + } + + public Block putVariable(Variable variable, Class type) + { + nodes.add(loadClass(type)); + putVariable(variable); + return this; + } + + public Block putVariable(Variable variable, ParameterizedType type) + { + nodes.add(loadClass(type)); + putVariable(variable); + return this; + } + + public Block putVariable(Variable variable, String value) + { + nodes.add(Constant.loadString(value)); + putVariable(variable); + return this; + } + + public Block putVariable(Variable variable, Number value) + { + nodes.add(loadNumber(value)); + putVariable(variable); + return this; + } + + public Block putVariable(Variable variable, int value) + { + nodes.add(loadInt(value)); + putVariable(variable); + return this; + } + + public Block putVariable(Variable variable, boolean value) + { + nodes.add(loadBoolean(value)); + putVariable(variable); + return this; + } + + public Block incrementVariable(Variable variable, byte increment) + { + String type = variable.getType().getClassName(); + checkArgument(ImmutableList.of("byte", "short", "int").contains(type), "variable must be an byte, short or int, but is %s", type); + nodes.add(VariableInstruction.incrementVariable(variable, increment)); + return this; + } + + public Block getObjectArrayElement() + { + nodes.add(OpCode.AALOAD); + return this; + } + + public Block putObjectArrayElement() + { + nodes.add(OpCode.AASTORE); + return this; + } + + public Block visitLineNumber(int currentLineNumber) + { + checkArgument(currentLineNumber >= 0, "currentLineNumber must be positive"); + if (this.currentLineNumber != currentLineNumber) { + nodes.add(new LineNumberNode(currentLineNumber)); + this.currentLineNumber = currentLineNumber; + } + return this; + } + + @Override + public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) + { + for (ByteCodeNode node : nodes) { + node.accept(visitor, generationContext); + } + } + + @Override + public T accept(ByteCodeNode parent, ByteCodeVisitor visitor) + { + return visitor.visitBlock(parent, this); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/ByteCodeNode.java b/presto-main/src/main/java/com/facebook/presto/byteCode/ByteCodeNode.java new file mode 100644 index 00000000..e5d08c03 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/ByteCodeNode.java @@ -0,0 +1,27 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode; + +import org.objectweb.asm.MethodVisitor; + +import java.util.List; + +public interface ByteCodeNode +{ + List getChildNodes(); + + void accept(MethodVisitor visitor, MethodGenerationContext generationContext); + + T accept(ByteCodeNode parent, ByteCodeVisitor visitor); +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/ByteCodeVisitor.java b/presto-main/src/main/java/com/facebook/presto/byteCode/ByteCodeVisitor.java new file mode 100644 index 00000000..39e79f28 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/ByteCodeVisitor.java @@ -0,0 +1,327 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode; + +import com.facebook.presto.byteCode.control.DoWhileLoop; +import com.facebook.presto.byteCode.control.FlowControl; +import com.facebook.presto.byteCode.control.ForLoop; +import com.facebook.presto.byteCode.control.IfStatement; +import com.facebook.presto.byteCode.control.LookupSwitch; +import com.facebook.presto.byteCode.control.TryCatch; +import com.facebook.presto.byteCode.control.WhileLoop; +import com.facebook.presto.byteCode.debug.DebugNode; +import com.facebook.presto.byteCode.debug.LineNumberNode; +import com.facebook.presto.byteCode.debug.LocalVariableNode; +import com.facebook.presto.byteCode.expression.ByteCodeExpression; +import com.facebook.presto.byteCode.instruction.Constant; +import com.facebook.presto.byteCode.instruction.Constant.BooleanConstant; +import com.facebook.presto.byteCode.instruction.Constant.BoxedBooleanConstant; +import com.facebook.presto.byteCode.instruction.Constant.BoxedDoubleConstant; +import com.facebook.presto.byteCode.instruction.Constant.BoxedFloatConstant; +import com.facebook.presto.byteCode.instruction.Constant.BoxedIntegerConstant; +import com.facebook.presto.byteCode.instruction.Constant.BoxedLongConstant; +import com.facebook.presto.byteCode.instruction.Constant.ClassConstant; +import com.facebook.presto.byteCode.instruction.Constant.DoubleConstant; +import com.facebook.presto.byteCode.instruction.Constant.FloatConstant; +import com.facebook.presto.byteCode.instruction.Constant.IntConstant; +import com.facebook.presto.byteCode.instruction.Constant.LongConstant; +import com.facebook.presto.byteCode.instruction.Constant.StringConstant; +import com.facebook.presto.byteCode.instruction.FieldInstruction; +import com.facebook.presto.byteCode.instruction.FieldInstruction.GetFieldInstruction; +import com.facebook.presto.byteCode.instruction.FieldInstruction.PutFieldInstruction; +import com.facebook.presto.byteCode.instruction.InstructionNode; +import com.facebook.presto.byteCode.instruction.InvokeInstruction; +import com.facebook.presto.byteCode.instruction.InvokeInstruction.InvokeDynamicInstruction; +import com.facebook.presto.byteCode.instruction.JumpInstruction; +import com.facebook.presto.byteCode.instruction.LabelNode; +import com.facebook.presto.byteCode.instruction.VariableInstruction; +import com.facebook.presto.byteCode.instruction.VariableInstruction.IncrementVariableInstruction; +import com.facebook.presto.byteCode.instruction.VariableInstruction.LoadVariableInstruction; +import com.facebook.presto.byteCode.instruction.VariableInstruction.StoreVariableInstruction; + +public class ByteCodeVisitor +{ + public T visitClass(ClassDefinition classDefinition) + { + for (AnnotationDefinition annotationDefinition : classDefinition.getAnnotations()) { + visitAnnotation(classDefinition, annotationDefinition); + } + for (FieldDefinition fieldDefinition : classDefinition.getFields()) { + visitField(classDefinition, fieldDefinition); + } + for (MethodDefinition methodDefinition : classDefinition.getMethods()) { + visitMethod(classDefinition, methodDefinition); + } + return null; + } + + public T visitAnnotation(Object parent, AnnotationDefinition annotationDefinition) + { + return null; + } + + public T visitField(ClassDefinition classDefinition, FieldDefinition fieldDefinition) + { + for (AnnotationDefinition annotationDefinition : fieldDefinition.getAnnotations()) { + visitAnnotation(fieldDefinition, annotationDefinition); + } + return null; + } + + public T visitMethod(ClassDefinition classDefinition, MethodDefinition methodDefinition) + { + for (AnnotationDefinition annotationDefinition : methodDefinition.getAnnotations()) { + visitAnnotation(methodDefinition, annotationDefinition); + } + methodDefinition.getBody().accept(null, this); + return null; + } + + public T visitNode(ByteCodeNode parent, ByteCodeNode node) + { + for (ByteCodeNode byteCodeNode : node.getChildNodes()) { + byteCodeNode.accept(node, this); + } + return null; + } + + // + // Comment + // + + public T visitComment(ByteCodeNode parent, Comment node) + { + return visitNode(parent, node); + } + + // + // Block + // + + public T visitBlock(ByteCodeNode parent, Block block) + { + return visitNode(parent, block); + } + + // + // Byte Code Expression + // + public T visitByteCodeExpression(ByteCodeNode parent, ByteCodeExpression byteCodeExpression) + { + return visitNode(parent, byteCodeExpression); + } + + // + // Flow Control + // + + public T visitFlowControl(ByteCodeNode parent, FlowControl flowControl) + { + return visitNode(parent, flowControl); + } + + public T visitTryCatch(ByteCodeNode parent, TryCatch tryCatch) + { + return visitFlowControl(parent, tryCatch); + } + + public T visitIf(ByteCodeNode parent, IfStatement ifStatement) + { + return visitFlowControl(parent, ifStatement); + } + + public T visitFor(ByteCodeNode parent, ForLoop forLoop) + { + return visitFlowControl(parent, forLoop); + } + + public T visitWhile(ByteCodeNode parent, WhileLoop whileLoop) + { + return visitFlowControl(parent, whileLoop); + } + + public T visitDoWhile(ByteCodeNode parent, DoWhileLoop doWhileLoop) + { + return visitFlowControl(parent, doWhileLoop); + } + + public T visitLookupSwitch(ByteCodeNode parent, LookupSwitch lookupSwitch) + { + return visitFlowControl(parent, lookupSwitch); + } + + // + // Instructions + // + + public T visitInstruction(ByteCodeNode parent, InstructionNode node) + { + return visitNode(parent, node); + } + + public T visitLabel(ByteCodeNode parent, LabelNode labelNode) + { + return visitInstruction(parent, labelNode); + } + + public T visitJumpInstruction(ByteCodeNode parent, JumpInstruction jumpInstruction) + { + return visitInstruction(parent, jumpInstruction); + } + + // + // Constants + // + + public T visitConstant(ByteCodeNode parent, Constant constant) + { + return visitInstruction(parent, constant); + } + + public T visitBoxedBooleanConstant(ByteCodeNode parent, BoxedBooleanConstant boxedBooleanConstant) + { + return visitConstant(parent, boxedBooleanConstant); + } + + public T visitBooleanConstant(ByteCodeNode parent, BooleanConstant booleanConstant) + { + return visitConstant(parent, booleanConstant); + } + + public T visitIntConstant(ByteCodeNode parent, IntConstant intConstant) + { + return visitConstant(parent, intConstant); + } + + public T visitBoxedIntegerConstant(ByteCodeNode parent, BoxedIntegerConstant boxedIntegerConstant) + { + return visitConstant(parent, boxedIntegerConstant); + } + + public T visitFloatConstant(ByteCodeNode parent, FloatConstant floatConstant) + { + return visitConstant(parent, floatConstant); + } + + public T visitBoxedFloatConstant(ByteCodeNode parent, BoxedFloatConstant boxedFloatConstant) + { + return visitConstant(parent, boxedFloatConstant); + } + + public T visitLongConstant(ByteCodeNode parent, LongConstant longConstant) + { + return visitConstant(parent, longConstant); + } + + public T visitBoxedLongConstant(ByteCodeNode parent, BoxedLongConstant boxedLongConstant) + { + return visitConstant(parent, boxedLongConstant); + } + + public T visitDoubleConstant(ByteCodeNode parent, DoubleConstant doubleConstant) + { + return visitConstant(parent, doubleConstant); + } + + public T visitBoxedDoubleConstant(ByteCodeNode parent, BoxedDoubleConstant boxedDoubleConstant) + { + return visitConstant(parent, boxedDoubleConstant); + } + + public T visitStringConstant(ByteCodeNode parent, StringConstant stringConstant) + { + return visitConstant(parent, stringConstant); + } + + public T visitClassConstant(ByteCodeNode parent, ClassConstant classConstant) + { + return visitConstant(parent, classConstant); + } + + // + // Local Variable Instructions + // + + public T visitVariableInstruction(ByteCodeNode parent, VariableInstruction variableInstruction) + { + return visitInstruction(parent, variableInstruction); + } + + public T visitLoadVariable(ByteCodeNode parent, LoadVariableInstruction loadVariableInstruction) + { + return visitVariableInstruction(parent, loadVariableInstruction); + } + + public T visitStoreVariable(ByteCodeNode parent, StoreVariableInstruction storeVariableInstruction) + { + return visitVariableInstruction(parent, storeVariableInstruction); + } + + public T visitIncrementVariable(ByteCodeNode parent, IncrementVariableInstruction incrementVariableInstruction) + { + return visitVariableInstruction(parent, incrementVariableInstruction); + } + + // + // Field Instructions + // + + public T visitFieldInstruction(ByteCodeNode parent, FieldInstruction fieldInstruction) + { + return visitInstruction(parent, fieldInstruction); + } + + public T visitGetField(ByteCodeNode parent, GetFieldInstruction getFieldInstruction) + { + return visitFieldInstruction(parent, getFieldInstruction); + } + + public T visitPutField(ByteCodeNode parent, PutFieldInstruction putFieldInstruction) + { + return visitFieldInstruction(parent, putFieldInstruction); + } + + // + // Invoke + // + + public T visitInvoke(ByteCodeNode parent, InvokeInstruction invokeInstruction) + { + return visitInstruction(parent, invokeInstruction); + } + + public T visitInvokeDynamic(ByteCodeNode parent, InvokeDynamicInstruction invokeDynamicInstruction) + { + return visitInvoke(parent, invokeDynamicInstruction); + } + + // + // Debug + // + + public T visitDebug(ByteCodeNode parent, DebugNode debugNode) + { + return visitNode(parent, debugNode); + } + + public T visitLineNumber(ByteCodeNode parent, LineNumberNode lineNumberNode) + { + return visitDebug(parent, lineNumberNode); + } + + public T visitLocalVariable(ByteCodeNode parent, LocalVariableNode localVariableNode) + { + return visitDebug(parent, localVariableNode); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/ClassDefinition.java b/presto-main/src/main/java/com/facebook/presto/byteCode/ClassDefinition.java new file mode 100644 index 00000000..279873c7 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/ClassDefinition.java @@ -0,0 +1,302 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode; + +import com.google.common.base.Joiner; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import org.objectweb.asm.ClassVisitor; + +import javax.annotation.concurrent.NotThreadSafe; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; + +import static com.facebook.presto.byteCode.Access.STATIC; +import static com.facebook.presto.byteCode.Access.a; +import static com.facebook.presto.byteCode.Access.toAccessModifier; +import static com.google.common.collect.Iterables.any; +import static com.google.common.collect.Iterables.concat; +import static org.objectweb.asm.Opcodes.ACC_SUPER; +import static org.objectweb.asm.Opcodes.V1_7; + +@NotThreadSafe +public class ClassDefinition +{ + private final EnumSet access; + private final ParameterizedType type; + private final ParameterizedType superClass; + private final List interfaces = new ArrayList<>(); + private final List annotations = new ArrayList<>(); + private final List fields = new ArrayList<>(); + private final List methods = new ArrayList<>(); + private final MethodDefinition classInitializer; + private String source; + private String debug; + + public ClassDefinition( + EnumSet access, + String name, + ParameterizedType superClass, + ParameterizedType... interfaces) + { + this(access, new ParameterizedType(name), superClass, interfaces); + } + + public ClassDefinition( + EnumSet access, + ParameterizedType type, + ParameterizedType superClass, + ParameterizedType... interfaces) + { + Preconditions.checkNotNull(access, "access is null"); + Preconditions.checkNotNull(access, "access is null"); + Preconditions.checkNotNull(superClass, "superClass is null"); + Preconditions.checkNotNull(interfaces, "interfaces is null"); + + this.access = access; + this.type = type; + this.superClass = superClass; + this.interfaces.addAll(ImmutableList.copyOf(interfaces)); + + classInitializer = new MethodDefinition(this, a(STATIC), "", ParameterizedType.type(void.class), ImmutableList.of()); + } + + public Set getAccess() + { + return ImmutableSet.copyOf(access); + } + + public String getName() + { + return type.getClassName(); + } + + public ParameterizedType getType() + { + return type; + } + + public ParameterizedType getSuperClass() + { + return superClass; + } + + public String getSource() + { + return source; + } + + public List getInterfaces() + { + return ImmutableList.copyOf(interfaces); + } + + public List getAnnotations() + { + return ImmutableList.copyOf(annotations); + } + + public List getFields() + { + return ImmutableList.copyOf(fields); + } + + public List getMethods() + { + return ImmutableList.copyOf(methods); + } + + public void visit(ClassVisitor visitor) + { + // Generic signature if super class or any interface is generic + String signature = null; + if (superClass.isGeneric() || any(interfaces, ParameterizedType::isGeneric)) { + signature = genericClassSignature(superClass, interfaces); + } + + String[] interfaces = new String[this.interfaces.size()]; + for (int i = 0; i < interfaces.length; i++) { + interfaces[i] = this.interfaces.get(i).getClassName(); + } + visitor.visit(V1_7, toAccessModifier(access) | ACC_SUPER, type.getClassName(), signature, superClass.getClassName(), interfaces); + + // visit source + if (source != null) { + visitor.visitSource(source, debug); + } + + // visit annotations + for (AnnotationDefinition annotation : annotations) { + annotation.visitClassAnnotation(visitor); + } + + // visit fields + for (FieldDefinition field : fields) { + field.visit(visitor); + } + + // visit clinit method + classInitializer.visit(visitor, true); + + // visit methods + for (MethodDefinition method : methods) { + method.visit(visitor); + } + + // done + visitor.visitEnd(); + } + + public AnnotationDefinition declareAnnotation(Class type) + { + AnnotationDefinition annotationDefinition = new AnnotationDefinition(type); + annotations.add(annotationDefinition); + return annotationDefinition; + } + + public AnnotationDefinition declareAnnotation(ParameterizedType type) + { + AnnotationDefinition annotationDefinition = new AnnotationDefinition(type); + annotations.add(annotationDefinition); + return annotationDefinition; + } + + public FieldDefinition declareField(EnumSet access, String name, Class type) + { + FieldDefinition fieldDefinition = new FieldDefinition(this, access, name, type); + fields.add(fieldDefinition); + return fieldDefinition; + } + + public ClassDefinition addField(EnumSet access, String name, Class type) + { + declareField(access, name, type); + return this; + } + + public FieldDefinition declareField(EnumSet access, String name, ParameterizedType type) + { + FieldDefinition fieldDefinition = new FieldDefinition(this, access, name, type); + fields.add(fieldDefinition); + return fieldDefinition; + } + + public ClassDefinition addField(EnumSet access, String name, ParameterizedType type) + { + declareField(access, name, type); + return this; + } + + public ClassDefinition addField(FieldDefinition field) + { + fields.add(field); + return this; + } + + public MethodDefinition getClassInitializer() + { + return classInitializer; + } + + public MethodDefinition declareConstructor( + EnumSet access, + Parameter... parameters) + { + return declareMethod(access, "", ParameterizedType.type(void.class), ImmutableList.copyOf(parameters)); + } + + public MethodDefinition declareConstructor( + EnumSet access, + Iterable parameters) + { + return declareMethod(access, "", ParameterizedType.type(void.class), ImmutableList.copyOf(parameters)); + } + + public ClassDefinition declareDefaultConstructor(EnumSet access) + { + MethodDefinition constructor = declareConstructor(access); + constructor + .getBody() + .append(constructor.getThis()) + .invokeConstructor(superClass) + .ret(); + return this; + } + + public ClassDefinition addMethod(MethodDefinition method) + { + methods.add(method); + return this; + } + + public ClassDefinition visitSource(String source, String debug) + { + this.source = source; + this.debug = debug; + return this; + } + + public MethodDefinition declareMethod( + EnumSet access, + String name, + ParameterizedType returnType, + Parameter... parameters) + { + return declareMethod(access, name, returnType, ImmutableList.copyOf(parameters)); + } + + public MethodDefinition declareMethod( + EnumSet access, + String name, + ParameterizedType returnType, + Iterable parameters) + { + MethodDefinition methodDefinition = new MethodDefinition(this, access, name, returnType, parameters); + methods.add(methodDefinition); + return methodDefinition; + } + + public static String genericClassSignature( + ParameterizedType classType, + ParameterizedType... interfaceTypes + ) + { + return Joiner.on("").join( + concat(ImmutableList.of(classType), ImmutableList.copyOf(interfaceTypes)) + ); + } + + public static String genericClassSignature( + ParameterizedType classType, + List interfaceTypes + ) + { + return Joiner.on("").join(concat(ImmutableList.of(classType), interfaceTypes)); + } + + @Override + public String toString() + { + final StringBuilder sb = new StringBuilder(); + sb.append("ClassDefinition"); + sb.append("{access=").append(access); + sb.append(", type=").append(type); + sb.append('}'); + return sb.toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/ClassInfo.java b/presto-main/src/main/java/com/facebook/presto/byteCode/ClassInfo.java new file mode 100644 index 00000000..aefc3d5b --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/ClassInfo.java @@ -0,0 +1,186 @@ +/*** + * ASM tests + * Copyright (c) 2002-2005 France Telecom + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holders nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.facebook.presto.byteCode; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.MethodNode; + +import java.util.List; + +import static com.facebook.presto.byteCode.ParameterizedType.type; +import static com.facebook.presto.byteCode.ParameterizedType.typeFromPathName; +import static com.google.common.collect.Iterables.transform; +import static java.util.Arrays.asList; + +/** + * @author Eugene Kuleshov + */ +public class ClassInfo +{ + private final ClassInfoLoader loader; + private final ParameterizedType type; + private final int access; + private final ParameterizedType superClass; + private final List interfaces; + private final List methods; + + public ClassInfo(ClassInfoLoader loader, ClassNode classNode) + { + this(loader, + typeFromPathName(classNode.name), + classNode.access, + classNode.superName == null ? null : typeFromPathName(classNode.superName), + transform((List) classNode.interfaces, ParameterizedType::typeFromPathName), + (List) classNode.methods); + } + + public ClassInfo(ClassInfoLoader loader, Class aClass) + { + this(loader, + type(aClass), + aClass.getModifiers(), + aClass.getSuperclass() == null ? null : type(aClass.getSuperclass()), + transform(asList(aClass.getInterfaces()), ParameterizedType::type), + null); + } + + public ClassInfo(ClassInfoLoader loader, ParameterizedType type, int access, ParameterizedType superClass, Iterable interfaces, Iterable methods) + { + Preconditions.checkNotNull(loader, "loader is null"); + Preconditions.checkNotNull(type, "type is null"); + Preconditions.checkNotNull(interfaces, "interfaces is null"); + + this.loader = loader; + this.type = type; + this.access = access; + this.superClass = superClass; + this.interfaces = ImmutableList.copyOf(interfaces); + if (methods != null) { + this.methods = ImmutableList.copyOf(methods); + } + else { + this.methods = null; + } + } + + public ParameterizedType getType() + { + return type; + } + + public int getModifiers() + { + return access; + } + + public ClassInfo getSuperclass() + { + if (superClass == null) { + return null; + } + return loader.loadClassInfo(superClass); + } + + public List getInterfaces() + { + if (interfaces == null) { + return ImmutableList.of(); + } + ImmutableList.Builder builder = ImmutableList.builder(); + for (ParameterizedType anInterface : interfaces) { + builder.add(loader.loadClassInfo(anInterface)); + } + return builder.build(); + } + + public List getMethods() + { + Preconditions.checkState(methods != null, "Methods were not loaded for type %s", type); + return methods; + } + + boolean isInterface() + { + return (getModifiers() & Opcodes.ACC_INTERFACE) > 0; + } + + private boolean implementsInterface(ClassInfo that) + { + for (ClassInfo classInfo = this; classInfo != null; classInfo = classInfo.getSuperclass()) { + for (ClassInfo anInterface : classInfo.getInterfaces()) { + if (anInterface.type.equals(that.type) || anInterface.implementsInterface(that)) { + return true; + } + } + } + return false; + } + + private boolean isSubclassOf(ClassInfo that) + { + for (ClassInfo classInfo = this; classInfo != null; classInfo = classInfo.getSuperclass()) { + if (classInfo.getSuperclass() != null && + classInfo.getSuperclass().type.equals(that.type)) { + return true; + } + } + return false; + } + + public boolean isAssignableFrom(ClassInfo that) + { + if (this == that) { + return true; + } + + if (that.isSubclassOf(this)) { + return true; + } + + if (that.implementsInterface(this)) { + return true; + } + + if (that.isInterface() && getType().equals(type(Object.class))) { + return true; + } + + return false; + } + + @Override + public String toString() + { + return type.toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/ClassInfoLoader.java b/presto-main/src/main/java/com/facebook/presto/byteCode/ClassInfoLoader.java new file mode 100644 index 00000000..31f17029 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/ClassInfoLoader.java @@ -0,0 +1,154 @@ +/*** + * ASM tests + * Copyright (c) 2002-2005 France Telecom + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holders nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.facebook.presto.byteCode; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.util.CheckClassAdapter; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +import static com.facebook.presto.byteCode.ParameterizedType.typeFromPathName; + +public class ClassInfoLoader +{ + public static ClassInfoLoader createClassInfoLoader(Iterable classDefinitions, ClassLoader classLoader) + { + ImmutableMap.Builder classNodes = ImmutableMap.builder(); + for (ClassDefinition classDefinition : classDefinitions) { + ClassNode classNode = new ClassNode(); + classDefinition.visit(classNode); + classNodes.put(classDefinition.getType(), classNode); + } + return new ClassInfoLoader(classNodes.build(), ImmutableMap.of(), classLoader, true); + } + + public static ClassInfoLoader createByteCodeInfoLoader(Map byteCodes, ClassLoader classLoader, boolean loadMethodNodes) + { + return new ClassInfoLoader(ImmutableMap.of(), byteCodes, classLoader, loadMethodNodes); + } + + private final Map classNodes; + private final Map byteCodes; + private final ClassLoader classLoader; + private final Map classInfoCache = new HashMap<>(); + private final boolean loadMethodNodes; + + public ClassInfoLoader(Map classNodes, Map byteCodes, ClassLoader classLoader, boolean loadMethodNodes) + { + this.classNodes = ImmutableMap.copyOf(classNodes); + this.byteCodes = ImmutableMap.copyOf(byteCodes); + this.classLoader = classLoader; + this.loadMethodNodes = loadMethodNodes; + } + + public ClassInfo loadClassInfo(ParameterizedType type) + { + ClassInfo classInfo = classInfoCache.get(type); + if (classInfo == null) { + classInfo = readClassInfoQuick(type); + classInfoCache.put(type, classInfo); + } + return classInfo; + } + + private ClassInfo readClassInfoQuick(ParameterizedType type) + { + // check for user supplied class node + ClassNode classNode = classNodes.get(type); + if (classNode != null) { + return new ClassInfo(this, classNode); + } + + // check for user supplied byte code + ClassReader classReader; + byte[] byteCode = byteCodes.get(type); + if (byteCode != null) { + classReader = new ClassReader(byteCode); + } + else { + // load class file from class loader + String classFileName = type.getClassName() + ".class"; + try (InputStream is = classLoader.getResourceAsStream(classFileName)) { + classReader = new ClassReader(is); + } + catch (IOException e) { + // check if class is already loaded + try { + Class aClass = classLoader.loadClass(type.getJavaClassName()); + return new ClassInfo(this, aClass); + } + catch (ClassNotFoundException e1) { + throw new RuntimeException("Class not found " + type, e); + } + } + } + + if (loadMethodNodes) { + // slower version that loads all operations + classNode = new ClassNode(); + classReader.accept(new CheckClassAdapter(classNode, false), ClassReader.SKIP_DEBUG); + + return new ClassInfo(this, classNode); + } + else { + // optimized version + int header = classReader.header; + int access = classReader.readUnsignedShort(header); + + char[] buf = new char[2048]; + + // read super class name + int superClassIndex = classReader.getItem(classReader.readUnsignedShort(header + 4)); + ParameterizedType superClass; + if (superClassIndex == 0) { + superClass = null; + } + else { + superClass = typeFromPathName(classReader.readUTF8(superClassIndex, buf)); + } + + // read each interface name + int interfaceCount = classReader.readUnsignedShort(header + 6); + ImmutableList.Builder interfaces = ImmutableList.builder(); + header += 8; + for (int i = 0; i < interfaceCount; ++i) { + interfaces.add(typeFromPathName(classReader.readClass(header, buf))); + header += 2; + } + return new ClassInfo(this, type, access, superClass, interfaces.build(), null); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/Comment.java b/presto-main/src/main/java/com/facebook/presto/byteCode/Comment.java new file mode 100644 index 00000000..87a01406 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/Comment.java @@ -0,0 +1,63 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode; + +import com.facebook.presto.byteCode.instruction.InstructionNode; +import com.google.common.collect.ImmutableList; +import org.objectweb.asm.MethodVisitor; + +import java.util.List; + +import static com.google.common.base.MoreObjects.toStringHelper; + +public class Comment + implements InstructionNode +{ + protected final String comment; + + public Comment(String comment) + { + this.comment = comment; + } + + public String getComment() + { + return comment; + } + + @Override + public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) + { + } + + @Override + public String toString() + { + return toStringHelper(this) + .addValue(comment) + .toString(); + } + + @Override + public List getChildNodes() + { + return ImmutableList.of(); + } + + @Override + public T accept(ByteCodeNode parent, ByteCodeVisitor visitor) + { + return visitor.visitComment(parent, this); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/DumpByteCodeVisitor.java b/presto-main/src/main/java/com/facebook/presto/byteCode/DumpByteCodeVisitor.java new file mode 100644 index 00000000..4dbac38b --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/DumpByteCodeVisitor.java @@ -0,0 +1,612 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode; + +import com.facebook.presto.byteCode.control.CaseStatement; +import com.facebook.presto.byteCode.control.DoWhileLoop; +import com.facebook.presto.byteCode.control.ForLoop; +import com.facebook.presto.byteCode.control.IfStatement; +import com.facebook.presto.byteCode.control.LookupSwitch; +import com.facebook.presto.byteCode.control.TryCatch; +import com.facebook.presto.byteCode.control.WhileLoop; +import com.facebook.presto.byteCode.debug.LineNumberNode; +import com.facebook.presto.byteCode.expression.ByteCodeExpression; +import com.facebook.presto.byteCode.instruction.Constant.BooleanConstant; +import com.facebook.presto.byteCode.instruction.Constant.BoxedBooleanConstant; +import com.facebook.presto.byteCode.instruction.Constant.BoxedDoubleConstant; +import com.facebook.presto.byteCode.instruction.Constant.BoxedFloatConstant; +import com.facebook.presto.byteCode.instruction.Constant.BoxedIntegerConstant; +import com.facebook.presto.byteCode.instruction.Constant.BoxedLongConstant; +import com.facebook.presto.byteCode.instruction.Constant.ClassConstant; +import com.facebook.presto.byteCode.instruction.Constant.DoubleConstant; +import com.facebook.presto.byteCode.instruction.Constant.FloatConstant; +import com.facebook.presto.byteCode.instruction.Constant.IntConstant; +import com.facebook.presto.byteCode.instruction.Constant.LongConstant; +import com.facebook.presto.byteCode.instruction.Constant.StringConstant; +import com.facebook.presto.byteCode.instruction.InstructionNode; +import com.facebook.presto.byteCode.instruction.InvokeInstruction; +import com.facebook.presto.byteCode.instruction.InvokeInstruction.InvokeDynamicInstruction; +import com.facebook.presto.byteCode.instruction.JumpInstruction; +import com.facebook.presto.byteCode.instruction.LabelNode; +import com.facebook.presto.byteCode.instruction.VariableInstruction.IncrementVariableInstruction; +import com.facebook.presto.byteCode.instruction.VariableInstruction.LoadVariableInstruction; +import com.facebook.presto.byteCode.instruction.VariableInstruction.StoreVariableInstruction; +import com.google.common.base.Joiner; + +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import static com.facebook.presto.byteCode.ParameterizedType.type; + +public class DumpByteCodeVisitor + extends ByteCodeVisitor +{ + private final PrintStream out; + private int indentLevel; + + public DumpByteCodeVisitor(PrintStream out) + { + this.out = out; + } + + @Override + public Void visitClass(ClassDefinition classDefinition) + { + // print annotations first + for (AnnotationDefinition annotationDefinition : classDefinition.getAnnotations()) { + visitAnnotation(classDefinition, annotationDefinition); + } + + // print class declaration + Line classDeclaration = line().addAll(classDefinition.getAccess()).add("class").add(classDefinition.getType().getJavaClassName()); + if (!classDefinition.getSuperClass().equals(type(Object.class))) { + classDeclaration.add("extends").add(classDefinition.getSuperClass().getJavaClassName()); + } + if (!classDefinition.getInterfaces().isEmpty()) { + classDeclaration.add("implements"); + for (ParameterizedType interfaceType : classDefinition.getInterfaces()) { + classDeclaration.add(interfaceType.getJavaClassName()); + } + } + classDeclaration.print(); + + // print class body + printLine("{"); + indentLevel++; + + // print fields + for (FieldDefinition fieldDefinition : classDefinition.getFields()) { + visitField(classDefinition, fieldDefinition); + } + + // print methods + for (MethodDefinition methodDefinition : classDefinition.getMethods()) { + visitMethod(classDefinition, methodDefinition); + } + + indentLevel--; + printLine("}"); + printLine(); + return null; + } + + @Override + public Void visitAnnotation(Object parent, AnnotationDefinition annotationDefinition) + { + printLine("@%s", annotationDefinition.getType().getJavaClassName(), annotationDefinition.getValues()); + return null; + } + + @Override + public Void visitField(ClassDefinition classDefinition, FieldDefinition fieldDefinition) + { + // print annotations first + for (AnnotationDefinition annotationDefinition : fieldDefinition.getAnnotations()) { + visitAnnotation(fieldDefinition, annotationDefinition); + } + + // print field declaration + line().addAll(fieldDefinition.getAccess()).add(fieldDefinition.getType().getJavaClassName()).add(fieldDefinition.getName()).add(";").print(); + + printLine(); + return null; + } + + @Override + public Void visitMethod(ClassDefinition classDefinition, MethodDefinition methodDefinition) + { + if (methodDefinition.getComment() != null) { + printLine("// %s", methodDefinition.getComment()); + } + // print annotations first + for (AnnotationDefinition annotationDefinition : methodDefinition.getAnnotations()) { + visitAnnotation(methodDefinition, annotationDefinition); + } + + // print method declaration + printLine(methodDefinition.toSourceString()); + + // print body + methodDefinition.getBody().accept(null, this); + + printLine(); + return null; + } + + @Override + public Void visitComment(ByteCodeNode parent, Comment node) + { + printLine(); + printLine("// %s", node.getComment()); + return null; + } + + @Override + public Void visitBlock(ByteCodeNode parent, Block block) + { + // only indent if we have a block description or more than one child node + boolean indented; + if (block.getDescription() != null) { + line().add(block.getDescription()).add("{").print(); + indentLevel++; + indented = true; + } + else if (block.getChildNodes().size() > 1) { + printLine("{"); + indentLevel++; + indented = true; + } + else { + indented = false; + } + + visitBlockContents(block); + if (indented) { + indentLevel--; + printLine("}"); + } + + return null; + } + + private void visitBlockContents(Block block) + { + for (ByteCodeNode node : block.getChildNodes()) { + if (node instanceof Block) { + Block childBlock = (Block) node; + if (childBlock.getDescription() != null) { + visitBlock(block, childBlock); + } + else { + visitBlockContents(childBlock); + } + } + else { + node.accept(node, this); + } + } + } + + @Override + public Void visitByteCodeExpression(ByteCodeNode parent, ByteCodeExpression byteCodeExpression) + { + printLine(byteCodeExpression.toString()); + return null; + } + + @Override + public Void visitNode(ByteCodeNode parent, ByteCodeNode node) + { + printLine(node.toString()); + super.visitNode(parent, node); + return null; + } + + @Override + public Void visitLabel(ByteCodeNode parent, LabelNode labelNode) + { + printLine("%s:", labelNode.getName()); + return null; + } + + @Override + public Void visitJumpInstruction(ByteCodeNode parent, JumpInstruction jumpInstruction) + { + printLine("%s %s", jumpInstruction.getOpCode(), jumpInstruction.getLabel().getName()); + return null; + } + + // + // Variable + // + + @Override + public Void visitLoadVariable(ByteCodeNode parent, LoadVariableInstruction loadVariableInstruction) + { + Variable variable = loadVariableInstruction.getVariable(); + printLine("load %s", variable.getName()); + return null; + } + + @Override + public Void visitStoreVariable(ByteCodeNode parent, StoreVariableInstruction storeVariableInstruction) + { + Variable variable = storeVariableInstruction.getVariable(); + printLine("store %s)", variable.getName()); + return null; + } + + @Override + public Void visitIncrementVariable(ByteCodeNode parent, IncrementVariableInstruction incrementVariableInstruction) + { + Variable variable = incrementVariableInstruction.getVariable(); + byte increment = incrementVariableInstruction.getIncrement(); + printLine("increment %s %s", variable.getName(), increment); + return null; + } + + // + // Invoke + // + + @Override + public Void visitInvoke(ByteCodeNode parent, InvokeInstruction invokeInstruction) + { + printLine("invoke %s.%s%s", + invokeInstruction.getTarget().getJavaClassName(), + invokeInstruction.getName(), + invokeInstruction.getMethodDescription()); + return null; + } + + @Override + public Void visitInvokeDynamic(ByteCodeNode parent, InvokeDynamicInstruction invokeDynamicInstruction) + { + printLine("invokeDynamic %s%s %s", + invokeDynamicInstruction.getName(), + invokeDynamicInstruction.getMethodDescription(), + invokeDynamicInstruction.getBootstrapArguments()); + return null; + } + + // + // Control Flow + // + + @Override + public Void visitTryCatch(ByteCodeNode parent, TryCatch tryCatch) + { + if (tryCatch.getComment() != null) { + printLine(); + printLine("// %s", tryCatch.getComment()); + } + + printLine("try {"); + indentLevel++; + tryCatch.getTryNode().accept(tryCatch, this); + indentLevel--; + printLine("}"); + + printLine("catch (%s) {", tryCatch.getExceptionName()); + indentLevel++; + tryCatch.getCatchNode().accept(tryCatch, this); + indentLevel--; + printLine("}"); + + return null; + } + + @Override + public Void visitIf(ByteCodeNode parent, IfStatement ifStatement) + { + if (ifStatement.getComment() != null) { + printLine(); + printLine("// %s", ifStatement.getComment()); + } + printLine("if {"); + indentLevel++; + visitNestedNode("condition", ifStatement.condition(), ifStatement); + if (ifStatement.ifTrue() != null) { + visitNestedNode("ifTrue", ifStatement.ifTrue(), ifStatement); + } + if (ifStatement.ifFalse() != null) { + visitNestedNode("ifFalse", ifStatement.ifFalse(), ifStatement); + } + indentLevel--; + printLine("}"); + return null; + } + + @Override + public Void visitFor(ByteCodeNode parent, ForLoop forLoop) + { + if (forLoop.getComment() != null) { + printLine(); + printLine("// %s", forLoop.getComment()); + } + printLine("for {"); + indentLevel++; + visitNestedNode("initialize", forLoop.initialize(), forLoop); + visitNestedNode("condition", forLoop.condition(), forLoop); + visitNestedNode("update", forLoop.update(), forLoop); + visitNestedNode("body", forLoop.body(), forLoop); + indentLevel--; + printLine("}"); + return null; + } + + @Override + public Void visitWhile(ByteCodeNode parent, WhileLoop whileLoop) + { + if (whileLoop.getComment() != null) { + printLine(); + printLine("// %s", whileLoop.getComment()); + } + printLine("while {"); + indentLevel++; + visitNestedNode("condition", whileLoop.condition(), whileLoop); + visitNestedNode("body", whileLoop.body(), whileLoop); + indentLevel--; + printLine("}"); + return null; + } + + @Override + public Void visitDoWhile(ByteCodeNode parent, DoWhileLoop doWhileLoop) + { + if (doWhileLoop.getComment() != null) { + printLine(); + printLine("// %s", doWhileLoop.getComment()); + } + printLine("while {"); + indentLevel++; + visitNestedNode("body", doWhileLoop.body(), doWhileLoop); + visitNestedNode("condition", doWhileLoop.condition(), doWhileLoop); + indentLevel--; + printLine("}"); + return null; + } + + @Override + public Void visitLookupSwitch(ByteCodeNode parent, LookupSwitch lookupSwitch) + { + if (lookupSwitch.getComment() != null) { + printLine(); + printLine("// %s", lookupSwitch.getComment()); + } + printLine("switch {"); + indentLevel++; + for (CaseStatement caseStatement : lookupSwitch.getCases()) { + printLine("case %s: goto %s", caseStatement.getKey(), caseStatement.getLabel().getName()); + } + printLine("default: goto %s", lookupSwitch.getDefaultCase().getName()); + indentLevel--; + printLine("}"); + return null; + } + + // + // Instructions + // + + @Override + public Void visitInstruction(ByteCodeNode parent, InstructionNode node) + { + return super.visitInstruction(parent, node); + } + + // + // Constants + // + + @Override + public Void visitBoxedBooleanConstant(ByteCodeNode parent, BoxedBooleanConstant boxedBooleanConstant) + { + printLine("load constant %s", boxedBooleanConstant.getValue()); + return null; + } + + @Override + public Void visitBooleanConstant(ByteCodeNode parent, BooleanConstant booleanConstant) + { + printLine("load constant %s", booleanConstant.getValue()); + return null; + } + + @Override + public Void visitIntConstant(ByteCodeNode parent, IntConstant intConstant) + { + printLine("load constant %s", intConstant.getValue()); + return null; + } + + @Override + public Void visitBoxedIntegerConstant(ByteCodeNode parent, BoxedIntegerConstant boxedIntegerConstant) + { + printLine("load constant new Integer(%s)", boxedIntegerConstant.getValue()); + return null; + } + + @Override + public Void visitFloatConstant(ByteCodeNode parent, FloatConstant floatConstant) + { + printLine("load constant %sf", floatConstant.getValue()); + return null; + } + + @Override + public Void visitBoxedFloatConstant(ByteCodeNode parent, BoxedFloatConstant boxedFloatConstant) + { + printLine("load constant new Float(%sf)", boxedFloatConstant.getValue()); + return null; + } + + @Override + public Void visitLongConstant(ByteCodeNode parent, LongConstant longConstant) + { + printLine("load constant %sL", longConstant.getValue()); + return null; + } + + @Override + public Void visitBoxedLongConstant(ByteCodeNode parent, BoxedLongConstant boxedLongConstant) + { + printLine("load constant new Long(%sL)", boxedLongConstant.getValue()); + return null; + } + + @Override + public Void visitDoubleConstant(ByteCodeNode parent, DoubleConstant doubleConstant) + { + printLine("load constant %s", doubleConstant.getValue()); + return null; + } + + @Override + public Void visitBoxedDoubleConstant(ByteCodeNode parent, BoxedDoubleConstant boxedDoubleConstant) + { + printLine("load constant new Double(%s)", boxedDoubleConstant.getValue()); + return null; + } + + @Override + public Void visitStringConstant(ByteCodeNode parent, StringConstant stringConstant) + { + printLine("load constant \"%s\"", stringConstant.getValue()); + return null; + } + + @Override + public Void visitClassConstant(ByteCodeNode parent, ClassConstant classConstant) + { + printLine("load constant %s.class", classConstant.getValue().getJavaClassName()); + return null; + } + + // + // Line Number + // + + @Override + public Void visitLineNumber(ByteCodeNode parent, LineNumberNode lineNumberNode) + { + lineNumber = lineNumberNode.getLineNumber(); + printLine("LINE %s", lineNumber); + return null; + } + + // + // Print + // + + private int lineNumber = -1; + + public void printLine() + { + out.println(indent(indentLevel)); + } + + public void printLine(String line) + { + out.println(String.format("%s%s", indent(indentLevel), line)); + } + + public void printLine(String format, Object... args) + { + String line = String.format(format, args); + out.println(String.format("%s%s", indent(indentLevel), line)); + } + + public void printWords(String... words) + { + String line = Joiner.on(" ").join(words); + out.println(String.format("%s%s", indent(indentLevel), line)); + } + + private String indent(int level) + { + StringBuilder builder = new StringBuilder(); + +// if (lineNumber >= 0) { +// builder.append(String.format("%4s", lineNumber + ": ")); +// } else { +// builder.append(" "); +// } + + for (int i = 0; i < level; i++) { + builder.append(" "); + } + return builder.toString(); + } + + private void visitNestedNode(String description, ByteCodeNode node, ByteCodeNode parent) + { + printLine(description + " {"); + indentLevel++; + node.accept(parent, this); + indentLevel--; + printLine("}"); + } + + private Line line() + { + return new Line(); + } + + private Line line(String separator) + { + return new Line(separator); + } + + private class Line + { + private final String separator; + private final List parts = new ArrayList<>(); + + private Line() + { + separator = " "; + } + + private Line(String separator) + { + this.separator = separator; + } + + public Line add(Object element) + { + parts.add(element); + return this; + } + + public Line addAll(Collection c) + { + parts.addAll(c); + return this; + } + + public void print() + { + printLine(Joiner.on(separator).join(parts)); + } + + @Override + public String toString() + { + return Joiner.on(separator).join(parts); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/DynamicClassLoader.java b/presto-main/src/main/java/com/facebook/presto/byteCode/DynamicClassLoader.java new file mode 100644 index 00000000..bbae88e8 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/DynamicClassLoader.java @@ -0,0 +1,143 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode; + +import com.google.common.base.Preconditions; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Sets; +import com.google.common.collect.Sets.SetView; + +import java.lang.invoke.MethodHandle; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +public class DynamicClassLoader + extends ClassLoader +{ + private final ConcurrentMap pendingClasses = new ConcurrentHashMap<>(); + private final Map callSiteBindings; + + public DynamicClassLoader() + { + this(null); + } + + public DynamicClassLoader(ClassLoader parentClassLoader) + { + this(parentClassLoader, ImmutableMap.of()); + } + + public DynamicClassLoader(ClassLoader parentClassLoader, Map callSiteBindings) + { + super(resolveClassLoader(parentClassLoader)); + this.callSiteBindings = ImmutableMap.copyOf(callSiteBindings); + } + + public Class defineClass(String className, byte[] byteCode) + { + return defineClass(className, byteCode, 0, byteCode.length); + } + + public Map> defineClasses(Map newClasses) + { + SetView conflicts = Sets.intersection(pendingClasses.keySet(), newClasses.keySet()); + Preconditions.checkArgument(conflicts.isEmpty(), "The classes %s have already been defined", conflicts); + + pendingClasses.putAll(newClasses); + try { + Map> classes = new HashMap<>(); + for (String className : newClasses.keySet()) { + try { + Class clazz = loadClass(className); + classes.put(className, clazz); + } + catch (ClassNotFoundException e) { + // this should never happen + throw Throwables.propagate(e); + } + } + return classes; + } + finally { + pendingClasses.keySet().removeAll(newClasses.keySet()); + } + } + + public Map getCallSiteBindings() + { + return callSiteBindings; + } + + @Override + protected Class findClass(String name) + throws ClassNotFoundException + { + byte[] byteCode = pendingClasses.get(name); + if (byteCode == null) { + throw new ClassNotFoundException(name); + } + + return defineClass(name, byteCode); + } + + @Override + protected Class loadClass(String name, boolean resolve) + throws ClassNotFoundException + { + // grab the magic lock + synchronized (getClassLoadingLock(name)) { + // Check if class is in the loaded classes cache + Class cachedClass = findLoadedClass(name); + if (cachedClass != null) { + return resolveClass(cachedClass, resolve); + } + + try { + Class clazz = findClass(name); + return resolveClass(clazz, resolve); + } + catch (ClassNotFoundException ignored) { + // not a local class + } + + Class clazz = getParent().loadClass(name); + return resolveClass(clazz, resolve); + } + } + + private Class resolveClass(Class clazz, boolean resolve) + { + if (resolve) { + resolveClass(clazz); + } + return clazz; + } + + private static ClassLoader resolveClassLoader(ClassLoader parentClassLoader) + { + if (parentClassLoader == null) { + parentClassLoader = Thread.currentThread().getContextClassLoader(); + } + if (parentClassLoader == null) { + parentClassLoader = DynamicClassLoader.class.getClassLoader(); + } + if (parentClassLoader == null) { + parentClassLoader = ClassLoader.getSystemClassLoader(); + } + return parentClassLoader; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/FieldDefinition.java b/presto-main/src/main/java/com/facebook/presto/byteCode/FieldDefinition.java new file mode 100644 index 00000000..0daad743 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/FieldDefinition.java @@ -0,0 +1,123 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; + +import javax.annotation.concurrent.Immutable; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; + +import static com.facebook.presto.byteCode.Access.toAccessModifier; +import static com.facebook.presto.byteCode.ParameterizedType.type; + +@SuppressWarnings("UnusedDeclaration") +@Immutable +public class FieldDefinition +{ + private final ClassDefinition declaringClass; + private final ImmutableSet access; + private final String name; + private final ParameterizedType type; + private final List annotations = new ArrayList<>(); + + public FieldDefinition(ClassDefinition declaringClass, EnumSet access, String name, ParameterizedType type) + { + this.declaringClass = declaringClass; + this.access = Sets.immutableEnumSet(access); + this.name = name; + this.type = type; + } + + public FieldDefinition(ClassDefinition declaringClass, EnumSet access, String name, Class type) + { + this(declaringClass, access, name, type(type)); + } + + public ClassDefinition getDeclaringClass() + { + return declaringClass; + } + + public ImmutableSet getAccess() + { + return access; + } + + public String getName() + { + return name; + } + + public ParameterizedType getType() + { + return type; + } + + public List getAnnotations() + { + return ImmutableList.copyOf(annotations); + } + + public AnnotationDefinition declareAnnotation(Class type) + { + AnnotationDefinition annotationDefinition = new AnnotationDefinition(type); + annotations.add(annotationDefinition); + return annotationDefinition; + } + + public AnnotationDefinition declareAnnotation(ParameterizedType type) + { + AnnotationDefinition annotationDefinition = new AnnotationDefinition(type); + annotations.add(annotationDefinition); + return annotationDefinition; + } + + public void visit(ClassVisitor visitor) + { + FieldVisitor fieldVisitor = visitor.visitField(toAccessModifier(access), + name, + type.getType(), + type.getGenericSignature(), + null); + + if (fieldVisitor == null) { + return; + } + + for (AnnotationDefinition annotation : annotations) { + annotation.visitFieldAnnotation(fieldVisitor); + } + + fieldVisitor.visitEnd(); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("FieldDefinition"); + sb.append("{access=").append(access); + sb.append(", name='").append(name).append('\''); + sb.append(", type=").append(type); + sb.append('}'); + return sb.toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/MethodDefinition.java b/presto-main/src/main/java/com/facebook/presto/byteCode/MethodDefinition.java new file mode 100644 index 00000000..bdbe83e5 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/MethodDefinition.java @@ -0,0 +1,327 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode; + +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.tree.InsnNode; + +import javax.annotation.concurrent.NotThreadSafe; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import java.util.Optional; + +import static com.facebook.presto.byteCode.Access.STATIC; +import static com.facebook.presto.byteCode.Access.toAccessModifier; +import static com.facebook.presto.byteCode.ParameterizedType.type; +import static com.google.common.collect.Iterables.transform; +import static org.objectweb.asm.Opcodes.RETURN; + +@SuppressWarnings("UnusedDeclaration") +@NotThreadSafe +public class MethodDefinition +{ + private final Scope scope; + private final ClassDefinition declaringClass; + private final EnumSet access; + private final String name; + private final List annotations = new ArrayList<>(); + private final ParameterizedType returnType; + private final List parameters; + private final List parameterTypes; + private final List> parameterAnnotations; + private final List exceptions = new ArrayList<>(); + + private final Block body; + private String comment; + + public MethodDefinition( + ClassDefinition declaringClass, + EnumSet access, + String name, + ParameterizedType returnType, + Parameter... parameters + ) + { + this(declaringClass, access, name, returnType, ImmutableList.copyOf(parameters)); + } + + public MethodDefinition( + ClassDefinition declaringClass, + EnumSet access, + String name, + ParameterizedType returnType, + Iterable parameters + ) + { + this.declaringClass = declaringClass; + body = new Block(); + + this.access = access; + this.name = name; + if (returnType != null) { + this.returnType = returnType; + } + else { + this.returnType = type(void.class); + } + this.parameters = ImmutableList.copyOf(parameters); + this.parameterTypes = Lists.transform(this.parameters, Parameter::getType); + this.parameterAnnotations = ImmutableList.copyOf(transform(parameters, input -> new ArrayList<>())); + + Optional thisType = Optional.empty(); + if (!access.contains(STATIC)) { + thisType = Optional.of(declaringClass.getType()); + } + scope = new Scope(thisType, parameters); + } + + public ClassDefinition getDeclaringClass() + { + return declaringClass; + } + + public List getAnnotations() + { + return ImmutableList.copyOf(annotations); + } + + public List getParameterAnnotations(int index) + { + return ImmutableList.copyOf(parameterAnnotations.get(index)); + } + + public EnumSet getAccess() + { + return access; + } + + public String getName() + { + return name; + } + + public ParameterizedType getReturnType() + { + return returnType; + } + + public List getParameters() + { + return parameters; + } + + public List getParameterTypes() + { + return parameterTypes; + } + + public List getExceptions() + { + return exceptions; + } + + public MethodDefinition addException(Class exceptionClass) + { + exceptions.add(type(exceptionClass)); + return this; + } + + public MethodDefinition comment(String format, Object... args) + { + this.comment = String.format(format, args); + return this; + } + + public String getComment() + { + return comment; + } + + public Scope getScope() + { + return scope; + } + + public Variable getThis() + { + return scope.getThis(); + } + + public String getMethodDescriptor() + { + return methodDescription(returnType, parameterTypes); + } + + public Block getBody() + { + return body; + } + + public AnnotationDefinition declareAnnotation(Class type) + { + AnnotationDefinition annotationDefinition = new AnnotationDefinition(type); + annotations.add(annotationDefinition); + return annotationDefinition; + } + + public AnnotationDefinition declareAnnotation(ParameterizedType type) + { + AnnotationDefinition annotationDefinition = new AnnotationDefinition(type); + annotations.add(annotationDefinition); + return annotationDefinition; + } + + public AnnotationDefinition declareParameterAnnotation(Class type, int parameterIndex) + { + AnnotationDefinition annotationDefinition = new AnnotationDefinition(type); + parameterAnnotations.get(parameterIndex).add(annotationDefinition); + return annotationDefinition; + } + + public AnnotationDefinition declareParameterAnnotation(ParameterizedType type, int parameterIndex) + { + AnnotationDefinition annotationDefinition = new AnnotationDefinition(type); + parameterAnnotations.get(parameterIndex).add(annotationDefinition); + return annotationDefinition; + } + + public void visit(ClassVisitor visitor) + { + visit(visitor, false); + } + + public void visit(ClassVisitor visitor, boolean addReturn) + { + String[] exceptions = new String[this.exceptions.size()]; + for (int i = 0; i < exceptions.length; i++) { + exceptions[i] = this.exceptions.get(i).getClassName(); + } + + MethodVisitor methodVisitor = visitor.visitMethod(toAccessModifier(access), + name, + getMethodDescriptor(), + genericMethodSignature(returnType, parameterTypes), + exceptions); + + if (methodVisitor == null) { + return; + } + + // visit method annotations + for (AnnotationDefinition annotation : annotations) { + annotation.visitMethodAnnotation(methodVisitor); + } + + // visit parameter annotations + for (int parameterIndex = 0; parameterIndex < parameterAnnotations.size(); parameterIndex++) { + List parameterAnnotations1 = this.parameterAnnotations.get(parameterIndex); + for (AnnotationDefinition parameterAnnotation : parameterAnnotations1) { + parameterAnnotation.visitParameterAnnotation(parameterIndex, methodVisitor); + } + } + + // visit code + methodVisitor.visitCode(); + + // visit instructions + MethodGenerationContext generationContext = new MethodGenerationContext(methodVisitor); + generationContext.enterScope(scope); + body.accept(methodVisitor, generationContext); + if (addReturn) { + new InsnNode(RETURN).accept(methodVisitor); + } + generationContext.exitScope(scope); + + // done + methodVisitor.visitMaxs(-1, -1); + methodVisitor.visitEnd(); + } + + public String toSourceString() + { + StringBuilder sb = new StringBuilder(); + Joiner.on(' ').appendTo(sb, access).append(' '); + sb.append(returnType.getJavaClassName()).append(' '); + sb.append(name).append('('); + Joiner.on(", ").appendTo(sb, transform(parameters, Parameter::getSourceString)).append(')'); + return sb.toString(); + } + + @Override + public String toString() + { + return toSourceString(); + } + + public static String methodDescription(Class returnType, Class... parameterTypes) + { + return methodDescription(returnType, ImmutableList.copyOf(parameterTypes)); + } + + public static String methodDescription(Class returnType, List> parameterTypes) + { + return methodDescription( + type(returnType), + Lists.transform(parameterTypes, ParameterizedType::type) + ); + } + + public static String methodDescription( + ParameterizedType returnType, + ParameterizedType... parameterTypes + ) + { + return methodDescription(returnType, ImmutableList.copyOf(parameterTypes)); + } + + public static String methodDescription( + ParameterizedType returnType, + List parameterTypes + ) + { + StringBuilder sb = new StringBuilder(); + sb.append("("); + Joiner.on("").appendTo(sb, transform(parameterTypes, ParameterizedType::getType)); + sb.append(")"); + sb.append(returnType.getType()); + return sb.toString(); + } + + public static String genericMethodSignature( + ParameterizedType returnType, + ParameterizedType... parameterTypes + ) + { + return genericMethodSignature(returnType, ImmutableList.copyOf(parameterTypes)); + } + + public static String genericMethodSignature( + ParameterizedType returnType, + List parameterTypes + ) + { + StringBuilder sb = new StringBuilder(); + sb.append("("); + Joiner.on("").appendTo(sb, parameterTypes); + sb.append(")"); + sb.append(returnType); + return sb.toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/MethodGenerationContext.java b/presto-main/src/main/java/com/facebook/presto/byteCode/MethodGenerationContext.java new file mode 100644 index 00000000..1aac603c --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/MethodGenerationContext.java @@ -0,0 +1,135 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode; + +import com.facebook.presto.byteCode.debug.LocalVariableNode; +import com.facebook.presto.byteCode.instruction.LabelNode; +import com.google.common.collect.ImmutableList; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Type; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkArgument; +import static java.util.Objects.requireNonNull; + +public class MethodGenerationContext +{ + private final MethodVisitor methodVisitor; + + private final Set allEnteredScopes = new LinkedHashSet<>(); + private final Deque scopes = new ArrayDeque<>(); + + private final Map variableSlots = new HashMap<>(); + private int nextSlot; + + private int currentLineNumber = -1; + + public MethodGenerationContext(MethodVisitor methodVisitor) + { + this.methodVisitor = requireNonNull(methodVisitor, "methodVisitor is null"); + } + + public void enterScope(Scope scope) + { + requireNonNull(scope, "scope is null"); + + checkArgument(!allEnteredScopes.contains(scope), "scope has already been entered"); + allEnteredScopes.add(scope); + + ScopeContext scopeContext = new ScopeContext(scope); + scopes.addLast(scopeContext); + + for (Variable variable : scopeContext.getVariables()) { + checkArgument(!"this".equals(variable.getName()) || nextSlot == 0, "The 'this' variable must be in slot 0"); + variableSlots.put(variable, nextSlot); + nextSlot += Type.getType(variable.getType().getType()).getSize(); + } + + scopeContext.getStartLabel().accept(methodVisitor, this); + } + + public void exitScope(Scope scope) + { + checkArgument(allEnteredScopes.contains(scope), "scope has not been entered"); + checkArgument(!scopes.isEmpty() && scope == scopes.peekLast().getScope(), "Scope is not top of the stack"); + + ScopeContext scopeContext = scopes.removeLast(); + + scopeContext.getEndLabel().accept(methodVisitor, this); + + for (Variable variable : scopeContext.getVariables()) { + new LocalVariableNode(variable, scopeContext.getStartLabel(), scopeContext.getEndLabel()).accept(methodVisitor, this); + } + + variableSlots.keySet().removeAll(scopeContext.getVariables()); + } + + public int getVariableSlot(Variable variable) + { + Integer slot = variableSlots.get(variable); + checkArgument(slot != null, "Variable '%s' has not been assigned a slot", variable); + return slot; + } + + public boolean updateLineNumber(int lineNumber) + { + if (lineNumber == currentLineNumber) { + return false; + } + + currentLineNumber = lineNumber; + return true; + } + + private final class ScopeContext + { + private final Scope scope; + private final ImmutableList variables; + + private final LabelNode startLabel = new LabelNode("VariableStart"); + private final LabelNode endLabel = new LabelNode("VariableEnd"); + + public ScopeContext(Scope scope) + { + this.scope = scope; + this.variables = ImmutableList.copyOf(scope.getVariables()); + } + + public Scope getScope() + { + return scope; + } + + public ImmutableList getVariables() + { + return variables; + } + + public LabelNode getStartLabel() + { + return startLabel; + } + + public LabelNode getEndLabel() + { + return endLabel; + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/OpCode.java b/presto-main/src/main/java/com/facebook/presto/byteCode/OpCode.java new file mode 100644 index 00000000..d68050ba --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/OpCode.java @@ -0,0 +1,278 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode; + +import com.facebook.presto.byteCode.instruction.InstructionNode; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import org.objectweb.asm.MethodVisitor; + +import java.util.List; +import java.util.Map; + +import static com.google.common.base.Preconditions.checkArgument; + +@SuppressWarnings("SpellCheckingInspection") +public enum OpCode + implements InstructionNode +{ + NOP(0), + ACONST_NULL(1), + ICONST_M1(2), + ICONST_0(3), + ICONST_1(4), + ICONST_2(5), + ICONST_3(6), + ICONST_4(7), + ICONST_5(8), + LCONST_0(9), + LCONST_1(10), + FCONST_0(11), + FCONST_1(12), + FCONST_2(13), + DCONST_0(14), + DCONST_1(15), + BIPUSH(16), + SIPUSH(17), + LDC(18), + LDC_W(19), + LDC2_W(20), + ILOAD(21), + LLOAD(22), + FLOAD(23), + DLOAD(24), + ALOAD(25), + ILOAD_0(26), + ILOAD_1(27), + ILOAD_2(28), + ILOAD_3(29), + LLOAD_0(30), + LLOAD_1(31), + LLOAD_2(32), + LLOAD_3(33), + FLOAD_0(34), + FLOAD_1(35), + FLOAD_2(36), + FLOAD_3(37), + DLOAD_0(38), + DLOAD_1(39), + DLOAD_2(40), + DLOAD_3(41), + ALOAD_0(42), + ALOAD_1(43), + ALOAD_2(44), + ALOAD_3(45), + IALOAD(46), + LALOAD(47), + FALOAD(48), + DALOAD(49), + AALOAD(50), + BALOAD(51), + CALOAD(52), + SALOAD(53), + ISTORE(54), + LSTORE(55), + FSTORE(56), + DSTORE(57), + ASTORE(58), + ISTORE_0(59), + ISTORE_1(60), + ISTORE_2(61), + ISTORE_3(62), + LSTORE_0(63), + LSTORE_1(64), + LSTORE_2(65), + LSTORE_3(66), + FSTORE_0(67), + FSTORE_1(68), + FSTORE_2(69), + FSTORE_3(70), + DSTORE_0(71), + DSTORE_1(72), + DSTORE_2(73), + DSTORE_3(74), + ASTORE_0(75), + ASTORE_1(76), + ASTORE_2(77), + ASTORE_3(78), + IASTORE(79), + LASTORE(80), + FASTORE(81), + DASTORE(82), + AASTORE(83), + BASTORE(84), + CASTORE(85), + SASTORE(86), + POP(87), + POP2(88), + DUP(89), + DUP_X1(90), + DUP_X2(91), + DUP2(92), + DUP2_X1(93), + DUP2_X2(94), + SWAP(95), + IADD(96), + LADD(97), + FADD(98), + DADD(99), + ISUB(100), + LSUB(101), + FSUB(102), + DSUB(103), + IMUL(104), + LMUL(105), + FMUL(106), + DMUL(107), + IDIV(108), + LDIV(109), + FDIV(110), + DDIV(111), + IREM(112), + LREM(113), + FREM(114), + DREM(115), + INEG(116), + LNEG(117), + FNEG(118), + DNEG(119), + ISHL(120), + LSHL(121), + ISHR(122), + LSHR(123), + IUSHR(124), + LUSHR(125), + IAND(126), + LAND(127), + IOR(128), + LOR(129), + IXOR(130), + LXOR(131), + IINC(132), + I2L(133), + I2F(134), + I2D(135), + L2I(136), + L2F(137), + L2D(138), + F2I(139), + F2L(140), + F2D(141), + D2I(142), + D2L(143), + D2F(144), + I2B(145), + I2C(146), + I2S(147), + LCMP(148), + FCMPL(149), + FCMPG(150), + DCMPL(151), + DCMPG(152), + IFEQ(153), + IFNE(154), + IFLT(155), + IFGE(156), + IFGT(157), + IFLE(158), + IF_ICMPEQ(159), + IF_ICMPNE(160), + IF_ICMPLT(161), + IF_ICMPGE(162), + IF_ICMPGT(163), + IF_ICMPLE(164), + IF_ACMPEQ(165), + IF_ACMPNE(166), + GOTO(167), + JSR(168), + RET(169), + TABLESWITCH(170), + LOOKUPSWITCH(171), + IRETURN(172), + LRETURN(173), + FRETURN(174), + DRETURN(175), + ARETURN(176), + RETURN(177), + GETSTATIC(178), + PUTSTATIC(179), + GETFIELD(180), + PUTFIELD(181), + INVOKEVIRTUAL(182), + INVOKESPECIAL(183), + INVOKESTATIC(184), + INVOKEINTERFACE(185), + INVOKEDYNAMIC(186), + NEW(187), + NEWARRAY(188), + ANEWARRAY(189), + ARRAYLENGTH(190), + ATHROW(191), + CHECKCAST(192), + INSTANCEOF(193), + MONITORENTER(194), + MONITOREXIT(195), + WIDE(196), + MULTIANEWARRAY(197), + IFNULL(198), + IFNONNULL(199), + GOTO_W(200), + JSR_W(201); + + private static final Map OP_CODE_INDEX; + static { + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (OpCode opCode : OpCode.values()) { + builder.put(opCode.getOpCode(), opCode); + } + OP_CODE_INDEX = builder.build(); + } + + public static OpCode getOpCode(int opCode) + { + OpCode value = OP_CODE_INDEX.get(opCode); + checkArgument(value != null, "Unknown opCode %s", opCode); + return value; + } + + private final int opCode; + + OpCode(int opCode) + { + this.opCode = opCode; + } + + public int getOpCode() + { + return opCode; + } + + @Override + public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) + { + visitor.visitInsn(opCode); + } + + @Override + public List getChildNodes() + { + return ImmutableList.of(); + } + + @Override + public T accept(ByteCodeNode parent, ByteCodeVisitor visitor) + { + return visitor.visitInstruction(parent, this); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/Parameter.java b/presto-main/src/main/java/com/facebook/presto/byteCode/Parameter.java new file mode 100644 index 00000000..57cff0e4 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/Parameter.java @@ -0,0 +1,41 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode; + +import javax.annotation.concurrent.Immutable; + +@Immutable +public class Parameter + extends Variable +{ + public static Parameter arg(String name, Class type) + { + return new Parameter(name, ParameterizedType.type(type)); + } + + public static Parameter arg(String name, ParameterizedType type) + { + return new Parameter(name, type); + } + + Parameter(String name, ParameterizedType type) + { + super(name, type); + } + + String getSourceString() + { + return getType().getJavaClassName() + " " + getName(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/ParameterizedType.java b/presto-main/src/main/java/com/facebook/presto/byteCode/ParameterizedType.java new file mode 100644 index 00000000..0082478a --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/ParameterizedType.java @@ -0,0 +1,329 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode; + +import com.google.common.collect.ImmutableList; +import org.objectweb.asm.Type; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Immutable +public class ParameterizedType +{ + public static ParameterizedType typeFromJavaClassName(String className) + { + checkNotNull(className, "type is null"); + if (className.endsWith("/")) { + checkArgument(!className.endsWith(";"), "Invalid class name %s", className); + } + return new ParameterizedType(className.replace('.', '/')); + } + + public static ParameterizedType typeFromPathName(String className) + { + checkNotNull(className, "type is null"); + if (className.indexOf(".") > 0) { + checkArgument(!className.endsWith(";"), "Invalid class name %s", className); + } + return new ParameterizedType(className); + } + + public static ParameterizedType type(Type type) + { + checkNotNull(type, "type is null"); + return new ParameterizedType(type.getInternalName()); + } + + public static ParameterizedType type(Class type) + { + checkNotNull(type, "type is null"); + return new ParameterizedType(type); + } + + public static ParameterizedType type(Class type, Class... parameters) + { + checkNotNull(type, "type is null"); + return new ParameterizedType(type, parameters); + } + + public static ParameterizedType type(Class type, ParameterizedType... parameters) + { + checkNotNull(type, "type is null"); + return new ParameterizedType(type, parameters); + } + + private final String type; + private final String className; + private final String simpleName; + private final List parameters; + + private final boolean isInterface; + @Nullable + private final Class primitiveType; + @Nullable + private final ParameterizedType arrayComponentType; + + public ParameterizedType(String className) + { + checkNotNull(className, "className is null"); + checkArgument(!className.contains("."), "Invalid class name %s", className); + checkArgument(!className.endsWith(";"), "Invalid class name %s", className); + + this.className = className; + this.simpleName = className.substring(className.lastIndexOf("/") + 1); + this.type = "L" + className + ";"; + this.parameters = ImmutableList.of(); + + this.isInterface = false; + this.primitiveType = null; + this.arrayComponentType = null; + } + + private ParameterizedType(Class type) + { + checkNotNull(type, "type is null"); + this.type = toInternalIdentifier(type); + this.className = getPathName(type); + this.simpleName = type.getSimpleName(); + this.parameters = ImmutableList.of(); + + this.isInterface = type.isInterface(); + this.primitiveType = type.isPrimitive() ? type : null; + this.arrayComponentType = type.isArray() ? type(type.getComponentType()) : null; + } + + private ParameterizedType(Class type, Class... parameters) + { + checkNotNull(type, "type is null"); + this.type = toInternalIdentifier(type); + this.className = getPathName(type); + this.simpleName = type.getSimpleName(); + + ImmutableList.Builder builder = ImmutableList.builder(); + for (Class parameter : parameters) { + builder.add(toInternalIdentifier(parameter)); + } + this.parameters = builder.build(); + + this.isInterface = type.isInterface(); + this.primitiveType = type.isPrimitive() ? type : null; + this.arrayComponentType = type.isArray() ? type(type.getComponentType()) : null; + } + + private ParameterizedType(Class type, ParameterizedType... parameters) + { + checkNotNull(type, "type is null"); + this.type = toInternalIdentifier(type); + this.className = getPathName(type); + this.simpleName = type.getSimpleName(); + + ImmutableList.Builder builder = ImmutableList.builder(); + for (ParameterizedType parameter : parameters) { + builder.add(parameter.toString()); + } + this.parameters = builder.build(); + + this.isInterface = type.isInterface(); + this.primitiveType = type.isPrimitive() ? type : null; + this.arrayComponentType = type.isArray() ? type(type.getComponentType()) : null; + } + + public String getClassName() + { + return className; + } + + public String getJavaClassName() + { + return className.replace('/', '.'); + } + + public String getSimpleName() + { + return simpleName; + } + + public String getType() + { + return type; + } + + public Type getAsmType() + { + return Type.getObjectType(className); + } + + public String getGenericSignature() + { + StringBuilder sb = new StringBuilder(); + if (primitiveType != null || arrayComponentType != null) { + return type; + } + sb.append('L').append(className); + if (!parameters.isEmpty()) { + sb.append("<"); + for (String parameterType : parameters) { + sb.append(parameterType); + } + sb.append(">"); + } + sb.append(";"); + return sb.toString(); + } + + public boolean isGeneric() + { + return !parameters.isEmpty(); + } + + public boolean isInterface() + { + return isInterface; + } + + @Nullable + public Class getPrimitiveType() + { + return primitiveType; + } + + public boolean isPrimitive() + { + return primitiveType != null; + } + + @Nullable + public ParameterizedType getArrayComponentType() + { + return arrayComponentType; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + ParameterizedType that = (ParameterizedType) o; + + if (!type.equals(that.type)) { + return false; + } + + return true; + } + + @Override + public int hashCode() + { + return type.hashCode(); + } + + @Override + public String toString() + { + return getGenericSignature(); + } + + public static String getPathName(Class n) + { + return n.getName().replace('.', '/'); + } + + private static String toInternalIdentifier(Class n) + { + if (n.isArray()) { + n = n.getComponentType(); + if (n.isPrimitive()) { + if (n == Byte.TYPE) { + return "[B"; + } + else if (n == Boolean.TYPE) { + return "[Z"; + } + else if (n == Short.TYPE) { + return "[S"; + } + else if (n == Character.TYPE) { + return "[C"; + } + else if (n == Integer.TYPE) { + return "[I"; + } + else if (n == Float.TYPE) { + return "[F"; + } + else if (n == Double.TYPE) { + return "[D"; + } + else if (n == Long.TYPE) { + return "[J"; + } + else { + throw new RuntimeException("Unrecognized type in compiler: " + n.getName()); + } + } + else { + return "[" + toInternalIdentifier(n); + } + } + else { + if (n.isPrimitive()) { + if (n == Byte.TYPE) { + return "B"; + } + else if (n == Boolean.TYPE) { + return "Z"; + } + else if (n == Short.TYPE) { + return "S"; + } + else if (n == Character.TYPE) { + return "C"; + } + else if (n == Integer.TYPE) { + return "I"; + } + else if (n == Float.TYPE) { + return "F"; + } + else if (n == Double.TYPE) { + return "D"; + } + else if (n == Long.TYPE) { + return "J"; + } + else if (n == Void.TYPE) { + return "V"; + } + else { + throw new RuntimeException("Unrecognized type in compiler: " + n.getName()); + } + } + else { + return "L" + getPathName(n) + ";"; + } + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/Scope.java b/presto-main/src/main/java/com/facebook/presto/byteCode/Scope.java new file mode 100644 index 00000000..0b78b84c --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/Scope.java @@ -0,0 +1,105 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode; + +import com.google.common.collect.ImmutableList; +import org.objectweb.asm.Type; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.TreeMap; + +import static com.facebook.presto.byteCode.ParameterizedType.type; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; +import static java.util.Objects.requireNonNull; + +public class Scope +{ + private final Map variables = new TreeMap<>(); + private final List allVariables = new ArrayList<>(); + + private final Variable thisVariable; + + private int nextTempVariableId; + + // This can only be constructed by a method definition + Scope(Optional thisType, Iterable parameters) + { + if (thisType.isPresent()) { + thisVariable = new Variable("this", thisType.get()); + variables.put("this", thisVariable); + allVariables.add(thisVariable); + } + else { + thisVariable = null; + } + + for (Parameter parameter : parameters) { + variables.put(parameter.getName(), parameter); + allVariables.add(parameter); + } + } + + public List getVariables() + { + return ImmutableList.copyOf(allVariables); + } + + public Variable createTempVariable(Class type) + { + // reserve a slot for this variable + Variable variable = new Variable("temp_" + nextTempVariableId, type(type)); + nextTempVariableId += Type.getType(type(type).getType()).getSize(); + + allVariables.add(variable); + + return variable; + } + + public Variable getThis() + { + checkState(thisVariable != null, "Static methods do not have a 'this' variable"); + return thisVariable; + } + + public Variable getVariable(String name) + { + Variable variable = variables.get(name); + checkArgument(variable != null, "Variable %s not defined", name); + return variable; + } + + public Variable declareVariable(Class type, String variableName) + { + return declareVariable(type(type), variableName); + } + + public Variable declareVariable(ParameterizedType type, String variableName) + { + requireNonNull(type, "type is null"); + requireNonNull(variableName, "variableName is null"); + checkArgument(!variables.containsKey(variableName), "There is already a variable named %s", variableName); + checkArgument(!variableName.equals("this"), "The 'this' variable can not be declared"); + + Variable variable = new Variable(variableName, type); + + variables.put(variableName, variable); + allVariables.add(variable); + + return variable; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/SmartClassWriter.java b/presto-main/src/main/java/com/facebook/presto/byteCode/SmartClassWriter.java new file mode 100644 index 00000000..a8300421 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/SmartClassWriter.java @@ -0,0 +1,54 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode; + +import org.objectweb.asm.ClassWriter; + +import static com.facebook.presto.byteCode.ParameterizedType.typeFromPathName; + +public class SmartClassWriter + extends ClassWriter +{ + private final ClassInfoLoader classInfoLoader; + + public SmartClassWriter(ClassInfoLoader classInfoLoader) + { + super(ClassWriter.COMPUTE_FRAMES); + this.classInfoLoader = classInfoLoader; + } + + @Override + protected String getCommonSuperClass(String aType, String bType) + { + ClassInfo aClassInfo = classInfoLoader.loadClassInfo(typeFromPathName(aType)); + ClassInfo bClassInfo = classInfoLoader.loadClassInfo(typeFromPathName(bType)); + + if (aClassInfo.isAssignableFrom(bClassInfo)) { + return aType; + } + if (bClassInfo.isAssignableFrom(aClassInfo)) { + return bType; + } + if (aClassInfo.isInterface() || bClassInfo.isInterface()) { + return "java/lang/Object"; + } + else { + do { + aClassInfo = aClassInfo.getSuperclass(); + } + while (!aClassInfo.isAssignableFrom(bClassInfo)); + return aClassInfo.getType().getClassName(); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/Variable.java b/presto-main/src/main/java/com/facebook/presto/byteCode/Variable.java new file mode 100644 index 00000000..18a78427 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/Variable.java @@ -0,0 +1,97 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode; + +import com.facebook.presto.byteCode.expression.ByteCodeExpression; +import com.facebook.presto.byteCode.instruction.VariableInstruction; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.facebook.presto.byteCode.ParameterizedType.type; +import static com.google.common.base.Preconditions.checkNotNull; + +public class Variable + extends ByteCodeExpression +{ + private final String name; + + public Variable(String name, ParameterizedType type) + { + super(type); + this.name = checkNotNull(name, "name is null"); + } + + public String getName() + { + return name; + } + + public ByteCodeExpression set(ByteCodeExpression value) + { + return new SetVariableByteCodeExpression(this, value); + } + + @Override + public ByteCodeNode getByteCode(MethodGenerationContext generationContext) + { + return VariableInstruction.loadVariable(this); + } + + @Override + protected String formatOneLine() + { + return name; + } + + @Override + public List getChildNodes() + { + return ImmutableList.of(); + } + + private static final class SetVariableByteCodeExpression + extends ByteCodeExpression + { + private final Variable variable; + private final ByteCodeExpression value; + + public SetVariableByteCodeExpression(Variable variable, ByteCodeExpression value) + { + super(type(void.class)); + this.variable = checkNotNull(variable, "variable is null"); + this.value = checkNotNull(value, "value is null"); + } + + @Override + public ByteCodeNode getByteCode(MethodGenerationContext generationContext) + { + return new Block() + .append(value) + .putVariable(variable); + } + + @Override + public List getChildNodes() + { + return ImmutableList.of(value); + } + + @Override + protected String formatOneLine() + { + return variable.getName() + " = " + value; + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/control/CaseStatement.java b/presto-main/src/main/java/com/facebook/presto/byteCode/control/CaseStatement.java new file mode 100644 index 00000000..9e7b0c8c --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/control/CaseStatement.java @@ -0,0 +1,85 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode.control; + +import com.facebook.presto.byteCode.instruction.LabelNode; + +import javax.annotation.concurrent.Immutable; + +import java.util.Objects; + +import static com.google.common.base.MoreObjects.toStringHelper; + +@Immutable +public class CaseStatement + implements Comparable +{ + public static CaseStatement caseStatement(int key, LabelNode label) + { + return new CaseStatement(label, key); + } + + private final int key; + private final LabelNode label; + + CaseStatement(LabelNode label, int key) + { + this.label = label; + this.key = key; + } + + public int getKey() + { + return key; + } + + public LabelNode getLabel() + { + return label; + } + + @Override + public int compareTo(CaseStatement o) + { + return Integer.compare(key, o.key); + } + + @Override + public int hashCode() + { + return key; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + CaseStatement other = (CaseStatement) obj; + return Objects.equals(this.key, other.key); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("key", key) + .add("label", label) + .toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/control/DoWhileLoop.java b/presto-main/src/main/java/com/facebook/presto/byteCode/control/DoWhileLoop.java new file mode 100644 index 00000000..88d9544c --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/control/DoWhileLoop.java @@ -0,0 +1,121 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode.control; + +import com.facebook.presto.byteCode.Block; +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.ByteCodeVisitor; +import com.facebook.presto.byteCode.MethodGenerationContext; +import com.facebook.presto.byteCode.instruction.LabelNode; +import com.google.common.collect.ImmutableList; +import org.objectweb.asm.MethodVisitor; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkState; + +public class DoWhileLoop + implements FlowControl +{ + private final String comment; + private final Block body = new Block(); + private final Block condition = new Block(); + + private final LabelNode beginLabel = new LabelNode("begin"); + private final LabelNode continueLabel = new LabelNode("continue"); + private final LabelNode endLabel = new LabelNode("end"); + + public DoWhileLoop() + { + this.comment = null; + } + + public DoWhileLoop(String format, Object... args) + { + this.comment = String.format(format, args); + } + + @Override + public String getComment() + { + return comment; + } + + public LabelNode getContinueLabel() + { + return continueLabel; + } + + public LabelNode getEndLabel() + { + return endLabel; + } + + public Block body() + { + return body; + } + + public DoWhileLoop body(ByteCodeNode node) + { + checkState(body.isEmpty(), "body already set"); + body.append(node); + return this; + } + + public Block condition() + { + return condition; + } + + public DoWhileLoop condition(ByteCodeNode node) + { + checkState(condition.isEmpty(), "condition already set"); + condition.append(node); + return this; + } + + @Override + public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) + { + checkState(!condition.isEmpty(), "DoWhileLoop does not have a condition set"); + + Block block = new Block() + .visitLabel(beginLabel) + .append(new Block() + .setDescription("body") + .append(body)) + .visitLabel(continueLabel) + .append(new Block() + .setDescription("condition") + .append(condition)) + .ifFalseGoto(endLabel) + .gotoLabel(beginLabel) + .visitLabel(endLabel); + + block.accept(visitor, generationContext); + } + + @Override + public List getChildNodes() + { + return ImmutableList.of(body, condition); + } + + @Override + public T accept(ByteCodeNode parent, ByteCodeVisitor visitor) + { + return visitor.visitDoWhile(parent, this); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/control/FlowControl.java b/presto-main/src/main/java/com/facebook/presto/byteCode/control/FlowControl.java new file mode 100644 index 00000000..d25b67b0 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/control/FlowControl.java @@ -0,0 +1,22 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode.control; + +import com.facebook.presto.byteCode.ByteCodeNode; + +public interface FlowControl + extends ByteCodeNode +{ + String getComment(); +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/control/ForLoop.java b/presto-main/src/main/java/com/facebook/presto/byteCode/control/ForLoop.java new file mode 100644 index 00000000..01bbbaba --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/control/ForLoop.java @@ -0,0 +1,157 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode.control; + +import com.facebook.presto.byteCode.Block; +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.ByteCodeVisitor; +import com.facebook.presto.byteCode.MethodGenerationContext; +import com.facebook.presto.byteCode.instruction.LabelNode; +import com.google.common.collect.ImmutableList; +import org.objectweb.asm.MethodVisitor; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkState; + +public class ForLoop + implements FlowControl +{ + private final String comment; + private final Block initialize = new Block(); + private final Block condition = new Block(); + private final Block update = new Block(); + private final Block body = new Block(); + + private final LabelNode beginLabel = new LabelNode("beginLabel"); + private final LabelNode continueLabel = new LabelNode("continue"); + private final LabelNode endLabel = new LabelNode("end"); + + public ForLoop() + { + this.comment = null; + } + + public ForLoop(String format, Object... args) + { + this.comment = String.format(format, args); + } + + @Override + public String getComment() + { + return comment; + } + + public LabelNode getContinueLabel() + { + return continueLabel; + } + + public LabelNode getEndLabel() + { + return endLabel; + } + + public Block initialize() + { + return initialize; + } + + public ForLoop initialize(ByteCodeNode node) + { + checkState(initialize.isEmpty(), "initialize already set"); + initialize.append(node); + return this; + } + + public Block condition() + { + return condition; + } + + public ForLoop condition(ByteCodeNode node) + { + checkState(condition.isEmpty(), "condition already set"); + condition.append(node); + return this; + } + + public Block update() + { + return update; + } + + public ForLoop update(ByteCodeNode node) + { + checkState(update.isEmpty(), "update already set"); + update.append(node); + return this; + } + + public Block body() + { + return body; + } + + public ForLoop body(ByteCodeNode node) + { + checkState(body.isEmpty(), "body already set"); + body.append(node); + return this; + } + + @Override + public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) + { + checkState(!condition.isEmpty(), "ForLoop does not have a condition set"); + + Block block = new Block(); + + block.append(new Block() + .setDescription("initialize") + .append(initialize)); + + block.visitLabel(beginLabel) + .append(new Block() + .setDescription("condition") + .append(condition)) + .ifFalseGoto(endLabel); + + block.append(new Block() + .setDescription("body") + .append(body)); + + block.visitLabel(continueLabel) + .append(new Block() + .setDescription("update") + .append(update)) + .gotoLabel(beginLabel) + .visitLabel(endLabel); + + block.accept(visitor, generationContext); + } + + @Override + public List getChildNodes() + { + return ImmutableList.of(initialize, condition, update, body); + } + + @Override + public T accept(ByteCodeNode parent, ByteCodeVisitor visitor) + { + return visitor.visitFor(parent, this); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/control/IfStatement.java b/presto-main/src/main/java/com/facebook/presto/byteCode/control/IfStatement.java new file mode 100644 index 00000000..acf0a553 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/control/IfStatement.java @@ -0,0 +1,139 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode.control; + +import com.facebook.presto.byteCode.Block; +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.ByteCodeVisitor; +import com.facebook.presto.byteCode.MethodGenerationContext; +import com.facebook.presto.byteCode.instruction.LabelNode; +import com.google.common.collect.ImmutableList; +import org.objectweb.asm.MethodVisitor; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkState; + +public class IfStatement + implements FlowControl +{ + private final String comment; + private final Block condition = new Block(); + private final Block ifTrue = new Block(); + private final Block ifFalse = new Block(); + + private final LabelNode falseLabel = new LabelNode("false"); + private final LabelNode outLabel = new LabelNode("out"); + + public IfStatement() + { + this.comment = null; + } + + public IfStatement(String format, Object... args) + { + this.comment = String.format(format, args); + } + + @Override + public String getComment() + { + return comment; + } + + public Block condition() + { + return condition; + } + + public IfStatement condition(ByteCodeNode node) + { + checkState(condition.isEmpty(), "condition already set"); + condition.append(node); + return this; + } + + public Block ifTrue() + { + return ifTrue; + } + + public IfStatement ifTrue(ByteCodeNode node) + { + checkState(ifTrue.isEmpty(), "ifTrue already set"); + ifTrue.append(node); + return this; + } + + public Block ifFalse() + { + return ifFalse; + } + + public IfStatement ifFalse(ByteCodeNode node) + { + checkState(ifFalse.isEmpty(), "ifFalse already set"); + ifFalse.append(node); + return this; + } + + @Override + public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) + { + checkState(!condition.isEmpty(), "IfStatement does not have a condition set"); + checkState(!ifTrue.isEmpty() || !ifFalse.isEmpty(), "IfStatement does not have a true or false block set"); + + Block block = new Block(); + + // if !condition goto false; + block.append(new Block() + .setDescription("condition") + .append(condition)); + block.ifFalseGoto(falseLabel); + + if (!ifTrue.isEmpty()) { + block.append(new Block() + .setDescription("ifTrue") + .append(ifTrue)); + } + + if (!ifFalse.isEmpty()) { + // close true case by skipping to end + block.gotoLabel(outLabel); + + block.visitLabel(falseLabel); + block.append(new Block() + .setDescription("ifFalse") + .append(ifFalse)); + block.visitLabel(outLabel); + } + else { + block.visitLabel(falseLabel); + } + + block.accept(visitor, generationContext); + } + + @Override + public List getChildNodes() + { + return ImmutableList.of(condition, ifTrue, ifFalse); + } + + @Override + public T accept(ByteCodeNode parent, ByteCodeVisitor visitor) + { + return visitor.visitIf(parent, this); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/control/LookupSwitch.java b/presto-main/src/main/java/com/facebook/presto/byteCode/control/LookupSwitch.java new file mode 100644 index 00000000..7507db23 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/control/LookupSwitch.java @@ -0,0 +1,126 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode.control; + +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.ByteCodeVisitor; +import com.facebook.presto.byteCode.MethodGenerationContext; +import com.facebook.presto.byteCode.instruction.LabelNode; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSortedSet; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; + +import java.util.ArrayList; +import java.util.List; +import java.util.SortedSet; + +public class LookupSwitch + implements FlowControl +{ + public static LookupSwitchBuilder lookupSwitchBuilder() + { + return new LookupSwitchBuilder(); + } + + public static class LookupSwitchBuilder + { + private final List cases = new ArrayList<>(); + private String comment; + private LabelNode defaultCase; + + public LookupSwitchBuilder defaultCase(LabelNode defaultCase) + { + this.defaultCase = defaultCase; + return this; + } + + public LookupSwitchBuilder comment(String format, Object... args) + { + this.comment = String.format(format, args); + return this; + } + + public LookupSwitchBuilder addCase(int key, LabelNode label) + { + addCase(CaseStatement.caseStatement(key, label)); + return this; + } + + public LookupSwitchBuilder addCase(CaseStatement caseStatement) + { + cases.add(caseStatement); + return this; + } + + public LookupSwitch build() + { + return new LookupSwitch(comment, defaultCase, cases); + } + } + + private final String comment; + private final LabelNode defaultCase; + private final SortedSet cases; + + private LookupSwitch(String comment, LabelNode defaultCase, Iterable cases) + { + this.comment = comment; + this.defaultCase = defaultCase; + this.cases = ImmutableSortedSet.copyOf(cases); + } + + @Override + public String getComment() + { + return comment; + } + + public SortedSet getCases() + { + return cases; + } + + public LabelNode getDefaultCase() + { + return defaultCase; + } + + @Override + public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) + { + int[] keys = new int[cases.size()]; + Label[] labels = new Label[cases.size()]; + + int index = 0; + for (CaseStatement caseStatement : cases) { + keys[index] = caseStatement.getKey(); + labels[index] = caseStatement.getLabel().getLabel(); + index++; + } + visitor.visitLookupSwitchInsn(defaultCase.getLabel(), keys, labels); + } + + @Override + public List getChildNodes() + { + return ImmutableList.of(); + } + + @Override + public T accept(ByteCodeNode parent, ByteCodeVisitor visitor) + { + return visitor.visitLookupSwitch(parent, this); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/control/TryCatch.java b/presto-main/src/main/java/com/facebook/presto/byteCode/control/TryCatch.java new file mode 100644 index 00000000..d4176f72 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/control/TryCatch.java @@ -0,0 +1,104 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode.control; + +import com.facebook.presto.byteCode.Block; +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.ByteCodeVisitor; +import com.facebook.presto.byteCode.MethodGenerationContext; +import com.facebook.presto.byteCode.ParameterizedType; +import com.facebook.presto.byteCode.instruction.LabelNode; +import com.google.common.collect.ImmutableList; +import org.objectweb.asm.MethodVisitor; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class TryCatch + implements FlowControl +{ + private final String comment; + private final ByteCodeNode tryNode; + private final ByteCodeNode catchNode; + private final String exceptionName; + + public TryCatch(String comment, ByteCodeNode tryNode, ByteCodeNode catchNode, ParameterizedType exceptionType) + { + this.comment = comment; + this.tryNode = checkNotNull(tryNode, "tryNode is null"); + this.catchNode = checkNotNull(catchNode, "catchNode is null"); + this.exceptionName = (exceptionType != null) ? exceptionType.getClassName() : null; + } + + @Override + public String getComment() + { + return comment; + } + + public ByteCodeNode getTryNode() + { + return tryNode; + } + + public ByteCodeNode getCatchNode() + { + return catchNode; + } + + public String getExceptionName() + { + return exceptionName; + } + + @Override + public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) + { + LabelNode tryStart = new LabelNode("tryStart"); + LabelNode tryEnd = new LabelNode("tryEnd"); + LabelNode handler = new LabelNode("handler"); + LabelNode done = new LabelNode("done"); + + Block block = new Block(); + + // try block + block.visitLabel(tryStart) + .append(tryNode) + .visitLabel(tryEnd) + .gotoLabel(done); + + // handler block + block.visitLabel(handler) + .append(catchNode); + + // all done + block.visitLabel(done); + + block.accept(visitor, generationContext); + visitor.visitTryCatchBlock(tryStart.getLabel(), tryEnd.getLabel(), handler.getLabel(), exceptionName); + } + + @Override + public List getChildNodes() + { + return ImmutableList.of(tryNode, catchNode); + } + + @Override + public T accept(ByteCodeNode parent, ByteCodeVisitor visitor) + { + return visitor.visitTryCatch(parent, this); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/control/WhileLoop.java b/presto-main/src/main/java/com/facebook/presto/byteCode/control/WhileLoop.java new file mode 100644 index 00000000..4d24b5e1 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/control/WhileLoop.java @@ -0,0 +1,115 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode.control; + +import com.facebook.presto.byteCode.Block; +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.ByteCodeVisitor; +import com.facebook.presto.byteCode.MethodGenerationContext; +import com.facebook.presto.byteCode.instruction.LabelNode; +import com.google.common.collect.ImmutableList; +import org.objectweb.asm.MethodVisitor; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkState; + +public class WhileLoop + implements FlowControl +{ + private final String comment; + private final Block condition = new Block(); + private final Block body = new Block(); + + private final LabelNode continueLabel = new LabelNode("continue"); + private final LabelNode endLabel = new LabelNode("end"); + + public WhileLoop() + { + this.comment = null; + } + + public WhileLoop(String format, Object... args) + { + this.comment = String.format(format, args); + } + + @Override + public String getComment() + { + return comment; + } + + public LabelNode getContinueLabel() + { + return continueLabel; + } + + public LabelNode getEndLabel() + { + return endLabel; + } + + public Block condition() + { + return condition; + } + + public WhileLoop condition(ByteCodeNode node) + { + checkState(condition.isEmpty(), "condition already set"); + condition.append(node); + return this; + } + + public Block body() + { + return body; + } + + public WhileLoop body(ByteCodeNode node) + { + checkState(body.isEmpty(), "body already set"); + body.append(node); + return this; + } + + @Override + public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) + { + checkState(!condition.isEmpty(), "WhileLoop does not have a condition set"); + + Block block = new Block() + .visitLabel(continueLabel) + .append(condition) + .ifZeroGoto(endLabel) + .append(body) + .gotoLabel(continueLabel) + .visitLabel(endLabel); + + block.accept(visitor, generationContext); + } + + @Override + public List getChildNodes() + { + return ImmutableList.of(condition, body); + } + + @Override + public T accept(ByteCodeNode parent, ByteCodeVisitor visitor) + { + return visitor.visitWhile(parent, this); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/debug/DebugNode.java b/presto-main/src/main/java/com/facebook/presto/byteCode/debug/DebugNode.java new file mode 100644 index 00000000..b992ab17 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/debug/DebugNode.java @@ -0,0 +1,21 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode.debug; + +import com.facebook.presto.byteCode.ByteCodeNode; + +public interface DebugNode + extends ByteCodeNode +{ +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/debug/LineNumberNode.java b/presto-main/src/main/java/com/facebook/presto/byteCode/debug/LineNumberNode.java new file mode 100644 index 00000000..f95f02e3 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/debug/LineNumberNode.java @@ -0,0 +1,71 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode.debug; + +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.ByteCodeVisitor; +import com.facebook.presto.byteCode.MethodGenerationContext; +import com.facebook.presto.byteCode.instruction.LabelNode; +import com.google.common.collect.ImmutableList; +import org.objectweb.asm.MethodVisitor; + +import java.util.List; + +import static com.google.common.base.MoreObjects.toStringHelper; + +public class LineNumberNode + implements DebugNode +{ + private final int lineNumber; + private final LabelNode label = new LabelNode(); + + public LineNumberNode(int lineNumber) + { + this.lineNumber = lineNumber; + } + + @Override + public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) + { + if (generationContext.updateLineNumber(lineNumber)) { + label.accept(visitor, generationContext); + visitor.visitLineNumber(lineNumber, label.getLabel()); + } + } + + public int getLineNumber() + { + return lineNumber; + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("line", lineNumber) + .toString(); + } + + @Override + public List getChildNodes() + { + return ImmutableList.of(); + } + + @Override + public T accept(ByteCodeNode parent, ByteCodeVisitor visitor) + { + return visitor.visitLineNumber(parent, this); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/debug/LocalVariableNode.java b/presto-main/src/main/java/com/facebook/presto/byteCode/debug/LocalVariableNode.java new file mode 100644 index 00000000..faf8f7dc --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/debug/LocalVariableNode.java @@ -0,0 +1,74 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode.debug; + +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.ByteCodeVisitor; +import com.facebook.presto.byteCode.MethodGenerationContext; +import com.facebook.presto.byteCode.Variable; +import com.facebook.presto.byteCode.instruction.LabelNode; +import com.google.common.collect.ImmutableList; +import org.objectweb.asm.MethodVisitor; + +import java.util.List; + +import static com.google.common.base.MoreObjects.toStringHelper; + +public class LocalVariableNode + implements DebugNode +{ + private final Variable variable; + private final LabelNode start; + private final LabelNode end; + + public LocalVariableNode(Variable variable, LabelNode start, LabelNode end) + { + this.variable = variable; + this.start = start; + this.end = end; + } + + @Override + public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) + { + visitor.visitLocalVariable(variable.getName(), + variable.getType().getType(), + variable.getType().getGenericSignature(), + start.getLabel(), + end.getLabel(), + generationContext.getVariableSlot(variable)); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("variable", variable) + .add("start", start) + .add("end", end) + .toString(); + } + + @Override + public List getChildNodes() + { + return ImmutableList.of(); + } + + @Override + public T accept(ByteCodeNode parent, ByteCodeVisitor visitor) + { + return visitor.visitLocalVariable(parent, this); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/expression/AndByteCodeExpression.java b/presto-main/src/main/java/com/facebook/presto/byteCode/expression/AndByteCodeExpression.java new file mode 100644 index 00000000..7dea0c93 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/expression/AndByteCodeExpression.java @@ -0,0 +1,71 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode.expression; + +import com.facebook.presto.byteCode.Block; +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.MethodGenerationContext; +import com.facebook.presto.byteCode.instruction.LabelNode; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.facebook.presto.byteCode.ParameterizedType.type; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +class AndByteCodeExpression + extends ByteCodeExpression +{ + private final ByteCodeExpression left; + private final ByteCodeExpression right; + + AndByteCodeExpression(ByteCodeExpression left, ByteCodeExpression right) + { + super(type(boolean.class)); + this.left = checkNotNull(left, "left is null"); + checkArgument(left.getType().getPrimitiveType() == boolean.class, "Expected left to be type boolean but is %s", left.getType()); + this.right = checkNotNull(right, "right is null"); + checkArgument(right.getType().getPrimitiveType() == boolean.class, "Expected right to be type boolean but is %s", right.getType()); + } + + @Override + public ByteCodeNode getByteCode(MethodGenerationContext generationContext) + { + LabelNode falseLabel = new LabelNode("false"); + LabelNode endLabel = new LabelNode("end"); + return new Block() + .append(left) + .ifFalseGoto(falseLabel) + .append(right) + .ifFalseGoto(falseLabel) + .push(true) + .gotoLabel(endLabel) + .visitLabel(falseLabel) + .push(false) + .visitLabel(endLabel); + } + + @Override + public List getChildNodes() + { + return ImmutableList.of(left, right); + } + + @Override + protected String formatOneLine() + { + return "(" + left + " && " + right + ")"; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/expression/ArithmeticByteCodeExpression.java b/presto-main/src/main/java/com/facebook/presto/byteCode/expression/ArithmeticByteCodeExpression.java new file mode 100644 index 00000000..d77bcd64 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/expression/ArithmeticByteCodeExpression.java @@ -0,0 +1,204 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode.expression; + +import com.facebook.presto.byteCode.Block; +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.MethodGenerationContext; +import com.facebook.presto.byteCode.OpCode; +import com.facebook.presto.byteCode.ParameterizedType; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class ArithmeticByteCodeExpression + extends ByteCodeExpression +{ + public static ByteCodeExpression createArithmeticByteCodeExpression(OpCode baseOpCode, ByteCodeExpression left, ByteCodeExpression right) + { + checkNotNull(baseOpCode, "baseOpCode is null"); + String name = getName(baseOpCode); + String infixSymbol = getInfixSymbol(baseOpCode); + + checkArgumentTypes(baseOpCode, name, left, right); + + OpCode opCode = getNumericOpCode(name, baseOpCode, left.getType().getPrimitiveType()); + return new ArithmeticByteCodeExpression(infixSymbol, left.getType(), opCode, left, right); + } + + private static String getName(OpCode baseOpCode) + { + switch (baseOpCode) { + case IAND: + return "Bitwise AND"; + case IOR: + return "Bitwise OR"; + case IXOR: + return "Bitwise XOR"; + case IADD: + return "Add"; + case ISUB: + return "Subtract"; + case IMUL: + return "Multiply"; + case IDIV: + return "Divide"; + case IREM: + return "Remainder"; + case ISHL: + return "Shift left"; + case ISHR: + return "Shift right"; + case IUSHR: + return "Shift right unsigned"; + default: + throw new IllegalArgumentException("Unsupported OpCode " + baseOpCode); + } + } + + private static String getInfixSymbol(OpCode baseOpCode) + { + switch (baseOpCode) { + case IAND: + return "&"; + case IOR: + return "|"; + case IXOR: + return "^"; + case IADD: + return "+"; + case ISUB: + return "-"; + case IMUL: + return "*"; + case IDIV: + return "/"; + case IREM: + return "%"; + case ISHL: + return "<<"; + case ISHR: + return ">>"; + case IUSHR: + return ">>>"; + default: + throw new IllegalArgumentException("Unsupported OpCode " + baseOpCode); + } + } + + private static void checkArgumentTypes(OpCode baseOpCode, String name, ByteCodeExpression left, ByteCodeExpression right) + { + Class leftType = getPrimitiveType(left, "left"); + Class rightType = getPrimitiveType(right, "right"); + switch (baseOpCode) { + case IAND: + case IOR: + case IXOR: + checkArgument(leftType == rightType, "left and right must be the same type"); + checkArgument(leftType == int.class || leftType == long.class, "%s argument must be int or long, but is %s", name, leftType); + return; + case IADD: + case ISUB: + case IMUL: + case IDIV: + case IREM: + checkArgument(leftType == rightType, "left and right must be the same type"); + checkArgument(leftType == int.class || leftType == long.class || leftType == float.class || leftType == double.class, + "%s argument must be int, long, float, or double, but is %s", + name, + leftType); + return; + case ISHL: + case ISHR: + case IUSHR: + checkArgument(leftType == int.class || leftType == long.class, "%s left argument be int or long, but is %s", name, leftType); + checkArgument(rightType == int.class, "%s right argument be and int, but is %s", name, rightType); + return; + default: + throw new IllegalArgumentException("Unsupported OpCode " + baseOpCode); + } + } + + static OpCode getNumericOpCode(String name, OpCode baseOpCode, Class type) + { + // Arithmetic OpCodes are laid out int, long, float and then double + if (type == int.class) { + return baseOpCode; + } + else if (type == long.class) { + return OpCode.getOpCode(baseOpCode.getOpCode() + 1); + } + else if (type == float.class) { + return OpCode.getOpCode(baseOpCode.getOpCode() + 2); + } + else if (type == double.class) { + return OpCode.getOpCode(baseOpCode.getOpCode() + 3); + } + else { + throw new IllegalArgumentException(name + " does not support " + type); + } + } + + private static Class getPrimitiveType(ByteCodeExpression expression, String name) + { + checkNotNull(expression, name + " is null"); + Class leftType = expression.getType().getPrimitiveType(); + checkArgument(leftType != null, name + " is not a primitive"); + checkArgument(leftType != void.class, name + " is void"); + return leftType; + } + + private final String infixSymbol; + private final OpCode opCode; + private final ByteCodeExpression left; + private final ByteCodeExpression right; + + private ArithmeticByteCodeExpression( + String infixSymbol, + ParameterizedType type, + OpCode opCode, + ByteCodeExpression left, + ByteCodeExpression right) + { + super(type); + this.infixSymbol = infixSymbol; + this.opCode = opCode; + this.left = left; + this.right = right; + } + + @Override + public ByteCodeNode getByteCode(MethodGenerationContext generationContext) + { + return new Block() + .append(left) + .append(right) + .append(opCode); + } + + @Override + public List getChildNodes() + { + return ImmutableList.of(left, right); + } + + @Override + protected String formatOneLine() + { + return "(" + left + " " + infixSymbol + " " + right + ")"; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/expression/ByteCodeExpression.java b/presto-main/src/main/java/com/facebook/presto/byteCode/expression/ByteCodeExpression.java new file mode 100644 index 00000000..b39f68e8 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/expression/ByteCodeExpression.java @@ -0,0 +1,209 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode.expression; + +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.ByteCodeVisitor; +import com.facebook.presto.byteCode.FieldDefinition; +import com.facebook.presto.byteCode.MethodGenerationContext; +import com.facebook.presto.byteCode.ParameterizedType; +import com.google.common.collect.ImmutableList; +import org.objectweb.asm.MethodVisitor; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +import static com.facebook.presto.byteCode.ParameterizedType.type; +import static com.facebook.presto.byteCode.expression.ByteCodeExpressions.constantInt; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.Iterables.transform; + +/** + * A ByteCodeExpression is chain of Java like expressions that results in at most + * a single value being pushed on the stack. The chain starts with a constant, + * local variable, static field, static method or invoke dynamic followed followed + * by zero or more invocations, field dereferences, array element fetches, or casts. + * The expression can optionally be terminated by a set expression, and in this + * case no value is pushed on the stack. + * + * A ByteCodeExpression is a ByteCodeNode so it works with tools like tree dump. + * + * This abstraction makes it easy to write generic byte code generators that can + * work with data that may come from a parameter, field or the result of a method + * invocation. + */ +public abstract class ByteCodeExpression + implements ByteCodeNode +{ + private final ParameterizedType type; + + protected ByteCodeExpression(ParameterizedType type) + { + this.type = checkNotNull(type, "type is null"); + } + + public final ParameterizedType getType() + { + return type; + } + + public abstract ByteCodeNode getByteCode(MethodGenerationContext generationContext); + + protected abstract String formatOneLine(); + + @Override + public final String toString() + { + return formatOneLine() + (type.getPrimitiveType() == void.class ? ";" : ""); + } + + public final ByteCodeExpression getField(Class declaringClass, String name) + { + return new GetFieldByteCodeExpression(this, declaringClass, name); + } + + public final ByteCodeExpression getField(String name, Class type) + { + return new GetFieldByteCodeExpression(this, this.getType(), name, type(type)); + } + + public final ByteCodeExpression getField(Field field) + { + return new GetFieldByteCodeExpression(this, field); + } + + public final ByteCodeExpression getField(FieldDefinition field) + { + return new GetFieldByteCodeExpression(this, field); + } + + public final ByteCodeExpression getField(ParameterizedType declaringClass, String name, ParameterizedType type) + { + return new GetFieldByteCodeExpression(this, declaringClass, name, type); + } + + public final ByteCodeExpression setField(String name, ByteCodeExpression value) + { + return new SetFieldByteCodeExpression(this, this.getType(), name, value); + } + + public final ByteCodeExpression setField(Field field, ByteCodeExpression value) + { + return new SetFieldByteCodeExpression(this, field, value); + } + + public final ByteCodeExpression setField(FieldDefinition field, ByteCodeExpression value) + { + return new SetFieldByteCodeExpression(this, field, value); + } + + public final ByteCodeExpression cast(Class type) + { + return new CastByteCodeExpression(this, type(type)); + } + + public final ByteCodeExpression cast(ParameterizedType type) + { + return new CastByteCodeExpression(this, type); + } + + public final ByteCodeExpression invoke(Method method, ByteCodeExpression... parameters) + { + return invoke(method, ImmutableList.copyOf(checkNotNull(parameters, "parameters is null"))); + } + + public final ByteCodeExpression invoke(Method method, Iterable parameters) + { + return invoke(method.getName(), type(method.getReturnType()), parameters); + } + + public final ByteCodeExpression invoke(String methodName, Class returnType, ByteCodeExpression... parameters) + { + return invoke(methodName, type(returnType), ImmutableList.copyOf(checkNotNull(parameters, "parameters is null"))); + } + + public final ByteCodeExpression invoke(String methodName, Class returnType, Iterable parameters) + { + return invoke(methodName, type(returnType), parameters); + } + + public final ByteCodeExpression invoke(String methodName, ParameterizedType returnType, Iterable parameters) + { + checkNotNull(parameters, "parameters is null"); + + return invoke(methodName, + returnType, + ImmutableList.copyOf(transform(parameters, ByteCodeExpression::getType)), + parameters); + } + + public final ByteCodeExpression invoke(String methodName, Class returnType, Iterable> parameterTypes, ByteCodeExpression... parameters) + { + return invoke(methodName, type(returnType), transform(parameterTypes, ParameterizedType::type), ImmutableList.copyOf(checkNotNull(parameters, "parameters is null"))); + } + + public final ByteCodeExpression invoke(String methodName, ParameterizedType returnType, Iterable parameterTypes, ByteCodeExpression... parameters) + { + return invoke(methodName, returnType, parameterTypes, ImmutableList.copyOf(checkNotNull(parameters, "parameters is null"))); + } + + public final ByteCodeExpression invoke( + String methodName, + ParameterizedType returnType, + Iterable parameterTypes, + Iterable parameters) + { + return InvokeByteCodeExpression.createInvoke( + this, + methodName, + returnType, + parameterTypes, + parameters); + } + + public final ByteCodeExpression getElement(int index) + { + return new GetElementByteCodeExpression(this, constantInt(index)); + } + + public final ByteCodeExpression getElement(ByteCodeExpression index) + { + return new GetElementByteCodeExpression(this, index); + } + + public final ByteCodeExpression ret() + { + return new ReturnByteCodeExpression(this); + } + + public final ByteCodeExpression pop() + { + if (this.getType().getPrimitiveType() == void.class) { + return this; + } + return new PopByteCodeExpression(this); + } + + @Override + public final void accept(MethodVisitor visitor, MethodGenerationContext generationContext) + { + getByteCode(generationContext).accept(visitor, generationContext); + } + + @Override + public final T accept(ByteCodeNode parent, ByteCodeVisitor visitor) + { + return visitor.visitByteCodeExpression(parent, this); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/expression/ByteCodeExpressions.java b/presto-main/src/main/java/com/facebook/presto/byteCode/expression/ByteCodeExpressions.java new file mode 100644 index 00000000..c1b0f224 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/expression/ByteCodeExpressions.java @@ -0,0 +1,574 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode.expression; + +import com.facebook.presto.byteCode.FieldDefinition; +import com.facebook.presto.byteCode.OpCode; +import com.facebook.presto.byteCode.ParameterizedType; +import com.google.common.collect.ImmutableList; + +import java.lang.invoke.MethodType; +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +import static com.facebook.presto.byteCode.ParameterizedType.type; +import static com.facebook.presto.byteCode.expression.ArithmeticByteCodeExpression.createArithmeticByteCodeExpression; +import static com.facebook.presto.byteCode.instruction.Constant.loadBoolean; +import static com.facebook.presto.byteCode.instruction.Constant.loadClass; +import static com.facebook.presto.byteCode.instruction.Constant.loadDouble; +import static com.facebook.presto.byteCode.instruction.Constant.loadFloat; +import static com.facebook.presto.byteCode.instruction.Constant.loadInt; +import static com.facebook.presto.byteCode.instruction.Constant.loadLong; +import static com.facebook.presto.byteCode.instruction.Constant.loadNull; +import static com.facebook.presto.byteCode.instruction.Constant.loadString; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.Iterables.transform; +import static java.util.Objects.requireNonNull; + +public final class ByteCodeExpressions +{ + private ByteCodeExpressions() + { + } + + // + // Constants + // + + public static ByteCodeExpression constantTrue() + { + return new ConstantByteCodeExpression(boolean.class, loadBoolean(true)); + } + + public static ByteCodeExpression constantFalse() + { + return new ConstantByteCodeExpression(boolean.class, loadBoolean(false)); + } + + public static ByteCodeExpression constantBoolean(boolean value) + { + return new ConstantByteCodeExpression(boolean.class, loadBoolean(value)); + } + + public static ByteCodeExpression constantClass(Class value) + { + return new ConstantByteCodeExpression(Class.class, loadClass(value)); + } + + public static ByteCodeExpression constantClass(ParameterizedType value) + { + return new ConstantByteCodeExpression(Class.class, loadClass(value)); + } + + public static ByteCodeExpression constantDouble(double value) + { + return new ConstantByteCodeExpression(double.class, loadDouble(value)); + } + + public static ByteCodeExpression constantFloat(float value) + { + return new ConstantByteCodeExpression(float.class, loadFloat(value)); + } + + public static ByteCodeExpression constantInt(int value) + { + return new ConstantByteCodeExpression(int.class, loadInt(value)); + } + + public static ByteCodeExpression constantLong(long value) + { + return new ConstantByteCodeExpression(long.class, loadLong(value)); + } + + public static ByteCodeExpression constantNumber(Number value) + { + if (value instanceof Byte) { + return constantInt((value).intValue()).cast(byte.class); + } + if (value instanceof Short) { + return constantInt((value).intValue()).cast(short.class); + } + if (value instanceof Integer) { + return constantInt((Integer) value); + } + if (value instanceof Long) { + return constantLong((Long) value); + } + if (value instanceof Float) { + return constantFloat((Float) value); + } + if (value instanceof Double) { + return constantDouble((Double) value); + } + throw new IllegalStateException("Unsupported number type " + value.getClass().getSimpleName()); + } + + public static ByteCodeExpression constantNull(Class type) + { + return new ConstantByteCodeExpression(type, loadNull()); + } + + public static ByteCodeExpression constantNull(ParameterizedType type) + { + return new ConstantByteCodeExpression(type, loadNull()); + } + + public static ByteCodeExpression constantString(String value) + { + return new ConstantByteCodeExpression(String.class, loadString(value)); + } + + public static ByteCodeExpression defaultValue(Class type) + { + requireNonNull(type, "type is null"); + if (type == boolean.class) { + return constantInt(0).cast(boolean.class); + } + if (type == byte.class) { + return constantInt(0).cast(byte.class); + } + if (type == int.class) { + return constantInt(0); + } + if (type == short.class) { + return constantInt(0).cast(short.class); + } + if (type == long.class) { + return constantLong(0L); + } + if (type == float.class) { + return constantFloat(0.0f); + } + if (type == double.class) { + return constantDouble(0.0d); + } + checkArgument(!type.isPrimitive(), "Unsupported type %s", type); + return constantNull(type); + } + + // + // Get static field + // + + public static ByteCodeExpression getStatic(Class declaringClass, String name) + { + return new GetFieldByteCodeExpression(null, declaringClass, name); + } + + public static ByteCodeExpression getStatic(Field staticField) + { + return new GetFieldByteCodeExpression(null, staticField); + } + + public static ByteCodeExpression getStatic(FieldDefinition staticField) + { + return new GetFieldByteCodeExpression(null, staticField); + } + + public static ByteCodeExpression getStatic(ParameterizedType declaringClass, String name, ParameterizedType type) + { + return new GetFieldByteCodeExpression(null, declaringClass, name, type); + } + + // + // Set static field + // + + public static ByteCodeExpression setStatic(Class declaringClass, String name, ByteCodeExpression value) + { + return new SetFieldByteCodeExpression(null, declaringClass, name, value); + } + + public static ByteCodeExpression setStatic(Field staticField, ByteCodeExpression value) + { + return new SetFieldByteCodeExpression(null, staticField, value); + } + + public static ByteCodeExpression setStatic(FieldDefinition staticField, ByteCodeExpression value) + { + return new SetFieldByteCodeExpression(null, staticField, value); + } + + public static ByteCodeExpression setStatic(ParameterizedType declaringClass, String name, ByteCodeExpression value) + { + return new SetFieldByteCodeExpression(null, declaringClass, name, value); + } + + // + // New instance + // + + public static ByteCodeExpression newInstance(Class returnType, ByteCodeExpression... parameters) + { + return newInstance(type(returnType), ImmutableList.copyOf(checkNotNull(parameters, "parameters is null"))); + } + + public static ByteCodeExpression newInstance(Class returnType, Iterable parameters) + { + return newInstance(type(returnType), parameters); + } + + public static ByteCodeExpression newInstance(ParameterizedType returnType, ByteCodeExpression... parameters) + { + checkNotNull(parameters, "parameters is null"); + + return newInstance(returnType, ImmutableList.copyOf(parameters)); + } + + public static ByteCodeExpression newInstance(ParameterizedType returnType, Iterable parameters) + { + checkNotNull(parameters, "parameters is null"); + + return newInstance( + returnType, + ImmutableList.copyOf(transform(parameters, ByteCodeExpression::getType)), + parameters); + } + + public static ByteCodeExpression newInstance(Class returnType, Iterable> parameterTypes, ByteCodeExpression... parameters) + { + return newInstance(type(returnType), transform(parameterTypes, ParameterizedType::type), ImmutableList.copyOf(checkNotNull(parameters, "parameters is null"))); + } + + public static ByteCodeExpression newInstance(ParameterizedType returnType, Iterable parameterTypes, ByteCodeExpression... parameters) + { + return newInstance(returnType, parameterTypes, ImmutableList.copyOf(checkNotNull(parameters, "parameters is null"))); + } + + public static ByteCodeExpression newInstance( + ParameterizedType type, + Iterable parameterTypes, + Iterable parameters) + { + return new NewInstanceByteCodeExpression(type, parameterTypes, parameters); + } + + // + // Invoke static method + // + + public static ByteCodeExpression invokeStatic(Method method, ByteCodeExpression... parameters) + { + return invokeStatic(method, ImmutableList.copyOf(checkNotNull(parameters, "parameters is null"))); + } + + public static ByteCodeExpression invokeStatic(Method method, Iterable parameters) + { + return invokeStatic(method.getDeclaringClass(), method.getName(), method.getReturnType(), parameters); + } + + public static ByteCodeExpression invokeStatic(Class methodTargetType, String methodName, Class returnType, ByteCodeExpression... parameters) + { + return invokeStatic(methodTargetType, methodName, returnType, ImmutableList.copyOf(checkNotNull(parameters, "parameters is null"))); + } + + public static ByteCodeExpression invokeStatic( + Class methodTargetType, + String methodName, + Class returnType, + Iterable parameters) + { + checkNotNull(methodTargetType, "methodTargetType is null"); + checkNotNull(returnType, "returnType is null"); + checkNotNull(parameters, "parameters is null"); + + return invokeStatic( + type(methodTargetType), + methodName, + type(returnType), + ImmutableList.copyOf(transform(parameters, ByteCodeExpression::getType)), + parameters); + } + + public static ByteCodeExpression invokeStatic( + Class methodTargetType, + String methodName, + Class returnType, + Iterable> parameterTypes, + ByteCodeExpression... parameters) + { + checkNotNull(methodTargetType, "methodTargetType is null"); + checkNotNull(returnType, "returnType is null"); + checkNotNull(parameterTypes, "parameterTypes is null"); + checkNotNull(parameters, "parameters is null"); + + return invokeStatic( + type(methodTargetType), + methodName, + type(returnType), + transform(parameterTypes, ParameterizedType::type), + ImmutableList.copyOf(parameters)); + } + + public static ByteCodeExpression invokeStatic( + ParameterizedType methodTargetType, + String methodName, + ParameterizedType returnType, + Iterable parameterTypes, + ByteCodeExpression... parameters) + { + return invokeStatic(methodTargetType, methodName, returnType, parameterTypes, ImmutableList.copyOf(checkNotNull(parameters, "parameters is null"))); + } + + public static ByteCodeExpression invokeStatic( + ParameterizedType methodTargetType, + String methodName, + ParameterizedType returnType, + Iterable parameterTypes, + Iterable parameters) + { + return new InvokeByteCodeExpression( + null, + methodTargetType, + methodName, + returnType, + parameterTypes, + parameters); + } + + // + // Invoke dynamic + // + + public static ByteCodeExpression invokeDynamic( + Method bootstrapMethod, + Iterable bootstrapArgs, + String methodName, + Class returnType, + ByteCodeExpression... parameters) + { + return invokeDynamic(bootstrapMethod, bootstrapArgs, methodName, returnType, ImmutableList.copyOf(checkNotNull(parameters, "parameters is null"))); + } + + public static ByteCodeExpression invokeDynamic( + Method bootstrapMethod, + Iterable bootstrapArgs, + String methodName, + Class returnType, + Iterable parameters) + { + checkNotNull(returnType, "returnType is null"); + checkNotNull(parameters, "parameters is null"); + + return invokeDynamic( + bootstrapMethod, + bootstrapArgs, + methodName, + type(returnType), + ImmutableList.copyOf(transform(parameters, ByteCodeExpression::getType)), + parameters); + } + + public static ByteCodeExpression invokeDynamic( + Method bootstrapMethod, + Iterable bootstrapArgs, + String methodName, + ParameterizedType returnType, + ByteCodeExpression... parameters) + { + return invokeDynamic(bootstrapMethod, bootstrapArgs, methodName, returnType, ImmutableList.copyOf(checkNotNull(parameters, "parameters is null"))); + } + + public static ByteCodeExpression invokeDynamic( + Method bootstrapMethod, + Iterable bootstrapArgs, + String methodName, + ParameterizedType returnType, + Iterable parameters) + { + checkNotNull(returnType, "returnType is null"); + checkNotNull(parameters, "parameters is null"); + + return invokeDynamic( + bootstrapMethod, + bootstrapArgs, + methodName, + returnType, + ImmutableList.copyOf(transform(parameters, ByteCodeExpression::getType)), + parameters); + } + + public static ByteCodeExpression invokeDynamic( + Method bootstrapMethod, + Iterable bootstrapArgs, + String methodName, + MethodType methodType, + ByteCodeExpression... parameters) + { + checkNotNull(methodType, "methodType is null"); + checkNotNull(parameters, "parameters is null"); + + return invokeDynamic(bootstrapMethod, bootstrapArgs, methodName, methodType, ImmutableList.copyOf(parameters)); + } + + public static ByteCodeExpression invokeDynamic( + Method bootstrapMethod, + Iterable bootstrapArgs, + String methodName, + MethodType methodType, + Iterable parameters) + { + return invokeDynamic( + bootstrapMethod, + bootstrapArgs, + methodName, + type(methodType.returnType()), + transform(methodType.parameterList(), ParameterizedType::type), + ImmutableList.copyOf(checkNotNull(parameters, "parameters is null"))); + } + + public static ByteCodeExpression invokeDynamic( + Method bootstrapMethod, + Iterable bootstrapArgs, + String methodName, + ParameterizedType returnType, + Iterable parameterTypes, + Iterable parameters) + { + return new InvokeDynamicByteCodeExpression( + bootstrapMethod, + bootstrapArgs, + methodName, + returnType, + parameters, + parameterTypes); + } + + // + // Arithmetic operations + // + + public static ByteCodeExpression add(ByteCodeExpression left, ByteCodeExpression right) + { + return createArithmeticByteCodeExpression(OpCode.IADD, left, right); + } + + public static ByteCodeExpression subtract(ByteCodeExpression left, ByteCodeExpression right) + { + return createArithmeticByteCodeExpression(OpCode.ISUB, left, right); + } + + public static ByteCodeExpression multiply(ByteCodeExpression left, ByteCodeExpression right) + { + return createArithmeticByteCodeExpression(OpCode.IMUL, left, right); + } + + public static ByteCodeExpression divide(ByteCodeExpression left, ByteCodeExpression right) + { + return createArithmeticByteCodeExpression(OpCode.IDIV, left, right); + } + + public static ByteCodeExpression remainder(ByteCodeExpression left, ByteCodeExpression right) + { + return createArithmeticByteCodeExpression(OpCode.IREM, left, right); + } + + public static ByteCodeExpression bitwiseAnd(ByteCodeExpression left, ByteCodeExpression right) + { + return createArithmeticByteCodeExpression(OpCode.IAND, left, right); + } + + public static ByteCodeExpression bitwiseOr(ByteCodeExpression left, ByteCodeExpression right) + { + return createArithmeticByteCodeExpression(OpCode.IOR, left, right); + } + + public static ByteCodeExpression bitwiseXor(ByteCodeExpression left, ByteCodeExpression right) + { + return createArithmeticByteCodeExpression(OpCode.IXOR, left, right); + } + + public static ByteCodeExpression shiftLeft(ByteCodeExpression left, ByteCodeExpression right) + { + return createArithmeticByteCodeExpression(OpCode.ISHL, left, right); + } + + public static ByteCodeExpression shiftRight(ByteCodeExpression left, ByteCodeExpression right) + { + return createArithmeticByteCodeExpression(OpCode.ISHR, left, right); + } + + public static ByteCodeExpression shiftRightUnsigned(ByteCodeExpression left, ByteCodeExpression right) + { + return createArithmeticByteCodeExpression(OpCode.IUSHR, left, right); + } + + public static ByteCodeExpression negate(ByteCodeExpression value) + { + return new NegateByteCodeExpression(value); + } + + // + // Comparison operations + // + + public static ByteCodeExpression lessThan(ByteCodeExpression left, ByteCodeExpression right) + { + return ComparisonByteCodeExpression.lessThan(left, right); + } + + public static ByteCodeExpression greaterThan(ByteCodeExpression left, ByteCodeExpression right) + { + return ComparisonByteCodeExpression.greaterThan(left, right); + } + + public static ByteCodeExpression lessThanOrEqual(ByteCodeExpression left, ByteCodeExpression right) + { + return ComparisonByteCodeExpression.lessThanOrEqual(left, right); + } + + public static ByteCodeExpression greaterThanOrEqual(ByteCodeExpression left, ByteCodeExpression right) + { + return ComparisonByteCodeExpression.greaterThanOrEqual(left, right); + } + + public static ByteCodeExpression equal(ByteCodeExpression left, ByteCodeExpression right) + { + return ComparisonByteCodeExpression.equal(left, right); + } + + public static ByteCodeExpression notEqual(ByteCodeExpression left, ByteCodeExpression right) + { + return ComparisonByteCodeExpression.notEqual(left, right); + } + + // + // Logical binary operations + // + + public static ByteCodeExpression and(ByteCodeExpression left, ByteCodeExpression right) + { + return new AndByteCodeExpression(left, right); + } + + public static ByteCodeExpression or(ByteCodeExpression left, ByteCodeExpression right) + { + return new OrByteCodeExpression(left, right); + } + + public static ByteCodeExpression not(ByteCodeExpression value) + { + return new NotByteCodeExpression(value); + } + + // + // Complex expressions + // + + public static ByteCodeExpression inlineIf(ByteCodeExpression condition, ByteCodeExpression ifTrue, ByteCodeExpression ifFalse) + { + return new InlineIfByteCodeExpression(condition, ifTrue, ifFalse); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/expression/CastByteCodeExpression.java b/presto-main/src/main/java/com/facebook/presto/byteCode/expression/CastByteCodeExpression.java new file mode 100644 index 00000000..d60882d2 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/expression/CastByteCodeExpression.java @@ -0,0 +1,283 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode.expression; + +import com.facebook.presto.byteCode.Block; +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.MethodGenerationContext; +import com.facebook.presto.byteCode.OpCode; +import com.facebook.presto.byteCode.ParameterizedType; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.facebook.presto.byteCode.ParameterizedType.type; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.primitives.Primitives.wrap; +import static java.lang.String.format; + +class CastByteCodeExpression + extends ByteCodeExpression +{ + private final ByteCodeExpression instance; + + public CastByteCodeExpression(ByteCodeExpression instance, ParameterizedType type) + { + super(type); + + this.instance = checkNotNull(instance, "instance is null"); + + checkArgument(type.getPrimitiveType() != void.class, "Type %s can not be cast to %s", instance.getType(), type); + + // if we have a primitive to object or object to primitive conversion, it must be an exact boxing or unboxing conversion + if (instance.getType().isPrimitive() != type.isPrimitive()) { + checkArgument(unwrapPrimitiveType(instance.getType()) == unwrapPrimitiveType(type), "Type %s can not be cast to %s", instance.getType(), type); + } + } + + @Override + public ByteCodeNode getByteCode(MethodGenerationContext generationContext) + { + Block block = new Block().append(instance.getByteCode(generationContext)); + + if (instance.getType().isPrimitive()) { + Class sourceType = instance.getType().getPrimitiveType(); + castPrimitiveToPrimitive(block, sourceType, unwrapPrimitiveType(getType())); + + // insert boxing conversion + if (!getType().isPrimitive()) { + Class primitiveTargetType = unwrapPrimitiveType(getType()); + return block.invokeStatic(getType(), "valueOf", getType(), type(primitiveTargetType)); + } + + return block; + } + else if (getType().isPrimitive()) { + // unbox + Class targetType = getType().getPrimitiveType(); + return block.invokeVirtual(wrap(targetType), targetType.getSimpleName() + "Value", targetType); + } + else { + block.checkCast(getType()); + } + return block; + } + + private static Block castPrimitiveToPrimitive(Block block, Class sourceType, Class targetType) + { + if (sourceType == byte.class) { + if (targetType == byte.class) { + return block; + } + if (targetType == char.class) { + return block; + } + if (targetType == short.class) { + return block; + } + if (targetType == int.class) { + return block; + } + if (targetType == long.class) { + return block.append(OpCode.I2L); + } + if (targetType == float.class) { + return block.append(OpCode.I2F); + } + if (targetType == double.class) { + return block.append(OpCode.I2D); + } + } + if (sourceType == char.class) { + if (targetType == byte.class) { + return block.append(OpCode.I2B); + } + if (targetType == char.class) { + return block; + } + if (targetType == short.class) { + return block; + } + if (targetType == int.class) { + return block; + } + if (targetType == long.class) { + return block.append(OpCode.I2L); + } + if (targetType == float.class) { + return block.append(OpCode.I2F); + } + if (targetType == double.class) { + return block.append(OpCode.I2D); + } + } + if (sourceType == short.class) { + if (targetType == byte.class) { + return block.append(OpCode.I2B); + } + if (targetType == char.class) { + return block.append(OpCode.I2C); + } + if (targetType == short.class) { + return block; + } + if (targetType == int.class) { + return block; + } + if (targetType == long.class) { + return block.append(OpCode.I2L); + } + if (targetType == float.class) { + return block.append(OpCode.I2F); + } + if (targetType == double.class) { + return block.append(OpCode.I2D); + } + } + if (sourceType == int.class) { + if (targetType == boolean.class) { + return block; + } + if (targetType == byte.class) { + return block.append(OpCode.I2B); + } + if (targetType == char.class) { + return block.append(OpCode.I2C); + } + if (targetType == short.class) { + return block.append(OpCode.I2S); + } + if (targetType == int.class) { + return block; + } + if (targetType == long.class) { + return block.append(OpCode.I2L); + } + if (targetType == float.class) { + return block.append(OpCode.I2F); + } + if (targetType == double.class) { + return block.append(OpCode.I2D); + } + } + if (sourceType == long.class) { + if (targetType == byte.class) { + return block.append(OpCode.L2I).append(OpCode.I2B); + } + if (targetType == char.class) { + return block.append(OpCode.L2I).append(OpCode.I2C); + } + if (targetType == short.class) { + return block.append(OpCode.L2I).append(OpCode.I2S); + } + if (targetType == int.class) { + return block.append(OpCode.L2I); + } + if (targetType == long.class) { + return block; + } + if (targetType == float.class) { + return block.append(OpCode.L2F); + } + if (targetType == double.class) { + return block.append(OpCode.L2D); + } + } + if (sourceType == float.class) { + if (targetType == byte.class) { + return block.append(OpCode.F2I).append(OpCode.I2B); + } + if (targetType == char.class) { + return block.append(OpCode.F2I).append(OpCode.I2C); + } + if (targetType == short.class) { + return block.append(OpCode.F2I).append(OpCode.I2S); + } + if (targetType == int.class) { + return block.append(OpCode.F2I); + } + if (targetType == long.class) { + return block.append(OpCode.F2L); + } + if (targetType == float.class) { + return block; + } + if (targetType == double.class) { + return block.append(OpCode.F2D); + } + } + if (sourceType == double.class) { + if (targetType == byte.class) { + return block.append(OpCode.D2I).append(OpCode.I2B); + } + if (targetType == char.class) { + return block.append(OpCode.D2I).append(OpCode.I2C); + } + if (targetType == short.class) { + return block.append(OpCode.D2I).append(OpCode.I2S); + } + if (targetType == int.class) { + return block.append(OpCode.D2I); + } + if (targetType == long.class) { + return block.append(OpCode.D2L); + } + if (targetType == float.class) { + return block.append(OpCode.D2F); + } + if (targetType == double.class) { + return block; + } + } + throw new IllegalArgumentException(format("Type %s can not be cast to %s", sourceType, targetType)); + } + + private static Class unwrapPrimitiveType(ParameterizedType type) + { + if (type.isPrimitive()) { + return type.getPrimitiveType(); + } + switch (type.getJavaClassName()) { + case "java.lang.Byte": + return byte.class; + case "java.lang.Character": + return char.class; + case "java.lang.Short": + return short.class; + case "java.lang.Integer": + return int.class; + case "java.lang.Long": + return long.class; + case "java.lang.Float": + return float.class; + case "java.lang.Double": + return double.class; + default: + return null; + } + } + + @Override + protected String formatOneLine() + { + return "((" + getType().getSimpleName() + ") " + instance + ")"; + } + + @Override + public List getChildNodes() + { + return ImmutableList.of(instance); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/expression/ComparisonByteCodeExpression.java b/presto-main/src/main/java/com/facebook/presto/byteCode/expression/ComparisonByteCodeExpression.java new file mode 100644 index 00000000..a85ad1b3 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/expression/ComparisonByteCodeExpression.java @@ -0,0 +1,315 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode.expression; + +import com.facebook.presto.byteCode.Block; +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.MethodGenerationContext; +import com.facebook.presto.byteCode.OpCode; +import com.facebook.presto.byteCode.instruction.JumpInstruction; +import com.facebook.presto.byteCode.instruction.LabelNode; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.facebook.presto.byteCode.OpCode.DCMPG; +import static com.facebook.presto.byteCode.OpCode.DCMPL; +import static com.facebook.presto.byteCode.OpCode.FCMPG; +import static com.facebook.presto.byteCode.OpCode.FCMPL; +import static com.facebook.presto.byteCode.OpCode.IFEQ; +import static com.facebook.presto.byteCode.OpCode.IFGE; +import static com.facebook.presto.byteCode.OpCode.IFGT; +import static com.facebook.presto.byteCode.OpCode.IFLE; +import static com.facebook.presto.byteCode.OpCode.IFLT; +import static com.facebook.presto.byteCode.OpCode.IFNE; +import static com.facebook.presto.byteCode.OpCode.IF_ACMPEQ; +import static com.facebook.presto.byteCode.OpCode.IF_ACMPNE; +import static com.facebook.presto.byteCode.OpCode.IF_ICMPEQ; +import static com.facebook.presto.byteCode.OpCode.IF_ICMPGE; +import static com.facebook.presto.byteCode.OpCode.IF_ICMPGT; +import static com.facebook.presto.byteCode.OpCode.IF_ICMPLE; +import static com.facebook.presto.byteCode.OpCode.IF_ICMPLT; +import static com.facebook.presto.byteCode.OpCode.IF_ICMPNE; +import static com.facebook.presto.byteCode.OpCode.LCMP; +import static com.facebook.presto.byteCode.ParameterizedType.type; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +class ComparisonByteCodeExpression + extends ByteCodeExpression +{ + static ByteCodeExpression lessThan(ByteCodeExpression left, ByteCodeExpression right) + { + checkArgumentTypes(left, right); + + OpCode comparisonInstruction; + OpCode noMatchJumpInstruction; + + Class type = left.getType().getPrimitiveType(); + if (type == int.class) { + comparisonInstruction = null; + noMatchJumpInstruction = IF_ICMPGE; + } + else if (type == long.class) { + comparisonInstruction = LCMP; + noMatchJumpInstruction = IFGE; + } + else if (type == float.class) { + comparisonInstruction = FCMPG; + noMatchJumpInstruction = IFGE; + } + else if (type == double.class) { + comparisonInstruction = DCMPG; + noMatchJumpInstruction = IFGE; + } + else { + throw new IllegalArgumentException("Less than does not support " + type); + } + + return new ComparisonByteCodeExpression("<", comparisonInstruction, noMatchJumpInstruction, left, right); + } + + static ByteCodeExpression greaterThan(ByteCodeExpression left, ByteCodeExpression right) + { + checkArgumentTypes(left, right); + + OpCode comparisonInstruction; + OpCode noMatchJumpInstruction; + + Class type = left.getType().getPrimitiveType(); + if (type == int.class) { + comparisonInstruction = null; + noMatchJumpInstruction = IF_ICMPLE; + } + else if (type == long.class) { + comparisonInstruction = LCMP; + noMatchJumpInstruction = IFLE; + } + else if (type == float.class) { + comparisonInstruction = FCMPL; + noMatchJumpInstruction = IFLE; + } + else if (type == double.class) { + comparisonInstruction = DCMPL; + noMatchJumpInstruction = IFLE; + } + else { + throw new IllegalArgumentException("Less than does not support " + type); + } + return new ComparisonByteCodeExpression(">", comparisonInstruction, noMatchJumpInstruction, left, right); + } + + static ByteCodeExpression lessThanOrEqual(ByteCodeExpression left, ByteCodeExpression right) + { + checkArgumentTypes(left, right); + + OpCode comparisonInstruction; + OpCode noMatchJumpInstruction; + + Class type = left.getType().getPrimitiveType(); + if (type == int.class) { + comparisonInstruction = null; + noMatchJumpInstruction = IF_ICMPGT; + } + else if (type == long.class) { + comparisonInstruction = LCMP; + noMatchJumpInstruction = IFGT; + } + else if (type == float.class) { + comparisonInstruction = FCMPG; + noMatchJumpInstruction = IFGT; + } + else if (type == double.class) { + comparisonInstruction = DCMPG; + noMatchJumpInstruction = IFGT; + } + else { + throw new IllegalArgumentException("Less than does not support " + type); + } + return new ComparisonByteCodeExpression("<=", comparisonInstruction, noMatchJumpInstruction, left, right); + } + + static ByteCodeExpression greaterThanOrEqual(ByteCodeExpression left, ByteCodeExpression right) + { + checkArgumentTypes(left, right); + + OpCode comparisonInstruction; + OpCode noMatchJumpInstruction; + + Class type = left.getType().getPrimitiveType(); + if (type == int.class) { + comparisonInstruction = null; + noMatchJumpInstruction = IF_ICMPLT; + } + else if (type == long.class) { + comparisonInstruction = LCMP; + noMatchJumpInstruction = IFLT; + } + else if (type == float.class) { + comparisonInstruction = FCMPL; + noMatchJumpInstruction = IFLT; + } + else if (type == double.class) { + comparisonInstruction = DCMPL; + noMatchJumpInstruction = IFLT; + } + else { + throw new IllegalArgumentException("Less than does not support " + type); + } + return new ComparisonByteCodeExpression(">=", comparisonInstruction, noMatchJumpInstruction, left, right); + } + + static ByteCodeExpression equal(ByteCodeExpression left, ByteCodeExpression right) + { + checkNotNull(left, "left is null"); + checkNotNull(right, "right is null"); + checkArgument(left.getType().equals(right.getType()), "left and right must be the same type"); + + OpCode comparisonInstruction; + OpCode noMatchJumpInstruction; + + Class type = left.getType().getPrimitiveType(); + if (type == int.class) { + comparisonInstruction = null; + noMatchJumpInstruction = IF_ICMPNE; + } + else if (type == long.class) { + comparisonInstruction = LCMP; + noMatchJumpInstruction = IFNE; + } + else if (type == float.class) { + comparisonInstruction = FCMPL; + noMatchJumpInstruction = IFNE; + } + else if (type == double.class) { + comparisonInstruction = DCMPL; + noMatchJumpInstruction = IFNE; + } + else if (type == null) { + comparisonInstruction = null; + noMatchJumpInstruction = IF_ACMPNE; + } + else { + throw new IllegalArgumentException("Less than does not support " + type); + } + return new ComparisonByteCodeExpression("==", comparisonInstruction, noMatchJumpInstruction, left, right); + } + + static ByteCodeExpression notEqual(ByteCodeExpression left, ByteCodeExpression right) + { + checkNotNull(left, "left is null"); + checkNotNull(right, "right is null"); + checkArgument(left.getType().equals(right.getType()), "left and right must be the same type"); + + OpCode comparisonInstruction; + OpCode noMatchJumpInstruction; + + Class type = left.getType().getPrimitiveType(); + if (type == int.class) { + comparisonInstruction = null; + noMatchJumpInstruction = IF_ICMPEQ; + } + else if (type == long.class) { + comparisonInstruction = LCMP; + noMatchJumpInstruction = IFEQ; + } + else if (type == float.class) { + comparisonInstruction = FCMPL; + noMatchJumpInstruction = IFEQ; + } + else if (type == double.class) { + comparisonInstruction = DCMPL; + noMatchJumpInstruction = IFEQ; + } + else if (type == null) { + comparisonInstruction = null; + noMatchJumpInstruction = IF_ACMPEQ; + } + else { + throw new IllegalArgumentException("Less than does not support " + type); + } + return new ComparisonByteCodeExpression("!=", comparisonInstruction, noMatchJumpInstruction, left, right); + } + + private static void checkArgumentTypes(ByteCodeExpression left, ByteCodeExpression right) + { + Class leftType = getPrimitiveType(left, "left"); + Class rightType = getPrimitiveType(right, "right"); + checkArgument(leftType == rightType, "left and right must be the same type"); + } + + private static Class getPrimitiveType(ByteCodeExpression expression, String name) + { + checkNotNull(expression, name + " is null"); + Class leftType = expression.getType().getPrimitiveType(); + checkArgument(leftType != null, name + " is not a primitive"); + checkArgument(leftType != void.class, name + " is void"); + return leftType; + } + + private final String infixSymbol; + private final OpCode comparisonInstruction; + private final OpCode noMatchJumpInstruction; + private final ByteCodeExpression left; + private final ByteCodeExpression right; + + private ComparisonByteCodeExpression( + String infixSymbol, + OpCode comparisonInstruction, + OpCode noMatchJumpInstruction, + ByteCodeExpression left, + ByteCodeExpression right) + { + super(type(boolean.class)); + this.infixSymbol = infixSymbol; + this.comparisonInstruction = comparisonInstruction; + this.noMatchJumpInstruction = noMatchJumpInstruction; + this.left = left; + this.right = right; + } + + @Override + public ByteCodeNode getByteCode(MethodGenerationContext generationContext) + { + Block block = new Block() + .append(left) + .append(right); + + if (comparisonInstruction != null) { + block.append(comparisonInstruction); + } + + LabelNode noMatch = new LabelNode("no_match"); + LabelNode end = new LabelNode("end"); + return block + .append(new JumpInstruction(noMatchJumpInstruction, noMatch)) + .push(true) + .gotoLabel(end) + .append(noMatch) + .push(false) + .append(end); + } + + @Override + public List getChildNodes() + { + return ImmutableList.of(left, right); + } + + @Override + protected String formatOneLine() + { + return "(" + left + " " + infixSymbol + " " + right + ")"; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/expression/ConstantByteCodeExpression.java b/presto-main/src/main/java/com/facebook/presto/byteCode/expression/ConstantByteCodeExpression.java new file mode 100644 index 00000000..90b7ba56 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/expression/ConstantByteCodeExpression.java @@ -0,0 +1,77 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode.expression; + +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.MethodGenerationContext; +import com.facebook.presto.byteCode.ParameterizedType; +import com.facebook.presto.byteCode.instruction.Constant; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.facebook.presto.byteCode.ParameterizedType.type; + +class ConstantByteCodeExpression + extends ByteCodeExpression +{ + private final Constant value; + + public ConstantByteCodeExpression(Class type, Constant value) + { + this(type(type), value); + } + + public ConstantByteCodeExpression(ParameterizedType type, Constant value) + { + super(type); + this.value = value; + } + + @Override + public Constant getByteCode(MethodGenerationContext generationContext) + { + return value; + } + + @Override + protected String formatOneLine() + { + return renderConstant(value.getValue()); + } + + public static String renderConstant(Object value) + { + if (value instanceof Long) { + return value + "L"; + } + if (value instanceof Float) { + return value + "f"; + } + if (value instanceof ParameterizedType) { + return ((ParameterizedType) value).getSimpleName() + ".class"; + } + // todo escape string + if (value instanceof String) { + return "\"" + value + "\""; + } + return String.valueOf(value); + } + + @Override + public List getChildNodes() + { + return ImmutableList.of(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/expression/GetElementByteCodeExpression.java b/presto-main/src/main/java/com/facebook/presto/byteCode/expression/GetElementByteCodeExpression.java new file mode 100644 index 00000000..16105755 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/expression/GetElementByteCodeExpression.java @@ -0,0 +1,95 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode.expression; + +import com.facebook.presto.byteCode.Block; +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.MethodGenerationContext; +import com.facebook.presto.byteCode.OpCode; +import com.facebook.presto.byteCode.ParameterizedType; +import com.facebook.presto.byteCode.instruction.InstructionNode; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +class GetElementByteCodeExpression + extends ByteCodeExpression +{ + private final ByteCodeExpression instance; + private final ByteCodeExpression index; + private final InstructionNode arrayLoadInstruction; + + public GetElementByteCodeExpression(ByteCodeExpression instance, ByteCodeExpression index) + { + super(instance.getType().getArrayComponentType()); + this.instance = checkNotNull(instance, "instance is null"); + this.index = checkNotNull(index, "index is null"); + checkArgument(index.getType().getPrimitiveType() == int.class, "index must be int type, but is " + index.getType()); + this.arrayLoadInstruction = arrayLoadInstruction(instance.getType().getArrayComponentType()); + } + + @Override + public ByteCodeNode getByteCode(MethodGenerationContext generationContext) + { + return new Block() + .append(instance.getByteCode(generationContext)).append(index) + .append(arrayLoadInstruction); + } + + @Override + protected String formatOneLine() + { + return instance + "[" + index + "]"; + } + + @Override + public List getChildNodes() + { + return ImmutableList.of(instance, index); + } + + private static InstructionNode arrayLoadInstruction(ParameterizedType componentType) + { + Class primitiveType = componentType.getPrimitiveType(); + if (primitiveType == null) { + return OpCode.AALOAD; + } + + if (primitiveType == byte.class || primitiveType == boolean.class) { + return OpCode.BALOAD; + } + if (primitiveType == char.class) { + return OpCode.CALOAD; + } + if (primitiveType == short.class) { + return OpCode.SALOAD; + } + if (primitiveType == int.class) { + return OpCode.IALOAD; + } + if (primitiveType == long.class) { + return OpCode.LALOAD; + } + if (primitiveType == float.class) { + return OpCode.FALOAD; + } + if (primitiveType == double.class) { + return OpCode.DALOAD; + } + throw new IllegalArgumentException("Unsupported array type: " + primitiveType); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/expression/GetFieldByteCodeExpression.java b/presto-main/src/main/java/com/facebook/presto/byteCode/expression/GetFieldByteCodeExpression.java new file mode 100644 index 00000000..87fccb2a --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/expression/GetFieldByteCodeExpression.java @@ -0,0 +1,120 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode.expression; + +import com.facebook.presto.byteCode.Block; +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.FieldDefinition; +import com.facebook.presto.byteCode.MethodGenerationContext; +import com.facebook.presto.byteCode.ParameterizedType; +import com.google.common.collect.ImmutableList; + +import javax.annotation.Nullable; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.List; + +import static com.facebook.presto.byteCode.Access.STATIC; +import static com.facebook.presto.byteCode.ParameterizedType.type; +import static com.facebook.presto.byteCode.instruction.FieldInstruction.getStaticInstruction; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.lang.String.format; + +class GetFieldByteCodeExpression + extends ByteCodeExpression +{ + private final ByteCodeExpression instance; + private final ParameterizedType declaringClass; + private final String name; + + public GetFieldByteCodeExpression(@Nullable ByteCodeExpression instance, Class declaringClass, String name) + { + this(instance, getDeclaredField(declaringClass, name)); + } + + public GetFieldByteCodeExpression(@Nullable ByteCodeExpression instance, Field field) + { + this(instance, type(checkNotNull(field, "field is null").getDeclaringClass()), field.getName(), type(field.getType())); + + boolean isStatic = Modifier.isStatic(field.getModifiers()); + if (instance == null) { + checkArgument(isStatic, "Field is not static: %s", field); + } + else { + checkArgument(!isStatic, "Field is static: %s", field); + } + } + + public GetFieldByteCodeExpression(@Nullable ByteCodeExpression instance, FieldDefinition field) + { + this(instance, checkNotNull(field, "field is null").getDeclaringClass().getType(), field.getName(), field.getType()); + if (instance == null) { + checkArgument(field.getAccess().contains(STATIC), "Field is not static: %s", field); + } + else { + checkArgument(!field.getAccess().contains(STATIC), "Field is static: %s", field); + } + } + + public GetFieldByteCodeExpression(@Nullable ByteCodeExpression instance, ParameterizedType declaringClass, String name, ParameterizedType type) + { + super(type); + checkArgument(instance == null || !instance.getType().isPrimitive(), "Type %s does not have fields", getType()); + this.instance = instance; + this.declaringClass = checkNotNull(declaringClass, "declaringClass is null"); + this.name = checkNotNull(name, "name is null"); + } + + @Override + public ByteCodeNode getByteCode(MethodGenerationContext generationContext) + { + if (instance == null) { + return getStaticInstruction(declaringClass, name, getType()); + } + + return new Block() + .append(instance.getByteCode(generationContext)) + .getField(declaringClass, name, getType()); + } + + @Override + protected String formatOneLine() + { + if (instance == null) { + return declaringClass.getSimpleName() + "." + name; + } + return instance + "." + name; + } + + @Override + public List getChildNodes() + { + return (instance == null) ? ImmutableList.of() : ImmutableList.of(instance); + } + + private static Field getDeclaredField(Class declaringClass, String name) + { + checkNotNull(declaringClass, "declaringClass is null"); + checkNotNull(name, "name is null"); + + try { + return declaringClass.getField(name); + } + catch (NoSuchFieldException e) { + throw new IllegalArgumentException(format("Class %s does not have a '%s' field", declaringClass.getName(), name)); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/expression/InlineIfByteCodeExpression.java b/presto-main/src/main/java/com/facebook/presto/byteCode/expression/InlineIfByteCodeExpression.java new file mode 100644 index 00000000..def72881 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/expression/InlineIfByteCodeExpression.java @@ -0,0 +1,71 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode.expression; + +import com.facebook.presto.byteCode.Block; +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.MethodGenerationContext; +import com.facebook.presto.byteCode.instruction.LabelNode; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +class InlineIfByteCodeExpression + extends ByteCodeExpression +{ + private final ByteCodeExpression condition; + private final ByteCodeExpression ifTrue; + private final ByteCodeExpression ifFalse; + + InlineIfByteCodeExpression(ByteCodeExpression condition, ByteCodeExpression ifTrue, ByteCodeExpression ifFalse) + { + super(ifTrue.getType()); + this.condition = condition; + this.ifTrue = checkNotNull(ifTrue, "ifTrue is null"); + this.ifFalse = checkNotNull(ifFalse, "ifFalse is null"); + + checkArgument(condition.getType().getPrimitiveType() == boolean.class, "Expected condition to be type boolean but is %s", condition.getType()); + checkArgument(ifTrue.getType().equals(ifFalse.getType()), "Expected ifTrue and ifFalse to be the same type"); + } + + @Override + public ByteCodeNode getByteCode(MethodGenerationContext generationContext) + { + LabelNode falseLabel = new LabelNode("false"); + LabelNode endLabel = new LabelNode("end"); + return new Block() + .append(condition) + .ifFalseGoto(falseLabel) + .append(ifTrue) + .gotoLabel(endLabel) + .visitLabel(falseLabel) + .append(ifFalse) + .visitLabel(endLabel); + } + + @Override + public List getChildNodes() + { + return ImmutableList.of(condition, ifTrue, ifFalse); + } + + @Override + protected String formatOneLine() + { + return "(" + condition + " ? " + ifTrue + " : " + ifFalse + ")"; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/expression/InvokeByteCodeExpression.java b/presto-main/src/main/java/com/facebook/presto/byteCode/expression/InvokeByteCodeExpression.java new file mode 100644 index 00000000..29b76a67 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/expression/InvokeByteCodeExpression.java @@ -0,0 +1,118 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode.expression; + +import com.facebook.presto.byteCode.Block; +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.MethodGenerationContext; +import com.facebook.presto.byteCode.ParameterizedType; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; + +import javax.annotation.Nullable; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +class InvokeByteCodeExpression + extends ByteCodeExpression +{ + public static InvokeByteCodeExpression createInvoke( + ByteCodeExpression instance, + String methodName, + ParameterizedType returnType, + Iterable parameterTypes, + Iterable parameters) + { + return new InvokeByteCodeExpression( + checkNotNull(instance, "instance is null"), + instance.getType(), + checkNotNull(methodName, "methodName is null"), + checkNotNull(returnType, "returnType is null"), + checkNotNull(parameterTypes, "parameterTypes is null"), + checkNotNull(parameters, "parameters is null")); + } + + @Nullable + private final ByteCodeExpression instance; + private final ParameterizedType methodTargetType; + private final String methodName; + private final ParameterizedType returnType; + private final List parameters; + private final ImmutableList parameterTypes; + + public InvokeByteCodeExpression( + @Nullable ByteCodeExpression instance, + ParameterizedType methodTargetType, + String methodName, + ParameterizedType returnType, + Iterable parameterTypes, + Iterable parameters) + { + super(checkNotNull(returnType, "returnType is null")); + checkArgument(instance == null || !instance.getType().isPrimitive(), "Type %s does not have methods", getType()); + this.instance = instance; + this.methodTargetType = checkNotNull(methodTargetType, "methodTargetType is null"); + this.methodName = checkNotNull(methodName, "methodName is null"); + this.returnType = returnType; + this.parameterTypes = ImmutableList.copyOf(checkNotNull(parameterTypes, "parameterTypes is null")); + this.parameters = ImmutableList.copyOf(checkNotNull(parameters, "parameters is null")); + } + + @Override + public ByteCodeNode getByteCode(MethodGenerationContext generationContext) + { + Block block = new Block(); + if (instance != null) { + block.append(instance); + } + + for (ByteCodeExpression parameter : parameters) { + block.append(parameter); + } + + if (instance == null) { + return block.invokeStatic(methodTargetType, methodName, returnType, parameterTypes); + } + else if (instance.getType().isInterface()) { + return block.invokeInterface(methodTargetType, methodName, returnType, parameterTypes); + } + else { + return block.invokeVirtual(methodTargetType, methodName, returnType, parameterTypes); + } + } + + @Override + protected String formatOneLine() + { + if (instance == null) { + return methodTargetType.getSimpleName() + "." + methodName + "(" + Joiner.on(", ").join(parameters) + ")"; + } + + return instance + "." + methodName + "(" + Joiner.on(", ").join(parameters) + ")"; + } + + @Override + public List getChildNodes() + { + ImmutableList.Builder children = ImmutableList.builder(); + if (instance != null) { + children.add(instance); + } + children.addAll(parameters); + return children.build(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/expression/InvokeDynamicByteCodeExpression.java b/presto-main/src/main/java/com/facebook/presto/byteCode/expression/InvokeDynamicByteCodeExpression.java new file mode 100644 index 00000000..f85bfb88 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/expression/InvokeDynamicByteCodeExpression.java @@ -0,0 +1,86 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode.expression; + +import com.facebook.presto.byteCode.Block; +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.MethodGenerationContext; +import com.facebook.presto.byteCode.ParameterizedType; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; + +import java.lang.reflect.Method; +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.Iterables.transform; + +class InvokeDynamicByteCodeExpression + extends ByteCodeExpression +{ + private final Method bootstrapMethod; + private final List bootstrapArgs; + private final String methodName; + private final ParameterizedType returnType; + private final List parameters; + private final List parameterTypes; + + InvokeDynamicByteCodeExpression( + Method bootstrapMethod, + Iterable bootstrapArgs, + String methodName, + ParameterizedType returnType, + Iterable parameters, + Iterable parameterTypes) + { + super(returnType); + this.bootstrapMethod = checkNotNull(bootstrapMethod, "bootstrapMethod is null"); + this.bootstrapArgs = ImmutableList.copyOf(checkNotNull(bootstrapArgs, "bootstrapArgs is null")); + this.methodName = checkNotNull(methodName, "methodName is null"); + this.returnType = checkNotNull(returnType, "returnType is null"); + this.parameters = ImmutableList.copyOf(checkNotNull(parameters, "parameters is null")); + this.parameterTypes = ImmutableList.copyOf(checkNotNull(parameterTypes, "parameterTypes is null")); + } + + @Override + public ByteCodeNode getByteCode(MethodGenerationContext generationContext) + { + Block block = new Block(); + for (ByteCodeExpression parameter : parameters) { + block.append(parameter); + } + return block.invokeDynamic(methodName, returnType, parameterTypes, bootstrapMethod, bootstrapArgs); + } + + @Override + public List getChildNodes() + { + return ImmutableList.of(); + } + + @Override + protected String formatOneLine() + { + StringBuilder builder = new StringBuilder(); + builder.append("[").append(bootstrapMethod.getName()); + if (!bootstrapArgs.isEmpty()) { + builder.append("(").append(Joiner.on(", ").join(transform(bootstrapArgs, ConstantByteCodeExpression::renderConstant))).append(")"); + } + builder.append("]=>"); + + builder.append(methodName).append("(").append(Joiner.on(", ").join(parameters)).append(")"); + + return builder.toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/expression/NegateByteCodeExpression.java b/presto-main/src/main/java/com/facebook/presto/byteCode/expression/NegateByteCodeExpression.java new file mode 100644 index 00000000..e6b0ade1 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/expression/NegateByteCodeExpression.java @@ -0,0 +1,68 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode.expression; + +import com.facebook.presto.byteCode.Block; +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.MethodGenerationContext; +import com.facebook.presto.byteCode.OpCode; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.facebook.presto.byteCode.expression.ArithmeticByteCodeExpression.getNumericOpCode; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +class NegateByteCodeExpression + extends ByteCodeExpression +{ + private final ByteCodeExpression value; + private final OpCode negateOpCode; + + NegateByteCodeExpression(ByteCodeExpression value) + { + super(checkNotNull(value, "value is null").getType()); + this.value = value; + + Class type = value.getType().getPrimitiveType(); + checkArgument(type != null, "value is not a primitive"); + checkArgument(type != void.class, "value is void"); + checkArgument(type == int.class || type == long.class || type == float.class || type == double.class, + "value argument must be int, long, float, or double, but is %s", + type); + + negateOpCode = getNumericOpCode("Negate", OpCode.INEG, type); + } + + @Override + public ByteCodeNode getByteCode(MethodGenerationContext generationContext) + { + return new Block() + .append(value) + .append(negateOpCode); + } + + @Override + public List getChildNodes() + { + return ImmutableList.of(value); + } + + @Override + protected String formatOneLine() + { + return "-(" + value + ")"; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/expression/NewInstanceByteCodeExpression.java b/presto-main/src/main/java/com/facebook/presto/byteCode/expression/NewInstanceByteCodeExpression.java new file mode 100644 index 00000000..5d707171 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/expression/NewInstanceByteCodeExpression.java @@ -0,0 +1,67 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode.expression; + +import com.facebook.presto.byteCode.Block; +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.MethodGenerationContext; +import com.facebook.presto.byteCode.ParameterizedType; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; + +class NewInstanceByteCodeExpression + extends ByteCodeExpression +{ + private final List parameters; + private final ImmutableList parameterTypes; + + public NewInstanceByteCodeExpression( + ParameterizedType type, + Iterable parameterTypes, + Iterable parameters) + { + super(type); + this.parameterTypes = ImmutableList.copyOf(checkNotNull(parameterTypes, "parameterTypes is null")); + this.parameters = ImmutableList.copyOf(checkNotNull(parameters, "parameters is null")); + } + + @Override + public ByteCodeNode getByteCode(MethodGenerationContext generationContext) + { + Block block = new Block() + .newObject(getType()) + .dup(); + + for (ByteCodeExpression parameter : parameters) { + block.append(parameter); + } + return block.invokeConstructor(getType(), parameterTypes); + } + + @Override + protected String formatOneLine() + { + return "new " + getType().getSimpleName() + "(" + Joiner.on(", ").join(parameters) + ")"; + } + + @Override + public List getChildNodes() + { + return ImmutableList.copyOf(parameters); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/expression/NotByteCodeExpression.java b/presto-main/src/main/java/com/facebook/presto/byteCode/expression/NotByteCodeExpression.java new file mode 100644 index 00000000..d1b0268a --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/expression/NotByteCodeExpression.java @@ -0,0 +1,65 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode.expression; + +import com.facebook.presto.byteCode.Block; +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.MethodGenerationContext; +import com.facebook.presto.byteCode.instruction.LabelNode; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.facebook.presto.byteCode.ParameterizedType.type; +import static com.google.common.base.Preconditions.checkArgument; + +class NotByteCodeExpression + extends ByteCodeExpression +{ + private final ByteCodeExpression value; + + NotByteCodeExpression(ByteCodeExpression value) + { + super(type(boolean.class)); + this.value = value; + checkArgument(value.getType().getPrimitiveType() == boolean.class, "Expected value to be type boolean but is %s", value.getType()); + } + + @Override + public ByteCodeNode getByteCode(MethodGenerationContext generationContext) + { + LabelNode trueLabel = new LabelNode("true"); + LabelNode endLabel = new LabelNode("end"); + return new Block() + .append(value) + .ifTrueGoto(trueLabel) + .push(true) + .gotoLabel(endLabel) + .visitLabel(trueLabel) + .push(false) + .visitLabel(endLabel); + } + + @Override + public List getChildNodes() + { + return ImmutableList.of(value); + } + + @Override + protected String formatOneLine() + { + return "(!" + value + ")"; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/expression/OrByteCodeExpression.java b/presto-main/src/main/java/com/facebook/presto/byteCode/expression/OrByteCodeExpression.java new file mode 100644 index 00000000..f70876a4 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/expression/OrByteCodeExpression.java @@ -0,0 +1,71 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode.expression; + +import com.facebook.presto.byteCode.Block; +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.MethodGenerationContext; +import com.facebook.presto.byteCode.instruction.LabelNode; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.facebook.presto.byteCode.ParameterizedType.type; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +class OrByteCodeExpression + extends ByteCodeExpression +{ + private final ByteCodeExpression left; + private final ByteCodeExpression right; + + OrByteCodeExpression(ByteCodeExpression left, ByteCodeExpression right) + { + super(type(boolean.class)); + this.left = checkNotNull(left, "left is null"); + checkArgument(left.getType().getPrimitiveType() == boolean.class, "Expected left to be type boolean but is %s", left.getType()); + this.right = checkNotNull(right, "right is null"); + checkArgument(right.getType().getPrimitiveType() == boolean.class, "Expected right to be type boolean but is %s", right.getType()); + } + + @Override + public ByteCodeNode getByteCode(MethodGenerationContext generationContext) + { + LabelNode trueLabel = new LabelNode("true"); + LabelNode endLabel = new LabelNode("end"); + return new Block() + .append(left) + .ifTrueGoto(trueLabel) + .append(right) + .ifTrueGoto(trueLabel) + .push(false) + .gotoLabel(endLabel) + .visitLabel(trueLabel) + .push(true) + .visitLabel(endLabel); + } + + @Override + public List getChildNodes() + { + return ImmutableList.of(left, right); + } + + @Override + protected String formatOneLine() + { + return "(" + left + " || " + right + ")"; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/expression/PopByteCodeExpression.java b/presto-main/src/main/java/com/facebook/presto/byteCode/expression/PopByteCodeExpression.java new file mode 100644 index 00000000..4b99188e --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/expression/PopByteCodeExpression.java @@ -0,0 +1,56 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode.expression; + +import com.facebook.presto.byteCode.Block; +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.MethodGenerationContext; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.facebook.presto.byteCode.ParameterizedType.type; +import static com.google.common.base.Preconditions.checkNotNull; + +class PopByteCodeExpression + extends ByteCodeExpression +{ + private final ByteCodeExpression instance; + + PopByteCodeExpression(ByteCodeExpression instance) + { + super(type(void.class)); + this.instance = checkNotNull(instance, "instance is null"); + } + + @Override + public ByteCodeNode getByteCode(MethodGenerationContext generationContext) + { + return new Block() + .append(instance.getByteCode(generationContext)) + .pop(instance.getType()); + } + + @Override + protected String formatOneLine() + { + return instance.toString(); + } + + @Override + public List getChildNodes() + { + return ImmutableList.of(instance); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/expression/ReturnByteCodeExpression.java b/presto-main/src/main/java/com/facebook/presto/byteCode/expression/ReturnByteCodeExpression.java new file mode 100644 index 00000000..8d8eec74 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/expression/ReturnByteCodeExpression.java @@ -0,0 +1,90 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode.expression; + +import com.facebook.presto.byteCode.Block; +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.MethodGenerationContext; +import com.facebook.presto.byteCode.OpCode; +import com.facebook.presto.byteCode.ParameterizedType; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.facebook.presto.byteCode.ParameterizedType.type; +import static com.google.common.base.Preconditions.checkNotNull; + +class ReturnByteCodeExpression + extends ByteCodeExpression +{ + private final ByteCodeExpression instance; + private final OpCode returnOpCode; + + ReturnByteCodeExpression(ByteCodeExpression instance) + { + super(type(void.class)); + this.instance = checkNotNull(instance, "instance is null"); + this.returnOpCode = returnOpCode(instance.getType()); + } + + @Override + public ByteCodeNode getByteCode(MethodGenerationContext generationContext) + { + return new Block() + .append(instance.getByteCode(generationContext)) + .append(returnOpCode); + } + + @Override + protected String formatOneLine() + { + return "return " + instance; + } + + @Override + public List getChildNodes() + { + return ImmutableList.of(instance); + } + + private static OpCode returnOpCode(ParameterizedType componentType) + { + Class primitiveType = componentType.getPrimitiveType(); + if (primitiveType != null) { + if (primitiveType == byte.class || + primitiveType == boolean.class || + primitiveType == char.class || + primitiveType == short.class || + primitiveType == int.class) { + return OpCode.IRETURN; + } + if (primitiveType == long.class) { + return OpCode.LRETURN; + } + if (primitiveType == float.class) { + return OpCode.FRETURN; + } + if (primitiveType == double.class) { + return OpCode.DRETURN; + } + if (primitiveType == void.class) { + return OpCode.RETURN; + } + throw new IllegalArgumentException("Unsupported array type: " + primitiveType); + } + else { + return OpCode.ARETURN; + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/expression/SetFieldByteCodeExpression.java b/presto-main/src/main/java/com/facebook/presto/byteCode/expression/SetFieldByteCodeExpression.java new file mode 100644 index 00000000..33e1a4dc --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/expression/SetFieldByteCodeExpression.java @@ -0,0 +1,144 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode.expression; + +import com.facebook.presto.byteCode.Block; +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.FieldDefinition; +import com.facebook.presto.byteCode.MethodGenerationContext; +import com.facebook.presto.byteCode.ParameterizedType; +import com.google.common.collect.ImmutableList; + +import javax.annotation.Nullable; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.List; + +import static com.facebook.presto.byteCode.Access.STATIC; +import static com.facebook.presto.byteCode.ParameterizedType.type; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.lang.String.format; + +class SetFieldByteCodeExpression + extends ByteCodeExpression +{ + private final ByteCodeExpression instance; + private final ParameterizedType declaringClass; + private final String name; + private final ByteCodeExpression value; + private final ParameterizedType fieldType; + + public SetFieldByteCodeExpression(@Nullable ByteCodeExpression instance, Class declaringClass, String name, ByteCodeExpression value) + { + this(instance, getDeclaredField(declaringClass, name), value); + } + + public SetFieldByteCodeExpression(@Nullable ByteCodeExpression instance, Field field, ByteCodeExpression value) + { + this(instance, type(checkNotNull(field, "field is null").getDeclaringClass()), field.getName(), value, type(field.getType())); + + boolean isStatic = Modifier.isStatic(field.getModifiers()); + if (instance == null) { + checkArgument(isStatic, "Field is not static: %s", field); + } + else { + checkArgument(!isStatic, "Field is static: %s", field); + } + } + + public SetFieldByteCodeExpression(@Nullable ByteCodeExpression instance, FieldDefinition field, ByteCodeExpression value) + { + this(instance, checkNotNull(field, "field is null").getDeclaringClass().getType(), field.getName(), value, field.getType()); + if (instance == null) { + checkArgument(field.getAccess().contains(STATIC), "Field is not static: %s", field); + } + else { + checkArgument(!field.getAccess().contains(STATIC), "Field is static: %s", field); + } + } + + public SetFieldByteCodeExpression(@Nullable ByteCodeExpression instance, ParameterizedType declaringClass, String name, ByteCodeExpression value) + { + this(instance, declaringClass, name, value, value.getType()); + } + + public SetFieldByteCodeExpression(@Nullable ByteCodeExpression instance, + ParameterizedType declaringClass, + String name, + ByteCodeExpression value, + ParameterizedType fieldType) + { + super(type(void.class)); + if (instance != null) { + checkArgument(!instance.getType().isPrimitive(), "Type %s does not have fields", instance.getType()); + } + this.instance = instance; + this.declaringClass = checkNotNull(declaringClass, "declaringClass is null"); + this.name = checkNotNull(name, "name is null"); + this.fieldType = checkNotNull(fieldType, "fieldType is null"); + this.value = checkNotNull(value, "value is null"); + } + + @Override + public ByteCodeNode getByteCode(MethodGenerationContext generationContext) + { + if (instance == null) { + return new Block() + .append(value.getByteCode(generationContext)) + .putStaticField(declaringClass, name, fieldType); + } + + return new Block() + .append(instance.getByteCode(generationContext)) + .append(value.getByteCode(generationContext)) + .putField(declaringClass, name, fieldType); + } + + @Override + protected String formatOneLine() + { + if (instance == null) { + return declaringClass.getSimpleName() + "." + name + " = " + value; + } + else { + return instance + "." + name + " = " + value; + } + } + + @Override + public List getChildNodes() + { + ImmutableList.Builder children = ImmutableList.builder(); + if (instance != null) { + children.add(instance); + } + children.add(value); + return children.build(); + } + + private static Field getDeclaredField(Class declaringClass, String name) + { + checkNotNull(declaringClass, "declaringClass is null"); + checkNotNull(name, "name is null"); + + try { + return declaringClass.getField(name); + } + catch (NoSuchFieldException e) { + throw new IllegalArgumentException(format("Class %s does not have a '%s' field", declaringClass.getName(), name)); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/instruction/Constant.java b/presto-main/src/main/java/com/facebook/presto/byteCode/instruction/Constant.java new file mode 100644 index 00000000..8e8b8b03 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/instruction/Constant.java @@ -0,0 +1,623 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode.instruction; + +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.ByteCodeVisitor; +import com.facebook.presto.byteCode.MethodGenerationContext; +import com.facebook.presto.byteCode.ParameterizedType; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Type; + +import java.util.List; + +import static com.facebook.presto.byteCode.OpCode.ACONST_NULL; +import static com.facebook.presto.byteCode.OpCode.BIPUSH; +import static com.facebook.presto.byteCode.OpCode.DCONST_0; +import static com.facebook.presto.byteCode.OpCode.DCONST_1; +import static com.facebook.presto.byteCode.OpCode.FCONST_0; +import static com.facebook.presto.byteCode.OpCode.FCONST_1; +import static com.facebook.presto.byteCode.OpCode.FCONST_2; +import static com.facebook.presto.byteCode.OpCode.ICONST_0; +import static com.facebook.presto.byteCode.OpCode.ICONST_1; +import static com.facebook.presto.byteCode.OpCode.ICONST_2; +import static com.facebook.presto.byteCode.OpCode.ICONST_3; +import static com.facebook.presto.byteCode.OpCode.ICONST_4; +import static com.facebook.presto.byteCode.OpCode.ICONST_5; +import static com.facebook.presto.byteCode.OpCode.ICONST_M1; +import static com.facebook.presto.byteCode.OpCode.LCONST_0; +import static com.facebook.presto.byteCode.OpCode.LCONST_1; +import static com.facebook.presto.byteCode.OpCode.SIPUSH; +import static com.facebook.presto.byteCode.ParameterizedType.type; +import static com.facebook.presto.byteCode.instruction.FieldInstruction.getStaticInstruction; +import static com.facebook.presto.byteCode.instruction.InvokeInstruction.invokeStatic; +import static com.google.common.base.MoreObjects.toStringHelper; + +@SuppressWarnings("UnusedDeclaration") +public abstract class Constant + implements InstructionNode +{ + public static Constant loadNull() + { + return new NullConstant(); + } + + public static Constant loadBoolean(boolean value) + { + return new BooleanConstant(value); + } + + public static Constant loadBoxedBoolean(boolean value) + { + return new BoxedBooleanConstant(value); + } + + public static Constant loadInt(int value) + { + return new IntConstant(value); + } + + public static Constant loadBoxedInt(int value) + { + return new BoxedIntegerConstant(value); + } + + public static Constant loadFloat(float value) + { + return new FloatConstant(value); + } + + public static Constant loadBoxedFloat(float value) + { + return new BoxedFloatConstant(value); + } + + public static Constant loadLong(long value) + { + return new LongConstant(value); + } + + public static Constant loadBoxedLong(long value) + { + return new BoxedLongConstant(value); + } + + public static Constant loadDouble(double value) + { + return new DoubleConstant(value); + } + + public static Constant loadBoxedDouble(double value) + { + return new BoxedDoubleConstant(value); + } + + public static Constant loadNumber(Number value) + { + Preconditions.checkNotNull(value, "value is null"); + if (value instanceof Byte) { + return loadInt((value).intValue()); + } + if (value instanceof Short) { + return loadInt((value).intValue()); + } + if (value instanceof Integer) { + return loadInt((Integer) value); + } + if (value instanceof Long) { + return loadLong((Long) value); + } + if (value instanceof Float) { + return loadFloat((Float) value); + } + if (value instanceof Double) { + return loadDouble((Double) value); + } + throw new IllegalStateException("Unsupported number type " + value.getClass().getSimpleName()); + } + + public static Constant loadString(String value) + { + Preconditions.checkNotNull(value, "value is null"); + return new StringConstant(value); + } + + public static Constant loadClass(Class value) + { + Preconditions.checkNotNull(value, "value is null"); + return new ClassConstant(type(value)); + } + + public static Constant loadClass(ParameterizedType value) + { + Preconditions.checkNotNull(value, "value is null"); + return new ClassConstant(value); + } + + public abstract Object getValue(); + + @Override + public List getChildNodes() + { + return ImmutableList.of(); + } + + @Override + public T accept(ByteCodeNode parent, ByteCodeVisitor visitor) + { + return visitor.visitConstant(parent, this); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("value", getValue()) + .toString(); + } + + public static class NullConstant + extends Constant + { + @Override + public Object getValue() + { + return null; + } + + @Override + public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) + { + visitor.visitInsn(ACONST_NULL.getOpCode()); + } + + @Override + public T accept(ByteCodeNode parent, ByteCodeVisitor visitor) + { + return visitor.visitConstant(parent, this); + } + } + + public static class BooleanConstant + extends Constant + { + private final boolean value; + + private BooleanConstant(boolean value) + { + this.value = value; + } + + @Override + public Boolean getValue() + { + return value; + } + + @Override + public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) + { + if (value) { + visitor.visitInsn(ICONST_1.getOpCode()); + } + else { + visitor.visitInsn(ICONST_0.getOpCode()); + } + } + + @Override + public T accept(ByteCodeNode parent, ByteCodeVisitor visitor) + { + return visitor.visitBooleanConstant(parent, this); + } + } + + public static class BoxedBooleanConstant + extends Constant + { + private final boolean value; + + private BoxedBooleanConstant(boolean value) + { + this.value = value; + } + + @Override + public Boolean getValue() + { + return value; + } + + @Override + public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) + { + if (value) { + getStaticInstruction(Boolean.class, "TRUE", Boolean.class).accept(visitor, generationContext); + } + else { + getStaticInstruction(Boolean.class, "FALSE", Boolean.class).accept(visitor, generationContext); + } + } + + @Override + public T accept(ByteCodeNode parent, ByteCodeVisitor visitor) + { + return visitor.visitBoxedBooleanConstant(parent, this); + } + } + + public static class IntConstant + extends Constant + { + private final int value; + + private IntConstant(int value) + { + this.value = value; + } + + @Override + public Integer getValue() + { + return value; + } + + @Override + public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) + { + if (value <= Byte.MAX_VALUE && value >= Byte.MIN_VALUE) { + switch (value) { + case -1: + visitor.visitInsn(ICONST_M1.getOpCode()); + break; + case 0: + visitor.visitInsn(ICONST_0.getOpCode()); + break; + case 1: + visitor.visitInsn(ICONST_1.getOpCode()); + break; + case 2: + visitor.visitInsn(ICONST_2.getOpCode()); + break; + case 3: + visitor.visitInsn(ICONST_3.getOpCode()); + break; + case 4: + visitor.visitInsn(ICONST_4.getOpCode()); + break; + case 5: + visitor.visitInsn(ICONST_5.getOpCode()); + break; + default: + visitor.visitIntInsn(BIPUSH.getOpCode(), value); + break; + } + } + else if (value <= Short.MAX_VALUE && value >= Short.MIN_VALUE) { + visitor.visitIntInsn(SIPUSH.getOpCode(), value); + } + else { + visitor.visitLdcInsn(value); + } + } + + @Override + public T accept(ByteCodeNode parent, ByteCodeVisitor visitor) + { + return visitor.visitIntConstant(parent, this); + } + } + + public static class BoxedIntegerConstant + extends Constant + { + private final int value; + + private BoxedIntegerConstant(int value) + { + this.value = value; + } + + @Override + public Integer getValue() + { + return value; + } + + @Override + public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) + { + loadInt(value).accept(visitor, generationContext); + invokeStatic(Integer.class, "valueOf", Integer.class, int.class).accept(visitor, generationContext); + } + + @Override + public T accept(ByteCodeNode parent, ByteCodeVisitor visitor) + { + return visitor.visitBoxedIntegerConstant(parent, this); + } + } + + public static class FloatConstant + extends Constant + { + private final float value; + + private FloatConstant(float value) + { + this.value = value; + } + + @Override + public Float getValue() + { + return value; + } + + @Override + @SuppressWarnings("FloatingPointEquality") + public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) + { + // We can not use "value == 0.0" because when value is "-0.0" the expression + // will evaluate to true and we would convert "-0.0" to "0.0" which is + // not the same value + if (Float.floatToIntBits(value) == Float.floatToIntBits(0.0f)) { + visitor.visitInsn(FCONST_0.getOpCode()); + } + else if (value == 1.0f) { + visitor.visitInsn(FCONST_1.getOpCode()); + } + else if (value == 2.0f) { + visitor.visitInsn(FCONST_2.getOpCode()); + } + else { + visitor.visitLdcInsn(value); + } + } + + @Override + public T accept(ByteCodeNode parent, ByteCodeVisitor visitor) + { + return visitor.visitFloatConstant(parent, this); + } + } + + public static class BoxedFloatConstant + extends Constant + { + private final float value; + + private BoxedFloatConstant(float value) + { + this.value = value; + } + + @Override + public Float getValue() + { + return value; + } + + @Override + public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) + { + loadFloat(value).accept(visitor, generationContext); + invokeStatic(Float.class, "valueOf", Float.class, float.class).accept(visitor, generationContext); + } + + @Override + public T accept(ByteCodeNode parent, ByteCodeVisitor visitor) + { + return visitor.visitBoxedFloatConstant(parent, this); + } + } + + public static class LongConstant + extends Constant + { + private final long value; + + private LongConstant(long value) + { + this.value = value; + } + + @Override + public Long getValue() + { + return value; + } + + @Override + public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) + { + if (value == 0) { + visitor.visitInsn(LCONST_0.getOpCode()); + } + else if (value == 1) { + visitor.visitInsn(LCONST_1.getOpCode()); + } + else { + visitor.visitLdcInsn(value); + } + } + + @Override + public T accept(ByteCodeNode parent, ByteCodeVisitor visitor) + { + return visitor.visitLongConstant(parent, this); + } + } + + public static class BoxedLongConstant + extends Constant + { + private final long value; + + private BoxedLongConstant(long value) + { + this.value = value; + } + + @Override + public Long getValue() + { + return value; + } + + @Override + public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) + { + loadLong(value).accept(visitor, generationContext); + invokeStatic(Long.class, "valueOf", Long.class, long.class).accept(visitor, generationContext); + } + + @Override + public T accept(ByteCodeNode parent, ByteCodeVisitor visitor) + { + return visitor.visitBoxedLongConstant(parent, this); + } + } + + public static class DoubleConstant + extends Constant + { + private final double value; + + private DoubleConstant(double value) + { + this.value = value; + } + + @Override + public Double getValue() + { + return value; + } + + @Override + @SuppressWarnings("FloatingPointEquality") + public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) + { + // We can not use "value == 0.0" because when value is "-0.0" the expression + // will evaluate to true and we would convert "-0.0" to "0.0" which is + // not the same value + if (Double.doubleToLongBits(value) == Double.doubleToLongBits(0.0)) { + visitor.visitInsn(DCONST_0.getOpCode()); + } + else if (value == 1.0) { + visitor.visitInsn(DCONST_1.getOpCode()); + } + else { + visitor.visitLdcInsn(value); + } + } + + @Override + public T accept(ByteCodeNode parent, ByteCodeVisitor visitor) + { + return visitor.visitDoubleConstant(parent, this); + } + } + + public static class BoxedDoubleConstant + extends Constant + { + private final double value; + + private BoxedDoubleConstant(double value) + { + this.value = value; + } + + @Override + public Double getValue() + { + return value; + } + + @Override + public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) + { + loadDouble(value).accept(visitor, generationContext); + invokeStatic(Double.class, "valueOf", Double.class, double.class).accept(visitor, generationContext); + } + + @Override + public T accept(ByteCodeNode parent, ByteCodeVisitor visitor) + { + return visitor.visitBoxedDoubleConstant(parent, this); + } + } + + public static class StringConstant + extends Constant + { + private final String value; + + private StringConstant(String value) + { + this.value = value; + } + + @Override + public String getValue() + { + return value; + } + + @Override + public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) + { + visitor.visitLdcInsn(value); + } + + @Override + public T accept(ByteCodeNode parent, ByteCodeVisitor visitor) + { + return visitor.visitStringConstant(parent, this); + } + } + + public static class ClassConstant + extends Constant + { + private final ParameterizedType value; + + private ClassConstant(ParameterizedType value) + { + this.value = value; + } + + @Override + public ParameterizedType getValue() + { + return value; + } + + @Override + public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) + { + visitor.visitLdcInsn(Type.getType(value.getType())); + } + + @Override + public T accept(ByteCodeNode parent, ByteCodeVisitor visitor) + { + return visitor.visitClassConstant(parent, this); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/instruction/FieldInstruction.java b/presto-main/src/main/java/com/facebook/presto/byteCode/instruction/FieldInstruction.java new file mode 100644 index 00000000..558065f9 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/instruction/FieldInstruction.java @@ -0,0 +1,177 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode.instruction; + +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.ByteCodeVisitor; +import com.facebook.presto.byteCode.MethodGenerationContext; +import com.facebook.presto.byteCode.OpCode; +import com.facebook.presto.byteCode.ParameterizedType; +import com.google.common.collect.ImmutableList; +import org.objectweb.asm.MethodVisitor; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.List; + +import static com.facebook.presto.byteCode.OpCode.GETFIELD; +import static com.facebook.presto.byteCode.OpCode.GETSTATIC; +import static com.facebook.presto.byteCode.OpCode.PUTFIELD; +import static com.facebook.presto.byteCode.OpCode.PUTSTATIC; +import static com.facebook.presto.byteCode.ParameterizedType.type; +import static com.google.common.base.MoreObjects.toStringHelper; + +public abstract class FieldInstruction + implements InstructionNode +{ + public static FieldInstruction getFieldInstruction(Field field) + { + boolean isStatic = Modifier.isStatic(field.getModifiers()); + return new GetFieldInstruction(isStatic, type(field.getDeclaringClass()), field.getName(), type(field.getType())); + } + + public static FieldInstruction putFieldInstruction(Field field) + { + boolean isStatic = Modifier.isStatic(field.getModifiers()); + return new PutFieldInstruction(isStatic, type(field.getDeclaringClass()), field.getName(), type(field.getType())); + } + + public static FieldInstruction getFieldInstruction(ParameterizedType classType, String fieldName, ParameterizedType fieldType) + { + return new GetFieldInstruction(false, classType, fieldName, fieldType); + } + + public static FieldInstruction getFieldInstruction(Class classType, String fieldName, Class fieldType) + { + return new GetFieldInstruction(false, classType, fieldName, fieldType); + } + + public static FieldInstruction putFieldInstruction(ParameterizedType classType, String fieldName, ParameterizedType fieldType) + { + return new PutFieldInstruction(false, classType, fieldName, fieldType); + } + + public static FieldInstruction putFieldInstruction(Class classType, String fieldName, Class fieldType) + { + return new PutFieldInstruction(false, classType, fieldName, fieldType); + } + + public static FieldInstruction getStaticInstruction(ParameterizedType classType, String fieldName, ParameterizedType fieldType) + { + return new GetFieldInstruction(true, classType, fieldName, fieldType); + } + + public static FieldInstruction getStaticInstruction(Class classType, String fieldName, Class fieldType) + { + return new GetFieldInstruction(true, classType, fieldName, fieldType); + } + + public static FieldInstruction putStaticInstruction(ParameterizedType classType, String fieldName, ParameterizedType fieldType) + { + return new PutFieldInstruction(true, classType, fieldName, fieldType); + } + + public static FieldInstruction putStaticInstruction(Class classType, String fieldName, Class fieldType) + { + return new PutFieldInstruction(true, classType, fieldName, fieldType); + } + + private final boolean isStatic; + + private final OpCode opCode; + + private final ParameterizedType classType; + + private final String fieldName; + + private final ParameterizedType fieldType; + + private FieldInstruction(boolean isStatic, OpCode opCode, ParameterizedType classType, String fieldName, ParameterizedType fieldType) + { + this.isStatic = isStatic; + this.opCode = opCode; + this.classType = classType; + this.fieldName = fieldName; + this.fieldType = fieldType; + } + + @Override + public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) + { + visitor.visitFieldInsn(opCode.getOpCode(), classType.getClassName(), fieldName, fieldType.getType()); + } + + @Override + public List getChildNodes() + { + return ImmutableList.of(); + } + + @Override + public T accept(ByteCodeNode parent, ByteCodeVisitor visitor) + { + return visitor.visitFieldInstruction(parent, this); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("isStatic", isStatic) + .add("classType", classType) + .add("fieldName", fieldName) + .add("fieldType", fieldType) + .toString(); + } + + public static class GetFieldInstruction + extends FieldInstruction + { + public GetFieldInstruction(boolean isStatic, ParameterizedType classType, String fieldName, ParameterizedType fieldType) + { + super(isStatic, isStatic ? GETSTATIC : GETFIELD, classType, fieldName, fieldType); + } + + public GetFieldInstruction(boolean isStatic, Class classType, String fieldName, Class fieldType) + { + super(isStatic, isStatic ? GETSTATIC : GETFIELD, type(classType), fieldName, type(fieldType)); + } + + @Override + public T accept(ByteCodeNode parent, ByteCodeVisitor visitor) + { + return visitor.visitGetField(parent, this); + } + } + + public static class PutFieldInstruction + extends FieldInstruction + { + public PutFieldInstruction(boolean isStatic, ParameterizedType classType, String fieldName, ParameterizedType fieldType) + { + super(isStatic, isStatic ? PUTSTATIC : PUTFIELD, classType, fieldName, fieldType); + } + + public PutFieldInstruction(boolean isStatic, Class classType, String fieldName, Class fieldType) + { + super(isStatic, isStatic ? PUTSTATIC : PUTFIELD, type(classType), fieldName, type(fieldType)); + } + + @Override + public T accept(ByteCodeNode parent, ByteCodeVisitor visitor) + { + return visitor.visitPutField(parent, this); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/instruction/InstructionNode.java b/presto-main/src/main/java/com/facebook/presto/byteCode/instruction/InstructionNode.java new file mode 100644 index 00000000..ea937894 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/instruction/InstructionNode.java @@ -0,0 +1,21 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode.instruction; + +import com.facebook.presto.byteCode.ByteCodeNode; + +public interface InstructionNode + extends ByteCodeNode +{ +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/instruction/InvokeInstruction.java b/presto-main/src/main/java/com/facebook/presto/byteCode/instruction/InvokeInstruction.java new file mode 100644 index 00000000..db909acf --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/instruction/InvokeInstruction.java @@ -0,0 +1,442 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode.instruction; + +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.ByteCodeVisitor; +import com.facebook.presto.byteCode.MethodDefinition; +import com.facebook.presto.byteCode.MethodGenerationContext; +import com.facebook.presto.byteCode.OpCode; +import com.facebook.presto.byteCode.ParameterizedType; +import com.google.common.base.CharMatcher; +import com.google.common.collect.ImmutableList; +import org.objectweb.asm.Handle; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +import java.lang.invoke.MethodType; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.List; + +import static com.facebook.presto.byteCode.MethodDefinition.methodDescription; +import static com.facebook.presto.byteCode.OpCode.INVOKEDYNAMIC; +import static com.facebook.presto.byteCode.OpCode.INVOKEINTERFACE; +import static com.facebook.presto.byteCode.OpCode.INVOKESPECIAL; +import static com.facebook.presto.byteCode.OpCode.INVOKESTATIC; +import static com.facebook.presto.byteCode.OpCode.INVOKEVIRTUAL; +import static com.facebook.presto.byteCode.ParameterizedType.type; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.Iterables.transform; + +@SuppressWarnings("UnusedDeclaration") +public class InvokeInstruction + implements InstructionNode +{ + // + // Invoke Static + // + + public static InstructionNode invokeStatic(Method method) + { + return invoke(INVOKESTATIC, method); + } + + public static InstructionNode invokeStatic(MethodDefinition method) + { + return invoke(INVOKESTATIC, method); + } + + public static InstructionNode invokeStatic(Class target, String name, Class returnType, Class... parameterTypes) + { + return invoke(INVOKESTATIC, target, name, returnType, ImmutableList.copyOf(parameterTypes)); + } + + public static InstructionNode invokeStatic(Class target, String name, Class returnType, Iterable> parameterTypes) + { + return invoke(INVOKESTATIC, target, name, returnType, parameterTypes); + } + + public static InstructionNode invokeStatic(ParameterizedType target, String name, ParameterizedType returnType, ParameterizedType... parameterTypes) + { + return invoke(INVOKESTATIC, target, name, returnType, ImmutableList.copyOf(parameterTypes)); + } + + public static InstructionNode invokeStatic(ParameterizedType target, String name, ParameterizedType returnType, Iterable parameterTypes) + { + return invoke(INVOKESTATIC, target, name, returnType, parameterTypes); + } + + // + // Invoke Virtual + // + + public static InstructionNode invokeVirtual(Method method) + { + return invoke(INVOKEVIRTUAL, method); + } + + public static InstructionNode invokeVirtual(MethodDefinition method) + { + return invoke(INVOKEVIRTUAL, method); + } + + public static InstructionNode invokeVirtual(Class target, String name, Class returnType, Class... parameterTypes) + { + return invoke(INVOKEVIRTUAL, target, name, returnType, ImmutableList.copyOf(parameterTypes)); + } + + public static InstructionNode invokeVirtual(Class target, String name, Class returnType, Iterable> parameterTypes) + { + return invoke(INVOKEVIRTUAL, target, name, returnType, parameterTypes); + } + + public static InstructionNode invokeVirtual(ParameterizedType target, String name, ParameterizedType returnType, ParameterizedType... parameterTypes) + { + return invoke(INVOKEVIRTUAL, target, name, returnType, ImmutableList.copyOf(parameterTypes)); + } + + public static InstructionNode invokeVirtual(ParameterizedType target, String name, ParameterizedType returnType, Iterable parameterTypes) + { + return invoke(INVOKEVIRTUAL, target, name, returnType, parameterTypes); + } + + // + // Invoke Interface + // + + public static InstructionNode invokeInterface(Method method) + { + return invoke(INVOKEINTERFACE, method); + } + + public static InstructionNode invokeInterface(MethodDefinition method) + { + return invoke(INVOKEINTERFACE, method); + } + + public static InstructionNode invokeInterface(Class target, String name, Class returnType, Class... parameterTypes) + { + return invoke(INVOKEINTERFACE, target, name, returnType, ImmutableList.copyOf(parameterTypes)); + } + + public static InstructionNode invokeInterface(Class target, String name, Class returnType, Iterable> parameterTypes) + { + return invoke(INVOKEINTERFACE, target, name, returnType, parameterTypes); + } + + public static InstructionNode invokeInterface(ParameterizedType target, String name, ParameterizedType returnType, ParameterizedType... parameterTypes) + { + return invoke(INVOKEINTERFACE, target, name, returnType, ImmutableList.copyOf(parameterTypes)); + } + + public static InstructionNode invokeInterface(ParameterizedType target, String name, ParameterizedType returnType, Iterable parameterTypes) + { + return invoke(INVOKEINTERFACE, target, name, returnType, parameterTypes); + } + + // + // Invoke Constructor + // + + public static InstructionNode invokeConstructor(Constructor constructor) + { + return invokeConstructor(constructor.getDeclaringClass(), constructor.getParameterTypes()); + } + + public static InstructionNode invokeConstructor(Class target, Class... parameterTypes) + { + return invokeConstructor(type(target), transform(ImmutableList.copyOf(parameterTypes), ParameterizedType::type)); + } + + public static InstructionNode invokeConstructor(Class target, Iterable> parameterTypes) + { + return invokeConstructor(type(target), transform(parameterTypes, ParameterizedType::type)); + } + + public static InstructionNode invokeConstructor(ParameterizedType target, ParameterizedType... parameterTypes) + { + return invokeConstructor(target, ImmutableList.copyOf(parameterTypes)); + } + + public static InstructionNode invokeConstructor(ParameterizedType target, Iterable parameterTypes) + { + return invokeSpecial(target, "", type(void.class), parameterTypes); + } + + // + // Invoke Special + // + + public static InstructionNode invokeSpecial(Method method) + { + return invoke(INVOKESPECIAL, method); + } + + public static InstructionNode invokeSpecial(MethodDefinition method) + { + return invoke(INVOKESPECIAL, method); + } + + public static InstructionNode invokeSpecial(Class target, String name, Class returnType, Class... parameterTypes) + { + return invoke(INVOKESPECIAL, target, name, returnType, ImmutableList.copyOf(parameterTypes)); + } + + public static InstructionNode invokeSpecial(Class target, String name, Class returnType, Iterable> parameterTypes) + { + return invoke(INVOKESPECIAL, target, name, returnType, parameterTypes); + } + + public static InstructionNode invokeSpecial(ParameterizedType target, String name, ParameterizedType returnType, ParameterizedType... parameterTypes) + { + return invoke(INVOKESPECIAL, target, name, returnType, ImmutableList.copyOf(parameterTypes)); + } + + public static InstructionNode invokeSpecial(ParameterizedType target, String name, ParameterizedType returnType, Iterable parameterTypes) + { + return invoke(INVOKESPECIAL, target, name, returnType, parameterTypes); + } + + // + // Generic + // + + private static InstructionNode invoke(OpCode invocationType, Method method) + { + return new InvokeInstruction(invocationType, + type(method.getDeclaringClass()), + method.getName(), + type(method.getReturnType()), + transform(ImmutableList.copyOf(method.getParameterTypes()), ParameterizedType::type)); + } + + private static InstructionNode invoke(OpCode invocationType, MethodDefinition method) + { + return new InvokeInstruction(invocationType, + method.getDeclaringClass().getType(), + method.getName(), + method.getReturnType(), + method.getParameterTypes()); + } + + private static InstructionNode invoke(OpCode invocationType, ParameterizedType target, String name, ParameterizedType returnType, Iterable parameterTypes) + { + return new InvokeInstruction(invocationType, + target, + name, + returnType, + parameterTypes); + } + + private static InstructionNode invoke(OpCode invocationType, Class target, String name, Class returnType, Iterable> parameterTypes) + { + return new InvokeInstruction(invocationType, + type(target), + name, + type(returnType), + transform(parameterTypes, ParameterizedType::type)); + } + + // + // Invoke Dynamic + // + + public static InstructionNode invokeDynamic(String name, + ParameterizedType returnType, + Iterable parameterTypes, + Method bootstrapMethod, + Iterable bootstrapArguments) + { + return new InvokeDynamicInstruction(name, + returnType, + parameterTypes, + bootstrapMethod, + ImmutableList.copyOf(bootstrapArguments)); + } + + public static InstructionNode invokeDynamic(String name, + ParameterizedType returnType, + Iterable parameterTypes, + Method bootstrapMethod, + Object... bootstrapArguments) + { + return new InvokeDynamicInstruction(name, + returnType, + parameterTypes, + bootstrapMethod, + ImmutableList.copyOf(bootstrapArguments)); + } + + public static InstructionNode invokeDynamic(String name, + MethodType methodType, + Method bootstrapMethod, + Iterable bootstrapArguments) + { + return new InvokeDynamicInstruction(name, + type(methodType.returnType()), + transform(methodType.parameterList(), ParameterizedType::type), + bootstrapMethod, + ImmutableList.copyOf(bootstrapArguments)); + } + + public static InstructionNode invokeDynamic(String name, + MethodType methodType, + Method bootstrapMethod, + Object... bootstrapArguments) + { + return new InvokeDynamicInstruction(name, + type(methodType.returnType()), + transform(methodType.parameterList(), ParameterizedType::type), + bootstrapMethod, + ImmutableList.copyOf(bootstrapArguments)); + } + + private final OpCode opCode; + private final ParameterizedType target; + private final String name; + private final ParameterizedType returnType; + private final List parameterTypes; + + public InvokeInstruction(OpCode opCode, + ParameterizedType target, + String name, + ParameterizedType returnType, + Iterable parameterTypes) + { + checkUnqualifiedName(name); + this.opCode = opCode; + this.target = target; + this.name = name; + this.returnType = returnType; + this.parameterTypes = ImmutableList.copyOf(parameterTypes); + } + + public OpCode getOpCode() + { + return opCode; + } + + public ParameterizedType getTarget() + { + return target; + } + + public String getName() + { + return name; + } + + public ParameterizedType getReturnType() + { + return returnType; + } + + public List getParameterTypes() + { + return parameterTypes; + } + + public String getMethodDescription() + { + return methodDescription(returnType, parameterTypes); + } + + @Override + public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) + { + visitor.visitMethodInsn(opCode.getOpCode(), target.getClassName(), name, getMethodDescription()); + } + + @Override + public List getChildNodes() + { + return ImmutableList.of(); + } + + @Override + public T accept(ByteCodeNode parent, ByteCodeVisitor visitor) + { + return visitor.visitInvoke(parent, this); + } + + public static class InvokeDynamicInstruction + extends InvokeInstruction + { + private final Method bootstrapMethod; + private final List bootstrapArguments; + + public InvokeDynamicInstruction(String name, + ParameterizedType returnType, + Iterable parameterTypes, + Method bootstrapMethod, + List bootstrapArguments) + { + super(INVOKEDYNAMIC, null, name, returnType, parameterTypes); + this.bootstrapMethod = bootstrapMethod; + this.bootstrapArguments = ImmutableList.copyOf(bootstrapArguments); + } + + @Override + public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) + { + Handle bootstrapMethodHandle = new Handle(Opcodes.H_INVOKESTATIC, + type(bootstrapMethod.getDeclaringClass()).getClassName(), + bootstrapMethod.getName(), + methodDescription( + bootstrapMethod.getReturnType(), + bootstrapMethod.getParameterTypes())); + + visitor.visitInvokeDynamicInsn(getName(), + getMethodDescription(), + bootstrapMethodHandle, + bootstrapArguments.toArray(new Object[bootstrapArguments.size()])); + } + + public Method getBootstrapMethod() + { + return bootstrapMethod; + } + + public List getBootstrapArguments() + { + return bootstrapArguments; + } + + @Override + public List getChildNodes() + { + return ImmutableList.of(); + } + + @Override + public T accept(ByteCodeNode parent, ByteCodeVisitor visitor) + { + return visitor.visitInvokeDynamic(parent, this); + } + } + + private static void checkUnqualifiedName(String name) + { + // JVM Specification 4.2.2 Unqualified Names + checkNotNull(name, "name is null"); + checkArgument(!name.isEmpty(), "name is empty"); + if (name.equals("") || name.equals("")) { + return; + } + CharMatcher invalid = CharMatcher.anyOf(".;[/<>"); + checkArgument(invalid.matchesNoneOf(name), "invalid name: %s", name); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/instruction/JumpInstruction.java b/presto-main/src/main/java/com/facebook/presto/byteCode/instruction/JumpInstruction.java new file mode 100644 index 00000000..8638d43c --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/instruction/JumpInstruction.java @@ -0,0 +1,162 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode.instruction; + +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.ByteCodeVisitor; +import com.facebook.presto.byteCode.MethodGenerationContext; +import com.facebook.presto.byteCode.OpCode; +import com.google.common.collect.ImmutableList; +import org.objectweb.asm.MethodVisitor; + +import java.util.List; + +import static com.facebook.presto.byteCode.OpCode.GOTO; +import static com.facebook.presto.byteCode.OpCode.IFEQ; +import static com.facebook.presto.byteCode.OpCode.IFGE; +import static com.facebook.presto.byteCode.OpCode.IFGT; +import static com.facebook.presto.byteCode.OpCode.IFLE; +import static com.facebook.presto.byteCode.OpCode.IFLT; +import static com.facebook.presto.byteCode.OpCode.IFNE; +import static com.facebook.presto.byteCode.OpCode.IFNONNULL; +import static com.facebook.presto.byteCode.OpCode.IFNULL; +import static com.facebook.presto.byteCode.OpCode.IF_ACMPEQ; +import static com.facebook.presto.byteCode.OpCode.IF_ACMPNE; +import static com.facebook.presto.byteCode.OpCode.IF_ICMPEQ; +import static com.facebook.presto.byteCode.OpCode.IF_ICMPGT; +import static com.facebook.presto.byteCode.OpCode.IF_ICMPLE; +import static com.facebook.presto.byteCode.OpCode.IF_ICMPLT; +import static com.facebook.presto.byteCode.OpCode.IF_ICMPNE; + +@SuppressWarnings("UnusedDeclaration") +public class JumpInstruction + implements InstructionNode +{ + public static InstructionNode jump(LabelNode label) + { + return new JumpInstruction(GOTO, label); + } + + public static InstructionNode jumpIfEqualZero(LabelNode label) + { + return new JumpInstruction(IFEQ, label); + } + + public static InstructionNode jumpIfNotEqualZero(LabelNode label) + { + return new JumpInstruction(IFNE, label); + } + + public static InstructionNode jumpIfLessThanZero(LabelNode label) + { + return new JumpInstruction(IFLT, label); + } + + public static InstructionNode jumpIfGreaterThanZero(LabelNode label) + { + return new JumpInstruction(IFGT, label); + } + + public static InstructionNode jumpIfLessThanOrEqualZero(LabelNode label) + { + return new JumpInstruction(IFLE, label); + } + + public static InstructionNode jumpIfIntGreaterThanOrEqualZero(LabelNode label) + { + return new JumpInstruction(IFGE, label); + } + + public static InstructionNode jumpIfIntEqual(LabelNode label) + { + return new JumpInstruction(IF_ICMPEQ, label); + } + + public static InstructionNode jumpIfIntNotEqual(LabelNode label) + { + return new JumpInstruction(IF_ICMPNE, label); + } + + public static InstructionNode jumpIfIntLessThan(LabelNode label) + { + return new JumpInstruction(IF_ICMPLT, label); + } + + public static InstructionNode jumpIfIntGreaterThan(LabelNode label) + { + return new JumpInstruction(IF_ICMPGT, label); + } + + public static InstructionNode jumpIfIntLessThanOrEqual(LabelNode label) + { + return new JumpInstruction(IF_ICMPLE, label); + } + + public static InstructionNode jumpIfNull(LabelNode label) + { + return new JumpInstruction(IFNULL, label); + } + + public static InstructionNode jumpIfNotNull(LabelNode label) + { + return new JumpInstruction(IFNONNULL, label); + } + + public static InstructionNode jumpIfObjectSame(LabelNode label) + { + return new JumpInstruction(IF_ACMPEQ, label); + } + + public static InstructionNode jumpIfObjectNotSame(LabelNode label) + { + return new JumpInstruction(IF_ACMPNE, label); + } + + private final OpCode opCode; + private final LabelNode label; + + public JumpInstruction(OpCode opCode, LabelNode label) + { + this.opCode = opCode; + this.label = label; + } + + public OpCode getOpCode() + { + return opCode; + } + + public LabelNode getLabel() + { + return label; + } + + @Override + public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) + { + visitor.visitJumpInsn(opCode.getOpCode(), label.getLabel()); + } + + @Override + public List getChildNodes() + { + return ImmutableList.of(); + } + + @Override + public T accept(ByteCodeNode parent, ByteCodeVisitor visitor) + { + return visitor.visitJumpInstruction(parent, this); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/instruction/LabelNode.java b/presto-main/src/main/java/com/facebook/presto/byteCode/instruction/LabelNode.java new file mode 100644 index 00000000..9f499ef1 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/instruction/LabelNode.java @@ -0,0 +1,86 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode.instruction; + +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.ByteCodeVisitor; +import com.facebook.presto.byteCode.MethodGenerationContext; +import com.google.common.collect.ImmutableList; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; + +import java.util.List; + +import static com.google.common.base.MoreObjects.toStringHelper; + +public class LabelNode + implements InstructionNode +{ + private final String name; + private final Label label; + + public LabelNode() + { + this.label = new Label(); + this.name = "label@" + label.hashCode(); + } + + public LabelNode(LabelNode labelNode) + { + this.label = labelNode.getLabel(); + this.name = "label@" + label.hashCode(); + } + + public LabelNode(String name) + { + this.label = new Label(); + this.name = name + "@" + label.hashCode(); + } + + public String getName() + { + return name; + } + + public Label getLabel() + { + return label; + } + + @Override + public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) + { + visitor.visitLabel(label); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("name", name) + .toString(); + } + + @Override + public List getChildNodes() + { + return ImmutableList.of(); + } + + @Override + public T accept(ByteCodeNode parent, ByteCodeVisitor visitor) + { + return visitor.visitLabel(parent, this); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/instruction/TypeInstruction.java b/presto-main/src/main/java/com/facebook/presto/byteCode/instruction/TypeInstruction.java new file mode 100644 index 00000000..a83bbb2d --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/instruction/TypeInstruction.java @@ -0,0 +1,102 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode.instruction; + +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.ByteCodeVisitor; +import com.facebook.presto.byteCode.MethodGenerationContext; +import com.facebook.presto.byteCode.OpCode; +import com.facebook.presto.byteCode.ParameterizedType; +import com.google.common.collect.ImmutableList; +import org.objectweb.asm.MethodVisitor; + +import java.util.List; + +import static com.facebook.presto.byteCode.OpCode.ANEWARRAY; +import static com.facebook.presto.byteCode.OpCode.CHECKCAST; +import static com.facebook.presto.byteCode.OpCode.INSTANCEOF; +import static com.facebook.presto.byteCode.OpCode.NEW; +import static com.facebook.presto.byteCode.ParameterizedType.type; + +@SuppressWarnings("UnusedDeclaration") +public class TypeInstruction + implements InstructionNode +{ + public static InstructionNode newObject(Class type) + { + return new TypeInstruction(NEW, type(type)); + } + + public static InstructionNode newObject(ParameterizedType type) + { + return new TypeInstruction(NEW, type); + } + + public static InstructionNode newObjectArray(Class type) + { + return new TypeInstruction(ANEWARRAY, type(type)); + } + + public static InstructionNode newObjectArray(ParameterizedType type) + { + return new TypeInstruction(ANEWARRAY, type); + } + + public static InstructionNode instanceOf(Class type) + { + return new TypeInstruction(INSTANCEOF, type(type)); + } + + public static InstructionNode instanceOf(ParameterizedType type) + { + return new TypeInstruction(INSTANCEOF, type); + } + + public static InstructionNode cast(Class type) + { + return new TypeInstruction(CHECKCAST, type(type)); + } + + public static InstructionNode cast(ParameterizedType type) + { + return new TypeInstruction(CHECKCAST, type); + } + + private final OpCode opCode; + private final ParameterizedType type; + + public TypeInstruction(OpCode opCode, ParameterizedType type) + { + this.opCode = opCode; + this.type = type; + } + + @Override + public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) + { + visitor.visitTypeInsn(opCode.getOpCode(), type.getClassName()); + } + + @Override + public List getChildNodes() + { + return ImmutableList.of(); + } + + @Override + public T accept(ByteCodeNode parent, ByteCodeVisitor visitor) + { + return visitor.visitInstruction(parent, this); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/byteCode/instruction/VariableInstruction.java b/presto-main/src/main/java/com/facebook/presto/byteCode/instruction/VariableInstruction.java new file mode 100644 index 00000000..5004f354 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/byteCode/instruction/VariableInstruction.java @@ -0,0 +1,153 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.byteCode.instruction; + +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.ByteCodeVisitor; +import com.facebook.presto.byteCode.MethodGenerationContext; +import com.facebook.presto.byteCode.Variable; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Type; + +import java.util.List; + +import static com.facebook.presto.byteCode.OpCode.ILOAD; +import static com.facebook.presto.byteCode.OpCode.ISTORE; +import static com.google.common.base.MoreObjects.toStringHelper; + +public abstract class VariableInstruction + implements InstructionNode +{ + public static InstructionNode loadVariable(Variable variable) + { + return new LoadVariableInstruction(variable); + } + + public static InstructionNode storeVariable(Variable variable) + { + return new StoreVariableInstruction(variable); + } + + public static InstructionNode incrementVariable(Variable variable, byte increment) + { + return new IncrementVariableInstruction(variable, increment); + } + + private final Variable variable; + + private VariableInstruction(Variable variable) + { + this.variable = variable; + } + + public Variable getVariable() + { + return variable; + } + + @Override + public List getChildNodes() + { + return ImmutableList.of(); + } + + @Override + public T accept(ByteCodeNode parent, ByteCodeVisitor visitor) + { + return visitor.visitVariableInstruction(parent, this); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("variable", variable) + .toString(); + } + + public static class LoadVariableInstruction + extends VariableInstruction + { + public LoadVariableInstruction(Variable variable) + { + super(variable); + } + + @Override + public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) + { + visitor.visitVarInsn(Type.getType(getVariable().getType().getType()).getOpcode(ILOAD.getOpCode()), generationContext.getVariableSlot(getVariable())); + } + + @Override + public T accept(ByteCodeNode parent, ByteCodeVisitor visitor) + { + return visitor.visitLoadVariable(parent, this); + } + } + + public static class StoreVariableInstruction + extends VariableInstruction + { + public StoreVariableInstruction(Variable variable) + { + super(variable); + } + + @Override + public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) + { + visitor.visitVarInsn(Type.getType(getVariable().getType().getType()).getOpcode(ISTORE.getOpCode()), generationContext.getVariableSlot(getVariable())); + } + + @Override + public T accept(ByteCodeNode parent, ByteCodeVisitor visitor) + { + return visitor.visitStoreVariable(parent, this); + } + } + + public static class IncrementVariableInstruction + extends VariableInstruction + { + private final byte increment; + + public IncrementVariableInstruction(Variable variable, byte increment) + { + super(variable); + String type = variable.getType().getClassName(); + Preconditions.checkArgument(ImmutableList.of("byte", "short", "int").contains(type), "variable must be an byte, short or int, but is %s", type); + this.increment = increment; + } + + public byte getIncrement() + { + return increment; + } + + @Override + public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) + { + visitor.visitIincInsn(generationContext.getVariableSlot(getVariable()), increment); + } + + @Override + public T accept(ByteCodeNode parent, ByteCodeVisitor visitor) + { + return visitor.visitIncrementVariable(parent, this); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/concurrent/FairBatchExecutor.java b/presto-main/src/main/java/com/facebook/presto/concurrent/FairBatchExecutor.java new file mode 100644 index 00000000..a35b837b --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/concurrent/FairBatchExecutor.java @@ -0,0 +1,198 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.concurrent; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.primitives.Longs; +import io.airlift.log.Logger; + +import javax.annotation.concurrent.GuardedBy; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.FutureTask; +import java.util.concurrent.PriorityBlockingQueue; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + *

Executes batches of tasks such that individual tasks within each batch are interleaved with tasks from other batches.

+ *

+ *

E.g, if two batches containing elements ["a1, "a2", "a3", "a4"] and ["b1", "b2", "b3", "b4", "b5"] are submitted + * in that order, a possible execution might be:

+ *

+ *

"a1", "b1", "a2", "b2", "a3", "b3", "a4", "b4", "b5"

+ *

+ *

If a third batch ["c1", "c2", "c3"] were submitted when the execution in the example above is at "a3", a possible + * execution might be:

+ *

+ *

"a1", "b1", "a2", "b2", "a3", "b3", "c1", "a4", "b4", "c2", "b5", "c3"

+ *

+ *

The first task of a batch will not execute before the first task of a previously submitted task, therefore + * guaranteeing that no batch will get starved.

+ */ +public class FairBatchExecutor +{ + private static final Logger log = Logger.get(FairBatchExecutor.class); + + private final AtomicBoolean shutdown = new AtomicBoolean(); + private final int threads; + private final ExecutorService executor; + private final PriorityBlockingQueue queue = new PriorityBlockingQueue<>(); + + @GuardedBy("this") + private long basePriority; + + public FairBatchExecutor(int threads, ThreadFactory threadFactory) + { + this.threads = threads; + this.executor = new ThreadPoolExecutor(threads, threads, + 1, TimeUnit.MINUTES, + new SynchronousQueue(), + threadFactory, + new ThreadPoolExecutor.DiscardPolicy()); + } + + public void shutdown() + { + shutdown.set(true); + executor.shutdown(); + + // poison pills + for (int i = 0; i < threads; i++) { + queue.add(new PrioritizedFutureTask<>(-1, new Callable() + { + @Override + public Void call() + throws Exception + { + return null; + } + })); + } + } + + // TODO: add shutdownNow + + public List> processBatch(Collection> tasks) + { + Preconditions.checkState(!shutdown.get(), "Executor is already shut down"); + + long priority = computeStartingPriority(); + + ImmutableList.Builder> result = ImmutableList.builder(); + for (Callable task : tasks) { + PrioritizedFutureTask future = new PrioritizedFutureTask<>(priority++, task); + + queue.add(future); + result.add(future); + } + + // Make sure we have enough processors to achieve the desired concurrency level + for (int i = 0; i < Math.min(threads, tasks.size()); ++i) { + executor.execute(new Runnable() + { + @Override + public void run() + { + trigger(); + } + }); + } + + return result.build(); + } + + private long computeStartingPriority() + { + synchronized (this) { + // increment the base priority so that the first pending task + // of previously submitted batches takes precedence + basePriority++; + return basePriority; + } + } + + private void updateStartingPriority(long newBase) + { + synchronized (this) { + // update the base priority so that newly submitted batches are + // interleaved correctly with tasks at the front of the queue + if (basePriority < newBase) { + basePriority = newBase; + } + } + } + + private void trigger() + { + boolean interrupted = false; + try { + while (!Thread.currentThread().isInterrupted() && !shutdown.get()) { + PrioritizedFutureTask task = queue.take(); + try { + task.run(); + } + finally { + updateStartingPriority(task.priority); + } + } + } + catch (InterruptedException e) { + interrupted = true; + } + finally { + if (!shutdown.get()) { + // attempt to submit a new task in case we died due to unexpected reasons + executor.execute(new Runnable() + { + @Override + public void run() + { + trigger(); + } + }); + } + } + + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + + private static class PrioritizedFutureTask + extends FutureTask + implements Comparable + { + private final long priority; + + private PrioritizedFutureTask(long priority, Callable callable) + { + super(callable); + this.priority = priority; + } + + @Override + public int compareTo(PrioritizedFutureTask o) + { + return Longs.compare(priority, o.priority); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/connector/ConnectorManager.java b/presto-main/src/main/java/com/facebook/presto/connector/ConnectorManager.java new file mode 100644 index 00000000..9ddc1906 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/connector/ConnectorManager.java @@ -0,0 +1,292 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.connector; + +import com.facebook.presto.connector.informationSchema.InformationSchemaMetadata; +import com.facebook.presto.connector.informationSchema.InformationSchemaPageSourceProvider; +import com.facebook.presto.connector.informationSchema.InformationSchemaSplitManager; +import com.facebook.presto.connector.system.SystemConnector; +import com.facebook.presto.index.IndexManager; +import com.facebook.presto.metadata.HandleResolver; +import com.facebook.presto.metadata.MetadataManager; +import com.facebook.presto.spi.Connector; +import com.facebook.presto.spi.ConnectorFactory; +import com.facebook.presto.spi.ConnectorHandleResolver; +import com.facebook.presto.spi.ConnectorIndexResolver; +import com.facebook.presto.spi.ConnectorMetadata; +import com.facebook.presto.spi.ConnectorPageSinkProvider; +import com.facebook.presto.spi.ConnectorPageSourceProvider; +import com.facebook.presto.spi.ConnectorRecordSetProvider; +import com.facebook.presto.spi.ConnectorRecordSinkProvider; +import com.facebook.presto.spi.ConnectorSplitManager; +import com.facebook.presto.spi.NodeManager; +import com.facebook.presto.spi.SystemTable; +import com.facebook.presto.spi.classloader.ThreadContextClassLoader; +import com.facebook.presto.split.PageSinkManager; +import com.facebook.presto.split.PageSourceManager; +import com.facebook.presto.split.RecordPageSinkProvider; +import com.facebook.presto.split.RecordPageSourceProvider; +import com.facebook.presto.split.SplitManager; +import io.airlift.log.Logger; + +import javax.annotation.PreDestroy; +import javax.inject.Inject; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicBoolean; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +public class ConnectorManager +{ + public static final String INFORMATION_SCHEMA_CONNECTOR_PREFIX = "$info_schema@"; + public static final String SYSTEM_TABLES_CONNECTOR_PREFIX = "$system@"; + + private static final Logger log = Logger.get(ConnectorManager.class); + + private final MetadataManager metadataManager; + private final SplitManager splitManager; + private final PageSourceManager pageSourceManager; + private final IndexManager indexManager; + + private final PageSinkManager pageSinkManager; + private final HandleResolver handleResolver; + private final NodeManager nodeManager; + + private final ConcurrentMap connectorFactories = new ConcurrentHashMap<>(); + + private final ConcurrentMap connectors = new ConcurrentHashMap<>(); + + private final AtomicBoolean stopped = new AtomicBoolean(); + + @Inject + public ConnectorManager(MetadataManager metadataManager, + SplitManager splitManager, + PageSourceManager pageSourceManager, + IndexManager indexManager, + PageSinkManager pageSinkManager, + HandleResolver handleResolver, + Map connectorFactories, + NodeManager nodeManager) + { + this.metadataManager = metadataManager; + this.splitManager = splitManager; + this.pageSourceManager = pageSourceManager; + this.indexManager = indexManager; + this.pageSinkManager = pageSinkManager; + this.handleResolver = handleResolver; + this.nodeManager = nodeManager; + this.connectorFactories.putAll(connectorFactories); + } + + @PreDestroy + public void stop() + { + if (stopped.getAndSet(true)) { + return; + } + + for (Map.Entry entry : connectors.entrySet()) { + Connector connector = entry.getValue(); + try (ThreadContextClassLoader ignored = new ThreadContextClassLoader(connector.getClass().getClassLoader())) { + connector.shutdown(); + } + catch (Throwable t) { + log.error(t, "Error shutting down connector: %s", entry.getKey()); + } + } + } + + public void addConnectorFactory(ConnectorFactory connectorFactory) + { + checkState(!stopped.get(), "ConnectorManager is stopped"); + ConnectorFactory existingConnectorFactory = connectorFactories.putIfAbsent(connectorFactory.getName(), connectorFactory); + checkArgument(existingConnectorFactory == null, "Connector %s is already registered", connectorFactory.getName()); + } + + public synchronized void createConnection(String catalogName, String connectorName, Map properties) + { + checkState(!stopped.get(), "ConnectorManager is stopped"); + checkNotNull(catalogName, "catalogName is null"); + checkNotNull(connectorName, "connectorName is null"); + checkNotNull(properties, "properties is null"); + + ConnectorFactory connectorFactory = connectorFactories.get(connectorName); + checkArgument(connectorFactory != null, "No factory for connector %s", connectorName); + try (ThreadContextClassLoader ignored = new ThreadContextClassLoader(connectorFactory.getClass().getClassLoader())) { + createConnection(catalogName, connectorFactory, properties); + } + } + + public synchronized void createConnection(String catalogName, ConnectorFactory connectorFactory, Map properties) + { + checkState(!stopped.get(), "ConnectorManager is stopped"); + checkNotNull(catalogName, "catalogName is null"); + checkNotNull(properties, "properties is null"); + checkNotNull(connectorFactory, "connectorFactory is null"); + + String connectorId = getConnectorId(catalogName); + checkState(!connectors.containsKey(connectorId), "A connector %s already exists", connectorId); + + Connector connector = connectorFactory.create(connectorId, properties); + + addConnector(catalogName, connectorId, connector); + } + + public synchronized void createConnection(String catalogName, Connector connector) + { + checkState(!stopped.get(), "ConnectorManager is stopped"); + checkNotNull(catalogName, "catalogName is null"); + checkNotNull(connector, "connector is null"); + + addConnector(catalogName, getConnectorId(catalogName), connector); + } + + private synchronized void addConnector(String catalogName, String connectorId, Connector connector) + { + checkState(!stopped.get(), "ConnectorManager is stopped"); + checkState(!connectors.containsKey(connectorId), "A connector %s already exists", connectorId); + connectors.put(connectorId, connector); + + ConnectorMetadata connectorMetadata = connector.getMetadata(); + checkState(connectorMetadata != null, "Connector %s can not provide metadata", connectorId); + + ConnectorSplitManager connectorSplitManager = connector.getSplitManager(); + checkState(connectorSplitManager != null, "Connector %s does not have a split manager", connectorId); + + Set systemTables = connector.getSystemTables(); + checkNotNull(systemTables, "Connector %s returned a null system tables set"); + + ConnectorPageSourceProvider connectorPageSourceProvider = null; + try { + connectorPageSourceProvider = connector.getPageSourceProvider(); + checkNotNull(connectorPageSourceProvider, "Connector %s returned a null page source provider", connectorId); + } + catch (UnsupportedOperationException ignored) { + } + + if (connectorPageSourceProvider == null) { + ConnectorRecordSetProvider connectorRecordSetProvider = null; + try { + connectorRecordSetProvider = connector.getRecordSetProvider(); + checkNotNull(connectorRecordSetProvider, "Connector %s returned a null record set provider", connectorId); + } + catch (UnsupportedOperationException ignored) { + } + checkState(connectorRecordSetProvider != null, "Connector %s has neither a PageSource or RecordSet provider", connectorId); + connectorPageSourceProvider = new RecordPageSourceProvider(connectorRecordSetProvider); + } + + ConnectorHandleResolver connectorHandleResolver = connector.getHandleResolver(); + checkNotNull(connectorHandleResolver, "Connector %s does not have a handle resolver", connectorId); + + ConnectorPageSinkProvider connectorPageSinkProvider = null; + try { + connectorPageSinkProvider = connector.getPageSinkProvider(); + checkNotNull(connectorPageSinkProvider, "Connector %s returned a null page sink provider", connectorId); + } + catch (UnsupportedOperationException ignored) { + } + + if (connectorPageSinkProvider == null) { + ConnectorRecordSinkProvider connectorRecordSinkProvider = null; + try { + connectorRecordSinkProvider = connector.getRecordSinkProvider(); + checkNotNull(connectorRecordSinkProvider, "Connector %s returned a null record sink provider", connectorId); + connectorPageSinkProvider = new RecordPageSinkProvider(connectorRecordSinkProvider); + } + catch (UnsupportedOperationException ignored) { + } + } + + ConnectorIndexResolver indexResolver = null; + try { + indexResolver = connector.getIndexResolver(); + checkNotNull(indexResolver, "Connector %s returned a null index resolver", connectorId); + } + catch (UnsupportedOperationException ignored) { + } + + // IMPORTANT: all the instances need to be fetched from the connector *before* we add them to the corresponding managers. + // Otherwise, a broken connector would leave the managers in an inconsistent state with respect to each other + + metadataManager.addConnectorMetadata(connectorId, catalogName, connectorMetadata); + + metadataManager.addInformationSchemaMetadata(makeInformationSchemaConnectorId(connectorId), catalogName, new InformationSchemaMetadata(catalogName)); + splitManager.addConnectorSplitManager(makeInformationSchemaConnectorId(connectorId), new InformationSchemaSplitManager(nodeManager)); + pageSourceManager.addConnectorPageSourceProvider(makeInformationSchemaConnectorId(connectorId), new InformationSchemaPageSourceProvider(metadataManager)); + + Connector systemConnector = new SystemConnector(nodeManager, systemTables); + metadataManager.addSystemTablesMetadata(makeSystemTablesConnectorId(connectorId), catalogName, systemConnector.getMetadata()); + splitManager.addConnectorSplitManager(makeSystemTablesConnectorId(connectorId), systemConnector.getSplitManager()); + pageSourceManager.addConnectorPageSourceProvider(makeSystemTablesConnectorId(connectorId), new RecordPageSourceProvider(systemConnector.getRecordSetProvider())); + + splitManager.addConnectorSplitManager(connectorId, connectorSplitManager); + handleResolver.addHandleResolver(connectorId, connectorHandleResolver); + pageSourceManager.addConnectorPageSourceProvider(connectorId, connectorPageSourceProvider); + + if (connectorPageSinkProvider != null) { + pageSinkManager.addConnectorPageSinkProvider(connectorId, connectorPageSinkProvider); + } + + if (indexResolver != null) { + indexManager.addIndexResolver(connectorId, indexResolver); + } + } + + public synchronized void removeConnector(String catalogName) + { + String connectorId = getConnectorId(catalogName); + metadataManager.removeConnectorMetadata(catalogName); + + splitManager.removeConnectorSplitManager(makeInformationSchemaConnectorId(connectorId)); + pageSourceManager.removeConnectorPageSourceProvider(makeInformationSchemaConnectorId(connectorId)); + metadataManager.removeInformationSchemaMetadata(makeInformationSchemaConnectorId(connectorId), catalogName); + + splitManager.removeConnectorSplitManager(makeSystemTablesConnectorId(connectorId)); + pageSourceManager.removeConnectorPageSourceProvider(makeSystemTablesConnectorId(connectorId)); + metadataManager.removeSystemTablesMetadata(makeSystemTablesConnectorId(connectorId), catalogName); + + splitManager.removeConnectorSplitManager(connectorId); + pageSourceManager.removeConnectorPageSourceProvider(connectorId); + handleResolver.removeHandleResolver(connectorId); + metadataManager.removeConnectorsById(connectorId); + + pageSinkManager.removeConnectorPageSinkProvider(connectorId); + indexManager.removeIndexResolver(connectorId); + + connectors.remove(connectorId); + } + + private static String makeInformationSchemaConnectorId(String connectorId) + { + return INFORMATION_SCHEMA_CONNECTOR_PREFIX + connectorId; + } + + private static String makeSystemTablesConnectorId(String connectorId) + { + return SYSTEM_TABLES_CONNECTOR_PREFIX + connectorId; + } + + private static String getConnectorId(String catalogName) + { + // for now connectorId == catalogName + return catalogName; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/connector/informationSchema/InformationSchemaColumnHandle.java b/presto-main/src/main/java/com/facebook/presto/connector/informationSchema/InformationSchemaColumnHandle.java new file mode 100644 index 00000000..7f23ef12 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/connector/informationSchema/InformationSchemaColumnHandle.java @@ -0,0 +1,77 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.connector.informationSchema; + +import com.facebook.presto.spi.ColumnMetadata; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorTableMetadata; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableMap; + +import java.util.Map; +import java.util.Objects; + +public class InformationSchemaColumnHandle + implements ColumnHandle +{ + private final String columnName; + + @JsonCreator + public InformationSchemaColumnHandle(@JsonProperty("columnName") String columnName) + { + this.columnName = columnName; + } + + @JsonProperty + public String getColumnName() + { + return columnName; + } + + @Override + public int hashCode() + { + return Objects.hash(columnName); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final InformationSchemaColumnHandle other = (InformationSchemaColumnHandle) obj; + return Objects.equals(this.columnName, other.columnName); + } + + @Override + public String toString() + { + return "information_schema:" + columnName; + } + + public static Map toInformationSchemaColumnHandles(ConnectorTableMetadata tableMetadata) + { + ImmutableMap.Builder columnHandles = ImmutableMap.builder(); + for (ColumnMetadata columnMetadata : tableMetadata.getColumns()) { + columnHandles.put(columnMetadata.getName(), new InformationSchemaColumnHandle(columnMetadata.getName())); + } + + return columnHandles.build(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/connector/informationSchema/InformationSchemaHandleResolver.java b/presto-main/src/main/java/com/facebook/presto/connector/informationSchema/InformationSchemaHandleResolver.java new file mode 100644 index 00000000..01d0e968 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/connector/informationSchema/InformationSchemaHandleResolver.java @@ -0,0 +1,59 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.connector.informationSchema; + +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorHandleResolver; +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.ConnectorTableHandle; + +public class InformationSchemaHandleResolver + implements ConnectorHandleResolver +{ + @Override + public boolean canHandle(ConnectorTableHandle tableHandle) + { + return tableHandle instanceof InformationSchemaTableHandle; + } + + @Override + public boolean canHandle(ColumnHandle columnHandle) + { + return columnHandle instanceof InformationSchemaColumnHandle; + } + + @Override + public boolean canHandle(ConnectorSplit split) + { + return split instanceof InformationSchemaSplit; + } + + @Override + public Class getTableHandleClass() + { + return InformationSchemaTableHandle.class; + } + + @Override + public Class getColumnHandleClass() + { + return InformationSchemaColumnHandle.class; + } + + @Override + public Class getSplitClass() + { + return InformationSchemaSplit.class; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/connector/informationSchema/InformationSchemaMetadata.java b/presto-main/src/main/java/com/facebook/presto/connector/informationSchema/InformationSchemaMetadata.java new file mode 100644 index 00000000..cd1b0dbe --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/connector/informationSchema/InformationSchemaMetadata.java @@ -0,0 +1,210 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.connector.informationSchema; + +import com.facebook.presto.Session; +import com.facebook.presto.spi.ColumnMetadata; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.ConnectorTableHandle; +import com.facebook.presto.spi.ConnectorTableMetadata; +import com.facebook.presto.spi.ReadOnlyConnectorMetadata; +import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.spi.SchemaTablePrefix; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import static com.facebook.presto.connector.informationSchema.InformationSchemaColumnHandle.toInformationSchemaColumnHandles; +import static com.facebook.presto.metadata.MetadataUtil.SchemaMetadataBuilder.schemaMetadataBuilder; +import static com.facebook.presto.metadata.MetadataUtil.TableMetadataBuilder.tableMetadataBuilder; +import static com.facebook.presto.metadata.MetadataUtil.findColumnMetadata; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.BooleanType.BOOLEAN; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static com.facebook.presto.util.Types.checkType; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Predicates.compose; +import static com.google.common.base.Predicates.equalTo; +import static com.google.common.collect.Iterables.filter; + +public class InformationSchemaMetadata + extends ReadOnlyConnectorMetadata +{ + public static final String INFORMATION_SCHEMA = "information_schema"; + + public static final SchemaTableName TABLE_COLUMNS = new SchemaTableName(INFORMATION_SCHEMA, "columns"); + public static final SchemaTableName TABLE_TABLES = new SchemaTableName(INFORMATION_SCHEMA, "tables"); + public static final SchemaTableName TABLE_VIEWS = new SchemaTableName(INFORMATION_SCHEMA, "views"); + public static final SchemaTableName TABLE_SCHEMATA = new SchemaTableName(INFORMATION_SCHEMA, "schemata"); + public static final SchemaTableName TABLE_INTERNAL_FUNCTIONS = new SchemaTableName(INFORMATION_SCHEMA, "__internal_functions__"); + public static final SchemaTableName TABLE_INTERNAL_PARTITIONS = new SchemaTableName(INFORMATION_SCHEMA, "__internal_partitions__"); + + public static final Map TABLES = schemaMetadataBuilder() + .table(tableMetadataBuilder(TABLE_COLUMNS) + .column("table_catalog", VARCHAR) + .column("table_schema", VARCHAR) + .column("table_name", VARCHAR) + .column("column_name", VARCHAR) + .column("ordinal_position", BIGINT) + .column("column_default", VARCHAR) + .column("is_nullable", VARCHAR) + .column("data_type", VARCHAR) + .column("is_partition_key", VARCHAR) + .column("comment", VARCHAR) + .build()) + .table(tableMetadataBuilder(TABLE_TABLES) + .column("table_catalog", VARCHAR) + .column("table_schema", VARCHAR) + .column("table_name", VARCHAR) + .column("table_type", VARCHAR) + .build()) + .table(tableMetadataBuilder(TABLE_VIEWS) + .column("table_catalog", VARCHAR) + .column("table_schema", VARCHAR) + .column("table_name", VARCHAR) + .column("view_definition", VARCHAR) + .build()) + .table(tableMetadataBuilder(TABLE_SCHEMATA) + .column("catalog_name", VARCHAR) + .column("schema_name", VARCHAR) + .build()) + .table(tableMetadataBuilder(TABLE_INTERNAL_FUNCTIONS) + .column("function_name", VARCHAR) + .column("argument_types", VARCHAR) + .column("return_type", VARCHAR) + .column("function_type", VARCHAR) + .column("deterministic", BOOLEAN) + .column("description", VARCHAR) + .build()) + .table(tableMetadataBuilder(TABLE_INTERNAL_PARTITIONS) + .column("table_catalog", VARCHAR) + .column("table_schema", VARCHAR) + .column("table_name", VARCHAR) + .column("partition_number", BIGINT) + .column("partition_key", VARCHAR) + .column("partition_value", VARCHAR) + .build()) + .build(); + + private final String catalogName; + + public InformationSchemaMetadata(String catalogName) + { + this.catalogName = catalogName; + } + + private InformationSchemaTableHandle checkTableHandle(ConnectorTableHandle tableHandle) + { + InformationSchemaTableHandle handle = checkType(tableHandle, InformationSchemaTableHandle.class, "tableHandle"); + checkArgument(handle.getCatalogName().equals(catalogName), "invalid table handle: expected catalog %s but got %s", catalogName, handle.getCatalogName()); + checkArgument(TABLES.containsKey(handle.getSchemaTableName()), "table %s does not exist", handle.getSchemaTableName()); + return handle; + } + + @Override + public List listSchemaNames(ConnectorSession session) + { + return ImmutableList.of(INFORMATION_SCHEMA); + } + + @Override + public ConnectorTableHandle getTableHandle(ConnectorSession connectorSession, SchemaTableName tableName) + { + if (!TABLES.containsKey(tableName)) { + return null; + } + + Session session = Session.builder() + .setUser(connectorSession.getUser()) + .setSource("information_schema") + .setCatalog("") // default catalog is not be used + .setSchema("") // default schema is not be used + .setTimeZoneKey(connectorSession.getTimeZoneKey()) + .setLocale(connectorSession.getLocale()) + .setStartTime(connectorSession.getStartTime()) + .build(); + + return new InformationSchemaTableHandle(session, catalogName, tableName.getSchemaName(), tableName.getTableName()); + } + + @Override + public ConnectorTableMetadata getTableMetadata(ConnectorTableHandle tableHandle) + { + InformationSchemaTableHandle informationSchemaTableHandle = checkTableHandle(tableHandle); + return TABLES.get(informationSchemaTableHandle.getSchemaTableName()); + } + + @Override + public List listTables(ConnectorSession session, String schemaNameOrNull) + { + if (schemaNameOrNull == null) { + return ImmutableList.copyOf(TABLES.keySet()); + } + + return ImmutableList.copyOf(filter(TABLES.keySet(), compose(equalTo(schemaNameOrNull), SchemaTableName::getSchemaName))); + } + + @Override + public ColumnHandle getSampleWeightColumnHandle(ConnectorTableHandle tableHandle) + { + return null; + } + + @Override + public ColumnMetadata getColumnMetadata(ConnectorTableHandle tableHandle, ColumnHandle columnHandle) + { + InformationSchemaTableHandle informationSchemaTableHandle = checkTableHandle(tableHandle); + ConnectorTableMetadata tableMetadata = TABLES.get(informationSchemaTableHandle.getSchemaTableName()); + + String columnName = checkType(columnHandle, InformationSchemaColumnHandle.class, "columnHandle").getColumnName(); + + ColumnMetadata columnMetadata = findColumnMetadata(tableMetadata, columnName); + checkArgument(columnMetadata != null, "Column %s on table %s does not exist", columnName, tableMetadata.getTable()); + return columnMetadata; + } + + @Override + public Map getColumnHandles(ConnectorTableHandle tableHandle) + { + InformationSchemaTableHandle informationSchemaTableHandle = checkTableHandle(tableHandle); + + ConnectorTableMetadata tableMetadata = TABLES.get(informationSchemaTableHandle.getSchemaTableName()); + + return toInformationSchemaColumnHandles(tableMetadata); + } + + @Override + public Map> listTableColumns(ConnectorSession session, SchemaTablePrefix prefix) + { + checkNotNull(prefix, "prefix is null"); + ImmutableMap.Builder> builder = ImmutableMap.builder(); + for (Entry entry : TABLES.entrySet()) { + if (prefix.matches(entry.getKey())) { + builder.put(entry.getKey(), entry.getValue().getColumns()); + } + } + return builder.build(); + } + + static List informationSchemaTableColumns(SchemaTableName tableName) + { + checkArgument(TABLES.containsKey(tableName), "table does not exist: %s", tableName); + return TABLES.get(tableName).getColumns(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/connector/informationSchema/InformationSchemaModule.java b/presto-main/src/main/java/com/facebook/presto/connector/informationSchema/InformationSchemaModule.java new file mode 100644 index 00000000..7d597c81 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/connector/informationSchema/InformationSchemaModule.java @@ -0,0 +1,36 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.connector.informationSchema; + +import com.facebook.presto.spi.ConnectorHandleResolver; +import com.facebook.presto.spi.ConnectorPageSourceProvider; +import com.facebook.presto.spi.ConnectorSplitManager; +import com.google.inject.Binder; +import com.google.inject.Module; +import com.google.inject.Scopes; + +import static com.google.inject.multibindings.MapBinder.newMapBinder; +import static com.google.inject.multibindings.Multibinder.newSetBinder; + +public class InformationSchemaModule + implements Module +{ + @Override + public void configure(Binder binder) + { + newSetBinder(binder, ConnectorSplitManager.class).addBinding().to(InformationSchemaSplitManager.class).in(Scopes.SINGLETON); + newSetBinder(binder, ConnectorPageSourceProvider.class).addBinding().to(InformationSchemaPageSourceProvider.class).in(Scopes.SINGLETON); + newMapBinder(binder, String.class, ConnectorHandleResolver.class).addBinding("information_schema").to(InformationSchemaHandleResolver.class).in(Scopes.SINGLETON); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/connector/informationSchema/InformationSchemaPageSourceProvider.java b/presto-main/src/main/java/com/facebook/presto/connector/informationSchema/InformationSchemaPageSourceProvider.java new file mode 100644 index 00000000..7a476d3e --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/connector/informationSchema/InformationSchemaPageSourceProvider.java @@ -0,0 +1,341 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.connector.informationSchema; + +import com.facebook.presto.Session; +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.InternalTable; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.metadata.OperatorNotFoundException; +import com.facebook.presto.metadata.ParametricFunction; +import com.facebook.presto.metadata.QualifiedTableName; +import com.facebook.presto.metadata.QualifiedTablePrefix; +import com.facebook.presto.metadata.TableHandle; +import com.facebook.presto.metadata.TableLayout; +import com.facebook.presto.metadata.TableLayoutResult; +import com.facebook.presto.metadata.ViewDefinition; +import com.facebook.presto.spi.ColumnMetadata; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorPageSource; +import com.facebook.presto.spi.ConnectorPageSourceProvider; +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.Constraint; +import com.facebook.presto.spi.FixedPageSource; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.spi.SerializableNativeValue; +import com.facebook.presto.spi.TupleDomain; +import com.facebook.presto.spi.block.Block; +import com.google.common.base.Joiner; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableBiMap; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import io.airlift.slice.Slice; + +import javax.inject.Inject; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.Set; + +import static com.facebook.presto.connector.informationSchema.InformationSchemaMetadata.TABLE_COLUMNS; +import static com.facebook.presto.connector.informationSchema.InformationSchemaMetadata.TABLE_INTERNAL_FUNCTIONS; +import static com.facebook.presto.connector.informationSchema.InformationSchemaMetadata.TABLE_INTERNAL_PARTITIONS; +import static com.facebook.presto.connector.informationSchema.InformationSchemaMetadata.TABLE_SCHEMATA; +import static com.facebook.presto.connector.informationSchema.InformationSchemaMetadata.TABLE_TABLES; +import static com.facebook.presto.connector.informationSchema.InformationSchemaMetadata.TABLE_VIEWS; +import static com.facebook.presto.connector.informationSchema.InformationSchemaMetadata.informationSchemaTableColumns; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static com.facebook.presto.util.Types.checkType; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Strings.nullToEmpty; +import static com.google.common.collect.Sets.union; +import static java.lang.String.format; + +public class InformationSchemaPageSourceProvider + implements ConnectorPageSourceProvider +{ + private final Metadata metadata; + + @Inject + public InformationSchemaPageSourceProvider(Metadata metadata) + { + this.metadata = checkNotNull(metadata, "metadata is null"); + } + + @Override + public ConnectorPageSource createPageSource(ConnectorSplit split, List columns) + { + InternalTable table = getInternalTable(split, columns); + + List channels = new ArrayList<>(); + for (ColumnHandle column : columns) { + String columnName = checkType(column, InformationSchemaColumnHandle.class, "column").getColumnName(); + int columnIndex = table.getColumnIndex(columnName); + channels.add(columnIndex); + } + + ImmutableList.Builder pages = ImmutableList.builder(); + for (Page page : table.getPages()) { + Block[] blocks = new Block[channels.size()]; + for (int index = 0; index < blocks.length; index++) { + blocks[index] = page.getBlock(channels.get(index)); + } + pages.add(new Page(page.getPositionCount(), blocks)); + } + return new FixedPageSource(pages.build()); + } + + private InternalTable getInternalTable(ConnectorSplit connectorSplit, List columns) + { + InformationSchemaSplit split = checkType(connectorSplit, InformationSchemaSplit.class, "split"); + + checkNotNull(columns, "columns is null"); + + InformationSchemaTableHandle handle = split.getTableHandle(); + Map filters = split.getFilters(); + + return getInformationSchemaTable(handle.getSession(), handle.getCatalogName(), handle.getSchemaTableName(), filters); + } + + public InternalTable getInformationSchemaTable(Session session, String catalog, SchemaTableName table, Map filters) + { + if (table.equals(TABLE_COLUMNS)) { + return buildColumns(session, catalog, filters); + } + if (table.equals(TABLE_TABLES)) { + return buildTables(session, catalog, filters); + } + if (table.equals(TABLE_VIEWS)) { + return buildViews(session, catalog, filters); + } + if (table.equals(TABLE_SCHEMATA)) { + return buildSchemata(session, catalog); + } + if (table.equals(TABLE_INTERNAL_FUNCTIONS)) { + return buildFunctions(); + } + if (table.equals(TABLE_INTERNAL_PARTITIONS)) { + return buildPartitions(session, catalog, filters); + } + + throw new IllegalArgumentException(format("table does not exist: %s", table)); + } + + private InternalTable buildColumns(Session session, String catalogName, Map filters) + { + InternalTable.Builder table = InternalTable.builder(informationSchemaTableColumns(TABLE_COLUMNS)); + for (Entry> entry : getColumnsList(session, catalogName, filters).entrySet()) { + QualifiedTableName tableName = entry.getKey(); + int ordinalPosition = 1; + for (ColumnMetadata column : entry.getValue()) { + if (column.isHidden()) { + continue; + } + table.add( + tableName.getCatalogName(), + tableName.getSchemaName(), + tableName.getTableName(), + column.getName(), + ordinalPosition, + null, + "YES", + column.getType().getDisplayName(), + column.isPartitionKey() ? "YES" : "NO", + column.getComment()); + ordinalPosition++; + } + } + return table.build(); + } + + private Map> getColumnsList(Session session, String catalogName, Map filters) + { + return metadata.listTableColumns(session, extractQualifiedTablePrefix(catalogName, filters)); + } + + private InternalTable buildTables(Session session, String catalogName, Map filters) + { + Set tables = ImmutableSet.copyOf(getTablesList(session, catalogName, filters)); + Set views = ImmutableSet.copyOf(getViewsList(session, catalogName, filters)); + + InternalTable.Builder table = InternalTable.builder(informationSchemaTableColumns(TABLE_TABLES)); + for (QualifiedTableName name : union(tables, views)) { + // if table and view names overlap, the view wins + String type = views.contains(name) ? "VIEW" : "BASE TABLE"; + table.add( + name.getCatalogName(), + name.getSchemaName(), + name.getTableName(), + type); + } + return table.build(); + } + + private List getTablesList(Session session, String catalogName, Map filters) + { + return metadata.listTables(session, extractQualifiedTablePrefix(catalogName, filters)); + } + + private List getViewsList(Session session, String catalogName, Map filters) + { + return metadata.listViews(session, extractQualifiedTablePrefix(catalogName, filters)); + } + + private InternalTable buildViews(Session session, String catalogName, Map filters) + { + InternalTable.Builder table = InternalTable.builder(informationSchemaTableColumns(TABLE_VIEWS)); + for (Entry entry : getViews(session, catalogName, filters).entrySet()) { + table.add( + entry.getKey().getCatalogName(), + entry.getKey().getSchemaName(), + entry.getKey().getTableName(), + entry.getValue().getOriginalSql()); + } + return table.build(); + } + + private Map getViews(Session session, String catalogName, Map filters) + { + return metadata.getViews(session, extractQualifiedTablePrefix(catalogName, filters)); + } + + private InternalTable buildFunctions() + { + InternalTable.Builder table = InternalTable.builder(informationSchemaTableColumns(TABLE_INTERNAL_FUNCTIONS)); + for (ParametricFunction function : metadata.listFunctions()) { + if (function.isApproximate()) { + continue; + } + table.add( + function.getSignature().getName(), + Joiner.on(", ").join(function.getSignature().getArgumentTypes()), + function.getSignature().getReturnType().toString(), + getFunctionType(function), + function.isDeterministic(), + nullToEmpty(function.getDescription())); + } + return table.build(); + } + + private InternalTable buildSchemata(Session session, String catalogName) + { + InternalTable.Builder table = InternalTable.builder(informationSchemaTableColumns(TABLE_SCHEMATA)); + for (String schema : metadata.listSchemaNames(session, catalogName)) { + table.add(catalogName, schema); + } + return table.build(); + } + + private InternalTable buildPartitions(Session session, String catalogName, Map filters) + { + QualifiedTableName tableName = extractQualifiedTableName(catalogName, filters); + + InternalTable.Builder table = InternalTable.builder(informationSchemaTableColumns(TABLE_INTERNAL_PARTITIONS)); + + Optional tableHandle = metadata.getTableHandle(session, tableName); + checkArgument(tableHandle.isPresent(), "Table %s does not exist", tableName); + Map columnHandles = ImmutableBiMap.copyOf(metadata.getColumnHandles(tableHandle.get())).inverse(); + + List layouts = metadata.getLayouts(tableHandle.get(), Constraint.alwaysTrue(), Optional.empty()); + + if (layouts.size() == 1) { + TableLayout layout = Iterables.getOnlyElement(layouts).getLayout(); + + layout.getDiscretePredicates().ifPresent(domains -> { + int partitionNumber = 1; + for (TupleDomain domain : domains) { + for (Entry entry : domain.extractNullableFixedValues().entrySet()) { + ColumnHandle columnHandle = entry.getKey(); + String columnName = columnHandles.get(columnHandle); + String value = null; + if (entry.getValue().getValue() != null) { + ColumnMetadata columnMetadata = metadata.getColumnMetadata(tableHandle.get(), columnHandle); + try { + FunctionInfo operator = metadata.getFunctionRegistry().getCoercion(columnMetadata.getType(), VARCHAR); + value = ((Slice) operator.getMethodHandle().invokeWithArguments(entry.getValue().getValue())).toStringUtf8(); + } + catch (OperatorNotFoundException e) { + value = ""; + } + catch (Throwable throwable) { + throw Throwables.propagate(throwable); + } + } + table.add( + catalogName, + tableName.getSchemaName(), + tableName.getTableName(), + partitionNumber, + columnName, + value); + } + partitionNumber++; + } + }); + } + return table.build(); + } + + private static QualifiedTableName extractQualifiedTableName(String catalogName, Map filters) + { + Optional schemaName = getFilterColumn(filters, "table_schema"); + checkArgument(schemaName.isPresent(), "filter is required for column: %s.%s", TABLE_INTERNAL_PARTITIONS, "table_schema"); + Optional tableName = getFilterColumn(filters, "table_name"); + checkArgument(tableName.isPresent(), "filter is required for column: %s.%s", TABLE_INTERNAL_PARTITIONS, "table_name"); + return new QualifiedTableName(catalogName, schemaName.get(), tableName.get()); + } + + private static QualifiedTablePrefix extractQualifiedTablePrefix(String catalogName, Map filters) + { + Optional schemaName = getFilterColumn(filters, "table_schema"); + Optional tableName = getFilterColumn(filters, "table_name"); + if (!schemaName.isPresent()) { + return new QualifiedTablePrefix(catalogName, Optional.empty(), Optional.empty()); + } + return new QualifiedTablePrefix(catalogName, schemaName, tableName); + } + + private static Optional getFilterColumn(Map filters, String columnName) + { + SerializableNativeValue value = filters.get(columnName); + if (value == null || value.getValue() == null) { + return Optional.empty(); + } + if (Slice.class.isAssignableFrom(value.getType())) { + return Optional.ofNullable(((Slice) value.getValue()).toStringUtf8()); + } + if (String.class.isAssignableFrom(value.getType())) { + return Optional.ofNullable((String) value.getValue()); + } + return Optional.empty(); + } + + private static String getFunctionType(ParametricFunction function) + { + if (function.isAggregate()) { + return "aggregate"; + } + if (function.isWindow()) { + return "window"; + } + return "scalar"; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/connector/informationSchema/InformationSchemaSplit.java b/presto-main/src/main/java/com/facebook/presto/connector/informationSchema/InformationSchemaSplit.java new file mode 100644 index 00000000..86c43f38 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/connector/informationSchema/InformationSchemaSplit.java @@ -0,0 +1,91 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.connector.informationSchema; + +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.HostAddress; +import com.facebook.presto.spi.SerializableNativeValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; + +import java.util.List; +import java.util.Map; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class InformationSchemaSplit + implements ConnectorSplit +{ + private final InformationSchemaTableHandle tableHandle; + private final Map filters; + private final List addresses; + + @JsonCreator + public InformationSchemaSplit( + @JsonProperty("tableHandle") InformationSchemaTableHandle tableHandle, + @JsonProperty("filters") Map filters, + @JsonProperty("addresses") List addresses) + { + this.tableHandle = checkNotNull(tableHandle, "tableHandle is null"); + this.filters = checkNotNull(filters, "filters is null"); + + checkNotNull(addresses, "hosts is null"); + checkArgument(!addresses.isEmpty(), "hosts is empty"); + this.addresses = ImmutableList.copyOf(addresses); + } + + @Override + public boolean isRemotelyAccessible() + { + return false; + } + + @Override + @JsonProperty + public List getAddresses() + { + return addresses; + } + + @JsonProperty + public InformationSchemaTableHandle getTableHandle() + { + return tableHandle; + } + + @JsonProperty + public Map getFilters() + { + return filters; + } + + @Override + public Object getInfo() + { + return this; + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("tableHandle", tableHandle) + .add("filters", filters) + .add("addresses", addresses) + .toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/connector/informationSchema/InformationSchemaSplitManager.java b/presto-main/src/main/java/com/facebook/presto/connector/informationSchema/InformationSchemaSplitManager.java new file mode 100644 index 00000000..0143bca0 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/connector/informationSchema/InformationSchemaSplitManager.java @@ -0,0 +1,134 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.connector.informationSchema; + +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorPartition; +import com.facebook.presto.spi.ConnectorPartitionResult; +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.ConnectorSplitManager; +import com.facebook.presto.spi.ConnectorSplitSource; +import com.facebook.presto.spi.ConnectorTableHandle; +import com.facebook.presto.spi.FixedSplitSource; +import com.facebook.presto.spi.HostAddress; +import com.facebook.presto.spi.NodeManager; +import com.facebook.presto.spi.SerializableNativeValue; +import com.facebook.presto.spi.TupleDomain; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; + +import javax.inject.Inject; + +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import static com.facebook.presto.util.Types.checkType; +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkNotNull; + +public class InformationSchemaSplitManager + implements ConnectorSplitManager +{ + private final NodeManager nodeManager; + + @Inject + public InformationSchemaSplitManager(NodeManager nodeManager) + { + this.nodeManager = checkNotNull(nodeManager, "nodeManager is null"); + } + + @Override + public ConnectorPartitionResult getPartitions(ConnectorTableHandle table, TupleDomain tupleDomain) + { + checkNotNull(tupleDomain, "tupleDomain is null"); + InformationSchemaTableHandle informationSchemaTableHandle = checkType(table, InformationSchemaTableHandle.class, "table"); + + Map bindings = tupleDomain.extractNullableFixedValues(); + + List partitions = ImmutableList.of(new InformationSchemaPartition(informationSchemaTableHandle, bindings)); + // We don't strip out the bindings that we have created from the undeterminedTupleDomain b/c the current InformationSchema + // system requires that all filters be re-applied at execution time. + return new ConnectorPartitionResult(partitions, tupleDomain); + } + + @Override + public ConnectorSplitSource getPartitionSplits(ConnectorTableHandle table, List partitions) + { + checkNotNull(partitions, "partitions is null"); + if (partitions.isEmpty()) { + return new FixedSplitSource(null, ImmutableList.of()); + } + + ConnectorPartition partition = Iterables.getOnlyElement(partitions); + InformationSchemaPartition informationSchemaPartition = checkType(partition, InformationSchemaPartition.class, "partition"); + + List localAddress = ImmutableList.of(nodeManager.getCurrentNode().getHostAndPort()); + + ImmutableMap.Builder filters = ImmutableMap.builder(); + for (Entry entry : informationSchemaPartition.getFilters().entrySet()) { + InformationSchemaColumnHandle informationSchemaColumnHandle = (InformationSchemaColumnHandle) entry.getKey(); + filters.put(informationSchemaColumnHandle.getColumnName(), entry.getValue()); + } + + ConnectorSplit split = new InformationSchemaSplit(informationSchemaPartition.getTable(), filters.build(), localAddress); + + return new FixedSplitSource(null, ImmutableList.of(split)); + } + + public static class InformationSchemaPartition + implements ConnectorPartition + { + private final InformationSchemaTableHandle table; + private final Map filters; + + public InformationSchemaPartition(InformationSchemaTableHandle table, Map filters) + { + this.table = checkNotNull(table, "table is null"); + this.filters = ImmutableMap.copyOf(checkNotNull(filters, "filters is null")); + } + + public InformationSchemaTableHandle getTable() + { + return table; + } + + @Override + public String getPartitionId() + { + return table.getSchemaTableName().toString(); + } + + @Override + public TupleDomain getTupleDomain() + { + return TupleDomain.withNullableFixedValues(filters); + } + + public Map getFilters() + { + return filters; + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("table", table) + .add("filters", filters) + .toString(); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/connector/informationSchema/InformationSchemaTableHandle.java b/presto-main/src/main/java/com/facebook/presto/connector/informationSchema/InformationSchemaTableHandle.java new file mode 100644 index 00000000..a754cfea --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/connector/informationSchema/InformationSchemaTableHandle.java @@ -0,0 +1,103 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.connector.informationSchema; + +import com.facebook.presto.Session; +import com.facebook.presto.spi.ConnectorTableHandle; +import com.facebook.presto.spi.SchemaTableName; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class InformationSchemaTableHandle + implements ConnectorTableHandle +{ + private final Session session; + private final String catalogName; + private final String schemaName; + private final String tableName; + + @JsonCreator + public InformationSchemaTableHandle( + @JsonProperty("session") Session session, + @JsonProperty("catalogName") String catalogName, + @JsonProperty("schemaName") String schemaName, + @JsonProperty("tableName") String tableName) + { + this.session = session; + this.catalogName = checkNotNull(catalogName, "catalogName is null"); + this.schemaName = checkNotNull(schemaName, "schemaName is null"); + this.tableName = checkNotNull(tableName, "tableName is null"); + } + + @JsonProperty + public Session getSession() + { + return session; + } + + @JsonProperty + public String getCatalogName() + { + return catalogName; + } + + @JsonProperty + public String getSchemaName() + { + return schemaName; + } + + @JsonProperty + public String getTableName() + { + return tableName; + } + + public SchemaTableName getSchemaTableName() + { + return new SchemaTableName(schemaName, tableName); + } + + @Override + public String toString() + { + return "information_schema:" + catalogName + ":" + schemaName + ":" + tableName; + } + + @Override + public int hashCode() + { + return Objects.hash(session, catalogName, schemaName, tableName); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + InformationSchemaTableHandle other = (InformationSchemaTableHandle) obj; + return Objects.equals(this.session, other.session) && + Objects.equals(this.catalogName, other.catalogName) && + Objects.equals(this.schemaName, other.schemaName) && + Objects.equals(this.tableName, other.tableName); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/connector/jmx/JmxColumnHandle.java b/presto-main/src/main/java/com/facebook/presto/connector/jmx/JmxColumnHandle.java new file mode 100644 index 00000000..43cda2d4 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/connector/jmx/JmxColumnHandle.java @@ -0,0 +1,106 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.connector.jmx; + +import com.facebook.presto.spi.ColumnMetadata; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.type.Type; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +import static com.google.common.base.MoreObjects.toStringHelper; + +public class JmxColumnHandle + implements ColumnHandle +{ + private final String connectorId; + private final String columnName; + private final Type columnType; + private final int ordinalPosition; + + @JsonCreator + public JmxColumnHandle( + @JsonProperty("connectorId") String connectorId, + @JsonProperty("columnName") String columnName, + @JsonProperty("columnType") Type columnType, + @JsonProperty("ordinalPosition") int ordinalPosition) + { + this.connectorId = connectorId; + this.columnName = columnName; + this.columnType = columnType; + this.ordinalPosition = ordinalPosition; + } + + @JsonProperty + public String getConnectorId() + { + return connectorId; + } + + @JsonProperty + public String getColumnName() + { + return columnName; + } + + @JsonProperty + public Type getColumnType() + { + return columnType; + } + + @JsonProperty + public int getOrdinalPosition() + { + return ordinalPosition; + } + + @Override + public int hashCode() + { + return Objects.hash(connectorId, columnName, columnType); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + JmxColumnHandle other = (JmxColumnHandle) obj; + return Objects.equals(this.connectorId, other.connectorId) && + Objects.equals(this.columnName, other.columnName) && + Objects.equals(this.columnType, other.columnType); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("connectorId", connectorId) + .add("columnName", columnName) + .add("columnType", columnType) + .toString(); + } + + public ColumnMetadata getColumnMetadata() + { + return new ColumnMetadata(columnName, columnType, false); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/connector/jmx/JmxConnectorFactory.java b/presto-main/src/main/java/com/facebook/presto/connector/jmx/JmxConnectorFactory.java new file mode 100644 index 00000000..b87c6fe8 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/connector/jmx/JmxConnectorFactory.java @@ -0,0 +1,80 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.connector.jmx; + +import com.facebook.presto.spi.Connector; +import com.facebook.presto.spi.ConnectorFactory; +import com.facebook.presto.spi.ConnectorHandleResolver; +import com.facebook.presto.spi.ConnectorMetadata; +import com.facebook.presto.spi.ConnectorRecordSetProvider; +import com.facebook.presto.spi.ConnectorSplitManager; +import com.facebook.presto.spi.NodeManager; + +import javax.inject.Inject; +import javax.management.MBeanServer; + +import java.util.Map; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class JmxConnectorFactory + implements ConnectorFactory +{ + private final MBeanServer mbeanServer; + private final NodeManager nodeManager; + + @Inject + public JmxConnectorFactory(MBeanServer mbeanServer, NodeManager nodeManager) + { + this.mbeanServer = checkNotNull(mbeanServer, "mbeanServer is null"); + this.nodeManager = checkNotNull(nodeManager, "nodeManager is null"); + } + + @Override + public String getName() + { + return "jmx"; + } + + @Override + public Connector create(final String connectorId, Map properties) + { + return new Connector() + { + @Override + public ConnectorHandleResolver getHandleResolver() + { + return new JmxHandleResolver(); + } + + @Override + public ConnectorMetadata getMetadata() + { + return new JmxMetadata(new JmxConnectorId(connectorId), mbeanServer); + } + + @Override + public ConnectorSplitManager getSplitManager() + { + return new JmxSplitManager(new JmxConnectorId(connectorId), nodeManager); + } + + @Override + public ConnectorRecordSetProvider getRecordSetProvider() + { + return new JmxRecordSetProvider(mbeanServer, nodeManager); + } + }; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/connector/jmx/JmxConnectorId.java b/presto-main/src/main/java/com/facebook/presto/connector/jmx/JmxConnectorId.java new file mode 100644 index 00000000..31b9448a --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/connector/jmx/JmxConnectorId.java @@ -0,0 +1,51 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.connector.jmx; + +import java.util.Objects; + +public class JmxConnectorId +{ + private final String id; + + public JmxConnectorId(String id) + { + this.id = id; + } + + @Override + public String toString() + { + return id; + } + + @Override + public int hashCode() + { + return Objects.hash(id); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final JmxConnectorId other = (JmxConnectorId) obj; + return Objects.equals(this.id, other.id); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/connector/jmx/JmxHandleResolver.java b/presto-main/src/main/java/com/facebook/presto/connector/jmx/JmxHandleResolver.java new file mode 100644 index 00000000..88602f29 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/connector/jmx/JmxHandleResolver.java @@ -0,0 +1,59 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.connector.jmx; + +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorHandleResolver; +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.ConnectorTableHandle; + +public class JmxHandleResolver + implements ConnectorHandleResolver +{ + @Override + public boolean canHandle(ConnectorTableHandle tableHandle) + { + return tableHandle instanceof JmxTableHandle; + } + + @Override + public boolean canHandle(ColumnHandle columnHandle) + { + return columnHandle instanceof JmxColumnHandle; + } + + @Override + public boolean canHandle(ConnectorSplit split) + { + return split instanceof JmxSplit; + } + + @Override + public Class getTableHandleClass() + { + return JmxTableHandle.class; + } + + @Override + public Class getColumnHandleClass() + { + return JmxColumnHandle.class; + } + + @Override + public Class getSplitClass() + { + return JmxSplit.class; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/connector/jmx/JmxMetadata.java b/presto-main/src/main/java/com/facebook/presto/connector/jmx/JmxMetadata.java new file mode 100644 index 00000000..c30eb36e --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/connector/jmx/JmxMetadata.java @@ -0,0 +1,212 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.connector.jmx; + +import com.facebook.presto.spi.ColumnMetadata; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.ConnectorTableHandle; +import com.facebook.presto.spi.ConnectorTableMetadata; +import com.facebook.presto.spi.ReadOnlyConnectorMetadata; +import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.spi.SchemaTablePrefix; +import com.facebook.presto.spi.type.Type; +import com.google.common.base.Predicate; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableList.Builder; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; +import com.google.common.collect.Maps; + +import javax.inject.Inject; +import javax.management.JMException; +import javax.management.MBeanAttributeInfo; +import javax.management.MBeanInfo; +import javax.management.MBeanServer; +import javax.management.ObjectName; + +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; + +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.BooleanType.BOOLEAN; +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static com.facebook.presto.util.Types.checkType; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.Locale.ENGLISH; +import static javax.management.ObjectName.WILDCARD; + +public class JmxMetadata + extends ReadOnlyConnectorMetadata +{ + public static final String SCHEMA_NAME = "jmx"; + + private final String connectorId; + private final MBeanServer mbeanServer; + + @Inject + public JmxMetadata(JmxConnectorId jmxConnectorId, MBeanServer mbeanServer) + { + this.connectorId = checkNotNull(jmxConnectorId, "jmxConnectorId is null").toString(); + this.mbeanServer = checkNotNull(mbeanServer, "mbeanServer is null"); + } + + @Override + public List listSchemaNames(ConnectorSession session) + { + return ImmutableList.of(SCHEMA_NAME); + } + + @Override + public JmxTableHandle getTableHandle(ConnectorSession session, SchemaTableName tableName) + { + checkNotNull(tableName, "tableName is null"); + if (!tableName.getSchemaName().equals(SCHEMA_NAME)) { + return null; + } + + try { + ObjectName objectName = Iterables.find(mbeanServer.queryNames(WILDCARD, null), objectNameEqualsIgnoreCase(new ObjectName(tableName.getTableName()))); + MBeanInfo mbeanInfo = mbeanServer.getMBeanInfo(objectName); + + ImmutableList.Builder columns = ImmutableList.builder(); + int ordinalPosition = 0; + columns.add(new JmxColumnHandle(connectorId, "node", VARCHAR, ordinalPosition++)); + for (MBeanAttributeInfo attribute : mbeanInfo.getAttributes()) { + if (!attribute.isReadable()) { + continue; + } + columns.add(new JmxColumnHandle(connectorId, attribute.getName(), getColumnType(attribute), ordinalPosition++)); + } + return new JmxTableHandle(connectorId, objectName.toString(), columns.build()); + } + catch (NoSuchElementException | JMException e) { + return null; + } + } + + @Override + public ConnectorTableMetadata getTableMetadata(ConnectorTableHandle tableHandle) + { + return checkType(tableHandle, JmxTableHandle.class, "tableHandle").getTableMetadata(); + } + + @Override + public List listTables(ConnectorSession session, String schemaNameOrNull) + { + if (schemaNameOrNull != null && !schemaNameOrNull.equals(SCHEMA_NAME)) { + return ImmutableList.of(); + } + + Builder tableNames = ImmutableList.builder(); + for (ObjectName objectName : mbeanServer.queryNames(WILDCARD, null)) { + // todo remove lower case when presto supports mixed case names + tableNames.add(new SchemaTableName(SCHEMA_NAME, objectName.toString().toLowerCase(ENGLISH))); + } + return tableNames.build(); + } + + @Override + public ColumnHandle getSampleWeightColumnHandle(ConnectorTableHandle tableHandle) + { + return null; + } + + @Override + public Map getColumnHandles(ConnectorTableHandle tableHandle) + { + JmxTableHandle jmxTableHandle = checkType(tableHandle, JmxTableHandle.class, "tableHandle"); + return ImmutableMap.copyOf(Maps.uniqueIndex(jmxTableHandle.getColumns(), column -> column.getColumnName().toLowerCase(ENGLISH))); + } + + @Override + public ColumnMetadata getColumnMetadata(ConnectorTableHandle tableHandle, ColumnHandle columnHandle) + { + checkType(tableHandle, JmxTableHandle.class, "tableHandle"); + return checkType(columnHandle, JmxColumnHandle.class, "columnHandle").getColumnMetadata(); + } + + @Override + public Map> listTableColumns(ConnectorSession session, SchemaTablePrefix prefix) + { + checkNotNull(prefix, "prefix is null"); + if (prefix.getSchemaName() != null && !prefix.getSchemaName().equals(SCHEMA_NAME)) { + return ImmutableMap.of(); + } + + ImmutableMap.Builder> columns = ImmutableMap.builder(); + + List tableNames; + if (prefix.getTableName() == null) { + tableNames = listTables(session, prefix.getSchemaName()); + } + else { + tableNames = ImmutableList.of(new SchemaTableName(prefix.getSchemaName(), prefix.getTableName())); + } + + for (SchemaTableName tableName : tableNames) { + JmxTableHandle tableHandle = getTableHandle(session, tableName); + columns.put(tableName, tableHandle.getTableMetadata().getColumns()); + } + return columns.build(); + } + + private Type getColumnType(MBeanAttributeInfo attribute) + { + Type columnType; + switch (attribute.getType()) { + case "boolean": + case "java.lang.Boolean": + columnType = BOOLEAN; + break; + case "byte": + case "java.lang.Byte": + case "short": + case "java.lang.Short": + case "int": + case "java.lang.Integer": + case "long": + case "java.lang.Long": + columnType = BIGINT; + break; + case "java.lang.Number": + case "float": + case "java.lang.Float": + case "double": + case "java.lang.Double": + columnType = DOUBLE; + break; + default: + columnType = VARCHAR; + break; + } + return columnType; + } + + private Predicate objectNameEqualsIgnoreCase(ObjectName objectName) + { + final String canonicalObjectName = objectName.getCanonicalName(); + + return new Predicate() + { + @Override + public boolean apply(ObjectName input) + { + return canonicalObjectName.equalsIgnoreCase(input.getCanonicalName()); + } + }; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/connector/jmx/JmxRecordSetProvider.java b/presto-main/src/main/java/com/facebook/presto/connector/jmx/JmxRecordSetProvider.java new file mode 100644 index 00000000..acc68cce --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/connector/jmx/JmxRecordSetProvider.java @@ -0,0 +1,176 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.connector.jmx; + +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorRecordSetProvider; +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.InMemoryRecordSet; +import com.facebook.presto.spi.NodeManager; +import com.facebook.presto.spi.RecordSet; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.airlift.slice.Slice; + +import javax.inject.Inject; +import javax.management.Attribute; +import javax.management.JMException; +import javax.management.MBeanServer; +import javax.management.ObjectName; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import static com.facebook.presto.util.Types.checkType; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.stream.Collectors.toMap; + +public class JmxRecordSetProvider + implements ConnectorRecordSetProvider +{ + private final MBeanServer mbeanServer; + private final String nodeId; + + @Inject + public JmxRecordSetProvider(MBeanServer mbeanServer, NodeManager nodeManager) + { + this.mbeanServer = checkNotNull(mbeanServer, "mbeanServer is null"); + this.nodeId = checkNotNull(nodeManager, "nodeManager is null").getCurrentNode().getNodeIdentifier(); + } + + @Override + public RecordSet getRecordSet(ConnectorSplit split, List columns) + { + JmxTableHandle tableHandle = checkType(split, JmxSplit.class, "split").getTableHandle(); + + checkNotNull(columns, "columns is null"); + checkArgument(!columns.isEmpty(), "must provide at least one column"); + + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (ColumnHandle column : columns) { + JmxColumnHandle jmxColumnHandle = checkType(column, JmxColumnHandle.class, "column"); + builder.put(jmxColumnHandle.getColumnName(), jmxColumnHandle.getColumnType()); + } + ImmutableMap columnTypes = builder.build(); + + List> rows; + try { + Map attributes = getAttributes(columnTypes.keySet(), tableHandle); + List row = new ArrayList<>(); + // NOTE: data must be produced in the order of the columns parameter. This code relies on the + // fact that columnTypes is an ImmutableMap which is an order preserving LinkedHashMap under + // the covers. + for (Entry entry : columnTypes.entrySet()) { + if (entry.getKey().equals("node")) { + row.add(nodeId); + } + else { + Object value = attributes.get(entry.getKey()); + if (value == null) { + row.add(null); + } + else { + Class javaType = entry.getValue().getJavaType(); + if (javaType == boolean.class) { + if (value instanceof Boolean) { + row.add(value); + } + else { + // mbeans can lie about types + row.add(null); + } + } + else if (javaType == long.class) { + if (value instanceof Number) { + row.add(((Number) value).longValue()); + } + else { + // mbeans can lie about types + row.add(null); + } + } + else if (javaType == double.class) { + if (value instanceof Number) { + row.add(((Number) value).doubleValue()); + } + else { + // mbeans can lie about types + row.add(null); + } + } + else if (javaType == Slice.class) { + if (value.getClass().isArray()) { + // return a string representation of the array + if (value.getClass().getComponentType() == String.class) { + row.add(Arrays.toString((String[]) value)); + } + else if (value.getClass().getComponentType() == boolean.class) { + row.add(Arrays.toString((boolean[]) value)); + } + else if (value.getClass().getComponentType() == byte.class) { + row.add(Arrays.toString((byte[]) value)); + } + else if (value.getClass().getComponentType() == char.class) { + row.add(Arrays.toString((char[]) value)); + } + else if (value.getClass().getComponentType() == double.class) { + row.add(Arrays.toString((double[]) value)); + } + else if (value.getClass().getComponentType() == float.class) { + row.add(Arrays.toString((float[]) value)); + } + else if (value.getClass().getComponentType() == int.class) { + row.add(Arrays.toString((int[]) value)); + } + else if (value.getClass().getComponentType() == long.class) { + row.add(Arrays.toString((long[]) value)); + } + else if (value.getClass().getComponentType() == short.class) { + row.add(Arrays.toString((short[]) value)); + } + } + else { + row.add(value.toString()); + } + } + } + } + } + rows = ImmutableList.of(row); + } + catch (JMException e) { + rows = ImmutableList.of(); + } + + return new InMemoryRecordSet(columnTypes.values(), rows); + } + + private Map getAttributes(Set uniqueColumnNames, JmxTableHandle tableHandle) + throws JMException + { + ObjectName objectName = new ObjectName(tableHandle.getObjectName()); + + String[] columnNamesArray = uniqueColumnNames.toArray(new String[uniqueColumnNames.size()]); + + return mbeanServer.getAttributes(objectName, columnNamesArray) + .asList().stream() + .collect(toMap(Attribute::getName, Attribute::getValue)); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/connector/jmx/JmxSplit.java b/presto-main/src/main/java/com/facebook/presto/connector/jmx/JmxSplit.java new file mode 100644 index 00000000..8dd19e07 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/connector/jmx/JmxSplit.java @@ -0,0 +1,65 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.connector.jmx; + +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.HostAddress; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class JmxSplit + implements ConnectorSplit +{ + private final JmxTableHandle tableHandle; + private final List addresses; + + @JsonCreator + public JmxSplit( + @JsonProperty("tableHandle") JmxTableHandle tableHandle, + @JsonProperty("addresses") List addresses) + { + this.tableHandle = checkNotNull(tableHandle, "tableHandle is null"); + this.addresses = ImmutableList.copyOf(checkNotNull(addresses, "addresses is null")); + } + + @JsonProperty + public JmxTableHandle getTableHandle() + { + return tableHandle; + } + + @Override + public boolean isRemotelyAccessible() + { + return false; + } + + @JsonProperty + @Override + public List getAddresses() + { + return addresses; + } + + @Override + public Object getInfo() + { + return this; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/connector/jmx/JmxSplitManager.java b/presto-main/src/main/java/com/facebook/presto/connector/jmx/JmxSplitManager.java new file mode 100644 index 00000000..e04b24ee --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/connector/jmx/JmxSplitManager.java @@ -0,0 +1,135 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.connector.jmx; + +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorPartition; +import com.facebook.presto.spi.ConnectorPartitionResult; +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.ConnectorSplitManager; +import com.facebook.presto.spi.ConnectorSplitSource; +import com.facebook.presto.spi.ConnectorTableHandle; +import com.facebook.presto.spi.FixedSplitSource; +import com.facebook.presto.spi.NodeManager; +import com.facebook.presto.spi.TupleDomain; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +import javax.inject.Inject; + +import java.util.List; + +import static com.facebook.presto.spi.TupleDomain.withFixedValues; +import static com.facebook.presto.util.ImmutableCollectors.toImmutableList; +import static com.facebook.presto.util.Types.checkType; +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.Iterables.getOnlyElement; +import static io.airlift.slice.Slices.utf8Slice; + +public class JmxSplitManager + implements ConnectorSplitManager +{ + private final String connectorId; + private final NodeManager nodeManager; + + @Inject + public JmxSplitManager(JmxConnectorId jmxConnectorId, NodeManager nodeManager) + { + this.connectorId = checkNotNull(jmxConnectorId, "jmxConnectorId is null").toString(); + this.nodeManager = checkNotNull(nodeManager, "nodeManager is null"); + } + + @Override + public ConnectorPartitionResult getPartitions(ConnectorTableHandle table, TupleDomain tupleDomain) + { + checkNotNull(tupleDomain, "tupleDomain is null"); + JmxTableHandle jmxTableHandle = checkType(table, JmxTableHandle.class, "table"); + + List partitions = ImmutableList.of(new JmxPartition(jmxTableHandle, tupleDomain)); + return new ConnectorPartitionResult(partitions, tupleDomain); + } + + @Override + public ConnectorSplitSource getPartitionSplits(ConnectorTableHandle table, List partitions) + { + checkNotNull(partitions, "partitions is null"); + if (partitions.isEmpty()) { + return new FixedSplitSource(connectorId, ImmutableList.of()); + } + + JmxPartition jmxPartition = checkType(getOnlyElement(partitions), JmxPartition.class, "partition"); + JmxTableHandle tableHandle = jmxPartition.getTableHandle(); + TupleDomain predicate = jmxPartition.getPredicate(); + + //TODO is there a better way to get the node column? + JmxColumnHandle nodeColumnHandle = tableHandle.getColumns().get(0); + + ImmutableList splits = nodeManager.getActiveNodes() + .stream() + .filter(node -> { + TupleDomain exactNodeMatch = withFixedValues(ImmutableMap.of(nodeColumnHandle, utf8Slice(node.getNodeIdentifier()))); + return predicate.overlaps(exactNodeMatch); + } + ) + .map(node -> new JmxSplit(tableHandle, ImmutableList.of(node.getHostAndPort()))) + .collect(toImmutableList()); + + return new FixedSplitSource(connectorId, splits); + } + + public static class JmxPartition + implements ConnectorPartition + { + private final JmxTableHandle tableHandle; + private final TupleDomain predicate; + + public JmxPartition(JmxTableHandle tableHandle, TupleDomain predicate) + { + this.tableHandle = checkNotNull(tableHandle, "tableHandle is null"); + this.predicate = checkNotNull(predicate, "predicate is null"); + } + + public JmxTableHandle getTableHandle() + { + return tableHandle; + } + + public TupleDomain getPredicate() + { + return predicate; + } + + @Override + public String getPartitionId() + { + return "jmx"; + } + + @Override + public TupleDomain getTupleDomain() + { + return TupleDomain.all(); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("tableHandle", tableHandle) + .add("predicate", predicate) + .toString(); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/connector/jmx/JmxTableHandle.java b/presto-main/src/main/java/com/facebook/presto/connector/jmx/JmxTableHandle.java new file mode 100644 index 00000000..e61932db --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/connector/jmx/JmxTableHandle.java @@ -0,0 +1,100 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.connector.jmx; + +import com.facebook.presto.spi.ConnectorTableHandle; +import com.facebook.presto.spi.ConnectorTableMetadata; +import com.facebook.presto.spi.SchemaTableName; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; + +import java.util.List; +import java.util.Objects; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.collect.Iterables.transform; + +public class JmxTableHandle + implements ConnectorTableHandle +{ + private final String connectorId; + private final String objectName; + private final List columns; + + @JsonCreator + public JmxTableHandle( + @JsonProperty("connectorId") String connectorId, + @JsonProperty("objectName") String objectName, + @JsonProperty("columns") List columns) + { + this.connectorId = connectorId; + this.objectName = objectName; + this.columns = columns; + } + + @JsonProperty + public String getConnectorId() + { + return connectorId; + } + + @JsonProperty + public String getObjectName() + { + return objectName; + } + + @JsonProperty + public List getColumns() + { + return columns; + } + + @Override + public int hashCode() + { + return Objects.hash(connectorId, objectName, columns); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + JmxTableHandle other = (JmxTableHandle) obj; + return Objects.equals(this.connectorId, other.connectorId) && + Objects.equals(this.objectName, other.objectName) && + Objects.equals(this.columns, other.columns); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("connectorId", connectorId) + .add("objectName", objectName) + .add("columns", columns) + .toString(); + } + + public ConnectorTableMetadata getTableMetadata() + { + return new ConnectorTableMetadata(new SchemaTableName(JmxMetadata.SCHEMA_NAME, objectName), ImmutableList.copyOf(transform(columns, JmxColumnHandle::getColumnMetadata))); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/connector/system/CatalogSystemTable.java b/presto-main/src/main/java/com/facebook/presto/connector/system/CatalogSystemTable.java new file mode 100644 index 00000000..9955195e --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/connector/system/CatalogSystemTable.java @@ -0,0 +1,70 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.connector.system; + +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.spi.ConnectorTableMetadata; +import com.facebook.presto.spi.InMemoryRecordSet; +import com.facebook.presto.spi.InMemoryRecordSet.Builder; +import com.facebook.presto.spi.RecordCursor; +import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.spi.SystemTable; + +import javax.inject.Inject; + +import java.util.Map; + +import static com.facebook.presto.metadata.MetadataUtil.TableMetadataBuilder.tableMetadataBuilder; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static com.google.common.base.Preconditions.checkNotNull; + +public class CatalogSystemTable + implements SystemTable +{ + public static final SchemaTableName CATALOG_TABLE_NAME = new SchemaTableName("metadata", "catalogs"); + + public static final ConnectorTableMetadata CATALOG_TABLE = tableMetadataBuilder(CATALOG_TABLE_NAME) + .column("catalog_name", VARCHAR) + .column("connector_id", VARCHAR) + .build(); + private final Metadata metadata; + + @Inject + public CatalogSystemTable(Metadata metadata) + { + this.metadata = checkNotNull(metadata); + } + + @Override + public boolean isDistributed() + { + return false; + } + + @Override + public ConnectorTableMetadata getTableMetadata() + { + return CATALOG_TABLE; + } + + @Override + public RecordCursor cursor() + { + Builder table = InMemoryRecordSet.builder(CATALOG_TABLE); + for (Map.Entry entry : metadata.getCatalogNames().entrySet()) { + table.addRow(entry.getKey(), entry.getValue()); + } + return table.build().cursor(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/connector/system/NodeSystemTable.java b/presto-main/src/main/java/com/facebook/presto/connector/system/NodeSystemTable.java new file mode 100644 index 00000000..7f6ee98b --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/connector/system/NodeSystemTable.java @@ -0,0 +1,93 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.connector.system; + +import com.facebook.presto.metadata.AllNodes; +import com.facebook.presto.metadata.InternalNodeManager; +import com.facebook.presto.metadata.PrestoNode; +import com.facebook.presto.spi.ConnectorTableMetadata; +import com.facebook.presto.spi.InMemoryRecordSet; +import com.facebook.presto.spi.InMemoryRecordSet.Builder; +import com.facebook.presto.spi.Node; +import com.facebook.presto.spi.RecordCursor; +import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.spi.SystemTable; + +import javax.inject.Inject; + +import static com.facebook.presto.metadata.MetadataUtil.TableMetadataBuilder.tableMetadataBuilder; +import static com.facebook.presto.spi.type.BooleanType.BOOLEAN; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static com.google.common.base.Preconditions.checkNotNull; + +public class NodeSystemTable + implements SystemTable +{ + public static final SchemaTableName NODES_TABLE_NAME = new SchemaTableName("runtime", "nodes"); + + public static final ConnectorTableMetadata NODES_TABLE = tableMetadataBuilder(NODES_TABLE_NAME) + .column("node_id", VARCHAR) + .column("http_uri", VARCHAR) + .column("node_version", VARCHAR) + .column("coordinator", BOOLEAN) + .column("active", BOOLEAN) + .build(); + + private final InternalNodeManager nodeManager; + + @Inject + public NodeSystemTable(InternalNodeManager nodeManager) + { + this.nodeManager = checkNotNull(nodeManager, "nodeManager is null"); + } + + @Override + public boolean isDistributed() + { + return false; + } + + @Override + public ConnectorTableMetadata getTableMetadata() + { + return NODES_TABLE; + } + + @Override + public RecordCursor cursor() + { + Builder table = InMemoryRecordSet.builder(NODES_TABLE); + AllNodes allNodes = nodeManager.getAllNodes(); + for (Node node : allNodes.getActiveNodes()) { + table.addRow(node.getNodeIdentifier(), node.getHttpUri().toString(), getNodeVersion(node), isCoordinator(node), Boolean.TRUE); + } + for (Node node : allNodes.getInactiveNodes()) { + table.addRow(node.getNodeIdentifier(), node.getHttpUri().toString(), getNodeVersion(node), isCoordinator(node), Boolean.FALSE); + } + return table.build().cursor(); + } + + private static String getNodeVersion(Node node) + { + if (node instanceof PrestoNode) { + return ((PrestoNode) node).getNodeVersion().toString(); + } + return ""; + } + + private boolean isCoordinator(Node node) + { + return nodeManager.getCoordinators().contains(node); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/connector/system/QuerySystemTable.java b/presto-main/src/main/java/com/facebook/presto/connector/system/QuerySystemTable.java new file mode 100644 index 00000000..20437743 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/connector/system/QuerySystemTable.java @@ -0,0 +1,122 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.connector.system; + +import com.facebook.presto.execution.QueryInfo; +import com.facebook.presto.execution.QueryManager; +import com.facebook.presto.execution.QueryStats; +import com.facebook.presto.spi.ConnectorTableMetadata; +import com.facebook.presto.spi.InMemoryRecordSet; +import com.facebook.presto.spi.InMemoryRecordSet.Builder; +import com.facebook.presto.spi.RecordCursor; +import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.spi.SystemTable; +import io.airlift.node.NodeInfo; +import io.airlift.units.Duration; +import org.joda.time.DateTime; + +import javax.inject.Inject; + +import static com.facebook.presto.metadata.MetadataUtil.TableMetadataBuilder.tableMetadataBuilder; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.TimestampType.TIMESTAMP; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; + +public class QuerySystemTable + implements SystemTable +{ + public static final SchemaTableName QUERY_TABLE_NAME = new SchemaTableName("runtime", "queries"); + + public static final ConnectorTableMetadata QUERY_TABLE = tableMetadataBuilder(QUERY_TABLE_NAME) + .column("node_id", VARCHAR) + .column("query_id", VARCHAR) + .column("state", VARCHAR) + .column("user", VARCHAR) + .column("source", VARCHAR) + .column("query", VARCHAR) + + .column("queued_time_ms", BIGINT) + .column("analysis_time_ms", BIGINT) + .column("distributed_planning_time_ms", BIGINT) + + .column("created", TIMESTAMP) + .column("started", TIMESTAMP) + .column("last_heartbeat", TIMESTAMP) + .column("end", TIMESTAMP) + .build(); + + private final QueryManager queryManager; + private final String nodeId; + + @Inject + public QuerySystemTable(QueryManager queryManager, NodeInfo nodeInfo) + { + this.queryManager = queryManager; + this.nodeId = nodeInfo.getNodeId(); + } + + @Override + public boolean isDistributed() + { + return true; + } + + @Override + public ConnectorTableMetadata getTableMetadata() + { + return QUERY_TABLE; + } + + @Override + public RecordCursor cursor() + { + Builder table = InMemoryRecordSet.builder(QUERY_TABLE); + for (QueryInfo queryInfo : queryManager.getAllQueryInfo()) { + QueryStats queryStats = queryInfo.getQueryStats(); + table.addRow( + nodeId, + queryInfo.getQueryId().toString(), + queryInfo.getState().toString(), + queryInfo.getSession().getUser(), + queryInfo.getSession().getSource(), + queryInfo.getQuery(), + + toMillis(queryStats.getQueuedTime()), + toMillis(queryStats.getAnalysisTime()), + toMillis(queryStats.getDistributedPlanningTime()), + + toTimeStamp(queryStats.getCreateTime()), + toTimeStamp(queryStats.getExecutionStartTime()), + toTimeStamp(queryStats.getLastHeartbeat()), + toTimeStamp(queryStats.getEndTime())); + } + return table.build().cursor(); + } + + private static Long toMillis(Duration duration) + { + if (duration == null) { + return null; + } + return duration.toMillis(); + } + + private static Long toTimeStamp(DateTime dateTime) + { + if (dateTime == null) { + return null; + } + return dateTime.getMillis(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/connector/system/SystemColumnHandle.java b/presto-main/src/main/java/com/facebook/presto/connector/system/SystemColumnHandle.java new file mode 100644 index 00000000..5004ac03 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/connector/system/SystemColumnHandle.java @@ -0,0 +1,77 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.connector.system; + +import com.facebook.presto.spi.ColumnMetadata; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorTableMetadata; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableMap; + +import java.util.Map; +import java.util.Objects; + +public class SystemColumnHandle + implements ColumnHandle +{ + private final String columnName; + + @JsonCreator + public SystemColumnHandle(@JsonProperty("columnName") String columnName) + { + this.columnName = columnName; + } + + @JsonProperty + public String getColumnName() + { + return columnName; + } + + @Override + public int hashCode() + { + return Objects.hash(columnName); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final SystemColumnHandle other = (SystemColumnHandle) obj; + return Objects.equals(this.columnName, other.columnName); + } + + @Override + public String toString() + { + return "system:" + columnName; + } + + public static Map toSystemColumnHandles(ConnectorTableMetadata tableMetadata) + { + ImmutableMap.Builder columnHandles = ImmutableMap.builder(); + for (ColumnMetadata columnMetadata : tableMetadata.getColumns()) { + columnHandles.put(columnMetadata.getName(), new SystemColumnHandle(columnMetadata.getName())); + } + + return columnHandles.build(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/connector/system/SystemConnector.java b/presto-main/src/main/java/com/facebook/presto/connector/system/SystemConnector.java new file mode 100644 index 00000000..bf60e036 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/connector/system/SystemConnector.java @@ -0,0 +1,68 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.connector.system; + +import com.facebook.presto.spi.Connector; +import com.facebook.presto.spi.ConnectorHandleResolver; +import com.facebook.presto.spi.ConnectorMetadata; +import com.facebook.presto.spi.ConnectorRecordSetProvider; +import com.facebook.presto.spi.ConnectorSplitManager; +import com.facebook.presto.spi.NodeManager; +import com.facebook.presto.spi.SystemTable; + +import javax.inject.Inject; + +import java.util.Set; + +public class SystemConnector + implements Connector +{ + public static final String NAME = "system"; + + private final ConnectorMetadata metadata; + private final ConnectorSplitManager splitManager; + private final ConnectorRecordSetProvider recordSetProvider; + + @Inject + public SystemConnector(NodeManager nodeManager, Set tables) + { + metadata = new SystemTablesMetadata(tables); + splitManager = new SystemSplitManager(nodeManager, tables); + recordSetProvider = new SystemRecordSetProvider(tables); + } + + @Override + public ConnectorMetadata getMetadata() + { + return metadata; + } + + @Override + public ConnectorSplitManager getSplitManager() + { + return splitManager; + } + + @Override + public ConnectorHandleResolver getHandleResolver() + { + return new SystemHandleResolver(); + } + + @Override + public ConnectorRecordSetProvider getRecordSetProvider() + { + return recordSetProvider; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/connector/system/SystemHandleResolver.java b/presto-main/src/main/java/com/facebook/presto/connector/system/SystemHandleResolver.java new file mode 100644 index 00000000..d0d95f05 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/connector/system/SystemHandleResolver.java @@ -0,0 +1,59 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.connector.system; + +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorHandleResolver; +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.ConnectorTableHandle; + +public class SystemHandleResolver + implements ConnectorHandleResolver +{ + @Override + public boolean canHandle(ConnectorTableHandle tableHandle) + { + return tableHandle instanceof SystemTableHandle; + } + + @Override + public boolean canHandle(ColumnHandle columnHandle) + { + return columnHandle instanceof SystemColumnHandle; + } + + @Override + public boolean canHandle(ConnectorSplit split) + { + return split instanceof SystemSplit; + } + + @Override + public Class getTableHandleClass() + { + return SystemTableHandle.class; + } + + @Override + public Class getColumnHandleClass() + { + return SystemColumnHandle.class; + } + + @Override + public Class getSplitClass() + { + return SystemSplit.class; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/connector/system/SystemRecordSetProvider.java b/presto-main/src/main/java/com/facebook/presto/connector/system/SystemRecordSetProvider.java new file mode 100644 index 00000000..a1ce9378 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/connector/system/SystemRecordSetProvider.java @@ -0,0 +1,82 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.connector.system; + +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ColumnMetadata; +import com.facebook.presto.spi.ConnectorRecordSetProvider; +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.RecordSet; +import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.spi.SystemTable; +import com.facebook.presto.split.MappedRecordSet; +import com.google.common.collect.ImmutableList; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.facebook.presto.spi.StandardErrorCode.INTERNAL_ERROR; +import static com.facebook.presto.util.Types.checkType; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.Maps.uniqueIndex; +import static java.lang.String.format; + +public class SystemRecordSetProvider + implements ConnectorRecordSetProvider +{ + private final Map tables; + + public SystemRecordSetProvider(Set tables) + { + this.tables = uniqueIndex(tables, table -> table.getTableMetadata().getTable()); + } + + @Override + public RecordSet getRecordSet(ConnectorSplit split, List columns) + { + SchemaTableName tableName = checkType(split, SystemSplit.class, "split").getTableHandle().getSchemaTableName(); + + checkNotNull(columns, "columns is null"); + + SystemTable systemTable = tables.get(tableName); + checkArgument(systemTable != null, "Table %s does not exist", tableName); + List tableColumns = systemTable.getTableMetadata().getColumns(); + + Map columnsByName = new HashMap<>(); + for (int i = 0; i < tableColumns.size(); i++) { + ColumnMetadata column = tableColumns.get(i); + if (columnsByName.put(column.getName(), i) != null) { + throw new PrestoException(INTERNAL_ERROR, "Duplicate column name: " + column.getName()); + } + } + + ImmutableList.Builder userToSystemFieldIndex = ImmutableList.builder(); + for (ColumnHandle column : columns) { + String columnName = checkType(column, SystemColumnHandle.class, "column").getColumnName(); + + Integer index = columnsByName.get(columnName); + if (index == null) { + throw new PrestoException(INTERNAL_ERROR, format("Column does not exist: %s.%s", tableName, columnName)); + } + + userToSystemFieldIndex.add(index); + } + + return new MappedRecordSet(systemTable, userToSystemFieldIndex.build()); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/connector/system/SystemSplit.java b/presto-main/src/main/java/com/facebook/presto/connector/system/SystemSplit.java new file mode 100644 index 00000000..dabac663 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/connector/system/SystemSplit.java @@ -0,0 +1,84 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.connector.system; + +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.HostAddress; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class SystemSplit + implements ConnectorSplit +{ + private final SystemTableHandle tableHandle; + private final List addresses; + + public SystemSplit(SystemTableHandle tableHandle, HostAddress address) + { + this(tableHandle, ImmutableList.of(checkNotNull(address, "address is null"))); + } + + @JsonCreator + public SystemSplit( + @JsonProperty("tableHandle") SystemTableHandle tableHandle, + @JsonProperty("addresses") List addresses) + { + this.tableHandle = checkNotNull(tableHandle, "tableHandle is null"); + + checkNotNull(addresses, "hosts is null"); + checkArgument(!addresses.isEmpty(), "hosts is empty"); + this.addresses = ImmutableList.copyOf(addresses); + } + + @Override + public boolean isRemotelyAccessible() + { + return false; + } + + @Override + @JsonProperty + public List getAddresses() + { + return addresses; + } + + @JsonProperty + public SystemTableHandle getTableHandle() + { + return tableHandle; + } + + @Override + public Object getInfo() + { + return this; + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("tableHandle", tableHandle) + .add("addresses", addresses) + .toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/connector/system/SystemSplitManager.java b/presto-main/src/main/java/com/facebook/presto/connector/system/SystemSplitManager.java new file mode 100644 index 00000000..90ec532c --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/connector/system/SystemSplitManager.java @@ -0,0 +1,127 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.connector.system; + +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorPartition; +import com.facebook.presto.spi.ConnectorPartitionResult; +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.ConnectorSplitManager; +import com.facebook.presto.spi.ConnectorSplitSource; +import com.facebook.presto.spi.ConnectorTableHandle; +import com.facebook.presto.spi.FixedSplitSource; +import com.facebook.presto.spi.HostAddress; +import com.facebook.presto.spi.Node; +import com.facebook.presto.spi.NodeManager; +import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.spi.SystemTable; +import com.facebook.presto.spi.TupleDomain; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.facebook.presto.util.Types.checkType; +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.Maps.uniqueIndex; + +public class SystemSplitManager + implements ConnectorSplitManager +{ + private final NodeManager nodeManager; + private final Map tables; + + public SystemSplitManager(NodeManager nodeManager, Set tables) + { + this.nodeManager = checkNotNull(nodeManager, "nodeManager is null"); + this.tables = uniqueIndex(tables, table -> table.getTableMetadata().getTable()); + } + + @Override + public ConnectorPartitionResult getPartitions(ConnectorTableHandle table, TupleDomain tupleDomain) + { + checkNotNull(tupleDomain, "tupleDomain is null"); + SystemTableHandle systemTableHandle = checkType(table, SystemTableHandle.class, "table"); + + List partitions = ImmutableList.of(new SystemPartition(systemTableHandle)); + return new ConnectorPartitionResult(partitions, tupleDomain); + } + + @Override + public ConnectorSplitSource getPartitionSplits(ConnectorTableHandle table, List partitions) + { + checkNotNull(partitions, "partitions is null"); + if (partitions.isEmpty()) { + return new FixedSplitSource(SystemConnector.NAME, ImmutableList.of()); + } + + ConnectorPartition partition = Iterables.getOnlyElement(partitions); + SystemPartition systemPartition = checkType(partition, SystemPartition.class, "partition"); + + SystemTable systemTable = tables.get(systemPartition.getTableHandle().getSchemaTableName()); + checkArgument(systemTable != null, "Table %s does not exist", systemPartition.getTableHandle().getTableName()); + + if (systemTable.isDistributed()) { + ImmutableList.Builder splits = ImmutableList.builder(); + for (Node node : nodeManager.getActiveNodes()) { + splits.add(new SystemSplit(systemPartition.getTableHandle(), node.getHostAndPort())); + } + return new FixedSplitSource(SystemConnector.NAME, splits.build()); + } + + HostAddress address = nodeManager.getCurrentNode().getHostAndPort(); + ConnectorSplit split = new SystemSplit(systemPartition.getTableHandle(), address); + return new FixedSplitSource(SystemConnector.NAME, ImmutableList.of(split)); + } + + public static class SystemPartition + implements ConnectorPartition + { + private final SystemTableHandle tableHandle; + + public SystemPartition(SystemTableHandle tableHandle) + { + this.tableHandle = checkNotNull(tableHandle, "tableHandle is null"); + } + + public SystemTableHandle getTableHandle() + { + return tableHandle; + } + + @Override + public String getPartitionId() + { + return tableHandle.getSchemaTableName().toString(); + } + + @Override + public TupleDomain getTupleDomain() + { + return TupleDomain.all(); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("tableHandle", tableHandle) + .toString(); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/connector/system/SystemTableHandle.java b/presto-main/src/main/java/com/facebook/presto/connector/system/SystemTableHandle.java new file mode 100644 index 00000000..a3f36d9d --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/connector/system/SystemTableHandle.java @@ -0,0 +1,89 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.connector.system; + +import com.facebook.presto.spi.ConnectorTableHandle; +import com.facebook.presto.spi.SchemaTableName; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +import static com.facebook.presto.metadata.MetadataUtil.checkSchemaName; +import static com.facebook.presto.metadata.MetadataUtil.checkTableName; +import static com.google.common.base.Preconditions.checkNotNull; + +public class SystemTableHandle + implements ConnectorTableHandle +{ + private final String schemaName; + private final String tableName; + + @JsonCreator + public SystemTableHandle(@JsonProperty("schemaName") String schemaName, @JsonProperty("tableName") String tableName) + { + this.schemaName = checkSchemaName(schemaName); + this.tableName = checkTableName(tableName); + } + + public SystemTableHandle(SchemaTableName tableName) + { + checkNotNull(tableName, "tableName is null"); + this.schemaName = tableName.getSchemaName(); + this.tableName = tableName.getTableName(); + } + + @JsonProperty + public String getSchemaName() + { + return schemaName; + } + + @JsonProperty + public String getTableName() + { + return tableName; + } + + public SchemaTableName getSchemaTableName() + { + return new SchemaTableName(schemaName, tableName); + } + + @Override + public String toString() + { + return "system:" + schemaName + "." + tableName; + } + + @Override + public int hashCode() + { + return Objects.hash(schemaName, tableName); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final SystemTableHandle other = (SystemTableHandle) obj; + return Objects.equals(this.schemaName, other.schemaName) && + Objects.equals(this.tableName, other.tableName); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/connector/system/SystemTablesMetadata.java b/presto-main/src/main/java/com/facebook/presto/connector/system/SystemTablesMetadata.java new file mode 100644 index 00000000..47e6dc32 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/connector/system/SystemTablesMetadata.java @@ -0,0 +1,137 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.connector.system; + +import com.facebook.presto.spi.ColumnMetadata; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.ConnectorTableHandle; +import com.facebook.presto.spi.ConnectorTableMetadata; +import com.facebook.presto.spi.ReadOnlyConnectorMetadata; +import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.spi.SchemaTablePrefix; +import com.facebook.presto.spi.SystemTable; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import static com.facebook.presto.connector.system.SystemColumnHandle.toSystemColumnHandles; +import static com.facebook.presto.metadata.MetadataUtil.findColumnMetadata; +import static com.facebook.presto.util.ImmutableCollectors.toImmutableList; +import static com.facebook.presto.util.Types.checkType; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.function.Function.identity; +import static java.util.stream.Collectors.toMap; + +public class SystemTablesMetadata + extends ReadOnlyConnectorMetadata +{ + private final Map tables; + + public SystemTablesMetadata(Set tables) + { + this.tables = tables.stream() + .map(SystemTable::getTableMetadata) + .collect(toMap(ConnectorTableMetadata::getTable, identity())); + } + + private SystemTableHandle checkTableHandle(ConnectorTableHandle tableHandle) + { + SystemTableHandle systemTableHandle = checkType(tableHandle, SystemTableHandle.class, "tableHandle"); + checkArgument(tables.containsKey(systemTableHandle.getSchemaTableName())); + return systemTableHandle; + } + + @Override + public List listSchemaNames(ConnectorSession session) + { + return tables.keySet().stream() + .map(SchemaTableName::getSchemaName) + .distinct() + .collect(toImmutableList()); + } + + @Override + public ConnectorTableHandle getTableHandle(ConnectorSession session, SchemaTableName tableName) + { + if (!tables.containsKey(tableName)) { + return null; + } + return new SystemTableHandle(tableName); + } + + @Override + public ConnectorTableMetadata getTableMetadata(ConnectorTableHandle tableHandle) + { + SystemTableHandle systemTableHandle = checkTableHandle(tableHandle); + return tables.get(systemTableHandle.getSchemaTableName()); + } + + @Override + public List listTables(ConnectorSession session, String schemaNameOrNull) + { + if (schemaNameOrNull == null) { + return ImmutableList.copyOf(tables.keySet()); + } + + return tables.keySet().stream() + .filter(table -> table.getSchemaName().equals(schemaNameOrNull)) + .collect(toImmutableList()); + } + + @Override + public ColumnHandle getSampleWeightColumnHandle(ConnectorTableHandle tableHandle) + { + return null; + } + + @Override + public ColumnMetadata getColumnMetadata(ConnectorTableHandle tableHandle, ColumnHandle columnHandle) + { + SystemTableHandle systemTableHandle = checkTableHandle(tableHandle); + ConnectorTableMetadata tableMetadata = tables.get(systemTableHandle.getSchemaTableName()); + + String columnName = checkType(columnHandle, SystemColumnHandle.class, "columnHandle").getColumnName(); + + ColumnMetadata columnMetadata = findColumnMetadata(tableMetadata, columnName); + checkArgument(columnMetadata != null, "Column %s on table %s does not exist", columnName, tableMetadata.getTable()); + return columnMetadata; + } + + @Override + public Map getColumnHandles(ConnectorTableHandle tableHandle) + { + SystemTableHandle systemTableHandle = checkTableHandle(tableHandle); + + return toSystemColumnHandles(tables.get(systemTableHandle.getSchemaTableName())); + } + + @Override + public Map> listTableColumns(ConnectorSession session, SchemaTablePrefix prefix) + { + checkNotNull(prefix, "prefix is null"); + ImmutableMap.Builder> builder = ImmutableMap.builder(); + for (Entry entry : tables.entrySet()) { + if (prefix.matches(entry.getKey())) { + builder.put(entry.getKey(), entry.getValue().getColumns()); + } + } + return builder.build(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/connector/system/SystemTablesModule.java b/presto-main/src/main/java/com/facebook/presto/connector/system/SystemTablesModule.java new file mode 100644 index 00000000..86f3d32e --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/connector/system/SystemTablesModule.java @@ -0,0 +1,49 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.connector.system; + +import com.facebook.presto.connector.ConnectorManager; +import com.facebook.presto.spi.SystemTable; +import com.google.inject.Binder; +import com.google.inject.Module; +import com.google.inject.Scopes; +import com.google.inject.multibindings.Multibinder; + +import javax.inject.Inject; + +public class SystemTablesModule + implements Module +{ + @Override + public void configure(Binder binder) + { + Multibinder globalTableBinder = Multibinder.newSetBinder(binder, SystemTable.class); + globalTableBinder.addBinding().to(NodeSystemTable.class).in(Scopes.SINGLETON); + globalTableBinder.addBinding().to(QuerySystemTable.class).in(Scopes.SINGLETON); + globalTableBinder.addBinding().to(TaskSystemTable.class).in(Scopes.SINGLETON); + globalTableBinder.addBinding().to(CatalogSystemTable.class).in(Scopes.SINGLETON); + + binder.bind(SystemConnector.class).in(Scopes.SINGLETON); + binder.bind(SystemTablesRegistrar.class).asEagerSingleton(); + } + + private static class SystemTablesRegistrar + { + @Inject + public SystemTablesRegistrar(ConnectorManager manager, SystemConnector connector) + { + manager.createConnection(SystemConnector.NAME, connector); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/connector/system/TaskSystemTable.java b/presto-main/src/main/java/com/facebook/presto/connector/system/TaskSystemTable.java new file mode 100644 index 00000000..fe6349b3 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/connector/system/TaskSystemTable.java @@ -0,0 +1,161 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.connector.system; + +import com.facebook.presto.execution.TaskInfo; +import com.facebook.presto.execution.TaskManager; +import com.facebook.presto.operator.TaskStats; +import com.facebook.presto.spi.ConnectorTableMetadata; +import com.facebook.presto.spi.InMemoryRecordSet; +import com.facebook.presto.spi.InMemoryRecordSet.Builder; +import com.facebook.presto.spi.RecordCursor; +import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.spi.SystemTable; +import io.airlift.node.NodeInfo; +import io.airlift.units.DataSize; +import io.airlift.units.Duration; +import org.joda.time.DateTime; + +import javax.inject.Inject; + +import static com.facebook.presto.metadata.MetadataUtil.TableMetadataBuilder.tableMetadataBuilder; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.TimestampType.TIMESTAMP; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; + +public class TaskSystemTable + implements SystemTable +{ + public static final SchemaTableName TASK_TABLE_NAME = new SchemaTableName("runtime", "tasks"); + + public static final ConnectorTableMetadata TASK_TABLE = tableMetadataBuilder(TASK_TABLE_NAME) + .column("node_id", VARCHAR) + + .column("task_id", VARCHAR) + .column("stage_id", VARCHAR) + .column("query_id", VARCHAR) + .column("state", VARCHAR) + + .column("splits", BIGINT) + .column("queued_splits", BIGINT) + .column("running_splits", BIGINT) + .column("completed_splits", BIGINT) + + .column("split_scheduled_time_ms", BIGINT) + .column("split_cpu_time_ms", BIGINT) + .column("split_user_time_ms", BIGINT) + .column("split_blocked_time_ms", BIGINT) + + .column("raw_input_bytes", BIGINT) + .column("raw_input_rows", BIGINT) + + .column("processed_input_bytes", BIGINT) + .column("processed_input_rows", BIGINT) + + .column("output_bytes", BIGINT) + .column("output_rows", BIGINT) + + .column("created", TIMESTAMP) + .column("start", TIMESTAMP) + .column("last_heartbeat", TIMESTAMP) + .column("end", TIMESTAMP) + .build(); + + private final TaskManager taskManager; + private final String nodeId; + + @Inject + public TaskSystemTable(TaskManager taskManager, NodeInfo nodeInfo) + { + this.taskManager = taskManager; + this.nodeId = nodeInfo.getNodeId(); + } + + @Override + public boolean isDistributed() + { + return true; + } + + @Override + public ConnectorTableMetadata getTableMetadata() + { + return TASK_TABLE; + } + + @Override + public RecordCursor cursor() + { + Builder table = InMemoryRecordSet.builder(TASK_TABLE); + for (TaskInfo taskInfo : taskManager.getAllTaskInfo()) { + TaskStats stats = taskInfo.getStats(); + table.addRow( + nodeId, + + taskInfo.getTaskId().toString(), + taskInfo.getTaskId().getStageId().toString(), + taskInfo.getTaskId().getQueryId().toString(), + taskInfo.getState().toString(), + + (long) stats.getTotalDrivers(), + (long) stats.getQueuedDrivers(), + (long) stats.getRunningDrivers(), + (long) stats.getCompletedDrivers(), + + toMillis(stats.getTotalScheduledTime()), + toMillis(stats.getTotalCpuTime()), + toMillis(stats.getTotalUserTime()), + toMillis(stats.getTotalBlockedTime()), + + toBytes(stats.getRawInputDataSize()), + stats.getRawInputPositions(), + + toBytes(stats.getProcessedInputDataSize()), + stats.getProcessedInputPositions(), + + toBytes(stats.getOutputDataSize()), + stats.getOutputPositions(), + + toTimeStamp(stats.getCreateTime()), + toTimeStamp(stats.getFirstStartTime()), + toTimeStamp(taskInfo.getLastHeartbeat()), + toTimeStamp(stats.getEndTime())); + } + return table.build().cursor(); + } + + private static Long toMillis(Duration duration) + { + if (duration == null) { + return null; + } + return duration.toMillis(); + } + + private static Long toBytes(DataSize dataSize) + { + if (dataSize == null) { + return null; + } + return dataSize.toBytes(); + } + + private static Long toTimeStamp(DateTime dateTime) + { + if (dateTime == null) { + return null; + } + return dateTime.getMillis(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/discovery/EmbeddedDiscoveryConfig.java b/presto-main/src/main/java/com/facebook/presto/discovery/EmbeddedDiscoveryConfig.java new file mode 100644 index 00000000..71df8177 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/discovery/EmbeddedDiscoveryConfig.java @@ -0,0 +1,33 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.discovery; + +import io.airlift.configuration.Config; + +public class EmbeddedDiscoveryConfig +{ + private boolean enabled; + + public boolean isEnabled() + { + return enabled; + } + + @Config("discovery-server.enabled") + public EmbeddedDiscoveryConfig setEnabled(boolean enabled) + { + this.enabled = enabled; + return this; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/discovery/EmbeddedDiscoveryModule.java b/presto-main/src/main/java/com/facebook/presto/discovery/EmbeddedDiscoveryModule.java new file mode 100644 index 00000000..856ca137 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/discovery/EmbeddedDiscoveryModule.java @@ -0,0 +1,149 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.discovery; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.inject.Binder; +import com.google.inject.Scopes; +import io.airlift.configuration.AbstractConfigurationAwareModule; +import io.airlift.discovery.client.ServiceDescriptor; +import io.airlift.discovery.client.ServiceInventory; +import io.airlift.discovery.client.ServiceSelector; +import io.airlift.discovery.server.DiscoveryConfig; +import io.airlift.discovery.server.DynamicAnnouncementResource; +import io.airlift.discovery.server.DynamicStore; +import io.airlift.discovery.server.ForDynamicStore; +import io.airlift.discovery.server.Id; +import io.airlift.discovery.server.ReplicatedDynamicStore; +import io.airlift.discovery.server.Service; +import io.airlift.discovery.server.ServiceResource; +import io.airlift.discovery.server.StaticStore; +import io.airlift.discovery.store.InMemoryStore; +import io.airlift.discovery.store.ReplicatedStoreModule; +import io.airlift.node.NodeInfo; + +import javax.inject.Inject; + +import java.util.List; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.util.concurrent.Futures.immediateFuture; +import static io.airlift.configuration.ConfigBinder.configBinder; +import static io.airlift.discovery.client.DiscoveryBinder.discoveryBinder; +import static io.airlift.jaxrs.JaxrsBinder.jaxrsBinder; +import static io.airlift.json.JsonCodecBinder.jsonCodecBinder; + +public class EmbeddedDiscoveryModule + extends AbstractConfigurationAwareModule +{ + @Override + protected void setup(Binder binder) + { + if (!buildConfigObject(EmbeddedDiscoveryConfig.class).isEnabled()) { + return; + } + + configBinder(binder).bindConfig(DiscoveryConfig.class); + jaxrsBinder(binder).bind(ServiceResource.class); + + discoveryBinder(binder).bindHttpAnnouncement("discovery"); + + jsonCodecBinder(binder).bindJsonCodec(Service.class); + jsonCodecBinder(binder).bindListJsonCodec(Service.class); + + binder.bind(ServiceSelector.class).to(DiscoveryServiceSelector.class); + binder.bind(StaticStore.class).to(EmptyStaticStore.class); + + jaxrsBinder(binder).bind(DynamicAnnouncementResource.class); + binder.bind(DynamicStore.class).to(ReplicatedDynamicStore.class).in(Scopes.SINGLETON); + binder.install(new ReplicatedStoreModule("dynamic", ForDynamicStore.class, InMemoryStore.class)); + } + + private static class DiscoveryServiceSelector + implements ServiceSelector + { + private final NodeInfo nodeInfo; + private final ServiceInventory inventory; + + @Inject + public DiscoveryServiceSelector(NodeInfo nodeInfo, ServiceInventory inventory) + { + this.nodeInfo = checkNotNull(nodeInfo, "nodeInfo is null"); + this.inventory = checkNotNull(inventory, "inventory is null"); + } + + @Override + public String getType() + { + return "discovery"; + } + + @Override + public String getPool() + { + return nodeInfo.getPool(); + } + + @Override + public List selectAllServices() + { + return ImmutableList.copyOf(inventory.getServiceDescriptors(getType())); + } + + @Override + public ListenableFuture> refresh() + { + // todo modify Service inventory to be async + inventory.updateServiceInventory(); + return immediateFuture(selectAllServices()); + } + } + + private static class EmptyStaticStore + implements StaticStore + { + @Override + public void put(Service service) + { + throw new UnsupportedOperationException(); + } + + @Override + public void delete(Id id) + { + throw new UnsupportedOperationException(); + } + + @Override + public Set getAll() + { + return ImmutableSet.of(); + } + + @Override + public Set get(String type) + { + return ImmutableSet.of(); + } + + @Override + public Set get(String type, String pool) + { + return ImmutableSet.of(); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/event/query/QueryCompletionEvent.java b/presto-main/src/main/java/com/facebook/presto/event/query/QueryCompletionEvent.java new file mode 100644 index 00000000..11ad5690 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/event/query/QueryCompletionEvent.java @@ -0,0 +1,396 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.event.query; + +import com.facebook.presto.execution.QueryId; +import com.facebook.presto.execution.QueryState; +import com.facebook.presto.spi.ErrorCode; +import com.google.common.collect.ImmutableList; +import io.airlift.event.client.EventField; +import io.airlift.event.client.EventType; +import io.airlift.units.DataSize; +import io.airlift.units.Duration; +import org.joda.time.DateTime; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +import java.net.URI; +import java.util.List; + +@Immutable +@EventType("QueryCompletion") +public class QueryCompletionEvent +{ + private final QueryId queryId; + private final String user; + private final String source; + private final String serverVersion; + private final String environment; + private final String catalog; + private final String schema; + private final String remoteClientAddress; + private final String userAgent; + private final QueryState queryState; + private final URI uri; + private final List fieldNames; + private final String query; + + private final DateTime createTime; + private final DateTime executionStartTime; + private final DateTime endTime; + + private final Long queuedTimeMs; + private final Long analysisTimeMs; + private final Long distributedPlanningTimeMs; + private final Long totalSplitWallTimeMs; + private final Long totalSplitCpuTimeMs; + private final Long totalBytes; + private final Long totalRows; + + private final Integer splits; + + private final ErrorCode errorCode; + private final String failureType; + private final String failureMessage; + + private final String outputStageJson; + private final String failuresJson; + + private final String inputsJson; + private final String sessionPropertiesJson; + + public QueryCompletionEvent( + QueryId queryId, + String user, + String source, + String serverVersion, + String environment, + String catalog, + String schema, + String remoteClientAddress, + String userAgent, + QueryState queryState, + URI uri, + List fieldNames, + String query, + DateTime createTime, + DateTime executionStartTime, + DateTime endTime, + Duration queuedTime, + Duration analysisTime, + Duration distributedPlanningTime, + Duration totalSplitWallTime, + Duration totalSplitCpuTime, + DataSize totalDataSize, + Long totalRows, + Integer splits, + ErrorCode errorCode, + String failureType, + String failureMessage, + String outputStageJson, + String failuresJson, + String inputsJson, + String sessionPropertiesJson) + { + this.queryId = queryId; + this.user = user; + this.source = source; + this.serverVersion = serverVersion; + this.environment = environment; + this.catalog = catalog; + this.schema = schema; + this.remoteClientAddress = remoteClientAddress; + this.userAgent = userAgent; + this.queryState = queryState; + this.uri = uri; + this.errorCode = errorCode; + this.fieldNames = ImmutableList.copyOf(fieldNames); + this.query = query; + this.createTime = createTime; + this.executionStartTime = executionStartTime; + this.endTime = endTime; + this.queuedTimeMs = durationToMillis(queuedTime); + this.analysisTimeMs = durationToMillis(analysisTime); + this.distributedPlanningTimeMs = durationToMillis(distributedPlanningTime); + this.totalSplitWallTimeMs = durationToMillis((totalSplitWallTime)); + this.totalSplitCpuTimeMs = durationToMillis(totalSplitCpuTime); + this.totalBytes = sizeToBytes(totalDataSize); + this.totalRows = totalRows; + this.splits = splits; + this.failureType = failureType; + this.failureMessage = failureMessage; + this.outputStageJson = outputStageJson; + this.failuresJson = failuresJson; + this.inputsJson = inputsJson; + this.sessionPropertiesJson = sessionPropertiesJson; + } + + @Nullable + private static Long durationToMillis(@Nullable Duration duration) + { + if (duration == null) { + return null; + } + return duration.toMillis(); + } + + @Nullable + private static Long sizeToBytes(@Nullable DataSize dataSize) + { + if (dataSize == null) { + return null; + } + return dataSize.toBytes(); + } + + @EventField + public String getQueryId() + { + return queryId.toString(); + } + + @EventField + public String getUser() + { + return user; + } + + @EventField + public String getSource() + { + return source; + } + + @EventField + public String getServerVersion() + { + return serverVersion; + } + + @EventField + public String getEnvironment() + { + return environment; + } + + @EventField + public String getCatalog() + { + return catalog; + } + + @EventField + public String getSchema() + { + return schema; + } + + @EventField + public String getRemoteClientAddress() + { + return remoteClientAddress; + } + + @EventField + public String getUserAgent() + { + return userAgent; + } + + @EventField + public String getQueryState() + { + return queryState.name(); + } + + @EventField + public String getUri() + { + return uri.toString(); + } + + @EventField + public List getFieldNames() + { + return fieldNames; + } + + @EventField + public String getQuery() + { + return query; + } + + @EventField + public DateTime getCreateTime() + { + return createTime; + } + + @EventField + public DateTime getExecutionStartTime() + { + return executionStartTime; + } + + @EventField + public DateTime getEndTime() + { + return endTime; + } + + @EventField + public Long getQueryWallTimeMs() + { + if (createTime == null || endTime == null) { + return null; + } + return endTime.getMillis() - createTime.getMillis(); + } + + @EventField + public Long getQueuedTimeMs() + { + return queuedTimeMs; + } + + @EventField + public Long getAnalysisTimeMs() + { + return analysisTimeMs; + } + + @EventField + public Long getDistributedPlanningTimeMs() + { + return distributedPlanningTimeMs; + } + + @EventField + public Long getTotalSplitWallTimeMs() + { + return totalSplitWallTimeMs; + } + + @EventField + public Long getTotalSplitCpuTimeMs() + { + return totalSplitCpuTimeMs; + } + + @EventField + public Long getBytesPerSec() + { + Long queryWallTimeMs = getQueryWallTimeMs(); + if (totalBytes == null || queryWallTimeMs == null) { + return null; + } + return totalBytes * 1000 / (queryWallTimeMs + 1); // add 1 to avoid divide by zero + } + + @EventField + public Long getBytesPerCpuSec() + { + if (totalBytes == null || totalSplitCpuTimeMs == null) { + return null; + } + return totalBytes * 1000 / (totalSplitCpuTimeMs + 1); // add 1 to avoid divide by zero + } + + @EventField + public Long getTotalBytes() + { + return totalBytes; + } + + @EventField + public Long getRowsPerSec() + { + Long queryWallTimeMs = getQueryWallTimeMs(); + if (totalRows == null || queryWallTimeMs == null) { + return null; + } + return totalRows * 1000 / (queryWallTimeMs + 1); // add 1 to avoid divide by zero + } + + @EventField + public Long getRowsPerCpuSec() + { + if (totalRows == null || totalSplitCpuTimeMs == null) { + return null; + } + return totalRows * 1000 / (totalSplitCpuTimeMs + 1); // add 1 to avoid divide by zero + } + + @EventField + public Long getTotalRows() + { + return totalRows; + } + + @EventField + public Integer getSplits() + { + return splits; + } + + @EventField + public Integer getErrorCode() + { + return errorCode == null ? null : errorCode.getCode(); + } + + @EventField + public String getErrorCodeName() + { + return errorCode == null ? null : errorCode.getName(); + } + + @EventField + public String getFailureType() + { + return failureType; + } + + @EventField + public String getFailureMessage() + { + return failureMessage; + } + + @EventField + public String getOutputStageJson() + { + return outputStageJson; + } + + @EventField + public String getFailuresJson() + { + return failuresJson; + } + + @EventField + public String getInputsJson() + { + return inputsJson; + } + + @EventField + public String getSessionPropertiesJson() + { + return sessionPropertiesJson; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/event/query/QueryCreatedEvent.java b/presto-main/src/main/java/com/facebook/presto/event/query/QueryCreatedEvent.java new file mode 100644 index 00000000..d9286501 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/event/query/QueryCreatedEvent.java @@ -0,0 +1,141 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.event.query; + +import com.facebook.presto.execution.QueryId; +import io.airlift.event.client.EventField; +import io.airlift.event.client.EventType; +import org.joda.time.DateTime; + +import javax.annotation.concurrent.Immutable; + +import java.net.URI; + +@Immutable +@EventType("QueryCreated") +public class QueryCreatedEvent +{ + private final QueryId queryId; + private final String user; + private final String source; + private final String serverVersion; + private final String environment; + private final String catalog; + private final String schema; + private final String remoteClientAddress; + private final String userAgent; + private final URI uri; + private final String query; + private final DateTime createTime; + + public QueryCreatedEvent( + QueryId queryId, + String user, + String source, + String serverVersion, + String environment, + String catalog, + String schema, + String remoteClientAddress, + String userAgent, + URI uri, + String query, + DateTime createTime) + { + this.queryId = queryId; + this.user = user; + this.source = source; + this.serverVersion = serverVersion; + this.environment = environment; + this.catalog = catalog; + this.schema = schema; + this.remoteClientAddress = remoteClientAddress; + this.userAgent = userAgent; + this.uri = uri; + this.query = query; + this.createTime = createTime; + } + + @EventField + public String getQueryId() + { + return queryId.toString(); + } + + @EventField + public String getUser() + { + return user; + } + + @EventField + public String getSource() + { + return source; + } + + @EventField + public String getServerVersion() + { + return serverVersion; + } + + @EventField + public String getEnvironment() + { + return environment; + } + + @EventField + public String getCatalog() + { + return catalog; + } + + @EventField + public String getSchema() + { + return schema; + } + + @EventField + public String getRemoteClientAddress() + { + return remoteClientAddress; + } + + @EventField + public String getUserAgent() + { + return userAgent; + } + + @EventField + public String getUri() + { + return uri.toString(); + } + + @EventField + public String getQuery() + { + return query; + } + + @EventField + public DateTime getCreateTime() + { + return createTime; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/event/query/QueryMonitor.java b/presto-main/src/main/java/com/facebook/presto/event/query/QueryMonitor.java new file mode 100644 index 00000000..f1660e79 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/event/query/QueryMonitor.java @@ -0,0 +1,271 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.event.query; + +import com.facebook.presto.client.FailureInfo; +import com.facebook.presto.execution.QueryInfo; +import com.facebook.presto.execution.QueryStats; +import com.facebook.presto.execution.StageInfo; +import com.facebook.presto.execution.TaskId; +import com.facebook.presto.execution.TaskInfo; +import com.facebook.presto.metadata.NodeVersion; +import com.facebook.presto.operator.DriverStats; +import com.facebook.presto.operator.TaskStats; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableMap; +import io.airlift.event.client.EventClient; +import io.airlift.log.Logger; +import io.airlift.node.NodeInfo; +import io.airlift.units.Duration; +import org.joda.time.DateTime; + +import javax.annotation.Nullable; +import javax.inject.Inject; + +import java.util.List; +import java.util.Map; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +public class QueryMonitor +{ + private static final Logger log = Logger.get(QueryMonitor.class); + + private final ObjectMapper objectMapper; + private final EventClient eventClient; + private final String environment; + private final String serverVersion; + + @Inject + public QueryMonitor(ObjectMapper objectMapper, EventClient eventClient, NodeInfo nodeInfo, NodeVersion nodeVersion) + { + this.objectMapper = checkNotNull(objectMapper, "objectMapper is null"); + this.eventClient = checkNotNull(eventClient, "eventClient is null"); + this.environment = checkNotNull(nodeInfo, "nodeInfo is null").getEnvironment(); + this.serverVersion = checkNotNull(nodeVersion, "nodeVersion is null").toString(); + } + + public void createdEvent(QueryInfo queryInfo) + { + eventClient.post( + new QueryCreatedEvent( + queryInfo.getQueryId(), + queryInfo.getSession().getUser(), + queryInfo.getSession().getSource(), + serverVersion, + environment, + queryInfo.getSession().getCatalog(), + queryInfo.getSession().getSchema(), + queryInfo.getSession().getRemoteUserAddress(), + queryInfo.getSession().getUserAgent(), + queryInfo.getSelf(), + queryInfo.getQuery(), + queryInfo.getQueryStats().getCreateTime() + ) + ); + } + + public void completionEvent(QueryInfo queryInfo) + { + try { + QueryStats queryStats = queryInfo.getQueryStats(); + FailureInfo failureInfo = queryInfo.getFailureInfo(); + + String failureType = failureInfo == null ? null : failureInfo.getType(); + String failureMessage = failureInfo == null ? null : failureInfo.getMessage(); + + ImmutableMap.Builder mergedProperties = ImmutableMap.builder(); + mergedProperties.putAll(queryInfo.getSession().getSystemProperties()); + for (Map.Entry> catalogEntry : queryInfo.getSession().getCatalogProperties().entrySet()) { + for (Map.Entry entry : catalogEntry.getValue().entrySet()) { + mergedProperties.put(catalogEntry.getKey() + "." + entry.getKey(), entry.getValue()); + } + } + + eventClient.post( + new QueryCompletionEvent( + queryInfo.getQueryId(), + queryInfo.getSession().getUser(), + queryInfo.getSession().getSource(), + serverVersion, + environment, + queryInfo.getSession().getCatalog(), + queryInfo.getSession().getSchema(), + queryInfo.getSession().getRemoteUserAddress(), + queryInfo.getSession().getUserAgent(), + queryInfo.getState(), + queryInfo.getSelf(), + queryInfo.getFieldNames(), + queryInfo.getQuery(), + queryStats.getCreateTime(), + queryStats.getExecutionStartTime(), + queryStats.getEndTime(), + queryStats.getQueuedTime(), + queryStats.getAnalysisTime(), + queryStats.getDistributedPlanningTime(), + queryStats.getTotalScheduledTime(), + queryStats.getTotalCpuTime(), + queryStats.getRawInputDataSize(), + queryStats.getRawInputPositions(), + queryStats.getTotalDrivers(), + queryInfo.getErrorCode(), + failureType, + failureMessage, + objectMapper.writeValueAsString(queryInfo.getOutputStage()), + objectMapper.writeValueAsString(queryInfo.getFailureInfo()), + objectMapper.writeValueAsString(queryInfo.getInputs()), + objectMapper.writeValueAsString(mergedProperties.build()) + ) + ); + + logQueryTimeline(queryInfo); + } + catch (JsonProcessingException e) { + throw Throwables.propagate(e); + } + } + + private void logQueryTimeline(QueryInfo queryInfo) + { + try { + QueryStats queryStats = queryInfo.getQueryStats(); + DateTime queryStartTime = queryStats.getCreateTime(); + DateTime queryEndTime = queryStats.getEndTime(); + + // query didn't finish cleanly + if (queryStartTime == null || queryEndTime == null) { + return; + } + + // planning duration -- start to end of planning + Duration planning = queryStats.getTotalPlanningTime(); + if (planning == null) { + planning = new Duration(0, MILLISECONDS); + } + + List stages = StageInfo.getAllStages(queryInfo.getOutputStage()); + // long lastSchedulingCompletion = 0; + long firstTaskStartTime = queryEndTime.getMillis(); + long lastTaskStartTime = queryStartTime.getMillis() + planning.toMillis(); + long lastTaskEndTime = queryStartTime.getMillis() + planning.toMillis(); + for (StageInfo stage : stages) { + // only consider leaf stages + if (!stage.getSubStages().isEmpty()) { + continue; + } + + for (TaskInfo taskInfo : stage.getTasks()) { + TaskStats taskStats = taskInfo.getStats(); + + DateTime firstStartTime = taskStats.getFirstStartTime(); + if (firstStartTime != null) { + firstTaskStartTime = Math.min(firstStartTime.getMillis(), firstTaskStartTime); + } + + DateTime lastStartTime = taskStats.getLastStartTime(); + if (lastStartTime != null) { + lastTaskStartTime = Math.max(lastStartTime.getMillis(), lastTaskStartTime); + } + + DateTime endTime = taskStats.getEndTime(); + if (endTime != null) { + lastTaskEndTime = Math.max(endTime.getMillis(), lastTaskEndTime); + } + } + } + + Duration elapsed = millis(queryEndTime.getMillis() - queryStartTime.getMillis()); + + Duration scheduling = millis(firstTaskStartTime - queryStartTime.getMillis() - planning.toMillis()); + + Duration running = millis(lastTaskEndTime - firstTaskStartTime); + + Duration finishing = millis(queryEndTime.getMillis() - lastTaskEndTime); + + log.info("TIMELINE: Query %s :: elapsed %s :: planning %s :: scheduling %s :: running %s :: finishing %s :: begin %s :: end %s", + queryInfo.getQueryId(), + elapsed, + planning, + scheduling, + running, + finishing, + queryStartTime, + queryEndTime + ); + } + catch (Exception e) { + log.error(e, "Error logging query timeline"); + } + } + + public void splitCompletionEvent(TaskId taskId, DriverStats driverStats) + { + splitCompletionEvent(taskId, driverStats, null, null); + } + + public void splitFailedEvent(TaskId taskId, DriverStats driverStats, Throwable cause) + { + splitCompletionEvent(taskId, driverStats, cause.getClass().getName(), cause.getMessage()); + } + + private void splitCompletionEvent(TaskId taskId, DriverStats driverStats, @Nullable String failureType, @Nullable String failureMessage) + { + Duration timeToStart = null; + if (driverStats.getStartTime() != null) { + timeToStart = millis(driverStats.getStartTime().getMillis() - driverStats.getCreateTime().getMillis()); + } + Duration timeToEnd = null; + if (driverStats.getEndTime() != null) { + timeToEnd = millis(driverStats.getEndTime().getMillis() - driverStats.getCreateTime().getMillis()); + } + + try { + eventClient.post( + new SplitCompletionEvent( + taskId.getQueryId(), + taskId.getStageId(), + taskId, + environment, + driverStats.getQueuedTime(), + driverStats.getStartTime(), + timeToStart, + timeToEnd, + driverStats.getRawInputDataSize(), + driverStats.getRawInputPositions(), + driverStats.getRawInputReadTime(), + driverStats.getElapsedTime(), + driverStats.getTotalCpuTime(), + driverStats.getTotalUserTime(), + failureType, + failureMessage, + objectMapper.writeValueAsString(driverStats) + ) + ); + } + catch (JsonProcessingException e) { + log.error(e, "Error posting split completion event for task %s", taskId); + } + } + + private static Duration millis(long millis) + { + if (millis < 0) { + millis = 0; + } + return new Duration(millis, MILLISECONDS); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/event/query/SplitCompletionEvent.java b/presto-main/src/main/java/com/facebook/presto/event/query/SplitCompletionEvent.java new file mode 100644 index 00000000..8bd5c268 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/event/query/SplitCompletionEvent.java @@ -0,0 +1,212 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.event.query; + +import com.facebook.presto.execution.QueryId; +import com.facebook.presto.execution.StageId; +import com.facebook.presto.execution.TaskId; +import com.google.common.base.Preconditions; +import io.airlift.event.client.EventField; +import io.airlift.event.client.EventType; +import io.airlift.units.DataSize; +import io.airlift.units.Duration; +import org.joda.time.DateTime; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +@Immutable +@EventType("SplitCompletion") +public class SplitCompletionEvent +{ + private final QueryId queryId; + private final StageId stageId; + private final TaskId taskId; + + private final String environment; + + private final Duration queuedTimeMs; + private final DateTime executionStartTime; + + private final Long timeToFirstByteMs; + private final Long timeToLastByteMs; + + private final DataSize completedDataSize; + private final long completedPositions; + private final Long completedReadTimeMs; + + private final Long wallTimeMs; + private final Long cpuTimeMs; + private final Long userTimeMs; + + private final String failureType; + private final String failureMessage; + + private final String splitInfoJson; + + public SplitCompletionEvent( + QueryId queryId, + StageId stageId, + TaskId taskId, + String environment, + Duration queuedTimeMs, + @Nullable DateTime executionStartTime, + @Nullable Duration timeToFirstByte, + @Nullable Duration timeToLastByte, + DataSize completedDataSize, + long completedPositions, + Duration completedReadTime, + @Nullable Duration wallTime, + @Nullable Duration cpuTime, + @Nullable Duration userTime, + @Nullable String failureType, + @Nullable String failureMessage, + String splitInfoJson) + { + Preconditions.checkNotNull(queryId, "queryId is null"); + Preconditions.checkNotNull(stageId, "stageId is null"); + Preconditions.checkNotNull(taskId, "taskId is null"); + Preconditions.checkNotNull(completedDataSize, "completedDataSize is null"); + Preconditions.checkNotNull(splitInfoJson, "splitInfoJson is null"); + + this.queryId = queryId; + this.stageId = stageId; + this.taskId = taskId; + this.environment = environment; + this.queuedTimeMs = queuedTimeMs; + this.executionStartTime = executionStartTime; + this.timeToFirstByteMs = durationToMillis(timeToFirstByte); + this.timeToLastByteMs = durationToMillis(timeToLastByte); + this.completedDataSize = completedDataSize; + this.completedPositions = completedPositions; + this.completedReadTimeMs = durationToMillis(completedReadTime); + this.wallTimeMs = durationToMillis(wallTime); + this.cpuTimeMs = durationToMillis(cpuTime); + this.userTimeMs = durationToMillis(userTime); + this.failureType = failureType; + this.failureMessage = failureMessage; + this.splitInfoJson = splitInfoJson; + } + + @Nullable + private static Long durationToMillis(@Nullable Duration duration) + { + if (duration == null) { + return null; + } + return duration.toMillis(); + } + + @EventField + public String getQueryId() + { + return queryId.toString(); + } + + @EventField + public String getStageId() + { + return stageId.toString(); + } + + @EventField + public String getTaskId() + { + return taskId.toString(); + } + + @EventField + public String getEnvironment() + { + return environment; + } + + @EventField + public long getQueuedTimeMs() + { + return queuedTimeMs.toMillis(); + } + + @EventField + public DateTime getExecutionStartTime() + { + return executionStartTime; + } + + @EventField + public Long getTimeToFirstByteMs() + { + return timeToFirstByteMs; + } + + @EventField + public Long getTimeToLastByteMs() + { + return timeToLastByteMs; + } + + @EventField + public long getCompletedDataSizeTotal() + { + return completedDataSize.toBytes(); + } + + @EventField + public long getCompletedPositionsTotal() + { + return completedPositions; + } + + @EventField + public Long getCompletedReadTimeMs() + { + return completedReadTimeMs; + } + + @EventField + public Long getWallTimeMs() + { + return wallTimeMs; + } + + @EventField + public Long getCpuTimeMs() + { + return cpuTimeMs; + } + + @EventField + public Long getUserTimeMs() + { + return userTimeMs; + } + + @EventField + public String getFailureType() + { + return failureType; + } + + @EventField + public String getFailureMessage() + { + return failureMessage; + } + + @EventField + public String getSplitInfoJson() + { + return splitInfoJson; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/AbandonedException.java b/presto-main/src/main/java/com/facebook/presto/execution/AbandonedException.java new file mode 100644 index 00000000..2aeec89b --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/AbandonedException.java @@ -0,0 +1,29 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.spi.PrestoException; +import org.joda.time.DateTime; + +import static com.facebook.presto.spi.StandardErrorCode.ABANDONED_QUERY; +import static java.lang.String.format; + +public class AbandonedException + extends PrestoException +{ + public AbandonedException(String name, DateTime lastHeartbeat, DateTime now) + { + super(ABANDONED_QUERY, format("%s has not been accessed since %s: currentTime %s", name, lastHeartbeat, now)); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/BufferInfo.java b/presto-main/src/main/java/com/facebook/presto/execution/BufferInfo.java new file mode 100644 index 00000000..9fdef378 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/BufferInfo.java @@ -0,0 +1,116 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class BufferInfo +{ + private final TaskId bufferId; + private final boolean finished; + private final int bufferedPages; + + private final long pagesSent; + private final PageBufferInfo pageBufferInfo; + + @JsonCreator + public BufferInfo( + @JsonProperty("bufferId") TaskId bufferId, + @JsonProperty("finished") boolean finished, + @JsonProperty("bufferedPages") int bufferedPages, + @JsonProperty("pagesSent") long pagesSent, + @JsonProperty("pageBufferInfo") PageBufferInfo pageBufferInfo) + { + checkArgument(bufferedPages >= 0, "bufferedPages must be >= 0"); + checkArgument(pagesSent >= 0, "pagesSent must be >= 0"); + + this.bufferId = checkNotNull(bufferId, "bufferId is null"); + this.pagesSent = pagesSent; + this.pageBufferInfo = checkNotNull(pageBufferInfo, "pageBufferInfo is null"); + this.finished = finished; + this.bufferedPages = bufferedPages; + } + + @JsonProperty + public TaskId getBufferId() + { + return bufferId; + } + + @JsonProperty + public boolean isFinished() + { + return finished; + } + + @JsonProperty + public int getBufferedPages() + { + return bufferedPages; + } + + @JsonProperty + public long getPagesSent() + { + return pagesSent; + } + + @JsonProperty + public PageBufferInfo getPageBufferInfo() + { + return pageBufferInfo; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + BufferInfo that = (BufferInfo) o; + return Objects.equals(finished, that.finished) && + Objects.equals(bufferedPages, that.bufferedPages) && + Objects.equals(pagesSent, that.pagesSent) && + Objects.equals(bufferId, that.bufferId) && + Objects.equals(pageBufferInfo, that.pageBufferInfo); + } + + @Override + public int hashCode() + { + return Objects.hash(bufferId, finished, bufferedPages, pagesSent, pageBufferInfo); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("bufferId", bufferId) + .add("finished", finished) + .add("bufferedPages", bufferedPages) + .add("pagesSent", pagesSent) + .add("pageBufferInfo", pageBufferInfo) + .toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/BufferResult.java b/presto-main/src/main/java/com/facebook/presto/execution/BufferResult.java new file mode 100644 index 00000000..77a1acff --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/BufferResult.java @@ -0,0 +1,107 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.spi.Page; +import com.google.common.collect.ImmutableList; + +import java.util.List; +import java.util.Objects; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkNotNull; + +public class BufferResult +{ + public static BufferResult emptyResults(long token, boolean bufferClosed) + { + return new BufferResult(token, token, bufferClosed, ImmutableList.of()); + } + + private final long token; + private final long nextToken; + private final boolean bufferClosed; + private final List pages; + + public BufferResult(long token, long nextToken, boolean bufferClosed, List pages) + { + this.token = token; + this.nextToken = nextToken; + this.bufferClosed = bufferClosed; + this.pages = ImmutableList.copyOf(checkNotNull(pages, "pages is null")); + } + + public long getToken() + { + return token; + } + + public long getNextToken() + { + return nextToken; + } + + public boolean isBufferClosed() + { + return bufferClosed; + } + + public List getPages() + { + return pages; + } + + public int size() + { + return pages.size(); + } + + public boolean isEmpty() + { + return pages.isEmpty(); + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + BufferResult that = (BufferResult) o; + return Objects.equals(token, that.token) && + Objects.equals(nextToken, that.nextToken) && + Objects.equals(bufferClosed, that.bufferClosed) && + Objects.equals(pages, that.pages); + } + + @Override + public int hashCode() + { + return Objects.hash(token, nextToken, bufferClosed, pages); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("token", token) + .add("nextToken", nextToken) + .add("bufferClosed", bufferClosed) + .add("pages", pages) + .toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/Column.java b/presto-main/src/main/java/com/facebook/presto/execution/Column.java new file mode 100644 index 00000000..34b5826b --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/Column.java @@ -0,0 +1,92 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; +import java.util.Optional; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkNotNull; + +public final class Column +{ + private final String name; + private final String type; + private final Optional domain; + + @JsonCreator + public Column( + @JsonProperty("name") String name, + @JsonProperty("type") String type, + @JsonProperty("domain") Optional domain) + { + this.name = checkNotNull(name, "name is null"); + this.type = checkNotNull(type, "type is null"); + this.domain = checkNotNull(domain, "domain is null"); + } + + @JsonProperty + public String getName() + { + return name; + } + + @JsonProperty + public String getType() + { + return type; + } + + @JsonProperty + public Optional getDomain() + { + return domain; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + Column that = (Column) o; + + return Objects.equals(this.name, that.name) && + Objects.equals(this.type, that.type) && + Objects.equals(this.domain, that.domain); + } + + @Override + public int hashCode() + { + return Objects.hash(name, type, domain); + } + + @Override + public String toString() + { + return toStringHelper(this) + .addValue(name) + .addValue(type) + .addValue(domain) + .toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/CreateTableTask.java b/presto-main/src/main/java/com/facebook/presto/execution/CreateTableTask.java new file mode 100644 index 00000000..c2abc03e --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/CreateTableTask.java @@ -0,0 +1,78 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.Session; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.metadata.QualifiedTableName; +import com.facebook.presto.metadata.TableHandle; +import com.facebook.presto.metadata.TableMetadata; +import com.facebook.presto.spi.ColumnMetadata; +import com.facebook.presto.spi.ConnectorTableMetadata; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.analyzer.SemanticException; +import com.facebook.presto.sql.tree.CreateTable; +import com.facebook.presto.sql.tree.TableElement; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static com.facebook.presto.metadata.MetadataUtil.createQualifiedTableName; +import static com.facebook.presto.spi.type.TypeSignature.parseTypeSignature; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.TABLE_ALREADY_EXISTS; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.TYPE_MISMATCH; +import static com.facebook.presto.type.UnknownType.UNKNOWN; +import static com.google.common.base.Preconditions.checkArgument; + +public class CreateTableTask + implements DataDefinitionTask +{ + @Override + public String getName() + { + return "CREATE TABLE"; + } + + @Override + public void execute(CreateTable statement, Session session, Metadata metadata, QueryStateMachine stateMachine) + { + checkArgument(!statement.getElements().isEmpty(), "no columns for table"); + + QualifiedTableName tableName = createQualifiedTableName(session, statement.getName()); + Optional tableHandle = metadata.getTableHandle(session, tableName); + if (tableHandle.isPresent()) { + if (!statement.isExists()) { + throw new SemanticException(TABLE_ALREADY_EXISTS, statement, "Table '%s' already exists", tableName); + } + return; + } + + List columns = new ArrayList<>(); + for (TableElement element : statement.getElements()) { + Type type = metadata.getType(parseTypeSignature(element.getType())); + if ((type == null) || type.equals(UNKNOWN)) { + throw new SemanticException(TYPE_MISMATCH, element, "Unknown type for column '%s' ", element.getName()); + } + String comment = element.getComment().isPresent() ? element.getComment().get() : null; + columns.add(new ColumnMetadata(element.getName(), type, element.isPartitionKey(), comment, false)); + } + + TableMetadata tableMetadata = new TableMetadata( + tableName.getCatalogName(), + new ConnectorTableMetadata(tableName.asSchemaTableName(), columns, session.getUser(), false)); + + metadata.createTable(session, tableName.getCatalogName(), tableMetadata); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/CreateViewTask.java b/presto-main/src/main/java/com/facebook/presto/execution/CreateViewTask.java new file mode 100644 index 00000000..4f526acd --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/CreateViewTask.java @@ -0,0 +1,115 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.Session; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.metadata.QualifiedTableName; +import com.facebook.presto.metadata.ViewDefinition; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.sql.analyzer.Analysis; +import com.facebook.presto.sql.analyzer.Analyzer; +import com.facebook.presto.sql.analyzer.FeaturesConfig; +import com.facebook.presto.sql.analyzer.QueryExplainer; +import com.facebook.presto.sql.parser.ParsingException; +import com.facebook.presto.sql.parser.SqlParser; +import com.facebook.presto.sql.planner.optimizations.PlanOptimizer; +import com.facebook.presto.sql.tree.CreateView; +import com.facebook.presto.sql.tree.Query; +import com.facebook.presto.sql.tree.Statement; +import com.google.common.collect.ImmutableList; +import io.airlift.json.JsonCodec; + +import javax.inject.Inject; + +import java.util.List; +import java.util.Optional; + +import static com.facebook.presto.metadata.MetadataUtil.createQualifiedTableName; +import static com.facebook.presto.metadata.ViewDefinition.ViewColumn; +import static com.facebook.presto.spi.StandardErrorCode.INTERNAL_ERROR; +import static com.facebook.presto.sql.SqlFormatter.formatSql; +import static com.facebook.presto.util.ImmutableCollectors.toImmutableList; +import static com.google.common.base.Preconditions.checkNotNull; + +public class CreateViewTask + implements DataDefinitionTask +{ + private final JsonCodec codec; + private final SqlParser sqlParser; + private final List planOptimizers; + private final boolean experimentalSyntaxEnabled; + + @Inject + public CreateViewTask(JsonCodec codec, SqlParser sqlParser, List planOptimizers, FeaturesConfig featuresConfig) + { + this.codec = checkNotNull(codec, "codec is null"); + this.sqlParser = checkNotNull(sqlParser, "sqlParser is null"); + this.planOptimizers = ImmutableList.copyOf(checkNotNull(planOptimizers, "planOptimizers is null")); + checkNotNull(featuresConfig, "featuresConfig is null"); + this.experimentalSyntaxEnabled = featuresConfig.isExperimentalSyntaxEnabled(); + } + + @Override + public String getName() + { + return "CREATE VIEW"; + } + + @Override + public void execute(CreateView statement, Session session, Metadata metadata, QueryStateMachine stateMachine) + { + QualifiedTableName name = createQualifiedTableName(session, statement.getName()); + + String sql = getFormattedSql(statement); + + Analysis analysis = analyzeStatement(statement, session, metadata); + + List columns = analysis.getOutputDescriptor() + .getVisibleFields().stream() + .map(field -> new ViewColumn(field.getName().get(), field.getType())) + .collect(toImmutableList()); + + String data = codec.toJson(new ViewDefinition(sql, session.getCatalog(), session.getSchema(), columns)); + + metadata.createView(session, name, data, statement.isReplace()); + } + + private Analysis analyzeStatement(Statement statement, Session session, Metadata metadata) + { + QueryExplainer explainer = new QueryExplainer(session, planOptimizers, metadata, sqlParser, experimentalSyntaxEnabled); + Analyzer analyzer = new Analyzer(session, metadata, sqlParser, Optional.of(explainer), experimentalSyntaxEnabled); + return analyzer.analyze(statement); + } + + private String getFormattedSql(CreateView statement) + { + Query query = statement.getQuery(); + String sql = formatSql(query); + + // verify round-trip + Statement parsed; + try { + parsed = sqlParser.createStatement(sql); + } + catch (ParsingException e) { + throw new PrestoException(INTERNAL_ERROR, "Formatted query does not parse: " + query); + } + if (!query.equals(parsed)) { + throw new PrestoException(INTERNAL_ERROR, "Query does not round-trip: " + query); + } + + return sql; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/DataDefinitionExecution.java b/presto-main/src/main/java/com/facebook/presto/execution/DataDefinitionExecution.java new file mode 100644 index 00000000..67ffc9c5 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/DataDefinitionExecution.java @@ -0,0 +1,208 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.Session; +import com.facebook.presto.execution.StateMachine.StateChangeListener; +import com.facebook.presto.memory.VersionedMemoryPoolId; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.metadata.MetadataManager; +import com.facebook.presto.sql.tree.Statement; +import com.google.common.base.Throwables; +import io.airlift.units.Duration; + +import javax.inject.Inject; + +import java.net.URI; +import java.util.Map; +import java.util.concurrent.ExecutorService; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class DataDefinitionExecution + implements QueryExecution +{ + private final DataDefinitionTask task; + private final T statement; + private final Session session; + private final Metadata metadata; + private final QueryStateMachine stateMachine; + + private DataDefinitionExecution( + DataDefinitionTask task, + T statement, + Session session, + Metadata metadata, + QueryStateMachine stateMachine) + { + this.task = checkNotNull(task, "task is null"); + this.statement = checkNotNull(statement, "statement is null"); + this.session = checkNotNull(session, "session is null"); + this.metadata = checkNotNull(metadata, "metadata is null"); + this.stateMachine = checkNotNull(stateMachine, "stateMachine is null"); + } + + @Override + public VersionedMemoryPoolId getMemoryPool() + { + return stateMachine.getMemoryPool(); + } + + @Override + public void setMemoryPool(VersionedMemoryPoolId poolId) + { + stateMachine.setMemoryPool(poolId); + } + + @Override + public long getTotalMemoryReservation() + { + return 0; + } + + @Override + public Session getSession() + { + return session; + } + + @Override + public void start() + { + try { + // transition to running + if (!stateMachine.transitionToRunning()) { + // query already running or finished + return; + } + + task.execute(statement, session, metadata, stateMachine); + + stateMachine.transitionToFinished(); + } + catch (Throwable e) { + fail(e); + if (!(e instanceof RuntimeException)) { + throw Throwables.propagate(e); + } + } + } + + @Override + public Duration waitForStateChange(QueryState currentState, Duration maxWait) + throws InterruptedException + { + return stateMachine.waitForStateChange(currentState, maxWait); + } + + @Override + public void addStateChangeListener(StateChangeListener stateChangeListener) + { + stateMachine.addStateChangeListener(stateChangeListener); + } + + @Override + public void fail(Throwable cause) + { + stateMachine.transitionToFailed(cause); + } + + @Override + public void cancelStage(StageId stageId) + { + // no-op + } + + @Override + public void recordHeartbeat() + { + stateMachine.recordHeartbeat(); + } + + @Override + public void pruneInfo() + { + // no-op + } + + @Override + public QueryId getQueryId() + { + return stateMachine.getQueryId(); + } + + @Override + public QueryInfo getQueryInfo() + { + return stateMachine.getQueryInfoWithoutDetails(); + } + + @Override + public QueryState getState() + { + return stateMachine.getQueryState(); + } + + public static class DataDefinitionExecutionFactory + implements QueryExecutionFactory> + { + private final LocationFactory locationFactory; + private final Metadata metadata; + private final ExecutorService executor; + private final Map, DataDefinitionTask> tasks; + + @Inject + public DataDefinitionExecutionFactory( + LocationFactory locationFactory, + MetadataManager metadata, + @ForQueryExecution ExecutorService executor, + Map, DataDefinitionTask> tasks) + { + this.locationFactory = checkNotNull(locationFactory, "locationFactory is null"); + this.metadata = checkNotNull(metadata, "metadata is null"); + this.executor = checkNotNull(executor, "executor is null"); + this.tasks = checkNotNull(tasks, "tasks is null"); + } + + @Override + public DataDefinitionExecution createQueryExecution( + QueryId queryId, + String query, + Session session, + Statement statement) + { + URI self = locationFactory.createQueryLocation(queryId); + QueryStateMachine stateMachine = new QueryStateMachine(queryId, query, session, self, executor); + return createExecution(statement, session, stateMachine); + } + + private DataDefinitionExecution createExecution( + T statement, + Session session, + QueryStateMachine stateMachine) + { + DataDefinitionTask task = getTask(statement); + checkArgument(task != null, "no task for statement: %s", statement.getClass().getSimpleName()); + stateMachine.setUpdateType(task.getName()); + return new DataDefinitionExecution<>(task, statement, session, metadata, stateMachine); + } + + @SuppressWarnings("unchecked") + private DataDefinitionTask getTask(T statement) + { + return (DataDefinitionTask) tasks.get(statement.getClass()); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/DataDefinitionTask.java b/presto-main/src/main/java/com/facebook/presto/execution/DataDefinitionTask.java new file mode 100644 index 00000000..cca3c049 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/DataDefinitionTask.java @@ -0,0 +1,25 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.Session; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.sql.tree.Statement; + +public interface DataDefinitionTask +{ + String getName(); + + void execute(T statement, Session session, Metadata metadata, QueryStateMachine stateMachine); +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/DropTableTask.java b/presto-main/src/main/java/com/facebook/presto/execution/DropTableTask.java new file mode 100644 index 00000000..a42f578a --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/DropTableTask.java @@ -0,0 +1,52 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.Session; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.metadata.QualifiedTableName; +import com.facebook.presto.metadata.TableHandle; +import com.facebook.presto.sql.analyzer.SemanticException; +import com.facebook.presto.sql.tree.DropTable; + +import java.util.Optional; + +import static com.facebook.presto.metadata.MetadataUtil.createQualifiedTableName; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.MISSING_TABLE; + +public class DropTableTask + implements DataDefinitionTask +{ + @Override + public String getName() + { + return "DROP TABLE"; + } + + @Override + public void execute(DropTable statement, Session session, Metadata metadata, QueryStateMachine stateMachine) + { + QualifiedTableName tableName = createQualifiedTableName(session, statement.getTableName()); + + Optional tableHandle = metadata.getTableHandle(session, tableName); + if (!tableHandle.isPresent()) { + if (!statement.isExists()) { + throw new SemanticException(MISSING_TABLE, statement, "Table '%s' does not exist", tableName); + } + return; + } + + metadata.dropTable(tableHandle.get()); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/DropViewTask.java b/presto-main/src/main/java/com/facebook/presto/execution/DropViewTask.java new file mode 100644 index 00000000..9b5d4eba --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/DropViewTask.java @@ -0,0 +1,52 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.Session; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.metadata.QualifiedTableName; +import com.facebook.presto.metadata.ViewDefinition; +import com.facebook.presto.sql.analyzer.SemanticException; +import com.facebook.presto.sql.tree.DropView; + +import java.util.Optional; + +import static com.facebook.presto.metadata.MetadataUtil.createQualifiedTableName; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.MISSING_TABLE; + +public class DropViewTask + implements DataDefinitionTask +{ + @Override + public String getName() + { + return "DROP VIEW"; + } + + @Override + public void execute(DropView statement, Session session, Metadata metadata, QueryStateMachine stateMachine) + { + QualifiedTableName name = createQualifiedTableName(session, statement.getName()); + + Optional view = metadata.getView(session, name); + if (!view.isPresent()) { + if (!statement.isExists()) { + throw new SemanticException(MISSING_TABLE, statement, "View '%s' does not exist", name); + } + return; + } + + metadata.dropView(session, name); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/ExecutionFailureInfo.java b/presto-main/src/main/java/com/facebook/presto/execution/ExecutionFailureInfo.java new file mode 100644 index 00000000..77c52bcd --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/ExecutionFailureInfo.java @@ -0,0 +1,170 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.client.ErrorLocation; +import com.facebook.presto.client.FailureInfo; +import com.facebook.presto.spi.ErrorCode; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; +import javax.validation.constraints.NotNull; + +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static com.facebook.presto.util.ImmutableCollectors.toImmutableList; + +@Immutable +public class ExecutionFailureInfo +{ + private static final Pattern STACK_TRACE_PATTERN = Pattern.compile("(.*)\\.(.*)\\(([^:]*)(?::(.*))?\\)"); + + private final String type; + private final String message; + private final ExecutionFailureInfo cause; + private final List suppressed; + private final List stack; + private final ErrorLocation errorLocation; + private final ErrorCode errorCode; + + @JsonCreator + public ExecutionFailureInfo( + @JsonProperty("type") String type, + @JsonProperty("message") String message, + @JsonProperty("cause") ExecutionFailureInfo cause, + @JsonProperty("suppressed") List suppressed, + @JsonProperty("stack") List stack, + @JsonProperty("errorLocation") @Nullable ErrorLocation errorLocation, + @JsonProperty("errorCode") @Nullable ErrorCode errorCode) + { + Preconditions.checkNotNull(type, "type is null"); + Preconditions.checkNotNull(suppressed, "suppressed is null"); + Preconditions.checkNotNull(stack, "stack is null"); + + this.type = type; + this.message = message; + this.cause = cause; + this.suppressed = ImmutableList.copyOf(suppressed); + this.stack = ImmutableList.copyOf(stack); + this.errorLocation = errorLocation; + this.errorCode = errorCode; + } + + @NotNull + @JsonProperty + public String getType() + { + return type; + } + + @Nullable + @JsonProperty + public String getMessage() + { + return message; + } + + @Nullable + @JsonProperty + public ExecutionFailureInfo getCause() + { + return cause; + } + + @NotNull + @JsonProperty + public List getSuppressed() + { + return suppressed; + } + + @NotNull + @JsonProperty + public List getStack() + { + return stack; + } + + @Nullable + @JsonProperty + public ErrorLocation getErrorLocation() + { + return errorLocation; + } + + @Nullable + @JsonProperty + public ErrorCode getErrorCode() + { + return errorCode; + } + + public FailureInfo toFailureInfo() + { + List suppressed = this.suppressed.stream() + .map(ExecutionFailureInfo::toFailureInfo) + .collect(toImmutableList()); + + return new FailureInfo(type, message, cause == null ? null : cause.toFailureInfo(), suppressed, stack, errorLocation); + } + + public RuntimeException toException() + { + return toException(this); + } + + private static Failure toException(ExecutionFailureInfo executionFailureInfo) + { + if (executionFailureInfo == null) { + return null; + } + Failure failure = new Failure(executionFailureInfo.getType(), executionFailureInfo.getMessage(), executionFailureInfo.getErrorCode(), toException(executionFailureInfo.getCause())); + for (ExecutionFailureInfo suppressed : executionFailureInfo.getSuppressed()) { + failure.addSuppressed(toException(suppressed)); + } + ImmutableList.Builder stackTraceBuilder = ImmutableList.builder(); + for (String stack : executionFailureInfo.getStack()) { + stackTraceBuilder.add(toStackTraceElement(stack)); + } + ImmutableList stackTrace = stackTraceBuilder.build(); + failure.setStackTrace(stackTrace.toArray(new StackTraceElement[stackTrace.size()])); + return failure; + } + + public static StackTraceElement toStackTraceElement(String stack) + { + Matcher matcher = STACK_TRACE_PATTERN.matcher(stack); + if (matcher.matches()) { + String declaringClass = matcher.group(1); + String methodName = matcher.group(2); + String fileName = matcher.group(3); + int number = -1; + if (fileName.equals("Native Method")) { + fileName = null; + number = -2; + } + else if (matcher.group(4) != null) { + number = Integer.parseInt(matcher.group(4)); + } + return new StackTraceElement(declaringClass, methodName, fileName, number); + } + return new StackTraceElement("Unknown", stack, null, -1); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/FailedQueryException.java b/presto-main/src/main/java/com/facebook/presto/execution/FailedQueryException.java new file mode 100644 index 00000000..0927e233 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/FailedQueryException.java @@ -0,0 +1,35 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; + +public class FailedQueryException + extends RuntimeException +{ + public FailedQueryException(Throwable... causes) + { + this(ImmutableList.copyOf(causes)); + } + + public FailedQueryException(Iterable causes) + { + Preconditions.checkNotNull(causes, "causes is null"); + for (Throwable cause : causes) { + Preconditions.checkNotNull(cause, "cause is null"); + addSuppressed(cause); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/FailedQueryExecution.java b/presto-main/src/main/java/com/facebook/presto/execution/FailedQueryExecution.java new file mode 100644 index 00000000..4221594a --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/FailedQueryExecution.java @@ -0,0 +1,127 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.Session; +import com.facebook.presto.execution.StateMachine.StateChangeListener; +import com.facebook.presto.memory.VersionedMemoryPoolId; +import io.airlift.units.Duration; + +import java.net.URI; +import java.util.concurrent.Executor; + +import static com.facebook.presto.memory.LocalMemoryManager.GENERAL_POOL; +import static java.util.Objects.requireNonNull; + +public class FailedQueryExecution + implements QueryExecution +{ + private final QueryInfo queryInfo; + private final Session session; + + public FailedQueryExecution(QueryId queryId, String query, Session session, URI self, Executor executor, Throwable cause) + { + requireNonNull(cause, "cause is null"); + this.session = requireNonNull(session, "session is null"); + QueryStateMachine queryStateMachine = new QueryStateMachine(queryId, query, session, self, executor); + queryStateMachine.transitionToFailed(cause); + + queryInfo = queryStateMachine.getQueryInfo(null); + } + + @Override + public QueryId getQueryId() + { + return queryInfo.getQueryId(); + } + + @Override + public QueryInfo getQueryInfo() + { + return queryInfo; + } + + @Override + public QueryState getState() + { + return queryInfo.getState(); + } + + @Override + public VersionedMemoryPoolId getMemoryPool() + { + return new VersionedMemoryPoolId(GENERAL_POOL, 0); + } + + @Override + public void setMemoryPool(VersionedMemoryPoolId poolId) + { + // no-op + } + + @Override + public long getTotalMemoryReservation() + { + return 0; + } + + @Override + public Session getSession() + { + return session; + } + + @Override + public void start() + { + // no-op + } + + @Override + public Duration waitForStateChange(QueryState currentState, Duration maxWait) + throws InterruptedException + { + return maxWait; + } + + @Override + public void addStateChangeListener(StateChangeListener stateChangeListener) + { + stateChangeListener.stateChanged(QueryState.FAILED); + } + + @Override + public void fail(Throwable cause) + { + // no-op + } + + @Override + public void cancelStage(StageId stageId) + { + // no-op + } + + @Override + public void recordHeartbeat() + { + // no-op + } + + @Override + public void pruneInfo() + { + // no-op + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/Failure.java b/presto-main/src/main/java/com/facebook/presto/execution/Failure.java new file mode 100644 index 00000000..128c8d20 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/Failure.java @@ -0,0 +1,54 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.spi.ErrorCode; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class Failure + extends RuntimeException +{ + private final String type; + private final ErrorCode errorCode; + + Failure(String type, String message, @Nullable ErrorCode errorCode, Failure cause) + { + super(message, cause, true, true); + this.type = checkNotNull(type, "type is null"); + this.errorCode = errorCode; + } + + public String getType() + { + return type; + } + + public ErrorCode getErrorCode() + { + return errorCode; + } + + @Override + public String toString() + { + String message = getMessage(); + if (message != null) { + return type + ": " + message; + } + return type; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/ForQueryExecution.java b/presto-main/src/main/java/com/facebook/presto/execution/ForQueryExecution.java new file mode 100644 index 00000000..3dc08c52 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/ForQueryExecution.java @@ -0,0 +1,31 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import javax.inject.Qualifier; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention(RUNTIME) +@Target({FIELD, PARAMETER, METHOD}) +@Qualifier +public @interface ForQueryExecution +{ +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/Input.java b/presto-main/src/main/java/com/facebook/presto/execution/Input.java new file mode 100644 index 00000000..5ac7d611 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/Input.java @@ -0,0 +1,112 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; + +import javax.annotation.concurrent.Immutable; + +import java.util.List; +import java.util.Objects; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkNotNull; + +@Immutable +public final class Input +{ + private final String connectorId; + private final String schema; + private final String table; + private final List columns; + + @JsonCreator + public Input( + @JsonProperty("connectorId") String connectorId, + @JsonProperty("schema") String schema, + @JsonProperty("table") String table, + @JsonProperty("columns") List columns) + { + checkNotNull(connectorId, "connectorId is null"); + checkNotNull(schema, "schema is null"); + checkNotNull(table, "table is null"); + checkNotNull(columns, "columns is null"); + + this.connectorId = connectorId; + this.schema = schema; + this.table = table; + this.columns = ImmutableList.copyOf(columns); + } + + @JsonProperty + public String getConnectorId() + { + return connectorId; + } + + @JsonProperty + public String getSchema() + { + return schema; + } + + @JsonProperty + public String getTable() + { + return table; + } + + @JsonProperty + public List getColumns() + { + return columns; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + Input that = (Input) o; + + return Objects.equals(this.connectorId, that.connectorId) && + Objects.equals(this.schema, that.schema) && + Objects.equals(this.table, that.table) && + Objects.equals(this.columns, that.columns); + } + + @Override + public int hashCode() + { + return Objects.hash(connectorId, schema, table, columns); + } + + @Override + public String toString() + { + return toStringHelper(this) + .addValue(connectorId) + .addValue(schema) + .addValue(table) + .addValue(columns) + .toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/LocationFactory.java b/presto-main/src/main/java/com/facebook/presto/execution/LocationFactory.java new file mode 100644 index 00000000..8f8e13c2 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/LocationFactory.java @@ -0,0 +1,31 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.spi.Node; + +import java.net.URI; + +public interface LocationFactory +{ + URI createQueryLocation(QueryId queryId); + + URI createStageLocation(StageId stageId); + + URI createLocalTaskLocation(TaskId taskId); + + URI createTaskLocation(Node node, TaskId taskId); + + URI createMemoryInfoLocation(Node node); +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/NoSuchBufferException.java b/presto-main/src/main/java/com/facebook/presto/execution/NoSuchBufferException.java new file mode 100644 index 00000000..cf78368f --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/NoSuchBufferException.java @@ -0,0 +1,23 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +public class NoSuchBufferException + extends RuntimeException +{ + public NoSuchBufferException(String bufferName, Iterable availableBuffers) + { + super(String.format("Unknown output %s: available outputs %s", bufferName, availableBuffers)); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/NodeScheduler.java b/presto-main/src/main/java/com/facebook/presto/execution/NodeScheduler.java new file mode 100644 index 00000000..f2bed5fb --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/NodeScheduler.java @@ -0,0 +1,519 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.metadata.Split; +import com.facebook.presto.spi.HostAddress; +import com.facebook.presto.spi.Node; +import com.facebook.presto.spi.NodeManager; +import com.facebook.presto.sql.planner.PlanFragment; +import com.google.common.base.Splitter; +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSetMultimap; +import com.google.common.collect.Multimap; +import com.google.common.collect.SetMultimap; +import com.google.common.net.InetAddresses; +import org.weakref.jmx.Managed; + +import javax.inject.Inject; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +import static com.facebook.presto.spi.StandardErrorCode.NO_NODES_AVAILABLE; +import static com.facebook.presto.util.Failures.checkCondition; +import static com.facebook.presto.util.ImmutableCollectors.toImmutableList; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class NodeScheduler +{ + private final String coordinatorNodeId; + private final NodeManager nodeManager; + private final AtomicLong scheduleLocal = new AtomicLong(); + private final AtomicLong scheduleRack = new AtomicLong(); + private final AtomicLong scheduleRandom = new AtomicLong(); + private final int minCandidates; + private final boolean locationAwareScheduling; + private final boolean includeCoordinator; + private final int maxSplitsPerNode; + private final int maxSplitsPerNodePerTaskWhenFull; + private final NodeTaskMap nodeTaskMap; + private final boolean doubleScheduling; + + private final String reportNodes; + private final List reportNodeList; + private final boolean schedulerFixToReport; + private final boolean schedulerSourceToReport; + + @Inject + public NodeScheduler(NodeManager nodeManager, NodeSchedulerConfig config, NodeTaskMap nodeTaskMap) + { + this.nodeManager = nodeManager; + this.coordinatorNodeId = nodeManager.getCurrentNode().getNodeIdentifier(); + this.minCandidates = config.getMinCandidates(); + this.locationAwareScheduling = config.isLocationAwareSchedulingEnabled(); + this.includeCoordinator = config.isIncludeCoordinator(); + this.doubleScheduling = config.isMultipleTasksPerNodeEnabled(); + this.maxSplitsPerNode = config.getMaxSplitsPerNode(); + this.maxSplitsPerNodePerTaskWhenFull = config.getMaxPendingSplitsPerNodePerTask(); + this.nodeTaskMap = checkNotNull(nodeTaskMap, "nodeTaskMap is null"); + checkArgument(maxSplitsPerNode > maxSplitsPerNodePerTaskWhenFull, "maxSplitsPerNode must be > maxSplitsPerNodePerTaskWhenFull"); + + this.reportNodes = config.getReportNodes(); + this.schedulerFixToReport = config.getSchedulerFixToReport(); + this.schedulerSourceToReport = config.getSchedulerSourceToReport(); + this.reportNodeList = Splitter.on(",").splitToList(reportNodes); + } + + @Managed + public long getScheduleLocal() + { + return scheduleLocal.get(); + } + + @Managed + public long getScheduleRack() + { + return scheduleRack.get(); + } + + @Managed + public long getScheduleRandom() + { + return scheduleRandom.get(); + } + + @Managed + public void reset() + { + scheduleLocal.set(0); + scheduleRack.set(0); + scheduleRandom.set(0); + } + + public NodeSelector createNodeSelector(String dataSourceName) + { + // this supplier is thread-safe. TODO: this logic should probably move to the scheduler since the choice of which node to run in should be + // done as close to when the the split is about to be scheduled + Supplier nodeMap = Suppliers.memoizeWithExpiration(() -> { + ImmutableSetMultimap.Builder byHostAndPort = ImmutableSetMultimap.builder(); + ImmutableSetMultimap.Builder byHost = ImmutableSetMultimap.builder(); + ImmutableSetMultimap.Builder byRack = ImmutableSetMultimap.builder(); + + Set nodes; + if (dataSourceName != null) { + nodes = nodeManager.getActiveDatasourceNodes(dataSourceName); + } + else { + nodes = nodeManager.getActiveNodes(); + } + + for (Node node : nodes) { + try { + byHostAndPort.put(node.getHostAndPort(), node); + + InetAddress host = InetAddress.getByName(node.getHttpUri().getHost()); + byHost.put(host, node); + + byRack.put(Rack.of(host), node); + } + catch (UnknownHostException e) { + // ignore + } + } + + return new NodeMap(byHostAndPort.build(), byHost.build(), byRack.build()); + }, 5, TimeUnit.SECONDS); + + return new NodeSelector(nodeMap); + } + + public class NodeSelector + { + private final AtomicReference> nodeMap; + + public NodeSelector(Supplier nodeMap) + { + this.nodeMap = new AtomicReference<>(nodeMap); + } + + public void lockDownNodes() + { + nodeMap.set(Suppliers.ofInstance(nodeMap.get().get())); + } + + public List allNodes() + { + return ImmutableList.copyOf(nodeMap.get().get().getNodesByHostAndPort().values()); + } + + public Node selectCurrentNode() + { + // TODO: this is a hack to force scheduling on the coordinator + return nodeManager.getCurrentNode(); + } + + public List selectRandomNodes(int limit, final PlanFragment.PlanDistribution pd) + { + return selectNodes(limit, randomizedNodes(), pd); + } + + private List selectNodes(int limit, Iterator candidates, final PlanFragment.PlanDistribution pd) + { + checkArgument(limit > 0, "limit must be at least 1"); + + List selected = new ArrayList<>(limit); + while (selected.size() < limit && candidates.hasNext()) { + Node node = candidates.next(); + if (scheduleReportNodeIfNecessary(node, pd)) { + continue; + } + selected.add(node); + } + + if (doubleScheduling && !selected.isEmpty()) { + // Cycle the nodes until we reach the limit + int uniqueNodes = selected.size(); + int i = 0; + while (selected.size() < limit) { + if (i >= uniqueNodes) { + i = 0; + } + selected.add(selected.get(i)); + i++; + } + } + return selected; + } + + public List selectReportNode(int limit) + { + if (NodeSchedulerConfig.REPORT_NODE_DEFAULT.equals(reportNodes)) { + return selectRandomNodes(limit, PlanFragment.PlanDistribution.SINGLE); + } + else { + checkArgument(limit > 0, "limit must be at least 1"); + ImmutableList nodes = nodeMap.get().get().getNodesByHostAndPort().values().stream() + .filter(node -> reportNodeList.contains(node.getHostAndPort().getHostText())) + .collect(toImmutableList()); + return selectNodes(limit, new ResettableRandomizedIterator<>(nodes), PlanFragment.PlanDistribution.SINGLE); + } + } + + /** + * check whether should schedule the task to report node + */ + public boolean scheduleReportNodeIfNecessary(Node node, final PlanFragment.PlanDistribution pd) + { + boolean skipFlag = false; + if (NodeSchedulerConfig.REPORT_NODE_DEFAULT.equals(reportNodes)) { + return skipFlag; + } + else { + if (reportNodeList != null && reportNodeList.contains(node.getHostAndPort().getHostText())) { + if ((!schedulerFixToReport && PlanFragment.PlanDistribution.FIXED == pd) + || (!schedulerSourceToReport && PlanFragment.PlanDistribution.SOURCE == pd)) { + skipFlag = true; + } + } + return skipFlag; + } + } + + /** + * Identifies the nodes for running the specified splits. + * + * @param splits the splits that need to be assigned to nodes + * @return a multimap from node to splits only for splits for which we could identify a node to schedule on. + * If we cannot find an assignment for a split, it is not included in the map. + */ + public Multimap computeAssignments(Set splits, Iterable existingTasks, + final PlanFragment.PlanDistribution pd, boolean controlScanConcurrencyEnabled, int scanConcurrencyCount) + { + Multimap assignment = HashMultimap.create(); + Map assignmentCount = new HashMap<>(); + // pre-populate the assignment counts with zeros. This makes getOrDefault() faster + for (Node node : nodeMap.get().get().getNodesByHostAndPort().values()) { + assignmentCount.put(node, 0); + } + + // maintain a temporary local cache of partitioned splits on the node + Map splitCountByNode = new HashMap<>(); + + Map queuedSplitCountByNode = new HashMap<>(); + int allSplits = 0; + for (RemoteTask task : existingTasks) { + String nodeId = task.getNodeId(); + queuedSplitCountByNode.put(nodeId, queuedSplitCountByNode.getOrDefault(nodeId, 0) + task.getQueuedPartitionedSplitCount()); + allSplits += task.getPartitionedSplitCount(); + } + + ResettableRandomizedIterator randomCandidates = null; + if (!locationAwareScheduling) { + randomCandidates = randomizedNodes(); + } + + for (Split split : splits) { + List candidateNodes; + if (locationAwareScheduling || !split.isRemotelyAccessible()) { + candidateNodes = selectCandidateNodes(nodeMap.get().get(), split, pd); + } + else { + randomCandidates.reset(); + candidateNodes = selectNodes(minCandidates, randomCandidates, pd); + } + checkCondition(!candidateNodes.isEmpty(), NO_NODES_AVAILABLE, "No nodes available to run query"); + + // compute and cache number of splits currently assigned to each node + // NOTE: This does not use the Stream API for performance reasons. + for (Node node : candidateNodes) { + if (!splitCountByNode.containsKey(node)) { + splitCountByNode.put(node, nodeTaskMap.getPartitionedSplitsOnNode(node)); + } + } + + Node chosenNode = null; + int min = Integer.MAX_VALUE; + + for (Node node : candidateNodes) { + int totalSplitCount = assignmentCount.getOrDefault(node, 0) + splitCountByNode.get(node); + + if (totalSplitCount < min && totalSplitCount < maxSplitsPerNode) { + chosenNode = node; + min = totalSplitCount; + } + } + if (chosenNode == null) { + for (Node node : candidateNodes) { + int assignedSplitCount = assignmentCount.getOrDefault(node, 0); + int queuedSplitCount = queuedSplitCountByNode.getOrDefault(node.getNodeIdentifier(), 0); + int totalSplitCount = queuedSplitCount + assignedSplitCount; + if (totalSplitCount < min && totalSplitCount < maxSplitsPerNodePerTaskWhenFull) { + chosenNode = node; + min = totalSplitCount; + } + } + } + if (chosenNode != null) { + allSplits += 1; + if (controlScanConcurrencyEnabled && scanConcurrencyCount < allSplits) { + break; + } + assignment.put(chosenNode, split); + assignmentCount.put(chosenNode, assignmentCount.getOrDefault(chosenNode, 0) + 1); + } + } + return assignment; + } + + private ResettableRandomizedIterator randomizedNodes() + { + ImmutableList nodes = nodeMap.get().get().getNodesByHostAndPort().values().stream() + .filter(node -> includeCoordinator || !coordinatorNodeId.equals(node.getNodeIdentifier())) + .collect(toImmutableList()); + return new ResettableRandomizedIterator<>(nodes); + } + + private List selectCandidateNodes(NodeMap nodeMap, Split split, final PlanFragment.PlanDistribution pd) + { + Set chosen = new LinkedHashSet<>(minCandidates); + String coordinatorIdentifier = nodeManager.getCurrentNode().getNodeIdentifier(); + + // first look for nodes that match the hint + for (HostAddress hint : split.getAddresses()) { + nodeMap.getNodesByHostAndPort().get(hint).stream() + .filter(node -> includeCoordinator || !coordinatorIdentifier.equals(node.getNodeIdentifier())) + .filter(node -> !scheduleReportNodeIfNecessary(node, pd)) + .filter(chosen::add) + .forEach(node -> scheduleLocal.incrementAndGet()); + + InetAddress address; + try { + address = hint.toInetAddress(); + } + catch (UnknownHostException e) { + // skip addresses that don't resolve + continue; + } + + // consider a split with a host hint without a port as being accessible + // by all nodes in that host + if (!hint.hasPort() || split.isRemotelyAccessible()) { + nodeMap.getNodesByHost().get(address).stream() + .filter(node -> includeCoordinator || !coordinatorIdentifier.equals(node.getNodeIdentifier())) + .filter(node -> !scheduleReportNodeIfNecessary(node, pd)) + .filter(chosen::add) + .forEach(node -> scheduleLocal.incrementAndGet()); + } + } + + // add nodes in same rack, if below the minimum count + if (split.isRemotelyAccessible() && chosen.size() < minCandidates) { + for (HostAddress hint : split.getAddresses()) { + InetAddress address; + try { + address = hint.toInetAddress(); + } + catch (UnknownHostException e) { + // skip addresses that don't resolve + continue; + } + for (Node node : nodeMap.getNodesByRack().get(Rack.of(address))) { + if (includeCoordinator || !coordinatorIdentifier.equals(node.getNodeIdentifier())) { + if (scheduleReportNodeIfNecessary(node, pd)) { + continue; + } + if (chosen.add(node)) { + scheduleRack.incrementAndGet(); + } + if (chosen.size() == minCandidates) { + break; + } + } + } + if (chosen.size() == minCandidates) { + break; + } + } + } + + // add some random nodes if below the minimum count + if (split.isRemotelyAccessible()) { + if (chosen.size() < minCandidates) { + ResettableRandomizedIterator randomizedIterator = randomizedNodes(); + while (randomizedIterator.hasNext()) { + Node node = randomizedIterator.next(); + if (scheduleReportNodeIfNecessary(node, pd)) { + continue; + } + if (chosen.add(node)) { + scheduleRandom.incrementAndGet(); + } + + if (chosen.size() == minCandidates) { + break; + } + } + } + } + + // if the chosen set is empty and the hint includes the coordinator, + // force pick the coordinator + if (chosen.isEmpty() && !includeCoordinator) { + HostAddress coordinatorHostAddress = nodeManager.getCurrentNode().getHostAndPort(); + if (split.getAddresses().stream().anyMatch(host -> canSplitRunOnHost(split, coordinatorHostAddress, host))) { + chosen.add(nodeManager.getCurrentNode()); + } + } + + return ImmutableList.copyOf(chosen); + } + + private boolean canSplitRunOnHost(Split split, HostAddress coordinatorHost, HostAddress host) + { + // Exact match of the coordinator + if (host.equals(coordinatorHost)) { + return true; + } + // If the split is remotely accessible or the split location doesn't specify a port, + // we can ignore the coordinator's port and match just the ip address + return (!host.hasPort() || split.isRemotelyAccessible()) && + host.getHostText().equals(coordinatorHost.getHostText()); + } + } + + private static class NodeMap + { + private final SetMultimap nodesByHostAndPort; + private final SetMultimap nodesByHost; + private final SetMultimap nodesByRack; + + public NodeMap(SetMultimap nodesByHostAndPort, SetMultimap nodesByHost, SetMultimap nodesByRack) + { + this.nodesByHostAndPort = nodesByHostAndPort; + this.nodesByHost = nodesByHost; + this.nodesByRack = nodesByRack; + } + + private SetMultimap getNodesByHostAndPort() + { + return nodesByHostAndPort; + } + + public SetMultimap getNodesByHost() + { + return nodesByHost; + } + + public SetMultimap getNodesByRack() + { + return nodesByRack; + } + } + + private static class Rack + { + private final int id; + + public static Rack of(InetAddress address) + { + // TODO: we need a plugin for this + int id = InetAddresses.coerceToInteger(address) & 0xFF_FF_FF_00; + return new Rack(id); + } + + private Rack(int id) + { + this.id = id; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + Rack rack = (Rack) o; + + if (id != rack.id) { + return false; + } + + return true; + } + + @Override + public int hashCode() + { + return id; + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/NodeSchedulerConfig.java b/presto-main/src/main/java/com/facebook/presto/execution/NodeSchedulerConfig.java new file mode 100644 index 00000000..723ed7af --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/NodeSchedulerConfig.java @@ -0,0 +1,144 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import io.airlift.configuration.Config; +import io.airlift.configuration.ConfigDescription; + +import javax.validation.constraints.Min; + +public class NodeSchedulerConfig +{ + private int minCandidates = 10; + private boolean locationAwareScheduling = true; + private boolean includeCoordinator = true; + private boolean multipleTasksPerNode; + private int maxSplitsPerNode = 100; + private int maxPendingSplitsPerNodePerTask = 10; + + public static final String REPORT_NODE_DEFAULT = "NA"; + private String reportNodes = REPORT_NODE_DEFAULT; + private boolean schedulerFixToReport = true; + private boolean schedulerSourceToReport = true; + + public boolean isMultipleTasksPerNodeEnabled() + { + return multipleTasksPerNode; + } + + @ConfigDescription("Allow nodes to be selected multiple times by the node scheduler, in a single stage") + @Config("node-scheduler.multiple-tasks-per-node-enabled") + public NodeSchedulerConfig setMultipleTasksPerNodeEnabled(boolean multipleTasksPerNode) + { + this.multipleTasksPerNode = multipleTasksPerNode; + return this; + } + + @Min(1) + public int getMinCandidates() + { + return minCandidates; + } + + @Config("node-scheduler.min-candidates") + public NodeSchedulerConfig setMinCandidates(int candidates) + { + this.minCandidates = candidates; + return this; + } + + public boolean isLocationAwareSchedulingEnabled() + { + return locationAwareScheduling; + } + + @Config("node-scheduler.location-aware-scheduling-enabled") + public NodeSchedulerConfig setLocationAwareSchedulingEnabled(boolean locationAwareScheduling) + { + this.locationAwareScheduling = locationAwareScheduling; + return this; + } + + public boolean isIncludeCoordinator() + { + return includeCoordinator; + } + + @Config("node-scheduler.include-coordinator") + public NodeSchedulerConfig setIncludeCoordinator(boolean includeCoordinator) + { + this.includeCoordinator = includeCoordinator; + return this; + } + + @Config("node-scheduler.max-pending-splits-per-node-per-task") + public NodeSchedulerConfig setMaxPendingSplitsPerNodePerTask(int maxPendingSplitsPerNodePerTask) + { + this.maxPendingSplitsPerNodePerTask = maxPendingSplitsPerNodePerTask; + return this; + } + + public int getMaxPendingSplitsPerNodePerTask() + { + return maxPendingSplitsPerNodePerTask; + } + + public int getMaxSplitsPerNode() + { + return maxSplitsPerNode; + } + + @Config("node-scheduler.max-splits-per-node") + public NodeSchedulerConfig setMaxSplitsPerNode(int maxSplitsPerNode) + { + this.maxSplitsPerNode = maxSplitsPerNode; + return this; + } + + public String getReportNodes() + { + return reportNodes; + } + + @Config("node-scheduler.report-nodes") + public NodeSchedulerConfig setReportNodes(String reportNodes) + { + this.reportNodes = reportNodes; + return this; + } + + public boolean getSchedulerFixToReport() + { + return schedulerFixToReport; + } + + @Config("node-scheduler.scheduler-fix-to-report") + public NodeSchedulerConfig setSchedulerFixToReport(boolean schedulerFixToReport) + { + this.schedulerFixToReport = schedulerFixToReport; + return this; + } + + public boolean getSchedulerSourceToReport() + { + return schedulerSourceToReport; + } + + @Config("node-scheduler.scheduler-source-to-report") + public NodeSchedulerConfig setSchedulerSourceToReport(boolean schedulerSourceToReport) + { + this.schedulerSourceToReport = schedulerSourceToReport; + return this; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/NodeTaskMap.java b/presto-main/src/main/java/com/facebook/presto/execution/NodeTaskMap.java new file mode 100644 index 00000000..e3861ef0 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/NodeTaskMap.java @@ -0,0 +1,89 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.spi.Node; + +import javax.annotation.concurrent.GuardedBy; +import javax.annotation.concurrent.ThreadSafe; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +@ThreadSafe +public class NodeTaskMap +{ + private final ConcurrentHashMap nodeTasksMap = new ConcurrentHashMap<>(); + + public void addTask(Node node, RemoteTask task) + { + NodeTasks nodeTasks = nodeTasksMap.get(node); + if (nodeTasks == null) { + nodeTasks = addNodeTask(node); + } + nodeTasks.addTask(task); + } + + private NodeTasks addNodeTask(Node node) + { + NodeTasks newNodeTasks = new NodeTasks(); + NodeTasks nodeTasks = nodeTasksMap.putIfAbsent(node, newNodeTasks); + if (nodeTasks == null) { + return newNodeTasks; + } + return nodeTasks; + } + + public int getPartitionedSplitsOnNode(Node node) + { + NodeTasks nodeTasks = nodeTasksMap.get(node); + if (nodeTasks == null) { + nodeTasks = addNodeTask(node); + } + return nodeTasks.getPartitionedSplitCount(); + } + + private static class NodeTasks + { + @GuardedBy("this") + private final List remoteTasks = new ArrayList<>(); + + private synchronized int getPartitionedSplitCount() + { + int partitionedSplitCount = 0; + for (RemoteTask task : remoteTasks) { + partitionedSplitCount += task.getPartitionedSplitCount(); + } + return partitionedSplitCount; + } + + private synchronized void addTask(RemoteTask task) + { + remoteTasks.add(task); + task.addStateChangeListener(taskInfo -> { + if (taskInfo.getState().isDone()) { + synchronized (NodeTasks.this) { + remoteTasks.remove(task); + } + } + }); + + // Check if task state changes before adding the listener + if (task.getTaskInfo().getState().isDone()) { + remoteTasks.remove(task); + } + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/PageBufferInfo.java b/presto-main/src/main/java/com/facebook/presto/execution/PageBufferInfo.java new file mode 100644 index 00000000..244c6c62 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/PageBufferInfo.java @@ -0,0 +1,113 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +import static com.google.common.base.MoreObjects.toStringHelper; + +public class PageBufferInfo +{ + private final int partition; + private final long bufferedPages; + private final long queuedPages; + private final long bufferedBytes; + private final long pagesAdded; + + public PageBufferInfo( + @JsonProperty("partition") int partition, + @JsonProperty("bufferedPages") long bufferedPages, + @JsonProperty("queuedPages") long queuedPages, + @JsonProperty("bufferedBytes") long bufferedBytes, + @JsonProperty("pagesAdded") long pagesAdded) + { + this.partition = partition; + this.bufferedPages = bufferedPages; + this.queuedPages = queuedPages; + this.bufferedBytes = bufferedBytes; + this.pagesAdded = pagesAdded; + } + + @JsonProperty + public int getPartition() + { + return partition; + } + + @JsonProperty + public long getBufferedPages() + { + return bufferedPages; + } + + @JsonProperty + public long getQueuedPages() + { + return queuedPages; + } + + @JsonProperty + public long getBufferedBytes() + { + return bufferedBytes; + } + + @JsonProperty + public long getPagesAdded() + { + return pagesAdded; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PageBufferInfo that = (PageBufferInfo) o; + return Objects.equals(partition, that.partition) && + Objects.equals(bufferedPages, that.bufferedPages) && + Objects.equals(queuedPages, that.queuedPages) && + Objects.equals(bufferedBytes, that.bufferedBytes) && + Objects.equals(pagesAdded, that.pagesAdded); + } + + @Override + public int hashCode() + { + return Objects.hash(partition, bufferedPages, queuedPages, bufferedBytes, pagesAdded); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("partition", partition) + .add("bufferedPages", bufferedPages) + .add("queuedPages", queuedPages) + .add("bufferedBytes", bufferedBytes) + .add("pagesAdded", pagesAdded) + .toString(); + } + + public static PageBufferInfo empty() + { + return new PageBufferInfo(0, 0, 0, 0, 0); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/PageSplitterUtil.java b/presto-main/src/main/java/com/facebook/presto/execution/PageSplitterUtil.java new file mode 100644 index 00000000..1f92c846 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/PageSplitterUtil.java @@ -0,0 +1,48 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.spi.Page; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkArgument; + +public final class PageSplitterUtil +{ + private PageSplitterUtil() {} + + public static List splitPage(Page page, long maxPageSizeInBytes) + { + checkArgument(page.getPositionCount() > 0, "page is empty"); + checkArgument(maxPageSizeInBytes > 0, "maxPageSizeInBytes must be > 0"); + + if (page.getSizeInBytes() <= maxPageSizeInBytes || page.getPositionCount() == 1) { + return ImmutableList.of(page); + } + + ImmutableList.Builder outputPages = ImmutableList.builder(); + int positionCount = page.getPositionCount(); + int half = positionCount / 2; + + Page splitPage1 = page.getRegion(0, half); + outputPages.addAll(splitPage(splitPage1, maxPageSizeInBytes)); + + Page splitPage2 = page.getRegion(half, positionCount - half); + outputPages.addAll(splitPage(splitPage2, maxPageSizeInBytes)); + + return outputPages.build(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/PartitionBuffer.java b/presto-main/src/main/java/com/facebook/presto/execution/PartitionBuffer.java new file mode 100644 index 00000000..77a1e329 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/PartitionBuffer.java @@ -0,0 +1,215 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.spi.Page; +import com.google.common.collect.ImmutableList; +import com.google.common.primitives.Ints; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import io.airlift.units.DataSize; + +import javax.annotation.concurrent.Immutable; +import javax.annotation.concurrent.ThreadSafe; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicLong; + +import static com.facebook.presto.execution.PageSplitterUtil.splitPage; +import static com.facebook.presto.spi.block.PageBuilderStatus.DEFAULT_MAX_PAGE_SIZE_IN_BYTES; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Verify.verify; +import static com.google.common.util.concurrent.Futures.immediateFuture; +import static java.util.Objects.requireNonNull; + +@ThreadSafe +public class PartitionBuffer +{ + private final LinkedList masterBuffer = new LinkedList<>(); + private final BlockingQueue queuedPages = new LinkedBlockingQueue<>(); + private final AtomicLong pagesAdded = new AtomicLong(); // Number of pages added to the masterBuffer + private final AtomicLong masterSequenceId = new AtomicLong(); + private final AtomicLong bufferedBytes = new AtomicLong(); // Bytes in the master buffer + private final int partition; + private final SharedBufferMemoryManager memoryManager; + + public PartitionBuffer(int partition, SharedBufferMemoryManager memoryManager) + { + checkArgument(partition >= 0, "partition must be >= 0"); + this.partition = partition; + this.memoryManager = requireNonNull(memoryManager, "memoryManager is null"); + } + + public synchronized ListenableFuture enqueuePage(Page page) + { + if (!memoryManager.isFull()) { + addToMasterBuffer(page); + return immediateFuture(true); + } + else { + QueuedPage queuedPage = new QueuedPage(page); + queuedPages.add(queuedPage); + return queuedPage.getFuture(); + } + } + + private synchronized void addToMasterBuffer(Page page) + { + long bytesAdded = 0; + List pages = splitPage(page, DEFAULT_MAX_PAGE_SIZE_IN_BYTES); + masterBuffer.addAll(pages); + pagesAdded.addAndGet(pages.size()); + for (Page p : pages) { + bytesAdded += p.getSizeInBytes(); + } + updateMemoryUsage(bytesAdded); + } + + public synchronized List getPages(DataSize maxSize, long sequenceId) + { + long maxBytes = maxSize.toBytes(); + List pages = new ArrayList<>(); + long bytes = 0; + + int listOffset = Ints.checkedCast(sequenceId - masterSequenceId.get()); + while (listOffset < masterBuffer.size()) { + Page page = masterBuffer.get(listOffset++); + bytes += page.getSizeInBytes(); + // break (and don't add) if this page would exceed the limit + if (!pages.isEmpty() && bytes > maxBytes) { + break; + } + pages.add(page); + } + return ImmutableList.copyOf(pages); + } + + public synchronized void advanceSequenceId(long newSequenceId) + { + long oldMasterSequenceId = masterSequenceId.get(); + checkArgument(newSequenceId >= oldMasterSequenceId, "Master sequence id moved backwards: oldMasterSequenceId=%s, newMasterSequenceId=%s", + oldMasterSequenceId, + newSequenceId); + + if (newSequenceId == oldMasterSequenceId) { + return; + } + masterSequenceId.set(newSequenceId); + + // drop consumed pages + int pagesToRemove = Ints.checkedCast(newSequenceId - oldMasterSequenceId); + checkState(masterBuffer.size() >= pagesToRemove, + "MasterBuffer does not have any pages to remove: pagesToRemove %s oldMasterSequenceId: %s newSequenceId: %s", + pagesToRemove, + oldMasterSequenceId, + newSequenceId); + long bytesRemoved = 0; + for (int i = 0; i < pagesToRemove; i++) { + Page page = masterBuffer.removeFirst(); + bytesRemoved += page.getSizeInBytes(); + } + updateMemoryUsage(-bytesRemoved); + dequeuePages(); + } + + public synchronized void dequeuePages() + { + // refill buffer from queued pages + while (!queuedPages.isEmpty() && !memoryManager.isFull()) { + QueuedPage queuedPage = queuedPages.remove(); + addToMasterBuffer(queuedPage.getPage()); + queuedPage.getFuture().set(null); + } + } + + public synchronized void destroy() + { + // clear the buffer + masterBuffer.clear(); + updateMemoryUsage(-bufferedBytes.get()); + clearQueue(); + } + + public synchronized void clearQueue() + { + for (QueuedPage queuedPage : queuedPages) { + queuedPage.getFuture().set(null); + } + queuedPages.clear(); + } + + private void updateMemoryUsage(long bytesAdded) + { + bufferedBytes.addAndGet(bytesAdded); + memoryManager.updateMemoryUsage(bytesAdded); + verify(bufferedBytes.get() >= 0); + } + + public long getPageCount() + { + return pagesAdded.get(); + } + + public long getBufferedBytes() + { + return bufferedBytes.get(); + } + + public long getBufferedPageCount() + { + return masterBuffer.size(); + } + + public long getQueuedPageCount() + { + return queuedPages.size(); + } + + public int getPartition() + { + return partition; + } + + public PageBufferInfo getInfo() + { + return new PageBufferInfo(partition, getBufferedPageCount(), getQueuedPageCount(), getBufferedBytes(), pagesAdded.get()); + } + + @Immutable + private static final class QueuedPage + { + private final Page page; + private final SettableFuture future = SettableFuture.create(); + + QueuedPage(Page page) + { + this.page = page; + } + + public Page getPage() + { + return page; + } + + public SettableFuture getFuture() + { + return future; + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/QueryExecution.java b/presto-main/src/main/java/com/facebook/presto/execution/QueryExecution.java new file mode 100644 index 00000000..e2fac093 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/QueryExecution.java @@ -0,0 +1,58 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.Session; +import com.facebook.presto.execution.StateMachine.StateChangeListener; +import com.facebook.presto.memory.VersionedMemoryPoolId; +import com.facebook.presto.sql.tree.Statement; +import io.airlift.units.Duration; + +public interface QueryExecution +{ + QueryId getQueryId(); + + QueryInfo getQueryInfo(); + + QueryState getState(); + + Duration waitForStateChange(QueryState currentState, Duration maxWait) + throws InterruptedException; + + VersionedMemoryPoolId getMemoryPool(); + + void setMemoryPool(VersionedMemoryPoolId poolId); + + long getTotalMemoryReservation(); + + Session getSession(); + + void start(); + + void fail(Throwable cause); + + void cancelStage(StageId stageId); + + void recordHeartbeat(); + + // XXX: This should be removed when the client protocol is improved, so that we don't need to hold onto so much query history + void pruneInfo(); + + void addStateChangeListener(StateChangeListener stateChangeListener); + + interface QueryExecutionFactory + { + T createQueryExecution(QueryId queryId, String query, Session session, Statement statement); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/QueryExecutionMBean.java b/presto-main/src/main/java/com/facebook/presto/execution/QueryExecutionMBean.java new file mode 100644 index 00000000..20d0dab4 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/QueryExecutionMBean.java @@ -0,0 +1,43 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import io.airlift.concurrent.ThreadPoolExecutorMBean; +import org.weakref.jmx.Managed; +import org.weakref.jmx.Nested; + +import javax.inject.Inject; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ThreadPoolExecutor; + +import static com.facebook.presto.util.Types.checkType; + +public class QueryExecutionMBean +{ + private final ThreadPoolExecutorMBean executorMBean; + + @Inject + public QueryExecutionMBean(@ForQueryExecution ExecutorService executor) + { + this.executorMBean = new ThreadPoolExecutorMBean(checkType(executor, ThreadPoolExecutor.class, "executor")); + } + + @Managed + @Nested + public ThreadPoolExecutorMBean getExecutor() + { + return executorMBean; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/QueryId.java b/presto-main/src/main/java/com/facebook/presto/execution/QueryId.java new file mode 100644 index 00000000..b198757c --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/QueryId.java @@ -0,0 +1,107 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; + +import javax.annotation.concurrent.Immutable; + +import java.util.List; +import java.util.Objects; +import java.util.regex.Pattern; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Immutable +public class QueryId +{ + @JsonCreator + public static QueryId valueOf(String queryId) + { + List ids = parseDottedId(queryId, 1, "queryId"); + return new QueryId(ids.get(0)); + } + + private final String id; + + public QueryId(String id) + { + this.id = validateId(id); + } + + public String getId() + { + return id; + } + + @Override + @JsonValue + public String toString() + { + return id; + } + + @Override + public int hashCode() + { + return Objects.hash(id); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final QueryId other = (QueryId) obj; + return Objects.equals(this.id, other.id); + } + + // + // Id helper methods + // + + static final Pattern ID_PATTERN = Pattern.compile("[_a-z0-9]+"); + + static String validateId(String id) + { + checkNotNull(id, "id is null"); + checkArgument(!id.isEmpty(), "id is empty"); + checkArgument(ID_PATTERN.matcher(id).matches(), "Invalid id %s", id); + return id; + } + + static List parseDottedId(String id, int expectedParts, String name) + { + checkNotNull(id, "id is null"); + checkArgument(expectedParts > 0, "expectedParts must be at least 1"); + checkNotNull(name, "name is null"); + + ImmutableList ids = ImmutableList.copyOf(Splitter.on('.').split(id)); + checkArgument(ids.size() == expectedParts, "Invalid %s %s", name, id); + + for (String part : ids) { + checkArgument(!part.isEmpty(), "Invalid id %s", id); + checkArgument(ID_PATTERN.matcher(part).matches(), "Invalid id %s", id); + } + return ids; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/QueryIdGenerator.java b/presto-main/src/main/java/com/facebook/presto/execution/QueryIdGenerator.java new file mode 100644 index 00000000..f18f07b3 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/QueryIdGenerator.java @@ -0,0 +1,114 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableSet; +import com.google.common.primitives.Chars; +import com.google.common.util.concurrent.Uninterruptibles; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; + +import javax.annotation.concurrent.GuardedBy; + +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; + +import static com.google.common.base.Preconditions.checkState; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +public class QueryIdGenerator +{ + // a-z, 0-9, except: l, o, 0, 1 + private static final char[] BASE_32 = { + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', + 'i', 'j', 'k', 'm', 'n', 'p', 'q', 'r', + 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '2', '3', '4', '5', '6', '7', '8', '9'}; + + static { + checkState(BASE_32.length == 32); + checkState(ImmutableSet.copyOf(Chars.asList(BASE_32)).size() == 32); + } + + private static final DateTimeFormatter TIMESTAMP_FORMAT = DateTimeFormat.forPattern("YYYYMMdd_HHmmss").withZoneUTC(); + private static final long BASE_SYSTEM_TIME_MILLIS = System.currentTimeMillis(); + private static final long BASE_NANO_TIME = System.nanoTime(); + + private final String coordinatorId; + @GuardedBy("this") + private long lastTimeInDays; + @GuardedBy("this") + private long lastTimeInSeconds; + @GuardedBy("this") + private String lastTimestamp; + @GuardedBy("this") + private int counter; + + public QueryIdGenerator() + { + StringBuilder coordinatorId = new StringBuilder(5); + for (int i = 0; i < 5; i++) { + coordinatorId.append(BASE_32[ThreadLocalRandom.current().nextInt(32)]); + } + this.coordinatorId = coordinatorId.toString(); + } + + public String getCoordinatorId() + { + return coordinatorId; + } + + /** + * Generate next queryId using the following format: + * YYYYMMdd_HHmmss_index_coordId + *

+ * Index rolls at the start of every day or when it reaches 99,999, and the + * coordId is a randomly generated when this instance is created. + */ + public synchronized QueryId createNextQueryId() + { + // only generate 100,000 ids per day + if (counter > 99_999) { + // wait for the second to rollover + while (MILLISECONDS.toSeconds(nowInMillis()) == lastTimeInSeconds) { + Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS); + } + counter = 0; + } + + // if it has been a second since the last id was generated, generate a new timestamp + long now = nowInMillis(); + if (MILLISECONDS.toSeconds(now) != lastTimeInSeconds) { + // generate new timestamp + lastTimeInSeconds = MILLISECONDS.toSeconds(now); + lastTimestamp = TIMESTAMP_FORMAT.print(now); + + // if the day has rolled over, restart the counter + if (MILLISECONDS.toDays(now) != lastTimeInDays) { + lastTimeInDays = MILLISECONDS.toDays(now); + counter = 0; + } + } + + return new QueryId(String.format("%s_%05d_%s", lastTimestamp, counter++, coordinatorId)); + } + + @VisibleForTesting + protected long nowInMillis() + { + // avoid problems with the clock moving backwards + return BASE_SYSTEM_TIME_MILLIS + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - BASE_NANO_TIME); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/QueryInfo.java b/presto-main/src/main/java/com/facebook/presto/execution/QueryInfo.java new file mode 100644 index 00000000..6844fbc9 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/QueryInfo.java @@ -0,0 +1,225 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.Session; +import com.facebook.presto.client.FailureInfo; +import com.facebook.presto.memory.MemoryPoolId; +import com.facebook.presto.spi.ErrorCode; +import com.facebook.presto.spi.StandardErrorCode.ErrorType; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +import java.net.URI; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.facebook.presto.spi.StandardErrorCode.toErrorType; +import static com.google.common.base.MoreObjects.toStringHelper; +import static java.util.Objects.requireNonNull; + +@Immutable +public class QueryInfo +{ + private final QueryId queryId; + private final Session session; + private final QueryState state; + private final MemoryPoolId memoryPool; + private final boolean scheduled; + private final URI self; + private final List fieldNames; + private final String query; + private final QueryStats queryStats; + private final Map setSessionProperties; + private final Set resetSessionProperties; + private final String updateType; + private final StageInfo outputStage; + private final FailureInfo failureInfo; + private final ErrorType errorType; + private final ErrorCode errorCode; + private final Set inputs; + + @JsonCreator + public QueryInfo( + @JsonProperty("queryId") QueryId queryId, + @JsonProperty("session") Session session, + @JsonProperty("state") QueryState state, + @JsonProperty("memoryPool") MemoryPoolId memoryPool, + @JsonProperty("scheduled") boolean scheduled, + @JsonProperty("self") URI self, + @JsonProperty("fieldNames") List fieldNames, + @JsonProperty("query") String query, + @JsonProperty("queryStats") QueryStats queryStats, + @JsonProperty("setSessionProperties") Map setSessionProperties, + @JsonProperty("resetSessionProperties") Set resetSessionProperties, + @JsonProperty("updateType") String updateType, + @JsonProperty("outputStage") StageInfo outputStage, + @JsonProperty("failureInfo") FailureInfo failureInfo, + @JsonProperty("errorCode") ErrorCode errorCode, + @JsonProperty("inputs") Set inputs) + { + Preconditions.checkNotNull(queryId, "queryId is null"); + Preconditions.checkNotNull(session, "session is null"); + Preconditions.checkNotNull(state, "state is null"); + Preconditions.checkNotNull(self, "self is null"); + Preconditions.checkNotNull(fieldNames, "fieldNames is null"); + Preconditions.checkNotNull(queryStats, "queryStats is null"); + Preconditions.checkNotNull(setSessionProperties, "setSessionProperties is null"); + Preconditions.checkNotNull(resetSessionProperties, "resetSessionProperties is null"); + Preconditions.checkNotNull(query, "query is null"); + Preconditions.checkNotNull(inputs, "inputs is null"); + + this.queryId = queryId; + this.session = session; + this.state = state; + this.memoryPool = requireNonNull(memoryPool, "memoryPool is null"); + this.scheduled = scheduled; + this.self = self; + this.fieldNames = ImmutableList.copyOf(fieldNames); + this.query = query; + this.queryStats = queryStats; + this.setSessionProperties = ImmutableMap.copyOf(setSessionProperties); + this.resetSessionProperties = ImmutableSet.copyOf(resetSessionProperties); + this.updateType = updateType; + this.outputStage = outputStage; + this.failureInfo = failureInfo; + this.errorType = errorCode == null ? null : toErrorType(errorCode.getCode()); + this.errorCode = errorCode; + this.inputs = ImmutableSet.copyOf(inputs); + } + + @JsonProperty + public QueryId getQueryId() + { + return queryId; + } + + @JsonProperty + public Session getSession() + { + return session; + } + + @JsonProperty + public QueryState getState() + { + return state; + } + + @JsonProperty + public MemoryPoolId getMemoryPool() + { + return memoryPool; + } + + @JsonProperty + public boolean isScheduled() + { + return scheduled; + } + + @JsonProperty + public URI getSelf() + { + return self; + } + + @JsonProperty + public List getFieldNames() + { + return fieldNames; + } + + @JsonProperty + public String getQuery() + { + return query; + } + + @JsonProperty + public QueryStats getQueryStats() + { + return queryStats; + } + + @JsonProperty + public Map getSetSessionProperties() + { + return setSessionProperties; + } + + @JsonProperty + public Set getResetSessionProperties() + { + return resetSessionProperties; + } + + @Nullable + @JsonProperty + public String getUpdateType() + { + return updateType; + } + + @JsonProperty + public StageInfo getOutputStage() + { + return outputStage; + } + + @Nullable + @JsonProperty + public FailureInfo getFailureInfo() + { + return failureInfo; + } + + @Nullable + @JsonProperty + public ErrorType getErrorType() + { + return errorType; + } + + @Nullable + @JsonProperty + public ErrorCode getErrorCode() + { + return errorCode; + } + + @JsonProperty + public Set getInputs() + { + return inputs; + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("queryId", queryId) + .add("state", state) + .add("fieldNames", fieldNames) + .toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/QueryManager.java b/presto-main/src/main/java/com/facebook/presto/execution/QueryManager.java new file mode 100644 index 00000000..756a5ae5 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/QueryManager.java @@ -0,0 +1,40 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.Session; +import io.airlift.units.Duration; + +import java.util.List; +import java.util.Optional; + +public interface QueryManager +{ + List getAllQueryInfo(); + + Duration waitForStateChange(QueryId queryId, QueryState currentState, Duration maxWait) + throws InterruptedException; + + QueryInfo getQueryInfo(QueryId queryId); + + Optional getQueryState(QueryId queryId); + + void recordHeartbeat(QueryId queryId); + + QueryInfo createQuery(Session session, String query); + + void cancelQuery(QueryId queryId); + + void cancelStage(StageId stageId); +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/QueryManagerConfig.java b/presto-main/src/main/java/com/facebook/presto/execution/QueryManagerConfig.java new file mode 100644 index 00000000..482bfff5 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/QueryManagerConfig.java @@ -0,0 +1,252 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import io.airlift.configuration.Config; +import io.airlift.configuration.DefunctConfig; +import io.airlift.units.Duration; +import io.airlift.units.MinDuration; + +import javax.annotation.Nullable; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; + +import java.util.concurrent.TimeUnit; + +@DefunctConfig("query.max-pending-splits-per-node") +public class QueryManagerConfig +{ + private int scheduleSplitBatchSize = 1000; + private int maxConcurrentQueries = 1000; + private int maxQueuedQueries = 5000; + private int maxConcurrentBigQueries = 10; + private int maxQueuedBigQueries = 500; + private String queueConfigFile; + + private int initialHashPartitions = 8; + private Integer bigQueryInitialHashPartitions; + private Duration maxQueryAge = new Duration(15, TimeUnit.MINUTES); + private int maxQueryHistory = 100; + private Duration clientTimeout = new Duration(5, TimeUnit.MINUTES); + + private int queryManagerExecutorPoolSize = 5; + + private int remoteTaskMaxConsecutiveErrorCount = 10; + private Duration remoteTaskMinErrorDuration = new Duration(2, TimeUnit.MINUTES); + private int remoteTaskMaxCallbackThreads = 1000; + + public String getQueueConfigFile() + { + return queueConfigFile; + } + + @Config("query.queue-config-file") + public QueryManagerConfig setQueueConfigFile(String queueConfigFile) + { + this.queueConfigFile = queueConfigFile; + return this; + } + + @Min(1) + public int getScheduleSplitBatchSize() + { + return scheduleSplitBatchSize; + } + + @Config("query.schedule-split-batch-size") + public QueryManagerConfig setScheduleSplitBatchSize(int scheduleSplitBatchSize) + { + this.scheduleSplitBatchSize = scheduleSplitBatchSize; + return this; + } + + @Deprecated + @Min(1) + public int getMaxConcurrentBigQueries() + { + return maxConcurrentBigQueries; + } + + @Deprecated + @Config("experimental.max-concurrent-big-queries") + public QueryManagerConfig setMaxConcurrentBigQueries(int maxConcurrentBigQueries) + { + this.maxConcurrentBigQueries = maxConcurrentBigQueries; + return this; + } + + @Deprecated + @Min(1) + public int getMaxQueuedBigQueries() + { + return maxQueuedBigQueries; + } + + @Deprecated + @Config("experimental.max-queued-big-queries") + public QueryManagerConfig setMaxQueuedBigQueries(int maxQueuedBigQueries) + { + this.maxQueuedBigQueries = maxQueuedBigQueries; + return this; + } + + @Deprecated + @Min(1) + public int getMaxConcurrentQueries() + { + return maxConcurrentQueries; + } + + @Deprecated + @Config("query.max-concurrent-queries") + public QueryManagerConfig setMaxConcurrentQueries(int maxConcurrentQueries) + { + this.maxConcurrentQueries = maxConcurrentQueries; + return this; + } + + @Deprecated + @Min(1) + public int getMaxQueuedQueries() + { + return maxQueuedQueries; + } + + @Deprecated + @Config("query.max-queued-queries") + public QueryManagerConfig setMaxQueuedQueries(int maxQueuedQueries) + { + this.maxQueuedQueries = maxQueuedQueries; + return this; + } + + @Nullable + @Min(1) + public Integer getBigQueryInitialHashPartitions() + { + return bigQueryInitialHashPartitions; + } + + @Config("experimental.big-query-initial-hash-partitions") + public QueryManagerConfig setBigQueryInitialHashPartitions(Integer bigQueryinitialHashPartitions) + { + this.bigQueryInitialHashPartitions = bigQueryinitialHashPartitions; + return this; + } + + @Min(1) + public int getInitialHashPartitions() + { + return initialHashPartitions; + } + + @Config("query.initial-hash-partitions") + public QueryManagerConfig setInitialHashPartitions(int initialHashPartitions) + { + this.initialHashPartitions = initialHashPartitions; + return this; + } + + @NotNull + public Duration getMaxQueryAge() + { + return maxQueryAge; + } + + @Config("query.max-age") + public QueryManagerConfig setMaxQueryAge(Duration maxQueryAge) + { + this.maxQueryAge = maxQueryAge; + return this; + } + + @Min(0) + public int getMaxQueryHistory() + { + return maxQueryHistory; + } + + @Config("query.max-history") + public QueryManagerConfig setMaxQueryHistory(int maxQueryHistory) + { + this.maxQueryHistory = maxQueryHistory; + return this; + } + + @MinDuration("5s") + @NotNull + public Duration getClientTimeout() + { + return clientTimeout; + } + + @Config("query.client.timeout") + public QueryManagerConfig setClientTimeout(Duration clientTimeout) + { + this.clientTimeout = clientTimeout; + return this; + } + + @Min(1) + public int getQueryManagerExecutorPoolSize() + { + return queryManagerExecutorPoolSize; + } + + @Config("query.manager-executor-pool-size") + public QueryManagerConfig setQueryManagerExecutorPoolSize(int queryManagerExecutorPoolSize) + { + this.queryManagerExecutorPoolSize = queryManagerExecutorPoolSize; + return this; + } + + @Min(0) + public int getRemoteTaskMaxConsecutiveErrorCount() + { + return remoteTaskMaxConsecutiveErrorCount; + } + + @Config("query.remote-task.max-consecutive-error-count") + public QueryManagerConfig setRemoteTaskMaxConsecutiveErrorCount(int remoteTaskMaxConsecutiveErrorCount) + { + this.remoteTaskMaxConsecutiveErrorCount = remoteTaskMaxConsecutiveErrorCount; + return this; + } + + @NotNull + public Duration getRemoteTaskMinErrorDuration() + { + return remoteTaskMinErrorDuration; + } + + @Config("query.remote-task.min-error-duration") + public QueryManagerConfig setRemoteTaskMinErrorDuration(Duration remoteTaskMinErrorDuration) + { + this.remoteTaskMinErrorDuration = remoteTaskMinErrorDuration; + return this; + } + + @Min(1) + public int getRemoteTaskMaxCallbackThreads() + { + return remoteTaskMaxCallbackThreads; + } + + @Config("query.remote-task.max-callback-threads") + public QueryManagerConfig setRemoteTaskMaxCallbackThreads(int remoteTaskMaxCallbackThreads) + { + this.remoteTaskMaxCallbackThreads = remoteTaskMaxCallbackThreads; + return this; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/QueryQueue.java b/presto-main/src/main/java/com/facebook/presto/execution/QueryQueue.java new file mode 100644 index 00000000..02ffafdf --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/QueryQueue.java @@ -0,0 +1,111 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.MoreExecutors; +import io.airlift.concurrent.AsyncSemaphore; +import org.weakref.jmx.Managed; + +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import static com.facebook.presto.execution.SqlQueryManager.addCompletionCallback; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class QueryQueue +{ + private final int maxQueuedQueries; + private final AtomicInteger queryQueueSize = new AtomicInteger(); + private final AtomicInteger queuePermits; + private final AsyncSemaphore asyncSemaphore; + + QueryQueue(Executor queryExecutor, int maxQueuedQueries, int maxConcurrentQueries) + { + checkNotNull(queryExecutor, "queryExecutor is null"); + checkArgument(maxQueuedQueries > 0, "maxQueuedQueries must be greater than zero"); + checkArgument(maxConcurrentQueries > 0, "maxConcurrentQueries must be greater than zero"); + + this.maxQueuedQueries = maxQueuedQueries; + this.queuePermits = new AtomicInteger(maxQueuedQueries + maxConcurrentQueries); + this.asyncSemaphore = new AsyncSemaphore<>(maxConcurrentQueries, + queryExecutor, + queueEntry -> { + QueuedExecution queuedExecution = queueEntry.dequeue(); + if (queuedExecution != null) { + queuedExecution.start(); + return queuedExecution.getCompletionFuture(); + } + return Futures.immediateFuture(null); + }); + } + + @Managed + public int getQueueSize() + { + return queryQueueSize.get(); + } + + public boolean reserve(QueryExecution queryExecution) + { + if (queuePermits.getAndDecrement() < 0) { + queuePermits.incrementAndGet(); + return false; + } + + addCompletionCallback(queryExecution, queuePermits::incrementAndGet); + return true; + } + + public boolean enqueue(QueuedExecution queuedExecution) + { + if (queryQueueSize.incrementAndGet() > maxQueuedQueries) { + queryQueueSize.decrementAndGet(); + return false; + } + + // Add a callback to dequeue the entry if it is ever completed. + // This enables us to remove the entry sooner if is cancelled before starting, + // and has no effect if called after starting. + QueueEntry entry = new QueueEntry(queuedExecution, queryQueueSize::decrementAndGet); + queuedExecution.getCompletionFuture().addListener(entry::dequeue, MoreExecutors.directExecutor()); + + asyncSemaphore.submit(entry); + return true; + } + + private static class QueueEntry + { + private final AtomicReference queryExecution; + private final Runnable onDequeue; + + private QueueEntry(QueuedExecution queuedExecution, Runnable onDequeue) + { + checkNotNull(queuedExecution, "queueableExecution is null"); + this.queryExecution = new AtomicReference<>(queuedExecution); + this.onDequeue = checkNotNull(onDequeue, "onDequeue is null"); + } + + public QueuedExecution dequeue() + { + QueuedExecution value = queryExecution.getAndSet(null); + if (value != null) { + onDequeue.run(); + } + return value; + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/QueryQueueDefinition.java b/presto-main/src/main/java/com/facebook/presto/execution/QueryQueueDefinition.java new file mode 100644 index 00000000..d3f25569 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/QueryQueueDefinition.java @@ -0,0 +1,67 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.Session; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Strings.nullToEmpty; + +public class QueryQueueDefinition +{ + private static final Pattern USER_PATTERN = Pattern.compile(Pattern.quote("${USER}")); + private static final Pattern SOURCE_PATTERN = Pattern.compile(Pattern.quote("${SOURCE}")); + private final String template; + private final int maxConcurrent; + private final int maxQueued; + + public QueryQueueDefinition(String template, int maxConcurrent, int maxQueued) + { + this.template = checkNotNull(template, "template is null"); + Matcher matcher = Pattern.compile("\\$\\{(.*?)\\}").matcher(template); + while (matcher.find()) { + String group = matcher.group(1); + checkArgument(group.equals("USER") || group.equals("SOURCE"), "Unsupported template parameter: ${%s}", group); + } + checkArgument(maxConcurrent > 0, "maxConcurrent must be positive"); + checkArgument(maxQueued > 0, "maxQueued must be positive"); + this.maxConcurrent = maxConcurrent; + this.maxQueued = maxQueued; + } + + public String getExpandedTemplate(Session session) + { + String expanded = USER_PATTERN.matcher(template).replaceAll(session.getUser()); + return SOURCE_PATTERN.matcher(expanded).replaceAll(nullToEmpty(session.getSource())); + } + + String getTemplate() + { + return template; + } + + public int getMaxConcurrent() + { + return maxConcurrent; + } + + public int getMaxQueued() + { + return maxQueued; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/QueryQueueManager.java b/presto-main/src/main/java/com/facebook/presto/execution/QueryQueueManager.java new file mode 100644 index 00000000..5903e3c7 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/QueryQueueManager.java @@ -0,0 +1,28 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import javax.annotation.concurrent.ThreadSafe; + +import java.util.concurrent.Executor; + +/** + * Classes implementing this interface must be thread safe. That is, all the methods listed below + * may be called concurrently from any thread. + */ +@ThreadSafe +public interface QueryQueueManager +{ + boolean submit(QueryExecution queryExecution, Executor executor, SqlQueryManagerStats stats); +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/QueryQueueRule.java b/presto-main/src/main/java/com/facebook/presto/execution/QueryQueueRule.java new file mode 100644 index 00000000..aaa82cd0 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/QueryQueueRule.java @@ -0,0 +1,93 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.Session; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +import javax.annotation.Nullable; + +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Strings.nullToEmpty; +import static java.lang.String.format; + +public class QueryQueueRule +{ + @Nullable + private final Pattern userRegex; + @Nullable + private final Pattern sourceRegex; + private final Map sessionPropertyRegexes; + private final List queues; + + QueryQueueRule(@Nullable Pattern userRegex, @Nullable Pattern sourceRegex, Map sessionPropertyRegexes, List queues) + { + this.userRegex = userRegex; + this.sourceRegex = sourceRegex; + this.sessionPropertyRegexes = ImmutableMap.copyOf(checkNotNull(sessionPropertyRegexes, "sessionPropertyRegexes is null")); + checkNotNull(queues, "queues is null"); + checkArgument(!queues.isEmpty(), "queues is empty"); + this.queues = ImmutableList.copyOf(queues); + } + + public static QueryQueueRule createRule(@Nullable Pattern userRegex, @Nullable Pattern sourceRegex, Map sessionPropertyRegexes, List queueKeys, Map definedQueues) + { + ImmutableList.Builder queues = ImmutableList.builder(); + + for (String key : queueKeys) { + if (!definedQueues.containsKey(key)) { + throw new IllegalArgumentException(format("Undefined queue %s. Defined queues are %s", key, definedQueues.keySet())); + } + queues.add(definedQueues.get(key)); + } + + return new QueryQueueRule(userRegex, sourceRegex, sessionPropertyRegexes, queues.build()); + } + + /** + * Returns list of queues to enter, or null if query does not match rule + */ + public List match(Session session) + { + if (userRegex != null && !userRegex.matcher(session.getUser()).matches()) { + return null; + } + if (sourceRegex != null) { + String source = session.getSource(); + if (!sourceRegex.matcher(nullToEmpty(source)).matches()) { + return null; + } + } + + for (Map.Entry entry : sessionPropertyRegexes.entrySet()) { + String value = session.getSystemProperties().getOrDefault(entry.getKey(), ""); + if (!entry.getValue().matcher(value).matches()) { + return null; + } + } + + return queues; + } + + List getQueues() + { + return queues; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/QueryState.java b/presto-main/src/main/java/com/facebook/presto/execution/QueryState.java new file mode 100644 index 00000000..70d9eaad --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/QueryState.java @@ -0,0 +1,64 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import java.util.Set; +import java.util.stream.Stream; + +import static com.facebook.presto.util.ImmutableCollectors.toImmutableSet; + +public enum QueryState +{ + /** + * Query has been accepted and is awaiting execution. + */ + QUEUED(false), + /** + * Query is being planned. + */ + PLANNING(false), + /** + * Query execution is being started. + */ + STARTING(false), + /** + * Query has at least one task in the output stage. + */ + RUNNING(false), + /** + * Query has finished executing and all output has been consumed. + */ + FINISHED(true), + /** + * Query execution failed. + */ + FAILED(true); + + public static final Set TERMINAL_QUERY_STATES = Stream.of(QueryState.values()).filter(QueryState::isDone).collect(toImmutableSet()); + + private final boolean doneState; + + QueryState(boolean doneState) + { + this.doneState = doneState; + } + + /** + * Is this a terminal state. + */ + public boolean isDone() + { + return doneState; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/QueryStateMachine.java b/presto-main/src/main/java/com/facebook/presto/execution/QueryStateMachine.java new file mode 100644 index 00000000..2698eb60 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/QueryStateMachine.java @@ -0,0 +1,421 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.Session; +import com.facebook.presto.client.FailureInfo; +import com.facebook.presto.execution.StateMachine.StateChangeListener; +import com.facebook.presto.memory.VersionedMemoryPoolId; +import com.facebook.presto.operator.BlockedReason; +import com.facebook.presto.spi.ErrorCode; +import com.facebook.presto.sql.planner.PlanFragment; +import com.facebook.presto.sql.planner.plan.TableScanNode; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; +import io.airlift.log.Logger; +import io.airlift.units.DataSize; +import io.airlift.units.Duration; +import org.joda.time.DateTime; + +import javax.annotation.concurrent.ThreadSafe; + +import java.net.URI; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicReference; + +import static com.facebook.presto.execution.QueryState.FAILED; +import static com.facebook.presto.execution.QueryState.FINISHED; +import static com.facebook.presto.execution.QueryState.PLANNING; +import static com.facebook.presto.execution.QueryState.QUEUED; +import static com.facebook.presto.execution.QueryState.RUNNING; +import static com.facebook.presto.execution.QueryState.STARTING; +import static com.facebook.presto.execution.QueryState.TERMINAL_QUERY_STATES; +import static com.facebook.presto.execution.StageInfo.getAllStages; +import static com.facebook.presto.memory.LocalMemoryManager.GENERAL_POOL; +import static com.facebook.presto.util.Failures.toFailure; +import static io.airlift.units.DataSize.Unit.BYTE; +import static io.airlift.units.Duration.nanosSince; +import static java.util.Objects.requireNonNull; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.NANOSECONDS; + +@ThreadSafe +public class QueryStateMachine +{ + private static final Logger log = Logger.get(QueryStateMachine.class); + + private final DateTime createTime = DateTime.now(); + private final long createNanos = System.nanoTime(); + + private final QueryId queryId; + private final String query; + private final Session session; + private final URI self; + + private final AtomicReference memoryPool = new AtomicReference<>(new VersionedMemoryPoolId(GENERAL_POOL, 0)); + + private final AtomicReference lastHeartbeat = new AtomicReference<>(DateTime.now()); + private final AtomicReference executionStartTime = new AtomicReference<>(); + private final AtomicReference endTime = new AtomicReference<>(); + + private final AtomicReference queuedTime = new AtomicReference<>(); + private final AtomicReference analysisTime = new AtomicReference<>(); + private final AtomicReference distributedPlanningTime = new AtomicReference<>(); + + private final AtomicReference totalPlanningTime = new AtomicReference<>(); + + private final StateMachine queryState; + + private final Map setSessionProperties = new ConcurrentHashMap<>(); + private final Set resetSessionProperties = Sets.newConcurrentHashSet(); + + private final AtomicReference updateType = new AtomicReference<>(); + + private final AtomicReference failureCause = new AtomicReference<>(); + + private final AtomicReference> outputFieldNames = new AtomicReference<>(ImmutableList.of()); + + private final AtomicReference> inputs = new AtomicReference<>(ImmutableSet.of()); + + public QueryStateMachine(QueryId queryId, String query, Session session, URI self, Executor executor) + { + this.queryId = requireNonNull(queryId, "queryId is null"); + this.query = requireNonNull(query, "query is null"); + this.session = requireNonNull(session, "session is null"); + this.self = requireNonNull(self, "self is null"); + + this.queryState = new StateMachine<>("query " + query, executor, QUEUED, TERMINAL_QUERY_STATES); + queryState.addStateChangeListener(currentState -> log.debug("Query %s is %s", QueryStateMachine.this.queryId, currentState)); + } + + public QueryId getQueryId() + { + return queryId; + } + + public Session getSession() + { + return session; + } + + public QueryInfo getQueryInfoWithoutDetails() + { + return getQueryInfo(null); + } + + public QueryInfo getQueryInfo(StageInfo rootStage) + { + // Query state must be captured first in order to provide a + // correct view of the query. For example, building this + // information, the query could finish, and the task states would + // never be visible. + QueryState state = queryState.get(); + + Duration elapsedTime; + if (endTime.get() != null) { + elapsedTime = new Duration(endTime.get().getMillis() - createTime.getMillis(), MILLISECONDS); + } + else { + elapsedTime = nanosSince(createNanos); + } + + // don't report failure info is query is marked as success + FailureInfo failureInfo = null; + ErrorCode errorCode = null; + if (state != FINISHED) { + ExecutionFailureInfo failureCause = this.failureCause.get(); + if (failureCause != null) { + failureInfo = failureCause.toFailureInfo(); + errorCode = failureCause.getErrorCode(); + } + } + + int totalTasks = 0; + int runningTasks = 0; + int completedTasks = 0; + + int totalDrivers = 0; + int queuedDrivers = 0; + int runningDrivers = 0; + int completedDrivers = 0; + + long totalMemoryReservation = 0; + + long totalScheduledTime = 0; + long totalCpuTime = 0; + long totalUserTime = 0; + long totalBlockedTime = 0; + + long rawInputDataSize = 0; + long rawInputPositions = 0; + + long processedInputDataSize = 0; + long processedInputPositions = 0; + + long outputDataSize = 0; + long outputPositions = 0; + + boolean fullyBlocked = rootStage != null; + Set blockedReasons = new HashSet<>(); + + if (rootStage != null) { + for (StageInfo stageInfo : getAllStages(rootStage)) { + StageStats stageStats = stageInfo.getStageStats(); + totalTasks += stageStats.getTotalTasks(); + runningTasks += stageStats.getRunningTasks(); + completedTasks += stageStats.getCompletedTasks(); + + totalDrivers += stageStats.getTotalDrivers(); + queuedDrivers += stageStats.getQueuedDrivers(); + runningDrivers += stageStats.getRunningDrivers(); + completedDrivers += stageStats.getCompletedDrivers(); + + totalMemoryReservation += stageStats.getTotalMemoryReservation().toBytes(); + + totalScheduledTime += stageStats.getTotalScheduledTime().roundTo(NANOSECONDS); + totalCpuTime += stageStats.getTotalCpuTime().roundTo(NANOSECONDS); + totalUserTime += stageStats.getTotalUserTime().roundTo(NANOSECONDS); + totalBlockedTime += stageStats.getTotalBlockedTime().roundTo(NANOSECONDS); + if (!stageInfo.getState().isDone()) { + fullyBlocked &= stageStats.isFullyBlocked(); + blockedReasons.addAll(stageStats.getBlockedReasons()); + } + + PlanFragment plan = stageInfo.getPlan(); + if (plan != null && plan.getPartitionedSourceNode() instanceof TableScanNode) { + rawInputDataSize += stageStats.getRawInputDataSize().toBytes(); + rawInputPositions += stageStats.getRawInputPositions(); + + processedInputDataSize += stageStats.getProcessedInputDataSize().toBytes(); + processedInputPositions += stageStats.getProcessedInputPositions(); + } + } + + StageStats outputStageStats = rootStage.getStageStats(); + outputDataSize += outputStageStats.getOutputDataSize().toBytes(); + outputPositions += outputStageStats.getOutputPositions(); + } + + QueryStats queryStats = new QueryStats( + createTime, + executionStartTime.get(), + lastHeartbeat.get(), + endTime.get(), + + elapsedTime.convertToMostSuccinctTimeUnit(), + queuedTime.get(), + analysisTime.get(), + distributedPlanningTime.get(), + totalPlanningTime.get(), + + totalTasks, + runningTasks, + completedTasks, + + totalDrivers, + queuedDrivers, + runningDrivers, + completedDrivers, + + new DataSize(totalMemoryReservation, BYTE).convertToMostSuccinctDataSize(), + new Duration(totalScheduledTime, NANOSECONDS).convertToMostSuccinctTimeUnit(), + new Duration(totalCpuTime, NANOSECONDS).convertToMostSuccinctTimeUnit(), + new Duration(totalUserTime, NANOSECONDS).convertToMostSuccinctTimeUnit(), + new Duration(totalBlockedTime, NANOSECONDS).convertToMostSuccinctTimeUnit(), + fullyBlocked, + blockedReasons, + + new DataSize(rawInputDataSize, BYTE).convertToMostSuccinctDataSize(), + rawInputPositions, + new DataSize(processedInputDataSize, BYTE).convertToMostSuccinctDataSize(), + processedInputPositions, + new DataSize(outputDataSize, BYTE).convertToMostSuccinctDataSize(), + outputPositions); + + return new QueryInfo(queryId, + session, + state, + memoryPool.get().getId(), + isScheduled(rootStage), + self, + outputFieldNames.get(), + query, + queryStats, + setSessionProperties, + resetSessionProperties, + updateType.get(), + rootStage, + failureInfo, + errorCode, + inputs.get()); + } + + public VersionedMemoryPoolId getMemoryPool() + { + return memoryPool.get(); + } + + public void setMemoryPool(VersionedMemoryPoolId memoryPool) + { + this.memoryPool.set(requireNonNull(memoryPool, "memoryPool is null")); + } + + public void setOutputFieldNames(List outputFieldNames) + { + requireNonNull(outputFieldNames, "outputFieldNames is null"); + this.outputFieldNames.set(ImmutableList.copyOf(outputFieldNames)); + } + + public void setInputs(List inputs) + { + requireNonNull(inputs, "inputs is null"); + this.inputs.set(ImmutableSet.copyOf(inputs)); + } + + public Map getSetSessionProperties() + { + return setSessionProperties; + } + + public void addSetSessionProperties(String key, String value) + { + setSessionProperties.put(requireNonNull(key, "key is null"), requireNonNull(value, "value is null")); + } + + public Set getResetSessionProperties() + { + return resetSessionProperties; + } + + public void addResetSessionProperties(String name) + { + resetSessionProperties.add(requireNonNull(name, "name is null")); + } + + public void setUpdateType(String updateType) + { + this.updateType.set(updateType); + } + + public QueryState getQueryState() + { + return queryState.get(); + } + + public boolean isDone() + { + return queryState.get().isDone(); + } + + public boolean transitionToPlanning() + { + queuedTime.compareAndSet(null, nanosSince(createNanos).convertToMostSuccinctTimeUnit()); + return queryState.compareAndSet(QUEUED, PLANNING); + } + + public boolean transitionToStarting() + { + Duration durationSinceCreation = nanosSince(createNanos).convertToMostSuccinctTimeUnit(); + queuedTime.compareAndSet(null, durationSinceCreation); + totalPlanningTime.compareAndSet(null, durationSinceCreation); + + return queryState.setIf(STARTING, currentState -> currentState == QUEUED || currentState == PLANNING); + } + + public boolean transitionToRunning() + { + Duration durationSinceCreation = nanosSince(createNanos).convertToMostSuccinctTimeUnit(); + queuedTime.compareAndSet(null, durationSinceCreation); + totalPlanningTime.compareAndSet(null, durationSinceCreation); + executionStartTime.compareAndSet(null, DateTime.now()); + + return queryState.setIf(RUNNING, currentState -> currentState != RUNNING && !currentState.isDone()); + } + + public boolean transitionToFinished() + { + Duration durationSinceCreation = nanosSince(createNanos).convertToMostSuccinctTimeUnit(); + queuedTime.compareAndSet(null, durationSinceCreation); + totalPlanningTime.compareAndSet(null, durationSinceCreation); + DateTime now = DateTime.now(); + executionStartTime.compareAndSet(null, now); + endTime.compareAndSet(null, now); + + return queryState.setIf(FINISHED, currentState ->!currentState.isDone()); + } + + public boolean transitionToFailed(Throwable throwable) + { + requireNonNull(throwable, "throwable is null"); + + Duration durationSinceCreation = nanosSince(createNanos).convertToMostSuccinctTimeUnit(); + queuedTime.compareAndSet(null, durationSinceCreation); + totalPlanningTime.compareAndSet(null, durationSinceCreation); + DateTime now = DateTime.now(); + executionStartTime.compareAndSet(null, now); + endTime.compareAndSet(null, now); + + failureCause.compareAndSet(null, toFailure(throwable)); + boolean failed = queryState.setIf(FAILED, currentState -> !currentState.isDone()); + if (failed) { + log.error(throwable, "Query %s failed", queryId); + } + else { + log.debug(throwable, "Failure after query %s finished", queryId); + } + return failed; + } + + public void addStateChangeListener(StateChangeListener stateChangeListener) + { + queryState.addStateChangeListener(stateChangeListener); + } + + public Duration waitForStateChange(QueryState currentState, Duration maxWait) + throws InterruptedException + { + return queryState.waitForStateChange(currentState, maxWait); + } + + public void recordHeartbeat() + { + this.lastHeartbeat.set(DateTime.now()); + } + + public void recordAnalysisTime(long analysisStart) + { + analysisTime.compareAndSet(null, nanosSince(analysisStart).convertToMostSuccinctTimeUnit()); + } + + public void recordDistributedPlanningTime(long distributedPlanningStart) + { + distributedPlanningTime.compareAndSet(null, nanosSince(distributedPlanningStart).convertToMostSuccinctTimeUnit()); + } + + private static boolean isScheduled(StageInfo rootStage) + { + if (rootStage == null) { + return false; + } + return getAllStages(rootStage).stream() + .map(StageInfo::getState) + .allMatch(state -> (state == StageState.RUNNING) || state.isDone()); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/QueryStats.java b/presto-main/src/main/java/com/facebook/presto/execution/QueryStats.java new file mode 100644 index 00000000..90570ca3 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/QueryStats.java @@ -0,0 +1,367 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.operator.BlockedReason; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableSet; +import io.airlift.units.DataSize; +import io.airlift.units.Duration; +import org.joda.time.DateTime; + +import java.util.Set; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.Objects.requireNonNull; + +public class QueryStats +{ + private final DateTime createTime; + + private final DateTime executionStartTime; + private final DateTime lastHeartbeat; + private final DateTime endTime; + + private final Duration elapsedTime; + private final Duration queuedTime; + private final Duration analysisTime; + private final Duration distributedPlanningTime; + private final Duration totalPlanningTime; + + private final int totalTasks; + private final int runningTasks; + private final int completedTasks; + + private final int totalDrivers; + private final int queuedDrivers; + private final int runningDrivers; + private final int completedDrivers; + + private final DataSize totalMemoryReservation; + + private final Duration totalScheduledTime; + private final Duration totalCpuTime; + private final Duration totalUserTime; + private final Duration totalBlockedTime; + private final boolean fullyBlocked; + private final Set blockedReasons; + + private final DataSize rawInputDataSize; + private final long rawInputPositions; + + private final DataSize processedInputDataSize; + private final long processedInputPositions; + + private final DataSize outputDataSize; + private final long outputPositions; + + @VisibleForTesting + public QueryStats() + { + this.createTime = null; + this.executionStartTime = null; + this.lastHeartbeat = null; + this.endTime = null; + this.elapsedTime = null; + this.queuedTime = null; + this.analysisTime = null; + this.distributedPlanningTime = null; + this.totalPlanningTime = null; + this.totalTasks = 0; + this.runningTasks = 0; + this.completedTasks = 0; + this.totalDrivers = 0; + this.queuedDrivers = 0; + this.runningDrivers = 0; + this.completedDrivers = 0; + this.totalMemoryReservation = null; + this.totalScheduledTime = null; + this.totalCpuTime = null; + this.totalUserTime = null; + this.totalBlockedTime = null; + this.fullyBlocked = false; + this.blockedReasons = ImmutableSet.of(); + this.rawInputDataSize = null; + this.rawInputPositions = 0; + this.processedInputDataSize = null; + this.processedInputPositions = 0; + this.outputDataSize = null; + this.outputPositions = 0; + } + + @JsonCreator + public QueryStats( + @JsonProperty("createTime") DateTime createTime, + @JsonProperty("executionStartTime") DateTime executionStartTime, + @JsonProperty("lastHeartbeat") DateTime lastHeartbeat, + @JsonProperty("endTime") DateTime endTime, + + @JsonProperty("elapsedTime") Duration elapsedTime, + @JsonProperty("queuedTime") Duration queuedTime, + @JsonProperty("analysisTime") Duration analysisTime, + @JsonProperty("distributedPlanningTime") Duration distributedPlanningTime, + @JsonProperty("totalPlanningTime") Duration totalPlanningTime, + + @JsonProperty("totalTasks") int totalTasks, + @JsonProperty("runningTasks") int runningTasks, + @JsonProperty("completedTasks") int completedTasks, + + @JsonProperty("totalDrivers") int totalDrivers, + @JsonProperty("queuedDrivers") int queuedDrivers, + @JsonProperty("runningDrivers") int runningDrivers, + @JsonProperty("completedDrivers") int completedDrivers, + + @JsonProperty("totalMemoryReservation") DataSize totalMemoryReservation, + + @JsonProperty("totalScheduledTime") Duration totalScheduledTime, + @JsonProperty("totalCpuTime") Duration totalCpuTime, + @JsonProperty("totalUserTime") Duration totalUserTime, + @JsonProperty("totalBlockedTime") Duration totalBlockedTime, + @JsonProperty("fullyBlocked") boolean fullyBlocked, + @JsonProperty("blockedReasons") Set blockedReasons, + + @JsonProperty("rawInputDataSize") DataSize rawInputDataSize, + @JsonProperty("rawInputPositions") long rawInputPositions, + + @JsonProperty("processedInputDataSize") DataSize processedInputDataSize, + @JsonProperty("processedInputPositions") long processedInputPositions, + + @JsonProperty("outputDataSize") DataSize outputDataSize, + @JsonProperty("outputPositions") long outputPositions) + { + this.createTime = checkNotNull(createTime, "createTime is null"); + this.executionStartTime = executionStartTime; + this.lastHeartbeat = checkNotNull(lastHeartbeat, "lastHeartbeat is null"); + this.endTime = endTime; + + this.elapsedTime = elapsedTime; + this.queuedTime = queuedTime; + this.analysisTime = analysisTime; + this.distributedPlanningTime = distributedPlanningTime; + this.totalPlanningTime = totalPlanningTime; + + checkArgument(totalTasks >= 0, "totalTasks is negative"); + this.totalTasks = totalTasks; + checkArgument(runningTasks >= 0, "runningTasks is negative"); + this.runningTasks = runningTasks; + checkArgument(completedTasks >= 0, "completedTasks is negative"); + this.completedTasks = completedTasks; + + checkArgument(totalDrivers >= 0, "totalDrivers is negative"); + this.totalDrivers = totalDrivers; + checkArgument(queuedDrivers >= 0, "queuedDrivers is negative"); + this.queuedDrivers = queuedDrivers; + checkArgument(runningDrivers >= 0, "runningDrivers is negative"); + this.runningDrivers = runningDrivers; + checkArgument(completedDrivers >= 0, "completedDrivers is negative"); + this.completedDrivers = completedDrivers; + + this.totalMemoryReservation = checkNotNull(totalMemoryReservation, "totalMemoryReservation is null"); + this.totalScheduledTime = checkNotNull(totalScheduledTime, "totalScheduledTime is null"); + this.totalCpuTime = checkNotNull(totalCpuTime, "totalCpuTime is null"); + this.totalUserTime = checkNotNull(totalUserTime, "totalUserTime is null"); + this.totalBlockedTime = checkNotNull(totalBlockedTime, "totalBlockedTime is null"); + this.fullyBlocked = fullyBlocked; + this.blockedReasons = ImmutableSet.copyOf(requireNonNull(blockedReasons, "blockedReasons is null")); + + this.rawInputDataSize = checkNotNull(rawInputDataSize, "rawInputDataSize is null"); + checkArgument(rawInputPositions >= 0, "rawInputPositions is negative"); + this.rawInputPositions = rawInputPositions; + + this.processedInputDataSize = checkNotNull(processedInputDataSize, "processedInputDataSize is null"); + checkArgument(processedInputPositions >= 0, "processedInputPositions is negative"); + this.processedInputPositions = processedInputPositions; + + this.outputDataSize = checkNotNull(outputDataSize, "outputDataSize is null"); + checkArgument(outputPositions >= 0, "outputPositions is negative"); + this.outputPositions = outputPositions; + } + + @JsonProperty + public DateTime getCreateTime() + { + return createTime; + } + + @JsonProperty + public DateTime getExecutionStartTime() + { + return executionStartTime; + } + + @JsonProperty + public DateTime getLastHeartbeat() + { + return lastHeartbeat; + } + + @JsonProperty + public DateTime getEndTime() + { + return endTime; + } + + @JsonProperty + public Duration getElapsedTime() + { + return elapsedTime; + } + + @JsonProperty + public Duration getQueuedTime() + { + return queuedTime; + } + + @JsonProperty + public Duration getAnalysisTime() + { + return analysisTime; + } + + @JsonProperty + public Duration getDistributedPlanningTime() + { + return distributedPlanningTime; + } + + @JsonProperty + public Duration getTotalPlanningTime() + { + return totalPlanningTime; + } + + @JsonProperty + public int getTotalTasks() + { + return totalTasks; + } + + @JsonProperty + public int getRunningTasks() + { + return runningTasks; + } + + @JsonProperty + public int getCompletedTasks() + { + return completedTasks; + } + + @JsonProperty + public int getTotalDrivers() + { + return totalDrivers; + } + + @JsonProperty + public int getQueuedDrivers() + { + return queuedDrivers; + } + + @JsonProperty + public int getRunningDrivers() + { + return runningDrivers; + } + + @JsonProperty + public int getCompletedDrivers() + { + return completedDrivers; + } + + @JsonProperty + public DataSize getTotalMemoryReservation() + { + return totalMemoryReservation; + } + + @JsonProperty + public Duration getTotalScheduledTime() + { + return totalScheduledTime; + } + + @JsonProperty + public Duration getTotalCpuTime() + { + return totalCpuTime; + } + + @JsonProperty + public Duration getTotalUserTime() + { + return totalUserTime; + } + + @JsonProperty + public Duration getTotalBlockedTime() + { + return totalBlockedTime; + } + + @JsonProperty + public boolean isFullyBlocked() + { + return fullyBlocked; + } + + @JsonProperty + public Set getBlockedReasons() + { + return blockedReasons; + } + + @JsonProperty + public DataSize getRawInputDataSize() + { + return rawInputDataSize; + } + + @JsonProperty + public long getRawInputPositions() + { + return rawInputPositions; + } + + @JsonProperty + public DataSize getProcessedInputDataSize() + { + return processedInputDataSize; + } + + @JsonProperty + public long getProcessedInputPositions() + { + return processedInputPositions; + } + + @JsonProperty + public DataSize getOutputDataSize() + { + return outputDataSize; + } + + @JsonProperty + public long getOutputPositions() + { + return outputPositions; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/QueuedExecution.java b/presto-main/src/main/java/com/facebook/presto/execution/QueuedExecution.java new file mode 100644 index 00000000..18a9d52f --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/QueuedExecution.java @@ -0,0 +1,78 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.SettableFuture; +import io.airlift.concurrent.SetThreadName; + +import java.util.List; +import java.util.concurrent.Executor; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class QueuedExecution +{ + private final QueryExecution queryExecution; + private final List nextQueues; + private final ListenableFuture listenableFuture; + private final Executor executor; + private final SqlQueryManagerStats stats; + + public static QueuedExecution createQueuedExecution(QueryExecution queryExecution, List nextQueues, Executor executor, SqlQueryManagerStats stats) + { + SettableFuture settableFuture = SettableFuture.create(); + SqlQueryManager.addCompletionCallback(queryExecution, () -> settableFuture.set(null)); + return new QueuedExecution(queryExecution, nextQueues, executor, stats, settableFuture); + } + + private QueuedExecution(QueryExecution queryExecution, List nextQueues, Executor executor, SqlQueryManagerStats stats, ListenableFuture listenableFuture) + { + this.queryExecution = checkNotNull(queryExecution, "queryExecution is null"); + this.nextQueues = ImmutableList.copyOf(checkNotNull(nextQueues, "nextQueues is null")); + this.executor = checkNotNull(executor, "executor is null"); + this.stats = checkNotNull(stats, "stats is null"); + this.listenableFuture = checkNotNull(listenableFuture, "listenableFuture is null"); + } + + public ListenableFuture getCompletionFuture() + { + return listenableFuture; + } + + public void start() + { + // Only execute if the query is not already completed (e.g. cancelled) + if (listenableFuture.isDone()) { + return; + } + if (nextQueues.isEmpty()) { + executor.execute(() -> { + try (SetThreadName setThreadName = new SetThreadName("Query-%s", queryExecution.getQueryInfo().getQueryId())) { + stats.queryStarted(); + listenableFuture.addListener(stats::queryStopped, MoreExecutors.directExecutor()); + + queryExecution.start(); + } + }); + } + else { + if (!nextQueues.get(0).enqueue(new QueuedExecution(queryExecution, nextQueues.subList(1, nextQueues.size()), executor, stats, listenableFuture))) { + queryExecution.fail(new IllegalStateException("Entering secondary queue failed")); + } + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/RemoteTask.java b/presto-main/src/main/java/com/facebook/presto/execution/RemoteTask.java new file mode 100644 index 00000000..b19f304d --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/RemoteTask.java @@ -0,0 +1,44 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.OutputBuffers; +import com.facebook.presto.execution.StateMachine.StateChangeListener; +import com.facebook.presto.metadata.Split; +import com.facebook.presto.sql.planner.plan.PlanNodeId; + +public interface RemoteTask +{ + String getNodeId(); + + TaskInfo getTaskInfo(); + + void start(); + + void addSplits(PlanNodeId sourceId, Iterable split); + + void noMoreSplits(PlanNodeId sourceId); + + void setOutputBuffers(OutputBuffers outputBuffers); + + void addStateChangeListener(StateChangeListener stateChangeListener); + + void cancel(); + + void abort(); + + int getPartitionedSplitCount(); + + int getQueuedPartitionedSplitCount(); +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/RemoteTaskFactory.java b/presto-main/src/main/java/com/facebook/presto/execution/RemoteTaskFactory.java new file mode 100644 index 00000000..f893e2dc --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/RemoteTaskFactory.java @@ -0,0 +1,32 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.OutputBuffers; +import com.facebook.presto.Session; +import com.facebook.presto.metadata.Split; +import com.facebook.presto.spi.Node; +import com.facebook.presto.sql.planner.PlanFragment; +import com.facebook.presto.sql.planner.plan.PlanNodeId; +import com.google.common.collect.Multimap; + +public interface RemoteTaskFactory +{ + RemoteTask createRemoteTask(Session session, + TaskId taskId, + Node node, + PlanFragment fragment, + Multimap initialSplits, + OutputBuffers outputBuffers); +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/RenameColumnTask.java b/presto-main/src/main/java/com/facebook/presto/execution/RenameColumnTask.java new file mode 100644 index 00000000..6d24b866 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/RenameColumnTask.java @@ -0,0 +1,60 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.Session; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.metadata.QualifiedTableName; +import com.facebook.presto.metadata.TableHandle; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.sql.analyzer.SemanticException; +import com.facebook.presto.sql.tree.RenameColumn; + +import java.util.Map; +import java.util.Optional; + +import static com.facebook.presto.metadata.MetadataUtil.createQualifiedTableName; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.COLUMN_ALREADY_EXISTS; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.MISSING_COLUMN; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.MISSING_TABLE; + +public class RenameColumnTask + implements DataDefinitionTask +{ + @Override + public String getName() + { + return "RENAME COLUMN"; + } + + @Override + public void execute(RenameColumn statement, Session session, Metadata metadata, QueryStateMachine stateMachine) + { + QualifiedTableName tableName = createQualifiedTableName(session, statement.getTable()); + Optional tableHandle = metadata.getTableHandle(session, tableName); + if (!tableHandle.isPresent()) { + throw new SemanticException(MISSING_TABLE, statement, "Table '%s' does not exist", tableName); + } + + Map columnHandles = metadata.getColumnHandles(tableHandle.get()); + if (!columnHandles.containsKey(statement.getSource())) { + throw new SemanticException(MISSING_COLUMN, statement, "Column '%s' does not exist", statement.getSource()); + } + + if (columnHandles.containsKey(statement.getTarget())) { + throw new SemanticException(COLUMN_ALREADY_EXISTS, statement, "Column '%s' already exists", statement.getTarget()); + } + metadata.renameColumn(tableHandle.get(), columnHandles.get(statement.getSource()), statement.getTarget()); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/RenameTableTask.java b/presto-main/src/main/java/com/facebook/presto/execution/RenameTableTask.java new file mode 100644 index 00000000..e2b96205 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/RenameTableTask.java @@ -0,0 +1,58 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.Session; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.metadata.QualifiedTableName; +import com.facebook.presto.metadata.TableHandle; +import com.facebook.presto.sql.analyzer.SemanticException; +import com.facebook.presto.sql.tree.RenameTable; + +import java.util.Optional; + +import static com.facebook.presto.metadata.MetadataUtil.createQualifiedTableName; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.MISSING_CATALOG; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.MISSING_TABLE; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.TABLE_ALREADY_EXISTS; + +public class RenameTableTask + implements DataDefinitionTask +{ + @Override + public String getName() + { + return "RENAME TABLE"; + } + + @Override + public void execute(RenameTable statement, Session session, Metadata metadata, QueryStateMachine stateMachine) + { + QualifiedTableName tableName = createQualifiedTableName(session, statement.getSource()); + Optional tableHandle = metadata.getTableHandle(session, tableName); + if (!tableHandle.isPresent()) { + throw new SemanticException(MISSING_TABLE, statement, "Table '%s' does not exist", tableName); + } + + QualifiedTableName target = createQualifiedTableName(session, statement.getTarget()); + if (!metadata.getCatalogNames().containsKey(target.getCatalogName())) { + throw new SemanticException(MISSING_CATALOG, statement, "Target catalog '%s' does not exist", target.getCatalogName()); + } + if (metadata.getTableHandle(session, target).isPresent()) { + throw new SemanticException(TABLE_ALREADY_EXISTS, statement, "Target table '%s' already exists", target); + } + + metadata.renameTable(tableHandle.get(), target); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/ResetSessionTask.java b/presto-main/src/main/java/com/facebook/presto/execution/ResetSessionTask.java new file mode 100644 index 00000000..ac999851 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/ResetSessionTask.java @@ -0,0 +1,41 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.Session; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.sql.analyzer.SemanticException; +import com.facebook.presto.sql.tree.ResetSession; + +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.INVALID_SESSION_PROPERTY; + +public class ResetSessionTask + implements DataDefinitionTask +{ + @Override + public String getName() + { + return "RESET SESSION"; + } + + @Override + public void execute(ResetSession statement, Session session, Metadata metadata, QueryStateMachine stateMachine) + { + if (statement.getName().getParts().size() > 2) { + throw new SemanticException(INVALID_SESSION_PROPERTY, statement, "Invalid session property '%s'", statement.getName()); + } + + stateMachine.addResetSessionProperties(statement.getName().toString()); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/ResettableRandomizedIterator.java b/presto-main/src/main/java/com/facebook/presto/execution/ResettableRandomizedIterator.java new file mode 100644 index 00000000..b44fa295 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/ResettableRandomizedIterator.java @@ -0,0 +1,60 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.concurrent.ThreadLocalRandom; + +class ResettableRandomizedIterator + implements Iterator +{ + private final List list; + private int position; + + public ResettableRandomizedIterator(Collection elements) + { + this.list = new ArrayList<>(elements); + } + + public void reset() + { + position = 0; + } + + @Override + public boolean hasNext() + { + return position < list.size(); + } + + @Override + public T next() + { + if (!hasNext()) { + throw new NoSuchElementException(); + } + + int position = ThreadLocalRandom.current().nextInt(this.position, list.size()); + + T result = list.set(position, list.get(this.position)); + list.set(this.position, result); + this.position++; + + return result; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/SetSessionTask.java b/presto-main/src/main/java/com/facebook/presto/execution/SetSessionTask.java new file mode 100644 index 00000000..4b6615ca --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/SetSessionTask.java @@ -0,0 +1,42 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.Session; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.sql.analyzer.SemanticException; +import com.facebook.presto.sql.tree.SetSession; + +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.INVALID_SESSION_PROPERTY; + +public class SetSessionTask + implements DataDefinitionTask +{ + @Override + public String getName() + { + return "SET SESSION"; + } + + @Override + public void execute(SetSession statement, Session session, Metadata metadata, QueryStateMachine stateMachine) + { + if (statement.getName().getParts().size() > 2) { + throw new SemanticException(INVALID_SESSION_PROPERTY, statement, "Invalid session property '%s'", statement.getName()); + } + + // todo verify session properties are valid + stateMachine.addSetSessionProperties(statement.getName().toString(), statement.getValue()); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/SharedBuffer.java b/presto-main/src/main/java/com/facebook/presto/execution/SharedBuffer.java new file mode 100644 index 00000000..ae1b17f4 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/SharedBuffer.java @@ -0,0 +1,586 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.HashPagePartitionFunction; +import com.facebook.presto.OutputBuffers; +import com.facebook.presto.PagePartitionFunction; +import com.facebook.presto.execution.StateMachine.StateChangeListener; +import com.facebook.presto.spi.Page; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Sets; +import com.google.common.collect.Sets.SetView; +import com.google.common.primitives.Ints; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import io.airlift.units.DataSize; + +import javax.annotation.concurrent.GuardedBy; +import javax.annotation.concurrent.Immutable; +import javax.annotation.concurrent.ThreadSafe; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Stream; + +import static com.facebook.presto.OutputBuffers.INITIAL_EMPTY_OUTPUT_BUFFERS; +import static com.facebook.presto.execution.BufferResult.emptyResults; +import static com.facebook.presto.execution.SharedBuffer.BufferState.FAILED; +import static com.facebook.presto.execution.SharedBuffer.BufferState.FINISHED; +import static com.facebook.presto.execution.SharedBuffer.BufferState.FLUSHING; +import static com.facebook.presto.execution.SharedBuffer.BufferState.NO_MORE_BUFFERS; +import static com.facebook.presto.execution.SharedBuffer.BufferState.NO_MORE_PAGES; +import static com.facebook.presto.execution.SharedBuffer.BufferState.OPEN; +import static com.facebook.presto.execution.SharedBuffer.BufferState.TERMINAL_BUFFER_STATES; +import static com.facebook.presto.util.ImmutableCollectors.toImmutableSet; +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.util.concurrent.Futures.immediateFuture; +import static java.util.Objects.requireNonNull; + +@ThreadSafe +public class SharedBuffer +{ + public enum BufferState + { + /** + * Additional buffers can be added. + * Any next state is allowed. + */ + OPEN(true, true, false), + /** + * No more buffers can be added. + * Next state is {@link #FLUSHING}. + */ + NO_MORE_BUFFERS(true, false, false), + /** + * No more pages can be added. + * Next state is {@link #FLUSHING}. + */ + NO_MORE_PAGES(false, true, false), + /** + * No more pages or buffers can be added, and buffer is waiting + * for the final pages to be consumed. + * Next state is {@link #FINISHED}. + */ + FLUSHING(false, false, false), + /** + * No more buffers can be added and all pages have been consumed. + * This is the terminal state. + */ + FINISHED(false, false, true), + /** + * Buffer has failed. No more buffers or pages can be added. Readers + * will be blocked, as to not communicate a finished state. It is + * assumed that the reader will be cleaned up elsewhere. + * This is the terminal state. + */ + FAILED(false, false, true); + + public static final Set TERMINAL_BUFFER_STATES = Stream.of(BufferState.values()).filter(BufferState::isTerminal).collect(toImmutableSet()); + + private final boolean newPagesAllowed; + private final boolean newBuffersAllowed; + private final boolean terminal; + + BufferState(boolean newPagesAllowed, boolean newBuffersAllowed, boolean terminal) + { + this.newPagesAllowed = newPagesAllowed; + this.newBuffersAllowed = newBuffersAllowed; + this.terminal = terminal; + } + + public boolean canAddPages() + { + return newPagesAllowed; + } + + public boolean canAddBuffers() + { + return newBuffersAllowed; + } + + public boolean isTerminal() + { + return terminal; + } + } + + private final SettableFuture finalOutputBuffers = SettableFuture.create(); + + @GuardedBy("this") + private OutputBuffers outputBuffers = INITIAL_EMPTY_OUTPUT_BUFFERS; + @GuardedBy("this") + private final Map partitionBuffers = new ConcurrentHashMap<>(); + @GuardedBy("this") + private final Map> partitionToNamedBuffer = new ConcurrentHashMap<>(); + @GuardedBy("this") + private final ConcurrentMap namedBuffers = new ConcurrentHashMap<>(); + @GuardedBy("this") + private final Set abortedBuffers = new HashSet<>(); + + private final StateMachine state; + + @GuardedBy("this") + private final List stateChangeListeners = new ArrayList<>(); + + private final SharedBufferMemoryManager memoryManager; + + public SharedBuffer(TaskId taskId, Executor executor, DataSize maxBufferSize) + { + checkNotNull(taskId, "taskId is null"); + checkNotNull(executor, "executor is null"); + state = new StateMachine<>(taskId + "-buffer", executor, OPEN, TERMINAL_BUFFER_STATES); + + checkNotNull(maxBufferSize, "maxBufferSize is null"); + checkArgument(maxBufferSize.toBytes() > 0, "maxBufferSize must be at least 1"); + this.memoryManager = new SharedBufferMemoryManager(maxBufferSize.toBytes()); + } + + public void addStateChangeListener(StateChangeListener stateChangeListener) + { + state.addStateChangeListener(stateChangeListener); + } + + public boolean isFinished() + { + return state.get() == FINISHED; + } + + public SharedBufferInfo getInfo() + { + // + // NOTE: this code must be lock free to we are not hanging state machine updates + // + checkState(!Thread.holdsLock(this), "Thread must NOT hold a lock on the %s", SharedBuffer.class.getSimpleName()); + BufferState state = this.state.get(); + ImmutableList.Builder infos = ImmutableList.builder(); + for (NamedBuffer namedBuffer : namedBuffers.values()) { + infos.add(namedBuffer.getInfo()); + } + + long totalBufferedBytes = partitionBuffers.values().stream().mapToLong(PartitionBuffer::getBufferedBytes).sum(); + long totalBufferedPages = partitionBuffers.values().stream().mapToLong(PartitionBuffer::getBufferedPageCount).sum(); + long totalQueuedPages = partitionBuffers.values().stream().mapToLong(PartitionBuffer::getQueuedPageCount).sum(); + long totalPagesSent = partitionBuffers.values().stream().mapToLong(PartitionBuffer::getPageCount).sum(); + + return new SharedBufferInfo(state, state.canAddBuffers(), state.canAddPages(), totalBufferedBytes, totalBufferedPages, totalQueuedPages, totalPagesSent, infos.build()); + } + + public ListenableFuture getFinalOutputBuffers() + { + return finalOutputBuffers; + } + + public synchronized void setOutputBuffers(OutputBuffers newOutputBuffers) + { + checkNotNull(newOutputBuffers, "newOutputBuffers is null"); + // ignore buffers added after query finishes, which can happen when a query is canceled + // also ignore old versions, which is normal + if (state.get().isTerminal() || outputBuffers.getVersion() >= newOutputBuffers.getVersion()) { + return; + } + + // verify this is valid state change + SetView missingBuffers = Sets.difference(outputBuffers.getBuffers().keySet(), newOutputBuffers.getBuffers().keySet()); + checkArgument(missingBuffers.isEmpty(), "newOutputBuffers does not have existing buffers %s", missingBuffers); + checkArgument(!outputBuffers.isNoMoreBufferIds() || newOutputBuffers.isNoMoreBufferIds(), "Expected newOutputBuffers to have noMoreBufferIds set"); + outputBuffers = newOutputBuffers; + + // add the new buffers + for (Entry entry : outputBuffers.getBuffers().entrySet()) { + TaskId bufferId = entry.getKey(); + if (!namedBuffers.containsKey(bufferId)) { + checkState(state.get().canAddBuffers(), "Cannot add buffers to %s", SharedBuffer.class.getSimpleName()); + PagePartitionFunction partitionFunction = entry.getValue(); + + int partition = 0; + if (partitionFunction instanceof HashPagePartitionFunction) { + partition = ((HashPagePartitionFunction) partitionFunction).getPartition(); + } + + PartitionBuffer partitionBuffer = createOrGetPartitionBuffer(partition); + NamedBuffer namedBuffer = new NamedBuffer(bufferId, partitionBuffer); + + // the buffer may have been aborted before the creation message was received + if (abortedBuffers.contains(bufferId)) { + namedBuffer.abort(); + } + namedBuffers.put(bufferId, namedBuffer); + Set namedBuffers = partitionToNamedBuffer.computeIfAbsent(partition, k -> new HashSet<>()); + namedBuffers.add(namedBuffer); + } + } + + // update state if no more buffers is set + if (outputBuffers.isNoMoreBufferIds()) { + state.compareAndSet(OPEN, NO_MORE_BUFFERS); + state.compareAndSet(NO_MORE_PAGES, FLUSHING); + finalOutputBuffers.set(outputBuffers); + } + + updateState(); + } + + private PartitionBuffer createOrGetPartitionBuffer(int partition) + { + checkState(Thread.holdsLock(this), "Thread must hold a lock on the %s", SharedBuffer.class.getSimpleName()); + return partitionBuffers.computeIfAbsent(partition, k -> new PartitionBuffer(partition, memoryManager)); + } + + public synchronized ListenableFuture enqueue(Page page) + { + return enqueue(0, page); + } + + public synchronized ListenableFuture enqueue(int partition, Page page) + { + checkNotNull(page, "page is null"); + + // ignore pages after no more pages is set + // this can happen with a limit query + if (!state.get().canAddPages()) { + return immediateFuture(true); + } + + PartitionBuffer partitionBuffer = createOrGetPartitionBuffer(partition); + ListenableFuture result = partitionBuffer.enqueuePage(page); + processPendingReads(); + updateState(); + return result; + } + + public synchronized ListenableFuture get(TaskId outputId, long startingSequenceId, DataSize maxSize) + { + checkNotNull(outputId, "outputId is null"); + checkArgument(maxSize.toBytes() > 0, "maxSize must be at least 1 byte"); + + // if no buffers can be added, and the requested buffer does not exist, return a closed empty result + // this can happen with limit queries + BufferState state = this.state.get(); + if (state != FAILED && !state.canAddBuffers() && namedBuffers.get(outputId) == null) { + return immediateFuture(emptyResults(0, true)); + } + + // return a future for data + GetBufferResult getBufferResult = new GetBufferResult(outputId, startingSequenceId, maxSize); + stateChangeListeners.add(getBufferResult); + updateState(); + return getBufferResult.getFuture(); + } + + public synchronized void abort(TaskId outputId) + { + checkNotNull(outputId, "outputId is null"); + + abortedBuffers.add(outputId); + + NamedBuffer namedBuffer = namedBuffers.get(outputId); + if (namedBuffer != null) { + namedBuffer.abort(); + } + + updateState(); + } + + public synchronized void setNoMorePages() + { + if (state.compareAndSet(OPEN, NO_MORE_PAGES) || state.compareAndSet(NO_MORE_BUFFERS, FLUSHING)) { + updateState(); + } + } + + /** + * Destroys the buffer, discarding all pages. + */ + public synchronized void destroy() + { + // ignore destroy if the buffer already in a terminal state. + if (state.get().isTerminal()) { + return; + } + + state.set(FINISHED); + + partitionBuffers.values().forEach(PartitionBuffer::destroy); + // free readers + namedBuffers.values().forEach(SharedBuffer.NamedBuffer::abort); + processPendingReads(); + } + + /** + * Fail the buffer, discarding all pages, but blocking readers. + */ + public synchronized void fail() + { + // ignore fail if the buffer already in a terminal state. + if (state.get().isTerminal()) { + return; + } + + state.set(FAILED); + partitionBuffers.values().forEach(PartitionBuffer::destroy); + + // DO NOT free readers + } + + private void checkFlushComplete() + { + checkState(Thread.holdsLock(this), "Thread must hold a lock on the %s", SharedBuffer.class.getSimpleName()); + + if (state.get() == FLUSHING) { + for (NamedBuffer namedBuffer : namedBuffers.values()) { + if (!namedBuffer.checkCompletion()) { + return; + } + } + destroy(); + } + } + + private void updateState() + { + checkState(Thread.holdsLock(this), "Thread must hold a lock on the %s", SharedBuffer.class.getSimpleName()); + + try { + processPendingReads(); + + BufferState state = this.state.get(); + + // do not update if the buffer is already in a terminal state + if (state.isTerminal()) { + return; + } + + if (!state.canAddPages()) { + // discard queued pages (not officially in the buffer) + partitionBuffers.values().forEach(PartitionBuffer::clearQueue); + } + + // advanced master queue + if (!state.canAddBuffers() && !namedBuffers.isEmpty()) { + for (Map.Entry> entry : partitionToNamedBuffer.entrySet()) { + PartitionBuffer partitionBuffer = partitionBuffers.get(entry.getKey()); + long newMasterSequenceId = entry.getValue().stream() + .mapToLong(NamedBuffer::getSequenceId) + .min() + .getAsLong(); + partitionBuffer.advanceSequenceId(newMasterSequenceId); + // this might have freed up space in the buffers, try to dequeue pages + partitionBuffers.values().forEach(PartitionBuffer::dequeuePages); + } + } + + // remove any completed buffers + if (!state.canAddPages()) { + namedBuffers.values().forEach(SharedBuffer.NamedBuffer::checkCompletion); + } + } + finally { + checkFlushComplete(); + } + } + + private void processPendingReads() + { + checkState(Thread.holdsLock(this), "Thread must hold a lock on the %s", SharedBuffer.class.getSimpleName()); + + ImmutableList.copyOf(stateChangeListeners).stream().filter(GetBufferResult::execute).forEach(stateChangeListeners::remove); + } + + @ThreadSafe + private final class NamedBuffer + { + private final TaskId bufferId; + private final PartitionBuffer partitionBuffer; + + private final AtomicLong sequenceId = new AtomicLong(); + private final AtomicBoolean finished = new AtomicBoolean(); + + private NamedBuffer(TaskId bufferId, PartitionBuffer partitionBuffer) + { + this.bufferId = requireNonNull(bufferId, "bufferId is null"); + this.partitionBuffer = requireNonNull(partitionBuffer, "partitionBuffer is null"); + } + + public BufferInfo getInfo() + { + // + // NOTE: this code must be lock free to we are not hanging state machine updates + // + checkState(!Thread.holdsLock(SharedBuffer.this), "Thread must NOT hold a lock on the %s", SharedBuffer.class.getSimpleName()); + + long sequenceId = this.sequenceId.get(); + + if (finished.get()) { + return new BufferInfo(bufferId, true, 0, sequenceId, partitionBuffer.getInfo()); + } + + int bufferedPages = Math.max(Ints.checkedCast(partitionBuffer.getPageCount() - sequenceId), 0); + return new BufferInfo(bufferId, finished.get(), bufferedPages, sequenceId, partitionBuffer.getInfo()); + } + + public long getSequenceId() + { + checkState(Thread.holdsLock(SharedBuffer.this), "Thread must hold a lock on the %s", SharedBuffer.class.getSimpleName()); + + return sequenceId.get(); + } + + public BufferResult getPages(long startingSequenceId, DataSize maxSize) + { + checkState(Thread.holdsLock(SharedBuffer.this), "Thread must hold a lock on the %s", SharedBuffer.class.getSimpleName()); + checkArgument(maxSize.toBytes() > 0, "maxSize must be at least 1 byte"); + + long sequenceId = this.sequenceId.get(); + checkArgument(startingSequenceId >= sequenceId, "startingSequenceId is before the beginning of the buffer"); + + // acknowledge previous pages + if (startingSequenceId > sequenceId) { + this.sequenceId.set(startingSequenceId); + sequenceId = startingSequenceId; + } + + if (checkCompletion()) { + return emptyResults(startingSequenceId, true); + } + + List pages = partitionBuffer.getPages(maxSize, sequenceId); + return new BufferResult(startingSequenceId, startingSequenceId + pages.size(), false, pages); + } + + public void abort() + { + checkState(Thread.holdsLock(SharedBuffer.this), "Thread must hold a lock on the %s", SharedBuffer.class.getSimpleName()); + + finished.set(true); + } + + public boolean checkCompletion() + { + checkState(Thread.holdsLock(SharedBuffer.this), "Thread must hold a lock on the %s", SharedBuffer.class.getSimpleName()); + // WARNING: finish must short circuit this call, or the call to checkFlushComplete below will cause an infinite recursion + if (finished.get()) { + return true; + } + + long pagesAdded = partitionBuffer.getPageCount(); + if (!state.get().canAddPages() && sequenceId.get() >= pagesAdded) { + // WARNING: finish must set before the call to checkFlushComplete of the short circuit above will not trigger and the code enter an infinite recursion + finished.set(true); + + // check if master buffer is finished + checkFlushComplete(); + } + return finished.get(); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("bufferId", bufferId) + .add("sequenceId", sequenceId.get()) + .add("finished", finished.get()) + .toString(); + } + } + + @Immutable + private class GetBufferResult + { + private final SettableFuture future = SettableFuture.create(); + + private final TaskId outputId; + private final long startingSequenceId; + private final DataSize maxSize; + + public GetBufferResult(TaskId outputId, long startingSequenceId, DataSize maxSize) + { + this.outputId = outputId; + this.startingSequenceId = startingSequenceId; + this.maxSize = maxSize; + } + + public SettableFuture getFuture() + { + return future; + } + + public boolean execute() + { + checkState(Thread.holdsLock(SharedBuffer.this), "Thread must hold a lock on the %s", SharedBuffer.class.getSimpleName()); + + if (future.isDone()) { + return true; + } + + // Buffer is failed, block the reader. Eventually, the reader will be aborted by the coordinator. + if (state.get() == FAILED) { + return false; + } + + try { + NamedBuffer namedBuffer = namedBuffers.get(outputId); + + // if buffer is finished return an empty page + // this could be a request for a buffer that never existed, but that is ok since the buffer + // could have been destroyed before the creation message was received + if (state.get() == FINISHED) { + future.set(emptyResults(namedBuffer == null ? 0 : namedBuffer.getSequenceId(), true)); + return true; + } + + // buffer doesn't exist yet. Block reader until buffer is created + if (namedBuffer == null) { + return false; + } + + // if request is for pages before the current position, just return an empty page + if (startingSequenceId < namedBuffer.getSequenceId()) { + future.set(emptyResults(startingSequenceId, false)); + return true; + } + + // read pages from the buffer + BufferResult bufferResult = namedBuffer.getPages(startingSequenceId, maxSize); + + // if this was the last page, we're done + checkFlushComplete(); + + // if we got an empty result, wait for more pages + if (bufferResult.isEmpty() && !bufferResult.isBufferClosed()) { + return false; + } + + future.set(bufferResult); + } + catch (Throwable throwable) { + future.setException(throwable); + } + return true; + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/SharedBufferInfo.java b/presto-main/src/main/java/com/facebook/presto/execution/SharedBufferInfo.java new file mode 100644 index 00000000..b0e5a100 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/SharedBufferInfo.java @@ -0,0 +1,146 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.execution.SharedBuffer.BufferState; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; + +import java.util.List; +import java.util.Objects; + +import static com.google.common.base.MoreObjects.toStringHelper; + +public final class SharedBufferInfo +{ + private final BufferState state; + private final boolean canAddBuffers; + private final boolean canAddPages; + private final long totalBufferedBytes; + private final long totalBufferedPages; + private final long totalQueuedPages; + private final long totalPagesSent; + private final List buffers; + + @JsonCreator + public SharedBufferInfo( + @JsonProperty("state") BufferState state, + @JsonProperty("canAddBuffers") boolean canAddBuffers, + @JsonProperty("canAddPages") boolean canAddPages, + @JsonProperty("totalBufferedBytes") long totalBufferedBytes, + @JsonProperty("totalBufferedPages") long totalBufferedPages, + @JsonProperty("totalQueuedPages") long totalQueuedPages, + @JsonProperty("totalPagesSent") long totalPagesSent, + @JsonProperty("buffers") List buffers) + { + this.state = state; + this.canAddBuffers = canAddBuffers; + this.canAddPages = canAddPages; + this.totalBufferedBytes = totalBufferedBytes; + this.totalBufferedPages = totalBufferedPages; + this.totalQueuedPages = totalQueuedPages; + this.totalPagesSent = totalPagesSent; + this.buffers = ImmutableList.copyOf(buffers); + } + + @JsonProperty + public BufferState getState() + { + return state; + } + + @JsonProperty + public List getBuffers() + { + return buffers; + } + + @JsonProperty + public boolean isCanAddBuffers() + { + return canAddBuffers; + } + + @JsonProperty + public boolean isCanAddPages() + { + return canAddPages; + } + + @JsonProperty + public long getTotalBufferedBytes() + { + return totalBufferedBytes; + } + + @JsonProperty + public long getTotalBufferedPages() + { + return totalBufferedPages; + } + + @JsonProperty + public long getTotalQueuedPages() + { + return totalQueuedPages; + } + + @JsonProperty + public long getTotalPagesSent() + { + return totalPagesSent; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SharedBufferInfo that = (SharedBufferInfo) o; + return Objects.equals(canAddBuffers, that.canAddBuffers) && + Objects.equals(canAddPages, that.canAddPages) && + Objects.equals(totalBufferedBytes, that.totalBufferedBytes) && + Objects.equals(totalBufferedPages, that.totalBufferedPages) && + Objects.equals(totalQueuedPages, that.totalQueuedPages) && + Objects.equals(totalPagesSent, that.totalPagesSent) && + Objects.equals(state, that.state) && + Objects.equals(buffers, that.buffers); + } + + @Override + public int hashCode() + { + return Objects.hash(state, canAddBuffers, canAddPages, totalBufferedBytes, totalBufferedPages, totalQueuedPages, totalPagesSent, buffers); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("state", state) + .add("canAddBuffers", canAddBuffers) + .add("canAddPages", canAddPages) + .add("totalBufferedBytes", totalBufferedBytes) + .add("totalBufferedPages", totalBufferedPages) + .add("totalQueuedPages", totalQueuedPages) + .add("totalPagesSent", totalPagesSent) + .add("buffers", buffers) + .toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/SharedBufferMemoryManager.java b/presto-main/src/main/java/com/facebook/presto/execution/SharedBufferMemoryManager.java new file mode 100644 index 00000000..8ba0aca5 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/SharedBufferMemoryManager.java @@ -0,0 +1,40 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import java.util.concurrent.atomic.AtomicLong; + +import static com.google.common.base.Preconditions.checkArgument; + +public class SharedBufferMemoryManager +{ + private final long maxBufferedBytes; + private final AtomicLong bufferedBytes = new AtomicLong(); + + public SharedBufferMemoryManager(long maxBufferedBytes) + { + checkArgument(maxBufferedBytes > 0, "maxBufferedBytes must be > 0"); + this.maxBufferedBytes = maxBufferedBytes; + } + + public void updateMemoryUsage(long bytesAdded) + { + bufferedBytes.addAndGet(bytesAdded); + } + + public boolean isFull() + { + return bufferedBytes.get() >= maxBufferedBytes; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/SimpleDomain.java b/presto-main/src/main/java/com/facebook/presto/execution/SimpleDomain.java new file mode 100644 index 00000000..a3174e97 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/SimpleDomain.java @@ -0,0 +1,117 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.spi.Domain; +import com.facebook.presto.spi.Range; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; + +import javax.annotation.concurrent.Immutable; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkNotNull; + +@Immutable +public final class SimpleDomain +{ + private final boolean nullAllowed; + private final Optional> ranges; + + @JsonCreator + public SimpleDomain( + @JsonProperty("nullAllowed") boolean nullAllowed, + @JsonProperty("ranges") Optional> ranges) + { + this.nullAllowed = nullAllowed; + checkNotNull(ranges, "ranges is null"); + List rangesCopy = null; + if (ranges.isPresent()) { + rangesCopy = ImmutableList.copyOf(ranges.get()); + } + this.ranges = Optional.ofNullable(rangesCopy); + } + + @JsonProperty + public boolean isNullAllowed() + { + return nullAllowed; + } + + @JsonProperty + public Optional> getRanges() + { + return ranges; + } + + private static List getSimpleRangeList(Domain domain) + { + checkNotNull(domain, "domain is null"); + if (domain.isAll()) { + return null; + } + if (domain.isNone()) { + return ImmutableList.of(); + } + ImmutableList.Builder rangeBuilder = ImmutableList.builder(); + for (Range range : domain.getRanges()) { + rangeBuilder.add(SimpleRange.fromRange(range)); + } + return rangeBuilder.build(); + } + + public static SimpleDomain fromDomain(Domain domain) + { + if (domain == null) { + return null; + } + return new SimpleDomain(domain.isNullAllowed(), Optional.of(getSimpleRangeList(domain))); + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + SimpleDomain that = (SimpleDomain) o; + + return Objects.equals(this.nullAllowed, that.nullAllowed) && + Objects.equals(this.ranges, that.ranges); + } + + @Override + public int hashCode() + { + return Objects.hash(nullAllowed, ranges); + } + + @Override + public String toString() + { + return toStringHelper(this) + .addValue(nullAllowed) + .addValue(ranges) + .toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/SimpleMarker.java b/presto-main/src/main/java/com/facebook/presto/execution/SimpleMarker.java new file mode 100644 index 00000000..f618ab46 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/SimpleMarker.java @@ -0,0 +1,113 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.spi.Marker; +import com.facebook.presto.spi.SerializableNativeValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.concurrent.Immutable; + +import java.util.Objects; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Immutable +public final class SimpleMarker +{ + private final boolean inclusive; + private final Comparable value; + private final Class type; + + private SimpleMarker(boolean inclusive, Comparable value, Class type) + { + checkNotNull(value, "value is null"); + checkNotNull(type, "type is null"); + checkArgument(type.isInstance(value), String.format("value (%s) must be of specified type (%s)", value, type)); + this.inclusive = inclusive; + this.value = value; + this.type = type; + } + + @JsonCreator + public SimpleMarker( + @JsonProperty("inclusive") boolean inclusive, + @JsonProperty("value") SerializableNativeValue value) + { + this(inclusive, value.getValue(), value.getType()); + } + + @JsonProperty + public boolean isInclusive() + { + return inclusive; + } + + @JsonIgnore + public Comparable getValue() + { + return value; + } + + @JsonProperty("value") + public SerializableNativeValue getSerializableNativeValue() + { + return new SerializableNativeValue(type, value); + } + + public static SimpleMarker fromMarker(Marker marker) + { + if (marker == null || marker.isUpperUnbounded() || marker.isLowerUnbounded()) { + return null; + } + return new SimpleMarker(marker.getBound() == Marker.Bound.EXACTLY, marker.getValue(), marker.getType()); + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + SimpleMarker that = (SimpleMarker) o; + + return Objects.equals(this.inclusive, that.inclusive) && + Objects.equals(this.value, that.value) && + Objects.equals(this.type, that.type); + } + + @Override + public int hashCode() + { + return Objects.hash(inclusive, value, type); + } + + @Override + public String toString() + { + return toStringHelper(this) + .addValue(inclusive) + .addValue(value) + .addValue(type) + .toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/SimpleRange.java b/presto-main/src/main/java/com/facebook/presto/execution/SimpleRange.java new file mode 100644 index 00000000..a6e42f02 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/SimpleRange.java @@ -0,0 +1,93 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.spi.Range; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.concurrent.Immutable; + +import java.util.Objects; +import java.util.Optional; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkNotNull; + +@Immutable +public final class SimpleRange +{ + private final Optional low; + private final Optional high; + + @JsonCreator + public SimpleRange( + @JsonProperty("low") Optional low, + @JsonProperty("high") Optional high) + { + this.low = checkNotNull(low, "low is null"); + this.high = checkNotNull(high, "high is null"); + } + + @JsonProperty + public Optional getLow() + { + return low; + } + + @JsonProperty + public Optional getHigh() + { + return high; + } + + public static SimpleRange fromRange(Range range) + { + checkNotNull(range, "range is null"); + return new SimpleRange( + Optional.ofNullable(SimpleMarker.fromMarker(range.getLow())), + Optional.ofNullable(SimpleMarker.fromMarker(range.getHigh()))); + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + SimpleRange that = (SimpleRange) o; + + return Objects.equals(this.low, that.low) && + Objects.equals(this.high, that.high); + } + + @Override + public int hashCode() + { + return Objects.hash(low, high); + } + + @Override + public String toString() + { + return toStringHelper(this) + .addValue(low) + .addValue(high) + .toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/SplitRunner.java b/presto-main/src/main/java/com/facebook/presto/execution/SplitRunner.java new file mode 100644 index 00000000..83c9c991 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/SplitRunner.java @@ -0,0 +1,31 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.google.common.util.concurrent.ListenableFuture; +import io.airlift.units.Duration; + +import java.io.Closeable; + +public interface SplitRunner + extends Closeable +{ + boolean isFinished(); + + ListenableFuture processFor(Duration duration) + throws Exception; + + @Override + void close(); +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/SqlQueryExecution.java b/presto-main/src/main/java/com/facebook/presto/execution/SqlQueryExecution.java new file mode 100644 index 00000000..ac5a143a --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/SqlQueryExecution.java @@ -0,0 +1,562 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.OutputBuffers; +import com.facebook.presto.Session; +import com.facebook.presto.UnpartitionedPagePartitionFunction; +import com.facebook.presto.execution.StateMachine.StateChangeListener; +import com.facebook.presto.memory.VersionedMemoryPoolId; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.spi.NodeManager; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.split.SplitManager; +import com.facebook.presto.sql.analyzer.Analysis; +import com.facebook.presto.sql.analyzer.Analyzer; +import com.facebook.presto.sql.analyzer.FeaturesConfig; +import com.facebook.presto.sql.analyzer.QueryExplainer; +import com.facebook.presto.sql.parser.SqlParser; +import com.facebook.presto.sql.planner.DistributedExecutionPlanner; +import com.facebook.presto.sql.planner.InputExtractor; +import com.facebook.presto.sql.planner.LogicalPlanner; +import com.facebook.presto.sql.planner.Plan; +import com.facebook.presto.sql.planner.PlanFragmenter; +import com.facebook.presto.sql.planner.PlanNodeIdAllocator; +import com.facebook.presto.sql.planner.StageExecutionPlan; +import com.facebook.presto.sql.planner.SubPlan; +import com.facebook.presto.sql.planner.optimizations.PlanOptimizer; +import com.facebook.presto.sql.tree.Statement; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import io.airlift.concurrent.SetThreadName; +import io.airlift.units.Duration; + +import javax.annotation.concurrent.ThreadSafe; +import javax.inject.Inject; + +import java.net.URI; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicReference; + +import static com.facebook.presto.OutputBuffers.INITIAL_EMPTY_OUTPUT_BUFFERS; +import static com.facebook.presto.SystemSessionProperties.getHashPartitionCount; +import static com.facebook.presto.SystemSessionProperties.isBigQueryEnabled; +import static com.facebook.presto.spi.StandardErrorCode.INTERNAL_ERROR; +import static com.facebook.presto.spi.StandardErrorCode.NOT_SUPPORTED; +import static com.facebook.presto.spi.StandardErrorCode.USER_CANCELED; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.Objects.requireNonNull; + +@ThreadSafe +public final class SqlQueryExecution + implements QueryExecution +{ + private static final OutputBuffers ROOT_OUTPUT_BUFFERS = INITIAL_EMPTY_OUTPUT_BUFFERS + .withBuffer(new TaskId("output", "buffer", "id"), new UnpartitionedPagePartitionFunction()) + .withNoMoreBufferIds(); + + private final QueryStateMachine stateMachine; + + private final Statement statement; + private final Metadata metadata; + private final SqlParser sqlParser; + private final SplitManager splitManager; + private final NodeScheduler nodeScheduler; + private final List planOptimizers; + private final RemoteTaskFactory remoteTaskFactory; + private final LocationFactory locationFactory; + private final int scheduleSplitBatchSize; + private final int initialHashPartitions; + private final boolean experimentalSyntaxEnabled; + private final ExecutorService queryExecutor; + + private final QueryExplainer queryExplainer; + private final AtomicReference outputStage = new AtomicReference<>(); + private final AtomicReference finalQueryInfo = new AtomicReference<>(); + private final NodeTaskMap nodeTaskMap; + private final Session session; + + public SqlQueryExecution(QueryId queryId, + String query, + Session session, + URI self, + Statement statement, + Metadata metadata, + SqlParser sqlParser, + SplitManager splitManager, + NodeScheduler nodeScheduler, + List planOptimizers, + RemoteTaskFactory remoteTaskFactory, + LocationFactory locationFactory, + int scheduleSplitBatchSize, + int initialHashPartitions, + boolean experimentalSyntaxEnabled, + ExecutorService queryExecutor, + NodeTaskMap nodeTaskMap) + { + try (SetThreadName ignored = new SetThreadName("Query-%s", queryId)) { + this.statement = checkNotNull(statement, "statement is null"); + this.metadata = checkNotNull(metadata, "metadata is null"); + this.sqlParser = checkNotNull(sqlParser, "sqlParser is null"); + this.splitManager = checkNotNull(splitManager, "splitManager is null"); + this.nodeScheduler = checkNotNull(nodeScheduler, "nodeScheduler is null"); + this.planOptimizers = checkNotNull(planOptimizers, "planOptimizers is null"); + this.remoteTaskFactory = checkNotNull(remoteTaskFactory, "remoteTaskFactory is null"); + this.locationFactory = checkNotNull(locationFactory, "locationFactory is null"); + this.queryExecutor = checkNotNull(queryExecutor, "queryExecutor is null"); + this.experimentalSyntaxEnabled = experimentalSyntaxEnabled; + this.nodeTaskMap = checkNotNull(nodeTaskMap, "nodeTaskMap is null"); + this.session = checkNotNull(session, "session is null"); + + checkArgument(scheduleSplitBatchSize > 0, "scheduleSplitBatchSize must be greater than 0"); + this.scheduleSplitBatchSize = scheduleSplitBatchSize; + + checkArgument(initialHashPartitions > 0, "initialHashPartitions must be greater than 0"); + this.initialHashPartitions = initialHashPartitions; + + checkNotNull(queryId, "queryId is null"); + checkNotNull(query, "query is null"); + checkNotNull(session, "session is null"); + checkNotNull(self, "self is null"); + this.stateMachine = new QueryStateMachine(queryId, query, session, self, queryExecutor); + + // when the query finishes cache the final query info, and clear the reference to the output stage + stateMachine.addStateChangeListener(state -> { + if (!state.isDone()) { + return; + } + + // query is now done, so abort any work that is still running + SqlStageExecution stage = outputStage.get(); + if (stage != null) { + stage.abort(); + } + + // capture the final query state and drop reference to the output stage + finalQueryInfo.compareAndSet(null, getQueryInfo(stage)); + outputStage.set(null); + }); + + this.queryExplainer = new QueryExplainer(session, planOptimizers, metadata, sqlParser, experimentalSyntaxEnabled); + } + } + + @Override + public VersionedMemoryPoolId getMemoryPool() + { + return stateMachine.getMemoryPool(); + } + + @Override + public void setMemoryPool(VersionedMemoryPoolId poolId) + { + stateMachine.setMemoryPool(poolId); + } + + @Override + public long getTotalMemoryReservation() + { + // acquire reference to outputStage before checking finalQueryInfo, because + // state change listener sets finalQueryInfo and then clears outputStage when + // the query finishes. + SqlStageExecution stage = outputStage.get(); + QueryInfo queryInfo = finalQueryInfo.get(); + if (queryInfo != null) { + return queryInfo.getQueryStats().getTotalMemoryReservation().toBytes(); + } + return stage.getTotalMemoryReservation(); + } + + @Override + public Session getSession() + { + return session; + } + + @Override + public void start() + { + try (SetThreadName ignored = new SetThreadName("Query-%s", stateMachine.getQueryId())) { + try { + // transition to planning + if (!stateMachine.transitionToPlanning()) { + // query already started or finished + return; + } + + // analyze query + SubPlan subplan = analyzeQuery(); + + // plan distribution of query + planDistribution(subplan); + + // transition to starting + if (!stateMachine.transitionToStarting()) { + // query already started or finished + return; + } + + // if query is not finished, start the stage, otherwise cancel it + SqlStageExecution stage = outputStage.get(); + + if (!stateMachine.isDone()) { + stage.start(); + } + } + catch (Throwable e) { + fail(e); + Throwables.propagateIfInstanceOf(e, Error.class); + } + } + } + + @Override + public void addStateChangeListener(StateChangeListener stateChangeListener) + { + try (SetThreadName ignored = new SetThreadName("Query-%s", stateMachine.getQueryId())) { + stateMachine.addStateChangeListener(stateChangeListener); + } + } + + private SubPlan analyzeQuery() + { + try { + return doAnalyzeQuery(); + } + catch (StackOverflowError e) { + throw new PrestoException(NOT_SUPPORTED, "statement is too large (stack overflow during analysis)", e); + } + } + + private SubPlan doAnalyzeQuery() + { + // time analysis phase + long analysisStart = System.nanoTime(); + + // analyze query + Analyzer analyzer = new Analyzer(stateMachine.getSession(), metadata, sqlParser, Optional.of(queryExplainer), experimentalSyntaxEnabled); + Analysis analysis = analyzer.analyze(statement); + + stateMachine.setUpdateType(analysis.getUpdateType()); + + // plan query + PlanNodeIdAllocator idAllocator = new PlanNodeIdAllocator(); + LogicalPlanner logicalPlanner = new LogicalPlanner(stateMachine.getSession(), planOptimizers, idAllocator, metadata); + Plan plan = logicalPlanner.plan(analysis); + + // extract inputs + List inputs = new InputExtractor(metadata).extract(plan.getRoot()); + stateMachine.setInputs(inputs); + + // fragment the plan + SubPlan subplan = new PlanFragmenter().createSubPlans(plan); + + // record analysis time + stateMachine.recordAnalysisTime(analysisStart); + + return subplan; + } + + private void planDistribution(SubPlan subplan) + { + // time distribution planning + long distributedPlanningStart = System.nanoTime(); + + // plan the execution on the active nodes + DistributedExecutionPlanner distributedPlanner = new DistributedExecutionPlanner(splitManager); + StageExecutionPlan outputStageExecutionPlan = distributedPlanner.plan(subplan); + + if (stateMachine.isDone()) { + return; + } + + // record field names + stateMachine.setOutputFieldNames(outputStageExecutionPlan.getFieldNames()); + + // build the stage execution objects (this doesn't schedule execution) + SqlStageExecution outputStage = new SqlStageExecution(stateMachine.getQueryId(), + locationFactory, + outputStageExecutionPlan, + nodeScheduler, + remoteTaskFactory, + stateMachine.getSession(), + scheduleSplitBatchSize, + initialHashPartitions, + queryExecutor, + nodeTaskMap, + ROOT_OUTPUT_BUFFERS); + + outputStage.addStateChangeListener(state -> { + if (state == StageState.FINISHED) { + stateMachine.transitionToFinished(); + } + else if (state == StageState.CANCELED) { + // output stage was canceled + stateMachine.transitionToFailed(new PrestoException(USER_CANCELED, "Query was canceled")); + } + }); + + for (SqlStageExecution stage : getAllStages(outputStage)) { + stage.addStateChangeListener(state -> { + if (stateMachine.isDone()) { + return; + } + if (stateMachine.getQueryState() == QueryState.STARTING) { + // if any stage has at least one task, we are running + if (!stage.getStageInfo().getTasks().isEmpty()) { + stateMachine.transitionToRunning(); + } + } + else if (state == StageState.FAILED) { + stateMachine.transitionToFailed(stage.getStageInfo().getFailureCause().toException()); + } + else if (state == StageState.ABORTED) { + // this should never happen, since abort can only be triggered in query clean up after the query is finished + stateMachine.transitionToFailed(new PrestoException(INTERNAL_ERROR, "Query stage was aborted")); + } + }); + } + + // only export output stage reference after listeners are added + this.outputStage.set(outputStage); + + // if query was canceled during stage creation, abort the output stage + // directly since the callback may have already fired + if (stateMachine.isDone()) { + outputStage.abort(); + } + + // record planning time + stateMachine.recordDistributedPlanningTime(distributedPlanningStart); + } + + @Override + public void cancelStage(StageId stageId) + { + checkNotNull(stageId, "stageId is null"); + + try (SetThreadName ignored = new SetThreadName("Query-%s", stateMachine.getQueryId())) { + SqlStageExecution stageExecution = outputStage.get(); + if (stageExecution != null) { + stageExecution.cancelStage(stageId); + } + } + } + + @Override + public void fail(Throwable cause) + { + requireNonNull(cause, "cause is null"); + + try (SetThreadName ignored = new SetThreadName("Query-%s", stateMachine.getQueryId())) { + stateMachine.transitionToFailed(cause); + } + } + + @Override + public Duration waitForStateChange(QueryState currentState, Duration maxWait) + throws InterruptedException + { + try (SetThreadName ignored = new SetThreadName("Query-%s", stateMachine.getQueryId())) { + return stateMachine.waitForStateChange(currentState, maxWait); + } + } + + @Override + public void recordHeartbeat() + { + stateMachine.recordHeartbeat(); + } + + @Override + public void pruneInfo() + { + QueryInfo queryInfo = finalQueryInfo.get(); + if (queryInfo == null || queryInfo.getOutputStage() == null) { + return; + } + + StageInfo prunedOutputStage = new StageInfo( + queryInfo.getOutputStage().getStageId(), + queryInfo.getOutputStage().getState(), + queryInfo.getOutputStage().getSelf(), + null, // Remove the plan + queryInfo.getOutputStage().getTypes(), + queryInfo.getOutputStage().getStageStats(), + ImmutableList.of(), // Remove the tasks + ImmutableList.of(), // Remove the substages + queryInfo.getOutputStage().getFailureCause() + ); + + QueryInfo prunedQueryInfo = new QueryInfo( + queryInfo.getQueryId(), + queryInfo.getSession(), + queryInfo.getState(), + getMemoryPool().getId(), + queryInfo.isScheduled(), + queryInfo.getSelf(), + queryInfo.getFieldNames(), + queryInfo.getQuery(), + queryInfo.getQueryStats(), + queryInfo.getSetSessionProperties(), + queryInfo.getResetSessionProperties(), + queryInfo.getUpdateType(), + prunedOutputStage, + queryInfo.getFailureInfo(), + queryInfo.getErrorCode(), + queryInfo.getInputs() + ); + finalQueryInfo.compareAndSet(queryInfo, prunedQueryInfo); + } + + @Override + public QueryId getQueryId() + { + return stateMachine.getQueryId(); + } + + @Override + public QueryInfo getQueryInfo() + { + try (SetThreadName ignored = new SetThreadName("Query-%s", stateMachine.getQueryId())) { + // acquire reference to outputStage before checking finalQueryInfo, because + // state change listener sets finalQueryInfo and then clears outputStage when + // the query finishes. + SqlStageExecution outputStage = this.outputStage.get(); + + QueryInfo finalQueryInfo = this.finalQueryInfo.get(); + if (finalQueryInfo != null) { + return finalQueryInfo; + } + + return getQueryInfo(outputStage); + } + } + + @Override + public QueryState getState() + { + return stateMachine.getQueryState(); + } + + private QueryInfo getQueryInfo(SqlStageExecution outputStage) + { + StageInfo stageInfo = null; + if (outputStage != null) { + stageInfo = outputStage.getStageInfo(); + } + return stateMachine.getQueryInfo(stageInfo); + } + + private static List getAllStages(SqlStageExecution stage) + { + ImmutableList.Builder collector = ImmutableList.builder(); + if (stage != null) { + addAllStages(stage, collector); + } + return collector.build(); + } + + private static void addAllStages(SqlStageExecution stage, ImmutableList.Builder collector) + { + collector.add(stage); + for (SqlStageExecution subStage : stage.getSubStages()) { + addAllStages(subStage, collector); + } + } + + public static class SqlQueryExecutionFactory + implements QueryExecutionFactory + { + private final int scheduleSplitBatchSize; + private final int initialHashPartitions; + private final Integer bigQueryInitialHashPartitions; + private final boolean experimentalSyntaxEnabled; + private final Metadata metadata; + private final SqlParser sqlParser; + private final SplitManager splitManager; + private final NodeScheduler nodeScheduler; + private final List planOptimizers; + private final RemoteTaskFactory remoteTaskFactory; + private final LocationFactory locationFactory; + private final ExecutorService executor; + private final NodeTaskMap nodeTaskMap; + private final NodeManager nodeManager; + + @Inject + SqlQueryExecutionFactory(QueryManagerConfig config, + FeaturesConfig featuresConfig, + Metadata metadata, + SqlParser sqlParser, + LocationFactory locationFactory, + SplitManager splitManager, + NodeScheduler nodeScheduler, + NodeManager nodeManager, + List planOptimizers, + RemoteTaskFactory remoteTaskFactory, + @ForQueryExecution ExecutorService executor, + NodeTaskMap nodeTaskMap) + { + checkNotNull(config, "config is null"); + this.scheduleSplitBatchSize = config.getScheduleSplitBatchSize(); + this.initialHashPartitions = config.getInitialHashPartitions(); + this.bigQueryInitialHashPartitions = config.getBigQueryInitialHashPartitions(); + this.metadata = checkNotNull(metadata, "metadata is null"); + this.sqlParser = checkNotNull(sqlParser, "sqlParser is null"); + this.locationFactory = checkNotNull(locationFactory, "locationFactory is null"); + this.splitManager = checkNotNull(splitManager, "splitManager is null"); + this.nodeScheduler = checkNotNull(nodeScheduler, "nodeScheduler is null"); + this.planOptimizers = checkNotNull(planOptimizers, "planOptimizers is null"); + this.remoteTaskFactory = checkNotNull(remoteTaskFactory, "remoteTaskFactory is null"); + checkNotNull(featuresConfig, "featuresConfig is null"); + this.experimentalSyntaxEnabled = featuresConfig.isExperimentalSyntaxEnabled(); + this.executor = checkNotNull(executor, "executor is null"); + this.nodeTaskMap = checkNotNull(nodeTaskMap, "nodeTaskMap is null"); + this.nodeManager = checkNotNull(nodeManager, "nodeManager is null"); + } + + @Override + public SqlQueryExecution createQueryExecution(QueryId queryId, String query, Session session, Statement statement) + { + int initialHashPartitions = this.initialHashPartitions; + if (isBigQueryEnabled(session, false)) { + initialHashPartitions = (bigQueryInitialHashPartitions == null) ? nodeManager.getActiveNodes().size() : bigQueryInitialHashPartitions; + } + initialHashPartitions = getHashPartitionCount(session, initialHashPartitions); + + SqlQueryExecution queryExecution = new SqlQueryExecution(queryId, + query, + session, + locationFactory.createQueryLocation(queryId), + statement, + metadata, + sqlParser, + splitManager, + nodeScheduler, + planOptimizers, + remoteTaskFactory, + locationFactory, + scheduleSplitBatchSize, + initialHashPartitions, + experimentalSyntaxEnabled, + executor, + nodeTaskMap); + + return queryExecution; + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/SqlQueryManager.java b/presto-main/src/main/java/com/facebook/presto/execution/SqlQueryManager.java new file mode 100644 index 00000000..26a06d04 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/SqlQueryManager.java @@ -0,0 +1,455 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.Session; +import com.facebook.presto.event.query.QueryMonitor; +import com.facebook.presto.execution.QueryExecution.QueryExecutionFactory; +import com.facebook.presto.memory.ClusterMemoryManager; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.sql.parser.ParsingException; +import com.facebook.presto.sql.parser.SqlParser; +import com.facebook.presto.sql.tree.Statement; +import io.airlift.concurrent.ThreadPoolExecutorMBean; +import io.airlift.log.Logger; +import io.airlift.units.Duration; +import org.joda.time.DateTime; +import org.weakref.jmx.Flatten; +import org.weakref.jmx.Managed; +import org.weakref.jmx.Nested; + +import javax.annotation.PreDestroy; +import javax.annotation.concurrent.ThreadSafe; +import javax.inject.Inject; + +import java.net.URI; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Optional; +import java.util.Queue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import static com.facebook.presto.execution.QueryState.RUNNING; +import static com.facebook.presto.spi.StandardErrorCode.NOT_SUPPORTED; +import static com.facebook.presto.spi.StandardErrorCode.QUERY_QUEUE_FULL; +import static com.facebook.presto.spi.StandardErrorCode.SERVER_SHUTTING_DOWN; +import static com.facebook.presto.spi.StandardErrorCode.USER_CANCELED; +import static com.facebook.presto.util.ImmutableCollectors.toImmutableList; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.airlift.concurrent.Threads.threadsNamed; +import static java.util.Objects.requireNonNull; +import static java.util.concurrent.Executors.newCachedThreadPool; + +@ThreadSafe +public class SqlQueryManager + implements QueryManager +{ + private static final Logger log = Logger.get(SqlQueryManager.class); + + private final SqlParser sqlParser; + + private final ExecutorService queryExecutor; + private final ThreadPoolExecutorMBean queryExecutorMBean; + private final QueryQueueManager queueManager; + private final ClusterMemoryManager memoryManager; + + private final int maxQueryHistory; + private final Duration maxQueryAge; + + private final ConcurrentMap queries = new ConcurrentHashMap<>(); + private final Queue expirationQueue = new LinkedBlockingQueue<>(); + + private final Duration clientTimeout; + + private final ScheduledExecutorService queryManagementExecutor; + private final ThreadPoolExecutorMBean queryManagementExecutorMBean; + + private final QueryMonitor queryMonitor; + private final LocationFactory locationFactory; + private final QueryIdGenerator queryIdGenerator; + + private final Map, QueryExecutionFactory> executionFactories; + + private final SqlQueryManagerStats stats = new SqlQueryManagerStats(); + + @Inject + public SqlQueryManager( + SqlParser sqlParser, + QueryManagerConfig config, + QueryMonitor queryMonitor, + QueryQueueManager queueManager, + ClusterMemoryManager memoryManager, + QueryIdGenerator queryIdGenerator, + LocationFactory locationFactory, + Map, QueryExecutionFactory> executionFactories) + { + this.sqlParser = checkNotNull(sqlParser, "sqlParser is null"); + + this.executionFactories = checkNotNull(executionFactories, "executionFactories is null"); + + this.queryExecutor = newCachedThreadPool(threadsNamed("query-scheduler-%s")); + this.queryExecutorMBean = new ThreadPoolExecutorMBean((ThreadPoolExecutor) queryExecutor); + + checkNotNull(config, "config is null"); + this.queueManager = checkNotNull(queueManager, "queueManager is null"); + this.memoryManager = requireNonNull(memoryManager, "memoryManager is null"); + + this.queryMonitor = checkNotNull(queryMonitor, "queryMonitor is null"); + this.locationFactory = checkNotNull(locationFactory, "locationFactory is null"); + this.queryIdGenerator = checkNotNull(queryIdGenerator, "queryIdGenerator is null"); + + this.maxQueryAge = config.getMaxQueryAge(); + this.maxQueryHistory = config.getMaxQueryHistory(); + this.clientTimeout = config.getClientTimeout(); + + queryManagementExecutor = Executors.newScheduledThreadPool(config.getQueryManagerExecutorPoolSize(), threadsNamed("query-management-%s")); + queryManagementExecutorMBean = new ThreadPoolExecutorMBean((ThreadPoolExecutor) queryManagementExecutor); + queryManagementExecutor.scheduleWithFixedDelay(new Runnable() + { + @Override + public void run() + { + try { + failAbandonedQueries(); + } + catch (Throwable e) { + log.warn(e, "Error cancelling abandoned queries"); + } + + try { + enforceMemoryLimits(); + } + catch (Throwable e) { + log.warn(e, "Error enforcing memory limits"); + } + + try { + removeExpiredQueries(); + } + catch (Throwable e) { + log.warn(e, "Error removing expired queries"); + } + + try { + pruneExpiredQueries(); + } + catch (Throwable e) { + log.warn(e, "Error pruning expired queries"); + } + } + }, 1, 1, TimeUnit.SECONDS); + } + + @PreDestroy + public void stop() + { + boolean queryCancelled = false; + for (QueryExecution queryExecution : queries.values()) { + QueryInfo queryInfo = queryExecution.getQueryInfo(); + if (queryInfo.getState().isDone()) { + continue; + } + + log.info("Server shutting down. Query %s has been cancelled", queryExecution.getQueryInfo().getQueryId()); + queryExecution.fail(new PrestoException(SERVER_SHUTTING_DOWN, "Server is shutting down. Query " + queryInfo.getQueryId() + " has been cancelled")); + queryCancelled = true; + } + if (queryCancelled) { + try { + TimeUnit.SECONDS.sleep(5); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + queryManagementExecutor.shutdownNow(); + queryExecutor.shutdownNow(); + } + + @Override + public List getAllQueryInfo() + { + return queries.values().stream() + .map(queryExecution -> { + try { + return queryExecution.getQueryInfo(); + } + catch (RuntimeException ignored) { + return null; + } + }) + .filter(Objects::nonNull) + .collect(toImmutableList()); + } + + @Override + public Duration waitForStateChange(QueryId queryId, QueryState currentState, Duration maxWait) + throws InterruptedException + { + checkNotNull(queryId, "queryId is null"); + checkNotNull(maxWait, "maxWait is null"); + + QueryExecution query = queries.get(queryId); + if (query == null) { + return maxWait; + } + + return query.waitForStateChange(currentState, maxWait); + } + + @Override + public QueryInfo getQueryInfo(QueryId queryId) + { + checkNotNull(queryId, "queryId is null"); + + QueryExecution query = queries.get(queryId); + if (query == null) { + throw new NoSuchElementException(); + } + + return query.getQueryInfo(); + } + + @Override + public Optional getQueryState(QueryId queryId) + { + checkNotNull(queryId, "queryId is null"); + + return Optional.ofNullable(queries.get(queryId)) + .map(QueryExecution::getState); + } + + @Override + public void recordHeartbeat(QueryId queryId) + { + checkNotNull(queryId, "queryId is null"); + + QueryExecution query = queries.get(queryId); + if (query == null) { + return; + } + + query.recordHeartbeat(); + } + + @Override + public QueryInfo createQuery(Session session, String query) + { + checkNotNull(query, "query is null"); + checkArgument(!query.isEmpty(), "query must not be empty string"); + + QueryId queryId = queryIdGenerator.createNextQueryId(); + + Statement statement; + QueryExecutionFactory queryExecutionFactory; + try { + statement = sqlParser.createStatement(query); + queryExecutionFactory = executionFactories.get(statement.getClass()); + if (queryExecutionFactory == null) { + throw new PrestoException(NOT_SUPPORTED, "Unsupported statement type: " + statement.getClass().getSimpleName()); + } + } + catch (ParsingException | PrestoException e) { + // This is intentionally not a method, since after the state change listener is registered + // it's not safe to do any of this, and we had bugs before where people reused this code in a method + URI self = locationFactory.createQueryLocation(queryId); + QueryExecution execution = new FailedQueryExecution(queryId, query, session, self, queryExecutor, e); + + queries.put(queryId, execution); + queryMonitor.createdEvent(execution.getQueryInfo()); + queryMonitor.completionEvent(execution.getQueryInfo()); + stats.queryFinished(execution.getQueryInfo()); + expirationQueue.add(execution); + + return execution.getQueryInfo(); + } + + QueryExecution queryExecution = queryExecutionFactory.createQueryExecution(queryId, query, session, statement); + queryMonitor.createdEvent(queryExecution.getQueryInfo()); + + queryExecution.addStateChangeListener(newValue -> { + if (newValue.isDone()) { + QueryInfo info = queryExecution.getQueryInfo(); + + stats.queryFinished(info); + queryMonitor.completionEvent(info); + expirationQueue.add(queryExecution); + } + }); + + queries.put(queryId, queryExecution); + + // start the query in the background + if (!queueManager.submit(queryExecution, queryExecutor, stats)) { + queryExecution.fail(new PrestoException(QUERY_QUEUE_FULL, "Too many queued queries!")); + } + + return queryExecution.getQueryInfo(); + } + + @Override + public void cancelQuery(QueryId queryId) + { + checkNotNull(queryId, "queryId is null"); + + log.debug("Cancel query %s", queryId); + + QueryExecution query = queries.get(queryId); + if (query != null) { + query.fail(new PrestoException(USER_CANCELED, "Query was canceled")); + } + } + + @Override + public void cancelStage(StageId stageId) + { + checkNotNull(stageId, "stageId is null"); + + log.debug("Cancel stage %s", stageId); + + QueryExecution query = queries.get(stageId.getQueryId()); + if (query != null) { + query.cancelStage(stageId); + } + } + + @Managed + @Flatten + public SqlQueryManagerStats getStats() + { + return stats; + } + + @Managed(description = "Query scheduler executor") + @Nested + public ThreadPoolExecutorMBean getExecutor() + { + return queryExecutorMBean; + } + + @Managed(description = "Query garbage collector executor") + @Nested + public ThreadPoolExecutorMBean getManagementExecutor() + { + return queryManagementExecutorMBean; + } + + /** + * Enforce memory limits at the query level + */ + public void enforceMemoryLimits() + { + memoryManager.process(queries.values().stream() + .filter(query -> query.getQueryInfo().getState() == RUNNING) + .collect(toImmutableList())); + } + + /** + * Prune extraneous info from old queries + */ + private void pruneExpiredQueries() + { + if (expirationQueue.size() <= maxQueryHistory) { + return; + } + + int count = 0; + // we're willing to keep full info for up to maxQueryHistory queries + for (QueryExecution query : expirationQueue) { + if (expirationQueue.size() - count <= maxQueryHistory) { + break; + } + query.pruneInfo(); + count++; + } + } + + /** + * Remove completed queries after a waiting period + */ + private void removeExpiredQueries() + { + DateTime timeHorizon = DateTime.now().minus(maxQueryAge.toMillis()); + + // we're willing to keep queries beyond timeHorizon as long as we have fewer than maxQueryHistory + while (expirationQueue.size() > maxQueryHistory) { + QueryInfo queryInfo = expirationQueue.peek().getQueryInfo(); + + // expirationQueue is FIFO based on query end time. Stop when we see the + // first query that's too young to expire + if (queryInfo.getQueryStats().getEndTime().isAfter(timeHorizon)) { + return; + } + + // only expire them if they are older than maxQueryAge. We need to keep them + // around for a while in case clients come back asking for status + QueryId queryId = queryInfo.getQueryId(); + + log.debug("Remove query %s", queryId); + queries.remove(queryId); + expirationQueue.remove(); + } + } + + public void failAbandonedQueries() + { + for (QueryExecution queryExecution : queries.values()) { + QueryInfo queryInfo = queryExecution.getQueryInfo(); + if (queryInfo.getState().isDone()) { + continue; + } + + if (isAbandoned(queryExecution)) { + log.info("Failing abandoned query %s", queryExecution.getQueryInfo().getQueryId()); + queryExecution.fail(new AbandonedException("Query " + queryInfo.getQueryId(), queryInfo.getQueryStats().getLastHeartbeat(), DateTime.now())); + } + } + } + + private boolean isAbandoned(QueryExecution query) + { + DateTime oldestAllowedHeartbeat = DateTime.now().minus(clientTimeout.toMillis()); + DateTime lastHeartbeat = query.getQueryInfo().getQueryStats().getLastHeartbeat(); + + return lastHeartbeat != null && lastHeartbeat.isBefore(oldestAllowedHeartbeat); + } + + /** + * Set up a callback to fire when a query is completed. The callback will be called at most once. + */ + static void addCompletionCallback(QueryExecution queryExecution, Runnable callback) + { + AtomicBoolean taskExecuted = new AtomicBoolean(); + queryExecution.addStateChangeListener(newValue -> { + if (newValue.isDone() && taskExecuted.compareAndSet(false, true)) { + callback.run(); + } + }); + // Need to do this check in case the state changed before we added the previous state change listener + if (queryExecution.getQueryInfo().getState().isDone() && taskExecuted.compareAndSet(false, true)) { + callback.run(); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/SqlQueryManagerStats.java b/presto-main/src/main/java/com/facebook/presto/execution/SqlQueryManagerStats.java new file mode 100644 index 00000000..c4daf1fc --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/SqlQueryManagerStats.java @@ -0,0 +1,189 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.spi.StandardErrorCode; +import io.airlift.stats.CounterStat; +import io.airlift.stats.DistributionStat; +import io.airlift.stats.TimeStat; +import org.weakref.jmx.Managed; +import org.weakref.jmx.Nested; + +import java.util.concurrent.atomic.AtomicInteger; + +import static com.facebook.presto.spi.StandardErrorCode.ABANDONED_QUERY; +import static com.facebook.presto.spi.StandardErrorCode.USER_CANCELED; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +public class SqlQueryManagerStats +{ + private final AtomicInteger runningQueries = new AtomicInteger(); + private final CounterStat startedQueries = new CounterStat(); + private final CounterStat completedQueries = new CounterStat(); + private final CounterStat failedQueries = new CounterStat(); + private final CounterStat abandonedQueries = new CounterStat(); + private final CounterStat canceledQueries = new CounterStat(); + private final CounterStat userErrorFailures = new CounterStat(); + private final CounterStat internalFailures = new CounterStat(); + private final CounterStat externalFailures = new CounterStat(); + private final CounterStat insufficientResourcesFailures = new CounterStat(); + private final TimeStat executionTime = new TimeStat(MILLISECONDS); + private final DistributionStat wallInputBytesRate = new DistributionStat(); + private final DistributionStat cpuInputByteRate = new DistributionStat(); + + public void queryStarted() + { + startedQueries.update(1); + runningQueries.incrementAndGet(); + } + + public void queryStopped() + { + runningQueries.decrementAndGet(); + } + + public void queryFinished(QueryInfo info) + { + completedQueries.update(1); + + long rawInputBytes = info.getQueryStats().getRawInputDataSize().toBytes(); + + long executionWallMillis = info.getQueryStats().getEndTime().getMillis() - info.getQueryStats().getCreateTime().getMillis(); + executionTime.add(executionWallMillis, MILLISECONDS); + if (executionWallMillis > 0) { + wallInputBytesRate.add(rawInputBytes * 1000 / executionWallMillis); + } + + long executionCpuMillis = info.getQueryStats().getTotalCpuTime().toMillis(); + if (executionCpuMillis > 0) { + cpuInputByteRate.add(rawInputBytes * 1000 / executionCpuMillis); + } + + if (info.getErrorCode() != null) { + switch (StandardErrorCode.toErrorType(info.getErrorCode().getCode())) { + case USER_ERROR: + userErrorFailures.update(1); + break; + case INTERNAL_ERROR: + internalFailures.update(1); + break; + case INSUFFICIENT_RESOURCES: + insufficientResourcesFailures.update(1); + break; + case EXTERNAL: + externalFailures.update(1); + break; + } + + if (info.getErrorCode().getCode() == ABANDONED_QUERY.toErrorCode().getCode()) { + abandonedQueries.update(1); + } + else if (info.getErrorCode().getCode() == USER_CANCELED.toErrorCode().getCode()) { + canceledQueries.update(1); + } + failedQueries.update(1); + } + } + + @Managed + public long getRunningQueries() + { + // This is not startedQueries - completeQueries, since queries can finish without ever starting (cancelled before started, for example) + return runningQueries.get(); + } + + @Managed + @Nested + public CounterStat getStartedQueries() + { + return startedQueries; + } + + @Managed + @Nested + public CounterStat getCompletedQueries() + { + return completedQueries; + } + + @Managed + @Nested + public CounterStat getFailedQueries() + { + return failedQueries; + } + + @Managed + @Nested + public TimeStat getExecutionTime() + { + return executionTime; + } + + @Managed + @Nested + public CounterStat getUserErrorFailures() + { + return userErrorFailures; + } + + @Managed + @Nested + public CounterStat getInternalFailures() + { + return internalFailures; + } + + @Managed + @Nested + public CounterStat getAbandonedQueries() + { + return abandonedQueries; + } + + @Managed + @Nested + public CounterStat getCanceledQueries() + { + return canceledQueries; + } + + @Managed + @Nested + public CounterStat getExternalFailures() + { + return externalFailures; + } + + @Managed + @Nested + public CounterStat getInsufficientResourcesFailures() + { + return insufficientResourcesFailures; + } + + @Managed(description = "Distribution of query input data rates (wall)") + @Nested + public DistributionStat getWallInputBytesRate() + { + return wallInputBytesRate; + } + + @Managed(description = "Distribution of query input data rates (cpu)") + @Nested + public DistributionStat getCpuInputByteRate() + { + return cpuInputByteRate; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/SqlQueryQueueManager.java b/presto-main/src/main/java/com/facebook/presto/execution/SqlQueryQueueManager.java new file mode 100644 index 00000000..dff5b250 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/SqlQueryQueueManager.java @@ -0,0 +1,353 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.Session; +import com.facebook.presto.spi.PrestoException; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Joiner; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import org.jgrapht.DirectedGraph; +import org.jgrapht.GraphPath; +import org.jgrapht.Graphs; +import org.jgrapht.alg.FloydWarshallShortestPaths; +import org.jgrapht.graph.DefaultEdge; +import org.jgrapht.graph.DirectedPseudograph; +import org.weakref.jmx.MBeanExporter; +import org.weakref.jmx.ObjectNames; + +import javax.annotation.Nullable; +import javax.annotation.PreDestroy; +import javax.annotation.concurrent.ThreadSafe; +import javax.inject.Inject; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executor; +import java.util.regex.Pattern; + +import static com.facebook.presto.SystemSessionProperties.BIG_QUERY; +import static com.facebook.presto.execution.QueuedExecution.createQueuedExecution; +import static com.facebook.presto.spi.StandardErrorCode.USER_ERROR; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.lang.String.format; + +@ThreadSafe +public class SqlQueryQueueManager + implements QueryQueueManager +{ + private final ConcurrentMap queryQueues = new ConcurrentHashMap<>(); + private final List rules; + private final MBeanExporter mbeanExporter; + + @Inject + public SqlQueryQueueManager(QueryManagerConfig config, ObjectMapper mapper, MBeanExporter mbeanExporter) + { + checkNotNull(config, "config is null"); + this.mbeanExporter = checkNotNull(mbeanExporter, "mbeanExporter is null"); + + ImmutableList.Builder rules = ImmutableList.builder(); + if (config.getQueueConfigFile() == null) { + QueryQueueDefinition global = new QueryQueueDefinition("global", config.getMaxConcurrentQueries(), config.getMaxQueuedQueries()); + QueryQueueDefinition big = new QueryQueueDefinition("big", config.getMaxConcurrentBigQueries(), config.getMaxQueuedBigQueries()); + rules.add(new QueryQueueRule(null, null, ImmutableMap.of(BIG_QUERY, Pattern.compile("true", Pattern.CASE_INSENSITIVE)), ImmutableList.of(big))); + rules.add(new QueryQueueRule(null, null, ImmutableMap.of(), ImmutableList.of(global))); + } + else { + File file = new File(config.getQueueConfigFile()); + ManagerSpec managerSpec; + try { + managerSpec = mapper.readValue(file, ManagerSpec.class); + } + catch (IOException e) { + throw Throwables.propagate(e); + } + Map definitions = new HashMap<>(); + for (Map.Entry queue : managerSpec.getQueues().entrySet()) { + definitions.put(queue.getKey(), new QueryQueueDefinition(queue.getKey(), queue.getValue().getMaxConcurrent(), queue.getValue().getMaxQueued())); + } + + for (RuleSpec rule : managerSpec.getRules()) { + rules.add(QueryQueueRule.createRule(rule.getUserRegex(), rule.getSourceRegex(), rule.getSessionPropertyRegexes(), rule.getQueues(), definitions)); + } + } + this.rules = rules.build(); + checkIsDAG(this.rules); + } + + private static void checkIsDAG(List rules) + { + DirectedPseudograph graph = new DirectedPseudograph<>(DefaultEdge.class); + for (QueryQueueRule rule : rules) { + String lastQueueName = null; + for (QueryQueueDefinition queue : rule.getQueues()) { + String currentQueueName = queue.getTemplate(); + graph.addVertex(currentQueueName); + if (lastQueueName != null) { + graph.addEdge(lastQueueName, currentQueueName); + } + lastQueueName = currentQueueName; + } + } + + List shortestCycle = shortestCycle(graph); + + if (shortestCycle != null) { + String s = Joiner.on(", ").join(shortestCycle); + throw new IllegalArgumentException(format("Queues must not contain a cycle. The shortest cycle found is [%s]", s)); + } + } + + private static List shortestCycle(DirectedGraph graph) + { + FloydWarshallShortestPaths floyd = new FloydWarshallShortestPaths<>(graph); + int minDistance = Integer.MAX_VALUE; + String minSource = null; + String minDestination = null; + for (DefaultEdge edge : graph.edgeSet()) { + String src = graph.getEdgeSource(edge); + String dst = graph.getEdgeTarget(edge); + int dist = (int) Math.round(floyd.shortestDistance(dst, src)); // from dst to src + if (dist < 0) { + continue; + } + if (dist < minDistance) { + minDistance = dist; + minSource = src; + minDestination = dst; + } + } + if (minSource == null) { + return null; + } + GraphPath shortestPath = floyd.getShortestPath(minDestination, minSource); + List pathVertexList = Graphs.getPathVertexList(shortestPath); + // note: pathVertexList will be [a, a] instead of [a] when the shortest path is a loop edge + if (!Objects.equals(shortestPath.getStartVertex(), shortestPath.getEndVertex())) { + pathVertexList.add(pathVertexList.get(0)); + } + return pathVertexList; + } + + @Override + public boolean submit(QueryExecution queryExecution, Executor executor, SqlQueryManagerStats stats) + { + List queues = selectQueues(queryExecution.getQueryInfo().getSession(), executor); + + for (QueryQueue queue : queues) { + if (!queue.reserve(queryExecution)) { + // Reject query if we couldn't acquire a permit to enter the queue. + // The permits will be released when this query fails. + return false; + } + } + + return queues.get(0).enqueue(createQueuedExecution(queryExecution, queues.subList(1, queues.size()), executor, stats)); + } + + // Queues returned have already been created and added queryQueues + private List selectQueues(Session session, Executor executor) + { + for (QueryQueueRule rule : rules) { + List definitions = rule.match(session); + if (definitions != null) { + return getOrCreateQueues(session, executor, definitions); + } + } + throw new PrestoException(USER_ERROR, "Query did not match any queuing rule"); + } + + private List getOrCreateQueues(Session session, Executor executor, List definitions) + { + ImmutableList.Builder queues = ImmutableList.builder(); + for (QueryQueueDefinition definition : definitions) { + String expandedName = definition.getExpandedTemplate(session); + QueueKey key = new QueueKey(definition, expandedName); + if (!queryQueues.containsKey(key)) { + QueryQueue queue = new QueryQueue(executor, definition.getMaxQueued(), definition.getMaxConcurrent()); + if (queryQueues.putIfAbsent(key, queue) == null) { + // Export the mbean, after checking for races + String objectName = ObjectNames.builder(QueryQueue.class, definition.getTemplate()).withProperty("expansion", expandedName).build(); + mbeanExporter.export(objectName, queue); + } + } + queues.add(queryQueues.get(key)); + } + return queues.build(); + } + + @PreDestroy + public void destroy() + { + for (QueueKey key : queryQueues.keySet()) { + String objectName = ObjectNames.builder(QueryQueue.class, key.getQueue().getTemplate()).withProperty("expansion", key.getName()).build(); + mbeanExporter.unexport(objectName); + } + } + + private static class QueueKey + { + private final QueryQueueDefinition queue; + private final String name; + + private QueueKey(QueryQueueDefinition queue, String name) + { + this.queue = checkNotNull(queue, "queue is null"); + this.name = checkNotNull(name, "name is null"); + } + + public QueryQueueDefinition getQueue() + { + return queue; + } + + public String getName() + { + return name; + } + + @Override + public boolean equals(Object other) + { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + + QueueKey queueKey = (QueueKey) other; + + return Objects.equals(name, queueKey.name) && Objects.equals(queue.getTemplate(), queueKey.queue.getTemplate()); + } + + @Override + public int hashCode() + { + return Objects.hash(queue.getTemplate(), name); + } + } + + public static class ManagerSpec + { + private final Map queues; + private final List rules; + + @JsonCreator + public ManagerSpec( + @JsonProperty("queues") Map queues, + @JsonProperty("rules") List rules) + { + this.queues = ImmutableMap.copyOf(checkNotNull(queues, "queues is null")); + this.rules = ImmutableList.copyOf(checkNotNull(rules, "rules is null")); + } + + public Map getQueues() + { + return queues; + } + + public List getRules() + { + return rules; + } + } + + public static class QueueSpec + { + private final int maxQueued; + private final int maxConcurrent; + + @JsonCreator + public QueueSpec( + @JsonProperty("maxQueued") int maxQueued, + @JsonProperty("maxConcurrent") int maxConcurrent) + { + this.maxQueued = maxQueued; + this.maxConcurrent = maxConcurrent; + } + + public int getMaxQueued() + { + return maxQueued; + } + + public int getMaxConcurrent() + { + return maxConcurrent; + } + } + + public static class RuleSpec + { + @Nullable + private final Pattern userRegex; + @Nullable + private final Pattern sourceRegex; + private final Map sessionPropertyRegexes = new HashMap<>(); + private final List queues; + + @JsonCreator + public RuleSpec( + @JsonProperty("user") @Nullable Pattern userRegex, + @JsonProperty("source") @Nullable Pattern sourceRegex, + @JsonProperty("queues") List queues) + { + this.userRegex = userRegex; + this.sourceRegex = sourceRegex; + this.queues = ImmutableList.copyOf(queues); + } + + @JsonAnySetter + public void setSessionProperty(String property, Pattern value) + { + checkArgument(property.startsWith("session."), "Unrecognized property: %s", property); + sessionPropertyRegexes.put(property.substring("session.".length(), property.length()), value); + } + + @Nullable + public Pattern getUserRegex() + { + return userRegex; + } + + @Nullable + public Pattern getSourceRegex() + { + return sourceRegex; + } + + public Map getSessionPropertyRegexes() + { + return ImmutableMap.copyOf(sessionPropertyRegexes); + } + + public List getQueues() + { + return queues; + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/SqlStageExecution.java b/presto-main/src/main/java/com/facebook/presto/execution/SqlStageExecution.java new file mode 100644 index 00000000..4190924f --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/SqlStageExecution.java @@ -0,0 +1,830 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.HashPagePartitionFunction; +import com.facebook.presto.OutputBuffers; +import com.facebook.presto.PagePartitionFunction; +import com.facebook.presto.Session; +import com.facebook.presto.UnpartitionedPagePartitionFunction; +import com.facebook.presto.execution.NodeScheduler.NodeSelector; +import com.facebook.presto.execution.StateMachine.StateChangeListener; +import com.facebook.presto.metadata.Split; +import com.facebook.presto.spi.Node; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.StandardErrorCode; +import com.facebook.presto.split.RemoteSplit; +import com.facebook.presto.split.SplitSource; +import com.facebook.presto.sql.planner.PlanFragment; +import com.facebook.presto.sql.planner.PlanFragment.OutputPartitioning; +import com.facebook.presto.sql.planner.PlanFragment.PlanDistribution; +import com.facebook.presto.sql.planner.StageExecutionPlan; +import com.facebook.presto.sql.planner.plan.PlanFragmentId; +import com.facebook.presto.sql.planner.plan.PlanNodeId; +import com.facebook.presto.sql.planner.plan.RemoteSourceNode; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Functions; +import com.google.common.base.Throwables; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.collect.Multimap; +import com.google.common.collect.Sets; +import io.airlift.concurrent.SetThreadName; + +import javax.annotation.concurrent.GuardedBy; +import javax.annotation.concurrent.ThreadSafe; + +import java.net.URI; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static com.facebook.presto.OutputBuffers.INITIAL_EMPTY_OUTPUT_BUFFERS; +import static com.facebook.presto.spi.StandardErrorCode.NO_NODES_AVAILABLE; +import static com.facebook.presto.util.Failures.checkCondition; +import static com.facebook.presto.util.ImmutableCollectors.toImmutableList; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Predicates.equalTo; +import static com.google.common.collect.Iterables.any; +import static com.google.common.collect.Sets.newConcurrentHashSet; +import static io.airlift.concurrent.MoreFutures.getFutureValue; +import static io.airlift.http.client.HttpUriBuilder.uriBuilderFrom; + +@ThreadSafe +public final class SqlStageExecution +{ + private final PlanFragment fragment; + private final Set allSources; + private final Map subStages; + + private final Multimap localNodeTaskMap = HashMultimap.create(); + private final ConcurrentMap tasks = new ConcurrentHashMap<>(); + + private final Optional dataSource; + private final RemoteTaskFactory remoteTaskFactory; + private final int splitBatchSize; + + private final int initialHashPartitions; + + private final StageStateMachine stateMachine; + + private final Set completeSources = newConcurrentHashSet(); + + @GuardedBy("this") + private OutputBuffers currentOutputBuffers = INITIAL_EMPTY_OUTPUT_BUFFERS; + @GuardedBy("this") + private OutputBuffers nextOutputBuffers; + + private final ExecutorService executor; + + private final NodeSelector nodeSelector; + private final NodeTaskMap nodeTaskMap; + + // Note: atomic is needed to assure thread safety between constructor and scheduler thread + private final AtomicReference> exchangeLocations = new AtomicReference<>(ImmutableMultimap.of()); + + public SqlStageExecution(QueryId queryId, + LocationFactory locationFactory, + StageExecutionPlan plan, + NodeScheduler nodeScheduler, + RemoteTaskFactory remoteTaskFactory, + Session session, + int splitBatchSize, + int initialHashPartitions, + ExecutorService executor, + NodeTaskMap nodeTaskMap, + OutputBuffers nextOutputBuffers) + { + this( + queryId, + new AtomicInteger(), + locationFactory, + plan, + nodeScheduler, + remoteTaskFactory, + session, + splitBatchSize, + initialHashPartitions, + executor, + nodeTaskMap); + + // add a single output buffer + this.nextOutputBuffers = nextOutputBuffers; + } + + private SqlStageExecution( + QueryId queryId, + AtomicInteger nextStageId, + LocationFactory locationFactory, + StageExecutionPlan plan, + NodeScheduler nodeScheduler, + RemoteTaskFactory remoteTaskFactory, + Session session, + int splitBatchSize, + int initialHashPartitions, + ExecutorService executor, + NodeTaskMap nodeTaskMap) + { + checkNotNull(queryId, "queryId is null"); + checkNotNull(nextStageId, "nextStageId is null"); + checkNotNull(locationFactory, "locationFactory is null"); + checkNotNull(plan, "plan is null"); + checkNotNull(nodeScheduler, "nodeScheduler is null"); + checkNotNull(remoteTaskFactory, "remoteTaskFactory is null"); + checkNotNull(session, "session is null"); + checkArgument(initialHashPartitions > 0, "initialHashPartitions must be greater than 0"); + checkNotNull(executor, "executor is null"); + checkNotNull(nodeTaskMap, "nodeTaskMap is null"); + + StageId stageId = new StageId(queryId, String.valueOf(nextStageId.getAndIncrement())); + try (SetThreadName ignored = new SetThreadName("Stage-%s", stageId)) { + this.fragment = plan.getFragment(); + this.dataSource = plan.getDataSource(); + this.remoteTaskFactory = remoteTaskFactory; + this.splitBatchSize = splitBatchSize; + this.initialHashPartitions = initialHashPartitions; + this.executor = executor; + + this.allSources = Stream.concat( + Stream.of(plan.getFragment().getPartitionedSource()), + plan.getFragment().getRemoteSourceNodes().stream() + .map(RemoteSourceNode::getId)) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + + ImmutableMap.Builder subStages = ImmutableMap.builder(); + for (StageExecutionPlan subStagePlan : plan.getSubStages()) { + PlanFragmentId subStageFragmentId = subStagePlan.getFragment().getId(); + SqlStageExecution subStage = new SqlStageExecution( + queryId, + nextStageId, + locationFactory, + subStagePlan, + nodeScheduler, + remoteTaskFactory, + session, + splitBatchSize, + initialHashPartitions, + executor, + nodeTaskMap); + + subStages.put(subStageFragmentId, subStage); + } + this.subStages = subStages.build(); + + String dataSourceName = dataSource.isPresent() ? dataSource.get().getDataSourceName() : null; + this.nodeSelector = nodeScheduler.createNodeSelector(dataSourceName); + this.nodeTaskMap = nodeTaskMap; + this.stateMachine = new StageStateMachine(stageId, locationFactory.createStageLocation(stageId), session, plan.getFragment(), executor); + } + } + + public void cancelStage(StageId stageId) + { + try (SetThreadName ignored = new SetThreadName("Stage-%s", stageId)) { + if (stageId.equals(stateMachine.getStageId())) { + cancel(); + } + else { + for (SqlStageExecution subStage : subStages.values()) { + subStage.cancelStage(stageId); + } + } + } + } + + public StageState getState() + { + return stateMachine.getState(); + } + + public long getTotalMemoryReservation() + { + long memory = 0; + for (RemoteTask task : tasks.values()) { + memory += task.getTaskInfo().getStats().getMemoryReservation().toBytes(); + } + for (SqlStageExecution subStage : subStages.values()) { + memory += subStage.getTotalMemoryReservation(); + } + return memory; + } + + public StageInfo getStageInfo() + { + return stateMachine.getStageInfo( + () -> tasks.values().stream() + .map(RemoteTask::getTaskInfo) + .collect(toImmutableList()), + () -> subStages.values().stream() + .map(SqlStageExecution::getStageInfo) + .collect(toImmutableList())); + } + + public Collection getSubStages() + { + return subStages.values(); + } + + private synchronized void parentTasksAdded(List parentTasks, boolean noMoreParentNodes) + { + checkNotNull(parentTasks, "parentTasks is null"); + + // get the current buffers + OutputBuffers startingOutputBuffers = nextOutputBuffers != null ? nextOutputBuffers : currentOutputBuffers; + + // add new buffers + OutputBuffers newOutputBuffers; + if (fragment.getOutputPartitioning() == OutputPartitioning.NONE) { + ImmutableMap.Builder newBuffers = ImmutableMap.builder(); + for (TaskId taskId : parentTasks) { + newBuffers.put(taskId, new UnpartitionedPagePartitionFunction()); + } + newOutputBuffers = startingOutputBuffers.withBuffers(newBuffers.build()); + + // no more flag + if (noMoreParentNodes) { + newOutputBuffers = newOutputBuffers.withNoMoreBufferIds(); + } + } + else if (fragment.getOutputPartitioning() == OutputPartitioning.HASH) { + checkArgument(noMoreParentNodes, "Hash partitioned output requires all parent nodes be added in a single call"); + + ImmutableMap.Builder buffers = ImmutableMap.builder(); + for (int nodeIndex = 0; nodeIndex < parentTasks.size(); nodeIndex++) { + TaskId taskId = parentTasks.get(nodeIndex); + buffers.put(taskId, new HashPagePartitionFunction(nodeIndex, parentTasks.size(), getPartitioningChannels(fragment), getHashChannel(fragment), fragment.getTypes())); + } + + newOutputBuffers = startingOutputBuffers + .withBuffers(buffers.build()) + .withNoMoreBufferIds(); + } + else { + throw new UnsupportedOperationException("Unsupported output partitioning " + fragment.getOutputPartitioning()); + } + + // only notify scheduler and tasks if the buffers changed + if (newOutputBuffers.getVersion() != startingOutputBuffers.getVersion()) { + this.nextOutputBuffers = newOutputBuffers; + this.notifyAll(); + } + } + + private synchronized OutputBuffers getCurrentOutputBuffers() + { + return currentOutputBuffers; + } + + private synchronized OutputBuffers updateToNextOutputBuffers() + { + if (nextOutputBuffers == null) { + return currentOutputBuffers; + } + + currentOutputBuffers = nextOutputBuffers; + nextOutputBuffers = null; + this.notifyAll(); + return currentOutputBuffers; + } + + public void addStateChangeListener(StateChangeListener stateChangeListener) + { + stateMachine.addStateChangeListener(stateChangeListener::stateChanged); + } + + private Multimap getNewExchangeLocations() + { + Multimap exchangeLocations = this.exchangeLocations.get(); + + ImmutableMultimap.Builder newExchangeLocations = ImmutableMultimap.builder(); + for (RemoteSourceNode remoteSourceNode : fragment.getRemoteSourceNodes()) { + for (PlanFragmentId planFragmentId : remoteSourceNode.getSourceFragmentIds()) { + SqlStageExecution subStage = subStages.get(planFragmentId); + checkState(subStage != null, "Unknown sub stage %s, known stages %s", planFragmentId, subStages.keySet()); + + // add new task locations + for (URI taskLocation : subStage.getTaskLocations()) { + if (!exchangeLocations.containsEntry(remoteSourceNode.getId(), taskLocation)) { + newExchangeLocations.putAll(remoteSourceNode.getId(), taskLocation); + } + } + } + } + return newExchangeLocations.build(); + } + + private synchronized List getTaskLocations() + { + try (SetThreadName ignored = new SetThreadName("Stage-%s", stateMachine.getStageId())) { + ImmutableList.Builder locations = ImmutableList.builder(); + for (RemoteTask task : tasks.values()) { + locations.add(task.getTaskInfo().getSelf()); + } + return locations.build(); + } + } + + @VisibleForTesting + public List getAllTasks() + { + return ImmutableList.copyOf(tasks.values()); + } + + @VisibleForTesting + public List getTasks(Node node) + { + return FluentIterable.from(localNodeTaskMap.get(node)).transform(Functions.forMap(tasks)).toList(); + } + + public Future start() + { + try (SetThreadName ignored = new SetThreadName("Stage-%s", stateMachine.getStageId())) { + return scheduleStartTasks(); + } + } + + private Future scheduleStartTasks() + { + try (SetThreadName ignored = new SetThreadName("Stage-%s", stateMachine.getStageId())) { + // start sub-stages (starts bottom-up) + subStages.values().forEach(SqlStageExecution::scheduleStartTasks); + return executor.submit(this::startTasks); + } + } + + private void startTasks() + { + try (SetThreadName ignored = new SetThreadName("Stage-%s", stateMachine.getStageId())) { + try { + checkState(!Thread.holdsLock(this), "Can not start while holding a lock on this"); + + // transition to scheduling + if (!stateMachine.transitionToScheduling()) { + // stage has already been started, has been canceled or has no tasks due to partition pruning + return; + } + + // schedule tasks + if (fragment.getDistribution() == PlanDistribution.SINGLE) { + scheduleFixedNodeCount(1); + } + else if (fragment.getDistribution() == PlanDistribution.FIXED) { + scheduleFixedNodeCount(initialHashPartitions); + } + else if (fragment.getDistribution() == PlanDistribution.SOURCE) { + scheduleSourcePartitionedNodes(); + } + else if (fragment.getDistribution() == PlanDistribution.COORDINATOR_ONLY) { + scheduleOnCurrentNode(); + } + else { + throw new IllegalStateException("Unsupported partitioning: " + fragment.getDistribution()); + } + + stateMachine.transitionToScheduled(); + + // add the missing exchanges output buffers + updateNewExchangesAndBuffers(true); + } + catch (Throwable e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + + if (stateMachine.transitionToFailed(e)) { + throw Throwables.propagate(e); + } + + // stage is already finished, so only throw if this is an error + Throwables.propagateIfInstanceOf(e, Error.class); + } + finally { + doUpdateState(); + } + } + } + + private void scheduleFixedNodeCount(int nodeCount) + { + // create tasks on "nodeCount" random nodes + List nodes; + if (nodeCount == 1) { + nodes = nodeSelector.selectReportNode(nodeCount); + } + else { + nodes = nodeSelector.selectRandomNodes(nodeCount, PlanDistribution.FIXED); + } + checkCondition(!nodes.isEmpty(), NO_NODES_AVAILABLE, "No worker nodes available"); + ImmutableList.Builder tasks = ImmutableList.builder(); + for (int taskId = 0; taskId < nodes.size(); taskId++) { + Node node = nodes.get(taskId); + RemoteTask task = scheduleTask(taskId, node); + tasks.add(task.getTaskInfo().getTaskId()); + } + + // tell sub stages about all nodes and that there will not be more nodes + for (SqlStageExecution subStage : subStages.values()) { + subStage.parentTasksAdded(tasks.build(), true); + } + } + + private void scheduleOnCurrentNode() + { + // create task on current node + Node node = nodeSelector.selectCurrentNode(); + RemoteTask task = scheduleTask(0, node); + + // tell sub stages about all nodes and that there will not be more nodes + for (SqlStageExecution subStage : subStages.values()) { + subStage.parentTasksAdded(ImmutableList.of(task.getTaskInfo().getTaskId()), true); + } + } + + private void scheduleSourcePartitionedNodes() + throws InterruptedException + { + AtomicInteger nextTaskId = new AtomicInteger(0); + + try (SplitSource splitSource = this.dataSource.get()) { + while (!splitSource.isFinished()) { + // if query has been canceled, exit cleanly; query will never run regardless + if (getState().isDone()) { + break; + } + + long start = System.nanoTime(); + boolean controlScanConcurrencyEnabled = splitSource.isControlScanConcurrencyEnabled(); + int scanConcurrencyCount = splitSource.getScanConcurrencyCount(); + Set pendingSplits = ImmutableSet.copyOf(getFutureValue( + splitSource.getNextBatch(controlScanConcurrencyEnabled ? scanConcurrencyCount : splitBatchSize))); + stateMachine.recordGetSplitTime(start); + + while (!pendingSplits.isEmpty() && !getState().isDone()) { + Multimap splitAssignment = nodeSelector.computeAssignments(pendingSplits, tasks.values(), + PlanDistribution.SOURCE, controlScanConcurrencyEnabled, scanConcurrencyCount); + pendingSplits = ImmutableSet.copyOf(Sets.difference(pendingSplits, ImmutableSet.copyOf(splitAssignment.values()))); + + assignSplits(nextTaskId, splitAssignment); + + if (!pendingSplits.isEmpty()) { + waitForFreeNode(nextTaskId); + } + } + } + } + + for (RemoteTask task : tasks.values()) { + task.noMoreSplits(fragment.getPartitionedSource()); + } + completeSources.add(fragment.getPartitionedSource()); + + // tell sub stages there will be no more output buffers + setNoMoreStageNodes(); + } + + private void assignSplits(AtomicInteger nextTaskId, Multimap splitAssignment) + { + for (Entry> taskSplits : splitAssignment.asMap().entrySet()) { + long scheduleSplitStart = System.nanoTime(); + Node node = taskSplits.getKey(); + + TaskId taskId = Iterables.getOnlyElement(localNodeTaskMap.get(node), null); + RemoteTask task = taskId != null ? tasks.get(taskId) : null; + if (task == null) { + RemoteTask remoteTask = scheduleTask(nextTaskId.getAndIncrement(), node, fragment.getPartitionedSource(), taskSplits.getValue()); + + // tell the sub stages to create a buffer for this task + addStageNode(remoteTask.getTaskInfo().getTaskId()); + + stateMachine.recordScheduleTaskTime(scheduleSplitStart); + } + else { + task.addSplits(fragment.getPartitionedSource(), taskSplits.getValue()); + stateMachine.recordAddSplit(scheduleSplitStart); + } + } + } + + private void waitForFreeNode(AtomicInteger nextTaskId) + { + // if we have sub stages... + if (!subStages.isEmpty()) { + // before we block, we need to create all possible output buffers on the sub stages, or they can deadlock + // waiting for the "noMoreBuffers" call + nodeSelector.lockDownNodes(); + for (Node node : Sets.difference(new HashSet<>(nodeSelector.allNodes()), localNodeTaskMap.keySet())) { + RemoteTask remoteTask = scheduleTask(nextTaskId.getAndIncrement(), node); + + // tell the sub stages to create a buffer for this task + addStageNode(remoteTask.getTaskInfo().getTaskId()); + } + // tell sub stages there will be no more output buffers + setNoMoreStageNodes(); + } + + synchronized (this) { + // otherwise wait for some tasks to complete + try { + // todo this adds latency: replace this wait with an event listener + TimeUnit.MILLISECONDS.timedWait(this, 100); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw Throwables.propagate(e); + } + } + updateNewExchangesAndBuffers(false); + } + + private void addStageNode(TaskId task) + { + for (SqlStageExecution subStage : subStages.values()) { + subStage.parentTasksAdded(ImmutableList.of(task), false); + } + } + + private void setNoMoreStageNodes() + { + for (SqlStageExecution subStage : subStages.values()) { + subStage.parentTasksAdded(ImmutableList.of(), true); + } + } + + private RemoteTask scheduleTask(int id, Node node) + { + return scheduleTask(id, node, null, ImmutableList.of()); + } + + private RemoteTask scheduleTask(int id, Node node, PlanNodeId sourceId, Iterable sourceSplits) + { + // before scheduling a new task update all existing tasks with new exchanges and output buffers + addNewExchangesAndBuffers(); + + TaskId taskId = new TaskId(stateMachine.getStageId(), String.valueOf(id)); + + ImmutableMultimap.Builder initialSplits = ImmutableMultimap.builder(); + for (Split sourceSplit : sourceSplits) { + initialSplits.put(sourceId, sourceSplit); + } + for (Entry entry : exchangeLocations.get().entries()) { + initialSplits.put(entry.getKey(), createRemoteSplitFor(taskId, entry.getValue())); + } + + RemoteTask task = remoteTaskFactory.createRemoteTask(stateMachine.getSession(), + taskId, + node, + fragment, + initialSplits.build(), + getCurrentOutputBuffers()); + + task.addStateChangeListener(taskInfo -> doUpdateState()); + + // create and update task + task.start(); + + // record this task + tasks.put(task.getTaskInfo().getTaskId(), task); + localNodeTaskMap.put(node, task.getTaskInfo().getTaskId()); + nodeTaskMap.addTask(node, task); + + // check whether the stage finished while we were scheduling this task + if (stateMachine.getState().isDone()) { + task.cancel(); + } + + // update in case task finished before listener was registered + doUpdateState(); + + return task; + } + + private void updateNewExchangesAndBuffers(boolean waitUntilFinished) + { + checkState(!Thread.holdsLock(this), "Can not add exchanges or buffers to tasks while holding a lock on this"); + + while (!getState().isDone()) { + boolean finished = addNewExchangesAndBuffers(); + + if (finished || !waitUntilFinished) { + return; + } + + synchronized (this) { + // wait for a state change + // + // NOTE this must be a wait with a timeout since there is no notification + // for new exchanges from the child stages + try { + TimeUnit.MILLISECONDS.timedWait(this, 100); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw Throwables.propagate(e); + } + } + } + } + + private boolean addNewExchangesAndBuffers() + { + // get new exchanges and update exchange state + Set completeSources = updateCompleteSources(); + boolean allSourceComplete = completeSources.containsAll(allSources); + Multimap newExchangeLocations = getNewExchangeLocations(); + exchangeLocations.set(ImmutableMultimap.builder() + .putAll(exchangeLocations.get()) + .putAll(newExchangeLocations) + .build()); + + // get new output buffer and update output buffer state + OutputBuffers outputBuffers = updateToNextOutputBuffers(); + + // finished state must be decided before update to avoid race conditions + boolean finished = allSourceComplete && outputBuffers.isNoMoreBufferIds(); + + // update tasks + try (SetThreadName ignored = new SetThreadName("SqlStageExecution-%s", stateMachine.getStageId())) { + for (RemoteTask task : tasks.values()) { + for (Entry entry : newExchangeLocations.entries()) { + Split remoteSplit = createRemoteSplitFor(task.getTaskInfo().getTaskId(), entry.getValue()); + task.addSplits(entry.getKey(), ImmutableList.of(remoteSplit)); + } + task.setOutputBuffers(outputBuffers); + completeSources.forEach(task::noMoreSplits); + } + } + + return finished; + } + + private Set updateCompleteSources() + { + for (RemoteSourceNode remoteSourceNode : fragment.getRemoteSourceNodes()) { + if (!completeSources.contains(remoteSourceNode.getId())) { + boolean exchangeFinished = true; + for (PlanFragmentId planFragmentId : remoteSourceNode.getSourceFragmentIds()) { + SqlStageExecution subStage = subStages.get(planFragmentId); + switch (subStage.getState()) { + case PLANNED: + case SCHEDULING: + exchangeFinished = false; + break; + } + } + if (exchangeFinished) { + completeSources.add(remoteSourceNode.getId()); + } + } + } + return completeSources; + } + + @SuppressWarnings("NakedNotify") + private void doUpdateState() + { + checkState(!Thread.holdsLock(this), "Can not doUpdateState while holding a lock on this"); + + try (SetThreadName ignored = new SetThreadName("Stage-%s", stateMachine.getStageId())) { + synchronized (this) { + // wake up worker thread waiting for state changes + this.notifyAll(); + + StageState initialState = getState(); + if (initialState.isDone()) { + return; + } + + List taskInfos = tasks.values().stream() + .map(RemoteTask::getTaskInfo) + .collect(toImmutableList()); + + List taskStates = taskInfos.stream() + .map(TaskInfo::getState) + .collect(toImmutableList()); + + if (any(taskStates, equalTo(TaskState.FAILED))) { + RuntimeException failure = taskInfos.stream() + .map(taskInfo -> Iterables.getFirst(taskInfo.getFailures(), null)) + .filter(Objects::nonNull) + .findFirst() + .map(ExecutionFailureInfo::toException) + .orElse(new PrestoException(StandardErrorCode.INTERNAL_ERROR, "A task failed for an unknown reason")); + stateMachine.transitionToFailed(failure); + } + else if (taskStates.stream().anyMatch(TaskState.ABORTED::equals)) { + // A task should only be in the aborted state if the STAGE is done (ABORTED or FAILED) + stateMachine.transitionToFailed(new PrestoException(StandardErrorCode.INTERNAL_ERROR, "A task is in the ABORTED state but stage is " + initialState)); + } + else if (initialState != StageState.PLANNED && initialState != StageState.SCHEDULING) { + // all tasks are now scheduled, so we can check the finished state + if (taskStates.stream().allMatch(TaskState::isDone)) { + stateMachine.transitionToFinished(); + } + else if (taskStates.stream().anyMatch(TaskState.RUNNING::equals)) { + stateMachine.transitionToRunning(); + } + } + } + + // if this stage is now finished, cancel all work + if (getState().isDone()) { + cancel(); + } + } + } + + public void cancel() + { + checkState(!Thread.holdsLock(this), "Can not cancel while holding a lock on this"); + + try (SetThreadName ignored = new SetThreadName("Stage-%s", stateMachine.getStageId())) { + // check if the stage already completed naturally + doUpdateState(); + + stateMachine.transitionToCanceled(); + + // cancel all tasks + tasks.values().forEach(RemoteTask::cancel); + + // propagate cancel to sub-stages + subStages.values().forEach(SqlStageExecution::cancel); + } + } + + public void abort() + { + checkState(!Thread.holdsLock(this), "Can not abort while holding a lock on this"); + + try (SetThreadName ignored = new SetThreadName("Stage-%s", stateMachine.getStageId())) { + // transition to aborted state, only if not already finished + doUpdateState(); + + stateMachine.transitionToAborted(); + + // abort all tasks + tasks.values().forEach(RemoteTask::abort); + + // propagate abort to sub-stages + subStages.values().forEach(SqlStageExecution::abort); + } + } + + private static Split createRemoteSplitFor(TaskId taskId, URI taskLocation) + { + URI splitLocation = uriBuilderFrom(taskLocation).appendPath("results").appendPath(taskId.toString()).build(); + return new Split("remote", new RemoteSplit(splitLocation)); + } + + @Override + public String toString() + { + return stateMachine.toString(); + } + + private static Optional getHashChannel(PlanFragment fragment) + { + return fragment.getHash().map(symbol -> fragment.getOutputLayout().indexOf(symbol)); + } + + private static List getPartitioningChannels(PlanFragment fragment) + { + checkState(fragment.getOutputPartitioning() == OutputPartitioning.HASH, "fragment is not hash partitioned"); + // We can convert the symbols directly into channels, because the root must be a sink and therefore the layout is fixed + return fragment.getPartitionBy().stream() + .map(symbol -> fragment.getOutputLayout().indexOf(symbol)) + .collect(toImmutableList()); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/SqlTask.java b/presto-main/src/main/java/com/facebook/presto/execution/SqlTask.java new file mode 100644 index 00000000..ba1e6b37 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/SqlTask.java @@ -0,0 +1,353 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.OutputBuffers; +import com.facebook.presto.Session; +import com.facebook.presto.TaskSource; +import com.facebook.presto.execution.StateMachine.StateChangeListener; +import com.facebook.presto.memory.QueryContext; +import com.facebook.presto.operator.TaskContext; +import com.facebook.presto.operator.TaskStats; +import com.facebook.presto.sql.planner.PlanFragment; +import com.facebook.presto.sql.planner.plan.PlanNodeId; +import com.google.common.base.Function; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import io.airlift.concurrent.SetThreadName; +import io.airlift.log.Logger; +import io.airlift.units.DataSize; +import org.joda.time.DateTime; + +import javax.annotation.Nullable; + +import java.net.URI; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +import static com.facebook.presto.util.Failures.toFailures; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.Objects.requireNonNull; + +public class SqlTask +{ + private static final Logger log = Logger.get(SqlTask.class); + + private final TaskId taskId; + private final String nodeInstanceId; + private final URI location; + private final TaskStateMachine taskStateMachine; + private final SharedBuffer sharedBuffer; + private final QueryContext queryContext; + + private final SqlTaskExecutionFactory sqlTaskExecutionFactory; + + private final AtomicReference lastHeartbeat = new AtomicReference<>(DateTime.now()); + private final AtomicLong nextTaskInfoVersion = new AtomicLong(TaskInfo.STARTING_VERSION); + + private final AtomicReference taskHolderReference = new AtomicReference<>(new TaskHolder()); + + public SqlTask( + TaskId taskId, + String nodeInstanceId, + URI location, + QueryContext queryContext, + SqlTaskExecutionFactory sqlTaskExecutionFactory, + ExecutorService taskNotificationExecutor, + final Function onDone, + DataSize maxBufferSize) + { + this.taskId = checkNotNull(taskId, "taskId is null"); + this.nodeInstanceId = checkNotNull(nodeInstanceId, "nodeInstanceId is null"); + this.location = checkNotNull(location, "location is null"); + this.queryContext = requireNonNull(queryContext, "queryContext is null"); + this.sqlTaskExecutionFactory = checkNotNull(sqlTaskExecutionFactory, "sqlTaskExecutionFactory is null"); + checkNotNull(taskNotificationExecutor, "taskNotificationExecutor is null"); + checkNotNull(onDone, "onDone is null"); + checkNotNull(maxBufferSize, "maxBufferSize is null"); + + sharedBuffer = new SharedBuffer(taskId, taskNotificationExecutor, maxBufferSize); + taskStateMachine = new TaskStateMachine(taskId, taskNotificationExecutor); + taskStateMachine.addStateChangeListener(new StateChangeListener() + { + @Override + public void stateChanged(TaskState newState) + { + if (!newState.isDone()) { + return; + } + + // store final task info + while (true) { + TaskHolder taskHolder = taskHolderReference.get(); + if (taskHolder.isFinished()) { + // another concurrent worker already set the final state + return; + } + + if (taskHolderReference.compareAndSet(taskHolder, new TaskHolder(createTaskInfo(taskHolder), taskHolder.getIoStats()))) { + break; + } + } + + // make sure buffers are cleaned up + if (newState == TaskState.FAILED || newState == TaskState.ABORTED) { + // don't close buffers for a failed query + // closed buffers signal to upstream tasks that everything finished cleanly + sharedBuffer.fail(); + } + else { + sharedBuffer.destroy(); + } + + try { + onDone.apply(SqlTask.this); + } + catch (Exception e) { + log.warn(e, "Error running task cleanup callback %s", SqlTask.this.taskId); + } + } + }); + } + + public SqlTaskIoStats getIoStats() + { + return taskHolderReference.get().getIoStats(); + } + + public TaskId getTaskId() + { + return taskStateMachine.getTaskId(); + } + + public void recordHeartbeat() + { + lastHeartbeat.set(DateTime.now()); + } + + public TaskInfo getTaskInfo() + { + try (SetThreadName ignored = new SetThreadName("Task-%s", taskId)) { + return createTaskInfo(taskHolderReference.get()); + } + } + + private TaskInfo createTaskInfo(TaskHolder taskHolder) + { + // Always return a new TaskInfo with a larger version number; + // otherwise a client will not accept the update + long versionNumber = nextTaskInfoVersion.getAndIncrement(); + + TaskState state = taskStateMachine.getState(); + List failures = ImmutableList.of(); + if (state == TaskState.FAILED) { + failures = toFailures(taskStateMachine.getFailureCauses()); + } + + TaskStats taskStats; + Set noMoreSplits; + + TaskInfo finalTaskInfo = taskHolder.getFinalTaskInfo(); + if (finalTaskInfo != null) { + taskStats = finalTaskInfo.getStats(); + noMoreSplits = finalTaskInfo.getNoMoreSplits(); + } + else { + SqlTaskExecution taskExecution = taskHolder.getTaskExecution(); + if (taskExecution != null) { + taskStats = taskExecution.getTaskContext().getTaskStats(); + noMoreSplits = taskExecution.getNoMoreSplits(); + } + else { + // if the task completed without creation, set end time + DateTime endTime = state.isDone() ? DateTime.now() : null; + taskStats = new TaskStats(taskStateMachine.getCreatedTime(), endTime); + noMoreSplits = ImmutableSet.of(); + } + } + + return new TaskInfo( + taskStateMachine.getTaskId(), + Optional.of(nodeInstanceId), + versionNumber, + state, + location, + lastHeartbeat.get(), + sharedBuffer.getInfo(), + noMoreSplits, + taskStats, + failures); + } + + public ListenableFuture getTaskInfo(TaskState callersCurrentState) + { + checkNotNull(callersCurrentState, "callersCurrentState is null"); + + // If the caller's current state is already done, just return the current + // state of this task as it will either be done or possibly still running + // (due to a bug in the caller), since we can not transition from a done + // state. + if (callersCurrentState.isDone()) { + return Futures.immediateFuture(getTaskInfo()); + } + + ListenableFuture futureTaskState = taskStateMachine.getStateChange(callersCurrentState); + return Futures.transform(futureTaskState, (TaskState input) -> getTaskInfo()); + } + + public TaskInfo updateTask(Session session, PlanFragment fragment, List sources, OutputBuffers outputBuffers) + { + try { + // assure the task execution is only created once + SqlTaskExecution taskExecution; + synchronized (this) { + // is task already complete? + TaskHolder taskHolder = taskHolderReference.get(); + if (taskHolder.isFinished()) { + return taskHolder.getFinalTaskInfo(); + } + taskExecution = taskHolder.getTaskExecution(); + if (taskExecution == null) { + taskExecution = sqlTaskExecutionFactory.create(session, queryContext, taskStateMachine, sharedBuffer, fragment, sources); + taskHolderReference.compareAndSet(taskHolder, new TaskHolder(taskExecution)); + } + } + + if (taskExecution != null) { + // addSources checks for task completion, so update the buffers first and the task might complete earlier + sharedBuffer.setOutputBuffers(outputBuffers); + taskExecution.addSources(sources); + } + } + catch (Error e) { + failed(e); + throw e; + } + catch (RuntimeException e) { + failed(e); + } + + return getTaskInfo(); + } + + public ListenableFuture getTaskResults(TaskId outputName, long startingSequenceId, DataSize maxSize) + { + checkNotNull(outputName, "outputName is null"); + checkArgument(maxSize.toBytes() > 0, "maxSize must be at least 1 byte"); + + return sharedBuffer.get(outputName, startingSequenceId, maxSize); + } + + public TaskInfo abortTaskResults(TaskId outputId) + { + checkNotNull(outputId, "outputId is null"); + + log.debug("Aborting task %s output %s", taskId, outputId); + sharedBuffer.abort(outputId); + + return getTaskInfo(); + } + + public void failed(Throwable cause) + { + checkNotNull(cause, "cause is null"); + + taskStateMachine.failed(cause); + } + + public TaskInfo cancel() + { + taskStateMachine.cancel(); + return getTaskInfo(); + } + + public TaskInfo abort() + { + taskStateMachine.abort(); + return getTaskInfo(); + } + + @Override + public String toString() + { + return taskId.toString(); + } + + private static final class TaskHolder + { + private final SqlTaskExecution taskExecution; + private final TaskInfo finalTaskInfo; + private final SqlTaskIoStats finalIoStats; + + private TaskHolder() + { + this.taskExecution = null; + this.finalTaskInfo = null; + this.finalIoStats = null; + } + + private TaskHolder(SqlTaskExecution taskExecution) + { + this.taskExecution = checkNotNull(taskExecution, "taskExecution is null"); + this.finalTaskInfo = null; + this.finalIoStats = null; + } + + private TaskHolder(TaskInfo finalTaskInfo, SqlTaskIoStats finalIoStats) + { + this.taskExecution = null; + this.finalTaskInfo = checkNotNull(finalTaskInfo, "finalTaskInfo is null"); + this.finalIoStats = checkNotNull(finalIoStats, "finalIoStats is null"); + } + + public boolean isFinished() + { + return finalTaskInfo != null; + } + + @Nullable + public SqlTaskExecution getTaskExecution() + { + return taskExecution; + } + + @Nullable + public TaskInfo getFinalTaskInfo() + { + return finalTaskInfo; + } + + public SqlTaskIoStats getIoStats() + { + // if we are finished, return the final IoStats + if (finalIoStats != null) { + return finalIoStats; + } + // if we haven't started yet, return an empty IoStats + if (taskExecution == null) { + return new SqlTaskIoStats(); + } + // get IoStats from the current task execution + TaskContext taskContext = taskExecution.getTaskContext(); + return new SqlTaskIoStats(taskContext.getInputDataSize(), taskContext.getInputPositions(), taskContext.getOutputDataSize(), taskContext.getOutputPositions()); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/SqlTaskExecution.java b/presto-main/src/main/java/com/facebook/presto/execution/SqlTaskExecution.java new file mode 100644 index 00000000..c6ffc654 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/SqlTaskExecution.java @@ -0,0 +1,647 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.ScheduledSplit; +import com.facebook.presto.TaskSource; +import com.facebook.presto.event.query.QueryMonitor; +import com.facebook.presto.execution.SharedBuffer.BufferState; +import com.facebook.presto.execution.StateMachine.StateChangeListener; +import com.facebook.presto.execution.TaskExecutor.TaskHandle; +import com.facebook.presto.operator.Driver; +import com.facebook.presto.operator.DriverContext; +import com.facebook.presto.operator.DriverFactory; +import com.facebook.presto.operator.DriverStats; +import com.facebook.presto.operator.OutputFactory; +import com.facebook.presto.operator.PartitionedOutputOperator.PartitionedOutputFactory; +import com.facebook.presto.operator.PipelineContext; +import com.facebook.presto.operator.TaskContext; +import com.facebook.presto.operator.TaskOutputOperator.TaskOutputFactory; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.sql.planner.LocalExecutionPlanner; +import com.facebook.presto.sql.planner.LocalExecutionPlanner.LocalExecutionPlan; +import com.facebook.presto.sql.planner.PlanFragment; +import com.facebook.presto.sql.planner.PlanFragment.OutputPartitioning; +import com.facebook.presto.sql.planner.PlanFragment.PlanDistribution; +import com.facebook.presto.sql.planner.plan.PlanNodeId; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import io.airlift.concurrent.SetThreadName; +import io.airlift.units.Duration; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.GuardedBy; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import static com.facebook.presto.spi.StandardErrorCode.NOT_SUPPORTED; +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static java.lang.Math.max; +import static java.lang.String.format; + +public class SqlTaskExecution +{ + private final TaskId taskId; + private final TaskStateMachine taskStateMachine; + private final TaskContext taskContext; + private final SharedBuffer sharedBuffer; + + private final TaskHandle taskHandle; + private final TaskExecutor taskExecutor; + + private final Executor notificationExecutor; + + private final QueryMonitor queryMonitor; + + private final List> drivers = new CopyOnWriteArrayList<>(); + + /** + * Number of drivers that have been sent to the TaskExecutor that have not finished. + */ + private final AtomicInteger remainingDrivers = new AtomicInteger(); + + // guarded for update only + @GuardedBy("this") + private final ConcurrentMap unpartitionedSources = new ConcurrentHashMap<>(); + + @GuardedBy("this") + private long maxAcknowledgedSplit = Long.MIN_VALUE; + + private final PlanNodeId partitionedSourceId; + private final DriverSplitRunnerFactory partitionedDriverFactory; + + private final List unpartitionedDriverFactories; + + public static SqlTaskExecution createSqlTaskExecution( + TaskStateMachine taskStateMachine, + TaskContext taskContext, + SharedBuffer sharedBuffer, + PlanFragment fragment, + List sources, + LocalExecutionPlanner planner, + TaskExecutor taskExecutor, + Executor notificationExecutor, + QueryMonitor queryMonitor) + { + SqlTaskExecution task = new SqlTaskExecution( + taskStateMachine, taskContext, sharedBuffer, fragment, + planner, + taskExecutor, + queryMonitor, + notificationExecutor + ); + + try (SetThreadName ignored = new SetThreadName("Task-%s", task.getTaskId())) { + task.start(); + task.addSources(sources); + return task; + } + } + + private SqlTaskExecution( + TaskStateMachine taskStateMachine, + TaskContext taskContext, + SharedBuffer sharedBuffer, + PlanFragment fragment, + LocalExecutionPlanner planner, + TaskExecutor taskExecutor, + QueryMonitor queryMonitor, + Executor notificationExecutor) + { + this.taskStateMachine = checkNotNull(taskStateMachine, "taskStateMachine is null"); + this.taskId = taskStateMachine.getTaskId(); + this.taskContext = checkNotNull(taskContext, "taskContext is null"); + this.sharedBuffer = checkNotNull(sharedBuffer, "sharedBuffer is null"); + + this.taskExecutor = checkNotNull(taskExecutor, "driverExecutor is null"); + this.notificationExecutor = checkNotNull(notificationExecutor, "notificationExecutor is null"); + + this.queryMonitor = checkNotNull(queryMonitor, "queryMonitor is null"); + + try (SetThreadName ignored = new SetThreadName("Task-%s", taskId)) { + List driverFactories; + try { + OutputFactory outputOperatorFactory; + if (fragment.getOutputPartitioning() == OutputPartitioning.NONE) { + outputOperatorFactory = new TaskOutputFactory(sharedBuffer); + } + else if (fragment.getOutputPartitioning() == OutputPartitioning.HASH) { + outputOperatorFactory = new PartitionedOutputFactory(sharedBuffer); + } + else { + throw new PrestoException(NOT_SUPPORTED, format("OutputPartitioning %s is not supported", fragment.getOutputPartitioning())); + } + + LocalExecutionPlan localExecutionPlan = planner.plan( + taskContext.getSession(), + fragment.getRoot(), + fragment.getOutputLayout(), + fragment.getSymbols(), + fragment.getDistribution(), + outputOperatorFactory); + driverFactories = localExecutionPlan.getDriverFactories(); + } + catch (Throwable e) { + // planning failed + taskStateMachine.failed(e); + throw Throwables.propagate(e); + } + + // index driver factories + DriverSplitRunnerFactory partitionedDriverFactory = null; + ImmutableList.Builder unpartitionedDriverFactories = ImmutableList.builder(); + for (DriverFactory driverFactory : driverFactories) { + if (driverFactory.getSourceIds().contains(fragment.getPartitionedSource())) { + checkState(partitionedDriverFactory == null, "multiple partitioned sources are not supported"); + partitionedDriverFactory = new DriverSplitRunnerFactory(driverFactory); + } + else { + unpartitionedDriverFactories.add(new DriverSplitRunnerFactory(driverFactory)); + } + } + this.unpartitionedDriverFactories = unpartitionedDriverFactories.build(); + + if (fragment.getDistribution() == PlanDistribution.SOURCE) { + checkArgument(partitionedDriverFactory != null, "Fragment is partitioned, but no partitioned driver found"); + } + this.partitionedSourceId = fragment.getPartitionedSource(); + this.partitionedDriverFactory = partitionedDriverFactory; + + // don't register the task if it is already completed (most likely failed during planning above) + if (!taskStateMachine.getState().isDone()) { + taskHandle = taskExecutor.addTask(taskId); + taskStateMachine.addStateChangeListener(new RemoveTaskHandleWhenDone(taskExecutor, taskHandle)); + } + else { + taskHandle = null; + } + + sharedBuffer.addStateChangeListener(new CheckTaskCompletionOnBufferFinish(SqlTaskExecution.this)); + } + } + + // + // This code starts registers a callback with access to this class, and this + // call back is access from another thread, so this code can not be placed in the constructor + private void start() + { + // start unpartitioned drivers + List runners = new ArrayList<>(); + for (DriverSplitRunnerFactory driverFactory : unpartitionedDriverFactories) { + for (int i = 0; i < driverFactory.getDriverInstances(); i++) { + runners.add(driverFactory.createDriverRunner(null, false)); + } + driverFactory.setNoMoreSplits(); + } + enqueueDrivers(true, runners); + } + + public TaskId getTaskId() + { + return taskId; + } + + public TaskContext getTaskContext() + { + return taskContext; + } + + public void addSources(List sources) + { + checkNotNull(sources, "sources is null"); + checkState(!Thread.holdsLock(this), "Can not add sources while holding a lock on the %s", getClass().getSimpleName()); + + try (SetThreadName ignored = new SetThreadName("Task-%s", taskId)) { + // update our record of sources and schedule drivers for new partitioned splits + Map updatedUnpartitionedSources = updateSources(sources); + + // tell existing drivers about the new splits; it is safe to update drivers + // multiple times and out of order because sources contain full record of + // the unpartitioned splits + for (TaskSource source : updatedUnpartitionedSources.values()) { + // tell all the existing drivers this source is finished + for (WeakReference driverReference : drivers) { + Driver driver = driverReference.get(); + // the driver can be GCed due to a failure or a limit + if (driver != null) { + driver.updateSource(source); + } + else { + // remove the weak reference from the list to avoid a memory leak + // NOTE: this is a concurrent safe operation on a CopyOnWriteArrayList + drivers.remove(driverReference); + } + } + } + + // we may have transitioned to no more splits, so check for completion + checkTaskCompletion(); + } + } + + private synchronized Map updateSources(List sources) + { + Map updatedUnpartitionedSources = new HashMap<>(); + + // don't update maxAcknowledgedSplit until the end because task sources may not + // be in sorted order and if we updated early we could skip splits + long newMaxAcknowledgedSplit = maxAcknowledgedSplit; + + for (TaskSource source : sources) { + PlanNodeId sourceId = source.getPlanNodeId(); + if (sourceId.equals(partitionedSourceId)) { + // partitioned split + ImmutableList.Builder runners = ImmutableList.builder(); + for (ScheduledSplit scheduledSplit : source.getSplits()) { + // only add a split if we have not already scheduled it + if (scheduledSplit.getSequenceId() > maxAcknowledgedSplit) { + // create a new driver for the split + runners.add(partitionedDriverFactory.createDriverRunner(scheduledSplit, true)); + newMaxAcknowledgedSplit = max(scheduledSplit.getSequenceId(), newMaxAcknowledgedSplit); + } + } + + enqueueDrivers(false, runners.build()); + if (source.isNoMoreSplits()) { + partitionedDriverFactory.setNoMoreSplits(); + } + } + else { + // unpartitioned split + + // update newMaxAcknowledgedSplit + for (ScheduledSplit scheduledSplit : source.getSplits()) { + newMaxAcknowledgedSplit = max(scheduledSplit.getSequenceId(), newMaxAcknowledgedSplit); + } + + // create new source + TaskSource newSource; + TaskSource currentSource = unpartitionedSources.get(sourceId); + if (currentSource == null) { + newSource = source; + } + else { + newSource = currentSource.update(source); + } + + // only record new source if something changed + if (newSource != currentSource) { + unpartitionedSources.put(sourceId, newSource); + updatedUnpartitionedSources.put(sourceId, newSource); + } + } + } + + maxAcknowledgedSplit = newMaxAcknowledgedSplit; + return updatedUnpartitionedSources; + } + + private synchronized void enqueueDrivers(boolean forceRunSplit, List runners) + { + // schedule driver to be executed + List> finishedFutures = taskExecutor.enqueueSplits(taskHandle, forceRunSplit, runners); + checkState(finishedFutures.size() == runners.size(), "Expected %s futures but got %s", runners.size(), finishedFutures.size()); + + // record new driver + remainingDrivers.addAndGet(finishedFutures.size()); + + // when driver completes, update state and fire events + for (int i = 0; i < finishedFutures.size(); i++) { + ListenableFuture finishedFuture = finishedFutures.get(i); + final DriverSplitRunner splitRunner = runners.get(i); + Futures.addCallback(finishedFuture, new FutureCallback() + { + @Override + public void onSuccess(Object result) + { + try (SetThreadName ignored = new SetThreadName("Task-%s", taskId)) { + // record driver is finished + remainingDrivers.decrementAndGet(); + + checkTaskCompletion(); + + queryMonitor.splitCompletionEvent(taskId, getDriverStats()); + } + } + + @Override + public void onFailure(Throwable cause) + { + try (SetThreadName ignored = new SetThreadName("Task-%s", taskId)) { + taskStateMachine.failed(cause); + + // record driver is finished + remainingDrivers.decrementAndGet(); + + // fire failed event with cause + queryMonitor.splitFailedEvent(taskId, getDriverStats(), cause); + } + } + + private DriverStats getDriverStats() + { + DriverContext driverContext = splitRunner.getDriverContext(); + DriverStats driverStats; + if (driverContext != null) { + driverStats = driverContext.getDriverStats(); + } + else { + // split runner did not start successfully + driverStats = new DriverStats(); + } + + return driverStats; + } + }, notificationExecutor); + } + } + + public Set getNoMoreSplits() + { + ImmutableSet.Builder noMoreSplits = ImmutableSet.builder(); + if (partitionedDriverFactory != null && partitionedDriverFactory.isNoMoreSplits()) { + noMoreSplits.add(partitionedSourceId); + } + for (TaskSource taskSource : unpartitionedSources.values()) { + if (taskSource.isNoMoreSplits()) { + noMoreSplits.add(taskSource.getPlanNodeId()); + } + } + return noMoreSplits.build(); + } + + private synchronized void checkTaskCompletion() + { + if (taskStateMachine.getState().isDone()) { + return; + } + + // are there more partition splits expected? + if (partitionedDriverFactory != null && !partitionedDriverFactory.isNoMoreSplits()) { + return; + } + // do we still have running tasks? + if (remainingDrivers.get() != 0) { + return; + } + + // no more output will be created + sharedBuffer.setNoMorePages(); + + // are there still pages in the output buffer + if (!sharedBuffer.isFinished()) { + return; + } + + // Cool! All done! + taskStateMachine.finished(); + } + + public void cancel() + { + // todo this should finish all input sources and let the task finish naturally + try (SetThreadName ignored = new SetThreadName("Task-%s", taskId)) { + taskStateMachine.cancel(); + } + } + + public void fail(Throwable cause) + { + try (SetThreadName ignored = new SetThreadName("Task-%s", taskId)) { + taskStateMachine.failed(cause); + } + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("taskId", taskId) + .add("remainingDrivers", remainingDrivers) + .add("unpartitionedSources", unpartitionedSources) + .toString(); + } + + private class DriverSplitRunnerFactory + { + private final DriverFactory driverFactory; + private final PipelineContext pipelineContext; + + private final AtomicInteger pendingCreation = new AtomicInteger(); + private final AtomicBoolean noMoreSplits = new AtomicBoolean(); + + private DriverSplitRunnerFactory(DriverFactory driverFactory) + { + this.driverFactory = driverFactory; + this.pipelineContext = taskContext.addPipelineContext(driverFactory.isInputDriver(), driverFactory.isOutputDriver()); + } + + private DriverSplitRunner createDriverRunner(@Nullable ScheduledSplit partitionedSplit, boolean partitioned) + { + pendingCreation.incrementAndGet(); + // create driver context immediately so the driver existence is recorded in the stats + // the number of drivers is used to balance work across nodes + DriverContext driverContext = pipelineContext.addDriverContext(partitioned); + return new DriverSplitRunner(this, driverContext, partitionedSplit); + } + + private Driver createDriver(DriverContext driverContext, @Nullable ScheduledSplit partitionedSplit) + { + Driver driver = driverFactory.createDriver(driverContext); + + // record driver so other threads add unpartitioned sources can see the driver + // NOTE: this MUST be done before reading unpartitionedSources, so we see a consistent view of the unpartitioned sources + drivers.add(new WeakReference<>(driver)); + + if (partitionedSplit != null) { + // TableScanOperator requires partitioned split to be added before the first call to process + driver.updateSource(new TaskSource(partitionedSourceId, ImmutableSet.of(partitionedSplit), true)); + } + + // add unpartitioned sources + for (TaskSource source : unpartitionedSources.values()) { + driver.updateSource(source); + } + + pendingCreation.decrementAndGet(); + closeDriverFactoryIfFullyCreated(); + + return driver; + } + + private boolean isNoMoreSplits() + { + return noMoreSplits.get(); + } + + private void setNoMoreSplits() + { + noMoreSplits.set(true); + closeDriverFactoryIfFullyCreated(); + } + + private void closeDriverFactoryIfFullyCreated() + { + if (isNoMoreSplits() && pendingCreation.get() <= 0) { + driverFactory.close(); + } + } + + public int getDriverInstances() + { + return driverFactory.getDriverInstances(); + } + } + + private static class DriverSplitRunner + implements SplitRunner + { + private final DriverSplitRunnerFactory driverSplitRunnerFactory; + private final DriverContext driverContext; + + @GuardedBy("this") + private boolean closed; + + @Nullable + private final ScheduledSplit partitionedSplit; + + @GuardedBy("this") + private Driver driver; + + private DriverSplitRunner(DriverSplitRunnerFactory driverSplitRunnerFactory, DriverContext driverContext, @Nullable ScheduledSplit partitionedSplit) + { + this.driverSplitRunnerFactory = checkNotNull(driverSplitRunnerFactory, "driverFactory is null"); + this.driverContext = checkNotNull(driverContext, "driverContext is null"); + this.partitionedSplit = partitionedSplit; + } + + public synchronized DriverContext getDriverContext() + { + if (driver == null) { + return null; + } + return driver.getDriverContext(); + } + + @Override + public synchronized boolean isFinished() + { + if (closed) { + return true; + } + + if (driver == null) { + return false; + } + + return driver.isFinished(); + } + + @Override + public ListenableFuture processFor(Duration duration) + { + Driver driver; + synchronized (this) { + // if close() was called before we get here, there's not point in even creating the driver + if (closed) { + return Futures.immediateFuture(null); + } + + if (this.driver == null) { + this.driver = driverSplitRunnerFactory.createDriver(driverContext, partitionedSplit); + } + + driver = this.driver; + } + + return driver.processFor(duration); + } + + @Override + public void close() + { + Driver driver; + synchronized (this) { + closed = true; + driver = this.driver; + } + + if (driver != null) { + driver.close(); + } + } + } + + private static final class RemoveTaskHandleWhenDone + implements StateChangeListener + { + private final TaskExecutor taskExecutor; + private final TaskHandle taskHandle; + + private RemoveTaskHandleWhenDone(TaskExecutor taskExecutor, TaskHandle taskHandle) + { + this.taskExecutor = checkNotNull(taskExecutor, "taskExecutor is null"); + this.taskHandle = checkNotNull(taskHandle, "taskHandle is null"); + } + + @Override + public void stateChanged(TaskState newState) + { + if (newState.isDone()) { + taskExecutor.removeTask(taskHandle); + } + } + } + + private static final class CheckTaskCompletionOnBufferFinish + implements StateChangeListener + { + private final WeakReference sqlTaskExecutionReference; + + public CheckTaskCompletionOnBufferFinish(SqlTaskExecution sqlTaskExecution) + { + // we are only checking for completion of the task, so don't hold up GC if the task is dead + this.sqlTaskExecutionReference = new WeakReference<>(sqlTaskExecution); + } + + @Override + public void stateChanged(BufferState newState) + { + if (newState == BufferState.FINISHED) { + SqlTaskExecution sqlTaskExecution = sqlTaskExecutionReference.get(); + if (sqlTaskExecution != null) { + sqlTaskExecution.checkTaskCompletion(); + } + } + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/SqlTaskExecutionFactory.java b/presto-main/src/main/java/com/facebook/presto/execution/SqlTaskExecutionFactory.java new file mode 100644 index 00000000..70159820 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/SqlTaskExecutionFactory.java @@ -0,0 +1,106 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.Session; +import com.facebook.presto.TaskSource; +import com.facebook.presto.event.query.QueryMonitor; +import com.facebook.presto.memory.QueryContext; +import com.facebook.presto.operator.TaskContext; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.sql.planner.LocalExecutionPlanner; +import com.facebook.presto.sql.planner.PlanFragment; +import io.airlift.units.DataSize; + +import java.util.List; +import java.util.concurrent.Executor; + +import static com.facebook.presto.SystemSessionProperties.isBigQueryEnabled; +import static com.facebook.presto.execution.SqlTaskExecution.createSqlTaskExecution; +import static com.facebook.presto.spi.StandardErrorCode.NOT_SUPPORTED; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.Objects.requireNonNull; + +public class SqlTaskExecutionFactory +{ + private static final String VERBOSE_STATS_PROPERTY = "verbose_stats"; + private final Executor taskNotificationExecutor; + + private final TaskExecutor taskExecutor; + + private final LocalExecutionPlanner planner; + private final QueryMonitor queryMonitor; + private final DataSize maxTaskMemoryUsage; + private final DataSize bigQueryMaxTaskMemoryUsage; + private final DataSize operatorPreAllocatedMemory; + private final boolean verboseStats; + private final boolean cpuTimerEnabled; + + public SqlTaskExecutionFactory( + Executor taskNotificationExecutor, + TaskExecutor taskExecutor, + LocalExecutionPlanner planner, + QueryMonitor queryMonitor, + TaskManagerConfig config) + { + this.taskNotificationExecutor = checkNotNull(taskNotificationExecutor, "taskNotificationExecutor is null"); + this.taskExecutor = checkNotNull(taskExecutor, "taskExecutor is null"); + this.planner = checkNotNull(planner, "planner is null"); + this.queryMonitor = checkNotNull(queryMonitor, "queryMonitor is null"); + requireNonNull(config, "config is null"); + this.maxTaskMemoryUsage = config.getMaxTaskMemoryUsage(); + this.bigQueryMaxTaskMemoryUsage = config.getBigQueryMaxTaskMemoryUsage(); + this.operatorPreAllocatedMemory = config.getOperatorPreAllocatedMemory(); + this.verboseStats = config.isVerboseStats(); + this.cpuTimerEnabled = config.isTaskCpuTimerEnabled(); + } + + public SqlTaskExecution create(Session session, QueryContext queryContext, TaskStateMachine taskStateMachine, SharedBuffer sharedBuffer, PlanFragment fragment, List sources) + { + boolean verboseStats = getVerboseStats(session); + TaskContext taskContext = queryContext.addTaskContext( + taskStateMachine, + session, + isBigQueryEnabled(session, false) ? bigQueryMaxTaskMemoryUsage : maxTaskMemoryUsage, + checkNotNull(operatorPreAllocatedMemory, "operatorPreAllocatedMemory is null"), + verboseStats, + cpuTimerEnabled); + + return createSqlTaskExecution( + taskStateMachine, + taskContext, + sharedBuffer, + fragment, + sources, + planner, + taskExecutor, + taskNotificationExecutor, + queryMonitor); + } + + private boolean getVerboseStats(Session session) + { + String verboseStats = session.getSystemProperties().get(VERBOSE_STATS_PROPERTY); + if (verboseStats == null) { + return this.verboseStats; + } + + try { + return Boolean.valueOf(verboseStats.toUpperCase()); + } + catch (IllegalArgumentException e) { + throw new PrestoException(NOT_SUPPORTED, "Invalid property '" + VERBOSE_STATS_PROPERTY + "=" + verboseStats + "'"); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/SqlTaskIoStats.java b/presto-main/src/main/java/com/facebook/presto/execution/SqlTaskIoStats.java new file mode 100644 index 00000000..1acd61f6 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/SqlTaskIoStats.java @@ -0,0 +1,86 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import io.airlift.stats.CounterStat; +import org.weakref.jmx.Managed; +import org.weakref.jmx.Nested; + +import static com.google.common.base.Preconditions.checkNotNull; + +public final class SqlTaskIoStats +{ + private final CounterStat inputDataSize; + private final CounterStat inputPositions; + private final CounterStat outputDataSize; + private final CounterStat outputPositions; + + public SqlTaskIoStats() + { + this(new CounterStat(), new CounterStat(), new CounterStat(), new CounterStat()); + } + + public SqlTaskIoStats(CounterStat inputDataSize, CounterStat inputPositions, CounterStat outputDataSize, CounterStat outputPositions) + { + this.inputDataSize = checkNotNull(inputDataSize, "inputDataSize is null"); + this.inputPositions = checkNotNull(inputPositions, "inputPositions is null"); + this.outputDataSize = checkNotNull(outputDataSize, "outputDataSize is null"); + this.outputPositions = checkNotNull(outputPositions, "outputPositions is null"); + } + + @Managed + @Nested + public CounterStat getInputDataSize() + { + return inputDataSize; + } + + @Managed + @Nested + public CounterStat getInputPositions() + { + return inputPositions; + } + + @Managed + @Nested + public CounterStat getOutputDataSize() + { + return outputDataSize; + } + + @Managed + @Nested + public CounterStat getOutputPositions() + { + return outputPositions; + } + + public void merge(SqlTaskIoStats ioStats) + { + inputDataSize.merge(ioStats.inputDataSize); + inputPositions.merge(ioStats.inputPositions); + outputDataSize.merge(ioStats.outputDataSize); + outputPositions.merge(ioStats.outputPositions); + } + + @SuppressWarnings("deprecation") + public void resetTo(SqlTaskIoStats ioStats) + { + inputDataSize.resetTo(ioStats.inputDataSize); + inputPositions.resetTo(ioStats.inputPositions); + outputDataSize.resetTo(ioStats.outputDataSize); + outputPositions.resetTo(ioStats.outputPositions); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/SqlTaskManager.java b/presto-main/src/main/java/com/facebook/presto/execution/SqlTaskManager.java new file mode 100644 index 00000000..29064b2c --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/SqlTaskManager.java @@ -0,0 +1,361 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.OutputBuffers; +import com.facebook.presto.Session; +import com.facebook.presto.TaskSource; +import com.facebook.presto.event.query.QueryMonitor; +import com.facebook.presto.memory.LocalMemoryManager; +import com.facebook.presto.memory.MemoryManagerConfig; +import com.facebook.presto.memory.MemoryPoolAssignment; +import com.facebook.presto.memory.MemoryPoolAssignmentsRequest; +import com.facebook.presto.memory.QueryContext; +import com.facebook.presto.sql.planner.LocalExecutionPlanner; +import com.facebook.presto.sql.planner.PlanFragment; +import com.google.common.base.Preconditions; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.ListenableFuture; +import io.airlift.concurrent.ThreadPoolExecutorMBean; +import io.airlift.log.Logger; +import io.airlift.node.NodeInfo; +import io.airlift.units.DataSize; +import io.airlift.units.Duration; +import org.joda.time.DateTime; +import org.weakref.jmx.Flatten; +import org.weakref.jmx.Managed; +import org.weakref.jmx.Nested; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import javax.annotation.concurrent.GuardedBy; +import javax.inject.Inject; + +import java.io.Closeable; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Predicates.notNull; +import static com.google.common.collect.Iterables.filter; +import static com.google.common.collect.Iterables.transform; +import static io.airlift.concurrent.Threads.threadsNamed; +import static java.util.Objects.requireNonNull; +import static java.util.concurrent.Executors.newCachedThreadPool; +import static java.util.concurrent.Executors.newScheduledThreadPool; + +public class SqlTaskManager + implements TaskManager, Closeable +{ + private static final Logger log = Logger.get(SqlTaskManager.class); + + private final ExecutorService taskNotificationExecutor; + private final ThreadPoolExecutorMBean taskNotificationExecutorMBean; + + private final ScheduledExecutorService taskManagementExecutor; + private final ThreadPoolExecutorMBean taskManagementExecutorMBean; + + private final Duration infoCacheTime; + private final Duration clientTimeout; + + private final LocalMemoryManager localMemoryManager; + private final LoadingCache queryContexts; + private final LoadingCache tasks; + + private final SqlTaskIoStats cachedStats = new SqlTaskIoStats(); + private final SqlTaskIoStats finishedTaskStats = new SqlTaskIoStats(); + + @GuardedBy("this") + private long currentMemoryPoolAssignmentVersion; + @GuardedBy("this") + private String coordinatorId; + + @Inject + public SqlTaskManager( + LocalExecutionPlanner planner, + LocationFactory locationFactory, + TaskExecutor taskExecutor, + QueryMonitor queryMonitor, + NodeInfo nodeInfo, + LocalMemoryManager localMemoryManager, + TaskManagerConfig config, + MemoryManagerConfig memoryManagerConfig) + { + checkNotNull(nodeInfo, "nodeInfo is null"); + checkNotNull(config, "config is null"); + infoCacheTime = config.getInfoMaxAge(); + clientTimeout = config.getClientTimeout(); + + DataSize maxBufferSize = config.getSinkMaxBufferSize(); + + taskNotificationExecutor = newCachedThreadPool(threadsNamed("task-notification-%s")); + taskNotificationExecutorMBean = new ThreadPoolExecutorMBean((ThreadPoolExecutor) taskNotificationExecutor); + + taskManagementExecutor = newScheduledThreadPool(5, threadsNamed("task-management-%s")); + taskManagementExecutorMBean = new ThreadPoolExecutorMBean((ThreadPoolExecutor) taskManagementExecutor); + + SqlTaskExecutionFactory sqlTaskExecutionFactory = new SqlTaskExecutionFactory(taskNotificationExecutor, taskExecutor, planner, queryMonitor, config); + + this.localMemoryManager = requireNonNull(localMemoryManager, "localMemoryManager is null"); + DataSize maxQueryMemoryPerNode = memoryManagerConfig.getMaxQueryMemoryPerNode(); + boolean clusterMemoryManagerEnabled = memoryManagerConfig.isClusterMemoryManagerEnabled(); + + queryContexts = CacheBuilder.newBuilder().weakValues().build(new CacheLoader() + { + @Override + public QueryContext load(QueryId key) + throws Exception + { + return new QueryContext(clusterMemoryManagerEnabled, maxQueryMemoryPerNode, localMemoryManager.getPool(LocalMemoryManager.GENERAL_POOL), taskNotificationExecutor); + } + }); + + tasks = CacheBuilder.newBuilder().build(new CacheLoader() + { + @Override + public SqlTask load(TaskId taskId) + throws Exception + { + return new SqlTask( + taskId, + nodeInfo.getInstanceId(), + locationFactory.createLocalTaskLocation(taskId), + queryContexts.getUnchecked(taskId.getQueryId()), + sqlTaskExecutionFactory, + taskNotificationExecutor, + sqlTask -> { + finishedTaskStats.merge(sqlTask.getIoStats()); + return null; + }, + maxBufferSize + ); + } + }); + } + + @Override + public synchronized void updateMemoryPoolAssignments(MemoryPoolAssignmentsRequest assignments) + { + if (coordinatorId != null && coordinatorId.equals(assignments.getCoordinatorId()) && assignments.getVersion() <= currentMemoryPoolAssignmentVersion) { + return; + } + currentMemoryPoolAssignmentVersion = assignments.getVersion(); + if (coordinatorId != null && !coordinatorId.equals(assignments.getCoordinatorId())) { + log.warn("Switching coordinator affinity from " + coordinatorId + " to " + assignments.getCoordinatorId()); + } + coordinatorId = assignments.getCoordinatorId(); + + for (MemoryPoolAssignment assignment : assignments.getAssignments()) { + queryContexts.getUnchecked(assignment.getQueryId()).setMemoryPool(localMemoryManager.getPool(assignment.getPoolId())); + } + } + + @PostConstruct + public void start() + { + taskManagementExecutor.scheduleWithFixedDelay(() -> { + try { + removeOldTasks(); + } + catch (Throwable e) { + log.warn(e, "Error removing old tasks"); + } + try { + failAbandonedTasks(); + } + catch (Throwable e) { + log.warn(e, "Error canceling abandoned tasks"); + } + }, 200, 200, TimeUnit.MILLISECONDS); + + taskManagementExecutor.scheduleWithFixedDelay(() -> { + try { + updateStats(); + } + catch (Throwable e) { + log.warn(e, "Error updating stats"); + } + }, 0, 1, TimeUnit.SECONDS); + } + + @Override + @PreDestroy + public void close() + { + taskNotificationExecutor.shutdownNow(); + taskManagementExecutor.shutdownNow(); + } + + @Managed + @Flatten + public SqlTaskIoStats getIoStats() + { + return cachedStats; + } + + @Managed(description = "Task notification executor") + @Nested + public ThreadPoolExecutorMBean getTaskNotificationExecutor() + { + return taskNotificationExecutorMBean; + } + + @Managed(description = "Task garbage collector executor") + @Nested + public ThreadPoolExecutorMBean getTaskManagementExecutor() + { + return taskManagementExecutorMBean; + } + + @Override + public List getAllTaskInfo() + { + return ImmutableList.copyOf(transform(tasks.asMap().values(), SqlTask::getTaskInfo)); + } + + @Override + public TaskInfo getTaskInfo(TaskId taskId) + { + checkNotNull(taskId, "taskId is null"); + + SqlTask sqlTask = tasks.getUnchecked(taskId); + sqlTask.recordHeartbeat(); + return sqlTask.getTaskInfo(); + } + + @Override + public ListenableFuture getTaskInfo(TaskId taskId, TaskState currentState) + { + checkNotNull(taskId, "taskId is null"); + checkNotNull(currentState, "currentState is null"); + + SqlTask sqlTask = tasks.getUnchecked(taskId); + sqlTask.recordHeartbeat(); + return sqlTask.getTaskInfo(currentState); + } + + @Override + public TaskInfo updateTask(Session session, TaskId taskId, PlanFragment fragment, List sources, OutputBuffers outputBuffers) + { + checkNotNull(session, "session is null"); + checkNotNull(taskId, "taskId is null"); + checkNotNull(fragment, "fragment is null"); + checkNotNull(sources, "sources is null"); + checkNotNull(outputBuffers, "outputBuffers is null"); + + SqlTask sqlTask = tasks.getUnchecked(taskId); + sqlTask.recordHeartbeat(); + return sqlTask.updateTask(session, fragment, sources, outputBuffers); + } + + @Override + public ListenableFuture getTaskResults(TaskId taskId, TaskId outputName, long startingSequenceId, DataSize maxSize) + { + checkNotNull(taskId, "taskId is null"); + checkNotNull(outputName, "outputName is null"); + Preconditions.checkArgument(startingSequenceId >= 0, "startingSequenceId is negative"); + checkNotNull(maxSize, "maxSize is null"); + + return tasks.getUnchecked(taskId).getTaskResults(outputName, startingSequenceId, maxSize); + } + + @Override + public TaskInfo abortTaskResults(TaskId taskId, TaskId outputId) + { + checkNotNull(taskId, "taskId is null"); + checkNotNull(outputId, "outputId is null"); + + return tasks.getUnchecked(taskId).abortTaskResults(outputId); + } + + @Override + public TaskInfo cancelTask(TaskId taskId) + { + checkNotNull(taskId, "taskId is null"); + + return tasks.getUnchecked(taskId).cancel(); + } + + @Override + public TaskInfo abortTask(TaskId taskId) + { + checkNotNull(taskId, "taskId is null"); + + return tasks.getUnchecked(taskId).abort(); + } + + public void removeOldTasks() + { + DateTime oldestAllowedTask = DateTime.now().minus(infoCacheTime.toMillis()); + for (TaskInfo taskInfo : filter(transform(tasks.asMap().values(), SqlTask::getTaskInfo), notNull())) { + try { + DateTime endTime = taskInfo.getStats().getEndTime(); + if (endTime != null && endTime.isBefore(oldestAllowedTask)) { + tasks.asMap().remove(taskInfo.getTaskId()); + } + } + catch (RuntimeException e) { + log.warn(e, "Error while inspecting age of complete task %s", taskInfo.getTaskId()); + } + } + } + + public void failAbandonedTasks() + { + DateTime now = DateTime.now(); + DateTime oldestAllowedHeartbeat = now.minus(clientTimeout.toMillis()); + for (SqlTask sqlTask : tasks.asMap().values()) { + try { + TaskInfo taskInfo = sqlTask.getTaskInfo(); + if (taskInfo.getState().isDone()) { + continue; + } + DateTime lastHeartbeat = taskInfo.getLastHeartbeat(); + if (lastHeartbeat != null && lastHeartbeat.isBefore(oldestAllowedHeartbeat)) { + log.info("Failing abandoned task %s", taskInfo.getTaskId()); + sqlTask.failed(new AbandonedException("Task " + taskInfo.getTaskId(), lastHeartbeat, now)); + } + } + catch (RuntimeException e) { + log.warn(e, "Error while inspecting age of task %s", sqlTask.getTaskId()); + } + } + } + + // + // Jmxutils only calls nested getters once, so we are forced to maintain a single + // instance and periodically recalculate the stats. + // + private void updateStats() + { + SqlTaskIoStats tempIoStats = new SqlTaskIoStats(); + tempIoStats.merge(finishedTaskStats); + + // there is a race here between task completion, which merges stats into + // finishedTaskStats, and getting the stats from the task. Since we have + // already merged the final stats, we could miss the stats from this task + // which would result in an under-count, but we will not get an over-count. + tasks.asMap().values().stream() + .filter(task -> !task.getTaskInfo().getState().isDone()) + .forEach(task -> tempIoStats.merge(task.getIoStats())); + + cachedStats.resetTo(tempIoStats); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/SqlTaskManagerStats.java b/presto-main/src/main/java/com/facebook/presto/execution/SqlTaskManagerStats.java new file mode 100644 index 00000000..4878595d --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/SqlTaskManagerStats.java @@ -0,0 +1,167 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import io.airlift.stats.CounterStat; +import io.airlift.stats.TimeStat; +import io.airlift.units.DataSize; +import io.airlift.units.Duration; +import org.weakref.jmx.Managed; +import org.weakref.jmx.Nested; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +public class SqlTaskManagerStats +{ + private final CounterStat scheduledSplits = new CounterStat(); + private final CounterStat startedSplits = new CounterStat(); + private final CounterStat completedSplits = new CounterStat(); + + private final CounterStat completedPositions = new CounterStat(); + private final CounterStat completedBytes = new CounterStat(); + + private final CounterStat splitWallTime = new CounterStat(); + private final CounterStat splitCpuTime = new CounterStat(); + + private final TimeStat splitQueuedTime = new TimeStat(MILLISECONDS); + + private final TimeStat timeToFirstByte = new TimeStat(MILLISECONDS); + private final TimeStat timeToLastByte = new TimeStat(MILLISECONDS); + + @Managed + @Nested + public CounterStat getScheduledSplits() + { + return scheduledSplits; + } + + @Managed + @Nested + public CounterStat getStartedSplits() + { + return startedSplits; + } + + @Managed + @Nested + public CounterStat getCompletedSplits() + { + return completedSplits; + } + + @Managed + @Nested + public CounterStat getCompletedPositions() + { + return completedPositions; + } + + @Managed + @Nested + public CounterStat getCompletedBytes() + { + return completedBytes; + } + + @Managed + @Nested + public CounterStat getSplitWallTime() + { + return splitWallTime; + } + + @Managed + @Nested + public CounterStat getSplitCpuTime() + { + return splitCpuTime; + } + + @Managed + @Nested + public TimeStat getTimeToFirstByte() + { + return timeToFirstByte; + } + + @Managed + @Nested + public TimeStat getSplitQueuedTime() + { + return splitQueuedTime; + } + + @Managed + @Nested + public TimeStat getTimeToLastByte() + { + return timeToLastByte; + } + + @Managed + public long getRunningSplits() + { + return Math.max(0, startedSplits.getTotalCount() - completedSplits.getTotalCount()); + } + + public void addSplits(int count) + { + scheduledSplits.update(count); + } + + public void splitStarted() + { + startedSplits.update(1); + } + + public void splitCompleted() + { + completedSplits.update(1); + } + + public void addSplitCpuTime(Duration duration) + { + splitCpuTime.update(duration.toMillis()); + } + + public void addSplitWallTime(Duration duration) + { + splitWallTime.update(duration.toMillis()); + } + + public void addCompletedPositions(long positions) + { + completedPositions.update(positions); + } + + public void addCompletedDataSize(DataSize addedDataSize) + { + completedBytes.update(addedDataSize.toBytes()); + } + + public void addSplitQueuedTime(Duration duration) + { + splitQueuedTime.add(duration); + } + + public void addTimeToFirstByte(Duration duration) + { + timeToFirstByte.add(duration); + } + + public void addTimeToLastByte(Duration duration) + { + timeToLastByte.add(duration); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/StageId.java b/presto-main/src/main/java/com/facebook/presto/execution/StageId.java new file mode 100644 index 00000000..ae68c36d --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/StageId.java @@ -0,0 +1,85 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +import java.util.List; +import java.util.Objects; + +import static com.facebook.presto.execution.QueryId.validateId; +import static com.google.common.base.Preconditions.checkNotNull; + +public class StageId +{ + @JsonCreator + public static StageId valueOf(String stageId) + { + List ids = QueryId.parseDottedId(stageId, 2, "stageId"); + return new StageId(new QueryId(ids.get(0)), ids.get(1)); + } + + private final QueryId queryId; + private final String id; + + public StageId(QueryId queryId, String id) + { + this.queryId = checkNotNull(queryId, "queryId is null"); + this.id = validateId(id); + } + + public StageId(String queryId, String id) + { + this.queryId = new QueryId(queryId); + this.id = validateId(id); + } + + public QueryId getQueryId() + { + return queryId; + } + + public String getId() + { + return id; + } + + @Override + @JsonValue + public String toString() + { + return queryId + "." + id; + } + + @Override + public int hashCode() + { + return Objects.hash(id, queryId); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final StageId other = (StageId) obj; + return Objects.equals(this.id, other.id) && + Objects.equals(this.queryId, other.queryId); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/StageInfo.java b/presto-main/src/main/java/com/facebook/presto/execution/StageInfo.java new file mode 100644 index 00000000..0a78be27 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/StageInfo.java @@ -0,0 +1,154 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.planner.PlanFragment; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +import java.net.URI; +import java.util.List; + +import static com.google.common.base.MoreObjects.toStringHelper; + +@Immutable +public class StageInfo +{ + private final StageId stageId; + private final StageState state; + private final URI self; + private final PlanFragment plan; + private final List types; + private final StageStats stageStats; + private final List tasks; + private final List subStages; + private final ExecutionFailureInfo failureCause; + + @JsonCreator + public StageInfo( + @JsonProperty("stageId") StageId stageId, + @JsonProperty("state") StageState state, + @JsonProperty("self") URI self, + @JsonProperty("plan") @Nullable PlanFragment plan, + @JsonProperty("types") List types, + @JsonProperty("stageStats") StageStats stageStats, + @JsonProperty("tasks") List tasks, + @JsonProperty("subStages") List subStages, + @JsonProperty("failureCause") ExecutionFailureInfo failureCause) + { + Preconditions.checkNotNull(stageId, "stageId is null"); + Preconditions.checkNotNull(state, "state is null"); + Preconditions.checkNotNull(self, "self is null"); + Preconditions.checkNotNull(stageStats, "stageStats is null"); + Preconditions.checkNotNull(tasks, "tasks is null"); + Preconditions.checkNotNull(subStages, "subStages is null"); + + this.stageId = stageId; + this.state = state; + this.self = self; + this.plan = plan; + this.types = types; + this.stageStats = stageStats; + this.tasks = ImmutableList.copyOf(tasks); + this.subStages = subStages; + this.failureCause = failureCause; + } + + @JsonProperty + public StageId getStageId() + { + return stageId; + } + + @JsonProperty + public StageState getState() + { + return state; + } + + @JsonProperty + public URI getSelf() + { + return self; + } + + @JsonProperty + @Nullable + public PlanFragment getPlan() + { + return plan; + } + + @JsonProperty + public List getTypes() + { + return types; + } + + @JsonProperty + public StageStats getStageStats() + { + return stageStats; + } + + @JsonProperty + public List getTasks() + { + return tasks; + } + + @JsonProperty + public List getSubStages() + { + return subStages; + } + + @JsonProperty + public ExecutionFailureInfo getFailureCause() + { + return failureCause; + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("stageId", stageId) + .add("state", state) + .toString(); + } + + public static List getAllStages(StageInfo stageInfo) + { + ImmutableList.Builder collector = ImmutableList.builder(); + if (stageInfo != null) { + addAllStages(stageInfo, collector); + } + return collector.build(); + } + + private static void addAllStages(StageInfo stageInfo, ImmutableList.Builder collector) + { + collector.add(stageInfo); + for (StageInfo subStage : stageInfo.getSubStages()) { + addAllStages(subStage, collector); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/StageState.java b/presto-main/src/main/java/com/facebook/presto/execution/StageState.java new file mode 100644 index 00000000..a4e3bfdb --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/StageState.java @@ -0,0 +1,87 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import java.util.Set; +import java.util.stream.Stream; + +import static com.facebook.presto.util.ImmutableCollectors.toImmutableSet; +import static com.google.common.base.Preconditions.checkArgument; + +public enum StageState +{ + /** + * Stage is planned but has not been scheduled yet. A stage will + * be in the planned state until, the dependencies of the stage + * have begun producing output. + */ + PLANNED(false, false), + /** + * Stage tasks are being scheduled on nodes. + */ + SCHEDULING(false, false), + /** + * Stage has been scheduled on nodes and ready to execute, but all tasks are still queued. + */ + SCHEDULED(false, false), + /** + * Stage is running. + */ + RUNNING(false, false), + /** + * Stage has finished executing and all output has been consumed. + */ + FINISHED(true, false), + /** + * Stage was canceled by a user. + */ + CANCELED(true, false), + /** + * Stage was aborted due to a failure in the query. The failure + * was not in this stage. + */ + ABORTED(true, true), + /** + * Stage execution failed. + */ + FAILED(true, true); + + public static final Set TERMINAL_STAGE_STATES = Stream.of(StageState.values()).filter(StageState::isDone).collect(toImmutableSet()); + + private final boolean doneState; + private final boolean failureState; + + StageState(boolean doneState, boolean failureState) + { + checkArgument(!failureState || doneState, "%s is a non-done failure state", name()); + this.doneState = doneState; + this.failureState = failureState; + } + + /** + * Is this a terminal state. + */ + public boolean isDone() + { + return doneState; + } + + /** + * Is this a non-success terminal state. + */ + public boolean isFailure() + { + return failureState; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/StageStateMachine.java b/presto-main/src/main/java/com/facebook/presto/execution/StageStateMachine.java new file mode 100644 index 00000000..09816dd4 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/StageStateMachine.java @@ -0,0 +1,296 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.Session; +import com.facebook.presto.execution.StateMachine.StateChangeListener; +import com.facebook.presto.operator.BlockedReason; +import com.facebook.presto.operator.TaskStats; +import com.facebook.presto.sql.planner.PlanFragment; +import com.facebook.presto.util.Failures; +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableList; +import io.airlift.log.Logger; +import io.airlift.stats.Distribution; +import org.joda.time.DateTime; + +import javax.annotation.concurrent.ThreadSafe; + +import java.net.URI; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +import static com.facebook.presto.execution.StageState.ABORTED; +import static com.facebook.presto.execution.StageState.CANCELED; +import static com.facebook.presto.execution.StageState.FAILED; +import static com.facebook.presto.execution.StageState.FINISHED; +import static com.facebook.presto.execution.StageState.PLANNED; +import static com.facebook.presto.execution.StageState.RUNNING; +import static com.facebook.presto.execution.StageState.SCHEDULED; +import static com.facebook.presto.execution.StageState.SCHEDULING; +import static com.facebook.presto.execution.StageState.TERMINAL_STAGE_STATES; +import static io.airlift.units.DataSize.Unit.BYTE; +import static io.airlift.units.DataSize.succinctDataSize; +import static io.airlift.units.Duration.succinctDuration; +import static java.util.Objects.requireNonNull; +import static java.util.concurrent.TimeUnit.NANOSECONDS; + +@ThreadSafe +public class StageStateMachine +{ + private static final Logger log = Logger.get(StageStateMachine.class); + + private final StageId stageId; + private final URI location; + private final PlanFragment fragment; + private final Session session; + + private final StateMachine stageState; + private final AtomicReference failureCause = new AtomicReference<>(); + + private final AtomicReference schedulingComplete = new AtomicReference<>(); + private final Distribution getSplitDistribution = new Distribution(); + private final Distribution scheduleTaskDistribution = new Distribution(); + private final Distribution addSplitDistribution = new Distribution(); + + public StageStateMachine(StageId stageId, URI location, Session session, PlanFragment fragment, ExecutorService executor) + { + this.stageId = requireNonNull(stageId, "stageId is null"); + this.location = requireNonNull(location, "location is null"); + this.session = requireNonNull(session, "session is null"); + this.fragment = requireNonNull(fragment, "fragment is null"); + + stageState = new StateMachine<>("stage " + stageId, executor, PLANNED, TERMINAL_STAGE_STATES); + stageState.addStateChangeListener(state -> log.debug("Stage %s is %s", stageId, state)); + } + + public StageId getStageId() + { + return stageId; + } + + public URI getLocation() + { + return location; + } + + public Session getSession() + { + return session; + } + + public StageState getState() + { + return stageState.get(); + } + + public void addStateChangeListener(StateChangeListener stateChangeListener) + { + stageState.addStateChangeListener(stateChangeListener); + } + + public synchronized boolean transitionToScheduling() + { + return stageState.compareAndSet(PLANNED, SCHEDULING); + } + + public synchronized boolean transitionToScheduled() + { + schedulingComplete.compareAndSet(null, DateTime.now()); + return stageState.setIf(SCHEDULED, currentState -> currentState == PLANNED || currentState == SCHEDULING); + } + + public boolean transitionToRunning() + { + return stageState.setIf(RUNNING, currentState -> currentState != RUNNING && !currentState.isDone()); + } + + public boolean transitionToFinished() + { + return stageState.setIf(FINISHED, currentState -> !currentState.isDone()); + } + + public boolean transitionToCanceled() + { + return stageState.setIf(CANCELED, currentState -> !currentState.isDone()); + } + + public boolean transitionToAborted() + { + return stageState.setIf(ABORTED, currentState -> !currentState.isDone()); + } + + public boolean transitionToFailed(Throwable throwable) + { + requireNonNull(throwable, "throwable is null"); + + failureCause.compareAndSet(null, Failures.toFailure(throwable)); + boolean failed = stageState.setIf(FAILED, currentState -> !currentState.isDone()); + if (failed) { + log.error(throwable, "Stage %s failed", stageId); + } + else { + log.debug(throwable, "Failure after stage %s finished", stageId); + } + return failed; + } + + public StageInfo getStageInfo(Supplier> taskInfosSupplier, Supplier> subStageInfosSupplier) + { + // stage state must be captured first in order to provide a + // consistent view of the stage. For example, building this + // information, the stage could finish, and the task states would + // never be visible. + StageState state = stageState.get(); + + List taskInfos = ImmutableList.copyOf(taskInfosSupplier.get()); + List subStageInfos = ImmutableList.copyOf(subStageInfosSupplier.get()); + + int totalTasks = taskInfos.size(); + int runningTasks = 0; + int completedTasks = 0; + + int totalDrivers = 0; + int queuedDrivers = 0; + int runningDrivers = 0; + int completedDrivers = 0; + + long totalMemoryReservation = 0; + + long totalScheduledTime = 0; + long totalCpuTime = 0; + long totalUserTime = 0; + long totalBlockedTime = 0; + + long rawInputDataSize = 0; + long rawInputPositions = 0; + + long processedInputDataSize = 0; + long processedInputPositions = 0; + + long outputDataSize = 0; + long outputPositions = 0; + + boolean fullyBlocked = true; + Set blockedReasons = new HashSet<>(); + + for (TaskInfo taskInfo : taskInfos) { + if (taskInfo.getState().isDone()) { + completedTasks++; + } + else { + runningTasks++; + } + + TaskStats taskStats = taskInfo.getStats(); + + totalDrivers += taskStats.getTotalDrivers(); + queuedDrivers += taskStats.getQueuedDrivers(); + runningDrivers += taskStats.getRunningDrivers(); + completedDrivers += taskStats.getCompletedDrivers(); + + totalMemoryReservation += taskStats.getMemoryReservation().toBytes(); + + totalScheduledTime += taskStats.getTotalScheduledTime().roundTo(NANOSECONDS); + totalCpuTime += taskStats.getTotalCpuTime().roundTo(NANOSECONDS); + totalUserTime += taskStats.getTotalUserTime().roundTo(NANOSECONDS); + totalBlockedTime += taskStats.getTotalBlockedTime().roundTo(NANOSECONDS); + if (!taskInfo.getState().isDone()) { + fullyBlocked &= taskStats.isFullyBlocked(); + blockedReasons.addAll(taskStats.getBlockedReasons()); + } + + rawInputDataSize += taskStats.getRawInputDataSize().toBytes(); + rawInputPositions += taskStats.getRawInputPositions(); + + processedInputDataSize += taskStats.getProcessedInputDataSize().toBytes(); + processedInputPositions += taskStats.getProcessedInputPositions(); + + outputDataSize += taskStats.getOutputDataSize().toBytes(); + outputPositions += taskStats.getOutputPositions(); + } + + StageStats stageStats = new StageStats( + schedulingComplete.get(), + getSplitDistribution.snapshot(), + scheduleTaskDistribution.snapshot(), + addSplitDistribution.snapshot(), + + totalTasks, + runningTasks, + completedTasks, + + totalDrivers, + queuedDrivers, + runningDrivers, + completedDrivers, + + succinctDataSize(totalMemoryReservation, BYTE), + succinctDuration(totalScheduledTime, NANOSECONDS), + succinctDuration(totalCpuTime, NANOSECONDS), + succinctDuration(totalUserTime, NANOSECONDS), + succinctDuration(totalBlockedTime, NANOSECONDS), + fullyBlocked && runningTasks > 0, + blockedReasons, + + succinctDataSize(rawInputDataSize, BYTE), + rawInputPositions, + succinctDataSize(processedInputDataSize, BYTE), + processedInputPositions, + succinctDataSize(outputDataSize, BYTE), + outputPositions); + + ExecutionFailureInfo failureInfo = null; + if (state == FAILED) { + failureInfo = failureCause.get(); + } + return new StageInfo(stageId, + state, + location, + fragment, + fragment.getTypes(), + stageStats, + taskInfos, + subStageInfos, + failureInfo); + } + + public void recordGetSplitTime(long startNanos) + { + getSplitDistribution.add(System.nanoTime() - startNanos); + } + + public void recordScheduleTaskTime(long startNanos) + { + scheduleTaskDistribution.add(System.nanoTime() - startNanos); + } + + public void recordAddSplit(long startNanos) + { + addSplitDistribution.add(System.nanoTime() - startNanos); + } + + @Override + public String toString() + { + return MoreObjects.toStringHelper(this) + .add("stageId", stageId) + .add("stageState", stageState) + .toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/StageStats.java b/presto-main/src/main/java/com/facebook/presto/execution/StageStats.java new file mode 100644 index 00000000..02c07c79 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/StageStats.java @@ -0,0 +1,320 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.operator.BlockedReason; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableSet; +import io.airlift.stats.Distribution.DistributionSnapshot; +import io.airlift.units.DataSize; +import io.airlift.units.Duration; +import org.joda.time.DateTime; + +import javax.annotation.concurrent.Immutable; + +import java.util.Set; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.Objects.requireNonNull; + +@Immutable +public class StageStats +{ + private final DateTime schedulingComplete; + + private final DistributionSnapshot getSplitDistribution; + private final DistributionSnapshot scheduleTaskDistribution; + private final DistributionSnapshot addSplitDistribution; + + private final int totalTasks; + private final int runningTasks; + private final int completedTasks; + + private final int totalDrivers; + private final int queuedDrivers; + private final int runningDrivers; + private final int completedDrivers; + + private final DataSize totalMemoryReservation; + + private final Duration totalScheduledTime; + private final Duration totalCpuTime; + private final Duration totalUserTime; + private final Duration totalBlockedTime; + private final boolean fullyBlocked; + private final Set blockedReasons; + + private final DataSize rawInputDataSize; + private final long rawInputPositions; + + private final DataSize processedInputDataSize; + private final long processedInputPositions; + + private final DataSize outputDataSize; + private final long outputPositions; + + @VisibleForTesting + public StageStats() + { + this.schedulingComplete = null; + this.getSplitDistribution = null; + this.scheduleTaskDistribution = null; + this.addSplitDistribution = null; + this.totalTasks = 0; + this.runningTasks = 0; + this.completedTasks = 0; + this.totalDrivers = 0; + this.queuedDrivers = 0; + this.runningDrivers = 0; + this.completedDrivers = 0; + this.totalMemoryReservation = null; + this.totalScheduledTime = null; + this.totalCpuTime = null; + this.totalUserTime = null; + this.totalBlockedTime = null; + this.fullyBlocked = false; + this.blockedReasons = ImmutableSet.of(); + this.rawInputDataSize = null; + this.rawInputPositions = 0; + this.processedInputDataSize = null; + this.processedInputPositions = 0; + this.outputDataSize = null; + this.outputPositions = 0; + } + + @JsonCreator + public StageStats( + @JsonProperty("schedulingComplete") DateTime schedulingComplete, + + @JsonProperty("getSplitDistribution") DistributionSnapshot getSplitDistribution, + @JsonProperty("scheduleTaskDistribution") DistributionSnapshot scheduleTaskDistribution, + @JsonProperty("addSplitDistribution") DistributionSnapshot addSplitDistribution, + + @JsonProperty("totalTasks") int totalTasks, + @JsonProperty("runningTasks") int runningTasks, + @JsonProperty("completedTasks") int completedTasks, + + @JsonProperty("totalDrivers") int totalDrivers, + @JsonProperty("queuedDrivers") int queuedDrivers, + @JsonProperty("runningDrivers") int runningDrivers, + @JsonProperty("completedDrivers") int completedDrivers, + + @JsonProperty("totalMemoryReservation") DataSize totalMemoryReservation, + + @JsonProperty("totalScheduledTime") Duration totalScheduledTime, + @JsonProperty("totalCpuTime") Duration totalCpuTime, + @JsonProperty("totalUserTime") Duration totalUserTime, + @JsonProperty("totalBlockedTime") Duration totalBlockedTime, + @JsonProperty("fullyBlocked") boolean fullyBlocked, + @JsonProperty("blockedReasons") Set blockedReasons, + + @JsonProperty("rawInputDataSize") DataSize rawInputDataSize, + @JsonProperty("rawInputPositions") long rawInputPositions, + + @JsonProperty("processedInputDataSize") DataSize processedInputDataSize, + @JsonProperty("processedInputPositions") long processedInputPositions, + + @JsonProperty("outputDataSize") DataSize outputDataSize, + @JsonProperty("outputPositions") long outputPositions) + { + this.schedulingComplete = schedulingComplete; + this.getSplitDistribution = checkNotNull(getSplitDistribution, "getSplitDistribution is null"); + this.scheduleTaskDistribution = checkNotNull(scheduleTaskDistribution, "scheduleTaskDistribution is null"); + this.addSplitDistribution = checkNotNull(addSplitDistribution, "addSplitDistribution is null"); + + checkArgument(totalTasks >= 0, "totalTasks is negative"); + this.totalTasks = totalTasks; + checkArgument(runningTasks >= 0, "runningTasks is negative"); + this.runningTasks = runningTasks; + checkArgument(completedTasks >= 0, "completedTasks is negative"); + this.completedTasks = completedTasks; + + checkArgument(totalDrivers >= 0, "totalDrivers is negative"); + this.totalDrivers = totalDrivers; + checkArgument(queuedDrivers >= 0, "queuedDrivers is negative"); + this.queuedDrivers = queuedDrivers; + checkArgument(runningDrivers >= 0, "runningDrivers is negative"); + this.runningDrivers = runningDrivers; + checkArgument(completedDrivers >= 0, "completedDrivers is negative"); + this.completedDrivers = completedDrivers; + + this.totalMemoryReservation = checkNotNull(totalMemoryReservation, "totalMemoryReservation is null"); + + this.totalScheduledTime = checkNotNull(totalScheduledTime, "totalScheduledTime is null"); + this.totalCpuTime = checkNotNull(totalCpuTime, "totalCpuTime is null"); + this.totalUserTime = checkNotNull(totalUserTime, "totalUserTime is null"); + this.totalBlockedTime = checkNotNull(totalBlockedTime, "totalBlockedTime is null"); + this.fullyBlocked = fullyBlocked; + this.blockedReasons = ImmutableSet.copyOf(requireNonNull(blockedReasons, "blockedReasons is null")); + + this.rawInputDataSize = checkNotNull(rawInputDataSize, "rawInputDataSize is null"); + checkArgument(rawInputPositions >= 0, "rawInputPositions is negative"); + this.rawInputPositions = rawInputPositions; + + this.processedInputDataSize = checkNotNull(processedInputDataSize, "processedInputDataSize is null"); + checkArgument(processedInputPositions >= 0, "processedInputPositions is negative"); + this.processedInputPositions = processedInputPositions; + + this.outputDataSize = checkNotNull(outputDataSize, "outputDataSize is null"); + checkArgument(outputPositions >= 0, "outputPositions is negative"); + this.outputPositions = outputPositions; + } + + @JsonProperty + public DateTime getSchedulingComplete() + { + return schedulingComplete; + } + + @JsonProperty + public DistributionSnapshot getGetSplitDistribution() + { + return getSplitDistribution; + } + + @JsonProperty + public DistributionSnapshot getScheduleTaskDistribution() + { + return scheduleTaskDistribution; + } + + @JsonProperty + public DistributionSnapshot getAddSplitDistribution() + { + return addSplitDistribution; + } + + @JsonProperty + public int getTotalTasks() + { + return totalTasks; + } + + @JsonProperty + public int getRunningTasks() + { + return runningTasks; + } + + @JsonProperty + public int getCompletedTasks() + { + return completedTasks; + } + + @JsonProperty + public int getTotalDrivers() + { + return totalDrivers; + } + + @JsonProperty + public int getQueuedDrivers() + { + return queuedDrivers; + } + + @JsonProperty + public int getRunningDrivers() + { + return runningDrivers; + } + + @JsonProperty + public int getCompletedDrivers() + { + return completedDrivers; + } + + @JsonProperty + public DataSize getTotalMemoryReservation() + { + return totalMemoryReservation; + } + + @JsonProperty + public Duration getTotalScheduledTime() + { + return totalScheduledTime; + } + + @JsonProperty + public Duration getTotalCpuTime() + { + return totalCpuTime; + } + + @JsonProperty + public Duration getTotalUserTime() + { + return totalUserTime; + } + + @JsonProperty + public Duration getTotalBlockedTime() + { + return totalBlockedTime; + } + + @JsonProperty + public boolean isFullyBlocked() + { + return fullyBlocked; + } + + @JsonProperty + public Set getBlockedReasons() + { + return blockedReasons; + } + + @JsonProperty + public DataSize getRawInputDataSize() + { + return rawInputDataSize; + } + + @JsonProperty + public long getRawInputPositions() + { + return rawInputPositions; + } + + @JsonProperty + public DataSize getProcessedInputDataSize() + { + return processedInputDataSize; + } + + @JsonProperty + public long getProcessedInputPositions() + { + return processedInputPositions; + } + + @JsonProperty + public DataSize getOutputDataSize() + { + return outputDataSize; + } + + @JsonProperty + public long getOutputPositions() + { + return outputPositions; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/StateMachine.java b/presto-main/src/main/java/com/facebook/presto/execution/StateMachine.java new file mode 100644 index 00000000..dc548344 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/StateMachine.java @@ -0,0 +1,365 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Predicate; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import io.airlift.log.Logger; +import io.airlift.units.Duration; + +import javax.annotation.Nonnull; +import javax.annotation.concurrent.GuardedBy; +import javax.annotation.concurrent.ThreadSafe; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Executor; + +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.Sets.newIdentityHashSet; +import static java.util.Objects.requireNonNull; +import static java.util.concurrent.TimeUnit.NANOSECONDS; + +/** + * Simple state machine which holds a single state. Callers can register for + * state change events, and can wait for the state to change. + */ +@ThreadSafe +public class StateMachine +{ + private static final Logger log = Logger.get(StateMachine.class); + + private final String name; + private final Executor executor; + private final Object lock = new Object(); + private final Set terminalStates; + + @GuardedBy("lock") + private volatile T state; + + @GuardedBy("lock") + private final List> stateChangeListeners = new ArrayList<>(); + + @GuardedBy("lock") + private final Set> futureStateChanges = newIdentityHashSet(); + + /** + * Creates a state machine with the specified initial state and no terminal states. + * + * @param name name of this state machine to use in debug statements + * @param executor executor for firing state change events; must not be a same thread executor + * @param initialState the initial state + */ + public StateMachine(String name, Executor executor, T initialState) + { + this(name, executor, initialState, ImmutableSet.of()); + } + + /** + * Creates a state machine with the specified initial state and terminal states. + * + * @param name name of this state machine to use in debug statements + * @param executor executor for firing state change events; must not be a same thread executor + * @param initialState the initial state + * @param terminalStates the terminal states + */ + public StateMachine(String name, Executor executor, T initialState, Iterable terminalStates) + { + this.name = requireNonNull(name, "name is null"); + this.executor = requireNonNull(executor, "executor is null"); + this.state = requireNonNull(initialState, "initialState is null"); + this.terminalStates = ImmutableSet.copyOf(requireNonNull(terminalStates, "terminalStates is null")); + } + + @Nonnull + public T get() + { + return state; + } + + /** + * Sets the state. + * If the new state does not {@code .equals()} the current state, listeners and waiters will be notified. + * + * @return the old state + */ + @Nonnull + public T set(T newState) + { + checkState(!Thread.holdsLock(lock), "Can not set state while holding the lock"); + requireNonNull(newState, "newState is null"); + + T oldState; + ImmutableList> futureStateChanges; + ImmutableList> stateChangeListeners; + synchronized (lock) { + if (state.equals(newState)) { + return state; + } + + checkState(!isTerminalState(state), "%s can not transition from %s to %s", name, state, newState); + + oldState = state; + state = newState; + + futureStateChanges = ImmutableList.copyOf(this.futureStateChanges); + this.futureStateChanges.clear(); + stateChangeListeners = ImmutableList.copyOf(this.stateChangeListeners); + + // if we are now in a terminal state, free the listeners since this will be the last notification + if (isTerminalState(state)) { + this.stateChangeListeners.clear(); + } + + lock.notifyAll(); + } + + fireStateChanged(newState, futureStateChanges, stateChangeListeners); + return oldState; + } + + /** + * Sets the state if the current state satisfies the specified predicate. + * If the new state does not {@code .equals()} the current state, listeners and waiters will be notified. + * + * @return the old state + */ + public boolean setIf(T newState, Predicate predicate) + { + checkState(!Thread.holdsLock(lock), "Can not set state while holding the lock"); + requireNonNull(newState, "newState is null"); + + while (true) { + // check if the current state passes the predicate + T currentState = get(); + + // change to same state is not a change, and does not notify the notify listeners + if (currentState.equals(newState)) { + return false; + } + + // do not call predicate while holding the lock + if (!predicate.apply(currentState)) { + return false; + } + + // if state did not change while, checking the predicate, apply the new state + if (compareAndSet(currentState, newState)) { + return true; + } + } + } + + /** + * Sets the state if the current state {@code .equals()} the specified expected state. + * If the new state does not {@code .equals()} the current state, listeners and waiters will be notified. + * + * @return the old state + */ + public boolean compareAndSet(T expectedState, T newState) + { + checkState(!Thread.holdsLock(lock), "Can not set state while holding the lock"); + requireNonNull(expectedState, "expectedState is null"); + requireNonNull(newState, "newState is null"); + + ImmutableList> futureStateChanges; + ImmutableList> stateChangeListeners; + synchronized (lock) { + if (!state.equals(expectedState)) { + return false; + } + + // change to same state is not a change, and does not notify the notify listeners + if (state.equals(newState)) { + return false; + } + + checkState(!isTerminalState(state), "%s can not transition from %s to %s", name, state, newState); + + state = newState; + + futureStateChanges = ImmutableList.copyOf(this.futureStateChanges); + this.futureStateChanges.clear(); + stateChangeListeners = ImmutableList.copyOf(this.stateChangeListeners); + + // if we are now in a terminal state, free the listeners since this will be the last notification + if (isTerminalState(state)) { + this.stateChangeListeners.clear(); + } + + lock.notifyAll(); + } + + fireStateChanged(newState, futureStateChanges, stateChangeListeners); + return true; + } + + private void fireStateChanged(T newState, List> futureStateChanges, List> stateChangeListeners) + { + checkState(!Thread.holdsLock(lock), "Can not fire state change event while holding the lock"); + requireNonNull(newState, "newState is null"); + + executor.execute(() -> { + checkState(!Thread.holdsLock(lock), "Can not notify while holding the lock"); + for (SettableFuture futureStateChange : futureStateChanges) { + try { + futureStateChange.set(newState); + } + catch (Throwable e) { + log.error(e, "Error setting future state for %s", name); + } + } + for (StateChangeListener stateChangeListener : stateChangeListeners) { + try { + stateChangeListener.stateChanged(newState); + } + catch (Throwable e) { + log.error(e, "Error notifying state change listener for %s", name); + } + } + }); + } + + /** + * Gets a future that completes when the state is no longer {@code .equals()} to {@code currentState)} + */ + public ListenableFuture getStateChange(T currentState) + { + checkState(!Thread.holdsLock(lock), "Can not wait for state change while holding the lock"); + requireNonNull(currentState, "currentState is null"); + + synchronized (lock) { + // return a completed future if the state has already changed, or we are in a terminal state + if (!isPossibleStateChange(currentState)) { + return Futures.immediateFuture(state); + } + + SettableFuture futureStateChange = SettableFuture.create(); + futureStateChanges.add(futureStateChange); + Futures.addCallback(futureStateChange, new FutureCallback() + { + @Override + public void onSuccess(T result) + { + // no-op. The futureStateChanges list is already cleared before fireStateChanged is called. + } + + @Override + public void onFailure(Throwable t) + { + // Remove the Future early, in case it's cancelled. + synchronized (lock) { + futureStateChanges.remove(futureStateChange); + } + } + }); + return futureStateChange; + } + } + + /** + * Adds a listener to be notified when the state instance changes according to {@code .equals()}. + */ + public void addStateChangeListener(StateChangeListener stateChangeListener) + { + requireNonNull(stateChangeListener, "stateChangeListener is null"); + + boolean inTerminalState; + synchronized (lock) { + inTerminalState = isTerminalState(state); + if (!inTerminalState) { + stateChangeListeners.add(stateChangeListener); + } + } + + // state machine will never transition from a terminal state, so fire state change immediately + if (inTerminalState) { + stateChangeListener.stateChanged(state); + } + } + + /** + * Wait for the state to not be {@code .equals()} to the specified current state. + */ + public Duration waitForStateChange(T currentState, Duration maxWait) + throws InterruptedException + { + checkState(!Thread.holdsLock(lock), "Can not wait for state change while holding the lock"); + requireNonNull(currentState, "currentState is null"); + requireNonNull(maxWait, "maxWait is null"); + + // don't wait if the state has already changed, or we are in a terminal state + if (!isPossibleStateChange(currentState)) { + return maxWait; + } + + // wait for task state to change + long remainingNanos = maxWait.roundTo(NANOSECONDS); + long start = System.nanoTime(); + long end = start + remainingNanos; + + synchronized (lock) { + while (remainingNanos > 0 && isPossibleStateChange(currentState)) { + // wait for timeout or notification + NANOSECONDS.timedWait(lock, remainingNanos); + remainingNanos = end - System.nanoTime(); + } + } + if (remainingNanos < 0) { + remainingNanos = 0; + } + return new Duration(remainingNanos, NANOSECONDS); + } + + private boolean isPossibleStateChange(T currentState) + { + return state.equals(currentState) && !isTerminalState(state); + } + + @VisibleForTesting + boolean isTerminalState(T state) + { + return terminalStates.contains(state); + } + + @VisibleForTesting + synchronized List> getStateChangeListeners() + { + return ImmutableList.copyOf(stateChangeListeners); + } + + @VisibleForTesting + synchronized Set> getFutureStateChanges() + { + return ImmutableSet.copyOf(futureStateChanges); + } + + public interface StateChangeListener + { + void stateChanged(T newState); + } + + @Override + public String toString() + { + return get().toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/TaskExecutor.java b/presto-main/src/main/java/com/facebook/presto/execution/TaskExecutor.java new file mode 100644 index 00000000..755f7f2a --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/TaskExecutor.java @@ -0,0 +1,827 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.util.CpuTimer; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Ticker; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import io.airlift.concurrent.SetThreadName; +import io.airlift.concurrent.ThreadPoolExecutorMBean; +import io.airlift.log.Logger; +import io.airlift.stats.TimeStat; +import io.airlift.units.Duration; +import org.weakref.jmx.Managed; +import org.weakref.jmx.Nested; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import javax.annotation.concurrent.GuardedBy; +import javax.annotation.concurrent.NotThreadSafe; +import javax.annotation.concurrent.ThreadSafe; +import javax.inject.Inject; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.PriorityBlockingQueue; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicLongArray; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.Sets.newConcurrentHashSet; +import static io.airlift.concurrent.Threads.threadsNamed; +import static java.util.concurrent.Executors.newCachedThreadPool; +import static java.util.concurrent.TimeUnit.NANOSECONDS; + +@ThreadSafe +public class TaskExecutor +{ + private static final Logger log = Logger.get(TaskExecutor.class); + + // each task is guaranteed a minimum number of tasks + private static final int GUARANTEED_SPLITS_PER_TASK = 3; + + // each time we run a split, run it for this length before returning to the pool + private static final Duration SPLIT_RUN_QUANTA = new Duration(1, TimeUnit.SECONDS); + + private static final AtomicLong NEXT_RUNNER_ID = new AtomicLong(); + private static final AtomicLong NEXT_WORKER_ID = new AtomicLong(); + + private final ExecutorService executor; + private final ThreadPoolExecutorMBean executorMBean; + + private final int runnerThreads; + private final int minimumNumberOfDrivers; + + private final Ticker ticker; + + @GuardedBy("this") + private final List tasks; + + /** + * All splits registered with the task executor. + */ + @GuardedBy("this") + private final Set allSplits = new HashSet<>(); + + /** + * Splits waiting for a runner thread. + */ + private final PriorityBlockingQueue pendingSplits; + + /** + * Splits running on a thread. + */ + private final Set runningSplits = newConcurrentHashSet(); + + /** + * Splits blocked by the driver (typically output buffer is full or input buffer is empty). + */ + private final Map> blockedSplits = new ConcurrentHashMap<>(); + + private final AtomicLongArray completedTasksPerLevel = new AtomicLongArray(5); + + private final TimeStat queuedTime = new TimeStat(NANOSECONDS); + private final TimeStat wallTime = new TimeStat(NANOSECONDS); + + private volatile boolean closed; + + @Inject + public TaskExecutor(TaskManagerConfig config) + { + this(checkNotNull(config, "config is null").getMaxWorkerThreads(), config.getMinDrivers()); + } + + public TaskExecutor(int runnerThreads, int minDrivers) + { + this(runnerThreads, minDrivers, Ticker.systemTicker()); + } + + @VisibleForTesting + public TaskExecutor(int runnerThreads, int minDrivers, Ticker ticker) + { + checkArgument(runnerThreads > 0, "runnerThreads must be at least 1"); + + // we manages thread pool size directly, so create an unlimited pool + this.executor = newCachedThreadPool(threadsNamed("task-processor-%s")); + this.executorMBean = new ThreadPoolExecutorMBean((ThreadPoolExecutor) executor); + this.runnerThreads = runnerThreads; + + this.ticker = checkNotNull(ticker, "ticker is null"); + + this.minimumNumberOfDrivers = minDrivers; + this.pendingSplits = new PriorityBlockingQueue<>(Runtime.getRuntime().availableProcessors() * 10); + this.tasks = new LinkedList<>(); + } + + @PostConstruct + public synchronized void start() + { + checkState(!closed, "TaskExecutor is closed"); + for (int i = 0; i < runnerThreads; i++) { + addRunnerThread(); + } + } + + @PreDestroy + public synchronized void stop() + { + closed = true; + executor.shutdownNow(); + } + + @Override + public synchronized String toString() + { + return toStringHelper(this) + .add("runnerThreads", runnerThreads) + .add("allSplits", allSplits.size()) + .add("pendingSplits", pendingSplits.size()) + .add("runningSplits", runningSplits.size()) + .add("blockedSplits", blockedSplits.size()) + .toString(); + } + + private synchronized void addRunnerThread() + { + try { + executor.execute(new Runner()); + } + catch (RejectedExecutionException ignored) { + } + } + + public synchronized TaskHandle addTask(TaskId taskId) + { + TaskHandle taskHandle = new TaskHandle(checkNotNull(taskId, "taskId is null")); + tasks.add(taskHandle); + return taskHandle; + } + + public void removeTask(TaskHandle taskHandle) + { + List splits; + synchronized (this) { + tasks.remove(taskHandle); + splits = taskHandle.destroy(); + + // stop tracking splits (especially blocked splits which may never unblock) + allSplits.removeAll(splits); + blockedSplits.keySet().removeAll(splits); + pendingSplits.removeAll(splits); + } + + // call destroy outside of synchronized block as it is expensive and doesn't need a lock on the task executor + for (PrioritizedSplitRunner split : splits) { + split.destroy(); + } + + // record completed stats + long threadUsageNanos = taskHandle.getThreadUsageNanos(); + int priorityLevel = calculatePriorityLevel(threadUsageNanos); + completedTasksPerLevel.incrementAndGet(priorityLevel); + + // replace blocked splits that were terminated + addNewEntrants(); + } + + public List> enqueueSplits(TaskHandle taskHandle, boolean forceStart, List taskSplits) + { + List splitsToDestroy = new ArrayList<>(); + List> finishedFutures = new ArrayList<>(taskSplits.size()); + synchronized (this) { + for (SplitRunner taskSplit : taskSplits) { + PrioritizedSplitRunner prioritizedSplitRunner = new PrioritizedSplitRunner(taskHandle, taskSplit, ticker); + + if (taskHandle.isDestroyed()) { + // If the handle is destroyed, we destroy the task splits to complete the future + splitsToDestroy.add(prioritizedSplitRunner); + } + else if (forceStart) { + // Note: we do not record queued time for forced splits + startSplit(prioritizedSplitRunner); + // add the runner to the handle so it can be destroyed if the task is canceled + taskHandle.recordForcedRunningSplit(prioritizedSplitRunner); + } + else { + // add this to the work queue for the task + taskHandle.enqueueSplit(prioritizedSplitRunner); + // if task is under the limit for gaurenteed splits, start one + scheduleTaskIfNecessary(taskHandle); + // if globally we have more resources, start more + addNewEntrants(); + } + + finishedFutures.add(prioritizedSplitRunner.getFinishedFuture()); + } + } + for (PrioritizedSplitRunner split : splitsToDestroy) { + split.destroy(); + } + return finishedFutures; + } + + private void splitFinished(PrioritizedSplitRunner split) + { + synchronized (this) { + allSplits.remove(split); + + TaskHandle taskHandle = split.getTaskHandle(); + taskHandle.splitComplete(split); + + wallTime.add(Duration.nanosSince(split.createdNanos)); + + scheduleTaskIfNecessary(taskHandle); + + addNewEntrants(); + } + // call destroy outside of synchronized block as it is expensive and doesn't need a lock on the task executor + split.destroy(); + } + + private synchronized void scheduleTaskIfNecessary(TaskHandle taskHandle) + { + // if task has less than the minimum guaranteed splits running, + // immediately schedule a new split for this task. This assures + // that a task gets its fair amount of consideration (you have to + // have splits to be considered for running on a thread). + if (taskHandle.getRunningSplits() < GUARANTEED_SPLITS_PER_TASK) { + PrioritizedSplitRunner split = taskHandle.pollNextSplit(); + if (split != null) { + startSplit(split); + queuedTime.add(Duration.nanosSince(split.createdNanos)); + } + } + } + + private synchronized void addNewEntrants() + { + int running = allSplits.size(); + for (int i = 0; i < minimumNumberOfDrivers - running; i++) { + PrioritizedSplitRunner split = pollNextSplitWorker(); + if (split == null) { + break; + } + + queuedTime.add(Duration.nanosSince(split.createdNanos)); + startSplit(split); + } + } + + private synchronized void startSplit(PrioritizedSplitRunner split) + { + allSplits.add(split); + pendingSplits.put(split); + } + + private synchronized PrioritizedSplitRunner pollNextSplitWorker() + { + // todo find a better algorithm for this + // find the first task that produces a split, then move that task to the + // end of the task list, so we get round robin + for (Iterator iterator = tasks.iterator(); iterator.hasNext(); ) { + TaskHandle task = iterator.next(); + PrioritizedSplitRunner split = task.pollNextSplit(); + if (split != null) { + // move task to end of list + iterator.remove(); + + // CAUTION: we are modifying the list in the loop which would normally + // cause a ConcurrentModificationException but we exit immediately + tasks.add(task); + return split; + } + } + return null; + } + + @NotThreadSafe + public static class TaskHandle + { + private final TaskId taskId; + private final Queue queuedSplits = new ArrayDeque<>(10); + private final List runningSplits = new ArrayList<>(10); + private final List forcedRunningSplits = new ArrayList<>(10); + private final AtomicLong taskThreadUsageNanos = new AtomicLong(); + + private final AtomicBoolean destroyed = new AtomicBoolean(); + + private final AtomicInteger nextSplitId = new AtomicInteger(); + + private TaskHandle(TaskId taskId) + { + this.taskId = taskId; + } + + private long addThreadUsageNanos(long durationNanos) + { + return taskThreadUsageNanos.addAndGet(durationNanos); + } + + private TaskId getTaskId() + { + return taskId; + } + + public boolean isDestroyed() + { + return destroyed.get(); + } + + // Returns any remaining splits. The caller must destroy these. + private List destroy() + { + destroyed.set(true); + + ImmutableList.Builder builder = ImmutableList.builder(); + builder.addAll(forcedRunningSplits); + builder.addAll(runningSplits); + builder.addAll(queuedSplits); + forcedRunningSplits.clear(); + runningSplits.clear(); + queuedSplits.clear(); + return builder.build(); + } + + private void enqueueSplit(PrioritizedSplitRunner split) + { + checkState(!destroyed.get(), "Can not add split to destroyed task handle"); + queuedSplits.add(split); + } + + private void recordForcedRunningSplit(PrioritizedSplitRunner split) + { + checkState(!destroyed.get(), "Can not add split to destroyed task handle"); + forcedRunningSplits.add(split); + } + + @VisibleForTesting + int getRunningSplits() + { + return runningSplits.size(); + } + + private long getThreadUsageNanos() + { + return taskThreadUsageNanos.get(); + } + + private PrioritizedSplitRunner pollNextSplit() + { + if (destroyed.get()) { + return null; + } + + PrioritizedSplitRunner split = queuedSplits.poll(); + if (split != null) { + runningSplits.add(split); + } + return split; + } + + private void splitComplete(PrioritizedSplitRunner split) + { + forcedRunningSplits.remove(split); + runningSplits.remove(split); + } + + private int getNextSplitId() + { + return nextSplitId.getAndIncrement(); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("taskId", taskId) + .toString(); + } + } + + private static class PrioritizedSplitRunner + implements Comparable + { + private final long createdNanos = System.nanoTime(); + + private final TaskHandle taskHandle; + private final int splitId; + private final long workerId; + private final SplitRunner split; + + private final Ticker ticker; + + private final SettableFuture finishedFuture = SettableFuture.create(); + + private final AtomicBoolean destroyed = new AtomicBoolean(); + + private final AtomicInteger priorityLevel = new AtomicInteger(); + private final AtomicLong threadUsageNanos = new AtomicLong(); + private final AtomicLong lastRun = new AtomicLong(); + private final AtomicLong start = new AtomicLong(); + + private final AtomicLong cpuTime = new AtomicLong(); + private final AtomicLong processCalls = new AtomicLong(); + + private PrioritizedSplitRunner(TaskHandle taskHandle, SplitRunner split, Ticker ticker) + { + this.taskHandle = taskHandle; + this.splitId = taskHandle.getNextSplitId(); + this.split = split; + this.ticker = ticker; + this.workerId = NEXT_WORKER_ID.getAndIncrement(); + } + + private TaskHandle getTaskHandle() + { + return taskHandle; + } + + private ListenableFuture getFinishedFuture() + { + return finishedFuture; + } + + public void destroy() + { + destroyed.set(true); + try { + split.close(); + } + catch (RuntimeException e) { + log.error(e, "Error closing split for task %s", taskHandle.getTaskId()); + } + } + + public boolean isFinished() + { + boolean finished = split.isFinished(); + if (finished) { + finishedFuture.set(null); + } + return finished || destroyed.get() || taskHandle.isDestroyed(); + } + + public ListenableFuture process() + throws Exception + { + try { + start.compareAndSet(0, System.currentTimeMillis()); + + processCalls.incrementAndGet(); + CpuTimer timer = new CpuTimer(); + ListenableFuture blocked = split.processFor(SPLIT_RUN_QUANTA); + + CpuTimer.CpuDuration elapsed = timer.elapsedTime(); + + // update priority level base on total thread usage of task + long durationNanos = elapsed.getWall().roundTo(NANOSECONDS); + long threadUsageNanos = taskHandle.addThreadUsageNanos(durationNanos); + this.threadUsageNanos.set(threadUsageNanos); + priorityLevel.set(calculatePriorityLevel(threadUsageNanos)); + + // record last run for prioritization within a level + lastRun.set(ticker.read()); + + cpuTime.addAndGet(elapsed.getCpu().roundTo(NANOSECONDS)); + return blocked; + } + catch (Throwable e) { + finishedFuture.setException(e); + throw e; + } + } + + public boolean updatePriorityLevel() + { + int newPriority = calculatePriorityLevel(taskHandle.getThreadUsageNanos()); + if (newPriority == priorityLevel.getAndSet(newPriority)) { + return false; + } + + // update thread usage while if level changed + threadUsageNanos.set(taskHandle.getThreadUsageNanos()); + return true; + } + + @Override + public int compareTo(PrioritizedSplitRunner o) + { + int level = priorityLevel.get(); + + int result = Integer.compare(level, o.priorityLevel.get()); + if (result != 0) { + return result; + } + + if (level < 4) { + result = Long.compare(threadUsageNanos.get(), o.threadUsageNanos.get()); + } + else { + result = Long.compare(lastRun.get(), o.lastRun.get()); + } + if (result != 0) { + return result; + } + + return Long.compare(workerId, o.workerId); + } + + public int getSplitId() + { + return splitId; + } + + public String getInfo() + { + return String.format("Split %-15s-%d (start = %s, wall = %s ms, cpu = %s ms, calls = %s)", + taskHandle.getTaskId(), + splitId, + start.get(), + System.currentTimeMillis() - start.get(), + (int) (cpuTime.get() / 1.0e6), + processCalls.get()); + } + + @Override + public String toString() + { + return String.format("Split %-15s-%d", taskHandle.getTaskId(), splitId); + } + } + + private static int calculatePriorityLevel(long threadUsageNanos) + { + long millis = NANOSECONDS.toMillis(threadUsageNanos); + + int priorityLevel; + if (millis < 1000) { + priorityLevel = 0; + } + else if (millis < 10_000) { + priorityLevel = 1; + } + else if (millis < 60_000) { + priorityLevel = 2; + } + else if (millis < 300_000) { + priorityLevel = 3; + } + else { + priorityLevel = 4; + } + return priorityLevel; + } + + private class Runner + implements Runnable + { + private final long runnerId = NEXT_RUNNER_ID.getAndIncrement(); + + @Override + public void run() + { + try (SetThreadName runnerName = new SetThreadName("SplitRunner-%s", runnerId)) { + while (!closed && !Thread.currentThread().isInterrupted()) { + // select next worker + final PrioritizedSplitRunner split; + try { + split = pendingSplits.take(); + if (split.updatePriorityLevel()) { + // priority level changed, return split to queue for re-prioritization + pendingSplits.put(split); + continue; + } + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + + try (SetThreadName splitName = new SetThreadName(split.getTaskHandle().getTaskId() + "-" + split.getSplitId())) { + runningSplits.add(split); + + boolean finished; + ListenableFuture blocked; + try { + blocked = split.process(); + finished = split.isFinished(); + } + finally { + runningSplits.remove(split); + } + + if (finished) { + log.debug("%s is finished", split.getInfo()); + splitFinished(split); + } + else { + if (blocked.isDone()) { + pendingSplits.put(split); + } + else { + blockedSplits.put(split, blocked); + blocked.addListener(new Runnable() + { + @Override + public void run() + { + blockedSplits.remove(split); + split.updatePriorityLevel(); + pendingSplits.put(split); + } + }, executor); + } + } + } + catch (Throwable t) { + if (t instanceof PrestoException) { + PrestoException e = (PrestoException) t; + log.error("Error processing %s: %s: %s", split.getInfo(), e.getErrorCode().getName(), e.getMessage()); + } + else { + log.error(t, "Error processing %s", split.getInfo()); + } + splitFinished(split); + } + } + } + finally { + // unless we have been closed, we need to replace this thread + if (!closed) { + addRunnerThread(); + } + } + } + } + + // + // STATS + // + + @Managed + public synchronized int getTasks() + { + return tasks.size(); + } + + @Managed + public int getRunnerThreads() + { + return runnerThreads; + } + + @Managed + public int getMinimumNumberOfDrivers() + { + return minimumNumberOfDrivers; + } + + @Managed + public synchronized int getTotalSplits() + { + return allSplits.size(); + } + + @Managed + public int getPendingSplits() + { + return pendingSplits.size(); + } + + @Managed + public int getRunningSplits() + { + return runningSplits.size(); + } + + @Managed + public int getBlockedSplits() + { + return blockedSplits.size(); + } + + @Managed + public long getCompletedTasksLevel0() + { + return completedTasksPerLevel.get(0); + } + + @Managed + public long getCompletedTasksLevel1() + { + return completedTasksPerLevel.get(1); + } + + @Managed + public long getCompletedTasksLevel2() + { + return completedTasksPerLevel.get(2); + } + + @Managed + public long getCompletedTasksLevel3() + { + return completedTasksPerLevel.get(3); + } + + @Managed + public long getCompletedTasksLevel4() + { + return completedTasksPerLevel.get(4); + } + + @Managed + public long getRunningTasksLevel0() + { + return calculateRunningTasksForLevel(0); + } + + @Managed + public long getRunningTasksLevel1() + { + return calculateRunningTasksForLevel(1); + } + + @Managed + public long getRunningTasksLevel2() + { + return calculateRunningTasksForLevel(2); + } + + @Managed + public long getRunningTasksLevel3() + { + return calculateRunningTasksForLevel(3); + } + + @Managed + public long getRunningTasksLevel4() + { + return calculateRunningTasksForLevel(4); + } + + @Managed + @Nested + public TimeStat getQueuedTime() + { + return queuedTime; + } + + @Managed + @Nested + public TimeStat getWallTime() + { + return wallTime; + } + + private synchronized int calculateRunningTasksForLevel(int level) + { + int count = 0; + for (TaskHandle task : tasks) { + if (calculatePriorityLevel(task.getThreadUsageNanos()) == level) { + count++; + } + } + return count; + } + + @Managed(description = "Task processor executor") + @Nested + public ThreadPoolExecutorMBean getProcessorExecutor() + { + return executorMBean; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/TaskId.java b/presto-main/src/main/java/com/facebook/presto/execution/TaskId.java new file mode 100644 index 00000000..97db8245 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/TaskId.java @@ -0,0 +1,92 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +import java.util.List; +import java.util.Objects; + +import static com.facebook.presto.execution.QueryId.validateId; + +public class TaskId +{ + @JsonCreator + public static TaskId valueOf(String taskId) + { + return new TaskId(taskId); + } + + private final String fullId; + + public TaskId(String queryId, String stageId, String id) + { + validateId(id); + this.fullId = queryId + "." + stageId + "." + id; + } + + public TaskId(StageId stageId, String id) + { + validateId(id); + this.fullId = stageId.getQueryId().getId() + "." + stageId.getId() + "." + id; + } + + public TaskId(String fullId) + { + this.fullId = fullId; + } + + public QueryId getQueryId() + { + return new QueryId(QueryId.parseDottedId(fullId, 3, "taskId").get(0)); + } + + public StageId getStageId() + { + List ids = QueryId.parseDottedId(fullId, 3, "taskId"); + return new StageId(new QueryId(ids.get(0)), ids.get(1)); + } + + public String getId() + { + return QueryId.parseDottedId(fullId, 3, "taskId").get(2); + } + + @Override + @JsonValue + public String toString() + { + return fullId; + } + + @Override + public int hashCode() + { + return Objects.hash(fullId); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + TaskId other = (TaskId) obj; + return Objects.equals(this.fullId, other.fullId); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/TaskInfo.java b/presto-main/src/main/java/com/facebook/presto/execution/TaskInfo.java new file mode 100644 index 00000000..92930eea --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/TaskInfo.java @@ -0,0 +1,167 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.operator.TaskStats; +import com.facebook.presto.sql.planner.plan.PlanNodeId; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; +import org.joda.time.DateTime; + +import javax.annotation.concurrent.Immutable; + +import java.net.URI; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkNotNull; + +@Immutable +public class TaskInfo +{ + /** + * The first valid version that will be returned for a remote task. + */ + public static final long STARTING_VERSION = 1; + + /** + * A value lower than {@link #STARTING_VERSION}. This value can be used to + * create an initial local task that is always older than any remote task. + */ + public static final long MIN_VERSION = 0; + + /** + * A value larger than any valid value. This value can be used to create + * a final local task that is always newer than any remote task. + */ + public static final long MAX_VERSION = Long.MAX_VALUE; + + private final TaskId taskId; + private final Optional nodeInstanceId; + private final long version; + private final TaskState state; + private final URI self; + private final DateTime lastHeartbeat; + private final SharedBufferInfo outputBuffers; + private final Set noMoreSplits; + private final TaskStats stats; + private final List failures; + + @JsonCreator + public TaskInfo(@JsonProperty("taskId") TaskId taskId, + @JsonProperty("nodeInstanceId") Optional nodeInstanceId, + @JsonProperty("version") long version, + @JsonProperty("state") TaskState state, + @JsonProperty("self") URI self, + @JsonProperty("lastHeartbeat") DateTime lastHeartbeat, + @JsonProperty("outputBuffers") SharedBufferInfo outputBuffers, + @JsonProperty("noMoreSplits") Set noMoreSplits, + @JsonProperty("stats") TaskStats stats, + @JsonProperty("failures") List failures) + { + this.taskId = checkNotNull(taskId, "taskId is null"); + this.nodeInstanceId = checkNotNull(nodeInstanceId, "nodeInstanceId is null"); + this.version = version; + this.state = checkNotNull(state, "state is null"); + this.self = checkNotNull(self, "self is null"); + this.lastHeartbeat = checkNotNull(lastHeartbeat, "lastHeartbeat is null"); + this.outputBuffers = checkNotNull(outputBuffers, "outputBuffers is null"); + this.noMoreSplits = checkNotNull(noMoreSplits, "noMoreSplits is null"); + this.stats = checkNotNull(stats, "stats is null"); + + if (failures != null) { + this.failures = ImmutableList.copyOf(failures); + } + else { + this.failures = ImmutableList.of(); + } + } + + @JsonProperty + public TaskId getTaskId() + { + return taskId; + } + + @JsonProperty + public Optional getNodeInstanceId() + { + return nodeInstanceId; + } + + @JsonProperty + public long getVersion() + { + return version; + } + + @JsonProperty + public TaskState getState() + { + return state; + } + + @JsonProperty + public URI getSelf() + { + return self; + } + + @JsonProperty + public DateTime getLastHeartbeat() + { + return lastHeartbeat; + } + + @JsonProperty + public SharedBufferInfo getOutputBuffers() + { + return outputBuffers; + } + + @JsonProperty + public Set getNoMoreSplits() + { + return noMoreSplits; + } + + @JsonProperty + public TaskStats getStats() + { + return stats; + } + + @JsonProperty + public List getFailures() + { + return failures; + } + + public TaskInfo summarize() + { + return new TaskInfo(taskId, nodeInstanceId, version, state, self, lastHeartbeat, outputBuffers, noMoreSplits, stats.summarize(), failures); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("taskId", taskId) + .add("state", state) + .toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/TaskManager.java b/presto-main/src/main/java/com/facebook/presto/execution/TaskManager.java new file mode 100644 index 00000000..96762c78 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/TaskManager.java @@ -0,0 +1,93 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.OutputBuffers; +import com.facebook.presto.Session; +import com.facebook.presto.TaskSource; +import com.facebook.presto.memory.MemoryPoolAssignmentsRequest; +import com.facebook.presto.sql.planner.PlanFragment; +import com.google.common.util.concurrent.ListenableFuture; +import io.airlift.units.DataSize; + +import java.util.List; + +public interface TaskManager +{ + /** + * Gets all of the currently tracked tasks. This will included + * uninitialized, running, and completed tasks. + */ + List getAllTaskInfo(); + + /** + * Gets the info for the specified task. If the task has not been created + * yet, an uninitialized task is created and the info is returned. + * + * NOTE: this design assumes that only tasks that will eventually exist are + * queried. + */ + TaskInfo getTaskInfo(TaskId taskId); + + /** + * Gets future info for the task after the state changes from + * {@code current state}. If the task has not been created yet, an + * uninitialized task is created and the future is returned. If the task + * is already in a final state, the info is returned immediately. + * + * NOTE: this design assumes that only tasks that will eventually exist are + * queried. + */ + ListenableFuture getTaskInfo(TaskId taskId, TaskState currentState); + + void updateMemoryPoolAssignments(MemoryPoolAssignmentsRequest assignments); + + /** + * Updates the task plan, sources and output buffers. If the task does not + * already exist, is is created and then updated. + */ + TaskInfo updateTask(Session session, TaskId taskId, PlanFragment fragment, List sources, OutputBuffers outputBuffers); + + /** + * Cancels a task. If the task does not already exist, is is created and then + * canceled. + */ + TaskInfo cancelTask(TaskId taskId); + + /** + * Aborts a task. If the task does not already exist, is is created and then + * aborted. + */ + TaskInfo abortTask(TaskId taskId); + + /** + * Gets results from a task either immediately or in the future. If the + * task or buffer has not been created yet, an uninitialized task is + * created and a future is returned. + * + * NOTE: this design assumes that only tasks and buffers that will + * eventually exist are queried. + */ + ListenableFuture getTaskResults(TaskId taskId, TaskId outputName, long startingSequenceId, DataSize maxSize); + + /** + * Aborts a result buffer for a task. If the task or buffer has not been + * created yet, an uninitialized task is created and a the buffer is + * aborted. + * + * NOTE: this design assumes that only tasks and buffers that will + * eventually exist are queried. + */ + TaskInfo abortTaskResults(TaskId taskId, TaskId outputId); +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/TaskManagerConfig.java b/presto-main/src/main/java/com/facebook/presto/execution/TaskManagerConfig.java new file mode 100644 index 00000000..ee64fb64 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/TaskManagerConfig.java @@ -0,0 +1,267 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import io.airlift.configuration.Config; +import io.airlift.configuration.ConfigDescription; +import io.airlift.configuration.LegacyConfig; +import io.airlift.units.DataSize; +import io.airlift.units.DataSize.Unit; +import io.airlift.units.Duration; +import io.airlift.units.MaxDuration; +import io.airlift.units.MinDuration; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; + +import java.util.concurrent.TimeUnit; + +public class TaskManagerConfig +{ + private boolean verboseStats; + private boolean taskCpuTimerEnabled = true; + private DataSize maxTaskMemoryUsage = new DataSize(256, Unit.MEGABYTE); + private DataSize bigQueryMaxTaskMemoryUsage; + private DataSize maxPartialAggregationMemoryUsage = new DataSize(16, Unit.MEGABYTE); + private DataSize operatorPreAllocatedMemory = new DataSize(16, Unit.MEGABYTE); + private DataSize maxTaskIndexMemoryUsage = new DataSize(64, Unit.MEGABYTE); + private int maxWorkerThreads = Runtime.getRuntime().availableProcessors() * 4; + private Integer minDrivers; + + private DataSize sinkMaxBufferSize = new DataSize(32, Unit.MEGABYTE); + + private Duration clientTimeout = new Duration(2, TimeUnit.MINUTES); + private Duration infoMaxAge = new Duration(15, TimeUnit.MINUTES); + private Duration infoRefreshMaxWait = new Duration(200, TimeUnit.MILLISECONDS); + private int writerCount = 1; + private int taskDefaultConcurrency = 1; + private int httpNotificationThreads = 25; + + @MinDuration("1ms") + @MaxDuration("10s") + @NotNull + public Duration getInfoRefreshMaxWait() + { + return infoRefreshMaxWait; + } + + @Config("task.info-refresh-max-wait") + public TaskManagerConfig setInfoRefreshMaxWait(Duration infoRefreshMaxWait) + { + this.infoRefreshMaxWait = infoRefreshMaxWait; + return this; + } + + public boolean isVerboseStats() + { + return verboseStats; + } + + @Config("task.verbose-stats") + public TaskManagerConfig setVerboseStats(boolean verboseStats) + { + this.verboseStats = verboseStats; + return this; + } + + public boolean isTaskCpuTimerEnabled() + { + return taskCpuTimerEnabled; + } + + @Config("task.cpu-timer-enabled") + public TaskManagerConfig setTaskCpuTimerEnabled(boolean taskCpuTimerEnabled) + { + this.taskCpuTimerEnabled = taskCpuTimerEnabled; + return this; + } + + @NotNull + public DataSize getMaxPartialAggregationMemoryUsage() + { + return maxPartialAggregationMemoryUsage; + } + + @Config("task.max-partial-aggregation-memory") + public TaskManagerConfig setMaxPartialAggregationMemoryUsage(DataSize maxPartialAggregationMemoryUsage) + { + this.maxPartialAggregationMemoryUsage = maxPartialAggregationMemoryUsage; + return this; + } + + public DataSize getBigQueryMaxTaskMemoryUsage() + { + if (bigQueryMaxTaskMemoryUsage == null) { + return new DataSize(2 * maxTaskMemoryUsage.toBytes(), Unit.BYTE); + } + return bigQueryMaxTaskMemoryUsage; + } + + @Config("experimental.big-query-max-task-memory") + public TaskManagerConfig setBigQueryMaxTaskMemoryUsage(DataSize bigQueryMaxTaskMemoryUsage) + { + this.bigQueryMaxTaskMemoryUsage = bigQueryMaxTaskMemoryUsage; + return this; + } + + @NotNull + public DataSize getMaxTaskMemoryUsage() + { + return maxTaskMemoryUsage; + } + + @Config("task.max-memory") + public TaskManagerConfig setMaxTaskMemoryUsage(DataSize maxTaskMemoryUsage) + { + this.maxTaskMemoryUsage = maxTaskMemoryUsage; + return this; + } + + @NotNull + public DataSize getOperatorPreAllocatedMemory() + { + return operatorPreAllocatedMemory; + } + + @Config("task.operator-pre-allocated-memory") + public TaskManagerConfig setOperatorPreAllocatedMemory(DataSize operatorPreAllocatedMemory) + { + this.operatorPreAllocatedMemory = operatorPreAllocatedMemory; + return this; + } + + @NotNull + public DataSize getMaxTaskIndexMemoryUsage() + { + return maxTaskIndexMemoryUsage; + } + + @Config("task.max-index-memory") + public TaskManagerConfig setMaxTaskIndexMemoryUsage(DataSize maxTaskIndexMemoryUsage) + { + this.maxTaskIndexMemoryUsage = maxTaskIndexMemoryUsage; + return this; + } + + @Min(1) + public int getMaxWorkerThreads() + { + return maxWorkerThreads; + } + + @LegacyConfig("task.shard.max-threads") + @Config("task.max-worker-threads") + public TaskManagerConfig setMaxWorkerThreads(int maxWorkerThreads) + { + this.maxWorkerThreads = maxWorkerThreads; + return this; + } + + @Min(1) + public int getMinDrivers() + { + if (minDrivers == null) { + return 2 * maxWorkerThreads; + } + return minDrivers; + } + + @Config("task.min-drivers") + public TaskManagerConfig setMinDrivers(int minDrivers) + { + this.minDrivers = minDrivers; + return this; + } + + @NotNull + public DataSize getSinkMaxBufferSize() + { + return sinkMaxBufferSize; + } + + @Config("sink.max-buffer-size") + public TaskManagerConfig setSinkMaxBufferSize(DataSize sinkMaxBufferSize) + { + this.sinkMaxBufferSize = sinkMaxBufferSize; + return this; + } + + @MinDuration("5s") + @NotNull + public Duration getClientTimeout() + { + return clientTimeout; + } + + @Config("task.client.timeout") + public TaskManagerConfig setClientTimeout(Duration clientTimeout) + { + this.clientTimeout = clientTimeout; + return this; + } + + @NotNull + public Duration getInfoMaxAge() + { + return infoMaxAge; + } + + @Config("task.info.max-age") + public TaskManagerConfig setInfoMaxAge(Duration infoMaxAge) + { + this.infoMaxAge = infoMaxAge; + return this; + } + + @Min(1) + public int getWriterCount() + { + return writerCount; + } + + @Config("task.writer-count") + @ConfigDescription("Number of writers per task") + public TaskManagerConfig setWriterCount(int writerCount) + { + this.writerCount = writerCount; + return this; + } + + @Min(1) + public int getTaskDefaultConcurrency() + { + return taskDefaultConcurrency; + } + + @Config("task.default-concurrency") + @ConfigDescription("Default local concurrency for parallel operators") + public TaskManagerConfig setTaskDefaultConcurrency(int taskDefaultConcurrency) + { + this.taskDefaultConcurrency = taskDefaultConcurrency; + return this; + } + + @Min(1) + public int getHttpNotificationThreads() + { + return httpNotificationThreads; + } + + @Config("task.http-notification-threads") + public TaskManagerConfig setHttpNotificationThreads(int httpNotificationThreads) + { + this.httpNotificationThreads = httpNotificationThreads; + return this; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/TaskState.java b/presto-main/src/main/java/com/facebook/presto/execution/TaskState.java new file mode 100644 index 00000000..505adf69 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/TaskState.java @@ -0,0 +1,67 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import java.util.Set; +import java.util.stream.Stream; + +import static com.facebook.presto.util.ImmutableCollectors.toImmutableSet; + +public enum TaskState +{ + /** + * Task is planned but has not been scheduled yet. A task will + * be in the planned state until, the dependencies of the task + * have begun producing output. + */ + PLANNED(false), + /** + * Task is running. + */ + RUNNING(false), + /** + * Task has finished executing and all output has been consumed. + */ + FINISHED(true), + /** + * Task was canceled by a user. + */ + CANCELED(true), + /** + * Task was aborted due to a failure in the query. The failure + * was not in this task. + */ + ABORTED(true), + /** + * Task execution failed. + */ + FAILED(true); + + public static final Set TERMINAL_TASK_STATES = Stream.of(TaskState.values()).filter(TaskState::isDone).collect(toImmutableSet()); + + private final boolean doneState; + + TaskState(boolean doneState) + { + this.doneState = doneState; + } + + /** + * Is this a terminal state. + */ + public boolean isDone() + { + return doneState; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/execution/TaskStateMachine.java b/presto-main/src/main/java/com/facebook/presto/execution/TaskStateMachine.java new file mode 100644 index 00000000..2500eb07 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/execution/TaskStateMachine.java @@ -0,0 +1,140 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.execution; + +import com.facebook.presto.execution.StateMachine.StateChangeListener; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import io.airlift.log.Logger; +import io.airlift.units.Duration; +import org.joda.time.DateTime; + +import javax.annotation.concurrent.ThreadSafe; + +import java.util.concurrent.Executor; +import java.util.concurrent.LinkedBlockingQueue; + +import static com.facebook.presto.execution.TaskState.TERMINAL_TASK_STATES; +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@ThreadSafe +public class TaskStateMachine +{ + private static final Logger log = Logger.get(TaskStateMachine.class); + + private final DateTime createdTime = DateTime.now(); + + private final TaskId taskId; + private final StateMachine taskState; + private final LinkedBlockingQueue failureCauses = new LinkedBlockingQueue<>(); + + public TaskStateMachine(TaskId taskId, Executor executor) + { + this.taskId = checkNotNull(taskId, "taskId is null"); + taskState = new StateMachine<>("task " + taskId, executor, TaskState.RUNNING, TERMINAL_TASK_STATES); + taskState.addStateChangeListener(new StateChangeListener() + { + @Override + public void stateChanged(TaskState newState) + { + log.debug("Task %s is %s", TaskStateMachine.this.taskId, newState); + } + }); + } + + public DateTime getCreatedTime() + { + return createdTime; + } + + public TaskId getTaskId() + { + return taskId; + } + + public TaskState getState() + { + return taskState.get(); + } + + public ListenableFuture getStateChange(TaskState currentState) + { + checkNotNull(currentState, "currentState is null"); + checkArgument(!currentState.isDone(), "Current state is already done"); + + ListenableFuture future = taskState.getStateChange(currentState); + TaskState state = taskState.get(); + if (state.isDone()) { + return Futures.immediateFuture(state); + } + return future; + } + + public LinkedBlockingQueue getFailureCauses() + { + return failureCauses; + } + + public void finished() + { + transitionToDoneState(TaskState.FINISHED); + } + + public void cancel() + { + transitionToDoneState(TaskState.CANCELED); + } + + public void abort() + { + transitionToDoneState(TaskState.ABORTED); + } + + public void failed(Throwable cause) + { + failureCauses.add(cause); + transitionToDoneState(TaskState.FAILED); + } + + private void transitionToDoneState(TaskState doneState) + { + checkNotNull(doneState, "doneState is null"); + checkArgument(doneState.isDone(), "doneState %s is not a done state", doneState); + + taskState.setIf(doneState, currentState -> !currentState.isDone()); + } + + public Duration waitForStateChange(TaskState currentState, Duration maxWait) + throws InterruptedException + { + return taskState.waitForStateChange(currentState, maxWait); + } + + public void addStateChangeListener(StateChangeListener stateChangeListener) + { + taskState.addStateChangeListener(stateChangeListener); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("taskId", taskId) + .add("taskState", taskState) + .add("failureCauses", failureCauses) + .toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/failureDetector/FailureDetector.java b/presto-main/src/main/java/com/facebook/presto/failureDetector/FailureDetector.java new file mode 100644 index 00000000..e2187f73 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/failureDetector/FailureDetector.java @@ -0,0 +1,23 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.failureDetector; + +import io.airlift.discovery.client.ServiceDescriptor; + +import java.util.Set; + +public interface FailureDetector +{ + Set getFailed(); +} diff --git a/presto-main/src/main/java/com/facebook/presto/failureDetector/FailureDetectorConfig.java b/presto-main/src/main/java/com/facebook/presto/failureDetector/FailureDetectorConfig.java new file mode 100644 index 00000000..4384261c --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/failureDetector/FailureDetectorConfig.java @@ -0,0 +1,102 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.failureDetector; + +import io.airlift.configuration.Config; +import io.airlift.configuration.ConfigDescription; +import io.airlift.units.Duration; +import io.airlift.units.MinDuration; + +import javax.validation.constraints.DecimalMax; +import javax.validation.constraints.DecimalMin; +import javax.validation.constraints.NotNull; + +import java.util.concurrent.TimeUnit; + +public class FailureDetectorConfig +{ + private boolean enabled = true; + private double failureRatioThreshold = 0.01; // 1% failure rate + private Duration heartbeatInterval = new Duration(500, TimeUnit.MILLISECONDS); + private Duration warmupInterval = new Duration(5, TimeUnit.SECONDS); + private Duration expirationGraceInterval = new Duration(10, TimeUnit.MINUTES); + + @NotNull + public Duration getExpirationGraceInterval() + { + return expirationGraceInterval; + } + + @Config("failure-detector.expiration-grace-interval") + @ConfigDescription("How long to wait before 'forgetting' a service after it disappears from discovery") + public FailureDetectorConfig setExpirationGraceInterval(Duration expirationGraceInterval) + { + this.expirationGraceInterval = expirationGraceInterval; + return this; + } + + public boolean isEnabled() + { + return enabled; + } + + @Config("failure-detector.enabled") + public FailureDetectorConfig setEnabled(boolean enabled) + { + this.enabled = enabled; + return this; + } + + @NotNull + public Duration getWarmupInterval() + { + return warmupInterval; + } + + @Config("failure-detector.warmup-interval") + @ConfigDescription("How long to wait after transitioning to success before considering a service alive") + public FailureDetectorConfig setWarmupInterval(Duration warmupInterval) + { + this.warmupInterval = warmupInterval; + return this; + } + + @MinDuration("1ms") + @NotNull + public Duration getHeartbeatInterval() + { + return heartbeatInterval; + } + + @Config("failure-detector.heartbeat-interval") + public FailureDetectorConfig setHeartbeatInterval(Duration interval) + { + this.heartbeatInterval = interval; + return this; + } + + @DecimalMin("0.0") + @DecimalMax("1.0") + public double getFailureRatioThreshold() + { + return failureRatioThreshold; + } + + @Config("failure-detector.threshold") + public FailureDetectorConfig setFailureRatioThreshold(double threshold) + { + this.failureRatioThreshold = threshold; + return this; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/failureDetector/FailureDetectorModule.java b/presto-main/src/main/java/com/facebook/presto/failureDetector/FailureDetectorModule.java new file mode 100644 index 00000000..067db000 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/failureDetector/FailureDetectorModule.java @@ -0,0 +1,47 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.failureDetector; + +import com.google.inject.Binder; +import com.google.inject.Module; +import com.google.inject.Scopes; +import org.weakref.jmx.guice.ExportBinder; + +import static io.airlift.configuration.ConfigBinder.configBinder; +import static io.airlift.http.client.HttpClientBinder.httpClientBinder; + +public class FailureDetectorModule + implements Module +{ + @Override + public void configure(Binder binder) + { + httpClientBinder(binder) + .bindHttpClient("failure-detector", ForFailureDetector.class) + .withPrivateIoThreadPool() + .withTracing(); + + configBinder(binder).bindConfig(FailureDetectorConfig.class); + + binder.bind(HeartbeatFailureDetector.class).in(Scopes.SINGLETON); + + binder.bind(FailureDetector.class) + .to(HeartbeatFailureDetector.class) + .in(Scopes.SINGLETON); + + ExportBinder.newExporter(binder) + .export(HeartbeatFailureDetector.class) + .withGeneratedName(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/failureDetector/ForFailureDetector.java b/presto-main/src/main/java/com/facebook/presto/failureDetector/ForFailureDetector.java new file mode 100644 index 00000000..a149d5d8 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/failureDetector/ForFailureDetector.java @@ -0,0 +1,31 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.failureDetector; + +import com.google.inject.BindingAnnotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention(RUNTIME) +@Target({FIELD, PARAMETER, METHOD}) +@BindingAnnotation +public @interface ForFailureDetector +{ +} diff --git a/presto-main/src/main/java/com/facebook/presto/failureDetector/HeartbeatFailureDetector.java b/presto-main/src/main/java/com/facebook/presto/failureDetector/HeartbeatFailureDetector.java new file mode 100644 index 00000000..f4973853 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/failureDetector/HeartbeatFailureDetector.java @@ -0,0 +1,463 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.failureDetector; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableMap; +import io.airlift.discovery.client.ServiceDescriptor; +import io.airlift.discovery.client.ServiceSelector; +import io.airlift.discovery.client.ServiceType; +import io.airlift.http.client.HttpClient; +import io.airlift.http.client.Request; +import io.airlift.http.client.Response; +import io.airlift.http.client.ResponseHandler; +import io.airlift.log.Logger; +import io.airlift.node.NodeInfo; +import io.airlift.stats.DecayCounter; +import io.airlift.stats.ExponentialDecay; +import io.airlift.units.Duration; +import org.joda.time.DateTime; +import org.weakref.jmx.Managed; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import javax.annotation.concurrent.GuardedBy; +import javax.annotation.concurrent.ThreadSafe; +import javax.inject.Inject; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import static com.facebook.presto.util.ImmutableCollectors.toImmutableList; +import static com.facebook.presto.util.ImmutableCollectors.toImmutableSet; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.airlift.concurrent.Threads.daemonThreadsNamed; +import static io.airlift.http.client.Request.Builder.prepareHead; +import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor; + +public class HeartbeatFailureDetector + implements FailureDetector +{ + private static final Logger log = Logger.get(HeartbeatFailureDetector.class); + + private final ServiceSelector selector; + private final HttpClient httpClient; + private final NodeInfo nodeInfo; + + private final ScheduledExecutorService executor = newSingleThreadScheduledExecutor(daemonThreadsNamed("failure-detector")); + + // monitoring tasks by service id + private final ConcurrentMap tasks = new ConcurrentHashMap<>(); + + private final double failureRatioThreshold; + private final Duration heartbeat; + private final boolean isEnabled; + private final Duration warmupInterval; + private final Duration gcGraceInterval; + + private final AtomicBoolean started = new AtomicBoolean(); + + @Inject + public HeartbeatFailureDetector( + @ServiceType("presto") ServiceSelector selector, + @ForFailureDetector HttpClient httpClient, + FailureDetectorConfig config, + NodeInfo nodeInfo) + { + checkNotNull(selector, "selector is null"); + checkNotNull(httpClient, "httpClient is null"); + checkNotNull(nodeInfo, "nodeInfo is null"); + checkNotNull(config, "config is null"); + checkArgument(config.getHeartbeatInterval().toMillis() >= 1, "heartbeat interval must be >= 1ms"); + + this.selector = selector; + this.httpClient = httpClient; + this.nodeInfo = nodeInfo; + + this.failureRatioThreshold = config.getFailureRatioThreshold(); + this.heartbeat = config.getHeartbeatInterval(); + this.warmupInterval = config.getWarmupInterval(); + this.gcGraceInterval = config.getExpirationGraceInterval(); + + this.isEnabled = config.isEnabled(); + } + + @PostConstruct + public void start() + { + if (isEnabled && started.compareAndSet(false, true)) { + executor.scheduleWithFixedDelay(new Runnable() + { + @Override + public void run() + { + try { + updateMonitoredServices(); + } + catch (Throwable e) { + // ignore to avoid getting unscheduled + log.warn(e, "Error updating services"); + } + } + }, 0, 5, TimeUnit.SECONDS); + } + } + + @PreDestroy + public void shutdown() + { + executor.shutdownNow(); + } + + @Override + public Set getFailed() + { + return tasks.values().stream() + .filter(MonitoringTask::isFailed) + .map(MonitoringTask::getService) + .collect(toImmutableSet()); + } + + @Managed(description = "Number of failed services") + public int getFailedCount() + { + return getFailed().size(); + } + + @Managed(description = "Total number of known services") + public int getTotalCount() + { + return tasks.size(); + } + + @Managed + public int getActiveCount() + { + return tasks.size() - getFailed().size(); + } + + public Map getStats() + { + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (MonitoringTask task : tasks.values()) { + builder.put(task.getService(), task.getStats()); + } + return builder.build(); + } + + @VisibleForTesting + void updateMonitoredServices() + { + Set online = selector.selectAllServices().stream() + .filter(descriptor -> !nodeInfo.getNodeId().equals(descriptor.getNodeId())) + .collect(toImmutableSet()); + + Set onlineIds = online.stream() + .map(ServiceDescriptor::getId) + .collect(toImmutableSet()); + + // make sure only one thread is updating the registrations + synchronized (tasks) { + // 1. remove expired tasks + List expiredIds = tasks.values().stream() + .filter(MonitoringTask::isExpired) + .map(MonitoringTask::getService) + .map(ServiceDescriptor::getId) + .collect(toImmutableList()); + + tasks.keySet().removeAll(expiredIds); + + // 2. disable offline services + tasks.values().stream() + .filter(task -> !onlineIds.contains(task.getService().getId())) + .forEach(MonitoringTask::disable); + + // 3. create tasks for new services + Set newServices = online.stream() + .filter(service -> !tasks.keySet().contains(service.getId())) + .collect(toImmutableSet()); + + for (ServiceDescriptor service : newServices) { + URI uri = getHttpUri(service); + + if (uri != null) { + tasks.put(service.getId(), new MonitoringTask(service, uri)); + } + } + + // 4. enable all online tasks (existing plus newly created) + tasks.values().stream() + .filter(task -> onlineIds.contains(task.getService().getId())) + .forEach(MonitoringTask::enable); + } + } + + private static URI getHttpUri(ServiceDescriptor service) + { + try { + String uri = service.getProperties().get("http"); + if (uri != null) { + return new URI(uri); + } + } + catch (URISyntaxException e) { + // ignore, not a valid http uri + } + + return null; + } + + @ThreadSafe + private class MonitoringTask + { + private final ServiceDescriptor service; + private final URI uri; + private final Stats stats; + + @GuardedBy("this") + private ScheduledFuture future; + + @GuardedBy("this") + private Long disabledTimestamp; + + @GuardedBy("this") + private Long successTransitionTimestamp; + + private MonitoringTask(ServiceDescriptor service, URI uri) + { + this.uri = uri; + this.service = service; + this.stats = new Stats(uri); + } + + public Stats getStats() + { + return stats; + } + + public ServiceDescriptor getService() + { + return service; + } + + public synchronized void enable() + { + if (future == null) { + future = executor.scheduleAtFixedRate(new Runnable() + { + @Override + public void run() + { + try { + ping(); + updateState(); + } + catch (Throwable e) { + // ignore to avoid getting unscheduled + log.warn(e, "Error pinging service %s (%s)", service.getId(), uri); + } + } + }, heartbeat.toMillis(), heartbeat.toMillis(), TimeUnit.MILLISECONDS); + disabledTimestamp = null; + } + } + + public synchronized void disable() + { + if (future != null) { + future.cancel(true); + future = null; + disabledTimestamp = System.nanoTime(); + } + } + + public synchronized boolean isExpired() + { + return future == null && disabledTimestamp != null && Duration.nanosSince(disabledTimestamp).compareTo(gcGraceInterval) > 0; + } + + public synchronized boolean isFailed() + { + return future == null || // are we disabled? + successTransitionTimestamp == null || // are we in success state? + Duration.nanosSince(successTransitionTimestamp).compareTo(warmupInterval) < 0; // are we within the warmup period? + } + + private void ping() + { + try { + stats.recordStart(); + httpClient.executeAsync(prepareHead().setUri(uri).build(), new ResponseHandler() + { + @Override + public Exception handleException(Request request, Exception exception) + { + // ignore error + stats.recordFailure(exception); + + // TODO: this will technically cause an NPE in httpClient, but it's not triggered because + // we never call get() on the response future. This behavior needs to be fixed in airlift + return null; + } + + @Override + public Object handle(Request request, Response response) + throws Exception + { + stats.recordSuccess(); + return null; + } + }); + } + catch (RuntimeException e) { + log.warn(e, "Error scheduling request for %s", uri); + } + } + + private synchronized void updateState() + { + // is this an over/under transition? + if (stats.getRecentFailureRatio() > failureRatioThreshold) { + successTransitionTimestamp = null; + } + else if (successTransitionTimestamp == null) { + successTransitionTimestamp = System.nanoTime(); + } + } + } + + public static class Stats + { + private final long start = System.nanoTime(); + private final URI uri; + + private final DecayCounter recentRequests = new DecayCounter(ExponentialDecay.oneMinute()); + private final DecayCounter recentFailures = new DecayCounter(ExponentialDecay.oneMinute()); + private final DecayCounter recentSuccesses = new DecayCounter(ExponentialDecay.oneMinute()); + private final AtomicReference lastRequestTime = new AtomicReference<>(); + private final AtomicReference lastResponseTime = new AtomicReference<>(); + + @GuardedBy("this") + private final Map, DecayCounter> failureCountByType = new HashMap<>(); + + public Stats(URI uri) + { + this.uri = uri; + } + + public void recordStart() + { + recentRequests.add(1); + lastRequestTime.set(new DateTime()); + } + + public void recordSuccess() + { + recentSuccesses.add(1); + lastResponseTime.set(new DateTime()); + } + + public void recordFailure(Exception exception) + { + recentFailures.add(1); + lastResponseTime.set(new DateTime()); + + Throwable cause = exception; + while (cause.getClass() == RuntimeException.class && cause.getCause() != null) { + cause = cause.getCause(); + } + + synchronized (this) { + DecayCounter counter = failureCountByType.get(cause.getClass()); + if (counter == null) { + counter = new DecayCounter(ExponentialDecay.oneMinute()); + failureCountByType.put(cause.getClass(), counter); + } + counter.add(1); + } + } + + @JsonProperty + public Duration getAge() + { + return Duration.nanosSince(start); + } + + @JsonProperty + public URI getUri() + { + return uri; + } + + @JsonProperty + public double getRecentFailures() + { + return recentFailures.getCount(); + } + + @JsonProperty + public double getRecentSuccesses() + { + return recentSuccesses.getCount(); + } + + @JsonProperty + public double getRecentRequests() + { + return recentRequests.getCount(); + } + + @JsonProperty + public double getRecentFailureRatio() + { + return recentFailures.getCount() / recentRequests.getCount(); + } + + @JsonProperty + public DateTime getLastRequestTime() + { + return lastRequestTime.get(); + } + + @JsonProperty + public DateTime getLastResponseTime() + { + return lastResponseTime.get(); + } + + @JsonProperty + public synchronized Map getRecentFailuresByType() + { + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (Map.Entry, DecayCounter> entry : failureCountByType.entrySet()) { + builder.put(entry.getKey().getName(), entry.getValue().getCount()); + } + return builder.build(); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/index/IndexHandleJacksonModule.java b/presto-main/src/main/java/com/facebook/presto/index/IndexHandleJacksonModule.java new file mode 100644 index 00000000..56d5388e --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/index/IndexHandleJacksonModule.java @@ -0,0 +1,56 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.index; + +import com.facebook.presto.metadata.AbstractTypedJacksonModule; +import com.facebook.presto.metadata.HandleResolver; +import com.facebook.presto.metadata.JsonTypeIdResolver; +import com.facebook.presto.spi.ConnectorIndexHandle; + +import javax.inject.Inject; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class IndexHandleJacksonModule + extends AbstractTypedJacksonModule +{ + @Inject + public IndexHandleJacksonModule(HandleResolver handleResolver) + { + super(ConnectorIndexHandle.class, "type", new IndexHandleJsonTypeIdResolver(handleResolver)); + } + + private static class IndexHandleJsonTypeIdResolver + implements JsonTypeIdResolver + { + private final HandleResolver handleResolver; + + private IndexHandleJsonTypeIdResolver(HandleResolver handleResolver) + { + this.handleResolver = checkNotNull(handleResolver, "handleResolver is null"); + } + + @Override + public String getId(ConnectorIndexHandle indexHandle) + { + return handleResolver.getId(indexHandle); + } + + @Override + public Class getType(String id) + { + return handleResolver.getIndexHandleClass(id); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/index/IndexManager.java b/presto-main/src/main/java/com/facebook/presto/index/IndexManager.java new file mode 100644 index 00000000..6a5207c9 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/index/IndexManager.java @@ -0,0 +1,80 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.index; + +import com.facebook.presto.metadata.IndexHandle; +import com.facebook.presto.metadata.ResolvedIndex; +import com.facebook.presto.metadata.TableHandle; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorIndex; +import com.facebook.presto.spi.ConnectorIndexResolver; +import com.facebook.presto.spi.ConnectorResolvedIndex; +import com.facebook.presto.spi.TupleDomain; + +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; + +public class IndexManager +{ + private final ConcurrentMap resolvers = new ConcurrentHashMap<>(); + + public void addIndexResolver(String connectorId, ConnectorIndexResolver resolver) + { + checkState(resolvers.putIfAbsent(connectorId, resolver) == null, "IndexResolver for connector '%s' is already registered", connectorId); + } + + public void removeIndexResolver(String connectorId) + { + if (resolvers.containsKey(connectorId)) { + resolvers.remove(connectorId); + } + } + + public Optional resolveIndex(TableHandle tableHandle, Set indexableColumns, TupleDomain tupleDomain) + { + ConnectorIndexResolver resolver = resolvers.get(tableHandle.getConnectorId()); + if (resolver == null) { + return Optional.empty(); + } + + ConnectorResolvedIndex resolved = resolver.resolveIndex(tableHandle.getConnectorHandle(), indexableColumns, tupleDomain); + + if (resolved == null) { + return Optional.empty(); + } + + return Optional.of(new ResolvedIndex(tableHandle.getConnectorId(), resolved)); + } + + public ConnectorIndex getIndex(IndexHandle indexHandle, List lookupSchema, List outputSchema) + { + return getResolver(indexHandle) + .getIndex(indexHandle.getConnectorHandle(), lookupSchema, outputSchema); + } + + private ConnectorIndexResolver getResolver(IndexHandle handle) + { + ConnectorIndexResolver result = resolvers.get(handle.getConnectorId()); + + checkArgument(result != null, "No index resolver for connector '%s'", handle.getConnectorId()); + + return result; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/memory/ClusterMemoryManager.java b/presto-main/src/main/java/com/facebook/presto/memory/ClusterMemoryManager.java new file mode 100644 index 00000000..017d2876 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/memory/ClusterMemoryManager.java @@ -0,0 +1,285 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.memory; + +import com.facebook.presto.ExceededMemoryLimitException; +import com.facebook.presto.execution.LocationFactory; +import com.facebook.presto.execution.QueryExecution; +import com.facebook.presto.execution.QueryIdGenerator; +import com.facebook.presto.server.ServerConfig; +import com.facebook.presto.spi.Node; +import com.facebook.presto.spi.NodeManager; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import io.airlift.http.client.HttpClient; +import io.airlift.json.JsonCodec; +import io.airlift.log.Logger; +import io.airlift.units.DataSize; +import io.airlift.units.DataSize.Unit; + +import org.weakref.jmx.JmxException; +import org.weakref.jmx.MBeanExporter; +import org.weakref.jmx.Managed; +import org.weakref.jmx.ObjectNames; + +import javax.annotation.PreDestroy; +import javax.annotation.concurrent.GuardedBy; +import javax.inject.Inject; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; + +import static com.facebook.presto.memory.LocalMemoryManager.GENERAL_POOL; +import static com.facebook.presto.memory.LocalMemoryManager.RESERVED_POOL; +import static com.facebook.presto.SystemSessionProperties.getQueryMaxMemory; +import static com.facebook.presto.util.ImmutableCollectors.toImmutableList; +import static com.facebook.presto.util.ImmutableCollectors.toImmutableSet; +import static com.google.common.collect.Sets.difference; +import static java.util.Objects.requireNonNull; + +public class ClusterMemoryManager +{ + private static final Logger log = Logger.get(ClusterMemoryManager.class); + private final NodeManager nodeManager; + private final LocationFactory locationFactory; + private final HttpClient httpClient; + private final MBeanExporter exporter; + private final JsonCodec memoryInfoCodec; + private final JsonCodec assignmentsRequestJsonCodec; + private final DataSize maxQueryMemory; + private final boolean enabled; + private final String coordinatorId; + private final AtomicLong memoryPoolAssignmentsVersion = new AtomicLong(); + private final AtomicLong clusterMemoryUsageBytes = new AtomicLong(); + private final AtomicLong clusterMemoryBytes = new AtomicLong(); + private final Map nodes = new HashMap<>(); + + @GuardedBy("this") + private final Map pools = new HashMap<>(); + + @Inject + public ClusterMemoryManager( + @ForMemoryManager HttpClient httpClient, + NodeManager nodeManager, + LocationFactory locationFactory, + MBeanExporter exporter, + JsonCodec memoryInfoCodec, + JsonCodec assignmentsRequestJsonCodec, + QueryIdGenerator queryIdGenerator, + ServerConfig serverConfig, + MemoryManagerConfig config) + { + requireNonNull(config, "config is null"); + this.nodeManager = requireNonNull(nodeManager, "nodeManager is null"); + this.locationFactory = requireNonNull(locationFactory, "locationFactory is null"); + this.httpClient = requireNonNull(httpClient, "httpClient is null"); + this.exporter = requireNonNull(exporter, "exporter is null"); + this.memoryInfoCodec = requireNonNull(memoryInfoCodec, "memoryInfoCodec is null"); + this.assignmentsRequestJsonCodec = requireNonNull(assignmentsRequestJsonCodec, "assignmentsRequestJsonCodec is null"); + this.maxQueryMemory = config.getMaxQueryMemory(); + this.coordinatorId = queryIdGenerator.getCoordinatorId(); + this.enabled = config.isClusterMemoryManagerEnabled() && serverConfig.isCoordinator(); + } + + public void process(Iterable queries) + { + if (!enabled) { + return; + } + long totalBytes = 0; + for (QueryExecution query : queries) { + long bytes = query.getTotalMemoryReservation(); + DataSize sessionMaxQueryMemory = getQueryMaxMemory(query.getSession(), maxQueryMemory); + long queryMemoryLimit = Math.min(maxQueryMemory.toBytes(), sessionMaxQueryMemory.toBytes()); + totalBytes += bytes; + if (bytes > queryMemoryLimit) { + query.fail(new ExceededMemoryLimitException("Query", DataSize.succinctDataSize(queryMemoryLimit, Unit.BYTE))); + } + } + clusterMemoryUsageBytes.set(totalBytes); + + Map countByPool = new HashMap<>(); + for (QueryExecution query : queries) { + MemoryPoolId id = query.getMemoryPool().getId(); + countByPool.put(id, countByPool.getOrDefault(id, 0) + 1); + } + + updatePools(countByPool); + + updateNodes(updateAssignments(queries)); + } + + @VisibleForTesting + synchronized Map getPools() + { + return ImmutableMap.copyOf(pools); + } + + private MemoryPoolAssignmentsRequest updateAssignments(Iterable queries) + { + ClusterMemoryPool reservedPool = pools.get(RESERVED_POOL); + ClusterMemoryPool generalPool = pools.get(GENERAL_POOL); + long version = memoryPoolAssignmentsVersion.incrementAndGet(); + // Check that all previous assignments have propagated to the visible nodes. This doesn't account for temporary network issues, + // and is more of a safety check than a guarantee + if (reservedPool != null && generalPool != null && allAssignmentsHavePropagated(queries)) { + if (reservedPool.getQueries() == 0 && generalPool.getBlockedNodes() > 0) { + QueryExecution biggestQuery = null; + long maxMemory = -1; + for (QueryExecution queryExecution : queries) { + long bytesUsed = queryExecution.getTotalMemoryReservation(); + if (bytesUsed > maxMemory) { + biggestQuery = queryExecution; + maxMemory = bytesUsed; + } + } + for (QueryExecution queryExecution : queries) { + if (queryExecution.getQueryId().equals(biggestQuery.getQueryId())) { + queryExecution.setMemoryPool(new VersionedMemoryPoolId(RESERVED_POOL, version)); + } + } + } + } + + ImmutableList.Builder assignments = ImmutableList.builder(); + for (QueryExecution queryExecution : queries) { + assignments.add(new MemoryPoolAssignment(queryExecution.getQueryId(), queryExecution.getMemoryPool().getId())); + } + return new MemoryPoolAssignmentsRequest(coordinatorId, version, assignments.build()); + } + + private boolean allAssignmentsHavePropagated(Iterable queries) + { + if (nodes.isEmpty()) { + // Assignments can't have propagated, if there are no visible nodes. + return false; + } + long newestAssignment = ImmutableList.copyOf(queries).stream() + .map(QueryExecution::getMemoryPool) + .mapToLong(VersionedMemoryPoolId::getVersion) + .min() + .orElse(-1); + + long mostOutOfDateNode = nodes.values().stream() + .mapToLong(RemoteNodeMemory::getCurrentAssignmentVersion) + .min() + .orElse(Long.MAX_VALUE); + + return newestAssignment <= mostOutOfDateNode; + } + + private void updateNodes(MemoryPoolAssignmentsRequest assignments) + { + Set activeNodes = nodeManager.getActiveNodes(); + ImmutableSet activeNodeIds = activeNodes.stream() + .map(Node::getNodeIdentifier) + .collect(toImmutableSet()); + + // Remove nodes that don't exist anymore + // Make a copy to materialize the set difference + Set deadNodes = ImmutableSet.copyOf(difference(nodes.keySet(), activeNodeIds)); + nodes.keySet().removeAll(deadNodes); + + // Add new nodes + for (Node node : activeNodes) { + if (!nodes.containsKey(node.getNodeIdentifier())) { + nodes.put(node.getNodeIdentifier(), new RemoteNodeMemory(httpClient, memoryInfoCodec, assignmentsRequestJsonCodec, locationFactory.createMemoryInfoLocation(node))); + } + } + + // Schedule refresh + for (RemoteNodeMemory node : nodes.values()) { + node.asyncRefresh(assignments); + } + } + + private synchronized void updatePools(Map queryCounts) + { + // Update view of cluster memory and pools + List nodeMemoryInfos = nodes.values().stream() + .map(RemoteNodeMemory::getInfo) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(toImmutableList()); + + long totalClusterMemory = nodeMemoryInfos.stream() + .map(MemoryInfo::getTotalNodeMemory) + .mapToLong(DataSize::toBytes) + .sum(); + clusterMemoryBytes.set(totalClusterMemory); + + Set activePoolIds = nodeMemoryInfos.stream() + .flatMap(info -> info.getPools().keySet().stream()) + .collect(toImmutableSet()); + + // Make a copy to materialize the set difference + Set removedPools = ImmutableSet.copyOf(difference(pools.keySet(), activePoolIds)); + for (MemoryPoolId removed : removedPools) { + unexport(pools.get(removed)); + pools.remove(removed); + } + for (MemoryPoolId id : activePoolIds) { + ClusterMemoryPool pool = pools.computeIfAbsent(id, poolId -> { + ClusterMemoryPool newPool = new ClusterMemoryPool(poolId); + String objectName = ObjectNames.builder(ClusterMemoryPool.class, newPool.getId().toString()).build(); + try { + exporter.export(objectName, newPool); + } + catch (JmxException e) { + log.error(e, "Error exporting memory pool %s", poolId); + } + return newPool; + }); + pool.update(nodeMemoryInfos, queryCounts.getOrDefault(pool.getId(), 0)); + } + } + + @PreDestroy + public synchronized void destroy() + { + for (ClusterMemoryPool pool : pools.values()) { + unexport(pool); + } + pools.clear(); + } + + private void unexport(ClusterMemoryPool pool) + { + try { + String objectName = ObjectNames.builder(ClusterMemoryPool.class, pool.getId().toString()).build(); + exporter.unexport(objectName); + } + catch (JmxException e) { + log.error(e, "Failed to unexport pool %s", pool.getId()); + } + } + + @Managed + public long getClusterMemoryUsageBytes() + { + return clusterMemoryUsageBytes.get(); + } + + @Managed + public long getClusterMemoryBytes() + { + return clusterMemoryBytes.get(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/memory/ClusterMemoryPool.java b/presto-main/src/main/java/com/facebook/presto/memory/ClusterMemoryPool.java new file mode 100644 index 00000000..1fd68136 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/memory/ClusterMemoryPool.java @@ -0,0 +1,139 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.memory; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; +import org.weakref.jmx.Managed; + +import javax.annotation.concurrent.GuardedBy; +import javax.annotation.concurrent.ThreadSafe; + +import java.util.List; + +import static java.util.Objects.requireNonNull; + +@ThreadSafe +public class ClusterMemoryPool +{ + private final MemoryPoolId id; + + @GuardedBy("this") + private long totalDistributedBytes; + + @GuardedBy("this") + private long freeDistributedBytes; + + @GuardedBy("this") + private int nodes; + + @GuardedBy("this") + private int blockedNodes; + + @GuardedBy("this") + private int queries; + + public ClusterMemoryPool(MemoryPoolId id) + { + this.id = requireNonNull(id, "id is null"); + } + + public MemoryPoolId getId() + { + return id; + } + + @Managed + public synchronized long getTotalDistributedBytes() + { + return totalDistributedBytes; + } + + @Managed + public synchronized long getFreeDistributedBytes() + { + return freeDistributedBytes; + } + + @Managed + public synchronized int getNodes() + { + return nodes; + } + + @Managed + public synchronized int getBlockedNodes() + { + return blockedNodes; + } + + @Managed + public synchronized int getQueries() + { + return queries; + } + + public synchronized void update(List memoryInfos, int queries) + { + nodes = 0; + blockedNodes = 0; + totalDistributedBytes = 0; + freeDistributedBytes = 0; + this.queries = queries; + + for (MemoryInfo info : memoryInfos) { + MemoryPoolInfo poolInfo = info.getPools().get(id); + if (poolInfo != null) { + nodes++; + if (poolInfo.getFreeBytes() <= 0) { + blockedNodes++; + } + totalDistributedBytes += poolInfo.getMaxBytes(); + freeDistributedBytes += poolInfo.getFreeBytes(); + } + } + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ClusterMemoryPool that = (ClusterMemoryPool) o; + return Objects.equal(id, that.id); + } + + @Override + public int hashCode() + { + return Objects.hashCode(id); + } + + @Override + public synchronized String toString() + { + return MoreObjects.toStringHelper(this) + .add("id", id) + .add("totalDistributedBytes", totalDistributedBytes) + .add("freeDistributedBytes", freeDistributedBytes) + .add("nodes", nodes) + .add("blockedNodes", blockedNodes) + .add("queries", queries) + .toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/memory/ForMemoryManager.java b/presto-main/src/main/java/com/facebook/presto/memory/ForMemoryManager.java new file mode 100644 index 00000000..469a1331 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/memory/ForMemoryManager.java @@ -0,0 +1,31 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.memory; + +import javax.inject.Qualifier; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention(RUNTIME) +@Target({FIELD, PARAMETER, METHOD}) +@Qualifier +public @interface ForMemoryManager +{ +} diff --git a/presto-main/src/main/java/com/facebook/presto/memory/LocalMemoryManager.java b/presto-main/src/main/java/com/facebook/presto/memory/LocalMemoryManager.java new file mode 100644 index 00000000..50ce4398 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/memory/LocalMemoryManager.java @@ -0,0 +1,71 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.memory; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.airlift.units.DataSize; + +import javax.inject.Inject; + +import java.util.List; +import java.util.Map; + +import static com.google.common.base.Preconditions.checkArgument; +import static io.airlift.units.DataSize.Unit.BYTE; +import static java.util.Objects.requireNonNull; + +public final class LocalMemoryManager +{ + public static final MemoryPoolId GENERAL_POOL = new MemoryPoolId("general"); + public static final MemoryPoolId RESERVED_POOL = new MemoryPoolId("reserved"); + + private final DataSize maxMemory; + private final Map pools; + + @Inject + public LocalMemoryManager(MemoryManagerConfig config, ReservedSystemMemoryConfig systemMemoryConfig) + { + requireNonNull(config, "config is null"); + requireNonNull(systemMemoryConfig, "systemMemoryConfig is null"); + long maxHeap = Runtime.getRuntime().maxMemory(); + checkArgument(systemMemoryConfig.getReservedSystemMemory().toBytes() < maxHeap, "Reserved memory %s is greater than available heap %s", systemMemoryConfig.getReservedSystemMemory(), new DataSize(maxHeap, BYTE)); + maxMemory = new DataSize(maxHeap - systemMemoryConfig.getReservedSystemMemory().toBytes(), BYTE); + + ImmutableMap.Builder builder = ImmutableMap.builder(); + builder.put(RESERVED_POOL, new MemoryPool(RESERVED_POOL, config.getMaxQueryMemoryPerNode(), config.isClusterMemoryManagerEnabled())); + DataSize generalPoolSize = new DataSize(Math.max(0, maxMemory.toBytes() - config.getMaxQueryMemoryPerNode().toBytes()), BYTE); + builder.put(GENERAL_POOL, new MemoryPool(GENERAL_POOL, generalPoolSize, config.isClusterMemoryManagerEnabled())); + this.pools = builder.build(); + } + + public MemoryInfo getInfo() + { + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (Map.Entry entry : pools.entrySet()) { + builder.put(entry.getKey(), entry.getValue().getInfo()); + } + return new MemoryInfo(maxMemory, builder.build()); + } + + public List getPools() + { + return ImmutableList.copyOf(pools.values()); + } + + public MemoryPool getPool(MemoryPoolId id) + { + return pools.get(id); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/memory/LocalMemoryManagerExporter.java b/presto-main/src/main/java/com/facebook/presto/memory/LocalMemoryManagerExporter.java new file mode 100644 index 00000000..d4c8c17a --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/memory/LocalMemoryManagerExporter.java @@ -0,0 +1,70 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.memory; + +import org.weakref.jmx.JmxException; +import org.weakref.jmx.MBeanExporter; +import org.weakref.jmx.ObjectNames; + +import javax.annotation.PreDestroy; +import javax.annotation.concurrent.GuardedBy; +import javax.inject.Inject; + +import java.util.ArrayList; +import java.util.List; + +import static java.util.Objects.requireNonNull; + +public final class LocalMemoryManagerExporter +{ + private final MBeanExporter exporter; + @GuardedBy("this") + private final List pools = new ArrayList<>(); + + @Inject + public LocalMemoryManagerExporter(LocalMemoryManager memoryManager, MBeanExporter exporter) + { + this.exporter = requireNonNull(exporter, "exporter is null"); + for (MemoryPool pool : memoryManager.getPools()) { + addPool(pool); + } + } + + private synchronized void addPool(MemoryPool pool) + { + try { + String objectName = ObjectNames.builder(MemoryPool.class, pool.getId().toString()).build(); + exporter.export(objectName, pool); + pools.add(pool); + } + catch (JmxException e) { + // ignored + } + } + + @PreDestroy + public synchronized void destroy() + { + for (MemoryPool pool : pools) { + String objectName = ObjectNames.builder(MemoryPool.class, pool.getId().toString()).build(); + try { + exporter.unexport(objectName); + } + catch (JmxException e) { + // ignored + } + } + pools.clear(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/memory/MemoryInfo.java b/presto-main/src/main/java/com/facebook/presto/memory/MemoryInfo.java new file mode 100644 index 00000000..6df69720 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/memory/MemoryInfo.java @@ -0,0 +1,58 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.memory; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableMap; +import io.airlift.units.DataSize; + +import java.util.Map; + +import static java.util.Objects.requireNonNull; + +public class MemoryInfo +{ + private final DataSize totalNodeMemory; + private final Map pools; + + @JsonCreator + public MemoryInfo(@JsonProperty("totalNodeMemory") DataSize totalNodeMemory, @JsonProperty("pools") Map pools) + { + this.totalNodeMemory = requireNonNull(totalNodeMemory, "totalNodeMemory is null"); + this.pools = ImmutableMap.copyOf(requireNonNull(pools, "pools is null")); + } + + @JsonProperty + public DataSize getTotalNodeMemory() + { + return totalNodeMemory; + } + + @JsonProperty + public Map getPools() + { + return pools; + } + + @Override + public String toString() + { + return MoreObjects.toStringHelper(this) + .add("totalNodeMemory", totalNodeMemory) + .add("pools", pools) + .toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/memory/MemoryManagerConfig.java b/presto-main/src/main/java/com/facebook/presto/memory/MemoryManagerConfig.java new file mode 100644 index 00000000..abd96e77 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/memory/MemoryManagerConfig.java @@ -0,0 +1,66 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.memory; + +import io.airlift.configuration.Config; +import io.airlift.units.DataSize; + +import javax.validation.constraints.NotNull; + +import static io.airlift.units.DataSize.Unit.GIGABYTE; + +public class MemoryManagerConfig +{ + private DataSize maxQueryMemory = new DataSize(20, GIGABYTE); + private DataSize maxQueryMemoryPerNode = new DataSize(1, GIGABYTE); + private boolean clusterMemoryManagerEnabled; + + @NotNull + public DataSize getMaxQueryMemory() + { + return maxQueryMemory; + } + + @Config("query.max-memory") + public MemoryManagerConfig setMaxQueryMemory(DataSize maxQueryMemory) + { + this.maxQueryMemory = maxQueryMemory; + return this; + } + + @NotNull + public DataSize getMaxQueryMemoryPerNode() + { + return maxQueryMemoryPerNode; + } + + @Config("query.max-memory-per-node") + public MemoryManagerConfig setMaxQueryMemoryPerNode(DataSize maxQueryMemoryPerNode) + { + this.maxQueryMemoryPerNode = maxQueryMemoryPerNode; + return this; + } + + public boolean isClusterMemoryManagerEnabled() + { + return clusterMemoryManagerEnabled; + } + + @Config("experimental.cluster-memory-manager-enabled") + public MemoryManagerConfig setClusterMemoryManagerEnabled(boolean clusterMemoryManagerEnabled) + { + this.clusterMemoryManagerEnabled = clusterMemoryManagerEnabled; + return this; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/memory/MemoryPool.java b/presto-main/src/main/java/com/facebook/presto/memory/MemoryPool.java new file mode 100644 index 00000000..3acd72fd --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/memory/MemoryPool.java @@ -0,0 +1,131 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.memory; + +import com.google.common.base.MoreObjects; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import io.airlift.units.DataSize; +import org.weakref.jmx.Managed; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.GuardedBy; + +import static com.facebook.presto.operator.Operator.NOT_BLOCKED; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; +import static java.util.Objects.requireNonNull; + +public class MemoryPool +{ + private final MemoryPoolId id; + private final long maxBytes; + private final boolean enableBlocking; + + @GuardedBy("this") + private long freeBytes; + + @Nullable + @GuardedBy("this") + private SettableFuture future; + + public MemoryPool(MemoryPoolId id, DataSize size, boolean enableBlocking) + { + this.id = requireNonNull(id, "name is null"); + requireNonNull(size, "size is null"); + maxBytes = size.toBytes(); + this.enableBlocking = enableBlocking; + freeBytes = size.toBytes(); + } + + public MemoryPoolId getId() + { + return id; + } + + public synchronized MemoryPoolInfo getInfo() + { + return new MemoryPoolInfo(maxBytes, freeBytes); + } + + /** + * Reserves the given number of bytes. Caller should wait on the returned future, before allocating more memory. + */ + public synchronized ListenableFuture reserve(long bytes) + { + checkArgument(bytes >= 0, "bytes is negative"); + freeBytes -= bytes; + if (freeBytes <= 0) { + if (future == null) { + future = SettableFuture.create(); + } + checkState(!future.isDone(), "future is already completed"); + if (enableBlocking) { + return future; + } + } + return NOT_BLOCKED; + } + + /** + * Try to reserve the given number of bytes. Return value indicates whether the caller may use the requested memory. + */ + public synchronized boolean tryReserve(long bytes) + { + checkArgument(bytes >= 0, "bytes is negative"); + if (freeBytes - bytes < 0) { + return false; + } + freeBytes -= bytes; + return true; + } + + public synchronized void free(long bytes) + { + checkArgument(bytes >= 0, "bytes is negative"); + checkArgument(freeBytes + bytes <= maxBytes, "tried to free more memory than is reserved"); + freeBytes += bytes; + if (freeBytes > 0 && future != null) { + future.set(null); + future = null; + } + } + + /** + * Returns the number of free bytes. This value may be negative, which indicates that the pool is over-committed. + */ + @Managed + public synchronized long getFreeBytes() + { + return freeBytes; + } + + @Managed + public synchronized long getMaxBytes() + { + return maxBytes; + } + + @Override + public synchronized String toString() + { + return MoreObjects.toStringHelper(this) + .add("id", id) + .add("maxBytes", maxBytes) + .add("enableBlocking", enableBlocking) + .add("freeBytes", freeBytes) + .add("future", future) + .toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/memory/MemoryPoolAssignment.java b/presto-main/src/main/java/com/facebook/presto/memory/MemoryPoolAssignment.java new file mode 100644 index 00000000..ffc088a8 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/memory/MemoryPoolAssignment.java @@ -0,0 +1,55 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.memory; + +import com.facebook.presto.execution.QueryId; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.MoreObjects; + +import static java.util.Objects.requireNonNull; + +public class MemoryPoolAssignment +{ + private final QueryId queryId; + private final MemoryPoolId poolId; + + @JsonCreator + public MemoryPoolAssignment(@JsonProperty("queryId") QueryId queryId, @JsonProperty("poolId") MemoryPoolId poolId) + { + this.queryId = requireNonNull(queryId, "queryId is null"); + this.poolId = requireNonNull(poolId, "poolId is null"); + } + + @JsonProperty + public QueryId getQueryId() + { + return queryId; + } + + @JsonProperty + public MemoryPoolId getPoolId() + { + return poolId; + } + + @Override + public String toString() + { + return MoreObjects.toStringHelper(this) + .add("queryId", queryId) + .add("poolId", poolId) + .toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/memory/MemoryPoolAssignmentsRequest.java b/presto-main/src/main/java/com/facebook/presto/memory/MemoryPoolAssignmentsRequest.java new file mode 100644 index 00000000..25543c61 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/memory/MemoryPoolAssignmentsRequest.java @@ -0,0 +1,65 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.memory; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static java.util.Objects.requireNonNull; + +public class MemoryPoolAssignmentsRequest +{ + private final String coordinatorId; + private final long version; + private final List assignments; + + @JsonCreator + public MemoryPoolAssignmentsRequest(@JsonProperty("coordinatorId") String coordinatorId, @JsonProperty("version") long version, @JsonProperty("assignments") List assignments) + { + this.coordinatorId = requireNonNull(coordinatorId, "coordinatorId is null"); + this.version = version; + this.assignments = ImmutableList.copyOf(requireNonNull(assignments, "assignments is null")); + } + + @JsonProperty + public String getCoordinatorId() + { + return coordinatorId; + } + + @JsonProperty + public long getVersion() + { + return version; + } + + @JsonProperty + public List getAssignments() + { + return assignments; + } + + @Override + public String toString() + { + return MoreObjects.toStringHelper(this) + .add("version", version) + .add("assignments", assignments) + .toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/memory/MemoryPoolId.java b/presto-main/src/main/java/com/facebook/presto/memory/MemoryPoolId.java new file mode 100644 index 00000000..4f4ec81a --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/memory/MemoryPoolId.java @@ -0,0 +1,66 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.memory; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import com.google.common.base.Objects; + +import static com.google.common.base.Preconditions.checkArgument; +import static java.util.Objects.requireNonNull; + +public class MemoryPoolId +{ + private final String id; + + @JsonCreator + public MemoryPoolId(String id) + { + requireNonNull(id, "id is null"); + checkArgument(!id.isEmpty(), "id is empty"); + this.id = id; + } + + @JsonValue + public String getId() + { + return id; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MemoryPoolId that = (MemoryPoolId) o; + return Objects.equal(id, that.id); + } + + @Override + public int hashCode() + { + return Objects.hashCode(id); + } + + @Override + public String toString() + { + // Return id here, because Jackson uses toString() when MemoryPoolId is the key of a Map + return id; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/memory/MemoryPoolInfo.java b/presto-main/src/main/java/com/facebook/presto/memory/MemoryPoolInfo.java new file mode 100644 index 00000000..cf00ad8f --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/memory/MemoryPoolInfo.java @@ -0,0 +1,52 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.memory; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.MoreObjects; + +public class MemoryPoolInfo +{ + private final long maxBytes; + private final long freeBytes; + + @JsonCreator + public MemoryPoolInfo(@JsonProperty("maxBytes") long maxBytes, @JsonProperty("freeBytes") long freeBytes) + { + this.maxBytes = maxBytes; + this.freeBytes = freeBytes; + } + + @JsonProperty + public long getMaxBytes() + { + return maxBytes; + } + + @JsonProperty + public long getFreeBytes() + { + return freeBytes; + } + + @Override + public String toString() + { + return MoreObjects.toStringHelper(this) + .add("maxBytes", maxBytes) + .add("freeBytes", freeBytes) + .toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/memory/MemoryResource.java b/presto-main/src/main/java/com/facebook/presto/memory/MemoryResource.java new file mode 100644 index 00000000..1451e6b8 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/memory/MemoryResource.java @@ -0,0 +1,51 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.memory; + +import com.facebook.presto.execution.TaskManager; + +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import static java.util.Objects.requireNonNull; + +/** + * Manages memory pools on this worker node + */ +@Path("/v1/memory") +public class MemoryResource +{ + private final LocalMemoryManager memoryManager; + private final TaskManager taskManager; + + @Inject + public MemoryResource(LocalMemoryManager memoryManager, TaskManager taskManager) + { + this.memoryManager = requireNonNull(memoryManager, "memoryManager is null"); + this.taskManager = requireNonNull(taskManager, "taskManager is null"); + } + + @POST + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + public MemoryInfo getMemoryInfo(MemoryPoolAssignmentsRequest request) + { + taskManager.updateMemoryPoolAssignments(request); + return memoryManager.getInfo(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/memory/QueryContext.java b/presto-main/src/main/java/com/facebook/presto/memory/QueryContext.java new file mode 100644 index 00000000..3d5dccaa --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/memory/QueryContext.java @@ -0,0 +1,127 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.memory; + +import com.facebook.presto.Session; +import com.facebook.presto.execution.TaskStateMachine; +import com.facebook.presto.operator.TaskContext; +import com.facebook.presto.spi.PrestoException; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import io.airlift.units.DataSize; + +import javax.annotation.concurrent.GuardedBy; +import javax.annotation.concurrent.ThreadSafe; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executor; + +import static com.facebook.presto.spi.StandardErrorCode.EXCEEDED_MEMORY_LIMIT; +import static com.google.common.base.Preconditions.checkArgument; +import static java.util.Objects.requireNonNull; + +@ThreadSafe +public class QueryContext +{ + private final long maxMemory; + private final boolean enforceLimit; + private final Executor executor; + private final List taskContexts = new CopyOnWriteArrayList<>(); + + @GuardedBy("this") + private long reserved; + + @GuardedBy("this") + private MemoryPool memoryPool; + + public QueryContext(boolean enforceLimit, DataSize maxMemory, MemoryPool memoryPool, Executor executor) + { + this.enforceLimit = enforceLimit; + this.maxMemory = requireNonNull(maxMemory, "maxMemory is null").toBytes(); + this.memoryPool = requireNonNull(memoryPool, "memoryPool is null"); + this.executor = requireNonNull(executor, "executor is null"); + } + + public synchronized ListenableFuture reserveMemory(long bytes) + { + checkArgument(bytes >= 0, "bytes is negative"); + + if (reserved + bytes > maxMemory && enforceLimit) { + throw new PrestoException(EXCEEDED_MEMORY_LIMIT, "Query exceeded local memory limit of " + new DataSize(maxMemory, DataSize.Unit.BYTE).convertToMostSuccinctDataSize()); + } + ListenableFuture future = memoryPool.reserve(bytes); + reserved += bytes; + return future; + } + + public synchronized boolean tryReserveMemory(long bytes) + { + checkArgument(bytes >= 0, "bytes is negative"); + + if (reserved + bytes > maxMemory && enforceLimit) { + return false; + } + if (memoryPool.tryReserve(bytes)) { + reserved += bytes; + return true; + } + return false; + } + + public synchronized void freeMemory(long bytes) + { + checkArgument(reserved - bytes >= 0, "tried to free more memory than is reserved"); + reserved -= bytes; + memoryPool.free(bytes); + } + + public synchronized void setMemoryPool(MemoryPool pool) + { + requireNonNull(pool, "pool is null"); + if (pool.getId().equals(memoryPool.getId())) { + // Don't unblock our tasks and thrash the pools, if this is a no-op + return; + } + MemoryPool originalPool = memoryPool; + long originalReserved = reserved; + memoryPool = pool; + ListenableFuture future = pool.reserve(reserved); + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(Object result) + { + originalPool.free(originalReserved); + // Unblock all the tasks, if they were waiting for memory, since we're in a new pool. + taskContexts.stream().forEach(TaskContext::moreMemoryAvailable); + } + + @Override + public void onFailure(Throwable t) + { + originalPool.free(originalReserved); + // Unblock all the tasks, if they were waiting for memory, since we're in a new pool. + taskContexts.stream().forEach(TaskContext::moreMemoryAvailable); + } + }); + } + + public TaskContext addTaskContext(TaskStateMachine taskStateMachine, Session session, DataSize maxTaskMemory, DataSize operatorPreAllocatedMemory, boolean verboseStats, boolean cpuTimerEnabled) + { + TaskContext taskContext = new TaskContext(this, taskStateMachine, executor, session, maxTaskMemory, operatorPreAllocatedMemory, verboseStats, cpuTimerEnabled); + taskContexts.add(taskContext); + return taskContext; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/memory/RemoteNodeMemory.java b/presto-main/src/main/java/com/facebook/presto/memory/RemoteNodeMemory.java new file mode 100644 index 00000000..0c30e724 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/memory/RemoteNodeMemory.java @@ -0,0 +1,128 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.memory; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import io.airlift.http.client.FullJsonResponseHandler.JsonResponse; +import io.airlift.http.client.HttpClient; +import io.airlift.http.client.HttpClient.HttpResponseFuture; +import io.airlift.http.client.Request; +import io.airlift.json.JsonCodec; +import io.airlift.log.Logger; +import io.airlift.units.Duration; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.ThreadSafe; + +import java.net.URI; +import java.util.Optional; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +import static com.google.common.net.MediaType.JSON_UTF_8; +import static io.airlift.http.client.FullJsonResponseHandler.createFullJsonResponseHandler; +import static io.airlift.http.client.HttpStatus.OK; +import static io.airlift.http.client.JsonBodyGenerator.jsonBodyGenerator; +import static io.airlift.http.client.Request.Builder.preparePost; +import static io.airlift.units.Duration.nanosSince; +import static java.util.Objects.requireNonNull; +import static java.util.Optional.empty; +import static java.util.Optional.ofNullable; +import static java.util.concurrent.TimeUnit.SECONDS; +import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE; + +@ThreadSafe +public class RemoteNodeMemory +{ + private static final Logger log = Logger.get(RemoteNodeMemory.class); + + private final HttpClient httpClient; + private final URI memoryInfoUri; + private final JsonCodec memoryInfoCodec; + private final JsonCodec assignmentsRequestJsonCodec; + private final AtomicReference> memoryInfo = new AtomicReference<>(empty()); + private final AtomicReference> future = new AtomicReference<>(); + private final AtomicLong lastUpdateNanos = new AtomicLong(); + private final AtomicLong lastWarningLogged = new AtomicLong(); + private final AtomicLong currentAssignmentVersion = new AtomicLong(-1); + + public RemoteNodeMemory(HttpClient httpClient, JsonCodec memoryInfoCodec, JsonCodec assignmentsRequestJsonCodec, URI memoryInfoUri) + { + this.httpClient = requireNonNull(httpClient, "httpClient is null"); + this.memoryInfoUri = requireNonNull(memoryInfoUri, "memoryInfoUri is null"); + this.memoryInfoCodec = requireNonNull(memoryInfoCodec, "memoryInfoCodec is null"); + this.assignmentsRequestJsonCodec = requireNonNull(assignmentsRequestJsonCodec, "assignmentsRequestJsonCodec is null"); + } + + public long getCurrentAssignmentVersion() + { + return currentAssignmentVersion.get(); + } + + public Optional getInfo() + { + return memoryInfo.get(); + } + + public void asyncRefresh(MemoryPoolAssignmentsRequest assignments) + { + Duration sinceUpdate = nanosSince(lastUpdateNanos.get()); + if (nanosSince(lastWarningLogged.get()).toMillis() > 1_000 && + sinceUpdate.toMillis() > 10_000 && + future.get() != null) { + log.warn("Memory info update request to %s has not returned in %s", memoryInfoUri, sinceUpdate.toString(SECONDS)); + lastWarningLogged.set(System.nanoTime()); + } + if (sinceUpdate.toMillis() > 1_000 && future.get() == null) { + Request request = preparePost() + .setUri(memoryInfoUri) + .setHeader(CONTENT_TYPE, JSON_UTF_8.toString()) + .setBodyGenerator(jsonBodyGenerator(assignmentsRequestJsonCodec, assignments)) + .build(); + HttpResponseFuture> responseFuture = httpClient.executeAsync(request, createFullJsonResponseHandler(memoryInfoCodec)); + future.compareAndSet(null, responseFuture); + + Futures.addCallback(responseFuture, new FutureCallback>() { + @Override + public void onSuccess(@Nullable JsonResponse result) + + { + lastUpdateNanos.set(System.nanoTime()); + future.compareAndSet(responseFuture, null); + long version = currentAssignmentVersion.get(); + if (result != null) { + if (result.hasValue()) { + memoryInfo.set(ofNullable(result.getValue())); + } + if (result.getStatusCode() != OK.code()) { + log.warn("Error fetching memory info from %s returned status %d: %s", memoryInfoUri, result.getStatusCode(), result.getStatusMessage()); + return; + } + } + currentAssignmentVersion.compareAndSet(version, assignments.getVersion()); + } + + @Override + public void onFailure(Throwable t) + { + log.warn(t, "Error fetching memory info from %s", memoryInfoUri); + lastUpdateNanos.set(System.nanoTime()); + future.compareAndSet(responseFuture, null); + } + }); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/memory/ReservedSystemMemoryConfig.java b/presto-main/src/main/java/com/facebook/presto/memory/ReservedSystemMemoryConfig.java new file mode 100644 index 00000000..11e05c67 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/memory/ReservedSystemMemoryConfig.java @@ -0,0 +1,40 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.memory; + +import io.airlift.configuration.Config; +import io.airlift.units.DataSize; + +import javax.validation.constraints.NotNull; + +import static io.airlift.units.DataSize.Unit.BYTE; + +// This is separate from MemoryManagerConfig because it's difficult to test the default value of reservedSystemMemory +public class ReservedSystemMemoryConfig +{ + private DataSize reservedSystemMemory = new DataSize(Runtime.getRuntime().maxMemory() * 0.4, BYTE); + + @NotNull + public DataSize getReservedSystemMemory() + { + return reservedSystemMemory; + } + + @Config("resources.reserved-system-memory") + public ReservedSystemMemoryConfig setReservedSystemMemory(DataSize reservedSystemMemory) + { + this.reservedSystemMemory = reservedSystemMemory; + return this; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/memory/VersionedMemoryPoolId.java b/presto-main/src/main/java/com/facebook/presto/memory/VersionedMemoryPoolId.java new file mode 100644 index 00000000..cb4a0831 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/memory/VersionedMemoryPoolId.java @@ -0,0 +1,49 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.memory; + +import com.google.common.base.MoreObjects; + +import static java.util.Objects.requireNonNull; + +public class VersionedMemoryPoolId +{ + private final MemoryPoolId id; + private final long version; + + public VersionedMemoryPoolId(MemoryPoolId id, long version) + { + this.id = requireNonNull(id, "id is null"); + this.version = version; + } + + public MemoryPoolId getId() + { + return id; + } + + public long getVersion() + { + return version; + } + + @Override + public String toString() + { + return MoreObjects.toStringHelper(this) + .add("id", id) + .add("version", version) + .toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/AbstractTypedJacksonModule.java b/presto-main/src/main/java/com/facebook/presto/metadata/AbstractTypedJacksonModule.java new file mode 100644 index 00000000..37520347 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/AbstractTypedJacksonModule.java @@ -0,0 +1,188 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +import com.fasterxml.jackson.annotation.JsonTypeInfo.Id; +import com.fasterxml.jackson.core.JsonGenerationException; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.jsontype.TypeIdResolver; +import com.fasterxml.jackson.databind.jsontype.TypeSerializer; +import com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer; +import com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeSerializer; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.BeanSerializerFactory; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import com.fasterxml.jackson.databind.type.SimpleType; +import com.google.common.base.Throwables; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; + +import java.io.IOException; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public abstract class AbstractTypedJacksonModule + extends SimpleModule +{ + private final String typeProperty; + + protected AbstractTypedJacksonModule(Class baseClass, String typeProperty, JsonTypeIdResolver typeIdResolver) + { + super(baseClass.getSimpleName() + "Module", Version.unknownVersion()); + this.typeProperty = typeProperty; + + TypeIdResolver typeResolver = new InternalTypeResolver((JsonTypeIdResolver) typeIdResolver); + + addSerializer(baseClass, new InternalTypeSerializer(baseClass, typeResolver)); + addDeserializer(baseClass, new InternalTypeDeserializer(baseClass, typeResolver)); + } + + public class InternalTypeDeserializer + extends StdDeserializer + { + private final TypeDeserializer typeDeserializer; + + InternalTypeDeserializer(Class baseClass, TypeIdResolver typeIdResolver) + { + super(baseClass); + this.typeDeserializer = new AsPropertyTypeDeserializer(SimpleType.construct(baseClass), typeIdResolver, typeProperty, false, null); + } + + @SuppressWarnings("unchecked") + @Override + public T deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) + throws IOException + { + return (T) typeDeserializer.deserializeTypedFromAny(jsonParser, deserializationContext); + } + } + + public class InternalTypeSerializer + extends StdSerializer + { + private final TypeSerializer typeSerializer; + private final Cache, JsonSerializer> serializerCache = CacheBuilder.newBuilder().build(); + + InternalTypeSerializer(Class baseClass, TypeIdResolver typeIdResolver) + { + super(baseClass); + this.typeSerializer = new AsPropertyTypeSerializer(typeIdResolver, null, typeProperty); + } + + @Override + public void serialize(final T value, JsonGenerator jsonGenerator, final SerializerProvider serializerProvider) + throws IOException + { + if (value == null) { + serializerProvider.defaultSerializeNull(jsonGenerator); + } + else { + try { + JsonSerializer serializer = serializerCache.get(value.getClass(), new Callable>() + { + @Override + public JsonSerializer call() + throws Exception + { + return BeanSerializerFactory.instance.createSerializer(serializerProvider, serializerProvider.constructType(value.getClass())); + } + }); + + serializer.serializeWithType(value, jsonGenerator, serializerProvider, typeSerializer); + } + catch (ExecutionException e) { + Throwables.propagateIfInstanceOf(e.getCause(), IOException.class); + Throwables.propagateIfInstanceOf(e.getCause(), JsonGenerationException.class); + throw Throwables.propagate(e.getCause()); + } + } + } + } + + class InternalTypeResolver + implements TypeIdResolver + { + private final JsonTypeIdResolver typeIdResolver; + private final LoadingCache, SimpleType> simpleTypes; + + InternalTypeResolver(JsonTypeIdResolver typeIdResolver) + { + this.typeIdResolver = checkNotNull(typeIdResolver, "typeIdResolver is null"); + simpleTypes = CacheBuilder.newBuilder().weakKeys().weakValues().build(new CacheLoader, SimpleType>() + { + @Override + public SimpleType load(Class typeClass) + throws Exception + { + return SimpleType.construct(typeClass); + } + }); + } + + @Override + public void init(JavaType baseType) + { + } + + @Override + public String idFromValue(Object value) + { + checkNotNull(value, "value is null"); + return idFromValueAndType(value, value.getClass()); + } + + @Override + public String idFromValueAndType(Object value, Class suggestedType) + { + checkNotNull(value, "value is null"); + String type = typeIdResolver.getId(value); + checkArgument(type != null, "Unknown class %s", suggestedType.getSimpleName()); + return type; + } + + @Override + public String idFromBaseType() + { + throw new UnsupportedOperationException(); + } + + @Override + public JavaType typeFromId(String id) + { + checkNotNull(id, "id is null"); + Class typeClass = typeIdResolver.getType(id); + checkArgument(typeClass != null, "Unknown type id %s", id); + return simpleTypes.getUnchecked(typeClass); + } + + @Override + public Id getMechanism() + { + return Id.NAME; + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/AllNodes.java b/presto-main/src/main/java/com/facebook/presto/metadata/AllNodes.java new file mode 100644 index 00000000..f271a3b3 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/AllNodes.java @@ -0,0 +1,43 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +import com.facebook.presto.spi.Node; +import com.google.common.collect.ImmutableSet; + +import java.util.Set; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class AllNodes +{ + private final Set activeNodes; + private final Set inactiveNodes; + + public AllNodes(Set activeNodes, Set inactiveNodes) + { + this.activeNodes = ImmutableSet.copyOf(checkNotNull(activeNodes, "activeNodes is null")); + this.inactiveNodes = ImmutableSet.copyOf(checkNotNull(inactiveNodes, "inactiveNodes is null")); + } + + public Set getActiveNodes() + { + return activeNodes; + } + + public Set getInactiveNodes() + { + return inactiveNodes; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/CatalogManager.java b/presto-main/src/main/java/com/facebook/presto/metadata/CatalogManager.java new file mode 100644 index 00000000..18547152 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/CatalogManager.java @@ -0,0 +1,201 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +import com.facebook.presto.connector.ConnectorManager; +import com.facebook.presto.server.PrestoServer.DatasourceAction; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.io.Files; +import io.airlift.log.Logger; + +import javax.inject.Inject; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.file.FileSystems; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardWatchEventKinds; +import java.nio.file.WatchEvent; +import java.nio.file.WatchKey; +import java.nio.file.WatchService; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import static com.facebook.presto.server.PrestoServer.updateDatasourcesAnnouncement; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.Maps.fromProperties; + +public class CatalogManager +{ + private static final Logger log = Logger.get(CatalogManager.class); + private final ConnectorManager connectorManager; + private final File catalogConfigurationDir; + private final AtomicBoolean catalogsLoading = new AtomicBoolean(); + private final AtomicBoolean catalogsLoaded = new AtomicBoolean(); + + @Inject + public CatalogManager(ConnectorManager connectorManager, CatalogManagerConfig config) + { + this(connectorManager, config.getCatalogConfigurationDir()); + } + + public CatalogManager(ConnectorManager connectorManager, File catalogConfigurationDir) + { + this.connectorManager = connectorManager; + this.catalogConfigurationDir = catalogConfigurationDir; + } + + public boolean areCatalogsLoaded() + { + return catalogsLoaded.get(); + } + + public void loadCatalogs() + throws Exception + { + if (!catalogsLoading.compareAndSet(false, true)) { + return; + } + + for (File file : listFiles(catalogConfigurationDir)) { + if (file.isFile() && file.getName().endsWith(".properties")) { + loadCatalog(file); + } + } + + catalogsLoaded.set(true); + + // add catalogs automatically + new Thread(() -> { + try { + log.info("-- Catalog watcher thread start --"); + startCatalogWatcher(catalogConfigurationDir); + } + catch (Exception e) { + e.printStackTrace(); + } + }).start(); + } + + private void loadCatalog(File file) + throws Exception + { + log.info("-- Loading catalog %s --", file); + Map properties = new HashMap<>(loadProperties(file)); + + String connectorName = properties.remove("connector.name"); + checkState(connectorName != null, "Catalog configuration %s does not contain conector.name", file.getAbsoluteFile()); + + String catalogName = Files.getNameWithoutExtension(file.getName()); + + connectorManager.createConnection(catalogName, connectorName, ImmutableMap.copyOf(properties)); + log.info("-- Added catalog %s using connector %s --", catalogName, connectorName); + } + + private static List listFiles(File installedPluginsDir) + { + if (installedPluginsDir != null && installedPluginsDir.isDirectory()) { + File[] files = installedPluginsDir.listFiles(); + if (files != null) { + return ImmutableList.copyOf(files); + } + } + return ImmutableList.of(); + } + + private static Map loadProperties(File file) + throws Exception + { + checkNotNull(file, "file is null"); + + Properties properties = new Properties(); + try (FileInputStream in = new FileInputStream(file)) { + properties.load(in); + } + return fromProperties(properties); + } + + private void startCatalogWatcher(File catalogConfigurationDir) throws IOException, InterruptedException + { + WatchService watchService = FileSystems.getDefault().newWatchService(); + Paths.get(catalogConfigurationDir.getAbsolutePath()).register( + watchService, StandardWatchEventKinds.ENTRY_CREATE, + StandardWatchEventKinds.ENTRY_DELETE, + StandardWatchEventKinds.ENTRY_MODIFY); + while (true) { + WatchKey key = watchService.take(); + for (WatchEvent event : key.pollEvents()) { + if (event.kind() == StandardWatchEventKinds.ENTRY_CREATE) { + log.info("New file in catalog directory : " + event.context()); + Path newCatalog = (Path) event.context(); + addCatalog(newCatalog); + } + else if (event.kind() == StandardWatchEventKinds.ENTRY_DELETE) { + log.info("Delete file from catalog directory : " + event.context()); + Path deletedCatalog = (Path) event.context(); + deleteCatalog(deletedCatalog); + } + else if (event.kind() == StandardWatchEventKinds.ENTRY_MODIFY) { + log.info("Modify file from catalog directory : " + event.context()); + Path modifiedCatalog = (Path) event.context(); + modifyCatalog(modifiedCatalog); + } + } + boolean valid = key.reset(); + if (!valid) { + break; + } + } + } + + private void addCatalog(Path catalogPath) + { + File file = new File(catalogConfigurationDir, catalogPath.getFileName().toString()); + if (file.isFile() && file.getName().endsWith(".properties")) { + try { + TimeUnit.SECONDS.sleep(5); + loadCatalog(file); + updateDatasourcesAnnouncement(Files.getNameWithoutExtension(catalogPath.getFileName().toString()), DatasourceAction.ADD); + } + catch (Exception e) { + e.printStackTrace(); + } + } + } + + private void deleteCatalog(Path catalogPath) + { + if (catalogPath.getFileName().toString().endsWith(".properties")) { + String catalogName = Files.getNameWithoutExtension(catalogPath.getFileName().toString()); + log.info("-- Removing catalog %s", catalogName); + connectorManager.removeConnector(catalogName); + updateDatasourcesAnnouncement(catalogName, DatasourceAction.DELETE); + } + } + + private void modifyCatalog(Path catalogPath) + { + deleteCatalog(catalogPath); + addCatalog(catalogPath); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/CatalogManagerConfig.java b/presto-main/src/main/java/com/facebook/presto/metadata/CatalogManagerConfig.java new file mode 100644 index 00000000..ccf5d6a3 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/CatalogManagerConfig.java @@ -0,0 +1,38 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +import io.airlift.configuration.Config; + +import javax.validation.constraints.NotNull; + +import java.io.File; + +public class CatalogManagerConfig +{ + private File catalogConfigurationDir = new File("etc/catalog/"); + + @NotNull + public File getCatalogConfigurationDir() + { + return catalogConfigurationDir; + } + + @Config("plugin.config-dir") + public CatalogManagerConfig setCatalogConfigurationDir(File pluginConfigurationDir) + { + this.catalogConfigurationDir = pluginConfigurationDir; + return this; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/ColumnHandleJacksonModule.java b/presto-main/src/main/java/com/facebook/presto/metadata/ColumnHandleJacksonModule.java new file mode 100644 index 00000000..32c490e1 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/ColumnHandleJacksonModule.java @@ -0,0 +1,53 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +import com.facebook.presto.spi.ColumnHandle; + +import javax.inject.Inject; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class ColumnHandleJacksonModule + extends AbstractTypedJacksonModule +{ + @Inject + public ColumnHandleJacksonModule(HandleResolver handleResolver) + { + super(ColumnHandle.class, "type", new ColumnHandleJsonTypeIdResolver(handleResolver)); + } + + private static class ColumnHandleJsonTypeIdResolver + implements JsonTypeIdResolver + { + private final HandleResolver handleResolver; + + private ColumnHandleJsonTypeIdResolver(HandleResolver handleResolver) + { + this.handleResolver = checkNotNull(handleResolver, "handleResolver is null"); + } + + @Override + public String getId(ColumnHandle columnHandle) + { + return handleResolver.getId(columnHandle); + } + + @Override + public Class getType(String id) + { + return handleResolver.getColumnHandleClass(id); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/DiscoveryNodeManager.java b/presto-main/src/main/java/com/facebook/presto/metadata/DiscoveryNodeManager.java new file mode 100644 index 00000000..f9882d6f --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/DiscoveryNodeManager.java @@ -0,0 +1,211 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +import com.facebook.presto.connector.system.SystemConnector; +import com.facebook.presto.failureDetector.FailureDetector; +import com.facebook.presto.spi.Node; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSetMultimap; +import com.google.common.collect.SetMultimap; +import io.airlift.discovery.client.ServiceDescriptor; +import io.airlift.discovery.client.ServiceSelector; +import io.airlift.discovery.client.ServiceType; +import io.airlift.node.NodeInfo; +import io.airlift.units.Duration; + +import javax.annotation.concurrent.GuardedBy; +import javax.annotation.concurrent.ThreadSafe; +import javax.inject.Inject; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import static com.facebook.presto.util.ImmutableCollectors.toImmutableSet; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static java.util.Arrays.asList; +import static java.util.Locale.ENGLISH; + +@ThreadSafe +public final class DiscoveryNodeManager + implements InternalNodeManager +{ + private static final Duration MAX_AGE = new Duration(5, TimeUnit.SECONDS); + + private static final Splitter DATASOURCES_SPLITTER = Splitter.on(',').trimResults().omitEmptyStrings(); + private final ServiceSelector serviceSelector; + private final NodeInfo nodeInfo; + private final FailureDetector failureDetector; + private final NodeVersion expectedNodeVersion; + + @GuardedBy("this") + private SetMultimap activeNodesByDataSource; + + @GuardedBy("this") + private AllNodes allNodes; + + @GuardedBy("this") + private long lastUpdateTimestamp; + + @GuardedBy("this") + private PrestoNode currentNode; + + @GuardedBy("this") + private Set coordinators; + + @Inject + public DiscoveryNodeManager(@ServiceType("presto") ServiceSelector serviceSelector, NodeInfo nodeInfo, FailureDetector failureDetector, NodeVersion expectedNodeVersion) + { + this.serviceSelector = checkNotNull(serviceSelector, "serviceSelector is null"); + this.nodeInfo = checkNotNull(nodeInfo, "nodeInfo is null"); + this.failureDetector = checkNotNull(failureDetector, "failureDetector is null"); + this.expectedNodeVersion = checkNotNull(expectedNodeVersion, "expectedNodeVersion is null"); + + refreshNodes(); + } + + @Override + public synchronized void refreshNodes() + { + lastUpdateTimestamp = System.nanoTime(); + + // This is currently a blacklist. + // TODO: make it a whitelist (a failure-detecting service selector) and maybe build in support for injecting this in airlift + Set services = serviceSelector.selectAllServices().stream() + .filter(service -> !failureDetector.getFailed().contains(service)) + .collect(toImmutableSet()); + + // reset current node + currentNode = null; + + ImmutableSet.Builder activeNodesBuilder = ImmutableSet.builder(); + ImmutableSet.Builder inactiveNodesBuilder = ImmutableSet.builder(); + ImmutableSet.Builder coordinatorsBuilder = ImmutableSet.builder(); + ImmutableSetMultimap.Builder byDataSourceBuilder = ImmutableSetMultimap.builder(); + + for (ServiceDescriptor service : services) { + URI uri = getHttpUri(service); + NodeVersion nodeVersion = getNodeVersion(service); + if (uri != null && nodeVersion != null) { + PrestoNode node = new PrestoNode(service.getNodeId(), uri, nodeVersion); + + // record current node + if (node.getNodeIdentifier().equals(nodeInfo.getNodeId())) { + currentNode = node; + checkState(currentNode.getNodeVersion().equals(expectedNodeVersion), "INVARIANT: current node version should be equal to expected node version"); + } + + if (isActive(node)) { + activeNodesBuilder.add(node); + if (Boolean.parseBoolean(service.getProperties().get("coordinator"))) { + coordinatorsBuilder.add(node); + } + + // record available active nodes organized by data source + String dataSources = service.getProperties().get("datasources"); + if (dataSources != null) { + dataSources = dataSources.toLowerCase(ENGLISH); + for (String dataSource : DATASOURCES_SPLITTER.split(dataSources)) { + byDataSourceBuilder.put(dataSource, node); + } + } + + // always add system data source + byDataSourceBuilder.put(SystemConnector.NAME, node); + } + else { + inactiveNodesBuilder.add(node); + } + } + } + + allNodes = new AllNodes(activeNodesBuilder.build(), inactiveNodesBuilder.build()); + activeNodesByDataSource = byDataSourceBuilder.build(); + coordinators = coordinatorsBuilder.build(); + + checkState(currentNode != null, "INVARIANT: current node not returned from service selector"); + } + + private synchronized void refreshIfNecessary() + { + if (Duration.nanosSince(lastUpdateTimestamp).compareTo(MAX_AGE) > 0) { + refreshNodes(); + } + } + + private boolean isActive(PrestoNode node) + { + return expectedNodeVersion.equals(node.getNodeVersion()); + } + + @Override + public synchronized AllNodes getAllNodes() + { + refreshIfNecessary(); + return allNodes; + } + + @Override + public Set getActiveNodes() + { + return getAllNodes().getActiveNodes(); + } + + @Override + public synchronized Set getActiveDatasourceNodes(String datasourceName) + { + refreshIfNecessary(); + return activeNodesByDataSource.get(datasourceName); + } + + @Override + public synchronized Node getCurrentNode() + { + refreshIfNecessary(); + return currentNode; + } + + @Override + public synchronized Set getCoordinators() + { + refreshIfNecessary(); + return coordinators; + } + + private static URI getHttpUri(ServiceDescriptor descriptor) + { + // favor https over http + for (String type : asList("https", "http")) { + String url = descriptor.getProperties().get(type); + if (url != null) { + try { + return new URI(url); + } + catch (URISyntaxException ignored) { + } + } + } + return null; + } + + private static NodeVersion getNodeVersion(ServiceDescriptor descriptor) + { + String nodeVersion = descriptor.getProperties().get("node_version"); + return nodeVersion == null ? null : new NodeVersion(nodeVersion); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/FunctionFactory.java b/presto-main/src/main/java/com/facebook/presto/metadata/FunctionFactory.java new file mode 100644 index 00000000..e9a914a8 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/FunctionFactory.java @@ -0,0 +1,22 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.presto.metadata; + +import java.util.List; + +public interface FunctionFactory +{ + List listFunctions(); +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/FunctionInfo.java b/presto-main/src/main/java/com/facebook/presto/metadata/FunctionInfo.java new file mode 100644 index 00000000..8163902d --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/FunctionInfo.java @@ -0,0 +1,252 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +import com.facebook.presto.operator.WindowFunctionDefinition; +import com.facebook.presto.operator.aggregation.InternalAggregationFunction; +import com.facebook.presto.operator.window.AggregateWindowFunction; +import com.facebook.presto.operator.window.WindowFunctionSupplier; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.spi.type.TypeSignature; +import com.facebook.presto.sql.tree.QualifiedName; +import com.google.common.collect.ImmutableList; + +import java.lang.invoke.MethodHandle; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static com.facebook.presto.operator.WindowFunctionDefinition.window; +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +public final class FunctionInfo + implements ParametricFunction +{ + private final Signature signature; + private final String description; + private final boolean hidden; + private final boolean nullable; + private final List nullableArguments; + + private final boolean isAggregate; + private final TypeSignature intermediateType; + private final InternalAggregationFunction aggregationFunction; + private final boolean isApproximate; + + private final MethodHandle methodHandle; + private final boolean deterministic; + + private final boolean isWindow; + private final WindowFunctionSupplier windowFunctionSupplier; + + public FunctionInfo(Signature signature, String description, WindowFunctionSupplier windowFunctionSupplier) + { + this.signature = signature; + this.description = description; + this.hidden = false; + this.deterministic = true; + this.nullable = false; + this.nullableArguments = ImmutableList.copyOf(Collections.nCopies(signature.getArgumentTypes().size(), false)); + + this.isAggregate = false; + this.intermediateType = null; + this.aggregationFunction = null; + this.isApproximate = false; + this.methodHandle = null; + + this.isWindow = true; + this.windowFunctionSupplier = checkNotNull(windowFunctionSupplier, "windowFunction is null"); + } + + public FunctionInfo(Signature signature, String description, InternalAggregationFunction function) + { + this.signature = signature; + this.description = description; + this.isApproximate = function.isApproximate(); + this.hidden = false; + this.intermediateType = function.getIntermediateType().getTypeSignature(); + this.aggregationFunction = function; + this.isAggregate = true; + this.methodHandle = null; + this.deterministic = true; + this.nullable = false; + this.nullableArguments = ImmutableList.copyOf(Collections.nCopies(signature.getArgumentTypes().size(), false)); + this.isWindow = true; + this.windowFunctionSupplier = AggregateWindowFunction.supplier(signature, function); + } + + public FunctionInfo(Signature signature, String description, boolean hidden, MethodHandle function, boolean deterministic, boolean nullableResult, List nullableArguments) + { + this.signature = signature; + this.description = description; + this.hidden = hidden; + this.deterministic = deterministic; + this.nullable = nullableResult; + this.nullableArguments = ImmutableList.copyOf(checkNotNull(nullableArguments, "nullableArguments is null")); + checkArgument(nullableArguments.size() == signature.getArgumentTypes().size(), String.format("nullableArguments size (%d) does not match signature %s", nullableArguments.size(), signature)); + + this.isAggregate = false; + this.intermediateType = null; + this.aggregationFunction = null; + this.isApproximate = false; + + this.isWindow = false; + this.windowFunctionSupplier = null; + this.methodHandle = checkNotNull(function, "function is null"); + } + + @Override + public Signature getSignature() + { + return signature; + } + + public QualifiedName getName() + { + return QualifiedName.of(signature.getName()); + } + + @Override + public String getDescription() + { + return description; + } + + @Override + public boolean isHidden() + { + return hidden; + } + + @Override + public boolean isAggregate() + { + return isAggregate; + } + + @Override + public boolean isWindow() + { + return isWindow; + } + + @Override + public boolean isScalar() + { + return !isWindow && !isAggregate; + } + + @Override + public boolean isUnbound() + { + return false; + } + + @Override + public boolean isApproximate() + { + return isApproximate; + } + + public TypeSignature getReturnType() + { + return signature.getReturnType(); + } + + public List getArgumentTypes() + { + return signature.getArgumentTypes(); + } + + public TypeSignature getIntermediateType() + { + return intermediateType; + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + return this; + } + + public WindowFunctionDefinition bindWindowFunction(Type type, List inputs) + { + checkState(isWindow, "not a window function"); + return window(windowFunctionSupplier, type, inputs); + } + + public InternalAggregationFunction getAggregationFunction() + { + checkState(aggregationFunction != null, "not an aggregation function"); + return aggregationFunction; + } + + public MethodHandle getMethodHandle() + { + checkState(methodHandle != null, "not a scalar function or operator"); + return methodHandle; + } + + @Override + public boolean isDeterministic() + { + return deterministic; + } + + public boolean isNullable() + { + return nullable; + } + + public List getNullableArguments() + { + return nullableArguments; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + FunctionInfo other = (FunctionInfo) obj; + return Objects.equals(this.signature, other.signature) && + Objects.equals(this.isAggregate, other.isAggregate) && + Objects.equals(this.isWindow, other.isWindow); + } + + @Override + public int hashCode() + { + return Objects.hash(signature, isAggregate, isWindow); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("signature", signature) + .add("isAggregate", isAggregate) + .add("isWindow", isWindow) + .toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/FunctionListBuilder.java b/presto-main/src/main/java/com/facebook/presto/metadata/FunctionListBuilder.java new file mode 100644 index 00000000..cc67fa27 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/FunctionListBuilder.java @@ -0,0 +1,371 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +import com.facebook.presto.operator.Description; +import com.facebook.presto.operator.aggregation.GenericAggregationFunctionFactory; +import com.facebook.presto.operator.aggregation.InternalAggregationFunction; +import com.facebook.presto.operator.scalar.JsonPath; +import com.facebook.presto.operator.scalar.ScalarFunction; +import com.facebook.presto.operator.scalar.ScalarOperator; +import com.facebook.presto.operator.window.ReflectionWindowFunctionSupplier; +import com.facebook.presto.operator.window.WindowFunction; +import com.facebook.presto.operator.window.WindowFunctionSupplier; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.spi.type.TypeSignature; +import com.facebook.presto.type.SqlType; +import com.google.common.base.Throwables; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import com.google.common.primitives.Primitives; +import io.airlift.joni.Regex; +import io.airlift.slice.Slice; + +import javax.annotation.Nullable; + +import java.lang.annotation.Annotation; +import java.lang.invoke.MethodHandle; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +import static com.facebook.presto.metadata.FunctionRegistry.operatorInfo; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.TypeSignature.parseTypeSignature; +import static com.facebook.presto.type.TypeUtils.resolveTypes; +import static com.google.common.base.CaseFormat.LOWER_CAMEL; +import static com.google.common.base.CaseFormat.LOWER_UNDERSCORE; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.lang.invoke.MethodHandles.lookup; +import static java.util.Locale.ENGLISH; + +public class FunctionListBuilder +{ + private static final Set> NULLABLE_ARGUMENT_TYPES = ImmutableSet.>of(Boolean.class, Long.class, Double.class, Slice.class); + + private static final Set> SUPPORTED_TYPES = ImmutableSet.of( + long.class, + Long.class, + double.class, + Double.class, + Slice.class, + boolean.class, + Boolean.class, + Regex.class, + JsonPath.class); + + private static final Set> SUPPORTED_RETURN_TYPES = ImmutableSet.of( + long.class, + double.class, + Slice.class, + boolean.class, + int.class, + Regex.class, + JsonPath.class); + + private final List functions = new ArrayList<>(); + private final TypeManager typeManager; + + public FunctionListBuilder(TypeManager typeManager) + { + this.typeManager = checkNotNull(typeManager, "typeManager is null"); + } + + public FunctionListBuilder window(String name, Type returnType, List argumentTypes, Class functionClass) + { + WindowFunctionSupplier windowFunctionSupplier = new ReflectionWindowFunctionSupplier<>( + new Signature(name, returnType.getTypeSignature(), Lists.transform(ImmutableList.copyOf(argumentTypes), Type::getTypeSignature)), + functionClass); + + functions.add(new FunctionInfo(windowFunctionSupplier.getSignature(), windowFunctionSupplier.getDescription(), windowFunctionSupplier)); + return this; + } + + public FunctionListBuilder aggregate(List functions) + { + for (InternalAggregationFunction function : functions) { + aggregate(function); + } + return this; + } + + public FunctionListBuilder aggregate(InternalAggregationFunction function) + { + String name = function.name(); + name = name.toLowerCase(ENGLISH); + + String description = getDescription(function.getClass()); + Signature signature = new Signature(name, function.getFinalType().getTypeSignature(), Lists.transform(ImmutableList.copyOf(function.getParameterTypes()), Type::getTypeSignature)); + functions.add(new FunctionInfo(signature, description, function)); + return this; + } + + public FunctionListBuilder aggregate(Class aggregationDefinition) + { + functions.addAll(GenericAggregationFunctionFactory.fromAggregationDefinition(aggregationDefinition, typeManager).listFunctions()); + return this; + } + + public FunctionListBuilder scalar(Signature signature, MethodHandle function, boolean deterministic, String description, boolean hidden, boolean nullable, List nullableArguments) + { + functions.add(new FunctionInfo(signature, description, hidden, function, deterministic, nullable, nullableArguments)); + return this; + } + + private FunctionListBuilder operator(OperatorType operatorType, Type returnType, List parameterTypes, MethodHandle function, boolean nullable, List nullableArguments) + { + FunctionInfo operatorInfo = operatorInfo(operatorType, returnType.getTypeSignature(), Lists.transform(parameterTypes, Type::getTypeSignature), function, nullable, nullableArguments); + functions.add(operatorInfo); + return this; + } + + public FunctionListBuilder scalar(Class clazz) + { + try { + boolean foundOne = false; + for (Method method : clazz.getMethods()) { + foundOne = processScalarFunction(method) || foundOne; + foundOne = processScalarOperator(method) || foundOne; + } + checkArgument(foundOne, "Expected class %s to contain at least one method annotated with @%s", clazz.getName(), ScalarFunction.class.getSimpleName()); + } + catch (IllegalAccessException e) { + throw Throwables.propagate(e); + } + return this; + } + + public FunctionListBuilder functions(ParametricFunction ... parametricFunctions) + { + for (ParametricFunction parametricFunction : parametricFunctions) { + function(parametricFunction); + } + return this; + } + + public FunctionListBuilder function(ParametricFunction parametricFunction) + { + checkNotNull(parametricFunction, "parametricFunction is null"); + functions.add(parametricFunction); + return this; + } + + private boolean processScalarFunction(Method method) + throws IllegalAccessException + { + ScalarFunction scalarFunction = method.getAnnotation(ScalarFunction.class); + if (scalarFunction == null) { + return false; + } + checkValidMethod(method); + MethodHandle methodHandle = lookup().unreflect(method); + String name = scalarFunction.value(); + if (name.isEmpty()) { + name = camelToSnake(method.getName()); + } + SqlType returnTypeAnnotation = method.getAnnotation(SqlType.class); + checkArgument(returnTypeAnnotation != null, "Method %s return type does not have a @SqlType annotation", method); + Type returnType = type(typeManager, returnTypeAnnotation); + Signature signature = new Signature(name.toLowerCase(ENGLISH), returnType.getTypeSignature(), Lists.transform(parameterTypes(typeManager, method), Type::getTypeSignature)); + + verifyMethodSignature(method, signature.getReturnType(), signature.getArgumentTypes(), typeManager); + + List nullableArguments = getNullableArguments(method); + + scalar(signature, methodHandle, scalarFunction.deterministic(), getDescription(method), scalarFunction.hidden(), method.isAnnotationPresent(Nullable.class), nullableArguments); + for (String alias : scalarFunction.alias()) { + scalar(signature.withAlias(alias.toLowerCase(ENGLISH)), methodHandle, scalarFunction.deterministic(), getDescription(method), scalarFunction.hidden(), method.isAnnotationPresent(Nullable.class), nullableArguments); + } + return true; + } + + private static Type type(TypeManager typeManager, SqlType explicitType) + { + Type type = typeManager.getType(parseTypeSignature(explicitType.value())); + checkNotNull(type, "No type found for '%s'", explicitType.value()); + return type; + } + + private static List parameterTypes(TypeManager typeManager, Method method) + { + Annotation[][] parameterAnnotations = method.getParameterAnnotations(); + + ImmutableList.Builder types = ImmutableList.builder(); + for (int i = 0; i < method.getParameterTypes().length; i++) { + Class clazz = method.getParameterTypes()[i]; + // skip session parameters + if (clazz == ConnectorSession.class) { + continue; + } + + // find the explicit type annotation if present + SqlType explicitType = null; + for (Annotation annotation : parameterAnnotations[i]) { + if (annotation instanceof SqlType) { + explicitType = (SqlType) annotation; + break; + } + } + checkArgument(explicitType != null, "Method %s argument %s does not have a @SqlType annotation", method, i); + types.add(type(typeManager, explicitType)); + } + return types.build(); + } + + private static void verifyMethodSignature(Method method, TypeSignature returnTypeName, List argumentTypeNames, TypeManager typeManager) + { + Type returnType = typeManager.getType(returnTypeName); + checkNotNull(returnType, "returnType is null"); + List argumentTypes = resolveTypes(argumentTypeNames, typeManager); + checkArgument(Primitives.unwrap(method.getReturnType()) == returnType.getJavaType(), + "Expected method %s return type to be %s (%s)", + method, + returnType.getJavaType().getName(), + returnType); + + // skip Session argument + Class[] parameterTypes = method.getParameterTypes(); + Annotation[][] annotations = method.getParameterAnnotations(); + if (parameterTypes.length > 0 && parameterTypes[0] == ConnectorSession.class) { + parameterTypes = Arrays.copyOfRange(parameterTypes, 1, parameterTypes.length); + annotations = Arrays.copyOfRange(annotations, 1, annotations.length); + } + + for (int i = 0; i < parameterTypes.length; i++) { + Class actualType = parameterTypes[i]; + Type expectedType = argumentTypes.get(i); + boolean nullable = !FluentIterable.from(Arrays.asList(annotations[i])).filter(Nullable.class).isEmpty(); + // Only allow boxing for functions that need to see nulls + if (Primitives.isWrapperType(actualType)) { + checkArgument(nullable, "Method %s has parameter with type %s that is missing @Nullable", method, actualType); + } + if (nullable) { + checkArgument(NULLABLE_ARGUMENT_TYPES.contains(actualType), "Method %s has parameter type %s, but @Nullable is not supported on this type", method, actualType); + } + checkArgument(Primitives.unwrap(actualType) == expectedType.getJavaType(), + "Expected method %s parameter %s type to be %s (%s)", + method, + i, + expectedType.getJavaType().getName(), + expectedType); + } + } + + private static List getNullableArguments(Method method) + { + List nullableArguments = new ArrayList<>(); + for (Annotation[] annotations : method.getParameterAnnotations()) { + boolean nullable = false; + boolean foundSqlType = false; + for (Annotation annotation : annotations) { + if (annotation instanceof Nullable) { + nullable = true; + } + if (annotation instanceof SqlType) { + foundSqlType = true; + } + } + // Check that this is a real argument. For example, some functions take ConnectorSession which isn't a SqlType + if (foundSqlType) { + nullableArguments.add(nullable); + } + } + return nullableArguments; + } + + private boolean processScalarOperator(Method method) + throws IllegalAccessException + { + ScalarOperator scalarOperator = method.getAnnotation(ScalarOperator.class); + if (scalarOperator == null) { + return false; + } + checkValidMethod(method); + MethodHandle methodHandle = lookup().unreflect(method); + OperatorType operatorType = scalarOperator.value(); + + List parameterTypes = parameterTypes(typeManager, method); + + Type returnType; + if (operatorType == OperatorType.HASH_CODE) { + // todo hack for hashCode... should be int + returnType = BIGINT; + } + else { + SqlType explicitType = method.getAnnotation(SqlType.class); + checkArgument(explicitType != null, "Method %s return type does not have a @SqlType annotation", method); + returnType = type(typeManager, explicitType); + + verifyMethodSignature(method, returnType.getTypeSignature(), Lists.transform(parameterTypes, Type::getTypeSignature), typeManager); + } + + List nullableArguments = getNullableArguments(method); + + operator(operatorType, returnType, parameterTypes, methodHandle, method.isAnnotationPresent(Nullable.class), nullableArguments); + return true; + } + + private static String getDescription(AnnotatedElement annotatedElement) + { + Description description = annotatedElement.getAnnotation(Description.class); + return (description == null) ? null : description.value(); + } + + private static String camelToSnake(String name) + { + return LOWER_CAMEL.to(LOWER_UNDERSCORE, name); + } + + private static void checkValidMethod(Method method) + { + String message = "@ScalarFunction method %s is not valid: "; + + checkArgument(Modifier.isStatic(method.getModifiers()), message + "must be static", method); + + checkArgument(SUPPORTED_RETURN_TYPES.contains(Primitives.unwrap(method.getReturnType())), message + "return type not supported", method); + if (method.getAnnotation(Nullable.class) != null) { + checkArgument(!method.getReturnType().isPrimitive(), message + "annotated with @Nullable but has primitive return type", method); + } + else { + checkArgument(!Primitives.isWrapperType(method.getReturnType()), "not annotated with @Nullable but has boxed primitive return type", method); + } + + for (Class type : getParameterTypes(method.getParameterTypes())) { + checkArgument(SUPPORTED_TYPES.contains(type), message + "parameter type [%s] not supported", method, type.getName()); + } + } + + private static List> getParameterTypes(Class... types) + { + ImmutableList> parameterTypes = ImmutableList.copyOf(types); + if (!parameterTypes.isEmpty() && parameterTypes.get(0) == ConnectorSession.class) { + parameterTypes = parameterTypes.subList(1, parameterTypes.size()); + } + return parameterTypes; + } + + public List getFunctions() + { + return ImmutableList.copyOf(functions); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/FunctionRegistry.java b/presto-main/src/main/java/com/facebook/presto/metadata/FunctionRegistry.java new file mode 100644 index 00000000..65798624 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/FunctionRegistry.java @@ -0,0 +1,789 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +import com.facebook.presto.operator.aggregation.ApproximateAverageAggregations; +import com.facebook.presto.operator.aggregation.ApproximateCountAggregation; +import com.facebook.presto.operator.aggregation.ApproximateCountColumnAggregations; +import com.facebook.presto.operator.aggregation.ApproximateCountDistinctAggregations; +import com.facebook.presto.operator.aggregation.ApproximateDoublePercentileAggregations; +import com.facebook.presto.operator.aggregation.ApproximateLongPercentileAggregations; +import com.facebook.presto.operator.aggregation.ApproximateSetAggregation; +import com.facebook.presto.operator.aggregation.ApproximateSumAggregations; +import com.facebook.presto.operator.aggregation.AverageAggregations; +import com.facebook.presto.operator.aggregation.BooleanAndAggregation; +import com.facebook.presto.operator.aggregation.BooleanOrAggregation; +import com.facebook.presto.operator.aggregation.CountAggregation; +import com.facebook.presto.operator.aggregation.CountIfAggregation; +import com.facebook.presto.operator.aggregation.CorrelationAggregation; +import com.facebook.presto.operator.aggregation.CovarianceAggregation; +import com.facebook.presto.operator.aggregation.DoubleSumAggregation; +import com.facebook.presto.operator.aggregation.LongSumAggregation; +import com.facebook.presto.operator.aggregation.MergeHyperLogLogAggregation; +import com.facebook.presto.operator.aggregation.NumericHistogramAggregation; +import com.facebook.presto.operator.aggregation.RegressionAggregation; +import com.facebook.presto.operator.aggregation.VarianceAggregation; +import com.facebook.presto.operator.scalar.ArrayFunctions; +import com.facebook.presto.operator.scalar.ColorFunctions; +import com.facebook.presto.operator.scalar.CombineHashFunction; +import com.facebook.presto.operator.scalar.DateTimeFunctions; +import com.facebook.presto.operator.scalar.HyperLogLogFunctions; +import com.facebook.presto.operator.scalar.JsonFunctions; +import com.facebook.presto.operator.scalar.JsonOperators; +import com.facebook.presto.operator.scalar.MathFunctions; +import com.facebook.presto.operator.scalar.RegexpFunctions; +import com.facebook.presto.operator.scalar.StringFunctions; +import com.facebook.presto.operator.scalar.UrlFunctions; +import com.facebook.presto.operator.scalar.VarbinaryFunctions; +import com.facebook.presto.operator.window.CumulativeDistributionFunction; +import com.facebook.presto.operator.window.DenseRankFunction; +import com.facebook.presto.operator.window.FirstValueFunction.BigintFirstValueFunction; +import com.facebook.presto.operator.window.FirstValueFunction.BooleanFirstValueFunction; +import com.facebook.presto.operator.window.FirstValueFunction.DoubleFirstValueFunction; +import com.facebook.presto.operator.window.FirstValueFunction.VarcharFirstValueFunction; +import com.facebook.presto.operator.window.FirstValueFunction.TimestampFirstValueFunction; +import com.facebook.presto.operator.window.LagFunction.BigintLagFunction; +import com.facebook.presto.operator.window.LagFunction.BooleanLagFunction; +import com.facebook.presto.operator.window.LagFunction.DoubleLagFunction; +import com.facebook.presto.operator.window.LagFunction.VarcharLagFunction; +import com.facebook.presto.operator.window.LagFunction.TimestampLagFunction; +import com.facebook.presto.operator.window.LastValueFunction.BigintLastValueFunction; +import com.facebook.presto.operator.window.LastValueFunction.BooleanLastValueFunction; +import com.facebook.presto.operator.window.LastValueFunction.DoubleLastValueFunction; +import com.facebook.presto.operator.window.LastValueFunction.VarcharLastValueFunction; +import com.facebook.presto.operator.window.LastValueFunction.TimestampLastValueFunction; +import com.facebook.presto.operator.window.LeadFunction.BigintLeadFunction; +import com.facebook.presto.operator.window.LeadFunction.BooleanLeadFunction; +import com.facebook.presto.operator.window.LeadFunction.DoubleLeadFunction; +import com.facebook.presto.operator.window.LeadFunction.VarcharLeadFunction; +import com.facebook.presto.operator.window.LeadFunction.TimestampLeadFunction; +import com.facebook.presto.operator.window.NTileFunction; +import com.facebook.presto.operator.window.NthValueFunction.BigintNthValueFunction; +import com.facebook.presto.operator.window.NthValueFunction.BooleanNthValueFunction; +import com.facebook.presto.operator.window.NthValueFunction.DoubleNthValueFunction; +import com.facebook.presto.operator.window.NthValueFunction.VarcharNthValueFunction; +import com.facebook.presto.operator.window.NthValueFunction.TimestampNthValueFunction; +import com.facebook.presto.operator.window.PercentRankFunction; +import com.facebook.presto.operator.window.RankFunction; +import com.facebook.presto.operator.window.RowNumberFunction; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.block.BlockEncodingSerde; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.spi.type.TypeSignature; +import com.facebook.presto.sql.tree.QualifiedName; +import com.facebook.presto.type.ArrayType; +import com.facebook.presto.type.BigintOperators; +import com.facebook.presto.type.BooleanOperators; +import com.facebook.presto.type.DateOperators; +import com.facebook.presto.type.DateTimeOperators; +import com.facebook.presto.type.DoubleOperators; +import com.facebook.presto.type.HyperLogLogOperators; +import com.facebook.presto.type.IntervalDayTimeOperators; +import com.facebook.presto.type.IntervalYearMonthOperators; +import com.facebook.presto.type.LikeFunctions; +import com.facebook.presto.type.RowParametricType; +import com.facebook.presto.type.RowType; +import com.facebook.presto.type.TimeOperators; +import com.facebook.presto.type.TimeWithTimeZoneOperators; +import com.facebook.presto.type.TimestampOperators; +import com.facebook.presto.type.TimestampWithTimeZoneOperators; +import com.facebook.presto.type.VarbinaryOperators; +import com.facebook.presto.type.VarcharOperators; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Joiner; +import com.google.common.base.Throwables; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.common.collect.Multimap; +import com.google.common.collect.Multimaps; +import com.google.common.primitives.Primitives; +import com.google.common.util.concurrent.UncheckedExecutionException; +import io.airlift.slice.Slice; + +import javax.annotation.concurrent.ThreadSafe; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static com.facebook.presto.operator.aggregation.ArbitraryAggregation.ARBITRARY_AGGREGATION; +import static com.facebook.presto.operator.aggregation.ArrayAggregation.ARRAY_AGGREGATION; +import static com.facebook.presto.operator.aggregation.CountColumn.COUNT_COLUMN; +import static com.facebook.presto.operator.aggregation.MapAggregation.MAP_AGG; +import static com.facebook.presto.operator.aggregation.MaxAggregation.MAX_AGGREGATION; +import static com.facebook.presto.operator.aggregation.MaxBy.MAX_BY; +import static com.facebook.presto.operator.aggregation.MinAggregation.MIN_AGGREGATION; +import static com.facebook.presto.operator.aggregation.MinBy.MIN_BY; +import static com.facebook.presto.operator.scalar.ArrayCardinalityFunction.ARRAY_CARDINALITY; +import static com.facebook.presto.operator.scalar.ArrayConcatFunction.ARRAY_CONCAT_FUNCTION; +import static com.facebook.presto.operator.scalar.ArrayConstructor.ARRAY_CONSTRUCTOR; +import static com.facebook.presto.operator.scalar.ArrayContains.ARRAY_CONTAINS; +import static com.facebook.presto.operator.scalar.ArrayDistinctFunction.ARRAY_DISTINCT_FUNCTION; +import static com.facebook.presto.operator.scalar.ArrayEqualOperator.ARRAY_EQUAL; +import static com.facebook.presto.operator.scalar.ArrayGreaterThanOperator.ARRAY_GREATER_THAN; +import static com.facebook.presto.operator.scalar.ArrayGreaterThanOrEqualOperator.ARRAY_GREATER_THAN_OR_EQUAL; +import static com.facebook.presto.operator.scalar.ArrayHashCodeOperator.ARRAY_HASH_CODE; +import static com.facebook.presto.operator.scalar.ArrayIntersectFunction.ARRAY_INTERSECT_FUNCTION; +import static com.facebook.presto.operator.scalar.ArrayJoin.ARRAY_JOIN; +import static com.facebook.presto.operator.scalar.ArrayJoin.ARRAY_JOIN_WITH_NULL_REPLACEMENT; +import static com.facebook.presto.operator.scalar.ArrayLessThanOperator.ARRAY_LESS_THAN; +import static com.facebook.presto.operator.scalar.ArrayLessThanOrEqualOperator.ARRAY_LESS_THAN_OR_EQUAL; +import static com.facebook.presto.operator.scalar.ArrayNotEqualOperator.ARRAY_NOT_EQUAL; +import static com.facebook.presto.operator.scalar.ArrayPositionFunction.ARRAY_POSITION; +import static com.facebook.presto.operator.scalar.ArraySortFunction.ARRAY_SORT_FUNCTION; +import static com.facebook.presto.operator.scalar.ArrayRemoveFunction.ARRAY_REMOVE_FUNCTION; +import static com.facebook.presto.operator.scalar.ArraySubscriptOperator.ARRAY_SUBSCRIPT; +import static com.facebook.presto.operator.scalar.ArrayToArrayCast.ARRAY_TO_ARRAY_CAST; +import static com.facebook.presto.operator.scalar.ArrayToElementConcatFunction.ARRAY_TO_ELEMENT_CONCAT_FUNCTION; +import static com.facebook.presto.operator.scalar.ArrayToJsonCast.ARRAY_TO_JSON; +import static com.facebook.presto.operator.scalar.ElementToArrayConcatFunction.ELEMENT_TO_ARRAY_CONCAT_FUNCTION; +import static com.facebook.presto.operator.scalar.Greatest.GREATEST; +import static com.facebook.presto.operator.scalar.IdentityCast.IDENTITY_CAST; +import static com.facebook.presto.operator.scalar.JsonToArrayCast.JSON_TO_ARRAY; +import static com.facebook.presto.operator.scalar.JsonToMapCast.JSON_TO_MAP; +import static com.facebook.presto.operator.scalar.Least.LEAST; +import static com.facebook.presto.operator.scalar.MapCardinalityFunction.MAP_CARDINALITY; +import static com.facebook.presto.operator.scalar.MapConstructor.MAP_CONSTRUCTOR; +import static com.facebook.presto.operator.scalar.MapEqualOperator.MAP_EQUAL; +import static com.facebook.presto.operator.scalar.MapHashCodeOperator.MAP_HASH_CODE; +import static com.facebook.presto.operator.scalar.MapKeys.MAP_KEYS; +import static com.facebook.presto.operator.scalar.MapNotEqualOperator.MAP_NOT_EQUAL; +import static com.facebook.presto.operator.scalar.MapSubscriptOperator.MAP_SUBSCRIPT; +import static com.facebook.presto.operator.scalar.MapToJsonCast.MAP_TO_JSON; +import static com.facebook.presto.operator.scalar.MapValues.MAP_VALUES; +import static com.facebook.presto.operator.scalar.RowEqualOperator.ROW_EQUAL; +import static com.facebook.presto.operator.scalar.RowHashCodeOperator.ROW_HASH_CODE; +import static com.facebook.presto.operator.scalar.RowNotEqualOperator.ROW_NOT_EQUAL; +import static com.facebook.presto.operator.scalar.RowToJsonCast.ROW_TO_JSON; +import static com.facebook.presto.operator.scalar.TryCastFunction.TRY_CAST; +import static com.facebook.presto.spi.StandardErrorCode.FUNCTION_NOT_FOUND; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.BooleanType.BOOLEAN; +import static com.facebook.presto.spi.type.DateType.DATE; +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; +import static com.facebook.presto.spi.type.TimeType.TIME; +import static com.facebook.presto.spi.type.TimeWithTimeZoneType.TIME_WITH_TIME_ZONE; +import static com.facebook.presto.spi.type.TimestampType.TIMESTAMP; +import static com.facebook.presto.spi.type.TimestampWithTimeZoneType.TIMESTAMP_WITH_TIME_ZONE; +import static com.facebook.presto.spi.type.TypeSignature.parseTypeSignature; +import static com.facebook.presto.spi.type.VarbinaryType.VARBINARY; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static com.facebook.presto.type.JsonPathType.JSON_PATH; +import static com.facebook.presto.type.LikePatternType.LIKE_PATTERN; +import static com.facebook.presto.type.RegexpType.REGEXP; +import static com.facebook.presto.type.TypeUtils.resolveTypes; +import static com.facebook.presto.type.UnknownType.UNKNOWN; +import static com.facebook.presto.util.ImmutableCollectors.toImmutableList; +import static com.facebook.presto.util.ImmutableCollectors.toImmutableSet; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Predicates.not; +import static java.lang.String.format; + +@ThreadSafe +public class FunctionRegistry +{ + private static final String MAGIC_LITERAL_FUNCTION_PREFIX = "$literal$"; + private static final String OPERATOR_PREFIX = "$operator$"; + + // hack: java classes for types that can be used with magic literals + private static final Set> SUPPORTED_LITERAL_TYPES = ImmutableSet.>of(long.class, double.class, Slice.class, boolean.class); + + private final TypeManager typeManager; + private final BlockEncodingSerde blockEncodingSerde; + private final LoadingCache specializedFunctionCache; + private volatile FunctionMap functions = new FunctionMap(); + + public FunctionRegistry(TypeManager typeManager, BlockEncodingSerde blockEncodingSerde, boolean experimentalSyntaxEnabled) + { + this.typeManager = checkNotNull(typeManager, "typeManager is null"); + this.blockEncodingSerde = checkNotNull(blockEncodingSerde, "blockEncodingSerde is null"); + + specializedFunctionCache = CacheBuilder.newBuilder() + .maximumSize(1000) + .build(new CacheLoader() + { + @Override + public FunctionInfo load(SpecializedFunctionKey key) + throws Exception + { + return key.getFunction().specialize(key.getBoundTypeParameters(), key.getArity(), typeManager, FunctionRegistry.this); + } + }); + + FunctionListBuilder builder = new FunctionListBuilder(typeManager) + .window("row_number", BIGINT, ImmutableList.of(), RowNumberFunction.class) + .window("rank", BIGINT, ImmutableList.of(), RankFunction.class) + .window("dense_rank", BIGINT, ImmutableList.of(), DenseRankFunction.class) + .window("percent_rank", DOUBLE, ImmutableList.of(), PercentRankFunction.class) + .window("cume_dist", DOUBLE, ImmutableList.of(), CumulativeDistributionFunction.class) + .window("ntile", BIGINT, ImmutableList.of(BIGINT), NTileFunction.class) + .window("first_value", BIGINT, ImmutableList.of(BIGINT), BigintFirstValueFunction.class) + .window("first_value", DOUBLE, ImmutableList.of(DOUBLE), DoubleFirstValueFunction.class) + .window("first_value", BOOLEAN, ImmutableList.of(BOOLEAN), BooleanFirstValueFunction.class) + .window("first_value", VARCHAR, ImmutableList.of(VARCHAR), VarcharFirstValueFunction.class) + .window("first_value", TIMESTAMP, ImmutableList.of(TIMESTAMP), TimestampFirstValueFunction.class) + .window("last_value", BIGINT, ImmutableList.of(BIGINT), BigintLastValueFunction.class) + .window("last_value", DOUBLE, ImmutableList.of(DOUBLE), DoubleLastValueFunction.class) + .window("last_value", BOOLEAN, ImmutableList.of(BOOLEAN), BooleanLastValueFunction.class) + .window("last_value", VARCHAR, ImmutableList.of(VARCHAR), VarcharLastValueFunction.class) + .window("last_value", TIMESTAMP, ImmutableList.of(TIMESTAMP), TimestampLastValueFunction.class) + .window("nth_value", BIGINT, ImmutableList.of(BIGINT, BIGINT), BigintNthValueFunction.class) + .window("nth_value", DOUBLE, ImmutableList.of(DOUBLE, BIGINT), DoubleNthValueFunction.class) + .window("nth_value", BOOLEAN, ImmutableList.of(BOOLEAN, BIGINT), BooleanNthValueFunction.class) + .window("nth_value", VARCHAR, ImmutableList.of(VARCHAR, BIGINT), VarcharNthValueFunction.class) + .window("nth_value", TIMESTAMP, ImmutableList.of(TIMESTAMP, BIGINT), TimestampNthValueFunction.class) + .window("lag", BIGINT, ImmutableList.of(BIGINT), BigintLagFunction.class) + .window("lag", BIGINT, ImmutableList.of(BIGINT, BIGINT), BigintLagFunction.class) + .window("lag", BIGINT, ImmutableList.of(BIGINT, BIGINT, BIGINT), BigintLagFunction.class) + .window("lag", DOUBLE, ImmutableList.of(DOUBLE), DoubleLagFunction.class) + .window("lag", DOUBLE, ImmutableList.of(DOUBLE, BIGINT), DoubleLagFunction.class) + .window("lag", DOUBLE, ImmutableList.of(DOUBLE, BIGINT, DOUBLE), DoubleLagFunction.class) + .window("lag", BOOLEAN, ImmutableList.of(BOOLEAN), BooleanLagFunction.class) + .window("lag", BOOLEAN, ImmutableList.of(BOOLEAN, BIGINT), BooleanLagFunction.class) + .window("lag", BOOLEAN, ImmutableList.of(BOOLEAN, BIGINT, BOOLEAN), BooleanLagFunction.class) + .window("lag", VARCHAR, ImmutableList.of(VARCHAR), VarcharLagFunction.class) + .window("lag", VARCHAR, ImmutableList.of(VARCHAR, BIGINT), VarcharLagFunction.class) + .window("lag", VARCHAR, ImmutableList.of(VARCHAR, BIGINT, VARCHAR), VarcharLagFunction.class) + .window("lag", TIMESTAMP, ImmutableList.of(TIMESTAMP), TimestampLagFunction.class) + .window("lag", TIMESTAMP, ImmutableList.of(TIMESTAMP, BIGINT), TimestampLagFunction.class) + .window("lag", TIMESTAMP, ImmutableList.of(TIMESTAMP, BIGINT, VARCHAR), TimestampLagFunction.class) + .window("lead", BIGINT, ImmutableList.of(BIGINT), BigintLeadFunction.class) + .window("lead", BIGINT, ImmutableList.of(BIGINT, BIGINT), BigintLeadFunction.class) + .window("lead", BIGINT, ImmutableList.of(BIGINT, BIGINT, BIGINT), BigintLeadFunction.class) + .window("lead", DOUBLE, ImmutableList.of(DOUBLE), DoubleLeadFunction.class) + .window("lead", DOUBLE, ImmutableList.of(DOUBLE, BIGINT), DoubleLeadFunction.class) + .window("lead", DOUBLE, ImmutableList.of(DOUBLE, BIGINT, DOUBLE), DoubleLeadFunction.class) + .window("lead", BOOLEAN, ImmutableList.of(BOOLEAN), BooleanLeadFunction.class) + .window("lead", BOOLEAN, ImmutableList.of(BOOLEAN, BIGINT), BooleanLeadFunction.class) + .window("lead", BOOLEAN, ImmutableList.of(BOOLEAN, BIGINT, BOOLEAN), BooleanLeadFunction.class) + .window("lead", VARCHAR, ImmutableList.of(VARCHAR), VarcharLeadFunction.class) + .window("lead", VARCHAR, ImmutableList.of(VARCHAR, BIGINT), VarcharLeadFunction.class) + .window("lead", VARCHAR, ImmutableList.of(VARCHAR, BIGINT, VARCHAR), VarcharLeadFunction.class) + .window("lead", TIMESTAMP, ImmutableList.of(TIMESTAMP), TimestampLeadFunction.class) + .window("lead", TIMESTAMP, ImmutableList.of(TIMESTAMP, BIGINT), TimestampLeadFunction.class) + .window("lead", TIMESTAMP, ImmutableList.of(TIMESTAMP, BIGINT, VARCHAR), TimestampLeadFunction.class) + .aggregate(CountAggregation.class) + .aggregate(VarianceAggregation.class) + .aggregate(ApproximateLongPercentileAggregations.class) + .aggregate(ApproximateDoublePercentileAggregations.class) + .aggregate(CountIfAggregation.class) + .aggregate(BooleanAndAggregation.class) + .aggregate(BooleanOrAggregation.class) + .aggregate(DoubleSumAggregation.class) + .aggregate(LongSumAggregation.class) + .aggregate(AverageAggregations.class) + .aggregate(ApproximateCountDistinctAggregations.class) + .aggregate(MergeHyperLogLogAggregation.class) + .aggregate(ApproximateSetAggregation.class) + .aggregate(NumericHistogramAggregation.class) + .aggregate(CovarianceAggregation.class) + .aggregate(RegressionAggregation.class) + .aggregate(CorrelationAggregation.class) + .scalar(StringFunctions.class) + .scalar(VarbinaryFunctions.class) + .scalar(RegexpFunctions.class) + .scalar(UrlFunctions.class) + .scalar(MathFunctions.class) + .scalar(DateTimeFunctions.class) + .scalar(JsonFunctions.class) + .scalar(ColorFunctions.class) + .scalar(HyperLogLogFunctions.class) + .scalar(BooleanOperators.class) + .scalar(BigintOperators.class) + .scalar(DoubleOperators.class) + .scalar(VarcharOperators.class) + .scalar(VarbinaryOperators.class) + .scalar(DateOperators.class) + .scalar(TimeOperators.class) + .scalar(TimestampOperators.class) + .scalar(IntervalDayTimeOperators.class) + .scalar(IntervalYearMonthOperators.class) + .scalar(TimeWithTimeZoneOperators.class) + .scalar(TimestampWithTimeZoneOperators.class) + .scalar(DateTimeOperators.class) + .scalar(HyperLogLogOperators.class) + .scalar(LikeFunctions.class) + .scalar(ArrayFunctions.class) + .scalar(CombineHashFunction.class) + .scalar(JsonOperators.class) + .function(IDENTITY_CAST) + .functions(ARRAY_CONTAINS, ARRAY_JOIN, ARRAY_JOIN_WITH_NULL_REPLACEMENT) + .functions(ARRAY_TO_ARRAY_CAST, ARRAY_HASH_CODE, ARRAY_EQUAL, ARRAY_NOT_EQUAL, ARRAY_LESS_THAN, ARRAY_LESS_THAN_OR_EQUAL, ARRAY_GREATER_THAN, ARRAY_GREATER_THAN_OR_EQUAL) + .functions(ARRAY_CONCAT_FUNCTION, ARRAY_TO_ELEMENT_CONCAT_FUNCTION, ELEMENT_TO_ARRAY_CONCAT_FUNCTION) + .functions(MAP_EQUAL, MAP_NOT_EQUAL, MAP_HASH_CODE) + .functions(ARRAY_CONSTRUCTOR, ARRAY_SUBSCRIPT, ARRAY_CARDINALITY, ARRAY_POSITION, ARRAY_SORT_FUNCTION, ARRAY_INTERSECT_FUNCTION, ARRAY_TO_JSON, JSON_TO_ARRAY, ARRAY_DISTINCT_FUNCTION, ARRAY_REMOVE_FUNCTION) + .functions(MAP_CONSTRUCTOR, MAP_CARDINALITY, MAP_SUBSCRIPT, MAP_TO_JSON, JSON_TO_MAP, MAP_KEYS, MAP_VALUES, MAP_AGG) + .function(ARBITRARY_AGGREGATION) + .function(ARRAY_AGGREGATION) + .function(LEAST) + .function(GREATEST) + .function(MAX_BY) + .function(MIN_BY) + .functions(MAX_AGGREGATION, MIN_AGGREGATION) + .function(COUNT_COLUMN) + .functions(ROW_HASH_CODE, ROW_TO_JSON, ROW_EQUAL, ROW_NOT_EQUAL) + .function(TRY_CAST); + + if (experimentalSyntaxEnabled) { + builder.aggregate(ApproximateAverageAggregations.class) + .aggregate(ApproximateSumAggregations.class) + .aggregate(ApproximateCountAggregation.class) + .aggregate(ApproximateCountColumnAggregations.class); + } + + addFunctions(builder.getFunctions()); + } + + public final synchronized void addFunctions(List functions) + { + for (ParametricFunction function : functions) { + for (ParametricFunction existingFunction : this.functions.list()) { + checkArgument(!function.getSignature().equals(existingFunction.getSignature()), "Function already registered: %s", function.getSignature()); + } + } + this.functions = new FunctionMap(this.functions, functions); + } + + public List list() + { + return FluentIterable.from(functions.list()) + .filter(not(ParametricFunction::isHidden)) + .toList(); + } + + public boolean isAggregationFunction(QualifiedName name) + { + return Iterables.any(functions.get(name), ParametricFunction::isAggregate); + } + + public FunctionInfo resolveFunction(QualifiedName name, List parameterTypes, boolean approximate) + { + List candidates = functions.get(name).stream() + .filter(function -> function.isScalar() || function.isApproximate() == approximate) + .collect(toImmutableList()); + + List resolvedTypes = resolveTypes(parameterTypes, typeManager); + // search for exact match + FunctionInfo match = null; + for (ParametricFunction function : candidates) { + Map boundTypeParameters = function.getSignature().bindTypeParameters(resolvedTypes, false, typeManager); + if (boundTypeParameters != null) { + checkArgument(match == null, "Ambiguous call to %s with parameters %s", name, parameterTypes); + try { + match = specializedFunctionCache.getUnchecked(new SpecializedFunctionKey(function, boundTypeParameters, resolvedTypes.size())); + } + catch (UncheckedExecutionException e) { + throw Throwables.propagate(e.getCause()); + } + } + } + + if (match != null) { + return match; + } + + // search for coerced match + for (ParametricFunction function : candidates) { + Map boundTypeParameters = function.getSignature().bindTypeParameters(resolvedTypes, true, typeManager); + if (boundTypeParameters != null) { + // TODO: This should also check for ambiguities + try { + return specializedFunctionCache.getUnchecked(new SpecializedFunctionKey(function, boundTypeParameters, resolvedTypes.size())); + } + catch (UncheckedExecutionException e) { + throw Throwables.propagate(e.getCause()); + } + } + } + + List expectedParameters = new ArrayList<>(); + for (ParametricFunction function : candidates) { + expectedParameters.add(format("%s(%s) %s", + name, + Joiner.on(", ").join(function.getSignature().getArgumentTypes()), + Joiner.on(", ").join(function.getSignature().getTypeParameters()))); + } + String parameters = Joiner.on(", ").join(parameterTypes); + String message = format("Function %s not registered", name); + if (!expectedParameters.isEmpty()) { + String expected = Joiner.on(", ").join(expectedParameters); + message = format("Unexpected parameters (%s) for function %s. Expected: %s", parameters, name, expected); + } + + if (name.getSuffix().startsWith(MAGIC_LITERAL_FUNCTION_PREFIX)) { + // extract type from function name + String typeName = name.getSuffix().substring(MAGIC_LITERAL_FUNCTION_PREFIX.length()); + + // lookup the type + Type type = typeManager.getType(parseTypeSignature(typeName)); + checkNotNull(type, "Type %s not registered", typeName); + + // verify we have one parameter of the proper type + checkArgument(parameterTypes.size() == 1, "Expected one argument to literal function, but got %s", parameterTypes); + Type parameterType = typeManager.getType(parameterTypes.get(0)); + checkNotNull(parameterType, "Type %s not found", parameterTypes.get(0)); + checkArgument(parameterType.getJavaType() == type.getJavaType(), + "Expected type %s to use Java type %s, but Java type is %s", + type, + parameterType.getJavaType(), + type.getJavaType()); + + MethodHandle identity = MethodHandles.identity(parameterType.getJavaType()); + return new FunctionInfo( + getMagicLiteralFunctionSignature(type), + null, + true, + identity, + true, + false, + ImmutableList.of(false)); + } + + // TODO this should be made to work for any parametric type + for (TypeSignature typeSignature : parameterTypes) { + if (typeSignature.getBase().equals(StandardTypes.ROW)) { + RowType rowType = RowParametricType.ROW.createType(resolveTypes(typeSignature.getParameters(), typeManager), typeSignature.getLiteralParameters()); + // search for exact match + for (ParametricFunction function : RowParametricType.ROW.createFunctions(rowType)) { + if (!function.getSignature().getName().equals(name.toString())) { + continue; + } + Map boundTypeParameters = function.getSignature().bindTypeParameters(resolvedTypes, false, typeManager); + if (boundTypeParameters != null) { + checkArgument(match == null, "Ambiguous call to %s with parameters %s", name, parameterTypes); + try { + match = specializedFunctionCache.getUnchecked(new SpecializedFunctionKey(function, boundTypeParameters, resolvedTypes.size())); + } + catch (UncheckedExecutionException e) { + throw Throwables.propagate(e.getCause()); + } + } + } + + if (match != null) { + return match; + } + } + } + + throw new PrestoException(FUNCTION_NOT_FOUND, message); + } + + public FunctionInfo getExactFunction(Signature signature) + { + Iterable candidates = functions.get(QualifiedName.of(signature.getName())); + // search for exact match + for (ParametricFunction operator : candidates) { + Type returnType = typeManager.getType(signature.getReturnType()); + List argumentTypes = resolveTypes(signature.getArgumentTypes(), typeManager); + Map boundTypeParameters = operator.getSignature().bindTypeParameters(returnType, argumentTypes, false, typeManager); + if (boundTypeParameters != null) { + try { + return specializedFunctionCache.getUnchecked(new SpecializedFunctionKey(operator, boundTypeParameters, signature.getArgumentTypes().size())); + } + catch (UncheckedExecutionException e) { + throw Throwables.propagate(e.getCause()); + } + } + } + return null; + } + + @VisibleForTesting + public List listOperators() + { + Set operatorNames = Arrays.asList(OperatorType.values()).stream() + .map(FunctionRegistry::mangleOperatorName) + .collect(toImmutableSet()); + + return functions.list().stream() + .filter(function -> operatorNames.contains(function.getSignature().getName())) + .collect(toImmutableList()); + } + + public FunctionInfo resolveOperator(OperatorType operatorType, List argumentTypes) + throws OperatorNotFoundException + { + try { + return resolveFunction(QualifiedName.of(mangleOperatorName(operatorType)), Lists.transform(argumentTypes, Type::getTypeSignature), false); + } + catch (PrestoException e) { + if (e.getErrorCode().getCode() == FUNCTION_NOT_FOUND.toErrorCode().getCode()) { + throw new OperatorNotFoundException(operatorType, argumentTypes); + } + else { + throw e; + } + } + } + + public FunctionInfo getCoercion(Type fromType, Type toType) + { + FunctionInfo functionInfo = getExactFunction(Signature.internalOperator(OperatorType.CAST.name(), toType.getTypeSignature(), ImmutableList.of(fromType.getTypeSignature()))); + if (functionInfo == null) { + throw new OperatorNotFoundException(OperatorType.CAST, ImmutableList.of(fromType), toType); + } + return functionInfo; + } + + public static boolean canCoerce(List actualTypes, List expectedTypes) + { + if (actualTypes.size() != expectedTypes.size()) { + return false; + } + for (int i = 0; i < expectedTypes.size(); i++) { + Type expectedType = expectedTypes.get(i); + Type actualType = actualTypes.get(i); + if (!canCoerce(actualType, expectedType)) { + return false; + } + } + return true; + } + + public static boolean canCoerce(Type actualType, Type expectedType) + { + // are types the same + if (expectedType.equals(actualType)) { + return true; + } + // null can be cast to anything + if (actualType.equals(UNKNOWN)) { + return true; + } + // widen bigint to double + if (actualType.equals(BIGINT) && expectedType.equals(DOUBLE)) { + return true; + } + // add auto type conversion from varchar to bigint + if (actualType.equals(VARCHAR) && expectedType.equals(BIGINT)) { + return true; + } + // add auto type conversion from varchar to double + if (actualType.equals(VARCHAR) && expectedType.equals(DOUBLE)) { + return true; + } + // widen date to timestamp + if (actualType.equals(DATE) && expectedType.equals(TIMESTAMP)) { + return true; + } + // widen date to timestamp with time zone + if (actualType.equals(DATE) && expectedType.equals(TIMESTAMP_WITH_TIME_ZONE)) { + return true; + } + // widen time to time with time zone + if (actualType.equals(TIME) && expectedType.equals(TIME_WITH_TIME_ZONE)) { + return true; + } + // widen timestamp to timestamp with time zone + if (actualType.equals(TIMESTAMP) && expectedType.equals(TIMESTAMP_WITH_TIME_ZONE)) { + return true; + } + + if (actualType.equals(VARCHAR) && expectedType.equals(REGEXP)) { + return true; + } + + if (actualType.equals(VARCHAR) && expectedType.equals(LIKE_PATTERN)) { + return true; + } + + if (actualType.equals(VARCHAR) && expectedType.equals(JSON_PATH)) { + return true; + } + + if (actualType instanceof ArrayType && expectedType instanceof ArrayType) { + Type actualElementType = ((ArrayType) actualType).getElementType(); + Type expectedElementType = ((ArrayType) expectedType).getElementType(); + return canCoerce(actualElementType, expectedElementType); + } + + return false; + } + + public static Optional getCommonSuperType(List types) + { + checkArgument(!types.isEmpty(), "types is empty"); + Type superType = UNKNOWN; + for (Type type : types) { + Optional commonSuperType = getCommonSuperType(superType, type); + if (!commonSuperType.isPresent()) { + return Optional.empty(); + } + superType = commonSuperType.get(); + } + return Optional.of(superType); + } + + public static Optional getCommonSuperType(Type firstType, Type secondType) + { + if (firstType.equals(UNKNOWN)) { + return Optional.of(secondType); + } + + if (secondType.equals(UNKNOWN)) { + return Optional.of(firstType); + } + + if (firstType.equals(secondType)) { + return Optional.of(firstType); + } + + if ((firstType.equals(BIGINT) || firstType.equals(DOUBLE)) && (secondType.equals(BIGINT) || secondType.equals(DOUBLE))) { + return Optional.of(DOUBLE); + } + // add auto type conversion from varchar to double + if ((firstType.equals(VARCHAR) || firstType.equals(DOUBLE)) && (secondType.equals(VARCHAR) || secondType.equals(DOUBLE))) { + return Optional.of(DOUBLE); + } + // add auto type conversion from varchar to double + if ((firstType.equals(VARCHAR) || firstType.equals(BIGINT)) && (secondType.equals(VARCHAR) || secondType.equals(BIGINT))) { + return Optional.of(BIGINT); + } + + if ((firstType.equals(TIME) || firstType.equals(TIME_WITH_TIME_ZONE)) && (secondType.equals(TIME) || secondType.equals(TIME_WITH_TIME_ZONE))) { + return Optional.of(TIME_WITH_TIME_ZONE); + } + + if ((firstType.equals(TIMESTAMP) || firstType.equals(TIMESTAMP_WITH_TIME_ZONE)) && (secondType.equals(TIMESTAMP) || secondType.equals(TIMESTAMP_WITH_TIME_ZONE))) { + return Optional.of(TIMESTAMP_WITH_TIME_ZONE); + } + + if (firstType instanceof ArrayType && secondType instanceof ArrayType) { + Optional elementType = getCommonSuperType(((ArrayType) firstType).getElementType(), ((ArrayType) secondType).getElementType()); + if (elementType.isPresent()) { + return Optional.of(new ArrayType(elementType.get())); + } + } + + // TODO add row and map type + + return Optional.empty(); + } + + public static Type type(Class clazz) + { + clazz = Primitives.unwrap(clazz); + if (clazz == long.class) { + return BIGINT; + } + if (clazz == double.class) { + return DOUBLE; + } + if (clazz == Slice.class) { + return VARCHAR; + } + if (clazz == boolean.class) { + return BOOLEAN; + } + throw new IllegalArgumentException("Unhandled Java type: " + clazz.getName()); + } + + public static Signature getMagicLiteralFunctionSignature(Type type) + { + TypeSignature argumentType; + if (type.getJavaType() == Slice.class && !type.equals(VARCHAR)) { + argumentType = VARBINARY.getTypeSignature(); + } + else { + argumentType = type(type.getJavaType()).getTypeSignature(); + } + + return new Signature(MAGIC_LITERAL_FUNCTION_PREFIX + type.getTypeSignature(), + type.getTypeSignature(), + argumentType); + } + + public static boolean isSupportedLiteralType(Type type) + { + return SUPPORTED_LITERAL_TYPES.contains(type.getJavaType()); + } + + public static FunctionInfo operatorInfo(OperatorType operatorType, TypeSignature returnType, List argumentTypes, MethodHandle method, boolean nullable, List nullableArguments) + { + operatorType.validateSignature(returnType, argumentTypes); + + Signature signature = Signature.internalOperator(operatorType.name(), returnType, argumentTypes); + return new FunctionInfo(signature, operatorType.getOperator(), true, method, true, nullable, nullableArguments); + } + + public static String mangleOperatorName(OperatorType operatorType) + { + return mangleOperatorName(operatorType.name()); + } + + public static String mangleOperatorName(String operatorName) + { + return OPERATOR_PREFIX + operatorName; + } + + @VisibleForTesting + public static OperatorType unmangleOperator(String mangledName) + { + checkArgument(mangledName.startsWith(OPERATOR_PREFIX), "%s is not a mangled operator name", mangledName); + return OperatorType.valueOf(mangledName.substring(OPERATOR_PREFIX.length())); + } + + private static class FunctionMap + { + private final Multimap functions; + + public FunctionMap() + { + functions = ImmutableListMultimap.of(); + } + + public FunctionMap(FunctionMap map, Iterable functions) + { + this.functions = ImmutableListMultimap.builder() + .putAll(map.functions) + .putAll(Multimaps.index(functions, function -> QualifiedName.of(function.getSignature().getName()))) + .build(); + + // Make sure all functions with the same name are aggregations or none of them are + for (Map.Entry> entry : this.functions.asMap().entrySet()) { + Collection values = entry.getValue(); + checkState(Iterables.all(values, ParametricFunction::isAggregate) || !Iterables.any(values, ParametricFunction::isAggregate), + "'%s' is both an aggregation and a scalar function", entry.getKey()); + } + } + + public List list() + { + return ImmutableList.copyOf(functions.values()); + } + + public Collection get(QualifiedName name) + { + return functions.get(name); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/HandleJsonModule.java b/presto-main/src/main/java/com/facebook/presto/metadata/HandleJsonModule.java new file mode 100644 index 00000000..d711b95a --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/HandleJsonModule.java @@ -0,0 +1,44 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +import com.facebook.presto.index.IndexHandleJacksonModule; +import com.facebook.presto.spi.ConnectorHandleResolver; +import com.google.inject.Binder; +import com.google.inject.Module; +import com.google.inject.Scopes; +import com.google.inject.multibindings.MapBinder; + +import static com.google.inject.multibindings.MapBinder.newMapBinder; +import static io.airlift.json.JsonBinder.jsonBinder; + +public class HandleJsonModule + implements Module +{ + @Override + public void configure(Binder binder) + { + jsonBinder(binder).addModuleBinding().to(TableHandleJacksonModule.class); + jsonBinder(binder).addModuleBinding().to(TableLayoutHandleJacksonModule.class); + jsonBinder(binder).addModuleBinding().to(ColumnHandleJacksonModule.class); + jsonBinder(binder).addModuleBinding().to(SplitJacksonModule.class); + jsonBinder(binder).addModuleBinding().to(OutputTableHandleJacksonModule.class); + jsonBinder(binder).addModuleBinding().to(InsertTableHandleJacksonModule.class); + jsonBinder(binder).addModuleBinding().to(IndexHandleJacksonModule.class); + + binder.bind(HandleResolver.class).in(Scopes.SINGLETON); + MapBinder connectorHandleResolverBinder = newMapBinder(binder, String.class, ConnectorHandleResolver.class); + connectorHandleResolverBinder.addBinding("remote").to(RemoteSplitHandleResolver.class).in(Scopes.SINGLETON); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/HandleResolver.java b/presto-main/src/main/java/com/facebook/presto/metadata/HandleResolver.java new file mode 100644 index 00000000..90c68dd1 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/HandleResolver.java @@ -0,0 +1,188 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorHandleResolver; +import com.facebook.presto.spi.ConnectorIndexHandle; +import com.facebook.presto.spi.ConnectorInsertTableHandle; +import com.facebook.presto.spi.ConnectorOutputTableHandle; +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.ConnectorTableHandle; +import com.facebook.presto.spi.ConnectorTableLayoutHandle; + +import javax.inject.Inject; + +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; + +public class HandleResolver +{ + private final ConcurrentMap handleIdResolvers = new ConcurrentHashMap<>(); + + public HandleResolver() + { + } + + @Inject + public HandleResolver(Map handleIdResolvers) + { + this.handleIdResolvers.putAll(handleIdResolvers); + } + + public void addHandleResolver(String id, ConnectorHandleResolver connectorHandleResolver) + { + ConnectorHandleResolver existingResolver = handleIdResolvers.putIfAbsent(id, connectorHandleResolver); + checkState(existingResolver == null, "Id %s is already assigned to resolver %s", id, existingResolver); + } + + public void removeHandleResolver(String id) + { + if (handleIdResolvers.containsKey(id)) { + handleIdResolvers.remove(id); + } + } + + public String getId(ConnectorTableHandle tableHandle) + { + for (Entry entry : handleIdResolvers.entrySet()) { + if (entry.getValue().canHandle(tableHandle)) { + return entry.getKey(); + } + } + throw new IllegalArgumentException("No connector for table handle: " + tableHandle); + } + + public String getId(ConnectorTableLayoutHandle handle) + { + if (handle instanceof LegacyTableLayoutHandle) { + LegacyTableLayoutHandle legacyHandle = (LegacyTableLayoutHandle) handle; + for (Entry entry : handleIdResolvers.entrySet()) { + if (entry.getValue().canHandle(legacyHandle.getTable())) { + return entry.getKey(); + } + } + } + else { + for (Entry entry : handleIdResolvers.entrySet()) { + if (entry.getValue().canHandle(handle)) { + return entry.getKey(); + } + } + } + throw new IllegalArgumentException("No connector for table handle: " + handle); + } + + public String getId(ColumnHandle columnHandle) + { + for (Entry entry : handleIdResolvers.entrySet()) { + if (entry.getValue().canHandle(columnHandle)) { + return entry.getKey(); + } + } + throw new IllegalArgumentException("No connector for column handle: " + columnHandle); + } + + public String getId(ConnectorSplit split) + { + for (Entry entry : handleIdResolvers.entrySet()) { + if (entry.getValue().canHandle(split)) { + return entry.getKey(); + } + } + throw new IllegalArgumentException("No connector for split: " + split); + } + + public String getId(ConnectorIndexHandle indexHandle) + { + for (Entry entry : handleIdResolvers.entrySet()) { + if (entry.getValue().canHandle(indexHandle)) { + return entry.getKey(); + } + } + throw new IllegalArgumentException("No connector for index handle: " + indexHandle); + } + + public String getId(ConnectorOutputTableHandle outputHandle) + { + for (Entry entry : handleIdResolvers.entrySet()) { + if (entry.getValue().canHandle(outputHandle)) { + return entry.getKey(); + } + } + throw new IllegalArgumentException("No connector for output table handle: " + outputHandle); + } + + public String getId(ConnectorInsertTableHandle insertHandle) + { + for (Entry entry : handleIdResolvers.entrySet()) { + if (entry.getValue().canHandle(insertHandle)) { + return entry.getKey(); + } + } + throw new IllegalArgumentException("No connector for insert table handle: " + insertHandle); + } + + public Class getTableHandleClass(String id) + { + return resolverFor(id).getTableHandleClass(); + } + + public Class getTableLayoutHandleClass(String id) + { + try { + return resolverFor(id).getTableLayoutHandleClass(); + } + catch (UnsupportedOperationException e) { + return LegacyTableLayoutHandle.class; + } + } + + public Class getColumnHandleClass(String id) + { + return resolverFor(id).getColumnHandleClass(); + } + + public Class getSplitClass(String id) + { + return resolverFor(id).getSplitClass(); + } + + public Class getIndexHandleClass(String id) + { + return resolverFor(id).getIndexHandleClass(); + } + + public Class getOutputTableHandleClass(String id) + { + return resolverFor(id).getOutputTableHandleClass(); + } + + public Class getInsertTableHandleClass(String id) + { + return resolverFor(id).getInsertTableHandleClass(); + } + + public ConnectorHandleResolver resolverFor(String id) + { + ConnectorHandleResolver resolver = handleIdResolvers.get(id); + checkArgument(resolver != null, "No handle resolver for %s", id); + return resolver; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/InMemoryNodeManager.java b/presto-main/src/main/java/com/facebook/presto/metadata/InMemoryNodeManager.java new file mode 100644 index 00000000..f4b59575 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/InMemoryNodeManager.java @@ -0,0 +1,96 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +import com.facebook.presto.spi.Node; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Multimaps; +import com.google.common.collect.SetMultimap; + +import javax.inject.Inject; + +import java.net.URI; +import java.util.Set; + +public class InMemoryNodeManager + implements InternalNodeManager +{ + private final Node localNode; + private final SetMultimap remoteNodes = Multimaps.synchronizedSetMultimap(HashMultimap.create()); + + @Inject + public InMemoryNodeManager() + { + this(URI.create("local://127.0.0.1")); + } + + public InMemoryNodeManager(URI localUri) + { + localNode = new PrestoNode("local", localUri, NodeVersion.UNKNOWN); + } + + public void addCurrentNodeDatasource(String datasourceName) + { + addNode(datasourceName, localNode); + } + + public void addNode(String datasourceName, Node... nodes) + { + addNode(datasourceName, ImmutableList.copyOf(nodes)); + } + + public void addNode(String datasourceName, Iterable nodes) + { + remoteNodes.putAll(datasourceName, nodes); + } + + @Override + public Set getActiveNodes() + { + return getAllNodes().getActiveNodes(); + } + + @Override + public Set getActiveDatasourceNodes(String datasourceName) + { + return ImmutableSet.copyOf(remoteNodes.get(datasourceName)); + } + + @Override + public AllNodes getAllNodes() + { + return new AllNodes(ImmutableSet.builder().add(localNode).addAll(remoteNodes.values()).build(), ImmutableSet.of()); + } + + @Override + public Node getCurrentNode() + { + return localNode; + } + + @Override + public Set getCoordinators() + { + // always use localNode as coordinator + return ImmutableSet.of(localNode); + } + + @Override + public void refreshNodes() + { + // no-op + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/IndexHandle.java b/presto-main/src/main/java/com/facebook/presto/metadata/IndexHandle.java new file mode 100644 index 00000000..203db330 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/IndexHandle.java @@ -0,0 +1,75 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +import com.facebook.presto.spi.ConnectorIndexHandle; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +import static com.google.common.base.Preconditions.checkNotNull; + +public final class IndexHandle +{ + private final String connectorId; + private final ConnectorIndexHandle connectorHandle; + + @JsonCreator + public IndexHandle( + @JsonProperty("connectorId") String connectorId, + @JsonProperty("connectorHandle") ConnectorIndexHandle connectorHandle) + { + this.connectorId = checkNotNull(connectorId, "connectorId is null"); + this.connectorHandle = checkNotNull(connectorHandle, "connectorHandle is null"); + } + + @JsonProperty + public String getConnectorId() + { + return connectorId; + } + + @JsonProperty + public ConnectorIndexHandle getConnectorHandle() + { + return connectorHandle; + } + + @Override + public int hashCode() + { + return Objects.hash(connectorId, connectorHandle); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final IndexHandle other = (IndexHandle) obj; + return Objects.equals(this.connectorId, other.connectorId) && + Objects.equals(this.connectorHandle, other.connectorHandle); + } + + @Override + public String toString() + { + return connectorId + ":" + connectorHandle; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/InsertTableHandle.java b/presto-main/src/main/java/com/facebook/presto/metadata/InsertTableHandle.java new file mode 100644 index 00000000..3d19e46c --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/InsertTableHandle.java @@ -0,0 +1,75 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +import com.facebook.presto.spi.ConnectorInsertTableHandle; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +import static com.google.common.base.Preconditions.checkNotNull; + +public final class InsertTableHandle +{ + private final String connectorId; + private final ConnectorInsertTableHandle connectorHandle; + + @JsonCreator + public InsertTableHandle( + @JsonProperty("connectorId") String connectorId, + @JsonProperty("connectorHandle") ConnectorInsertTableHandle connectorHandle) + { + this.connectorId = checkNotNull(connectorId, "connectorId is null"); + this.connectorHandle = checkNotNull(connectorHandle, "connectorHandle is null"); + } + + @JsonProperty + public String getConnectorId() + { + return connectorId; + } + + @JsonProperty + public ConnectorInsertTableHandle getConnectorHandle() + { + return connectorHandle; + } + + @Override + public int hashCode() + { + return Objects.hash(connectorId, connectorHandle); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + InsertTableHandle o = (InsertTableHandle) obj; + return Objects.equals(this.connectorId, o.connectorId) && + Objects.equals(this.connectorHandle, o.connectorHandle); + } + + @Override + public String toString() + { + return connectorId + ":" + connectorHandle; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/InsertTableHandleJacksonModule.java b/presto-main/src/main/java/com/facebook/presto/metadata/InsertTableHandleJacksonModule.java new file mode 100644 index 00000000..c0f4e228 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/InsertTableHandleJacksonModule.java @@ -0,0 +1,53 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +import com.facebook.presto.spi.ConnectorInsertTableHandle; + +import javax.inject.Inject; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class InsertTableHandleJacksonModule + extends AbstractTypedJacksonModule +{ + @Inject + public InsertTableHandleJacksonModule(HandleResolver handleResolver) + { + super(ConnectorInsertTableHandle.class, "type", new InsertTableHandleJsonTypeIdResolver(handleResolver)); + } + + private static class InsertTableHandleJsonTypeIdResolver + implements JsonTypeIdResolver + { + private final HandleResolver handleResolver; + + private InsertTableHandleJsonTypeIdResolver(HandleResolver handleResolver) + { + this.handleResolver = checkNotNull(handleResolver, "handleResolver is null"); + } + + @Override + public String getId(ConnectorInsertTableHandle tableHandle) + { + return handleResolver.getId(tableHandle); + } + + @Override + public Class getType(String id) + { + return handleResolver.getInsertTableHandleClass(id); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/InternalNodeManager.java b/presto-main/src/main/java/com/facebook/presto/metadata/InternalNodeManager.java new file mode 100644 index 00000000..d8b69d9a --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/InternalNodeManager.java @@ -0,0 +1,24 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +import com.facebook.presto.spi.NodeManager; + +public interface InternalNodeManager + extends NodeManager +{ + AllNodes getAllNodes(); + + void refreshNodes(); +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/InternalTable.java b/presto-main/src/main/java/com/facebook/presto/metadata/InternalTable.java new file mode 100644 index 00000000..7b942685 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/InternalTable.java @@ -0,0 +1,123 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +import com.facebook.presto.block.BlockUtils; +import com.facebook.presto.spi.ColumnMetadata; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class InternalTable +{ + private final Map columnIndexes; + private final List pages; + + public InternalTable(Map columnIndexes, Iterable pages) + { + this.columnIndexes = ImmutableMap.copyOf(checkNotNull(columnIndexes, "columnIndexes is null")); + this.pages = ImmutableList.copyOf(checkNotNull(pages, "pages is null")); + } + + public int getColumnIndex(String columnName) + { + Integer index = columnIndexes.get(columnName); + checkArgument(index != null, "Column %s not found", columnName); + return index; + } + + public List getPages() + { + return pages; + } + + public static Builder builder(ColumnMetadata... columns) + { + return builder(ImmutableList.copyOf(columns)); + } + + public static Builder builder(List columns) + { + ImmutableList.Builder names = ImmutableList.builder(); + ImmutableList.Builder types = ImmutableList.builder(); + for (ColumnMetadata column : columns) { + names.add(column.getName()); + types.add(column.getType()); + } + return new Builder(names.build(), types.build()); + } + + public static class Builder + { + private final Map columnIndexes; + private final List types; + private final List pages; + private PageBuilder pageBuilder; + + public Builder(List columnNames, List types) + { + checkNotNull(columnNames, "columnNames is null"); + + ImmutableMap.Builder columnIndexes = ImmutableMap.builder(); + int columnIndex = 0; + for (String columnName : columnNames) { + columnIndexes.put(columnName, columnIndex++); + } + this.columnIndexes = columnIndexes.build(); + + this.types = ImmutableList.copyOf(checkNotNull(types, "types is null")); + checkArgument(columnNames.size() == types.size(), + "Column name count does not match type count: columnNames=%s, types=%s", columnNames, types.size()); + + pages = new ArrayList<>(); + pageBuilder = new PageBuilder(types); + } + + public Builder add(Object... values) + { + pageBuilder.declarePosition(); + for (int i = 0; i < types.size(); i++) { + BlockUtils.appendObject(types.get(i), pageBuilder.getBlockBuilder(i), values[i]); + } + + if (pageBuilder.isFull()) { + flushPage(); + } + return this; + } + + public InternalTable build() + { + flushPage(); + return new InternalTable(columnIndexes, pages); + } + + private void flushPage() + { + if (!pageBuilder.isEmpty()) { + pages.add(pageBuilder.build()); + pageBuilder.reset(); + } + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/JsonTypeIdResolver.java b/presto-main/src/main/java/com/facebook/presto/metadata/JsonTypeIdResolver.java new file mode 100644 index 00000000..b114ddf1 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/JsonTypeIdResolver.java @@ -0,0 +1,21 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +public interface JsonTypeIdResolver +{ + String getId(T value); + + Class getType(String id); +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/LegacyTableLayoutHandle.java b/presto-main/src/main/java/com/facebook/presto/metadata/LegacyTableLayoutHandle.java new file mode 100644 index 00000000..dbe2b661 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/LegacyTableLayoutHandle.java @@ -0,0 +1,85 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +import com.facebook.presto.spi.ConnectorPartition; +import com.facebook.presto.spi.ConnectorTableHandle; +import com.facebook.presto.spi.ConnectorTableLayoutHandle; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; +import java.util.Objects; + +import static com.google.common.base.Preconditions.checkState; +import static java.util.Objects.requireNonNull; + +// Adaptation layer for connectors that implement the old getPartitions and getPartitionSplits API +// TODO: remove once all connectors migrate to getTableLayouts/getSplits API +public final class LegacyTableLayoutHandle + implements ConnectorTableLayoutHandle +{ + private final ConnectorTableHandle table; + private final List partitions; + + @JsonCreator + public LegacyTableLayoutHandle(@JsonProperty("table") ConnectorTableHandle table) + { + requireNonNull(table, "table is null"); + + this.table = table; + this.partitions = null; + } + + public LegacyTableLayoutHandle(ConnectorTableHandle table, List partitions) + { + requireNonNull(table, "table is null"); + requireNonNull(partitions, "partitions is null"); + + this.table = table; + this.partitions = partitions; + } + + @JsonProperty + public ConnectorTableHandle getTable() + { + return table; + } + + public List getPartitions() + { + checkState(partitions != null, "Partitions dropped by serialization"); + return partitions; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + LegacyTableLayoutHandle that = (LegacyTableLayoutHandle) o; + return Objects.equals(table, that.table) && + Objects.equals(partitions, that.partitions); + } + + @Override + public int hashCode() + { + return Objects.hash(table, partitions); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/Metadata.java b/presto-main/src/main/java/com/facebook/presto/metadata/Metadata.java new file mode 100644 index 00000000..974cd174 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/Metadata.java @@ -0,0 +1,235 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +import com.facebook.presto.Session; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ColumnMetadata; +import com.facebook.presto.spi.Constraint; +import com.facebook.presto.spi.InsertOption; +import com.facebook.presto.spi.block.BlockEncodingSerde; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.spi.type.TypeSignature; +import com.facebook.presto.sql.tree.QualifiedName; + +import io.airlift.slice.Slice; + +import javax.validation.constraints.NotNull; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +public interface Metadata +{ + Type getType(TypeSignature signature); + + FunctionInfo resolveFunction(QualifiedName name, List parameterTypes, boolean approximate); + + @NotNull + FunctionInfo getExactFunction(Signature handle); + + boolean isAggregationFunction(QualifiedName name); + + @NotNull + List listFunctions(); + + void addFunctions(List functions); + + FunctionInfo resolveOperator(OperatorType operatorType, List argumentTypes) + throws OperatorNotFoundException; + + @NotNull + List listSchemaNames(Session session, String catalogName); + + /** + * Returns a table handle for the specified table name. + */ + @NotNull + Optional getTableHandle(Session session, QualifiedTableName tableName); + + @NotNull + List getLayouts(TableHandle tableHandle, Constraint constraint, Optional> desiredColumns); + + @NotNull + TableLayout getLayout(TableLayoutHandle handle); + + /** + * Return the metadata for the specified table handle. + * + * @throws RuntimeException if table handle is no longer valid + */ + @NotNull + TableMetadata getTableMetadata(TableHandle tableHandle); + + /** + * Get the names that match the specified table prefix (never null). + */ + @NotNull + List listTables(Session session, QualifiedTablePrefix prefix); + + /** + * Returns the handle for the sample weight column. + * + * @throws RuntimeException if the table handle is no longer valid + */ + @NotNull + Optional getSampleWeightColumnHandle(TableHandle tableHandle); + + /** + * Returns true iff this catalog supports creation of sampled tables + * + */ + boolean canCreateSampledTables(Session session, String catalogName); + + /** + * Gets all of the columns on the specified table, or an empty map if the columns can not be enumerated. + * + * @throws RuntimeException if table handle is no longer valid + */ + @NotNull + Map getColumnHandles(TableHandle tableHandle); + + /** + * Gets the metadata for the specified table column. + * + * @throws RuntimeException if table or column handles are no longer valid + */ + @NotNull + ColumnMetadata getColumnMetadata(TableHandle tableHandle, ColumnHandle columnHandle); + + /** + * Gets the metadata for all columns that match the specified table prefix. + */ + @NotNull + Map> listTableColumns(Session session, QualifiedTablePrefix prefix); + + /** + * Creates a table using the specified table metadata. + */ + @NotNull + void createTable(Session session, String catalogName, TableMetadata tableMetadata); + + /** + * Rename the specified table. + */ + void renameTable(TableHandle tableHandle, QualifiedTableName newTableName); + + /** + * Rename the specified column. + */ + void renameColumn(TableHandle tableHandle, ColumnHandle source, String target); + + /** + * Drops the specified table + * + * @throws RuntimeException if the table can not be dropped or table handle is no longer valid + */ + void dropTable(TableHandle tableHandle); + + /** + * Begin the atomic creation of a table with data. + */ + OutputTableHandle beginCreateTable(Session session, String catalogName, TableMetadata tableMetadata); + + /** + * Commit a table creation with data after the data is written. + */ + void commitCreateTable(OutputTableHandle tableHandle, Collection fragments); + + /** + * Rollback a table creation + */ + void rollbackCreateTable(OutputTableHandle tableHandle); + + /** + * Begin insert query + */ + InsertTableHandle beginInsert(Session session, TableHandle tableHandle, InsertOption insertOption); + + /** + * Commit insert query + */ + void commitInsert(InsertTableHandle tableHandle, Collection fragments); + + /** + * Rollback insert query + */ + void rollbackInsert(InsertTableHandle tableHandle); + + /** + * Get the row ID column handle used with UpdatablePageSource. + */ + ColumnHandle getUpdateRowIdColumnHandle(TableHandle tableHandle); + + /** + * Begin delete query + */ + TableHandle beginDelete(Session session, TableHandle tableHandle); + + /** + * Commit delete query + */ + void commitDelete(TableHandle tableHandle, Collection fragments); + + /** + * Rollback delete query + */ + void rollbackDelete(TableHandle tableHandle); + + /** + * Gets all the loaded catalogs + * + * @return Map of catalog name to connector id + */ + @NotNull + Map getCatalogNames(); + + /** + * Get the names that match the specified table prefix (never null). + */ + @NotNull + List listViews(Session session, QualifiedTablePrefix prefix); + + /** + * Get the view definitions that match the specified table prefix (never null). + */ + @NotNull + Map getViews(Session session, QualifiedTablePrefix prefix); + + /** + * Returns the view definition for the specified view name. + */ + @NotNull + Optional getView(Session session, QualifiedTableName viewName); + + /** + * Creates the specified view with the specified view definition. + */ + void createView(Session session, QualifiedTableName viewName, String viewData, boolean replace); + + /** + * Drops the specified view. + */ + void dropView(Session session, QualifiedTableName viewName); + + FunctionRegistry getFunctionRegistry(); + + TypeManager getTypeManager(); + + BlockEncodingSerde getBlockEncodingSerde(); +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/MetadataManager.java b/presto-main/src/main/java/com/facebook/presto/metadata/MetadataManager.java new file mode 100644 index 00000000..5e10eb31 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/MetadataManager.java @@ -0,0 +1,725 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +import com.facebook.presto.Session; +import com.facebook.presto.block.BlockEncodingManager; +import com.facebook.presto.connector.informationSchema.InformationSchemaMetadata; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ColumnMetadata; +import com.facebook.presto.spi.ConnectorInsertTableHandle; +import com.facebook.presto.spi.ConnectorMetadata; +import com.facebook.presto.spi.ConnectorOutputTableHandle; +import com.facebook.presto.spi.ConnectorPartition; +import com.facebook.presto.spi.ConnectorPartitionResult; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.ConnectorSplitManager; +import com.facebook.presto.spi.ConnectorTableHandle; +import com.facebook.presto.spi.ConnectorTableLayout; +import com.facebook.presto.spi.ConnectorTableLayoutResult; +import com.facebook.presto.spi.ConnectorTableMetadata; +import com.facebook.presto.spi.Constraint; +import com.facebook.presto.spi.InsertOption; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.spi.SchemaTablePrefix; +import com.facebook.presto.spi.TupleDomain; +import com.facebook.presto.spi.block.BlockEncodingSerde; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.spi.type.TypeSignature; +import com.facebook.presto.split.SplitManager; +import com.facebook.presto.sql.analyzer.FeaturesConfig; +import com.facebook.presto.sql.tree.QualifiedName; +import com.facebook.presto.type.TypeDeserializer; +import com.facebook.presto.type.TypeRegistry; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; + +import io.airlift.json.JsonCodec; +import io.airlift.json.JsonCodecFactory; +import io.airlift.json.ObjectMapperProvider; +import io.airlift.slice.Slice; + +import javax.inject.Inject; + +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.function.Predicate; + +import static com.facebook.presto.metadata.MetadataUtil.checkCatalogName; +import static com.facebook.presto.metadata.QualifiedTableName.convertFromSchemaTableName; +import static com.facebook.presto.metadata.TableLayout.fromConnectorLayout; +import static com.facebook.presto.metadata.ViewDefinition.ViewColumn; +import static com.facebook.presto.spi.StandardErrorCode.INVALID_VIEW; +import static com.facebook.presto.spi.StandardErrorCode.NOT_FOUND; +import static com.facebook.presto.spi.StandardErrorCode.SYNTAX_ERROR; +import static com.facebook.presto.util.ImmutableCollectors.toImmutableList; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.Iterables.transform; +import static java.lang.String.format; + +public class MetadataManager + implements Metadata +{ + private static final String INFORMATION_SCHEMA_NAME = "information_schema"; + + private final ConcurrentMap informationSchemasByCatalog = new ConcurrentHashMap<>(); + private final ConcurrentMap systemTablesByCatalog = new ConcurrentHashMap<>(); + private final ConcurrentMap connectorsByCatalog = new ConcurrentHashMap<>(); + private final ConcurrentMap connectorsById = new ConcurrentHashMap<>(); + private final FunctionRegistry functions; + private final TypeManager typeManager; + private final JsonCodec viewCodec; + private final SplitManager splitManager; + private final BlockEncodingSerde blockEncodingSerde; + + public MetadataManager(FeaturesConfig featuresConfig, TypeManager typeManager, SplitManager splitManager, BlockEncodingSerde blockEncodingSerde) + { + this(featuresConfig, typeManager, createTestingViewCodec(), splitManager, blockEncodingSerde); + } + + @Inject + public MetadataManager(FeaturesConfig featuresConfig, TypeManager typeManager, JsonCodec viewCodec, SplitManager splitManager, BlockEncodingSerde blockEncodingSerde) + { + functions = new FunctionRegistry(typeManager, blockEncodingSerde, featuresConfig.isExperimentalSyntaxEnabled()); + this.typeManager = checkNotNull(typeManager, "types is null"); + this.viewCodec = checkNotNull(viewCodec, "viewCodec is null"); + this.splitManager = checkNotNull(splitManager, "splitManager is null"); + this.blockEncodingSerde = checkNotNull(blockEncodingSerde, "blockEncodingSerde is null"); + } + + public static MetadataManager createTestMetadataManager() + { + FeaturesConfig featuresConfig = new FeaturesConfig(); + TypeManager typeManager = new TypeRegistry(); + SplitManager splitManager = new SplitManager(); + BlockEncodingSerde blockEncodingSerde = new BlockEncodingManager(typeManager); + return new MetadataManager(featuresConfig, typeManager, splitManager, blockEncodingSerde); + } + + public synchronized void addConnectorMetadata(String connectorId, String catalogName, ConnectorMetadata connectorMetadata) + { + checkMetadataArguments(connectorId, catalogName, connectorMetadata); + checkArgument(!connectorsByCatalog.containsKey(catalogName), "Catalog '%s' is already registered", catalogName); + + connectorsById.put(connectorId, connectorMetadata); + connectorsByCatalog.put(catalogName, new ConnectorMetadataEntry(connectorId, connectorMetadata)); + } + + public synchronized void removeConnectorMetadata(String catalogName) + { + if (connectorsByCatalog.containsKey(catalogName)) { + connectorsByCatalog.remove(catalogName); + } + } + + public synchronized void addInformationSchemaMetadata(String connectorId, String catalogName, InformationSchemaMetadata metadata) + { + checkMetadataArguments(connectorId, catalogName, metadata); + checkArgument(!informationSchemasByCatalog.containsKey(catalogName), "Information schema for catalog '%s' is already registered", catalogName); + + connectorsById.put(connectorId, metadata); + informationSchemasByCatalog.put(catalogName, new ConnectorMetadataEntry(connectorId, metadata)); + } + + public synchronized void removeInformationSchemaMetadata(String connectorId, String catalogName) + { + if (informationSchemasByCatalog.containsKey(catalogName)) { + informationSchemasByCatalog.remove(catalogName); + } + if (connectorsById.containsKey(connectorId)) { + connectorsById.remove(connectorId); + } + } + + public synchronized void addSystemTablesMetadata(String connectorId, String catalogName, ConnectorMetadata metadata) + { + checkMetadataArguments(connectorId, catalogName, metadata); + checkArgument(!systemTablesByCatalog.containsKey(catalogName), "System tables for catalog '%s' are already registered", catalogName); + + connectorsById.put(connectorId, metadata); + systemTablesByCatalog.put(catalogName, new ConnectorMetadataEntry(connectorId, metadata)); + } + + public synchronized void removeSystemTablesMetadata(String connectorId, String catalogName) + { + if (systemTablesByCatalog.containsKey(catalogName)) { + systemTablesByCatalog.remove(catalogName); + } + if (connectorsById.containsKey(connectorId)) { + connectorsById.remove(connectorId); + } + } + + public synchronized void removeConnectorsById(String connectorId) + { + if (connectorsById.containsKey(connectorId)) { + connectorsById.remove(connectorId); + } + } + + private void checkMetadataArguments(String connectorId, String catalogName, ConnectorMetadata metadata) + { + checkNotNull(connectorId, "connectorId is null"); + checkNotNull(catalogName, "catalogName is null"); + checkNotNull(metadata, "metadata is null"); + checkArgument(!connectorsById.containsKey(connectorId), "Connector '%s' is already registered", connectorId); + } + + @Override + public Type getType(TypeSignature signature) + { + return typeManager.getType(signature); + } + + @Override + public FunctionInfo resolveFunction(QualifiedName name, List parameterTypes, boolean approximate) + { + return functions.resolveFunction(name, parameterTypes, approximate); + } + + @Override + public FunctionInfo getExactFunction(Signature handle) + { + return functions.getExactFunction(handle); + } + + @Override + public boolean isAggregationFunction(QualifiedName name) + { + return functions.isAggregationFunction(name); + } + + @Override + public List listFunctions() + { + return functions.list(); + } + + @Override + public void addFunctions(List functionInfos) + { + functions.addFunctions(functionInfos); + } + + @Override + public FunctionInfo resolveOperator(OperatorType operatorType, List argumentTypes) + throws OperatorNotFoundException + { + return functions.resolveOperator(operatorType, argumentTypes); + } + + @Override + public List listSchemaNames(Session session, String catalogName) + { + checkCatalogName(catalogName); + ImmutableSet.Builder schemaNames = ImmutableSet.builder(); + for (ConnectorMetadataEntry entry : allConnectorsFor(catalogName)) { + schemaNames.addAll(entry.getMetadata().listSchemaNames(session.toConnectorSession(entry.getCatalog()))); + } + return ImmutableList.copyOf(schemaNames.build()); + } + + @Override + public Optional getTableHandle(Session session, QualifiedTableName table) + { + checkNotNull(table, "table is null"); + + ConnectorMetadataEntry entry = getConnectorFor(table); + if (entry != null) { + ConnectorMetadata metadata = entry.getMetadata(); + + ConnectorTableHandle tableHandle = metadata.getTableHandle(session.toConnectorSession(entry.getCatalog()), table.asSchemaTableName()); + + if (tableHandle != null) { + return Optional.of(new TableHandle(entry.getConnectorId(), tableHandle)); + } + } + return Optional.empty(); + } + + @Override + public List getLayouts(TableHandle table, Constraint constraint, Optional> desiredColumns) + { + if (constraint.getSummary().isNone()) { + return ImmutableList.of(); + } + + TupleDomain summary = constraint.getSummary(); + String connectorId = table.getConnectorId(); + ConnectorTableHandle connectorTable = table.getConnectorHandle(); + Predicate> predicate = constraint.predicate(); + + List layouts; + try { + ConnectorMetadata metadata = getConnectorMetadata(connectorId); + layouts = metadata.getTableLayouts(connectorTable, new Constraint<>(summary, predicate::test), desiredColumns); + } + catch (UnsupportedOperationException e) { + ConnectorSplitManager connectorSplitManager = splitManager.getConnectorSplitManager(connectorId); + ConnectorPartitionResult result = connectorSplitManager.getPartitions(connectorTable, summary); + + List partitions = result.getPartitions().stream() + .filter(partition -> predicate.test(partition.getTupleDomain().extractFixedValues())) + .collect(toImmutableList()); + + List> partitionDomains = partitions.stream() + .map(ConnectorPartition::getTupleDomain) + .collect(toImmutableList()); + + TupleDomain effectivePredicate = TupleDomain.none(); + if (!partitionDomains.isEmpty()) { + effectivePredicate = TupleDomain.columnWiseUnion(partitionDomains); + } + + ConnectorTableLayout layout = new ConnectorTableLayout(new LegacyTableLayoutHandle(connectorTable, partitions), Optional.empty(), effectivePredicate, Optional.empty(), Optional.of(partitionDomains), ImmutableList.of()); + layouts = ImmutableList.of(new ConnectorTableLayoutResult(layout, result.getUndeterminedTupleDomain())); + } + + return layouts.stream() + .map(entry -> new TableLayoutResult(fromConnectorLayout(connectorId, entry.getTableLayout()), entry.getUnenforcedConstraint())) + .collect(toImmutableList()); + } + + public TableLayout getLayout(TableLayoutHandle handle) + { + if (handle.getConnectorHandle() instanceof LegacyTableLayoutHandle) { + LegacyTableLayoutHandle legacyHandle = (LegacyTableLayoutHandle) handle.getConnectorHandle(); + List> partitionDomains = legacyHandle.getPartitions().stream() + .map(ConnectorPartition::getTupleDomain) + .collect(toImmutableList()); + + TupleDomain predicate = TupleDomain.none(); + if (!partitionDomains.isEmpty()) { + predicate = TupleDomain.columnWiseUnion(partitionDomains); + } + return new TableLayout(handle, new ConnectorTableLayout(legacyHandle, Optional.empty(), predicate, Optional.empty(), Optional.of(partitionDomains), ImmutableList.of())); + } + + String connectorId = handle.getConnectorId(); + ConnectorMetadata metadata = getConnectorMetadata(connectorId); + return fromConnectorLayout(connectorId, metadata.getTableLayout(handle.getConnectorHandle())); + } + + @Override + public TableMetadata getTableMetadata(TableHandle tableHandle) + { + ConnectorTableMetadata tableMetadata = lookupConnectorFor(tableHandle).getTableMetadata(tableHandle.getConnectorHandle()); + + return new TableMetadata(tableHandle.getConnectorId(), tableMetadata); + } + + @Override + public Map getColumnHandles(TableHandle tableHandle) + { + return lookupConnectorFor(tableHandle).getColumnHandles(tableHandle.getConnectorHandle()); + } + + @Override + public ColumnMetadata getColumnMetadata(TableHandle tableHandle, ColumnHandle columnHandle) + { + checkNotNull(tableHandle, "tableHandle is null"); + checkNotNull(columnHandle, "columnHandle is null"); + + return lookupConnectorFor(tableHandle).getColumnMetadata(tableHandle.getConnectorHandle(), columnHandle); + } + + @Override + public List listTables(Session session, QualifiedTablePrefix prefix) + { + checkNotNull(prefix, "prefix is null"); + + String schemaNameOrNull = prefix.getSchemaName().orElse(null); + Set tables = new LinkedHashSet<>(); + for (ConnectorMetadataEntry entry : allConnectorsFor(prefix.getCatalogName())) { + ConnectorSession connectorSession = session.toConnectorSession(entry.getCatalog()); + for (QualifiedTableName tableName : transform(entry.getMetadata().listTables(connectorSession, schemaNameOrNull), convertFromSchemaTableName(prefix.getCatalogName()))) { + tables.add(tableName); + } + } + return ImmutableList.copyOf(tables); + } + + @Override + public Optional getSampleWeightColumnHandle(TableHandle tableHandle) + { + checkNotNull(tableHandle, "tableHandle is null"); + ColumnHandle handle = lookupConnectorFor(tableHandle).getSampleWeightColumnHandle(tableHandle.getConnectorHandle()); + + return Optional.ofNullable(handle); + } + + @Override + public boolean canCreateSampledTables(Session session, String catalogName) + { + ConnectorMetadataEntry connectorMetadata = connectorsByCatalog.get(catalogName); + checkArgument(connectorMetadata != null, "Catalog %s does not exist", catalogName); + return connectorMetadata.getMetadata().canCreateSampledTables(session.toConnectorSession(connectorMetadata.getCatalog())); + } + + @Override + public Map> listTableColumns(Session session, QualifiedTablePrefix prefix) + { + checkNotNull(prefix, "prefix is null"); + SchemaTablePrefix tablePrefix = prefix.asSchemaTablePrefix(); + + Map> tableColumns = new HashMap<>(); + for (ConnectorMetadataEntry connectorMetadata : allConnectorsFor(prefix.getCatalogName())) { + ConnectorMetadata metadata = connectorMetadata.getMetadata(); + + ConnectorSession connectorSession = session.toConnectorSession(connectorMetadata.getCatalog()); + for (Entry> entry : metadata.listTableColumns(connectorSession, tablePrefix).entrySet()) { + QualifiedTableName tableName = new QualifiedTableName( + prefix.getCatalogName(), + entry.getKey().getSchemaName(), + entry.getKey().getTableName()); + tableColumns.put(tableName, entry.getValue()); + } + + // if table and view names overlap, the view wins + for (Entry entry : metadata.getViews(connectorSession, tablePrefix).entrySet()) { + QualifiedTableName tableName = new QualifiedTableName( + prefix.getCatalogName(), + entry.getKey().getSchemaName(), + entry.getKey().getTableName()); + + ImmutableList.Builder columns = ImmutableList.builder(); + for (ViewColumn column : deserializeView(entry.getValue()).getColumns()) { + columns.add(new ColumnMetadata(column.getName(), column.getType(), false)); + } + + tableColumns.put(tableName, columns.build()); + } + } + return ImmutableMap.copyOf(tableColumns); + } + + @Override + public void createTable(Session session, String catalogName, TableMetadata tableMetadata) + { + ConnectorMetadataEntry connectorMetadata = connectorsByCatalog.get(catalogName); + checkArgument(connectorMetadata != null, "Catalog %s does not exist", catalogName); + + connectorMetadata.getMetadata().createTable(session.toConnectorSession(connectorMetadata.getCatalog()), tableMetadata.getMetadata()); + } + + @Override + public void renameTable(TableHandle tableHandle, QualifiedTableName newTableName) + { + String catalogName = newTableName.getCatalogName(); + ConnectorMetadataEntry target = connectorsByCatalog.get(catalogName); + if (target == null) { + throw new PrestoException(NOT_FOUND, format("Target catalog '%s' does not exist", catalogName)); + } + if (!tableHandle.getConnectorId().equals(target.getConnectorId())) { + throw new PrestoException(SYNTAX_ERROR, "Cannot rename tables across catalogs"); + } + + lookupConnectorFor(tableHandle).renameTable(tableHandle.getConnectorHandle(), newTableName.asSchemaTableName()); + } + + @Override + public void renameColumn(TableHandle tableHandle, ColumnHandle source, String target) + { + lookupConnectorFor(tableHandle).renameColumn(tableHandle.getConnectorHandle(), source, target); + } + + @Override + public void dropTable(TableHandle tableHandle) + { + lookupConnectorFor(tableHandle).dropTable(tableHandle.getConnectorHandle()); + } + + @Override + public OutputTableHandle beginCreateTable(Session session, String catalogName, TableMetadata tableMetadata) + { + ConnectorMetadataEntry connectorMetadata = connectorsByCatalog.get(catalogName); + checkArgument(connectorMetadata != null, "Catalog %s does not exist", catalogName); + ConnectorSession connectorSession = session.toConnectorSession(connectorMetadata.getCatalog()); + ConnectorOutputTableHandle handle = connectorMetadata.getMetadata().beginCreateTable(connectorSession, tableMetadata.getMetadata()); + return new OutputTableHandle(connectorMetadata.getConnectorId(), handle); + } + + @Override + public void commitCreateTable(OutputTableHandle tableHandle, Collection fragments) + { + lookupConnectorFor(tableHandle).commitCreateTable(tableHandle.getConnectorHandle(), fragments); + } + + @Override + public void rollbackCreateTable(OutputTableHandle tableHandle) + { + lookupConnectorFor(tableHandle).rollbackCreateTable(tableHandle.getConnectorHandle()); + } + + @Override + public InsertTableHandle beginInsert(Session session, TableHandle tableHandle, InsertOption insertOption) + { + // assume connectorId and catalog are the same + ConnectorSession connectorSession = session.toConnectorSession(tableHandle.getConnectorId()); + ConnectorInsertTableHandle handle = lookupConnectorFor(tableHandle).beginInsert(connectorSession, tableHandle.getConnectorHandle(), insertOption); + return new InsertTableHandle(tableHandle.getConnectorId(), handle); + } + + @Override + public void commitInsert(InsertTableHandle tableHandle, Collection fragments) + { + lookupConnectorFor(tableHandle).commitInsert(tableHandle.getConnectorHandle(), fragments); + } + + @Override + public void rollbackInsert(InsertTableHandle tableHandle) + { + lookupConnectorFor(tableHandle).rollbackInsert(tableHandle.getConnectorHandle()); + } + + @Override + public ColumnHandle getUpdateRowIdColumnHandle(TableHandle tableHandle) + { + return lookupConnectorFor(tableHandle).getUpdateRowIdColumnHandle(tableHandle.getConnectorHandle()); + } + + @Override + public TableHandle beginDelete(Session session, TableHandle tableHandle) + { + ConnectorTableHandle newHandle = lookupConnectorFor(tableHandle).beginDelete(tableHandle.getConnectorHandle()); + return new TableHandle(tableHandle.getConnectorId(), newHandle); + } + + @Override + public void commitDelete(TableHandle tableHandle, Collection fragments) + { + lookupConnectorFor(tableHandle).commitDelete(tableHandle.getConnectorHandle(), fragments); + } + + @Override + public void rollbackDelete(TableHandle tableHandle) + { + lookupConnectorFor(tableHandle).rollbackDelete(tableHandle.getConnectorHandle()); + } + + @Override + public Map getCatalogNames() + { + ImmutableMap.Builder catalogsMap = ImmutableMap.builder(); + for (Map.Entry entry : connectorsByCatalog.entrySet()) { + catalogsMap.put(entry.getKey(), entry.getValue().getConnectorId()); + } + return catalogsMap.build(); + } + + @Override + public List listViews(Session session, QualifiedTablePrefix prefix) + { + checkNotNull(prefix, "prefix is null"); + + String schemaNameOrNull = prefix.getSchemaName().orElse(null); + Set views = new LinkedHashSet<>(); + for (ConnectorMetadataEntry entry : allConnectorsFor(prefix.getCatalogName())) { + ConnectorSession connectorSession = session.toConnectorSession(entry.getCatalog()); + for (QualifiedTableName tableName : transform(entry.getMetadata().listViews(connectorSession, schemaNameOrNull), convertFromSchemaTableName(prefix.getCatalogName()))) { + views.add(tableName); + } + } + return ImmutableList.copyOf(views); + } + + @Override + public Map getViews(Session session, QualifiedTablePrefix prefix) + { + checkNotNull(prefix, "prefix is null"); + SchemaTablePrefix tablePrefix = prefix.asSchemaTablePrefix(); + + Map views = new LinkedHashMap<>(); + for (ConnectorMetadataEntry metadata : allConnectorsFor(prefix.getCatalogName())) { + ConnectorSession connectorSession = session.toConnectorSession(metadata.getCatalog()); + for (Entry entry : metadata.getMetadata().getViews(connectorSession, tablePrefix).entrySet()) { + QualifiedTableName viewName = new QualifiedTableName( + prefix.getCatalogName(), + entry.getKey().getSchemaName(), + entry.getKey().getTableName()); + views.put(viewName, deserializeView(entry.getValue())); + } + } + return ImmutableMap.copyOf(views); + } + + @Override + public Optional getView(Session session, QualifiedTableName viewName) + { + ConnectorMetadataEntry entry = getConnectorFor(viewName); + if (entry != null) { + SchemaTablePrefix prefix = viewName.asSchemaTableName().toSchemaTablePrefix(); + Map views = entry.getMetadata().getViews(session.toConnectorSession(entry.getCatalog()), prefix); + String view = views.get(viewName.asSchemaTableName()); + if (view != null) { + return Optional.of(deserializeView(view)); + } + } + return Optional.empty(); + } + + @Override + public void createView(Session session, QualifiedTableName viewName, String viewData, boolean replace) + { + ConnectorMetadataEntry connectorMetadata = connectorsByCatalog.get(viewName.getCatalogName()); + checkArgument(connectorMetadata != null, "Catalog %s does not exist", viewName.getCatalogName()); + connectorMetadata.getMetadata().createView(session.toConnectorSession(connectorMetadata.getCatalog()), viewName.asSchemaTableName(), viewData, replace); + } + + @Override + public void dropView(Session session, QualifiedTableName viewName) + { + ConnectorMetadataEntry connectorMetadata = connectorsByCatalog.get(viewName.getCatalogName()); + checkArgument(connectorMetadata != null, "Catalog %s does not exist", viewName.getCatalogName()); + connectorMetadata.getMetadata().dropView(session.toConnectorSession(connectorMetadata.getCatalog()), viewName.asSchemaTableName()); + } + + @Override + public FunctionRegistry getFunctionRegistry() + { + return functions; + } + + @Override + public TypeManager getTypeManager() + { + return typeManager; + } + + @Override + public BlockEncodingSerde getBlockEncodingSerde() + { + return blockEncodingSerde; + } + + private ViewDefinition deserializeView(String data) + { + try { + return viewCodec.fromJson(data); + } + catch (IllegalArgumentException e) { + throw new PrestoException(INVALID_VIEW, "Invalid view JSON: " + data, e); + } + } + + private List allConnectorsFor(String catalogName) + { + ImmutableList.Builder builder = ImmutableList.builder(); + + ConnectorMetadataEntry entry = informationSchemasByCatalog.get(catalogName); + if (entry != null) { + builder.add(entry); + } + + ConnectorMetadataEntry systemTables = systemTablesByCatalog.get(catalogName); + if (systemTables != null) { + builder.add(systemTables); + } + + ConnectorMetadataEntry connector = connectorsByCatalog.get(catalogName); + if (connector != null) { + builder.add(connector); + } + + return builder.build(); + } + + private ConnectorMetadataEntry getConnectorFor(QualifiedTableName name) + { + String catalog = name.getCatalogName(); + String schema = name.getSchemaName(); + + if (schema.equals(INFORMATION_SCHEMA_NAME)) { + return informationSchemasByCatalog.get(catalog); + } + + ConnectorMetadataEntry entry = systemTablesByCatalog.get(catalog); + if ((entry != null) && (entry.getMetadata().getTableHandle(null, name.asSchemaTableName()) != null)) { + return entry; + } + + return connectorsByCatalog.get(catalog); + } + + private ConnectorMetadata lookupConnectorFor(TableHandle tableHandle) + { + return getConnectorMetadata(tableHandle.getConnectorId()); + } + + private ConnectorMetadata lookupConnectorFor(OutputTableHandle tableHandle) + { + return getConnectorMetadata(tableHandle.getConnectorId()); + } + + private ConnectorMetadata lookupConnectorFor(InsertTableHandle tableHandle) + { + return getConnectorMetadata(tableHandle.getConnectorId()); + } + + private ConnectorMetadata getConnectorMetadata(String connectorId) + { + ConnectorMetadata result = connectorsById.get(connectorId); + checkArgument(result != null, "No connector for connector ID: %s", connectorId); + return result; + } + + private static class ConnectorMetadataEntry + { + private final String connectorId; + private final ConnectorMetadata metadata; + + private ConnectorMetadataEntry(String connectorId, ConnectorMetadata metadata) + { + this.connectorId = checkNotNull(connectorId, "connectorId is null"); + this.metadata = checkNotNull(metadata, "metadata is null"); + } + + private String getConnectorId() + { + return connectorId; + } + + private String getCatalog() + { + // assume connectorId and catalog are the same + return connectorId; + } + + private ConnectorMetadata getMetadata() + { + return metadata; + } + } + + private static JsonCodec createTestingViewCodec() + { + ObjectMapperProvider provider = new ObjectMapperProvider(); + provider.setJsonDeserializers(ImmutableMap., JsonDeserializer>of(Type.class, new TypeDeserializer(new TypeRegistry()))); + return new JsonCodecFactory(provider).jsonCodec(ViewDefinition.class); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/MetadataUtil.java b/presto-main/src/main/java/com/facebook/presto/metadata/MetadataUtil.java new file mode 100644 index 00000000..9bd8344a --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/MetadataUtil.java @@ -0,0 +1,180 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +import com.facebook.presto.Session; +import com.facebook.presto.spi.ColumnMetadata; +import com.facebook.presto.spi.ConnectorTableMetadata; +import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.tree.QualifiedName; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; + +import java.util.List; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.Locale.ENGLISH; + +public final class MetadataUtil +{ + private MetadataUtil() {} + + public static void checkTableName(String catalogName, Optional schemaName, Optional tableName) + { + checkCatalogName(catalogName); + checkSchemaName(schemaName); + checkTableName(tableName); + + checkArgument(schemaName.isPresent() || !tableName.isPresent(), "tableName specified but schemaName is missing"); + } + + public static String checkCatalogName(String catalogName) + { + return checkLowerCase(catalogName, "catalogName"); + } + + public static String checkSchemaName(String schemaName) + { + return checkLowerCase(schemaName, "schemaName"); + } + + public static Optional checkSchemaName(Optional schemaName) + { + return checkLowerCase(schemaName, "schemaName"); + } + + public static String checkTableName(String tableName) + { + return checkLowerCase(tableName, "tableName"); + } + + public static Optional checkTableName(Optional tableName) + { + return checkLowerCase(tableName, "tableName"); + } + + public static String checkColumnName(String catalogName) + { + return checkLowerCase(catalogName, "catalogName"); + } + + public static void checkTableName(String catalogName, String schemaName, String tableName) + { + checkLowerCase(catalogName, "catalogName"); + checkLowerCase(schemaName, "schemaName"); + checkLowerCase(tableName, "tableName"); + } + + public static Optional checkLowerCase(Optional value, String name) + { + if (value.isPresent()) { + checkLowerCase(value.get(), name); + } + return value; + } + + public static String checkLowerCase(String value, String name) + { + checkNotNull(value, "%s is null", name); + checkArgument(value.equals(value.toLowerCase(ENGLISH)), "%s is not lowercase", name); + return value; + } + + public static ColumnMetadata findColumnMetadata(ConnectorTableMetadata tableMetadata, String columnName) + { + for (ColumnMetadata columnMetadata : tableMetadata.getColumns()) { + if (columnName.equals(columnMetadata.getName())) { + return columnMetadata; + } + } + return null; + } + + public static QualifiedTableName createQualifiedTableName(Session session, QualifiedName name) + { + checkNotNull(session, "session is null"); + checkNotNull(name, "name is null"); + checkArgument(name.getParts().size() <= 3, "Too many dots in table name: %s", name); + + List parts = Lists.reverse(name.getParts()); + String tableName = parts.get(0); + String schemaName = (parts.size() > 1) ? parts.get(1) : session.getSchema(); + String catalogName = (parts.size() > 2) ? parts.get(2) : session.getCatalog(); + + return new QualifiedTableName(catalogName, schemaName, tableName); + } + + public static class SchemaMetadataBuilder + { + public static SchemaMetadataBuilder schemaMetadataBuilder() + { + return new SchemaMetadataBuilder(); + } + + private final ImmutableMap.Builder tables = ImmutableMap.builder(); + + public SchemaMetadataBuilder table(ConnectorTableMetadata tableMetadata) + { + tables.put(tableMetadata.getTable(), tableMetadata); + return this; + } + + public ImmutableMap build() + { + return tables.build(); + } + } + + public static class TableMetadataBuilder + { + public static TableMetadataBuilder tableMetadataBuilder(String schemaName, String tableName) + { + return new TableMetadataBuilder(new SchemaTableName(schemaName, tableName)); + } + + public static TableMetadataBuilder tableMetadataBuilder(SchemaTableName tableName) + { + return new TableMetadataBuilder(tableName); + } + + private final SchemaTableName tableName; + private final ImmutableList.Builder columns = ImmutableList.builder(); + + private TableMetadataBuilder(SchemaTableName tableName) + { + this.tableName = tableName; + } + + public TableMetadataBuilder column(String columnName, Type type) + { + columns.add(new ColumnMetadata(columnName, type, false)); + return this; + } + + public TableMetadataBuilder partitionKeyColumn(String columnName, Type type) + { + columns.add(new ColumnMetadata(columnName, type, true)); + return this; + } + + public ConnectorTableMetadata build() + { + return new ConnectorTableMetadata(tableName, columns.build()); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/NodeVersion.java b/presto-main/src/main/java/com/facebook/presto/metadata/NodeVersion.java new file mode 100644 index 00000000..538fc712 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/NodeVersion.java @@ -0,0 +1,56 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +import java.util.Objects; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class NodeVersion +{ + public static final NodeVersion UNKNOWN = new NodeVersion(""); + + private final String version; + + public NodeVersion(String version) + { + this.version = checkNotNull(version, "version is null"); + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + NodeVersion that = (NodeVersion) o; + return Objects.equals(version, that.version); + } + + @Override + public int hashCode() + { + return Objects.hash(version); + } + + @Override + public String toString() + { + return version; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/OperatorNotFoundException.java b/presto-main/src/main/java/com/facebook/presto/metadata/OperatorNotFoundException.java new file mode 100644 index 00000000..099a6619 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/OperatorNotFoundException.java @@ -0,0 +1,63 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.type.Type; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.facebook.presto.spi.StandardErrorCode.OPERATOR_NOT_FOUND; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.lang.String.format; + +public class OperatorNotFoundException extends PrestoException +{ + private final OperatorType operatorType; + private final Type returnType; + private final List argumentTypes; + + public OperatorNotFoundException(OperatorType operatorType, List argumentTypes) + { + super(OPERATOR_NOT_FOUND, format("Operator %s(%s) not registered", operatorType, Joiner.on(", ").join(argumentTypes))); + this.operatorType = checkNotNull(operatorType, "operatorType is null"); + this.returnType = null; + this.argumentTypes = ImmutableList.copyOf(checkNotNull(argumentTypes, "argumentTypes is null")); + } + + public OperatorNotFoundException(OperatorType operatorType, List argumentTypes, Type returnType) + { + super(OPERATOR_NOT_FOUND, format("Operator %s(%s):%s not registered", operatorType, Joiner.on(", ").join(argumentTypes), returnType)); + this.operatorType = checkNotNull(operatorType, "operatorType is null"); + this.argumentTypes = ImmutableList.copyOf(checkNotNull(argumentTypes, "argumentTypes is null")); + this.returnType = checkNotNull(returnType, "returnType is null"); + } + + public OperatorType getOperatorType() + { + return operatorType; + } + + public Type getReturnType() + { + return returnType; + } + + public List getArgumentTypes() + { + return argumentTypes; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/OperatorType.java b/presto-main/src/main/java/com/facebook/presto/metadata/OperatorType.java new file mode 100644 index 00000000..c51813fa --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/OperatorType.java @@ -0,0 +1,212 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.TypeSignature; +import com.facebook.presto.type.UnknownType; +import com.google.common.base.Joiner; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkArgument; + +public enum OperatorType +{ + ADD("+") + { + @Override + void validateSignature(TypeSignature returnType, List argumentTypes) + { + validateOperatorSignature(this, returnType, argumentTypes, 2); + } + }, + + SUBTRACT("-") + { + @Override + void validateSignature(TypeSignature returnType, List argumentTypes) + { + validateOperatorSignature(this, returnType, argumentTypes, 2); + } + }, + + MULTIPLY("*") + { + @Override + void validateSignature(TypeSignature returnType, List argumentTypes) + { + validateOperatorSignature(this, returnType, argumentTypes, 2); + } + }, + + DIVIDE("/") + { + @Override + void validateSignature(TypeSignature returnType, List argumentTypes) + { + validateOperatorSignature(this, returnType, argumentTypes, 2); + } + }, + + MODULUS("%") + { + @Override + void validateSignature(TypeSignature returnType, List argumentTypes) + { + validateOperatorSignature(this, returnType, argumentTypes, 2); + } + }, + + NEGATION("-") + { + @Override + void validateSignature(TypeSignature returnType, List argumentTypes) + { + validateOperatorSignature(this, returnType, argumentTypes, 1); + } + }, + + EQUAL("=") + { + @Override + void validateSignature(TypeSignature returnType, List argumentTypes) + { + validateComparisonOperatorSignature(this, returnType, argumentTypes, 2); + } + }, + + NOT_EQUAL("<>") + { + @Override + void validateSignature(TypeSignature returnType, List argumentTypes) + { + validateComparisonOperatorSignature(this, returnType, argumentTypes, 2); + } + }, + + LESS_THAN("<") + { + @Override + void validateSignature(TypeSignature returnType, List argumentTypes) + { + validateComparisonOperatorSignature(this, returnType, argumentTypes, 2); + } + }, + LESS_THAN_OR_EQUAL("<=") + { + @Override + void validateSignature(TypeSignature returnType, List argumentTypes) + { + validateComparisonOperatorSignature(this, returnType, argumentTypes, 2); + } + }, + + GREATER_THAN(">") + { + @Override + void validateSignature(TypeSignature returnType, List argumentTypes) + { + validateComparisonOperatorSignature(this, returnType, argumentTypes, 2); + } + }, + + GREATER_THAN_OR_EQUAL(">=") + { + @Override + void validateSignature(TypeSignature returnType, List argumentTypes) + { + validateComparisonOperatorSignature(this, returnType, argumentTypes, 2); + } + }, + + BETWEEN("BETWEEN") + { + @Override + void validateSignature(TypeSignature returnType, List argumentTypes) + { + validateComparisonOperatorSignature(this, returnType, argumentTypes, 3); + } + }, + + CAST("CAST") + { + @Override + void validateSignature(TypeSignature returnType, List argumentTypes) + { + validateOperatorSignature(this, returnType, argumentTypes, 1); + } + }, + + SUBSCRIPT("[]") + { + @Override + void validateSignature(TypeSignature returnType, List argumentTypes) + { + validateOperatorSignature(this, returnType, argumentTypes, 2); + checkArgument(argumentTypes.get(0).getBase().equals(StandardTypes.ARRAY) || argumentTypes.get(0).getBase().equals(StandardTypes.MAP), "First argument must be an ARRAY or MAP"); + if (argumentTypes.get(0).getBase().equals(StandardTypes.ARRAY)) { + checkArgument(argumentTypes.get(1).getBase().equals(StandardTypes.BIGINT), "Second argument must be a BIGINT"); + TypeSignature elementType = argumentTypes.get(0).getParameters().get(0); + checkArgument(returnType.equals(elementType), "[] return type does not match ARRAY element type"); + } + else { + TypeSignature valueType = argumentTypes.get(0).getParameters().get(1); + checkArgument(returnType.equals(valueType), "[] return type does not match MAP value type"); + } + } + }, + + HASH_CODE("HASH CODE") + { + @Override + void validateSignature(TypeSignature returnType, List argumentTypes) + { + validateOperatorSignature(this, returnType, argumentTypes, 1); + checkArgument(returnType.getBase().equals(StandardTypes.BIGINT), "%s operator must return a BIGINT: %s", this, formatSignature(this, returnType, argumentTypes)); + } + }; + + private final String operator; + + OperatorType(String operator) + { + this.operator = operator; + } + + public String getOperator() + { + return operator; + } + + abstract void validateSignature(TypeSignature returnType, List argumentTypes); + + private static void validateOperatorSignature(OperatorType operatorType, TypeSignature returnType, List argumentTypes, int expectedArgumentCount) + { + String signature = formatSignature(operatorType, returnType, argumentTypes); + checkArgument(!returnType.getBase().equals(UnknownType.NAME), "%s operator return type can not be NULL: %s", operatorType, signature); + checkArgument(argumentTypes.size() == expectedArgumentCount, "%s operator must have exactly %s argument: %s", operatorType, expectedArgumentCount, signature); + } + + private static void validateComparisonOperatorSignature(OperatorType operatorType, TypeSignature returnType, List argumentTypes, int expectedArgumentCount) + { + validateOperatorSignature(operatorType, returnType, argumentTypes, expectedArgumentCount); + checkArgument(returnType.getBase().equals(StandardTypes.BOOLEAN), "%s operator must return a BOOLEAN: %s", operatorType, formatSignature(operatorType, returnType, argumentTypes)); + } + + private static String formatSignature(OperatorType operatorType, TypeSignature returnType, List argumentTypes) + { + return operatorType + "(" + Joiner.on(", ").join(argumentTypes) + ")::" + returnType; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/OutputTableHandle.java b/presto-main/src/main/java/com/facebook/presto/metadata/OutputTableHandle.java new file mode 100644 index 00000000..99ad2086 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/OutputTableHandle.java @@ -0,0 +1,75 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +import com.facebook.presto.spi.ConnectorOutputTableHandle; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +import static com.google.common.base.Preconditions.checkNotNull; + +public final class OutputTableHandle +{ + private final String connectorId; + private final ConnectorOutputTableHandle connectorHandle; + + @JsonCreator + public OutputTableHandle( + @JsonProperty("connectorId") String connectorId, + @JsonProperty("connectorHandle") ConnectorOutputTableHandle connectorHandle) + { + this.connectorId = checkNotNull(connectorId, "connectorId is null"); + this.connectorHandle = checkNotNull(connectorHandle, "connectorHandle is null"); + } + + @JsonProperty + public String getConnectorId() + { + return connectorId; + } + + @JsonProperty + public ConnectorOutputTableHandle getConnectorHandle() + { + return connectorHandle; + } + + @Override + public int hashCode() + { + return Objects.hash(connectorId, connectorHandle); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + OutputTableHandle other = (OutputTableHandle) obj; + return Objects.equals(this.connectorId, other.connectorId) && + Objects.equals(this.connectorHandle, other.connectorHandle); + } + + @Override + public String toString() + { + return connectorId + ":" + connectorHandle; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/OutputTableHandleJacksonModule.java b/presto-main/src/main/java/com/facebook/presto/metadata/OutputTableHandleJacksonModule.java new file mode 100644 index 00000000..8cbbc925 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/OutputTableHandleJacksonModule.java @@ -0,0 +1,53 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +import com.facebook.presto.spi.ConnectorOutputTableHandle; + +import javax.inject.Inject; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class OutputTableHandleJacksonModule + extends AbstractTypedJacksonModule +{ + @Inject + public OutputTableHandleJacksonModule(HandleResolver handleResolver) + { + super(ConnectorOutputTableHandle.class, "type", new OutputTableHandleJsonTypeIdResolver(handleResolver)); + } + + private static class OutputTableHandleJsonTypeIdResolver + implements JsonTypeIdResolver + { + private final HandleResolver handleResolver; + + private OutputTableHandleJsonTypeIdResolver(HandleResolver handleResolver) + { + this.handleResolver = checkNotNull(handleResolver, "handleResolver is null"); + } + + @Override + public String getId(ConnectorOutputTableHandle tableHandle) + { + return handleResolver.getId(tableHandle); + } + + @Override + public Class getType(String id) + { + return handleResolver.getOutputTableHandleClass(id); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/ParametricAggregation.java b/presto-main/src/main/java/com/facebook/presto/metadata/ParametricAggregation.java new file mode 100644 index 00000000..45f45a9d --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/ParametricAggregation.java @@ -0,0 +1,60 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +public abstract class ParametricAggregation + implements ParametricFunction +{ + @Override + public final boolean isScalar() + { + return false; + } + + @Override + public final boolean isAggregate() + { + return true; + } + + @Override + public boolean isHidden() + { + return false; + } + + @Override + public boolean isApproximate() + { + return false; + } + + @Override + public final boolean isWindow() + { + return false; + } + + @Override + public boolean isDeterministic() + { + return true; + } + + @Override + public final boolean isUnbound() + { + return true; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/ParametricFunction.java b/presto-main/src/main/java/com/facebook/presto/metadata/ParametricFunction.java new file mode 100644 index 00000000..19de0695 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/ParametricFunction.java @@ -0,0 +1,43 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; + +import java.util.Map; + +public interface ParametricFunction +{ + Signature getSignature(); + + boolean isScalar(); + + boolean isAggregate(); + + boolean isHidden(); + + boolean isApproximate(); + + boolean isWindow(); + + boolean isDeterministic(); + + boolean isUnbound(); + + String getDescription(); + + // TODO: This should really return an object with just the MethodHandle/InternalAggregation...etc. However, due to the magic literal hack this is not possible + FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry); +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/ParametricOperator.java b/presto-main/src/main/java/com/facebook/presto/metadata/ParametricOperator.java new file mode 100644 index 00000000..329d400c --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/ParametricOperator.java @@ -0,0 +1,93 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.facebook.presto.metadata.FunctionRegistry.mangleOperatorName; +import static com.google.common.base.Preconditions.checkNotNull; + +public abstract class ParametricOperator + implements ParametricFunction +{ + private final OperatorType operatorType; + private final List typeParameters; + private final String returnType; + private final List argumentTypes; + + protected ParametricOperator(OperatorType operatorType, List typeParameters, String returnType, List argumentTypes) + { + this.typeParameters = ImmutableList.copyOf(checkNotNull(typeParameters, "typeParameters is null")); + this.returnType = checkNotNull(returnType, "returnType is null"); + this.argumentTypes = ImmutableList.copyOf(checkNotNull(argumentTypes, "argumentTypes is null")); + this.operatorType = checkNotNull(operatorType, "operatorType is null"); + } + + @Override + public Signature getSignature() + { + return new Signature(mangleOperatorName(operatorType), typeParameters, returnType, argumentTypes, false, true); + } + + @Override + public final boolean isScalar() + { + return true; + } + + @Override + public final boolean isAggregate() + { + return false; + } + + @Override + public final boolean isHidden() + { + return true; + } + + @Override + public final boolean isApproximate() + { + return false; + } + + @Override + public final boolean isWindow() + { + return false; + } + + @Override + public final boolean isDeterministic() + { + return true; + } + + @Override + public final boolean isUnbound() + { + return true; + } + + @Override + public final String getDescription() + { + // Operators are internal, and don't need a description + return null; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/ParametricScalar.java b/presto-main/src/main/java/com/facebook/presto/metadata/ParametricScalar.java new file mode 100644 index 00000000..469c5745 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/ParametricScalar.java @@ -0,0 +1,48 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +public abstract class ParametricScalar + implements ParametricFunction +{ + @Override + public final boolean isScalar() + { + return true; + } + + @Override + public final boolean isAggregate() + { + return false; + } + + @Override + public final boolean isApproximate() + { + return false; + } + + @Override + public final boolean isWindow() + { + return false; + } + + @Override + public final boolean isUnbound() + { + return true; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/PrestoNode.java b/presto-main/src/main/java/com/facebook/presto/metadata/PrestoNode.java new file mode 100644 index 00000000..31c94573 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/PrestoNode.java @@ -0,0 +1,95 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +import com.facebook.presto.spi.HostAddress; +import com.facebook.presto.spi.Node; + +import java.net.URI; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Strings.emptyToNull; +import static com.google.common.base.Strings.nullToEmpty; + +/** + * A node is a server in a cluster than can process queries. + */ +public class PrestoNode + implements Node +{ + private final String nodeIdentifier; + private final URI httpUri; + private final NodeVersion nodeVersion; + + public PrestoNode(String nodeIdentifier, URI httpUri, NodeVersion nodeVersion) + { + nodeIdentifier = emptyToNull(nullToEmpty(nodeIdentifier).trim()); + this.nodeIdentifier = checkNotNull(nodeIdentifier, "nodeIdentifier is null or empty"); + this.httpUri = checkNotNull(httpUri, "httpUri is null"); + this.nodeVersion = checkNotNull(nodeVersion, "nodeVersion is null"); + } + + @Override + public String getNodeIdentifier() + { + return nodeIdentifier; + } + + @Override + public URI getHttpUri() + { + return httpUri; + } + + @Override + public HostAddress getHostAndPort() + { + return HostAddress.fromUri(httpUri); + } + + public NodeVersion getNodeVersion() + { + return nodeVersion; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if ((obj == null) || (getClass() != obj.getClass())) { + return false; + } + PrestoNode o = (PrestoNode) obj; + return nodeIdentifier.equals(o.nodeIdentifier); + } + + @Override + public int hashCode() + { + return nodeIdentifier.hashCode(); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("nodeIdentifier", nodeIdentifier) + .add("httpUri", httpUri) + .add("nodeVersion", nodeVersion) + .toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/QualifiedTableName.java b/presto-main/src/main/java/com/facebook/presto/metadata/QualifiedTableName.java new file mode 100644 index 00000000..c8d84db2 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/QualifiedTableName.java @@ -0,0 +1,109 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +import com.facebook.presto.spi.SchemaTableName; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import com.google.common.base.Function; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; + +import javax.annotation.concurrent.Immutable; + +import java.util.Objects; + +import static com.facebook.presto.metadata.MetadataUtil.checkTableName; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Immutable +public class QualifiedTableName +{ + @JsonCreator + public static QualifiedTableName valueOf(String tableName) + { + checkNotNull(tableName, "tableName is null"); + + ImmutableList ids = ImmutableList.copyOf(Splitter.on('.').split(tableName)); + checkArgument(ids.size() == 3, "Invalid tableName %s", tableName); + + return new QualifiedTableName(ids.get(0), ids.get(1), ids.get(2)); + } + + private final String catalogName; + private final String schemaName; + private final String tableName; + + public QualifiedTableName(String catalogName, String schemaName, String tableName) + { + checkTableName(catalogName, schemaName, tableName); + this.catalogName = catalogName; + this.schemaName = schemaName; + this.tableName = tableName; + } + + public String getCatalogName() + { + return catalogName; + } + + public String getSchemaName() + { + return schemaName; + } + + public String getTableName() + { + return tableName; + } + + public SchemaTableName asSchemaTableName() + { + return new SchemaTableName(schemaName, tableName); + } + + @Override + public boolean equals(Object obj) + { + if (obj == this) { + return true; + } + if ((obj == null) || (getClass() != obj.getClass())) { + return false; + } + QualifiedTableName o = (QualifiedTableName) obj; + return Objects.equals(catalogName, o.catalogName) && + Objects.equals(schemaName, o.schemaName) && + Objects.equals(tableName, o.tableName); + } + + @Override + public int hashCode() + { + return Objects.hash(catalogName, schemaName, tableName); + } + + @JsonValue + @Override + public String toString() + { + return catalogName + '.' + schemaName + '.' + tableName; + } + + public static Function convertFromSchemaTableName(final String catalogName) + { + return input -> new QualifiedTableName(catalogName, input.getSchemaName(), input.getTableName()); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/QualifiedTablePrefix.java b/presto-main/src/main/java/com/facebook/presto/metadata/QualifiedTablePrefix.java new file mode 100644 index 00000000..8b67e204 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/QualifiedTablePrefix.java @@ -0,0 +1,127 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +import com.facebook.presto.spi.SchemaTablePrefix; + +import javax.annotation.concurrent.Immutable; + +import java.util.Objects; +import java.util.Optional; + +import static com.facebook.presto.metadata.MetadataUtil.checkCatalogName; +import static com.facebook.presto.metadata.MetadataUtil.checkSchemaName; +import static com.facebook.presto.metadata.MetadataUtil.checkTableName; + +@Immutable +public class QualifiedTablePrefix +{ + private final String catalogName; + private final Optional schemaName; + private final Optional tableName; + + public QualifiedTablePrefix(String catalogName) + { + this.catalogName = checkCatalogName(catalogName); + this.schemaName = Optional.empty(); + this.tableName = Optional.empty(); + } + + public QualifiedTablePrefix(String catalogName, String schemaName) + { + this.catalogName = checkCatalogName(catalogName); + this.schemaName = Optional.of(checkSchemaName(schemaName)); + this.tableName = Optional.empty(); + } + + public QualifiedTablePrefix(String catalogName, String schemaName, String tableName) + { + this.catalogName = checkCatalogName(catalogName); + this.schemaName = Optional.of(checkSchemaName(schemaName)); + this.tableName = Optional.of(checkTableName(tableName)); + } + + public QualifiedTablePrefix(String catalogName, Optional schemaName, Optional tableName) + { + checkTableName(catalogName, schemaName, tableName); + this.catalogName = catalogName; + this.schemaName = schemaName; + this.tableName = tableName; + } + + public String getCatalogName() + { + return catalogName; + } + + public Optional getSchemaName() + { + return schemaName; + } + + public Optional getTableName() + { + return tableName; + } + + public boolean hasSchemaName() + { + return schemaName.isPresent(); + } + + public boolean hasTableName() + { + return tableName.isPresent(); + } + + public SchemaTablePrefix asSchemaTablePrefix() + { + if (!schemaName.isPresent()) { + return new SchemaTablePrefix(); + } + else if (!tableName.isPresent()) { + return new SchemaTablePrefix(schemaName.get()); + } + else { + return new SchemaTablePrefix(schemaName.get(), tableName.get()); + } + } + + @Override + public boolean equals(Object obj) + { + if (obj == this) { + return true; + } + if ((obj == null) || (getClass() != obj.getClass())) { + return false; + } + QualifiedTablePrefix o = (QualifiedTablePrefix) obj; + return Objects.equals(catalogName, o.catalogName) && + Objects.equals(schemaName, o.schemaName) && + Objects.equals(tableName, o.tableName); + } + + @Override + public int hashCode() + { + return Objects.hash(catalogName, schemaName, tableName); + } + + @Override + public String toString() + { + return catalogName + '.' + schemaName.orElse("*") + '.' + tableName.orElse("*"); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/RemoteSplitHandleResolver.java b/presto-main/src/main/java/com/facebook/presto/metadata/RemoteSplitHandleResolver.java new file mode 100644 index 00000000..f61dca5c --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/RemoteSplitHandleResolver.java @@ -0,0 +1,60 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorHandleResolver; +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.ConnectorTableHandle; +import com.facebook.presto.split.RemoteSplit; + +public class RemoteSplitHandleResolver + implements ConnectorHandleResolver +{ + @Override + public boolean canHandle(ConnectorTableHandle tableHandle) + { + return false; + } + + @Override + public boolean canHandle(ColumnHandle columnHandle) + { + return false; + } + + @Override + public boolean canHandle(ConnectorSplit split) + { + return split instanceof RemoteSplit; + } + + @Override + public Class getTableHandleClass() + { + throw new UnsupportedOperationException(); + } + + @Override + public Class getColumnHandleClass() + { + throw new UnsupportedOperationException(); + } + + @Override + public Class getSplitClass() + { + return RemoteSplit.class; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/ResolvedIndex.java b/presto-main/src/main/java/com/facebook/presto/metadata/ResolvedIndex.java new file mode 100644 index 00000000..d7a5ee22 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/ResolvedIndex.java @@ -0,0 +1,44 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorResolvedIndex; +import com.facebook.presto.spi.TupleDomain; +import com.google.common.base.Preconditions; + +public final class ResolvedIndex +{ + private final IndexHandle indexHandle; + private final TupleDomain undeterminedTupleDomain; + + public ResolvedIndex(String connectorId, ConnectorResolvedIndex index) + { + Preconditions.checkNotNull(connectorId, "connectorId is null"); + Preconditions.checkNotNull(index, "index is null"); + + indexHandle = new IndexHandle(connectorId, index.getIndexHandle()); + undeterminedTupleDomain = index.getUnresolvedTupleDomain(); + } + + public IndexHandle getIndexHandle() + { + return indexHandle; + } + + public TupleDomain getUnresolvedTupleDomain() + { + return undeterminedTupleDomain; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/Signature.java b/presto-main/src/main/java/com/facebook/presto/metadata/Signature.java new file mode 100644 index 00000000..98b988e5 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/Signature.java @@ -0,0 +1,373 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.spi.type.TypeSignature; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; + +import javax.annotation.Nullable; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +import static com.facebook.presto.metadata.FunctionRegistry.canCoerce; +import static com.facebook.presto.metadata.FunctionRegistry.getCommonSuperType; +import static com.facebook.presto.metadata.FunctionRegistry.mangleOperatorName; +import static com.facebook.presto.spi.type.TypeSignature.parseTypeSignature; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +public final class Signature +{ + private final String name; + private final List typeParameters; + private final TypeSignature returnType; + private final List argumentTypes; + private final boolean variableArity; + private final boolean internal; + + @JsonCreator + public Signature( + @JsonProperty("name") String name, + @JsonProperty("typeParameters") List typeParameters, + @JsonProperty("returnType") TypeSignature returnType, + @JsonProperty("argumentTypes") List argumentTypes, + @JsonProperty("variableArity") boolean variableArity, + @JsonProperty("internal") boolean internal) + { + checkNotNull(name, "name is null"); + checkNotNull(typeParameters, "typeParameters is null"); + + this.name = name; + this.typeParameters = ImmutableList.copyOf(typeParameters); + this.returnType = checkNotNull(returnType, "returnType is null"); + this.argumentTypes = ImmutableList.copyOf(checkNotNull(argumentTypes, "argumentTypes is null")); + this.variableArity = variableArity; + this.internal = internal; + } + + public Signature(String name, List typeParameters, String returnType, List argumentTypes, boolean variableArity, boolean internal) + { + this(name, typeParameters, parseTypeSignature(returnType), Lists.transform(argumentTypes, TypeSignature::parseTypeSignature), variableArity, internal); + } + + public Signature(String name, String returnType, List argumentTypes) + { + this(name, ImmutableList.of(), parseTypeSignature(returnType), Lists.transform(argumentTypes, TypeSignature::parseTypeSignature), false, false); + } + + public Signature(String name, String returnType, String... argumentTypes) + { + this(name, returnType, ImmutableList.copyOf(argumentTypes)); + } + + public Signature(String name, TypeSignature returnType, List argumentTypes) + { + this(name, ImmutableList.of(), returnType, argumentTypes, false, false); + } + + public Signature(String name, TypeSignature returnType, TypeSignature... argumentTypes) + { + this(name, returnType, ImmutableList.copyOf(argumentTypes)); + } + + public static Signature internalOperator(String name, TypeSignature returnType, List argumentTypes) + { + return internalFunction(mangleOperatorName(name), returnType, argumentTypes); + } + + public static Signature internalOperator(String name, TypeSignature returnType, TypeSignature... argumentTypes) + { + return internalFunction(mangleOperatorName(name), returnType, ImmutableList.copyOf(argumentTypes)); + } + + public static Signature internalFunction(String name, String returnType, String... argumentTypes) + { + return internalFunction(name, returnType, ImmutableList.copyOf(argumentTypes)); + } + + public static Signature internalFunction(String name, String returnType, List argumentTypes) + { + return new Signature(name, ImmutableList.of(), returnType, argumentTypes, false, true); + } + + public static Signature internalFunction(String name, TypeSignature returnType, TypeSignature... argumentTypes) + { + return internalFunction(name, returnType, ImmutableList.copyOf(argumentTypes)); + } + + public static Signature internalFunction(String name, TypeSignature returnType, List argumentTypes) + { + return new Signature(name, ImmutableList.of(), returnType, argumentTypes, false, true); + } + + @JsonProperty + public String getName() + { + return name; + } + + @JsonProperty + public TypeSignature getReturnType() + { + return returnType; + } + + @JsonProperty + public List getArgumentTypes() + { + return argumentTypes; + } + + @JsonProperty + public boolean isInternal() + { + return internal; + } + + @JsonProperty + public boolean isVariableArity() + { + return variableArity; + } + + @JsonProperty + public List getTypeParameters() + { + return typeParameters; + } + + @Override + public int hashCode() + { + return Objects.hash(name, typeParameters, returnType, argumentTypes, variableArity, internal); + } + + Signature withAlias(String name) + { + return new Signature(name, typeParameters, getReturnType(), getArgumentTypes(), variableArity, internal); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + Signature other = (Signature) obj; + return Objects.equals(this.name, other.name) && + Objects.equals(this.typeParameters, other.typeParameters) && + Objects.equals(this.returnType, other.returnType) && + Objects.equals(this.argumentTypes, other.argumentTypes) && + Objects.equals(this.variableArity, other.variableArity) && + Objects.equals(this.internal, other.internal); + } + + @Override + public String toString() + { + return (internal ? "%" : "") + name + (typeParameters.isEmpty() ? "" : "<" + Joiner.on(",").join(typeParameters) + ">") + "(" + Joiner.on(",").join(argumentTypes) + "):" + returnType; + } + + @Nullable + public Map bindTypeParameters(Type returnType, List types, boolean allowCoercion, TypeManager typeManager) + { + Map boundParameters = new HashMap<>(); + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (TypeParameter parameter : typeParameters) { + builder.put(parameter.getName(), parameter); + } + + ImmutableMap parameters = builder.build(); + if (!matchAndBind(boundParameters, parameters, this.returnType, returnType, allowCoercion, typeManager)) { + return null; + } + + if (!matchArguments(boundParameters, parameters, argumentTypes, types, allowCoercion, variableArity, typeManager)) { + return null; + } + + checkState(boundParameters.keySet().equals(parameters.keySet()), + "%s matched arguments %s, but type parameters %s are still unbound", + this, + types, + Sets.difference(parameters.keySet(), boundParameters.keySet())); + + return boundParameters; + } + + @Nullable + public Map bindTypeParameters(List types, boolean allowCoercion, TypeManager typeManager) + { + Map boundParameters = new HashMap<>(); + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (TypeParameter parameter : typeParameters) { + builder.put(parameter.getName(), parameter); + } + + ImmutableMap parameters = builder.build(); + if (!matchArguments(boundParameters, parameters, argumentTypes, types, allowCoercion, variableArity, typeManager)) { + return null; + } + + checkState(boundParameters.keySet().equals(parameters.keySet()), "%s matched arguments %s, but type parameters %s are still unbound", this, types, Sets.difference(parameters.keySet(), boundParameters.keySet())); + + return boundParameters; + } + + private static boolean matchArguments( + Map boundParameters, + Map parameters, + List argumentTypes, + List types, + boolean allowCoercion, + boolean varArgs, + TypeManager typeManager) + { + if (varArgs) { + if (types.size() < argumentTypes.size() - 1) { + return false; + } + } + else { + if (argumentTypes.size() != types.size()) { + return false; + } + } + + // Bind the variable arity argument first, to make sure it's bound to the common super type + if (varArgs && types.size() >= argumentTypes.size()) { + Optional superType = getCommonSuperType(types.subList(argumentTypes.size() - 1, types.size())); + if (!superType.isPresent()) { + return false; + } + if (!matchAndBind(boundParameters, parameters, argumentTypes.get(argumentTypes.size() - 1), superType.get(), allowCoercion, typeManager)) { + return false; + } + } + + for (int i = 0; i < types.size(); i++) { + // Get the current argument signature, or the last one, if this is a varargs function + TypeSignature typeSignature = argumentTypes.get(Math.min(i, argumentTypes.size() - 1)); + Type type = types.get(i); + if (!matchAndBind(boundParameters, parameters, typeSignature, type, allowCoercion, typeManager)) { + return false; + } + } + + return true; + } + + private static boolean matchAndBind(Map boundParameters, Map typeParameters, TypeSignature parameter, Type type, boolean allowCoercion, TypeManager typeManager) + { + // If this parameter is already bound, then match (with coercion) + if (boundParameters.containsKey(parameter.getBase())) { + checkArgument(parameter.getParameters().isEmpty(), "Unexpected parameteric type"); + if (allowCoercion) { + if (canCoerce(type, boundParameters.get(parameter.getBase()))) { + return true; + } + else if (canCoerce(boundParameters.get(parameter.getBase()), type) && typeParameters.get(parameter.getBase()).canBind(type)) { + // Broaden the binding + boundParameters.put(parameter.getBase(), type); + return true; + } + return false; + } + else { + return type.equals(boundParameters.get(parameter.getBase())); + } + } + + // Recurse into component types + if (!parameter.getParameters().isEmpty()) { + if (type.getTypeParameters().size() != parameter.getParameters().size()) { + return false; + } + for (int i = 0; i < parameter.getParameters().size(); i++) { + Type componentType = type.getTypeParameters().get(i); + TypeSignature componentSignature = parameter.getParameters().get(i); + if (!matchAndBind(boundParameters, typeParameters, componentSignature, componentType, allowCoercion, typeManager)) { + return false; + } + } + } + + // Bind parameter, if this is a free type parameter + if (typeParameters.containsKey(parameter.getBase())) { + TypeParameter typeParameter = typeParameters.get(parameter.getBase()); + if (!typeParameter.canBind(type)) { + return false; + } + boundParameters.put(parameter.getBase(), type); + return true; + } + + // We've already checked all the components, so just match the base type + if (!parameter.getParameters().isEmpty()) { + return type.getTypeSignature().getBase().equals(parameter.getBase()); + } + + // The parameter is not a type parameter, so it must be a concrete type + if (allowCoercion) { + return canCoerce(type, typeManager.getType(parseTypeSignature(parameter.getBase()))); + } + else { + return type.equals(typeManager.getType(parseTypeSignature(parameter.getBase()))); + } + } + + /* + * similar to T extends MyClass, if Java supported varargs wildcards + */ + public static TypeParameter withVariadicBound(String name, String variadicBound) + { + return new TypeParameter(name, false, false, variadicBound); + } + + public static TypeParameter comparableWithVariadicBound(String name, String variadicBound) + { + return new TypeParameter(name, true, false, variadicBound); + } + + public static TypeParameter typeParameter(String name) + { + return new TypeParameter(name, false, false, null); + } + + public static TypeParameter comparableTypeParameter(String name) + { + return new TypeParameter(name, true, false, null); + } + + public static TypeParameter orderableTypeParameter(String name) + { + return new TypeParameter(name, false, true, null); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/SpecializedFunctionKey.java b/presto-main/src/main/java/com/facebook/presto/metadata/SpecializedFunctionKey.java new file mode 100644 index 00000000..2ef60e91 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/SpecializedFunctionKey.java @@ -0,0 +1,73 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +import com.facebook.presto.spi.type.Type; + +import java.util.Map; +import java.util.Objects; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class SpecializedFunctionKey +{ + private final ParametricFunction function; + private final Map boundTypeParameters; + private final int arity; + + public SpecializedFunctionKey(ParametricFunction function, Map boundTypeParameters, int arity) + { + this.function = checkNotNull(function, "function is null"); + this.boundTypeParameters = checkNotNull(boundTypeParameters, "boundTypeParameters is null"); + this.arity = arity; + } + + public ParametricFunction getFunction() + { + return function; + } + + public Map getBoundTypeParameters() + { + return boundTypeParameters; + } + + public int getArity() + { + return arity; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + SpecializedFunctionKey that = (SpecializedFunctionKey) o; + + return Objects.equals(arity, that.arity) && + Objects.equals(boundTypeParameters, that.boundTypeParameters) && + Objects.equals(function.getSignature(), that.function.getSignature()); + } + + @Override + public int hashCode() + { + return Objects.hash(function.getSignature(), boundTypeParameters, arity); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/Split.java b/presto-main/src/main/java/com/facebook/presto/metadata/Split.java new file mode 100644 index 00000000..273a88bf --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/Split.java @@ -0,0 +1,75 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.HostAddress; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkNotNull; + +public final class Split +{ + private final String connectorId; + private final ConnectorSplit connectorSplit; + + @JsonCreator + public Split( + @JsonProperty("connectorId") String connectorId, + @JsonProperty("connectorSplit") ConnectorSplit connectorSplit) + { + this.connectorId = checkNotNull(connectorId, "connectorId is null"); + this.connectorSplit = checkNotNull(connectorSplit, "connectorSplit is null"); + } + + @JsonProperty + public String getConnectorId() + { + return connectorId; + } + + @JsonProperty + public ConnectorSplit getConnectorSplit() + { + return connectorSplit; + } + + public Object getInfo() + { + return connectorSplit.getInfo(); + } + + public List getAddresses() + { + return connectorSplit.getAddresses(); + } + + public boolean isRemotelyAccessible() + { + return connectorSplit.isRemotelyAccessible(); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("connectorId", connectorId) + .add("connectorSplit", connectorSplit) + .toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/SplitJacksonModule.java b/presto-main/src/main/java/com/facebook/presto/metadata/SplitJacksonModule.java new file mode 100644 index 00000000..e3778c34 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/SplitJacksonModule.java @@ -0,0 +1,53 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +import com.facebook.presto.spi.ConnectorSplit; + +import javax.inject.Inject; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class SplitJacksonModule + extends AbstractTypedJacksonModule +{ + @Inject + public SplitJacksonModule(HandleResolver handleResolver) + { + super(ConnectorSplit.class, "type", new SplitJsonTypeIdResolver(handleResolver)); + } + + private static class SplitJsonTypeIdResolver + implements JsonTypeIdResolver + { + private final HandleResolver handleResolver; + + private SplitJsonTypeIdResolver(HandleResolver handleResolver) + { + this.handleResolver = checkNotNull(handleResolver, "handleResolver is null"); + } + + @Override + public String getId(ConnectorSplit split) + { + return handleResolver.getId(split); + } + + @Override + public Class getType(String id) + { + return handleResolver.getSplitClass(id); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/TableHandle.java b/presto-main/src/main/java/com/facebook/presto/metadata/TableHandle.java new file mode 100644 index 00000000..8347d818 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/TableHandle.java @@ -0,0 +1,75 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +import com.facebook.presto.spi.ConnectorTableHandle; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +import static com.google.common.base.Preconditions.checkNotNull; + +public final class TableHandle +{ + private final String connectorId; + private final ConnectorTableHandle connectorHandle; + + @JsonCreator + public TableHandle( + @JsonProperty("connectorId") String connectorId, + @JsonProperty("connectorHandle") ConnectorTableHandle connectorHandle) + { + this.connectorId = checkNotNull(connectorId, "connectorId is null"); + this.connectorHandle = checkNotNull(connectorHandle, "connectorHandle is null"); + } + + @JsonProperty + public String getConnectorId() + { + return connectorId; + } + + @JsonProperty + public ConnectorTableHandle getConnectorHandle() + { + return connectorHandle; + } + + @Override + public int hashCode() + { + return Objects.hash(connectorId, connectorHandle); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final TableHandle other = (TableHandle) obj; + return Objects.equals(this.connectorId, other.connectorId) && + Objects.equals(this.connectorHandle, other.connectorHandle); + } + + @Override + public String toString() + { + return connectorId + ":" + connectorHandle; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/TableHandleJacksonModule.java b/presto-main/src/main/java/com/facebook/presto/metadata/TableHandleJacksonModule.java new file mode 100644 index 00000000..42d3ede8 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/TableHandleJacksonModule.java @@ -0,0 +1,53 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +import com.facebook.presto.spi.ConnectorTableHandle; + +import javax.inject.Inject; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class TableHandleJacksonModule + extends AbstractTypedJacksonModule +{ + @Inject + public TableHandleJacksonModule(HandleResolver handleResolver) + { + super(ConnectorTableHandle.class, "type", new TableHandleJsonTypeIdResolver(handleResolver)); + } + + private static class TableHandleJsonTypeIdResolver + implements JsonTypeIdResolver + { + private final HandleResolver handleResolver; + + private TableHandleJsonTypeIdResolver(HandleResolver handleResolver) + { + this.handleResolver = checkNotNull(handleResolver, "handleResolver is null"); + } + + @Override + public String getId(ConnectorTableHandle tableHandle) + { + return handleResolver.getId(tableHandle); + } + + @Override + public Class getType(String id) + { + return handleResolver.getTableHandleClass(id); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/TableLayout.java b/presto-main/src/main/java/com/facebook/presto/metadata/TableLayout.java new file mode 100644 index 00000000..14ada21a --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/TableLayout.java @@ -0,0 +1,75 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorTableLayout; +import com.facebook.presto.spi.LocalProperty; +import com.facebook.presto.spi.TupleDomain; + +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class TableLayout +{ + private final TableLayoutHandle handle; + private final ConnectorTableLayout layout; + + public TableLayout(TableLayoutHandle handle, ConnectorTableLayout layout) + { + checkNotNull(handle, "handle is null"); + checkNotNull(layout, "layout is null"); + + this.handle = handle; + this.layout = layout; + } + + public Optional> getColumns() + { + return layout.getColumns(); + } + + public TupleDomain getPredicate() + { + return layout.getPredicate(); + } + + public List> getLocalProperties() + { + return layout.getLocalProperties(); + } + + public TableLayoutHandle getHandle() + { + return handle; + } + + public Optional> getPartitioningColumns() + { + return layout.getPartitioningColumns(); + } + + public Optional>> getDiscretePredicates() + { + return layout.getDiscretePredicates(); + } + + public static TableLayout fromConnectorLayout(String connectorId, ConnectorTableLayout layout) + { + return new TableLayout(new TableLayoutHandle(connectorId, layout.getHandle()), layout); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/TableLayoutHandle.java b/presto-main/src/main/java/com/facebook/presto/metadata/TableLayoutHandle.java new file mode 100644 index 00000000..43d07774 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/TableLayoutHandle.java @@ -0,0 +1,72 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +import com.facebook.presto.spi.ConnectorTableLayoutHandle; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +import static com.google.common.base.Preconditions.checkNotNull; + +public final class TableLayoutHandle +{ + private final String connectorId; + private final ConnectorTableLayoutHandle layout; + + @JsonCreator + public TableLayoutHandle( + @JsonProperty("connectorId") String connectorId, + @JsonProperty("connectorHandle") ConnectorTableLayoutHandle layout) + { + checkNotNull(connectorId, "connectorId is null"); + checkNotNull(layout, "layout is null"); + + this.connectorId = connectorId; + this.layout = layout; + } + + @JsonProperty + public String getConnectorId() + { + return connectorId; + } + + @JsonProperty + public ConnectorTableLayoutHandle getConnectorHandle() + { + return layout; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + TableLayoutHandle that = (TableLayoutHandle) o; + return Objects.equals(connectorId, that.connectorId) && + Objects.equals(layout, that.layout); + } + + @Override + public int hashCode() + { + return Objects.hash(connectorId, layout); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/TableLayoutHandleJacksonModule.java b/presto-main/src/main/java/com/facebook/presto/metadata/TableLayoutHandleJacksonModule.java new file mode 100644 index 00000000..5d652a58 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/TableLayoutHandleJacksonModule.java @@ -0,0 +1,53 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +import com.facebook.presto.spi.ConnectorTableLayoutHandle; + +import javax.inject.Inject; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class TableLayoutHandleJacksonModule + extends AbstractTypedJacksonModule +{ + @Inject + public TableLayoutHandleJacksonModule(HandleResolver handleResolver) + { + super(ConnectorTableLayoutHandle.class, "type", new TableLayoutHandleJsonTypeIdResolver(handleResolver)); + } + + private static class TableLayoutHandleJsonTypeIdResolver + implements JsonTypeIdResolver + { + private final HandleResolver handleResolver; + + private TableLayoutHandleJsonTypeIdResolver(HandleResolver handleResolver) + { + this.handleResolver = checkNotNull(handleResolver, "handleResolver is null"); + } + + @Override + public String getId(ConnectorTableLayoutHandle handle) + { + return handleResolver.getId(handle); + } + + @Override + public Class getType(String id) + { + return handleResolver.getTableLayoutHandleClass(id); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/TableLayoutResult.java b/presto-main/src/main/java/com/facebook/presto/metadata/TableLayoutResult.java new file mode 100644 index 00000000..77ce1a1c --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/TableLayoutResult.java @@ -0,0 +1,39 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.TupleDomain; + +public class TableLayoutResult +{ + private final TableLayout layout; + private final TupleDomain unenforcedConstraint; + + public TableLayoutResult(TableLayout layout, TupleDomain unenforcedConstraint) + { + this.layout = layout; + this.unenforcedConstraint = unenforcedConstraint; + } + + public TableLayout getLayout() + { + return layout; + } + + public TupleDomain getUnenforcedConstraint() + { + return unenforcedConstraint; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/TableMetadata.java b/presto-main/src/main/java/com/facebook/presto/metadata/TableMetadata.java new file mode 100644 index 00000000..f5689cb9 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/TableMetadata.java @@ -0,0 +1,56 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +import com.facebook.presto.spi.ColumnMetadata; +import com.facebook.presto.spi.ConnectorTableMetadata; +import com.facebook.presto.spi.SchemaTableName; +import com.google.common.base.Preconditions; + +import java.util.List; + +public class TableMetadata +{ + private final String connectorId; + private final ConnectorTableMetadata metadata; + + public TableMetadata(String connectorId, ConnectorTableMetadata metadata) + { + Preconditions.checkNotNull(connectorId, "catalog is null"); + Preconditions.checkNotNull(metadata, "metadata is null"); + + this.connectorId = connectorId; + this.metadata = metadata; + } + + public String getConnectorId() + { + return connectorId; + } + + public ConnectorTableMetadata getMetadata() + { + return metadata; + } + + public SchemaTableName getTable() + { + return metadata.getTable(); + } + + public List getColumns() + { + return metadata.getColumns(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/TypeParameter.java b/presto-main/src/main/java/com/facebook/presto/metadata/TypeParameter.java new file mode 100644 index 00000000..fc530ff0 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/TypeParameter.java @@ -0,0 +1,123 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +import com.facebook.presto.spi.type.Type; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.Nullable; + +import java.util.Objects; + +import static com.google.common.base.Preconditions.checkNotNull; + +public final class TypeParameter +{ + private final String name; + private final boolean comparableRequired; + private final boolean orderableRequired; + private final String variadicBound; + + @JsonCreator + public TypeParameter( + @JsonProperty("name") String name, + @JsonProperty("comparableRequired") boolean comparableRequired, + @JsonProperty("orderableRequired") boolean orderableRequired, + @JsonProperty("variadicBound") @Nullable String variadicBound) + { + this.name = checkNotNull(name, "name is null"); + this.comparableRequired = comparableRequired; + this.orderableRequired = orderableRequired; + this.variadicBound = variadicBound; + } + + @JsonProperty + public String getName() + { + return name; + } + + @JsonProperty + public boolean isComparableRequired() + { + return comparableRequired; + } + + @JsonProperty + public boolean isOrderableRequired() + { + return orderableRequired; + } + + @JsonProperty + public String getVariadicBound() + { + return variadicBound; + } + + public boolean canBind(Type type) + { + if (comparableRequired && !type.isComparable()) { + return false; + } + if (orderableRequired && !type.isOrderable()) { + return false; + } + if (variadicBound != null && !type.getTypeSignature().getBase().equals(variadicBound)) { + return false; + } + return true; + } + + @Override + public String toString() + { + String value = name; + if (comparableRequired) { + value += ":comparable"; + } + if (orderableRequired) { + value += ":orderable"; + } + if (variadicBound != null) { + value += ":" + variadicBound + "<*>"; + } + return value; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + TypeParameter other = (TypeParameter) o; + + return Objects.equals(this.name, other.name) && + Objects.equals(this.comparableRequired, other.comparableRequired) && + Objects.equals(this.orderableRequired, other.orderableRequired) && + Objects.equals(this.variadicBound, other.variadicBound); + } + + @Override + public int hashCode() + { + return Objects.hash(name, comparableRequired, orderableRequired, variadicBound); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/metadata/ViewDefinition.java b/presto-main/src/main/java/com/facebook/presto/metadata/ViewDefinition.java new file mode 100644 index 00000000..c11d1d3c --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/metadata/ViewDefinition.java @@ -0,0 +1,113 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.metadata; + +import com.facebook.presto.spi.type.Type; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkNotNull; + +public final class ViewDefinition +{ + private final String originalSql; + private final String catalog; + private final String schema; + private final List columns; + + @JsonCreator + public ViewDefinition( + @JsonProperty("originalSql") String originalSql, + @JsonProperty("catalog") String catalog, + @JsonProperty("schema") String schema, + @JsonProperty("columns") List columns) + { + this.originalSql = checkNotNull(originalSql, "originalSql is null"); + this.catalog = checkNotNull(catalog, "catalog is null"); + this.schema = checkNotNull(schema, "schema is null"); + this.columns = ImmutableList.copyOf(checkNotNull(columns, "columns is null")); + } + + @JsonProperty + public String getOriginalSql() + { + return originalSql; + } + + @JsonProperty + public String getCatalog() + { + return catalog; + } + + @JsonProperty + public String getSchema() + { + return schema; + } + + @JsonProperty + public List getColumns() + { + return columns; + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("originalSql", originalSql) + .add("catalog", catalog) + .add("schema", schema) + .add("columns", columns) + .toString(); + } + + public static final class ViewColumn + { + private final String name; + private final Type type; + + @JsonCreator + public ViewColumn( + @JsonProperty("name") String name, + @JsonProperty("type") Type type) + { + this.name = checkNotNull(name, "name is null"); + this.type = checkNotNull(type, "type is null"); + } + + @JsonProperty + public String getName() + { + return name; + } + + @JsonProperty + public Type getType() + { + return type; + } + + @Override + public String toString() + { + return name + ":" + type; + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/AggregationOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/AggregationOperator.java new file mode 100644 index 00000000..b91025bd --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/AggregationOperator.java @@ -0,0 +1,240 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.operator.aggregation.Accumulator; +import com.facebook.presto.operator.aggregation.AccumulatorFactory; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.planner.plan.AggregationNode.Step; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.facebook.presto.util.ImmutableCollectors.toImmutableList; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +/** + * Group input data and produce a single block for each sequence of identical values. + */ +public class AggregationOperator + implements Operator +{ + public static class AggregationOperatorFactory + implements OperatorFactory + { + private final int operatorId; + private final Step step; + private final List accumulatorFactories; + private final List types; + private boolean closed; + + public AggregationOperatorFactory(int operatorId, Step step, List accumulatorFactories) + { + this.operatorId = operatorId; + this.step = step; + this.accumulatorFactories = ImmutableList.copyOf(accumulatorFactories); + this.types = toTypes(step, accumulatorFactories); + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public Operator createOperator(DriverContext driverContext) + { + checkState(!closed, "Factory is already closed"); + OperatorContext operatorContext = driverContext.addOperatorContext(operatorId, AggregationOperator.class.getSimpleName()); + return new AggregationOperator(operatorContext, step, accumulatorFactories); + } + + @Override + public void close() + { + closed = true; + } + } + + private enum State + { + NEEDS_INPUT, + HAS_OUTPUT, + FINISHED + } + + private final OperatorContext operatorContext; + private final List types; + private final List aggregates; + + private State state = State.NEEDS_INPUT; + + public AggregationOperator(OperatorContext operatorContext, Step step, List accumulatorFactories) + { + this.operatorContext = checkNotNull(operatorContext, "operatorContext is null"); + + checkNotNull(step, "step is null"); + checkNotNull(accumulatorFactories, "accumulatorFactories is null"); + + this.types = toTypes(step, accumulatorFactories); + + // wrapper each function with an aggregator + ImmutableList.Builder builder = ImmutableList.builder(); + for (AccumulatorFactory accumulatorFactory : accumulatorFactories) { + builder.add(new Aggregator(accumulatorFactory, step)); + } + aggregates = builder.build(); + } + + @Override + public OperatorContext getOperatorContext() + { + return operatorContext; + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public void finish() + { + if (state == State.NEEDS_INPUT) { + state = State.HAS_OUTPUT; + } + } + + @Override + public boolean isFinished() + { + return state == State.FINISHED; + } + + @Override + public boolean needsInput() + { + return state == State.NEEDS_INPUT; + } + + @Override + public void addInput(Page page) + { + checkState(needsInput(), "Operator is already finishing"); + checkNotNull(page, "page is null"); + + long memorySize = 0; + for (Aggregator aggregate : aggregates) { + aggregate.processPage(page); + memorySize += aggregate.getEstimatedSize(); + } + memorySize -= operatorContext.getOperatorPreAllocatedMemory().toBytes(); + operatorContext.setMemoryReservation(Math.max(0, memorySize)); + } + + @Override + public Page getOutput() + { + if (state != State.HAS_OUTPUT) { + return null; + } + + // project results into output blocks + List types = aggregates.stream().map(Aggregator::getType).collect(toImmutableList()); + + PageBuilder pageBuilder = new PageBuilder(types); + + pageBuilder.declarePosition(); + for (int i = 0; i < aggregates.size(); i++) { + Aggregator aggregator = aggregates.get(i); + BlockBuilder blockBuilder = pageBuilder.getBlockBuilder(i); + aggregator.evaluate(blockBuilder); + } + + state = State.FINISHED; + return pageBuilder.build(); + } + + private static List toTypes(Step step, List accumulatorFactories) + { + ImmutableList.Builder types = ImmutableList.builder(); + for (AccumulatorFactory accumulatorFactory : accumulatorFactories) { + types.add(new Aggregator(accumulatorFactory, step).getType()); + } + return types.build(); + } + + private static class Aggregator + { + private final Accumulator aggregation; + private final Step step; + private final int intermediateChannel; + + private Aggregator(AccumulatorFactory accumulatorFactory, Step step) + { + if (step == Step.FINAL) { + checkArgument(accumulatorFactory.getInputChannels().size() == 1, "expected 1 input channel for intermediate aggregation"); + intermediateChannel = accumulatorFactory.getInputChannels().get(0); + aggregation = accumulatorFactory.createIntermediateAccumulator(); + } + else { + intermediateChannel = -1; + aggregation = accumulatorFactory.createAccumulator(); + } + this.step = step; + } + + public Type getType() + { + if (step == Step.PARTIAL) { + return aggregation.getIntermediateType(); + } + else { + return aggregation.getFinalType(); + } + } + + public void processPage(Page page) + { + if (step == Step.FINAL) { + aggregation.addIntermediate(page.getBlock(intermediateChannel)); + } + else { + aggregation.addInput(page); + } + } + + public void evaluate(BlockBuilder blockBuilder) + { + if (step == Step.PARTIAL) { + aggregation.evaluateIntermediate(blockBuilder); + } + else { + aggregation.evaluateFinal(blockBuilder); + } + } + + public long getEstimatedSize() + { + return aggregation.getEstimatedSize(); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/ArrayUnnester.java b/presto-main/src/main/java/com/facebook/presto/operator/ArrayUnnester.java new file mode 100644 index 00000000..bbdf9b32 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/ArrayUnnester.java @@ -0,0 +1,77 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.type.ArrayType; +import io.airlift.slice.Slice; + +import javax.annotation.Nullable; + +import static com.facebook.presto.type.TypeUtils.readStructuralBlock; +import static com.google.common.base.Preconditions.checkNotNull; + +public class ArrayUnnester + implements Unnester +{ + private final Type elementType; + private final Block arrayBlock; + private final int channelCount; + + private int position; + private final int positionCount; + + public ArrayUnnester(ArrayType arrayType, @Nullable Slice slice) + { + this.channelCount = 1; + this.elementType = checkNotNull(arrayType, "arrayType is null").getElementType(); + + if (slice == null) { + arrayBlock = null; + positionCount = 0; + } + else { + arrayBlock = readStructuralBlock(slice); + positionCount = arrayBlock.getPositionCount(); + } + } + + protected void appendTo(PageBuilder pageBuilder, int outputChannelOffset) + { + BlockBuilder blockBuilder = pageBuilder.getBlockBuilder(outputChannelOffset); + elementType.appendTo(arrayBlock, position, blockBuilder); + position++; + } + + @Override + public boolean hasNext() + { + return position < positionCount; + } + + @Override + public final int getChannelCount() + { + return channelCount; + } + + @Override + public final void appendNext(PageBuilder pageBuilder, int outputChannelOffset) + { + appendTo(pageBuilder, outputChannelOffset); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/BigintGroupByHash.java b/presto-main/src/main/java/com/facebook/presto/operator/BigintGroupByHash.java new file mode 100644 index 00000000..abb99eab --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/BigintGroupByHash.java @@ -0,0 +1,309 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.type.BigintOperators; +import com.facebook.presto.util.array.IntBigArray; +import com.facebook.presto.util.array.LongBigArray; +import com.google.common.collect.ImmutableList; + +import java.util.List; +import java.util.Optional; + +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.BooleanType.BOOLEAN; +import static com.facebook.presto.type.TypeUtils.NULL_HASH_CODE; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static it.unimi.dsi.fastutil.HashCommon.arraySize; +import static it.unimi.dsi.fastutil.HashCommon.murmurHash3; + +public class BigintGroupByHash + implements GroupByHash +{ + private static final float FILL_RATIO = 0.9f; + private static final List TYPES = ImmutableList.of(BIGINT); + private static final List TYPES_WITH_RAW_HASH = ImmutableList.of(BIGINT, BIGINT); + + private final int hashChannel; + private final int maskChannel; + private final boolean outputRawHash; + + private int maxFill; + private int mask; + + // the hash table from values to groupIds + private LongBigArray values; + private IntBigArray groupIds; + + // groupId for the null value + private int nullGroupId = -1; + + // reverse index from the groupId back to the value + private final LongBigArray valuesByGroupId; + + private int nextGroupId; + + public BigintGroupByHash(int hashChannel, Optional maskChannel, boolean outputRawHash, int expectedSize) + { + checkArgument(hashChannel >= 0, "hashChannel must be at least zero"); + checkArgument(expectedSize > 0, "expectedSize must be greater than zero"); + + this.hashChannel = hashChannel; + this.maskChannel = checkNotNull(maskChannel, "maskChannel is null").orElse(-1); + this.outputRawHash = outputRawHash; + + int hashSize = arraySize(expectedSize, FILL_RATIO); + + maxFill = calculateMaxFill(hashSize); + mask = hashSize - 1; + values = new LongBigArray(); + values.ensureCapacity(hashSize); + groupIds = new IntBigArray(-1); + groupIds.ensureCapacity(hashSize); + + valuesByGroupId = new LongBigArray(); + valuesByGroupId.ensureCapacity(hashSize); + } + + @Override + public long getEstimatedSize() + { + return groupIds.sizeOf() + + values.sizeOf() + + valuesByGroupId.sizeOf(); + } + + @Override + public List getTypes() + { + return outputRawHash ? TYPES_WITH_RAW_HASH : TYPES; + } + + @Override + public int getGroupCount() + { + return nextGroupId; + } + + @Override + public void appendValuesTo(int groupId, PageBuilder pageBuilder, int outputChannelOffset) + { + BlockBuilder blockBuilder = pageBuilder.getBlockBuilder(outputChannelOffset); + if (groupId == nullGroupId) { + blockBuilder.appendNull(); + } + else { + BIGINT.writeLong(blockBuilder, valuesByGroupId.get(groupId)); + } + + if (outputRawHash) { + BlockBuilder hashBlockBuilder = pageBuilder.getBlockBuilder(outputChannelOffset + 1); + if (groupId == nullGroupId) { + BIGINT.writeLong(hashBlockBuilder, NULL_HASH_CODE); + } + else { + BIGINT.writeLong(hashBlockBuilder, BigintOperators.hashCode(valuesByGroupId.get(groupId))); + } + } + } + + @Override + public void addPage(Page page) + { + int positionCount = page.getPositionCount(); + + Block maskBlock = null; + if (maskChannel >= 0) { + maskBlock = page.getBlock(maskChannel); + } + + // get the group id for each position + Block block = page.getBlock(hashChannel); + for (int position = 0; position < positionCount; position++) { + // skip masked rows + if (maskBlock != null && !BOOLEAN.getBoolean(maskBlock, position)) { + continue; + } + + // get the group for the current row + putIfAbsent(position, block); + } + } + + @Override + public GroupByIdBlock getGroupIds(Page page) + { + int positionCount = page.getPositionCount(); + + // we know the exact size required for the block + BlockBuilder blockBuilder = BIGINT.createFixedSizeBlockBuilder(positionCount); + + Block maskBlock = null; + if (maskChannel >= 0) { + maskBlock = page.getBlock(maskChannel); + } + + // get the group id for each position + Block block = page.getBlock(hashChannel); + for (int position = 0; position < positionCount; position++) { + // skip masked rows + if (maskBlock != null && !BOOLEAN.getBoolean(maskBlock, position)) { + blockBuilder.appendNull(); + continue; + } + + // get the group for the current row + int groupId = putIfAbsent(position, block); + + // output the group id for this row + BIGINT.writeLong(blockBuilder, groupId); + } + return new GroupByIdBlock(nextGroupId, blockBuilder.build()); + } + + @Override + public boolean contains(int position, Page page) + { + Block block = page.getBlock(hashChannel); + if (block.isNull(position)) { + return nullGroupId >= 0; + } + + long value = BIGINT.getLong(block, position); + int hashPosition = getHashPosition(value, mask); + + // look for an empty slot or a slot containing this key + while (true) { + int groupId = groupIds.get(hashPosition); + if (groupId == -1) { + return false; + } + else if (value == values.get(hashPosition)) { + return true; + } + + // increment position and mask to handle wrap around + hashPosition = (hashPosition + 1) & mask; + } + } + + @Override + public int putIfAbsent(int position, Page page) + { + Block block = page.getBlock(hashChannel); + return putIfAbsent(position, block); + } + + private int putIfAbsent(int position, Block block) + { + if (block.isNull(position)) { + if (nullGroupId < 0) { + // set null group id + nullGroupId = nextGroupId++; + } + + return nullGroupId; + } + + long value = BIGINT.getLong(block, position); + int hashPosition = getHashPosition(value, mask); + + // look for an empty slot or a slot containing this key + while (true) { + int groupId = groupIds.get(hashPosition); + if (groupId == -1) { + break; + } + + if (value == values.get(hashPosition)) { + return groupId; + } + + // increment position and mask to handle wrap around + hashPosition = (hashPosition + 1) & mask; + } + + return addNewGroup(hashPosition, value); + } + + private int addNewGroup(int hashPosition, long value) + { + // record group id in hash + int groupId = nextGroupId++; + + values.set(hashPosition, value); + valuesByGroupId.set(groupId, value); + groupIds.set(hashPosition, groupId); + + // increase capacity, if necessary + if (nextGroupId >= maxFill) { + rehash(maxFill * 2); + } + return groupId; + } + + private void rehash(int size) + { + int newSize = arraySize(size + 1, FILL_RATIO); + + int newMask = newSize - 1; + LongBigArray newValues = new LongBigArray(); + newValues.ensureCapacity(newSize); + IntBigArray newGroupIds = new IntBigArray(-1); + newGroupIds.ensureCapacity(newSize); + + for (int groupId = 0; groupId < nextGroupId; groupId++) { + long value = valuesByGroupId.get(groupId); + + // find an empty slot for the address + int hashPosition = getHashPosition(value, newMask); + while (newGroupIds.get(hashPosition) != -1) { + hashPosition = (hashPosition + 1) & newMask; + } + + // record the mapping + newValues.set(hashPosition, value); + newGroupIds.set(hashPosition, groupId); + } + + mask = newMask; + maxFill = calculateMaxFill(newSize); + values = newValues; + groupIds = newGroupIds; + + this.valuesByGroupId.ensureCapacity(maxFill); + } + + private static int getHashPosition(long rawHash, int mask) + { + return ((int) murmurHash3(rawHash)) & mask; + } + + private static int calculateMaxFill(int hashSize) + { + checkArgument(hashSize > 0, "hashSize must greater than 0"); + int maxFill = (int) Math.ceil(hashSize * FILL_RATIO); + if (maxFill == hashSize) { + maxFill--; + } + checkArgument(hashSize > maxFill, "hashSize must be larger than maxFill"); + return maxFill; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/BlockedReason.java b/presto-main/src/main/java/com/facebook/presto/operator/BlockedReason.java new file mode 100644 index 00000000..c913f5e6 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/BlockedReason.java @@ -0,0 +1,19 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +public enum BlockedReason +{ + WAITING_FOR_MEMORY +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/ChannelSet.java b/presto-main/src/main/java/com/facebook/presto/operator/ChannelSet.java new file mode 100644 index 00000000..beb3f1ef --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/ChannelSet.java @@ -0,0 +1,101 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.block.BlockBuilderStatus; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; + +import java.util.List; +import java.util.Optional; + +import static com.facebook.presto.operator.GroupByHash.createGroupByHash; +import static com.facebook.presto.type.UnknownType.UNKNOWN; + +public class ChannelSet +{ + private final GroupByHash hash; + private final boolean containsNull; + + public ChannelSet(GroupByHash hash, boolean containsNull) + { + this.hash = hash; + this.containsNull = containsNull; + } + + public Type getType() + { + return hash.getTypes().get(0); + } + + public long getEstimatedSizeInBytes() + { + return hash.getEstimatedSize(); + } + + public int size() + { + return hash.getGroupCount(); + } + + public boolean containsNull() + { + return containsNull; + } + + public boolean contains(int position, Page page) + { + return hash.contains(position, page); + } + + public static class ChannelSetBuilder + { + private final GroupByHash hash; + private final OperatorContext operatorContext; + private final Page nullBlockPage; + + public ChannelSetBuilder(Type type, Optional hashChannel, int expectedPositions, OperatorContext operatorContext) + { + List types = ImmutableList.of(type); + this.hash = createGroupByHash(types, new int[] {0}, Optional.empty(), hashChannel, expectedPositions); + this.operatorContext = operatorContext; + this.nullBlockPage = new Page(type.createBlockBuilder(new BlockBuilderStatus(), 1, UNKNOWN.getFixedSize()).appendNull().build()); + } + + public ChannelSet build() + { + return new ChannelSet(hash, hash.contains(0, nullBlockPage)); + } + + public long getEstimatedSize() + { + return hash.getEstimatedSize(); + } + + public int size() + { + return hash.getGroupCount(); + } + + public void addPage(Page page) + { + hash.addPage(page); + + if (operatorContext != null) { + operatorContext.setMemoryReservation(hash.getEstimatedSize()); + } + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/CursorProcessor.java b/presto-main/src/main/java/com/facebook/presto/operator/CursorProcessor.java new file mode 100644 index 00000000..2623d899 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/CursorProcessor.java @@ -0,0 +1,26 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.RecordCursor; + +public interface CursorProcessor +{ + /** + * @return 0 if processing is complete + */ + int process(ConnectorSession session, RecordCursor cursor, int count, PageBuilder pageBuilder); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/DeleteOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/DeleteOperator.java new file mode 100644 index 00000000..d73b2fa2 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/DeleteOperator.java @@ -0,0 +1,190 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.UpdatablePageSource; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.function.Supplier; + +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.VarbinaryType.VARBINARY; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +public class DeleteOperator + implements Operator +{ + public static final List TYPES = ImmutableList.of(BIGINT, VARBINARY); + + public static class DeleteOperatorFactory + implements OperatorFactory + { + private final int operatorId; + private final int rowIdChannel; + private boolean closed; + + public DeleteOperatorFactory(int operatorId, int rowIdChannel) + { + this.operatorId = operatorId; + this.rowIdChannel = rowIdChannel; + } + + @Override + public List getTypes() + { + return TYPES; + } + + @Override + public Operator createOperator(DriverContext driverContext) + { + checkState(!closed, "Factory is already closed"); + OperatorContext context = driverContext.addOperatorContext(operatorId, DeleteOperator.class.getSimpleName()); + return new DeleteOperator(context, rowIdChannel); + } + + @Override + public void close() + { + closed = true; + } + } + + private enum State + { + RUNNING, FINISHING, FINISHED + } + + private final OperatorContext operatorContext; + private final int rowIdChannel; + + private State state = State.RUNNING; + private long rowCount; + private boolean committed; + private boolean closed; + private Supplier> pageSource = Optional::empty; + + public DeleteOperator(OperatorContext operatorContext, int rowIdChannel) + { + this.operatorContext = checkNotNull(operatorContext, "operatorContext is null"); + this.rowIdChannel = rowIdChannel; + } + + @Override + public OperatorContext getOperatorContext() + { + return operatorContext; + } + + @Override + public List getTypes() + { + return TYPES; + } + + @Override + public void finish() + { + if (state == State.RUNNING) { + state = State.FINISHING; + } + } + + @Override + public boolean isFinished() + { + return state == State.FINISHED; + } + + @Override + public boolean needsInput() + { + return state == State.RUNNING; + } + + @Override + public void addInput(Page page) + { + checkNotNull(page, "page is null"); + checkState(state == State.RUNNING, "Operator is %s", state); + + Block rowIds = page.getBlock(rowIdChannel); + pageSource().deleteRows(rowIds); + rowCount += rowIds.getPositionCount(); + } + + @Override + public Page getOutput() + { + if (state != State.FINISHING) { + return null; + } + state = State.FINISHED; + + Collection fragments = pageSource().commit(); + committed = true; + + PageBuilder page = new PageBuilder(TYPES); + BlockBuilder rowsBuilder = page.getBlockBuilder(0); + BlockBuilder fragmentBuilder = page.getBlockBuilder(1); + + // write row count + page.declarePosition(); + BIGINT.writeLong(rowsBuilder, rowCount); + fragmentBuilder.appendNull(); + + // write fragments + for (Slice fragment : fragments) { + page.declarePosition(); + rowsBuilder.appendNull(); + VARBINARY.writeSlice(fragmentBuilder, fragment); + } + + return page.build(); + } + + @Override + public void close() + throws Exception + { + if (!closed) { + closed = true; + if (!committed) { + pageSource.get().ifPresent(UpdatablePageSource::rollback); + } + } + } + + public void setPageSource(Supplier> pageSource) + { + this.pageSource = checkNotNull(pageSource, "pageSource is null"); + } + + private UpdatablePageSource pageSource() + { + Optional source = pageSource.get(); + checkState(source.isPresent(), "UpdatablePageSource not set"); + return source.get(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/Description.java b/presto-main/src/main/java/com/facebook/presto/operator/Description.java new file mode 100644 index 00000000..2d36cba3 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/Description.java @@ -0,0 +1,28 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention(RUNTIME) +@Target({METHOD, TYPE}) +public @interface Description +{ + String value(); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/DistinctLimitOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/DistinctLimitOperator.java new file mode 100644 index 00000000..78919e11 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/DistinctLimitOperator.java @@ -0,0 +1,171 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; +import com.google.common.primitives.Ints; + +import java.util.List; +import java.util.Optional; + +import static com.facebook.presto.operator.GroupByHash.createGroupByHash; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +public class DistinctLimitOperator + implements Operator +{ + public static class DistinctLimitOperatorFactory + implements OperatorFactory + { + private final int operatorId; + private final List distinctChannels; + private final List types; + private final long limit; + private final Optional hashChannel; + private boolean closed; + + public DistinctLimitOperatorFactory(int operatorId, List types, List distinctChannels, long limit, Optional hashChannel) + { + this.operatorId = operatorId; + this.types = ImmutableList.copyOf(checkNotNull(types, "types is null")); + this.distinctChannels = checkNotNull(distinctChannels, "distinctChannels is null"); + + checkArgument(limit >= 0, "limit must be at least zero"); + this.limit = limit; + this.hashChannel = checkNotNull(hashChannel, "hashChannel is null"); + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public Operator createOperator(DriverContext driverContext) + { + checkState(!closed, "Factory is already closed"); + OperatorContext operatorContext = driverContext.addOperatorContext(operatorId, DistinctLimitOperator.class.getSimpleName()); + return new DistinctLimitOperator(operatorContext, types, distinctChannels, limit, hashChannel); + } + + @Override + public void close() + { + closed = true; + } + } + + private final OperatorContext operatorContext; + private final List types; + + private final PageBuilder pageBuilder; + private Page outputPage; + private long remainingLimit; + + private boolean finishing; + + private final GroupByHash groupByHash; + private long nextDistinctId; + + public DistinctLimitOperator(OperatorContext operatorContext, List types, List distinctChannels, long limit, Optional hashChannel) + { + this.operatorContext = checkNotNull(operatorContext, "operatorContext is null"); + this.types = ImmutableList.copyOf(checkNotNull(types, "types is null")); + checkNotNull(distinctChannels, "distinctChannels is null"); + checkArgument(limit >= 0, "limit must be at least zero"); + checkNotNull(hashChannel, "hashChannel is null"); + + ImmutableList.Builder distinctTypes = ImmutableList.builder(); + for (int channel : distinctChannels) { + distinctTypes.add(types.get(channel)); + } + this.groupByHash = createGroupByHash(distinctTypes.build(), Ints.toArray(distinctChannels), Optional.empty(), hashChannel, Math.min((int) limit, 10_000)); + this.pageBuilder = new PageBuilder(types); + remainingLimit = limit; + } + + @Override + public OperatorContext getOperatorContext() + { + return operatorContext; + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public void finish() + { + finishing = true; + pageBuilder.reset(); + } + + @Override + public boolean isFinished() + { + return (finishing && outputPage == null) || (remainingLimit == 0 && outputPage == null); + } + + @Override + public boolean needsInput() + { + operatorContext.setMemoryReservation(groupByHash.getEstimatedSize()); + return !finishing && remainingLimit > 0 && outputPage == null; + } + + @Override + public void addInput(Page page) + { + checkState(needsInput()); + operatorContext.setMemoryReservation(groupByHash.getEstimatedSize()); + + pageBuilder.reset(); + + GroupByIdBlock ids = groupByHash.getGroupIds(page); + for (int position = 0; position < ids.getPositionCount(); position++) { + if (ids.getGroupId(position) == nextDistinctId) { + pageBuilder.declarePosition(); + for (int channel = 0; channel < types.size(); channel++) { + Type type = types.get(channel); + type.appendTo(page.getBlock(channel), position, pageBuilder.getBlockBuilder(channel)); + } + remainingLimit--; + nextDistinctId++; + if (remainingLimit == 0) { + break; + } + } + } + if (!pageBuilder.isEmpty()) { + outputPage = pageBuilder.build(); + } + } + + @Override + public Page getOutput() + { + Page result = outputPage; + outputPage = null; + return result; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/Driver.java b/presto-main/src/main/java/com/facebook/presto/operator/Driver.java new file mode 100644 index 00000000..9245fc14 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/Driver.java @@ -0,0 +1,626 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.ScheduledSplit; +import com.facebook.presto.TaskSource; +import com.facebook.presto.metadata.Split; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.UpdatablePageSource; +import com.facebook.presto.sql.planner.plan.PlanNodeId; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Sets; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.SettableFuture; +import io.airlift.log.Logger; +import io.airlift.units.Duration; + +import javax.annotation.concurrent.GuardedBy; + +import java.io.Closeable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Supplier; + +import static com.facebook.presto.operator.Operator.NOT_BLOCKED; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +// +// NOTE: As a general strategy the methods should "stage" a change and only +// process the actual change before lock release (DriverLockResult.close()). +// The assures that only one thread will be working with the operators at a +// time and state changer threads are not blocked. +// +public class Driver + implements Closeable +{ + private static final Logger log = Logger.get(Driver.class); + + private final DriverContext driverContext; + private final List operators; + private final Map sourceOperators; + private final Optional deleteOperator; + private final ConcurrentMap newSources = new ConcurrentHashMap<>(); + + private final AtomicReference state = new AtomicReference<>(State.ALIVE); + + private final ReentrantLock exclusiveLock = new ReentrantLock(); + + @GuardedBy("this") + private Thread lockHolder; + + @GuardedBy("exclusiveLock") + private final Map currentSources = new ConcurrentHashMap<>(); + + private enum State + { + ALIVE, NEED_DESTRUCTION, DESTROYED + } + + public Driver(DriverContext driverContext, Operator firstOperator, Operator... otherOperators) + { + this(checkNotNull(driverContext, "driverContext is null"), + ImmutableList.builder() + .add(checkNotNull(firstOperator, "firstOperator is null")) + .add(checkNotNull(otherOperators, "otherOperators is null")) + .build()); + } + + public Driver(DriverContext driverContext, List operators) + { + this.driverContext = checkNotNull(driverContext, "driverContext is null"); + this.operators = ImmutableList.copyOf(checkNotNull(operators, "operators is null")); + checkArgument(!operators.isEmpty(), "There must be at least one operator"); + + ImmutableMap.Builder sourceOperators = ImmutableMap.builder(); + Optional deleteOperator = Optional.empty(); + for (Operator operator : operators) { + if (operator instanceof SourceOperator) { + SourceOperator sourceOperator = (SourceOperator) operator; + sourceOperators.put(sourceOperator.getSourceId(), sourceOperator); + } + else if (operator instanceof DeleteOperator) { + checkArgument(!deleteOperator.isPresent(), "There must be at most one DeleteOperator"); + deleteOperator = Optional.of((DeleteOperator) operator); + } + } + this.sourceOperators = sourceOperators.build(); + this.deleteOperator = deleteOperator; + } + + public DriverContext getDriverContext() + { + return driverContext; + } + + public Set getSourceIds() + { + return sourceOperators.keySet(); + } + + @Override + public void close() + { + // mark the service for destruction + if (!state.compareAndSet(State.ALIVE, State.NEED_DESTRUCTION)) { + return; + } + + // if we can get the lock, attempt a clean shutdown; otherwise someone else will shutdown + try (DriverLockResult lockResult = tryLockAndProcessPendingStateChanges(0, TimeUnit.MILLISECONDS)) { + // if we did not get the lock, interrupt the lock holder + if (!lockResult.wasAcquired()) { + // there is a benign race condition here were the lock holder + // can be change between attempting to get lock and grabbing + // the synchronized lock here, but in either case we want to + // interrupt the lock holder thread + synchronized (this) { + if (lockHolder != null) { + lockHolder.interrupt(); + } + } + } + + // clean shutdown is automatically triggered during lock release + } + } + + public boolean isFinished() + { + checkLockNotHeld("Can not check finished status while holding the driver lock"); + + // if we can get the lock, attempt a clean shutdown; otherwise someone else will shutdown + try (DriverLockResult lockResult = tryLockAndProcessPendingStateChanges(0, TimeUnit.MILLISECONDS)) { + if (lockResult.wasAcquired()) { + return isFinishedInternal(); + } + else { + // did not get the lock, so we can't check operators, or destroy + return state.get() != State.ALIVE || driverContext.isDone(); + } + } + } + + private boolean isFinishedInternal() + { + checkLockHeld("Lock must be held to call isFinishedInternal"); + + boolean finished = state.get() != State.ALIVE || driverContext.isDone() || operators.get(operators.size() - 1).isFinished(); + if (finished) { + state.compareAndSet(State.ALIVE, State.NEED_DESTRUCTION); + } + return finished; + } + + public void updateSource(TaskSource source) + { + checkLockNotHeld("Can not update sources while holding the driver lock"); + + // does this driver have an operator for the specified source? + if (!sourceOperators.containsKey(source.getPlanNodeId())) { + return; + } + + // stage the new updates + while (true) { + // attempt to update directly to the new source + TaskSource currentNewSource = newSources.putIfAbsent(source.getPlanNodeId(), source); + + // if update succeeded, just break + if (currentNewSource == null) { + break; + } + + // merge source into the current new source + TaskSource newSource = currentNewSource.update(source); + + // if this is not a new source, just return + if (newSource == currentNewSource) { + break; + } + + // attempt to replace the currentNewSource with the new source + if (newSources.replace(source.getPlanNodeId(), currentNewSource, newSource)) { + break; + } + + // someone else updated while we were processing + } + + // attempt to get the lock and process the updates we staged above + // updates will be processed in close if and only if we got the lock + tryLockAndProcessPendingStateChanges(0, TimeUnit.MILLISECONDS).close(); + } + + private void processNewSources() + { + checkLockHeld("Lock must be held to call processNewSources"); + + // only update if the driver is still alive + if (state.get() != State.ALIVE) { + return; + } + + // copy the pending sources + // it is ok to "miss" a source added during the copy as it will be + // handled on the next call to this method + Map sources = new HashMap<>(newSources); + for (Entry entry : sources.entrySet()) { + // Remove the entries we are going to process from the newSources map. + // It is ok if someone already updated the entry; we will catch it on + // the next iteration. + newSources.remove(entry.getKey(), entry.getValue()); + + processNewSource(entry.getValue()); + } + } + + private void processNewSource(TaskSource source) + { + checkLockHeld("Lock must be held to call processNewSources"); + + // create new source + Set newSplits; + TaskSource currentSource = currentSources.get(source.getPlanNodeId()); + if (currentSource == null) { + newSplits = source.getSplits(); + currentSources.put(source.getPlanNodeId(), source); + } + else { + // merge the current source and the specified source + TaskSource newSource = currentSource.update(source); + + // if this is not a new source, just return + if (newSource == currentSource) { + return; + } + + // find the new splits to add + newSplits = Sets.difference(newSource.getSplits(), currentSource.getSplits()); + currentSources.put(source.getPlanNodeId(), newSource); + } + + // add new splits + for (ScheduledSplit newSplit : newSplits) { + Split split = newSplit.getSplit(); + + SourceOperator sourceOperator = sourceOperators.get(source.getPlanNodeId()); + if (sourceOperator != null) { + Supplier> pageSource = sourceOperator.addSplit(split); + if (deleteOperator.isPresent()) { + deleteOperator.get().setPageSource(pageSource); + } + } + } + + // set no more splits + if (source.isNoMoreSplits()) { + sourceOperators.get(source.getPlanNodeId()).noMoreSplits(); + } + } + + public ListenableFuture processFor(Duration duration) + { + checkLockNotHeld("Can not process for a duration while holding the driver lock"); + + checkNotNull(duration, "duration is null"); + + long maxRuntime = duration.roundTo(TimeUnit.NANOSECONDS); + + try (DriverLockResult lockResult = tryLockAndProcessPendingStateChanges(100, TimeUnit.MILLISECONDS)) { + if (lockResult.wasAcquired()) { + driverContext.startProcessTimer(); + try { + long start = System.nanoTime(); + do { + ListenableFuture future = processInternal(); + if (!future.isDone()) { + return future; + } + } + while (System.nanoTime() - start < maxRuntime && !isFinishedInternal()); + } + finally { + driverContext.recordProcessed(); + } + } + } + return NOT_BLOCKED; + } + + public ListenableFuture process() + { + checkLockNotHeld("Can not process while holding the driver lock"); + + try (DriverLockResult lockResult = tryLockAndProcessPendingStateChanges(100, TimeUnit.MILLISECONDS)) { + if (!lockResult.wasAcquired()) { + // this is unlikely to happen unless the driver is being + // destroyed and in that case the caller should notice notice + // this state change by calling isFinished + return NOT_BLOCKED; + } + return processInternal(); + } + } + + private ListenableFuture processInternal() + { + checkLockHeld("Lock must be held to call processInternal"); + + try { + if (!newSources.isEmpty()) { + processNewSources(); + } + + // special handling for drivers with a single operator + if (operators.size() == 1) { + if (driverContext.isDone()) { + return NOT_BLOCKED; + } + + // check if operator is blocked + Operator current = operators.get(0); + ListenableFuture blocked = isBlocked(current); + if (!blocked.isDone()) { + current.getOperatorContext().recordBlocked(blocked); + return blocked; + } + + // there is only one operator so just finish it + current.getOperatorContext().startIntervalTimer(); + current.finish(); + current.getOperatorContext().recordFinish(); + return NOT_BLOCKED; + } + + boolean movedPage = false; + for (int i = 0; i < operators.size() - 1 && !driverContext.isDone(); i++) { + Operator current = operators.get(i); + Operator next = operators.get(i + 1); + + // skip blocked operators + if (!isBlocked(current).isDone()) { + continue; + } + if (!isBlocked(next).isDone()) { + continue; + } + + // if the current operator is not finished and next operator needs input... + if (!current.isFinished() && next.needsInput()) { + // get an output page from current operator + current.getOperatorContext().startIntervalTimer(); + Page page = current.getOutput(); + current.getOperatorContext().recordGetOutput(page); + + // if we got an output page, add it to the next operator + if (page != null) { + next.getOperatorContext().startIntervalTimer(); + next.addInput(page); + next.getOperatorContext().recordAddInput(page); + movedPage = true; + } + } + + // if current operator is finished... + if (current.isFinished()) { + // let next operator know there will be no more data + next.getOperatorContext().startIntervalTimer(); + next.finish(); + next.getOperatorContext().recordFinish(); + } + } + + // if we did not move any pages, check if we are blocked + if (!movedPage) { + List blockedOperators = new ArrayList<>(); + List> blockedFutures = new ArrayList<>(); + for (Operator operator : operators) { + ListenableFuture blocked = isBlocked(operator); + if (!blocked.isDone()) { + blockedOperators.add(operator); + blockedFutures.add(blocked); + } + } + + if (!blockedFutures.isEmpty()) { + // unblock when the first future is complete + ListenableFuture blocked = firstFinishedFuture(blockedFutures); + // driver records serial blocked time + driverContext.recordBlocked(blocked); + // each blocked operator is responsible for blocking the execution + // until one of the operators can continue + for (Operator operator : blockedOperators) { + operator.getOperatorContext().recordBlocked(blocked); + } + return blocked; + } + } + + return NOT_BLOCKED; + } + catch (Throwable t) { + driverContext.failed(t); + throw t; + } + } + + private void destroyIfNecessary() + { + checkLockHeld("Lock must be held to call destroyIfNecessary"); + + if (!state.compareAndSet(State.NEED_DESTRUCTION, State.DESTROYED)) { + return; + } + + // record the current interrupted status (and clear the flag); we'll reset it later + boolean wasInterrupted = Thread.interrupted(); + + // if we get an error while closing a driver, record it and we will throw it at the end + Throwable inFlightException = null; + try { + for (Operator operator : operators) { + try { + operator.close(); + } + catch (InterruptedException t) { + // don't record the stack + wasInterrupted = true; + } + catch (Throwable t) { + inFlightException = addSuppressedException( + inFlightException, + t, + "Error closing operator %s for task %s", + operator.getOperatorContext().getOperatorId(), + driverContext.getTaskId()); + } + try { + operator.getOperatorContext().setMemoryReservation(0); + } + catch (Throwable t) { + inFlightException = addSuppressedException( + inFlightException, + t, + "Error freeing memory for operator %s for task %s", + operator.getOperatorContext().getOperatorId(), + driverContext.getTaskId()); + } + } + driverContext.finished(); + } + catch (Throwable t) { + // this shouldn't happen but be safe + inFlightException = addSuppressedException( + inFlightException, + t, + "Error destroying driver for task %s", + driverContext.getTaskId()); + } + finally { + // reset the interrupted flag + if (wasInterrupted) { + Thread.currentThread().interrupt(); + } + } + + if (inFlightException != null) { + // this will always be an Error or Runtime + throw Throwables.propagate(inFlightException); + } + } + + private static ListenableFuture isBlocked(Operator operator) + { + ListenableFuture blocked = operator.isBlocked(); + if (blocked.isDone()) { + blocked = operator.getOperatorContext().isWaitingForMemory(); + } + return blocked; + } + + private static Throwable addSuppressedException(Throwable inFlightException, Throwable newException, String message, Object... args) + { + if (newException instanceof Error) { + if (inFlightException == null) { + inFlightException = newException; + } + else { + // Self-suppression not permitted + if (inFlightException != newException) { + inFlightException.addSuppressed(newException); + } + } + } + else { + // log normal exceptions instead of rethrowing them + log.error(newException, message, args); + } + return inFlightException; + } + + private DriverLockResult tryLockAndProcessPendingStateChanges(int timeout, TimeUnit unit) + { + checkLockNotHeld("Can not acquire the driver lock while already holding the driver lock"); + + return new DriverLockResult(timeout, unit); + } + + private synchronized void checkLockNotHeld(String message) + { + checkState(Thread.currentThread() != lockHolder, message); + } + + private synchronized void checkLockHeld(String message) + { + checkState(Thread.currentThread() == lockHolder, message); + } + + private static ListenableFuture firstFinishedFuture(List> futures) + { + SettableFuture result = SettableFuture.create(); + ExecutorService executor = MoreExecutors.newDirectExecutorService(); + + for (ListenableFuture future : futures) { + future.addListener(() -> result.set(null), executor); + } + + return result; + } + + private class DriverLockResult + implements AutoCloseable + { + private final boolean acquired; + + private DriverLockResult(int timeout, TimeUnit unit) + { + acquired = tryAcquire(timeout, unit); + } + + private boolean tryAcquire(int timeout, TimeUnit unit) + { + boolean acquired = false; + try { + acquired = exclusiveLock.tryLock(timeout, unit); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + if (acquired) { + synchronized (Driver.this) { + lockHolder = Thread.currentThread(); + } + } + + return acquired; + } + + public boolean wasAcquired() + { + return acquired; + } + + @Override + public void close() + { + if (!acquired) { + return; + } + + boolean done = false; + while (!done) { + done = true; + // before releasing the lock, process any new sources and/or destroy the driver + try { + try { + processNewSources(); + } + finally { + destroyIfNecessary(); + } + } + finally { + synchronized (Driver.this) { + lockHolder = null; + } + exclusiveLock.unlock(); + + // if new sources were added after we processed them, go around and try again + // in case someone else failed to acquire the lock and as a result won't update them + if (!newSources.isEmpty() && state.get() == State.ALIVE && tryAcquire(0, TimeUnit.MILLISECONDS)) { + done = false; + } + } + } + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/DriverContext.java b/presto-main/src/main/java/com/facebook/presto/operator/DriverContext.java new file mode 100644 index 00000000..82cda77d --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/DriverContext.java @@ -0,0 +1,427 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.Session; +import com.facebook.presto.execution.TaskId; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.util.concurrent.ListenableFuture; +import io.airlift.stats.CounterStat; +import io.airlift.units.DataSize; +import io.airlift.units.Duration; +import org.joda.time.DateTime; + +import javax.annotation.concurrent.ThreadSafe; + +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadMXBean; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.Iterables.getFirst; +import static com.google.common.collect.Iterables.getLast; +import static com.google.common.collect.Iterables.transform; +import static io.airlift.units.DataSize.Unit.BYTE; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.NANOSECONDS; + +@ThreadSafe +public class DriverContext +{ + private static final ThreadMXBean THREAD_MX_BEAN = ManagementFactory.getThreadMXBean(); + + private final PipelineContext pipelineContext; + private final Executor executor; + + private final AtomicBoolean finished = new AtomicBoolean(); + + private final DateTime createdTime = DateTime.now(); + private final long createNanos = System.nanoTime(); + + private final AtomicLong startNanos = new AtomicLong(); + private final AtomicLong endNanos = new AtomicLong(); + + private final AtomicLong intervalWallStart = new AtomicLong(); + private final AtomicLong intervalCpuStart = new AtomicLong(); + private final AtomicLong intervalUserStart = new AtomicLong(); + + private final AtomicLong processCalls = new AtomicLong(); + private final AtomicLong processWallNanos = new AtomicLong(); + private final AtomicLong processCpuNanos = new AtomicLong(); + private final AtomicLong processUserNanos = new AtomicLong(); + + private final AtomicReference blockedMonitor = new AtomicReference<>(); + private final AtomicLong blockedWallNanos = new AtomicLong(); + + private final AtomicReference executionStartTime = new AtomicReference<>(); + private final AtomicReference executionEndTime = new AtomicReference<>(); + + private final AtomicLong memoryReservation = new AtomicLong(); + + private final List operatorContexts = new CopyOnWriteArrayList<>(); + private final boolean partitioned; + + public DriverContext(PipelineContext pipelineContext, Executor executor, boolean partitioned) + { + this.pipelineContext = checkNotNull(pipelineContext, "pipelineContext is null"); + this.executor = checkNotNull(executor, "executor is null"); + this.partitioned = partitioned; + } + + public TaskId getTaskId() + { + return pipelineContext.getTaskId(); + } + + public OperatorContext addOperatorContext(int operatorId, String operatorType) + { + return addOperatorContext(operatorId, operatorType, pipelineContext.getMaxMemorySize().toBytes()); + } + + public OperatorContext addOperatorContext(int operatorId, String operatorType, long maxMemoryReservation) + { + checkArgument(operatorId >= 0, "operatorId is negative"); + + for (OperatorContext operatorContext : operatorContexts) { + checkArgument(operatorId != operatorContext.getOperatorId(), "A context already exists for operatorId %s", operatorId); + } + + OperatorContext operatorContext = new OperatorContext(operatorId, operatorType, this, executor, maxMemoryReservation); + operatorContexts.add(operatorContext); + return operatorContext; + } + + public List getOperatorContexts() + { + return ImmutableList.copyOf(operatorContexts); + } + + public PipelineContext getPipelineContext() + { + return pipelineContext; + } + + public Session getSession() + { + return pipelineContext.getSession(); + } + + public void startProcessTimer() + { + if (startNanos.compareAndSet(0, System.nanoTime())) { + pipelineContext.start(); + executionStartTime.set(DateTime.now()); + } + + intervalWallStart.set(System.nanoTime()); + intervalCpuStart.set(currentThreadCpuTime()); + intervalUserStart.set(currentThreadUserTime()); + } + + public void recordProcessed() + { + processCalls.incrementAndGet(); + processWallNanos.getAndAdd(nanosBetween(intervalWallStart.get(), System.nanoTime())); + processCpuNanos.getAndAdd(nanosBetween(intervalCpuStart.get(), currentThreadCpuTime())); + processUserNanos.getAndAdd(nanosBetween(intervalUserStart.get(), currentThreadUserTime())); + } + + public void recordBlocked(ListenableFuture blocked) + { + checkNotNull(blocked, "blocked is null"); + + BlockedMonitor monitor = new BlockedMonitor(); + + BlockedMonitor oldMonitor = blockedMonitor.getAndSet(monitor); + if (oldMonitor != null) { + oldMonitor.run(); + } + + blocked.addListener(monitor, executor); + } + + public void finished() + { + if (!finished.compareAndSet(false, true)) { + // already finished + return; + } + executionEndTime.set(DateTime.now()); + endNanos.set(System.nanoTime()); + + freeMemory(memoryReservation.get()); + + pipelineContext.driverFinished(this); + } + + public void failed(Throwable cause) + { + pipelineContext.failed(cause); + finished.set(true); + } + + public boolean isDone() + { + return finished.get() || pipelineContext.isDone(); + } + + public DataSize getMaxMemorySize() + { + return pipelineContext.getMaxMemorySize(); + } + + public DataSize getOperatorPreAllocatedMemory() + { + return pipelineContext.getOperatorPreAllocatedMemory(); + } + + public ListenableFuture reserveMemory(long bytes) + { + ListenableFuture future = pipelineContext.reserveMemory(bytes); + memoryReservation.getAndAdd(bytes); + return future; + } + + public boolean tryReserveMemory(long bytes) + { + if (pipelineContext.tryReserveMemory(bytes)) { + memoryReservation.getAndAdd(bytes); + return true; + } + return false; + } + + public void freeMemory(long bytes) + { + checkArgument(bytes >= 0, "bytes is negative"); + checkArgument(bytes <= memoryReservation.get(), "tried to free more memory than is reserved"); + pipelineContext.freeMemory(bytes); + memoryReservation.getAndAdd(-bytes); + } + + public void moreMemoryAvailable() + { + operatorContexts.stream().forEach(OperatorContext::moreMemoryAvailable); + } + + public boolean isVerboseStats() + { + return pipelineContext.isVerboseStats(); + } + + public boolean isCpuTimerEnabled() + { + return pipelineContext.isCpuTimerEnabled(); + } + + public CounterStat getInputDataSize() + { + OperatorContext inputOperator = getFirst(operatorContexts, null); + if (inputOperator != null) { + return inputOperator.getInputDataSize(); + } + else { + return new CounterStat(); + } + } + + public CounterStat getInputPositions() + { + OperatorContext inputOperator = getFirst(operatorContexts, null); + if (inputOperator != null) { + return inputOperator.getInputPositions(); + } + else { + return new CounterStat(); + } + } + + public CounterStat getOutputDataSize() + { + OperatorContext inputOperator = getLast(operatorContexts, null); + if (inputOperator != null) { + return inputOperator.getOutputDataSize(); + } + else { + return new CounterStat(); + } + } + + public CounterStat getOutputPositions() + { + OperatorContext inputOperator = getLast(operatorContexts, null); + if (inputOperator != null) { + return inputOperator.getOutputPositions(); + } + else { + return new CounterStat(); + } + } + + public DriverStats getDriverStats() + { + long totalScheduledTime = processWallNanos.get(); + long totalCpuTime = processCpuNanos.get(); + long totalUserTime = processUserNanos.get(); + + long totalBlockedTime = blockedWallNanos.get(); + BlockedMonitor blockedMonitor = this.blockedMonitor.get(); + if (blockedMonitor != null) { + totalBlockedTime += blockedMonitor.getBlockedTime(); + } + + List operators = ImmutableList.copyOf(transform(operatorContexts, OperatorContext::getOperatorStats)); + OperatorStats inputOperator = getFirst(operators, null); + DataSize rawInputDataSize; + long rawInputPositions; + Duration rawInputReadTime; + DataSize processedInputDataSize; + long processedInputPositions; + DataSize outputDataSize; + long outputPositions; + if (inputOperator != null) { + rawInputDataSize = inputOperator.getInputDataSize(); + rawInputPositions = inputOperator.getInputPositions(); + rawInputReadTime = inputOperator.getAddInputWall(); + + processedInputDataSize = inputOperator.getOutputDataSize(); + processedInputPositions = inputOperator.getOutputPositions(); + + OperatorStats outputOperator = checkNotNull(getLast(operators, null)); + outputDataSize = outputOperator.getOutputDataSize(); + outputPositions = outputOperator.getOutputPositions(); + } + else { + rawInputDataSize = new DataSize(0, BYTE); + rawInputPositions = 0; + rawInputReadTime = new Duration(0, MILLISECONDS); + + processedInputDataSize = new DataSize(0, BYTE); + processedInputPositions = 0; + + outputDataSize = new DataSize(0, BYTE); + outputPositions = 0; + } + + long startNanos = this.startNanos.get(); + if (startNanos < createNanos) { + startNanos = System.nanoTime(); + } + Duration queuedTime = new Duration(startNanos - createNanos, NANOSECONDS); + + long endNanos = this.endNanos.get(); + Duration elapsedTime; + if (endNanos >= startNanos) { + elapsedTime = new Duration(endNanos - createNanos, NANOSECONDS); + } + else { + elapsedTime = new Duration(0, NANOSECONDS); + } + + ImmutableSet.Builder builder = ImmutableSet.builder(); + + for (OperatorStats operator : operators) { + if (operator.getBlockedReason().isPresent()) { + builder.add(operator.getBlockedReason().get()); + } + } + + return new DriverStats( + createdTime, + executionStartTime.get(), + executionEndTime.get(), + queuedTime.convertToMostSuccinctTimeUnit(), + elapsedTime.convertToMostSuccinctTimeUnit(), + new DataSize(memoryReservation.get(), BYTE).convertToMostSuccinctDataSize(), + new Duration(totalScheduledTime, NANOSECONDS).convertToMostSuccinctTimeUnit(), + new Duration(totalCpuTime, NANOSECONDS).convertToMostSuccinctTimeUnit(), + new Duration(totalUserTime, NANOSECONDS).convertToMostSuccinctTimeUnit(), + new Duration(totalBlockedTime, NANOSECONDS).convertToMostSuccinctTimeUnit(), + blockedMonitor != null, + builder.build(), + rawInputDataSize.convertToMostSuccinctDataSize(), + rawInputPositions, + rawInputReadTime, + processedInputDataSize.convertToMostSuccinctDataSize(), + processedInputPositions, + outputDataSize.convertToMostSuccinctDataSize(), + outputPositions, + ImmutableList.copyOf(transform(operatorContexts, OperatorContext::getOperatorStats))); + } + + public boolean isPartitioned() + { + return partitioned; + } + + private long currentThreadUserTime() + { + if (!isCpuTimerEnabled()) { + return 0; + } + return THREAD_MX_BEAN.getCurrentThreadUserTime(); + } + + private long currentThreadCpuTime() + { + if (!isCpuTimerEnabled()) { + return 0; + } + return THREAD_MX_BEAN.getCurrentThreadCpuTime(); + } + + private static long nanosBetween(long start, long end) + { + return Math.abs(end - start); + } + + // hack for index joins + @Deprecated + public Executor getExecutor() + { + return executor; + } + + private class BlockedMonitor + implements Runnable + { + private final long start = System.nanoTime(); + private boolean finished; + + @Override + public void run() + { + synchronized (this) { + if (finished) { + return; + } + finished = true; + blockedMonitor.compareAndSet(this, null); + blockedWallNanos.getAndAdd(getBlockedTime()); + } + } + + public long getBlockedTime() + { + return nanosBetween(start, System.nanoTime()); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/DriverFactory.java b/presto-main/src/main/java/com/facebook/presto/operator/DriverFactory.java new file mode 100644 index 00000000..545821c3 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/DriverFactory.java @@ -0,0 +1,114 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.sql.planner.plan.PlanNodeId; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; + +import java.io.Closeable; +import java.util.List; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +public class DriverFactory + implements Closeable +{ + private final boolean inputDriver; + private final boolean outputDriver; + private final List operatorFactories; + private final Set sourceIds; + private final int driverInstances; + private boolean closed; + + public DriverFactory(boolean inputDriver, boolean outputDriver, OperatorFactory firstOperatorFactory, OperatorFactory... otherOperatorFactories) + { + this(inputDriver, + outputDriver, + ImmutableList.builder() + .add(checkNotNull(firstOperatorFactory, "firstOperatorFactory is null")) + .add(checkNotNull(otherOperatorFactories, "otherOperatorFactories is null")) + .build(), + 1); + } + + public DriverFactory(boolean inputDriver, boolean outputDriver, List operatorFactories) + { + this(inputDriver, outputDriver, operatorFactories, 1); + } + + public DriverFactory(boolean inputDriver, boolean outputDriver, List operatorFactories, int driverInstances) + { + this.inputDriver = inputDriver; + this.outputDriver = outputDriver; + this.operatorFactories = ImmutableList.copyOf(checkNotNull(operatorFactories, "operatorFactories is null")); + checkArgument(!operatorFactories.isEmpty(), "There must be at least one operator"); + this.driverInstances = driverInstances; + + ImmutableSet.Builder sourceIds = ImmutableSet.builder(); + for (OperatorFactory operatorFactory : operatorFactories) { + if (operatorFactory instanceof SourceOperatorFactory) { + SourceOperatorFactory sourceOperatorFactory = (SourceOperatorFactory) operatorFactory; + sourceIds.add(sourceOperatorFactory.getSourceId()); + } + } + this.sourceIds = sourceIds.build(); + } + + public boolean isInputDriver() + { + return inputDriver; + } + + public boolean isOutputDriver() + { + return outputDriver; + } + + public Set getSourceIds() + { + return sourceIds; + } + + public synchronized Driver createDriver(DriverContext driverContext) + { + checkState(!closed, "DriverFactory is already closed"); + checkNotNull(driverContext, "driverContext is null"); + ImmutableList.Builder operators = ImmutableList.builder(); + for (OperatorFactory operatorFactory : operatorFactories) { + Operator operator = operatorFactory.createOperator(driverContext); + operators.add(operator); + } + return new Driver(driverContext, operators.build()); + } + + @Override + public synchronized void close() + { + if (!closed) { + closed = true; + for (OperatorFactory operatorFactory : operatorFactories) { + operatorFactory.close(); + } + } + } + + public int getDriverInstances() + { + return driverInstances; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/DriverStats.java b/presto-main/src/main/java/com/facebook/presto/operator/DriverStats.java new file mode 100644 index 00000000..ece1fa1f --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/DriverStats.java @@ -0,0 +1,278 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import io.airlift.units.DataSize; +import io.airlift.units.Duration; +import org.joda.time.DateTime; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +import java.util.List; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkNotNull; +import static io.airlift.units.DataSize.Unit.BYTE; +import static java.util.Objects.requireNonNull; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +@Immutable +public class DriverStats +{ + private final DateTime createTime; + private final DateTime startTime; + private final DateTime endTime; + + private final Duration queuedTime; + private final Duration elapsedTime; + + private final DataSize memoryReservation; + + private final Duration totalScheduledTime; + private final Duration totalCpuTime; + private final Duration totalUserTime; + private final Duration totalBlockedTime; + private final boolean fullyBlocked; + private final Set blockedReasons; + + private final DataSize rawInputDataSize; + private final long rawInputPositions; + private final Duration rawInputReadTime; + + private final DataSize processedInputDataSize; + private final long processedInputPositions; + + private final DataSize outputDataSize; + private final long outputPositions; + + private final List operatorStats; + + public DriverStats() + { + this.createTime = DateTime.now(); + this.startTime = null; + this.endTime = null; + this.queuedTime = new Duration(0, MILLISECONDS); + this.elapsedTime = new Duration(0, MILLISECONDS); + + this.memoryReservation = new DataSize(0, BYTE); + + this.totalScheduledTime = new Duration(0, MILLISECONDS); + this.totalCpuTime = new Duration(0, MILLISECONDS); + this.totalUserTime = new Duration(0, MILLISECONDS); + this.totalBlockedTime = new Duration(0, MILLISECONDS); + this.fullyBlocked = false; + this.blockedReasons = ImmutableSet.of(); + + this.rawInputDataSize = new DataSize(0, BYTE); + this.rawInputPositions = 0; + this.rawInputReadTime = new Duration(0, MILLISECONDS); + + this.processedInputDataSize = new DataSize(0, BYTE); + this.processedInputPositions = 0; + + this.outputDataSize = new DataSize(0, BYTE); + this.outputPositions = 0; + + this.operatorStats = ImmutableList.of(); + } + + @JsonCreator + public DriverStats( + @JsonProperty("createTime") DateTime createTime, + @JsonProperty("startTime") DateTime startTime, + @JsonProperty("endTime") DateTime endTime, + @JsonProperty("queuedTime") Duration queuedTime, + @JsonProperty("elapsedTime") Duration elapsedTime, + + @JsonProperty("memoryReservation") DataSize memoryReservation, + + @JsonProperty("totalScheduledTime") Duration totalScheduledTime, + @JsonProperty("totalCpuTime") Duration totalCpuTime, + @JsonProperty("totalUserTime") Duration totalUserTime, + @JsonProperty("totalBlockedTime") Duration totalBlockedTime, + @JsonProperty("fullyBlocked") boolean fullyBlocked, + @JsonProperty("blockedReasons") Set blockedReasons, + + @JsonProperty("rawInputDataSize") DataSize rawInputDataSize, + @JsonProperty("rawInputPositions") long rawInputPositions, + @JsonProperty("rawInputReadTime") Duration rawInputReadTime, + + @JsonProperty("processedInputDataSize") DataSize processedInputDataSize, + @JsonProperty("processedInputPositions") long processedInputPositions, + + @JsonProperty("outputDataSize") DataSize outputDataSize, + @JsonProperty("outputPositions") long outputPositions, + + @JsonProperty("operatorStats") List operatorStats) + { + this.createTime = checkNotNull(createTime, "createTime is null"); + this.startTime = startTime; + this.endTime = endTime; + this.queuedTime = checkNotNull(queuedTime, "queuedTime is null"); + this.elapsedTime = checkNotNull(elapsedTime, "elapsedTime is null"); + + this.memoryReservation = checkNotNull(memoryReservation, "memoryReservation is null"); + + this.totalScheduledTime = checkNotNull(totalScheduledTime, "totalScheduledTime is null"); + this.totalCpuTime = checkNotNull(totalCpuTime, "totalCpuTime is null"); + this.totalUserTime = checkNotNull(totalUserTime, "totalUserTime is null"); + this.totalBlockedTime = checkNotNull(totalBlockedTime, "totalBlockedTime is null"); + this.fullyBlocked = fullyBlocked; + this.blockedReasons = ImmutableSet.copyOf(requireNonNull(blockedReasons, "blockedReasons is null")); + + this.rawInputDataSize = checkNotNull(rawInputDataSize, "rawInputDataSize is null"); + Preconditions.checkArgument(rawInputPositions >= 0, "rawInputPositions is negative"); + this.rawInputPositions = rawInputPositions; + this.rawInputReadTime = checkNotNull(rawInputReadTime, "rawInputReadTime is null"); + + this.processedInputDataSize = checkNotNull(processedInputDataSize, "processedInputDataSize is null"); + Preconditions.checkArgument(processedInputPositions >= 0, "processedInputPositions is negative"); + this.processedInputPositions = processedInputPositions; + + this.outputDataSize = checkNotNull(outputDataSize, "outputDataSize is null"); + Preconditions.checkArgument(outputPositions >= 0, "outputPositions is negative"); + this.outputPositions = outputPositions; + + this.operatorStats = ImmutableList.copyOf(checkNotNull(operatorStats, "operatorStats is null")); + } + + @JsonProperty + public DateTime getCreateTime() + { + return createTime; + } + + @Nullable + @JsonProperty + public DateTime getStartTime() + { + return startTime; + } + + @Nullable + @JsonProperty + public DateTime getEndTime() + { + return endTime; + } + + @JsonProperty + public Duration getQueuedTime() + { + return queuedTime; + } + + @JsonProperty + public Duration getElapsedTime() + { + return elapsedTime; + } + + @JsonProperty + public DataSize getMemoryReservation() + { + return memoryReservation; + } + + @JsonProperty + public Duration getTotalScheduledTime() + { + return totalScheduledTime; + } + + @JsonProperty + public Duration getTotalCpuTime() + { + return totalCpuTime; + } + + @JsonProperty + public Duration getTotalUserTime() + { + return totalUserTime; + } + + @JsonProperty + public Duration getTotalBlockedTime() + { + return totalBlockedTime; + } + + @JsonProperty + public boolean isFullyBlocked() + { + return fullyBlocked; + } + + @JsonProperty + public Set getBlockedReasons() + { + return blockedReasons; + } + + @JsonProperty + public DataSize getRawInputDataSize() + { + return rawInputDataSize; + } + + @JsonProperty + public Duration getRawInputReadTime() + { + return rawInputReadTime; + } + + @JsonProperty + public long getRawInputPositions() + { + return rawInputPositions; + } + + @JsonProperty + public DataSize getProcessedInputDataSize() + { + return processedInputDataSize; + } + + @JsonProperty + public long getProcessedInputPositions() + { + return processedInputPositions; + } + + @JsonProperty + public DataSize getOutputDataSize() + { + return outputDataSize; + } + + @JsonProperty + public long getOutputPositions() + { + return outputPositions; + } + + @JsonProperty + public List getOperatorStats() + { + return operatorStats; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/ExchangeClient.java b/presto-main/src/main/java/com/facebook/presto/operator/ExchangeClient.java new file mode 100644 index 00000000..e2cb7c39 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/ExchangeClient.java @@ -0,0 +1,405 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.operator.HttpPageBufferClient.ClientCallback; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.block.BlockEncodingSerde; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import io.airlift.http.client.HttpClient; +import io.airlift.units.DataSize; +import io.airlift.units.Duration; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.GuardedBy; +import javax.annotation.concurrent.ThreadSafe; + +import java.io.Closeable; +import java.net.URI; +import java.util.ArrayList; +import java.util.Deque; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.Sets.newConcurrentHashSet; + +@ThreadSafe +public class ExchangeClient + implements Closeable +{ + private static final Page NO_MORE_PAGES = new Page(0); + + private final BlockEncodingSerde blockEncodingSerde; + private final long maxBufferedBytes; + private final DataSize maxResponseSize; + private final int concurrentRequestMultiplier; + private final Duration minErrorDuration; + private final HttpClient httpClient; + private final ScheduledExecutorService executor; + + @GuardedBy("this") + private final Set locations = new HashSet<>(); + + @GuardedBy("this") + private boolean noMoreLocations; + + private final ConcurrentMap allClients = new ConcurrentHashMap<>(); + + @GuardedBy("this") + private final Deque queuedClients = new LinkedList<>(); + + private final Set completedClients = newConcurrentHashSet(); + private final LinkedBlockingDeque pageBuffer = new LinkedBlockingDeque<>(); + + @GuardedBy("this") + private final List> blockedCallers = new ArrayList<>(); + + @GuardedBy("this") + private long bufferBytes; + @GuardedBy("this") + private long successfulRequests; + @GuardedBy("this") + private long averageBytesPerRequest; + + private final AtomicBoolean closed = new AtomicBoolean(); + private final AtomicReference failure = new AtomicReference<>(); + + public ExchangeClient( + BlockEncodingSerde blockEncodingSerde, + DataSize maxBufferedBytes, + DataSize maxResponseSize, + int concurrentRequestMultiplier, + Duration minErrorDuration, + HttpClient httpClient, + ScheduledExecutorService executor) + { + this.blockEncodingSerde = blockEncodingSerde; + this.maxBufferedBytes = maxBufferedBytes.toBytes(); + this.maxResponseSize = maxResponseSize; + this.concurrentRequestMultiplier = concurrentRequestMultiplier; + this.minErrorDuration = minErrorDuration; + this.httpClient = httpClient; + this.executor = executor; + } + + public synchronized ExchangeClientStatus getStatus() + { + int bufferedPages = pageBuffer.size(); + if (bufferedPages > 0 && pageBuffer.peekLast() == NO_MORE_PAGES) { + bufferedPages--; + } + + ImmutableList.Builder exchangeStatus = ImmutableList.builder(); + for (HttpPageBufferClient client : allClients.values()) { + exchangeStatus.add(client.getStatus()); + } + return new ExchangeClientStatus(bufferBytes, averageBytesPerRequest, bufferedPages, noMoreLocations, exchangeStatus.build()); + } + + public synchronized void addLocation(URI location) + { + checkNotNull(location, "location is null"); + if (locations.contains(location)) { + return; + } + checkState(!noMoreLocations, "No more locations already set"); + locations.add(location); + scheduleRequestIfNecessary(); + } + + public synchronized void noMoreLocations() + { + noMoreLocations = true; + scheduleRequestIfNecessary(); + } + + @Nullable + public Page pollPage() + { + checkState(!Thread.holdsLock(this), "Can not get next page while holding a lock on this"); + + throwIfFailed(); + + if (closed.get()) { + return null; + } + + Page page = pageBuffer.poll(); + page = postProcessPage(page); + return page; + } + + @Nullable + public Page getNextPage(Duration maxWaitTime) + throws InterruptedException + { + checkState(!Thread.holdsLock(this), "Can not get next page while holding a lock on this"); + + throwIfFailed(); + + if (closed.get()) { + return null; + } + + scheduleRequestIfNecessary(); + + Page page = pageBuffer.poll(); + // only wait for a page if we have remote clients + if (page == null && maxWaitTime.toMillis() >= 1 && !allClients.isEmpty()) { + page = pageBuffer.poll(maxWaitTime.toMillis(), TimeUnit.MILLISECONDS); + } + + page = postProcessPage(page); + return page; + } + + private Page postProcessPage(Page page) + { + checkState(!Thread.holdsLock(this), "Can not get next page while holding a lock on this"); + + if (page == NO_MORE_PAGES) { + // mark client closed + closed.set(true); + + // add end marker back to queue + checkState(pageBuffer.add(NO_MORE_PAGES), "Could not add no more pages marker"); + notifyBlockedCallers(); + + // don't return end of stream marker + page = null; + } + + if (page != null) { + synchronized (this) { + bufferBytes -= page.getSizeInBytes(); + } + if (!closed.get() && pageBuffer.peek() == NO_MORE_PAGES) { + closed.set(true); + } + scheduleRequestIfNecessary(); + } + return page; + } + + public boolean isClosed() + { + return closed.get(); + } + + @Override + public synchronized void close() + { + closed.set(true); + for (HttpPageBufferClient client : allClients.values()) { + closeQuietly(client); + } + pageBuffer.clear(); + bufferBytes = 0; + if (pageBuffer.peekLast() != NO_MORE_PAGES) { + checkState(pageBuffer.add(NO_MORE_PAGES), "Could not add no more pages marker"); + } + notifyBlockedCallers(); + } + + public synchronized void scheduleRequestIfNecessary() + { + if (isClosed() || isFailed()) { + return; + } + + // if finished, add the end marker + if (noMoreLocations && completedClients.size() == locations.size()) { + if (pageBuffer.peekLast() != NO_MORE_PAGES) { + checkState(pageBuffer.add(NO_MORE_PAGES), "Could not add no more pages marker"); + } + if (!closed.get() && pageBuffer.peek() == NO_MORE_PAGES) { + closed.set(true); + } + notifyBlockedCallers(); + return; + } + + // add clients for new locations + for (URI location : locations) { + if (!allClients.containsKey(location)) { + HttpPageBufferClient client = new HttpPageBufferClient( + httpClient, + maxResponseSize, + minErrorDuration, + location, + new ExchangeClientCallback(), + blockEncodingSerde, + executor); + allClients.put(location, client); + queuedClients.add(client); + } + } + + long neededBytes = maxBufferedBytes - bufferBytes; + if (neededBytes <= 0) { + return; + } + + int clientCount = (int) ((1.0 * neededBytes / averageBytesPerRequest) * concurrentRequestMultiplier); + clientCount = Math.max(clientCount, 1); + + int pendingClients = allClients.size() - queuedClients.size() - completedClients.size(); + clientCount -= pendingClients; + + for (int i = 0; i < clientCount; i++) { + HttpPageBufferClient client = queuedClients.poll(); + if (client == null) { + // no more clients available + return; + } + client.scheduleRequest(); + } + } + + public synchronized ListenableFuture isBlocked() + { + if (isClosed() || isFailed() || pageBuffer.peek() != null) { + return Futures.immediateFuture(true); + } + SettableFuture future = SettableFuture.create(); + blockedCallers.add(future); + return future; + } + + private synchronized void addPage(Page page) + { + if (isClosed() || isFailed()) { + return; + } + + pageBuffer.add(page); + + // notify all blocked callers + notifyBlockedCallers(); + + bufferBytes += page.getSizeInBytes(); + successfulRequests++; + + // AVG_n = AVG_(n-1) * (n-1)/n + VALUE_n / n + averageBytesPerRequest = (long) (1.0 * averageBytesPerRequest * (successfulRequests - 1) / successfulRequests + page.getSizeInBytes() / successfulRequests); + + scheduleRequestIfNecessary(); + } + + private synchronized void notifyBlockedCallers() + { + List> callers = ImmutableList.copyOf(blockedCallers); + blockedCallers.clear(); + for (SettableFuture blockedCaller : callers) { + blockedCaller.set(null); + } + } + + private synchronized void requestComplete(HttpPageBufferClient client) + { + if (!queuedClients.contains(client)) { + queuedClients.add(client); + } + scheduleRequestIfNecessary(); + } + + private synchronized void clientFinished(HttpPageBufferClient client) + { + checkNotNull(client, "client is null"); + completedClients.add(client); + scheduleRequestIfNecessary(); + } + + private synchronized void clientFailed(Throwable cause) + { + // TODO: properly handle the failed vs closed state + // it is important not to treat failures as a successful close + if (!isClosed()) { + failure.compareAndSet(null, cause); + notifyBlockedCallers(); + } + } + + private boolean isFailed() + { + return failure.get() != null; + } + + private void throwIfFailed() + { + Throwable t = failure.get(); + if (t != null) { + throw Throwables.propagate(t); + } + } + + private class ExchangeClientCallback + implements ClientCallback + { + @Override + public void addPage(HttpPageBufferClient client, Page page) + { + checkNotNull(client, "client is null"); + checkNotNull(page, "page is null"); + ExchangeClient.this.addPage(page); + scheduleRequestIfNecessary(); + } + + @Override + public void requestComplete(HttpPageBufferClient client) + { + checkNotNull(client, "client is null"); + ExchangeClient.this.requestComplete(client); + } + + @Override + public void clientFinished(HttpPageBufferClient client) + { + ExchangeClient.this.clientFinished(client); + } + + @Override + public void clientFailed(HttpPageBufferClient client, Throwable cause) + { + checkNotNull(client, "client is null"); + checkNotNull(cause, "cause is null"); + ExchangeClient.this.clientFailed(cause); + } + } + + private static void closeQuietly(HttpPageBufferClient client) + { + try { + client.close(); + } + catch (RuntimeException e) { + // ignored + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/ExchangeClientConfig.java b/presto-main/src/main/java/com/facebook/presto/operator/ExchangeClientConfig.java new file mode 100644 index 00000000..147247e6 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/ExchangeClientConfig.java @@ -0,0 +1,101 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import io.airlift.configuration.Config; +import io.airlift.http.client.HttpClientConfig; +import io.airlift.units.DataSize; +import io.airlift.units.DataSize.Unit; +import io.airlift.units.Duration; +import io.airlift.units.MinDuration; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; + +import java.util.concurrent.TimeUnit; + +public class ExchangeClientConfig +{ + private DataSize maxBufferSize = new DataSize(32, Unit.MEGABYTE); + private int concurrentRequestMultiplier = 3; + private Duration minErrorDuration = new Duration(1, TimeUnit.MINUTES); + private DataSize maxResponseSize = new HttpClientConfig().getMaxContentLength(); + private int clientThreads = 25; + + @NotNull + public DataSize getMaxBufferSize() + { + return maxBufferSize; + } + + @Config("exchange.max-buffer-size") + public ExchangeClientConfig setMaxBufferSize(DataSize maxBufferSize) + { + this.maxBufferSize = maxBufferSize; + return this; + } + + @Min(1) + public int getConcurrentRequestMultiplier() + { + return concurrentRequestMultiplier; + } + + @Config("exchange.concurrent-request-multiplier") + public ExchangeClientConfig setConcurrentRequestMultiplier(int concurrentRequestMultiplier) + { + this.concurrentRequestMultiplier = concurrentRequestMultiplier; + return this; + } + + @NotNull + @MinDuration("1ms") + public Duration getMinErrorDuration() + { + return minErrorDuration; + } + + @Config("exchange.min-error-duration") + public ExchangeClientConfig setMinErrorDuration(Duration minErrorDuration) + { + this.minErrorDuration = minErrorDuration; + return this; + } + + @NotNull + public DataSize getMaxResponseSize() + { + return maxResponseSize; + } + + @Config("exchange.max-response-size") + public ExchangeClientConfig setMaxResponseSize(DataSize maxResponseSize) + { + this.maxResponseSize = maxResponseSize; + return this; + } + + @Min(1) + public int getClientThreads() + { + return clientThreads; + } + + @Config("exchange.client-threads") + public ExchangeClientConfig setClientThreads(int clientThreads) + { + this.clientThreads = clientThreads; + return this; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/ExchangeClientFactory.java b/presto-main/src/main/java/com/facebook/presto/operator/ExchangeClientFactory.java new file mode 100644 index 00000000..96832632 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/ExchangeClientFactory.java @@ -0,0 +1,89 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.block.BlockEncodingSerde; +import io.airlift.http.client.HttpClient; +import io.airlift.units.DataSize; +import io.airlift.units.Duration; + +import javax.inject.Inject; + +import java.util.concurrent.ScheduledExecutorService; +import java.util.function.Supplier; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class ExchangeClientFactory + implements Supplier +{ + private final BlockEncodingSerde blockEncodingSerde; + private final DataSize maxBufferedBytes; + private final int concurrentRequestMultiplier; + private final Duration minErrorDuration; + private final HttpClient httpClient; + private final DataSize maxResponseSize; + private final ScheduledExecutorService executor; + + @Inject + public ExchangeClientFactory(BlockEncodingSerde blockEncodingSerde, + ExchangeClientConfig config, + @ForExchange HttpClient httpClient, + @ForExchange ScheduledExecutorService executor) + { + this(blockEncodingSerde, + config.getMaxBufferSize(), + config.getMaxResponseSize(), + config.getConcurrentRequestMultiplier(), + config.getMinErrorDuration(), + httpClient, + executor); + } + + public ExchangeClientFactory( + BlockEncodingSerde blockEncodingSerde, + DataSize maxBufferedBytes, + DataSize maxResponseSize, + int concurrentRequestMultiplier, + Duration minErrorDuration, + HttpClient httpClient, + ScheduledExecutorService executor) + { + this.blockEncodingSerde = blockEncodingSerde; + this.maxBufferedBytes = checkNotNull(maxBufferedBytes, "maxBufferedBytes is null"); + this.concurrentRequestMultiplier = concurrentRequestMultiplier; + this.minErrorDuration = checkNotNull(minErrorDuration, "minErrorDuration is null"); + this.httpClient = checkNotNull(httpClient, "httpClient is null"); + this.maxResponseSize = checkNotNull(maxResponseSize, "maxResponseSize is null"); + this.executor = checkNotNull(executor, "executor is null"); + + checkArgument(maxBufferedBytes.toBytes() > 0, "maxBufferSize must be at least 1 byte: %s", maxBufferedBytes); + checkArgument(maxResponseSize.toBytes() > 0, "maxResponseSize must be at least 1 byte: %s", maxResponseSize); + checkArgument(concurrentRequestMultiplier > 0, "concurrentRequestMultiplier must be at least 1: %s", concurrentRequestMultiplier); + } + + @Override + public ExchangeClient get() + { + return new ExchangeClient( + blockEncodingSerde, + maxBufferedBytes, + maxResponseSize, + concurrentRequestMultiplier, + minErrorDuration, + httpClient, + executor); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/ExchangeClientStatus.java b/presto-main/src/main/java/com/facebook/presto/operator/ExchangeClientStatus.java new file mode 100644 index 00000000..3d3f857d --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/ExchangeClientStatus.java @@ -0,0 +1,90 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkNotNull; + +public class ExchangeClientStatus +{ + private final long bufferedBytes; + private final long averageBytesPerRequest; + private final int bufferedPages; + private final boolean noMoreLocations; + private final List pageBufferClientStatuses; + + @JsonCreator + public ExchangeClientStatus( + @JsonProperty("bufferedBytes") long bufferedBytes, + @JsonProperty("averageBytesPerRequest") long averageBytesPerRequest, + @JsonProperty("bufferedPages") int bufferedPages, + @JsonProperty("noMoreLocations") boolean noMoreLocations, + @JsonProperty("pageBufferClientStatuses") List pageBufferClientStatuses) + { + this.bufferedBytes = bufferedBytes; + this.averageBytesPerRequest = averageBytesPerRequest; + this.bufferedPages = bufferedPages; + this.noMoreLocations = noMoreLocations; + this.pageBufferClientStatuses = ImmutableList.copyOf(checkNotNull(pageBufferClientStatuses, "pageBufferClientStatuses is null")); + } + + @JsonProperty + public long getBufferedBytes() + { + return bufferedBytes; + } + + @JsonProperty + public long getAverageBytesPerRequest() + { + return averageBytesPerRequest; + } + + @JsonProperty + public int getBufferedPages() + { + return bufferedPages; + } + + @JsonProperty + public boolean isNoMoreLocations() + { + return noMoreLocations; + } + + @JsonProperty + + public List getPageBufferClientStatuses() + { + return pageBufferClientStatuses; + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("bufferBytes", bufferedBytes) + .add("averageBytesPerRequest", averageBytesPerRequest) + .add("bufferedPages", bufferedPages) + .add("noMoreLocations", noMoreLocations) + .add("pageBufferClientStatuses", pageBufferClientStatuses) + .toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/ExchangeOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/ExchangeOperator.java new file mode 100644 index 00000000..ac040f39 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/ExchangeOperator.java @@ -0,0 +1,190 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.metadata.Split; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.UpdatablePageSource; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.split.RemoteSplit; +import com.facebook.presto.sql.planner.plan.PlanNodeId; +import com.google.common.util.concurrent.ListenableFuture; + +import java.io.Closeable; +import java.net.URI; +import java.util.List; +import java.util.function.Supplier; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +public class ExchangeOperator + implements SourceOperator, Closeable +{ + public static class ExchangeOperatorFactory + implements SourceOperatorFactory + { + private final int operatorId; + private final PlanNodeId sourceId; + private final Supplier exchangeClientSupplier; + private final List types; + private boolean closed; + + public ExchangeOperatorFactory(int operatorId, PlanNodeId sourceId, Supplier exchangeClientSupplier, List types) + { + this.operatorId = operatorId; + this.sourceId = sourceId; + this.exchangeClientSupplier = exchangeClientSupplier; + this.types = types; + } + + @Override + public PlanNodeId getSourceId() + { + return sourceId; + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public SourceOperator createOperator(DriverContext driverContext) + { + checkState(!closed, "Factory is already closed"); + + OperatorContext operatorContext = driverContext.addOperatorContext(operatorId, ExchangeOperator.class.getSimpleName()); + return new ExchangeOperator( + operatorContext, + types, + sourceId, + exchangeClientSupplier.get()); + } + + @Override + public void close() + { + closed = true; + } + } + + private final OperatorContext operatorContext; + private final PlanNodeId sourceId; + private final ExchangeClient exchangeClient; + private final List types; + + public ExchangeOperator( + OperatorContext operatorContext, + List types, + PlanNodeId sourceId, + ExchangeClient exchangeClient) + { + this.operatorContext = checkNotNull(operatorContext, "operatorContext is null"); + this.sourceId = checkNotNull(sourceId, "sourceId is null"); + this.exchangeClient = checkNotNull(exchangeClient, "exchangeClient is null"); + this.types = checkNotNull(types, "types is null"); + + operatorContext.setInfoSupplier(exchangeClient::getStatus); + } + + @Override + public PlanNodeId getSourceId() + { + return sourceId; + } + + @Override + public Supplier> addSplit(Split split) + { + checkNotNull(split, "split is null"); + checkArgument(split.getConnectorId().equals("remote"), "split is not a remote split"); + + URI location = ((RemoteSplit) split.getConnectorSplit()).getLocation(); + exchangeClient.addLocation(location); + + return Optional::empty; + } + + @Override + public void noMoreSplits() + { + exchangeClient.noMoreLocations(); + } + + @Override + public OperatorContext getOperatorContext() + { + return operatorContext; + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public void finish() + { + close(); + } + + @Override + public boolean isFinished() + { + return exchangeClient.isClosed(); + } + + @Override + public ListenableFuture isBlocked() + { + ListenableFuture blocked = exchangeClient.isBlocked(); + if (blocked.isDone()) { + return NOT_BLOCKED; + } + return blocked; + } + + @Override + public boolean needsInput() + { + return false; + } + + @Override + public void addInput(Page page) + { + throw new UnsupportedOperationException(getClass().getName() + " can not take input"); + } + + @Override + public Page getOutput() + { + Page page = exchangeClient.pollPage(); + if (page != null) { + operatorContext.recordGeneratedInput(page.getSizeInBytes(), page.getPositionCount()); + } + return page; + } + + @Override + public void close() + { + exchangeClient.close(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/FilterAndProjectOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/FilterAndProjectOperator.java new file mode 100644 index 00000000..1425d933 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/FilterAndProjectOperator.java @@ -0,0 +1,142 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +public class FilterAndProjectOperator + implements Operator +{ + private final OperatorContext operatorContext; + private final List types; + + private final PageBuilder pageBuilder; + private final PageProcessor processor; + private Page currentPage; + private int currentPosition; + private boolean finishing; + + public FilterAndProjectOperator(OperatorContext operatorContext, Iterable types, PageProcessor processor) + { + this.processor = checkNotNull(processor, "processor is null"); + this.operatorContext = checkNotNull(operatorContext, "operatorContext is null"); + this.types = ImmutableList.copyOf(checkNotNull(types, "types is null")); + this.pageBuilder = new PageBuilder(getTypes()); + } + + @Override + public OperatorContext getOperatorContext() + { + return operatorContext; + } + + @Override + public final List getTypes() + { + return types; + } + + @Override + public final void finish() + { + finishing = true; + } + + @Override + public final boolean isFinished() + { + return finishing && pageBuilder.isEmpty() && currentPage == null; + } + + @Override + public final boolean needsInput() + { + return !finishing && !pageBuilder.isFull() && currentPage == null; + } + + @Override + public final void addInput(Page page) + { + checkState(!finishing, "Operator is already finishing"); + checkNotNull(page, "page is null"); + checkState(!pageBuilder.isFull(), "Page buffer is full"); + + currentPage = page; + currentPosition = 0; + } + + @Override + public final Page getOutput() + { + if (!pageBuilder.isFull() && currentPage != null) { + currentPosition = processor.process(operatorContext.getSession().toConnectorSession(), currentPage, currentPosition, currentPage.getPositionCount(), pageBuilder); + if (currentPosition == currentPage.getPositionCount()) { + currentPage = null; + currentPosition = 0; + } + } + + if (!finishing && !pageBuilder.isFull() || pageBuilder.isEmpty()) { + return null; + } + + Page page = pageBuilder.build(); + pageBuilder.reset(); + return page; + } + + public static class FilterAndProjectOperatorFactory + implements OperatorFactory + { + private final int operatorId; + private final PageProcessor processor; + private final List types; + private boolean closed; + + public FilterAndProjectOperatorFactory(int operatorId, PageProcessor processor, List types) + { + this.operatorId = operatorId; + this.processor = processor; + this.types = types; + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public Operator createOperator(DriverContext driverContext) + { + checkState(!closed, "Factory is already closed"); + OperatorContext operatorContext = driverContext.addOperatorContext(operatorId, FilterAndProjectOperator.class.getSimpleName()); + return new FilterAndProjectOperator(operatorContext, types, processor); + } + + @Override + public void close() + { + closed = true; + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/FilterFunction.java b/presto-main/src/main/java/com/facebook/presto/operator/FilterFunction.java new file mode 100644 index 00000000..e49bd645 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/FilterFunction.java @@ -0,0 +1,24 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.RecordCursor; +import com.facebook.presto.spi.block.Block; + +public interface FilterFunction +{ + boolean filter(int position, Block... blocks); + + boolean filter(RecordCursor cursor); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/FilterFunctions.java b/presto-main/src/main/java/com/facebook/presto/operator/FilterFunctions.java new file mode 100644 index 00000000..a2be7b91 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/FilterFunctions.java @@ -0,0 +1,46 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.RecordCursor; +import com.facebook.presto.spi.block.Block; + +public final class FilterFunctions +{ + public static final FilterFunction TRUE_FUNCTION = new TrueFilterFunction(); + + private FilterFunctions() {} + + private static class TrueFilterFunction + implements FilterFunction + { + @Override + public boolean filter(int position, Block... blocks) + { + return true; + } + + @Override + public boolean filter(RecordCursor cursor) + { + return true; + } + + @Override + public String toString() + { + return "TRUE"; + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/FinishedOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/FinishedOperator.java new file mode 100644 index 00000000..f8f9ed11 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/FinishedOperator.java @@ -0,0 +1,75 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.type.Type; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class FinishedOperator + implements Operator +{ + private final OperatorContext operatorContext; + private final List types; + + public FinishedOperator(OperatorContext operatorContext, List types) + { + this.operatorContext = checkNotNull(operatorContext, "operatorContext is null"); + this.types = checkNotNull(types, "types is null"); + } + + @Override + public OperatorContext getOperatorContext() + { + return operatorContext; + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public void finish() + { + } + + @Override + public boolean isFinished() + { + return true; + } + + @Override + public boolean needsInput() + { + return false; + } + + @Override + public void addInput(Page page) + { + throw new UnsupportedOperationException(); + } + + @Override + public Page getOutput() + { + return null; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/ForExchange.java b/presto-main/src/main/java/com/facebook/presto/operator/ForExchange.java new file mode 100644 index 00000000..f49adfe1 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/ForExchange.java @@ -0,0 +1,31 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import javax.inject.Qualifier; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention(RUNTIME) +@Target({FIELD, PARAMETER, METHOD}) +@Qualifier +public @interface ForExchange +{ +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/ForScheduler.java b/presto-main/src/main/java/com/facebook/presto/operator/ForScheduler.java new file mode 100644 index 00000000..7d33c100 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/ForScheduler.java @@ -0,0 +1,31 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import javax.inject.Qualifier; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention(RUNTIME) +@Target({FIELD, PARAMETER, METHOD}) +@Qualifier +public @interface ForScheduler +{ +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/GenericCursorProcessor.java b/presto-main/src/main/java/com/facebook/presto/operator/GenericCursorProcessor.java new file mode 100644 index 00000000..1431f84f --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/GenericCursorProcessor.java @@ -0,0 +1,63 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.RecordCursor; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkArgument; + +public class GenericCursorProcessor + implements CursorProcessor +{ + private final FilterFunction filterFunction; + private final List projections; + + public GenericCursorProcessor(FilterFunction filterFunction, Iterable projections) + { + this.filterFunction = filterFunction; + this.projections = ImmutableList.copyOf(projections); + } + + @Override + public int process(ConnectorSession session, RecordCursor cursor, int count, PageBuilder pageBuilder) + { + checkArgument(!pageBuilder.isFull(), "page builder can't be full"); + checkArgument(count > 0, "count must be > 0"); + + int position = 0; + for (; position < count; position++) { + if (pageBuilder.isFull()) { + break; + } + + if (!cursor.advanceNextPosition()) { + break; + } + + if (filterFunction.filter(cursor)) { + pageBuilder.declarePosition(); + for (int channel = 0; channel < projections.size(); channel++) { + // todo: if the projection function increases the size of the data significantly, this could cause the servers to OOM + projections.get(channel).project(cursor, pageBuilder.getBlockBuilder(channel)); + } + } + } + return position; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/GenericPageProcessor.java b/presto-main/src/main/java/com/facebook/presto/operator/GenericPageProcessor.java new file mode 100644 index 00000000..6cbbcedb --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/GenericPageProcessor.java @@ -0,0 +1,51 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PageBuilder; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +public class GenericPageProcessor + implements PageProcessor +{ + private final FilterFunction filterFunction; + private final List projections; + + public GenericPageProcessor(FilterFunction filterFunction, Iterable projections) + { + this.filterFunction = filterFunction; + this.projections = ImmutableList.copyOf(projections); + } + + @Override + public int process(ConnectorSession session, Page page, int start, int end, PageBuilder pageBuilder) + { + int position = start; + for (; position < end && !pageBuilder.isFull(); position++) { + if (filterFunction.filter(position, page.getBlocks())) { + pageBuilder.declarePosition(); + for (int i = 0; i < projections.size(); i++) { + // todo: if the projection function increases the size of the data significantly, this could cause the servers to OOM + projections.get(i).project(position, page.getBlocks(), pageBuilder.getBlockBuilder(i)); + } + } + } + + return position; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/GroupByHash.java b/presto-main/src/main/java/com/facebook/presto/operator/GroupByHash.java new file mode 100644 index 00000000..cd4bf1d0 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/GroupByHash.java @@ -0,0 +1,50 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.type.Type; + +import java.util.List; +import java.util.Optional; + +import static com.facebook.presto.spi.type.BigintType.BIGINT; + +public interface GroupByHash +{ + static GroupByHash createGroupByHash(List hashTypes, int[] hashChannels, Optional maskChannel, Optional inputHashChannel, int expectedSize) + { + if (hashTypes.size() == 1 && hashTypes.get(0).equals(BIGINT) && hashChannels.length == 1) { + return new BigintGroupByHash(hashChannels[0], maskChannel, inputHashChannel.isPresent(), expectedSize); + } + return new MultiChannelGroupByHash(hashTypes, hashChannels, maskChannel, inputHashChannel, expectedSize); + } + + long getEstimatedSize(); + + List getTypes(); + + int getGroupCount(); + + void appendValuesTo(int groupId, PageBuilder pageBuilder, int outputChannelOffset); + + void addPage(Page page); + + GroupByIdBlock getGroupIds(Page page); + + boolean contains(int position, Page page); + + int putIfAbsent(int position, Page page); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/GroupByIdBlock.java b/presto-main/src/main/java/com/facebook/presto/operator/GroupByIdBlock.java new file mode 100644 index 00000000..e8fb1536 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/GroupByIdBlock.java @@ -0,0 +1,193 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.block.BlockEncoding; +import io.airlift.slice.Slice; + +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkNotNull; + +public class GroupByIdBlock + implements Block +{ + private final long groupCount; + private final Block block; + + public GroupByIdBlock(long groupCount, Block block) + { + checkNotNull(block, "block is null"); + this.groupCount = groupCount; + this.block = block; + } + + public long getGroupCount() + { + return groupCount; + } + + public long getGroupId(int position) + { + return BIGINT.getLong(block, position); + } + + @Override + public Block getRegion(int positionOffset, int length) + { + return block.getRegion(positionOffset, length); + } + + @Override + public Block copyRegion(int positionOffset, int length) + { + return block.copyRegion(positionOffset, length); + } + + @Override + public int getLength(int position) + { + return block.getLength(position); + } + + @Override + public byte getByte(int position, int offset) + { + return block.getByte(position, offset); + } + + @Override + public short getShort(int position, int offset) + { + return block.getShort(position, offset); + } + + @Override + public int getInt(int position, int offset) + { + return block.getInt(position, offset); + } + + @Override + public long getLong(int position, int offset) + { + return block.getLong(position, offset); + } + + @Override + public float getFloat(int position, int offset) + { + return block.getFloat(position, offset); + } + + @Override + public double getDouble(int position, int offset) + { + return block.getDouble(position, offset); + } + + @Override + public Slice getSlice(int position, int offset, int length) + { + return block.getSlice(position, offset, length); + } + + @Override + public boolean bytesEqual(int position, int offset, Slice otherSlice, int otherOffset, int length) + { + return block.bytesEqual(position, offset, otherSlice, otherOffset, length); + } + + @Override + public int bytesCompare(int position, int offset, int length, Slice otherSlice, int otherOffset, int otherLength) + { + return block.bytesCompare(position, offset, length, otherSlice, otherOffset, otherLength); + } + + @Override + public void writeBytesTo(int position, int offset, int length, BlockBuilder blockBuilder) + { + block.writeBytesTo(position, offset, length, blockBuilder); + } + + @Override + public boolean equals(int position, int offset, Block otherBlock, int otherPosition, int otherOffset, int length) + { + return block.equals(position, offset, otherBlock, otherPosition, otherOffset, length); + } + + @Override + public int hash(int position, int offset, int length) + { + return block.hash(position, offset, length); + } + + @Override + public int compareTo(int leftPosition, int leftOffset, int leftLength, Block rightBlock, int rightPosition, int rightOffset, int rightLength) + { + return block.compareTo(leftPosition, leftOffset, leftLength, rightBlock, rightPosition, rightOffset, rightLength); + } + + @Override + public Block getSingleValueBlock(int position) + { + return block.getSingleValueBlock(position); + } + + @Override + public boolean isNull(int position) + { + return block.isNull(position); + } + + @Override + public int getPositionCount() + { + return block.getPositionCount(); + } + + @Override + public int getSizeInBytes() + { + return block.getSizeInBytes(); + } + + @Override + public int getRetainedSizeInBytes() + { + return block.getRetainedSizeInBytes(); + } + + @Override + public BlockEncoding getEncoding() + { + return block.getEncoding(); + } + + @Override + public void assureLoaded() + { + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("groupCount", groupCount) + .add("positionCount", getPositionCount()) + .toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/HashAggregationOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/HashAggregationOperator.java new file mode 100644 index 00000000..0e9a6907 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/HashAggregationOperator.java @@ -0,0 +1,417 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.operator.aggregation.AccumulatorFactory; +import com.facebook.presto.operator.aggregation.GroupedAccumulator; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.planner.plan.AggregationNode.Step; +import com.google.common.collect.AbstractIterator; +import com.google.common.collect.ImmutableList; +import com.google.common.primitives.Ints; +import io.airlift.units.DataSize; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; + +import static com.facebook.presto.operator.GroupByHash.createGroupByHash; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +public class HashAggregationOperator + implements Operator +{ + public static class HashAggregationOperatorFactory + implements OperatorFactory + { + private final int operatorId; + private final Optional maskChannel; + private final List groupByTypes; + private final List groupByChannels; + private final Step step; + private final List accumulatorFactories; + private final Optional hashChannel; + + private final int expectedGroups; + private final List types; + private boolean closed; + private final long maxPartialMemory; + + public HashAggregationOperatorFactory( + int operatorId, + List groupByTypes, + List groupByChannels, + Step step, + List accumulatorFactories, + Optional maskChannel, + Optional hashChannel, + int expectedGroups, + DataSize maxPartialMemory) + { + this.operatorId = operatorId; + this.maskChannel = checkNotNull(maskChannel, "maskChannel is null"); + this.hashChannel = checkNotNull(hashChannel, "hashChannel is null"); + this.groupByTypes = ImmutableList.copyOf(groupByTypes); + this.groupByChannels = ImmutableList.copyOf(groupByChannels); + this.step = step; + this.accumulatorFactories = ImmutableList.copyOf(accumulatorFactories); + this.expectedGroups = expectedGroups; + this.maxPartialMemory = checkNotNull(maxPartialMemory, "maxPartialMemory is null").toBytes(); + + this.types = toTypes(groupByTypes, step, accumulatorFactories, hashChannel); + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public Operator createOperator(DriverContext driverContext) + { + checkState(!closed, "Factory is already closed"); + + OperatorContext operatorContext; + if (step == Step.PARTIAL) { + operatorContext = driverContext.addOperatorContext(operatorId, HashAggregationOperator.class.getSimpleName(), maxPartialMemory); + } + else { + operatorContext = driverContext.addOperatorContext(operatorId, HashAggregationOperator.class.getSimpleName()); + } + HashAggregationOperator hashAggregationOperator = new HashAggregationOperator( + operatorContext, + groupByTypes, + groupByChannels, + step, + accumulatorFactories, + maskChannel, + hashChannel, + expectedGroups); + return hashAggregationOperator; + } + + @Override + public void close() + { + closed = true; + } + } + + private final OperatorContext operatorContext; + private final List groupByTypes; + private final List groupByChannels; + private final Step step; + private final List accumulatorFactories; + private final Optional maskChannel; + private final Optional hashChannel; + private final int expectedGroups; + + private final List types; + + private GroupByHashAggregationBuilder aggregationBuilder; + private Iterator outputIterator; + private boolean finishing; + + public HashAggregationOperator( + OperatorContext operatorContext, + List groupByTypes, + List groupByChannels, + Step step, + List accumulatorFactories, + Optional maskChannel, + Optional hashChannel, + int expectedGroups) + { + this.operatorContext = checkNotNull(operatorContext, "operatorContext is null"); + checkNotNull(step, "step is null"); + checkNotNull(accumulatorFactories, "accumulatorFactories is null"); + checkNotNull(operatorContext, "operatorContext is null"); + + this.groupByTypes = ImmutableList.copyOf(groupByTypes); + this.groupByChannels = ImmutableList.copyOf(groupByChannels); + this.accumulatorFactories = ImmutableList.copyOf(accumulatorFactories); + this.maskChannel = checkNotNull(maskChannel, "maskChannel is null"); + this.hashChannel = checkNotNull(hashChannel, "hashChannel is null"); + this.step = step; + this.expectedGroups = expectedGroups; + this.types = toTypes(groupByTypes, step, accumulatorFactories, hashChannel); + } + + @Override + public OperatorContext getOperatorContext() + { + return operatorContext; + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public void finish() + { + finishing = true; + } + + @Override + public boolean isFinished() + { + return finishing && aggregationBuilder == null && (outputIterator == null || !outputIterator.hasNext()); + } + + @Override + public boolean needsInput() + { + return !finishing && outputIterator == null && (aggregationBuilder == null || !aggregationBuilder.isFull()); + } + + @Override + public void addInput(Page page) + { + checkState(!finishing, "Operator is already finishing"); + checkNotNull(page, "page is null"); + if (aggregationBuilder == null) { + aggregationBuilder = new GroupByHashAggregationBuilder( + accumulatorFactories, + step, + expectedGroups, + groupByTypes, + groupByChannels, + maskChannel, + hashChannel, + operatorContext); + + // assume initial aggregationBuilder is not full + } + else { + checkState(!aggregationBuilder.isFull(), "Aggregation buffer is full"); + } + aggregationBuilder.processPage(page); + } + + @Override + public Page getOutput() + { + if (outputIterator == null || !outputIterator.hasNext()) { + // current output iterator is done + outputIterator = null; + + // no data + if (aggregationBuilder == null) { + return null; + } + + // only flush if we are finishing or the aggregation builder is full + if (!finishing && !aggregationBuilder.isFull()) { + return null; + } + + outputIterator = aggregationBuilder.build(); + aggregationBuilder = null; + + if (!outputIterator.hasNext()) { + // current output iterator is done + outputIterator = null; + return null; + } + } + + return outputIterator.next(); + } + + private static List toTypes(List groupByType, Step step, List factories, Optional hashChannel) + { + ImmutableList.Builder types = ImmutableList.builder(); + types.addAll(groupByType); + if (hashChannel.isPresent()) { + types.add(BIGINT); + } + for (AccumulatorFactory factory : factories) { + types.add(new Aggregator(factory, step).getType()); + } + return types.build(); + } + + private static class GroupByHashAggregationBuilder + { + private final GroupByHash groupByHash; + private final List aggregators; + private final OperatorContext operatorContext; + private final boolean partial; + + private GroupByHashAggregationBuilder( + List accumulatorFactories, + Step step, + int expectedGroups, + List groupByTypes, + List groupByChannels, + Optional maskChannel, + Optional hashChannel, + OperatorContext operatorContext) + { + this.groupByHash = createGroupByHash(groupByTypes, Ints.toArray(groupByChannels), maskChannel, hashChannel, expectedGroups); + this.operatorContext = operatorContext; + this.partial = (step == Step.PARTIAL); + + // wrapper each function with an aggregator + ImmutableList.Builder builder = ImmutableList.builder(); + checkNotNull(accumulatorFactories, "accumulatorFactories is null"); + for (int i = 0; i < accumulatorFactories.size(); i++) { + AccumulatorFactory accumulatorFactory = accumulatorFactories.get(i); + builder.add(new Aggregator(accumulatorFactory, step)); + } + aggregators = builder.build(); + } + + private void processPage(Page page) + { + if (aggregators.isEmpty()) { + groupByHash.addPage(page); + return; + } + + GroupByIdBlock groupIds = groupByHash.getGroupIds(page); + + for (Aggregator aggregator : aggregators) { + aggregator.processPage(groupIds, page); + } + } + + public boolean isFull() + { + long memorySize = groupByHash.getEstimatedSize(); + for (Aggregator aggregator : aggregators) { + memorySize += aggregator.getEstimatedSize(); + } + memorySize -= operatorContext.getOperatorPreAllocatedMemory().toBytes(); + if (memorySize < 0) { + memorySize = 0; + } + if (partial) { + return !operatorContext.trySetMemoryReservation(memorySize); + } + else { + operatorContext.setMemoryReservation(memorySize); + return false; + } + } + + public Iterator build() + { + List types = new ArrayList<>(groupByHash.getTypes()); + for (Aggregator aggregator : aggregators) { + types.add(aggregator.getType()); + } + + final PageBuilder pageBuilder = new PageBuilder(types); + return new AbstractIterator() + { + private final int groupCount = groupByHash.getGroupCount(); + private int groupId; + + @Override + protected Page computeNext() + { + if (groupId >= groupCount) { + return endOfData(); + } + + pageBuilder.reset(); + + List types = groupByHash.getTypes(); + while (!pageBuilder.isFull() && groupId < groupCount) { + groupByHash.appendValuesTo(groupId, pageBuilder, 0); + + pageBuilder.declarePosition(); + for (int i = 0; i < aggregators.size(); i++) { + Aggregator aggregator = aggregators.get(i); + BlockBuilder output = pageBuilder.getBlockBuilder(types.size() + i); + aggregator.evaluate(groupId, output); + } + + groupId++; + } + + return pageBuilder.build(); + } + }; + } + } + + private static class Aggregator + { + private final GroupedAccumulator aggregation; + private final Step step; + private final int intermediateChannel; + + private Aggregator(AccumulatorFactory accumulatorFactory, Step step) + { + if (step == Step.FINAL) { + checkArgument(accumulatorFactory.getInputChannels().size() == 1, "expected 1 input channel for intermediate aggregation"); + intermediateChannel = accumulatorFactory.getInputChannels().get(0); + aggregation = accumulatorFactory.createGroupedIntermediateAccumulator(); + } + else { + intermediateChannel = -1; + aggregation = accumulatorFactory.createGroupedAccumulator(); + } + this.step = step; + } + + public long getEstimatedSize() + { + return aggregation.getEstimatedSize(); + } + + public Type getType() + { + if (step == Step.PARTIAL) { + return aggregation.getIntermediateType(); + } + else { + return aggregation.getFinalType(); + } + } + + public void processPage(GroupByIdBlock groupIds, Page page) + { + if (step == Step.FINAL) { + aggregation.addIntermediate(groupIds, page.getBlock(intermediateChannel)); + } + else { + aggregation.addInput(groupIds, page); + } + } + + public void evaluate(int groupId, BlockBuilder output) + { + if (step == Step.PARTIAL) { + aggregation.evaluateIntermediate(groupId, output); + } + else { + aggregation.evaluateFinal(groupId, output); + } + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/HashBuilderOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/HashBuilderOperator.java new file mode 100644 index 00000000..2311b792 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/HashBuilderOperator.java @@ -0,0 +1,175 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.type.Type; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; + +import javax.annotation.concurrent.ThreadSafe; + +import java.util.List; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +@ThreadSafe +public class HashBuilderOperator + implements Operator +{ + public static class HashBuilderOperatorFactory + implements OperatorFactory + { + private final int operatorId; + private final SettableLookupSourceSupplier lookupSourceSupplier; + private final List hashChannels; + private final Optional hashChannel; + + private final int expectedPositions; + private boolean closed; + + public HashBuilderOperatorFactory( + int operatorId, + List types, + List hashChannels, + Optional hashChannel, + int expectedPositions) + { + this.operatorId = operatorId; + this.lookupSourceSupplier = new SettableLookupSourceSupplier(checkNotNull(types, "types is null")); + + Preconditions.checkArgument(!hashChannels.isEmpty(), "hashChannels is empty"); + this.hashChannels = ImmutableList.copyOf(checkNotNull(hashChannels, "hashChannels is null")); + this.hashChannel = checkNotNull(hashChannel, "hashChannel is null"); + + this.expectedPositions = expectedPositions; + } + + public LookupSourceSupplier getLookupSourceSupplier() + { + return lookupSourceSupplier; + } + + @Override + public List getTypes() + { + return lookupSourceSupplier.getTypes(); + } + + @Override + public Operator createOperator(DriverContext driverContext) + { + checkState(!closed, "Factory is already closed"); + OperatorContext operatorContext = driverContext.addOperatorContext(operatorId, HashBuilderOperator.class.getSimpleName()); + return new HashBuilderOperator( + operatorContext, + lookupSourceSupplier, + hashChannels, + hashChannel, + expectedPositions); + } + + @Override + public void close() + { + closed = true; + } + } + + private final OperatorContext operatorContext; + private final SettableLookupSourceSupplier lookupSourceSupplier; + private final List hashChannels; + private final Optional hashChannel; + + private final PagesIndex pagesIndex; + + private boolean finished; + + public HashBuilderOperator( + OperatorContext operatorContext, + SettableLookupSourceSupplier lookupSourceSupplier, + List hashChannels, + Optional hashChannel, + int expectedPositions) + { + this.operatorContext = checkNotNull(operatorContext, "operatorContext is null"); + + this.lookupSourceSupplier = checkNotNull(lookupSourceSupplier, "hashSupplier is null"); + + Preconditions.checkArgument(!hashChannels.isEmpty(), "hashChannels is empty"); + this.hashChannels = ImmutableList.copyOf(checkNotNull(hashChannels, "hashChannels is null")); + this.hashChannel = checkNotNull(hashChannel, "hashChannel is null"); + + this.pagesIndex = new PagesIndex(lookupSourceSupplier.getTypes(), expectedPositions); + } + + @Override + public OperatorContext getOperatorContext() + { + return operatorContext; + } + + @Override + public List getTypes() + { + return lookupSourceSupplier.getTypes(); + } + + @Override + public void finish() + { + if (finished) { + return; + } + + // Free memory, as the SharedLookupSource is going to take it over + operatorContext.setMemoryReservation(0); + lookupSourceSupplier.setLookupSource(new SharedLookupSource(pagesIndex.createLookupSource(hashChannels, hashChannel), operatorContext.getDriverContext().getPipelineContext().getTaskContext())); + finished = true; + } + + @Override + public boolean isFinished() + { + return finished; + } + + @Override + public boolean needsInput() + { + return !finished; + } + + @Override + public void addInput(Page page) + { + checkNotNull(page, "page is null"); + checkState(!isFinished(), "Operator is already finished"); + + pagesIndex.addPage(page); + if (!operatorContext.trySetMemoryReservation(pagesIndex.getEstimatedSize().toBytes())) { + pagesIndex.compact(); + } + operatorContext.setMemoryReservation(pagesIndex.getEstimatedSize().toBytes()); + operatorContext.recordGeneratedOutput(page.getSizeInBytes(), page.getPositionCount()); + } + + @Override + public Page getOutput() + { + return null; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/HashGenerator.java b/presto-main/src/main/java/com/facebook/presto/operator/HashGenerator.java new file mode 100644 index 00000000..269d26ab --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/HashGenerator.java @@ -0,0 +1,55 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; + +import java.util.List; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkState; + +public interface HashGenerator +{ + static HashGenerator createHashGenerator(Optional hashChannel, List partitioningChannels, List types) + { + if (hashChannel.isPresent()) { + return new PrecomputedHashGenerator(hashChannel.get()); + } + ImmutableList.Builder hashTypes = ImmutableList.builder(); + int[] hashChannels = new int[partitioningChannels.size()]; + for (int i = 0; i < partitioningChannels.size(); i++) { + int channel = partitioningChannels.get(i); + hashTypes.add(types.get(channel)); + hashChannels[i] = channel; + } + return new InterpretedHashGenerator(hashTypes.build(), hashChannels); + } + + int hashPosition(int position, Page page); + + default int getPartitionHashBucket(int partitionCount, int position, Page page) + { + int rawHash = hashPosition(position, page); + + // clear the sign bit + rawHash &= 0x7fff_ffffL; + + int bucket = rawHash % partitionCount; + checkState(bucket >= 0 && bucket < partitionCount); + return bucket; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/HashPartitionMaskOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/HashPartitionMaskOperator.java new file mode 100644 index 00000000..2146164d --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/HashPartitionMaskOperator.java @@ -0,0 +1,236 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.block.BlockBuilderStatus; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; +import com.google.common.primitives.Ints; +import io.airlift.slice.XxHash64; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +import static com.facebook.presto.spi.type.BooleanType.BOOLEAN; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static java.lang.Math.abs; + +public class HashPartitionMaskOperator + implements Operator +{ + public static class HashPartitionMaskOperatorFactory + implements OperatorFactory + { + private final int operatorId; + private final int partitionCount; + private final Optional hashChannel; + private final int[] maskChannels; + private final int[] partitionChannels; + private final List types; + private int partition; + private boolean closed; + + public HashPartitionMaskOperatorFactory( + int operatorId, + int partitionCount, + List sourceTypes, + Collection maskChannels, + Collection partitionChannels, + Optional hashChannel) + { + this.operatorId = operatorId; + checkArgument(partitionCount > 1, "partition count must be greater than 1"); + this.partitionCount = partitionCount; + checkNotNull(maskChannels, "maskChannels is null"); + this.maskChannels = Ints.toArray(maskChannels); + + checkNotNull(partitionChannels, "partitionChannels is null"); + checkArgument(!partitionChannels.isEmpty(), "partitionChannels is empty"); + this.partitionChannels = Ints.toArray(partitionChannels); + + this.hashChannel = checkNotNull(hashChannel, "hashChannel is null"); + + this.types = ImmutableList.builder() + .addAll(sourceTypes) + .add(BOOLEAN) + .build(); + } + + @Override + public List getTypes() + { + return types; + } + + public int getDefaultMaskChannel() + { + // default mask is in the last channel + return types.size() - 1; + } + + @Override + public Operator createOperator(DriverContext driverContext) + { + checkState(!closed, "Factory is already closed"); + checkState(partition < partitionCount, "All operators already created"); + OperatorContext operatorContext = driverContext.addOperatorContext(operatorId, MarkDistinctOperator.class.getSimpleName()); + return new HashPartitionMaskOperator(operatorContext, partition++, partitionCount, types, maskChannels, partitionChannels, hashChannel); + } + + @Override + public void close() + { + closed = true; + } + } + + private final OperatorContext operatorContext; + private final int partition; + private final int partitionCount; + private final List types; + private final int[] maskChannels; + private final HashGenerator hashGenerator; + + private Page outputPage; + private boolean finishing; + + public HashPartitionMaskOperator(OperatorContext operatorContext, + int partition, + int partitionCount, + List types, + int[] maskChannels, + int[] partitionChannels, + Optional hashChannel) + { + this.partition = partition; + this.partitionCount = partitionCount; + this.operatorContext = checkNotNull(operatorContext, "operatorContext is null"); + this.types = ImmutableList.copyOf(checkNotNull(types, "types is null")); + this.maskChannels = maskChannels; + + checkNotNull(hashChannel, "hashChannel is null"); + + ImmutableList.Builder distinctTypes = ImmutableList.builder(); + for (int channel : partitionChannels) { + distinctTypes.add(types.get(channel)); + } + + ImmutableList.Builder partitionChannelTypes = ImmutableList.builder(); + for (int channel : partitionChannels) { + partitionChannelTypes.add(types.get(channel)); + } + + if (hashChannel.isPresent()) { + this.hashGenerator = new PrecomputedHashGenerator(hashChannel.get()); + } + else { + this.hashGenerator = new InterpretedHashGenerator(partitionChannelTypes.build(), partitionChannels); + } + } + + @Override + public OperatorContext getOperatorContext() + { + return operatorContext; + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public void finish() + { + finishing = true; + } + + @Override + public boolean isFinished() + { + return finishing && outputPage == null; + } + + @Override + public boolean needsInput() + { + if (finishing || outputPage != null) { + return false; + } + return true; + } + + @Override + public void addInput(Page page) + { + checkNotNull(page, "page is null"); + checkState(!finishing, "Operator is finishing"); + checkState(outputPage == null, "Operator still has pending output"); + + BlockBuilder activePositions = BOOLEAN.createBlockBuilder(new BlockBuilderStatus(), page.getPositionCount()); + BlockBuilder[] maskBuilders = new BlockBuilder[maskChannels.length]; + for (int i = 0; i < maskBuilders.length; i++) { + maskBuilders[i] = BOOLEAN.createBlockBuilder(new BlockBuilderStatus(), page.getPositionCount()); + } + for (int position = 0; position < page.getPositionCount(); position++) { + int rawHash = hashGenerator.hashPosition(position, page); + // mix the bits so we don't use the same hash used to distribute between stages + rawHash = abs((int) XxHash64.hash(Integer.reverse(rawHash))); + + boolean active = (rawHash % partitionCount == partition); + BOOLEAN.writeBoolean(activePositions, active); + + for (int i = 0; i < maskBuilders.length; i++) { + Block maskBlock = page.getBlock(maskChannels[i]); + if (maskBlock.isNull(position)) { + maskBuilders[i].appendNull(); + } + else { + boolean maskValue = active && BOOLEAN.getBoolean(maskBlock, position); + BOOLEAN.writeBoolean(maskBuilders[i], maskValue); + } + } + } + + // build output page + Block[] sourceBlocks = page.getBlocks(); + Block[] outputBlocks = new Block[sourceBlocks.length + 1]; // +1 for the single boolean output channel + System.arraycopy(sourceBlocks, 0, outputBlocks, 0, sourceBlocks.length); + + // add the new boolean column to the page + outputBlocks[sourceBlocks.length] = activePositions.build(); + + // replace mask blocks + for (int i = 0; i < maskBuilders.length; i++) { + outputBlocks[maskChannels[i]] = maskBuilders[i].build(); + } + + outputPage = new Page(outputBlocks); + } + + @Override + public Page getOutput() + { + Page result = outputPage; + outputPage = null; + return result; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/HashSemiJoinOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/HashSemiJoinOperator.java new file mode 100644 index 00000000..0d47dc95 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/HashSemiJoinOperator.java @@ -0,0 +1,203 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.operator.SetBuilderOperator.SetSupplier; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.ListenableFuture; + +import java.util.List; +import java.util.Optional; + +import static com.facebook.presto.spi.type.BooleanType.BOOLEAN; +import static com.facebook.presto.util.MoreFutures.tryGetUnchecked; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +public class HashSemiJoinOperator + implements Operator +{ + private final OperatorContext operatorContext; + + public static class HashSemiJoinOperatorFactory + implements OperatorFactory + { + private final int operatorId; + private final SetSupplier setSupplier; + private final List probeTypes; + private final int probeJoinChannel; + private final Optional probeHashChannel; + private final List types; + private boolean closed; + + public HashSemiJoinOperatorFactory(int operatorId, SetSupplier setSupplier, List probeTypes, int probeJoinChannel, Optional probeHashChannel) + { + this.operatorId = operatorId; + this.setSupplier = setSupplier; + this.probeTypes = ImmutableList.copyOf(probeTypes); + checkArgument(probeJoinChannel >= 0, "probeJoinChannel is negative"); + this.probeJoinChannel = probeJoinChannel; + this.probeHashChannel = checkNotNull(probeHashChannel, "probeHashChannel is null"); + + this.types = ImmutableList.builder() + .addAll(probeTypes) + .add(BOOLEAN) + .build(); + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public Operator createOperator(DriverContext driverContext) + { + checkState(!closed, "Factory is already closed"); + OperatorContext operatorContext = driverContext.addOperatorContext(operatorId, HashBuilderOperator.class.getSimpleName()); + return new HashSemiJoinOperator(operatorContext, setSupplier, probeTypes, probeJoinChannel, probeHashChannel); + } + + @Override + public void close() + { + closed = true; + } + } + + private final int probeJoinChannel; + private final Optional probeHashChannel; + private final List types; + private final ListenableFuture channelSetFuture; + + private ChannelSet channelSet; + private Page outputPage; + private boolean finishing; + + public HashSemiJoinOperator(OperatorContext operatorContext, SetSupplier channelSetFuture, List probeTypes, int probeJoinChannel, Optional probeHashChannel) + { + this.operatorContext = checkNotNull(operatorContext, "operatorContext is null"); + + // todo pass in desired projection + checkNotNull(channelSetFuture, "hashProvider is null"); + checkNotNull(probeTypes, "probeTypes is null"); + checkArgument(probeJoinChannel >= 0, "probeJoinChannel is negative"); + + this.channelSetFuture = channelSetFuture.getChannelSet(); + this.probeJoinChannel = probeJoinChannel; + this.probeHashChannel = probeHashChannel; + + this.types = ImmutableList.builder() + .addAll(probeTypes) + .add(BOOLEAN) + .build(); + } + + @Override + public OperatorContext getOperatorContext() + { + return operatorContext; + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public void finish() + { + finishing = true; + } + + @Override + public boolean isFinished() + { + return finishing && outputPage == null; + } + + @Override + public ListenableFuture isBlocked() + { + return channelSetFuture; + } + + @Override + public boolean needsInput() + { + if (finishing || outputPage != null) { + return false; + } + + if (channelSet == null) { + channelSet = tryGetUnchecked(channelSetFuture); + } + return channelSet != null; + } + + @Override + public void addInput(Page page) + { + checkNotNull(page, "page is null"); + checkState(!finishing, "Operator is finishing"); + checkState(channelSet != null, "Set has not been built yet"); + checkState(outputPage == null, "Operator still has pending output"); + + // create the block builder for the new boolean column + // we know the exact size required for the block + BlockBuilder blockBuilder = BOOLEAN.createFixedSizeBlockBuilder(page.getPositionCount()); + + Page probeJoinPage = new Page(page.getBlock(probeJoinChannel)); + + // update hashing strategy to use probe cursor + for (int position = 0; position < page.getPositionCount(); position++) { + if (probeJoinPage.getBlock(0).isNull(position)) { + blockBuilder.appendNull(); + } + else { + boolean contains = channelSet.contains(position, probeJoinPage); + if (!contains && channelSet.containsNull()) { + blockBuilder.appendNull(); + } + else { + BOOLEAN.writeBoolean(blockBuilder, contains); + } + } + } + + // add the new boolean column to the page + Block[] sourceBlocks = page.getBlocks(); + Block[] outputBlocks = new Block[sourceBlocks.length + 1]; // +1 for the single boolean output channel + + System.arraycopy(sourceBlocks, 0, outputBlocks, 0, sourceBlocks.length); + outputBlocks[sourceBlocks.length] = blockBuilder.build(); + + outputPage = new Page(outputBlocks); + } + + @Override + public Page getOutput() + { + Page result = outputPage; + outputPage = null; + return result; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/HttpPageBufferClient.java b/presto-main/src/main/java/com/facebook/presto/operator/HttpPageBufferClient.java new file mode 100644 index 00000000..2bc95e4e --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/HttpPageBufferClient.java @@ -0,0 +1,563 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.block.BlockEncodingSerde; +import com.google.common.base.Stopwatch; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.common.net.MediaType; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import io.airlift.http.client.HttpClient; +import io.airlift.http.client.HttpClient.HttpResponseFuture; +import io.airlift.http.client.HttpStatus; +import io.airlift.http.client.HttpUriBuilder; +import io.airlift.http.client.Request; +import io.airlift.http.client.Response; +import io.airlift.http.client.ResponseHandler; +import io.airlift.http.client.ResponseTooLargeException; +import io.airlift.log.Logger; +import io.airlift.slice.InputStreamSliceInput; +import io.airlift.slice.SliceInput; +import io.airlift.units.DataSize; +import io.airlift.units.Duration; +import org.joda.time.DateTime; + +import javax.annotation.concurrent.GuardedBy; +import javax.annotation.concurrent.ThreadSafe; + +import java.io.Closeable; +import java.io.IOException; +import java.net.URI; +import java.util.List; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static com.facebook.presto.PrestoMediaTypes.PRESTO_PAGES_TYPE; +import static com.facebook.presto.block.PagesSerde.readPages; +import static com.facebook.presto.client.PrestoHeaders.PRESTO_MAX_SIZE; +import static com.facebook.presto.client.PrestoHeaders.PRESTO_PAGE_NEXT_TOKEN; +import static com.facebook.presto.client.PrestoHeaders.PRESTO_PAGE_TOKEN; +import static com.facebook.presto.operator.HttpPageBufferClient.PagesResponse.createClosedResponse; +import static com.facebook.presto.operator.HttpPageBufferClient.PagesResponse.createEmptyPagesResponse; +import static com.facebook.presto.operator.HttpPageBufferClient.PagesResponse.createPagesResponse; +import static com.facebook.presto.util.Failures.WORKER_NODE_ERROR; +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.net.HttpHeaders.CONTENT_TYPE; +import static io.airlift.http.client.Request.Builder.prepareDelete; +import static io.airlift.http.client.Request.Builder.prepareGet; +import static io.airlift.http.client.ResponseHandlerUtils.propagate; +import static io.airlift.http.client.StatusResponseHandler.createStatusResponseHandler; +import static java.lang.Math.min; +import static java.lang.String.format; + +@ThreadSafe +public final class HttpPageBufferClient + implements Closeable +{ + private static final int INITIAL_DELAY_MILLIS = 1; + private static final int MAX_DELAY_MILLIS = 100; + + private static final Logger log = Logger.get(HttpPageBufferClient.class); + + /** + * For each request, the addPage method will be called zero or more times, + * followed by either requestComplete or bufferFinished. If the client is + * closed, requestComplete or bufferFinished may never be called. + *

+ * NOTE: Implementations of this interface are not allowed to perform + * blocking operations. + */ + public interface ClientCallback + { + void addPage(HttpPageBufferClient client, Page page); + + void requestComplete(HttpPageBufferClient client); + + void clientFinished(HttpPageBufferClient client); + + void clientFailed(HttpPageBufferClient client, Throwable cause); + } + + private final HttpClient httpClient; + private final DataSize maxResponseSize; + private final Duration minErrorDuration; + private final URI location; + private final ClientCallback clientCallback; + private final BlockEncodingSerde blockEncodingSerde; + private final ScheduledExecutorService executor; + + @GuardedBy("this") + private final Stopwatch errorStopwatch; + + @GuardedBy("this") + private boolean closed; + @GuardedBy("this") + private HttpResponseFuture future; + @GuardedBy("this") + private DateTime lastUpdate = DateTime.now(); + @GuardedBy("this") + private long token; + @GuardedBy("this") + private boolean scheduled; + @GuardedBy("this") + private long errorDelayMillis; + + private final AtomicInteger pagesReceived = new AtomicInteger(); + + private final AtomicInteger requestsScheduled = new AtomicInteger(); + private final AtomicInteger requestsCompleted = new AtomicInteger(); + private final AtomicInteger requestsFailed = new AtomicInteger(); + + public HttpPageBufferClient( + HttpClient httpClient, + DataSize maxResponseSize, + Duration minErrorDuration, + URI location, + ClientCallback clientCallback, + BlockEncodingSerde blockEncodingSerde, + ScheduledExecutorService executor) + { + this(httpClient, maxResponseSize, minErrorDuration, location, clientCallback, blockEncodingSerde, executor, Stopwatch.createUnstarted()); + } + + public HttpPageBufferClient( + HttpClient httpClient, + DataSize maxResponseSize, + Duration minErrorDuration, + URI location, + ClientCallback clientCallback, + BlockEncodingSerde blockEncodingSerde, + ScheduledExecutorService executor, + Stopwatch errorStopwatch) + { + this.httpClient = checkNotNull(httpClient, "httpClient is null"); + this.maxResponseSize = checkNotNull(maxResponseSize, "maxResponseSize is null"); + this.minErrorDuration = checkNotNull(minErrorDuration, "minErrorDuration is null"); + this.location = checkNotNull(location, "location is null"); + this.clientCallback = checkNotNull(clientCallback, "clientCallback is null"); + this.blockEncodingSerde = checkNotNull(blockEncodingSerde, "blockEncodingManager is null"); + this.executor = checkNotNull(executor, "executor is null"); + this.errorStopwatch = checkNotNull(errorStopwatch, "errorStopwatch is null").reset(); + } + + public synchronized PageBufferClientStatus getStatus() + { + String state; + if (closed) { + state = "closed"; + } + else if (future != null) { + state = "running"; + } + else if (scheduled) { + state = "scheduled"; + } + else { + state = "queued"; + } + String httpRequestState = "not scheduled"; + if (future != null) { + httpRequestState = future.getState(); + } + return new PageBufferClientStatus( + location, + state, + lastUpdate, + pagesReceived.get(), + requestsScheduled.get(), + requestsCompleted.get(), + requestsFailed.get(), + httpRequestState); + } + + public synchronized boolean isRunning() + { + return future != null; + } + + @Override + public void close() + { + boolean shouldSendDelete; + Future future; + synchronized (this) { + shouldSendDelete = !closed; + + closed = true; + + future = this.future; + this.future = null; + + lastUpdate = DateTime.now(); + } + + if (future != null) { + future.cancel(true); + } + + // abort the output buffer on the remote node; response of delete is ignored + if (shouldSendDelete) { + httpClient.executeAsync(prepareDelete().setUri(location).build(), createStatusResponseHandler()); + } + } + + public synchronized void scheduleRequest() + { + if (closed || (future != null) || scheduled) { + return; + } + scheduled = true; + + // start before scheduling to include error delay + errorStopwatch.start(); + + executor.schedule(() -> { + try { + initiateRequest(); + } + catch (Throwable t) { + // should not happen, but be safe and fail the operator + clientCallback.clientFailed(HttpPageBufferClient.this, t); + } + }, errorDelayMillis, TimeUnit.MILLISECONDS); + + lastUpdate = DateTime.now(); + requestsScheduled.incrementAndGet(); + } + + private synchronized void initiateRequest() + { + scheduled = false; + if (closed || (future != null)) { + return; + } + + final URI uri = HttpUriBuilder.uriBuilderFrom(location).appendPath(String.valueOf(token)).build(); + future = httpClient.executeAsync( + prepareGet() + .setHeader(PRESTO_MAX_SIZE, maxResponseSize.toString()) + .setUri(uri).build(), + new PageResponseHandler(blockEncodingSerde)); + + Futures.addCallback(future, new FutureCallback() + { + @Override + public void onSuccess(PagesResponse result) + { + if (Thread.holdsLock(HttpPageBufferClient.this)) { + log.error("Can not handle callback while holding a lock on this"); + } + + resetErrors(); + + requestsCompleted.incrementAndGet(); + + List pages; + synchronized (HttpPageBufferClient.this) { + if (result.getToken() == token) { + pages = result.getPages(); + token = result.getNextToken(); + } + else { + pages = ImmutableList.of(); + } + } + + // add pages + for (Page page : pages) { + pagesReceived.incrementAndGet(); + clientCallback.addPage(HttpPageBufferClient.this, page); + } + + // complete request or close client + if (result.isClientClosed()) { + synchronized (HttpPageBufferClient.this) { + closed = true; + future = null; + lastUpdate = DateTime.now(); + } + clientCallback.clientFinished(HttpPageBufferClient.this); + } + else { + synchronized (HttpPageBufferClient.this) { + future = null; + lastUpdate = DateTime.now(); + } + clientCallback.requestComplete(HttpPageBufferClient.this); + } + } + + @Override + public void onFailure(Throwable t) + { + log.debug("Request to %s failed %s", uri, t); + + if (Thread.holdsLock(HttpPageBufferClient.this)) { + log.error("Can not handle callback while holding a lock on this"); + } + + t = rewriteException(t); + if (t instanceof PrestoException) { + clientCallback.clientFailed(HttpPageBufferClient.this, t); + } + + Duration errorDuration = elapsedErrorDuration(); + if (errorDuration.compareTo(minErrorDuration) > 0) { + String message = format("%s (%s - requests failed for %s)", WORKER_NODE_ERROR, uri, errorDuration); + clientCallback.clientFailed(HttpPageBufferClient.this, new PageTransportTimeoutException(message, t)); + } + + increaseErrorDelay(); + + requestsFailed.incrementAndGet(); + requestsCompleted.incrementAndGet(); + synchronized (HttpPageBufferClient.this) { + future = null; + lastUpdate = DateTime.now(); + } + clientCallback.requestComplete(HttpPageBufferClient.this); + } + }, executor); + + lastUpdate = DateTime.now(); + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + HttpPageBufferClient that = (HttpPageBufferClient) o; + + if (!location.equals(that.location)) { + return false; + } + + return true; + } + + @Override + public int hashCode() + { + return location.hashCode(); + } + + @Override + public String toString() + { + String state; + synchronized (this) { + if (closed) { + state = "CLOSED"; + } + else if (future != null) { + state = "RUNNING"; + } + else { + state = "QUEUED"; + } + } + return toStringHelper(this) + .add("location", location) + .addValue(state) + .toString(); + } + + private static Throwable rewriteException(Throwable t) + { + if (t instanceof ResponseTooLargeException) { + return new PageTooLargeException(); + } + return t; + } + + private synchronized Duration elapsedErrorDuration() + { + if (errorStopwatch.isRunning()) { + errorStopwatch.stop(); + } + long nanos = errorStopwatch.elapsed(TimeUnit.NANOSECONDS); + return new Duration(nanos, TimeUnit.NANOSECONDS).convertTo(TimeUnit.SECONDS); + } + + private synchronized void increaseErrorDelay() + { + if (errorDelayMillis == 0) { + errorDelayMillis = INITIAL_DELAY_MILLIS; + } + else { + errorDelayMillis = min(errorDelayMillis * 2, MAX_DELAY_MILLIS); + } + } + + private synchronized void resetErrors() + { + errorStopwatch.reset(); + } + + public static class PageResponseHandler + implements ResponseHandler + { + private final BlockEncodingSerde blockEncodingSerde; + + public PageResponseHandler(BlockEncodingSerde blockEncodingSerde) + { + this.blockEncodingSerde = blockEncodingSerde; + } + + @Override + public PagesResponse handleException(Request request, Exception exception) + { + throw propagate(request, exception); + } + + @Override + public PagesResponse handle(Request request, Response response) + { + // job is finished when we get a GONE response + if (response.getStatusCode() == HttpStatus.GONE.code()) { + return createClosedResponse(getToken(response)); + } + + // no content means no content was created within the wait period, but query is still ok + if (response.getStatusCode() == HttpStatus.NO_CONTENT.code()) { + return createEmptyPagesResponse(getToken(response), getNextToken(response)); + } + + // otherwise we must have gotten an OK response, everything else is considered fatal + if (response.getStatusCode() != HttpStatus.OK.code()) { + throw new PageTransportErrorException(format("Expected response code to be 200, but was %s %s: %s", response.getStatusCode(), response.getStatusMessage(), request.getUri())); + } + + String contentType = response.getHeader(CONTENT_TYPE); + if ((contentType == null) || !mediaTypeMatches(contentType, PRESTO_PAGES_TYPE)) { + // this can happen when an error page is returned, but is unlikely given the above 200 + throw new PageTransportErrorException(format("Expected %s response from server but got %s: %s", PRESTO_PAGES_TYPE, contentType, request.getUri())); + } + + long token = getToken(response); + long nextToken = getNextToken(response); + + try (SliceInput input = new InputStreamSliceInput(response.getInputStream())) { + List pages = ImmutableList.copyOf(readPages(blockEncodingSerde, input)); + return createPagesResponse(token, nextToken, pages); + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } + + private static long getToken(Response response) + { + String tokenHeader = response.getHeader(PRESTO_PAGE_TOKEN); + if (tokenHeader == null) { + throw new PageTransportErrorException(format("Expected %s header", PRESTO_PAGE_TOKEN)); + } + return Long.parseLong(tokenHeader); + } + + private static long getNextToken(Response response) + { + String nextTokenHeader = response.getHeader(PRESTO_PAGE_NEXT_TOKEN); + if (nextTokenHeader == null) { + throw new PageTransportErrorException(format("Expected %s header", PRESTO_PAGE_NEXT_TOKEN)); + } + return Long.parseLong(nextTokenHeader); + } + + private static boolean mediaTypeMatches(String value, MediaType range) + { + try { + return MediaType.parse(value).is(range); + } + catch (IllegalArgumentException | IllegalStateException e) { + return false; + } + } + } + + public static class PagesResponse + { + public static PagesResponse createPagesResponse(long token, long nextToken, Iterable pages) + { + return new PagesResponse(token, nextToken, pages, false); + } + + public static PagesResponse createEmptyPagesResponse(long token, long nextToken) + { + return new PagesResponse(token, nextToken, ImmutableList.of(), false); + } + + public static PagesResponse createClosedResponse(long token) + { + return new PagesResponse(token, -1, ImmutableList.of(), true); + } + + private final long token; + private final long nextToken; + private final List pages; + private final boolean clientClosed; + + private PagesResponse(long token, long nextToken, Iterable pages, boolean clientClosed) + { + this.token = token; + this.nextToken = nextToken; + this.pages = ImmutableList.copyOf(pages); + this.clientClosed = clientClosed; + } + + public long getToken() + { + return token; + } + + public long getNextToken() + { + return nextToken; + } + + public List getPages() + { + return pages; + } + + public boolean isClientClosed() + { + return clientClosed; + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("token", token) + .add("nextToken", nextToken) + .add("pagesSize", pages.size()) + .add("clientClosed", clientClosed) + .toString(); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/InMemoryExchange.java b/presto-main/src/main/java/com/facebook/presto/operator/InMemoryExchange.java new file mode 100644 index 00000000..8d815c3b --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/InMemoryExchange.java @@ -0,0 +1,283 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import io.airlift.units.DataSize; + +import javax.annotation.concurrent.GuardedBy; +import javax.annotation.concurrent.ThreadSafe; + +import java.util.List; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +import static com.facebook.presto.operator.Operator.NOT_BLOCKED; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static io.airlift.units.DataSize.Unit.MEGABYTE; + +@ThreadSafe +public class InMemoryExchange +{ + private static final DataSize DEFAULT_MAX_BUFFERED_BYTES = new DataSize(32, MEGABYTE); + private final List types; + private final List> buffers; + private final long maxBufferedBytes; + + @GuardedBy("this") + private boolean finishing; + + @GuardedBy("this") + private boolean noMoreSinkFactories; + + @GuardedBy("this") + private int sinkFactories; + + @GuardedBy("this") + private int sinks; + + @GuardedBy("this") + private long bufferBytes; + + @GuardedBy("this") + private SettableFuture readerFuture; + + @GuardedBy("this") + private SettableFuture writerFuture; + + public InMemoryExchange(List types) + { + this(types, 1, DEFAULT_MAX_BUFFERED_BYTES); + } + + public InMemoryExchange(List types, int bufferCount) + { + this(types, bufferCount, DEFAULT_MAX_BUFFERED_BYTES); + } + + public InMemoryExchange(List types, int bufferCount, DataSize maxBufferedBytes) + { + this.types = ImmutableList.copyOf(checkNotNull(types, "types is null")); + + ImmutableList.Builder> buffers = ImmutableList.builder(); + for (int i = 0; i < bufferCount; i++) { + buffers.add(new ConcurrentLinkedQueue<>()); + } + this.buffers = buffers.build(); + + checkArgument(maxBufferedBytes.toBytes() > 0, "maxBufferedBytes must be greater than zero"); + this.maxBufferedBytes = maxBufferedBytes.toBytes(); + } + + public List getTypes() + { + return types; + } + + public int getBufferCount() + { + return buffers.size(); + } + + public synchronized OperatorFactory createSinkFactory(int operatorId) + { + sinkFactories++; + return new InMemoryExchangeSinkOperatorFactory(operatorId); + } + + private synchronized void addSink() + { + checkState(sinkFactories > 0, "All sink factories already closed"); + sinks++; + } + + public synchronized void sinkFinished() + { + checkState(sinks != 0, "All sinks are already complete"); + sinks--; + updateState(); + } + + public synchronized void noMoreSinkFactories() + { + this.noMoreSinkFactories = true; + updateState(); + } + + private synchronized void sinkFactoryClosed() + { + checkState(sinkFactories != 0, "All sinks factories are already closed"); + sinkFactories--; + updateState(); + } + + private void updateState() + { + if (noMoreSinkFactories && (sinkFactories == 0) && (sinks == 0)) { + finish(); + } + } + + public synchronized boolean isFinishing() + { + return finishing; + } + + public synchronized void finish() + { + finishing = true; + notifyBlockedReaders(); + notifyBlockedWriters(); + } + + public synchronized boolean isFinished(int bufferIndex) + { + return finishing && buffers.get(bufferIndex).isEmpty(); + } + + public synchronized void addPage(Page page) + { + if (finishing) { + return; + } + PageReference pageReference = new PageReference(page, buffers.size()); + for (Queue buffer : buffers) { + buffer.add(pageReference); + } + bufferBytes += page.getSizeInBytes(); + // TODO: record memory usage using OperatorContext.setMemoryReservation() + notifyBlockedReaders(); + } + + private synchronized void notifyBlockedReaders() + { + if (readerFuture != null) { + readerFuture.set(null); + readerFuture = null; + } + } + + public synchronized ListenableFuture waitForReading(int bufferIndex) + { + if (finishing || !buffers.get(bufferIndex).isEmpty()) { + return NOT_BLOCKED; + } + if (readerFuture == null) { + readerFuture = SettableFuture.create(); + } + return readerFuture; + } + + public synchronized Page removePage(int bufferIndex) + { + PageReference pageReference = buffers.get(bufferIndex).poll(); + if (pageReference == null) { + return null; + } + + Page page = pageReference.removePage(); + if (!pageReference.isReferenced()) { + bufferBytes -= page.getSizeInBytes(); + if (bufferBytes < maxBufferedBytes) { + notifyBlockedWriters(); + } + } + return page; + } + + private synchronized void notifyBlockedWriters() + { + if (writerFuture != null) { + writerFuture.set(null); + writerFuture = null; + } + } + + public synchronized ListenableFuture waitForWriting() + { + if (bufferBytes < maxBufferedBytes) { + return NOT_BLOCKED; + } + if (writerFuture == null) { + writerFuture = SettableFuture.create(); + } + return writerFuture; + } + + private static class PageReference + { + private final Page page; + private int referenceCount; + + public PageReference(Page page, int referenceCount) + { + this.page = page; + this.referenceCount = referenceCount; + } + + public Page removePage() + { + checkArgument(referenceCount > 0); + referenceCount--; + return page; + } + + public boolean isReferenced() + { + return referenceCount > 0; + } + } + + private class InMemoryExchangeSinkOperatorFactory + implements OperatorFactory + { + private final int operatorId; + private boolean closed; + + private InMemoryExchangeSinkOperatorFactory(int operatorId) + { + this.operatorId = operatorId; + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public Operator createOperator(DriverContext driverContext) + { + checkState(!closed, "Factory is already closed"); + OperatorContext operatorContext = driverContext.addOperatorContext(operatorId, InMemoryExchangeSinkOperator.class.getSimpleName()); + addSink(); + return new InMemoryExchangeSinkOperator(operatorContext, InMemoryExchange.this); + } + + @Override + public void close() + { + if (!closed) { + closed = true; + sinkFactoryClosed(); + } + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/InMemoryExchangeSinkOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/InMemoryExchangeSinkOperator.java new file mode 100644 index 00000000..d0c0a9bd --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/InMemoryExchangeSinkOperator.java @@ -0,0 +1,105 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.type.Type; +import com.google.common.util.concurrent.ListenableFuture; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +public class InMemoryExchangeSinkOperator + implements Operator +{ + private final OperatorContext operatorContext; + private final InMemoryExchange inMemoryExchange; + private boolean finished; + + InMemoryExchangeSinkOperator(OperatorContext operatorContext, InMemoryExchange inMemoryExchange) + { + this.operatorContext = checkNotNull(operatorContext, "operatorContext is null"); + this.inMemoryExchange = checkNotNull(inMemoryExchange, "inMemoryExchange is null"); + } + + @Override + public OperatorContext getOperatorContext() + { + return operatorContext; + } + + @Override + public List getTypes() + { + return inMemoryExchange.getTypes(); + } + + @Override + public void finish() + { + if (!finished) { + finished = true; + inMemoryExchange.sinkFinished(); + } + } + + @Override + public boolean isFinished() + { + if (!finished) { + finished = inMemoryExchange.isFinishing(); + } + return finished; + } + + @Override + public ListenableFuture isBlocked() + { + ListenableFuture blocked = inMemoryExchange.waitForWriting(); + if (blocked.isDone()) { + return NOT_BLOCKED; + } + return blocked; + } + + @Override + public boolean needsInput() + { + return !isFinished() && isBlocked().isDone(); + } + + @Override + public void addInput(Page page) + { + checkNotNull(page, "page is null"); + checkState(!finished, "Already finished"); + inMemoryExchange.addPage(page); + operatorContext.recordGeneratedOutput(page.getSizeInBytes(), page.getPositionCount()); + } + + @Override + public Page getOutput() + { + return null; + } + + @Override + public void close() + throws Exception + { + finish(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/InMemoryExchangeSourceOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/InMemoryExchangeSourceOperator.java new file mode 100644 index 00000000..80be99e7 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/InMemoryExchangeSourceOperator.java @@ -0,0 +1,161 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.type.Type; +import com.google.common.util.concurrent.ListenableFuture; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static java.util.Objects.requireNonNull; + +public class InMemoryExchangeSourceOperator + implements Operator +{ + public static class InMemoryExchangeSourceOperatorFactory + implements OperatorFactory + { + private final int operatorId; + private final InMemoryExchange inMemoryExchange; + private final boolean broadcast; + private int bufferIndex; + private boolean closed; + + public static InMemoryExchangeSourceOperatorFactory createRandomDistribution(int operatorId, InMemoryExchange inMemoryExchange) + { + requireNonNull(inMemoryExchange, "inMemoryExchange is null"); + checkArgument(inMemoryExchange.getBufferCount() == 1, "exchange must have only one buffer"); + return new InMemoryExchangeSourceOperatorFactory(operatorId, inMemoryExchange, false); + } + + public static InMemoryExchangeSourceOperatorFactory createBroadcastDistribution(int operatorId, InMemoryExchange inMemoryExchange) + { + requireNonNull(inMemoryExchange, "inMemoryExchange is null"); + checkArgument(inMemoryExchange.getBufferCount() > 1, "exchange must have more than one buffer"); + return new InMemoryExchangeSourceOperatorFactory(operatorId, inMemoryExchange, true); + } + + private InMemoryExchangeSourceOperatorFactory(int operatorId, InMemoryExchange inMemoryExchange, boolean broadcast) + { + this.operatorId = operatorId; + this.inMemoryExchange = requireNonNull(inMemoryExchange, "inMemoryExchange is null"); + checkArgument(bufferIndex < inMemoryExchange.getBufferCount()); + this.broadcast = broadcast; + } + + @Override + public List getTypes() + { + return inMemoryExchange.getTypes(); + } + + @Override + public Operator createOperator(DriverContext driverContext) + { + checkState(!closed, "Factory is already closed"); + checkState(bufferIndex < inMemoryExchange.getBufferCount(), "All operators already created"); + OperatorContext operatorContext = driverContext.addOperatorContext(operatorId, InMemoryExchangeSourceOperator.class.getSimpleName()); + Operator operator = new InMemoryExchangeSourceOperator(operatorContext, inMemoryExchange, bufferIndex); + if (broadcast) { + bufferIndex++; + } + return operator; + } + + @Override + public void close() + { + closed = true; + } + } + + private final OperatorContext operatorContext; + private final InMemoryExchange exchange; + private final int bufferIndex; + + public InMemoryExchangeSourceOperator(OperatorContext operatorContext, InMemoryExchange exchange, int bufferIndex) + { + this.operatorContext = checkNotNull(operatorContext, "operatorContext is null"); + this.exchange = checkNotNull(exchange, "exchange is null"); + checkArgument(bufferIndex < exchange.getBufferCount()); + this.bufferIndex = bufferIndex; + } + + @Override + public OperatorContext getOperatorContext() + { + return operatorContext; + } + + @Override + public List getTypes() + { + return exchange.getTypes(); + } + + @Override + public void finish() + { + exchange.finish(); + } + + @Override + public boolean isFinished() + { + return exchange.isFinished(bufferIndex); + } + + @Override + public ListenableFuture isBlocked() + { + ListenableFuture blocked = exchange.waitForReading(bufferIndex); + if (blocked.isDone()) { + return NOT_BLOCKED; + } + return blocked; + } + + @Override + public boolean needsInput() + { + return false; + } + + @Override + public void addInput(Page page) + { + throw new UnsupportedOperationException(); + } + + @Override + public Page getOutput() + { + Page page = exchange.removePage(bufferIndex); + if (page != null) { + operatorContext.recordGeneratedInput(page.getSizeInBytes(), page.getPositionCount()); + } + return page; + } + + @Override + public void close() + throws Exception + { + finish(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/InMemoryJoinHash.java b/presto-main/src/main/java/com/facebook/presto/operator/InMemoryJoinHash.java new file mode 100644 index 00000000..b5e95363 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/InMemoryJoinHash.java @@ -0,0 +1,234 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; +import com.google.common.primitives.Ints; +import io.airlift.slice.XxHash64; +import it.unimi.dsi.fastutil.HashCommon; +import it.unimi.dsi.fastutil.longs.AbstractLongIterator; +import it.unimi.dsi.fastutil.longs.LongArrayList; +import it.unimi.dsi.fastutil.longs.LongIterator; + +import java.util.Arrays; +import java.util.List; + +import static com.facebook.presto.operator.SyntheticAddress.decodePosition; +import static com.facebook.presto.operator.SyntheticAddress.decodeSliceIndex; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.airlift.slice.SizeOf.sizeOfBooleanArray; +import static io.airlift.slice.SizeOf.sizeOfIntArray; + +// This implementation assumes arrays used in the hash are always a power of 2 +public final class InMemoryJoinHash + implements LookupSource +{ + private final LongArrayList addresses; + private final PagesHashStrategy pagesHashStrategy; + + private final int channelCount; + private final int mask; + private final int[] key; + private final boolean[] keyVisited; + private final int[] positionLinks; + private final long size; + private final List hashTypes; + + public InMemoryJoinHash(LongArrayList addresses, List hashTypes, PagesHashStrategy pagesHashStrategy) + { + this.addresses = checkNotNull(addresses, "addresses is null"); + this.hashTypes = ImmutableList.copyOf(checkNotNull(hashTypes, "hashTypes is null")); + this.pagesHashStrategy = checkNotNull(pagesHashStrategy, "pagesHashStrategy is null"); + this.channelCount = pagesHashStrategy.getChannelCount(); + + // reserve memory for the arrays + int hashSize = HashCommon.arraySize(addresses.size(), 0.75f); + size = sizeOfIntArray(hashSize) + sizeOfBooleanArray(hashSize) + sizeOfIntArray(addresses.size()); + + mask = hashSize - 1; + key = new int[hashSize]; + keyVisited = new boolean[hashSize]; + Arrays.fill(key, -1); + + this.positionLinks = new int[addresses.size()]; + Arrays.fill(positionLinks, -1); + + // index pages + for (int position = 0; position < addresses.size(); position++) { + int pos = getHashPosition(hashPosition(position), mask); + + // look for an empty slot or a slot containing this key + while (key[pos] != -1) { + int currentKey = key[pos]; + if (positionEqualsPosition(currentKey, position)) { + // found a slot for this key + // link the new key position to the current key position + positionLinks[position] = currentKey; + + // key[pos] updated outside of this loop + break; + } + // increment position and mask to handler wrap around + pos = (pos + 1) & mask; + } + + key[pos] = position; + } + } + + @Override + public final int getChannelCount() + { + return channelCount; + } + + @Override + public long getInMemorySizeInBytes() + { + return size; + } + + @Override + public long getJoinPosition(int position, Page page) + { + return getJoinPosition(position, page, pagesHashStrategy.hashRow(position, page.getBlocks())); + } + + @Override + public long getJoinPosition(int position, Page page, int rawHash) + { + int pos = getHashPosition(rawHash, mask); + + while (key[pos] != -1) { + if (positionEqualsCurrentRow(key[pos], position, page.getBlocks())) { + keyVisited[pos] = true; + return key[pos]; + } + // increment position and mask to handler wrap around + pos = (pos + 1) & mask; + } + return -1; + } + + @Override + public final long getNextJoinPosition(long currentPosition) + { + return positionLinks[Ints.checkedCast(currentPosition)]; + } + + @Override + public LongIterator getUnvisitedJoinPositions() + { + return new UnvisitedJoinPositionIterator(); + } + + public class UnvisitedJoinPositionIterator extends AbstractLongIterator + { + private int nextKeyId = 0; + private long nextJoinPosition = -1; + + private UnvisitedJoinPositionIterator() + { + findUnvisitedKeyId(); + } + + @Override + public long nextLong() + { + long result = nextJoinPosition; + + nextJoinPosition = getNextJoinPosition(nextJoinPosition); + if (nextJoinPosition < 0) { + nextKeyId++; + findUnvisitedKeyId(); + } + + return result; + } + + @Override + public boolean hasNext() + { + return nextKeyId < keyVisited.length; + } + + private void findUnvisitedKeyId() + { + while (nextKeyId < keyVisited.length) { + if (key[nextKeyId] != -1 && !keyVisited[nextKeyId]) { + break; + } + nextKeyId++; + } + if (nextKeyId < keyVisited.length) { + nextJoinPosition = key[nextKeyId]; + } + } + } + + @Override + public void appendTo(long position, PageBuilder pageBuilder, int outputChannelOffset) + { + long pageAddress = addresses.getLong(Ints.checkedCast(position)); + int blockIndex = decodeSliceIndex(pageAddress); + int blockPosition = decodePosition(pageAddress); + + pagesHashStrategy.appendTo(blockIndex, blockPosition, pageBuilder, outputChannelOffset); + } + + @Override + public void close() + { + } + + private int hashPosition(int position) + { + long pageAddress = addresses.getLong(position); + int blockIndex = decodeSliceIndex(pageAddress); + int blockPosition = decodePosition(pageAddress); + + return pagesHashStrategy.hashPosition(blockIndex, blockPosition); + } + + private boolean positionEqualsCurrentRow(int leftPosition, int rightPosition, Block... rightBlocks) + { + long pageAddress = addresses.getLong(leftPosition); + int blockIndex = decodeSliceIndex(pageAddress); + int blockPosition = decodePosition(pageAddress); + + return pagesHashStrategy.positionEqualsRow(blockIndex, blockPosition, rightPosition, rightBlocks); + } + + private boolean positionEqualsPosition(int leftPosition, int rightPosition) + { + long leftPageAddress = addresses.getLong(leftPosition); + int leftBlockIndex = decodeSliceIndex(leftPageAddress); + int leftBlockPosition = decodePosition(leftPageAddress); + + long rightPageAddress = addresses.getLong(rightPosition); + int rightBlockIndex = decodeSliceIndex(rightPageAddress); + int rightBlockPosition = decodePosition(rightPageAddress); + + return pagesHashStrategy.positionEqualsPosition(leftBlockIndex, leftBlockPosition, rightBlockIndex, rightBlockPosition); + } + + private static int getHashPosition(int rawHash, int mask) + { + return ((int) XxHash64.hash(rawHash)) & mask; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/InterpretedHashGenerator.java b/presto-main/src/main/java/com/facebook/presto/operator/InterpretedHashGenerator.java new file mode 100644 index 00000000..b88a761a --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/InterpretedHashGenerator.java @@ -0,0 +1,63 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.operator.scalar.CombineHashFunction; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.planner.optimizations.HashGenerationOptimizer; +import com.facebook.presto.type.TypeUtils; +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class InterpretedHashGenerator + implements HashGenerator +{ + private final List hashChannelTypes; + private final int[] hashChannels; + + public InterpretedHashGenerator(List hashChannelTypes, int[] hashChannels) + { + this.hashChannels = checkNotNull(hashChannels, "hashChannels is null"); + this.hashChannelTypes = ImmutableList.copyOf(checkNotNull(hashChannelTypes, "hashChannelTypes is null")); + checkArgument(hashChannelTypes.size() == hashChannels.length); + } + + @Override + public int hashPosition(int position, Page page) + { + Block[] blocks = page.getBlocks(); + int result = HashGenerationOptimizer.INITIAL_HASH_VALUE; + for (int i = 0; i < hashChannels.length; i++) { + Type type = hashChannelTypes.get(i); + result = (int) CombineHashFunction.getHash(result, TypeUtils.hashPosition(type, blocks[hashChannels[i]], position)); + } + return result; + } + + @Override + public String toString() + { + return MoreObjects.toStringHelper(this) + .add("hashChannelTypes", hashChannelTypes) + .add("hashChannels", hashChannels) + .toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/JoinProbe.java b/presto-main/src/main/java/com/facebook/presto/operator/JoinProbe.java new file mode 100644 index 00000000..9eba840d --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/JoinProbe.java @@ -0,0 +1,27 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.PageBuilder; + +public interface JoinProbe +{ + int getChannelCount(); + + boolean advanceNextPosition(); + + long getCurrentJoinPosition(); + + void appendTo(PageBuilder pageBuilder); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/JoinProbeFactory.java b/presto-main/src/main/java/com/facebook/presto/operator/JoinProbeFactory.java new file mode 100644 index 00000000..c5941fb0 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/JoinProbeFactory.java @@ -0,0 +1,21 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.Page; + +public interface JoinProbeFactory +{ + JoinProbe createJoinProbe(LookupSource lookupSource, Page page); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/LimitOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/LimitOperator.java new file mode 100644 index 00000000..2bea9bb0 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/LimitOperator.java @@ -0,0 +1,137 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +public class LimitOperator + implements Operator +{ + public static class LimitOperatorFactory + implements OperatorFactory + { + private final int operatorId; + private final List types; + private final long limit; + private boolean closed; + + public LimitOperatorFactory(int operatorId, List types, long limit) + { + this.operatorId = operatorId; + this.types = ImmutableList.copyOf(types); + this.limit = limit; + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public Operator createOperator(DriverContext driverContext) + { + checkState(!closed, "Factory is already closed"); + OperatorContext operatorContext = driverContext.addOperatorContext(operatorId, LimitOperator.class.getSimpleName()); + return new LimitOperator(operatorContext, types, limit); + } + + @Override + public void close() + { + closed = true; + } + } + + private final OperatorContext operatorContext; + private final List types; + private Page nextPage; + private long remainingLimit; + + public LimitOperator(OperatorContext operatorContext, List types, long limit) + { + this.operatorContext = checkNotNull(operatorContext, "operatorContext is null"); + this.types = checkNotNull(types, "types is null"); + + checkArgument(limit >= 0, "limit must be at least zero"); + this.remainingLimit = limit; + } + + @Override + public OperatorContext getOperatorContext() + { + return operatorContext; + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public void finish() + { + remainingLimit = 0; + } + + @Override + public boolean isFinished() + { + return remainingLimit == 0 && nextPage == null; + } + + @Override + public boolean needsInput() + { + return remainingLimit > 0 && nextPage == null; + } + + @Override + public void addInput(Page page) + { + checkState(needsInput()); + + if (page.getPositionCount() <= remainingLimit) { + remainingLimit -= page.getPositionCount(); + nextPage = page; + } + else { + Block[] blocks = new Block[page.getChannelCount()]; + for (int channel = 0; channel < page.getChannelCount(); channel++) { + Block block = page.getBlock(channel); + blocks[channel] = block.getRegion(0, (int) remainingLimit); + } + nextPage = new Page((int) remainingLimit, blocks); + remainingLimit = 0; + } + } + + @Override + public Page getOutput() + { + Page page = nextPage; + nextPage = null; + return page; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/LookupJoinOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/LookupJoinOperator.java new file mode 100644 index 00000000..123cf1df --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/LookupJoinOperator.java @@ -0,0 +1,288 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.operator.LookupJoinOperators.JoinType; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.ListenableFuture; +import it.unimi.dsi.fastutil.longs.LongIterator; + +import java.io.Closeable; +import java.util.List; + +import static com.facebook.presto.operator.LookupJoinOperators.JoinType.FULL_OUTER; +import static com.facebook.presto.operator.LookupJoinOperators.JoinType.LOOKUP_OUTER; +import static com.facebook.presto.operator.LookupJoinOperators.JoinType.PROBE_OUTER; +import static com.facebook.presto.util.MoreFutures.tryGetUnchecked; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +public class LookupJoinOperator + implements Operator, Closeable +{ + private final ListenableFuture lookupSourceFuture; + private final LookupSourceSupplier lookupSourceSupplier; + + private final OperatorContext operatorContext; + private final JoinProbeFactory joinProbeFactory; + private final List types; + private final List probeTypes; + private final PageBuilder pageBuilder; + + private final boolean lookupOnOuterSide; + private final boolean probeOnOuterSide; + + private LookupSource lookupSource; + private JoinProbe probe; + + private boolean closed; + private boolean finishing; + private long joinPosition = -1; + + private LongIterator unvisitedJoinPositions; + + public LookupJoinOperator( + OperatorContext operatorContext, + LookupSourceSupplier lookupSourceSupplier, + List probeTypes, + JoinType joinType, + JoinProbeFactory joinProbeFactory) + { + this.operatorContext = checkNotNull(operatorContext, "operatorContext is null"); + + // todo pass in desired projection + this.lookupSourceSupplier = checkNotNull(lookupSourceSupplier, "lookupSourceSupplier is null"); + lookupSourceSupplier.retain(); + checkNotNull(probeTypes, "probeTypes is null"); + + this.lookupSourceFuture = lookupSourceSupplier.getLookupSource(operatorContext); + this.joinProbeFactory = joinProbeFactory; + + // Cannot use switch case here, because javac will synthesize an inner class and cause IllegalAccessError + probeOnOuterSide = joinType == PROBE_OUTER || joinType == FULL_OUTER; + lookupOnOuterSide = joinType == LOOKUP_OUTER || joinType == FULL_OUTER; + + this.types = ImmutableList.builder() + .addAll(probeTypes) + .addAll(lookupSourceSupplier.getTypes()) + .build(); + this.probeTypes = probeTypes; + this.pageBuilder = new PageBuilder(types); + } + + @Override + public OperatorContext getOperatorContext() + { + return operatorContext; + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public void finish() + { + finishing = true; + } + + @Override + public boolean isFinished() + { + boolean finished = + finishing && + probe == null && + pageBuilder.isEmpty() && + (!lookupOnOuterSide || (unvisitedJoinPositions != null && !unvisitedJoinPositions.hasNext())); + + // if finished drop references so memory is freed early + if (finished) { + if (lookupSource != null) { + lookupSource.close(); + lookupSource = null; + } + probe = null; + pageBuilder.reset(); + } + return finished; + } + + @Override + public ListenableFuture isBlocked() + { + return lookupSourceFuture; + } + + @Override + public boolean needsInput() + { + if (finishing) { + return false; + } + + if (lookupSource == null) { + lookupSource = tryGetUnchecked(lookupSourceFuture); + } + return lookupSource != null && probe == null; + } + + @Override + public void addInput(Page page) + { + checkNotNull(page, "page is null"); + checkState(!finishing, "Operator is finishing"); + checkState(lookupSource != null, "Lookup source has not been built yet"); + checkState(probe == null, "Current page has not been completely processed yet"); + + // create probe + probe = joinProbeFactory.createJoinProbe(lookupSource, page); + + // initialize to invalid join position to force output code to advance the cursors + joinPosition = -1; + } + + @Override + public Page getOutput() + { + // If needsInput was never called, lookupSource has not been initialized so far. + if (lookupSource == null) { + lookupSource = tryGetUnchecked(lookupSourceFuture); + if (lookupSource == null) { + return null; + } + } + + // join probe page with the lookup source + if (probe != null) { + while (joinCurrentPosition()) { + if (!advanceProbePosition()) { + break; + } + if (!outerJoinCurrentPosition()) { + break; + } + } + } + + if (lookupOnOuterSide && finishing && probe == null) { + buildSideOuterJoinUnvisitedPositions(); + } + + // only flush full pages unless we are done + if (pageBuilder.isFull() || (finishing && !pageBuilder.isEmpty() && probe == null)) { + Page page = pageBuilder.build(); + pageBuilder.reset(); + return page; + } + + return null; + } + + @Override + public void close() + { + if (lookupSource != null) { + lookupSource.close(); + lookupSource = null; + } + // Closing the lookupSource is always safe to do, but we don't want to release the supplier multiple times, since its reference counted + if (closed) { + return; + } + closed = true; + lookupSourceSupplier.release(); + } + + private boolean joinCurrentPosition() + { + // while we have a position to join against... + while (joinPosition >= 0) { + pageBuilder.declarePosition(); + + // write probe columns + probe.appendTo(pageBuilder); + + // write build columns + lookupSource.appendTo(joinPosition, pageBuilder, probe.getChannelCount()); + + // get next join position for this row + joinPosition = lookupSource.getNextJoinPosition(joinPosition); + if (pageBuilder.isFull()) { + return false; + } + } + return true; + } + + private boolean advanceProbePosition() + { + if (!probe.advanceNextPosition()) { + probe = null; + return false; + } + + // update join position + joinPosition = probe.getCurrentJoinPosition(); + return true; + } + + private boolean outerJoinCurrentPosition() + { + if (probeOnOuterSide && joinPosition < 0) { + // write probe columns + pageBuilder.declarePosition(); + probe.appendTo(pageBuilder); + + // write nulls into build columns + int outputIndex = probe.getChannelCount(); + for (int buildChannel = 0; buildChannel < lookupSource.getChannelCount(); buildChannel++) { + pageBuilder.getBlockBuilder(outputIndex).appendNull(); + outputIndex++; + } + if (pageBuilder.isFull()) { + return false; + } + } + return true; + } + + private void buildSideOuterJoinUnvisitedPositions() + { + if (unvisitedJoinPositions == null) { + unvisitedJoinPositions = lookupSource.getUnvisitedJoinPositions(); + } + + while (unvisitedJoinPositions.hasNext()) { + long buildSideOuterJoinPosition = unvisitedJoinPositions.nextLong(); + pageBuilder.declarePosition(); + + // write nulls into probe columns + for (int probeChannel = 0; probeChannel < probeTypes.size(); probeChannel++) { + pageBuilder.getBlockBuilder(probeChannel).appendNull(); + } + + // write build columns + lookupSource.appendTo(buildSideOuterJoinPosition, pageBuilder, probeTypes.size()); + + if (pageBuilder.isFull()) { + return; + } + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/LookupJoinOperatorFactory.java b/presto-main/src/main/java/com/facebook/presto/operator/LookupJoinOperatorFactory.java new file mode 100644 index 00000000..6667c1c5 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/LookupJoinOperatorFactory.java @@ -0,0 +1,77 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.operator.LookupJoinOperators.JoinType; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkState; + +public class LookupJoinOperatorFactory + implements OperatorFactory +{ + private final int operatorId; + private final LookupSourceSupplier lookupSourceSupplier; + private final List probeTypes; + private final JoinType joinType; + private final List types; + private final JoinProbeFactory joinProbeFactory; + private boolean closed; + + public LookupJoinOperatorFactory(int operatorId, + LookupSourceSupplier lookupSourceSupplier, + List probeTypes, + JoinType joinType, + JoinProbeFactory joinProbeFactory) + { + this.operatorId = operatorId; + this.lookupSourceSupplier = lookupSourceSupplier; + this.probeTypes = probeTypes; + this.joinType = joinType; + + this.joinProbeFactory = joinProbeFactory; + + this.types = ImmutableList.builder() + .addAll(probeTypes) + .addAll(lookupSourceSupplier.getTypes()) + .build(); + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public Operator createOperator(DriverContext driverContext) + { + checkState(!closed, "Factory is already closed"); + OperatorContext operatorContext = driverContext.addOperatorContext(operatorId, LookupJoinOperator.class.getSimpleName()); + return new LookupJoinOperator(operatorContext, lookupSourceSupplier, probeTypes, joinType, joinProbeFactory); + } + + @Override + public void close() + { + if (closed) { + return; + } + closed = true; + lookupSourceSupplier.release(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/LookupJoinOperators.java b/presto-main/src/main/java/com/facebook/presto/operator/LookupJoinOperators.java new file mode 100644 index 00000000..367c6b3c --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/LookupJoinOperators.java @@ -0,0 +1,56 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.gen.JoinProbeCompiler; + +import java.util.List; +import java.util.Optional; + +public class LookupJoinOperators +{ + public enum JoinType { + INNER, + PROBE_OUTER, // the Probe is the outer side of the join + LOOKUP_OUTER, // The LookupSource is the outer side of the join + FULL_OUTER, + } + + private LookupJoinOperators() + { + } + + private static final JoinProbeCompiler JOIN_PROBE_COMPILER = new JoinProbeCompiler(); + + public static OperatorFactory innerJoin(int operatorId, LookupSourceSupplier lookupSourceSupplier, List probeTypes, List probeJoinChannel, Optional probeHashChannel) + { + return JOIN_PROBE_COMPILER.compileJoinOperatorFactory(operatorId, lookupSourceSupplier, probeTypes, probeJoinChannel, probeHashChannel, JoinType.INNER); + } + + public static OperatorFactory probeOuterJoin(int operatorId, LookupSourceSupplier lookupSourceSupplier, List probeTypes, List probeJoinChannel, Optional probeHashChannel) + { + return JOIN_PROBE_COMPILER.compileJoinOperatorFactory(operatorId, lookupSourceSupplier, probeTypes, probeJoinChannel, probeHashChannel, JoinType.PROBE_OUTER); + } + + public static OperatorFactory lookupOuterJoin(int operatorId, LookupSourceSupplier lookupSourceSupplier, List probeTypes, List probeJoinChannel, Optional probeHashChannel) + { + return JOIN_PROBE_COMPILER.compileJoinOperatorFactory(operatorId, lookupSourceSupplier, probeTypes, probeJoinChannel, probeHashChannel, JoinType.LOOKUP_OUTER); + } + + public static OperatorFactory fullOuterJoin(int operatorId, LookupSourceSupplier lookupSourceSupplier, List probeTypes, List probeJoinChannel, Optional probeHashChannel) + { + return JOIN_PROBE_COMPILER.compileJoinOperatorFactory(operatorId, lookupSourceSupplier, probeTypes, probeJoinChannel, probeHashChannel, JoinType.FULL_OUTER); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/LookupSource.java b/presto-main/src/main/java/com/facebook/presto/operator/LookupSource.java new file mode 100644 index 00000000..909e4aac --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/LookupSource.java @@ -0,0 +1,41 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PageBuilder; +import it.unimi.dsi.fastutil.longs.LongIterator; + +import java.io.Closeable; + +public interface LookupSource + extends Closeable +{ + int getChannelCount(); + + long getInMemorySizeInBytes(); + + long getJoinPosition(int position, Page page, int rawHash); + + long getJoinPosition(int position, Page page); + + long getNextJoinPosition(long currentPosition); + + void appendTo(long position, PageBuilder pageBuilder, int outputChannelOffset); + + LongIterator getUnvisitedJoinPositions(); + + @Override + void close(); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/LookupSourceSupplier.java b/presto-main/src/main/java/com/facebook/presto/operator/LookupSourceSupplier.java new file mode 100644 index 00000000..c7763ff4 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/LookupSourceSupplier.java @@ -0,0 +1,34 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.type.Type; +import com.google.common.util.concurrent.ListenableFuture; + +import java.util.List; + +public interface LookupSourceSupplier +{ + List getTypes(); + + ListenableFuture getLookupSource(OperatorContext operatorContext); + + /** + * NOTE: LookupSourceSupplier must be reference counted, because some of them own a SharedLookupSource. + * Ideally, that would be owned by the pipeline instead. + */ + void retain(); + + void release(); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/MapUnnester.java b/presto-main/src/main/java/com/facebook/presto/operator/MapUnnester.java new file mode 100644 index 00000000..4d8d491a --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/MapUnnester.java @@ -0,0 +1,81 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.type.MapType; +import io.airlift.slice.Slice; + +import javax.annotation.Nullable; + +import static com.facebook.presto.type.TypeUtils.readStructuralBlock; +import static com.google.common.base.Preconditions.checkNotNull; + +public class MapUnnester + implements Unnester +{ + private final Type keyType; + private final Type valueType; + private final Block block; + private final int channelCount; + + private int position; + private final int positionCount; + + public MapUnnester(MapType mapType, @Nullable Slice slice) + { + this.channelCount = 2; + checkNotNull(mapType, "mapType is null"); + this.keyType = mapType.getKeyType(); + this.valueType = mapType.getValueType(); + + if (slice == null) { + block = null; + positionCount = 0; + } + else { + block = readStructuralBlock(slice); + positionCount = block.getPositionCount(); + } + } + + protected void appendTo(PageBuilder pageBuilder, int outputChannelOffset) + { + BlockBuilder keyBlockBuilder = pageBuilder.getBlockBuilder(outputChannelOffset); + BlockBuilder valueBlockBuilder = pageBuilder.getBlockBuilder(outputChannelOffset + 1); + keyType.appendTo(block, position++, keyBlockBuilder); + valueType.appendTo(block, position++, valueBlockBuilder); + } + + @Override + public boolean hasNext() + { + return position < positionCount; + } + + @Override + public final int getChannelCount() + { + return channelCount; + } + + @Override + public final void appendNext(PageBuilder pageBuilder, int outputChannelOffset) + { + appendTo(pageBuilder, outputChannelOffset); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/MarkDistinctHash.java b/presto-main/src/main/java/com/facebook/presto/operator/MarkDistinctHash.java new file mode 100644 index 00000000..bf0c9dc2 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/MarkDistinctHash.java @@ -0,0 +1,64 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.block.BlockBuilderStatus; +import com.facebook.presto.spi.type.Type; + +import java.util.List; +import java.util.Optional; + +import static com.facebook.presto.operator.GroupByHash.createGroupByHash; +import static com.facebook.presto.spi.type.BooleanType.BOOLEAN; + +public class MarkDistinctHash +{ + private final GroupByHash groupByHash; + private long nextDistinctId; + + public MarkDistinctHash(List types, int[] channels, Optional hashChannel) + { + this(types, channels, hashChannel, 10_000); + } + + public MarkDistinctHash(List types, int[] channels, Optional hashChannel, int expectedDistinctValues) + { + this.groupByHash = createGroupByHash(types, channels, Optional.empty(), hashChannel, expectedDistinctValues); + } + + public long getEstimatedSize() + { + return groupByHash.getEstimatedSize(); + } + + public Block markDistinctRows(Page page) + { + GroupByIdBlock ids = groupByHash.getGroupIds(page); + BlockBuilder blockBuilder = BOOLEAN.createBlockBuilder(new BlockBuilderStatus(), ids.getPositionCount()); + for (int i = 0; i < ids.getPositionCount(); i++) { + if (ids.getGroupId(i) == nextDistinctId) { + BOOLEAN.writeBoolean(blockBuilder, true); + nextDistinctId++; + } + else { + BOOLEAN.writeBoolean(blockBuilder, false); + } + } + + return blockBuilder.build(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/MarkDistinctOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/MarkDistinctOperator.java new file mode 100644 index 00000000..f42fcc4b --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/MarkDistinctOperator.java @@ -0,0 +1,159 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; +import com.google.common.primitives.Ints; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +import static com.facebook.presto.spi.type.BooleanType.BOOLEAN; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +public class MarkDistinctOperator + implements Operator +{ + public static class MarkDistinctOperatorFactory + implements OperatorFactory + { + private final int operatorId; + private final Optional hashChannel; + private final int[] markDistinctChannels; + private final List types; + private boolean closed; + + public MarkDistinctOperatorFactory(int operatorId, List sourceTypes, Collection markDistinctChannels, Optional hashChannel) + { + this.operatorId = operatorId; + checkNotNull(markDistinctChannels, "markDistinctChannels is null"); + checkArgument(!markDistinctChannels.isEmpty(), "markDistinctChannels is empty"); + this.markDistinctChannels = Ints.toArray(markDistinctChannels); + this.hashChannel = checkNotNull(hashChannel, "hashChannel is null"); + this.types = ImmutableList.builder() + .addAll(sourceTypes) + .add(BOOLEAN) + .build(); + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public Operator createOperator(DriverContext driverContext) + { + checkState(!closed, "Factory is already closed"); + OperatorContext operatorContext = driverContext.addOperatorContext(operatorId, MarkDistinctOperator.class.getSimpleName()); + return new MarkDistinctOperator(operatorContext, types, markDistinctChannels, hashChannel); + } + + @Override + public void close() + { + closed = true; + } + } + + private final OperatorContext operatorContext; + private final List types; + private final MarkDistinctHash markDistinctHash; + + private Page outputPage; + private boolean finishing; + + public MarkDistinctOperator(OperatorContext operatorContext, List types, int[] markDistinctChannels, Optional hashChannel) + { + this.operatorContext = checkNotNull(operatorContext, "operatorContext is null"); + + this.types = ImmutableList.copyOf(checkNotNull(types, "types is null")); + checkNotNull(hashChannel, "hashChannel is null"); + + ImmutableList.Builder distinctTypes = ImmutableList.builder(); + for (int channel : markDistinctChannels) { + distinctTypes.add(types.get(channel)); + } + this.markDistinctHash = new MarkDistinctHash(distinctTypes.build(), markDistinctChannels, hashChannel); + } + + @Override + public OperatorContext getOperatorContext() + { + return operatorContext; + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public void finish() + { + finishing = true; + } + + @Override + public boolean isFinished() + { + return finishing && outputPage == null; + } + + @Override + public boolean needsInput() + { + operatorContext.setMemoryReservation(markDistinctHash.getEstimatedSize()); + if (finishing || outputPage != null) { + return false; + } + return true; + } + + @Override + public void addInput(Page page) + { + checkNotNull(page, "page is null"); + checkState(!finishing, "Operator is finishing"); + checkState(outputPage == null, "Operator still has pending output"); + operatorContext.setMemoryReservation(markDistinctHash.getEstimatedSize()); + + Block markerBlock = markDistinctHash.markDistinctRows(page); + + // add the new boolean column to the page + Block[] sourceBlocks = page.getBlocks(); + Block[] outputBlocks = new Block[sourceBlocks.length + 1]; // +1 for the single boolean output channel + + System.arraycopy(sourceBlocks, 0, outputBlocks, 0, sourceBlocks.length); + outputBlocks[sourceBlocks.length] = markerBlock; + + outputPage = new Page(outputBlocks); + } + + @Override + public Page getOutput() + { + Page result = outputPage; + outputPage = null; + return result; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/Mergeable.java b/presto-main/src/main/java/com/facebook/presto/operator/Mergeable.java new file mode 100644 index 00000000..b7301e4d --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/Mergeable.java @@ -0,0 +1,20 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.presto.operator; + +public interface Mergeable +{ + T mergeWith(T other); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/MultiChannelGroupByHash.java b/presto-main/src/main/java/com/facebook/presto/operator/MultiChannelGroupByHash.java new file mode 100644 index 00000000..cbd309d6 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/MultiChannelGroupByHash.java @@ -0,0 +1,389 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.gen.JoinCompiler; +import com.facebook.presto.util.array.LongBigArray; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import static com.facebook.presto.operator.SyntheticAddress.decodePosition; +import static com.facebook.presto.operator.SyntheticAddress.decodeSliceIndex; +import static com.facebook.presto.operator.SyntheticAddress.encodeSyntheticAddress; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.BooleanType.BOOLEAN; +import static com.facebook.presto.sql.gen.JoinCompiler.PagesHashStrategyFactory; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.airlift.slice.SizeOf.sizeOf; +import static it.unimi.dsi.fastutil.HashCommon.arraySize; +import static it.unimi.dsi.fastutil.HashCommon.murmurHash3; + +// This implementation assumes arrays used in the hash are always a power of 2 +public class MultiChannelGroupByHash + implements GroupByHash +{ + private static final JoinCompiler JOIN_COMPILER = new JoinCompiler(); + + private static final float FILL_RATIO = 0.9f; + private final List types; + private final int[] channels; + + private final PagesHashStrategy hashStrategy; + private final List> channelBuilders; + private final HashGenerator hashGenerator; + private final Optional precomputedHashChannel; + private final int maskChannel; + private PageBuilder currentPageBuilder; + + private long completedPagesMemorySize; + + private int maxFill; + private int mask; + private long[] groupAddressByHash; + private int[] groupIdsByHash; + + private final LongBigArray groupAddressByGroupId; + + private int nextGroupId; + + public MultiChannelGroupByHash(List hashTypes, int[] hashChannels, Optional maskChannel, Optional inputHashChannel, int expectedSize) + { + checkNotNull(hashTypes, "hashTypes is null"); + checkArgument(hashTypes.size() == hashChannels.length, "hashTypes and hashChannels have different sizes"); + checkNotNull(inputHashChannel, "inputHashChannel is null"); + checkArgument(expectedSize > 0, "expectedSize must be greater than zero"); + + this.types = inputHashChannel.isPresent() ? ImmutableList.copyOf(Iterables.concat(hashTypes, ImmutableList.of(BIGINT))) : ImmutableList.copyOf(hashTypes); + this.channels = checkNotNull(hashChannels, "hashChannels is null").clone(); + this.maskChannel = checkNotNull(maskChannel, "maskChannel is null").orElse(-1); + this.hashGenerator = inputHashChannel.isPresent() ? new PrecomputedHashGenerator(inputHashChannel.get()) : new InterpretedHashGenerator(ImmutableList.copyOf(hashTypes), hashChannels); + + // For each hashed channel, create an appendable list to hold the blocks (builders). As we + // add new values we append them to the existing block builder until it fills up and then + // we add a new block builder to each list. + ImmutableList.Builder outputChannels = ImmutableList.builder(); + ImmutableList.Builder> channelBuilders = ImmutableList.builder(); + for (int i = 0; i < hashChannels.length; i++) { + outputChannels.add(i); + channelBuilders.add(ObjectArrayList.wrap(new Block[1024], 0)); + } + if (inputHashChannel.isPresent()) { + this.precomputedHashChannel = Optional.of(hashChannels.length); + channelBuilders.add(ObjectArrayList.wrap(new Block[1024], 0)); + } + else { + this.precomputedHashChannel = Optional.empty(); + } + this.channelBuilders = channelBuilders.build(); + PagesHashStrategyFactory pagesHashStrategyFactory = JOIN_COMPILER.compilePagesHashStrategyFactory(this.types, outputChannels.build()); + hashStrategy = pagesHashStrategyFactory.createPagesHashStrategy(this.channelBuilders, this.precomputedHashChannel); + + startNewPage(); + + // reserve memory for the arrays + int hashSize = arraySize(expectedSize, FILL_RATIO); + + maxFill = calculateMaxFill(hashSize); + mask = hashSize - 1; + groupAddressByHash = new long[hashSize]; + Arrays.fill(groupAddressByHash, -1); + + groupIdsByHash = new int[hashSize]; + + groupAddressByGroupId = new LongBigArray(); + groupAddressByGroupId.ensureCapacity(maxFill); + } + + @Override + public long getEstimatedSize() + { + return (sizeOf(channelBuilders.get(0).elements()) * channelBuilders.size()) + + completedPagesMemorySize + + currentPageBuilder.getSizeInBytes() + + sizeOf(groupAddressByHash) + + sizeOf(groupIdsByHash) + + groupAddressByGroupId.sizeOf(); + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public int getGroupCount() + { + return nextGroupId; + } + + @Override + public void appendValuesTo(int groupId, PageBuilder pageBuilder, int outputChannelOffset) + { + long address = groupAddressByGroupId.get(groupId); + int blockIndex = decodeSliceIndex(address); + int position = decodePosition(address); + hashStrategy.appendTo(blockIndex, position, pageBuilder, outputChannelOffset); + } + + @Override + public void addPage(Page page) + { + Block[] hashBlocks = extractHashColumns(page); + + Block maskBlock = null; + if (maskChannel >= 0) { + maskBlock = page.getBlock(maskChannel); + } + + // get the group id for each position + int positionCount = page.getPositionCount(); + for (int position = 0; position < positionCount; position++) { + // skip masked rows + if (maskBlock != null && !BOOLEAN.getBoolean(maskBlock, position)) { + continue; + } + + // get the group for the current row + putIfAbsent(position, page, hashBlocks); + } + } + + @Override + public GroupByIdBlock getGroupIds(Page page) + { + int positionCount = page.getPositionCount(); + + // we know the exact size required for the block + BlockBuilder blockBuilder = BIGINT.createFixedSizeBlockBuilder(positionCount); + + Block maskBlock = null; + if (maskChannel >= 0) { + maskBlock = page.getBlock(maskChannel); + } + + // extract the hash columns + Block[] hashBlocks = extractHashColumns(page); + + // get the group id for each position + for (int position = 0; position < positionCount; position++) { + // skip masked rows + if (maskBlock != null && !BOOLEAN.getBoolean(maskBlock, position)) { + blockBuilder.appendNull(); + continue; + } + + // get the group for the current row + int groupId = putIfAbsent(position, page, hashBlocks); + + // output the group id for this row + BIGINT.writeLong(blockBuilder, groupId); + } + return new GroupByIdBlock(nextGroupId, blockBuilder.build()); + } + + @Override + public boolean contains(int position, Page page) + { + int rawHash = hashStrategy.hashRow(position, page.getBlocks()); + int hashPosition = getHashPosition(rawHash, mask); + + // look for a slot containing this key + while (groupAddressByHash[hashPosition] != -1) { + long address = groupAddressByHash[hashPosition]; + if (hashStrategy.positionEqualsRow(decodeSliceIndex(address), decodePosition(address), position, page.getBlocks())) { + // found an existing slot for this key + return true; + } + // increment position and mask to handle wrap around + hashPosition = (hashPosition + 1) & mask; + } + + return false; + } + + @Override + public int putIfAbsent(int position, Page page) + { + return putIfAbsent(position, page, extractHashColumns(page)); + } + + private int putIfAbsent(int position, Page page, Block[] hashBlocks) + { + int rawHash = hashGenerator.hashPosition(position, page); + int hashPosition = getHashPosition(rawHash, mask); + + // look for an empty slot or a slot containing this key + int groupId = -1; + while (groupAddressByHash[hashPosition] != -1) { + long address = groupAddressByHash[hashPosition]; + if (positionEqualsCurrentRow(decodeSliceIndex(address), decodePosition(address), position, hashBlocks)) { + // found an existing slot for this key + groupId = groupIdsByHash[hashPosition]; + + break; + } + // increment position and mask to handle wrap around + hashPosition = (hashPosition + 1) & mask; + } + + // did we find an existing group? + if (groupId < 0) { + groupId = addNewGroup(hashPosition, position, page, rawHash); + } + return groupId; + } + + private int addNewGroup(int hashPosition, int position, Page page, int rawHash) + { + // add the row to the open page + Block[] blocks = page.getBlocks(); + for (int i = 0; i < channels.length; i++) { + int hashChannel = channels[i]; + Type type = types.get(i); + type.appendTo(blocks[hashChannel], position, currentPageBuilder.getBlockBuilder(i)); + } + if (precomputedHashChannel.isPresent()) { + BIGINT.writeLong(currentPageBuilder.getBlockBuilder(precomputedHashChannel.get()), rawHash); + } + currentPageBuilder.declarePosition(); + int pageIndex = channelBuilders.get(0).size() - 1; + int pagePosition = currentPageBuilder.getPositionCount() - 1; + long address = encodeSyntheticAddress(pageIndex, pagePosition); + + // record group id in hash + int groupId = nextGroupId++; + + groupAddressByHash[hashPosition] = address; + groupIdsByHash[hashPosition] = groupId; + groupAddressByGroupId.set(groupId, address); + + // create new page builder if this page is full + if (currentPageBuilder.isFull()) { + startNewPage(); + } + + // increase capacity, if necessary + if (nextGroupId >= maxFill) { + rehash(maxFill * 2); + } + return groupId; + } + + private void startNewPage() + { + if (currentPageBuilder != null) { + completedPagesMemorySize += currentPageBuilder.getSizeInBytes(); + } + + currentPageBuilder = new PageBuilder(types); + for (int i = 0; i < types.size(); i++) { + channelBuilders.get(i).add(currentPageBuilder.getBlockBuilder(i)); + } + } + + private void rehash(int size) + { + int newSize = arraySize(size + 1, FILL_RATIO); + + int newMask = newSize - 1; + long[] newKey = new long[newSize]; + Arrays.fill(newKey, -1); + int[] newValue = new int[newSize]; + + int oldIndex = 0; + for (int groupId = 0; groupId < nextGroupId; groupId++) { + // seek to the next used slot + while (groupAddressByHash[oldIndex] == -1) { + oldIndex++; + } + + // get the address for this slot + long address = groupAddressByHash[oldIndex]; + + // find an empty slot for the address + int pos = getHashPosition(hashPosition(address), newMask); + while (newKey[pos] != -1) { + pos = (pos + 1) & newMask; + } + + // record the mapping + newKey[pos] = address; + newValue[pos] = groupIdsByHash[oldIndex]; + oldIndex++; + } + + this.mask = newMask; + this.maxFill = calculateMaxFill(newSize); + this.groupAddressByHash = newKey; + this.groupIdsByHash = newValue; + groupAddressByGroupId.ensureCapacity(maxFill); + } + + private Block[] extractHashColumns(Page page) + { + Block[] hashBlocks = new Block[channels.length]; + for (int i = 0; i < channels.length; i++) { + hashBlocks[i] = page.getBlock(channels[i]); + } + return hashBlocks; + } + + private int hashPosition(long sliceAddress) + { + int sliceIndex = decodeSliceIndex(sliceAddress); + int position = decodePosition(sliceAddress); + if (precomputedHashChannel.isPresent()) { + return getRawHash(sliceIndex, position); + } + return hashStrategy.hashPosition(sliceIndex, position); + } + + private int getRawHash(int sliceIndex, int position) + { + return (int) channelBuilders.get(precomputedHashChannel.get()).get(sliceIndex).getLong(position, 0); + } + + private boolean positionEqualsCurrentRow(int sliceIndex, int slicePosition, int position, Block[] blocks) + { + return hashStrategy.positionEqualsRow(sliceIndex, slicePosition, position, blocks); + } + + private static int getHashPosition(int rawHash, int mask) + { + return murmurHash3(rawHash) & mask; + } + + private static int calculateMaxFill(int hashSize) + { + checkArgument(hashSize > 0, "hashSize must greater than 0"); + int maxFill = (int) Math.ceil(hashSize * FILL_RATIO); + if (maxFill == hashSize) { + maxFill--; + } + checkArgument(hashSize > maxFill, "hashSize must be larger than maxFill"); + return maxFill; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/Operator.java b/presto-main/src/main/java/com/facebook/presto/operator/Operator.java new file mode 100644 index 00000000..3e92518a --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/Operator.java @@ -0,0 +1,83 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.type.Type; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; + +import java.util.List; + +public interface Operator + extends AutoCloseable +{ + ListenableFuture NOT_BLOCKED = Futures.immediateFuture(null); + + OperatorContext getOperatorContext(); + + /** + * Gets the column types of pages produced by this operator. + */ + List getTypes(); + + /** + * Notifies the operator that no more pages will be added and the + * operator should finish processing and flush results. This method + * will not be called if the Task is already failed or canceled. + */ + void finish(); + + /** + * Is this operator completely finished processing and no more + * output pages will be produced. + */ + boolean isFinished(); + + /** + * Returns a future that will be completed when the operator becomes + * unblocked. If the operator is not blocked, this method should return + * {@code NOT_BLOCKED}. + */ + default ListenableFuture isBlocked() + { + return NOT_BLOCKED; + } + + /** + * Returns true if and only if this operator can accept an input page. + */ + boolean needsInput(); + + /** + * Adds an input page to the operator. This method will only be called if + * {@code needsInput()} returns true. + */ + void addInput(Page page); + + /** + * Gets an output page from the operator. If no output data is currently + * available, return null. + */ + Page getOutput(); + + /** + * This method will always be called before releasing the Operator reference. + */ + @Override + default void close() + throws Exception + { + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/OperatorContext.java b/presto-main/src/main/java/com/facebook/presto/operator/OperatorContext.java new file mode 100644 index 00000000..472b3cee --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/OperatorContext.java @@ -0,0 +1,425 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.ExceededMemoryLimitException; +import com.facebook.presto.Session; +import com.facebook.presto.spi.Page; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import io.airlift.stats.CounterStat; +import io.airlift.units.DataSize; +import io.airlift.units.Duration; + +import javax.annotation.concurrent.ThreadSafe; + +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadMXBean; +import java.util.Optional; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +import static com.facebook.presto.operator.BlockedReason.WAITING_FOR_MEMORY; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.airlift.units.DataSize.Unit.BYTE; +import static java.util.concurrent.TimeUnit.NANOSECONDS; + +@ThreadSafe +public class OperatorContext +{ + private static final ThreadMXBean THREAD_MX_BEAN = ManagementFactory.getThreadMXBean(); + + private final int operatorId; + private final String operatorType; + private final DriverContext driverContext; + private final Executor executor; + + private final AtomicLong intervalWallStart = new AtomicLong(); + private final AtomicLong intervalCpuStart = new AtomicLong(); + private final AtomicLong intervalUserStart = new AtomicLong(); + + private final AtomicLong addInputCalls = new AtomicLong(); + private final AtomicLong addInputWallNanos = new AtomicLong(); + private final AtomicLong addInputCpuNanos = new AtomicLong(); + private final AtomicLong addInputUserNanos = new AtomicLong(); + private final CounterStat inputDataSize = new CounterStat(); + private final CounterStat inputPositions = new CounterStat(); + + private final AtomicLong getOutputCalls = new AtomicLong(); + private final AtomicLong getOutputWallNanos = new AtomicLong(); + private final AtomicLong getOutputCpuNanos = new AtomicLong(); + private final AtomicLong getOutputUserNanos = new AtomicLong(); + private final CounterStat outputDataSize = new CounterStat(); + private final CounterStat outputPositions = new CounterStat(); + + private final AtomicReference> memoryFuture = new AtomicReference<>(); + private final AtomicReference blockedMonitor = new AtomicReference<>(); + private final AtomicLong blockedWallNanos = new AtomicLong(); + + private final AtomicLong finishCalls = new AtomicLong(); + private final AtomicLong finishWallNanos = new AtomicLong(); + private final AtomicLong finishCpuNanos = new AtomicLong(); + private final AtomicLong finishUserNanos = new AtomicLong(); + + private final AtomicLong memoryReservation = new AtomicLong(); + private final long maxMemoryReservation; + + private final AtomicReference> infoSupplier = new AtomicReference<>(); + private final boolean collectTimings; + + public OperatorContext(int operatorId, String operatorType, DriverContext driverContext, Executor executor, long maxMemoryReservation) + { + checkArgument(operatorId >= 0, "operatorId is negative"); + this.operatorId = operatorId; + this.maxMemoryReservation = maxMemoryReservation; + this.operatorType = checkNotNull(operatorType, "operatorType is null"); + this.driverContext = checkNotNull(driverContext, "driverContext is null"); + this.executor = checkNotNull(executor, "executor is null"); + SettableFuture future = SettableFuture.create(); + future.set(null); + this.memoryFuture.set(future); + + collectTimings = driverContext.isVerboseStats() && driverContext.isCpuTimerEnabled(); + } + + public int getOperatorId() + { + return operatorId; + } + + public String getOperatorType() + { + return operatorType; + } + + public DriverContext getDriverContext() + { + return driverContext; + } + + public Session getSession() + { + return driverContext.getSession(); + } + + public boolean isDone() + { + return driverContext.isDone(); + } + + public void startIntervalTimer() + { + intervalWallStart.set(System.nanoTime()); + intervalCpuStart.set(currentThreadCpuTime()); + intervalUserStart.set(currentThreadUserTime()); + } + + public void recordAddInput(Page page) + { + addInputCalls.incrementAndGet(); + recordInputWallNanos(nanosBetween(intervalWallStart.get(), System.nanoTime())); + addInputCpuNanos.getAndAdd(nanosBetween(intervalCpuStart.get(), currentThreadCpuTime())); + addInputUserNanos.getAndAdd(nanosBetween(intervalUserStart.get(), currentThreadUserTime())); + + if (page != null) { + inputDataSize.update(page.getSizeInBytes()); + inputPositions.update(page.getPositionCount()); + } + } + + public void recordGeneratedInput(long sizeInBytes, long positions) + { + recordGeneratedInput(sizeInBytes, positions, 0); + } + + public void recordGeneratedInput(long sizeInBytes, long positions, long readNanos) + { + inputDataSize.update(sizeInBytes); + inputPositions.update(positions); + recordInputWallNanos(readNanos); + } + + public long recordInputWallNanos(long readNanos) + { + return addInputWallNanos.getAndAdd(readNanos); + } + + public void recordGetOutput(Page page) + { + getOutputCalls.incrementAndGet(); + getOutputWallNanos.getAndAdd(nanosBetween(intervalWallStart.get(), System.nanoTime())); + getOutputCpuNanos.getAndAdd(nanosBetween(intervalCpuStart.get(), currentThreadCpuTime())); + getOutputUserNanos.getAndAdd(nanosBetween(intervalUserStart.get(), currentThreadUserTime())); + + if (page != null) { + outputDataSize.update(page.getSizeInBytes()); + outputPositions.update(page.getPositionCount()); + } + } + + public void recordGeneratedOutput(long sizeInBytes, long positions) + { + outputDataSize.update(sizeInBytes); + outputPositions.update(positions); + } + + public void recordBlocked(ListenableFuture blocked) + { + checkNotNull(blocked, "blocked is null"); + + BlockedMonitor monitor = new BlockedMonitor(); + + BlockedMonitor oldMonitor = blockedMonitor.getAndSet(monitor); + if (oldMonitor != null) { + oldMonitor.run(); + } + + blocked.addListener(monitor, executor); + // Do not register blocked with driver context. The driver handles this directly. + } + + public void recordFinish() + { + finishCalls.incrementAndGet(); + finishWallNanos.getAndAdd(nanosBetween(intervalWallStart.get(), System.nanoTime())); + finishCpuNanos.getAndAdd(nanosBetween(intervalCpuStart.get(), currentThreadCpuTime())); + finishUserNanos.getAndAdd(nanosBetween(intervalUserStart.get(), currentThreadUserTime())); + } + + public ListenableFuture isWaitingForMemory() + { + return memoryFuture.get(); + } + + public DataSize getMaxMemorySize() + { + return driverContext.getMaxMemorySize(); + } + + public DataSize getOperatorPreAllocatedMemory() + { + return driverContext.getOperatorPreAllocatedMemory(); + } + + public void reserveMemory(long bytes) + { + ListenableFuture future = driverContext.reserveMemory(bytes); + if (!future.isDone()) { + SettableFuture currentMemoryFuture = memoryFuture.get(); + while (currentMemoryFuture.isDone()) { + SettableFuture settableFuture = SettableFuture.create(); + // We can't replace one that's not done, because the task may be blocked on that future + if (memoryFuture.compareAndSet(currentMemoryFuture, settableFuture)) { + currentMemoryFuture = settableFuture; + } + else { + currentMemoryFuture = memoryFuture.get(); + } + } + + SettableFuture finalMemoryFuture = currentMemoryFuture; + // Create a new future, so that this operator can un-block before the pool does, if it's moved to a new pool + Futures.addCallback(future, new FutureCallback() + { + @Override + public void onSuccess(Object result) + { + finalMemoryFuture.set(null); + } + + @Override + public void onFailure(Throwable t) + { + finalMemoryFuture.set(null); + } + }); + } + long newReservation = memoryReservation.getAndAdd(bytes); + if (newReservation > maxMemoryReservation) { + memoryReservation.getAndAdd(-bytes); + throw new ExceededMemoryLimitException(getMaxMemorySize()); + } + } + + public boolean tryReserveMemory(long bytes) + { + if (!driverContext.tryReserveMemory(bytes)) { + return false; + } + + long newReservation = memoryReservation.getAndAdd(bytes); + if (newReservation > maxMemoryReservation) { + memoryReservation.getAndAdd(-bytes); + return false; + } + return true; + } + + public void freeMemory(long bytes) + { + checkArgument(bytes >= 0, "bytes is negative"); + checkArgument(bytes <= memoryReservation.get(), "tried to free more memory than is reserved"); + driverContext.freeMemory(bytes); + memoryReservation.getAndAdd(-bytes); + } + + public void moreMemoryAvailable() + { + memoryFuture.get().set(null); + } + + public void setMemoryReservation(long newMemoryReservation) + { + checkArgument(newMemoryReservation >= 0, "newMemoryReservation is negative"); + + long delta = newMemoryReservation - memoryReservation.get(); + + if (delta > 0) { + reserveMemory(delta); + } + else { + freeMemory(-delta); + } + } + + public boolean trySetMemoryReservation(long newMemoryReservation) + { + checkArgument(newMemoryReservation >= 0, "newMemoryReservation is negative"); + + long delta = newMemoryReservation - memoryReservation.get(); + + if (delta > 0) { + return tryReserveMemory(delta); + } + else { + freeMemory(-delta); + return true; + } + } + + public void setInfoSupplier(Supplier infoSupplier) + { + checkNotNull(infoSupplier, "infoProvider is null"); + this.infoSupplier.set(infoSupplier); + } + + public CounterStat getInputDataSize() + { + return inputDataSize; + } + + public CounterStat getInputPositions() + { + return inputPositions; + } + + public CounterStat getOutputDataSize() + { + return outputDataSize; + } + + public CounterStat getOutputPositions() + { + return outputPositions; + } + + public OperatorStats getOperatorStats() + { + Supplier infoSupplier = this.infoSupplier.get(); + Object info = null; + if (infoSupplier != null) { + info = infoSupplier.get(); + } + + return new OperatorStats( + operatorId, + operatorType, + + addInputCalls.get(), + new Duration(addInputWallNanos.get(), NANOSECONDS).convertToMostSuccinctTimeUnit(), + new Duration(addInputCpuNanos.get(), NANOSECONDS).convertToMostSuccinctTimeUnit(), + new Duration(addInputUserNanos.get(), NANOSECONDS).convertToMostSuccinctTimeUnit(), + new DataSize(inputDataSize.getTotalCount(), BYTE).convertToMostSuccinctDataSize(), + inputPositions.getTotalCount(), + + getOutputCalls.get(), + new Duration(getOutputWallNanos.get(), NANOSECONDS).convertToMostSuccinctTimeUnit(), + new Duration(getOutputCpuNanos.get(), NANOSECONDS).convertToMostSuccinctTimeUnit(), + new Duration(getOutputUserNanos.get(), NANOSECONDS).convertToMostSuccinctTimeUnit(), + new DataSize(outputDataSize.getTotalCount(), BYTE).convertToMostSuccinctDataSize(), + outputPositions.getTotalCount(), + + new Duration(blockedWallNanos.get(), NANOSECONDS).convertToMostSuccinctTimeUnit(), + + finishCalls.get(), + new Duration(finishWallNanos.get(), NANOSECONDS).convertToMostSuccinctTimeUnit(), + new Duration(finishCpuNanos.get(), NANOSECONDS).convertToMostSuccinctTimeUnit(), + new Duration(finishUserNanos.get(), NANOSECONDS).convertToMostSuccinctTimeUnit(), + + new DataSize(memoryReservation.get(), BYTE).convertToMostSuccinctDataSize(), + memoryFuture.get().isDone() ? Optional.empty() : Optional.of(WAITING_FOR_MEMORY), + info); + } + + private long currentThreadUserTime() + { + if (!collectTimings) { + return 0; + } + return THREAD_MX_BEAN.getCurrentThreadUserTime(); + } + + private long currentThreadCpuTime() + { + if (!collectTimings) { + return 0; + } + return THREAD_MX_BEAN.getCurrentThreadCpuTime(); + } + + private static long nanosBetween(long start, long end) + { + return Math.abs(end - start); + } + + private class BlockedMonitor + implements Runnable + { + private final long start = System.nanoTime(); + private boolean finished; + + @Override + public synchronized void run() + { + synchronized (this) { + if (finished) { + return; + } + finished = true; + blockedMonitor.compareAndSet(this, null); + blockedWallNanos.getAndAdd(getBlockedTime()); + } + } + + public long getBlockedTime() + { + return nanosBetween(start, System.nanoTime()); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/OperatorFactory.java b/presto-main/src/main/java/com/facebook/presto/operator/OperatorFactory.java new file mode 100644 index 00000000..5f37e465 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/OperatorFactory.java @@ -0,0 +1,30 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.type.Type; + +import java.io.Closeable; +import java.util.List; + +public interface OperatorFactory + extends Closeable +{ + List getTypes(); + + Operator createOperator(DriverContext driverContext); + + @Override + void close(); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/OperatorStats.java b/presto-main/src/main/java/com/facebook/presto/operator/OperatorStats.java new file mode 100644 index 00000000..e5a88f61 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/OperatorStats.java @@ -0,0 +1,366 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; +import io.airlift.units.DataSize; +import io.airlift.units.Duration; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.airlift.units.DataSize.Unit.BYTE; +import static java.util.concurrent.TimeUnit.NANOSECONDS; + +@Immutable +public class OperatorStats +{ + private final int operatorId; + private final String operatorType; + + private final long addInputCalls; + private final Duration addInputWall; + private final Duration addInputCpu; + private final Duration addInputUser; + private final DataSize inputDataSize; + private final long inputPositions; + + private final long getOutputCalls; + private final Duration getOutputWall; + private final Duration getOutputCpu; + private final Duration getOutputUser; + private final DataSize outputDataSize; + private final long outputPositions; + + private final Duration blockedWall; + + private final long finishCalls; + private final Duration finishWall; + private final Duration finishCpu; + private final Duration finishUser; + + private final DataSize memoryReservation; + private final Optional blockedReason; + + private final Object info; + + @JsonCreator + public OperatorStats( + @JsonProperty("operatorId") int operatorId, + @JsonProperty("operatorType") String operatorType, + + @JsonProperty("addInputCalls") long addInputCalls, + @JsonProperty("addInputWall") Duration addInputWall, + @JsonProperty("addInputCpu") Duration addInputCpu, + @JsonProperty("addInputUser") Duration addInputUser, + @JsonProperty("inputDataSize") DataSize inputDataSize, + @JsonProperty("inputPositions") long inputPositions, + + @JsonProperty("getOutputCalls") long getOutputCalls, + @JsonProperty("getOutputWall") Duration getOutputWall, + @JsonProperty("getOutputCpu") Duration getOutputCpu, + @JsonProperty("getOutputUser") Duration getOutputUser, + @JsonProperty("outputDataSize") DataSize outputDataSize, + @JsonProperty("outputPositions") long outputPositions, + + @JsonProperty("blockedWall") Duration blockedWall, + + @JsonProperty("finishCalls") long finishCalls, + @JsonProperty("finishWall") Duration finishWall, + @JsonProperty("finishCpu") Duration finishCpu, + @JsonProperty("finishUser") Duration finishUser, + + @JsonProperty("memoryReservation") DataSize memoryReservation, + @JsonProperty("blockedReason") Optional blockedReason, + + @JsonProperty("info") Object info) + { + checkArgument(operatorId >= 0, "operatorId is negative"); + this.operatorId = operatorId; + this.operatorType = checkNotNull(operatorType, "operatorType is null"); + + this.addInputCalls = addInputCalls; + this.addInputWall = checkNotNull(addInputWall, "addInputWall is null"); + this.addInputCpu = checkNotNull(addInputCpu, "addInputCpu is null"); + this.addInputUser = checkNotNull(addInputUser, "addInputUser is null"); + this.inputDataSize = checkNotNull(inputDataSize, "inputDataSize is null"); + checkArgument(inputPositions >= 0, "inputPositions is negative"); + this.inputPositions = inputPositions; + + this.getOutputCalls = getOutputCalls; + this.getOutputWall = checkNotNull(getOutputWall, "getOutputWall is null"); + this.getOutputCpu = checkNotNull(getOutputCpu, "getOutputCpu is null"); + this.getOutputUser = checkNotNull(getOutputUser, "getOutputUser is null"); + this.outputDataSize = checkNotNull(outputDataSize, "outputDataSize is null"); + checkArgument(outputPositions >= 0, "outputPositions is negative"); + this.outputPositions = outputPositions; + + this.blockedWall = checkNotNull(blockedWall, "blockedWall is null"); + + this.finishCalls = finishCalls; + this.finishWall = checkNotNull(finishWall, "finishWall is null"); + this.finishCpu = checkNotNull(finishCpu, "finishCpu is null"); + this.finishUser = checkNotNull(finishUser, "finishUser is null"); + + this.memoryReservation = checkNotNull(memoryReservation, "memoryReservation is null"); + this.blockedReason = blockedReason; + + this.info = info; + } + + @JsonProperty + public int getOperatorId() + { + return operatorId; + } + + @JsonProperty + public String getOperatorType() + { + return operatorType; + } + + @JsonProperty + public long getAddInputCalls() + { + return addInputCalls; + } + + @JsonProperty + public Duration getAddInputWall() + { + return addInputWall; + } + + @JsonProperty + public Duration getAddInputCpu() + { + return addInputCpu; + } + + @JsonProperty + public Duration getAddInputUser() + { + return addInputUser; + } + + @JsonProperty + public DataSize getInputDataSize() + { + return inputDataSize; + } + + @JsonProperty + public long getInputPositions() + { + return inputPositions; + } + + @JsonProperty + public long getGetOutputCalls() + { + return getOutputCalls; + } + + @JsonProperty + public Duration getGetOutputWall() + { + return getOutputWall; + } + + @JsonProperty + public Duration getGetOutputCpu() + { + return getOutputCpu; + } + + @JsonProperty + public Duration getGetOutputUser() + { + return getOutputUser; + } + + @JsonProperty + public DataSize getOutputDataSize() + { + return outputDataSize; + } + + @JsonProperty + public long getOutputPositions() + { + return outputPositions; + } + + @JsonProperty + public Duration getBlockedWall() + { + return blockedWall; + } + + @JsonProperty + public long getFinishCalls() + { + return finishCalls; + } + + @JsonProperty + public Duration getFinishWall() + { + return finishWall; + } + + @JsonProperty + public Duration getFinishCpu() + { + return finishCpu; + } + + @JsonProperty + public Duration getFinishUser() + { + return finishUser; + } + + @JsonProperty + public DataSize getMemoryReservation() + { + return memoryReservation; + } + + @JsonProperty + public Optional getBlockedReason() + { + return blockedReason; + } + + @Nullable + @JsonProperty + public Object getInfo() + { + return info; + } + + public OperatorStats add(OperatorStats... operators) + { + return add(ImmutableList.copyOf(operators)); + } + + public OperatorStats add(Iterable operators) + { + long addInputCalls = this.addInputCalls; + long addInputWall = this.addInputWall.roundTo(NANOSECONDS); + long addInputCpu = this.addInputCpu.roundTo(NANOSECONDS); + long addInputUser = this.addInputUser.roundTo(NANOSECONDS); + long inputDataSize = this.inputDataSize.toBytes(); + long inputPositions = this.inputPositions; + + long getOutputCalls = this.getOutputCalls; + long getOutputWall = this.getOutputWall.roundTo(NANOSECONDS); + long getOutputCpu = this.getOutputCpu.roundTo(NANOSECONDS); + long getOutputUser = this.getOutputUser.roundTo(NANOSECONDS); + long outputDataSize = this.outputDataSize.toBytes(); + long outputPositions = this.outputPositions; + + long blockedWall = this.blockedWall.roundTo(NANOSECONDS); + + long finishCalls = this.finishCalls; + long finishWall = this.finishWall.roundTo(NANOSECONDS); + long finishCpu = this.finishCpu.roundTo(NANOSECONDS); + long finishUser = this.finishUser.roundTo(NANOSECONDS); + + long memoryReservation = this.memoryReservation.toBytes(); + Optional blockedReason = this.blockedReason; + + Mergeable base = null; + if (info instanceof Mergeable) { + base = (Mergeable) info; + } + for (OperatorStats operator : operators) { + checkArgument(operator.getOperatorId() == operatorId, "Expected operatorId to be %s but was %s", operatorId, operator.getOperatorId()); + + addInputCalls += operator.getAddInputCalls(); + addInputWall += operator.getAddInputWall().roundTo(NANOSECONDS); + addInputCpu += operator.getAddInputCpu().roundTo(NANOSECONDS); + addInputUser += operator.getAddInputUser().roundTo(NANOSECONDS); + inputDataSize += operator.getInputDataSize().toBytes(); + inputPositions += operator.getInputPositions(); + + getOutputCalls += operator.getGetOutputCalls(); + getOutputWall += operator.getGetOutputWall().roundTo(NANOSECONDS); + getOutputCpu += operator.getGetOutputCpu().roundTo(NANOSECONDS); + getOutputUser += operator.getGetOutputUser().roundTo(NANOSECONDS); + outputDataSize += operator.getOutputDataSize().toBytes(); + outputPositions += operator.getOutputPositions(); + + finishCalls += operator.getFinishCalls(); + finishWall += operator.getFinishWall().roundTo(NANOSECONDS); + finishCpu += operator.getFinishCpu().roundTo(NANOSECONDS); + finishUser += operator.getFinishUser().roundTo(NANOSECONDS); + + blockedWall += operator.getBlockedWall().roundTo(NANOSECONDS); + + memoryReservation += operator.getMemoryReservation().toBytes(); + if (operator.getBlockedReason().isPresent()) { + blockedReason = operator.getBlockedReason(); + } + + Object info = operator.getInfo(); + if (base != null && info != null && base.getClass() == info.getClass()) { + base = mergeInfo(base, info); + } + } + + return new OperatorStats( + operatorId, + operatorType, + + addInputCalls, + new Duration(addInputWall, NANOSECONDS).convertToMostSuccinctTimeUnit(), + new Duration(addInputCpu, NANOSECONDS).convertToMostSuccinctTimeUnit(), + new Duration(addInputUser, NANOSECONDS).convertToMostSuccinctTimeUnit(), + new DataSize(inputDataSize, BYTE).convertToMostSuccinctDataSize(), + inputPositions, + + getOutputCalls, + new Duration(getOutputWall, NANOSECONDS).convertToMostSuccinctTimeUnit(), + new Duration(getOutputCpu, NANOSECONDS).convertToMostSuccinctTimeUnit(), + new Duration(getOutputUser, NANOSECONDS).convertToMostSuccinctTimeUnit(), + new DataSize(outputDataSize, BYTE).convertToMostSuccinctDataSize(), + outputPositions, + + new Duration(blockedWall, NANOSECONDS).convertToMostSuccinctTimeUnit(), + + finishCalls, + new Duration(finishWall, NANOSECONDS).convertToMostSuccinctTimeUnit(), + new Duration(finishCpu, NANOSECONDS).convertToMostSuccinctTimeUnit(), + new Duration(finishUser, NANOSECONDS).convertToMostSuccinctTimeUnit(), + + new DataSize(memoryReservation, BYTE).convertToMostSuccinctDataSize(), + blockedReason, + + base); + } + + public static > Mergeable mergeInfo(Object base, Object other) + { + return ((T) base).mergeWith(((T) other)); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/OrderByOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/OrderByOperator.java new file mode 100644 index 00000000..60343653 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/OrderByOperator.java @@ -0,0 +1,207 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.block.SortOrder; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; +import com.google.common.primitives.Ints; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +public class OrderByOperator + implements Operator +{ + public static class OrderByOperatorFactory + implements OperatorFactory + { + private final int operatorId; + private final List sourceTypes; + private final List outputChannels; + private final int expectedPositions; + private final List sortChannels; + private final List sortOrder; + private final List types; + private boolean closed; + + public OrderByOperatorFactory( + int operatorId, + List sourceTypes, + List outputChannels, + int expectedPositions, + List sortChannels, + List sortOrder) + { + this.operatorId = operatorId; + this.sourceTypes = ImmutableList.copyOf(checkNotNull(sourceTypes, "sourceTypes is null")); + this.outputChannels = checkNotNull(outputChannels, "outputChannels is null"); + this.expectedPositions = expectedPositions; + this.sortChannels = ImmutableList.copyOf(checkNotNull(sortChannels, "sortChannels is null")); + this.sortOrder = ImmutableList.copyOf(checkNotNull(sortOrder, "sortOrder is null")); + + this.types = toTypes(sourceTypes, outputChannels); + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public Operator createOperator(DriverContext driverContext) + { + checkState(!closed, "Factory is already closed"); + + OperatorContext operatorContext = driverContext.addOperatorContext(operatorId, OrderByOperator.class.getSimpleName()); + return new OrderByOperator( + operatorContext, + sourceTypes, + outputChannels, + expectedPositions, + sortChannels, + sortOrder); + } + + @Override + public void close() + { + closed = true; + } + } + + private enum State + { + NEEDS_INPUT, + HAS_OUTPUT, + FINISHED + } + + private final OperatorContext operatorContext; + private final List sortChannels; + private final List sortOrder; + private final int[] outputChannels; + private final List types; + + private final PagesIndex pageIndex; + + private final PageBuilder pageBuilder; + private int currentPosition; + + private State state = State.NEEDS_INPUT; + + public OrderByOperator( + OperatorContext operatorContext, + List sourceTypes, + List outputChannels, + int expectedPositions, + List sortChannels, + List sortOrder) + { + this.operatorContext = checkNotNull(operatorContext, "operatorContext is null"); + this.outputChannels = Ints.toArray(checkNotNull(outputChannels, "outputChannels is null")); + this.types = toTypes(sourceTypes, outputChannels); + this.sortChannels = ImmutableList.copyOf(checkNotNull(sortChannels, "sortChannels is null")); + this.sortOrder = ImmutableList.copyOf(checkNotNull(sortOrder, "sortOrder is null")); + + this.pageIndex = new PagesIndex(sourceTypes, expectedPositions); + + this.pageBuilder = new PageBuilder(this.types); + } + + @Override + public OperatorContext getOperatorContext() + { + return operatorContext; + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public void finish() + { + if (state == State.NEEDS_INPUT) { + state = State.HAS_OUTPUT; + + // sort the index + pageIndex.sort(sortChannels, sortOrder); + } + } + + @Override + public boolean isFinished() + { + return state == State.FINISHED; + } + + @Override + public boolean needsInput() + { + return state == State.NEEDS_INPUT; + } + + @Override + public void addInput(Page page) + { + checkState(state == State.NEEDS_INPUT, "Operator is already finishing"); + checkNotNull(page, "page is null"); + + pageIndex.addPage(page); + operatorContext.setMemoryReservation(pageIndex.getEstimatedSize().toBytes()); + } + + @Override + public Page getOutput() + { + if (state != State.HAS_OUTPUT) { + return null; + } + + if (currentPosition >= pageIndex.getPositionCount()) { + state = State.FINISHED; + return null; + } + + // iterate through the positions sequentially until we have one full page + pageBuilder.reset(); + currentPosition = pageIndex.buildPage(currentPosition, outputChannels, pageBuilder); + + // output the page if we have any data + if (pageBuilder.isEmpty()) { + state = State.FINISHED; + return null; + } + + Page page = pageBuilder.build(); + return page; + } + + private static List toTypes(List sourceTypes, List outputChannels) + { + ImmutableList.Builder types = ImmutableList.builder(); + for (int channel : outputChannels) { + types.add(sourceTypes.get(channel)); + } + return types.build(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/OutputFactory.java b/presto-main/src/main/java/com/facebook/presto/operator/OutputFactory.java new file mode 100644 index 00000000..9e95d4fe --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/OutputFactory.java @@ -0,0 +1,23 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.type.Type; + +import java.util.List; + +public interface OutputFactory +{ + OperatorFactory createOutputOperator(int operatorId, List sourceTypes); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/PageBufferClientStatus.java b/presto-main/src/main/java/com/facebook/presto/operator/PageBufferClientStatus.java new file mode 100644 index 00000000..adfe3bdd --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/PageBufferClientStatus.java @@ -0,0 +1,114 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.joda.time.DateTime; + +import java.net.URI; + +import static com.google.common.base.MoreObjects.toStringHelper; + +public class PageBufferClientStatus +{ + private final URI uri; + private final String state; + private final DateTime lastUpdate; + private final int pagesReceived; + private final int requestsScheduled; + private final int requestsCompleted; + private final int requestsFailed; + private final String httpRequestState; + + @JsonCreator + public PageBufferClientStatus(@JsonProperty("uri") URI uri, + @JsonProperty("state") String state, + @JsonProperty("lastUpdate") DateTime lastUpdate, + @JsonProperty("pagesReceived") int pagesReceived, + @JsonProperty("requestsScheduled") int requestsScheduled, + @JsonProperty("requestsCompleted") int requestsCompleted, + @JsonProperty("requestsFailed") int requestsFailed, + @JsonProperty("httpRequestState") String httpRequestState) + { + this.uri = uri; + this.state = state; + this.lastUpdate = lastUpdate; + this.pagesReceived = pagesReceived; + this.requestsScheduled = requestsScheduled; + this.requestsCompleted = requestsCompleted; + this.requestsFailed = requestsFailed; + this.httpRequestState = httpRequestState; + } + + @JsonProperty + public URI getUri() + { + return uri; + } + + @JsonProperty + public String getState() + { + return state; + } + + @JsonProperty + public DateTime getLastUpdate() + { + return lastUpdate; + } + + @JsonProperty + public int getPagesReceived() + { + return pagesReceived; + } + + @JsonProperty + public int getRequestsScheduled() + { + return requestsScheduled; + } + + @JsonProperty + public int getRequestsCompleted() + { + return requestsCompleted; + } + + @JsonProperty + public int getRequestsFailed() + { + return requestsFailed; + } + + @JsonProperty + public String getHttpRequestState() + { + return httpRequestState; + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("uri", uri) + .add("state", state) + .add("lastUpdate", lastUpdate) + .add("pagesReceived", pagesReceived) + .add("httpRequestState", httpRequestState) + .toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/PageProcessor.java b/presto-main/src/main/java/com/facebook/presto/operator/PageProcessor.java new file mode 100644 index 00000000..0ee8a8ee --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/PageProcessor.java @@ -0,0 +1,23 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PageBuilder; + +public interface PageProcessor +{ + int process(ConnectorSession session, Page page, int start, int end, PageBuilder pageBuilder); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/PageSourceOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/PageSourceOperator.java new file mode 100644 index 00000000..8fc9f2c7 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/PageSourceOperator.java @@ -0,0 +1,109 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.ConnectorPageSource; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.type.Type; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; + +import java.io.Closeable; +import java.io.IOException; +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class PageSourceOperator + implements Operator, Closeable +{ + private final ConnectorPageSource pageSource; + private final List types; + private final OperatorContext operatorContext; + private long completedBytes; + private long readTimeNanos; + + public PageSourceOperator(ConnectorPageSource pageSource, List types, OperatorContext operatorContext) + { + this.pageSource = checkNotNull(pageSource, "pageSource is null"); + this.types = ImmutableList.copyOf(checkNotNull(types, "types is null")); + this.operatorContext = checkNotNull(operatorContext, "operatorContext is null"); + } + + @Override + public OperatorContext getOperatorContext() + { + return operatorContext; + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public void finish() + { + try { + pageSource.close(); + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } + + @Override + public boolean isFinished() + { + return pageSource.isFinished(); + } + + @Override + public boolean needsInput() + { + return false; + } + + @Override + public void addInput(Page page) + { + throw new UnsupportedOperationException(); + } + + @Override + public Page getOutput() + { + Page page = pageSource.getNextPage(); + if (page == null) { + return null; + } + + // update operator stats + long endCompletedBytes = pageSource.getCompletedBytes(); + long endReadTimeNanos = pageSource.getReadTimeNanos(); + operatorContext.recordGeneratedInput(endCompletedBytes - completedBytes, page.getPositionCount(), endReadTimeNanos - readTimeNanos); + completedBytes = endCompletedBytes; + readTimeNanos = endReadTimeNanos; + + return page; + } + + @Override + public void close() + throws IOException + { + pageSource.close(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/PageTooLargeException.java b/presto-main/src/main/java/com/facebook/presto/operator/PageTooLargeException.java new file mode 100644 index 00000000..31b3885f --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/PageTooLargeException.java @@ -0,0 +1,27 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.PrestoException; + +import static com.facebook.presto.spi.StandardErrorCode.PAGE_TOO_LARGE; + +public class PageTooLargeException + extends PrestoException +{ + public PageTooLargeException() + { + super(PAGE_TOO_LARGE, "Remote page is too large"); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/PageTransportErrorException.java b/presto-main/src/main/java/com/facebook/presto/operator/PageTransportErrorException.java new file mode 100644 index 00000000..6be2ec45 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/PageTransportErrorException.java @@ -0,0 +1,27 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.PrestoException; + +import static com.facebook.presto.spi.StandardErrorCode.PAGE_TRANSPORT_ERROR; + +public class PageTransportErrorException + extends PrestoException +{ + public PageTransportErrorException(String message) + { + super(PAGE_TRANSPORT_ERROR, message); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/PageTransportTimeoutException.java b/presto-main/src/main/java/com/facebook/presto/operator/PageTransportTimeoutException.java new file mode 100644 index 00000000..f9962fe7 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/PageTransportTimeoutException.java @@ -0,0 +1,27 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.PrestoException; + +import static com.facebook.presto.spi.StandardErrorCode.PAGE_TRANSPORT_TIMEOUT; + +public class PageTransportTimeoutException + extends PrestoException +{ + public PageTransportTimeoutException(String message, Throwable cause) + { + super(PAGE_TRANSPORT_TIMEOUT, message, cause); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/PagesHashStrategy.java b/presto-main/src/main/java/com/facebook/presto/operator/PagesHashStrategy.java new file mode 100644 index 00000000..8977c17a --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/PagesHashStrategy.java @@ -0,0 +1,61 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.block.Block; + +public interface PagesHashStrategy +{ + /** + * Gets the total of the columns held in in this PagesHashStrategy. This includes both the hashed + * and non-hashed columns. + */ + int getChannelCount(); + + /** + * Appends all values at the specified position to the page builder starting at {@code outputChannelOffset}. + */ + void appendTo(int blockIndex, int position, PageBuilder pageBuilder, int outputChannelOffset); + + /** + * Calculates the hash code the hashed columns in this PagesHashStrategy at the specified position. + */ + int hashPosition(int blockIndex, int position); + + /** + * Calculates the hash code at {@code position} in {@code blocks}. Blocks must have the same number of + * entries as the hashed columns and each entry is expected to be the same type. + */ + int hashRow(int position, Block... blocks); + + /** + * Compares the values in the specified blocks. The values are compared positionally, so {@code leftBlocks} + * and {@code rightBlocks} must have the same number of entries as the hashed columns and each entry + * is expected to be the same type. + */ + boolean rowEqualsRow(int leftPosition, Block[] leftBlocks, int rightPosition, Block[] rightBlocks); + + /** + * Compares the hashed columns in this PagesHashStrategy to the values in the specified blocks. The + * values are compared positionally, so {@code rightBlocks} must have the same number of entries as + * the hashed columns and each entry is expected to be the same type. + */ + boolean positionEqualsRow(int leftBlockIndex, int leftPosition, int rightPosition, Block... rightBlocks); + + /** + * Compares the hashed columns in this PagesHashStrategy at the specified positions. + */ + boolean positionEqualsPosition(int leftBlockIndex, int leftPosition, int rightBlockIndex, int rightPosition); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/PagesIndex.java b/presto-main/src/main/java/com/facebook/presto/operator/PagesIndex.java new file mode 100644 index 00000000..921b3f5a --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/PagesIndex.java @@ -0,0 +1,402 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.block.SortOrder; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.gen.JoinCompiler; +import com.facebook.presto.sql.gen.OrderingCompiler; +import com.google.common.collect.ImmutableList; +import io.airlift.log.Logger; +import io.airlift.slice.Slice; +import io.airlift.units.DataSize; +import it.unimi.dsi.fastutil.Swapper; +import it.unimi.dsi.fastutil.longs.LongArrayList; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; + +import java.util.List; +import java.util.Optional; + +import static com.facebook.presto.operator.SyntheticAddress.decodePosition; +import static com.facebook.presto.operator.SyntheticAddress.decodeSliceIndex; +import static com.facebook.presto.operator.SyntheticAddress.encodeSyntheticAddress; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.sql.gen.JoinCompiler.LookupSourceFactory; +import static com.facebook.presto.util.ImmutableCollectors.toImmutableList; +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.airlift.slice.SizeOf.sizeOf; +import static io.airlift.units.DataSize.Unit.BYTE; + +/** + * PagesIndex a low-level data structure which contains the address of every value position of every channel. + * This data structure is not general purpose and is designed for a few specific uses: + *
    + *
  • Sort via the {@link #sort} method
  • + *
  • Hash build via the {@link #createLookupSource} method
  • + *
  • Positional output via the {@link #appendTo} method
  • + *
+ */ +public class PagesIndex + implements Swapper +{ + private static final Logger log = Logger.get(PagesIndex.class); + + // todo this should be a services assigned in the constructor + private static final OrderingCompiler orderingCompiler = new OrderingCompiler(); + + // todo this should be a services assigned in the constructor + private static final JoinCompiler joinCompiler = new JoinCompiler(); + + private final List types; + private final LongArrayList valueAddresses; + private final ObjectArrayList[] channels; + + private int nextBlockToCompact; + private int positionCount; + private long pagesMemorySize; + private long estimatedSize; + + public PagesIndex(List types, int expectedPositions) + { + this.types = ImmutableList.copyOf(checkNotNull(types, "types is null")); + this.valueAddresses = new LongArrayList(expectedPositions); + + //noinspection rawtypes + channels = (ObjectArrayList[]) new ObjectArrayList[types.size()]; + for (int i = 0; i < channels.length; i++) { + channels[i] = ObjectArrayList.wrap(new Block[1024], 0); + } + } + + public List getTypes() + { + return types; + } + + public int getPositionCount() + { + return positionCount; + } + + public LongArrayList getValueAddresses() + { + return valueAddresses; + } + + public ObjectArrayList getChannel(int channel) + { + return channels[channel]; + } + + public void clear() + { + for (ObjectArrayList channel : channels) { + channel.clear(); + } + valueAddresses.clear(); + positionCount = 0; + pagesMemorySize = 0; + + estimatedSize = calculateEstimatedSize(); + } + + public void addPage(Page page) + { + // ignore empty pages + if (page.getPositionCount() == 0) { + return; + } + + positionCount += page.getPositionCount(); + + int pageIndex = (channels.length > 0) ? channels[0].size() : 0; + for (int i = 0; i < channels.length; i++) { + Block block = page.getBlock(i); + channels[i].add(block); + pagesMemorySize += block.getRetainedSizeInBytes(); + } + + for (int position = 0; position < page.getPositionCount(); position++) { + long sliceAddress = encodeSyntheticAddress(pageIndex, position); + valueAddresses.add(sliceAddress); + } + + estimatedSize = calculateEstimatedSize(); + } + + /** + * Add positions in page with the specified partitionId. + * NOTE: this method does not track memory used in the page, since + * only part of the page will be used. + */ + public void addPage(Page page, int partitionId, Block partitionIds) + { + // ignore empty pages + if (page.getPositionCount() == 0) { + return; + } + + positionCount += page.getPositionCount(); + + int pageIndex = (channels.length > 0) ? channels[0].size() : 0; + for (int i = 0; i < channels.length; i++) { + Block block = page.getBlock(i); + channels[i].add(block); + } + + for (int position = 0; position < page.getPositionCount(); position++) { + if (partitionId == BIGINT.getLong(partitionIds, position)) { + long sliceAddress = encodeSyntheticAddress(pageIndex, position); + valueAddresses.add(sliceAddress); + + positionCount++; + } + } + + estimatedSize = calculateEstimatedSize(); + } + + public DataSize getEstimatedSize() + { + return new DataSize(estimatedSize, BYTE); + } + + public void compact() + { + for (int channel = 0; channel < types.size(); channel++) { + Type type = types.get(channel); + ObjectArrayList blocks = channels[channel]; + for (int i = nextBlockToCompact; i < blocks.size(); i++) { + Block block = blocks.get(i); + if (block.getSizeInBytes() < block.getRetainedSizeInBytes()) { + // Copy the block to compact its size + Block compactedBlock = block.copyRegion(0, block.getPositionCount()); + blocks.set(i, compactedBlock); + pagesMemorySize -= block.getRetainedSizeInBytes(); + pagesMemorySize += compactedBlock.getRetainedSizeInBytes(); + } + } + } + nextBlockToCompact = channels[0].size(); + estimatedSize = calculateEstimatedSize(); + } + + private long calculateEstimatedSize() + { + long elementsSize = (channels.length > 0) ? sizeOf(channels[0].elements()) : 0; + long channelsArraySize = elementsSize * channels.length; + long addressesArraySize = sizeOf(valueAddresses.elements()); + return pagesMemorySize + channelsArraySize + addressesArraySize; + } + + public Type getType(int channel) + { + return types.get(channel); + } + + @Override + public void swap(int a, int b) + { + long[] elements = valueAddresses.elements(); + long temp = elements[a]; + elements[a] = elements[b]; + elements[b] = temp; + } + + public int buildPage(int position, int[] outputChannels, PageBuilder pageBuilder) + { + while (!pageBuilder.isFull() && position < positionCount) { + long pageAddress = valueAddresses.getLong(position); + int blockIndex = decodeSliceIndex(pageAddress); + int blockPosition = decodePosition(pageAddress); + + // append the row + pageBuilder.declarePosition(); + for (int i = 0; i < outputChannels.length; i++) { + int outputChannel = outputChannels[i]; + Type type = types.get(outputChannel); + Block block = this.channels[outputChannel].get(blockIndex); + type.appendTo(block, blockPosition, pageBuilder.getBlockBuilder(i)); + } + + position++; + } + + return position; + } + + public void appendTo(int channel, int position, BlockBuilder output) + { + long pageAddress = valueAddresses.getLong(position); + + Type type = types.get(channel); + Block block = channels[channel].get(decodeSliceIndex(pageAddress)); + int blockPosition = decodePosition(pageAddress); + type.appendTo(block, blockPosition, output); + } + + public boolean isNull(int channel, int position) + { + long pageAddress = valueAddresses.getLong(position); + + Block block = channels[channel].get(decodeSliceIndex(pageAddress)); + int blockPosition = decodePosition(pageAddress); + return block.isNull(blockPosition); + } + + public boolean getBoolean(int channel, int position) + { + long pageAddress = valueAddresses.getLong(position); + + Block block = channels[channel].get(decodeSliceIndex(pageAddress)); + int blockPosition = decodePosition(pageAddress); + return types.get(channel).getBoolean(block, blockPosition); + } + + public long getLong(int channel, int position) + { + long pageAddress = valueAddresses.getLong(position); + + Block block = channels[channel].get(decodeSliceIndex(pageAddress)); + int blockPosition = decodePosition(pageAddress); + return types.get(channel).getLong(block, blockPosition); + } + + public double getDouble(int channel, int position) + { + long pageAddress = valueAddresses.getLong(position); + + Block block = channels[channel].get(decodeSliceIndex(pageAddress)); + int blockPosition = decodePosition(pageAddress); + return types.get(channel).getDouble(block, blockPosition); + } + + public Slice getSlice(int channel, int position) + { + long pageAddress = valueAddresses.getLong(position); + + Block block = channels[channel].get(decodeSliceIndex(pageAddress)); + int blockPosition = decodePosition(pageAddress); + return types.get(channel).getSlice(block, blockPosition); + } + + public void sort(List sortChannels, List sortOrders) + { + sort(sortChannels, sortOrders, 0, getPositionCount()); + } + + public void sort(List sortChannels, List sortOrders, int startPosition, int endPosition) + { + createPagesIndexComparator(sortChannels, sortOrders).sort(this, startPosition, endPosition); + } + + public boolean positionEqualsPosition(PagesHashStrategy partitionHashStrategy, int leftPosition, int rightPosition) + { + long leftAddress = valueAddresses.getLong(leftPosition); + int leftPageIndex = decodeSliceIndex(leftAddress); + int leftPagePosition = decodePosition(leftAddress); + + long rightAddress = valueAddresses.getLong(rightPosition); + int rightPageIndex = decodeSliceIndex(rightAddress); + int rightPagePosition = decodePosition(rightAddress); + + return partitionHashStrategy.positionEqualsPosition(leftPageIndex, leftPagePosition, rightPageIndex, rightPagePosition); + } + + public boolean positionEqualsRow(PagesHashStrategy pagesHashStrategy, int indexPosition, int rowPosition, Block... row) + { + long pageAddress = valueAddresses.getLong(indexPosition); + int pageIndex = decodeSliceIndex(pageAddress); + int pagePosition = decodePosition(pageAddress); + + return pagesHashStrategy.positionEqualsRow(pageIndex, pagePosition, rowPosition, row); + } + + private PagesIndexOrdering createPagesIndexComparator(List sortChannels, List sortOrders) + { + List sortTypes = sortChannels.stream() + .map(types::get) + .collect(toImmutableList()); + return orderingCompiler.compilePagesIndexOrdering(sortTypes, sortChannels, sortOrders); + } + + public LookupSource createLookupSource(List joinChannels) + { + return createLookupSource(joinChannels, Optional.empty()); + } + + public PagesHashStrategy createPagesHashStrategy(List joinChannels, Optional hashChannel) + { + try { + return joinCompiler.compilePagesHashStrategyFactory(types, joinChannels) + .createPagesHashStrategy(ImmutableList.copyOf(channels), hashChannel); + } + catch (Exception e) { + log.error(e, "Lookup source compile failed for types=%s error=%s", types, e); + } + + // if compilation fails, use interpreter + return new SimplePagesHashStrategy(types, ImmutableList.>copyOf(channels), joinChannels, hashChannel); + } + + public LookupSource createLookupSource(List joinChannels, Optional hashChannel) + { + try { + LookupSourceFactory lookupSourceFactory = joinCompiler.compileLookupSourceFactory(types, joinChannels); + + ImmutableList.Builder joinChannelTypes = ImmutableList.builder(); + for (Integer joinChannel : joinChannels) { + joinChannelTypes.add(types.get(joinChannel)); + } + LookupSource lookupSource = lookupSourceFactory.createLookupSource( + valueAddresses, + joinChannelTypes.build(), + ImmutableList.>copyOf(channels), + hashChannel); + + return lookupSource; + } + catch (Exception e) { + log.error(e, "Lookup source compile failed for types=%s error=%s", types, e); + } + + // if compilation fails + PagesHashStrategy hashStrategy = new SimplePagesHashStrategy( + types, + ImmutableList.>copyOf(channels), + joinChannels, + hashChannel); + + ImmutableList.Builder hashTypes = ImmutableList.builder(); + for (Integer channel : joinChannels) { + hashTypes.add(types.get(channel)); + } + return new InMemoryJoinHash(valueAddresses, hashTypes.build(), hashStrategy); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("positionCount", positionCount) + .add("types", types) + .add("estimatedSize", estimatedSize) + .toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/PagesIndexComparator.java b/presto-main/src/main/java/com/facebook/presto/operator/PagesIndexComparator.java new file mode 100644 index 00000000..fea27b41 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/PagesIndexComparator.java @@ -0,0 +1,19 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +public interface PagesIndexComparator +{ + int compareTo(PagesIndex pagesIndex, int leftPosition, int rightPosition); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/PagesIndexOrdering.java b/presto-main/src/main/java/com/facebook/presto/operator/PagesIndexOrdering.java new file mode 100644 index 00000000..bd9d2a37 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/PagesIndexOrdering.java @@ -0,0 +1,162 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class PagesIndexOrdering +{ + private static final int SMALL = 7; + private static final int MEDIUM = 40; + + private final PagesIndexComparator comparator; + + public PagesIndexOrdering(PagesIndexComparator comparator) + { + this.comparator = checkNotNull(comparator, "comparator is null"); + } + + public PagesIndexComparator getComparator() + { + return comparator; + } + + public void sort(PagesIndex pagesIndex, int startPosition, int endPosition) + { + quickSort(pagesIndex, startPosition, endPosition); + } + + /** + * Sorts the specified range of elements using the specified swapper and according to the order induced by the specified + * comparator using quickSort. + *

+ *

The sorting algorithm is a tuned quickSort adapted from Jon L. Bentley and M. Douglas + * McIlroy, “Engineering a Sort Function”, Software: Practice and Experience, 23(11), pages + * 1249−1265, 1993. + * + * @param from the index of the first element (inclusive) to be sorted. + * @param to the index of the last element (exclusive) to be sorted. + */ + // note this code was forked from Fastutils + private void quickSort(PagesIndex pagesIndex, int from, int to) + { + int len = to - from; + // Insertion sort on smallest arrays + if (len < SMALL) { + for (int i = from; i < to; i++) { + for (int j = i; j > from && (comparator.compareTo(pagesIndex, j - 1, j) > 0); j--) { + pagesIndex.swap(j, j - 1); + } + } + return; + } + + // Choose a partition element, v + int m = from + len / 2; // Small arrays, middle element + if (len > SMALL) { + int l = from; + int n = to - 1; + if (len > MEDIUM) { // Big arrays, pseudomedian of 9 + int s = len / 8; + l = median3(pagesIndex, l, l + s, l + 2 * s); + m = median3(pagesIndex, m - s, m, m + s); + n = median3(pagesIndex, n - 2 * s, n - s, n); + } + m = median3(pagesIndex, l, m, n); // Mid-size, med of 3 + } + // int v = x[m]; + + int a = from; + int b = a; + int c = to - 1; + // Establish Invariant: v* (v)* v* + int d = c; + while (true) { + int comparison; + while (b <= c && ((comparison = comparator.compareTo(pagesIndex, b, m)) <= 0)) { + if (comparison == 0) { + if (a == m) { + m = b; // moving target; DELTA to JDK !!! + } + else if (b == m) { + m = a; // moving target; DELTA to JDK !!! + } + pagesIndex.swap(a++, b); + } + b++; + } + while (c >= b && ((comparison = comparator.compareTo(pagesIndex, c, m)) >= 0)) { + if (comparison == 0) { + if (c == m) { + m = d; // moving target; DELTA to JDK !!! + } + else if (d == m) { + m = c; // moving target; DELTA to JDK !!! + } + pagesIndex.swap(c, d--); + } + c--; + } + if (b > c) { + break; + } + if (b == m) { + m = d; // moving target; DELTA to JDK !!! + } + else if (c == m) { + m = c; // moving target; DELTA to JDK !!! + } + pagesIndex.swap(b++, c--); + } + + // Swap partition elements back to middle + int s; + int n = to; + s = Math.min(a - from, b - a); + vectorSwap(pagesIndex, from, b - s, s); + s = Math.min(d - c, n - d - 1); + vectorSwap(pagesIndex, b, n - s, s); + + // Recursively sort non-partition-elements + if ((s = b - a) > 1) { + quickSort(pagesIndex, from, from + s); + } + if ((s = d - c) > 1) { + quickSort(pagesIndex, n - s, n); + } + } + + /** + * Returns the index of the median of the three positions. + */ + private int median3(PagesIndex pagesIndex, int a, int b, int c) + { + int ab = comparator.compareTo(pagesIndex, a, b); + int ac = comparator.compareTo(pagesIndex, a, c); + int bc = comparator.compareTo(pagesIndex, b, c); + return (ab < 0 ? + (bc < 0 ? b : ac < 0 ? c : a) : + (bc > 0 ? b : ac > 0 ? c : a)); + } + + /** + * Swaps x[a .. (a+n-1)] with x[b .. (b+n-1)]. + */ + private void vectorSwap(PagesIndex pagesIndex, int from, int l, int s) + { + for (int i = 0; i < s; i++, from++, l++) { + pagesIndex.swap(from, l); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/ParallelHashBuilder.java b/presto-main/src/main/java/com/facebook/presto/operator/ParallelHashBuilder.java new file mode 100644 index 00000000..ff97f6eb --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/ParallelHashBuilder.java @@ -0,0 +1,430 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.block.BlockBuilderStatus; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; +import com.google.common.primitives.Ints; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; + +import javax.annotation.concurrent.ThreadSafe; + +import java.util.List; +import java.util.Optional; + +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static it.unimi.dsi.fastutil.HashCommon.murmurHash3; +import static java.util.Objects.requireNonNull; + +@ThreadSafe +public class ParallelHashBuilder +{ + private final List hashChannels; + private final Optional hashChannel; + private final int expectedPositions; + private final List> pagesIndexFutures; + private final List> lookupSourceFutures; + private final LookupSourceSupplier lookupSourceSupplier; + private final List types; + + public ParallelHashBuilder( + List types, + List hashChannels, + Optional hashChannel, + int expectedPositions, + int partitionCount) + { + this.types = ImmutableList.copyOf(requireNonNull(types, "types is null")); + this.hashChannels = ImmutableList.copyOf(requireNonNull(hashChannels, "hashChannels is null")); + this.hashChannel = requireNonNull(hashChannel, "hashChannel is null"); + checkArgument(expectedPositions >= 0, "expectedPositions is negative"); + this.expectedPositions = expectedPositions; + + checkArgument(Integer.bitCount(partitionCount) == 1, "partitionCount must be a power of 2"); + ImmutableList.Builder> pagesIndexFutures = ImmutableList.builder(); + ImmutableList.Builder> lookupSourceFutures = ImmutableList.builder(); + for (int i = 0; i < partitionCount; i++) { + pagesIndexFutures.add(SettableFuture.create()); + lookupSourceFutures.add(SettableFuture.create()); + } + this.pagesIndexFutures = pagesIndexFutures.build(); + this.lookupSourceFutures = lookupSourceFutures.build(); + + lookupSourceSupplier = new ParallelLookupSourceSupplier(types, hashChannels, this.lookupSourceFutures); + } + + public OperatorFactory getCollectOperatorFactory(int operatorId) + { + return new ParallelHashCollectOperatorFactory( + operatorId, + pagesIndexFutures, + types, + hashChannels, + hashChannel, + expectedPositions); + } + + public OperatorFactory getBuildOperatorFactory() + { + return new ParallelHashBuilderOperatorFactory( + 0, + types, + pagesIndexFutures, + lookupSourceFutures, + hashChannels, + hashChannel); + } + + public LookupSourceSupplier getLookupSourceSupplier() + { + return lookupSourceSupplier; + } + + private static class ParallelHashCollectOperatorFactory + implements OperatorFactory + { + private final int operatorId; + private final List> partitionFutures; + private final List types; + private final List hashChannels; + private final Optional hashChannel; + + private final int expectedPositions; + private boolean closed; + + public ParallelHashCollectOperatorFactory( + int operatorId, + List> partitionFutures, + List types, + List hashChannels, + Optional hashChannel, + int expectedPositions) + { + this.operatorId = operatorId; + this.partitionFutures = partitionFutures; + this.types = types; + this.hashChannels = hashChannels; + this.hashChannel = hashChannel; + this.expectedPositions = expectedPositions; + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public Operator createOperator(DriverContext driverContext) + { + checkState(!closed, "Factory is already closed"); + OperatorContext operatorContext = driverContext.addOperatorContext(operatorId, ParallelHashBuilder.class.getSimpleName()); + return new ParallelHashCollectOperator( + operatorContext, + partitionFutures, + types, + hashChannels, + hashChannel, + expectedPositions); + } + + @Override + public void close() + { + closed = true; + } + } + + private static class ParallelHashCollectOperator + implements Operator + { + private final OperatorContext operatorContext; + private final List> partitionFutures; + + private final HashGenerator hashGenerator; + private final int parallelStreamMask; + private final PagesIndex[] partitions; + private final List types; + + private boolean finished; + + public ParallelHashCollectOperator( + OperatorContext operatorContext, + List> partitionFutures, + List types, + List hashChannels, + Optional hashChannel, + int expectedPositions) + { + this.operatorContext = operatorContext; + this.partitionFutures = partitionFutures; + + this.types = types; + + if (hashChannel.isPresent()) { + this.hashGenerator = new PrecomputedHashGenerator(hashChannel.get()); + } + else { + ImmutableList.Builder hashChannelTypes = ImmutableList.builder(); + for (int channel : hashChannels) { + hashChannelTypes.add(types.get(channel)); + } + this.hashGenerator = new InterpretedHashGenerator(hashChannelTypes.build(), Ints.toArray(hashChannels)); + } + + parallelStreamMask = partitionFutures.size() - 1; + partitions = new PagesIndex[partitionFutures.size()]; + for (int partition = 0; partition < partitions.length; partition++) { + this.partitions[partition] = new PagesIndex(types, expectedPositions); + } + } + + @Override + public OperatorContext getOperatorContext() + { + return operatorContext; + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public void finish() + { + if (finished) { + return; + } + + for (int partition = 0; partition < partitions.length; partition++) { + partitionFutures.get(partition).set(partitions[partition]); + } + + finished = true; + } + + @Override + public boolean isFinished() + { + return finished; + } + + @Override + public boolean needsInput() + { + return !finished; + } + + @Override + public void addInput(Page page) + { + checkNotNull(page, "page is null"); + checkState(!isFinished(), "Operator is already finished"); + + // build a block containing the partition id of each position + BlockBuilder blockBuilder = BIGINT.createBlockBuilder(new BlockBuilderStatus(), page.getPositionCount()); + for (int position = 0; position < page.getPositionCount(); position++) { + int rawHash = hashGenerator.hashPosition(position, page); + int partition = murmurHash3(rawHash) & parallelStreamMask; + BIGINT.writeLong(blockBuilder, partition); + } + Block partitionIds = blockBuilder.build(); + + long size = 0; + for (int partition = 0; partition < partitions.length; partition++) { + PagesIndex index = partitions[partition]; + index.addPage(page, partition, partitionIds); + size += index.getEstimatedSize().toBytes(); + } + + operatorContext.setMemoryReservation(size); + operatorContext.recordGeneratedOutput(page.getSizeInBytes(), page.getPositionCount()); + } + + @Override + public Page getOutput() + { + return null; + } + } + + private static class ParallelHashBuilderOperatorFactory + implements OperatorFactory + { + private final int operatorId; + private final List types; + private final List> partitionFutures; + private final List> lookupSourceFutures; + private final List hashChannels; + private final Optional hashChannel; + + private int partition; + private boolean closed; + + public ParallelHashBuilderOperatorFactory( + int operatorId, + List types, + List> partitionFutures, + List> lookupSourceFutures, + List hashChannels, + Optional hashChannel) + { + this.operatorId = operatorId; + this.types = types; + this.partitionFutures = ImmutableList.copyOf(partitionFutures); + this.lookupSourceFutures = lookupSourceFutures; + + this.hashChannels = hashChannels; + this.hashChannel = hashChannel; + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public Operator createOperator(DriverContext driverContext) + { + checkState(!closed, "Factory is already closed"); + checkState(partition < lookupSourceFutures.size(), "All operators already created"); + + OperatorContext operatorContext = driverContext.addOperatorContext(operatorId, ParallelHashBuilder.class.getSimpleName()); + ParallelHashBuilderOperator parallelHashBuilderOperator = new ParallelHashBuilderOperator( + operatorContext, + types, + partitionFutures.get(partition), + lookupSourceFutures.get(partition), + hashChannels, + hashChannel); + + partition++; + + return parallelHashBuilderOperator; + } + + @Override + public void close() + { + closed = true; + } + } + + private static class ParallelHashBuilderOperator + implements Operator + { + private final OperatorContext operatorContext; + private final List types; + private final ListenableFuture pagesIndexFuture; + private final SettableFuture lookupSourceFuture; + private final List hashChannels; + private final Optional hashChannel; + + private boolean finished; + + public ParallelHashBuilderOperator( + OperatorContext operatorContext, + List types, + ListenableFuture pagesIndexFuture, + SettableFuture lookupSourceFuture, + List hashChannels, + Optional hashChannel) + { + this.operatorContext = operatorContext; + this.types = types; + this.pagesIndexFuture = pagesIndexFuture; + this.lookupSourceFuture = lookupSourceFuture; + + this.hashChannels = hashChannels; + this.hashChannel = hashChannel; + } + + @Override + public OperatorContext getOperatorContext() + { + return operatorContext; + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public ListenableFuture isBlocked() + { + if (pagesIndexFuture.isDone()) { + return NOT_BLOCKED; + } + return pagesIndexFuture; + } + + @Override + public void finish() + { + if (finished) { + return; + } + + PagesIndex pagesIndex = Futures.getUnchecked(pagesIndexFuture); + // Free memory, as the SharedLookupSource is going to take it over + operatorContext.setMemoryReservation(0); + SharedLookupSource sharedLookupSource = new SharedLookupSource(pagesIndex.createLookupSource(hashChannels, hashChannel), operatorContext.getDriverContext().getPipelineContext().getTaskContext()); + + if (!lookupSourceFuture.set(sharedLookupSource)) { + sharedLookupSource.freeMemory(); + sharedLookupSource.close(); + } + + finished = true; + } + + @Override + public boolean isFinished() + { + return finished; + } + + @Override + public boolean needsInput() + { + return false; + } + + @Override + public void addInput(Page page) + { + throw new UnsupportedOperationException(getClass().getName() + " can not take input"); + } + + @Override + public Page getOutput() + { + return null; + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/ParallelLookupSourceSupplier.java b/presto-main/src/main/java/com/facebook/presto/operator/ParallelLookupSourceSupplier.java new file mode 100644 index 00000000..c6cabcdc --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/ParallelLookupSourceSupplier.java @@ -0,0 +1,94 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import static com.facebook.presto.util.ImmutableCollectors.toImmutableList; +import static com.google.common.base.Preconditions.checkArgument; +import static java.util.Objects.requireNonNull; + +public final class ParallelLookupSourceSupplier + implements LookupSourceSupplier +{ + private final List types; + private final List hashChannelTypes; + private final ListenableFuture lookupSourceFuture; + private final List> partitions; + private final AtomicInteger referenceCount = new AtomicInteger(1); + + public ParallelLookupSourceSupplier(List types, List hashChannels, List> partitions) + { + this.types = ImmutableList.copyOf(requireNonNull(types, "types is null")); + this.partitions = requireNonNull(partitions, "partitions is null"); + + hashChannelTypes = hashChannels.stream() + .map(types::get) + .collect(toImmutableList()); + + checkArgument(Integer.bitCount(partitions.size()) == 1, "partitions must be a power of 2"); + lookupSourceFuture = Futures.transform(Futures.allAsList(partitions), (List input) -> { + return new PartitionedLookupSource(input, hashChannelTypes); + }); + + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public ListenableFuture getLookupSource(OperatorContext operatorContext) + { + return lookupSourceFuture; + } + + @Override + public void retain() + { + referenceCount.incrementAndGet(); + } + + @Override + public void release() + { + if (referenceCount.decrementAndGet() == 0) { + // We own the shared lookup sources, so we need to free their memory + for (ListenableFuture future : partitions) { + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(SharedLookupSource result) + { + result.freeMemory(); + } + + @Override + public void onFailure(Throwable t) + { + // ignored + } + }); + } + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/PartitionedLookupSource.java b/presto-main/src/main/java/com/facebook/presto/operator/PartitionedLookupSource.java new file mode 100644 index 00000000..3ddc35a7 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/PartitionedLookupSource.java @@ -0,0 +1,118 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.type.Type; +import it.unimi.dsi.fastutil.longs.LongIterator; + +import java.util.Arrays; +import java.util.List; + +import static it.unimi.dsi.fastutil.HashCommon.murmurHash3; + +public class PartitionedLookupSource + implements LookupSource +{ + private final LookupSource[] lookupSources; + private final HashGenerator hashGenerator; + private final int partitionMask; + + public PartitionedLookupSource(List lookupSources, List hashChannelTypes) + { + this.lookupSources = lookupSources.toArray(new LookupSource[lookupSources.size()]); + + // this generator is only used for getJoinPosition without a rawHash and in this case + // the hash channels are always packed in a page without extra columns + int[] hashChannels = new int[hashChannelTypes.size()]; + for (int i = 0; i < hashChannels.length; i++) { + hashChannels[i] = i; + } + this.hashGenerator = new InterpretedHashGenerator(hashChannelTypes, hashChannels); + + this.partitionMask = lookupSources.size() - 1; + } + + @Override + public int getChannelCount() + { + return lookupSources[0].getChannelCount(); + } + + @Override + public long getInMemorySizeInBytes() + { + return Arrays.stream(lookupSources).mapToLong(LookupSource::getInMemorySizeInBytes).sum(); + } + + @Override + public long getJoinPosition(int position, Page page) + { + return getJoinPosition(position, page, hashGenerator.hashPosition(position, page)); + } + + @Override + public long getJoinPosition(int position, Page page, int rawHash) + { + int partition = murmurHash3(rawHash) & partitionMask; + LookupSource lookupSource = lookupSources[partition]; + long joinPosition = lookupSource.getJoinPosition(position, page, rawHash); + if (joinPosition < 0) { + return joinPosition; + } + return encodePartitionedJoinPosition(partition, joinPosition); + } + + @Override + public long getNextJoinPosition(long partitionedJoinPosition) + { + int partition = (int) (partitionedJoinPosition & partitionMask); + long joinPosition = partitionedJoinPosition >>> lookupSources.length; + LookupSource lookupSource = lookupSources[partition]; + long nextJoinPosition = lookupSource.getNextJoinPosition(joinPosition); + if (nextJoinPosition < 0) { + return nextJoinPosition; + } + return encodePartitionedJoinPosition(partition, nextJoinPosition); + } + + @Override + public void appendTo(long partitionedJoinPosition, PageBuilder pageBuilder, int outputChannelOffset) + { + int partition = (int) (partitionedJoinPosition & partitionMask); + long joinPosition = partitionedJoinPosition >>> lookupSources.length; + LookupSource lookupSource = lookupSources[partition]; + lookupSource.appendTo(joinPosition, pageBuilder, outputChannelOffset); + } + + @Override + public LongIterator getUnvisitedJoinPositions() + { + throw new UnsupportedOperationException(); + } + + @Override + public void close() + { + for (LookupSource lookupSource : lookupSources) { + lookupSource.close(); + } + } + + private long encodePartitionedJoinPosition(int partition, long joinPosition) + { + return (joinPosition << lookupSources.length) | (partition); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/PartitionedOutputOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/PartitionedOutputOperator.java new file mode 100644 index 00000000..7590fba6 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/PartitionedOutputOperator.java @@ -0,0 +1,251 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.HashPagePartitionFunction; +import com.facebook.presto.OutputBuffers; +import com.facebook.presto.PagePartitionFunction; +import com.facebook.presto.execution.SharedBuffer; +import com.facebook.presto.execution.TaskId; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static com.facebook.presto.operator.HashGenerator.createHashGenerator; +import static com.facebook.presto.util.ImmutableCollectors.toImmutableList; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.util.concurrent.Futures.getUnchecked; +import static java.util.Objects.requireNonNull; + +public class PartitionedOutputOperator + implements Operator +{ + public static class PartitionedOutputFactory + implements OutputFactory + { + private final SharedBuffer sharedBuffer; + + public PartitionedOutputFactory(SharedBuffer sharedBuffer) + { + this.sharedBuffer = requireNonNull(sharedBuffer, "sharedBuffer is null"); + } + + @Override + public OperatorFactory createOutputOperator(int operatorId, List sourceTypes) + { + return new PartitionedOutputOperatorFactory(operatorId, sourceTypes, sharedBuffer); + } + } + + public static class PartitionedOutputOperatorFactory + implements OperatorFactory + { + private final int operatorId; + private final List sourceTypes; + private final SharedBuffer sharedBuffer; + + public PartitionedOutputOperatorFactory(int operatorId, List sourceTypes, SharedBuffer sharedBuffer) + { + this.operatorId = operatorId; + this.sourceTypes = requireNonNull(sourceTypes, "sourceTypes is null"); + this.sharedBuffer = requireNonNull(sharedBuffer, "sharedBuffer is null"); + } + + @Override + public List getTypes() + { + return ImmutableList.of(); + } + + @Override + public Operator createOperator(DriverContext driverContext) + { + OperatorContext operatorContext = driverContext.addOperatorContext(operatorId, PartitionedOutputOperator.class.getSimpleName()); + return new PartitionedOutputOperator(operatorContext, sourceTypes, sharedBuffer); + } + + @Override + public void close() + { + } + } + + private final OperatorContext operatorContext; + private final ListenableFuture partitionFunction; + private ListenableFuture blocked = NOT_BLOCKED; + private boolean finished; + + public PartitionedOutputOperator(OperatorContext operatorContext, List sourceTypes, SharedBuffer sharedBuffer) + { + this.operatorContext = requireNonNull(operatorContext, "operatorContext is null"); + this.partitionFunction = Futures.transform(sharedBuffer.getFinalOutputBuffers(), (OutputBuffers outputBuffers) -> { + return new PartitionFunction(sharedBuffer, sourceTypes, outputBuffers); + }); + } + + @Override + public OperatorContext getOperatorContext() + { + return operatorContext; + } + + @Override + public List getTypes() + { + return ImmutableList.of(); + } + + @Override + public void finish() + { + finished = true; + blocked = getUnchecked(partitionFunction).flush(true); + } + + @Override + public boolean isFinished() + { + return finished && isBlocked().isDone(); + } + + @Override + public ListenableFuture isBlocked() + { + if (!partitionFunction.isDone()) { + return partitionFunction; + } + if (blocked != NOT_BLOCKED && blocked.isDone()) { + blocked = NOT_BLOCKED; + } + return blocked; + } + + @Override + public boolean needsInput() + { + return !finished && isBlocked().isDone(); + } + + @Override + public void addInput(Page page) + { + requireNonNull(page, "page is null"); + checkState(isBlocked().isDone(), "output is already blocked"); + + if (page.getPositionCount() == 0) { + return; + } + + blocked = getUnchecked(partitionFunction).partitionPage(page); + + operatorContext.recordGeneratedOutput(page.getSizeInBytes(), page.getPositionCount()); + } + + @Override + public Page getOutput() + { + return null; + } + + private static class PartitionFunction + { + private final SharedBuffer sharedBuffer; + private final List sourceTypes; + private final HashGenerator hashGenerator; + private final int partitionCount; + private final List pageBuilders; + + public PartitionFunction(SharedBuffer sharedBuffer, List sourceTypes, OutputBuffers outputBuffers) + { + this.sharedBuffer = requireNonNull(sharedBuffer, "sharedBuffer is null"); + this.sourceTypes = requireNonNull(sourceTypes, "sourceTypes is null"); + + // verify output buffers are a complete set of hash partitions + checkArgument(outputBuffers.isNoMoreBufferIds(), "output buffers is not final version"); + Map buffers = outputBuffers.getBuffers(); + checkArgument(!buffers.isEmpty(), "output buffers is empty"); + checkArgument(buffers.values().stream().allMatch(HashPagePartitionFunction.class::isInstance), "All buffers must use hash partitioning"); + + List hashFunctions = buffers.values().stream() + .map(HashPagePartitionFunction.class::cast) + .collect(toImmutableList()); + + checkArgument(hashFunctions.stream() + .map(HashPagePartitionFunction::getPartitionCount) + .distinct().count() == 1, + "All buffers must have the same partition count"); + + checkArgument(hashFunctions.stream() + .map(HashPagePartitionFunction::getPartition) + .distinct().count() == hashFunctions.size(), + "All buffers must have a different partition"); + + HashPagePartitionFunction partitionFunction = hashFunctions.stream().findAny().get(); + hashGenerator = createHashGenerator(partitionFunction.getHashChannel(), partitionFunction.getPartitioningChannels(), partitionFunction.getTypes()); + + partitionCount = partitionFunction.getPartitionCount(); + + ImmutableList.Builder pageBuilders = ImmutableList.builder(); + for (int i = 0; i < partitionCount; i++) { + pageBuilders.add(new PageBuilder(sourceTypes)); + } + this.pageBuilders = pageBuilders.build(); + } + + public ListenableFuture partitionPage(Page page) + { + requireNonNull(page, "page is null"); + + for (int position = 0; position < page.getPositionCount(); position++) { + int partitionHashBucket = hashGenerator.getPartitionHashBucket(partitionCount, position, page); + PageBuilder pageBuilder = pageBuilders.get(partitionHashBucket); + pageBuilder.declarePosition(); + + for (int channel = 0; channel < sourceTypes.size(); channel++) { + Type type = sourceTypes.get(channel); + type.appendTo(page.getBlock(channel), position, pageBuilder.getBlockBuilder(channel)); + } + } + return flush(false); + } + + public ListenableFuture flush(boolean force) + { + // add all full pages to output buffer + List> blockedFutures = new ArrayList<>(); + for (int partition = 0; partition < partitionCount; partition++) { + PageBuilder partitionPageBuilder = pageBuilders.get(partition); + if (!partitionPageBuilder.isEmpty() && (force || partitionPageBuilder.isFull())) { + Page pagePartition = partitionPageBuilder.build(); + partitionPageBuilder.reset(); + + blockedFutures.add(sharedBuffer.enqueue(partition, pagePartition)); + } + } + ListenableFuture future = Futures.allAsList(blockedFutures); + if (future.isDone()) { + return NOT_BLOCKED; + } + return future; + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/PipelineContext.java b/presto-main/src/main/java/com/facebook/presto/operator/PipelineContext.java new file mode 100644 index 00000000..0260ec74 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/PipelineContext.java @@ -0,0 +1,415 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.Session; +import com.facebook.presto.execution.TaskId; +import com.facebook.presto.util.ImmutableCollectors; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Multimap; +import com.google.common.util.concurrent.ListenableFuture; +import io.airlift.stats.CounterStat; +import io.airlift.stats.Distribution; +import io.airlift.units.DataSize; +import io.airlift.units.Duration; + +import javax.annotation.concurrent.ThreadSafe; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map.Entry; +import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.Iterables.transform; +import static io.airlift.units.DataSize.Unit.BYTE; +import static java.util.concurrent.TimeUnit.NANOSECONDS; + +@ThreadSafe +public class PipelineContext +{ + private final TaskContext taskContext; + private final Executor executor; + + private final boolean inputPipeline; + private final boolean outputPipeline; + + private final List drivers = new CopyOnWriteArrayList<>(); + + private final AtomicInteger completedDrivers = new AtomicInteger(); + + private final AtomicLong memoryReservation = new AtomicLong(); + + private final Distribution queuedTime = new Distribution(); + private final Distribution elapsedTime = new Distribution(); + + private final AtomicLong totalScheduledTime = new AtomicLong(); + private final AtomicLong totalCpuTime = new AtomicLong(); + private final AtomicLong totalUserTime = new AtomicLong(); + private final AtomicLong totalBlockedTime = new AtomicLong(); + + private final CounterStat rawInputDataSize = new CounterStat(); + private final CounterStat rawInputPositions = new CounterStat(); + + private final CounterStat processedInputDataSize = new CounterStat(); + private final CounterStat processedInputPositions = new CounterStat(); + + private final CounterStat outputDataSize = new CounterStat(); + private final CounterStat outputPositions = new CounterStat(); + + private final ConcurrentMap operatorSummaries = new ConcurrentHashMap<>(); + + public PipelineContext(TaskContext taskContext, Executor executor, boolean inputPipeline, boolean outputPipeline) + { + this.inputPipeline = inputPipeline; + this.outputPipeline = outputPipeline; + this.taskContext = checkNotNull(taskContext, "taskContext is null"); + this.executor = checkNotNull(executor, "executor is null"); + } + + public TaskContext getTaskContext() + { + return taskContext; + } + + public TaskId getTaskId() + { + return taskContext.getTaskId(); + } + + public boolean isInputPipeline() + { + return inputPipeline; + } + + public boolean isOutputPipeline() + { + return outputPipeline; + } + + public DriverContext addDriverContext() + { + return addDriverContext(false); + } + + public DriverContext addDriverContext(boolean partitioned) + { + DriverContext driverContext = new DriverContext(this, executor, partitioned); + drivers.add(driverContext); + return driverContext; + } + + public Session getSession() + { + return taskContext.getSession(); + } + + public void driverFinished(DriverContext driverContext) + { + checkNotNull(driverContext, "driverContext is null"); + + if (!drivers.remove(driverContext)) { + throw new IllegalArgumentException("Unknown driver " + driverContext); + } + + DriverStats driverStats = driverContext.getDriverStats(); + + completedDrivers.getAndIncrement(); + + queuedTime.add(driverStats.getQueuedTime().roundTo(NANOSECONDS)); + elapsedTime.add(driverStats.getElapsedTime().roundTo(NANOSECONDS)); + + totalScheduledTime.getAndAdd(driverStats.getTotalScheduledTime().roundTo(NANOSECONDS)); + totalCpuTime.getAndAdd(driverStats.getTotalCpuTime().roundTo(NANOSECONDS)); + totalUserTime.getAndAdd(driverStats.getTotalUserTime().roundTo(NANOSECONDS)); + + totalBlockedTime.getAndAdd(driverStats.getTotalBlockedTime().roundTo(NANOSECONDS)); + + // merge the operator stats into the operator summary + List operators = driverStats.getOperatorStats(); + for (OperatorStats operator : operators) { + // TODO: replace with ConcurrentMap.compute() when we migrate to java 8 + OperatorStats updated; + OperatorStats current; + do { + current = operatorSummaries.get(operator.getOperatorId()); + if (current != null) { + updated = current.add(operator); + } + else { + updated = operator; + } + } + while (!compareAndSet(operatorSummaries, operator.getOperatorId(), current, updated)); + } + + rawInputDataSize.update(driverStats.getRawInputDataSize().toBytes()); + rawInputPositions.update(driverStats.getRawInputPositions()); + + processedInputDataSize.update(driverStats.getProcessedInputDataSize().toBytes()); + processedInputPositions.update(driverStats.getProcessedInputPositions()); + + outputDataSize.update(driverStats.getOutputDataSize().toBytes()); + outputPositions.update(driverStats.getOutputPositions()); + } + + public void start() + { + taskContext.start(); + } + + public void failed(Throwable cause) + { + taskContext.failed(cause); + } + + public boolean isDone() + { + return taskContext.isDone(); + } + + public DataSize getMaxMemorySize() + { + return taskContext.getMaxMemorySize(); + } + + public DataSize getOperatorPreAllocatedMemory() + { + return taskContext.getOperatorPreAllocatedMemory(); + } + + public synchronized ListenableFuture reserveMemory(long bytes) + { + ListenableFuture future = taskContext.reserveMemory(bytes); + memoryReservation.getAndAdd(bytes); + return future; + } + + public synchronized boolean tryReserveMemory(long bytes) + { + if (taskContext.tryReserveMemory(bytes)) { + memoryReservation.getAndAdd(bytes); + return true; + } + return false; + } + + public synchronized void freeMemory(long bytes) + { + checkArgument(bytes >= 0, "bytes is negative"); + checkArgument(bytes <= memoryReservation.get(), "tried to free more memory than is reserved"); + taskContext.freeMemory(bytes); + memoryReservation.getAndAdd(-bytes); + } + + public void moreMemoryAvailable() + { + drivers.stream().forEach(DriverContext::moreMemoryAvailable); + } + + public boolean isVerboseStats() + { + return taskContext.isVerboseStats(); + } + + public boolean isCpuTimerEnabled() + { + return taskContext.isCpuTimerEnabled(); + } + + public CounterStat getInputDataSize() + { + CounterStat stat = new CounterStat(); + stat.merge(rawInputDataSize); + for (DriverContext driver : drivers) { + stat.merge(driver.getInputDataSize()); + } + return stat; + } + + public CounterStat getInputPositions() + { + CounterStat stat = new CounterStat(); + stat.merge(rawInputPositions); + for (DriverContext driver : drivers) { + stat.merge(driver.getInputPositions()); + } + return stat; + } + + public CounterStat getOutputDataSize() + { + CounterStat stat = new CounterStat(); + stat.merge(outputDataSize); + for (DriverContext driver : drivers) { + stat.merge(driver.getOutputDataSize()); + } + return stat; + } + + public CounterStat getOutputPositions() + { + CounterStat stat = new CounterStat(); + stat.merge(outputPositions); + for (DriverContext driver : drivers) { + stat.merge(driver.getOutputPositions()); + } + return stat; + } + + public PipelineStats getPipelineStats() + { + List driverContexts = ImmutableList.copyOf(this.drivers); + + int totalDriers = completedDrivers.get() + driverContexts.size(); + int queuedDrivers = 0; + int queuedPartitionedDrivers = 0; + int runningDrivers = 0; + int runningPartitionedDrivers = 0; + int completedDrivers = this.completedDrivers.get(); + + Distribution queuedTime = new Distribution(this.queuedTime); + Distribution elapsedTime = new Distribution(this.elapsedTime); + + long totalScheduledTime = this.totalScheduledTime.get(); + long totalCpuTime = this.totalCpuTime.get(); + long totalUserTime = this.totalUserTime.get(); + long totalBlockedTime = this.totalBlockedTime.get(); + + long rawInputDataSize = this.rawInputDataSize.getTotalCount(); + long rawInputPositions = this.rawInputPositions.getTotalCount(); + + long processedInputDataSize = this.processedInputDataSize.getTotalCount(); + long processedInputPositions = this.processedInputPositions.getTotalCount(); + + long outputDataSize = this.outputDataSize.getTotalCount(); + long outputPositions = this.outputPositions.getTotalCount(); + + List drivers = new ArrayList<>(); + + Multimap runningOperators = ArrayListMultimap.create(); + for (DriverContext driverContext : driverContexts) { + DriverStats driverStats = driverContext.getDriverStats(); + drivers.add(driverStats); + + if (driverStats.getStartTime() == null) { + queuedDrivers++; + if (driverContext.isPartitioned()) { + queuedPartitionedDrivers++; + } + } + else { + runningDrivers++; + if (driverContext.isPartitioned()) { + runningPartitionedDrivers++; + } + } + + queuedTime.add(driverStats.getQueuedTime().roundTo(NANOSECONDS)); + elapsedTime.add(driverStats.getElapsedTime().roundTo(NANOSECONDS)); + + totalScheduledTime += driverStats.getTotalScheduledTime().roundTo(NANOSECONDS); + totalCpuTime += driverStats.getTotalCpuTime().roundTo(NANOSECONDS); + totalUserTime += driverStats.getTotalUserTime().roundTo(NANOSECONDS); + totalBlockedTime += driverStats.getTotalBlockedTime().roundTo(NANOSECONDS); + + List operators = ImmutableList.copyOf(transform(driverContext.getOperatorContexts(), OperatorContext::getOperatorStats)); + for (OperatorStats operator : operators) { + runningOperators.put(operator.getOperatorId(), operator); + } + + rawInputDataSize += driverStats.getRawInputDataSize().toBytes(); + rawInputPositions += driverStats.getRawInputPositions(); + + processedInputDataSize += driverStats.getProcessedInputDataSize().toBytes(); + processedInputPositions += driverStats.getProcessedInputPositions(); + + outputDataSize += driverStats.getOutputDataSize().toBytes(); + outputPositions += driverStats.getOutputPositions(); + } + + // merge the running operator stats into the operator summary + TreeMap operatorSummaries = new TreeMap<>(this.operatorSummaries); + for (Entry entry : runningOperators.entries()) { + OperatorStats current = operatorSummaries.get(entry.getKey()); + if (current == null) { + current = entry.getValue(); + } + else { + current = current.add(entry.getValue()); + } + operatorSummaries.put(entry.getKey(), current); + } + + ImmutableSet blockedReasons = drivers.stream() + .filter(driver -> driver.getEndTime() == null && driver.getStartTime() != null) + .flatMap(driver -> driver.getBlockedReasons().stream()) + .collect(ImmutableCollectors.toImmutableSet()); + boolean fullyBlocked = drivers.stream() + .filter(driver -> driver.getEndTime() == null && driver.getStartTime() != null) + .allMatch(DriverStats::isFullyBlocked); + return new PipelineStats( + inputPipeline, + outputPipeline, + + totalDriers, + queuedDrivers, + queuedPartitionedDrivers, + runningDrivers, + runningPartitionedDrivers, + completedDrivers, + + new DataSize(memoryReservation.get(), BYTE).convertToMostSuccinctDataSize(), + + queuedTime.snapshot(), + elapsedTime.snapshot(), + + new Duration(totalScheduledTime, NANOSECONDS).convertToMostSuccinctTimeUnit(), + new Duration(totalCpuTime, NANOSECONDS).convertToMostSuccinctTimeUnit(), + new Duration(totalUserTime, NANOSECONDS).convertToMostSuccinctTimeUnit(), + new Duration(totalBlockedTime, NANOSECONDS).convertToMostSuccinctTimeUnit(), + fullyBlocked && (runningDrivers > 0 || runningPartitionedDrivers > 0), + blockedReasons, + + new DataSize(rawInputDataSize, BYTE).convertToMostSuccinctDataSize(), + rawInputPositions, + + new DataSize(processedInputDataSize, BYTE).convertToMostSuccinctDataSize(), + processedInputPositions, + + new DataSize(outputDataSize, BYTE).convertToMostSuccinctDataSize(), + outputPositions, + + ImmutableList.copyOf(operatorSummaries.values()), + drivers); + } + + private static boolean compareAndSet(ConcurrentMap map, K key, V oldValue, V newValue) + { + if (oldValue == null) { + return map.putIfAbsent(key, newValue) == null; + } + + return map.replace(key, oldValue, newValue); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/PipelineStats.java b/presto-main/src/main/java/com/facebook/presto/operator/PipelineStats.java new file mode 100644 index 00000000..589cc6e6 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/PipelineStats.java @@ -0,0 +1,329 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import io.airlift.stats.Distribution.DistributionSnapshot; +import io.airlift.units.DataSize; +import io.airlift.units.Duration; + +import javax.annotation.concurrent.Immutable; + +import java.util.List; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.Objects.requireNonNull; + +@Immutable +public class PipelineStats +{ + private final boolean inputPipeline; + private final boolean outputPipeline; + + private final int totalDrivers; + private final int queuedDrivers; + private final int queuedPartitionedDrivers; + private final int runningDrivers; + private final int runningPartitionedDrivers; + private final int completedDrivers; + + private final DataSize memoryReservation; + + private final DistributionSnapshot queuedTime; + private final DistributionSnapshot elapsedTime; + + private final Duration totalScheduledTime; + private final Duration totalCpuTime; + private final Duration totalUserTime; + private final Duration totalBlockedTime; + private final boolean fullyBlocked; + private final Set blockedReasons; + + private final DataSize rawInputDataSize; + private final long rawInputPositions; + + private final DataSize processedInputDataSize; + private final long processedInputPositions; + + private final DataSize outputDataSize; + private final long outputPositions; + + private final List operatorSummaries; + private final List drivers; + + @JsonCreator + public PipelineStats( + @JsonProperty("inputPipeline") boolean inputPipeline, + @JsonProperty("outputPipeline") boolean outputPipeline, + + @JsonProperty("totalDrivers") int totalDrivers, + @JsonProperty("queuedDrivers") int queuedDrivers, + @JsonProperty("queuedPartitionedDrivers") int queuedPartitionedDrivers, + @JsonProperty("runningDrivers") int runningDrivers, + @JsonProperty("runningPartitionedDrivers") int runningPartitionedDrivers, + @JsonProperty("completedDrivers") int completedDrivers, + + @JsonProperty("memoryReservation") DataSize memoryReservation, + + @JsonProperty("queuedTime") DistributionSnapshot queuedTime, + @JsonProperty("elapsedTime") DistributionSnapshot elapsedTime, + + @JsonProperty("totalScheduledTime") Duration totalScheduledTime, + @JsonProperty("totalCpuTime") Duration totalCpuTime, + @JsonProperty("totalUserTime") Duration totalUserTime, + @JsonProperty("totalBlockedTime") Duration totalBlockedTime, + @JsonProperty("fullyBlocked") boolean fullyBlocked, + @JsonProperty("blockedReasons") Set blockedReasons, + + @JsonProperty("rawInputDataSize") DataSize rawInputDataSize, + @JsonProperty("rawInputPositions") long rawInputPositions, + + @JsonProperty("processedInputDataSize") DataSize processedInputDataSize, + @JsonProperty("processedInputPositions") long processedInputPositions, + + @JsonProperty("outputDataSize") DataSize outputDataSize, + @JsonProperty("outputPositions") long outputPositions, + + @JsonProperty("operatorSummaries") List operatorSummaries, + @JsonProperty("drivers") List drivers) + { + this.inputPipeline = inputPipeline; + this.outputPipeline = outputPipeline; + + checkArgument(totalDrivers >= 0, "totalDrivers is negative"); + this.totalDrivers = totalDrivers; + checkArgument(queuedDrivers >= 0, "queuedDrivers is negative"); + this.queuedDrivers = queuedDrivers; + checkArgument(queuedPartitionedDrivers >= 0, "queuedPartitionedDrivers is negative"); + this.queuedPartitionedDrivers = queuedPartitionedDrivers; + checkArgument(runningDrivers >= 0, "runningDrivers is negative"); + this.runningDrivers = runningDrivers; + checkArgument(runningPartitionedDrivers >= 0, "runningPartitionedDrivers is negative"); + this.runningPartitionedDrivers = runningPartitionedDrivers; + checkArgument(completedDrivers >= 0, "completedDrivers is negative"); + this.completedDrivers = completedDrivers; + + this.memoryReservation = checkNotNull(memoryReservation, "memoryReservation is null"); + + this.queuedTime = checkNotNull(queuedTime, "queuedTime is null"); + this.elapsedTime = checkNotNull(elapsedTime, "elapsedTime is null"); + this.totalScheduledTime = checkNotNull(totalScheduledTime, "totalScheduledTime is null"); + + this.totalCpuTime = checkNotNull(totalCpuTime, "totalCpuTime is null"); + this.totalUserTime = checkNotNull(totalUserTime, "totalUserTime is null"); + this.totalBlockedTime = checkNotNull(totalBlockedTime, "totalBlockedTime is null"); + this.fullyBlocked = fullyBlocked; + this.blockedReasons = ImmutableSet.copyOf(requireNonNull(blockedReasons, "blockedReasons is null")); + + this.rawInputDataSize = checkNotNull(rawInputDataSize, "rawInputDataSize is null"); + checkArgument(rawInputPositions >= 0, "rawInputPositions is negative"); + this.rawInputPositions = rawInputPositions; + + this.processedInputDataSize = checkNotNull(processedInputDataSize, "processedInputDataSize is null"); + checkArgument(processedInputPositions >= 0, "processedInputPositions is negative"); + this.processedInputPositions = processedInputPositions; + + this.outputDataSize = checkNotNull(outputDataSize, "outputDataSize is null"); + checkArgument(outputPositions >= 0, "outputPositions is negative"); + this.outputPositions = outputPositions; + + this.operatorSummaries = ImmutableList.copyOf(checkNotNull(operatorSummaries, "operatorSummaries is null")); + this.drivers = ImmutableList.copyOf(checkNotNull(drivers, "drivers is null")); + } + + @JsonProperty + public boolean isInputPipeline() + { + return inputPipeline; + } + + @JsonProperty + public boolean isOutputPipeline() + { + return outputPipeline; + } + + @JsonProperty + public int getTotalDrivers() + { + return totalDrivers; + } + + @JsonProperty + public int getQueuedDrivers() + { + return queuedDrivers; + } + + @JsonProperty + public int getQueuedPartitionedDrivers() + { + return queuedPartitionedDrivers; + } + + @JsonProperty + public int getRunningDrivers() + { + return runningDrivers; + } + + @JsonProperty + public int getRunningPartitionedDrivers() + { + return runningPartitionedDrivers; + } + + @JsonProperty + public int getCompletedDrivers() + { + return completedDrivers; + } + + @JsonProperty + public DataSize getMemoryReservation() + { + return memoryReservation; + } + + @JsonProperty + public DistributionSnapshot getQueuedTime() + { + return queuedTime; + } + + @JsonProperty + public DistributionSnapshot getElapsedTime() + { + return elapsedTime; + } + + @JsonProperty + public Duration getTotalScheduledTime() + { + return totalScheduledTime; + } + + @JsonProperty + public Duration getTotalCpuTime() + { + return totalCpuTime; + } + + @JsonProperty + public Duration getTotalUserTime() + { + return totalUserTime; + } + + @JsonProperty + public Duration getTotalBlockedTime() + { + return totalBlockedTime; + } + + @JsonProperty + public boolean isFullyBlocked() + { + return fullyBlocked; + } + + @JsonProperty + public Set getBlockedReasons() + { + return blockedReasons; + } + + @JsonProperty + public DataSize getRawInputDataSize() + { + return rawInputDataSize; + } + + @JsonProperty + public long getRawInputPositions() + { + return rawInputPositions; + } + + @JsonProperty + public DataSize getProcessedInputDataSize() + { + return processedInputDataSize; + } + + @JsonProperty + public long getProcessedInputPositions() + { + return processedInputPositions; + } + + @JsonProperty + public DataSize getOutputDataSize() + { + return outputDataSize; + } + + @JsonProperty + public long getOutputPositions() + { + return outputPositions; + } + + @JsonProperty + public List getOperatorSummaries() + { + return operatorSummaries; + } + + @JsonProperty + public List getDrivers() + { + return drivers; + } + + public PipelineStats summarize() + { + return new PipelineStats( + inputPipeline, + outputPipeline, + totalDrivers, + queuedDrivers, + queuedPartitionedDrivers, + runningDrivers, + runningPartitionedDrivers, + completedDrivers, + memoryReservation, + queuedTime, + elapsedTime, + totalScheduledTime, + totalCpuTime, + totalUserTime, + totalBlockedTime, + fullyBlocked, + blockedReasons, + rawInputDataSize, + rawInputPositions, + processedInputDataSize, + processedInputPositions, + outputDataSize, + outputPositions, + operatorSummaries, + ImmutableList.of()); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/PrecomputedHashGenerator.java b/presto-main/src/main/java/com/facebook/presto/operator/PrecomputedHashGenerator.java new file mode 100644 index 00000000..afdc0ec8 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/PrecomputedHashGenerator.java @@ -0,0 +1,43 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.type.BigintType; +import com.google.common.base.MoreObjects; + +public class PrecomputedHashGenerator + implements HashGenerator +{ + private final int hashChannel; + + public PrecomputedHashGenerator(int hashChannel) + { + this.hashChannel = hashChannel; + } + + @Override + public int hashPosition(int position, Page page) + { + return (int) BigintType.BIGINT.getLong(page.getBlock(hashChannel), position); + } + + @Override + public String toString() + { + return MoreObjects.toStringHelper(this) + .add("hashChannel", hashChannel) + .toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/ProjectionFunction.java b/presto-main/src/main/java/com/facebook/presto/operator/ProjectionFunction.java new file mode 100644 index 00000000..80ba28a1 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/ProjectionFunction.java @@ -0,0 +1,28 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.RecordCursor; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; + +public interface ProjectionFunction +{ + Type getType(); + + void project(int position, Block[] blocks, BlockBuilder output); + + void project(RecordCursor cursor, BlockBuilder output); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/ProjectionFunctions.java b/presto-main/src/main/java/com/facebook/presto/operator/ProjectionFunctions.java new file mode 100644 index 00000000..84d26c9b --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/ProjectionFunctions.java @@ -0,0 +1,92 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.RecordCursor; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; +import com.google.common.base.Preconditions; +import io.airlift.slice.Slice; + +public final class ProjectionFunctions +{ + private ProjectionFunctions() {} + + public static ProjectionFunction singleColumn(Type columnType, int channelIndex) + { + return new SingleColumnProjection(columnType, channelIndex); + } + + private static class SingleColumnProjection + implements ProjectionFunction + { + private final Type columnType; + private final int channelIndex; + + public SingleColumnProjection(Type columnType, int channelIndex) + { + Preconditions.checkNotNull(columnType, "columnType is null"); + Preconditions.checkArgument(channelIndex >= 0, "channelIndex is negative"); + + this.columnType = columnType; + this.channelIndex = channelIndex; + } + + @Override + public Type getType() + { + return columnType; + } + + @Override + public void project(int position, Block[] blocks, BlockBuilder output) + { + if (blocks[channelIndex].isNull(position)) { + output.appendNull(); + } + else { + columnType.appendTo(blocks[channelIndex], position, output); + } + } + + @Override + public void project(RecordCursor cursor, BlockBuilder output) + { + // record cursors have each value in a separate field + if (cursor.isNull(channelIndex)) { + output.appendNull(); + } + else { + Class javaType = columnType.getJavaType(); + if (javaType == boolean.class) { + columnType.writeBoolean(output, cursor.getBoolean(channelIndex)); + } + else if (javaType == long.class) { + columnType.writeLong(output, cursor.getLong(channelIndex)); + } + else if (javaType == double.class) { + columnType.writeDouble(output, cursor.getDouble(channelIndex)); + } + else if (javaType == Slice.class) { + Slice slice = cursor.getSlice(channelIndex); + columnType.writeSlice(output, slice, 0, slice.length()); + } + else { + throw new UnsupportedOperationException("not yet implemented: " + javaType); + } + } + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/RecordProjectOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/RecordProjectOperator.java new file mode 100644 index 00000000..15a72df4 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/RecordProjectOperator.java @@ -0,0 +1,170 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.RecordCursor; +import com.facebook.presto.spi.RecordSet; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; + +import java.io.Closeable; +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class RecordProjectOperator + implements Operator, Closeable +{ + private static final int ROWS_PER_REQUEST = 4096; + private final OperatorContext operatorContext; + private final RecordCursor cursor; + private final List types; + private final PageBuilder pageBuilder; + private boolean finishing; + private long completedBytes; + private long readTimeNanos; + + public RecordProjectOperator(OperatorContext operatorContext, RecordSet recordSet) + { + this(operatorContext, recordSet.getColumnTypes(), recordSet.cursor()); + } + + public RecordProjectOperator(OperatorContext operatorContext, List columnTypes, RecordCursor cursor) + { + this.operatorContext = checkNotNull(operatorContext, "operatorContext is null"); + this.cursor = checkNotNull(cursor, "cursor is null"); + + ImmutableList.Builder types = ImmutableList.builder(); + for (Type columnType : columnTypes) { + types.add(columnType); + } + this.types = types.build(); + + pageBuilder = new PageBuilder(getTypes()); + } + + @Override + public OperatorContext getOperatorContext() + { + return operatorContext; + } + + public RecordCursor getCursor() + { + return cursor; + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public void finish() + { + close(); + } + + @Override + public void close() + { + finishing = true; + cursor.close(); + } + + @Override + public boolean isFinished() + { + return finishing && pageBuilder.isEmpty(); + } + + @Override + public boolean needsInput() + { + return false; + } + + @Override + public void addInput(Page page) + { + throw new UnsupportedOperationException(getClass().getName() + " can not take input"); + } + + @Override + public Page getOutput() + { + if (!finishing) { + int i; + for (i = 0; i < ROWS_PER_REQUEST; i++) { + if (pageBuilder.isFull()) { + break; + } + + if (!cursor.advanceNextPosition()) { + finishing = true; + break; + } + + pageBuilder.declarePosition(); + for (int column = 0; column < types.size(); column++) { + BlockBuilder output = pageBuilder.getBlockBuilder(column); + if (cursor.isNull(column)) { + output.appendNull(); + } + else { + Type type = getTypes().get(column); + Class javaType = type.getJavaType(); + if (javaType == boolean.class) { + type.writeBoolean(output, cursor.getBoolean(column)); + } + else if (javaType == long.class) { + type.writeLong(output, cursor.getLong(column)); + } + else if (javaType == double.class) { + type.writeDouble(output, cursor.getDouble(column)); + } + else if (javaType == Slice.class) { + Slice slice = cursor.getSlice(column); + type.writeSlice(output, slice, 0, slice.length()); + } + else { + throw new AssertionError("Unimplemented type: " + javaType.getName()); + } + } + } + } + + long endCompletedBytes = cursor.getCompletedBytes(); + long endReadTimeNanos = cursor.getReadTimeNanos(); + operatorContext.recordGeneratedInput(endCompletedBytes - completedBytes, i, endReadTimeNanos - readTimeNanos); + completedBytes = endCompletedBytes; + readTimeNanos = endReadTimeNanos; + } + + // only return a full page is buffer is full or we are finishing + if (pageBuilder.isEmpty() || (!finishing && !pageBuilder.isFull())) { + return null; + } + + Page page = pageBuilder.build(); + pageBuilder.reset(); + + return page; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/RowComparator.java b/presto-main/src/main/java/com/facebook/presto/operator/RowComparator.java new file mode 100644 index 00000000..15876bca --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/RowComparator.java @@ -0,0 +1,61 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.SortOrder; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; + +import java.util.Comparator; +import java.util.List; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class RowComparator + implements Comparator +{ + private final List sortTypes; + private final List sortChannels; + private final List sortOrders; + + public RowComparator(List sortTypes, List sortChannels, List sortOrders) + { + this.sortTypes = ImmutableList.copyOf(checkNotNull(sortTypes, "sortTypes is null")); + this.sortChannels = ImmutableList.copyOf(checkNotNull(sortChannels, "sortChannels is null")); + this.sortOrders = ImmutableList.copyOf(checkNotNull(sortOrders, "sortOrders is null")); + checkArgument(sortTypes.size() == sortChannels.size(), "sortTypes size (%s) doesn't match sortChannels size (%s)", sortTypes.size(), sortChannels.size()); + checkArgument(sortChannels.size() == sortOrders.size(), "sortFields size (%s) doesn't match sortOrders size (%s)", sortChannels.size(), sortOrders.size()); + } + + @Override + public int compare(Block[] leftRow, Block[] rightRow) + { + for (int index = 0; index < sortChannels.size(); index++) { + Type type = sortTypes.get(index); + int channel = sortChannels.get(index); + SortOrder sortOrder = sortOrders.get(index); + + Block left = leftRow[channel]; + Block right = rightRow[channel]; + + int comparison = sortOrder.compareBlockValue(type, left, 0, right, 0); + if (comparison != 0) { + return comparison; + } + } + return 0; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/RowNumberOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/RowNumberOperator.java new file mode 100644 index 00000000..fde3ed01 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/RowNumberOperator.java @@ -0,0 +1,287 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.util.array.LongBigArray; +import com.google.common.collect.ImmutableList; +import com.google.common.primitives.Ints; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import static com.facebook.presto.operator.GroupByHash.createGroupByHash; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +public class RowNumberOperator + implements Operator +{ + public static class RowNumberOperatorFactory + implements OperatorFactory + { + private final int operatorId; + private final Optional maxRowsPerPartition; + private final List sourceTypes; + private final List outputChannels; + private final List partitionChannels; + private final List partitionTypes; + private final Optional hashChannel; + private final int expectedPositions; + private final List types; + private boolean closed; + + public RowNumberOperatorFactory( + int operatorId, + List sourceTypes, + List outputChannels, + List partitionChannels, + List partitionTypes, + Optional maxRowsPerPartition, + Optional hashChannel, + int expectedPositions) + { + this.operatorId = operatorId; + this.sourceTypes = ImmutableList.copyOf(sourceTypes); + this.outputChannels = ImmutableList.copyOf(checkNotNull(outputChannels, "outputChannels is null")); + this.partitionChannels = ImmutableList.copyOf(checkNotNull(partitionChannels, "partitionChannels is null")); + this.partitionTypes = ImmutableList.copyOf(checkNotNull(partitionTypes, "partitionTypes is null")); + this.maxRowsPerPartition = checkNotNull(maxRowsPerPartition, "maxRowsPerPartition is null"); + + this.hashChannel = checkNotNull(hashChannel, "hashChannel is null"); + checkArgument(expectedPositions > 0, "expectedPositions < 0"); + this.expectedPositions = expectedPositions; + this.types = toTypes(sourceTypes, outputChannels); + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public Operator createOperator(DriverContext driverContext) + { + checkState(!closed, "Factory is already closed"); + + OperatorContext operatorContext = driverContext.addOperatorContext(operatorId, RowNumberOperator.class.getSimpleName()); + return new RowNumberOperator( + operatorContext, + sourceTypes, + outputChannels, + partitionChannels, + partitionTypes, + maxRowsPerPartition, + hashChannel, + expectedPositions); + } + + @Override + public void close() + { + closed = true; + } + } + + private final OperatorContext operatorContext; + private boolean finishing; + + private final int[] outputChannels; + private final List types; + + private GroupByIdBlock partitionIds; + private final Optional groupByHash; + + private Page inputPage; + private final LongBigArray partitionRowCount; + private final Optional maxRowsPerPartition; + + public RowNumberOperator( + OperatorContext operatorContext, + List sourceTypes, + List outputChannels, + List partitionChannels, + List partitionTypes, + Optional maxRowsPerPartition, + Optional hashChannel, + int expectedPositions) + { + this.operatorContext = checkNotNull(operatorContext, "operatorContext is null"); + this.outputChannels = Ints.toArray(outputChannels); + this.maxRowsPerPartition = maxRowsPerPartition; + + this.partitionRowCount = new LongBigArray(0); + if (partitionChannels.isEmpty()) { + this.groupByHash = Optional.empty(); + } + else { + int[] channels = Ints.toArray(partitionChannels); + this.groupByHash = Optional.of(createGroupByHash(partitionTypes, channels, Optional.empty(), hashChannel, expectedPositions)); + } + this.types = toTypes(sourceTypes, outputChannels); + } + + @Override + public OperatorContext getOperatorContext() + { + return operatorContext; + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public void finish() + { + finishing = true; + } + + @Override + public boolean isFinished() + { + if (isSinglePartition() && maxRowsPerPartition.isPresent()) { + if (finishing && inputPage == null) { + return true; + } + return partitionRowCount.get(0) == maxRowsPerPartition.get(); + } + + return finishing && inputPage == null; + } + + @Override + public boolean needsInput() + { + if (isSinglePartition() && maxRowsPerPartition.isPresent()) { + // Check if single partition is done + return partitionRowCount.get(0) < maxRowsPerPartition.get(); + } + return !finishing && inputPage == null; + } + + @Override + public void addInput(Page page) + { + checkState(!finishing, "Operator is already finishing"); + checkNotNull(page, "page is null"); + checkState(inputPage == null); + inputPage = page; + if (groupByHash.isPresent()) { + partitionIds = groupByHash.get().getGroupIds(inputPage); + partitionRowCount.ensureCapacity(partitionIds.getGroupCount()); + } + } + + @Override + public Page getOutput() + { + if (inputPage == null) { + return null; + } + + Page outputPage; + if (maxRowsPerPartition.isPresent()) { + outputPage = getSelectedRows(); + } + else { + outputPage = getRowsWithRowNumber(); + } + + inputPage = null; + return outputPage; + } + + private boolean isSinglePartition() + { + return !groupByHash.isPresent(); + } + + private Page getRowsWithRowNumber() + { + Block rowNumberBlock = createRowNumberBlock(); + Block[] sourceBlocks = new Block[inputPage.getChannelCount()]; + for (int i = 0; i < outputChannels.length; i++) { + sourceBlocks[i] = inputPage.getBlock(outputChannels[i]); + } + + Block[] outputBlocks = Arrays.copyOf(sourceBlocks, sourceBlocks.length + 1); // +1 for the row number column + outputBlocks[sourceBlocks.length] = rowNumberBlock; + + return new Page(inputPage.getPositionCount(), outputBlocks); + } + + private Block createRowNumberBlock() + { + BlockBuilder rowNumberBlock = BIGINT.createFixedSizeBlockBuilder(inputPage.getPositionCount()); + for (int currentPosition = 0; currentPosition < inputPage.getPositionCount(); currentPosition++) { + long partitionId = getPartitionId(currentPosition); + long nextRowCount = partitionRowCount.get(partitionId) + 1; + BIGINT.writeLong(rowNumberBlock, nextRowCount); + partitionRowCount.set(partitionId, nextRowCount); + } + return rowNumberBlock.build(); + } + + private Page getSelectedRows() + { + PageBuilder pageBuilder = new PageBuilder(types); + int rowNumberChannel = types.size() - 1; + + for (int currentPosition = 0; currentPosition < inputPage.getPositionCount(); currentPosition++) { + long partitionId = getPartitionId(currentPosition); + long rowCount = partitionRowCount.get(partitionId); + if (rowCount == maxRowsPerPartition.get()) { + continue; + } + pageBuilder.declarePosition(); + for (int i = 0; i < outputChannels.length; i++) { + int channel = outputChannels[i]; + Type type = types.get(channel); + type.appendTo(inputPage.getBlock(channel), currentPosition, pageBuilder.getBlockBuilder(i)); + } + BIGINT.writeLong(pageBuilder.getBlockBuilder(rowNumberChannel), rowCount + 1); + partitionRowCount.set(partitionId, rowCount + 1); + } + if (pageBuilder.isEmpty()) { + return null; + } + return pageBuilder.build(); + } + + private long getPartitionId(int position) + { + return isSinglePartition() ? 0 : partitionIds.getGroupId(position); + } + + private static List toTypes(List sourceTypes, List outputChannels) + { + ImmutableList.Builder types = ImmutableList.builder(); + for (int channel : outputChannels) { + types.add(sourceTypes.get(channel)); + } + types.add(BIGINT); + return types.build(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/SampleOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/SampleOperator.java new file mode 100644 index 00000000..97e0657b --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/SampleOperator.java @@ -0,0 +1,179 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; +import org.apache.commons.math3.random.RandomDataGenerator; + +import java.util.List; + +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +public class SampleOperator + implements Operator +{ + public static class SampleOperatorFactory + implements OperatorFactory + { + private final int operatorId; + private final double sampleRatio; + private final boolean rescaled; + + private final List types; + private boolean closed; + + public SampleOperatorFactory(int operatorId, double sampleRatio, boolean rescaled, List sourceTypes) + { + this.operatorId = operatorId; + this.sampleRatio = sampleRatio; + this.rescaled = rescaled; + this.types = ImmutableList.builder() + .addAll(checkNotNull(sourceTypes, "sourceTypes is null")) + .add(BIGINT) + .build(); + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public Operator createOperator(DriverContext driverContext) + { + checkState(!closed, "Factory is already closed"); + OperatorContext operatorContext = driverContext.addOperatorContext(operatorId, SampleOperator.class.getSimpleName()); + return new SampleOperator(operatorContext, sampleRatio, rescaled, types); + } + + @Override + public void close() + { + closed = true; + } + } + + private final OperatorContext operatorContext; + private final List types; + private final PageBuilder pageBuilder; + private final RandomDataGenerator rand = new RandomDataGenerator(); + private final double sampleRatio; + private final boolean rescaled; + private final int sampleWeightChannel; + private boolean finishing; + + private int position = -1; + private Page page; + + public SampleOperator(OperatorContext operatorContext, double sampleRatio, boolean rescaled, List types) + { + //Note: Poissonized Samples can be larger than the original dataset if desired + checkArgument(sampleRatio > 0, "sample ratio must be strictly positive"); + + this.operatorContext = checkNotNull(operatorContext, "operatorContext is null"); + this.types = ImmutableList.copyOf(types); + this.pageBuilder = new PageBuilder(types); + this.sampleWeightChannel = types.size() - 1; + this.sampleRatio = sampleRatio; + this.rescaled = rescaled; + } + + @Override + public OperatorContext getOperatorContext() + { + return operatorContext; + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public final void finish() + { + finishing = true; + } + + @Override + public final boolean isFinished() + { + return finishing && pageBuilder.isEmpty() && page == null; + } + + @Override + public final boolean needsInput() + { + return !finishing && !pageBuilder.isFull() && page == null; + } + + @Override + public void addInput(Page page) + { + checkState(!finishing, "Operator is already finishing"); + checkNotNull(page, "page is null"); + checkState(!pageBuilder.isFull(), "Page buffer is full"); + checkState(this.page == null, "previous page has not been completely processed"); + + this.page = page; + this.position = 0; + } + + @Override + public Page getOutput() + { + if (page != null) { + while (position < page.getPositionCount() && !pageBuilder.isFull()) { + long repeats = rand.nextPoisson(sampleRatio); + if (rescaled && repeats > 0) { + repeats *= rand.nextPoisson(1.0 / sampleRatio); + } + + if (repeats > 0) { + // copy input values to output page + // NOTE: last output type is sample weight so we skip it + pageBuilder.declarePosition(); + for (int channel = 0; channel < types.size() - 1; channel++) { + Type type = types.get(channel); + type.appendTo(page.getBlock(channel), position, pageBuilder.getBlockBuilder(channel)); + } + BIGINT.writeLong(pageBuilder.getBlockBuilder(sampleWeightChannel), repeats); + } + + position++; + } + + if (position >= page.getPositionCount()) { + page = null; + } + } + + // only flush full pages unless we are done + if (pageBuilder.isFull() || (finishing && !pageBuilder.isEmpty())) { + Page page = pageBuilder.build(); + pageBuilder.reset(); + return page; + } + + return null; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/ScanFilterAndProjectOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/ScanFilterAndProjectOperator.java new file mode 100644 index 00000000..19e1e237 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/ScanFilterAndProjectOperator.java @@ -0,0 +1,327 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.metadata.Split; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorPageSource; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.RecordCursor; +import com.facebook.presto.spi.RecordPageSource; +import com.facebook.presto.spi.UpdatablePageSource; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.split.PageSourceProvider; +import com.facebook.presto.sql.planner.plan.PlanNodeId; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; + +import java.io.Closeable; +import java.io.IOException; +import java.util.List; +import java.util.Optional; +import java.util.function.Supplier; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +public class ScanFilterAndProjectOperator + implements SourceOperator, Closeable +{ + private static final int ROWS_PER_PAGE = 16384; + + private final OperatorContext operatorContext; + private final PlanNodeId planNodeId; + private final PageSourceProvider pageSourceProvider; + private final List types; + private final List columns; + private final PageBuilder pageBuilder; + private final CursorProcessor cursorProcessor; + private final PageProcessor pageProcessor; + private final SettableFuture blocked = SettableFuture.create(); + + private RecordCursor cursor; + private ConnectorPageSource pageSource; + + private Split split; + private Page currentPage; + private int currentPosition; + + private boolean finishing; + + private long completedBytes; + private long readTimeNanos; + + protected ScanFilterAndProjectOperator( + OperatorContext operatorContext, + PlanNodeId sourceId, + PageSourceProvider pageSourceProvider, + CursorProcessor cursorProcessor, + PageProcessor pageProcessor, + Iterable columns, + Iterable types) + { + this.cursorProcessor = checkNotNull(cursorProcessor, "cursorProcessor is null"); + this.pageProcessor = checkNotNull(pageProcessor, "pageProcessor is null"); + this.operatorContext = checkNotNull(operatorContext, "operatorContext is null"); + this.planNodeId = checkNotNull(sourceId, "sourceId is null"); + this.pageSourceProvider = checkNotNull(pageSourceProvider, "pageSourceManager is null"); + this.types = ImmutableList.copyOf(checkNotNull(types, "types is null")); + this.columns = ImmutableList.copyOf(checkNotNull(columns, "columns is null")); + + this.pageBuilder = new PageBuilder(getTypes()); + } + + @Override + public OperatorContext getOperatorContext() + { + return operatorContext; + } + + @Override + public PlanNodeId getSourceId() + { + return planNodeId; + } + + @Override + public Supplier> addSplit(Split split) + { + checkNotNull(split, "split is null"); + checkState(this.split == null, "Table scan split already set"); + + if (finishing) { + return Optional::empty; + } + + this.split = split; + + Object splitInfo = split.getInfo(); + if (splitInfo != null) { + operatorContext.setInfoSupplier(() -> splitInfo); + } + blocked.set(null); + + return () -> { + if (pageSource instanceof UpdatablePageSource) { + return Optional.of((UpdatablePageSource) pageSource); + } + return Optional.empty(); + }; + } + + @Override + public void noMoreSplits() + { + if (split == null) { + finishing = true; + } + blocked.set(null); + } + + @Override + public final List getTypes() + { + return types; + } + + @Override + public void close() + { + finish(); + } + + @Override + public void finish() + { + blocked.set(null); + if (pageSource != null) { + try { + pageSource.close(); + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } + else if (cursor != null) { + cursor.close(); + } + finishing = true; + } + + @Override + public final boolean isFinished() + { + if (!finishing) { + createSourceIfNecessary(); + } + + if (pageSource != null && pageSource.isFinished() && currentPage == null) { + finishing = true; + } + + return finishing && pageBuilder.isEmpty(); + } + + @Override + public ListenableFuture isBlocked() + { + return blocked; + } + + @Override + public final boolean needsInput() + { + return false; + } + + @Override + public final void addInput(Page page) + { + throw new UnsupportedOperationException(); + } + + @Override + public Page getOutput() + { + if (!finishing) { + createSourceIfNecessary(); + + if (cursor != null) { + int rowsProcessed = cursorProcessor.process(operatorContext.getSession().toConnectorSession(), cursor, ROWS_PER_PAGE, pageBuilder); + long bytesProcessed = cursor.getCompletedBytes() - completedBytes; + long elapsedNanos = cursor.getReadTimeNanos() - readTimeNanos; + operatorContext.recordGeneratedInput(bytesProcessed, rowsProcessed, elapsedNanos); + completedBytes = cursor.getCompletedBytes(); + readTimeNanos = cursor.getReadTimeNanos(); + + if (rowsProcessed == 0) { + finishing = true; + } + } + else { + if (currentPage == null) { + currentPage = pageSource.getNextPage(); + + if (currentPage != null) { + // update operator stats + long endCompletedBytes = pageSource.getCompletedBytes(); + long endReadTimeNanos = pageSource.getReadTimeNanos(); + operatorContext.recordGeneratedInput(endCompletedBytes - completedBytes, currentPage.getPositionCount(), endReadTimeNanos - readTimeNanos); + completedBytes = endCompletedBytes; + readTimeNanos = endReadTimeNanos; + } + + currentPosition = 0; + } + + if (currentPage != null) { + currentPosition = pageProcessor.process(operatorContext.getSession().toConnectorSession(), currentPage, currentPosition, currentPage.getPositionCount(), pageBuilder); + if (currentPosition == currentPage.getPositionCount()) { + currentPage = null; + currentPosition = 0; + } + } + } + } + + // only return a full page if buffer is full or we are finishing + if (pageBuilder.isEmpty() || (!finishing && !pageBuilder.isFull())) { + return null; + } + + Page page = pageBuilder.build(); + pageBuilder.reset(); + return page; + } + + private void createSourceIfNecessary() + { + if ((split != null) && (pageSource == null) && (cursor == null)) { + ConnectorPageSource source = pageSourceProvider.createPageSource(split, columns); + if (source instanceof RecordPageSource) { + cursor = ((RecordPageSource) source).getCursor(); + } + else { + pageSource = source; + } + } + } + + public static class ScanFilterAndProjectOperatorFactory + implements SourceOperatorFactory + { + private final int operatorId; + private final CursorProcessor cursorProcessor; + private final PageProcessor pageProcessor; + private final PlanNodeId sourceId; + private final PageSourceProvider pageSourceProvider; + private final List columns; + private final List types; + private boolean closed; + + public ScanFilterAndProjectOperatorFactory( + int operatorId, + PlanNodeId sourceId, + PageSourceProvider pageSourceProvider, + CursorProcessor cursorProcessor, + PageProcessor pageProcessor, + Iterable columns, + List types) + { + this.operatorId = operatorId; + this.cursorProcessor = checkNotNull(cursorProcessor, "cursorProcessor is null"); + this.pageProcessor = checkNotNull(pageProcessor, "pageProcessor is null"); + this.sourceId = checkNotNull(sourceId, "sourceId is null"); + this.pageSourceProvider = checkNotNull(pageSourceProvider, "pageSourceProvider is null"); + this.columns = ImmutableList.copyOf(checkNotNull(columns, "columns is null")); + this.types = checkNotNull(types, "types is null"); + } + + @Override + public PlanNodeId getSourceId() + { + return sourceId; + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public SourceOperator createOperator(DriverContext driverContext) + { + checkState(!closed, "Factory is already closed"); + OperatorContext operatorContext = driverContext.addOperatorContext(operatorId, ScanFilterAndProjectOperator.class.getSimpleName()); + return new ScanFilterAndProjectOperator( + operatorContext, + sourceId, + pageSourceProvider, + cursorProcessor, + pageProcessor, + columns, + types); + } + + @Override + public void close() + { + closed = true; + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/SetBuilderOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/SetBuilderOperator.java new file mode 100644 index 00000000..1cd6023b --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/SetBuilderOperator.java @@ -0,0 +1,198 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.operator.ChannelSet.ChannelSetBuilder; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.type.Type; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; + +import javax.annotation.concurrent.ThreadSafe; + +import java.util.List; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +@ThreadSafe +public class SetBuilderOperator + implements Operator +{ + public static class SetSupplier + { + private final Type type; + private final SettableFuture channelSetFuture = SettableFuture.create(); + + public SetSupplier(Type type) + { + this.type = checkNotNull(type, "type is null"); + } + + public Type getType() + { + return type; + } + + public ListenableFuture getChannelSet() + { + return channelSetFuture; + } + + void setChannelSet(ChannelSet channelSet) + { + boolean wasSet = channelSetFuture.set(checkNotNull(channelSet, "channelSet is null")); + checkState(wasSet, "ChannelSet already set"); + } + } + + public static class SetBuilderOperatorFactory + implements OperatorFactory + { + private final int operatorId; + private final Optional hashChannel; + private final SetSupplier setProvider; + private final int setChannel; + private final int expectedPositions; + private boolean closed; + + public SetBuilderOperatorFactory( + int operatorId, + List types, + int setChannel, + Optional hashChannel, + int expectedPositions) + { + this.operatorId = operatorId; + Preconditions.checkArgument(setChannel >= 0, "setChannel is negative"); + this.setProvider = new SetSupplier(checkNotNull(types, "types is null").get(setChannel)); + this.setChannel = setChannel; + this.hashChannel = checkNotNull(hashChannel, "hashChannel is null"); + this.expectedPositions = expectedPositions; + } + + public SetSupplier getSetProvider() + { + return setProvider; + } + + @Override + public List getTypes() + { + return ImmutableList.of(); + } + + @Override + public Operator createOperator(DriverContext driverContext) + { + checkState(!closed, "Factory is already closed"); + OperatorContext operatorContext = driverContext.addOperatorContext(operatorId, SetBuilderOperator.class.getSimpleName()); + return new SetBuilderOperator(operatorContext, setProvider, setChannel, hashChannel, expectedPositions); + } + + @Override + public void close() + { + closed = true; + } + } + + private final OperatorContext operatorContext; + private final SetSupplier setSupplier; + private final int setChannel; + private final Optional hashChannel; + + private final ChannelSetBuilder channelSetBuilder; + + private boolean finished; + + public SetBuilderOperator( + OperatorContext operatorContext, + SetSupplier setSupplier, + int setChannel, + Optional hashChannel, + int expectedPositions) + { + this.operatorContext = checkNotNull(operatorContext, "operatorContext is null"); + this.setSupplier = checkNotNull(setSupplier, "setProvider is null"); + this.setChannel = setChannel; + + this.hashChannel = checkNotNull(hashChannel, "hashChannel is null"); + // Set builder is has a single channel which goes in channel 0, if hash is present, add a hachBlock to channel 1 + Optional channelSetHashChannel = hashChannel.isPresent() ? Optional.of(1) : Optional.empty(); + this.channelSetBuilder = new ChannelSetBuilder( + setSupplier.getType(), + channelSetHashChannel, + expectedPositions, + checkNotNull(operatorContext, "operatorContext is null")); + } + + @Override + public OperatorContext getOperatorContext() + { + return operatorContext; + } + + @Override + public List getTypes() + { + return ImmutableList.of(); + } + + @Override + public void finish() + { + if (finished) { + return; + } + + ChannelSet channelSet = channelSetBuilder.build(); + setSupplier.setChannelSet(channelSet); + operatorContext.recordGeneratedOutput(channelSet.getEstimatedSizeInBytes(), channelSet.size()); + finished = true; + } + + @Override + public boolean isFinished() + { + return finished; + } + + @Override + public boolean needsInput() + { + return !finished; + } + + @Override + public void addInput(Page page) + { + checkNotNull(page, "page is null"); + checkState(!isFinished(), "Operator is already finished"); + + Block sourceBlock = page.getBlock(setChannel); + Page sourcePage = hashChannel.isPresent() ? new Page(sourceBlock, page.getBlock(hashChannel.get())) : new Page(sourceBlock); + channelSetBuilder.addPage(sourcePage); + } + + @Override + public Page getOutput() + { + return null; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/SettableLookupSourceSupplier.java b/presto-main/src/main/java/com/facebook/presto/operator/SettableLookupSourceSupplier.java new file mode 100644 index 00000000..5d29085d --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/SettableLookupSourceSupplier.java @@ -0,0 +1,88 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.AsyncFunction; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.util.concurrent.Futures.transform; + +public final class SettableLookupSourceSupplier + implements LookupSourceSupplier +{ + private final List types; + private final SettableFuture lookupSourceFuture = SettableFuture.create(); + private final AtomicInteger referenceCount = new AtomicInteger(1); + + public SettableLookupSourceSupplier(List types) + { + this.types = ImmutableList.copyOf(checkNotNull(types, "types is null")); + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public ListenableFuture getLookupSource(OperatorContext operatorContext) + { + return transform(lookupSourceFuture, (AsyncFunction) Futures::immediateFuture); + } + + public void setLookupSource(SharedLookupSource lookupSource) + { + checkNotNull(lookupSource, "lookupSource is null"); + boolean wasSet = lookupSourceFuture.set(lookupSource); + checkState(wasSet, "Lookup source already set"); + } + + @Override + public void retain() + { + referenceCount.incrementAndGet(); + } + + @Override + public void release() + { + if (referenceCount.decrementAndGet() == 0) { + // We own the shared lookup source, so we need to free their memory + Futures.addCallback(lookupSourceFuture, new FutureCallback() { + @Override + public void onSuccess(SharedLookupSource result) + { + result.freeMemory(); + } + + @Override + public void onFailure(Throwable t) + { + // ignored + } + }); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/SharedLookupSource.java b/presto-main/src/main/java/com/facebook/presto/operator/SharedLookupSource.java new file mode 100644 index 00000000..a446ff32 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/SharedLookupSource.java @@ -0,0 +1,94 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PageBuilder; +import it.unimi.dsi.fastutil.longs.LongIterator; + +import javax.annotation.concurrent.GuardedBy; + +import static com.google.common.base.Preconditions.checkState; +import static java.util.Objects.requireNonNull; + +public final class SharedLookupSource + implements LookupSource +{ + private final TaskContext taskContext; + private final LookupSource lookupSource; + @GuardedBy("this") + private boolean freed; + + public SharedLookupSource(LookupSource lookupSource, TaskContext taskContext) + { + this.lookupSource = requireNonNull(lookupSource, "lookupSource is null"); + this.taskContext = requireNonNull(taskContext, "taskContext is null"); + taskContext.reserveMemory(lookupSource.getInMemorySizeInBytes()); + } + + @Override + public int getChannelCount() + { + return lookupSource.getChannelCount(); + } + + @Override + public long getInMemorySizeInBytes() + { + return lookupSource.getInMemorySizeInBytes(); + } + + @Override + public long getJoinPosition(int position, Page page, int rawHash) + { + return lookupSource.getJoinPosition(position, page, rawHash); + } + + @Override + public long getJoinPosition(int position, Page page) + { + return lookupSource.getJoinPosition(position, page); + } + + @Override + public long getNextJoinPosition(long currentPosition) + { + return lookupSource.getNextJoinPosition(currentPosition); + } + + @Override + public void appendTo(long position, PageBuilder pageBuilder, int outputChannelOffset) + { + lookupSource.appendTo(position, pageBuilder, outputChannelOffset); + } + + @Override + public LongIterator getUnvisitedJoinPositions() + { + return lookupSource.getUnvisitedJoinPositions(); + } + + synchronized void freeMemory() + { + checkState(!freed, "Already freed"); + freed = true; + taskContext.freeMemory(lookupSource.getInMemorySizeInBytes()); + } + + @Override + public void close() + { + lookupSource.close(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/SimpleJoinProbe.java b/presto-main/src/main/java/com/facebook/presto/operator/SimpleJoinProbe.java new file mode 100644 index 00000000..72edb762 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/SimpleJoinProbe.java @@ -0,0 +1,124 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.type.Type; + +import java.util.List; +import java.util.Optional; + +import static com.facebook.presto.spi.type.BigintType.BIGINT; + +public class SimpleJoinProbe + implements JoinProbe +{ + public static class SimpleJoinProbeFactory + implements JoinProbeFactory + { + private List types; + private List probeJoinChannels; + private final Optional probeHashChannel; + + public SimpleJoinProbeFactory(List types, List probeJoinChannels, Optional probeHashChannel) + { + this.types = types; + this.probeJoinChannels = probeJoinChannels; + this.probeHashChannel = probeHashChannel; + } + + @Override + public JoinProbe createJoinProbe(LookupSource lookupSource, Page page) + { + return new SimpleJoinProbe(types, lookupSource, page, probeJoinChannels, probeHashChannel); + } + } + + private final List types; + private final LookupSource lookupSource; + private final int positionCount; + private final Block[] blocks; + private final Block[] probeBlocks; + private final Page probePage; + private final Optional probeHashBlock; + + private int position = -1; + + private SimpleJoinProbe(List types, LookupSource lookupSource, Page page, List probeJoinChannels, Optional hashChannel) + { + this.types = types; + this.lookupSource = lookupSource; + this.positionCount = page.getPositionCount(); + this.blocks = new Block[page.getChannelCount()]; + this.probeBlocks = new Block[probeJoinChannels.size()]; + + for (int i = 0; i < page.getChannelCount(); i++) { + blocks[i] = page.getBlock(i); + } + + for (int i = 0; i < probeJoinChannels.size(); i++) { + probeBlocks[i] = blocks[probeJoinChannels.get(i)]; + } + this.probePage = new Page(probeBlocks); + this.probeHashBlock = hashChannel.isPresent() ? Optional.of(page.getBlock(hashChannel.get())) : Optional.empty(); + } + + @Override + public int getChannelCount() + { + return blocks.length; + } + + @Override + public boolean advanceNextPosition() + { + position++; + return position < positionCount; + } + + @Override + public void appendTo(PageBuilder pageBuilder) + { + for (int outputIndex = 0; outputIndex < blocks.length; outputIndex++) { + Type type = types.get(outputIndex); + Block block = blocks[outputIndex]; + type.appendTo(block, position, pageBuilder.getBlockBuilder(outputIndex)); + } + } + + @Override + public long getCurrentJoinPosition() + { + if (currentRowContainsNull()) { + return -1; + } + if (probeHashBlock.isPresent()) { + int rawHash = (int) BIGINT.getLong(probeHashBlock.get(), position); + return lookupSource.getJoinPosition(position, probePage, rawHash); + } + return lookupSource.getJoinPosition(position, probePage); + } + + private boolean currentRowContainsNull() + { + for (Block probeBlock : probeBlocks) { + if (probeBlock.isNull(position)) { + return true; + } + } + return false; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/SimplePagesHashStrategy.java b/presto-main/src/main/java/com/facebook/presto/operator/SimplePagesHashStrategy.java new file mode 100644 index 00000000..39177154 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/SimplePagesHashStrategy.java @@ -0,0 +1,142 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.type.TypeUtils; +import com.google.common.collect.ImmutableList; + +import java.util.List; +import java.util.Optional; + +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class SimplePagesHashStrategy + implements PagesHashStrategy +{ + private final List types; + private final List> channels; + private final List hashChannels; + private final List precomputedHashChannel; + + public SimplePagesHashStrategy(List types, List> channels, List hashChannels, Optional precomputedHashChannel) + { + this.types = ImmutableList.copyOf(checkNotNull(types, "types is null")); + this.channels = ImmutableList.copyOf(checkNotNull(channels, "channels is null")); + checkArgument(types.size() == channels.size(), "Expected types and channels to be the same length"); + this.hashChannels = ImmutableList.copyOf(checkNotNull(hashChannels, "hashChannels is null")); + if (precomputedHashChannel.isPresent()) { + this.precomputedHashChannel = channels.get(precomputedHashChannel.get()); + } + else { + this.precomputedHashChannel = null; + } + } + + @Override + public int getChannelCount() + { + return channels.size(); + } + + @Override + public void appendTo(int blockIndex, int position, PageBuilder pageBuilder, int outputChannelOffset) + { + for (int i = 0; i < channels.size(); i++) { + Type type = types.get(i); + List channel = channels.get(i); + Block block = channel.get(blockIndex); + type.appendTo(block, position, pageBuilder.getBlockBuilder(outputChannelOffset)); + outputChannelOffset++; + } + } + + @Override + public int hashPosition(int blockIndex, int position) + { + if (precomputedHashChannel != null) { + return (int) BIGINT.getLong(precomputedHashChannel.get(blockIndex), position); + } + int result = 0; + for (int hashChannel : hashChannels) { + Type type = types.get(hashChannel); + Block block = channels.get(hashChannel).get(blockIndex); + result = result * 31 + TypeUtils.hashPosition(type, block, position); + } + return result; + } + + @Override + public int hashRow(int position, Block... blocks) + { + int result = 0; + for (int i = 0; i < hashChannels.size(); i++) { + int hashChannel = hashChannels.get(i); + Type type = types.get(hashChannel); + Block block = blocks[i]; + result = result * 31 + TypeUtils.hashPosition(type, block, position); + } + return result; + } + + @Override + public boolean rowEqualsRow(int leftPosition, Block[] leftBlocks, int rightPosition, Block[] rightBlocks) + { + for (int i = 0; i < hashChannels.size(); i++) { + int hashChannel = hashChannels.get(i); + Type type = types.get(hashChannel); + Block leftBlock = leftBlocks[i]; + Block rightBlock = rightBlocks[i]; + if (!TypeUtils.positionEqualsPosition(type, leftBlock, leftPosition, rightBlock, rightPosition)) { + return false; + } + } + return true; + } + + @Override + public boolean positionEqualsRow(int leftBlockIndex, int leftPosition, int rightPosition, Block... rightBlocks) + { + for (int i = 0; i < hashChannels.size(); i++) { + int hashChannel = hashChannels.get(i); + Type type = types.get(hashChannel); + Block leftBlock = channels.get(hashChannel).get(leftBlockIndex); + Block rightBlock = rightBlocks[i]; + if (!TypeUtils.positionEqualsPosition(type, leftBlock, leftPosition, rightBlock, rightPosition)) { + return false; + } + } + return true; + } + + @Override + public boolean positionEqualsPosition(int leftBlockIndex, int leftPosition, int rightBlockIndex, int rightPosition) + { + for (int hashChannel : hashChannels) { + Type type = types.get(hashChannel); + List channel = channels.get(hashChannel); + Block leftBlock = channel.get(leftBlockIndex); + Block rightBlock = channel.get(rightBlockIndex); + if (!TypeUtils.positionEqualsPosition(type, leftBlock, leftPosition, rightBlock, rightPosition)) { + return false; + } + } + + return true; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/SimplePagesIndexComparator.java b/presto-main/src/main/java/com/facebook/presto/operator/SimplePagesIndexComparator.java new file mode 100644 index 00000000..9d6dccbe --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/SimplePagesIndexComparator.java @@ -0,0 +1,65 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.SortOrder; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.facebook.presto.operator.SyntheticAddress.decodePosition; +import static com.facebook.presto.operator.SyntheticAddress.decodeSliceIndex; +import static com.google.common.base.Preconditions.checkNotNull; + +public class SimplePagesIndexComparator + implements PagesIndexComparator +{ + private final List sortChannels; + private final List sortOrders; + private final List sortTypes; + + public SimplePagesIndexComparator(List sortTypes, List sortChannels, List sortOrders) + { + this.sortTypes = ImmutableList.copyOf(checkNotNull(sortTypes, "sortTypes is null")); + this.sortChannels = ImmutableList.copyOf(checkNotNull(sortChannels, "sortChannels is null")); + this.sortOrders = ImmutableList.copyOf(checkNotNull(sortOrders, "sortOrders is null")); + } + + @Override + public int compareTo(PagesIndex pagesIndex, int leftPosition, int rightPosition) + { + long leftPageAddress = pagesIndex.getValueAddresses().getLong(leftPosition); + int leftBlockIndex = decodeSliceIndex(leftPageAddress); + int leftBlockPosition = decodePosition(leftPageAddress); + + long rightPageAddress = pagesIndex.getValueAddresses().getLong(rightPosition); + int rightBlockIndex = decodeSliceIndex(rightPageAddress); + int rightBlockPosition = decodePosition(rightPageAddress); + + for (int i = 0; i < sortChannels.size(); i++) { + int sortChannel = sortChannels.get(i); + Block leftBlock = pagesIndex.getChannel(sortChannel).get(leftBlockIndex); + Block rightBlock = pagesIndex.getChannel(sortChannel).get(rightBlockIndex); + + SortOrder sortOrder = sortOrders.get(i); + int compare = sortOrder.compareBlockValue(sortTypes.get(i), leftBlock, leftBlockPosition, rightBlock, rightBlockPosition); + if (compare != 0) { + return compare; + } + } + return 0; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/SourceOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/SourceOperator.java new file mode 100644 index 00000000..923c4859 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/SourceOperator.java @@ -0,0 +1,31 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.metadata.Split; +import com.facebook.presto.spi.UpdatablePageSource; +import com.facebook.presto.sql.planner.plan.PlanNodeId; + +import java.util.Optional; +import java.util.function.Supplier; + +public interface SourceOperator + extends Operator +{ + PlanNodeId getSourceId(); + + Supplier> addSplit(Split split); + + void noMoreSplits(); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/SourceOperatorFactory.java b/presto-main/src/main/java/com/facebook/presto/operator/SourceOperatorFactory.java new file mode 100644 index 00000000..43c53951 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/SourceOperatorFactory.java @@ -0,0 +1,25 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.sql.planner.plan.PlanNodeId; + +public interface SourceOperatorFactory + extends OperatorFactory +{ + PlanNodeId getSourceId(); + + @Override + SourceOperator createOperator(DriverContext driverContext); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/SyntheticAddress.java b/presto-main/src/main/java/com/facebook/presto/operator/SyntheticAddress.java new file mode 100644 index 00000000..0bc979f1 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/SyntheticAddress.java @@ -0,0 +1,43 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +/** + * Methods for creating and decoding synthetic addresses. + * A synthetic address is a physical position within an array of Slices. The address is encoded + * as a long with the high 32 bits containing the index of the slice in the array and the low 32 + * bits containing an offset within the slice. + */ +public final class SyntheticAddress +{ + private SyntheticAddress() + { + } + + public static long encodeSyntheticAddress(int sliceIndex, int sliceOffset) + { + return (((long) sliceIndex) << 32) | sliceOffset; + } + + public static int decodeSliceIndex(long sliceAddress) + { + return ((int) (sliceAddress >> 32)); + } + + public static int decodePosition(long sliceAddress) + { + // low order bits contain the raw offset, so a simple cast here will suffice + return (int) sliceAddress; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/TableCommitOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/TableCommitOperator.java new file mode 100644 index 00000000..4bfb2fff --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/TableCommitOperator.java @@ -0,0 +1,174 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; + +import java.util.Collection; +import java.util.List; + +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.VarbinaryType.VARBINARY; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +public class TableCommitOperator + implements Operator +{ + public static final List TYPES = ImmutableList.of(BIGINT); + + public static class TableCommitOperatorFactory + implements OperatorFactory + { + private final int operatorId; + private final TableCommitter tableCommitter; + private boolean closed; + + public TableCommitOperatorFactory(int operatorId, TableCommitter tableCommitter) + { + this.operatorId = operatorId; + this.tableCommitter = checkNotNull(tableCommitter, "tableCommitter is null"); + } + + @Override + public List getTypes() + { + return TYPES; + } + + @Override + public Operator createOperator(DriverContext driverContext) + { + checkState(!closed, "Factory is already closed"); + OperatorContext context = driverContext.addOperatorContext(operatorId, TableCommitOperator.class.getSimpleName()); + return new TableCommitOperator(context, tableCommitter); + } + + @Override + public void close() + { + closed = true; + } + } + + private enum State + { + RUNNING, FINISHING, FINISHED + } + + private final OperatorContext operatorContext; + private final TableCommitter tableCommitter; + + private State state = State.RUNNING; + private long rowCount; + private boolean committed; + private boolean closed; + private final ImmutableList.Builder fragmentBuilder = ImmutableList.builder(); + + public TableCommitOperator(OperatorContext operatorContext, TableCommitter tableCommitter) + { + this.operatorContext = checkNotNull(operatorContext, "operatorContext is null"); + this.tableCommitter = checkNotNull(tableCommitter, "tableCommitter is null"); + } + + @Override + public OperatorContext getOperatorContext() + { + return operatorContext; + } + + @Override + public List getTypes() + { + return TYPES; + } + + @Override + public void finish() + { + if (state == State.RUNNING) { + state = State.FINISHING; + } + } + + @Override + public boolean isFinished() + { + return state == State.FINISHED; + } + + @Override + public boolean needsInput() + { + return state == State.RUNNING; + } + + @Override + public void addInput(Page page) + { + checkNotNull(page, "page is null"); + checkState(state == State.RUNNING, "Operator is %s", state); + + Block rowCountBlock = page.getBlock(0); + Block fragmentBlock = page.getBlock(1); + for (int position = 0; position < page.getPositionCount(); position++) { + if (!rowCountBlock.isNull(position)) { + rowCount += BIGINT.getLong(rowCountBlock, position); + } + if (!fragmentBlock.isNull(position)) { + fragmentBuilder.add(VARBINARY.getSlice(fragmentBlock, position)); + } + } + } + + @Override + public Page getOutput() + { + if (state != State.FINISHING) { + return null; + } + state = State.FINISHED; + + tableCommitter.commitTable(fragmentBuilder.build()); + committed = true; + + PageBuilder page = new PageBuilder(getTypes()); + page.declarePosition(); + BIGINT.writeLong(page.getBlockBuilder(0), rowCount); + return page.build(); + } + + @Override + public void close() + throws Exception + { + if (!closed) { + closed = true; + if (!committed) { + tableCommitter.rollbackTable(); + } + } + } + + public interface TableCommitter + { + void commitTable(Collection fragments); + void rollbackTable(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/TableScanOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/TableScanOperator.java new file mode 100644 index 00000000..5021fecc --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/TableScanOperator.java @@ -0,0 +1,261 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.metadata.Split; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorPageSource; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.UpdatablePageSource; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.split.PageSourceProvider; +import com.facebook.presto.sql.planner.plan.PlanNodeId; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; + +import java.io.Closeable; +import java.io.IOException; +import java.util.List; +import java.util.Optional; +import java.util.function.Supplier; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +public class TableScanOperator + implements SourceOperator, Closeable +{ + public static class TableScanOperatorFactory + implements SourceOperatorFactory + { + private final int operatorId; + private final PlanNodeId sourceId; + private final PageSourceProvider pageSourceProvider; + private final List types; + private final List columns; + private boolean closed; + + public TableScanOperatorFactory( + int operatorId, + PlanNodeId sourceId, + PageSourceProvider pageSourceProvider, + List types, + Iterable columns) + { + this.operatorId = operatorId; + this.sourceId = checkNotNull(sourceId, "sourceId is null"); + this.types = checkNotNull(types, "types is null"); + this.pageSourceProvider = checkNotNull(pageSourceProvider, "pageSourceManager is null"); + this.columns = ImmutableList.copyOf(checkNotNull(columns, "columns is null")); + } + + @Override + public PlanNodeId getSourceId() + { + return sourceId; + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public SourceOperator createOperator(DriverContext driverContext) + { + checkState(!closed, "Factory is already closed"); + OperatorContext operatorContext = driverContext.addOperatorContext(operatorId, TableScanOperator.class.getSimpleName()); + return new TableScanOperator( + operatorContext, + sourceId, + pageSourceProvider, + types, + columns); + } + + @Override + public void close() + { + closed = true; + } + } + + private final OperatorContext operatorContext; + private final PlanNodeId planNodeId; + private final PageSourceProvider pageSourceProvider; + private final List types; + private final List columns; + private final SettableFuture blocked = SettableFuture.create(); + + private Split split; + private ConnectorPageSource source; + + private boolean finished; + + private long completedBytes; + private long readTimeNanos; + + public TableScanOperator( + OperatorContext operatorContext, + PlanNodeId planNodeId, + PageSourceProvider pageSourceProvider, + List types, + Iterable columns) + { + this.operatorContext = checkNotNull(operatorContext, "operatorContext is null"); + this.planNodeId = checkNotNull(planNodeId, "planNodeId is null"); + this.types = checkNotNull(types, "types is null"); + this.pageSourceProvider = checkNotNull(pageSourceProvider, "pageSourceManager is null"); + this.columns = ImmutableList.copyOf(checkNotNull(columns, "columns is null")); + } + + @Override + public OperatorContext getOperatorContext() + { + return operatorContext; + } + + @Override + public PlanNodeId getSourceId() + { + return planNodeId; + } + + @Override + public Supplier> addSplit(Split split) + { + checkNotNull(split, "split is null"); + checkState(this.split == null, "Table scan split already set"); + + if (finished) { + return Optional::empty; + } + + this.split = split; + + Object splitInfo = split.getInfo(); + if (splitInfo != null) { + operatorContext.setInfoSupplier(() -> splitInfo); + } + + blocked.set(null); + + return () -> { + if (source instanceof UpdatablePageSource) { + return Optional.of((UpdatablePageSource) source); + } + return Optional.empty(); + }; + } + + @Override + public void noMoreSplits() + { + if (split == null) { + finished = true; + } + blocked.set(null); + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public void close() + { + finish(); + } + + @Override + public void finish() + { + finished = true; + blocked.set(null); + + if (source != null) { + try { + source.close(); + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } + } + + @Override + public boolean isFinished() + { + if (!finished) { + createSourceIfNecessary(); + finished = (source != null) && source.isFinished(); + } + + return finished; + } + + @Override + public ListenableFuture isBlocked() + { + return blocked; + } + + @Override + public boolean needsInput() + { + return false; + } + + @Override + public void addInput(Page page) + { + throw new UnsupportedOperationException(getClass().getName() + " can not take input"); + } + + @Override + public Page getOutput() + { + createSourceIfNecessary(); + if (source == null) { + return null; + } + + Page page = source.getNextPage(); + if (page != null) { + // assure the page is in memory before handing to another operator + page.assureLoaded(); + + // update operator stats + long endCompletedBytes = source.getCompletedBytes(); + long endReadTimeNanos = source.getReadTimeNanos(); + operatorContext.recordGeneratedInput(endCompletedBytes - completedBytes, page.getPositionCount(), endReadTimeNanos - readTimeNanos); + completedBytes = endCompletedBytes; + readTimeNanos = endReadTimeNanos; + } + + return page; + } + + private void createSourceIfNecessary() + { + if ((split != null) && (source == null)) { + source = pageSourceProvider.createPageSource(split, columns); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/TableWriterOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/TableWriterOperator.java new file mode 100644 index 00000000..ec6d6a9e --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/TableWriterOperator.java @@ -0,0 +1,213 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.ConnectorPageSink; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.split.PageSinkManager; +import com.facebook.presto.sql.planner.plan.TableWriterNode.WriterTarget; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.VarbinaryType.VARBINARY; +import static com.facebook.presto.sql.planner.plan.TableWriterNode.CreateHandle; +import static com.facebook.presto.sql.planner.plan.TableWriterNode.InsertHandle; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +public class TableWriterOperator + implements Operator +{ + public static final List TYPES = ImmutableList.of(BIGINT, VARBINARY); + + public static class TableWriterOperatorFactory + implements OperatorFactory + { + private final int operatorId; + private final PageSinkManager pageSinkManager; + private final WriterTarget target; + private final List inputChannels; + private final Optional sampleWeightChannel; + private boolean closed; + + public TableWriterOperatorFactory(int operatorId, PageSinkManager pageSinkManager, WriterTarget writerTarget, List inputChannels, Optional sampleWeightChannel) + { + this.operatorId = operatorId; + this.inputChannels = checkNotNull(inputChannels, "inputChannels is null"); + this.pageSinkManager = checkNotNull(pageSinkManager, "pageSinkManager is null"); + checkArgument(writerTarget instanceof CreateHandle || writerTarget instanceof InsertHandle, "writerTarget must be CreateHandle or InsertHandle"); + this.target = checkNotNull(writerTarget, "writerTarget is null"); + this.sampleWeightChannel = checkNotNull(sampleWeightChannel, "sampleWeightChannel is null"); + } + + @Override + public List getTypes() + { + return TYPES; + } + + @Override + public Operator createOperator(DriverContext driverContext) + { + checkState(!closed, "Factory is already closed"); + OperatorContext context = driverContext.addOperatorContext(operatorId, TableWriterOperator.class.getSimpleName()); + return new TableWriterOperator(context, createPageSink(), inputChannels, sampleWeightChannel); + } + + private ConnectorPageSink createPageSink() + { + if (target instanceof CreateHandle) { + return pageSinkManager.createPageSink(((CreateHandle) target).getHandle()); + } + if (target instanceof InsertHandle) { + return pageSinkManager.createPageSink(((InsertHandle) target).getHandle()); + } + throw new UnsupportedOperationException("Unhandled target type: " + target.getClass().getName()); + } + + @Override + public void close() + { + closed = true; + } + } + + private enum State + { + RUNNING, FINISHING, FINISHED + } + + private final OperatorContext operatorContext; + private final ConnectorPageSink pageSink; + private final Optional sampleWeightChannel; + private final List inputChannels; + + private State state = State.RUNNING; + private long rowCount; + private boolean committed; + private boolean closed; + + public TableWriterOperator(OperatorContext operatorContext, + ConnectorPageSink pageSink, + List inputChannels, + Optional sampleWeightChannel) + { + this.operatorContext = checkNotNull(operatorContext, "operatorContext is null"); + this.pageSink = checkNotNull(pageSink, "pageSink is null"); + this.sampleWeightChannel = checkNotNull(sampleWeightChannel, "sampleWeightChannel is null"); + this.inputChannels = checkNotNull(inputChannels, "inputChannels is null"); + } + + @Override + public OperatorContext getOperatorContext() + { + return operatorContext; + } + + @Override + public List getTypes() + { + return TYPES; + } + + @Override + public void finish() + { + if (state == State.RUNNING) { + state = State.FINISHING; + } + } + + @Override + public boolean isFinished() + { + return state == State.FINISHED; + } + + @Override + public boolean needsInput() + { + return state == State.RUNNING; + } + + @Override + public void addInput(Page page) + { + checkNotNull(page, "page is null"); + checkState(state == State.RUNNING, "Operator is %s", state); + + Block[] blocks = new Block[inputChannels.size()]; + for (int outputChannel = 0; outputChannel < inputChannels.size(); outputChannel++) { + blocks[outputChannel] = page.getBlock(inputChannels.get(outputChannel)); + } + Block sampleWeightBlock = null; + if (sampleWeightChannel.isPresent()) { + sampleWeightBlock = page.getBlock(sampleWeightChannel.get()); + } + pageSink.appendPage(new Page(blocks), sampleWeightBlock); + rowCount += page.getPositionCount(); + } + + @Override + public Page getOutput() + { + if (state != State.FINISHING) { + return null; + } + state = State.FINISHED; + + Collection fragments = pageSink.commit(); + committed = true; + + PageBuilder page = new PageBuilder(TYPES); + BlockBuilder rowsBuilder = page.getBlockBuilder(0); + BlockBuilder fragmentBuilder = page.getBlockBuilder(1); + + // write row count + page.declarePosition(); + BIGINT.writeLong(rowsBuilder, rowCount); + fragmentBuilder.appendNull(); + + // write fragments + for (Slice fragment : fragments) { + page.declarePosition(); + rowsBuilder.appendNull(); + VARBINARY.writeSlice(fragmentBuilder, fragment); + } + + return page.build(); + } + + @Override + public void close() + throws Exception + { + if (!closed) { + closed = true; + if (!committed) { + pageSink.rollback(); + } + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/TaskContext.java b/presto-main/src/main/java/com/facebook/presto/operator/TaskContext.java new file mode 100644 index 00000000..23616a74 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/TaskContext.java @@ -0,0 +1,362 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.ExceededMemoryLimitException; +import com.facebook.presto.Session; +import com.facebook.presto.execution.StateMachine.StateChangeListener; +import com.facebook.presto.execution.TaskId; +import com.facebook.presto.execution.TaskState; +import com.facebook.presto.execution.TaskStateMachine; +import com.facebook.presto.memory.QueryContext; +import com.facebook.presto.util.ImmutableCollectors; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.util.concurrent.ListenableFuture; +import io.airlift.stats.CounterStat; +import io.airlift.units.DataSize; +import io.airlift.units.Duration; +import org.joda.time.DateTime; + +import javax.annotation.concurrent.ThreadSafe; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.Iterables.transform; +import static io.airlift.units.DataSize.Unit.BYTE; +import static java.util.Objects.requireNonNull; +import static java.util.concurrent.TimeUnit.NANOSECONDS; + +@ThreadSafe +public class TaskContext +{ + private final QueryContext queryContext; + private final TaskStateMachine taskStateMachine; + private final Executor executor; + private final Session session; + + private final long maxMemory; + private final DataSize operatorPreAllocatedMemory; + + private final AtomicLong memoryReservation = new AtomicLong(); + + private final long createNanos = System.nanoTime(); + + private final AtomicLong startNanos = new AtomicLong(); + private final AtomicLong endNanos = new AtomicLong(); + + private final AtomicReference executionStartTime = new AtomicReference<>(); + private final AtomicReference lastExecutionStartTime = new AtomicReference<>(); + private final AtomicReference executionEndTime = new AtomicReference<>(); + + private final List pipelineContexts = new CopyOnWriteArrayList<>(); + + private final boolean verboseStats; + private final boolean cpuTimerEnabled; + + public TaskContext(QueryContext queryContext, + TaskStateMachine taskStateMachine, + Executor executor, + Session session, + DataSize maxMemory, + DataSize operatorPreAllocatedMemory, + boolean verboseStats, + boolean cpuTimerEnabled) + { + this.taskStateMachine = checkNotNull(taskStateMachine, "taskStateMachine is null"); + this.queryContext = requireNonNull(queryContext, "queryContext is null"); + this.executor = checkNotNull(executor, "executor is null"); + this.session = session; + this.maxMemory = checkNotNull(maxMemory, "maxMemory is null").toBytes(); + this.operatorPreAllocatedMemory = checkNotNull(operatorPreAllocatedMemory, "operatorPreAllocatedMemory is null"); + + taskStateMachine.addStateChangeListener(new StateChangeListener() + { + @Override + public void stateChanged(TaskState newState) + { + if (newState.isDone()) { + executionEndTime.set(DateTime.now()); + endNanos.set(System.nanoTime()); + } + } + }); + + this.verboseStats = verboseStats; + this.cpuTimerEnabled = cpuTimerEnabled; + } + + public TaskId getTaskId() + { + return taskStateMachine.getTaskId(); + } + + public PipelineContext addPipelineContext(boolean inputPipeline, boolean outputPipeline) + { + PipelineContext pipelineContext = new PipelineContext(this, executor, inputPipeline, outputPipeline); + pipelineContexts.add(pipelineContext); + return pipelineContext; + } + + public Session getSession() + { + return session; + } + + public void start() + { + DateTime now = DateTime.now(); + executionStartTime.compareAndSet(null, now); + startNanos.compareAndSet(0, System.nanoTime()); + + // always update last execution start time + lastExecutionStartTime.set(now); + } + + public void failed(Throwable cause) + { + taskStateMachine.failed(cause); + } + + public boolean isDone() + { + return taskStateMachine.getState().isDone(); + } + + public TaskState getState() + { + return taskStateMachine.getState(); + } + + public DataSize getMaxMemorySize() + { + return new DataSize(maxMemory, BYTE).convertToMostSuccinctDataSize(); + } + + public DataSize getOperatorPreAllocatedMemory() + { + return operatorPreAllocatedMemory; + } + + public synchronized ListenableFuture reserveMemory(long bytes) + { + checkArgument(bytes >= 0, "bytes is negative"); + + if (memoryReservation.get() + bytes > maxMemory) { + throw new ExceededMemoryLimitException(getMaxMemorySize()); + } + ListenableFuture future = queryContext.reserveMemory(bytes); + memoryReservation.getAndAdd(bytes); + return future; + } + + public synchronized boolean tryReserveMemory(long bytes) + { + checkArgument(bytes >= 0, "bytes is negative"); + + if (memoryReservation.get() + bytes > maxMemory) { + return false; + } + if (queryContext.tryReserveMemory(bytes)) { + memoryReservation.getAndAdd(bytes); + return true; + } + return false; + } + + public synchronized void freeMemory(long bytes) + { + checkArgument(bytes >= 0, "bytes is negative"); + checkArgument(bytes <= memoryReservation.get(), "tried to free more memory than is reserved"); + memoryReservation.getAndAdd(-bytes); + queryContext.freeMemory(bytes); + } + + public void moreMemoryAvailable() + { + pipelineContexts.stream().forEach(PipelineContext::moreMemoryAvailable); + } + + public boolean isVerboseStats() + { + return verboseStats; + } + + public boolean isCpuTimerEnabled() + { + return cpuTimerEnabled; + } + + public CounterStat getInputDataSize() + { + CounterStat stat = new CounterStat(); + for (PipelineContext pipelineContext : pipelineContexts) { + if (pipelineContext.isInputPipeline()) { + stat.merge(pipelineContext.getInputDataSize()); + } + } + return stat; + } + + public CounterStat getInputPositions() + { + CounterStat stat = new CounterStat(); + for (PipelineContext pipelineContext : pipelineContexts) { + if (pipelineContext.isInputPipeline()) { + stat.merge(pipelineContext.getInputPositions()); + } + } + return stat; + } + + public CounterStat getOutputDataSize() + { + CounterStat stat = new CounterStat(); + for (PipelineContext pipelineContext : pipelineContexts) { + if (pipelineContext.isOutputPipeline()) { + stat.merge(pipelineContext.getOutputDataSize()); + } + } + return stat; + } + + public CounterStat getOutputPositions() + { + CounterStat stat = new CounterStat(); + for (PipelineContext pipelineContext : pipelineContexts) { + if (pipelineContext.isOutputPipeline()) { + stat.merge(pipelineContext.getOutputPositions()); + } + } + return stat; + } + + public TaskStats getTaskStats() + { + // check for end state to avoid callback ordering problems + if (taskStateMachine.getState().isDone()) { + DateTime now = DateTime.now(); + if (executionEndTime.compareAndSet(null, now)) { + lastExecutionStartTime.compareAndSet(null, now); + endNanos.set(System.nanoTime()); + } + } + + List pipelineStats = ImmutableList.copyOf(transform(pipelineContexts, PipelineContext::getPipelineStats)); + + int totalDrivers = 0; + int queuedDrivers = 0; + int queuedPartitionedDrivers = 0; + int runningDrivers = 0; + int runningPartitionedDrivers = 0; + int completedDrivers = 0; + + long totalScheduledTime = 0; + long totalCpuTime = 0; + long totalUserTime = 0; + long totalBlockedTime = 0; + + long rawInputDataSize = 0; + long rawInputPositions = 0; + + long processedInputDataSize = 0; + long processedInputPositions = 0; + + long outputDataSize = 0; + long outputPositions = 0; + + for (PipelineStats pipeline : pipelineStats) { + totalDrivers += pipeline.getTotalDrivers(); + queuedDrivers += pipeline.getQueuedDrivers(); + queuedPartitionedDrivers += pipeline.getQueuedPartitionedDrivers(); + runningDrivers += pipeline.getRunningDrivers(); + runningPartitionedDrivers += pipeline.getRunningPartitionedDrivers(); + completedDrivers += pipeline.getCompletedDrivers(); + + totalScheduledTime += pipeline.getTotalScheduledTime().roundTo(NANOSECONDS); + totalCpuTime += pipeline.getTotalCpuTime().roundTo(NANOSECONDS); + totalUserTime += pipeline.getTotalUserTime().roundTo(NANOSECONDS); + totalBlockedTime += pipeline.getTotalBlockedTime().roundTo(NANOSECONDS); + + if (pipeline.isInputPipeline()) { + rawInputDataSize += pipeline.getRawInputDataSize().toBytes(); + rawInputPositions += pipeline.getRawInputPositions(); + + processedInputDataSize += pipeline.getProcessedInputDataSize().toBytes(); + processedInputPositions += pipeline.getProcessedInputPositions(); + } + + if (pipeline.isOutputPipeline()) { + outputDataSize += pipeline.getOutputDataSize().toBytes(); + outputPositions += pipeline.getOutputPositions(); + } + } + + long startNanos = this.startNanos.get(); + if (startNanos < createNanos) { + startNanos = System.nanoTime(); + } + Duration queuedTime = new Duration(startNanos - createNanos, NANOSECONDS); + + long endNanos = this.endNanos.get(); + Duration elapsedTime; + if (endNanos >= startNanos) { + elapsedTime = new Duration(endNanos - createNanos, NANOSECONDS); + } + else { + elapsedTime = new Duration(0, NANOSECONDS); + } + + boolean fullyBlocked = pipelineStats.stream() + .filter(pipeline -> pipeline.getRunningDrivers() > 0 || pipeline.getRunningPartitionedDrivers() > 0) + .allMatch(PipelineStats::isFullyBlocked); + ImmutableSet blockedReasons = pipelineStats.stream() + .filter(pipeline -> pipeline.getRunningDrivers() > 0 || pipeline.getRunningPartitionedDrivers() > 0) + .flatMap(pipeline -> pipeline.getBlockedReasons().stream()) + .collect(ImmutableCollectors.toImmutableSet()); + return new TaskStats( + taskStateMachine.getCreatedTime(), + executionStartTime.get(), + lastExecutionStartTime.get(), + executionEndTime.get(), + elapsedTime.convertToMostSuccinctTimeUnit(), + queuedTime.convertToMostSuccinctTimeUnit(), + totalDrivers, + queuedDrivers, + queuedPartitionedDrivers, + runningDrivers, + runningPartitionedDrivers, + completedDrivers, + new DataSize(memoryReservation.get(), BYTE).convertToMostSuccinctDataSize(), + new Duration(totalScheduledTime, NANOSECONDS).convertToMostSuccinctTimeUnit(), + new Duration(totalCpuTime, NANOSECONDS).convertToMostSuccinctTimeUnit(), + new Duration(totalUserTime, NANOSECONDS).convertToMostSuccinctTimeUnit(), + new Duration(totalBlockedTime, NANOSECONDS).convertToMostSuccinctTimeUnit(), + fullyBlocked && (runningDrivers > 0 || runningPartitionedDrivers > 0), + blockedReasons, + new DataSize(rawInputDataSize, BYTE).convertToMostSuccinctDataSize(), + rawInputPositions, + new DataSize(processedInputDataSize, BYTE).convertToMostSuccinctDataSize(), + processedInputPositions, + new DataSize(outputDataSize, BYTE).convertToMostSuccinctDataSize(), + outputPositions, + pipelineStats); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/TaskOutputOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/TaskOutputOperator.java new file mode 100644 index 00000000..ed87c360 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/TaskOutputOperator.java @@ -0,0 +1,152 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.execution.SharedBuffer; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.ListenableFuture; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +public class TaskOutputOperator + implements Operator +{ + public static class TaskOutputFactory + implements OutputFactory + { + private final SharedBuffer sharedBuffer; + + public TaskOutputFactory(SharedBuffer sharedBuffer) + { + this.sharedBuffer = checkNotNull(sharedBuffer, "sharedBuffer is null"); + } + + @Override + public OperatorFactory createOutputOperator(int operatorId, List sourceTypes) + { + return new TaskOutputOperatorFactory(operatorId, sharedBuffer); + } + } + + public static class TaskOutputOperatorFactory + implements OperatorFactory + { + private final int operatorId; + private final SharedBuffer sharedBuffer; + + public TaskOutputOperatorFactory(int operatorId, SharedBuffer sharedBuffer) + { + this.operatorId = operatorId; + this.sharedBuffer = checkNotNull(sharedBuffer, "sharedBuffer is null"); + } + + @Override + public List getTypes() + { + return ImmutableList.of(); + } + + @Override + public Operator createOperator(DriverContext driverContext) + { + OperatorContext operatorContext = driverContext.addOperatorContext(operatorId, TaskOutputOperator.class.getSimpleName()); + return new TaskOutputOperator(operatorContext, sharedBuffer); + } + + @Override + public void close() + { + } + } + + private final OperatorContext operatorContext; + private final SharedBuffer sharedBuffer; + private ListenableFuture blocked = NOT_BLOCKED; + private boolean finished; + + public TaskOutputOperator(OperatorContext operatorContext, SharedBuffer sharedBuffer) + { + this.operatorContext = checkNotNull(operatorContext, "operatorContext is null"); + this.sharedBuffer = checkNotNull(sharedBuffer, "sharedBuffer is null"); + } + + @Override + public OperatorContext getOperatorContext() + { + return operatorContext; + } + + @Override + public List getTypes() + { + return ImmutableList.of(); + } + + @Override + public void finish() + { + finished = true; + } + + @Override + public boolean isFinished() + { + if (blocked != NOT_BLOCKED && blocked.isDone()) { + blocked = NOT_BLOCKED; + } + + return finished && blocked == NOT_BLOCKED; + } + + @Override + public ListenableFuture isBlocked() + { + if (blocked != NOT_BLOCKED && blocked.isDone()) { + blocked = NOT_BLOCKED; + } + return blocked; + } + + @Override + public boolean needsInput() + { + if (blocked != NOT_BLOCKED && blocked.isDone()) { + blocked = NOT_BLOCKED; + } + return !finished && blocked == NOT_BLOCKED; + } + + @Override + public void addInput(Page page) + { + checkNotNull(page, "page is null"); + checkState(blocked == NOT_BLOCKED, "output is already blocked"); + ListenableFuture future = sharedBuffer.enqueue(page); + if (!future.isDone()) { + this.blocked = future; + } + operatorContext.recordGeneratedOutput(page.getSizeInBytes(), page.getPositionCount()); + } + + @Override + public Page getOutput() + { + return null; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/TaskStats.java b/presto-main/src/main/java/com/facebook/presto/operator/TaskStats.java new file mode 100644 index 00000000..283e1a23 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/TaskStats.java @@ -0,0 +1,373 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import io.airlift.units.DataSize; +import io.airlift.units.Duration; +import org.joda.time.DateTime; + +import javax.annotation.Nullable; + +import java.util.List; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.airlift.units.DataSize.Unit.BYTE; +import static java.util.Objects.requireNonNull; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +public class TaskStats +{ + private final DateTime createTime; + private final DateTime firstStartTime; + private final DateTime lastStartTime; + private final DateTime endTime; + + private final Duration elapsedTime; + private final Duration queuedTime; + + private final int totalDrivers; + private final int queuedDrivers; + private final int queuedPartitionedDrivers; + private final int runningDrivers; + private final int runningPartitionedDrivers; + private final int completedDrivers; + + private final DataSize memoryReservation; + + private final Duration totalScheduledTime; + private final Duration totalCpuTime; + private final Duration totalUserTime; + private final Duration totalBlockedTime; + private final boolean fullyBlocked; + private final Set blockedReasons; + + private final DataSize rawInputDataSize; + private final long rawInputPositions; + + private final DataSize processedInputDataSize; + private final long processedInputPositions; + + private final DataSize outputDataSize; + private final long outputPositions; + + private final List pipelines; + + public TaskStats(DateTime createTime, DateTime endTime) + { + this(createTime, + null, + null, + endTime, + new Duration(0, MILLISECONDS), + new Duration(0, MILLISECONDS), + 0, + 0, + 0, + 0, + 0, + 0, + new DataSize(0, BYTE), + new Duration(0, MILLISECONDS), + new Duration(0, MILLISECONDS), + new Duration(0, MILLISECONDS), + new Duration(0, MILLISECONDS), + false, + ImmutableSet.of(), + new DataSize(0, BYTE), + 0, + new DataSize(0, BYTE), + 0, + new DataSize(0, BYTE), + 0, + ImmutableList.of()); + } + + @JsonCreator + public TaskStats( + @JsonProperty("createTime") DateTime createTime, + @JsonProperty("firstStartTime") DateTime firstStartTime, + @JsonProperty("lastStartTime") DateTime lastStartTime, + @JsonProperty("endTime") DateTime endTime, + @JsonProperty("elapsedTime") Duration elapsedTime, + @JsonProperty("queuedTime") Duration queuedTime, + + @JsonProperty("totalDrivers") int totalDrivers, + @JsonProperty("queuedDrivers") int queuedDrivers, + @JsonProperty("queuedPartitionedDrivers") int queuedPartitionedDrivers, + @JsonProperty("runningDrivers") int runningDrivers, + @JsonProperty("runningPartitionedDrivers") int runningPartitionedDrivers, + @JsonProperty("completedDrivers") int completedDrivers, + + @JsonProperty("memoryReservation") DataSize memoryReservation, + + @JsonProperty("totalScheduledTime") Duration totalScheduledTime, + @JsonProperty("totalCpuTime") Duration totalCpuTime, + @JsonProperty("totalUserTime") Duration totalUserTime, + @JsonProperty("totalBlockedTime") Duration totalBlockedTime, + @JsonProperty("fullyBlocked") boolean fullyBlocked, + @JsonProperty("blockedReasons") Set blockedReasons, + + @JsonProperty("rawInputDataSize") DataSize rawInputDataSize, + @JsonProperty("rawInputPositions") long rawInputPositions, + + @JsonProperty("processedInputDataSize") DataSize processedInputDataSize, + @JsonProperty("processedInputPositions") long processedInputPositions, + + @JsonProperty("outputDataSize") DataSize outputDataSize, + @JsonProperty("outputPositions") long outputPositions, + + @JsonProperty("pipelines") List pipelines) + { + this.createTime = checkNotNull(createTime, "createTime is null"); + this.firstStartTime = firstStartTime; + this.lastStartTime = lastStartTime; + this.endTime = endTime; + this.elapsedTime = checkNotNull(elapsedTime, "elapsedTime is null"); + this.queuedTime = checkNotNull(queuedTime, "queuedTime is null"); + + checkArgument(totalDrivers >= 0, "totalDrivers is negative"); + this.totalDrivers = totalDrivers; + checkArgument(queuedDrivers >= 0, "queuedDrivers is negative"); + this.queuedDrivers = queuedDrivers; + checkArgument(queuedPartitionedDrivers >= 0, "queuedPartitionedDrivers is negative"); + this.queuedPartitionedDrivers = queuedPartitionedDrivers; + + checkArgument(runningDrivers >= 0, "runningDrivers is negative"); + this.runningDrivers = runningDrivers; + checkArgument(runningPartitionedDrivers >= 0, "runningPartitionedDrivers is negative"); + this.runningPartitionedDrivers = runningPartitionedDrivers; + + checkArgument(completedDrivers >= 0, "completedDrivers is negative"); + this.completedDrivers = completedDrivers; + + this.memoryReservation = checkNotNull(memoryReservation, "memoryReservation is null"); + + this.totalScheduledTime = checkNotNull(totalScheduledTime, "totalScheduledTime is null"); + this.totalCpuTime = checkNotNull(totalCpuTime, "totalCpuTime is null"); + this.totalUserTime = checkNotNull(totalUserTime, "totalUserTime is null"); + this.totalBlockedTime = checkNotNull(totalBlockedTime, "totalBlockedTime is null"); + this.fullyBlocked = fullyBlocked; + this.blockedReasons = ImmutableSet.copyOf(requireNonNull(blockedReasons, "blockedReasons is null")); + + this.rawInputDataSize = checkNotNull(rawInputDataSize, "rawInputDataSize is null"); + checkArgument(rawInputPositions >= 0, "rawInputPositions is negative"); + this.rawInputPositions = rawInputPositions; + + this.processedInputDataSize = checkNotNull(processedInputDataSize, "processedInputDataSize is null"); + checkArgument(processedInputPositions >= 0, "processedInputPositions is negative"); + this.processedInputPositions = processedInputPositions; + + this.outputDataSize = checkNotNull(outputDataSize, "outputDataSize is null"); + checkArgument(outputPositions >= 0, "outputPositions is negative"); + this.outputPositions = outputPositions; + + this.pipelines = ImmutableList.copyOf(checkNotNull(pipelines, "pipelines is null")); + } + + @JsonProperty + public DateTime getCreateTime() + { + return createTime; + } + + @Nullable + @JsonProperty + public DateTime getFirstStartTime() + { + return firstStartTime; + } + + @Nullable + @JsonProperty + public DateTime getLastStartTime() + { + return lastStartTime; + } + + @Nullable + @JsonProperty + public DateTime getEndTime() + { + return endTime; + } + + @JsonProperty + public Duration getElapsedTime() + { + return elapsedTime; + } + + @JsonProperty + public Duration getQueuedTime() + { + return queuedTime; + } + + @JsonProperty + public int getTotalDrivers() + { + return totalDrivers; + } + + @JsonProperty + public int getQueuedDrivers() + { + return queuedDrivers; + } + + @JsonProperty + public int getRunningDrivers() + { + return runningDrivers; + } + + @JsonProperty + public int getCompletedDrivers() + { + return completedDrivers; + } + + @JsonProperty + public DataSize getMemoryReservation() + { + return memoryReservation; + } + + @JsonProperty + public Duration getTotalScheduledTime() + { + return totalScheduledTime; + } + + @JsonProperty + public Duration getTotalCpuTime() + { + return totalCpuTime; + } + + @JsonProperty + public Duration getTotalUserTime() + { + return totalUserTime; + } + + @JsonProperty + public Duration getTotalBlockedTime() + { + return totalBlockedTime; + } + + @JsonProperty + public boolean isFullyBlocked() + { + return fullyBlocked; + } + + @JsonProperty + public Set getBlockedReasons() + { + return blockedReasons; + } + + @JsonProperty + public DataSize getRawInputDataSize() + { + return rawInputDataSize; + } + + @JsonProperty + public long getRawInputPositions() + { + return rawInputPositions; + } + + @JsonProperty + public DataSize getProcessedInputDataSize() + { + return processedInputDataSize; + } + + @JsonProperty + public long getProcessedInputPositions() + { + return processedInputPositions; + } + + @JsonProperty + public DataSize getOutputDataSize() + { + return outputDataSize; + } + + @JsonProperty + public long getOutputPositions() + { + return outputPositions; + } + + @JsonProperty + public List getPipelines() + { + return pipelines; + } + + @JsonProperty + public int getQueuedPartitionedDrivers() + { + return queuedPartitionedDrivers; + } + + @JsonProperty + public int getRunningPartitionedDrivers() + { + return runningPartitionedDrivers; + } + + public TaskStats summarize() + { + return new TaskStats( + createTime, + firstStartTime, + lastStartTime, + endTime, + elapsedTime, + queuedTime, + totalDrivers, + queuedDrivers, + queuedPartitionedDrivers, + runningDrivers, + runningPartitionedDrivers, + completedDrivers, + memoryReservation, + totalScheduledTime, + totalCpuTime, + totalUserTime, + totalBlockedTime, + fullyBlocked, + blockedReasons, + rawInputDataSize, + rawInputPositions, + processedInputDataSize, + processedInputPositions, + outputDataSize, + outputPositions, + ImmutableList.of()); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/TopNOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/TopNOperator.java new file mode 100644 index 00000000..a98cc3ef --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/TopNOperator.java @@ -0,0 +1,358 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.SortOrder; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Ordering; +import io.airlift.units.DataSize; + +import java.util.Iterator; +import java.util.List; +import java.util.PriorityQueue; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +/** + * Returns the top N rows from the source sorted according to the specified ordering in the keyChannelIndex channel. + */ +public class TopNOperator + implements Operator +{ + public static class TopNOperatorFactory + implements OperatorFactory + { + private final int operatorId; + private final List sourceTypes; + private final int n; + private final List sortTypes; + private final List sortChannels; + private final List sortOrders; + private final boolean partial; + private boolean closed; + + public TopNOperatorFactory( + int operatorId, + List types, + int n, + List sortChannels, + List sortOrders, + boolean partial) + { + this.operatorId = operatorId; + this.sourceTypes = ImmutableList.copyOf(checkNotNull(types, "types is null")); + this.n = n; + ImmutableList.Builder sortTypes = ImmutableList.builder(); + for (int channel : sortChannels) { + sortTypes.add(types.get(channel)); + } + this.sortTypes = sortTypes.build(); + this.sortChannels = ImmutableList.copyOf(checkNotNull(sortChannels, "sortChannels is null")); + this.sortOrders = ImmutableList.copyOf(checkNotNull(sortOrders, "sortOrders is null")); + this.partial = partial; + } + + @Override + public List getTypes() + { + return sourceTypes; + } + + @Override + public Operator createOperator(DriverContext driverContext) + { + checkState(!closed, "Factory is already closed"); + OperatorContext operatorContext = driverContext.addOperatorContext(operatorId, TopNOperator.class.getSimpleName()); + return new TopNOperator( + operatorContext, + sourceTypes, + n, + sortTypes, + sortChannels, + sortOrders, + partial); + } + + @Override + public void close() + { + closed = true; + } + } + + private static final int MAX_INITIAL_PRIORITY_QUEUE_SIZE = 10000; + private static final DataSize OVERHEAD_PER_VALUE = new DataSize(100, DataSize.Unit.BYTE); // for estimating in-memory size. This is a completely arbitrary number + + private final OperatorContext operatorContext; + private final List types; + private final int n; + private final List sortTypes; + private final List sortChannels; + private final List sortOrders; + private final boolean partial; + + private final PageBuilder pageBuilder; + + private TopNBuilder topNBuilder; + private boolean finishing; + + private Iterator outputIterator; + + public TopNOperator( + OperatorContext operatorContext, + List types, + int n, + List sortTypes, + List sortChannels, + List sortOrders, + boolean partial) + { + this.operatorContext = checkNotNull(operatorContext, "operatorContext is null"); + this.types = checkNotNull(types, "types is null"); + + checkArgument(n >= 0, "n must be positive"); + this.n = n; + + this.sortTypes = checkNotNull(sortTypes, "sortTypes is null"); + this.sortChannels = checkNotNull(sortChannels, "sortChannels is null"); + this.sortOrders = checkNotNull(sortOrders, "sortOrders is null"); + + this.partial = partial; + + this.pageBuilder = new PageBuilder(types); + + if (n == 0) { + finishing = true; + } + } + + @Override + public OperatorContext getOperatorContext() + { + return operatorContext; + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public void finish() + { + finishing = true; + } + + @Override + public boolean isFinished() + { + return finishing && topNBuilder == null && (outputIterator == null || !outputIterator.hasNext()); + } + + @Override + public boolean needsInput() + { + return !finishing && outputIterator == null && (topNBuilder == null || !topNBuilder.isFull()); + } + + @Override + public void addInput(Page page) + { + checkState(!finishing, "Operator is already finishing"); + checkNotNull(page, "page is null"); + if (topNBuilder == null) { + topNBuilder = new TopNBuilder( + n, + partial, + sortTypes, + sortChannels, + sortOrders, + operatorContext); + } + + checkState(!topNBuilder.isFull(), "Aggregation buffer is full"); + topNBuilder.processPage(page); + } + + @Override + public Page getOutput() + { + if (outputIterator == null || !outputIterator.hasNext()) { + // no data + if (topNBuilder == null) { + return null; + } + + // only flush if we are finishing or the aggregation builder is full + if (!finishing && !topNBuilder.isFull()) { + return null; + } + + // Only partial aggregation can flush early. Also, check that we are not flushing tiny bits at a time + if (finishing || partial) { + outputIterator = topNBuilder.build(); + topNBuilder = null; + } + } + + pageBuilder.reset(); + while (!pageBuilder.isFull() && outputIterator.hasNext()) { + Block[] next = outputIterator.next(); + pageBuilder.declarePosition(); + for (int i = 0; i < next.length; i++) { + Type type = types.get(i); + type.appendTo(next[i], 0, pageBuilder.getBlockBuilder(i)); + } + } + + return pageBuilder.build(); + } + + private static class TopNBuilder + { + private final int n; + private final boolean partial; + private final List sortTypes; + private final List sortChannels; + private final List sortOrders; + private final OperatorContext operatorContext; + private final PriorityQueue globalCandidates; + + private long memorySize; + + private TopNBuilder(int n, + boolean partial, + List sortTypes, + List sortChannels, + List sortOrders, + OperatorContext operatorContext) + { + this.n = n; + this.partial = partial; + + this.sortTypes = sortTypes; + this.sortChannels = sortChannels; + this.sortOrders = sortOrders; + + this.operatorContext = operatorContext; + + Ordering comparator = Ordering.from(new RowComparator(sortTypes, sortChannels, sortOrders)).reverse(); + this.globalCandidates = new PriorityQueue<>(Math.min(n, MAX_INITIAL_PRIORITY_QUEUE_SIZE), comparator); + } + + public void processPage(Page page) + { + long sizeDelta = mergeWithGlobalCandidates(page); + memorySize += sizeDelta; + } + + private long mergeWithGlobalCandidates(Page page) + { + long sizeDelta = 0; + + Block[] blocks = page.getBlocks(); + for (int position = 0; position < page.getPositionCount(); position++) { + if (globalCandidates.size() < n || compare(position, blocks, globalCandidates.peek()) < 0) { + sizeDelta += addRow(position, blocks); + } + } + + return sizeDelta; + } + + private int compare(int position, Block[] blocks, Block[] currentMax) + { + for (int i = 0; i < sortChannels.size(); i++) { + Type type = sortTypes.get(i); + int sortChannel = sortChannels.get(i); + SortOrder sortOrder = sortOrders.get(i); + + Block block = blocks[sortChannel]; + Block currentMaxValue = currentMax[sortChannel]; + + // compare the right value to the left block but negate the result since we are evaluating in the opposite order + int compare = -sortOrder.compareBlockValue(type, currentMaxValue, 0, block, position); + if (compare != 0) { + return compare; + } + } + return 0; + } + + private long addRow(int position, Block[] blocks) + { + long sizeDelta = 0; + Block[] row = getValues(position, blocks); + + sizeDelta += sizeOfRow(row); + globalCandidates.add(row); + + while (globalCandidates.size() > n) { + Block[] previous = globalCandidates.remove(); + sizeDelta -= sizeOfRow(previous); + } + return sizeDelta; + } + + private static long sizeOfRow(Block[] row) + { + long size = OVERHEAD_PER_VALUE.toBytes(); + for (Block value : row) { + size += value.getRetainedSizeInBytes(); + } + return size; + } + + private static Block[] getValues(int position, Block[] blocks) + { + Block[] row = new Block[blocks.length]; + for (int i = 0; i < blocks.length; i++) { + row[i] = blocks[i].getSingleValueBlock(position); + } + return row; + } + + private boolean isFull() + { + long memorySize = this.memorySize - operatorContext.getOperatorPreAllocatedMemory().toBytes(); + if (memorySize < 0) { + memorySize = 0; + } + if (partial) { + return !operatorContext.trySetMemoryReservation(memorySize); + } + else { + operatorContext.setMemoryReservation(memorySize); + return false; + } + } + + public Iterator build() + { + ImmutableList.Builder minSortedGlobalCandidates = ImmutableList.builder(); + while (!globalCandidates.isEmpty()) { + Block[] row = globalCandidates.remove(); + minSortedGlobalCandidates.add(row); + } + return minSortedGlobalCandidates.build().reverse().iterator(); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/TopNRowNumberOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/TopNRowNumberOperator.java new file mode 100644 index 00000000..7b1113fc --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/TopNRowNumberOperator.java @@ -0,0 +1,473 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.SortOrder; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.MinMaxPriorityQueue; +import com.google.common.collect.Ordering; +import com.google.common.primitives.Ints; +import io.airlift.units.DataSize; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static com.facebook.presto.operator.GroupByHash.createGroupByHash; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +public class TopNRowNumberOperator + implements Operator +{ + public static class TopNRowNumberOperatorFactory + implements OperatorFactory + { + private final int operatorId; + + private final List sourceTypes; + + private final List outputChannels; + private final List partitionChannels; + private final List partitionTypes; + private final List sortChannels; + private final List sortOrder; + private final int maxRowCountPerPartition; + private final Optional hashChannel; + private final int expectedPositions; + + private final List types; + private final List sortTypes; + private final boolean generateRowNumber; + private boolean closed; + + public TopNRowNumberOperatorFactory( + int operatorId, + List sourceTypes, + List outputChannels, + List partitionChannels, + List partitionTypes, + List sortChannels, + List sortOrder, + int maxRowCountPerPartition, + boolean partial, + Optional hashChannel, + int expectedPositions) + { + this.operatorId = operatorId; + this.sourceTypes = ImmutableList.copyOf(sourceTypes); + this.outputChannels = ImmutableList.copyOf(checkNotNull(outputChannels, "outputChannels is null")); + this.partitionChannels = ImmutableList.copyOf(checkNotNull(partitionChannels, "partitionChannels is null")); + this.partitionTypes = ImmutableList.copyOf(checkNotNull(partitionTypes, "partitionTypes is null")); + this.sortChannels = ImmutableList.copyOf(checkNotNull(sortChannels)); + this.sortOrder = ImmutableList.copyOf(checkNotNull(sortOrder)); + this.hashChannel = checkNotNull(hashChannel, "hashChannel is null"); + checkArgument(maxRowCountPerPartition > 0, "maxRowCountPerPartition must be > 0"); + this.maxRowCountPerPartition = maxRowCountPerPartition; + checkArgument(expectedPositions > 0, "expectedPositions must be > 0"); + this.generateRowNumber = !partial || !partitionChannels.isEmpty(); + this.expectedPositions = expectedPositions; + + this.types = toTypes(sourceTypes, outputChannels, generateRowNumber); + ImmutableList.Builder sortTypes = ImmutableList.builder(); + for (int channel : sortChannels) { + sortTypes.add(types.get(channel)); + } + this.sortTypes = sortTypes.build(); + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public Operator createOperator(DriverContext driverContext) + { + checkState(!closed, "Factory is already closed"); + OperatorContext operatorContext = driverContext.addOperatorContext(operatorId, TopNRowNumberOperator.class.getSimpleName()); + return new TopNRowNumberOperator( + operatorContext, + sourceTypes, + outputChannels, + partitionChannels, + partitionTypes, + sortChannels, + sortOrder, + sortTypes, + maxRowCountPerPartition, + generateRowNumber, + hashChannel, + expectedPositions); + } + + @Override + public void close() + { + closed = true; + } + } + + private static final DataSize OVERHEAD_PER_VALUE = new DataSize(100, DataSize.Unit.BYTE); // for estimating in-memory size. This is a completely arbitrary number + + private final OperatorContext operatorContext; + private boolean finishing; + private final List types; + private final int[] outputChannels; + + private final List sortChannels; + private final List sortOrders; + private final List sortTypes; + private final boolean generateRowNumber; + private final int maxRowCountPerPartition; + + private final Map partitionRows; + private Optional flushingPartition; + private final PageBuilder pageBuilder; + private final Optional groupByHash; + + public TopNRowNumberOperator( + OperatorContext operatorContext, + List sourceTypes, + List outputChannels, + List partitionChannels, + List partitionTypes, + List sortChannels, + List sortOrders, + List sortTypes, + int maxRowCountPerPartition, + boolean generateRowNumber, + Optional hashChannel, + int expectedPositions) + { + this.operatorContext = checkNotNull(operatorContext, "operatorContext is null"); + this.outputChannels = Ints.toArray(checkNotNull(outputChannels, "outputChannels is null")); + + this.sortChannels = checkNotNull(sortChannels, "sortChannels is null"); + this.sortOrders = checkNotNull(sortOrders, "sortOrders is null"); + this.sortTypes = checkNotNull(sortTypes, "sortTypes is null"); + + checkArgument(maxRowCountPerPartition > 0, "maxRowCountPerPartition must be > 0"); + this.maxRowCountPerPartition = maxRowCountPerPartition; + this.generateRowNumber = generateRowNumber; + checkArgument(expectedPositions > 0, "expectedPositions must be > 0"); + + this.types = toTypes(sourceTypes, outputChannels, generateRowNumber); + this.partitionRows = new HashMap<>(); + if (partitionChannels.isEmpty()) { + this.groupByHash = Optional.empty(); + } + else { + this.groupByHash = Optional.of(createGroupByHash(partitionTypes, Ints.toArray(partitionChannels), Optional.empty(), hashChannel, expectedPositions)); + } + this.flushingPartition = Optional.empty(); + this.pageBuilder = new PageBuilder(types); + } + + @Override + public OperatorContext getOperatorContext() + { + return operatorContext; + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public void finish() + { + finishing = true; + } + + @Override + public boolean isFinished() + { + return finishing && isEmpty(); + } + + @Override + public boolean needsInput() + { + return !finishing && !isFlushing(); + } + + @Override + public void addInput(Page page) + { + checkState(!finishing, "Operator is already finishing"); + checkNotNull(page, "page is null"); + checkState(!isFlushing(), "Cannot add input with the operator is flushing data"); + processPage(page); + } + + @Override + public Page getOutput() + { + if (finishing && !isEmpty()) { + return getPage(); + } + return null; + } + + private void processPage(Page page) + { + Optional partitionIds = Optional.empty(); + if (groupByHash.isPresent()) { + GroupByHash hash = groupByHash.get(); + long groupByHashSize = hash.getEstimatedSize(); + partitionIds = Optional.of(hash.getGroupIds(page)); + operatorContext.reserveMemory(hash.getEstimatedSize() - groupByHashSize); + } + + long sizeDelta = 0; + Block[] blocks = page.getBlocks(); + for (int position = 0; position < page.getPositionCount(); position++) { + long partitionId = groupByHash.isPresent() ? partitionIds.get().getGroupId(position) : 0; + if (!partitionRows.containsKey(partitionId)) { + partitionRows.put(partitionId, new PartitionBuilder(sortTypes, sortChannels, sortOrders, maxRowCountPerPartition)); + } + PartitionBuilder partitionBuilder = partitionRows.get(partitionId); + if (partitionBuilder.getRowCount() < maxRowCountPerPartition) { + Block[] row = getSingleValueBlocks(page, position); + sizeDelta += partitionBuilder.addRow(row); + } + else if (compare(position, blocks, partitionBuilder.peekLastRow()) < 0) { + Block[] row = getSingleValueBlocks(page, position); + sizeDelta += partitionBuilder.replaceRow(row); + } + } + if (sizeDelta > 0) { + operatorContext.reserveMemory(sizeDelta); + } + else { + operatorContext.freeMemory(-sizeDelta); + } + } + + private int compare(int position, Block[] blocks, Block[] currentMax) + { + for (int i = 0; i < sortChannels.size(); i++) { + Type type = sortTypes.get(i); + int sortChannel = sortChannels.get(i); + SortOrder sortOrder = sortOrders.get(i); + + Block block = blocks[sortChannel]; + Block currentMaxValue = currentMax[sortChannel]; + + int compare = sortOrder.compareBlockValue(type, block, position, currentMaxValue, 0); + if (compare != 0) { + return compare; + } + } + return 0; + } + + private Page getPage() + { + if (!flushingPartition.isPresent()) { + flushingPartition = getFlushingPartition(); + } + + pageBuilder.reset(); + long sizeDelta = 0; + while (!pageBuilder.isFull() && flushingPartition.isPresent()) { + FlushingPartition currentFlushingPartition = flushingPartition.get(); + + while (!pageBuilder.isFull() && currentFlushingPartition.hasNext()) { + Block[] next = currentFlushingPartition.next(); + sizeDelta += sizeOfRow(next); + + pageBuilder.declarePosition(); + for (int i = 0; i < outputChannels.length; i++) { + int channel = outputChannels[i]; + Type type = types.get(channel); + type.appendTo(next[channel], 0, pageBuilder.getBlockBuilder(i)); + } + if (generateRowNumber) { + BIGINT.writeLong(pageBuilder.getBlockBuilder(outputChannels.length), currentFlushingPartition.getRowNumber()); + } + } + if (!currentFlushingPartition.hasNext()) { + flushingPartition = getFlushingPartition(); + } + } + if (pageBuilder.isEmpty()) { + return null; + } + Page page = pageBuilder.build(); + operatorContext.freeMemory(sizeDelta); + return page; + } + + private Optional getFlushingPartition() + { + int maxPartitionSize = 0; + PartitionBuilder chosenPartitionBuilder = null; + long chosenPartitionId = -1; + + for (Map.Entry entry : partitionRows.entrySet()) { + if (entry.getValue().getRowCount() > maxPartitionSize) { + chosenPartitionBuilder = entry.getValue(); + maxPartitionSize = chosenPartitionBuilder.getRowCount(); + chosenPartitionId = entry.getKey(); + if (maxPartitionSize == maxRowCountPerPartition) { + break; + } + } + } + if (chosenPartitionBuilder == null) { + return Optional.empty(); + } + FlushingPartition flushingPartition = new FlushingPartition(chosenPartitionBuilder.build()); + partitionRows.remove(chosenPartitionId); + return Optional.of(flushingPartition); + } + + public boolean isFlushing() + { + return flushingPartition.isPresent(); + } + + public boolean isEmpty() + { + return partitionRows.isEmpty(); + } + + private static Block[] getSingleValueBlocks(Page page, int position) + { + Block[] blocks = page.getBlocks(); + Block[] row = new Block[blocks.length]; + for (int i = 0; i < blocks.length; i++) { + row[i] = blocks[i].getSingleValueBlock(position); + } + return row; + } + + private static List toTypes(List sourceTypes, List outputChannels, boolean generateRowNumber) + { + ImmutableList.Builder types = ImmutableList.builder(); + for (int channel : outputChannels) { + types.add(sourceTypes.get(channel)); + } + if (generateRowNumber) { + types.add(BIGINT); + } + return types.build(); + } + + private static long sizeOfRow(Block[] row) + { + long size = OVERHEAD_PER_VALUE.toBytes(); + for (Block value : row) { + size += value.getRetainedSizeInBytes(); + } + return size; + } + + private static class PartitionBuilder + { + private final MinMaxPriorityQueue candidateRows; + private final int maxRowCountPerPartition; + + private PartitionBuilder(List sortTypes, List sortChannels, List sortOrders, int maxRowCountPerPartition) + { + this.maxRowCountPerPartition = maxRowCountPerPartition; + Ordering comparator = Ordering.from(new RowComparator(sortTypes, sortChannels, sortOrders)); + this.candidateRows = MinMaxPriorityQueue.orderedBy(comparator).maximumSize(maxRowCountPerPartition).create(); + } + + private long replaceRow(Block[] row) + { + checkState(candidateRows.size() == maxRowCountPerPartition); + Block[] previousRow = candidateRows.removeLast(); + long sizeDelta = addRow(row); + return sizeDelta - sizeOfRow(previousRow); + } + + private long addRow(Block[] row) + { + checkState(candidateRows.size() < maxRowCountPerPartition); + long sizeDelta = sizeOfRow(row); + candidateRows.add(row); + return sizeDelta; + } + + private Iterator build() + { + ImmutableList.Builder sortedRows = ImmutableList.builder(); + while (!candidateRows.isEmpty()) { + sortedRows.add(candidateRows.poll()); + } + return sortedRows.build().iterator(); + } + + private int getRowCount() + { + return candidateRows.size(); + } + + private Block[] peekLastRow() + { + return candidateRows.peekLast(); + } + } + + private static class FlushingPartition + implements Iterator + { + private final Iterator outputIterator; + private int rowNumber; + + private FlushingPartition(Iterator outputIterator) + { + this.outputIterator = outputIterator; + } + + @Override + public boolean hasNext() + { + return outputIterator.hasNext(); + } + + @Override + public Block[] next() + { + rowNumber++; + return outputIterator.next(); + } + + @Override + public void remove() + { + throw new UnsupportedOperationException(); + } + + public int getRowNumber() + { + return rowNumber; + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/TwoChannelJoinProbe.java b/presto-main/src/main/java/com/facebook/presto/operator/TwoChannelJoinProbe.java new file mode 100644 index 00000000..d9deeefe --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/TwoChannelJoinProbe.java @@ -0,0 +1,124 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.type.Type; + +import java.util.List; + +// This class exists as template for code generation and for testing +public class TwoChannelJoinProbe + implements JoinProbe +{ + public static class TwoChannelJoinProbeFactory + implements JoinProbeFactory + { + private final List types; + + public TwoChannelJoinProbeFactory(List types) + { + this.types = types; + } + + @Override + public JoinProbe createJoinProbe(LookupSource lookupSource, Page page) + { + return new TwoChannelJoinProbe(types, lookupSource, page); + } + } + + private final LookupSource lookupSource; + private final int positionCount; + private final Type typeA; + private final Type typeB; + private final Block blockA; + private final Block blockB; + private final Block probeBlockA; + private final Block probeBlockB; + private final Block[] probeBlocks; + private final Page probePage; + private int position = -1; + + public TwoChannelJoinProbe(List types, LookupSource lookupSource, Page page) + { + this.lookupSource = lookupSource; + this.positionCount = page.getPositionCount(); + this.typeA = types.get(0); + this.typeB = types.get(1); + this.blockA = page.getBlock(0); + this.blockB = page.getBlock(1); + this.probeBlockA = blockA; + this.probeBlockB = blockB; + this.probeBlocks = new Block[2]; + probeBlocks[0] = probeBlockA; + probeBlocks[1] = probeBlockB; + this.probePage = new Page(probeBlocks); + } + + @Override + public int getChannelCount() + { + return 2; + } + + @Override + public void appendTo(PageBuilder pageBuilder) + { + typeA.appendTo(blockA, position, pageBuilder.getBlockBuilder(0)); + typeB.appendTo(blockB, position, pageBuilder.getBlockBuilder(1)); + } + + @Override + public boolean advanceNextPosition() + { + position++; + return position < positionCount; + } + + @Override + public long getCurrentJoinPosition() + { + if (currentRowContainsNull()) { + return -1; + } + return lookupSource.getJoinPosition(position, probePage); + } + + private boolean currentRowContainsNull() + { + if (probeBlockA.isNull(position)) { + return true; + } + if (probeBlockB.isNull(position)) { + return true; + } + return false; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/UnnestOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/UnnestOperator.java new file mode 100644 index 00000000..29309483 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/UnnestOperator.java @@ -0,0 +1,259 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.type.ArrayType; +import com.facebook.presto.type.MapType; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; + +import java.util.ArrayList; +import java.util.List; + +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +public class UnnestOperator + implements Operator +{ + public static class UnnestOperatorFactory + implements OperatorFactory + { + private final int operatorId; + private final List replicateChannels; + private final List replicateTypes; + private final List unnestChannels; + private final List unnestTypes; + private final boolean withOrdinality; + private boolean closed; + private final ImmutableList types; + + public UnnestOperatorFactory(int operatorId, List replicateChannels, List replicateTypes, List unnestChannels, List unnestTypes, boolean withOrdinality) + { + this.operatorId = operatorId; + this.replicateChannels = ImmutableList.copyOf(checkNotNull(replicateChannels, "replicateChannels is null")); + this.replicateTypes = ImmutableList.copyOf(checkNotNull(replicateTypes, "replicateTypes is null")); + checkArgument(replicateChannels.size() == replicateTypes.size(), "replicateChannels and replicateTypes do not match"); + this.unnestChannels = ImmutableList.copyOf(checkNotNull(unnestChannels, "unnestChannels is null")); + this.unnestTypes = ImmutableList.copyOf(checkNotNull(unnestTypes, "unnestTypes is null")); + checkArgument(unnestChannels.size() == unnestTypes.size(), "unnestChannels and unnestTypes do not match"); + this.withOrdinality = withOrdinality; + ImmutableList.Builder typesBuilder = ImmutableList.builder() + .addAll(replicateTypes) + .addAll(getUnnestedTypes(unnestTypes)); + if (withOrdinality) { + typesBuilder.add(BIGINT); + } + this.types = typesBuilder.build(); + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public Operator createOperator(DriverContext driverContext) + { + checkState(!closed, "Factory is already closed"); + OperatorContext operatorContext = driverContext.addOperatorContext(operatorId, UnnestOperator.class.getSimpleName()); + return new UnnestOperator(operatorContext, replicateChannels, replicateTypes, unnestChannels, unnestTypes, withOrdinality); + } + + @Override + public void close() + { + closed = true; + } + } + + private final OperatorContext operatorContext; + private final List replicateChannels; + private final List replicateTypes; + private final List unnestChannels; + private final List unnestTypes; + private final boolean withOrdinality; + private final List outputTypes; + private final PageBuilder pageBuilder; + private final List unnesters; + private boolean finishing; + private Page currentPage; + private int currentPosition; + private int ordinalityCount; + + public UnnestOperator(OperatorContext operatorContext, List replicateChannels, List replicateTypes, List unnestChannels, List unnestTypes, boolean withOrdinality) + { + this.operatorContext = checkNotNull(operatorContext, "operatorContext is null"); + this.replicateChannels = ImmutableList.copyOf(checkNotNull(replicateChannels, "replicateChannels is null")); + this.replicateTypes = ImmutableList.copyOf(checkNotNull(replicateTypes, "replicateTypes is null")); + this.unnestChannels = ImmutableList.copyOf(checkNotNull(unnestChannels, "unnestChannels is null")); + this.unnestTypes = ImmutableList.copyOf(checkNotNull(unnestTypes, "unnestTypes is null")); + this.withOrdinality = withOrdinality; + checkArgument(replicateChannels.size() == replicateTypes.size(), "replicate channels or types has wrong size"); + checkArgument(unnestChannels.size() == unnestTypes.size(), "unnest channels or types has wrong size"); + ImmutableList.Builder outputTypesBuilder = ImmutableList.builder() + .addAll(replicateTypes) + .addAll(getUnnestedTypes(unnestTypes)); + if (withOrdinality) { + outputTypesBuilder.add(BIGINT); + } + this.outputTypes = outputTypesBuilder.build(); + this.pageBuilder = new PageBuilder(outputTypes); + this.unnesters = new ArrayList<>(); + } + + private static List getUnnestedTypes(List types) + { + ImmutableList.Builder builder = ImmutableList.builder(); + for (Type type : types) { + checkArgument(type instanceof ArrayType || type instanceof MapType, "Can only unnest map and array types"); + builder.addAll(type.getTypeParameters()); + } + return builder.build(); + } + + @Override + public OperatorContext getOperatorContext() + { + return operatorContext; + } + + @Override + public final List getTypes() + { + return outputTypes; + } + + @Override + public void finish() + { + finishing = true; + } + + @Override + public boolean isFinished() + { + return finishing && pageBuilder.isEmpty() && currentPage == null; + } + + @Override + public boolean needsInput() + { + return !finishing && !pageBuilder.isFull() && currentPage == null; + } + + @Override + public void addInput(Page page) + { + checkState(!finishing, "Operator is already finishing"); + checkNotNull(page, "page is null"); + checkState(currentPage == null, "currentPage is not null"); + checkState(!pageBuilder.isFull(), "Page buffer is full"); + + currentPage = page; + currentPosition = 0; + initializeUnnesters(); + } + + private void initializeUnnesters() + { + unnesters.clear(); + for (int i = 0; i < unnestTypes.size(); i++) { + Type type = unnestTypes.get(i); + int channel = unnestChannels.get(i); + Slice slice = null; + if (!currentPage.getBlock(channel).isNull(currentPosition)) { + slice = type.getSlice(currentPage.getBlock(channel), currentPosition); + } + if (type instanceof ArrayType) { + unnesters.add(new ArrayUnnester((ArrayType) type, slice)); + } + else if (type instanceof MapType) { + unnesters.add(new MapUnnester((MapType) type, slice)); + } + else { + throw new IllegalArgumentException("Cannot unnest type: " + type); + } + } + ordinalityCount = 0; + } + + private boolean anyUnnesterHasData() + { + for (Unnester unnester : unnesters) { + if (unnester.hasNext()) { + return true; + } + } + return false; + } + + @Override + public Page getOutput() + { + while (!pageBuilder.isFull() && currentPage != null) { + // Advance until we find data to unnest + while (!anyUnnesterHasData()) { + currentPosition++; + if (currentPosition == currentPage.getPositionCount()) { + currentPage = null; + currentPosition = 0; + break; + } + initializeUnnesters(); + } + while (!pageBuilder.isFull() && anyUnnesterHasData()) { + // Copy all the channels marked for replication + for (int replicateChannel = 0; replicateChannel < replicateTypes.size(); replicateChannel++) { + Type type = replicateTypes.get(replicateChannel); + int channel = replicateChannels.get(replicateChannel); + type.appendTo(currentPage.getBlock(channel), currentPosition, pageBuilder.getBlockBuilder(replicateChannel)); + } + int offset = replicateTypes.size(); + + pageBuilder.declarePosition(); + for (Unnester unnester : unnesters) { + if (unnester.hasNext()) { + unnester.appendNext(pageBuilder, offset); + } + else { + for (int unnesterChannelIndex = 0; unnesterChannelIndex < unnester.getChannelCount(); unnesterChannelIndex++) { + pageBuilder.getBlockBuilder(offset + unnesterChannelIndex).appendNull(); + } + } + offset += unnester.getChannelCount(); + } + + if (withOrdinality) { + ordinalityCount++; + BIGINT.writeLong(pageBuilder.getBlockBuilder(offset), ordinalityCount); + } + } + } + + if ((!finishing && !pageBuilder.isFull()) || pageBuilder.isEmpty()) { + return null; + } + + Page page = pageBuilder.build(); + pageBuilder.reset(); + return page; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/Unnester.java b/presto-main/src/main/java/com/facebook/presto/operator/Unnester.java new file mode 100644 index 00000000..f3cc9f13 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/Unnester.java @@ -0,0 +1,25 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.PageBuilder; + +public interface Unnester +{ + int getChannelCount(); + + void appendNext(PageBuilder pageBuilder, int outputChannelOffset); + + boolean hasNext(); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/ValuesOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/ValuesOperator.java new file mode 100644 index 00000000..5985c73c --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/ValuesOperator.java @@ -0,0 +1,128 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterators; + +import java.util.Iterator; +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +public class ValuesOperator + implements Operator +{ + public static class ValuesOperatorFactory + implements OperatorFactory + { + private final int operatorId; + private final List types; + private final List pages; + private boolean closed; + + public ValuesOperatorFactory(int operatorId, List types, List pages) + { + this.operatorId = operatorId; + this.types = ImmutableList.copyOf(checkNotNull(types, "types is null")); + this.pages = ImmutableList.copyOf(checkNotNull(pages, "pages is null")); + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public Operator createOperator(DriverContext driverContext) + { + checkState(!closed, "Factory is already closed"); + OperatorContext operatorContext = driverContext.addOperatorContext(operatorId, ValuesOperator.class.getSimpleName()); + return new ValuesOperator(operatorContext, types, pages); + } + + @Override + public void close() + { + closed = true; + } + } + + private final OperatorContext operatorContext; + private final ImmutableList types; + private final Iterator pages; + + public ValuesOperator(OperatorContext operatorContext, List types, List pages) + { + this.operatorContext = checkNotNull(operatorContext, "operatorContext is null"); + this.types = ImmutableList.copyOf(checkNotNull(types, "types is null")); + + checkNotNull(pages, "pages is null"); + + this.pages = ImmutableList.copyOf(pages).iterator(); + } + + @Override + public OperatorContext getOperatorContext() + { + return operatorContext; + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public void finish() + { + Iterators.size(pages); + } + + @Override + public boolean isFinished() + { + return !pages.hasNext(); + } + + @Override + public boolean needsInput() + { + return false; + } + + @Override + public void addInput(Page page) + { + throw new UnsupportedOperationException(); + } + + @Override + public Page getOutput() + { + if (!pages.hasNext()) { + return null; + } + Page page = pages.next(); + if (page != null) { + operatorContext.recordGeneratedInput(page.getSizeInBytes(), page.getPositionCount()); + } + return page; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/WindowFunctionDefinition.java b/presto-main/src/main/java/com/facebook/presto/operator/WindowFunctionDefinition.java new file mode 100644 index 00000000..bbb3388a --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/WindowFunctionDefinition.java @@ -0,0 +1,65 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.operator.window.WindowFunction; +import com.facebook.presto.operator.window.WindowFunctionSupplier; +import com.facebook.presto.spi.type.Type; +import com.google.common.base.Preconditions; + +import java.util.Arrays; +import java.util.List; + +public class WindowFunctionDefinition +{ + private final WindowFunctionSupplier functionSupplier; + private final Type type; + private final List argumentChannels; + + public static WindowFunctionDefinition window(WindowFunctionSupplier functionSupplier, Type type, List inputs) + { + Preconditions.checkNotNull(functionSupplier, "functionSupplier is null"); + Preconditions.checkNotNull(type, "type is null"); + Preconditions.checkNotNull(inputs, "inputs is null"); + + return new WindowFunctionDefinition(functionSupplier, type, inputs); + } + + public static WindowFunctionDefinition window(WindowFunctionSupplier functionSupplier, Type type, Integer... inputs) + { + Preconditions.checkNotNull(functionSupplier, "functionSupplier is null"); + Preconditions.checkNotNull(type, "type is null"); + Preconditions.checkNotNull(inputs, "inputs is null"); + + return window(functionSupplier, type, Arrays.asList(inputs)); + } + + WindowFunctionDefinition(WindowFunctionSupplier functionSupplier, Type type, List argumentChannels) + { + this.functionSupplier = functionSupplier; + this.type = type; + this.argumentChannels = argumentChannels; + } + + public Type getType() + { + return type; + + } + + public WindowFunction createWindowFunction() + { + return functionSupplier.createWindowFunction(argumentChannels); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/WindowOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/WindowOperator.java new file mode 100644 index 00000000..24c7aa3a --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/WindowOperator.java @@ -0,0 +1,464 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator; + +import com.facebook.presto.operator.window.FrameInfo; +import com.facebook.presto.operator.window.WindowFunction; +import com.facebook.presto.operator.window.WindowPartition; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.SortOrder; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.primitives.Ints; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +import static com.facebook.presto.spi.block.SortOrder.ASC_NULLS_LAST; +import static com.facebook.presto.util.ImmutableCollectors.toImmutableList; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkPositionIndex; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.Iterables.concat; +import static java.util.Collections.nCopies; +import static java.util.Objects.requireNonNull; + +public class WindowOperator + implements Operator +{ + public static class WindowOperatorFactory + implements OperatorFactory + { + private final int operatorId; + private final List sourceTypes; + private final List outputChannels; + private final List windowFunctionDefinitions; + private final List partitionChannels; + private final List preGroupedChannels; + private final List sortChannels; + private final List sortOrder; + private final int preSortedChannelPrefix; + private final FrameInfo frameInfo; + private final int expectedPositions; + private final List types; + private boolean closed; + + public WindowOperatorFactory( + int operatorId, + List sourceTypes, + List outputChannels, + List windowFunctionDefinitions, + List partitionChannels, + List preGroupedChannels, + List sortChannels, + List sortOrder, + int preSortedChannelPrefix, + FrameInfo frameInfo, + int expectedPositions) + { + requireNonNull(sourceTypes, "sourceTypes is null"); + requireNonNull(outputChannels, "outputChannels is null"); + requireNonNull(windowFunctionDefinitions, "windowFunctionDefinitions is null"); + requireNonNull(partitionChannels, "partitionChannels is null"); + requireNonNull(preGroupedChannels, "preGroupedChannels is null"); + checkArgument(partitionChannels.containsAll(preGroupedChannels), "preGroupedChannels must be a subset of partitionChannels"); + requireNonNull(sortChannels, "sortChannels is null"); + requireNonNull(sortOrder, "sortOrder is null"); + checkArgument(sortChannels.size() == sortOrder.size(), "Must have same number of sort channels as sort orders"); + checkArgument(preSortedChannelPrefix <= sortChannels.size(), "Cannot have more pre-sorted channels than specified sorted channels"); + checkArgument(preSortedChannelPrefix == 0 || ImmutableSet.copyOf(preGroupedChannels).equals(ImmutableSet.copyOf(partitionChannels)), "preSortedChannelPrefix can only be greater than zero if all partition channels are pre-grouped"); + requireNonNull(frameInfo, "frameInfo is null"); + + this.operatorId = operatorId; + this.sourceTypes = ImmutableList.copyOf(sourceTypes); + this.outputChannels = ImmutableList.copyOf(outputChannels); + this.windowFunctionDefinitions = ImmutableList.copyOf(windowFunctionDefinitions); + this.partitionChannels = ImmutableList.copyOf(partitionChannels); + this.preGroupedChannels = ImmutableList.copyOf(preGroupedChannels); + this.sortChannels = ImmutableList.copyOf(sortChannels); + this.sortOrder = ImmutableList.copyOf(sortOrder); + this.preSortedChannelPrefix = preSortedChannelPrefix; + this.frameInfo = frameInfo; + this.expectedPositions = expectedPositions; + this.types = Stream.concat( + outputChannels.stream() + .map(sourceTypes::get), + windowFunctionDefinitions.stream() + .map(WindowFunctionDefinition::getType)) + .collect(toImmutableList()); + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public Operator createOperator(DriverContext driverContext) + { + checkState(!closed, "Factory is already closed"); + + OperatorContext operatorContext = driverContext.addOperatorContext(operatorId, WindowOperator.class.getSimpleName()); + return new WindowOperator( + operatorContext, + sourceTypes, + outputChannels, + windowFunctionDefinitions, + partitionChannels, + preGroupedChannels, + sortChannels, + sortOrder, + preSortedChannelPrefix, + frameInfo, + expectedPositions); + } + + @Override + public void close() + { + closed = true; + } + } + + private enum State + { + NEEDS_INPUT, + HAS_OUTPUT, + FINISHING, + FINISHED + } + + private final OperatorContext operatorContext; + private final int[] outputChannels; + private final List windowFunctions; + private final List orderChannels; + private final List ordering; + private final List types; + + private final int[] preGroupedChannels; + + private final PagesHashStrategy preGroupedPartitionHashStrategy; + private final PagesHashStrategy unGroupedPartitionHashStrategy; + private final PagesHashStrategy preSortedPartitionHashStrategy; + private final PagesHashStrategy peerGroupHashStrategy; + + private final FrameInfo frameInfo; + + private final PagesIndex pagesIndex; + + private final PageBuilder pageBuilder; + + private State state = State.NEEDS_INPUT; + + private WindowPartition partition; + + private Page pendingInput; + + public WindowOperator( + OperatorContext operatorContext, + List sourceTypes, + List outputChannels, + List windowFunctionDefinitions, + List partitionChannels, + List preGroupedChannels, + List sortChannels, + List sortOrder, + int preSortedChannelPrefix, + FrameInfo frameInfo, + int expectedPositions) + { + requireNonNull(operatorContext, "operatorContext is null"); + requireNonNull(outputChannels, "outputChannels is null"); + requireNonNull(windowFunctionDefinitions, "windowFunctionDefinitions is null"); + requireNonNull(partitionChannels, "partitionChannels is null"); + requireNonNull(preGroupedChannels, "preGroupedChannels is null"); + checkArgument(partitionChannels.containsAll(preGroupedChannels), "preGroupedChannels must be a subset of partitionChannels"); + requireNonNull(sortChannels, "sortChannels is null"); + requireNonNull(sortOrder, "sortOrder is null"); + checkArgument(sortChannels.size() == sortOrder.size(), "Must have same number of sort channels as sort orders"); + checkArgument(preSortedChannelPrefix <= sortChannels.size(), "Cannot have more pre-sorted channels than specified sorted channels"); + checkArgument(preSortedChannelPrefix == 0 || ImmutableSet.copyOf(preGroupedChannels).equals(ImmutableSet.copyOf(partitionChannels)), "preSortedChannelPrefix can only be greater than zero if all partition channels are pre-grouped"); + requireNonNull(frameInfo, "frameInfo is null"); + + this.operatorContext = operatorContext; + this.outputChannels = Ints.toArray(outputChannels); + this.windowFunctions = windowFunctionDefinitions.stream() + .map(WindowFunctionDefinition::createWindowFunction) + .collect(toImmutableList()); + this.frameInfo = frameInfo; + + this.types = Stream.concat( + outputChannels.stream() + .map(sourceTypes::get), + windowFunctions.stream() + .map(WindowFunction::getType)) + .collect(toImmutableList()); + + this.pagesIndex = new PagesIndex(sourceTypes, expectedPositions); + this.preGroupedChannels = Ints.toArray(preGroupedChannels); + this.preGroupedPartitionHashStrategy = pagesIndex.createPagesHashStrategy(preGroupedChannels, Optional.empty()); + List unGroupedPartitionChannels = partitionChannels.stream() + .filter(channel -> !preGroupedChannels.contains(channel)) + .collect(toImmutableList()); + this.unGroupedPartitionHashStrategy = pagesIndex.createPagesHashStrategy(unGroupedPartitionChannels, Optional.empty()); + List preSortedChannels = sortChannels.stream() + .limit(preSortedChannelPrefix) + .collect(toImmutableList()); + this.preSortedPartitionHashStrategy = pagesIndex.createPagesHashStrategy(preSortedChannels, Optional.empty()); + this.peerGroupHashStrategy = pagesIndex.createPagesHashStrategy(sortChannels, Optional.empty()); + + this.pageBuilder = new PageBuilder(this.types); + + if (preSortedChannelPrefix > 0) { + // This already implies that set(preGroupedChannels) == set(partitionChannels) (enforced with checkArgument) + this.orderChannels = ImmutableList.copyOf(Iterables.skip(sortChannels, preSortedChannelPrefix)); + this.ordering = ImmutableList.copyOf(Iterables.skip(sortOrder, preSortedChannelPrefix)); + } + else { + // Otherwise, we need to sort by the unGroupedPartitionChannels and all original sort channels + this.orderChannels = ImmutableList.copyOf(concat(unGroupedPartitionChannels, sortChannels)); + this.ordering = ImmutableList.copyOf(concat(nCopies(unGroupedPartitionChannels.size(), ASC_NULLS_LAST), sortOrder)); + } + } + + @Override + public OperatorContext getOperatorContext() + { + return operatorContext; + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public void finish() + { + if (state == State.FINISHING || state == State.FINISHED) { + return; + } + if (state == State.NEEDS_INPUT) { + // Since was waiting for more input, prepare what we have for output since we will not be getting any more input + sortPagesIndexIfNecessary(); + } + state = State.FINISHING; + } + + @Override + public boolean isFinished() + { + return state == State.FINISHED; + } + + @Override + public boolean needsInput() + { + return state == State.NEEDS_INPUT; + } + + @Override + public void addInput(Page page) + { + checkState(state == State.NEEDS_INPUT, "Operator can not take input at this time"); + requireNonNull(page, "page is null"); + checkState(pendingInput == null, "Operator already has pending input"); + + if (page.getPositionCount() == 0) { + return; + } + + pendingInput = page; + if (processPendingInput()) { + state = State.HAS_OUTPUT; + } + operatorContext.setMemoryReservation(pagesIndex.getEstimatedSize().toBytes()); + } + + /** + * @return true if a full group has been buffered after processing the pendingInput, false otherwise + */ + private boolean processPendingInput() + { + checkState(pendingInput != null); + pendingInput = updatePagesIndex(pendingInput); + + // If we have unused input or are finishing, then we have buffered a full group + if (pendingInput != null || state == State.FINISHING) { + sortPagesIndexIfNecessary(); + return true; + } + else { + return false; + } + } + + /** + * @return the unused section of the page, or null if fully applied. + * pagesIndex guaranteed to have at least one row after this method returns + */ + private Page updatePagesIndex(Page page) + { + checkArgument(page.getPositionCount() > 0); + + // TODO: Fix pagesHashStrategy to allow specifying channels for comparison, it currently requires us to rearrange the right side blocks in consecutive channel order + Page preGroupedPage = rearrangePage(page, preGroupedChannels); + if (pagesIndex.getPositionCount() == 0 || pagesIndex.positionEqualsRow(preGroupedPartitionHashStrategy, 0, 0, preGroupedPage.getBlocks())) { + // Find the position where the pre-grouped columns change + int groupEnd = findGroupEnd(preGroupedPage, preGroupedPartitionHashStrategy, 0); + + // Add the section of the page that contains values for the current group + pagesIndex.addPage(page.getRegion(0, groupEnd)); + + if (page.getPositionCount() - groupEnd > 0) { + // Save the remaining page, which may contain multiple partitions + return page.getRegion(groupEnd, page.getPositionCount() - groupEnd); + } + else { + // Page fully consumed + return null; + } + } + else { + // We had previous results buffered, but the new page starts with new group values + return page; + } + } + + private static Page rearrangePage(Page page, int[] channels) + { + Block[] newBlocks = new Block[channels.length]; + for (int i = 0; i < channels.length; i++) { + newBlocks[i] = page.getBlock(channels[i]); + } + return new Page(page.getPositionCount(), newBlocks); + } + + @Override + public Page getOutput() + { + if (state == State.NEEDS_INPUT || state == State.FINISHED) { + return null; + } + + Page page = extractOutput(); + operatorContext.setMemoryReservation(pagesIndex.getEstimatedSize().toBytes()); + return page; + } + + private Page extractOutput() + { + // INVARIANT: pagesIndex contains the full grouped & sorted data for one or more partitions + + // Iterate through the positions sequentially until we have one full page + while (!pageBuilder.isFull()) { + if (partition == null || !partition.hasNext()) { + int partitionStart = partition == null ? 0 : partition.getPartitionEnd(); + + if (partitionStart >= pagesIndex.getPositionCount()) { + // Finished all of the partitions in the current pagesIndex + partition = null; + pagesIndex.clear(); + + // Try to extract more partitions from the pendingInput + if (pendingInput != null && processPendingInput()) { + partitionStart = 0; + } + else if (state == State.FINISHING) { + state = State.FINISHED; + // Output the remaining page if we have anything buffered + if (!pageBuilder.isEmpty()) { + Page page = pageBuilder.build(); + pageBuilder.reset(); + return page; + } + return null; + } + else { + state = State.NEEDS_INPUT; + return null; + } + } + + int partitionEnd = findGroupEnd(pagesIndex, unGroupedPartitionHashStrategy, partitionStart); + partition = new WindowPartition(pagesIndex, partitionStart, partitionEnd, outputChannels, windowFunctions, frameInfo, peerGroupHashStrategy); + } + + partition.processNextRow(pageBuilder); + } + + Page page = pageBuilder.build(); + pageBuilder.reset(); + return page; + } + + private void sortPagesIndexIfNecessary() + { + if (pagesIndex.getPositionCount() > 1 && !orderChannels.isEmpty()) { + int startPosition = 0; + while (startPosition < pagesIndex.getPositionCount()) { + int endPosition = findGroupEnd(pagesIndex, preSortedPartitionHashStrategy, startPosition); + pagesIndex.sort(orderChannels, ordering, startPosition, endPosition); + startPosition = endPosition; + } + } + } + + // Assumes input grouped on relevant pagesHashStrategy columns + private static int findGroupEnd(Page page, PagesHashStrategy pagesHashStrategy, int startPosition) + { + checkArgument(page.getPositionCount() > 0, "Must have at least one position"); + checkPositionIndex(startPosition, page.getPositionCount(), "startPosition out of bounds"); + + // Short circuit if the whole page has the same value + if (pagesHashStrategy.rowEqualsRow(startPosition, page.getBlocks(), page.getPositionCount() - 1, page.getBlocks())) { + return page.getPositionCount(); + } + + // TODO: do position binary search + int endPosition = startPosition + 1; + while (endPosition < page.getPositionCount() && + pagesHashStrategy.rowEqualsRow(endPosition - 1, page.getBlocks(), endPosition, page.getBlocks())) { + endPosition++; + } + return endPosition; + } + + // Assumes input grouped on relevant pagesHashStrategy columns + private static int findGroupEnd(PagesIndex pagesIndex, PagesHashStrategy pagesHashStrategy, int startPosition) + { + checkArgument(pagesIndex.getPositionCount() > 0, "Must have at least one position"); + checkPositionIndex(startPosition, pagesIndex.getPositionCount(), "startPosition out of bounds"); + + // Short circuit if the whole page has the same value + if (pagesIndex.positionEqualsPosition(pagesHashStrategy, startPosition, pagesIndex.getPositionCount() - 1)) { + return pagesIndex.getPositionCount(); + } + + // TODO: do position binary search + int endPosition = startPosition + 1; + while ((endPosition < pagesIndex.getPositionCount()) && + pagesIndex.positionEqualsPosition(pagesHashStrategy, endPosition - 1, endPosition)) { + endPosition++; + } + return endPosition; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/AbstractMinMaxAggregation.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/AbstractMinMaxAggregation.java new file mode 100644 index 00000000..6ab7eafb --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/AbstractMinMaxAggregation.java @@ -0,0 +1,236 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.facebook.presto.byteCode.DynamicClassLoader; +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.OperatorType; +import com.facebook.presto.metadata.ParametricAggregation; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.operator.aggregation.state.AccumulatorState; +import com.facebook.presto.operator.aggregation.state.AccumulatorStateFactory; +import com.facebook.presto.operator.aggregation.state.AccumulatorStateSerializer; +import com.facebook.presto.operator.aggregation.state.NullableBooleanState; +import com.facebook.presto.operator.aggregation.state.NullableBooleanStateSerializer; +import com.facebook.presto.operator.aggregation.state.NullableDoubleState; +import com.facebook.presto.operator.aggregation.state.NullableDoubleStateSerializer; +import com.facebook.presto.operator.aggregation.state.NullableLongState; +import com.facebook.presto.operator.aggregation.state.NullableLongStateSerializer; +import com.facebook.presto.operator.aggregation.state.SliceState; +import com.facebook.presto.operator.aggregation.state.SliceStateSerializer; +import com.facebook.presto.operator.aggregation.state.StateCompiler; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.StandardErrorCode; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; + +import java.lang.invoke.MethodHandle; +import java.util.List; +import java.util.Map; + +import static com.facebook.presto.metadata.Signature.orderableTypeParameter; +import static com.facebook.presto.operator.aggregation.AggregationMetadata.ParameterMetadata; +import static com.facebook.presto.operator.aggregation.AggregationMetadata.ParameterMetadata.ParameterType.INPUT_CHANNEL; +import static com.facebook.presto.operator.aggregation.AggregationMetadata.ParameterMetadata.ParameterType.STATE; +import static com.facebook.presto.operator.aggregation.AggregationUtils.generateAggregationName; +import static com.facebook.presto.spi.StandardErrorCode.INTERNAL_ERROR; +import static com.facebook.presto.util.Reflection.methodHandle; +import static com.google.common.base.Preconditions.checkNotNull; + +public abstract class AbstractMinMaxAggregation + extends ParametricAggregation +{ + private static final MethodHandle LONG_INPUT_FUNCTION = methodHandle(AbstractMinMaxAggregation.class, "input", MethodHandle.class, NullableLongState.class, long.class); + private static final MethodHandle DOUBLE_INPUT_FUNCTION = methodHandle(AbstractMinMaxAggregation.class, "input", MethodHandle.class, NullableDoubleState.class, double.class); + private static final MethodHandle SLICE_INPUT_FUNCTION = methodHandle(AbstractMinMaxAggregation.class, "input", MethodHandle.class, SliceState.class, Slice.class); + private static final MethodHandle BOOLEAN_INPUT_FUNCTION = methodHandle(AbstractMinMaxAggregation.class, "input", MethodHandle.class, NullableBooleanState.class, boolean.class); + + private final String name; + private final OperatorType operatorType; + private final Signature signature; + + private final StateCompiler compiler = new StateCompiler(); + + protected AbstractMinMaxAggregation(String name, OperatorType operatorType) + { + checkNotNull(name); + checkNotNull(operatorType); + this.name = name; + this.operatorType = operatorType; + this.signature = new Signature(name, ImmutableList.of(orderableTypeParameter("E")), "E", ImmutableList.of("E"), false, false); + } + + @Override + public Signature getSignature() + { + return signature; + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + Type type = types.get("E"); + MethodHandle compareMethodHandle = functionRegistry.resolveOperator(operatorType, ImmutableList.of(type, type)).getMethodHandle(); + Signature signature = new Signature(name, type.getTypeSignature(), type.getTypeSignature()); + InternalAggregationFunction aggregation = generateAggregation(type, compareMethodHandle); + return new FunctionInfo(signature, getDescription(), aggregation); + } + + protected InternalAggregationFunction generateAggregation(Type type, MethodHandle compareMethodHandle) + { + DynamicClassLoader classLoader = new DynamicClassLoader(AbstractMinMaxAggregation.class.getClassLoader()); + + List inputTypes = ImmutableList.of(type); + + AccumulatorStateSerializer stateSerializer; + AccumulatorStateFactory stateFactory; + MethodHandle inputFunction; + Class stateInterface; + + if (type.getJavaType() == long.class) { + stateFactory = compiler.generateStateFactory(NullableLongState.class, classLoader); + stateSerializer = new NullableLongStateSerializer(type); + stateInterface = NullableLongState.class; + inputFunction = LONG_INPUT_FUNCTION; + } + else if (type.getJavaType() == double.class) { + stateFactory = compiler.generateStateFactory(NullableDoubleState.class, classLoader); + stateSerializer = new NullableDoubleStateSerializer(type); + stateInterface = NullableDoubleState.class; + inputFunction = DOUBLE_INPUT_FUNCTION; + } + else if (type.getJavaType() == Slice.class) { + stateFactory = compiler.generateStateFactory(SliceState.class, classLoader); + stateSerializer = new SliceStateSerializer(type); + stateInterface = SliceState.class; + inputFunction = SLICE_INPUT_FUNCTION; + } + else if (type.getJavaType() == boolean.class) { + stateFactory = compiler.generateStateFactory(NullableBooleanState.class, classLoader); + stateSerializer = new NullableBooleanStateSerializer(type); + stateInterface = NullableBooleanState.class; + inputFunction = BOOLEAN_INPUT_FUNCTION; + } + else { + throw new PrestoException(StandardErrorCode.INVALID_FUNCTION_ARGUMENT, "Argument type to max/min unsupported"); + } + + inputFunction = inputFunction.bindTo(compareMethodHandle); + + Type intermediateType = stateSerializer.getSerializedType(); + List inputParameterMetadata = createInputParameterMetadata(type); + AggregationMetadata metadata = new AggregationMetadata( + generateAggregationName(name, type, inputTypes), + inputParameterMetadata, + inputFunction, + inputParameterMetadata, + inputFunction, + null, + null, + stateInterface, + stateSerializer, + stateFactory, + type, + false); + + GenericAccumulatorFactoryBinder factory = new AccumulatorCompiler().generateAccumulatorFactoryBinder(metadata, classLoader); + return new InternalAggregationFunction(name, inputTypes, intermediateType, type, true, false, factory); + } + + private static List createInputParameterMetadata(Type type) + { + return ImmutableList.of( + new ParameterMetadata(STATE), + new ParameterMetadata(INPUT_CHANNEL, type)); + } + + public static void input(MethodHandle methodHandle, NullableDoubleState state, double value) + { + if (state.isNull()) { + state.setNull(false); + state.setDouble(value); + return; + } + try { + if ((boolean) methodHandle.invokeExact(value, state.getDouble())) { + state.setDouble(value); + } + } + catch (Throwable t) { + Throwables.propagateIfInstanceOf(t, Error.class); + Throwables.propagateIfInstanceOf(t, PrestoException.class); + throw new PrestoException(INTERNAL_ERROR, t); + } + } + + public static void input(MethodHandle methodHandle, NullableLongState state, long value) + { + if (state.isNull()) { + state.setNull(false); + state.setLong(value); + return; + } + try { + if ((boolean) methodHandle.invokeExact(value, state.getLong())) { + state.setLong(value); + } + } + catch (Throwable t) { + Throwables.propagateIfInstanceOf(t, Error.class); + Throwables.propagateIfInstanceOf(t, PrestoException.class); + throw new PrestoException(INTERNAL_ERROR, t); + } + } + + public static void input(MethodHandle methodHandle, SliceState state, Slice value) + { + if (state.getSlice() == null) { + state.setSlice(value); + return; + } + try { + if ((boolean) methodHandle.invokeExact(value, state.getSlice())) { + state.setSlice(value); + } + } + catch (Throwable t) { + Throwables.propagateIfInstanceOf(t, Error.class); + Throwables.propagateIfInstanceOf(t, PrestoException.class); + throw new PrestoException(INTERNAL_ERROR, t); + } + } + + public static void input(MethodHandle methodHandle, NullableBooleanState state, boolean value) + { + if (state.isNull()) { + state.setNull(false); + state.setBoolean(value); + return; + } + try { + if ((boolean) methodHandle.invokeExact(value, state.getBoolean())) { + state.setBoolean(value); + } + } + catch (Throwable t) { + Throwables.propagateIfInstanceOf(t, Error.class); + Throwables.propagateIfInstanceOf(t, PrestoException.class); + throw new PrestoException(INTERNAL_ERROR, t); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/Accumulator.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/Accumulator.java new file mode 100644 index 00000000..188189fe --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/Accumulator.java @@ -0,0 +1,36 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; + +public interface Accumulator +{ + long getEstimatedSize(); + + Type getFinalType(); + + Type getIntermediateType(); + + void addInput(Page page); + + void addIntermediate(Block block); + + void evaluateIntermediate(BlockBuilder blockBuilder); + + void evaluateFinal(BlockBuilder blockBuilder); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/AccumulatorCompiler.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/AccumulatorCompiler.java new file mode 100644 index 00000000..016c59af --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/AccumulatorCompiler.java @@ -0,0 +1,789 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.facebook.presto.byteCode.Block; +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.ClassDefinition; +import com.facebook.presto.byteCode.DynamicClassLoader; +import com.facebook.presto.byteCode.FieldDefinition; +import com.facebook.presto.byteCode.MethodDefinition; +import com.facebook.presto.byteCode.Parameter; +import com.facebook.presto.byteCode.Scope; +import com.facebook.presto.byteCode.Variable; +import com.facebook.presto.byteCode.control.ForLoop; +import com.facebook.presto.byteCode.control.IfStatement; +import com.facebook.presto.byteCode.expression.ByteCodeExpression; +import com.facebook.presto.operator.GroupByIdBlock; +import com.facebook.presto.operator.aggregation.state.AccumulatorStateFactory; +import com.facebook.presto.operator.aggregation.state.AccumulatorStateSerializer; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.gen.CallSiteBinder; +import com.facebook.presto.sql.gen.CompilerOperations; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; + +import javax.annotation.Nullable; + +import java.lang.invoke.MethodHandle; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; + +import static com.facebook.presto.byteCode.Access.FINAL; +import static com.facebook.presto.byteCode.Access.PRIVATE; +import static com.facebook.presto.byteCode.Access.PUBLIC; +import static com.facebook.presto.byteCode.Access.a; +import static com.facebook.presto.byteCode.OpCode.NOP; +import static com.facebook.presto.byteCode.Parameter.arg; +import static com.facebook.presto.byteCode.ParameterizedType.type; +import static com.facebook.presto.byteCode.expression.ByteCodeExpressions.constantInt; +import static com.facebook.presto.byteCode.expression.ByteCodeExpressions.constantString; +import static com.facebook.presto.byteCode.expression.ByteCodeExpressions.invokeStatic; +import static com.facebook.presto.byteCode.expression.ByteCodeExpressions.not; +import static com.facebook.presto.operator.aggregation.AggregationMetadata.ParameterMetadata; +import static com.facebook.presto.operator.aggregation.AggregationMetadata.countInputChannels; +import static com.facebook.presto.sql.gen.ByteCodeUtils.invoke; +import static com.facebook.presto.sql.gen.CompilerUtils.defineClass; +import static com.facebook.presto.sql.gen.CompilerUtils.makeClassName; +import static com.facebook.presto.sql.gen.SqlTypeByteCodeExpression.constantType; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +public class AccumulatorCompiler +{ + public GenericAccumulatorFactoryBinder generateAccumulatorFactoryBinder(AggregationMetadata metadata, DynamicClassLoader classLoader) + { + Class accumulatorClass = generateAccumulatorClass( + Accumulator.class, + metadata, + classLoader); + + Class groupedAccumulatorClass = generateAccumulatorClass( + GroupedAccumulator.class, + metadata, + classLoader); + + return new GenericAccumulatorFactoryBinder( + metadata.getStateSerializer(), + metadata.getStateFactory(), + accumulatorClass, + groupedAccumulatorClass, + metadata.isApproximate()); + } + + private static Class generateAccumulatorClass( + Class accumulatorInterface, + AggregationMetadata metadata, + DynamicClassLoader classLoader) + { + boolean grouped = accumulatorInterface == GroupedAccumulator.class; + boolean approximate = metadata.isApproximate(); + + ClassDefinition definition = new ClassDefinition( + a(PUBLIC, FINAL), + makeClassName(metadata.getName() + accumulatorInterface.getSimpleName()), + type(Object.class), + type(accumulatorInterface)); + + CallSiteBinder callSiteBinder = new CallSiteBinder(); + + AccumulatorStateSerializer stateSerializer = metadata.getStateSerializer(); + AccumulatorStateFactory stateFactory = metadata.getStateFactory(); + + FieldDefinition stateSerializerField = definition.declareField(a(PRIVATE, FINAL), "stateSerializer", AccumulatorStateSerializer.class); + FieldDefinition stateFactoryField = definition.declareField(a(PRIVATE, FINAL), "stateFactory", AccumulatorStateFactory.class); + FieldDefinition inputChannelsField = definition.declareField(a(PRIVATE, FINAL), "inputChannels", type(List.class, Integer.class)); + FieldDefinition maskChannelField = definition.declareField(a(PRIVATE, FINAL), "maskChannel", type(Optional.class, Integer.class)); + FieldDefinition sampleWeightChannelField = null; + FieldDefinition confidenceField = null; + if (approximate) { + sampleWeightChannelField = definition.declareField(a(PRIVATE, FINAL), "sampleWeightChannel", type(Optional.class, Integer.class)); + confidenceField = definition.declareField(a(PRIVATE, FINAL), "confidence", double.class); + } + FieldDefinition stateField = definition.declareField(a(PRIVATE, FINAL), "state", grouped ? stateFactory.getGroupedStateClass() : stateFactory.getSingleStateClass()); + + // Generate constructor + generateConstructor( + definition, + stateSerializerField, + stateFactoryField, + inputChannelsField, + maskChannelField, + sampleWeightChannelField, + confidenceField, + stateField, + grouped); + + // Generate methods + generateAddInput(definition, stateField, inputChannelsField, maskChannelField, sampleWeightChannelField, metadata.getInputMetadata(), metadata.getInputFunction(), callSiteBinder, grouped); + generateGetEstimatedSize(definition, stateField); + generateGetIntermediateType(definition, callSiteBinder, stateSerializer.getSerializedType()); + generateGetFinalType(definition, callSiteBinder, metadata.getOutputType()); + + if (metadata.getIntermediateInputFunction() == null) { + generateAddIntermediateAsCombine(definition, stateField, stateSerializerField, stateFactoryField, metadata.getCombineFunction(), stateFactory.getSingleStateClass(), callSiteBinder, grouped); + } + else { + generateAddIntermediateAsIntermediateInput(definition, stateField, metadata.getIntermediateInputMetadata(), metadata.getIntermediateInputFunction(), callSiteBinder, grouped); + } + + if (grouped) { + generateGroupedEvaluateIntermediate(definition, stateSerializerField, stateField); + } + else { + generateEvaluateIntermediate(definition, stateSerializerField, stateField); + } + + if (grouped) { + generateGroupedEvaluateFinal(definition, confidenceField, stateSerializerField, stateField, metadata.getOutputFunction(), metadata.isApproximate(), callSiteBinder); + } + else { + generateEvaluateFinal(definition, confidenceField, stateSerializerField, stateField, metadata.getOutputFunction(), metadata.isApproximate(), callSiteBinder); + } + + return defineClass(definition, accumulatorInterface, callSiteBinder.getBindings(), classLoader); + } + + private static MethodDefinition generateGetIntermediateType(ClassDefinition definition, CallSiteBinder callSiteBinder, Type type) + { + MethodDefinition methodDefinition = definition.declareMethod(a(PUBLIC), "getIntermediateType", type(Type.class)); + + methodDefinition.getBody() + .append(constantType(callSiteBinder, type)) + .retObject(); + + return methodDefinition; + } + + private static MethodDefinition generateGetFinalType(ClassDefinition definition, CallSiteBinder callSiteBinder, Type type) + { + MethodDefinition methodDefinition = definition.declareMethod(a(PUBLIC), "getFinalType", type(Type.class)); + + methodDefinition.getBody() + .append(constantType(callSiteBinder, type)) + .retObject(); + + return methodDefinition; + } + + private static void generateGetEstimatedSize(ClassDefinition definition, FieldDefinition stateField) + { + MethodDefinition method = definition.declareMethod(a(PUBLIC), "getEstimatedSize", type(long.class)); + ByteCodeExpression state = method.getThis().getField(stateField); + method.getBody() + .append(state.invoke("getEstimatedSize", long.class).ret()); + } + + private static void generateAddInput( + ClassDefinition definition, + FieldDefinition stateField, + FieldDefinition inputChannelsField, + FieldDefinition maskChannelField, + @Nullable FieldDefinition sampleWeightChannelField, + List parameterMetadatas, + MethodHandle inputFunction, + CallSiteBinder callSiteBinder, + boolean grouped) + { + ImmutableList.Builder parameters = ImmutableList.builder(); + if (grouped) { + parameters.add(arg("groupIdsBlock", GroupByIdBlock.class)); + } + Parameter page = arg("page", Page.class); + parameters.add(page); + + MethodDefinition method = definition.declareMethod(a(PUBLIC), "addInput", type(void.class), parameters.build()); + Scope scope = method.getScope(); + Block body = method.getBody(); + Variable thisVariable = method.getThis(); + + if (grouped) { + generateEnsureCapacity(scope, stateField, body); + } + + List parameterVariables = new ArrayList<>(); + for (int i = 0; i < countInputChannels(parameterMetadatas); i++) { + parameterVariables.add(scope.declareVariable(com.facebook.presto.spi.block.Block.class, "block" + i)); + } + Variable masksBlock = scope.declareVariable(com.facebook.presto.spi.block.Block.class, "masksBlock"); + Variable sampleWeightsBlock = null; + if (sampleWeightChannelField != null) { + sampleWeightsBlock = scope.declareVariable(com.facebook.presto.spi.block.Block.class, "sampleWeightsBlock"); + } + body.comment("masksBlock = maskChannel.map(page.blockGetter()).orElse(null);") + .append(thisVariable.getField(maskChannelField)) + .append(page) + .invokeStatic(type(AggregationUtils.class), "pageBlockGetter", type(Function.class, Integer.class, com.facebook.presto.spi.block.Block.class), type(Page.class)) + .invokeVirtual(Optional.class, "map", Optional.class, Function.class) + .pushNull() + .invokeVirtual(Optional.class, "orElse", Object.class, Object.class) + .checkCast(com.facebook.presto.spi.block.Block.class) + .putVariable(masksBlock); + + if (sampleWeightChannelField != null) { + body.comment("sampleWeightsBlock = sampleWeightChannel.map(page.blockGetter()).get();") + .append(thisVariable.getField(sampleWeightChannelField)) + .append(page) + .invokeStatic(type(AggregationUtils.class), "pageBlockGetter", type(Function.class, Integer.class, com.facebook.presto.spi.block.Block.class), type(Page.class)) + .invokeVirtual(Optional.class, "map", Optional.class, Function.class) + .invokeVirtual(Optional.class, "get", Object.class) + .checkCast(com.facebook.presto.spi.block.Block.class) + .putVariable(sampleWeightsBlock); + } + + // Get all parameter blocks + for (int i = 0; i < countInputChannels(parameterMetadatas); i++) { + body.comment("%s = page.getBlock(inputChannels.get(%d));", parameterVariables.get(i).getName(), i) + .append(page) + .append(thisVariable.getField(inputChannelsField)) + .push(i) + .invokeInterface(List.class, "get", Object.class, int.class) + .checkCast(Integer.class) + .invokeVirtual(Integer.class, "intValue", int.class) + .invokeVirtual(Page.class, "getBlock", com.facebook.presto.spi.block.Block.class, int.class) + .putVariable(parameterVariables.get(i)); + } + Block block = generateInputForLoop(stateField, parameterMetadatas, inputFunction, scope, parameterVariables, masksBlock, sampleWeightsBlock, callSiteBinder, grouped); + + body.append(block); + body.ret(); + } + + private static Block generateInputForLoop( + FieldDefinition stateField, + List parameterMetadatas, + MethodHandle inputFunction, + Scope scope, + List parameterVariables, + Variable masksBlock, + @Nullable Variable sampleWeightsBlock, + CallSiteBinder callSiteBinder, + boolean grouped) + { + // For-loop over rows + Variable page = scope.getVariable("page"); + Variable positionVariable = scope.declareVariable(int.class, "position"); + Variable sampleWeightVariable = null; + if (sampleWeightsBlock != null) { + sampleWeightVariable = scope.declareVariable(long.class, "sampleWeight"); + } + Variable rowsVariable = scope.declareVariable(int.class, "rows"); + + Block block = new Block() + .append(page) + .invokeVirtual(Page.class, "getPositionCount", int.class) + .putVariable(rowsVariable) + .initializeVariable(positionVariable); + if (sampleWeightVariable != null) { + block.initializeVariable(sampleWeightVariable); + } + + ByteCodeNode loopBody = generateInvokeInputFunction(scope, stateField, positionVariable, sampleWeightVariable, parameterVariables, parameterMetadatas, inputFunction, callSiteBinder, grouped); + + // Wrap with null checks + List nullable = new ArrayList<>(); + for (ParameterMetadata metadata : parameterMetadatas) { + switch (metadata.getParameterType()) { + case INPUT_CHANNEL: + case BLOCK_INPUT_CHANNEL: + nullable.add(false); + break; + case NULLABLE_BLOCK_INPUT_CHANNEL: + nullable.add(true); + break; + default: // do nothing + } + } + checkState(nullable.size() == parameterVariables.size(), "Number of parameters does not match"); + for (int i = 0; i < parameterVariables.size(); i++) { + if (!nullable.get(i)) { + Variable variableDefinition = parameterVariables.get(i); + loopBody = new IfStatement("if(!%s.isNull(position))", variableDefinition.getName()) + .condition(new Block() + .getVariable(variableDefinition) + .getVariable(positionVariable) + .invokeInterface(com.facebook.presto.spi.block.Block.class, "isNull", boolean.class, int.class)) + .ifFalse(loopBody); + } + } + + // Check that sample weight is > 0 (also checks the mask) + if (sampleWeightVariable != null) { + loopBody = generateComputeSampleWeightAndCheckGreaterThanZero(loopBody, sampleWeightVariable, masksBlock, sampleWeightsBlock, positionVariable); + } + // Otherwise just check the mask + else { + loopBody = new IfStatement("if(testMask(%s, position))", masksBlock.getName()) + .condition(new Block() + .getVariable(masksBlock) + .getVariable(positionVariable) + .invokeStatic(CompilerOperations.class, "testMask", boolean.class, com.facebook.presto.spi.block.Block.class, int.class)) + .ifTrue(loopBody); + } + + block.append(new ForLoop() + .initialize(new Block().putVariable(positionVariable, 0)) + .condition(new Block() + .getVariable(positionVariable) + .getVariable(rowsVariable) + .invokeStatic(CompilerOperations.class, "lessThan", boolean.class, int.class, int.class)) + .update(new Block().incrementVariable(positionVariable, (byte) 1)) + .body(loopBody)); + + return block; + } + + private static ByteCodeNode generateComputeSampleWeightAndCheckGreaterThanZero(ByteCodeNode body, Variable sampleWeight, Variable masks, Variable sampleWeights, Variable position) + { + Block block = new Block() + .comment("sampleWeight = computeSampleWeight(masks, sampleWeights, position);") + .getVariable(masks) + .getVariable(sampleWeights) + .getVariable(position) + .invokeStatic(ApproximateUtils.class, "computeSampleWeight", long.class, com.facebook.presto.spi.block.Block.class, com.facebook.presto.spi.block.Block.class, int.class) + .putVariable(sampleWeight); + + block.append(new IfStatement("if(sampleWeight > 0)") + .condition(new Block() + .getVariable(sampleWeight) + .invokeStatic(CompilerOperations.class, "longGreaterThanZero", boolean.class, long.class)) + .ifTrue(body) + .ifFalse(NOP)); + + return block; + } + + private static Block generateInvokeInputFunction( + Scope scope, + FieldDefinition stateField, + Variable position, + @Nullable Variable sampleWeight, + List parameterVariables, + List parameterMetadatas, + MethodHandle inputFunction, + CallSiteBinder callSiteBinder, + boolean grouped) + { + Block block = new Block(); + + if (grouped) { + generateSetGroupIdFromGroupIdsBlock(scope, stateField, block); + } + + block.comment("Call input function with unpacked Block arguments"); + + Class[] parameters = inputFunction.type().parameterArray(); + int inputChannel = 0; + for (int i = 0; i < parameters.length; i++) { + ParameterMetadata parameterMetadata = parameterMetadatas.get(i); + switch (parameterMetadata.getParameterType()) { + case STATE: + block.append(scope.getThis().getField(stateField)); + break; + case BLOCK_INDEX: + block.getVariable(position); + break; + case SAMPLE_WEIGHT: + checkNotNull(sampleWeight, "sampleWeight is null"); + block.getVariable(sampleWeight); + break; + case BLOCK_INPUT_CHANNEL: + case NULLABLE_BLOCK_INPUT_CHANNEL: + block.getVariable(parameterVariables.get(inputChannel)); + inputChannel++; + break; + case INPUT_CHANNEL: + Block getBlockByteCode = new Block() + .getVariable(parameterVariables.get(inputChannel)); + pushStackType(scope, block, parameterMetadata.getSqlType(), getBlockByteCode, parameters[i], callSiteBinder); + inputChannel++; + break; + default: + throw new IllegalArgumentException("Unsupported parameter type: " + parameterMetadata.getParameterType()); + } + } + + block.append(invoke(callSiteBinder.bind(inputFunction), "input")); + return block; + } + + // Assumes that there is a variable named 'position' in the block, which is the current index + private static void pushStackType(Scope scope, Block block, Type sqlType, Block getBlockByteCode, Class parameter, CallSiteBinder callSiteBinder) + { + Variable position = scope.getVariable("position"); + if (parameter == long.class) { + block.comment("%s.getLong(block, position)", sqlType.getTypeSignature()) + .append(constantType(callSiteBinder, sqlType)) + .append(getBlockByteCode) + .append(position) + .invokeInterface(Type.class, "getLong", long.class, com.facebook.presto.spi.block.Block.class, int.class); + } + else if (parameter == double.class) { + block.comment("%s.getDouble(block, position)", sqlType.getTypeSignature()) + .append(constantType(callSiteBinder, sqlType)) + .append(getBlockByteCode) + .append(position) + .invokeInterface(Type.class, "getDouble", double.class, com.facebook.presto.spi.block.Block.class, int.class); + } + else if (parameter == boolean.class) { + block.comment("%s.getBoolean(block, position)", sqlType.getTypeSignature()) + .append(constantType(callSiteBinder, sqlType)) + .append(getBlockByteCode) + .append(position) + .invokeInterface(Type.class, "getBoolean", boolean.class, com.facebook.presto.spi.block.Block.class, int.class); + } + else if (parameter == Slice.class) { + block.comment("%s.getBoolean(block, position)", sqlType.getTypeSignature()) + .append(constantType(callSiteBinder, sqlType)) + .append(getBlockByteCode) + .append(position) + .invokeInterface(Type.class, "getSlice", Slice.class, com.facebook.presto.spi.block.Block.class, int.class); + } + else { + throw new IllegalArgumentException("Unsupported parameter type: " + parameter.getSimpleName()); + } + } + + private static void generateAddIntermediateAsCombine( + ClassDefinition definition, + FieldDefinition stateField, + FieldDefinition stateSerializerField, + FieldDefinition stateFactoryField, + MethodHandle combineFunction, + Class singleStateClass, + CallSiteBinder callSiteBinder, + boolean grouped) + { + MethodDefinition method = declareAddIntermediate(definition, grouped); + Scope scope = method.getScope(); + Block body = method.getBody(); + Variable thisVariable = method.getThis(); + + Variable block = scope.getVariable("block"); + Variable scratchState = scope.declareVariable(singleStateClass, "scratchState"); + Variable position = scope.declareVariable(int.class, "position"); + + body.comment("scratchState = stateFactory.createSingleState();") + .append(thisVariable.getField(stateFactoryField)) + .invokeInterface(AccumulatorStateFactory.class, "createSingleState", Object.class) + .checkCast(scratchState.getType()) + .putVariable(scratchState); + + if (grouped) { + generateEnsureCapacity(scope, stateField, body); + } + + Block loopBody = new Block(); + + if (grouped) { + Variable groupIdsBlock = scope.getVariable("groupIdsBlock"); + loopBody.append(thisVariable.getField(stateField).invoke("setGroupId", void.class, groupIdsBlock.invoke("getGroupId", long.class, position))); + } + + loopBody.append(thisVariable.getField(stateSerializerField).invoke("deserialize", void.class, block, position, scratchState.cast(Object.class))); + + loopBody.comment("combine(state, scratchState)") + .append(thisVariable.getField(stateField)) + .append(scratchState) + .append(invoke(callSiteBinder.bind(combineFunction), "combine")); + + if (grouped) { + // skip rows with null group id + IfStatement ifStatement = new IfStatement("if (!groupIdsBlock.isNull(position))") + .condition(not(scope.getVariable("groupIdsBlock").invoke("isNull", boolean.class, position))) + .ifTrue(loopBody); + + loopBody = new Block().append(ifStatement); + } + + body.append(generateBlockNonNullPositionForLoop(scope, position, loopBody)) + .ret(); + } + + private static void generateSetGroupIdFromGroupIdsBlock(Scope scope, FieldDefinition stateField, Block block) + { + Variable groupIdsBlock = scope.getVariable("groupIdsBlock"); + Variable position = scope.getVariable("position"); + ByteCodeExpression state = scope.getThis().getField(stateField); + block.append(state.invoke("setGroupId", void.class, groupIdsBlock.invoke("getGroupId", long.class, position))); + } + + private static void generateEnsureCapacity(Scope scope, FieldDefinition stateField, Block block) + { + Variable groupIdsBlock = scope.getVariable("groupIdsBlock"); + ByteCodeExpression state = scope.getThis().getField(stateField); + block.append(state.invoke("ensureCapacity", void.class, groupIdsBlock.invoke("getGroupCount", long.class))); + } + + private static MethodDefinition declareAddIntermediate(ClassDefinition definition, boolean grouped) + { + ImmutableList.Builder parameters = ImmutableList.builder(); + if (grouped) { + parameters.add(arg("groupIdsBlock", GroupByIdBlock.class)); + } + parameters.add(arg("block", com.facebook.presto.spi.block.Block.class)); + + return definition.declareMethod( + a(PUBLIC), + "addIntermediate", + type(void.class), + parameters.build()); + } + + private static void generateAddIntermediateAsIntermediateInput( + ClassDefinition definition, + FieldDefinition stateField, + List parameterMetadatas, + MethodHandle intermediateInputFunction, + CallSiteBinder callSiteBinder, + boolean grouped) + { + MethodDefinition method = declareAddIntermediate(definition, grouped); + Scope scope = method.getScope(); + Block body = method.getBody(); + + if (grouped) { + generateEnsureCapacity(scope, stateField, body); + } + + Variable positionVariable = scope.declareVariable(int.class, "position"); + + Block loopBody = generateInvokeInputFunction(scope, stateField, positionVariable, null, ImmutableList.of(scope.getVariable("block")), parameterMetadatas, intermediateInputFunction, callSiteBinder, grouped); + + if (grouped) { + // skip rows with null group id + IfStatement ifStatement = new IfStatement("if (!groupIdsBlock.isNull(position))") + .condition(not(scope.getVariable("groupIdsBlock").invoke("isNull", boolean.class, positionVariable))) + .ifTrue(loopBody); + + loopBody = new Block().append(ifStatement); + } + + body.append(generateBlockNonNullPositionForLoop(scope, positionVariable, loopBody)) + .ret(); + } + + // Generates a for-loop with a local variable named "position" defined, with the current position in the block, + // loopBody will only be executed for non-null positions in the Block + private static Block generateBlockNonNullPositionForLoop(Scope scope, Variable positionVariable, Block loopBody) + { + Variable rowsVariable = scope.declareVariable(int.class, "rows"); + Variable blockVariable = scope.getVariable("block"); + + Block block = new Block() + .append(blockVariable) + .invokeInterface(com.facebook.presto.spi.block.Block.class, "getPositionCount", int.class) + .putVariable(rowsVariable); + + IfStatement ifStatement = new IfStatement("if(!block.isNull(position))") + .condition(new Block() + .append(blockVariable) + .append(positionVariable) + .invokeInterface(com.facebook.presto.spi.block.Block.class, "isNull", boolean.class, int.class)) + .ifFalse(loopBody); + + block.append(new ForLoop() + .initialize(positionVariable.set(constantInt(0))) + .condition(new Block() + .append(positionVariable) + .append(rowsVariable) + .invokeStatic(CompilerOperations.class, "lessThan", boolean.class, int.class, int.class)) + .update(new Block().incrementVariable(positionVariable, (byte) 1)) + .body(ifStatement)); + + return block; + } + + private static void generateGroupedEvaluateIntermediate(ClassDefinition definition, FieldDefinition stateSerializerField, FieldDefinition stateField) + { + Parameter groupId = arg("groupId", int.class); + Parameter out = arg("out", BlockBuilder.class); + MethodDefinition method = definition.declareMethod(a(PUBLIC), "evaluateIntermediate", type(void.class), groupId, out); + + Variable thisVariable = method.getThis(); + ByteCodeExpression state = thisVariable.getField(stateField); + ByteCodeExpression stateSerializer = thisVariable.getField(stateSerializerField); + + method.getBody() + .append(state.invoke("setGroupId", void.class, groupId.cast(long.class))) + .append(stateSerializer.invoke("serialize", void.class, state.cast(Object.class), out)) + .ret(); + } + + private static void generateEvaluateIntermediate(ClassDefinition definition, FieldDefinition stateSerializerField, FieldDefinition stateField) + { + Parameter out = arg("out", BlockBuilder.class); + MethodDefinition method = definition.declareMethod( + a(PUBLIC), + "evaluateIntermediate", + type(void.class), + out); + + Variable thisVariable = method.getThis(); + ByteCodeExpression stateSerializer = thisVariable.getField(stateSerializerField); + ByteCodeExpression state = thisVariable.getField(stateField); + + method.getBody() + .append(stateSerializer.invoke("serialize", void.class, state.cast(Object.class), out)) + .ret(); + } + + private static void generateGroupedEvaluateFinal( + ClassDefinition definition, + FieldDefinition confidenceField, + FieldDefinition stateSerializerField, + FieldDefinition stateField, + @Nullable MethodHandle outputFunction, + boolean approximate, + CallSiteBinder callSiteBinder) + { + Parameter groupId = arg("groupId", int.class); + Parameter out = arg("out", BlockBuilder.class); + MethodDefinition method = definition.declareMethod(a(PUBLIC), "evaluateFinal", type(void.class), groupId, out); + + Block body = method.getBody(); + Variable thisVariable = method.getThis(); + + ByteCodeExpression state = thisVariable.getField(stateField); + + body.append(state.invoke("setGroupId", void.class, groupId.cast(long.class))); + + if (outputFunction != null) { + body.comment("output(state, out)"); + body.append(state); + if (approximate) { + checkNotNull(confidenceField, "confidenceField is null"); + body.append(thisVariable.getField(confidenceField)); + } + body.append(out); + body.append(invoke(callSiteBinder.bind(outputFunction), "output")); + } + else { + checkArgument(!approximate, "Approximate aggregations must specify an output function"); + ByteCodeExpression stateSerializer = thisVariable.getField(stateSerializerField); + body.append(stateSerializer.invoke("serialize", void.class, state.cast(Object.class), out)); + } + body.ret(); + } + + private static void generateEvaluateFinal( + ClassDefinition definition, + FieldDefinition confidenceField, + FieldDefinition stateSerializerField, + FieldDefinition stateField, + @Nullable + MethodHandle outputFunction, + boolean approximate, + CallSiteBinder callSiteBinder) + { + Parameter out = arg("out", BlockBuilder.class); + MethodDefinition method = definition.declareMethod( + a(PUBLIC), + "evaluateFinal", + type(void.class), + out); + + Block body = method.getBody(); + Variable thisVariable = method.getThis(); + + ByteCodeExpression state = thisVariable.getField(stateField); + + if (outputFunction != null) { + body.comment("output(state, out)"); + body.append(state); + if (approximate) { + checkNotNull(confidenceField, "confidenceField is null"); + body.append(thisVariable.getField(confidenceField)); + } + body.append(out); + body.append(invoke(callSiteBinder.bind(outputFunction), "output")); + } + else { + checkArgument(!approximate, "Approximate aggregations must specify an output function"); + ByteCodeExpression stateSerializer = thisVariable.getField(stateSerializerField); + body.append(stateSerializer.invoke("serialize", void.class, state.cast(Object.class), out)); + } + body.ret(); + } + + private static void generateConstructor( + ClassDefinition definition, + FieldDefinition stateSerializerField, + FieldDefinition stateFactoryField, + FieldDefinition inputChannelsField, + FieldDefinition maskChannelField, + @Nullable FieldDefinition sampleWeightChannelField, + @Nullable FieldDefinition confidenceField, + FieldDefinition stateField, + boolean grouped) + { + Parameter stateSerializer = arg("stateSerializer", AccumulatorStateSerializer.class); + Parameter stateFactory = arg("stateFactory", AccumulatorStateFactory.class); + Parameter inputChannels = arg("inputChannels", type(List.class, Integer.class)); + Parameter maskChannel = arg("maskChannel", type(Optional.class, Integer.class)); + Parameter sampleWeightChannel = arg("sampleWeightChannel", type(Optional.class, Integer.class)); + Parameter confidence = arg("confidence", double.class); + MethodDefinition method = definition.declareConstructor( + a(PUBLIC), + stateSerializer, + stateFactory, + inputChannels, + maskChannel, + sampleWeightChannel, + confidence); + + Block body = method.getBody(); + Variable thisVariable = method.getThis(); + + body.comment("super();") + .append(thisVariable) + .invokeConstructor(Object.class); + + body.append(thisVariable.setField(stateSerializerField, generateRequireNotNull(stateSerializer))); + body.append(thisVariable.setField(stateFactoryField, generateRequireNotNull(stateFactory))); + body.append(thisVariable.setField(inputChannelsField, generateRequireNotNull(inputChannels))); + body.append(thisVariable.setField(maskChannelField, generateRequireNotNull(maskChannel))); + + if (sampleWeightChannelField != null) { + body.append(thisVariable.setField(sampleWeightChannelField, generateRequireNotNull(sampleWeightChannel))); + } + + String createState; + if (grouped) { + createState = "createGroupedState"; + } + else { + createState = "createSingleState"; + } + + if (confidenceField != null) { + body.append(thisVariable.setField(confidenceField, confidence)); + } + + body.append(thisVariable.setField(stateField, stateFactory.invoke(createState, Object.class).cast(stateField.getType()))); + body.ret(); + } + + private static ByteCodeExpression generateRequireNotNull(Variable variable) + { + return invokeStatic(Objects.class, "requireNonNull", Object.class, variable.cast(Object.class), constantString(variable.getName() + " is null")) + .cast(variable.getType()); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/AccumulatorFactory.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/AccumulatorFactory.java new file mode 100644 index 00000000..c9e118e1 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/AccumulatorFactory.java @@ -0,0 +1,29 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import java.util.List; + +public interface AccumulatorFactory +{ + List getInputChannels(); + + Accumulator createAccumulator(); + + Accumulator createIntermediateAccumulator(); + + GroupedAccumulator createGroupedAccumulator(); + + GroupedAccumulator createGroupedIntermediateAccumulator(); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/AccumulatorFactoryBinder.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/AccumulatorFactoryBinder.java new file mode 100644 index 00000000..b92e9205 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/AccumulatorFactoryBinder.java @@ -0,0 +1,22 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import java.util.List; +import java.util.Optional; + +public interface AccumulatorFactoryBinder +{ + AccumulatorFactory bind(List argumentChannels, Optional maskChannel, Optional sampleWeightChannel, double confidence); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/AggregationCompiler.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/AggregationCompiler.java new file mode 100644 index 00000000..8c17a3a4 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/AggregationCompiler.java @@ -0,0 +1,262 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.facebook.presto.byteCode.DynamicClassLoader; +import com.facebook.presto.operator.aggregation.state.AccumulatorState; +import com.facebook.presto.operator.aggregation.state.AccumulatorStateFactory; +import com.facebook.presto.operator.aggregation.state.AccumulatorStateSerializer; +import com.facebook.presto.operator.aggregation.state.StateCompiler; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.type.SqlType; +import com.facebook.presto.type.TypeRegistry; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; + +import javax.annotation.Nullable; + +import java.lang.annotation.Annotation; +import java.lang.invoke.MethodHandle; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +import static com.facebook.presto.operator.aggregation.AggregationMetadata.ParameterMetadata; +import static com.facebook.presto.operator.aggregation.AggregationMetadata.ParameterMetadata.ParameterType.STATE; +import static com.facebook.presto.operator.aggregation.AggregationMetadata.ParameterMetadata.fromAnnotations; +import static com.facebook.presto.operator.aggregation.AggregationUtils.generateAggregationName; +import static com.facebook.presto.spi.type.TypeSignature.parseTypeSignature; +import static com.facebook.presto.util.ImmutableCollectors.toImmutableList; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.lang.invoke.MethodHandles.lookup; + +public class AggregationCompiler +{ + private final TypeManager typeManager; + + public AggregationCompiler() + { + this(new TypeRegistry()); + } + + public AggregationCompiler(TypeManager typeManager) + { + this.typeManager = checkNotNull(typeManager, "typeManager is null"); + } + + private static List findPublicStaticMethodsWithAnnotation(Class clazz, Class annotationClass) + { + ImmutableList.Builder methods = ImmutableList.builder(); + for (Method method : clazz.getMethods()) { + for (Annotation annotation : method.getAnnotations()) { + if (annotationClass.isInstance(annotation)) { + checkArgument(Modifier.isStatic(method.getModifiers()) && Modifier.isPublic(method.getModifiers()), "%s annotated with %s must be static and public", method.getName(), annotationClass.getSimpleName()); + methods.add(method); + } + } + } + return methods.build(); + } + + public InternalAggregationFunction generateAggregationFunction(Class clazz) + { + List aggregations = generateAggregationFunctions(clazz); + checkArgument(aggregations.size() == 1, "More than one aggregation function found"); + return aggregations.get(0); + } + + public InternalAggregationFunction generateAggregationFunction(Class clazz, Type returnType, List argumentTypes) + { + checkNotNull(returnType, "returnType is null"); + checkNotNull(argumentTypes, "argumentTypes is null"); + for (InternalAggregationFunction aggregation : generateAggregationFunctions(clazz)) { + if (aggregation.getFinalType().equals(returnType) && aggregation.getParameterTypes().equals(argumentTypes)) { + return aggregation; + } + } + throw new IllegalArgumentException(String.format("No method with return type %s and arguments %s", returnType, argumentTypes)); + } + + public List generateAggregationFunctions(Class clazz) + { + AggregationFunction aggregationAnnotation = clazz.getAnnotation(AggregationFunction.class); + checkNotNull(aggregationAnnotation, "aggregationAnnotation is null"); + + DynamicClassLoader classLoader = new DynamicClassLoader(clazz.getClassLoader()); + + ImmutableList.Builder builder = ImmutableList.builder(); + for (Class stateClass : getStateClasses(clazz)) { + AccumulatorStateSerializer stateSerializer = new StateCompiler().generateStateSerializer(stateClass, classLoader); + Type intermediateType = stateSerializer.getSerializedType(); + Method intermediateInputFunction = getIntermediateInputFunction(clazz, stateClass); + Method combineFunction = getCombineFunction(clazz, stateClass); + AccumulatorStateFactory stateFactory = new StateCompiler().generateStateFactory(stateClass, classLoader); + + for (Method outputFunction : getOutputFunctions(clazz, stateClass)) { + for (Method inputFunction : getInputFunctions(clazz, stateClass)) { + for (String name : getNames(outputFunction, aggregationAnnotation)) { + List inputTypes = getInputTypes(inputFunction); + Type outputType = AggregationUtils.getOutputType(outputFunction, stateSerializer, typeManager); + + AggregationMetadata metadata; + try { + MethodHandle inputHandle = lookup().unreflect(inputFunction); + MethodHandle intermediateInputHandle = intermediateInputFunction == null ? null : lookup().unreflect(intermediateInputFunction); + MethodHandle combineHandle = combineFunction == null ? null : lookup().unreflect(combineFunction); + MethodHandle outputHandle = outputFunction == null ? null : lookup().unreflect(outputFunction); + metadata = new AggregationMetadata( + generateAggregationName(name, outputType, inputTypes), + getParameterMetadata(inputFunction, aggregationAnnotation.approximate()), + inputHandle, + getParameterMetadata(intermediateInputFunction, false), + intermediateInputHandle, + combineHandle, + outputHandle, + stateClass, + stateSerializer, + stateFactory, + outputType, + aggregationAnnotation.approximate()); + } + catch (IllegalAccessException e) { + throw Throwables.propagate(e); + } + + AccumulatorFactoryBinder factory = new LazyAccumulatorFactoryBinder(metadata, classLoader); + builder.add(new InternalAggregationFunction(name, inputTypes, intermediateType, outputType, aggregationAnnotation.decomposable(), aggregationAnnotation.approximate(), factory)); + } + } + } + } + + return builder.build(); + } + + private List getParameterMetadata(@Nullable Method method, boolean sampleWeightAllowed) + { + if (method == null) { + return null; + } + + ImmutableList.Builder builder = ImmutableList.builder(); + builder.add(new ParameterMetadata(STATE)); + + Annotation[][] annotations = method.getParameterAnnotations(); + // Start at 1 because 0 is the STATE + for (int i = 1; i < annotations.length; i++) { + builder.add(fromAnnotations(annotations[i], method.getDeclaringClass() + "." + method.getName(), typeManager, sampleWeightAllowed)); + } + return builder.build(); + } + + private static List getNames(@Nullable Method outputFunction, AggregationFunction aggregationAnnotation) + { + List defaultNames = ImmutableList.builder().add(aggregationAnnotation.value()).addAll(Arrays.asList(aggregationAnnotation.alias())).build(); + + if (outputFunction == null) { + return defaultNames; + } + + AggregationFunction annotation = outputFunction.getAnnotation(AggregationFunction.class); + if (annotation == null) { + return defaultNames; + } + else { + return ImmutableList.builder().add(annotation.value()).addAll(Arrays.asList(annotation.alias())).build(); + } + } + + private static Method getIntermediateInputFunction(Class clazz, Class stateClass) + { + for (Method method : findPublicStaticMethodsWithAnnotation(clazz, IntermediateInputFunction.class)) { + if (method.getParameterTypes()[0] == stateClass) { + return method; + } + } + return null; + } + + private static Method getCombineFunction(Class clazz, Class stateClass) + { + for (Method method : findPublicStaticMethodsWithAnnotation(clazz, CombineFunction.class)) { + if (method.getParameterTypes()[0] == stateClass) { + return method; + } + } + return null; + } + + private static List getOutputFunctions(Class clazz, Class stateClass) + { + // Only include methods that match this state class + List methods = findPublicStaticMethodsWithAnnotation(clazz, OutputFunction.class).stream() + .filter(method -> method.getParameterTypes()[0] == stateClass) + .collect(toImmutableList()); + + if (methods.isEmpty()) { + List noOutputFunction = new ArrayList<>(); + noOutputFunction.add(null); + return noOutputFunction; + } + return methods; + } + + private static List getInputFunctions(Class clazz, Class stateClass) + { + // Only include methods that match this state class + List inputFunctions = findPublicStaticMethodsWithAnnotation(clazz, InputFunction.class).stream() + .filter(method -> method.getParameterTypes()[0] == stateClass) + .collect(toImmutableList()); + + checkArgument(!inputFunctions.isEmpty(), "Aggregation has no input functions"); + return inputFunctions; + } + + private List getInputTypes(Method inputFunction) + { + ImmutableList.Builder builder = ImmutableList.builder(); + Annotation[][] parameterAnnotations = inputFunction.getParameterAnnotations(); + for (Annotation[] annotations : parameterAnnotations) { + for (Annotation annotation : annotations) { + if (annotation instanceof SqlType) { + String typeName = ((SqlType) annotation).value(); + builder.add(typeManager.getType(parseTypeSignature(typeName))); + } + } + } + + return builder.build(); + } + + private static Set> getStateClasses(Class clazz) + { + ImmutableSet.Builder> builder = ImmutableSet.builder(); + for (Method inputFunction : findPublicStaticMethodsWithAnnotation(clazz, InputFunction.class)) { + checkArgument(inputFunction.getParameterTypes().length > 0, "Input function has no parameters"); + Class stateClass = inputFunction.getParameterTypes()[0]; + checkArgument(AccumulatorState.class.isAssignableFrom(stateClass), "stateClass is not a subclass of AccumulatorState"); + builder.add(stateClass); + } + ImmutableSet> stateClasses = builder.build(); + checkArgument(!stateClasses.isEmpty(), "No input functions found"); + + return stateClasses; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/AggregationFunction.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/AggregationFunction.java new file mode 100644 index 00000000..6f392925 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/AggregationFunction.java @@ -0,0 +1,32 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +public @interface AggregationFunction +{ + String value(); + + boolean approximate() default false; + + boolean decomposable() default true; + + String[] alias() default {}; +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/AggregationMetadata.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/AggregationMetadata.java new file mode 100644 index 00000000..45ac88e0 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/AggregationMetadata.java @@ -0,0 +1,327 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.facebook.presto.operator.aggregation.state.AccumulatorStateFactory; +import com.facebook.presto.operator.aggregation.state.AccumulatorStateSerializer; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.spi.type.TypeSignature; +import com.facebook.presto.type.SqlType; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import io.airlift.slice.Slice; + +import javax.annotation.Nullable; + +import java.lang.annotation.Annotation; +import java.lang.invoke.MethodHandle; +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +import static com.facebook.presto.operator.aggregation.AggregationMetadata.ParameterMetadata.ParameterType.BLOCK_INDEX; +import static com.facebook.presto.operator.aggregation.AggregationMetadata.ParameterMetadata.ParameterType.BLOCK_INPUT_CHANNEL; +import static com.facebook.presto.operator.aggregation.AggregationMetadata.ParameterMetadata.ParameterType.INPUT_CHANNEL; +import static com.facebook.presto.operator.aggregation.AggregationMetadata.ParameterMetadata.ParameterType.NULLABLE_BLOCK_INPUT_CHANNEL; +import static com.facebook.presto.operator.aggregation.AggregationMetadata.ParameterMetadata.ParameterType.SAMPLE_WEIGHT; +import static com.facebook.presto.operator.aggregation.AggregationMetadata.ParameterMetadata.ParameterType.STATE; +import static com.facebook.presto.spi.type.TypeSignature.parseTypeSignature; +import static com.facebook.presto.util.ImmutableCollectors.toImmutableList; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class AggregationMetadata +{ + public static final Set> SUPPORTED_PARAMETER_TYPES = ImmutableSet.of(Block.class, long.class, double.class, boolean.class, Slice.class); + + private final String name; + private final List inputMetadata; + private final MethodHandle inputFunction; + private final List intermediateInputMetadata; + @Nullable + private final MethodHandle intermediateInputFunction; + @Nullable + private final MethodHandle combineFunction; + @Nullable + private final MethodHandle outputFunction; + private final AccumulatorStateSerializer stateSerializer; + private final AccumulatorStateFactory stateFactory; + private final Type outputType; + private final boolean approximate; + + public AggregationMetadata( + String name, + List inputMetadata, + MethodHandle inputFunction, + @Nullable List intermediateInputMetadata, + @Nullable MethodHandle intermediateInputFunction, + @Nullable MethodHandle combineFunction, + @Nullable MethodHandle outputFunction, + Class stateInterface, + AccumulatorStateSerializer stateSerializer, + AccumulatorStateFactory stateFactory, + Type outputType, + boolean approximate) + { + this.outputType = checkNotNull(outputType); + this.inputMetadata = ImmutableList.copyOf(checkNotNull(inputMetadata, "inputMetadata is null")); + checkArgument((intermediateInputFunction == null) == (intermediateInputMetadata == null), "intermediate input parameters must be specified iff an intermediate function is provided"); + if (intermediateInputMetadata != null) { + this.intermediateInputMetadata = ImmutableList.copyOf(intermediateInputMetadata); + } + else { + this.intermediateInputMetadata = null; + } + this.name = checkNotNull(name, "name is null"); + this.inputFunction = checkNotNull(inputFunction, "inputFunction is null"); + checkArgument(combineFunction == null || intermediateInputFunction == null, "Aggregation cannot have both a combine and a intermediate input method"); + checkArgument(combineFunction != null || intermediateInputFunction != null, "Aggregation must have either a combine or a intermediate input method"); + this.intermediateInputFunction = intermediateInputFunction; + this.combineFunction = combineFunction; + this.outputFunction = outputFunction; + this.stateSerializer = checkNotNull(stateSerializer, "stateSerializer is null"); + this.stateFactory = checkNotNull(stateFactory, "stateFactory is null"); + this.approximate = approximate; + + verifyInputFunctionSignature(inputFunction, inputMetadata, stateInterface); + if (intermediateInputFunction != null) { + checkArgument(countInputChannels(intermediateInputMetadata) == 1, "Intermediate input function may only have one input channel"); + verifyInputFunctionSignature(intermediateInputFunction, intermediateInputMetadata, stateInterface); + } + if (combineFunction != null) { + verifyCombineFunction(combineFunction, stateInterface); + } + if (approximate) { + verifyApproximateOutputFunction(outputFunction, stateInterface); + } + else { + verifyExactOutputFunction(outputFunction, stateInterface); + } + } + + public Type getOutputType() + { + return outputType; + } + + public List getInputMetadata() + { + return inputMetadata; + } + + public List getIntermediateInputMetadata() + { + return intermediateInputMetadata; + } + + public String getName() + { + return name; + } + + public MethodHandle getInputFunction() + { + return inputFunction; + } + + @Nullable + public MethodHandle getIntermediateInputFunction() + { + return intermediateInputFunction; + } + + @Nullable + public MethodHandle getCombineFunction() + { + return combineFunction; + } + + @Nullable + public MethodHandle getOutputFunction() + { + return outputFunction; + } + + public AccumulatorStateSerializer getStateSerializer() + { + return stateSerializer; + } + + public AccumulatorStateFactory getStateFactory() + { + return stateFactory; + } + + public boolean isApproximate() + { + return approximate; + } + + private static void verifyInputFunctionSignature(MethodHandle method, List parameterMetadatas, Class stateInterface) + { + Class[] parameters = method.type().parameterArray(); + checkArgument(stateInterface == parameters[0], "First argument of aggregation input function must be %s", stateInterface.getSimpleName()); + checkArgument(parameters.length > 0, "Aggregation input function must have at least one parameter"); + checkArgument(parameterMetadatas.get(0).getParameterType() == STATE, "First parameter must be state"); + for (int i = 1; i < parameters.length; i++) { + ParameterMetadata metadata = parameterMetadatas.get(i); + switch (metadata.getParameterType()) { + case BLOCK_INPUT_CHANNEL: + case NULLABLE_BLOCK_INPUT_CHANNEL: + checkArgument(parameters[i] == Block.class, "Parameter must be Block if it has @BlockPosition"); + break; + case INPUT_CHANNEL: + checkArgument(SUPPORTED_PARAMETER_TYPES.contains(parameters[i]), "Unsupported type: %s", parameters[i].getSimpleName()); + checkArgument(parameters[i] == metadata.getSqlType().getJavaType(), + "Expected method %s parameter %s type to be %s (%s)", + method, + i, + metadata.getSqlType().getJavaType().getName(), + metadata.getSqlType()); + break; + case BLOCK_INDEX: + checkArgument(parameters[i] == int.class, "Block index parameter must be an int"); + break; + case SAMPLE_WEIGHT: + checkArgument(parameters[i] == long.class, "Sample weight parameter must be a long"); + break; + default: + throw new IllegalArgumentException("Unsupported parameter: " + metadata.getParameterType()); + } + } + } + + private static void verifyCombineFunction(MethodHandle method, Class stateInterface) + { + Class[] parameterTypes = method.type().parameterArray(); + checkArgument(parameterTypes.length == 2 && parameterTypes[0] == stateInterface && parameterTypes[1] == stateInterface, "Combine function must have the signature (%s, %s)", stateInterface.getSimpleName(), stateInterface.getSimpleName()); + } + + private static void verifyApproximateOutputFunction(MethodHandle method, Class stateInterface) + { + checkNotNull(method, "Approximate aggregations must specify an output function"); + Class[] parameterTypes = method.type().parameterArray(); + checkArgument(parameterTypes.length == 3 && parameterTypes[0] == stateInterface && parameterTypes[1] == double.class && parameterTypes[2] == BlockBuilder.class, "Output function must have the signature (%s, double, BlockBuilder)", stateInterface.getSimpleName()); + } + + private static void verifyExactOutputFunction(MethodHandle method, Class stateInterface) + { + if (method == null) { + return; + } + Class[] parameterTypes = method.type().parameterArray(); + checkArgument(parameterTypes.length == 2 && parameterTypes[0] == stateInterface && parameterTypes[1] == BlockBuilder.class, "Output function must have the signature (%s, BlockBuilder)", stateInterface.getSimpleName()); + } + + public static int countInputChannels(List metadatas) + { + int parameters = 0; + for (ParameterMetadata metadata : metadatas) { + if (metadata.getParameterType() == INPUT_CHANNEL || + metadata.getParameterType() == BLOCK_INPUT_CHANNEL || + metadata.getParameterType() == NULLABLE_BLOCK_INPUT_CHANNEL) { + parameters++; + } + } + + return parameters; + } + + public static class ParameterMetadata + { + private final ParameterType parameterType; + private final Type sqlType; + + public ParameterMetadata(ParameterType parameterType) + { + this(parameterType, null); + } + + public ParameterMetadata(ParameterType parameterType, Type sqlType) + { + checkArgument((sqlType == null) == (parameterType == BLOCK_INDEX || parameterType == SAMPLE_WEIGHT || parameterType == STATE), + "sqlType must be provided only for input channels"); + this.parameterType = parameterType; + this.sqlType = sqlType; + } + + public static ParameterMetadata fromAnnotations(Annotation[] annotations, String methodName, TypeManager typeManager, boolean sampleWeightAllowed) + { + List baseTypes = Arrays.asList(annotations).stream() + .filter(annotation -> annotation instanceof SqlType || annotation instanceof BlockIndex || annotation instanceof SampleWeight) + .collect(toImmutableList()); + + checkArgument(baseTypes.size() == 1, "Parameter of %s must have exactly one of @SqlType, @BlockIndex, and @SampleWeight", methodName); + + boolean nullable = Arrays.asList(annotations).stream().anyMatch(annotation -> annotation instanceof NullablePosition); + boolean isBlock = Arrays.asList(annotations).stream().anyMatch(annotation -> annotation instanceof BlockPosition); + + Annotation annotation = baseTypes.get(0); + checkArgument((!isBlock && !nullable) || (annotation instanceof SqlType), + "%s contains a parameter with @BlockPosition and/or @NullablePosition that is not @SqlType", methodName); + if (annotation instanceof SqlType) { + TypeSignature signature = parseTypeSignature(((SqlType) annotation).value()); + if (isBlock) { + if (nullable) { + return new ParameterMetadata(NULLABLE_BLOCK_INPUT_CHANNEL, typeManager.getType(signature)); + } + else { + return new ParameterMetadata(BLOCK_INPUT_CHANNEL, typeManager.getType(signature)); + } + } + else { + if (nullable) { + throw new IllegalArgumentException(methodName + " contains a parameter with @NullablePosition that is not @BlockPosition"); + } + else { + return new ParameterMetadata(INPUT_CHANNEL, typeManager.getType(signature)); + } + } + } + else if (annotation instanceof BlockIndex) { + return new ParameterMetadata(BLOCK_INDEX); + } + else if (annotation instanceof SampleWeight) { + checkArgument(sampleWeightAllowed, "@SampleWeight only allowed in approximate aggregations"); + return new ParameterMetadata(SAMPLE_WEIGHT); + } + else { + throw new IllegalArgumentException("Unsupported annotation: " + annotation); + } + } + + public ParameterType getParameterType() + { + return parameterType; + } + + public Type getSqlType() + { + return sqlType; + } + + public enum ParameterType + { + INPUT_CHANNEL, + BLOCK_INPUT_CHANNEL, + NULLABLE_BLOCK_INPUT_CHANNEL, + BLOCK_INDEX, + SAMPLE_WEIGHT, + STATE + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/AggregationUtils.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/AggregationUtils.java new file mode 100644 index 00000000..fe5d6237 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/AggregationUtils.java @@ -0,0 +1,157 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.facebook.presto.operator.aggregation.state.AccumulatorStateSerializer; +import com.facebook.presto.operator.aggregation.state.CorrelationState; +import com.facebook.presto.operator.aggregation.state.CovarianceState; +import com.facebook.presto.operator.aggregation.state.RegressionState; +import com.facebook.presto.operator.aggregation.state.VarianceState; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.spi.type.TypeSignature; +import com.google.common.base.CaseFormat; + +import javax.annotation.Nullable; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.function.Function; + +import static com.google.common.base.Preconditions.checkArgument; +import static java.util.Locale.ENGLISH; + +public final class AggregationUtils +{ + private AggregationUtils() + { + } + + public static void updateVarianceState(VarianceState state, double value) + { + state.setCount(state.getCount() + 1); + double delta = value - state.getMean(); + state.setMean(state.getMean() + delta / state.getCount()); + state.setM2(state.getM2() + delta * (value - state.getMean())); + } + + public static void updateCovarianceState(CovarianceState state, double x, double y) + { + state.setCount(state.getCount() + 1); + state.setSumXY(state.getSumXY() + x * y); + state.setSumX(state.getSumX() + x); + state.setSumY(state.getSumY() + y); + } + + public static void updateCorrelationState(CorrelationState state, double x, double y) + { + updateCovarianceState(state, x, y); + state.setSumXSquare(state.getSumXSquare() + x * x); + state.setSumYSquare(state.getSumYSquare() + y * y); + } + + public static void updateRegressionState(RegressionState state, double x, double y) + { + updateCovarianceState(state, x, y); + state.setSumXSquare(state.getSumXSquare() + x * x); + } + + public static void mergeVarianceState(VarianceState state, VarianceState otherState) + { + long count = otherState.getCount(); + double mean = otherState.getMean(); + double m2 = otherState.getM2(); + + checkArgument(count >= 0, "count is negative"); + if (count == 0) { + return; + } + long newCount = count + state.getCount(); + double newMean = ((count * mean) + (state.getCount() * state.getMean())) / (double) newCount; + double delta = mean - state.getMean(); + double m2Delta = m2 + delta * delta * count * state.getCount() / (double) newCount; + state.setM2(state.getM2() + m2Delta); + state.setCount(newCount); + state.setMean(newMean); + } + + private static void updateCovarianceState(CovarianceState state, CovarianceState otherState) + { + state.setSumX(state.getSumX() + otherState.getSumX()); + state.setSumY(state.getSumY() + otherState.getSumY()); + state.setSumXY(state.getSumXY() + otherState.getSumXY()); + state.setCount(state.getCount() + otherState.getCount()); + } + + public static void mergeCovarianceState(CovarianceState state, CovarianceState otherState) + { + if (otherState.getCount() == 0) { + return; + } + + updateCovarianceState(state, otherState); + } + + public static void mergeCorrelationState(CorrelationState state, CorrelationState otherState) + { + if (otherState.getCount() == 0) { + return; + } + + updateCovarianceState(state, otherState); + state.setSumXSquare(state.getSumXSquare() + otherState.getSumXSquare()); + state.setSumYSquare(state.getSumYSquare() + otherState.getSumYSquare()); + } + + public static void mergeRegressionState(RegressionState state, RegressionState otherState) + { + if (otherState.getCount() == 0) { + return; + } + + updateCovarianceState(state, otherState); + state.setSumXSquare(state.getSumXSquare() + otherState.getSumXSquare()); + } + + public static Type getOutputType(@Nullable Method outputFunction, AccumulatorStateSerializer serializer, TypeManager typeManager) + { + if (outputFunction == null) { + return serializer.getSerializedType(); + } + else { + return typeManager.getType(TypeSignature.parseTypeSignature(outputFunction.getAnnotation(OutputFunction.class).value())); + } + } + + public static String generateAggregationName(String baseName, Type outputType, List inputTypes) + { + StringBuilder sb = new StringBuilder(); + sb.append(CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_CAMEL, outputType.getTypeSignature().toString())); + for (Type inputType : inputTypes) { + sb.append(CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_CAMEL, inputType.getTypeSignature().toString())); + } + sb.append(CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, baseName.toLowerCase(ENGLISH))); + + return sb.toString(); + } + + // used by aggregation compiler + @SuppressWarnings("UnusedDeclaration") + public static Function pageBlockGetter(final Page page) + { + return page::getBlock; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/ApproximateAverageAggregations.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/ApproximateAverageAggregations.java new file mode 100644 index 00000000..fb50727c --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/ApproximateAverageAggregations.java @@ -0,0 +1,118 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.facebook.presto.operator.aggregation.state.AccumulatorState; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.type.SqlType; + +import static com.facebook.presto.operator.aggregation.ApproximateUtils.formatApproximateResult; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; + +@AggregationFunction(value = "avg", approximate = true) +public final class ApproximateAverageAggregations +{ + private ApproximateAverageAggregations() {} + + @InputFunction + public static void bigintInput(ApproximateAverageState state, @SqlType(StandardTypes.BIGINT) long value, @SampleWeight long sampleWeight) + { + doubleInput(state, (double) value, sampleWeight); + } + + @InputFunction + public static void doubleInput(ApproximateAverageState state, @SqlType(StandardTypes.DOUBLE) double value, @SampleWeight long sampleWeight) + { + long currentCount = state.getCount(); + double currentMean = state.getMean(); + + // Use numerically stable variant + for (int i = 0; i < sampleWeight; i++) { + currentCount++; + double delta = value - currentMean; + currentMean += (delta / currentCount); + // update m2 inline + state.setM2(state.getM2() + (delta * (value - currentMean))); + } + + // write values back out + state.setCount(currentCount); + state.setMean(currentMean); + state.setSamples(state.getSamples() + 1); + } + + @CombineFunction + public static void combine(ApproximateAverageState state, ApproximateAverageState otherState) + { + long inputCount = otherState.getCount(); + long inputSamples = otherState.getSamples(); + double inputMean = otherState.getMean(); + double inputM2 = otherState.getM2(); + + long currentCount = state.getCount(); + double currentMean = state.getMean(); + double currentM2 = state.getM2(); + + // Use numerically stable variant + if (inputCount > 0) { + long newCount = currentCount + inputCount; + double newMean = ((currentCount * currentMean) + (inputCount * inputMean)) / newCount; + double delta = inputMean - currentMean; + double newM2 = currentM2 + inputM2 + ((delta * delta) * (currentCount * inputCount)) / newCount; + + state.setCount(newCount); + state.setSamples(state.getSamples() + inputSamples); + state.setMean(newMean); + state.setM2(newM2); + } + } + + @OutputFunction(StandardTypes.VARCHAR) + public static void output(ApproximateAverageState state, double confidence, BlockBuilder out) + { + if (state.getCount() == 0) { + out.appendNull(); + } + else { + String result = formatApproximateAverage(state.getSamples(), state.getMean(), state.getM2() / state.getCount(), confidence); + VARCHAR.writeString(out, result); + } + } + + private static String formatApproximateAverage(long samples, double mean, double variance, double confidence) + { + return formatApproximateResult(mean, Math.sqrt(variance / samples), confidence, false); + } + + public interface ApproximateAverageState + extends AccumulatorState + { + long getCount(); + + void setCount(long value); + + long getSamples(); + + void setSamples(long value); + + double getMean(); + + void setMean(double value); + + double getM2(); + + void setM2(double value); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/ApproximateCountAggregation.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/ApproximateCountAggregation.java new file mode 100644 index 00000000..024ab3e3 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/ApproximateCountAggregation.java @@ -0,0 +1,65 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.facebook.presto.operator.aggregation.state.AccumulatorState; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.StandardTypes; +import io.airlift.slice.Slice; +import io.airlift.slice.Slices; + +import static com.facebook.presto.operator.aggregation.ApproximateUtils.countError; +import static com.facebook.presto.operator.aggregation.ApproximateUtils.formatApproximateResult; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; + +@AggregationFunction(value = "count", approximate = true) +public final class ApproximateCountAggregation +{ + private ApproximateCountAggregation() {} + + @InputFunction + public static void input(ApproximateCountState state, @SampleWeight long sampleWeight) + { + if (sampleWeight > 0) { + state.setSamples(state.getSamples() + 1); + state.setCount(state.getCount() + sampleWeight); + } + } + + @CombineFunction + public static void combine(ApproximateCountState state, ApproximateCountState otherState) + { + state.setCount(state.getCount() + otherState.getCount()); + state.setSamples(state.getSamples() + otherState.getSamples()); + } + + @OutputFunction(StandardTypes.VARCHAR) + public static void output(ApproximateCountState state, double confidence, BlockBuilder out) + { + Slice value = Slices.utf8Slice(formatApproximateResult(state.getCount(), countError(state.getSamples(), state.getCount()), confidence, true)); + VARCHAR.writeSlice(out, value); + } + + public interface ApproximateCountState + extends AccumulatorState + { + long getCount(); + + void setCount(long value); + + long getSamples(); + + void setSamples(long value); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/ApproximateCountColumnAggregations.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/ApproximateCountColumnAggregations.java new file mode 100644 index 00000000..e8ce5783 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/ApproximateCountColumnAggregations.java @@ -0,0 +1,85 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.facebook.presto.operator.aggregation.state.AccumulatorState; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.type.SqlType; +import io.airlift.slice.Slices; + +import static com.facebook.presto.operator.aggregation.ApproximateUtils.countError; +import static com.facebook.presto.operator.aggregation.ApproximateUtils.formatApproximateResult; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; + +@AggregationFunction(value = "count", approximate = true) +public final class ApproximateCountColumnAggregations +{ + private ApproximateCountColumnAggregations() {} + + @InputFunction + public static void booleanInput(ApproximateCountState state, @BlockPosition @SqlType(StandardTypes.BOOLEAN) Block block, @BlockIndex int index, @SampleWeight long sampleWeight) + { + state.setCount(state.getCount() + sampleWeight); + state.setSamples(state.getSamples() + 1); + } + + @InputFunction + public static void bigintInput(ApproximateCountState state, @BlockPosition @SqlType(StandardTypes.BIGINT) Block block, @BlockIndex int index, @SampleWeight long sampleWeight) + { + state.setCount(state.getCount() + sampleWeight); + state.setSamples(state.getSamples() + 1); + } + + @InputFunction + public static void doubleInput(ApproximateCountState state, @BlockPosition @SqlType(StandardTypes.DOUBLE) Block block, @BlockIndex int index, @SampleWeight long sampleWeight) + { + state.setCount(state.getCount() + sampleWeight); + state.setSamples(state.getSamples() + 1); + } + + @InputFunction + public static void varcharInput(ApproximateCountState state, @BlockPosition @SqlType(StandardTypes.VARCHAR) Block block, @BlockIndex int index, @SampleWeight long sampleWeight) + { + state.setCount(state.getCount() + sampleWeight); + state.setSamples(state.getSamples() + 1); + } + + @CombineFunction + public static void combine(ApproximateCountState state, ApproximateCountState otherState) + { + state.setCount(state.getCount() + otherState.getCount()); + state.setSamples(state.getSamples() + otherState.getSamples()); + } + + @OutputFunction(StandardTypes.VARCHAR) + public static void output(ApproximateCountState state, double confidence, BlockBuilder out) + { + String result = formatApproximateResult(state.getCount(), countError(state.getSamples(), state.getCount()), confidence, true); + VARCHAR.writeSlice(out, Slices.utf8Slice(result)); + } + + public interface ApproximateCountState + extends AccumulatorState + { + long getCount(); + + void setCount(long value); + + long getSamples(); + + void setSamples(long value); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/ApproximateCountDistinctAggregations.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/ApproximateCountDistinctAggregations.java new file mode 100644 index 00000000..5beb5fe7 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/ApproximateCountDistinctAggregations.java @@ -0,0 +1,139 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.facebook.presto.operator.aggregation.state.HyperLogLogState; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.type.SqlType; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; +import io.airlift.stats.cardinality.HyperLogLog; + +import static com.facebook.presto.spi.StandardErrorCode.INVALID_FUNCTION_ARGUMENT; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static com.facebook.presto.util.Failures.checkCondition; + +@AggregationFunction("approx_distinct") +public final class ApproximateCountDistinctAggregations +{ + public static final InternalAggregationFunction LONG_APPROXIMATE_COUNT_DISTINCT_AGGREGATIONS = new AggregationCompiler().generateAggregationFunction(ApproximateCountDistinctAggregations.class, BIGINT, ImmutableList.of(BIGINT, DOUBLE)); + public static final InternalAggregationFunction DOUBLE_APPROXIMATE_COUNT_DISTINCT_AGGREGATIONS = new AggregationCompiler().generateAggregationFunction(ApproximateCountDistinctAggregations.class, BIGINT, ImmutableList.of(DOUBLE, DOUBLE)); + public static final InternalAggregationFunction VARBINARY_APPROXIMATE_COUNT_DISTINCT_AGGREGATIONS = new AggregationCompiler().generateAggregationFunction(ApproximateCountDistinctAggregations.class, BIGINT, ImmutableList.of(VARCHAR, DOUBLE)); + private static final double DEFAULT_STANDARD_ERROR = 0.023; + private static final double LOWEST_MAX_STANDARD_ERROR = 0.01150; + private static final double HIGHEST_MAX_STANDARD_ERROR = 0.26000; + + private ApproximateCountDistinctAggregations() {} + + @InputFunction + public static void input(HyperLogLogState state, @SqlType(StandardTypes.BIGINT) long value) + { + input(state, value, DEFAULT_STANDARD_ERROR); + } + + @InputFunction + public static void input(HyperLogLogState state, @SqlType(StandardTypes.BIGINT) long value, @SqlType(StandardTypes.DOUBLE) double maxStandardError) + { + HyperLogLog hll = getOrCreateHyperLogLog(state, maxStandardError); + state.addMemoryUsage(-hll.estimatedInMemorySize()); + hll.add(value); + state.addMemoryUsage(hll.estimatedInMemorySize()); + } + + @InputFunction + public static void input(HyperLogLogState state, @SqlType(StandardTypes.DOUBLE) double value) + { + input(state, value, DEFAULT_STANDARD_ERROR); + } + + @InputFunction + public static void input(HyperLogLogState state, @SqlType(StandardTypes.DOUBLE) double value, @SqlType(StandardTypes.DOUBLE) double maxStandardError) + { + input(state, Double.doubleToLongBits(value), maxStandardError); + } + + @InputFunction + public static void input(HyperLogLogState state, @SqlType(StandardTypes.VARCHAR) Slice value) + { + input(state, value, DEFAULT_STANDARD_ERROR); + } + + @InputFunction + public static void input(HyperLogLogState state, @SqlType(StandardTypes.VARCHAR) Slice value, @SqlType(StandardTypes.DOUBLE) double maxStandardError) + { + HyperLogLog hll = getOrCreateHyperLogLog(state, maxStandardError); + state.addMemoryUsage(-hll.estimatedInMemorySize()); + hll.add(value); + state.addMemoryUsage(hll.estimatedInMemorySize()); + } + + private static HyperLogLog getOrCreateHyperLogLog(HyperLogLogState state, double maxStandardError) + { + HyperLogLog hll = state.getHyperLogLog(); + if (hll == null) { + hll = HyperLogLog.newInstance(standardErrorToBuckets(maxStandardError)); + state.setHyperLogLog(hll); + state.addMemoryUsage(hll.estimatedInMemorySize()); + } + return hll; + } + + @VisibleForTesting + static int standardErrorToBuckets(double maxStandardError) + { + checkCondition(maxStandardError >= LOWEST_MAX_STANDARD_ERROR && maxStandardError <= HIGHEST_MAX_STANDARD_ERROR, + INVALID_FUNCTION_ARGUMENT, + "Max standard error must be in [%s, %s]: %s", LOWEST_MAX_STANDARD_ERROR, HIGHEST_MAX_STANDARD_ERROR, maxStandardError); + return log2Ceiling((int) Math.ceil(1.0816 / (maxStandardError * maxStandardError))); + } + + private static int log2Ceiling(int value) + { + return Integer.highestOneBit(value - 1) << 1; + } + + @CombineFunction + public static void combineState(HyperLogLogState state, HyperLogLogState otherState) + { + HyperLogLog input = otherState.getHyperLogLog(); + + HyperLogLog previous = state.getHyperLogLog(); + if (previous == null) { + state.setHyperLogLog(input); + state.addMemoryUsage(input.estimatedInMemorySize()); + } + else { + state.addMemoryUsage(-previous.estimatedInMemorySize()); + previous.mergeWith(input); + state.addMemoryUsage(previous.estimatedInMemorySize()); + } + } + + @OutputFunction(StandardTypes.BIGINT) + public static void evaluateFinal(HyperLogLogState state, BlockBuilder out) + { + HyperLogLog hyperLogLog = state.getHyperLogLog(); + if (hyperLogLog == null) { + BIGINT.writeLong(out, 0); + } + else { + BIGINT.writeLong(out, hyperLogLog.cardinality()); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/ApproximateDoublePercentileAggregations.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/ApproximateDoublePercentileAggregations.java new file mode 100644 index 00000000..3e432704 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/ApproximateDoublePercentileAggregations.java @@ -0,0 +1,90 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.facebook.presto.operator.aggregation.state.DigestAndPercentileState; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.type.SqlType; +import com.google.common.collect.ImmutableList; +import io.airlift.stats.QuantileDigest; + +import static com.facebook.presto.spi.StandardErrorCode.INVALID_FUNCTION_ARGUMENT; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; +import static com.facebook.presto.util.Failures.checkCondition; +import static com.google.common.base.Preconditions.checkState; + +@AggregationFunction("approx_percentile") +public final class ApproximateDoublePercentileAggregations +{ + public static final InternalAggregationFunction DOUBLE_APPROXIMATE_PERCENTILE_AGGREGATION = new AggregationCompiler().generateAggregationFunction(ApproximateDoublePercentileAggregations.class, DOUBLE, ImmutableList.of(DOUBLE, DOUBLE)); + public static final InternalAggregationFunction DOUBLE_APPROXIMATE_PERCENTILE_WEIGHTED_AGGREGATION = new AggregationCompiler().generateAggregationFunction(ApproximateDoublePercentileAggregations.class, DOUBLE, ImmutableList.of(DOUBLE, BIGINT, DOUBLE)); + + private ApproximateDoublePercentileAggregations() {} + + @InputFunction + public static void input(DigestAndPercentileState state, @SqlType(StandardTypes.DOUBLE) double value, @SqlType(StandardTypes.DOUBLE) double percentile) + { + ApproximateLongPercentileAggregations.input(state, doubleToSortableLong(value), percentile); + } + + @InputFunction + public static void weightedInput(DigestAndPercentileState state, @SqlType(StandardTypes.DOUBLE) double value, @SqlType(StandardTypes.BIGINT) long weight, @SqlType(StandardTypes.DOUBLE) double percentile) + { + ApproximateLongPercentileAggregations.weightedInput(state, doubleToSortableLong(value), weight, percentile); + } + + @CombineFunction + public static void combine(DigestAndPercentileState state, DigestAndPercentileState otherState) + { + ApproximateLongPercentileAggregations.combine(state, otherState); + } + + @OutputFunction(StandardTypes.DOUBLE) + public static void output(DigestAndPercentileState state, BlockBuilder out) + { + QuantileDigest digest = state.getDigest(); + double percentile = state.getPercentile(); + if (digest == null || digest.getCount() == 0.0) { + out.appendNull(); + } + else { + checkState(percentile != -1.0, "Percentile is missing"); + checkCondition(0 <= percentile && percentile <= 1, INVALID_FUNCTION_ARGUMENT, "Percentile must be between 0 and 1"); + DOUBLE.writeDouble(out, longToDouble(digest.getQuantile(percentile))); + } + } + + private static double longToDouble(long value) + { + if (value < 0) { + value ^= 0x7fffffffffffffffL; + } + + return Double.longBitsToDouble(value); + } + + private static long doubleToSortableLong(double value) + { + long result = Double.doubleToRawLongBits(value); + + if (result < 0) { + result ^= 0x7fffffffffffffffL; + } + + return result; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/ApproximateLongPercentileAggregations.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/ApproximateLongPercentileAggregations.java new file mode 100644 index 00000000..c0f9ca51 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/ApproximateLongPercentileAggregations.java @@ -0,0 +1,108 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.facebook.presto.operator.aggregation.state.DigestAndPercentileState; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.type.SqlType; +import com.google.common.collect.ImmutableList; +import io.airlift.stats.QuantileDigest; + +import static com.facebook.presto.spi.StandardErrorCode.INVALID_FUNCTION_ARGUMENT; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; +import static com.facebook.presto.util.Failures.checkCondition; +import static com.google.common.base.Preconditions.checkState; + +@AggregationFunction("approx_percentile") +public final class ApproximateLongPercentileAggregations +{ + public static final InternalAggregationFunction LONG_APPROXIMATE_PERCENTILE_AGGREGATION = new AggregationCompiler().generateAggregationFunction(ApproximateLongPercentileAggregations.class, BIGINT, ImmutableList.of(BIGINT, DOUBLE)); + public static final InternalAggregationFunction LONG_APPROXIMATE_PERCENTILE_WEIGHTED_AGGREGATION = new AggregationCompiler().generateAggregationFunction(ApproximateLongPercentileAggregations.class, BIGINT, ImmutableList.of(BIGINT, BIGINT, DOUBLE)); + + private ApproximateLongPercentileAggregations() {} + + @InputFunction + public static void input(DigestAndPercentileState state, @SqlType(StandardTypes.BIGINT) long value, @SqlType(StandardTypes.DOUBLE) double percentile) + { + QuantileDigest digest = state.getDigest(); + + if (digest == null) { + digest = new QuantileDigest(0.01); + state.setDigest(digest); + state.addMemoryUsage(digest.estimatedInMemorySizeInBytes()); + } + + state.addMemoryUsage(-digest.estimatedInMemorySizeInBytes()); + digest.add(value); + state.addMemoryUsage(digest.estimatedInMemorySizeInBytes()); + + // use last percentile + state.setPercentile(percentile); + } + + @InputFunction + public static void weightedInput(DigestAndPercentileState state, @SqlType(StandardTypes.BIGINT) long value, @SqlType(StandardTypes.BIGINT) long weight, @SqlType(StandardTypes.DOUBLE) double percentile) + { + QuantileDigest digest = state.getDigest(); + + if (digest == null) { + digest = new QuantileDigest(0.01); + state.setDigest(digest); + state.addMemoryUsage(digest.estimatedInMemorySizeInBytes()); + } + + state.addMemoryUsage(-digest.estimatedInMemorySizeInBytes()); + digest.add(value, weight); + state.addMemoryUsage(digest.estimatedInMemorySizeInBytes()); + + // use last percentile + state.setPercentile(percentile); + } + + @CombineFunction + public static void combine(DigestAndPercentileState state, DigestAndPercentileState otherState) + { + QuantileDigest input = otherState.getDigest(); + + QuantileDigest previous = state.getDigest(); + if (previous == null) { + state.setDigest(input); + state.addMemoryUsage(input.estimatedInMemorySizeInBytes()); + } + else { + state.addMemoryUsage(-previous.estimatedInMemorySizeInBytes()); + previous.merge(input); + state.addMemoryUsage(previous.estimatedInMemorySizeInBytes()); + } + state.setPercentile(otherState.getPercentile()); + } + + @OutputFunction(StandardTypes.BIGINT) + public static void output(DigestAndPercentileState state, BlockBuilder out) + { + QuantileDigest digest = state.getDigest(); + double percentile = state.getPercentile(); + if (digest == null || digest.getCount() == 0.0) { + out.appendNull(); + } + else { + checkState(percentile != -1.0, "Percentile is missing"); + checkCondition(0 <= percentile && percentile <= 1, INVALID_FUNCTION_ARGUMENT, "Percentile must be between 0 and 1"); + BIGINT.writeLong(out, digest.getQuantile(percentile)); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/ApproximateSetAggregation.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/ApproximateSetAggregation.java new file mode 100644 index 00000000..43ea501c --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/ApproximateSetAggregation.java @@ -0,0 +1,93 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.facebook.presto.operator.aggregation.state.AccumulatorStateSerializer; +import com.facebook.presto.operator.aggregation.state.HyperLogLogState; +import com.facebook.presto.operator.aggregation.state.StateCompiler; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.type.SqlType; +import io.airlift.slice.Slice; +import io.airlift.stats.cardinality.HyperLogLog; + +@AggregationFunction("approx_set") +public final class ApproximateSetAggregation +{ + private static final int NUMBER_OF_BUCKETS = 4096; + private static final AccumulatorStateSerializer SERIALIZER = new StateCompiler().generateStateSerializer(HyperLogLogState.class); + + private ApproximateSetAggregation() {} + + @InputFunction + public static void input(HyperLogLogState state, @SqlType(StandardTypes.DOUBLE) double value) + { + HyperLogLog hll = getOrCreateHyperLogLog(state); + state.addMemoryUsage(-hll.estimatedInMemorySize()); + hll.add(Double.doubleToLongBits(value)); + state.addMemoryUsage(hll.estimatedInMemorySize()); + } + + @InputFunction + public static void input(HyperLogLogState state, @SqlType(StandardTypes.VARCHAR) Slice value) + { + HyperLogLog hll = getOrCreateHyperLogLog(state); + state.addMemoryUsage(-hll.estimatedInMemorySize()); + hll.add(value); + state.addMemoryUsage(hll.estimatedInMemorySize()); + } + + @InputFunction + public static void input(HyperLogLogState state, @SqlType(StandardTypes.BIGINT) long value) + { + HyperLogLog hll = getOrCreateHyperLogLog(state); + state.addMemoryUsage(-hll.estimatedInMemorySize()); + hll.add(value); + state.addMemoryUsage(hll.estimatedInMemorySize()); + } + + private static HyperLogLog getOrCreateHyperLogLog(HyperLogLogState state) + { + HyperLogLog hll = state.getHyperLogLog(); + if (hll == null) { + hll = HyperLogLog.newInstance(NUMBER_OF_BUCKETS); + state.setHyperLogLog(hll); + state.addMemoryUsage(hll.estimatedInMemorySize()); + } + return hll; + } + + @CombineFunction + public static void combineState(HyperLogLogState state, HyperLogLogState otherState) + { + HyperLogLog input = otherState.getHyperLogLog(); + + HyperLogLog previous = state.getHyperLogLog(); + if (previous == null) { + state.setHyperLogLog(input); + state.addMemoryUsage(input.estimatedInMemorySize()); + } + else { + state.addMemoryUsage(-previous.estimatedInMemorySize()); + previous.mergeWith(input); + state.addMemoryUsage(previous.estimatedInMemorySize()); + } + } + + @OutputFunction(StandardTypes.HYPER_LOG_LOG) + public static void evaluateFinal(HyperLogLogState state, BlockBuilder out) + { + SERIALIZER.serialize(state, out); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/ApproximateSumAggregations.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/ApproximateSumAggregations.java new file mode 100644 index 00000000..da3741a2 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/ApproximateSumAggregations.java @@ -0,0 +1,120 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.facebook.presto.operator.aggregation.state.VarianceState; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.type.SqlType; +import io.airlift.slice.Slices; + +import static com.facebook.presto.operator.aggregation.AggregationUtils.mergeVarianceState; +import static com.facebook.presto.operator.aggregation.AggregationUtils.updateVarianceState; +import static com.facebook.presto.operator.aggregation.ApproximateUtils.formatApproximateResult; +import static com.facebook.presto.operator.aggregation.ApproximateUtils.sumError; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; + +@AggregationFunction(value = "sum", approximate = true) +public final class ApproximateSumAggregations +{ + private ApproximateSumAggregations() {} + + @InputFunction + public static void input(ApproximateDoubleSumState state, @SqlType(StandardTypes.DOUBLE) double value, @SampleWeight long sampleWeight) + { + state.setWeightedCount(state.getWeightedCount() + sampleWeight); + state.setSum(state.getSum() + value * sampleWeight); + updateVarianceState(state, value); + } + + @CombineFunction + public static void combine(ApproximateDoubleSumState state, ApproximateDoubleSumState otherState) + { + state.setSum(state.getSum() + otherState.getSum()); + state.setWeightedCount(state.getWeightedCount() + otherState.getWeightedCount()); + mergeVarianceState(state, otherState); + } + + @OutputFunction(StandardTypes.VARCHAR) + public static void output(ApproximateDoubleSumState state, double confidence, BlockBuilder out) + { + if (state.getWeightedCount() == 0) { + out.appendNull(); + return; + } + + String result = formatApproximateResult( + state.getSum(), + sumError(state.getCount(), state.getWeightedCount(), state.getM2(), state.getMean()), + confidence, + false); + VARCHAR.writeSlice(out, Slices.utf8Slice(result)); + } + + @InputFunction + public static void input(ApproximateLongSumState state, @SqlType(StandardTypes.BIGINT) long value, @SampleWeight long sampleWeight) + { + state.setWeightedCount(state.getWeightedCount() + sampleWeight); + state.setSum(state.getSum() + value * sampleWeight); + updateVarianceState(state, value); + } + + @CombineFunction + public static void combine(ApproximateLongSumState state, ApproximateLongSumState otherState) + { + state.setSum(state.getSum() + otherState.getSum()); + state.setWeightedCount(state.getWeightedCount() + otherState.getWeightedCount()); + mergeVarianceState(state, otherState); + } + + @OutputFunction(StandardTypes.VARCHAR) + public static void evaluateFinal(ApproximateLongSumState state, double confidence, BlockBuilder out) + { + if (state.getWeightedCount() == 0) { + out.appendNull(); + return; + } + + String result = formatApproximateResult( + state.getSum(), + sumError(state.getCount(), state.getWeightedCount(), state.getM2(), state.getMean()), + confidence, + true); + VARCHAR.writeSlice(out, Slices.utf8Slice(result)); + } + + public interface ApproximateDoubleSumState + extends VarianceState + { + double getSum(); + + void setSum(double value); + + long getWeightedCount(); + + void setWeightedCount(long value); + } + + public interface ApproximateLongSumState + extends VarianceState + { + long getSum(); + + void setSum(long value); + + long getWeightedCount(); + + void setWeightedCount(long value); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/ApproximateUtils.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/ApproximateUtils.java new file mode 100644 index 00000000..6e41b78a --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/ApproximateUtils.java @@ -0,0 +1,137 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.facebook.presto.spi.block.Block; +import com.google.common.base.Throwables; +import org.apache.commons.math3.distribution.NormalDistribution; +import org.apache.commons.math3.exception.OutOfRangeException; + +import javax.annotation.Nullable; + +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.BooleanType.BOOLEAN; + +public final class ApproximateUtils +{ + private static final NormalDistribution NORMAL_DISTRIBUTION = new NormalDistribution(); + + private ApproximateUtils() + { + } + + public static String formatApproximateResult(double mean, double error, double confidence, boolean integral) + { + double zScore; + try { + zScore = NORMAL_DISTRIBUTION.inverseCumulativeProbability((1 + confidence) / 2); + } + catch (OutOfRangeException e) { + throw Throwables.propagate(e); + } + + StringBuilder builder = new StringBuilder(); + if (integral) { + builder.append((long) mean); + } + else { + builder.append(mean); + } + builder.append(" +/- "); + if (integral) { + builder.append((long) Math.ceil(zScore * error)); + } + else { + builder.append(zScore * error); + } + return builder.toString(); + } + + /** + * Computes the standard deviation for the random variable C = sum(1 / p * Bern(p)) + *

+ * Derivation: + *

+     * Var(C) = Var(sum(1 / p * Bern(p)))
+     *        = sum(Var(1 / p * Bern(p)))   [Bienayme formula]
+     *        = n * Var(1 / p * Bern(p))    [Bern(p) are iid]
+     *        = n * 1 / p^2 * Var(Bern(p))  [1 / p is constant]
+     *        = n * 1 / p^2 * p * (1 - p)   [Variance of a Bernoulli distribution]
+     *        = n * (1 - p) / p
+     *        = samples / p * (1 - p) / p   [samples = n * p, since it's only the observed rows]
+     * 
+ * Therefore Stddev(C) = 1 / p * sqrt(samples * (1 - p)) + */ + public static double countError(long samples, long count) + { + if (count == 0) { + return Double.POSITIVE_INFINITY; + } + + double p = samples / (double) count; + double error = 1 / p * Math.sqrt(samples * (1 - p)); + return conservativeError(error, p, samples); + } + + /** + * Computes the standard deviation for the random variable S = sum(1 / p * X * Bern(p)) + *

+ * Derivation: + *
+     * Var(S) = Var(sum(1 / p * X * Bern(p)))
+     *        = sum(Var(1 / p * X * Bern(p)))                                                           [Bienayme formula]
+     *        = n * Var(1 / p * X * Bern(p))                                                            [X * Bern(p) are iid]
+     *        = n * 1 / p^2 * Var(X * Bern(p))                                                          [1 / p is constant]
+     *        = n * 1 / p^2 * (Var(X) * Var(Bern(p)) + E(X)^2 * Var(Bern(p)) + Var(X) * E(Bern(p))^2    [Product of independent variables]
+     *        = n * 1 / p^2 * (Var(X) * p(1 - p) + E(X)^2 * p(1 - p) + Var(X) * p^2)                    [Variance of a Bernoulli distribution]
+     *        = n * 1 / p * (Var(X) + E(X)^2 * (1 - p))
+     *        = samples / p^2 * (Var(X) + E(X)^2 * (1 - p))                                             [samples = n * p, since it's only the observed rows]
+     * 
+ * Therefore Stddev(S) = 1 / p * sqrt(samples * (variance + mean^2 * (1 - p))) + */ + public static double sumError(long samples, long count, double m2, double mean) + { + if (count == 0) { + return Double.POSITIVE_INFINITY; + } + + double p = samples / (double) count; + double variance = m2 / samples; + double error = 1 / p * Math.sqrt(samples * (variance + mean * mean * (1 - p))); + return conservativeError(error, p, samples); + } + + private static double conservativeError(double error, double p, double samples) + { + // Heuristic to determine that the sample is too small + if (p < 0.01 && samples < 100) { + return Double.POSITIVE_INFINITY; + } + return error; + } + + public static long computeSampleWeight(@Nullable Block masks, @Nullable Block sampleWeights, int index) + { + if (masks != null) { + // TODO: support for DISTINCT should be removed from sampled aggregations, + // since it doesn't make sense to try to process distinct rows when the data is sampled. + + // DISTINCT is enabled, so ignore the sample weight + return BOOLEAN.getBoolean(masks, index) ? 1 : 0; + } + else { + return sampleWeights != null ? BIGINT.getLong(sampleWeights, index) : 1; + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/ArbitraryAggregation.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/ArbitraryAggregation.java new file mode 100644 index 00000000..83e923c8 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/ArbitraryAggregation.java @@ -0,0 +1,129 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.facebook.presto.byteCode.DynamicClassLoader; +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.ParametricAggregation; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.operator.aggregation.state.ArbitraryAggregationState; +import com.facebook.presto.operator.aggregation.state.ArbitraryAggregationStateFactory; +import com.facebook.presto.operator.aggregation.state.ArbitraryAggregationStateSerializer; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.google.common.collect.ImmutableList; + +import java.lang.invoke.MethodHandle; +import java.util.List; +import java.util.Map; + +import static com.facebook.presto.metadata.Signature.typeParameter; +import static com.facebook.presto.operator.aggregation.AggregationMetadata.ParameterMetadata; +import static com.facebook.presto.operator.aggregation.AggregationMetadata.ParameterMetadata.ParameterType.BLOCK_INDEX; +import static com.facebook.presto.operator.aggregation.AggregationMetadata.ParameterMetadata.ParameterType.BLOCK_INPUT_CHANNEL; +import static com.facebook.presto.operator.aggregation.AggregationMetadata.ParameterMetadata.ParameterType.STATE; +import static com.facebook.presto.operator.aggregation.AggregationUtils.generateAggregationName; +import static com.facebook.presto.util.Reflection.methodHandle; + +public class ArbitraryAggregation + extends ParametricAggregation +{ + public static final ArbitraryAggregation ARBITRARY_AGGREGATION = new ArbitraryAggregation(); + private static final String NAME = "arbitrary"; + private static final MethodHandle OUTPUT_FUNCTION = methodHandle(ArbitraryAggregation.class, "output", ArbitraryAggregationState.class, BlockBuilder.class); + private static final MethodHandle INPUT_FUNCTION = methodHandle(ArbitraryAggregation.class, "input", ArbitraryAggregationState.class, Block.class, int.class); + private static final MethodHandle COMBINE_FUNCTION = methodHandle(ArbitraryAggregation.class, "combine", ArbitraryAggregationState.class, ArbitraryAggregationState.class); + private static final Signature SIGNATURE = new Signature(NAME, ImmutableList.of(typeParameter("T")), "T", ImmutableList.of("T"), false, false); + + @Override + public Signature getSignature() + { + return SIGNATURE; + } + + @Override + public String getDescription() + { + return "return an arbitrary non-null input value"; + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + Type valueType = types.get("T"); + Signature signature = new Signature(NAME, valueType.getTypeSignature(), valueType.getTypeSignature()); + InternalAggregationFunction aggregation = generateAggregation(valueType); + return new FunctionInfo(signature, getDescription(), aggregation); + } + + private static InternalAggregationFunction generateAggregation(Type valueType) + { + DynamicClassLoader classLoader = new DynamicClassLoader(ArbitraryAggregation.class.getClassLoader()); + + ArbitraryAggregationStateSerializer stateSerializer = new ArbitraryAggregationStateSerializer(); + Type intermediateType = stateSerializer.getSerializedType(); + + List inputTypes = ImmutableList.of(valueType); + + ArbitraryAggregationStateFactory stateFactory = new ArbitraryAggregationStateFactory(valueType); + AggregationMetadata metadata = new AggregationMetadata( + generateAggregationName(NAME, valueType, inputTypes), + createInputParameterMetadata(valueType), + INPUT_FUNCTION, + null, + null, + COMBINE_FUNCTION, + OUTPUT_FUNCTION, + ArbitraryAggregationState.class, + stateSerializer, + stateFactory, + valueType, + false); + + GenericAccumulatorFactoryBinder factory = new AccumulatorCompiler().generateAccumulatorFactoryBinder(metadata, classLoader); + return new InternalAggregationFunction(NAME, inputTypes, intermediateType, valueType, true, false, factory); + } + + private static List createInputParameterMetadata(Type value) + { + return ImmutableList.of(new ParameterMetadata(STATE), new ParameterMetadata(BLOCK_INPUT_CHANNEL, value), new ParameterMetadata(BLOCK_INDEX)); + } + + public static void input(ArbitraryAggregationState state, Block value, int position) + { + if (state.getValue() == null) { + state.setValue(value.getSingleValueBlock(position)); + } + } + + public static void combine(ArbitraryAggregationState state, ArbitraryAggregationState otherState) + { + if (state.getValue() == null && otherState.getValue() != null) { + state.setValue(otherState.getValue()); + } + } + + public static void output(ArbitraryAggregationState state, BlockBuilder out) + { + if (state.getValue() == null) { + out.appendNull(); + } + else { + state.getType().appendTo(state.getValue(), 0, out); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/ArrayAggregation.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/ArrayAggregation.java new file mode 100644 index 00000000..ee04e056 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/ArrayAggregation.java @@ -0,0 +1,163 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.facebook.presto.byteCode.DynamicClassLoader; +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.ParametricAggregation; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.operator.aggregation.AggregationMetadata.ParameterMetadata; +import com.facebook.presto.operator.aggregation.state.AccumulatorState; +import com.facebook.presto.operator.aggregation.state.AccumulatorStateFactory; +import com.facebook.presto.operator.aggregation.state.AccumulatorStateSerializer; +import com.facebook.presto.operator.aggregation.state.ArrayAggregationState; +import com.facebook.presto.operator.aggregation.state.ArrayAggregationStateFactory; +import com.facebook.presto.operator.aggregation.state.ArrayAggregationStateSerializer; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.block.BlockBuilderStatus; +import com.facebook.presto.spi.block.VariableWidthBlockBuilder; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.type.ArrayType; +import com.google.common.collect.ImmutableList; + +import java.lang.invoke.MethodHandle; +import java.util.List; +import java.util.Map; + +import static com.facebook.presto.metadata.Signature.typeParameter; +import static com.facebook.presto.operator.aggregation.AggregationMetadata.ParameterMetadata.ParameterType.BLOCK_INDEX; +import static com.facebook.presto.operator.aggregation.AggregationMetadata.ParameterMetadata.ParameterType.BLOCK_INPUT_CHANNEL; +import static com.facebook.presto.operator.aggregation.AggregationMetadata.ParameterMetadata.ParameterType.STATE; +import static com.facebook.presto.operator.aggregation.AggregationUtils.generateAggregationName; +import static com.facebook.presto.spi.type.VarbinaryType.VARBINARY; +import static com.facebook.presto.type.TypeUtils.buildStructuralSlice; +import static com.facebook.presto.type.TypeUtils.parameterizedTypeName; +import static com.facebook.presto.util.Reflection.methodHandle; + +public class ArrayAggregation + extends ParametricAggregation +{ + public static final ArrayAggregation ARRAY_AGGREGATION = new ArrayAggregation(); + private static final String NAME = "array_agg"; + private static final MethodHandle INPUT_FUNCTION = methodHandle(ArrayAggregation.class, "input", Type.class, ArrayAggregationState.class, Block.class, int.class); + private static final MethodHandle COMBINE_FUNCTION = methodHandle(ArrayAggregation.class, "combine", Type.class, ArrayAggregationState.class, ArrayAggregationState.class); + private static final MethodHandle OUTPUT_FUNCTION = methodHandle(ArrayAggregation.class, "output", ArrayAggregationState.class, BlockBuilder.class); + private static final Signature SIGNATURE = new Signature(NAME, ImmutableList.of(typeParameter("T")), "array", ImmutableList.of("T"), false, false); + + @Override + public Signature getSignature() + { + return SIGNATURE; + } + + @Override + public String getDescription() + { + return "return an array of values"; + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + Type type = types.get("T"); + Signature signature = new Signature(NAME, + parameterizedTypeName("array", type.getTypeSignature()), + type.getTypeSignature()); + InternalAggregationFunction aggregation = generateAggregation(type); + return new FunctionInfo(signature, getDescription(), aggregation); + } + + private InternalAggregationFunction generateAggregation(Type type) + { + DynamicClassLoader classLoader = new DynamicClassLoader(ArrayAggregation.class.getClassLoader()); + + AccumulatorStateSerializer stateSerializer = new ArrayAggregationStateSerializer(type); + AccumulatorStateFactory stateFactory = new ArrayAggregationStateFactory(); + MethodHandle inputFunction = INPUT_FUNCTION.bindTo(type); + MethodHandle combineFunction = COMBINE_FUNCTION.bindTo(type); + MethodHandle outputFunction = OUTPUT_FUNCTION; + Class stateInterface = ArrayAggregationState.class; + + List inputTypes = ImmutableList.of(type); + Type outputType = new ArrayType(type); + Type intermediateType = stateSerializer.getSerializedType(); + List inputParameterMetadata = createInputParameterMetadata(type); + AggregationMetadata metadata = new AggregationMetadata( + generateAggregationName(NAME, type, inputTypes), + inputParameterMetadata, + inputFunction, + null, + null, + combineFunction, + outputFunction, + stateInterface, + stateSerializer, + stateFactory, + outputType, + false); + + GenericAccumulatorFactoryBinder factory = new AccumulatorCompiler().generateAccumulatorFactoryBinder(metadata, classLoader); + return new InternalAggregationFunction(NAME, inputTypes, intermediateType, outputType, true, false, factory); + } + + private static List createInputParameterMetadata(Type value) + { + return ImmutableList.of(new ParameterMetadata(STATE), new ParameterMetadata(BLOCK_INPUT_CHANNEL, value), new ParameterMetadata(BLOCK_INDEX)); + } + + public static void input(Type type, ArrayAggregationState state, Block value, int position) + { + BlockBuilder blockBuilder = state.getBlockBuilder(); + if (blockBuilder == null) { + blockBuilder = new VariableWidthBlockBuilder(new BlockBuilderStatus()); // not using type.createBlockBuilder because fixedWidth ones can't be serialized + state.setBlockBuilder(blockBuilder); + } + long startSize = blockBuilder.getRetainedSizeInBytes(); + type.appendTo(value, position, blockBuilder); + state.addMemoryUsage(blockBuilder.getRetainedSizeInBytes() - startSize); + } + + public static void combine(Type type, ArrayAggregationState state, ArrayAggregationState otherState) + { + BlockBuilder stateBlockBuilder = state.getBlockBuilder(); + BlockBuilder otherStateBlockBuilder = otherState.getBlockBuilder(); + if (otherStateBlockBuilder == null) { + return; + } + if (stateBlockBuilder == null) { + state.setBlockBuilder(otherStateBlockBuilder); + return; + } + int otherPositionCount = otherStateBlockBuilder.getPositionCount(); + long startSize = stateBlockBuilder.getRetainedSizeInBytes(); + for (int i = 0; i < otherPositionCount; i++) { + type.appendTo(otherStateBlockBuilder, i, stateBlockBuilder); + } + state.addMemoryUsage(stateBlockBuilder.getRetainedSizeInBytes() - startSize); + } + + public static void output(ArrayAggregationState state, BlockBuilder out) + { + BlockBuilder stateBlockBuilder = state.getBlockBuilder(); + if (stateBlockBuilder == null || stateBlockBuilder.getPositionCount() == 0) { + out.appendNull(); + } + else { + VARBINARY.writeSlice(out, buildStructuralSlice(stateBlockBuilder)); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/AverageAggregations.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/AverageAggregations.java new file mode 100644 index 00000000..6fca145a --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/AverageAggregations.java @@ -0,0 +1,67 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.facebook.presto.operator.aggregation.state.LongAndDoubleState; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.type.SqlType; +import com.google.common.collect.ImmutableList; + +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; + +@AggregationFunction("avg") +public final class AverageAggregations +{ + public static final InternalAggregationFunction LONG_AVERAGE = new AggregationCompiler().generateAggregationFunction(AverageAggregations.class, DOUBLE, ImmutableList.of(BIGINT)); + public static final InternalAggregationFunction DOUBLE_AVERAGE = new AggregationCompiler().generateAggregationFunction(AverageAggregations.class, DOUBLE, ImmutableList.of(DOUBLE)); + + private AverageAggregations() {} + + @InputFunction + public static void input(LongAndDoubleState state, @SqlType(StandardTypes.BIGINT) long value) + { + state.setLong(state.getLong() + 1); + state.setDouble(state.getDouble() + value); + } + + @InputFunction + public static void input(LongAndDoubleState state, @SqlType(StandardTypes.DOUBLE) double value) + { + state.setLong(state.getLong() + 1); + state.setDouble(state.getDouble() + value); + } + + @CombineFunction + public static void combine(LongAndDoubleState state, LongAndDoubleState otherState) + { + state.setLong(state.getLong() + otherState.getLong()); + state.setDouble(state.getDouble() + otherState.getDouble()); + } + + @OutputFunction(StandardTypes.DOUBLE) + public static void output(LongAndDoubleState state, BlockBuilder out) + { + long count = state.getLong(); + if (count == 0) { + out.appendNull(); + } + else { + double value = state.getDouble(); + DOUBLE.writeDouble(out, value / count); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/BlockIndex.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/BlockIndex.java new file mode 100644 index 00000000..065de6b9 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/BlockIndex.java @@ -0,0 +1,25 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface BlockIndex +{ +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/BlockPosition.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/BlockPosition.java new file mode 100644 index 00000000..b4a62b5d --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/BlockPosition.java @@ -0,0 +1,25 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface BlockPosition +{ +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/BooleanAndAggregation.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/BooleanAndAggregation.java new file mode 100644 index 00000000..36aed571 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/BooleanAndAggregation.java @@ -0,0 +1,44 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.facebook.presto.operator.aggregation.state.TriStateBooleanState; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.type.SqlType; + +import static com.facebook.presto.operator.aggregation.state.TriStateBooleanState.FALSE_VALUE; +import static com.facebook.presto.operator.aggregation.state.TriStateBooleanState.NULL_VALUE; +import static com.facebook.presto.operator.aggregation.state.TriStateBooleanState.TRUE_VALUE; + +@AggregationFunction(value = "bool_and", alias = "every") +public final class BooleanAndAggregation +{ + private BooleanAndAggregation() {} + + @InputFunction + @IntermediateInputFunction + public static void booleanAnd(TriStateBooleanState state, @SqlType(StandardTypes.BOOLEAN) boolean value) + { + // if the value is false, the result is false + if (!value) { + state.setByte(FALSE_VALUE); + } + else { + // if the current value is unset, set result to true + if (state.getByte() == NULL_VALUE) { + state.setByte(TRUE_VALUE); + } + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/BooleanOrAggregation.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/BooleanOrAggregation.java new file mode 100644 index 00000000..f44d5ad6 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/BooleanOrAggregation.java @@ -0,0 +1,44 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.facebook.presto.operator.aggregation.state.TriStateBooleanState; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.type.SqlType; + +import static com.facebook.presto.operator.aggregation.state.TriStateBooleanState.FALSE_VALUE; +import static com.facebook.presto.operator.aggregation.state.TriStateBooleanState.NULL_VALUE; +import static com.facebook.presto.operator.aggregation.state.TriStateBooleanState.TRUE_VALUE; + +@AggregationFunction(value = "bool_or") +public final class BooleanOrAggregation +{ + private BooleanOrAggregation() {} + + @InputFunction + @IntermediateInputFunction + public static void booleanOr(TriStateBooleanState state, @SqlType(StandardTypes.BOOLEAN) boolean value) + { + // if value is true, the result is true + if (value) { + state.setByte(TRUE_VALUE); + } + else { + // if the current value is unset, set result to false + if (state.getByte() == NULL_VALUE) { + state.setByte(FALSE_VALUE); + } + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/CombineFunction.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/CombineFunction.java new file mode 100644 index 00000000..71a02661 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/CombineFunction.java @@ -0,0 +1,25 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface CombineFunction +{ +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/CorrelationAggregation.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/CorrelationAggregation.java new file mode 100644 index 00000000..af5c5257 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/CorrelationAggregation.java @@ -0,0 +1,60 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.facebook.presto.operator.aggregation.state.CorrelationState; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.type.SqlType; + +import static com.facebook.presto.operator.aggregation.AggregationUtils.mergeCorrelationState; +import static com.facebook.presto.operator.aggregation.AggregationUtils.updateCorrelationState; +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; + +@AggregationFunction("corr") +public class CorrelationAggregation +{ + private CorrelationAggregation() {} + + @InputFunction + public static void input(CorrelationState state, @SqlType(StandardTypes.DOUBLE) double dependentValue, @SqlType(StandardTypes.DOUBLE) double independentValue) + { + updateCorrelationState(state, independentValue, dependentValue); + } + + @CombineFunction + public static void combine(CorrelationState state, CorrelationState otherState) + { + mergeCorrelationState(state, otherState); + } + + @OutputFunction(StandardTypes.DOUBLE) + public static void corr(CorrelationState state, BlockBuilder out) + { + // Math comes from ISO9075-2:2011(E) 10.9 General Rules 7 c x + double dividend = state.getCount() * state.getSumXY() - state.getSumX() * state.getSumY(); + dividend = dividend * dividend; + double divisor1 = state.getCount() * state.getSumXSquare() - state.getSumX() * state.getSumX(); + double divisor2 = state.getCount() * state.getSumYSquare() - state.getSumY() * state.getSumY(); + + // divisor1 and divisor2 deliberately not checked for zero because the result can be Infty or NaN even if they are both not zero + double result = dividend / divisor1 / divisor2; // When the left expression yields a finite value, dividend / (divisor1 * divisor2) can yield Infty or NaN. + if (Double.isFinite(result)) { + DOUBLE.writeDouble(out, Math.sqrt(result)); // sqrt cannot turn finite value to non-finite value + } + else { + out.appendNull(); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/CountAggregation.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/CountAggregation.java new file mode 100644 index 00000000..eafab558 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/CountAggregation.java @@ -0,0 +1,38 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.facebook.presto.operator.aggregation.state.LongState; + +@AggregationFunction("count") +public final class CountAggregation +{ + public static final InternalAggregationFunction COUNT = new AggregationCompiler().generateAggregationFunction(CountAggregation.class); + + private CountAggregation() + { + } + + @InputFunction + public static void input(LongState state) + { + state.setLong(state.getLong() + 1); + } + + @CombineFunction + public static void combine(LongState state, LongState otherState) + { + state.setLong(state.getLong() + otherState.getLong()); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/CountColumn.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/CountColumn.java new file mode 100644 index 00000000..94960924 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/CountColumn.java @@ -0,0 +1,117 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.facebook.presto.byteCode.DynamicClassLoader; +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.ParametricAggregation; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.operator.aggregation.state.AccumulatorStateFactory; +import com.facebook.presto.operator.aggregation.state.AccumulatorStateSerializer; +import com.facebook.presto.operator.aggregation.state.LongState; +import com.facebook.presto.operator.aggregation.state.StateCompiler; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.google.common.collect.ImmutableList; + +import java.lang.invoke.MethodHandle; +import java.util.List; +import java.util.Map; + +import static com.facebook.presto.metadata.Signature.typeParameter; +import static com.facebook.presto.operator.aggregation.AggregationMetadata.ParameterMetadata; +import static com.facebook.presto.operator.aggregation.AggregationMetadata.ParameterMetadata.ParameterType.BLOCK_INDEX; +import static com.facebook.presto.operator.aggregation.AggregationMetadata.ParameterMetadata.ParameterType.BLOCK_INPUT_CHANNEL; +import static com.facebook.presto.operator.aggregation.AggregationMetadata.ParameterMetadata.ParameterType.STATE; +import static com.facebook.presto.operator.aggregation.AggregationUtils.generateAggregationName; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.TypeSignature.parseTypeSignature; +import static com.facebook.presto.util.Reflection.methodHandle; + +public class CountColumn + extends ParametricAggregation +{ + public static final CountColumn COUNT_COLUMN = new CountColumn(); + private static final String NAME = "count"; + private static final Signature SIGNATURE = new Signature(NAME, ImmutableList.of(typeParameter("T")), StandardTypes.BIGINT, ImmutableList.of("T"), false, false); + private static final MethodHandle INPUT_FUNCTION = methodHandle(CountColumn.class, "input", LongState.class, Block.class, int.class); + private static final MethodHandle COMBINE_FUNCTION = methodHandle(CountColumn.class, "combine", LongState.class, LongState.class); + + @Override + public Signature getSignature() + { + return SIGNATURE; + } + + @Override + public String getDescription() + { + return "Counts the non-null values"; + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + Type type = types.get("T"); + Signature signature = new Signature(NAME, parseTypeSignature(StandardTypes.BIGINT), type.getTypeSignature()); + InternalAggregationFunction aggregation = generateAggregation(type); + return new FunctionInfo(signature, getDescription(), aggregation); + } + + private static InternalAggregationFunction generateAggregation(Type type) + { + DynamicClassLoader classLoader = new DynamicClassLoader(CountColumn.class.getClassLoader()); + + AccumulatorStateSerializer stateSerializer = new StateCompiler().generateStateSerializer(LongState.class, classLoader); + AccumulatorStateFactory stateFactory = new StateCompiler().generateStateFactory(LongState.class, classLoader); + Type intermediateType = stateSerializer.getSerializedType(); + + List inputTypes = ImmutableList.of(type); + + AggregationMetadata metadata = new AggregationMetadata( + generateAggregationName(NAME, BIGINT, inputTypes), + createInputParameterMetadata(type), + INPUT_FUNCTION, + null, + null, + COMBINE_FUNCTION, + null, + LongState.class, + stateSerializer, + stateFactory, + BIGINT, + false); + + GenericAccumulatorFactoryBinder factory = new AccumulatorCompiler().generateAccumulatorFactoryBinder(metadata, classLoader); + return new InternalAggregationFunction(NAME, inputTypes, intermediateType, BIGINT, true, false, factory); + } + + private static List createInputParameterMetadata(Type type) + { + return ImmutableList.of(new ParameterMetadata(STATE), new ParameterMetadata(BLOCK_INPUT_CHANNEL, type), new ParameterMetadata(BLOCK_INDEX)); + } + + public static void input(LongState state, Block block, int index) + { + state.setLong(state.getLong() + 1); + } + + public static void combine(LongState state, LongState otherState) + { + state.setLong(state.getLong() + otherState.getLong()); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/CountIfAggregation.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/CountIfAggregation.java new file mode 100644 index 00000000..df396d5c --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/CountIfAggregation.java @@ -0,0 +1,38 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.facebook.presto.operator.aggregation.state.LongState; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.type.SqlType; + +@AggregationFunction("count_if") +public final class CountIfAggregation +{ + private CountIfAggregation() {} + + @InputFunction + public static void input(LongState state, @SqlType(StandardTypes.BOOLEAN) boolean value) + { + if (value) { + state.setLong(state.getLong() + 1); + } + } + + @CombineFunction + public static void combine(LongState state, LongState otherState) + { + state.setLong(state.getLong() + otherState.getLong()); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/CovarianceAggregation.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/CovarianceAggregation.java new file mode 100644 index 00000000..0f18b661 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/CovarianceAggregation.java @@ -0,0 +1,67 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.facebook.presto.operator.aggregation.state.CovarianceState; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.type.SqlType; + +import static com.facebook.presto.operator.aggregation.AggregationUtils.mergeCovarianceState; +import static com.facebook.presto.operator.aggregation.AggregationUtils.updateCovarianceState; +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; + +@AggregationFunction("") +public class CovarianceAggregation +{ + private CovarianceAggregation() {} + + @InputFunction + public static void input(CovarianceState state, @SqlType(StandardTypes.DOUBLE) double dependentValue, @SqlType(StandardTypes.DOUBLE) double independentValue) + { + updateCovarianceState(state, independentValue, dependentValue); + } + + @CombineFunction + public static void combine(CovarianceState state, CovarianceState otherState) + { + mergeCovarianceState(state, otherState); + } + + @AggregationFunction("covar_samp") + @OutputFunction(StandardTypes.DOUBLE) + public static void covarSamp(CovarianceState state, BlockBuilder out) + { + if (state.getCount() <= 1) { + out.appendNull(); + } + else { + double result = (state.getSumXY() - state.getSumX() * state.getSumY() / state.getCount()) / (state.getCount() - 1); + DOUBLE.writeDouble(out, result); + } + } + + @AggregationFunction("covar_pop") + @OutputFunction(StandardTypes.DOUBLE) + public static void covarPop(CovarianceState state, BlockBuilder out) + { + if (state.getCount() == 0) { + out.appendNull(); + } + else { + double result = (state.getSumXY() - state.getSumX() * state.getSumY() / state.getCount()) / state.getCount(); + DOUBLE.writeDouble(out, result); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/DoubleSumAggregation.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/DoubleSumAggregation.java new file mode 100644 index 00000000..56ed4848 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/DoubleSumAggregation.java @@ -0,0 +1,34 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.facebook.presto.operator.aggregation.state.NullableDoubleState; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.type.SqlType; + +@AggregationFunction("sum") +public final class DoubleSumAggregation +{ + public static final InternalAggregationFunction DOUBLE_SUM = new AggregationCompiler().generateAggregationFunction(DoubleSumAggregation.class); + + private DoubleSumAggregation() {} + + @InputFunction + @IntermediateInputFunction + public static void sum(NullableDoubleState state, @SqlType(StandardTypes.DOUBLE) double value) + { + state.setNull(false); + state.setDouble(state.getDouble() + value); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/GenericAccumulatorFactory.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/GenericAccumulatorFactory.java new file mode 100644 index 00000000..74505178 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/GenericAccumulatorFactory.java @@ -0,0 +1,109 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.facebook.presto.operator.aggregation.state.AccumulatorStateFactory; +import com.facebook.presto.operator.aggregation.state.AccumulatorStateSerializer; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.List; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class GenericAccumulatorFactory + implements AccumulatorFactory +{ + private final AccumulatorStateSerializer stateSerializer; + private final AccumulatorStateFactory stateFactory; + private final Constructor accumulatorConstructor; + private final Constructor groupedAccumulatorConstructor; + private final Optional maskChannel; + private final Optional sampleWeightChannel; + private final double confidence; + private final List inputChannels; + + public GenericAccumulatorFactory( + AccumulatorStateSerializer stateSerializer, + AccumulatorStateFactory stateFactory, + Constructor accumulatorConstructor, + Constructor groupedAccumulatorConstructor, + List inputChannels, + Optional maskChannel, + Optional sampleWeightChannel, + double confidence) + { + this.stateSerializer = checkNotNull(stateSerializer, "stateSerializer is null"); + this.stateFactory = checkNotNull(stateFactory, "stateFactory is null"); + this.accumulatorConstructor = checkNotNull(accumulatorConstructor, "accumulatorConstructor is null"); + this.groupedAccumulatorConstructor = checkNotNull(groupedAccumulatorConstructor, "groupedAccumulatorConstructor is null"); + this.maskChannel = checkNotNull(maskChannel, "maskChannel is null"); + this.sampleWeightChannel = checkNotNull(sampleWeightChannel, "sampleWeightChannel is null"); + this.confidence = confidence; + this.inputChannels = ImmutableList.copyOf(checkNotNull(inputChannels, "inputChannels is null")); + } + + @Override + public List getInputChannels() + { + return inputChannels; + } + + @Override + public Accumulator createAccumulator() + { + try { + return accumulatorConstructor.newInstance(stateSerializer, stateFactory, inputChannels, maskChannel, sampleWeightChannel, confidence); + } + catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw Throwables.propagate(e); + } + } + + @Override + public Accumulator createIntermediateAccumulator() + { + try { + return accumulatorConstructor.newInstance(stateSerializer, stateFactory, ImmutableList.of(), Optional.empty(), Optional.empty(), confidence); + } + catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw Throwables.propagate(e); + } + } + + @Override + public GroupedAccumulator createGroupedAccumulator() + { + try { + return groupedAccumulatorConstructor.newInstance(stateSerializer, stateFactory, inputChannels, maskChannel, sampleWeightChannel, confidence); + } + catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw Throwables.propagate(e); + } + } + + @Override + public GroupedAccumulator createGroupedIntermediateAccumulator() + { + try { + return groupedAccumulatorConstructor.newInstance(stateSerializer, stateFactory, ImmutableList.of(), maskChannel, Optional.empty(), confidence); + } + catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw Throwables.propagate(e); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/GenericAccumulatorFactoryBinder.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/GenericAccumulatorFactoryBinder.java new file mode 100644 index 00000000..15b11eed --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/GenericAccumulatorFactoryBinder.java @@ -0,0 +1,78 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.facebook.presto.operator.aggregation.state.AccumulatorStateFactory; +import com.facebook.presto.operator.aggregation.state.AccumulatorStateSerializer; +import com.google.common.base.Throwables; + +import java.lang.reflect.Constructor; +import java.util.List; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class GenericAccumulatorFactoryBinder + implements AccumulatorFactoryBinder +{ + private final boolean approximationSupported; + private final AccumulatorStateSerializer stateSerializer; + private final AccumulatorStateFactory stateFactory; + private final Constructor accumulatorConstructor; + private final Constructor groupedAccumulatorConstructor; + + public GenericAccumulatorFactoryBinder( + AccumulatorStateSerializer stateSerializer, + AccumulatorStateFactory stateFactory, + Class accumulatorClass, + Class groupedAccumulatorClass, + boolean approximationSupported) + { + this.stateSerializer = checkNotNull(stateSerializer, "stateSerializer is null"); + this.stateFactory = checkNotNull(stateFactory, "stateFactory is null"); + this.approximationSupported = approximationSupported; + + try { + accumulatorConstructor = accumulatorClass.getConstructor( + AccumulatorStateSerializer.class, + AccumulatorStateFactory.class, + List.class, + Optional.class, + Optional.class, + double.class); + + groupedAccumulatorConstructor = groupedAccumulatorClass.getConstructor( + AccumulatorStateSerializer.class, + AccumulatorStateFactory.class, + List.class, + Optional.class, + Optional.class, + double.class); + } + catch (NoSuchMethodException e) { + throw Throwables.propagate(e); + } + } + + @Override + public AccumulatorFactory bind(List argumentChannels, Optional maskChannel, Optional sampleWeightChannel, double confidence) + { + if (!approximationSupported) { + checkArgument(confidence == 1.0, "Approximate queries not supported"); + checkArgument(!sampleWeightChannel.isPresent(), "Sampled data not supported"); + } + return new GenericAccumulatorFactory(stateSerializer, stateFactory, accumulatorConstructor, groupedAccumulatorConstructor, argumentChannels, maskChannel, sampleWeightChannel, confidence); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/GenericAggregationFunctionFactory.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/GenericAggregationFunctionFactory.java new file mode 100644 index 00000000..03d5c71c --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/GenericAggregationFunctionFactory.java @@ -0,0 +1,51 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.facebook.presto.metadata.FunctionFactory; +import com.facebook.presto.metadata.FunctionListBuilder; +import com.facebook.presto.metadata.ParametricFunction; +import com.facebook.presto.spi.type.TypeManager; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class GenericAggregationFunctionFactory + implements FunctionFactory +{ + private final List aggregations; + + public static GenericAggregationFunctionFactory fromAggregationDefinition(Class clazz, TypeManager typeManager) + { + FunctionListBuilder builder = new FunctionListBuilder(typeManager); + for (InternalAggregationFunction aggregation : new AggregationCompiler(typeManager).generateAggregationFunctions(clazz)) { + builder.aggregate(aggregation); + } + + return new GenericAggregationFunctionFactory(builder.getFunctions()); + } + + private GenericAggregationFunctionFactory(List aggregations) + { + this.aggregations = ImmutableList.copyOf(checkNotNull(aggregations, "aggregations is null")); + } + + @Override + public List listFunctions() + { + return aggregations; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/GroupedAccumulator.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/GroupedAccumulator.java new file mode 100644 index 00000000..eacf54f3 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/GroupedAccumulator.java @@ -0,0 +1,37 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.facebook.presto.operator.GroupByIdBlock; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; + +public interface GroupedAccumulator +{ + long getEstimatedSize(); + + Type getFinalType(); + + Type getIntermediateType(); + + void addInput(GroupByIdBlock groupIdsBlock, Page page); + + void addIntermediate(GroupByIdBlock groupIdsBlock, Block block); + + void evaluateIntermediate(int groupId, BlockBuilder output); + + void evaluateFinal(int groupId, BlockBuilder output); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/InputFunction.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/InputFunction.java new file mode 100644 index 00000000..2e26adf8 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/InputFunction.java @@ -0,0 +1,25 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface InputFunction +{ +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/IntermediateInputFunction.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/IntermediateInputFunction.java new file mode 100644 index 00000000..7ce08f43 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/IntermediateInputFunction.java @@ -0,0 +1,25 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface IntermediateInputFunction +{ +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/InternalAggregationFunction.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/InternalAggregationFunction.java new file mode 100644 index 00000000..e03af1f0 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/InternalAggregationFunction.java @@ -0,0 +1,84 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; + +import java.util.List; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public final class InternalAggregationFunction +{ + private final String name; + private final List parameterTypes; + private final Type intermediateType; + private final Type finalType; + private final boolean decomposable; + private final boolean approximate; + private final AccumulatorFactoryBinder factory; + + public InternalAggregationFunction(String name, List parameterTypes, Type intermediateType, Type finalType, boolean decomposable, boolean approximate, AccumulatorFactoryBinder factory) + { + this.name = checkNotNull(name, "name is null"); + checkArgument(!name.isEmpty(), "name is empty"); + this.parameterTypes = ImmutableList.copyOf(checkNotNull(parameterTypes, "parameterTypes is null")); + this.intermediateType = checkNotNull(intermediateType, "intermediateType is null"); + this.finalType = checkNotNull(finalType, "finalType is null"); + this.decomposable = decomposable; + this.approximate = approximate; + this.factory = checkNotNull(factory, "factory is null"); + } + + public String name() + { + return name; + } + + public List getParameterTypes() + { + return parameterTypes; + } + + public Type getFinalType() + { + return finalType; + } + + public Type getIntermediateType() + { + return intermediateType; + } + + /** + * Indicates that the aggregation can be decomposed, and run as partial aggregations followed by a final aggregation to combine the intermediate results + */ + public boolean isDecomposable() + { + return decomposable; + } + + public boolean isApproximate() + { + return approximate; + } + + public AccumulatorFactory bind(List inputChannels, Optional maskChannel, Optional sampleWeightChannel, double confidence) + { + return factory.bind(inputChannels, maskChannel, sampleWeightChannel, confidence); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/KeyValuePairs.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/KeyValuePairs.java new file mode 100644 index 00000000..3526a580 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/KeyValuePairs.java @@ -0,0 +1,113 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.block.BlockBuilderStatus; +import com.facebook.presto.spi.block.VariableWidthBlockBuilder; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.type.TypeUtils; +import io.airlift.slice.Slice; +import org.openjdk.jol.info.ClassLayout; + +import static com.facebook.presto.type.TypeUtils.buildStructuralSlice; +import static com.facebook.presto.type.TypeUtils.expectedValueSize; +import static com.google.common.base.Preconditions.checkNotNull; + +public class KeyValuePairs +{ + private static final int INSTANCE_SIZE = ClassLayout.parseClass(KeyValuePairs.class).instanceSize(); + private static final int EXPECTED_ENTRIES = 10; + private static final int EXPECTED_ENTRY_SIZE = 16; + + private final TypedSet keySet; + private final BlockBuilder keyBlockBuilder; + private final Type keyType; + + private final BlockBuilder valueBlockBuilder; + private final Type valueType; + + public KeyValuePairs(Type keyType, Type valueType) + { + checkNotNull(keyType, "keyType is null"); + checkNotNull(valueType, "valueType is null"); + + this.keyType = keyType; + this.valueType = valueType; + this.keySet = new SimpleTypedSet(keyType, EXPECTED_ENTRIES); + keyBlockBuilder = this.keyType.createBlockBuilder(new BlockBuilderStatus(), EXPECTED_ENTRIES, expectedValueSize(keyType, EXPECTED_ENTRY_SIZE)); + valueBlockBuilder = this.valueType.createBlockBuilder(new BlockBuilderStatus(), EXPECTED_ENTRIES, expectedValueSize(valueType, EXPECTED_ENTRY_SIZE)); + } + + public KeyValuePairs(Slice serialized, Type keyType, Type valueType) + { + this(keyType, valueType); + checkNotNull(serialized, "serialized is null"); + deserialize(serialized); + } + + public Block getKeys() + { + return keyBlockBuilder.build(); + } + + public Block getValues() + { + return valueBlockBuilder.build(); + } + + private void deserialize(Slice serialized) + { + Block block = TypeUtils.readStructuralBlock(serialized); + for (int i = 0; i < block.getPositionCount(); i += 2) { + add(block, block, i, i + 1); + } + } + + public Slice serialize() + { + Block values = valueBlockBuilder.build(); + Block keys = keyBlockBuilder.build(); + BlockBuilder blockBuilder = new VariableWidthBlockBuilder(new BlockBuilderStatus(), keys.getSizeInBytes() + values.getSizeInBytes()); + for (int i = 0; i < keys.getPositionCount(); i++) { + keyType.appendTo(keys, i, blockBuilder); + valueType.appendTo(values, i, blockBuilder); + } + return buildStructuralSlice(blockBuilder); + } + + public long estimatedInMemorySize() + { + long size = INSTANCE_SIZE; + size += keyBlockBuilder.getRetainedSizeInBytes(); + size += valueBlockBuilder.getRetainedSizeInBytes(); + size += keySet.getEstimatedSize(); + return size; + } + + public void add(Block key, Block value, int keyPosition, int valuePosition) + { + if (!keySet.contains(key, keyPosition)) { + keySet.add(key, keyPosition); + keyType.appendTo(key, keyPosition, keyBlockBuilder); + if (value.isNull(valuePosition)) { + valueBlockBuilder.appendNull(); + } + else { + valueType.appendTo(value, valuePosition, valueBlockBuilder); + } + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/LazyAccumulatorFactoryBinder.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/LazyAccumulatorFactoryBinder.java new file mode 100644 index 00000000..d2f7b0bc --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/LazyAccumulatorFactoryBinder.java @@ -0,0 +1,38 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.facebook.presto.byteCode.DynamicClassLoader; +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; + +import java.util.List; +import java.util.Optional; + +public class LazyAccumulatorFactoryBinder + implements AccumulatorFactoryBinder +{ + private final Supplier binder; + + public LazyAccumulatorFactoryBinder(AggregationMetadata metadata, DynamicClassLoader classLoader) + { + binder = Suppliers.memoize(() -> new AccumulatorCompiler().generateAccumulatorFactoryBinder(metadata, classLoader)); + } + + @Override + public AccumulatorFactory bind(List argumentChannels, Optional maskChannel, Optional sampleWeightChannel, double confidence) + { + return binder.get().bind(argumentChannels, maskChannel, sampleWeightChannel, confidence); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/LongSumAggregation.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/LongSumAggregation.java new file mode 100644 index 00000000..51adc887 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/LongSumAggregation.java @@ -0,0 +1,34 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.facebook.presto.operator.aggregation.state.NullableLongState; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.type.SqlType; + +@AggregationFunction("sum") +public final class LongSumAggregation +{ + public static final InternalAggregationFunction LONG_SUM = new AggregationCompiler().generateAggregationFunction(LongSumAggregation.class); + + private LongSumAggregation() {} + + @InputFunction + @IntermediateInputFunction + public static void sum(NullableLongState state, @SqlType(StandardTypes.BIGINT) long value) + { + state.setNull(false); + state.setLong(state.getLong() + value); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/MapAggregation.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/MapAggregation.java new file mode 100644 index 00000000..8ab1f918 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/MapAggregation.java @@ -0,0 +1,170 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.facebook.presto.ExceededMemoryLimitException; +import com.facebook.presto.byteCode.DynamicClassLoader; +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.ParametricAggregation; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.operator.aggregation.state.KeyValuePairStateSerializer; +import com.facebook.presto.operator.aggregation.state.KeyValuePairsState; +import com.facebook.presto.operator.aggregation.state.KeyValuePairsStateFactory; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.type.MapType; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; + +import java.lang.invoke.MethodHandle; +import java.util.List; +import java.util.Map; + +import static com.facebook.presto.metadata.Signature.comparableTypeParameter; +import static com.facebook.presto.metadata.Signature.typeParameter; +import static com.facebook.presto.operator.aggregation.AggregationMetadata.ParameterMetadata; +import static com.facebook.presto.operator.aggregation.AggregationMetadata.ParameterMetadata.ParameterType.BLOCK_INDEX; +import static com.facebook.presto.operator.aggregation.AggregationMetadata.ParameterMetadata.ParameterType.BLOCK_INPUT_CHANNEL; +import static com.facebook.presto.operator.aggregation.AggregationMetadata.ParameterMetadata.ParameterType.NULLABLE_BLOCK_INPUT_CHANNEL; +import static com.facebook.presto.operator.aggregation.AggregationMetadata.ParameterMetadata.ParameterType.STATE; +import static com.facebook.presto.operator.aggregation.AggregationUtils.generateAggregationName; +import static com.facebook.presto.spi.StandardErrorCode.INVALID_FUNCTION_ARGUMENT; +import static com.facebook.presto.util.Reflection.methodHandle; +import static java.lang.String.format; + +public class MapAggregation + extends ParametricAggregation +{ + public static final MapAggregation MAP_AGG = new MapAggregation(); + public static final String NAME = "map_agg"; + private static final MethodHandle OUTPUT_FUNCTION = methodHandle(MapAggregation.class, "output", KeyValuePairsState.class, BlockBuilder.class); + private static final MethodHandle INPUT_FUNCTION = methodHandle(MapAggregation.class, "input", KeyValuePairsState.class, Block.class, Block.class, int.class); + private static final MethodHandle COMBINE_FUNCTION = methodHandle(MapAggregation.class, "combine", KeyValuePairsState.class, KeyValuePairsState.class); + + private static final Signature SIGNATURE = new Signature(NAME, ImmutableList.of(comparableTypeParameter("K"), typeParameter("V")), + "map", ImmutableList.of("K", "V"), false, false); + + @Override + public Signature getSignature() + { + return SIGNATURE; + } + + @Override + public String getDescription() + { + return "Aggregates all the rows (key/value pairs) into a single map"; + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + Type keyType = types.get("K"); + Type valueType = types.get("V"); + Signature signature = new Signature(NAME, new MapType(keyType, valueType).getTypeSignature(), keyType.getTypeSignature(), valueType.getTypeSignature()); + InternalAggregationFunction aggregation = generateAggregation(keyType, valueType); + return new FunctionInfo(signature, getDescription(), aggregation); + } + + private static InternalAggregationFunction generateAggregation(Type keyType, Type valueType) + { + DynamicClassLoader classLoader = new DynamicClassLoader(MapAggregation.class.getClassLoader()); + List inputTypes = ImmutableList.of(keyType, valueType); + Type outputType = new MapType(keyType, valueType); + KeyValuePairStateSerializer stateSerializer = new KeyValuePairStateSerializer(); + Type intermediateType = stateSerializer.getSerializedType(); + + AggregationMetadata metadata = new AggregationMetadata( + generateAggregationName(NAME, outputType, inputTypes), + createInputParameterMetadata(keyType, valueType), + INPUT_FUNCTION, + null, + null, + COMBINE_FUNCTION, + OUTPUT_FUNCTION, + KeyValuePairsState.class, + stateSerializer, + new KeyValuePairsStateFactory(keyType, valueType), + outputType, + false); + + GenericAccumulatorFactoryBinder factory = new AccumulatorCompiler().generateAccumulatorFactoryBinder(metadata, classLoader); + return new InternalAggregationFunction(NAME, inputTypes, intermediateType, outputType, true, false, factory); + } + + private static List createInputParameterMetadata(Type keyType, Type valueType) + { + return ImmutableList.of(new ParameterMetadata(STATE), + new ParameterMetadata(BLOCK_INPUT_CHANNEL, keyType), + new ParameterMetadata(NULLABLE_BLOCK_INPUT_CHANNEL, valueType), + new ParameterMetadata(BLOCK_INDEX)); + } + + public static void input(KeyValuePairsState state, Block key, Block value, int position) + { + KeyValuePairs pairs = state.get(); + if (pairs == null) { + pairs = new KeyValuePairs(state.getKeyType(), state.getValueType()); + state.set(pairs); + } + + long startSize = pairs.estimatedInMemorySize(); + try { + pairs.add(key, value, position, position); + } + catch (ExceededMemoryLimitException e) { + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, format("The result of map_agg may not exceed %s", e.getMaxMemory())); + } + state.addMemoryUsage(pairs.estimatedInMemorySize() - startSize); + } + + public static void combine(KeyValuePairsState state, KeyValuePairsState otherState) + { + if (state.get() != null && otherState.get() != null) { + Block keys = otherState.get().getKeys(); + Block values = otherState.get().getValues(); + KeyValuePairs pairs = state.get(); + long startSize = pairs.estimatedInMemorySize(); + for (int i = 0; i < keys.getPositionCount(); i++) { + try { + pairs.add(keys, values, i, i); + } + catch (ExceededMemoryLimitException e) { + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, format("The result of map_agg may not exceed %s", e.getMaxMemory())); + } + } + state.addMemoryUsage(pairs.estimatedInMemorySize() - startSize); + } + else if (state.get() == null) { + state.set(otherState.get()); + } + } + + public static void output(KeyValuePairsState state, BlockBuilder out) + { + KeyValuePairs pairs = state.get(); + if (pairs == null) { + out.appendNull(); + } + else { + Slice slice = pairs.serialize(); + out.writeBytes(slice, 0, slice.length()); + out.closeEntry(); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/MaxAggregation.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/MaxAggregation.java new file mode 100644 index 00000000..95a849e6 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/MaxAggregation.java @@ -0,0 +1,36 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.facebook.presto.metadata.OperatorType; + +public class MaxAggregation + extends AbstractMinMaxAggregation +{ + private static final OperatorType OPERATOR_TYPE = OperatorType.GREATER_THAN; + private static final String NAME = "max"; + + public static final MaxAggregation MAX_AGGREGATION = new MaxAggregation(); + + public MaxAggregation() + { + super(NAME, OPERATOR_TYPE); + } + + @Override + public String getDescription() + { + return "Returns the maximum value of the argument"; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/MaxBy.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/MaxBy.java new file mode 100644 index 00000000..a39f84bd --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/MaxBy.java @@ -0,0 +1,141 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.facebook.presto.byteCode.DynamicClassLoader; +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.ParametricAggregation; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.operator.aggregation.state.MaxOrMinByState; +import com.facebook.presto.operator.aggregation.state.MaxOrMinByStateFactory; +import com.facebook.presto.operator.aggregation.state.MaxOrMinByStateSerializer; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.google.common.collect.ImmutableList; + +import java.lang.invoke.MethodHandle; +import java.util.List; +import java.util.Map; + +import static com.facebook.presto.metadata.Signature.orderableTypeParameter; +import static com.facebook.presto.metadata.Signature.typeParameter; +import static com.facebook.presto.operator.aggregation.AggregationMetadata.ParameterMetadata; +import static com.facebook.presto.operator.aggregation.AggregationMetadata.ParameterMetadata.ParameterType.BLOCK_INDEX; +import static com.facebook.presto.operator.aggregation.AggregationMetadata.ParameterMetadata.ParameterType.NULLABLE_BLOCK_INPUT_CHANNEL; +import static com.facebook.presto.operator.aggregation.AggregationMetadata.ParameterMetadata.ParameterType.STATE; +import static com.facebook.presto.operator.aggregation.AggregationUtils.generateAggregationName; +import static com.facebook.presto.util.Reflection.methodHandle; + +public class MaxBy + extends ParametricAggregation +{ + public static final MaxBy MAX_BY = new MaxBy(); + private static final String NAME = "max_by"; + private static final MethodHandle OUTPUT_FUNCTION = methodHandle(MaxBy.class, "output", Type.class, MaxOrMinByState.class, BlockBuilder.class); + private static final MethodHandle INPUT_FUNCTION = methodHandle(MaxBy.class, "input", Type.class, MaxOrMinByState.class, Block.class, Block.class, int.class); + private static final MethodHandle COMBINE_FUNCTION = methodHandle(MaxBy.class, "combine", Type.class, MaxOrMinByState.class, MaxOrMinByState.class); + private static final Signature SIGNATURE = new Signature(NAME, ImmutableList.of(orderableTypeParameter("K"), typeParameter("V")), "V", ImmutableList.of("V", "K"), false, false); + + @Override + public Signature getSignature() + { + return SIGNATURE; + } + + @Override + public String getDescription() + { + return "Returns the value of the first argument, associated with the maximum value of the second argument"; + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + Type keyType = types.get("K"); + Type valueType = types.get("V"); + Signature signature = new Signature(NAME, valueType.getTypeSignature(), valueType.getTypeSignature(), keyType.getTypeSignature()); + InternalAggregationFunction aggregation = generateAggregation(valueType, keyType); + return new FunctionInfo(signature, getDescription(), aggregation); + } + + private static InternalAggregationFunction generateAggregation(Type valueType, Type keyType) + { + DynamicClassLoader classLoader = new DynamicClassLoader(MaxBy.class.getClassLoader()); + + MaxOrMinByStateSerializer stateSerializer = new MaxOrMinByStateSerializer(valueType, keyType); + Type intermediateType = stateSerializer.getSerializedType(); + + List inputTypes = ImmutableList.of(valueType, keyType); + + MaxOrMinByStateFactory stateFactory = new MaxOrMinByStateFactory(); + AggregationMetadata metadata = new AggregationMetadata( + generateAggregationName(NAME, valueType, inputTypes), + createInputParameterMetadata(valueType, keyType), + INPUT_FUNCTION.bindTo(keyType), + null, + null, + COMBINE_FUNCTION.bindTo(keyType), + OUTPUT_FUNCTION.bindTo(valueType), + MaxOrMinByState.class, + stateSerializer, + stateFactory, + valueType, + false); + + GenericAccumulatorFactoryBinder factory = new AccumulatorCompiler().generateAccumulatorFactoryBinder(metadata, classLoader); + return new InternalAggregationFunction(NAME, inputTypes, intermediateType, valueType, true, false, factory); + } + + private static List createInputParameterMetadata(Type value, Type key) + { + return ImmutableList.of(new ParameterMetadata(STATE), new ParameterMetadata(NULLABLE_BLOCK_INPUT_CHANNEL, value), new ParameterMetadata(NULLABLE_BLOCK_INPUT_CHANNEL, key), new ParameterMetadata(BLOCK_INDEX)); + } + + public static void input(Type keyType, MaxOrMinByState state, Block value, Block key, int position) + { + if (state.getKey() == null || state.getKey().isNull(0)) { + state.setKey(key.getSingleValueBlock(position)); + state.setValue(value.getSingleValueBlock(position)); + } + else if (keyType.compareTo(key, position, state.getKey(), 0) > 0) { + state.setKey(key.getSingleValueBlock(position)); + state.setValue(value.getSingleValueBlock(position)); + } + } + + public static void combine(Type keyType, MaxOrMinByState state, MaxOrMinByState otherState) + { + if (state.getKey() == null) { + state.setKey(otherState.getKey()); + state.setValue(otherState.getValue()); + } + else if (otherState.getKey() != null && keyType.compareTo(otherState.getKey(), 0, state.getKey(), 0) > 0) { + state.setKey(otherState.getKey()); + state.setValue(otherState.getValue()); + } + } + + public static void output(Type valueType, MaxOrMinByState state, BlockBuilder out) + { + if (state.getValue() == null) { + out.appendNull(); + } + else { + valueType.appendTo(state.getValue(), 0, out); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/MergeHyperLogLogAggregation.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/MergeHyperLogLogAggregation.java new file mode 100644 index 00000000..ef3b76be --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/MergeHyperLogLogAggregation.java @@ -0,0 +1,55 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.facebook.presto.operator.aggregation.state.AccumulatorStateSerializer; +import com.facebook.presto.operator.aggregation.state.HyperLogLogState; +import com.facebook.presto.operator.aggregation.state.StateCompiler; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.type.SqlType; +import io.airlift.slice.Slice; +import io.airlift.stats.cardinality.HyperLogLog; + +@AggregationFunction("merge") +public final class MergeHyperLogLogAggregation +{ + private static final AccumulatorStateSerializer serializer = new StateCompiler().generateStateSerializer(HyperLogLogState.class); + + private MergeHyperLogLogAggregation() {} + + @InputFunction + @IntermediateInputFunction + public static void merge(HyperLogLogState state, @SqlType(StandardTypes.HYPER_LOG_LOG) Slice value) + { + HyperLogLog input = HyperLogLog.newInstance(value); + + HyperLogLog previous = state.getHyperLogLog(); + if (previous == null) { + state.setHyperLogLog(input); + state.addMemoryUsage(input.estimatedInMemorySize()); + } + else { + state.addMemoryUsage(-previous.estimatedInMemorySize()); + previous.mergeWith(input); + state.addMemoryUsage(previous.estimatedInMemorySize()); + } + } + + @OutputFunction(StandardTypes.HYPER_LOG_LOG) + public static void output(HyperLogLogState state, BlockBuilder out) + { + serializer.serialize(state, out); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/MinAggregation.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/MinAggregation.java new file mode 100644 index 00000000..33e6c24b --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/MinAggregation.java @@ -0,0 +1,36 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.facebook.presto.metadata.OperatorType; + +public class MinAggregation + extends AbstractMinMaxAggregation +{ + private static final OperatorType OPERATOR_TYPE = OperatorType.LESS_THAN; + private static final String NAME = "min"; + + public static final MinAggregation MIN_AGGREGATION = new MinAggregation(); + + public MinAggregation() + { + super(NAME, OPERATOR_TYPE); + } + + @Override + public String getDescription() + { + return "Returns the minimum value of the argument"; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/MinBy.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/MinBy.java new file mode 100644 index 00000000..b14d5f9e --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/MinBy.java @@ -0,0 +1,141 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.facebook.presto.byteCode.DynamicClassLoader; +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.ParametricAggregation; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.operator.aggregation.state.MaxOrMinByState; +import com.facebook.presto.operator.aggregation.state.MaxOrMinByStateFactory; +import com.facebook.presto.operator.aggregation.state.MaxOrMinByStateSerializer; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.google.common.collect.ImmutableList; + +import java.lang.invoke.MethodHandle; +import java.util.List; +import java.util.Map; + +import static com.facebook.presto.metadata.Signature.orderableTypeParameter; +import static com.facebook.presto.metadata.Signature.typeParameter; +import static com.facebook.presto.operator.aggregation.AggregationMetadata.ParameterMetadata; +import static com.facebook.presto.operator.aggregation.AggregationMetadata.ParameterMetadata.ParameterType.BLOCK_INDEX; +import static com.facebook.presto.operator.aggregation.AggregationMetadata.ParameterMetadata.ParameterType.NULLABLE_BLOCK_INPUT_CHANNEL; +import static com.facebook.presto.operator.aggregation.AggregationMetadata.ParameterMetadata.ParameterType.STATE; +import static com.facebook.presto.operator.aggregation.AggregationUtils.generateAggregationName; +import static com.facebook.presto.util.Reflection.methodHandle; + +public class MinBy + extends ParametricAggregation +{ + public static final MinBy MIN_BY = new MinBy(); + private static final String NAME = "min_by"; + private static final MethodHandle OUTPUT_FUNCTION = methodHandle(MinBy.class, "output", Type.class, MaxOrMinByState.class, BlockBuilder.class); + private static final MethodHandle INPUT_FUNCTION = methodHandle(MinBy.class, "input", Type.class, MaxOrMinByState.class, Block.class, Block.class, int.class); + private static final MethodHandle COMBINE_FUNCTION = methodHandle(MinBy.class, "combine", Type.class, MaxOrMinByState.class, MaxOrMinByState.class); + private static final Signature SIGNATURE = new Signature(NAME, ImmutableList.of(orderableTypeParameter("K"), typeParameter("V")), "V", ImmutableList.of("V", "K"), false, false); + + @Override + public Signature getSignature() + { + return SIGNATURE; + } + + @Override + public String getDescription() + { + return "Returns the value of the first argument, associated with the minimum value of the second argument"; + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + Type keyType = types.get("K"); + Type valueType = types.get("V"); + Signature signature = new Signature(NAME, valueType.getTypeSignature(), valueType.getTypeSignature(), keyType.getTypeSignature()); + InternalAggregationFunction aggregation = generateAggregation(valueType, keyType); + return new FunctionInfo(signature, getDescription(), aggregation); + } + + private static InternalAggregationFunction generateAggregation(Type valueType, Type keyType) + { + DynamicClassLoader classLoader = new DynamicClassLoader(MinBy.class.getClassLoader()); + + MaxOrMinByStateSerializer stateSerializer = new MaxOrMinByStateSerializer(valueType, keyType); + Type intermediateType = stateSerializer.getSerializedType(); + + List inputTypes = ImmutableList.of(valueType, keyType); + + MaxOrMinByStateFactory stateFactory = new MaxOrMinByStateFactory(); + AggregationMetadata metadata = new AggregationMetadata( + generateAggregationName(NAME, valueType, inputTypes), + createInputParameterMetadata(valueType, keyType), + INPUT_FUNCTION.bindTo(keyType), + null, + null, + COMBINE_FUNCTION.bindTo(keyType), + OUTPUT_FUNCTION.bindTo(valueType), + MaxOrMinByState.class, + stateSerializer, + stateFactory, + valueType, + false); + + GenericAccumulatorFactoryBinder factory = new AccumulatorCompiler().generateAccumulatorFactoryBinder(metadata, classLoader); + return new InternalAggregationFunction(NAME, inputTypes, intermediateType, valueType, true, false, factory); + } + + private static List createInputParameterMetadata(Type value, Type key) + { + return ImmutableList.of(new ParameterMetadata(STATE), new ParameterMetadata(NULLABLE_BLOCK_INPUT_CHANNEL, value), new ParameterMetadata(NULLABLE_BLOCK_INPUT_CHANNEL, key), new ParameterMetadata(BLOCK_INDEX)); + } + + public static void input(Type keyType, MaxOrMinByState state, Block value, Block key, int position) + { + if (state.getKey() == null || state.getKey().isNull(0)) { + state.setKey(key.getSingleValueBlock(position)); + state.setValue(value.getSingleValueBlock(position)); + } + else if (keyType.compareTo(key, position, state.getKey(), 0) < 0) { + state.setKey(key.getSingleValueBlock(position)); + state.setValue(value.getSingleValueBlock(position)); + } + } + + public static void combine(Type keyType, MaxOrMinByState state, MaxOrMinByState otherState) + { + if (state.getKey() == null) { + state.setKey(otherState.getKey()); + state.setValue(otherState.getValue()); + } + else if (otherState.getKey() != null && keyType.compareTo(otherState.getKey(), 0, state.getKey(), 0) < 0) { + state.setKey(otherState.getKey()); + state.setValue(otherState.getValue()); + } + } + + public static void output(Type valueType, MaxOrMinByState state, BlockBuilder out) + { + if (state.getValue() == null) { + out.appendNull(); + } + else { + valueType.appendTo(state.getValue(), 0, out); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/NullablePosition.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/NullablePosition.java new file mode 100644 index 00000000..4c4b051b --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/NullablePosition.java @@ -0,0 +1,25 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface NullablePosition +{ +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/NumericHistogram.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/NumericHistogram.java new file mode 100644 index 00000000..4f9bad66 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/NumericHistogram.java @@ -0,0 +1,418 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.primitives.Doubles; +import io.airlift.slice.SizeOf; +import io.airlift.slice.Slice; +import io.airlift.slice.SliceInput; +import io.airlift.slice.Slices; +import it.unimi.dsi.fastutil.Arrays; +import it.unimi.dsi.fastutil.Swapper; +import it.unimi.dsi.fastutil.ints.AbstractIntComparator; +import org.openjdk.jol.info.ClassLayout; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.PriorityQueue; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +public class NumericHistogram +{ + private static final byte FORMAT_TAG = 0; + private static final int INSTANCE_SIZE = ClassLayout.parseClass(NumericHistogram.class).instanceSize(); + + private final int maxBuckets; + private final double[] values; + private final double[] weights; + + private int nextIndex = 0; + + public NumericHistogram(int maxBuckets) + { + this(maxBuckets, Math.max((int) (maxBuckets * 0.2), 1)); + } + + public NumericHistogram(int maxBuckets, int buffer) + { + checkArgument(maxBuckets >= 2, "maxBuckets must be >= 2"); + checkArgument(buffer >= 1, "buffer must be >= 1"); + + this.maxBuckets = maxBuckets; + this.values = new double[maxBuckets + buffer]; + this.weights = new double[maxBuckets + buffer]; + } + + public NumericHistogram(Slice serialized, int buffer) + { + checkNotNull(serialized, "serialized is null"); + checkArgument(buffer >= 1, "buffer must be >= 1"); + + SliceInput input = serialized.getInput(); + + checkArgument(input.readByte() == FORMAT_TAG, "Unsupported format tag"); + + maxBuckets = input.readInt(); + nextIndex = input.readInt(); + values = new double[maxBuckets + buffer]; + weights = new double[maxBuckets + buffer]; + + input.readBytes(Slices.wrappedDoubleArray(values), nextIndex * SizeOf.SIZE_OF_DOUBLE); + input.readBytes(Slices.wrappedDoubleArray(weights), nextIndex * SizeOf.SIZE_OF_DOUBLE); + } + + public Slice serialize() + { + compact(); + + int requiredBytes = SizeOf.SIZE_OF_BYTE + // format + SizeOf.SIZE_OF_INT + // max buckets + SizeOf.SIZE_OF_INT + // entry count + SizeOf.SIZE_OF_DOUBLE * nextIndex + // values + SizeOf.SIZE_OF_DOUBLE * nextIndex; // weights + + return Slices.allocate(requiredBytes) + .getOutput() + .appendByte(FORMAT_TAG) + .appendInt(maxBuckets) + .appendInt(nextIndex) + .appendBytes(Slices.wrappedDoubleArray(values, 0, nextIndex)) + .appendBytes(Slices.wrappedDoubleArray(weights, 0, nextIndex)) + .getUnderlyingSlice(); + } + + public long estimatedInMemorySize() + { + return INSTANCE_SIZE + SizeOf.sizeOf(values) + SizeOf.sizeOf(weights); + } + + public void add(double value) + { + add(value, 1); + } + + public void add(double value, double weight) + { + if (nextIndex == values.length) { + compact(); + } + + values[nextIndex] = value; + weights[nextIndex] = weight; + + nextIndex++; + } + + public void mergeWith(NumericHistogram other) + { + int count = nextIndex + other.nextIndex; + + double[] newValues = new double[count]; + double[] newWeights = new double[count]; + + concat(newValues, this.values, this.nextIndex, other.values, other.nextIndex); + concat(newWeights, this.weights, this.nextIndex, other.weights, other.nextIndex); + + count = mergeSameBuckets(newValues, newWeights, count); + + if (count <= maxBuckets) { + // copy back into this.values/this.weights + System.arraycopy(newValues, 0, this.values, 0, count); + System.arraycopy(newWeights, 0, this.weights, 0, count); + nextIndex = count; + return; + } + + sort(newValues, newWeights, count); + store(mergeBuckets(newValues, newWeights, count, maxBuckets)); + } + + public Map getBuckets() + { + compact(); + + Map result = new LinkedHashMap<>(); + for (int i = 0; i < nextIndex; i++) { + result.put(values[i], weights[i]); + } + return result; + } + + @VisibleForTesting + void compact() + { + nextIndex = mergeSameBuckets(values, weights, nextIndex); + + if (nextIndex <= maxBuckets) { + return; + } + + // entries are guaranteed to be sorted as a side-effect of the call to mergeSameBuckets + store(mergeBuckets(values, weights, nextIndex, maxBuckets)); + } + + private static PriorityQueue mergeBuckets(double[] values, double[] weights, int count, int targetCount) + { + checkArgument(targetCount > 0, "targetCount must be > 0"); + + PriorityQueue queue = initializeQueue(values, weights, count); + + while (count > targetCount) { + Entry current = queue.poll(); + if (!current.isValid()) { + // ignore entries that have already been replaced + continue; + } + + count--; + + Entry right = current.getRight(); + + // right is guaranteed to exist because we set the penalty of the last bucket to infinity + // so the first current in the queue can never be the last bucket + checkState(right != null, "Expected right to be != null"); + checkState(right.isValid(), "Expected right to be valid"); + + // merge "current" with "right" + double newWeight = current.getWeight() + right.getWeight(); + double newValue = (current.getValue() * current.getWeight() + right.getValue() * right.getWeight()) / newWeight; + + // mark "right" as invalid so we can skip it if it shows up as we poll from the head of the queue + right.invalidate(); + + // compute the merged entry linked to right of right + Entry merged = new Entry(current.getId(), newValue, newWeight, right.getRight()); + queue.add(merged); + + Entry left = current.getLeft(); + if (left != null) { + checkState(left.isValid(), "Expected left to be valid"); + + // replace "left" with a new entry with a penalty adjusted to account for (newValue, newWeight) + left.invalidate(); + + // create a new left entry linked to the merged entry + queue.add(new Entry(left.getId(), left.getValue(), left.getWeight(), left.getLeft(), merged)); + } + } + + return queue; + } + + /** + * Dump the entries in the queue back into the bucket arrays + * The values are guaranteed to be sorted in increasing order after this method completes + */ + private void store(PriorityQueue queue) + { + nextIndex = 0; + for (Entry entry : queue) { + if (entry.isValid()) { + values[nextIndex] = entry.getValue(); + weights[nextIndex] = entry.getWeight(); + nextIndex++; + } + } + sort(values, weights, nextIndex); + } + + /** + * Copy two arrays back-to-back onto the target array starting at offset 0 + */ + private static void concat(double[] target, double[] first, int firstLength, double[] second, int secondLength) + { + System.arraycopy(first, 0, target, 0, firstLength); + System.arraycopy(second, 0, target, firstLength, secondLength); + } + + /** + * Simple pass that merges entries with the same value + */ + private static int mergeSameBuckets(double[] values, double[] weights, int nextIndex) + { + sort(values, weights, nextIndex); + + int current = 0; + for (int i = 1; i < nextIndex; i++) { + if (values[current] == values[i]) { + weights[current] += weights[i]; + } + else { + current++; + values[current] = values[i]; + weights[current] = weights[i]; + } + } + return current + 1; + } + + /** + * Create a priority queue with an entry for each bucket, ordered by the penalty score with respect to the bucket to its right + * The inputs must be sorted by "value" in increasing order + * The last bucket has a penalty of infinity + * Entries are doubly-linked to keep track of the relative position of each bucket + */ + private static PriorityQueue initializeQueue(double[] values, double[] weights, int nextIndex) + { + checkArgument(nextIndex > 0, "nextIndex must be > 0"); + + PriorityQueue queue = new PriorityQueue<>(nextIndex); + + Entry right = new Entry(nextIndex - 1, values[nextIndex - 1], weights[nextIndex - 1], null); + queue.add(right); + for (int i = nextIndex - 2; i >= 0; i--) { + Entry current = new Entry(i, values[i], weights[i], right); + queue.add(current); + right = current; + } + + return queue; + } + + private static void sort(final double[] values, final double[] weights, int nextIndex) + { + // sort x and y value arrays based on the x values + Arrays.quickSort(0, nextIndex, new AbstractIntComparator() + { + @Override + public int compare(int a, int b) + { + return Doubles.compare(values[a], values[b]); + } + }, new Swapper() + { + @Override + public void swap(int a, int b) + { + double temp = values[a]; + values[a] = values[b]; + values[b] = temp; + + temp = weights[a]; + weights[a] = weights[b]; + weights[b] = temp; + } + }); + } + + private static double computePenalty(double value1, double value2, double weight1, double weight2) + { + double weight = value2 + weight2; + double squaredDifference = (value1 - weight1) * (value1 - weight1); + double proportionsProduct = (value2 * weight2) / ((value2 + weight2) * (value2 + weight2)); + return weight * squaredDifference * proportionsProduct; + } + + private static class Entry + implements Comparable + { + private final double penalty; + + private final int id; + private final double value; + private final double weight; + + private boolean valid = true; + private Entry left; + private Entry right; + + private Entry(int id, double value, double weight, Entry right) + { + this(id, value, weight, null, right); + } + + private Entry(int id, double value, double weight, Entry left, Entry right) + { + this.id = id; + this.value = value; + this.weight = weight; + this.right = right; + this.left = left; + + if (right != null) { + right.left = this; + penalty = computePenalty(value, weight, right.value, right.weight); + } + else { + penalty = Double.POSITIVE_INFINITY; + } + + if (left != null) { + left.right = this; + } + } + + public int getId() + { + return id; + } + + public Entry getLeft() + { + return left; + } + + public Entry getRight() + { + return right; + } + + public double getValue() + { + return value; + } + + public double getWeight() + { + return weight; + } + + public boolean isValid() + { + return valid; + } + + public void invalidate() + { + this.valid = false; + } + + @Override + public int compareTo(Entry other) + { + int result = Double.compare(penalty, other.penalty); + if (result == 0) { + result = Integer.compare(id, other.id); + } + return result; + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("id", id) + .add("value", value) + .add("weight", weight) + .add("penalty", penalty) + .add("valid", valid) + .toString(); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/NumericHistogramAggregation.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/NumericHistogramAggregation.java new file mode 100644 index 00000000..686c58d7 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/NumericHistogramAggregation.java @@ -0,0 +1,96 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.facebook.presto.operator.aggregation.state.AccumulatorState; +import com.facebook.presto.operator.aggregation.state.AccumulatorStateMetadata; +import com.facebook.presto.operator.aggregation.state.NumericHistogramStateSerializer; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.DoubleType; +import com.facebook.presto.type.MapType; +import com.facebook.presto.type.SqlType; +import com.google.common.primitives.Ints; +import io.airlift.slice.Slice; + +import javax.validation.constraints.NotNull; + +import static com.facebook.presto.spi.StandardErrorCode.INVALID_FUNCTION_ARGUMENT; +import static com.facebook.presto.spi.type.StandardTypes.BIGINT; +import static com.facebook.presto.spi.type.StandardTypes.DOUBLE; +import static com.facebook.presto.util.Failures.checkCondition; + +@AggregationFunction("numeric_histogram") +public class NumericHistogramAggregation +{ + public static final int ENTRY_BUFFER_SIZE = 100; + + private NumericHistogramAggregation() + { + } + + @AccumulatorStateMetadata(stateSerializerClass = NumericHistogramStateSerializer.class, stateFactoryClass = NumericHistogramStateFactory.class) + public interface State + extends AccumulatorState + { + @NotNull + NumericHistogram get(); + void set(NumericHistogram value); + } + + @InputFunction + public static void add(State state, @SqlType(BIGINT) long buckets, @SqlType(DOUBLE) double value, @SqlType(DOUBLE) double weight) + { + NumericHistogram histogram = state.get(); + if (histogram == null) { + checkCondition(buckets >= 2, INVALID_FUNCTION_ARGUMENT, "numeric_histogram bucket count must be greater than one"); + histogram = new NumericHistogram(Ints.checkedCast(buckets), ENTRY_BUFFER_SIZE); + state.set(histogram); + } + + histogram.add(value, weight); + } + + @InputFunction + public static void add(State state, @SqlType(BIGINT) long buckets, @SqlType(DOUBLE) double value) + { + add(state, buckets, value, 1); + } + + @CombineFunction + public static void merge(State state, State other) + { + NumericHistogram input = other.get(); + NumericHistogram previous = state.get(); + + if (previous == null) { + state.set(input); + } + else { + previous.mergeWith(input); + } + } + + @OutputFunction("map") + public static void output(State state, BlockBuilder out) + { + if (state.get() == null) { + out.appendNull(); + } + else { + Slice slice = MapType.toStackRepresentation(state.get().getBuckets(), DoubleType.DOUBLE, DoubleType.DOUBLE); + out.writeBytes(slice, 0, slice.length()); + out.closeEntry(); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/NumericHistogramStateFactory.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/NumericHistogramStateFactory.java new file mode 100644 index 00000000..56116025 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/NumericHistogramStateFactory.java @@ -0,0 +1,115 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.facebook.presto.operator.aggregation.state.AbstractGroupedAccumulatorState; +import com.facebook.presto.operator.aggregation.state.AccumulatorStateFactory; +import com.facebook.presto.util.array.ObjectBigArray; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class NumericHistogramStateFactory + implements AccumulatorStateFactory +{ + @Override + public NumericHistogramAggregation.State createSingleState() + { + return new SingleState(); + } + + @Override + public Class getSingleStateClass() + { + return SingleState.class; + } + + @Override + public NumericHistogramAggregation.State createGroupedState() + { + return new GroupedState(); + } + + @Override + public Class getGroupedStateClass() + { + return GroupedState.class; + } + + public static class GroupedState + extends AbstractGroupedAccumulatorState + implements NumericHistogramAggregation.State + { + private final ObjectBigArray histograms = new ObjectBigArray<>(); + private long size; + + @Override + public void ensureCapacity(long size) + { + histograms.ensureCapacity(size); + } + + @Override + public NumericHistogram get() + { + return histograms.get(getGroupId()); + } + + @Override + public void set(NumericHistogram value) + { + checkNotNull(value, "value is null"); + + NumericHistogram previous = get(); + if (previous != null) { + size -= previous.estimatedInMemorySize(); + } + + histograms.set(getGroupId(), value); + size += value.estimatedInMemorySize(); + } + + @Override + public long getEstimatedSize() + { + return size + histograms.sizeOf(); + } + } + + public static class SingleState + implements NumericHistogramAggregation.State + { + private NumericHistogram histogram; + + @Override + public NumericHistogram get() + { + return histogram; + } + + @Override + public void set(NumericHistogram value) + { + histogram = value; + } + + @Override + public long getEstimatedSize() + { + if (histogram == null) { + return 0; + } + return histogram.estimatedInMemorySize(); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/OutputFunction.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/OutputFunction.java new file mode 100644 index 00000000..39104181 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/OutputFunction.java @@ -0,0 +1,26 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface OutputFunction +{ + String value(); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/RegressionAggregation.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/RegressionAggregation.java new file mode 100644 index 00000000..1ccdc12f --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/RegressionAggregation.java @@ -0,0 +1,77 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.facebook.presto.operator.aggregation.state.RegressionState; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.type.SqlType; + +import static com.facebook.presto.operator.aggregation.AggregationUtils.mergeRegressionState; +import static com.facebook.presto.operator.aggregation.AggregationUtils.updateRegressionState; +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; + +@AggregationFunction("") // Names are on output methods +public class RegressionAggregation +{ + private RegressionAggregation() {} + + @InputFunction + public static void input(RegressionState state, @SqlType(StandardTypes.DOUBLE) double dependentValue, @SqlType(StandardTypes.DOUBLE) double independentValue) + { + updateRegressionState(state, independentValue, dependentValue); + } + + @CombineFunction + public static void combine(RegressionState state, RegressionState otherState) + { + mergeRegressionState(state, otherState); + } + + @AggregationFunction("regr_slope") + @OutputFunction(StandardTypes.DOUBLE) + public static void regrSlope(RegressionState state, BlockBuilder out) + { + // Math comes from ISO9075-2:2011(E) 10.9 General Rules 7 c xii + double dividend = state.getCount() * state.getSumXY() - state.getSumX() * state.getSumY(); + double divisor = state.getCount() * state.getSumXSquare() - state.getSumX() * state.getSumX(); + + // divisor deliberately not checked for zero because the result can be Infty or NaN even if it is not zero + double result = dividend / divisor; + if (Double.isFinite(result)) { + DOUBLE.writeDouble(out, result); + } + else { + out.appendNull(); + } + } + + @AggregationFunction("regr_intercept") + @OutputFunction(StandardTypes.DOUBLE) + public static void regrIntercept(RegressionState state, BlockBuilder out) + { + // Math comes from ISO9075-2:2011(E) 10.9 General Rules 7 c xiii + double dividend = state.getSumY() * state.getSumXSquare() - state.getSumX() * state.getSumXY(); + double divisor = state.getCount() * state.getSumXSquare() - state.getSumX() * state.getSumX(); + + // divisor deliberately not checked for zero because the result can be Infty or NaN even if it is not zero + double result = dividend / divisor; + if (Double.isFinite(result)) { + DOUBLE.writeDouble(out, result); + } + else { + out.appendNull(); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/SampleWeight.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/SampleWeight.java new file mode 100644 index 00000000..1f987c69 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/SampleWeight.java @@ -0,0 +1,25 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface SampleWeight +{ +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/SimpleTypedSet.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/SimpleTypedSet.java new file mode 100644 index 00000000..4d3ec665 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/SimpleTypedSet.java @@ -0,0 +1,176 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.facebook.presto.ExceededMemoryLimitException; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.block.BlockBuilderStatus; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.util.array.IntBigArray; +import io.airlift.units.DataSize; + +import static com.facebook.presto.type.TypeUtils.hashPosition; +import static com.facebook.presto.type.TypeUtils.positionEqualsPosition; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.airlift.units.DataSize.Unit.MEGABYTE; +import static it.unimi.dsi.fastutil.HashCommon.arraySize; + +public class SimpleTypedSet + implements TypedSet +{ + private static final float FILL_RATIO = 0.75f; + private static final long FOUR_MEGABYTES = new DataSize(4, MEGABYTE).toBytes(); + + private final Type elementType; + private final IntBigArray blockPositionByHash = new IntBigArray(); + private final BlockBuilder elementBlock; + + private int maxFill; + private int hashMask; + private static final int EMPTY_SLOT = -1; + + private boolean containsNullElement; + + public SimpleTypedSet(Type elementType, int expectedSize) + { + checkArgument(expectedSize > 0, "expectedSize must be > 0"); + this.elementType = checkNotNull(elementType, "elementType must not be null"); + this.elementBlock = elementType.createBlockBuilder(new BlockBuilderStatus(), expectedSize); + + int hashSize = arraySize(expectedSize, FILL_RATIO); + this.maxFill = calculateMaxFill(hashSize); + this.hashMask = hashSize - 1; + + blockPositionByHash.ensureCapacity(hashSize); + for (int i = 0; i < hashSize; i++) { + blockPositionByHash.set(i, EMPTY_SLOT); + } + + this.containsNullElement = false; + } + + @Override + public long getEstimatedSize() + { + return elementBlock.getSizeInBytes() + blockPositionByHash.sizeOf(); + } + + @Override + public boolean contains(Block block, int position) + { + checkNotNull(block, "block must not be null"); + checkArgument(position >= 0, "position must be >= 0"); + + if (block.isNull(position)) { + return containsNullElement; + } + else { + return blockPositionByHash.get(getHashPositionOfElement(block, position)) != EMPTY_SLOT; + } + } + + @Override + public void add(Block block, int position) + { + checkNotNull(block, "block must not be null"); + checkArgument(position >= 0, "position must be >= 0"); + + if (block.isNull(position)) { + containsNullElement = true; + } + else { + int hashPosition = getHashPositionOfElement(block, position); + if (blockPositionByHash.get(hashPosition) == EMPTY_SLOT) { + addNewElement(hashPosition, block, position); + } + } + } + + @Override + public int size() + { + return elementBlock.getPositionCount() + (containsNullElement ? 1 : 0); + } + + /** + * Get slot position of element at {@code position} of {@code block} + */ + private int getHashPositionOfElement(Block block, int position) + { + int hashPosition = getMaskedHash(hashPosition(elementType, block, position)); + while (true) { + int blockPosition = blockPositionByHash.get(hashPosition); + // Doesn't have this element + if (blockPosition == EMPTY_SLOT) { + return hashPosition; + } + // Already has this element + else if (positionEqualsPosition(elementType, elementBlock, blockPosition, block, position)) { + return hashPosition; + } + + hashPosition = getMaskedHash(hashPosition + 1); + } + } + + private void addNewElement(int hashPosition, Block block, int position) + { + elementType.appendTo(block, position, elementBlock); + if (elementBlock.getSizeInBytes() > FOUR_MEGABYTES) { + throw new ExceededMemoryLimitException(new DataSize(4, MEGABYTE)); + } + blockPositionByHash.set(hashPosition, elementBlock.getPositionCount() - 1); + + // increase capacity, if necessary + if (elementBlock.getPositionCount() >= maxFill) { + rehash(maxFill * 2); + } + } + + private void rehash(int size) + { + int newHashSize = arraySize(size + 1, FILL_RATIO); + hashMask = newHashSize - 1; + maxFill = calculateMaxFill(newHashSize); + blockPositionByHash.ensureCapacity(newHashSize); + for (int i = 0; i < newHashSize; i++) { + blockPositionByHash.set(i, EMPTY_SLOT); + } + + rehashBlock(elementBlock); + } + + private void rehashBlock(Block block) + { + for (int blockPosition = 0; blockPosition < block.getPositionCount(); blockPosition++) { + blockPositionByHash.set(getHashPositionOfElement(block, blockPosition), blockPosition); + } + } + + private static int calculateMaxFill(int hashSize) + { + int maxFill = (int) Math.ceil(hashSize * FILL_RATIO); + if (maxFill == hashSize) { + maxFill--; + } + return maxFill; + } + + private int getMaskedHash(int rawHash) + { + return rawHash & hashMask; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/TypedSet.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/TypedSet.java new file mode 100644 index 00000000..b93d9e19 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/TypedSet.java @@ -0,0 +1,34 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.facebook.presto.spi.block.Block; + +/** + * Set of {@code Type} elements + */ +public interface TypedSet +{ + // Check if contains element at {@code position} of {@code block} + boolean contains(Block block, int position); + + // Add element at {@code position} of {@code block} + void add(Block block, int position); + + // Number of elements + int size(); + + // Estimated memory size in bytes + long getEstimatedSize(); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/VarianceAggregation.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/VarianceAggregation.java new file mode 100644 index 00000000..8b0ce454 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/VarianceAggregation.java @@ -0,0 +1,109 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation; + +import com.facebook.presto.operator.aggregation.state.VarianceState; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.type.SqlType; + +import static com.facebook.presto.operator.aggregation.AggregationUtils.mergeVarianceState; +import static com.facebook.presto.operator.aggregation.AggregationUtils.updateVarianceState; +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; + +@AggregationFunction("") // Names are on output methods +public final class VarianceAggregation +{ + private VarianceAggregation() {} + + @InputFunction + public static void doubleInput(VarianceState state, @SqlType(StandardTypes.DOUBLE) double value) + { + updateVarianceState(state, value); + } + + @InputFunction + public static void bigintInput(VarianceState state, @SqlType(StandardTypes.BIGINT) long value) + { + updateVarianceState(state, (double) value); + } + + @CombineFunction + public static void combine(VarianceState state, VarianceState otherState) + { + mergeVarianceState(state, otherState); + } + + @AggregationFunction(value = "variance", alias = "var_samp") + @OutputFunction(StandardTypes.DOUBLE) + public static void variance(VarianceState state, BlockBuilder out) + { + long count = state.getCount(); + if (count < 2) { + out.appendNull(); + } + else { + double m2 = state.getM2(); + double result = m2 / (count - 1); + DOUBLE.writeDouble(out, result); + } + } + + @AggregationFunction("var_pop") + @OutputFunction(StandardTypes.DOUBLE) + public static void variancePop(VarianceState state, BlockBuilder out) + { + long count = state.getCount(); + if (count == 0) { + out.appendNull(); + } + else { + double m2 = state.getM2(); + double result = m2 / count; + DOUBLE.writeDouble(out, result); + } + } + + @AggregationFunction(value = "stddev", alias = "stddev_samp") + @OutputFunction(StandardTypes.DOUBLE) + public static void stddev(VarianceState state, BlockBuilder out) + { + long count = state.getCount(); + if (count < 2) { + out.appendNull(); + } + else { + double m2 = state.getM2(); + double result = m2 / (count - 1); + result = Math.sqrt(result); + DOUBLE.writeDouble(out, result); + } + } + + @AggregationFunction("stddev_pop") + @OutputFunction(StandardTypes.DOUBLE) + public static void stddevPop(VarianceState state, BlockBuilder out) + { + long count = state.getCount(); + if (count == 0) { + out.appendNull(); + } + else { + double m2 = state.getM2(); + double result = m2 / count; + result = Math.sqrt(result); + DOUBLE.writeDouble(out, result); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/AbstractGroupedAccumulatorState.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/AbstractGroupedAccumulatorState.java new file mode 100644 index 00000000..6f0bdcf6 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/AbstractGroupedAccumulatorState.java @@ -0,0 +1,31 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation.state; + +public abstract class AbstractGroupedAccumulatorState + implements GroupedAccumulatorState +{ + private long groupId; + + @Override + public final void setGroupId(long groupId) + { + this.groupId = groupId; + } + + protected final long getGroupId() + { + return groupId; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/AccumulatorState.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/AccumulatorState.java new file mode 100644 index 00000000..6b84cc34 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/AccumulatorState.java @@ -0,0 +1,19 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation.state; + +public interface AccumulatorState +{ + long getEstimatedSize(); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/AccumulatorStateFactory.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/AccumulatorStateFactory.java new file mode 100644 index 00000000..ad2ea1a1 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/AccumulatorStateFactory.java @@ -0,0 +1,28 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation.state; + +public interface AccumulatorStateFactory +{ + T createSingleState(); + + Class getSingleStateClass(); + + /** + * Return value is also guaranteed to implement GroupedAccumulatorState + */ + T createGroupedState(); + + Class getGroupedStateClass(); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/AccumulatorStateMetadata.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/AccumulatorStateMetadata.java new file mode 100644 index 00000000..4e334352 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/AccumulatorStateMetadata.java @@ -0,0 +1,28 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation.state; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface AccumulatorStateMetadata +{ + Class stateSerializerClass() default void.class; + + Class stateFactoryClass() default void.class; +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/AccumulatorStateSerializer.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/AccumulatorStateSerializer.java new file mode 100644 index 00000000..9e5f00a0 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/AccumulatorStateSerializer.java @@ -0,0 +1,27 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation.state; + +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; + +public interface AccumulatorStateSerializer +{ + Type getSerializedType(); + + void serialize(T state, BlockBuilder out); + + void deserialize(Block block, int index, T state); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/ArbitraryAggregationState.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/ArbitraryAggregationState.java new file mode 100644 index 00000000..be3c38cf --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/ArbitraryAggregationState.java @@ -0,0 +1,28 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation.state; + +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.type.Type; + +@AccumulatorStateMetadata(stateFactoryClass = ArbitraryAggregationStateFactory.class, stateSerializerClass = ArbitraryAggregationStateSerializer.class) +public interface ArbitraryAggregationState + extends AccumulatorState +{ + Type getType(); + + Block getValue(); + + void setValue(Block value); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/ArbitraryAggregationStateFactory.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/ArbitraryAggregationStateFactory.java new file mode 100644 index 00000000..152455db --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/ArbitraryAggregationStateFactory.java @@ -0,0 +1,137 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation.state; + +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.util.array.BlockBigArray; + +public class ArbitraryAggregationStateFactory + implements AccumulatorStateFactory +{ + private final Type valueType; + + public ArbitraryAggregationStateFactory(Type valueType) + { + this.valueType = valueType; + } + + @Override + public ArbitraryAggregationState createSingleState() + { + return new SingleArbitraryAggregationState(valueType); + } + + @Override + public Class getSingleStateClass() + { + return SingleArbitraryAggregationState.class; + } + + @Override + public ArbitraryAggregationState createGroupedState() + { + return new GroupedArbitraryAggregationState(valueType); + } + + @Override + public Class getGroupedStateClass() + { + return GroupedArbitraryAggregationState.class; + } + + public static class GroupedArbitraryAggregationState + extends AbstractGroupedAccumulatorState + implements ArbitraryAggregationState + { + private final Type valueType; + private final BlockBigArray values = new BlockBigArray(); + + public GroupedArbitraryAggregationState(Type valueType) + { + this.valueType = valueType; + } + + @Override + public void ensureCapacity(long size) + { + values.ensureCapacity(size); + } + + @Override + public long getEstimatedSize() + { + return values.sizeOf(); + } + + @Override + public Type getType() + { + return valueType; + } + + @Override + public Block getValue() + { + return values.get(getGroupId()); + } + + @Override + public void setValue(Block value) + { + values.set(getGroupId(), value); + } + } + + public static class SingleArbitraryAggregationState + implements ArbitraryAggregationState + { + private final Type valueType; + private Block value; + + public SingleArbitraryAggregationState(Type valueType) + { + this.valueType = valueType; + } + + @Override + public long getEstimatedSize() + { + if (value != null) { + return (long) value.getRetainedSizeInBytes(); + } + else { + return 0L; + } + } + + @Override + public Type getType() + { + return valueType; + } + + @Override + public Block getValue() + { + return value; + } + + @Override + public void setValue(Block value) + { + this.value = value; + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/ArbitraryAggregationStateSerializer.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/ArbitraryAggregationStateSerializer.java new file mode 100644 index 00000000..958ec6fc --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/ArbitraryAggregationStateSerializer.java @@ -0,0 +1,107 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation.state; + +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.block.BlockBuilderStatus; +import com.facebook.presto.spi.type.Type; +import io.airlift.slice.DynamicSliceOutput; +import io.airlift.slice.Slice; +import io.airlift.slice.SliceInput; +import io.airlift.slice.SliceOutput; + +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; + +public class ArbitraryAggregationStateSerializer + implements AccumulatorStateSerializer +{ + @Override + public Type getSerializedType() + { + return VARCHAR; + } + + @Override + public void serialize(ArbitraryAggregationState state, BlockBuilder out) + { + SliceOutput sliceOutput = new DynamicSliceOutput((int) state.getEstimatedSize()); + + int valueLength = 0; + if (state.getValue() != null && !state.getValue().isNull(0)) { + valueLength = state.getValue().getLength(0); + } + sliceOutput.writeInt(valueLength); + + if (state.getValue() != null && !state.getValue().isNull(0)) { + appendTo(state.getType(), sliceOutput, state.getValue()); + } + Slice slice = sliceOutput.slice(); + out.writeBytes(slice, 0, slice.length()); + out.closeEntry(); + } + + private static void appendTo(Type type, SliceOutput output, Block block) + { + if (type.getJavaType() == long.class) { + output.appendLong(type.getLong(block, 0)); + } + else if (type.getJavaType() == double.class) { + output.appendDouble(type.getDouble(block, 0)); + } + else if (type.getJavaType() == Slice.class) { + output.appendBytes(type.getSlice(block, 0)); + } + else if (type.getJavaType() == boolean.class) { + output.appendByte(type.getBoolean(block, 0) ? 1 : 0); + } + else { + throw new IllegalArgumentException("Unsupported type: " + type.getJavaType().getSimpleName()); + } + } + + @Override + public void deserialize(Block block, int index, ArbitraryAggregationState state) + { + SliceInput input = block.getSlice(index, 0, block.getLength(index)).getInput(); + + int valueLength = input.readInt(); + state.setValue(null); + if (valueLength > 0) { + state.setValue(toBlock(state.getType(), input, valueLength)); + } + } + + private static Block toBlock(Type type, SliceInput input, int length) + { + BlockBuilder builder = type.createBlockBuilder(new BlockBuilderStatus(), 1, length); + if (type.getJavaType() == long.class) { + type.writeLong(builder, input.readLong()); + } + else if (type.getJavaType() == double.class) { + type.writeDouble(builder, input.readDouble()); + } + else if (type.getJavaType() == Slice.class) { + type.writeSlice(builder, input.readSlice(length)); + } + else if (type.getJavaType() == boolean.class) { + type.writeBoolean(builder, input.readByte() != 0); + } + else { + throw new IllegalArgumentException("Unsupported type: " + type.getJavaType().getSimpleName()); + } + + return builder.build(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/ArrayAggregationState.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/ArrayAggregationState.java new file mode 100644 index 00000000..f7a28d86 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/ArrayAggregationState.java @@ -0,0 +1,27 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation.state; + +import com.facebook.presto.spi.block.BlockBuilder; + +@AccumulatorStateMetadata(stateFactoryClass = ArrayAggregationStateFactory.class, stateSerializerClass = ArrayAggregationStateSerializer.class) +public interface ArrayAggregationState + extends AccumulatorState +{ + BlockBuilder getBlockBuilder(); + + void setBlockBuilder(BlockBuilder value); + + void addMemoryUsage(long memory); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/ArrayAggregationStateFactory.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/ArrayAggregationStateFactory.java new file mode 100644 index 00000000..9a64b60b --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/ArrayAggregationStateFactory.java @@ -0,0 +1,128 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation.state; + +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.util.array.ObjectBigArray; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class ArrayAggregationStateFactory + implements AccumulatorStateFactory +{ + @Override + public ArrayAggregationState createSingleState() + { + return new SingleArrayAggregationState(); + } + + @Override + public Class getSingleStateClass() + { + return SingleArrayAggregationState.class; + } + + @Override + public ArrayAggregationState createGroupedState() + { + return new GroupedArrayAggregationState(); + } + + @Override + public Class getGroupedStateClass() + { + return GroupedArrayAggregationState.class; + } + + public static class GroupedArrayAggregationState + extends AbstractGroupedAccumulatorState + implements ArrayAggregationState + { + private final ObjectBigArray blockBuilders = new ObjectBigArray(); + private long size; + + @Override + public void ensureCapacity(long size) + { + blockBuilders.ensureCapacity(size); + } + + @Override + public long getEstimatedSize() + { + return size + blockBuilders.sizeOf(); + } + + @Override + public void addMemoryUsage(long memory) + { + size += memory; + } + + @Override + public BlockBuilder getBlockBuilder() + { + return blockBuilders.get(getGroupId()); + } + + @Override + public void setBlockBuilder(BlockBuilder value) + { + checkNotNull(value, "value is null"); + + BlockBuilder previous = getBlockBuilder(); + if (previous != null) { + size -= previous.getRetainedSizeInBytes(); + } + blockBuilders.set(getGroupId(), value); + size += value.getRetainedSizeInBytes(); + } + } + + public static class SingleArrayAggregationState + implements ArrayAggregationState + { + private BlockBuilder blockBuilder; + + @Override + public long getEstimatedSize() + { + if (blockBuilder == null) { + return 0L; + } + else { + return blockBuilder.getRetainedSizeInBytes(); + } + } + + @Override + public BlockBuilder getBlockBuilder() + { + return blockBuilder; + } + + @Override + public void setBlockBuilder(BlockBuilder value) + { + checkNotNull(value, "value is null"); + blockBuilder = value; + } + + @Override + public void addMemoryUsage(long memory) + { + // no op + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/ArrayAggregationStateSerializer.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/ArrayAggregationStateSerializer.java new file mode 100644 index 00000000..1942e0c0 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/ArrayAggregationStateSerializer.java @@ -0,0 +1,66 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation.state; + +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.block.BlockBuilderStatus; +import com.facebook.presto.spi.block.VariableWidthBlockBuilder; +import com.facebook.presto.spi.type.Type; +import io.airlift.slice.Slice; + +import static com.facebook.presto.spi.type.VarbinaryType.VARBINARY; +import static com.facebook.presto.type.TypeUtils.buildStructuralSlice; +import static com.facebook.presto.type.TypeUtils.readStructuralBlock; + +public class ArrayAggregationStateSerializer + implements AccumulatorStateSerializer +{ + private final Type type; + + public ArrayAggregationStateSerializer(Type type) + { + this.type = type; + } + + @Override + public Type getSerializedType() + { + return VARBINARY; + } + + @Override + public void serialize(ArrayAggregationState state, BlockBuilder out) + { + if (state.getBlockBuilder() == null) { + out.appendNull(); + } + else { + BlockBuilder stateBlockBuilder = state.getBlockBuilder(); + VARBINARY.writeSlice(out, buildStructuralSlice(stateBlockBuilder)); + } + } + + @Override + public void deserialize(Block block, int index, ArrayAggregationState state) + { + Slice slice = VARBINARY.getSlice(block, index); + Block stateBlock = readStructuralBlock(slice); + BlockBuilder stateBlockBuilder = new VariableWidthBlockBuilder(new BlockBuilderStatus()); // not using type.createBlockBuilder because fixedWidth ones can't be serialized + for (int i = 0; i < stateBlock.getPositionCount(); i++) { + type.appendTo(stateBlock, i, stateBlockBuilder); + } + state.setBlockBuilder(stateBlockBuilder); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/CorrelationState.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/CorrelationState.java new file mode 100644 index 00000000..78d620e4 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/CorrelationState.java @@ -0,0 +1,26 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation.state; + +public interface CorrelationState + extends CovarianceState +{ + double getSumXSquare(); + + void setSumXSquare(double value); + + double getSumYSquare(); + + void setSumYSquare(double value); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/CovarianceState.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/CovarianceState.java new file mode 100644 index 00000000..e40c4fc4 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/CovarianceState.java @@ -0,0 +1,34 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation.state; + +public interface CovarianceState + extends AccumulatorState +{ + long getCount(); + + void setCount(long value); + + double getSumXY(); + + void setSumXY(double value); + + double getSumX(); + + void setSumX(double value); + + double getSumY(); + + void setSumY(double value); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/DigestAndPercentileState.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/DigestAndPercentileState.java new file mode 100644 index 00000000..65d38c9a --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/DigestAndPercentileState.java @@ -0,0 +1,31 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation.state; + +import io.airlift.stats.QuantileDigest; + +@AccumulatorStateMetadata(stateSerializerClass = DigestAndPercentileStateSerializer.class, stateFactoryClass = DigestAndPercentileStateFactory.class) +public interface DigestAndPercentileState + extends AccumulatorState +{ + QuantileDigest getDigest(); + + void setDigest(QuantileDigest digest); + + double getPercentile(); + + void setPercentile(double percentile); + + void addMemoryUsage(int value); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/DigestAndPercentileStateFactory.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/DigestAndPercentileStateFactory.java new file mode 100644 index 00000000..c28489c4 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/DigestAndPercentileStateFactory.java @@ -0,0 +1,148 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation.state; + +import com.facebook.presto.util.array.DoubleBigArray; +import com.facebook.presto.util.array.ObjectBigArray; +import io.airlift.stats.QuantileDigest; + +import static com.google.common.base.Preconditions.checkNotNull; +import static io.airlift.slice.SizeOf.SIZE_OF_DOUBLE; + +public class DigestAndPercentileStateFactory + implements AccumulatorStateFactory +{ + @Override + public DigestAndPercentileState createSingleState() + { + return new SingleDigestAndPercentileState(); + } + + @Override + public Class getSingleStateClass() + { + return SingleDigestAndPercentileState.class; + } + + @Override + public DigestAndPercentileState createGroupedState() + { + return new GroupedDigestAndPercentileState(); + } + + @Override + public Class getGroupedStateClass() + { + return GroupedDigestAndPercentileState.class; + } + + public static class GroupedDigestAndPercentileState + extends AbstractGroupedAccumulatorState + implements DigestAndPercentileState + { + private final ObjectBigArray digests = new ObjectBigArray<>(); + private final DoubleBigArray percentiles = new DoubleBigArray(); + private long size; + + @Override + public void ensureCapacity(long size) + { + digests.ensureCapacity(size); + percentiles.ensureCapacity(size); + } + + @Override + public QuantileDigest getDigest() + { + return digests.get(getGroupId()); + } + + @Override + public void setDigest(QuantileDigest digest) + { + checkNotNull(digest, "value is null"); + digests.set(getGroupId(), digest); + } + + @Override + public double getPercentile() + { + return percentiles.get(getGroupId()); + } + + @Override + public void setPercentile(double percentile) + { + percentiles.set(getGroupId(), percentile); + } + + @Override + public void addMemoryUsage(int value) + { + size += value; + } + + @Override + public long getEstimatedSize() + { + return size + digests.sizeOf() + percentiles.sizeOf(); + } + } + + public static class SingleDigestAndPercentileState + implements DigestAndPercentileState + { + private QuantileDigest digest; + private double percentile; + + @Override + public QuantileDigest getDigest() + { + return digest; + } + + @Override + public void setDigest(QuantileDigest digest) + { + this.digest = digest; + } + + @Override + public double getPercentile() + { + return percentile; + } + + @Override + public void setPercentile(double percentile) + { + this.percentile = percentile; + } + + @Override + public void addMemoryUsage(int value) + { + // noop + } + + @Override + public long getEstimatedSize() + { + if (digest == null) { + return SIZE_OF_DOUBLE; + } + return digest.estimatedInMemorySizeInBytes() + SIZE_OF_DOUBLE; + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/DigestAndPercentileStateSerializer.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/DigestAndPercentileStateSerializer.java new file mode 100644 index 00000000..be49efe6 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/DigestAndPercentileStateSerializer.java @@ -0,0 +1,68 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation.state; + +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; +import io.airlift.slice.DynamicSliceOutput; +import io.airlift.slice.Slice; +import io.airlift.slice.SliceInput; +import io.airlift.stats.QuantileDigest; + +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static io.airlift.slice.SizeOf.SIZE_OF_DOUBLE; + +public class DigestAndPercentileStateSerializer + implements AccumulatorStateSerializer +{ + @Override + public Type getSerializedType() + { + return VARCHAR; + } + + @Override + public void serialize(DigestAndPercentileState state, BlockBuilder out) + { + if (state.getDigest() == null) { + out.appendNull(); + } + else { + DynamicSliceOutput sliceOutput = new DynamicSliceOutput(state.getDigest().estimatedSerializedSizeInBytes() + SIZE_OF_DOUBLE); + // write digest + state.getDigest().serialize(sliceOutput); + // write percentile + sliceOutput.appendDouble(state.getPercentile()); + + Slice slice = sliceOutput.slice(); + VARCHAR.writeSlice(out, slice); + } + } + + @Override + public void deserialize(Block block, int index, DigestAndPercentileState state) + { + if (!block.isNull(index)) { + SliceInput input = VARCHAR.getSlice(block, index).getInput(); + + // read digest + state.setDigest(QuantileDigest.deserialize(input)); + state.addMemoryUsage(state.getDigest().estimatedInMemorySizeInBytes()); + + // read percentile + state.setPercentile(input.readDouble()); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/GroupedAccumulatorState.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/GroupedAccumulatorState.java new file mode 100644 index 00000000..b0b5a156 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/GroupedAccumulatorState.java @@ -0,0 +1,22 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation.state; + +public interface GroupedAccumulatorState + extends AccumulatorState +{ + void setGroupId(long groupId); + + void ensureCapacity(long size); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/HyperLogLogState.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/HyperLogLogState.java new file mode 100644 index 00000000..52c491d0 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/HyperLogLogState.java @@ -0,0 +1,32 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation.state; + +import io.airlift.stats.cardinality.HyperLogLog; + +import javax.validation.constraints.NotNull; + +@AccumulatorStateMetadata(stateSerializerClass = HyperLogLogStateSerializer.class, stateFactoryClass = HyperLogLogStateFactory.class) +public interface HyperLogLogState + extends AccumulatorState +{ + int NUMBER_OF_BUCKETS = 4096; + + @NotNull + HyperLogLog getHyperLogLog(); + + void setHyperLogLog(HyperLogLog value); + + void addMemoryUsage(int value); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/HyperLogLogStateFactory.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/HyperLogLogStateFactory.java new file mode 100644 index 00000000..91b294c4 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/HyperLogLogStateFactory.java @@ -0,0 +1,119 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation.state; + +import com.facebook.presto.util.array.ObjectBigArray; +import io.airlift.stats.cardinality.HyperLogLog; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class HyperLogLogStateFactory + implements AccumulatorStateFactory +{ + @Override + public HyperLogLogState createSingleState() + { + return new SingleHyperLogLogState(); + } + + @Override + public Class getSingleStateClass() + { + return SingleHyperLogLogState.class; + } + + @Override + public HyperLogLogState createGroupedState() + { + return new GroupedHyperLogLogState(); + } + + @Override + public Class getGroupedStateClass() + { + return GroupedHyperLogLogState.class; + } + + public static class GroupedHyperLogLogState + extends AbstractGroupedAccumulatorState + implements HyperLogLogState + { + private final ObjectBigArray hlls = new ObjectBigArray<>(); + private long size; + + @Override + public void ensureCapacity(long size) + { + hlls.ensureCapacity(size); + } + + @Override + public HyperLogLog getHyperLogLog() + { + return hlls.get(getGroupId()); + } + + @Override + public void setHyperLogLog(HyperLogLog value) + { + checkNotNull(value, "value is null"); + hlls.set(getGroupId(), value); + } + + @Override + public void addMemoryUsage(int value) + { + size += value; + } + + @Override + public long getEstimatedSize() + { + return size + hlls.sizeOf(); + } + } + + public static class SingleHyperLogLogState + implements HyperLogLogState + { + private HyperLogLog hll; + + @Override + public HyperLogLog getHyperLogLog() + { + return hll; + } + + @Override + public void setHyperLogLog(HyperLogLog value) + { + hll = value; + } + + @Override + public void addMemoryUsage(int value) + { + // noop + } + + @Override + public long getEstimatedSize() + { + if (hll == null) { + return 0; + } + return hll.estimatedInMemorySize(); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/HyperLogLogStateSerializer.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/HyperLogLogStateSerializer.java new file mode 100644 index 00000000..51e27ebe --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/HyperLogLogStateSerializer.java @@ -0,0 +1,50 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation.state; + +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; +import io.airlift.stats.cardinality.HyperLogLog; + +import static com.facebook.presto.spi.type.HyperLogLogType.HYPER_LOG_LOG; + +public class HyperLogLogStateSerializer + implements AccumulatorStateSerializer +{ + @Override + public Type getSerializedType() + { + return HYPER_LOG_LOG; + } + + @Override + public void serialize(HyperLogLogState state, BlockBuilder out) + { + if (state.getHyperLogLog() == null) { + out.appendNull(); + } + else { + HYPER_LOG_LOG.writeSlice(out, state.getHyperLogLog().serialize()); + } + } + + @Override + public void deserialize(Block block, int index, HyperLogLogState state) + { + if (!block.isNull(index)) { + state.setHyperLogLog(HyperLogLog.newInstance(HYPER_LOG_LOG.getSlice(block, index))); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/InitialBooleanValue.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/InitialBooleanValue.java new file mode 100644 index 00000000..f8c79778 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/InitialBooleanValue.java @@ -0,0 +1,26 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation.state; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface InitialBooleanValue +{ + boolean value(); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/InitialDoubleValue.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/InitialDoubleValue.java new file mode 100644 index 00000000..eae65f63 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/InitialDoubleValue.java @@ -0,0 +1,26 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation.state; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface InitialDoubleValue +{ + double value(); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/InitialLongValue.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/InitialLongValue.java new file mode 100644 index 00000000..a6f98a91 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/InitialLongValue.java @@ -0,0 +1,26 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation.state; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface InitialLongValue +{ + long value(); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/KeyValuePairStateSerializer.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/KeyValuePairStateSerializer.java new file mode 100644 index 00000000..95e966da --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/KeyValuePairStateSerializer.java @@ -0,0 +1,50 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation.state; + +import com.facebook.presto.operator.aggregation.KeyValuePairs; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; + +import static com.facebook.presto.spi.type.VarbinaryType.VARBINARY; + +public class KeyValuePairStateSerializer + implements AccumulatorStateSerializer +{ + @Override + public Type getSerializedType() + { + return VARBINARY; + } + + @Override + public void serialize(KeyValuePairsState state, BlockBuilder out) + { + if (state.get() == null) { + out.appendNull(); + } + else { + VARBINARY.writeSlice(out, state.get().serialize()); + } + } + + @Override + public void deserialize(Block block, int index, KeyValuePairsState state) + { + if (!block.isNull(index)) { + state.set(new KeyValuePairs(VARBINARY.getSlice(block, index), state.getKeyType(), state.getValueType())); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/KeyValuePairsState.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/KeyValuePairsState.java new file mode 100644 index 00000000..ea2d4dcd --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/KeyValuePairsState.java @@ -0,0 +1,32 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation.state; + +import com.facebook.presto.operator.aggregation.KeyValuePairs; +import com.facebook.presto.spi.type.Type; + +@AccumulatorStateMetadata(stateFactoryClass = KeyValuePairsStateFactory.class, stateSerializerClass = KeyValuePairStateSerializer.class) +public interface KeyValuePairsState + extends AccumulatorState +{ + KeyValuePairs get(); + + void set(KeyValuePairs value); + + void addMemoryUsage(long memory); + + Type getKeyType(); + + Type getValueType(); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/KeyValuePairsStateFactory.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/KeyValuePairsStateFactory.java new file mode 100644 index 00000000..b3ceba75 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/KeyValuePairsStateFactory.java @@ -0,0 +1,175 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation.state; + +import com.facebook.presto.operator.aggregation.KeyValuePairs; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.util.array.ObjectBigArray; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class KeyValuePairsStateFactory + implements AccumulatorStateFactory +{ + private final Type keyType; + private final Type valueType; + + public KeyValuePairsStateFactory(Type keyType, Type valueType) + { + this.keyType = keyType; + this.valueType = valueType; + } + + @Override + public KeyValuePairsState createSingleState() + { + return new SingleState(keyType, valueType); + } + + @Override + public Class getSingleStateClass() + { + return SingleState.class; + } + + @Override + public KeyValuePairsState createGroupedState() + { + return new GroupedState(keyType, valueType); + } + + @Override + public Class getGroupedStateClass() + { + return GroupedState.class; + } + + public static class GroupedState + extends AbstractGroupedAccumulatorState + implements KeyValuePairsState + { + private final Type keyType; + private final Type valueType; + private final ObjectBigArray pairs = new ObjectBigArray<>(); + private long size; + + public GroupedState(Type keyType, Type valueType) + { + this.keyType = keyType; + this.valueType = valueType; + } + + @Override + public void ensureCapacity(long size) + { + pairs.ensureCapacity(size); + } + + @Override + public KeyValuePairs get() + { + return pairs.get(getGroupId()); + } + + @Override + public void set(KeyValuePairs value) + { + checkNotNull(value, "value is null"); + + KeyValuePairs previous = get(); + if (previous != null) { + size -= previous.estimatedInMemorySize(); + } + + pairs.set(getGroupId(), value); + size += value.estimatedInMemorySize(); + } + + @Override + public void addMemoryUsage(long memory) + { + size += memory; + } + + @Override + public Type getKeyType() + { + return keyType; + } + + @Override + public Type getValueType() + { + return valueType; + } + + @Override + public long getEstimatedSize() + { + return size + pairs.sizeOf(); + } + } + + public static class SingleState + implements KeyValuePairsState + { + private final Type keyType; + private final Type valueType; + private KeyValuePairs pair; + + public SingleState(Type keyType, Type valueType) + { + this.keyType = keyType; + this.valueType = valueType; + } + + @Override + public KeyValuePairs get() + { + return pair; + } + + @Override + public void set(KeyValuePairs value) + { + pair = value; + } + + @Override + public void addMemoryUsage(long memory) + { + } + + @Override + public Type getKeyType() + { + return keyType; + } + + @Override + public Type getValueType() + { + return valueType; + } + + @Override + public long getEstimatedSize() + { + if (pair == null) { + return 0; + } + return pair.estimatedInMemorySize(); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/LongAndDoubleState.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/LongAndDoubleState.java new file mode 100644 index 00000000..3049a284 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/LongAndDoubleState.java @@ -0,0 +1,26 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation.state; + +public interface LongAndDoubleState + extends AccumulatorState +{ + long getLong(); + + void setLong(long value); + + double getDouble(); + + void setDouble(double value); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/LongState.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/LongState.java new file mode 100644 index 00000000..d23c8340 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/LongState.java @@ -0,0 +1,22 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation.state; + +public interface LongState + extends AccumulatorState +{ + long getLong(); + + void setLong(long value); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/MaxOrMinByState.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/MaxOrMinByState.java new file mode 100644 index 00000000..b95941cd --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/MaxOrMinByState.java @@ -0,0 +1,29 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation.state; + +import com.facebook.presto.spi.block.Block; + +@AccumulatorStateMetadata(stateFactoryClass = MaxOrMinByStateFactory.class, stateSerializerClass = MaxOrMinByStateSerializer.class) +public interface MaxOrMinByState + extends AccumulatorState +{ + Block getKey(); + + void setKey(Block key); + + Block getValue(); + + void setValue(Block value); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/MaxOrMinByStateFactory.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/MaxOrMinByStateFactory.java new file mode 100644 index 00000000..0097c173 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/MaxOrMinByStateFactory.java @@ -0,0 +1,134 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation.state; + +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.util.array.BlockBigArray; + +public class MaxOrMinByStateFactory + implements AccumulatorStateFactory +{ + @Override + public MaxOrMinByState createSingleState() + { + return new SingleMaxOrMinByState(); + } + + @Override + public Class getSingleStateClass() + { + return SingleMaxOrMinByState.class; + } + + @Override + public MaxOrMinByState createGroupedState() + { + return new GroupedMaxOrMinByState(); + } + + @Override + public Class getGroupedStateClass() + { + return GroupedMaxOrMinByState.class; + } + + public static class GroupedMaxOrMinByState + extends AbstractGroupedAccumulatorState + implements MaxOrMinByState + { + private final BlockBigArray keys = new BlockBigArray(); + private final BlockBigArray values = new BlockBigArray(); + + @Override + public void ensureCapacity(long size) + { + keys.ensureCapacity(size); + values.ensureCapacity(size); + } + + @Override + public long getEstimatedSize() + { + return keys.sizeOf() + values.sizeOf(); + } + + @Override + public Block getKey() + { + return keys.get(getGroupId()); + } + + @Override + public void setKey(Block key) + { + keys.set(getGroupId(), key); + } + + @Override + public Block getValue() + { + return values.get(getGroupId()); + } + + @Override + public void setValue(Block value) + { + values.set(getGroupId(), value); + } + } + + public static class SingleMaxOrMinByState + implements MaxOrMinByState + { + private Block key; + private Block value; + + @Override + public long getEstimatedSize() + { + long size = 0; + if (key != null) { + size += key.getRetainedSizeInBytes(); + } + if (value != null) { + size += value.getRetainedSizeInBytes(); + } + return size; + } + + @Override + public Block getKey() + { + return key; + } + + @Override + public void setKey(Block key) + { + this.key = key; + } + + @Override + public Block getValue() + { + return value; + } + + @Override + public void setValue(Block value) + { + this.value = value; + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/MaxOrMinByStateSerializer.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/MaxOrMinByStateSerializer.java new file mode 100644 index 00000000..d262e774 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/MaxOrMinByStateSerializer.java @@ -0,0 +1,130 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation.state; + +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.block.BlockBuilderStatus; +import com.facebook.presto.spi.type.Type; +import io.airlift.slice.DynamicSliceOutput; +import io.airlift.slice.Slice; +import io.airlift.slice.SliceInput; +import io.airlift.slice.SliceOutput; + +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; + +public class MaxOrMinByStateSerializer + implements AccumulatorStateSerializer +{ + private final Type valueType; + private final Type keyType; + + public MaxOrMinByStateSerializer(Type valueType, Type keyType) + { + this.valueType = valueType; + this.keyType = keyType; + } + + @Override + public Type getSerializedType() + { + return VARCHAR; + } + + @Override + public void serialize(MaxOrMinByState state, BlockBuilder out) + { + SliceOutput sliceOutput = new DynamicSliceOutput((int) state.getEstimatedSize()); + + int keyLength = 0; + if (state.getKey() != null && !state.getKey().isNull(0)) { + keyLength = state.getKey().getLength(0); + } + sliceOutput.writeInt(keyLength); + + int valueLength = 0; + if (state.getValue() != null && !state.getValue().isNull(0)) { + valueLength = state.getValue().getLength(0); + } + sliceOutput.writeInt(valueLength); + + if (state.getKey() != null && !state.getKey().isNull(0)) { + appendTo(keyType, sliceOutput, state.getKey()); + } + if (state.getValue() != null && !state.getValue().isNull(0)) { + appendTo(valueType, sliceOutput, state.getValue()); + } + Slice slice = sliceOutput.slice(); + out.writeBytes(slice, 0, slice.length()); + out.closeEntry(); + } + + private static void appendTo(Type type, SliceOutput output, Block block) + { + if (type.getJavaType() == long.class) { + output.appendLong(type.getLong(block, 0)); + } + else if (type.getJavaType() == double.class) { + output.appendDouble(type.getDouble(block, 0)); + } + else if (type.getJavaType() == Slice.class) { + output.appendBytes(type.getSlice(block, 0)); + } + else if (type.getJavaType() == boolean.class) { + output.appendByte(type.getBoolean(block, 0) ? 1 : 0); + } + else { + throw new IllegalArgumentException("Unsupported type: " + type.getJavaType().getSimpleName()); + } + } + + @Override + public void deserialize(Block block, int index, MaxOrMinByState state) + { + SliceInput input = block.getSlice(index, 0, block.getLength(index)).getInput(); + + int keyLength = input.readInt(); + int valueLength = input.readInt(); + state.setKey(null); + state.setValue(null); + if (keyLength > 0) { + state.setKey(toBlock(keyType, input, keyLength)); + } + if (valueLength > 0) { + state.setValue(toBlock(valueType, input, valueLength)); + } + } + + private static Block toBlock(Type type, SliceInput input, int length) + { + BlockBuilder builder = type.createBlockBuilder(new BlockBuilderStatus(), 1, length); + if (type.getJavaType() == long.class) { + type.writeLong(builder, input.readLong()); + } + else if (type.getJavaType() == double.class) { + type.writeDouble(builder, input.readDouble()); + } + else if (type.getJavaType() == Slice.class) { + type.writeSlice(builder, input.readSlice(length)); + } + else if (type.getJavaType() == boolean.class) { + type.writeBoolean(builder, input.readByte() != 0); + } + else { + throw new IllegalArgumentException("Unsupported type: " + type.getJavaType().getSimpleName()); + } + + return builder.build(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/NullableBooleanState.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/NullableBooleanState.java new file mode 100644 index 00000000..40782385 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/NullableBooleanState.java @@ -0,0 +1,28 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation.state; + +@AccumulatorStateMetadata(stateSerializerClass = NullableBooleanStateSerializer.class) +public interface NullableBooleanState + extends AccumulatorState +{ + boolean getBoolean(); + + void setBoolean(boolean value); + + @InitialBooleanValue(true) + boolean isNull(); + + void setNull(boolean value); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/NullableBooleanStateSerializer.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/NullableBooleanStateSerializer.java new file mode 100644 index 00000000..d2818e41 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/NullableBooleanStateSerializer.java @@ -0,0 +1,62 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation.state; + +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; + +import static com.facebook.presto.spi.type.BooleanType.BOOLEAN; + +public class NullableBooleanStateSerializer + implements AccumulatorStateSerializer +{ + private final Type type; + + public NullableBooleanStateSerializer() + { + this(BOOLEAN); + } + + public NullableBooleanStateSerializer(Type type) + { + this.type = type; + } + + @Override + public Type getSerializedType() + { + return type; + } + + @Override + public void serialize(NullableBooleanState state, BlockBuilder out) + { + if (state.isNull()) { + out.appendNull(); + } + else { + type.writeBoolean(out, state.getBoolean()); + } + } + + @Override + public void deserialize(Block block, int index, NullableBooleanState state) + { + state.setNull(block.isNull(index)); + if (!state.isNull()) { + state.setBoolean(type.getBoolean(block, index)); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/NullableDoubleState.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/NullableDoubleState.java new file mode 100644 index 00000000..c34ed0ff --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/NullableDoubleState.java @@ -0,0 +1,28 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation.state; + +@AccumulatorStateMetadata(stateSerializerClass = NullableDoubleStateSerializer.class) +public interface NullableDoubleState + extends AccumulatorState +{ + double getDouble(); + + void setDouble(double value); + + @InitialBooleanValue(true) + boolean isNull(); + + void setNull(boolean value); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/NullableDoubleStateSerializer.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/NullableDoubleStateSerializer.java new file mode 100644 index 00000000..87607c8d --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/NullableDoubleStateSerializer.java @@ -0,0 +1,62 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation.state; + +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; + +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; + +public class NullableDoubleStateSerializer + implements AccumulatorStateSerializer +{ + private final Type type; + + public NullableDoubleStateSerializer() + { + this(DOUBLE); + } + + public NullableDoubleStateSerializer(Type type) + { + this.type = type; + } + + @Override + public Type getSerializedType() + { + return type; + } + + @Override + public void serialize(NullableDoubleState state, BlockBuilder out) + { + if (state.isNull()) { + out.appendNull(); + } + else { + type.writeDouble(out, state.getDouble()); + } + } + + @Override + public void deserialize(Block block, int index, NullableDoubleState state) + { + state.setNull(block.isNull(index)); + if (!state.isNull()) { + state.setDouble(type.getDouble(block, index)); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/NullableLongState.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/NullableLongState.java new file mode 100644 index 00000000..98aa1a50 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/NullableLongState.java @@ -0,0 +1,28 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation.state; + +@AccumulatorStateMetadata(stateSerializerClass = NullableLongStateSerializer.class) +public interface NullableLongState + extends AccumulatorState +{ + long getLong(); + + void setLong(long value); + + @InitialBooleanValue(true) + boolean isNull(); + + void setNull(boolean value); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/NullableLongStateSerializer.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/NullableLongStateSerializer.java new file mode 100644 index 00000000..270b59e9 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/NullableLongStateSerializer.java @@ -0,0 +1,62 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation.state; + +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; + +import static com.facebook.presto.spi.type.BigintType.BIGINT; + +public class NullableLongStateSerializer + implements AccumulatorStateSerializer +{ + private final Type type; + + public NullableLongStateSerializer() + { + this(BIGINT); + } + + public NullableLongStateSerializer(Type type) + { + this.type = type; + } + + @Override + public Type getSerializedType() + { + return type; + } + + @Override + public void serialize(NullableLongState state, BlockBuilder out) + { + if (state.isNull()) { + out.appendNull(); + } + else { + type.writeLong(out, state.getLong()); + } + } + + @Override + public void deserialize(Block block, int index, NullableLongState state) + { + state.setNull(block.isNull(index)); + if (!state.isNull()) { + state.setLong(type.getLong(block, index)); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/NumericHistogramStateSerializer.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/NumericHistogramStateSerializer.java new file mode 100644 index 00000000..2c9ebb49 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/NumericHistogramStateSerializer.java @@ -0,0 +1,51 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation.state; + +import com.facebook.presto.operator.aggregation.NumericHistogram; +import com.facebook.presto.operator.aggregation.NumericHistogramAggregation; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; + +import static com.facebook.presto.spi.type.VarbinaryType.VARBINARY; + +public class NumericHistogramStateSerializer + implements AccumulatorStateSerializer +{ + @Override + public Type getSerializedType() + { + return VARBINARY; + } + + @Override + public void serialize(NumericHistogramAggregation.State state, BlockBuilder out) + { + if (state.get() == null) { + out.appendNull(); + } + else { + VARBINARY.writeSlice(out, state.get().serialize()); + } + } + + @Override + public void deserialize(Block block, int index, NumericHistogramAggregation.State state) + { + if (!block.isNull(index)) { + state.set(new NumericHistogram(VARBINARY.getSlice(block, index), NumericHistogramAggregation.ENTRY_BUFFER_SIZE)); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/RegressionState.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/RegressionState.java new file mode 100644 index 00000000..6595b72b --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/RegressionState.java @@ -0,0 +1,22 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation.state; + +public interface RegressionState + extends CovarianceState +{ + double getSumXSquare(); + + void setSumXSquare(double value); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/SliceState.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/SliceState.java new file mode 100644 index 00000000..ede5eda3 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/SliceState.java @@ -0,0 +1,25 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation.state; + +import io.airlift.slice.Slice; + +@AccumulatorStateMetadata(stateSerializerClass = SliceStateSerializer.class) +public interface SliceState + extends AccumulatorState +{ + Slice getSlice(); + + void setSlice(Slice value); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/SliceStateSerializer.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/SliceStateSerializer.java new file mode 100644 index 00000000..b3a291e5 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/SliceStateSerializer.java @@ -0,0 +1,52 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation.state; + +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; + +public class SliceStateSerializer + implements AccumulatorStateSerializer +{ + private final Type type; + + public SliceStateSerializer(Type type) + { + this.type = type; + } + + @Override + public Type getSerializedType() + { + return type; + } + + @Override + public void serialize(SliceState state, BlockBuilder out) + { + if (state.getSlice() == null) { + out.appendNull(); + } + else { + type.writeSlice(out, state.getSlice()); + } + } + + @Override + public void deserialize(Block block, int index, SliceState state) + { + state.setSlice(type.getSlice(block, index)); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/StateCompiler.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/StateCompiler.java new file mode 100644 index 00000000..368752ac --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/StateCompiler.java @@ -0,0 +1,727 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation.state; + +import com.facebook.presto.byteCode.Block; +import com.facebook.presto.byteCode.ClassDefinition; +import com.facebook.presto.byteCode.Scope; +import com.facebook.presto.byteCode.DynamicClassLoader; +import com.facebook.presto.byteCode.FieldDefinition; +import com.facebook.presto.byteCode.MethodDefinition; +import com.facebook.presto.byteCode.Parameter; +import com.facebook.presto.byteCode.Variable; +import com.facebook.presto.byteCode.expression.ByteCodeExpression; +import com.facebook.presto.operator.aggregation.GroupedAccumulator; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.gen.CallSiteBinder; +import com.facebook.presto.util.array.BooleanBigArray; +import com.facebook.presto.util.array.ByteBigArray; +import com.facebook.presto.util.array.DoubleBigArray; +import com.facebook.presto.util.array.LongBigArray; +import com.facebook.presto.util.array.SliceBigArray; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Ordering; +import io.airlift.slice.SizeOf; +import io.airlift.slice.Slice; +import io.airlift.slice.Slices; +import org.openjdk.jol.info.ClassLayout; + +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.facebook.presto.byteCode.Access.FINAL; +import static com.facebook.presto.byteCode.Access.PRIVATE; +import static com.facebook.presto.byteCode.Access.PUBLIC; +import static com.facebook.presto.byteCode.Access.STATIC; +import static com.facebook.presto.byteCode.Access.a; +import static com.facebook.presto.byteCode.Parameter.arg; +import static com.facebook.presto.byteCode.ParameterizedType.type; +import static com.facebook.presto.byteCode.expression.ByteCodeExpressions.add; +import static com.facebook.presto.byteCode.expression.ByteCodeExpressions.constantBoolean; +import static com.facebook.presto.byteCode.expression.ByteCodeExpressions.constantInt; +import static com.facebook.presto.byteCode.expression.ByteCodeExpressions.constantLong; +import static com.facebook.presto.byteCode.expression.ByteCodeExpressions.constantNumber; +import static com.facebook.presto.byteCode.expression.ByteCodeExpressions.defaultValue; +import static com.facebook.presto.byteCode.expression.ByteCodeExpressions.invokeStatic; +import static com.facebook.presto.byteCode.expression.ByteCodeExpressions.newInstance; +import static com.facebook.presto.operator.aggregation.state.StateCompilerUtils.getBlockBuilderAppend; +import static com.facebook.presto.operator.aggregation.state.StateCompilerUtils.getBlockGetter; +import static com.facebook.presto.operator.aggregation.state.StateCompilerUtils.getSliceGetter; +import static com.facebook.presto.operator.aggregation.state.StateCompilerUtils.getSliceSetter; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.BooleanType.BOOLEAN; +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static com.facebook.presto.sql.gen.CompilerUtils.defineClass; +import static com.facebook.presto.sql.gen.CompilerUtils.makeClassName; +import static com.facebook.presto.sql.gen.SqlTypeByteCodeExpression.constantType; +import static com.google.common.base.CaseFormat.LOWER_CAMEL; +import static com.google.common.base.CaseFormat.UPPER_CAMEL; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class StateCompiler +{ + private static Class getBigArrayType(Class type) + { + if (type.equals(long.class)) { + return LongBigArray.class; + } + if (type.equals(byte.class)) { + return ByteBigArray.class; + } + if (type.equals(double.class)) { + return DoubleBigArray.class; + } + if (type.equals(boolean.class)) { + return BooleanBigArray.class; + } + if (type.equals(Slice.class)) { + return SliceBigArray.class; + } + // TODO: support more reference types + throw new IllegalArgumentException("Unsupported type: " + type.getName()); + } + + public AccumulatorStateSerializer generateStateSerializer(Class clazz) + { + return generateStateSerializer(clazz, new DynamicClassLoader(clazz.getClassLoader())); + } + + public AccumulatorStateSerializer generateStateSerializer(Class clazz, DynamicClassLoader classLoader) + { + AccumulatorStateMetadata metadata = getMetadataAnnotation(clazz); + if (metadata != null && metadata.stateSerializerClass() != void.class) { + try { + return (AccumulatorStateSerializer) metadata.stateSerializerClass().getConstructor().newInstance(); + } + catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { + throw Throwables.propagate(e); + } + } + + ClassDefinition definition = new ClassDefinition( + a(PUBLIC, FINAL), + makeClassName(clazz.getSimpleName() + "Serializer"), + type(Object.class), + type(AccumulatorStateSerializer.class)); + + CallSiteBinder callSiteBinder = new CallSiteBinder(); + + // Generate constructor + definition.declareDefaultConstructor(a(PUBLIC)); + + List fields = enumerateFields(clazz); + generateGetSerializedType(definition, fields, callSiteBinder); + generateSerialize(definition, clazz, fields); + generateDeserialize(definition, clazz, fields); + + Class serializerClass = defineClass(definition, AccumulatorStateSerializer.class, callSiteBinder.getBindings(), classLoader); + try { + return (AccumulatorStateSerializer) serializerClass.newInstance(); + } + catch (InstantiationException | IllegalAccessException e) { + throw Throwables.propagate(e); + } + } + + private static void generateGetSerializedType(ClassDefinition definition, List fields, CallSiteBinder callSiteBinder) + { + Block body = definition.declareMethod(a(PUBLIC), "getSerializedType", type(Type.class)).getBody(); + + Type type; + if (fields.size() > 1) { + type = VARCHAR; + } + else { + Class stackType = fields.get(0).getType(); + if (stackType == long.class) { + type = BIGINT; + } + else if (stackType == double.class) { + type = DOUBLE; + } + else if (stackType == boolean.class) { + type = BOOLEAN; + } + else if (stackType == byte.class) { + type = BIGINT; + } + else if (stackType == Slice.class) { + type = VARCHAR; + } + else { + throw new IllegalArgumentException("Unsupported type: " + stackType); + } + } + + body.comment("return %s", type.getTypeSignature()) + .append(constantType(callSiteBinder, type)) + .retObject(); + } + + private static AccumulatorStateMetadata getMetadataAnnotation(Class clazz) + { + AccumulatorStateMetadata metadata = clazz.getAnnotation(AccumulatorStateMetadata.class); + if (metadata != null) { + return metadata; + } + // If the annotation wasn't found, then search the super classes + for (Class superInterface : clazz.getInterfaces()) { + metadata = superInterface.getAnnotation(AccumulatorStateMetadata.class); + if (metadata != null) { + return metadata; + } + } + + return null; + } + + private static void generateDeserialize(ClassDefinition definition, Class clazz, List fields) + { + Parameter block = arg("block", com.facebook.presto.spi.block.Block.class); + Parameter index = arg("index", int.class); + Parameter state = arg("state", Object.class); + MethodDefinition method = definition.declareMethod(a(PUBLIC), "deserialize", type(void.class), block, index, state); + + Block deserializerBody = method.getBody(); + + if (fields.size() == 1) { + Method setter = getSetter(clazz, fields.get(0)); + Method blockGetter = getBlockGetter(setter.getParameterTypes()[0]); + deserializerBody.append(state.cast(setter.getDeclaringClass()).invoke(setter, invokeStatic(blockGetter, block, index))); + } + else { + Variable slice = method.getScope().declareVariable(Slice.class, "slice"); + deserializerBody.append(slice.set(block.invoke("getSlice", Slice.class, index, constantInt(0), block.invoke("getLength", int.class, index)))); + + for (StateField field : fields) { + Method setter = getSetter(clazz, field); + Method getter = getSliceGetter(setter.getParameterTypes()[0]); + int offset = offsetOfField(field, fields); + deserializerBody.append(state.cast(setter.getDeclaringClass()).invoke(setter, invokeStatic(getter, slice, constantInt(offset)))); + } + } + deserializerBody.ret(); + } + + private static void generateSerialize(ClassDefinition definition, Class clazz, List fields) + { + Parameter state = arg("state", Object.class); + Parameter out = arg("out", BlockBuilder.class); + MethodDefinition method = definition.declareMethod(a(PUBLIC), "serialize", type(void.class), state, out); + + Block serializerBody = method.getBody(); + + if (fields.size() == 1) { + Method getter = getGetter(clazz, fields.get(0)); + Method append = getBlockBuilderAppend(getter.getReturnType()); + serializerBody.append(invokeStatic(append, out, state.cast(getter.getDeclaringClass()).invoke(getter))); + } + else { + Variable slice = method.getScope().declareVariable(Slice.class, "slice"); + ByteCodeExpression size = constantInt(serializedSizeOf(clazz)); + serializerBody.append(slice.set(invokeStatic(Slices.class, "allocate", Slice.class, size))); + + for (StateField field : fields) { + Method getter = getGetter(clazz, field); + Method sliceSetter = getSliceSetter(getter.getReturnType()); + serializerBody.append(invokeStatic(sliceSetter, slice, constantInt(offsetOfField(field, fields)), state.cast(getter.getDeclaringClass()).invoke(getter))); + } + serializerBody.append(out.invoke("writeBytes", BlockBuilder.class, slice, constantInt(0), size) + .invoke("closeEntry", BlockBuilder.class) + .pop()); + } + serializerBody.ret(); + } + + /** + * Computes the byte offset to store this field at, when serializing it to a Slice + */ + private static int offsetOfField(StateField targetField, List fields) + { + int offset = 0; + for (StateField field : fields) { + if (targetField.getName().equals(field.getName())) { + break; + } + offset += field.sizeOfType(); + } + + return offset; + } + + /** + * Computes the size in bytes that this state will occupy, when serialized as a Slice + */ + private static int serializedSizeOf(Class stateClass) + { + List fields = enumerateFields(stateClass); + int size = 0; + for (StateField field : fields) { + size += field.sizeOfType(); + } + return size; + } + + private static Method getSetter(Class clazz, StateField field) + { + try { + return clazz.getMethod(field.getSetterName(), field.getType()); + } + catch (NoSuchMethodException e) { + throw Throwables.propagate(e); + } + } + + private static Method getGetter(Class clazz, StateField field) + { + try { + return clazz.getMethod(field.getGetterName()); + } + catch (NoSuchMethodException e) { + throw Throwables.propagate(e); + } + } + + public AccumulatorStateFactory generateStateFactory(Class clazz) + { + return generateStateFactory(clazz, new DynamicClassLoader(clazz.getClassLoader())); + } + + public AccumulatorStateFactory generateStateFactory(Class clazz, DynamicClassLoader classLoader) + { + AccumulatorStateMetadata metadata = getMetadataAnnotation(clazz); + if (metadata != null && metadata.stateFactoryClass() != void.class) { + try { + return (AccumulatorStateFactory) metadata.stateFactoryClass().getConstructor().newInstance(); + } + catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { + throw Throwables.propagate(e); + } + } + + Class singleStateClass = generateSingleStateClass(clazz, classLoader); + Class groupedStateClass = generateGroupedStateClass(clazz, classLoader); + + ClassDefinition definition = new ClassDefinition( + a(PUBLIC, FINAL), + makeClassName(clazz.getSimpleName() + "Factory"), + type(Object.class), + type(AccumulatorStateFactory.class)); + + // Generate constructor + definition.declareDefaultConstructor(a(PUBLIC)); + + // Generate single state creation method + definition.declareMethod(a(PUBLIC), "createSingleState", type(Object.class)) + .getBody() + .newObject(singleStateClass) + .dup() + .invokeConstructor(singleStateClass) + .retObject(); + + // Generate grouped state creation method + definition.declareMethod(a(PUBLIC), "createGroupedState", type(Object.class)) + .getBody() + .newObject(groupedStateClass) + .dup() + .invokeConstructor(groupedStateClass) + .retObject(); + + // Generate getters for state class + definition.declareMethod(a(PUBLIC), "getSingleStateClass", type(Class.class, singleStateClass)) + .getBody() + .push(singleStateClass) + .retObject(); + + definition.declareMethod(a(PUBLIC), "getGroupedStateClass", type(Class.class, groupedStateClass)) + .getBody() + .push(groupedStateClass) + .retObject(); + + Class factoryClass = defineClass(definition, AccumulatorStateFactory.class, classLoader); + try { + return (AccumulatorStateFactory) factoryClass.newInstance(); + } + catch (InstantiationException | IllegalAccessException e) { + throw Throwables.propagate(e); + } + } + + private static Class generateSingleStateClass(Class clazz, DynamicClassLoader classLoader) + { + ClassDefinition definition = new ClassDefinition( + a(PUBLIC, FINAL), + makeClassName("Single" + clazz.getSimpleName()), + type(Object.class), + type(clazz)); + + // Store class size in static field + FieldDefinition classSize = definition.declareField(a(PRIVATE, STATIC, FINAL), "CLASS_SIZE", long.class); + definition.getClassInitializer() + .getBody() + .comment("CLASS_SIZE = ClassLayout.parseClass(%s.class).instanceSize()", definition.getName()) + .push(definition.getType()) + .invokeStatic(ClassLayout.class, "parseClass", ClassLayout.class, Class.class) + .invokeVirtual(ClassLayout.class, "instanceSize", int.class) + .intToLong() + .putStaticField(classSize); + + // Add getter for class size + definition.declareMethod(a(PUBLIC), "getEstimatedSize", type(long.class)) + .getBody() + .getStaticField(classSize) + .retLong(); + + // Generate constructor + MethodDefinition constructor = definition.declareConstructor(a(PUBLIC)); + + constructor.getBody() + .append(constructor.getThis()) + .invokeConstructor(Object.class); + + // Generate fields + List fields = enumerateFields(clazz); + for (StateField field : fields) { + generateField(definition, constructor, field); + } + + constructor.getBody() + .ret(); + + return defineClass(definition, clazz, classLoader); + } + + private static Class generateGroupedStateClass(Class clazz, DynamicClassLoader classLoader) + { + ClassDefinition definition = new ClassDefinition( + a(PUBLIC, FINAL), + makeClassName("Grouped" + clazz.getSimpleName()), + type(AbstractGroupedAccumulatorState.class), + type(clazz), + type(GroupedAccumulator.class)); + + List fields = enumerateFields(clazz); + + // Create constructor + MethodDefinition constructor = definition.declareConstructor(a(PUBLIC)); + constructor.getBody() + .append(constructor.getThis()) + .invokeConstructor(AbstractGroupedAccumulatorState.class); + + // Create ensureCapacity + MethodDefinition ensureCapacity = definition.declareMethod(a(PUBLIC), "ensureCapacity", type(void.class), arg("size", long.class)); + + // Generate fields, constructor, and ensureCapacity + List fieldDefinitions = new ArrayList<>(); + for (StateField field : fields) { + fieldDefinitions.add(generateGroupedField(definition, constructor, ensureCapacity, field)); + } + + constructor.getBody().ret(); + ensureCapacity.getBody().ret(); + + // Generate getEstimatedSize + MethodDefinition getEstimatedSize = definition.declareMethod(a(PUBLIC), "getEstimatedSize", type(long.class)); + Block body = getEstimatedSize.getBody(); + + Variable size = getEstimatedSize.getScope().declareVariable(long.class, "size"); + + // initialize size to 0L + body.append(size.set(constantLong(0))); + + // add field to size + for (FieldDefinition field : fieldDefinitions) { + body.append(size.set(add(size, getEstimatedSize.getThis().getField(field).invoke("sizeOf", long.class)))); + } + + // return size + body.append(size.ret()); + + return defineClass(definition, clazz, classLoader); + } + + private static void generateField(ClassDefinition definition, MethodDefinition constructor, StateField stateField) + { + FieldDefinition field = definition.declareField(a(PRIVATE), UPPER_CAMEL.to(LOWER_CAMEL, stateField.getName()) + "Value", stateField.getType()); + + // Generate getter + MethodDefinition getter = definition.declareMethod(a(PUBLIC), stateField.getGetterName(), type(stateField.getType())); + getter.getBody() + .append(getter.getThis().getField(field).ret()); + + // Generate setter + Parameter value = arg("value", stateField.getType()); + MethodDefinition setter = definition.declareMethod(a(PUBLIC), stateField.getSetterName(), type(void.class), value); + setter.getBody() + .append(setter.getThis().setField(field, value)) + .ret(); + + constructor.getBody() + .append(constructor.getThis().setField(field, stateField.initialValueExpression())); + } + + private static FieldDefinition generateGroupedField(ClassDefinition definition, MethodDefinition constructor, MethodDefinition ensureCapacity, StateField stateField) + { + Class bigArrayType = getBigArrayType(stateField.getType()); + FieldDefinition field = definition.declareField(a(PRIVATE), UPPER_CAMEL.to(LOWER_CAMEL, stateField.getName()) + "Values", bigArrayType); + + // Generate getter + MethodDefinition getter = definition.declareMethod(a(PUBLIC), stateField.getGetterName(), type(stateField.getType())); + getter.getBody() + .append(getter.getThis().getField(field).invoke( + "get", + stateField.getType(), + getter.getThis().invoke("getGroupId", long.class)) + .ret()); + + // Generate setter + Parameter value = arg("value", stateField.getType()); + MethodDefinition setter = definition.declareMethod(a(PUBLIC), stateField.getSetterName(), type(void.class), value); + setter.getBody() + .append(setter.getThis().getField(field).invoke( + "set", + void.class, + setter.getThis().invoke("getGroupId", long.class), + value)) + .ret(); + + Scope ensureCapacityScope = ensureCapacity.getScope(); + ensureCapacity.getBody() + .append(ensureCapacity.getThis().getField(field).invoke("ensureCapacity", void.class, ensureCapacityScope.getVariable("size"))); + + // Initialize field in constructor + constructor.getBody() + .append(constructor.getThis().setField(field, newInstance(field.getType(), stateField.initialValueExpression()))); + + return field; + } + + /** + * Enumerates all the fields in this state interface. + * + * @param clazz a subclass of AccumulatorState + * @return list of state fields. Ordering is guaranteed to be stable, and have all primitive fields at the beginning. + */ + private static List enumerateFields(Class clazz) + { + ImmutableList.Builder builder = ImmutableList.builder(); + final Set> primitiveClasses = ImmutableSet.>of(byte.class, boolean.class, long.class, double.class); + Set> supportedClasses = ImmutableSet.>of(byte.class, boolean.class, long.class, double.class, Slice.class); + + for (Method method : clazz.getMethods()) { + if (method.getName().equals("getEstimatedSize")) { + continue; + } + if (method.getName().startsWith("get")) { + Class type = method.getReturnType(); + checkArgument(supportedClasses.contains(type), type.getName() + " is not supported"); + String name = method.getName().substring(3); + builder.add(new StateField(name, type, getInitialValue(method))); + } + if (method.getName().startsWith("is")) { + Class type = method.getReturnType(); + checkArgument(type == boolean.class, "Only boolean is support for 'is' methods"); + String name = method.getName().substring(2); + builder.add(new StateField(name, type, getInitialValue(method), method.getName())); + } + } + + // We need this ordering because the serializer and deserializer are on different machines, and so the ordering of fields must be stable + Ordering ordering = new Ordering() + { + @Override + public int compare(StateField left, StateField right) + { + if (primitiveClasses.contains(left.getType()) && !primitiveClasses.contains(right.getType())) { + return -1; + } + if (primitiveClasses.contains(right.getType()) && !primitiveClasses.contains(left.getType())) { + return 1; + } + // If they're the category, just sort by name + return left.getName().compareTo(right.getName()); + } + }; + List fields = ordering.sortedCopy(builder.build()); + checkInterface(clazz, fields); + + return fields; + } + + private static Object getInitialValue(Method method) + { + Object value = null; + + for (Annotation annotation : method.getAnnotations()) { + if (annotation instanceof InitialLongValue) { + checkArgument(value == null, "%s has multiple initialValue annotations", method.getName()); + checkArgument(method.getReturnType() == long.class, "%s does not return a long, but is annotated with @InitialLongValue", method.getName()); + value = ((InitialLongValue) annotation).value(); + } + else if (annotation instanceof InitialDoubleValue) { + checkArgument(value == null, "%s has multiple initialValue annotations", method.getName()); + checkArgument(method.getReturnType() == double.class, "%s does not return a double, but is annotated with @InitialDoubleValue", method.getName()); + value = ((InitialDoubleValue) annotation).value(); + } + else if (annotation instanceof InitialBooleanValue) { + checkArgument(value == null, "%s has multiple initialValue annotations", method.getName()); + checkArgument(method.getReturnType() == boolean.class, "%s does not return a boolean, but is annotated with @InitialBooleanValue", method.getName()); + value = ((InitialBooleanValue) annotation).value(); + } + } + + return value; + } + + private static void checkInterface(Class clazz, List fields) + { + checkArgument(clazz.isInterface(), clazz.getName() + " is not an interface"); + Set setters = new HashSet<>(); + Set getters = new HashSet<>(); + Set isGetters = new HashSet<>(); + + Map> fieldTypes = new HashMap<>(); + for (StateField field : fields) { + fieldTypes.put(field.getName(), field.getType()); + } + + for (Method method : clazz.getMethods()) { + if (method.getName().equals("getEstimatedSize")) { + checkArgument(method.getReturnType().equals(long.class), "getEstimatedSize must return long"); + checkArgument(method.getParameterTypes().length == 0, "getEstimatedSize may not have parameters"); + continue; + } + + if (method.getName().startsWith("get")) { + String name = method.getName().substring(3); + checkArgument(fieldTypes.get(name).equals(method.getReturnType()), + "Expected %s to return type %s, but found %s", method.getName(), fieldTypes.get(name), method.getReturnType()); + checkArgument(method.getParameterTypes().length == 0, "Expected %s to have zero parameters", method.getName()); + getters.add(name); + } + else if (method.getName().startsWith("is")) { + String name = method.getName().substring(2); + checkArgument(fieldTypes.get(name) == boolean.class, + "Expected %s to have type boolean, but found %s", name, fieldTypes.get(name)); + checkArgument(method.getParameterTypes().length == 0, "Expected %s to have zero parameters", method.getName()); + checkArgument(method.getReturnType() == boolean.class, "Expected %s to return boolean", method.getName()); + isGetters.add(name); + } + else if (method.getName().startsWith("set")) { + String name = method.getName().substring(3); + checkArgument(method.getParameterTypes().length == 1, "Expected setter to have one parameter"); + checkArgument(fieldTypes.get(name).equals(method.getParameterTypes()[0]), + "Expected %s to accept type %s, but found %s", method.getName(), fieldTypes.get(name), method.getParameterTypes()[0]); + checkArgument(getInitialValue(method) == null, "initial value annotation not allowed on setter"); + checkArgument(method.getReturnType().equals(void.class), "%s may not return a value", method.getName()); + setters.add(name); + } + else { + throw new IllegalArgumentException("Cannot generate implementation for method: " + method.getName()); + } + } + checkArgument(getters.size() + isGetters.size() == setters.size() && setters.size() == fields.size(), "Wrong number of getters/setters"); + } + + private static final class StateField + { + private final String name; + private final String getterName; + private final Class type; + private final Object initialValue; + + private StateField(String name, Class type, Object initialValue) + { + this(name, type, initialValue, "get" + name); + } + + private StateField(String name, Class type, Object initialValue, String getterName) + { + this.name = checkNotNull(name, "name is null"); + checkArgument(!name.isEmpty(), "name is empty"); + this.type = checkNotNull(type, "type is null"); + this.getterName = checkNotNull(getterName, "getterName is null"); + this.initialValue = initialValue; + } + + public String getGetterName() + { + return getterName; + } + + public String getSetterName() + { + return "set" + getName(); + } + + public String getName() + { + return name; + } + + public Class getType() + { + return type; + } + + public int sizeOfType() + { + if (getType() == long.class) { + return SizeOf.SIZE_OF_LONG; + } + else if (getType() == double.class) { + return SizeOf.SIZE_OF_DOUBLE; + } + else if (getType() == boolean.class || getType() == byte.class) { + return SizeOf.SIZE_OF_BYTE; + } + else { + throw new IllegalArgumentException("Unsupported type: " + getType()); + } + } + + public Object getInitialValue() + { + return initialValue; + } + + public ByteCodeExpression initialValueExpression() + { + if (initialValue == null) { + return defaultValue(type); + } + if (initialValue instanceof Number) { + return constantNumber((Number) initialValue); + } + else if (initialValue instanceof Boolean) { + return constantBoolean((boolean) initialValue); + } + else { + throw new IllegalArgumentException("Unsupported initial value type: " + initialValue.getClass()); + } + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/StateCompilerUtils.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/StateCompilerUtils.java new file mode 100644 index 00000000..27e1fece --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/StateCompilerUtils.java @@ -0,0 +1,230 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation.state; + +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.google.common.base.Throwables; +import io.airlift.slice.Slice; + +import javax.annotation.Nullable; + +import java.lang.reflect.Method; + +public final class StateCompilerUtils +{ + private StateCompilerUtils() {} + + public static Method getBlockGetter(Class type) + { + try { + if (type == long.class) { + return StateCompilerUtils.class.getMethod("getLongBlock", Block.class, int.class); + } + else if (type == double.class) { + return StateCompilerUtils.class.getMethod("getDoubleBlock", Block.class, int.class); + } + else if (type == boolean.class) { + return StateCompilerUtils.class.getMethod("getBooleanBlock", Block.class, int.class); + } + else if (type == byte.class) { + return StateCompilerUtils.class.getMethod("getByteBlock", Block.class, int.class); + } + else if (type == Slice.class) { + return StateCompilerUtils.class.getMethod("getSliceBlock", Block.class, int.class); + } + else { + throw new IllegalArgumentException("Unsupported type: " + type.getSimpleName()); + } + } + catch (NoSuchMethodException e) { + throw Throwables.propagate(e); + } + } + + public static Method getBlockBuilderAppend(Class type) + { + try { + if (type == long.class) { + return StateCompilerUtils.class.getMethod("appendLongBlockBuilder", BlockBuilder.class, long.class); + } + else if (type == double.class) { + return StateCompilerUtils.class.getMethod("appendDoubleBlockBuilder", BlockBuilder.class, double.class); + } + else if (type == boolean.class) { + return StateCompilerUtils.class.getMethod("appendBooleanBlockBuilder", BlockBuilder.class, boolean.class); + } + else if (type == byte.class) { + return StateCompilerUtils.class.getMethod("appendByteBlockBuilder", BlockBuilder.class, byte.class); + } + else if (type == Slice.class) { + return StateCompilerUtils.class.getMethod("appendSliceBlockBuilder", BlockBuilder.class, Slice.class); + } + else { + throw new IllegalArgumentException("Unsupported type: " + type.getSimpleName()); + } + } + catch (NoSuchMethodException e) { + throw Throwables.propagate(e); + } + } + + public static Method getSliceSetter(Class type) + { + try { + if (type == long.class) { + return StateCompilerUtils.class.getMethod("setLongSlice", Slice.class, int.class, long.class); + } + else if (type == double.class) { + return StateCompilerUtils.class.getMethod("setDoubleSlice", Slice.class, int.class, double.class); + } + else if (type == boolean.class) { + return StateCompilerUtils.class.getMethod("setBooleanSlice", Slice.class, int.class, boolean.class); + } + else if (type == byte.class) { + return StateCompilerUtils.class.getMethod("setByteSlice", Slice.class, int.class, byte.class); + } + else { + throw new IllegalArgumentException("Unsupported type: " + type.getSimpleName()); + } + } + catch (NoSuchMethodException e) { + throw Throwables.propagate(e); + } + } + + public static Method getSliceGetter(Class type) + { + try { + if (type == long.class) { + return StateCompilerUtils.class.getMethod("getLongSlice", Slice.class, int.class); + } + else if (type == double.class) { + return StateCompilerUtils.class.getMethod("getDoubleSlice", Slice.class, int.class); + } + else if (type == boolean.class) { + return StateCompilerUtils.class.getMethod("getBooleanSlice", Slice.class, int.class); + } + else if (type == byte.class) { + return StateCompilerUtils.class.getMethod("getByteSlice", Slice.class, int.class); + } + else { + throw new IllegalArgumentException("Unsupported type: " + type.getSimpleName()); + } + } + catch (NoSuchMethodException e) { + throw Throwables.propagate(e); + } + } + + public static long getLongBlock(Block block, int index) + { + return block.getLong(index, 0); + } + + public static byte getByteBlock(Block block, int index) + { + return (byte) block.getLong(index, 0); + } + + public static double getDoubleBlock(Block block, int index) + { + return block.getDouble(index, 0); + } + + public static boolean getBooleanBlock(Block block, int index) + { + return block.getByte(index, 0) != 0; + } + + public static Slice getSliceBlock(Block block, int index) + { + if (block.isNull(index)) { + return null; + } + else { + return block.getSlice(index, 0, block.getLength(index)); + } + } + + public static void appendLongBlockBuilder(BlockBuilder builder, long value) + { + builder.writeLong(value).closeEntry(); + } + + public static void appendDoubleBlockBuilder(BlockBuilder builder, double value) + { + builder.writeDouble(value).closeEntry(); + } + + public static void appendBooleanBlockBuilder(BlockBuilder builder, boolean value) + { + builder.writeByte(value ? 1 : 0).closeEntry(); + } + + public static void appendByteBlockBuilder(BlockBuilder builder, byte value) + { + builder.writeLong(value).closeEntry(); + } + + public static void appendSliceBlockBuilder(BlockBuilder builder, @Nullable Slice value) + { + if (value == null) { + builder.appendNull(); + } + else { + builder.writeBytes(value, 0, value.length()).closeEntry(); + } + } + + public static void setLongSlice(Slice slice, int offset, long value) + { + slice.setLong(offset, value); + } + + public static void setDoubleSlice(Slice slice, int offset, double value) + { + slice.setDouble(offset, value); + } + + public static void setBooleanSlice(Slice slice, int offset, boolean value) + { + slice.setByte(offset, value ? 1 : 0); + } + + public static void setByteSlice(Slice slice, int offset, byte value) + { + slice.setByte(offset, value); + } + + public static long getLongSlice(Slice slice, int offset) + { + return slice.getLong(offset); + } + + public static double getDoubleSlice(Slice slice, int offset) + { + return slice.getDouble(offset); + } + + public static boolean getBooleanSlice(Slice slice, int offset) + { + return slice.getByte(offset) == 1; + } + + public static byte getByteSlice(Slice slice, int offset) + { + return slice.getByte(offset); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/TriStateBooleanState.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/TriStateBooleanState.java new file mode 100644 index 00000000..7ee5ac45 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/TriStateBooleanState.java @@ -0,0 +1,27 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation.state; + +@AccumulatorStateMetadata(stateSerializerClass = TriStateBooleanStateSerializer.class) +public interface TriStateBooleanState + extends AccumulatorState +{ + byte NULL_VALUE = 0; + byte TRUE_VALUE = 1; + byte FALSE_VALUE = -1; + + byte getByte(); + + void setByte(byte value); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/TriStateBooleanStateSerializer.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/TriStateBooleanStateSerializer.java new file mode 100644 index 00000000..1ef3acff --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/TriStateBooleanStateSerializer.java @@ -0,0 +1,55 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation.state; + +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; + +import static com.facebook.presto.operator.aggregation.state.TriStateBooleanState.FALSE_VALUE; +import static com.facebook.presto.operator.aggregation.state.TriStateBooleanState.NULL_VALUE; +import static com.facebook.presto.operator.aggregation.state.TriStateBooleanState.TRUE_VALUE; +import static com.facebook.presto.spi.type.BooleanType.BOOLEAN; + +public class TriStateBooleanStateSerializer + implements AccumulatorStateSerializer +{ + @Override + public Type getSerializedType() + { + return BOOLEAN; + } + + @Override + public void serialize(TriStateBooleanState state, BlockBuilder out) + { + if (state.getByte() == NULL_VALUE) { + out.appendNull(); + } + else { + out.writeByte(state.getByte() == TRUE_VALUE ? 1 : 0).closeEntry(); + } + } + + @Override + public void deserialize(Block block, int index, TriStateBooleanState state) + { + if (block.isNull(index)) { + state.setByte(NULL_VALUE); + } + else { + state.setByte(BOOLEAN.getBoolean(block, index) ? TRUE_VALUE : FALSE_VALUE); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/VarianceState.java b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/VarianceState.java new file mode 100644 index 00000000..d5a4e0d2 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/aggregation/state/VarianceState.java @@ -0,0 +1,30 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.aggregation.state; + +public interface VarianceState + extends AccumulatorState +{ + long getCount(); + + void setCount(long value); + + double getMean(); + + void setMean(double value); + + double getM2(); + + void setM2(double value); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/index/DynamicTupleFilterFactory.java b/presto-main/src/main/java/com/facebook/presto/operator/index/DynamicTupleFilterFactory.java new file mode 100644 index 00000000..b48bc8b8 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/index/DynamicTupleFilterFactory.java @@ -0,0 +1,65 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.index; + +import com.facebook.presto.operator.OperatorFactory; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.facebook.presto.operator.FilterAndProjectOperator.FilterAndProjectOperatorFactory; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class DynamicTupleFilterFactory +{ + private final int filterOperatorId; + private final int[] tupleFilterChannels; + private final int[] outputFilterChannels; + private final List outputTypes; + + public DynamicTupleFilterFactory(int filterOperatorId, int[] tupleFilterChannels, int[] outputFilterChannels, List outputTypes) + { + checkNotNull(tupleFilterChannels, "tupleFilterChannels is null"); + checkArgument(tupleFilterChannels.length > 0, "Must have at least one tupleFilterChannel"); + checkNotNull(outputFilterChannels, "outputFilterChannels is null"); + checkArgument(outputFilterChannels.length == tupleFilterChannels.length, "outputFilterChannels must have same length as tupleFilterChannels"); + checkNotNull(outputTypes, "outputTypes is null"); + checkArgument(outputTypes.size() >= outputFilterChannels.length, "Must have at least as many output channels as those used for filtering"); + + this.filterOperatorId = filterOperatorId; + this.tupleFilterChannels = tupleFilterChannels.clone(); + this.outputFilterChannels = outputFilterChannels.clone(); + this.outputTypes = ImmutableList.copyOf(outputTypes); + } + + public OperatorFactory filterWithTuple(Page tuplePage) + { + Page normalizedTuplePage = normalizeTuplePage(tuplePage); + TupleFilterProcessor processor = new TupleFilterProcessor(normalizedTuplePage, outputTypes, outputFilterChannels); + return new FilterAndProjectOperatorFactory(filterOperatorId, processor, outputTypes); + } + + private Page normalizeTuplePage(Page tuplePage) + { + Block[] normalizedBlocks = new Block[tupleFilterChannels.length]; + for (int i = 0; i < tupleFilterChannels.length; i++) { + normalizedBlocks[i] = tuplePage.getBlock(tupleFilterChannels[i]); + } + return new Page(normalizedBlocks); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/index/FieldSetFilteringRecordSet.java b/presto-main/src/main/java/com/facebook/presto/operator/index/FieldSetFilteringRecordSet.java new file mode 100644 index 00000000..56de80e7 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/index/FieldSetFilteringRecordSet.java @@ -0,0 +1,190 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.index; + +import com.facebook.presto.spi.RecordCursor; +import com.facebook.presto.spi.RecordSet; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; + +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Only retains rows that have identical values for each respective fieldSet. + */ +public class FieldSetFilteringRecordSet + implements RecordSet +{ + private final RecordSet delegate; + private final List> fieldSets; + + public FieldSetFilteringRecordSet(RecordSet delegate, List> fieldSets) + { + this.delegate = checkNotNull(delegate, "delegate is null"); + this.fieldSets = ImmutableList.copyOf(checkNotNull(fieldSets, "fieldSets is null")); + } + + @Override + public List getColumnTypes() + { + return delegate.getColumnTypes(); + } + + @Override + public RecordCursor cursor() + { + return new FieldSetFilteringRecordCursor(delegate.cursor(), fieldSets); + } + + private static class FieldSetFilteringRecordCursor + implements RecordCursor + { + private final RecordCursor delegate; + private final List> fieldSets; + + private FieldSetFilteringRecordCursor(RecordCursor delegate, List> fieldSets) + { + this.delegate = delegate; + this.fieldSets = fieldSets; + } + + @Override + public long getTotalBytes() + { + return delegate.getTotalBytes(); + } + + @Override + public long getCompletedBytes() + { + return delegate.getCompletedBytes(); + } + + @Override + public long getReadTimeNanos() + { + return delegate.getReadTimeNanos(); + } + + @Override + public Type getType(int field) + { + return delegate.getType(field); + } + + @Override + public boolean advanceNextPosition() + { + while (delegate.advanceNextPosition()) { + if (fieldSetsEqual(delegate, fieldSets)) { + return true; + } + } + return false; + } + + private static boolean fieldSetsEqual(RecordCursor cursor, List> fieldSets) + { + for (Set fieldSet : fieldSets) { + if (!fieldsEquals(cursor, fieldSet)) { + return false; + } + } + return true; + } + + private static boolean fieldsEquals(RecordCursor cursor, Set fields) + { + if (fields.size() < 2) { + return true; // Nothing to compare + } + Iterator fieldIterator = fields.iterator(); + int firstField = fieldIterator.next(); + while (fieldIterator.hasNext()) { + if (!fieldEquals(cursor, firstField, fieldIterator.next())) { + return false; + } + } + return true; + } + + private static boolean fieldEquals(RecordCursor cursor, int field1, int field2) + { + checkArgument(cursor.getType(field1).equals(cursor.getType(field2)), "Should only be comparing fields of the same type"); + + if (cursor.isNull(field1) || cursor.isNull(field2)) { + return false; + } + + Class javaType = cursor.getType(field1).getJavaType(); + if (javaType == long.class) { + return cursor.getLong(field1) == cursor.getLong(field2); + } + else if (javaType == double.class) { + return cursor.getDouble(field1) == cursor.getDouble(field2); + } + else if (javaType == boolean.class) { + return cursor.getBoolean(field1) == cursor.getBoolean(field2); + } + else if (javaType == Slice.class) { + return cursor.getSlice(field1).equals(cursor.getSlice(field2)); + } + else { + throw new IllegalArgumentException("Unknown java type: " + javaType); + } + } + + @Override + public boolean getBoolean(int field) + { + return delegate.getBoolean(field); + } + + @Override + public long getLong(int field) + { + return delegate.getLong(field); + } + + @Override + public double getDouble(int field) + { + return delegate.getDouble(field); + } + + @Override + public Slice getSlice(int field) + { + return delegate.getSlice(field); + } + + @Override + public boolean isNull(int field) + { + return delegate.isNull(field); + } + + @Override + public void close() + { + delegate.close(); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/index/IndexBuildDriverFactoryProvider.java b/presto-main/src/main/java/com/facebook/presto/operator/index/IndexBuildDriverFactoryProvider.java new file mode 100644 index 00000000..0be66109 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/index/IndexBuildDriverFactoryProvider.java @@ -0,0 +1,79 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.index; + +import com.facebook.presto.operator.DriverFactory; +import com.facebook.presto.operator.OperatorFactory; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; + +import java.util.List; +import java.util.Optional; + +import static com.facebook.presto.operator.index.PageBufferOperator.PageBufferOperatorFactory; +import static com.facebook.presto.operator.index.PagesIndexBuilderOperator.PagesIndexBuilderOperatorFactory; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class IndexBuildDriverFactoryProvider +{ + private final int outputOperatorId; + private final boolean inputDriver; + private final List coreOperatorFactories; + private final List outputTypes; + private final Optional dynamicTupleFilterFactory; + + public IndexBuildDriverFactoryProvider(int outputOperatorId, boolean inputDriver, List coreOperatorFactories, Optional dynamicTupleFilterFactory) + { + checkNotNull(coreOperatorFactories, "coreOperatorFactories is null"); + checkArgument(!coreOperatorFactories.isEmpty(), "coreOperatorFactories is empty"); + checkNotNull(dynamicTupleFilterFactory, "dynamicTupleFilterFactory is null"); + + this.outputOperatorId = outputOperatorId; + this.inputDriver = inputDriver; + this.coreOperatorFactories = ImmutableList.copyOf(coreOperatorFactories); + this.outputTypes = ImmutableList.copyOf(this.coreOperatorFactories.get(this.coreOperatorFactories.size() - 1).getTypes()); + this.dynamicTupleFilterFactory = dynamicTupleFilterFactory; + } + + public List getOutputTypes() + { + return outputTypes; + } + + public DriverFactory createSnapshot(IndexSnapshotBuilder indexSnapshotBuilder) + { + checkArgument(indexSnapshotBuilder.getOutputTypes().equals(outputTypes)); + return new DriverFactory(inputDriver, false, ImmutableList.builder() + .addAll(coreOperatorFactories) + .add(new PagesIndexBuilderOperatorFactory(outputOperatorId, indexSnapshotBuilder)) + .build()); + } + + public DriverFactory createStreaming(PageBuffer pageBuffer, Page indexKeyTuple) + { + ImmutableList.Builder operatorFactories = ImmutableList.builder() + .addAll(coreOperatorFactories); + + if (dynamicTupleFilterFactory.isPresent()) { + // Bind in a dynamic tuple filter if necessary + operatorFactories.add(dynamicTupleFilterFactory.get().filterWithTuple(indexKeyTuple)); + } + + operatorFactories.add(new PageBufferOperatorFactory(outputOperatorId, pageBuffer)); + + return new DriverFactory(inputDriver, false, operatorFactories.build()); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/index/IndexJoinLookupStats.java b/presto-main/src/main/java/com/facebook/presto/operator/index/IndexJoinLookupStats.java new file mode 100644 index 00000000..f6bef1ce --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/index/IndexJoinLookupStats.java @@ -0,0 +1,90 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.index; + +import io.airlift.stats.CounterStat; +import org.weakref.jmx.Managed; +import org.weakref.jmx.Nested; + +import javax.annotation.concurrent.ThreadSafe; + +@ThreadSafe +public class IndexJoinLookupStats +{ + private final CounterStat totalIndexJoinLookups = new CounterStat(); + private final CounterStat successfulIndexJoinLookupsByCacheReset = new CounterStat(); + private final CounterStat successfulIndexJoinLookupsBySingleRequest = new CounterStat(); + private final CounterStat successfulIndexJoinLookupsByLimitedRequest = new CounterStat(); + private final CounterStat streamedIndexJoinLookups = new CounterStat(); + + @Managed + @Nested + public CounterStat getTotalIndexJoinLookups() + { + return totalIndexJoinLookups; + } + + @Managed + @Nested + public CounterStat getSuccessfulIndexJoinLookupsByCacheReset() + { + return successfulIndexJoinLookupsByCacheReset; + } + + @Managed + @Nested + public CounterStat getSuccessfulIndexJoinLookupsBySingleRequest() + { + return successfulIndexJoinLookupsBySingleRequest; + } + + @Managed + @Nested + public CounterStat getSuccessfulIndexJoinLookupsByLimitedRequest() + { + return successfulIndexJoinLookupsByLimitedRequest; + } + + @Managed + @Nested + public CounterStat getStreamedIndexJoinLookups() + { + return streamedIndexJoinLookups; + } + + public void recordIndexJoinLookup() + { + totalIndexJoinLookups.update(1); + } + + public void recordSuccessfulIndexJoinLookupByCacheReset() + { + successfulIndexJoinLookupsByCacheReset.update(1); + } + + public void recordSuccessfulIndexJoinLookupBySingleRequest() + { + successfulIndexJoinLookupsBySingleRequest.update(1); + } + + public void recordSuccessfulIndexJoinLookupByLimitedRequest() + { + successfulIndexJoinLookupsByLimitedRequest.update(1); + } + + public void recordStreamedIndexJoinLookup() + { + streamedIndexJoinLookups.update(1); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/index/IndexLoader.java b/presto-main/src/main/java/com/facebook/presto/operator/index/IndexLoader.java new file mode 100644 index 00000000..6bafe681 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/index/IndexLoader.java @@ -0,0 +1,410 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.index; + +import com.facebook.presto.ScheduledSplit; +import com.facebook.presto.TaskSource; +import com.facebook.presto.metadata.Split; +import com.facebook.presto.operator.Driver; +import com.facebook.presto.operator.DriverFactory; +import com.facebook.presto.operator.LookupSource; +import com.facebook.presto.operator.PipelineContext; +import com.facebook.presto.operator.TaskContext; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.planner.plan.PlanNodeId; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.util.concurrent.ListenableFuture; +import io.airlift.units.DataSize; +import it.unimi.dsi.fastutil.longs.LongIterator; + +import javax.annotation.concurrent.GuardedBy; +import javax.annotation.concurrent.NotThreadSafe; +import javax.annotation.concurrent.ThreadSafe; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicReference; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Predicates.equalTo; +import static com.google.common.base.Predicates.not; +import static com.google.common.collect.Iterables.filter; + +@ThreadSafe +public class IndexLoader +{ + private final BlockingQueue updateRequests = new LinkedBlockingQueue<>(); + + private final List outputTypes; + private final IndexBuildDriverFactoryProvider indexBuildDriverFactoryProvider; + private final int expectedPositions; + private final DataSize maxIndexMemorySize; + private final IndexJoinLookupStats stats; + + private final AtomicReference taskContextReference = new AtomicReference<>(); + private final Set lookupSourceInputChannels; + private final List keyOutputChannels; + private final Optional keyOutputHashChannel; + private final List keyTypes; + + @GuardedBy("this") + private IndexSnapshotLoader indexSnapshotLoader; // Lazily initialized + + @GuardedBy("this") + private PipelineContext pipelineContext; // Lazily initialized + + @GuardedBy("this") + private final AtomicReference indexSnapshotReference; + + public IndexLoader( + Set lookupSourceInputChannels, + List keyOutputChannels, + Optional keyOutputHashChannel, + List outputTypes, + IndexBuildDriverFactoryProvider indexBuildDriverFactoryProvider, + int expectedPositions, + DataSize maxIndexMemorySize, + IndexJoinLookupStats stats) + { + checkNotNull(lookupSourceInputChannels, "lookupSourceInputChannels is null"); + checkArgument(!lookupSourceInputChannels.isEmpty(), "lookupSourceInputChannels must not be empty"); + checkNotNull(keyOutputChannels, "keyOutputChannels is null"); + checkArgument(!keyOutputChannels.isEmpty(), "keyOutputChannels must not be empty"); + checkNotNull(keyOutputHashChannel, "keyOutputHashChannel is null"); + checkArgument(lookupSourceInputChannels.size() <= keyOutputChannels.size(), "Lookup channels must supply a subset of the actual index columns"); + checkNotNull(outputTypes, "outputTypes is null"); + checkNotNull(indexBuildDriverFactoryProvider, "indexBuildDriverFactoryProvider is null"); + checkNotNull(maxIndexMemorySize, "maxIndexMemorySize is null"); + checkNotNull(stats, "stats is null"); + + this.lookupSourceInputChannels = ImmutableSet.copyOf(lookupSourceInputChannels); + this.keyOutputChannels = ImmutableList.copyOf(keyOutputChannels); + this.keyOutputHashChannel = keyOutputHashChannel; + this.outputTypes = ImmutableList.copyOf(outputTypes); + this.indexBuildDriverFactoryProvider = indexBuildDriverFactoryProvider; + this.expectedPositions = expectedPositions; + this.maxIndexMemorySize = maxIndexMemorySize; + this.stats = stats; + + ImmutableList.Builder keyTypeBuilder = ImmutableList.builder(); + for (int keyOutputChannel : keyOutputChannels) { + keyTypeBuilder.add(outputTypes.get(keyOutputChannel)); + } + this.keyTypes = keyTypeBuilder.build(); + + // start with an empty source + this.indexSnapshotReference = new AtomicReference<>(new IndexSnapshot(new EmptyLookupSource(outputTypes.size()), new EmptyLookupSource(keyOutputChannels.size()))); + } + + // This is a ghetto way to acquire a TaskContext at runtime (unavailable at planning) + public void setContext(TaskContext taskContext) + { + taskContextReference.compareAndSet(null, taskContext); + } + + public int getChannelCount() + { + return outputTypes.size(); + } + + public List getOutputTypes() + { + return outputTypes; + } + + public IndexSnapshot getIndexSnapshot() + { + return indexSnapshotReference.get(); + } + + private static Block[] sliceBlocks(Block[] indexBlocks, int startPosition, int length) + { + Block[] slicedIndexBlocks = new Block[indexBlocks.length]; + for (int i = 0; i < indexBlocks.length; i++) { + slicedIndexBlocks[i] = indexBlocks[i].getRegion(startPosition, length); + } + return slicedIndexBlocks; + } + + public IndexedData getIndexedDataForKeys(int position, Block[] indexBlocks) + { + // Normalize the indexBlocks so that they only encompass the unloaded positions + int totalPositions = indexBlocks[0].getPositionCount(); + int remainingPositions = totalPositions - position; + return getIndexedDataForKeys(sliceBlocks(indexBlocks, position, remainingPositions)); + } + + private IndexedData getIndexedDataForKeys(Block[] indexBlocks) + { + UpdateRequest myUpdateRequest = new UpdateRequest(indexBlocks); + updateRequests.add(myUpdateRequest); + + synchronized (this) { + if (!myUpdateRequest.isFinished()) { + stats.recordIndexJoinLookup(); + initializeStateIfNecessary(); + + List requests = new ArrayList<>(); + updateRequests.drainTo(requests); + + long initialCacheSizeInBytes = indexSnapshotLoader.getCacheSizeInBytes(); + + // TODO: add heuristic to jump to load strategy that is most likely to succeed + + // Try to load all the requests + if (indexSnapshotLoader.load(requests)) { + return myUpdateRequest.getFinishedIndexSnapshot(); + } + + // Retry again if there was initial data (load failures will clear the cache automatically) + if (initialCacheSizeInBytes > 0 && indexSnapshotLoader.load(requests)) { + stats.recordSuccessfulIndexJoinLookupByCacheReset(); + return myUpdateRequest.getFinishedIndexSnapshot(); + } + + // Try loading just my request + if (requests.size() > 1) { + // Add all other requests back into the queue + Iterables.addAll(updateRequests, filter(requests, not(equalTo(myUpdateRequest)))); + + if (indexSnapshotLoader.load(ImmutableList.of(myUpdateRequest))) { + stats.recordSuccessfulIndexJoinLookupBySingleRequest(); + return myUpdateRequest.getFinishedIndexSnapshot(); + } + } + + // Repeatedly decrease the number of rows to load by a factor of 10 + int totalPositions = indexBlocks[0].getPositionCount(); + int attemptedPositions = totalPositions / 10; + while (attemptedPositions > 1) { + myUpdateRequest = new UpdateRequest(sliceBlocks(indexBlocks, 0, attemptedPositions)); + if (indexSnapshotLoader.load(ImmutableList.of(myUpdateRequest))) { + stats.recordSuccessfulIndexJoinLookupByLimitedRequest(); + return myUpdateRequest.getFinishedIndexSnapshot(); + } + attemptedPositions /= 10; + } + + // Just load the single index key in a streaming fashion (no caching) + stats.recordStreamedIndexJoinLookup(); + return streamIndexDataForSingleKey(myUpdateRequest); + } + } + + // return the snapshot from the update request as another thread may have already flushed the request + return myUpdateRequest.getFinishedIndexSnapshot(); + } + + public IndexedData streamIndexDataForSingleKey(UpdateRequest updateRequest) + { + Page indexKeyTuple = new Page(sliceBlocks(updateRequest.getBlocks(), 0, 1)); + + PageBuffer pageBuffer = new PageBuffer(100); + DriverFactory driverFactory = indexBuildDriverFactoryProvider.createStreaming(pageBuffer, indexKeyTuple); + Driver driver = driverFactory.createDriver(pipelineContext.addDriverContext()); + + PageRecordSet pageRecordSet = new PageRecordSet(keyTypes, indexKeyTuple); + PlanNodeId planNodeId = Iterables.getOnlyElement(driverFactory.getSourceIds()); + driver.updateSource(new TaskSource(planNodeId, ImmutableSet.of(new ScheduledSplit(0, new Split("index", new IndexSplit(pageRecordSet)))), true)); + + return new StreamingIndexedData(outputTypes, keyTypes, indexKeyTuple, pageBuffer, driver); + } + + private synchronized void initializeStateIfNecessary() + { + if (pipelineContext == null) { + TaskContext taskContext = taskContextReference.get(); + checkState(taskContext != null, "Task context must be set before index can be built"); + pipelineContext = taskContext.addPipelineContext(false, false); + } + if (indexSnapshotLoader == null) { + indexSnapshotLoader = new IndexSnapshotLoader( + indexBuildDriverFactoryProvider, + pipelineContext, + indexSnapshotReference, + lookupSourceInputChannels, + keyTypes, + keyOutputChannels, + keyOutputHashChannel, + expectedPositions, + maxIndexMemorySize); + } + } + + @NotThreadSafe + private static class IndexSnapshotLoader + { + private final DriverFactory driverFactory; + private final PipelineContext pipelineContext; + private final Set lookupSourceInputChannels; + private final Set allInputChannels; + private final List outputTypes; + private final List indexTypes; + private final AtomicReference indexSnapshotReference; + + private final IndexSnapshotBuilder indexSnapshotBuilder; + + private IndexSnapshotLoader(IndexBuildDriverFactoryProvider indexBuildDriverFactoryProvider, + PipelineContext pipelineContext, + AtomicReference indexSnapshotReference, + Set lookupSourceInputChannels, + List indexTypes, + List keyOutputChannels, + Optional keyOutputHashChannel, + int expectedPositions, + DataSize maxIndexMemorySize) + { + this.pipelineContext = pipelineContext; + this.indexSnapshotReference = indexSnapshotReference; + this.lookupSourceInputChannels = lookupSourceInputChannels; + this.outputTypes = indexBuildDriverFactoryProvider.getOutputTypes(); + this.indexTypes = indexTypes; + + this.indexSnapshotBuilder = new IndexSnapshotBuilder( + outputTypes, + keyOutputChannels, + keyOutputHashChannel, + pipelineContext.addDriverContext(), + maxIndexMemorySize, + expectedPositions); + this.driverFactory = indexBuildDriverFactoryProvider.createSnapshot(this.indexSnapshotBuilder); + + ImmutableSet.Builder builder = ImmutableSet.builder(); + for (int i = 0; i < indexTypes.size(); i++) { + builder.add(i); + } + this.allInputChannels = builder.build(); + } + + public long getCacheSizeInBytes() + { + return indexSnapshotBuilder.getMemoryInBytes(); + } + + public boolean load(List requests) + { + // Generate a RecordSet that only presents index keys that have not been cached and are deduped based on lookupSourceInputChannels + UnloadedIndexKeyRecordSet recordSetForLookupSource = new UnloadedIndexKeyRecordSet(indexSnapshotReference.get(), lookupSourceInputChannels, indexTypes, requests); + + // Drive index lookup to produce the output (landing in indexSnapshotBuilder) + try (Driver driver = driverFactory.createDriver(pipelineContext.addDriverContext())) { + PlanNodeId sourcePlanNodeId = Iterables.getOnlyElement(driverFactory.getSourceIds()); + driver.updateSource(new TaskSource(sourcePlanNodeId, ImmutableSet.of(new ScheduledSplit(0, new Split("index", new IndexSplit(recordSetForLookupSource)))), true)); + while (!driver.isFinished()) { + ListenableFuture process = driver.process(); + checkState(process.isDone(), "Driver should never block"); + } + } + + if (indexSnapshotBuilder.isMemoryExceeded()) { + clearCachedData(); + return false; + } + + // Generate a RecordSet that presents unique index keys that have not been cached + UnloadedIndexKeyRecordSet indexKeysRecordSet = (lookupSourceInputChannels.equals(allInputChannels)) + ? recordSetForLookupSource + : new UnloadedIndexKeyRecordSet(indexSnapshotReference.get(), allInputChannels, indexTypes, requests); + + // Create lookup source with new data + IndexSnapshot newValue = indexSnapshotBuilder.createIndexSnapshot(indexKeysRecordSet); + if (newValue == null) { + clearCachedData(); + return false; + } + + indexSnapshotReference.set(newValue); + for (UpdateRequest request : requests) { + request.finished(newValue); + } + return true; + } + + private void clearCachedData() + { + indexSnapshotReference.set(new IndexSnapshot(new EmptyLookupSource(outputTypes.size()), new EmptyLookupSource(indexTypes.size()))); + indexSnapshotBuilder.reset(); + } + + } + + private static class EmptyLookupSource + implements LookupSource + { + private final int channelCount; + + public EmptyLookupSource(int channelCount) + { + this.channelCount = channelCount; + } + + @Override + public int getChannelCount() + { + return channelCount; + } + + @Override + public long getInMemorySizeInBytes() + { + return 0; + } + + @Override + public long getJoinPosition(int position, Page page, int rawHash) + { + return IndexSnapshot.UNLOADED_INDEX_KEY; + } + + @Override + public long getJoinPosition(int position, Page page) + { + return IndexSnapshot.UNLOADED_INDEX_KEY; + } + + @Override + public long getNextJoinPosition(long currentPosition) + { + return IndexSnapshot.UNLOADED_INDEX_KEY; + } + + @Override + public LongIterator getUnvisitedJoinPositions() + { + throw new UnsupportedOperationException(); + } + + @Override + public void appendTo(long position, PageBuilder pageBuilder, int outputChannelOffset) + { + throw new UnsupportedOperationException(); + } + + @Override + public void close() + { + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/index/IndexLookupSource.java b/presto-main/src/main/java/com/facebook/presto/operator/index/IndexLookupSource.java new file mode 100644 index 00000000..b8bc14f8 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/index/IndexLookupSource.java @@ -0,0 +1,101 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.index; + +import com.facebook.presto.operator.LookupSource; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.block.Block; +import it.unimi.dsi.fastutil.longs.LongIterator; + +import javax.annotation.concurrent.NotThreadSafe; + +import static com.facebook.presto.operator.index.IndexSnapshot.UNLOADED_INDEX_KEY; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +@NotThreadSafe +public class IndexLookupSource + implements LookupSource +{ + private final IndexLoader indexLoader; + private IndexedData indexedData; + + public IndexLookupSource(IndexLoader indexLoader) + { + this.indexLoader = checkNotNull(indexLoader, "indexLoader is null"); + this.indexedData = indexLoader.getIndexSnapshot(); + } + + @Override + public int getChannelCount() + { + return indexLoader.getChannelCount(); + } + + @Override + public long getInMemorySizeInBytes() + { + return 0; + } + + @Override + public long getJoinPosition(int position, Page page, int rawHash) + { + // TODO update to take advantage of precomputed hash + return getJoinPosition(position, page); + } + + @Override + public long getJoinPosition(int position, Page page) + { + Block[] blocks = page.getBlocks(); + long joinPosition = indexedData.getJoinPosition(position, page); + if (joinPosition == UNLOADED_INDEX_KEY) { + indexedData.close(); // Close out the old indexedData + indexedData = indexLoader.getIndexedDataForKeys(position, blocks); + joinPosition = indexedData.getJoinPosition(position, page); + checkState(joinPosition != UNLOADED_INDEX_KEY); + } + // INVARIANT: position is -1 or a valid position greater than or equal to zero + return joinPosition; + } + + @Override + public long getNextJoinPosition(long currentPosition) + { + long nextPosition = indexedData.getNextJoinPosition(currentPosition); + checkState(nextPosition != UNLOADED_INDEX_KEY); + // INVARIANT: currentPosition is -1 or a valid currentPosition greater than or equal to zero + return nextPosition; + } + + @Override + public LongIterator getUnvisitedJoinPositions() + { + throw new UnsupportedOperationException(); + } + + @Override + public void appendTo(long position, PageBuilder pageBuilder, int outputChannelOffset) + { + indexedData.appendTo(position, pageBuilder, outputChannelOffset); + } + + @Override + public void close() + { + indexedData.close(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/index/IndexLookupSourceSupplier.java b/presto-main/src/main/java/com/facebook/presto/operator/index/IndexLookupSourceSupplier.java new file mode 100644 index 00000000..9266a188 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/index/IndexLookupSourceSupplier.java @@ -0,0 +1,69 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.index; + +import com.facebook.presto.operator.LookupSource; +import com.facebook.presto.operator.LookupSourceSupplier; +import com.facebook.presto.operator.OperatorContext; +import com.facebook.presto.spi.type.Type; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import io.airlift.units.DataSize; + +import java.util.List; +import java.util.Optional; +import java.util.Set; + +public class IndexLookupSourceSupplier + implements LookupSourceSupplier +{ + private final IndexLoader indexLoader; + + public IndexLookupSourceSupplier( + Set lookupSourceInputChannels, + List keyOutputChannels, + Optional keyOutputHashChannel, + List outputTypes, + IndexBuildDriverFactoryProvider indexBuildDriverFactoryProvider, + DataSize maxIndexMemorySize, + IndexJoinLookupStats stats) + { + this.indexLoader = new IndexLoader(lookupSourceInputChannels, keyOutputChannels, keyOutputHashChannel, outputTypes, indexBuildDriverFactoryProvider, 10_000, maxIndexMemorySize, stats); + } + + @Override + public List getTypes() + { + return indexLoader.getOutputTypes(); + } + + @Override + public ListenableFuture getLookupSource(OperatorContext operatorContext) + { + indexLoader.setContext(operatorContext.getDriverContext().getPipelineContext().getTaskContext()); + return Futures.immediateFuture(new IndexLookupSource(indexLoader)); + } + + @Override + public void release() + { + // no-op + } + + @Override + public void retain() + { + // no-op + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/index/IndexSnapshot.java b/presto-main/src/main/java/com/facebook/presto/operator/index/IndexSnapshot.java new file mode 100644 index 00000000..b103deed --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/index/IndexSnapshot.java @@ -0,0 +1,68 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.index; + +import com.facebook.presto.operator.LookupSource; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PageBuilder; + +import javax.annotation.concurrent.Immutable; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Immutable +public class IndexSnapshot + implements IndexedData +{ + private final LookupSource values; + private final LookupSource missingKeys; + + public IndexSnapshot(LookupSource values, LookupSource missingKeys) + { + this.values = checkNotNull(values, "values is null"); + this.missingKeys = checkNotNull(missingKeys, "missingKeys is null"); + } + + @Override + public long getJoinPosition(int position, Page page) + { + long joinPosition = values.getJoinPosition(position, page); + if (joinPosition < 0) { + if (missingKeys.getJoinPosition(position, page) < 0) { + return UNLOADED_INDEX_KEY; + } + else { + return NO_MORE_POSITIONS; + } + } + return joinPosition; + } + + @Override + public long getNextJoinPosition(long currentPosition) + { + return values.getNextJoinPosition(currentPosition); + } + + @Override + public void appendTo(long position, PageBuilder pageBuilder, int outputChannelOffset) + { + values.appendTo(position, pageBuilder, outputChannelOffset); + } + + @Override + public void close() + { + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/index/IndexSnapshotBuilder.java b/presto-main/src/main/java/com/facebook/presto/operator/index/IndexSnapshotBuilder.java new file mode 100644 index 00000000..7477abbf --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/index/IndexSnapshotBuilder.java @@ -0,0 +1,162 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.index; + +import com.facebook.presto.operator.DriverContext; +import com.facebook.presto.operator.LookupSource; +import com.facebook.presto.operator.PagesIndex; +import com.facebook.presto.operator.index.UnloadedIndexKeyRecordSet.UnloadedIndexKeyRecordCursor; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; +import io.airlift.units.DataSize; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +public class IndexSnapshotBuilder +{ + private final int expectedPositions; + private final List outputTypes; + private final List missingKeysTypes; + private final List keyOutputChannels; + private final Optional keyOutputHashChannel; + private final List missingKeysChannels; + + private final long maxMemoryInBytes; + private PagesIndex outputPagesIndex; + private PagesIndex missingKeysIndex; + private LookupSource missingKeys; + + private final List pages = new ArrayList<>(); + private long memoryInBytes; + + public IndexSnapshotBuilder(List outputTypes, + List keyOutputChannels, + Optional keyOutputHashChannel, + DriverContext driverContext, + DataSize maxMemoryInBytes, + int expectedPositions) + { + checkNotNull(outputTypes, "outputTypes is null"); + checkNotNull(keyOutputChannels, "keyOutputChannels is null"); + checkNotNull(keyOutputHashChannel, "keyOutputHashChannel is null"); + checkNotNull(driverContext, "driverContext is null"); + checkNotNull(maxMemoryInBytes, "maxMemoryInBytes is null"); + checkArgument(expectedPositions > 0, "expectedPositions must be greater than zero"); + + this.outputTypes = ImmutableList.copyOf(outputTypes); + this.expectedPositions = expectedPositions; + this.keyOutputChannels = ImmutableList.copyOf(keyOutputChannels); + this.keyOutputHashChannel = keyOutputHashChannel; + this.maxMemoryInBytes = maxMemoryInBytes.toBytes(); + + ImmutableList.Builder missingKeysTypes = ImmutableList.builder(); + ImmutableList.Builder missingKeysChannels = ImmutableList.builder(); + for (int i = 0; i < keyOutputChannels.size(); i++) { + Integer keyOutputChannel = keyOutputChannels.get(i); + missingKeysTypes.add(outputTypes.get(keyOutputChannel)); + missingKeysChannels.add(i); + } + this.missingKeysTypes = missingKeysTypes.build(); + this.missingKeysChannels = missingKeysChannels.build(); + + this.outputPagesIndex = new PagesIndex(outputTypes, expectedPositions); + this.missingKeysIndex = new PagesIndex(missingKeysTypes.build(), expectedPositions); + this.missingKeys = missingKeysIndex.createLookupSource(this.missingKeysChannels); + } + + public List getOutputTypes() + { + return outputTypes; + } + + public long getMemoryInBytes() + { + return memoryInBytes; + } + + public boolean isMemoryExceeded() + { + return memoryInBytes > maxMemoryInBytes; + } + + public boolean tryAddPage(Page page) + { + memoryInBytes += page.getSizeInBytes(); + if (isMemoryExceeded()) { + return false; + } + pages.add(page); + return true; + } + + public IndexSnapshot createIndexSnapshot(UnloadedIndexKeyRecordSet indexKeysRecordSet) + { + checkArgument(indexKeysRecordSet.getColumnTypes().equals(missingKeysTypes), "indexKeysRecordSet must have same schema as missingKeys"); + checkState(!isMemoryExceeded(), "Max memory exceeded"); + for (Page page : pages) { + outputPagesIndex.addPage(page); + } + pages.clear(); + + LookupSource lookupSource = outputPagesIndex.createLookupSource(keyOutputChannels, keyOutputHashChannel); + + // Build a page containing the keys that produced no output rows, so in future requests can skip these keys + PageBuilder missingKeysPageBuilder = new PageBuilder(missingKeysIndex.getTypes()); + UnloadedIndexKeyRecordCursor indexKeysRecordCursor = indexKeysRecordSet.cursor(); + while (indexKeysRecordCursor.advanceNextPosition()) { + Block[] blocks = indexKeysRecordCursor.getBlocks(); + Page page = indexKeysRecordCursor.getPage(); + int position = indexKeysRecordCursor.getPosition(); + if (lookupSource.getJoinPosition(position, page) < 0) { + missingKeysPageBuilder.declarePosition(); + for (int i = 0; i < blocks.length; i++) { + Block block = blocks[i]; + Type type = indexKeysRecordCursor.getType(i); + type.appendTo(block, position, missingKeysPageBuilder.getBlockBuilder(i)); + } + } + } + Page missingKeysPage = missingKeysPageBuilder.build(); + + memoryInBytes += missingKeysPage.getSizeInBytes(); + if (isMemoryExceeded()) { + return null; + } + + // only update missing keys if we have new missing keys + if (!missingKeysPageBuilder.isEmpty()) { + missingKeysIndex.addPage(missingKeysPage); + missingKeys = missingKeysIndex.createLookupSource(missingKeysChannels); + } + + return new IndexSnapshot(lookupSource, missingKeys); + } + + public void reset() + { + memoryInBytes = 0; + pages.clear(); + outputPagesIndex = new PagesIndex(outputTypes, expectedPositions); + missingKeysIndex = new PagesIndex(missingKeysTypes, expectedPositions); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/index/IndexSourceOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/index/IndexSourceOperator.java new file mode 100644 index 00000000..a6bee74f --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/index/IndexSourceOperator.java @@ -0,0 +1,210 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.index; + +import com.facebook.presto.metadata.Split; +import com.facebook.presto.operator.DriverContext; +import com.facebook.presto.operator.FinishedOperator; +import com.facebook.presto.operator.Operator; +import com.facebook.presto.operator.OperatorContext; +import com.facebook.presto.operator.PageSourceOperator; +import com.facebook.presto.operator.SourceOperator; +import com.facebook.presto.operator.SourceOperatorFactory; +import com.facebook.presto.spi.ConnectorIndex; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.RecordPageSource; +import com.facebook.presto.spi.RecordSet; +import com.facebook.presto.spi.UpdatablePageSource; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.planner.plan.PlanNodeId; +import com.google.common.collect.ImmutableList; + +import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Supplier; + +import static com.facebook.presto.util.Types.checkType; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +public class IndexSourceOperator + implements SourceOperator +{ + public static class IndexSourceOperatorFactory + implements SourceOperatorFactory + { + private final int operatorId; + private final PlanNodeId sourceId; + private final ConnectorIndex index; + private final List types; + private final Function probeKeyNormalizer; + private boolean closed; + + public IndexSourceOperatorFactory( + int operatorId, + PlanNodeId sourceId, + ConnectorIndex index, + List types, + Function probeKeyNormalizer) + { + this.operatorId = operatorId; + this.sourceId = checkNotNull(sourceId, "sourceId is null"); + this.index = checkNotNull(index, "index is null"); + this.types = checkNotNull(types, "types is null"); + this.probeKeyNormalizer = checkNotNull(probeKeyNormalizer, "probeKeyNormalizer is null"); + } + + @Override + public PlanNodeId getSourceId() + { + return sourceId; + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public SourceOperator createOperator(DriverContext driverContext) + { + checkState(!closed, "Factory is already closed"); + OperatorContext operatorContext = driverContext.addOperatorContext(operatorId, IndexSourceOperator.class.getSimpleName()); + return new IndexSourceOperator( + operatorContext, + sourceId, + index, + types, + probeKeyNormalizer); + } + + @Override + public void close() + { + closed = true; + } + } + + private final OperatorContext operatorContext; + private final PlanNodeId planNodeId; + private final ConnectorIndex index; + private final List types; + private final Function probeKeyNormalizer; + + private Operator source; + + public IndexSourceOperator( + OperatorContext operatorContext, + PlanNodeId planNodeId, + ConnectorIndex index, + List types, + Function probeKeyNormalizer) + { + this.operatorContext = checkNotNull(operatorContext, "operatorContext is null"); + this.planNodeId = checkNotNull(planNodeId, "planNodeId is null"); + this.index = checkNotNull(index, "index is null"); + this.types = ImmutableList.copyOf(checkNotNull(types, "types is null")); + this.probeKeyNormalizer = checkNotNull(probeKeyNormalizer, "probeKeyNormalizer is null"); + } + + @Override + public OperatorContext getOperatorContext() + { + return operatorContext; + } + + @Override + public PlanNodeId getSourceId() + { + return planNodeId; + } + + @Override + public Supplier> addSplit(Split split) + { + checkNotNull(split, "split is null"); + checkType(split.getConnectorSplit(), IndexSplit.class, "connectorSplit"); + checkState(source == null, "Index source split already set"); + + IndexSplit indexSplit = (IndexSplit) split.getConnectorSplit(); + + // Normalize the incoming RecordSet to something that can be consumed by the index + RecordSet normalizedRecordSet = probeKeyNormalizer.apply(indexSplit.getKeyRecordSet()); + RecordSet result = index.lookup(normalizedRecordSet); + source = new PageSourceOperator(new RecordPageSource(result), result.getColumnTypes(), operatorContext); + + operatorContext.setInfoSupplier(split::getInfo); + + return Optional::empty; + } + + @Override + public void noMoreSplits() + { + if (source == null) { + source = new FinishedOperator(operatorContext, types); + } + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public void finish() + { + noMoreSplits(); + source.finish(); + } + + @Override + public boolean isFinished() + { + return (source != null) && source.isFinished(); + } + + @Override + public boolean needsInput() + { + return false; + } + + @Override + public void addInput(Page page) + { + throw new UnsupportedOperationException(getClass().getName() + " can not take input"); + } + + @Override + public Page getOutput() + { + if (source == null) { + return null; + } + return source.getOutput(); + } + + @Override + public void close() + throws Exception + { + if (source != null) { + source.close(); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/index/IndexSplit.java b/presto-main/src/main/java/com/facebook/presto/operator/index/IndexSplit.java new file mode 100644 index 00000000..cd96dc13 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/index/IndexSplit.java @@ -0,0 +1,56 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.index; + +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.HostAddress; +import com.facebook.presto.spi.RecordSet; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class IndexSplit + implements ConnectorSplit +{ + private final RecordSet keyRecordSet; + + public IndexSplit(RecordSet keyRecordSet) + { + this.keyRecordSet = checkNotNull(keyRecordSet, "keyRecordSet is null"); + } + + @Override + public boolean isRemotelyAccessible() + { + throw new UnsupportedOperationException(); + } + + @Override + public List getAddresses() + { + throw new UnsupportedOperationException(); + } + + @Override + public Object getInfo() + { + return null; + } + + public RecordSet getKeyRecordSet() + { + return keyRecordSet; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/index/IndexedData.java b/presto-main/src/main/java/com/facebook/presto/operator/index/IndexedData.java new file mode 100644 index 00000000..5aafc068 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/index/IndexedData.java @@ -0,0 +1,43 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.index; + +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PageBuilder; + +import java.io.Closeable; + +public interface IndexedData + extends Closeable +{ + public static final long UNLOADED_INDEX_KEY = -2; + public static final long NO_MORE_POSITIONS = -1; + + /** + * Returns UNLOADED_INDEX_KEY if the key has not been loaded. + * Returns NO_MORE_POSITIONS if the key has been loaded, but has no values. + * Returns a valid address if the key has been loaded and has values. + */ + long getJoinPosition(int position, Page page); + + /** + * Returns the next address to join. + * Returns NO_MORE_POSITIONS if there are no more values to join. + */ + long getNextJoinPosition(long currentPosition); + + void appendTo(long position, PageBuilder pageBuilder, int outputChannelOffset); + + void close(); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/index/PageBuffer.java b/presto-main/src/main/java/com/facebook/presto/operator/index/PageBuffer.java new file mode 100644 index 00000000..cb8c9da1 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/index/PageBuffer.java @@ -0,0 +1,78 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.index; + +import com.facebook.presto.spi.Page; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; + +import javax.annotation.concurrent.ThreadSafe; + +import java.util.ArrayDeque; +import java.util.Queue; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; + +@ThreadSafe +public class PageBuffer +{ + private static final ListenableFuture NOT_FULL = Futures.immediateFuture(null); + + private final int maxBufferedPages; + private final Queue pages; + private SettableFuture settableFuture; + + public PageBuffer(int maxBufferedPages) + { + checkArgument(maxBufferedPages > 0, "maxBufferedPages must be at least one"); + this.maxBufferedPages = maxBufferedPages; + this.pages = new ArrayDeque<>(maxBufferedPages); + } + + public synchronized boolean isFull() + { + return pages.size() >= maxBufferedPages; + } + + /** + * Adds a page to the buffer. + * Returns a ListenableFuture that is marked as done when the next page can be added. + */ + public synchronized ListenableFuture add(Page page) + { + checkState(!isFull(), "PageBuffer is full!"); + pages.offer(page); + if (isFull()) { + if (settableFuture == null) { + settableFuture = SettableFuture.create(); + } + return settableFuture; + } + return NOT_FULL; + } + + /** + * Return a page from the buffer, or null if none exists + */ + public synchronized Page poll() + { + if (settableFuture != null) { + settableFuture.set(null); + settableFuture = null; + } + return pages.poll(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/index/PageBufferOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/index/PageBufferOperator.java new file mode 100644 index 00000000..7dba656e --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/index/PageBufferOperator.java @@ -0,0 +1,138 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.index; + +import com.facebook.presto.operator.DriverContext; +import com.facebook.presto.operator.Operator; +import com.facebook.presto.operator.OperatorContext; +import com.facebook.presto.operator.OperatorFactory; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.ListenableFuture; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +public class PageBufferOperator + implements Operator +{ + public static class PageBufferOperatorFactory + implements OperatorFactory + { + private final int operatorId; + private final PageBuffer pageBuffer; + + public PageBufferOperatorFactory(int operatorId, PageBuffer pageBuffer) + { + this.operatorId = operatorId; + this.pageBuffer = checkNotNull(pageBuffer, "pageBuffer is null"); + } + + @Override + public List getTypes() + { + return ImmutableList.of(); + } + + @Override + public Operator createOperator(DriverContext driverContext) + { + OperatorContext operatorContext = driverContext.addOperatorContext(operatorId, PageBufferOperator.class.getSimpleName()); + return new PageBufferOperator(operatorContext, pageBuffer); + } + + @Override + public void close() + { + } + } + + private final OperatorContext operatorContext; + private final PageBuffer pageBuffer; + private ListenableFuture blocked = NOT_BLOCKED; + private boolean finished; + + public PageBufferOperator(OperatorContext operatorContext, PageBuffer pageBuffer) + { + this.operatorContext = checkNotNull(operatorContext, "operatorContext is null"); + this.pageBuffer = checkNotNull(pageBuffer, "pageBuffer is null"); + } + + @Override + public OperatorContext getOperatorContext() + { + return operatorContext; + } + + @Override + public List getTypes() + { + return ImmutableList.of(); + } + + @Override + public void finish() + { + finished = true; + } + + @Override + public boolean isFinished() + { + updateBlockedIfNecessary(); + return finished && blocked == NOT_BLOCKED; + } + + @Override + public ListenableFuture isBlocked() + { + updateBlockedIfNecessary(); + return blocked; + } + + @Override + public boolean needsInput() + { + updateBlockedIfNecessary(); + return !finished && blocked == NOT_BLOCKED; + } + + private void updateBlockedIfNecessary() + { + if (blocked != NOT_BLOCKED && blocked.isDone()) { + blocked = NOT_BLOCKED; + } + } + + @Override + public void addInput(Page page) + { + checkNotNull(page, "page is null"); + checkState(blocked == NOT_BLOCKED, "output is already blocked"); + ListenableFuture future = pageBuffer.add(page); + if (!future.isDone()) { + this.blocked = future; + } + operatorContext.recordGeneratedOutput(page.getSizeInBytes(), page.getPositionCount()); + } + + @Override + public Page getOutput() + { + return null; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/index/PageRecordSet.java b/presto-main/src/main/java/com/facebook/presto/operator/index/PageRecordSet.java new file mode 100644 index 00000000..b9759806 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/index/PageRecordSet.java @@ -0,0 +1,151 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.index; + +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.RecordCursor; +import com.facebook.presto.spi.RecordSet; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +public class PageRecordSet + implements RecordSet +{ + private final List types; + private final Page page; + + public PageRecordSet(List types, Page page) + { + this.types = ImmutableList.copyOf(checkNotNull(types, "types is null")); + this.page = checkNotNull(page, "page is null"); + checkArgument(types.size() == page.getChannelCount(), "Types do not match page channels"); + } + + @Override + public List getColumnTypes() + { + return types; + } + + @Override + public RecordCursor cursor() + { + return new PageRecordCursor(types, page); + } + + public static class PageRecordCursor + implements RecordCursor + { + private final List types; + private final Page page; + private int position = -1; + + private PageRecordCursor(List types, Page page) + { + this.types = ImmutableList.copyOf(checkNotNull(types, "types is null")); + this.page = checkNotNull(page, "page is null"); + checkArgument(types.size() == page.getChannelCount(), "Types do not match page channels"); + } + + @Override + public long getTotalBytes() + { + return page.getSizeInBytes(); + } + + @Override + public long getCompletedBytes() + { + return 0; + } + + @Override + public long getReadTimeNanos() + { + return 0; + } + + @Override + public Type getType(int field) + { + return types.get(field); + } + + @Override + public boolean advanceNextPosition() + { + position++; + if (position >= page.getPositionCount()) { + return false; + } + return true; + } + + @Override + public boolean getBoolean(int field) + { + checkState(position >= 0, "Not yet advanced"); + checkState(position < page.getPositionCount(), "Already finished"); + Type type = types.get(field); + return type.getBoolean(page.getBlock(field), position); + } + + @Override + public long getLong(int field) + { + checkState(position >= 0, "Not yet advanced"); + checkState(position < page.getPositionCount(), "Already finished"); + Type type = types.get(field); + return type.getLong(page.getBlock(field), position); + } + + @Override + public double getDouble(int field) + { + checkState(position >= 0, "Not yet advanced"); + checkState(position < page.getPositionCount(), "Already finished"); + Type type = types.get(field); + return type.getDouble(page.getBlock(field), position); + } + + @Override + public Slice getSlice(int field) + { + checkState(position >= 0, "Not yet advanced"); + checkState(position < page.getPositionCount(), "Already finished"); + Type type = types.get(field); + return type.getSlice(page.getBlock(field), position); + } + + @Override + public boolean isNull(int field) + { + checkState(position >= 0, "Not yet advanced"); + checkState(position < page.getPositionCount(), "Already finished"); + return page.getBlock(field).isNull(position); + } + + @Override + public void close() + { + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/index/PagesIndexBuilderOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/index/PagesIndexBuilderOperator.java new file mode 100644 index 00000000..a80f3fea --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/index/PagesIndexBuilderOperator.java @@ -0,0 +1,129 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.index; + +import com.facebook.presto.operator.DriverContext; +import com.facebook.presto.operator.Operator; +import com.facebook.presto.operator.OperatorContext; +import com.facebook.presto.operator.OperatorFactory; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; + +import javax.annotation.concurrent.ThreadSafe; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +@ThreadSafe +public class PagesIndexBuilderOperator + implements Operator +{ + public static class PagesIndexBuilderOperatorFactory + implements OperatorFactory + { + private final int operatorId; + private final IndexSnapshotBuilder indexSnapshotBuilder; + private boolean closed; + + public PagesIndexBuilderOperatorFactory(int operatorId, IndexSnapshotBuilder indexSnapshotBuilder) + { + this.operatorId = operatorId; + this.indexSnapshotBuilder = checkNotNull(indexSnapshotBuilder, "indexSnapshotBuilder is null"); + } + + @Override + public List getTypes() + { + return ImmutableList.of(); + } + + @Override + public Operator createOperator(DriverContext driverContext) + { + checkState(!closed, "Factory is already closed"); + + OperatorContext operatorContext = driverContext.addOperatorContext(operatorId, PagesIndexBuilderOperator.class.getSimpleName()); + return new PagesIndexBuilderOperator(operatorContext, indexSnapshotBuilder); + } + + @Override + public void close() + { + closed = true; + } + } + + private final OperatorContext operatorContext; + private final IndexSnapshotBuilder indexSnapshotBuilder; + + private boolean finished; + + public PagesIndexBuilderOperator(OperatorContext operatorContext, IndexSnapshotBuilder indexSnapshotBuilder) + { + this.operatorContext = checkNotNull(operatorContext, "operatorContext is null"); + this.indexSnapshotBuilder = checkNotNull(indexSnapshotBuilder, "indexSnapshotBuilder is null"); + } + + @Override + public OperatorContext getOperatorContext() + { + return operatorContext; + } + + @Override + public List getTypes() + { + return ImmutableList.of(); + } + + @Override + public void finish() + { + finished = true; + } + + @Override + public boolean isFinished() + { + return finished; + } + + @Override + public boolean needsInput() + { + return !finished; + } + + @Override + public void addInput(Page page) + { + checkNotNull(page, "page is null"); + checkState(!isFinished(), "Operator is already finished"); + + if (!indexSnapshotBuilder.tryAddPage(page)) { + finish(); + return; + } + operatorContext.recordGeneratedOutput(page.getSizeInBytes(), page.getPositionCount()); + } + + @Override + public Page getOutput() + { + return null; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/index/StreamingIndexedData.java b/presto-main/src/main/java/com/facebook/presto/operator/index/StreamingIndexedData.java new file mode 100644 index 00000000..881cbe00 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/index/StreamingIndexedData.java @@ -0,0 +1,142 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.index; + +import com.facebook.presto.operator.Driver; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; +import com.google.common.primitives.Ints; + +import javax.annotation.concurrent.NotThreadSafe; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +@NotThreadSafe +public class StreamingIndexedData + implements IndexedData +{ + private final List outputTypes; + private final List indexKeyTypes; + private final Page indexKeyTuple; + private final PageBuffer pageBuffer; + private final Driver driver; + + private boolean started; + private Page currentPage; + + public StreamingIndexedData(List outputTypes, List indexKeyTypes, Page indexKeyTuple, PageBuffer pageBuffer, Driver driver) + { + this.outputTypes = ImmutableList.copyOf(checkNotNull(outputTypes, "outputTypes is null")); + this.indexKeyTypes = ImmutableList.copyOf(checkNotNull(indexKeyTypes, "indexKeyTypes is null")); + this.indexKeyTuple = checkNotNull(indexKeyTuple, "indexKeyTuple is null"); + checkArgument(indexKeyTuple.getPositionCount() == 1, "indexKeyTuple Page should only have one position"); + checkArgument(indexKeyTypes.size() == indexKeyTuple.getChannelCount(), "indexKeyTypes doesn't match indexKeyTuple columns"); + this.pageBuffer = checkNotNull(pageBuffer, "pageBuffer is null"); + this.driver = checkNotNull(driver, "driver is null"); + } + + @Override + public long getJoinPosition(int position, Page page) + { + checkArgument(page.getChannelCount() == indexKeyTypes.size(), "Number of blocks does not match the number of key columns"); + if (started || !matchesExpectedKey(position, page)) { + return IndexedData.UNLOADED_INDEX_KEY; + } + started = true; + if (!loadNextPage()) { + return IndexedData.NO_MORE_POSITIONS; + } + return 0; + } + + // TODO: use the code generator here + private boolean matchesExpectedKey(int position, Page page) + { + for (int i = 0; i < indexKeyTypes.size(); i++) { + if (!indexKeyTypes.get(i).equalTo(page.getBlock(i), position, indexKeyTuple.getBlock(i), 0)) { + return false; + } + } + return true; + } + + @Override + public long getNextJoinPosition(long currentPosition) + { + checkState(currentPage != null, "getJoinPosition not called first"); + long nextPosition = currentPosition + 1; + if (nextPosition >= currentPage.getPositionCount()) { + if (!loadNextPage()) { + return IndexedData.NO_MORE_POSITIONS; + } + nextPosition = 0; + } + return nextPosition; + } + + private boolean loadNextPage() + { + Page nextPage = extractNonEmptyPage(pageBuffer); + while (nextPage == null) { + if (driver.isFinished()) { + return false; + } + driver.process(); + nextPage = extractNonEmptyPage(pageBuffer); + } + currentPage = nextPage; + return true; + } + + /** + * Return the next page from pageBuffer that has a non-zero position count, or null if none available + */ + private static Page extractNonEmptyPage(PageBuffer pageBuffer) + { + Page page = pageBuffer.poll(); + while (page != null && page.getPositionCount() == 0) { + page = pageBuffer.poll(); + } + return page; + } + + @Override + public void appendTo(long position, PageBuilder pageBuilder, int outputChannelOffset) + { + // TODO: use the code generator here + checkState(currentPage != null, "getJoinPosition not called first"); + int intPosition = Ints.checkedCast(position); + for (int i = 0; i < outputTypes.size(); i++) { + Type type = outputTypes.get(i); + Block block = currentPage.getBlock(i); + BlockBuilder blockBuilder = pageBuilder.getBlockBuilder(i + outputChannelOffset); + type.appendTo(block, intPosition, blockBuilder); + } + } + + @Override + public void close() + { + driver.close(); + currentPage = null; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/index/TupleFilterProcessor.java b/presto-main/src/main/java/com/facebook/presto/operator/index/TupleFilterProcessor.java new file mode 100644 index 00000000..efd6d3b6 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/index/TupleFilterProcessor.java @@ -0,0 +1,86 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.index; + +import com.facebook.presto.operator.PageProcessor; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Filters out rows that do not match the values from the specified tuple + */ +public class TupleFilterProcessor + implements PageProcessor +{ + private final Page tuplePage; + private final List outputTypes; + private final int[] outputTupleChannels; + + public TupleFilterProcessor(Page tuplePage, List outputTypes, int[] outputTupleChannels) + { + checkNotNull(tuplePage, "tuplePage is null"); + checkArgument(tuplePage.getPositionCount() == 1, "tuplePage should only have one position"); + checkArgument(tuplePage.getChannelCount() > 0, "tuplePage must have at least one channel"); + checkNotNull(outputTypes, "outputTypes is null"); + checkNotNull(outputTupleChannels, "outputTupleChannels is null"); + checkArgument(tuplePage.getChannelCount() == outputTupleChannels.length, "tuplePage and outputTupleChannels have different number of channels"); + checkArgument(outputTypes.size() >= outputTupleChannels.length, "Must have at least as many output channels as those used for filtering"); + + this.tuplePage = tuplePage; + this.outputTypes = ImmutableList.copyOf(outputTypes); + this.outputTupleChannels = outputTupleChannels; + } + + @Override + public int process(ConnectorSession session, Page page, int start, int end, PageBuilder pageBuilder) + { + // TODO: generate bytecode for this in the future + for (int position = start; position < end; position++) { + if (matches(position, page)) { + pageBuilder.declarePosition(); + for (int i = 0; i < outputTypes.size(); i++) { + Type type = outputTypes.get(i); + Block block = page.getBlock(i); + BlockBuilder blockBuilder = pageBuilder.getBlockBuilder(i); + type.appendTo(block, position, blockBuilder); + } + } + } + + return end; + } + + private boolean matches(int position, Page page) + { + for (int i = 0; i < outputTupleChannels.length; i++) { + Type type = outputTypes.get(outputTupleChannels[i]); + Block outputBlock = page.getBlock(outputTupleChannels[i]); + Block singleTupleBlock = tuplePage.getBlock(i); + if (!type.equalTo(singleTupleBlock, 0, outputBlock, position)) { + return false; + } + } + return true; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/index/UnloadedIndexKeyRecordSet.java b/presto-main/src/main/java/com/facebook/presto/operator/index/UnloadedIndexKeyRecordSet.java new file mode 100644 index 00000000..caf69db9 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/index/UnloadedIndexKeyRecordSet.java @@ -0,0 +1,257 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.index; + +import com.facebook.presto.operator.GroupByHash; +import com.facebook.presto.operator.GroupByIdBlock; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.RecordCursor; +import com.facebook.presto.spi.RecordSet; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; +import com.google.common.primitives.Ints; +import io.airlift.slice.Slice; +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntList; +import it.unimi.dsi.fastutil.ints.IntListIterator; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static com.facebook.presto.operator.GroupByHash.createGroupByHash; +import static com.facebook.presto.operator.index.IndexSnapshot.UNLOADED_INDEX_KEY; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +public class UnloadedIndexKeyRecordSet + implements RecordSet +{ + private final List types; + private final List pageAndPositions; + + public UnloadedIndexKeyRecordSet(IndexSnapshot existingSnapshot, Set channelsForDistinct, List types, List requests) + { + checkNotNull(existingSnapshot, "existingSnapshot is null"); + this.types = ImmutableList.copyOf(checkNotNull(types, "types is null")); + checkNotNull(requests, "requests is null"); + + int[] distinctChannels = Ints.toArray(channelsForDistinct); + int[] normalizedDistinctChannels = new int[distinctChannels.length]; + List distinctChannelTypes = new ArrayList<>(distinctChannels.length); + for (int i = 0; i < distinctChannels.length; i++) { + normalizedDistinctChannels[i] = i; + distinctChannelTypes.add(types.get(distinctChannels[i])); + } + + ImmutableList.Builder builder = ImmutableList.builder(); + GroupByHash groupByHash = createGroupByHash(distinctChannelTypes, normalizedDistinctChannels, Optional.empty(), Optional.empty(), 10_000); + for (UpdateRequest request : requests) { + Page page = request.getPage(); + Block[] blocks = page.getBlocks(); + + Block[] distinctBlocks = new Block[distinctChannels.length]; + for (int i = 0; i < distinctBlocks.length; i++) { + distinctBlocks[i] = blocks[distinctChannels[i]]; + } + + // Move through the positions while advancing the cursors in lockstep + GroupByIdBlock groupIds = groupByHash.getGroupIds(new Page(distinctBlocks)); + int positionCount = blocks[0].getPositionCount(); + long nextDistinctId = -1; + checkArgument(groupIds.getGroupCount() <= Integer.MAX_VALUE); + IntList positions = new IntArrayList((int) groupIds.getGroupCount()); + for (int position = 0; position < positionCount; position++) { + // We are reading ahead in the cursors, so we need to filter any nulls since they can not join + if (!containsNullValue(position, blocks)) { + // Only include the key if it is not already in the index + if (existingSnapshot.getJoinPosition(position, page) == UNLOADED_INDEX_KEY) { + // Only add the position if we have not seen this tuple before (based on the distinct channels) + long groupId = groupIds.getGroupId(position); + if (nextDistinctId < groupId) { + nextDistinctId = groupId; + positions.add(position); + } + } + } + } + + if (!positions.isEmpty()) { + builder.add(new PageAndPositions(request, positions)); + } + } + + pageAndPositions = builder.build(); + } + + @Override + public List getColumnTypes() + { + return types; + } + + @Override + public UnloadedIndexKeyRecordCursor cursor() + { + return new UnloadedIndexKeyRecordCursor(types, pageAndPositions); + } + + private static boolean containsNullValue(int position, Block... blocks) + { + for (Block block : blocks) { + if (block.isNull(position)) { + return true; + } + } + return false; + } + + public static class UnloadedIndexKeyRecordCursor + implements RecordCursor + { + private final List types; + private final Iterator pageAndPositionsIterator; + private Block[] blocks; + private Page page; + private IntListIterator positionIterator; + private int position; + + public UnloadedIndexKeyRecordCursor(List types, List pageAndPositions) + { + this.types = ImmutableList.copyOf(checkNotNull(types, "types is null")); + this.pageAndPositionsIterator = checkNotNull(pageAndPositions, "pageAndPositions is null").iterator(); + this.blocks = new Block[types.size()]; + } + + @Override + public long getTotalBytes() + { + return 0; + } + + @Override + public long getCompletedBytes() + { + return 0; + } + + @Override + public long getReadTimeNanos() + { + return 0; + } + + @Override + public Type getType(int field) + { + return types.get(field); + } + + @Override + public boolean advanceNextPosition() + { + while (positionIterator == null || !positionIterator.hasNext()) { + if (!pageAndPositionsIterator.hasNext()) { + return false; + } + PageAndPositions pageAndPositions = pageAndPositionsIterator.next(); + page = pageAndPositions.getUpdateRequest().getPage(); + blocks = page.getBlocks(); + checkState(types.size() == blocks.length); + positionIterator = pageAndPositions.getPositions().iterator(); + } + + position = positionIterator.nextInt(); + + return true; + } + + public Block[] getBlocks() + { + return blocks; + } + + public Page getPage() + { + return page; + } + + public int getPosition() + { + return position; + } + + @Override + public boolean getBoolean(int field) + { + return types.get(field).getBoolean(blocks[field], position); + } + + @Override + public long getLong(int field) + { + return types.get(field).getLong(blocks[field], position); + } + + @Override + public double getDouble(int field) + { + return types.get(field).getDouble(blocks[field], position); + } + + @Override + public Slice getSlice(int field) + { + return types.get(field).getSlice(blocks[field], position); + } + + @Override + public boolean isNull(int field) + { + return blocks[field].isNull(position); + } + + @Override + public void close() + { + // Do nothing + } + } + + private static class PageAndPositions + { + private final UpdateRequest updateRequest; + private final IntList positions; + + private PageAndPositions(UpdateRequest updateRequest, IntList positions) + { + this.updateRequest = checkNotNull(updateRequest, "updateRequest is null"); + this.positions = checkNotNull(positions, "positions is null"); + } + + private UpdateRequest getUpdateRequest() + { + return updateRequest; + } + + private IntList getPositions() + { + return positions; + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/index/UpdateRequest.java b/presto-main/src/main/java/com/facebook/presto/operator/index/UpdateRequest.java new file mode 100644 index 00000000..b40ea54c --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/index/UpdateRequest.java @@ -0,0 +1,67 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.index; + +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.block.Block; + +import javax.annotation.concurrent.ThreadSafe; + +import java.util.concurrent.atomic.AtomicReference; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +@ThreadSafe +class UpdateRequest +{ + private final Block[] blocks; + private final AtomicReference indexSnapshotReference = new AtomicReference<>(); + private final Page page; + + public UpdateRequest(Block... blocks) + { + this.blocks = checkNotNull(blocks, "blocks is null"); + this.page = new Page(blocks); + } + + @Deprecated + public Block[] getBlocks() + { + return blocks; + } + + public Page getPage() + { + return page; + } + + public void finished(IndexSnapshot indexSnapshot) + { + checkNotNull(indexSnapshot, "indexSnapshot is null"); + checkState(indexSnapshotReference.compareAndSet(null, indexSnapshot), "Already finished!"); + } + + public boolean isFinished() + { + return indexSnapshotReference.get() != null; + } + + public IndexSnapshot getFinishedIndexSnapshot() + { + IndexSnapshot indexSnapshot = indexSnapshotReference.get(); + checkState(indexSnapshot != null, "Update request is not finished"); + return indexSnapshot; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayCardinalityFunction.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayCardinalityFunction.java new file mode 100644 index 00000000..66454cc0 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayCardinalityFunction.java @@ -0,0 +1,79 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.ParametricScalar; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; + +import java.lang.invoke.MethodHandle; +import java.util.Map; + +import static com.facebook.presto.metadata.Signature.typeParameter; +import static com.facebook.presto.type.TypeUtils.readStructuralBlock; +import static com.facebook.presto.spi.type.TypeSignature.parseTypeSignature; +import static com.facebook.presto.type.TypeUtils.parameterizedTypeName; +import static com.facebook.presto.util.Reflection.methodHandle; +import static com.google.common.base.Preconditions.checkArgument; + +public final class ArrayCardinalityFunction + extends ParametricScalar +{ + public static final ArrayCardinalityFunction ARRAY_CARDINALITY = new ArrayCardinalityFunction(); + private static final Signature SIGNATURE = new Signature("cardinality", ImmutableList.of(typeParameter("E")), "bigint", ImmutableList.of("array"), false, false); + private static final MethodHandle METHOD_HANDLE = methodHandle(ArrayCardinalityFunction.class, "arrayLength", Slice.class); + + @Override + public Signature getSignature() + { + return SIGNATURE; + } + + @Override + public boolean isHidden() + { + return false; + } + + @Override + public boolean isDeterministic() + { + return true; + } + + @Override + public String getDescription() + { + return "Returns the cardinality (length) of the array"; + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + checkArgument(types.size() == 1, "Cardinality expects only one argument"); + Type type = types.get("E"); + return new FunctionInfo(new Signature("cardinality", parseTypeSignature(StandardTypes.BIGINT), parameterizedTypeName("array", type.getTypeSignature())), getDescription(), isHidden(), METHOD_HANDLE, isDeterministic(), false, ImmutableList.of(false)); + } + + public static long arrayLength(Slice slice) + { + return (long) readStructuralBlock(slice).getPositionCount(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayConcatFunction.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayConcatFunction.java new file mode 100644 index 00000000..777d9c07 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayConcatFunction.java @@ -0,0 +1,74 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.ParametricScalar; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.spi.type.TypeSignature; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; + +import java.lang.invoke.MethodHandle; +import java.util.Map; + +import static com.facebook.presto.metadata.Signature.typeParameter; +import static com.facebook.presto.type.TypeUtils.parameterizedTypeName; +import static com.facebook.presto.util.Reflection.methodHandle; + +public class ArrayConcatFunction + extends ParametricScalar +{ + public static final ArrayConcatFunction ARRAY_CONCAT_FUNCTION = new ArrayConcatFunction(); + private static final String FUNCTION_NAME = "concat"; + private static final Signature SIGNATURE = new Signature(FUNCTION_NAME, ImmutableList.of(typeParameter("E")), "array", ImmutableList.of("array", "array"), false, false); + private static final MethodHandle METHOD_HANDLE = methodHandle(ArrayConcatUtils.class, FUNCTION_NAME, Type.class, Slice.class, Slice.class); + + @Override + public Signature getSignature() + { + return SIGNATURE; + } + + @Override + public boolean isHidden() + { + return false; + } + + @Override + public boolean isDeterministic() + { + return true; + } + + @Override + public String getDescription() + { + return "Concatenates given arrays"; + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + Type elementType = types.get("E"); + MethodHandle methodHandle = METHOD_HANDLE.bindTo(elementType); + TypeSignature typeSignature = parameterizedTypeName("array", elementType.getTypeSignature()); + Signature signature = new Signature(FUNCTION_NAME, typeSignature, typeSignature, typeSignature); // return type & arg types are the same + return new FunctionInfo(signature, getDescription(), isHidden(), methodHandle, isDeterministic(), false, ImmutableList.of(false, false)); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayConcatUtils.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayConcatUtils.java new file mode 100644 index 00000000..7fa75ec7 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayConcatUtils.java @@ -0,0 +1,147 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.block.BlockBuilderStatus; +import com.facebook.presto.spi.block.VariableWidthBlockBuilder; +import com.facebook.presto.spi.type.Type; +import io.airlift.slice.Slice; + +import static com.facebook.presto.type.TypeUtils.readStructuralBlock; +import static com.facebook.presto.type.TypeUtils.buildStructuralSlice; + +public final class ArrayConcatUtils +{ + private ArrayConcatUtils() {} + + public static Slice concat(Type elementType, Slice left, Slice right) + { + Block leftBlock = readStructuralBlock(left); + Block rightBlock = readStructuralBlock(right); + BlockBuilder blockBuilder = new VariableWidthBlockBuilder(new BlockBuilderStatus(), leftBlock.getSizeInBytes() + rightBlock.getSizeInBytes()); + for (int i = 0; i < leftBlock.getPositionCount(); i++) { + elementType.appendTo(leftBlock, i, blockBuilder); + } + for (int i = 0; i < rightBlock.getPositionCount(); i++) { + elementType.appendTo(rightBlock, i, blockBuilder); + } + return buildStructuralSlice(blockBuilder); + } + + public static Slice appendElement(Type elementType, Slice in, long value) + { + Block block = readStructuralBlock(in); + BlockBuilder blockBuilder = new VariableWidthBlockBuilder(new BlockBuilderStatus(), block.getSizeInBytes()); + for (int i = 0; i < block.getPositionCount(); i++) { + elementType.appendTo(block, i, blockBuilder); + } + + elementType.writeLong(blockBuilder, value); + + return buildStructuralSlice(blockBuilder); + } + + public static Slice appendElement(Type elementType, Slice in, boolean value) + { + Block block = readStructuralBlock(in); + BlockBuilder blockBuilder = new VariableWidthBlockBuilder(new BlockBuilderStatus(), block.getSizeInBytes()); + for (int i = 0; i < block.getPositionCount(); i++) { + elementType.appendTo(block, i, blockBuilder); + } + + elementType.writeBoolean(blockBuilder, value); + + return buildStructuralSlice(blockBuilder); + } + + public static Slice appendElement(Type elementType, Slice in, double value) + { + Block block = readStructuralBlock(in); + BlockBuilder blockBuilder = new VariableWidthBlockBuilder(new BlockBuilderStatus(), block.getSizeInBytes()); + for (int i = 0; i < block.getPositionCount(); i++) { + elementType.appendTo(block, i, blockBuilder); + } + + elementType.writeDouble(blockBuilder, value); + + return buildStructuralSlice(blockBuilder); + } + + public static Slice appendElement(Type elementType, Slice in, Slice value) + { + Block block = readStructuralBlock(in); + BlockBuilder blockBuilder = new VariableWidthBlockBuilder(new BlockBuilderStatus(), block.getSizeInBytes()); + for (int i = 0; i < block.getPositionCount(); i++) { + elementType.appendTo(block, i, blockBuilder); + } + + elementType.writeSlice(blockBuilder, value); + + return buildStructuralSlice(blockBuilder); + } + + public static Slice prependElement(Type elementType, Slice value, Slice in) + { + Block block = readStructuralBlock(in); + BlockBuilder blockBuilder = new VariableWidthBlockBuilder(new BlockBuilderStatus(), block.getSizeInBytes()); + + elementType.writeSlice(blockBuilder, value); + for (int i = 0; i < block.getPositionCount(); i++) { + elementType.appendTo(block, i, blockBuilder); + } + + return buildStructuralSlice(blockBuilder); + } + + public static Slice prependElement(Type elementType, long value, Slice in) + { + Block block = readStructuralBlock(in); + BlockBuilder blockBuilder = new VariableWidthBlockBuilder(new BlockBuilderStatus(), block.getSizeInBytes()); + + elementType.writeLong(blockBuilder, value); + for (int i = 0; i < block.getPositionCount(); i++) { + elementType.appendTo(block, i, blockBuilder); + } + + return buildStructuralSlice(blockBuilder); + } + + public static Slice prependElement(Type elementType, boolean value, Slice in) + { + Block block = readStructuralBlock(in); + BlockBuilder blockBuilder = new VariableWidthBlockBuilder(new BlockBuilderStatus(), block.getSizeInBytes()); + + elementType.writeBoolean(blockBuilder, value); + for (int i = 0; i < block.getPositionCount(); i++) { + elementType.appendTo(block, i, blockBuilder); + } + + return buildStructuralSlice(blockBuilder); + } + + public static Slice prependElement(Type elementType, double value, Slice in) + { + Block block = readStructuralBlock(in); + BlockBuilder blockBuilder = new VariableWidthBlockBuilder(new BlockBuilderStatus(), block.getSizeInBytes()); + + elementType.writeDouble(blockBuilder, value); + for (int i = 0; i < block.getPositionCount(); i++) { + elementType.appendTo(block, i, blockBuilder); + } + + return buildStructuralSlice(blockBuilder); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayConstructor.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayConstructor.java new file mode 100644 index 00000000..09ae539e --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayConstructor.java @@ -0,0 +1,179 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import com.facebook.presto.byteCode.Block; +import com.facebook.presto.byteCode.ClassDefinition; +import com.facebook.presto.byteCode.Scope; +import com.facebook.presto.byteCode.DynamicClassLoader; +import com.facebook.presto.byteCode.MethodDefinition; +import com.facebook.presto.byteCode.Parameter; +import com.facebook.presto.byteCode.Variable; +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.ParametricScalar; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.sql.gen.ByteCodeUtils; +import com.facebook.presto.sql.gen.CallSiteBinder; +import com.facebook.presto.sql.gen.CompilerUtils; +import com.facebook.presto.type.ArrayType; +import com.google.common.base.Joiner; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.common.primitives.Primitives; +import io.airlift.slice.Slice; + +import java.lang.invoke.MethodHandle; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static com.facebook.presto.byteCode.Access.FINAL; +import static com.facebook.presto.byteCode.Access.PRIVATE; +import static com.facebook.presto.byteCode.Access.PUBLIC; +import static com.facebook.presto.byteCode.Access.STATIC; +import static com.facebook.presto.byteCode.Access.a; +import static com.facebook.presto.byteCode.Parameter.arg; +import static com.facebook.presto.byteCode.ParameterizedType.type; +import static com.facebook.presto.metadata.Signature.typeParameter; +import static com.facebook.presto.sql.gen.CompilerUtils.defineClass; +import static com.facebook.presto.sql.gen.SqlTypeByteCodeExpression.constantType; +import static com.facebook.presto.sql.relational.Signatures.arrayConstructorSignature; +import static com.facebook.presto.type.TypeUtils.parameterizedTypeName; +import static com.facebook.presto.util.ImmutableCollectors.toImmutableList; +import static com.google.common.base.Preconditions.checkArgument; +import static java.lang.invoke.MethodHandles.lookup; + +public final class ArrayConstructor + extends ParametricScalar +{ + public static final ArrayConstructor ARRAY_CONSTRUCTOR = new ArrayConstructor(); + private static final Signature SIGNATURE = new Signature("array_constructor", ImmutableList.of(typeParameter("E")), "array", ImmutableList.of("E", "E"), true, true); + + @Override + public Signature getSignature() + { + return SIGNATURE; + } + + @Override + public boolean isHidden() + { + return true; + } + + @Override + public boolean isDeterministic() + { + return true; + } + + @Override + public String getDescription() + { + // Internal function, doesn't need a description + return ""; + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + checkArgument(types.size() == 1, "Can only construct arrays from exactly matching types"); + ImmutableList.Builder> builder = ImmutableList.builder(); + Type type = types.get("E"); + for (int i = 0; i < arity; i++) { + if (type.getJavaType().isPrimitive()) { + builder.add(Primitives.wrap(type.getJavaType())); + } + else { + builder.add(type.getJavaType()); + } + } + ImmutableList> stackTypes = builder.build(); + Class clazz = generateArrayConstructor(stackTypes, type); + MethodHandle methodHandle; + try { + Method method = clazz.getMethod("arrayConstructor", stackTypes.toArray(new Class[stackTypes.size()])); + methodHandle = lookup().unreflect(method); + } + catch (ReflectiveOperationException e) { + throw Throwables.propagate(e); + } + List nullableParameters = ImmutableList.copyOf(Collections.nCopies(stackTypes.size(), true)); + return new FunctionInfo(arrayConstructorSignature(parameterizedTypeName("array", type.getTypeSignature()), Collections.nCopies(arity, type.getTypeSignature())), "Constructs an array of the given elements", true, methodHandle, true, false, nullableParameters); + } + + private static Class generateArrayConstructor(List> stackTypes, Type elementType) + { + List stackTypeNames = stackTypes.stream() + .map(Class::getSimpleName) + .collect(toImmutableList()); + + ClassDefinition definition = new ClassDefinition( + a(PUBLIC, FINAL), + CompilerUtils.makeClassName(Joiner.on("").join(stackTypeNames) + "ArrayConstructor"), + type(Object.class)); + + // Generate constructor + definition.declareDefaultConstructor(a(PRIVATE)); + + // Generate arrayConstructor() + ImmutableList.Builder parameters = ImmutableList.builder(); + for (int i = 0; i < stackTypes.size(); i++) { + Class stackType = stackTypes.get(i); + parameters.add(arg("arg" + i, stackType)); + } + + MethodDefinition method = definition.declareMethod(a(PUBLIC, STATIC), "arrayConstructor", type(Slice.class), parameters.build()); + Scope scope = method.getScope(); + Block body = method.getBody(); + + Variable elementTypeVariable = scope.declareVariable(Type.class, "elementTypeVariable"); + CallSiteBinder binder = new CallSiteBinder(); + + body.comment("elementTypeVariable = elementType;") + .append(constantType(binder, elementType)) + .putVariable(elementTypeVariable); + + Variable valuesVariable = scope.declareVariable(List.class, "values"); + body.comment("List values = new ArrayList();") + .newObject(ArrayList.class) + .dup() + .invokeConstructor(ArrayList.class) + .putVariable(valuesVariable); + + for (int i = 0; i < stackTypes.size(); i++) { + body.comment("values.add(arg%d);", i) + .getVariable(valuesVariable) + .append(scope.getVariable("arg" + i)); + Class stackType = stackTypes.get(i); + if (stackType.isPrimitive()) { + body.append(ByteCodeUtils.boxPrimitiveIfNecessary(scope, stackType)); + } + body.invokeInterface(List.class, "add", boolean.class, Object.class); + } + + body.comment("return toStackRepresentation(values, elementType);") + .getVariable(valuesVariable) + .getVariable(elementTypeVariable) + .invokeStatic(ArrayType.class, "toStackRepresentation", Slice.class, List.class, Type.class) + .retObject(); + + return defineClass(definition, Object.class, binder.getBindings(), new DynamicClassLoader(ArrayConstructor.class.getClassLoader())); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayContains.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayContains.java new file mode 100644 index 00000000..92571614 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayContains.java @@ -0,0 +1,157 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.ParametricScalar; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.spi.type.TypeSignature; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; + +import java.lang.invoke.MethodHandle; +import java.util.Map; + +import static com.facebook.presto.metadata.Signature.comparableTypeParameter; +import static com.facebook.presto.spi.type.TypeSignature.parseTypeSignature; +import static com.facebook.presto.type.TypeUtils.createBlock; +import static com.facebook.presto.type.TypeUtils.parameterizedTypeName; +import static com.facebook.presto.type.TypeUtils.readStructuralBlock; +import static com.facebook.presto.util.Reflection.methodHandle; + +public final class ArrayContains + extends ParametricScalar +{ + public static final ArrayContains ARRAY_CONTAINS = new ArrayContains(); + private static final TypeSignature RETURN_TYPE = parseTypeSignature(StandardTypes.BOOLEAN); + private static final String FUNCTION_NAME = "contains"; + private static final Signature SIGNATURE = new Signature(FUNCTION_NAME, ImmutableList.of(comparableTypeParameter("T")), StandardTypes.BOOLEAN, ImmutableList.of("array", "T"), false, false); + + @Override + public Signature getSignature() + { + return SIGNATURE; + } + + @Override + public boolean isHidden() + { + return false; + } + + @Override + public boolean isDeterministic() + { + return true; + } + + @Override + public String getDescription() + { + return "Determines whether given value exists in the array"; + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + Type type = types.get("T"); + TypeSignature valueType = type.getTypeSignature(); + TypeSignature arrayType = parameterizedTypeName(StandardTypes.ARRAY, valueType); + MethodHandle methodHandle = methodHandle(ArrayContains.class, "contains", Type.class, Slice.class, type.getJavaType()); + Signature signature = new Signature(FUNCTION_NAME, RETURN_TYPE, arrayType, valueType); + + return new FunctionInfo(signature, getDescription(), isHidden(), methodHandle.bindTo(type), isDeterministic(), true, ImmutableList.of(false, false)); + } + + public static Boolean contains(Type type, Slice slice, Slice value) + { + Block arrayBlock = readStructuralBlock(slice); + Block valueBlock = createBlock(type, value); + boolean foundNull = false; + for (int i = 0; i < arrayBlock.getPositionCount(); i++) { + if (arrayBlock.isNull(i)) { + foundNull = true; + } + if (type.equalTo(arrayBlock, i, valueBlock, 0)) { + return true; + } + } + if (foundNull) { + return null; + } + return false; + } + + public static Boolean contains(Type type, Slice slice, long value) + { + Block arrayBlock = readStructuralBlock(slice); + Block valueBlock = createBlock(type, value); + boolean foundNull = false; + for (int i = 0; i < arrayBlock.getPositionCount(); i++) { + if (arrayBlock.isNull(i)) { + foundNull = true; + } + if (type.equalTo(arrayBlock, i, valueBlock, 0)) { + return true; + } + } + if (foundNull) { + return null; + } + return false; + } + + public static Boolean contains(Type type, Slice slice, boolean value) + { + Block arrayBlock = readStructuralBlock(slice); + Block valueBlock = createBlock(type, value); + boolean foundNull = false; + for (int i = 0; i < arrayBlock.getPositionCount(); i++) { + if (arrayBlock.isNull(i)) { + foundNull = true; + } + if (type.equalTo(arrayBlock, i, valueBlock, 0)) { + return true; + } + } + if (foundNull) { + return null; + } + return false; + } + + public static Boolean contains(Type type, Slice slice, double value) + { + Block arrayBlock = readStructuralBlock(slice); + Block valueBlock = createBlock(type, value); + boolean foundNull = false; + for (int i = 0; i < arrayBlock.getPositionCount(); i++) { + if (arrayBlock.isNull(i)) { + foundNull = true; + } + if (type.equalTo(arrayBlock, i, valueBlock, 0)) { + return true; + } + } + if (foundNull) { + return null; + } + return false; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayDistinctFunction.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayDistinctFunction.java new file mode 100644 index 00000000..955a4081 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayDistinctFunction.java @@ -0,0 +1,104 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.ParametricScalar; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.operator.aggregation.SimpleTypedSet; +import com.facebook.presto.operator.aggregation.TypedSet; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.block.BlockBuilderStatus; +import com.facebook.presto.spi.block.VariableWidthBlockBuilder; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; + +import java.lang.invoke.MethodHandle; +import java.util.Map; + +import static com.facebook.presto.metadata.Signature.comparableTypeParameter; +import static com.facebook.presto.type.TypeUtils.buildStructuralSlice; +import static com.facebook.presto.type.TypeUtils.parameterizedTypeName; +import static com.facebook.presto.type.TypeUtils.readStructuralBlock; +import static com.facebook.presto.util.Reflection.methodHandle; +import static com.google.common.base.Preconditions.checkArgument; +import static java.lang.String.format; + +public final class ArrayDistinctFunction + extends ParametricScalar +{ + public static final ArrayDistinctFunction ARRAY_DISTINCT_FUNCTION = new ArrayDistinctFunction(); + private static final String FUNCTION_NAME = "array_distinct"; + private static final Signature SIGNATURE = new Signature(FUNCTION_NAME, ImmutableList.of(comparableTypeParameter("E")), "array", ImmutableList.of("array"), false, false); + private static final MethodHandle METHOD_HANDLE = methodHandle(ArrayDistinctFunction.class, "distinct", Type.class, Slice.class); + + @Override + public Signature getSignature() + { + return SIGNATURE; + } + + @Override + public boolean isHidden() + { + return false; + } + + @Override + public boolean isDeterministic() + { + return true; + } + + @Override + public String getDescription() + { + return "Remove duplicate values from the given array"; + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + checkArgument(types.size() == 1, format("%s expects only one argument", FUNCTION_NAME)); + Type type = types.get("E"); + MethodHandle methodHandle = METHOD_HANDLE.bindTo(type); + Signature signature = new Signature(FUNCTION_NAME, + parameterizedTypeName("array", type.getTypeSignature()), + parameterizedTypeName("array", type.getTypeSignature())); + return new FunctionInfo(signature, getDescription(), isHidden(), methodHandle, isDeterministic(), false, ImmutableList.of(false)); + } + + public static Slice distinct(Type type, Slice array) + { + Block elementsBlock = readStructuralBlock(array); + if (elementsBlock.getPositionCount() == 0) { + return array; + } + + TypedSet typedSet = new SimpleTypedSet(type, elementsBlock.getPositionCount()); + BlockBuilder distinctElementBlockBuilder = new VariableWidthBlockBuilder(new BlockBuilderStatus(), elementsBlock.getPositionCount()); + for (int i = 0; i < elementsBlock.getPositionCount(); i++) { + if (!typedSet.contains(elementsBlock, i)) { + typedSet.add(elementsBlock, i); + type.appendTo(elementsBlock, i, distinctElementBlockBuilder); + } + } + + return buildStructuralSlice(distinctElementBlockBuilder); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayEqualOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayEqualOperator.java new file mode 100644 index 00000000..56b01dab --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayEqualOperator.java @@ -0,0 +1,91 @@ +package com.facebook.presto.operator.scalar; +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.ParametricOperator; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.spi.type.TypeSignature; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; + +import java.lang.invoke.MethodHandle; +import java.util.Map; + +import static com.facebook.presto.metadata.FunctionRegistry.operatorInfo; +import static com.facebook.presto.metadata.OperatorType.EQUAL; +import static com.facebook.presto.metadata.Signature.comparableTypeParameter; +import static com.facebook.presto.type.ArrayType.ARRAY_NULL_ELEMENT_MSG; +import static com.facebook.presto.type.TypeUtils.castValue; +import static com.facebook.presto.type.TypeUtils.checkElementNotNull; +import static com.facebook.presto.type.TypeUtils.readStructuralBlock; +import static com.facebook.presto.spi.StandardErrorCode.INTERNAL_ERROR; +import static com.facebook.presto.spi.type.TypeSignature.parseTypeSignature; +import static com.facebook.presto.util.Reflection.methodHandle; + +public class ArrayEqualOperator + extends ParametricOperator +{ + public static final ArrayEqualOperator ARRAY_EQUAL = new ArrayEqualOperator(); + private static final TypeSignature RETURN_TYPE = parseTypeSignature(StandardTypes.BOOLEAN); + private static final MethodHandle METHOD_HANDLE = methodHandle(ArrayEqualOperator.class, "equals", MethodHandle.class, Type.class, Slice.class, Slice.class); + + private ArrayEqualOperator() + { + super(EQUAL, ImmutableList.of(comparableTypeParameter("T")), StandardTypes.BOOLEAN, ImmutableList.of("array", "array")); + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + Type elementType = types.get("T"); + Type type = typeManager.getParameterizedType(StandardTypes.ARRAY, ImmutableList.of(elementType.getTypeSignature()), ImmutableList.of()); + TypeSignature typeSignature = type.getTypeSignature(); + MethodHandle equalsFunction = functionRegistry.resolveOperator(EQUAL, ImmutableList.of(elementType, elementType)).getMethodHandle(); + MethodHandle method = METHOD_HANDLE.bindTo(equalsFunction).bindTo(elementType); + return operatorInfo(EQUAL, RETURN_TYPE, ImmutableList.of(typeSignature, typeSignature), method, false, ImmutableList.of(false, false)); + } + + public static boolean equals(MethodHandle equalsFunction, Type type, Slice left, Slice right) + { + Block leftArray = readStructuralBlock(left); + Block rightArray = readStructuralBlock(right); + if (leftArray.getPositionCount() != rightArray.getPositionCount()) { + return false; + } + for (int i = 0; i < leftArray.getPositionCount(); i++) { + checkElementNotNull(leftArray.isNull(i), ARRAY_NULL_ELEMENT_MSG); + checkElementNotNull(rightArray.isNull(i), ARRAY_NULL_ELEMENT_MSG); + Object leftElement = castValue(type, leftArray, i); + Object rightElement = castValue(type, rightArray, i); + try { + if (!(boolean) equalsFunction.invoke(leftElement, rightElement)) { + return false; + } + } + catch (Throwable t) { + Throwables.propagateIfInstanceOf(t, Error.class); + Throwables.propagateIfInstanceOf(t, PrestoException.class); + + throw new PrestoException(INTERNAL_ERROR, t); + } + } + return true; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayFunctions.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayFunctions.java new file mode 100644 index 00000000..d79a3f58 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayFunctions.java @@ -0,0 +1,38 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.block.BlockBuilderStatus; +import com.facebook.presto.spi.block.VariableWidthBlockBuilder; + +import com.facebook.presto.type.SqlType; +import io.airlift.slice.Slice; + +import static com.facebook.presto.type.TypeUtils.buildStructuralSlice; + +public final class ArrayFunctions +{ + private ArrayFunctions() + { + } + + @ScalarFunction(hidden = true) + @SqlType("array") + public static Slice arrayConstructor() + { + BlockBuilder blockBuilder = new VariableWidthBlockBuilder(new BlockBuilderStatus(), 0); + return buildStructuralSlice(blockBuilder); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayGreaterThanOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayGreaterThanOperator.java new file mode 100644 index 00000000..e9ab145a --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayGreaterThanOperator.java @@ -0,0 +1,97 @@ +package com.facebook.presto.operator.scalar; +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.ParametricOperator; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.spi.type.TypeSignature; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; + +import java.lang.invoke.MethodHandle; +import java.util.Map; + +import static com.facebook.presto.metadata.FunctionRegistry.operatorInfo; +import static com.facebook.presto.metadata.OperatorType.GREATER_THAN; +import static com.facebook.presto.metadata.Signature.orderableTypeParameter; +import static com.facebook.presto.spi.StandardErrorCode.INTERNAL_ERROR; +import static com.facebook.presto.spi.type.TypeSignature.parseTypeSignature; +import static com.facebook.presto.type.ArrayType.ARRAY_NULL_ELEMENT_MSG; +import static com.facebook.presto.type.TypeUtils.castValue; +import static com.facebook.presto.type.TypeUtils.checkElementNotNull; +import static com.facebook.presto.type.TypeUtils.readStructuralBlock; +import static com.facebook.presto.util.Reflection.methodHandle; + +public class ArrayGreaterThanOperator + extends ParametricOperator +{ + public static final ArrayGreaterThanOperator ARRAY_GREATER_THAN = new ArrayGreaterThanOperator(); + private static final TypeSignature RETURN_TYPE = parseTypeSignature(StandardTypes.BOOLEAN); + private static final MethodHandle METHOD_HANDLE = methodHandle(ArrayGreaterThanOperator.class, "greaterThan", MethodHandle.class, Type.class, Slice.class, Slice.class); + + private ArrayGreaterThanOperator() + { + super(GREATER_THAN, ImmutableList.of(orderableTypeParameter("T")), StandardTypes.BOOLEAN, ImmutableList.of("array", "array")); + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + Type elementType = types.get("T"); + Type type = typeManager.getParameterizedType(StandardTypes.ARRAY, ImmutableList.of(elementType.getTypeSignature()), ImmutableList.of()); + TypeSignature typeSignature = type.getTypeSignature(); + MethodHandle greaterThanFunction = functionRegistry.resolveOperator(GREATER_THAN, ImmutableList.of(elementType, elementType)).getMethodHandle(); + MethodHandle method = METHOD_HANDLE.bindTo(greaterThanFunction).bindTo(elementType); + return operatorInfo(GREATER_THAN, RETURN_TYPE, ImmutableList.of(typeSignature, typeSignature), method, false, ImmutableList.of(false, false)); + } + + public static boolean greaterThan(MethodHandle greaterThanFunction, Type type, Slice left, Slice right) + { + Block leftArray = readStructuralBlock(left); + Block rightArray = readStructuralBlock(right); + + int len = Math.min(leftArray.getPositionCount(), rightArray.getPositionCount()); + int index = 0; + while (index < len) { + checkElementNotNull(leftArray.isNull(index), ARRAY_NULL_ELEMENT_MSG); + checkElementNotNull(rightArray.isNull(index), ARRAY_NULL_ELEMENT_MSG); + Object leftElement = castValue(type, leftArray, index); + Object rightElement = castValue(type, rightArray, index); + try { + if ((boolean) greaterThanFunction.invoke(leftElement, rightElement)) { + return true; + } + if ((boolean) greaterThanFunction.invoke(rightElement, leftElement)) { + return false; + } + } + catch (Throwable t) { + Throwables.propagateIfInstanceOf(t, Error.class); + Throwables.propagateIfInstanceOf(t, PrestoException.class); + + throw new PrestoException(INTERNAL_ERROR, t); + } + index++; + } + + return leftArray.getPositionCount() > rightArray.getPositionCount(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayGreaterThanOrEqualOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayGreaterThanOrEqualOperator.java new file mode 100644 index 00000000..23910b6e --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayGreaterThanOrEqualOperator.java @@ -0,0 +1,98 @@ +package com.facebook.presto.operator.scalar; +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.ParametricOperator; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.spi.type.TypeSignature; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; + +import java.lang.invoke.MethodHandle; +import java.util.Map; + +import static com.facebook.presto.metadata.FunctionRegistry.operatorInfo; +import static com.facebook.presto.metadata.OperatorType.GREATER_THAN; +import static com.facebook.presto.metadata.OperatorType.GREATER_THAN_OR_EQUAL; +import static com.facebook.presto.metadata.Signature.orderableTypeParameter; +import static com.facebook.presto.spi.StandardErrorCode.INTERNAL_ERROR; +import static com.facebook.presto.spi.type.TypeSignature.parseTypeSignature; +import static com.facebook.presto.type.ArrayType.ARRAY_NULL_ELEMENT_MSG; +import static com.facebook.presto.type.TypeUtils.castValue; +import static com.facebook.presto.type.TypeUtils.checkElementNotNull; +import static com.facebook.presto.type.TypeUtils.readStructuralBlock; +import static com.facebook.presto.util.Reflection.methodHandle; + +public class ArrayGreaterThanOrEqualOperator + extends ParametricOperator +{ + public static final ArrayGreaterThanOrEqualOperator ARRAY_GREATER_THAN_OR_EQUAL = new ArrayGreaterThanOrEqualOperator(); + private static final TypeSignature RETURN_TYPE = parseTypeSignature(StandardTypes.BOOLEAN); + private static final MethodHandle METHOD_HANDLE = methodHandle(ArrayGreaterThanOrEqualOperator.class, "greaterThanOrEqual", MethodHandle.class, Type.class, Slice.class, Slice.class); + + private ArrayGreaterThanOrEqualOperator() + { + super(GREATER_THAN_OR_EQUAL, ImmutableList.of(orderableTypeParameter("T")), StandardTypes.BOOLEAN, ImmutableList.of("array", "array")); + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + Type elementType = types.get("T"); + Type type = typeManager.getParameterizedType(StandardTypes.ARRAY, ImmutableList.of(elementType.getTypeSignature()), ImmutableList.of()); + TypeSignature typeSignature = type.getTypeSignature(); + MethodHandle greaterThanFunction = functionRegistry.resolveOperator(GREATER_THAN, ImmutableList.of(elementType, elementType)).getMethodHandle(); + MethodHandle method = METHOD_HANDLE.bindTo(greaterThanFunction).bindTo(elementType); + return operatorInfo(GREATER_THAN_OR_EQUAL, RETURN_TYPE, ImmutableList.of(typeSignature, typeSignature), method, false, ImmutableList.of(false, false)); + } + + public static boolean greaterThanOrEqual(MethodHandle greaterThanFunction, Type type, Slice left, Slice right) + { + Block leftArray = readStructuralBlock(left); + Block rightArray = readStructuralBlock(right); + + int len = Math.min(leftArray.getPositionCount(), rightArray.getPositionCount()); + int index = 0; + while (index < len) { + checkElementNotNull(leftArray.isNull(index), ARRAY_NULL_ELEMENT_MSG); + checkElementNotNull(rightArray.isNull(index), ARRAY_NULL_ELEMENT_MSG); + Object leftElement = castValue(type, leftArray, index); + Object rightElement = castValue(type, rightArray, index); + try { + if ((boolean) greaterThanFunction.invoke(leftElement, rightElement)) { + return true; + } + if ((boolean) greaterThanFunction.invoke(rightElement, leftElement)) { + return false; + } + } + catch (Throwable t) { + Throwables.propagateIfInstanceOf(t, Error.class); + Throwables.propagateIfInstanceOf(t, PrestoException.class); + + throw new PrestoException(INTERNAL_ERROR, t); + } + index++; + } + + return leftArray.getPositionCount() >= rightArray.getPositionCount(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayHashCodeOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayHashCodeOperator.java new file mode 100644 index 00000000..9b90ff36 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayHashCodeOperator.java @@ -0,0 +1,63 @@ +package com.facebook.presto.operator.scalar; +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.ParametricOperator; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.block.BlockBuilderStatus; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.spi.type.TypeSignature; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; + +import java.lang.invoke.MethodHandle; +import java.util.Map; + +import static com.facebook.presto.metadata.FunctionRegistry.operatorInfo; +import static com.facebook.presto.metadata.OperatorType.HASH_CODE; +import static com.facebook.presto.metadata.Signature.comparableTypeParameter; +import static com.facebook.presto.spi.type.TypeSignature.parseTypeSignature; +import static com.facebook.presto.util.Reflection.methodHandle; + +public class ArrayHashCodeOperator + extends ParametricOperator +{ + public static final ArrayHashCodeOperator ARRAY_HASH_CODE = new ArrayHashCodeOperator(); + private static final TypeSignature RETURN_TYPE = parseTypeSignature(StandardTypes.BIGINT); + public static final MethodHandle METHOD_HANDLE = methodHandle(ArrayHashCodeOperator.class, "hash", Type.class, Slice.class); + + private ArrayHashCodeOperator() + { + super(HASH_CODE, ImmutableList.of(comparableTypeParameter("T")), StandardTypes.BIGINT, ImmutableList.of("array")); + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + Type type = types.get("T"); + type = typeManager.getParameterizedType(StandardTypes.ARRAY, ImmutableList.of(type.getTypeSignature()), ImmutableList.of()); + TypeSignature typeSignature = type.getTypeSignature(); + return operatorInfo(HASH_CODE, RETURN_TYPE, ImmutableList.of(typeSignature), METHOD_HANDLE.bindTo(type), false, ImmutableList.of(false)); + } + + public static long hash(Type type, Slice slice) + { + BlockBuilder blockBuilder = type.createBlockBuilder(new BlockBuilderStatus(), 1, slice.length()); + blockBuilder.writeBytes(slice, 0, slice.length()); + return type.hash(blockBuilder.closeEntry().build(), 0); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayIntersectFunction.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayIntersectFunction.java new file mode 100644 index 00000000..36e89e31 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayIntersectFunction.java @@ -0,0 +1,159 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.ParametricScalar; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.block.BlockBuilderStatus; +import com.facebook.presto.spi.block.VariableWidthBlockBuilder; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.spi.type.TypeSignature; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; +import it.unimi.dsi.fastutil.ints.AbstractIntComparator; +import it.unimi.dsi.fastutil.ints.IntArrays; +import it.unimi.dsi.fastutil.ints.IntComparator; + +import java.lang.invoke.MethodHandle; +import java.util.Map; + +import static com.facebook.presto.metadata.Signature.orderableTypeParameter; +import static com.facebook.presto.type.TypeUtils.buildStructuralSlice; +import static com.facebook.presto.type.TypeUtils.parameterizedTypeName; +import static com.facebook.presto.type.TypeUtils.readStructuralBlock; +import static com.facebook.presto.util.Reflection.methodHandle; +import static com.google.common.base.Preconditions.checkArgument; +import static java.lang.String.format; + +public final class ArrayIntersectFunction + extends ParametricScalar +{ + public static final ArrayIntersectFunction ARRAY_INTERSECT_FUNCTION = new ArrayIntersectFunction(); + private static final String FUNCTION_NAME = "array_intersect"; + private static final Signature SIGNATURE = new Signature(FUNCTION_NAME, ImmutableList.of(orderableTypeParameter("E")), "array", ImmutableList.of("array", "array"), false, false); + private static final MethodHandle METHOD_HANDLE = methodHandle(ArrayIntersectFunction.class, "intersect", Type.class, Slice.class, Slice.class); + + @Override + public Signature getSignature() + { + return SIGNATURE; + } + + @Override + public boolean isHidden() + { + return false; + } + + @Override + public boolean isDeterministic() + { + return true; + } + + @Override + public String getDescription() + { + return "Intersects elements of the two given arrays"; + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + checkArgument(types.size() == 1, format("%s expects only one argument", FUNCTION_NAME)); + TypeSignature typeSignature = parameterizedTypeName("array", types.get("E").getTypeSignature()); + MethodHandle methodHandle = METHOD_HANDLE.bindTo(types.get("E")); + Signature signature = new Signature(FUNCTION_NAME, typeSignature, typeSignature, typeSignature); + return new FunctionInfo(signature, getDescription(), isHidden(), methodHandle, isDeterministic(), false, ImmutableList.of(false, false)); + } + + private static IntComparator IntBlockCompare(Type type, Block block) + { + return new AbstractIntComparator() + { + @Override + public int compare(int left, int right) + { + if (block.isNull(left) && block.isNull(right)) { + return 0; + } + if (block.isNull(left)) { + return -1; + } + if (block.isNull(right)) { + return 1; + } + return type.compareTo(block, left, block, right); + } + }; + } + + public static Slice intersect(Type type, Slice leftArray, Slice rightArray) + { + Block leftBlock = readStructuralBlock(leftArray); + Block rightBlock = readStructuralBlock(rightArray); + + int leftPositionCount = leftBlock.getPositionCount(); + int rightPositionCount = rightBlock.getPositionCount(); + + int[] leftPositions = new int[leftPositionCount]; + int[] rightPositions = new int[rightPositionCount]; + + for (int i = 0; i < leftPositionCount; i++) { + leftPositions[i] = i; + } + for (int i = 0; i < rightPositionCount; i++) { + rightPositions[i] = i; + } + IntArrays.quickSort(leftPositions, IntBlockCompare(type, leftBlock)); + IntArrays.quickSort(rightPositions, IntBlockCompare(type, rightBlock)); + + BlockBuilder resultBlockBuilder = new VariableWidthBlockBuilder(new BlockBuilderStatus(), leftBlock.getSizeInBytes()); + + int leftCurrentPosition = 0; + int rightCurrentPosition = 0; + int leftBasePosition; + int rightBasePosition; + + while (leftCurrentPosition < leftPositionCount && rightCurrentPosition < rightPositionCount) { + leftBasePosition = leftCurrentPosition; + rightBasePosition = rightCurrentPosition; + int compareValue = type.compareTo(leftBlock, leftPositions[leftCurrentPosition], rightBlock, rightPositions[rightCurrentPosition]); + if (compareValue > 0) { + rightCurrentPosition++; + } + else if (compareValue < 0) { + leftCurrentPosition++; + } + else { + type.appendTo(leftBlock, leftPositions[leftCurrentPosition], resultBlockBuilder); + leftCurrentPosition++; + rightCurrentPosition++; + while (leftCurrentPosition < leftPositionCount && type.equalTo(leftBlock, leftPositions[leftBasePosition], leftBlock, leftPositions[leftCurrentPosition])) { + leftCurrentPosition++; + } + while (rightCurrentPosition < rightPositionCount && type.equalTo(rightBlock, rightPositions[rightBasePosition], rightBlock, rightPositions[rightCurrentPosition])) { + rightCurrentPosition++; + } + } + } + + return buildStructuralSlice(resultBlockBuilder); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayJoin.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayJoin.java new file mode 100644 index 00000000..2527f077 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayJoin.java @@ -0,0 +1,216 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.ParametricScalar; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.spi.type.TypeSignature; +import com.facebook.presto.type.UnknownType; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.DynamicSliceOutput; +import io.airlift.slice.Slice; + +import java.lang.invoke.MethodHandle; +import java.util.List; +import java.util.Map; + +import static com.facebook.presto.metadata.OperatorType.CAST; +import static com.facebook.presto.metadata.Signature.internalOperator; +import static com.facebook.presto.metadata.Signature.typeParameter; +import static com.facebook.presto.spi.StandardErrorCode.INTERNAL_ERROR; +import static com.facebook.presto.spi.StandardErrorCode.INVALID_FUNCTION_ARGUMENT; +import static com.facebook.presto.spi.type.TypeSignature.parseTypeSignature; +import static com.facebook.presto.type.TypeUtils.parameterizedTypeName; +import static com.facebook.presto.type.TypeUtils.readStructuralBlock; +import static com.facebook.presto.util.Failures.checkCondition; +import static com.facebook.presto.util.Reflection.methodHandle; +import static java.lang.String.format; + +public final class ArrayJoin + extends ParametricScalar +{ + public static final ArrayJoin ARRAY_JOIN = new ArrayJoin(); + public static final ArrayJoinWithNullReplacement ARRAY_JOIN_WITH_NULL_REPLACEMENT = new ArrayJoinWithNullReplacement(); + + private static final TypeSignature VARCHAR_TYPE_SIGNATURE = parseTypeSignature(StandardTypes.VARCHAR); + private static final String FUNCTION_NAME = "array_join"; + private static final String DESCRIPTION = "Concatenates the elements of the given array using a delimiter and an optional string to replace nulls"; + private static final Signature SIGNATURE = new Signature(FUNCTION_NAME, ImmutableList.of(typeParameter("T")), StandardTypes.VARCHAR, ImmutableList.of("array", StandardTypes.VARCHAR), false, false); + + public static class ArrayJoinWithNullReplacement + extends ParametricScalar + { + private final Signature signature = new Signature(FUNCTION_NAME, ImmutableList.of(typeParameter("T")), StandardTypes.VARCHAR, ImmutableList.of("array", StandardTypes.VARCHAR, StandardTypes.VARCHAR), false, false); + + @Override + public Signature getSignature() + { + return signature; + } + + @Override + public boolean isHidden() + { + return false; + } + + @Override + public boolean isDeterministic() + { + return true; + } + + @Override + public String getDescription() + { + return DESCRIPTION; + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + Type type = types.get("T"); + TypeSignature arrayType = parameterizedTypeName(StandardTypes.ARRAY, type.getTypeSignature()); + Signature signature = new Signature(FUNCTION_NAME, VARCHAR_TYPE_SIGNATURE, arrayType, VARCHAR_TYPE_SIGNATURE, VARCHAR_TYPE_SIGNATURE); + MethodHandle methodHandle = methodHandle(ArrayJoin.class, "arrayJoin", FunctionInfo.class, Type.class, ConnectorSession.class, Slice.class, Slice.class, Slice.class); + return specializeArrayJoin(types, functionRegistry, ImmutableList.of(false, false, false), signature, methodHandle); + } + } + + @Override + public Signature getSignature() + { + return SIGNATURE; + } + + @Override + public boolean isHidden() + { + return false; + } + + @Override + public boolean isDeterministic() + { + return true; + } + + @Override + public String getDescription() + { + return DESCRIPTION; + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + Type type = types.get("T"); + TypeSignature arrayType = parameterizedTypeName(StandardTypes.ARRAY, type.getTypeSignature()); + Signature signature = new Signature(FUNCTION_NAME, VARCHAR_TYPE_SIGNATURE, arrayType, VARCHAR_TYPE_SIGNATURE); + MethodHandle methodHandle = methodHandle(ArrayJoin.class, "arrayJoin", FunctionInfo.class, Type.class, ConnectorSession.class, Slice.class, Slice.class); + return specializeArrayJoin(types, functionRegistry, ImmutableList.of(false, false), signature, methodHandle); + } + + private static FunctionInfo specializeArrayJoin(Map types, FunctionRegistry functionRegistry, List nullableArguments, Signature signature, MethodHandle methodHandle) + { + Type type = types.get("T"); + FunctionInfo castFunction = functionRegistry.getExactFunction(internalOperator(CAST.name(), VARCHAR_TYPE_SIGNATURE, ImmutableList.of(type.getTypeSignature()))); + + if (!(type instanceof UnknownType)) { + checkCondition(castFunction != null, INVALID_FUNCTION_ARGUMENT, "Input type %s not supported", type); + } + + return new FunctionInfo(signature, DESCRIPTION, false, methodHandle.bindTo(castFunction).bindTo(type), true, false, nullableArguments); + } + + public static Slice arrayJoin(FunctionInfo castFunction, Type elementType, ConnectorSession session, Slice array, Slice delimiter) + { + return arrayJoin(castFunction, elementType, session, array, delimiter, null); + } + + public static Slice arrayJoin(FunctionInfo castFunction, Type elementType, ConnectorSession session, Slice array, Slice delimiter, Slice nullReplacement) + { + Block arrayBlock = readStructuralBlock(array); + int numElements = arrayBlock.getPositionCount(); + DynamicSliceOutput sliceOutput = new DynamicSliceOutput(array.length()); + Class javaType = elementType.getJavaType(); + + Class[] parameters = null; + MethodHandle castFunctionHandle = null; + // can be null for the unknown type + if (castFunction != null) { + parameters = castFunction.getMethodHandle().type().parameterArray(); + castFunctionHandle = castFunction.getMethodHandle(); + } + + for (int i = 0; i < numElements; i++) { + if (arrayBlock.isNull(i)) { + if (nullReplacement != null) { + sliceOutput.appendBytes(nullReplacement); + } + else { + continue; + } + } + else { + if (javaType == boolean.class) { + sliceOutput.appendBytes(invokeCast(parameters, castFunctionHandle, session, elementType.getBoolean(arrayBlock, i))); + } + else if (javaType == double.class) { + sliceOutput.appendBytes(invokeCast(parameters, castFunctionHandle, session, elementType.getDouble(arrayBlock, i))); + } + else if (javaType == long.class) { + sliceOutput.appendBytes(invokeCast(parameters, castFunctionHandle, session, elementType.getLong(arrayBlock, i))); + } + else if (javaType == Slice.class) { + sliceOutput.appendBytes(invokeCast(parameters, castFunctionHandle, session, elementType.getSlice(arrayBlock, i))); + } + else { + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, format("Unexpected type %s", javaType.getName())); + } + } + + if (i != numElements - 1) { + sliceOutput.appendBytes(delimiter); + } + } + + return sliceOutput.slice(); + } + + private static Slice invokeCast(Class[] castFunctionParameters, MethodHandle castFunctionHandle, ConnectorSession session, Object arg) + { + Slice slice; + try { + if (castFunctionParameters[0] == ConnectorSession.class) { + slice = (Slice) castFunctionHandle.invokeWithArguments(session, arg); + } + else { + slice = (Slice) castFunctionHandle.invokeWithArguments(arg); + } + } + catch (Throwable throwable) { + throw new PrestoException(INTERNAL_ERROR, format("Error casting array element %s to VARCHAR", arg)); + } + return slice; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayLessThanOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayLessThanOperator.java new file mode 100644 index 00000000..b0307e47 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayLessThanOperator.java @@ -0,0 +1,97 @@ +package com.facebook.presto.operator.scalar; +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.ParametricOperator; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.spi.type.TypeSignature; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; + +import java.lang.invoke.MethodHandle; +import java.util.Map; + +import static com.facebook.presto.metadata.FunctionRegistry.operatorInfo; +import static com.facebook.presto.metadata.OperatorType.LESS_THAN; +import static com.facebook.presto.metadata.Signature.orderableTypeParameter; +import static com.facebook.presto.spi.StandardErrorCode.INTERNAL_ERROR; +import static com.facebook.presto.spi.type.TypeSignature.parseTypeSignature; +import static com.facebook.presto.type.ArrayType.ARRAY_NULL_ELEMENT_MSG; +import static com.facebook.presto.type.TypeUtils.castValue; +import static com.facebook.presto.type.TypeUtils.checkElementNotNull; +import static com.facebook.presto.type.TypeUtils.readStructuralBlock; +import static com.facebook.presto.util.Reflection.methodHandle; + +public class ArrayLessThanOperator + extends ParametricOperator +{ + public static final ArrayLessThanOperator ARRAY_LESS_THAN = new ArrayLessThanOperator(); + private static final TypeSignature RETURN_TYPE = parseTypeSignature(StandardTypes.BOOLEAN); + private static final MethodHandle METHOD_HANDLE = methodHandle(ArrayLessThanOperator.class, "lessThan", MethodHandle.class, Type.class, Slice.class, Slice.class); + + private ArrayLessThanOperator() + { + super(LESS_THAN, ImmutableList.of(orderableTypeParameter("T")), StandardTypes.BOOLEAN, ImmutableList.of("array", "array")); + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + Type elementType = types.get("T"); + Type type = typeManager.getParameterizedType(StandardTypes.ARRAY, ImmutableList.of(elementType.getTypeSignature()), ImmutableList.of()); + TypeSignature typeSignature = type.getTypeSignature(); + MethodHandle lessThanFunction = functionRegistry.resolveOperator(LESS_THAN, ImmutableList.of(elementType, elementType)).getMethodHandle(); + MethodHandle method = METHOD_HANDLE.bindTo(lessThanFunction).bindTo(elementType); + return operatorInfo(LESS_THAN, RETURN_TYPE, ImmutableList.of(typeSignature, typeSignature), method, false, ImmutableList.of(false, false)); + } + + public static boolean lessThan(MethodHandle lessThanFunction, Type type, Slice left, Slice right) + { + Block leftArray = readStructuralBlock(left); + Block rightArray = readStructuralBlock(right); + + int len = Math.min(leftArray.getPositionCount(), rightArray.getPositionCount()); + int index = 0; + while (index < len) { + checkElementNotNull(leftArray.isNull(index), ARRAY_NULL_ELEMENT_MSG); + checkElementNotNull(rightArray.isNull(index), ARRAY_NULL_ELEMENT_MSG); + Object leftElement = castValue(type, leftArray, index); + Object rightElement = castValue(type, rightArray, index); + try { + if ((boolean) lessThanFunction.invoke(leftElement, rightElement)) { + return true; + } + if ((boolean) lessThanFunction.invoke(rightElement, leftElement)) { + return false; + } + } + catch (Throwable t) { + Throwables.propagateIfInstanceOf(t, Error.class); + Throwables.propagateIfInstanceOf(t, PrestoException.class); + + throw new PrestoException(INTERNAL_ERROR, t); + } + index++; + } + + return leftArray.getPositionCount() < rightArray.getPositionCount(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayLessThanOrEqualOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayLessThanOrEqualOperator.java new file mode 100644 index 00000000..040d8c2a --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayLessThanOrEqualOperator.java @@ -0,0 +1,98 @@ +package com.facebook.presto.operator.scalar; +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.ParametricOperator; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.spi.type.TypeSignature; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; + +import java.lang.invoke.MethodHandle; +import java.util.Map; + +import static com.facebook.presto.metadata.FunctionRegistry.operatorInfo; +import static com.facebook.presto.metadata.OperatorType.LESS_THAN; +import static com.facebook.presto.metadata.OperatorType.LESS_THAN_OR_EQUAL; +import static com.facebook.presto.metadata.Signature.orderableTypeParameter; +import static com.facebook.presto.spi.StandardErrorCode.INTERNAL_ERROR; +import static com.facebook.presto.spi.type.TypeSignature.parseTypeSignature; +import static com.facebook.presto.type.ArrayType.ARRAY_NULL_ELEMENT_MSG; +import static com.facebook.presto.type.TypeUtils.castValue; +import static com.facebook.presto.type.TypeUtils.checkElementNotNull; +import static com.facebook.presto.type.TypeUtils.readStructuralBlock; +import static com.facebook.presto.util.Reflection.methodHandle; + +public class ArrayLessThanOrEqualOperator + extends ParametricOperator +{ + public static final ArrayLessThanOrEqualOperator ARRAY_LESS_THAN_OR_EQUAL = new ArrayLessThanOrEqualOperator(); + private static final TypeSignature RETURN_TYPE = parseTypeSignature(StandardTypes.BOOLEAN); + private static final MethodHandle METHOD_HANDLE = methodHandle(ArrayLessThanOrEqualOperator.class, "lessThanOrEqual", MethodHandle.class, Type.class, Slice.class, Slice.class); + + private ArrayLessThanOrEqualOperator() + { + super(LESS_THAN_OR_EQUAL, ImmutableList.of(orderableTypeParameter("T")), StandardTypes.BOOLEAN, ImmutableList.of("array", "array")); + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + Type elementType = types.get("T"); + Type type = typeManager.getParameterizedType(StandardTypes.ARRAY, ImmutableList.of(elementType.getTypeSignature()), ImmutableList.of()); + TypeSignature typeSignature = type.getTypeSignature(); + MethodHandle lessThanFunction = functionRegistry.resolveOperator(LESS_THAN, ImmutableList.of(elementType, elementType)).getMethodHandle(); + MethodHandle method = METHOD_HANDLE.bindTo(lessThanFunction).bindTo(elementType); + return operatorInfo(LESS_THAN_OR_EQUAL, RETURN_TYPE, ImmutableList.of(typeSignature, typeSignature), method, false, ImmutableList.of(false, false)); + } + + public static boolean lessThanOrEqual(MethodHandle lessThanFunction, Type type, Slice left, Slice right) + { + Block leftArray = readStructuralBlock(left); + Block rightArray = readStructuralBlock(right); + + int len = Math.min(leftArray.getPositionCount(), rightArray.getPositionCount()); + int index = 0; + while (index < len) { + checkElementNotNull(leftArray.isNull(index), ARRAY_NULL_ELEMENT_MSG); + checkElementNotNull(rightArray.isNull(index), ARRAY_NULL_ELEMENT_MSG); + Object leftElement = castValue(type, leftArray, index); + Object rightElement = castValue(type, rightArray, index); + try { + if ((boolean) lessThanFunction.invoke(leftElement, rightElement)) { + return true; + } + if ((boolean) lessThanFunction.invoke(rightElement, leftElement)) { + return false; + } + } + catch (Throwable t) { + Throwables.propagateIfInstanceOf(t, Error.class); + Throwables.propagateIfInstanceOf(t, PrestoException.class); + + throw new PrestoException(INTERNAL_ERROR, t); + } + index++; + } + + return leftArray.getPositionCount() <= rightArray.getPositionCount(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayNotEqualOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayNotEqualOperator.java new file mode 100644 index 00000000..7b32a5cf --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayNotEqualOperator.java @@ -0,0 +1,63 @@ +package com.facebook.presto.operator.scalar; +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.ParametricOperator; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.spi.type.TypeSignature; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; + +import java.lang.invoke.MethodHandle; +import java.util.Map; + +import static com.facebook.presto.metadata.FunctionRegistry.operatorInfo; +import static com.facebook.presto.metadata.OperatorType.EQUAL; +import static com.facebook.presto.metadata.OperatorType.NOT_EQUAL; +import static com.facebook.presto.metadata.Signature.comparableTypeParameter; +import static com.facebook.presto.spi.type.TypeSignature.parseTypeSignature; +import static com.facebook.presto.util.Reflection.methodHandle; + +public class ArrayNotEqualOperator + extends ParametricOperator +{ + public static final ArrayNotEqualOperator ARRAY_NOT_EQUAL = new ArrayNotEqualOperator(); + private static final TypeSignature RETURN_TYPE = parseTypeSignature(StandardTypes.BOOLEAN); + private static final MethodHandle METHOD_HANDLE = methodHandle(ArrayNotEqualOperator.class, "notEqual", MethodHandle.class, Type.class, Slice.class, Slice.class); + + private ArrayNotEqualOperator() + { + super(NOT_EQUAL, ImmutableList.of(comparableTypeParameter("T")), StandardTypes.BOOLEAN, ImmutableList.of("array", "array")); + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + Type elementType = types.get("T"); + Type type = typeManager.getParameterizedType(StandardTypes.ARRAY, ImmutableList.of(elementType.getTypeSignature()), ImmutableList.of()); + TypeSignature typeSignature = type.getTypeSignature(); + MethodHandle equalsFunction = functionRegistry.resolveOperator(EQUAL, ImmutableList.of(elementType, elementType)).getMethodHandle(); + MethodHandle method = METHOD_HANDLE.bindTo(equalsFunction).bindTo(elementType); + return operatorInfo(NOT_EQUAL, RETURN_TYPE, ImmutableList.of(typeSignature, typeSignature), method, false, ImmutableList.of(false, false)); + } + + public static boolean notEqual(MethodHandle equalsFunction, Type type, Slice left, Slice right) + { + return !ArrayEqualOperator.equals(equalsFunction, type, left, right); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayPositionFunction.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayPositionFunction.java new file mode 100644 index 00000000..9cdf8653 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayPositionFunction.java @@ -0,0 +1,193 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.OperatorType; +import com.facebook.presto.metadata.ParametricScalar; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; + +import java.lang.invoke.MethodHandle; +import java.util.Map; + +import static com.facebook.presto.metadata.Signature.comparableTypeParameter; +import static com.facebook.presto.spi.StandardErrorCode.INTERNAL_ERROR; +import static com.facebook.presto.spi.StandardErrorCode.INVALID_FUNCTION_ARGUMENT; +import static com.facebook.presto.spi.type.TypeSignature.parseTypeSignature; +import static com.facebook.presto.type.TypeUtils.parameterizedTypeName; +import static com.facebook.presto.type.TypeUtils.readStructuralBlock; +import static com.facebook.presto.util.Reflection.methodHandle; + +public final class ArrayPositionFunction + extends ParametricScalar +{ + public static final ArrayPositionFunction ARRAY_POSITION = new ArrayPositionFunction(); + private static final Signature SIGNATURE = new Signature("array_position", ImmutableList.of(comparableTypeParameter("E")), "bigint", ImmutableList.of("array", "E"), false, false); + private static final MethodHandle METHOD_HANDLE_BOOLEAN = methodHandle(ArrayPositionFunction.class, "arrayPosition", Type.class, MethodHandle.class, Slice.class, boolean.class); + private static final MethodHandle METHOD_HANDLE_LONG = methodHandle(ArrayPositionFunction.class, "arrayPosition", Type.class, MethodHandle.class, Slice.class, long.class); + private static final MethodHandle METHOD_HANDLE_DOUBLE = methodHandle(ArrayPositionFunction.class, "arrayPosition", Type.class, MethodHandle.class, Slice.class, double.class); + private static final MethodHandle METHOD_HANDLE_SLICE = methodHandle(ArrayPositionFunction.class, "arrayPosition", Type.class, MethodHandle.class, Slice.class, Slice.class); + + @Override + public Signature getSignature() + { + return SIGNATURE; + } + + @Override + public boolean isHidden() + { + return false; + } + + @Override + public boolean isDeterministic() + { + return true; + } + + @Override + public String getDescription() + { + return "Returns the position of the first occurrence of the given value in array (or 0 if not found)"; + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + Type type = types.get("E"); + MethodHandle equalMethodHandle = functionRegistry.resolveOperator(OperatorType.EQUAL, ImmutableList.of(type, type)).getMethodHandle(); + MethodHandle arrayPositionMethodHandle; + if (type.getJavaType() == boolean.class) { + arrayPositionMethodHandle = METHOD_HANDLE_BOOLEAN; + } + else if (type.getJavaType() == long.class) { + arrayPositionMethodHandle = METHOD_HANDLE_LONG; + } + else if (type.getJavaType() == double.class) { + arrayPositionMethodHandle = METHOD_HANDLE_DOUBLE; + } + else if (type.getJavaType() == Slice.class) { + arrayPositionMethodHandle = METHOD_HANDLE_SLICE; + } + else { + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, "Argument type to array_position unsupported"); + } + return new FunctionInfo( + new Signature("array_position", parseTypeSignature(StandardTypes.BIGINT), parameterizedTypeName("array", type.getTypeSignature()), type.getTypeSignature()), + getDescription(), + isHidden(), + arrayPositionMethodHandle.bindTo(type).bindTo(equalMethodHandle), + isDeterministic(), + false, + ImmutableList.of(false, false)); + } + + public static long arrayPosition(Type type, MethodHandle equalMethodHandle, Slice array, boolean element) + { + Block block = readStructuralBlock(array); + int size = block.getPositionCount(); + for (int i = 0; i < size; i++) { + if (!block.isNull(i)) { + boolean arrayValue = type.getBoolean(block, i); + try { + if ((boolean) equalMethodHandle.invokeExact(arrayValue, element)) { + return i + 1; // result is 1-based (instead of 0) + } + } + catch (Throwable t) { + Throwables.propagateIfInstanceOf(t, Error.class); + Throwables.propagateIfInstanceOf(t, PrestoException.class); + throw new PrestoException(INTERNAL_ERROR, t); + } + } + } + return 0; + } + + public static long arrayPosition(Type type, MethodHandle equalMethodHandle, Slice array, long element) + { + Block block = readStructuralBlock(array); + int size = block.getPositionCount(); + for (int i = 0; i < size; i++) { + if (!block.isNull(i)) { + long arrayValue = type.getLong(block, i); + try { + if ((boolean) equalMethodHandle.invokeExact(arrayValue, element)) { + return i + 1; // result is 1-based (instead of 0) + } + } + catch (Throwable t) { + Throwables.propagateIfInstanceOf(t, Error.class); + Throwables.propagateIfInstanceOf(t, PrestoException.class); + throw new PrestoException(INTERNAL_ERROR, t); + } + } + } + return 0; + } + + public static long arrayPosition(Type type, MethodHandle equalMethodHandle, Slice array, double element) + { + Block block = readStructuralBlock(array); + int size = block.getPositionCount(); + for (int i = 0; i < size; i++) { + if (!block.isNull(i)) { + double arrayValue = type.getDouble(block, i); + try { + if ((boolean) equalMethodHandle.invokeExact(arrayValue, element)) { + return i + 1; // result is 1-based (instead of 0) + } + } + catch (Throwable t) { + Throwables.propagateIfInstanceOf(t, Error.class); + Throwables.propagateIfInstanceOf(t, PrestoException.class); + throw new PrestoException(INTERNAL_ERROR, t); + } + } + } + return 0; + } + + public static long arrayPosition(Type type, MethodHandle equalMethodHandle, Slice array, Slice element) + { + Block block = readStructuralBlock(array); + int size = block.getPositionCount(); + for (int i = 0; i < size; i++) { + if (!block.isNull(i)) { + Slice arrayValue = type.getSlice(block, i); + try { + if ((boolean) equalMethodHandle.invokeExact(arrayValue, element)) { + return i + 1; // result is 1-based (instead of 0) + } + } + catch (Throwable t) { + Throwables.propagateIfInstanceOf(t, Error.class); + Throwables.propagateIfInstanceOf(t, PrestoException.class); + throw new PrestoException(INTERNAL_ERROR, t); + } + } + } + return 0; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayRemoveFunction.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayRemoveFunction.java new file mode 100644 index 00000000..f8917ce3 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayRemoveFunction.java @@ -0,0 +1,151 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.ParametricScalar; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.block.BlockBuilderStatus; +import com.facebook.presto.spi.block.VariableWidthBlockBuilder; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.spi.type.TypeSignature; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; + +import java.lang.invoke.MethodHandle; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static com.facebook.presto.metadata.OperatorType.EQUAL; +import static com.facebook.presto.metadata.Signature.comparableTypeParameter; +import static com.facebook.presto.spi.StandardErrorCode.INTERNAL_ERROR; +import static com.facebook.presto.type.TypeUtils.buildStructuralSlice; +import static com.facebook.presto.type.TypeUtils.castValue; +import static com.facebook.presto.type.TypeUtils.parameterizedTypeName; +import static com.facebook.presto.type.TypeUtils.readStructuralBlock; +import static com.facebook.presto.util.Reflection.methodHandle; +import static com.google.common.base.Preconditions.checkArgument; +import static java.lang.String.format; + +public final class ArrayRemoveFunction + extends ParametricScalar +{ + public static final ArrayRemoveFunction ARRAY_REMOVE_FUNCTION = new ArrayRemoveFunction(); + private static final String FUNCTION_NAME = "array_remove"; + private static final Signature SIGNATURE = new Signature(FUNCTION_NAME, ImmutableList.of(comparableTypeParameter("E")), "array", ImmutableList.of("array", "E"), false, false); + + @Override + public Signature getSignature() + { + return SIGNATURE; + } + + @Override + public boolean isHidden() + { + return false; + } + + @Override + public boolean isDeterministic() + { + return true; + } + + @Override + public String getDescription() + { + return "Remove specified values from the given array"; + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + checkArgument(types.size() == 1, format("%s expects only one argument", FUNCTION_NAME)); + Type type = types.get("E"); + TypeSignature valueType = type.getTypeSignature(); + TypeSignature arrayType = parameterizedTypeName(StandardTypes.ARRAY, valueType); + + MethodHandle equalsFunction = functionRegistry.resolveOperator(EQUAL, ImmutableList.of(type, type)).getMethodHandle(); + MethodHandle baseMethodHandle = methodHandle(ArrayRemoveFunction.class, "remove", MethodHandle.class, Type.class, Slice.class, type.getJavaType()); + MethodHandle methodHandle = baseMethodHandle.bindTo(equalsFunction).bindTo(type); + Signature signature = new Signature(FUNCTION_NAME, arrayType, arrayType, valueType); + return new FunctionInfo(signature, getDescription(), isHidden(), methodHandle, isDeterministic(), false, ImmutableList.of(false, false)); + } + + public static Slice remove(MethodHandle equalsFunction, Type type, Slice array, Slice value) + { + return remove(equalsFunction, type, array, (Object) value); + } + + public static Slice remove(MethodHandle equalsFunction, Type type, Slice array, long value) + { + return remove(equalsFunction, type, array, (Object) value); + } + + public static Slice remove(MethodHandle equalsFunction, Type type, Slice array, double value) + { + return remove(equalsFunction, type, array, (Object) value); + } + + public static Slice remove(MethodHandle equalsFunction, Type type, Slice array, boolean value) + { + return remove(equalsFunction, type, array, (Object) value); + } + + private static Slice remove(MethodHandle equalsFunction, Type type, Slice array, Object value) + { + Block elementsBlock = readStructuralBlock(array); + + int sizeAfterRemove = 0; + List positions = new ArrayList<>(); + + for (int i = 0; i < elementsBlock.getPositionCount(); i++) { + Object element = castValue(type, elementsBlock, i); + + try { + if (element == null || !(boolean) equalsFunction.invoke(element, value)) { + positions.add(i); + sizeAfterRemove += elementsBlock.getLength(i); + } + } + catch (Throwable t) { + Throwables.propagateIfInstanceOf(t, Error.class); + Throwables.propagateIfInstanceOf(t, PrestoException.class); + + throw new PrestoException(INTERNAL_ERROR, t); + } + } + + if (elementsBlock.getPositionCount() == positions.size()) { + return array; + } + + BlockBuilder blockBuilder = new VariableWidthBlockBuilder(new BlockBuilderStatus(), sizeAfterRemove); + + for (int position : positions) { + type.appendTo(elementsBlock, position, blockBuilder); + } + + return buildStructuralSlice(blockBuilder); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArraySortFunction.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArraySortFunction.java new file mode 100644 index 00000000..3e04514a --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArraySortFunction.java @@ -0,0 +1,115 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.ParametricScalar; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.block.BlockBuilderStatus; +import com.facebook.presto.spi.block.VariableWidthBlockBuilder; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.google.common.collect.ImmutableList; +import com.google.common.primitives.Ints; +import io.airlift.slice.Slice; + +import java.lang.invoke.MethodHandle; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; + +import static com.facebook.presto.metadata.Signature.orderableTypeParameter; +import static com.facebook.presto.type.TypeUtils.readStructuralBlock; +import static com.facebook.presto.type.TypeUtils.buildStructuralSlice; +import static com.facebook.presto.type.TypeUtils.parameterizedTypeName; +import static com.facebook.presto.util.Reflection.methodHandle; +import static com.google.common.base.Preconditions.checkArgument; +import static java.lang.String.format; + +public final class ArraySortFunction + extends ParametricScalar +{ + public static final ArraySortFunction ARRAY_SORT_FUNCTION = new ArraySortFunction(); + private static final String FUNCTION_NAME = "array_sort"; + private static final Signature SIGNATURE = new Signature(FUNCTION_NAME, ImmutableList.of(orderableTypeParameter("E")), "array", ImmutableList.of("array"), false, false); + private static final MethodHandle METHOD_HANDLE = methodHandle(ArraySortFunction.class, "sort", Type.class, Slice.class); + + @Override + public Signature getSignature() + { + return SIGNATURE; + } + + @Override + public boolean isHidden() + { + return false; + } + + @Override + public boolean isDeterministic() + { + return true; + } + + @Override + public String getDescription() + { + return "Sorts the given array in ascending order according to the natural ordering of its elements."; + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + checkArgument(types.size() == 1, format("%s expects only one argument", FUNCTION_NAME)); + Type type = types.get("E"); + MethodHandle methodHandle = METHOD_HANDLE.bindTo(type); + Signature signature = new Signature(FUNCTION_NAME, + parameterizedTypeName("array", type.getTypeSignature()), + parameterizedTypeName("array", type.getTypeSignature())); + return new FunctionInfo(signature, getDescription(), isHidden(), methodHandle, isDeterministic(), false, ImmutableList.of(false)); + } + + public static Slice sort(Type type, Slice encodedArray) + { + Block block = readStructuralBlock(encodedArray); + + List positions = Ints.asList(new int[block.getPositionCount()]); + for (int i = 0; i < block.getPositionCount(); i++) { + positions.set(i, i); + } + + Collections.sort(positions, new Comparator() + { + @Override + public int compare(Integer p1, Integer p2) + { + //TODO: This could be quite slow, it should use parametric equals + return type.compareTo(block, p1, block, p2); + } + }); + + BlockBuilder blockBuilder = new VariableWidthBlockBuilder(new BlockBuilderStatus(), block.getSizeInBytes()); + + for (int position : positions) { + type.appendTo(block, position, blockBuilder); + } + + return buildStructuralSlice(blockBuilder); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArraySubscriptOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArraySubscriptOperator.java new file mode 100644 index 00000000..115c63ab --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArraySubscriptOperator.java @@ -0,0 +1,133 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.ParametricOperator; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.primitives.Ints; +import io.airlift.slice.Slice; + +import java.lang.invoke.MethodHandle; +import java.util.Map; + +import static com.facebook.presto.metadata.OperatorType.SUBSCRIPT; +import static com.facebook.presto.metadata.Signature.typeParameter; +import static com.facebook.presto.spi.StandardErrorCode.INVALID_FUNCTION_ARGUMENT; +import static com.facebook.presto.type.TypeUtils.readStructuralBlock; +import static com.facebook.presto.spi.type.TypeSignature.parseTypeSignature; +import static com.facebook.presto.type.TypeUtils.parameterizedTypeName; +import static com.facebook.presto.util.Reflection.methodHandle; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class ArraySubscriptOperator + extends ParametricOperator +{ + public static final ArraySubscriptOperator ARRAY_SUBSCRIPT = new ArraySubscriptOperator(); + + private static final Map, MethodHandle> METHOD_HANDLES = ImmutableMap., MethodHandle>builder() + .put(boolean.class, methodHandle(ArraySubscriptOperator.class, "booleanSubscript", Type.class, Slice.class, long.class)) + .put(long.class, methodHandle(ArraySubscriptOperator.class, "longSubscript", Type.class, Slice.class, long.class)) + .put(void.class, methodHandle(ArraySubscriptOperator.class, "arrayWithUnknownType", Type.class, Slice.class, long.class)) + .put(double.class, methodHandle(ArraySubscriptOperator.class, "doubleSubscript", Type.class, Slice.class, long.class)) + .put(Slice.class, methodHandle(ArraySubscriptOperator.class, "sliceSubscript", Type.class, Slice.class, long.class)) + .build(); + + protected ArraySubscriptOperator() + { + super(SUBSCRIPT, ImmutableList.of(typeParameter("E")), "E", ImmutableList.of("array", "bigint")); + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + checkArgument(types.size() == 1, "Expected one type, got %s", types); + Type elementType = types.get("E"); + + MethodHandle methodHandle = METHOD_HANDLES.get(elementType.getJavaType()); + methodHandle = methodHandle.bindTo(elementType); + checkNotNull(methodHandle, "methodHandle is null"); + return new FunctionInfo(Signature.internalOperator(SUBSCRIPT.name(), elementType.getTypeSignature(), parameterizedTypeName("array", elementType.getTypeSignature()), parseTypeSignature(StandardTypes.BIGINT)), "Array subscript", true, methodHandle, true, true, ImmutableList.of(false, false)); + } + + public static void arrayWithUnknownType(Type elementType, Slice array, long index) + { + readBlockAndCheckIndex(array, index); + } + + public static Long longSubscript(Type elementType, Slice array, long index) + { + Block block = readBlockAndCheckIndex(array, index); + int position = Ints.checkedCast(index - 1); + if (block.isNull(position)) { + return null; + } + + return elementType.getLong(block, position); + } + + public static Boolean booleanSubscript(Type elementType, Slice array, long index) + { + Block block = readBlockAndCheckIndex(array, index); + int position = Ints.checkedCast(index - 1); + if (block.isNull(position)) { + return null; + } + + return elementType.getBoolean(block, position); + } + + public static Double doubleSubscript(Type elementType, Slice array, long index) + { + Block block = readBlockAndCheckIndex(array, index); + int position = Ints.checkedCast(index - 1); + if (block.isNull(position)) { + return null; + } + + return elementType.getDouble(block, position); + } + + public static Slice sliceSubscript(Type elementType, Slice array, long index) + { + Block block = readBlockAndCheckIndex(array, index); + int position = Ints.checkedCast(index - 1); + if (block.isNull(position)) { + return null; + } + + return elementType.getSlice(block, position); + } + + public static Block readBlockAndCheckIndex(Slice array, long index) + { + if (index <= 0) { + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, "Index out of bounds"); + } + Block block = readStructuralBlock(array); + if (index > block.getPositionCount()) { + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, "Index out of bounds"); + } + return block; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayToArrayCast.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayToArrayCast.java new file mode 100644 index 00000000..057c409a --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayToArrayCast.java @@ -0,0 +1,131 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import com.facebook.presto.byteCode.ClassDefinition; +import com.facebook.presto.byteCode.MethodDefinition; +import com.facebook.presto.byteCode.Parameter; +import com.facebook.presto.byteCode.Scope; +import com.facebook.presto.byteCode.Variable; +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.ParametricOperator; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.sql.gen.ArrayGeneratorUtils; +import com.facebook.presto.sql.gen.ArrayMapByteCodeExpression; +import com.facebook.presto.sql.gen.CallSiteBinder; +import com.facebook.presto.sql.gen.CompilerUtils; +import com.facebook.presto.type.ArrayType; +import com.facebook.presto.type.TypeUtils; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; + +import java.lang.invoke.MethodHandle; +import java.util.Map; + +import static com.facebook.presto.byteCode.Access.FINAL; +import static com.facebook.presto.byteCode.Access.PRIVATE; +import static com.facebook.presto.byteCode.Access.PUBLIC; +import static com.facebook.presto.byteCode.Access.STATIC; +import static com.facebook.presto.byteCode.Access.a; +import static com.facebook.presto.byteCode.Parameter.arg; +import static com.facebook.presto.byteCode.ParameterizedType.type; +import static com.facebook.presto.byteCode.expression.ByteCodeExpressions.constantBoolean; +import static com.facebook.presto.byteCode.expression.ByteCodeExpressions.invokeStatic; +import static com.facebook.presto.metadata.FunctionRegistry.operatorInfo; +import static com.facebook.presto.metadata.OperatorType.CAST; +import static com.facebook.presto.metadata.Signature.internalOperator; +import static com.facebook.presto.metadata.Signature.typeParameter; +import static com.facebook.presto.spi.StandardErrorCode.FUNCTION_NOT_FOUND; +import static com.facebook.presto.sql.gen.CompilerUtils.defineClass; +import static com.facebook.presto.util.Reflection.methodHandle; +import static com.google.common.base.Preconditions.checkArgument; +import static java.lang.String.format; + +public class ArrayToArrayCast + extends ParametricOperator +{ + public static final ArrayToArrayCast ARRAY_TO_ARRAY_CAST = new ArrayToArrayCast(); + + private ArrayToArrayCast() + { + super(CAST, ImmutableList.of(typeParameter("F"), typeParameter("T")), "array", ImmutableList.of("array")); + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + checkArgument(arity == 1, "Expected arity to be 1"); + Type fromType = types.get("F"); + Type toType = types.get("T"); + + ArrayType fromArrayType = (ArrayType) typeManager.getParameterizedType(StandardTypes.ARRAY, ImmutableList.of(fromType.getTypeSignature()), ImmutableList.of()); + ArrayType toArrayType = (ArrayType) typeManager.getParameterizedType(StandardTypes.ARRAY, ImmutableList.of(toType.getTypeSignature()), ImmutableList.of()); + + FunctionInfo functionInfo = functionRegistry.getExactFunction(internalOperator(CAST.name(), toType.getTypeSignature(), ImmutableList.of(fromType.getTypeSignature()))); + if (functionInfo == null) { + throw new PrestoException(FUNCTION_NOT_FOUND, format("Can not cast %s to %s", fromArrayType, toArrayType)); + } + + Class castOperatorClass = generateArrayCast(typeManager, functionInfo); + MethodHandle methodHandle = methodHandle(castOperatorClass, "castArray", ConnectorSession.class, Slice.class); + + return operatorInfo(CAST, toArrayType.getTypeSignature(), ImmutableList.of(fromArrayType.getTypeSignature()), methodHandle, false, ImmutableList.of(false)); + } + + private static Class generateArrayCast(TypeManager typeManager, FunctionInfo elementCast) + { + CallSiteBinder binder = new CallSiteBinder(); + + ClassDefinition definition = new ClassDefinition( + a(PUBLIC, FINAL), + CompilerUtils.makeClassName(Joiner.on("$").join("ArrayCast", elementCast.getArgumentTypes().get(0), elementCast.getReturnType())), + type(Object.class)); + + definition.declareDefaultConstructor(a(PRIVATE)); + + Parameter session = arg("session", ConnectorSession.class); + Parameter value = arg("value", Slice.class); + + MethodDefinition method = definition.declareMethod( + a(PUBLIC, STATIC), + "castArray", + type(Slice.class), + session, + value); + + Scope scope = method.getScope(); + com.facebook.presto.byteCode.Block body = method.getBody(); + + Variable wasNull = scope.declareVariable(boolean.class, "wasNull"); + body.append(wasNull.set(constantBoolean(false))); + + Variable array = scope.declareVariable(Block.class, "array"); + body.append(array.set(invokeStatic(TypeUtils.class, "readStructuralBlock", Block.class, value))); + + // cast map elements + ArrayMapByteCodeExpression newArray = ArrayGeneratorUtils.map(scope, binder, typeManager, array, elementCast); + + // convert block to slice + body.append(invokeStatic(TypeUtils.class, "buildStructuralSlice", Slice.class, newArray).ret()); + + return defineClass(definition, Object.class, binder.getBindings(), ArrayToArrayCast.class.getClassLoader()); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayToElementConcatFunction.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayToElementConcatFunction.java new file mode 100644 index 00000000..4771b067 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayToElementConcatFunction.java @@ -0,0 +1,76 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.ParametricScalar; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.spi.type.TypeSignature; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; + +import java.lang.invoke.MethodHandle; +import java.util.Map; + +import static com.facebook.presto.metadata.Signature.typeParameter; +import static com.facebook.presto.type.TypeUtils.parameterizedTypeName; +import static com.facebook.presto.util.Reflection.methodHandle; + +public class ArrayToElementConcatFunction + extends ParametricScalar +{ + public static final ArrayToElementConcatFunction ARRAY_TO_ELEMENT_CONCAT_FUNCTION = new ArrayToElementConcatFunction(); + private static final String FUNCTION_NAME = "concat"; + private static final Signature SIGNATURE = new Signature(FUNCTION_NAME, ImmutableList.of(typeParameter("E")), "array", ImmutableList.of("array", "E"), false, false); + + @Override + public Signature getSignature() + { + return SIGNATURE; + } + + @Override + public boolean isHidden() + { + return false; + } + + @Override + public boolean isDeterministic() + { + return true; + } + + @Override + public String getDescription() + { + return "Concatenates an array to an element"; + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + Type type = types.get("E"); + MethodHandle methodHandle = methodHandle(ArrayConcatUtils.class, "appendElement", Type.class, Slice.class, type.getJavaType()); + methodHandle = methodHandle.bindTo(type); + + TypeSignature typeSignature = type.getTypeSignature(); + TypeSignature returnType = parameterizedTypeName("array", typeSignature); + Signature signature = new Signature(FUNCTION_NAME, returnType, returnType, typeSignature); + return new FunctionInfo(signature, getDescription(), isHidden(), methodHandle, isDeterministic(), false, ImmutableList.of(false, false)); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayToJsonCast.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayToJsonCast.java new file mode 100644 index 00000000..ee7ef4ea --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ArrayToJsonCast.java @@ -0,0 +1,80 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.OperatorType; +import com.facebook.presto.metadata.ParametricOperator; +import com.facebook.presto.server.SliceSerializer; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import io.airlift.json.ObjectMapperProvider; +import io.airlift.slice.Slice; +import io.airlift.slice.Slices; + +import java.lang.invoke.MethodHandle; +import java.util.Map; + +import static com.facebook.presto.metadata.FunctionRegistry.operatorInfo; +import static com.facebook.presto.metadata.Signature.typeParameter; +import static com.facebook.presto.spi.type.TypeSignature.parseTypeSignature; +import static com.facebook.presto.type.TypeUtils.createBlock; +import static com.facebook.presto.util.Reflection.methodHandle; +import static com.google.common.base.Preconditions.checkArgument; + +public class ArrayToJsonCast + extends ParametricOperator +{ + public static final ArrayToJsonCast ARRAY_TO_JSON = new ArrayToJsonCast(); + private static final Supplier OBJECT_MAPPER = Suppliers.memoize(() -> new ObjectMapperProvider().get().registerModule(new SimpleModule().addSerializer(Slice.class, new SliceSerializer()))); + private static final MethodHandle METHOD_HANDLE = methodHandle(ArrayToJsonCast.class, "toJson", Type.class, ConnectorSession.class, Slice.class); + + private ArrayToJsonCast() + { + super(OperatorType.CAST, ImmutableList.of(typeParameter("T")), StandardTypes.JSON, ImmutableList.of("array")); + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + checkArgument(arity == 1, "Expected arity to be 1"); + Type type = types.get("T"); + Type arrayType = typeManager.getParameterizedType(StandardTypes.ARRAY, ImmutableList.of(type.getTypeSignature()), ImmutableList.of()); + + MethodHandle methodHandle = METHOD_HANDLE.bindTo(arrayType); + + return operatorInfo(OperatorType.CAST, parseTypeSignature(StandardTypes.JSON), ImmutableList.of(arrayType.getTypeSignature()), methodHandle, false, ImmutableList.of(false)); + } + + public static Slice toJson(Type arrayType, ConnectorSession session, Slice array) + { + Object object = arrayType.getObjectValue(session, createBlock(arrayType, array), 0); + try { + return Slices.utf8Slice(OBJECT_MAPPER.get().writeValueAsString(object)); + } + catch (JsonProcessingException e) { + throw Throwables.propagate(e); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/ColorFunctions.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ColorFunctions.java new file mode 100644 index 00000000..da5ee0a5 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ColorFunctions.java @@ -0,0 +1,316 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.type.ColorType; +import com.facebook.presto.type.SqlType; +import com.google.common.annotations.VisibleForTesting; +import io.airlift.slice.Slice; +import io.airlift.slice.Slices; + +import java.awt.Color; + +import static com.facebook.presto.operator.scalar.StringFunctions.upper; +import static com.facebook.presto.spi.StandardErrorCode.INTERNAL_ERROR; +import static com.facebook.presto.spi.StandardErrorCode.INVALID_FUNCTION_ARGUMENT; +import static com.facebook.presto.util.Failures.checkCondition; +import static java.lang.String.format; +import static java.nio.charset.StandardCharsets.UTF_8; + +public final class ColorFunctions +{ + private static final String ANSI_RESET = "\u001b[0m"; + + private static final Slice RENDERED_TRUE = render(Slices.copiedBuffer("\u2713", UTF_8), color(Slices.copiedBuffer("green", UTF_8))); + private static final Slice RENDERED_FALSE = render(Slices.copiedBuffer("\u2717", UTF_8), color(Slices.copiedBuffer("red", UTF_8))); + + public enum SystemColor + { + BLACK(0, "black"), + RED(1, "red"), + GREEN(2, "green"), + YELLOW(3, "yellow"), + BLUE(4, "blue"), + MAGENTA(5, "magenta"), + CYAN(6, "cyan"), + WHITE(7, "white"); + + private final int index; + private final String name; + + SystemColor(int index, String name) + { + this.index = index; + this.name = name; + } + + private int getIndex() + { + return index; + } + + public String getName() + { + return name; + } + + public static SystemColor valueOf(int index) + { + for (SystemColor color : values()) { + if (index == color.getIndex()) { + return color; + } + } + throw new PrestoException(INTERNAL_ERROR, "Invalid color index: " + index); + } + } + + private ColorFunctions() {} + + @ScalarFunction + @SqlType(ColorType.NAME) + public static long color(@SqlType(StandardTypes.VARCHAR) Slice color) + { + int rgb = parseRgb(color); + + if (rgb != -1) { + return rgb; + } + + // encode system colors (0-15) as negative values, offset by one + try { + SystemColor systemColor = SystemColor.valueOf(upper(color).toString(UTF_8)); + int index = systemColor.getIndex(); + return -(index + 1); + } + catch (IllegalArgumentException e) { + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, format("Invalid color: '%s'", color.toString(UTF_8)), e); + } + } + + @ScalarFunction + @SqlType(ColorType.NAME) + public static long rgb(@SqlType(StandardTypes.BIGINT) long red, @SqlType(StandardTypes.BIGINT) long green, @SqlType(StandardTypes.BIGINT) long blue) + { + checkCondition(red >= 0 && red <= 255, INVALID_FUNCTION_ARGUMENT, "red must be between 0 and 255"); + checkCondition(green >= 0 && green <= 255, INVALID_FUNCTION_ARGUMENT, "green must be between 0 and 255"); + checkCondition(blue >= 0 && blue <= 255, INVALID_FUNCTION_ARGUMENT, "blue must be between 0 and 255"); + + return (red << 16) | (green << 8) | blue; + } + + /** + * Interpolate a color between lowColor and highColor based the provided value + *

+ * The value is truncated to the range [low, high] if it's outside. + * Color must be a valid rgb value of the form #rgb + */ + @ScalarFunction + @SqlType(ColorType.NAME) + public static long color( + @SqlType(StandardTypes.DOUBLE) double value, + @SqlType(StandardTypes.DOUBLE) double low, + @SqlType(StandardTypes.DOUBLE) double high, + @SqlType(ColorType.NAME) long lowColor, + @SqlType(ColorType.NAME) long highColor) + { + return color((value - low) * 1.0 / (high - low), lowColor, highColor); + } + + /** + * Interpolate a color between lowColor and highColor based on the provided value + *

+ * The value is truncated to the range [0, 1] if necessary + * Color must be a valid rgb value of the form #rgb + */ + @ScalarFunction + @SqlType(ColorType.NAME) + public static long color(@SqlType(StandardTypes.DOUBLE) double fraction, @SqlType(ColorType.NAME) long lowColor, @SqlType(ColorType.NAME) long highColor) + { + checkCondition(lowColor >= 0, INVALID_FUNCTION_ARGUMENT, "lowColor not a valid RGB color"); + checkCondition(highColor >= 0, INVALID_FUNCTION_ARGUMENT, "highColor not a valid RGB color"); + + fraction = Math.min(1, fraction); + fraction = Math.max(0, fraction); + + return interpolate((float) fraction, lowColor, highColor); + } + + @ScalarFunction + @SqlType(StandardTypes.VARCHAR) + public static Slice render(@SqlType(StandardTypes.VARCHAR) Slice value, @SqlType(ColorType.NAME) long color) + { + StringBuilder builder = new StringBuilder(value.length()); + + // color + builder.append(ansiColorEscape(color)) + .append(value.toString(UTF_8)) + .append(ANSI_RESET); + + return Slices.copiedBuffer(builder.toString(), UTF_8); + } + + @ScalarFunction + @SqlType(StandardTypes.VARCHAR) + public static Slice render(@SqlType(StandardTypes.BIGINT) long value, @SqlType(ColorType.NAME) long color) + { + return render(Slices.copiedBuffer(Long.toString(value), UTF_8), color); + } + + @ScalarFunction + @SqlType(StandardTypes.VARCHAR) + public static Slice render(@SqlType(StandardTypes.DOUBLE) double value, @SqlType(ColorType.NAME) long color) + { + return render(Slices.copiedBuffer(Double.toString(value), UTF_8), color); + } + + @ScalarFunction + @SqlType(StandardTypes.VARCHAR) + public static Slice render(@SqlType(StandardTypes.BOOLEAN) boolean value) + { + return value ? RENDERED_TRUE : RENDERED_FALSE; + } + + @ScalarFunction + @SqlType(StandardTypes.VARCHAR) + public static Slice bar(@SqlType(StandardTypes.DOUBLE) double percent, @SqlType(StandardTypes.BIGINT) long width) + { + return bar(percent, width, rgb(255, 0, 0), rgb(0, 255, 0)); + } + + @ScalarFunction + @SqlType(StandardTypes.VARCHAR) + public static Slice bar( + @SqlType(StandardTypes.DOUBLE) double percent, + @SqlType(StandardTypes.BIGINT) long width, + @SqlType(ColorType.NAME) long lowColor, + @SqlType(ColorType.NAME) long highColor) + { + long count = (int) (percent * width); + count = Math.min(width, count); + count = Math.max(0, count); + + StringBuilder builder = new StringBuilder(); + + for (int i = 0; i < count; i++) { + float fraction = (float) (i * 1.0 / (width - 1)); + + int color = interpolate(fraction, lowColor, highColor); + + builder.append(ansiColorEscape(color)) + .append('\u2588'); + } + // reset + builder.append(ANSI_RESET); + + // pad to force column to be the requested width + for (long i = count; i < width; ++i) { + builder.append(' '); + } + + return Slices.copiedBuffer(builder.toString(), UTF_8); + } + + private static int interpolate(float fraction, long lowRgb, long highRgb) + { + float[] lowHsv = Color.RGBtoHSB(getRed(lowRgb), getGreen(lowRgb), getBlue(lowRgb), null); + float[] highHsv = Color.RGBtoHSB(getRed(highRgb), getGreen(highRgb), getBlue(highRgb), null); + + float h = fraction * (highHsv[0] - lowHsv[0]) + lowHsv[0]; + float s = fraction * (highHsv[1] - lowHsv[1]) + lowHsv[1]; + float v = fraction * (highHsv[2] - lowHsv[2]) + lowHsv[2]; + + return Color.HSBtoRGB(h, s, v) & 0xFF_FF_FF; + } + + /** + * Convert the given color (rgb or system) to an ansi-compatible index (for use with ESC[38;5;m) + */ + private static int toAnsi(int red, int green, int blue) + { + // rescale to 0-5 range + red = red * 6 / 256; + green = green * 6 / 256; + blue = blue * 6 / 256; + + return 16 + red * 36 + green * 6 + blue; + } + + private static String ansiColorEscape(long color) + { + return "\u001b[38;5;" + toAnsi(color) + 'm'; + } + + /** + * Convert the given color (rgb or system) to an ansi-compatible index (for use with ESC[38;5;m) + */ + private static int toAnsi(long color) + { + if (color >= 0) { // an rgb value encoded as in Color.getRGB + return toAnsi(getRed(color), getGreen(color), getBlue(color)); + } + else { + return (int) (-color - 1); + } + } + + @VisibleForTesting + static int parseRgb(Slice color) + { + if (color.length() != 4 || color.getByte(0) != '#') { + return -1; + } + + int red = Character.digit((char) color.getByte(1), 16); + int green = Character.digit((char) color.getByte(2), 16); + int blue = Character.digit((char) color.getByte(3), 16); + + if (red == -1 || green == -1 || blue == -1) { + return -1; + } + + // replicate the nibbles to turn a color of the form #rgb => #rrggbb (css semantics) + red = (red << 4) | red; + green = (green << 4) | green; + blue = (blue << 4) | blue; + + return (int) rgb(red, green, blue); + } + + @VisibleForTesting + static int getRed(long color) + { + checkCondition(color >= 0, INVALID_FUNCTION_ARGUMENT, "color is not a valid rgb value"); + + return (int) ((color >>> 16) & 0xff); + } + + @VisibleForTesting + static int getGreen(long color) + { + checkCondition(color >= 0, INVALID_FUNCTION_ARGUMENT, "color is not a valid rgb value"); + + return (int) ((color >>> 8) & 0xff); + } + + @VisibleForTesting + static int getBlue(long color) + { + checkCondition(color >= 0, INVALID_FUNCTION_ARGUMENT, "color is not a valid rgb value"); + + return (int) (color & 0xff); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/CombineHashFunction.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/CombineHashFunction.java new file mode 100644 index 00000000..a8b34cd3 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/CombineHashFunction.java @@ -0,0 +1,29 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.type.SqlType; + +public final class CombineHashFunction +{ + private CombineHashFunction() {} + + @ScalarFunction(value = "combine_hash", hidden = true) + @SqlType(StandardTypes.BIGINT) + public static long getHash(@SqlType(StandardTypes.BIGINT) long previousHashValue, @SqlType(StandardTypes.BIGINT) long value) + { + return 31 * previousHashValue + value; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/DateTimeFunctions.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/DateTimeFunctions.java new file mode 100644 index 00000000..b23cc108 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/DateTimeFunctions.java @@ -0,0 +1,982 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import com.facebook.presto.operator.Description; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.type.SqlType; +import com.facebook.presto.util.DateTimeZoneIndex; +import com.facebook.presto.util.ThreadLocalCache; +import com.google.common.primitives.Ints; +import io.airlift.slice.Slice; +import io.airlift.slice.Slices; +import org.joda.time.DateTime; +import org.joda.time.DateTimeField; +import org.joda.time.chrono.ISOChronology; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; +import org.joda.time.format.DateTimeFormatterBuilder; + +import java.util.Locale; + +import static com.facebook.presto.operator.scalar.QuarterOfYearDateTimeField.QUARTER_OF_YEAR; +import static com.facebook.presto.spi.StandardErrorCode.INVALID_FUNCTION_ARGUMENT; +import static com.facebook.presto.spi.type.DateTimeEncoding.packDateTimeWithZone; +import static com.facebook.presto.spi.type.DateTimeEncoding.unpackMillisUtc; +import static com.facebook.presto.spi.type.DateTimeEncoding.updateMillisUtc; +import static com.facebook.presto.spi.type.TimeZoneKey.getTimeZoneKeyForOffset; +import static com.facebook.presto.type.DateTimeOperators.modulo24Hour; +import static com.facebook.presto.util.DateTimeZoneIndex.extractZoneOffsetMinutes; +import static com.facebook.presto.util.DateTimeZoneIndex.getChronology; +import static com.facebook.presto.util.DateTimeZoneIndex.unpackChronology; +import static com.facebook.presto.util.Failures.checkCondition; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Locale.ENGLISH; +import static java.util.concurrent.TimeUnit.DAYS; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.joda.time.DateTimeZone.UTC; + +public final class DateTimeFunctions +{ + private static final ThreadLocalCache DATETIME_FORMATTER_CACHE = new ThreadLocalCache(100) + { + @Override + protected DateTimeFormatter load(Slice format) + { + return createDateTimeFormatter(format); + } + }; + + private static final ISOChronology UTC_CHRONOLOGY = ISOChronology.getInstance(UTC); + private static final DateTimeField SECOND_OF_MINUTE = UTC_CHRONOLOGY.secondOfMinute(); + private static final DateTimeField DAY_OF_WEEK = UTC_CHRONOLOGY.dayOfWeek(); + private static final DateTimeField DAY_OF_MONTH = UTC_CHRONOLOGY.dayOfMonth(); + private static final DateTimeField DAY_OF_YEAR = UTC_CHRONOLOGY.dayOfYear(); + private static final DateTimeField WEEK_OF_YEAR = UTC_CHRONOLOGY.weekOfWeekyear(); + private static final DateTimeField YEAR_OF_WEEK = UTC_CHRONOLOGY.weekyear(); + private static final DateTimeField MONTH_OF_YEAR = UTC_CHRONOLOGY.monthOfYear(); + private static final DateTimeField QUARTER = QUARTER_OF_YEAR.getField(UTC_CHRONOLOGY); + private static final DateTimeField YEAR = UTC_CHRONOLOGY.year(); + private static final int MILLISECONDS_IN_SECOND = 1000; + private static final int MILLISECONDS_IN_MINUTE = 60 * MILLISECONDS_IN_SECOND; + private static final int MILLISECONDS_IN_HOUR = 60 * MILLISECONDS_IN_MINUTE; + private static final int MILLISECONDS_IN_DAY = 24 * MILLISECONDS_IN_HOUR; + + private DateTimeFunctions() {} + + @Description("current date") + @ScalarFunction + @SqlType(StandardTypes.DATE) + public static long currentDate(ConnectorSession session) + { + long millis = getChronology(session.getTimeZoneKey()).dayOfMonth().roundFloor(session.getStartTime()); + return MILLISECONDS.toDays(millis); + } + + @Description("current time with time zone") + @ScalarFunction + @SqlType(StandardTypes.TIME_WITH_TIME_ZONE) + public static long currentTime(ConnectorSession session) + { + // Stack value is number of milliseconds from start of the current day, + // but the start of the day is relative to the current time zone. + long millis = getChronology(session.getTimeZoneKey()).millisOfDay().get(session.getStartTime()); + return packDateTimeWithZone(millis, session.getTimeZoneKey()); + } + + @Description("current time without time zone") + @ScalarFunction("localtime") + @SqlType(StandardTypes.TIME) + public static long localTime(ConnectorSession session) + { + // Stack value is number of milliseconds from start of the current day, + // but the start of the day is relative to the current time zone. + return getChronology(session.getTimeZoneKey()).millisOfDay().get(session.getStartTime()); + } + + @Description("current time zone") + @ScalarFunction("current_timezone") + @SqlType(StandardTypes.VARCHAR) + public static Slice currentTimeZone(ConnectorSession session) + { + return Slices.copiedBuffer(session.getTimeZoneKey().getId(), UTF_8); + } + + @Description("current timestamp with time zone") + @ScalarFunction(value = "current_timestamp", alias = "now") + @SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) + public static long currentTimestamp(ConnectorSession session) + { + return packDateTimeWithZone(session.getStartTime(), session.getTimeZoneKey()); + } + + @Description("current timestamp without time zone") + @ScalarFunction("localtimestamp") + @SqlType(StandardTypes.TIMESTAMP) + public static long localTimestamp(ConnectorSession session) + { + return session.getStartTime(); + } + + @ScalarFunction("from_unixtime") + @SqlType(StandardTypes.TIMESTAMP) + public static long fromUnixTime(@SqlType(StandardTypes.DOUBLE) double unixTime) + { + return Math.round(unixTime * 1000); + } + + @ScalarFunction("from_unixtime") + @SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) + public static long fromUnixTime(@SqlType(StandardTypes.DOUBLE) double unixTime, @SqlType(StandardTypes.BIGINT) long hoursOffset, @SqlType(StandardTypes.BIGINT) long minutesOffset) + { + return packDateTimeWithZone(Math.round(unixTime * 1000), (int) (hoursOffset * 60 + minutesOffset)); + } + + @ScalarFunction("to_unixtime") + @SqlType(StandardTypes.DOUBLE) + public static double toUnixTime(@SqlType(StandardTypes.TIMESTAMP) long timestamp) + { + return timestamp / 1000.0; + } + + @ScalarFunction("to_unixtime") + @SqlType(StandardTypes.DOUBLE) + public static double toUnixTimeFromTimestampWithTimeZone(@SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long timestampWithTimeZone) + { + return unpackMillisUtc(timestampWithTimeZone) / 1000.0; + } + + @ScalarFunction("at_timezone") + @SqlType(StandardTypes.TIME_WITH_TIME_ZONE) + public static long timeAtTimeZone(@SqlType(StandardTypes.TIME_WITH_TIME_ZONE) long timeWithTimeZone, @SqlType(StandardTypes.VARCHAR) Slice zoneId) + { + return packDateTimeWithZone(unpackMillisUtc(timeWithTimeZone), zoneId.toStringUtf8()); + } + + @ScalarFunction("at_timezone") + @SqlType(StandardTypes.TIME_WITH_TIME_ZONE) + public static long timeAtTimeZone(@SqlType(StandardTypes.TIME_WITH_TIME_ZONE) long timeWithTimeZone, @SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long zoneOffset) + { + checkCondition((zoneOffset % 60_000) == 0, INVALID_FUNCTION_ARGUMENT, "Invalid time zone offset interval: interval contains seconds"); + int zoneOffsetMinutes = (int) (zoneOffset / 60_000); + return packDateTimeWithZone(unpackMillisUtc(timeWithTimeZone), getTimeZoneKeyForOffset(zoneOffsetMinutes)); + } + + @ScalarFunction("at_timezone") + @SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) + public static long timestampAtTimeZone(@SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long timestampWithTimeZone, @SqlType(StandardTypes.VARCHAR) Slice zoneId) + { + return packDateTimeWithZone(unpackMillisUtc(timestampWithTimeZone), zoneId.toStringUtf8()); + } + + @ScalarFunction("at_timezone") + @SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) + public static long timestampAtTimeZone(@SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long timestampWithTimeZone, @SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long zoneOffset) + { + checkCondition((zoneOffset % 60_000) == 0, INVALID_FUNCTION_ARGUMENT, "Invalid time zone offset interval: interval contains seconds"); + int zoneOffsetMinutes = (int) (zoneOffset / 60_000); + return packDateTimeWithZone(unpackMillisUtc(timestampWithTimeZone), getTimeZoneKeyForOffset(zoneOffsetMinutes)); + } + + @Description("truncate to the specified precision in the session timezone") + @ScalarFunction("date_trunc") + @SqlType(StandardTypes.DATE) + public static long truncateDate(ConnectorSession session, @SqlType(StandardTypes.VARCHAR) Slice unit, @SqlType(StandardTypes.DATE) long date) + { + long millis = getDateField(UTC_CHRONOLOGY, unit).roundFloor(DAYS.toMillis(date)); + return MILLISECONDS.toDays(millis); + } + + @Description("truncate to the specified precision in the session timezone") + @ScalarFunction("date_trunc") + @SqlType(StandardTypes.TIME) + public static long truncateTime(ConnectorSession session, @SqlType(StandardTypes.VARCHAR) Slice unit, @SqlType(StandardTypes.TIME) long time) + { + return getTimeField(getChronology(session.getTimeZoneKey()), unit).roundFloor(time); + } + + @Description("truncate to the specified precision") + @ScalarFunction("date_trunc") + @SqlType(StandardTypes.TIME_WITH_TIME_ZONE) + public static long truncateTimeWithTimeZone(@SqlType(StandardTypes.VARCHAR) Slice unit, @SqlType(StandardTypes.TIME_WITH_TIME_ZONE) long timeWithTimeZone) + { + long millis = getTimeField(unpackChronology(timeWithTimeZone), unit).roundFloor(unpackMillisUtc(timeWithTimeZone)); + return updateMillisUtc(millis, timeWithTimeZone); + } + + @Description("truncate to the specified precision in the session timezone") + @ScalarFunction("date_trunc") + @SqlType(StandardTypes.TIMESTAMP) + public static long truncateTimestamp(ConnectorSession session, @SqlType(StandardTypes.VARCHAR) Slice unit, @SqlType(StandardTypes.TIMESTAMP) long timestamp) + { + return getTimestampField(getChronology(session.getTimeZoneKey()), unit).roundFloor(timestamp); + } + + @Description("truncate to the specified precision") + @ScalarFunction("date_trunc") + @SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) + public static long truncateTimestampWithTimezone(@SqlType(StandardTypes.VARCHAR) Slice unit, @SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long timestampWithTimeZone) + { + long millis = getTimestampField(unpackChronology(timestampWithTimeZone), unit).roundFloor(unpackMillisUtc(timestampWithTimeZone)); + return updateMillisUtc(millis, timestampWithTimeZone); + } + + @Description("add the specified amount of date to the given date") + @ScalarFunction("date_add") + @SqlType(StandardTypes.DATE) + public static long addFieldValueDate(ConnectorSession session, @SqlType(StandardTypes.VARCHAR) Slice unit, @SqlType(StandardTypes.BIGINT) long value, @SqlType(StandardTypes.DATE) long date) + { + long millis = getDateField(UTC_CHRONOLOGY, unit).add(DAYS.toMillis(date), Ints.checkedCast(value)); + return MILLISECONDS.toDays(millis); + } + + @Description("add the specified amount of time to the given time") + @ScalarFunction("date_add") + @SqlType(StandardTypes.TIME) + public static long addFieldValueTime(ConnectorSession session, @SqlType(StandardTypes.VARCHAR) Slice unit, @SqlType(StandardTypes.BIGINT) long value, @SqlType(StandardTypes.TIME) long time) + { + ISOChronology chronology = getChronology(session.getTimeZoneKey()); + return modulo24Hour(chronology, getTimeField(chronology, unit).add(time, Ints.checkedCast(value))); + } + + @Description("add the specified amount of time to the given time") + @ScalarFunction("date_add") + @SqlType(StandardTypes.TIME_WITH_TIME_ZONE) + public static long addFieldValueTimeWithTimeZone( + @SqlType(StandardTypes.VARCHAR) Slice unit, + @SqlType(StandardTypes.BIGINT) long value, + @SqlType(StandardTypes.TIME_WITH_TIME_ZONE) long timeWithTimeZone) + { + ISOChronology chronology = unpackChronology(timeWithTimeZone); + long millis = modulo24Hour(chronology, getTimeField(chronology, unit).add(unpackMillisUtc(timeWithTimeZone), Ints.checkedCast(value))); + return updateMillisUtc(millis, timeWithTimeZone); + } + + @Description("add the specified amount of time to the given timestamp") + @ScalarFunction("date_add") + @SqlType(StandardTypes.TIMESTAMP) + public static long addFieldValueTimestamp( + ConnectorSession session, + @SqlType(StandardTypes.VARCHAR) Slice unit, + @SqlType(StandardTypes.BIGINT) long value, + @SqlType(StandardTypes.TIMESTAMP) long timestamp) + { + return getTimestampField(getChronology(session.getTimeZoneKey()), unit).add(timestamp, Ints.checkedCast(value)); + } + + @Description("add the specified amount of time to the given timestamp") + @ScalarFunction("date_add") + @SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) + public static long addFieldValueTimestampWithTimeZone( + @SqlType(StandardTypes.VARCHAR) Slice unit, + @SqlType(StandardTypes.BIGINT) long value, + @SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long timestampWithTimeZone) + { + long millis = getTimestampField(unpackChronology(timestampWithTimeZone), unit).add(unpackMillisUtc(timestampWithTimeZone), Ints.checkedCast(value)); + return updateMillisUtc(millis, timestampWithTimeZone); + } + + @Description("difference of the given dates in the given unit") + @ScalarFunction("date_diff") + @SqlType(StandardTypes.BIGINT) + public static long diffDate(ConnectorSession session, @SqlType(StandardTypes.VARCHAR) Slice unit, @SqlType(StandardTypes.DATE) long date1, @SqlType(StandardTypes.DATE) long date2) + { + return getDateField(UTC_CHRONOLOGY, unit).getDifferenceAsLong(DAYS.toMillis(date2), DAYS.toMillis(date1)); + } + + @Description("difference of the given times in the given unit") + @ScalarFunction("date_diff") + @SqlType(StandardTypes.BIGINT) + public static long diffTime(ConnectorSession session, @SqlType(StandardTypes.VARCHAR) Slice unit, @SqlType(StandardTypes.TIME) long time1, @SqlType(StandardTypes.TIME) long time2) + { + ISOChronology chronology = getChronology(session.getTimeZoneKey()); + return getTimeField(chronology, unit).getDifferenceAsLong(time2, time1); + } + + @Description("difference of the given times in the given unit") + @ScalarFunction("date_diff") + @SqlType(StandardTypes.BIGINT) + public static long diffTimeWithTimeZone( + @SqlType(StandardTypes.VARCHAR) Slice unit, + @SqlType(StandardTypes.TIME_WITH_TIME_ZONE) long timeWithTimeZone1, + @SqlType(StandardTypes.TIME_WITH_TIME_ZONE) long timeWithTimeZone2) + { + return getTimeField(unpackChronology(timeWithTimeZone1), unit).getDifferenceAsLong(unpackMillisUtc(timeWithTimeZone2), unpackMillisUtc(timeWithTimeZone1)); + } + + @Description("difference of the given times in the given unit") + @ScalarFunction("date_diff") + @SqlType(StandardTypes.BIGINT) + public static long diffTimestamp( + ConnectorSession session, + @SqlType(StandardTypes.VARCHAR) Slice unit, + @SqlType(StandardTypes.TIMESTAMP) long timestamp1, + @SqlType(StandardTypes.TIMESTAMP) long timestamp2) + { + return getTimestampField(getChronology(session.getTimeZoneKey()), unit).getDifferenceAsLong(timestamp2, timestamp1); + } + + @Description("difference of the given times in the given unit") + @ScalarFunction("date_diff") + @SqlType(StandardTypes.BIGINT) + public static long diffTimestampWithTimeZone( + @SqlType(StandardTypes.VARCHAR) Slice unit, + @SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long timestampWithTimeZone1, + @SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long timestampWithTimeZone2) + { + return getTimestampField(unpackChronology(timestampWithTimeZone1), unit).getDifferenceAsLong(unpackMillisUtc(timestampWithTimeZone2), unpackMillisUtc(timestampWithTimeZone1)); + } + + private static DateTimeField getDateField(ISOChronology chronology, Slice unit) + { + String unitString = unit.toString(UTF_8).toLowerCase(ENGLISH); + switch (unitString) { + case "day": + return chronology.dayOfMonth(); + case "week": + return chronology.weekOfWeekyear(); + case "month": + return chronology.monthOfYear(); + case "quarter": + return QUARTER_OF_YEAR.getField(chronology); + case "year": + return chronology.year(); + } + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, "'" + unitString + "' is not a valid DATE field"); + } + + private static DateTimeField getTimeField(ISOChronology chronology, Slice unit) + { + String unitString = unit.toString(UTF_8).toLowerCase(ENGLISH); + switch (unitString) { + case "millisecond": + return chronology.millisOfSecond(); + case "second": + return chronology.secondOfMinute(); + case "minute": + return chronology.minuteOfHour(); + case "hour": + return chronology.hourOfDay(); + } + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, "'" + unitString + "' is not a valid Time field"); + } + + private static DateTimeField getTimestampField(ISOChronology chronology, Slice unit) + { + String unitString = unit.toString(UTF_8).toLowerCase(ENGLISH); + switch (unitString) { + case "millisecond": + return chronology.millisOfSecond(); + case "second": + return chronology.secondOfMinute(); + case "minute": + return chronology.minuteOfHour(); + case "hour": + return chronology.hourOfDay(); + case "day": + return chronology.dayOfMonth(); + case "week": + return chronology.weekOfWeekyear(); + case "month": + return chronology.monthOfYear(); + case "quarter": + return QUARTER_OF_YEAR.getField(chronology); + case "year": + return chronology.year(); + } + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, "'" + unitString + "' is not a valid Timestamp field"); + } + + @Description("parses the specified date/time by the given format") + @ScalarFunction + @SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) + public static long parseDatetime(ConnectorSession session, @SqlType(StandardTypes.VARCHAR) Slice datetime, @SqlType(StandardTypes.VARCHAR) Slice formatString) + { + String pattern = formatString.toString(UTF_8); + DateTimeFormatter formatter = DateTimeFormat.forPattern(pattern) + .withChronology(getChronology(session.getTimeZoneKey())) + .withOffsetParsed() + .withLocale(session.getLocale()); + + String datetimeString = datetime.toString(UTF_8); + return DateTimeZoneIndex.packDateTimeWithZone(parseDateTimeHelper(formatter, datetimeString)); + } + + private static DateTime parseDateTimeHelper(DateTimeFormatter formatter, String datetimeString) + { + try { + return formatter.parseDateTime(datetimeString); + } + catch (IllegalArgumentException e) { + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, e); + } + } + + @Description("formats the given time by the given format") + @ScalarFunction + @SqlType(StandardTypes.VARCHAR) + public static Slice formatDatetime(ConnectorSession session, @SqlType(StandardTypes.TIMESTAMP) long timestamp, @SqlType(StandardTypes.VARCHAR) Slice formatString) + { + return formatDatetime(getChronology(session.getTimeZoneKey()), session.getLocale(), timestamp, formatString); + } + + @Description("formats the given time by the given format") + @ScalarFunction("format_datetime") + @SqlType(StandardTypes.VARCHAR) + public static Slice formatDatetimeWithTimeZone( + ConnectorSession session, + @SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long timestampWithTimeZone, + @SqlType(StandardTypes.VARCHAR) Slice formatString) + { + return formatDatetime(unpackChronology(timestampWithTimeZone), session.getLocale(), unpackMillisUtc(timestampWithTimeZone), formatString); + } + + private static Slice formatDatetime(ISOChronology chronology, Locale locale, long timestamp, Slice formatString) + { + String pattern = formatString.toString(UTF_8); + DateTimeFormatter formatter = DateTimeFormat.forPattern(pattern) + .withChronology(chronology) + .withLocale(locale); + + String datetimeString = formatter.print(timestamp); + return Slices.wrappedBuffer(datetimeString.getBytes(UTF_8)); + } + + @ScalarFunction + @SqlType(StandardTypes.VARCHAR) + public static Slice dateFormat(ConnectorSession session, @SqlType(StandardTypes.TIMESTAMP) long timestamp, @SqlType(StandardTypes.VARCHAR) Slice formatString) + { + return dateFormat(getChronology(session.getTimeZoneKey()), session.getLocale(), timestamp, formatString); + } + + @ScalarFunction("date_format") + @SqlType(StandardTypes.VARCHAR) + public static Slice dateFormatWithTimeZone( + ConnectorSession session, + @SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long timestampWithTimeZone, + @SqlType(StandardTypes.VARCHAR) Slice formatString) + { + return dateFormat(unpackChronology(timestampWithTimeZone), session.getLocale(), unpackMillisUtc(timestampWithTimeZone), formatString); + } + + private static Slice dateFormat(ISOChronology chronology, Locale locale, long timestamp, Slice formatString) + { + DateTimeFormatter formatter = DATETIME_FORMATTER_CACHE.get(formatString) + .withChronology(chronology) + .withLocale(locale); + + return Slices.copiedBuffer(formatter.print(timestamp), UTF_8); + } + + @ScalarFunction + @SqlType(StandardTypes.TIMESTAMP) + public static long dateParse(ConnectorSession session, @SqlType(StandardTypes.VARCHAR) Slice dateTime, @SqlType(StandardTypes.VARCHAR) Slice formatString) + { + DateTimeFormatter formatter = DATETIME_FORMATTER_CACHE.get(formatString) + .withChronology(getChronology(session.getTimeZoneKey())) + .withLocale(session.getLocale()); + + try { + return formatter.parseMillis(dateTime.toString(UTF_8)); + } + catch (IllegalArgumentException e) { + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, e); + } + } + + @Description("second of the minute of the given timestamp") + @ScalarFunction("second") + @SqlType(StandardTypes.BIGINT) + public static long secondFromTimestamp(@SqlType(StandardTypes.TIMESTAMP) long timestamp) + { + // Time is effectively UTC so no need for a custom chronology + return SECOND_OF_MINUTE.get(timestamp); + } + + @Description("second of the minute of the given timestamp") + @ScalarFunction("second") + @SqlType(StandardTypes.BIGINT) + public static long secondFromTimestampWithTimeZone(@SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long timestampWithTimeZone) + { + // Time is effectively UTC so no need for a custom chronology + return SECOND_OF_MINUTE.get(unpackMillisUtc(timestampWithTimeZone)); + } + + @Description("second of the minute of the given time") + @ScalarFunction("second") + @SqlType(StandardTypes.BIGINT) + public static long secondFromTime(@SqlType(StandardTypes.TIME) long time) + { + // Time is effectively UTC so no need for a custom chronology + return SECOND_OF_MINUTE.get(time); + } + + @Description("second of the minute of the given time") + @ScalarFunction("second") + @SqlType(StandardTypes.BIGINT) + public static long secondFromTimeWithTimeZone(@SqlType(StandardTypes.TIME_WITH_TIME_ZONE) long time) + { + // Time is effectively UTC so no need for a custom chronology + return SECOND_OF_MINUTE.get(unpackMillisUtc(time)); + } + + @Description("second of the minute of the given interval") + @ScalarFunction("second") + @SqlType(StandardTypes.BIGINT) + public static long secondFromInterval(@SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long milliseconds) + { + return (milliseconds % MILLISECONDS_IN_MINUTE) / MILLISECONDS_IN_SECOND; + } + + @Description("minute of the hour of the given timestamp") + @ScalarFunction("minute") + @SqlType(StandardTypes.BIGINT) + public static long minuteFromTimestamp(ConnectorSession session, @SqlType(StandardTypes.TIMESTAMP) long timestamp) + { + return getChronology(session.getTimeZoneKey()).minuteOfHour().get(timestamp); + } + + @Description("minute of the hour of the given timestamp") + @ScalarFunction("minute") + @SqlType(StandardTypes.BIGINT) + public static long minuteFromTimestampWithTimeZone(@SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long timestampWithTimeZone) + { + return unpackChronology(timestampWithTimeZone).minuteOfHour().get(unpackMillisUtc(timestampWithTimeZone)); + } + + @Description("minute of the hour of the given time") + @ScalarFunction("minute") + @SqlType(StandardTypes.BIGINT) + public static long minuteFromTime(ConnectorSession session, @SqlType(StandardTypes.TIME) long time) + { + return getChronology(session.getTimeZoneKey()).minuteOfHour().get(time); + } + + @Description("minute of the hour of the given time") + @ScalarFunction("minute") + @SqlType(StandardTypes.BIGINT) + public static long minuteFromTimeWithTimeZone(@SqlType(StandardTypes.TIME_WITH_TIME_ZONE) long timeWithTimeZone) + { + return unpackChronology(timeWithTimeZone).minuteOfHour().get(unpackMillisUtc(timeWithTimeZone)); + } + + @Description("minute of the hour of the given interval") + @ScalarFunction("minute") + @SqlType(StandardTypes.BIGINT) + public static long minuteFromInterval(@SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long milliseconds) + { + return (milliseconds % MILLISECONDS_IN_HOUR) / MILLISECONDS_IN_MINUTE; + } + + @Description("hour of the day of the given timestamp") + @ScalarFunction("hour") + @SqlType(StandardTypes.BIGINT) + public static long hourFromTimestamp(ConnectorSession session, @SqlType(StandardTypes.TIMESTAMP) long timestamp) + { + return getChronology(session.getTimeZoneKey()).hourOfDay().get(timestamp); + } + + @Description("hour of the day of the given timestamp") + @ScalarFunction("hour") + @SqlType(StandardTypes.BIGINT) + public static long hourFromTimestampWithTimeZone(@SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long timestampWithTimeZone) + { + return unpackChronology(timestampWithTimeZone).hourOfDay().get(unpackMillisUtc(timestampWithTimeZone)); + } + + @Description("hour of the day of the given time") + @ScalarFunction("hour") + @SqlType(StandardTypes.BIGINT) + public static long hourFromTime(ConnectorSession session, @SqlType(StandardTypes.TIME) long time) + { + return getChronology(session.getTimeZoneKey()).hourOfDay().get(time); + } + + @Description("hour of the day of the given time") + @ScalarFunction("hour") + @SqlType(StandardTypes.BIGINT) + public static long hourFromTimeWithTimeZone(@SqlType(StandardTypes.TIME_WITH_TIME_ZONE) long timeWithTimeZone) + { + return unpackChronology(timeWithTimeZone).hourOfDay().get(unpackMillisUtc(timeWithTimeZone)); + } + + @Description("hour of the day of the given interval") + @ScalarFunction("hour") + @SqlType(StandardTypes.BIGINT) + public static long hourFromInterval(@SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long milliseconds) + { + return (milliseconds % MILLISECONDS_IN_DAY) / MILLISECONDS_IN_HOUR; + } + + @Description("day of the week of the given timestamp") + @ScalarFunction(value = "day_of_week", alias = "dow") + @SqlType(StandardTypes.BIGINT) + public static long dayOfWeekFromTimestamp(ConnectorSession session, @SqlType(StandardTypes.TIMESTAMP) long timestamp) + { + return getChronology(session.getTimeZoneKey()).dayOfWeek().get(timestamp); + } + + @Description("day of the week of the given timestamp") + @ScalarFunction(value = "day_of_week", alias = "dow") + @SqlType(StandardTypes.BIGINT) + public static long dayOfWeekFromTimestampWithTimeZone(@SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long timestampWithTimeZone) + { + return unpackChronology(timestampWithTimeZone).dayOfWeek().get(unpackMillisUtc(timestampWithTimeZone)); + } + + @Description("day of the week of the given date") + @ScalarFunction(value = "day_of_week", alias = "dow") + @SqlType(StandardTypes.BIGINT) + public static long dayOfWeekFromDate(@SqlType(StandardTypes.DATE) long date) + { + return DAY_OF_WEEK.get(DAYS.toMillis(date)); + } + + @Description("day of the month of the given timestamp") + @ScalarFunction(value = "day", alias = "day_of_month") + @SqlType(StandardTypes.BIGINT) + public static long dayFromTimestamp(ConnectorSession session, @SqlType(StandardTypes.TIMESTAMP) long timestamp) + { + return getChronology(session.getTimeZoneKey()).dayOfMonth().get(timestamp); + } + + @Description("day of the month of the given timestamp") + @ScalarFunction(value = "day", alias = "day_of_month") + @SqlType(StandardTypes.BIGINT) + public static long dayFromTimestampWithTimeZone(@SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long timestampWithTimeZone) + { + return unpackChronology(timestampWithTimeZone).dayOfMonth().get(unpackMillisUtc(timestampWithTimeZone)); + } + + @Description("day of the month of the given date") + @ScalarFunction(value = "day", alias = "day_of_month") + @SqlType(StandardTypes.BIGINT) + public static long dayFromDate(@SqlType(StandardTypes.DATE) long date) + { + return DAY_OF_MONTH.get(DAYS.toMillis(date)); + } + + @Description("day of the month of the given interval") + @ScalarFunction(value = "day", alias = "day_of_month") + @SqlType(StandardTypes.BIGINT) + public static long dayFromInterval(@SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long milliseconds) + { + return milliseconds / MILLISECONDS_IN_DAY; + } + + @Description("day of the year of the given timestamp") + @ScalarFunction(value = "day_of_year", alias = "doy") + @SqlType(StandardTypes.BIGINT) + public static long dayOfYearFromTimestamp(ConnectorSession session, @SqlType(StandardTypes.TIMESTAMP) long timestamp) + { + return getChronology(session.getTimeZoneKey()).dayOfYear().get(timestamp); + } + + @Description("day of the year of the given timestamp") + @ScalarFunction(value = "day_of_year", alias = "doy") + @SqlType(StandardTypes.BIGINT) + public static long dayOfYearFromTimestampWithTimeZone(@SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long timestampWithTimeZone) + { + return unpackChronology(timestampWithTimeZone).dayOfYear().get(unpackMillisUtc(timestampWithTimeZone)); + } + + @Description("day of the year of the given date") + @ScalarFunction(value = "day_of_year", alias = "doy") + @SqlType(StandardTypes.BIGINT) + public static long dayOfYearFromDate(@SqlType(StandardTypes.DATE) long date) + { + return DAY_OF_YEAR.get(DAYS.toMillis(date)); + } + + @Description("week of the year of the given timestamp") + @ScalarFunction(value = "week", alias = "week_of_year") + @SqlType(StandardTypes.BIGINT) + public static long weekFromTimestamp(ConnectorSession session, @SqlType(StandardTypes.TIMESTAMP) long timestamp) + { + return getChronology(session.getTimeZoneKey()).weekOfWeekyear().get(timestamp); + } + + @Description("week of the year of the given timestamp") + @ScalarFunction(value = "week", alias = "week_of_year") + @SqlType(StandardTypes.BIGINT) + public static long weekFromTimestampWithTimeZone(@SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long timestampWithTimeZone) + { + return unpackChronology(timestampWithTimeZone).weekOfWeekyear().get(unpackMillisUtc(timestampWithTimeZone)); + } + + @Description("week of the year of the given date") + @ScalarFunction(value = "week", alias = "week_of_year") + @SqlType(StandardTypes.BIGINT) + public static long weekFromDate(@SqlType(StandardTypes.DATE) long date) + { + return WEEK_OF_YEAR.get(DAYS.toMillis(date)); + } + + @Description("year of the ISO week of the given timestamp") + @ScalarFunction(value = "year_of_week", alias = "yow") + @SqlType(StandardTypes.BIGINT) + public static long yearOfWeekFromTimestamp(ConnectorSession session, @SqlType(StandardTypes.TIMESTAMP) long timestamp) + { + return getChronology(session.getTimeZoneKey()).weekyear().get(timestamp); + } + + @Description("year of the ISO week of the given timestamp") + @ScalarFunction(value = "year_of_week", alias = "yow") + @SqlType(StandardTypes.BIGINT) + public static long yearOfWeekFromTimestampWithTimeZone(@SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long timestampWithTimeZone) + { + return unpackChronology(timestampWithTimeZone).weekyear().get(unpackMillisUtc(timestampWithTimeZone)); + } + + @Description("year of the ISO week of the given date") + @ScalarFunction(value = "year_of_week", alias = "yow") + @SqlType(StandardTypes.BIGINT) + public static long yearOfWeekFromDate(@SqlType(StandardTypes.DATE) long date) + { + return YEAR_OF_WEEK.get(DAYS.toMillis(date)); + } + + @Description("month of the year of the given timestamp") + @ScalarFunction("month") + @SqlType(StandardTypes.BIGINT) + public static long monthFromTimestamp(ConnectorSession session, @SqlType(StandardTypes.TIMESTAMP) long timestamp) + { + return getChronology(session.getTimeZoneKey()).monthOfYear().get(timestamp); + } + + @Description("month of the year of the given timestamp") + @ScalarFunction("month") + @SqlType(StandardTypes.BIGINT) + public static long monthFromTimestampWithTimeZone(@SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long timestampWithTimeZone) + { + return unpackChronology(timestampWithTimeZone).monthOfYear().get(unpackMillisUtc(timestampWithTimeZone)); + } + + @Description("month of the year of the given date") + @ScalarFunction("month") + @SqlType(StandardTypes.BIGINT) + public static long monthFromDate(@SqlType(StandardTypes.DATE) long date) + { + return MONTH_OF_YEAR.get(DAYS.toMillis(date)); + } + + @Description("month of the year of the given interval") + @ScalarFunction("month") + @SqlType(StandardTypes.BIGINT) + public static long monthFromInterval(@SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) long months) + { + return months % 12; + } + + @Description("quarter of the year of the given timestamp") + @ScalarFunction("quarter") + @SqlType(StandardTypes.BIGINT) + public static long quarterFromTimestamp(ConnectorSession session, @SqlType(StandardTypes.TIMESTAMP) long timestamp) + { + return QUARTER_OF_YEAR.getField(getChronology(session.getTimeZoneKey())).get(timestamp); + } + + @Description("quarter of the year of the given timestamp") + @ScalarFunction("quarter") + @SqlType(StandardTypes.BIGINT) + public static long quarterFromTimestampWithTimeZone(@SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long timestampWithTimeZone) + { + return QUARTER_OF_YEAR.getField(unpackChronology(timestampWithTimeZone)).get(unpackMillisUtc(timestampWithTimeZone)); + } + + @Description("quarter of the year of the given date") + @ScalarFunction("quarter") + @SqlType(StandardTypes.BIGINT) + public static long quarterFromDate(@SqlType(StandardTypes.DATE) long date) + { + return QUARTER.get(DAYS.toMillis(date)); + } + + @Description("year of the given timestamp") + @ScalarFunction("year") + @SqlType(StandardTypes.BIGINT) + public static long yearFromTimestamp(ConnectorSession session, @SqlType(StandardTypes.TIMESTAMP) long timestamp) + { + return getChronology(session.getTimeZoneKey()).year().get(timestamp); + } + + @Description("year of the given timestamp") + @ScalarFunction("year") + @SqlType(StandardTypes.BIGINT) + public static long yearFromTimestampWithTimeZone(@SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long timestampWithTimeZone) + { + return unpackChronology(timestampWithTimeZone).year().get(unpackMillisUtc(timestampWithTimeZone)); + } + + @Description("year of the given date") + @ScalarFunction("year") + @SqlType(StandardTypes.BIGINT) + public static long yearFromDate(@SqlType(StandardTypes.DATE) long date) + { + return YEAR.get(DAYS.toMillis(date)); + } + + @Description("year of the given interval") + @ScalarFunction("year") + @SqlType(StandardTypes.BIGINT) + public static long yearFromInterval(@SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) long months) + { + return months / 12; + } + + @Description("time zone minute of the given timestamp") + @ScalarFunction("timezone_minute") + @SqlType(StandardTypes.BIGINT) + public static long timeZoneMinuteFromTimestampWithTimeZone(@SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long timestampWithTimeZone) + { + return extractZoneOffsetMinutes(timestampWithTimeZone) % 60; + } + + @Description("time zone hour of the given timestamp") + @ScalarFunction("timezone_hour") + @SqlType(StandardTypes.BIGINT) + public static long timeZoneHourFromTimestampWithTimeZone(@SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long timestampWithTimeZone) + { + return extractZoneOffsetMinutes(timestampWithTimeZone) / 60; + } + + @SuppressWarnings("fallthrough") + public static DateTimeFormatter createDateTimeFormatter(Slice format) + { + DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder(); + + String formatString = format.toString(UTF_8); + boolean escaped = false; + for (int i = 0; i < format.length(); i++) { + char character = formatString.charAt(i); + + if (escaped) { + switch (character) { + case 'a': // %a Abbreviated weekday name (Sun..Sat) + builder.appendDayOfWeekShortText(); + break; + case 'b': // %b Abbreviated month name (Jan..Dec) + builder.appendMonthOfYearShortText(); + break; + case 'c': // %c Month, numeric (0..12) + builder.appendMonthOfYear(1); + break; + case 'd': // %d Day of the month, numeric (00..31) + builder.appendDayOfMonth(2); + break; + case 'e': // %e Day of the month, numeric (0..31) + builder.appendDayOfMonth(1); + break; + case 'f': // %f Microseconds (000000..999999) + builder.appendFractionOfSecond(6, 6); + break; + case 'H': // %H Hour (00..23) + builder.appendHourOfDay(2); + break; + case 'h': // %h Hour (01..12) + case 'I': // %I Hour (01..12) + builder.appendClockhourOfHalfday(2); + break; + case 'i': // %i Minutes, numeric (00..59) + builder.appendMinuteOfHour(2); + break; + case 'j': // %j Day of year (001..366) + builder.appendDayOfYear(3); + break; + case 'k': // %k Hour (0..23) + builder.appendClockhourOfDay(1); + break; + case 'l': // %l Hour (1..12) + builder.appendClockhourOfHalfday(1); + break; + case 'M': // %M Month name (January..December) + builder.appendMonthOfYearText(); + break; + case 'm': // %m Month, numeric (00..12) + builder.appendMonthOfYear(2); + break; + case 'p': // %p AM or PM + builder.appendHalfdayOfDayText(); + break; + case 'r': // %r Time, 12-hour (hh:mm:ss followed by AM or PM) + builder.appendClockhourOfHalfday(2) + .appendLiteral(':') + .appendMinuteOfHour(2) + .appendLiteral(':') + .appendSecondOfMinute(2) + .appendLiteral(' ') + .appendHalfdayOfDayText(); + break; + case 'S': // %S Seconds (00..59) + case 's': // %s Seconds (00..59) + builder.appendSecondOfMinute(2); + break; + case 'T': // %T Time, 24-hour (hh:mm:ss) + builder.appendHourOfDay(2) + .appendLiteral(':') + .appendMinuteOfHour(2) + .appendLiteral(':') + .appendSecondOfMinute(2); + break; + case 'v': // %v Week (01..53), where Monday is the first day of the week; used with %x + builder.appendWeekOfWeekyear(2); + break; + case 'x': // %x Year for the week, where Monday is the first day of the week, numeric, four digits; used with %v + builder.appendWeekyear(4, 4); + break; + case 'W': // %W Weekday name (Sunday..Saturday) + builder.appendDayOfWeekText(); + break; + case 'w': // %w Day of the week (0=Sunday..6=Saturday) + builder.appendDayOfWeek(1); + break; + case 'Y': // %Y Year, numeric, four digits + builder.appendYear(4, 4); + break; + case 'y': // %y Year, numeric (two digits) + builder.appendYearOfCentury(2, 2); + break; + case 'U': // %U Week (00..53), where Sunday is the first day of the week + case 'u': // %u Week (00..53), where Monday is the first day of the week + case 'V': // %V Week (01..53), where Sunday is the first day of the week; used with %X + case 'X': // %X Year for the week where Sunday is the first day of the week, numeric, four digits; used with %V + case 'D': // %D Day of the month with English suffix (0th, 1st, 2nd, 3rd, …) + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, String.format("%%%s not supported in date format string", character)); + case '%': // %% A literal “%†character + builder.appendLiteral('%'); + break; + default: // % The literal character represented by + builder.appendLiteral(character); + break; + } + escaped = false; + } + else if (character == '%') { + escaped = true; + } + else { + builder.appendLiteral(character); + } + } + + try { + return builder.toFormatter(); + } + catch (UnsupportedOperationException e) { + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, e); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/ElementToArrayConcatFunction.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ElementToArrayConcatFunction.java new file mode 100644 index 00000000..ebac863d --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ElementToArrayConcatFunction.java @@ -0,0 +1,75 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.ParametricScalar; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.spi.type.TypeSignature; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; + +import java.lang.invoke.MethodHandle; +import java.util.Map; + +import static com.facebook.presto.metadata.Signature.typeParameter; +import static com.facebook.presto.type.TypeUtils.parameterizedTypeName; +import static com.facebook.presto.util.Reflection.methodHandle; + +public class ElementToArrayConcatFunction + extends ParametricScalar +{ + public static final ElementToArrayConcatFunction ELEMENT_TO_ARRAY_CONCAT_FUNCTION = new ElementToArrayConcatFunction(); + private static final String FUNCTION_NAME = "concat"; + private static final Signature SIGNATURE = new Signature(FUNCTION_NAME, ImmutableList.of(typeParameter("E")), "array", ImmutableList.of("E", "array"), false, false); + + @Override + public Signature getSignature() + { + return SIGNATURE; + } + + @Override + public boolean isHidden() + { + return false; + } + + @Override + public boolean isDeterministic() + { + return true; + } + + @Override + public String getDescription() + { + return "Concatenates an element to an array"; + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + Type type = types.get("E"); + MethodHandle methodHandle = methodHandle(ArrayConcatUtils.class, "prependElement", Type.class, type.getJavaType(), Slice.class); + methodHandle = methodHandle.bindTo(type); + TypeSignature typeSignature = type.getTypeSignature(); + TypeSignature returnType = parameterizedTypeName("array", typeSignature); + Signature signature = new Signature(FUNCTION_NAME, returnType, typeSignature, returnType); + return new FunctionInfo(signature, getDescription(), isHidden(), methodHandle, isDeterministic(), false, ImmutableList.of(false, false)); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/Greatest.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/Greatest.java new file mode 100644 index 00000000..d3416939 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/Greatest.java @@ -0,0 +1,242 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import com.facebook.presto.byteCode.Block; +import com.facebook.presto.byteCode.ClassDefinition; +import com.facebook.presto.byteCode.Scope; +import com.facebook.presto.byteCode.DynamicClassLoader; +import com.facebook.presto.byteCode.MethodDefinition; +import com.facebook.presto.byteCode.Parameter; +import com.facebook.presto.byteCode.Variable; +import com.facebook.presto.byteCode.control.IfStatement; +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.ParametricScalar; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.block.BlockBuilderStatus; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.sql.gen.CallSiteBinder; +import com.facebook.presto.sql.gen.CompilerOperations; +import com.facebook.presto.sql.gen.CompilerUtils; +import com.facebook.presto.util.ImmutableCollectors; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; + +import java.lang.invoke.MethodHandle; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static com.facebook.presto.byteCode.Access.FINAL; +import static com.facebook.presto.byteCode.Access.PRIVATE; +import static com.facebook.presto.byteCode.Access.PUBLIC; +import static com.facebook.presto.byteCode.Access.STATIC; +import static com.facebook.presto.byteCode.Access.a; +import static com.facebook.presto.byteCode.Parameter.arg; +import static com.facebook.presto.byteCode.ParameterizedType.type; +import static com.facebook.presto.metadata.Signature.internalFunction; +import static com.facebook.presto.metadata.Signature.orderableTypeParameter; +import static com.facebook.presto.spi.StandardErrorCode.INTERNAL_ERROR; +import static com.facebook.presto.spi.StandardErrorCode.INVALID_FUNCTION_ARGUMENT; +import static com.facebook.presto.sql.gen.CompilerUtils.defineClass; +import static com.facebook.presto.sql.gen.SqlTypeByteCodeExpression.constantType; +import static com.facebook.presto.util.Reflection.methodHandle; +import static com.google.common.base.Preconditions.checkArgument; +import static java.lang.String.format; + +public final class Greatest + extends ParametricScalar +{ + public static final Greatest GREATEST = new Greatest(); + private static final Signature SIGNATURE = new Signature("greatest", ImmutableList.of(orderableTypeParameter("E")), "E", ImmutableList.of("E"), true, false); + public static final int EXPECTED_ELEMENT_SIZE = 32; + + @Override + public Signature getSignature() + { + return SIGNATURE; + } + + @Override + public boolean isHidden() + { + return false; + } + + @Override + public boolean isDeterministic() + { + return true; + } + + @Override + public String getDescription() + { + return "get the largest of the given values"; + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + Type type = types.get("E"); + checkArgument(type.isOrderable(), "Type must be orderable"); + + ImmutableList.Builder> builder = ImmutableList.builder(); + for (int i = 0; i < arity; i++) { + builder.add(type.getJavaType()); + } + + ImmutableList> stackTypes = builder.build(); + Class clazz = generateGreatest(stackTypes, type); + MethodHandle methodHandle = methodHandle(clazz, "greatest", stackTypes.toArray(new Class[stackTypes.size()])); + List nullableParameters = ImmutableList.copyOf(Collections.nCopies(stackTypes.size(), false)); + + Signature specializedSignature = internalFunction(SIGNATURE.getName(), type.getTypeSignature(), Collections.nCopies(arity, type.getTypeSignature())); + return new FunctionInfo(specializedSignature, getDescription(), isHidden(), methodHandle, isDeterministic(), false, nullableParameters); + } + + public static void checkNotNaN(double value) + { + if (Double.isNaN(value)) { + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, "Invalid argument to greatest(): NaN"); + } + } + + private static Class generateGreatest(List> nativeContainerTypes, Type type) + { + List nativeContainerTypeNames = nativeContainerTypes.stream().map(Class::getSimpleName).collect(ImmutableCollectors.toImmutableList()); + + ClassDefinition definition = new ClassDefinition( + a(PUBLIC, FINAL), + CompilerUtils.makeClassName(Joiner.on("").join(nativeContainerTypeNames) + "Greatest"), + type(Object.class)); + + definition.declareDefaultConstructor(a(PRIVATE)); + + ImmutableList.Builder parameters = ImmutableList.builder(); + for (int i = 0; i < nativeContainerTypes.size(); i++) { + Class nativeContainerType = nativeContainerTypes.get(i); + parameters.add(arg("arg" + i, nativeContainerType)); + } + + MethodDefinition method = definition.declareMethod(a(PUBLIC, STATIC), "greatest", type(nativeContainerTypes.get(0)), parameters.build()); + Scope scope = method.getScope(); + Block body = method.getBody(); + + Variable typeVariable = scope.declareVariable(Type.class, "typeVariable"); + CallSiteBinder binder = new CallSiteBinder(); + body.comment("typeVariable = type;") + .append(constantType(binder, type)) + .putVariable(typeVariable); + + for (int i = 0; i < nativeContainerTypes.size(); i++) { + Class nativeContainerType = nativeContainerTypes.get(i); + Variable currentBlock = scope.declareVariable(com.facebook.presto.spi.block.Block.class, "block" + i); + Variable blockBuilder = scope.declareVariable(BlockBuilder.class, "blockBuilder" + i); + Block buildBlock = new Block() + .comment("blockBuilder%d = typeVariable.createBlockBuilder(new BlockBuilderStatus(), 1, EXPECTED_ELEMENT_SIZE);", i) + .getVariable(typeVariable) + .newObject(BlockBuilderStatus.class) + .dup() + .invokeConstructor(BlockBuilderStatus.class) + .push(1) + .push(EXPECTED_ELEMENT_SIZE) + .invokeInterface(Type.class, "createBlockBuilder", BlockBuilder.class, BlockBuilderStatus.class, int.class, int.class) + .putVariable(blockBuilder); + + String writeMethodName; + if (nativeContainerType == long.class) { + writeMethodName = "writeLong"; + } + else if (nativeContainerType == boolean.class) { + writeMethodName = "writeBoolean"; + } + else if (nativeContainerType == double.class) { + writeMethodName = "writeDouble"; + } + else if (nativeContainerType == Slice.class) { + writeMethodName = "writeSlice"; + } + else { + throw new PrestoException(INTERNAL_ERROR, format("Unexpected type %s", nativeContainerType.getName())); + } + + if (type.getTypeSignature().getBase().equals(StandardTypes.DOUBLE)) { + buildBlock + .append(scope.getVariable("arg" + i)) + .invokeStatic(Greatest.class, "checkNotNaN", void.class, double.class); + } + + Block writeBlock = new Block() + .comment("typeVariable.%s(blockBuilder%d, arg%d);", writeMethodName, i, i) + .getVariable(typeVariable) + .getVariable(blockBuilder) + .append(scope.getVariable("arg" + i)) + .invokeInterface(Type.class, writeMethodName, void.class, BlockBuilder.class, nativeContainerType); + + buildBlock.append(writeBlock); + + Block storeBlock = new Block() + .comment("block%d = blockBuilder%d.build();", i, i) + .getVariable(blockBuilder) + .invokeInterface(BlockBuilder.class, "build", com.facebook.presto.spi.block.Block.class) + .putVariable(currentBlock); + buildBlock.append(storeBlock); + body.append(buildBlock); + } + + Variable greatestVariable = scope.declareVariable(nativeContainerTypes.get(0), "greatest"); + Variable greatestBlockVariable = scope.declareVariable(com.facebook.presto.spi.block.Block.class, "greatestBlock"); + + body.comment("greatest = arg0; greatestBlock = block0;") + .append(scope.getVariable("arg0")) + .putVariable(greatestVariable) + .append(scope.getVariable("block0")) + .putVariable(greatestBlockVariable); + + for (int i = 1; i < nativeContainerTypes.size(); i++) { + IfStatement ifStatement = new IfStatement("if (type.compareTo(greatestBlock, 0, block" + i + ", 0) < 0)"); + + ifStatement.condition() + .getVariable(typeVariable) + .getVariable(greatestBlockVariable) + .push(0) + .append(scope.getVariable("block" + i)) + .push(0) + .invokeInterface(Type.class, "compareTo", int.class, com.facebook.presto.spi.block.Block.class, int.class, com.facebook.presto.spi.block.Block.class, int.class) + .push(0) + .invokeStatic(CompilerOperations.class, "greaterThan", boolean.class, int.class, int.class); + + ifStatement.ifFalse() + .append(scope.getVariable("arg" + i)) + .putVariable(greatestVariable) + .append(scope.getVariable("block" + i)) + .putVariable(greatestBlockVariable); + + body.append(ifStatement); + } + + body.comment("return greatest;") + .getVariable(greatestVariable) + .ret(nativeContainerTypes.get(0)); + + return defineClass(definition, Object.class, binder.getBindings(), new DynamicClassLoader(Greatest.class.getClassLoader())); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/HyperLogLogFunctions.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/HyperLogLogFunctions.java new file mode 100644 index 00000000..3f1ebae3 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/HyperLogLogFunctions.java @@ -0,0 +1,33 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import com.facebook.presto.operator.Description; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.type.SqlType; +import io.airlift.slice.Slice; +import io.airlift.stats.cardinality.HyperLogLog; + +public final class HyperLogLogFunctions +{ + private HyperLogLogFunctions() {} + + @ScalarFunction + @Description("compute the cardinality of a HyperLogLog instance") + @SqlType(StandardTypes.BIGINT) + public static long cardinality(@SqlType(StandardTypes.HYPER_LOG_LOG) Slice serializedHll) + { + return HyperLogLog.newInstance(serializedHll).cardinality(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/IdentityCast.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/IdentityCast.java new file mode 100644 index 00000000..032d57a3 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/IdentityCast.java @@ -0,0 +1,50 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.OperatorType; +import com.facebook.presto.metadata.ParametricOperator; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.google.common.collect.ImmutableList; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.util.Map; + +import static com.facebook.presto.metadata.FunctionRegistry.operatorInfo; +import static com.facebook.presto.metadata.Signature.typeParameter; +import static com.google.common.base.Preconditions.checkArgument; + +public class IdentityCast + extends ParametricOperator +{ + public static final IdentityCast IDENTITY_CAST = new IdentityCast(); + + protected IdentityCast() + { + super(OperatorType.CAST, ImmutableList.of(typeParameter("T")), "T", ImmutableList.of("T")); + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + checkArgument(types.size() == 1, "Expected only one type"); + Type type = types.get("T"); + MethodHandle identity = MethodHandles.identity(type.getJavaType()); + return operatorInfo(OperatorType.CAST, type.getTypeSignature(), ImmutableList.of(type.getTypeSignature()), identity, false, ImmutableList.of(false)); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/JsonExtract.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/JsonExtract.java new file mode 100644 index 00000000..e0f162d5 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/JsonExtract.java @@ -0,0 +1,359 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import com.facebook.presto.spi.PrestoException; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.io.SerializedString; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.DynamicSliceOutput; +import io.airlift.slice.Slice; +import io.airlift.slice.Slices; + +import java.io.IOException; + +import static com.facebook.presto.spi.StandardErrorCode.INVALID_FUNCTION_ARGUMENT; +import static com.fasterxml.jackson.core.JsonFactory.Feature.CANONICALIZE_FIELD_NAMES; +import static com.fasterxml.jackson.core.JsonToken.END_ARRAY; +import static com.fasterxml.jackson.core.JsonToken.END_OBJECT; +import static com.fasterxml.jackson.core.JsonToken.FIELD_NAME; +import static com.fasterxml.jackson.core.JsonToken.START_ARRAY; +import static com.fasterxml.jackson.core.JsonToken.START_OBJECT; +import static com.fasterxml.jackson.core.JsonToken.VALUE_NULL; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * Extracts values from JSON + *

+ * Supports the following JSON path primitives: + *

+ *    $ : Root object
+ *    . or [] : Child operator
+ *   [] : Subscript operator for array
+ * 
+ *

+ * Supported JSON Path Examples: + *

+ *    { "store": {
+ *        "book": [
+ *          { "category": "reference",
+ *            "author": "Nigel Rees",
+ *            "title": "Sayings of the Century",
+ *            "price": 8.95,
+ *            "contributors": [["Adam", "Levine"], ["Bob", "Strong"]]
+ *          },
+ *          { "category": "fiction",
+ *            "author": "Evelyn Waugh",
+ *            "title": "Sword of Honour",
+ *            "price": 12.99,
+ *            "isbn": "0-553-21311-3",
+ *            "last_owner": null
+ *          }
+ *        ],
+ *        "bicycle": {
+ *          "color": "red",
+ *          "price": 19.95
+ *        }
+ *      }
+ *    }
+ * 
+ *

+ * With only scalar values using dot-notation of path: + *

+ *    $.store.book[0].author => Nigel Rees
+ *    $.store.bicycle.price => 19.95
+ *    $.store.book[0].isbn => NULL (Doesn't exist becomes java null)
+ *    $.store.book[1].last_owner => NULL (json null becomes java null)
+ *    $.store.book[0].contributors[0][1] => Levine
+ * 
+ *

+ * With json values using dot-notation of path: + *

+ *    $.store.book[0].author => "Nigel Rees"
+ *    $.store.bicycle.price => 19.95
+ *    $.store.book[0].isbn => NULL (Doesn't exist becomes java null)
+ *    $.store.book[1].last_owner => null (json null becomes the string "null")
+ *    $.store.book[0].contributors[0] => ["Adam", "Levine"]
+ *    $.store.bicycle => {"color": "red", "price": 19.95}
+ * 
+ * With only scalar values using bracket-notation of path: + *
+ *    $["store"]["book"][0]["author"] => Nigel Rees
+ *    $["store"]["bicycle"]["price"] => 19.95
+ *    $["store"]["book"][0]["isbn"] => NULL (Doesn't exist becomes java null)
+ *    $["store"]["book"][1]["last_owner"] => NULL (json null becomes java null)
+ *    $["store"]["book"][0]["contributors"][0][1] => Levine
+ * 
+ *

+ * With json values using bracket-notation of path: + *

+ *    $["store"]["book"][0]["author"] => "Nigel Rees"
+ *    $["store"]["bicycle"]["price"] => 19.95
+ *    $["store"]["book"][0]["isbn"] => NULL (Doesn't exist becomes java null)
+ *    $["store"]["book"][1]["last_owner"] => null (json null becomes the string "null")
+ *    $["store"]["book"][0]["contributors"][0] => ["Adam", "Levine"]
+ *    $["store"]["bicycle"] => {"color": "red", "price": 19.95}
+ * 
+ */ +public final class JsonExtract +{ + private static final int ESTIMATED_JSON_OUTPUT_SIZE = 512; + + private static final JsonFactory JSON_FACTORY = new JsonFactory() + .disable(CANONICALIZE_FIELD_NAMES); + + private JsonExtract() {} + + public static T extract(Slice jsonInput, JsonExtractor jsonExtractor) + { + checkNotNull(jsonInput, "jsonInput is null"); + try { + try (JsonParser jsonParser = JSON_FACTORY.createJsonParser(jsonInput.getInput())) { + // Initialize by advancing to first token and make sure it exists + if (jsonParser.nextToken() == null) { + throw new JsonParseException("Missing starting token", jsonParser.getCurrentLocation()); + } + + return jsonExtractor.extract(jsonParser); + } + } + catch (JsonParseException e) { + // Return null if we failed to parse something + return null; + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } + + public static JsonExtractor generateExtractor(String path, JsonExtractor rootExtractor) + { + return generateExtractor(path, rootExtractor, false); + } + + public static JsonExtractor generateExtractor(String path, JsonExtractor rootExtractor, boolean exceptionOnOutOfBounds) + { + ImmutableList tokens = ImmutableList.copyOf(new JsonPathTokenizer(path)); + + JsonExtractor jsonExtractor = rootExtractor; + for (String token : tokens.reverse()) { + jsonExtractor = new ObjectFieldJsonExtractor<>(token, jsonExtractor, exceptionOnOutOfBounds); + } + return jsonExtractor; + } + + public interface JsonExtractor + { + /** + * Executes the extraction on the existing content of the JsonParser and outputs the match. + *

+ * Notes: + *

    + *
  • JsonParser must be on the FIRST token of the value to be processed when extract is called
  • + *
  • INVARIANT: when extract() returns, the current token of the parser will be the LAST token of the value
  • + *
+ * + * @return the value, or null if not applicable + */ + T extract(JsonParser jsonParser) + throws IOException; + } + + public static class ObjectFieldJsonExtractor + implements JsonExtractor + { + private final SerializedString fieldName; + private final JsonExtractor delegate; + private final int index; + private final boolean exceptionOnOutOfBounds; + + public ObjectFieldJsonExtractor(String fieldName, JsonExtractor delegate) + { + this(fieldName, delegate, false); + } + + public ObjectFieldJsonExtractor(String fieldName, JsonExtractor delegate, boolean exceptionOnOutOfBounds) + { + this.fieldName = new SerializedString(checkNotNull(fieldName, "fieldName is null")); + this.delegate = checkNotNull(delegate, "delegate is null"); + this.exceptionOnOutOfBounds = exceptionOnOutOfBounds; + this.index = tryParseInt(fieldName, -1); + } + + @Override + public T extract(JsonParser jsonParser) + throws IOException + { + if (jsonParser.getCurrentToken() == START_OBJECT) { + return processJsonObject(jsonParser); + } + + if (jsonParser.getCurrentToken() == START_ARRAY) { + return processJsonArray(jsonParser); + } + + throw new JsonParseException("Expected a JSON object or array", jsonParser.getCurrentLocation()); + } + + public T processJsonObject(JsonParser jsonParser) + throws IOException + { + while (!jsonParser.nextFieldName(fieldName)) { + if (!jsonParser.hasCurrentToken()) { + throw new JsonParseException("Unexpected end of object", jsonParser.getCurrentLocation()); + } + if (jsonParser.getCurrentToken() == END_OBJECT) { + // Unable to find matching field + return null; + } + jsonParser.skipChildren(); // Skip nested structure if currently at the start of one + } + + jsonParser.nextToken(); // Shift to first token of the value + + return delegate.extract(jsonParser); + } + + public T processJsonArray(JsonParser jsonParser) + throws IOException + { + int currentIndex = 0; + while (true) { + JsonToken token = jsonParser.nextToken(); + if (token == null) { + throw new JsonParseException("Unexpected end of array", jsonParser.getCurrentLocation()); + } + if (token == END_ARRAY) { + // Index out of bounds + if (exceptionOnOutOfBounds) { + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, "Index out of bounds"); + } + return null; + } + if (currentIndex == index) { + break; + } + currentIndex++; + jsonParser.skipChildren(); // Skip nested structure if currently at the start of one + } + + return delegate.extract(jsonParser); + } + } + + public static class ScalarValueJsonExtractor + implements JsonExtractor + { + @Override + public Slice extract(JsonParser jsonParser) + throws IOException + { + JsonToken token = jsonParser.getCurrentToken(); + if (token == null) { + throw new JsonParseException("Unexpected end of value", jsonParser.getCurrentLocation()); + } + if (!token.isScalarValue() || token == VALUE_NULL) { + return null; + } + return Slices.wrappedBuffer(jsonParser.getText().getBytes(UTF_8)); + } + } + + public static class JsonValueJsonExtractor + implements JsonExtractor + { + @Override + public Slice extract(JsonParser jsonParser) + throws IOException + { + if (!jsonParser.hasCurrentToken()) { + throw new JsonParseException("Unexpected end of value", jsonParser.getCurrentLocation()); + } + + DynamicSliceOutput dynamicSliceOutput = new DynamicSliceOutput(ESTIMATED_JSON_OUTPUT_SIZE); + try (JsonGenerator jsonGenerator = JSON_FACTORY.createJsonGenerator(dynamicSliceOutput)) { + jsonGenerator.copyCurrentStructure(jsonParser); + } + return dynamicSliceOutput.slice(); + } + } + + public static class JsonSizeExtractor + implements JsonExtractor + { + @Override + public Long extract(JsonParser jsonParser) + throws IOException + { + if (!jsonParser.hasCurrentToken()) { + throw new JsonParseException("Unexpected end of value", jsonParser.getCurrentLocation()); + } + + if (jsonParser.getCurrentToken() == START_ARRAY) { + long length = 0; + while (true) { + JsonToken token = jsonParser.nextToken(); + if (token == null) { + return null; + } + if (token == END_ARRAY) { + return length; + } + jsonParser.skipChildren(); + + length++; + } + } + + if (jsonParser.getCurrentToken() == START_OBJECT) { + long length = 0; + while (true) { + JsonToken token = jsonParser.nextToken(); + if (token == null) { + return null; + } + if (token == END_OBJECT) { + return length; + } + + if (token == FIELD_NAME) { + length++; + } + else { + jsonParser.skipChildren(); + } + } + } + + return 0L; + } + } + + private static int tryParseInt(String fieldName, int defaultValue) + { + int index = defaultValue; + try { + index = Integer.parseInt(fieldName); + } + catch (NumberFormatException ignored) { + } + return index; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/JsonFunctions.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/JsonFunctions.java new file mode 100644 index 00000000..562ebc2f --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/JsonFunctions.java @@ -0,0 +1,382 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import com.facebook.presto.metadata.OperatorType; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.type.JsonPathType; +import com.facebook.presto.type.SqlType; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.MappingJsonFactory; +import com.google.common.primitives.Doubles; +import io.airlift.slice.Slice; + +import javax.annotation.Nullable; + +import java.io.IOException; +import java.util.LinkedList; +import java.util.List; + +import static com.fasterxml.jackson.core.JsonFactory.Feature.CANONICALIZE_FIELD_NAMES; +import static com.fasterxml.jackson.core.JsonParser.NumberType; +import static com.fasterxml.jackson.core.JsonToken.END_ARRAY; +import static com.fasterxml.jackson.core.JsonToken.START_ARRAY; +import static com.fasterxml.jackson.core.JsonToken.START_OBJECT; +import static com.fasterxml.jackson.core.JsonToken.VALUE_FALSE; +import static com.fasterxml.jackson.core.JsonToken.VALUE_NUMBER_FLOAT; +import static com.fasterxml.jackson.core.JsonToken.VALUE_NUMBER_INT; +import static com.fasterxml.jackson.core.JsonToken.VALUE_STRING; +import static com.fasterxml.jackson.core.JsonToken.VALUE_TRUE; +import static io.airlift.slice.Slices.utf8Slice; +import static java.nio.charset.StandardCharsets.UTF_8; + +public final class JsonFunctions +{ + private static final JsonFactory JSON_FACTORY = new JsonFactory() + .disable(CANONICALIZE_FIELD_NAMES); + + private static final JsonFactory MAPPING_JSON_FACTORY = new MappingJsonFactory() + .disable(CANONICALIZE_FIELD_NAMES); + + private JsonFunctions() {} + + @ScalarOperator(OperatorType.CAST) + @SqlType(JsonPathType.NAME) + public static JsonPath castToJsonPath(@SqlType(StandardTypes.VARCHAR) Slice pattern) + { + return new JsonPath(pattern.toString(UTF_8)); + } + + @ScalarOperator(OperatorType.CAST) + @SqlType(StandardTypes.VARCHAR) + public static Slice castToVarchar(@SqlType(StandardTypes.JSON) Slice slice) + { + return slice; + } + + @Nullable + @ScalarFunction("json_array_length") + @SqlType(StandardTypes.BIGINT) + public static Long varcharJsonArrayLength(@SqlType(StandardTypes.VARCHAR) Slice json) + { + return jsonArrayLength(json); + } + + @Nullable + @ScalarFunction + @SqlType(StandardTypes.BIGINT) + public static Long jsonArrayLength(@SqlType(StandardTypes.JSON) Slice json) + { + try (JsonParser parser = JSON_FACTORY.createJsonParser(json.getInput())) { + if (parser.nextToken() != START_ARRAY) { + return null; + } + + long length = 0; + while (true) { + JsonToken token = parser.nextToken(); + if (token == null) { + return null; + } + if (token == END_ARRAY) { + return length; + } + parser.skipChildren(); + + length++; + } + } + catch (IOException e) { + return null; + } + } + + @Nullable + @ScalarFunction("json_array_contains") + @SqlType(StandardTypes.BOOLEAN) + public static Boolean varcharJsonArrayContains(@SqlType(StandardTypes.VARCHAR) Slice json, @SqlType(StandardTypes.BOOLEAN) boolean value) + { + return jsonArrayContains(json, value); + } + + @Nullable + @ScalarFunction + @SqlType(StandardTypes.BOOLEAN) + public static Boolean jsonArrayContains(@SqlType(StandardTypes.JSON) Slice json, @SqlType(StandardTypes.BOOLEAN) boolean value) + { + try (JsonParser parser = JSON_FACTORY.createJsonParser(json.getInput())) { + if (parser.nextToken() != START_ARRAY) { + return null; + } + + while (true) { + JsonToken token = parser.nextToken(); + if (token == null) { + return null; + } + if (token == END_ARRAY) { + return false; + } + parser.skipChildren(); + + if (((token == VALUE_TRUE) && value) || + ((token == VALUE_FALSE) && (!value))) { + return true; + } + } + } + catch (IOException e) { + return null; + } + } + + @Nullable + @ScalarFunction("json_array_contains") + @SqlType(StandardTypes.BOOLEAN) + public static Boolean varcharJsonArrayContains(@SqlType(StandardTypes.VARCHAR) Slice json, @SqlType(StandardTypes.BIGINT) long value) + { + return jsonArrayContains(json, value); + } + + @Nullable + @ScalarFunction + @SqlType(StandardTypes.BOOLEAN) + public static Boolean jsonArrayContains(@SqlType(StandardTypes.JSON) Slice json, @SqlType(StandardTypes.BIGINT) long value) + { + try (JsonParser parser = JSON_FACTORY.createJsonParser(json.getInput())) { + if (parser.nextToken() != START_ARRAY) { + return null; + } + + while (true) { + JsonToken token = parser.nextToken(); + if (token == null) { + return null; + } + if (token == END_ARRAY) { + return false; + } + parser.skipChildren(); + + if ((token == VALUE_NUMBER_INT) && + ((parser.getNumberType() == NumberType.INT) || (parser.getNumberType() == NumberType.LONG)) && + (parser.getLongValue() == value)) { + return true; + } + } + } + catch (IOException e) { + return null; + } + } + + @Nullable + @ScalarFunction("json_array_contains") + @SqlType(StandardTypes.BOOLEAN) + public static Boolean varcharJsonArrayContains(@SqlType(StandardTypes.VARCHAR) Slice json, @SqlType(StandardTypes.DOUBLE) double value) + { + return jsonArrayContains(json, value); + } + + @Nullable + @ScalarFunction + @SqlType(StandardTypes.BOOLEAN) + public static Boolean jsonArrayContains(@SqlType(StandardTypes.JSON) Slice json, @SqlType(StandardTypes.DOUBLE) double value) + { + if (!Doubles.isFinite(value)) { + return false; + } + + try (JsonParser parser = JSON_FACTORY.createJsonParser(json.getInput())) { + if (parser.nextToken() != START_ARRAY) { + return null; + } + + while (true) { + JsonToken token = parser.nextToken(); + if (token == null) { + return null; + } + if (token == END_ARRAY) { + return false; + } + parser.skipChildren(); + + // noinspection FloatingPointEquality + if ((token == VALUE_NUMBER_FLOAT) && (parser.getDoubleValue() == value) && + (Doubles.isFinite(parser.getDoubleValue()))) { + return true; + } + } + } + catch (IOException e) { + return null; + } + } + + @Nullable + @ScalarFunction("json_array_contains") + @SqlType(StandardTypes.BOOLEAN) + public static Boolean varcharJsonArrayContains(@SqlType(StandardTypes.VARCHAR) Slice json, @SqlType(StandardTypes.VARCHAR) Slice value) + { + return jsonArrayContains(json, value); + } + + @Nullable + @ScalarFunction + @SqlType(StandardTypes.BOOLEAN) + public static Boolean jsonArrayContains(@SqlType(StandardTypes.JSON) Slice json, @SqlType(StandardTypes.VARCHAR) Slice value) + { + String valueString = value.toString(UTF_8); + + try (JsonParser parser = JSON_FACTORY.createJsonParser(json.getInput())) { + if (parser.nextToken() != START_ARRAY) { + return null; + } + + while (true) { + JsonToken token = parser.nextToken(); + if (token == null) { + return null; + } + if (token == END_ARRAY) { + return false; + } + parser.skipChildren(); + + if (token == VALUE_STRING && valueString.equals(parser.getValueAsString())) { + return true; + } + } + } + catch (IOException e) { + return null; + } + } + + @Nullable + @ScalarFunction("json_array_get") + @SqlType(StandardTypes.JSON) + public static Slice varcharJsonArrayGet(@SqlType(StandardTypes.VARCHAR) Slice json, @SqlType(StandardTypes.BIGINT) long index) + { + return jsonArrayGet(json, index); + } + + @Nullable + @ScalarFunction + @SqlType(StandardTypes.JSON) + public static Slice jsonArrayGet(@SqlType(StandardTypes.JSON) Slice json, @SqlType(StandardTypes.BIGINT) long index) + { + try (JsonParser parser = MAPPING_JSON_FACTORY.createJsonParser(json.getInput())) { + if (parser.nextToken() != START_ARRAY) { + return null; + } + + List tokens = null; + if (index < 0) { + tokens = new LinkedList<>(); + } + + long count = 0; + while (true) { + JsonToken token = parser.nextToken(); + if (token == null) { + return null; + } + if (token == END_ARRAY) { + if (tokens != null && count >= index * -1) { + return utf8Slice(tokens.get(0)); + } + + return null; + } + + String arrayElement; + if (token == START_OBJECT || token == START_ARRAY) { + arrayElement = parser.readValueAsTree().toString(); + } + else { + arrayElement = parser.getValueAsString(); + } + + if (count == index) { + return arrayElement == null ? null : utf8Slice(arrayElement); + } + + if (tokens != null) { + tokens.add(arrayElement); + + if (count >= index * -1) { + tokens.remove(0); + } + } + + count++; + } + } + catch (IOException e) { + return null; + } + } + + @ScalarFunction("json_extract_scalar") + @Nullable + @SqlType(StandardTypes.VARCHAR) + public static Slice varcharJsonExtractScalar(@SqlType(StandardTypes.VARCHAR) Slice json, @SqlType(JsonPathType.NAME) JsonPath jsonPath) + { + return JsonExtract.extract(json, jsonPath.getScalarExtractor()); + } + + @ScalarFunction + @Nullable + @SqlType(StandardTypes.VARCHAR) + public static Slice jsonExtractScalar(@SqlType(StandardTypes.JSON) Slice json, @SqlType(JsonPathType.NAME) JsonPath jsonPath) + { + return JsonExtract.extract(json, jsonPath.getScalarExtractor()); + } + + @ScalarFunction("json_extract") + @Nullable + @SqlType(StandardTypes.JSON) + public static Slice varcharJsonExtract(@SqlType(StandardTypes.VARCHAR) Slice json, @SqlType(JsonPathType.NAME) JsonPath jsonPath) + { + return JsonExtract.extract(json, jsonPath.getObjectExtractor()); + } + + @ScalarFunction + @Nullable + @SqlType(StandardTypes.JSON) + public static Slice jsonExtract(@SqlType(StandardTypes.JSON) Slice json, @SqlType(JsonPathType.NAME) JsonPath jsonPath) + { + return JsonExtract.extract(json, jsonPath.getObjectExtractor()); + } + + @ScalarFunction("json_size") + @Nullable + @SqlType(StandardTypes.BIGINT) + public static Long varcharJsonSize(@SqlType(StandardTypes.VARCHAR) Slice json, @SqlType(JsonPathType.NAME) JsonPath jsonPath) + { + return JsonExtract.extract(json, jsonPath.getSizeExtractor()); + } + + @ScalarFunction + @Nullable + @SqlType(StandardTypes.BIGINT) + public static Long jsonSize(@SqlType(StandardTypes.JSON) Slice json, @SqlType(JsonPathType.NAME) JsonPath jsonPath) + { + return JsonExtract.extract(json, jsonPath.getSizeExtractor()); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/JsonOperators.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/JsonOperators.java new file mode 100644 index 00000000..e19fddd7 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/JsonOperators.java @@ -0,0 +1,50 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.type.SqlType; +import io.airlift.slice.Slice; + +import static com.facebook.presto.metadata.OperatorType.EQUAL; +import static com.facebook.presto.metadata.OperatorType.HASH_CODE; +import static com.facebook.presto.metadata.OperatorType.NOT_EQUAL; + +public final class JsonOperators +{ + private JsonOperators() + { + } + + @ScalarOperator(HASH_CODE) + @SqlType(StandardTypes.BIGINT) + public static long hashCode(@SqlType(StandardTypes.JSON) Slice value) + { + return value.hashCode(); + } + + @ScalarOperator(EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean equals(@SqlType(StandardTypes.JSON) Slice leftJson, @SqlType(StandardTypes.JSON) Slice rightJson) + { + return leftJson.equals(rightJson); + } + + @ScalarOperator(NOT_EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean notEqual(@SqlType(StandardTypes.JSON) Slice leftJson, @SqlType(StandardTypes.JSON) Slice rightJson) + { + return !leftJson.equals(rightJson); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/JsonPath.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/JsonPath.java new file mode 100644 index 00000000..b9b0328c --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/JsonPath.java @@ -0,0 +1,45 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import io.airlift.slice.Slice; + +public class JsonPath +{ + private final JsonExtract.JsonExtractor scalarExtractor; + private final JsonExtract.JsonExtractor objectExtractor; + private final JsonExtract.JsonExtractor sizeExtractor; + + public JsonPath(String pattern) + { + scalarExtractor = JsonExtract.generateExtractor(pattern, new JsonExtract.ScalarValueJsonExtractor()); + objectExtractor = JsonExtract.generateExtractor(pattern, new JsonExtract.JsonValueJsonExtractor()); + sizeExtractor = JsonExtract.generateExtractor(pattern, new JsonExtract.JsonSizeExtractor()); + } + + public JsonExtract.JsonExtractor getScalarExtractor() + { + return scalarExtractor; + } + + public JsonExtract.JsonExtractor getObjectExtractor() + { + return objectExtractor; + } + + public JsonExtract.JsonExtractor getSizeExtractor() + { + return sizeExtractor; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/JsonPathTokenizer.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/JsonPathTokenizer.java new file mode 100644 index 00000000..9be7b227 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/JsonPathTokenizer.java @@ -0,0 +1,175 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import com.facebook.presto.spi.PrestoException; +import com.google.common.collect.AbstractIterator; + +import static com.facebook.presto.spi.StandardErrorCode.INVALID_FUNCTION_ARGUMENT; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.lang.Character.isLetterOrDigit; +import static java.lang.String.format; + +public class JsonPathTokenizer + extends AbstractIterator +{ + private static final char QUOTE = '\"'; + private static final char DOT = '.'; + private static final char OPEN_BRACKET = '['; + private static final char CLOSE_BRACKET = ']'; + private static final char UNICODE_CARET = '\u2038'; + + private final String path; + private int index; + + public JsonPathTokenizer(String path) + { + this.path = checkNotNull(path, "path is null"); + + if (path.isEmpty()) { + throw invalidJsonPath(); + } + + // skip the start token + match('$'); + } + + @Override + protected String computeNext() + { + if (!hasNextCharacter()) { + return endOfData(); + } + + if (tryMatch(DOT)) { + return matchPathSegment(); + } + + if (tryMatch(OPEN_BRACKET)) { + String token = tryMatch(QUOTE) ? matchQuotedSubscript() : matchUnquotedSubscript(); + + match(CLOSE_BRACKET); + return token; + } + + throw invalidJsonPath(); + } + + private String matchPathSegment() + { + // seek until we see a special character or whitespace + int start = index; + while (hasNextCharacter() && isUnquotedPathCharacter(peekCharacter())) { + nextCharacter(); + } + int end = index; + + String token = path.substring(start, end); + + // an empty unquoted token is not allowed + if (token.isEmpty()) { + throw invalidJsonPath(); + } + + return token; + } + + private static boolean isUnquotedPathCharacter(char c) + { + return c == ':' || isUnquotedSubscriptCharacter(c); + } + + private String matchUnquotedSubscript() + { + // seek until we see a special character or whitespace + int start = index; + while (hasNextCharacter() && isUnquotedSubscriptCharacter(peekCharacter())) { + nextCharacter(); + } + int end = index; + + String token = path.substring(start, end); + + // an empty unquoted token is not allowed + if (token.isEmpty()) { + throw invalidJsonPath(); + } + + return token; + } + + private static boolean isUnquotedSubscriptCharacter(char c) + { + return c == '_' || isLetterOrDigit(c); + } + + private String matchQuotedSubscript() + { + // quote has already been matched + + // seek until we see the close quote + int start = index; + while (hasNextCharacter() && peekCharacter() != QUOTE) { + nextCharacter(); + } + int end = index; + + String token = path.substring(start, end); + + match(QUOTE); + return token; + } + + private boolean hasNextCharacter() + { + return index < path.length(); + } + + private void match(char expected) + { + if (!tryMatch(expected)) { + throw invalidJsonPath(); + } + } + + private boolean tryMatch(char expected) + { + if (peekCharacter() != expected) { + return false; + } + index++; + return true; + } + + private void nextCharacter() + { + index++; + } + + private char peekCharacter() + { + return path.charAt(index); + } + + private PrestoException invalidJsonPath() + { + return new PrestoException(INVALID_FUNCTION_ARGUMENT, format("Invalid JSON path: '%s'", path)); + } + + @Override + public String toString() + { + return path.substring(0, index) + UNICODE_CARET + path.substring(index); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/JsonToArrayCast.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/JsonToArrayCast.java new file mode 100644 index 00000000..21a85fa4 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/JsonToArrayCast.java @@ -0,0 +1,79 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.OperatorType; +import com.facebook.presto.metadata.ParametricOperator; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.type.ArrayType; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; + +import java.lang.invoke.MethodHandle; +import java.util.List; +import java.util.Map; + +import static com.facebook.presto.metadata.FunctionRegistry.operatorInfo; +import static com.facebook.presto.metadata.Signature.typeParameter; +import static com.facebook.presto.spi.StandardErrorCode.INVALID_CAST_ARGUMENT; +import static com.facebook.presto.type.ArrayType.toStackRepresentation; +import static com.facebook.presto.type.TypeJsonUtils.canCastFromJson; +import static com.facebook.presto.type.TypeJsonUtils.stackRepresentationToObject; +import static com.facebook.presto.type.TypeUtils.parameterizedTypeName; +import static com.facebook.presto.util.Failures.checkCondition; +import static com.facebook.presto.util.Reflection.methodHandle; +import static com.google.common.base.Preconditions.checkArgument; + +public class JsonToArrayCast + extends ParametricOperator +{ + public static final JsonToArrayCast JSON_TO_ARRAY = new JsonToArrayCast(); + private static final MethodHandle METHOD_HANDLE = methodHandle(JsonToArrayCast.class, "toArray", Type.class, ConnectorSession.class, Slice.class); + + private JsonToArrayCast() + { + super(OperatorType.CAST, ImmutableList.of(typeParameter("T")), "array", ImmutableList.of(StandardTypes.JSON)); + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + checkArgument(arity == 1, "Expected arity to be 1"); + Type type = types.get("T"); + Type arrayType = typeManager.getParameterizedType(StandardTypes.ARRAY, ImmutableList.of(type.getTypeSignature()), ImmutableList.of()); + checkCondition(canCastFromJson(arrayType), INVALID_CAST_ARGUMENT, "Cannot cast JSON to %s", arrayType); + MethodHandle methodHandle = METHOD_HANDLE.bindTo(arrayType); + return operatorInfo(OperatorType.CAST, arrayType.getTypeSignature(), ImmutableList.of(parameterizedTypeName(StandardTypes.JSON)), methodHandle, true, ImmutableList.of(false)); + } + + public static Slice toArray(Type arrayType, ConnectorSession connectorSession, Slice json) + { + try { + Object array = stackRepresentationToObject(connectorSession, json, arrayType); + if (array == null) { + return null; + } + return toStackRepresentation((List) array, ((ArrayType) arrayType).getElementType()); + } + catch (RuntimeException e) { + throw new PrestoException(INVALID_CAST_ARGUMENT, "Value cannot be cast to " + arrayType, e); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/JsonToMapCast.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/JsonToMapCast.java new file mode 100644 index 00000000..4830e4bb --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/JsonToMapCast.java @@ -0,0 +1,80 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.OperatorType; +import com.facebook.presto.metadata.ParametricOperator; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.type.MapType; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; + +import java.lang.invoke.MethodHandle; +import java.util.Map; + +import static com.facebook.presto.metadata.FunctionRegistry.operatorInfo; +import static com.facebook.presto.metadata.Signature.comparableTypeParameter; +import static com.facebook.presto.metadata.Signature.typeParameter; +import static com.facebook.presto.spi.StandardErrorCode.INVALID_CAST_ARGUMENT; +import static com.facebook.presto.type.MapType.toStackRepresentation; +import static com.facebook.presto.type.TypeJsonUtils.canCastFromJson; +import static com.facebook.presto.type.TypeJsonUtils.stackRepresentationToObject; +import static com.facebook.presto.type.TypeUtils.parameterizedTypeName; +import static com.facebook.presto.util.Failures.checkCondition; +import static com.facebook.presto.util.Reflection.methodHandle; +import static com.google.common.base.Preconditions.checkArgument; + +public class JsonToMapCast + extends ParametricOperator +{ + public static final JsonToMapCast JSON_TO_MAP = new JsonToMapCast(); + private static final MethodHandle METHOD_HANDLE = methodHandle(JsonToMapCast.class, "toMap", Type.class, ConnectorSession.class, Slice.class); + + private JsonToMapCast() + { + super(OperatorType.CAST, ImmutableList.of(comparableTypeParameter("K"), typeParameter("V")), "map", ImmutableList.of(StandardTypes.JSON)); + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + checkArgument(arity == 1, "Expected arity to be 1"); + Type keyType = types.get("K"); + Type valueType = types.get("V"); + Type mapType = typeManager.getParameterizedType(StandardTypes.MAP, ImmutableList.of(keyType.getTypeSignature(), valueType.getTypeSignature()), ImmutableList.of()); + checkCondition(canCastFromJson(mapType), INVALID_CAST_ARGUMENT, "Cannot cast JSON to %s", mapType); + MethodHandle methodHandle = METHOD_HANDLE.bindTo(mapType); + return operatorInfo(OperatorType.CAST, mapType.getTypeSignature(), ImmutableList.of(parameterizedTypeName(StandardTypes.JSON)), methodHandle, true, ImmutableList.of(false)); + } + + public static Slice toMap(Type mapType, ConnectorSession connectorSession, Slice json) + { + try { + Object map = stackRepresentationToObject(connectorSession, json, mapType); + if (map == null) { + return null; + } + return toStackRepresentation((Map) map, ((MapType) mapType).getKeyType(), ((MapType) mapType).getValueType()); + } + catch (RuntimeException e) { + throw new PrestoException(INVALID_CAST_ARGUMENT, "Value cannot be cast to " + mapType, e); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/Least.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/Least.java new file mode 100644 index 00000000..320e615e --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/Least.java @@ -0,0 +1,241 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import com.facebook.presto.byteCode.Block; +import com.facebook.presto.byteCode.ClassDefinition; +import com.facebook.presto.byteCode.Scope; +import com.facebook.presto.byteCode.DynamicClassLoader; +import com.facebook.presto.byteCode.MethodDefinition; +import com.facebook.presto.byteCode.Parameter; +import com.facebook.presto.byteCode.Variable; +import com.facebook.presto.byteCode.control.IfStatement; +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.ParametricScalar; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.block.BlockBuilderStatus; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.sql.gen.CallSiteBinder; +import com.facebook.presto.sql.gen.CompilerOperations; +import com.facebook.presto.sql.gen.CompilerUtils; +import com.facebook.presto.util.ImmutableCollectors; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; + +import java.lang.invoke.MethodHandle; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static com.facebook.presto.byteCode.Access.FINAL; +import static com.facebook.presto.byteCode.Access.PRIVATE; +import static com.facebook.presto.byteCode.Access.PUBLIC; +import static com.facebook.presto.byteCode.Access.STATIC; +import static com.facebook.presto.byteCode.Access.a; +import static com.facebook.presto.byteCode.Parameter.arg; +import static com.facebook.presto.byteCode.ParameterizedType.type; +import static com.facebook.presto.metadata.Signature.internalFunction; +import static com.facebook.presto.metadata.Signature.orderableTypeParameter; +import static com.facebook.presto.spi.StandardErrorCode.INTERNAL_ERROR; +import static com.facebook.presto.spi.StandardErrorCode.INVALID_FUNCTION_ARGUMENT; +import static com.facebook.presto.sql.gen.CompilerUtils.defineClass; +import static com.facebook.presto.sql.gen.SqlTypeByteCodeExpression.constantType; +import static com.facebook.presto.util.Reflection.methodHandle; +import static com.google.common.base.Preconditions.checkArgument; +import static java.lang.String.format; + +public final class Least + extends ParametricScalar +{ + public static final Least LEAST = new Least(); + private static final Signature SIGNATURE = new Signature("least", ImmutableList.of(orderableTypeParameter("E")), "E", ImmutableList.of("E"), true, false); + + @Override + public Signature getSignature() + { + return SIGNATURE; + } + + @Override + public boolean isHidden() + { + return false; + } + + @Override + public boolean isDeterministic() + { + return true; + } + + @Override + public String getDescription() + { + return "get the smallest of the given values"; + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + Type type = types.get("E"); + checkArgument(type.isOrderable(), "Type must be orderable"); + + ImmutableList.Builder> builder = ImmutableList.builder(); + for (int i = 0; i < arity; i++) { + builder.add(type.getJavaType()); + } + ImmutableList> stackTypes = builder.build(); + + Class clazz = generateLeast(stackTypes, type); + MethodHandle methodHandle = methodHandle(clazz, "least", stackTypes.toArray(new Class[stackTypes.size()])); + List nullableParameters = ImmutableList.copyOf(Collections.nCopies(stackTypes.size(), false)); + + Signature specializedSignature = internalFunction(SIGNATURE.getName(), type.getTypeSignature(), Collections.nCopies(arity, type.getTypeSignature())); + return new FunctionInfo(specializedSignature, getDescription(), isHidden(), methodHandle, isDeterministic(), false, nullableParameters); + } + + public static void checkNotNaN(double value) + { + if (Double.isNaN(value)) { + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, "Invalid argument to least(): NaN"); + } + } + + private static Class generateLeast(List> nativeContainerTypes, Type type) + { + List nativeContainerTypeNames = nativeContainerTypes.stream().map(Class::getSimpleName).collect(ImmutableCollectors.toImmutableList()); + + ClassDefinition definition = new ClassDefinition( + a(PUBLIC, FINAL), + CompilerUtils.makeClassName(Joiner.on("").join(nativeContainerTypeNames) + "Least"), + type(Object.class)); + + definition.declareDefaultConstructor(a(PRIVATE)); + + ImmutableList.Builder parameters = ImmutableList.builder(); + for (int i = 0; i < nativeContainerTypes.size(); i++) { + Class nativeContainerType = nativeContainerTypes.get(i); + parameters.add(arg("arg" + i, nativeContainerType)); + } + + MethodDefinition method = definition.declareMethod(a(PUBLIC, STATIC), "least", type(nativeContainerTypes.get(0)), parameters.build()); + Scope scope = method.getScope(); + Block body = method.getBody(); + + Variable typeVariable = scope.declareVariable(Type.class, "typeVariable"); + CallSiteBinder binder = new CallSiteBinder(); + body.comment("typeVariable = type;") + .append(constantType(binder, type)) + .putVariable(typeVariable); + + for (int i = 0; i < nativeContainerTypes.size(); i++) { + Class nativeContainerType = nativeContainerTypes.get(i); + Variable currentBlock = scope.declareVariable(com.facebook.presto.spi.block.Block.class, "block" + i); + Variable blockBuilder = scope.declareVariable(BlockBuilder.class, "blockBuilder" + i); + Block buildBlock = new Block() + .comment("blockBuilder%d = typeVariable.createBlockBuilder(new BlockBuilderStatus(), 1, 32);", i) + .getVariable(typeVariable) + .newObject(BlockBuilderStatus.class) + .dup() + .invokeConstructor(BlockBuilderStatus.class) + .push(1) + .push(32) + .invokeInterface(Type.class, "createBlockBuilder", BlockBuilder.class, BlockBuilderStatus.class, int.class, int.class) + .putVariable(blockBuilder); + + String writeMethodName; + if (nativeContainerType == long.class) { + writeMethodName = "writeLong"; + } + else if (nativeContainerType == boolean.class) { + writeMethodName = "writeBoolean"; + } + else if (nativeContainerType == double.class) { + writeMethodName = "writeDouble"; + } + else if (nativeContainerType == Slice.class) { + writeMethodName = "writeSlice"; + } + else { + throw new PrestoException(INTERNAL_ERROR, format("Unexpected type %s", nativeContainerType.getName())); + } + + if (type.getTypeSignature().getBase().equals(StandardTypes.DOUBLE)) { + buildBlock + .append(scope.getVariable("arg" + i)) + .invokeStatic(Least.class, "checkNotNaN", void.class, double.class); + } + + Block writeBlock = new Block() + .comment("typeVariable.%s(blockBuilder%d, arg%d);", writeMethodName, i, i) + .getVariable(typeVariable) + .getVariable(blockBuilder) + .append(scope.getVariable("arg" + i)) + .invokeInterface(Type.class, writeMethodName, void.class, BlockBuilder.class, nativeContainerType); + + buildBlock.append(writeBlock); + + Block storeBlock = new Block() + .comment("block%d = blockBuilder%d.build();", i, i) + .getVariable(blockBuilder) + .invokeInterface(BlockBuilder.class, "build", com.facebook.presto.spi.block.Block.class) + .putVariable(currentBlock); + buildBlock.append(storeBlock); + body.append(buildBlock); + } + + Variable leastVariable = scope.declareVariable(nativeContainerTypes.get(0), "least"); + Variable leastBlockVariable = scope.declareVariable(com.facebook.presto.spi.block.Block.class, "leastBlock"); + + body.comment("least = arg0; leastBlock = block0;") + .append(scope.getVariable("arg0")) + .putVariable(leastVariable) + .append(scope.getVariable("block0")) + .putVariable(leastBlockVariable); + + for (int i = 1; i < nativeContainerTypes.size(); i++) { + IfStatement ifStatement = new IfStatement("if (type.compareTo(leastBlock, 0, block" + i + ", 0) < 0)"); + + ifStatement.condition() + .getVariable(typeVariable) + .getVariable(leastBlockVariable) + .push(0) + .append(scope.getVariable("block" + i)) + .push(0) + .invokeInterface(Type.class, "compareTo", int.class, com.facebook.presto.spi.block.Block.class, int.class, com.facebook.presto.spi.block.Block.class, int.class) + .push(0) + .invokeStatic(CompilerOperations.class, "lessThan", boolean.class, int.class, int.class); + + ifStatement.ifFalse() + .append(scope.getVariable("arg" + i)) + .putVariable(leastVariable) + .append(scope.getVariable("block" + i)) + .putVariable(leastBlockVariable); + + body.append(ifStatement); + } + + body.comment("return least;") + .getVariable(leastVariable) + .ret(nativeContainerTypes.get(0)); + + return defineClass(definition, Object.class, binder.getBindings(), new DynamicClassLoader(Least.class.getClassLoader())); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/MapCardinalityFunction.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/MapCardinalityFunction.java new file mode 100644 index 00000000..ac72d959 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/MapCardinalityFunction.java @@ -0,0 +1,87 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.ParametricScalar; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; + +import java.lang.invoke.MethodHandle; +import java.util.Map; + +import static com.facebook.presto.metadata.Signature.typeParameter; +import static com.facebook.presto.type.TypeUtils.readStructuralBlock; +import static com.facebook.presto.spi.type.TypeSignature.parseTypeSignature; +import static com.facebook.presto.type.TypeUtils.parameterizedTypeName; +import static com.facebook.presto.util.Reflection.methodHandle; +import static com.google.common.base.Preconditions.checkArgument; + +public final class MapCardinalityFunction + extends ParametricScalar +{ + public static final MapCardinalityFunction MAP_CARDINALITY = new MapCardinalityFunction(); + private static final Signature SIGNATURE = new Signature("cardinality", ImmutableList.of(typeParameter("K"), typeParameter("V")), "bigint", ImmutableList.of("map"), false, false); + private static final MethodHandle METHOD_HANDLE = methodHandle(MapCardinalityFunction.class, "mapCardinality", Slice.class); + + @Override + public Signature getSignature() + { + return SIGNATURE; + } + + @Override + public boolean isHidden() + { + return false; + } + + @Override + public boolean isDeterministic() + { + return true; + } + + @Override + public String getDescription() + { + return null; + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + checkArgument(arity == 1, "Cardinality expects only one argument"); + Type keyType = types.get("K"); + Type valueType = types.get("V"); + return new FunctionInfo( + new Signature("cardinality", parseTypeSignature(StandardTypes.BIGINT), parameterizedTypeName("map", keyType.getTypeSignature(), valueType.getTypeSignature())), + "Returns the cardinality (size) of the map", + isHidden(), + METHOD_HANDLE, + isDeterministic(), + false, + ImmutableList.of(false)); + } + + public static long mapCardinality(Slice slice) + { + return readStructuralBlock(slice).getPositionCount() / 2; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/MapConstructor.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/MapConstructor.java new file mode 100644 index 00000000..cad3dfc5 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/MapConstructor.java @@ -0,0 +1,112 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.ParametricScalar; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.metadata.TypeParameter; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.block.BlockBuilderStatus; +import com.facebook.presto.spi.block.VariableWidthBlockBuilder; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.spi.type.TypeSignature; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; + +import java.lang.invoke.MethodHandle; +import java.util.Map; + +import static com.facebook.presto.metadata.Signature.comparableTypeParameter; +import static com.facebook.presto.metadata.Signature.typeParameter; +import static com.facebook.presto.spi.StandardErrorCode.INVALID_FUNCTION_ARGUMENT; +import static com.facebook.presto.spi.type.StandardTypes.MAP; +import static com.facebook.presto.type.TypeUtils.buildStructuralSlice; +import static com.facebook.presto.type.TypeUtils.parameterizedTypeName; +import static com.facebook.presto.type.TypeUtils.readStructuralBlock; +import static com.facebook.presto.util.Failures.checkCondition; +import static com.facebook.presto.util.Reflection.methodHandle; + +public final class MapConstructor + extends ParametricScalar +{ + public static final MapConstructor MAP_CONSTRUCTOR = new MapConstructor(); + + private static final Signature SIGNATURE = new Signature("map", ImmutableList.of(comparableTypeParameter("K"), typeParameter("V")), "map", ImmutableList.of("array", "array"), false, false); + private static final MethodHandle METHOD_HANDLE = methodHandle(MapConstructor.class, "createMap", Type.class, Type.class, Slice.class, Slice.class); + private static final String DESCRIPTION = "Constructs a map from the given key/value arrays"; + + @Override + public Signature getSignature() + { + return SIGNATURE; + } + + @Override + public boolean isHidden() + { + return false; + } + + @Override + public boolean isDeterministic() + { + return true; + } + + @Override + public String getDescription() + { + return DESCRIPTION; + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + Type keyType = types.get("K"); + Type valueType = types.get("V"); + + MethodHandle methodHandle = METHOD_HANDLE.bindTo(keyType); + methodHandle = methodHandle.bindTo(valueType); + + Type mapType = typeManager.getParameterizedType(MAP, ImmutableList.of(keyType.getTypeSignature(), valueType.getTypeSignature()), ImmutableList.of()); + ImmutableList argumentTypes = ImmutableList.of(parameterizedTypeName(StandardTypes.ARRAY, keyType.getTypeSignature()), parameterizedTypeName(StandardTypes.ARRAY, valueType.getTypeSignature())); + Signature signature = new Signature("map", ImmutableList.of(), mapType.getTypeSignature(), argumentTypes, false, false); + + return new FunctionInfo(signature, DESCRIPTION, isHidden(), methodHandle, isDeterministic(), false, ImmutableList.of(false, false)); + } + + public static Slice createMap(Type keyType, Type valueType, Slice keys, Slice values) + { + Block keyBlock = readStructuralBlock(keys); + Block valueBlock = readStructuralBlock(values); + BlockBuilder blockBuilder = new VariableWidthBlockBuilder(new BlockBuilderStatus(), keyBlock.getSizeInBytes() + valueBlock.getSizeInBytes()); + + checkCondition(keyBlock.getPositionCount() == valueBlock.getPositionCount(), INVALID_FUNCTION_ARGUMENT, "Key and value arrays must be the same length"); + for (int i = 0; i < keyBlock.getPositionCount(); i++) { + if (keyBlock.isNull(i)) { + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, "map key cannot be null"); + } + keyType.appendTo(keyBlock, i, blockBuilder); + valueType.appendTo(valueBlock, i, blockBuilder); + } + + return buildStructuralSlice(blockBuilder); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/MapEqualOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/MapEqualOperator.java new file mode 100644 index 00000000..b9653a6d --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/MapEqualOperator.java @@ -0,0 +1,171 @@ +package com.facebook.presto.operator.scalar; +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.ParametricOperator; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.spi.type.TypeSignature; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; + +import java.lang.invoke.MethodHandle; +import java.util.LinkedHashMap; +import java.util.Map; + +import static com.facebook.presto.metadata.FunctionRegistry.operatorInfo; +import static com.facebook.presto.metadata.OperatorType.EQUAL; +import static com.facebook.presto.metadata.OperatorType.HASH_CODE; +import static com.facebook.presto.metadata.Signature.comparableTypeParameter; +import static com.facebook.presto.spi.StandardErrorCode.INTERNAL_ERROR; +import static com.facebook.presto.type.TypeUtils.readStructuralBlock; +import static com.facebook.presto.spi.type.TypeSignature.parseTypeSignature; +import static com.facebook.presto.type.TypeUtils.castValue; +import static com.facebook.presto.util.Reflection.methodHandle; + +public class MapEqualOperator + extends ParametricOperator +{ + public static final MapEqualOperator MAP_EQUAL = new MapEqualOperator(); + private static final TypeSignature RETURN_TYPE = parseTypeSignature(StandardTypes.BOOLEAN); + + private MapEqualOperator() + { + super(EQUAL, ImmutableList.of(comparableTypeParameter("K"), comparableTypeParameter("V")), StandardTypes.BOOLEAN, ImmutableList.of("map", "map")); + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + Type keyType = types.get("K"); + Type valueType = types.get("V"); + + Type type = typeManager.getParameterizedType(StandardTypes.MAP, ImmutableList.of(keyType.getTypeSignature(), valueType.getTypeSignature()), ImmutableList.of()); + TypeSignature typeSignature = type.getTypeSignature(); + + MethodHandle keyEqualsFunction = functionRegistry.resolveOperator(EQUAL, ImmutableList.of(keyType, keyType)).getMethodHandle(); + MethodHandle keyHashcodeFunction = functionRegistry.resolveOperator(HASH_CODE, ImmutableList.of(keyType)).getMethodHandle(); + MethodHandle valueEqualsFunction = functionRegistry.resolveOperator(EQUAL, ImmutableList.of(valueType, valueType)).getMethodHandle(); + + MethodHandle methodHandle = methodHandle(MapEqualOperator.class, "equals", MethodHandle.class, MethodHandle.class, MethodHandle.class, Type.class, Type.class, Slice.class, Slice.class); + MethodHandle method = methodHandle.bindTo(keyEqualsFunction).bindTo(keyHashcodeFunction).bindTo(valueEqualsFunction).bindTo(keyType).bindTo(valueType); + return operatorInfo(EQUAL, RETURN_TYPE, ImmutableList.of(typeSignature, typeSignature), method, true, ImmutableList.of(false, false)); + } + + public static Boolean equals(MethodHandle keyEqualsFunction, MethodHandle keyHashcodeFunction, MethodHandle valueEqualsFunction, Type keyType, Type valueType, Slice left, Slice right) + { + Block leftMapBlock = readStructuralBlock(left); + Block rightMapBlock = readStructuralBlock(right); + + Map wrappedLeftMap = new LinkedHashMap<>(); + for (int position = 0; position < leftMapBlock.getPositionCount(); position += 2) { + wrappedLeftMap.put(new KeyWrapper(castValue(keyType, leftMapBlock, position), keyEqualsFunction, keyHashcodeFunction), position + 1); + } + + Map wrappedRightMap = new LinkedHashMap<>(); + for (int position = 0; position < rightMapBlock.getPositionCount(); position += 2) { + wrappedRightMap.put(new KeyWrapper(castValue(keyType, rightMapBlock, position), keyEqualsFunction, keyHashcodeFunction), position + 1); + } + + if (wrappedLeftMap.size() != wrappedRightMap.size()) { + return false; + } + + for (Map.Entry entry : wrappedRightMap.entrySet()) { + KeyWrapper key = entry.getKey(); + Integer leftValuePosition = wrappedLeftMap.get(key); + if (leftValuePosition == null) { + return false; + } + + Object leftValue = castValue(valueType, leftMapBlock, leftValuePosition); + if (leftValue == null) { + return null; + } + + Object rightValue = castValue(valueType, rightMapBlock, entry.getValue()); + if (rightValue == null) { + return null; + } + + try { + Boolean result = (Boolean) valueEqualsFunction.invoke(leftValue, rightValue); + if (result == null) { + return null; + } + else if (!result) { + return false; + } + } + catch (Throwable t) { + Throwables.propagateIfInstanceOf(t, Error.class); + Throwables.propagateIfInstanceOf(t, PrestoException.class); + + throw new PrestoException(INTERNAL_ERROR, t); + } + } + return true; + } + + private static final class KeyWrapper + { + private final Object key; + private final MethodHandle hashCode; + private final MethodHandle equals; + + public KeyWrapper(Object key, MethodHandle equals, MethodHandle hashCode) + { + this.key = key; + this.equals = equals; + this.hashCode = hashCode; + } + + @Override + public int hashCode() + { + try { + return Long.hashCode((long) hashCode.invoke(key)); + } + catch (Throwable t) { + Throwables.propagateIfInstanceOf(t, Error.class); + Throwables.propagateIfInstanceOf(t, PrestoException.class); + + throw new PrestoException(INTERNAL_ERROR, t); + } + } + + @Override + public boolean equals(Object obj) + { + if (obj == null || !getClass().equals(obj.getClass())) { + return false; + } + KeyWrapper other = (KeyWrapper) obj; + try { + return (Boolean) equals.invoke(key, other.key); + } + catch (Throwable t) { + Throwables.propagateIfInstanceOf(t, Error.class); + Throwables.propagateIfInstanceOf(t, PrestoException.class); + + throw new PrestoException(INTERNAL_ERROR, t); + } + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/MapHashCodeOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/MapHashCodeOperator.java new file mode 100644 index 00000000..d2cf215c --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/MapHashCodeOperator.java @@ -0,0 +1,76 @@ +package com.facebook.presto.operator.scalar; +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.ParametricOperator; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.spi.type.TypeSignature; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; + +import java.lang.invoke.MethodHandle; +import java.util.Map; + +import static com.facebook.presto.metadata.FunctionRegistry.operatorInfo; +import static com.facebook.presto.metadata.OperatorType.HASH_CODE; +import static com.facebook.presto.metadata.Signature.comparableTypeParameter; +import static com.facebook.presto.spi.type.TypeSignature.parseTypeSignature; +import static com.facebook.presto.type.TypeUtils.hashPosition; +import static com.facebook.presto.type.TypeUtils.readStructuralBlock; +import static com.facebook.presto.util.Reflection.methodHandle; + +public class MapHashCodeOperator + extends ParametricOperator +{ + public static final MapHashCodeOperator MAP_HASH_CODE = new MapHashCodeOperator(); + private static final TypeSignature RETURN_TYPE = parseTypeSignature(StandardTypes.BIGINT); + + private MapHashCodeOperator() + { + super(HASH_CODE, ImmutableList.of(comparableTypeParameter("K"), comparableTypeParameter("V")), StandardTypes.BIGINT, ImmutableList.of("map")); + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + Type keyType = types.get("K"); + Type valueType = types.get("V"); + + Type type = typeManager.getParameterizedType(StandardTypes.MAP, ImmutableList.of(keyType.getTypeSignature(), valueType.getTypeSignature()), ImmutableList.of()); + TypeSignature typeSignature = type.getTypeSignature(); + + MethodHandle keyHashCodeFunction = functionRegistry.resolveOperator(HASH_CODE, ImmutableList.of(keyType)).getMethodHandle(); + MethodHandle valueHashCodeFunction = functionRegistry.resolveOperator(HASH_CODE, ImmutableList.of(valueType)).getMethodHandle(); + + MethodHandle methodHandle = methodHandle(MapHashCodeOperator.class, "hash", MethodHandle.class, MethodHandle.class, Type.class, Type.class, Slice.class); + MethodHandle method = methodHandle.bindTo(keyHashCodeFunction).bindTo(valueHashCodeFunction).bindTo(keyType).bindTo(valueType); + return operatorInfo(HASH_CODE, RETURN_TYPE, ImmutableList.of(typeSignature), method, false, ImmutableList.of(false)); + } + + public static long hash(MethodHandle keyHashCodeFunction, MethodHandle valueHashCodeFunction, Type keyType, Type valueType, Slice slice) + { + Block block = readStructuralBlock(slice); + long result = 0; + for (int position = 0; position < block.getPositionCount(); position += 2) { + result += hashPosition(keyHashCodeFunction, keyType, block, position); + result += hashPosition(valueHashCodeFunction, valueType, block, position + 1); + } + return result; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/MapKeys.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/MapKeys.java new file mode 100644 index 00000000..15deec88 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/MapKeys.java @@ -0,0 +1,92 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.ParametricScalar; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.block.BlockBuilderStatus; +import com.facebook.presto.spi.block.VariableWidthBlockBuilder; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; + +import java.lang.invoke.MethodHandle; +import java.util.Map; + +import static com.facebook.presto.metadata.Signature.typeParameter; +import static com.facebook.presto.type.TypeUtils.buildStructuralSlice; +import static com.facebook.presto.type.TypeUtils.parameterizedTypeName; +import static com.facebook.presto.type.TypeUtils.readStructuralBlock; +import static com.facebook.presto.util.Reflection.methodHandle; +import static com.google.common.base.Preconditions.checkArgument; + +public class MapKeys + extends ParametricScalar +{ + public static final MapKeys MAP_KEYS = new MapKeys(); + private static final Signature SIGNATURE = new Signature("map_keys", ImmutableList.of(typeParameter("K"), typeParameter("V")), "array", ImmutableList.of("map"), false, false); + private static final MethodHandle METHOD_HANDLE = methodHandle(MapKeys.class, "getKeys", Type.class, Slice.class); + + @Override + public Signature getSignature() + { + return SIGNATURE; + } + + @Override + public boolean isHidden() + { + return false; + } + + @Override + public boolean isDeterministic() + { + return true; + } + + @Override + public String getDescription() + { + return "Returns the keys of the given map as an array"; + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + checkArgument(arity == 1, "map_keys expects only one argument"); + Type keyType = types.get("K"); + Type valueType = types.get("V"); + MethodHandle methodHandle = METHOD_HANDLE.bindTo(keyType); + Signature signature = new Signature("map_keys", + parameterizedTypeName("array", keyType.getTypeSignature()), + parameterizedTypeName("map", keyType.getTypeSignature(), valueType.getTypeSignature())); + return new FunctionInfo(signature, getDescription(), isHidden(), methodHandle, isDeterministic(), true, ImmutableList.of(false)); + } + + public static Slice getKeys(Type keyType, Slice map) + { + Block block = readStructuralBlock(map); + BlockBuilder blockBuilder = new VariableWidthBlockBuilder(new BlockBuilderStatus(), block.getSizeInBytes()); + for (int i = 0; i < block.getPositionCount(); i += 2) { + keyType.appendTo(block, i, blockBuilder); + } + return buildStructuralSlice(blockBuilder); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/MapNotEqualOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/MapNotEqualOperator.java new file mode 100644 index 00000000..e16d1d22 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/MapNotEqualOperator.java @@ -0,0 +1,75 @@ +package com.facebook.presto.operator.scalar; +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.ParametricOperator; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.spi.type.TypeSignature; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; + +import java.lang.invoke.MethodHandle; +import java.util.Map; + +import static com.facebook.presto.metadata.FunctionRegistry.operatorInfo; +import static com.facebook.presto.metadata.OperatorType.EQUAL; +import static com.facebook.presto.metadata.OperatorType.HASH_CODE; +import static com.facebook.presto.metadata.OperatorType.NOT_EQUAL; +import static com.facebook.presto.metadata.Signature.comparableTypeParameter; +import static com.facebook.presto.spi.type.TypeSignature.parseTypeSignature; +import static com.facebook.presto.util.Reflection.methodHandle; + +public class MapNotEqualOperator + extends ParametricOperator +{ + public static final MapNotEqualOperator MAP_NOT_EQUAL = new MapNotEqualOperator(); + private static final TypeSignature RETURN_TYPE = parseTypeSignature(StandardTypes.BOOLEAN); + + private MapNotEqualOperator() + { + super(NOT_EQUAL, ImmutableList.of(comparableTypeParameter("K"), comparableTypeParameter("V")), StandardTypes.BOOLEAN, ImmutableList.of("map", "map")); + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + Type keyType = types.get("K"); + Type valueType = types.get("V"); + + Type type = typeManager.getParameterizedType(StandardTypes.MAP, ImmutableList.of(keyType.getTypeSignature(), valueType.getTypeSignature()), ImmutableList.of()); + TypeSignature typeSignature = type.getTypeSignature(); + + MethodHandle keyEqualsFunction = functionRegistry.resolveOperator(EQUAL, ImmutableList.of(keyType, keyType)).getMethodHandle(); + MethodHandle keyHashcodeFunction = functionRegistry.resolveOperator(HASH_CODE, ImmutableList.of(keyType)).getMethodHandle(); + MethodHandle valueEqualsFunction = functionRegistry.resolveOperator(EQUAL, ImmutableList.of(valueType, valueType)).getMethodHandle(); + + MethodHandle methodHandle = methodHandle(MapNotEqualOperator.class, "notEqual", MethodHandle.class, MethodHandle.class, MethodHandle.class, Type.class, Type.class, Slice.class, Slice.class); + MethodHandle method = methodHandle.bindTo(keyEqualsFunction).bindTo(keyHashcodeFunction).bindTo(valueEqualsFunction).bindTo(keyType).bindTo(valueType); + return operatorInfo(NOT_EQUAL, RETURN_TYPE, ImmutableList.of(typeSignature, typeSignature), method, true, ImmutableList.of(false, false)); + } + + public static Boolean notEqual(MethodHandle keyEqualsFunction, MethodHandle keyHashcodeFunction, MethodHandle valueEqualsFunction, Type keyType, Type valueType, Slice left, Slice right) + { + Boolean equals = MapEqualOperator.equals(keyEqualsFunction, keyHashcodeFunction, valueEqualsFunction, keyType, valueType, left, right); + if (equals == null) { + return null; + } + + return !equals; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/MapSubscriptOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/MapSubscriptOperator.java new file mode 100644 index 00000000..e8fb6522 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/MapSubscriptOperator.java @@ -0,0 +1,222 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.OperatorType; +import com.facebook.presto.metadata.ParametricOperator; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; + +import java.lang.invoke.MethodHandle; +import java.util.Map; + +import static com.facebook.presto.metadata.OperatorType.SUBSCRIPT; +import static com.facebook.presto.metadata.Signature.typeParameter; +import static com.facebook.presto.spi.StandardErrorCode.INTERNAL_ERROR; +import static com.facebook.presto.type.TypeUtils.castValue; +import static com.facebook.presto.type.TypeUtils.parameterizedTypeName; +import static com.facebook.presto.type.TypeUtils.readStructuralBlock; +import static java.lang.invoke.MethodHandles.lookup; + +public class MapSubscriptOperator + extends ParametricOperator +{ + public static final MapSubscriptOperator MAP_SUBSCRIPT = new MapSubscriptOperator(); + + protected MapSubscriptOperator() + { + super(SUBSCRIPT, ImmutableList.of(typeParameter("K"), typeParameter("V")), "V", ImmutableList.of("map", "K")); + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + Type keyType = types.get("K"); + Type valueType = types.get("V"); + + MethodHandle keyEqualsMethod = functionRegistry.resolveOperator(OperatorType.EQUAL, ImmutableList.of(keyType, keyType)).getMethodHandle(); + + MethodHandle methodHandle = lookupMethod(keyType, valueType); + methodHandle = methodHandle.bindTo(keyEqualsMethod).bindTo(keyType).bindTo(valueType); + + Signature signature = new Signature(SUBSCRIPT.name(), valueType.getTypeSignature(), parameterizedTypeName("map", keyType.getTypeSignature(), valueType.getTypeSignature()), keyType.getTypeSignature()); + return new FunctionInfo(signature, "Map subscript", true, methodHandle, true, true, ImmutableList.of(false, false)); + } + + private static MethodHandle lookupMethod(Type keyType, Type valueType) + { + String methodName = keyType.getJavaType().getSimpleName(); + methodName += valueType.getJavaType().getSimpleName(); + methodName += "Subscript"; + try { + return lookup().unreflect(MapSubscriptOperator.class.getMethod(methodName, MethodHandle.class, Type.class, Type.class, Slice.class, keyType.getJavaType())); + } + catch (IllegalAccessException | NoSuchMethodException e) { + throw Throwables.propagate(e); + } + } + + public static void longvoidSubscript(MethodHandle keyEqualsMethod, Type keyType, Type valueType, Slice map, long key) + { + } + + public static void SlicevoidSubscript(MethodHandle keyEqualsMethod, Type keyType, Type valueType, Slice map, Slice key) + { + } + + public static void booleanvoidSubscript(MethodHandle keyEqualsMethod, Type keyType, Type valueType, Slice map, boolean key) + { + } + + public static void doublevoidSubscript(MethodHandle keyEqualsMethod, Type keyType, Type valueType, Slice map, double key) + { + } + + public static Long SlicelongSubscript(MethodHandle keyEqualsMethod, Type keyType, Type valueType, Slice map, Slice key) + { + return subscript(keyEqualsMethod, keyType, valueType, map, key); + } + + public static Boolean SlicebooleanSubscript(MethodHandle keyEqualsMethod, Type keyType, Type valueType, Slice map, Slice key) + { + return subscript(keyEqualsMethod, keyType, valueType, map, key); + } + + public static Double SlicedoubleSubscript(MethodHandle keyEqualsMethod, Type keyType, Type valueType, Slice map, Slice key) + { + return subscript(keyEqualsMethod, keyType, valueType, map, key); + } + + public static Slice SliceSliceSubscript(MethodHandle keyEqualsMethod, Type keyType, Type valueType, Slice map, Slice key) + { + return subscript(keyEqualsMethod, keyType, valueType, map, key); + } + + public static Long doublelongSubscript(MethodHandle keyEqualsMethod, Type keyType, Type valueType, Slice map, double key) + { + return subscript(keyEqualsMethod, keyType, valueType, map, key); + } + + public static Boolean doublebooleanSubscript(MethodHandle keyEqualsMethod, Type keyType, Type valueType, Slice map, double key) + { + return subscript(keyEqualsMethod, keyType, valueType, map, key); + } + + public static Double doubledoubleSubscript(MethodHandle keyEqualsMethod, Type keyType, Type valueType, Slice map, double key) + { + return subscript(keyEqualsMethod, keyType, valueType, map, key); + } + + public static Slice doubleSliceSubscript(MethodHandle keyEqualsMethod, Type keyType, Type valueType, Slice map, double key) + { + return subscript(keyEqualsMethod, keyType, valueType, map, key); + } + + public static Long booleanlongSubscript(MethodHandle keyEqualsMethod, Type keyType, Type valueType, Slice map, boolean key) + { + return subscript(keyEqualsMethod, keyType, valueType, map, key); + } + + public static Boolean booleanbooleanSubscript(MethodHandle keyEqualsMethod, Type keyType, Type valueType, Slice map, boolean key) + { + return subscript(keyEqualsMethod, keyType, valueType, map, key); + } + + public static Double booleandoubleSubscript(MethodHandle keyEqualsMethod, Type keyType, Type valueType, Slice map, boolean key) + { + return subscript(keyEqualsMethod, keyType, valueType, map, key); + } + + public static Slice booleanSliceSubscript(MethodHandle keyEqualsMethod, Type keyType, Type valueType, Slice map, boolean key) + { + return subscript(keyEqualsMethod, keyType, valueType, map, key); + } + + public static Long longlongSubscript(MethodHandle keyEqualsMethod, Type keyType, Type valueType, Slice map, long key) + { + return subscript(keyEqualsMethod, keyType, valueType, map, key); + } + + public static Boolean longbooleanSubscript(MethodHandle keyEqualsMethod, Type keyType, Type valueType, Slice map, long key) + { + return subscript(keyEqualsMethod, keyType, valueType, map, key); + } + + public static Double longdoubleSubscript(MethodHandle keyEqualsMethod, Type keyType, Type valueType, Slice map, long key) + { + return subscript(keyEqualsMethod, keyType, valueType, map, key); + } + + public static Slice longSliceSubscript(MethodHandle keyEqualsMethod, Type keyType, Type valueType, Slice map, long key) + { + return subscript(keyEqualsMethod, keyType, valueType, map, key); + } + + @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) + private static T subscript(MethodHandle keyEqualsMethod, Type keyType, Type valueType, Slice map, Object key) + { + Block block = readStructuralBlock(map); + + int position = 0; + + Class keyTypeJavaType = keyType.getJavaType(); + + for (; position < block.getPositionCount(); position += 2) { + try { + boolean equals; + if (keyTypeJavaType == long.class) { + equals = (boolean) keyEqualsMethod.invokeExact(keyType.getLong(block, position), (long) key); + } + else if (keyTypeJavaType == double.class) { + equals = (boolean) keyEqualsMethod.invokeExact(keyType.getDouble(block, position), (double) key); + } + else if (keyTypeJavaType == boolean.class) { + equals = (boolean) keyEqualsMethod.invokeExact(keyType.getBoolean(block, position), (boolean) key); + } + else if (keyTypeJavaType == Slice.class) { + equals = (boolean) keyEqualsMethod.invokeExact(keyType.getSlice(block, position), (Slice) key); + } + else { + throw new IllegalArgumentException("Unsupported type: " + keyTypeJavaType.getSimpleName()); + } + + if (equals) { + break; + } + } + catch (Throwable t) { + Throwables.propagateIfInstanceOf(t, Error.class); + Throwables.propagateIfInstanceOf(t, PrestoException.class); + throw new PrestoException(INTERNAL_ERROR, t); + } + } + + if (position == block.getPositionCount()) { + // key not found + return null; + } + + position += 1; // value position + + return (T) castValue(valueType, block, position); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/MapToJsonCast.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/MapToJsonCast.java new file mode 100644 index 00000000..20aa6c5c --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/MapToJsonCast.java @@ -0,0 +1,109 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.OperatorType; +import com.facebook.presto.metadata.ParametricOperator; +import com.facebook.presto.server.SliceSerializer; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.type.MapType; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import io.airlift.json.ObjectMapperProvider; +import io.airlift.slice.Slice; +import io.airlift.slice.Slices; + +import java.io.IOException; +import java.lang.invoke.MethodHandle; +import java.util.Map; +import java.util.TreeMap; + +import static com.facebook.presto.metadata.FunctionRegistry.operatorInfo; +import static com.facebook.presto.metadata.Signature.typeParameter; +import static com.facebook.presto.spi.type.TypeSignature.parseTypeSignature; +import static com.facebook.presto.type.TypeUtils.createBlock; +import static com.facebook.presto.util.Reflection.methodHandle; +import static com.google.common.base.Preconditions.checkArgument; + +public class MapToJsonCast + extends ParametricOperator +{ + public static final MapToJsonCast MAP_TO_JSON = new MapToJsonCast(); + private static final Supplier OBJECT_MAPPER = Suppliers.memoize(() -> new ObjectMapperProvider().get().registerModule(new SimpleModule().addSerializer(Slice.class, new SliceSerializer()).addSerializer(Map.class, new MapSerializer()))); + private static final MethodHandle METHOD_HANDLE = methodHandle(MapToJsonCast.class, "toJson", Type.class, Type.class, ConnectorSession.class, Slice.class); + + private MapToJsonCast() + { + super(OperatorType.CAST, ImmutableList.of(typeParameter("K"), typeParameter("V")), StandardTypes.JSON, ImmutableList.of("map")); + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + checkArgument(arity == 1, "Expected arity to be 1"); + Type keyType = types.get("K"); + Type valueType = types.get("V"); + Type mapType = typeManager.getParameterizedType(StandardTypes.MAP, ImmutableList.of(keyType.getTypeSignature(), valueType.getTypeSignature()), ImmutableList.of()); + + MethodHandle methodHandle = METHOD_HANDLE.bindTo(keyType); + methodHandle = methodHandle.bindTo(valueType); + + return operatorInfo(OperatorType.CAST, parseTypeSignature(StandardTypes.JSON), ImmutableList.of(mapType.getTypeSignature()), methodHandle, false, ImmutableList.of(false)); + } + + public static Slice toJson(Type keyType, Type valueType, ConnectorSession session, Slice slice) + { + MapType mapType = new MapType(keyType, valueType); + Object object = mapType.getObjectValue(session, createBlock(mapType, slice), 0); + try { + return Slices.utf8Slice(OBJECT_MAPPER.get().writeValueAsString(object)); + } + catch (JsonProcessingException e) { + throw Throwables.propagate(e); + } + } + + // Unfortunately this has to be a raw Map, since Map doesn't seem to work in Jackson + private static class MapSerializer + extends JsonSerializer + { + @Override + public void serialize(Map map, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) + throws IOException + { + Map orderedMap = new TreeMap<>(); + for (Map.Entry entry : ((Map) map).entrySet()) { + orderedMap.put(entry.getKey().toString(), entry.getValue()); + } + jsonGenerator.writeStartObject(); + for (Map.Entry entry : orderedMap.entrySet()) { + jsonGenerator.writeObjectField(entry.getKey(), entry.getValue()); + } + jsonGenerator.writeEndObject(); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/MapValues.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/MapValues.java new file mode 100644 index 00000000..cdcc9351 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/MapValues.java @@ -0,0 +1,92 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.ParametricScalar; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.block.BlockBuilderStatus; +import com.facebook.presto.spi.block.VariableWidthBlockBuilder; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; + +import java.lang.invoke.MethodHandle; +import java.util.Map; + +import static com.facebook.presto.metadata.Signature.typeParameter; +import static com.facebook.presto.type.TypeUtils.buildStructuralSlice; +import static com.facebook.presto.type.TypeUtils.parameterizedTypeName; +import static com.facebook.presto.type.TypeUtils.readStructuralBlock; +import static com.facebook.presto.util.Reflection.methodHandle; +import static com.google.common.base.Preconditions.checkArgument; + +public class MapValues + extends ParametricScalar +{ + public static final MapValues MAP_VALUES = new MapValues(); + private static final Signature SIGNATURE = new Signature("map_values", ImmutableList.of(typeParameter("K"), typeParameter("V")), "array", ImmutableList.of("map"), false, false); + private static final MethodHandle METHOD_HANDLE = methodHandle(MapValues.class, "getValues", Type.class, Slice.class); + + @Override + public Signature getSignature() + { + return SIGNATURE; + } + + @Override + public boolean isHidden() + { + return false; + } + + @Override + public boolean isDeterministic() + { + return true; + } + + @Override + public String getDescription() + { + return "Returns the values of the given map as an array"; + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + checkArgument(arity == 1, "map_values expects only one argument"); + Type keyType = types.get("K"); + Type valueType = types.get("V"); + MethodHandle methodHandle = METHOD_HANDLE.bindTo(valueType); + Signature signature = new Signature("map_values", + parameterizedTypeName("array", valueType.getTypeSignature()), + parameterizedTypeName("map", keyType.getTypeSignature(), valueType.getTypeSignature())); + return new FunctionInfo(signature, getDescription(), isHidden(), methodHandle, isDeterministic(), true, ImmutableList.of(false)); + } + + public static Slice getValues(Type valueType, Slice map) + { + Block block = readStructuralBlock(map); + BlockBuilder blockBuilder = new VariableWidthBlockBuilder(new BlockBuilderStatus(), block.getSizeInBytes()); + for (int i = 0; i < block.getPositionCount(); i += 2) { + valueType.appendTo(block, i + 1, blockBuilder); + } + return buildStructuralSlice(blockBuilder); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/MathFunctions.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/MathFunctions.java new file mode 100644 index 00000000..a9fe5560 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/MathFunctions.java @@ -0,0 +1,384 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import com.facebook.presto.operator.Description; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.type.SqlType; +import com.google.common.primitives.Doubles; +import io.airlift.slice.Slice; + +import java.util.concurrent.ThreadLocalRandom; + +import static com.facebook.presto.spi.StandardErrorCode.INVALID_FUNCTION_ARGUMENT; +import static com.facebook.presto.util.Failures.checkCondition; +import static io.airlift.slice.Slices.utf8Slice; +import static java.lang.Character.MAX_RADIX; +import static java.lang.Character.MIN_RADIX; +import static java.lang.String.format; + +public final class MathFunctions +{ + private MathFunctions() {} + + @Description("absolute value") + @ScalarFunction + @SqlType(StandardTypes.BIGINT) + public static long abs(@SqlType(StandardTypes.BIGINT) long num) + { + return Math.abs(num); + } + + @Description("absolute value") + @ScalarFunction + @SqlType(StandardTypes.DOUBLE) + public static double abs(@SqlType(StandardTypes.DOUBLE) double num) + { + return Math.abs(num); + } + + @Description("arc cosine") + @ScalarFunction + @SqlType(StandardTypes.DOUBLE) + public static double acos(@SqlType(StandardTypes.DOUBLE) double num) + { + return Math.acos(num); + } + + @Description("arc sine") + @ScalarFunction + @SqlType(StandardTypes.DOUBLE) + public static double asin(@SqlType(StandardTypes.DOUBLE) double num) + { + return Math.asin(num); + } + + @Description("arc tangent") + @ScalarFunction + @SqlType(StandardTypes.DOUBLE) + public static double atan(@SqlType(StandardTypes.DOUBLE) double num) + { + return Math.atan(num); + } + + @Description("arc tangent of given fraction") + @ScalarFunction + @SqlType(StandardTypes.DOUBLE) + public static double atan2(@SqlType(StandardTypes.DOUBLE) double num1, @SqlType(StandardTypes.DOUBLE) double num2) + { + return Math.atan2(num1, num2); + } + + @Description("cube root") + @ScalarFunction + @SqlType(StandardTypes.DOUBLE) + public static double cbrt(@SqlType(StandardTypes.DOUBLE) double num) + { + return Math.cbrt(num); + } + + @Description("round up to nearest integer") + @ScalarFunction(alias = "ceil") + @SqlType(StandardTypes.BIGINT) + public static long ceiling(@SqlType(StandardTypes.BIGINT) long num) + { + return num; + } + + @Description("round up to nearest integer") + @ScalarFunction(alias = "ceil") + @SqlType(StandardTypes.DOUBLE) + public static double ceiling(@SqlType(StandardTypes.DOUBLE) double num) + { + return Math.ceil(num); + } + + @Description("cosine") + @ScalarFunction + @SqlType(StandardTypes.DOUBLE) + public static double cos(@SqlType(StandardTypes.DOUBLE) double num) + { + return Math.cos(num); + } + + @Description("hyperbolic cosine") + @ScalarFunction + @SqlType(StandardTypes.DOUBLE) + public static double cosh(@SqlType(StandardTypes.DOUBLE) double num) + { + return Math.cosh(num); + } + + @Description("converts an angle in radians to degrees") + @ScalarFunction + @SqlType(StandardTypes.DOUBLE) + public static double degrees(@SqlType(StandardTypes.DOUBLE) double radians) + { + return Math.toDegrees(radians); + } + + @Description("Euler's number") + @ScalarFunction + @SqlType(StandardTypes.DOUBLE) + public static double e() + { + return Math.E; + } + + @Description("Euler's number raised to the given power") + @ScalarFunction + @SqlType(StandardTypes.DOUBLE) + public static double exp(@SqlType(StandardTypes.DOUBLE) double num) + { + return Math.exp(num); + } + + @Description("round down to nearest integer") + @ScalarFunction + @SqlType(StandardTypes.BIGINT) + public static long floor(@SqlType(StandardTypes.BIGINT) long num) + { + return num; + } + + @Description("round down to nearest integer") + @ScalarFunction + @SqlType(StandardTypes.DOUBLE) + public static double floor(@SqlType(StandardTypes.DOUBLE) double num) + { + return Math.floor(num); + } + + @Description("natural logarithm") + @ScalarFunction + @SqlType(StandardTypes.DOUBLE) + public static double ln(@SqlType(StandardTypes.DOUBLE) double num) + { + return Math.log(num); + } + + @Description("logarithm to base 2") + @ScalarFunction + @SqlType(StandardTypes.DOUBLE) + public static double log2(@SqlType(StandardTypes.DOUBLE) double num) + { + return Math.log(num) / Math.log(2); + } + + @Description("logarithm to base 10") + @ScalarFunction + @SqlType(StandardTypes.DOUBLE) + public static double log10(@SqlType(StandardTypes.DOUBLE) double num) + { + return Math.log10(num); + } + + @Description("logarithm to given base") + @ScalarFunction + @SqlType(StandardTypes.DOUBLE) + public static double log(@SqlType(StandardTypes.DOUBLE) double num, @SqlType(StandardTypes.DOUBLE) double base) + { + return Math.log(num) / Math.log(base); + } + + @Description("remainder of given quotient") + @ScalarFunction + @SqlType(StandardTypes.BIGINT) + public static long mod(@SqlType(StandardTypes.BIGINT) long num1, @SqlType(StandardTypes.BIGINT) long num2) + { + return num1 % num2; + } + + @Description("remainder of given quotient") + @ScalarFunction + @SqlType(StandardTypes.DOUBLE) + public static double mod(@SqlType(StandardTypes.DOUBLE) double num1, @SqlType(StandardTypes.DOUBLE) double num2) + { + return num1 % num2; + } + + @Description("the constant Pi") + @ScalarFunction + @SqlType(StandardTypes.DOUBLE) + public static double pi() + { + return Math.PI; + } + + @Description("value raised to the power of exponent") + @ScalarFunction + @SqlType(StandardTypes.DOUBLE) + public static double pow(@SqlType(StandardTypes.DOUBLE) double num, @SqlType(StandardTypes.DOUBLE) double exponent) + { + return Math.pow(num, exponent); + } + + @Description("converts an angle in degrees to radians") + @ScalarFunction + @SqlType(StandardTypes.DOUBLE) + public static double radians(@SqlType(StandardTypes.DOUBLE) double degrees) + { + return Math.toRadians(degrees); + } + + @Description("a pseudo-random value") + @ScalarFunction(alias = "rand", deterministic = false) + @SqlType(StandardTypes.DOUBLE) + public static double random() + { + return ThreadLocalRandom.current().nextDouble(); + } + + @Description("round to nearest integer") + @ScalarFunction + @SqlType(StandardTypes.BIGINT) + public static long round(@SqlType(StandardTypes.BIGINT) long num) + { + return round(num, 0); + } + + @Description("round to nearest integer") + @ScalarFunction + @SqlType(StandardTypes.BIGINT) + public static long round(@SqlType(StandardTypes.BIGINT) long num, @SqlType(StandardTypes.BIGINT) long decimals) + { + return num; + } + + @Description("round to nearest integer") + @ScalarFunction + @SqlType(StandardTypes.DOUBLE) + public static double round(@SqlType(StandardTypes.DOUBLE) double num) + { + return round(num, 0); + } + + @Description("round to given number of decimal places") + @ScalarFunction + @SqlType(StandardTypes.DOUBLE) + public static double round(@SqlType(StandardTypes.DOUBLE) double num, @SqlType(StandardTypes.BIGINT) long decimals) + { + if (num == 0.0) { + return 0; + } + if (num < 0) { + return -round(-num, decimals); + } + + double factor = Math.pow(10, decimals); + return Math.floor(num * factor + 0.5) / factor; + } + + @Description("sine") + @ScalarFunction + @SqlType(StandardTypes.DOUBLE) + public static double sin(@SqlType(StandardTypes.DOUBLE) double num) + { + return Math.sin(num); + } + + @Description("square root") + @ScalarFunction + @SqlType(StandardTypes.DOUBLE) + public static double sqrt(@SqlType(StandardTypes.DOUBLE) double num) + { + return Math.sqrt(num); + } + + @Description("tangent") + @ScalarFunction + @SqlType(StandardTypes.DOUBLE) + public static double tan(@SqlType(StandardTypes.DOUBLE) double num) + { + return Math.tan(num); + } + + @Description("hyperbolic tangent") + @ScalarFunction + @SqlType(StandardTypes.DOUBLE) + public static double tanh(@SqlType(StandardTypes.DOUBLE) double num) + { + return Math.tanh(num); + } + + @Description("test if value is not-a-number") + @ScalarFunction("is_nan") + @SqlType(StandardTypes.BOOLEAN) + public static boolean isNaN(@SqlType(StandardTypes.DOUBLE) double num) + { + return Double.isNaN(num); + } + + @Description("test if value is finite") + @ScalarFunction + @SqlType(StandardTypes.BOOLEAN) + public static boolean isFinite(@SqlType(StandardTypes.DOUBLE) double num) + { + return Doubles.isFinite(num); + } + + @Description("test if value is infinite") + @ScalarFunction + @SqlType(StandardTypes.BOOLEAN) + public static boolean isInfinite(@SqlType(StandardTypes.DOUBLE) double num) + { + return Double.isInfinite(num); + } + + @Description("constant representing not-a-number") + @ScalarFunction("nan") + @SqlType(StandardTypes.DOUBLE) + public static double NaN() + { + return Double.NaN; + } + + @Description("Infinity") + @ScalarFunction + @SqlType(StandardTypes.DOUBLE) + public static double infinity() + { + return Double.POSITIVE_INFINITY; + } + + @Description("convert a number to a string in the given base") + @ScalarFunction + @SqlType(StandardTypes.VARCHAR) + public static Slice toBase(@SqlType(StandardTypes.BIGINT) long value, @SqlType(StandardTypes.BIGINT) long radix) + { + checkRadix(radix); + return utf8Slice(Long.toString(value, (int) radix)); + } + + @Description("convert a string in the given base to a number") + @ScalarFunction + @SqlType(StandardTypes.BIGINT) + public static long fromBase(@SqlType(StandardTypes.VARCHAR) Slice value, @SqlType(StandardTypes.BIGINT) long radix) + { + checkRadix(radix); + try { + return Long.parseLong(value.toStringUtf8(), (int) radix); + } + catch (NumberFormatException e) { + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, format("Not a valid base-%d number: %s", radix, value.toStringUtf8()), e); + } + } + + private static void checkRadix(long radix) + { + checkCondition(radix >= MIN_RADIX && radix <= MAX_RADIX, + INVALID_FUNCTION_ARGUMENT, "Radix must be between %d and %d", MIN_RADIX, MAX_RADIX); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/QuarterOfYearDateTimeField.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/QuarterOfYearDateTimeField.java new file mode 100644 index 00000000..774fc0f5 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/QuarterOfYearDateTimeField.java @@ -0,0 +1,74 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import org.joda.time.Chronology; +import org.joda.time.DateTimeField; +import org.joda.time.DateTimeFieldType; +import org.joda.time.DurationField; +import org.joda.time.DurationFieldType; +import org.joda.time.field.DividedDateTimeField; +import org.joda.time.field.OffsetDateTimeField; +import org.joda.time.field.ScaledDurationField; + +// Forked from org.elasticsearch.common.joda.Joda +public final class QuarterOfYearDateTimeField + extends DateTimeFieldType +{ + private static final long serialVersionUID = -5677872459807379123L; + + private static final DurationFieldType QUARTER_OF_YEAR_DURATION_FIELD_TYPE = new QuarterOfYearDurationFieldType(); + + public static final DateTimeFieldType QUARTER_OF_YEAR = new QuarterOfYearDateTimeField(); + + private QuarterOfYearDateTimeField() + { + super("quarterOfYear"); + } + + @Override + public DurationFieldType getDurationType() + { + return QUARTER_OF_YEAR_DURATION_FIELD_TYPE; + } + + @Override + public DurationFieldType getRangeDurationType() + { + return DurationFieldType.years(); + } + + @Override + public DateTimeField getField(Chronology chronology) + { + return new OffsetDateTimeField(new DividedDateTimeField(new OffsetDateTimeField(chronology.monthOfYear(), -1), QUARTER_OF_YEAR, 3), 1); + } + + private static class QuarterOfYearDurationFieldType + extends DurationFieldType + { + private static final long serialVersionUID = -8167713675442491871L; + + public QuarterOfYearDurationFieldType() + { + super("quarters"); + } + + @Override + public DurationField getField(Chronology chronology) + { + return new ScaledDurationField(chronology.months(), QUARTER_OF_YEAR_DURATION_FIELD_TYPE, 3); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/RegexpFunctions.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/RegexpFunctions.java new file mode 100644 index 00000000..278ec404 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/RegexpFunctions.java @@ -0,0 +1,315 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import com.facebook.presto.metadata.OperatorType; +import com.facebook.presto.operator.Description; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.block.BlockBuilderStatus; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.type.RegexpType; +import com.facebook.presto.type.SqlType; +import com.google.common.primitives.Ints; +import io.airlift.jcodings.specific.NonStrictUTF8Encoding; +import io.airlift.joni.Matcher; +import io.airlift.joni.Option; +import io.airlift.joni.Regex; +import io.airlift.joni.Region; +import io.airlift.joni.Syntax; +import io.airlift.joni.exception.ValueException; +import io.airlift.slice.DynamicSliceOutput; +import io.airlift.slice.Slice; +import io.airlift.slice.SliceOutput; +import io.airlift.slice.Slices; + +import javax.annotation.Nullable; + +import java.nio.charset.StandardCharsets; + +import static com.facebook.presto.spi.StandardErrorCode.INVALID_FUNCTION_ARGUMENT; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static com.facebook.presto.type.TypeUtils.buildStructuralSlice; +import static java.lang.String.format; + +public final class RegexpFunctions +{ + private RegexpFunctions() + { + } + + @ScalarOperator(OperatorType.CAST) + @SqlType(RegexpType.NAME) + public static Regex castToRegexp(@SqlType(StandardTypes.VARCHAR) Slice pattern) + { + Regex regex; + try { + // When normal UTF8 encoding instead of non-strict UTF8) is used, joni can infinite loop when invalid UTF8 slice is supplied to it. + regex = new Regex(pattern.getBytes(), 0, pattern.length(), Option.DEFAULT, NonStrictUTF8Encoding.INSTANCE, Syntax.Java); + } + catch (Exception e) { + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, e); + } + return regex; + } + + @Description("returns substrings matching a regular expression") + @ScalarFunction + @SqlType(StandardTypes.BOOLEAN) + public static boolean regexpLike(@SqlType(StandardTypes.VARCHAR) Slice source, @SqlType(RegexpType.NAME) Regex pattern) + { + Matcher m = pattern.matcher(source.getBytes()); + int offset = m.search(0, source.length(), Option.DEFAULT); + return offset != -1; + } + + @Description("removes substrings matching a regular expression") + @ScalarFunction + @SqlType(StandardTypes.VARCHAR) + public static Slice regexpReplace(@SqlType(StandardTypes.VARCHAR) Slice source, @SqlType(RegexpType.NAME) Regex pattern) + { + return regexpReplace(source, pattern, Slices.EMPTY_SLICE); + } + + @Description("replaces substrings matching a regular expression by given string") + @ScalarFunction + @SqlType(StandardTypes.VARCHAR) + public static Slice regexpReplace(@SqlType(StandardTypes.VARCHAR) Slice source, @SqlType(RegexpType.NAME) Regex pattern, @SqlType(StandardTypes.VARCHAR) Slice replacement) + { + Matcher matcher = pattern.matcher(source.getBytes()); + SliceOutput sliceOutput = new DynamicSliceOutput(source.length() + replacement.length() * 5); + + int lastEnd = 0; + int nextStart = 0; // nextStart is the same as lastEnd, unless the last match was zero-width. In such case, nextStart is lastEnd + 1. + while (true) { + int offset = matcher.search(nextStart, source.length(), Option.DEFAULT); + if (offset == -1) { + break; + } + if (matcher.getEnd() == matcher.getBegin()) { + nextStart = matcher.getEnd() + 1; + } + else { + nextStart = matcher.getEnd(); + } + Slice sliceBetweenReplacements = source.slice(lastEnd, matcher.getBegin() - lastEnd); + lastEnd = matcher.getEnd(); + sliceOutput.appendBytes(sliceBetweenReplacements); + appendReplacement(sliceOutput, source, pattern, matcher.getEagerRegion(), replacement); + } + sliceOutput.appendBytes(source.slice(lastEnd, source.length() - lastEnd)); + + return sliceOutput.slice(); + } + + private static void appendReplacement(SliceOutput result, Slice source, Regex pattern, Region region, Slice replacement) + { + // Handle the following items: + // 1. ${name}; + // 2. $0, $1, $123 (group 123, if exists; or group 12, if exists; or group 1); + // 3. \\, \$, \t (literal 't'). + // 4. Anything that doesn't starts with \ or $ is considered regular bytes + + int idx = 0; + + while (idx < replacement.length()) { + byte nextByte = replacement.getByte(idx); + if (nextByte == '$') { + idx++; + if (idx == replacement.length()) { // not using checkArgument because `.toStringUtf8` is expensive + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, "Illegal replacement sequence: " + replacement.toStringUtf8()); + } + nextByte = replacement.getByte(idx); + int backref; + if (nextByte == '{') { // case 1 in the above comment + idx++; + int startCursor = idx; + while (idx < replacement.length()) { + nextByte = replacement.getByte(idx); + if (nextByte == '}') { + break; + } + idx++; + } + byte[] groupName = replacement.getBytes(startCursor, idx - startCursor); + try { + backref = pattern.nameToBackrefNumber(groupName, 0, groupName.length, region); + } + catch (ValueException e) { + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, "Illegal replacement sequence: unknown group { " + new String(groupName, StandardCharsets.UTF_8) + " }"); + } + idx++; + } + else { // case 2 in the above comment + backref = nextByte - '0'; + if (backref < 0 || backref > 9) { // not using checkArgument because `.toStringUtf8` is expensive + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, "Illegal replacement sequence: " + replacement.toStringUtf8()); + } + if (region.numRegs <= backref) { + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, "Illegal replacement sequence: unknown group " + backref); + } + idx++; + while (idx < replacement.length()) { // Adaptive group number: find largest group num that is not greater than actual number of groups + int nextDigit = replacement.getByte(idx) - '0'; + if (nextDigit < 0 || nextDigit > 9) { + break; + } + int newBackref = (backref * 10) + nextDigit; + if (region.numRegs <= newBackref) { + break; + } + backref = newBackref; + idx++; + } + } + int beg = region.beg[backref]; + int end = region.end[backref]; + if (beg != -1 && end != -1) { // the specific group doesn't exist in the current match, skip + result.appendBytes(source.slice(beg, end - beg)); + } + } + else { // case 3 and 4 in the above comment + if (nextByte == '\\') { + idx++; + if (idx == replacement.length()) { // not using checkArgument because `.toStringUtf8` is expensive + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, "Illegal replacement sequence: " + replacement.toStringUtf8()); + } + nextByte = replacement.getByte(idx); + } + result.appendByte(nextByte); + idx++; + } + } + } + + @Description("string(s) extracted using the given pattern") + @ScalarFunction + @SqlType("array") + public static Slice regexpExtractAll(@SqlType(StandardTypes.VARCHAR) Slice source, @SqlType(RegexpType.NAME) Regex pattern) + { + return regexpExtractAll(source, pattern, 0); + } + + @Description("group(s) extracted using the given pattern") + @ScalarFunction + @SqlType("array") + public static Slice regexpExtractAll(@SqlType(StandardTypes.VARCHAR) Slice source, @SqlType(RegexpType.NAME) Regex pattern, @SqlType(StandardTypes.BIGINT) long groupIndex) + { + Matcher matcher = pattern.matcher(source.getBytes()); + validateGroup(groupIndex, matcher.getEagerRegion()); + BlockBuilder blockBuilder = VARCHAR.createBlockBuilder(new BlockBuilderStatus(), 32); + int group = Ints.checkedCast(groupIndex); + + int nextStart = 0; + while (true) { + int offset = matcher.search(nextStart, source.length(), Option.DEFAULT); + if (offset == -1) { + break; + } + if (matcher.getEnd() == matcher.getBegin()) { + nextStart = matcher.getEnd() + 1; + } + else { + nextStart = matcher.getEnd(); + } + Region region = matcher.getEagerRegion(); + int beg = region.beg[group]; + int end = region.end[group]; + if (beg == -1 || end == -1) { + blockBuilder.appendNull(); + } + else { + Slice slice = source.slice(beg, end - beg); + VARCHAR.writeSlice(blockBuilder, slice); + } + } + + return buildStructuralSlice(blockBuilder); + } + + @Nullable + @Description("string extracted using the given pattern") + @ScalarFunction + @SqlType(StandardTypes.VARCHAR) + public static Slice regexpExtract(@SqlType(StandardTypes.VARCHAR) Slice source, @SqlType(RegexpType.NAME) Regex pattern) + { + return regexpExtract(source, pattern, 0); + } + + @Nullable + @Description("returns regex group of extracted string with a pattern") + @ScalarFunction + @SqlType(StandardTypes.VARCHAR) + public static Slice regexpExtract(@SqlType(StandardTypes.VARCHAR) Slice source, @SqlType(RegexpType.NAME) Regex pattern, @SqlType(StandardTypes.BIGINT) long groupIndex) + { + Matcher matcher = pattern.matcher(source.getBytes()); + validateGroup(groupIndex, matcher.getEagerRegion()); + int group = Ints.checkedCast(groupIndex); + + int offset = matcher.search(0, source.length(), Option.DEFAULT); + if (offset == -1) { + return null; + } + Region region = matcher.getEagerRegion(); + int beg = region.beg[group]; + int end = region.end[group]; + if (beg == -1) { + // end == -1 must be true + return null; + } + + Slice slice = source.slice(beg, end - beg); + return slice; + } + + @ScalarFunction + @Description("returns array of strings split by pattern") + @SqlType("array") + public static Slice regexpSplit(@SqlType(StandardTypes.VARCHAR) Slice source, @SqlType(RegexpType.NAME) Regex pattern) + { + Matcher matcher = pattern.matcher(source.getBytes()); + BlockBuilder blockBuilder = VARCHAR.createBlockBuilder(new BlockBuilderStatus(), 32); + + int lastEnd = 0; + int nextStart = 0; + while (true) { + int offset = matcher.search(nextStart, source.length(), Option.DEFAULT); + if (offset == -1) { + break; + } + if (matcher.getEnd() == matcher.getBegin()) { + nextStart = matcher.getEnd() + 1; + } + else { + nextStart = matcher.getEnd(); + } + Slice slice = source.slice(lastEnd, matcher.getBegin() - lastEnd); + lastEnd = matcher.getEnd(); + VARCHAR.writeSlice(blockBuilder, slice); + } + VARCHAR.writeSlice(blockBuilder, source.slice(lastEnd, source.length() - lastEnd)); + + return buildStructuralSlice(blockBuilder); + } + + private static void validateGroup(long group, Region region) + { + if (group < 0) { + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, "Group cannot be negative"); + } + if (group > region.numRegs - 1) { + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, format("Pattern has %d groups. Cannot access group %d", region.numRegs - 1, group)); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/RowEqualOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/RowEqualOperator.java new file mode 100644 index 00000000..686b38bb --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/RowEqualOperator.java @@ -0,0 +1,65 @@ +package com.facebook.presto.operator.scalar; +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.ParametricOperator; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.block.BlockBuilderStatus; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.spi.type.TypeSignature; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; + +import java.lang.invoke.MethodHandle; +import java.util.Map; + +import static com.facebook.presto.metadata.FunctionRegistry.operatorInfo; +import static com.facebook.presto.metadata.OperatorType.EQUAL; +import static com.facebook.presto.metadata.Signature.comparableWithVariadicBound; +import static com.facebook.presto.spi.type.TypeSignature.parseTypeSignature; +import static com.facebook.presto.util.Reflection.methodHandle; + +public class RowEqualOperator + extends ParametricOperator +{ + public static final RowEqualOperator ROW_EQUAL = new RowEqualOperator(); + private static final TypeSignature RETURN_TYPE = parseTypeSignature(StandardTypes.BOOLEAN); + private static final MethodHandle METHOD_HANDLE = methodHandle(RowEqualOperator.class, "equals", Type.class, Slice.class, Slice.class); + + private RowEqualOperator() + { + super(EQUAL, ImmutableList.of(comparableWithVariadicBound("T", "row")), StandardTypes.BOOLEAN, ImmutableList.of("T", "T")); + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + Type type = types.get("T"); + TypeSignature typeSignature = type.getTypeSignature(); + return operatorInfo(EQUAL, RETURN_TYPE, ImmutableList.of(typeSignature, typeSignature), METHOD_HANDLE.bindTo(type), false, ImmutableList.of(false, false)); + } + + public static boolean equals(Type rowType, Slice leftRow, Slice rightRow) + { + BlockBuilder leftBlockBuilder = rowType.createBlockBuilder(new BlockBuilderStatus(), 1, leftRow.length()); + BlockBuilder rightBlockBuilder = rowType.createBlockBuilder(new BlockBuilderStatus(), 1, rightRow.length()); + leftBlockBuilder.writeBytes(leftRow, 0, leftRow.length()); + rightBlockBuilder.writeBytes(rightRow, 0, rightRow.length()); + return rowType.equalTo(leftBlockBuilder.closeEntry().build(), 0, rightBlockBuilder.closeEntry().build(), 0); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/RowFieldReference.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/RowFieldReference.java new file mode 100644 index 00000000..350f3623 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/RowFieldReference.java @@ -0,0 +1,129 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.ParametricScalar; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.type.RowType; +import com.facebook.presto.util.Reflection; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.airlift.slice.Slice; + +import java.lang.invoke.MethodHandle; +import java.util.Map; +import java.util.Optional; + +import static com.facebook.presto.sql.QueryUtil.mangleFieldReference; +import static com.facebook.presto.type.RowType.RowField; +import static com.facebook.presto.type.TypeUtils.readStructuralBlock; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +public class RowFieldReference + extends ParametricScalar +{ + private static final Map METHOD_HANDLE_MAP; + + private final Signature signature; + private final MethodHandle methodHandle; + + static { + ImmutableMap.Builder builder = ImmutableMap.builder(); + builder.put("long", Reflection.methodHandle(RowFieldReference.class, "longAccessor", Type.class, Integer.class, Slice.class)); + builder.put("double", Reflection.methodHandle(RowFieldReference.class, "doubleAccessor", Type.class, Integer.class, Slice.class)); + builder.put("boolean", Reflection.methodHandle(RowFieldReference.class, "booleanAccessor", Type.class, Integer.class, Slice.class)); + builder.put("slice", Reflection.methodHandle(RowFieldReference.class, "sliceAccessor", Type.class, Integer.class, Slice.class)); + METHOD_HANDLE_MAP = builder.build(); + } + + public RowFieldReference(RowType type, String fieldName) + { + Type returnType = null; + int index = 0; + for (RowField field : type.getFields()) { + if (field.getName().equals(Optional.of(fieldName))) { + returnType = field.getType(); + break; + } + index++; + } + checkNotNull(returnType, "%s not found in row type %s", fieldName, type); + signature = new Signature(mangleFieldReference(fieldName), returnType.getTypeSignature(), type.getTypeSignature()); + + String stackType = returnType.getJavaType().getSimpleName().toLowerCase(); + checkState(METHOD_HANDLE_MAP.containsKey(stackType), "method handle missing for %s stack type", stackType); + methodHandle = METHOD_HANDLE_MAP.get(stackType).bindTo(returnType).bindTo(index); + } + + @Override + public Signature getSignature() + { + return signature; + } + + @Override + public boolean isHidden() + { + return true; + } + + @Override + public boolean isDeterministic() + { + return true; + } + + @Override + public String getDescription() + { + return null; + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + checkNotNull(methodHandle, "methodHandle is null"); + return new FunctionInfo(signature, getDescription(), isHidden(), methodHandle, isDeterministic(), true, ImmutableList.of(false)); + } + + public static Long longAccessor(Type type, Integer field, Slice row) + { + Block block = readStructuralBlock(row); + return block.isNull(field) ? null : type.getLong(block, field); + } + + public static Boolean booleanAccessor(Type type, Integer field, Slice row) + { + Block block = readStructuralBlock(row); + return block.isNull(field) ? null : type.getBoolean(block, field); + } + + public static Double doubleAccessor(Type type, Integer field, Slice row) + { + Block block = readStructuralBlock(row); + return block.isNull(field) ? null : type.getDouble(block, field); + } + + public static Slice sliceAccessor(Type type, Integer field, Slice row) + { + Block block = readStructuralBlock(row); + return block.isNull(field) ? null : type.getSlice(block, field); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/RowHashCodeOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/RowHashCodeOperator.java new file mode 100644 index 00000000..3707aa9c --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/RowHashCodeOperator.java @@ -0,0 +1,63 @@ +package com.facebook.presto.operator.scalar; +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.ParametricOperator; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.block.BlockBuilderStatus; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.spi.type.TypeSignature; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; + +import java.lang.invoke.MethodHandle; +import java.util.Map; + +import static com.facebook.presto.metadata.FunctionRegistry.operatorInfo; +import static com.facebook.presto.metadata.OperatorType.HASH_CODE; +import static com.facebook.presto.metadata.Signature.comparableWithVariadicBound; +import static com.facebook.presto.spi.type.TypeSignature.parseTypeSignature; +import static com.facebook.presto.util.Reflection.methodHandle; + +public class RowHashCodeOperator + extends ParametricOperator +{ + public static final RowHashCodeOperator ROW_HASH_CODE = new RowHashCodeOperator(); + private static final TypeSignature RETURN_TYPE = parseTypeSignature(StandardTypes.BIGINT); + private static final MethodHandle METHOD_HANDLE = methodHandle(RowHashCodeOperator.class, "hash", Type.class, Slice.class); + + private RowHashCodeOperator() + { + super(HASH_CODE, ImmutableList.of(comparableWithVariadicBound("T", "row")), StandardTypes.BIGINT, ImmutableList.of("T")); + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + Type type = types.get("T"); + TypeSignature typeSignature = type.getTypeSignature(); + return operatorInfo(HASH_CODE, RETURN_TYPE, ImmutableList.of(typeSignature), METHOD_HANDLE.bindTo(type), false, ImmutableList.of(false)); + } + + public static long hash(Type rowType, Slice slice) + { + BlockBuilder blockBuilder = rowType.createBlockBuilder(new BlockBuilderStatus(), 1, slice.length()); + blockBuilder.writeBytes(slice, 0, slice.length()); + return rowType.hash(blockBuilder.closeEntry().build(), 0); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/RowNotEqualOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/RowNotEqualOperator.java new file mode 100644 index 00000000..960b54ec --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/RowNotEqualOperator.java @@ -0,0 +1,59 @@ +package com.facebook.presto.operator.scalar; +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.ParametricOperator; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.spi.type.TypeSignature; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; + +import java.lang.invoke.MethodHandle; +import java.util.Map; + +import static com.facebook.presto.metadata.FunctionRegistry.operatorInfo; +import static com.facebook.presto.metadata.OperatorType.NOT_EQUAL; +import static com.facebook.presto.metadata.Signature.comparableWithVariadicBound; +import static com.facebook.presto.spi.type.TypeSignature.parseTypeSignature; +import static com.facebook.presto.util.Reflection.methodHandle; + +public class RowNotEqualOperator + extends ParametricOperator +{ + public static final RowNotEqualOperator ROW_NOT_EQUAL = new RowNotEqualOperator(); + private static final TypeSignature RETURN_TYPE = parseTypeSignature(StandardTypes.BOOLEAN); + private static final MethodHandle METHOD_HANDLE = methodHandle(RowNotEqualOperator.class, "notEqual", Type.class, Slice.class, Slice.class); + + private RowNotEqualOperator() + { + super(NOT_EQUAL, ImmutableList.of(comparableWithVariadicBound("T", "row")), StandardTypes.BOOLEAN, ImmutableList.of("T", "T")); + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + Type type = types.get("T"); + TypeSignature typeSignature = type.getTypeSignature(); + return operatorInfo(NOT_EQUAL, RETURN_TYPE, ImmutableList.of(typeSignature, typeSignature), METHOD_HANDLE.bindTo(type), false, ImmutableList.of(false, false)); + } + + public static boolean notEqual(Type rowType, Slice leftRow, Slice rightRow) + { + return !RowEqualOperator.equals(rowType, leftRow, rightRow); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/RowToJsonCast.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/RowToJsonCast.java new file mode 100644 index 00000000..6c529673 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/RowToJsonCast.java @@ -0,0 +1,77 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.OperatorType; +import com.facebook.presto.metadata.ParametricOperator; +import com.facebook.presto.server.SliceSerializer; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import io.airlift.json.ObjectMapperProvider; +import io.airlift.slice.Slice; +import io.airlift.slice.Slices; + +import java.lang.invoke.MethodHandle; +import java.util.Map; + +import static com.facebook.presto.metadata.FunctionRegistry.operatorInfo; +import static com.facebook.presto.metadata.Signature.withVariadicBound; +import static com.facebook.presto.spi.type.TypeSignature.parseTypeSignature; +import static com.facebook.presto.type.TypeUtils.createBlock; +import static com.facebook.presto.util.Reflection.methodHandle; +import static com.google.common.base.Preconditions.checkArgument; + +public class RowToJsonCast + extends ParametricOperator +{ + public static final RowToJsonCast ROW_TO_JSON = new RowToJsonCast(); + private static final Supplier OBJECT_MAPPER = Suppliers.memoize(() -> new ObjectMapperProvider().get().registerModule(new SimpleModule().addSerializer(Slice.class, new SliceSerializer()))); + private static final MethodHandle METHOD_HANDLE = methodHandle(RowToJsonCast.class, "toJson", Type.class, ConnectorSession.class, Slice.class); + + private RowToJsonCast() + { + super(OperatorType.CAST, ImmutableList.of(withVariadicBound("T", "row")), StandardTypes.JSON, ImmutableList.of("T")); + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + checkArgument(arity == 1, "Expected arity to be 1"); + Type type = types.get("T"); + MethodHandle methodHandle = METHOD_HANDLE.bindTo(type); + return operatorInfo(OperatorType.CAST, parseTypeSignature(StandardTypes.JSON), ImmutableList.of(type.getTypeSignature()), methodHandle, false, ImmutableList.of(false)); + } + + public static Slice toJson(Type rowType, ConnectorSession session, Slice row) + { + Object object = rowType.getObjectValue(session, createBlock(rowType, row), 0); + try { + return Slices.utf8Slice(OBJECT_MAPPER.get().writeValueAsString(object)); + } + catch (JsonProcessingException e) { + throw Throwables.propagate(e); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/ScalarFunction.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ScalarFunction.java new file mode 100644 index 00000000..08ca7480 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ScalarFunction.java @@ -0,0 +1,32 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface ScalarFunction +{ + String value() default ""; + + String[] alias() default {}; + + boolean hidden() default false; + + boolean deterministic() default true; +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/ScalarOperator.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ScalarOperator.java new file mode 100644 index 00000000..5c38b561 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/ScalarOperator.java @@ -0,0 +1,28 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import com.facebook.presto.metadata.OperatorType; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface ScalarOperator +{ + OperatorType value(); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/StringFunctions.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/StringFunctions.java new file mode 100644 index 00000000..b24efdcc --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/StringFunctions.java @@ -0,0 +1,453 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import com.facebook.presto.operator.Description; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.type.SqlType; +import com.google.common.collect.ImmutableList; +import com.google.common.primitives.Ints; +import io.airlift.slice.InvalidCodePointException; +import io.airlift.slice.InvalidUtf8Exception; +import io.airlift.slice.Slice; +import io.airlift.slice.SliceUtf8; +import io.airlift.slice.Slices; + +import javax.annotation.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.OptionalInt; + +import static com.facebook.presto.spi.StandardErrorCode.INVALID_FUNCTION_ARGUMENT; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static com.facebook.presto.type.ArrayType.toStackRepresentation; +import static com.facebook.presto.util.Failures.checkCondition; +import static io.airlift.slice.SliceUtf8.countCodePoints; +import static io.airlift.slice.SliceUtf8.lengthOfCodePoint; +import static io.airlift.slice.SliceUtf8.lengthOfCodePointSafe; +import static io.airlift.slice.SliceUtf8.offsetOfCodePoint; +import static io.airlift.slice.SliceUtf8.toLowerCase; +import static io.airlift.slice.SliceUtf8.toUpperCase; +import static java.lang.Character.MAX_CODE_POINT; +import static java.lang.Character.SURROGATE; + +/** + * Current implementation is based on code points from Unicode and does ignore grapheme cluster boundaries. + * Therefore only some methods work correctly with grapheme cluster boundaries. + */ +public final class StringFunctions +{ + private StringFunctions() {} + + @Description("convert Unicode code point to a string") + @ScalarFunction + @SqlType(StandardTypes.VARCHAR) + public static Slice chr(@SqlType(StandardTypes.BIGINT) long codepoint) + { + try { + return SliceUtf8.codePointToUtf8(Ints.saturatedCast(codepoint)); + } + catch (InvalidCodePointException e) { + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, "Not a valid Unicode code point: " + codepoint, e); + } + } + + @Description("concatenates given strings") + @ScalarFunction + @SqlType(StandardTypes.VARCHAR) + public static Slice concat(@SqlType(StandardTypes.VARCHAR) Slice str1, @SqlType(StandardTypes.VARCHAR) Slice str2) + { + Slice concat = Slices.allocate(str1.length() + str2.length()); + concat.setBytes(0, str1); + concat.setBytes(str1.length(), str2); + return concat; + } + + @Description("count of code points of the given string") + @ScalarFunction + @SqlType(StandardTypes.BIGINT) + public static long length(@SqlType(StandardTypes.VARCHAR) Slice slice) + { + return countCodePoints(slice); + } + + @Description("greedily removes occurrences of a pattern in a string") + @ScalarFunction + @SqlType(StandardTypes.VARCHAR) + public static Slice replace(@SqlType(StandardTypes.VARCHAR) Slice str, @SqlType(StandardTypes.VARCHAR) Slice search) + { + return replace(str, search, Slices.EMPTY_SLICE); + } + + @Description("greedily replaces occurrences of a pattern with a string") + @ScalarFunction + @SqlType(StandardTypes.VARCHAR) + public static Slice replace(@SqlType(StandardTypes.VARCHAR) Slice str, @SqlType(StandardTypes.VARCHAR) Slice search, @SqlType(StandardTypes.VARCHAR) Slice replace) + { + // Empty search? + if (search.length() == 0) { + // With empty `search` we insert `replace` in front of every character and and the end + Slice buffer = Slices.allocate((countCodePoints(str) + 1) * replace.length() + str.length()); + // Always start with replace + buffer.setBytes(0, replace); + int indexBuffer = replace.length(); + // After every code point insert `replace` + int index = 0; + while (index < str.length()) { + int codePointLength = lengthOfCodePointSafe(str, index); + // Append current code point + buffer.setBytes(indexBuffer, str, index, codePointLength); + indexBuffer += codePointLength; + // Append `replace` + buffer.setBytes(indexBuffer, replace); + indexBuffer += replace.length(); + // Advance pointer to current code point + index += codePointLength; + } + + return buffer; + } + // Allocate a reasonable buffer + Slice buffer = Slices.allocate(str.length()); + + int index = 0; + int indexBuffer = 0; + while (index < str.length()) { + int matchIndex = str.indexOf(search, index); + // Found a match? + if (matchIndex < 0) { + // No match found so copy the rest of string + int bytesToCopy = str.length() - index; + buffer = Slices.ensureSize(buffer, indexBuffer + bytesToCopy); + buffer.setBytes(indexBuffer, str, index, bytesToCopy); + indexBuffer += bytesToCopy; + + break; + } + + int bytesToCopy = matchIndex - index; + buffer = Slices.ensureSize(buffer, indexBuffer + bytesToCopy + replace.length()); + // Non empty match? + if (bytesToCopy > 0) { + buffer.setBytes(indexBuffer, str, index, bytesToCopy); + indexBuffer += bytesToCopy; + } + // Non empty replace? + if (replace.length() > 0) { + buffer.setBytes(indexBuffer, replace); + indexBuffer += replace.length(); + } + // Continue searching after match + index = matchIndex + search.length(); + } + + return buffer.slice(0, indexBuffer); + } + + @Description("reverse all code points in a given string") + @ScalarFunction + @SqlType(StandardTypes.VARCHAR) + public static Slice reverse(@SqlType(StandardTypes.VARCHAR) Slice slice) + { + return SliceUtf8.reverse(slice); + } + + @Description("returns index of first occurrence of a substring (or 0 if not found)") + @ScalarFunction("strpos") + @SqlType(StandardTypes.BIGINT) + public static long stringPosition(@SqlType(StandardTypes.VARCHAR) Slice string, @SqlType(StandardTypes.VARCHAR) Slice substring) + { + if (substring.length() == 0) { + return 1; + } + + int index = string.indexOf(substring); + if (index < 0) { + return 0; + } + return countCodePoints(string, 0, index) + 1; + } + + @Description("suffix starting at given index") + @ScalarFunction + @SqlType(StandardTypes.VARCHAR) + public static Slice substr(@SqlType(StandardTypes.VARCHAR) Slice utf8, @SqlType(StandardTypes.BIGINT) long start) + { + if ((start == 0) || utf8.length() == 0) { + return Slices.EMPTY_SLICE; + } + + int startCodePoint = Ints.saturatedCast(start); + + if (startCodePoint > 0) { + int indexStart = offsetOfCodePoint(utf8, startCodePoint - 1); + if (indexStart < 0) { + // before beginning of string + return Slices.EMPTY_SLICE; + } + int indexEnd = utf8.length(); + + return utf8.slice(indexStart, indexEnd - indexStart); + } + + // negative start is relative to end of string + int codePoints = countCodePoints(utf8); + startCodePoint += codePoints; + + // before beginning of string + if (startCodePoint < 0) { + return Slices.EMPTY_SLICE; + } + + int indexStart = offsetOfCodePoint(utf8, startCodePoint); + int indexEnd = utf8.length(); + + return utf8.slice(indexStart, indexEnd - indexStart); + } + + @Description("substring of given length starting at an index") + @ScalarFunction + @SqlType(StandardTypes.VARCHAR) + public static Slice substr(@SqlType(StandardTypes.VARCHAR) Slice utf8, @SqlType(StandardTypes.BIGINT) long start, @SqlType(StandardTypes.BIGINT) long length) + { + if (start == 0 || (length <= 0) || (utf8.length() == 0)) { + return Slices.EMPTY_SLICE; + } + + int startCodePoint = Ints.saturatedCast(start); + int lengthCodePoints = Ints.saturatedCast(length); + + if (startCodePoint > 0) { + int indexStart = offsetOfCodePoint(utf8, startCodePoint - 1); + if (indexStart < 0) { + // before beginning of string + return Slices.EMPTY_SLICE; + } + int indexEnd = offsetOfCodePoint(utf8, indexStart, lengthCodePoints); + if (indexEnd < 0) { + // after end of string + indexEnd = utf8.length(); + } + + return utf8.slice(indexStart, indexEnd - indexStart); + } + + // negative start is relative to end of string + int codePoints = countCodePoints(utf8); + startCodePoint += codePoints; + + // before beginning of string + if (startCodePoint < 0) { + return Slices.EMPTY_SLICE; + } + + int indexStart = offsetOfCodePoint(utf8, startCodePoint); + int indexEnd; + if (startCodePoint + lengthCodePoints < codePoints) { + indexEnd = offsetOfCodePoint(utf8, indexStart, lengthCodePoints); + } + else { + indexEnd = utf8.length(); + } + + return utf8.slice(indexStart, indexEnd - indexStart); + } + + @ScalarFunction + @SqlType("array") + public static Slice split(@SqlType(StandardTypes.VARCHAR) Slice string, @SqlType(StandardTypes.VARCHAR) Slice delimiter) + { + return split(string, delimiter, string.length() + 1); + } + + @ScalarFunction + @SqlType("array") + public static Slice split(@SqlType(StandardTypes.VARCHAR) Slice string, @SqlType(StandardTypes.VARCHAR) Slice delimiter, @SqlType(StandardTypes.BIGINT) long limit) + { + checkCondition(limit > 0, INVALID_FUNCTION_ARGUMENT, "Limit must be positive"); + checkCondition(limit <= Integer.MAX_VALUE, INVALID_FUNCTION_ARGUMENT, "Limit is too large"); + checkCondition(delimiter.length() > 0, INVALID_FUNCTION_ARGUMENT, "The delimiter may not be the empty string"); + // If limit is one, the last and only element is the complete string + if (limit == 1) { + return toStackRepresentation(ImmutableList.of(string), VARCHAR); + } + + // todo this should write directly into a block + List parts = new ArrayList<>(); + + int index = 0; + while (index < string.length()) { + int splitIndex = string.indexOf(delimiter, index); + // Found split? + if (splitIndex < 0) { + break; + } + // Add the part from current index to found split + parts.add(string.slice(index, splitIndex - index)); + // Continue searching after delimiter + index = splitIndex + delimiter.length(); + // Reached limit-1 parts so we can stop + if (parts.size() == limit - 1) { + break; + } + } + // Rest of string + parts.add(string.slice(index, string.length() - index)); + + return toStackRepresentation(parts, VARCHAR); + } + + @Nullable + @Description("splits a string by a delimiter and returns the specified field (counting from one)") + @ScalarFunction + @SqlType(StandardTypes.VARCHAR) + public static Slice splitPart(@SqlType(StandardTypes.VARCHAR) Slice string, @SqlType(StandardTypes.VARCHAR) Slice delimiter, @SqlType(StandardTypes.BIGINT) long index) + { + checkCondition(index > 0, INVALID_FUNCTION_ARGUMENT, "Index must be greater than zero"); + // Empty delimiter? Then every character will be a split + if (delimiter.length() == 0) { + int startCodePoint = Ints.checkedCast(index); + + int indexStart = offsetOfCodePoint(string, startCodePoint - 1); + if (indexStart < 0) { + // index too big + return null; + } + int length = lengthOfCodePoint(string, indexStart); + if (indexStart + length > string.length()) { + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, "Invalid UTF-8 encoding"); + } + return string.slice(indexStart, length); + } + + int matchCount = 0; + + int previousIndex = 0; + while (previousIndex < string.length()) { + int matchIndex = string.indexOf(delimiter, previousIndex); + // No match + if (matchIndex < 0) { + break; + } + // Reached the requested part? + if (++matchCount == index) { + return string.slice(previousIndex, matchIndex - previousIndex); + } + // Continue searching after the delimiter + previousIndex = matchIndex + delimiter.length(); + } + + if (matchCount == index - 1) { + // returns last section of the split + return string.slice(previousIndex, string.length() - previousIndex); + } + + // index is too big, null is returned + return null; + } + + @Description("removes whitespace from the beginning of a string") + @ScalarFunction("ltrim") + @SqlType(StandardTypes.VARCHAR) + public static Slice leftTrim(@SqlType(StandardTypes.VARCHAR) Slice slice) + { + return SliceUtf8.leftTrim(slice); + } + + @Description("removes whitespace from the end of a string") + @ScalarFunction("rtrim") + @SqlType(StandardTypes.VARCHAR) + public static Slice rightTrim(@SqlType(StandardTypes.VARCHAR) Slice slice) + { + return SliceUtf8.rightTrim(slice); + } + + @Description("removes whitespace from the beginning and end of a string") + @ScalarFunction + @SqlType(StandardTypes.VARCHAR) + public static Slice trim(@SqlType(StandardTypes.VARCHAR) Slice slice) + { + return SliceUtf8.trim(slice); + } + + @Description("converts the string to lower case") + @ScalarFunction + @SqlType(StandardTypes.VARCHAR) + public static Slice lower(@SqlType(StandardTypes.VARCHAR) Slice slice) + { + return toLowerCase(slice); + } + + @Description("converts the string to upper case") + @ScalarFunction + @SqlType(StandardTypes.VARCHAR) + public static Slice upper(@SqlType(StandardTypes.VARCHAR) Slice slice) + { + return toUpperCase(slice); + } + + @Description("decodes the UTF-8 encoded string") + @ScalarFunction + @SqlType(StandardTypes.VARCHAR) + public static Slice fromUtf8(@SqlType(StandardTypes.VARBINARY) Slice slice) + { + return SliceUtf8.fixInvalidUtf8(slice); + } + + @Description("decodes the UTF-8 encoded string") + @ScalarFunction + @SqlType(StandardTypes.VARCHAR) + public static Slice fromUtf8(@SqlType(StandardTypes.VARBINARY) Slice slice, @SqlType(StandardTypes.VARCHAR) Slice replacementCharacter) + { + int count = countCodePoints(replacementCharacter); + if (count > 1) { + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, "Replacement character string must empty or a single character"); + } + + OptionalInt replacementCodePoint; + if (count == 1) { + try { + replacementCodePoint = OptionalInt.of(SliceUtf8.getCodePointAt(replacementCharacter, 0)); + } + catch (InvalidUtf8Exception e) { + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, "Invalid replacement character"); + } + } + else { + replacementCodePoint = OptionalInt.empty(); + } + return SliceUtf8.fixInvalidUtf8(slice, replacementCodePoint); + } + + @Description("decodes the UTF-8 encoded string") + @ScalarFunction + @SqlType(StandardTypes.VARCHAR) + public static Slice fromUtf8(@SqlType(StandardTypes.VARBINARY) Slice slice, @SqlType(StandardTypes.BIGINT) long replacementCodePoint) + { + if (replacementCodePoint > MAX_CODE_POINT || Character.getType((int) replacementCodePoint) == SURROGATE) { + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, "Invalid replacement character"); + } + return SliceUtf8.fixInvalidUtf8(slice, OptionalInt.of((int) replacementCodePoint)); + } + + @Description("encodes the string to UTF-8") + @ScalarFunction + @SqlType(StandardTypes.VARBINARY) + public static Slice toUtf8(@SqlType(StandardTypes.VARCHAR) Slice slice) + { + return slice; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/TestingRowConstructor.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/TestingRowConstructor.java new file mode 100644 index 00000000..654cb4ab --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/TestingRowConstructor.java @@ -0,0 +1,168 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.block.BlockBuilderStatus; +import com.facebook.presto.spi.block.VariableWidthBlockBuilder; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.type.ArrayType; +import com.facebook.presto.type.MapType; +import com.facebook.presto.type.RowType; +import com.facebook.presto.type.SqlType; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; + +import javax.annotation.Nullable; + +import java.util.List; +import java.util.Optional; + +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.BooleanType.BOOLEAN; +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; +import static com.facebook.presto.spi.type.HyperLogLogType.HYPER_LOG_LOG; +import static com.facebook.presto.spi.type.TimestampType.TIMESTAMP; +import static com.facebook.presto.spi.type.TimestampWithTimeZoneType.TIMESTAMP_WITH_TIME_ZONE; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static com.facebook.presto.type.TypeUtils.appendToBlockBuilder; +import static com.facebook.presto.type.TypeUtils.buildStructuralSlice; + +public final class TestingRowConstructor +{ + private TestingRowConstructor() {} + + @ScalarFunction("test_row") + @SqlType("row('col0','col1')") + public static Slice testRowBigintBigint(@Nullable @SqlType(StandardTypes.BIGINT) Long arg1, @Nullable @SqlType(StandardTypes.BIGINT) Long arg2) + { + return toStackRepresentation(ImmutableList.of(BIGINT, BIGINT), arg1, arg2); + } + + @ScalarFunction("test_row") + @SqlType("row('col0','col1')") + public static Slice testRowBigintBigint(@Nullable @SqlType(StandardTypes.BIGINT) Long arg1, @Nullable @SqlType(StandardTypes.DOUBLE) Double arg2) + { + return toStackRepresentation(ImmutableList.of(BIGINT, DOUBLE), arg1, arg2); + } + + @ScalarFunction("test_row") + @SqlType("row('col0','col1','col2','col3','col4')") + public static Slice testRowBigintDoubleBooleanVarcharTimestamp(@Nullable @SqlType(StandardTypes.BIGINT) Long arg1, @Nullable @SqlType(StandardTypes.DOUBLE) Double arg2, + @Nullable @SqlType(StandardTypes.BOOLEAN) Boolean arg3, @Nullable @SqlType(StandardTypes.VARCHAR) Slice arg4, + @Nullable @SqlType(StandardTypes.TIMESTAMP) Long arg5) + { + return toStackRepresentation(ImmutableList.of(BIGINT, DOUBLE, BOOLEAN, VARCHAR, TIMESTAMP), arg1, arg2, arg3, arg4, arg5); + } + + @ScalarFunction("test_row") + @SqlType("row('col0')") + public static Slice testRowHyperLogLog(@Nullable @SqlType(StandardTypes.HYPER_LOG_LOG) Slice arg1) + { + return toStackRepresentation(ImmutableList.of(HYPER_LOG_LOG), arg1); + } + + @ScalarFunction("test_row") + @SqlType("row('col0','col1')>('col2','col3')") + public static Slice testNestedRowsWithTimestampsWithTimeZones(@Nullable @SqlType(StandardTypes.DOUBLE) Double arg1, + @Nullable @SqlType("row('col0','col1')") Slice arg2) + { + List parameterTypes = ImmutableList.of( + DOUBLE, + new RowType(ImmutableList.of(TIMESTAMP_WITH_TIME_ZONE, TIMESTAMP_WITH_TIME_ZONE), Optional.of(ImmutableList.of("col0", "col1")))); + return toStackRepresentation(parameterTypes, arg1, arg2); + } + + @ScalarFunction("test_row") + @SqlType("row('col0','col1')") + public static Slice testRowTimestampsWithTimeZones(@Nullable @SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) Long arg1, + @Nullable @SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) Long arg2) + { + return toStackRepresentation(ImmutableList.of(TIMESTAMP_WITH_TIME_ZONE, TIMESTAMP_WITH_TIME_ZONE), arg1, arg2); + } + + @ScalarFunction("test_row") + @SqlType("row('col0','col1')") + public static Slice testRowDoubleDouble(@Nullable @SqlType(StandardTypes.DOUBLE) Double arg1, @Nullable @SqlType(StandardTypes.DOUBLE) Double arg2) + { + return toStackRepresentation(ImmutableList.of(DOUBLE, DOUBLE), arg1, arg2); + } + + @ScalarFunction("test_row") + @SqlType("row('col0','col1')") + public static Slice testRowDoubleBigint(@Nullable @SqlType(StandardTypes.DOUBLE) Double arg1, @Nullable @SqlType(StandardTypes.VARCHAR) Slice arg2) + { + return toStackRepresentation(ImmutableList.of(DOUBLE, VARCHAR), arg1, arg2); + } + + @ScalarFunction("test_row") + @SqlType("row('col0','col1')") + public static Slice testRowBigintBigint(@Nullable @SqlType(StandardTypes.BOOLEAN) Boolean arg1, @Nullable @SqlType(StandardTypes.BOOLEAN) Boolean arg2) + { + return toStackRepresentation(ImmutableList.of(BOOLEAN, BOOLEAN), arg1, arg2); + } + + @ScalarFunction("test_row") + @SqlType("row('col0','col1','col2','col3')") + public static Slice testRowFourBooleans(@Nullable @SqlType(StandardTypes.BOOLEAN) Boolean arg1, @Nullable @SqlType(StandardTypes.BOOLEAN) Boolean arg2, + @Nullable @SqlType(StandardTypes.BOOLEAN) Boolean arg3, @Nullable @SqlType(StandardTypes.BOOLEAN) Boolean arg4) + { + return toStackRepresentation(ImmutableList.of(BOOLEAN, BOOLEAN, BOOLEAN, BOOLEAN), arg1, arg2, arg3, arg4); + } + + @ScalarFunction("test_row") + @SqlType("row>('col0','col1')") + public static Slice testRowBooleanArray(@Nullable @SqlType(StandardTypes.BOOLEAN) Boolean arg1, @Nullable @SqlType("array") Slice arg2) + { + List parameterTypes = ImmutableList.of(BOOLEAN, new ArrayType(BIGINT)); + return toStackRepresentation(parameterTypes, arg1, arg2); + } + + @ScalarFunction("test_row") + @SqlType("row,map>('col0','col1','col2')") + public static Slice testRowBooleanArrayMap(@Nullable @SqlType(StandardTypes.BOOLEAN) Boolean arg1, @Nullable @SqlType("array") Slice arg2, + @Nullable @SqlType("map") Slice arg3) + { + List parameterTypes = ImmutableList.of(BOOLEAN, new ArrayType(BIGINT), new MapType(BIGINT, DOUBLE)); + return toStackRepresentation(parameterTypes, arg1, arg2, arg3); + } + + @ScalarFunction("test_row") + @SqlType("row,row('col0','col1')>('col0','col1','col2')") + public static Slice testNestedRow(@Nullable @SqlType(StandardTypes.DOUBLE) Double arg1, @Nullable @SqlType("array") Slice arg2, + @Nullable @SqlType("row('col0','col1')") Slice arg3) + { + List parameterTypes = ImmutableList.of( + DOUBLE, new ArrayType(BIGINT), + new RowType(ImmutableList.of(BIGINT, DOUBLE), Optional.of(ImmutableList.of("col0", "col1")))); + return toStackRepresentation(parameterTypes, arg1, arg2, arg3); + } + + @ScalarFunction("test_row") + @SqlType("row('col0')") + public static Slice testRowBigintBigint(@Nullable @SqlType(StandardTypes.TIMESTAMP) Long arg1) + { + return toStackRepresentation(ImmutableList.of(TIMESTAMP), arg1); + } + + private static Slice toStackRepresentation(List parameterTypes, Object... values) + { + BlockBuilder blockBuilder = new VariableWidthBlockBuilder(new BlockBuilderStatus(), 1024); + for (int i = 0; i < values.length; i++) { + appendToBlockBuilder(parameterTypes.get(i), values[i], blockBuilder); + } + return buildStructuralSlice(blockBuilder); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/TryCastFunction.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/TryCastFunction.java new file mode 100644 index 00000000..14677a55 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/TryCastFunction.java @@ -0,0 +1,97 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.ParametricScalar; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.google.common.collect.ImmutableList; +import com.google.common.primitives.Primitives; + +import java.lang.invoke.MethodHandle; +import java.util.Map; + +import static com.facebook.presto.metadata.Signature.internalFunction; +import static com.facebook.presto.metadata.Signature.typeParameter; +import static com.facebook.presto.type.UnknownType.UNKNOWN; +import static java.lang.invoke.MethodHandles.catchException; +import static java.lang.invoke.MethodHandles.constant; +import static java.lang.invoke.MethodHandles.dropArguments; +import static java.lang.invoke.MethodType.methodType; + +public class TryCastFunction + extends ParametricScalar +{ + public static final TryCastFunction TRY_CAST = new TryCastFunction(); + + private static final Signature SIGNATURE = new Signature("TRY_CAST", ImmutableList.of(typeParameter("F"), typeParameter("T")), "T", ImmutableList.of("F"), false, false); + + @Override + public Signature getSignature() + { + return SIGNATURE; + } + + @Override + public boolean isHidden() + { + return true; + } + + @Override + public boolean isDeterministic() + { + return true; + } + + @Override + public String getDescription() + { + return ""; + } + + @Override + public FunctionInfo specialize(Map types, int arity, TypeManager typeManager, FunctionRegistry functionRegistry) + { + Type fromType = types.get("F"); + Type toType = types.get("T"); + + Class returnType = Primitives.wrap(toType.getJavaType()); + MethodHandle tryCastHandle; + + if (fromType.equals(UNKNOWN)) { + tryCastHandle = dropArguments(constant(returnType, null), 0, Void.class); + } + else { + // the resulting method needs to return a boxed type + MethodHandle coercion = functionRegistry.getCoercion(fromType, toType).getMethodHandle(); + coercion = coercion.asType(methodType(returnType, coercion.type())); + + MethodHandle exceptionHandler = dropArguments(constant(returnType, null), 0, RuntimeException.class); + tryCastHandle = catchException(coercion, RuntimeException.class, exceptionHandler); + } + + return new FunctionInfo( + internalFunction(SIGNATURE.getName(), toType.getTypeSignature(), ImmutableList.of(fromType.getTypeSignature())), + getDescription(), + isHidden(), + tryCastHandle, + isDeterministic(), + true, + ImmutableList.of(true)); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/UrlFunctions.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/UrlFunctions.java new file mode 100644 index 00000000..c964ac4d --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/UrlFunctions.java @@ -0,0 +1,147 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import com.facebook.presto.operator.Description; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.type.SqlType; +import com.google.common.base.Splitter; +import io.airlift.slice.Slice; +import io.airlift.slice.Slices; + +import javax.annotation.Nullable; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Iterator; + +import static com.google.common.base.Strings.nullToEmpty; +import static java.nio.charset.StandardCharsets.UTF_8; + +public final class UrlFunctions +{ + private static final Splitter QUERY_SPLITTER = Splitter.on('&'); + private static final Splitter ARG_SPLITTER = Splitter.on('=').limit(2); + + private UrlFunctions() {} + + @Nullable + @Description("extract protocol from url") + @ScalarFunction + @SqlType(StandardTypes.VARCHAR) + public static Slice urlExtractProtocol(@SqlType(StandardTypes.VARCHAR) Slice url) + { + URI uri = parseUrl(url); + return (uri == null) ? null : slice(uri.getScheme()); + } + + @Nullable + @Description("extract host from url") + @ScalarFunction + @SqlType(StandardTypes.VARCHAR) + public static Slice urlExtractHost(@SqlType(StandardTypes.VARCHAR) Slice url) + { + URI uri = parseUrl(url); + return (uri == null) ? null : slice(uri.getHost()); + } + + @Nullable + @Description("extract port from url") + @ScalarFunction + @SqlType(StandardTypes.BIGINT) + public static Long urlExtractPort(@SqlType(StandardTypes.VARCHAR) Slice url) + { + URI uri = parseUrl(url); + if ((uri == null) || (uri.getPort() < 0)) { + return null; + } + return (long) uri.getPort(); + } + + @Nullable + @Description("extract part from url") + @ScalarFunction + @SqlType(StandardTypes.VARCHAR) + public static Slice urlExtractPath(@SqlType(StandardTypes.VARCHAR) Slice url) + { + URI uri = parseUrl(url); + return (uri == null) ? null : slice(uri.getPath()); + } + + @Nullable + @Description("extract query from url") + @ScalarFunction + @SqlType(StandardTypes.VARCHAR) + public static Slice urlExtractQuery(@SqlType(StandardTypes.VARCHAR) Slice url) + { + URI uri = parseUrl(url); + return (uri == null) ? null : slice(uri.getQuery()); + } + + @Nullable + @Description("extract fragment from url") + @ScalarFunction + @SqlType(StandardTypes.VARCHAR) + public static Slice urlExtractFragment(@SqlType(StandardTypes.VARCHAR) Slice url) + { + URI uri = parseUrl(url); + return (uri == null) ? null : slice(uri.getFragment()); + } + + @Nullable + @Description("extract query parameter from url") + @ScalarFunction + @SqlType(StandardTypes.VARCHAR) + public static Slice urlExtractParameter(@SqlType(StandardTypes.VARCHAR) Slice url, @SqlType(StandardTypes.VARCHAR) Slice parameterName) + { + URI uri = parseUrl(url); + if ((uri == null) || (uri.getQuery() == null)) { + return null; + } + + Slice query = slice(uri.getQuery()); + String parameter = parameterName.toString(UTF_8); + Iterable queryArgs = QUERY_SPLITTER.split(query.toString(UTF_8)); + + for (String queryArg : queryArgs) { + Iterator arg = ARG_SPLITTER.split(queryArg).iterator(); + if (arg.next().equals(parameter)) { + if (arg.hasNext()) { + return Slices.copiedBuffer(arg.next(), UTF_8); + } + // first matched key is empty + return Slices.EMPTY_SLICE; + } + } + + // no key matched + return null; + } + + private static Slice slice(@Nullable String s) + { + return Slices.copiedBuffer(nullToEmpty(s), UTF_8); + } + + @Nullable + private static URI parseUrl(Slice url) + { + try { + return new URI(url.toString(UTF_8)); + } + catch (URISyntaxException e) { + return null; + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/scalar/VarbinaryFunctions.java b/presto-main/src/main/java/com/facebook/presto/operator/scalar/VarbinaryFunctions.java new file mode 100644 index 00000000..219bc0d5 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/scalar/VarbinaryFunctions.java @@ -0,0 +1,133 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.scalar; + +import com.facebook.presto.operator.Description; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.type.SqlType; +import com.google.common.io.BaseEncoding; +import io.airlift.slice.Slice; +import io.airlift.slice.Slices; + +import java.util.Base64; + +import static com.facebook.presto.spi.StandardErrorCode.INVALID_FUNCTION_ARGUMENT; + +public final class VarbinaryFunctions +{ + private VarbinaryFunctions() {} + + @Description("length of the given binary") + @ScalarFunction + @SqlType(StandardTypes.BIGINT) + public static long length(@SqlType(StandardTypes.VARBINARY) Slice slice) + { + return slice.length(); + } + + @Description("encode binary data as base64") + @ScalarFunction + @SqlType(StandardTypes.VARCHAR) + public static Slice toBase64(@SqlType(StandardTypes.VARBINARY) Slice slice) + { + return Slices.wrappedBuffer(Base64.getEncoder().encode(slice.getBytes())); + } + + @Description("decode base64 encoded binary data") + @ScalarFunction("from_base64") + @SqlType(StandardTypes.VARBINARY) + public static Slice fromBase64Varchar(@SqlType(StandardTypes.VARCHAR) Slice slice) + { + return Slices.wrappedBuffer(Base64.getDecoder().decode(slice.getBytes())); + } + + @Description("decode base64 encoded binary data") + @ScalarFunction("from_base64") + @SqlType(StandardTypes.VARBINARY) + public static Slice fromBase64Varbinary(@SqlType(StandardTypes.VARBINARY) Slice slice) + { + return Slices.wrappedBuffer(Base64.getDecoder().decode(slice.getBytes())); + } + + @Description("encode binary data as base64 using the URL safe alphabet") + @ScalarFunction("to_base64url") + @SqlType(StandardTypes.VARCHAR) + public static Slice toBase64Url(@SqlType(StandardTypes.VARBINARY) Slice slice) + { + return Slices.wrappedBuffer(Base64.getUrlEncoder().encode(slice.getBytes())); + } + + @Description("decode URL safe base64 encoded binary data") + @ScalarFunction("from_base64url") + @SqlType(StandardTypes.VARBINARY) + public static Slice fromBase64UrlVarchar(@SqlType(StandardTypes.VARCHAR) Slice slice) + { + return Slices.wrappedBuffer(Base64.getUrlDecoder().decode(slice.getBytes())); + } + + @Description("decode URL safe base64 encoded binary data") + @ScalarFunction("from_base64url") + @SqlType(StandardTypes.VARBINARY) + public static Slice fromBase64UrlVarbinary(@SqlType(StandardTypes.VARBINARY) Slice slice) + { + return Slices.wrappedBuffer(Base64.getUrlDecoder().decode(slice.getBytes())); + } + + @Description("encode binary data as hex") + @ScalarFunction + @SqlType(StandardTypes.VARCHAR) + public static Slice toHex(@SqlType(StandardTypes.VARBINARY) Slice slice) + { + return Slices.utf8Slice(BaseEncoding.base16().encode(slice.getBytes())); + } + + @Description("decode hex encoded binary data") + @ScalarFunction("from_hex") + @SqlType(StandardTypes.VARBINARY) + public static Slice fromHexVarchar(@SqlType(StandardTypes.VARCHAR) Slice slice) + { + if (slice.length() % 2 != 0) { + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, "invalid input length " + slice.length()); + } + + byte[] result = new byte[slice.length() / 2]; + for (int i = 0; i < slice.length(); i += 2) { + result[i / 2] = (byte) ((hexDigitCharToInt(slice.getByte(i)) << 4) | hexDigitCharToInt(slice.getByte(i + 1))); + } + return Slices.wrappedBuffer(result); + } + + private static int hexDigitCharToInt(byte b) + { + if (b >= '0' && b <= '9') { + return b - '0'; + } + else if (b >= 'a' && b <= 'f') { + return b - 'a' + 10; + } + else if (b >= 'A' && b <= 'F') { + return b - 'A' + 10; + } + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, "invalid hex character: " + (char) b); + } + + @Description("decode hex encoded binary data") + @ScalarFunction("from_hex") + @SqlType(StandardTypes.VARBINARY) + public static Slice fromHexVarbinary(@SqlType(StandardTypes.VARBINARY) Slice slice) + { + return fromHexVarchar(slice); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/window/AbstractWindowFunctionSupplier.java b/presto-main/src/main/java/com/facebook/presto/operator/window/AbstractWindowFunctionSupplier.java new file mode 100644 index 00000000..15b4ed80 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/window/AbstractWindowFunctionSupplier.java @@ -0,0 +1,65 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.window; + +import com.facebook.presto.metadata.Signature; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public abstract class AbstractWindowFunctionSupplier + implements WindowFunctionSupplier +{ + private final Signature signature; + private final String description; + + protected AbstractWindowFunctionSupplier(Signature signature, String description) + { + this.signature = checkNotNull(signature, "signature is null"); + this.description = description; + } + + @Override + public final Signature getSignature() + { + return signature; + } + + @Override + public final String getDescription() + { + return description; + } + + @Override + public final WindowFunction createWindowFunction(List argumentChannels) + { + checkNotNull(argumentChannels, "inputs is null"); + checkArgument(argumentChannels.size() == signature.getArgumentTypes().size(), + "Expected %s arguments for function %s, but got %s", + signature.getArgumentTypes().size(), + signature.getName(), + argumentChannels.size()); + + return newWindowFunction(argumentChannels); + } + + /** + * Create window function instance using the supplied arguments. The + * inputs have already validated. + */ + protected abstract WindowFunction newWindowFunction(List inputs); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/window/AggregateWindowFunction.java b/presto-main/src/main/java/com/facebook/presto/operator/window/AggregateWindowFunction.java new file mode 100644 index 00000000..0f7b16c3 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/window/AggregateWindowFunction.java @@ -0,0 +1,129 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.window; + +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.operator.aggregation.Accumulator; +import com.facebook.presto.operator.aggregation.AccumulatorFactory; +import com.facebook.presto.operator.aggregation.InternalAggregationFunction; +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; +import com.google.common.primitives.Ints; + +import java.util.List; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class AggregateWindowFunction + implements WindowFunction +{ + private final InternalAggregationFunction function; + private final int[] argumentChannels; + private final AccumulatorFactory accumulatorFactory; + + private WindowIndex windowIndex; + private Accumulator accumulator; + private int currentStart; + private int currentEnd; + + private AggregateWindowFunction(InternalAggregationFunction function, List argumentChannels) + { + this.function = checkNotNull(function, "function is null"); + this.argumentChannels = Ints.toArray(argumentChannels); + this.accumulatorFactory = function.bind(createArgs(function), Optional.empty(), Optional.empty(), 1.0); + } + + @Override + public Type getType() + { + return function.getFinalType(); + } + + @Override + public void reset(WindowIndex windowIndex) + { + this.windowIndex = windowIndex; + resetAccumulator(); + } + + @Override + public void processRow(BlockBuilder output, int peerGroupStart, int peerGroupEnd, int frameStart, int frameEnd) + { + if (frameStart < 0) { + // empty frame + resetAccumulator(); + } + else if ((frameStart == currentStart) && (frameEnd >= currentEnd)) { + // same or expanding frame + accumulate(currentEnd + 1, frameEnd); + currentEnd = frameEnd; + } + else { + // different frame + resetAccumulator(); + accumulate(frameStart, frameEnd); + currentStart = frameStart; + currentEnd = frameEnd; + } + + accumulator.evaluateFinal(output); + } + + private void accumulate(int start, int end) + { + // TODO: add Accumulator method that does not require creating pages + PageBuilder pageBuilder = new PageBuilder(function.getParameterTypes()); + for (int position = start; position <= end; position++) { + for (int i = 0; i < function.getParameterTypes().size(); i++) { + windowIndex.appendTo(argumentChannels[i], position, pageBuilder.getBlockBuilder(i)); + } + pageBuilder.declarePosition(); + } + accumulator.addInput(pageBuilder.build()); + } + + private void resetAccumulator() + { + if (currentStart >= 0) { + accumulator = accumulatorFactory.createAccumulator(); + currentStart = -1; + currentEnd = -1; + } + } + + public static WindowFunctionSupplier supplier(Signature signature, final InternalAggregationFunction function) + { + checkNotNull(function, "function is null"); + return new AbstractWindowFunctionSupplier(signature, null) + { + @Override + protected WindowFunction newWindowFunction(List inputs) + { + return new AggregateWindowFunction(function, inputs); + } + }; + } + + private static List createArgs(InternalAggregationFunction function) + { + ImmutableList.Builder list = ImmutableList.builder(); + for (int i = 0; i < function.getParameterTypes().size(); i++) { + list.add(i); + } + return list.build(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/window/CumulativeDistributionFunction.java b/presto-main/src/main/java/com/facebook/presto/operator/window/CumulativeDistributionFunction.java new file mode 100644 index 00000000..1cba999c --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/window/CumulativeDistributionFunction.java @@ -0,0 +1,48 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.window; + +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; + +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; + +public class CumulativeDistributionFunction + extends RankingWindowFunction +{ + private long totalCount; + private long count; + + @Override + public Type getType() + { + return DOUBLE; + } + + @Override + public void reset() + { + totalCount = windowIndex.size(); + count = 0; + } + + @Override + public void processRow(BlockBuilder output, boolean newPeerGroup, int peerGroupCount, int currentPosition) + { + if (newPeerGroup) { + count += peerGroupCount; + } + DOUBLE.writeDouble(output, ((double) count) / totalCount); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/window/DenseRankFunction.java b/presto-main/src/main/java/com/facebook/presto/operator/window/DenseRankFunction.java new file mode 100644 index 00000000..1388aa66 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/window/DenseRankFunction.java @@ -0,0 +1,46 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.window; + +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; + +import static com.facebook.presto.spi.type.BigintType.BIGINT; + +public class DenseRankFunction + extends RankingWindowFunction +{ + private long rank; + + @Override + public Type getType() + { + return BIGINT; + } + + @Override + public void reset() + { + rank = 0; + } + + @Override + public void processRow(BlockBuilder output, boolean newPeerGroup, int peerGroupCount, int currentPosition) + { + if (newPeerGroup) { + rank++; + } + BIGINT.writeLong(output, rank); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/window/FirstValueFunction.java b/presto-main/src/main/java/com/facebook/presto/operator/window/FirstValueFunction.java new file mode 100644 index 00000000..93f41899 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/window/FirstValueFunction.java @@ -0,0 +1,101 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.window; + +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; + +import java.util.List; + +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.BooleanType.BOOLEAN; +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static com.facebook.presto.spi.type.TimestampType.TIMESTAMP; +import static com.google.common.collect.Iterables.getOnlyElement; + +public class FirstValueFunction + extends ValueWindowFunction +{ + public static class BigintFirstValueFunction + extends FirstValueFunction + { + public BigintFirstValueFunction(List argumentChannels) + { + super(BIGINT, argumentChannels); + } + } + + public static class BooleanFirstValueFunction + extends FirstValueFunction + { + public BooleanFirstValueFunction(List argumentChannels) + { + super(BOOLEAN, argumentChannels); + } + } + + public static class DoubleFirstValueFunction + extends FirstValueFunction + { + public DoubleFirstValueFunction(List argumentChannels) + { + super(DOUBLE, argumentChannels); + } + } + + public static class VarcharFirstValueFunction + extends FirstValueFunction + { + public VarcharFirstValueFunction(List argumentChannels) + { + super(VARCHAR, argumentChannels); + } + } + + public static class TimestampFirstValueFunction + extends FirstValueFunction + { + public TimestampFirstValueFunction(List argumentChannels) + { + super(TIMESTAMP, argumentChannels); + } + } + + private final Type type; + private final int argumentChannel; + + protected FirstValueFunction(Type type, List argumentChannels) + { + this.type = type; + this.argumentChannel = getOnlyElement(argumentChannels); + } + + @Override + public Type getType() + { + return type; + } + + @Override + public void processRow(BlockBuilder output, int frameStart, int frameEnd, int currentPosition) + { + if (frameStart < 0) { + output.appendNull(); + return; + } + + windowIndex.appendTo(argumentChannel, frameStart, output); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/window/FrameInfo.java b/presto-main/src/main/java/com/facebook/presto/operator/window/FrameInfo.java new file mode 100644 index 00000000..d34fbcde --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/window/FrameInfo.java @@ -0,0 +1,82 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.window; + +import com.facebook.presto.sql.tree.FrameBound; +import com.facebook.presto.sql.tree.WindowFrame; + +import java.util.Optional; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static java.util.Objects.requireNonNull; + +public class FrameInfo +{ + private final WindowFrame.Type type; + private final FrameBound.Type startType; + private final int startChannel; + private final FrameBound.Type endType; + private final int endChannel; + + public FrameInfo( + WindowFrame.Type type, + FrameBound.Type startType, + Optional startChannel, + FrameBound.Type endType, + Optional endChannel) + { + this.type = requireNonNull(type, "type is null"); + this.startType = requireNonNull(startType, "startType is null"); + this.startChannel = requireNonNull(startChannel, "startChannel is null").orElse(-1); + this.endType = requireNonNull(endType, "endType is null"); + this.endChannel = requireNonNull(endChannel, "endChannel is null").orElse(-1); + } + + public WindowFrame.Type getType() + { + return type; + } + + public FrameBound.Type getStartType() + { + return startType; + } + + public int getStartChannel() + { + return startChannel; + } + + public FrameBound.Type getEndType() + { + return endType; + } + + public int getEndChannel() + { + return endChannel; + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("type", type) + .add("startType", startType) + .add("startChannel", startChannel) + .add("endType", endType) + .add("endChannel", endChannel) + .toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/window/LagFunction.java b/presto-main/src/main/java/com/facebook/presto/operator/window/LagFunction.java new file mode 100644 index 00000000..fdeaeec6 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/window/LagFunction.java @@ -0,0 +1,120 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.window; + +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; +import com.google.common.primitives.Ints; + +import java.util.List; + +import static com.facebook.presto.spi.StandardErrorCode.INVALID_FUNCTION_ARGUMENT; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.BooleanType.BOOLEAN; +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static com.facebook.presto.spi.type.TimestampType.TIMESTAMP; +import static com.facebook.presto.util.Failures.checkCondition; + +public class LagFunction + extends ValueWindowFunction +{ + public static class BigintLagFunction + extends LagFunction + { + public BigintLagFunction(List argumentChannels) + { + super(BIGINT, argumentChannels); + } + } + + public static class BooleanLagFunction + extends LagFunction + { + public BooleanLagFunction(List argumentChannels) + { + super(BOOLEAN, argumentChannels); + } + } + + public static class DoubleLagFunction + extends LagFunction + { + public DoubleLagFunction(List argumentChannels) + { + super(DOUBLE, argumentChannels); + } + } + + public static class VarcharLagFunction + extends LagFunction + { + public VarcharLagFunction(List argumentChannels) + { + super(VARCHAR, argumentChannels); + } + } + + public static class TimestampLagFunction + extends LagFunction + { + public TimestampLagFunction(List argumentChannels) + { + super(TIMESTAMP, argumentChannels); + } + } + + private final Type type; + private final int valueChannel; + private final int offsetChannel; + private final int defaultChannel; + + protected LagFunction(Type type, List argumentChannels) + { + this.type = type; + this.valueChannel = argumentChannels.get(0); + this.offsetChannel = (argumentChannels.size() > 1) ? argumentChannels.get(1) : -1; + this.defaultChannel = (argumentChannels.size() > 2) ? argumentChannels.get(2) : -1; + } + + @Override + public Type getType() + { + return type; + } + + @Override + public void processRow(BlockBuilder output, int frameStart, int frameEnd, int currentPosition) + { + if ((offsetChannel >= 0) && windowIndex.isNull(offsetChannel, currentPosition)) { + output.appendNull(); + } + else { + long offset = (offsetChannel < 0) ? 1 : windowIndex.getLong(offsetChannel, currentPosition); + checkCondition(offset >= 0, INVALID_FUNCTION_ARGUMENT, "Offset must be at least 0"); + + long valuePosition = currentPosition - offset; + + if ((valuePosition >= 0) && (valuePosition <= currentPosition)) { + windowIndex.appendTo(valueChannel, Ints.checkedCast(valuePosition), output); + } + else if (defaultChannel >= 0) { + windowIndex.appendTo(defaultChannel, currentPosition, output); + } + else { + output.appendNull(); + } + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/window/LastValueFunction.java b/presto-main/src/main/java/com/facebook/presto/operator/window/LastValueFunction.java new file mode 100644 index 00000000..f8508066 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/window/LastValueFunction.java @@ -0,0 +1,101 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.window; + +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; + +import java.util.List; + +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.BooleanType.BOOLEAN; +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static com.facebook.presto.spi.type.TimestampType.TIMESTAMP; +import static com.google.common.collect.Iterables.getOnlyElement; + +public class LastValueFunction + extends ValueWindowFunction +{ + public static class BigintLastValueFunction + extends LastValueFunction + { + public BigintLastValueFunction(List argumentChannels) + { + super(BIGINT, argumentChannels); + } + } + + public static class BooleanLastValueFunction + extends LastValueFunction + { + public BooleanLastValueFunction(List argumentChannels) + { + super(BOOLEAN, argumentChannels); + } + } + + public static class DoubleLastValueFunction + extends LastValueFunction + { + public DoubleLastValueFunction(List argumentChannels) + { + super(DOUBLE, argumentChannels); + } + } + + public static class VarcharLastValueFunction + extends LastValueFunction + { + public VarcharLastValueFunction(List argumentChannels) + { + super(VARCHAR, argumentChannels); + } + } + + public static class TimestampLastValueFunction + extends LastValueFunction + { + public TimestampLastValueFunction(List argumentChannels) + { + super(TIMESTAMP, argumentChannels); + } + } + + private final Type type; + private final int argumentChannel; + + protected LastValueFunction(Type type, List argumentChannels) + { + this.type = type; + this.argumentChannel = getOnlyElement(argumentChannels); + } + + @Override + public Type getType() + { + return type; + } + + @Override + public void processRow(BlockBuilder output, int frameStart, int frameEnd, int currentPosition) + { + if (frameStart < 0) { + output.appendNull(); + return; + } + + windowIndex.appendTo(argumentChannel, frameEnd, output); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/window/LeadFunction.java b/presto-main/src/main/java/com/facebook/presto/operator/window/LeadFunction.java new file mode 100644 index 00000000..c60ad3bc --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/window/LeadFunction.java @@ -0,0 +1,120 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.window; + +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; +import com.google.common.primitives.Ints; + +import java.util.List; + +import static com.facebook.presto.spi.StandardErrorCode.INVALID_FUNCTION_ARGUMENT; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.BooleanType.BOOLEAN; +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static com.facebook.presto.spi.type.TimestampType.TIMESTAMP; +import static com.facebook.presto.util.Failures.checkCondition; + +public class LeadFunction + extends ValueWindowFunction +{ + public static class BigintLeadFunction + extends LeadFunction + { + public BigintLeadFunction(List argumentChannels) + { + super(BIGINT, argumentChannels); + } + } + + public static class BooleanLeadFunction + extends LeadFunction + { + public BooleanLeadFunction(List argumentChannels) + { + super(BOOLEAN, argumentChannels); + } + } + + public static class DoubleLeadFunction + extends LeadFunction + { + public DoubleLeadFunction(List argumentChannels) + { + super(DOUBLE, argumentChannels); + } + } + + public static class VarcharLeadFunction + extends LeadFunction + { + public VarcharLeadFunction(List argumentChannels) + { + super(VARCHAR, argumentChannels); + } + } + + public static class TimestampLeadFunction + extends LeadFunction + { + public TimestampLeadFunction(List argumentChannels) + { + super(TIMESTAMP, argumentChannels); + } + } + + private final Type type; + private final int valueChannel; + private final int offsetChannel; + private final int defaultChannel; + + protected LeadFunction(Type type, List argumentChannels) + { + this.type = type; + this.valueChannel = argumentChannels.get(0); + this.offsetChannel = (argumentChannels.size() > 1) ? argumentChannels.get(1) : -1; + this.defaultChannel = (argumentChannels.size() > 2) ? argumentChannels.get(2) : -1; + } + + @Override + public Type getType() + { + return type; + } + + @Override + public void processRow(BlockBuilder output, int frameStart, int frameEnd, int currentPosition) + { + if ((offsetChannel >= 0) && windowIndex.isNull(offsetChannel, currentPosition)) { + output.appendNull(); + } + else { + long offset = (offsetChannel < 0) ? 1 : windowIndex.getLong(offsetChannel, currentPosition); + checkCondition(offset >= 0, INVALID_FUNCTION_ARGUMENT, "Offset must be at least 0"); + + long valuePosition = currentPosition + offset; + + if ((valuePosition >= 0) && (valuePosition < windowIndex.size())) { + windowIndex.appendTo(valueChannel, Ints.checkedCast(valuePosition), output); + } + else if (defaultChannel >= 0) { + windowIndex.appendTo(defaultChannel, currentPosition, output); + } + else { + output.appendNull(); + } + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/window/NTileFunction.java b/presto-main/src/main/java/com/facebook/presto/operator/window/NTileFunction.java new file mode 100644 index 00000000..e9507103 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/window/NTileFunction.java @@ -0,0 +1,79 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.window; + +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; + +import java.util.List; + +import static com.facebook.presto.spi.StandardErrorCode.INVALID_FUNCTION_ARGUMENT; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.util.Failures.checkCondition; + +public class NTileFunction + extends RankingWindowFunction +{ + private final int valueChannel; + private int rowCount; + + public NTileFunction(List argumentChannels) + { + this.valueChannel = argumentChannels.get(0); + } + + @Override + public Type getType() + { + return BIGINT; + } + + @Override + public void reset() + { + rowCount = windowIndex.size(); + } + + @Override + public void processRow(BlockBuilder output, boolean newPeerGroup, int peerGroupCount, int currentPosition) + { + if (windowIndex.isNull(valueChannel, currentPosition)) { + output.appendNull(); + } + else { + long buckets = windowIndex.getLong(valueChannel, currentPosition); + checkCondition(buckets > 0, INVALID_FUNCTION_ARGUMENT, "Buckets must be greater than 0"); + BIGINT.writeLong(output, bucket(buckets, currentPosition) + 1); + } + } + + private long bucket(long buckets, int currentRow) + { + if (rowCount < buckets) { + return currentRow; + } + + long remainderRows = rowCount % buckets; + long rowsPerBucket = rowCount / buckets; + + // Remainder rows are assigned starting from the first bucket. + // Thus, each of those buckets have an additional row. + if (currentRow < ((rowsPerBucket + 1) * remainderRows)) { + return currentRow / (rowsPerBucket + 1); + } + + // Shift the remaining rows to account for the remainder rows. + return (currentRow - remainderRows) / rowsPerBucket; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/window/NthValueFunction.java b/presto-main/src/main/java/com/facebook/presto/operator/window/NthValueFunction.java new file mode 100644 index 00000000..667ca10c --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/window/NthValueFunction.java @@ -0,0 +1,116 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.window; + +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; +import com.google.common.primitives.Ints; + +import java.util.List; + +import static com.facebook.presto.spi.StandardErrorCode.INVALID_FUNCTION_ARGUMENT; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.BooleanType.BOOLEAN; +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static com.facebook.presto.spi.type.TimestampType.TIMESTAMP; +import static com.facebook.presto.util.Failures.checkCondition; + +public class NthValueFunction + extends ValueWindowFunction +{ + public static class BigintNthValueFunction + extends NthValueFunction + { + public BigintNthValueFunction(List argumentChannels) + { + super(BIGINT, argumentChannels); + } + } + + public static class BooleanNthValueFunction + extends NthValueFunction + { + public BooleanNthValueFunction(List argumentChannels) + { + super(BOOLEAN, argumentChannels); + } + } + + public static class DoubleNthValueFunction + extends NthValueFunction + { + public DoubleNthValueFunction(List argumentChannels) + { + super(DOUBLE, argumentChannels); + } + } + + public static class VarcharNthValueFunction + extends NthValueFunction + { + public VarcharNthValueFunction(List argumentChannels) + { + super(VARCHAR, argumentChannels); + } + } + + public static class TimestampNthValueFunction + extends NthValueFunction + { + public TimestampNthValueFunction(List argumentChannels) + { + super(TIMESTAMP, argumentChannels); + } + } + + private final Type type; + private final int valueChannel; + private final int offsetChannel; + + protected NthValueFunction(Type type, List argumentChannels) + { + this.type = type; + this.valueChannel = argumentChannels.get(0); + this.offsetChannel = argumentChannels.get(1); + } + + @Override + public Type getType() + { + return type; + } + + @Override + public void processRow(BlockBuilder output, int frameStart, int frameEnd, int currentPosition) + { + if ((frameStart < 0) || windowIndex.isNull(offsetChannel, currentPosition)) { + output.appendNull(); + } + else { + long offset = windowIndex.getLong(offsetChannel, currentPosition); + checkCondition(offset >= 1, INVALID_FUNCTION_ARGUMENT, "Offset must be at least 1"); + + // offset is base 1 + long valuePosition = frameStart + (offset - 1); + + if ((valuePosition >= frameStart) && (valuePosition <= frameEnd)) { + windowIndex.appendTo(valueChannel, Ints.checkedCast(valuePosition), output); + } + else { + output.appendNull(); + } + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/window/PercentRankFunction.java b/presto-main/src/main/java/com/facebook/presto/operator/window/PercentRankFunction.java new file mode 100644 index 00000000..98a04116 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/window/PercentRankFunction.java @@ -0,0 +1,60 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.window; + +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; + +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; + +public class PercentRankFunction + extends RankingWindowFunction +{ + private long totalCount; + private long rank; + private long count; + + @Override + public Type getType() + { + return DOUBLE; + } + + @Override + public void reset() + { + totalCount = windowIndex.size(); + rank = 0; + count = 1; + } + + @Override + public void processRow(BlockBuilder output, boolean newPeerGroup, int peerGroupCount, int currentPosition) + { + if (totalCount == 1) { + DOUBLE.writeDouble(output, 0.0); + return; + } + + if (newPeerGroup) { + rank += count; + count = 1; + } + else { + count++; + } + + DOUBLE.writeDouble(output, ((double) (rank - 1)) / (totalCount - 1)); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/window/RankFunction.java b/presto-main/src/main/java/com/facebook/presto/operator/window/RankFunction.java new file mode 100644 index 00000000..ac9e4151 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/window/RankFunction.java @@ -0,0 +1,52 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.window; + +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; + +import static com.facebook.presto.spi.type.BigintType.BIGINT; + +public class RankFunction + extends RankingWindowFunction +{ + private long rank; + private long count; + + @Override + public Type getType() + { + return BIGINT; + } + + @Override + public void reset() + { + rank = 0; + count = 1; + } + + @Override + public void processRow(BlockBuilder output, boolean newPeerGroup, int peerGroupCount, int currentPosition) + { + if (newPeerGroup) { + rank += count; + count = 1; + } + else { + count++; + } + BIGINT.writeLong(output, rank); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/window/RankingWindowFunction.java b/presto-main/src/main/java/com/facebook/presto/operator/window/RankingWindowFunction.java new file mode 100644 index 00000000..252eeed9 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/window/RankingWindowFunction.java @@ -0,0 +1,74 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.window; + +import com.facebook.presto.spi.block.BlockBuilder; + +public abstract class RankingWindowFunction + implements WindowFunction +{ + protected WindowIndex windowIndex; + + private int currentPeerGroupStart; + private int currentPosition; + + @Override + public final void reset(WindowIndex windowIndex) + { + this.windowIndex = windowIndex; + this.currentPeerGroupStart = -1; + this.currentPosition = 0; + + reset(); + } + + @Override + public final void processRow(BlockBuilder output, int peerGroupStart, int peerGroupEnd, int frameStart, int frameEnd) + { + boolean newPeerGroup = false; + if (peerGroupStart != currentPeerGroupStart) { + currentPeerGroupStart = peerGroupStart; + newPeerGroup = true; + } + + int peerGroupCount = (peerGroupEnd - peerGroupStart) + 1; + + processRow(output, newPeerGroup, peerGroupCount, currentPosition); + + currentPosition++; + } + + /** + * Reset state for a new partition (including the first one). + */ + public void reset() + { + // subclasses can override + } + + /** + * Process a row by outputting the result of the window function. + *

+ * This method provides information about the ordering peer group. A peer group is all + * of the rows that are peers within the specified ordering. Rows are peers if they + * compare equal to each other using the specified ordering expression. The ordering + * of rows within a peer group is undefined (otherwise they would not be peers). + * + * @param output the {@link BlockBuilder} to use for writing the output row + * @param newPeerGroup if this row starts a new peer group + * @param peerGroupCount the total number of rows in this peer group + * @param currentPosition the current position for this row + */ + public abstract void processRow(BlockBuilder output, boolean newPeerGroup, int peerGroupCount, int currentPosition); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/window/ReflectionWindowFunctionSupplier.java b/presto-main/src/main/java/com/facebook/presto/operator/window/ReflectionWindowFunctionSupplier.java new file mode 100644 index 00000000..ea5273b9 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/window/ReflectionWindowFunctionSupplier.java @@ -0,0 +1,79 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.window; + +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.operator.Description; +import com.facebook.presto.spi.type.Type; +import com.google.common.base.Throwables; +import com.google.common.collect.Lists; + +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class ReflectionWindowFunctionSupplier + extends AbstractWindowFunctionSupplier +{ + private final Constructor constructor; + + public ReflectionWindowFunctionSupplier(String name, Type returnType, List argumentTypes, Class type) + { + this(new Signature(name, returnType.getTypeSignature(), Lists.transform(argumentTypes, Type::getTypeSignature)), type); + } + + public ReflectionWindowFunctionSupplier(Signature signature, Class type) + { + super(signature, getDescription(checkNotNull(type, "type is null"))); + try { + if (signature.getArgumentTypes().isEmpty()) { + constructor = type.getConstructor(); + } + else { + constructor = type.getConstructor(List.class); + } + } + catch (NoSuchMethodException e) { + throw Throwables.propagate(e); + } + } + + @Override + protected T newWindowFunction(List inputs) + { + try { + if (getSignature().getArgumentTypes().isEmpty()) { + return constructor.newInstance(); + } + else { + return constructor.newInstance(inputs); + } + } + catch (InvocationTargetException e) { + throw Throwables.propagate(e.getCause()); + } + catch (Exception e) { + throw Throwables.propagate(e); + } + } + + private static String getDescription(AnnotatedElement annotatedElement) + { + Description description = annotatedElement.getAnnotation(Description.class); + return (description == null) ? null : description.value(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/window/RowNumberFunction.java b/presto-main/src/main/java/com/facebook/presto/operator/window/RowNumberFunction.java new file mode 100644 index 00000000..f83868c0 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/window/RowNumberFunction.java @@ -0,0 +1,35 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.window; + +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; + +import static com.facebook.presto.spi.type.BigintType.BIGINT; + +public class RowNumberFunction + extends RankingWindowFunction +{ + @Override + public Type getType() + { + return BIGINT; + } + + @Override + public void processRow(BlockBuilder output, boolean newPeerGroup, int peerGroupCount, int currentPosition) + { + BIGINT.writeLong(output, currentPosition + 1); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/window/ValueWindowFunction.java b/presto-main/src/main/java/com/facebook/presto/operator/window/ValueWindowFunction.java new file mode 100644 index 00000000..6be30d47 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/window/ValueWindowFunction.java @@ -0,0 +1,59 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.window; + +import com.facebook.presto.spi.block.BlockBuilder; + +public abstract class ValueWindowFunction + implements WindowFunction +{ + protected WindowIndex windowIndex; + + private int currentPosition; + + @Override + public final void reset(WindowIndex windowIndex) + { + this.windowIndex = windowIndex; + this.currentPosition = 0; + + reset(); + } + + @Override + public final void processRow(BlockBuilder output, int peerGroupStart, int peerGroupEnd, int frameStart, int frameEnd) + { + processRow(output, frameStart, frameEnd, currentPosition); + + currentPosition++; + } + + /** + * Reset state for a new partition (including the first one). + */ + public void reset() + { + // subclasses can override + } + + /** + * Process a row by outputting the result of the window function. + * + * @param output the {@link com.facebook.presto.spi.block.BlockBuilder} to use for writing the output row + * @param frameStart the position of the first row in the window frame + * @param frameEnd the position of the last row in the window frame + * @param currentPosition the current position for this row + */ + public abstract void processRow(BlockBuilder output, int frameStart, int frameEnd, int currentPosition); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/window/WindowFunction.java b/presto-main/src/main/java/com/facebook/presto/operator/window/WindowFunction.java new file mode 100644 index 00000000..29805138 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/window/WindowFunction.java @@ -0,0 +1,45 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.window; + +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; + +public interface WindowFunction +{ + Type getType(); + + /** + * Reset state for a new partition (including the first one). + * + * @param windowIndex the window index which contains sorted values for the partition + */ + void reset(WindowIndex windowIndex); + + /** + * Process a row by outputting the result of the window function. + *

+ * This method provides information about the ordering peer group. A peer group is all + * of the rows that are peers within the specified ordering. Rows are peers if they + * compare equal to each other using the specified ordering expression. The ordering + * of rows within a peer group is undefined (otherwise they would not be peers). + * + * @param output the {@link BlockBuilder} to use for writing the output row + * @param peerGroupStart the position of the first row in the peer group + * @param peerGroupEnd the position of the last row in the peer group + * @param frameStart the position of the first row in the window frame + * @param frameEnd the position of the last row in the window frame + */ + void processRow(BlockBuilder output, int peerGroupStart, int peerGroupEnd, int frameStart, int frameEnd); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/window/WindowFunctionSupplier.java b/presto-main/src/main/java/com/facebook/presto/operator/window/WindowFunctionSupplier.java new file mode 100644 index 00000000..dc52bded --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/window/WindowFunctionSupplier.java @@ -0,0 +1,27 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.window; + +import com.facebook.presto.metadata.Signature; + +import java.util.List; + +public interface WindowFunctionSupplier +{ + Signature getSignature(); + + String getDescription(); + + WindowFunction createWindowFunction(List argumentChannels); +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/window/WindowIndex.java b/presto-main/src/main/java/com/facebook/presto/operator/window/WindowIndex.java new file mode 100644 index 00000000..84f2d091 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/window/WindowIndex.java @@ -0,0 +1,92 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.window; + +import com.facebook.presto.operator.PagesIndex; +import com.facebook.presto.spi.block.BlockBuilder; +import io.airlift.slice.Slice; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkElementIndex; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkPositionIndex; + +public class WindowIndex +{ + private final PagesIndex pagesIndex; + private final int start; + private final int size; + + public WindowIndex(PagesIndex pagesIndex, int start, int end) + { + checkNotNull(pagesIndex, "pagesIndex is null"); + checkPositionIndex(start, pagesIndex.getPositionCount(), "start"); + checkPositionIndex(end, pagesIndex.getPositionCount(), "end"); + checkArgument(start < end, "start must be before end"); + + this.pagesIndex = pagesIndex; + this.start = start; + this.size = end - start; + } + + public int size() + { + return size; + } + + public boolean isNull(int channel, int position) + { + return pagesIndex.isNull(channel, position(position)); + } + + public boolean getBoolean(int channel, int position) + { + return pagesIndex.getBoolean(channel, position(position)); + } + + public long getLong(int channel, int position) + { + return pagesIndex.getLong(channel, position(position)); + } + + public double getDouble(int channel, int position) + { + return pagesIndex.getDouble(channel, position(position)); + } + + public Slice getSlice(int channel, int position) + { + return pagesIndex.getSlice(channel, position(position)); + } + + public void appendTo(int channel, int position, BlockBuilder output) + { + pagesIndex.appendTo(channel, position(position), output); + } + + private int position(int position) + { + checkElementIndex(position, size, "position"); + return position + start; + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("size", size) + .toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/operator/window/WindowPartition.java b/presto-main/src/main/java/com/facebook/presto/operator/window/WindowPartition.java new file mode 100644 index 00000000..a9071281 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/operator/window/WindowPartition.java @@ -0,0 +1,232 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.operator.window; + +import com.facebook.presto.operator.PagesHashStrategy; +import com.facebook.presto.operator.PagesIndex; +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.sql.tree.FrameBound; +import com.google.common.primitives.Ints; + +import java.util.List; + +import static com.facebook.presto.spi.StandardErrorCode.INVALID_WINDOW_FRAME; +import static com.facebook.presto.sql.tree.FrameBound.Type.FOLLOWING; +import static com.facebook.presto.sql.tree.FrameBound.Type.PRECEDING; +import static com.facebook.presto.sql.tree.FrameBound.Type.UNBOUNDED_FOLLOWING; +import static com.facebook.presto.sql.tree.FrameBound.Type.UNBOUNDED_PRECEDING; +import static com.facebook.presto.sql.tree.WindowFrame.Type.RANGE; +import static com.facebook.presto.util.Failures.checkCondition; +import static com.google.common.base.Preconditions.checkState; + +public final class WindowPartition +{ + private final PagesIndex pagesIndex; + private final int partitionStart; + private final int partitionEnd; + + private final int[] outputChannels; + private final List windowFunctions; + private final FrameInfo frameInfo; + private final PagesHashStrategy peerGroupHashStrategy; + + private int peerGroupStart; + private int peerGroupEnd; + private int frameStart; + private int frameEnd; + + private int currentPosition; + + public WindowPartition(PagesIndex pagesIndex, + int partitionStart, + int partitionEnd, + int[] outputChannels, + List windowFunctions, + FrameInfo frameInfo, + PagesHashStrategy peerGroupHashStrategy) + { + this.pagesIndex = pagesIndex; + this.partitionStart = partitionStart; + this.partitionEnd = partitionEnd; + this.outputChannels = outputChannels; + this.windowFunctions = windowFunctions; + this.frameInfo = frameInfo; + this.peerGroupHashStrategy = peerGroupHashStrategy; + + // reset functions for new partition + WindowIndex windowIndex = new WindowIndex(pagesIndex, partitionStart, partitionEnd); + for (WindowFunction windowFunction : windowFunctions) { + windowFunction.reset(windowIndex); + } + + currentPosition = partitionStart; + updatePeerGroup(); + } + + public int getPartitionEnd() + { + return partitionEnd; + } + + public boolean hasNext() + { + return currentPosition < partitionEnd; + } + + public void processNextRow(PageBuilder pageBuilder) + { + checkState(hasNext(), "No more rows in partition"); + + // copy output channels + pageBuilder.declarePosition(); + int channel = 0; + while (channel < outputChannels.length) { + pagesIndex.appendTo(outputChannels[channel], currentPosition, pageBuilder.getBlockBuilder(channel)); + channel++; + } + + // check for new peer group + if (currentPosition == peerGroupEnd) { + updatePeerGroup(); + } + + // compute window frame + updateFrame(); + + // process window functions + for (WindowFunction function : windowFunctions) { + function.processRow( + pageBuilder.getBlockBuilder(channel), + peerGroupStart - partitionStart, + peerGroupEnd - partitionStart - 1, + frameStart, + frameEnd); + channel++; + } + + currentPosition++; + } + + private void updatePeerGroup() + { + peerGroupStart = currentPosition; + // find end of peer group + peerGroupEnd = peerGroupStart + 1; + while ((peerGroupEnd < partitionEnd) && pagesIndex.positionEqualsPosition(peerGroupHashStrategy, peerGroupStart, peerGroupEnd)) { + peerGroupEnd++; + } + } + + private void updateFrame() + { + int rowPosition = currentPosition - partitionStart; + int endPosition = partitionEnd - partitionStart - 1; + + // frame start + if (frameInfo.getStartType() == UNBOUNDED_PRECEDING) { + frameStart = 0; + } + else if (frameInfo.getStartType() == PRECEDING) { + frameStart = preceding(rowPosition, getStartValue()); + } + else if (frameInfo.getStartType() == FOLLOWING) { + frameStart = following(rowPosition, endPosition, getStartValue()); + } + else if (frameInfo.getType() == RANGE) { + frameStart = peerGroupStart - partitionStart; + } + else { + frameStart = rowPosition; + } + + // frame end + if (frameInfo.getEndType() == UNBOUNDED_FOLLOWING) { + frameEnd = endPosition; + } + else if (frameInfo.getEndType() == PRECEDING) { + frameEnd = preceding(rowPosition, getEndValue()); + } + else if (frameInfo.getEndType() == FOLLOWING) { + frameEnd = following(rowPosition, endPosition, getEndValue()); + } + else if (frameInfo.getType() == RANGE) { + frameEnd = peerGroupEnd - partitionStart - 1; + } + else { + frameEnd = rowPosition; + } + + // handle empty frame + if (emptyFrame(rowPosition, endPosition)) { + frameStart = -1; + frameEnd = -1; + } + } + + private boolean emptyFrame(int rowPosition, int endPosition) + { + if (frameInfo.getStartType() != frameInfo.getEndType()) { + return false; + } + + FrameBound.Type type = frameInfo.getStartType(); + if ((type != PRECEDING) && (type != FOLLOWING)) { + return false; + } + + long start = getStartValue(); + long end = getEndValue(); + + if (type == PRECEDING) { + return (start < end) || ((start > rowPosition) && (end > rowPosition)); + } + + int positions = endPosition - rowPosition; + return (start > end) || ((start > positions) && (end > positions)); + } + + private static int preceding(int rowPosition, long value) + { + if (value > rowPosition) { + return 0; + } + return Ints.checkedCast(rowPosition - value); + } + + private static int following(int rowPosition, int endPosition, long value) + { + if (value > (endPosition - rowPosition)) { + return endPosition; + } + return Ints.checkedCast(rowPosition + value); + } + + private long getStartValue() + { + return getFrameValue(frameInfo.getStartChannel(), "starting"); + } + + private long getEndValue() + { + return getFrameValue(frameInfo.getEndChannel(), "ending"); + } + + private long getFrameValue(int channel, String type) + { + checkCondition(!pagesIndex.isNull(channel, currentPosition), INVALID_WINDOW_FRAME, "Window frame %s offset must not be null", type); + long value = pagesIndex.getLong(channel, currentPosition); + checkCondition(value >= 0, INVALID_WINDOW_FRAME, "Window frame %s offset must not be negative"); + return value; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/pdbo/PdboConfig.java b/presto-main/src/main/java/com/facebook/presto/pdbo/PdboConfig.java new file mode 100644 index 00000000..f4398b01 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/pdbo/PdboConfig.java @@ -0,0 +1,115 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.pdbo; + +import io.airlift.configuration.Config; +import io.airlift.units.Duration; + +import java.util.concurrent.TimeUnit; + +public class PdboConfig +{ + public static final String DEFAULT_VALUE = "NA"; + private Duration pdboRefreshInterval = new Duration(3, TimeUnit.MINUTES); + private Duration pdboCleanHistoryInterval = new Duration(6, TimeUnit.HOURS); + private boolean pdboExecuteEnable = false; + private String pdboConnectionURL = DEFAULT_VALUE; + private String pdboConnectionUser = DEFAULT_VALUE; + private String pdboConnectionPassword = DEFAULT_VALUE; + private int pdboCalcThreads = 4; + + public Duration getPdboRefreshInterval() + { + return pdboRefreshInterval; + } + + @Config("pdbo-refresh-interval") + public PdboConfig setPdboRefreshInterval(Duration pdboRefreshInterval) + { + this.pdboRefreshInterval = pdboRefreshInterval; + return this; + } + + public Duration getPdboCleanHistoryInterval() + { + return pdboCleanHistoryInterval; + } + + @Config("pdbo-clean-history-interval") + public PdboConfig setPdboCleanHistoryInterval(Duration pdboCleanHistoryInterval) + { + this.pdboCleanHistoryInterval = pdboCleanHistoryInterval; + return this; + } + + public boolean getPdboExecuteEnable() + { + return pdboExecuteEnable; + } + + @Config("pdbo-execute-enable") + public PdboConfig setPdboExecuteEnable(boolean pdboExecuteEnable) + { + this.pdboExecuteEnable = pdboExecuteEnable; + return this; + } + + public String getPdboConnectionURL() + { + return pdboConnectionURL; + } + + @Config("pdbo-connection-url") + public PdboConfig setPdboConnectionURL(String pdboConnectionURL) + { + this.pdboConnectionURL = pdboConnectionURL; + return this; + } + + public String getPdboConnectionUser() + { + return pdboConnectionUser; + } + + @Config("pdbo-connection-user") + public PdboConfig setPdboConnectionUser(String pdboConnectionUser) + { + this.pdboConnectionUser = pdboConnectionUser; + return this; + } + + public String getPdboConnectionPassword() + { + return pdboConnectionPassword; + } + + @Config("pdbo-connection-password") + public PdboConfig setPdboConnectionPassword(String pdboConnectionPassword) + { + this.pdboConnectionPassword = pdboConnectionPassword; + return this; + } + + public int getPdboCalcThreads() + { + return pdboCalcThreads; + } + + @Config("pdbo-calc-threads") + public PdboConfig setPdboCalcThreads(int pdboCalcThreads) + { + this.pdboCalcThreads = pdboCalcThreads; + return this; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/pdbo/PdboManager.java b/presto-main/src/main/java/com/facebook/presto/pdbo/PdboManager.java new file mode 100644 index 00000000..74b03121 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/pdbo/PdboManager.java @@ -0,0 +1,53 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.pdbo; + +import io.airlift.log.Logger; + +import java.sql.SQLException; +import java.util.Properties; + +import javax.inject.Inject; + +import com.facebook.presto.server.ServerConfig; + +public class PdboManager +{ + private static final Logger log = Logger.get(PdboManager.class); + + private StepCalcManager stepCalcManager; + @Inject + public PdboManager(PdboConfig config, ServerConfig serverConfig) + { + boolean pdboExecuteEnable = config.getPdboExecuteEnable(); + Properties connectionProperties = new Properties(); + connectionProperties.setProperty("user", config.getPdboConnectionUser()); + connectionProperties.setProperty("password", config.getPdboConnectionPassword()); + if (pdboExecuteEnable && serverConfig.isCoordinator()) { + try { + stepCalcManager = new StepCalcManager(config.getPdboConnectionURL(), + connectionProperties, config.getPdboRefreshInterval(), + config.getPdboCleanHistoryInterval(), config.getPdboCalcThreads()); + } + catch (SQLException e) { + log.error("start StepCalcManager error", e.getMessage()); + } + Thread stepCalcThread = new Thread(stepCalcManager); + stepCalcThread.setName("step calc thread"); + stepCalcThread.setDaemon(true); + stepCalcThread.start(); + stepCalcManager.start(); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/pdbo/PdboTable.java b/presto-main/src/main/java/com/facebook/presto/pdbo/PdboTable.java new file mode 100644 index 00000000..d6a3ac7a --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/pdbo/PdboTable.java @@ -0,0 +1,132 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.pdbo; + +import com.google.common.primitives.Longs; + +public class PdboTable implements Comparable +{ + private String connectorId; + private String schemaName; + private String tableName; + private Long rows; + private Long beginIndex; + private Long endIndex; + private String recordFlag; + private Integer scanNodes; + + public String getConnectorId() + { + return connectorId; + } + + public PdboTable setConnectorId(String connectorId) + { + this.connectorId = connectorId; + return this; + } + + public String getSchemaName() + { + return schemaName; + } + + public PdboTable setSchemaName(String schemaName) + { + this.schemaName = schemaName; + return this; + } + + public String getTableName() + { + return tableName; + } + + public PdboTable setTableName(String tableName) + { + this.tableName = tableName; + return this; + } + + public Long getRows() + { + return rows; + } + + public PdboTable setRows(Long rows) + { + this.rows = rows; + return this; + } + + public Long getBeginIndex() + { + return beginIndex; + } + + public PdboTable setBeginIndex(Long beginIndex) + { + this.beginIndex = beginIndex; + return this; + } + + public Long getEndIndex() + { + return endIndex; + } + + public PdboTable setEndIndex(Long endIndex) + { + this.endIndex = endIndex; + return this; + } + + public String getRecordFlag() + { + return recordFlag; + } + + public PdboTable setRecordFlag(String recordFlag) + { + this.recordFlag = recordFlag; + return this; + } + + public Integer getScanNodes() + { + return scanNodes; + } + + public PdboTable setScanNodes(Integer scanNodes) + { + this.scanNodes = scanNodes; + return this; + } + + @Override + public int compareTo(PdboTable o) + { + return Longs.compare(this.hashCode(), o.hashCode()); + } + + @Override + public String toString() + { + return getConnectorId() + "-" + + getSchemaName() + "-" + + getTableName() + "-" + + getBeginIndex() + "-" + + getEndIndex() + "-"; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/pdbo/StepCalcManager.java b/presto-main/src/main/java/com/facebook/presto/pdbo/StepCalcManager.java new file mode 100644 index 00000000..a8aedd53 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/pdbo/StepCalcManager.java @@ -0,0 +1,411 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.pdbo; + +import static com.google.common.base.Preconditions.checkState; +import static io.airlift.concurrent.Threads.threadsNamed; +import static java.util.concurrent.Executors.newCachedThreadPool; +import io.airlift.concurrent.SetThreadName; +import io.airlift.concurrent.ThreadPoolExecutorMBean; +import io.airlift.log.Logger; +import io.airlift.units.Duration; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.PriorityBlockingQueue; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.atomic.AtomicLong; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; + +import org.apache.commons.dbutils.DbUtils; +import org.apache.commons.dbutils.QueryRunner; +import org.apache.commons.dbutils.ResultSetHandler; + +import com.mysql.jdbc.Driver; + +public class StepCalcManager implements Runnable +{ + private static final Logger log = Logger.get(StepCalcManager.class); + public static final String PDBO_DATABASE = "route_schema"; + public static final String PDBO_TABLE = PDBO_DATABASE + ".pdbo_table"; + public static final String PDBO_ROUTE = PDBO_DATABASE + ".pdbo_route"; + public static final String PDBO_LOG = PDBO_DATABASE + ".pdbo_log"; + private final ExecutorService executor; + private final ThreadPoolExecutorMBean executorMBean; + private final PriorityBlockingQueue pdboQueue; + + private static final AtomicLong NEXT_RUNNER_ID = new AtomicLong(); + private volatile boolean closed; + private final String connectionUrl; + private final Properties connectionProperties; + private final Duration pdboRefreshInterval; + private final Duration pdboCleanHistoryInterval; + private final int pdboCalcThreads; + private long pdboLastUpdateTime = 0L; + + private final Driver driver; + + public StepCalcManager(String connectionUrl, + Properties connectionProperties, + Duration pdboRefreshInterval, + Duration pdboCleanHistoryInterval, + int pdboCalcThreads) throws SQLException + { + this.executor = newCachedThreadPool(threadsNamed("step-calc-processor-%d")); + this.executorMBean = new ThreadPoolExecutorMBean((ThreadPoolExecutor) executor); + + this.pdboQueue = new PriorityBlockingQueue<>(Runtime.getRuntime().availableProcessors() * 10); + this.closed = false; + this.connectionUrl = connectionUrl; + this.connectionProperties = connectionProperties; + this.pdboRefreshInterval = pdboRefreshInterval; + this.pdboCleanHistoryInterval = pdboCleanHistoryInterval; + this.pdboCalcThreads = pdboCalcThreads; + this.driver = new Driver(); + } + + public void run() + { + while (!closed) { + try { + if (pdboLastUpdateTime == 0L) { + addTableInfo(); + } + else { + Thread.sleep(pdboRefreshInterval.toMillis()); + long curTime = System.currentTimeMillis(); + addTableInfo(); + log.debug("Load pdbo table spend time : " + (System.currentTimeMillis() - curTime) + " ms"); + } + long curTime = System.currentTimeMillis(); + if ((curTime - pdboLastUpdateTime) >= pdboCleanHistoryInterval.toMillis()) { + cleanPdboHistoryLogs(); + log.debug("clean pdbo history logs finish in " + (System.currentTimeMillis() - curTime) + " ms"); + } + pdboLastUpdateTime = System.currentTimeMillis(); + } + catch (Exception e) { + log.error("Load pdbo table error : ", e.getMessage()); + pdboLastUpdateTime = System.currentTimeMillis(); + } + } + } + + public void addTableInfo() + { + List tables = null; + String sql = "SELECT CONNECTORID,PRESTO_SCHEMA,PRESTO_TABLE FROM " + PDBO_TABLE + + " WHERE CALC_STEP_ENABLE = 'Y' GROUP BY CONNECTORID,PRESTO_SCHEMA,PRESTO_TABLE"; + QueryRunner runner = new QueryRunner(); + Connection conn = getConnection(); + try { + tables = runner.query(conn, sql, new PdboTableResultHandler()); + } + catch (SQLException e) { + log.error(e, "SQL : " + sql + ",addTableInfo error %s", e.getMessage()); + } + finally { + DbUtils.closeQuietly(conn); + } + startSplit(tables); + } + + private class PdboTableResultHandler implements ResultSetHandler> + { + @Override + public List handle(ResultSet rs) throws SQLException + { + List tables = new ArrayList<>(); + while (rs.next()) { + tables.add(new PdboTable() + .setConnectorId(rs.getString(1)) + .setSchemaName(rs.getString(2)) + .setTableName(rs.getString(3))); + } + return tables; + } + } + + public void cleanPdboHistoryLogs() + { + String sql = "DELETE FROM " + PDBO_LOG + " WHERE RECORDFLAG IN ('runhistory','calchistory')"; + Connection conn = getConnection(); + QueryRunner runner = new QueryRunner(); + try { + runner.update(conn, sql); + } + catch (SQLException e) { + log.error(e, "SQL : " + sql + ",cleanPdboHistoryLogs error %s", e.getMessage()); + } + finally { + DbUtils.closeQuietly(conn); + } + } + + private synchronized void startSplit(List tables) + { + pdboQueue.addAll(tables); + } + + @PostConstruct + public synchronized void start() + { + checkState(!closed, "StepCalcManager is closed"); + for (int i = 0; i < pdboCalcThreads; i++) { + addRunnerThread(); + } + } + + @PreDestroy + public synchronized void stop() + { + closed = true; + executor.shutdownNow(); + } + + private synchronized void addRunnerThread() + { + try { + executor.execute(new Runner()); + } + catch (RejectedExecutionException ignored) { + } + } + + private class Runner implements Runnable + { + private final long runnerId = NEXT_RUNNER_ID.getAndIncrement(); + + @Override + public void run() + { + try (SetThreadName runnerName = new SetThreadName("SplitRunner-%s", runnerId)) { + while (!closed && !Thread.currentThread().isInterrupted()) { + final PdboTable take; + try { + take = pdboQueue.take(); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + stepCalc(take); + } + } + finally { + if (!closed) { + addRunnerThread(); + } + } + } + } + + public void stepCalc(PdboTable table) + { + List tableLogs = getTableRunningLogs(table); + if (tableLogs == null || tableLogs.size() == 0) { + return; + } + long allRows = tableLogs.stream().mapToLong(PdboTable::getRows).sum(); + int scanNodes = tableLogs.size(); + if (tableLogs.get(0).getScanNodes() != scanNodes) { + return; + } + long averageStep = allRows / scanNodes; + List result = new ArrayList<>(); + List otherSplit = new ArrayList<>(); + int usedNodes = combineTableLog(tableLogs, averageStep, result, otherSplit); + int unUsedNodes = scanNodes - usedNodes; + long calcRows = otherSplit.stream().mapToLong(PdboTable::getRows).sum(); + for (PdboTable split : otherSplit) { + long curNode = Math.round((split.getRows() * 1.0 / calcRows) * unUsedNodes); + long targetChunkSize = (long) Math.ceil((split.getEndIndex() - split.getBeginIndex()) * 1.0 / curNode); + long chunkOffset = split.getBeginIndex(); + while (chunkOffset < split.getEndIndex()) { + long chunkLength = Math.min(targetChunkSize, split.getEndIndex() - chunkOffset); + result.add(new PdboTable().setConnectorId(split.getConnectorId()). + setSchemaName(split.getSchemaName()). + setTableName(split.getTableName()). + setRows(split.getRows()). + setScanNodes(split.getScanNodes()). + setBeginIndex(chunkOffset). + setEndIndex(chunkOffset + chunkLength)); + chunkOffset += chunkLength; + } + } + commitTableLogs(result, table); + } + + private int combineTableLog(List tableLogs, long averageStep, + List result, List otherSplit) + { + List tmp = new ArrayList<>(); + int usedNodes = 0; + int logSize = 0; + long sum = 0; + for (PdboTable split : tableLogs) { + boolean isCombine = split.getRows() < averageStep; + logSize++; + if (averageStep > (sum + split.getRows())) { + tmp.add(split); + sum += split.getRows(); + if (logSize == tableLogs.size()) { + combineResult(tmp, result); + usedNodes++; + } + continue; + } + else { + if (!tmp.isEmpty()) { + combineResult(tmp, result); + usedNodes++; + tmp.clear(); + } + if (isCombine) { + sum = split.getRows(); + tmp.add(split); + } + else { + otherSplit.add(split); + sum = 0; + } + } + if (logSize == tableLogs.size() && tmp.size() > 0) { + combineResult(tmp, result); + usedNodes++; + } + } + return usedNodes; + } + + private void combineResult(List tmp, List combineResult) + { + combineResult.add(new PdboTable().setConnectorId(tmp.get(0).getConnectorId()). + setSchemaName(tmp.get(0).getSchemaName()). + setTableName(tmp.get(0).getTableName()). + setRows(tmp.stream().mapToLong(PdboTable::getRows).sum()). + setScanNodes(tmp.get(0).getScanNodes()). + setBeginIndex(tmp.get(0).getBeginIndex()). + setEndIndex(tmp.get(tmp.size() - 1).getEndIndex())); + } + + public List getTableRunningLogs(PdboTable table) + { + List tables = null; + Connection conn = getConnection(); + QueryRunner runner = new QueryRunner(); + String sql = "SELECT CONNECTORID,SCHEMANAME,TABLENAME,ROWS,BEGININDEX,ENDINDEX,SCANNODES " + + "FROM " + PDBO_LOG + " WHERE CONNECTORID = '" + table.getConnectorId() + + "' AND SCHEMANAME = '" + table.getSchemaName() + + "' AND TABLENAME = '" + table.getTableName() + + "' and RECORDFLAG = 'new' ORDER BY BEGININDEX"; + try { + tables = runner.query(conn, sql, new PdboLogResultHandler()); + } + catch (SQLException e) { + log.error(e, "SQL : " + sql + ",getTableRunningLogs error %s", e.getMessage()); + } + finally { + DbUtils.closeQuietly(conn); + } + return tables; + } + + private class PdboLogResultHandler implements ResultSetHandler> + { + @Override + public List handle(ResultSet rs) throws SQLException + { + List tables = new ArrayList<>(); + while (rs.next()) { + tables.add(new PdboTable().setConnectorId(rs.getString(1)). + setSchemaName(rs.getString(2)). + setTableName(rs.getString(3)). + setRows(rs.getLong(4)). + setBeginIndex(rs.getLong(5)). + setEndIndex(rs.getLong(6)). + setScanNodes(rs.getInt(7))); + } + return tables; + } + } + + public void commitTableLogs(List result, PdboTable pdboTable) + { + long timeStamp = System.nanoTime(); + int scanNodes = result.get(0).getScanNodes(); + boolean shouldUpdateTableRoute = scanNodes != result.size(); + StringBuilder sql = new StringBuilder().append("INSERT INTO " + PDBO_LOG + + "(CONNECTORID,SCHEMANAME,TABLENAME,ROWS,BEGININDEX,ENDINDEX,RECORDFLAG,SCANNODES,TIMESTAMP) VALUES"); + for (PdboTable table : result) { + sql.append("('" + table.getConnectorId() + "',") + .append("'" + table.getSchemaName() + "',") + .append("'" + table.getTableName() + "',") + .append(table.getRows() + ",") + .append(table.getBeginIndex() + ",") + .append(table.getEndIndex() + ",") + .append("'finish',") + .append(result.size() + ",") + .append(timeStamp + "),"); + } + String updateSql = "UPDATE " + PDBO_LOG + " SET RECORDFLAG = 'calchistory' " + + "WHERE RECORDFLAG in ('new','finish') AND CONNECTORID = '" + pdboTable.getConnectorId() + + "' AND SCHEMANAME = '" + pdboTable.getSchemaName() + "' AND TABLENAME = '" + pdboTable.getTableName() + "'"; + String updateTableRouteSql = "UPDATE " + PDBO_ROUTE + " SET SCANNODENUMBER = " + result.size() + + " WHERE TABLEID IN (SELECT TABLEID FROM " + PDBO_TABLE + + " WHERE CONNECTORID='" + pdboTable.getConnectorId() + + "' AND PRESTO_SCHEMA='" + pdboTable.getSchemaName() + + "' AND PRESTO_TABLE='" + pdboTable.getTableName() + "')"; + Connection conn = getConnection(); + QueryRunner runner = new QueryRunner(); + try { + runner.update(conn, updateSql); + runner.update(conn, sql.substring(0, sql.length() - 1).toString()); + if (shouldUpdateTableRoute) { + runner.update(conn, updateTableRouteSql); + } + } + catch (SQLException e) { + log.error(e, "updateSql = %s, updateTableRouteSql = %s, getTableRunningLogs error %s", updateSql, updateTableRouteSql, e.getMessage()); + } + finally { + DbUtils.closeQuietly(conn); + } + } + + public Connection getConnection() + { + Connection conn = null; + try { + conn = driver.connect(connectionUrl, connectionProperties); + } + catch (SQLException e) { + log.error(e, "Connect db error %s" + e.getMessage()); + } + return conn; + } + + public ThreadPoolExecutorMBean getProcessorExecutor() + { + return executorMBean; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/server/AsyncHttpExecutionMBean.java b/presto-main/src/main/java/com/facebook/presto/server/AsyncHttpExecutionMBean.java new file mode 100644 index 00000000..322339bb --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/server/AsyncHttpExecutionMBean.java @@ -0,0 +1,45 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.server; + +import io.airlift.concurrent.ThreadPoolExecutorMBean; +import org.weakref.jmx.Managed; +import org.weakref.jmx.Nested; + +import javax.inject.Inject; + +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadPoolExecutor; + +import static com.facebook.presto.util.Types.checkType; +import static com.google.common.base.Preconditions.checkNotNull; + +public class AsyncHttpExecutionMBean +{ + private final ThreadPoolExecutorMBean executorMBean; + + @Inject + public AsyncHttpExecutionMBean(@ForAsyncHttpResponse ScheduledExecutorService executor) + { + checkNotNull(executor, "executor is null"); + this.executorMBean = new ThreadPoolExecutorMBean(checkType(executor, ThreadPoolExecutor.class, "executor")); + } + + @Managed + @Nested + public ThreadPoolExecutorMBean getExecutor() + { + return executorMBean; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/server/BasicQueryInfo.java b/presto-main/src/main/java/com/facebook/presto/server/BasicQueryInfo.java new file mode 100644 index 00000000..d8437fe7 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/server/BasicQueryInfo.java @@ -0,0 +1,239 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.server; + +import com.facebook.presto.Session; +import com.facebook.presto.execution.QueryId; +import com.facebook.presto.execution.QueryInfo; +import com.facebook.presto.execution.QueryState; +import com.facebook.presto.operator.BlockedReason; +import com.facebook.presto.spi.ErrorCode; +import com.facebook.presto.spi.StandardErrorCode.ErrorType; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableSet; +import io.airlift.units.Duration; +import org.joda.time.DateTime; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +import java.net.URI; +import java.util.Set; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.Objects.requireNonNull; + +@Immutable +public class BasicQueryInfo +{ + private final QueryId queryId; + private final Session session; + private final QueryState state; + private final ErrorType errorType; + private final ErrorCode errorCode; + private final boolean scheduled; + private final boolean fullyBlocked; + private final Set blockedReasons; + private final URI self; + private final String query; + private final Duration elapsedTime; + private final DateTime endTime; + private final DateTime createTime; + private final int runningDrivers; + private final int queuedDrivers; + private final int completedDrivers; + private final int totalDrivers; + + @JsonCreator + public BasicQueryInfo( + @JsonProperty("queryId") QueryId queryId, + @JsonProperty("session") Session session, + @JsonProperty("state") QueryState state, + @JsonProperty("errorType") ErrorType errorType, + @JsonProperty("errorCode") ErrorCode errorCode, + @JsonProperty("scheduled") boolean scheduled, + @JsonProperty("fullyBlocked") boolean fullyBlocked, + @JsonProperty("blockedReasons") Set blockedReasons, + @JsonProperty("self") URI self, + @JsonProperty("query") String query, + @JsonProperty("elapsedTime") Duration elapsedTime, + @JsonProperty("endTime") DateTime endTime, + @JsonProperty("createTime") DateTime createTime, + @JsonProperty("runningDrivers") int runningDrivers, + @JsonProperty("queuedDrivers") int queuedDrivers, + @JsonProperty("completedDrivers") int completedDrivers, + @JsonProperty("totalDrivers") int totalDrivers) + + { + this.queryId = checkNotNull(queryId, "queryId is null"); + this.session = checkNotNull(session, "session is null"); + this.state = checkNotNull(state, "state is null"); + this.errorType = errorType; + this.errorCode = errorCode; + this.scheduled = scheduled; + this.fullyBlocked = fullyBlocked; + this.blockedReasons = ImmutableSet.copyOf(requireNonNull(blockedReasons, "blockedReasons is null")); + this.self = checkNotNull(self, "self is null"); + this.query = checkNotNull(query, "query is null"); + this.elapsedTime = elapsedTime; + this.endTime = endTime; + this.createTime = createTime; + + checkArgument(runningDrivers >= 0, "runningDrivers is less than zero"); + this.runningDrivers = runningDrivers; + checkArgument(queuedDrivers >= 0, "queuedDrivers is less than zero"); + this.queuedDrivers = queuedDrivers; + checkArgument(completedDrivers >= 0, "completedDrivers is less than zero"); + this.completedDrivers = completedDrivers; + checkArgument(totalDrivers >= 0, "totalDrivers is less than zero"); + this.totalDrivers = totalDrivers; + } + + public BasicQueryInfo(QueryInfo queryInfo) + { + this(queryInfo.getQueryId(), + queryInfo.getSession(), + queryInfo.getState(), + queryInfo.getErrorType(), + queryInfo.getErrorCode(), + queryInfo.isScheduled(), + queryInfo.getQueryStats().isFullyBlocked(), + queryInfo.getQueryStats().getBlockedReasons(), + queryInfo.getSelf(), + queryInfo.getQuery(), + queryInfo.getQueryStats().getElapsedTime(), + queryInfo.getQueryStats().getEndTime(), + queryInfo.getQueryStats().getCreateTime(), + queryInfo.getQueryStats().getRunningDrivers(), + queryInfo.getQueryStats().getQueuedDrivers(), + queryInfo.getQueryStats().getCompletedDrivers(), + queryInfo.getQueryStats().getTotalDrivers()); + } + + @JsonProperty + public QueryId getQueryId() + { + return queryId; + } + + @JsonProperty + public Session getSession() + { + return session; + } + + @JsonProperty + public QueryState getState() + { + return state; + } + + @Nullable + @JsonProperty + public ErrorType getErrorType() + { + return errorType; + } + + @Nullable + @JsonProperty + public ErrorCode getErrorCode() + { + return errorCode; + } + + @JsonProperty + public boolean isScheduled() + { + return scheduled; + } + + @JsonProperty + public boolean isFullyBlocked() + { + return fullyBlocked; + } + + @JsonProperty + public Set getBlockedReasons() + { + return blockedReasons; + } + + @JsonProperty + public URI getSelf() + { + return self; + } + + @JsonProperty + public String getQuery() + { + return query; + } + + @JsonProperty + public Duration getElapsedTime() + { + return elapsedTime; + } + + @JsonProperty + public DateTime getEndTime() + { + return endTime; + } + + @JsonProperty + public int getRunningDrivers() + { + return runningDrivers; + } + + @JsonProperty + public int getQueuedDrivers() + { + return queuedDrivers; + } + + @JsonProperty + public int getTotalDrivers() + { + return totalDrivers; + } + + @JsonProperty + public int getCompletedDrivers() + { + return completedDrivers; + } + + @JsonProperty + public DateTime getCreateTime() + { + return createTime; + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("queryId", queryId) + .add("state", state) + .toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/server/CoordinatorModule.java b/presto-main/src/main/java/com/facebook/presto/server/CoordinatorModule.java new file mode 100644 index 00000000..5233ff8a --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/server/CoordinatorModule.java @@ -0,0 +1,172 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.server; + +import com.facebook.presto.execution.CreateTableTask; +import com.facebook.presto.execution.CreateViewTask; +import com.facebook.presto.execution.DataDefinitionTask; +import com.facebook.presto.execution.DropTableTask; +import com.facebook.presto.execution.DropViewTask; +import com.facebook.presto.execution.ForQueryExecution; +import com.facebook.presto.execution.NodeScheduler; +import com.facebook.presto.execution.NodeSchedulerConfig; +import com.facebook.presto.execution.NodeTaskMap; +import com.facebook.presto.execution.QueryExecution; +import com.facebook.presto.execution.QueryExecutionMBean; +import com.facebook.presto.execution.QueryIdGenerator; +import com.facebook.presto.execution.QueryManager; +import com.facebook.presto.execution.QueryManagerConfig; +import com.facebook.presto.execution.QueryQueueManager; +import com.facebook.presto.execution.RenameColumnTask; +import com.facebook.presto.execution.RenameTableTask; +import com.facebook.presto.execution.ResetSessionTask; +import com.facebook.presto.execution.SetSessionTask; +import com.facebook.presto.execution.SqlQueryManager; +import com.facebook.presto.execution.SqlQueryQueueManager; +import com.facebook.presto.metadata.DiscoveryNodeManager; +import com.facebook.presto.metadata.InternalNodeManager; +import com.facebook.presto.metadata.ViewDefinition; +import com.facebook.presto.spi.NodeManager; +import com.facebook.presto.split.SplitManager; +import com.facebook.presto.sql.analyzer.FeaturesConfig; +import com.facebook.presto.sql.tree.CreateTable; +import com.facebook.presto.sql.tree.CreateTableAsSelect; +import com.facebook.presto.sql.tree.CreateView; +import com.facebook.presto.sql.tree.Delete; +import com.facebook.presto.sql.tree.DropTable; +import com.facebook.presto.sql.tree.DropView; +import com.facebook.presto.sql.tree.Explain; +import com.facebook.presto.sql.tree.Insert; +import com.facebook.presto.sql.tree.Query; +import com.facebook.presto.sql.tree.RenameColumn; +import com.facebook.presto.sql.tree.RenameTable; +import com.facebook.presto.sql.tree.ResetSession; +import com.facebook.presto.sql.tree.SetSession; +import com.facebook.presto.sql.tree.ShowCatalogs; +import com.facebook.presto.sql.tree.ShowColumns; +import com.facebook.presto.sql.tree.ShowFunctions; +import com.facebook.presto.sql.tree.ShowPartitions; +import com.facebook.presto.sql.tree.ShowSchemas; +import com.facebook.presto.sql.tree.ShowSession; +import com.facebook.presto.sql.tree.ShowTables; +import com.facebook.presto.sql.tree.Statement; +import com.facebook.presto.sql.tree.Use; +import com.google.inject.Binder; +import com.google.inject.Key; +import com.google.inject.Module; +import com.google.inject.Scopes; +import com.google.inject.TypeLiteral; +import com.google.inject.multibindings.MapBinder; + +import java.util.concurrent.ExecutorService; + +import static com.facebook.presto.execution.DataDefinitionExecution.DataDefinitionExecutionFactory; +import static com.facebook.presto.execution.QueryExecution.QueryExecutionFactory; +import static com.facebook.presto.execution.SqlQueryExecution.SqlQueryExecutionFactory; +import static com.google.inject.multibindings.MapBinder.newMapBinder; +import static io.airlift.concurrent.Threads.threadsNamed; +import static io.airlift.configuration.ConfigBinder.configBinder; +import static io.airlift.discovery.client.DiscoveryBinder.discoveryBinder; +import static io.airlift.http.server.HttpServerBinder.httpServerBinder; +import static io.airlift.jaxrs.JaxrsBinder.jaxrsBinder; +import static io.airlift.json.JsonCodecBinder.jsonCodecBinder; +import static java.util.concurrent.Executors.newCachedThreadPool; +import static org.weakref.jmx.ObjectNames.generatedNameOf; +import static org.weakref.jmx.guice.ExportBinder.newExporter; + +public class CoordinatorModule + implements Module +{ + @Override + public void configure(Binder binder) + { + // TODO: currently, this module is ALWAYS installed (even for non-coordinators) + + httpServerBinder(binder).bindResource("/", "webapp").withWelcomeFile("index.html"); + + discoveryBinder(binder).bindSelector("presto"); + + // query manager + jaxrsBinder(binder).bind(QueryResource.class); + jaxrsBinder(binder).bind(StageResource.class); + binder.bind(QueryIdGenerator.class).in(Scopes.SINGLETON); + binder.bind(QueryManager.class).to(SqlQueryManager.class).in(Scopes.SINGLETON); + binder.bind(QueryQueueManager.class).to(SqlQueryQueueManager.class).in(Scopes.SINGLETON); + newExporter(binder).export(QueryManager.class).withGeneratedName(); + configBinder(binder).bindConfig(QueryManagerConfig.class); + + // analyzer + configBinder(binder).bindConfig(FeaturesConfig.class); + + // split manager + binder.bind(SplitManager.class).in(Scopes.SINGLETON); + + // node scheduler + binder.bind(InternalNodeManager.class).to(DiscoveryNodeManager.class).in(Scopes.SINGLETON); + binder.bind(NodeManager.class).to(Key.get(InternalNodeManager.class)).in(Scopes.SINGLETON); + configBinder(binder).bindConfig(NodeSchedulerConfig.class); + binder.bind(NodeScheduler.class).in(Scopes.SINGLETON); + binder.bind(NodeTaskMap.class).in(Scopes.SINGLETON); + newExporter(binder).export(NodeScheduler.class).withGeneratedName(); + + // query execution + binder.bind(ExecutorService.class).annotatedWith(ForQueryExecution.class) + .toInstance(newCachedThreadPool(threadsNamed("query-execution-%s"))); + binder.bind(QueryExecutionMBean.class).in(Scopes.SINGLETON); + newExporter(binder).export(QueryExecutionMBean.class).as(generatedNameOf(QueryExecution.class)); + + MapBinder, QueryExecutionFactory> executionBinder = newMapBinder(binder, + new TypeLiteral>() {}, new TypeLiteral>() {}); + + binder.bind(SqlQueryExecutionFactory.class).in(Scopes.SINGLETON); + executionBinder.addBinding(Query.class).to(SqlQueryExecutionFactory.class).in(Scopes.SINGLETON); + executionBinder.addBinding(Explain.class).to(SqlQueryExecutionFactory.class).in(Scopes.SINGLETON); + executionBinder.addBinding(ShowColumns.class).to(SqlQueryExecutionFactory.class).in(Scopes.SINGLETON); + executionBinder.addBinding(ShowPartitions.class).to(SqlQueryExecutionFactory.class).in(Scopes.SINGLETON); + executionBinder.addBinding(ShowFunctions.class).to(SqlQueryExecutionFactory.class).in(Scopes.SINGLETON); + executionBinder.addBinding(ShowTables.class).to(SqlQueryExecutionFactory.class).in(Scopes.SINGLETON); + executionBinder.addBinding(ShowSchemas.class).to(SqlQueryExecutionFactory.class).in(Scopes.SINGLETON); + executionBinder.addBinding(ShowCatalogs.class).to(SqlQueryExecutionFactory.class).in(Scopes.SINGLETON); + executionBinder.addBinding(Use.class).to(SqlQueryExecutionFactory.class).in(Scopes.SINGLETON); + executionBinder.addBinding(ShowSession.class).to(SqlQueryExecutionFactory.class).in(Scopes.SINGLETON); + executionBinder.addBinding(CreateTableAsSelect.class).to(SqlQueryExecutionFactory.class).in(Scopes.SINGLETON); + executionBinder.addBinding(Insert.class).to(SqlQueryExecutionFactory.class).in(Scopes.SINGLETON); + executionBinder.addBinding(Delete.class).to(SqlQueryExecutionFactory.class).in(Scopes.SINGLETON); + + binder.bind(DataDefinitionExecutionFactory.class).in(Scopes.SINGLETON); + bindDataDefinitionTask(binder, executionBinder, CreateTable.class, CreateTableTask.class); + bindDataDefinitionTask(binder, executionBinder, RenameTable.class, RenameTableTask.class); + bindDataDefinitionTask(binder, executionBinder, RenameColumn.class, RenameColumnTask.class); + bindDataDefinitionTask(binder, executionBinder, DropTable.class, DropTableTask.class); + bindDataDefinitionTask(binder, executionBinder, CreateView.class, CreateViewTask.class); + bindDataDefinitionTask(binder, executionBinder, DropView.class, DropViewTask.class); + bindDataDefinitionTask(binder, executionBinder, SetSession.class, SetSessionTask.class); + bindDataDefinitionTask(binder, executionBinder, ResetSession.class, ResetSessionTask.class); + + jsonCodecBinder(binder).bindJsonCodec(ViewDefinition.class); + } + + private static void bindDataDefinitionTask( + Binder binder, + MapBinder, QueryExecutionFactory> executionBinder, + Class statement, + Class> task) + { + MapBinder, DataDefinitionTask> taskBinder = newMapBinder(binder, + new TypeLiteral>() {}, new TypeLiteral>() {}); + + taskBinder.addBinding(statement).to(task).in(Scopes.SINGLETON); + executionBinder.addBinding(statement).to(DataDefinitionExecutionFactory.class).in(Scopes.SINGLETON); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/server/ExchangeExecutionMBean.java b/presto-main/src/main/java/com/facebook/presto/server/ExchangeExecutionMBean.java new file mode 100644 index 00000000..e52ddbc2 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/server/ExchangeExecutionMBean.java @@ -0,0 +1,46 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.server; + +import com.facebook.presto.operator.ForExchange; +import io.airlift.concurrent.ThreadPoolExecutorMBean; +import org.weakref.jmx.Managed; +import org.weakref.jmx.Nested; + +import javax.inject.Inject; + +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadPoolExecutor; + +import static com.facebook.presto.util.Types.checkType; +import static com.google.common.base.Preconditions.checkNotNull; + +public class ExchangeExecutionMBean +{ + private final ThreadPoolExecutorMBean executorMBean; + + @Inject + public ExchangeExecutionMBean(@ForExchange ScheduledExecutorService executor) + { + checkNotNull(executor, "executor is null"); + this.executorMBean = new ThreadPoolExecutorMBean(checkType(executor, ThreadPoolExecutor.class, "executor")); + } + + @Managed + @Nested + public ThreadPoolExecutorMBean getExecutor() + { + return executorMBean; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/server/ExecuteResource.java b/presto-main/src/main/java/com/facebook/presto/server/ExecuteResource.java new file mode 100644 index 00000000..754c4e44 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/server/ExecuteResource.java @@ -0,0 +1,194 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.server; + +import com.facebook.presto.Session; +import com.facebook.presto.client.ClientSession; +import com.facebook.presto.client.Column; +import com.facebook.presto.client.QueryResults; +import com.facebook.presto.client.StatementClient; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.AbstractIterator; +import io.airlift.http.client.HttpClient; +import io.airlift.http.server.HttpServerInfo; +import io.airlift.json.JsonCodec; + +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import java.net.URI; +import java.util.Iterator; +import java.util.List; + +import static com.facebook.presto.client.PrestoHeaders.PRESTO_CATALOG; +import static com.facebook.presto.client.PrestoHeaders.PRESTO_LANGUAGE; +import static com.facebook.presto.client.PrestoHeaders.PRESTO_SCHEMA; +import static com.facebook.presto.client.PrestoHeaders.PRESTO_SOURCE; +import static com.facebook.presto.client.PrestoHeaders.PRESTO_TIME_ZONE; +import static com.facebook.presto.client.PrestoHeaders.PRESTO_USER; +import static com.facebook.presto.server.ResourceUtil.assertRequest; +import static com.facebook.presto.server.ResourceUtil.createSessionForRequest; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Strings.isNullOrEmpty; +import static com.google.common.collect.Iterators.concat; +import static com.google.common.collect.Iterators.transform; +import static java.lang.String.format; +import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR; +import static javax.ws.rs.core.Response.status; + +@Path("/v1/execute") +public class ExecuteResource +{ + private final HttpServerInfo serverInfo; + private final HttpClient httpClient; + private final JsonCodec queryResultsCodec; + + @Inject + public ExecuteResource( + HttpServerInfo serverInfo, + @ForExecute HttpClient httpClient, + JsonCodec queryResultsCodec) + { + this.serverInfo = checkNotNull(serverInfo, "serverInfo is null"); + this.httpClient = checkNotNull(httpClient, "httpClient is null"); + this.queryResultsCodec = checkNotNull(queryResultsCodec, "queryResultsCodec is null"); + } + + @POST + @Produces(MediaType.APPLICATION_JSON) + public Response createQuery( + String query, + @HeaderParam(PRESTO_USER) String user, + @HeaderParam(PRESTO_SOURCE) String source, + @HeaderParam(PRESTO_CATALOG) String catalog, + @HeaderParam(PRESTO_SCHEMA) String schema, + @HeaderParam(PRESTO_TIME_ZONE) String timeZoneId, + @HeaderParam(PRESTO_LANGUAGE) String language, + @Context HttpServletRequest servletRequest) + { + assertRequest(!isNullOrEmpty(query), "SQL query is empty"); + + Session session = createSessionForRequest(servletRequest); + ClientSession clientSession = session.toClientSession(serverUri(), false); + + StatementClient client = new StatementClient(httpClient, queryResultsCodec, clientSession, query); + + List columns = getColumns(client); + Iterator> iterator = flatten(new ResultsPageIterator(client)); + SimpleQueryResults results = new SimpleQueryResults(columns, iterator); + + return Response.ok(results, MediaType.APPLICATION_JSON_TYPE).build(); + } + + private URI serverUri() + { + checkState(serverInfo.getHttpUri() != null, "No HTTP URI for this server (HTTP disabled?)"); + return serverInfo.getHttpUri(); + } + + private static List getColumns(StatementClient client) + { + while (client.isValid()) { + List columns = client.current().getColumns(); + if (columns != null) { + return columns; + } + client.advance(); + } + + if (!client.isFailed()) { + throw internalServerError("No columns"); + } + throw internalServerError(failureMessage(client.finalResults())); + } + + @SuppressWarnings("RedundantTypeArguments") + private static Iterator flatten(Iterator> iterator) + { + // the explicit type argument is required by the Eclipse compiler + return concat(transform(iterator, Iterable::iterator)); + } + + private static class ResultsPageIterator + extends AbstractIterator>> + { + private final StatementClient client; + + private ResultsPageIterator(StatementClient client) + { + this.client = checkNotNull(client, "client is null"); + } + + @Override + protected Iterable> computeNext() + { + while (client.isValid()) { + Iterable> data = client.current().getData(); + client.advance(); + if (data != null) { + return data; + } + } + + if (client.isFailed()) { + throw internalServerError(failureMessage(client.finalResults())); + } + + return endOfData(); + } + } + + private static WebApplicationException internalServerError(String message) + { + return new WebApplicationException(status(INTERNAL_SERVER_ERROR).entity(message).build()); + } + + private static String failureMessage(QueryResults results) + { + return format("Query failed (#%s): %s", results.getId(), results.getError().getMessage()); + } + + public static class SimpleQueryResults + { + private final List columns; + private final Iterator> data; + + public SimpleQueryResults(List columns, Iterator> data) + { + this.columns = checkNotNull(columns, "columns is null"); + this.data = checkNotNull(data, "data is null"); + } + + @JsonProperty + public List getColumns() + { + return columns; + } + + @JsonProperty + public Iterator> getData() + { + return data; + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/server/ForAsyncHttpResponse.java b/presto-main/src/main/java/com/facebook/presto/server/ForAsyncHttpResponse.java new file mode 100644 index 00000000..2b59b5db --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/server/ForAsyncHttpResponse.java @@ -0,0 +1,31 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.server; + +import javax.inject.Qualifier; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention(RUNTIME) +@Target({FIELD, PARAMETER, METHOD}) +@Qualifier +public @interface ForAsyncHttpResponse +{ +} diff --git a/presto-main/src/main/java/com/facebook/presto/server/ForExecute.java b/presto-main/src/main/java/com/facebook/presto/server/ForExecute.java new file mode 100644 index 00000000..80212667 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/server/ForExecute.java @@ -0,0 +1,30 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.server; + +import javax.inject.Qualifier; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention(RUNTIME) +@Target({FIELD, PARAMETER, METHOD}) +@Qualifier +public @interface ForExecute +{} diff --git a/presto-main/src/main/java/com/facebook/presto/server/HttpLocationFactory.java b/presto-main/src/main/java/com/facebook/presto/server/HttpLocationFactory.java new file mode 100644 index 00000000..fa317132 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/server/HttpLocationFactory.java @@ -0,0 +1,94 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.server; + +import com.facebook.presto.execution.LocationFactory; +import com.facebook.presto.execution.QueryId; +import com.facebook.presto.execution.StageId; +import com.facebook.presto.execution.TaskId; +import com.facebook.presto.spi.Node; +import com.facebook.presto.spi.NodeManager; +import com.google.common.base.Preconditions; +import io.airlift.http.server.HttpServerInfo; + +import javax.inject.Inject; + +import java.net.URI; + +import static io.airlift.http.client.HttpUriBuilder.uriBuilderFrom; +import static java.util.Objects.requireNonNull; + +public class HttpLocationFactory + implements LocationFactory +{ + private final NodeManager nodeManager; + private final URI baseUri; + + @Inject + public HttpLocationFactory(NodeManager nodeManager, HttpServerInfo httpServerInfo) + { + this(nodeManager, httpServerInfo.getHttpUri()); + } + + public HttpLocationFactory(NodeManager nodeManager, URI baseUri) + { + this.nodeManager = nodeManager; + this.baseUri = baseUri; + } + + @Override + public URI createQueryLocation(QueryId queryId) + { + Preconditions.checkNotNull(queryId, "queryId is null"); + return uriBuilderFrom(baseUri) + .appendPath("/v1/query") + .appendPath(queryId.toString()) + .build(); + } + + @Override + public URI createStageLocation(StageId stageId) + { + Preconditions.checkNotNull(stageId, "stageId is null"); + return uriBuilderFrom(baseUri) + .appendPath("v1/stage") + .appendPath(stageId.toString()) + .build(); + } + + @Override + public URI createLocalTaskLocation(TaskId taskId) + { + return createTaskLocation(nodeManager.getCurrentNode(), taskId); + } + + @Override + public URI createTaskLocation(Node node, TaskId taskId) + { + Preconditions.checkNotNull(node, "node is null"); + Preconditions.checkNotNull(taskId, "taskId is null"); + return uriBuilderFrom(node.getHttpUri()) + .appendPath("/v1/task") + .appendPath(taskId.toString()) + .build(); + } + + @Override + public URI createMemoryInfoLocation(Node node) + { + requireNonNull(node, "node is null"); + return uriBuilderFrom(node.getHttpUri()) + .appendPath("/v1/memory").build(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/server/HttpRemoteTask.java b/presto-main/src/main/java/com/facebook/presto/server/HttpRemoteTask.java new file mode 100644 index 00000000..eb292f71 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/server/HttpRemoteTask.java @@ -0,0 +1,945 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.server; + +import com.facebook.presto.OutputBuffers; +import com.facebook.presto.ScheduledSplit; +import com.facebook.presto.Session; +import com.facebook.presto.TaskSource; +import com.facebook.presto.client.PrestoHeaders; +import com.facebook.presto.execution.BufferInfo; +import com.facebook.presto.execution.ExecutionFailureInfo; +import com.facebook.presto.execution.PageBufferInfo; +import com.facebook.presto.execution.RemoteTask; +import com.facebook.presto.execution.SharedBuffer.BufferState; +import com.facebook.presto.execution.SharedBufferInfo; +import com.facebook.presto.execution.StateMachine; +import com.facebook.presto.execution.StateMachine.StateChangeListener; +import com.facebook.presto.execution.TaskId; +import com.facebook.presto.execution.TaskInfo; +import com.facebook.presto.execution.TaskState; +import com.facebook.presto.metadata.Split; +import com.facebook.presto.operator.TaskStats; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.sql.planner.PlanFragment; +import com.facebook.presto.sql.planner.plan.PlanNode; +import com.facebook.presto.sql.planner.plan.PlanNodeId; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Multimap; +import com.google.common.collect.ObjectArrays; +import com.google.common.collect.SetMultimap; +import com.google.common.net.HttpHeaders; +import com.google.common.net.MediaType; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.RateLimiter; +import io.airlift.concurrent.SetThreadName; +import io.airlift.http.client.FullJsonResponseHandler.JsonResponse; +import io.airlift.http.client.HttpClient; +import io.airlift.http.client.HttpStatus; +import io.airlift.http.client.Request; +import io.airlift.http.client.StatusResponseHandler.StatusResponse; +import io.airlift.json.JsonCodec; +import io.airlift.log.Logger; +import io.airlift.units.Duration; +import org.joda.time.DateTime; + +import javax.annotation.concurrent.GuardedBy; +import javax.annotation.concurrent.ThreadSafe; + +import java.io.EOFException; +import java.net.SocketException; +import java.net.SocketTimeoutException; +import java.net.URI; +import java.util.HashSet; +import java.util.List; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Optional; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.Future; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Stream; + +import static com.facebook.presto.spi.StandardErrorCode.REMOTE_TASK_ERROR; +import static com.facebook.presto.spi.StandardErrorCode.TOO_MANY_REQUESTS_FAILED; +import static com.facebook.presto.spi.StandardErrorCode.WORKER_RESTARTED; +import static com.facebook.presto.util.Failures.WORKER_NODE_ERROR; +import static com.facebook.presto.util.Failures.WORKER_RESTARTED_ERROR; +import static com.facebook.presto.util.Failures.toFailure; +import static com.facebook.presto.util.ImmutableCollectors.toImmutableList; +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static io.airlift.http.client.FullJsonResponseHandler.createFullJsonResponseHandler; +import static io.airlift.http.client.HttpUriBuilder.uriBuilderFrom; +import static io.airlift.http.client.JsonBodyGenerator.jsonBodyGenerator; +import static io.airlift.http.client.Request.Builder.prepareDelete; +import static io.airlift.http.client.Request.Builder.prepareGet; +import static io.airlift.http.client.Request.Builder.preparePost; +import static io.airlift.http.client.StatusResponseHandler.createStatusResponseHandler; +import static java.lang.String.format; + +public class HttpRemoteTask + implements RemoteTask +{ + private static final Logger log = Logger.get(HttpRemoteTask.class); + + private final TaskId taskId; + + private final Session session; + private final String nodeId; + private final PlanFragment planFragment; + + private final AtomicLong nextSplitId = new AtomicLong(); + + private final StateMachine taskInfo; + + @GuardedBy("this") + private Future currentRequest; + @GuardedBy("this") + private long currentRequestStartNanos; + + @GuardedBy("this") + private final SetMultimap pendingSplits = HashMultimap.create(); + @GuardedBy("this") + private volatile int pendingSourceSplitCount; + @GuardedBy("this") + private final Set noMoreSplits = new HashSet<>(); + @GuardedBy("this") + private final AtomicReference outputBuffers = new AtomicReference<>(); + + private final ContinuousTaskInfoFetcher continuousTaskInfoFetcher; + + private final HttpClient httpClient; + private final Executor executor; + private final JsonCodec taskInfoCodec; + private final JsonCodec taskUpdateRequestCodec; + + private final RequestErrorTracker updateErrorTracker; + private final RequestErrorTracker getErrorTracker; + + private final AtomicBoolean needsUpdate = new AtomicBoolean(true); + + public HttpRemoteTask(Session session, + TaskId taskId, + String nodeId, + URI location, + PlanFragment planFragment, + Multimap initialSplits, + OutputBuffers outputBuffers, + HttpClient httpClient, + Executor executor, + int maxConsecutiveErrorCount, + Duration minErrorDuration, + Duration refreshMaxWait, + JsonCodec taskInfoCodec, + JsonCodec taskUpdateRequestCodec) + { + checkNotNull(session, "session is null"); + checkNotNull(taskId, "taskId is null"); + checkNotNull(nodeId, "nodeId is null"); + checkNotNull(location, "location is null"); + checkNotNull(planFragment, "planFragment1 is null"); + checkNotNull(outputBuffers, "outputBuffers is null"); + checkNotNull(httpClient, "httpClient is null"); + checkNotNull(executor, "executor is null"); + checkNotNull(taskInfoCodec, "taskInfoCodec is null"); + checkNotNull(taskUpdateRequestCodec, "taskUpdateRequestCodec is null"); + + try (SetThreadName ignored = new SetThreadName("HttpRemoteTask-%s", taskId)) { + this.taskId = taskId; + this.session = session; + this.nodeId = nodeId; + this.planFragment = planFragment; + this.outputBuffers.set(outputBuffers); + this.httpClient = httpClient; + this.executor = executor; + this.taskInfoCodec = taskInfoCodec; + this.taskUpdateRequestCodec = taskUpdateRequestCodec; + this.updateErrorTracker = new RequestErrorTracker(taskId, location, maxConsecutiveErrorCount, minErrorDuration); + this.getErrorTracker = new RequestErrorTracker(taskId, location, maxConsecutiveErrorCount, minErrorDuration); + + for (Entry entry : checkNotNull(initialSplits, "initialSplits is null").entries()) { + ScheduledSplit scheduledSplit = new ScheduledSplit(nextSplitId.getAndIncrement(), entry.getValue()); + pendingSplits.put(entry.getKey(), scheduledSplit); + } + if (initialSplits.containsKey(planFragment.getPartitionedSource())) { + pendingSourceSplitCount = initialSplits.get(planFragment.getPartitionedSource()).size(); + } + + List bufferStates = outputBuffers.getBuffers() + .keySet().stream() + .map(outputId -> new BufferInfo(outputId, false, 0, 0, PageBufferInfo.empty())) + .collect(toImmutableList()); + + TaskStats taskStats = new TaskStats(DateTime.now(), null); + + taskInfo = new StateMachine<>("task " + taskId, executor, new TaskInfo( + taskId, + Optional.empty(), + TaskInfo.MIN_VERSION, + TaskState.PLANNED, + location, + DateTime.now(), + new SharedBufferInfo(BufferState.OPEN, true, true, 0, 0, 0, 0, bufferStates), + ImmutableSet.of(), + taskStats, + ImmutableList.of())); + + continuousTaskInfoFetcher = new ContinuousTaskInfoFetcher(refreshMaxWait); + } + } + + @Override + public String getNodeId() + { + return nodeId; + } + + @Override + public TaskInfo getTaskInfo() + { + return taskInfo.get(); + } + + @Override + public void start() + { + try (SetThreadName ignored = new SetThreadName("HttpRemoteTask-%s", taskId)) { + // to start we just need to trigger an update + scheduleUpdate(); + + // begin the info fetcher + continuousTaskInfoFetcher.start(); + } + } + + @Override + public synchronized void addSplits(PlanNodeId sourceId, Iterable splits) + { + try (SetThreadName ignored = new SetThreadName("HttpRemoteTask-%s", taskId)) { + checkNotNull(sourceId, "sourceId is null"); + checkNotNull(splits, "splits is null"); + checkState(!noMoreSplits.contains(sourceId), "noMoreSplits has already been set for %s", sourceId); + + // only add pending split if not done + if (!getTaskInfo().getState().isDone()) { + int added = 0; + for (Split split : splits) { + if (pendingSplits.put(sourceId, new ScheduledSplit(nextSplitId.getAndIncrement(), split))) { + added++; + } + } + if (sourceId.equals(planFragment.getPartitionedSource())) { + pendingSourceSplitCount += added; + } + needsUpdate.set(true); + } + + scheduleUpdate(); + } + } + + @Override + public synchronized void noMoreSplits(PlanNodeId sourceId) + { + if (noMoreSplits.add(sourceId)) { + needsUpdate.set(true); + scheduleUpdate(); + } + } + + @Override + public synchronized void setOutputBuffers(OutputBuffers newOutputBuffers) + { + if (getTaskInfo().getState().isDone()) { + return; + } + + if (newOutputBuffers.getVersion() > outputBuffers.get().getVersion()) { + outputBuffers.set(newOutputBuffers); + needsUpdate.set(true); + scheduleUpdate(); + } + } + + @Override + public int getPartitionedSplitCount() + { + int splitCount = pendingSourceSplitCount; + return splitCount + taskInfo.get().getStats().getQueuedPartitionedDrivers() + taskInfo.get().getStats().getRunningPartitionedDrivers(); + } + + @Override + public int getQueuedPartitionedSplitCount() + { + int splitCount = pendingSourceSplitCount; + return splitCount + taskInfo.get().getStats().getQueuedPartitionedDrivers(); + } + + @Override + public void addStateChangeListener(StateChangeListener stateChangeListener) + { + try (SetThreadName ignored = new SetThreadName("HttpRemoteTask-%s", taskId)) { + taskInfo.addStateChangeListener(stateChangeListener); + } + } + + private synchronized void updateTaskInfo(TaskInfo newValue) + { + updateTaskInfo(newValue, ImmutableList.of()); + } + + private synchronized void updateTaskInfo(TaskInfo newValue, List sources) + { + if (newValue.getState().isDone()) { + // splits can be huge so clear the list + pendingSplits.clear(); + pendingSourceSplitCount = 0; + } + + // change to new value if old value is not changed and new value has a newer version + AtomicBoolean workerRestarted = new AtomicBoolean(); + taskInfo.setIf(newValue, oldValue -> { + // did the worker restart + if (oldValue.getNodeInstanceId().isPresent() && !oldValue.getNodeInstanceId().equals(newValue.getNodeInstanceId())) { + workerRestarted.set(true); + return false; + } + + if (oldValue.getState().isDone()) { + // never update if the task has reached a terminal state + return false; + } + if (newValue.getVersion() < oldValue.getVersion()) { + // don't update to an older version (same version is ok) + return false; + } + return true; + }); + + if (workerRestarted.get()) { + PrestoException exception = new PrestoException(WORKER_RESTARTED, format("%s (%s)", WORKER_RESTARTED_ERROR, newValue.getSelf())); + failTask(exception); + abort(); + } + + // remove acknowledged splits, which frees memory + for (TaskSource source : sources) { + PlanNodeId planNodeId = source.getPlanNodeId(); + int removed = 0; + for (ScheduledSplit split : source.getSplits()) { + if (pendingSplits.remove(planNodeId, split)) { + removed++; + } + } + if (planNodeId.equals(planFragment.getPartitionedSource())) { + pendingSourceSplitCount -= removed; + } + } + } + + private synchronized void scheduleUpdate() + { + // don't update if the task hasn't been started yet or if it is already finished + if (!needsUpdate.get() || taskInfo.get().getState().isDone()) { + return; + } + + // if we have an old request outstanding, cancel it + if (currentRequest != null && Duration.nanosSince(currentRequestStartNanos).compareTo(new Duration(2, TimeUnit.SECONDS)) >= 0) { + needsUpdate.set(true); + currentRequest.cancel(true); + currentRequest = null; + currentRequestStartNanos = 0; + } + + // if there is a request already running, wait for it to complete + if (this.currentRequest != null && !this.currentRequest.isDone()) { + return; + } + + updateErrorTracker.acquireRequestPermit(); + + List sources = getSources(); + TaskUpdateRequest updateRequest = new TaskUpdateRequest(session, + planFragment, + sources, + outputBuffers.get()); + + Request request = preparePost() + .setUri(uriBuilderFrom(taskInfo.get().getSelf()).addParameter("summarize").build()) + .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.JSON_UTF_8.toString()) + .setBodyGenerator(jsonBodyGenerator(taskUpdateRequestCodec, updateRequest)) + .build(); + + ListenableFuture> future = httpClient.executeAsync(request, createFullJsonResponseHandler(taskInfoCodec)); + currentRequest = future; + currentRequestStartNanos = System.nanoTime(); + + // The needsUpdate flag needs to be set to false BEFORE adding the Future callback since callback might change the flag value + // and does so without grabbing the instance lock. + needsUpdate.set(false); + + Futures.addCallback(future, new SimpleHttpResponseHandler<>(new UpdateResponseHandler(sources), request.getUri()), executor); + } + + private synchronized List getSources() + { + return Stream.concat(Stream.of(planFragment.getPartitionedSourceNode()), planFragment.getRemoteSourceNodes().stream()) + .filter(Objects::nonNull) + .map(PlanNode::getId) + .map(this::getSource) + .filter(Objects::nonNull) + .collect(toImmutableList()); + } + + private TaskSource getSource(PlanNodeId planNodeId) + { + Set splits = pendingSplits.get(planNodeId); + boolean noMoreSplits = this.noMoreSplits.contains(planNodeId); + TaskSource element = null; + if (!splits.isEmpty() || noMoreSplits) { + element = new TaskSource(planNodeId, splits, noMoreSplits); + } + return element; + } + + @Override + public synchronized void cancel() + { + try (SetThreadName ignored = new SetThreadName("HttpRemoteTask-%s", taskId)) { + if (getTaskInfo().getState().isDone()) { + return; + } + + URI uri = getTaskInfo().getSelf(); + if (uri == null) { + return; + } + + // send cancel to task and ignore response + long start = System.nanoTime(); + Request request = prepareDelete() + .setUri(uriBuilderFrom(uri).addParameter("abort", "false").addParameter("summarize").build()) + .build(); + Futures.addCallback(httpClient.executeAsync(request, createStatusResponseHandler()), new FutureCallback() + { + @Override + public void onSuccess(StatusResponse result) + { + // assume any response is good enough + } + + @Override + public void onFailure(Throwable t) + { + if (t instanceof RejectedExecutionException) { + // client has been shutdown + return; + } + + // reschedule + if (Duration.nanosSince(start).compareTo(new Duration(2, TimeUnit.MINUTES)) < 0) { + Futures.addCallback(httpClient.executeAsync(request, createStatusResponseHandler()), this, executor); + } + else { + logError(t, "Unable to cancel task at %s", request.getUri()); + } + } + }, executor); + } + } + + @Override + public synchronized void abort() + { + try (SetThreadName ignored = new SetThreadName("HttpRemoteTask-%s", taskId)) { + // clear pending splits to free memory + pendingSplits.clear(); + pendingSourceSplitCount = 0; + + // cancel pending request + if (currentRequest != null) { + currentRequest.cancel(true); + currentRequest = null; + currentRequestStartNanos = 0; + } + + // mark task as canceled (if not already done) + TaskInfo taskInfo = getTaskInfo(); + URI uri = taskInfo.getSelf(); + + updateTaskInfo(new TaskInfo(taskInfo.getTaskId(), + taskInfo.getNodeInstanceId(), + TaskInfo.MAX_VERSION, + TaskState.ABORTED, + uri, + taskInfo.getLastHeartbeat(), + taskInfo.getOutputBuffers(), + taskInfo.getNoMoreSplits(), + taskInfo.getStats(), + ImmutableList.of())); + + // send abort to task and ignore response + long start = System.nanoTime(); + Request request = prepareDelete() + .setUri(uriBuilderFrom(uri).addParameter("summarize").build()) + .build(); + Futures.addCallback(httpClient.executeAsync(request, createStatusResponseHandler()), new FutureCallback() + { + @Override + public void onSuccess(StatusResponse result) + { + // assume any response is good enough + } + + @Override + public void onFailure(Throwable t) + { + if (t instanceof RejectedExecutionException) { + // client has been shutdown + return; + } + + // reschedule + if (Duration.nanosSince(start).compareTo(new Duration(2, TimeUnit.MINUTES)) < 0) { + Futures.addCallback(httpClient.executeAsync(request, createStatusResponseHandler()), this, executor); + } + else { + logError(t, "Unable to abort task at %s", request.getUri()); + } + } + }, executor); + } + } + + /** + * Move the task directly to the failed state + */ + private void failTask(Throwable cause) + { + TaskInfo taskInfo = getTaskInfo(); + if (!taskInfo.getState().isDone()) { + log.debug(cause, "Remote task failed: %s", taskInfo.getSelf()); + } + updateTaskInfo(new TaskInfo(taskInfo.getTaskId(), + taskInfo.getNodeInstanceId(), + TaskInfo.MAX_VERSION, + TaskState.FAILED, + taskInfo.getSelf(), + taskInfo.getLastHeartbeat(), + taskInfo.getOutputBuffers(), + taskInfo.getNoMoreSplits(), + taskInfo.getStats(), + ImmutableList.of(toFailure(cause)))); + } + + @Override + public String toString() + { + return toStringHelper(this) + .addValue(getTaskInfo()) + .toString(); + } + + private class UpdateResponseHandler + implements SimpleHttpResponseCallback + { + private final List sources; + + private UpdateResponseHandler(List sources) + { + this.sources = ImmutableList.copyOf(checkNotNull(sources, "sources is null")); + } + + @Override + public void success(TaskInfo value) + { + try (SetThreadName ignored = new SetThreadName("UpdateResponseHandler-%s", taskId)) { + try { + synchronized (HttpRemoteTask.this) { + currentRequest = null; + } + updateTaskInfo(value, sources); + updateErrorTracker.requestSucceeded(); + } + finally { + scheduleUpdate(); + } + } + } + + @Override + public void failed(Throwable cause) + { + try (SetThreadName ignored = new SetThreadName("UpdateResponseHandler-%s", taskId)) { + try { + synchronized (HttpRemoteTask.this) { + currentRequest = null; + } + + // on failure assume we need to update again + needsUpdate.set(true); + + // if task not already done, record error + TaskInfo taskInfo = getTaskInfo(); + if (!taskInfo.getState().isDone()) { + updateErrorTracker.requestFailed(cause); + } + } + catch (Error e) { + failTask(e); + abort(); + throw e; + } + catch (RuntimeException e) { + failTask(e); + abort(); + } + finally { + scheduleUpdate(); + } + } + } + + @Override + public void fatal(Throwable cause) + { + try (SetThreadName ignored = new SetThreadName("UpdateResponseHandler-%s", taskId)) { + failTask(cause); + } + } + } + + /** + * Continuous update loop for task info. Wait for a short period for task state to change, and + * if it does not, return the current state of the task. This will cause stats to be updated at a + * regular interval, and state changes will be immediately recorded. + */ + private class ContinuousTaskInfoFetcher + implements SimpleHttpResponseCallback + { + private final Duration refreshMaxWait; + + @GuardedBy("this") + private boolean running; + + @GuardedBy("this") + private ListenableFuture> future; + + public ContinuousTaskInfoFetcher(Duration refreshMaxWait) + { + this.refreshMaxWait = refreshMaxWait; + } + + public synchronized void start() + { + if (running) { + // already running + return; + } + running = true; + scheduleNextRequest(); + } + + public synchronized void stop() + { + running = false; + if (future != null) { + future.cancel(true); + future = null; + } + } + + private synchronized void scheduleNextRequest() + { + // stopped or done? + TaskInfo taskInfo = HttpRemoteTask.this.taskInfo.get(); + if (!running || taskInfo.getState().isDone()) { + return; + } + + // outstanding request? + if (future != null && !future.isDone()) { + // this should never happen + log.error("Can not reschedule update because an update is already running"); + return; + } + + Request request = prepareGet() + .setUri(uriBuilderFrom(taskInfo.getSelf()).addParameter("summarize").build()) + .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.JSON_UTF_8.toString()) + .setHeader(PrestoHeaders.PRESTO_CURRENT_STATE, taskInfo.getState().toString()) + .setHeader(PrestoHeaders.PRESTO_MAX_WAIT, refreshMaxWait.toString()) + .build(); + + future = httpClient.executeAsync(request, createFullJsonResponseHandler(taskInfoCodec)); + Futures.addCallback(future, new SimpleHttpResponseHandler<>(this, request.getUri()), executor); + } + + @Override + public void success(TaskInfo value) + { + try (SetThreadName ignored = new SetThreadName("ContinuousTaskInfoFetcher-%s", taskId)) { + synchronized (this) { + future = null; + } + + try { + updateTaskInfo(value, ImmutableList.of()); + getErrorTracker.requestSucceeded(); + } + finally { + scheduleNextRequest(); + } + } + } + + @Override + public void failed(Throwable cause) + { + try (SetThreadName ignored = new SetThreadName("ContinuousTaskInfoFetcher-%s", taskId)) { + synchronized (this) { + future = null; + } + + try { + // if task not already done, record error + TaskInfo taskInfo = getTaskInfo(); + if (!taskInfo.getState().isDone()) { + getErrorTracker.requestFailed(cause); + } + } + catch (Error e) { + failTask(e); + abort(); + throw e; + } + catch (RuntimeException e) { + failTask(e); + abort(); + } + finally { + // there is no back off here so we can get a lot of error messages when a server spins + // down, but it typically goes away quickly because the queries get canceled + scheduleNextRequest(); + } + } + } + + @Override + public void fatal(Throwable cause) + { + try (SetThreadName ignored = new SetThreadName("ContinuousTaskInfoFetcher-%s", taskId)) { + synchronized (this) { + future = null; + } + + failTask(cause); + } + } + } + + public static class SimpleHttpResponseHandler + implements FutureCallback> + { + private final SimpleHttpResponseCallback callback; + + private final URI uri; + + public SimpleHttpResponseHandler(SimpleHttpResponseCallback callback, URI uri) + { + this.callback = callback; + this.uri = uri; + } + + @Override + public void onSuccess(JsonResponse response) + { + try { + if (response.getStatusCode() == HttpStatus.OK.code() && response.hasValue()) { + callback.success(response.getValue()); + } + else if (response.getStatusCode() == HttpStatus.SERVICE_UNAVAILABLE.code()) { + callback.failed(new ServiceUnavailableException(uri)); + } + else { + // Something is broken in the server or the client, so fail the task immediately (includes 500 errors) + Exception cause = response.getException(); + if (cause == null) { + if (response.getStatusCode() == HttpStatus.OK.code()) { + cause = new PrestoException(REMOTE_TASK_ERROR, format("Expected response from %s is empty", uri)); + } + else { + cause = new PrestoException(REMOTE_TASK_ERROR, format("Expected response code from %s to be %s, but was %s: %s%n%s", + uri, + HttpStatus.OK.code(), + response.getStatusCode(), + response.getStatusMessage(), + response.getResponseBody())); + } + } + callback.fatal(cause); + } + } + catch (Throwable t) { + // this should never happen + callback.fatal(t); + } + } + + @Override + public void onFailure(Throwable t) + { + callback.failed(t); + } + } + + @ThreadSafe + private static class RequestErrorTracker + { + private final TaskId taskId; + private final URI taskUri; + private final int maxConsecutiveErrorCount; + private final Duration minErrorDuration; + + private final RateLimiter errorRequestRateLimiter = RateLimiter.create(0.1); + + private final AtomicLong lastSuccessfulRequest = new AtomicLong(System.nanoTime()); + private final AtomicLong errorCount = new AtomicLong(); + private final Queue errorsSinceLastSuccess = new ConcurrentLinkedQueue<>(); + + public RequestErrorTracker(TaskId taskId, URI taskUri, int maxConsecutiveErrorCount, Duration minErrorDuration) + { + this.taskId = taskId; + this.taskUri = taskUri; + this.maxConsecutiveErrorCount = maxConsecutiveErrorCount; + this.minErrorDuration = minErrorDuration; + } + + public void acquireRequestPermit() + { + // don't update too fast in the face of errors + if (errorCount.get() > 0) { + errorRequestRateLimiter.acquire(); + } + } + + public void requestSucceeded() + { + lastSuccessfulRequest.set(System.nanoTime()); + errorCount.set(0); + errorsSinceLastSuccess.clear(); + } + + public void requestFailed(Throwable reason) + throws PrestoException + { + // cancellation is not a failure + if (reason instanceof CancellationException) { + return; + } + + if (reason instanceof RejectedExecutionException) { + throw new PrestoException(REMOTE_TASK_ERROR, reason); + } + + // log failure message + if (isExpectedError(reason)) { + // don't print a stack for a known errors + log.warn("Error updating task %s: %s: %s", taskId, reason.getMessage(), taskUri); + } + else { + log.warn(reason, "Error updating task %s: %s", taskId, taskUri); + } + + // remember the first 10 errors + if (errorsSinceLastSuccess.size() < 10) { + errorsSinceLastSuccess.add(reason); + } + + // fail the task, if we have more than X failures in a row and more than Y seconds have passed since the last request + long errorCount = this.errorCount.incrementAndGet(); + Duration timeSinceLastSuccess = Duration.nanosSince(lastSuccessfulRequest.get()); + if (errorCount > maxConsecutiveErrorCount && timeSinceLastSuccess.compareTo(minErrorDuration) > 0) { + // it is weird to mark the task failed locally and then cancel the remote task, but there is no way to tell a remote task that it is failed + PrestoException exception = new PrestoException(TOO_MANY_REQUESTS_FAILED, + format("%s (%s - %s failures, time since last success %s)", + WORKER_NODE_ERROR, + taskUri, + errorCount, + timeSinceLastSuccess.convertTo(TimeUnit.SECONDS))); + errorsSinceLastSuccess.forEach(exception::addSuppressed); + throw exception; + } + } + } + + public interface SimpleHttpResponseCallback + { + void success(T value); + + void failed(Throwable cause); + + void fatal(Throwable cause); + } + + private static void logError(Throwable t, String format, Object... args) + { + if (isExpectedError(t)) { + log.error(format + ": %s", ObjectArrays.concat(args, t)); + } + else { + log.error(t, format, args); + } + } + + private static boolean isExpectedError(Throwable t) + { + while (t != null) { + if ((t instanceof SocketException) || + (t instanceof SocketTimeoutException) || + (t instanceof EOFException) || + (t instanceof TimeoutException) || + (t instanceof ServiceUnavailableException)) { + return true; + } + t = t.getCause(); + } + return false; + } + + private static class ServiceUnavailableException + extends RuntimeException + { + public ServiceUnavailableException(URI uri) + { + super("Server returned SERVICE_UNAVAILABLE: " + uri); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/server/HttpRemoteTaskFactory.java b/presto-main/src/main/java/com/facebook/presto/server/HttpRemoteTaskFactory.java new file mode 100644 index 00000000..5b7f685d --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/server/HttpRemoteTaskFactory.java @@ -0,0 +1,112 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.server; + +import com.facebook.presto.OutputBuffers; +import com.facebook.presto.Session; +import com.facebook.presto.execution.LocationFactory; +import com.facebook.presto.execution.QueryManagerConfig; +import com.facebook.presto.execution.RemoteTask; +import com.facebook.presto.execution.RemoteTaskFactory; +import com.facebook.presto.execution.TaskId; +import com.facebook.presto.execution.TaskInfo; +import com.facebook.presto.execution.TaskManagerConfig; +import com.facebook.presto.metadata.Split; +import com.facebook.presto.operator.ForScheduler; +import com.facebook.presto.spi.Node; +import com.facebook.presto.sql.planner.PlanFragment; +import com.facebook.presto.sql.planner.plan.PlanNodeId; +import com.google.common.collect.Multimap; +import io.airlift.concurrent.BoundedExecutor; +import io.airlift.concurrent.ExecutorServiceAdapter; +import io.airlift.concurrent.ThreadPoolExecutorMBean; +import io.airlift.http.client.HttpClient; +import io.airlift.json.JsonCodec; +import io.airlift.units.Duration; +import org.weakref.jmx.Managed; +import org.weakref.jmx.Nested; + +import javax.inject.Inject; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ThreadPoolExecutor; + +import static io.airlift.concurrent.Threads.daemonThreadsNamed; +import static java.util.concurrent.Executors.newCachedThreadPool; + +public class HttpRemoteTaskFactory + implements RemoteTaskFactory +{ + private final HttpClient httpClient; + private final LocationFactory locationFactory; + private final JsonCodec taskInfoCodec; + private final JsonCodec taskUpdateRequestCodec; + private final int maxConsecutiveErrorCount; + private final Duration minErrorDuration; + private final Duration taskInfoRefreshMaxWait; + private final ExecutorService executor; + private final ThreadPoolExecutorMBean executorMBean; + + @Inject + public HttpRemoteTaskFactory(QueryManagerConfig config, + TaskManagerConfig taskConfig, + @ForScheduler HttpClient httpClient, + LocationFactory locationFactory, + JsonCodec taskInfoCodec, + JsonCodec taskUpdateRequestCodec) + { + this.httpClient = httpClient; + this.locationFactory = locationFactory; + this.taskInfoCodec = taskInfoCodec; + this.taskUpdateRequestCodec = taskUpdateRequestCodec; + this.maxConsecutiveErrorCount = config.getRemoteTaskMaxConsecutiveErrorCount(); + this.minErrorDuration = config.getRemoteTaskMinErrorDuration(); + this.taskInfoRefreshMaxWait = taskConfig.getInfoRefreshMaxWait(); + ExecutorService coreExecutor = newCachedThreadPool(daemonThreadsNamed("remote-task-callback-%s")); + this.executor = ExecutorServiceAdapter.from(new BoundedExecutor(coreExecutor, config.getRemoteTaskMaxCallbackThreads())); + this.executorMBean = new ThreadPoolExecutorMBean((ThreadPoolExecutor) coreExecutor); + } + + @Managed + @Nested + public ThreadPoolExecutorMBean getExecutor() + { + return executorMBean; + } + + @Override + public RemoteTask createRemoteTask(Session session, + TaskId taskId, + Node node, + PlanFragment fragment, + Multimap initialSplits, + OutputBuffers outputBuffers) + { + return new HttpRemoteTask(session, + taskId, + node.getNodeIdentifier(), + locationFactory.createTaskLocation(node, taskId), + fragment, + initialSplits, + outputBuffers, + httpClient, + executor, + maxConsecutiveErrorCount, + minErrorDuration, + taskInfoRefreshMaxWait, + taskInfoCodec, + taskUpdateRequestCodec + ); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/server/NodeResource.java b/presto-main/src/main/java/com/facebook/presto/server/NodeResource.java new file mode 100644 index 00000000..e79b1870 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/server/NodeResource.java @@ -0,0 +1,50 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.server; + +import com.facebook.presto.failureDetector.HeartbeatFailureDetector; +import com.google.common.collect.Maps; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import java.util.Collection; + +import static com.google.common.base.Predicates.in; + +@Path("/v1/node") +public class NodeResource +{ + private final HeartbeatFailureDetector failureDetector; + + @Inject + public NodeResource(HeartbeatFailureDetector failureDetector) + { + this.failureDetector = failureDetector; + } + + @GET + public Collection getNodeStats() + { + return failureDetector.getStats().values(); + } + + @GET + @Path("failed") + public Collection getFailed() + { + return Maps.filterKeys(failureDetector.getStats(), in(failureDetector.getFailed())).values(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/server/PagesResponseWriter.java b/presto-main/src/main/java/com/facebook/presto/server/PagesResponseWriter.java new file mode 100644 index 00000000..e9f4e410 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/server/PagesResponseWriter.java @@ -0,0 +1,101 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.server; + +import com.facebook.presto.block.PagesSerde; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.block.BlockEncodingSerde; +import com.google.common.base.Throwables; +import com.google.common.reflect.TypeToken; +import io.airlift.slice.OutputStreamSliceOutput; +import io.airlift.slice.RuntimeIOException; + +import javax.inject.Inject; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.MessageBodyWriter; +import javax.ws.rs.ext.Provider; + +import java.io.EOFException; +import java.io.IOException; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.List; + +import static com.facebook.presto.PrestoMediaTypes.PRESTO_PAGES; + +@Provider +@Produces(PRESTO_PAGES) +public class PagesResponseWriter + implements MessageBodyWriter> +{ + private static final MediaType PRESTO_PAGES_TYPE = MediaType.valueOf(PRESTO_PAGES); + private static final Type LIST_GENERIC_TOKEN; + + static { + try { + LIST_GENERIC_TOKEN = List.class.getMethod("get", int.class).getGenericReturnType(); + } + catch (NoSuchMethodException e) { + throw Throwables.propagate(e); + } + } + + private final BlockEncodingSerde blockEncodingSerde; + + @Inject + public PagesResponseWriter(BlockEncodingSerde blockEncodingSerde) + { + this.blockEncodingSerde = blockEncodingSerde; + } + + @Override + public boolean isWriteable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) + { + return List.class.isAssignableFrom(type) && + TypeToken.of(genericType).resolveType(LIST_GENERIC_TOKEN).getRawType().equals(Page.class) && + mediaType.isCompatible(PRESTO_PAGES_TYPE); + } + + @Override + public long getSize(List pages, Class type, Type genericType, Annotation[] annotations, MediaType mediaType) + { + return -1; + } + + @Override + public void writeTo(List pages, + Class type, + Type genericType, + Annotation[] annotations, + MediaType mediaType, + MultivaluedMap httpHeaders, + OutputStream output) + throws IOException, WebApplicationException + { + try { + PagesSerde.writePages(blockEncodingSerde, new OutputStreamSliceOutput(output), pages); + } + catch (RuntimeIOException e) { + // EOF exception occurs when the client disconnects while writing data + // This is not a "server" problem so we don't want to log this + if (!(e.getCause() instanceof EOFException)) { + throw e; + } + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/server/PluginClassLoader.java b/presto-main/src/main/java/com/facebook/presto/server/PluginClassLoader.java new file mode 100644 index 00000000..cc0545b4 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/server/PluginClassLoader.java @@ -0,0 +1,221 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.server; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.common.collect.Iterators; + +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; + +class PluginClassLoader + extends URLClassLoader +{ + private final List hiddenClasses; + private final List parentFirstClasses; + private final List hiddenResources; + private final List parentFirstResources; + + public PluginClassLoader(List urls, + ClassLoader parent, + Iterable hiddenClasses, + Iterable parentFirstClasses) + { + this(urls, + parent, + hiddenClasses, + parentFirstClasses, + Iterables.transform(hiddenClasses, PluginClassLoader::classNameToResource), + Iterables.transform(parentFirstClasses, PluginClassLoader::classNameToResource)); + } + + public PluginClassLoader(List urls, + ClassLoader parent, + Iterable hiddenClasses, + Iterable parentFirstClasses, + Iterable hiddenResources, + Iterable parentFirstResources) + { + // child first requires a parent class loader + super(urls.toArray(new URL[urls.size()]), checkNotNull(parent, "parent is null")); + this.hiddenClasses = ImmutableList.copyOf(hiddenClasses); + this.parentFirstClasses = ImmutableList.copyOf(parentFirstClasses); + this.hiddenResources = ImmutableList.copyOf(hiddenResources); + this.parentFirstResources = ImmutableList.copyOf(parentFirstResources); + } + + @Override + protected Class loadClass(String name, boolean resolve) + throws ClassNotFoundException + { + // grab the magic lock + synchronized (getClassLoadingLock(name)) { + // Check if class is in the loaded classes cache + Class cachedClass = findLoadedClass(name); + if (cachedClass != null) { + return resolveClass(cachedClass, resolve); + } + + // If this is not a parent first class, look for the class locally + if (!isParentFirstClass(name)) { + try { + Class clazz = findClass(name); + return resolveClass(clazz, resolve); + } + catch (ClassNotFoundException ignored) { + // not a local class + } + } + + // Check parent class loaders, unless this is a hidden class + if (!isHiddenClass(name)) { + try { + Class clazz = getParent().loadClass(name); + return resolveClass(clazz, resolve); + } + catch (ClassNotFoundException ignored) { + // this parent didn't have the class + } + } + + // If this is a parent first class, now look for the class locally + if (isParentFirstClass(name)) { + Class clazz = findClass(name); + return resolveClass(clazz, resolve); + } + + throw new ClassNotFoundException(name); + } + } + + private Class resolveClass(Class clazz, boolean resolve) + { + if (resolve) { + resolveClass(clazz); + } + return clazz; + } + + private boolean isParentFirstClass(String name) + { + for (String nonOverridableClass : parentFirstClasses) { + // todo maybe make this more precise and only match base package + if (name.startsWith(nonOverridableClass)) { + return true; + } + } + return false; + } + + private boolean isHiddenClass(String name) + { + for (String hiddenClass : hiddenClasses) { + // todo maybe make this more precise and only match base package + if (name.startsWith(hiddenClass)) { + return true; + } + } + return false; + } + + @Override + public URL getResource(String name) + { + // If this is not a parent first resource, check local resources first + if (!isParentFirstResource(name)) { + URL url = findResource(name); + if (url != null) { + return url; + } + } + + // Check parent class loaders + if (!isHiddenResource(name)) { + URL url = getParent().getResource(name); + if (url != null) { + return url; + } + } + + // If this is a parent first resource, now check local resources + if (isParentFirstResource(name)) { + URL url = findResource(name); + if (url != null) { + return url; + } + } + + return null; + } + + @Override + public Enumeration getResources(String name) + throws IOException + { + List> resources = new ArrayList<>(); + + // If this is not a parent first resource, add resources from local urls first + if (!isParentFirstResource(name)) { + Iterator myResources = Iterators.forEnumeration(findResources(name)); + resources.add(myResources); + } + + // Add parent resources + if (!isHiddenResource(name)) { + Iterator parentResources = Iterators.forEnumeration(getParent().getResources(name)); + resources.add(parentResources); + } + + // If this is a parent first resource, now add resources from local urls + if (isParentFirstResource(name)) { + Iterator myResources = Iterators.forEnumeration(findResources(name)); + resources.add(myResources); + } + + return Iterators.asEnumeration(Iterators.concat(resources.iterator())); + } + + private boolean isParentFirstResource(String name) + { + for (String nonOverridableResource : parentFirstResources) { + if (name.startsWith(nonOverridableResource)) { + return true; + } + } + return false; + } + + private boolean isHiddenResource(String name) + { + for (String hiddenResource : hiddenResources) { + if (name.startsWith(hiddenResource)) { + return true; + } + } + return false; + } + + private static String classNameToResource(String className) + { + return className.replace('.', '/'); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/server/PluginManager.java b/presto-main/src/main/java/com/facebook/presto/server/PluginManager.java new file mode 100644 index 00000000..989ad6be --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/server/PluginManager.java @@ -0,0 +1,291 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.server; + +import com.facebook.presto.block.BlockEncodingManager; +import com.facebook.presto.connector.ConnectorManager; +import com.facebook.presto.metadata.FunctionFactory; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.spi.ConnectorFactory; +import com.facebook.presto.spi.Plugin; +import com.facebook.presto.spi.block.BlockEncodingFactory; +import com.facebook.presto.spi.classloader.ThreadContextClassLoader; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.type.ParametricType; +import com.facebook.presto.type.TypeRegistry; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.google.common.collect.Ordering; +import com.google.inject.Injector; +import io.airlift.configuration.ConfigurationFactory; +import io.airlift.http.server.HttpServerInfo; +import io.airlift.log.Logger; +import io.airlift.node.NodeInfo; +import io.airlift.resolver.ArtifactResolver; +import io.airlift.resolver.DefaultArtifact; +import org.sonatype.aether.artifact.Artifact; + +import javax.annotation.concurrent.ThreadSafe; +import javax.inject.Inject; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.TreeMap; +import java.util.concurrent.atomic.AtomicBoolean; + +import static com.google.common.base.Preconditions.checkNotNull; + +@ThreadSafe +public class PluginManager +{ + private static final List HIDDEN_CLASSES = ImmutableList.builder() + .add("org.slf4j") + .build(); + + private static final ImmutableList PARENT_FIRST_CLASSES = ImmutableList.builder() + .add("com.facebook.presto") + .add("com.fasterxml.jackson") + .add("io.airlift.slice") + .add("javax.inject") + .add("javax.annotation") + .add("java.") + .build(); + + private static final Logger log = Logger.get(PluginManager.class); + + private final Injector injector; + private final ConnectorManager connectorManager; + private final Metadata metadata; + private final BlockEncodingManager blockEncodingManager; + private final TypeRegistry typeRegistry; + private final ArtifactResolver resolver; + private final File installedPluginsDir; + private final List plugins; + private final Map optionalConfig; + private final AtomicBoolean pluginsLoading = new AtomicBoolean(); + private final AtomicBoolean pluginsLoaded = new AtomicBoolean(); + + @Inject + public PluginManager(Injector injector, + NodeInfo nodeInfo, + HttpServerInfo httpServerInfo, + PluginManagerConfig config, + ConnectorManager connectorManager, + ConfigurationFactory configurationFactory, + Metadata metadata, + BlockEncodingManager blockEncodingManager, + TypeRegistry typeRegistry) + { + checkNotNull(injector, "injector is null"); + checkNotNull(nodeInfo, "nodeInfo is null"); + checkNotNull(httpServerInfo, "httpServerInfo is null"); + checkNotNull(config, "config is null"); + checkNotNull(configurationFactory, "configurationFactory is null"); + + this.injector = injector; + installedPluginsDir = config.getInstalledPluginsDir(); + if (config.getPlugins() == null) { + this.plugins = ImmutableList.of(); + } + else { + this.plugins = ImmutableList.copyOf(config.getPlugins()); + } + this.resolver = new ArtifactResolver(config.getMavenLocalRepository(), config.getMavenRemoteRepository()); + + Map optionalConfig = new TreeMap<>(configurationFactory.getProperties()); + optionalConfig.put("node.id", nodeInfo.getNodeId()); + // TODO: make this work with and without HTTP and HTTPS + optionalConfig.put("http-server.http.port", Integer.toString(httpServerInfo.getHttpUri().getPort())); + this.optionalConfig = ImmutableMap.copyOf(optionalConfig); + + this.connectorManager = checkNotNull(connectorManager, "connectorManager is null"); + this.metadata = checkNotNull(metadata, "metadata is null"); + this.blockEncodingManager = checkNotNull(blockEncodingManager, "blockEncodingManager is null"); + this.typeRegistry = checkNotNull(typeRegistry, "typeRegistry is null"); + } + + public boolean arePluginsLoaded() + { + return pluginsLoaded.get(); + } + + public void loadPlugins() + throws Exception + { + if (!pluginsLoading.compareAndSet(false, true)) { + return; + } + + for (File file : listFiles(installedPluginsDir)) { + if (file.isDirectory()) { + loadPlugin(file.getAbsolutePath()); + } + } + + for (String plugin : plugins) { + loadPlugin(plugin); + } + + pluginsLoaded.set(true); + } + + private void loadPlugin(String plugin) + throws Exception + { + log.info("-- Loading plugin %s --", plugin); + URLClassLoader pluginClassLoader = buildClassLoader(plugin); + try (ThreadContextClassLoader ignored = new ThreadContextClassLoader(pluginClassLoader)) { + loadPlugin(pluginClassLoader); + } + log.info("-- Finished loading plugin %s --", plugin); + } + + private void loadPlugin(URLClassLoader pluginClassLoader) + throws Exception + { + ServiceLoader serviceLoader = ServiceLoader.load(Plugin.class, pluginClassLoader); + List plugins = ImmutableList.copyOf(serviceLoader); + + if (plugins.isEmpty()) { + log.warn("No service providers of type %s", Plugin.class.getName()); + } + + for (Plugin plugin : plugins) { + log.info("Installing %s", plugin.getClass().getName()); + installPlugin(plugin); + } + } + + public void installPlugin(Plugin plugin) + { + injector.injectMembers(plugin); + + plugin.setOptionalConfig(optionalConfig); + + for (BlockEncodingFactory blockEncodingFactory : plugin.getServices(BlockEncodingFactory.class)) { + log.info("Registering block encoding %s", blockEncodingFactory.getName()); + blockEncodingManager.addBlockEncodingFactory(blockEncodingFactory); + } + + for (Type type : plugin.getServices(Type.class)) { + log.info("Registering type %s", type.getTypeSignature()); + typeRegistry.addType(type); + } + + for (ParametricType parametricType : plugin.getServices(ParametricType.class)) { + log.info("Registering parametric type %s", parametricType.getName()); + typeRegistry.addParametricType(parametricType); + } + + for (ConnectorFactory connectorFactory : plugin.getServices(ConnectorFactory.class)) { + log.info("Registering connector %s", connectorFactory.getName()); + connectorManager.addConnectorFactory(connectorFactory); + } + + for (FunctionFactory functionFactory : plugin.getServices(FunctionFactory.class)) { + log.info("Registering functions from %s", functionFactory.getClass().getName()); + metadata.addFunctions(functionFactory.listFunctions()); + } + } + + private URLClassLoader buildClassLoader(String plugin) + throws Exception + { + File file = new File(plugin); + if (file.isFile() && (file.getName().equals("pom.xml") || file.getName().endsWith(".pom"))) { + return buildClassLoaderFromPom(file); + } + if (file.isDirectory()) { + return buildClassLoaderFromDirectory(file); + } + return buildClassLoaderFromCoordinates(plugin); + } + + private URLClassLoader buildClassLoaderFromPom(File pomFile) + throws Exception + { + List artifacts = resolver.resolvePom(pomFile); + return createClassLoader(artifacts, pomFile.getPath()); + } + + private URLClassLoader buildClassLoaderFromDirectory(File dir) + throws Exception + { + log.debug("Classpath for %s:", dir.getName()); + List urls = new ArrayList<>(); + for (File file : listFiles(dir)) { + log.debug(" %s", file); + urls.add(file.toURI().toURL()); + } + return createClassLoader(urls); + } + + private URLClassLoader buildClassLoaderFromCoordinates(String coordinates) + throws Exception + { + Artifact rootArtifact = new DefaultArtifact(coordinates); + List artifacts = resolver.resolveArtifacts(rootArtifact); + return createClassLoader(artifacts, rootArtifact.toString()); + } + + private URLClassLoader createClassLoader(List artifacts, String name) + throws IOException + { + log.debug("Classpath for %s:", name); + List urls = new ArrayList<>(); + for (Artifact artifact : sortedArtifacts(artifacts)) { + if (artifact.getFile() == null) { + throw new RuntimeException("Could not resolve artifact: " + artifact); + } + File file = artifact.getFile().getCanonicalFile(); + log.debug(" %s", file); + urls.add(file.toURI().toURL()); + } + return createClassLoader(urls); + } + + private URLClassLoader createClassLoader(List urls) + { + ClassLoader parent = getClass().getClassLoader(); + return new PluginClassLoader(urls, parent, HIDDEN_CLASSES, PARENT_FIRST_CLASSES); + } + + private static List listFiles(File installedPluginsDir) + { + if (installedPluginsDir != null && installedPluginsDir.isDirectory()) { + File[] files = installedPluginsDir.listFiles(); + if (files != null) { + Arrays.sort(files); + return ImmutableList.copyOf(files); + } + } + return ImmutableList.of(); + } + + private static List sortedArtifacts(List artifacts) + { + List list = Lists.newArrayList(artifacts); + Collections.sort(list, Ordering.natural().nullsLast().onResultOf(Artifact::getFile)); + return list; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/server/PluginManagerConfig.java b/presto-main/src/main/java/com/facebook/presto/server/PluginManagerConfig.java new file mode 100644 index 00000000..eb582cf1 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/server/PluginManagerConfig.java @@ -0,0 +1,113 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.server; + +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; +import io.airlift.configuration.Config; +import io.airlift.resolver.ArtifactResolver; + +import javax.validation.constraints.NotNull; + +import java.io.File; +import java.util.List; + +public class PluginManagerConfig +{ + private File installedPluginsDir = new File("plugin"); + private List plugins; + private File pluginConfigurationDir = new File("etc/"); + private String mavenLocalRepository = ArtifactResolver.USER_LOCAL_REPO; + private List mavenRemoteRepository = ImmutableList.of(ArtifactResolver.MAVEN_CENTRAL_URI); + + public File getInstalledPluginsDir() + { + return installedPluginsDir; + } + + @Config("plugin.dir") + public PluginManagerConfig setInstalledPluginsDir(File installedPluginsDir) + { + this.installedPluginsDir = installedPluginsDir; + return this; + } + + public List getPlugins() + { + return plugins; + } + + public PluginManagerConfig setPlugins(List plugins) + { + this.plugins = plugins; + return this; + } + + @Config("plugin.bundles") + public PluginManagerConfig setPlugins(String plugins) + { + if (plugins == null) { + this.plugins = null; + } + else { + this.plugins = ImmutableList.copyOf(Splitter.on(',').omitEmptyStrings().trimResults().split(plugins)); + } + return this; + } + + @NotNull + public File getPluginConfigurationDir() + { + return pluginConfigurationDir; + } + + @Config("plugin.config-dir") + public PluginManagerConfig setPluginConfigurationDir(File pluginConfigurationDir) + { + this.pluginConfigurationDir = pluginConfigurationDir; + return this; + } + + @NotNull + public String getMavenLocalRepository() + { + return mavenLocalRepository; + } + + @Config("maven.repo.local") + public PluginManagerConfig setMavenLocalRepository(String mavenLocalRepository) + { + this.mavenLocalRepository = mavenLocalRepository; + return this; + } + + @NotNull + public List getMavenRemoteRepository() + { + return mavenRemoteRepository; + } + + public PluginManagerConfig setMavenRemoteRepository(List mavenRemoteRepository) + { + this.mavenRemoteRepository = mavenRemoteRepository; + return this; + } + + @Config("maven.repo.remote") + public PluginManagerConfig setMavenRemoteRepository(String mavenRemoteRepository) + { + this.mavenRemoteRepository = ImmutableList.copyOf(Splitter.on(',').omitEmptyStrings().trimResults().split(mavenRemoteRepository)); + return this; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/server/PrestoJvmRequirements.java b/presto-main/src/main/java/com/facebook/presto/server/PrestoJvmRequirements.java new file mode 100644 index 00000000..b20260bf --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/server/PrestoJvmRequirements.java @@ -0,0 +1,83 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.server; + +import com.google.common.base.StandardSystemProperty; +import io.airlift.slice.Slice; +import io.airlift.slice.Slices; + +import java.nio.ByteOrder; + +final class PrestoJvmRequirements +{ + private PrestoJvmRequirements() {} + + public static void verifyJvmRequirements() + { + String specVersion = StandardSystemProperty.JAVA_SPECIFICATION_VERSION.value(); + if ((specVersion == null) || (specVersion.compareTo("1.8") < 0)) { + failRequirement("Presto requires Java 1.8+ (found %s)", specVersion); + } + + String vendor = StandardSystemProperty.JAVA_VENDOR.value(); + if (!"Oracle Corporation".equals(vendor)) { + failRequirement("Presto requires an Oracle or OpenJDK JVM (found %s)", vendor); + } + + String dataModel = System.getProperty("sun.arch.data.model"); + if (!"64".equals(dataModel)) { + failRequirement("Presto requires a 64-bit JVM (found %s)", dataModel); + } + + String osName = StandardSystemProperty.OS_NAME.value(); + String osArch = StandardSystemProperty.OS_ARCH.value(); + if ("Linux".equals(osName)) { + if (!"amd64".equals(osArch)) { + failRequirement("Presto requires x86-64 or amd64 on Linux (found %s)", osArch); + } + } + else if ("Mac OS X".equals(osName)) { + if (!"x86_64".equals(osArch)) { + failRequirement("Presto requires x86_64 on Mac OS X (found %s)", osArch); + } + } + else { + failRequirement("Presto requires Linux or Mac OS X (found %s)", osName); + } + + if (!ByteOrder.nativeOrder().equals(ByteOrder.LITTLE_ENDIAN)) { + failRequirement("Presto requires a little endian platform (found %s)", ByteOrder.nativeOrder()); + } + + verifySlice(); + } + + private static void verifySlice() + { + Slice slice = Slices.wrappedBuffer(new byte[5]); + slice.setByte(4, 0xDE); + slice.setByte(3, 0xAD); + slice.setByte(2, 0xBE); + slice.setByte(1, 0xEF); + if (slice.getInt(1) != 0xDEADBEEF) { + failRequirement("Slice library produced an unexpected result"); + } + } + + private static void failRequirement(String format, Object... args) + { + System.err.println(String.format(format, args)); + System.exit(100); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/server/PrestoServer.java b/presto-main/src/main/java/com/facebook/presto/server/PrestoServer.java new file mode 100644 index 00000000..8a4825ef --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/server/PrestoServer.java @@ -0,0 +1,216 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.server; + +import com.facebook.presto.discovery.EmbeddedDiscoveryModule; +import com.facebook.presto.execution.NodeSchedulerConfig; +import com.facebook.presto.metadata.CatalogManager; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.sql.parser.SqlParserOptions; +import com.google.common.base.Joiner; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; +import com.google.inject.Injector; +import com.google.inject.Module; +import io.airlift.bootstrap.Bootstrap; +import io.airlift.discovery.client.Announcer; +import io.airlift.discovery.client.DiscoveryModule; +import io.airlift.discovery.client.ServiceAnnouncement; +import io.airlift.event.client.HttpEventModule; +import io.airlift.event.client.JsonEventModule; +import io.airlift.http.server.HttpServerModule; +import io.airlift.jaxrs.JaxrsModule; +import io.airlift.jmx.JmxHttpModule; +import io.airlift.jmx.JmxModule; +import io.airlift.json.JsonModule; +import io.airlift.log.LogJmxModule; +import io.airlift.log.Logger; +import io.airlift.node.NodeModule; +import io.airlift.tracetoken.TraceTokenModule; +import org.weakref.jmx.guice.MBeanModule; + +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.facebook.presto.server.PrestoJvmRequirements.verifyJvmRequirements; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Strings.nullToEmpty; +import static io.airlift.discovery.client.ServiceAnnouncement.ServiceAnnouncementBuilder; +import static io.airlift.discovery.client.ServiceAnnouncement.serviceAnnouncement; + +public class PrestoServer + implements Runnable +{ + private static Announcer announcer; + private static ServerConfig serverConfig; + + public enum DatasourceAction { + ADD, DELETE; + } + + public static void main(String[] args) + { + new PrestoServer().run(); + } + + private final SqlParserOptions sqlParserOptions; + + public PrestoServer() + { + this(new SqlParserOptions()); + } + + public PrestoServer(SqlParserOptions sqlParserOptions) + { + this.sqlParserOptions = checkNotNull(sqlParserOptions, "sqlParserOptions is null"); + } + + @Override + public void run() + { + verifyJvmRequirements(); + + Logger log = Logger.get(PrestoServer.class); + + ImmutableList.Builder modules = ImmutableList.builder(); + modules.add( + new NodeModule(), + new DiscoveryModule(), + new HttpServerModule(), + new JsonModule(), + new JaxrsModule(true), + new MBeanModule(), + new JmxModule(), + new JmxHttpModule(), + new LogJmxModule(), + new TraceTokenModule(), + new JsonEventModule(), + new HttpEventModule(), + new EmbeddedDiscoveryModule(), + new ServerMainModule(sqlParserOptions)); + + modules.addAll(getAdditionalModules()); + + Bootstrap app = new Bootstrap(modules.build()); + + try { + Injector injector = app.strictConfig().initialize(); + serverConfig = injector.getInstance(ServerConfig.class); + + injector.getInstance(PluginManager.class).loadPlugins(); + + injector.getInstance(CatalogManager.class).loadCatalogs(); + + announcer = injector.getInstance(Announcer.class); + // TODO: remove this huge hack + updateDatasources( + announcer, + injector.getInstance(Metadata.class), + injector.getInstance(ServerConfig.class), + injector.getInstance(NodeSchedulerConfig.class)); + + announcer.start(); + + log.info("======== SERVER STARTED ========"); + } + catch (Throwable e) { + log.error(e); + System.exit(1); + } + } + + protected Iterable getAdditionalModules() + { + return ImmutableList.of(); + } + + private static void updateDatasources(Announcer announcer, Metadata metadata, ServerConfig serverConfig, NodeSchedulerConfig schedulerConfig) + { + // get existing announcement + ServiceAnnouncement announcement = getPrestoAnnouncement(announcer.getServiceAnnouncements()); + + // get existing sources + String property = nullToEmpty(announcement.getProperties().get("datasources")); + List values = Splitter.on(',').trimResults().omitEmptyStrings().splitToList(property); + Set datasources = new LinkedHashSet<>(values); + + // automatically build sources if not configured + if (datasources.isEmpty()) { + Set catalogs = metadata.getCatalogNames().keySet(); + // if this is a dedicated coordinator, only add jmx + if (serverConfig.isCoordinator() && !schedulerConfig.isIncludeCoordinator()) { + if (catalogs.contains("jmx")) { + datasources.add("jmx"); + } + } + else { + datasources.addAll(catalogs); + } + } + + // build announcement with updated sources + ServiceAnnouncementBuilder builder = serviceAnnouncement(announcement.getType()); + for (Map.Entry entry : announcement.getProperties().entrySet()) { + if (!entry.getKey().equals("datasources")) { + builder.addProperty(entry.getKey(), entry.getValue()); + } + } + builder.addProperty("datasources", Joiner.on(',').join(datasources)); + + // update announcement + announcer.removeServiceAnnouncement(announcement.getId()); + announcer.addServiceAnnouncement(builder.build()); + } + + public static void updateDatasourcesAnnouncement(String connectorId, DatasourceAction action) + { + // get existing announcement + ServiceAnnouncement announcement = getPrestoAnnouncement(announcer.getServiceAnnouncements()); + + // update datasources property + Map properties = new LinkedHashMap<>(announcement.getProperties()); + String property = nullToEmpty(properties.get("datasources")); + Set datasources = new LinkedHashSet<>(Splitter.on(',').trimResults().omitEmptyStrings().splitToList(property)); + if (action == DatasourceAction.ADD) { + datasources.add(connectorId); + } + else if (action == DatasourceAction.DELETE) { + datasources.remove(connectorId); + } + properties.put("datasources", Joiner.on(',').join(datasources)); + + // update announcement + announcer.removeServiceAnnouncement(announcement.getId()); + announcer.addServiceAnnouncement(serviceAnnouncement(announcement.getType()).addProperties(properties).build()); + announcer.forceAnnounce(); + } + + private static ServiceAnnouncement getPrestoAnnouncement(Set announcements) + { + for (ServiceAnnouncement announcement : announcements) { + if (announcement.getType().equals("presto")) { + return announcement; + } + } + throw new IllegalArgumentException("Presto announcement not found: " + announcements); + } + + public static boolean isCoordinator() + { + return serverConfig == null || serverConfig.isCoordinator(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/server/QueryExecutionResource.java b/presto-main/src/main/java/com/facebook/presto/server/QueryExecutionResource.java new file mode 100644 index 00000000..34cade8c --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/server/QueryExecutionResource.java @@ -0,0 +1,337 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.server; + +import com.facebook.presto.execution.BufferInfo; +import com.facebook.presto.execution.QueryId; +import com.facebook.presto.execution.QueryInfo; +import com.facebook.presto.execution.QueryManager; +import com.facebook.presto.execution.StageInfo; +import com.facebook.presto.execution.TaskId; +import com.facebook.presto.execution.TaskInfo; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.io.Resources; +import io.airlift.units.DataSize; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.concurrent.TimeUnit; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.io.Resources.getResource; + +@Path("/") +public class QueryExecutionResource +{ + // synthetic task id used by the output buffer of the top task + private static final TaskId OUTPUT_TASK_ID = new TaskId("output", "buffer", "id"); + + private final QueryManager manager; + + @Inject + public QueryExecutionResource(QueryManager manager) + { + checkNotNull(manager, "manager is null"); + this.manager = manager; + } + + @GET + @Path("/ui/query-execution") + @Produces(MediaType.TEXT_HTML) + public String getUi() + throws IOException + { + return Resources.toString(getResource(getClass(), "query-execution.html"), StandardCharsets.UTF_8); + } + + @GET + @Path("/v1/query-execution/{queryId}") + @Produces(MediaType.APPLICATION_JSON) + public Response getTaskInfo(@PathParam("queryId") String queryId) + { + QueryInfo query; + try { + query = manager.getQueryInfo(QueryId.valueOf(queryId)); + } + catch (NoSuchElementException e) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + + List stages = collectStages(query.getOutputStage()); + + List tasks = new ArrayList<>(); + List flows = new ArrayList<>(); + for (StageInfo stage : stages) { + for (TaskInfo task : stage.getTasks()) { + int bufferedPages = 0; + for (BufferInfo bufferInfo : task.getOutputBuffers().getBuffers()) { + bufferedPages += bufferInfo.getBufferedPages(); + + if (!bufferInfo.getBufferId().equals(OUTPUT_TASK_ID)) { + flows.add(new Flow( + task.getTaskId().toString(), + bufferInfo.getBufferId().toString(), + bufferInfo.getPageBufferInfo().getPagesAdded(), + bufferInfo.getBufferedPages(), + bufferInfo.isFinished())); + } + } + + long last = System.currentTimeMillis(); + if (task.getStats().getEndTime() != null) { + last = task.getStats().getEndTime().getMillis(); + } + + tasks.add(new Task(task.getTaskId().toString(), + task.getState().toString(), + task.getSelf().getHost(), + last - task.getStats().getCreateTime().getMillis(), + task.getStats().getTotalCpuTime().roundTo(TimeUnit.MILLISECONDS), + task.getStats().getTotalBlockedTime().roundTo(TimeUnit.MILLISECONDS), + task.getStats().getRawInputDataSize().roundTo(DataSize.Unit.BYTE), + task.getStats().getRawInputPositions(), + task.getStats().getOutputDataSize().roundTo(DataSize.Unit.BYTE), + task.getStats().getOutputPositions(), + task.getStats().getMemoryReservation().roundTo(DataSize.Unit.BYTE), + task.getStats().getQueuedDrivers(), + task.getStats().getRunningDrivers(), + task.getStats().getCompletedDrivers(), + bufferedPages)); + } + } + + Map result = ImmutableMap.builder() + .put("tasks", tasks) + .put("flows", flows) + .build(); + + return Response.ok(result).build(); + } + + private static List collectStages(StageInfo stage) + { + ImmutableList.Builder result = ImmutableList.builder(); + result.add(stage); + for (StageInfo child : stage.getSubStages()) { + result.addAll(collectStages(child)); + } + + return result.build(); + } + + public static class Flow + { + private final String from; + private final String to; + private final long pagesSent; + private final int bufferedPages; + private final boolean finished; + + public Flow(String from, String to, long pagesSent, int bufferedPages, boolean finished) + { + this.from = from; + this.to = to; + this.pagesSent = pagesSent; + this.bufferedPages = bufferedPages; + this.finished = finished; + } + + @JsonProperty + public String getFrom() + { + return from; + } + + @JsonProperty + public String getTo() + { + return to; + } + + @JsonProperty + public long getPagesSent() + { + return pagesSent; + } + + @JsonProperty + public int getBufferedPages() + { + return bufferedPages; + } + + @JsonProperty + public boolean isFinished() + { + return finished; + } + } + + public static class Task + { + private final String taskId; + private final String state; + private final String host; + private final long uptime; + private final long cpuMillis; + private final long blockedMillis; + private final long inputBytes; + private final long inputRows; + private final long outputBytes; + private final long outputRows; + private final long usedMemoryBytes; + private final int queuedSplits; + private final int runningSplits; + private final int completedSplits; + private final int bufferedPages; + + public Task( + String taskId, + String state, + String host, + long uptimeMillis, + long cpuMillis, + long blockedMillis, + long inputBytes, + long inputRows, + long outputBytes, + long outputRows, + long usedMemoryBytes, + int queuedSplits, + int runningSplits, + int completedSplits, + int bufferedPages) + { + this.taskId = taskId; + this.state = state; + this.host = host; + this.uptime = uptimeMillis; + this.cpuMillis = cpuMillis; + this.blockedMillis = blockedMillis; + this.inputBytes = inputBytes; + this.inputRows = inputRows; + this.outputBytes = outputBytes; + this.outputRows = outputRows; + this.usedMemoryBytes = usedMemoryBytes; + this.queuedSplits = queuedSplits; + this.runningSplits = runningSplits; + this.completedSplits = completedSplits; + this.bufferedPages = bufferedPages; + } + + @JsonProperty + public String getTaskId() + { + return taskId; + } + + @JsonProperty + public String getState() + { + return state; + } + + @JsonProperty + public String getHost() + { + return host; + } + + @JsonProperty + public long getUptime() + { + return uptime; + } + + @JsonProperty + public long getCpuMillis() + { + return cpuMillis; + } + + @JsonProperty + public long getBlockedMillis() + { + return blockedMillis; + } + + @JsonProperty + public long getInputBytes() + { + return inputBytes; + } + + @JsonProperty + public long getInputRows() + { + return inputRows; + } + + @JsonProperty + public long getOutputBytes() + { + return outputBytes; + } + + @JsonProperty + public long getOutputRows() + { + return outputRows; + } + + @JsonProperty + public long getUsedMemoryBytes() + { + return usedMemoryBytes; + } + + @JsonProperty + public int getQueuedSplits() + { + return queuedSplits; + } + + @JsonProperty + public int getRunningSplits() + { + return runningSplits; + } + + @JsonProperty + public int getCompletedSplits() + { + return completedSplits; + } + + @JsonProperty + public int getBufferedPages() + { + return bufferedPages; + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/server/QueryResource.java b/presto-main/src/main/java/com/facebook/presto/server/QueryResource.java new file mode 100644 index 00000000..731e708c --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/server/QueryResource.java @@ -0,0 +1,122 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.server; + +import com.facebook.presto.Session; +import com.facebook.presto.execution.QueryId; +import com.facebook.presto.execution.QueryInfo; +import com.facebook.presto.execution.QueryManager; +import com.facebook.presto.execution.StageId; +import com.google.common.collect.ImmutableList; + +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.core.UriInfo; + +import java.net.URI; +import java.util.List; +import java.util.NoSuchElementException; + +import static com.facebook.presto.server.ResourceUtil.assertRequest; +import static com.facebook.presto.server.ResourceUtil.createSessionForRequest; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Strings.isNullOrEmpty; +import static io.airlift.http.client.HttpUriBuilder.uriBuilderFrom; + +/** + * Manage queries scheduled on this node + */ +@Path("/v1/query") +public class QueryResource +{ + private final QueryManager queryManager; + + @Inject + public QueryResource(QueryManager queryManager) + { + this.queryManager = checkNotNull(queryManager, "queryManager is null"); + } + + @GET + public List getAllQueryInfo() + { + return extractBasicQueryInfo(queryManager.getAllQueryInfo()); + } + + private static List extractBasicQueryInfo(List allQueryInfo) + { + ImmutableList.Builder basicQueryInfo = ImmutableList.builder(); + for (QueryInfo queryInfo : allQueryInfo) { + basicQueryInfo.add(new BasicQueryInfo(queryInfo)); + } + return basicQueryInfo.build(); + } + + @GET + @Path("{queryId}") + public Response getQueryInfo(@PathParam("queryId") QueryId queryId) + { + checkNotNull(queryId, "queryId is null"); + + try { + QueryInfo queryInfo = queryManager.getQueryInfo(queryId); + return Response.ok(queryInfo).build(); + } + catch (NoSuchElementException e) { + return Response.status(Status.GONE).build(); + } + } + + @POST + @Produces(MediaType.APPLICATION_JSON) + public Response createQuery( + String statement, + @Context HttpServletRequest servletRequest, + @Context UriInfo uriInfo) + { + assertRequest(!isNullOrEmpty(statement), "SQL statement is empty"); + + Session session = createSessionForRequest(servletRequest); + + QueryInfo queryInfo = queryManager.createQuery(session, statement); + URI pagesUri = uriBuilderFrom(uriInfo.getRequestUri()).appendPath(queryInfo.getQueryId().toString()).build(); + return Response.created(pagesUri).entity(queryInfo).build(); + } + + @DELETE + @Path("{queryId}") + public void cancelQuery(@PathParam("queryId") QueryId queryId) + { + checkNotNull(queryId, "queryId is null"); + queryManager.cancelQuery(queryId); + } + + @DELETE + @Path("stage/{stageId}") + public void cancelStage(@PathParam("stageId") StageId stageId) + { + checkNotNull(stageId, "stageId is null"); + queryManager.cancelStage(stageId); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/server/ResourceUtil.java b/presto-main/src/main/java/com/facebook/presto/server/ResourceUtil.java new file mode 100644 index 00000000..88773a5c --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/server/ResourceUtil.java @@ -0,0 +1,170 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.server; + +import com.facebook.presto.Session; +import com.facebook.presto.Session.SessionBuilder; +import com.facebook.presto.spi.type.TimeZoneKey; +import com.facebook.presto.spi.type.TimeZoneNotSupportedException; +import com.google.common.base.Splitter; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; + +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; + +import static com.facebook.presto.client.PrestoHeaders.PRESTO_CATALOG; +import static com.facebook.presto.client.PrestoHeaders.PRESTO_LANGUAGE; +import static com.facebook.presto.client.PrestoHeaders.PRESTO_SCHEMA; +import static com.facebook.presto.client.PrestoHeaders.PRESTO_SESSION; +import static com.facebook.presto.client.PrestoHeaders.PRESTO_SOURCE; +import static com.facebook.presto.client.PrestoHeaders.PRESTO_TIME_ZONE; +import static com.facebook.presto.client.PrestoHeaders.PRESTO_USER; +import static com.facebook.presto.util.ImmutableCollectors.toImmutableList; +import static com.google.common.base.Strings.isNullOrEmpty; +import static com.google.common.net.HttpHeaders.USER_AGENT; +import static java.lang.String.format; + +final class ResourceUtil +{ + private ResourceUtil() + { + } + + public static Session createSessionForRequest(HttpServletRequest servletRequest) + { + SessionBuilder sessionBuilder = Session.builder() + .setUser(getRequiredHeader(servletRequest, PRESTO_USER, "User")) + .setSource(servletRequest.getHeader(PRESTO_SOURCE)) + .setCatalog(getRequiredHeader(servletRequest, PRESTO_CATALOG, "Catalog")) + .setSchema(getRequiredHeader(servletRequest, PRESTO_SCHEMA, "Schema")) + .setRemoteUserAddress(servletRequest.getRemoteAddr()) + .setUserAgent(servletRequest.getHeader(USER_AGENT)); + + String timeZoneId = servletRequest.getHeader(PRESTO_TIME_ZONE); + if (timeZoneId != null) { + sessionBuilder.setTimeZoneKey(getTimeZoneKey(timeZoneId)); + } + + String language = servletRequest.getHeader(PRESTO_LANGUAGE); + if (language != null) { + sessionBuilder.setLocale(Locale.forLanguageTag(language)); + } + + // parse session properties + Multimap> sessionPropertiesByCatalog = HashMultimap.create(); + for (String sessionHeader : splitSessionHeader(servletRequest.getHeaders(PRESTO_SESSION))) { + parseSessionHeader(sessionHeader, sessionPropertiesByCatalog); + } + sessionBuilder.setSystemProperties(toMap(sessionPropertiesByCatalog.get(null))); + for (Entry>> entry : sessionPropertiesByCatalog.asMap().entrySet()) { + if (entry.getKey() != null) { + sessionBuilder.setCatalogProperties(entry.getKey(), toMap(entry.getValue())); + } + } + + return sessionBuilder.build(); + } + + private static List splitSessionHeader(Enumeration headers) + { + Splitter splitter = Splitter.on(',').trimResults().omitEmptyStrings(); + return Collections.list(headers).stream() + .map(splitter::splitToList) + .flatMap(Collection::stream) + .collect(toImmutableList()); + } + + private static void parseSessionHeader(String header, Multimap> sessionPropertiesByCatalog) + { + List nameValue = Splitter.on('=').limit(2).splitToList(header); + assertRequest(nameValue.size() == 2, "Invalid %s header", PRESTO_SESSION); + + String catalog; + String name; + List nameParts = Splitter.on('.').splitToList(nameValue.get(0)); + if (nameParts.size() == 1) { + catalog = null; + name = nameParts.get(0); + } + else if (nameParts.size() == 2) { + catalog = nameParts.get(0); + name = nameParts.get(1); + } + else { + throw badRequest(format("Invalid %s header", PRESTO_SESSION)); + } + assertRequest(catalog == null || !catalog.isEmpty(), "Invalid %s header", PRESTO_SESSION); + assertRequest(!name.isEmpty(), "Invalid %s header", PRESTO_SESSION); + + String value = nameValue.get(1); + + sessionPropertiesByCatalog.put(catalog, Maps.immutableEntry(name, value)); + } + + private static Map toMap(Iterable> entries) + { + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (Entry entry : entries) { + builder.put(entry); + } + return builder.build(); + } + + private static String getRequiredHeader(HttpServletRequest servletRequest, String name, String description) + { + String value = servletRequest.getHeader(name); + assertRequest(!isNullOrEmpty(value), description + " (%s) is empty", name); + return value; + } + + public static void assertRequest(boolean expression, String format, Object... args) + { + if (!expression) { + throw badRequest(format(format, args)); + } + } + + private static TimeZoneKey getTimeZoneKey(String timeZoneId) + { + try { + return TimeZoneKey.getTimeZoneKey(timeZoneId); + } + catch (TimeZoneNotSupportedException e) { + throw badRequest(e.getMessage()); + } + } + + private static WebApplicationException badRequest(String message) + { + throw new WebApplicationException(Response + .status(Status.BAD_REQUEST) + .type(MediaType.TEXT_PLAIN) + .entity(message) + .build()); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/server/ServerConfig.java b/presto-main/src/main/java/com/facebook/presto/server/ServerConfig.java new file mode 100644 index 00000000..84b34c77 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/server/ServerConfig.java @@ -0,0 +1,74 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.server; + +import io.airlift.configuration.Config; + +public class ServerConfig +{ + private boolean coordinator = true; + private String prestoVersion; + private String dataSources; + private boolean includeExceptionInResponse = true; + + public boolean isCoordinator() + { + return coordinator; + } + + @Config("coordinator") + public ServerConfig setCoordinator(boolean coordinator) + { + this.coordinator = coordinator; + return this; + } + + public String getPrestoVersion() + { + return prestoVersion; + } + + @Config("presto.version") + public ServerConfig setPrestoVersion(String prestoVersion) + { + this.prestoVersion = prestoVersion; + return this; + } + + @Deprecated + public String getDataSources() + { + return dataSources; + } + + @Deprecated + @Config("datasources") + public ServerConfig setDataSources(String dataSources) + { + this.dataSources = dataSources; + return this; + } + + public boolean isIncludeExceptionInResponse() + { + return includeExceptionInResponse; + } + + @Config("http.include-exception-in-response") + public ServerConfig setIncludeExceptionInResponse(boolean includeExceptionInResponse) + { + this.includeExceptionInResponse = includeExceptionInResponse; + return this; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/server/ServerMainModule.java b/presto-main/src/main/java/com/facebook/presto/server/ServerMainModule.java new file mode 100644 index 00000000..460c120a --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/server/ServerMainModule.java @@ -0,0 +1,363 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.server; + +import com.facebook.presto.PagesIndexPageSorter; +import com.facebook.presto.block.BlockEncodingManager; +import com.facebook.presto.client.QueryResults; +import com.facebook.presto.connector.ConnectorManager; +import com.facebook.presto.connector.informationSchema.InformationSchemaModule; +import com.facebook.presto.connector.jmx.JmxConnectorFactory; +import com.facebook.presto.connector.system.SystemTablesModule; +import com.facebook.presto.event.query.QueryCompletionEvent; +import com.facebook.presto.event.query.QueryCreatedEvent; +import com.facebook.presto.event.query.QueryMonitor; +import com.facebook.presto.event.query.SplitCompletionEvent; +import com.facebook.presto.execution.LocationFactory; +import com.facebook.presto.execution.QueryInfo; +import com.facebook.presto.execution.RemoteTaskFactory; +import com.facebook.presto.execution.SqlTaskManager; +import com.facebook.presto.execution.TaskExecutor; +import com.facebook.presto.execution.TaskInfo; +import com.facebook.presto.execution.TaskManager; +import com.facebook.presto.execution.TaskManagerConfig; +import com.facebook.presto.failureDetector.FailureDetector; +import com.facebook.presto.failureDetector.FailureDetectorModule; +import com.facebook.presto.index.IndexManager; +import com.facebook.presto.memory.ClusterMemoryManager; +import com.facebook.presto.memory.ForMemoryManager; +import com.facebook.presto.memory.LocalMemoryManager; +import com.facebook.presto.memory.LocalMemoryManagerExporter; +import com.facebook.presto.memory.MemoryInfo; +import com.facebook.presto.memory.MemoryManagerConfig; +import com.facebook.presto.memory.MemoryPoolAssignmentsRequest; +import com.facebook.presto.memory.MemoryResource; +import com.facebook.presto.memory.ReservedSystemMemoryConfig; +import com.facebook.presto.metadata.CatalogManager; +import com.facebook.presto.metadata.CatalogManagerConfig; +import com.facebook.presto.metadata.HandleJsonModule; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.metadata.MetadataManager; +import com.facebook.presto.metadata.NodeVersion; +import com.facebook.presto.operator.ExchangeClient; +import com.facebook.presto.operator.ExchangeClientConfig; +import com.facebook.presto.operator.ExchangeClientFactory; +import com.facebook.presto.operator.ForExchange; +import com.facebook.presto.operator.ForScheduler; +import com.facebook.presto.operator.index.IndexJoinLookupStats; +import com.facebook.presto.spi.ConnectorFactory; +import com.facebook.presto.spi.ConnectorPageSinkProvider; +import com.facebook.presto.spi.ConnectorPageSourceProvider; +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.PageSorter; +import com.facebook.presto.spi.block.BlockEncodingFactory; +import com.facebook.presto.spi.block.BlockEncodingSerde; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.split.PageSinkManager; +import com.facebook.presto.split.PageSinkProvider; +import com.facebook.presto.split.PageSourceManager; +import com.facebook.presto.split.PageSourceProvider; +import com.facebook.presto.sql.Serialization.ExpressionDeserializer; +import com.facebook.presto.sql.Serialization.ExpressionSerializer; +import com.facebook.presto.sql.Serialization.FunctionCallDeserializer; +import com.facebook.presto.sql.gen.ExpressionCompiler; +import com.facebook.presto.sql.parser.SqlParser; +import com.facebook.presto.sql.parser.SqlParserOptions; +import com.facebook.presto.sql.planner.CompilerConfig; +import com.facebook.presto.sql.planner.LocalExecutionPlanner; +import com.facebook.presto.sql.planner.PlanOptimizersFactory; +import com.facebook.presto.sql.planner.optimizations.PlanOptimizer; +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.FunctionCall; +import com.facebook.presto.type.TypeDeserializer; +import com.facebook.presto.type.TypeRegistry; +import com.google.common.collect.ImmutableSet; +import com.google.inject.Binder; +import com.google.inject.Provides; +import com.google.inject.Scopes; +import com.google.inject.TypeLiteral; +import com.google.inject.multibindings.MapBinder; +import io.airlift.configuration.AbstractConfigurationAwareModule; +import io.airlift.discovery.client.ServiceDescriptor; +import io.airlift.slice.Slice; +import io.airlift.units.Duration; + +import javax.inject.Singleton; + +import java.util.List; +import java.util.Set; +import java.util.concurrent.ScheduledExecutorService; +import java.util.function.Supplier; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Strings.nullToEmpty; +import static com.google.inject.multibindings.MapBinder.newMapBinder; +import static com.google.inject.multibindings.Multibinder.newSetBinder; +import static io.airlift.concurrent.Threads.daemonThreadsNamed; +import static io.airlift.configuration.ConfigBinder.configBinder; +import static io.airlift.discovery.client.DiscoveryBinder.discoveryBinder; +import static io.airlift.event.client.EventBinder.eventBinder; +import static io.airlift.http.client.HttpClientBinder.httpClientBinder; +import static io.airlift.jaxrs.JaxrsBinder.jaxrsBinder; +import static io.airlift.json.JsonBinder.jsonBinder; +import static io.airlift.json.JsonCodecBinder.jsonCodecBinder; +import static java.util.concurrent.Executors.newScheduledThreadPool; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.weakref.jmx.guice.ExportBinder.newExporter; + +public class ServerMainModule + extends AbstractConfigurationAwareModule +{ + private final SqlParserOptions sqlParserOptions; + + public ServerMainModule(SqlParserOptions sqlParserOptions) + { + this.sqlParserOptions = checkNotNull(sqlParserOptions, "sqlParserOptions is null"); + } + + @Override + protected void setup(Binder binder) + { + ServerConfig serverConfig = buildConfigObject(ServerConfig.class); + + // TODO: this should only be installed if this is a coordinator + binder.install(new CoordinatorModule()); + + if (serverConfig.isCoordinator()) { + discoveryBinder(binder).bindHttpAnnouncement("presto-coordinator"); + } + + binder.bind(SqlParser.class).in(Scopes.SINGLETON); + binder.bind(SqlParserOptions.class).toInstance(sqlParserOptions); + + bindFailureDetector(binder, serverConfig.isCoordinator()); + + jaxrsBinder(binder).bind(ThrowableMapper.class); + + // task execution + jaxrsBinder(binder).bind(TaskResource.class); + binder.bind(TaskManager.class).to(SqlTaskManager.class).in(Scopes.SINGLETON); + configBinder(binder).bindConfig(MemoryManagerConfig.class); + configBinder(binder).bindConfig(ReservedSystemMemoryConfig.class); + newExporter(binder).export(ClusterMemoryManager.class).withGeneratedName(); + binder.bind(ClusterMemoryManager.class).in(Scopes.SINGLETON); + binder.bind(LocalMemoryManager.class).in(Scopes.SINGLETON); + binder.bind(LocalMemoryManagerExporter.class).in(Scopes.SINGLETON); + newExporter(binder).export(TaskManager.class).withGeneratedName(); + binder.bind(TaskExecutor.class).in(Scopes.SINGLETON); + newExporter(binder).export(TaskExecutor.class).withGeneratedName(); + binder.bind(LocalExecutionPlanner.class).in(Scopes.SINGLETON); + configBinder(binder).bindConfig(CompilerConfig.class); + binder.bind(ExpressionCompiler.class).in(Scopes.SINGLETON); + newExporter(binder).export(ExpressionCompiler.class).withGeneratedName(); + configBinder(binder).bindConfig(TaskManagerConfig.class); + binder.bind(IndexJoinLookupStats.class).in(Scopes.SINGLETON); + newExporter(binder).export(IndexJoinLookupStats.class).withGeneratedName(); + binder.bind(AsyncHttpExecutionMBean.class).in(Scopes.SINGLETON); + newExporter(binder).export(AsyncHttpExecutionMBean.class).withGeneratedName(); + + jsonCodecBinder(binder).bindJsonCodec(TaskInfo.class); + jaxrsBinder(binder).bind(PagesResponseWriter.class); + + // exchange client + binder.bind(new TypeLiteral>() {}).to(ExchangeClientFactory.class).in(Scopes.SINGLETON); + httpClientBinder(binder).bindHttpClient("exchange", ForExchange.class) + .withTracing() + .withConfigDefaults(config -> { + config.setIdleTimeout(new Duration(2, SECONDS)); + config.setRequestTimeout(new Duration(10, SECONDS)); + config.setMaxConnectionsPerServer(250); + }); + + configBinder(binder).bindConfig(ExchangeClientConfig.class); + binder.bind(ExchangeExecutionMBean.class).in(Scopes.SINGLETON); + newExporter(binder).export(ExchangeExecutionMBean.class).withGeneratedName(); + + // execution + binder.bind(LocationFactory.class).to(HttpLocationFactory.class).in(Scopes.SINGLETON); + binder.bind(RemoteTaskFactory.class).to(HttpRemoteTaskFactory.class).in(Scopes.SINGLETON); + newExporter(binder).export(RemoteTaskFactory.class).withGeneratedName(); + httpClientBinder(binder).bindHttpClient("scheduler", ForScheduler.class) + .withTracing() + .withConfigDefaults(config -> { + config.setIdleTimeout(new Duration(2, SECONDS)); + config.setRequestTimeout(new Duration(10, SECONDS)); + config.setMaxConnectionsPerServer(250); + }); + + // memory manager + jaxrsBinder(binder).bind(MemoryResource.class); + httpClientBinder(binder).bindHttpClient("memoryManager", ForMemoryManager.class) + .withTracing() + .withConfigDefaults(config -> { + config.setIdleTimeout(new Duration(2, SECONDS)); + config.setRequestTimeout(new Duration(10, SECONDS)); + }); + + jsonCodecBinder(binder).bindJsonCodec(MemoryInfo.class); + jsonCodecBinder(binder).bindJsonCodec(MemoryPoolAssignmentsRequest.class); + + // data stream provider + binder.bind(PageSourceManager.class).in(Scopes.SINGLETON); + binder.bind(PageSourceProvider.class).to(PageSourceManager.class).in(Scopes.SINGLETON); + newSetBinder(binder, ConnectorPageSourceProvider.class); + + // page sink provider + binder.bind(PageSinkManager.class).in(Scopes.SINGLETON); + binder.bind(PageSinkProvider.class).to(PageSinkManager.class).in(Scopes.SINGLETON); + newSetBinder(binder, ConnectorPageSinkProvider.class); + + // metadata + binder.bind(CatalogManager.class).in(Scopes.SINGLETON); + configBinder(binder).bindConfig(CatalogManagerConfig.class); + binder.bind(MetadataManager.class).in(Scopes.SINGLETON); + binder.bind(Metadata.class).to(MetadataManager.class).in(Scopes.SINGLETON); + + // type + binder.bind(TypeRegistry.class).in(Scopes.SINGLETON); + binder.bind(TypeManager.class).to(TypeRegistry.class).in(Scopes.SINGLETON); + jsonBinder(binder).addDeserializerBinding(Type.class).to(TypeDeserializer.class); + newSetBinder(binder, Type.class); + + // index manager + binder.bind(IndexManager.class).in(Scopes.SINGLETON); + + // handle resolver + binder.install(new HandleJsonModule()); + + // connector + binder.bind(ConnectorManager.class).in(Scopes.SINGLETON); + MapBinder connectorFactoryBinder = newMapBinder(binder, String.class, ConnectorFactory.class); + + // jmx connector + connectorFactoryBinder.addBinding("jmx").to(JmxConnectorFactory.class); + + // information schema + binder.install(new InformationSchemaModule()); + + // system tables + binder.install(new SystemTablesModule()); + + // splits + jsonCodecBinder(binder).bindJsonCodec(TaskUpdateRequest.class); + jsonCodecBinder(binder).bindJsonCodec(ConnectorSplit.class); + jsonBinder(binder).addSerializerBinding(Slice.class).to(SliceSerializer.class); + jsonBinder(binder).addDeserializerBinding(Slice.class).to(SliceDeserializer.class); + jsonBinder(binder).addSerializerBinding(Expression.class).to(ExpressionSerializer.class); + jsonBinder(binder).addDeserializerBinding(Expression.class).to(ExpressionDeserializer.class); + jsonBinder(binder).addDeserializerBinding(FunctionCall.class).to(FunctionCallDeserializer.class); + + // query monitor + binder.bind(QueryMonitor.class).in(Scopes.SINGLETON); + eventBinder(binder).bindEventClient(QueryCreatedEvent.class); + eventBinder(binder).bindEventClient(QueryCompletionEvent.class); + eventBinder(binder).bindEventClient(SplitCompletionEvent.class); + + // Determine the NodeVersion + String prestoVersion = serverConfig.getPrestoVersion(); + if (prestoVersion == null) { + prestoVersion = detectPrestoVersion(); + } + checkState(prestoVersion != null, "presto.version must be provided when it cannot be automatically determined"); + + NodeVersion nodeVersion = new NodeVersion(prestoVersion); + binder.bind(NodeVersion.class).toInstance(nodeVersion); + + // presto announcement + discoveryBinder(binder).bindHttpAnnouncement("presto") + .addProperty("node_version", nodeVersion.toString()) + .addProperty("coordinator", String.valueOf(serverConfig.isCoordinator())) + .addProperty("datasources", nullToEmpty(serverConfig.getDataSources())); + + // statement resource + jsonCodecBinder(binder).bindJsonCodec(QueryInfo.class); + jsonCodecBinder(binder).bindJsonCodec(TaskInfo.class); + jsonCodecBinder(binder).bindJsonCodec(QueryResults.class); + jaxrsBinder(binder).bind(StatementResource.class); + + // execute resource + jaxrsBinder(binder).bind(ExecuteResource.class); + httpClientBinder(binder).bindHttpClient("execute", ForExecute.class) + .withTracing() + .withConfigDefaults(config -> { + config.setIdleTimeout(new Duration(2, SECONDS)); + config.setRequestTimeout(new Duration(10, SECONDS)); + }); + + // plugin manager + binder.bind(PluginManager.class).in(Scopes.SINGLETON); + configBinder(binder).bindConfig(PluginManagerConfig.class); + + // optimizers + binder.bind(new TypeLiteral>() {}).toProvider(PlanOptimizersFactory.class).in(Scopes.SINGLETON); + + // block encodings + binder.bind(BlockEncodingManager.class).in(Scopes.SINGLETON); + binder.bind(BlockEncodingSerde.class).to(BlockEncodingManager.class).in(Scopes.SINGLETON); + newSetBinder(binder, new TypeLiteral>() {}); + + // thread visualizer + jaxrsBinder(binder).bind(ThreadResource.class); + + // thread execution visualizer + jaxrsBinder(binder).bind(QueryExecutionResource.class); + + // PageSorter + binder.bind(PageSorter.class).to(PagesIndexPageSorter.class).in(Scopes.SINGLETON); + } + + @Provides + @Singleton + @ForExchange + public ScheduledExecutorService createExchangeExecutor(ExchangeClientConfig config) + { + return newScheduledThreadPool(config.getClientThreads(), daemonThreadsNamed("exchange-client-%s")); + } + + @Provides + @Singleton + @ForAsyncHttpResponse + public static ScheduledExecutorService createAsyncHttpResponseExecutor(TaskManagerConfig config) + { + return newScheduledThreadPool(config.getHttpNotificationThreads(), daemonThreadsNamed("async-http-response-%s")); + } + + private static String detectPrestoVersion() + { + String title = PrestoServer.class.getPackage().getImplementationTitle(); + String version = PrestoServer.class.getPackage().getImplementationVersion(); + return ((title == null) || (version == null)) ? null : (title + ":" + version); + } + + private static void bindFailureDetector(Binder binder, boolean coordinator) + { + // TODO: this is a hack until the coordinator module works correctly + if (coordinator) { + binder.install(new FailureDetectorModule()); + jaxrsBinder(binder).bind(NodeResource.class); + } + else { + binder.bind(FailureDetector.class).toInstance(new FailureDetector() + { + @Override + public Set getFailed() + { + return ImmutableSet.of(); + } + }); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/server/SliceDeserializer.java b/presto-main/src/main/java/com/facebook/presto/server/SliceDeserializer.java new file mode 100644 index 00000000..1dc99d51 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/server/SliceDeserializer.java @@ -0,0 +1,34 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.server; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import io.airlift.slice.Slice; + +import java.io.IOException; + +import static io.airlift.slice.Slices.utf8Slice; + +public class SliceDeserializer + extends JsonDeserializer +{ + @Override + public Slice deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) + throws IOException + { + return utf8Slice(jsonParser.readValueAs(String.class)); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/server/SliceSerializer.java b/presto-main/src/main/java/com/facebook/presto/server/SliceSerializer.java new file mode 100644 index 00000000..b9b6ba74 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/server/SliceSerializer.java @@ -0,0 +1,32 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.server; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import io.airlift.slice.Slice; + +import java.io.IOException; + +public class SliceSerializer + extends JsonSerializer +{ + @Override + public void serialize(Slice slice, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) + throws IOException + { + jsonGenerator.writeString(slice.toStringUtf8()); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/server/StageResource.java b/presto-main/src/main/java/com/facebook/presto/server/StageResource.java new file mode 100644 index 00000000..e25d3d11 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/server/StageResource.java @@ -0,0 +1,44 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.server; + +import com.facebook.presto.execution.QueryManager; +import com.facebook.presto.execution.StageId; + +import javax.inject.Inject; +import javax.ws.rs.DELETE; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Path("/v1/stage") +public class StageResource +{ + private final QueryManager queryManager; + + @Inject + public StageResource(QueryManager queryManager) + { + this.queryManager = checkNotNull(queryManager, "queryManager is null"); + } + + @DELETE + @Path("{stageId}") + public void cancelStage(@PathParam("stageId") StageId stageId) + { + checkNotNull(stageId, "stageId is null"); + queryManager.cancelStage(stageId); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/server/StatementResource.java b/presto-main/src/main/java/com/facebook/presto/server/StatementResource.java new file mode 100644 index 00000000..c1847404 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/server/StatementResource.java @@ -0,0 +1,744 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.server; + +import com.facebook.presto.Session; +import com.facebook.presto.client.ClientTypeSignature; +import com.facebook.presto.client.Column; +import com.facebook.presto.client.FailureInfo; +import com.facebook.presto.client.QueryError; +import com.facebook.presto.client.QueryResults; +import com.facebook.presto.client.StageStats; +import com.facebook.presto.client.StatementStats; +import com.facebook.presto.execution.BufferInfo; +import com.facebook.presto.execution.QueryId; +import com.facebook.presto.execution.QueryInfo; +import com.facebook.presto.execution.QueryManager; +import com.facebook.presto.execution.QueryState; +import com.facebook.presto.execution.QueryStats; +import com.facebook.presto.execution.SharedBufferInfo; +import com.facebook.presto.execution.StageInfo; +import com.facebook.presto.execution.StageState; +import com.facebook.presto.execution.TaskId; +import com.facebook.presto.execution.TaskInfo; +import com.facebook.presto.operator.ExchangeClient; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.ErrorCode; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeSignature; +import com.google.common.base.Preconditions; +import com.google.common.collect.AbstractIterator; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.common.collect.Ordering; +import io.airlift.log.Logger; +import io.airlift.units.DataSize; +import io.airlift.units.Duration; + +import javax.annotation.PreDestroy; +import javax.annotation.concurrent.GuardedBy; +import javax.annotation.concurrent.ThreadSafe; +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.ResponseBuilder; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.core.UriInfo; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Supplier; + +import static com.facebook.presto.client.PrestoHeaders.PRESTO_CLEAR_SESSION; +import static com.facebook.presto.client.PrestoHeaders.PRESTO_SET_SESSION; +import static com.facebook.presto.server.ResourceUtil.assertRequest; +import static com.facebook.presto.server.ResourceUtil.createSessionForRequest; +import static com.facebook.presto.spi.StandardErrorCode.INTERNAL_ERROR; +import static com.facebook.presto.spi.StandardErrorCode.toErrorType; +import static com.facebook.presto.util.Failures.toFailure; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Strings.isNullOrEmpty; +import static io.airlift.concurrent.Threads.threadsNamed; +import static io.airlift.http.client.HttpUriBuilder.uriBuilderFrom; +import static io.airlift.units.DataSize.Unit.MEGABYTE; +import static java.lang.String.format; +import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; + +@Path("/v1/statement") +public class StatementResource +{ + private static final Logger log = Logger.get(StatementResource.class); + + private static final Duration MAX_WAIT_TIME = new Duration(1, SECONDS); + private static final Ordering> WAIT_ORDERING = Ordering.natural().nullsLast(); + private static final long DESIRED_RESULT_BYTES = new DataSize(1, MEGABYTE).toBytes(); + + private final QueryManager queryManager; + private final Supplier exchangeClientSupplier; + + private final ConcurrentMap queries = new ConcurrentHashMap<>(); + private final ScheduledExecutorService queryPurger = newSingleThreadScheduledExecutor(threadsNamed("query-purger")); + + @Inject + public StatementResource(QueryManager queryManager, Supplier exchangeClientSupplier) + { + this.queryManager = checkNotNull(queryManager, "queryManager is null"); + this.exchangeClientSupplier = checkNotNull(exchangeClientSupplier, "exchangeClientSupplier is null"); + + queryPurger.scheduleWithFixedDelay(new PurgeQueriesRunnable(queries, queryManager), 200, 200, MILLISECONDS); + } + + @PreDestroy + public void stop() + { + queryPurger.shutdownNow(); + } + + @POST + @Produces(MediaType.APPLICATION_JSON) + public Response createQuery( + String statement, + @Context HttpServletRequest servletRequest, + @Context UriInfo uriInfo) + throws InterruptedException + { + assertRequest(!isNullOrEmpty(statement), "SQL statement is empty"); + + Session session = createSessionForRequest(servletRequest); + + ExchangeClient exchangeClient = exchangeClientSupplier.get(); + Query query = new Query(session, statement, queryManager, exchangeClient); + queries.put(query.getQueryId(), query); + + return getQueryResults(query, Optional.empty(), uriInfo, new Duration(1, MILLISECONDS)); + } + + @GET + @Path("{queryId}/{token}") + @Produces(MediaType.APPLICATION_JSON) + public Response getQueryResults( + @PathParam("queryId") QueryId queryId, + @PathParam("token") long token, + @QueryParam("maxWait") Duration maxWait, + @Context UriInfo uriInfo) + throws InterruptedException + { + Query query = queries.get(queryId); + if (query == null) { + return Response.status(Status.NOT_FOUND).build(); + } + + Duration wait = WAIT_ORDERING.min(MAX_WAIT_TIME, maxWait); + return getQueryResults(query, Optional.of(token), uriInfo, wait); + } + + private static Response getQueryResults(Query query, Optional token, UriInfo uriInfo, Duration wait) + throws InterruptedException + { + QueryResults queryResults; + if (token.isPresent()) { + queryResults = query.getResults(token.get(), uriInfo, wait); + } + else { + queryResults = query.getNextResults(uriInfo, wait); + } + + ResponseBuilder response = Response.ok(queryResults); + + // add set session properties + query.getSetSessionProperties().entrySet().stream() + .forEach(entry -> response.header(PRESTO_SET_SESSION, entry.getKey() + '=' + entry.getValue())); + + // add clear session properties + query.getResetSessionProperties().stream() + .forEach(name -> response.header(PRESTO_CLEAR_SESSION, name)); + + return response.build(); + } + + @DELETE + @Path("{queryId}/{token}") + @Produces(MediaType.APPLICATION_JSON) + public Response cancelQuery(@PathParam("queryId") QueryId queryId, + @PathParam("token") long token) + { + Query query = queries.get(queryId); + if (query == null) { + return Response.status(Status.NOT_FOUND).build(); + } + query.cancel(); + return Response.noContent().build(); + } + + @ThreadSafe + public static class Query + { + private final QueryManager queryManager; + private final QueryId queryId; + private final ExchangeClient exchangeClient; + + private final AtomicLong resultId = new AtomicLong(); + private final Session session; + + @GuardedBy("this") + private QueryResults lastResult; + + @GuardedBy("this") + private String lastResultPath; + + @GuardedBy("this") + private List columns; + + @GuardedBy("this") + private Map setSessionProperties; + + @GuardedBy("this") + private Set resetSessionProperties; + + @GuardedBy("this") + private Long updateCount; + + public Query(Session session, + String query, + QueryManager queryManager, + ExchangeClient exchangeClient) + { + checkNotNull(session, "session is null"); + checkNotNull(query, "query is null"); + checkNotNull(queryManager, "queryManager is null"); + checkNotNull(exchangeClient, "exchangeClient is null"); + + this.session = session; + this.queryManager = queryManager; + + QueryInfo queryInfo = queryManager.createQuery(session, query); + queryId = queryInfo.getQueryId(); + this.exchangeClient = exchangeClient; + } + + public void cancel() + { + queryManager.cancelQuery(queryId); + dispose(); + } + + public void dispose() + { + exchangeClient.close(); + } + + public QueryId getQueryId() + { + return queryId; + } + + public synchronized Map getSetSessionProperties() + { + return setSessionProperties; + } + + public synchronized Set getResetSessionProperties() + { + return resetSessionProperties; + } + + public synchronized QueryResults getResults(long token, UriInfo uriInfo, Duration maxWaitTime) + throws InterruptedException + { + // is the a repeated request for the last results? + String requestedPath = uriInfo.getAbsolutePath().getPath(); + if (lastResultPath != null && requestedPath.equals(lastResultPath)) { + // tell query manager we are still interested in the query + queryManager.getQueryInfo(queryId); + queryManager.recordHeartbeat(queryId); + return lastResult; + } + + if (token < resultId.get()) { + throw new WebApplicationException(Status.GONE); + } + + // if this is not a request for the next results, return not found + if (lastResult.getNextUri() == null || !requestedPath.equals(lastResult.getNextUri().getPath())) { + // unknown token + throw new WebApplicationException(Status.NOT_FOUND); + } + + return getNextResults(uriInfo, maxWaitTime); + } + + public synchronized QueryResults getNextResults(UriInfo uriInfo, Duration maxWaitTime) + throws InterruptedException + { + Iterable> data = getData(maxWaitTime); + + // get the query info before returning + // force update if query manager is closed + QueryInfo queryInfo = queryManager.getQueryInfo(queryId); + queryManager.recordHeartbeat(queryId); + + // if we have received all of the output data and the query is not marked as done, wait for the query to finish + if (exchangeClient.isClosed() && !queryInfo.getState().isDone()) { + queryManager.waitForStateChange(queryId, queryInfo.getState(), maxWaitTime); + queryInfo = queryManager.getQueryInfo(queryId); + } + + // TODO: figure out a better way to do this + // grab the update count for non-queries + if ((data != null) && (queryInfo.getUpdateType() != null) && (updateCount == null) && + (columns.size() == 1) && (columns.get(0).getType().equals(StandardTypes.BIGINT))) { + Iterator> iterator = data.iterator(); + if (iterator.hasNext()) { + updateCount = ((Number) iterator.next().get(0)).longValue(); + } + } + + // close exchange client if the query has failed + if (queryInfo.getState().isDone()) { + if (queryInfo.getState() != QueryState.FINISHED) { + exchangeClient.close(); + } + else if (queryInfo.getOutputStage() == null) { + // For simple executions (e.g. drop table), there will never be an output stage, + // so close the exchange as soon as the query is done. + exchangeClient.close(); + + // Return a single value for clients that require a result. + columns = ImmutableList.of(new Column("result", "boolean", new ClientTypeSignature(StandardTypes.BOOLEAN, ImmutableList.of(), ImmutableList.of()))); + data = ImmutableSet.>of(ImmutableList.of(true)); + } + } + + // only return a next if the query is not done or there is more data to send (due to buffering) + URI nextResultsUri = null; + if ((!queryInfo.getState().isDone()) || (!exchangeClient.isClosed())) { + nextResultsUri = createNextResultsUri(uriInfo); + } + + // update setSessionProperties + setSessionProperties = queryInfo.getSetSessionProperties(); + resetSessionProperties = queryInfo.getResetSessionProperties(); + + // first time through, self is null + QueryResults queryResults = new QueryResults( + queryId.toString(), + uriInfo.getRequestUriBuilder().replaceQuery("").replacePath(queryInfo.getSelf().getPath()).build(), + findCancelableLeafStage(queryInfo), + nextResultsUri, + columns, + data, + toStatementStats(queryInfo), + toQueryError(queryInfo), + queryInfo.getUpdateType(), + updateCount); + + // cache the last results + if (lastResult != null && lastResult.getNextUri() != null) { + lastResultPath = lastResult.getNextUri().getPath(); + } + else { + lastResultPath = null; + } + lastResult = queryResults; + return queryResults; + } + + private synchronized Iterable> getData(Duration maxWait) + throws InterruptedException + { + // wait for query to start + QueryInfo queryInfo = queryManager.getQueryInfo(queryId); + while (maxWait.toMillis() > 1 && !isQueryStarted(queryInfo)) { + queryManager.recordHeartbeat(queryId); + maxWait = queryManager.waitForStateChange(queryId, queryInfo.getState(), maxWait); + queryInfo = queryManager.getQueryInfo(queryId); + } + + // if query did not finish starting or does not have output, just return + if (!isQueryStarted(queryInfo) || queryInfo.getOutputStage() == null) { + return null; + } + + if (columns == null) { + columns = createColumnsList(queryInfo); + } + + List types = queryInfo.getOutputStage().getTypes(); + + updateExchangeClient(queryInfo.getOutputStage()); + + ImmutableList.Builder pages = ImmutableList.builder(); + // wait up to max wait for data to arrive; then try to return at least DESIRED_RESULT_BYTES + long bytes = 0; + while (bytes < DESIRED_RESULT_BYTES) { + Page page = exchangeClient.getNextPage(maxWait); + if (page == null) { + break; + } + bytes += page.getSizeInBytes(); + pages.add(new RowIterable(session.toConnectorSession(), types, page)); + + // only wait on first call + maxWait = new Duration(0, MILLISECONDS); + } + + if (bytes == 0) { + return null; + } + + return Iterables.concat(pages.build()); + } + + private static boolean isQueryStarted(QueryInfo queryInfo) + { + QueryState state = queryInfo.getState(); + return state != QueryState.QUEUED && queryInfo.getState() != QueryState.PLANNING && queryInfo.getState() != QueryState.STARTING; + } + + private synchronized void updateExchangeClient(StageInfo outputStage) + { + // add any additional output locations + if (!outputStage.getState().isDone()) { + for (TaskInfo taskInfo : outputStage.getTasks()) { + SharedBufferInfo outputBuffers = taskInfo.getOutputBuffers(); + List buffers = outputBuffers.getBuffers(); + if (buffers.isEmpty() || outputBuffers.getState().canAddBuffers()) { + // output buffer has not been created yet + continue; + } + Preconditions.checkState(buffers.size() == 1, + "Expected a single output buffer for task %s, but found %s", + taskInfo.getTaskId(), + buffers); + + TaskId bufferId = Iterables.getOnlyElement(buffers).getBufferId(); + URI uri = uriBuilderFrom(taskInfo.getSelf()).appendPath("results").appendPath(bufferId.toString()).build(); + exchangeClient.addLocation(uri); + } + } + + if (allOutputBuffersCreated(outputStage)) { + exchangeClient.noMoreLocations(); + } + } + + private static boolean allOutputBuffersCreated(StageInfo outputStage) + { + StageState stageState = outputStage.getState(); + + // if the stage is already done, then there will be no more buffers + if (stageState.isDone()) { + return true; + } + + // has the stage finished scheduling? + if (stageState == StageState.PLANNED || stageState == StageState.SCHEDULING) { + return false; + } + + // have all tasks finished adding buffers + return outputStage.getTasks().stream() + .allMatch(taskInfo -> !taskInfo.getOutputBuffers().getState().canAddBuffers()); + } + + private synchronized URI createNextResultsUri(UriInfo uriInfo) + { + return uriInfo.getBaseUriBuilder().replacePath("/v1/statement").path(queryId.toString()).path(String.valueOf(resultId.incrementAndGet())).replaceQuery("").build(); + } + + private static List createColumnsList(QueryInfo queryInfo) + { + checkNotNull(queryInfo, "queryInfo is null"); + StageInfo outputStage = queryInfo.getOutputStage(); + checkNotNull(outputStage, "outputStage is null"); + + List names = queryInfo.getFieldNames(); + List types = outputStage.getTypes(); + + checkArgument(names.size() == types.size(), "names and types size mismatch"); + + ImmutableList.Builder list = ImmutableList.builder(); + for (int i = 0; i < names.size(); i++) { + String name = names.get(i); + TypeSignature typeSignature = types.get(i).getTypeSignature(); + String type = typeSignature.toString(); + list.add(new Column(name, type, new ClientTypeSignature(typeSignature))); + } + return list.build(); + } + + private static StatementStats toStatementStats(QueryInfo queryInfo) + { + QueryStats queryStats = queryInfo.getQueryStats(); + + return StatementStats.builder() + .setState(queryInfo.getState().toString()) + .setScheduled(queryInfo.isScheduled()) + .setNodes(globalUniqueNodes(queryInfo.getOutputStage()).size()) + .setTotalSplits(queryStats.getTotalDrivers()) + .setQueuedSplits(queryStats.getQueuedDrivers()) + .setRunningSplits(queryStats.getRunningDrivers()) + .setCompletedSplits(queryStats.getCompletedDrivers()) + .setUserTimeMillis(queryStats.getTotalUserTime().toMillis()) + .setCpuTimeMillis(queryStats.getTotalCpuTime().toMillis()) + .setWallTimeMillis(queryStats.getTotalScheduledTime().toMillis()) + .setProcessedRows(queryStats.getRawInputPositions()) + .setProcessedBytes(queryStats.getRawInputDataSize().toBytes()) + .setRootStage(toStageStats(queryInfo.getOutputStage())) + .build(); + } + + private static StageStats toStageStats(StageInfo stageInfo) + { + if (stageInfo == null) { + return null; + } + + com.facebook.presto.execution.StageStats stageStats = stageInfo.getStageStats(); + + ImmutableList.Builder subStages = ImmutableList.builder(); + for (StageInfo subStage : stageInfo.getSubStages()) { + subStages.add(toStageStats(subStage)); + } + + Set uniqueNodes = new HashSet<>(); + for (TaskInfo task : stageInfo.getTasks()) { + // todo add nodeId to TaskInfo + URI uri = task.getSelf(); + uniqueNodes.add(uri.getHost() + ":" + uri.getPort()); + } + + return StageStats.builder() + .setStageId(String.valueOf(stageInfo.getStageId().getId())) + .setState(stageInfo.getState().toString()) + .setDone(stageInfo.getState().isDone()) + .setNodes(uniqueNodes.size()) + .setTotalSplits(stageStats.getTotalDrivers()) + .setQueuedSplits(stageStats.getQueuedDrivers()) + .setRunningSplits(stageStats.getRunningDrivers()) + .setCompletedSplits(stageStats.getCompletedDrivers()) + .setUserTimeMillis(stageStats.getTotalUserTime().toMillis()) + .setCpuTimeMillis(stageStats.getTotalCpuTime().toMillis()) + .setWallTimeMillis(stageStats.getTotalScheduledTime().toMillis()) + .setProcessedRows(stageStats.getRawInputPositions()) + .setProcessedBytes(stageStats.getRawInputDataSize().toBytes()) + .setSubStages(subStages.build()) + .build(); + } + + private static Set globalUniqueNodes(StageInfo stageInfo) + { + if (stageInfo == null) { + return ImmutableSet.of(); + } + ImmutableSet.Builder nodes = ImmutableSet.builder(); + for (TaskInfo task : stageInfo.getTasks()) { + // todo add nodeId to TaskInfo + URI uri = task.getSelf(); + nodes.add(uri.getHost() + ":" + uri.getPort()); + } + + for (StageInfo subStage : stageInfo.getSubStages()) { + nodes.addAll(globalUniqueNodes(subStage)); + } + return nodes.build(); + } + + private static URI findCancelableLeafStage(QueryInfo queryInfo) + { + if (queryInfo.getOutputStage() == null) { + // query is not running yet, cannot cancel leaf stage + return null; + } + + // query is running, find the leaf-most running stage + return findCancelableLeafStage(queryInfo.getOutputStage()); + } + + private static URI findCancelableLeafStage(StageInfo stage) + { + // if this stage is already done, we can't cancel it + if (stage.getState().isDone()) { + return null; + } + + // attempt to find a cancelable sub stage + // check in reverse order since build side of a join will be later in the list + for (StageInfo subStage : Lists.reverse(stage.getSubStages())) { + URI leafStage = findCancelableLeafStage(subStage); + if (leafStage != null) { + return leafStage; + } + } + + // no matching sub stage, so return this stage + return stage.getSelf(); + } + + private static QueryError toQueryError(QueryInfo queryInfo) + { + FailureInfo failure = queryInfo.getFailureInfo(); + if (failure == null) { + QueryState state = queryInfo.getState(); + if ((!state.isDone()) || (state == QueryState.FINISHED)) { + return null; + } + log.warn("Query %s in state %s has no failure info", queryInfo.getQueryId(), state); + failure = toFailure(new RuntimeException(format("Query is %s (reason unknown)", state))).toFailureInfo(); + } + + ErrorCode errorCode; + if (queryInfo.getErrorCode() != null) { + errorCode = queryInfo.getErrorCode(); + } + else { + errorCode = INTERNAL_ERROR.toErrorCode(); + log.warn("Failed query %s has no error code", queryInfo.getQueryId()); + } + return new QueryError( + failure.getMessage(), + null, + errorCode.getCode(), + errorCode.getName(), + toErrorType(errorCode.getCode()).toString(), + failure.getErrorLocation(), + failure); + } + + private static class RowIterable + implements Iterable> + { + private final ConnectorSession session; + private final List types; + private final Page page; + + private RowIterable(ConnectorSession session, List types, Page page) + { + this.session = session; + this.types = ImmutableList.copyOf(checkNotNull(types, "types is null")); + this.page = checkNotNull(page, "page is null"); + } + + @Override + public Iterator> iterator() + { + return new RowIterator(session, types, page); + } + } + + private static class RowIterator + extends AbstractIterator> + { + private final ConnectorSession session; + private final List types; + private final Page page; + private int position = -1; + + private RowIterator(ConnectorSession session, List types, Page page) + { + this.session = session; + this.types = types; + this.page = page; + } + + @Override + protected List computeNext() + { + position++; + if (position >= page.getPositionCount()) { + return endOfData(); + } + + List values = new ArrayList<>(page.getChannelCount()); + for (int channel = 0; channel < page.getChannelCount(); channel++) { + Type type = types.get(channel); + Block block = page.getBlock(channel); + values.add(type.getObjectValue(session, block, position)); + } + return Collections.unmodifiableList(values); + } + } + } + + private static class PurgeQueriesRunnable + implements Runnable + { + private final ConcurrentMap queries; + private final QueryManager queryManager; + + public PurgeQueriesRunnable(ConcurrentMap queries, QueryManager queryManager) + { + this.queries = queries; + this.queryManager = queryManager; + } + + @Override + public void run() + { + try { + // Queries are added to the query manager before being recorded in queryIds set. + // Therefore, we take a snapshot if queryIds before getting the live queries + // from the query manager. Then we remove only the queries in the snapshot and + // not live queries set. If we did this in the other order, a query could be + // registered between fetching the live queries and inspecting the queryIds set. + for (QueryId queryId : ImmutableSet.copyOf(queries.keySet())) { + Query query = queries.get(queryId); + Optional state = queryManager.getQueryState(queryId); + + // free up resources if the query completed + if (!state.isPresent() || state.get() == QueryState.FAILED) { + query.dispose(); + } + + // forget about this query if the query manager is no longer tracking it + if (!state.isPresent()) { + queries.remove(queryId); + } + } + } + catch (Throwable e) { + log.warn(e, "Error removing old queries"); + } + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/server/TaskResource.java b/presto-main/src/main/java/com/facebook/presto/server/TaskResource.java new file mode 100644 index 00000000..dc82873a --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/server/TaskResource.java @@ -0,0 +1,246 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.server; + +import com.facebook.presto.execution.BufferResult; +import com.facebook.presto.execution.TaskId; +import com.facebook.presto.execution.TaskInfo; +import com.facebook.presto.execution.TaskManager; +import com.facebook.presto.execution.TaskState; +import com.facebook.presto.spi.Page; +import com.facebook.presto.util.MoreFutures; +import com.google.common.collect.ImmutableList; +import com.google.common.reflect.TypeToken; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import io.airlift.units.DataSize; +import io.airlift.units.DataSize.Unit; +import io.airlift.units.Duration; + +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.container.AsyncResponse; +import javax.ws.rs.container.Suspended; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.GenericEntity; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.core.UriInfo; + +import java.util.List; +import java.util.concurrent.ScheduledExecutorService; + +import static com.facebook.presto.PrestoMediaTypes.PRESTO_PAGES; +import static com.facebook.presto.client.PrestoHeaders.PRESTO_CURRENT_STATE; +import static com.facebook.presto.client.PrestoHeaders.PRESTO_MAX_WAIT; +import static com.facebook.presto.client.PrestoHeaders.PRESTO_PAGE_NEXT_TOKEN; +import static com.facebook.presto.client.PrestoHeaders.PRESTO_PAGE_TOKEN; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.Iterables.transform; +import static io.airlift.http.server.AsyncResponseHandler.bindAsyncResponse; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; + +/** + * Manages tasks on this worker node + */ +@Path("/v1/task") +public class TaskResource +{ + private static final DataSize DEFAULT_MAX_SIZE = new DataSize(10, Unit.MEGABYTE); + private static final Duration DEFAULT_MAX_WAIT_TIME = new Duration(1, SECONDS); + + private final TaskManager taskManager; + private final ScheduledExecutorService executor; + + @Inject + public TaskResource(TaskManager taskManager, @ForAsyncHttpResponse ScheduledExecutorService executor) + { + this.taskManager = checkNotNull(taskManager, "taskManager is null"); + this.executor = checkNotNull(executor, "executor is null"); + } + + @GET + @Produces(MediaType.APPLICATION_JSON) + public List getAllTaskInfo(@Context UriInfo uriInfo) + { + List allTaskInfo = taskManager.getAllTaskInfo(); + if (shouldSummarize(uriInfo)) { + allTaskInfo = ImmutableList.copyOf(transform(allTaskInfo, TaskInfo::summarize)); + } + return allTaskInfo; + } + + @POST + @Path("{taskId}") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public Response createOrUpdateTask(@PathParam("taskId") TaskId taskId, TaskUpdateRequest taskUpdateRequest, @Context UriInfo uriInfo) + { + checkNotNull(taskUpdateRequest, "taskUpdateRequest is null"); + + TaskInfo taskInfo = taskManager.updateTask(taskUpdateRequest.getSession(), + taskId, + taskUpdateRequest.getFragment(), + taskUpdateRequest.getSources(), + taskUpdateRequest.getOutputIds()); + + if (shouldSummarize(uriInfo)) { + taskInfo = taskInfo.summarize(); + } + + return Response.ok().entity(taskInfo).build(); + } + + @GET + @Path("{taskId}") + @Produces(MediaType.APPLICATION_JSON) + public void getTaskInfo(@PathParam("taskId") final TaskId taskId, + @HeaderParam(PRESTO_CURRENT_STATE) TaskState currentState, + @HeaderParam(PRESTO_MAX_WAIT) Duration maxWait, + @Context UriInfo uriInfo, + @Suspended AsyncResponse asyncResponse) + { + checkNotNull(taskId, "taskId is null"); + + if (currentState == null || maxWait == null) { + TaskInfo taskInfo = taskManager.getTaskInfo(taskId); + if (shouldSummarize(uriInfo)) { + taskInfo = taskInfo.summarize(); + } + asyncResponse.resume(taskInfo); + return; + } + + ListenableFuture futureTaskInfo = MoreFutures.addTimeout( + taskManager.getTaskInfo(taskId, currentState), + () -> taskManager.getTaskInfo(taskId), + maxWait, + executor); + + if (shouldSummarize(uriInfo)) { + futureTaskInfo = Futures.transform(futureTaskInfo, TaskInfo::summarize); + } + + // For hard timeout, add an additional 5 seconds to max wait for thread scheduling contention and GC + Duration timeout = new Duration(maxWait.toMillis() + 5000, MILLISECONDS); + bindAsyncResponse(asyncResponse, futureTaskInfo, executor) + .withTimeout(timeout); + } + + @DELETE + @Path("{taskId}") + @Produces(MediaType.APPLICATION_JSON) + public Response deleteTask(@PathParam("taskId") TaskId taskId, + @QueryParam("abort") @DefaultValue("true") boolean abort, + @Context UriInfo uriInfo) + { + checkNotNull(taskId, "taskId is null"); + + TaskInfo taskInfo; + if (abort) { + taskInfo = taskManager.abortTask(taskId); + } + else { + taskInfo = taskManager.cancelTask(taskId); + } + + if (shouldSummarize(uriInfo)) { + taskInfo = taskInfo.summarize(); + } + return Response.ok(taskInfo).build(); + } + + @GET + @Path("{taskId}/results/{outputId}/{token}") + @Produces(PRESTO_PAGES) + public void getResults(@PathParam("taskId") TaskId taskId, + @PathParam("outputId") TaskId outputId, + @PathParam("token") final long token, + @Suspended AsyncResponse asyncResponse) + throws InterruptedException + { + checkNotNull(taskId, "taskId is null"); + checkNotNull(outputId, "outputId is null"); + + ListenableFuture bufferResultFuture = taskManager.getTaskResults(taskId, outputId, token, DEFAULT_MAX_SIZE); + bufferResultFuture = MoreFutures.addTimeout( + bufferResultFuture, + () -> BufferResult.emptyResults(token, false), + DEFAULT_MAX_WAIT_TIME, + executor); + + ListenableFuture responseFuture = Futures.transform(bufferResultFuture, (BufferResult result) -> { + List pages = result.getPages(); + + GenericEntity entity = null; + Status status; + if (!pages.isEmpty()) { + entity = new GenericEntity<>(pages, new TypeToken>() {}.getType()); + status = Status.OK; + } + else if (result.isBufferClosed()) { + status = Status.GONE; + } + else { + status = Status.NO_CONTENT; + } + + return Response.status(status) + .entity(entity) + .header(PRESTO_PAGE_TOKEN, result.getToken()) + .header(PRESTO_PAGE_NEXT_TOKEN, result.getNextToken()) + .build(); + }); + + // For hard timeout, add an additional 5 seconds to max wait for thread scheduling contention and GC + Duration timeout = new Duration(DEFAULT_MAX_WAIT_TIME.toMillis() + 5000, MILLISECONDS); + bindAsyncResponse(asyncResponse, responseFuture, executor) + .withTimeout(timeout, + Response.status(Status.NO_CONTENT) + .header(PRESTO_PAGE_TOKEN, token) + .header(PRESTO_PAGE_NEXT_TOKEN, token) + .build()); + } + + @DELETE + @Path("{taskId}/results/{outputId}") + @Produces(MediaType.APPLICATION_JSON) + public Response abortResults(@PathParam("taskId") TaskId taskId, @PathParam("outputId") TaskId outputId, @Context UriInfo uriInfo) + { + checkNotNull(taskId, "taskId is null"); + checkNotNull(outputId, "outputId is null"); + + TaskInfo taskInfo = taskManager.abortTaskResults(taskId, outputId); + if (shouldSummarize(uriInfo)) { + taskInfo = taskInfo.summarize(); + } + return Response.ok(taskInfo).build(); + } + + private static boolean shouldSummarize(UriInfo uriInfo) + { + return uriInfo.getQueryParameters().containsKey("summarize"); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/server/TaskUpdateRequest.java b/presto-main/src/main/java/com/facebook/presto/server/TaskUpdateRequest.java new file mode 100644 index 00000000..b60d70e6 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/server/TaskUpdateRequest.java @@ -0,0 +1,88 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.server; + +import com.facebook.presto.OutputBuffers; +import com.facebook.presto.Session; +import com.facebook.presto.TaskSource; +import com.facebook.presto.sql.planner.PlanFragment; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.google.common.base.MoreObjects.toStringHelper; + +public class TaskUpdateRequest +{ + private final Session session; + private final PlanFragment fragment; + private final List sources; + private final OutputBuffers outputIds; + + @JsonCreator + public TaskUpdateRequest( + @JsonProperty("session") Session session, + @JsonProperty("fragment") PlanFragment fragment, + @JsonProperty("sources") List sources, + @JsonProperty("outputIds") OutputBuffers outputIds) + { + Preconditions.checkNotNull(session, "session is null"); + Preconditions.checkNotNull(fragment, "fragment is null"); + Preconditions.checkNotNull(sources, "sources is null"); + Preconditions.checkNotNull(outputIds, "outputIds is null"); + + this.session = session; + this.fragment = fragment; + this.sources = ImmutableList.copyOf(sources); + this.outputIds = outputIds; + } + + @JsonProperty + public Session getSession() + { + return session; + } + + @JsonProperty + public PlanFragment getFragment() + { + return fragment; + } + + @JsonProperty + public List getSources() + { + return sources; + } + + @JsonProperty + public OutputBuffers getOutputIds() + { + return outputIds; + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("session", session) + .add("fragment", fragment) + .add("sources", sources) + .add("outputIds", outputIds) + .toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/server/ThreadResource.java b/presto-main/src/main/java/com/facebook/presto/server/ThreadResource.java new file mode 100644 index 00000000..68177eaf --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/server/ThreadResource.java @@ -0,0 +1,194 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.server; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Ordering; +import com.google.common.io.Resources; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import java.io.IOException; +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadInfo; +import java.lang.management.ThreadMXBean; +import java.nio.charset.StandardCharsets; +import java.util.Comparator; +import java.util.List; + +import static com.facebook.presto.server.ThreadResource.Info.byName; +import static com.google.common.io.Resources.getResource; + +@Path("/") +public class ThreadResource +{ + @GET + @Path("/ui/thread") + @Produces(MediaType.TEXT_HTML) + public String getUi() + throws IOException + { + return Resources.toString(getResource(getClass(), "thread.html"), StandardCharsets.UTF_8); + } + + @GET + @Path("/v1/thread") + @Produces(MediaType.APPLICATION_JSON) + public List getThreadInfo() + { + ThreadMXBean mbean = ManagementFactory.getThreadMXBean(); + + ImmutableList.Builder builder = ImmutableList.builder(); + for (ThreadInfo info : mbean.getThreadInfo(mbean.getAllThreadIds(), Integer.MAX_VALUE)) { + builder.add(new Info( + info.getThreadId(), + info.getThreadName(), + info.getThreadState().name(), + info.getLockOwnerId() == -1 ? null : info.getLockOwnerId(), + toStackTrace(info.getStackTrace()))); + } + return Ordering.from(byName()).sortedCopy(builder.build()); + } + + private static List toStackTrace(StackTraceElement[] stackTrace) + { + ImmutableList.Builder builder = ImmutableList.builder(); + + for (StackTraceElement item : stackTrace) { + builder.add(new StackLine( + item.getFileName(), + item.getLineNumber(), + item.getClassName(), + item.getMethodName())); + } + + return builder.build(); + } + + public static class Info + { + private final long id; + private final String name; + private final String state; + private final Long lockOwnerId; + private final List stackTrace; + + @JsonCreator + public Info( + @JsonProperty("id") long id, + @JsonProperty("name") String name, + @JsonProperty("state") String state, + @JsonProperty("lockOwner") Long lockOwnerId, + @JsonProperty("stackTrace") List stackTrace) + { + this.id = id; + this.name = name; + this.state = state; + this.lockOwnerId = lockOwnerId; + this.stackTrace = stackTrace; + } + + @JsonProperty + public long getId() + { + return id; + } + + @JsonProperty + public String getName() + { + return name; + } + + @JsonProperty + public String getState() + { + return state; + } + + @JsonProperty + public Long getLockOwnerId() + { + return lockOwnerId; + } + + @JsonProperty + public List getStackTrace() + { + return stackTrace; + } + + public static Comparator byName() + { + return new Comparator() + { + @Override + public int compare(Info info, Info info2) + { + return info.getName().compareTo(info2.getName()); + } + }; + } + } + + public static class StackLine + { + private final String file; + private final int line; + private final String className; + private final String method; + + @JsonCreator + public StackLine( + @JsonProperty("file") String file, + @JsonProperty("line") int line, + @JsonProperty("class") String className, + @JsonProperty("method") String method) + { + this.file = file; + this.line = line; + this.className = className; + this.method = method; + } + + @JsonProperty + public String getFile() + { + return file; + } + + @JsonProperty + public int getLine() + { + return line; + } + + @JsonProperty + public String getClassName() + { + return className; + } + + @JsonProperty + public String getMethod() + { + return method; + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/server/ThrowableMapper.java b/presto-main/src/main/java/com/facebook/presto/server/ThrowableMapper.java new file mode 100644 index 00000000..08f38f87 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/server/ThrowableMapper.java @@ -0,0 +1,65 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.server; + +import com.google.common.base.Throwables; +import io.airlift.log.Logger; + +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.ResponseBuilder; +import javax.ws.rs.ext.ExceptionMapper; + +import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE; +import static javax.ws.rs.core.MediaType.TEXT_PLAIN; + +public class ThrowableMapper + implements ExceptionMapper +{ + private static final Logger log = Logger.get(ThrowableMapper.class); + + private final boolean includeExceptionInResponse; + + @Context + private HttpServletRequest request; + + @Inject + public ThrowableMapper(ServerConfig config) + { + includeExceptionInResponse = config.isIncludeExceptionInResponse(); + } + + @Override + public Response toResponse(Throwable throwable) + { + if (throwable instanceof WebApplicationException) { + return ((WebApplicationException) throwable).getResponse(); + } + + log.warn(throwable, "Request failed for %s", request.getRequestURI()); + + ResponseBuilder responseBuilder = Response.serverError() + .header(CONTENT_TYPE, TEXT_PLAIN); + if (includeExceptionInResponse) { + responseBuilder.entity(Throwables.getStackTraceAsString(throwable)); + } + else { + responseBuilder.entity("Exception processing request"); + } + return responseBuilder.build(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/server/testing/FileUtils.java b/presto-main/src/main/java/com/facebook/presto/server/testing/FileUtils.java new file mode 100644 index 00000000..2355014c --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/server/testing/FileUtils.java @@ -0,0 +1,83 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.server.testing; + +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.FileVisitor; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; + +final class FileUtils +{ + private FileUtils() {} + + public static void deleteRecursively(Path path) + { + try { + Files.walkFileTree(path, deletionVisitor()); + } + catch (IOException e) { + // ignored + } + } + + private static FileVisitor deletionVisitor() + { + return new FileVisitor() + { + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) + throws IOException + { + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) + throws IOException + { + delete(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFileFailed(Path file, IOException e) + throws IOException + { + delete(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException e) + throws IOException + { + delete(dir); + return FileVisitResult.CONTINUE; + } + }; + } + + private static void delete(Path path) + { + try { + Files.delete(path); + } + catch (IOException e) { + // ignored + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/server/testing/TestingPrestoServer.java b/presto-main/src/main/java/com/facebook/presto/server/testing/TestingPrestoServer.java new file mode 100644 index 00000000..6a5c14e4 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/server/testing/TestingPrestoServer.java @@ -0,0 +1,291 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.server.testing; + +import com.facebook.presto.connector.ConnectorManager; +import com.facebook.presto.execution.QueryManager; +import com.facebook.presto.memory.ClusterMemoryManager; +import com.facebook.presto.memory.LocalMemoryManager; +import com.facebook.presto.metadata.AllNodes; +import com.facebook.presto.metadata.InternalNodeManager; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.server.PluginManager; +import com.facebook.presto.server.ServerMainModule; +import com.facebook.presto.spi.Node; +import com.facebook.presto.spi.Plugin; +import com.facebook.presto.sql.parser.SqlParserOptions; +import com.google.common.base.Joiner; +import com.google.common.base.Optional; +import com.google.common.base.Splitter; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.net.HostAndPort; +import com.google.inject.Injector; +import com.google.inject.Module; +import io.airlift.bootstrap.Bootstrap; +import io.airlift.bootstrap.LifeCycleManager; +import io.airlift.discovery.client.Announcer; +import io.airlift.discovery.client.DiscoveryModule; +import io.airlift.discovery.client.ServiceAnnouncement; +import io.airlift.discovery.client.ServiceSelectorManager; +import io.airlift.discovery.client.testing.TestingDiscoveryModule; +import io.airlift.event.client.EventModule; +import io.airlift.http.server.testing.TestingHttpServer; +import io.airlift.http.server.testing.TestingHttpServerModule; +import io.airlift.jaxrs.JaxrsModule; +import io.airlift.jmx.testing.TestingJmxModule; +import io.airlift.json.JsonModule; +import io.airlift.node.testing.TestingNodeModule; +import io.airlift.tracetoken.TraceTokenModule; +import org.weakref.jmx.guice.MBeanModule; + +import java.io.Closeable; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.facebook.presto.server.testing.FileUtils.deleteRecursively; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Strings.nullToEmpty; +import static io.airlift.discovery.client.ServiceAnnouncement.serviceAnnouncement; + +public class TestingPrestoServer + implements Closeable +{ + public static final String TEST_CATALOG = "default"; // TODO: change this to test_catalog + + private final Path baseDataDir; + private final LifeCycleManager lifeCycleManager; + private final PluginManager pluginManager; + private final ConnectorManager connectorManager; + private final TestingHttpServer server; + private final Metadata metadata; + private final ClusterMemoryManager clusterMemoryManager; + private final LocalMemoryManager localMemoryManager; + private final InternalNodeManager nodeManager; + private final ServiceSelectorManager serviceSelectorManager; + private final Announcer announcer; + private QueryManager queryManager; + + public TestingPrestoServer() + throws Exception + { + this(ImmutableList.of()); + } + + public TestingPrestoServer(List additionalModules) + throws Exception + { + this(true, ImmutableMap.of(), null, null, additionalModules); + } + + public TestingPrestoServer(boolean coordinator, Map properties, String environment, URI discoveryUri, List additionalModules) + throws Exception + { + baseDataDir = Files.createTempDirectory("PrestoTest"); + + ImmutableMap.Builder serverProperties = ImmutableMap.builder() + .putAll(properties) + .put("coordinator", String.valueOf(coordinator)) + .put("presto.version", "testversion") + .put("task.default-concurrency", "4") + .put("analyzer.experimental-syntax-enabled", "true"); + + if (coordinator) { + // TODO: enable failure detector + serverProperties.put("failure-detector.enabled", "false"); + } + + ImmutableList.Builder modules = ImmutableList.builder() + .add(new TestingNodeModule(Optional.fromNullable(environment))) + .add(new TestingHttpServerModule()) + .add(new JsonModule()) + .add(new JaxrsModule(true)) + .add(new MBeanModule()) + .add(new TestingJmxModule()) + .add(new EventModule()) + .add(new TraceTokenModule()) + .add(new ServerMainModule(new SqlParserOptions())); + + if (discoveryUri != null) { + checkNotNull(environment, "environment required when discoveryUri is present"); + serverProperties.put("discovery.uri", discoveryUri.toString()); + modules.add(new DiscoveryModule()); + } + else { + modules.add(new TestingDiscoveryModule()); + } + + modules.addAll(additionalModules); + + Bootstrap app = new Bootstrap(modules.build()); + + Map optionalProperties = new HashMap<>(); + if (environment != null) { + optionalProperties.put("node.environment", environment); + } + + Injector injector = app + .strictConfig() + .doNotInitializeLogging() + .setRequiredConfigurationProperties(serverProperties.build()) + .setOptionalConfigurationProperties(optionalProperties) + .initialize(); + + injector.getInstance(Announcer.class).start(); + + lifeCycleManager = injector.getInstance(LifeCycleManager.class); + + queryManager = injector.getInstance(QueryManager.class); + + pluginManager = injector.getInstance(PluginManager.class); + + connectorManager = injector.getInstance(ConnectorManager.class); + + server = injector.getInstance(TestingHttpServer.class); + metadata = injector.getInstance(Metadata.class); + clusterMemoryManager = injector.getInstance(ClusterMemoryManager.class); + localMemoryManager = injector.getInstance(LocalMemoryManager.class); + nodeManager = injector.getInstance(InternalNodeManager.class); + serviceSelectorManager = injector.getInstance(ServiceSelectorManager.class); + announcer = injector.getInstance(Announcer.class); + + announcer.forceAnnounce(); + + refreshNodes(); + } + + @Override + public void close() + { + try { + if (lifeCycleManager != null) { + lifeCycleManager.stop(); + } + } + catch (Exception e) { + throw Throwables.propagate(e); + } + finally { + deleteRecursively(baseDataDir); + } + } + + public void installPlugin(Plugin plugin) + { + pluginManager.installPlugin(plugin); + } + + public QueryManager getQueryManager() + { + return queryManager; + } + + public void createCatalog(String catalogName, String connectorName) + { + createCatalog(catalogName, connectorName, ImmutableMap.of()); + } + + public void createCatalog(String catalogName, String connectorName, Map properties) + { + connectorManager.createConnection(catalogName, connectorName, properties); + updateDatasourcesAnnouncement(announcer, catalogName); + } + + public Path getBaseDataDir() + { + return baseDataDir; + } + + public URI getBaseUrl() + { + return server.getBaseUrl(); + } + + public URI resolve(String path) + { + return server.getBaseUrl().resolve(path); + } + + public HostAndPort getAddress() + { + return HostAndPort.fromParts(getBaseUrl().getHost(), getBaseUrl().getPort()); + } + + public Metadata getMetadata() + { + return metadata; + } + + public LocalMemoryManager getLocalMemoryManager() + { + return localMemoryManager; + } + + public ClusterMemoryManager getClusterMemoryManager() + { + return clusterMemoryManager; + } + + public final AllNodes refreshNodes() + { + serviceSelectorManager.forceRefresh(); + nodeManager.refreshNodes(); + return nodeManager.getAllNodes(); + } + + public Set getActiveNodesWithConnector(String connectorName) + { + return nodeManager.getActiveDatasourceNodes(connectorName); + } + + private static void updateDatasourcesAnnouncement(Announcer announcer, String connectorId) + { + // + // This code was copied from PrestoServer, and is a hack that should be removed when the data source property is removed + // + + // get existing announcement + ServiceAnnouncement announcement = getPrestoAnnouncement(announcer.getServiceAnnouncements()); + + // update datasources property + Map properties = new LinkedHashMap<>(announcement.getProperties()); + String property = nullToEmpty(properties.get("datasources")); + Set datasources = new LinkedHashSet<>(Splitter.on(',').trimResults().omitEmptyStrings().splitToList(property)); + datasources.add(connectorId); + properties.put("datasources", Joiner.on(',').join(datasources)); + + // update announcement + announcer.removeServiceAnnouncement(announcement.getId()); + announcer.addServiceAnnouncement(serviceAnnouncement(announcement.getType()).addProperties(properties).build()); + announcer.forceAnnounce(); + } + + private static ServiceAnnouncement getPrestoAnnouncement(Set announcements) + { + for (ServiceAnnouncement announcement : announcements) { + if (announcement.getType().equals("presto")) { + return announcement; + } + } + throw new RuntimeException("Presto announcement not found: " + announcements); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/split/ConnectorAwareSplitSource.java b/presto-main/src/main/java/com/facebook/presto/split/ConnectorAwareSplitSource.java new file mode 100644 index 00000000..913eca5c --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/split/ConnectorAwareSplitSource.java @@ -0,0 +1,79 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.split; + +import com.facebook.presto.metadata.Split; +import com.facebook.presto.spi.ConnectorSplitSource; +import com.google.common.collect.Lists; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class ConnectorAwareSplitSource + implements SplitSource +{ + private final String connectorId; + private final ConnectorSplitSource source; + + public ConnectorAwareSplitSource(String connectorId, ConnectorSplitSource source) + { + this.connectorId = checkNotNull(connectorId, "connectorId is null"); + this.source = checkNotNull(source, "source is null"); + } + + @Override + public String getDataSourceName() + { + return source.getDataSourceName(); + } + + @Override + public CompletableFuture> getNextBatch(int maxSize) + { + return source.getNextBatch(maxSize) + .thenApply(splits -> Lists.transform(splits, split -> new Split(connectorId, split))); + } + + @Override + public void close() + { + source.close(); + } + + @Override + public boolean isFinished() + { + return source.isFinished(); + } + + @Override + public String toString() + { + return connectorId + ":" + source; + } + + @Override + public boolean isControlScanConcurrencyEnabled() + { + return source.isControlScanConcurrencyEnabled(); + } + + @Override + public int getScanConcurrencyCount() + { + return source.getScanConcurrencyCount(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/split/MappedRecordSet.java b/presto-main/src/main/java/com/facebook/presto/split/MappedRecordSet.java new file mode 100644 index 00000000..186ed2a7 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/split/MappedRecordSet.java @@ -0,0 +1,147 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.split; + +import com.facebook.presto.spi.RecordCursor; +import com.facebook.presto.spi.RecordSet; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkArgument; + +public class MappedRecordSet + implements RecordSet +{ + private final RecordSet delegate; + private final int[] delegateFieldIndex; + private final List columnTypes; + + public MappedRecordSet(RecordSet delegate, List delegateFieldIndex) + { + this.delegate = delegate; + this.delegateFieldIndex = new int[delegateFieldIndex.size()]; + for (int i = 0; i < delegateFieldIndex.size(); i++) { + this.delegateFieldIndex[i] = delegateFieldIndex.get(i); + } + + List delegateColumnTypes = delegate.getColumnTypes(); + ImmutableList.Builder columnTypes = ImmutableList.builder(); + for (int delegateField : delegateFieldIndex) { + checkArgument(delegateField >= 0 && delegateField < delegateColumnTypes.size(), "Invalid system field %s", delegateField); + columnTypes.add(delegateColumnTypes.get(delegateField)); + } + this.columnTypes = columnTypes.build(); + } + + @Override + public List getColumnTypes() + { + return columnTypes; + } + + @Override + public RecordCursor cursor() + { + return new MappedRecordCursor(delegate.cursor(), delegateFieldIndex); + } + + private static class MappedRecordCursor + implements RecordCursor + { + private final RecordCursor delegate; + private final int[] delegateFieldIndex; + + private MappedRecordCursor(RecordCursor delegate, int[] delegateFieldIndex) + { + this.delegate = delegate; + this.delegateFieldIndex = delegateFieldIndex; + } + + @Override + public long getTotalBytes() + { + return delegate.getTotalBytes(); + } + + @Override + public long getCompletedBytes() + { + return delegate.getCompletedBytes(); + } + + @Override + public long getReadTimeNanos() + { + return delegate.getReadTimeNanos(); + } + + @Override + public Type getType(int field) + { + return delegate.getType(toDelegateField(field)); + } + + @Override + public boolean advanceNextPosition() + { + return delegate.advanceNextPosition(); + } + + @Override + public boolean getBoolean(int field) + { + return delegate.getBoolean(toDelegateField(field)); + } + + @Override + public long getLong(int field) + { + return delegate.getLong(toDelegateField(field)); + } + + @Override + public double getDouble(int field) + { + return delegate.getDouble(toDelegateField(field)); + } + + @Override + public Slice getSlice(int field) + { + return delegate.getSlice(toDelegateField(field)); + } + + @Override + public boolean isNull(int field) + { + return delegate.isNull(toDelegateField(field)); + } + + @Override + public void close() + { + delegate.close(); + } + + private int toDelegateField(int field) + { + checkArgument(field >= 0, "field is negative"); + checkArgument(field < delegateFieldIndex.length); + return delegateFieldIndex[field]; + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/split/PageSinkManager.java b/presto-main/src/main/java/com/facebook/presto/split/PageSinkManager.java new file mode 100644 index 00000000..454f9ede --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/split/PageSinkManager.java @@ -0,0 +1,61 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.split; + +import com.facebook.presto.metadata.InsertTableHandle; +import com.facebook.presto.metadata.OutputTableHandle; +import com.facebook.presto.spi.ConnectorPageSink; +import com.facebook.presto.spi.ConnectorPageSinkProvider; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import static com.google.common.base.Preconditions.checkArgument; + +public class PageSinkManager + implements PageSinkProvider +{ + private final ConcurrentMap pageSinkProviders = new ConcurrentHashMap<>(); + + public void addConnectorPageSinkProvider(String connectorId, ConnectorPageSinkProvider connectorPageSinkProvider) + { + pageSinkProviders.put(connectorId, connectorPageSinkProvider); + } + + public void removeConnectorPageSinkProvider(String connectorId) + { + if (pageSinkProviders.containsKey(connectorId)) { + pageSinkProviders.remove(connectorId); + } + } + + @Override + public ConnectorPageSink createPageSink(OutputTableHandle tableHandle) + { + return providerFor(tableHandle.getConnectorId()).createPageSink(tableHandle.getConnectorHandle()); + } + + @Override + public ConnectorPageSink createPageSink(InsertTableHandle tableHandle) + { + return providerFor(tableHandle.getConnectorId()).createPageSink(tableHandle.getConnectorHandle()); + } + + private ConnectorPageSinkProvider providerFor(String connectorId) + { + ConnectorPageSinkProvider provider = pageSinkProviders.get(connectorId); + checkArgument(provider != null, "No page sink provider for connector '%s'", connectorId); + return provider; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/split/PageSinkProvider.java b/presto-main/src/main/java/com/facebook/presto/split/PageSinkProvider.java new file mode 100644 index 00000000..2541a3d0 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/split/PageSinkProvider.java @@ -0,0 +1,25 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.split; + +import com.facebook.presto.metadata.InsertTableHandle; +import com.facebook.presto.metadata.OutputTableHandle; +import com.facebook.presto.spi.ConnectorPageSink; + +public interface PageSinkProvider +{ + ConnectorPageSink createPageSink(OutputTableHandle tableHandle); + + ConnectorPageSink createPageSink(InsertTableHandle tableHandle); +} diff --git a/presto-main/src/main/java/com/facebook/presto/split/PageSourceManager.java b/presto-main/src/main/java/com/facebook/presto/split/PageSourceManager.java new file mode 100644 index 00000000..fdf1a8f2 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/split/PageSourceManager.java @@ -0,0 +1,62 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.split; + +import com.facebook.presto.metadata.Split; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorPageSource; +import com.facebook.presto.spi.ConnectorPageSourceProvider; + +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class PageSourceManager + implements PageSourceProvider +{ + private final ConcurrentMap pageSourceProviders = new ConcurrentHashMap<>(); + + public void addConnectorPageSourceProvider(String connectorId, ConnectorPageSourceProvider connectorPageSourceProvider) + { + pageSourceProviders.put(connectorId, connectorPageSourceProvider); + } + + public void removeConnectorPageSourceProvider(String connectorId) + { + if (pageSourceProviders.containsKey(connectorId)) { + pageSourceProviders.remove(connectorId); + } + } + + @Override + public ConnectorPageSource createPageSource(Split split, List columns) + { + checkNotNull(split, "split is null"); + checkNotNull(columns, "columns is null"); + + return getPageSourceProvider(split).createPageSource(split.getConnectorSplit(), columns); + } + + private ConnectorPageSourceProvider getPageSourceProvider(Split split) + { + ConnectorPageSourceProvider provider = pageSourceProviders.get(split.getConnectorId()); + + checkArgument(provider != null, "No page stream provider for '%s", split.getConnectorId()); + + return provider; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/split/PageSourceProvider.java b/presto-main/src/main/java/com/facebook/presto/split/PageSourceProvider.java new file mode 100644 index 00000000..ebf7d614 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/split/PageSourceProvider.java @@ -0,0 +1,25 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.split; + +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.metadata.Split; +import com.facebook.presto.spi.ConnectorPageSource; + +import java.util.List; + +public interface PageSourceProvider +{ + ConnectorPageSource createPageSource(Split split, List columns); +} diff --git a/presto-main/src/main/java/com/facebook/presto/split/RecordPageSinkProvider.java b/presto-main/src/main/java/com/facebook/presto/split/RecordPageSinkProvider.java new file mode 100644 index 00000000..e24cdffd --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/split/RecordPageSinkProvider.java @@ -0,0 +1,46 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.split; + +import com.facebook.presto.spi.ConnectorInsertTableHandle; +import com.facebook.presto.spi.ConnectorOutputTableHandle; +import com.facebook.presto.spi.ConnectorPageSink; +import com.facebook.presto.spi.ConnectorPageSinkProvider; +import com.facebook.presto.spi.ConnectorRecordSinkProvider; +import com.facebook.presto.spi.RecordPageSink; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class RecordPageSinkProvider + implements ConnectorPageSinkProvider +{ + private final ConnectorRecordSinkProvider recordSinkProvider; + + public RecordPageSinkProvider(ConnectorRecordSinkProvider recordSinkProvider) + { + this.recordSinkProvider = checkNotNull(recordSinkProvider, "recordSinkProvider is null"); + } + + @Override + public ConnectorPageSink createPageSink(ConnectorOutputTableHandle outputTableHandle) + { + return new RecordPageSink(recordSinkProvider.getRecordSink(outputTableHandle)); + } + + @Override + public ConnectorPageSink createPageSink(ConnectorInsertTableHandle insertTableHandle) + { + return new RecordPageSink(recordSinkProvider.getRecordSink(insertTableHandle)); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/split/RecordPageSourceProvider.java b/presto-main/src/main/java/com/facebook/presto/split/RecordPageSourceProvider.java new file mode 100644 index 00000000..a16bd157 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/split/RecordPageSourceProvider.java @@ -0,0 +1,42 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.split; + +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorPageSource; +import com.facebook.presto.spi.ConnectorPageSourceProvider; +import com.facebook.presto.spi.ConnectorRecordSetProvider; +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.RecordPageSource; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class RecordPageSourceProvider + implements ConnectorPageSourceProvider +{ + private ConnectorRecordSetProvider recordSetProvider; + + public RecordPageSourceProvider(ConnectorRecordSetProvider recordSetProvider) + { + this.recordSetProvider = checkNotNull(recordSetProvider, "recordSetProvider is null"); + } + + @Override + public ConnectorPageSource createPageSource(ConnectorSplit split, List columns) + { + return new RecordPageSource(recordSetProvider.getRecordSet(split, columns)); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/split/RemoteSplit.java b/presto-main/src/main/java/com/facebook/presto/split/RemoteSplit.java new file mode 100644 index 00000000..4fe174d2 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/split/RemoteSplit.java @@ -0,0 +1,70 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.split; + +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.HostAddress; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; + +import java.net.URI; +import java.util.List; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkNotNull; + +public class RemoteSplit + implements ConnectorSplit +{ + private final URI location; + + @JsonCreator + public RemoteSplit(@JsonProperty("location") URI location) + { + this.location = checkNotNull(location, "location is null"); + } + + @JsonProperty + public URI getLocation() + { + return location; + } + + @Override + public Object getInfo() + { + return this; + } + + @Override + public boolean isRemotelyAccessible() + { + return true; + } + + @Override + public List getAddresses() + { + return ImmutableList.of(); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("location", location) + .toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/split/SampledSplitSource.java b/presto-main/src/main/java/com/facebook/presto/split/SampledSplitSource.java new file mode 100644 index 00000000..a75377c0 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/split/SampledSplitSource.java @@ -0,0 +1,66 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.split; + +import com.facebook.presto.metadata.Split; + +import javax.annotation.Nullable; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ThreadLocalRandom; + +import static com.facebook.presto.util.ImmutableCollectors.toImmutableList; +import static com.google.common.base.Preconditions.checkNotNull; + +public class SampledSplitSource + implements SplitSource +{ + private final SplitSource splitSource; + private final double sampleRatio; + + public SampledSplitSource(SplitSource splitSource, double sampleRatio) + { + this.splitSource = checkNotNull(splitSource, "dataSource is null"); + this.sampleRatio = sampleRatio; + } + + @Nullable + @Override + public String getDataSourceName() + { + return splitSource.getDataSourceName(); + } + + @Override + public CompletableFuture> getNextBatch(int maxSize) + { + return splitSource.getNextBatch(maxSize) + .thenApply(splits -> splits.stream() + .filter(input -> ThreadLocalRandom.current().nextDouble() < sampleRatio) + .collect(toImmutableList())); + } + + @Override + public void close() + { + splitSource.close(); + } + + @Override + public boolean isFinished() + { + return splitSource.isFinished(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/split/SplitManager.java b/presto-main/src/main/java/com/facebook/presto/split/SplitManager.java new file mode 100644 index 00000000..d82b9524 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/split/SplitManager.java @@ -0,0 +1,74 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.split; + +import com.facebook.presto.metadata.LegacyTableLayoutHandle; +import com.facebook.presto.metadata.TableLayoutHandle; +import com.facebook.presto.spi.ConnectorSplit; +import com.facebook.presto.spi.ConnectorSplitManager; +import com.facebook.presto.spi.ConnectorSplitSource; +import com.facebook.presto.spi.FixedSplitSource; +import com.google.common.collect.ImmutableList; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; + +public class SplitManager +{ + private final ConcurrentMap splitManagers = new ConcurrentHashMap<>(); + + public void addConnectorSplitManager(String connectorId, ConnectorSplitManager connectorSplitManager) + { + checkState(splitManagers.putIfAbsent(connectorId, connectorSplitManager) == null, "SplitManager for connector '%s' is already registered", connectorId); + } + + public void removeConnectorSplitManager(String connectorId) + { + if (splitManagers.containsKey(connectorId)) { + splitManagers.remove(connectorId); + } + } + + public SplitSource getSplits(TableLayoutHandle layout) + { + String connectorId = layout.getConnectorId(); + ConnectorSplitManager splitManager = getConnectorSplitManager(connectorId); + + ConnectorSplitSource source; + if (layout.getConnectorHandle() instanceof LegacyTableLayoutHandle) { + LegacyTableLayoutHandle handle = (LegacyTableLayoutHandle) layout.getConnectorHandle(); + if (handle.getPartitions().isEmpty()) { + return new ConnectorAwareSplitSource(connectorId, new FixedSplitSource(connectorId, ImmutableList.of())); + } + + source = splitManager.getPartitionSplits(handle.getTable(), handle.getPartitions()); + } + else { + source = splitManager.getSplits(layout.getConnectorHandle()); + } + + return new ConnectorAwareSplitSource(connectorId, source); + } + + public ConnectorSplitManager getConnectorSplitManager(String connectorId) + { + ConnectorSplitManager result = splitManagers.get(connectorId); + checkArgument(result != null, "No split manager for connector '%s'", connectorId); + + return result; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/split/SplitSource.java b/presto-main/src/main/java/com/facebook/presto/split/SplitSource.java new file mode 100644 index 00000000..1ab2d116 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/split/SplitSource.java @@ -0,0 +1,43 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.split; + +import com.facebook.presto.metadata.Split; + +import java.io.Closeable; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public interface SplitSource + extends Closeable +{ + String getDataSourceName(); + + CompletableFuture> getNextBatch(int maxSize); + + @Override + void close(); + + boolean isFinished(); + + default boolean isControlScanConcurrencyEnabled() + { + return false; + } + + default int getScanConcurrencyCount() + { + return 1; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/ExpressionUtils.java b/presto-main/src/main/java/com/facebook/presto/sql/ExpressionUtils.java new file mode 100644 index 00000000..9d7d6530 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/ExpressionUtils.java @@ -0,0 +1,200 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql; + +import com.facebook.presto.sql.planner.DependencyExtractor; +import com.facebook.presto.sql.planner.DeterminismEvaluator; +import com.facebook.presto.sql.planner.Symbol; +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.IsNullPredicate; +import com.facebook.presto.sql.tree.LogicalBinaryExpression; +import com.facebook.presto.sql.tree.QualifiedNameReference; +import com.google.common.base.Function; +import com.google.common.base.Preconditions; +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +import static com.facebook.presto.sql.tree.BooleanLiteral.FALSE_LITERAL; +import static com.facebook.presto.sql.tree.BooleanLiteral.TRUE_LITERAL; +import static com.facebook.presto.util.ImmutableCollectors.toImmutableList; +import static com.google.common.base.Predicates.not; +import static com.google.common.collect.Iterables.filter; + +public final class ExpressionUtils +{ + private ExpressionUtils() {} + + public static List extractConjuncts(Expression expression) + { + if (expression instanceof LogicalBinaryExpression && ((LogicalBinaryExpression) expression).getType() == LogicalBinaryExpression.Type.AND) { + LogicalBinaryExpression and = (LogicalBinaryExpression) expression; + return ImmutableList.builder() + .addAll(extractConjuncts(and.getLeft())) + .addAll(extractConjuncts(and.getRight())) + .build(); + } + + return ImmutableList.of(expression); + } + + public static List extractDisjuncts(Expression expression) + { + if (expression instanceof LogicalBinaryExpression && ((LogicalBinaryExpression) expression).getType() == LogicalBinaryExpression.Type.OR) { + LogicalBinaryExpression or = (LogicalBinaryExpression) expression; + return ImmutableList.builder() + .addAll(extractDisjuncts(or.getLeft())) + .addAll(extractDisjuncts(or.getRight())) + .build(); + } + + return ImmutableList.of(expression); + } + + public static Expression and(Expression... expressions) + { + return and(Arrays.asList(expressions)); + } + + public static Expression and(Iterable expressions) + { + return binaryExpression(LogicalBinaryExpression.Type.AND, expressions); + } + + public static Expression or(Expression... expressions) + { + return or(Arrays.asList(expressions)); + } + + public static Expression or(Iterable expressions) + { + return binaryExpression(LogicalBinaryExpression.Type.OR, expressions); + } + + public static Expression binaryExpression(LogicalBinaryExpression.Type type, Iterable expressions) + { + Preconditions.checkNotNull(type, "type is null"); + Preconditions.checkNotNull(expressions, "expressions is null"); + Preconditions.checkArgument(!Iterables.isEmpty(expressions), "expressions is empty"); + + Iterator iterator = expressions.iterator(); + + Expression result = iterator.next(); + while (iterator.hasNext()) { + result = new LogicalBinaryExpression(type, result, iterator.next()); + } + + return result; + } + + public static Expression combineConjuncts(Expression... expressions) + { + return combineConjuncts(Arrays.asList(expressions)); + } + + public static Expression combineConjuncts(Iterable expressions) + { + return combineConjunctsWithDefault(expressions, TRUE_LITERAL); + } + + public static Expression combineConjunctsWithDefault(Iterable expressions, Expression emptyDefault) + { + Preconditions.checkNotNull(expressions, "expressions is null"); + + // Flatten all the expressions into their component conjuncts + expressions = Iterables.concat(Iterables.transform(expressions, ExpressionUtils::extractConjuncts)); + + // Strip out all true literal conjuncts + expressions = Iterables.filter(expressions, not(Predicates.equalTo(TRUE_LITERAL))); + expressions = removeDuplicates(expressions); + return Iterables.isEmpty(expressions) ? emptyDefault : and(expressions); + } + + public static Expression combineDisjuncts(Expression... expressions) + { + return combineDisjuncts(Arrays.asList(expressions)); + } + + public static Expression combineDisjuncts(Iterable expressions) + { + return combineDisjunctsWithDefault(expressions, FALSE_LITERAL); + } + + public static Expression combineDisjunctsWithDefault(Iterable expressions, Expression emptyDefault) + { + Preconditions.checkNotNull(expressions, "expressions is null"); + + // Flatten all the expressions into their component disjuncts + expressions = Iterables.concat(Iterables.transform(expressions, ExpressionUtils::extractDisjuncts)); + + // Strip out all false literal disjuncts + expressions = Iterables.filter(expressions, not(Predicates.equalTo(FALSE_LITERAL))); + expressions = removeDuplicates(expressions); + return Iterables.isEmpty(expressions) ? emptyDefault : or(expressions); + } + + public static Expression stripNonDeterministicConjuncts(Expression expression) + { + return combineConjuncts(filter(extractConjuncts(expression), DeterminismEvaluator::isDeterministic)); + } + + public static Expression stripDeterministicConjuncts(Expression expression) + { + return combineConjuncts(extractConjuncts(expression) + .stream() + .filter((conjunct) -> !DeterminismEvaluator.isDeterministic(conjunct)) + .collect(toImmutableList())); + } + + public static Function expressionOrNullSymbols(final Predicate... nullSymbolScopes) + { + return expression -> { + ImmutableList.Builder resultDisjunct = ImmutableList.builder(); + resultDisjunct.add(expression); + + for (Predicate nullSymbolScope : nullSymbolScopes) { + Iterable symbols = filter(DependencyExtractor.extractUnique(expression), nullSymbolScope); + if (Iterables.isEmpty(symbols)) { + continue; + } + + ImmutableList.Builder nullConjuncts = ImmutableList.builder(); + for (Symbol symbol : symbols) { + nullConjuncts.add(new IsNullPredicate(new QualifiedNameReference(symbol.toQualifiedName()))); + } + + resultDisjunct.add(and(nullConjuncts.build())); + } + + return or(resultDisjunct.build()); + }; + } + + private static Iterable removeDuplicates(Iterable expressions) + { + // Capture all non-deterministic predicates + Iterable nonDeterministicDisjuncts = Iterables.filter(expressions, not(DeterminismEvaluator::isDeterministic)); + + // Capture and de-dupe all deterministic predicates + Iterable deterministicDisjuncts = ImmutableSet.copyOf(Iterables.filter(expressions, DeterminismEvaluator::isDeterministic)); + + return Iterables.concat(nonDeterministicDisjuncts, deterministicDisjuncts); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/analyzer/AggregateExtractor.java b/presto-main/src/main/java/com/facebook/presto/sql/analyzer/AggregateExtractor.java new file mode 100644 index 00000000..aeb774b1 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/analyzer/AggregateExtractor.java @@ -0,0 +1,53 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.analyzer; + +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.sql.tree.DefaultExpressionTraversalVisitor; +import com.facebook.presto.sql.tree.FunctionCall; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +public class AggregateExtractor + extends DefaultExpressionTraversalVisitor +{ + private final Metadata metadata; + + private final ImmutableList.Builder aggregates = ImmutableList.builder(); + + public AggregateExtractor(Metadata metadata) + { + Preconditions.checkNotNull(metadata, "metadata is null"); + + this.metadata = metadata; + } + + @Override + protected Void visitFunctionCall(FunctionCall node, Void context) + { + if (metadata.isAggregationFunction(node.getName()) && !node.getWindow().isPresent()) { + aggregates.add(node); + return null; + } + + return super.visitFunctionCall(node, null); + } + + public List getAggregates() + { + return aggregates.build(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/analyzer/AggregationAnalyzer.java b/presto-main/src/main/java/com/facebook/presto/sql/analyzer/AggregationAnalyzer.java new file mode 100644 index 00000000..76f3ac04 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/analyzer/AggregationAnalyzer.java @@ -0,0 +1,431 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.analyzer; + +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.sql.tree.ArithmeticBinaryExpression; +import com.facebook.presto.sql.tree.ArithmeticUnaryExpression; +import com.facebook.presto.sql.tree.ArrayConstructor; +import com.facebook.presto.sql.tree.AstVisitor; +import com.facebook.presto.sql.tree.BetweenPredicate; +import com.facebook.presto.sql.tree.Cast; +import com.facebook.presto.sql.tree.CoalesceExpression; +import com.facebook.presto.sql.tree.ComparisonExpression; +import com.facebook.presto.sql.tree.CurrentTime; +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.Extract; +import com.facebook.presto.sql.tree.FunctionCall; +import com.facebook.presto.sql.tree.IfExpression; +import com.facebook.presto.sql.tree.InListExpression; +import com.facebook.presto.sql.tree.InPredicate; +import com.facebook.presto.sql.tree.IsNotNullPredicate; +import com.facebook.presto.sql.tree.IsNullPredicate; +import com.facebook.presto.sql.tree.LikePredicate; +import com.facebook.presto.sql.tree.Literal; +import com.facebook.presto.sql.tree.LogicalBinaryExpression; +import com.facebook.presto.sql.tree.Node; +import com.facebook.presto.sql.tree.NotExpression; +import com.facebook.presto.sql.tree.NullIfExpression; +import com.facebook.presto.sql.tree.QualifiedName; +import com.facebook.presto.sql.tree.QualifiedNameReference; +import com.facebook.presto.sql.tree.Row; +import com.facebook.presto.sql.tree.SearchedCaseExpression; +import com.facebook.presto.sql.tree.SimpleCaseExpression; +import com.facebook.presto.sql.tree.SortItem; +import com.facebook.presto.sql.tree.SubqueryExpression; +import com.facebook.presto.sql.tree.SubscriptExpression; +import com.facebook.presto.sql.tree.WhenClause; +import com.facebook.presto.sql.tree.Window; +import com.facebook.presto.sql.tree.WindowFrame; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; + +import javax.annotation.Nullable; + +import java.util.List; +import java.util.Optional; + +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.MUST_BE_AGGREGATE_OR_GROUP_BY; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.NESTED_AGGREGATION; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.NESTED_WINDOW; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.NOT_SUPPORTED; +import static com.facebook.presto.util.ImmutableCollectors.toImmutableList; +import static com.google.common.base.Predicates.equalTo; +import static com.google.common.base.Predicates.instanceOf; + +/** + * Checks whether an expression is constant with respect to the group + */ +public class AggregationAnalyzer +{ + // fields and expressions in the group by clause + private final List fieldIndexes; + private final List expressions; + + private final Metadata metadata; + + private final TupleDescriptor tupleDescriptor; + + public AggregationAnalyzer(List groupByExpressions, Metadata metadata, TupleDescriptor tupleDescriptor) + { + Preconditions.checkNotNull(groupByExpressions, "groupByExpressions is null"); + Preconditions.checkNotNull(metadata, "metadata is null"); + Preconditions.checkNotNull(tupleDescriptor, "tupleDescriptor is null"); + + this.tupleDescriptor = tupleDescriptor; + this.metadata = metadata; + + this.expressions = groupByExpressions.stream() + .filter(FieldOrExpression::isExpression) + .map(FieldOrExpression::getExpression) + .collect(toImmutableList()); + + ImmutableList.Builder fieldIndexes = ImmutableList.builder(); + + fieldIndexes.addAll(groupByExpressions.stream() + .filter(FieldOrExpression::isFieldReference) + .map(FieldOrExpression::getFieldIndex) + .iterator()); + + // For a query like "SELECT * FROM T GROUP BY a", groupByExpressions will contain "a", + // and the '*' will be expanded to Field references. Therefore we translate all simple name expressions + // in the group by clause to fields they reference so that the expansion from '*' can be matched against them + for (Expression expression : Iterables.filter(expressions, instanceOf(QualifiedNameReference.class))) { + QualifiedName name = ((QualifiedNameReference) expression).getName(); + + List fields = tupleDescriptor.resolveFields(name); + Preconditions.checkState(fields.size() <= 1, "Found more than one field for name '%s': %s", name, fields); + + if (fields.size() == 1) { + Field field = Iterables.getOnlyElement(fields); + fieldIndexes.add(tupleDescriptor.indexOf(field)); + } + } + + this.fieldIndexes = fieldIndexes.build(); + } + + public boolean analyze(int fieldIndex) + { + return Iterables.any(fieldIndexes, equalTo(fieldIndex)); + } + + public void analyze(Expression expression) + { + Visitor visitor = new Visitor(); + if (!visitor.process(expression, null)) { + throw new SemanticException(MUST_BE_AGGREGATE_OR_GROUP_BY, expression, "'%s' must be an aggregate expression or appear in GROUP BY clause", expression); + } + } + + private class Visitor + extends AstVisitor + { + @Override + protected Boolean visitExpression(Expression node, Void context) + { + throw new UnsupportedOperationException("aggregation analysis not yet implemented for: " + node.getClass().getName()); + } + + @Override + protected Boolean visitSubqueryExpression(SubqueryExpression node, Void context) + { + throw new SemanticException(NOT_SUPPORTED, node, "Scalar subqueries not yet supported"); + } + + @Override + protected Boolean visitSubscriptExpression(SubscriptExpression node, Void context) + { + return process(node.getBase(), context) && + process(node.getIndex(), context); + } + + @Override + protected Boolean visitArrayConstructor(ArrayConstructor node, Void context) + { + return node.getValues().stream().allMatch(expression -> process(expression, context)); + } + + @Override + protected Boolean visitCast(Cast node, Void context) + { + return process(node.getExpression(), context); + } + + @Override + protected Boolean visitCoalesceExpression(CoalesceExpression node, Void context) + { + return node.getOperands().stream().allMatch(expression -> process(expression, context)); + } + + @Override + protected Boolean visitNullIfExpression(NullIfExpression node, Void context) + { + return process(node.getFirst(), context) && process(node.getSecond(), context); + } + + @Override + protected Boolean visitExtract(Extract node, Void context) + { + return process(node.getExpression(), context); + } + + @Override + protected Boolean visitBetweenPredicate(BetweenPredicate node, Void context) + { + return process(node.getMin(), context) && + process(node.getValue(), context) && + process(node.getMax(), context); + } + + @Override + protected Boolean visitCurrentTime(CurrentTime node, Void context) + { + return true; + } + + @Override + protected Boolean visitArithmeticBinary(ArithmeticBinaryExpression node, Void context) + { + return process(node.getLeft(), context) && process(node.getRight(), context); + } + + @Override + protected Boolean visitComparisonExpression(ComparisonExpression node, Void context) + { + return process(node.getLeft(), context) && process(node.getRight(), context); + } + + @Override + protected Boolean visitLiteral(Literal node, Void context) + { + return true; + } + + @Override + protected Boolean visitIsNotNullPredicate(IsNotNullPredicate node, Void context) + { + return process(node.getValue(), context); + } + + @Override + protected Boolean visitIsNullPredicate(IsNullPredicate node, Void context) + { + return process(node.getValue(), context); + } + + @Override + protected Boolean visitLikePredicate(LikePredicate node, Void context) + { + return process(node.getValue(), context) && process(node.getPattern(), context); + } + + @Override + protected Boolean visitInListExpression(InListExpression node, Void context) + { + return node.getValues().stream().allMatch(expression -> process(expression, context)); + } + + @Override + protected Boolean visitInPredicate(InPredicate node, Void context) + { + return process(node.getValue(), context) && process(node.getValueList(), context); + } + + @Override + protected Boolean visitFunctionCall(FunctionCall node, Void context) + { + if (!node.getWindow().isPresent() && metadata.isAggregationFunction(node.getName())) { + AggregateExtractor aggregateExtractor = new AggregateExtractor(metadata); + WindowFunctionExtractor windowExtractor = new WindowFunctionExtractor(); + + for (Expression argument : node.getArguments()) { + aggregateExtractor.process(argument, null); + windowExtractor.process(argument, null); + } + + if (!aggregateExtractor.getAggregates().isEmpty()) { + throw new SemanticException(NESTED_AGGREGATION, + node, + "Cannot nest aggregations inside aggregation '%s': %s", + node.getName(), + aggregateExtractor.getAggregates()); + } + + if (!windowExtractor.getWindowFunctions().isEmpty()) { + throw new SemanticException(NESTED_WINDOW, + node, + "Cannot nest window functions inside aggregation '%s': %s", + node.getName(), + windowExtractor.getWindowFunctions()); + } + + return true; + } + + if (node.getWindow().isPresent() && !process(node.getWindow().get(), context)) { + return false; + } + + return node.getArguments().stream().allMatch(expression -> process(expression, context)); + } + + @Override + public Boolean visitWindow(Window node, Void context) + { + for (Expression expression : node.getPartitionBy()) { + if (!process(expression, context)) { + throw new SemanticException(MUST_BE_AGGREGATE_OR_GROUP_BY, + expression, + "PARTITION BY expression '%s' must be an aggregate expression or appear in GROUP BY clause", + expression); + } + } + + for (SortItem sortItem : node.getOrderBy()) { + Expression expression = sortItem.getSortKey(); + if (!process(expression, context)) { + throw new SemanticException(MUST_BE_AGGREGATE_OR_GROUP_BY, + expression, + "ORDER BY expression '%s' must be an aggregate expression or appear in GROUP BY clause", + expression); + } + } + + if (node.getFrame().isPresent()) { + process(node.getFrame().get(), context); + } + + return true; + } + + @Override + public Boolean visitWindowFrame(WindowFrame node, Void context) + { + Optional start = node.getStart().getValue(); + if (start.isPresent()) { + if (!process(start.get(), context)) { + throw new SemanticException(MUST_BE_AGGREGATE_OR_GROUP_BY, start.get(), "Window frame start must be an aggregate expression or appear in GROUP BY clause"); + } + } + if (node.getEnd().isPresent() && node.getEnd().get().getValue().isPresent()) { + Expression endValue = node.getEnd().get().getValue().get(); + if (!process(endValue, context)) { + throw new SemanticException(MUST_BE_AGGREGATE_OR_GROUP_BY, endValue, "Window frame end must be an aggregate expression or appear in GROUP BY clause"); + } + } + + return true; + } + + @Override + protected Boolean visitQualifiedNameReference(QualifiedNameReference node, Void context) + { + QualifiedName name = node.getName(); + + List fields = tupleDescriptor.resolveFields(name); + Preconditions.checkState(!fields.isEmpty(), "No fields for name '%s'", name); + Preconditions.checkState(fields.size() <= 1, "Found more than one field for name '%s': %s", name, fields); + + Field field = Iterables.getOnlyElement(fields); + return fieldIndexes.contains(tupleDescriptor.indexOf(field)); + } + + @Override + protected Boolean visitArithmeticUnary(ArithmeticUnaryExpression node, Void context) + { + return process(node.getValue(), context); + } + + @Override + protected Boolean visitNotExpression(NotExpression node, Void context) + { + return process(node.getValue(), context); + } + + @Override + protected Boolean visitLogicalBinaryExpression(LogicalBinaryExpression node, Void context) + { + return process(node.getLeft(), context) && process(node.getRight(), context); + } + + @Override + protected Boolean visitIfExpression(IfExpression node, Void context) + { + ImmutableList.Builder expressions = ImmutableList.builder() + .add(node.getCondition()) + .add(node.getTrueValue()); + + if (node.getFalseValue().isPresent()) { + expressions.add(node.getFalseValue().get()); + } + + return expressions.build().stream().allMatch(expression -> process(expression, context)); + } + + @Override + protected Boolean visitSimpleCaseExpression(SimpleCaseExpression node, Void context) + { + if (!process(node.getOperand(), context)) { + return false; + } + + for (WhenClause whenClause : node.getWhenClauses()) { + if (!process(whenClause.getOperand(), context) || !process(whenClause.getResult(), context)) { + return false; + } + } + + if (node.getDefaultValue().isPresent() && !process(node.getDefaultValue().get(), context)) { + return false; + } + + return true; + } + + @Override + protected Boolean visitSearchedCaseExpression(SearchedCaseExpression node, Void context) + { + for (WhenClause whenClause : node.getWhenClauses()) { + if (!process(whenClause.getOperand(), context) || !process(whenClause.getResult(), context)) { + return false; + } + } + + if (node.getDefaultValue().isPresent() && !process(node.getDefaultValue().get(), context)) { + return false; + } + + return true; + } + + @Override + public Boolean visitRow(Row node, final Void context) + { + return node.getItems().stream() + .allMatch(item -> process(item, context)); + } + + @Override + public Boolean process(Node node, @Nullable Void context) + { + if (expressions.stream().anyMatch(node::equals)) { + return true; + } + + return super.process(node, context); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/analyzer/Analysis.java b/presto-main/src/main/java/com/facebook/presto/sql/analyzer/Analysis.java new file mode 100644 index 00000000..03de388a --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/analyzer/Analysis.java @@ -0,0 +1,436 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.analyzer; + +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.QualifiedTableName; +import com.facebook.presto.metadata.TableHandle; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.CreateTableOption; +import com.facebook.presto.spi.InsertOption; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.tree.Delete; +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.FunctionCall; +import com.facebook.presto.sql.tree.InPredicate; +import com.facebook.presto.sql.tree.Join; +import com.facebook.presto.sql.tree.Node; +import com.facebook.presto.sql.tree.QualifiedName; +import com.facebook.presto.sql.tree.QualifiedNameReference; +import com.facebook.presto.sql.tree.Query; +import com.facebook.presto.sql.tree.QuerySpecification; +import com.facebook.presto.sql.tree.SampledRelation; +import com.facebook.presto.sql.tree.Table; +import com.google.common.base.Preconditions; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.SetMultimap; + +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class Analysis +{ + private Query query; + private String updateType; + + private final IdentityHashMap namedQueries = new IdentityHashMap<>(); + + private TupleDescriptor outputDescriptor; + private final IdentityHashMap outputDescriptors = new IdentityHashMap<>(); + private final IdentityHashMap> resolvedNames = new IdentityHashMap<>(); + + private final IdentityHashMap> aggregates = new IdentityHashMap<>(); + private final IdentityHashMap> groupByExpressions = new IdentityHashMap<>(); + private final IdentityHashMap where = new IdentityHashMap<>(); + private final IdentityHashMap having = new IdentityHashMap<>(); + private final IdentityHashMap> orderByExpressions = new IdentityHashMap<>(); + private final IdentityHashMap> outputExpressions = new IdentityHashMap<>(); + private final IdentityHashMap> windowFunctions = new IdentityHashMap<>(); + + private final IdentityHashMap joins = new IdentityHashMap<>(); + private final SetMultimap inPredicates = HashMultimap.create(); + private final IdentityHashMap joinInPredicates = new IdentityHashMap<>(); + + private final IdentityHashMap tables = new IdentityHashMap<>(); + + private final IdentityHashMap rowFieldReferences = new IdentityHashMap<>(); + private final IdentityHashMap types = new IdentityHashMap<>(); + private final IdentityHashMap coercions = new IdentityHashMap<>(); + private final IdentityHashMap functionInfo = new IdentityHashMap<>(); + + private final IdentityHashMap columns = new IdentityHashMap<>(); + + private final IdentityHashMap sampleRatios = new IdentityHashMap<>(); + + // for create table + private Optional createTableDestination = Optional.empty(); + private CreateTableOption createTableOption; + + // for insert + private Optional insertTarget = Optional.empty(); + private InsertOption insertOption; + + // for delete + private Optional delete = Optional.empty(); + + public Query getQuery() + { + return query; + } + + public void setQuery(Query query) + { + this.query = query; + } + + public String getUpdateType() + { + return updateType; + } + + public void setUpdateType(String updateType) + { + this.updateType = updateType; + } + + public void addResolvedNames(Expression expression, Map mappings) + { + resolvedNames.put(expression, mappings); + } + + public Map getResolvedNames(Expression expression) + { + return resolvedNames.get(expression); + } + + public void setAggregates(QuerySpecification node, List aggregates) + { + this.aggregates.put(node, aggregates); + } + + public List getAggregates(QuerySpecification query) + { + return aggregates.get(query); + } + + public IdentityHashMap getTypes() + { + return new IdentityHashMap<>(types); + } + + public boolean isRowFieldReference(QualifiedNameReference qualifiedNameReference) + { + return rowFieldReferences.containsKey(qualifiedNameReference); + } + + public Type getType(Expression expression) + { + Preconditions.checkArgument(types.containsKey(expression), "Expression not analyzed: %s", expression); + return types.get(expression); + } + + public Type getCoercion(Expression expression) + { + return coercions.get(expression); + } + + public void setGroupByExpressions(QuerySpecification node, List expressions) + { + groupByExpressions.put(node, expressions); + } + + public List getGroupByExpressions(QuerySpecification node) + { + return groupByExpressions.get(node); + } + + public void setWhere(Node node, Expression expression) + { + where.put(node, expression); + } + + public Expression getWhere(QuerySpecification node) + { + return where.get(node); + } + + public void setOrderByExpressions(Node node, List items) + { + orderByExpressions.put(node, items); + } + + public List getOrderByExpressions(Node node) + { + return orderByExpressions.get(node); + } + + public void setOutputExpressions(Node node, List expressions) + { + outputExpressions.put(node, expressions); + } + + public List getOutputExpressions(Node node) + { + return outputExpressions.get(node); + } + + public void setHaving(QuerySpecification node, Expression expression) + { + having.put(node, expression); + } + + public void setJoinCriteria(Join node, Expression criteria) + { + joins.put(node, criteria); + } + + public Expression getJoinCriteria(Join join) + { + return joins.get(join); + } + + public void addInPredicates(Node node, Set inPredicates) + { + this.inPredicates.putAll(node, inPredicates); + } + + public Set getInPredicates(Node node) + { + return inPredicates.get(node); + } + + public void addJoinInPredicates(Join node, JoinInPredicates joinInPredicates) + { + this.joinInPredicates.put(node, joinInPredicates); + } + + public JoinInPredicates getJoinInPredicates(Join node) + { + return joinInPredicates.get(node); + } + + public void setWindowFunctions(QuerySpecification node, List functions) + { + windowFunctions.put(node, functions); + } + + public Map> getWindowFunctions() + { + return windowFunctions; + } + + public List getWindowFunctions(QuerySpecification query) + { + return windowFunctions.get(query); + } + + public void setOutputDescriptor(TupleDescriptor descriptor) + { + outputDescriptor = descriptor; + } + + public TupleDescriptor getOutputDescriptor() + { + return outputDescriptor; + } + + public void setOutputDescriptor(Node node, TupleDescriptor descriptor) + { + outputDescriptors.put(node, descriptor); + } + + public TupleDescriptor getOutputDescriptor(Node node) + { + Preconditions.checkState(outputDescriptors.containsKey(node), "Output descriptor missing for %s. Broken analysis?", node); + return outputDescriptors.get(node); + } + + public TableHandle getTableHandle(Table table) + { + return tables.get(table); + } + + public void registerTable(Table table, TableHandle handle) + { + tables.put(table, handle); + } + + public FunctionInfo getFunctionInfo(FunctionCall function) + { + return functionInfo.get(function); + } + + public void addFunctionInfos(IdentityHashMap infos) + { + functionInfo.putAll(infos); + } + + public void addTypes(IdentityHashMap types) + { + this.types.putAll(types); + } + + public void addRowFieldReferences(IdentityHashMap rowFieldReferences) + { + this.rowFieldReferences.putAll(rowFieldReferences); + } + + public void addCoercion(Expression expression, Type type) + { + this.coercions.put(expression, type); + } + + public void addCoercions(IdentityHashMap coercions) + { + this.coercions.putAll(coercions); + } + + public Expression getHaving(QuerySpecification query) + { + return having.get(query); + } + + public void setColumn(Field field, ColumnHandle handle) + { + columns.put(field, handle); + } + + public ColumnHandle getColumn(Field field) + { + return columns.get(field); + } + + public void setCreateTableDestination(QualifiedTableName destination) + { + this.createTableDestination = Optional.of(destination); + } + + public void setCreateTableOption(CreateTableOption createTableOption) + { + this.createTableOption = createTableOption; + } + + public Optional getCreateTableDestination() + { + return createTableDestination; + } + + public CreateTableOption getCreateTableOption() + { + return createTableOption; + } + + public void setInsertTarget(TableHandle target) + { + this.insertTarget = Optional.of(target); + } + + public Optional getInsertTarget() + { + return insertTarget; + } + + public void setInsertOption(InsertOption insertOption) + { + this.insertOption = insertOption; + } + + public InsertOption getInsertOption() + { + return insertOption; + } + + public void setDelete(Delete delete) + { + this.delete = Optional.of(delete); + } + + public Optional getDelete() + { + return delete; + } + + public Query getNamedQuery(Table table) + { + return namedQueries.get(table); + } + + public void registerNamedQuery(Table tableReference, Query query) + { + checkNotNull(tableReference, "tableReference is null"); + checkNotNull(query, "query is null"); + + namedQueries.put(tableReference, query); + } + + public void setSampleRatio(SampledRelation relation, double ratio) + { + sampleRatios.put(relation, ratio); + } + + public double getSampleRatio(SampledRelation relation) + { + Preconditions.checkState(sampleRatios.containsKey(relation), "Sample ratio missing for %s. Broken analysis?", relation); + return sampleRatios.get(relation); + } + + public static class JoinInPredicates + { + private final Set leftInPredicates; + private final Set rightInPredicates; + + public JoinInPredicates(Set leftInPredicates, Set rightInPredicates) + { + this.leftInPredicates = ImmutableSet.copyOf(checkNotNull(leftInPredicates, "leftInPredicates is null")); + this.rightInPredicates = ImmutableSet.copyOf(checkNotNull(rightInPredicates, "rightInPredicates is null")); + } + + public Set getLeftInPredicates() + { + return leftInPredicates; + } + + public Set getRightInPredicates() + { + return rightInPredicates; + } + + @Override + public int hashCode() + { + return Objects.hash(leftInPredicates, rightInPredicates); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final JoinInPredicates other = (JoinInPredicates) obj; + return Objects.equals(this.leftInPredicates, other.leftInPredicates) && + Objects.equals(this.rightInPredicates, other.rightInPredicates); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/analyzer/AnalysisContext.java b/presto-main/src/main/java/com/facebook/presto/sql/analyzer/AnalysisContext.java new file mode 100644 index 00000000..cb5d8918 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/analyzer/AnalysisContext.java @@ -0,0 +1,81 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.analyzer; + +import com.facebook.presto.sql.tree.Query; +import com.google.common.base.Preconditions; + +import java.util.HashMap; +import java.util.Map; + +public class AnalysisContext +{ + private final AnalysisContext parent; + private final Map namedQueries = new HashMap<>(); + private TupleDescriptor lateralTupleDescriptor = new TupleDescriptor(); + private boolean approximate; + + public AnalysisContext(AnalysisContext parent) + { + this.parent = parent; + this.approximate = parent.approximate; + } + + public AnalysisContext() + { + parent = null; + } + + public void setLateralTupleDescriptor(TupleDescriptor lateralTupleDescriptor) + { + this.lateralTupleDescriptor = lateralTupleDescriptor; + } + + public TupleDescriptor getLateralTupleDescriptor() + { + return lateralTupleDescriptor; + } + + public boolean isApproximate() + { + return approximate; + } + + public void setApproximate(boolean approximate) + { + this.approximate = approximate; + } + + public void addNamedQuery(String name, Query query) + { + Preconditions.checkState(!namedQueries.containsKey(name), "Named query already registered: %s", name); + namedQueries.put(name, query); + } + + public Query getNamedQuery(String name) + { + Query result = namedQueries.get(name); + + if (result == null && parent != null) { + return parent.getNamedQuery(name); + } + + return result; + } + + public boolean isNamedQueryDeclared(String name) + { + return namedQueries.containsKey(name); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/analyzer/Analyzer.java b/presto-main/src/main/java/com/facebook/presto/sql/analyzer/Analyzer.java new file mode 100644 index 00000000..5ee64e91 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/analyzer/Analyzer.java @@ -0,0 +1,71 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.analyzer; + +import com.facebook.presto.Session; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.sql.parser.SqlParser; +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.FunctionCall; +import com.facebook.presto.sql.tree.Statement; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; + +import java.util.List; +import java.util.Optional; + +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.CANNOT_HAVE_AGGREGATIONS_OR_WINDOWS; +import static com.google.common.base.Preconditions.checkNotNull; + +public class Analyzer +{ + private final Metadata metadata; + private final SqlParser sqlParser; + private final Session session; + private final Optional queryExplainer; + private final boolean experimentalSyntaxEnabled; + + public Analyzer(Session session, Metadata metadata, SqlParser sqlParser, Optional queryExplainer, boolean experimentalSyntaxEnabled) + { + this.session = checkNotNull(session, "session is null"); + this.metadata = checkNotNull(metadata, "metadata is null"); + this.sqlParser = checkNotNull(sqlParser, "sqlParser is null"); + this.queryExplainer = checkNotNull(queryExplainer, "query explainer is null"); + this.experimentalSyntaxEnabled = experimentalSyntaxEnabled; + } + + public Analysis analyze(Statement statement) + { + Analysis analysis = new Analysis(); + StatementAnalyzer analyzer = new StatementAnalyzer(analysis, metadata, sqlParser, session, experimentalSyntaxEnabled, queryExplainer); + TupleDescriptor outputDescriptor = analyzer.process(statement, new AnalysisContext()); + analysis.setOutputDescriptor(outputDescriptor); + return analysis; + } + + static void verifyNoAggregatesOrWindowFunctions(Metadata metadata, Expression predicate, String clause) + { + AggregateExtractor extractor = new AggregateExtractor(metadata); + extractor.process(predicate, null); + + WindowFunctionExtractor windowExtractor = new WindowFunctionExtractor(); + windowExtractor.process(predicate, null); + + List found = ImmutableList.copyOf(Iterables.concat(extractor.getAggregates(), windowExtractor.getWindowFunctions())); + + if (!found.isEmpty()) { + throw new SemanticException(CANNOT_HAVE_AGGREGATIONS_OR_WINDOWS, predicate, "%s clause cannot contain aggregations or window functions: %s", clause, found); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/analyzer/ExpressionAnalysis.java b/presto-main/src/main/java/com/facebook/presto/sql/analyzer/ExpressionAnalysis.java new file mode 100644 index 00000000..704b714c --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/analyzer/ExpressionAnalysis.java @@ -0,0 +1,60 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.analyzer; + +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.InPredicate; + +import java.util.IdentityHashMap; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class ExpressionAnalysis +{ + private final IdentityHashMap expressionTypes; + private final IdentityHashMap expressionCoercions; + private final Set subqueryInPredicates; + + public ExpressionAnalysis( + IdentityHashMap expressionTypes, + IdentityHashMap expressionCoercions, + Set subqueryInPredicates) + { + this.expressionTypes = checkNotNull(expressionTypes, "expressionTypes is null"); + this.expressionCoercions = checkNotNull(expressionCoercions, "expressionCoercions is null"); + this.subqueryInPredicates = checkNotNull(subqueryInPredicates, "subqueryInPredicates is null"); + } + + public Type getType(Expression expression) + { + return expressionTypes.get(expression); + } + + public IdentityHashMap getExpressionTypes() + { + return expressionTypes; + } + + public Type getCoercion(Expression expression) + { + return expressionCoercions.get(expression); + } + + public Set getSubqueryInPredicates() + { + return subqueryInPredicates; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/analyzer/ExpressionAnalyzer.java b/presto-main/src/main/java/com/facebook/presto/sql/analyzer/ExpressionAnalyzer.java new file mode 100644 index 00000000..fdce9159 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/analyzer/ExpressionAnalyzer.java @@ -0,0 +1,1052 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.analyzer; + +import com.facebook.presto.Session; +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.metadata.OperatorNotFoundException; +import com.facebook.presto.metadata.OperatorType; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.spi.type.TypeSignature; +import com.facebook.presto.sql.parser.SqlParser; +import com.facebook.presto.sql.planner.DependencyExtractor; +import com.facebook.presto.sql.planner.Symbol; +import com.facebook.presto.sql.tree.ArithmeticBinaryExpression; +import com.facebook.presto.sql.tree.ArithmeticUnaryExpression; +import com.facebook.presto.sql.tree.ArrayConstructor; +import com.facebook.presto.sql.tree.AstVisitor; +import com.facebook.presto.sql.tree.BetweenPredicate; +import com.facebook.presto.sql.tree.BooleanLiteral; +import com.facebook.presto.sql.tree.Cast; +import com.facebook.presto.sql.tree.CoalesceExpression; +import com.facebook.presto.sql.tree.ComparisonExpression; +import com.facebook.presto.sql.tree.CurrentTime; +import com.facebook.presto.sql.tree.DefaultTraversalVisitor; +import com.facebook.presto.sql.tree.DoubleLiteral; +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.Extract; +import com.facebook.presto.sql.tree.FunctionCall; +import com.facebook.presto.sql.tree.GenericLiteral; +import com.facebook.presto.sql.tree.IfExpression; +import com.facebook.presto.sql.tree.InListExpression; +import com.facebook.presto.sql.tree.InPredicate; +import com.facebook.presto.sql.tree.InputReference; +import com.facebook.presto.sql.tree.IntervalLiteral; +import com.facebook.presto.sql.tree.IsNotNullPredicate; +import com.facebook.presto.sql.tree.IsNullPredicate; +import com.facebook.presto.sql.tree.LikePredicate; +import com.facebook.presto.sql.tree.LogicalBinaryExpression; +import com.facebook.presto.sql.tree.LongLiteral; +import com.facebook.presto.sql.tree.Node; +import com.facebook.presto.sql.tree.NotExpression; +import com.facebook.presto.sql.tree.NullIfExpression; +import com.facebook.presto.sql.tree.NullLiteral; +import com.facebook.presto.sql.tree.QualifiedName; +import com.facebook.presto.sql.tree.QualifiedNameReference; +import com.facebook.presto.sql.tree.Row; +import com.facebook.presto.sql.tree.SearchedCaseExpression; +import com.facebook.presto.sql.tree.SimpleCaseExpression; +import com.facebook.presto.sql.tree.SortItem; +import com.facebook.presto.sql.tree.StringLiteral; +import com.facebook.presto.sql.tree.SubqueryExpression; +import com.facebook.presto.sql.tree.SubscriptExpression; +import com.facebook.presto.sql.tree.TimeLiteral; +import com.facebook.presto.sql.tree.TimestampLiteral; +import com.facebook.presto.sql.tree.WhenClause; +import com.facebook.presto.sql.tree.WindowFrame; +import com.facebook.presto.type.RowType; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; + +import javax.annotation.Nullable; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; + +import static com.facebook.presto.metadata.FunctionRegistry.canCoerce; +import static com.facebook.presto.metadata.FunctionRegistry.getCommonSuperType; +import static com.facebook.presto.metadata.OperatorType.SUBSCRIPT; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.BooleanType.BOOLEAN; +import static com.facebook.presto.spi.type.DateType.DATE; +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; +import static com.facebook.presto.spi.type.IntervalDayTimeType.INTERVAL_DAY_TIME; +import static com.facebook.presto.spi.type.IntervalYearMonthType.INTERVAL_YEAR_MONTH; +import static com.facebook.presto.spi.type.TimeType.TIME; +import static com.facebook.presto.spi.type.TimeWithTimeZoneType.TIME_WITH_TIME_ZONE; +import static com.facebook.presto.spi.type.TimestampType.TIMESTAMP; +import static com.facebook.presto.spi.type.TimestampWithTimeZoneType.TIMESTAMP_WITH_TIME_ZONE; +import static com.facebook.presto.spi.type.TypeSignature.parseTypeSignature; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.AMBIGUOUS_ATTRIBUTE; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.MISSING_ATTRIBUTE; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.MULTIPLE_FIELDS_FROM_SCALAR_SUBQUERY; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.NOT_SUPPORTED; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.TYPE_MISMATCH; +import static com.facebook.presto.sql.tree.Extract.Field.TIMEZONE_HOUR; +import static com.facebook.presto.sql.tree.Extract.Field.TIMEZONE_MINUTE; +import static com.facebook.presto.type.ArrayParametricType.ARRAY; +import static com.facebook.presto.type.RowType.RowField; +import static com.facebook.presto.type.UnknownType.UNKNOWN; +import static com.facebook.presto.util.DateTimeUtils.timeHasTimeZone; +import static com.facebook.presto.util.DateTimeUtils.timestampHasTimeZone; +import static com.facebook.presto.util.ImmutableCollectors.toImmutableList; +import static com.facebook.presto.util.Types.checkType; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.Sets.newIdentityHashSet; + +public class ExpressionAnalyzer +{ + private final FunctionRegistry functionRegistry; + private final TypeManager typeManager; + private final Function statementAnalyzerFactory; + private final Map resolvedNames = new HashMap<>(); + private final IdentityHashMap resolvedFunctions = new IdentityHashMap<>(); + private final IdentityHashMap expressionTypes = new IdentityHashMap<>(); + private final IdentityHashMap expressionCoercions = new IdentityHashMap<>(); + private final IdentityHashMap rowFieldReferences = new IdentityHashMap<>(); + private final Set subqueryInPredicates = newIdentityHashSet(); + + public ExpressionAnalyzer(FunctionRegistry functionRegistry, TypeManager typeManager, Function statementAnalyzerFactory) + { + this.functionRegistry = checkNotNull(functionRegistry, "functionRegistry is null"); + this.typeManager = checkNotNull(typeManager, "typeManager is null"); + this.statementAnalyzerFactory = checkNotNull(statementAnalyzerFactory, "statementAnalyzerFactory is null"); + } + + public Map getResolvedNames() + { + return resolvedNames; + } + + public IdentityHashMap getResolvedFunctions() + { + return resolvedFunctions; + } + + public IdentityHashMap getExpressionTypes() + { + return expressionTypes; + } + + public IdentityHashMap getExpressionCoercions() + { + return expressionCoercions; + } + + public IdentityHashMap getRowFieldReferences() + { + return rowFieldReferences; + } + + public Set getSubqueryInPredicates() + { + return subqueryInPredicates; + } + + /** + * @param tupleDescriptor the tuple descriptor to use to resolve QualifiedNames + * @param context the namespace context of the surrounding query + */ + public Type analyze(Expression expression, TupleDescriptor tupleDescriptor, AnalysisContext context) + { + ScalarSubqueryDetector scalarSubqueryDetector = new ScalarSubqueryDetector(); + expression.accept(scalarSubqueryDetector, null); + + Visitor visitor = new Visitor(tupleDescriptor); + return expression.accept(visitor, context); + } + + private static class ScalarSubqueryDetector + extends DefaultTraversalVisitor + { + @Override + protected Void visitInPredicate(InPredicate node, Void context) + { + Expression valueList = node.getValueList(); + if (valueList instanceof SubqueryExpression) { + process(node.getValue(), context); + super.visitSubqueryExpression((SubqueryExpression) valueList, context); + } + else { + super.visitInPredicate(node, context); + } + return null; + } + + @Override + protected Void visitSubqueryExpression(SubqueryExpression node, Void context) + { + throw new SemanticException(NOT_SUPPORTED, node, "Scalar subqueries not yet supported"); + } + } + + private class Visitor + extends AstVisitor + { + private final TupleDescriptor tupleDescriptor; + + private Visitor(TupleDescriptor tupleDescriptor) + { + this.tupleDescriptor = checkNotNull(tupleDescriptor, "tupleDescriptor is null"); + } + + @SuppressWarnings("SuspiciousMethodCalls") + @Override + public Type process(Node node, @Nullable AnalysisContext context) + { + // don't double processs a node + Type type = expressionTypes.get(node); + if (type != null) { + return type; + } + return super.process(node, context); + } + + @Override + protected Type visitRow(Row node, AnalysisContext context) + { + List types = node.getItems().stream() + .map((child) -> process(child, context)) + .collect(toImmutableList()); + + Type type = new RowType(types, Optional.empty()); + expressionTypes.put(node, type); + + return type; + } + + @Override + protected Type visitCurrentTime(CurrentTime node, AnalysisContext context) + { + if (node.getPrecision() != null) { + throw new SemanticException(NOT_SUPPORTED, node, "non-default precision not yet supported"); + } + + Type type; + switch (node.getType()) { + case DATE: + type = DATE; + break; + case TIME: + type = TIME_WITH_TIME_ZONE; + break; + case LOCALTIME: + type = TIME; + break; + case TIMESTAMP: + type = TIMESTAMP_WITH_TIME_ZONE; + break; + case LOCALTIMESTAMP: + type = TIMESTAMP; + break; + default: + throw new SemanticException(NOT_SUPPORTED, node, "%s not yet supported", node.getType().getName()); + } + + expressionTypes.put(node, type); + return type; + } + + @Override + protected Type visitQualifiedNameReference(QualifiedNameReference node, AnalysisContext context) + { + List matches = tupleDescriptor.resolveFields(node.getName()); + if (matches.isEmpty()) { + // TODO This is kind of hacky, instead we should change the way QualifiedNameReferences are parsed + return tryVisitRowFieldAccessor(node); + } + if (matches.size() > 1) { + throw new SemanticException(AMBIGUOUS_ATTRIBUTE, node, "Column '%s' is ambiguous", node.getName()); + } + + Field field = Iterables.getOnlyElement(matches); + int fieldIndex = tupleDescriptor.indexOf(field); + resolvedNames.put(node.getName(), fieldIndex); + expressionTypes.put(node, field.getType()); + + return field.getType(); + } + + private Type tryVisitRowFieldAccessor(QualifiedNameReference node) + { + if (node.getName().getParts().size() < 2) { + throw createMissingAttributeException(node); + } + QualifiedName base = new QualifiedName(node.getName().getParts().subList(0, node.getName().getParts().size() - 1)); + List matches = tupleDescriptor.resolveFields(base); + if (matches.isEmpty()) { + throw createMissingAttributeException(node); + } + if (matches.size() > 1) { + throw new SemanticException(AMBIGUOUS_ATTRIBUTE, node, "Column '%s' is ambiguous", node.getName()); + } + + Field field = Iterables.getOnlyElement(matches); + if (field.getType() instanceof RowType) { + RowType rowType = checkType(field.getType(), RowType.class, "field.getType()"); + Type rowFieldType = null; + for (RowField rowField : rowType.getFields()) { + if (rowField.getName().equals(Optional.of(node.getName().getSuffix()))) { + rowFieldType = rowField.getType(); + break; + } + } + if (rowFieldType == null) { + throw createMissingAttributeException(node); + } + int fieldIndex = tupleDescriptor.indexOf(field); + resolvedNames.put(node.getName(), fieldIndex); + expressionTypes.put(node, rowFieldType); + rowFieldReferences.put(node, true); + + return rowFieldType; + } + throw createMissingAttributeException(node); + } + + private SemanticException createMissingAttributeException(QualifiedNameReference node) + { + return new SemanticException(MISSING_ATTRIBUTE, node, "Column '%s' cannot be resolved", node.getName()); + } + + @Override + protected Type visitNotExpression(NotExpression node, AnalysisContext context) + { + coerceType(context, node.getValue(), BOOLEAN, "Value of logical NOT expression"); + + expressionTypes.put(node, BOOLEAN); + return BOOLEAN; + } + + @Override + protected Type visitLogicalBinaryExpression(LogicalBinaryExpression node, AnalysisContext context) + { + coerceType(context, node.getLeft(), BOOLEAN, "Left side of logical expression"); + coerceType(context, node.getRight(), BOOLEAN, "Right side of logical expression"); + + expressionTypes.put(node, BOOLEAN); + return BOOLEAN; + } + + @Override + protected Type visitComparisonExpression(ComparisonExpression node, AnalysisContext context) + { + OperatorType operatorType; + if (node.getType() == ComparisonExpression.Type.IS_DISTINCT_FROM) { + operatorType = OperatorType.EQUAL; + } + else { + operatorType = OperatorType.valueOf(node.getType().name()); + } + return getOperator(context, node, operatorType, node.getLeft(), node.getRight()); + } + + @Override + protected Type visitIsNullPredicate(IsNullPredicate node, AnalysisContext context) + { + process(node.getValue(), context); + + expressionTypes.put(node, BOOLEAN); + return BOOLEAN; + } + + @Override + protected Type visitIsNotNullPredicate(IsNotNullPredicate node, AnalysisContext context) + { + process(node.getValue(), context); + + expressionTypes.put(node, BOOLEAN); + return BOOLEAN; + } + + @Override + protected Type visitNullIfExpression(NullIfExpression node, AnalysisContext context) + { + Type firstType = process(node.getFirst(), context); + Type secondType = process(node.getSecond(), context); + + if (!getCommonSuperType(firstType, secondType).isPresent()) { + throw new SemanticException(TYPE_MISMATCH, node, "Types are not comparable with NULLIF: %s vs %s", firstType, secondType); + } + + expressionTypes.put(node, firstType); + return firstType; + } + + @Override + protected Type visitIfExpression(IfExpression node, AnalysisContext context) + { + coerceType(context, node.getCondition(), BOOLEAN, "IF condition"); + + Type type; + if (node.getFalseValue().isPresent()) { + type = coerceToSingleType(context, node, "Result types for IF must be the same: %s vs %s", node.getTrueValue(), node.getFalseValue().get()); + } + else { + type = process(node.getTrueValue(), context); + } + + expressionTypes.put(node, type); + return type; + } + + @Override + protected Type visitSearchedCaseExpression(SearchedCaseExpression node, AnalysisContext context) + { + for (WhenClause whenClause : node.getWhenClauses()) { + coerceType(context, whenClause.getOperand(), BOOLEAN, "CASE WHEN clause"); + } + + Type type = coerceToSingleType(context, + "All CASE results must be the same type: %s", + getCaseResultExpressions(node.getWhenClauses(), node.getDefaultValue())); + expressionTypes.put(node, type); + + for (WhenClause whenClause : node.getWhenClauses()) { + Type whenClauseType = process(whenClause.getResult(), context); + checkNotNull(whenClauseType, "Expression types does not contain an entry for %s", whenClause); + expressionTypes.put(whenClause, whenClauseType); + } + + return type; + } + + @Override + protected Type visitSimpleCaseExpression(SimpleCaseExpression node, AnalysisContext context) + { + for (WhenClause whenClause : node.getWhenClauses()) { + coerceToSingleType(context, node, "CASE operand type does not match WHEN clause operand type: %s vs %s", node.getOperand(), whenClause.getOperand()); + } + + Type type = coerceToSingleType(context, + "All CASE results must be the same type: %s", + getCaseResultExpressions(node.getWhenClauses(), node.getDefaultValue())); + expressionTypes.put(node, type); + + for (WhenClause whenClause : node.getWhenClauses()) { + Type whenClauseType = process(whenClause.getResult(), context); + checkNotNull(whenClauseType, "Expression types does not contain an entry for %s", whenClause); + expressionTypes.put(whenClause, whenClauseType); + } + + return type; + } + + private List getCaseResultExpressions(List whenClauses, Optional defaultValue) + { + List resultExpressions = new ArrayList<>(); + for (WhenClause whenClause : whenClauses) { + resultExpressions.add(whenClause.getResult()); + } + defaultValue.ifPresent(resultExpressions::add); + return resultExpressions; + } + + @Override + protected Type visitCoalesceExpression(CoalesceExpression node, AnalysisContext context) + { + Type type = coerceToSingleType(context, "All COALESCE operands must be the same type: %s", node.getOperands()); + + expressionTypes.put(node, type); + return type; + } + + @Override + protected Type visitArithmeticUnary(ArithmeticUnaryExpression node, AnalysisContext context) + { + switch (node.getSign()) { + case PLUS: + Type type = process(node.getValue(), context); + + if (!type.equals(BIGINT) && !type.equals(DOUBLE)) { + // TODO: figure out a type-agnostic way of dealing with this. Maybe add a special unary operator + // that types can chose to implement, or piggyback on the existence of the negation operator + throw new SemanticException(TYPE_MISMATCH, node, "Unary '+' operator cannot by applied to %s type", type); + } + expressionTypes.put(node, type); + return type; + case MINUS: + return getOperator(context, node, OperatorType.NEGATION, node.getValue()); + } + + throw new UnsupportedOperationException("Unsupported unary operator: " + node.getSign()); + } + + @Override + protected Type visitArithmeticBinary(ArithmeticBinaryExpression node, AnalysisContext context) + { + return getOperator(context, node, OperatorType.valueOf(node.getType().name()), node.getLeft(), node.getRight()); + } + + @Override + protected Type visitLikePredicate(LikePredicate node, AnalysisContext context) + { + coerceType(context, node.getValue(), VARCHAR, "Left side of LIKE expression"); + coerceType(context, node.getPattern(), VARCHAR, "Pattern for LIKE expression"); + if (node.getEscape() != null) { + coerceType(context, node.getEscape(), VARCHAR, "Escape for LIKE expression"); + } + + expressionTypes.put(node, BOOLEAN); + return BOOLEAN; + } + + @Override + protected Type visitSubscriptExpression(SubscriptExpression node, AnalysisContext context) + { + return getOperator(context, node, SUBSCRIPT, node.getBase(), node.getIndex()); + } + + @Override + protected Type visitArrayConstructor(ArrayConstructor node, AnalysisContext context) + { + Type type = coerceToSingleType(context, "All ARRAY elements must be the same type: %s", node.getValues()); + Type arrayType = typeManager.getParameterizedType(ARRAY.getName(), ImmutableList.of(type.getTypeSignature()), ImmutableList.of()); + expressionTypes.put(node, arrayType); + return arrayType; + } + + @Override + protected Type visitStringLiteral(StringLiteral node, AnalysisContext context) + { + expressionTypes.put(node, VARCHAR); + return VARCHAR; + } + + @Override + protected Type visitLongLiteral(LongLiteral node, AnalysisContext context) + { + expressionTypes.put(node, BIGINT); + return BIGINT; + } + + @Override + protected Type visitDoubleLiteral(DoubleLiteral node, AnalysisContext context) + { + expressionTypes.put(node, DOUBLE); + return DOUBLE; + } + + @Override + protected Type visitBooleanLiteral(BooleanLiteral node, AnalysisContext context) + { + expressionTypes.put(node, BOOLEAN); + return BOOLEAN; + } + + @Override + protected Type visitGenericLiteral(GenericLiteral node, AnalysisContext context) + { + Type type = typeManager.getType(parseTypeSignature(node.getType())); + if (type == null) { + throw new SemanticException(TYPE_MISMATCH, node, "Unknown type: " + node.getType()); + } + + try { + functionRegistry.getCoercion(VARCHAR, type); + } + catch (IllegalArgumentException e) { + throw new SemanticException(TYPE_MISMATCH, node, "No literal form for type %s", type); + } + + expressionTypes.put(node, type); + return type; + } + + @Override + protected Type visitTimeLiteral(TimeLiteral node, AnalysisContext context) + { + Type type; + if (timeHasTimeZone(node.getValue())) { + type = TIME_WITH_TIME_ZONE; + } + else { + type = TIME; + } + expressionTypes.put(node, type); + return type; + } + + @Override + protected Type visitTimestampLiteral(TimestampLiteral node, AnalysisContext context) + { + Type type; + if (timestampHasTimeZone(node.getValue())) { + type = TIMESTAMP_WITH_TIME_ZONE; + } + else { + type = TIMESTAMP; + } + expressionTypes.put(node, type); + return type; + } + + @Override + protected Type visitIntervalLiteral(IntervalLiteral node, AnalysisContext context) + { + Type type; + if (node.isYearToMonth()) { + type = INTERVAL_YEAR_MONTH; + } + else { + type = INTERVAL_DAY_TIME; + } + expressionTypes.put(node, type); + return type; + } + + @Override + protected Type visitNullLiteral(NullLiteral node, AnalysisContext context) + { + expressionTypes.put(node, UNKNOWN); + return UNKNOWN; + } + + @Override + protected Type visitFunctionCall(FunctionCall node, AnalysisContext context) + { + if (node.getWindow().isPresent()) { + for (Expression expression : node.getWindow().get().getPartitionBy()) { + process(expression, context); + Type type = expressionTypes.get(expression); + if (!type.isComparable()) { + throw new SemanticException(TYPE_MISMATCH, node, "%s is not comparable, and therefore cannot be used in window function PARTITION BY", type); + } + } + + for (SortItem sortItem : node.getWindow().get().getOrderBy()) { + process(sortItem.getSortKey(), context); + Type type = expressionTypes.get(sortItem.getSortKey()); + if (!type.isOrderable()) { + throw new SemanticException(TYPE_MISMATCH, node, "%s is not orderable, and therefore cannot be used in window function ORDER BY", type); + } + } + + if (node.getWindow().get().getFrame().isPresent()) { + WindowFrame frame = node.getWindow().get().getFrame().get(); + + if (frame.getStart().getValue().isPresent()) { + Type type = process(frame.getStart().getValue().get(), context); + if (!type.equals(BIGINT)) { + throw new SemanticException(TYPE_MISMATCH, node, "Window frame start value type must be BIGINT (actual %s)", type); + } + } + + if (frame.getEnd().isPresent() && frame.getEnd().get().getValue().isPresent()) { + Type type = process(frame.getEnd().get().getValue().get(), context); + if (!type.equals(BIGINT)) { + throw new SemanticException(TYPE_MISMATCH, node, "Window frame end value type must be BIGINT (actual %s)", type); + } + } + } + } + + ImmutableList.Builder argumentTypes = ImmutableList.builder(); + for (Expression expression : node.getArguments()) { + argumentTypes.add(process(expression, context).getTypeSignature()); + } + + FunctionInfo function = functionRegistry.resolveFunction(node.getName(), argumentTypes.build(), context.isApproximate()); + for (int i = 0; i < node.getArguments().size(); i++) { + Expression expression = node.getArguments().get(i); + Type type = typeManager.getType(function.getArgumentTypes().get(i)); + checkNotNull(type, "Type %s not found", function.getArgumentTypes().get(i)); + if (node.isDistinct() && !type.isComparable()) { + throw new SemanticException(TYPE_MISMATCH, node, "DISTINCT can only be applied to comparable types (actual: %s)", type); + } + coerceType(context, expression, type, String.format("Function %s argument %d", function.getSignature(), i)); + } + resolvedFunctions.put(node, function); + + Type type = typeManager.getType(function.getReturnType()); + expressionTypes.put(node, type); + + return type; + } + + @Override + protected Type visitExtract(Extract node, AnalysisContext context) + { + Type type = process(node.getExpression(), context); + if (!isDateTimeType(type)) { + throw new SemanticException(TYPE_MISMATCH, node.getExpression(), "Type of argument to extract must be DATE, TIME, TIMESTAMP, or INTERVAL (actual %s)", type); + } + Extract.Field field = node.getField(); + if ((field == TIMEZONE_HOUR || field == TIMEZONE_MINUTE) && !(type.equals(TIME_WITH_TIME_ZONE) || type.equals(TIMESTAMP_WITH_TIME_ZONE))) { + throw new SemanticException(TYPE_MISMATCH, node.getExpression(), "Type of argument to extract time zone field must have a time zone (actual %s)", type); + } + + expressionTypes.put(node, BIGINT); + return BIGINT; + } + + private boolean isDateTimeType(Type type) + { + return type.equals(DATE) || + type.equals(TIME) || + type.equals(TIME_WITH_TIME_ZONE) || + type.equals(TIMESTAMP) || + type.equals(TIMESTAMP_WITH_TIME_ZONE) || + type.equals(INTERVAL_DAY_TIME) || + type.equals(INTERVAL_YEAR_MONTH); + } + + @Override + protected Type visitBetweenPredicate(BetweenPredicate node, AnalysisContext context) + { + return getOperator(context, node, OperatorType.BETWEEN, node.getValue(), node.getMin(), node.getMax()); + } + + @Override + public Type visitCast(Cast node, AnalysisContext context) + { + Type type = typeManager.getType(parseTypeSignature(node.getType())); + if (type == null) { + throw new SemanticException(TYPE_MISMATCH, node, "Unknown type: " + node.getType()); + } + + if (type.equals(UNKNOWN)) { + throw new SemanticException(TYPE_MISMATCH, node, "UNKNOWN is not a valid type"); + } + + Type value = process(node.getExpression(), context); + if (!value.equals(UNKNOWN)) { + try { + functionRegistry.getCoercion(value, type); + } + catch (OperatorNotFoundException e) { + throw new SemanticException(TYPE_MISMATCH, node, "Cannot cast %s to %s", value, type); + } + } + + expressionTypes.put(node, type); + return type; + } + + @Override + protected Type visitInPredicate(InPredicate node, AnalysisContext context) + { + Expression value = node.getValue(); + process(value, context); + + Expression valueList = node.getValueList(); + process(valueList, context); + + if (valueList instanceof InListExpression) { + InListExpression inListExpression = (InListExpression) valueList; + + coerceToSingleType(context, + "IN value and list items must be the same type: %s", + ImmutableList.builder().add(value).addAll(inListExpression.getValues()).build()); + } + else if (valueList instanceof SubqueryExpression) { + coerceToSingleType(context, node, "value and result of subquery must be of the same type for IN expression: %s vs %s", value, valueList); + subqueryInPredicates.add(node); + } + + expressionTypes.put(node, BOOLEAN); + return BOOLEAN; + } + + @Override + protected Type visitInListExpression(InListExpression node, AnalysisContext context) + { + Type type = coerceToSingleType(context, "All IN list values must be the same type: %s", node.getValues()); + + expressionTypes.put(node, type); + return type; // TODO: this really should a be relation type + } + + @Override + protected Type visitSubqueryExpression(SubqueryExpression node, AnalysisContext context) + { + StatementAnalyzer analyzer = statementAnalyzerFactory.apply(node); + TupleDescriptor descriptor = analyzer.process(node.getQuery(), context); + + // Scalar subqueries should only produce one column + if (descriptor.getVisibleFieldCount() != 1) { + throw new SemanticException(MULTIPLE_FIELDS_FROM_SCALAR_SUBQUERY, + node, + "Subquery expression must produce only one field. Found %s", + descriptor.getVisibleFieldCount()); + } + + Type type = Iterables.getOnlyElement(descriptor.getVisibleFields()).getType(); + + expressionTypes.put(node, type); + return type; + } + + @Override + public Type visitInputReference(InputReference node, AnalysisContext context) + { + Type type = tupleDescriptor.getFieldByIndex(node.getChannel()).getType(); + expressionTypes.put(node, type); + return type; + } + + @Override + protected Type visitExpression(Expression node, AnalysisContext context) + { + throw new SemanticException(NOT_SUPPORTED, node, "not yet implemented: " + node.getClass().getName()); + } + + private Type getOperator(AnalysisContext context, Expression node, OperatorType operatorType, Expression... arguments) + { + ImmutableList.Builder argumentTypes = ImmutableList.builder(); + for (Expression expression : arguments) { + argumentTypes.add(process(expression, context)); + } + + FunctionInfo operatorInfo; + try { + operatorInfo = functionRegistry.resolveOperator(operatorType, argumentTypes.build()); + } + catch (OperatorNotFoundException e) { + throw new SemanticException(TYPE_MISMATCH, node, e.getMessage()); + } + + for (int i = 0; i < arguments.length; i++) { + Expression expression = arguments[i]; + Type type = typeManager.getType(operatorInfo.getArgumentTypes().get(i)); + coerceType(context, expression, type, String.format("Operator %s argument %d", operatorInfo, i)); + } + + Type type = typeManager.getType(operatorInfo.getReturnType()); + expressionTypes.put(node, type); + + return type; + } + + private void coerceType(AnalysisContext context, Expression expression, Type expectedType, String message) + { + Type actualType = process(expression, context); + if (!actualType.equals(expectedType)) { + if (!canCoerce(actualType, expectedType)) { + throw new SemanticException(TYPE_MISMATCH, expression, message + " must evaluate to a %s (actual: %s)", expectedType, actualType); + } + expressionCoercions.put(expression, expectedType); + } + } + + private Type coerceToSingleType(AnalysisContext context, Node node, String message, Expression first, Expression second) + { + Type firstType = null; + if (first != null) { + firstType = process(first, context); + } + Type secondType = null; + if (second != null) { + secondType = process(second, context); + } + + if (firstType == null) { + return secondType; + } + if (secondType == null) { + return firstType; + } + if (firstType.equals(secondType)) { + return firstType; + } + + // coerce types if possible + if (canCoerce(firstType, secondType)) { + expressionCoercions.put(first, secondType); + return secondType; + } + if (canCoerce(secondType, firstType)) { + expressionCoercions.put(second, firstType); + return firstType; + } + throw new SemanticException(TYPE_MISMATCH, node, message, firstType, secondType); + } + + private Type coerceToSingleType(AnalysisContext context, String message, List expressions) + { + // determine super type + Type superType = UNKNOWN; + for (Expression expression : expressions) { + Optional newSuperType = getCommonSuperType(superType, process(expression, context)); + if (!newSuperType.isPresent()) { + throw new SemanticException(TYPE_MISMATCH, expression, message, superType); + } + superType = newSuperType.get(); + } + + // verify all expressions can be coerced to the superType + for (Expression expression : expressions) { + Type type = process(expression, context); + if (!type.equals(superType)) { + if (!canCoerce(type, superType)) { + throw new SemanticException(TYPE_MISMATCH, expression, message, superType); + } + expressionCoercions.put(expression, superType); + } + } + + return superType; + } + } + + public static IdentityHashMap getExpressionTypes( + Session session, + Metadata metadata, + SqlParser sqlParser, + Map types, + Expression expression) + { + return getExpressionTypes(session, metadata, sqlParser, types, ImmutableList.of(expression)); + } + + public static IdentityHashMap getExpressionTypes( + Session session, + Metadata metadata, + SqlParser sqlParser, + Map types, + Iterable expressions) + { + return analyzeExpressionsWithSymbols(session, metadata, sqlParser, types, expressions).getExpressionTypes(); + } + + public static IdentityHashMap getExpressionTypesFromInput( + Session session, + Metadata metadata, + SqlParser sqlParser, + Map types, + Expression expression) + { + return getExpressionTypesFromInput(session, metadata, sqlParser, types, ImmutableList.of(expression)); + } + + public static IdentityHashMap getExpressionTypesFromInput( + Session session, + Metadata metadata, + SqlParser sqlParser, + Map types, + Iterable expressions) + { + return analyzeExpressionsWithInputs(session, metadata, sqlParser, types, expressions).getExpressionTypes(); + } + + public static ExpressionAnalysis analyzeExpressionsWithSymbols( + Session session, + Metadata metadata, + SqlParser sqlParser, + Map types, + Iterable expressions) + { + List fields = DependencyExtractor.extractUnique(expressions).stream() + .map(symbol -> { + Type type = types.get(symbol); + checkArgument(type != null, "No type for symbol %s", symbol); + return Field.newUnqualified(symbol.getName(), type); + }) + .collect(toImmutableList()); + + return analyzeExpressions(session, metadata, sqlParser, new TupleDescriptor(fields), expressions); + } + + public static ExpressionAnalysis analyzeExpressionsWithInputs( + Session session, + Metadata metadata, + SqlParser sqlParser, + Map types, + Iterable expressions) + { + Field[] fields = new Field[types.size()]; + for (Entry entry : types.entrySet()) { + fields[entry.getKey()] = Field.newUnqualified(Optional.empty(), entry.getValue()); + } + TupleDescriptor tupleDescriptor = new TupleDescriptor(fields); + + return analyzeExpressions(session, metadata, sqlParser, tupleDescriptor, expressions); + } + + private static ExpressionAnalysis analyzeExpressions( + Session session, + Metadata metadata, + SqlParser sqlParser, + TupleDescriptor tupleDescriptor, + Iterable expressions) + { + ExpressionAnalyzer analyzer = create(new Analysis(), session, metadata, sqlParser, false); + for (Expression expression : expressions) { + analyzer.analyze(expression, tupleDescriptor, new AnalysisContext()); + } + + return new ExpressionAnalysis( + analyzer.getExpressionTypes(), + analyzer.getExpressionCoercions(), + analyzer.getSubqueryInPredicates()); + } + + public static ExpressionAnalysis analyzeExpression( + Session session, + Metadata metadata, + SqlParser sqlParser, + TupleDescriptor tupleDescriptor, + Analysis analysis, + boolean approximateQueriesEnabled, + AnalysisContext context, + Expression expression) + { + ExpressionAnalyzer analyzer = create(analysis, session, metadata, sqlParser, approximateQueriesEnabled); + analyzer.analyze(expression, tupleDescriptor, context); + + IdentityHashMap expressionTypes = analyzer.getExpressionTypes(); + IdentityHashMap expressionCoercions = analyzer.getExpressionCoercions(); + IdentityHashMap resolvedFunctions = analyzer.getResolvedFunctions(); + + analysis.addTypes(expressionTypes); + analysis.addCoercions(expressionCoercions); + analysis.addFunctionInfos(resolvedFunctions); + analysis.addRowFieldReferences(analyzer.getRowFieldReferences()); + + for (Expression subExpression : expressionTypes.keySet()) { + analysis.addResolvedNames(subExpression, analyzer.getResolvedNames()); + } + + Set subqueryInPredicates = analyzer.getSubqueryInPredicates(); + + return new ExpressionAnalysis(expressionTypes, expressionCoercions, subqueryInPredicates); + } + + public static ExpressionAnalyzer create(Analysis analysis, Session session, Metadata metadata, SqlParser sqlParser, boolean experimentalSyntaxEnabled) + { + return new ExpressionAnalyzer( + metadata.getFunctionRegistry(), + metadata.getTypeManager(), + node -> new StatementAnalyzer(analysis, metadata, sqlParser, session, experimentalSyntaxEnabled, Optional.empty())); + } + + public static ExpressionAnalyzer createWithoutSubqueries(FunctionRegistry functionRegistry, TypeManager typeManager, SemanticErrorCode errorCode, String message) + { + return new ExpressionAnalyzer(functionRegistry, typeManager, node -> { + throw new SemanticException(errorCode, node, message); + }); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/analyzer/FeaturesConfig.java b/presto-main/src/main/java/com/facebook/presto/sql/analyzer/FeaturesConfig.java new file mode 100644 index 00000000..dfd3c849 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/analyzer/FeaturesConfig.java @@ -0,0 +1,100 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.analyzer; + +import io.airlift.configuration.Config; +import io.airlift.configuration.LegacyConfig; + +public class FeaturesConfig +{ + private boolean experimentalSyntaxEnabled; + private boolean distributedIndexJoinsEnabled; + private boolean distributedJoinsEnabled; + private boolean optimizeMetadataQueries; + private boolean optimizeHashGeneration; + private boolean optimizeSingleDistinct = true; + + @LegacyConfig("analyzer.experimental-syntax-enabled") + @Config("experimental-syntax-enabled") + public FeaturesConfig setExperimentalSyntaxEnabled(boolean enabled) + { + experimentalSyntaxEnabled = enabled; + return this; + } + + public boolean isExperimentalSyntaxEnabled() + { + return experimentalSyntaxEnabled; + } + + @Config("distributed-index-joins-enabled") + public FeaturesConfig setDistributedIndexJoinsEnabled(boolean distributedIndexJoinsEnabled) + { + this.distributedIndexJoinsEnabled = distributedIndexJoinsEnabled; + return this; + } + + public boolean isDistributedIndexJoinsEnabled() + { + return distributedIndexJoinsEnabled; + } + + @Config("distributed-joins-enabled") + public FeaturesConfig setDistributedJoinsEnabled(boolean distributedJoinsEnabled) + { + this.distributedJoinsEnabled = distributedJoinsEnabled; + return this; + } + + public boolean isDistributedJoinsEnabled() + { + return distributedJoinsEnabled; + } + + public boolean isOptimizeMetadataQueries() + { + return optimizeMetadataQueries; + } + + @Config("optimizer.optimize-metadata-queries") + public FeaturesConfig setOptimizeMetadataQueries(boolean optimizeMetadataQueries) + { + this.optimizeMetadataQueries = optimizeMetadataQueries; + return this; + } + + public boolean isOptimizeHashGeneration() + { + return optimizeHashGeneration; + } + + @Config("optimizer.optimize-hash-generation") + public FeaturesConfig setOptimizeHashGeneration(boolean optimizeHashGeneration) + { + this.optimizeHashGeneration = optimizeHashGeneration; + return this; + } + + public boolean isOptimizeSingleDistinct() + { + return optimizeSingleDistinct; + } + + @Config("optimizer.optimize-single-distinct") + public FeaturesConfig setOptimizeSingleDistinct(boolean optimizeSingleDistinct) + { + this.optimizeSingleDistinct = optimizeSingleDistinct; + return this; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/analyzer/Field.java b/presto-main/src/main/java/com/facebook/presto/sql/analyzer/Field.java new file mode 100644 index 00000000..f1b62f10 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/analyzer/Field.java @@ -0,0 +1,139 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.analyzer; + +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.tree.QualifiedName; +import com.google.common.base.Preconditions; + +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class Field +{ + private final Optional relationAlias; + private final Optional name; + private final Type type; + private final boolean hidden; + + public static Field newUnqualified(String name, Type type) + { + Preconditions.checkNotNull(name, "name is null"); + Preconditions.checkNotNull(type, "type is null"); + + return new Field(Optional.empty(), Optional.of(name), type, false); + } + + public static Field newUnqualified(Optional name, Type type) + { + Preconditions.checkNotNull(name, "name is null"); + Preconditions.checkNotNull(type, "type is null"); + + return new Field(Optional.empty(), name, type, false); + } + + public static Field newQualified(QualifiedName relationAlias, Optional name, Type type, boolean hidden) + { + Preconditions.checkNotNull(relationAlias, "relationAlias is null"); + Preconditions.checkNotNull(name, "name is null"); + Preconditions.checkNotNull(type, "type is null"); + + return new Field(Optional.of(relationAlias), name, type, hidden); + } + + private Field(Optional relationAlias, Optional name, Type type, boolean hidden) + { + checkNotNull(relationAlias, "relationAlias is null"); + checkNotNull(name, "name is null"); + checkNotNull(type, "type is null"); + + this.relationAlias = relationAlias; + this.name = name; + this.type = type; + this.hidden = hidden; + } + + public Optional getRelationAlias() + { + return relationAlias; + } + + public Optional getName() + { + return name; + } + + public Type getType() + { + return type; + } + + public boolean isHidden() + { + return hidden; + } + + public boolean matchesPrefix(Optional prefix) + { + return !prefix.isPresent() || relationAlias.isPresent() && relationAlias.get().hasSuffix(prefix.get()); + } + + /* + Namespaces can have names such as "x", "x.y" or "" if there's no name + Name to resolve can have names like "a", "x.a", "x.y.a" + + namespace name possible match + "" "a" y + "x" "a" y + "x.y" "a" y + + "" "x.a" n + "x" "x.a" y + "x.y" "x.a" n + + "" "x.y.a" n + "x" "x.y.a" n + "x.y" "x.y.a" n + + "" "y.a" n + "x" "y.a" n + "x.y" "y.a" y + */ + public boolean canResolve(QualifiedName name) + { + if (!this.name.isPresent()) { + return false; + } + + // TODO: need to know whether the qualified name and the name of this field were quoted + return matchesPrefix(name.getPrefix()) && this.name.get().equalsIgnoreCase(name.getSuffix()); + } + + @Override + public String toString() + { + StringBuilder result = new StringBuilder(); + if (relationAlias.isPresent()) { + result.append(relationAlias.get()) + .append("."); + } + + result.append(name.orElse("")) + .append(":") + .append(type); + + return result.toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/analyzer/FieldOrExpression.java b/presto-main/src/main/java/com/facebook/presto/sql/analyzer/FieldOrExpression.java new file mode 100644 index 00000000..9d8c2334 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/analyzer/FieldOrExpression.java @@ -0,0 +1,106 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.analyzer; + +import com.facebook.presto.sql.tree.Expression; +import com.google.common.base.Preconditions; + +import java.util.Optional; + +/** + * Represents an expression or a direct field reference. The latter is used, for + * instance, when expanding "*" in SELECT * FROM .... + */ +public class FieldOrExpression +{ + // reference to field in underlying relation + private final Optional fieldIndex; + private final Optional expression; + + public FieldOrExpression(int fieldIndex) + { + this.fieldIndex = Optional.of(fieldIndex); + this.expression = Optional.empty(); + } + + public FieldOrExpression(Expression expression) + { + Preconditions.checkNotNull(expression, "expression is null"); + + this.fieldIndex = Optional.empty(); + this.expression = Optional.of(expression); + } + + public boolean isFieldReference() + { + return fieldIndex.isPresent(); + } + + public int getFieldIndex() + { + Preconditions.checkState(isFieldReference(), "Not a field reference"); + return fieldIndex.get(); + } + + public boolean isExpression() + { + return expression.isPresent(); + } + + public Expression getExpression() + { + Preconditions.checkState(isExpression(), "Not an expression"); + return expression.get(); + } + + @Override + public String toString() + { + if (fieldIndex.isPresent()) { + return fieldIndex.get().toString(); + } + + return expression.get().toString(); + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + FieldOrExpression that = (FieldOrExpression) o; + + if (!expression.equals(that.expression)) { + return false; + } + if (!fieldIndex.equals(that.fieldIndex)) { + return false; + } + + return true; + } + + @Override + public int hashCode() + { + int result = fieldIndex.hashCode(); + result = 31 * result + expression.hashCode(); + return result; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/analyzer/QueryExplainer.java b/presto-main/src/main/java/com/facebook/presto/sql/analyzer/QueryExplainer.java new file mode 100644 index 00000000..45109bda --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/analyzer/QueryExplainer.java @@ -0,0 +1,115 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.analyzer; + +import com.facebook.presto.Session; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.sql.parser.SqlParser; +import com.facebook.presto.sql.planner.LogicalPlanner; +import com.facebook.presto.sql.planner.Plan; +import com.facebook.presto.sql.planner.PlanFragmenter; +import com.facebook.presto.sql.planner.PlanNodeIdAllocator; +import com.facebook.presto.sql.planner.PlanPrinter; +import com.facebook.presto.sql.planner.SubPlan; +import com.facebook.presto.sql.planner.optimizations.PlanOptimizer; +import com.facebook.presto.sql.tree.ExplainType; +import com.facebook.presto.sql.tree.Statement; + +import java.util.List; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class QueryExplainer +{ + private final Session session; + private final List planOptimizers; + private final Metadata metadata; + private final SqlParser sqlParser; + private final boolean experimentalSyntaxEnabled; + + public QueryExplainer( + Session session, + List planOptimizers, + Metadata metadata, + SqlParser sqlParser, + boolean experimentalSyntaxEnabled) + { + this.session = checkNotNull(session, "session is null"); + this.planOptimizers = checkNotNull(planOptimizers, "planOptimizers is null"); + this.metadata = checkNotNull(metadata, "metadata is null"); + this.sqlParser = checkNotNull(sqlParser, "sqlParser is null"); + this.experimentalSyntaxEnabled = experimentalSyntaxEnabled; + } + + public String getPlan(Statement statement, ExplainType.Type planType) + { + switch (planType) { + case LOGICAL: + Plan plan = getLogicalPlan(statement); + return PlanPrinter.textLogicalPlan(plan.getRoot(), plan.getTypes(), metadata); + case DISTRIBUTED: + SubPlan subPlan = getDistributedPlan(statement); + return PlanPrinter.textDistributedPlan(subPlan, metadata); + } + throw new IllegalArgumentException("Unhandled plan type: " + planType); + } + + public String getGraphvizPlan(Statement statement, ExplainType.Type planType) + { + switch (planType) { + case LOGICAL: + Plan plan = getLogicalPlan(statement); + return PlanPrinter.graphvizLogicalPlan(plan.getRoot(), plan.getTypes()); + case DISTRIBUTED: + SubPlan subPlan = getDistributedPlan(statement); + return PlanPrinter.graphvizDistributedPlan(subPlan); + } + throw new IllegalArgumentException("Unhandled plan type: " + planType); + } + + public String getJsonPlan(Statement statement) + { + Plan plan = getLogicalPlan(statement); + return PlanPrinter.getJsonPlanSource(plan.getRoot(), metadata); + } + + private Plan getLogicalPlan(Statement statement) + { + // analyze statement + Analyzer analyzer = new Analyzer(session, metadata, sqlParser, Optional.of(this), experimentalSyntaxEnabled); + + Analysis analysis = analyzer.analyze(statement); + PlanNodeIdAllocator idAllocator = new PlanNodeIdAllocator(); + + // plan statement + LogicalPlanner logicalPlanner = new LogicalPlanner(session, planOptimizers, idAllocator, metadata); + return logicalPlanner.plan(analysis); + } + + private SubPlan getDistributedPlan(Statement statement) + { + // analyze statement + Analyzer analyzer = new Analyzer(session, metadata, sqlParser, Optional.of(this), experimentalSyntaxEnabled); + + Analysis analysis = analyzer.analyze(statement); + PlanNodeIdAllocator idAllocator = new PlanNodeIdAllocator(); + + // plan statement + LogicalPlanner logicalPlanner = new LogicalPlanner(session, planOptimizers, idAllocator, metadata); + Plan plan = logicalPlanner.plan(analysis); + + return new PlanFragmenter().createSubPlans(plan); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/analyzer/SemanticErrorCode.java b/presto-main/src/main/java/com/facebook/presto/sql/analyzer/SemanticErrorCode.java new file mode 100644 index 00000000..55d3e33d --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/analyzer/SemanticErrorCode.java @@ -0,0 +1,74 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.analyzer; + +public enum SemanticErrorCode +{ + MUST_BE_AGGREGATE_OR_GROUP_BY, + NESTED_AGGREGATION, + NESTED_WINDOW, + MUST_BE_WINDOW_FUNCTION, + WINDOW_REQUIRES_OVER, + INVALID_WINDOW_FRAME, + + MISSING_CATALOG, + MISSING_SCHEMA, + MISSING_TABLE, + MISSING_COLUMN, + MISMATCHED_COLUMN_ALIASES, + NOT_SUPPORTED, + + INVALID_SCHEMA_NAME, + + TABLE_ALREADY_EXISTS, + COLUMN_ALREADY_EXISTS, + + DUPLICATE_RELATION, + + TYPE_MISMATCH, + AMBIGUOUS_ATTRIBUTE, + MISSING_ATTRIBUTE, + INVALID_ORDINAL, + + ORDER_BY_MUST_BE_IN_SELECT, + + CANNOT_HAVE_AGGREGATIONS_OR_WINDOWS, + + WILDCARD_WITHOUT_FROM, + + MISMATCHED_SET_COLUMN_TYPES, + + MULTIPLE_FIELDS_FROM_SCALAR_SUBQUERY, + + DUPLICATE_COLUMN_NAME, + COLUMN_NAME_NOT_SPECIFIED, + + EXPRESSION_NOT_CONSTANT, + + VIEW_PARSE_ERROR, + VIEW_ANALYSIS_ERROR, + VIEW_IS_STALE, + + NON_NUMERIC_SAMPLE_PERCENTAGE, + + SAMPLE_PERCENTAGE_OUT_OF_RANGE, + + INVALID_SESSION_PROPERTY, + + UNSUPPORTED_PARTITION_TYPE, + INVALID_PARTITION_VALUE, + MISSING_PARTITION, + MISMATCHED_PARTITION_NAME, + DUPLICATE_PARTITION_NAME +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/analyzer/SemanticException.java b/presto-main/src/main/java/com/facebook/presto/sql/analyzer/SemanticException.java new file mode 100644 index 00000000..480faf4e --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/analyzer/SemanticException.java @@ -0,0 +1,46 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.analyzer; + +import com.facebook.presto.sql.tree.Node; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class SemanticException + extends RuntimeException +{ + private final SemanticErrorCode code; + private final Node node; + + public SemanticException(SemanticErrorCode code, Node node, String format, Object... args) + { + super(String.format(format, args)); + + checkNotNull(code, "code is null"); + checkNotNull(node, "node is null"); + + this.code = code; + this.node = node; + } + + public Node getNode() + { + return node; + } + + public SemanticErrorCode getCode() + { + return code; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/analyzer/StatementAnalyzer.java b/presto-main/src/main/java/com/facebook/presto/sql/analyzer/StatementAnalyzer.java new file mode 100644 index 00000000..91af50f5 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/analyzer/StatementAnalyzer.java @@ -0,0 +1,790 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.analyzer; + +import com.facebook.presto.Session; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.metadata.MetadataUtil; +import com.facebook.presto.metadata.QualifiedTableName; +import com.facebook.presto.metadata.TableHandle; +import com.facebook.presto.spi.ColumnMetadata; +import com.facebook.presto.spi.CreateTableOption; +import com.facebook.presto.spi.InsertOption; +import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.parser.SqlParser; +import com.facebook.presto.sql.tree.AllColumns; +import com.facebook.presto.sql.tree.BooleanLiteral; +import com.facebook.presto.sql.tree.Cast; +import com.facebook.presto.sql.tree.CreateTableAsSelect; +import com.facebook.presto.sql.tree.CreateView; +import com.facebook.presto.sql.tree.DefaultTraversalVisitor; +import com.facebook.presto.sql.tree.Delete; +import com.facebook.presto.sql.tree.DoubleLiteral; +import com.facebook.presto.sql.tree.Explain; +import com.facebook.presto.sql.tree.ExplainFormat; +import com.facebook.presto.sql.tree.ExplainOption; +import com.facebook.presto.sql.tree.ExplainType; +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.GenericLiteral; +import com.facebook.presto.sql.tree.Insert; +import com.facebook.presto.sql.tree.LikePredicate; +import com.facebook.presto.sql.tree.LongLiteral; +import com.facebook.presto.sql.tree.PartitionElement; +import com.facebook.presto.sql.tree.QualifiedName; +import com.facebook.presto.sql.tree.Query; +import com.facebook.presto.sql.tree.Relation; +import com.facebook.presto.sql.tree.SelectItem; +import com.facebook.presto.sql.tree.ShowCatalogs; +import com.facebook.presto.sql.tree.ShowColumns; +import com.facebook.presto.sql.tree.ShowFunctions; +import com.facebook.presto.sql.tree.ShowPartitions; +import com.facebook.presto.sql.tree.ShowSchemas; +import com.facebook.presto.sql.tree.ShowSession; +import com.facebook.presto.sql.tree.ShowTables; +import com.facebook.presto.sql.tree.SingleColumn; +import com.facebook.presto.sql.tree.SortItem; +import com.facebook.presto.sql.tree.Statement; +import com.facebook.presto.sql.tree.StringLiteral; +import com.facebook.presto.sql.tree.TimeLiteral; +import com.facebook.presto.sql.tree.TimestampLiteral; +import com.facebook.presto.sql.tree.Use; +import com.facebook.presto.sql.tree.Values; +import com.facebook.presto.sql.tree.With; +import com.facebook.presto.sql.tree.WithQuery; +import com.facebook.presto.util.DateTimeUtils; +import com.google.common.base.Joiner; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableList; +import com.google.common.primitives.Ints; + +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.Set; +import java.util.TreeMap; + +import static com.facebook.presto.connector.informationSchema.InformationSchemaMetadata.TABLE_COLUMNS; +import static com.facebook.presto.connector.informationSchema.InformationSchemaMetadata.TABLE_INTERNAL_FUNCTIONS; +import static com.facebook.presto.connector.informationSchema.InformationSchemaMetadata.TABLE_INTERNAL_PARTITIONS; +import static com.facebook.presto.connector.informationSchema.InformationSchemaMetadata.TABLE_SCHEMATA; +import static com.facebook.presto.connector.informationSchema.InformationSchemaMetadata.TABLE_TABLES; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.sql.QueryUtil.aliased; +import static com.facebook.presto.sql.QueryUtil.aliasedName; +import static com.facebook.presto.sql.QueryUtil.aliasedNullToEmpty; +import static com.facebook.presto.sql.QueryUtil.aliasedYesNoToBoolean; +import static com.facebook.presto.sql.QueryUtil.ascending; +import static com.facebook.presto.sql.QueryUtil.caseWhen; +import static com.facebook.presto.sql.QueryUtil.equal; +import static com.facebook.presto.sql.QueryUtil.functionCall; +import static com.facebook.presto.sql.QueryUtil.logicalAnd; +import static com.facebook.presto.sql.QueryUtil.nameReference; +import static com.facebook.presto.sql.QueryUtil.ordering; +import static com.facebook.presto.sql.QueryUtil.row; +import static com.facebook.presto.sql.QueryUtil.selectAll; +import static com.facebook.presto.sql.QueryUtil.selectList; +import static com.facebook.presto.sql.QueryUtil.simpleQuery; +import static com.facebook.presto.sql.QueryUtil.subquery; +import static com.facebook.presto.sql.QueryUtil.table; +import static com.facebook.presto.sql.QueryUtil.unaliasedName; +import static com.facebook.presto.sql.QueryUtil.values; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.COLUMN_NAME_NOT_SPECIFIED; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.DUPLICATE_COLUMN_NAME; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.DUPLICATE_RELATION; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.INVALID_ORDINAL; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.INVALID_SCHEMA_NAME; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.MISMATCHED_SET_COLUMN_TYPES; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.MISSING_TABLE; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.NOT_SUPPORTED; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.TABLE_ALREADY_EXISTS; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.UNSUPPORTED_PARTITION_TYPE; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.INVALID_PARTITION_VALUE; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.MISSING_PARTITION; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.MISMATCHED_PARTITION_NAME; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.DUPLICATE_PARTITION_NAME; +import static com.facebook.presto.sql.tree.BooleanLiteral.FALSE_LITERAL; +import static com.facebook.presto.sql.tree.BooleanLiteral.TRUE_LITERAL; +import static com.facebook.presto.sql.tree.ExplainFormat.Type.TEXT; +import static com.facebook.presto.sql.tree.ExplainType.Type.LOGICAL; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.Iterables.elementsEqual; +import static com.google.common.collect.Iterables.transform; +import static com.google.common.collect.Iterables.contains; +import static java.util.stream.Collectors.toList; + +class StatementAnalyzer + extends DefaultTraversalVisitor +{ + private final Analysis analysis; + private final Metadata metadata; + private final Session session; + private final Optional queryExplainer; + private final boolean experimentalSyntaxEnabled; + private final SqlParser sqlParser; + + public StatementAnalyzer( + Analysis analysis, + Metadata metadata, + SqlParser sqlParser, + Session session, + boolean experimentalSyntaxEnabled, + Optional queryExplainer) + { + this.analysis = checkNotNull(analysis, "analysis is null"); + this.metadata = checkNotNull(metadata, "metadata is null"); + this.sqlParser = checkNotNull(sqlParser, "sqlParser is null"); + this.session = checkNotNull(session, "session is null"); + this.experimentalSyntaxEnabled = experimentalSyntaxEnabled; + this.queryExplainer = checkNotNull(queryExplainer, "queryExplainer is null"); + } + + @Override + protected TupleDescriptor visitShowTables(ShowTables showTables, AnalysisContext context) + { + String catalogName = session.getCatalog(); + String schemaName = session.getSchema(); + + Optional schema = showTables.getSchema(); + if (schema.isPresent()) { + List parts = schema.get().getParts(); + if (parts.size() > 2) { + throw new SemanticException(INVALID_SCHEMA_NAME, showTables, "too many parts in schema name: %s", schema); + } + if (parts.size() == 2) { + catalogName = parts.get(0); + } + schemaName = schema.get().getSuffix(); + } + + // TODO: throw SemanticException if schema does not exist + + Expression predicate = equal(nameReference("table_schema"), new StringLiteral(schemaName)); + + Optional likePattern = showTables.getLikePattern(); + if (likePattern.isPresent()) { + Expression likePredicate = new LikePredicate(nameReference("table_name"), new StringLiteral(likePattern.get()), null); + predicate = logicalAnd(predicate, likePredicate); + } + + Query query = simpleQuery( + selectList(aliasedName("table_name", "Table")), + from(catalogName, TABLE_TABLES), + predicate, + ordering(ascending("table_name"))); + + return process(query, context); + } + + @Override + protected TupleDescriptor visitShowSchemas(ShowSchemas node, AnalysisContext context) + { + Query query = simpleQuery( + selectList(aliasedName("schema_name", "Schema")), + from(node.getCatalog().orElse(session.getCatalog()), TABLE_SCHEMATA), + ordering(ascending("schema_name"))); + + return process(query, context); + } + + @Override + protected TupleDescriptor visitShowCatalogs(ShowCatalogs node, AnalysisContext context) + { + List rows = metadata.getCatalogNames().keySet().stream() + .map(name -> row(new StringLiteral(name))) + .collect(toList()); + + Query query = simpleQuery( + selectList(new AllColumns()), + aliased(new Values(rows), "catalogs", ImmutableList.of("Catalog"))); + + return process(query, context); + } + + @Override + protected TupleDescriptor visitShowColumns(ShowColumns showColumns, AnalysisContext context) + { + QualifiedTableName tableName = MetadataUtil.createQualifiedTableName(session, showColumns.getTable()); + + if (!metadata.getView(session, tableName).isPresent() && + !metadata.getTableHandle(session, tableName).isPresent()) { + throw new SemanticException(MISSING_TABLE, showColumns, "Table '%s' does not exist", tableName); + } + + Query query = simpleQuery( + selectList( + aliasedName("column_name", "Column"), + aliasedName("data_type", "Type"), + aliasedYesNoToBoolean("is_nullable", "Null"), + aliasedYesNoToBoolean("is_partition_key", "Partition Key"), + aliasedNullToEmpty("comment", "Comment")), + from(tableName.getCatalogName(), TABLE_COLUMNS), + logicalAnd( + equal(nameReference("table_schema"), new StringLiteral(tableName.getSchemaName())), + equal(nameReference("table_name"), new StringLiteral(tableName.getTableName()))), + ordering(ascending("ordinal_position"))); + + return process(query, context); + } + + @Override + protected TupleDescriptor visitUse(Use node, AnalysisContext context) + { + analysis.setUpdateType("USE"); + throw new SemanticException(NOT_SUPPORTED, node, "USE statement is not supported"); + } + + @Override + protected TupleDescriptor visitShowPartitions(ShowPartitions showPartitions, AnalysisContext context) + { + QualifiedTableName table = MetadataUtil.createQualifiedTableName(session, showPartitions.getTable()); + Optional tableHandle = metadata.getTableHandle(session, table); + if (!tableHandle.isPresent()) { + throw new SemanticException(MISSING_TABLE, showPartitions, "Table '%s' does not exist", table); + } + + /* + Generate a dynamic pivot to output one column per partition key. + For example, a table with two partition keys (ds, cluster_name) + would generate the following query: + + SELECT + partition_number + , max(CASE WHEN partition_key = 'ds' THEN partition_value END) ds + , max(CASE WHEN partition_key = 'cluster_name' THEN partition_value END) cluster_name + FROM ... + GROUP BY partition_number + + The values are also cast to the type of the partition column. + The query is then wrapped to allow custom filtering and ordering. + */ + + ImmutableList.Builder selectList = ImmutableList.builder(); + ImmutableList.Builder wrappedList = ImmutableList.builder(); + selectList.add(unaliasedName("partition_number")); + for (ColumnMetadata column : metadata.getTableMetadata(tableHandle.get()).getColumns()) { + if (!column.isPartitionKey()) { + continue; + } + Expression key = equal(nameReference("partition_key"), new StringLiteral(column.getName())); + Expression value = caseWhen(key, nameReference("partition_value")); + value = new Cast(value, column.getType().getTypeSignature().toString()); + Expression function = functionCall("max", value); + selectList.add(new SingleColumn(function, column.getName())); + wrappedList.add(unaliasedName(column.getName())); + } + + Query query = simpleQuery( + selectAll(selectList.build()), + from(table.getCatalogName(), TABLE_INTERNAL_PARTITIONS), + Optional.of(logicalAnd( + equal(nameReference("table_schema"), new StringLiteral(table.getSchemaName())), + equal(nameReference("table_name"), new StringLiteral(table.getTableName())))), + ImmutableList.of(nameReference("partition_number")), + Optional.empty(), + ImmutableList.of(), + Optional.empty()); + + query = simpleQuery( + selectAll(wrappedList.build()), + subquery(query), + showPartitions.getWhere(), + ImmutableList.of(), + Optional.empty(), + ImmutableList.builder() + .addAll(showPartitions.getOrderBy()) + .add(ascending("partition_number")) + .build(), + showPartitions.getLimit()); + + return process(query, context); + } + + @Override + protected TupleDescriptor visitShowFunctions(ShowFunctions node, AnalysisContext context) + { + Query query = simpleQuery(selectList( + aliasedName("function_name", "Function"), + aliasedName("return_type", "Return Type"), + aliasedName("argument_types", "Argument Types"), + aliasedName("function_type", "Function Type"), + aliasedName("deterministic", "Deterministic"), + aliasedName("description", "Description")), + from(session.getCatalog(), TABLE_INTERNAL_FUNCTIONS), + ordering( + ascending("function_name"), + ascending("return_type"), + ascending("argument_types"), + ascending("function_type"))); + + return process(query, context); + } + + @Override + protected TupleDescriptor visitShowSession(ShowSession node, AnalysisContext context) + { + ImmutableList.Builder rows = ImmutableList.builder(); + for (Entry property : new TreeMap<>(session.getSystemProperties()).entrySet()) { + rows.add(row( + new StringLiteral(property.getKey()), + new StringLiteral(property.getValue()), + TRUE_LITERAL)); + } + for (Entry> entry : new TreeMap<>(session.getCatalogProperties()).entrySet()) { + String catalog = entry.getKey(); + for (Entry property : new TreeMap<>(entry.getValue()).entrySet()) { + rows.add(row( + new StringLiteral(catalog + "." + property.getKey()), + new StringLiteral(property.getValue()), + TRUE_LITERAL)); + } + } + + // add bogus row so we can support empty sessions + rows.add(row(new StringLiteral(""), new StringLiteral(""), FALSE_LITERAL)); + + Query query = simpleQuery( + selectList( + aliasedName("name", "Name"), + aliasedName("value", "Value")), + aliased( + new Values(rows.build()), + "session", + ImmutableList.of("name", "value", "include")), + nameReference("include")); + + return process(query, context); + } + + @Override + protected TupleDescriptor visitInsert(Insert insert, AnalysisContext context) + { + analysis.setUpdateType("INSERT"); + + // analyze the query that creates the data + TupleDescriptor descriptor = process(insert.getQuery(), context); + + QualifiedTableName targetTable = MetadataUtil.createQualifiedTableName(session, insert.getTarget()); + Optional targetTableHandle = metadata.getTableHandle(session, targetTable); + if (!targetTableHandle.isPresent()) { + throw new SemanticException(MISSING_TABLE, insert, "Table '%s' does not exist", targetTable); + } + analysis.setInsertTarget(targetTableHandle.get()); + + List columns = metadata.getTableMetadata(targetTableHandle.get()).getColumns(); + + analyzeInsertOption(insert, columns, targetTable); + + // verify the insert destination columns match the query + Iterable tableTypes = FluentIterable.from(columns) + .filter(column -> !column.isHidden()) + .transform(ColumnMetadata::getType); + + Iterable queryTypes = transform(descriptor.getVisibleFields(), Field::getType); + + if (!elementsEqual(tableTypes, queryTypes)) { + throw new SemanticException(MISMATCHED_SET_COLUMN_TYPES, insert, "Insert query has mismatched column types: " + + "Table: (" + Joiner.on(", ").join(tableTypes) + "), " + + "Query: (" + Joiner.on(", ").join(queryTypes) + ")"); + } + + return new TupleDescriptor(Field.newUnqualified("rows", BIGINT)); + } + + @Override + protected TupleDescriptor visitDelete(Delete node, AnalysisContext context) + { + analysis.setUpdateType("DELETE"); + + analysis.setDelete(node); + + TupleAnalyzer analyzer = new TupleAnalyzer(analysis, session, metadata, sqlParser, experimentalSyntaxEnabled); + TupleDescriptor descriptor = analyzer.process(node.getTable(), context); + node.getWhere().ifPresent(where -> analyzer.analyzeWhere(node, descriptor, context, where)); + + return new TupleDescriptor(Field.newUnqualified("rows", BIGINT)); + } + + private void analyzeInsertOption(Insert insert, List columns, QualifiedTableName targetTable) + { + boolean dynamicPartition = false; + List targetPartitionNames = columns.stream() + .filter(column -> column.isPartitionKey()) + .map(ColumnMetadata::getName) + .collect(toList()); + List targetPartitionTypes = columns.stream() + .filter(column -> column.isPartitionKey()) + .map(ColumnMetadata::getType) + .collect(toList()); + + if (!targetPartitionNames.isEmpty() && !insert.isPartition()) { + throw new SemanticException(MISSING_PARTITION, insert, "Target table '%s' is partitioned but insert query does not specifty the partition", targetTable); + } + + if (insert.isPartition() && targetPartitionNames.isEmpty()) { + throw new SemanticException(MISSING_PARTITION, insert, "Target table '%s' is not partitioned but insert query speciftys the partition", targetTable); + } + + HashMap partitionKeyValue = new HashMap(); + HashMap partitionTypes = new HashMap(); + LinkedHashMap partitionKeyValueInorder = new LinkedHashMap(); + + if (insert.isPartition()) { + dynamicPartition = analyzePartitionElements(insert, targetTable, partitionKeyValue, partitionTypes); + } + if (partitionKeyValue.size() != targetPartitionNames.size()) { + throw new SemanticException(MISMATCHED_PARTITION_NAME, insert, "Insert query has mismatched partition names", targetTable); + } + else { + int index = 0; + for (Iterator it = targetPartitionNames.iterator(); it.hasNext(); ) { + String partitionName = it.next(); + if (!partitionKeyValue.containsKey(partitionName)) { + throw new SemanticException(MISMATCHED_PARTITION_NAME, insert, "Insert query has mismatched partition names", targetTable); + } + + if (!dynamicPartition) { + String targetPartitionType = targetPartitionTypes.get(index++).getTypeSignature().toString(); + String insertPartitionType = partitionTypes.get(partitionName); + if (!insertPartitionType.equalsIgnoreCase(targetPartitionType)) { + throw new SemanticException(INVALID_PARTITION_VALUE, + insert, "Insert query has invalid partition value, " + + "partition " + partitionName + + " should be of type " + + targetPartitionType + " but is of type " + + insertPartitionType, targetTable); + } + } + // reorder the partitionKeyValue as the order of select query order + partitionKeyValueInorder.put(partitionName, partitionKeyValue.get(partitionName)); + } + } + analysis.setInsertOption(new InsertOption(insert.isOverwrite(), insert.isPartition(), dynamicPartition, partitionKeyValueInorder)); + } + + private boolean analyzePartitionElements(Insert insert, QualifiedTableName targetTable, HashMap partitionKeyValue, HashMap partitionTypes) + { + boolean dynamicPartition = false; + for (PartitionElement element : insert.getPartitionElements()) { + String partitionName = element.getName(); + String partitionValue = ""; + String partitionType = ""; + if (!element.getValue().isPresent()) { + dynamicPartition = true; + partitionTypes.put(partitionName, partitionType); + partitionKeyValue.put(partitionName, partitionValue); + continue; + } + Expression exp = element.getValue().get(); + + if (partitionKeyValue.containsKey(partitionName)) { + throw new SemanticException(DUPLICATE_PARTITION_NAME, insert, "Insert query has duplicate partition names", targetTable); + } + + if (exp instanceof BooleanLiteral) { + partitionType = "boolean"; + partitionValue = String.valueOf(((BooleanLiteral) exp).getValue()); + } + else if (exp instanceof DoubleLiteral) { + partitionType = "double"; + partitionValue = String.valueOf(((DoubleLiteral) exp).getValue()); + } + else if (exp instanceof LongLiteral) { + partitionType = "bigint"; + partitionValue = String.valueOf(((LongLiteral) exp).getValue()); + } + else if (exp instanceof StringLiteral) { + partitionType = "varchar"; + partitionValue = ((StringLiteral) exp).getValue(); + } + else if (exp instanceof TimeLiteral) { + partitionType = "time"; + long value = DateTimeUtils.parseTime(session.getTimeZoneKey(), ((TimeLiteral) exp).getValue()); + partitionValue = new java.text.SimpleDateFormat("HH:mm:ss.SSS").format(new Date(value)); + } + else if (exp instanceof TimestampLiteral) { + partitionType = "timestamp"; + long value = DateTimeUtils.parseTimestamp(session.getTimeZoneKey(), ((TimestampLiteral) exp).getValue()); + partitionValue = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date(value)); + } + else if (exp instanceof GenericLiteral) { + GenericLiteral genericLiteral = (GenericLiteral) exp; + partitionType = genericLiteral.getType(); + partitionValue = genericLiteral.getValue(); + if (!isPartitionTypeSupported(partitionType)) { + throw new SemanticException(UNSUPPORTED_PARTITION_TYPE, insert, "Unsupported partition type", targetTable); + } + if (partitionType.equalsIgnoreCase("time")) { + long value = DateTimeUtils.parseTime(session.getTimeZoneKey(), partitionValue); + partitionValue = new java.text.SimpleDateFormat("HH:mm:ss.SSS").format(new Date(value)); + } + else if (partitionType.equalsIgnoreCase("timestamp")) { + long value = DateTimeUtils.parseTimestamp(session.getTimeZoneKey(), partitionValue); + partitionValue = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date(value)); + } + } + else { + throw new SemanticException(UNSUPPORTED_PARTITION_TYPE, insert, "Unsupported partition type", targetTable); + } + + partitionTypes.put(partitionName, partitionType); + partitionKeyValue.put(partitionName, partitionValue); + } + return dynamicPartition; + } + + private boolean isPartitionTypeSupported(String type) + { + if (type.equalsIgnoreCase("boolean") || + type.equalsIgnoreCase("double") || + type.equalsIgnoreCase("bigint") || + type.equalsIgnoreCase("varchar") || + type.equalsIgnoreCase("date") || + type.equalsIgnoreCase("time") || + type.equalsIgnoreCase("timestamp")) { + return true; + } + return false; + } + + @Override + protected TupleDescriptor visitCreateTableAsSelect(CreateTableAsSelect node, AnalysisContext context) + { + analysis.setUpdateType("CREATE TABLE"); + + // turn this into a query that has a new table writer node on top. + QualifiedTableName targetTable = MetadataUtil.createQualifiedTableName(session, node.getName()); + analysis.setCreateTableDestination(targetTable); + + Optional targetTableHandle = metadata.getTableHandle(session, targetTable); + if (targetTableHandle.isPresent()) { + throw new SemanticException(TABLE_ALREADY_EXISTS, node, "Destination table '%s' already exists", targetTable); + } + + // analyze the query that creates the table + TupleDescriptor descriptor = process(node.getQuery(), context); + + validateColumnNames(node, descriptor); + + // handle partition exceptions + if (node.isPartition()) { + Iterable> queryColumns = transform(descriptor.getVisibleFields(), Field::getName); + for (String partitionColumn : node.getPartitionList()) { + if (!contains(queryColumns, Optional.of(partitionColumn))) { + throw new SemanticException(MISSING_PARTITION, node, "Target table '%s' is partitioned but query does not speciftys the partition", targetTable); + } + } + } + analysis.setCreateTableOption(new CreateTableOption(node.isPartition(), node.getPartitionList())); + + return new TupleDescriptor(Field.newUnqualified("rows", BIGINT)); + } + + @Override + protected TupleDescriptor visitCreateView(CreateView node, AnalysisContext context) + { + analysis.setUpdateType("CREATE VIEW"); + + // analyze the query that creates the view + TupleDescriptor descriptor = process(node.getQuery(), context); + + validateColumnNames(node, descriptor); + + return descriptor; + } + + private static void validateColumnNames(Statement node, TupleDescriptor descriptor) + { + // verify that all column names are specified and unique + // TODO: collect errors and return them all at once + Set names = new HashSet<>(); + for (Field field : descriptor.getVisibleFields()) { + Optional fieldName = field.getName(); + if (!fieldName.isPresent()) { + throw new SemanticException(COLUMN_NAME_NOT_SPECIFIED, node, "Column name not specified at position %s", descriptor.indexOf(field) + 1); + } + if (!names.add(fieldName.get())) { + throw new SemanticException(DUPLICATE_COLUMN_NAME, node, "Column name '%s' specified more than once", fieldName.get()); + } + } + } + + @Override + protected TupleDescriptor visitExplain(Explain node, AnalysisContext context) + throws SemanticException + { + checkState(queryExplainer.isPresent(), "query explainer not available"); + ExplainType.Type planType = LOGICAL; + ExplainFormat.Type planFormat = TEXT; + List options = node.getOptions(); + + for (ExplainOption option : options) { + if (option instanceof ExplainType) { + planType = ((ExplainType) option).getType(); + break; + } + } + + for (ExplainOption option : options) { + if (option instanceof ExplainFormat) { + planFormat = ((ExplainFormat) option).getType(); + break; + } + } + + String queryPlan = getQueryPlan(node, planType, planFormat); + + Query query = simpleQuery( + selectList(new AllColumns()), + aliased( + values(row(new StringLiteral((queryPlan)))), + "plan", + ImmutableList.of("Query Plan"))); + + return process(query, context); + } + + private String getQueryPlan(Explain node, ExplainType.Type planType, ExplainFormat.Type planFormat) + throws IllegalArgumentException + { + switch (planFormat) { + case GRAPHVIZ: + return queryExplainer.get().getGraphvizPlan(node.getStatement(), planType); + case TEXT: + return queryExplainer.get().getPlan(node.getStatement(), planType); + case JSON: + // ignore planType if planFormat is JSON + return queryExplainer.get().getJsonPlan(node.getStatement()); + } + throw new IllegalArgumentException("Invalid Explain Format: " + planFormat.toString()); + } + + @Override + protected TupleDescriptor visitQuery(Query node, AnalysisContext parentContext) + { + AnalysisContext context = new AnalysisContext(parentContext); + + if (node.getApproximate().isPresent()) { + if (!experimentalSyntaxEnabled) { + throw new SemanticException(NOT_SUPPORTED, node, "approximate queries are not enabled"); + } + context.setApproximate(true); + } + + analyzeWith(node, context); + + TupleAnalyzer analyzer = new TupleAnalyzer(analysis, session, metadata, sqlParser, experimentalSyntaxEnabled); + TupleDescriptor descriptor = analyzer.process(node.getQueryBody(), context); + analyzeOrderBy(node, descriptor, context); + + // Input fields == Output fields + analysis.setOutputDescriptor(node, descriptor); + analysis.setOutputExpressions(node, descriptorToFields(descriptor)); + analysis.setQuery(node); + + return descriptor; + } + + private static List descriptorToFields(TupleDescriptor tupleDescriptor) + { + ImmutableList.Builder builder = ImmutableList.builder(); + for (int fieldIndex = 0; fieldIndex < tupleDescriptor.getAllFieldCount(); fieldIndex++) { + builder.add(new FieldOrExpression(fieldIndex)); + } + return builder.build(); + } + + private void analyzeWith(Query node, AnalysisContext context) + { + // analyze WITH clause + if (!node.getWith().isPresent()) { + return; + } + + With with = node.getWith().get(); + if (with.isRecursive()) { + throw new SemanticException(NOT_SUPPORTED, with, "Recursive WITH queries are not supported"); + } + + for (WithQuery withQuery : with.getQueries()) { + if (withQuery.getColumnNames() != null && !withQuery.getColumnNames().isEmpty()) { + throw new SemanticException(NOT_SUPPORTED, withQuery, "Column alias not supported in WITH queries"); + } + + Query query = withQuery.getQuery(); + process(query, context); + + String name = withQuery.getName(); + if (context.isNamedQueryDeclared(name)) { + throw new SemanticException(DUPLICATE_RELATION, withQuery, "WITH query name '%s' specified more than once", name); + } + + context.addNamedQuery(name, query); + } + } + + private void analyzeOrderBy(Query node, TupleDescriptor tupleDescriptor, AnalysisContext context) + { + List items = node.getOrderBy(); + + ImmutableList.Builder orderByFieldsBuilder = ImmutableList.builder(); + + if (!items.isEmpty()) { + for (SortItem item : items) { + Expression expression = item.getSortKey(); + + FieldOrExpression orderByField; + if (expression instanceof LongLiteral) { + // this is an ordinal in the output tuple + + long ordinal = ((LongLiteral) expression).getValue(); + if (ordinal < 1 || ordinal > tupleDescriptor.getVisibleFieldCount()) { + throw new SemanticException(INVALID_ORDINAL, expression, "ORDER BY position %s is not in select list", ordinal); + } + + orderByField = new FieldOrExpression(Ints.checkedCast(ordinal - 1)); + } + else { + // otherwise, just use the expression as is + orderByField = new FieldOrExpression(expression); + ExpressionAnalysis expressionAnalysis = ExpressionAnalyzer.analyzeExpression(session, + metadata, + sqlParser, + tupleDescriptor, + analysis, + experimentalSyntaxEnabled, + context, + orderByField.getExpression()); + analysis.addInPredicates(node, expressionAnalysis.getSubqueryInPredicates()); + } + + orderByFieldsBuilder.add(orderByField); + } + } + + analysis.setOrderByExpressions(node, orderByFieldsBuilder.build()); + } + + private static Relation from(String catalog, SchemaTableName table) + { + return table(QualifiedName.of(catalog, table.getSchemaName(), table.getTableName())); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/analyzer/TupleAnalyzer.java b/presto-main/src/main/java/com/facebook/presto/sql/analyzer/TupleAnalyzer.java new file mode 100644 index 00000000..0a44d338 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/analyzer/TupleAnalyzer.java @@ -0,0 +1,1140 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.analyzer; + +import com.facebook.presto.Session; +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.metadata.MetadataUtil; +import com.facebook.presto.metadata.QualifiedTableName; +import com.facebook.presto.metadata.TableHandle; +import com.facebook.presto.metadata.TableMetadata; +import com.facebook.presto.metadata.ViewDefinition; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ColumnMetadata; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.type.BigintType; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeSignature; +import com.facebook.presto.sql.ExpressionUtils; +import com.facebook.presto.sql.parser.ParsingException; +import com.facebook.presto.sql.parser.SqlParser; +import com.facebook.presto.sql.planner.ExpressionInterpreter; +import com.facebook.presto.sql.planner.NoOpSymbolResolver; +import com.facebook.presto.sql.planner.Symbol; +import com.facebook.presto.sql.planner.SymbolResolver; +import com.facebook.presto.sql.planner.optimizations.CanonicalizeExpressions; +import com.facebook.presto.sql.tree.AliasedRelation; +import com.facebook.presto.sql.tree.AllColumns; +import com.facebook.presto.sql.tree.ComparisonExpression; +import com.facebook.presto.sql.tree.DefaultExpressionTraversalVisitor; +import com.facebook.presto.sql.tree.DefaultTraversalVisitor; +import com.facebook.presto.sql.tree.Except; +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.FrameBound; +import com.facebook.presto.sql.tree.FunctionCall; +import com.facebook.presto.sql.tree.Intersect; +import com.facebook.presto.sql.tree.Join; +import com.facebook.presto.sql.tree.JoinCriteria; +import com.facebook.presto.sql.tree.JoinOn; +import com.facebook.presto.sql.tree.JoinUsing; +import com.facebook.presto.sql.tree.LongLiteral; +import com.facebook.presto.sql.tree.NaturalJoin; +import com.facebook.presto.sql.tree.Node; +import com.facebook.presto.sql.tree.QualifiedName; +import com.facebook.presto.sql.tree.QualifiedNameReference; +import com.facebook.presto.sql.tree.Query; +import com.facebook.presto.sql.tree.QuerySpecification; +import com.facebook.presto.sql.tree.Relation; +import com.facebook.presto.sql.tree.Row; +import com.facebook.presto.sql.tree.SampledRelation; +import com.facebook.presto.sql.tree.SelectItem; +import com.facebook.presto.sql.tree.SingleColumn; +import com.facebook.presto.sql.tree.SortItem; +import com.facebook.presto.sql.tree.Statement; +import com.facebook.presto.sql.tree.Table; +import com.facebook.presto.sql.tree.TableSubquery; +import com.facebook.presto.sql.tree.Union; +import com.facebook.presto.sql.tree.Unnest; +import com.facebook.presto.sql.tree.Values; +import com.facebook.presto.sql.tree.Window; +import com.facebook.presto.sql.tree.WindowFrame; +import com.facebook.presto.type.ArrayType; +import com.facebook.presto.type.MapType; +import com.facebook.presto.type.RowType; +import com.facebook.presto.util.ImmutableCollectors; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.common.collect.Multimap; +import com.google.common.collect.Sets; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static com.facebook.presto.metadata.FunctionRegistry.getCommonSuperType; +import static com.facebook.presto.metadata.ViewDefinition.ViewColumn; +import static com.facebook.presto.spi.StandardErrorCode.INVALID_FUNCTION_ARGUMENT; +import static com.facebook.presto.spi.type.BooleanType.BOOLEAN; +import static com.facebook.presto.sql.analyzer.ExpressionAnalyzer.getExpressionTypes; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.AMBIGUOUS_ATTRIBUTE; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.DUPLICATE_RELATION; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.INVALID_ORDINAL; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.INVALID_WINDOW_FRAME; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.MISMATCHED_COLUMN_ALIASES; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.MISMATCHED_SET_COLUMN_TYPES; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.MISSING_CATALOG; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.MISSING_SCHEMA; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.MISSING_TABLE; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.MUST_BE_AGGREGATE_OR_GROUP_BY; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.MUST_BE_WINDOW_FUNCTION; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.NESTED_WINDOW; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.NON_NUMERIC_SAMPLE_PERCENTAGE; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.NOT_SUPPORTED; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.ORDER_BY_MUST_BE_IN_SELECT; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.TYPE_MISMATCH; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.VIEW_ANALYSIS_ERROR; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.VIEW_IS_STALE; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.VIEW_PARSE_ERROR; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.WILDCARD_WITHOUT_FROM; +import static com.facebook.presto.sql.planner.ExpressionInterpreter.expressionOptimizer; +import static com.facebook.presto.sql.tree.ComparisonExpression.Type.EQUAL; +import static com.facebook.presto.sql.tree.FrameBound.Type.CURRENT_ROW; +import static com.facebook.presto.sql.tree.FrameBound.Type.FOLLOWING; +import static com.facebook.presto.sql.tree.FrameBound.Type.PRECEDING; +import static com.facebook.presto.sql.tree.FrameBound.Type.UNBOUNDED_FOLLOWING; +import static com.facebook.presto.sql.tree.FrameBound.Type.UNBOUNDED_PRECEDING; +import static com.facebook.presto.sql.tree.WindowFrame.Type.RANGE; +import static com.facebook.presto.type.UnknownType.UNKNOWN; +import static com.facebook.presto.util.ImmutableCollectors.toImmutableList; +import static com.facebook.presto.util.Types.checkType; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +public class TupleAnalyzer + extends DefaultTraversalVisitor +{ + private final Analysis analysis; + private final Session session; + private final Metadata metadata; + private final SqlParser sqlParser; + private final boolean experimentalSyntaxEnabled; + + public TupleAnalyzer(Analysis analysis, Session session, Metadata metadata, SqlParser sqlParser, boolean experimentalSyntaxEnabled) + { + checkNotNull(analysis, "analysis is null"); + checkNotNull(session, "session is null"); + checkNotNull(metadata, "metadata is null"); + + this.analysis = analysis; + this.session = session; + this.metadata = metadata; + this.sqlParser = sqlParser; + this.experimentalSyntaxEnabled = experimentalSyntaxEnabled; + } + + @Override + protected TupleDescriptor visitUnnest(Unnest node, AnalysisContext context) + { + ImmutableList.Builder outputFields = ImmutableList.builder(); + for (Expression expression : node.getExpressions()) { + ExpressionAnalysis expressionAnalysis = analyzeExpression(expression, context.getLateralTupleDescriptor(), context); + Type expressionType = expressionAnalysis.getType(expression); + if (expressionType instanceof ArrayType) { + outputFields.add(Field.newUnqualified(Optional.empty(), ((ArrayType) expressionType).getElementType())); + } + else if (expressionType instanceof MapType) { + outputFields.add(Field.newUnqualified(Optional.empty(), ((MapType) expressionType).getKeyType())); + outputFields.add(Field.newUnqualified(Optional.empty(), ((MapType) expressionType).getValueType())); + } + else { + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, "Cannot unnest type: " + expressionType); + } + } + if (node.isWithOrdinality()) { + outputFields.add(Field.newUnqualified(Optional.empty(), BigintType.BIGINT)); + } + TupleDescriptor descriptor = new TupleDescriptor(outputFields.build()); + analysis.setOutputDescriptor(node, descriptor); + return descriptor; + } + + @Override + protected TupleDescriptor visitTable(Table table, AnalysisContext context) + { + if (!table.getName().getPrefix().isPresent()) { + // is this a reference to a WITH query? + String name = table.getName().getSuffix(); + + Query query = context.getNamedQuery(name); + if (query != null) { + analysis.registerNamedQuery(table, query); + + // re-alias the fields with the name assigned to the query in the WITH declaration + TupleDescriptor queryDescriptor = analysis.getOutputDescriptor(query); + ImmutableList.Builder fields = ImmutableList.builder(); + for (Field field : queryDescriptor.getAllFields()) { + fields.add(Field.newQualified(QualifiedName.of(name), field.getName(), field.getType(), false)); + } + + TupleDescriptor descriptor = new TupleDescriptor(fields.build()); + analysis.setOutputDescriptor(table, descriptor); + return descriptor; + } + } + + QualifiedTableName name = MetadataUtil.createQualifiedTableName(session, table.getName()); + + Optional optionalView = metadata.getView(session, name); + if (optionalView.isPresent()) { + ViewDefinition view = optionalView.get(); + + Query query = parseView(view.getOriginalSql(), name, table); + + analysis.registerNamedQuery(table, query); + + TupleDescriptor descriptor = analyzeView(query, name, view.getCatalog(), view.getSchema(), table); + + if (isViewStale(view.getColumns(), descriptor.getVisibleFields())) { + throw new SemanticException(VIEW_IS_STALE, table, "View '%s' is stale; it must be re-created", name); + } + + analysis.setOutputDescriptor(table, descriptor); + return descriptor; + } + + Optional tableHandle = metadata.getTableHandle(session, name); + if (!tableHandle.isPresent()) { + if (!metadata.getCatalogNames().containsKey(name.getCatalogName())) { + throw new SemanticException(MISSING_CATALOG, table, "Catalog %s does not exist", name.getCatalogName()); + } + if (!metadata.listSchemaNames(session, name.getCatalogName()).contains(name.getSchemaName())) { + throw new SemanticException(MISSING_SCHEMA, table, "Schema %s does not exist", name.getSchemaName()); + } + + if (table.getName().getSuffix().equalsIgnoreCase("DUAL")) { + // TODO: remove this in a few releases + throw new SemanticException(MISSING_TABLE, table, "DUAL table is no longer supported. Please use VALUES or FROM-less queries instead"); + } + + throw new SemanticException(MISSING_TABLE, table, "Table %s does not exist", name); + } + TableMetadata tableMetadata = metadata.getTableMetadata(tableHandle.get()); + Map columnHandles = metadata.getColumnHandles(tableHandle.get()); + + // TODO: discover columns lazily based on where they are needed (to support datasources that can't enumerate all tables) + ImmutableList.Builder fields = ImmutableList.builder(); + for (ColumnMetadata column : tableMetadata.getColumns()) { + Field field = Field.newQualified(table.getName(), Optional.of(column.getName()), column.getType(), column.isHidden()); + fields.add(field); + ColumnHandle columnHandle = columnHandles.get(column.getName()); + checkArgument(columnHandle != null, "Unknown field %s", field); + analysis.setColumn(field, columnHandle); + } + + analysis.registerTable(table, tableHandle.get()); + + TupleDescriptor descriptor = new TupleDescriptor(fields.build()); + analysis.setOutputDescriptor(table, descriptor); + return descriptor; + } + + @Override + protected TupleDescriptor visitAliasedRelation(AliasedRelation relation, AnalysisContext context) + { + TupleDescriptor child = process(relation.getRelation(), context); + + // todo this check should be inside of TupleDescriptor.withAlias, but the exception needs the node object + if (relation.getColumnNames() != null) { + int totalColumns = child.getVisibleFieldCount(); + if (totalColumns != relation.getColumnNames().size()) { + throw new SemanticException(MISMATCHED_COLUMN_ALIASES, relation, "Column alias list has %s entries but '%s' has %s columns available", relation.getColumnNames().size(), relation.getAlias(), totalColumns); + } + } + + TupleDescriptor descriptor = child.withAlias(relation.getAlias(), relation.getColumnNames()); + + analysis.setOutputDescriptor(relation, descriptor); + return descriptor; + } + + @Override + protected TupleDescriptor visitSampledRelation(final SampledRelation relation, AnalysisContext context) + { + if (relation.getColumnsToStratifyOn().isPresent()) { + throw new SemanticException(NOT_SUPPORTED, relation, "STRATIFY ON is not yet implemented"); + } + + if (!DependencyExtractor.extract(relation.getSamplePercentage()).isEmpty()) { + throw new SemanticException(NON_NUMERIC_SAMPLE_PERCENTAGE, relation.getSamplePercentage(), "Sample percentage cannot contain column references"); + } + + IdentityHashMap expressionTypes = getExpressionTypes(session, metadata, sqlParser, ImmutableMap.of(), relation.getSamplePercentage()); + ExpressionInterpreter samplePercentageEval = expressionOptimizer(relation.getSamplePercentage(), metadata, session, expressionTypes); + + Object samplePercentageObject = samplePercentageEval.optimize(new SymbolResolver() + { + @Override + public Object getValue(Symbol symbol) + { + throw new SemanticException(NON_NUMERIC_SAMPLE_PERCENTAGE, relation.getSamplePercentage(), "Sample percentage cannot contain column references"); + } + }); + + if (!(samplePercentageObject instanceof Number)) { + throw new SemanticException(NON_NUMERIC_SAMPLE_PERCENTAGE, relation.getSamplePercentage(), "Sample percentage should evaluate to a numeric expression"); + } + + double samplePercentageValue = ((Number) samplePercentageObject).doubleValue(); + + if (samplePercentageValue < 0.0) { + throw new SemanticException(SemanticErrorCode.SAMPLE_PERCENTAGE_OUT_OF_RANGE, relation.getSamplePercentage(), "Sample percentage must be greater than or equal to 0"); + } + else if ((samplePercentageValue > 100.0) && (relation.getType() != SampledRelation.Type.POISSONIZED || relation.isRescaled())) { + throw new SemanticException(SemanticErrorCode.SAMPLE_PERCENTAGE_OUT_OF_RANGE, relation.getSamplePercentage(), "Sample percentage must be less than or equal to 100"); + } + + if (relation.isRescaled() && !experimentalSyntaxEnabled) { + throw new SemanticException(NOT_SUPPORTED, relation, "Rescaling is not enabled"); + } + + TupleDescriptor descriptor = process(relation.getRelation(), context); + + analysis.setOutputDescriptor(relation, descriptor); + analysis.setSampleRatio(relation, samplePercentageValue / 100); + + return descriptor; + } + + @Override + protected TupleDescriptor visitTableSubquery(TableSubquery node, AnalysisContext context) + { + StatementAnalyzer analyzer = new StatementAnalyzer(analysis, metadata, sqlParser, session, experimentalSyntaxEnabled, Optional.empty()); + TupleDescriptor descriptor = analyzer.process(node.getQuery(), context); + + analysis.setOutputDescriptor(node, descriptor); + + return descriptor; + } + + @Override + protected TupleDescriptor visitQuerySpecification(QuerySpecification node, AnalysisContext parentContext) + { + // TODO: extract candidate names from SELECT, WHERE, HAVING, GROUP BY and ORDER BY expressions + // to pass down to analyzeFrom + + AnalysisContext context = new AnalysisContext(parentContext); + + TupleDescriptor tupleDescriptor = analyzeFrom(node, context); + + node.getWhere().ifPresent(where -> analyzeWhere(node, tupleDescriptor, context, where)); + + List outputExpressions = analyzeSelect(node, tupleDescriptor, context); + List groupByExpressions = analyzeGroupBy(node, tupleDescriptor, context, outputExpressions); + List orderByExpressions = analyzeOrderBy(node, tupleDescriptor, context, outputExpressions); + analyzeHaving(node, tupleDescriptor, context); + + analyzeAggregations(node, tupleDescriptor, groupByExpressions, outputExpressions, orderByExpressions, context); + analyzeWindowFunctions(node, outputExpressions, orderByExpressions); + + TupleDescriptor descriptor = computeOutputDescriptor(node, tupleDescriptor); + analysis.setOutputDescriptor(node, descriptor); + + return descriptor; + } + + @Override + protected TupleDescriptor visitUnion(Union node, AnalysisContext context) + { + checkState(node.getRelations().size() >= 2); + + TupleAnalyzer analyzer = new TupleAnalyzer(analysis, session, metadata, sqlParser, experimentalSyntaxEnabled); + + // Use the first descriptor as the output descriptor for the UNION + TupleDescriptor outputDescriptor = analyzer.process(node.getRelations().get(0), context).withOnlyVisibleFields(); + + for (Relation relation : Iterables.skip(node.getRelations(), 1)) { + TupleDescriptor descriptor = analyzer.process(relation, context).withOnlyVisibleFields(); + int outputFieldSize = outputDescriptor.getVisibleFields().size(); + int descFieldSize = descriptor.getVisibleFields().size(); + if (outputFieldSize != descFieldSize) { + throw new SemanticException(MISMATCHED_SET_COLUMN_TYPES, + node, + "union query has different number of fields: %d, %d", + outputFieldSize, descFieldSize); + } + for (int i = 0; i < descriptor.getVisibleFields().size(); i++) { + Type outputFieldType = outputDescriptor.getFieldByIndex(i).getType(); + Type descFieldType = descriptor.getFieldByIndex(i).getType(); + if (!outputFieldType.equals(descFieldType)) { + throw new SemanticException(TYPE_MISMATCH, + node, + "column %d in union query has incompatible types: %s, %s", + i, outputFieldType.getDisplayName(), descFieldType.getDisplayName()); + } + } + } + + analysis.setOutputDescriptor(node, outputDescriptor); + return outputDescriptor; + } + + @Override + protected TupleDescriptor visitIntersect(Intersect node, AnalysisContext context) + { + throw new SemanticException(NOT_SUPPORTED, node, "INTERSECT not yet implemented"); + } + + @Override + protected TupleDescriptor visitExcept(Except node, AnalysisContext context) + { + throw new SemanticException(NOT_SUPPORTED, node, "EXCEPT not yet implemented"); + } + + @Override + protected TupleDescriptor visitJoin(Join node, AnalysisContext context) + { + JoinCriteria criteria = node.getCriteria().orElse(null); + if (criteria instanceof NaturalJoin) { + throw new SemanticException(NOT_SUPPORTED, node, "Natural join not supported"); + } + + AnalysisContext leftContext = new AnalysisContext(context); + TupleDescriptor left = process(node.getLeft(), context); + leftContext.setLateralTupleDescriptor(left); + TupleDescriptor right = process(node.getRight(), leftContext); + + // todo this check should be inside of TupleDescriptor.join and then remove the public getRelationAlias method, but the exception needs the node object + Sets.SetView duplicateAliases = Sets.intersection(left.getRelationAliases(), right.getRelationAliases()); + if (!duplicateAliases.isEmpty()) { + throw new SemanticException(DUPLICATE_RELATION, node, "Relations appear more than once: %s", duplicateAliases); + } + + TupleDescriptor output = left.joinWith(right); + + if (node.getType() == Join.Type.CROSS || node.getType() == Join.Type.IMPLICIT) { + analysis.setOutputDescriptor(node, output); + return output; + } + + if (criteria instanceof JoinUsing) { + // TODO: implement proper "using" semantics with respect to output columns + List columns = ((JoinUsing) criteria).getColumns(); + + List expressions = new ArrayList<>(); + for (String column : columns) { + Expression leftExpression = new QualifiedNameReference(QualifiedName.of(column)); + Expression rightExpression = new QualifiedNameReference(QualifiedName.of(column)); + + ExpressionAnalysis leftExpressionAnalysis = analyzeExpression(leftExpression, left, context); + ExpressionAnalysis rightExpressionAnalysis = analyzeExpression(rightExpression, right, context); + checkState(leftExpressionAnalysis.getSubqueryInPredicates().isEmpty(), "INVARIANT"); + checkState(rightExpressionAnalysis.getSubqueryInPredicates().isEmpty(), "INVARIANT"); + + addCoercionForJoinCriteria(node, leftExpression, rightExpression); + expressions.add(new ComparisonExpression(EQUAL, leftExpression, rightExpression)); + } + + analysis.setJoinCriteria(node, ExpressionUtils.and(expressions)); + } + else if (criteria instanceof JoinOn) { + Expression expression = ((JoinOn) criteria).getExpression(); + + // ensure all names can be resolved, types match, etc (we don't need to record resolved names, subexpression types, etc. because + // we do it further down when after we determine which subexpressions apply to left vs right tuple) + ExpressionAnalyzer analyzer = ExpressionAnalyzer.create(analysis, session, metadata, sqlParser, experimentalSyntaxEnabled); + analyzer.analyze(expression, output, context); + + Analyzer.verifyNoAggregatesOrWindowFunctions(metadata, expression, "JOIN"); + + // expressionInterpreter/optimizer only understands a subset of expression types + // TODO: remove this when the new expression tree is implemented + Expression canonicalized = CanonicalizeExpressions.canonicalizeExpression(expression); + + Object optimizedExpression = expressionOptimizer(canonicalized, metadata, session, analyzer.getExpressionTypes()).optimize(NoOpSymbolResolver.INSTANCE); + + if (!(optimizedExpression instanceof Expression) && optimizedExpression instanceof Boolean) { + // If the JoinOn clause evaluates to a boolean expression, simulate a cross join by adding the relevant redundant expression + if (optimizedExpression.equals(Boolean.TRUE)) { + optimizedExpression = new ComparisonExpression(EQUAL, new LongLiteral("0"), new LongLiteral("0")); + } + else { + optimizedExpression = new ComparisonExpression(EQUAL, new LongLiteral("0"), new LongLiteral("1")); + } + } + + if (!(optimizedExpression instanceof Expression)) { + throw new SemanticException(TYPE_MISMATCH, node, "Join clause must be a boolean expression"); + } + // The optimization above may have rewritten the expression tree which breaks all the identity maps, so redo the analysis + // to re-analyze coercions that might be necessary + analyzer = ExpressionAnalyzer.create(analysis, session, metadata, sqlParser, experimentalSyntaxEnabled); + analyzer.analyze((Expression) optimizedExpression, output, context); + analysis.addCoercions(analyzer.getExpressionCoercions()); + + for (Expression conjunct : ExpressionUtils.extractConjuncts((Expression) optimizedExpression)) { + if (!(conjunct instanceof ComparisonExpression)) { + throw new SemanticException(NOT_SUPPORTED, node, "Non-equi joins not supported: %s", conjunct); + } + + ComparisonExpression comparison = (ComparisonExpression) conjunct; + Set firstDependencies = DependencyExtractor.extract(comparison.getLeft()); + Set secondDependencies = DependencyExtractor.extract(comparison.getRight()); + + Expression leftExpression; + Expression rightExpression; + if (Iterables.all(firstDependencies, left.canResolvePredicate()) && Iterables.all(secondDependencies, right.canResolvePredicate())) { + leftExpression = comparison.getLeft(); + rightExpression = comparison.getRight(); + } + else if (Iterables.all(firstDependencies, right.canResolvePredicate()) && Iterables.all(secondDependencies, left.canResolvePredicate())) { + leftExpression = comparison.getRight(); + rightExpression = comparison.getLeft(); + } + else { + // must have a complex expression that involves both tuples on one side of the comparison expression (e.g., coalesce(left.x, right.x) = 1) + throw new SemanticException(NOT_SUPPORTED, node, "Non-equi joins not supported: %s", conjunct); + } + + // analyze the clauses to record the types of all subexpressions and resolve names against the left/right underlying tuples + ExpressionAnalysis leftExpressionAnalysis = analyzeExpression(leftExpression, left, context); + ExpressionAnalysis rightExpressionAnalysis = analyzeExpression(rightExpression, right, context); + addCoercionForJoinCriteria(node, leftExpression, rightExpression); + analysis.addJoinInPredicates(node, new Analysis.JoinInPredicates(leftExpressionAnalysis.getSubqueryInPredicates(), rightExpressionAnalysis.getSubqueryInPredicates())); + } + + analysis.setJoinCriteria(node, (Expression) optimizedExpression); + } + else { + throw new UnsupportedOperationException("unsupported join criteria: " + criteria.getClass().getName()); + } + + analysis.setOutputDescriptor(node, output); + return output; + } + + private void addCoercionForJoinCriteria(Join node, Expression leftExpression, Expression rightExpression) + { + Type leftType = analysis.getType(leftExpression); + Type rightType = analysis.getType(rightExpression); + Optional superType = FunctionRegistry.getCommonSuperType(leftType, rightType); + if (!superType.isPresent()) { + throw new SemanticException(TYPE_MISMATCH, node, "Join criteria has incompatible types: %s, %s", leftType.getDisplayName(), rightType.getDisplayName()); + } + if (!leftType.equals(superType.get())) { + analysis.addCoercion(leftExpression, superType.get()); + } + if (!rightType.equals(superType.get())) { + analysis.addCoercion(rightExpression, superType.get()); + } + } + + @Override + protected TupleDescriptor visitValues(Values node, AnalysisContext context) + { + checkState(node.getRows().size() >= 1); + + // get unique row types + Set> rowTypes = node.getRows().stream() + .map(row -> analyzeExpression(row, new TupleDescriptor(), context).getType(row)) + .map(type -> { + if (type instanceof RowType) { + return type.getTypeParameters(); + } + return ImmutableList.of(type); + }) + .collect(ImmutableCollectors.toImmutableSet()); + + // determine common super type of the rows + List fieldTypes = new ArrayList<>(rowTypes.iterator().next()); + for (List rowType : rowTypes) { + for (int i = 0; i < rowType.size(); i++) { + Type fieldType = rowType.get(i); + Type superType = fieldTypes.get(i); + + Optional commonSuperType = getCommonSuperType(fieldType, superType); + if (!commonSuperType.isPresent()) { + throw new SemanticException(MISMATCHED_SET_COLUMN_TYPES, + node, + "Values rows have mismatched types: %s vs %s", + Iterables.get(rowTypes, 0), + Iterables.get(rowTypes, 1)); + } + fieldTypes.set(i, commonSuperType.get()); + } + } + + // add coercions for the rows + for (Expression row : node.getRows()) { + if (row instanceof Row) { + List items = ((Row) row).getItems(); + for (int i = 0; i < items.size(); i++) { + Type expectedType = fieldTypes.get(i); + Expression item = items.get(i); + if (!analysis.getType(item).equals(expectedType)) { + analysis.addCoercion(item, expectedType); + } + } + } + else { + Type expectedType = fieldTypes.get(0); + if (!analysis.getType(row).equals(expectedType)) { + analysis.addCoercion(row, expectedType); + } + } + } + + TupleDescriptor descriptor = new TupleDescriptor(fieldTypes.stream() + .map(valueType -> Field.newUnqualified(Optional.empty(), valueType)) + .collect(toImmutableList())); + + analysis.setOutputDescriptor(node, descriptor); + return descriptor; + } + + private void analyzeWindowFunctions(QuerySpecification node, List outputExpressions, List orderByExpressions) + { + WindowFunctionExtractor extractor = new WindowFunctionExtractor(); + + for (FieldOrExpression fieldOrExpression : Iterables.concat(outputExpressions, orderByExpressions)) { + if (fieldOrExpression.isExpression()) { + extractor.process(fieldOrExpression.getExpression(), null); + new WindowFunctionValidator().process(fieldOrExpression.getExpression(), analysis); + } + } + + List windowFunctions = extractor.getWindowFunctions(); + + for (FunctionCall windowFunction : windowFunctions) { + Window window = windowFunction.getWindow().get(); + + WindowFunctionExtractor nestedExtractor = new WindowFunctionExtractor(); + for (Expression argument : windowFunction.getArguments()) { + nestedExtractor.process(argument, null); + } + + for (Expression expression : window.getPartitionBy()) { + nestedExtractor.process(expression, null); + } + + for (SortItem sortItem : window.getOrderBy()) { + nestedExtractor.process(sortItem.getSortKey(), null); + } + + if (window.getFrame().isPresent()) { + nestedExtractor.process(window.getFrame().get(), null); + } + + if (!nestedExtractor.getWindowFunctions().isEmpty()) { + throw new SemanticException(NESTED_WINDOW, node, "Cannot nest window functions inside window function '%s': %s", + windowFunction, + extractor.getWindowFunctions()); + } + + if (windowFunction.isDistinct()) { + throw new SemanticException(NOT_SUPPORTED, node, "DISTINCT in window function parameters not yet supported: %s", windowFunction); + } + + if (window.getFrame().isPresent()) { + analyzeWindowFrame(window.getFrame().get()); + } + + List argumentTypes = Lists.transform(windowFunction.getArguments(), expression -> analysis.getType(expression).getTypeSignature()); + + FunctionInfo info = metadata.resolveFunction(windowFunction.getName(), argumentTypes, false); + if (!info.isWindow()) { + throw new SemanticException(MUST_BE_WINDOW_FUNCTION, node, "Not a window function: %s", windowFunction.getName()); + } + } + + analysis.setWindowFunctions(node, windowFunctions); + } + + private static void analyzeWindowFrame(WindowFrame frame) + { + FrameBound.Type startType = frame.getStart().getType(); + FrameBound.Type endType = frame.getEnd().orElse(new FrameBound(CURRENT_ROW)).getType(); + + if (startType == UNBOUNDED_FOLLOWING) { + throw new SemanticException(INVALID_WINDOW_FRAME, frame, "Window frame start cannot be UNBOUNDED FOLLOWING"); + } + if (endType == UNBOUNDED_PRECEDING) { + throw new SemanticException(INVALID_WINDOW_FRAME, frame, "Window frame end cannot be UNBOUNDED PRECEDING"); + } + if ((startType == CURRENT_ROW) && (endType == PRECEDING)) { + throw new SemanticException(INVALID_WINDOW_FRAME, frame, "Window frame starting from CURRENT ROW cannot end with PRECEDING"); + } + if ((startType == FOLLOWING) && (endType == PRECEDING)) { + throw new SemanticException(INVALID_WINDOW_FRAME, frame, "Window frame starting from FOLLOWING cannot end with PRECEDING"); + } + if ((startType == FOLLOWING) && (endType == CURRENT_ROW)) { + throw new SemanticException(INVALID_WINDOW_FRAME, frame, "Window frame starting from FOLLOWING cannot end with CURRENT ROW"); + } + if ((frame.getType() == RANGE) && ((startType == PRECEDING) || (endType == PRECEDING))) { + throw new SemanticException(INVALID_WINDOW_FRAME, frame, "Window frame RANGE PRECEDING is only supported with UNBOUNDED"); + } + if ((frame.getType() == RANGE) && ((startType == FOLLOWING) || (endType == FOLLOWING))) { + throw new SemanticException(INVALID_WINDOW_FRAME, frame, "Window frame RANGE FOLLOWING is only supported with UNBOUNDED"); + } + } + + private void analyzeHaving(QuerySpecification node, TupleDescriptor tupleDescriptor, AnalysisContext context) + { + if (node.getHaving().isPresent()) { + Expression predicate = node.getHaving().get(); + + ExpressionAnalysis expressionAnalysis = analyzeExpression(predicate, tupleDescriptor, context); + analysis.addInPredicates(node, expressionAnalysis.getSubqueryInPredicates()); + + Type predicateType = expressionAnalysis.getType(predicate); + if (!predicateType.equals(BOOLEAN) && !predicateType.equals(UNKNOWN)) { + throw new SemanticException(TYPE_MISMATCH, predicate, "HAVING clause must evaluate to a boolean: actual type %s", predicateType); + } + + analysis.setHaving(node, predicate); + } + } + + private List analyzeOrderBy(QuerySpecification node, TupleDescriptor tupleDescriptor, AnalysisContext context, List outputExpressions) + { + List items = node.getOrderBy(); + + ImmutableList.Builder orderByExpressionsBuilder = ImmutableList.builder(); + + if (!items.isEmpty()) { + // Compute aliased output terms so we can resolve order by expressions against them first + ImmutableMultimap.Builder byAliasBuilder = ImmutableMultimap.builder(); + for (SelectItem item : node.getSelect().getSelectItems()) { + if (item instanceof SingleColumn) { + Optional alias = ((SingleColumn) item).getAlias(); + if (alias.isPresent()) { + byAliasBuilder.put(QualifiedName.of(alias.get()), ((SingleColumn) item).getExpression()); // TODO: need to know if alias was quoted + } + } + } + Multimap byAlias = byAliasBuilder.build(); + + for (SortItem item : items) { + Expression expression = item.getSortKey(); + + FieldOrExpression orderByExpression = null; + if (expression instanceof QualifiedNameReference && !((QualifiedNameReference) expression).getName().getPrefix().isPresent()) { + // if this is a simple name reference, try to resolve against output columns + + QualifiedName name = ((QualifiedNameReference) expression).getName(); + Collection expressions = byAlias.get(name); + if (expressions.size() > 1) { + throw new SemanticException(AMBIGUOUS_ATTRIBUTE, expression, "'%s' in ORDER BY is ambiguous", name.getSuffix()); + } + else if (expressions.size() == 1) { + orderByExpression = new FieldOrExpression(Iterables.getOnlyElement(expressions)); + } + + // otherwise, couldn't resolve name against output aliases, so fall through... + } + else if (expression instanceof LongLiteral) { + // this is an ordinal in the output tuple + + long ordinal = ((LongLiteral) expression).getValue(); + if (ordinal < 1 || ordinal > outputExpressions.size()) { + throw new SemanticException(INVALID_ORDINAL, expression, "ORDER BY position %s is not in select list", ordinal); + } + + orderByExpression = outputExpressions.get((int) (ordinal - 1)); + + if (orderByExpression.isExpression()) { + Type type = analysis.getType(orderByExpression.getExpression()); + if (!type.isOrderable()) { + throw new SemanticException(TYPE_MISMATCH, node, "The type of expression in position %s is not orderable (actual: %s), and therefore cannot be used in ORDER BY: %s", ordinal, type, orderByExpression); + } + } + else { + Type type = tupleDescriptor.getFieldByIndex(orderByExpression.getFieldIndex()).getType(); + if (!type.isOrderable()) { + throw new SemanticException(TYPE_MISMATCH, node, "The type of expression in position %s is not orderable (actual: %s), and therefore cannot be used in ORDER BY", ordinal, type); + } + } + } + + // otherwise, just use the expression as is + if (orderByExpression == null) { + orderByExpression = new FieldOrExpression(expression); + } + + if (orderByExpression.isExpression()) { + ExpressionAnalysis expressionAnalysis = analyzeExpression(orderByExpression.getExpression(), tupleDescriptor, context); + analysis.addInPredicates(node, expressionAnalysis.getSubqueryInPredicates()); + + Type type = expressionAnalysis.getType(orderByExpression.getExpression()); + if (!type.isOrderable()) { + throw new SemanticException(TYPE_MISMATCH, node, "Type %s is not orderable, and therefore cannot be used in ORDER BY: %s", type, expression); + } + } + + orderByExpressionsBuilder.add(orderByExpression); + } + } + + List orderByExpressions = orderByExpressionsBuilder.build(); + analysis.setOrderByExpressions(node, orderByExpressions); + + if (node.getSelect().isDistinct() && !outputExpressions.containsAll(orderByExpressions)) { + throw new SemanticException(ORDER_BY_MUST_BE_IN_SELECT, node.getSelect(), "For SELECT DISTINCT, ORDER BY expressions must appear in select list"); + } + return orderByExpressions; + } + + private List analyzeGroupBy(QuerySpecification node, TupleDescriptor tupleDescriptor, AnalysisContext context, List outputExpressions) + { + ImmutableList.Builder groupByExpressionsBuilder = ImmutableList.builder(); + if (!node.getGroupBy().isEmpty()) { + // Translate group by expressions that reference ordinals + for (Expression expression : node.getGroupBy()) { + // first, see if this is an ordinal + FieldOrExpression groupByExpression; + + if (expression instanceof LongLiteral) { + long ordinal = ((LongLiteral) expression).getValue(); + if (ordinal < 1 || ordinal > outputExpressions.size()) { + throw new SemanticException(INVALID_ORDINAL, expression, "GROUP BY position %s is not in select list", ordinal); + } + + groupByExpression = outputExpressions.get((int) (ordinal - 1)); + } + else { + ExpressionAnalysis expressionAnalysis = analyzeExpression(expression, tupleDescriptor, context); + analysis.addInPredicates(node, expressionAnalysis.getSubqueryInPredicates()); + groupByExpression = new FieldOrExpression(expression); + } + + Type type; + if (groupByExpression.isExpression()) { + Analyzer.verifyNoAggregatesOrWindowFunctions(metadata, groupByExpression.getExpression(), "GROUP BY"); + type = analysis.getType(groupByExpression.getExpression()); + } + else { + type = tupleDescriptor.getFieldByIndex(groupByExpression.getFieldIndex()).getType(); + } + if (!type.isComparable()) { + throw new SemanticException(TYPE_MISMATCH, node, "%s is not comparable, and therefore cannot be used in GROUP BY", type); + } + + groupByExpressionsBuilder.add(groupByExpression); + } + } + + List groupByExpressions = groupByExpressionsBuilder.build(); + analysis.setGroupByExpressions(node, groupByExpressions); + return groupByExpressions; + } + + private TupleDescriptor computeOutputDescriptor(QuerySpecification node, TupleDescriptor inputTupleDescriptor) + { + ImmutableList.Builder outputFields = ImmutableList.builder(); + + for (SelectItem item : node.getSelect().getSelectItems()) { + if (item instanceof AllColumns) { + // expand * and T.* + Optional starPrefix = ((AllColumns) item).getPrefix(); + + for (Field field : inputTupleDescriptor.resolveFieldsWithPrefix(starPrefix)) { + outputFields.add(Field.newUnqualified(field.getName(), field.getType())); + } + } + else if (item instanceof SingleColumn) { + SingleColumn column = (SingleColumn) item; + + Optional alias = column.getAlias(); + if (!alias.isPresent() && column.getExpression() instanceof QualifiedNameReference) { + alias = Optional.of(((QualifiedNameReference) column.getExpression()).getName().getSuffix()); + } + + outputFields.add(Field.newUnqualified(alias, analysis.getType(column.getExpression()))); // TODO don't use analysis as a side-channel. Use outputExpressions to look up the type + } + else { + throw new IllegalArgumentException("Unsupported SelectItem type: " + item.getClass().getName()); + } + } + + return new TupleDescriptor(outputFields.build()); + } + + private List analyzeSelect(QuerySpecification node, TupleDescriptor tupleDescriptor, AnalysisContext context) + { + ImmutableList.Builder outputExpressionBuilder = ImmutableList.builder(); + + for (SelectItem item : node.getSelect().getSelectItems()) { + if (item instanceof AllColumns) { + // expand * and T.* + Optional starPrefix = ((AllColumns) item).getPrefix(); + + List fields = tupleDescriptor.resolveFieldsWithPrefix(starPrefix); + if (fields.isEmpty()) { + if (starPrefix.isPresent()) { + throw new SemanticException(MISSING_TABLE, item, "Table '%s' not found", starPrefix.get()); + } + else { + throw new SemanticException(WILDCARD_WITHOUT_FROM, item, "SELECT * not allowed in queries without FROM clause"); + } + } + + for (Field field : fields) { + int fieldIndex = tupleDescriptor.indexOf(field); + outputExpressionBuilder.add(new FieldOrExpression(fieldIndex)); + + if (node.getSelect().isDistinct() && !field.getType().isComparable()) { + throw new SemanticException(TYPE_MISMATCH, node.getSelect(), "DISTINCT can only be applied to comparable types (actual: %s)", field.getType()); + } + } + } + else if (item instanceof SingleColumn) { + SingleColumn column = (SingleColumn) item; + ExpressionAnalysis expressionAnalysis = analyzeExpression(column.getExpression(), tupleDescriptor, context); + analysis.addInPredicates(node, expressionAnalysis.getSubqueryInPredicates()); + outputExpressionBuilder.add(new FieldOrExpression(column.getExpression())); + + Type type = expressionAnalysis.getType(column.getExpression()); + if (node.getSelect().isDistinct() && !type.isComparable()) { + throw new SemanticException(TYPE_MISMATCH, node.getSelect(), "DISTINCT can only be applied to comparable types (actual: %s): %s", type, column.getExpression()); + } + } + else { + throw new IllegalArgumentException("Unsupported SelectItem type: " + item.getClass().getName()); + } + } + + ImmutableList result = outputExpressionBuilder.build(); + analysis.setOutputExpressions(node, result); + + return result; + } + + public void analyzeWhere(Node node, TupleDescriptor tupleDescriptor, AnalysisContext context, Expression predicate) + { + Analyzer.verifyNoAggregatesOrWindowFunctions(metadata, predicate, "WHERE"); + + ExpressionAnalysis expressionAnalysis = analyzeExpression(predicate, tupleDescriptor, context); + analysis.addInPredicates(node, expressionAnalysis.getSubqueryInPredicates()); + + Type predicateType = expressionAnalysis.getType(predicate); + if (!predicateType.equals(BOOLEAN)) { + if (!predicateType.equals(UNKNOWN)) { + throw new SemanticException(TYPE_MISMATCH, predicate, "WHERE clause must evaluate to a boolean: actual type %s", predicateType); + } + // coerce null to boolean + analysis.addCoercion(predicate, BOOLEAN); + } + + analysis.setWhere(node, predicate); + } + + private TupleDescriptor analyzeFrom(QuerySpecification node, AnalysisContext context) + { + TupleDescriptor fromDescriptor = new TupleDescriptor(); + + if (node.getFrom().isPresent()) { + TupleAnalyzer analyzer = new TupleAnalyzer(analysis, session, metadata, sqlParser, experimentalSyntaxEnabled); + fromDescriptor = analyzer.process(node.getFrom().get(), context); + } + + return fromDescriptor; + } + + private void analyzeAggregations(QuerySpecification node, + TupleDescriptor tupleDescriptor, + List groupByExpressions, + List outputExpressions, + List orderByExpressions, + AnalysisContext context) + { + List aggregates = extractAggregates(node); + + if (context.isApproximate()) { + if (Iterables.any(aggregates, FunctionCall::isDistinct)) { + throw new SemanticException(NOT_SUPPORTED, node, "DISTINCT aggregations not supported for approximate queries"); + } + } + + // is this an aggregation query? + if (!aggregates.isEmpty() || !groupByExpressions.isEmpty()) { + // ensure SELECT, ORDER BY and HAVING are constant with respect to group + // e.g, these are all valid expressions: + // SELECT f(a) GROUP BY a + // SELECT f(a + 1) GROUP BY a + 1 + // SELECT a + sum(b) GROUP BY a + for (FieldOrExpression fieldOrExpression : Iterables.concat(outputExpressions, orderByExpressions)) { + verifyAggregations(node, groupByExpressions, tupleDescriptor, fieldOrExpression); + } + + if (node.getHaving().isPresent()) { + verifyAggregations(node, groupByExpressions, tupleDescriptor, new FieldOrExpression(node.getHaving().get())); + } + } + } + + private List extractAggregates(QuerySpecification node) + { + AggregateExtractor extractor = new AggregateExtractor(metadata); + for (SelectItem item : node.getSelect().getSelectItems()) { + if (item instanceof SingleColumn) { + ((SingleColumn) item).getExpression().accept(extractor, null); + } + } + + for (SortItem item : node.getOrderBy()) { + item.getSortKey().accept(extractor, null); + } + + if (node.getHaving().isPresent()) { + node.getHaving().get().accept(extractor, null); + } + + List aggregates = extractor.getAggregates(); + analysis.setAggregates(node, aggregates); + + return aggregates; + } + + private void verifyAggregations(QuerySpecification node, List groupByExpressions, TupleDescriptor tupleDescriptor, FieldOrExpression fieldOrExpression) + { + AggregationAnalyzer analyzer = new AggregationAnalyzer(groupByExpressions, metadata, tupleDescriptor); + + if (fieldOrExpression.isExpression()) { + analyzer.analyze(fieldOrExpression.getExpression()); + } + else { + int fieldIndex = fieldOrExpression.getFieldIndex(); + if (!analyzer.analyze(fieldIndex)) { + Field field = tupleDescriptor.getFieldByIndex(fieldIndex); + + if (field.getRelationAlias().isPresent()) { + if (field.getName().isPresent()) { + throw new SemanticException(MUST_BE_AGGREGATE_OR_GROUP_BY, node, "Column '%s.%s' not in GROUP BY clause", field.getRelationAlias().get(), field.getName().get()); + } + else { + throw new SemanticException(MUST_BE_AGGREGATE_OR_GROUP_BY, node, "Columns from '%s' not in GROUP BY clause", field.getRelationAlias().get()); + } + } + else { + if (field.getName().isPresent()) { + throw new SemanticException(MUST_BE_AGGREGATE_OR_GROUP_BY, node, "Column '%s' not in GROUP BY clause", field.getName().get()); + } + else { + throw new SemanticException(MUST_BE_AGGREGATE_OR_GROUP_BY, node, "Some columns from FROM clause not in GROUP BY clause"); + } + } + } + } + } + + private TupleDescriptor analyzeView(Query query, QualifiedTableName name, String catalog, String schema, Table node) + { + try { + Session viewSession = Session.builder() + .setUser(session.getUser()) + .setSource(session.getSource()) + .setCatalog(catalog) + .setSchema(schema) + .setTimeZoneKey(session.getTimeZoneKey()) + .setLocale(session.getLocale()) + .setRemoteUserAddress(session.getRemoteUserAddress()) + .setUserAgent(session.getUserAgent()) + .setStartTime(session.getStartTime()) + .build(); + + StatementAnalyzer analyzer = new StatementAnalyzer(analysis, metadata, sqlParser, viewSession, experimentalSyntaxEnabled, Optional.empty()); + return analyzer.process(query, new AnalysisContext()); + } + catch (RuntimeException e) { + throw new SemanticException(VIEW_ANALYSIS_ERROR, node, "Failed analyzing stored view '%s': %s", name, e.getMessage()); + } + } + + private Query parseView(String view, QualifiedTableName name, Table node) + { + try { + Statement statement = sqlParser.createStatement(view); + return checkType(statement, Query.class, "parsed view"); + } + catch (ParsingException e) { + throw new SemanticException(VIEW_PARSE_ERROR, node, "Failed parsing stored view '%s': %s", name, e.getMessage()); + } + } + + private static boolean isViewStale(List columns, Collection fields) + { + if (columns.size() != fields.size()) { + return true; + } + + List fieldList = ImmutableList.copyOf(fields); + for (int i = 0; i < columns.size(); i++) { + ViewColumn column = columns.get(i); + Field field = fieldList.get(i); + if (!column.getName().equals(field.getName().orElse(null)) || + !column.getType().equals(field.getType())) { + return true; + } + } + + return false; + } + + private ExpressionAnalysis analyzeExpression(Expression expression, TupleDescriptor tupleDescriptor, AnalysisContext context) + { + return ExpressionAnalyzer.analyzeExpression( + session, + metadata, + sqlParser, + tupleDescriptor, + analysis, + experimentalSyntaxEnabled, + context, + expression); + } + + public static class DependencyExtractor + { + public static Set extract(Expression expression) + { + ImmutableSet.Builder builder = ImmutableSet.builder(); + + Visitor visitor = new Visitor(); + visitor.process(expression, builder); + + return builder.build(); + } + + private static class Visitor + extends DefaultExpressionTraversalVisitor> + { + @Override + protected Void visitQualifiedNameReference(QualifiedNameReference node, ImmutableSet.Builder builder) + { + builder.add(node.getName()); + return null; + } + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/analyzer/TupleDescriptor.java b/presto-main/src/main/java/com/facebook/presto/sql/analyzer/TupleDescriptor.java new file mode 100644 index 00000000..356dd285 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/analyzer/TupleDescriptor.java @@ -0,0 +1,210 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.analyzer; + +import com.facebook.presto.sql.tree.QualifiedName; +import com.google.common.base.Predicate; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; + +import javax.annotation.concurrent.Immutable; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static com.facebook.presto.util.ImmutableCollectors.toImmutableList; +import static com.facebook.presto.util.ImmutableCollectors.toImmutableSet; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkElementIndex; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Predicates.not; + +@Immutable +public class TupleDescriptor +{ + private final List visibleFields; + private final List allFields; + + private final Map fieldIndexes; + + public TupleDescriptor(Field... fields) + { + this(ImmutableList.copyOf(fields)); + } + + public TupleDescriptor(List fields) + { + checkNotNull(fields, "fields is null"); + this.allFields = ImmutableList.copyOf(fields); + this.visibleFields = ImmutableList.copyOf(Iterables.filter(fields, not(Field::isHidden))); + + int index = 0; + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (Field field : fields) { + builder.put(field, index++); + } + fieldIndexes = builder.build(); + } + + /** + * Gets the index of the specified field or -1 if not found. + */ + public int indexOf(Field field) + { + return fieldIndexes.get(field); + } + + /** + * Gets the field at the specified index. + */ + public Field getFieldByIndex(int fieldIndex) + { + checkElementIndex(fieldIndex, allFields.size(), "fieldIndex"); + return allFields.get(fieldIndex); + } + + /** + * Gets only the visible fields. + * No assumptions should be made about the order of the fields returned from this method. + * To obtain the index of a field, call indexOf. + */ + public Collection getVisibleFields() + { + return visibleFields; + } + + public int getVisibleFieldCount() + { + return visibleFields.size(); + } + + /** + * Gets all fields including hidden fields. + * No assumptions should be made about the order of the fields returned from this method. + * To obtain the index of a field, call indexOf. + */ + public Collection getAllFields() + { + return ImmutableSet.copyOf(allFields); + } + + /** + * Gets the count of all fields including hidden fields. + */ + public int getAllFieldCount() + { + return allFields.size(); + } + + /** + * Returns all unique relations in this tuple. + * For detecting detecting duplicate relations in a Join. + */ + public Set getRelationAliases() + { + return allFields.stream() + .map(Field::getRelationAlias) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(toImmutableSet()); + } + + /** + * This method is used for SELECT * or x.* queries + */ + public List resolveFieldsWithPrefix(Optional prefix) + { + return visibleFields.stream() + .filter(input -> input.matchesPrefix(prefix)) + .collect(toImmutableList()); + } + + /** + * Gets the index of all columns matching the specified name + */ + public List resolveFields(QualifiedName name) + { + return allFields.stream() + .filter(input -> input.canResolve(name)) + .collect(toImmutableList()); + } + + public Predicate canResolvePredicate() + { + return input -> !resolveFields(input).isEmpty(); + } + + /** + * Creates a new tuple descriptor containing all fields from this tuple descriptor + * and all fields from the specified tuple decriptor. + */ + public TupleDescriptor joinWith(TupleDescriptor other) + { + List fields = ImmutableList.builder() + .addAll(this.allFields) + .addAll(other.allFields) + .build(); + + return new TupleDescriptor(fields); + } + + /** + * Creates a new tuple descriptor with the relation, and, optionally, the columns aliased. + */ + public TupleDescriptor withAlias(String relationAlias, List columnAliases) + { + if (columnAliases != null) { + checkArgument(columnAliases.size() == visibleFields.size(), + "Column alias list has %s entries but '%s' has %s columns available", + columnAliases.size(), + relationAlias, + visibleFields.size()); + } + + ImmutableList.Builder fieldsBuilder = ImmutableList.builder(); + for (int i = 0; i < allFields.size(); i++) { + Field field = allFields.get(i); + Optional columnAlias = field.getName(); + if (columnAliases == null) { + fieldsBuilder.add(Field.newQualified(QualifiedName.of(relationAlias), columnAlias, field.getType(), field.isHidden())); + } + else if (!field.isHidden()) { + // hidden fields are not exposed when there are column aliases + columnAlias = Optional.of(columnAliases.get(i)); + fieldsBuilder.add(Field.newQualified(QualifiedName.of(relationAlias), columnAlias, field.getType(), false)); + } + } + + return new TupleDescriptor(fieldsBuilder.build()); + } + + /** + * Creates a new tuple descriptor containing only the visible fields. + */ + public TupleDescriptor withOnlyVisibleFields() + { + return new TupleDescriptor(visibleFields); + } + + @Override + public String toString() + { + return allFields.toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/analyzer/WindowFunctionExtractor.java b/presto-main/src/main/java/com/facebook/presto/sql/analyzer/WindowFunctionExtractor.java new file mode 100644 index 00000000..671deea9 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/analyzer/WindowFunctionExtractor.java @@ -0,0 +1,42 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.analyzer; + +import com.facebook.presto.sql.tree.DefaultExpressionTraversalVisitor; +import com.facebook.presto.sql.tree.FunctionCall; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +public class WindowFunctionExtractor + extends DefaultExpressionTraversalVisitor +{ + private final ImmutableList.Builder windowFunctions = ImmutableList.builder(); + + @Override + protected Void visitFunctionCall(FunctionCall node, Void context) + { + if (node.getWindow().isPresent()) { + windowFunctions.add(node); + return null; + } + + return super.visitFunctionCall(node, null); + } + + public List getWindowFunctions() + { + return windowFunctions.build(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/analyzer/WindowFunctionValidator.java b/presto-main/src/main/java/com/facebook/presto/sql/analyzer/WindowFunctionValidator.java new file mode 100644 index 00000000..b71b304c --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/analyzer/WindowFunctionValidator.java @@ -0,0 +1,37 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.analyzer; + +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.sql.tree.DefaultExpressionTraversalVisitor; +import com.facebook.presto.sql.tree.FunctionCall; + +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.WINDOW_REQUIRES_OVER; +import static com.google.common.base.Preconditions.checkNotNull; + +public class WindowFunctionValidator + extends DefaultExpressionTraversalVisitor +{ + @Override + protected Void visitFunctionCall(FunctionCall functionCall, Analysis analysis) + { + checkNotNull(analysis, "analysis is null"); + + FunctionInfo functionInfo = analysis.getFunctionInfo(functionCall); + if (functionInfo != null && functionInfo.isWindow() && !functionInfo.isAggregate() && !functionCall.getWindow().isPresent()) { + throw new SemanticException(WINDOW_REQUIRES_OVER, functionCall, "Window function %s requires an OVER clause", functionInfo.getName()); + } + return super.visitFunctionCall(functionCall, analysis); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/gen/AndCodeGenerator.java b/presto-main/src/main/java/com/facebook/presto/sql/gen/AndCodeGenerator.java new file mode 100644 index 00000000..d5a2bd27 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/gen/AndCodeGenerator.java @@ -0,0 +1,102 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.gen; + +import com.facebook.presto.byteCode.Block; +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.Variable; +import com.facebook.presto.byteCode.control.IfStatement; +import com.facebook.presto.byteCode.instruction.LabelNode; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.relational.RowExpression; +import com.google.common.base.Preconditions; + +import java.util.List; + +import static com.facebook.presto.byteCode.expression.ByteCodeExpressions.constantFalse; + +public class AndCodeGenerator + implements ByteCodeGenerator +{ + @Override + public ByteCodeNode generateExpression(Signature signature, ByteCodeGeneratorContext generator, Type returnType, List arguments) + { + Preconditions.checkArgument(arguments.size() == 2); + + Variable wasNull = generator.wasNull(); + Block block = new Block() + .comment("AND") + .setDescription("AND"); + + ByteCodeNode left = generator.generate(arguments.get(0)); + ByteCodeNode right = generator.generate(arguments.get(1)); + + block.append(left); + + IfStatement ifLeftIsNull = new IfStatement("if left wasNull...") + .condition(wasNull); + + LabelNode end = new LabelNode("end"); + ifLeftIsNull.ifTrue() + .comment("clear the null flag, pop left value off stack, and push left null flag on the stack (true)") + .append(wasNull.set(constantFalse())) + .pop(arguments.get(0).getType().getJavaType()) // discard left value + .push(true); + + LabelNode leftIsTrue = new LabelNode("leftIsTrue"); + ifLeftIsNull.ifFalse() + .comment("if left is false, push false, and goto end") + .ifTrueGoto(leftIsTrue) + .push(false) + .gotoLabel(end) + .comment("left was true; push left null flag on the stack (false)") + .visitLabel(leftIsTrue) + .push(false); + + block.append(ifLeftIsNull); + + // At this point we know the left expression was either NULL or TRUE. The stack contains a single boolean + // value for this expression which indicates if the left value was NULL. + + // eval right! + block.append(right); + + IfStatement ifRightIsNull = new IfStatement("if right wasNull..."); + ifRightIsNull.condition() + .append(wasNull); + + // this leaves a single boolean on the stack which is ignored since the value in NULL + ifRightIsNull.ifTrue() + .comment("right was null, pop the right value off the stack; wasNull flag remains set to TRUE") + .pop(arguments.get(1).getType().getJavaType()); + + LabelNode rightIsTrue = new LabelNode("rightIsTrue"); + ifRightIsNull.ifFalse() + .comment("if right is false, pop left null flag off stack, push false and goto end") + .ifTrueGoto(rightIsTrue) + .pop(boolean.class) + .push(false) + .gotoLabel(end) + .comment("right was true; store left null flag (on stack) in wasNull variable, and push true") + .visitLabel(rightIsTrue) + .putVariable(wasNull) + .push(true); + + block.append(ifRightIsNull) + .visitLabel(end); + + return block; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/gen/ArrayGeneratorUtils.java b/presto-main/src/main/java/com/facebook/presto/sql/gen/ArrayGeneratorUtils.java new file mode 100644 index 00000000..211981d2 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/gen/ArrayGeneratorUtils.java @@ -0,0 +1,67 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.gen; + +import com.facebook.presto.byteCode.Scope; +import com.facebook.presto.byteCode.Variable; +import com.facebook.presto.byteCode.expression.ByteCodeExpression; +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.spi.TupleDomain.Function; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; + +import static com.facebook.presto.sql.gen.InvokeFunctionByteCodeExpression.invokeFunction; + +public final class ArrayGeneratorUtils +{ + private ArrayGeneratorUtils() + { + } + + public static ArrayMapByteCodeExpression map(Scope scope, CallSiteBinder binder, TypeManager typeManager, Variable array, FunctionInfo elementFunction) + { + return map( + scope, + binder, + typeManager, + array, + elementFunction.getSignature(), + element -> invokeFunction(scope, binder, elementFunction, element)); + } + + public static ArrayMapByteCodeExpression map( + Scope scope, + CallSiteBinder binder, + TypeManager typeManager, + ByteCodeExpression array, + Signature mapperSignature, + Function mapper) + { + Type fromElementType = typeManager.getType(mapperSignature.getArgumentTypes().get(0)); + Type toElementType = typeManager.getType(mapperSignature.getReturnType()); + return map(scope, binder, array, fromElementType, toElementType, mapper); + } + + public static ArrayMapByteCodeExpression map( + Scope scope, + CallSiteBinder binder, + ByteCodeExpression array, + Type fromElementType, + Type toElementType, + Function mapper) + { + return new ArrayMapByteCodeExpression(scope, binder, array, fromElementType, toElementType, mapper); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/gen/ArrayMapByteCodeExpression.java b/presto-main/src/main/java/com/facebook/presto/sql/gen/ArrayMapByteCodeExpression.java new file mode 100644 index 00000000..ca901c35 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/gen/ArrayMapByteCodeExpression.java @@ -0,0 +1,109 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.gen; + +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.MethodGenerationContext; +import com.facebook.presto.byteCode.Scope; +import com.facebook.presto.byteCode.Variable; +import com.facebook.presto.byteCode.control.ForLoop; +import com.facebook.presto.byteCode.control.IfStatement; +import com.facebook.presto.byteCode.expression.ByteCodeExpression; +import com.facebook.presto.spi.TupleDomain.Function; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.block.BlockBuilderStatus; +import com.facebook.presto.spi.block.VariableWidthBlockBuilder; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; + +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; + +import static com.facebook.presto.byteCode.ParameterizedType.type; +import static com.facebook.presto.byteCode.expression.ByteCodeExpressions.constantInt; +import static com.facebook.presto.byteCode.expression.ByteCodeExpressions.lessThan; +import static com.facebook.presto.byteCode.expression.ByteCodeExpressions.newInstance; +import static com.facebook.presto.byteCode.instruction.VariableInstruction.incrementVariable; +import static com.facebook.presto.sql.gen.SqlTypeByteCodeExpression.constantType; + +public class ArrayMapByteCodeExpression + extends ByteCodeExpression +{ + private static final AtomicLong NEXT_VARIABLE_ID = new AtomicLong(); + + private final com.facebook.presto.byteCode.Block body; + private final String oneLineDescription; + + public ArrayMapByteCodeExpression( + Scope scope, + CallSiteBinder binder, + ByteCodeExpression array, Type fromType, + Type toType, + Function mapper) + { + super(type(Block.class)); + + body = new com.facebook.presto.byteCode.Block(); + + Variable blockBuilder = scope.declareVariable(BlockBuilder.class, "blockBuilder_" + NEXT_VARIABLE_ID.getAndIncrement()); + body.append(blockBuilder.set(newInstance(VariableWidthBlockBuilder.class, newInstance(BlockBuilderStatus.class), array.invoke("getPositionCount", int.class)))); + + Variable element = scope.declareVariable(fromType.getJavaType(), "element_" + NEXT_VARIABLE_ID.getAndIncrement()); + Variable newElement = scope.declareVariable(toType.getJavaType(), "newElement_" + NEXT_VARIABLE_ID.getAndIncrement()); + Variable position = scope.declareVariable(int.class, "position_" + NEXT_VARIABLE_ID.getAndIncrement()); + + // get element, apply function, and write new element to block builder + SqlTypeByteCodeExpression elementTypeConstant = constantType(binder, fromType); + SqlTypeByteCodeExpression newElementTypeConstant = constantType(binder, toType); + com.facebook.presto.byteCode.Block mapElement = new com.facebook.presto.byteCode.Block() + .append(element.set(elementTypeConstant.getValue(array, position))) + .append(newElement.set(mapper.apply(element))) + .append(newElementTypeConstant.writeValue(blockBuilder, newElement)); + + // main loop + body.append(new ForLoop() + .initialize(position.set(constantInt(0))) + .condition(lessThan(position, array.invoke("getPositionCount", int.class))) + .update(incrementVariable(position, (byte) 1)) + .body(new IfStatement() + .condition(array.invoke("isNull", boolean.class, position)) + .ifTrue(blockBuilder.invoke("appendNull", BlockBuilder.class).pop()) + .ifFalse(mapElement))); + + // build block + body.append(blockBuilder.invoke("build", Block.class)); + + // pretty print + oneLineDescription = "arrayMap(" + array + ", element -> " + mapper.apply(element) + ")"; + } + + @Override + public ByteCodeNode getByteCode(MethodGenerationContext generationContext) + { + return body; + } + + @Override + public List getChildNodes() + { + return ImmutableList.of(); + } + + @Override + protected String formatOneLine() + { + return oneLineDescription; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/gen/Binding.java b/presto-main/src/main/java/com/facebook/presto/sql/gen/Binding.java new file mode 100644 index 00000000..cf768623 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/gen/Binding.java @@ -0,0 +1,49 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.gen; + +import java.lang.invoke.MethodType; + +import static com.google.common.base.MoreObjects.toStringHelper; + +public final class Binding +{ + private final long bindingId; + private final MethodType type; + + public Binding(long bindingId, MethodType type) + { + this.bindingId = bindingId; + this.type = type; + } + + public long getBindingId() + { + return bindingId; + } + + public MethodType getType() + { + return type; + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("bindingId", bindingId) + .add("type", type) + .toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/gen/BodyCompiler.java b/presto-main/src/main/java/com/facebook/presto/sql/gen/BodyCompiler.java new file mode 100644 index 00000000..c6f19e69 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/gen/BodyCompiler.java @@ -0,0 +1,24 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.gen; + +import com.facebook.presto.byteCode.ClassDefinition; +import com.facebook.presto.sql.relational.RowExpression; + +import java.util.List; + +public interface BodyCompiler +{ + void generateMethods(ClassDefinition classDefinition, CallSiteBinder callSiteBinder, RowExpression filter, List projections); +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/gen/Bootstrap.java b/presto-main/src/main/java/com/facebook/presto/sql/gen/Bootstrap.java new file mode 100644 index 00000000..af89a90e --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/gen/Bootstrap.java @@ -0,0 +1,64 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.gen; + +import com.facebook.presto.byteCode.DynamicClassLoader; +import com.google.common.base.Throwables; + +import java.lang.invoke.CallSite; +import java.lang.invoke.ConstantCallSite; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Method; + +import static com.google.common.base.Preconditions.checkArgument; + +public final class Bootstrap +{ + public static final Method BOOTSTRAP_METHOD; + + static { + try { + BOOTSTRAP_METHOD = Bootstrap.class.getMethod("bootstrap", MethodHandles.Lookup.class, String.class, MethodType.class, long.class); + } + catch (NoSuchMethodException e) { + throw Throwables.propagate(e); + } + } + + private Bootstrap() + { + } + + public static CallSite bootstrap(MethodHandles.Lookup callerLookup, String name, MethodType type, long bindingId) + { + try { + ClassLoader classLoader = callerLookup.lookupClass().getClassLoader(); + checkArgument(classLoader instanceof DynamicClassLoader, "Expected %s's classloader to be of type %s", callerLookup.lookupClass().getName(), DynamicClassLoader.class.getName()); + + DynamicClassLoader dynamicClassLoader = (DynamicClassLoader) classLoader; + MethodHandle target = dynamicClassLoader.getCallSiteBindings().get(bindingId); + checkArgument(target != null, "Binding %s for function %s%s not found", bindingId, name, type.parameterList()); + + return new ConstantCallSite(target); + } + catch (Throwable e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + throw Throwables.propagate(e); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/gen/ByteCodeExpressionVisitor.java b/presto-main/src/main/java/com/facebook/presto/sql/gen/ByteCodeExpressionVisitor.java new file mode 100644 index 00000000..72c84ae6 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/gen/ByteCodeExpressionVisitor.java @@ -0,0 +1,165 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.gen; + +import com.facebook.presto.byteCode.Block; +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.Scope; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.sql.relational.CallExpression; +import com.facebook.presto.sql.relational.ConstantExpression; +import com.facebook.presto.sql.relational.InputReferenceExpression; +import com.facebook.presto.sql.relational.RowExpressionVisitor; + +import static com.facebook.presto.byteCode.expression.ByteCodeExpressions.constantTrue; +import static com.facebook.presto.byteCode.instruction.Constant.loadBoolean; +import static com.facebook.presto.byteCode.instruction.Constant.loadDouble; +import static com.facebook.presto.byteCode.instruction.Constant.loadFloat; +import static com.facebook.presto.byteCode.instruction.Constant.loadInt; +import static com.facebook.presto.byteCode.instruction.Constant.loadLong; +import static com.facebook.presto.byteCode.instruction.Constant.loadString; +import static com.facebook.presto.sql.gen.ByteCodeUtils.loadConstant; +import static com.facebook.presto.sql.relational.Signatures.CAST; +import static com.facebook.presto.sql.relational.Signatures.COALESCE; +import static com.facebook.presto.sql.relational.Signatures.IF; +import static com.facebook.presto.sql.relational.Signatures.IN; +import static com.facebook.presto.sql.relational.Signatures.IS_NULL; +import static com.facebook.presto.sql.relational.Signatures.NULL_IF; +import static com.facebook.presto.sql.relational.Signatures.SWITCH; + +public class ByteCodeExpressionVisitor + implements RowExpressionVisitor +{ + private final CallSiteBinder callSiteBinder; + private final RowExpressionVisitor fieldReferenceCompiler; + private final FunctionRegistry registry; + + public ByteCodeExpressionVisitor( + CallSiteBinder callSiteBinder, + RowExpressionVisitor fieldReferenceCompiler, + FunctionRegistry registry) + { + this.callSiteBinder = callSiteBinder; + this.fieldReferenceCompiler = fieldReferenceCompiler; + this.registry = registry; + } + + @Override + public ByteCodeNode visitCall(CallExpression call, final Scope scope) + { + ByteCodeGenerator generator; + // special-cased in function registry + if (call.getSignature().getName().equals(CAST)) { + generator = new CastCodeGenerator(); + } + else { + switch (call.getSignature().getName()) { + // lazy evaluation + case IF: + generator = new IfCodeGenerator(); + break; + case NULL_IF: + generator = new NullIfCodeGenerator(); + break; + case SWITCH: + // (SWITCH (WHEN ) (WHEN ) ) + generator = new SwitchCodeGenerator(); + break; + // functions that take null as input + case IS_NULL: + generator = new IsNullCodeGenerator(); + break; + case "IS_DISTINCT_FROM": + generator = new IsDistinctFromCodeGenerator(); + break; + case COALESCE: + generator = new CoalesceCodeGenerator(); + break; + // functions that require varargs and/or complex types (e.g., lists) + case IN: + generator = new InCodeGenerator(); + break; + // optimized implementations (shortcircuiting behavior) + case "AND": + generator = new AndCodeGenerator(); + break; + case "OR": + generator = new OrCodeGenerator(); + break; + default: + generator = new FunctionCallCodeGenerator(); + } + } + + ByteCodeGeneratorContext generatorContext = new ByteCodeGeneratorContext( + this, + scope, + callSiteBinder, + registry); + + return generator.generateExpression(call.getSignature(), generatorContext, call.getType(), call.getArguments()); + } + + @Override + public ByteCodeNode visitConstant(ConstantExpression constant, Scope scope) + { + Object value = constant.getValue(); + Class javaType = constant.getType().getJavaType(); + + Block block = new Block(); + if (value == null) { + return block.comment("constant null") + .append(scope.getVariable("wasNull").set(constantTrue())) + .pushJavaDefault(javaType); + } + + // use LDC for primitives (boolean, short, int, long, float, double) + block.comment("constant " + constant.getType().getTypeSignature()); + if (javaType == boolean.class) { + return block.append(loadBoolean((Boolean) value)); + } + if (javaType == byte.class || javaType == short.class || javaType == int.class) { + return block.append(loadInt(((Number) value).intValue())); + } + if (javaType == long.class) { + return block.append(loadLong((Long) value)); + } + if (javaType == float.class) { + return block.append(loadFloat((Float) value)); + } + if (javaType == double.class) { + return block.append(loadDouble((Double) value)); + } + if (javaType == String.class) { + return block.append(loadString((String) value)); + } + if (javaType == void.class) { + return block; + } + + // bind constant object directly into the call-site using invoke dynamic + Binding binding = callSiteBinder.bind(value, constant.getType().getJavaType()); + + return new Block() + .setDescription("constant " + constant.getType()) + .comment(constant.toString()) + .append(loadConstant(binding)); + } + + @Override + public ByteCodeNode visitInputReference(InputReferenceExpression node, Scope scope) + { + return fieldReferenceCompiler.visitInputReference(node, scope); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/gen/ByteCodeGenerator.java b/presto-main/src/main/java/com/facebook/presto/sql/gen/ByteCodeGenerator.java new file mode 100644 index 00000000..4d19b7b7 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/gen/ByteCodeGenerator.java @@ -0,0 +1,26 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.gen; + +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.relational.RowExpression; + +import java.util.List; + +public interface ByteCodeGenerator +{ + ByteCodeNode generateExpression(Signature signature, ByteCodeGeneratorContext context, Type returnType, List arguments); +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/gen/ByteCodeGeneratorContext.java b/presto-main/src/main/java/com/facebook/presto/sql/gen/ByteCodeGeneratorContext.java new file mode 100644 index 00000000..333ecb15 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/gen/ByteCodeGeneratorContext.java @@ -0,0 +1,87 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.gen; + +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.Scope; +import com.facebook.presto.byteCode.Variable; +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.sql.relational.RowExpression; + +import java.util.List; + +import static com.facebook.presto.sql.gen.ByteCodeUtils.generateInvocation; +import static com.google.common.base.Preconditions.checkNotNull; + +public class ByteCodeGeneratorContext +{ + private final ByteCodeExpressionVisitor byteCodeGenerator; + private final Scope scope; + private final CallSiteBinder callSiteBinder; + private final FunctionRegistry registry; + private final Variable wasNull; + + public ByteCodeGeneratorContext( + ByteCodeExpressionVisitor byteCodeGenerator, + Scope scope, + CallSiteBinder callSiteBinder, + FunctionRegistry registry) + { + checkNotNull(byteCodeGenerator, "byteCodeGenerator is null"); + checkNotNull(scope, "scope is null"); + checkNotNull(callSiteBinder, "callSiteBinder is null"); + checkNotNull(registry, "registry is null"); + + this.byteCodeGenerator = byteCodeGenerator; + this.scope = scope; + this.callSiteBinder = callSiteBinder; + this.registry = registry; + this.wasNull = scope.getVariable("wasNull"); + } + + public Scope getScope() + { + return scope; + } + + public CallSiteBinder getCallSiteBinder() + { + return callSiteBinder; + } + + public ByteCodeNode generate(RowExpression expression) + { + return expression.accept(byteCodeGenerator, scope); + } + + public FunctionRegistry getRegistry() + { + return registry; + } + + /** + * Generates a function call with null handling, automatic binding of session parameter, etc. + */ + public ByteCodeNode generateCall(FunctionInfo function, List arguments) + { + Binding binding = callSiteBinder.bind(function.getMethodHandle()); + return generateInvocation(scope, function, arguments, binding); + } + + public Variable wasNull() + { + return wasNull; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/gen/ByteCodeUtils.java b/presto-main/src/main/java/com/facebook/presto/sql/gen/ByteCodeUtils.java new file mode 100644 index 00000000..af17dbdd --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/gen/ByteCodeUtils.java @@ -0,0 +1,314 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.gen; + +import com.facebook.presto.byteCode.Block; +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.Scope; +import com.facebook.presto.byteCode.Variable; +import com.facebook.presto.byteCode.control.IfStatement; +import com.facebook.presto.byteCode.expression.ByteCodeExpression; +import com.facebook.presto.byteCode.instruction.LabelNode; +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.common.primitives.Primitives; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.util.ArrayList; +import java.util.List; + +import static com.facebook.presto.byteCode.OpCode.NOP; +import static com.facebook.presto.byteCode.expression.ByteCodeExpressions.constantFalse; +import static com.facebook.presto.byteCode.expression.ByteCodeExpressions.constantTrue; +import static com.facebook.presto.byteCode.expression.ByteCodeExpressions.invokeDynamic; +import static com.facebook.presto.sql.gen.Bootstrap.BOOTSTRAP_METHOD; +import static java.lang.String.format; + +public final class ByteCodeUtils +{ + private ByteCodeUtils() + { + } + + public static ByteCodeNode ifWasNullPopAndGoto(Scope scope, LabelNode label, Class returnType, Class... stackArgsToPop) + { + return handleNullValue(scope, label, returnType, ImmutableList.copyOf(stackArgsToPop), false); + } + + public static ByteCodeNode ifWasNullPopAndGoto(Scope scope, LabelNode label, Class returnType, Iterable> stackArgsToPop) + { + return handleNullValue(scope, label, returnType, ImmutableList.copyOf(stackArgsToPop), false); + } + + public static ByteCodeNode ifWasNullClearPopAndGoto(Scope scope, LabelNode label, Class returnType, Class... stackArgsToPop) + { + return handleNullValue(scope, label, returnType, ImmutableList.copyOf(stackArgsToPop), true); + } + + public static ByteCodeNode handleNullValue(Scope scope, + LabelNode label, + Class returnType, + List> stackArgsToPop, + boolean clearNullFlag) + { + Variable wasNull = scope.getVariable("wasNull"); + + Block nullCheck = new Block() + .setDescription("ifWasNullGoto") + .append(wasNull); + + String clearComment = null; + if (clearNullFlag) { + nullCheck.append(wasNull.set(constantFalse())); + clearComment = "clear wasNull"; + } + + Block isNull = new Block(); + for (Class parameterType : stackArgsToPop) { + isNull.pop(parameterType); + } + + isNull.pushJavaDefault(returnType); + String loadDefaultComment = null; + if (returnType != void.class) { + loadDefaultComment = format("loadJavaDefault(%s)", returnType.getName()); + } + + isNull.gotoLabel(label); + + String popComment = null; + if (!stackArgsToPop.isEmpty()) { + popComment = format("pop(%s)", Joiner.on(", ").join(stackArgsToPop)); + } + + return new IfStatement("if wasNull then %s", Joiner.on(", ").skipNulls().join(clearComment, popComment, loadDefaultComment, "goto " + label.getLabel())) + .condition(nullCheck) + .ifTrue(isNull); + } + + public static ByteCodeNode boxPrimitive(Class type) + { + Block block = new Block().comment("box primitive"); + if (type == long.class) { + return block.invokeStatic(Long.class, "valueOf", Long.class, long.class); + } + if (type == double.class) { + return block.invokeStatic(Double.class, "valueOf", Double.class, double.class); + } + if (type == boolean.class) { + return block.invokeStatic(Boolean.class, "valueOf", Boolean.class, boolean.class); + } + if (type.isPrimitive()) { + throw new UnsupportedOperationException("not yet implemented: " + type); + } + + return NOP; + } + + public static ByteCodeNode unboxPrimitive(Class unboxedType) + { + Block block = new Block().comment("unbox primitive"); + if (unboxedType == long.class) { + return block.invokeVirtual(Long.class, "longValue", long.class); + } + if (unboxedType == double.class) { + return block.invokeVirtual(Double.class, "doubleValue", double.class); + } + if (unboxedType == boolean.class) { + return block.invokeVirtual(Boolean.class, "booleanValue", boolean.class); + } + throw new UnsupportedOperationException("not yet implemented: " + unboxedType); + } + + public static ByteCodeExpression loadConstant(CallSiteBinder callSiteBinder, Object constant, Class type) + { + Binding binding = callSiteBinder.bind(MethodHandles.constant(type, constant)); + return loadConstant(binding); + } + + public static ByteCodeExpression loadConstant(Binding binding) + { + return invokeDynamic( + BOOTSTRAP_METHOD, + ImmutableList.of(binding.getBindingId()), + "constant_" + binding.getBindingId(), + binding.getType().returnType()); + } + + public static ByteCodeNode generateInvocation(Scope scope, FunctionInfo function, List arguments, Binding binding) + { + MethodType methodType = binding.getType(); + + Signature signature = function.getSignature(); + Class returnType = methodType.returnType(); + Class unboxedReturnType = Primitives.unwrap(returnType); + + LabelNode end = new LabelNode("end"); + Block block = new Block() + .setDescription("invoke " + signature); + + List> stackTypes = new ArrayList<>(); + + int index = 0; + for (Class type : methodType.parameterArray()) { + stackTypes.add(type); + if (type == ConnectorSession.class) { + block.append(scope.getVariable("session")); + } + else { + block.append(arguments.get(index)); + if (!function.getNullableArguments().get(index)) { + block.append(ifWasNullPopAndGoto(scope, end, unboxedReturnType, Lists.reverse(stackTypes))); + } + else { + block.append(boxPrimitiveIfNecessary(scope, type)); + block.append(scope.getVariable("wasNull").set(constantFalse())); + } + index++; + } + } + block.append(invoke(binding, function.getSignature())); + + if (function.isNullable()) { + block.append(unboxPrimitiveIfNecessary(scope, returnType)); + } + block.visitLabel(end); + + return block; + } + + public static Block unboxPrimitiveIfNecessary(Scope scope, Class boxedType) + { + Block block = new Block(); + LabelNode end = new LabelNode("end"); + Class unboxedType = Primitives.unwrap(boxedType); + Variable wasNull = scope.getVariable("wasNull"); + + if (unboxedType.isPrimitive() && unboxedType != void.class) { + LabelNode notNull = new LabelNode("notNull"); + block.dup(boxedType) + .ifNotNullGoto(notNull) + .append(wasNull.set(constantTrue())) + .comment("swap boxed null with unboxed default") + .pop(boxedType) + .pushJavaDefault(unboxedType) + .gotoLabel(end) + .visitLabel(notNull) + .append(unboxPrimitive(unboxedType)); + } + else { + block.dup(boxedType) + .ifNotNullGoto(end) + .append(wasNull.set(constantTrue())); + } + block.visitLabel(end); + + return block; + } + + public static ByteCodeNode boxPrimitiveIfNecessary(Scope scope, Class type) + { + if (!Primitives.isWrapperType(type)) { + return NOP; + } + Block notNull = new Block().comment("box primitive"); + Class expectedCurrentStackType; + if (type == Long.class) { + notNull.invokeStatic(Long.class, "valueOf", Long.class, long.class); + expectedCurrentStackType = long.class; + } + else if (type == Double.class) { + notNull.invokeStatic(Double.class, "valueOf", Double.class, double.class); + expectedCurrentStackType = double.class; + } + else if (type == Boolean.class) { + notNull.invokeStatic(Boolean.class, "valueOf", Boolean.class, boolean.class); + expectedCurrentStackType = boolean.class; + } + else if (type == Void.class) { + notNull.pushNull() + .checkCast(Void.class); + expectedCurrentStackType = void.class; + } + else { + throw new UnsupportedOperationException("not yet implemented: " + type); + } + + Block condition = new Block().append(scope.getVariable("wasNull")); + + Block wasNull = new Block() + .pop(expectedCurrentStackType) + .pushNull() + .checkCast(type); + + return new IfStatement() + .condition(condition) + .ifTrue(wasNull) + .ifFalse(notNull); + } + + public static ByteCodeNode invoke(Binding binding, String name) + { + return invokeDynamic(BOOTSTRAP_METHOD, ImmutableList.of(binding.getBindingId()), name, binding.getType()); + } + + public static ByteCodeNode invoke(Binding binding, Signature signature) + { + return invoke(binding, signature.getName()); + } + + public static ByteCodeNode generateWrite(CallSiteBinder callSiteBinder, Scope scope, Variable wasNullVariable, Type type) + { + if (type.getJavaType() == void.class) { + return new Block().comment("output.appendNull();") + .invokeInterface(BlockBuilder.class, "appendNull", BlockBuilder.class) + .pop(); + } + String methodName = "write" + Primitives.wrap(type.getJavaType()).getSimpleName(); + + // the stack contains [output, value] + + // We should be able to insert the code to get the output variable and compute the value + // at the right place instead of assuming they are in the stack. We should also not need to + // use temp variables to re-shuffle the stack to the right shape before Type.writeXXX is called + // Unfortunately, because of the assumptions made by try_cast, we can't get around it yet. + // TODO: clean up once try_cast is fixed + Variable tempValue = scope.createTempVariable(type.getJavaType()); + Variable tempOutput = scope.createTempVariable(BlockBuilder.class); + return new Block() + .comment("if (wasNull)") + .append(new IfStatement() + .condition(wasNullVariable) + .ifTrue(new Block() + .comment("output.appendNull();") + .pop(type.getJavaType()) + .invokeInterface(BlockBuilder.class, "appendNull", BlockBuilder.class) + .pop()) + .ifFalse(new Block() + .comment("%s.%s(output, %s)", type.getTypeSignature(), methodName, type.getJavaType().getSimpleName()) + .putVariable(tempValue) + .putVariable(tempOutput) + .append(loadConstant(callSiteBinder.bind(type, Type.class))) + .getVariable(tempOutput) + .getVariable(tempValue) + .invokeInterface(Type.class, methodName, void.class, BlockBuilder.class, type.getJavaType()))); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/gen/CallSiteBinder.java b/presto-main/src/main/java/com/facebook/presto/sql/gen/CallSiteBinder.java new file mode 100644 index 00000000..b53218fe --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/gen/CallSiteBinder.java @@ -0,0 +1,58 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.gen; + +import com.google.common.collect.ImmutableMap; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.util.HashMap; +import java.util.Map; + +import static com.google.common.base.MoreObjects.toStringHelper; + +public final class CallSiteBinder +{ + private int nextId; + + private final Map bindings = new HashMap<>(); + + public Binding bind(MethodHandle method) + { + long bindingId = nextId++; + Binding binding = new Binding(bindingId, method.type()); + + bindings.put(bindingId, method); + return binding; + } + + public Binding bind(Object constant, Class type) + { + return bind(MethodHandles.constant(type, constant)); + } + + public Map getBindings() + { + return ImmutableMap.copyOf(bindings); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("nextId", nextId) + .add("bindings", bindings) + .toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/gen/CastCodeGenerator.java b/presto-main/src/main/java/com/facebook/presto/sql/gen/CastCodeGenerator.java new file mode 100644 index 00000000..6c2a5343 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/gen/CastCodeGenerator.java @@ -0,0 +1,49 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.gen; + +import com.facebook.presto.byteCode.Block; +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.relational.RowExpression; +import com.facebook.presto.type.UnknownType; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.facebook.presto.byteCode.expression.ByteCodeExpressions.constantTrue; + +public class CastCodeGenerator + implements ByteCodeGenerator +{ + @Override + public ByteCodeNode generateExpression(Signature signature, ByteCodeGeneratorContext generatorContext, Type returnType, List arguments) + { + RowExpression argument = arguments.get(0); + + if (argument.getType().equals(UnknownType.UNKNOWN)) { + return new Block() + .append(generatorContext.wasNull().set(constantTrue())) + .pushJavaDefault(returnType.getJavaType()); + } + + FunctionInfo function = generatorContext + .getRegistry() + .getCoercion(argument.getType(), returnType); + + return generatorContext.generateCall(function, ImmutableList.of(generatorContext.generate(argument))); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/gen/CoalesceCodeGenerator.java b/presto-main/src/main/java/com/facebook/presto/sql/gen/CoalesceCodeGenerator.java new file mode 100644 index 00000000..7734baea --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/gen/CoalesceCodeGenerator.java @@ -0,0 +1,66 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.gen; + +import com.facebook.presto.byteCode.Block; +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.Variable; +import com.facebook.presto.byteCode.control.IfStatement; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.relational.RowExpression; +import com.google.common.collect.Lists; + +import java.util.ArrayList; +import java.util.List; + +import static com.facebook.presto.byteCode.expression.ByteCodeExpressions.constantFalse; +import static com.facebook.presto.byteCode.expression.ByteCodeExpressions.constantTrue; + +public class CoalesceCodeGenerator + implements ByteCodeGenerator +{ + @Override + public ByteCodeNode generateExpression(Signature signature, ByteCodeGeneratorContext generatorContext, Type returnType, List arguments) + { + List operands = new ArrayList<>(); + for (RowExpression expression : arguments) { + operands.add(generatorContext.generate(expression)); + } + + Variable wasNull = generatorContext.wasNull(); + ByteCodeNode nullValue = new Block() + .append(wasNull.set(constantTrue())) + .pushJavaDefault(returnType.getJavaType()); + + // reverse list because current if statement builder doesn't support if/else so we need to build the if statements bottom up + for (ByteCodeNode operand : Lists.reverse(operands)) { + IfStatement ifStatement = new IfStatement(); + + ifStatement.condition() + .append(operand) + .append(wasNull); + + // if value was null, pop the null value, clear the null flag, and process the next operand + ifStatement.ifTrue() + .pop(returnType.getJavaType()) + .append(wasNull.set(constantFalse())) + .append(nullValue); + + nullValue = ifStatement; + } + + return nullValue; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/gen/CompilerOperations.java b/presto-main/src/main/java/com/facebook/presto/sql/gen/CompilerOperations.java new file mode 100644 index 00000000..0f91ebca --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/gen/CompilerOperations.java @@ -0,0 +1,74 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.gen; + +import com.facebook.presto.spi.block.Block; + +import javax.annotation.Nullable; + +import java.util.Set; + +import static com.facebook.presto.spi.type.BooleanType.BOOLEAN; + +// This methods are statically bound by the compiler +@SuppressWarnings("UnusedDeclaration") +public final class CompilerOperations +{ + private CompilerOperations() + { + } + + public static boolean longGreaterThanZero(long value) + { + return value > 0; + } + + public static boolean and(boolean left, boolean right) + { + return left && right; + } + + public static boolean or(boolean left, boolean right) + { + return left || right; + } + + public static boolean not(boolean value) + { + return !value; + } + + public static boolean lessThan(int left, int right) + { + return left < right; + } + + public static boolean greaterThan(int left, int right) + { + return left > right; + } + + public static boolean in(Object value, Set set) + { + return set.contains(value); + } + + public static boolean testMask(@Nullable Block masks, int index) + { + if (masks != null) { + return BOOLEAN.getBoolean(masks, index); + } + return true; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/gen/CompilerUtils.java b/presto-main/src/main/java/com/facebook/presto/sql/gen/CompilerUtils.java new file mode 100644 index 00000000..60954a2d --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/gen/CompilerUtils.java @@ -0,0 +1,140 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.gen; + +import com.facebook.presto.byteCode.ClassDefinition; +import com.facebook.presto.byteCode.ClassInfoLoader; +import com.facebook.presto.byteCode.DumpByteCodeVisitor; +import com.facebook.presto.byteCode.DynamicClassLoader; +import com.facebook.presto.byteCode.ParameterizedType; +import com.facebook.presto.byteCode.SmartClassWriter; +import com.google.common.collect.ImmutableList; +import com.google.common.io.Files; +import com.google.common.reflect.Reflection; +import io.airlift.log.Logger; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.util.CheckClassAdapter; +import org.objectweb.asm.util.TraceClassVisitor; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.lang.invoke.MethodHandle; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +public final class CompilerUtils +{ + private static final Logger log = Logger.get(CompilerUtils.class); + + private static final boolean DUMP_BYTE_CODE_TREE = false; + private static final boolean DUMP_BYTE_CODE_RAW = false; + private static final boolean RUN_ASM_VERIFIER = false; // verifier doesn't work right now + private static final AtomicReference DUMP_CLASS_FILES_TO = new AtomicReference<>(); + private static final AtomicLong CLASS_ID = new AtomicLong(); + + private CompilerUtils() + { + } + + public static ParameterizedType makeClassName(String baseName) + { + String className = "com.facebook.presto.$gen." + baseName + "_" + CLASS_ID.incrementAndGet(); + String javaClassName = toJavaIdentifierString(className); + return ParameterizedType.typeFromJavaClassName(javaClassName); + } + + public static String toJavaIdentifierString(String className) + { + // replace invalid characters with '_' + int[] codePoints = className.codePoints().map(c -> Character.isJavaIdentifierPart(c) ? c : '_').toArray(); + return new String(codePoints, 0, codePoints.length); + } + + public static Class defineClass(ClassDefinition classDefinition, Class superType, DynamicClassLoader classLoader) + { + Class clazz = defineClasses(ImmutableList.of(classDefinition), classLoader).values().iterator().next(); + return clazz.asSubclass(superType); + } + + public static Class defineClass(ClassDefinition classDefinition, Class superType, Map callSiteBindings, ClassLoader parentClassLoader) + { + Class clazz = defineClass(classDefinition, superType, new DynamicClassLoader(parentClassLoader, callSiteBindings)); + return clazz.asSubclass(superType); + } + + private static Map> defineClasses(List classDefinitions, DynamicClassLoader classLoader) + { + ClassInfoLoader classInfoLoader = ClassInfoLoader.createClassInfoLoader(classDefinitions, classLoader); + + if (DUMP_BYTE_CODE_TREE) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + DumpByteCodeVisitor dumpByteCode = new DumpByteCodeVisitor(new PrintStream(out)); + for (ClassDefinition classDefinition : classDefinitions) { + dumpByteCode.visitClass(classDefinition); + } + System.out.println(new String(out.toByteArray(), StandardCharsets.UTF_8)); + } + + Map byteCodes = new LinkedHashMap<>(); + for (ClassDefinition classDefinition : classDefinitions) { + ClassWriter cw = new SmartClassWriter(classInfoLoader); + classDefinition.visit(cw); + byte[] byteCode = cw.toByteArray(); + if (RUN_ASM_VERIFIER) { + ClassReader reader = new ClassReader(byteCode); + CheckClassAdapter.verify(reader, classLoader, true, new PrintWriter(System.out)); + } + byteCodes.put(classDefinition.getType().getJavaClassName(), byteCode); + } + + String dumpClassPath = DUMP_CLASS_FILES_TO.get(); + if (dumpClassPath != null) { + for (Map.Entry entry : byteCodes.entrySet()) { + File file = new File(dumpClassPath, ParameterizedType.typeFromJavaClassName(entry.getKey()).getClassName() + ".class"); + try { + log.debug("ClassFile: " + file.getAbsolutePath()); + Files.createParentDirs(file); + Files.write(entry.getValue(), file); + } + catch (IOException e) { + log.error(e, "Failed to write generated class file to: %s" + file.getAbsolutePath()); + } + } + } + if (DUMP_BYTE_CODE_RAW) { + for (byte[] byteCode : byteCodes.values()) { + ClassReader classReader = new ClassReader(byteCode); + classReader.accept(new TraceClassVisitor(new PrintWriter(System.err)), ClassReader.SKIP_FRAMES); + } + } + Map> classes = classLoader.defineClasses(byteCodes); + try { + for (Class clazz : classes.values()) { + Reflection.initialize(clazz); + } + } + catch (VerifyError e) { + throw new RuntimeException(e); + } + return classes; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/gen/CursorProcessorCompiler.java b/presto-main/src/main/java/com/facebook/presto/sql/gen/CursorProcessorCompiler.java new file mode 100644 index 00000000..ba67d4de --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/gen/CursorProcessorCompiler.java @@ -0,0 +1,259 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.gen; + +import com.facebook.presto.byteCode.Block; +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.ClassDefinition; +import com.facebook.presto.byteCode.Scope; +import com.facebook.presto.byteCode.MethodDefinition; +import com.facebook.presto.byteCode.Parameter; +import com.facebook.presto.byteCode.Variable; +import com.facebook.presto.byteCode.control.ForLoop; +import com.facebook.presto.byteCode.control.IfStatement; +import com.facebook.presto.byteCode.instruction.LabelNode; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.operator.CursorProcessor; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.RecordCursor; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.relational.CallExpression; +import com.facebook.presto.sql.relational.ConstantExpression; +import com.facebook.presto.sql.relational.InputReferenceExpression; +import com.facebook.presto.sql.relational.RowExpression; +import com.facebook.presto.sql.relational.RowExpressionVisitor; +import com.google.common.primitives.Primitives; + +import java.util.List; + +import static com.facebook.presto.byteCode.Access.PUBLIC; +import static com.facebook.presto.byteCode.Access.a; +import static com.facebook.presto.byteCode.Parameter.arg; +import static com.facebook.presto.byteCode.OpCode.NOP; +import static com.facebook.presto.byteCode.ParameterizedType.type; +import static com.facebook.presto.sql.gen.ByteCodeUtils.generateWrite; +import static java.lang.String.format; + +public class CursorProcessorCompiler + implements BodyCompiler +{ + private final Metadata metadata; + + public CursorProcessorCompiler(Metadata metadata) + { + this.metadata = metadata; + } + + @Override + public void generateMethods(ClassDefinition classDefinition, CallSiteBinder callSiteBinder, RowExpression filter, List projections) + { + generateProcessMethod(classDefinition, projections.size()); + generateFilterMethod(classDefinition, callSiteBinder, filter); + + for (int i = 0; i < projections.size(); i++) { + generateProjectMethod(classDefinition, callSiteBinder, "project_" + i, projections.get(i)); + } + } + + private void generateProcessMethod(ClassDefinition classDefinition, int projections) + { + Parameter session = arg("session", ConnectorSession.class); + Parameter cursor = arg("cursor", RecordCursor.class); + Parameter count = arg("count", int.class); + Parameter pageBuilder = arg("pageBuilder", PageBuilder.class); + MethodDefinition method = classDefinition.declareMethod(a(PUBLIC), "process", type(int.class), session, cursor, count, pageBuilder); + + Scope scope = method.getScope(); + Variable completedPositionsVariable = scope.declareVariable(int.class, "completedPositions"); + + method.getBody() + .comment("int completedPositions = 0;") + .putVariable(completedPositionsVariable, 0); + + // + // for loop loop body + // + LabelNode done = new LabelNode("done"); + ForLoop forLoop = new ForLoop() + .initialize(NOP) + .condition(new Block() + .comment("completedPositions < count") + .getVariable(completedPositionsVariable) + .getVariable(count) + .invokeStatic(CompilerOperations.class, "lessThan", boolean.class, int.class, int.class) + ) + .update(new Block() + .comment("completedPositions++") + .incrementVariable(completedPositionsVariable, (byte) 1) + ); + + Block forLoopBody = new Block() + .comment("if (pageBuilder.isFull()) break;") + .append(new Block() + .getVariable(pageBuilder) + .invokeVirtual(PageBuilder.class, "isFull", boolean.class) + .ifTrueGoto(done)) + .comment("if (!cursor.advanceNextPosition()) break;") + .append(new Block() + .getVariable(cursor) + .invokeInterface(RecordCursor.class, "advanceNextPosition", boolean.class) + .ifFalseGoto(done)); + + forLoop.body(forLoopBody); + + // if (filter(cursor)) + IfStatement ifStatement = new IfStatement(); + ifStatement.condition() + .append(method.getThis()) + .getVariable(session) + .getVariable(cursor) + .invokeVirtual(classDefinition.getType(), "filter", type(boolean.class), type(ConnectorSession.class), type(RecordCursor.class)); + + // pageBuilder.declarePosition(); + ifStatement.ifTrue() + .getVariable(pageBuilder) + .invokeVirtual(PageBuilder.class, "declarePosition", void.class); + + // this.project_43(session, cursor, pageBuilder.getBlockBuilder(42))); + for (int projectionIndex = 0; projectionIndex < projections; projectionIndex++) { + ifStatement.ifTrue() + .append(method.getThis()) + .getVariable(session) + .getVariable(cursor); + + // pageBuilder.getBlockBuilder(0) + ifStatement.ifTrue() + .getVariable(pageBuilder) + .push(projectionIndex) + .invokeVirtual(PageBuilder.class, "getBlockBuilder", BlockBuilder.class, int.class); + + // project(block..., blockBuilder) + ifStatement.ifTrue() + .invokeVirtual(classDefinition.getType(), + "project_" + projectionIndex, + type(void.class), + type(ConnectorSession.class), + type(RecordCursor.class), + type(BlockBuilder.class)); + } + forLoopBody.append(ifStatement); + + method.getBody() + .append(forLoop) + .visitLabel(done) + .comment("return completedPositions;") + .getVariable(completedPositionsVariable) + .retInt(); + } + + private void generateFilterMethod(ClassDefinition classDefinition, CallSiteBinder callSiteBinder, RowExpression filter) + { + Parameter session = arg("session", ConnectorSession.class); + Parameter cursor = arg("cursor", RecordCursor.class); + MethodDefinition method = classDefinition.declareMethod(a(PUBLIC), "filter", type(boolean.class), session, cursor); + + method.comment("Filter: %s", filter); + + Scope scope = method.getScope(); + Variable wasNullVariable = scope.declareVariable(type(boolean.class), "wasNull"); + + ByteCodeExpressionVisitor visitor = new ByteCodeExpressionVisitor(callSiteBinder, fieldReferenceCompiler(cursor, wasNullVariable), metadata.getFunctionRegistry()); + + LabelNode end = new LabelNode("end"); + method.getBody() + .comment("boolean wasNull = false;") + .putVariable(wasNullVariable, false) + .comment("evaluate filter: " + filter) + .append(filter.accept(visitor, scope)) + .comment("if (wasNull) return false;") + .getVariable(wasNullVariable) + .ifFalseGoto(end) + .pop(boolean.class) + .push(false) + .visitLabel(end) + .retBoolean(); + } + + private void generateProjectMethod(ClassDefinition classDefinition, CallSiteBinder callSiteBinder, String methodName, RowExpression projection) + { + Parameter session = arg("session", ConnectorSession.class); + Parameter cursor = arg("cursor", RecordCursor.class); + Parameter output = arg("output", BlockBuilder.class); + MethodDefinition method = classDefinition.declareMethod(a(PUBLIC), methodName, type(void.class), session, cursor, output); + + method.comment("Projection: %s", projection.toString()); + + Scope scope = method.getScope(); + Variable wasNullVariable = scope.declareVariable(type(boolean.class), "wasNull"); + + Block body = method.getBody() + .comment("boolean wasNull = false;") + .putVariable(wasNullVariable, false); + + ByteCodeExpressionVisitor visitor = new ByteCodeExpressionVisitor(callSiteBinder, fieldReferenceCompiler(cursor, wasNullVariable), metadata.getFunctionRegistry()); + + body.getVariable(output) + .comment("evaluate projection: " + projection.toString()) + .append(projection.accept(visitor, scope)) + .append(generateWrite(callSiteBinder, scope, wasNullVariable, projection.getType())) + .ret(); + } + + private RowExpressionVisitor fieldReferenceCompiler(final Variable cursorVariable, final Variable wasNullVariable) + { + return new RowExpressionVisitor() + { + @Override + public ByteCodeNode visitInputReference(InputReferenceExpression node, Scope scope) + { + int field = node.getField(); + Type type = node.getType(); + + Class javaType = type.getJavaType(); + + IfStatement ifStatement = new IfStatement(); + ifStatement.condition() + .setDescription(format("cursor.get%s(%d)", type, field)) + .getVariable(cursorVariable) + .push(field) + .invokeInterface(RecordCursor.class, "isNull", boolean.class, int.class); + + ifStatement.ifTrue() + .putVariable(wasNullVariable, true) + .pushJavaDefault(javaType); + + ifStatement.ifFalse() + .getVariable(cursorVariable) + .push(field) + .invokeInterface(RecordCursor.class, "get" + Primitives.wrap(javaType).getSimpleName(), javaType, int.class); + + return ifStatement; + } + + @Override + public ByteCodeNode visitCall(CallExpression call, Scope scope) + { + throw new UnsupportedOperationException("not yet implemented"); + } + + @Override + public ByteCodeNode visitConstant(ConstantExpression literal, Scope scope) + { + throw new UnsupportedOperationException("not yet implemented"); + } + }; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/gen/ExpressionCompiler.java b/presto-main/src/main/java/com/facebook/presto/sql/gen/ExpressionCompiler.java new file mode 100644 index 00000000..e868003d --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/gen/ExpressionCompiler.java @@ -0,0 +1,196 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.gen; + +import com.facebook.presto.byteCode.ClassDefinition; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.operator.CursorProcessor; +import com.facebook.presto.operator.PageProcessor; +import com.facebook.presto.sql.relational.RowExpression; +import com.google.common.base.Throwables; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.ImmutableList; +import org.weakref.jmx.Managed; + +import javax.inject.Inject; + +import java.util.List; +import java.util.Objects; + +import static com.facebook.presto.byteCode.Access.FINAL; +import static com.facebook.presto.byteCode.Access.PUBLIC; +import static com.facebook.presto.byteCode.Access.a; +import static com.facebook.presto.byteCode.ParameterizedType.type; +import static com.facebook.presto.sql.gen.ByteCodeUtils.invoke; +import static com.facebook.presto.sql.gen.CompilerUtils.defineClass; +import static com.facebook.presto.sql.gen.CompilerUtils.makeClassName; +import static com.google.common.base.MoreObjects.toStringHelper; + +public class ExpressionCompiler +{ + private final Metadata metadata; + + private final LoadingCache pageProcessors = CacheBuilder.newBuilder().maximumSize(1000).build( + new CacheLoader() + { + @Override + public PageProcessor load(CacheKey key) + throws Exception + { + return compileAndInstantiate(key.getFilter(), key.getProjections(), new PageProcessorCompiler(metadata), PageProcessor.class); + } + }); + + private final LoadingCache cursorProcessors = CacheBuilder.newBuilder().maximumSize(1000).build( + new CacheLoader() + { + @Override + public CursorProcessor load(CacheKey key) + throws Exception + { + return compileAndInstantiate(key.getFilter(), key.getProjections(), new CursorProcessorCompiler(metadata), CursorProcessor.class); + } + }); + + @Inject + public ExpressionCompiler(Metadata metadata) + { + this.metadata = metadata; + } + + @Managed + public long getCacheSize() + { + return pageProcessors.size(); + } + + public CursorProcessor compileCursorProcessor(RowExpression filter, List projections, Object uniqueKey) + { + return cursorProcessors.getUnchecked(new CacheKey(filter, projections, uniqueKey)); + } + + public PageProcessor compilePageProcessor(RowExpression filter, List projections) + { + return pageProcessors.getUnchecked(new CacheKey(filter, projections, null)); + } + + private T compileAndInstantiate(RowExpression filter, List projections, BodyCompiler bodyCompiler, Class superType) + { + // create filter and project page iterator class + Class clazz = compileProcessor(filter, projections, bodyCompiler, superType); + try { + return clazz.newInstance(); + } + catch (ReflectiveOperationException e) { + throw Throwables.propagate(e); + } + } + + private Class compileProcessor( + RowExpression filter, + List projections, + BodyCompiler bodyCompiler, + Class superType) + { + ClassDefinition classDefinition = new ClassDefinition( + a(PUBLIC, FINAL), + makeClassName(superType.getSimpleName()), + type(Object.class), + type(superType)); + + classDefinition.declareDefaultConstructor(a(PUBLIC)); + + CallSiteBinder callSiteBinder = new CallSiteBinder(); + bodyCompiler.generateMethods(classDefinition, callSiteBinder, filter, projections); + + // + // toString method + // + generateToString( + classDefinition, + callSiteBinder, + toStringHelper(classDefinition.getType().getJavaClassName()) + .add("filter", filter) + .add("projections", projections) + .toString()); + + return defineClass(classDefinition, superType, callSiteBinder.getBindings(), getClass().getClassLoader()); + } + + private static void generateToString(ClassDefinition classDefinition, CallSiteBinder callSiteBinder, String string) + { + // bind constant via invokedynamic to avoid constant pool issues due to large strings + classDefinition.declareMethod(a(PUBLIC), "toString", type(String.class)) + .getBody() + .append(invoke(callSiteBinder.bind(string, String.class), "toString")) + .retObject(); + } + + private static final class CacheKey + { + private final RowExpression filter; + private final List projections; + private final Object uniqueKey; + + private CacheKey(RowExpression filter, List projections, Object uniqueKey) + { + this.filter = filter; + this.uniqueKey = uniqueKey; + this.projections = ImmutableList.copyOf(projections); + } + + private RowExpression getFilter() + { + return filter; + } + + private List getProjections() + { + return projections; + } + + @Override + public int hashCode() + { + return Objects.hash(filter, projections, uniqueKey); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + CacheKey other = (CacheKey) obj; + return Objects.equals(this.filter, other.filter) && + Objects.equals(this.projections, other.projections) && + Objects.equals(this.uniqueKey, other.uniqueKey); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("filter", filter) + .add("projections", projections) + .add("uniqueKey", uniqueKey) + .toString(); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/gen/FunctionCallCodeGenerator.java b/presto-main/src/main/java/com/facebook/presto/sql/gen/FunctionCallCodeGenerator.java new file mode 100644 index 00000000..6206795b --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/gen/FunctionCallCodeGenerator.java @@ -0,0 +1,51 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.gen; + +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.relational.RowExpression; +import com.facebook.presto.sql.tree.QualifiedName; +import com.google.common.base.Preconditions; + +import java.util.ArrayList; +import java.util.List; + +public class FunctionCallCodeGenerator + implements ByteCodeGenerator +{ + @Override + public ByteCodeNode generateExpression(Signature signature, ByteCodeGeneratorContext context, Type returnType, List arguments) + { + FunctionRegistry registry = context.getRegistry(); + + FunctionInfo function = registry.getExactFunction(signature); + if (function == null) { + // TODO: temporary hack to deal with magic timestamp literal functions which don't have an "exact" form and need to be "resolved" + function = registry.resolveFunction(QualifiedName.of(signature.getName()), signature.getArgumentTypes(), false); + } + + Preconditions.checkArgument(function != null, "Function %s not found", signature); + + List argumentsByteCode = new ArrayList<>(); + for (RowExpression argument : arguments) { + argumentsByteCode.add(context.generate(argument)); + } + + return context.generateCall(function, argumentsByteCode); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/gen/IfCodeGenerator.java b/presto-main/src/main/java/com/facebook/presto/sql/gen/IfCodeGenerator.java new file mode 100644 index 00000000..89b51395 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/gen/IfCodeGenerator.java @@ -0,0 +1,51 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.gen; + +import com.facebook.presto.byteCode.Block; +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.Variable; +import com.facebook.presto.byteCode.control.IfStatement; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.relational.RowExpression; +import com.google.common.base.Preconditions; + +import java.util.List; + +import static com.facebook.presto.byteCode.expression.ByteCodeExpressions.constantFalse; + +public class IfCodeGenerator + implements ByteCodeGenerator +{ + @Override + public ByteCodeNode generateExpression(Signature signature, ByteCodeGeneratorContext context, Type returnType, List arguments) + { + Preconditions.checkArgument(arguments.size() == 3); + + Variable wasNull = context.wasNull(); + Block condition = new Block() + .append(context.generate(arguments.get(0))) + .comment("... and condition value was not null") + .append(wasNull) + .invokeStatic(CompilerOperations.class, "not", boolean.class, boolean.class) + .invokeStatic(CompilerOperations.class, "and", boolean.class, boolean.class, boolean.class) + .append(wasNull.set(constantFalse())); + + return new IfStatement() + .condition(condition) + .ifTrue(context.generate(arguments.get(1))) + .ifFalse(context.generate(arguments.get(2))); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/gen/InCodeGenerator.java b/presto-main/src/main/java/com/facebook/presto/sql/gen/InCodeGenerator.java new file mode 100644 index 00000000..4475db7e --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/gen/InCodeGenerator.java @@ -0,0 +1,244 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.gen; + +import com.facebook.presto.byteCode.Block; +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.Scope; +import com.facebook.presto.byteCode.Variable; +import com.facebook.presto.byteCode.control.IfStatement; +import com.facebook.presto.byteCode.control.LookupSwitch; +import com.facebook.presto.byteCode.instruction.LabelNode; +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.OperatorType; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.relational.ConstantExpression; +import com.facebook.presto.sql.relational.RowExpression; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.ImmutableSet; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.facebook.presto.byteCode.control.LookupSwitch.lookupSwitchBuilder; +import static com.facebook.presto.byteCode.expression.ByteCodeExpressions.constantFalse; +import static com.facebook.presto.byteCode.instruction.JumpInstruction.jump; +import static com.facebook.presto.sql.gen.ByteCodeUtils.ifWasNullPopAndGoto; +import static com.facebook.presto.sql.gen.ByteCodeUtils.invoke; +import static com.facebook.presto.sql.gen.ByteCodeUtils.loadConstant; + +public class InCodeGenerator + implements ByteCodeGenerator +{ + @Override + public ByteCodeNode generateExpression(Signature signature, ByteCodeGeneratorContext generatorContext, Type returnType, List arguments) + { + ByteCodeNode value = generatorContext.generate(arguments.get(0)); + + List values = arguments.subList(1, arguments.size()); + + ImmutableList.Builder valuesByteCode = ImmutableList.builder(); + for (int i = 1; i < arguments.size(); i++) { + ByteCodeNode testNode = generatorContext.generate(arguments.get(i)); + valuesByteCode.add(testNode); + } + + Type type = arguments.get(0).getType(); + Class javaType = type.getJavaType(); + + FunctionInfo hashCodeFunction = generatorContext.getRegistry().resolveOperator(OperatorType.HASH_CODE, ImmutableList.of(type)); + + ImmutableListMultimap.Builder hashBucketsBuilder = ImmutableListMultimap.builder(); + ImmutableList.Builder defaultBucket = ImmutableList.builder(); + ImmutableSet.Builder constantValuesBuilder = ImmutableSet.builder(); + + for (RowExpression testValue : values) { + ByteCodeNode testByteCode = generatorContext.generate(testValue); + + if (testValue instanceof ConstantExpression && ((ConstantExpression) testValue).getValue() != null) { + ConstantExpression constant = (ConstantExpression) testValue; + Object object = constant.getValue(); + constantValuesBuilder.add(object); + + try { + int hashCode = ((Long) hashCodeFunction.getMethodHandle().invoke(object)).intValue(); + hashBucketsBuilder.put(hashCode, testByteCode); + } + catch (Throwable throwable) { + throw new IllegalArgumentException("Error processing IN statement: error calculating hash code for " + object, throwable); + } + } + else { + defaultBucket.add(testByteCode); + } + } + ImmutableListMultimap hashBuckets = hashBucketsBuilder.build(); + ImmutableSet constantValues = constantValuesBuilder.build(); + + LabelNode end = new LabelNode("end"); + LabelNode match = new LabelNode("match"); + LabelNode noMatch = new LabelNode("noMatch"); + + LabelNode defaultLabel = new LabelNode("default"); + + Scope scope = generatorContext.getScope(); + + ByteCodeNode switchBlock; + if (constantValues.size() < 1000) { + Block switchCaseBlocks = new Block(); + LookupSwitch.LookupSwitchBuilder switchBuilder = lookupSwitchBuilder(); + for (Map.Entry> bucket : hashBuckets.asMap().entrySet()) { + LabelNode label = new LabelNode("inHash" + bucket.getKey()); + switchBuilder.addCase(bucket.getKey(), label); + Collection testValues = bucket.getValue(); + + Block caseBlock = buildInCase(generatorContext, scope, type, label, match, defaultLabel, testValues, false); + switchCaseBlocks + .append(caseBlock.setDescription("case " + bucket.getKey())); + } + switchBuilder.defaultCase(defaultLabel); + + Binding hashCodeBinding = generatorContext + .getCallSiteBinder() + .bind(hashCodeFunction.getMethodHandle()); + + switchBlock = new Block() + .comment("lookupSwitch(hashCode())") + .dup(javaType) + .append(invoke(hashCodeBinding, hashCodeFunction.getSignature())) + .longToInt() + .append(switchBuilder.build()) + .append(switchCaseBlocks); + } + else { + // TODO: replace Set with fastutils (or similar) primitive sets if types are primitive + // for huge IN lists, use a Set + Binding constant = generatorContext.getCallSiteBinder().bind(constantValues, Set.class); + + switchBlock = new Block() + .comment("inListSet.contains()") + .append(new IfStatement() + .condition(new Block() + .comment("value (+boxing if necessary)") + .dup(javaType) + .append(ByteCodeUtils.boxPrimitive(javaType)) + .comment("set") + .append(loadConstant(constant)) + // TODO: use invokeVirtual on the set instead. This requires swapping the two elements in the stack + .invokeStatic(CompilerOperations.class, "in", boolean.class, Object.class, Set.class)) + .ifTrue(jump(match))); + } + + Block defaultCaseBlock = buildInCase(generatorContext, scope, type, defaultLabel, match, noMatch, defaultBucket.build(), true).setDescription("default"); + + Block block = new Block() + .comment("IN") + .append(value) + .append(ifWasNullPopAndGoto(scope, end, boolean.class, javaType)) + .append(switchBlock) + .append(defaultCaseBlock); + + Block matchBlock = new Block() + .setDescription("match") + .visitLabel(match) + .pop(javaType) + .append(generatorContext.wasNull().set(constantFalse())) + .push(true) + .gotoLabel(end); + block.append(matchBlock); + + Block noMatchBlock = new Block() + .setDescription("noMatch") + .visitLabel(noMatch) + .pop(javaType) + .push(false) + .gotoLabel(end); + block.append(noMatchBlock); + + block.visitLabel(end); + + return block; + } + + private Block buildInCase(ByteCodeGeneratorContext generatorContext, + Scope scope, + Type type, + LabelNode caseLabel, + LabelNode matchLabel, + LabelNode noMatchLabel, + Collection testValues, + boolean checkForNulls) + { + Variable caseWasNull = null; + if (checkForNulls) { + caseWasNull = scope.createTempVariable(boolean.class); + } + + Block caseBlock = new Block() + .visitLabel(caseLabel); + + if (checkForNulls) { + caseBlock.putVariable(caseWasNull, false); + } + + LabelNode elseLabel = new LabelNode("else"); + Block elseBlock = new Block() + .visitLabel(elseLabel); + + Variable wasNull = generatorContext.wasNull(); + if (checkForNulls) { + elseBlock.append(wasNull.set(caseWasNull)); + } + + elseBlock.gotoLabel(noMatchLabel); + + FunctionInfo operator = generatorContext.getRegistry().resolveOperator(OperatorType.EQUAL, ImmutableList.of(type, type)); + + Binding equalsFunction = generatorContext + .getCallSiteBinder() + .bind(operator.getMethodHandle()); + + ByteCodeNode elseNode = elseBlock; + for (ByteCodeNode testNode : testValues) { + LabelNode testLabel = new LabelNode("test"); + IfStatement test = new IfStatement(); + + test.condition() + .visitLabel(testLabel) + .dup(type.getJavaType()) + .append(testNode); + + if (checkForNulls) { + test.condition() + .append(wasNull) + .putVariable(caseWasNull) + .append(ifWasNullPopAndGoto(scope, elseLabel, void.class, type.getJavaType(), type.getJavaType())); + } + test.condition() + .append(invoke(equalsFunction, operator.getSignature())); + + test.ifTrue().gotoLabel(matchLabel); + test.ifFalse(elseNode); + + elseNode = test; + elseLabel = testLabel; + } + caseBlock.append(elseNode); + return caseBlock; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/gen/InvokeFunctionByteCodeExpression.java b/presto-main/src/main/java/com/facebook/presto/sql/gen/InvokeFunctionByteCodeExpression.java new file mode 100644 index 00000000..5e090dff --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/gen/InvokeFunctionByteCodeExpression.java @@ -0,0 +1,81 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.gen; + +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.MethodGenerationContext; +import com.facebook.presto.byteCode.Scope; +import com.facebook.presto.byteCode.expression.ByteCodeExpression; +import com.facebook.presto.metadata.FunctionInfo; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.facebook.presto.byteCode.ParameterizedType.type; +import static com.facebook.presto.sql.gen.ByteCodeUtils.generateInvocation; +import static com.facebook.presto.util.ImmutableCollectors.toImmutableList; +import static java.util.Objects.requireNonNull; + +public class InvokeFunctionByteCodeExpression + extends ByteCodeExpression +{ + public static ByteCodeExpression invokeFunction(Scope scope, CallSiteBinder callSiteBinder, FunctionInfo functionInfo, ByteCodeExpression... parameters) + { + return invokeFunction(scope, callSiteBinder, functionInfo, ImmutableList.copyOf(parameters)); + } + + public static ByteCodeExpression invokeFunction(Scope scope, CallSiteBinder callSiteBinder, FunctionInfo functionInfo, List parameters) + { + requireNonNull(scope, "scope is null"); + requireNonNull(callSiteBinder, "callSiteBinder is null"); + requireNonNull(functionInfo, "functionInfo is null"); + + Binding binding = callSiteBinder.bind(functionInfo.getMethodHandle()); + return new InvokeFunctionByteCodeExpression(scope, binding, functionInfo, parameters); + } + + private final ByteCodeNode invocation; + private final String oneLineDescription; + + private InvokeFunctionByteCodeExpression( + Scope scope, + Binding binding, + FunctionInfo functionInfo, + List parameters) + { + super(type(functionInfo.getMethodHandle().type().returnType())); + + this.invocation = generateInvocation(scope, functionInfo, parameters.stream().map(ByteCodeNode.class::cast).collect(toImmutableList()), binding); + this.oneLineDescription = functionInfo.getSignature().getName() + "(" + Joiner.on(", ").join(parameters) + ")"; + } + + @Override + public ByteCodeNode getByteCode(MethodGenerationContext generationContext) + { + return invocation; + } + + @Override + public List getChildNodes() + { + return ImmutableList.of(); + } + + @Override + protected String formatOneLine() + { + return oneLineDescription; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/gen/IsDistinctFromCodeGenerator.java b/presto-main/src/main/java/com/facebook/presto/sql/gen/IsDistinctFromCodeGenerator.java new file mode 100644 index 00000000..99fe39d6 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/gen/IsDistinctFromCodeGenerator.java @@ -0,0 +1,91 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.gen; + +import com.facebook.presto.byteCode.Block; +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.Variable; +import com.facebook.presto.byteCode.control.IfStatement; +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.OperatorType; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.relational.RowExpression; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.facebook.presto.byteCode.expression.ByteCodeExpressions.constantFalse; +import static com.facebook.presto.sql.gen.ByteCodeUtils.invoke; + +public class IsDistinctFromCodeGenerator + implements ByteCodeGenerator +{ + @Override + public ByteCodeNode generateExpression(Signature signature, ByteCodeGeneratorContext generatorContext, Type returnType, List arguments) + { + Preconditions.checkArgument(arguments.size() == 2); + + Variable wasNull = generatorContext.wasNull(); + + RowExpression left = arguments.get(0); + RowExpression right = arguments.get(1); + + Type leftType = left.getType(); + Type rightType = right.getType(); + + FunctionInfo operator = generatorContext + .getRegistry() + .resolveOperator(OperatorType.EQUAL, ImmutableList.of(leftType, rightType)); + + Binding binding = generatorContext + .getCallSiteBinder() + .bind(operator.getMethodHandle()); + + ByteCodeNode equalsCall = new Block() + .comment("equals(%s, %s)", leftType, rightType) + .append(invoke(binding, operator.getSignature())); + + Block block = new Block() + .comment("IS DISTINCT FROM") + .comment("left") + .append(generatorContext.generate(left)) + .append(new IfStatement() + .condition(wasNull) + .ifTrue(new Block() + .pop(leftType.getJavaType()) + .append(wasNull.set(constantFalse())) + .comment("right is not null") + .append(generatorContext.generate(right)) + .pop(rightType.getJavaType()) + .append(wasNull) + .invokeStatic(CompilerOperations.class, "not", boolean.class, boolean.class)) + .ifFalse(new Block() + .comment("right") + .append(generatorContext.generate(right)) + .append(new IfStatement() + .condition(wasNull) + .ifTrue(new Block() + .pop(leftType.getJavaType()) + .pop(rightType.getJavaType()) + .push(true)) + .ifFalse(new Block() + .append(equalsCall) + .invokeStatic(CompilerOperations.class, "not", boolean.class, boolean.class))))) + .append(wasNull.set(constantFalse())); + + return block; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/gen/IsNullCodeGenerator.java b/presto-main/src/main/java/com/facebook/presto/sql/gen/IsNullCodeGenerator.java new file mode 100644 index 00000000..b83edcd7 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/gen/IsNullCodeGenerator.java @@ -0,0 +1,58 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.gen; + +import com.facebook.presto.byteCode.Block; +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.Variable; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.relational.RowExpression; +import com.google.common.base.Preconditions; + +import java.util.List; + +import static com.facebook.presto.byteCode.expression.ByteCodeExpressions.constantFalse; +import static com.facebook.presto.byteCode.instruction.Constant.loadBoolean; +import static com.facebook.presto.type.UnknownType.UNKNOWN; + +public class IsNullCodeGenerator + implements ByteCodeGenerator +{ + @Override + public ByteCodeNode generateExpression(Signature signature, ByteCodeGeneratorContext generatorContext, Type returnType, List arguments) + { + Preconditions.checkArgument(arguments.size() == 1); + + RowExpression argument = arguments.get(0); + if (argument.getType().equals(UNKNOWN)) { + return loadBoolean(true); + } + + ByteCodeNode value = generatorContext.generate(argument); + + // evaluate the expression, pop the produced value, and load the null flag + Variable wasNull = generatorContext.wasNull(); + Block block = new Block() + .comment("is null") + .append(value) + .pop(argument.getType().getJavaType()) + .append(wasNull); + + // clear the null flag + block.append(wasNull.set(constantFalse())); + + return block; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/gen/IsolatedClass.java b/presto-main/src/main/java/com/facebook/presto/sql/gen/IsolatedClass.java new file mode 100644 index 00000000..eb5eeb1d --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/gen/IsolatedClass.java @@ -0,0 +1,70 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.gen; + +import com.facebook.presto.byteCode.DynamicClassLoader; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableMap; +import com.google.common.io.ByteStreams; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; + +public final class IsolatedClass +{ + private IsolatedClass() {} + + public static Class isolateClass( + DynamicClassLoader dynamicClassLoader, + Class publicBaseClass, + Class implementationClass, + Class... additionalClasses) + { + ImmutableMap.Builder builder = ImmutableMap.builder(); + builder.put(implementationClass.getName(), getByteCode(implementationClass)); + for (Class additionalClass : additionalClasses) { + builder.put(additionalClass.getName(), getByteCode(additionalClass)); + } + + // load classes into a private class loader + Map> isolatedClasses = dynamicClassLoader.defineClasses(builder.build()); + Class isolatedClass = isolatedClasses.get(implementationClass.getName()); + + // verify the isolated class + checkArgument(isolatedClass != null, "Could load class %s", implementationClass.getName()); + checkArgument(publicBaseClass.isAssignableFrom(isolatedClass), + "Error isolating class %s, newly loaded class is not a sub type of %s", + implementationClass.getName(), + publicBaseClass.getName()); + checkState(isolatedClass != implementationClass, "Isolation failed"); + + return isolatedClass.asSubclass(publicBaseClass); + } + + private static byte[] getByteCode(Class clazz) + { + InputStream stream = clazz.getClassLoader().getResourceAsStream(clazz.getName().replace('.', '/') + ".class"); + checkArgument(stream != null, "Could not obtain byte code for class %s", clazz.getName()); + try { + return ByteStreams.toByteArray(stream); + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/gen/JoinCompiler.java b/presto-main/src/main/java/com/facebook/presto/sql/gen/JoinCompiler.java new file mode 100644 index 00000000..059ca147 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/gen/JoinCompiler.java @@ -0,0 +1,612 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.gen; + +import com.facebook.presto.byteCode.Block; +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.ClassDefinition; +import com.facebook.presto.byteCode.Scope; +import com.facebook.presto.byteCode.DynamicClassLoader; +import com.facebook.presto.byteCode.FieldDefinition; +import com.facebook.presto.byteCode.MethodDefinition; +import com.facebook.presto.byteCode.OpCode; +import com.facebook.presto.byteCode.Parameter; +import com.facebook.presto.byteCode.Variable; +import com.facebook.presto.byteCode.control.IfStatement; +import com.facebook.presto.byteCode.expression.ByteCodeExpression; +import com.facebook.presto.byteCode.instruction.LabelNode; +import com.facebook.presto.operator.InMemoryJoinHash; +import com.facebook.presto.operator.LookupSource; +import com.facebook.presto.operator.PagesHashStrategy; +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.BigintType; +import com.facebook.presto.spi.type.Type; +import com.google.common.base.Throwables; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.ExecutionError; +import com.google.common.util.concurrent.UncheckedExecutionException; +import it.unimi.dsi.fastutil.longs.LongArrayList; + +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.ExecutionException; + +import static com.facebook.presto.byteCode.Access.FINAL; +import static com.facebook.presto.byteCode.Access.PRIVATE; +import static com.facebook.presto.byteCode.Access.PUBLIC; +import static com.facebook.presto.byteCode.Access.a; +import static com.facebook.presto.byteCode.Parameter.arg; +import static com.facebook.presto.byteCode.ParameterizedType.type; +import static com.facebook.presto.byteCode.expression.ByteCodeExpressions.constantInt; +import static com.facebook.presto.byteCode.expression.ByteCodeExpressions.constantNull; +import static com.facebook.presto.byteCode.expression.ByteCodeExpressions.notEqual; +import static com.facebook.presto.sql.gen.CompilerUtils.defineClass; +import static com.facebook.presto.sql.gen.CompilerUtils.makeClassName; +import static com.facebook.presto.sql.gen.SqlTypeByteCodeExpression.constantType; +import static com.google.common.base.Preconditions.checkNotNull; + +public class JoinCompiler +{ + private final LoadingCache lookupSourceFactories = CacheBuilder.newBuilder().maximumSize(1000).build( + new CacheLoader() + { + @Override + public LookupSourceFactory load(CacheKey key) + throws Exception + { + return internalCompileLookupSourceFactory(key.getTypes(), key.getJoinChannels()); + } + }); + + private final LoadingCache> hashStrategies = CacheBuilder.newBuilder().maximumSize(1000).build( + new CacheLoader>() { + @Override + public Class load(CacheKey key) + throws Exception + { + return internalCompileHashStrategy(key.getTypes(), key.getJoinChannels()); + } + }); + + public LookupSourceFactory compileLookupSourceFactory(List types, List joinChannels) + { + try { + return lookupSourceFactories.get(new CacheKey(types, joinChannels)); + } + catch (ExecutionException | UncheckedExecutionException | ExecutionError e) { + throw Throwables.propagate(e.getCause()); + } + } + + public PagesHashStrategyFactory compilePagesHashStrategyFactory(List types, List joinChannels) + { + checkNotNull(types, "types is null"); + checkNotNull(joinChannels, "joinChannels is null"); + + try { + return new PagesHashStrategyFactory(hashStrategies.get(new CacheKey(types, joinChannels))); + } + catch (ExecutionException | UncheckedExecutionException | ExecutionError e) { + throw Throwables.propagate(e.getCause()); + } + } + + private LookupSourceFactory internalCompileLookupSourceFactory(List types, List joinChannels) + { + Class pagesHashStrategyClass = internalCompileHashStrategy(types, joinChannels); + + Class lookupSourceClass = IsolatedClass.isolateClass( + new DynamicClassLoader(getClass().getClassLoader()), + LookupSource.class, + InMemoryJoinHash.class, + InMemoryJoinHash.UnvisitedJoinPositionIterator.class); + + return new LookupSourceFactory(lookupSourceClass, new PagesHashStrategyFactory(pagesHashStrategyClass)); + } + + private Class internalCompileHashStrategy(List types, List joinChannels) + { + CallSiteBinder callSiteBinder = new CallSiteBinder(); + + ClassDefinition classDefinition = new ClassDefinition( + a(PUBLIC, FINAL), + makeClassName("PagesHashStrategy"), + type(Object.class), + type(PagesHashStrategy.class)); + + List channelFields = new ArrayList<>(); + for (int i = 0; i < types.size(); i++) { + FieldDefinition channelField = classDefinition.declareField(a(PRIVATE, FINAL), "channel_" + i, type(List.class, com.facebook.presto.spi.block.Block.class)); + channelFields.add(channelField); + } + List joinChannelTypes = new ArrayList<>(); + List joinChannelFields = new ArrayList<>(); + for (int i = 0; i < joinChannels.size(); i++) { + joinChannelTypes.add(types.get(joinChannels.get(i))); + FieldDefinition channelField = classDefinition.declareField(a(PRIVATE, FINAL), "joinChannel_" + i, type(List.class, com.facebook.presto.spi.block.Block.class)); + joinChannelFields.add(channelField); + } + FieldDefinition hashChannelField = classDefinition.declareField(a(PRIVATE, FINAL), "hashChannel", type(List.class, com.facebook.presto.spi.block.Block.class)); + + generateConstructor(classDefinition, joinChannels, channelFields, joinChannelFields, hashChannelField); + generateGetChannelCountMethod(classDefinition, channelFields); + generateAppendToMethod(classDefinition, callSiteBinder, types, channelFields); + generateHashPositionMethod(classDefinition, callSiteBinder, joinChannelTypes, joinChannelFields, hashChannelField); + generateHashRowMethod(classDefinition, callSiteBinder, joinChannelTypes); + generateRowEqualsRowMethod(classDefinition, callSiteBinder, joinChannelTypes); + generatePositionEqualsRowMethod(classDefinition, callSiteBinder, joinChannelTypes, joinChannelFields); + generatePositionEqualsPositionMethod(classDefinition, callSiteBinder, joinChannelTypes, joinChannelFields); + + return defineClass(classDefinition, PagesHashStrategy.class, callSiteBinder.getBindings(), getClass().getClassLoader()); + } + + private void generateConstructor(ClassDefinition classDefinition, + List joinChannels, + List channelFields, + List joinChannelFields, + FieldDefinition hashChannelField) + { + Parameter channels = arg("channels", type(List.class, type(List.class, com.facebook.presto.spi.block.Block.class))); + Parameter hashChannel = arg("hashChannel", type(Optional.class, Integer.class)); + MethodDefinition constructorDefinition = classDefinition.declareConstructor(a(PUBLIC), channels, hashChannel); + + Variable thisVariable = constructorDefinition.getThis(); + + Block constructor = constructorDefinition + .getBody() + .comment("super();") + .append(thisVariable) + .invokeConstructor(Object.class); + + constructor.comment("Set channel fields"); + + for (int index = 0; index < channelFields.size(); index++) { + ByteCodeExpression channel = channels.invoke("get", Object.class, constantInt(index)) + .cast(type(List.class, com.facebook.presto.spi.block.Block.class)); + + constructor.append(thisVariable.setField(channelFields.get(index), channel)); + } + + constructor.comment("Set join channel fields"); + for (int index = 0; index < joinChannelFields.size(); index++) { + ByteCodeExpression joinChannel = channels.invoke("get", Object.class, constantInt(joinChannels.get(index))) + .cast(type(List.class, com.facebook.presto.spi.block.Block.class)); + + constructor.append(thisVariable.setField(joinChannelFields.get(index), joinChannel)); + } + + constructor.comment("Set hashChannel"); + constructor.append(new IfStatement() + .condition(hashChannel.invoke("isPresent", boolean.class)) + .ifTrue(thisVariable.setField( + hashChannelField, + channels.invoke("get", Object.class, hashChannel.invoke("get", Object.class).cast(Integer.class).cast(int.class)))) + .ifFalse(thisVariable.setField( + hashChannelField, + constantNull(hashChannelField.getType())))); + constructor.ret(); + } + + private void generateGetChannelCountMethod(ClassDefinition classDefinition, List channelFields) + { + classDefinition.declareMethod( + a(PUBLIC), + "getChannelCount", + type(int.class)) + .getBody() + .push(channelFields.size()) + .retInt(); + } + + private void generateAppendToMethod(ClassDefinition classDefinition, CallSiteBinder callSiteBinder, List types, List channelFields) + { + Parameter blockIndex = arg("blockIndex", int.class); + Parameter blockPosition = arg("blockPosition", int.class); + Parameter pageBuilder = arg("pageBuilder", PageBuilder.class); + Parameter outputChannelOffset = arg("outputChannelOffset", int.class); + MethodDefinition method = classDefinition.declareMethod(a(PUBLIC), "appendTo", type(void.class), blockIndex, blockPosition, pageBuilder, outputChannelOffset); + + Variable thisVariable = method.getThis(); + Block appendToBody = method.getBody(); + + for (int index = 0; index < channelFields.size(); index++) { + Type type = types.get(index); + ByteCodeExpression typeExpression = constantType(callSiteBinder, type); + + ByteCodeExpression block = thisVariable + .getField(channelFields.get(index)) + .invoke("get", Object.class, blockIndex) + .cast(com.facebook.presto.spi.block.Block.class); + + appendToBody + .comment("%s.appendTo(channel_%s.get(blockIndex), blockPosition, pageBuilder.getBlockBuilder(outputChannelOffset + %s));", type.getClass(), index, index) + .append(typeExpression) + .append(block) + .append(blockPosition) + .append(pageBuilder) + .append(outputChannelOffset) + .push(index) + .append(OpCode.IADD) + .invokeVirtual(PageBuilder.class, "getBlockBuilder", BlockBuilder.class, int.class) + .invokeInterface(Type.class, "appendTo", void.class, com.facebook.presto.spi.block.Block.class, int.class, BlockBuilder.class); + } + appendToBody.ret(); + } + + private void generateHashPositionMethod(ClassDefinition classDefinition, CallSiteBinder callSiteBinder, List joinChannelTypes, List joinChannelFields, FieldDefinition hashChannelField) + { + Parameter blockIndex = arg("blockIndex", int.class); + Parameter blockPosition = arg("blockPosition", int.class); + MethodDefinition hashPositionMethod = classDefinition.declareMethod( + a(PUBLIC), + "hashPosition", + type(int.class), + blockIndex, + blockPosition); + + Variable thisVariable = hashPositionMethod.getThis(); + ByteCodeExpression hashChannel = thisVariable.getField(hashChannelField); + ByteCodeExpression bigintType = constantType(callSiteBinder, BigintType.BIGINT); + + IfStatement ifStatement = new IfStatement(); + ifStatement.condition(notEqual(hashChannel, constantNull(hashChannelField.getType()))); + ifStatement.ifTrue( + bigintType.invoke( + "getLong", + long.class, + hashChannel.invoke("get", Object.class, blockIndex).cast(com.facebook.presto.spi.block.Block.class), + blockPosition) + .cast(int.class) + .ret() + ); + + hashPositionMethod + .getBody() + .append(ifStatement); + + Variable resultVariable = hashPositionMethod.getScope().declareVariable(int.class, "result"); + hashPositionMethod.getBody().push(0).putVariable(resultVariable); + + for (int index = 0; index < joinChannelTypes.size(); index++) { + ByteCodeExpression type = constantType(callSiteBinder, joinChannelTypes.get(index)); + + ByteCodeExpression block = hashPositionMethod + .getThis() + .getField(joinChannelFields.get(index)) + .invoke("get", Object.class, blockIndex) + .cast(com.facebook.presto.spi.block.Block.class); + + hashPositionMethod + .getBody() + .getVariable(resultVariable) + .push(31) + .append(OpCode.IMUL) + .append(typeHashCode(type, block, blockPosition)) + .append(OpCode.IADD) + .putVariable(resultVariable); + } + + hashPositionMethod + .getBody() + .getVariable(resultVariable) + .retInt(); + } + + private void generateHashRowMethod(ClassDefinition classDefinition, CallSiteBinder callSiteBinder, List joinChannelTypes) + { + Parameter position = arg("position", int.class); + Parameter blocks = arg("blocks", com.facebook.presto.spi.block.Block[].class); + MethodDefinition hashPositionMethod = classDefinition.declareMethod(a(PUBLIC), "hashRow", type(int.class), position, blocks); + + Variable resultVariable = hashPositionMethod.getScope().declareVariable(int.class, "result"); + hashPositionMethod.getBody().push(0).putVariable(resultVariable); + + for (int index = 0; index < joinChannelTypes.size(); index++) { + ByteCodeExpression type = constantType(callSiteBinder, joinChannelTypes.get(index)); + + // todo is the case needed + ByteCodeExpression block = blocks.getElement(index).cast(com.facebook.presto.spi.block.Block.class); + + hashPositionMethod + .getBody() + .getVariable(resultVariable) + .push(31) + .append(OpCode.IMUL) + .append(typeHashCode(type, block, position)) + .append(OpCode.IADD) + .putVariable(resultVariable); + } + + hashPositionMethod + .getBody() + .getVariable(resultVariable) + .retInt(); + } + + private static ByteCodeNode typeHashCode(ByteCodeExpression type, ByteCodeExpression blockRef, ByteCodeExpression blockPosition) + { + return new IfStatement() + .condition(blockRef.invoke("isNull", boolean.class, blockPosition)) + .ifTrue(constantInt(0)) + .ifFalse(type.invoke("hash", int.class, blockRef, blockPosition)); + } + + private void generateRowEqualsRowMethod( + ClassDefinition classDefinition, + CallSiteBinder callSiteBinder, + List joinChannelTypes) + { + MethodDefinition hashPositionMethod = classDefinition.declareMethod( + a(PUBLIC), + "rowEqualsRow", + type(boolean.class), + arg("leftPosition", int.class), + arg("leftBlocks", com.facebook.presto.spi.block.Block[].class), + arg("rightPosition", int.class), + arg("rightBlocks", com.facebook.presto.spi.block.Block[].class)); + + Scope compilerContext = hashPositionMethod.getScope(); + for (int index = 0; index < joinChannelTypes.size(); index++) { + ByteCodeExpression type = constantType(callSiteBinder, joinChannelTypes.get(index)); + + ByteCodeExpression leftBlock = compilerContext + .getVariable("leftBlocks") + .getElement(index); + + ByteCodeExpression rightBlock = compilerContext + .getVariable("rightBlocks") + .getElement(index); + + LabelNode checkNextField = new LabelNode("checkNextField"); + hashPositionMethod + .getBody() + .append(typeEquals( + type, + leftBlock, + compilerContext.getVariable("leftPosition"), + rightBlock, + compilerContext.getVariable("rightPosition"))) + .ifTrueGoto(checkNextField) + .push(false) + .retBoolean() + .visitLabel(checkNextField); + } + + hashPositionMethod + .getBody() + .push(true) + .retInt(); + } + + private void generatePositionEqualsRowMethod( + ClassDefinition classDefinition, + CallSiteBinder callSiteBinder, + List joinChannelTypes, + List joinChannelFields) + { + Parameter leftBlockIndex = arg("leftBlockIndex", int.class); + Parameter leftBlockPosition = arg("leftBlockPosition", int.class); + Parameter rightPosition = arg("rightPosition", int.class); + Parameter rightBlocks = arg("rightBlocks", com.facebook.presto.spi.block.Block[].class); + MethodDefinition hashPositionMethod = classDefinition.declareMethod( + a(PUBLIC), + "positionEqualsRow", + type(boolean.class), + leftBlockIndex, + leftBlockPosition, + rightPosition, + rightBlocks); + + Variable thisVariable = hashPositionMethod.getThis(); + + for (int index = 0; index < joinChannelTypes.size(); index++) { + ByteCodeExpression type = constantType(callSiteBinder, joinChannelTypes.get(index)); + + ByteCodeExpression leftBlock = thisVariable + .getField(joinChannelFields.get(index)) + .invoke("get", Object.class, leftBlockIndex) + .cast(com.facebook.presto.spi.block.Block.class); + + ByteCodeExpression rightBlock = rightBlocks.getElement(index); + + LabelNode checkNextField = new LabelNode("checkNextField"); + hashPositionMethod + .getBody() + .append(typeEquals(type, leftBlock, leftBlockPosition, rightBlock, rightPosition)) + .ifTrueGoto(checkNextField) + .push(false) + .retBoolean() + .visitLabel(checkNextField); + } + + hashPositionMethod + .getBody() + .push(true) + .retInt(); + } + + private void generatePositionEqualsPositionMethod( + ClassDefinition classDefinition, + CallSiteBinder callSiteBinder, + List joinChannelTypes, + List joinChannelFields) + { + Parameter leftBlockIndex = arg("leftBlockIndex", int.class); + Parameter leftBlockPosition = arg("leftBlockPosition", int.class); + Parameter rightBlockIndex = arg("rightBlockIndex", int.class); + Parameter rightBlockPosition = arg("rightBlockPosition", int.class); + MethodDefinition hashPositionMethod = classDefinition.declareMethod( + a(PUBLIC), + "positionEqualsPosition", + type(boolean.class), + leftBlockIndex, + leftBlockPosition, + rightBlockIndex, + rightBlockPosition); + + Variable thisVariable = hashPositionMethod.getThis(); + for (int index = 0; index < joinChannelTypes.size(); index++) { + ByteCodeExpression type = constantType(callSiteBinder, joinChannelTypes.get(index)); + + ByteCodeExpression leftBlock = thisVariable + .getField(joinChannelFields.get(index)) + .invoke("get", Object.class, leftBlockIndex) + .cast(com.facebook.presto.spi.block.Block.class); + + ByteCodeExpression rightBlock = thisVariable + .getField(joinChannelFields.get(index)) + .invoke("get", Object.class, rightBlockIndex) + .cast(com.facebook.presto.spi.block.Block.class); + + LabelNode checkNextField = new LabelNode("checkNextField"); + hashPositionMethod + .getBody() + .append(typeEquals(type, leftBlock, leftBlockPosition, rightBlock, rightBlockPosition)) + .ifTrueGoto(checkNextField) + .push(false) + .retBoolean() + .visitLabel(checkNextField); + } + + hashPositionMethod + .getBody() + .push(true) + .retInt(); + } + + private static ByteCodeNode typeEquals( + ByteCodeExpression type, + ByteCodeExpression leftBlock, + ByteCodeExpression leftBlockPosition, + ByteCodeExpression rightBlock, + ByteCodeExpression rightBlockPosition) + { + IfStatement ifStatement = new IfStatement(); + ifStatement.condition() + .append(leftBlock.invoke("isNull", boolean.class, leftBlockPosition)) + .append(rightBlock.invoke("isNull", boolean.class, rightBlockPosition)) + .append(OpCode.IOR); + + ifStatement.ifTrue() + .append(leftBlock.invoke("isNull", boolean.class, leftBlockPosition)) + .append(rightBlock.invoke("isNull", boolean.class, rightBlockPosition)) + .append(OpCode.IAND); + + ifStatement.ifFalse().append(type.invoke("equalTo", boolean.class, leftBlock, leftBlockPosition, rightBlock, rightBlockPosition)); + + return ifStatement; + } + + public static class LookupSourceFactory + { + private final Constructor constructor; + private final PagesHashStrategyFactory pagesHashStrategyFactory; + + public LookupSourceFactory(Class lookupSourceClass, PagesHashStrategyFactory pagesHashStrategyFactory) + { + this.pagesHashStrategyFactory = pagesHashStrategyFactory; + try { + constructor = lookupSourceClass.getConstructor(LongArrayList.class, List.class, PagesHashStrategy.class); + } + catch (NoSuchMethodException e) { + throw Throwables.propagate(e); + } + } + + public LookupSource createLookupSource(LongArrayList addresses, List types, List> channels, Optional hashChannel) + { + PagesHashStrategy pagesHashStrategy = pagesHashStrategyFactory.createPagesHashStrategy(channels, hashChannel); + try { + return constructor.newInstance(addresses, types, pagesHashStrategy); + } + catch (Exception e) { + throw Throwables.propagate(e); + } + } + } + + public static class PagesHashStrategyFactory + { + private final Constructor constructor; + + public PagesHashStrategyFactory(Class pagesHashStrategyClass) + { + try { + constructor = pagesHashStrategyClass.getConstructor(List.class, Optional.class); + } + catch (NoSuchMethodException e) { + throw Throwables.propagate(e); + } + } + + public PagesHashStrategy createPagesHashStrategy(List> channels, Optional hashChannel) + { + try { + return constructor.newInstance(channels, hashChannel); + } + catch (Exception e) { + throw Throwables.propagate(e); + } + } + } + + private static final class CacheKey + { + private final List types; + private final List joinChannels; + + private CacheKey(List types, List joinChannels) + { + this.types = ImmutableList.copyOf(checkNotNull(types, "types is null")); + this.joinChannels = ImmutableList.copyOf(checkNotNull(joinChannels, "joinChannels is null")); + } + + private List getTypes() + { + return types; + } + + private List getJoinChannels() + { + return joinChannels; + } + + @Override + public int hashCode() + { + return Objects.hash(types, joinChannels); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (!(obj instanceof CacheKey)) { + return false; + } + CacheKey other = (CacheKey) obj; + return Objects.equals(this.types, other.types) && + Objects.equals(this.joinChannels, other.joinChannels); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/gen/JoinProbeCompiler.java b/presto-main/src/main/java/com/facebook/presto/sql/gen/JoinProbeCompiler.java new file mode 100644 index 00000000..28b96743 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/gen/JoinProbeCompiler.java @@ -0,0 +1,517 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.gen; + +import com.facebook.presto.byteCode.Block; +import com.facebook.presto.byteCode.ClassDefinition; +import com.facebook.presto.byteCode.DynamicClassLoader; +import com.facebook.presto.byteCode.FieldDefinition; +import com.facebook.presto.byteCode.MethodDefinition; +import com.facebook.presto.byteCode.Parameter; +import com.facebook.presto.byteCode.Variable; +import com.facebook.presto.byteCode.control.IfStatement; +import com.facebook.presto.byteCode.expression.ByteCodeExpression; +import com.facebook.presto.byteCode.instruction.JumpInstruction; +import com.facebook.presto.byteCode.instruction.LabelNode; +import com.facebook.presto.operator.JoinProbe; +import com.facebook.presto.operator.JoinProbeFactory; +import com.facebook.presto.operator.LookupJoinOperator; +import com.facebook.presto.operator.LookupJoinOperatorFactory; +import com.facebook.presto.operator.LookupJoinOperators.JoinType; +import com.facebook.presto.operator.LookupSource; +import com.facebook.presto.operator.LookupSourceSupplier; +import com.facebook.presto.operator.OperatorFactory; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.BigintType; +import com.facebook.presto.spi.type.Type; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Throwables; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.ExecutionError; +import com.google.common.util.concurrent.UncheckedExecutionException; + +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.ExecutionException; + +import static com.facebook.presto.byteCode.Access.FINAL; +import static com.facebook.presto.byteCode.Access.PRIVATE; +import static com.facebook.presto.byteCode.Access.PUBLIC; +import static com.facebook.presto.byteCode.Access.a; +import static com.facebook.presto.byteCode.Parameter.arg; +import static com.facebook.presto.byteCode.ParameterizedType.type; +import static com.facebook.presto.byteCode.expression.ByteCodeExpressions.constantInt; +import static com.facebook.presto.byteCode.expression.ByteCodeExpressions.constantLong; +import static com.facebook.presto.byteCode.expression.ByteCodeExpressions.newInstance; +import static com.facebook.presto.sql.gen.CompilerUtils.defineClass; +import static com.facebook.presto.sql.gen.CompilerUtils.makeClassName; +import static com.facebook.presto.sql.gen.SqlTypeByteCodeExpression.constantType; + +public class JoinProbeCompiler +{ + private final LoadingCache joinProbeFactories = CacheBuilder.newBuilder().maximumSize(1000).build( + new CacheLoader() + { + @Override + public HashJoinOperatorFactoryFactory load(JoinOperatorCacheKey key) + throws Exception + { + return internalCompileJoinOperatorFactory(key.getTypes(), key.getProbeChannels(), key.getProbeHashChannel()); + } + }); + + public OperatorFactory compileJoinOperatorFactory(int operatorId, + LookupSourceSupplier lookupSourceSupplier, + List probeTypes, + List probeJoinChannel, + Optional probeHashChannel, + JoinType joinType) + { + try { + HashJoinOperatorFactoryFactory operatorFactoryFactory = joinProbeFactories.get(new JoinOperatorCacheKey(probeTypes, probeJoinChannel, probeHashChannel, joinType)); + return operatorFactoryFactory.createHashJoinOperatorFactory(operatorId, lookupSourceSupplier, probeTypes, probeJoinChannel, joinType); + } + catch (ExecutionException | UncheckedExecutionException | ExecutionError e) { + throw Throwables.propagate(e.getCause()); + } + } + + public HashJoinOperatorFactoryFactory internalCompileJoinOperatorFactory(List types, List probeJoinChannel, Optional probeHashChannel) + { + Class joinProbeClass = compileJoinProbe(types, probeJoinChannel, probeHashChannel); + + ClassDefinition classDefinition = new ClassDefinition( + a(PUBLIC, FINAL), + makeClassName("JoinProbeFactory"), + type(Object.class), + type(JoinProbeFactory.class)); + + classDefinition.declareDefaultConstructor(a(PUBLIC)); + + Parameter lookupSource = arg("lookupSource", LookupSource.class); + Parameter page = arg("page", Page.class); + MethodDefinition method = classDefinition.declareMethod(a(PUBLIC), "createJoinProbe", type(JoinProbe.class), lookupSource, page); + + method.getBody() + .newObject(joinProbeClass) + .dup() + .append(lookupSource) + .append(page) + .invokeConstructor(joinProbeClass, LookupSource.class, Page.class) + .retObject(); + + DynamicClassLoader classLoader = new DynamicClassLoader(joinProbeClass.getClassLoader()); + Class joinProbeFactoryClass = defineClass(classDefinition, JoinProbeFactory.class, classLoader); + JoinProbeFactory joinProbeFactory; + try { + joinProbeFactory = joinProbeFactoryClass.newInstance(); + } + catch (Exception e) { + throw Throwables.propagate(e); + } + + Class operatorFactoryClass = IsolatedClass.isolateClass( + classLoader, + OperatorFactory.class, + LookupJoinOperatorFactory.class, + LookupJoinOperator.class); + + return new HashJoinOperatorFactoryFactory(joinProbeFactory, operatorFactoryClass); + } + + @VisibleForTesting + public JoinProbeFactory internalCompileJoinProbe(List types, List probeChannels, Optional probeHashChannel) + { + return new ReflectionJoinProbeFactory(compileJoinProbe(types, probeChannels, probeHashChannel)); + } + + private Class compileJoinProbe(List types, List probeChannels, Optional probeHashChannel) + { + CallSiteBinder callSiteBinder = new CallSiteBinder(); + + ClassDefinition classDefinition = new ClassDefinition( + a(PUBLIC, FINAL), + makeClassName("JoinProbe"), + type(Object.class), + type(JoinProbe.class)); + + // declare fields + FieldDefinition lookupSourceField = classDefinition.declareField(a(PRIVATE, FINAL), "lookupSource", LookupSource.class); + FieldDefinition positionCountField = classDefinition.declareField(a(PRIVATE, FINAL), "positionCount", int.class); + List blockFields = new ArrayList<>(); + for (int i = 0; i < types.size(); i++) { + FieldDefinition channelField = classDefinition.declareField(a(PRIVATE, FINAL), "block_" + i, com.facebook.presto.spi.block.Block.class); + blockFields.add(channelField); + } + List probeBlockFields = new ArrayList<>(); + for (int i = 0; i < probeChannels.size(); i++) { + FieldDefinition channelField = classDefinition.declareField(a(PRIVATE, FINAL), "probeBlock_" + i, com.facebook.presto.spi.block.Block.class); + probeBlockFields.add(channelField); + } + FieldDefinition probeBlocksArrayField = classDefinition.declareField(a(PRIVATE, FINAL), "probeBlocks", com.facebook.presto.spi.block.Block[].class); + FieldDefinition probePageField = classDefinition.declareField(a(PRIVATE, FINAL), "probePage", Page.class); + FieldDefinition positionField = classDefinition.declareField(a(PRIVATE), "position", int.class); + FieldDefinition probeHashBlockField = classDefinition.declareField(a(PRIVATE, FINAL), "probeHashBlock", com.facebook.presto.spi.block.Block.class); + + generateConstructor(classDefinition, probeChannels, probeHashChannel, lookupSourceField, blockFields, probeBlockFields, probeBlocksArrayField, probePageField, probeHashBlockField, positionField, positionCountField); + generateGetChannelCountMethod(classDefinition, blockFields.size()); + generateAppendToMethod(classDefinition, callSiteBinder, types, blockFields, positionField); + generateAdvanceNextPosition(classDefinition, positionField, positionCountField); + generateGetCurrentJoinPosition(classDefinition, callSiteBinder, lookupSourceField, probePageField, probeHashChannel, probeHashBlockField, positionField); + generateCurrentRowContainsNull(classDefinition, probeBlockFields, positionField); + + return defineClass(classDefinition, JoinProbe.class, callSiteBinder.getBindings(), getClass().getClassLoader()); + } + + private void generateConstructor(ClassDefinition classDefinition, + List probeChannels, + Optional probeHashChannel, + FieldDefinition lookupSourceField, + List blockFields, + List probeChannelFields, + FieldDefinition probeBlocksArrayField, + FieldDefinition probePageField, + FieldDefinition probeHashBlockField, + FieldDefinition positionField, + FieldDefinition positionCountField) + { + Parameter lookupSource = arg("lookupSource", LookupSource.class); + Parameter page = arg("page", Page.class); + MethodDefinition constructorDefinition = classDefinition.declareConstructor(a(PUBLIC), lookupSource, page); + + Variable thisVariable = constructorDefinition.getThis(); + + Block constructor = constructorDefinition + .getBody() + .comment("super();") + .append(thisVariable) + .invokeConstructor(Object.class); + + constructor.comment("this.lookupSource = lookupSource;") + .append(thisVariable.setField(lookupSourceField, lookupSource)); + + constructor.comment("this.positionCount = page.getPositionCount();") + .append(thisVariable.setField(positionCountField, page.invoke("getPositionCount", int.class))); + + constructor.comment("Set block fields"); + for (int index = 0; index < blockFields.size(); index++) { + constructor.append(thisVariable.setField( + blockFields.get(index), + page.invoke("getBlock", com.facebook.presto.spi.block.Block.class, constantInt(index)))); + } + + constructor.comment("Set probe channel fields"); + for (int index = 0; index < probeChannelFields.size(); index++) { + constructor.append(thisVariable.setField( + probeChannelFields.get(index), + thisVariable.getField(blockFields.get(probeChannels.get(index))))); + } + + constructor.comment("this.probeBlocks = new Block[];"); + constructor + .append(thisVariable) + .push(probeChannelFields.size()) + .newArray(com.facebook.presto.spi.block.Block.class) + .putField(probeBlocksArrayField); + for (int index = 0; index < probeChannelFields.size(); index++) { + constructor + .append(thisVariable) + .getField(probeBlocksArrayField) + .push(index) + .append(thisVariable) + .getField(probeChannelFields.get(index)) + .putObjectArrayElement(); + } + + constructor.comment("this.probePage = new Page(probeBlocks)") + .append(thisVariable.setField(probePageField, newInstance(Page.class, thisVariable.getField(probeBlocksArrayField)))); + + if (probeHashChannel.isPresent()) { + Integer index = probeHashChannel.get(); + constructor.comment("this.probeHashBlock = blocks[hashChannel.get()]") + .append(thisVariable.setField( + probeHashBlockField, + thisVariable.getField(blockFields.get(index)))); + } + + constructor.comment("this.position = -1;") + .append(thisVariable.setField(positionField, constantInt(-1))); + + constructor.ret(); + } + + private void generateGetChannelCountMethod(ClassDefinition classDefinition, int channelCount) + { + classDefinition.declareMethod( + a(PUBLIC), + "getChannelCount", + type(int.class)) + .getBody() + .push(channelCount) + .retInt(); + } + + private void generateAppendToMethod( + ClassDefinition classDefinition, + CallSiteBinder callSiteBinder, + List types, List blockFields, + FieldDefinition positionField) + { + Parameter pageBuilder = arg("pageBuilder", PageBuilder.class); + MethodDefinition method = classDefinition.declareMethod( + a(PUBLIC), + "appendTo", + type(void.class), + pageBuilder); + + Variable thisVariable = method.getThis(); + for (int index = 0; index < blockFields.size(); index++) { + Type type = types.get(index); + method.getBody() + .comment("%s.appendTo(block_%s, position, pageBuilder.getBlockBuilder(%s));", type.getClass(), index, index) + .append(constantType(callSiteBinder, type).invoke("appendTo", void.class, + thisVariable.getField(blockFields.get(index)), + thisVariable.getField(positionField), + pageBuilder.invoke("getBlockBuilder", BlockBuilder.class, constantInt(index)))); + } + method.getBody() + .ret(); + } + + private void generateAdvanceNextPosition(ClassDefinition classDefinition, FieldDefinition positionField, FieldDefinition positionCountField) + { + MethodDefinition method = classDefinition.declareMethod( + a(PUBLIC), + "advanceNextPosition", + type(boolean.class)); + + Variable thisVariable = method.getThis(); + method.getBody() + .comment("this.position = this.position + 1;") + .append(thisVariable) + .append(thisVariable) + .getField(positionField) + .push(1) + .intAdd() + .putField(positionField); + + LabelNode lessThan = new LabelNode("lessThan"); + LabelNode end = new LabelNode("end"); + method.getBody() + .comment("return position < positionCount;") + .append(thisVariable) + .getField(positionField) + .append(thisVariable) + .getField(positionCountField) + .append(JumpInstruction.jumpIfIntLessThan(lessThan)) + .push(false) + .gotoLabel(end) + .visitLabel(lessThan) + .push(true) + .visitLabel(end) + .retBoolean(); + } + + private void generateGetCurrentJoinPosition(ClassDefinition classDefinition, + CallSiteBinder callSiteBinder, + FieldDefinition lookupSourceField, + FieldDefinition probePageField, + Optional probeHashChannel, + FieldDefinition probeHashBlockField, + FieldDefinition positionField) + { + MethodDefinition method = classDefinition.declareMethod( + a(PUBLIC), + "getCurrentJoinPosition", + type(long.class)); + + Variable thisVariable = method.getThis(); + Block body = method.getBody() + .append(new IfStatement() + .condition(thisVariable.invoke("currentRowContainsNull", boolean.class)) + .ifTrue(constantLong(-1).ret())); + + ByteCodeExpression position = thisVariable.getField(positionField); + ByteCodeExpression page = thisVariable.getField(probePageField); + ByteCodeExpression probeHashBlock = thisVariable.getField(probeHashBlockField); + if (probeHashChannel.isPresent()) { + body.append(thisVariable.getField(lookupSourceField).invoke("getJoinPosition", long.class, + position, + page, + constantType(callSiteBinder, BigintType.BIGINT).invoke("getLong", + long.class, + probeHashBlock, + position) + .cast(int.class))) + .retLong(); + } + else { + body.append(thisVariable.getField(lookupSourceField).invoke("getJoinPosition", long.class, position, page)).retLong(); + } + } + + private void generateCurrentRowContainsNull(ClassDefinition classDefinition, List probeBlockFields, FieldDefinition positionField) + { + MethodDefinition method = classDefinition.declareMethod( + a(PRIVATE), + "currentRowContainsNull", + type(boolean.class)); + + Variable thisVariable = method.getThis(); + for (FieldDefinition probeBlockField : probeBlockFields) { + LabelNode checkNextField = new LabelNode("checkNextField"); + method.getBody() + .append(thisVariable.getField(probeBlockField).invoke("isNull", boolean.class, thisVariable.getField(positionField))) + .ifFalseGoto(checkNextField) + .push(true) + .retBoolean() + .visitLabel(checkNextField); + } + + method.getBody() + .push(false) + .retInt(); + } + + public static class ReflectionJoinProbeFactory + implements JoinProbeFactory + { + private final Constructor constructor; + + public ReflectionJoinProbeFactory(Class joinProbeClass) + { + try { + constructor = joinProbeClass.getConstructor(LookupSource.class, Page.class); + } + catch (NoSuchMethodException e) { + throw Throwables.propagate(e); + } + } + + @Override + public JoinProbe createJoinProbe(LookupSource lookupSource, Page page) + { + try { + return constructor.newInstance(lookupSource, page); + } + catch (Exception e) { + throw Throwables.propagate(e); + } + } + } + + private static final class JoinOperatorCacheKey + { + private final List types; + private final List probeChannels; + private final JoinType joinType; + private final Optional probeHashChannel; + + private JoinOperatorCacheKey(List types, + List probeChannels, + Optional probeHashChannel, + JoinType joinType) + { + this.probeHashChannel = probeHashChannel; + this.types = ImmutableList.copyOf(types); + this.probeChannels = ImmutableList.copyOf(probeChannels); + this.joinType = joinType; + } + + private List getTypes() + { + return types; + } + + private List getProbeChannels() + { + return probeChannels; + } + + private Optional getProbeHashChannel() + { + return probeHashChannel; + } + + @Override + public int hashCode() + { + return Objects.hash(types, probeChannels, joinType); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (!(obj instanceof JoinOperatorCacheKey)) { + return false; + } + JoinOperatorCacheKey other = (JoinOperatorCacheKey) obj; + return Objects.equals(this.types, other.types) && + Objects.equals(this.probeChannels, other.probeChannels) && + Objects.equals(this.probeHashChannel, other.probeHashChannel) && + Objects.equals(this.joinType, other.joinType); + } + } + + private static class HashJoinOperatorFactoryFactory + { + private final JoinProbeFactory joinProbeFactory; + private final Constructor constructor; + + private HashJoinOperatorFactoryFactory(JoinProbeFactory joinProbeFactory, Class operatorFactoryClass) + { + this.joinProbeFactory = joinProbeFactory; + + try { + constructor = operatorFactoryClass.getConstructor(int.class, LookupSourceSupplier.class, List.class, JoinType.class, JoinProbeFactory.class); + } + catch (NoSuchMethodException e) { + throw Throwables.propagate(e); + } + } + + public OperatorFactory createHashJoinOperatorFactory( + int operatorId, + LookupSourceSupplier lookupSourceSupplier, + List probeTypes, + List probeJoinChannel, + JoinType joinType) + { + try { + return constructor.newInstance(operatorId, lookupSourceSupplier, probeTypes, joinType, joinProbeFactory); + } + catch (Exception e) { + throw Throwables.propagate(e); + } + } + } + + public static void checkState(boolean left, boolean right) + { + if (left != right) { + throw new IllegalStateException(); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/gen/NullIfCodeGenerator.java b/presto-main/src/main/java/com/facebook/presto/sql/gen/NullIfCodeGenerator.java new file mode 100644 index 00000000..69a15ed6 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/gen/NullIfCodeGenerator.java @@ -0,0 +1,95 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.gen; + +import com.facebook.presto.byteCode.Block; +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.Scope; +import com.facebook.presto.byteCode.control.IfStatement; +import com.facebook.presto.byteCode.instruction.LabelNode; +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.OperatorType; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.relational.RowExpression; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.facebook.presto.byteCode.expression.ByteCodeExpressions.constantTrue; + +public class NullIfCodeGenerator + implements ByteCodeGenerator +{ + @Override + public ByteCodeNode generateExpression(Signature signature, ByteCodeGeneratorContext generatorContext, Type returnType, List arguments) + { + Scope scope = generatorContext.getScope(); + + RowExpression first = arguments.get(0); + RowExpression second = arguments.get(1); + + LabelNode notMatch = new LabelNode("notMatch"); + + // push first arg on the stack + Block block = new Block() + .comment("check if first arg is null") + .append(generatorContext.generate(first)) + .append(ByteCodeUtils.ifWasNullPopAndGoto(scope, notMatch, void.class)); + + Type firstType = first.getType(); + Type secondType = second.getType(); + + // this is a hack! We shouldn't be determining type coercions at this point, but there's no way + // around it in the current expression AST + Type commonType = FunctionRegistry.getCommonSuperType(firstType, secondType).get(); + + // if (equal(cast(first as ), cast(second as )) + FunctionInfo equalsFunction = generatorContext.getRegistry().resolveOperator(OperatorType.EQUAL, ImmutableList.of(firstType, secondType)); + ByteCodeNode equalsCall = generatorContext.generateCall( + equalsFunction, + ImmutableList.of( + cast(generatorContext, new Block().dup(firstType.getJavaType()), firstType, commonType), + cast(generatorContext, generatorContext.generate(second), secondType, commonType))); + + Block conditionBlock = new Block() + .append(equalsCall) + .append(ByteCodeUtils.ifWasNullClearPopAndGoto(scope, notMatch, void.class, boolean.class)); + + // if first and second are equal, return null + Block trueBlock = new Block() + .append(generatorContext.wasNull().set(constantTrue())) + .pop(first.getType().getJavaType()) + .pushJavaDefault(first.getType().getJavaType()); + + // else return first (which is still on the stack + block.append(new IfStatement() + .condition(conditionBlock) + .ifTrue(trueBlock) + .ifFalse(notMatch)); + + return block; + } + + private ByteCodeNode cast(ByteCodeGeneratorContext generatorContext, ByteCodeNode argument, Type fromType, Type toType) + { + FunctionInfo function = generatorContext + .getRegistry() + .getCoercion(fromType, toType); + + // TODO: do we need a full function call? (nullability checks, etc) + return generatorContext.generateCall(function, ImmutableList.of(argument)); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/gen/OrCodeGenerator.java b/presto-main/src/main/java/com/facebook/presto/sql/gen/OrCodeGenerator.java new file mode 100644 index 00000000..b0610cb2 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/gen/OrCodeGenerator.java @@ -0,0 +1,101 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.gen; + +import com.facebook.presto.byteCode.Block; +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.Variable; +import com.facebook.presto.byteCode.control.IfStatement; +import com.facebook.presto.byteCode.instruction.LabelNode; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.relational.RowExpression; +import com.google.common.base.Preconditions; + +import java.util.List; + +import static com.facebook.presto.byteCode.expression.ByteCodeExpressions.constantFalse; + +public class OrCodeGenerator + implements ByteCodeGenerator +{ + @Override + public ByteCodeNode generateExpression(Signature signature, ByteCodeGeneratorContext generator, Type returnType, List arguments) + { + Preconditions.checkArgument(arguments.size() == 2); + + Variable wasNull = generator.wasNull(); + Block block = new Block() + .comment("OR") + .setDescription("OR"); + + ByteCodeNode left = generator.generate(arguments.get(0)); + ByteCodeNode right = generator.generate(arguments.get(1)); + + block.append(left); + + IfStatement ifLeftIsNull = new IfStatement("if left wasNull...") + .condition(wasNull); + + LabelNode end = new LabelNode("end"); + ifLeftIsNull.ifTrue(new Block() + .comment("clear the null flag, pop left value off stack, and push left null flag on the stack (true)") + .append(wasNull.set(constantFalse())) + .pop(arguments.get(0).getType().getJavaType()) // discard left value + .push(true)); + + LabelNode leftIsFalse = new LabelNode("leftIsFalse"); + ifLeftIsNull.ifFalse(new Block() + .comment("if left is true, push true, and goto end") + .ifFalseGoto(leftIsFalse) + .push(true) + .gotoLabel(end) + .comment("left was false; push left null flag on the stack (false)") + .visitLabel(leftIsFalse) + .push(false)); + + block.append(ifLeftIsNull); + + // At this point we know the left expression was either NULL or FALSE. The stack contains a single boolean + // value for this expression which indicates if the left value was NULL. + + // eval right! + block.append(right); + + IfStatement ifRightIsNull = new IfStatement("if right wasNull...") + .condition(wasNull); + + // this leaves a single boolean on the stack which is ignored since the value in NULL + ifRightIsNull.ifTrue() + .comment("right was null, pop the right value off the stack; wasNull flag remains set to TRUE") + .pop(arguments.get(1).getType().getJavaType()); + + LabelNode rightIsTrue = new LabelNode("rightIsTrue"); + ifRightIsNull.ifFalse() + .comment("if right is true, pop left null flag off stack, push true and goto end") + .ifFalseGoto(rightIsTrue) + .pop(boolean.class) + .push(true) + .gotoLabel(end) + .comment("right was false; store left null flag (on stack) in wasNull variable, and push false") + .visitLabel(rightIsTrue) + .putVariable(wasNull) + .push(false); + + block.append(ifRightIsNull) + .visitLabel(end); + + return block; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/gen/OrderingCompiler.java b/presto-main/src/main/java/com/facebook/presto/sql/gen/OrderingCompiler.java new file mode 100644 index 00000000..dc4eec5c --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/gen/OrderingCompiler.java @@ -0,0 +1,274 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.gen; + +import com.facebook.presto.byteCode.Block; +import com.facebook.presto.byteCode.ClassDefinition; +import com.facebook.presto.byteCode.Scope; +import com.facebook.presto.byteCode.MethodDefinition; +import com.facebook.presto.byteCode.Parameter; +import com.facebook.presto.byteCode.Variable; +import com.facebook.presto.byteCode.expression.ByteCodeExpression; +import com.facebook.presto.byteCode.instruction.LabelNode; +import com.facebook.presto.operator.PagesIndex; +import com.facebook.presto.operator.PagesIndexComparator; +import com.facebook.presto.operator.PagesIndexOrdering; +import com.facebook.presto.operator.SimplePagesIndexComparator; +import com.facebook.presto.operator.SyntheticAddress; +import com.facebook.presto.spi.block.SortOrder; +import com.facebook.presto.spi.type.Type; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Throwables; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.ExecutionError; +import com.google.common.util.concurrent.UncheckedExecutionException; +import io.airlift.log.Logger; +import it.unimi.dsi.fastutil.longs.LongArrayList; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; + +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ExecutionException; + +import static com.facebook.presto.byteCode.Access.FINAL; +import static com.facebook.presto.byteCode.Access.PUBLIC; +import static com.facebook.presto.byteCode.Access.a; +import static com.facebook.presto.byteCode.Parameter.arg; +import static com.facebook.presto.byteCode.ParameterizedType.type; +import static com.facebook.presto.byteCode.expression.ByteCodeExpressions.constantInt; +import static com.facebook.presto.byteCode.expression.ByteCodeExpressions.getStatic; +import static com.facebook.presto.byteCode.expression.ByteCodeExpressions.invokeStatic; +import static com.facebook.presto.sql.gen.CompilerUtils.defineClass; +import static com.facebook.presto.sql.gen.CompilerUtils.makeClassName; +import static com.facebook.presto.sql.gen.SqlTypeByteCodeExpression.constantType; +import static com.google.common.base.Preconditions.checkNotNull; + +public class OrderingCompiler +{ + private static final Logger log = Logger.get(OrderingCompiler.class); + + private final LoadingCache pagesIndexOrderings = CacheBuilder.newBuilder().maximumSize(1000).build( + new CacheLoader() + { + @Override + public PagesIndexOrdering load(PagesIndexComparatorCacheKey key) + throws Exception + { + return internalCompilePagesIndexOrdering(key.getSortTypes(), key.getSortChannels(), key.getSortOrders()); + } + }); + + public PagesIndexOrdering compilePagesIndexOrdering(List sortTypes, List sortChannels, List sortOrders) + { + checkNotNull(sortTypes, "sortTypes is null"); + checkNotNull(sortChannels, "sortChannels is null"); + checkNotNull(sortOrders, "sortOrders is null"); + + try { + return pagesIndexOrderings.get(new PagesIndexComparatorCacheKey(sortTypes, sortChannels, sortOrders)); + } + catch (ExecutionException | UncheckedExecutionException | ExecutionError e) { + throw Throwables.propagate(e.getCause()); + } + } + + @VisibleForTesting + public PagesIndexOrdering internalCompilePagesIndexOrdering(List sortTypes, List sortChannels, List sortOrders) + throws Exception + { + checkNotNull(sortChannels, "sortChannels is null"); + checkNotNull(sortOrders, "sortOrders is null"); + + PagesIndexComparator comparator; + try { + Class pagesHashStrategyClass = compilePagesIndexComparator(sortTypes, sortChannels, sortOrders); + comparator = pagesHashStrategyClass.newInstance(); + } + catch (Throwable e) { + log.error(e, "Error compiling comparator for channels %s with order %s", sortChannels, sortChannels); + comparator = new SimplePagesIndexComparator(sortTypes, sortChannels, sortOrders); + } + + // we may want to load a separate PagesIndexOrdering for each comparator + return new PagesIndexOrdering(comparator); + } + + private Class compilePagesIndexComparator( + List sortTypes, + List sortChannels, + List sortOrders) + { + CallSiteBinder callSiteBinder = new CallSiteBinder(); + + ClassDefinition classDefinition = new ClassDefinition( + a(PUBLIC, FINAL), + makeClassName("PagesIndexComparator"), + type(Object.class), + type(PagesIndexComparator.class)); + + classDefinition.declareDefaultConstructor(a(PUBLIC)); + generateCompareTo(classDefinition, callSiteBinder, sortTypes, sortChannels, sortOrders); + + return defineClass(classDefinition, PagesIndexComparator.class, callSiteBinder.getBindings(), getClass().getClassLoader()); + } + + private void generateCompareTo(ClassDefinition classDefinition, CallSiteBinder callSiteBinder, List sortTypes, List sortChannels, List sortOrders) + { + Parameter pagesIndex = arg("pagesIndex", PagesIndex.class); + Parameter leftPosition = arg("leftPosition", int.class); + Parameter rightPosition = arg("rightPosition", int.class); + MethodDefinition compareToMethod = classDefinition.declareMethod(a(PUBLIC), "compareTo", type(int.class), pagesIndex, leftPosition, rightPosition); + Scope scope = compareToMethod.getScope(); + + Variable valueAddresses = scope.declareVariable(LongArrayList.class, "valueAddresses"); + compareToMethod + .getBody() + .comment("LongArrayList valueAddresses = pagesIndex.valueAddresses") + .append(valueAddresses.set(pagesIndex.invoke("getValueAddresses", LongArrayList.class))); + + Variable leftPageAddress = scope.declareVariable(long.class, "leftPageAddress"); + compareToMethod + .getBody() + .comment("long leftPageAddress = valueAddresses.getLong(leftPosition)") + .append(leftPageAddress.set(valueAddresses.invoke("getLong", long.class, leftPosition))); + + Variable leftBlockIndex = scope.declareVariable(int.class, "leftBlockIndex"); + compareToMethod + .getBody() + .comment("int leftBlockIndex = decodeSliceIndex(leftPageAddress)") + .append(leftBlockIndex.set(invokeStatic(SyntheticAddress.class, "decodeSliceIndex", int.class, leftPageAddress))); + + Variable leftBlockPosition = scope.declareVariable(int.class, "leftBlockPosition"); + compareToMethod + .getBody() + .comment("int leftBlockPosition = decodePosition(leftPageAddress)") + .append(leftBlockPosition.set(invokeStatic(SyntheticAddress.class, "decodePosition", int.class, leftPageAddress))); + + Variable rightPageAddress = scope.declareVariable(long.class, "rightPageAddress"); + compareToMethod + .getBody() + .comment("long rightPageAddress = valueAddresses.getLong(rightPosition);") + .append(rightPageAddress.set(valueAddresses.invoke("getLong", long.class, rightPosition))); + + Variable rightBlockIndex = scope.declareVariable(int.class, "rightBlockIndex"); + compareToMethod + .getBody() + .comment("int rightBlockIndex = decodeSliceIndex(rightPageAddress)") + .append(rightBlockIndex.set(invokeStatic(SyntheticAddress.class, "decodeSliceIndex", int.class, rightPageAddress))); + + Variable rightBlockPosition = scope.declareVariable(int.class, "rightBlockPosition"); + compareToMethod + .getBody() + .comment("int rightBlockPosition = decodePosition(rightPageAddress)") + .append(rightBlockPosition.set(invokeStatic(SyntheticAddress.class, "decodePosition", int.class, rightPageAddress))); + + for (int i = 0; i < sortChannels.size(); i++) { + int sortChannel = sortChannels.get(i); + SortOrder sortOrder = sortOrders.get(i); + + Block block = new Block() + .setDescription("compare channel " + sortChannel + " " + sortOrder); + + Type sortType = sortTypes.get(i); + + ByteCodeExpression leftBlock = pagesIndex + .invoke("getChannel", ObjectArrayList.class, constantInt(sortChannel)) + .invoke("get", Object.class, leftBlockIndex) + .cast(com.facebook.presto.spi.block.Block.class); + + ByteCodeExpression rightBlock = pagesIndex + .invoke("getChannel", ObjectArrayList.class, constantInt(sortChannel)) + .invoke("get", Object.class, rightBlockIndex) + .cast(com.facebook.presto.spi.block.Block.class); + + block.append(getStatic(SortOrder.class, sortOrder.name()) + .invoke("compareBlockValue", + int.class, + ImmutableList.of(Type.class, com.facebook.presto.spi.block.Block.class, int.class, com.facebook.presto.spi.block.Block.class, int.class), + constantType(callSiteBinder, sortType), + leftBlock, + leftBlockPosition, + rightBlock, + rightBlockPosition)); + + LabelNode equal = new LabelNode("equal"); + block.comment("if (compare != 0) return compare") + .dup() + .ifZeroGoto(equal) + .retInt() + .visitLabel(equal) + .pop(int.class); + + compareToMethod.getBody().append(block); + } + + // values are equal + compareToMethod.getBody() + .push(0) + .retInt(); + } + + private static final class PagesIndexComparatorCacheKey + { + private final List sortTypes; + private final List sortChannels; + private final List sortOrders; + + private PagesIndexComparatorCacheKey(List sortTypes, List sortChannels, List sortOrders) + { + this.sortTypes = ImmutableList.copyOf(sortTypes); + this.sortChannels = ImmutableList.copyOf(sortChannels); + this.sortOrders = ImmutableList.copyOf(sortOrders); + } + + public List getSortTypes() + { + return sortTypes; + } + + public List getSortChannels() + { + return sortChannels; + } + + public List getSortOrders() + { + return sortOrders; + } + + @Override + public int hashCode() + { + return Objects.hash(sortTypes, sortChannels, sortOrders); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + PagesIndexComparatorCacheKey other = (PagesIndexComparatorCacheKey) obj; + return Objects.equals(this.sortTypes, other.sortTypes) && + Objects.equals(this.sortChannels, other.sortChannels) && + Objects.equals(this.sortOrders, other.sortOrders); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/gen/PageProcessorCompiler.java b/presto-main/src/main/java/com/facebook/presto/sql/gen/PageProcessorCompiler.java new file mode 100644 index 00000000..5cb9f959 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/gen/PageProcessorCompiler.java @@ -0,0 +1,346 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.gen; + +import com.facebook.presto.byteCode.Block; +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.ClassDefinition; +import com.facebook.presto.byteCode.Scope; +import com.facebook.presto.byteCode.MethodDefinition; +import com.facebook.presto.byteCode.Parameter; +import com.facebook.presto.byteCode.ParameterizedType; +import com.facebook.presto.byteCode.Variable; +import com.facebook.presto.byteCode.control.ForLoop; +import com.facebook.presto.byteCode.control.IfStatement; +import com.facebook.presto.byteCode.instruction.LabelNode; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.operator.PageProcessor; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.relational.CallExpression; +import com.facebook.presto.sql.relational.ConstantExpression; +import com.facebook.presto.sql.relational.Expressions; +import com.facebook.presto.sql.relational.InputReferenceExpression; +import com.facebook.presto.sql.relational.RowExpression; +import com.facebook.presto.sql.relational.RowExpressionVisitor; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.common.primitives.Primitives; + +import java.util.List; +import java.util.TreeSet; + +import static com.facebook.presto.byteCode.Access.PUBLIC; +import static com.facebook.presto.byteCode.Access.a; +import static com.facebook.presto.byteCode.Parameter.arg; +import static com.facebook.presto.byteCode.OpCode.NOP; +import static com.facebook.presto.byteCode.ParameterizedType.type; +import static com.facebook.presto.sql.gen.ByteCodeUtils.generateWrite; +import static com.facebook.presto.sql.gen.ByteCodeUtils.loadConstant; +import static java.lang.String.format; +import static java.util.Collections.nCopies; + +public class PageProcessorCompiler + implements BodyCompiler +{ + private final Metadata metadata; + + public PageProcessorCompiler(Metadata metadata) + { + this.metadata = metadata; + } + + @Override + public void generateMethods(ClassDefinition classDefinition, CallSiteBinder callSiteBinder, RowExpression filter, List projections) + { + generateProcessMethod(classDefinition, filter, projections); + generateFilterMethod(classDefinition, callSiteBinder, filter); + + for (int i = 0; i < projections.size(); i++) { + generateProjectMethod(classDefinition, callSiteBinder, "project_" + i, projections.get(i)); + } + } + + private void generateProcessMethod(ClassDefinition classDefinition, RowExpression filter, List projections) + { + Parameter session = arg("session", ConnectorSession.class); + Parameter page = arg("page", Page.class); + Parameter start = arg("start", int.class); + Parameter end = arg("end", int.class); + Parameter pageBuilder = arg("pageBuilder", PageBuilder.class); + MethodDefinition method = classDefinition.declareMethod(a(PUBLIC), "process", type(int.class), session, page, start, end, pageBuilder); + + Scope scope = method.getScope(); + Variable thisVariable = method.getThis(); + Variable position = scope.declareVariable(int.class, "position"); + + method.getBody() + .comment("int position = start;") + .getVariable(start) + .putVariable(position); + + List allInputChannels = getInputChannels(Iterables.concat(projections, ImmutableList.of(filter))); + for (int channel : allInputChannels) { + Variable blockVariable = scope.declareVariable(com.facebook.presto.spi.block.Block.class, "block_" + channel); + method.getBody() + .comment("Block %s = page.getBlock(%s);", blockVariable.getName(), channel) + .getVariable(page) + .push(channel) + .invokeVirtual(Page.class, "getBlock", com.facebook.presto.spi.block.Block.class, int.class) + .putVariable(blockVariable); + } + + // + // for loop loop body + // + LabelNode done = new LabelNode("done"); + + Block loopBody = new Block(); + + ForLoop loop = new ForLoop() + .initialize(NOP) + .condition(new Block() + .comment("position < end") + .getVariable(position) + .getVariable(end) + .invokeStatic(CompilerOperations.class, "lessThan", boolean.class, int.class, int.class) + ) + .update(new Block() + .comment("position++") + .incrementVariable(position, (byte) 1)) + .body(loopBody); + + loopBody.comment("if (pageBuilder.isFull()) break;") + .getVariable(pageBuilder) + .invokeVirtual(PageBuilder.class, "isFull", boolean.class) + .ifTrueGoto(done); + + // if (filter(cursor)) + IfStatement filterBlock = new IfStatement(); + filterBlock.condition() + .append(thisVariable) + .getVariable(session) + .append(pushBlockVariables(scope, getInputChannels(filter))) + .getVariable(position) + .invokeVirtual(classDefinition.getType(), + "filter", + type(boolean.class), + ImmutableList.builder() + .add(type(ConnectorSession.class)) + .addAll(nCopies(getInputChannels(filter).size(), type(com.facebook.presto.spi.block.Block.class))) + .add(type(int.class)) + .build()); + + filterBlock.ifTrue() + .append(pageBuilder) + .invokeVirtual(PageBuilder.class, "declarePosition", void.class); + + for (int projectionIndex = 0; projectionIndex < projections.size(); projectionIndex++) { + List inputChannels = getInputChannels(projections.get(projectionIndex)); + + filterBlock.ifTrue() + .append(thisVariable) + .append(session) + .append(pushBlockVariables(scope, inputChannels)) + .getVariable(position); + + filterBlock.ifTrue() + .comment("pageBuilder.getBlockBuilder(%d)", projectionIndex) + .append(pageBuilder) + .push(projectionIndex) + .invokeVirtual(PageBuilder.class, "getBlockBuilder", BlockBuilder.class, int.class); + + filterBlock.ifTrue() + .comment("project_%d(session, block_%s, position, blockBuilder)", projectionIndex, inputChannels) + .invokeVirtual(classDefinition.getType(), + "project_" + projectionIndex, + type(void.class), + ImmutableList.builder() + .add(type(ConnectorSession.class)) + .addAll(nCopies(inputChannels.size(), type(com.facebook.presto.spi.block.Block.class))) + .add(type(int.class)) + .add(type(BlockBuilder.class)) + .build()); + } + + loopBody.append(filterBlock); + + method.getBody() + .append(loop) + .visitLabel(done) + .comment("return position;") + .getVariable(position) + .retInt(); + } + + private void generateFilterMethod(ClassDefinition classDefinition, CallSiteBinder callSiteBinder, RowExpression filter) + { + Parameter session = arg("session", ConnectorSession.class); + List blocks = toBlockParameters(getInputChannels(filter)); + Parameter position = arg("position", int.class); + MethodDefinition method = classDefinition.declareMethod( + a(PUBLIC), + "filter", + type(boolean.class), + ImmutableList.builder() + .add(session) + .addAll(blocks) + .add(position) + .build()); + + method.comment("Filter: %s", filter.toString()); + + Scope scope = method.getScope(); + Variable wasNullVariable = scope.declareVariable(type(boolean.class), "wasNull"); + + ByteCodeExpressionVisitor visitor = new ByteCodeExpressionVisitor( + callSiteBinder, + fieldReferenceCompiler(callSiteBinder, position, wasNullVariable), + metadata.getFunctionRegistry()); + ByteCodeNode body = filter.accept(visitor, scope); + + LabelNode end = new LabelNode("end"); + method + .getBody() + .comment("boolean wasNull = false;") + .putVariable(wasNullVariable, false) + .append(body) + .getVariable(wasNullVariable) + .ifFalseGoto(end) + .pop(boolean.class) + .push(false) + .visitLabel(end) + .retBoolean(); + } + + private void generateProjectMethod(ClassDefinition classDefinition, CallSiteBinder callSiteBinder, String methodName, RowExpression projection) + { + Parameter session = arg("session", ConnectorSession.class); + List inputs = toBlockParameters(getInputChannels(projection)); + Parameter position = arg("position", int.class); + Parameter output = arg("output", BlockBuilder.class); + MethodDefinition method = classDefinition.declareMethod( + a(PUBLIC), + methodName, + type(void.class), + ImmutableList.builder() + .add(session) + .addAll(inputs) + .add(position) + .add(output) + .build()); + + method.comment("Projection: %s", projection.toString()); + + Scope scope = method.getScope(); + Variable wasNullVariable = scope.declareVariable(type(boolean.class), "wasNull"); + + Block body = method.getBody() + .comment("boolean wasNull = false;") + .putVariable(wasNullVariable, false); + + ByteCodeExpressionVisitor visitor = new ByteCodeExpressionVisitor(callSiteBinder, fieldReferenceCompiler(callSiteBinder, position, wasNullVariable), metadata.getFunctionRegistry()); + + body.getVariable(output) + .comment("evaluate projection: " + projection.toString()) + .append(projection.accept(visitor, scope)) + .append(generateWrite(callSiteBinder, scope, wasNullVariable, projection.getType())) + .ret(); + } + + private static List getInputChannels(Iterable expressions) + { + TreeSet channels = new TreeSet<>(); + for (RowExpression expression : Expressions.subExpressions(expressions)) { + if (expression instanceof InputReferenceExpression) { + channels.add(((InputReferenceExpression) expression).getField()); + } + } + return ImmutableList.copyOf(channels); + } + + private static List getInputChannels(RowExpression expression) + { + return getInputChannels(ImmutableList.of(expression)); + } + + private static List toBlockParameters(List inputChannels) + { + ImmutableList.Builder parameters = ImmutableList.builder(); + for (int channel : inputChannels) { + parameters.add(arg("block_" + channel, com.facebook.presto.spi.block.Block.class)); + } + return parameters.build(); + } + + private static ByteCodeNode pushBlockVariables(Scope scope, List inputs) + { + Block block = new Block(); + for (int channel : inputs) { + block.append(scope.getVariable("block_" + channel)); + } + return block; + } + + private RowExpressionVisitor fieldReferenceCompiler(final CallSiteBinder callSiteBinder, final Variable positionVariable, final Variable wasNullVariable) + { + return new RowExpressionVisitor() + { + @Override + public ByteCodeNode visitInputReference(InputReferenceExpression node, Scope scope) + { + int field = node.getField(); + Type type = node.getType(); + Variable block = scope.getVariable("block_" + field); + + Class javaType = type.getJavaType(); + IfStatement ifStatement = new IfStatement(); + ifStatement.condition() + .setDescription(format("block_%d.get%s()", field, type)) + .append(block) + .getVariable(positionVariable) + .invokeInterface(com.facebook.presto.spi.block.Block.class, "isNull", boolean.class, int.class); + + ifStatement.ifTrue() + .putVariable(wasNullVariable, true) + .pushJavaDefault(javaType); + + String methodName = "get" + Primitives.wrap(javaType).getSimpleName(); + + ifStatement.ifFalse() + .append(loadConstant(callSiteBinder.bind(type, Type.class))) + .append(block) + .getVariable(positionVariable) + .invokeInterface(Type.class, methodName, javaType, com.facebook.presto.spi.block.Block.class, int.class); + + return ifStatement; + } + + @Override + public ByteCodeNode visitCall(CallExpression call, Scope scope) + { + throw new UnsupportedOperationException("not yet implemented"); + } + + @Override + public ByteCodeNode visitConstant(ConstantExpression literal, Scope scope) + { + throw new UnsupportedOperationException("not yet implemented"); + } + }; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/gen/SqlTypeByteCodeExpression.java b/presto-main/src/main/java/com/facebook/presto/sql/gen/SqlTypeByteCodeExpression.java new file mode 100644 index 00000000..daa57295 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/gen/SqlTypeByteCodeExpression.java @@ -0,0 +1,114 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.gen; + +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.MethodGenerationContext; +import com.facebook.presto.byteCode.expression.ByteCodeExpression; +import com.facebook.presto.byteCode.instruction.InvokeInstruction; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; + +import java.lang.reflect.Method; +import java.util.List; + +import static com.facebook.presto.byteCode.ParameterizedType.type; +import static com.facebook.presto.spi.StandardErrorCode.INVALID_FUNCTION_ARGUMENT; +import static com.facebook.presto.sql.gen.Bootstrap.BOOTSTRAP_METHOD; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.lang.String.format; + +public class SqlTypeByteCodeExpression + extends ByteCodeExpression +{ + public static SqlTypeByteCodeExpression constantType(CallSiteBinder callSiteBinder, Type type) + { + checkNotNull(callSiteBinder, "callSiteBinder is null"); + checkNotNull(type, "type is null"); + + Binding binding = callSiteBinder.bind(type, Type.class); + return new SqlTypeByteCodeExpression(type, binding, BOOTSTRAP_METHOD); + } + + private final Type type; + private final Binding binding; + private final Method bootstrapMethod; + + private SqlTypeByteCodeExpression(Type type, Binding binding, Method bootstrapMethod) + { + super(type(Type.class)); + + this.type = checkNotNull(type, "type is null"); + this.binding = checkNotNull(binding, "binding is null"); + this.bootstrapMethod = checkNotNull(bootstrapMethod, "bootstrapMethod is null"); + } + + @Override + public ByteCodeNode getByteCode(MethodGenerationContext generationContext) + { + return InvokeInstruction.invokeDynamic(type.getTypeSignature().toString().replaceAll("\\W+", "_"), binding.getType(), bootstrapMethod, binding.getBindingId()); + } + + @Override + public List getChildNodes() + { + return ImmutableList.of(); + } + + @Override + protected String formatOneLine() + { + return type.getTypeSignature().toString(); + } + + public ByteCodeExpression getValue(ByteCodeExpression block, ByteCodeExpression position) + { + Class fromJavaElementType = type.getJavaType(); + + if (fromJavaElementType == boolean.class) { + return invoke("getBoolean", boolean.class, block, position); + } + if (fromJavaElementType == long.class) { + return invoke("getLong", long.class, block, position); + } + if (fromJavaElementType == double.class) { + return invoke("getDouble", double.class, block, position); + } + if (fromJavaElementType == Slice.class) { + return invoke("getSlice", Slice.class, block, position); + } + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, format("Unexpected type %s", fromJavaElementType.getName())); + } + + public ByteCodeExpression writeValue(ByteCodeExpression blockBuilder, ByteCodeExpression value) + { + Class fromJavaElementType = type.getJavaType(); + + if (fromJavaElementType == boolean.class) { + return invoke("writeBoolean", void.class, blockBuilder, value); + } + if (fromJavaElementType == long.class) { + return invoke("writeLong", void.class, blockBuilder, value); + } + if (fromJavaElementType == double.class) { + return invoke("writeDouble", void.class, blockBuilder, value); + } + if (fromJavaElementType == Slice.class) { + return invoke("writeSlice", void.class, blockBuilder, value); + } + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, format("Unexpected type %s", fromJavaElementType.getName())); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/gen/SwitchCodeGenerator.java b/presto-main/src/main/java/com/facebook/presto/sql/gen/SwitchCodeGenerator.java new file mode 100644 index 00000000..2de838ca --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/gen/SwitchCodeGenerator.java @@ -0,0 +1,140 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.gen; + +import com.facebook.presto.byteCode.Block; +import com.facebook.presto.byteCode.ByteCodeNode; +import com.facebook.presto.byteCode.Scope; +import com.facebook.presto.byteCode.Variable; +import com.facebook.presto.byteCode.control.IfStatement; +import com.facebook.presto.byteCode.instruction.LabelNode; +import com.facebook.presto.byteCode.instruction.VariableInstruction; +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.OperatorType; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.relational.CallExpression; +import com.facebook.presto.sql.relational.RowExpression; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; + +import java.util.List; + +import static com.facebook.presto.byteCode.expression.ByteCodeExpressions.constantFalse; +import static com.facebook.presto.byteCode.expression.ByteCodeExpressions.constantTrue; + +public class SwitchCodeGenerator + implements ByteCodeGenerator +{ + @Override + public ByteCodeNode generateExpression(Signature signature, ByteCodeGeneratorContext generatorContext, Type returnType, List arguments) + { + // TODO: compile as + /* + hashCode = hashCode() + + // all constant expressions before a non-constant + switch (hashCode) { + case ...: + if ( == ) { + ... + } + else if ( == ) { + ... + } + else if (...) { + } + case ...: + ... + } + + if ( == ) { + ... + } + else if ( == ) { + ... + } + ... + + // repeat with next sequence of constant expressions + */ + + Scope scope = generatorContext.getScope(); + + // process value, else, and all when clauses + RowExpression value = arguments.get(0); + ByteCodeNode valueBytecode = generatorContext.generate(value); + ByteCodeNode elseValue; + + List whenClauses; + RowExpression last = arguments.get(arguments.size() - 1); + if (last instanceof CallExpression && ((CallExpression) last).getSignature().getName().equals("WHEN")) { + whenClauses = arguments.subList(1, arguments.size()); + elseValue = new Block() + .append(generatorContext.wasNull().set(constantTrue())) + .pushJavaDefault(returnType.getJavaType()); + } + else { + whenClauses = arguments.subList(1, arguments.size() - 1); + elseValue = generatorContext.generate(last); + } + + // determine the type of the value and result + Class valueType = value.getType().getJavaType(); + + // evaluate the value and store it in a variable + LabelNode nullValue = new LabelNode("nullCondition"); + Variable tempVariable = scope.createTempVariable(valueType); + Block block = new Block() + .append(valueBytecode) + .append(ByteCodeUtils.ifWasNullClearPopAndGoto(scope, nullValue, void.class, valueType)) + .putVariable(tempVariable); + + ByteCodeNode getTempVariableNode = VariableInstruction.loadVariable(tempVariable); + + // build the statements + elseValue = new Block().visitLabel(nullValue).append(elseValue); + // reverse list because current if statement builder doesn't support if/else so we need to build the if statements bottom up + for (RowExpression clause : Lists.reverse(whenClauses)) { + Preconditions.checkArgument(clause instanceof CallExpression && ((CallExpression) clause).getSignature().getName().equals("WHEN")); + + RowExpression operand = ((CallExpression) clause).getArguments().get(0); + RowExpression result = ((CallExpression) clause).getArguments().get(1); + + // call equals(value, operand) + FunctionInfo equalsFunction = generatorContext.getRegistry().resolveOperator(OperatorType.EQUAL, ImmutableList.of(value.getType(), operand.getType())); + + // TODO: what if operand is null? It seems that the call will return "null" (which is cleared below) + // and the code only does the right thing because the value in the stack for that scenario is + // Java's default for boolean == false + // This code should probably be checking for wasNull after the call and "failing" the equality + // check if wasNull is true + ByteCodeNode equalsCall = generatorContext.generateCall( + equalsFunction, + ImmutableList.of(generatorContext.generate(operand), getTempVariableNode)); + + Block condition = new Block() + .append(equalsCall) + .append(generatorContext.wasNull().set(constantFalse())); + + elseValue = new IfStatement("when") + .condition(condition) + .ifTrue(generatorContext.generate(result)) + .ifFalse(elseValue); + } + + return block.append(elseValue); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/CompilerConfig.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/CompilerConfig.java new file mode 100644 index 00000000..c25cf595 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/CompilerConfig.java @@ -0,0 +1,35 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner; + +import com.facebook.presto.operator.Description; +import io.airlift.configuration.Config; + +public class CompilerConfig +{ + private boolean interpreterEnabled; + + public boolean isInterpreterEnabled() + { + return interpreterEnabled; + } + + @Config("compiler.interpreter-enabled") + @Description("Allows evaluation to fall back to interpreter if compilation fails") + public CompilerConfig setInterpreterEnabled(boolean interpreterEnabled) + { + this.interpreterEnabled = interpreterEnabled; + return this; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/DependencyExtractor.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/DependencyExtractor.java new file mode 100644 index 00000000..445992df --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/DependencyExtractor.java @@ -0,0 +1,61 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner; + +import com.facebook.presto.sql.tree.DefaultExpressionTraversalVisitor; +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.QualifiedNameReference; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; + +import java.util.List; +import java.util.Set; + +// TODO: a similar class exists in (TupleAnalyzer.DependencyExtractor) +public final class DependencyExtractor +{ + private DependencyExtractor() {} + + public static Set extractUnique(Expression expression) + { + return ImmutableSet.copyOf(extractAll(expression)); + } + + public static Set extractUnique(Iterable expressions) + { + ImmutableSet.Builder unique = ImmutableSet.builder(); + for (Expression expression : expressions) { + unique.addAll(extractAll(expression)); + } + return unique.build(); + } + + public static List extractAll(Expression expression) + { + ImmutableList.Builder builder = ImmutableList.builder(); + new Visitor().process(expression, builder); + return builder.build(); + } + + private static class Visitor + extends DefaultExpressionTraversalVisitor> + { + @Override + protected Void visitQualifiedNameReference(QualifiedNameReference node, ImmutableList.Builder builder) + { + builder.add(Symbol.fromQualifiedName(node.getName())); + return null; + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/DeterminismEvaluator.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/DeterminismEvaluator.java new file mode 100644 index 00000000..bd7c3722 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/DeterminismEvaluator.java @@ -0,0 +1,53 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner; + +import com.facebook.presto.sql.tree.DefaultExpressionTraversalVisitor; +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.FunctionCall; +import com.facebook.presto.sql.tree.QualifiedName; +import com.google.common.base.Preconditions; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Determines whether a given Expression is deterministic + */ +public final class DeterminismEvaluator +{ + private DeterminismEvaluator() {} + + public static boolean isDeterministic(Expression expression) + { + Preconditions.checkNotNull(expression, "expression is null"); + + AtomicBoolean deterministic = new AtomicBoolean(true); + new Visitor().process(expression, deterministic); + return deterministic.get(); + } + + private static class Visitor + extends DefaultExpressionTraversalVisitor + { + @Override + protected Void visitFunctionCall(FunctionCall node, AtomicBoolean deterministic) + { + // TODO: total hack to figure out if a function is deterministic. martint should fix this when he refactors the planning code + if (node.getName().equals(new QualifiedName("rand")) || node.getName().equals(new QualifiedName("random"))) { + deterministic.set(false); + } + return super.visitFunctionCall(node, deterministic); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/DistributedExecutionPlanner.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/DistributedExecutionPlanner.java new file mode 100644 index 00000000..3e51cdac --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/DistributedExecutionPlanner.java @@ -0,0 +1,280 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner; + +import com.facebook.presto.split.SampledSplitSource; +import com.facebook.presto.split.SplitManager; +import com.facebook.presto.split.SplitSource; +import com.facebook.presto.sql.planner.plan.AggregationNode; +import com.facebook.presto.sql.planner.plan.DeleteNode; +import com.facebook.presto.sql.planner.plan.DistinctLimitNode; +import com.facebook.presto.sql.planner.plan.FilterNode; +import com.facebook.presto.sql.planner.plan.IndexJoinNode; +import com.facebook.presto.sql.planner.plan.JoinNode; +import com.facebook.presto.sql.planner.plan.LimitNode; +import com.facebook.presto.sql.planner.plan.MarkDistinctNode; +import com.facebook.presto.sql.planner.plan.OutputNode; +import com.facebook.presto.sql.planner.plan.PlanNode; +import com.facebook.presto.sql.planner.plan.PlanVisitor; +import com.facebook.presto.sql.planner.plan.ProjectNode; +import com.facebook.presto.sql.planner.plan.RemoteSourceNode; +import com.facebook.presto.sql.planner.plan.RowNumberNode; +import com.facebook.presto.sql.planner.plan.SampleNode; +import com.facebook.presto.sql.planner.plan.SemiJoinNode; +import com.facebook.presto.sql.planner.plan.SortNode; +import com.facebook.presto.sql.planner.plan.TableCommitNode; +import com.facebook.presto.sql.planner.plan.TableScanNode; +import com.facebook.presto.sql.planner.plan.TableWriterNode; +import com.facebook.presto.sql.planner.plan.TopNNode; +import com.facebook.presto.sql.planner.plan.TopNRowNumberNode; +import com.facebook.presto.sql.planner.plan.UnionNode; +import com.facebook.presto.sql.planner.plan.UnnestNode; +import com.facebook.presto.sql.planner.plan.ValuesNode; +import com.facebook.presto.sql.planner.plan.WindowNode; +import com.google.common.collect.ImmutableList; + +import javax.inject.Inject; + +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class DistributedExecutionPlanner +{ + private final SplitManager splitManager; + + @Inject + public DistributedExecutionPlanner(SplitManager splitManager) + { + this.splitManager = checkNotNull(splitManager, "splitManager is null"); + } + + public StageExecutionPlan plan(SubPlan root) + { + PlanFragment currentFragment = root.getFragment(); + + // get splits for this fragment, this is lazy so split assignments aren't actually calculated here + Visitor visitor = new Visitor(); + Optional splits = currentFragment.getRoot().accept(visitor, null); + + // create child stages + ImmutableList.Builder dependencies = ImmutableList.builder(); + for (SubPlan childPlan : root.getChildren()) { + dependencies.add(plan(childPlan)); + } + + return new StageExecutionPlan(currentFragment, + splits, + dependencies.build() + ); + } + + private final class Visitor + extends PlanVisitor> + { + @Override + public Optional visitTableScan(TableScanNode node, Void context) + { + // get dataSource for table + SplitSource splitSource = splitManager.getSplits(node.getLayout().get()); + + return Optional.of(splitSource); + } + + @Override + public Optional visitJoin(JoinNode node, Void context) + { + Optional leftSplits = node.getLeft().accept(this, context); + Optional rightSplits = node.getRight().accept(this, context); + if (leftSplits.isPresent() && rightSplits.isPresent()) { + throw new IllegalArgumentException("Both left and right join nodes are partitioned"); // TODO: "partitioned" may not be the right term + } + return leftSplits.isPresent() ? leftSplits : rightSplits; + } + + @Override + public Optional visitSemiJoin(SemiJoinNode node, Void context) + { + Optional sourceSplits = node.getSource().accept(this, context); + Optional filteringSourceSplits = node.getFilteringSource().accept(this, context); + if (sourceSplits.isPresent() && filteringSourceSplits.isPresent()) { + throw new IllegalArgumentException("Both source and filteringSource semi join nodes are partitioned"); // TODO: "partitioned" may not be the right term + } + return sourceSplits.isPresent() ? sourceSplits : filteringSourceSplits; + } + + @Override + public Optional visitIndexJoin(IndexJoinNode node, Void context) + { + return node.getProbeSource().accept(this, context); + } + + @Override + public Optional visitRemoteSource(RemoteSourceNode node, Void context) + { + // remote source node does not have splits + return Optional.empty(); + } + + @Override + public Optional visitValues(ValuesNode node, Void context) + { + // values node does not have splits + return Optional.empty(); + } + + @Override + public Optional visitFilter(FilterNode node, Void context) + { + return node.getSource().accept(this, context); + } + + @Override + public Optional visitSample(SampleNode node, Void context) + { + switch (node.getSampleType()) { + case BERNOULLI: + case POISSONIZED: + return node.getSource().accept(this, context); + + case SYSTEM: + Optional nodeSplits = node.getSource().accept(this, context); + if (nodeSplits.isPresent()) { + SplitSource sampledSplitSource = new SampledSplitSource(nodeSplits.get(), node.getSampleRatio()); + return Optional.of(sampledSplitSource); + } + // table sampling on a sub query without splits is meaningless + return nodeSplits; + + default: + throw new UnsupportedOperationException("Sampling is not supported for type " + node.getSampleType()); + } + } + + @Override + public Optional visitAggregation(AggregationNode node, Void context) + { + return node.getSource().accept(this, context); + } + + @Override + public Optional visitMarkDistinct(MarkDistinctNode node, Void context) + { + return node.getSource().accept(this, context); + } + + @Override + public Optional visitWindow(WindowNode node, Void context) + { + return node.getSource().accept(this, context); + } + + @Override + public Optional visitRowNumber(RowNumberNode node, Void context) + { + return node.getSource().accept(this, context); + } + + @Override + public Optional visitTopNRowNumber(TopNRowNumberNode node, Void context) + { + return node.getSource().accept(this, context); + } + + @Override + public Optional visitProject(ProjectNode node, Void context) + { + return node.getSource().accept(this, context); + } + + @Override + public Optional visitUnnest(UnnestNode node, Void context) + { + return node.getSource().accept(this, context); + } + + @Override + public Optional visitTopN(TopNNode node, Void context) + { + return node.getSource().accept(this, context); + } + + @Override + public Optional visitOutput(OutputNode node, Void context) + { + return node.getSource().accept(this, context); + } + + @Override + public Optional visitLimit(LimitNode node, Void context) + { + return node.getSource().accept(this, context); + } + + @Override + public Optional visitDistinctLimit(DistinctLimitNode node, Void context) + { + return node.getSource().accept(this, context); + } + + @Override + public Optional visitSort(SortNode node, Void context) + { + return node.getSource().accept(this, context); + } + + @Override + public Optional visitTableWriter(TableWriterNode node, Void context) + { + return node.getSource().accept(this, context); + } + + @Override + public Optional visitTableCommit(TableCommitNode node, Void context) + { + return node.getSource().accept(this, context); + } + + @Override + public Optional visitDelete(DeleteNode node, Void context) + { + return node.getSource().accept(this, context); + } + + @Override + public Optional visitUnion(UnionNode node, Void context) + { + Optional result = Optional.empty(); + for (PlanNode child : node.getSources()) { + Optional source = child.accept(this, context); + + if (result.isPresent() && source.isPresent()) { + throw new IllegalArgumentException("Multiple children are source-distributed"); + } + + if (source.isPresent()) { + result = source; + } + } + + return result; + } + + @Override + protected Optional visitPlan(PlanNode node, Void context) + { + throw new UnsupportedOperationException("not yet implemented: " + node.getClass().getName()); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/DomainTranslator.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/DomainTranslator.java new file mode 100644 index 00000000..40bf5f2e --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/DomainTranslator.java @@ -0,0 +1,610 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner; + +import com.facebook.presto.Session; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.Domain; +import com.facebook.presto.spi.Marker; +import com.facebook.presto.spi.Range; +import com.facebook.presto.spi.SortedRangeSet; +import com.facebook.presto.spi.TupleDomain; +import com.facebook.presto.spi.type.DoubleType; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.tree.AstVisitor; +import com.facebook.presto.sql.tree.BetweenPredicate; +import com.facebook.presto.sql.tree.BooleanLiteral; +import com.facebook.presto.sql.tree.ComparisonExpression; +import com.facebook.presto.sql.tree.DoubleLiteral; +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.FunctionCall; +import com.facebook.presto.sql.tree.InListExpression; +import com.facebook.presto.sql.tree.InPredicate; +import com.facebook.presto.sql.tree.IsNotNullPredicate; +import com.facebook.presto.sql.tree.IsNullPredicate; +import com.facebook.presto.sql.tree.Literal; +import com.facebook.presto.sql.tree.LogicalBinaryExpression; +import com.facebook.presto.sql.tree.LongLiteral; +import com.facebook.presto.sql.tree.NotExpression; +import com.facebook.presto.sql.tree.NullLiteral; +import com.facebook.presto.sql.tree.QualifiedNameReference; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.math.DoubleMath; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static com.facebook.presto.metadata.FunctionRegistry.getMagicLiteralFunctionSignature; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.DateType.DATE; +import static com.facebook.presto.spi.type.TimestampType.TIMESTAMP; +import static com.facebook.presto.sql.ExpressionUtils.and; +import static com.facebook.presto.sql.ExpressionUtils.combineConjuncts; +import static com.facebook.presto.sql.ExpressionUtils.combineDisjunctsWithDefault; +import static com.facebook.presto.sql.ExpressionUtils.or; +import static com.facebook.presto.sql.planner.LiteralInterpreter.toExpression; +import static com.facebook.presto.sql.tree.BooleanLiteral.FALSE_LITERAL; +import static com.facebook.presto.sql.tree.BooleanLiteral.TRUE_LITERAL; +import static com.facebook.presto.sql.tree.ComparisonExpression.Type.EQUAL; +import static com.facebook.presto.sql.tree.ComparisonExpression.Type.GREATER_THAN; +import static com.facebook.presto.sql.tree.ComparisonExpression.Type.GREATER_THAN_OR_EQUAL; +import static com.facebook.presto.sql.tree.ComparisonExpression.Type.LESS_THAN; +import static com.facebook.presto.sql.tree.ComparisonExpression.Type.LESS_THAN_OR_EQUAL; +import static com.facebook.presto.sql.tree.ComparisonExpression.Type.NOT_EQUAL; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.Iterables.getOnlyElement; +import static com.google.common.primitives.Primitives.wrap; +import static java.math.RoundingMode.CEILING; +import static java.math.RoundingMode.FLOOR; + +public final class DomainTranslator +{ + private static final String DATE_LITERAL = getMagicLiteralFunctionSignature(DATE).getName(); + private static final String TIMESTAMP_LITERAL = getMagicLiteralFunctionSignature(TIMESTAMP).getName(); + + private DomainTranslator() + { + } + + public static Expression toPredicate(TupleDomain tupleDomain, Map symbolTypes) + { + if (tupleDomain.isNone()) { + return FALSE_LITERAL; + } + ImmutableList.Builder conjunctBuilder = ImmutableList.builder(); + for (Map.Entry entry : tupleDomain.getDomains().entrySet()) { + Symbol symbol = entry.getKey(); + QualifiedNameReference reference = new QualifiedNameReference(symbol.toQualifiedName()); + Type type = symbolTypes.get(symbol); + conjunctBuilder.add(toPredicate(entry.getValue(), reference, type)); + } + return combineConjuncts(conjunctBuilder.build()); + } + + private static Expression toPredicate(Domain domain, QualifiedNameReference reference, Type type) + { + if (domain.getRanges().isNone()) { + return domain.isNullAllowed() ? new IsNullPredicate(reference) : FALSE_LITERAL; + } + + if (domain.getRanges().isAll()) { + return domain.isNullAllowed() ? TRUE_LITERAL : new NotExpression(new IsNullPredicate(reference)); + } + + // Add disjuncts for ranges + List disjuncts = new ArrayList<>(); + List singleValues = new ArrayList<>(); + for (Range range : domain.getRanges()) { + checkState(!range.isAll()); // Already checked + if (range.isSingleValue()) { + singleValues.add(toExpression(range.getLow().getValue(), type)); + } + else if (isBetween(range)) { + // Specialize the range with BETWEEN expression if possible b/c it is currently more efficient + disjuncts.add(new BetweenPredicate(reference, toExpression(range.getLow().getValue(), type), toExpression(range.getHigh().getValue(), type))); + } + else { + List rangeConjuncts = new ArrayList<>(); + if (!range.getLow().isLowerUnbounded()) { + switch (range.getLow().getBound()) { + case ABOVE: + rangeConjuncts.add(new ComparisonExpression(GREATER_THAN, reference, toExpression(range.getLow().getValue(), type))); + break; + case EXACTLY: + rangeConjuncts.add(new ComparisonExpression(GREATER_THAN_OR_EQUAL, reference, toExpression(range.getLow().getValue(), + type))); + break; + case BELOW: + throw new IllegalStateException("Low Marker should never use BELOW bound: " + range); + default: + throw new AssertionError("Unhandled bound: " + range.getLow().getBound()); + } + } + if (!range.getHigh().isUpperUnbounded()) { + switch (range.getHigh().getBound()) { + case ABOVE: + throw new IllegalStateException("High Marker should never use ABOVE bound: " + range); + case EXACTLY: + rangeConjuncts.add(new ComparisonExpression(LESS_THAN_OR_EQUAL, reference, toExpression(range.getHigh().getValue(), type))); + break; + case BELOW: + rangeConjuncts.add(new ComparisonExpression(LESS_THAN, reference, toExpression(range.getHigh().getValue(), type))); + break; + default: + throw new AssertionError("Unhandled bound: " + range.getHigh().getBound()); + } + } + // If rangeConjuncts is null, then the range was ALL, which should already have been checked for + checkState(!rangeConjuncts.isEmpty()); + disjuncts.add(combineConjuncts(rangeConjuncts)); + } + } + + // Add back all of the possible single values either as an equality or an IN predicate + if (singleValues.size() == 1) { + disjuncts.add(new ComparisonExpression(EQUAL, reference, getOnlyElement(singleValues))); + } + else if (singleValues.size() > 1) { + disjuncts.add(new InPredicate(reference, new InListExpression(singleValues))); + } + + // Add nullability disjuncts + checkState(!disjuncts.isEmpty()); + if (domain.isNullAllowed()) { + disjuncts.add(new IsNullPredicate(reference)); + } + return combineDisjunctsWithDefault(disjuncts, TRUE_LITERAL); + } + + private static boolean isBetween(Range range) + { + return !range.getLow().isLowerUnbounded() && range.getLow().getBound() == Marker.Bound.EXACTLY + && !range.getHigh().isUpperUnbounded() && range.getHigh().getBound() == Marker.Bound.EXACTLY; + } + + /** + * Convert an Expression predicate into an ExtractionResult consisting of: + * 1) A successfully extracted TupleDomain + * 2) An Expression fragment which represents the part of the original Expression that will need to be re-evaluated + * after filtering with the TupleDomain. + */ + public static ExtractionResult fromPredicate( + Metadata metadata, + Session session, + Expression predicate, + Map types) + { + return new Visitor(metadata, session, types).process(predicate, false); + } + + private static class Visitor + extends AstVisitor + { + private final Metadata metadata; + private final ConnectorSession session; + private final Map types; + + private Visitor(Metadata metadata, Session session, Map types) + { + this.metadata = checkNotNull(metadata, "metadata is null"); + this.session = checkNotNull(session, "session is null").toConnectorSession(); + this.types = ImmutableMap.copyOf(checkNotNull(types, "types is null")); + } + + private Type checkedTypeLookup(Symbol symbol) + { + Type type = types.get(symbol); + checkArgument(type != null, "Types is missing info for symbol: %s", symbol); + return type; + } + + private static SortedRangeSet complementIfNecessary(SortedRangeSet range, boolean complement) + { + return complement ? range.complement() : range; + } + + private static Domain complementIfNecessary(Domain domain, boolean complement) + { + return complement ? domain.complement() : domain; + } + + private static Expression complementIfNecessary(Expression expression, boolean complement) + { + return complement ? new NotExpression(expression) : expression; + } + + @Override + protected ExtractionResult visitExpression(Expression node, Boolean complement) + { + // If we don't know how to process this node, the default response is to say that the TupleDomain is "all" + return new ExtractionResult(TupleDomain.all(), complementIfNecessary(node, complement)); + } + + @Override + protected ExtractionResult visitLogicalBinaryExpression(LogicalBinaryExpression node, Boolean complement) + { + ExtractionResult leftResult = process(node.getLeft(), complement); + ExtractionResult rightResult = process(node.getRight(), complement); + + LogicalBinaryExpression.Type type = complement ? flipLogicalBinaryType(node.getType()) : node.getType(); + switch (type) { + case AND: + return new ExtractionResult( + leftResult.getTupleDomain().intersect(rightResult.getTupleDomain()), + combineConjuncts(leftResult.getRemainingExpression(), rightResult.getRemainingExpression())); + + case OR: + TupleDomain columnUnionedTupleDomain = TupleDomain.columnWiseUnion(leftResult.getTupleDomain(), rightResult.getTupleDomain()); + + // In most cases, the columnUnionedTupleDomain is only a superset of the actual strict union + // and so we can return the current node as the remainingExpression so that all bounds will be double checked again at execution time. + Expression remainingExpression = complementIfNecessary(node, complement); + + // However, there are a few cases where the column-wise union is actually equivalent to the strict union, so we if can detect + // some of these cases, we won't have to double check the bounds unnecessarily at execution time. + + // We can only make inferences if the remaining expressions on both side are equal and deterministic + if (leftResult.getRemainingExpression().equals(rightResult.getRemainingExpression()) && + DeterminismEvaluator.isDeterministic(leftResult.getRemainingExpression())) { + // The column-wise union is equivalent to the strict union if + // 1) If both TupleDomains consist of the same exact single column (e.g. left TupleDomain => (a > 0), right TupleDomain => (a < 10)) + // 2) If one TupleDomain is a superset of the other (e.g. left TupleDomain => (a > 0, b > 0 && b < 10), right TupleDomain => (a > 5, b = 5)) + boolean matchingSingleSymbolDomains = !leftResult.getTupleDomain().isNone() + && !rightResult.getTupleDomain().isNone() + && leftResult.getTupleDomain().getDomains().size() == 1 + && rightResult.getTupleDomain().getDomains().size() == 1 + && leftResult.getTupleDomain().getDomains().keySet().equals(rightResult.getTupleDomain().getDomains().keySet()); + boolean oneSideIsSuperSet = leftResult.getTupleDomain().contains(rightResult.getTupleDomain()) || rightResult.getTupleDomain().contains(leftResult.getTupleDomain()); + + if (matchingSingleSymbolDomains || oneSideIsSuperSet) { + remainingExpression = leftResult.getRemainingExpression(); + } + } + + return new ExtractionResult(columnUnionedTupleDomain, remainingExpression); + + default: + throw new AssertionError("Unknown type: " + node.getType()); + } + } + + private static LogicalBinaryExpression.Type flipLogicalBinaryType(LogicalBinaryExpression.Type type) + { + switch (type) { + case AND: + return LogicalBinaryExpression.Type.OR; + case OR: + return LogicalBinaryExpression.Type.AND; + default: + throw new AssertionError("Unknown type: " + type); + } + } + + @Override + protected ExtractionResult visitNotExpression(NotExpression node, Boolean complement) + { + return process(node.getValue(), !complement); + } + + @Override + protected ExtractionResult visitComparisonExpression(ComparisonExpression node, Boolean complement) + { + if (isSimpleMagicLiteralComparison(node)) { + node = normalizeSimpleComparison(node); + node = convertMagicLiteralComparison(node); + } + else if (isSimpleComparison(node)) { + node = normalizeSimpleComparison(node); + } + else { + return super.visitComparisonExpression(node, complement); + } + + Symbol symbol = Symbol.fromQualifiedName(((QualifiedNameReference) node.getLeft()).getName()); + Type columnType = checkedTypeLookup(symbol); + Object value = LiteralInterpreter.evaluate(metadata, session, node.getRight()); + + // Handle the cases where implicit coercions can happen in comparisons + // TODO: how to abstract this out + if (value instanceof Double && columnType.equals(BIGINT)) { + return process(coerceDoubleToLongComparison(node), complement); + } + if (value instanceof Long && columnType.equals(DoubleType.DOUBLE)) { + value = ((Long) value).doubleValue(); + } + verifyType(columnType, value); + return createComparisonExtractionResult(node.getType(), symbol, columnType, objectToComparable(value), complement); + } + + private ExtractionResult createComparisonExtractionResult(ComparisonExpression.Type comparisonType, Symbol column, Type columnType, Comparable value, boolean complement) + { + if (value == null) { + switch (comparisonType) { + case EQUAL: + case GREATER_THAN: + case GREATER_THAN_OR_EQUAL: + case LESS_THAN: + case LESS_THAN_OR_EQUAL: + case NOT_EQUAL: + return new ExtractionResult(TupleDomain.none(), TRUE_LITERAL); + + case IS_DISTINCT_FROM: + Domain domain = complementIfNecessary(Domain.notNull(wrap(columnType.getJavaType())), complement); + return new ExtractionResult( + TupleDomain.withColumnDomains(ImmutableMap.of(column, domain)), + TRUE_LITERAL); + + default: + throw new AssertionError("Unhandled type: " + comparisonType); + } + } + + Domain domain; + switch (comparisonType) { + case EQUAL: + domain = Domain.create(complementIfNecessary(SortedRangeSet.of(Range.equal(value)), complement), false); + break; + case GREATER_THAN: + domain = Domain.create(complementIfNecessary(SortedRangeSet.of(Range.greaterThan(value)), complement), false); + break; + case GREATER_THAN_OR_EQUAL: + domain = Domain.create(complementIfNecessary(SortedRangeSet.of(Range.greaterThanOrEqual(value)), complement), false); + break; + case LESS_THAN: + domain = Domain.create(complementIfNecessary(SortedRangeSet.of(Range.lessThan(value)), complement), false); + break; + case LESS_THAN_OR_EQUAL: + domain = Domain.create(complementIfNecessary(SortedRangeSet.of(Range.lessThanOrEqual(value)), complement), false); + break; + case NOT_EQUAL: + domain = Domain.create(complementIfNecessary(SortedRangeSet.of(Range.lessThan(value), Range.greaterThan(value)), complement), false); + break; + case IS_DISTINCT_FROM: + // Need to potential complement the whole domain for IS_DISTINCT_FROM since it is null-aware + domain = complementIfNecessary(Domain.create(SortedRangeSet.of(Range.lessThan(value), Range.greaterThan(value)), true), complement); + break; + default: + throw new AssertionError("Unhandled type: " + comparisonType); + } + + return new ExtractionResult( + TupleDomain.withColumnDomains(ImmutableMap.of(column, domain)), + TRUE_LITERAL); + } + + private static void verifyType(Type type, Object value) + { + checkState(value == null || wrap(type.getJavaType()).isInstance(value), "Value %s is not of expected type %s", value, type); + } + + private static Comparable objectToComparable(Object value) + { + return (Comparable) value; + } + + @Override + protected ExtractionResult visitInPredicate(InPredicate node, Boolean complement) + { + if (!(node.getValue() instanceof QualifiedNameReference) || !(node.getValueList() instanceof InListExpression)) { + return super.visitInPredicate(node, complement); + } + + InListExpression valueList = (InListExpression) node.getValueList(); + checkState(!valueList.getValues().isEmpty(), "InListExpression should never be empty"); + + ImmutableList.Builder disjuncts = ImmutableList.builder(); + for (Expression expression : valueList.getValues()) { + disjuncts.add(new ComparisonExpression(EQUAL, node.getValue(), expression)); + } + return process(or(disjuncts.build()), complement); + } + + @Override + protected ExtractionResult visitBetweenPredicate(BetweenPredicate node, Boolean complement) + { + // Re-write as two comparison expressions + return process(and( + new ComparisonExpression(GREATER_THAN_OR_EQUAL, node.getValue(), node.getMin()), + new ComparisonExpression(LESS_THAN_OR_EQUAL, node.getValue(), node.getMax())), complement); + } + + @Override + protected ExtractionResult visitIsNullPredicate(IsNullPredicate node, Boolean complement) + { + if (!(node.getValue() instanceof QualifiedNameReference)) { + return super.visitIsNullPredicate(node, complement); + } + + Symbol symbol = Symbol.fromQualifiedName(((QualifiedNameReference) node.getValue()).getName()); + Type columnType = checkedTypeLookup(symbol); + Domain domain = complementIfNecessary(Domain.onlyNull(wrap(columnType.getJavaType())), complement); + return new ExtractionResult( + TupleDomain.withColumnDomains(ImmutableMap.of(symbol, domain)), + TRUE_LITERAL); + } + + @Override + protected ExtractionResult visitIsNotNullPredicate(IsNotNullPredicate node, Boolean complement) + { + if (!(node.getValue() instanceof QualifiedNameReference)) { + return super.visitIsNotNullPredicate(node, complement); + } + + Symbol symbol = Symbol.fromQualifiedName(((QualifiedNameReference) node.getValue()).getName()); + Type columnType = checkedTypeLookup(symbol); + + Domain domain = complementIfNecessary(Domain.notNull(wrap(columnType.getJavaType())), complement); + return new ExtractionResult( + TupleDomain.withColumnDomains(ImmutableMap.of(symbol, domain)), + TRUE_LITERAL); + } + + @Override + protected ExtractionResult visitBooleanLiteral(BooleanLiteral node, Boolean complement) + { + boolean value = complement ? !node.getValue() : node.getValue(); + return new ExtractionResult(value ? TupleDomain.all() : TupleDomain.none(), TRUE_LITERAL); + } + + @Override + protected ExtractionResult visitNullLiteral(NullLiteral node, Boolean complement) + { + return new ExtractionResult(TupleDomain.none(), TRUE_LITERAL); + } + } + + private static boolean isSimpleComparison(ComparisonExpression comparison) + { + return (comparison.getLeft() instanceof QualifiedNameReference && comparison.getRight() instanceof Literal) || + (comparison.getLeft() instanceof Literal && comparison.getRight() instanceof QualifiedNameReference); + } + + /** + * Normalize a simple comparison between a QualifiedNameReference and a Literal such that the QualifiedNameReference will always be on the left and the Literal on the right. + */ + private static ComparisonExpression normalizeSimpleComparison(ComparisonExpression comparison) + { + if (comparison.getLeft() instanceof QualifiedNameReference) { + return comparison; + } + if (comparison.getRight() instanceof QualifiedNameReference) { + return new ComparisonExpression(flipComparisonDirection(comparison.getType()), comparison.getRight(), comparison.getLeft()); + } + throw new IllegalArgumentException("ComparisonExpression not a simple literal comparison: " + comparison); + } + + private static ComparisonExpression.Type flipComparisonDirection(ComparisonExpression.Type type) + { + switch (type) { + case LESS_THAN_OR_EQUAL: + return GREATER_THAN_OR_EQUAL; + case LESS_THAN: + return GREATER_THAN; + case GREATER_THAN_OR_EQUAL: + return LESS_THAN_OR_EQUAL; + case GREATER_THAN: + return LESS_THAN; + default: + // The remaining types have no direction association + return type; + } + } + + private static Expression coerceDoubleToLongComparison(ComparisonExpression comparison) + { + comparison = normalizeSimpleComparison(comparison); + + checkArgument(comparison.getLeft() instanceof QualifiedNameReference, "Left must be a QualifiedNameReference"); + checkArgument(comparison.getRight() instanceof DoubleLiteral, "Right must be a DoubleLiteral"); + + QualifiedNameReference reference = (QualifiedNameReference) comparison.getLeft(); + Double value = ((DoubleLiteral) comparison.getRight()).getValue(); + + switch (comparison.getType()) { + case GREATER_THAN_OR_EQUAL: + case LESS_THAN: + return new ComparisonExpression(comparison.getType(), reference, toExpression(DoubleMath.roundToLong(value, CEILING), BIGINT)); + + case GREATER_THAN: + case LESS_THAN_OR_EQUAL: + return new ComparisonExpression(comparison.getType(), reference, toExpression(DoubleMath.roundToLong(value, FLOOR), BIGINT)); + + case EQUAL: + Long equalValue = DoubleMath.roundToLong(value, FLOOR); + if (equalValue.doubleValue() != value) { + // Return something that is false for all non-null values + return and(new ComparisonExpression(EQUAL, reference, new LongLiteral("0")), + new ComparisonExpression(NOT_EQUAL, reference, new LongLiteral("0"))); + } + return new ComparisonExpression(comparison.getType(), reference, toExpression(equalValue, BIGINT)); + + case NOT_EQUAL: + Long notEqualValue = DoubleMath.roundToLong(value, FLOOR); + if (notEqualValue.doubleValue() != value) { + // Return something that is true for all non-null values + return or(new ComparisonExpression(EQUAL, reference, new LongLiteral("0")), + new ComparisonExpression(NOT_EQUAL, reference, new LongLiteral("0"))); + } + return new ComparisonExpression(comparison.getType(), reference, toExpression(notEqualValue, BIGINT)); + + case IS_DISTINCT_FROM: + Long distinctValue = DoubleMath.roundToLong(value, FLOOR); + if (distinctValue.doubleValue() != value) { + return TRUE_LITERAL; + } + return new ComparisonExpression(comparison.getType(), reference, toExpression(distinctValue, BIGINT)); + + default: + throw new AssertionError("Unhandled type: " + comparison.getType()); + } + } + + public static class ExtractionResult + { + private final TupleDomain tupleDomain; + private final Expression remainingExpression; + + public ExtractionResult(TupleDomain tupleDomain, Expression remainingExpression) + { + this.tupleDomain = checkNotNull(tupleDomain, "tupleDomain is null"); + this.remainingExpression = checkNotNull(remainingExpression, "remainingExpression is null"); + } + + public TupleDomain getTupleDomain() + { + return tupleDomain; + } + + public Expression getRemainingExpression() + { + return remainingExpression; + } + } + + // TODO: remove this horrible hack + private static boolean isSimpleMagicLiteralComparison(ComparisonExpression node) + { + FunctionCall call; + if ((node.getLeft() instanceof QualifiedNameReference) && (node.getRight() instanceof FunctionCall)) { + call = (FunctionCall) node.getRight(); + } + else if ((node.getLeft() instanceof FunctionCall) && (node.getRight() instanceof QualifiedNameReference)) { + call = (FunctionCall) node.getLeft(); + } + else { + return false; + } + + if (call.getName().getPrefix().isPresent()) { + return false; + } + String name = call.getName().getSuffix(); + + return name.equals(DATE_LITERAL) || name.equals(TIMESTAMP_LITERAL); + } + + private static ComparisonExpression convertMagicLiteralComparison(ComparisonExpression node) + { + // "magic literal" functions use the stack type value for the argument + checkArgument(isSimpleMagicLiteralComparison(node), "not a simple magic literal comparison"); + FunctionCall call = (FunctionCall) node.getRight(); + Expression value = call.getArguments().get(0); + return new ComparisonExpression(node.getType(), node.getLeft(), value); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/DomainUtils.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/DomainUtils.java new file mode 100644 index 00000000..aac2917a --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/DomainUtils.java @@ -0,0 +1,52 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner; + +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.Domain; +import com.facebook.presto.spi.SortedRangeSet; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableBiMap; +import com.google.common.collect.ImmutableMap; + +import java.util.Map; + +public final class DomainUtils +{ + private DomainUtils() + { + } + + public static Map columnHandleToSymbol(Map columnMap, Map assignments) + { + Map inverseAssignments = ImmutableBiMap.copyOf(assignments).inverse(); + Preconditions.checkArgument(inverseAssignments.keySet().containsAll(columnMap.keySet()), "assignments does not contain all required column handles"); + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (Map.Entry entry : columnMap.entrySet()) { + builder.put(inverseAssignments.get(entry.getKey()), entry.getValue()); + } + return builder.build(); + } + + /** + * Reduces the number of discrete ranges in the Domain if there are too many. + */ + public static Domain simplifyDomain(Domain domain) + { + if (domain.getRanges().getRangeCount() <= 32) { + return domain; + } + return Domain.create(SortedRangeSet.of(domain.getRanges().getSpan()), domain.isNullAllowed()); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/EffectivePredicateExtractor.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/EffectivePredicateExtractor.java new file mode 100644 index 00000000..3be2c315 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/EffectivePredicateExtractor.java @@ -0,0 +1,322 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner; + +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.Domain; +import com.facebook.presto.spi.SortedRangeSet; +import com.facebook.presto.spi.TupleDomain; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.planner.plan.AggregationNode; +import com.facebook.presto.sql.planner.plan.DistinctLimitNode; +import com.facebook.presto.sql.planner.plan.ExchangeNode; +import com.facebook.presto.sql.planner.plan.FilterNode; +import com.facebook.presto.sql.planner.plan.JoinNode; +import com.facebook.presto.sql.planner.plan.LimitNode; +import com.facebook.presto.sql.planner.plan.PlanNode; +import com.facebook.presto.sql.planner.plan.PlanVisitor; +import com.facebook.presto.sql.planner.plan.ProjectNode; +import com.facebook.presto.sql.planner.plan.SemiJoinNode; +import com.facebook.presto.sql.planner.plan.SortNode; +import com.facebook.presto.sql.planner.plan.TableScanNode; +import com.facebook.presto.sql.planner.plan.TopNNode; +import com.facebook.presto.sql.planner.plan.UnionNode; +import com.facebook.presto.sql.planner.plan.WindowNode; +import com.facebook.presto.sql.tree.BooleanLiteral; +import com.facebook.presto.sql.tree.ComparisonExpression; +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.QualifiedNameReference; +import com.google.common.base.Function; +import com.google.common.base.Predicate; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableBiMap; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.facebook.presto.sql.ExpressionUtils.combineConjuncts; +import static com.facebook.presto.sql.ExpressionUtils.expressionOrNullSymbols; +import static com.facebook.presto.sql.ExpressionUtils.extractConjuncts; +import static com.facebook.presto.sql.ExpressionUtils.stripNonDeterministicConjuncts; +import static com.facebook.presto.sql.planner.EqualityInference.createEqualityInference; +import static com.google.common.base.Predicates.in; +import static com.google.common.base.Predicates.not; +import static com.google.common.collect.Iterables.transform; + +/** + * Computes the effective predicate at the top of the specified PlanNode + *

+ * Note: non-deterministic predicates can not be pulled up (so they will be ignored) + */ +public class EffectivePredicateExtractor + extends PlanVisitor +{ + public static Expression extract(PlanNode node, Map symbolTypes) + { + return node.accept(new EffectivePredicateExtractor(symbolTypes), null); + } + + private final Map symbolTypes; + + public EffectivePredicateExtractor(Map symbolTypes) + { + this.symbolTypes = symbolTypes; + } + + @Override + protected Expression visitPlan(PlanNode node, Void context) + { + return BooleanLiteral.TRUE_LITERAL; + } + + @Override + public Expression visitAggregation(AggregationNode node, Void context) + { + Expression underlyingPredicate = node.getSource().accept(this, context); + + return pullExpressionThroughSymbols(underlyingPredicate, node.getGroupBy()); + } + + @Override + public Expression visitFilter(FilterNode node, Void context) + { + Expression underlyingPredicate = node.getSource().accept(this, context); + + Expression predicate = node.getPredicate(); + + // Remove non-deterministic conjuncts + predicate = stripNonDeterministicConjuncts(predicate); + + return combineConjuncts(predicate, underlyingPredicate); + } + + private static Predicate> symbolMatchesExpression() + { + return entry -> entry.getValue().equals(new QualifiedNameReference(entry.getKey().toQualifiedName())); + } + + private static Function, Expression> entryToEquality() + { + return entry -> { + QualifiedNameReference reference = new QualifiedNameReference(entry.getKey().toQualifiedName()); + Expression expression = entry.getValue(); + // TODO: switch this to 'IS NOT DISTINCT FROM' syntax when EqualityInference properly supports it + return new ComparisonExpression(ComparisonExpression.Type.EQUAL, reference, expression); + }; + } + + @Override + public Expression visitExchange(ExchangeNode node, Void context) + { + return deriveCommonPredicates(node, source -> { + Map mappings = new HashMap<>(); + for (int i = 0; i < node.getInputs().get(source).size(); i++) { + mappings.put( + node.getOutputSymbols().get(i), + node.getInputs().get(source).get(i).toQualifiedNameReference()); + } + return mappings.entrySet(); + }); + } + + @Override + public Expression visitProject(ProjectNode node, Void context) + { + // TODO: add simple algebraic solver for projection translation (right now only considers identity projections) + + Expression underlyingPredicate = node.getSource().accept(this, context); + + Iterable projectionEqualities = FluentIterable.from(node.getAssignments().entrySet()) + .filter(not(symbolMatchesExpression())) + .transform(entryToEquality()); + + return pullExpressionThroughSymbols(combineConjuncts( + ImmutableList.builder() + .addAll(projectionEqualities) + .add(underlyingPredicate) + .build()), + node.getOutputSymbols()); + } + + @Override + public Expression visitTopN(TopNNode node, Void context) + { + return node.getSource().accept(this, context); + } + + @Override + public Expression visitLimit(LimitNode node, Void context) + { + return node.getSource().accept(this, context); + } + + @Override + public Expression visitDistinctLimit(DistinctLimitNode node, Void context) + { + return node.getSource().accept(this, context); + } + + @Override + public Expression visitTableScan(TableScanNode node, Void context) + { + Map assignments = ImmutableBiMap.copyOf(node.getAssignments()).inverse(); + return DomainTranslator.toPredicate( + spanTupleDomain(node.getCurrentConstraint()).transform(assignments::get), + symbolTypes); + } + + private static TupleDomain spanTupleDomain(TupleDomain tupleDomain) + { + if (tupleDomain.isNone()) { + return tupleDomain; + } + + // Retain nullability, but collapse each SortedRangeSet into a single span + Map spannedDomains = Maps.transformValues(tupleDomain.getDomains(), domain -> Domain.create(getSortedRangeSpan(domain.getRanges()), domain.isNullAllowed())); + + return TupleDomain.withColumnDomains(spannedDomains); + } + + private static SortedRangeSet getSortedRangeSpan(SortedRangeSet rangeSet) + { + return rangeSet.isNone() ? SortedRangeSet.none(rangeSet.getType()) : SortedRangeSet.of(rangeSet.getSpan()); + } + + @Override + public Expression visitSort(SortNode node, Void context) + { + return node.getSource().accept(this, context); + } + + @Override + public Expression visitWindow(WindowNode node, Void context) + { + return node.getSource().accept(this, context); + } + + @Override + public Expression visitUnion(UnionNode node, Void context) + { + return deriveCommonPredicates(node, source -> node.outputSymbolMap(source).entries()); + } + + @Override + public Expression visitJoin(JoinNode node, Void context) + { + Expression leftPredicate = node.getLeft().accept(this, context); + Expression rightPredicate = node.getRight().accept(this, context); + + List joinConjuncts = new ArrayList<>(); + for (JoinNode.EquiJoinClause clause : node.getCriteria()) { + joinConjuncts.add(new ComparisonExpression(ComparisonExpression.Type.EQUAL, + new QualifiedNameReference(clause.getLeft().toQualifiedName()), + new QualifiedNameReference(clause.getRight().toQualifiedName()))); + } + + switch (node.getType()) { + case INNER: + case CROSS: + return combineConjuncts(ImmutableList.builder() + .add(leftPredicate) + .add(rightPredicate) + .addAll(joinConjuncts) + .build()); + case LEFT: + return combineConjuncts(ImmutableList.builder() + .add(leftPredicate) + .addAll(transform(extractConjuncts(rightPredicate), expressionOrNullSymbols(in(node.getRight().getOutputSymbols())))) + .addAll(transform(joinConjuncts, expressionOrNullSymbols(in(node.getRight().getOutputSymbols())))) + .build()); + case RIGHT: + return combineConjuncts(ImmutableList.builder() + .add(rightPredicate) + .addAll(transform(extractConjuncts(leftPredicate), expressionOrNullSymbols(in(node.getLeft().getOutputSymbols())))) + .addAll(transform(joinConjuncts, expressionOrNullSymbols(in(node.getLeft().getOutputSymbols())))) + .build()); + case FULL: + return combineConjuncts(ImmutableList.builder() + .addAll(transform(extractConjuncts(leftPredicate), expressionOrNullSymbols(in(node.getLeft().getOutputSymbols())))) + .addAll(transform(extractConjuncts(rightPredicate), expressionOrNullSymbols(in(node.getRight().getOutputSymbols())))) + .addAll(transform(joinConjuncts, expressionOrNullSymbols(in(node.getLeft().getOutputSymbols()), in(node.getRight().getOutputSymbols())))) + .build()); + default: + throw new UnsupportedOperationException("Unknown join type: " + node.getType()); + } + } + + @Override + public Expression visitSemiJoin(SemiJoinNode node, Void context) + { + // Filtering source does not change the effective predicate over the output symbols + return node.getSource().accept(this, context); + } + + private Expression deriveCommonPredicates(PlanNode node, Function>> mapping) + { + // Find the predicates that can be pulled up from each source + List> sourceOutputConjuncts = new ArrayList<>(); + for (int i = 0; i < node.getSources().size(); i++) { + Expression underlyingPredicate = node.getSources().get(i).accept(this, null); + + Iterable equalities = FluentIterable.from(mapping.apply(i)) + .filter(not(symbolMatchesExpression())) + .transform(entryToEquality()); + + sourceOutputConjuncts.add(ImmutableSet.copyOf(extractConjuncts(pullExpressionThroughSymbols(combineConjuncts( + ImmutableList.builder() + .addAll(equalities) + .add(underlyingPredicate) + .build()), + node.getOutputSymbols())))); + } + + // Find the intersection of predicates across all sources + // TODO: use a more precise way to determine overlapping conjuncts (e.g. commutative predicates) + Iterator> iterator = sourceOutputConjuncts.iterator(); + Set potentialOutputConjuncts = iterator.next(); + while (iterator.hasNext()) { + potentialOutputConjuncts = Sets.intersection(potentialOutputConjuncts, iterator.next()); + } + + return combineConjuncts(potentialOutputConjuncts); + } + + private static Expression pullExpressionThroughSymbols(Expression expression, Collection symbols) + { + EqualityInference equalityInference = createEqualityInference(expression); + + ImmutableList.Builder effectiveConjuncts = ImmutableList.builder(); + for (Expression conjunct : EqualityInference.nonInferrableConjuncts(expression)) { + if (DeterminismEvaluator.isDeterministic(conjunct)) { + Expression rewritten = equalityInference.rewriteExpression(conjunct, in(symbols)); + if (rewritten != null) { + effectiveConjuncts.add(rewritten); + } + } + } + + effectiveConjuncts.addAll(equalityInference.generateEqualitiesPartitionedBy(in(symbols)).getScopeEqualities()); + + return combineConjuncts(effectiveConjuncts.build()); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/EqualityInference.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/EqualityInference.java new file mode 100644 index 00000000..6df769fb --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/EqualityInference.java @@ -0,0 +1,409 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner; + +import com.facebook.presto.sql.tree.ComparisonExpression; +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.ExpressionTreeRewriter; +import com.facebook.presto.sql.tree.InListExpression; +import com.facebook.presto.sql.tree.InPredicate; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; +import com.google.common.collect.ComparisonChain; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSetMultimap; +import com.google.common.collect.Iterables; +import com.google.common.collect.Multimap; +import com.google.common.collect.Ordering; +import com.google.common.collect.SetMultimap; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.facebook.presto.sql.ExpressionUtils.extractConjuncts; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Predicates.equalTo; +import static com.google.common.base.Predicates.not; +import static com.google.common.collect.Iterables.filter; + +/** + * Makes equality based inferences to rewrite Expressions and generate equality sets in terms of specified symbol scopes + */ +public class EqualityInference +{ + // Ordering used to determine Expression preference when determining canonicals + private static final Ordering CANONICAL_ORDERING = Ordering.from(new Comparator() + { + @Override + public int compare(Expression expression1, Expression expression2) + { + // Current cost heuristic: + // 1) Prefer fewer input symbols + // 2) Prefer smaller expression trees + // 3) Ordering.arbitrary() - creates a stable consistent ordering (extremely useful for unit testing) + // TODO: be more precise in determining the cost of an expression + return ComparisonChain.start() + .compare(DependencyExtractor.extractAll(expression1).size(), DependencyExtractor.extractAll(expression2).size()) + .compare(SubExpressionExtractor.extract(expression1).size(), SubExpressionExtractor.extract(expression2).size()) + .compare(expression1, expression2, Ordering.arbitrary()) + .result(); + } + }); + + private final SetMultimap equalitySets; // Indexed by canonical expression + private final Map canonicalMap; // Map each known expression to canonical expression + + private EqualityInference(Iterable> equalityGroups) + { + ImmutableSetMultimap.Builder setBuilder = ImmutableSetMultimap.builder(); + for (Set equalityGroup : equalityGroups) { + if (!equalityGroup.isEmpty()) { + setBuilder.putAll(CANONICAL_ORDERING.min(equalityGroup), equalityGroup); + } + } + equalitySets = setBuilder.build(); + + ImmutableMap.Builder mapBuilder = ImmutableMap.builder(); + for (Map.Entry entry : equalitySets.entries()) { + Expression canonical = entry.getKey(); + Expression expression = entry.getValue(); + mapBuilder.put(expression, canonical); + } + canonicalMap = mapBuilder.build(); + } + + /** + * Attempts to rewrite an Expression in terms of the symbols allowed by the symbol scope + * given the known equalities. Returns null if unsuccessful. + */ + public Expression rewriteExpression(Expression expression, Predicate symbolScope) + { + checkArgument(DeterminismEvaluator.isDeterministic(expression), "Only deterministic expressions may be considered for rewrite"); + return rewriteExpression(expression, symbolScope, true); + } + + private Expression rewriteExpression(Expression expression, Predicate symbolScope, boolean allowFullReplacement) + { + Iterable subExpressions = SubExpressionExtractor.extract(expression); + if (!allowFullReplacement) { + subExpressions = filter(subExpressions, not(equalTo(expression))); + } + + ImmutableMap.Builder expressionRemap = ImmutableMap.builder(); + for (Expression subExpression : subExpressions) { + Expression canonical = getScopedCanonical(subExpression, symbolScope); + if (canonical != null) { + expressionRemap.put(subExpression, canonical); + } + } + + // Perform a naive single-pass traversal to try to rewrite non-compliant portions of the tree. Prefers to replace + // larger subtrees over smaller subtrees + // TODO: this rewrite can probably be made more sophisticated + Expression rewritten = ExpressionTreeRewriter.rewriteWith(new ExpressionNodeInliner(expressionRemap.build()), expression); + if (!symbolToExpressionPredicate(symbolScope).apply(rewritten)) { + // If the rewritten is still not compliant with the symbol scope, just give up + return null; + } + return rewritten; + } + + /** + * Dumps the inference equalities as equality expressions that are partitioned by the symbolScope. + * All stored equalities are returned in a compact set and will be classified into three groups as determined by the symbol scope: + *

    + *
  1. equalities that fit entirely within the symbol scope
  2. + *
  3. equalities that fit entirely outside of the symbol scope
  4. + *
  5. equalities that straddle the symbol scope
  6. + *
+ *
+     * Example:
+     *   Stored Equalities:
+     *     a = b = c
+     *     d = e = f = g
+     *
+     *   Symbol Scope:
+     *     a, b, d, e
+     *
+     *   Output EqualityPartition:
+     *     Scope Equalities:
+     *       a = b
+     *       d = e
+     *     Complement Scope Equalities
+     *       f = g
+     *     Scope Straddling Equalities
+     *       a = c
+     *       d = f
+     * 
+ */ + public EqualityPartition generateEqualitiesPartitionedBy(Predicate symbolScope) + { + Set scopeEqualities = new HashSet<>(); + Set scopeComplementEqualities = new HashSet<>(); + Set scopeStraddlingEqualities = new HashSet<>(); + + for (Collection equalitySet : equalitySets.asMap().values()) { + Set scopeExpressions = new HashSet<>(); + Set scopeComplementExpressions = new HashSet<>(); + Set scopeStraddlingExpressions = new HashSet<>(); + + // Try to push each expression into one side of the scope + for (Expression expression : equalitySet) { + Expression scopeRewritten = rewriteExpression(expression, symbolScope, false); + if (scopeRewritten != null) { + scopeExpressions.add(scopeRewritten); + } + Expression scopeComplementRewritten = rewriteExpression(expression, not(symbolScope), false); + if (scopeComplementRewritten != null) { + scopeComplementExpressions.add(scopeComplementRewritten); + } + if (scopeRewritten == null && scopeComplementRewritten == null) { + scopeStraddlingExpressions.add(expression); + } + } + + // Compile the equality expressions on each side of the scope + Expression matchingCanonical = getCanonical(scopeExpressions); + if (scopeExpressions.size() >= 2) { + for (Expression expression : filter(scopeExpressions, not(equalTo(matchingCanonical)))) { + scopeEqualities.add(new ComparisonExpression(ComparisonExpression.Type.EQUAL, matchingCanonical, expression)); + } + } + Expression complementCanonical = getCanonical(scopeComplementExpressions); + if (scopeComplementExpressions.size() >= 2) { + for (Expression expression : filter(scopeComplementExpressions, not(equalTo(complementCanonical)))) { + scopeComplementEqualities.add(new ComparisonExpression(ComparisonExpression.Type.EQUAL, complementCanonical, expression)); + } + } + + // Compile the scope straddling equality expressions + List connectingExpressions = new ArrayList<>(); + connectingExpressions.add(matchingCanonical); + connectingExpressions.add(complementCanonical); + connectingExpressions.addAll(scopeStraddlingExpressions); + connectingExpressions = ImmutableList.copyOf(filter(connectingExpressions, Predicates.notNull())); + Expression connectingCanonical = getCanonical(connectingExpressions); + if (connectingCanonical != null) { + for (Expression expression : filter(connectingExpressions, not(equalTo(connectingCanonical)))) { + scopeStraddlingEqualities.add(new ComparisonExpression(ComparisonExpression.Type.EQUAL, connectingCanonical, expression)); + } + } + } + + return new EqualityPartition(scopeEqualities, scopeComplementEqualities, scopeStraddlingEqualities); + } + + /** + * Returns the most preferrable expression to be used as the canonical expression + */ + private static Expression getCanonical(Iterable expressions) + { + if (Iterables.isEmpty(expressions)) { + return null; + } + return CANONICAL_ORDERING.min(expressions); + } + + /** + * Returns a canonical expression that is fully contained by the symbolScope and that is equivalent + * to the specified expression. Returns null if unable to to find a canonical. + */ + @VisibleForTesting + Expression getScopedCanonical(Expression expression, Predicate symbolScope) + { + Expression canonicalIndex = canonicalMap.get(expression); + if (canonicalIndex == null) { + return null; + } + return getCanonical(filter(equalitySets.get(canonicalIndex), symbolToExpressionPredicate(symbolScope))); + } + + private static Predicate symbolToExpressionPredicate(final Predicate symbolScope) + { + return expression -> Iterables.all(DependencyExtractor.extractUnique(expression), symbolScope); + } + + /** + * Determines whether an Expression may be successfully applied to the equality inference + */ + public static Predicate isInferenceCandidate() + { + return expression -> { + expression = normalizeInPredicateToEquality(expression); + if (DeterminismEvaluator.isDeterministic(expression) && expression instanceof ComparisonExpression) { + ComparisonExpression comparison = (ComparisonExpression) expression; + if (comparison.getType() == ComparisonExpression.Type.EQUAL) { + // We should only consider equalities that have distinct left and right components + return !comparison.getLeft().equals(comparison.getRight()); + } + } + return false; + }; + } + + /** + * Rewrite single value InPredicates as equality if possible + */ + private static Expression normalizeInPredicateToEquality(Expression expression) + { + if (expression instanceof InPredicate) { + InPredicate inPredicate = (InPredicate) expression; + if (inPredicate.getValueList() instanceof InListExpression) { + InListExpression valueList = (InListExpression) inPredicate.getValueList(); + if (valueList.getValues().size() == 1) { + return new ComparisonExpression(ComparisonExpression.Type.EQUAL, inPredicate.getValue(), Iterables.getOnlyElement(valueList.getValues())); + } + } + } + return expression; + } + + /** + * Provides a convenience Iterable of Expression conjuncts which have not been added to the inference + */ + public static Iterable nonInferrableConjuncts(Expression expression) + { + return filter(extractConjuncts(expression), not(isInferenceCandidate())); + } + + public static EqualityInference createEqualityInference(Expression... expressions) + { + EqualityInference.Builder builder = new EqualityInference.Builder(); + for (Expression expression : expressions) { + builder.extractInferenceCandidates(expression); + } + return builder.build(); + } + + public static class EqualityPartition + { + private final List scopeEqualities; + private final List scopeComplementEqualities; + private final List scopeStraddlingEqualities; + + public EqualityPartition(Iterable scopeEqualities, Iterable scopeComplementEqualities, Iterable scopeStraddlingEqualities) + { + this.scopeEqualities = ImmutableList.copyOf(checkNotNull(scopeEqualities, "scopeEqualities is null")); + this.scopeComplementEqualities = ImmutableList.copyOf(checkNotNull(scopeComplementEqualities, "scopeComplementEqualities is null")); + this.scopeStraddlingEqualities = ImmutableList.copyOf(checkNotNull(scopeStraddlingEqualities, "scopeStraddlingEqualities is null")); + } + + public List getScopeEqualities() + { + return scopeEqualities; + } + + public List getScopeComplementEqualities() + { + return scopeComplementEqualities; + } + + public List getScopeStraddlingEqualities() + { + return scopeStraddlingEqualities; + } + } + + public static class Builder + { + private final Map map = new HashMap<>(); + private final Multimap reverseMap = HashMultimap.create(); + + public Builder extractInferenceCandidates(Expression expression) + { + return addAllEqualities(filter(extractConjuncts(expression), isInferenceCandidate())); + } + + public Builder addAllEqualities(Iterable expressions) + { + for (Expression expression : expressions) { + addEquality(expression); + } + return this; + } + + public Builder addEquality(Expression expression) + { + expression = normalizeInPredicateToEquality(expression); + checkArgument(isInferenceCandidate().apply(expression), "Expression must be a simple equality: " + expression); + ComparisonExpression comparison = (ComparisonExpression) expression; + addEquality(comparison.getLeft(), comparison.getRight()); + return this; + } + + public Builder addEquality(Expression expression1, Expression expression2) + { + checkArgument(!expression1.equals(expression2), "need to provide equality between different expressions"); + checkArgument(DeterminismEvaluator.isDeterministic(expression1), "Expression must be deterministic: " + expression1); + checkArgument(DeterminismEvaluator.isDeterministic(expression2), "Expression must be deterministic: " + expression2); + + Expression canonical1 = canonicalize(expression1); + Expression canonical2 = canonicalize(expression2); + + if (!canonical1.equals(canonical2)) { + map.put(canonical1, canonical2); + reverseMap.put(canonical2, canonical1); + } + return this; + } + + private Expression canonicalize(Expression expression) + { + while (map.containsKey(expression)) { + expression = map.get(expression); + } + return expression; + } + + private void collectEqualities(Expression expression, ImmutableSet.Builder builder) + { + builder.add(expression); + for (Expression childExpression : reverseMap.get(expression)) { + collectEqualities(childExpression, builder); + } + } + + private Set extractEqualExpressions(Expression expression) + { + ImmutableSet.Builder builder = ImmutableSet.builder(); + collectEqualities(canonicalize(expression), builder); + return builder.build(); + } + + public EqualityInference build() + { + HashSet seenCanonicals = new HashSet<>(); + ImmutableList.Builder> builder = ImmutableList.builder(); + for (Expression expression : map.keySet()) { + Expression canonical = canonicalize(expression); + if (!seenCanonicals.contains(canonical)) { + builder.add(extractEqualExpressions(canonical)); + seenCanonicals.add(canonical); + } + } + return new EqualityInference(builder.build()); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/ExpressionInterpreter.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/ExpressionInterpreter.java new file mode 100644 index 00000000..834260b2 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/ExpressionInterpreter.java @@ -0,0 +1,890 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner; + +import com.facebook.presto.Session; +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.metadata.OperatorType; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.RecordCursor; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.tree.ArithmeticBinaryExpression; +import com.facebook.presto.sql.tree.ArithmeticUnaryExpression; +import com.facebook.presto.sql.tree.ArrayConstructor; +import com.facebook.presto.sql.tree.AstVisitor; +import com.facebook.presto.sql.tree.BetweenPredicate; +import com.facebook.presto.sql.tree.BooleanLiteral; +import com.facebook.presto.sql.tree.Cast; +import com.facebook.presto.sql.tree.CoalesceExpression; +import com.facebook.presto.sql.tree.ComparisonExpression; +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.FunctionCall; +import com.facebook.presto.sql.tree.InListExpression; +import com.facebook.presto.sql.tree.InPredicate; +import com.facebook.presto.sql.tree.InputReference; +import com.facebook.presto.sql.tree.IsNotNullPredicate; +import com.facebook.presto.sql.tree.IsNullPredicate; +import com.facebook.presto.sql.tree.LikePredicate; +import com.facebook.presto.sql.tree.Literal; +import com.facebook.presto.sql.tree.LogicalBinaryExpression; +import com.facebook.presto.sql.tree.Node; +import com.facebook.presto.sql.tree.NotExpression; +import com.facebook.presto.sql.tree.NullIfExpression; +import com.facebook.presto.sql.tree.NullLiteral; +import com.facebook.presto.sql.tree.QualifiedName; +import com.facebook.presto.sql.tree.QualifiedNameReference; +import com.facebook.presto.sql.tree.Row; +import com.facebook.presto.sql.tree.SearchedCaseExpression; +import com.facebook.presto.sql.tree.SimpleCaseExpression; +import com.facebook.presto.sql.tree.StringLiteral; +import com.facebook.presto.sql.tree.SubscriptExpression; +import com.facebook.presto.sql.tree.WhenClause; +import com.facebook.presto.type.LikeFunctions; +import com.google.common.base.Functions; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import io.airlift.joni.Regex; +import io.airlift.slice.Slice; + +import java.lang.invoke.MethodHandle; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +import static com.facebook.presto.spi.StandardErrorCode.NOT_SUPPORTED; +import static com.facebook.presto.spi.type.TypeSignature.parseTypeSignature; +import static com.facebook.presto.sql.planner.LiteralInterpreter.toExpression; +import static com.facebook.presto.sql.planner.LiteralInterpreter.toExpressions; +import static com.facebook.presto.util.ImmutableCollectors.toImmutableList; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Predicates.instanceOf; +import static com.google.common.collect.Iterables.any; +import static java.nio.charset.StandardCharsets.UTF_8; + +public class ExpressionInterpreter +{ + private final Expression expression; + private final Metadata metadata; + private final ConnectorSession session; + private final boolean optimize; + private final IdentityHashMap expressionTypes; + + private final Visitor visitor; + + // identity-based cache for LIKE expressions with constant pattern and escape char + private final IdentityHashMap likePatternCache = new IdentityHashMap<>(); + private final IdentityHashMap> inListCache = new IdentityHashMap<>(); + + public static ExpressionInterpreter expressionInterpreter(Expression expression, Metadata metadata, Session session, IdentityHashMap expressionTypes) + { + checkNotNull(expression, "expression is null"); + checkNotNull(metadata, "metadata is null"); + checkNotNull(session, "session is null"); + + return new ExpressionInterpreter(expression, metadata, session, expressionTypes, false); + } + + public static ExpressionInterpreter expressionOptimizer(Expression expression, Metadata metadata, Session session, IdentityHashMap expressionTypes) + { + checkNotNull(expression, "expression is null"); + checkNotNull(metadata, "metadata is null"); + checkNotNull(session, "session is null"); + + return new ExpressionInterpreter(expression, metadata, session, expressionTypes, true); + } + + private ExpressionInterpreter(Expression expression, Metadata metadata, Session session, IdentityHashMap expressionTypes, boolean optimize) + { + this.expression = expression; + this.metadata = metadata; + this.session = session.toConnectorSession(); + this.expressionTypes = expressionTypes; + this.optimize = optimize; + + this.visitor = new Visitor(); + } + + public Object evaluate(RecordCursor inputs) + { + checkState(!optimize, "evaluate(RecordCursor) not allowed for optimizer"); + return visitor.process(expression, inputs); + } + + public Object evaluate(int position, Block... inputs) + { + checkState(!optimize, "evaluate(int, Block...) not allowed for optimizer"); + return visitor.process(expression, new PagePositionContext(position, inputs)); + } + + public Object optimize(SymbolResolver inputs) + { + checkState(optimize, "evaluate(SymbolResolver) not allowed for interpreter"); + return visitor.process(expression, inputs); + } + + @SuppressWarnings("FloatingPointEquality") + private class Visitor + extends AstVisitor + { + @Override + public Object visitInputReference(InputReference node, Object context) + { + Type type = expressionTypes.get(node); + + int channel = node.getChannel(); + if (context instanceof PagePositionContext) { + PagePositionContext pagePositionContext = (PagePositionContext) context; + int position = pagePositionContext.getPosition(); + Block block = pagePositionContext.getBlock(channel); + + if (block.isNull(position)) { + return null; + } + + Class javaType = type.getJavaType(); + if (javaType == boolean.class) { + return type.getBoolean(block, position); + } + else if (javaType == long.class) { + return type.getLong(block, position); + } + else if (javaType == double.class) { + return type.getDouble(block, position); + } + else if (javaType == Slice.class) { + return type.getSlice(block, position); + } + else { + throw new UnsupportedOperationException("not yet implemented"); + } + } + else if (context instanceof RecordCursor) { + RecordCursor cursor = (RecordCursor) context; + if (cursor.isNull(channel)) { + return null; + } + + Class javaType = type.getJavaType(); + if (javaType == boolean.class) { + return cursor.getBoolean(channel); + } + else if (javaType == long.class) { + return cursor.getLong(channel); + } + else if (javaType == double.class) { + return cursor.getDouble(channel); + } + else if (javaType == Slice.class) { + return cursor.getSlice(channel); + } + else { + throw new UnsupportedOperationException("not yet implemented"); + } + } + throw new UnsupportedOperationException("Inputs or cursor myst be set"); + } + + @Override + protected Object visitQualifiedNameReference(QualifiedNameReference node, Object context) + { + if (node.getName().getPrefix().isPresent()) { + // not a symbol + return node; + } + + Symbol symbol = Symbol.fromQualifiedName(node.getName()); + return ((SymbolResolver) context).getValue(symbol); + } + + @Override + protected Object visitLiteral(Literal node, Object context) + { + return LiteralInterpreter.evaluate(metadata, session, node); + } + + @Override + protected Object visitIsNullPredicate(IsNullPredicate node, Object context) + { + Object value = process(node.getValue(), context); + + if (value instanceof Expression) { + return new IsNullPredicate(toExpression(value, expressionTypes.get(node.getValue()))); + } + + return value == null; + } + + @Override + protected Object visitIsNotNullPredicate(IsNotNullPredicate node, Object context) + { + Object value = process(node.getValue(), context); + + if (value instanceof Expression) { + return new IsNotNullPredicate(toExpression(value, expressionTypes.get(node.getValue()))); + } + + return value != null; + } + + @Override + protected Object visitSearchedCaseExpression(SearchedCaseExpression node, Object context) + { + Expression resultClause = node.getDefaultValue().orElse(null); + for (WhenClause whenClause : node.getWhenClauses()) { + Object value = process(whenClause.getOperand(), context); + if (value instanceof Expression) { + // TODO: optimize this case + return node; + } + + if (Boolean.TRUE.equals(value)) { + resultClause = whenClause.getResult(); + break; + } + } + + if (resultClause == null) { + return null; + } + + Object result = process(resultClause, context); + if (result instanceof Expression) { + return node; + } + return result; + } + + @Override + protected Object visitSimpleCaseExpression(SimpleCaseExpression node, Object context) + { + Object operand = process(node.getOperand(), context); + if (operand instanceof Expression) { + // TODO: optimize this case + return node; + } + + Expression resultClause = node.getDefaultValue().orElse(null); + if (operand != null) { + for (WhenClause whenClause : node.getWhenClauses()) { + Object value = process(whenClause.getOperand(), context); + if (value == null) { + continue; + } + if (value instanceof Expression) { + // TODO: optimize this case + return node; + } + + if ((Boolean) invokeOperator(OperatorType.EQUAL, types(node.getOperand(), whenClause.getOperand()), ImmutableList.of(operand, value))) { + resultClause = whenClause.getResult(); + break; + } + } + } + if (resultClause == null) { + return null; + } + + Object result = process(resultClause, context); + if (result instanceof Expression) { + return node; + } + return result; + } + + @Override + protected Object visitCoalesceExpression(CoalesceExpression node, Object context) + { + for (Expression expression : node.getOperands()) { + Object value = process(expression, context); + + if (value instanceof Expression) { + // TODO: optimize this case + return node; + } + + if (value != null) { + return value; + } + } + return null; + } + + @Override + protected Object visitInPredicate(InPredicate node, Object context) + { + Object value = process(node.getValue(), context); + if (value == null) { + return null; + } + + Expression valueListExpression = node.getValueList(); + if (!(valueListExpression instanceof InListExpression)) { + if (!optimize) { + throw new UnsupportedOperationException("IN predicate value list type not yet implemented: " + valueListExpression.getClass().getName()); + } + return node; + } + InListExpression valueList = (InListExpression) valueListExpression; + + Set set = inListCache.get(valueList); + + // We use the presence of the node in the map to indicate that we've already done + // the analysis below. If the value is null, it means that we can't apply the HashSet + // optimization + if (!inListCache.containsKey(valueList)) { + if (Iterables.all(valueList.getValues(), ExpressionInterpreter::isNullLiteral)) { + // if all elements are constant, create a set with them + set = new HashSet<>(); + for (Expression expression : valueList.getValues()) { + set.add(process(expression, context)); + } + } + inListCache.put(valueList, set); + } + + if (set != null && !(value instanceof Expression)) { + return set.contains(value); + } + + boolean hasUnresolvedValue = false; + if (value instanceof Expression) { + hasUnresolvedValue = true; + } + + boolean hasNullValue = false; + boolean found = false; + List values = new ArrayList<>(valueList.getValues().size()); + List types = new ArrayList<>(valueList.getValues().size()); + for (Expression expression : valueList.getValues()) { + Object inValue = process(expression, context); + if (value instanceof Expression || inValue instanceof Expression) { + hasUnresolvedValue = true; + values.add(inValue); + types.add(expressionTypes.get(expression)); + continue; + } + + if (inValue == null) { + hasNullValue = true; + } + else if (!found && (Boolean) invokeOperator(OperatorType.EQUAL, types(node.getValue(), expression), ImmutableList.of(value, inValue))) { + // in does not short-circuit so we must evaluate all value in the list + found = true; + } + } + if (found) { + return true; + } + + if (hasUnresolvedValue) { + Type type = expressionTypes.get(node.getValue()); + List expressionValues = toExpressions(values, types); + List simplifiedExpressionValues = Stream.concat( + expressionValues.stream() + .filter(DeterminismEvaluator::isDeterministic) + .distinct(), + expressionValues.stream() + .filter((expression -> !DeterminismEvaluator.isDeterministic(expression)))) + .collect(toImmutableList()); + return new InPredicate(toExpression(value, type), new InListExpression(simplifiedExpressionValues)); + } + if (hasNullValue) { + return null; + } + return false; + } + + @Override + protected Object visitArithmeticUnary(ArithmeticUnaryExpression node, Object context) + { + Object value = process(node.getValue(), context); + if (value == null) { + return null; + } + if (value instanceof Expression) { + return new ArithmeticUnaryExpression(node.getSign(), toExpression(value, expressionTypes.get(node.getValue()))); + } + + switch (node.getSign()) { + case PLUS: + return value; + case MINUS: + FunctionInfo operatorInfo = metadata.resolveOperator(OperatorType.NEGATION, types(node.getValue())); + + MethodHandle handle = operatorInfo.getMethodHandle(); + if (handle.type().parameterCount() > 0 && handle.type().parameterType(0) == ConnectorSession.class) { + handle = handle.bindTo(session); + } + try { + return handle.invokeWithArguments(value); + } + catch (Throwable throwable) { + Throwables.propagateIfInstanceOf(throwable, RuntimeException.class); + Throwables.propagateIfInstanceOf(throwable, Error.class); + throw new RuntimeException(throwable.getMessage(), throwable); + } + } + + throw new UnsupportedOperationException("Unsupported unary operator: " + node.getSign()); + } + + @Override + protected Object visitArithmeticBinary(ArithmeticBinaryExpression node, Object context) + { + Object left = process(node.getLeft(), context); + if (left == null) { + return null; + } + Object right = process(node.getRight(), context); + if (right == null) { + return null; + } + + if (hasUnresolvedValue(left, right)) { + return new ArithmeticBinaryExpression(node.getType(), toExpression(left, expressionTypes.get(node.getLeft())), toExpression(right, expressionTypes.get(node.getRight()))); + } + + return invokeOperator(OperatorType.valueOf(node.getType().name()), types(node.getLeft(), node.getRight()), ImmutableList.of(left, right)); + } + + @Override + protected Object visitComparisonExpression(ComparisonExpression node, Object context) + { + ComparisonExpression.Type type = node.getType(); + + Object left = process(node.getLeft(), context); + if (left == null && type != ComparisonExpression.Type.IS_DISTINCT_FROM) { + return null; + } + + Object right = process(node.getRight(), context); + if (type == ComparisonExpression.Type.IS_DISTINCT_FROM) { + if (left == null && right == null) { + return false; + } + else if (left == null || right == null) { + return true; + } + } + else if (right == null) { + return null; + } + + if (hasUnresolvedValue(left, right)) { + return new ComparisonExpression(type, toExpression(left, expressionTypes.get(node.getLeft())), toExpression(right, expressionTypes.get(node.getRight()))); + } + + if (type == ComparisonExpression.Type.IS_DISTINCT_FROM) { + type = ComparisonExpression.Type.NOT_EQUAL; + } + + return invokeOperator(OperatorType.valueOf(type.name()), types(node.getLeft(), node.getRight()), ImmutableList.of(left, right)); + } + + @Override + protected Object visitBetweenPredicate(BetweenPredicate node, Object context) + { + Object value = process(node.getValue(), context); + if (value == null) { + return null; + } + Object min = process(node.getMin(), context); + if (min == null) { + return null; + } + Object max = process(node.getMax(), context); + if (max == null) { + return null; + } + + if (hasUnresolvedValue(value, min, max)) { + return new BetweenPredicate( + toExpression(value, expressionTypes.get(node.getValue())), + toExpression(min, expressionTypes.get(node.getMin())), + toExpression(max, expressionTypes.get(node.getMax()))); + } + + return invokeOperator(OperatorType.BETWEEN, types(node.getValue(), node.getMin(), node.getMax()), ImmutableList.of(value, min, max)); + } + + @Override + protected Object visitNullIfExpression(NullIfExpression node, Object context) + { + Object first = process(node.getFirst(), context); + if (first == null) { + return null; + } + Object second = process(node.getSecond(), context); + if (second == null) { + return first; + } + + Type firstType = expressionTypes.get(node.getFirst()); + Type secondType = expressionTypes.get(node.getSecond()); + + if (hasUnresolvedValue(first, second)) { + return new NullIfExpression(toExpression(first, firstType), toExpression(second, secondType)); + } + + Type commonType = FunctionRegistry.getCommonSuperType(firstType, secondType).get(); + + FunctionInfo firstCast = metadata.getFunctionRegistry().getCoercion(firstType, commonType); + FunctionInfo secondCast = metadata.getFunctionRegistry().getCoercion(secondType, commonType); + + // cast(first as ) == cast(second as ) + boolean equal = (Boolean) invokeOperator( + OperatorType.EQUAL, + ImmutableList.of(commonType, commonType), + ImmutableList.of( + invoke(session, firstCast.getMethodHandle(), ImmutableList.of(first)), + invoke(session, secondCast.getMethodHandle(), ImmutableList.of(second)))); + + if (equal) { + return null; + } + else { + return first; + } + } + + @Override + protected Object visitNotExpression(NotExpression node, Object context) + { + Object value = process(node.getValue(), context); + if (value == null) { + return null; + } + + if (value instanceof Expression) { + return new NotExpression(toExpression(value, expressionTypes.get(node.getValue()))); + } + + return !(Boolean) value; + } + + @Override + protected Object visitLogicalBinaryExpression(LogicalBinaryExpression node, Object context) + { + Object left = process(node.getLeft(), context); + Object right = process(node.getRight(), context); + + switch (node.getType()) { + case AND: { + // if either left or right is false, result is always false regardless of nulls + if (Boolean.FALSE.equals(left) || Boolean.TRUE.equals(right)) { + return left; + } + + if (Boolean.FALSE.equals(right) || Boolean.TRUE.equals(left)) { + return right; + } + break; + } + case OR: { + // if either left or right is true, result is always true regardless of nulls + if (Boolean.TRUE.equals(left) || Boolean.FALSE.equals(right)) { + return left; + } + + if (Boolean.TRUE.equals(right) || Boolean.FALSE.equals(left)) { + return right; + } + break; + } + } + + if (left == null && right == null) { + return null; + } + + return new LogicalBinaryExpression(node.getType(), + toExpression(left, expressionTypes.get(node.getLeft())), + toExpression(right, expressionTypes.get(node.getRight()))); + } + + @Override + protected Object visitBooleanLiteral(BooleanLiteral node, Object context) + { + return node.equals(BooleanLiteral.TRUE_LITERAL); + } + + @Override + protected Object visitFunctionCall(FunctionCall node, Object context) + { + List argumentTypes = new ArrayList<>(); + List argumentValues = new ArrayList<>(); + for (Expression expression : node.getArguments()) { + Object value = process(expression, context); + Type type = expressionTypes.get(expression); + argumentValues.add(value); + argumentTypes.add(type); + } + FunctionInfo function = metadata.resolveFunction(node.getName(), Lists.transform(argumentTypes, Type::getTypeSignature), false); + for (int i = 0; i < argumentValues.size(); i++) { + Object value = argumentValues.get(i); + if (value == null && !function.getNullableArguments().get(i)) { + return null; + } + } + + // do not optimize non-deterministic functions + if (optimize && (!function.isDeterministic() || hasUnresolvedValue(argumentValues))) { + return new FunctionCall(node.getName(), node.getWindow(), node.isDistinct(), toExpressions(argumentValues, argumentTypes)); + } + return invoke(session, function.getMethodHandle(), argumentValues); + } + + @Override + protected Object visitLikePredicate(LikePredicate node, Object context) + { + Object value = process(node.getValue(), context); + + if (value == null) { + return null; + } + + if (value instanceof Slice && + node.getPattern() instanceof StringLiteral && + (node.getEscape() instanceof StringLiteral || node.getEscape() == null)) { + // fast path when we know the pattern and escape are constant + return LikeFunctions.like((Slice) value, getConstantPattern(node)); + } + + Object pattern = process(node.getPattern(), context); + + if (pattern == null) { + return null; + } + + Object escape = null; + if (node.getEscape() != null) { + escape = process(node.getEscape(), context); + + if (escape == null) { + return null; + } + } + + if (value instanceof Slice && + pattern instanceof Slice && + (escape == null || escape instanceof Slice)) { + Regex regex; + if (escape == null) { + regex = LikeFunctions.likePattern((Slice) pattern); + } + else { + regex = LikeFunctions.likePattern((Slice) pattern, (Slice) escape); + } + + return LikeFunctions.like((Slice) value, regex); + } + + // if pattern is a constant without % or _ replace with a comparison + if (pattern instanceof Slice && escape == null) { + String stringPattern = ((Slice) pattern).toString(UTF_8); + if (!stringPattern.contains("%") && !stringPattern.contains("_")) { + return new ComparisonExpression(ComparisonExpression.Type.EQUAL, + toExpression(value, expressionTypes.get(node.getValue())), + toExpression(pattern, expressionTypes.get(node.getPattern()))); + } + } + + Expression optimizedEscape = null; + if (node.getEscape() != null) { + optimizedEscape = toExpression(escape, expressionTypes.get(node.getEscape())); + } + + return new LikePredicate( + toExpression(value, expressionTypes.get(node.getValue())), + toExpression(pattern, expressionTypes.get(node.getPattern())), + optimizedEscape); + } + + private Regex getConstantPattern(LikePredicate node) + { + Regex result = likePatternCache.get(node); + + if (result == null) { + StringLiteral pattern = (StringLiteral) node.getPattern(); + StringLiteral escape = (StringLiteral) node.getEscape(); + + if (escape == null) { + result = LikeFunctions.likePattern(pattern.getSlice()); + } + else { + result = LikeFunctions.likePattern(pattern.getSlice(), escape.getSlice()); + } + + likePatternCache.put(node, result); + } + + return result; + } + + @Override + public Object visitCast(Cast node, Object context) + { + Object value = process(node.getExpression(), context); + + if (value instanceof Expression) { + return new Cast((Expression) value, node.getType(), node.isSafe()); + } + + // hack!!! don't optimize CASTs for types that cannot be represented in the SQL AST + // TODO: this will not be an issue when we migrate to RowExpression tree for this, which allows arbitrary literals. + if (optimize && !FunctionRegistry.isSupportedLiteralType(expressionTypes.get(node))) { + return new Cast(toExpression(value, expressionTypes.get(node.getExpression())), node.getType(), node.isSafe()); + } + + if (value == null) { + return null; + } + + Type type = metadata.getType(parseTypeSignature(node.getType())); + if (type == null) { + throw new IllegalArgumentException("Unsupported type: " + node.getType()); + } + + FunctionInfo operatorInfo = metadata.getFunctionRegistry().getCoercion(expressionTypes.get(node.getExpression()), type); + + try { + return invoke(session, operatorInfo.getMethodHandle(), ImmutableList.of(value)); + } + catch (RuntimeException e) { + if (node.isSafe()) { + return null; + } + throw e; + } + } + + @Override + protected Object visitArrayConstructor(ArrayConstructor node, Object context) + { + return visitFunctionCall(new FunctionCall(QualifiedName.of(ArrayConstructor.ARRAY_CONSTRUCTOR), node.getValues()), context); + } + + @Override + protected Object visitRow(Row node, Object context) + { + throw new PrestoException(NOT_SUPPORTED, "Row expressions not yet supported"); + } + + @Override + protected Object visitSubscriptExpression(SubscriptExpression node, Object context) + { + Object base = process(node.getBase(), context); + if (base == null) { + return null; + } + Object index = process(node.getIndex(), context); + if (index == null) { + return null; + } + + if (hasUnresolvedValue(base, index)) { + return new SubscriptExpression(toExpression(base, expressionTypes.get(node.getBase())), toExpression(index, expressionTypes.get(node.getIndex()))); + } + + return invokeOperator(OperatorType.SUBSCRIPT, types(node.getBase(), node.getIndex()), ImmutableList.of(base, index)); + } + + @Override + protected Object visitExpression(Expression node, Object context) + { + throw new PrestoException(NOT_SUPPORTED, "not yet implemented: " + node.getClass().getName()); + } + + @Override + protected Object visitNode(Node node, Object context) + { + throw new UnsupportedOperationException("Evaluator visitor can only handle Expression nodes"); + } + + private List types(Expression... types) + { + return ImmutableList.copyOf(Iterables.transform(ImmutableList.copyOf(types), Functions.forMap(expressionTypes))); + } + + private boolean hasUnresolvedValue(Object... values) + { + return hasUnresolvedValue(ImmutableList.copyOf(values)); + } + + private boolean hasUnresolvedValue(List values) + { + return any(values, instanceOf(Expression.class)); + } + + private Object invokeOperator(OperatorType operatorType, List argumentTypes, List argumentValues) + { + FunctionInfo operatorInfo = metadata.resolveOperator(operatorType, argumentTypes); + return invoke(session, operatorInfo.getMethodHandle(), argumentValues); + } + } + + private static class PagePositionContext + { + private final int position; + private final Block[] blocks; + + private PagePositionContext(int position, Block[] blocks) + { + this.position = position; + this.blocks = blocks; + } + + public Block getBlock(int channel) + { + return blocks[channel]; + } + + public int getPosition() + { + return position; + } + } + + public static Object invoke(ConnectorSession session, MethodHandle handle, List argumentValues) + { + if (handle.type().parameterCount() > 0 && handle.type().parameterType(0) == ConnectorSession.class) { + handle = handle.bindTo(session); + } + try { + return handle.invokeWithArguments(argumentValues); + } + catch (Throwable throwable) { + if (throwable instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + throw Throwables.propagate(throwable); + } + } + + private static boolean isNullLiteral(Expression entry) + { + return entry instanceof Literal && !(entry instanceof NullLiteral); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/ExpressionNodeInliner.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/ExpressionNodeInliner.java new file mode 100644 index 00000000..edadb6b7 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/ExpressionNodeInliner.java @@ -0,0 +1,37 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner; + +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.ExpressionRewriter; +import com.facebook.presto.sql.tree.ExpressionTreeRewriter; + +import java.util.Map; + +public class ExpressionNodeInliner + extends ExpressionRewriter +{ + private final Map mappings; + + public ExpressionNodeInliner(Map mappings) + { + this.mappings = mappings; + } + + @Override + public Expression rewriteExpression(Expression node, Void context, ExpressionTreeRewriter treeRewriter) + { + return mappings.get(node); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/ExpressionSymbolInliner.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/ExpressionSymbolInliner.java new file mode 100644 index 00000000..c497eca9 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/ExpressionSymbolInliner.java @@ -0,0 +1,38 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner; + +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.ExpressionRewriter; +import com.facebook.presto.sql.tree.ExpressionTreeRewriter; +import com.facebook.presto.sql.tree.QualifiedNameReference; + +import java.util.Map; + +public class ExpressionSymbolInliner + extends ExpressionRewriter +{ + private final Map mappings; + + public ExpressionSymbolInliner(Map mappings) + { + this.mappings = mappings; + } + + @Override + public Expression rewriteQualifiedNameReference(QualifiedNameReference node, Void context, ExpressionTreeRewriter treeRewriter) + { + return mappings.get(Symbol.fromQualifiedName(node.getName())); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/InputExtractor.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/InputExtractor.java new file mode 100644 index 00000000..8ea69c99 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/InputExtractor.java @@ -0,0 +1,177 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner; + +import com.facebook.presto.execution.Column; +import com.facebook.presto.execution.Input; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.metadata.TableHandle; +import com.facebook.presto.metadata.TableMetadata; +import com.facebook.presto.spi.ColumnMetadata; +import com.facebook.presto.spi.SchemaTableName; +import com.facebook.presto.sql.planner.plan.IndexSourceNode; +import com.facebook.presto.sql.planner.plan.PlanNode; +import com.facebook.presto.sql.planner.plan.PlanVisitor; +import com.facebook.presto.sql.planner.plan.TableScanNode; +import com.google.common.collect.ImmutableList; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +public class InputExtractor +{ + private final Metadata metadata; + + public InputExtractor(Metadata metadata) + { + this.metadata = metadata; + } + + public List extract(PlanNode root) + { + Visitor visitor = new Visitor(); + root.accept(visitor, null); + + ImmutableList.Builder inputBuilder = ImmutableList.builder(); + for (Map.Entry> entry : visitor.getInputs().entrySet()) { + Input input = new Input(entry.getKey().getConnectorId(), entry.getKey().getSchema(), entry.getKey().getTable(), ImmutableList.copyOf(entry.getValue())); + inputBuilder.add(input); + } + + return inputBuilder.build(); + } + + private static Column createColumnEntry(ColumnMetadata columnMetadata) + { + return new Column(columnMetadata.getName(), columnMetadata.getType().toString(), Optional.empty()); + } + + private static TableEntry createTableEntry(TableMetadata table) + { + SchemaTableName schemaTable = table.getTable(); + return new TableEntry(table.getConnectorId(), schemaTable.getSchemaName(), schemaTable.getTableName()); + } + + private class Visitor + extends PlanVisitor + { + private final Map> inputs = new HashMap<>(); + + public Map> getInputs() + { + return inputs; + } + + @Override + public Void visitTableScan(TableScanNode node, Void context) + { + TableHandle tableHandle = node.getTable(); + Optional sampleWeightColumn = metadata.getSampleWeightColumnHandle(tableHandle); + + Set columns = new HashSet<>(); + for (ColumnHandle columnHandle : node.getAssignments().values()) { + if (!columnHandle.equals(sampleWeightColumn.orElse(null))) { + columns.add(createColumnEntry(metadata.getColumnMetadata(tableHandle, columnHandle))); + } + } + + inputs.put(createTableEntry(metadata.getTableMetadata(tableHandle)), columns); + + return null; + } + + @Override + public Void visitIndexSource(IndexSourceNode node, Void context) + { + TableHandle tableHandle = node.getTableHandle(); + Optional sampleWeightColumn = metadata.getSampleWeightColumnHandle(tableHandle); + + Set columns = new HashSet<>(); + for (ColumnHandle columnHandle : node.getAssignments().values()) { + if (!columnHandle.equals(sampleWeightColumn.orElse(null))) { + columns.add(createColumnEntry(metadata.getColumnMetadata(tableHandle, columnHandle))); + } + } + + inputs.put(createTableEntry(metadata.getTableMetadata(tableHandle)), columns); + + return null; + } + + @Override + protected Void visitPlan(PlanNode node, Void context) + { + for (PlanNode child : node.getSources()) { + child.accept(this, context); + } + return null; + } + } + + private static final class TableEntry + { + private final String connectorId; + private final String schema; + private final String table; + + private TableEntry(String connectorId, String schema, String table) + { + this.connectorId = connectorId; + this.schema = schema; + this.table = table; + } + + public String getConnectorId() + { + return connectorId; + } + + public String getSchema() + { + return schema; + } + + public String getTable() + { + return table; + } + + @Override + public int hashCode() + { + return Objects.hash(connectorId, schema, table); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final TableEntry other = (TableEntry) obj; + return Objects.equals(this.connectorId, other.connectorId) && + Objects.equals(this.schema, other.schema) && + Objects.equals(this.table, other.table); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/InterpretedFilterFunction.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/InterpretedFilterFunction.java new file mode 100644 index 00000000..c840a466 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/InterpretedFilterFunction.java @@ -0,0 +1,70 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner; + +import com.facebook.presto.Session; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.operator.FilterFunction; +import com.facebook.presto.spi.RecordCursor; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.parser.SqlParser; +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.ExpressionTreeRewriter; +import com.google.common.collect.ImmutableMap; + +import java.util.IdentityHashMap; +import java.util.Map; + +import static com.facebook.presto.sql.analyzer.ExpressionAnalyzer.getExpressionTypesFromInput; +import static java.lang.Boolean.TRUE; + +public class InterpretedFilterFunction + implements FilterFunction +{ + private final ExpressionInterpreter evaluator; + + public InterpretedFilterFunction( + Expression predicate, + Map symbolTypes, + Map symbolToInputMappings, + Metadata metadata, + SqlParser sqlParser, + Session session) + { + // pre-compute symbol -> input mappings and replace the corresponding nodes in the tree + Expression rewritten = ExpressionTreeRewriter.rewriteWith(new SymbolToInputRewriter(symbolToInputMappings), predicate); + + // analyze expression so we can know the type of every expression in the tree + ImmutableMap.Builder inputTypes = ImmutableMap.builder(); + for (Map.Entry entry : symbolToInputMappings.entrySet()) { + inputTypes.put(entry.getValue(), symbolTypes.get(entry.getKey())); + } + IdentityHashMap expressionTypes = getExpressionTypesFromInput(session, metadata, sqlParser, inputTypes.build(), rewritten); + + evaluator = ExpressionInterpreter.expressionInterpreter(rewritten, metadata, session, expressionTypes); + } + + @Override + public boolean filter(int position, Block... blocks) + { + return evaluator.evaluate(position, blocks) == TRUE; + } + + @Override + public boolean filter(RecordCursor cursor) + { + return evaluator.evaluate(cursor) == TRUE; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/InterpretedProjectionFunction.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/InterpretedProjectionFunction.java new file mode 100644 index 00000000..4223807c --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/InterpretedProjectionFunction.java @@ -0,0 +1,108 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner; + +import com.facebook.presto.Session; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.operator.ProjectionFunction; +import com.facebook.presto.spi.RecordCursor; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.parser.SqlParser; +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.ExpressionTreeRewriter; +import com.google.common.collect.ImmutableMap; +import io.airlift.slice.Slice; + +import java.util.IdentityHashMap; +import java.util.Map; + +import static com.facebook.presto.sql.analyzer.ExpressionAnalyzer.getExpressionTypesFromInput; +import static com.google.common.base.Preconditions.checkNotNull; + +public class InterpretedProjectionFunction + implements ProjectionFunction +{ + private final Type type; + private final ExpressionInterpreter evaluator; + + public InterpretedProjectionFunction( + Expression expression, + Map symbolTypes, + Map symbolToInputMappings, + Metadata metadata, + SqlParser sqlParser, + Session session) + { + // pre-compute symbol -> input mappings and replace the corresponding nodes in the tree + Expression rewritten = ExpressionTreeRewriter.rewriteWith(new SymbolToInputRewriter(symbolToInputMappings), expression); + + // analyze expression so we can know the type of every expression in the tree + ImmutableMap.Builder inputTypes = ImmutableMap.builder(); + for (Map.Entry entry : symbolToInputMappings.entrySet()) { + inputTypes.put(entry.getValue(), symbolTypes.get(entry.getKey())); + } + IdentityHashMap expressionTypes = getExpressionTypesFromInput(session, metadata, sqlParser, inputTypes.build(), rewritten); + this.type = checkNotNull(expressionTypes.get(rewritten), "type is null"); + + evaluator = ExpressionInterpreter.expressionInterpreter(rewritten, metadata, session, expressionTypes); + } + + @Override + public Type getType() + { + return type; + } + + @Override + public void project(int position, Block[] blocks, BlockBuilder output) + { + Object value = evaluator.evaluate(position, blocks); + append(output, value); + } + + @Override + public void project(RecordCursor cursor, BlockBuilder output) + { + Object value = evaluator.evaluate(cursor); + append(output, value); + } + + private void append(BlockBuilder output, Object value) + { + if (value == null) { + output.appendNull(); + return; + } + + Class javaType = type.getJavaType(); + if (javaType == boolean.class) { + type.writeBoolean(output, (Boolean) value); + } + else if (javaType == long.class) { + type.writeLong(output, (Long) value); + } + else if (javaType == double.class) { + type.writeDouble(output, (Double) value); + } + else if (javaType == Slice.class) { + Slice slice = (Slice) value; + type.writeSlice(output, slice, 0, slice.length()); + } + else { + throw new UnsupportedOperationException("not yet implemented: " + type); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/LiteralInterpreter.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/LiteralInterpreter.java new file mode 100644 index 00000000..67149bdf --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/LiteralInterpreter.java @@ -0,0 +1,246 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner; + +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.operator.scalar.VarbinaryFunctions; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.analyzer.SemanticException; +import com.facebook.presto.sql.tree.ArithmeticUnaryExpression; +import com.facebook.presto.sql.tree.AstVisitor; +import com.facebook.presto.sql.tree.BooleanLiteral; +import com.facebook.presto.sql.tree.Cast; +import com.facebook.presto.sql.tree.DoubleLiteral; +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.FunctionCall; +import com.facebook.presto.sql.tree.GenericLiteral; +import com.facebook.presto.sql.tree.IntervalLiteral; +import com.facebook.presto.sql.tree.Literal; +import com.facebook.presto.sql.tree.LongLiteral; +import com.facebook.presto.sql.tree.NullLiteral; +import com.facebook.presto.sql.tree.QualifiedName; +import com.facebook.presto.sql.tree.StringLiteral; +import com.facebook.presto.sql.tree.TimeLiteral; +import com.facebook.presto.sql.tree.TimestampLiteral; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; + +import java.util.List; + +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.BooleanType.BOOLEAN; +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; +import static com.facebook.presto.spi.type.TypeSignature.parseTypeSignature; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.TYPE_MISMATCH; +import static com.facebook.presto.type.UnknownType.UNKNOWN; +import static com.facebook.presto.util.DateTimeUtils.parseDayTimeInterval; +import static com.facebook.presto.util.DateTimeUtils.parseTime; +import static com.facebook.presto.util.DateTimeUtils.parseTimestamp; +import static com.facebook.presto.util.DateTimeUtils.parseYearMonthInterval; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.airlift.slice.Slices.utf8Slice; +import static java.nio.charset.StandardCharsets.UTF_8; + +public final class LiteralInterpreter +{ + private LiteralInterpreter() {} + + public static Object evaluate(Metadata metadata, ConnectorSession session, Expression node) + { + if (!(node instanceof Literal)) { + throw new IllegalArgumentException("node must be a Literal"); + } + return new LiteralVisitor(metadata).process(node, session); + } + + public static List toExpressions(List objects, List types) + { + checkNotNull(objects, "objects is null"); + checkNotNull(types, "types is null"); + checkArgument(objects.size() == types.size(), "objects and types do not have the same size"); + + ImmutableList.Builder expressions = ImmutableList.builder(); + for (int i = 0; i < objects.size(); i++) { + Object object = objects.get(i); + Type type = types.get(i); + expressions.add(toExpression(object, type)); + } + return expressions.build(); + } + + public static Expression toExpression(Object object, Type type) + { + if (object instanceof Expression) { + return (Expression) object; + } + + if (object == null) { + if (type.equals(UNKNOWN)) { + return new NullLiteral(); + } + return new Cast(new NullLiteral(), type.getTypeSignature().toString()); + } + + if (type.equals(BIGINT)) { + return new LongLiteral(object.toString()); + } + + if (type.equals(DOUBLE)) { + Double value = (Double) object; + // WARNING: the ORC predicate code depends on NaN and infinity not appearing in a tuple domain, so + // if you remove this, you will need to update the TupleDomainOrcPredicate + if (value.isNaN()) { + return new FunctionCall(new QualifiedName("nan"), ImmutableList.of()); + } + else if (value.equals(Double.NEGATIVE_INFINITY)) { + return ArithmeticUnaryExpression.negative(new FunctionCall(new QualifiedName("infinity"), ImmutableList.of())); + } + else if (value.equals(Double.POSITIVE_INFINITY)) { + return new FunctionCall(new QualifiedName("infinity"), ImmutableList.of()); + } + else { + return new DoubleLiteral(object.toString()); + } + } + + if (type.equals(VARCHAR)) { + if (object instanceof Slice) { + return new StringLiteral(((Slice) object).toString(UTF_8)); + } + + if (object instanceof String) { + return new StringLiteral((String) object); + } + } + + if (type.equals(BOOLEAN)) { + return new BooleanLiteral(object.toString()); + } + + if (object instanceof Slice && !type.equals(VARCHAR)) { + // HACK: we need to serialize VARBINARY in a format that can be embedded in an expression to be + // able to encode it in the plan that gets sent to workers. + // We do this by transforming the in-memory varbinary into a call to from_base64() + FunctionCall fromBase64 = new FunctionCall(new QualifiedName("from_base64"), ImmutableList.of(new StringLiteral(VarbinaryFunctions.toBase64((Slice) object).toStringUtf8()))); + Signature signature = FunctionRegistry.getMagicLiteralFunctionSignature(type); + return new FunctionCall(new QualifiedName(signature.getName()), ImmutableList.of(fromBase64)); + } + + Signature signature = FunctionRegistry.getMagicLiteralFunctionSignature(type); + Expression rawLiteral = toExpression(object, FunctionRegistry.type(type.getJavaType())); + + return new FunctionCall(new QualifiedName(signature.getName()), ImmutableList.of(rawLiteral)); + } + + private static class LiteralVisitor + extends AstVisitor + { + private final Metadata metadata; + + private LiteralVisitor(Metadata metadata) + { + this.metadata = metadata; + } + + @Override + protected Object visitLiteral(Literal node, ConnectorSession session) + { + throw new UnsupportedOperationException("Unhandled literal type: " + node); + } + + @Override + protected Object visitBooleanLiteral(BooleanLiteral node, ConnectorSession session) + { + return node.getValue(); + } + + @Override + protected Long visitLongLiteral(LongLiteral node, ConnectorSession session) + { + return node.getValue(); + } + + @Override + protected Double visitDoubleLiteral(DoubleLiteral node, ConnectorSession session) + { + return node.getValue(); + } + + @Override + protected Slice visitStringLiteral(StringLiteral node, ConnectorSession session) + { + return node.getSlice(); + } + + @Override + protected Object visitGenericLiteral(GenericLiteral node, ConnectorSession session) + { + Type type = metadata.getType(parseTypeSignature(node.getType())); + if (type == null) { + throw new SemanticException(TYPE_MISMATCH, node, "Unknown type: " + node.getType()); + } + + FunctionInfo operator; + try { + operator = metadata.getFunctionRegistry().getCoercion(VARCHAR, type); + } + catch (IllegalArgumentException e) { + throw new SemanticException(TYPE_MISMATCH, node, "No literal form for type %s", type); + } + try { + return ExpressionInterpreter.invoke(session, operator.getMethodHandle(), ImmutableList.of(utf8Slice(node.getValue()))); + } + catch (Throwable throwable) { + throw Throwables.propagate(throwable); + } + } + + @Override + protected Long visitTimeLiteral(TimeLiteral node, ConnectorSession session) + { + return parseTime(session.getTimeZoneKey(), node.getValue()); + } + + @Override + protected Long visitTimestampLiteral(TimestampLiteral node, ConnectorSession session) + { + return parseTimestamp(session.getTimeZoneKey(), node.getValue()); + } + + @Override + protected Long visitIntervalLiteral(IntervalLiteral node, ConnectorSession session) + { + if (node.isYearToMonth()) { + return node.getSign().multiplier() * parseYearMonthInterval(node.getValue(), node.getStartField(), node.getEndField()); + } + else { + return node.getSign().multiplier() * parseDayTimeInterval(node.getValue(), node.getStartField(), node.getEndField()); + } + + } + + @Override + protected Object visitNullLiteral(NullLiteral node, ConnectorSession session) + { + return null; + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/LocalExecutionPlanner.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/LocalExecutionPlanner.java new file mode 100644 index 00000000..5e4111ef --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/LocalExecutionPlanner.java @@ -0,0 +1,1865 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner; + +import com.facebook.presto.Session; +import com.facebook.presto.block.BlockUtils; +import com.facebook.presto.execution.TaskManagerConfig; +import com.facebook.presto.index.IndexManager; +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.operator.AggregationOperator.AggregationOperatorFactory; +import com.facebook.presto.operator.CursorProcessor; +import com.facebook.presto.operator.DeleteOperator.DeleteOperatorFactory; +import com.facebook.presto.operator.DriverFactory; +import com.facebook.presto.operator.ExchangeClient; +import com.facebook.presto.operator.ExchangeOperator.ExchangeOperatorFactory; +import com.facebook.presto.operator.FilterAndProjectOperator; +import com.facebook.presto.operator.FilterFunction; +import com.facebook.presto.operator.FilterFunctions; +import com.facebook.presto.operator.GenericCursorProcessor; +import com.facebook.presto.operator.GenericPageProcessor; +import com.facebook.presto.operator.HashAggregationOperator.HashAggregationOperatorFactory; +import com.facebook.presto.operator.HashBuilderOperator.HashBuilderOperatorFactory; +import com.facebook.presto.operator.HashPartitionMaskOperator.HashPartitionMaskOperatorFactory; +import com.facebook.presto.operator.HashSemiJoinOperator.HashSemiJoinOperatorFactory; +import com.facebook.presto.operator.InMemoryExchange; +import com.facebook.presto.operator.LimitOperator.LimitOperatorFactory; +import com.facebook.presto.operator.LookupJoinOperators; +import com.facebook.presto.operator.LookupSourceSupplier; +import com.facebook.presto.operator.MarkDistinctOperator.MarkDistinctOperatorFactory; +import com.facebook.presto.operator.OperatorFactory; +import com.facebook.presto.operator.OrderByOperator.OrderByOperatorFactory; +import com.facebook.presto.operator.OutputFactory; +import com.facebook.presto.operator.PageProcessor; +import com.facebook.presto.operator.ParallelHashBuilder; +import com.facebook.presto.operator.ProjectionFunction; +import com.facebook.presto.operator.ProjectionFunctions; +import com.facebook.presto.operator.RowNumberOperator; +import com.facebook.presto.operator.SampleOperator.SampleOperatorFactory; +import com.facebook.presto.operator.ScanFilterAndProjectOperator; +import com.facebook.presto.operator.SetBuilderOperator.SetBuilderOperatorFactory; +import com.facebook.presto.operator.SetBuilderOperator.SetSupplier; +import com.facebook.presto.operator.SourceOperatorFactory; +import com.facebook.presto.operator.TableScanOperator.TableScanOperatorFactory; +import com.facebook.presto.operator.TopNOperator.TopNOperatorFactory; +import com.facebook.presto.operator.TopNRowNumberOperator; +import com.facebook.presto.operator.ValuesOperator.ValuesOperatorFactory; +import com.facebook.presto.operator.WindowFunctionDefinition; +import com.facebook.presto.operator.WindowOperator.WindowOperatorFactory; +import com.facebook.presto.operator.aggregation.AccumulatorFactory; +import com.facebook.presto.operator.index.DynamicTupleFilterFactory; +import com.facebook.presto.operator.index.FieldSetFilteringRecordSet; +import com.facebook.presto.operator.index.IndexBuildDriverFactoryProvider; +import com.facebook.presto.operator.index.IndexJoinLookupStats; +import com.facebook.presto.operator.index.IndexLookupSourceSupplier; +import com.facebook.presto.operator.index.IndexSourceOperator; +import com.facebook.presto.operator.window.FrameInfo; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConnectorIndex; +import com.facebook.presto.spi.PageBuilder; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.RecordSet; +import com.facebook.presto.spi.block.SortOrder; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.split.MappedRecordSet; +import com.facebook.presto.split.PageSinkManager; +import com.facebook.presto.split.PageSourceProvider; +import com.facebook.presto.sql.gen.ExpressionCompiler; +import com.facebook.presto.sql.parser.SqlParser; +import com.facebook.presto.sql.planner.PlanFragment.PlanDistribution; +import com.facebook.presto.sql.planner.optimizations.IndexJoinOptimizer; +import com.facebook.presto.sql.planner.plan.AggregationNode; +import com.facebook.presto.sql.planner.plan.AggregationNode.Step; +import com.facebook.presto.sql.planner.plan.DeleteNode; +import com.facebook.presto.sql.planner.plan.DistinctLimitNode; +import com.facebook.presto.sql.planner.plan.FilterNode; +import com.facebook.presto.sql.planner.plan.IndexJoinNode; +import com.facebook.presto.sql.planner.plan.IndexSourceNode; +import com.facebook.presto.sql.planner.plan.JoinNode; +import com.facebook.presto.sql.planner.plan.LimitNode; +import com.facebook.presto.sql.planner.plan.MarkDistinctNode; +import com.facebook.presto.sql.planner.plan.OutputNode; +import com.facebook.presto.sql.planner.plan.PlanNode; +import com.facebook.presto.sql.planner.plan.PlanVisitor; +import com.facebook.presto.sql.planner.plan.ProjectNode; +import com.facebook.presto.sql.planner.plan.RemoteSourceNode; +import com.facebook.presto.sql.planner.plan.RowNumberNode; +import com.facebook.presto.sql.planner.plan.SampleNode; +import com.facebook.presto.sql.planner.plan.SemiJoinNode; +import com.facebook.presto.sql.planner.plan.SortNode; +import com.facebook.presto.sql.planner.plan.TableCommitNode; +import com.facebook.presto.sql.planner.plan.TableScanNode; +import com.facebook.presto.sql.planner.plan.TableWriterNode; +import com.facebook.presto.sql.planner.plan.TableWriterNode.DeleteHandle; +import com.facebook.presto.sql.planner.plan.TopNNode; +import com.facebook.presto.sql.planner.plan.TopNRowNumberNode; +import com.facebook.presto.sql.planner.plan.UnionNode; +import com.facebook.presto.sql.planner.plan.UnnestNode; +import com.facebook.presto.sql.planner.plan.ValuesNode; +import com.facebook.presto.sql.planner.plan.WindowNode; +import com.facebook.presto.sql.planner.plan.WindowNode.Frame; +import com.facebook.presto.sql.relational.RowExpression; +import com.facebook.presto.sql.relational.SqlToRowExpressionTranslator; +import com.facebook.presto.sql.tree.BooleanLiteral; +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.ExpressionTreeRewriter; +import com.facebook.presto.sql.tree.FunctionCall; +import com.facebook.presto.sql.tree.QualifiedNameReference; +import com.google.common.base.Predicates; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableMap.Builder; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSetMultimap; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.common.collect.Multimap; +import com.google.common.collect.Ordering; +import com.google.common.collect.SetMultimap; +import com.google.common.primitives.Ints; +import io.airlift.log.Logger; +import io.airlift.slice.Slice; +import io.airlift.units.DataSize; + +import javax.annotation.Nullable; +import javax.inject.Inject; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Supplier; + +import static com.facebook.presto.SystemSessionProperties.getTaskAggregationConcurrency; +import static com.facebook.presto.SystemSessionProperties.getTaskHashBuildConcurrency; +import static com.facebook.presto.SystemSessionProperties.getTaskJoinConcurrency; +import static com.facebook.presto.SystemSessionProperties.getTaskWriterCount; +import static com.facebook.presto.operator.DistinctLimitOperator.DistinctLimitOperatorFactory; +import static com.facebook.presto.operator.InMemoryExchangeSourceOperator.InMemoryExchangeSourceOperatorFactory.createBroadcastDistribution; +import static com.facebook.presto.operator.InMemoryExchangeSourceOperator.InMemoryExchangeSourceOperatorFactory.createRandomDistribution; +import static com.facebook.presto.operator.TableCommitOperator.TableCommitOperatorFactory; +import static com.facebook.presto.operator.TableCommitOperator.TableCommitter; +import static com.facebook.presto.operator.TableWriterOperator.TableWriterOperatorFactory; +import static com.facebook.presto.operator.UnnestOperator.UnnestOperatorFactory; +import static com.facebook.presto.spi.StandardErrorCode.COMPILER_ERROR; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.sql.analyzer.ExpressionAnalyzer.getExpressionTypes; +import static com.facebook.presto.sql.analyzer.ExpressionAnalyzer.getExpressionTypesFromInput; +import static com.facebook.presto.sql.planner.plan.JoinNode.Type.FULL; +import static com.facebook.presto.sql.planner.plan.JoinNode.Type.RIGHT; +import static com.facebook.presto.sql.planner.plan.TableWriterNode.CreateHandle; +import static com.facebook.presto.sql.planner.plan.TableWriterNode.InsertHandle; +import static com.facebook.presto.sql.planner.plan.TableWriterNode.WriterTarget; +import static com.facebook.presto.util.ImmutableCollectors.toImmutableList; +import static com.google.common.base.Functions.forMap; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Predicates.in; +import static com.google.common.base.Predicates.not; +import static com.google.common.collect.Iterables.concat; +import static java.util.Collections.singleton; + +public class LocalExecutionPlanner +{ + private static final Logger log = Logger.get(LocalExecutionPlanner.class); + + private final Metadata metadata; + private final SqlParser sqlParser; + + private final PageSourceProvider pageSourceProvider; + private final IndexManager indexManager; + private final PageSinkManager pageSinkManager; + private final Supplier exchangeClientSupplier; + private final ExpressionCompiler compiler; + private final boolean interpreterEnabled; + private final DataSize maxIndexMemorySize; + private final IndexJoinLookupStats indexJoinLookupStats; + private final DataSize maxPartialAggregationMemorySize; + private final int writerCount; + private final int defaultConcurrency; + + @Inject + public LocalExecutionPlanner( + Metadata metadata, + SqlParser sqlParser, + PageSourceProvider pageSourceProvider, + IndexManager indexManager, + PageSinkManager pageSinkManager, + Supplier exchangeClientSupplier, + ExpressionCompiler compiler, + IndexJoinLookupStats indexJoinLookupStats, + CompilerConfig compilerConfig, + TaskManagerConfig taskManagerConfig) + { + checkNotNull(compilerConfig, "compilerConfig is null"); + this.pageSourceProvider = checkNotNull(pageSourceProvider, "pageSourceProvider is null"); + this.indexManager = checkNotNull(indexManager, "indexManager is null"); + this.exchangeClientSupplier = exchangeClientSupplier; + this.metadata = checkNotNull(metadata, "metadata is null"); + this.sqlParser = checkNotNull(sqlParser, "sqlParser is null"); + this.pageSinkManager = checkNotNull(pageSinkManager, "pageSinkManager is null"); + this.compiler = checkNotNull(compiler, "compiler is null"); + this.indexJoinLookupStats = checkNotNull(indexJoinLookupStats, "indexJoinLookupStats is null"); + this.maxIndexMemorySize = checkNotNull(taskManagerConfig, "taskManagerConfig is null").getMaxTaskIndexMemoryUsage(); + this.maxPartialAggregationMemorySize = taskManagerConfig.getMaxPartialAggregationMemoryUsage(); + this.writerCount = taskManagerConfig.getWriterCount(); + this.defaultConcurrency = taskManagerConfig.getTaskDefaultConcurrency(); + + interpreterEnabled = compilerConfig.isInterpreterEnabled(); + } + + public LocalExecutionPlan plan(Session session, + PlanNode plan, + List outputLayout, + Map types, + PlanDistribution distribution, + OutputFactory outputOperatorFactory) + { + LocalExecutionPlanContext context = new LocalExecutionPlanContext(session, types, distribution != PlanDistribution.SOURCE); + + PhysicalOperation physicalOperation = enforceLayout(outputLayout, context, plan.accept(new Visitor(session), context)); + + DriverFactory driverFactory = new DriverFactory( + context.isInputDriver(), + true, + ImmutableList.builder() + .addAll(physicalOperation.getOperatorFactories()) + .add(outputOperatorFactory.createOutputOperator(context.getNextOperatorId(), physicalOperation.getTypes())) + .build(), + context.getDriverInstanceCount()); + context.addDriverFactory(driverFactory); + + return new LocalExecutionPlan(context.getDriverFactories()); + } + + private static PhysicalOperation enforceLayout(List outputLayout, LocalExecutionPlanContext context, PhysicalOperation physicalOperation) + { + // are the symbols of the source in the same order as the sink expects? + boolean projectionMatchesOutput = physicalOperation.getLayout() + .entrySet().stream() + .sorted(Ordering.natural().onResultOf(Map.Entry::getValue)) + .map(Map.Entry::getKey) + .collect(toImmutableList()) + .equals(outputLayout); + + if (!projectionMatchesOutput) { + IdentityProjectionInfo mappings = computeIdentityMapping(outputLayout, physicalOperation.getLayout(), context.getTypes()); + OperatorFactory operatorFactory = new FilterAndProjectOperator.FilterAndProjectOperatorFactory( + context.getNextOperatorId(), + new GenericPageProcessor(FilterFunctions.TRUE_FUNCTION, mappings.getProjections()), + toTypes(mappings.getProjections())); + // NOTE: the generated output layout may not be completely accurate if the same field was projected as multiple inputs. + // However, this should not affect the operation of the sink. + physicalOperation = new PhysicalOperation(operatorFactory, mappings.getOutputLayout(), physicalOperation); + } + + return physicalOperation; + } + + private static class LocalExecutionPlanContext + { + private final Session session; + private final Map types; + private final boolean allowLocalParallel; + private final List driverFactories; + private final Optional indexSourceContext; + + private int nextOperatorId; + private boolean inputDriver = true; + private int driverInstanceCount = 1; + + public LocalExecutionPlanContext(Session session, Map types, boolean allowLocalParallel) + { + this(session, types, allowLocalParallel, new ArrayList<>(), Optional.empty()); + } + + private LocalExecutionPlanContext( + Session session, + Map types, + boolean allowLocalParallel, + List driverFactories, + Optional indexSourceContext) + { + this.session = session; + this.types = types; + this.allowLocalParallel = allowLocalParallel; + this.driverFactories = driverFactories; + this.indexSourceContext = indexSourceContext; + } + + public void addDriverFactory(DriverFactory driverFactory) + { + driverFactories.add(checkNotNull(driverFactory, "driverFactory is null")); + } + + private List getDriverFactories() + { + return ImmutableList.copyOf(driverFactories); + } + + public Session getSession() + { + return session; + } + + public Map getTypes() + { + return types; + } + + public Optional getIndexSourceContext() + { + return indexSourceContext; + } + + private int getNextOperatorId() + { + return nextOperatorId++; + } + + private boolean isInputDriver() + { + return inputDriver; + } + + private void setInputDriver(boolean inputDriver) + { + this.inputDriver = inputDriver; + } + + public LocalExecutionPlanContext createSubContext() + { + checkState(!indexSourceContext.isPresent(), "index build plan can not have sub-contexts"); + return new LocalExecutionPlanContext(session, types, allowLocalParallel, driverFactories, indexSourceContext); + } + + public LocalExecutionPlanContext createIndexSourceSubContext(IndexSourceContext indexSourceContext) + { + return new LocalExecutionPlanContext(session, types, false, driverFactories, Optional.of(indexSourceContext)); + } + + public boolean isAllowLocalParallel() + { + return allowLocalParallel; + } + + public int getDriverInstanceCount() + { + return driverInstanceCount; + } + + public void setDriverInstanceCount(int driverInstanceCount) + { + checkArgument(driverInstanceCount > 0, "driverInstanceCount must be > 0"); + this.driverInstanceCount = driverInstanceCount; + } + } + + private static class IndexSourceContext + { + private final SetMultimap indexLookupToProbeInput; + + public IndexSourceContext(SetMultimap indexLookupToProbeInput) + { + this.indexLookupToProbeInput = ImmutableSetMultimap.copyOf(checkNotNull(indexLookupToProbeInput, "indexLookupToProbeInput is null")); + } + + private SetMultimap getIndexLookupToProbeInput() + { + return indexLookupToProbeInput; + } + } + + public static class LocalExecutionPlan + { + private final List driverFactories; + + public LocalExecutionPlan(List driverFactories) + { + this.driverFactories = ImmutableList.copyOf(checkNotNull(driverFactories, "driverFactories is null")); + } + + public List getDriverFactories() + { + return driverFactories; + } + } + + private class Visitor + extends PlanVisitor + { + private final Session session; + + private Visitor(Session session) + { + this.session = session; + } + + @Override + public PhysicalOperation visitRemoteSource(RemoteSourceNode node, LocalExecutionPlanContext context) + { + List types = getSourceOperatorTypes(node, context.getTypes()); + + OperatorFactory operatorFactory = new ExchangeOperatorFactory(context.getNextOperatorId(), node.getId(), exchangeClientSupplier, types); + + return new PhysicalOperation(operatorFactory, makeLayout(node)); + } + + @Override + public PhysicalOperation visitOutput(OutputNode node, LocalExecutionPlanContext context) + { + return node.getSource().accept(this, context); + } + + @Override + public PhysicalOperation visitRowNumber(RowNumberNode node, LocalExecutionPlanContext context) + { + PhysicalOperation source = node.getSource().accept(this, context); + + List partitionBySymbols = node.getPartitionBy(); + List partitionChannels = getChannelsForSymbols(partitionBySymbols, source.getLayout()); + + List partitionTypes = partitionChannels.stream() + .map(channel -> source.getTypes().get(channel)) + .collect(toImmutableList()); + + ImmutableList.Builder outputChannels = ImmutableList.builder(); + for (int i = 0; i < source.getTypes().size(); i++) { + outputChannels.add(i); + } + + // compute the layout of the output from the window operator + ImmutableMap.Builder outputMappings = ImmutableMap.builder(); + outputMappings.putAll(source.getLayout()); + + // row number function goes in the last channel + int channel = source.getTypes().size(); + outputMappings.put(node.getRowNumberSymbol(), channel); + + Optional hashChannel = node.getHashSymbol().map(channelGetter(source)); + OperatorFactory operatorFactory = new RowNumberOperator.RowNumberOperatorFactory( + context.getNextOperatorId(), + source.getTypes(), + outputChannels.build(), + partitionChannels, + partitionTypes, + node.getMaxRowCountPerPartition(), + hashChannel, + 10_000); + return new PhysicalOperation(operatorFactory, outputMappings.build(), source); + } + + @Override + public PhysicalOperation visitTopNRowNumber(TopNRowNumberNode node, LocalExecutionPlanContext context) + { + PhysicalOperation source = node.getSource().accept(this, context); + + List partitionBySymbols = node.getPartitionBy(); + List partitionChannels = getChannelsForSymbols(partitionBySymbols, source.getLayout()); + List partitionTypes = partitionChannels.stream() + .map(channel -> source.getTypes().get(channel)) + .collect(toImmutableList()); + + List orderBySymbols = node.getOrderBy(); + List sortChannels = getChannelsForSymbols(orderBySymbols, source.getLayout()); + List sortOrder = orderBySymbols.stream() + .map(symbol -> node.getOrderings().get(symbol)) + .collect(toImmutableList()); + + ImmutableList.Builder outputChannels = ImmutableList.builder(); + for (int i = 0; i < source.getTypes().size(); i++) { + outputChannels.add(i); + } + + // compute the layout of the output from the window operator + ImmutableMap.Builder outputMappings = ImmutableMap.builder(); + outputMappings.putAll(source.getLayout()); + + if (!node.isPartial() || !partitionChannels.isEmpty()) { + // row number function goes in the last channel + int channel = source.getTypes().size(); + outputMappings.put(node.getRowNumberSymbol(), channel); + } + + Optional hashChannel = node.getHashSymbol().map(channelGetter(source)); + OperatorFactory operatorFactory = new TopNRowNumberOperator.TopNRowNumberOperatorFactory( + context.getNextOperatorId(), + source.getTypes(), + outputChannels.build(), + partitionChannels, + partitionTypes, + sortChannels, + sortOrder, + node.getMaxRowCountPerPartition(), + node.isPartial(), + hashChannel, + 1000); + + return new PhysicalOperation(operatorFactory, makeLayout(node), source); + } + + @Override + public PhysicalOperation visitWindow(WindowNode node, LocalExecutionPlanContext context) + { + PhysicalOperation source = node.getSource().accept(this, context); + + List partitionBySymbols = node.getPartitionBy(); + List orderBySymbols = node.getOrderBy(); + List partitionChannels = ImmutableList.copyOf(getChannelsForSymbols(partitionBySymbols, source.getLayout())); + List preGroupedChannels = ImmutableList.copyOf(getChannelsForSymbols(ImmutableList.copyOf(node.getPrePartitionedInputs()), source.getLayout())); + + List sortChannels = getChannelsForSymbols(orderBySymbols, source.getLayout()); + List sortOrder = orderBySymbols.stream() + .map(symbol -> node.getOrderings().get(symbol)) + .collect(toImmutableList()); + + Optional frameStartChannel = Optional.empty(); + Optional frameEndChannel = Optional.empty(); + Frame frame = node.getFrame(); + if (frame.getStartValue().isPresent()) { + frameStartChannel = Optional.of(source.getLayout().get(frame.getStartValue().get())); + } + if (frame.getEndValue().isPresent()) { + frameEndChannel = Optional.of(source.getLayout().get(frame.getEndValue().get())); + } + + ImmutableList.Builder outputChannels = ImmutableList.builder(); + for (int i = 0; i < source.getTypes().size(); i++) { + outputChannels.add(i); + } + + ImmutableList.Builder windowFunctionsBuilder = ImmutableList.builder(); + ImmutableList.Builder windowFunctionOutputSymbolsBuilder = ImmutableList.builder(); + for (Map.Entry entry : node.getWindowFunctions().entrySet()) { + ImmutableList.Builder arguments = ImmutableList.builder(); + for (Expression argument : entry.getValue().getArguments()) { + Symbol argumentSymbol = Symbol.fromQualifiedName(((QualifiedNameReference) argument).getName()); + arguments.add(source.getLayout().get(argumentSymbol)); + } + Symbol symbol = entry.getKey(); + Signature signature = node.getSignatures().get(symbol); + FunctionInfo functionInfo = metadata.getExactFunction(signature); + Type type = metadata.getType(functionInfo.getReturnType()); + windowFunctionsBuilder.add(functionInfo.bindWindowFunction(type, arguments.build())); + windowFunctionOutputSymbolsBuilder.add(symbol); + } + + List windowFunctionOutputSymbols = windowFunctionOutputSymbolsBuilder.build(); + List windowFunctions = windowFunctionsBuilder.build(); + + // compute the layout of the output from the window operator + ImmutableMap.Builder outputMappings = ImmutableMap.builder(); + for (Symbol symbol : node.getSource().getOutputSymbols()) { + outputMappings.put(symbol, source.getLayout().get(symbol)); + } + + // window functions go in remaining channels starting after the last channel from the source operator, one per channel + int channel = source.getTypes().size(); + for (Symbol symbol : windowFunctionOutputSymbols) { + outputMappings.put(symbol, channel); + channel++; + } + + OperatorFactory operatorFactory = new WindowOperatorFactory( + context.getNextOperatorId(), + source.getTypes(), + outputChannels.build(), + windowFunctions, + partitionChannels, + preGroupedChannels, + sortChannels, + sortOrder, + node.getPreSortedOrderPrefix(), + new FrameInfo(frame.getType(), frame.getStartType(), frameStartChannel, frame.getEndType(), frameEndChannel), + 10_000); + + return new PhysicalOperation(operatorFactory, outputMappings.build(), source); + } + + @Override + public PhysicalOperation visitTopN(TopNNode node, LocalExecutionPlanContext context) + { + PhysicalOperation source = node.getSource().accept(this, context); + + List orderBySymbols = node.getOrderBy(); + + List sortChannels = new ArrayList<>(); + List sortOrders = new ArrayList<>(); + for (Symbol symbol : orderBySymbols) { + sortChannels.add(source.getLayout().get(symbol)); + sortOrders.add(node.getOrderings().get(symbol)); + } + + OperatorFactory operator = new TopNOperatorFactory( + context.getNextOperatorId(), + source.getTypes(), + (int) node.getCount(), + sortChannels, + sortOrders, + node.isPartial()); + + return new PhysicalOperation(operator, source.getLayout(), source); + } + + @Override + public PhysicalOperation visitSort(SortNode node, LocalExecutionPlanContext context) + { + PhysicalOperation source = node.getSource().accept(this, context); + + List orderBySymbols = node.getOrderBy(); + + List orderByChannels = getChannelsForSymbols(orderBySymbols, source.getLayout()); + + ImmutableList.Builder sortOrder = ImmutableList.builder(); + for (Symbol symbol : orderBySymbols) { + sortOrder.add(node.getOrderings().get(symbol)); + } + + ImmutableList.Builder outputChannels = ImmutableList.builder(); + for (int i = 0; i < source.getTypes().size(); i++) { + outputChannels.add(i); + } + + OperatorFactory operator = new OrderByOperatorFactory( + context.getNextOperatorId(), + source.getTypes(), + outputChannels.build(), + 10_000, + orderByChannels, + sortOrder.build()); + + return new PhysicalOperation(operator, source.getLayout(), source); + } + + @Override + public PhysicalOperation visitLimit(LimitNode node, LocalExecutionPlanContext context) + { + PhysicalOperation source = node.getSource().accept(this, context); + + OperatorFactory operatorFactory = new LimitOperatorFactory(context.getNextOperatorId(), source.getTypes(), node.getCount()); + return new PhysicalOperation(operatorFactory, source.getLayout(), source); + } + + @Override + public PhysicalOperation visitDistinctLimit(DistinctLimitNode node, LocalExecutionPlanContext context) + { + PhysicalOperation source = node.getSource().accept(this, context); + + Optional hashChannel = node.getHashSymbol().map(channelGetter(source)); + List distinctChannels = getChannelsForSymbols(node.getDistinctSymbols(), source.getLayout()); + + OperatorFactory operatorFactory = new DistinctLimitOperatorFactory( + context.getNextOperatorId(), + source.getTypes(), + distinctChannels, + node.getLimit(), + hashChannel); + return new PhysicalOperation(operatorFactory, source.getLayout(), source); + } + + @Override + public PhysicalOperation visitAggregation(AggregationNode node, LocalExecutionPlanContext context) + { + if (node.getGroupBy().isEmpty()) { + PhysicalOperation source = node.getSource().accept(this, context); + return planGlobalAggregation(context.getNextOperatorId(), node, source); + } + + int aggregationConcurrency = getTaskAggregationConcurrency(session, defaultConcurrency); + if (node.getStep() == Step.PARTIAL || !context.isAllowLocalParallel() || context.getDriverInstanceCount() > 1 || aggregationConcurrency <= 1) { + PhysicalOperation source = node.getSource().accept(this, context); + return planGroupByAggregation(node, source, context, Optional.empty()); + } + + // create context for parallel operators + LocalExecutionPlanContext parallelContext = context.createSubContext(); + parallelContext.setDriverInstanceCount(aggregationConcurrency); + + // create context for source operators + LocalExecutionPlanContext sourceContext = parallelContext.createSubContext(); + parallelContext.setInputDriver(false); + + // plan aggregation source + PhysicalOperation source = node.getSource().accept(this, sourceContext); + + // add a broadcast exchange which copies every page into all parallel workers + InMemoryExchange exchange = new InMemoryExchange(source.getTypes(), aggregationConcurrency); + + // finish source operator + List factories = ImmutableList.builder() + .addAll(source.getOperatorFactories()) + .add(exchange.createSinkFactory(sourceContext.getNextOperatorId())) + .build(); + exchange.noMoreSinkFactories(); + parallelContext.addDriverFactory(new DriverFactory(sourceContext.isInputDriver(), false, factories)); + + // add broadcast exchange as first parallel operator + OperatorFactory exchangeSource = createBroadcastDistribution(parallelContext.getNextOperatorId(), exchange); + source = new PhysicalOperation(exchangeSource, source.getLayout()); + + // mask each parallel driver to only see one partition of groups + HashPartitionMaskOperatorFactory hashPartitionMask = new HashPartitionMaskOperatorFactory( + parallelContext.getNextOperatorId(), + aggregationConcurrency, + exchangeSource.getTypes(), + getChannelsForSymbols(ImmutableList.copyOf(node.getMasks().values()), source.getLayout()), + getChannelsForSymbols(ImmutableList.copyOf(node.getGroupBy()), source.getLayout()), + node.getHashSymbol().map(channelGetter(source))); + int defaultMaskChannel = hashPartitionMask.getDefaultMaskChannel(); + source = new PhysicalOperation(hashPartitionMask, source.getLayout(), source); + + // plan aggregation + PhysicalOperation operation = planGroupByAggregation(node, source, parallelContext, Optional.of(defaultMaskChannel)); + + // merge parallel tasks back into a single stream + operation = addInMemoryExchange(context, operation, parallelContext); + + return operation; + } + + @Override + public PhysicalOperation visitMarkDistinct(MarkDistinctNode node, LocalExecutionPlanContext context) + { + PhysicalOperation source = node.getSource().accept(this, context); + + List channels = getChannelsForSymbols(node.getDistinctSymbols(), source.getLayout()); + Optional hashChannel = node.getHashSymbol().map(channelGetter(source)); + MarkDistinctOperatorFactory operator = new MarkDistinctOperatorFactory(context.getNextOperatorId(), source.getTypes(), channels, hashChannel); + return new PhysicalOperation(operator, makeLayout(node), source); + } + + @Override + public PhysicalOperation visitSample(SampleNode node, LocalExecutionPlanContext context) + { + // For system sample, the splits are already filtered out, so no specific action needs to be taken here + if (node.getSampleType() == SampleNode.Type.SYSTEM) { + return node.getSource().accept(this, context); + } + + if (node.getSampleType() == SampleNode.Type.POISSONIZED) { + PhysicalOperation source = node.getSource().accept(this, context); + OperatorFactory operatorFactory = new SampleOperatorFactory(context.getNextOperatorId(), node.getSampleRatio(), node.isRescaled(), source.getTypes()); + checkState(node.getSampleWeightSymbol().isPresent(), "sample weight symbol missing"); + return new PhysicalOperation(operatorFactory, makeLayout(node), source); + } + + throw new UnsupportedOperationException("not yet implemented: " + node); + } + + @Override + public PhysicalOperation visitFilter(FilterNode node, LocalExecutionPlanContext context) + { + PlanNode sourceNode = node.getSource(); + + Expression filterExpression = node.getPredicate(); + + List projectionExpressions = new ArrayList<>(); + for (int i = 0; i < node.getOutputSymbols().size(); i++) { + Symbol symbol = node.getOutputSymbols().get(i); + projectionExpressions.add(new QualifiedNameReference(symbol.toQualifiedName())); + } + + List outputSymbols = node.getOutputSymbols(); + + return visitScanFilterAndProject(context, sourceNode, filterExpression, projectionExpressions, outputSymbols); + } + + @Override + public PhysicalOperation visitProject(ProjectNode node, LocalExecutionPlanContext context) + { + PlanNode sourceNode; + Expression filterExpression; + if (node.getSource() instanceof FilterNode) { + FilterNode filterNode = (FilterNode) node.getSource(); + sourceNode = filterNode.getSource(); + filterExpression = filterNode.getPredicate(); + } + else { + sourceNode = node.getSource(); + filterExpression = BooleanLiteral.TRUE_LITERAL; + } + + List projectionExpressions = node.getExpressions(); + + List outputSymbols = node.getOutputSymbols(); + + return visitScanFilterAndProject(context, sourceNode, filterExpression, projectionExpressions, outputSymbols); + } + + private PhysicalOperation visitScanFilterAndProject( + LocalExecutionPlanContext context, + PlanNode sourceNode, + Expression filterExpression, + List projectionExpressions, + List outputSymbols) + { + // if source is a table scan we fold it directly into the filter and project + // otherwise we plan it as a normal operator + Map sourceLayout; + Map sourceTypes; + List columns = null; + PhysicalOperation source = null; + if (sourceNode instanceof TableScanNode) { + TableScanNode tableScanNode = (TableScanNode) sourceNode; + + // extract the column handles and channel to type mapping + sourceLayout = new LinkedHashMap<>(); + sourceTypes = new LinkedHashMap<>(); + columns = new ArrayList<>(); + int channel = 0; + for (Symbol symbol : tableScanNode.getOutputSymbols()) { + columns.add(tableScanNode.getAssignments().get(symbol)); + + Integer input = channel; + sourceLayout.put(symbol, input); + + Type type = checkNotNull(context.getTypes().get(symbol), "No type for symbol %s", symbol); + sourceTypes.put(input, type); + + channel++; + } + } + else { + // plan source + source = sourceNode.accept(this, context); + sourceLayout = source.getLayout(); + sourceTypes = getInputTypes(source.getLayout(), source.getTypes()); + } + + // build output mapping + ImmutableMap.Builder outputMappingsBuilder = ImmutableMap.builder(); + for (int i = 0; i < outputSymbols.size(); i++) { + Symbol symbol = outputSymbols.get(i); + outputMappingsBuilder.put(symbol, i); + } + Map outputMappings = outputMappingsBuilder.build(); + + // compiler uses inputs instead of symbols, so rewrite the expressions first + SymbolToInputRewriter symbolToInputRewriter = new SymbolToInputRewriter(sourceLayout); + Expression rewrittenFilter = ExpressionTreeRewriter.rewriteWith(symbolToInputRewriter, filterExpression); + + List rewrittenProjections = new ArrayList<>(); + for (Expression projection : projectionExpressions) { + rewrittenProjections.add(ExpressionTreeRewriter.rewriteWith(symbolToInputRewriter, projection)); + } + + IdentityHashMap expressionTypes = getExpressionTypesFromInput( + context.getSession(), + metadata, + sqlParser, + sourceTypes, + concat(singleton(rewrittenFilter), rewrittenProjections)); + + RowExpression translatedFilter = toRowExpression(rewrittenFilter, expressionTypes); + List translatedProjections = rewrittenProjections.stream() + .map(expression -> toRowExpression(expression, expressionTypes)) + .collect(toImmutableList()); + + try { + if (columns != null) { + CursorProcessor cursorProcessor = compiler.compileCursorProcessor(translatedFilter, translatedProjections, sourceNode.getId()); + PageProcessor pageProcessor = compiler.compilePageProcessor(translatedFilter, translatedProjections); + + SourceOperatorFactory operatorFactory = new ScanFilterAndProjectOperator.ScanFilterAndProjectOperatorFactory( + context.getNextOperatorId(), + sourceNode.getId(), + pageSourceProvider, + cursorProcessor, + pageProcessor, + columns, + Lists.transform(rewrittenProjections, forMap(expressionTypes))); + + return new PhysicalOperation(operatorFactory, outputMappings); + } + else { + PageProcessor processor = compiler.compilePageProcessor(translatedFilter, translatedProjections); + + OperatorFactory operatorFactory = new FilterAndProjectOperator.FilterAndProjectOperatorFactory( + context.getNextOperatorId(), + processor, + Lists.transform(rewrittenProjections, forMap(expressionTypes))); + + return new PhysicalOperation(operatorFactory, outputMappings, source); + } + } + catch (RuntimeException e) { + if (!interpreterEnabled) { + throw new PrestoException(COMPILER_ERROR, "Compiler failed and interpreter is disabled", e); + } + + // compilation failed, use interpreter + log.error(e, "Compile failed for filter=%s projections=%s sourceTypes=%s error=%s", filterExpression, projectionExpressions, sourceTypes, e); + } + + FilterFunction filterFunction; + if (filterExpression != BooleanLiteral.TRUE_LITERAL) { + filterFunction = new InterpretedFilterFunction(filterExpression, context.getTypes(), sourceLayout, metadata, sqlParser, context.getSession()); + } + else { + filterFunction = FilterFunctions.TRUE_FUNCTION; + } + + List projectionFunctions = new ArrayList<>(); + for (Expression expression : projectionExpressions) { + ProjectionFunction function; + if (expression instanceof QualifiedNameReference) { + // fast path when we know it's a direct symbol reference + Symbol reference = Symbol.fromQualifiedName(((QualifiedNameReference) expression).getName()); + function = ProjectionFunctions.singleColumn(context.getTypes().get(reference), sourceLayout.get(reference)); + } + else { + function = new InterpretedProjectionFunction( + expression, + context.getTypes(), + sourceLayout, + metadata, + sqlParser, + context.getSession() + ); + } + projectionFunctions.add(function); + } + + if (columns != null) { + OperatorFactory operatorFactory = new ScanFilterAndProjectOperator.ScanFilterAndProjectOperatorFactory( + context.getNextOperatorId(), + sourceNode.getId(), + pageSourceProvider, + new GenericCursorProcessor(filterFunction, projectionFunctions), + new GenericPageProcessor(filterFunction, projectionFunctions), + columns, + toTypes(projectionFunctions)); + + return new PhysicalOperation(operatorFactory, outputMappings); + } + else { + OperatorFactory operatorFactory = new FilterAndProjectOperator.FilterAndProjectOperatorFactory( + context.getNextOperatorId(), + new GenericPageProcessor(filterFunction, projectionFunctions), + toTypes(projectionFunctions)); + return new PhysicalOperation(operatorFactory, outputMappings, source); + } + } + + private RowExpression toRowExpression(Expression expression, IdentityHashMap types) + { + return SqlToRowExpressionTranslator.translate(expression, types, metadata.getFunctionRegistry(), metadata.getTypeManager(), session, true); + } + + private Map getInputTypes(Map layout, List types) + { + Builder inputTypes = ImmutableMap.builder(); + for (Integer input : ImmutableSet.copyOf(layout.values())) { + Type type = types.get(input); + inputTypes.put(input, type); + } + return inputTypes.build(); + } + + @Override + public PhysicalOperation visitTableScan(TableScanNode node, LocalExecutionPlanContext context) + { + List columns = new ArrayList<>(); + for (Symbol symbol : node.getOutputSymbols()) { + columns.add(node.getAssignments().get(symbol)); + } + + List types = getSourceOperatorTypes(node, context.getTypes()); + OperatorFactory operatorFactory = new TableScanOperatorFactory(context.getNextOperatorId(), node.getId(), pageSourceProvider, types, columns); + return new PhysicalOperation(operatorFactory, makeLayout(node)); + } + + @Override + public PhysicalOperation visitValues(ValuesNode node, LocalExecutionPlanContext context) + { + List outputTypes = new ArrayList<>(); + + for (Symbol symbol : node.getOutputSymbols()) { + Type type = checkNotNull(context.getTypes().get(symbol), "No type for symbol %s", symbol); + outputTypes.add(type); + } + + if (node.getRows().isEmpty()) { + OperatorFactory operatorFactory = new ValuesOperatorFactory(context.getNextOperatorId(), outputTypes, ImmutableList.of()); + return new PhysicalOperation(operatorFactory, makeLayout(node)); + } + + PageBuilder pageBuilder = new PageBuilder(outputTypes); + for (List row : node.getRows()) { + pageBuilder.declarePosition(); + IdentityHashMap expressionTypes = getExpressionTypes( + context.getSession(), + metadata, + sqlParser, + ImmutableMap.of(), + ImmutableList.copyOf(row)); + for (int i = 0; i < row.size(); i++) { + // evaluate the literal value + Object result = ExpressionInterpreter.expressionInterpreter(row.get(i), metadata, context.getSession(), expressionTypes).evaluate(0); + BlockUtils.appendObject(outputTypes.get(i), pageBuilder.getBlockBuilder(i), result); + } + } + + OperatorFactory operatorFactory = new ValuesOperatorFactory(context.getNextOperatorId(), outputTypes, ImmutableList.of(pageBuilder.build())); + return new PhysicalOperation(operatorFactory, makeLayout(node)); + } + + @Override + public PhysicalOperation visitUnnest(UnnestNode node, LocalExecutionPlanContext context) + { + PhysicalOperation source = node.getSource().accept(this, context); + + ImmutableList.Builder replicateTypes = ImmutableList.builder(); + for (Symbol symbol : node.getReplicateSymbols()) { + replicateTypes.add(context.getTypes().get(symbol)); + } + List unnestSymbols = ImmutableList.copyOf(node.getUnnestSymbols().keySet()); + ImmutableList.Builder unnestTypes = ImmutableList.builder(); + for (Symbol symbol : unnestSymbols) { + unnestTypes.add(context.getTypes().get(symbol)); + } + Optional ordinalitySymbol = node.getOrdinalitySymbol(); + Optional ordinalityType = ordinalitySymbol.map(context.getTypes()::get); + ordinalityType.ifPresent(type -> checkState(type.equals(BIGINT), "Type of ordinalitySymbol must always be BIGINT.")); + + List replicateChannels = getChannelsForSymbols(node.getReplicateSymbols(), source.getLayout()); + List unnestChannels = getChannelsForSymbols(unnestSymbols, source.getLayout()); + + // Source channels are always laid out first, followed by the unnested symbols + ImmutableMap.Builder outputMappings = ImmutableMap.builder(); + int channel = 0; + for (Symbol symbol : node.getReplicateSymbols()) { + outputMappings.put(symbol, channel); + channel++; + } + for (Symbol symbol : unnestSymbols) { + for (Symbol unnestedSymbol : node.getUnnestSymbols().get(symbol)) { + outputMappings.put(unnestedSymbol, channel); + channel++; + } + } + if (ordinalitySymbol.isPresent()) { + outputMappings.put(ordinalitySymbol.get(), channel); + channel++; + } + OperatorFactory operatorFactory = new UnnestOperatorFactory( + context.getNextOperatorId(), + replicateChannels, + replicateTypes.build(), + unnestChannels, + unnestTypes.build(), + ordinalityType.isPresent()); + return new PhysicalOperation(operatorFactory, outputMappings.build(), source); + } + + private ImmutableMap makeLayout(PlanNode node) + { + Builder outputMappings = ImmutableMap.builder(); + int channel = 0; + for (Symbol symbol : node.getOutputSymbols()) { + outputMappings.put(symbol, channel); + channel++; + } + return outputMappings.build(); + } + + @Override + public PhysicalOperation visitIndexSource(IndexSourceNode node, LocalExecutionPlanContext context) + { + checkState(context.getIndexSourceContext().isPresent(), "Must be in an index source context"); + IndexSourceContext indexSourceContext = context.getIndexSourceContext().get(); + + SetMultimap indexLookupToProbeInput = indexSourceContext.getIndexLookupToProbeInput(); + checkState(indexLookupToProbeInput.keySet().equals(node.getLookupSymbols())); + + // Finalize the symbol lookup layout for the index source + List lookupSymbolSchema = ImmutableList.copyOf(node.getLookupSymbols()); + + // Identify how to remap the probe key Input to match the source index lookup layout + ImmutableList.Builder remappedProbeKeyChannelsBuilder = ImmutableList.builder(); + // Identify overlapping fields that can produce the same lookup symbol. + // We will filter incoming keys to ensure that overlapping fields will have the same value. + ImmutableList.Builder> overlappingFieldSetsBuilder = ImmutableList.builder(); + for (Symbol lookupSymbol : lookupSymbolSchema) { + Set potentialProbeInputs = indexLookupToProbeInput.get(lookupSymbol); + checkState(!potentialProbeInputs.isEmpty(), "Must have at least one source from the probe input"); + if (potentialProbeInputs.size() > 1) { + overlappingFieldSetsBuilder.add(FluentIterable.from(potentialProbeInputs) + .toSet()); + } + remappedProbeKeyChannelsBuilder.add(Iterables.getFirst(potentialProbeInputs, null)); + } + List> overlappingFieldSets = overlappingFieldSetsBuilder.build(); + List remappedProbeKeyChannels = remappedProbeKeyChannelsBuilder.build(); + Function probeKeyNormalizer = recordSet -> { + if (!overlappingFieldSets.isEmpty()) { + recordSet = new FieldSetFilteringRecordSet(recordSet, overlappingFieldSets); + } + return new MappedRecordSet(recordSet, remappedProbeKeyChannels); + }; + + // Declare the input and output schemas for the index and acquire the actual Index + List lookupSchema = Lists.transform(lookupSymbolSchema, forMap(node.getAssignments())); + List outputSchema = Lists.transform(node.getOutputSymbols(), forMap(node.getAssignments())); + ConnectorIndex index = indexManager.getIndex(node.getIndexHandle(), lookupSchema, outputSchema); + + List types = getSourceOperatorTypes(node, context.getTypes()); + OperatorFactory operatorFactory = new IndexSourceOperator.IndexSourceOperatorFactory(context.getNextOperatorId(), node.getId(), index, types, probeKeyNormalizer); + return new PhysicalOperation(operatorFactory, makeLayout(node)); + } + + /** + * This method creates a mapping from each index source lookup symbol (directly applied to the index) + * to the corresponding probe key Input + */ + private SetMultimap mapIndexSourceLookupSymbolToProbeKeyInput(IndexJoinNode node, Map probeKeyLayout) + { + Set indexJoinSymbols = FluentIterable.from(node.getCriteria()) + .transform(IndexJoinNode.EquiJoinClause::getIndex) + .toSet(); + + // Trace the index join symbols to the index source lookup symbols + // Map: Index join symbol => Index source lookup symbol + Map indexKeyTrace = IndexJoinOptimizer.IndexKeyTracer.trace(node.getIndexSource(), indexJoinSymbols); + + // Map the index join symbols to the probe key Input + Multimap indexToProbeKeyInput = HashMultimap.create(); + for (IndexJoinNode.EquiJoinClause clause : node.getCriteria()) { + indexToProbeKeyInput.put(clause.getIndex(), probeKeyLayout.get(clause.getProbe())); + } + + // Create the mapping from index source look up symbol to probe key Input + ImmutableSetMultimap.Builder builder = ImmutableSetMultimap.builder(); + for (Map.Entry entry : indexKeyTrace.entrySet()) { + Symbol indexJoinSymbol = entry.getKey(); + Symbol indexLookupSymbol = entry.getValue(); + builder.putAll(indexLookupSymbol, indexToProbeKeyInput.get(indexJoinSymbol)); + } + return builder.build(); + } + + @Override + public PhysicalOperation visitIndexJoin(IndexJoinNode node, LocalExecutionPlanContext context) + { + List clauses = node.getCriteria(); + + List probeSymbols = Lists.transform(clauses, IndexJoinNode.EquiJoinClause::getProbe); + List indexSymbols = Lists.transform(clauses, IndexJoinNode.EquiJoinClause::getIndex); + + // Plan probe side + PhysicalOperation probeSource = node.getProbeSource().accept(this, context); + List probeChannels = getChannelsForSymbols(probeSymbols, probeSource.getLayout()); + Optional probeHashChannel = node.getProbeHashSymbol().map(channelGetter(probeSource)); + + // The probe key channels will be handed to the index according to probeSymbol order + Map probeKeyLayout = new HashMap<>(); + for (int i = 0; i < probeSymbols.size(); i++) { + // Duplicate symbols can appear and we only need to take take one of the Inputs + probeKeyLayout.put(probeSymbols.get(i), i); + } + + // Plan the index source side + SetMultimap indexLookupToProbeInput = mapIndexSourceLookupSymbolToProbeKeyInput(node, probeKeyLayout); + LocalExecutionPlanContext indexContext = context.createIndexSourceSubContext(new IndexSourceContext(indexLookupToProbeInput)); + PhysicalOperation indexSource = node.getIndexSource().accept(this, indexContext); + List indexOutputChannels = getChannelsForSymbols(indexSymbols, indexSource.getLayout()); + Optional indexHashChannel = node.getIndexHashSymbol().map(channelGetter(indexSource)); + + // Identify just the join keys/channels needed for lookup by the index source (does not have to use all of them). + Set indexSymbolsNeededBySource = IndexJoinOptimizer.IndexKeyTracer.trace(node.getIndexSource(), ImmutableSet.copyOf(indexSymbols)).keySet(); + + Set lookupSourceInputChannels = FluentIterable.from(node.getCriteria()) + .filter(Predicates.compose(in(indexSymbolsNeededBySource), IndexJoinNode.EquiJoinClause::getIndex)) + .transform(IndexJoinNode.EquiJoinClause::getProbe) + .transform(forMap(probeKeyLayout)) + .toSet(); + + Optional dynamicTupleFilterFactory = Optional.empty(); + if (lookupSourceInputChannels.size() < probeKeyLayout.values().size()) { + int[] nonLookupInputChannels = Ints.toArray(FluentIterable.from(node.getCriteria()) + .filter(Predicates.compose(not(in(indexSymbolsNeededBySource)), IndexJoinNode.EquiJoinClause::getIndex)) + .transform(IndexJoinNode.EquiJoinClause::getProbe) + .transform(forMap(probeKeyLayout)) + .toList()); + int[] nonLookupOutputChannels = Ints.toArray(FluentIterable.from(node.getCriteria()) + .filter(Predicates.compose(not(in(indexSymbolsNeededBySource)), IndexJoinNode.EquiJoinClause::getIndex)) + .transform(IndexJoinNode.EquiJoinClause::getIndex) + .transform(forMap(indexSource.getLayout())) + .toList()); + + int filterOperatorId = indexContext.getNextOperatorId(); + dynamicTupleFilterFactory = Optional.of(new DynamicTupleFilterFactory(filterOperatorId, nonLookupInputChannels, nonLookupOutputChannels, indexSource.getTypes())); + } + + IndexBuildDriverFactoryProvider indexBuildDriverFactoryProvider = new IndexBuildDriverFactoryProvider( + indexContext.getNextOperatorId(), + indexContext.isInputDriver(), + indexSource.getOperatorFactories(), + dynamicTupleFilterFactory); + + IndexLookupSourceSupplier indexLookupSourceSupplier = new IndexLookupSourceSupplier( + lookupSourceInputChannels, + indexOutputChannels, + indexHashChannel, + indexSource.getTypes(), + indexBuildDriverFactoryProvider, + maxIndexMemorySize, + indexJoinLookupStats); + + ImmutableMap.Builder outputMappings = ImmutableMap.builder(); + outputMappings.putAll(probeSource.getLayout()); + + // inputs from index side of the join are laid out following the input from the probe side, + // so adjust the channel ids but keep the field layouts intact + int offset = probeSource.getTypes().size(); + for (Map.Entry entry : indexSource.getLayout().entrySet()) { + Integer input = entry.getValue(); + outputMappings.put(entry.getKey(), offset + input); + } + + OperatorFactory lookupJoinOperatorFactory; + switch (node.getType()) { + case INNER: + lookupJoinOperatorFactory = LookupJoinOperators.innerJoin(context.getNextOperatorId(), indexLookupSourceSupplier, probeSource.getTypes(), probeChannels, probeHashChannel); + break; + case SOURCE_OUTER: + lookupJoinOperatorFactory = LookupJoinOperators.probeOuterJoin(context.getNextOperatorId(), indexLookupSourceSupplier, probeSource.getTypes(), probeChannels, probeHashChannel); + break; + default: + throw new AssertionError("Unknown type: " + node.getType()); + } + return new PhysicalOperation(lookupJoinOperatorFactory, outputMappings.build(), probeSource); + } + + @Override + public PhysicalOperation visitJoin(JoinNode node, LocalExecutionPlanContext context) + { + List clauses = node.getCriteria(); + + List leftSymbols = Lists.transform(clauses, JoinNode.EquiJoinClause::getLeft); + List rightSymbols = Lists.transform(clauses, JoinNode.EquiJoinClause::getRight); + + switch (node.getType()) { + case INNER: + case LEFT: + case RIGHT: + case FULL: + return createJoinOperator(node, node.getLeft(), leftSymbols, node.getLeftHashSymbol(), node.getRight(), rightSymbols, node.getRightHashSymbol(), context); + default: + throw new UnsupportedOperationException("Unsupported join type: " + node.getType()); + } + } + + private PhysicalOperation createJoinOperator(JoinNode node, + PlanNode probeNode, + List probeSymbols, + Optional probeHashSymbol, + PlanNode buildNode, + List buildSymbols, + Optional buildHashSymbol, + LocalExecutionPlanContext context) + { + // Plan probe and introduce a projection to put all fields from the probe side into a single channel if necessary + PhysicalOperation probeSource; + LocalExecutionPlanContext parallelParentContext = null; + int joinConcurrency = getTaskJoinConcurrency(session, defaultConcurrency); + // currently we can not run joins with an outer build in parallel + if (!isBuildOuter(node) && context.isAllowLocalParallel() && context.getDriverInstanceCount() == 1 && joinConcurrency > 1) { + parallelParentContext = context; + context = context.createSubContext(); + probeSource = createInMemoryExchange(probeNode, context); + context.setDriverInstanceCount(joinConcurrency); + } + else { + probeSource = probeNode.accept(this, context); + } + List probeChannels = ImmutableList.copyOf(getChannelsForSymbols(probeSymbols, probeSource.getLayout())); + Optional probeHashChannel = probeHashSymbol.map(channelGetter(probeSource)); + + // do the same on the build side + LocalExecutionPlanContext buildContext = context.createSubContext(); + PhysicalOperation buildSource = buildNode.accept(this, buildContext); + List buildChannels = ImmutableList.copyOf(getChannelsForSymbols(buildSymbols, buildSource.getLayout())); + Optional buildHashChannel = buildHashSymbol.map(channelGetter(buildSource)); + + LookupSourceSupplier lookupSourceSupplier; + int hashBuildConcurrency = getTaskHashBuildConcurrency(session, defaultConcurrency); + if (isBuildOuter(node) || hashBuildConcurrency <= 1) { + HashBuilderOperatorFactory hashBuilderOperatorFactory = new HashBuilderOperatorFactory( + buildContext.getNextOperatorId(), + buildSource.getTypes(), + buildChannels, + buildHashChannel, + 10_000); + + context.addDriverFactory(new DriverFactory( + buildContext.isInputDriver(), + false, + ImmutableList.builder() + .addAll(buildSource.getOperatorFactories()) + .add(hashBuilderOperatorFactory) + .build())); + + lookupSourceSupplier = hashBuilderOperatorFactory.getLookupSourceSupplier(); + } + else { + // round partitionCount down to the last power of 2 + int parallelBuildCount = Integer.highestOneBit(hashBuildConcurrency); + + ParallelHashBuilder parallelHashBuilder = new ParallelHashBuilder( + buildSource.getTypes(), + buildChannels, + buildHashChannel, + 10_000, + parallelBuildCount); + + context.addDriverFactory(new DriverFactory( + buildContext.isInputDriver(), + false, + ImmutableList.builder() + .addAll(buildSource.getOperatorFactories()) + .add(parallelHashBuilder.getCollectOperatorFactory(buildContext.getNextOperatorId())) + .build())); + + context.addDriverFactory(new DriverFactory( + false, + false, + ImmutableList.of(parallelHashBuilder.getBuildOperatorFactory()), + parallelBuildCount)); + + lookupSourceSupplier = parallelHashBuilder.getLookupSourceSupplier(); + } + + ImmutableMap.Builder outputMappings = ImmutableMap.builder(); + outputMappings.putAll(probeSource.getLayout()); + + // inputs from build side of the join are laid out following the input from the probe side, + // so adjust the channel ids but keep the field layouts intact + int offset = probeSource.getTypes().size(); + for (Map.Entry entry : buildSource.getLayout().entrySet()) { + Integer input = entry.getValue(); + outputMappings.put(entry.getKey(), offset + input); + } + + OperatorFactory operator = createJoinOperator(node.getType(), lookupSourceSupplier, probeSource.getTypes(), probeChannels, probeHashChannel, context); + PhysicalOperation operation = new PhysicalOperation(operator, outputMappings.build(), probeSource); + + // merge parallel joiners back into a single stream + if (parallelParentContext != null) { + operation = addInMemoryExchange(parallelParentContext, operation, context); + } + + return operation; + } + + private boolean isBuildOuter(JoinNode node) + { + return node.getType() == RIGHT || node.getType() == FULL; + } + + private OperatorFactory createJoinOperator( + JoinNode.Type type, + LookupSourceSupplier lookupSourceSupplier, + List probeTypes, + List probeJoinChannels, + Optional probeHashChannel, + LocalExecutionPlanContext context) + { + switch (type) { + case INNER: + return LookupJoinOperators.innerJoin(context.getNextOperatorId(), lookupSourceSupplier, probeTypes, probeJoinChannels, probeHashChannel); + case LEFT: + return LookupJoinOperators.probeOuterJoin(context.getNextOperatorId(), lookupSourceSupplier, probeTypes, probeJoinChannels, probeHashChannel); + case RIGHT: + return LookupJoinOperators.lookupOuterJoin(context.getNextOperatorId(), lookupSourceSupplier, probeTypes, probeJoinChannels, probeHashChannel); + case FULL: + return LookupJoinOperators.fullOuterJoin(context.getNextOperatorId(), lookupSourceSupplier, probeTypes, probeJoinChannels, probeHashChannel); + default: + throw new UnsupportedOperationException("Unsupported join type: " + type); + } + } + + @Override + public PhysicalOperation visitSemiJoin(SemiJoinNode node, LocalExecutionPlanContext context) + { + // introduce a projection to put all fields from the probe side into a single channel if necessary + PhysicalOperation probeSource; + LocalExecutionPlanContext parallelParentContext = null; + int joinConcurrency = getTaskJoinConcurrency(session, defaultConcurrency); + if (context.isAllowLocalParallel() && context.getDriverInstanceCount() == 1 && joinConcurrency > 1) { + parallelParentContext = context; + context = context.createSubContext(); + probeSource = createInMemoryExchange(node.getSource(), context); + context.setDriverInstanceCount(joinConcurrency); + } + else { + probeSource = node.getSource().accept(this, context); + } + + // do the same on the build side + LocalExecutionPlanContext buildContext = context.createSubContext(); + PhysicalOperation buildSource = node.getFilteringSource().accept(this, buildContext); + + int probeChannel = probeSource.getLayout().get(node.getSourceJoinSymbol()); + int buildChannel = buildSource.getLayout().get(node.getFilteringSourceJoinSymbol()); + + Optional probeHashChannel = node.getSourceHashSymbol().map(channelGetter(probeSource)); + Optional buildHashChannel = node.getFilteringSourceHashSymbol().map(channelGetter(buildSource)); + + SetBuilderOperatorFactory setBuilderOperatorFactory = new SetBuilderOperatorFactory(buildContext.getNextOperatorId(), buildSource.getTypes(), buildChannel, buildHashChannel, 10_000); + SetSupplier setProvider = setBuilderOperatorFactory.getSetProvider(); + DriverFactory buildDriverFactory = new DriverFactory( + buildContext.isInputDriver(), + false, + ImmutableList.builder() + .addAll(buildSource.getOperatorFactories()) + .add(setBuilderOperatorFactory) + .build()); + context.addDriverFactory(buildDriverFactory); + + // Source channels are always laid out first, followed by the boolean output symbol + Map outputMappings = ImmutableMap.builder() + .putAll(probeSource.getLayout()) + .put(node.getSemiJoinOutput(), probeSource.getLayout().size()) + .build(); + + HashSemiJoinOperatorFactory operator = new HashSemiJoinOperatorFactory(context.getNextOperatorId(), setProvider, probeSource.getTypes(), probeChannel, probeHashChannel); + PhysicalOperation operation = new PhysicalOperation(operator, outputMappings, probeSource); + + // merge parallel joiners back into a single stream + if (parallelParentContext != null) { + operation = addInMemoryExchange(parallelParentContext, operation, context); + } + + return operation; + } + + @Override + public PhysicalOperation visitTableWriter(TableWriterNode node, LocalExecutionPlanContext context) + { + // serialize writes by forcing data through a single writer + PhysicalOperation exchange = createInMemoryExchange(node.getSource(), context); + + Optional sampleWeightChannel = node.getSampleWeightSymbol().map(exchange::symbolToChannel); + + // Set table writer count + context.setDriverInstanceCount(getTaskWriterCount(session, writerCount)); + + List inputChannels = node.getColumns().stream() + .map(exchange::symbolToChannel) + .collect(toImmutableList()); + + OperatorFactory operatorFactory = new TableWriterOperatorFactory(context.getNextOperatorId(), pageSinkManager, node.getTarget(), inputChannels, sampleWeightChannel); + + Map layout = ImmutableMap.builder() + .put(node.getOutputSymbols().get(0), 0) + .put(node.getOutputSymbols().get(1), 1) + .build(); + + return new PhysicalOperation(operatorFactory, layout, exchange); + } + + private PhysicalOperation createInMemoryExchange(PlanNode node, LocalExecutionPlanContext context) + { + LocalExecutionPlanContext subContext = context.createSubContext(); + PhysicalOperation source = node.accept(this, subContext); + + return addInMemoryExchange(context, source, subContext); + } + + private PhysicalOperation addInMemoryExchange(LocalExecutionPlanContext context, PhysicalOperation source, LocalExecutionPlanContext sourceContext) + { + InMemoryExchange exchange = new InMemoryExchange(source.getTypes()); + + // create exchange sink + List factories = ImmutableList.builder() + .addAll(source.getOperatorFactories()) + .add(exchange.createSinkFactory(sourceContext.getNextOperatorId())) + .build(); + + // add sub-context to current context + context.addDriverFactory(new DriverFactory(sourceContext.isInputDriver(), false, factories, sourceContext.getDriverInstanceCount())); + + exchange.noMoreSinkFactories(); + + // the main driver is not an input: the source is the input for the plan + context.setInputDriver(false); + + // add exchange source as first operator in the current context + OperatorFactory factory = createRandomDistribution(context.getNextOperatorId(), exchange); + return new PhysicalOperation(factory, source.getLayout()); + } + + @Override + public PhysicalOperation visitTableCommit(TableCommitNode node, LocalExecutionPlanContext context) + { + PhysicalOperation source = node.getSource().accept(this, context); + + OperatorFactory operatorFactory = new TableCommitOperatorFactory(context.getNextOperatorId(), createTableCommitter(node, metadata)); + Map layout = ImmutableMap.of(node.getOutputSymbols().get(0), 0); + + return new PhysicalOperation(operatorFactory, layout, source); + } + + @Override + public PhysicalOperation visitDelete(DeleteNode node, LocalExecutionPlanContext context) + { + PhysicalOperation source = node.getSource().accept(this, context); + + OperatorFactory operatorFactory = new DeleteOperatorFactory(context.getNextOperatorId(), source.getLayout().get(node.getRowId())); + + Map layout = ImmutableMap.builder() + .put(node.getOutputSymbols().get(0), 0) + .put(node.getOutputSymbols().get(1), 1) + .build(); + + return new PhysicalOperation(operatorFactory, layout, source); + } + + @Override + public PhysicalOperation visitUnion(UnionNode node, LocalExecutionPlanContext context) + { + List types = getSourceOperatorTypes(node, context.getTypes()); + InMemoryExchange inMemoryExchange = new InMemoryExchange(types); + + for (int i = 0; i < node.getSources().size(); i++) { + PlanNode subplan = node.getSources().get(i); + List expectedLayout = node.sourceOutputLayout(i); + + LocalExecutionPlanContext subContext = context.createSubContext(); + PhysicalOperation source = subplan.accept(this, subContext); + List operatorFactories = new ArrayList<>(source.getOperatorFactories()); + + boolean projectionMatchesOutput = source.getLayout() + .entrySet().stream() + .sorted(Ordering.natural().onResultOf(Map.Entry::getValue)) + .map(Map.Entry::getKey) + .collect(toImmutableList()) + .equals(expectedLayout); + + if (!projectionMatchesOutput) { + IdentityProjectionInfo mappings = computeIdentityMapping(expectedLayout, source.getLayout(), context.getTypes()); + operatorFactories.add(new FilterAndProjectOperator.FilterAndProjectOperatorFactory( + subContext.getNextOperatorId(), + new GenericPageProcessor(FilterFunctions.TRUE_FUNCTION, mappings.getProjections()), + toTypes(mappings.getProjections()))); + } + + operatorFactories.add(inMemoryExchange.createSinkFactory(subContext.getNextOperatorId())); + + DriverFactory driverFactory = new DriverFactory(subContext.isInputDriver(), false, operatorFactories); + context.addDriverFactory(driverFactory); + } + inMemoryExchange.noMoreSinkFactories(); + + // the main driver is not an input... the union sources are the input for the plan + context.setInputDriver(false); + + return new PhysicalOperation(createRandomDistribution(context.getNextOperatorId(), inMemoryExchange), makeLayout(node)); + } + + @Override + protected PhysicalOperation visitPlan(PlanNode node, LocalExecutionPlanContext context) + { + throw new UnsupportedOperationException("not yet implemented"); + } + + private List getSourceOperatorTypes(PlanNode node, Map types) + { + return getSymbolTypes(node.getOutputSymbols(), types); + } + + private List getSymbolTypes(List symbols, Map types) + { + return symbols.stream() + .map(types::get) + .collect(toImmutableList()); + } + + private AccumulatorFactory buildAccumulatorFactory( + PhysicalOperation source, + Signature function, + FunctionCall call, + @Nullable Symbol mask, + Optional defaultMaskChannel, + Optional sampleWeight, + double confidence) + { + List arguments = new ArrayList<>(); + for (Expression argument : call.getArguments()) { + Symbol argumentSymbol = Symbol.fromQualifiedName(((QualifiedNameReference) argument).getName()); + arguments.add(source.getLayout().get(argumentSymbol)); + } + + Optional maskChannel = defaultMaskChannel; + + if (mask != null) { + maskChannel = Optional.of(source.getLayout().get(mask)); + } + + Optional sampleWeightChannel = Optional.empty(); + if (sampleWeight.isPresent()) { + sampleWeightChannel = Optional.of(source.getLayout().get(sampleWeight.get())); + } + + return metadata.getExactFunction(function).getAggregationFunction().bind(arguments, maskChannel, sampleWeightChannel, confidence); + } + + private PhysicalOperation planGlobalAggregation(int operatorId, AggregationNode node, PhysicalOperation source) + { + int outputChannel = 0; + ImmutableMap.Builder outputMappings = ImmutableMap.builder(); + List accumulatorFactories = new ArrayList<>(); + for (Map.Entry entry : node.getAggregations().entrySet()) { + Symbol symbol = entry.getKey(); + + accumulatorFactories.add(buildAccumulatorFactory(source, + node.getFunctions().get(symbol), + entry.getValue(), + node.getMasks().get(entry.getKey()), + Optional.empty(), + node.getSampleWeight(), + node.getConfidence())); + outputMappings.put(symbol, outputChannel); // one aggregation per channel + outputChannel++; + } + + OperatorFactory operatorFactory = new AggregationOperatorFactory(operatorId, node.getStep(), accumulatorFactories); + return new PhysicalOperation(operatorFactory, outputMappings.build(), source); + } + + private PhysicalOperation planGroupByAggregation(AggregationNode node, PhysicalOperation source, LocalExecutionPlanContext context, Optional defaultMaskChannel) + { + List groupBySymbols = node.getGroupBy(); + + List aggregationOutputSymbols = new ArrayList<>(); + List accumulatorFactories = new ArrayList<>(); + for (Map.Entry entry : node.getAggregations().entrySet()) { + Symbol symbol = entry.getKey(); + + accumulatorFactories.add(buildAccumulatorFactory(source, node.getFunctions().get(symbol), entry.getValue(), node.getMasks().get(entry.getKey()), defaultMaskChannel, node.getSampleWeight(), node.getConfidence())); + aggregationOutputSymbols.add(symbol); + } + + ImmutableMap.Builder outputMappings = ImmutableMap.builder(); + // add group-by key fields each in a separate channel + int channel = 0; + for (Symbol symbol : groupBySymbols) { + outputMappings.put(symbol, channel); + channel++; + } + + // hashChannel follows the group by channels + if (node.getHashSymbol().isPresent()) { + outputMappings.put(node.getHashSymbol().get(), channel++); + } + + // aggregations go in following channels + for (Symbol symbol : aggregationOutputSymbols) { + outputMappings.put(symbol, channel); + channel++; + } + + List groupByChannels = getChannelsForSymbols(groupBySymbols, source.getLayout()); + List groupByTypes = groupByChannels.stream() + .map(entry -> source.getTypes().get(entry)) + .collect(toImmutableList()); + + Optional hashChannel = node.getHashSymbol().map(channelGetter(source)); + + OperatorFactory operatorFactory = new HashAggregationOperatorFactory( + context.getNextOperatorId(), + groupByTypes, + groupByChannels, + node.getStep(), + accumulatorFactories, + defaultMaskChannel, + hashChannel, + 10_000, + maxPartialAggregationMemorySize); + + return new PhysicalOperation(operatorFactory, outputMappings.build(), source); + } + } + + public static List toTypes(List projections) + { + ImmutableList.Builder builder = ImmutableList.builder(); + for (ProjectionFunction projection : projections) { + builder.add(projection.getType()); + } + return builder.build(); + } + + private static TableCommitter createTableCommitter(TableCommitNode node, Metadata metadata) + { + WriterTarget target = node.getTarget(); + return new TableCommitter() + { + @Override + public void commitTable(Collection fragments) + { + if (target instanceof CreateHandle) { + metadata.commitCreateTable(((CreateHandle) target).getHandle(), fragments); + } + else if (target instanceof InsertHandle) { + metadata.commitInsert(((InsertHandle) target).getHandle(), fragments); + } + else if (target instanceof DeleteHandle) { + metadata.commitDelete(((DeleteHandle) target).getHandle(), fragments); + } + else { + throw new AssertionError("Unhandled target type: " + target.getClass().getName()); + } + } + + @Override + public void rollbackTable() + { + if (target instanceof CreateHandle) { + metadata.rollbackCreateTable(((CreateHandle) target).getHandle()); + } + else if (target instanceof InsertHandle) { + metadata.rollbackInsert(((InsertHandle) target).getHandle()); + } + else if (target instanceof DeleteHandle) { + metadata.rollbackDelete(((DeleteHandle) target).getHandle()); + } + else { + throw new AssertionError("Unhandled target type: " + target.getClass().getName()); + } + } + }; + } + + private static IdentityProjectionInfo computeIdentityMapping(List symbols, Map inputLayout, Map types) + { + Map outputMappings = new HashMap<>(); + List projections = new ArrayList<>(); + + int channel = 0; + for (Symbol symbol : symbols) { + ProjectionFunction function = ProjectionFunctions.singleColumn(types.get(symbol), inputLayout.get(symbol)); + projections.add(function); + if (!outputMappings.containsKey(symbol)) { + outputMappings.put(symbol, channel); + channel++; + } + } + + return new IdentityProjectionInfo(ImmutableMap.copyOf(outputMappings), projections); + } + + private static List getChannelsForSymbols(List symbols, Map layout) + { + ImmutableList.Builder builder = ImmutableList.builder(); + for (Symbol symbol : symbols) { + builder.add(layout.get(symbol)); + } + return builder.build(); + } + + private static class IdentityProjectionInfo + { + private final Map layout; + private final List projections; + + public IdentityProjectionInfo(Map outputLayout, List projections) + { + this.layout = checkNotNull(outputLayout, "outputLayout is null"); + this.projections = checkNotNull(projections, "projections is null"); + } + + public Map getOutputLayout() + { + return layout; + } + + public List getProjections() + { + return projections; + } + } + + private static Function channelGetter(PhysicalOperation source) + { + return input -> { + checkArgument(source.getLayout().containsKey(input)); + return source.getLayout().get(input); + }; + } + + /** + * Encapsulates an physical operator plus the mapping of logical symbols to channel/field + */ + private static class PhysicalOperation + { + private final List operatorFactories; + private final Map layout; + private final List types; + + public PhysicalOperation(OperatorFactory operatorFactory, Map layout) + { + checkNotNull(operatorFactory, "operatorFactory is null"); + checkNotNull(layout, "layout is null"); + + this.operatorFactories = ImmutableList.of(operatorFactory); + this.layout = ImmutableMap.copyOf(layout); + this.types = operatorFactory.getTypes(); + } + + public PhysicalOperation(OperatorFactory operatorFactory, Map layout, PhysicalOperation source) + { + checkNotNull(operatorFactory, "operatorFactory is null"); + checkNotNull(layout, "layout is null"); + checkNotNull(source, "source is null"); + + this.operatorFactories = ImmutableList.builder().addAll(source.getOperatorFactories()).add(operatorFactory).build(); + this.layout = ImmutableMap.copyOf(layout); + this.types = operatorFactory.getTypes(); + } + + public int symbolToChannel(Symbol input) + { + checkArgument(layout.containsKey(input)); + return layout.get(input); + } + + public List getTypes() + { + return types; + } + + public Map getLayout() + { + return layout; + } + + private List getOperatorFactories() + { + return operatorFactories; + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/LogicalPlanner.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/LogicalPlanner.java new file mode 100644 index 00000000..a6a57004 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/LogicalPlanner.java @@ -0,0 +1,226 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner; + +import com.facebook.presto.Session; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.metadata.QualifiedTableName; +import com.facebook.presto.metadata.TableHandle; +import com.facebook.presto.metadata.TableMetadata; +import com.facebook.presto.spi.ColumnMetadata; +import com.facebook.presto.spi.ConnectorTableMetadata; +import com.facebook.presto.spi.CreateTableOption; +import com.facebook.presto.sql.analyzer.Analysis; +import com.facebook.presto.sql.analyzer.Field; +import com.facebook.presto.sql.analyzer.TupleDescriptor; +import com.facebook.presto.sql.planner.optimizations.PlanOptimizer; +import com.facebook.presto.sql.planner.plan.DeleteNode; +import com.facebook.presto.sql.planner.plan.OutputNode; +import com.facebook.presto.sql.planner.plan.PlanNode; +import com.facebook.presto.sql.planner.plan.TableCommitNode; +import com.facebook.presto.sql.planner.plan.TableWriterNode; +import com.google.common.collect.ImmutableList; + +import java.util.List; +import java.util.Optional; + +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.VarbinaryType.VARBINARY; +import static com.facebook.presto.sql.planner.plan.TableWriterNode.CreateName; +import static com.facebook.presto.sql.planner.plan.TableWriterNode.InsertReference; +import static com.facebook.presto.sql.planner.plan.TableWriterNode.WriterTarget; +import static com.facebook.presto.util.ImmutableCollectors.toImmutableList; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +public class LogicalPlanner +{ + private final PlanNodeIdAllocator idAllocator; + + private final Session session; + private final List planOptimizers; + private final SymbolAllocator symbolAllocator = new SymbolAllocator(); + private final Metadata metadata; + + public LogicalPlanner(Session session, + List planOptimizers, + PlanNodeIdAllocator idAllocator, + Metadata metadata) + { + checkNotNull(session, "session is null"); + checkNotNull(planOptimizers, "planOptimizers is null"); + checkNotNull(idAllocator, "idAllocator is null"); + checkNotNull(metadata, "metadata is null"); + + this.session = session; + this.planOptimizers = planOptimizers; + this.idAllocator = idAllocator; + this.metadata = metadata; + } + + public Plan plan(Analysis analysis) + { + RelationPlan plan; + if (analysis.getCreateTableDestination().isPresent()) { + plan = createTableCreationPlan(analysis); + } + else if (analysis.getInsertTarget().isPresent()) { + plan = createInsertPlan(analysis); + } + else if (analysis.getDelete().isPresent()) { + plan = createDeletePlan(analysis); + } + else { + plan = createRelationPlan(analysis); + } + + PlanNode root = createOutputPlan(plan, analysis); + + // make sure we produce a valid plan. This is mainly to catch programming errors + PlanSanityChecker.validate(root); + + for (PlanOptimizer optimizer : planOptimizers) { + root = optimizer.optimize(root, session, symbolAllocator.getTypes(), symbolAllocator, idAllocator); + checkNotNull(root, "%s returned a null plan", optimizer.getClass().getName()); + } + + // make sure we produce a valid plan after optimizations run. This is mainly to catch programming errors + PlanSanityChecker.validate(root); + + return new Plan(root, symbolAllocator); + } + + private RelationPlan createTableCreationPlan(Analysis analysis) + { + QualifiedTableName destination = analysis.getCreateTableDestination().get(); + + RelationPlan plan = createRelationPlan(analysis); + + TableMetadata tableMetadata = createTableMetadata(destination, getOutputTableColumns(plan, analysis.getCreateTableOption()), plan.getSampleWeight().isPresent()); + checkState(!plan.getSampleWeight().isPresent() || metadata.canCreateSampledTables(session, destination.getCatalogName()), "Cannot write sampled data to a store that doesn't support sampling"); + + return createTableWriterPlan( + analysis, + plan, + tableMetadata, + new CreateName(destination.getCatalogName(), tableMetadata)); + } + + private RelationPlan createInsertPlan(Analysis analysis) + { + TableHandle target = analysis.getInsertTarget().get(); + + return createTableWriterPlan( + analysis, + createRelationPlan(analysis), + metadata.getTableMetadata(target), + new InsertReference(target, analysis.getInsertOption())); + } + + private RelationPlan createTableWriterPlan(Analysis analysis, RelationPlan plan, TableMetadata tableMetadata, WriterTarget target) + { + List writerOutputs = ImmutableList.of( + symbolAllocator.newSymbol("partialrows", BIGINT), + symbolAllocator.newSymbol("fragment", VARBINARY)); + + TableWriterNode writerNode = new TableWriterNode( + idAllocator.getNextId(), + plan.getRoot(), + target, + plan.getOutputSymbols(), + getVisibleColumnNames(tableMetadata), + writerOutputs, + plan.getSampleWeight()); + + List outputs = ImmutableList.of(symbolAllocator.newSymbol("rows", BIGINT)); + + TableCommitNode commitNode = new TableCommitNode( + idAllocator.getNextId(), + writerNode, + target, + outputs); + + return new RelationPlan(commitNode, analysis.getOutputDescriptor(), outputs, Optional.empty()); + } + + private RelationPlan createDeletePlan(Analysis analysis) + { + QueryPlanner planner = new QueryPlanner(analysis, symbolAllocator, idAllocator, metadata, session); + DeleteNode deleteNode = planner.planDelete(analysis.getDelete().get()); + + List outputs = ImmutableList.of(symbolAllocator.newSymbol("rows", BIGINT)); + TableCommitNode commitNode = new TableCommitNode(idAllocator.getNextId(), deleteNode, deleteNode.getTarget(), outputs); + + return new RelationPlan(commitNode, analysis.getOutputDescriptor(), commitNode.getOutputSymbols(), Optional.empty()); + } + + private PlanNode createOutputPlan(RelationPlan plan, Analysis analysis) + { + ImmutableList.Builder outputs = ImmutableList.builder(); + ImmutableList.Builder names = ImmutableList.builder(); + + int columnNumber = 0; + TupleDescriptor outputDescriptor = analysis.getOutputDescriptor(); + for (Field field : outputDescriptor.getVisibleFields()) { + String name = field.getName().orElse("_col" + columnNumber); + names.add(name); + + int fieldIndex = outputDescriptor.indexOf(field); + Symbol symbol = plan.getSymbol(fieldIndex); + outputs.add(symbol); + + columnNumber++; + } + + return new OutputNode(idAllocator.getNextId(), plan.getRoot(), names.build(), outputs.build()); + } + + private RelationPlan createRelationPlan(Analysis analysis) + { + return new RelationPlanner(analysis, symbolAllocator, idAllocator, metadata, session) + .process(analysis.getQuery(), null); + } + + private TableMetadata createTableMetadata(QualifiedTableName table, List columns, boolean sampled) + { + String owner = session.getUser(); + ConnectorTableMetadata metadata = new ConnectorTableMetadata(table.asSchemaTableName(), columns, owner, sampled); + // TODO: first argument should actually be connectorId + return new TableMetadata(table.getCatalogName(), metadata); + } + + private static List getOutputTableColumns(RelationPlan plan, CreateTableOption createTableOption) + { + ImmutableList.Builder columns = ImmutableList.builder(); + boolean isPartition = createTableOption.isPartition(); + List partitionColumns = createTableOption.getPartitions(); + for (Field field : plan.getDescriptor().getVisibleFields()) { + // specify the partition columns + boolean isColumnPartitioned = false; + if (isPartition && partitionColumns.contains(field.getName().get())) { + isColumnPartitioned = true; + } + columns.add(new ColumnMetadata(field.getName().get(), field.getType(), isColumnPartitioned)); + } + return columns.build(); + } + + private static List getVisibleColumnNames(TableMetadata tableMetadata) + { + return tableMetadata.getColumns().stream() + .filter(column -> !column.isHidden()) + .map(ColumnMetadata::getName) + .collect(toImmutableList()); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/LookupSymbolResolver.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/LookupSymbolResolver.java new file mode 100644 index 00000000..2a983308 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/LookupSymbolResolver.java @@ -0,0 +1,60 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner; + +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.sql.tree.QualifiedNameReference; +import com.google.common.collect.ImmutableMap; +import io.airlift.slice.Slices; + +import java.util.Map; + +import static com.google.common.base.Preconditions.checkArgument; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Objects.requireNonNull; + +public class LookupSymbolResolver + implements SymbolResolver +{ + private final Map assignments; + private final Map bindings; + + public LookupSymbolResolver(Map assignments, Map bindings) + { + requireNonNull(assignments, "assignments is null"); + requireNonNull(bindings, "bindings is null"); + + this.assignments = ImmutableMap.copyOf(assignments); + this.bindings = ImmutableMap.copyOf(bindings); + } + + @Override + public Object getValue(Symbol symbol) + { + ColumnHandle column = assignments.get(symbol); + checkArgument(column != null, "Missing column assignment for %s", symbol); + + if (!bindings.containsKey(column)) { + return new QualifiedNameReference(symbol.toQualifiedName()); + } + + Object value = bindings.get(column); + + if (value instanceof String) { + return Slices.wrappedBuffer(((String) value).getBytes(UTF_8)); + } + + return value; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/NoOpSymbolResolver.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/NoOpSymbolResolver.java new file mode 100644 index 00000000..29e03ad8 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/NoOpSymbolResolver.java @@ -0,0 +1,28 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner; + +import com.facebook.presto.sql.tree.QualifiedNameReference; + +public class NoOpSymbolResolver + implements SymbolResolver +{ + public static final NoOpSymbolResolver INSTANCE = new NoOpSymbolResolver(); + + @Override + public Object getValue(Symbol symbol) + { + return new QualifiedNameReference(symbol.toQualifiedName()); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/Plan.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/Plan.java new file mode 100644 index 00000000..9513ed6d --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/Plan.java @@ -0,0 +1,50 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner; + +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.planner.plan.PlanNode; +import com.google.common.base.Preconditions; + +import java.util.Map; + +public class Plan +{ + private final PlanNode root; + private final SymbolAllocator symbolAllocator; + + public Plan(PlanNode root, SymbolAllocator symbolAllocator) + { + Preconditions.checkNotNull(root, "root is null"); + Preconditions.checkNotNull(symbolAllocator, "symbolAllocator is null"); + + this.root = root; + this.symbolAllocator = symbolAllocator; + } + + public PlanNode getRoot() + { + return root; + } + + public Map getTypes() + { + return symbolAllocator.getTypes(); + } + + public SymbolAllocator getSymbolAllocator() + { + return symbolAllocator; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/PlanBuilder.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/PlanBuilder.java new file mode 100644 index 00000000..04962298 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/PlanBuilder.java @@ -0,0 +1,79 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner; + +import com.facebook.presto.sql.analyzer.FieldOrExpression; +import com.facebook.presto.sql.planner.plan.PlanNode; +import com.facebook.presto.sql.tree.Expression; +import com.google.common.base.Preconditions; + +import java.util.Optional; + +class PlanBuilder +{ + private final TranslationMap translations; + private final PlanNode root; + private final Optional sampleWeight; + + public PlanBuilder(TranslationMap translations, PlanNode root, Optional sampleWeight) + { + Preconditions.checkNotNull(translations, "translations is null"); + Preconditions.checkNotNull(root, "root is null"); + Preconditions.checkNotNull(sampleWeight, "sampleWeight is null"); + + this.translations = translations; + this.root = root; + this.sampleWeight = sampleWeight; + } + + public Optional getSampleWeight() + { + return sampleWeight; + } + + public RelationPlan getRelationPlan() + { + return translations.getRelationPlan(); + } + + public PlanNode getRoot() + { + return root; + } + + public Symbol translate(Expression expression) + { + return translations.get(expression); + } + + public Symbol translate(FieldOrExpression fieldOrExpression) + { + return translations.get(fieldOrExpression); + } + + public Expression rewrite(Expression expression) + { + return translations.rewrite(expression); + } + + public Expression rewrite(FieldOrExpression fieldOrExpression) + { + return translations.rewrite(fieldOrExpression); + } + + public TranslationMap getTranslations() + { + return translations; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/PlanFragment.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/PlanFragment.java new file mode 100644 index 00000000..d895cf54 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/PlanFragment.java @@ -0,0 +1,210 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner; + +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.planner.plan.PlanFragmentId; +import com.facebook.presto.sql.planner.plan.PlanNode; +import com.facebook.presto.sql.planner.plan.PlanNodeId; +import com.facebook.presto.sql.planner.plan.RemoteSourceNode; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableList.Builder; +import com.google.common.collect.ImmutableSet; + +import javax.annotation.concurrent.Immutable; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +import static com.facebook.presto.util.ImmutableCollectors.toImmutableList; +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Immutable +public class PlanFragment +{ + public enum PlanDistribution + { + SINGLE, + FIXED, + SOURCE, + COORDINATOR_ONLY + } + + public enum OutputPartitioning + { + NONE, + HASH + } + + private final PlanFragmentId id; + private final PlanNode root; + private final Map symbols; + private final List outputLayout; + private final PlanDistribution distribution; + private final PlanNodeId partitionedSource; + private final List types; + private final PlanNode partitionedSourceNode; + private final List remoteSourceNodes; + private final OutputPartitioning outputPartitioning; + private final List partitionBy; + private final Optional hash; + + @JsonCreator + public PlanFragment( + @JsonProperty("id") PlanFragmentId id, + @JsonProperty("root") PlanNode root, + @JsonProperty("symbols") Map symbols, + @JsonProperty("outputLayout") List outputLayout, + @JsonProperty("distribution") PlanDistribution distribution, + @JsonProperty("partitionedSource") PlanNodeId partitionedSource, + @JsonProperty("outputPartitioning") OutputPartitioning outputPartitioning, + @JsonProperty("partitionBy") List partitionBy, + @JsonProperty("hash") Optional hash) + { + this.id = checkNotNull(id, "id is null"); + this.root = checkNotNull(root, "root is null"); + this.symbols = checkNotNull(symbols, "symbols is null"); + this.outputLayout = checkNotNull(outputLayout, "outputLayout is null"); + this.distribution = checkNotNull(distribution, "distribution is null"); + this.partitionedSource = partitionedSource; + this.partitionBy = ImmutableList.copyOf(checkNotNull(partitionBy, "partitionBy is null")); + this.hash = hash; + + checkArgument(ImmutableSet.copyOf(root.getOutputSymbols()).containsAll(outputLayout), + "Root node outputs (%s) don't include all fragment outputs (%s)", root.getOutputSymbols(), outputLayout); + + types = outputLayout.stream() + .map(symbols::get) + .collect(toImmutableList()); + + this.partitionedSourceNode = findSource(root, partitionedSource); + + ImmutableList.Builder remoteSourceNodes = ImmutableList.builder(); + findRemoteSourceNodes(root, remoteSourceNodes); + this.remoteSourceNodes = remoteSourceNodes.build(); + + this.outputPartitioning = checkNotNull(outputPartitioning, "outputPartitioning is null"); + } + + @JsonProperty + public PlanFragmentId getId() + { + return id; + } + + @JsonProperty + public PlanNode getRoot() + { + return root; + } + + @JsonProperty + public Map getSymbols() + { + return symbols; + } + + @JsonProperty + public List getOutputLayout() + { + return outputLayout; + } + + @JsonProperty + public PlanDistribution getDistribution() + { + return distribution; + } + + @JsonProperty + public PlanNodeId getPartitionedSource() + { + return partitionedSource; + } + + @JsonProperty + public OutputPartitioning getOutputPartitioning() + { + return outputPartitioning; + } + + @JsonProperty + public List getPartitionBy() + { + return partitionBy; + } + + @JsonProperty + public Optional getHash() + { + return hash; + } + + public List getTypes() + { + return types; + } + + public PlanNode getPartitionedSourceNode() + { + return partitionedSourceNode; + } + + public List getRemoteSourceNodes() + { + return remoteSourceNodes; + } + + private static PlanNode findSource(PlanNode node, PlanNodeId nodeId) + { + if (node.getId().equals(nodeId)) { + return node; + } + + return node.getSources().stream() + .map(source -> findSource(source, nodeId)) + .filter(Objects::nonNull) + .findAny() + .orElse(null); + } + + private static void findRemoteSourceNodes(PlanNode node, Builder builder) + { + for (PlanNode source : node.getSources()) { + findRemoteSourceNodes(source, builder); + } + + if (node instanceof RemoteSourceNode) { + builder.add((RemoteSourceNode) node); + } + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("id", id) + .add("distribution", distribution) + .add("partitionedSource", partitionedSource) + .add("outputPartitioning", outputPartitioning) + .add("hash", hash) + .toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/PlanFragmenter.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/PlanFragmenter.java new file mode 100644 index 00000000..e5e2c575 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/PlanFragmenter.java @@ -0,0 +1,323 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner; + +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.planner.PlanFragment.OutputPartitioning; +import com.facebook.presto.sql.planner.PlanFragment.PlanDistribution; +import com.facebook.presto.sql.planner.plan.ExchangeNode; +import com.facebook.presto.sql.planner.plan.OutputNode; +import com.facebook.presto.sql.planner.plan.PlanFragmentId; +import com.facebook.presto.sql.planner.plan.PlanNode; +import com.facebook.presto.sql.planner.plan.PlanNodeId; +import com.facebook.presto.sql.planner.plan.PlanRewriter; +import com.facebook.presto.sql.planner.plan.RemoteSourceNode; +import com.facebook.presto.sql.planner.plan.TableCommitNode; +import com.facebook.presto.sql.planner.plan.TableScanNode; +import com.facebook.presto.sql.planner.plan.ValuesNode; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.common.collect.Maps; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static com.facebook.presto.util.ImmutableCollectors.toImmutableList; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Predicates.in; + +/** + * Splits a logical plan into fragments that can be shipped and executed on distributed nodes + */ +public class PlanFragmenter +{ + public SubPlan createSubPlans(Plan plan) + { + Fragmenter fragmenter = new Fragmenter(plan.getSymbolAllocator().getTypes()); + + FragmentProperties properties = new FragmentProperties(); + PlanNode root = PlanRewriter.rewriteWith(fragmenter, plan.getRoot(), properties); + + SubPlan result = fragmenter.buildFragment(root, properties); + result.sanityCheck(); + + return result; + } + + private static class Fragmenter + extends PlanRewriter + { + private final Map types; + private int nextFragmentId; + + public Fragmenter(Map types) + { + this.types = types; + } + + private PlanFragmentId nextFragmentId() + { + return new PlanFragmentId(String.valueOf(nextFragmentId++)); + } + + private SubPlan buildFragment(PlanNode root, FragmentProperties properties) + { + Set dependencies = SymbolExtractor.extract(root); + + PlanFragment fragment = new PlanFragment( + nextFragmentId(), + root, + Maps.filterKeys(types, in(dependencies)), + properties.getOutputLayout(), + properties.getDistribution(), + properties.getDistributeBy(), + properties.getOutputPartitioning(), + properties.getPartitionBy(), + properties.getHash()); + + return new SubPlan(fragment, properties.getChildren()); + } + + @Override + public PlanNode visitOutput(OutputNode node, RewriteContext context) + { + context.get() + .setSingleNodeDistribution() // TODO: add support for distributed output + .setOutputLayout(node.getOutputSymbols()) + .setUnpartitionedOutput(); + + return context.defaultRewrite(node, context.get()); + } + + @Override + public PlanNode visitTableCommit(TableCommitNode node, RewriteContext context) + { + context.get().setCoordinatorOnlyDistribution(); + return context.defaultRewrite(node, context.get()); + } + + @Override + public PlanNode visitTableScan(TableScanNode node, RewriteContext context) + { + context.get().setSourceDistribution(node.getId()); + return context.defaultRewrite(node, context.get()); + } + + @Override + public PlanNode visitValues(ValuesNode node, RewriteContext context) + { + context.get().setSingleNodeDistribution(); + return context.defaultRewrite(node, context.get()); + } + + @Override + public PlanNode visitExchange(ExchangeNode exchange, RewriteContext context) + { + ImmutableList.Builder builder = ImmutableList.builder(); + if (exchange.getType() == ExchangeNode.Type.GATHER) { + context.get().setSingleNodeDistribution(); + + for (int i = 0; i < exchange.getSources().size(); i++) { + FragmentProperties childProperties = new FragmentProperties(); + childProperties.setUnpartitionedOutput(); + childProperties.setOutputLayout(exchange.getInputs().get(i)); + + builder.add(buildSubPlan(exchange.getSources().get(i), childProperties, context)); + } + } + else if (exchange.getType() == ExchangeNode.Type.REPARTITION) { + context.get().setFixedDistribution(); + + FragmentProperties childProperties = new FragmentProperties() + .setHashPartitionedOutput(exchange.getPartitionKeys(), exchange.getHashSymbol()) + .setOutputLayout(Iterables.getOnlyElement(exchange.getInputs())); + + builder.add(buildSubPlan(Iterables.getOnlyElement(exchange.getSources()), childProperties, context)); + } + else if (exchange.getType() == ExchangeNode.Type.REPLICATE) { + FragmentProperties childProperties = new FragmentProperties(); + childProperties.setUnpartitionedOutput(); + childProperties.setOutputLayout(Iterables.getOnlyElement(exchange.getInputs())); + + builder.add(buildSubPlan(Iterables.getOnlyElement(exchange.getSources()), childProperties, context)); + } + + List children = builder.build(); + context.get().addChildren(children); + + List childrenIds = children.stream() + .map(SubPlan::getFragment) + .map(PlanFragment::getId) + .collect(toImmutableList()); + + return new RemoteSourceNode(exchange.getId(), childrenIds, exchange.getOutputSymbols()); + } + + private SubPlan buildSubPlan(PlanNode node, FragmentProperties properties, RewriteContext context) + { + PlanNode child = context.rewrite(node, properties); + return buildFragment(child, properties); + } + } + + private static class FragmentProperties + { + private final List children = new ArrayList<>(); + + private Optional> outputLayout = Optional.empty(); + private Optional outputPartitioning = Optional.empty(); + + private List partitionBy = ImmutableList.of(); + private Optional hash = Optional.empty(); + + private Optional distribution = Optional.empty(); + private PlanNodeId distributeBy; + + public List getChildren() + { + return children; + } + + public FragmentProperties setSingleNodeDistribution() + { + if (distribution.isPresent()) { + PlanDistribution value = distribution.get(); + checkState(value == PlanDistribution.SINGLE || value == PlanDistribution.COORDINATOR_ONLY, + "Cannot overwrite distribution with %s (currently set to %s)", PlanDistribution.SINGLE, value); + } + else { + distribution = Optional.of(PlanDistribution.SINGLE); + } + + return this; + } + + public FragmentProperties setFixedDistribution() + { + distribution.ifPresent(current -> checkState(current == PlanDistribution.FIXED, + "Cannot set distribution to %s. Already set to %s", + PlanDistribution.FIXED, + current)); + + distribution = Optional.of(PlanDistribution.FIXED); + + return this; + } + + public FragmentProperties setCoordinatorOnlyDistribution() + { + // only SINGLE can be upgraded to COORDINATOR_ONLY + distribution.ifPresent(current -> checkState(distribution.get() == PlanDistribution.SINGLE, + "Cannot overwrite distribution with %s (currently set to %s)", + PlanDistribution.COORDINATOR_ONLY, + distribution.get())); + + distribution = Optional.of(PlanDistribution.COORDINATOR_ONLY); + + return this; + } + + public FragmentProperties setSourceDistribution(PlanNodeId source) + { + if (distribution.isPresent()) { + // If already SINGLE or COORDINATOR_ONLY, leave it as is (this is for single-node execution) + checkState(distribution.get() == PlanDistribution.SINGLE || distribution.get() == PlanDistribution.COORDINATOR_ONLY, + "Cannot overwrite distribution with %s (currently set to %s)", + PlanDistribution.SOURCE, + distribution.get()); + } + else { + distribution = Optional.of(PlanDistribution.SOURCE); + this.distributeBy = source; + } + + return this; + } + + public FragmentProperties setUnpartitionedOutput() + { + outputPartitioning.ifPresent(current -> { + throw new IllegalStateException(String.format("Output overwrite partitioning with %s (currently set to %s)", OutputPartitioning.NONE, current)); + }); + + outputPartitioning = Optional.of(OutputPartitioning.NONE); + + return this; + } + + public FragmentProperties setOutputLayout(List layout) + { + outputLayout.ifPresent(current -> { + throw new IllegalStateException(String.format("Cannot overwrite output layout with %s (currently set to %s)", layout, current)); + }); + + outputLayout = Optional.of(layout); + + return this; + } + + public FragmentProperties setHashPartitionedOutput(List partitionKeys, Optional hash) + { + outputPartitioning.ifPresent(current -> { + throw new IllegalStateException(String.format("Cannot overwrite output partitioning with %s (currently set to %s)", OutputPartitioning.HASH, current)); + }); + + this.outputPartitioning = Optional.of(OutputPartitioning.HASH); + this.partitionBy = ImmutableList.copyOf(partitionKeys); + this.hash = hash; + + return this; + } + + public FragmentProperties addChildren(List children) + { + this.children.addAll(children); + + return this; + } + + public List getOutputLayout() + { + return outputLayout.get(); + } + + public OutputPartitioning getOutputPartitioning() + { + return outputPartitioning.get(); + } + + public PlanDistribution getDistribution() + { + return distribution.get(); + } + + public List getPartitionBy() + { + return partitionBy; + } + + public Optional getHash() + { + return hash; + } + + public PlanNodeId getDistributeBy() + { + return distributeBy; + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/PlanNodeIdAllocator.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/PlanNodeIdAllocator.java new file mode 100644 index 00000000..2dd5cf9c --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/PlanNodeIdAllocator.java @@ -0,0 +1,26 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner; + +import com.facebook.presto.sql.planner.plan.PlanNodeId; + +public class PlanNodeIdAllocator +{ + private int nextId = 0; + + public PlanNodeId getNextId() + { + return new PlanNodeId(Integer.toString(nextId++)); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/PlanOptimizersFactory.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/PlanOptimizersFactory.java new file mode 100644 index 00000000..2207be07 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/PlanOptimizersFactory.java @@ -0,0 +1,116 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner; + +import com.facebook.presto.index.IndexManager; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.sql.analyzer.FeaturesConfig; +import com.facebook.presto.sql.parser.SqlParser; +import com.facebook.presto.sql.planner.optimizations.AddExchanges; +import com.facebook.presto.sql.planner.optimizations.BeginTableWrite; +import com.facebook.presto.sql.planner.optimizations.CanonicalizeExpressions; +import com.facebook.presto.sql.planner.optimizations.CountConstantOptimizer; +import com.facebook.presto.sql.planner.optimizations.HashGenerationOptimizer; +import com.facebook.presto.sql.planner.optimizations.ImplementSampleAsFilter; +import com.facebook.presto.sql.planner.optimizations.IndexJoinOptimizer; +import com.facebook.presto.sql.planner.optimizations.LimitPushDown; +import com.facebook.presto.sql.planner.optimizations.MergeProjections; +import com.facebook.presto.sql.planner.optimizations.MetadataQueryOptimizer; +import com.facebook.presto.sql.planner.optimizations.PickLayout; +import com.facebook.presto.sql.planner.optimizations.PlanOptimizer; +import com.facebook.presto.sql.planner.optimizations.PredicatePushDown; +import com.facebook.presto.sql.planner.optimizations.PruneRedundantProjections; +import com.facebook.presto.sql.planner.optimizations.PruneUnreferencedOutputs; +import com.facebook.presto.sql.planner.optimizations.SetFlatteningOptimizer; +import com.facebook.presto.sql.planner.optimizations.SimplifyExpressions; +import com.facebook.presto.sql.planner.optimizations.SingleDistinctOptimizer; +import com.facebook.presto.sql.planner.optimizations.UnaliasSymbolReferences; +import com.facebook.presto.sql.planner.optimizations.WindowFilterPushDown; +import com.google.common.collect.ImmutableList; +import com.google.inject.Inject; + +import javax.inject.Provider; + +import java.util.List; + +public class PlanOptimizersFactory + implements Provider> +{ + private final List optimizers; + + @Inject + public PlanOptimizersFactory(Metadata metadata, SqlParser sqlParser, IndexManager indexManager, FeaturesConfig featuresConfig) + { + this(metadata, sqlParser, indexManager, featuresConfig, false); + } + + public PlanOptimizersFactory(Metadata metadata, SqlParser sqlParser, IndexManager indexManager, FeaturesConfig featuresConfig, boolean forceSingleNode) + { + ImmutableList.Builder builder = ImmutableList.builder(); + + builder.add(new ImplementSampleAsFilter(), + new CanonicalizeExpressions(), + new SimplifyExpressions(metadata, sqlParser), + new UnaliasSymbolReferences(), + new PruneRedundantProjections(), + new SetFlatteningOptimizer(), + new LimitPushDown(), // Run the LimitPushDown after flattening set operators to make it easier to do the set flattening + new PredicatePushDown(metadata, sqlParser), + new MergeProjections(), + new SimplifyExpressions(metadata, sqlParser), // Re-run the SimplifyExpressions to simplify any recomposed expressions from other optimizations + new ProjectionPushDown(), + new UnaliasSymbolReferences(), // Run again because predicate pushdown and projection pushdown might add more projections + new IndexJoinOptimizer(metadata, indexManager), // Run this after projections and filters have been fully simplified and pushed down + new CountConstantOptimizer(), + new WindowFilterPushDown(), // This must run after PredicatePushDown so that it squashes any successive filter nodes + new HashGenerationOptimizer(featuresConfig.isOptimizeHashGeneration()), // This must run after all other optimizers have run to that all the PlanNodes are created + new MergeProjections(), + new PruneUnreferencedOutputs(), // Make sure to run this at the end to help clean the plan for logging/execution and not remove info that other optimizers might need at an earlier point + new PruneRedundantProjections()); // This MUST run after PruneUnreferencedOutputs as it may introduce new redundant projections + + if (featuresConfig.isOptimizeMetadataQueries()) { + builder.add(new MetadataQueryOptimizer(metadata)); + } + + if (featuresConfig.isOptimizeSingleDistinct()) { + builder.add(new SingleDistinctOptimizer()); + builder.add(new PruneUnreferencedOutputs()); + } + + builder.add(new BeginTableWrite(metadata)); // HACK! see comments in BeginTableWrite + + if (!forceSingleNode) { + builder.add(new AddExchanges(metadata, sqlParser, featuresConfig.isDistributedIndexJoinsEnabled(), featuresConfig.isDistributedJoinsEnabled())); + } + + builder.add(new PickLayout(metadata)); + + builder.add(new PredicatePushDown(metadata, sqlParser)); // Run predicate push down one more time in case we can leverage new information from layouts' effective predicate + builder.add(new UnaliasSymbolReferences()); + builder.add(new MergeProjections()); + builder.add(new PruneUnreferencedOutputs()); + builder.add(new PruneRedundantProjections()); + + // TODO: consider adding a formal final plan sanitization optimizer that prepares the plan for transmission/execution/logging + // TODO: figure out how to improve the set flattening optimizer so that it can run at any point + + this.optimizers = builder.build(); + } + + @Override + public synchronized List get() + { + return optimizers; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/PlanPrinter.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/PlanPrinter.java new file mode 100644 index 00000000..2ac52918 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/PlanPrinter.java @@ -0,0 +1,647 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner; + +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.metadata.OperatorNotFoundException; +import com.facebook.presto.metadata.TableHandle; +import com.facebook.presto.metadata.TableLayout; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ColumnMetadata; +import com.facebook.presto.spi.Domain; +import com.facebook.presto.spi.Marker; +import com.facebook.presto.spi.Range; +import com.facebook.presto.spi.TupleDomain; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.planner.PlanFragment.OutputPartitioning; +import com.facebook.presto.sql.planner.PlanFragment.PlanDistribution; +import com.facebook.presto.sql.planner.plan.AggregationNode; +import com.facebook.presto.sql.planner.plan.DeleteNode; +import com.facebook.presto.sql.planner.plan.DistinctLimitNode; +import com.facebook.presto.sql.planner.plan.ExchangeNode; +import com.facebook.presto.sql.planner.plan.FilterNode; +import com.facebook.presto.sql.planner.plan.IndexJoinNode; +import com.facebook.presto.sql.planner.plan.IndexSourceNode; +import com.facebook.presto.sql.planner.plan.JoinNode; +import com.facebook.presto.sql.planner.plan.LimitNode; +import com.facebook.presto.sql.planner.plan.MarkDistinctNode; +import com.facebook.presto.sql.planner.plan.OutputNode; +import com.facebook.presto.sql.planner.plan.PlanFragmentId; +import com.facebook.presto.sql.planner.plan.PlanNode; +import com.facebook.presto.sql.planner.plan.PlanVisitor; +import com.facebook.presto.sql.planner.plan.ProjectNode; +import com.facebook.presto.sql.planner.plan.RemoteSourceNode; +import com.facebook.presto.sql.planner.plan.RowNumberNode; +import com.facebook.presto.sql.planner.plan.SampleNode; +import com.facebook.presto.sql.planner.plan.SemiJoinNode; +import com.facebook.presto.sql.planner.plan.SortNode; +import com.facebook.presto.sql.planner.plan.TableCommitNode; +import com.facebook.presto.sql.planner.plan.TableScanNode; +import com.facebook.presto.sql.planner.plan.TableWriterNode; +import com.facebook.presto.sql.planner.plan.TopNNode; +import com.facebook.presto.sql.planner.plan.TopNRowNumberNode; +import com.facebook.presto.sql.planner.plan.UnionNode; +import com.facebook.presto.sql.planner.plan.UnnestNode; +import com.facebook.presto.sql.planner.plan.ValuesNode; +import com.facebook.presto.sql.planner.plan.WindowNode; +import com.facebook.presto.sql.tree.ComparisonExpression; +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.FunctionCall; +import com.facebook.presto.sql.tree.QualifiedNameReference; +import com.facebook.presto.util.GraphvizPrinter; +import com.facebook.presto.util.ImmutableCollectors; +import com.facebook.presto.util.JsonPlanPrinter; +import com.google.common.base.Functions; +import com.google.common.base.Joiner; +import com.google.common.base.Strings; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import io.airlift.slice.Slice; + +import java.lang.invoke.MethodHandle; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static com.facebook.presto.sql.planner.DomainUtils.simplifyDomain; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.lang.String.format; + +public class PlanPrinter +{ + private final StringBuilder output = new StringBuilder(); + private final Metadata metadata; + + private PlanPrinter(PlanNode plan, Map types, Metadata metadata) + { + this(plan, types, metadata, 0); + } + + private PlanPrinter(PlanNode plan, Map types, Metadata metadata, int indent) + { + checkNotNull(plan, "plan is null"); + checkNotNull(types, "types is null"); + checkNotNull(metadata, "metadata is null"); + + this.metadata = metadata; + + Visitor visitor = new Visitor(types); + plan.accept(visitor, indent); + } + + @Override + public String toString() + { + return output.toString(); + } + + public static String textLogicalPlan(PlanNode plan, Map types, Metadata metadata) + { + return new PlanPrinter(plan, types, metadata).toString(); + } + + public static String textLogicalPlan(PlanNode plan, Map types, Metadata metadata, int indent) + { + return new PlanPrinter(plan, types, metadata, indent).toString(); + } + + public static String getJsonPlanSource(PlanNode plan, Metadata metadata) + { + return JsonPlanPrinter.getPlan(plan, metadata); + } + + public static String textDistributedPlan(SubPlan plan, Metadata metadata) + { + StringBuilder builder = new StringBuilder(); + for (PlanFragment fragment : plan.getAllFragments()) { + builder.append(String.format("Fragment %s [%s]\n", + fragment.getId(), + fragment.getDistribution())); + + builder.append(indentString(1)) + .append(String.format("Output layout: [%s]\n", + Joiner.on(", ").join(fragment.getOutputLayout()))); + + if (fragment.getOutputPartitioning() == OutputPartitioning.HASH) { + builder.append(indentString(1)) + .append(String.format("Output partitioning: [%s]\n", + Joiner.on(", ").join(fragment.getPartitionBy()))); + } + + builder.append(textLogicalPlan(fragment.getRoot(), fragment.getSymbols(), metadata, 1)) + .append("\n"); + } + + return builder.toString(); + } + + public static String graphvizLogicalPlan(PlanNode plan, Map types) + { + PlanFragment fragment = new PlanFragment(new PlanFragmentId("graphviz_plan"), plan, types, plan.getOutputSymbols(), PlanDistribution.SINGLE, plan.getId(), OutputPartitioning.NONE, ImmutableList.of(), Optional.empty()); + return GraphvizPrinter.printLogical(ImmutableList.of(fragment)); + } + + public static String graphvizDistributedPlan(SubPlan plan) + { + return GraphvizPrinter.printDistributed(plan); + } + + private void print(int indent, String format, Object... args) + { + String value; + + if (args.length == 0) { + value = format; + } + else { + value = format(format, args); + } + output.append(indentString(indent)).append(value).append('\n'); + } + + private static String indentString(int indent) + { + return Strings.repeat(" ", indent); + } + + private class Visitor + extends PlanVisitor + { + private final Map types; + + public Visitor(Map types) + { + this.types = types; + } + + @Override + public Void visitJoin(JoinNode node, Integer indent) + { + List joinExpressions = new ArrayList<>(); + for (JoinNode.EquiJoinClause clause : node.getCriteria()) { + joinExpressions.add(new ComparisonExpression(ComparisonExpression.Type.EQUAL, + new QualifiedNameReference(clause.getLeft().toQualifiedName()), + new QualifiedNameReference(clause.getRight().toQualifiedName()))); + } + + print(indent, "- %s[%s] => [%s]", node.getType().getJoinLabel(), Joiner.on(" AND ").join(joinExpressions), formatOutputs(node.getOutputSymbols())); + node.getLeft().accept(this, indent + 1); + node.getRight().accept(this, indent + 1); + + return null; + } + + @Override + public Void visitSemiJoin(SemiJoinNode node, Integer indent) + { + print(indent, "- SemiJoin[%s = %s] => [%s]", node.getSourceJoinSymbol(), node.getFilteringSourceJoinSymbol(), formatOutputs(node.getOutputSymbols())); + node.getSource().accept(this, indent + 1); + node.getFilteringSource().accept(this, indent + 1); + + return null; + } + + @Override + public Void visitIndexSource(IndexSourceNode node, Integer indent) + { + print(indent, "- IndexSource[%s, lookup = %s] => [%s]", node.getIndexHandle(), node.getLookupSymbols(), formatOutputs(node.getOutputSymbols())); + for (Map.Entry entry : node.getAssignments().entrySet()) { + if (node.getOutputSymbols().contains(entry.getKey())) { + print(indent + 2, "%s := %s", entry.getKey(), entry.getValue()); + } + } + return null; + } + + @Override + public Void visitIndexJoin(IndexJoinNode node, Integer indent) + { + List joinExpressions = new ArrayList<>(); + for (IndexJoinNode.EquiJoinClause clause : node.getCriteria()) { + joinExpressions.add(new ComparisonExpression(ComparisonExpression.Type.EQUAL, + new QualifiedNameReference(clause.getProbe().toQualifiedName()), + new QualifiedNameReference(clause.getIndex().toQualifiedName()))); + } + + print(indent, "- %sIndexJoin[%s] => [%s]", node.getType().getJoinLabel(), Joiner.on(" AND ").join(joinExpressions), formatOutputs(node.getOutputSymbols())); + node.getProbeSource().accept(this, indent + 1); + node.getIndexSource().accept(this, indent + 1); + + return null; + } + + @Override + public Void visitLimit(LimitNode node, Integer indent) + { + print(indent, "- Limit[%s] => [%s]", node.getCount(), formatOutputs(node.getOutputSymbols())); + return processChildren(node, indent + 1); + } + + @Override + public Void visitDistinctLimit(DistinctLimitNode node, Integer indent) + { + print(indent, "- DistinctLimit[%s] => [%s]", node.getLimit(), formatOutputs(node.getOutputSymbols())); + return processChildren(node, indent + 1); + } + + @Override + public Void visitAggregation(AggregationNode node, Integer indent) + { + String type = ""; + if (node.getStep() != AggregationNode.Step.SINGLE) { + type = format("(%s)", node.getStep().toString()); + } + String key = ""; + if (!node.getGroupBy().isEmpty()) { + key = node.getGroupBy().toString(); + } + String sampleWeight = ""; + if (node.getSampleWeight().isPresent()) { + sampleWeight = format("[sampleWeight = %s]", node.getSampleWeight().get()); + } + + print(indent, "- Aggregate%s%s%s => [%s]", type, key, sampleWeight, formatOutputs(node.getOutputSymbols())); + + for (Map.Entry entry : node.getAggregations().entrySet()) { + if (node.getMasks().containsKey(entry.getKey())) { + print(indent + 2, "%s := %s (mask = %s)", entry.getKey(), entry.getValue(), node.getMasks().get(entry.getKey())); + } + else { + print(indent + 2, "%s := %s", entry.getKey(), entry.getValue()); + } + } + + return processChildren(node, indent + 1); + } + + @Override + public Void visitMarkDistinct(MarkDistinctNode node, Integer indent) + { + print(indent, "- MarkDistinct[distinct=%s marker=%s] => [%s]", formatOutputs(node.getDistinctSymbols()), node.getMarkerSymbol(), formatOutputs(node.getOutputSymbols())); + return processChildren(node, indent + 1); + } + + @Override + public Void visitWindow(final WindowNode node, Integer indent) + { + List partitionBy = Lists.transform(node.getPartitionBy(), Functions.toStringFunction()); + + List orderBy = Lists.transform(node.getOrderBy(), input -> input + " " + node.getOrderings().get(input)); + + List args = new ArrayList<>(); + if (!partitionBy.isEmpty()) { + List prePartitioned = node.getPartitionBy().stream() + .filter(node.getPrePartitionedInputs()::contains) + .collect(ImmutableCollectors.toImmutableList()); + + List notPrePartitioned = node.getPartitionBy().stream() + .filter(column -> !node.getPrePartitionedInputs().contains(column)) + .collect(ImmutableCollectors.toImmutableList()); + + StringBuilder builder = new StringBuilder(); + if (!prePartitioned.isEmpty()) { + builder.append("<") + .append(Joiner.on(", ").join(prePartitioned)) + .append(">"); + if (!notPrePartitioned.isEmpty()) { + builder.append(", "); + } + } + if (!notPrePartitioned.isEmpty()) { + builder.append(Joiner.on(", ").join(notPrePartitioned)); + } + args.add(format("partition by (%s)", builder)); + } + if (!orderBy.isEmpty()) { + args.add(format("order by (%s)", Stream.concat( + node.getOrderBy().stream() + .limit(node.getPreSortedOrderPrefix()) + .map(symbol -> "<" + symbol + ">"), + node.getOrderBy().stream() + .skip(node.getPreSortedOrderPrefix()) + .map(Symbol::toString)) + .collect(Collectors.joining(", ")))); + } + + print(indent, "- Window[%s] => [%s]", Joiner.on(", ").join(args), formatOutputs(node.getOutputSymbols())); + + for (Map.Entry entry : node.getWindowFunctions().entrySet()) { + print(indent + 2, "%s := %s(%s)", entry.getKey(), entry.getValue().getName(), Joiner.on(", ").join(entry.getValue().getArguments())); + } + return processChildren(node, indent + 1); + } + + @Override + public Void visitTopNRowNumber(final TopNRowNumberNode node, Integer indent) + { + List partitionBy = Lists.transform(node.getPartitionBy(), Functions.toStringFunction()); + + List orderBy = Lists.transform(node.getOrderBy(), input -> input + " " + node.getOrderings().get(input)); + + List args = new ArrayList<>(); + args.add(format("partition by (%s)", Joiner.on(", ").join(partitionBy))); + args.add(format("order by (%s)", Joiner.on(", ").join(orderBy))); + + print(indent, "- TopNRowNumber[%s limit %s] => [%s]", Joiner.on(", ").join(args), node.getMaxRowCountPerPartition(), formatOutputs(node.getOutputSymbols())); + + print(indent + 2, "%s := %s", node.getRowNumberSymbol(), "row_number()"); + return processChildren(node, indent + 1); + } + + @Override + public Void visitRowNumber(final RowNumberNode node, Integer indent) + { + List partitionBy = Lists.transform(node.getPartitionBy(), Functions.toStringFunction()); + List args = new ArrayList<>(); + if (!partitionBy.isEmpty()) { + args.add(format("partition by (%s) ", Joiner.on(", ").join(partitionBy))); + } + + if (node.getMaxRowCountPerPartition().isPresent()) { + args.add(format("limit (%s) ", node.getMaxRowCountPerPartition().get())); + } + + print(indent, "- RowNumber[%s] => [%s]", Joiner.on(", ").join(args), formatOutputs(node.getOutputSymbols())); + + print(indent + 2, "%s := %s", node.getRowNumberSymbol(), "row_number()"); + return processChildren(node, indent + 1); + } + + @Override + public Void visitTableScan(TableScanNode node, Integer indent) + { + TableHandle table = node.getTable(); + print(indent, "- TableScan[%s, original constraint=%s] => [%s]", table, node.getOriginalConstraint(), formatOutputs(node.getOutputSymbols())); + + TupleDomain predicate = node.getLayout() + .map(metadata::getLayout) + .map(TableLayout::getPredicate) + .orElse(TupleDomain.all()); + + if (node.getLayout().isPresent()) { + print(indent + 2, "LAYOUT: %s", node.getLayout().get().getConnectorHandle()); + } + + if (predicate.isNone()) { + print(indent + 2, ":: NONE"); + } + else { + // first, print output columns and their constraints + for (Map.Entry assignment : node.getAssignments().entrySet()) { + ColumnHandle column = assignment.getValue(); + print(indent + 2, "%s := %s", assignment.getKey(), column); + printConstraint(indent + 3, table, column, predicate); + } + + // then, print constraints for columns that are not in the output + if (!predicate.isAll()) { + Set outputs = ImmutableSet.copyOf(node.getAssignments().values()); + + predicate.getDomains() + .entrySet().stream() + .filter(entry -> !outputs.contains(entry.getKey())) + .forEach(entry -> { + ColumnHandle column = entry.getKey(); + print(indent + 2, "%s", column); + printConstraint(indent + 3, table, column, predicate); + }); + } + } + + return null; + } + + @Override + public Void visitValues(ValuesNode node, Integer indent) + { + print(indent, "- Values => [%s]", formatOutputs(node.getOutputSymbols())); + for (List row : node.getRows()) { + print(indent + 2, "(" + Joiner.on(", ").join(row) + ")"); + } + return null; + } + + @Override + public Void visitFilter(FilterNode node, Integer indent) + { + print(indent, "- Filter[%s] => [%s]", node.getPredicate(), formatOutputs(node.getOutputSymbols())); + return processChildren(node, indent + 1); + } + + @Override + public Void visitProject(ProjectNode node, Integer indent) + { + print(indent, "- Project => [%s]", formatOutputs(node.getOutputSymbols())); + for (Map.Entry entry : node.getAssignments().entrySet()) { + if (entry.getValue() instanceof QualifiedNameReference && ((QualifiedNameReference) entry.getValue()).getName().equals(entry.getKey().toQualifiedName())) { + // skip identity assignments + continue; + } + print(indent + 2, "%s := %s", entry.getKey(), entry.getValue()); + } + + return processChildren(node, indent + 1); + } + + @Override + public Void visitUnnest(UnnestNode node, Integer indent) + { + print(indent, "- Unnest [replicate=%s, unnest=%s] => [%s]", formatOutputs(node.getReplicateSymbols()), formatOutputs(node.getUnnestSymbols().keySet()), formatOutputs(node.getOutputSymbols())); + + return processChildren(node, indent + 1); + } + + @Override + public Void visitOutput(OutputNode node, Integer indent) + { + print(indent, "- Output[%s] => [%s]", Joiner.on(", ").join(node.getColumnNames()), formatOutputs(node.getOutputSymbols())); + for (int i = 0; i < node.getColumnNames().size(); i++) { + String name = node.getColumnNames().get(i); + Symbol symbol = node.getOutputSymbols().get(i); + if (!name.equals(symbol.toString())) { + print(indent + 2, "%s := %s", name, symbol); + } + } + + return processChildren(node, indent + 1); + } + + @Override + public Void visitTopN(final TopNNode node, Integer indent) + { + Iterable keys = Iterables.transform(node.getOrderBy(), input -> input + " " + node.getOrderings().get(input)); + + print(indent, "- TopN[%s by (%s)] => [%s]", node.getCount(), Joiner.on(", ").join(keys), formatOutputs(node.getOutputSymbols())); + return processChildren(node, indent + 1); + } + + @Override + public Void visitSort(final SortNode node, Integer indent) + { + Iterable keys = Iterables.transform(node.getOrderBy(), input -> input + " " + node.getOrderings().get(input)); + + print(indent, "- Sort[%s] => [%s]", Joiner.on(", ").join(keys), formatOutputs(node.getOutputSymbols())); + return processChildren(node, indent + 1); + } + + @Override + public Void visitRemoteSource(RemoteSourceNode node, Integer indent) + { + print(indent, "- RemoteSource[%s] => [%s]", Joiner.on(',').join(node.getSourceFragmentIds()), formatOutputs(node.getOutputSymbols())); + + return null; + } + + @Override + public Void visitUnion(UnionNode node, Integer indent) + { + print(indent, "- Union => [%s]", formatOutputs(node.getOutputSymbols())); + + return processChildren(node, indent + 1); + } + + @Override + public Void visitTableWriter(TableWriterNode node, Integer indent) + { + print(indent, "- TableWriter => [%s]", formatOutputs(node.getOutputSymbols())); + for (int i = 0; i < node.getColumnNames().size(); i++) { + String name = node.getColumnNames().get(i); + Symbol symbol = node.getColumns().get(i); + print(indent + 2, "%s := %s", name, symbol); + } + + return processChildren(node, indent + 1); + } + + @Override + public Void visitTableCommit(TableCommitNode node, Integer indent) + { + print(indent, "- TableCommit[%s] => [%s]", node.getTarget(), formatOutputs(node.getOutputSymbols())); + + return processChildren(node, indent + 1); + } + + @Override + public Void visitSample(SampleNode node, Integer indent) + { + print(indent, "- Sample[%s: %s] => [%s]", node.getSampleType(), node.getSampleRatio(), formatOutputs(node.getOutputSymbols())); + + return processChildren(node, indent + 1); + } + + @Override + public Void visitExchange(ExchangeNode node, Integer indent) + { + print(indent, "- Exchange[%s] => %s", node.getType(), formatOutputs(node.getOutputSymbols())); + + return processChildren(node, indent + 1); + } + + @Override + public Void visitDelete(DeleteNode node, Integer indent) + { + print(indent, "- Delete[%s] => [%s]", node.getTarget(), formatOutputs(node.getOutputSymbols())); + + return processChildren(node, indent + 1); + } + + @Override + protected Void visitPlan(PlanNode node, Integer context) + { + throw new UnsupportedOperationException("not yet implemented: " + node.getClass().getName()); + } + + private Void processChildren(PlanNode node, int indent) + { + for (PlanNode child : node.getSources()) { + child.accept(this, indent); + } + + return null; + } + + private String formatOutputs(Iterable symbols) + { + return Joiner.on(", ").join(Iterables.transform(symbols, input -> input + ":" + types.get(input))); + } + } + + private void printConstraint(int indent, TableHandle table, ColumnHandle column, TupleDomain constraint) + { + if (!constraint.isAll() && constraint.getDomains().containsKey(column)) { + print(indent, ":: %s", formatDomain(table, column, simplifyDomain(constraint.getDomains().get(column)))); + } + } + + private String formatDomain(TableHandle table, ColumnHandle column, Domain domain) + { + ImmutableList.Builder parts = ImmutableList.builder(); + + if (domain.isNullAllowed()) { + parts.add("NULL"); + } + + try { + ColumnMetadata columnMetadata = metadata.getColumnMetadata(table, column); + MethodHandle method = metadata.getFunctionRegistry().getCoercion(columnMetadata.getType(), VARCHAR) + .getMethodHandle(); + + for (Range range : domain.getRanges()) { + StringBuilder builder = new StringBuilder(); + if (range.isSingleValue()) { + String value = ((Slice) method.invokeWithArguments(range.getSingleValue())).toStringUtf8(); + builder.append('[').append(value).append(']'); + } + else { + builder.append((range.getLow().getBound() == Marker.Bound.EXACTLY) ? '[' : '('); + + if (range.getLow().isLowerUnbounded()) { + builder.append(""); + } + else { + builder.append(((Slice) method.invokeWithArguments(range.getLow().getValue())).toStringUtf8()); + } + + builder.append(", "); + + if (range.getHigh().isUpperUnbounded()) { + builder.append(""); + } + else { + builder.append(((Slice) method.invokeWithArguments(range.getHigh().getValue())).toStringUtf8()); + } + + builder.append((range.getHigh().getBound() == Marker.Bound.EXACTLY) ? ']' : ')'); + } + parts.add(builder.toString()); + } + } + catch (OperatorNotFoundException e) { + parts.add(""); + } + catch (Throwable e) { + throw Throwables.propagate(e); + } + + return "[" + Joiner.on(", ").join(parts.build()) + "]"; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/PlanSanityChecker.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/PlanSanityChecker.java new file mode 100644 index 00000000..9fc4bf20 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/PlanSanityChecker.java @@ -0,0 +1,481 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner; + +import com.facebook.presto.sql.planner.plan.AggregationNode; +import com.facebook.presto.sql.planner.plan.DistinctLimitNode; +import com.facebook.presto.sql.planner.plan.ExchangeNode; +import com.facebook.presto.sql.planner.plan.FilterNode; +import com.facebook.presto.sql.planner.plan.IndexJoinNode; +import com.facebook.presto.sql.planner.plan.IndexSourceNode; +import com.facebook.presto.sql.planner.plan.JoinNode; +import com.facebook.presto.sql.planner.plan.LimitNode; +import com.facebook.presto.sql.planner.plan.MarkDistinctNode; +import com.facebook.presto.sql.planner.plan.OutputNode; +import com.facebook.presto.sql.planner.plan.PlanNode; +import com.facebook.presto.sql.planner.plan.PlanNodeId; +import com.facebook.presto.sql.planner.plan.PlanVisitor; +import com.facebook.presto.sql.planner.plan.ProjectNode; +import com.facebook.presto.sql.planner.plan.RemoteSourceNode; +import com.facebook.presto.sql.planner.plan.RowNumberNode; +import com.facebook.presto.sql.planner.plan.SampleNode; +import com.facebook.presto.sql.planner.plan.SemiJoinNode; +import com.facebook.presto.sql.planner.plan.SortNode; +import com.facebook.presto.sql.planner.plan.TableCommitNode; +import com.facebook.presto.sql.planner.plan.TableScanNode; +import com.facebook.presto.sql.planner.plan.DeleteNode; +import com.facebook.presto.sql.planner.plan.TableWriterNode; +import com.facebook.presto.sql.planner.plan.TopNNode; +import com.facebook.presto.sql.planner.plan.TopNRowNumberNode; +import com.facebook.presto.sql.planner.plan.UnionNode; +import com.facebook.presto.sql.planner.plan.UnnestNode; +import com.facebook.presto.sql.planner.plan.ValuesNode; +import com.facebook.presto.sql.planner.plan.WindowNode; +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.FunctionCall; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import static com.facebook.presto.sql.planner.optimizations.IndexJoinOptimizer.IndexKeyTracer; +import static com.google.common.base.Preconditions.checkArgument; + +/** + * Ensures that all dependencies (i.e., symbols in expressions) for a plan node are provided by its source nodes + */ +public final class PlanSanityChecker +{ + private PlanSanityChecker() {} + + public static void validate(PlanNode plan) + { + plan.accept(new Visitor(), null); + } + + private static class Visitor + extends PlanVisitor + { + private final Map nodesById = new HashMap<>(); + + @Override + protected Void visitPlan(PlanNode node, Void context) + { + throw new UnsupportedOperationException("not yet implemented: " + node.getClass().getName()); + } + + @Override + public Void visitAggregation(AggregationNode node, Void context) + { + PlanNode source = node.getSource(); + source.accept(this, context); // visit child + + verifyUniqueId(node); + + Set inputs = ImmutableSet.copyOf(source.getOutputSymbols()); + checkDependencies(inputs, node.getGroupBy(), "Invalid node. Group by symbols (%s) not in source plan output (%s)", node.getGroupBy(), node.getSource().getOutputSymbols()); + + if (node.getSampleWeight().isPresent()) { + checkArgument(inputs.contains(node.getSampleWeight().get()), "Invalid node. Sample weight symbol (%s) is not in source plan output (%s)", node.getSampleWeight().get(), node.getSource().getOutputSymbols()); + } + + for (FunctionCall call : node.getAggregations().values()) { + Set dependencies = DependencyExtractor.extractUnique(call); + checkDependencies(inputs, dependencies, "Invalid node. Aggregation dependencies (%s) not in source plan output (%s)", dependencies, node.getSource().getOutputSymbols()); + } + + return null; + } + + @Override + public Void visitMarkDistinct(MarkDistinctNode node, Void context) + { + PlanNode source = node.getSource(); + source.accept(this, context); // visit child + + verifyUniqueId(node); + + checkDependencies(source.getOutputSymbols(), node.getDistinctSymbols(), "Invalid node. Mark distinct symbols (%s) not in source plan output (%s)", node.getDistinctSymbols(), source.getOutputSymbols()); + + return null; + } + + @Override + public Void visitWindow(WindowNode node, Void context) + { + PlanNode source = node.getSource(); + source.accept(this, context); // visit child + + verifyUniqueId(node); + + Set inputs = ImmutableSet.copyOf(source.getOutputSymbols()); + + checkDependencies(inputs, node.getPartitionBy(), "Invalid node. Partition by symbols (%s) not in source plan output (%s)", node.getPartitionBy(), node.getSource().getOutputSymbols()); + checkDependencies(inputs, node.getOrderBy(), "Invalid node. Order by symbols (%s) not in source plan output (%s)", node.getOrderBy(), node.getSource().getOutputSymbols()); + + ImmutableList.Builder bounds = ImmutableList.builder(); + if (node.getFrame().getStartValue().isPresent()) { + bounds.add(node.getFrame().getStartValue().get()); + } + if (node.getFrame().getEndValue().isPresent()) { + bounds.add(node.getFrame().getEndValue().get()); + } + checkDependencies(inputs, bounds.build(), "Invalid node. Frame bounds (%s) not in source plan output (%s)", bounds.build(), node.getSource().getOutputSymbols()); + + for (FunctionCall call : node.getWindowFunctions().values()) { + Set dependencies = DependencyExtractor.extractUnique(call); + checkDependencies(inputs, dependencies, "Invalid node. Window function dependencies (%s) not in source plan output (%s)", dependencies, node.getSource().getOutputSymbols()); + } + + return null; + } + + @Override + public Void visitTopNRowNumber(TopNRowNumberNode node, Void context) + { + PlanNode source = node.getSource(); + source.accept(this, context); // visit child + + verifyUniqueId(node); + + Set inputs = ImmutableSet.copyOf(source.getOutputSymbols()); + checkDependencies(inputs, node.getPartitionBy(), "Invalid node. Partition by symbols (%s) not in source plan output (%s)", node.getPartitionBy(), node.getSource().getOutputSymbols()); + checkDependencies(inputs, node.getOrderBy(), "Invalid node. Order by symbols (%s) not in source plan output (%s)", node.getOrderBy(), node.getSource().getOutputSymbols()); + + return null; + } + + @Override + public Void visitRowNumber(RowNumberNode node, Void context) + { + PlanNode source = node.getSource(); + source.accept(this, context); // visit child + + verifyUniqueId(node); + + checkDependencies(source.getOutputSymbols(), node.getPartitionBy(), "Invalid node. Partition by symbols (%s) not in source plan output (%s)", node.getPartitionBy(), node.getSource().getOutputSymbols()); + + return null; + } + + @Override + public Void visitFilter(FilterNode node, Void context) + { + PlanNode source = node.getSource(); + source.accept(this, context); // visit child + + verifyUniqueId(node); + + Set inputs = ImmutableSet.copyOf(source.getOutputSymbols()); + checkDependencies(inputs, node.getOutputSymbols(), "Invalid node. Output symbols (%s) not in source plan output (%s)", node.getOutputSymbols(), node.getSource().getOutputSymbols()); + + Set dependencies = DependencyExtractor.extractUnique(node.getPredicate()); + checkDependencies(inputs, dependencies, "Invalid node. Predicate dependencies (%s) not in source plan output (%s)", dependencies, node.getSource().getOutputSymbols()); + + return null; + } + + @Override + public Void visitSample(SampleNode node, Void context) + { + PlanNode source = node.getSource(); + source.accept(this, context); // visit child + + verifyUniqueId(node); + + return null; + } + + @Override + public Void visitProject(ProjectNode node, Void context) + { + PlanNode source = node.getSource(); + source.accept(this, context); // visit child + + verifyUniqueId(node); + + Set inputs = ImmutableSet.copyOf(source.getOutputSymbols()); + for (Expression expression : node.getExpressions()) { + Set dependencies = DependencyExtractor.extractUnique(expression); + checkDependencies(inputs, dependencies, "Invalid node. Expression dependencies (%s) not in source plan output (%s)", dependencies, inputs); + } + + return null; + } + + @Override + public Void visitTopN(TopNNode node, Void context) + { + PlanNode source = node.getSource(); + source.accept(this, context); // visit child + + verifyUniqueId(node); + + Set inputs = ImmutableSet.copyOf(source.getOutputSymbols()); + checkDependencies(inputs, node.getOutputSymbols(), "Invalid node. Output symbols (%s) not in source plan output (%s)", node.getOutputSymbols(), node.getSource().getOutputSymbols()); + checkDependencies(inputs, node.getOrderBy(), + "Invalid node. Order by dependencies (%s) not in source plan output (%s)", + node.getOrderBy(), + node.getSource().getOutputSymbols()); + + return null; + } + + @Override + public Void visitSort(SortNode node, Void context) + { + PlanNode source = node.getSource(); + source.accept(this, context); // visit child + + verifyUniqueId(node); + + Set inputs = ImmutableSet.copyOf(source.getOutputSymbols()); + checkDependencies(inputs, node.getOutputSymbols(), "Invalid node. Output symbols (%s) not in source plan output (%s)", node.getOutputSymbols(), node.getSource().getOutputSymbols()); + checkDependencies(inputs, node.getOrderBy(), "Invalid node. Order by dependencies (%s) not in source plan output (%s)", node.getOrderBy(), node.getSource().getOutputSymbols()); + + return null; + } + + @Override + public Void visitOutput(OutputNode node, Void context) + { + PlanNode source = node.getSource(); + source.accept(this, context); // visit child + + verifyUniqueId(node); + + checkDependencies(source.getOutputSymbols(), node.getOutputSymbols(), "Invalid node. Output column dependencies (%s) not in source plan output (%s)", node.getOutputSymbols(), source.getOutputSymbols()); + + return null; + } + + @Override + public Void visitLimit(LimitNode node, Void context) + { + PlanNode source = node.getSource(); + source.accept(this, context); // visit child + + verifyUniqueId(node); + + return null; + } + + @Override + public Void visitDistinctLimit(DistinctLimitNode node, Void context) + { + PlanNode source = node.getSource(); + source.accept(this, context); // visit child + + verifyUniqueId(node); + return null; + } + + @Override + public Void visitJoin(JoinNode node, Void context) + { + node.getLeft().accept(this, context); + node.getRight().accept(this, context); + + verifyUniqueId(node); + + Set leftInputs = ImmutableSet.copyOf(node.getLeft().getOutputSymbols()); + Set rightInputs = ImmutableSet.copyOf(node.getRight().getOutputSymbols()); + + for (JoinNode.EquiJoinClause clause : node.getCriteria()) { + checkArgument(leftInputs.contains(clause.getLeft()), "Symbol from join clause (%s) not in left source (%s)", clause.getLeft(), node.getLeft().getOutputSymbols()); + checkArgument(rightInputs.contains(clause.getRight()), "Symbol from join clause (%s) not in right source (%s)", clause.getRight(), node.getRight().getOutputSymbols()); + } + + return null; + } + + @Override + public Void visitSemiJoin(SemiJoinNode node, Void context) + { + node.getSource().accept(this, context); + node.getFilteringSource().accept(this, context); + + verifyUniqueId(node); + + checkArgument(node.getSource().getOutputSymbols().contains(node.getSourceJoinSymbol()), "Symbol from semi join clause (%s) not in source (%s)", node.getSourceJoinSymbol(), node.getSource().getOutputSymbols()); + checkArgument(node.getFilteringSource().getOutputSymbols().contains(node.getFilteringSourceJoinSymbol()), "Symbol from semi join clause (%s) not in filtering source (%s)", node.getSourceJoinSymbol(), node.getFilteringSource().getOutputSymbols()); + + Set outputs = ImmutableSet.copyOf(node.getOutputSymbols()); + checkArgument(outputs.containsAll(node.getSource().getOutputSymbols()), "Semi join output symbols (%s) must contain all of the source symbols (%s)", node.getOutputSymbols(), node.getSource().getOutputSymbols()); + checkArgument(outputs.contains(node.getSemiJoinOutput()), + "Semi join output symbols (%s) must contain join result (%s)", + node.getOutputSymbols(), + node.getSemiJoinOutput()); + + return null; + } + + @Override + public Void visitIndexJoin(IndexJoinNode node, Void context) + { + node.getProbeSource().accept(this, context); + node.getIndexSource().accept(this, context); + + verifyUniqueId(node); + + Set probeInputs = ImmutableSet.copyOf(node.getProbeSource().getOutputSymbols()); + Set indexSourceInputs = ImmutableSet.copyOf(node.getIndexSource().getOutputSymbols()); + for (IndexJoinNode.EquiJoinClause clause : node.getCriteria()) { + checkArgument(probeInputs.contains(clause.getProbe()), "Probe symbol from index join clause (%s) not in probe source (%s)", clause.getProbe(), node.getProbeSource().getOutputSymbols()); + checkArgument(indexSourceInputs.contains(clause.getIndex()), "Index symbol from index join clause (%s) not in index source (%s)", clause.getIndex(), node.getIndexSource().getOutputSymbols()); + } + + Set lookupSymbols = FluentIterable.from(node.getCriteria()) + .transform(IndexJoinNode.EquiJoinClause::getIndex) + .toSet(); + Map trace = IndexKeyTracer.trace(node.getIndexSource(), lookupSymbols); + checkArgument(!trace.isEmpty() && lookupSymbols.containsAll(trace.keySet()), + "Index lookup symbols are not traceable to index source: %s", + lookupSymbols); + + return null; + } + + @Override + public Void visitIndexSource(IndexSourceNode node, Void context) + { + verifyUniqueId(node); + + checkDependencies(node.getOutputSymbols(), node.getLookupSymbols(), "Lookup symbols must be part of output symbols"); + checkDependencies(node.getAssignments().keySet(), node.getOutputSymbols(), "Assignments must contain mappings for output symbols"); + + return null; + } + + @Override + public Void visitTableScan(TableScanNode node, Void context) + { + verifyUniqueId(node); + + checkArgument(node.getAssignments().keySet().containsAll(node.getOutputSymbols()), "Assignments must contain mappings for output symbols"); + + return null; + } + + @Override + public Void visitValues(ValuesNode node, Void context) + { + verifyUniqueId(node); + return null; + } + + @Override + public Void visitUnnest(UnnestNode node, Void context) + { + PlanNode source = node.getSource(); + source.accept(this, context); + + verifyUniqueId(node); + + Set required = ImmutableSet.builder() + .addAll(node.getReplicateSymbols()) + .addAll(node.getUnnestSymbols().keySet()) + .build(); + + checkDependencies(source.getOutputSymbols(), required, "Invalid node. Dependencies (%s) not in source plan output (%s)", required, source.getOutputSymbols()); + + return null; + } + + @Override + public Void visitRemoteSource(RemoteSourceNode node, Void context) + { + verifyUniqueId(node); + + return null; + } + + @Override + public Void visitExchange(ExchangeNode node, Void context) + { + verifyUniqueId(node); + + return null; + } + + @Override + public Void visitTableWriter(TableWriterNode node, Void context) + { + PlanNode source = node.getSource(); + source.accept(this, context); // visit child + + verifyUniqueId(node); + + if (node.getSampleWeightSymbol().isPresent()) { + checkArgument(source.getOutputSymbols().contains(node.getSampleWeightSymbol().get()), "Invalid node. Sample weight symbol (%s) is not in source plan output (%s)", node.getSampleWeightSymbol().get(), node.getSource().getOutputSymbols()); + } + + return null; + } + + @Override + public Void visitDelete(DeleteNode node, Void context) + { + PlanNode source = node.getSource(); + source.accept(this, context); // visit child + + verifyUniqueId(node); + + checkArgument(source.getOutputSymbols().contains(node.getRowId()), "Invalid node. Row ID symbol (%s) is not in source plan output (%s)", node.getRowId(), node.getSource().getOutputSymbols()); + + return null; + } + + @Override + public Void visitTableCommit(TableCommitNode node, Void context) + { + node.getSource().accept(this, context); // visit child + + verifyUniqueId(node); + + return null; + } + + @Override + public Void visitUnion(UnionNode node, Void context) + { + for (int i = 0; i < node.getSources().size(); i++) { + PlanNode subplan = node.getSources().get(i); + checkDependencies(subplan.getOutputSymbols(), node.sourceOutputLayout(i), "UNION subplan must provide all of the necessary symbols"); + subplan.accept(this, context); // visit child + } + + verifyUniqueId(node); + + return null; + } + + private void verifyUniqueId(PlanNode node) + { + PlanNodeId id = node.getId(); + checkArgument(!nodesById.containsKey(id), "Duplicate node id found %s between %s and %s", node.getId(), node, nodesById.get(id)); + + nodesById.put(id, node); + } + } + + private static void checkDependencies(Collection inputs, Collection required, String message, Object... parameters) + { + checkArgument(ImmutableSet.copyOf(inputs).containsAll(required), message, parameters); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/ProjectionPushDown.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/ProjectionPushDown.java new file mode 100644 index 00000000..e78345c7 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/ProjectionPushDown.java @@ -0,0 +1,126 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner; + +import com.facebook.presto.Session; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.planner.optimizations.PlanOptimizer; +import com.facebook.presto.sql.planner.plan.PlanNode; +import com.facebook.presto.sql.planner.plan.PlanRewriter; +import com.facebook.presto.sql.planner.plan.ProjectNode; +import com.facebook.presto.sql.planner.plan.UnionNode; +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.ExpressionRewriter; +import com.facebook.presto.sql.tree.ExpressionTreeRewriter; +import com.facebook.presto.sql.tree.QualifiedNameReference; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.ImmutableMap; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.google.common.base.Preconditions.checkState; +import static java.util.Objects.requireNonNull; + +public class ProjectionPushDown + extends PlanOptimizer +{ + @Override + public PlanNode optimize(PlanNode plan, Session session, Map types, SymbolAllocator symbolAllocator, PlanNodeIdAllocator idAllocator) + { + requireNonNull(plan, "plan is null"); + requireNonNull(session, "session is null"); + requireNonNull(types, "types is null"); + requireNonNull(symbolAllocator, "symbolAllocator is null"); + requireNonNull(idAllocator, "idAllocator is null"); + + return PlanRewriter.rewriteWith(new Rewriter(idAllocator, symbolAllocator), plan); + } + + private static class Rewriter + extends PlanRewriter + { + private final PlanNodeIdAllocator idAllocator; + private final SymbolAllocator symbolAllocator; + + public Rewriter(PlanNodeIdAllocator idAllocator, SymbolAllocator symbolAllocator) + { + this.idAllocator = requireNonNull(idAllocator, "idAllocator is null"); + this.symbolAllocator = requireNonNull(symbolAllocator, "symbolAllocator is null"); + } + + /** + * Convert a plan of the shape ... -> Project -> Union -> ... to ... -> Union -> Project -> ... + */ + @Override + public PlanNode visitProject(ProjectNode node, RewriteContext context) + { + // If we have a Project on a Union, push the Project through the Union + PlanNode source = context.rewrite(node.getSource()); + + if (!(source instanceof UnionNode)) { + return context.replaceChildren(node, ImmutableList.of(source)); + } + + UnionNode unionNode = (UnionNode) source; + + // OutputLayout of the resultant Union, will be same as the layout of the Project + List outputLayout = node.getOutputSymbols(); + + // Mapping from the output symbol to ordered list of symbols from each of the sources + ImmutableListMultimap.Builder mappings = ImmutableListMultimap.builder(); + + // sources for the resultant UnionNode + ImmutableList.Builder outputSources = ImmutableList.builder(); + + for (int i = 0; i < unionNode.getSources().size(); i++) { + Map outputToInput = unionNode.sourceSymbolMap(i); // Map: output of union -> input of this source to the union + ImmutableMap.Builder assignments = ImmutableMap.builder(); // assignments for the new ProjectNode + + // mapping from current ProjectNode to new ProjectNode, used to identify the output layout + Map projectSymbolMapping = new HashMap<>(); + + // Translate the assignments in the ProjectNode using symbols of the source of the UnionNode + for (Map.Entry entry : node.getAssignments().entrySet()) { + Expression translatedExpression = translateExpression(entry.getValue(), outputToInput); + Type type = symbolAllocator.getTypes().get(entry.getKey()); + Symbol symbol = symbolAllocator.newSymbol(translatedExpression, type); + assignments.put(symbol, translatedExpression); + projectSymbolMapping.put(entry.getKey(), symbol); + } + outputSources.add(new ProjectNode(idAllocator.getNextId(), unionNode.getSources().get(i), assignments.build())); + outputLayout.forEach(symbol -> mappings.put(symbol, projectSymbolMapping.get(symbol))); + } + + return new UnionNode(node.getId(), outputSources.build(), mappings.build()); + } + } + + private static Expression translateExpression(Expression inputExpression, Map symbolMapping) + { + return ExpressionTreeRewriter.rewriteWith(new ExpressionRewriter() + { + @Override + public Expression rewriteQualifiedNameReference(QualifiedNameReference node, Void context, ExpressionTreeRewriter treeRewriter) + { + QualifiedNameReference qualifiedNameReference = symbolMapping.get(Symbol.fromQualifiedName(node.getName())); + checkState(qualifiedNameReference != null, "Cannot resolve symbol %s", node.getName()); + + return qualifiedNameReference; + } + }, inputExpression); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/QueryPlanner.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/QueryPlanner.java new file mode 100644 index 00000000..03e08be0 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/QueryPlanner.java @@ -0,0 +1,749 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner; + +import com.facebook.presto.Session; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.metadata.TableHandle; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.TupleDomain; +import com.facebook.presto.spi.block.SortOrder; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.analyzer.Analysis; +import com.facebook.presto.sql.analyzer.Field; +import com.facebook.presto.sql.analyzer.FieldOrExpression; +import com.facebook.presto.sql.analyzer.TupleDescriptor; +import com.facebook.presto.sql.planner.plan.AggregationNode; +import com.facebook.presto.sql.planner.plan.DeleteNode; +import com.facebook.presto.sql.planner.plan.FilterNode; +import com.facebook.presto.sql.planner.plan.LimitNode; +import com.facebook.presto.sql.planner.plan.MarkDistinctNode; +import com.facebook.presto.sql.planner.plan.PlanNode; +import com.facebook.presto.sql.planner.plan.ProjectNode; +import com.facebook.presto.sql.planner.plan.SemiJoinNode; +import com.facebook.presto.sql.planner.plan.SortNode; +import com.facebook.presto.sql.planner.plan.TableScanNode; +import com.facebook.presto.sql.planner.plan.TableWriterNode.DeleteHandle; +import com.facebook.presto.sql.planner.plan.TopNNode; +import com.facebook.presto.sql.planner.plan.ValuesNode; +import com.facebook.presto.sql.planner.plan.WindowNode; +import com.facebook.presto.sql.tree.Cast; +import com.facebook.presto.sql.tree.DefaultTraversalVisitor; +import com.facebook.presto.sql.tree.Delete; +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.FrameBound; +import com.facebook.presto.sql.tree.FunctionCall; +import com.facebook.presto.sql.tree.InPredicate; +import com.facebook.presto.sql.tree.QualifiedNameReference; +import com.facebook.presto.sql.tree.Query; +import com.facebook.presto.sql.tree.QuerySpecification; +import com.facebook.presto.sql.tree.SortItem; +import com.facebook.presto.sql.tree.SortItem.NullOrdering; +import com.facebook.presto.sql.tree.SortItem.Ordering; +import com.facebook.presto.sql.tree.SubqueryExpression; +import com.facebook.presto.sql.tree.Window; +import com.facebook.presto.sql.tree.WindowFrame; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.BooleanType.BOOLEAN; +import static com.facebook.presto.spi.type.VarbinaryType.VARBINARY; +import static com.facebook.presto.util.ImmutableCollectors.toImmutableSet; +import static com.google.common.base.MoreObjects.firstNonNull; +import static com.google.common.base.Preconditions.checkState; + +class QueryPlanner + extends DefaultTraversalVisitor +{ + private final Analysis analysis; + private final SymbolAllocator symbolAllocator; + private final PlanNodeIdAllocator idAllocator; + private final Metadata metadata; + private final Session session; + + QueryPlanner(Analysis analysis, SymbolAllocator symbolAllocator, PlanNodeIdAllocator idAllocator, Metadata metadata, Session session) + { + Preconditions.checkNotNull(analysis, "analysis is null"); + Preconditions.checkNotNull(symbolAllocator, "symbolAllocator is null"); + Preconditions.checkNotNull(idAllocator, "idAllocator is null"); + Preconditions.checkNotNull(metadata, "metadata is null"); + Preconditions.checkNotNull(session, "session is null"); + + this.analysis = analysis; + this.symbolAllocator = symbolAllocator; + this.idAllocator = idAllocator; + this.metadata = metadata; + this.session = session; + } + + @Override + protected PlanBuilder visitQuery(Query query, Void context) + { + PlanBuilder builder = planQueryBody(query); + Set inPredicates = analysis.getInPredicates(query); + builder = appendSemiJoins(builder, inPredicates); + + List orderBy = analysis.getOrderByExpressions(query); + List outputs = analysis.getOutputExpressions(query); + builder = project(builder, Iterables.concat(orderBy, outputs)); + + builder = sort(builder, query); + builder = project(builder, analysis.getOutputExpressions(query)); + builder = limit(builder, query); + + return builder; + } + + @Override + protected PlanBuilder visitQuerySpecification(QuerySpecification node, Void context) + { + PlanBuilder builder = planFrom(node); + + Set inPredicates = analysis.getInPredicates(node); + builder = appendSemiJoins(builder, inPredicates); + + builder = filter(builder, analysis.getWhere(node)); + builder = aggregate(builder, node); + builder = filter(builder, analysis.getHaving(node)); + + builder = window(builder, node); + + List orderBy = analysis.getOrderByExpressions(node); + List outputs = analysis.getOutputExpressions(node); + builder = project(builder, Iterables.concat(orderBy, outputs)); + + builder = distinct(builder, node, outputs, orderBy); + builder = sort(builder, node); + builder = project(builder, analysis.getOutputExpressions(node)); + builder = limit(builder, node); + + return builder; + } + + private PlanBuilder planQueryBody(Query query) + { + RelationPlan relationPlan = new RelationPlanner(analysis, symbolAllocator, idAllocator, metadata, session) + .process(query.getQueryBody(), null); + + TranslationMap translations = new TranslationMap(relationPlan, analysis); + + // Make field->symbol mapping from underlying relation plan available for translations + // This makes it possible to rewrite FieldOrExpressions that reference fields from the QuerySpecification directly + translations.setFieldMappings(relationPlan.getOutputSymbols()); + + return new PlanBuilder(translations, relationPlan.getRoot(), relationPlan.getSampleWeight()); + } + + private PlanBuilder planFrom(QuerySpecification node) + { + RelationPlan relationPlan; + + if (node.getFrom().isPresent()) { + relationPlan = new RelationPlanner(analysis, symbolAllocator, idAllocator, metadata, session) + .process(node.getFrom().get(), null); + } + else { + relationPlan = planImplicitTable(); + } + + TranslationMap translations = new TranslationMap(relationPlan, analysis); + + // Make field->symbol mapping from underlying relation plan available for translations + // This makes it possible to rewrite FieldOrExpressions that reference fields from the FROM clause directly + translations.setFieldMappings(relationPlan.getOutputSymbols()); + + return new PlanBuilder(translations, relationPlan.getRoot(), relationPlan.getSampleWeight()); + } + + public DeleteNode planDelete(Delete node) + { + TupleDescriptor descriptor = analysis.getOutputDescriptor(node.getTable()); + TableHandle handle = analysis.getTableHandle(node.getTable()); + ColumnHandle rowIdHandle = metadata.getUpdateRowIdColumnHandle(handle); + Type rowIdType = metadata.getColumnMetadata(handle, rowIdHandle).getType(); + + // add table columns + ImmutableList.Builder outputSymbols = ImmutableList.builder(); + ImmutableMap.Builder columns = ImmutableMap.builder(); + ImmutableList.Builder fields = ImmutableList.builder(); + for (Field field : descriptor.getAllFields()) { + Symbol symbol = symbolAllocator.newSymbol(field.getName().get(), field.getType()); + outputSymbols.add(symbol); + columns.put(symbol, analysis.getColumn(field)); + fields.add(field); + } + + // add rowId column + Field rowIdField = Field.newUnqualified(Optional.empty(), rowIdType); + Symbol rowIdSymbol = symbolAllocator.newSymbol("$rowId", rowIdField.getType()); + outputSymbols.add(rowIdSymbol); + columns.put(rowIdSymbol, rowIdHandle); + fields.add(rowIdField); + + // create table scan + PlanNode tableScan = new TableScanNode(idAllocator.getNextId(), handle, outputSymbols.build(), columns.build(), Optional.empty(), TupleDomain.all(), null); + RelationPlan relationPlan = new RelationPlan(tableScan, new TupleDescriptor(fields.build()), outputSymbols.build(), Optional.empty()); + + TranslationMap translations = new TranslationMap(relationPlan, analysis); + translations.setFieldMappings(relationPlan.getOutputSymbols()); + + PlanBuilder builder = new PlanBuilder(translations, relationPlan.getRoot(), relationPlan.getSampleWeight()); + + // add semi-joins and filters + Set inPredicates = analysis.getInPredicates(node); + builder = appendSemiJoins(builder, inPredicates); + + if (node.getWhere().isPresent()) { + builder = filter(builder, node.getWhere().get()); + } + + // create delete node + Symbol rowId = builder.translate(new FieldOrExpression(relationPlan.getDescriptor().indexOf(rowIdField))); + List outputs = ImmutableList.of( + symbolAllocator.newSymbol("partialrows", BIGINT), + symbolAllocator.newSymbol("fragment", VARBINARY)); + + return new DeleteNode(idAllocator.getNextId(), builder.getRoot(), new DeleteHandle(handle), rowId, outputs); + } + + private RelationPlan planImplicitTable() + { + List emptyRow = ImmutableList.of(); + return new RelationPlan( + new ValuesNode(idAllocator.getNextId(), ImmutableList.of(), ImmutableList.of(emptyRow)), + new TupleDescriptor(), + ImmutableList.of(), + Optional.empty()); + } + + private PlanBuilder filter(PlanBuilder subPlan, Expression predicate) + { + if (predicate == null) { + return subPlan; + } + + Expression rewritten = subPlan.rewrite(predicate); + return new PlanBuilder(subPlan.getTranslations(), new FilterNode(idAllocator.getNextId(), subPlan.getRoot(), rewritten), subPlan.getSampleWeight()); + } + + private PlanBuilder project(PlanBuilder subPlan, Iterable expressions) + { + TranslationMap outputTranslations = new TranslationMap(subPlan.getRelationPlan(), analysis); + + ImmutableMap.Builder projections = ImmutableMap.builder(); + for (FieldOrExpression fieldOrExpression : ImmutableSet.copyOf(expressions)) { + Symbol symbol; + + if (fieldOrExpression.isFieldReference()) { + Field field = subPlan.getRelationPlan().getDescriptor().getFieldByIndex(fieldOrExpression.getFieldIndex()); + symbol = symbolAllocator.newSymbol(field); + } + else { + Expression expression = fieldOrExpression.getExpression(); + symbol = symbolAllocator.newSymbol(expression, analysis.getType(expression)); + } + + projections.put(symbol, subPlan.rewrite(fieldOrExpression)); + outputTranslations.put(fieldOrExpression, symbol); + } + + if (subPlan.getSampleWeight().isPresent()) { + Symbol symbol = subPlan.getSampleWeight().get(); + projections.put(symbol, new QualifiedNameReference(symbol.toQualifiedName())); + } + + return new PlanBuilder(outputTranslations, new ProjectNode(idAllocator.getNextId(), subPlan.getRoot(), projections.build()), subPlan.getSampleWeight()); + } + + private Map coerce(Iterable expressions, PlanBuilder subPlan, TranslationMap translations) + { + ImmutableMap.Builder projections = ImmutableMap.builder(); + + for (Expression expression : expressions) { + Type coercion = analysis.getCoercion(expression); + Symbol symbol = symbolAllocator.newSymbol(expression, firstNonNull(coercion, analysis.getType(expression))); + Expression rewritten = subPlan.rewrite(expression); + if (coercion != null) { + rewritten = new Cast(rewritten, coercion.getTypeSignature().toString()); + } + projections.put(symbol, rewritten); + translations.put(expression, symbol); + } + + return projections.build(); + } + + private PlanBuilder explicitCoercionFields(PlanBuilder subPlan, Iterable alreadyCoerced, Iterable uncoerced) + { + TranslationMap translations = new TranslationMap(subPlan.getRelationPlan(), analysis); + ImmutableMap.Builder projections = ImmutableMap.builder(); + + projections.putAll(coerce(uncoerced, subPlan, translations)); + + for (FieldOrExpression fieldOrExpression : alreadyCoerced) { + Symbol symbol; + if (fieldOrExpression.isFieldReference()) { + Field field = subPlan.getRelationPlan().getDescriptor().getFieldByIndex(fieldOrExpression.getFieldIndex()); + symbol = symbolAllocator.newSymbol(field); + } + else { + symbol = symbolAllocator.newSymbol(fieldOrExpression.getExpression(), analysis.getType(fieldOrExpression.getExpression())); + } + Expression rewritten = subPlan.rewrite(fieldOrExpression); + projections.put(symbol, rewritten); + translations.put(fieldOrExpression, symbol); + } + + return new PlanBuilder(translations, new ProjectNode(idAllocator.getNextId(), subPlan.getRoot(), projections.build()), subPlan.getSampleWeight()); + } + + private PlanBuilder explicitCoercionSymbols(PlanBuilder subPlan, Iterable alreadyCoerced, Iterable uncoerced) + { + TranslationMap translations = new TranslationMap(subPlan.getRelationPlan(), analysis); + translations.copyMappingsFrom(subPlan.getTranslations()); + ImmutableMap.Builder projections = ImmutableMap.builder(); + + projections.putAll(coerce(uncoerced, subPlan, translations)); + + for (Symbol symbol : alreadyCoerced) { + projections.put(symbol, new QualifiedNameReference(symbol.toQualifiedName())); + } + + return new PlanBuilder(translations, new ProjectNode(idAllocator.getNextId(), subPlan.getRoot(), projections.build()), subPlan.getSampleWeight()); + } + + private PlanBuilder aggregate(PlanBuilder subPlan, QuerySpecification node) + { + if (analysis.getAggregates(node).isEmpty() && analysis.getGroupByExpressions(node).isEmpty()) { + return subPlan; + } + + Set arguments = analysis.getAggregates(node).stream() + .map(FunctionCall::getArguments) + .flatMap(List::stream) + .map(FieldOrExpression::new) + .collect(toImmutableSet()); + + // 1. Pre-project all scalar inputs (arguments and non-trivial group by expressions) + Iterable inputs = Iterables.concat(analysis.getGroupByExpressions(node), arguments); + if (!Iterables.isEmpty(inputs)) { // avoid an empty projection if the only aggregation is COUNT (which has no arguments) + subPlan = project(subPlan, inputs); + } + + // 2. Aggregate + ImmutableMap.Builder aggregationAssignments = ImmutableMap.builder(); + ImmutableMap.Builder functions = ImmutableMap.builder(); + + // 2.a. Rewrite aggregates in terms of pre-projected inputs + TranslationMap translations = new TranslationMap(subPlan.getRelationPlan(), analysis); + boolean needPostProjectionCoercion = false; + for (FunctionCall aggregate : analysis.getAggregates(node)) { + Expression rewritten = subPlan.rewrite(aggregate); + Symbol newSymbol = symbolAllocator.newSymbol(rewritten, analysis.getType(aggregate)); + + // TODO: this is a hack, because we apply coercions to the output of expressions, rather than the arguments to expressions. + // Therefore we can end up with this implicit cast, and have to move it into a post-projection + if (rewritten instanceof Cast) { + rewritten = ((Cast) rewritten).getExpression(); + needPostProjectionCoercion = true; + } + aggregationAssignments.put(newSymbol, (FunctionCall) rewritten); + translations.put(aggregate, newSymbol); + + functions.put(newSymbol, analysis.getFunctionInfo(aggregate).getSignature()); + } + + // 2.b. Rewrite group by expressions in terms of pre-projected inputs + Set groupBySymbols = new LinkedHashSet<>(); + for (FieldOrExpression fieldOrExpression : analysis.getGroupByExpressions(node)) { + Symbol symbol = subPlan.translate(fieldOrExpression); + groupBySymbols.add(symbol); + translations.put(fieldOrExpression, symbol); + } + + // 2.c. Mark distinct rows for each aggregate that has DISTINCT + // Map from aggregate function arguments to marker symbols, so that we can reuse the markers, if two aggregates have the same argument + Map, Symbol> argumentMarkers = new HashMap<>(); + // Map from aggregate functions to marker symbols + Map masks = new HashMap<>(); + for (FunctionCall aggregate : Iterables.filter(analysis.getAggregates(node), FunctionCall::isDistinct)) { + Set args = ImmutableSet.copyOf(aggregate.getArguments()); + Symbol marker = argumentMarkers.get(args); + Symbol aggregateSymbol = translations.get(aggregate); + if (marker == null) { + if (args.size() == 1) { + marker = symbolAllocator.newSymbol(Iterables.getOnlyElement(args), BOOLEAN, "distinct"); + } + else { + marker = symbolAllocator.newSymbol(aggregateSymbol.getName(), BOOLEAN, "distinct"); + } + argumentMarkers.put(args, marker); + } + + masks.put(aggregateSymbol, marker); + } + + for (Map.Entry, Symbol> entry : argumentMarkers.entrySet()) { + ImmutableList.Builder builder = ImmutableList.builder(); + builder.addAll(groupBySymbols); + for (Expression expression : entry.getKey()) { + builder.add(subPlan.translate(expression)); + } + MarkDistinctNode markDistinct = new MarkDistinctNode(idAllocator.getNextId(), + subPlan.getRoot(), + entry.getValue(), + builder.build(), + Optional.empty()); + subPlan = new PlanBuilder(subPlan.getTranslations(), markDistinct, subPlan.getSampleWeight()); + } + + double confidence = 1.0; + if (analysis.getQuery().getApproximate().isPresent()) { + confidence = Double.valueOf(analysis.getQuery().getApproximate().get().getConfidence()) / 100.0; + } + + AggregationNode aggregationNode = new AggregationNode( + idAllocator.getNextId(), + subPlan.getRoot(), + ImmutableList.copyOf(groupBySymbols), + aggregationAssignments.build(), + functions.build(), + masks, + AggregationNode.Step.SINGLE, + subPlan.getSampleWeight(), + confidence, + Optional.empty()); + + subPlan = new PlanBuilder(translations, aggregationNode, Optional.empty()); + + // 3. Post-projection + // Add back the implicit casts that we removed in 2.a + // TODO: this is a hack, we should change type coercions to coerce the inputs to functions/operators instead of coercing the output + if (needPostProjectionCoercion) { + return explicitCoercionFields(subPlan, analysis.getGroupByExpressions(node), analysis.getAggregates(node)); + } + return subPlan; + } + + private PlanBuilder window(PlanBuilder subPlan, QuerySpecification node) + { + Set windowFunctions = ImmutableSet.copyOf(analysis.getWindowFunctions(node)); + if (windowFunctions.isEmpty()) { + return subPlan; + } + + for (FunctionCall windowFunction : windowFunctions) { + Window window = windowFunction.getWindow().get(); + + // Extract frame + WindowFrame.Type frameType = WindowFrame.Type.RANGE; + FrameBound.Type frameStartType = FrameBound.Type.UNBOUNDED_PRECEDING; + FrameBound.Type frameEndType = FrameBound.Type.CURRENT_ROW; + Expression frameStart = null; + Expression frameEnd = null; + + if (window.getFrame().isPresent()) { + WindowFrame frame = window.getFrame().get(); + frameType = frame.getType(); + + frameStartType = frame.getStart().getType(); + frameStart = frame.getStart().getValue().orElse(null); + + if (frame.getEnd().isPresent()) { + frameEndType = frame.getEnd().get().getType(); + frameEnd = frame.getEnd().get().getValue().orElse(null); + } + } + + // Pre-project inputs + ImmutableList.Builder inputs = ImmutableList.builder() + .addAll(windowFunction.getArguments()) + .addAll(window.getPartitionBy()) + .addAll(Iterables.transform(window.getOrderBy(), SortItem::getSortKey)); + + if (frameStart != null) { + inputs.add(frameStart); + } + if (frameEnd != null) { + inputs.add(frameEnd); + } + + subPlan = appendProjections(subPlan, inputs.build()); + + // Rewrite PARTITION BY in terms of pre-projected inputs + ImmutableList.Builder partitionBySymbols = ImmutableList.builder(); + for (Expression expression : window.getPartitionBy()) { + partitionBySymbols.add(subPlan.translate(expression)); + } + + // Rewrite ORDER BY in terms of pre-projected inputs + ImmutableList.Builder orderBySymbols = ImmutableList.builder(); + Map orderings = new HashMap<>(); + for (SortItem item : window.getOrderBy()) { + Symbol symbol = subPlan.translate(item.getSortKey()); + orderBySymbols.add(symbol); + orderings.put(symbol, toSortOrder(item)); + } + + // Rewrite frame bounds in terms of pre-projected inputs + Optional frameStartSymbol = Optional.empty(); + Optional frameEndSymbol = Optional.empty(); + if (frameStart != null) { + frameStartSymbol = Optional.of(subPlan.translate(frameStart)); + } + if (frameEnd != null) { + frameEndSymbol = Optional.of(subPlan.translate(frameEnd)); + } + + WindowNode.Frame frame = new WindowNode.Frame(frameType, + frameStartType, frameStartSymbol, + frameEndType, frameEndSymbol); + + TranslationMap outputTranslations = new TranslationMap(subPlan.getRelationPlan(), analysis); + outputTranslations.copyMappingsFrom(subPlan.getTranslations()); + + ImmutableMap.Builder assignments = ImmutableMap.builder(); + Map signatures = new HashMap<>(); + + // Rewrite function call in terms of pre-projected inputs + Expression rewritten = subPlan.rewrite(windowFunction); + Symbol newSymbol = symbolAllocator.newSymbol(rewritten, analysis.getType(windowFunction)); + + boolean needCoercion = rewritten instanceof Cast; + // Strip out the cast and add it back as a post-projection + if (rewritten instanceof Cast) { + rewritten = ((Cast) rewritten).getExpression(); + } + assignments.put(newSymbol, (FunctionCall) rewritten); + outputTranslations.put(windowFunction, newSymbol); + + signatures.put(newSymbol, analysis.getFunctionInfo(windowFunction).getSignature()); + + List sourceSymbols = subPlan.getRoot().getOutputSymbols(); + + // create window node + subPlan = new PlanBuilder(outputTranslations, + new WindowNode( + idAllocator.getNextId(), + subPlan.getRoot(), + partitionBySymbols.build(), + orderBySymbols.build(), + orderings, + frame, + assignments.build(), + signatures, + Optional.empty(), + ImmutableSet.of(), + 0), + subPlan.getSampleWeight()); + + if (needCoercion) { + subPlan = explicitCoercionSymbols(subPlan, sourceSymbols, ImmutableList.of(windowFunction)); + } + } + + return subPlan; + } + + private PlanBuilder appendProjections(PlanBuilder subPlan, Iterable expressions) + { + TranslationMap translations = new TranslationMap(subPlan.getRelationPlan(), analysis); + + // Carry over the translations from the source because we are appending projections + translations.copyMappingsFrom(subPlan.getTranslations()); + + ImmutableMap.Builder projections = ImmutableMap.builder(); + + // add an identity projection for underlying plan + for (Symbol symbol : subPlan.getRoot().getOutputSymbols()) { + Expression expression = new QualifiedNameReference(symbol.toQualifiedName()); + projections.put(symbol, expression); + } + + ImmutableMap.Builder newTranslations = ImmutableMap.builder(); + for (Expression expression : expressions) { + Symbol symbol = symbolAllocator.newSymbol(expression, analysis.getType(expression)); + + projections.put(symbol, translations.rewrite(expression)); + newTranslations.put(symbol, expression); + } + // Now append the new translations into the TranslationMap + for (Map.Entry entry : newTranslations.build().entrySet()) { + translations.put(entry.getValue(), entry.getKey()); + } + + return new PlanBuilder(translations, new ProjectNode(idAllocator.getNextId(), subPlan.getRoot(), projections.build()), subPlan.getSampleWeight()); + } + + private PlanBuilder appendSemiJoins(PlanBuilder subPlan, Set inPredicates) + { + for (InPredicate inPredicate : inPredicates) { + subPlan = appendSemiJoin(subPlan, inPredicate); + } + return subPlan; + } + + /** + * Semijoins are planned as follows: + * 1) SQL constructs that need to be semijoined are extracted during Analysis phase (currently only InPredicates so far) + * 2) Create a new SemiJoinNode that connects the semijoin lookup field with the planned subquery and have it output a new boolean + * symbol for the result of the semijoin. + * 3) Add an entry to the TranslationMap that notes to map the InPredicate into semijoin output symbol + *

+ * Currently, we only support semijoins deriving from InPredicates, but we will probably need + * to add support for more SQL constructs in the future. + */ + private PlanBuilder appendSemiJoin(PlanBuilder subPlan, InPredicate inPredicate) + { + TranslationMap translations = new TranslationMap(subPlan.getRelationPlan(), analysis); + translations.copyMappingsFrom(subPlan.getTranslations()); + + subPlan = appendProjections(subPlan, ImmutableList.of(inPredicate.getValue())); + Symbol sourceJoinSymbol = subPlan.translate(inPredicate.getValue()); + + Preconditions.checkState(inPredicate.getValueList() instanceof SubqueryExpression); + SubqueryExpression subqueryExpression = (SubqueryExpression) inPredicate.getValueList(); + RelationPlanner relationPlanner = new RelationPlanner(analysis, symbolAllocator, idAllocator, metadata, session); + RelationPlan valueListRelation = relationPlanner.process(subqueryExpression.getQuery(), null); + Symbol filteringSourceJoinSymbol = Iterables.getOnlyElement(valueListRelation.getRoot().getOutputSymbols()); + + Symbol semiJoinOutputSymbol = symbolAllocator.newSymbol("semijoinresult", BOOLEAN); + + translations.put(inPredicate, semiJoinOutputSymbol); + + return new PlanBuilder(translations, + new SemiJoinNode(idAllocator.getNextId(), + subPlan.getRoot(), + valueListRelation.getRoot(), + sourceJoinSymbol, + filteringSourceJoinSymbol, + semiJoinOutputSymbol, + Optional.empty(), + Optional.empty()), + subPlan.getSampleWeight()); + } + + private PlanBuilder distinct(PlanBuilder subPlan, QuerySpecification node, List outputs, List orderBy) + { + if (node.getSelect().isDistinct()) { + checkState(outputs.containsAll(orderBy), "Expected ORDER BY terms to be in SELECT. Broken analysis"); + + AggregationNode aggregation = new AggregationNode(idAllocator.getNextId(), + subPlan.getRoot(), + subPlan.getRoot().getOutputSymbols(), + ImmutableMap.of(), + ImmutableMap.of(), + ImmutableMap.of(), + AggregationNode.Step.SINGLE, + Optional.empty(), + 1.0, + Optional.empty()); + + return new PlanBuilder(subPlan.getTranslations(), aggregation, subPlan.getSampleWeight()); + } + + return subPlan; + } + + private PlanBuilder sort(PlanBuilder subPlan, Query node) + { + return sort(subPlan, node.getOrderBy(), node.getLimit(), analysis.getOrderByExpressions(node)); + } + + private PlanBuilder sort(PlanBuilder subPlan, QuerySpecification node) + { + return sort(subPlan, node.getOrderBy(), node.getLimit(), analysis.getOrderByExpressions(node)); + } + + private PlanBuilder sort(PlanBuilder subPlan, List orderBy, Optional limit, List orderByExpressions) + { + if (orderBy.isEmpty()) { + return subPlan; + } + + Iterator sortItems = orderBy.iterator(); + + ImmutableList.Builder orderBySymbols = ImmutableList.builder(); + ImmutableMap.Builder orderings = ImmutableMap.builder(); + for (FieldOrExpression fieldOrExpression : orderByExpressions) { + Symbol symbol = subPlan.translate(fieldOrExpression); + orderBySymbols.add(symbol); + + orderings.put(symbol, toSortOrder(sortItems.next())); + } + + PlanNode planNode; + if (limit.isPresent()) { + planNode = new TopNNode(idAllocator.getNextId(), subPlan.getRoot(), Long.parseLong(limit.get()), orderBySymbols.build(), orderings.build(), false); + } + else { + planNode = new SortNode(idAllocator.getNextId(), subPlan.getRoot(), orderBySymbols.build(), orderings.build()); + } + + return new PlanBuilder(subPlan.getTranslations(), planNode, subPlan.getSampleWeight()); + } + + private PlanBuilder limit(PlanBuilder subPlan, Query node) + { + return limit(subPlan, node.getOrderBy(), node.getLimit()); + } + + private PlanBuilder limit(PlanBuilder subPlan, QuerySpecification node) + { + return limit(subPlan, node.getOrderBy(), node.getLimit()); + } + + private PlanBuilder limit(PlanBuilder subPlan, List orderBy, Optional limit) + { + if (orderBy.isEmpty() && limit.isPresent()) { + long limitValue = Long.parseLong(limit.get()); + return new PlanBuilder(subPlan.getTranslations(), new LimitNode(idAllocator.getNextId(), subPlan.getRoot(), limitValue), subPlan.getSampleWeight()); + } + + return subPlan; + } + + private SortOrder toSortOrder(SortItem sortItem) + { + if (sortItem.getOrdering() == Ordering.ASCENDING) { + if (sortItem.getNullOrdering() == NullOrdering.FIRST) { + return SortOrder.ASC_NULLS_FIRST; + } + else { + return SortOrder.ASC_NULLS_LAST; + } + } + else { + if (sortItem.getNullOrdering() == NullOrdering.FIRST) { + return SortOrder.DESC_NULLS_FIRST; + } + else { + return SortOrder.DESC_NULLS_LAST; + } + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/RelationPlan.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/RelationPlan.java new file mode 100644 index 00000000..ce119a16 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/RelationPlan.java @@ -0,0 +1,75 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner; + +import com.facebook.presto.sql.analyzer.TupleDescriptor; +import com.facebook.presto.sql.planner.plan.PlanNode; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; + +import java.util.List; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +class RelationPlan +{ + private final PlanNode root; + private final List outputSymbols; + private final TupleDescriptor descriptor; + private final Optional sampleWeight; + + public RelationPlan(PlanNode root, TupleDescriptor descriptor, List outputSymbols, Optional sampleWeight) + { + checkNotNull(root, "root is null"); + checkNotNull(outputSymbols, "outputSymbols is null"); + checkNotNull(descriptor, "descriptor is null"); + checkNotNull(descriptor, "sampleWeight is null"); + + checkArgument(descriptor.getAllFieldCount() == outputSymbols.size(), + "Number of outputs (%s) doesn't match descriptor size (%s)", outputSymbols.size(), descriptor.getAllFieldCount()); + + this.root = root; + this.descriptor = descriptor; + this.outputSymbols = ImmutableList.copyOf(outputSymbols); + this.sampleWeight = sampleWeight; + } + + public Optional getSampleWeight() + { + return sampleWeight; + } + + public Symbol getSymbol(int fieldIndex) + { + Preconditions.checkArgument(fieldIndex >= 0 && fieldIndex < outputSymbols.size() && outputSymbols.get(fieldIndex) != null, "No field->symbol mapping for field %s", fieldIndex); + return outputSymbols.get(fieldIndex); + } + + public PlanNode getRoot() + { + return root; + } + + public List getOutputSymbols() + { + return outputSymbols; + } + + public TupleDescriptor getDescriptor() + { + return descriptor; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/RelationPlanner.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/RelationPlanner.java new file mode 100644 index 00000000..07085e55 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/RelationPlanner.java @@ -0,0 +1,747 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner; + +import com.facebook.presto.Session; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.metadata.TableHandle; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.TupleDomain; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.ExpressionUtils; +import com.facebook.presto.sql.analyzer.Analysis; +import com.facebook.presto.sql.analyzer.AnalysisContext; +import com.facebook.presto.sql.analyzer.ExpressionAnalyzer; +import com.facebook.presto.sql.analyzer.Field; +import com.facebook.presto.sql.analyzer.FieldOrExpression; +import com.facebook.presto.sql.analyzer.SemanticException; +import com.facebook.presto.sql.analyzer.TupleAnalyzer; +import com.facebook.presto.sql.analyzer.TupleDescriptor; +import com.facebook.presto.sql.planner.optimizations.CanonicalizeExpressions; +import com.facebook.presto.sql.planner.plan.AggregationNode; +import com.facebook.presto.sql.planner.plan.FilterNode; +import com.facebook.presto.sql.planner.plan.JoinNode; +import com.facebook.presto.sql.planner.plan.PlanNode; +import com.facebook.presto.sql.planner.plan.ProjectNode; +import com.facebook.presto.sql.planner.plan.SampleNode; +import com.facebook.presto.sql.planner.plan.SemiJoinNode; +import com.facebook.presto.sql.planner.plan.TableScanNode; +import com.facebook.presto.sql.planner.plan.UnionNode; +import com.facebook.presto.sql.planner.plan.UnnestNode; +import com.facebook.presto.sql.planner.plan.ValuesNode; +import com.facebook.presto.sql.tree.AliasedRelation; +import com.facebook.presto.sql.tree.ArithmeticBinaryExpression; +import com.facebook.presto.sql.tree.BooleanLiteral; +import com.facebook.presto.sql.tree.Cast; +import com.facebook.presto.sql.tree.CoalesceExpression; +import com.facebook.presto.sql.tree.ComparisonExpression; +import com.facebook.presto.sql.tree.DefaultTraversalVisitor; +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.ExpressionRewriter; +import com.facebook.presto.sql.tree.ExpressionTreeRewriter; +import com.facebook.presto.sql.tree.FunctionCall; +import com.facebook.presto.sql.tree.InPredicate; +import com.facebook.presto.sql.tree.InputReference; +import com.facebook.presto.sql.tree.Join; +import com.facebook.presto.sql.tree.LongLiteral; +import com.facebook.presto.sql.tree.QualifiedName; +import com.facebook.presto.sql.tree.QualifiedNameReference; +import com.facebook.presto.sql.tree.Query; +import com.facebook.presto.sql.tree.QuerySpecification; +import com.facebook.presto.sql.tree.Row; +import com.facebook.presto.sql.tree.SampledRelation; +import com.facebook.presto.sql.tree.SubqueryExpression; +import com.facebook.presto.sql.tree.Table; +import com.facebook.presto.sql.tree.TableSubquery; +import com.facebook.presto.sql.tree.Union; +import com.facebook.presto.sql.tree.Unnest; +import com.facebook.presto.sql.tree.Values; +import com.facebook.presto.type.ArrayType; +import com.facebook.presto.type.MapType; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; +import com.google.common.collect.UnmodifiableIterator; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.BooleanType.BOOLEAN; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.EXPRESSION_NOT_CONSTANT; +import static com.facebook.presto.sql.analyzer.SemanticErrorCode.NOT_SUPPORTED; +import static com.facebook.presto.sql.tree.ComparisonExpression.Type.EQUAL; +import static com.facebook.presto.sql.tree.ComparisonExpression.Type.GREATER_THAN; +import static com.facebook.presto.sql.tree.ComparisonExpression.Type.GREATER_THAN_OR_EQUAL; +import static com.facebook.presto.sql.tree.ComparisonExpression.Type.IS_DISTINCT_FROM; +import static com.facebook.presto.sql.tree.ComparisonExpression.Type.LESS_THAN; +import static com.facebook.presto.sql.tree.ComparisonExpression.Type.LESS_THAN_OR_EQUAL; +import static com.facebook.presto.sql.tree.ComparisonExpression.Type.NOT_EQUAL; +import static com.facebook.presto.sql.tree.Join.Type.INNER; +import static com.facebook.presto.util.ImmutableCollectors.toImmutableList; +import static com.facebook.presto.util.Types.checkType; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; + +class RelationPlanner + extends DefaultTraversalVisitor +{ + private final Analysis analysis; + private final SymbolAllocator symbolAllocator; + private final PlanNodeIdAllocator idAllocator; + private final Metadata metadata; + private final Session session; + + RelationPlanner(Analysis analysis, SymbolAllocator symbolAllocator, PlanNodeIdAllocator idAllocator, Metadata metadata, Session session) + { + Preconditions.checkNotNull(analysis, "analysis is null"); + Preconditions.checkNotNull(symbolAllocator, "symbolAllocator is null"); + Preconditions.checkNotNull(idAllocator, "idAllocator is null"); + Preconditions.checkNotNull(metadata, "metadata is null"); + Preconditions.checkNotNull(session, "session is null"); + + this.analysis = analysis; + this.symbolAllocator = symbolAllocator; + this.idAllocator = idAllocator; + this.metadata = metadata; + this.session = session; + } + + @Override + protected RelationPlan visitTable(Table node, Void context) + { + Query namedQuery = analysis.getNamedQuery(node); + if (namedQuery != null) { + RelationPlan subPlan = process(namedQuery, null); + return new RelationPlan(subPlan.getRoot(), analysis.getOutputDescriptor(node), subPlan.getOutputSymbols(), subPlan.getSampleWeight()); + } + + TupleDescriptor descriptor = analysis.getOutputDescriptor(node); + TableHandle handle = analysis.getTableHandle(node); + + ImmutableList.Builder outputSymbolsBuilder = ImmutableList.builder(); + ImmutableMap.Builder columns = ImmutableMap.builder(); + for (Field field : descriptor.getAllFields()) { + Symbol symbol = symbolAllocator.newSymbol(field.getName().get(), field.getType()); + + outputSymbolsBuilder.add(symbol); + columns.put(symbol, analysis.getColumn(field)); + } + + List planOutputSymbols = outputSymbolsBuilder.build(); + Optional sampleWeightColumn = metadata.getSampleWeightColumnHandle(handle); + Symbol sampleWeightSymbol = null; + if (sampleWeightColumn.isPresent()) { + sampleWeightSymbol = symbolAllocator.newSymbol("$sampleWeight", BIGINT); + outputSymbolsBuilder.add(sampleWeightSymbol); + columns.put(sampleWeightSymbol, sampleWeightColumn.get()); + } + + List nodeOutputSymbols = outputSymbolsBuilder.build(); + PlanNode root = new TableScanNode(idAllocator.getNextId(), handle, nodeOutputSymbols, columns.build(), Optional.empty(), TupleDomain.all(), null); + return new RelationPlan(root, descriptor, planOutputSymbols, Optional.ofNullable(sampleWeightSymbol)); + } + + @Override + protected RelationPlan visitAliasedRelation(AliasedRelation node, Void context) + { + RelationPlan subPlan = process(node.getRelation(), context); + + TupleDescriptor outputDescriptor = analysis.getOutputDescriptor(node); + + return new RelationPlan(subPlan.getRoot(), outputDescriptor, subPlan.getOutputSymbols(), subPlan.getSampleWeight()); + } + + @Override + protected RelationPlan visitSampledRelation(SampledRelation node, Void context) + { + if (node.getColumnsToStratifyOn().isPresent()) { + throw new UnsupportedOperationException("STRATIFY ON is not yet implemented"); + } + + RelationPlan subPlan = process(node.getRelation(), context); + + TupleDescriptor outputDescriptor = analysis.getOutputDescriptor(node); + double ratio = analysis.getSampleRatio(node); + Symbol sampleWeightSymbol = null; + if (node.getType() == SampledRelation.Type.POISSONIZED) { + sampleWeightSymbol = symbolAllocator.newSymbol("$sampleWeight", BIGINT); + } + PlanNode planNode = new SampleNode(idAllocator.getNextId(), + subPlan.getRoot(), + ratio, + SampleNode.Type.fromType(node.getType()), + node.isRescaled(), + Optional.ofNullable(sampleWeightSymbol)); + return new RelationPlan(planNode, outputDescriptor, subPlan.getOutputSymbols(), Optional.ofNullable(sampleWeightSymbol)); + } + + @Override + protected RelationPlan visitJoin(Join node, Void context) + { + // TODO: translate the RIGHT join into a mirrored LEFT join when we refactor (@martint) + RelationPlan leftPlan = process(node.getLeft(), context); + + // Convert CROSS JOIN UNNEST to an UnnestNode + if (node.getRight() instanceof Unnest || (node.getRight() instanceof AliasedRelation && ((AliasedRelation) node.getRight()).getRelation() instanceof Unnest)) { + Unnest unnest; + if (node.getRight() instanceof AliasedRelation) { + unnest = (Unnest) ((AliasedRelation) node.getRight()).getRelation(); + } + else { + unnest = (Unnest) node.getRight(); + } + if (node.getType() != Join.Type.CROSS && node.getType() != Join.Type.IMPLICIT) { + throw new SemanticException(NOT_SUPPORTED, unnest, "UNNEST only supported on the right side of CROSS JOIN"); + } + return planCrossJoinUnnest(leftPlan, node, unnest); + } + + RelationPlan rightPlan = process(node.getRight(), context); + + PlanBuilder leftPlanBuilder = initializePlanBuilder(leftPlan); + PlanBuilder rightPlanBuilder = initializePlanBuilder(rightPlan); + + TupleDescriptor outputDescriptor = analysis.getOutputDescriptor(node); + + // NOTE: symbols must be in the same order as the outputDescriptor + List outputSymbols = ImmutableList.builder() + .addAll(leftPlan.getOutputSymbols()) + .addAll(rightPlan.getOutputSymbols()) + .build(); + + ImmutableList.Builder equiClauses = ImmutableList.builder(); + Expression postInnerJoinCriteria = new BooleanLiteral("TRUE"); + if (node.getType() != Join.Type.CROSS && node.getType() != Join.Type.IMPLICIT) { + Expression criteria = analysis.getJoinCriteria(node); + + TupleDescriptor left = analysis.getOutputDescriptor(node.getLeft()); + TupleDescriptor right = analysis.getOutputDescriptor(node.getRight()); + List leftExpressions = new ArrayList<>(); + List rightExpressions = new ArrayList<>(); + List comparisonTypes = new ArrayList<>(); + for (Expression conjunct : ExpressionUtils.extractConjuncts(criteria)) { + if (!(conjunct instanceof ComparisonExpression)) { + throw new SemanticException(NOT_SUPPORTED, node, "Unsupported non-equi join form: %s", conjunct); + } + + ComparisonExpression comparison = (ComparisonExpression) conjunct; + ComparisonExpression.Type comparisonType = comparison.getType(); + if (comparison.getType() != EQUAL && node.getType() != INNER) { + throw new SemanticException(NOT_SUPPORTED, node, "Non-equi joins only supported for inner join: %s", conjunct); + } + Set firstDependencies = TupleAnalyzer.DependencyExtractor.extract(comparison.getLeft()); + Set secondDependencies = TupleAnalyzer.DependencyExtractor.extract(comparison.getRight()); + + Expression leftExpression; + Expression rightExpression; + if (Iterables.all(firstDependencies, left.canResolvePredicate()) && Iterables.all(secondDependencies, right.canResolvePredicate())) { + leftExpression = comparison.getLeft(); + rightExpression = comparison.getRight(); + } + else if (Iterables.all(firstDependencies, right.canResolvePredicate()) && Iterables.all(secondDependencies, left.canResolvePredicate())) { + leftExpression = comparison.getRight(); + rightExpression = comparison.getLeft(); + comparisonType = flipComparison(comparisonType); + } + else { + // must have a complex expression that involves both tuples on one side of the comparison expression (e.g., coalesce(left.x, right.x) = 1) + throw new SemanticException(NOT_SUPPORTED, node, "Unsupported non-equi join form: %s", conjunct); + } + leftExpressions.add(leftExpression); + rightExpressions.add(rightExpression); + comparisonTypes.add(comparisonType); + } + + Analysis.JoinInPredicates joinInPredicates = analysis.getJoinInPredicates(node); + + // Add semi joins if necessary + if (joinInPredicates != null) { + leftPlanBuilder = appendSemiJoins(leftPlanBuilder, joinInPredicates.getLeftInPredicates()); + rightPlanBuilder = appendSemiJoins(rightPlanBuilder, joinInPredicates.getRightInPredicates()); + } + + // Add projections for join criteria + leftPlanBuilder = appendProjections(leftPlanBuilder, leftExpressions); + rightPlanBuilder = appendProjections(rightPlanBuilder, rightExpressions); + + List postInnerJoinComparisons = new ArrayList<>(); + for (int i = 0; i < comparisonTypes.size(); i++) { + Symbol leftSymbol = leftPlanBuilder.translate(leftExpressions.get(i)); + Symbol rightSymbol = rightPlanBuilder.translate(rightExpressions.get(i)); + + equiClauses.add(new JoinNode.EquiJoinClause(leftSymbol, rightSymbol)); + + Expression leftExpression = leftPlanBuilder.rewrite(leftExpressions.get(i)); + Expression rightExpression = rightPlanBuilder.rewrite(rightExpressions.get(i)); + postInnerJoinComparisons.add(new ComparisonExpression(comparisonTypes.get(i), leftExpression, rightExpression)); + } + postInnerJoinCriteria = ExpressionUtils.and(postInnerJoinComparisons); + } + + PlanNode root; + if (node.getType() == INNER) { + root = new JoinNode(idAllocator.getNextId(), + JoinNode.Type.CROSS, + leftPlanBuilder.getRoot(), + rightPlanBuilder.getRoot(), + ImmutableList.of(), + Optional.empty(), + Optional.empty()); + root = new FilterNode(idAllocator.getNextId(), root, postInnerJoinCriteria); + } + else { + root = new JoinNode(idAllocator.getNextId(), + JoinNode.Type.typeConvert(node.getType()), + leftPlanBuilder.getRoot(), + rightPlanBuilder.getRoot(), + equiClauses.build(), + Optional.empty(), + Optional.empty()); + } + Optional sampleWeight = Optional.empty(); + if (leftPlanBuilder.getSampleWeight().isPresent() || rightPlanBuilder.getSampleWeight().isPresent()) { + Expression expression = new ArithmeticBinaryExpression(ArithmeticBinaryExpression.Type.MULTIPLY, + oneIfNull(leftPlanBuilder.getSampleWeight()), + oneIfNull(rightPlanBuilder.getSampleWeight())); + sampleWeight = Optional.of(symbolAllocator.newSymbol(expression, BIGINT)); + ImmutableMap.Builder projections = ImmutableMap.builder(); + projections.put(sampleWeight.get(), expression); + for (Symbol symbol : root.getOutputSymbols()) { + projections.put(symbol, new QualifiedNameReference(symbol.toQualifiedName())); + } + root = new ProjectNode(idAllocator.getNextId(), root, projections.build()); + } + + return new RelationPlan(root, outputDescriptor, outputSymbols, sampleWeight); + } + + private static ComparisonExpression.Type flipComparison(ComparisonExpression.Type type) + { + switch (type) { + case EQUAL: + return EQUAL; + case NOT_EQUAL: + return NOT_EQUAL; + case LESS_THAN: + return GREATER_THAN; + case LESS_THAN_OR_EQUAL: + return GREATER_THAN_OR_EQUAL; + case GREATER_THAN: + return LESS_THAN; + case GREATER_THAN_OR_EQUAL: + return LESS_THAN_OR_EQUAL; + case IS_DISTINCT_FROM: + return IS_DISTINCT_FROM; + default: + throw new IllegalArgumentException("Unsupported comparison: " + type); + } + } + + private RelationPlan planCrossJoinUnnest(RelationPlan leftPlan, Join joinNode, Unnest node) + { + TupleDescriptor outputDescriptor = analysis.getOutputDescriptor(joinNode); + TupleDescriptor unnestOutputDescriptor = analysis.getOutputDescriptor(node); + // Create symbols for the result of unnesting + ImmutableList.Builder unnestedSymbolsBuilder = ImmutableList.builder(); + for (Field field : unnestOutputDescriptor.getVisibleFields()) { + Symbol symbol = symbolAllocator.newSymbol(field); + unnestedSymbolsBuilder.add(symbol); + } + ImmutableList unnestedSymbols = unnestedSymbolsBuilder.build(); + + // Add a projection for all the unnest arguments + PlanBuilder planBuilder = initializePlanBuilder(leftPlan); + planBuilder = appendProjections(planBuilder, node.getExpressions()); + TranslationMap translations = planBuilder.getTranslations(); + ProjectNode projectNode = checkType(planBuilder.getRoot(), ProjectNode.class, "planBuilder.getRoot()"); + + ImmutableMap.Builder> unnestSymbols = ImmutableMap.builder(); + UnmodifiableIterator unnestedSymbolsIterator = unnestedSymbols.iterator(); + for (Expression expression : node.getExpressions()) { + Type type = analysis.getType(expression); + Symbol inputSymbol = translations.get(expression); + if (type instanceof ArrayType) { + unnestSymbols.put(inputSymbol, ImmutableList.of(unnestedSymbolsIterator.next())); + } + else if (type instanceof MapType) { + unnestSymbols.put(inputSymbol, ImmutableList.of(unnestedSymbolsIterator.next(), unnestedSymbolsIterator.next())); + } + else { + throw new IllegalArgumentException("Unsupported type for UNNEST: " + type); + } + } + Optional ordinalitySymbol = node.isWithOrdinality() ? Optional.of(unnestedSymbolsIterator.next()) : Optional.empty(); + checkState(!unnestedSymbolsIterator.hasNext(), "Not all output symbols were matched with input symbols"); + + UnnestNode unnestNode = new UnnestNode(idAllocator.getNextId(), projectNode, leftPlan.getOutputSymbols(), unnestSymbols.build(), ordinalitySymbol); + return new RelationPlan(unnestNode, outputDescriptor, unnestNode.getOutputSymbols(), Optional.empty()); + } + + private static Expression oneIfNull(Optional symbol) + { + if (symbol.isPresent()) { + return new CoalesceExpression(new QualifiedNameReference(symbol.get().toQualifiedName()), new LongLiteral("1")); + } + else { + return new LongLiteral("1"); + } + } + + @Override + protected RelationPlan visitTableSubquery(TableSubquery node, Void context) + { + return process(node.getQuery(), context); + } + + @Override + protected RelationPlan visitQuery(Query node, Void context) + { + PlanBuilder subPlan = new QueryPlanner(analysis, symbolAllocator, idAllocator, metadata, session).process(node, null); + + ImmutableList.Builder outputSymbols = ImmutableList.builder(); + for (FieldOrExpression fieldOrExpression : analysis.getOutputExpressions(node)) { + outputSymbols.add(subPlan.translate(fieldOrExpression)); + } + + return new RelationPlan(subPlan.getRoot(), analysis.getOutputDescriptor(node), outputSymbols.build(), subPlan.getSampleWeight()); + } + + @Override + protected RelationPlan visitQuerySpecification(QuerySpecification node, Void context) + { + PlanBuilder subPlan = new QueryPlanner(analysis, symbolAllocator, idAllocator, metadata, session).process(node, null); + + ImmutableList.Builder outputSymbols = ImmutableList.builder(); + for (FieldOrExpression fieldOrExpression : analysis.getOutputExpressions(node)) { + outputSymbols.add(subPlan.translate(fieldOrExpression)); + } + + return new RelationPlan(subPlan.getRoot(), analysis.getOutputDescriptor(node), outputSymbols.build(), subPlan.getSampleWeight()); + } + + @Override + protected RelationPlan visitValues(Values node, Void context) + { + TupleDescriptor descriptor = analysis.getOutputDescriptor(node); + ImmutableList.Builder outputSymbolsBuilder = ImmutableList.builder(); + for (Field field : descriptor.getVisibleFields()) { + Symbol symbol = symbolAllocator.newSymbol(field); + outputSymbolsBuilder.add(symbol); + } + + ImmutableList.Builder> rows = ImmutableList.builder(); + for (Expression row : node.getRows()) { + ImmutableList.Builder values = ImmutableList.builder(); + if (row instanceof Row) { + List items = ((Row) row).getItems(); + for (int i = 0; i < items.size(); i++) { + Expression expression = items.get(i); + Object constantValue = evaluateConstantExpression(expression); + values.add(LiteralInterpreter.toExpression(constantValue, descriptor.getFieldByIndex(i).getType())); + } + } + else { + Object constantValue = evaluateConstantExpression(row); + values.add(LiteralInterpreter.toExpression(constantValue, descriptor.getFieldByIndex(0).getType())); + } + + rows.add(values.build()); + } + + ValuesNode valuesNode = new ValuesNode(idAllocator.getNextId(), outputSymbolsBuilder.build(), rows.build()); + return new RelationPlan(valuesNode, descriptor, outputSymbolsBuilder.build(), Optional.empty()); + } + + @Override + protected RelationPlan visitUnnest(Unnest node, Void context) + { + TupleDescriptor descriptor = analysis.getOutputDescriptor(node); + ImmutableList.Builder outputSymbolsBuilder = ImmutableList.builder(); + for (Field field : descriptor.getVisibleFields()) { + Symbol symbol = symbolAllocator.newSymbol(field); + outputSymbolsBuilder.add(symbol); + } + List unnestedSymbols = outputSymbolsBuilder.build(); + + // If we got here, then we must be unnesting a constant, and not be in a join (where there could be column references) + ImmutableList.Builder argumentSymbols = ImmutableList.builder(); + ImmutableList.Builder values = ImmutableList.builder(); + ImmutableMap.Builder> unnestSymbols = ImmutableMap.builder(); + Iterator unnestedSymbolsIterator = unnestedSymbols.iterator(); + for (Expression expression : node.getExpressions()) { + Object constantValue = evaluateConstantExpression(expression); + Type type = analysis.getType(expression); + values.add(LiteralInterpreter.toExpression(constantValue, type)); + Symbol inputSymbol = symbolAllocator.newSymbol(expression, type); + argumentSymbols.add(inputSymbol); + if (type instanceof ArrayType) { + unnestSymbols.put(inputSymbol, ImmutableList.of(unnestedSymbolsIterator.next())); + } + else if (type instanceof MapType) { + unnestSymbols.put(inputSymbol, ImmutableList.of(unnestedSymbolsIterator.next(), unnestedSymbolsIterator.next())); + } + else { + throw new IllegalArgumentException("Unsupported type for UNNEST: " + type); + } + } + Optional ordinalitySymbol = node.isWithOrdinality() ? Optional.of(unnestedSymbolsIterator.next()) : Optional.empty(); + checkState(!unnestedSymbolsIterator.hasNext(), "Not all output symbols were matched with input symbols"); + ValuesNode valuesNode = new ValuesNode(idAllocator.getNextId(), argumentSymbols.build(), ImmutableList.>of(values.build())); + + UnnestNode unnestNode = new UnnestNode(idAllocator.getNextId(), valuesNode, ImmutableList.of(), unnestSymbols.build(), ordinalitySymbol); + return new RelationPlan(unnestNode, descriptor, unnestedSymbols, Optional.empty()); + } + + private Object evaluateConstantExpression(Expression expression) + { + // verify expression is constant + expression.accept(new DefaultTraversalVisitor() + { + @Override + protected Void visitQualifiedNameReference(QualifiedNameReference node, Void context) + { + throw new SemanticException(EXPRESSION_NOT_CONSTANT, expression, "Constant expression cannot contain column references"); + } + + @Override + protected Void visitInputReference(InputReference node, Void context) + { + throw new SemanticException(EXPRESSION_NOT_CONSTANT, expression, "Constant expression cannot contain input references"); + } + }, null); + + // add coercions + Expression rewrite = ExpressionTreeRewriter.rewriteWith(new ExpressionRewriter() + { + @Override + public Expression rewriteExpression(Expression node, Void context, ExpressionTreeRewriter treeRewriter) + { + Expression rewrittenExpression = treeRewriter.defaultRewrite(node, context); + + // cast expression if coercion is registered + Type coercion = analysis.getCoercion(node); + if (coercion != null) { + rewrittenExpression = new Cast(rewrittenExpression, coercion.getTypeSignature().toString()); + } + + return rewrittenExpression; + } + }, expression); + + try { + // expressionInterpreter/optimizer only understands a subset of expression types + // TODO: remove this when the new expression tree is implemented + Expression canonicalized = CanonicalizeExpressions.canonicalizeExpression(rewrite); + + // The optimization above may have rewritten the expression tree which breaks all the identity maps, so redo the analysis + // to re-analyze coercions that might be necessary + ExpressionAnalyzer analyzer = ExpressionAnalyzer.createWithoutSubqueries( + metadata.getFunctionRegistry(), + metadata.getTypeManager(), + EXPRESSION_NOT_CONSTANT, + "Constant expression cannot contain as sub-query"); + analyzer.analyze(canonicalized, new TupleDescriptor(), new AnalysisContext()); + + // evaluate the expression + Object result = ExpressionInterpreter.expressionInterpreter(canonicalized, metadata, session, analyzer.getExpressionTypes()).evaluate(0); + checkState(!(result instanceof Expression), "Expression interpreter returned an unresolved expression"); + + return result; + } + catch (Exception e) { + throw new SemanticException(EXPRESSION_NOT_CONSTANT, expression, "Error evaluating constant expression: %s", e.getMessage()); + } + } + + @Override + protected RelationPlan visitUnion(Union node, Void context) + { + checkArgument(!node.getRelations().isEmpty(), "No relations specified for UNION"); + + List unionOutputSymbols = null; + ImmutableList.Builder sources = ImmutableList.builder(); + ImmutableListMultimap.Builder symbolMapping = ImmutableListMultimap.builder(); + List subPlans = node.getRelations().stream() + .map(relation -> process(relation, context)) + .collect(toImmutableList()); + + boolean hasSampleWeight = false; + for (RelationPlan subPlan : subPlans) { + if (subPlan.getSampleWeight().isPresent()) { + hasSampleWeight = true; + break; + } + } + + Optional outputSampleWeight = Optional.empty(); + for (RelationPlan relationPlan : subPlans) { + if (hasSampleWeight && !relationPlan.getSampleWeight().isPresent()) { + relationPlan = addConstantSampleWeight(relationPlan); + } + + List childOutputSymbols = relationPlan.getOutputSymbols(); + if (unionOutputSymbols == null) { + // Use the first Relation to derive output symbol names + TupleDescriptor descriptor = relationPlan.getDescriptor(); + ImmutableList.Builder outputSymbolBuilder = ImmutableList.builder(); + for (Field field : descriptor.getVisibleFields()) { + int fieldIndex = descriptor.indexOf(field); + Symbol symbol = childOutputSymbols.get(fieldIndex); + outputSymbolBuilder.add(symbolAllocator.newSymbol(symbol.getName(), symbolAllocator.getTypes().get(symbol))); + } + unionOutputSymbols = outputSymbolBuilder.build(); + outputSampleWeight = relationPlan.getSampleWeight(); + } + + TupleDescriptor descriptor = relationPlan.getDescriptor(); + checkArgument(descriptor.getVisibleFieldCount() == unionOutputSymbols.size(), + "Expected relation to have %s symbols but has %s symbols", + descriptor.getVisibleFieldCount(), + unionOutputSymbols.size()); + + int unionFieldId = 0; + for (Field field : descriptor.getVisibleFields()) { + int fieldIndex = descriptor.indexOf(field); + symbolMapping.put(unionOutputSymbols.get(unionFieldId), childOutputSymbols.get(fieldIndex)); + unionFieldId++; + } + + sources.add(relationPlan.getRoot()); + } + + PlanNode planNode = new UnionNode(idAllocator.getNextId(), sources.build(), symbolMapping.build()); + if (node.isDistinct()) { + planNode = distinct(planNode); + } + return new RelationPlan(planNode, analysis.getOutputDescriptor(node), planNode.getOutputSymbols(), outputSampleWeight); + } + + private RelationPlan addConstantSampleWeight(RelationPlan subPlan) + { + ImmutableMap.Builder projections = ImmutableMap.builder(); + for (Symbol symbol : subPlan.getOutputSymbols()) { + Expression expression = new QualifiedNameReference(symbol.toQualifiedName()); + projections.put(symbol, expression); + } + Expression one = new LongLiteral("1"); + + Symbol sampleWeightSymbol = symbolAllocator.newSymbol("$sampleWeight", BIGINT); + projections.put(sampleWeightSymbol, one); + ProjectNode projectNode = new ProjectNode(idAllocator.getNextId(), subPlan.getRoot(), projections.build()); + return new RelationPlan(projectNode, subPlan.getDescriptor(), projectNode.getOutputSymbols(), Optional.of(sampleWeightSymbol)); + } + + private PlanBuilder initializePlanBuilder(RelationPlan relationPlan) + { + TranslationMap translations = new TranslationMap(relationPlan, analysis); + + // Make field->symbol mapping from underlying relation plan available for translations + // This makes it possible to rewrite FieldOrExpressions that reference fields from the underlying tuple directly + translations.setFieldMappings(relationPlan.getOutputSymbols()); + + return new PlanBuilder(translations, relationPlan.getRoot(), relationPlan.getSampleWeight()); + } + + private PlanBuilder appendProjections(PlanBuilder subPlan, Iterable expressions) + { + TranslationMap translations = new TranslationMap(subPlan.getRelationPlan(), analysis); + + // Carry over the translations from the source because we are appending projections + translations.copyMappingsFrom(subPlan.getTranslations()); + + ImmutableMap.Builder projections = ImmutableMap.builder(); + + // add an identity projection for underlying plan + for (Symbol symbol : subPlan.getRoot().getOutputSymbols()) { + Expression expression = new QualifiedNameReference(symbol.toQualifiedName()); + projections.put(symbol, expression); + } + + ImmutableMap.Builder newTranslations = ImmutableMap.builder(); + for (Expression expression : expressions) { + Symbol symbol = symbolAllocator.newSymbol(expression, analysis.getType(expression)); + + // TODO: CHECK IF THE REWRITE OF A SEMI JOINED EXPRESSION WILL WORK!!!!!!! + + projections.put(symbol, translations.rewrite(expression)); + newTranslations.put(symbol, expression); + } + // Now append the new translations into the TranslationMap + for (Map.Entry entry : newTranslations.build().entrySet()) { + translations.put(entry.getValue(), entry.getKey()); + } + + return new PlanBuilder(translations, new ProjectNode(idAllocator.getNextId(), subPlan.getRoot(), projections.build()), subPlan.getSampleWeight()); + } + + private PlanBuilder appendSemiJoins(PlanBuilder subPlan, Set inPredicates) + { + for (InPredicate inPredicate : inPredicates) { + subPlan = appendSemiJoin(subPlan, inPredicate); + } + return subPlan; + } + + private PlanBuilder appendSemiJoin(PlanBuilder subPlan, InPredicate inPredicate) + { + TranslationMap translations = new TranslationMap(subPlan.getRelationPlan(), analysis); + translations.copyMappingsFrom(subPlan.getTranslations()); + + subPlan = appendProjections(subPlan, ImmutableList.of(inPredicate.getValue())); + Symbol sourceJoinSymbol = subPlan.translate(inPredicate.getValue()); + + checkState(inPredicate.getValueList() instanceof SubqueryExpression); + SubqueryExpression subqueryExpression = (SubqueryExpression) inPredicate.getValueList(); + RelationPlanner relationPlanner = new RelationPlanner(analysis, symbolAllocator, idAllocator, metadata, session); + RelationPlan valueListRelation = relationPlanner.process(subqueryExpression.getQuery(), null); + Symbol filteringSourceJoinSymbol = Iterables.getOnlyElement(valueListRelation.getRoot().getOutputSymbols()); + + Symbol semiJoinOutputSymbol = symbolAllocator.newSymbol("semijoinresult", BOOLEAN); + + translations.put(inPredicate, semiJoinOutputSymbol); + + return new PlanBuilder(translations, + new SemiJoinNode(idAllocator.getNextId(), + subPlan.getRoot(), + valueListRelation.getRoot(), + sourceJoinSymbol, + filteringSourceJoinSymbol, + semiJoinOutputSymbol, + Optional.empty(), + Optional.empty()), + subPlan.getSampleWeight()); + } + + private PlanNode distinct(PlanNode node) + { + return new AggregationNode(idAllocator.getNextId(), + node, + node.getOutputSymbols(), + ImmutableMap.of(), + ImmutableMap.of(), + ImmutableMap.of(), + AggregationNode.Step.SINGLE, + Optional.empty(), + 1.0, + Optional.empty()); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/StageExecutionPlan.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/StageExecutionPlan.java new file mode 100644 index 00000000..df3dcabc --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/StageExecutionPlan.java @@ -0,0 +1,75 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner; + +import com.facebook.presto.split.SplitSource; +import com.facebook.presto.sql.planner.plan.OutputNode; +import com.google.common.collect.ImmutableList; + +import java.util.List; +import java.util.Optional; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +public class StageExecutionPlan +{ + private final PlanFragment fragment; + private final Optional dataSource; + private final List subStages; + private final Optional> fieldNames; + + public StageExecutionPlan(PlanFragment fragment, Optional dataSource, List subStages) + { + this.fragment = checkNotNull(fragment, "fragment is null"); + this.dataSource = checkNotNull(dataSource, "dataSource is null"); + this.subStages = ImmutableList.copyOf(checkNotNull(subStages, "dependencies is null")); + + fieldNames = (fragment.getRoot() instanceof OutputNode) ? + Optional.>of(ImmutableList.copyOf(((OutputNode) fragment.getRoot()).getColumnNames())) : + Optional.empty(); + } + + public List getFieldNames() + { + checkState(fieldNames.isPresent(), "cannot get field names from non-output stage"); + return fieldNames.get(); + } + + public PlanFragment getFragment() + { + return fragment; + } + + public Optional getDataSource() + { + return dataSource; + } + + public List getSubStages() + { + return subStages; + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("fragment", fragment) + .add("dataSource", dataSource) + .add("subStages", subStages) + .toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/SubExpressionExtractor.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/SubExpressionExtractor.java new file mode 100644 index 00000000..95057ceb --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/SubExpressionExtractor.java @@ -0,0 +1,72 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner; + +import com.facebook.presto.sql.tree.DefaultTraversalVisitor; +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.Node; +import com.google.common.collect.ImmutableCollection; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; + +import javax.annotation.Nullable; + +import java.util.List; +import java.util.Set; + +/** + * Extracts and returns the set of all expression subtrees within an Expression, including Expression itself + */ +public final class SubExpressionExtractor +{ + private SubExpressionExtractor() {} + + public static List extractAll(Iterable expressions) + { + ImmutableList.Builder builder = ImmutableList.builder(); + for (Expression expression : expressions) { + extract(builder, expression); + } + return builder.build(); + } + + public static List extractAll(Expression expression) + { + ImmutableList.Builder builder = ImmutableList.builder(); + extract(builder, expression); + return builder.build(); + } + + public static Set extract(Expression expression) + { + final ImmutableSet.Builder builder = ImmutableSet.builder(); + extract(builder, expression); + return builder.build(); + } + + private static void extract(final ImmutableCollection.Builder builder, Expression expression) + { + new DefaultTraversalVisitor() + { + @Override + public Void process(Node node, @Nullable Void context) + { + Expression expression = (Expression) node; + builder.add(expression); + + return super.process(node, context); + } + }.process(expression, null); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/SubPlan.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/SubPlan.java new file mode 100644 index 00000000..819f0272 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/SubPlan.java @@ -0,0 +1,86 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner; + +import com.facebook.presto.sql.planner.plan.PlanFragmentId; +import com.facebook.presto.sql.planner.plan.RemoteSourceNode; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Multiset; + +import javax.annotation.concurrent.Immutable; + +import java.util.List; + +import static com.facebook.presto.util.ImmutableCollectors.toImmutableMultiset; + +@Immutable +public class SubPlan +{ + private final PlanFragment fragment; + private final List children; + + public SubPlan(PlanFragment fragment, List children) + { + Preconditions.checkNotNull(fragment, "fragment is null"); + Preconditions.checkNotNull(children, "children is null"); + + this.fragment = fragment; + this.children = ImmutableList.copyOf(children); + } + + public PlanFragment getFragment() + { + return fragment; + } + + public List getChildren() + { + return children; + } + + /** + * Flattens the subplan and returns all PlanFragments in the tree + */ + public List getAllFragments() + { + ImmutableList.Builder fragments = ImmutableList.builder(); + + fragments.add(getFragment()); + for (SubPlan child : getChildren()) { + fragments.addAll(child.getAllFragments()); + } + + return fragments.build(); + } + + public void sanityCheck() + { + Multiset exchangeIds = fragment.getRemoteSourceNodes().stream() + .map(RemoteSourceNode::getSourceFragmentIds) + .flatMap(List::stream) + .collect(toImmutableMultiset()); + + Multiset childrenIds = children.stream() + .map(SubPlan::getFragment) + .map(PlanFragment::getId) + .collect(toImmutableMultiset()); + + Preconditions.checkState(exchangeIds.equals(childrenIds), "Subplan exchange ids don't match child fragment ids (%s vs %s)", exchangeIds, childrenIds); + + for (SubPlan child : children) { + child.sanityCheck(); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/Symbol.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/Symbol.java new file mode 100644 index 00000000..ecdb69bb --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/Symbol.java @@ -0,0 +1,92 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner; + +import com.facebook.presto.sql.tree.QualifiedName; +import com.facebook.presto.sql.tree.QualifiedNameReference; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import com.google.common.base.Preconditions; + +public class Symbol + implements Comparable +{ + private final String name; + + @JsonCreator + public Symbol(String name) + { + Preconditions.checkNotNull(name, "name is null"); + this.name = name; + } + + @JsonValue + public String getName() + { + return name; + } + + public QualifiedName toQualifiedName() + { + return QualifiedName.of(name); + } + + public QualifiedNameReference toQualifiedNameReference() + { + return new QualifiedNameReference(toQualifiedName()); + } + + public static Symbol fromQualifiedName(QualifiedName name) + { + Preconditions.checkArgument(!name.getPrefix().isPresent(), "Can't create a symbol from a qualified name with prefix"); + return new Symbol(name.getSuffix()); + } + + @Override + public String toString() + { + return name; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + Symbol symbol = (Symbol) o; + + if (!name.equals(symbol.name)) { + return false; + } + + return true; + } + + @Override + public int hashCode() + { + return name.hashCode(); + } + + @Override + public int compareTo(Symbol o) + { + return name.compareTo(o.name); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/SymbolAllocator.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/SymbolAllocator.java new file mode 100644 index 00000000..216f7a3e --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/SymbolAllocator.java @@ -0,0 +1,112 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner; + +import com.facebook.presto.spi.type.BigintType; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.analyzer.Field; +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.FunctionCall; +import com.facebook.presto.sql.tree.QualifiedNameReference; +import com.google.common.base.Preconditions; +import com.google.common.primitives.Ints; + +import java.util.HashMap; +import java.util.Map; + +import static java.util.Locale.ENGLISH; + +public class SymbolAllocator +{ + private final Map symbols = new HashMap<>(); + private int nextId; + + public Symbol newSymbol(String nameHint, Type type) + { + return newSymbol(nameHint, type, null); + } + + public Symbol newHashSymbol() + { + return newSymbol("$hashValue", BigintType.BIGINT); + } + + public Symbol newSymbol(String nameHint, Type type, String suffix) + { + Preconditions.checkNotNull(nameHint, "name is null"); + + // TODO: workaround for the fact that QualifiedName lowercases parts + nameHint = nameHint.toLowerCase(ENGLISH); + + // don't strip the tail if the only _ is the first character + int index = nameHint.lastIndexOf("_"); + if (index > 0) { + String tail = nameHint.substring(index + 1); + + // only strip if tail is numeric or _ is the last character + if (Ints.tryParse(tail) != null || index == nameHint.length() - 1) { + nameHint = nameHint.substring(0, index); + } + } + + String unique = nameHint; + + if (suffix != null) { + unique = unique + "$" + suffix; + } + + String attempt = unique; + while (symbols.containsKey(new Symbol(attempt))) { + attempt = unique + "_" + nextId(); + } + + Symbol symbol = new Symbol(attempt); + symbols.put(symbol, type); + return symbol; + } + + public Symbol newSymbol(Expression expression, Type type) + { + return newSymbol(expression, type, null); + } + + public Symbol newSymbol(Expression expression, Type type, String suffix) + { + String nameHint = "expr"; + if (expression instanceof QualifiedNameReference) { + nameHint = ((QualifiedNameReference) expression).getName().getSuffix(); + } + else if (expression instanceof FunctionCall) { + nameHint = ((FunctionCall) expression).getName().getSuffix(); + } + + return newSymbol(nameHint, type, suffix); + } + + public Symbol newSymbol(Field field) + { + String nameHint = field.getName().orElse("field"); + return newSymbol(nameHint, field.getType()); + } + + public Map getTypes() + { + return symbols; + } + + private int nextId() + { + return nextId++; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/SymbolExtractor.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/SymbolExtractor.java new file mode 100644 index 00000000..971bebb5 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/SymbolExtractor.java @@ -0,0 +1,330 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner; + +import com.facebook.presto.sql.planner.plan.AggregationNode; +import com.facebook.presto.sql.planner.plan.DeleteNode; +import com.facebook.presto.sql.planner.plan.DistinctLimitNode; +import com.facebook.presto.sql.planner.plan.ExchangeNode; +import com.facebook.presto.sql.planner.plan.FilterNode; +import com.facebook.presto.sql.planner.plan.IndexJoinNode; +import com.facebook.presto.sql.planner.plan.IndexSourceNode; +import com.facebook.presto.sql.planner.plan.JoinNode; +import com.facebook.presto.sql.planner.plan.LimitNode; +import com.facebook.presto.sql.planner.plan.MarkDistinctNode; +import com.facebook.presto.sql.planner.plan.OutputNode; +import com.facebook.presto.sql.planner.plan.PlanNode; +import com.facebook.presto.sql.planner.plan.PlanVisitor; +import com.facebook.presto.sql.planner.plan.ProjectNode; +import com.facebook.presto.sql.planner.plan.RemoteSourceNode; +import com.facebook.presto.sql.planner.plan.RowNumberNode; +import com.facebook.presto.sql.planner.plan.SampleNode; +import com.facebook.presto.sql.planner.plan.SemiJoinNode; +import com.facebook.presto.sql.planner.plan.SortNode; +import com.facebook.presto.sql.planner.plan.TableCommitNode; +import com.facebook.presto.sql.planner.plan.TableScanNode; +import com.facebook.presto.sql.planner.plan.TableWriterNode; +import com.facebook.presto.sql.planner.plan.TopNNode; +import com.facebook.presto.sql.planner.plan.TopNRowNumberNode; +import com.facebook.presto.sql.planner.plan.UnionNode; +import com.facebook.presto.sql.planner.plan.UnnestNode; +import com.facebook.presto.sql.planner.plan.ValuesNode; +import com.facebook.presto.sql.planner.plan.WindowNode; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; + +import java.util.Optional; +import java.util.Set; + +/** + * Computes all symbols declared by a logical plan + */ +public final class SymbolExtractor +{ + private SymbolExtractor() {} + + public static Set extract(PlanNode node) + { + ImmutableSet.Builder builder = ImmutableSet.builder(); + + node.accept(new Visitor(builder), null); + + return builder.build(); + } + + private static class Visitor + extends PlanVisitor + { + private final ImmutableSet.Builder builder; + + public Visitor(ImmutableSet.Builder builder) + { + this.builder = builder; + } + + @Override + public Void visitRemoteSource(RemoteSourceNode node, Void context) + { + builder.addAll(node.getOutputSymbols()); + + return null; + } + + @Override + public Void visitAggregation(AggregationNode node, Void context) + { + // visit child + node.getSource().accept(this, context); + + builder.addAll(node.getAggregations().keySet()); + + return null; + } + + @Override + public Void visitMarkDistinct(MarkDistinctNode node, Void context) + { + node.getSource().accept(this, context); + + builder.add(node.getMarkerSymbol()); + + return null; + } + + @Override + public Void visitWindow(WindowNode node, Void context) + { + // visit child + node.getSource().accept(this, context); + + builder.addAll(node.getWindowFunctions().keySet()); + + return null; + } + + @Override + public Void visitTopNRowNumber(TopNRowNumberNode node, Void context) + { + // visit child + node.getSource().accept(this, context); + + builder.add(node.getRowNumberSymbol()); + + return null; + } + + @Override + public Void visitRowNumber(RowNumberNode node, Void context) + { + // visit child + node.getSource().accept(this, context); + + builder.add(node.getRowNumberSymbol()); + + return null; + } + + @Override + public Void visitFilter(FilterNode node, Void context) + { + // visit child + node.getSource().accept(this, context); + + return null; + } + + @Override + public Void visitProject(ProjectNode node, Void context) + { + // visit child + node.getSource().accept(this, context); + + builder.addAll(node.getOutputSymbols()); + + return null; + } + + @Override + public Void visitUnnest(UnnestNode node, Void context) + { + node.getSource().accept(this, context); + + builder.addAll(Iterables.concat(node.getUnnestSymbols().values())); + node.getOrdinalitySymbol().ifPresent(builder::add); + + return null; + } + + @Override + public Void visitTopN(TopNNode node, Void context) + { + node.getSource().accept(this, context); + + return null; + } + + @Override + public Void visitSort(SortNode node, Void context) + { + node.getSource().accept(this, context); + + return null; + } + + @Override + public Void visitOutput(OutputNode node, Void context) + { + node.getSource().accept(this, context); + builder.addAll(node.getOutputSymbols()); + return null; + } + + @Override + public Void visitLimit(LimitNode node, Void context) + { + node.getSource().accept(this, context); + + return null; + } + + @Override + public Void visitDistinctLimit(DistinctLimitNode node, Void context) + { + node.getSource().accept(this, context); + + return null; + } + + @Override + public Void visitSample(SampleNode node, Void context) + { + node.getSource().accept(this, context); + + Optional sampleWeightSymbol = node.getSampleWeightSymbol(); + if (sampleWeightSymbol.isPresent()) { + builder.add(sampleWeightSymbol.get()); + } + + return null; + } + + @Override + public Void visitTableScan(TableScanNode node, Void context) + { + builder.addAll(node.getAssignments().keySet()); + + return null; + } + + @Override + public Void visitValues(ValuesNode node, Void context) + { + builder.addAll(node.getOutputSymbols()); + + return null; + } + + @Override + public Void visitTableWriter(TableWriterNode node, Void context) + { + node.getSource().accept(this, context); + + builder.addAll(node.getOutputSymbols()); + + return null; + } + + @Override + public Void visitTableCommit(TableCommitNode node, Void context) + { + node.getSource().accept(this, context); + + builder.addAll(node.getOutputSymbols()); + + return null; + } + + @Override + public Void visitDelete(DeleteNode node, Void context) + { + node.getSource().accept(this, context); + + builder.addAll(node.getOutputSymbols()); + + return null; + } + + @Override + public Void visitIndexSource(IndexSourceNode node, Void context) + { + builder.addAll(node.getAssignments().keySet()); + + return null; + } + + @Override + public Void visitJoin(JoinNode node, Void context) + { + node.getLeft().accept(this, context); + node.getRight().accept(this, context); + + return null; + } + + @Override + public Void visitSemiJoin(SemiJoinNode node, Void context) + { + builder.add(node.getSemiJoinOutput()); + + node.getSource().accept(this, context); + node.getFilteringSource().accept(this, context); + + return null; + } + + @Override + public Void visitIndexJoin(IndexJoinNode node, Void context) + { + node.getProbeSource().accept(this, context); + node.getIndexSource().accept(this, context); + + return null; + } + + @Override + public Void visitUnion(UnionNode node, Void context) + { + for (PlanNode subPlanNode : node.getSources()) { + subPlanNode.accept(this, context); + } + + builder.addAll(node.getOutputSymbols()); + return null; + } + + @Override + public Void visitExchange(ExchangeNode node, Void context) + { + builder.addAll(node.getOutputSymbols()); + + return null; + } + + @Override + protected Void visitPlan(PlanNode node, Void context) + { + throw new UnsupportedOperationException("not yet implemented: " + node.getClass().getName()); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/SymbolResolver.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/SymbolResolver.java new file mode 100644 index 00000000..9b6b8b51 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/SymbolResolver.java @@ -0,0 +1,19 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner; + +public interface SymbolResolver +{ + Object getValue(Symbol symbol); +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/SymbolToInputRewriter.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/SymbolToInputRewriter.java new file mode 100644 index 00000000..2089d643 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/SymbolToInputRewriter.java @@ -0,0 +1,47 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner; + +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.ExpressionRewriter; +import com.facebook.presto.sql.tree.ExpressionTreeRewriter; +import com.facebook.presto.sql.tree.InputReference; +import com.facebook.presto.sql.tree.QualifiedNameReference; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; + +import java.util.Map; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class SymbolToInputRewriter + extends ExpressionRewriter +{ + private final Map symbolToChannelMapping; + + public SymbolToInputRewriter(Map symbolToChannelMapping) + { + checkNotNull(symbolToChannelMapping, "symbolToChannelMapping is null"); + this.symbolToChannelMapping = ImmutableMap.copyOf(symbolToChannelMapping); + } + + @Override + public Expression rewriteQualifiedNameReference(QualifiedNameReference node, Void context, ExpressionTreeRewriter treeRewriter) + { + Integer channel = symbolToChannelMapping.get(Symbol.fromQualifiedName(node.getName())); + Preconditions.checkArgument(channel != null, "Cannot resolve symbol %s", node.getName()); + + return new InputReference(channel); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/TranslationMap.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/TranslationMap.java new file mode 100644 index 00000000..57db91e9 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/TranslationMap.java @@ -0,0 +1,216 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner; + +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.analyzer.Analysis; +import com.facebook.presto.sql.analyzer.FieldOrExpression; +import com.facebook.presto.sql.tree.Cast; +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.ExpressionRewriter; +import com.facebook.presto.sql.tree.ExpressionTreeRewriter; +import com.facebook.presto.sql.tree.FunctionCall; +import com.facebook.presto.sql.tree.QualifiedName; +import com.facebook.presto.sql.tree.QualifiedNameReference; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.facebook.presto.sql.QueryUtil.mangleFieldReference; + +/** + * Keeps track of fields and expressions and their mapping to symbols in the current plan + */ +class TranslationMap +{ + // all expressions are rewritten in terms of fields declared by this relation plan + private final RelationPlan rewriteBase; + private final Analysis analysis; + + // current mappings of underlying field -> symbol for translating direct field references + private final Symbol[] fieldSymbols; + + // current mappings of sub-expressions -> symbol + private final Map expressionMappings = new HashMap<>(); + + public TranslationMap(RelationPlan rewriteBase, Analysis analysis) + { + this.rewriteBase = rewriteBase; + this.analysis = analysis; + + fieldSymbols = new Symbol[rewriteBase.getOutputSymbols().size()]; + } + + public RelationPlan getRelationPlan() + { + return rewriteBase; + } + + public void setFieldMappings(List symbols) + { + Preconditions.checkArgument(symbols.size() == fieldSymbols.length, "size of symbols list (%s) doesn't match number of expected fields (%s)", symbols.size(), fieldSymbols.length); + + for (int i = 0; i < symbols.size(); i++) { + this.fieldSymbols[i] = symbols.get(i); + } + } + + public void copyMappingsFrom(TranslationMap other) + { + Preconditions.checkArgument(other.fieldSymbols.length == fieldSymbols.length, + "number of fields in other (%s) doesn't match number of expected fields (%s)", + other.fieldSymbols.length, + fieldSymbols.length); + + expressionMappings.putAll(other.expressionMappings); + System.arraycopy(other.fieldSymbols, 0, fieldSymbols, 0, other.fieldSymbols.length); + } + + public Expression rewrite(Expression expression) + { + // first, translate names from sql-land references to plan symbols + Expression mapped = translateNamesToSymbols(expression); + + // then rewrite subexpressions in terms of the current mappings + return ExpressionTreeRewriter.rewriteWith(new ExpressionRewriter() + { + @Override + public Expression rewriteExpression(Expression node, Void context, ExpressionTreeRewriter treeRewriter) + { + // convert expression to qualified name reference (containing a symbol) if rewrite registered + Expression rewrittenExpression; + Symbol symbol = expressionMappings.get(node); + if (symbol != null) { + rewrittenExpression = new QualifiedNameReference(symbol.toQualifiedName()); + } + else { + rewrittenExpression = treeRewriter.defaultRewrite(node, context); + } + + return rewrittenExpression; + } + }, mapped); + } + + public Expression rewrite(FieldOrExpression fieldOrExpression) + { + if (fieldOrExpression.isFieldReference()) { + int fieldIndex = fieldOrExpression.getFieldIndex(); + Symbol symbol = fieldSymbols[fieldIndex]; + Preconditions.checkState(symbol != null, "No mapping for field '%s'", fieldIndex); + + return new QualifiedNameReference(symbol.toQualifiedName()); + } + else { + return rewrite(fieldOrExpression.getExpression()); + } + } + + public void put(Expression expression, Symbol symbol) + { + Expression translated = translateNamesToSymbols(expression); + expressionMappings.put(translated, symbol); + + // also update the field mappings if this expression is a simple field reference + if (expression instanceof QualifiedNameReference) { + int fieldIndex = analysis.getResolvedNames(expression).get(((QualifiedNameReference) expression).getName()); + fieldSymbols[fieldIndex] = symbol; + } + } + + public void put(FieldOrExpression fieldOrExpression, Symbol symbol) + { + if (fieldOrExpression.isFieldReference()) { + int fieldIndex = fieldOrExpression.getFieldIndex(); + fieldSymbols[fieldIndex] = symbol; + expressionMappings.put(new QualifiedNameReference(rewriteBase.getSymbol(fieldIndex).toQualifiedName()), symbol); + } + else { + put(fieldOrExpression.getExpression(), symbol); + } + } + + public Symbol get(Expression expression) + { + Expression translated = translateNamesToSymbols(expression); + + Preconditions.checkArgument(expressionMappings.containsKey(translated), "No mapping for expression: %s", expression); + return expressionMappings.get(translated); + } + + public Symbol get(FieldOrExpression fieldOrExpression) + { + if (fieldOrExpression.isFieldReference()) { + int field = fieldOrExpression.getFieldIndex(); + Preconditions.checkArgument(fieldSymbols[field] != null, "No mapping for field: %s", field); + return fieldSymbols[field]; + } + else { + return get(fieldOrExpression.getExpression()); + } + } + + private Expression translateNamesToSymbols(Expression expression) + { + final Map resolvedNames = analysis.getResolvedNames(expression); + Preconditions.checkArgument(resolvedNames != null, "No resolved names for expression %s", expression); + + return ExpressionTreeRewriter.rewriteWith(new ExpressionRewriter() + { + @Override + public Expression rewriteExpression(Expression node, Void context, ExpressionTreeRewriter treeRewriter) + { + Expression rewrittenExpression = treeRewriter.defaultRewrite(node, context); + + // cast expression if coercion is registered + Type coercion = analysis.getCoercion(node); + if (coercion != null) { + rewrittenExpression = new Cast(rewrittenExpression, coercion.getTypeSignature().toString()); + } + + return rewrittenExpression; + } + + @Override + public Expression rewriteQualifiedNameReference(QualifiedNameReference node, Void context, ExpressionTreeRewriter treeRewriter) + { + QualifiedName name = node.getName(); + + Integer fieldIndex = resolvedNames.get(name); + Preconditions.checkState(fieldIndex != null, "No field mapping for name '%s'", name); + + Symbol symbol = rewriteBase.getSymbol(fieldIndex); + Preconditions.checkState(symbol != null, "No symbol mapping for name '%s' (%s)", name, fieldIndex); + + Expression rewrittenExpression = new QualifiedNameReference(symbol.toQualifiedName()); + + if (analysis.isRowFieldReference(node)) { + QualifiedName mangledName = QualifiedName.of(mangleFieldReference(node.getName().getSuffix())); + rewrittenExpression = new FunctionCall(mangledName, ImmutableList.of(rewrittenExpression)); + } + + // cast expression if coercion is registered + Type coercion = analysis.getCoercion(node); + if (coercion != null) { + rewrittenExpression = new Cast(rewrittenExpression, coercion.getTypeSignature().toString()); + } + + return rewrittenExpression; + } + }, expression); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/ActualProperties.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/ActualProperties.java new file mode 100644 index 00000000..1331b8c6 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/ActualProperties.java @@ -0,0 +1,336 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.optimizations; + +import com.facebook.presto.spi.ConstantProperty; +import com.facebook.presto.spi.LocalProperty; +import com.facebook.presto.sql.planner.Symbol; +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import static com.facebook.presto.util.ImmutableCollectors.toImmutableSet; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.collect.Iterables.transform; +import static java.util.Objects.requireNonNull; + +class ActualProperties +{ + private final Optional> partitioningColumns; // if missing => partitioned with some unknown scheme + private final Optional> hashingColumns; // if present => hash partitioned on the given columns. partitioningColumns and hashingColumns must contain the same columns + private final boolean partitioned; // true if executing on multiple instances; false if executing on a single instance, which implies partitioned on the empty set of columns + private final boolean coordinatorOnly; + private final List> localProperties; + private final Map constants; + + private ActualProperties( + Optional> partitioningColumns, + Optional> hashingColumns, + List> localProperties, + boolean partitioned, + boolean coordinatorOnly, + Map constants) + { + requireNonNull(partitioningColumns, "partitioningColumns is null"); + requireNonNull(hashingColumns, "hashingColumns is null"); + checkArgument(!hashingColumns.isPresent() || hashingColumns.map(ImmutableSet::copyOf).equals(partitioningColumns), "hashColumns must contain the columns as partitioningColumns if present"); + checkArgument(partitioned || partitioningColumns.isPresent() && partitioningColumns.get().isEmpty(), "partitioningColumns must contain the empty set when unpartitioned"); + checkArgument(!coordinatorOnly || !partitioned, "must not be partitioned when running as coordinatorOnly"); + requireNonNull(localProperties, "localProperties is null"); + requireNonNull(constants, "constants is null"); + + this.partitioningColumns = partitioningColumns.map(ImmutableSet::copyOf); + this.hashingColumns = hashingColumns.map(ImmutableList::copyOf); + this.partitioned = partitioned; + this.coordinatorOnly = coordinatorOnly; + + // There is some overlap between the localProperties (ConstantProperty) and constants fields. + // Let's normalize both of those structures here so that they are consistent with each other + Set propertyConstants = LocalProperties.extractLeadingConstants(localProperties); + localProperties = LocalProperties.stripLeadingConstants(localProperties); + + Map updatedConstants = new HashMap<>(); + propertyConstants.stream() + .forEach(symbol -> updatedConstants.put(symbol, new Object())); + updatedConstants.putAll(constants); + + List> updatedLocalProperties = LocalProperties.normalizeAndPrune(ImmutableList.>builder() + .addAll(transform(updatedConstants.keySet(), ConstantProperty::new)) + .addAll(localProperties) + .build()); + + this.localProperties = ImmutableList.copyOf(updatedLocalProperties); + this.constants = ImmutableMap.copyOf(updatedConstants); + } + + public static ActualProperties unpartitioned() + { + return new ActualProperties(Optional.of(ImmutableSet.of()), Optional.empty(), ImmutableList.of(), false, false, ImmutableMap.of()); + } + + public static ActualProperties hashPartitioned(List columns) + { + return new ActualProperties( + Optional.of(ImmutableSet.copyOf(columns)), + Optional.of(ImmutableList.copyOf(columns)), + ImmutableList.of(), + true, + false, + ImmutableMap.of()); + } + + public static ActualProperties partitioned() + { + return new ActualProperties(Optional.>empty(), Optional.>empty(), ImmutableList.of(), true, false, ImmutableMap.of()); + } + + public static ActualProperties partitioned(Collection columns) + { + return new ActualProperties(Optional.>of(ImmutableSet.copyOf(columns)), Optional.>empty(), ImmutableList.of(), true, false, ImmutableMap.of()); + } + + public static ActualProperties partitioned(ActualProperties other) + { + return new ActualProperties(other.partitioningColumns, other.hashingColumns, ImmutableList.of(), other.partitioned, false, ImmutableMap.of()); + } + + public boolean isCoordinatorOnly() + { + return coordinatorOnly; + } + + public boolean isPartitioned() + { + return partitioned; + } + + public boolean isPartitionedOn(Collection columns) + { + // partitioned on (k_1, k_2, ..., k_n) => partitioned on (k_1, k_2, ..., k_n, k_n+1, ...) + // can safely ignore all constant columns when comparing partition properties + return partitioningColumns.isPresent() && ImmutableSet.copyOf(columns).containsAll(getPartitioningColumnsWithoutConstants().get()); + } + + /** + * @return true if all the data will effectively land in a single stream + */ + public boolean isSingleStream() + { + Optional> partitioningWithoutConstants = getPartitioningColumnsWithoutConstants(); + return partitioningWithoutConstants.isPresent() && partitioningWithoutConstants.get().isEmpty(); + } + + /** + * @return true if repartitioning on the keys will yield some difference + */ + public boolean isRepartitionEffective(Collection keys) + { + Optional> partitioningWithoutConstants = getPartitioningColumnsWithoutConstants(); + if (!partitioningWithoutConstants.isPresent()) { + return true; + } + Set keysWithoutConstants = keys.stream() + .filter(symbol -> !constants.containsKey(symbol)) + .collect(toImmutableSet()); + return !partitioningWithoutConstants.get().equals(keysWithoutConstants); + } + + public boolean hasKnownPartitioningScheme() + { + return partitioningColumns.isPresent(); + } + + public Optional> getPartitioningColumns() + { + // can safely ignore all constant columns when comparing partition properties + return getPartitioningColumnsWithoutConstants(); + } + + private Optional> getPartitioningColumnsWithoutConstants() + { + return partitioningColumns.map(symbols -> { + return symbols.stream() + .filter(symbol -> !constants.containsKey(symbol)) + .collect(toImmutableSet()); + } + ); + } + + public boolean isHashPartitionedOn(List columns) + { + return hashingColumns.isPresent() && hashingColumns.get().equals(columns); + } + + public boolean isHashPartitioned() + { + return hashingColumns.isPresent(); + } + + public Optional> getHashPartitioningColumns() + { + return hashingColumns; + } + + public Map getConstants() + { + return constants; + } + + public List> getLocalProperties() + { + return localProperties; + } + + public static Builder builder() + { + return new Builder(); + } + + public static class Builder + { + private Optional> partitioningColumns; // if missing => partitioned with some unknown scheme + private Optional> hashingColumns; // if present => hash partitioned on the given columns. partitioningColumns and hashingColumns must contain the same columns + private boolean partitioned; // true if executing on multiple instances; false if executing on a single instance, which implies partitioned on the empty set of columns + private boolean coordinatorOnly; + private List> localProperties = ImmutableList.of(); + private Map constants = ImmutableMap.of(); + + public Builder unpartitioned() + { + partitioningColumns = Optional.of(ImmutableSet.of()); + hashingColumns = Optional.empty(); + partitioned = false; + + return this; + } + + public Builder partitioned(ActualProperties other) + { + partitioningColumns = other.partitioningColumns; + hashingColumns = other.hashingColumns; + partitioned = other.partitioned; + + return this; + } + + public Builder partitioned() + { + partitioningColumns = Optional.empty(); + hashingColumns = Optional.empty(); + partitioned = true; + + return this; + } + + public Builder coordinatorOnly(ActualProperties other) + { + coordinatorOnly = other.coordinatorOnly; + + return this; + } + + public Builder partitioned(Set columns) + { + partitioningColumns = Optional.of(ImmutableSet.copyOf(columns)); + hashingColumns = Optional.empty(); + partitioned = true; + + return this; + } + + public Builder hashPartitioned(List columns) + { + partitioningColumns = Optional.of(ImmutableSet.copyOf(columns)); + hashingColumns = Optional.of(ImmutableList.copyOf(columns)); + partitioned = true; + + return this; + } + + public Builder local(List> localProperties) + { + this.localProperties = ImmutableList.copyOf(localProperties); + return this; + } + + public Builder local(ActualProperties other) + { + this.localProperties = ImmutableList.copyOf(other.localProperties); + return this; + } + + public Builder constants(Map constants) + { + this.constants = ImmutableMap.copyOf(constants); + return this; + } + + public Builder constants(ActualProperties other) + { + this.constants = ImmutableMap.copyOf(other.constants); + return this; + } + + public ActualProperties build() + { + return new ActualProperties(partitioningColumns, hashingColumns, localProperties, partitioned, coordinatorOnly, constants); + } + } + + @Override + public String toString() + { + return MoreObjects.toStringHelper(this) + .add("partitioningColumns", partitioningColumns) + .add("hashingColumns", hashingColumns) + .add("partitioned", partitioned) + .add("coordinatorOnly", coordinatorOnly) + .add("localProperties", localProperties) + .add("constants", constants) + .toString(); + } + + @Override + public int hashCode() + { + return Objects.hash(partitioningColumns, hashingColumns, partitioned, coordinatorOnly, localProperties, constants.keySet()); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final ActualProperties other = (ActualProperties) obj; + return Objects.equals(this.partitioningColumns, other.partitioningColumns) + && Objects.equals(this.hashingColumns, other.hashingColumns) + && Objects.equals(this.partitioned, other.partitioned) + && Objects.equals(this.coordinatorOnly, other.coordinatorOnly) + && Objects.equals(this.localProperties, other.localProperties) + && Objects.equals(this.constants.keySet(), other.constants.keySet()); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/AddExchanges.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/AddExchanges.java new file mode 100644 index 00000000..e6151115 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/AddExchanges.java @@ -0,0 +1,1009 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.optimizations; + +import com.facebook.presto.Session; +import com.facebook.presto.SystemSessionProperties; +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.metadata.TableLayoutResult; +import com.facebook.presto.operator.aggregation.InternalAggregationFunction; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.Constraint; +import com.facebook.presto.spi.GroupingProperty; +import com.facebook.presto.spi.LocalProperty; +import com.facebook.presto.spi.SortingProperty; +import com.facebook.presto.spi.TupleDomain; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.parser.SqlParser; +import com.facebook.presto.sql.planner.DomainTranslator; +import com.facebook.presto.sql.planner.ExpressionInterpreter; +import com.facebook.presto.sql.planner.LookupSymbolResolver; +import com.facebook.presto.sql.planner.PlanNodeIdAllocator; +import com.facebook.presto.sql.planner.Symbol; +import com.facebook.presto.sql.planner.SymbolAllocator; +import com.facebook.presto.sql.planner.optimizations.PreferredProperties.PartitioningPreferences; +import com.facebook.presto.sql.planner.plan.AggregationNode; +import com.facebook.presto.sql.planner.plan.ChildReplacer; +import com.facebook.presto.sql.planner.plan.DistinctLimitNode; +import com.facebook.presto.sql.planner.plan.ExchangeNode; +import com.facebook.presto.sql.planner.plan.FilterNode; +import com.facebook.presto.sql.planner.plan.IndexJoinNode; +import com.facebook.presto.sql.planner.plan.JoinNode; +import com.facebook.presto.sql.planner.plan.LimitNode; +import com.facebook.presto.sql.planner.plan.MarkDistinctNode; +import com.facebook.presto.sql.planner.plan.OutputNode; +import com.facebook.presto.sql.planner.plan.PlanNode; +import com.facebook.presto.sql.planner.plan.PlanVisitor; +import com.facebook.presto.sql.planner.plan.ProjectNode; +import com.facebook.presto.sql.planner.plan.RowNumberNode; +import com.facebook.presto.sql.planner.plan.SemiJoinNode; +import com.facebook.presto.sql.planner.plan.SortNode; +import com.facebook.presto.sql.planner.plan.TableCommitNode; +import com.facebook.presto.sql.planner.plan.TableScanNode; +import com.facebook.presto.sql.planner.plan.TopNNode; +import com.facebook.presto.sql.planner.plan.TopNRowNumberNode; +import com.facebook.presto.sql.planner.plan.UnionNode; +import com.facebook.presto.sql.planner.plan.ValuesNode; +import com.facebook.presto.sql.planner.plan.WindowNode; +import com.facebook.presto.sql.tree.BooleanLiteral; +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.FunctionCall; +import com.facebook.presto.sql.tree.NullLiteral; +import com.facebook.presto.sql.tree.QualifiedNameReference; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.ComparisonChain; +import com.google.common.collect.ImmutableBiMap; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; + +import static com.facebook.presto.SystemSessionProperties.isBigQueryEnabled; +import static com.facebook.presto.sql.ExpressionUtils.combineConjuncts; +import static com.facebook.presto.sql.ExpressionUtils.extractConjuncts; +import static com.facebook.presto.sql.ExpressionUtils.stripDeterministicConjuncts; +import static com.facebook.presto.sql.ExpressionUtils.stripNonDeterministicConjuncts; +import static com.facebook.presto.sql.analyzer.ExpressionAnalyzer.getExpressionTypes; +import static com.facebook.presto.sql.planner.optimizations.LocalProperties.grouped; +import static com.facebook.presto.sql.planner.plan.AggregationNode.Step.FINAL; +import static com.facebook.presto.sql.planner.plan.AggregationNode.Step.PARTIAL; +import static com.facebook.presto.sql.planner.plan.ExchangeNode.gatheringExchange; +import static com.facebook.presto.sql.planner.plan.ExchangeNode.partitionedExchange; +import static com.facebook.presto.sql.planner.plan.JoinNode.Type.FULL; +import static com.facebook.presto.sql.planner.plan.JoinNode.Type.RIGHT; +import static com.facebook.presto.util.ImmutableCollectors.toImmutableSet; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.Iterables.getOnlyElement; +import static java.util.stream.Collectors.toList; + +public class AddExchanges + extends PlanOptimizer +{ + private final SqlParser parser; + private final Metadata metadata; + private final boolean distributedIndexJoins; + private final boolean distributedJoins; + + public AddExchanges(Metadata metadata, SqlParser parser, boolean distributedIndexJoins, boolean distributedJoins) + { + this.metadata = metadata; + this.parser = parser; + this.distributedIndexJoins = distributedIndexJoins; + this.distributedJoins = distributedJoins; + } + + @Override + public PlanNode optimize(PlanNode plan, Session session, Map types, SymbolAllocator symbolAllocator, PlanNodeIdAllocator idAllocator) + { + boolean distributedJoinEnabled = SystemSessionProperties.isDistributedJoinEnabled(session, distributedJoins); + PlanWithProperties result = plan.accept(new Rewriter(symbolAllocator, idAllocator, symbolAllocator, session, distributedIndexJoins, distributedJoinEnabled), PreferredProperties.any()); + return result.getNode(); + } + + private class Rewriter + extends PlanVisitor + { + private final SymbolAllocator allocator; + private final PlanNodeIdAllocator idAllocator; + private final SymbolAllocator symbolAllocator; + private final Session session; + private final boolean distributedIndexJoins; + private final boolean distributedJoins; + + public Rewriter(SymbolAllocator allocator, PlanNodeIdAllocator idAllocator, SymbolAllocator symbolAllocator, Session session, boolean distributedIndexJoins, boolean distributedJoins) + { + this.allocator = allocator; + this.idAllocator = idAllocator; + this.symbolAllocator = symbolAllocator; + this.session = session; + this.distributedIndexJoins = distributedIndexJoins; + this.distributedJoins = distributedJoins; + } + + @Override + protected PlanWithProperties visitPlan(PlanNode node, PreferredProperties preferred) + { + return rebaseAndDeriveProperties(node, planChild(node, preferred)); + } + + @Override + public PlanWithProperties visitProject(ProjectNode node, PreferredProperties preferred) + { + Map identities = computeIdentityTranslations(node.getAssignments()); + PreferredProperties translatedPreferred = PreferredProperties.translate(preferred, identities); + + return rebaseAndDeriveProperties(node, planChild(node, translatedPreferred)); + } + + @Override + public PlanWithProperties visitOutput(OutputNode node, PreferredProperties preferred) + { + PlanWithProperties child = planChild(node, PreferredProperties.any()); + + if (child.getProperties().isPartitioned()) { + child = withDerivedProperties( + gatheringExchange(idAllocator.getNextId(), child.getNode()), + child.getProperties()); + } + + return rebaseAndDeriveProperties(node, child); + } + + @Override + public PlanWithProperties visitAggregation(AggregationNode node, PreferredProperties preferred) + { + boolean decomposable = node.getFunctions() + .values().stream() + .map(metadata::getExactFunction) + .map(FunctionInfo::getAggregationFunction) + .allMatch(InternalAggregationFunction::isDecomposable); + + PreferredProperties preferredProperties = node.getGroupBy().isEmpty() + ? PreferredProperties.any() + : PreferredProperties.derivePreferences(preferred, ImmutableSet.copyOf(node.getGroupBy()), Optional.of(node.getGroupBy()), grouped(node.getGroupBy())); + + PlanWithProperties child = planChild(node, preferredProperties); + + if (!child.getProperties().isPartitioned()) { + // If already unpartitioned, just drop the single aggregation back on + return rebaseAndDeriveProperties(node, child); + } + + if (node.getGroupBy().isEmpty()) { + if (decomposable) { + return splitAggregation(node, child, partial -> gatheringExchange(idAllocator.getNextId(), partial)); + } + else { + child = withDerivedProperties( + gatheringExchange(idAllocator.getNextId(), child.getNode()), + child.getProperties()); + + return rebaseAndDeriveProperties(node, child); + } + } + else { + if (child.getProperties().isPartitionedOn(node.getGroupBy())) { + return rebaseAndDeriveProperties(node, child); + } + else { + if (decomposable) { + return splitAggregation(node, child, partial -> partitionedExchange(idAllocator.getNextId(), partial, node.getGroupBy(), node.getHashSymbol())); + } + else { + child = withDerivedProperties( + partitionedExchange(idAllocator.getNextId(), child.getNode(), node.getGroupBy(), node.getHashSymbol()), + child.getProperties()); + return rebaseAndDeriveProperties(node, child); + } + } + } + } + + @NotNull + private PlanWithProperties splitAggregation(AggregationNode node, PlanWithProperties newChild, Function exchanger) + { + // otherwise, add a partial and final with an exchange in between + Map masks = node.getMasks(); + + Map finalCalls = new HashMap<>(); + Map intermediateCalls = new HashMap<>(); + Map intermediateFunctions = new HashMap<>(); + Map intermediateMask = new HashMap<>(); + for (Map.Entry entry : node.getAggregations().entrySet()) { + Signature signature = node.getFunctions().get(entry.getKey()); + FunctionInfo function = metadata.getExactFunction(signature); + + Symbol intermediateSymbol = allocator.newSymbol(function.getName().getSuffix(), metadata.getType(function.getIntermediateType())); + intermediateCalls.put(intermediateSymbol, entry.getValue()); + intermediateFunctions.put(intermediateSymbol, signature); + if (masks.containsKey(entry.getKey())) { + intermediateMask.put(intermediateSymbol, masks.get(entry.getKey())); + } + + // rewrite final aggregation in terms of intermediate function + finalCalls.put(entry.getKey(), new FunctionCall(function.getName(), ImmutableList.of(new QualifiedNameReference(intermediateSymbol.toQualifiedName())))); + } + + PlanWithProperties partial = withDerivedProperties( + new AggregationNode( + idAllocator.getNextId(), + newChild.getNode(), + node.getGroupBy(), + intermediateCalls, + intermediateFunctions, + intermediateMask, + PARTIAL, + node.getSampleWeight(), + node.getConfidence(), + node.getHashSymbol()), + newChild.getProperties()); + + PlanNode exchange = exchanger.apply(partial.getNode()); + + return withDerivedProperties( + new AggregationNode( + node.getId(), + exchange, + node.getGroupBy(), + finalCalls, + node.getFunctions(), + ImmutableMap.of(), + FINAL, + Optional.empty(), + node.getConfidence(), + node.getHashSymbol()), + deriveProperties(exchange, partial.getProperties())); + } + + @Override + public PlanWithProperties visitMarkDistinct(MarkDistinctNode node, PreferredProperties preferred) + { + PreferredProperties preferredChildProperties = PreferredProperties.derivePreferences(preferred, ImmutableSet.copyOf(node.getDistinctSymbols()), Optional.of(node.getDistinctSymbols()), grouped(node.getDistinctSymbols())); + PlanWithProperties child = node.getSource().accept(this, preferredChildProperties); + + if ((!child.getProperties().isPartitioned() && isBigQueryEnabled(session, false)) || + !child.getProperties().isPartitionedOn(node.getDistinctSymbols())) { + child = withDerivedProperties( + partitionedExchange( + idAllocator.getNextId(), + child.getNode(), + node.getDistinctSymbols(), + node.getHashSymbol()), + child.getProperties()); + } + + return rebaseAndDeriveProperties(node, child); + } + + @Override + public PlanWithProperties visitWindow(WindowNode node, PreferredProperties preferred) + { + List> desiredProperties = new ArrayList<>(); + if (!node.getPartitionBy().isEmpty()) { + desiredProperties.add(new GroupingProperty<>(node.getPartitionBy())); + } + for (Symbol symbol : node.getOrderBy()) { + desiredProperties.add(new SortingProperty<>(symbol, node.getOrderings().get(symbol))); + } + + PlanWithProperties child = planChild(node, PreferredProperties.derivePreferences(preferred, ImmutableSet.copyOf(node.getPartitionBy()), desiredProperties)); + + if (!child.getProperties().isPartitionedOn(node.getPartitionBy())) { + if (node.getPartitionBy().isEmpty()) { + child = withDerivedProperties( + gatheringExchange(idAllocator.getNextId(), child.getNode()), + child.getProperties()); + } + else { + child = withDerivedProperties( + partitionedExchange(idAllocator.getNextId(), child.getNode(), node.getPartitionBy(), node.getHashSymbol()), + child.getProperties()); + } + } + + Iterator>> matchIterator = LocalProperties.match(child.getProperties().getLocalProperties(), desiredProperties).iterator(); + + Set prePartitionedInputs = ImmutableSet.of(); + if (!node.getPartitionBy().isEmpty()) { + Optional> groupingRequirement = matchIterator.next(); + Set unPartitionedInputs = groupingRequirement.map(LocalProperty::getColumns).orElse(ImmutableSet.of()); + prePartitionedInputs = node.getPartitionBy().stream() + .filter(symbol -> !unPartitionedInputs.contains(symbol)) + .collect(toImmutableSet()); + } + + int preSortedOrderPrefix = 0; + if (prePartitionedInputs.equals(ImmutableSet.copyOf(node.getPartitionBy()))) { + while (matchIterator.hasNext() && !matchIterator.next().isPresent()) { + preSortedOrderPrefix++; + } + } + + return withDerivedProperties( + new WindowNode( + node.getId(), + child.getNode(), + node.getPartitionBy(), + node.getOrderBy(), + node.getOrderings(), + node.getFrame(), + node.getWindowFunctions(), + node.getSignatures(), + node.getHashSymbol(), + prePartitionedInputs, + preSortedOrderPrefix), + child.getProperties()); + } + + @Override + public PlanWithProperties visitRowNumber(RowNumberNode node, PreferredProperties preferred) + { + if (node.getPartitionBy().isEmpty()) { + PlanWithProperties child = planChild(node, PreferredProperties.unpartitioned()); + + if (child.getProperties().isPartitioned()) { + child = withDerivedProperties( + gatheringExchange(idAllocator.getNextId(), child.getNode()), + child.getProperties()); + } + + return rebaseAndDeriveProperties(node, child); + } + + PlanWithProperties child = planChild(node, PreferredProperties.derivePreferences(preferred, ImmutableSet.copyOf(node.getPartitionBy()), grouped(node.getPartitionBy()))); + + // TODO: add config option/session property to force parallel plan if child is unpartitioned and window has a PARTITION BY clause + if (!child.getProperties().isPartitionedOn(node.getPartitionBy())) { + child = withDerivedProperties( + partitionedExchange( + idAllocator.getNextId(), + child.getNode(), + node.getPartitionBy(), + node.getHashSymbol()), + child.getProperties()); + } + + // TODO: streaming + + return rebaseAndDeriveProperties(node, child); + } + + @Override + public PlanWithProperties visitTopNRowNumber(TopNRowNumberNode node, PreferredProperties preferred) + { + PreferredProperties preferredChildProperties; + Function addExchange; + + if (node.getPartitionBy().isEmpty()) { + preferredChildProperties = PreferredProperties.any(); + addExchange = partial -> gatheringExchange(idAllocator.getNextId(), partial); + } + else { + preferredChildProperties = PreferredProperties.derivePreferences(preferred, ImmutableSet.copyOf(node.getPartitionBy()), grouped(node.getPartitionBy())); + addExchange = partial -> partitionedExchange(idAllocator.getNextId(), partial, node.getPartitionBy(), node.getHashSymbol()); + } + + PlanWithProperties child = planChild(node, preferredChildProperties); + if (!child.getProperties().isPartitionedOn(node.getPartitionBy())) { + // add exchange + push function to child + child = withDerivedProperties( + new TopNRowNumberNode( + idAllocator.getNextId(), + child.getNode(), + node.getPartitionBy(), + node.getOrderBy(), + node.getOrderings(), + node.getRowNumberSymbol(), + node.getMaxRowCountPerPartition(), + true, + node.getHashSymbol()), + child.getProperties()); + + child = withDerivedProperties(addExchange.apply(child.getNode()), child.getProperties()); + } + + return rebaseAndDeriveProperties(node, child); + } + + @Override + public PlanWithProperties visitTopN(TopNNode node, PreferredProperties preferred) + { + PlanWithProperties child = planChild(node, PreferredProperties.any()); + + if (child.getProperties().isPartitioned()) { + child = withDerivedProperties( + new TopNNode(idAllocator.getNextId(), child.getNode(), node.getCount(), node.getOrderBy(), node.getOrderings(), true), + child.getProperties()); + + child = withDerivedProperties( + gatheringExchange(idAllocator.getNextId(), child.getNode()), + child.getProperties()); + } + + return rebaseAndDeriveProperties(node, child); + } + + @Override + public PlanWithProperties visitSort(SortNode node, PreferredProperties preferred) + { + PlanWithProperties child = planChild(node, PreferredProperties.unpartitioned()); + + if (child.getProperties().isPartitioned()) { + child = withDerivedProperties( + gatheringExchange(idAllocator.getNextId(), child.getNode()), + child.getProperties()); + } + + return rebaseAndDeriveProperties(node, child); + } + + @Override + public PlanWithProperties visitLimit(LimitNode node, PreferredProperties preferred) + { + PlanWithProperties child = planChild(node, PreferredProperties.any()); + + if (child.getProperties().isPartitioned()) { + child = withDerivedProperties( + new LimitNode(idAllocator.getNextId(), child.getNode(), node.getCount()), + child.getProperties()); + + child = withDerivedProperties( + gatheringExchange(idAllocator.getNextId(), child.getNode()), + child.getProperties()); + } + + return rebaseAndDeriveProperties(node, child); + } + + @Override + public PlanWithProperties visitDistinctLimit(DistinctLimitNode node, PreferredProperties preferred) + { + PlanWithProperties child = planChild(node, PreferredProperties.any()); + + if (child.getProperties().isPartitioned()) { + child = withDerivedProperties( + new DistinctLimitNode(idAllocator.getNextId(), child.getNode(), node.getLimit(), node.getHashSymbol()), + child.getProperties()); + + child = withDerivedProperties( + gatheringExchange( + idAllocator.getNextId(), + new DistinctLimitNode(idAllocator.getNextId(), child.getNode(), node.getLimit(), node.getHashSymbol())), + child.getProperties()); + } + + return rebaseAndDeriveProperties(node, child); + } + + @Override + public PlanWithProperties visitFilter(FilterNode node, PreferredProperties preferred) + { + if (node.getSource() instanceof TableScanNode) { + return planTableScan((TableScanNode) node.getSource(), node.getPredicate(), preferred); + } + + return rebaseAndDeriveProperties(node, planChild(node, preferred)); + } + + @Override + public PlanWithProperties visitTableScan(TableScanNode node, PreferredProperties preferred) + { + return planTableScan(node, BooleanLiteral.TRUE_LITERAL, preferred); + } + + private PlanWithProperties planTableScan(TableScanNode node, Expression predicate, PreferredProperties preferred) + { + // don't include non-deterministic predicates + Expression deterministicPredicate = stripNonDeterministicConjuncts(predicate); + + DomainTranslator.ExtractionResult decomposedPredicate = DomainTranslator.fromPredicate( + metadata, + session, + deterministicPredicate, + symbolAllocator.getTypes()); + + TupleDomain simplifiedConstraint = decomposedPredicate.getTupleDomain() + .transform(node.getAssignments()::get) + .intersect(node.getCurrentConstraint()); + + Map assignments = ImmutableBiMap.copyOf(node.getAssignments()).inverse(); + + Expression constraint = combineConjuncts( + deterministicPredicate, + DomainTranslator.toPredicate( + node.getCurrentConstraint().transform(assignments::get), + symbolAllocator.getTypes())); + + // Layouts will be returned in order of the connector's preference + List layouts = metadata.getLayouts( + node.getTable(), + new Constraint<>(simplifiedConstraint, bindings -> !shouldPrune(constraint, node.getAssignments(), bindings)), + Optional.of(node.getOutputSymbols().stream() + .map(node.getAssignments()::get) + .collect(toImmutableSet()))); + + if (layouts.isEmpty()) { + return new PlanWithProperties( + new ValuesNode(idAllocator.getNextId(), node.getOutputSymbols(), ImmutableList.of()), + ActualProperties.unpartitioned()); + } + + // Filter out layouts that cannot supply all the required columns + layouts = layouts.stream() + .filter(layoutHasAllNeededOutputs(node)) + .collect(toList()); + checkState(!layouts.isEmpty(), "No usable layouts for %s", node); + + List possiblePlans = layouts.stream() + .map(layout -> { + TableScanNode tableScan = new TableScanNode( + node.getId(), + node.getTable(), + node.getOutputSymbols(), + node.getAssignments(), + Optional.of(layout.getLayout().getHandle()), + simplifiedConstraint.intersect(layout.getLayout().getPredicate()), + Optional.ofNullable(node.getOriginalConstraint()).orElse(predicate)); + + PlanWithProperties result = new PlanWithProperties(tableScan, deriveProperties(tableScan, ImmutableList.of())); + + Expression resultingPredicate = combineConjuncts( + DomainTranslator.toPredicate( + layout.getUnenforcedConstraint().transform(assignments::get), + symbolAllocator.getTypes()), + stripDeterministicConjuncts(predicate), + decomposedPredicate.getRemainingExpression()); + + if (!BooleanLiteral.TRUE_LITERAL.equals(resultingPredicate)) { + return withDerivedProperties( + new FilterNode(idAllocator.getNextId(), result.getNode(), resultingPredicate), + deriveProperties(tableScan, ImmutableList.of())); + } + + return result; + }) + .collect(toList()); + + return pickPlan(possiblePlans, preferred); + } + + private Predicate layoutHasAllNeededOutputs(TableScanNode node) + { + return layout -> !layout.getLayout().getColumns().isPresent() + || layout.getLayout().getColumns().get().containsAll(Lists.transform(node.getOutputSymbols(), node.getAssignments()::get)); + } + + /** + * possiblePlans should be provided in layout preference order + */ + private PlanWithProperties pickPlan(List possiblePlans, PreferredProperties preferred) + { + checkArgument(!possiblePlans.isEmpty()); + + if (SystemSessionProperties.preferStreamingOperators(session, false)) { + possiblePlans = new ArrayList<>(possiblePlans); + Collections.sort(possiblePlans, Comparator.comparing(PlanWithProperties::getProperties, streamingExecutionPreference(preferred))); // stable sort; is Collections.min() guaranteed to be stable? + } + + return possiblePlans.get(0); + } + + private boolean shouldPrune(Expression predicate, Map assignments, Map bindings) + { + List conjuncts = extractConjuncts(predicate); + IdentityHashMap expressionTypes = getExpressionTypes(session, metadata, parser, symbolAllocator.getTypes(), predicate); + + LookupSymbolResolver inputs = new LookupSymbolResolver(assignments, bindings); + + // If any conjuncts evaluate to FALSE or null, then the whole predicate will never be true and so the partition should be pruned + for (Expression expression : conjuncts) { + ExpressionInterpreter optimizer = ExpressionInterpreter.expressionOptimizer(expression, metadata, session, expressionTypes); + Object optimized = optimizer.optimize(inputs); + if (Boolean.FALSE.equals(optimized) || optimized == null || optimized instanceof NullLiteral) { + return true; + } + } + return false; + } + + @Override + public PlanWithProperties visitValues(ValuesNode node, PreferredProperties preferred) + { + return new PlanWithProperties(node, ActualProperties.unpartitioned()); + } + + @Override + public PlanWithProperties visitTableCommit(TableCommitNode node, PreferredProperties preferred) + { + PlanWithProperties child = planChild(node, PreferredProperties.any()); + if (child.getProperties().isPartitioned() || !child.getProperties().isCoordinatorOnly()) { + child = withDerivedProperties( + gatheringExchange(idAllocator.getNextId(), child.getNode()), + child.getProperties()); + } + + return rebaseAndDeriveProperties(node, child); + } + + @Override + public PlanWithProperties visitJoin(JoinNode node, PreferredProperties preferred) + { + List leftSymbols = Lists.transform(node.getCriteria(), JoinNode.EquiJoinClause::getLeft); + List rightSymbols = Lists.transform(node.getCriteria(), JoinNode.EquiJoinClause::getRight); + + PlanWithProperties left; + PlanWithProperties right; + + if (distributedJoins || node.getType() == FULL || node.getType() == RIGHT) { + // The implementation of full outer join only works if the data is hash partitioned. See LookupJoinOperators#buildSideOuterJoinUnvisitedPositions + + left = node.getLeft().accept(this, PreferredProperties.hashPartitioned(leftSymbols)); + right = node.getRight().accept(this, PreferredProperties.hashPartitioned(rightSymbols)); + + // force partitioning + if (!left.getProperties().isHashPartitionedOn(leftSymbols)) { + left = withDerivedProperties( + partitionedExchange(idAllocator.getNextId(), left.getNode(), leftSymbols, node.getLeftHashSymbol()), + left.getProperties()); + } + + if (!right.getProperties().isHashPartitionedOn(rightSymbols)) { + right = withDerivedProperties( + partitionedExchange(idAllocator.getNextId(), right.getNode(), rightSymbols, node.getRightHashSymbol()), + right.getProperties()); + } + } + else { + // It can only be INNER or LEFT here. Therefore, no flipping is necessary even though the below code assumes the node is not RIGHT. + + left = node.getLeft().accept(this, PreferredProperties.any()); + right = node.getRight().accept(this, PreferredProperties.any()); + + if (!left.getProperties().isPartitioned() && right.getProperties().isPartitioned()) { + // force single-node join + // TODO: if inner join, flip order and do a broadcast join + right = withDerivedProperties(gatheringExchange(idAllocator.getNextId(), right.getNode()), right.getProperties()); + } + else if (left.getProperties().isPartitioned() && !(left.getProperties().isHashPartitionedOn(leftSymbols) && right.getProperties().isHashPartitionedOn(rightSymbols))) { + right = withDerivedProperties(new ExchangeNode( + idAllocator.getNextId(), + ExchangeNode.Type.REPLICATE, + ImmutableList.of(), + Optional.empty(), + ImmutableList.of(right.getNode()), + right.getNode().getOutputSymbols(), + ImmutableList.of(right.getNode().getOutputSymbols())), + right.getProperties()); + } + } + + JoinNode result = new JoinNode(node.getId(), + node.getType(), + left.getNode(), + right.getNode(), + node.getCriteria(), + node.getLeftHashSymbol(), + node.getRightHashSymbol()); + + return new PlanWithProperties(result, deriveProperties(result, ImmutableList.of(left.getProperties(), right.getProperties()))); + } + + @Override + public PlanWithProperties visitSemiJoin(SemiJoinNode node, PreferredProperties preferred) + { + PlanWithProperties source = node.getSource().accept(this, PreferredProperties.any()); + PlanWithProperties filteringSource = node.getFilteringSource().accept(this, PreferredProperties.any()); + + // make filtering source match requirements of source + if (source.getProperties().isPartitioned()) { + filteringSource = withDerivedProperties( + new ExchangeNode( + idAllocator.getNextId(), + ExchangeNode.Type.REPLICATE, + ImmutableList.of(), + Optional.empty(), + ImmutableList.of(filteringSource.getNode()), + filteringSource.getNode().getOutputSymbols(), + ImmutableList.of(filteringSource.getNode().getOutputSymbols())), + filteringSource.getProperties()); + } + else { + filteringSource = withDerivedProperties( + gatheringExchange(idAllocator.getNextId(), filteringSource.getNode()), + filteringSource.getProperties()); + } + + // TODO: add support for hash-partitioned semijoins + + return rebaseAndDeriveProperties(node, ImmutableList.of(source, filteringSource)); + } + + @Override + public PlanWithProperties visitIndexJoin(IndexJoinNode node, PreferredProperties preferredProperties) + { + List joinColumns = Lists.transform(node.getCriteria(), IndexJoinNode.EquiJoinClause::getProbe); + PlanWithProperties probeSource = node.getProbeSource().accept(this, PreferredProperties.derivePreferences(preferredProperties, ImmutableSet.copyOf(joinColumns), grouped(joinColumns))); + ActualProperties probeProperties = probeSource.getProperties(); + + // TODO: allow repartitioning if unpartitioned to increase parallelism + if (distributedIndexJoins && probeProperties.isPartitioned()) { + // Force partitioned exchange if we are not effectively partitioned on the join keys, or if the probe is currently executing as a single stream + // and the repartitioning will make a difference. + if (!probeProperties.isPartitionedOn(joinColumns) || (probeProperties.isSingleStream() && probeProperties.isRepartitionEffective(joinColumns))) { + probeSource = withDerivedProperties( + partitionedExchange(idAllocator.getNextId(), probeSource.getNode(), joinColumns, node.getProbeHashSymbol()), + probeProperties); + } + } + + // TODO: if input is grouped, create streaming join + + // index side is really a nested-loops plan, so don't add exchanges + PlanNode result = ChildReplacer.replaceChildren(node, ImmutableList.of(probeSource.getNode(), node.getIndexSource())); + return new PlanWithProperties(result, deriveProperties(result, probeSource.getProperties())); + } + + @Override + public PlanWithProperties visitUnion(UnionNode node, PreferredProperties preferred) + { + if (!preferred.getPartitioningProperties().isPresent() || !preferred.getPartitioningProperties().get().isHashPartitioned()) { + // first, classify children into partitioned and unpartitioned + List unpartitionedChildren = new ArrayList<>(); + List> unpartitionedOutputLayouts = new ArrayList<>(); + + List partitionedChildren = new ArrayList<>(); + List> partitionedOutputLayouts = new ArrayList<>(); + + List sources = node.getSources(); + for (int i = 0; i < sources.size(); i++) { + PlanWithProperties child = sources.get(i).accept(this, PreferredProperties.any()); + if (!child.getProperties().isPartitioned()) { + unpartitionedChildren.add(child.getNode()); + unpartitionedOutputLayouts.add(node.sourceOutputLayout(i)); + } + else { + partitionedChildren.add(child.getNode()); + partitionedOutputLayouts.add(node.sourceOutputLayout(i)); + } + } + + PlanNode result = null; + if (!partitionedChildren.isEmpty()) { + // add an exchange above partitioned inputs and fold it into the + // set of unpartitioned inputs + result = new ExchangeNode( + idAllocator.getNextId(), + ExchangeNode.Type.GATHER, + ImmutableList.of(), + Optional.empty(), + partitionedChildren, + node.getOutputSymbols(), + partitionedOutputLayouts); + + unpartitionedChildren.add(result); + unpartitionedOutputLayouts.add(result.getOutputSymbols()); + } + + // if there's at least one unpartitioned input (including the exchange that might have been added in the + // previous step), add a local union + if (unpartitionedChildren.size() > 1) { + ImmutableListMultimap.Builder mappings = ImmutableListMultimap.builder(); + for (int i = 0; i < node.getOutputSymbols().size(); i++) { + for (List outputLayout : unpartitionedOutputLayouts) { + mappings.put(node.getOutputSymbols().get(i), outputLayout.get(i)); + } + } + + result = new UnionNode(node.getId(), unpartitionedChildren, mappings.build()); + } + + return new PlanWithProperties(result, ActualProperties.unpartitioned()); + } + + // hash partition the sources + List hashingColumns = preferred.getPartitioningProperties().get().getHashPartitioningColumns().get(); + + ImmutableList.Builder partitionedSources = ImmutableList.builder(); + ImmutableListMultimap.Builder outputToSourcesMapping = ImmutableListMultimap.builder(); + + for (int sourceIndex = 0; sourceIndex < node.getSources().size(); sourceIndex++) { + ImmutableList.Builder hashColumnsBuilder = ImmutableList.builder(); + for (Symbol column : hashingColumns) { + hashColumnsBuilder.add(node.getSymbolMapping().get(column).get(sourceIndex)); + } + List sourceHashColumns = hashColumnsBuilder.build(); + + PlanWithProperties source = node.getSources().get(sourceIndex).accept(this, PreferredProperties.hashPartitioned(sourceHashColumns)); + if (!source.getProperties().isHashPartitionedOn(sourceHashColumns)) { + source = withDerivedProperties( + partitionedExchange( + idAllocator.getNextId(), + source.getNode(), + sourceHashColumns, + Optional.empty()), + source.getProperties()); + } + partitionedSources.add(source.getNode()); + + for (int column = 0; column < node.getOutputSymbols().size(); column++) { + outputToSourcesMapping.put(node.getOutputSymbols().get(column), node.sourceOutputLayout(sourceIndex).get(column)); + } + } + return new PlanWithProperties(new UnionNode(node.getId(), partitionedSources.build(), outputToSourcesMapping.build()), ActualProperties.hashPartitioned(hashingColumns)); + } + + private PlanWithProperties planChild(PlanNode node, PreferredProperties preferred) + { + return getOnlyElement(node.getSources()).accept(this, preferred); + } + + private PlanWithProperties rebaseAndDeriveProperties(PlanNode node, PlanWithProperties child) + { + return withDerivedProperties( + ChildReplacer.replaceChildren(node, ImmutableList.of(child.getNode())), + child.getProperties()); + } + + private PlanWithProperties rebaseAndDeriveProperties(PlanNode node, List children) + { + PlanNode result = ChildReplacer.replaceChildren(node, children.stream().map(PlanWithProperties::getNode).collect(toList())); + return new PlanWithProperties(result, deriveProperties(result, children.stream().map(PlanWithProperties::getProperties).collect(toList()))); + } + + private PlanWithProperties withDerivedProperties(PlanNode node, ActualProperties inputProperties) + { + return new PlanWithProperties(node, deriveProperties(node, inputProperties)); + } + + private ActualProperties deriveProperties(PlanNode result, ActualProperties inputProperties) + { + return PropertyDerivations.deriveProperties(result, inputProperties, metadata, session, symbolAllocator.getTypes(), parser); + } + + private ActualProperties deriveProperties(PlanNode result, List inputProperties) + { + return PropertyDerivations.deriveProperties(result, inputProperties, metadata, session, symbolAllocator.getTypes(), parser); + } + } + + private static Map computeIdentityTranslations(Map assignments) + { + Map outputToInput = new HashMap<>(); + for (Map.Entry assignment : assignments.entrySet()) { + if (assignment.getValue() instanceof QualifiedNameReference) { + outputToInput.put(assignment.getKey(), Symbol.fromQualifiedName(((QualifiedNameReference) assignment.getValue()).getName())); + } + } + return outputToInput; + } + + @VisibleForTesting + static Comparator streamingExecutionPreference(PreferredProperties preferred) + { + // Calculating the matches can be a bit expensive, so cache the results between comparisons + LoadingCache>, List>>> matchCache = CacheBuilder.newBuilder() + .build(new CacheLoader>, List>>>() + { + @Override + public List>> load(List> actualProperties) + { + return LocalProperties.match(actualProperties, preferred.getLocalProperties()); + } + }); + + return (actual1, actual2) -> { + List>> matchLayout1 = matchCache.getUnchecked(actual1.getLocalProperties()); + List>> matchLayout2 = matchCache.getUnchecked(actual2.getLocalProperties()); + + return ComparisonChain.start() + .compareTrueFirst(hasLocalOptimization(preferred.getLocalProperties(), matchLayout1), hasLocalOptimization(preferred.getLocalProperties(), matchLayout2)) + .compareTrueFirst(meetsPartitioningRequirements(preferred, actual1), meetsPartitioningRequirements(preferred, actual2)) + .compare(matchLayout1, matchLayout2, matchedLayoutPreference()) + .result(); + }; + } + + private static boolean hasLocalOptimization(List> desiredLayout, List>> matchResult) + { + checkArgument(desiredLayout.size() == matchResult.size()); + if (matchResult.isEmpty()) { + return false; + } + // Optimizations can be applied if the first LocalProperty has been modified in the match in any way + return !matchResult.get(0).equals(Optional.of(desiredLayout.get(0))); + } + + private static boolean meetsPartitioningRequirements(PreferredProperties preferred, ActualProperties actual) + { + if (!preferred.getPartitioningProperties().isPresent()) { + return true; + } + PartitioningPreferences partitioningPreferences = preferred.getPartitioningProperties().get(); + if (!partitioningPreferences.isPartitioned()) { + return !actual.isPartitioned(); + } + if (!partitioningPreferences.getPartitioningColumns().isPresent()) { + return actual.isPartitioned(); + } + return actual.isPartitionedOn(partitioningPreferences.getPartitioningColumns().get()); + } + + // Prefer the match result that satisfied the most requirements + private static Comparator>>> matchedLayoutPreference() + { + return (matchLayout1, matchLayout2) -> { + Iterator>> match1Iterator = matchLayout1.iterator(); + Iterator>> match2Iterator = matchLayout2.iterator(); + while (match1Iterator.hasNext() && match2Iterator.hasNext()) { + Optional> match1 = match1Iterator.next(); + Optional> match2 = match2Iterator.next(); + if (match1.isPresent() && match2.isPresent()) { + return Integer.compare(match1.get().getColumns().size(), match2.get().getColumns().size()); + } + else if (match1.isPresent()) { + return 1; + } + else if (match2.isPresent()) { + return -1; + } + } + checkState(!match1Iterator.hasNext() && !match2Iterator.hasNext()); // Should be the same size + return 0; + }; + } + + @VisibleForTesting + static class PlanWithProperties + { + private final PlanNode node; + private final ActualProperties properties; + + public PlanWithProperties(PlanNode node, ActualProperties properties) + { + this.node = node; + this.properties = properties; + } + + public PlanNode getNode() + { + return node; + } + + public ActualProperties getProperties() + { + return properties; + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/BeginTableWrite.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/BeginTableWrite.java new file mode 100644 index 00000000..a0fbe2c9 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/BeginTableWrite.java @@ -0,0 +1,126 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.optimizations; + +import com.facebook.presto.Session; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.metadata.TableHandle; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.planner.PlanNodeIdAllocator; +import com.facebook.presto.sql.planner.Symbol; +import com.facebook.presto.sql.planner.SymbolAllocator; +import com.facebook.presto.sql.planner.plan.DeleteNode; +import com.facebook.presto.sql.planner.plan.PlanNode; +import com.facebook.presto.sql.planner.plan.PlanRewriter; +import com.facebook.presto.sql.planner.plan.TableCommitNode; +import com.facebook.presto.sql.planner.plan.TableWriterNode; +import com.facebook.presto.sql.planner.plan.TableWriterNode.DeleteHandle; + +import java.util.Map; + +/** + * Major HACK alert!!! + * + * This logic should be invoked on query start, not during planning. At that point, the token + * returned by beginCreate/beginInsert should be handed down to tasks in a mapping separate + * from the plan that links plan nodes to the corresponding token. + */ +public class BeginTableWrite + extends PlanOptimizer +{ + private final Metadata metadata; + + public BeginTableWrite(Metadata metadata) + { + this.metadata = metadata; + } + + @Override + public PlanNode optimize(PlanNode plan, Session session, Map types, SymbolAllocator symbolAllocator, PlanNodeIdAllocator idAllocator) + { + return PlanRewriter.rewriteWith(new Rewriter(session), plan); + } + + private class Rewriter + extends PlanRewriter + { + private final Session session; + + public Rewriter(Session session) + { + this.session = session; + } + + @Override + public PlanNode visitTableWriter(TableWriterNode node, RewriteContext context) + { + // TODO: begin create table or insert in pre-execution step, not here + // Part of the plan should be an Optional> and this + // callback can create the table and abort the table creation if the query fails. + return new TableWriterNode( + node.getId(), + node.getSource().accept(this, context), + createWriterTarget(node.getTarget()), + node.getColumns(), + node.getColumnNames(), + node.getOutputSymbols(), + node.getSampleWeightSymbol()); + } + + @Override + public PlanNode visitDelete(DeleteNode node, RewriteContext context) + { + // TODO: replace handle in scan nodes with this new handle + TableHandle handle = metadata.beginDelete(session, node.getTarget().getHandle()); + return new DeleteNode( + node.getId(), + node.getSource().accept(this, context), + new DeleteHandle(handle), + node.getRowId(), + node.getOutputSymbols()); + } + + @Override + public PlanNode visitTableCommit(TableCommitNode node, RewriteContext context) + { + PlanNode child = node.getSource().accept(this, context); + + TableWriterNode.WriterTarget target; + if (child instanceof TableWriterNode) { + target = ((TableWriterNode) child).getTarget(); + } + else if (child instanceof DeleteNode) { + target = ((DeleteNode) child).getTarget(); + } + else { + throw new IllegalArgumentException("Invalid child for TableCommitNode: " + child.getClass().getSimpleName()); + } + + return new TableCommitNode(node.getId(), child, target, node.getOutputSymbols()); + } + + private TableWriterNode.WriterTarget createWriterTarget(TableWriterNode.WriterTarget target) + { + if (target instanceof TableWriterNode.CreateName) { + TableWriterNode.CreateName create = (TableWriterNode.CreateName) target; + return new TableWriterNode.CreateHandle(metadata.beginCreateTable(session, create.getCatalog(), create.getTableMetadata())); + } + if (target instanceof TableWriterNode.InsertReference) { + TableWriterNode.InsertReference insert = (TableWriterNode.InsertReference) target; + return new TableWriterNode.InsertHandle(metadata.beginInsert(session, insert.getHandle(), insert.getInsertOption())); + } + throw new AssertionError("Unhandled target type: " + target.getClass().getName()); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/CanonicalizeExpressions.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/CanonicalizeExpressions.java new file mode 100644 index 00000000..82439d94 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/CanonicalizeExpressions.java @@ -0,0 +1,195 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.optimizations; + +import com.facebook.presto.Session; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.planner.PlanNodeIdAllocator; +import com.facebook.presto.sql.planner.Symbol; +import com.facebook.presto.sql.planner.SymbolAllocator; +import com.facebook.presto.sql.planner.plan.FilterNode; +import com.facebook.presto.sql.planner.plan.PlanNode; +import com.facebook.presto.sql.planner.plan.PlanRewriter; +import com.facebook.presto.sql.planner.plan.ProjectNode; +import com.facebook.presto.sql.planner.plan.TableScanNode; +import com.facebook.presto.sql.tree.BooleanLiteral; +import com.facebook.presto.sql.tree.CurrentTime; +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.ExpressionRewriter; +import com.facebook.presto.sql.tree.ExpressionTreeRewriter; +import com.facebook.presto.sql.tree.Extract; +import com.facebook.presto.sql.tree.FunctionCall; +import com.facebook.presto.sql.tree.IfExpression; +import com.facebook.presto.sql.tree.IsNotNullPredicate; +import com.facebook.presto.sql.tree.IsNullPredicate; +import com.facebook.presto.sql.tree.NotExpression; +import com.facebook.presto.sql.tree.QualifiedName; +import com.facebook.presto.sql.tree.SearchedCaseExpression; +import com.facebook.presto.sql.tree.WhenClause; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; + +import java.util.Map; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class CanonicalizeExpressions + extends PlanOptimizer +{ + public static Expression canonicalizeExpression(Expression expression) + { + return ExpressionTreeRewriter.rewriteWith(new CanonicalizeExpressionRewriter(), expression); + } + + @Override + public PlanNode optimize(PlanNode plan, Session session, Map types, SymbolAllocator symbolAllocator, PlanNodeIdAllocator idAllocator) + { + checkNotNull(plan, "plan is null"); + checkNotNull(session, "session is null"); + checkNotNull(types, "types is null"); + checkNotNull(symbolAllocator, "symbolAllocator is null"); + checkNotNull(idAllocator, "idAllocator is null"); + + return PlanRewriter.rewriteWith(new Rewriter(), plan); + } + + private static class Rewriter + extends PlanRewriter + { + @Override + public PlanNode visitProject(ProjectNode node, RewriteContext context) + { + PlanNode source = context.rewrite(node.getSource()); + Map assignments = ImmutableMap.copyOf(Maps.transformValues(node.getAssignments(), CanonicalizeExpressions::canonicalizeExpression)); + return new ProjectNode(node.getId(), source, assignments); + } + + @Override + public PlanNode visitFilter(FilterNode node, RewriteContext context) + { + PlanNode source = context.rewrite(node.getSource()); + Expression canonicalized = canonicalizeExpression(node.getPredicate()); + if (canonicalized.equals(BooleanLiteral.TRUE_LITERAL)) { + return source; + } + return new FilterNode(node.getId(), source, canonicalized); + } + + @Override + public PlanNode visitTableScan(TableScanNode node, RewriteContext context) + { + Expression originalConstraint = null; + if (node.getOriginalConstraint() != null) { + originalConstraint = canonicalizeExpression(node.getOriginalConstraint()); + } + return new TableScanNode( + node.getId(), + node.getTable(), + node.getOutputSymbols(), + node.getAssignments(), + node.getLayout(), + node.getCurrentConstraint(), + originalConstraint); + } + } + + private static class CanonicalizeExpressionRewriter + extends ExpressionRewriter + { + @Override + public Expression rewriteIsNotNullPredicate(IsNotNullPredicate node, Void context, ExpressionTreeRewriter treeRewriter) + { + Expression value = treeRewriter.rewrite(node.getValue(), context); + return new NotExpression(new IsNullPredicate(value)); + } + + @Override + public Expression rewriteIfExpression(IfExpression node, Void context, ExpressionTreeRewriter treeRewriter) + { + Expression condition = treeRewriter.rewrite(node.getCondition(), context); + Expression trueValue = treeRewriter.rewrite(node.getTrueValue(), context); + + Optional falseValue = node.getFalseValue() + .map((value) -> treeRewriter.rewrite(value, context)); + + return new SearchedCaseExpression(ImmutableList.of(new WhenClause(condition, trueValue)), falseValue); + } + + @Override + public Expression rewriteCurrentTime(CurrentTime node, Void context, ExpressionTreeRewriter treeRewriter) + { + if (node.getPrecision() != null) { + throw new UnsupportedOperationException("not yet implemented: non-default precision"); + } + + switch (node.getType()) { + case DATE: + return new FunctionCall(new QualifiedName("current_date"), ImmutableList.of()); + case TIME: + return new FunctionCall(new QualifiedName("current_time"), ImmutableList.of()); + case LOCALTIME: + return new FunctionCall(new QualifiedName("localtime"), ImmutableList.of()); + case TIMESTAMP: + return new FunctionCall(new QualifiedName("current_timestamp"), ImmutableList.of()); + case LOCALTIMESTAMP: + return new FunctionCall(new QualifiedName("localtimestamp"), ImmutableList.of()); + default: + throw new UnsupportedOperationException("not yet implemented: " + node.getType()); + } + } + + @Override + public Expression rewriteExtract(Extract node, Void context, ExpressionTreeRewriter treeRewriter) + { + Expression value = treeRewriter.rewrite(node.getExpression(), context); + + switch (node.getField()) { + case YEAR: + return new FunctionCall(new QualifiedName("year"), ImmutableList.of(value)); + case QUARTER: + return new FunctionCall(new QualifiedName("quarter"), ImmutableList.of(value)); + case MONTH: + return new FunctionCall(new QualifiedName("month"), ImmutableList.of(value)); + case WEEK: + return new FunctionCall(new QualifiedName("week"), ImmutableList.of(value)); + case DAY: + case DAY_OF_MONTH: + return new FunctionCall(new QualifiedName("day"), ImmutableList.of(value)); + case DAY_OF_WEEK: + case DOW: + return new FunctionCall(new QualifiedName("day_of_week"), ImmutableList.of(value)); + case DAY_OF_YEAR: + case DOY: + return new FunctionCall(new QualifiedName("day_of_year"), ImmutableList.of(value)); + case YEAR_OF_WEEK: + case YOW: + return new FunctionCall(new QualifiedName("year_of_week"), ImmutableList.of(value)); + case HOUR: + return new FunctionCall(new QualifiedName("hour"), ImmutableList.of(value)); + case MINUTE: + return new FunctionCall(new QualifiedName("minute"), ImmutableList.of(value)); + case SECOND: + return new FunctionCall(new QualifiedName("second"), ImmutableList.of(value)); + case TIMEZONE_MINUTE: + return new FunctionCall(new QualifiedName("timezone_minute"), ImmutableList.of(value)); + case TIMEZONE_HOUR: + return new FunctionCall(new QualifiedName("timezone_hour"), ImmutableList.of(value)); + } + + throw new UnsupportedOperationException("not yet implemented: " + node.getField()); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/CountConstantOptimizer.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/CountConstantOptimizer.java new file mode 100644 index 00000000..f5a2ff53 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/CountConstantOptimizer.java @@ -0,0 +1,116 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.optimizations; + +import com.facebook.presto.Session; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.planner.PlanNodeIdAllocator; +import com.facebook.presto.sql.planner.Symbol; +import com.facebook.presto.sql.planner.SymbolAllocator; +import com.facebook.presto.sql.planner.plan.AggregationNode; +import com.facebook.presto.sql.planner.plan.PlanNode; +import com.facebook.presto.sql.planner.plan.PlanRewriter; +import com.facebook.presto.sql.planner.plan.ProjectNode; +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.FunctionCall; +import com.facebook.presto.sql.tree.Literal; +import com.facebook.presto.sql.tree.NullLiteral; +import com.facebook.presto.sql.tree.QualifiedName; +import com.facebook.presto.sql.tree.QualifiedNameReference; +import com.google.common.collect.ImmutableList; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class CountConstantOptimizer + extends PlanOptimizer +{ + @Override + public PlanNode optimize(PlanNode plan, Session session, Map types, SymbolAllocator symbolAllocator, PlanNodeIdAllocator idAllocator) + { + checkNotNull(plan, "plan is null"); + checkNotNull(session, "session is null"); + checkNotNull(types, "types is null"); + checkNotNull(symbolAllocator, "symbolAllocator is null"); + checkNotNull(idAllocator, "idAllocator is null"); + + return PlanRewriter.rewriteWith(new Rewriter(), plan); + } + + private static class Rewriter + extends PlanRewriter + { + @Override + public PlanNode visitAggregation(AggregationNode node, RewriteContext context) + { + Map aggregations = new LinkedHashMap<>(node.getAggregations()); + Map functions = new LinkedHashMap<>(node.getFunctions()); + + PlanNode source = context.rewrite(node.getSource()); + if (source instanceof ProjectNode) { + ProjectNode projectNode = (ProjectNode) source; + for (Entry entry : node.getAggregations().entrySet()) { + Symbol symbol = entry.getKey(); + FunctionCall functionCall = entry.getValue(); + Signature signature = node.getFunctions().get(symbol); + if (isCountConstant(projectNode, functionCall, signature)) { + aggregations.put(symbol, new FunctionCall(functionCall.getName(), null, functionCall.isDistinct(), ImmutableList.of())); + functions.put(symbol, new Signature("count", StandardTypes.BIGINT)); + } + } + } + + return new AggregationNode( + node.getId(), + source, + node.getGroupBy(), + aggregations, + functions, + node.getMasks(), + node.getStep(), + node.getSampleWeight(), + node.getConfidence(), + node.getHashSymbol()); + } + + public static boolean isCountConstant(ProjectNode projectNode, FunctionCall functionCall, Signature signature) + { + if (!"count".equals(signature.getName()) || + signature.getArgumentTypes().size() != 1 || + !signature.getReturnType().equals(StandardTypes.BIGINT)) { + return false; + } + + Expression argument = functionCall.getArguments().get(0); + if (argument instanceof Literal) { + return true; + } + + if (argument instanceof QualifiedNameReference) { + QualifiedNameReference qualifiedNameReference = (QualifiedNameReference) argument; + QualifiedName qualifiedName = qualifiedNameReference.getName(); + Symbol argumentSymbol = Symbol.fromQualifiedName(qualifiedName); + Expression argumentExpression = projectNode.getAssignments().get(argumentSymbol); + return (argumentExpression instanceof Literal) && (!(argumentExpression instanceof NullLiteral)); + } + + return false; + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/HashGenerationOptimizer.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/HashGenerationOptimizer.java new file mode 100644 index 00000000..6de210bf --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/HashGenerationOptimizer.java @@ -0,0 +1,330 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.optimizations; + +import com.facebook.presto.Session; +import com.facebook.presto.SystemSessionProperties; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.planner.PlanNodeIdAllocator; +import com.facebook.presto.sql.planner.Symbol; +import com.facebook.presto.sql.planner.SymbolAllocator; +import com.facebook.presto.sql.planner.plan.AggregationNode; +import com.facebook.presto.sql.planner.plan.DistinctLimitNode; +import com.facebook.presto.sql.planner.plan.IndexJoinNode; +import com.facebook.presto.sql.planner.plan.JoinNode; +import com.facebook.presto.sql.planner.plan.MarkDistinctNode; +import com.facebook.presto.sql.planner.plan.PlanNode; +import com.facebook.presto.sql.planner.plan.PlanRewriter; +import com.facebook.presto.sql.planner.plan.ProjectNode; +import com.facebook.presto.sql.planner.plan.RowNumberNode; +import com.facebook.presto.sql.planner.plan.SemiJoinNode; +import com.facebook.presto.sql.planner.plan.TopNRowNumberNode; +import com.facebook.presto.sql.planner.plan.WindowNode; +import com.facebook.presto.sql.tree.CoalesceExpression; +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.FunctionCall; +import com.facebook.presto.sql.tree.LongLiteral; +import com.facebook.presto.sql.tree.QualifiedName; +import com.facebook.presto.sql.tree.QualifiedNameReference; +import com.facebook.presto.sql.tree.Window; +import com.facebook.presto.type.TypeUtils; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class HashGenerationOptimizer + extends PlanOptimizer +{ + public static final int INITIAL_HASH_VALUE = 0; + private static final String HASH_CODE = FunctionRegistry.mangleOperatorName("HASH_CODE"); + private final boolean optimizeHashGeneration; + + public HashGenerationOptimizer(boolean optimizeHashGeneration) + { + this.optimizeHashGeneration = optimizeHashGeneration; + } + + @Override + public PlanNode optimize(PlanNode plan, Session session, Map types, SymbolAllocator symbolAllocator, PlanNodeIdAllocator idAllocator) + { + checkNotNull(plan, "plan is null"); + checkNotNull(session, "session is null"); + checkNotNull(types, "types is null"); + checkNotNull(symbolAllocator, "symbolAllocator is null"); + checkNotNull(idAllocator, "idAllocator is null"); + if (SystemSessionProperties.isOptimizeHashGenerationEnabled(session, optimizeHashGeneration)) { + return PlanRewriter.rewriteWith(new Rewriter(idAllocator, symbolAllocator), plan, null); + } + return plan; + } + + private static class Rewriter + extends PlanRewriter + { + private final PlanNodeIdAllocator idAllocator; + private final SymbolAllocator symbolAllocator; + + private Rewriter(PlanNodeIdAllocator idAllocator, SymbolAllocator symbolAllocator) + { + this.idAllocator = checkNotNull(idAllocator, "idAllocator is null"); + this.symbolAllocator = checkNotNull(symbolAllocator, "symbolAllocator is null"); + } + + @Override + public PlanNode visitAggregation(AggregationNode node, RewriteContext context) + { + PlanNode rewrittenSource = context.rewrite(node.getSource(), null); + if (rewrittenSource == node.getSource() && node.getGroupBy().isEmpty()) { + return node; + } + if (node.getGroupBy().isEmpty()) { + return new AggregationNode(idAllocator.getNextId(), + rewrittenSource, + node.getGroupBy(), + node.getAggregations(), + node.getFunctions(), + node.getMasks(), + node.getStep(), + node.getSampleWeight(), + node.getConfidence(), + Optional.empty()); + } + + Symbol hashSymbol = symbolAllocator.newHashSymbol(); + PlanNode hashProjectNode = getHashProjectNode(idAllocator, rewrittenSource, hashSymbol, node.getGroupBy()); + return new AggregationNode(idAllocator.getNextId(), + hashProjectNode, + node.getGroupBy(), + node.getAggregations(), + node.getFunctions(), + node.getMasks(), + node.getStep(), + node.getSampleWeight(), + node.getConfidence(), + Optional.of(hashSymbol)); + } + + @Override + public PlanNode visitDistinctLimit(DistinctLimitNode node, RewriteContext context) + { + PlanNode rewrittenSource = context.rewrite(node.getSource(), null); + Symbol hashSymbol = symbolAllocator.newHashSymbol(); + PlanNode hashProjectNode = getHashProjectNode(idAllocator, rewrittenSource, hashSymbol, node.getOutputSymbols()); + return new DistinctLimitNode(idAllocator.getNextId(), hashProjectNode, node.getLimit(), Optional.of(hashSymbol)); + } + + @Override + public PlanNode visitMarkDistinct(MarkDistinctNode node, RewriteContext context) + { + PlanNode rewrittenSource = context.rewrite(node.getSource(), null); + Symbol hashSymbol = symbolAllocator.newHashSymbol(); + PlanNode hashProjectNode = getHashProjectNode(idAllocator, rewrittenSource, hashSymbol, node.getDistinctSymbols()); + return new MarkDistinctNode(idAllocator.getNextId(), hashProjectNode, node.getMarkerSymbol(), node.getDistinctSymbols(), Optional.of(hashSymbol)); + } + + @Override + public PlanNode visitRowNumber(RowNumberNode node, RewriteContext context) + { + PlanNode rewrittenSource = context.rewrite(node.getSource(), null); + if (rewrittenSource == node.getSource() && node.getPartitionBy().isEmpty()) { + return node; + } + + if (!node.getPartitionBy().isEmpty()) { + Symbol hashSymbol = symbolAllocator.newHashSymbol(); + PlanNode hashProjectNode = getHashProjectNode(idAllocator, rewrittenSource, hashSymbol, node.getPartitionBy()); + return new RowNumberNode(idAllocator.getNextId(), hashProjectNode, node.getPartitionBy(), node.getRowNumberSymbol(), node.getMaxRowCountPerPartition(), Optional.of(hashSymbol)); + } + return new RowNumberNode(idAllocator.getNextId(), rewrittenSource, node.getPartitionBy(), node.getRowNumberSymbol(), node.getMaxRowCountPerPartition(), node.getHashSymbol()); + } + + @Override + public PlanNode visitTopNRowNumber(TopNRowNumberNode node, RewriteContext context) + { + PlanNode rewrittenSource = context.rewrite(node.getSource(), null); + if (rewrittenSource == node.getSource() && node.getPartitionBy().isEmpty()) { + return node; + } + + if (node.getPartitionBy().isEmpty()) { + return new TopNRowNumberNode(idAllocator.getNextId(), + rewrittenSource, + node.getPartitionBy(), + node.getOrderBy(), + node.getOrderings(), + node.getRowNumberSymbol(), + node.getMaxRowCountPerPartition(), + node.isPartial(), + node.getHashSymbol()); + } + Symbol hashSymbol = symbolAllocator.newHashSymbol(); + PlanNode hashProjectNode = getHashProjectNode(idAllocator, rewrittenSource, hashSymbol, node.getPartitionBy()); + return new TopNRowNumberNode(idAllocator.getNextId(), + hashProjectNode, + node.getPartitionBy(), + node.getOrderBy(), + node.getOrderings(), + node.getRowNumberSymbol(), + node.getMaxRowCountPerPartition(), + node.isPartial(), + Optional.of(hashSymbol)); + } + + @Override + public PlanNode visitJoin(JoinNode node, RewriteContext context) + { + List clauses = node.getCriteria(); + + List leftSymbols = Lists.transform(clauses, JoinNode.EquiJoinClause::getLeft); + List rightSymbols = Lists.transform(clauses, JoinNode.EquiJoinClause::getRight); + + PlanNode rewrittenLeft = context.rewrite(node.getLeft(), null); + PlanNode rewrittenRight = context.rewrite(node.getRight(), null); + + Symbol leftHashSymbol = symbolAllocator.newHashSymbol(); + Symbol rightHashSymbol = symbolAllocator.newHashSymbol(); + + PlanNode leftHashProjectNode = getHashProjectNode(idAllocator, rewrittenLeft, leftHashSymbol, leftSymbols); + PlanNode rightHashProjectNode = getHashProjectNode(idAllocator, rewrittenRight, rightHashSymbol, rightSymbols); + + return new JoinNode(idAllocator.getNextId(), node.getType(), leftHashProjectNode, rightHashProjectNode, node.getCriteria(), Optional.of(leftHashSymbol), Optional.of(rightHashSymbol)); + } + + @Override + public PlanNode visitSemiJoin(SemiJoinNode node, RewriteContext context) + { + PlanNode rewrittenSource = context.rewrite(node.getSource(), null); + PlanNode rewrittenFilteringSource = context.rewrite(node.getFilteringSource(), null); + + Symbol sourceHashSymbol = symbolAllocator.newHashSymbol(); + Symbol filteringSourceHashSymbol = symbolAllocator.newHashSymbol(); + + PlanNode sourceHashProjectNode = getHashProjectNode(idAllocator, rewrittenSource, sourceHashSymbol, ImmutableList.of(node.getSourceJoinSymbol())); + PlanNode filteringSourceHashProjectNode = getHashProjectNode(idAllocator, rewrittenFilteringSource, filteringSourceHashSymbol, ImmutableList.of(node.getFilteringSourceJoinSymbol())); + + return new SemiJoinNode(idAllocator.getNextId(), + sourceHashProjectNode, + filteringSourceHashProjectNode, + node.getSourceJoinSymbol(), + node.getFilteringSourceJoinSymbol(), + node.getSemiJoinOutput(), + Optional.of(sourceHashSymbol), + Optional.of(filteringSourceHashSymbol)); + } + + @Override + public PlanNode visitIndexJoin(IndexJoinNode node, RewriteContext context) + { + PlanNode rewrittenIndex = context.rewrite(node.getIndexSource(), null); + PlanNode rewrittenProbe = context.rewrite(node.getProbeSource(), null); + + Symbol indexHashSymbol = symbolAllocator.newHashSymbol(); + Symbol probeHashSymbol = symbolAllocator.newHashSymbol(); + + List clauses = node.getCriteria(); + + List indexSymbols = Lists.transform(clauses, IndexJoinNode.EquiJoinClause::getIndex); + List probeSymbols = Lists.transform(clauses, IndexJoinNode.EquiJoinClause::getProbe); + + PlanNode indexHashProjectNode = getHashProjectNode(idAllocator, rewrittenIndex, indexHashSymbol, indexSymbols); + PlanNode probeHashProjectNode = getHashProjectNode(idAllocator, rewrittenProbe, probeHashSymbol, probeSymbols); + + return new IndexJoinNode(idAllocator.getNextId(), + node.getType(), + probeHashProjectNode, + indexHashProjectNode, + node.getCriteria(), + Optional.of(probeHashSymbol), + Optional.of(indexHashSymbol)); + } + + @Override + public PlanNode visitWindow(WindowNode node, RewriteContext context) + { + PlanNode rewrittenSource = context.rewrite(node.getSource(), null); + if (rewrittenSource == node.getSource() && node.getPartitionBy().isEmpty()) { + return node; + } + if (node.getPartitionBy().isEmpty()) { + return new WindowNode(idAllocator.getNextId(), + rewrittenSource, + node.getPartitionBy(), + node.getOrderBy(), + node.getOrderings(), + node.getFrame(), + node.getWindowFunctions(), + node.getSignatures(), + Optional.empty(), + node.getPrePartitionedInputs(), + node.getPreSortedOrderPrefix()); + } + Symbol hashSymbol = symbolAllocator.newHashSymbol(); + PlanNode hashProjectNode = getHashProjectNode(idAllocator, rewrittenSource, hashSymbol, node.getPartitionBy()); + return new WindowNode(idAllocator.getNextId(), + hashProjectNode, + node.getPartitionBy(), + node.getOrderBy(), + node.getOrderings(), + node.getFrame(), + node.getWindowFunctions(), + node.getSignatures(), + Optional.of(hashSymbol), + node.getPrePartitionedInputs(), + node.getPreSortedOrderPrefix()); + } + } + + private static ProjectNode getHashProjectNode(PlanNodeIdAllocator idAllocator, PlanNode source, Symbol hashSymbol, List partitioningSymbols) + { + checkArgument(!partitioningSymbols.isEmpty(), "partitioningSymbols is empty"); + ImmutableMap.Builder outputSymbols = ImmutableMap.builder(); + for (Symbol symbol : source.getOutputSymbols()) { + Expression expression = new QualifiedNameReference(symbol.toQualifiedName()); + outputSymbols.put(symbol, expression); + } + + Expression hashExpression = getHashExpression(partitioningSymbols); + outputSymbols.put(hashSymbol, hashExpression); + return new ProjectNode(idAllocator.getNextId(), source, outputSymbols.build()); + } + + private static Expression getHashExpression(List partitioningSymbols) + { + Expression hashExpression = new LongLiteral(String.valueOf(INITIAL_HASH_VALUE)); + for (Symbol symbol : partitioningSymbols) { + hashExpression = getHashFunctionCall(hashExpression, symbol); + } + return hashExpression; + } + + private static Expression getHashFunctionCall(Expression previousHashValue, Symbol symbol) + { + FunctionCall functionCall = new FunctionCall(QualifiedName.of(HASH_CODE), Optional.empty(), false, ImmutableList.of(new QualifiedNameReference(symbol.toQualifiedName()))); + List arguments = ImmutableList.of(previousHashValue, orNullHashCode(functionCall)); + return new FunctionCall(QualifiedName.of("combine_hash"), arguments); + } + + private static Expression orNullHashCode(Expression expression) + { + return new CoalesceExpression(expression, new LongLiteral(String.valueOf(TypeUtils.NULL_HASH_CODE))); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/ImplementSampleAsFilter.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/ImplementSampleAsFilter.java new file mode 100644 index 00000000..d112572a --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/ImplementSampleAsFilter.java @@ -0,0 +1,73 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.optimizations; + +import com.facebook.presto.Session; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.planner.PlanNodeIdAllocator; +import com.facebook.presto.sql.planner.Symbol; +import com.facebook.presto.sql.planner.SymbolAllocator; +import com.facebook.presto.sql.planner.plan.FilterNode; +import com.facebook.presto.sql.planner.plan.PlanNode; +import com.facebook.presto.sql.planner.plan.PlanRewriter; +import com.facebook.presto.sql.planner.plan.SampleNode; +import com.facebook.presto.sql.tree.ComparisonExpression; +import com.facebook.presto.sql.tree.DoubleLiteral; +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.FunctionCall; +import com.facebook.presto.sql.tree.QualifiedName; +import com.google.common.collect.ImmutableList; + +import java.util.Map; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class ImplementSampleAsFilter + extends PlanOptimizer +{ + @Override + public PlanNode optimize(PlanNode plan, Session session, Map types, SymbolAllocator symbolAllocator, PlanNodeIdAllocator idAllocator) + { + checkNotNull(plan, "plan is null"); + checkNotNull(session, "session is null"); + checkNotNull(types, "types is null"); + checkNotNull(symbolAllocator, "symbolAllocator is null"); + checkNotNull(idAllocator, "idAllocator is null"); + + return PlanRewriter.rewriteWith(new Rewriter(), plan, null); + } + + private static class Rewriter + extends PlanRewriter + { + @Override + public PlanNode visitSample(SampleNode node, RewriteContext context) + { + if (node.getSampleType() == SampleNode.Type.BERNOULLI) { + PlanNode rewrittenSource = context.rewrite(node.getSource()); + + ComparisonExpression expression = new ComparisonExpression( + ComparisonExpression.Type.LESS_THAN, + new FunctionCall(QualifiedName.of("rand"), ImmutableList.of()), + new DoubleLiteral(Double.toString(node.getSampleRatio()))); + return new FilterNode(node.getId(), rewrittenSource, expression); + } + else if (node.getSampleType() == SampleNode.Type.POISSONIZED || + node.getSampleType() == SampleNode.Type.SYSTEM) { + return context.defaultRewrite(node); + } + throw new UnsupportedOperationException("not yet implemented"); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/IndexJoinOptimizer.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/IndexJoinOptimizer.java new file mode 100644 index 00000000..6272bc1a --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/IndexJoinOptimizer.java @@ -0,0 +1,488 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.optimizations; + +import com.facebook.presto.Session; +import com.facebook.presto.index.IndexManager; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.metadata.ResolvedIndex; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.TupleDomain; +import com.facebook.presto.sql.planner.DomainTranslator; +import com.facebook.presto.sql.planner.PlanNodeIdAllocator; +import com.facebook.presto.sql.planner.Symbol; +import com.facebook.presto.sql.planner.SymbolAllocator; +import com.facebook.presto.sql.planner.plan.AggregationNode; +import com.facebook.presto.sql.planner.plan.FilterNode; +import com.facebook.presto.sql.planner.plan.IndexJoinNode; +import com.facebook.presto.sql.planner.plan.IndexSourceNode; +import com.facebook.presto.sql.planner.plan.JoinNode; +import com.facebook.presto.sql.planner.plan.PlanNode; +import com.facebook.presto.sql.planner.plan.PlanRewriter; +import com.facebook.presto.sql.planner.plan.PlanVisitor; +import com.facebook.presto.sql.planner.plan.ProjectNode; +import com.facebook.presto.sql.planner.plan.SortNode; +import com.facebook.presto.sql.planner.plan.TableScanNode; +import com.facebook.presto.sql.tree.BooleanLiteral; +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.QualifiedNameReference; +import com.google.common.base.Functions; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableBiMap; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +import static com.facebook.presto.sql.ExpressionUtils.combineConjuncts; +import static com.facebook.presto.sql.tree.BooleanLiteral.TRUE_LITERAL; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Predicates.in; +import static java.util.Objects.requireNonNull; + +public class IndexJoinOptimizer + extends PlanOptimizer +{ + private final IndexManager indexManager; + private final Metadata metadata; + + public IndexJoinOptimizer(Metadata metadata, IndexManager indexManager) + { + this.metadata = requireNonNull(metadata, "metadata is null"); + this.indexManager = requireNonNull(indexManager, "indexManager is null"); + } + + @Override + public PlanNode optimize(PlanNode plan, Session session, Map types, SymbolAllocator symbolAllocator, PlanNodeIdAllocator idAllocator) + { + checkNotNull(plan, "plan is null"); + checkNotNull(session, "session is null"); + checkNotNull(types, "types is null"); + checkNotNull(symbolAllocator, "symbolAllocator is null"); + checkNotNull(idAllocator, "idAllocator is null"); + + return PlanRewriter.rewriteWith(new Rewriter(symbolAllocator, idAllocator, indexManager, metadata, session), plan, null); + } + + private static class Rewriter + extends PlanRewriter + { + private final IndexManager indexManager; + private final SymbolAllocator symbolAllocator; + private final PlanNodeIdAllocator idAllocator; + private final Metadata metadata; + private final Session session; + + private Rewriter(SymbolAllocator symbolAllocator, PlanNodeIdAllocator idAllocator, IndexManager indexManager, Metadata metadata, Session session) + { + this.symbolAllocator = requireNonNull(symbolAllocator, "symbolAllocator is null"); + this.idAllocator = requireNonNull(idAllocator, "idAllocator is null"); + this.indexManager = requireNonNull(indexManager, "indexManager is null"); + this.metadata = requireNonNull(metadata, "metadata is null"); + this.session = requireNonNull(session, "session is null"); + } + + @Override + public PlanNode visitJoin(JoinNode node, RewriteContext context) + { + PlanNode leftRewritten = context.rewrite(node.getLeft()); + PlanNode rightRewritten = context.rewrite(node.getRight()); + + if (!node.getCriteria().isEmpty()) { // Index join only possible with JOIN criteria + List leftJoinSymbols = Lists.transform(node.getCriteria(), JoinNode.EquiJoinClause::getLeft); + List rightJoinSymbols = Lists.transform(node.getCriteria(), JoinNode.EquiJoinClause::getRight); + + Optional leftIndexCandidate = IndexSourceRewriter.rewriteWithIndex( + leftRewritten, + ImmutableSet.copyOf(leftJoinSymbols), + indexManager, + symbolAllocator, + idAllocator, + metadata, + session); + if (leftIndexCandidate.isPresent()) { + // Sanity check that we can trace the path for the index lookup key + Map trace = IndexKeyTracer.trace(leftIndexCandidate.get(), ImmutableSet.copyOf(leftJoinSymbols)); + checkState(!trace.isEmpty() && leftJoinSymbols.containsAll(trace.keySet())); + } + + Optional rightIndexCandidate = IndexSourceRewriter.rewriteWithIndex( + rightRewritten, + ImmutableSet.copyOf(rightJoinSymbols), + indexManager, + symbolAllocator, + idAllocator, + metadata, + session); + if (rightIndexCandidate.isPresent()) { + // Sanity check that we can trace the path for the index lookup key + Map trace = IndexKeyTracer.trace(rightIndexCandidate.get(), ImmutableSet.copyOf(rightJoinSymbols)); + checkState(!trace.isEmpty() && rightJoinSymbols.containsAll(trace.keySet())); + } + + switch (node.getType()) { + case INNER: + // Prefer the right candidate over the left candidate + if (rightIndexCandidate.isPresent()) { + return new IndexJoinNode(idAllocator.getNextId(), IndexJoinNode.Type.INNER, leftRewritten, rightIndexCandidate.get(), createEquiJoinClause(leftJoinSymbols, rightJoinSymbols), Optional.empty(), Optional.empty()); + } + else if (leftIndexCandidate.isPresent()) { + return new IndexJoinNode(idAllocator.getNextId(), IndexJoinNode.Type.INNER, rightRewritten, leftIndexCandidate.get(), createEquiJoinClause(rightJoinSymbols, leftJoinSymbols), Optional.empty(), Optional.empty()); + } + break; + + case LEFT: + if (rightIndexCandidate.isPresent()) { + return new IndexJoinNode(idAllocator.getNextId(), IndexJoinNode.Type.SOURCE_OUTER, leftRewritten, rightIndexCandidate.get(), createEquiJoinClause(leftJoinSymbols, rightJoinSymbols), Optional.empty(), Optional.empty()); + } + break; + + case RIGHT: + if (leftIndexCandidate.isPresent()) { + return new IndexJoinNode(idAllocator.getNextId(), IndexJoinNode.Type.SOURCE_OUTER, rightRewritten, leftIndexCandidate.get(), createEquiJoinClause(rightJoinSymbols, leftJoinSymbols), Optional.empty(), Optional.empty()); + } + break; + + case FULL: + break; + + default: + throw new IllegalArgumentException("Unknown type: " + node.getType()); + } + } + + if (leftRewritten != node.getLeft() || rightRewritten != node.getRight()) { + return new JoinNode(node.getId(), node.getType(), leftRewritten, rightRewritten, node.getCriteria(), node.getLeftHashSymbol(), node.getRightHashSymbol()); + } + return node; + } + + private static List createEquiJoinClause(List probeSymbols, List indexSymbols) + { + checkArgument(probeSymbols.size() == indexSymbols.size()); + ImmutableList.Builder builder = ImmutableList.builder(); + for (int i = 0; i < probeSymbols.size(); i++) { + builder.add(new IndexJoinNode.EquiJoinClause(probeSymbols.get(i), indexSymbols.get(i))); + } + return builder.build(); + } + } + + private static Symbol referenceToSymbol(Expression expression) + { + checkArgument(expression instanceof QualifiedNameReference); + return Symbol.fromQualifiedName(((QualifiedNameReference) expression).getName()); + } + + /** + * Tries to rewrite a PlanNode tree with an IndexSource instead of a TableScan + */ + private static class IndexSourceRewriter + extends PlanRewriter + { + private final IndexManager indexManager; + private final SymbolAllocator symbolAllocator; + private final PlanNodeIdAllocator idAllocator; + private final Metadata metadata; + private final Session session; + + private IndexSourceRewriter(IndexManager indexManager, SymbolAllocator symbolAllocator, PlanNodeIdAllocator idAllocator, Metadata metadata, Session session) + { + this.metadata = requireNonNull(metadata, "metadata is null"); + this.symbolAllocator = checkNotNull(symbolAllocator, "symbolAllocator is null"); + this.idAllocator = checkNotNull(idAllocator, "idAllocator is null"); + this.indexManager = checkNotNull(indexManager, "indexManager is null"); + this.session = requireNonNull(session, "session is null"); + } + + public static Optional rewriteWithIndex( + PlanNode planNode, + Set lookupSymbols, + IndexManager indexManager, + SymbolAllocator symbolAllocator, + PlanNodeIdAllocator idAllocator, + Metadata metadata, + Session session) + { + AtomicBoolean success = new AtomicBoolean(); + IndexSourceRewriter indexSourceRewriter = new IndexSourceRewriter(indexManager, symbolAllocator, idAllocator, metadata, session); + PlanNode rewritten = PlanRewriter.rewriteWith(indexSourceRewriter, planNode, new Context(lookupSymbols, success)); + if (success.get()) { + return Optional.of(rewritten); + } + return Optional.empty(); + } + + @Override + public PlanNode visitPlan(PlanNode node, RewriteContext context) + { + // We don't know how to process this PlanNode in the context of an IndexJoin, so just give up by returning something + return node; + } + + @Override + public PlanNode visitTableScan(TableScanNode node, RewriteContext context) + { + return planTableScan(node, BooleanLiteral.TRUE_LITERAL, context.get()); + } + + @NotNull + private PlanNode planTableScan(TableScanNode node, Expression predicate, Context context) + { + DomainTranslator.ExtractionResult decomposedPredicate = DomainTranslator.fromPredicate( + metadata, + session, + predicate, + symbolAllocator.getTypes()); + + TupleDomain simplifiedConstraint = decomposedPredicate.getTupleDomain() + .transform(node.getAssignments()::get) + .intersect(node.getCurrentConstraint()); + + checkState(node.getOutputSymbols().containsAll(context.getLookupSymbols())); + + Set lookupColumns = FluentIterable.from(context.getLookupSymbols()) + .transform(Functions.forMap(node.getAssignments())) + .toSet(); + + Optional optionalResolvedIndex = indexManager.resolveIndex(node.getTable(), lookupColumns, simplifiedConstraint); + if (!optionalResolvedIndex.isPresent()) { + // No index available, so give up by returning something + return node; + } + ResolvedIndex resolvedIndex = optionalResolvedIndex.get(); + + Map inverseAssignments = ImmutableBiMap.copyOf(node.getAssignments()).inverse(); + + PlanNode source = new IndexSourceNode( + idAllocator.getNextId(), + resolvedIndex.getIndexHandle(), + node.getTable(), + context.getLookupSymbols(), + node.getOutputSymbols(), + node.getAssignments(), + simplifiedConstraint); + + Expression resultingPredicate = combineConjuncts( + DomainTranslator.toPredicate( + resolvedIndex.getUnresolvedTupleDomain().transform(inverseAssignments::get), + symbolAllocator.getTypes()), + decomposedPredicate.getRemainingExpression()); + + if (!resultingPredicate.equals(TRUE_LITERAL)) { + // todo it is likely we end up with redundant filters here because the predicate push down has already been run... the fix is to run predicate push down again + source = new FilterNode(idAllocator.getNextId(), source, resultingPredicate); + } + context.markSuccess(); + return source; + } + + @Override + public PlanNode visitProject(ProjectNode node, RewriteContext context) + { + // Rewrite the lookup symbols in terms of only the pre-projected symbols that have direct translations + Set newLookupSymbols = FluentIterable.from(context.get().getLookupSymbols()) + .transform(Functions.forMap(node.getAssignments())) + .filter(QualifiedNameReference.class::isInstance) + .transform(IndexJoinOptimizer::referenceToSymbol) + .toSet(); + + if (newLookupSymbols.isEmpty()) { + return node; + } + + return context.defaultRewrite(node, new Context(newLookupSymbols, context.get().getSuccess())); + } + + @Override + public PlanNode visitFilter(FilterNode node, RewriteContext context) + { + if (node.getSource() instanceof TableScanNode) { + return planTableScan((TableScanNode) node.getSource(), node.getPredicate(), context.get()); + } + + return context.defaultRewrite(node, new Context(context.get().getLookupSymbols(), context.get().getSuccess())); + } + + @Override + public PlanNode visitIndexSource(IndexSourceNode node, RewriteContext context) + { + throw new IllegalStateException("Should not be trying to generate an Index on something that has already been determined to use an Index"); + } + + @Override + public PlanNode visitIndexJoin(IndexJoinNode node, RewriteContext context) + { + // Lookup symbols can only be passed through the probe side of an index join + Set probeLookupSymbols = FluentIterable.from(context.get().getLookupSymbols()) + .filter(in(node.getProbeSource().getOutputSymbols())) + .toSet(); + + if (probeLookupSymbols.isEmpty()) { + return node; + } + + PlanNode rewrittenProbeSource = context.rewrite(node.getProbeSource(), new Context(probeLookupSymbols, context.get().getSuccess())); + + PlanNode source = node; + if (rewrittenProbeSource != node.getProbeSource()) { + source = new IndexJoinNode(node.getId(), node.getType(), rewrittenProbeSource, node.getIndexSource(), node.getCriteria(), node.getProbeHashSymbol(), node.getIndexHashSymbol()); + } + + return source; + } + + @Override + public PlanNode visitAggregation(AggregationNode node, RewriteContext context) + { + // Lookup symbols can only be passed through if they are part of the group by columns + Set groupByLookupSymbols = FluentIterable.from(context.get().getLookupSymbols()) + .filter(in(node.getGroupBy())) + .toSet(); + + if (groupByLookupSymbols.isEmpty()) { + return node; + } + + return context.defaultRewrite(node, new Context(groupByLookupSymbols, context.get().getSuccess())); + } + + @Override + public PlanNode visitSort(SortNode node, RewriteContext context) + { + // Sort has no bearing when building an index, so just ignore the sort + return context.rewrite(node.getSource(), context.get()); + } + + public static class Context + { + private final Set lookupSymbols; + private final AtomicBoolean success; + + public Context(Set lookupSymbols, AtomicBoolean success) + { + checkArgument(!lookupSymbols.isEmpty(), "lookupSymbols can not be empty"); + this.lookupSymbols = ImmutableSet.copyOf(checkNotNull(lookupSymbols, "lookupSymbols is null")); + this.success = checkNotNull(success, "success is null"); + } + + public Set getLookupSymbols() + { + return lookupSymbols; + } + + public AtomicBoolean getSuccess() + { + return success; + } + + public void markSuccess() + { + checkState(success.compareAndSet(false, true), "Can only have one success per context"); + } + } + } + + /** + * Identify the mapping from the lookup symbols used at the top of the index plan to + * the actual symbols produced by the IndexSource. Note that multiple top-level lookup symbols may share the same + * underlying IndexSource symbol. Also note that lookup symbols that do not correspond to underlying index source symbols + * will be omitted from the returned Map. + */ + public static class IndexKeyTracer + { + public static Map trace(PlanNode node, Set lookupSymbols) + { + return node.accept(new Visitor(), lookupSymbols); + } + + private static class Visitor + extends PlanVisitor, Map> + { + @Override + protected Map visitPlan(PlanNode node, Set lookupSymbols) + { + throw new UnsupportedOperationException("Node not expected to be part of Index pipeline: " + node); + } + + @Override + public Map visitProject(ProjectNode node, Set lookupSymbols) + { + // Map from output Symbols to source Symbols + Map directSymbolTranslationOutputMap = Maps.transformValues(Maps.filterValues(node.getAssignments(), QualifiedNameReference.class::isInstance), IndexJoinOptimizer::referenceToSymbol); + Map outputToSourceMap = FluentIterable.from(lookupSymbols) + .filter(in(directSymbolTranslationOutputMap.keySet())) + .toMap(Functions.forMap(directSymbolTranslationOutputMap)); + checkState(!outputToSourceMap.isEmpty(), "No lookup symbols were able to pass through the projection"); + + // Map from source Symbols to underlying index source Symbols + Map sourceToIndexMap = node.getSource().accept(this, ImmutableSet.copyOf(outputToSourceMap.values())); + + // Generate the Map the connects lookup symbols to underlying index source symbols + Map outputToIndexMap = Maps.transformValues(Maps.filterValues(outputToSourceMap, in(sourceToIndexMap.keySet())), Functions.forMap(sourceToIndexMap)); + return ImmutableMap.copyOf(outputToIndexMap); + } + + @Override + public Map visitFilter(FilterNode node, Set lookupSymbols) + { + return node.getSource().accept(this, lookupSymbols); + } + + @Override + public Map visitIndexJoin(IndexJoinNode node, Set lookupSymbols) + { + Set probeLookupSymbols = FluentIterable.from(lookupSymbols) + .filter(in(node.getProbeSource().getOutputSymbols())) + .toSet(); + checkState(!probeLookupSymbols.isEmpty(), "No lookup symbols were able to pass through the index join probe source"); + return node.getProbeSource().accept(this, probeLookupSymbols); + } + + @Override + public Map visitAggregation(AggregationNode node, Set lookupSymbols) + { + Set groupByLookupSymbols = FluentIterable.from(lookupSymbols) + .filter(in(node.getGroupBy())) + .toSet(); + checkState(!groupByLookupSymbols.isEmpty(), "No lookup symbols were able to pass through the aggregation group by"); + return node.getSource().accept(this, groupByLookupSymbols); + } + + @Override + public Map visitSort(SortNode node, Set lookupSymbols) + { + return node.getSource().accept(this, lookupSymbols); + } + + @Override + public Map visitIndexSource(IndexSourceNode node, Set lookupSymbols) + { + checkState(node.getLookupSymbols().equals(lookupSymbols), "lookupSymbols must be the same as IndexSource lookup symbols"); + return FluentIterable.from(lookupSymbols) + .toMap(Functions.identity()); + } + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/LimitPushDown.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/LimitPushDown.java new file mode 100644 index 00000000..f8f0c941 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/LimitPushDown.java @@ -0,0 +1,211 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.optimizations; + +import com.facebook.presto.Session; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.planner.PlanNodeIdAllocator; +import com.facebook.presto.sql.planner.Symbol; +import com.facebook.presto.sql.planner.SymbolAllocator; +import com.facebook.presto.sql.planner.plan.AggregationNode; +import com.facebook.presto.sql.planner.plan.DistinctLimitNode; +import com.facebook.presto.sql.planner.plan.LimitNode; +import com.facebook.presto.sql.planner.plan.MarkDistinctNode; +import com.facebook.presto.sql.planner.plan.PlanNode; +import com.facebook.presto.sql.planner.plan.PlanRewriter; +import com.facebook.presto.sql.planner.plan.ProjectNode; +import com.facebook.presto.sql.planner.plan.SemiJoinNode; +import com.facebook.presto.sql.planner.plan.SortNode; +import com.facebook.presto.sql.planner.plan.TopNNode; +import com.facebook.presto.sql.planner.plan.UnionNode; +import com.facebook.presto.sql.planner.plan.ValuesNode; +import com.google.common.collect.ImmutableList; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class LimitPushDown + extends PlanOptimizer +{ + @Override + public PlanNode optimize(PlanNode plan, Session session, Map types, SymbolAllocator symbolAllocator, PlanNodeIdAllocator idAllocator) + { + checkNotNull(plan, "plan is null"); + checkNotNull(session, "session is null"); + checkNotNull(types, "types is null"); + checkNotNull(symbolAllocator, "symbolAllocator is null"); + checkNotNull(idAllocator, "idAllocator is null"); + + return PlanRewriter.rewriteWith(new Rewriter(idAllocator), plan, null); + } + + private static class LimitContext + { + private final long count; + + public LimitContext(long count) + { + this.count = count; + } + + public long getCount() + { + return count; + } + } + + private static class Rewriter + extends PlanRewriter + { + private final PlanNodeIdAllocator idAllocator; + + private Rewriter(PlanNodeIdAllocator idAllocator) + { + this.idAllocator = checkNotNull(idAllocator, "idAllocator is null"); + } + + @Override + public PlanNode visitPlan(PlanNode node, RewriteContext context) + { + PlanNode rewrittenNode = context.defaultRewrite(node); + + LimitContext limit = context.get(); + if (limit != null) { + // Drop in a LimitNode b/c we cannot push our limit down any further + rewrittenNode = new LimitNode(idAllocator.getNextId(), rewrittenNode, limit.getCount()); + } + return rewrittenNode; + } + + @Override + public PlanNode visitLimit(LimitNode node, RewriteContext context) + { + // return empty ValuesNode in case of limit 0 + if (node.getCount() == 0) { + return new ValuesNode(idAllocator.getNextId(), + node.getOutputSymbols(), + ImmutableList.of()); + } + + LimitContext limit = context.get(); + if (limit != null && limit.getCount() < node.getCount()) { + return context.rewrite(node.getSource(), limit); + } + else { + return context.rewrite(node.getSource(), new LimitContext(node.getCount())); + } + } + + @Override + public PlanNode visitAggregation(AggregationNode node, RewriteContext context) + { + LimitContext limit = context.get(); + + if (limit != null && + node.getAggregations().isEmpty() && + node.getOutputSymbols().size() == node.getGroupBy().size() && + node.getOutputSymbols().containsAll(node.getGroupBy())) { + checkArgument(!node.getSampleWeight().isPresent(), "DISTINCT aggregation has sample weight symbol"); + PlanNode rewrittenSource = context.rewrite(node.getSource()); + return new DistinctLimitNode(idAllocator.getNextId(), rewrittenSource, limit.getCount(), Optional.empty()); + } + PlanNode rewrittenNode = context.defaultRewrite(node); + if (limit != null) { + // Drop in a LimitNode b/c limits cannot be pushed through aggregations + rewrittenNode = new LimitNode(idAllocator.getNextId(), rewrittenNode, limit.getCount()); + } + return rewrittenNode; + } + + @Override + public PlanNode visitMarkDistinct(MarkDistinctNode node, RewriteContext context) + { + // the fallback logic (in visitPlan) for node types we don't know about introduces a limit node, + // so we need this here to push the limit through this trivial node type + return context.defaultRewrite(node, context.get()); + } + + @Override + public PlanNode visitProject(ProjectNode node, RewriteContext context) + { + // the fallback logic (in visitPlan) for node types we don't know about introduces a limit node, + // so we need this here to push the limit through this trivial node type + return context.defaultRewrite(node, context.get()); + } + + @Override + public PlanNode visitTopN(TopNNode node, RewriteContext context) + { + LimitContext limit = context.get(); + + PlanNode rewrittenSource = context.rewrite(node.getSource()); + if (rewrittenSource == node.getSource() && limit == null) { + return node; + } + + long count = node.getCount(); + if (limit != null) { + count = Math.min(count, limit.getCount()); + } + return new TopNNode(node.getId(), rewrittenSource, count, node.getOrderBy(), node.getOrderings(), node.isPartial()); + } + + @Override + public PlanNode visitSort(SortNode node, RewriteContext context) + { + LimitContext limit = context.get(); + + PlanNode rewrittenSource = context.rewrite(node.getSource()); + if (limit != null) { + return new TopNNode(node.getId(), rewrittenSource, limit.getCount(), node.getOrderBy(), node.getOrderings(), false); + } + else if (rewrittenSource != node.getSource()) { + return new SortNode(node.getId(), rewrittenSource, node.getOrderBy(), node.getOrderings()); + } + return node; + } + + @Override + public PlanNode visitUnion(UnionNode node, RewriteContext context) + { + LimitContext limit = context.get(); + + List sources = new ArrayList<>(); + for (int i = 0; i < node.getSources().size(); i++) { + sources.add(context.rewrite(node.getSources().get(i), limit)); + } + + PlanNode output = new UnionNode(node.getId(), sources, node.getSymbolMapping()); + if (limit != null) { + output = new LimitNode(idAllocator.getNextId(), output, limit.getCount()); + } + return output; + } + + @Override + public PlanNode visitSemiJoin(SemiJoinNode node, RewriteContext context) + { + PlanNode source = context.rewrite(node.getSource(), context.get()); + if (source != node.getSource()) { + return new SemiJoinNode(node.getId(), source, node.getFilteringSource(), node.getSourceJoinSymbol(), node.getFilteringSourceJoinSymbol(), node.getSemiJoinOutput(), node.getSourceHashSymbol(), node.getFilteringSourceHashSymbol()); + } + return node; + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/LocalProperties.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/LocalProperties.java new file mode 100644 index 00000000..2691ef1f --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/LocalProperties.java @@ -0,0 +1,141 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.optimizations; + +import com.facebook.presto.spi.ConstantProperty; +import com.facebook.presto.spi.GroupingProperty; +import com.facebook.presto.spi.LocalProperty; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.PeekingIterator; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static com.google.common.collect.Iterators.peekingIterator; + +final class LocalProperties +{ + private LocalProperties() + { + } + + public static List> none() + { + return ImmutableList.of(); + } + + public static List> grouped(Collection columns) + { + return ImmutableList.of(new GroupingProperty<>(columns)); + } + + public static List> stripLeadingConstants(List> properties) + { + PeekingIterator> iterator = peekingIterator(properties.iterator()); + while (iterator.hasNext() && iterator.peek() instanceof ConstantProperty) { + iterator.next(); + } + return ImmutableList.copyOf(iterator); + } + + public static Set extractLeadingConstants(List> properties) + { + ImmutableSet.Builder builder = ImmutableSet.builder(); + PeekingIterator> iterator = peekingIterator(properties.iterator()); + while (iterator.hasNext() && iterator.peek() instanceof ConstantProperty) { + builder.add(((ConstantProperty) iterator.next()).getColumn()); + } + return builder.build(); + } + + /** + * Translates the properties as much as possible, and truncates at the first non-translatable property + */ + public static List> translate(List> properties, Function> translator) + { + properties = normalizeAndPrune(properties); + + ImmutableList.Builder> builder = ImmutableList.builder(); + for (LocalProperty property : properties) { + Optional> translated = property.translate(translator); + if (translated.isPresent()) { + builder.add(translated.get()); + } + else if (!(property instanceof ConstantProperty)) { + break; // Only break if we fail to translate non-constants + } + } + + return builder.build(); + } + + /** + * Attempt to match the desired properties to a sequence of known properties. + *

+ * Returns a list of the same length as the original. Entries are: + * - Optional.empty(): the property was satisfied completely + * - non-empty: the (simplified) property that was not satisfied + */ + public static List>> match(List> actuals, List> desired) + { + // After normalizing actuals, each symbol should only appear once + PeekingIterator> actualIterator = peekingIterator(normalizeAndPrune(actuals).iterator()); + + Set constants = new HashSet<>(); + boolean consumeMoreActuals = true; + List>> result = new ArrayList<>(desired.size()); + for (LocalProperty desiredProperty : desired) { + while (consumeMoreActuals && actualIterator.hasNext() && desiredProperty.isSimplifiedBy(actualIterator.peek())) { + constants.addAll(actualIterator.next().getColumns()); + } + Optional> simplifiedDesired = desiredProperty.withConstants(constants); + consumeMoreActuals &= !simplifiedDesired.isPresent(); // Only continue processing actuals if all previous desired properties were fully satisfied + result.add(simplifiedDesired); + } + return result; + } + + /** + * Normalizes the local properties and potentially consolidates it to the smallest possible list + * NOTE: When normalized, each symbol will only appear once + */ + public static List> normalizeAndPrune(List> localProperties) + { + return normalize(localProperties).stream() + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList()); + } + + /** + * Normalizes the local properties by removing redundant symbols, but retains the original local property positions + */ + public static List>> normalize(List> localProperties) + { + List>> normalizedProperties = new ArrayList<>(localProperties.size()); + Set constants = new HashSet<>(); + for (LocalProperty localProperty : localProperties) { + normalizedProperties.add(localProperty.withConstants(constants)); + constants.addAll(localProperty.getColumns()); + } + return normalizedProperties; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/MergeProjections.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/MergeProjections.java new file mode 100644 index 00000000..effc6c02 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/MergeProjections.java @@ -0,0 +1,73 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.optimizations; + +import com.facebook.presto.Session; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.planner.ExpressionSymbolInliner; +import com.facebook.presto.sql.planner.PlanNodeIdAllocator; +import com.facebook.presto.sql.planner.Symbol; +import com.facebook.presto.sql.planner.SymbolAllocator; +import com.facebook.presto.sql.planner.plan.PlanNode; +import com.facebook.presto.sql.planner.plan.PlanRewriter; +import com.facebook.presto.sql.planner.plan.ProjectNode; +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.ExpressionTreeRewriter; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +import java.util.Map; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Merges chains of consecutive projections + */ +public class MergeProjections + extends PlanOptimizer +{ + @Override + public PlanNode optimize(PlanNode plan, Session session, Map types, SymbolAllocator symbolAllocator, PlanNodeIdAllocator idAllocator) + { + checkNotNull(plan, "plan is null"); + checkNotNull(session, "session is null"); + checkNotNull(types, "types is null"); + checkNotNull(symbolAllocator, "symbolAllocator is null"); + checkNotNull(idAllocator, "idAllocator is null"); + + return PlanRewriter.rewriteWith(new Rewriter(), plan); + } + + private static class Rewriter + extends PlanRewriter + { + @Override + public PlanNode visitProject(ProjectNode node, RewriteContext context) + { + PlanNode source = context.rewrite(node.getSource()); + + if (source instanceof ProjectNode) { + ImmutableMap.Builder projections = ImmutableMap.builder(); + for (Map.Entry projection : node.getAssignments().entrySet()) { + Expression inlined = ExpressionTreeRewriter.rewriteWith(new ExpressionSymbolInliner(((ProjectNode) source).getAssignments()), projection.getValue()); + projections.put(projection.getKey(), inlined); + } + + return new ProjectNode(node.getId(), ((ProjectNode) source).getSource(), projections.build()); + } + + return context.replaceChildren(node, ImmutableList.of(source)); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/MetadataQueryOptimizer.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/MetadataQueryOptimizer.java new file mode 100644 index 00000000..5ecbd7f3 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/MetadataQueryOptimizer.java @@ -0,0 +1,219 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.optimizations; + +import com.facebook.presto.Session; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.metadata.TableLayout; +import com.facebook.presto.metadata.TableLayoutResult; +import com.facebook.presto.spi.ColumnMetadata; +import com.facebook.presto.spi.Constraint; +import com.facebook.presto.spi.SerializableNativeValue; +import com.facebook.presto.spi.TupleDomain; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.planner.DeterminismEvaluator; +import com.facebook.presto.sql.planner.LiteralInterpreter; +import com.facebook.presto.sql.planner.PlanNodeIdAllocator; +import com.facebook.presto.sql.planner.Symbol; +import com.facebook.presto.sql.planner.SymbolAllocator; +import com.facebook.presto.sql.planner.plan.AggregationNode; +import com.facebook.presto.sql.planner.plan.FilterNode; +import com.facebook.presto.sql.planner.plan.LimitNode; +import com.facebook.presto.sql.planner.plan.MarkDistinctNode; +import com.facebook.presto.sql.planner.plan.PlanNode; +import com.facebook.presto.sql.planner.plan.PlanRewriter; +import com.facebook.presto.sql.planner.plan.ProjectNode; +import com.facebook.presto.sql.planner.plan.SortNode; +import com.facebook.presto.sql.planner.plan.TableScanNode; +import com.facebook.presto.sql.planner.plan.TopNNode; +import com.facebook.presto.sql.planner.plan.ValuesNode; +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.FunctionCall; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Converts cardinality-insensitive aggregations (max, min, "distinct") over partition keys + * into simple metadata queries + */ +public class MetadataQueryOptimizer + extends PlanOptimizer +{ + private static final Set ALLOWED_FUNCTIONS = ImmutableSet.of("max", "min", "approx_distinct"); + + private final Metadata metadata; + + public MetadataQueryOptimizer(Metadata metadata) + { + checkNotNull(metadata, "metadata is null"); + + this.metadata = metadata; + } + + @Override + public PlanNode optimize(PlanNode plan, Session session, Map types, SymbolAllocator symbolAllocator, PlanNodeIdAllocator idAllocator) + { + return PlanRewriter.rewriteWith(new Optimizer(metadata, idAllocator), plan, null); + } + + private static class Optimizer + extends PlanRewriter + { + private final PlanNodeIdAllocator idAllocator; + private final Metadata metadata; + + private Optimizer(Metadata metadata, PlanNodeIdAllocator idAllocator) + { + this.metadata = metadata; + this.idAllocator = idAllocator; + } + + @Override + public PlanNode visitAggregation(AggregationNode node, RewriteContext context) + { + // supported functions are only MIN/MAX/APPROX_DISTINCT or distinct aggregates + for (FunctionCall call : node.getAggregations().values()) { + if (!ALLOWED_FUNCTIONS.contains(call.getName().toString()) && !call.isDistinct()) { + return context.defaultRewrite(node); + } + } + + Optional result = findTableScan(node.getSource()); + if (!result.isPresent()) { + return context.defaultRewrite(node); + } + + // verify all outputs of table scan are partition keys + TableScanNode tableScan = result.get(); + + ImmutableMap.Builder typesBuilder = ImmutableMap.builder(); + ImmutableMap.Builder columnBuilder = ImmutableMap.builder(); + + List inputs = tableScan.getOutputSymbols(); + for (Symbol symbol : inputs) { + ColumnHandle column = tableScan.getAssignments().get(symbol); + ColumnMetadata columnMetadata = metadata.getColumnMetadata(tableScan.getTable(), column); + + if (!columnMetadata.isPartitionKey()) { + // the optimization is only valid if the aggregation node only + // relies on partition keys + return context.defaultRewrite(node); + } + + typesBuilder.put(symbol, columnMetadata.getType()); + columnBuilder.put(symbol, column); + } + + Map columns = columnBuilder.build(); + Map types = typesBuilder.build(); + + // Materialize the list of partitions and replace the TableScan node + // with a Values node + TableLayout layout = null; + if (!tableScan.getLayout().isPresent()) { + List layouts = metadata.getLayouts(tableScan.getTable(), Constraint.alwaysTrue(), Optional.empty()); + if (layouts.size() == 1) { + layout = Iterables.getOnlyElement(layouts).getLayout(); + } + } + else { + layout = metadata.getLayout(tableScan.getLayout().get()); + } + + if (layout == null || !layout.getDiscretePredicates().isPresent()) { + return context.defaultRewrite(node); + } + + ImmutableList.Builder> rowsBuilder = ImmutableList.builder(); + for (TupleDomain domain : layout.getDiscretePredicates().get()) { + Map entries = domain.extractNullableFixedValues(); + + ImmutableList.Builder rowBuilder = ImmutableList.builder(); + // for each input column, add a literal expression using the entry value + for (Symbol input : inputs) { + ColumnHandle column = columns.get(input); + Type type = types.get(input); + SerializableNativeValue value = entries.get(column); + if (value == null) { + // partition key does not have a single value, so bail out to be safe + return context.defaultRewrite(node); + } + else { + rowBuilder.add(LiteralInterpreter.toExpression(value.getValue(), type)); + } + } + rowsBuilder.add(rowBuilder.build()); + } + + // replace the tablescan node with a values node + ValuesNode valuesNode = new ValuesNode(idAllocator.getNextId(), inputs, rowsBuilder.build()); + return PlanRewriter.rewriteWith(new Replacer(valuesNode), node); + } + + private Optional findTableScan(PlanNode source) + { + while (true) { + // allow any chain of linear transformations + if (source instanceof MarkDistinctNode || + source instanceof FilterNode || + source instanceof LimitNode || + source instanceof TopNNode || + source instanceof SortNode) { + source = source.getSources().get(0); + } + else if (source instanceof ProjectNode) { + // verify projections are deterministic + ProjectNode project = (ProjectNode) source; + if (!Iterables.all(project.getExpressions(), DeterminismEvaluator::isDeterministic)) { + return Optional.empty(); + } + source = project.getSource(); + } + else if (source instanceof TableScanNode) { + return Optional.of((TableScanNode) source); + } + else { + return Optional.empty(); + } + } + } + } + + private static class Replacer + extends PlanRewriter + { + private final ValuesNode replacement; + + private Replacer(ValuesNode replacement) + { + this.replacement = replacement; + } + + @Override + public PlanNode visitTableScan(TableScanNode node, RewriteContext context) + { + return replacement; + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/PickLayout.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/PickLayout.java new file mode 100644 index 00000000..f0fef6dd --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/PickLayout.java @@ -0,0 +1,154 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.optimizations; + +import com.facebook.presto.Session; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.metadata.TableLayoutResult; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.Constraint; +import com.facebook.presto.spi.TupleDomain; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.planner.DomainTranslator; +import com.facebook.presto.sql.planner.PlanNodeIdAllocator; +import com.facebook.presto.sql.planner.Symbol; +import com.facebook.presto.sql.planner.SymbolAllocator; +import com.facebook.presto.sql.planner.plan.FilterNode; +import com.facebook.presto.sql.planner.plan.PlanNode; +import com.facebook.presto.sql.planner.plan.PlanRewriter; +import com.facebook.presto.sql.planner.plan.TableScanNode; +import com.facebook.presto.sql.planner.plan.ValuesNode; +import com.facebook.presto.sql.tree.BooleanLiteral; +import com.facebook.presto.sql.tree.Expression; +import com.google.common.collect.ImmutableBiMap; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static com.facebook.presto.sql.ExpressionUtils.combineConjuncts; +import static java.util.Objects.requireNonNull; + +/** + * Pick an arbitrary layout if none has been chosen + */ +public class PickLayout + extends PlanOptimizer +{ + private final Metadata metadata; + + public PickLayout(Metadata metadata) + { + requireNonNull(metadata, "metadata is null"); + + this.metadata = metadata; + } + + @Override + public PlanNode optimize(PlanNode plan, Session session, Map types, SymbolAllocator symbolAllocator, PlanNodeIdAllocator idAllocator) + { + return PlanRewriter.rewriteWith(new Rewriter(metadata, session, symbolAllocator, idAllocator), plan); + } + + private static class Rewriter + extends PlanRewriter + { + private final Metadata metadata; + private final Session session; + private final SymbolAllocator symbolAllocator; + private final PlanNodeIdAllocator idAllocator; + + public Rewriter(Metadata metadata, Session session, SymbolAllocator symbolAllocator, PlanNodeIdAllocator idAllocator) + { + this.metadata = metadata; + this.session = session; + this.symbolAllocator = symbolAllocator; + this.idAllocator = idAllocator; + } + + @Override + protected PlanNode visitPlan(PlanNode node, RewriteContext context) + { + return context.defaultRewrite(node); + } + + @Override + public PlanNode visitFilter(FilterNode node, RewriteContext context) + { + if (node.getSource() instanceof TableScanNode && !((TableScanNode) node.getSource()).getLayout().isPresent()) { + return planTableScan((TableScanNode) node.getSource(), node.getPredicate()); + } + + return context.defaultRewrite(node); + } + + @Override + public PlanNode visitTableScan(TableScanNode node, RewriteContext context) + { + if (node.getLayout().isPresent()) { + return node; + } + + return planTableScan(node, BooleanLiteral.TRUE_LITERAL); + } + + private PlanNode planTableScan(TableScanNode node, Expression predicate) + { + DomainTranslator.ExtractionResult decomposedPredicate = DomainTranslator.fromPredicate( + metadata, + session, + predicate, + symbolAllocator.getTypes()); + + TupleDomain simplifiedConstraint = decomposedPredicate.getTupleDomain() + .transform(node.getAssignments()::get) + .intersect(node.getCurrentConstraint()); + + List layouts = metadata.getLayouts( + node.getTable(), + new Constraint<>(simplifiedConstraint, bindings -> true), + Optional.of(ImmutableSet.copyOf(node.getAssignments().values()))); + + if (layouts.isEmpty()) { + return new ValuesNode(idAllocator.getNextId(), node.getOutputSymbols(), ImmutableList.of()); + } + + TableLayoutResult layout = layouts.get(0); + + TableScanNode result = new TableScanNode( + node.getId(), + node.getTable(), + node.getOutputSymbols(), + node.getAssignments(), + Optional.of(layout.getLayout().getHandle()), + simplifiedConstraint.intersect(layout.getLayout().getPredicate()), + Optional.ofNullable(node.getOriginalConstraint()).orElse(predicate)); + + Map assignments = ImmutableBiMap.copyOf(node.getAssignments()).inverse(); + Expression resultingPredicate = combineConjuncts( + decomposedPredicate.getRemainingExpression(), + DomainTranslator.toPredicate( + layout.getUnenforcedConstraint().transform(assignments::get), + symbolAllocator.getTypes())); + + if (!BooleanLiteral.TRUE_LITERAL.equals(resultingPredicate)) { + return new FilterNode(idAllocator.getNextId(), result, resultingPredicate); + } + + return result; + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/PlanOptimizer.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/PlanOptimizer.java new file mode 100644 index 00000000..50ad4447 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/PlanOptimizer.java @@ -0,0 +1,32 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.optimizations; + +import com.facebook.presto.Session; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.planner.PlanNodeIdAllocator; +import com.facebook.presto.sql.planner.Symbol; +import com.facebook.presto.sql.planner.SymbolAllocator; +import com.facebook.presto.sql.planner.plan.PlanNode; + +import java.util.Map; + +public abstract class PlanOptimizer +{ + public abstract PlanNode optimize(PlanNode plan, + Session session, + Map types, + SymbolAllocator symbolAllocator, + PlanNodeIdAllocator idAllocator); +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/PredicatePushDown.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/PredicatePushDown.java new file mode 100644 index 00000000..d641db76 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/PredicatePushDown.java @@ -0,0 +1,877 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.optimizations; + +import com.facebook.presto.Session; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.parser.SqlParser; +import com.facebook.presto.sql.planner.DependencyExtractor; +import com.facebook.presto.sql.planner.DeterminismEvaluator; +import com.facebook.presto.sql.planner.EffectivePredicateExtractor; +import com.facebook.presto.sql.planner.EqualityInference; +import com.facebook.presto.sql.planner.ExpressionInterpreter; +import com.facebook.presto.sql.planner.ExpressionSymbolInliner; +import com.facebook.presto.sql.planner.LiteralInterpreter; +import com.facebook.presto.sql.planner.NoOpSymbolResolver; +import com.facebook.presto.sql.planner.PlanNodeIdAllocator; +import com.facebook.presto.sql.planner.Symbol; +import com.facebook.presto.sql.planner.SymbolAllocator; +import com.facebook.presto.sql.planner.plan.AggregationNode; +import com.facebook.presto.sql.planner.plan.ExchangeNode; +import com.facebook.presto.sql.planner.plan.FilterNode; +import com.facebook.presto.sql.planner.plan.JoinNode; +import com.facebook.presto.sql.planner.plan.MarkDistinctNode; +import com.facebook.presto.sql.planner.plan.PlanNode; +import com.facebook.presto.sql.planner.plan.PlanRewriter; +import com.facebook.presto.sql.planner.plan.ProjectNode; +import com.facebook.presto.sql.planner.plan.SampleNode; +import com.facebook.presto.sql.planner.plan.SemiJoinNode; +import com.facebook.presto.sql.planner.plan.SortNode; +import com.facebook.presto.sql.planner.plan.TableScanNode; +import com.facebook.presto.sql.planner.plan.UnionNode; +import com.facebook.presto.sql.planner.plan.UnnestNode; +import com.facebook.presto.sql.planner.plan.ValuesNode; +import com.facebook.presto.sql.tree.BooleanLiteral; +import com.facebook.presto.sql.tree.ComparisonExpression; +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.ExpressionTreeRewriter; +import com.facebook.presto.sql.tree.LongLiteral; +import com.facebook.presto.sql.tree.NullLiteral; +import com.facebook.presto.sql.tree.QualifiedNameReference; +import com.google.common.base.Preconditions; +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import io.airlift.log.Logger; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static com.facebook.presto.sql.ExpressionUtils.and; +import static com.facebook.presto.sql.ExpressionUtils.combineConjuncts; +import static com.facebook.presto.sql.ExpressionUtils.expressionOrNullSymbols; +import static com.facebook.presto.sql.ExpressionUtils.extractConjuncts; +import static com.facebook.presto.sql.ExpressionUtils.stripNonDeterministicConjuncts; +import static com.facebook.presto.sql.analyzer.ExpressionAnalyzer.getExpressionTypes; +import static com.facebook.presto.sql.planner.DeterminismEvaluator.isDeterministic; +import static com.facebook.presto.sql.planner.EqualityInference.createEqualityInference; +import static com.facebook.presto.sql.planner.plan.JoinNode.Type.CROSS; +import static com.facebook.presto.sql.planner.plan.JoinNode.Type.FULL; +import static com.facebook.presto.sql.planner.plan.JoinNode.Type.INNER; +import static com.facebook.presto.sql.planner.plan.JoinNode.Type.LEFT; +import static com.facebook.presto.sql.planner.plan.JoinNode.Type.RIGHT; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Predicates.equalTo; +import static com.google.common.base.Predicates.in; +import static com.google.common.base.Predicates.not; +import static com.google.common.collect.Iterables.filter; +import static com.google.common.collect.Iterables.transform; + +public class PredicatePushDown + extends PlanOptimizer +{ + private static final Logger log = Logger.get(PredicatePushDown.class); + + private final Metadata metadata; + private final SqlParser sqlParser; + + public PredicatePushDown(Metadata metadata, SqlParser sqlParser) + { + this.metadata = checkNotNull(metadata, "metadata is null"); + this.sqlParser = checkNotNull(sqlParser, "sqlParser is null"); + } + + @Override + public PlanNode optimize(PlanNode plan, Session session, Map types, SymbolAllocator symbolAllocator, PlanNodeIdAllocator idAllocator) + { + checkNotNull(plan, "plan is null"); + checkNotNull(session, "session is null"); + checkNotNull(types, "types is null"); + checkNotNull(idAllocator, "idAllocator is null"); + + return PlanRewriter.rewriteWith(new Rewriter(symbolAllocator, idAllocator, metadata, sqlParser, session), plan, BooleanLiteral.TRUE_LITERAL); + } + + private static class Rewriter + extends PlanRewriter + { + private final SymbolAllocator symbolAllocator; + private final PlanNodeIdAllocator idAllocator; + private final Metadata metadata; + private final SqlParser sqlParser; + private final Session session; + + private Rewriter( + SymbolAllocator symbolAllocator, + PlanNodeIdAllocator idAllocator, + Metadata metadata, + SqlParser sqlParser, + Session session) + { + this.symbolAllocator = checkNotNull(symbolAllocator, "symbolAllocator is null"); + this.idAllocator = checkNotNull(idAllocator, "idAllocator is null"); + this.metadata = checkNotNull(metadata, "metadata is null"); + this.sqlParser = checkNotNull(sqlParser, "sqlParser is null"); + this.session = checkNotNull(session, "session is null"); + } + + @Override + public PlanNode visitPlan(PlanNode node, RewriteContext context) + { + PlanNode rewrittenNode = context.defaultRewrite(node, BooleanLiteral.TRUE_LITERAL); + if (!context.get().equals(BooleanLiteral.TRUE_LITERAL)) { + // Drop in a FilterNode b/c we cannot push our predicate down any further + rewrittenNode = new FilterNode(idAllocator.getNextId(), rewrittenNode, context.get()); + } + return rewrittenNode; + } + + @Override + public PlanNode visitExchange(ExchangeNode node, RewriteContext context) + { + boolean modified = false; + ImmutableList.Builder builder = ImmutableList.builder(); + for (int i = 0; i < node.getSources().size(); i++) { + Map outputsToInputs = new HashMap<>(); + for (int index = 0; index < node.getInputs().get(i).size(); index++) { + outputsToInputs.put( + node.getOutputSymbols().get(index), + node.getInputs().get(i).get(index).toQualifiedNameReference()); + } + + Expression sourcePredicate = ExpressionTreeRewriter.rewriteWith(new ExpressionSymbolInliner(outputsToInputs), context.get()); + PlanNode source = node.getSources().get(i); + PlanNode rewrittenSource = context.rewrite(source, sourcePredicate); + if (rewrittenSource != source) { + modified = true; + } + builder.add(rewrittenSource); + } + + if (modified) { + return new ExchangeNode( + node.getId(), + node.getType(), + node.getPartitionKeys(), + node.getHashSymbol(), + builder.build(), + node.getOutputSymbols(), + node.getInputs()); + } + + return node; + } + + @Override + public PlanNode visitProject(ProjectNode node, RewriteContext context) + { + Set deterministicSymbols = node.getAssignments().entrySet().stream() + .filter(entry -> DeterminismEvaluator.isDeterministic(entry.getValue())) + .map(Map.Entry::getKey) + .collect(Collectors.toSet()); + + java.util.function.Predicate deterministic = conjunct -> DependencyExtractor.extractAll(conjunct).stream() + .allMatch(deterministicSymbols::contains); + + Map> conjuncts = extractConjuncts(context.get()).stream().collect(Collectors.partitioningBy(deterministic)); + + // Push down conjuncts from the inherited predicate that don't depend on non-deterministic assignments + PlanNode rewrittenNode = context.defaultRewrite(node, + ExpressionTreeRewriter.rewriteWith(new ExpressionSymbolInliner(node.getAssignments()), combineConjuncts(conjuncts.get(true)))); + + // All non-deterministic conjuncts, if any, will be in the filter node. + if (!conjuncts.get(false).isEmpty()) { + rewrittenNode = new FilterNode(idAllocator.getNextId(), rewrittenNode, combineConjuncts(conjuncts.get(false))); + } + + return rewrittenNode; + } + + @Override + public PlanNode visitMarkDistinct(MarkDistinctNode node, RewriteContext context) + { + checkState(!DependencyExtractor.extractUnique(context.get()).contains(node.getMarkerSymbol()), "predicate depends on marker symbol"); + return context.defaultRewrite(node, context.get()); + } + + @Override + public PlanNode visitSort(SortNode node, RewriteContext context) + { + return context.defaultRewrite(node, context.get()); + } + + @Override + public PlanNode visitUnion(UnionNode node, RewriteContext context) + { + boolean modified = false; + ImmutableList.Builder builder = ImmutableList.builder(); + for (int i = 0; i < node.getSources().size(); i++) { + Expression sourcePredicate = ExpressionTreeRewriter.rewriteWith(new ExpressionSymbolInliner(node.sourceSymbolMap(i)), context.get()); + PlanNode source = node.getSources().get(i); + PlanNode rewrittenSource = context.rewrite(source, sourcePredicate); + if (rewrittenSource != source) { + modified = true; + } + builder.add(rewrittenSource); + } + + if (modified) { + return new UnionNode(node.getId(), builder.build(), node.getSymbolMapping()); + } + + return node; + } + + @Override + public PlanNode visitFilter(FilterNode node, RewriteContext context) + { + return context.rewrite(node.getSource(), combineConjuncts(node.getPredicate(), context.get())); + } + + @Override + public PlanNode visitJoin(JoinNode node, RewriteContext context) + { + Expression inheritedPredicate = context.get(); + + boolean isCrossJoin = (node.getType() == JoinNode.Type.CROSS); + + // See if we can rewrite outer joins in terms of a plain inner join + node = tryNormalizeToInnerJoin(node, inheritedPredicate); + + Expression leftEffectivePredicate = EffectivePredicateExtractor.extract(node.getLeft(), symbolAllocator.getTypes()); + Expression rightEffectivePredicate = EffectivePredicateExtractor.extract(node.getRight(), symbolAllocator.getTypes()); + Expression joinPredicate = extractJoinPredicate(node); + + Expression leftPredicate; + Expression rightPredicate; + Expression postJoinPredicate; + Expression newJoinPredicate; + + switch (node.getType()) { + case INNER: + InnerJoinPushDownResult innerJoinPushDownResult = processInnerJoin(inheritedPredicate, + leftEffectivePredicate, + rightEffectivePredicate, + joinPredicate, + node.getLeft().getOutputSymbols()); + leftPredicate = innerJoinPushDownResult.getLeftPredicate(); + rightPredicate = innerJoinPushDownResult.getRightPredicate(); + postJoinPredicate = innerJoinPushDownResult.getPostJoinPredicate(); + newJoinPredicate = innerJoinPushDownResult.getJoinPredicate(); + break; + case LEFT: + OuterJoinPushDownResult leftOuterJoinPushDownResult = processLimitedOuterJoin(inheritedPredicate, + leftEffectivePredicate, + rightEffectivePredicate, + joinPredicate, + node.getLeft().getOutputSymbols()); + leftPredicate = leftOuterJoinPushDownResult.getOuterJoinPredicate(); + rightPredicate = leftOuterJoinPushDownResult.getInnerJoinPredicate(); + postJoinPredicate = leftOuterJoinPushDownResult.getPostJoinPredicate(); + newJoinPredicate = joinPredicate; // Use the same as the original + break; + case RIGHT: + OuterJoinPushDownResult rightOuterJoinPushDownResult = processLimitedOuterJoin(inheritedPredicate, + rightEffectivePredicate, + leftEffectivePredicate, + joinPredicate, + node.getRight().getOutputSymbols()); + leftPredicate = rightOuterJoinPushDownResult.getInnerJoinPredicate(); + rightPredicate = rightOuterJoinPushDownResult.getOuterJoinPredicate(); + postJoinPredicate = rightOuterJoinPushDownResult.getPostJoinPredicate(); + newJoinPredicate = joinPredicate; // Use the same as the original + break; + case FULL: + leftPredicate = BooleanLiteral.TRUE_LITERAL; + rightPredicate = BooleanLiteral.TRUE_LITERAL; + postJoinPredicate = inheritedPredicate; + newJoinPredicate = joinPredicate; + break; + default: + throw new UnsupportedOperationException("Unsupported join type: " + node.getType()); + } + + PlanNode leftSource = context.rewrite(node.getLeft(), leftPredicate); + PlanNode rightSource = context.rewrite(node.getRight(), rightPredicate); + + PlanNode output = node; + if (leftSource != node.getLeft() || rightSource != node.getRight() || !newJoinPredicate.equals(joinPredicate) || isCrossJoin) { + List criteria = node.getCriteria(); + + // Rewrite criteria and add projections if there is a new join predicate + + if (!newJoinPredicate.equals(joinPredicate) || isCrossJoin) { + // Create identity projections for all existing symbols + ImmutableMap.Builder leftProjections = ImmutableMap.builder(); + + leftProjections.putAll(node.getLeft() + .getOutputSymbols().stream() + .collect(Collectors.toMap(key -> key, Symbol::toQualifiedNameReference))); + + ImmutableMap.Builder rightProjections = ImmutableMap.builder(); + rightProjections.putAll(node.getRight() + .getOutputSymbols().stream() + .collect(Collectors.toMap(key -> key, Symbol::toQualifiedNameReference))); + + // HACK! we don't support cross joins right now, so put in a simple fake join predicate instead if all of the join clauses got simplified out + // TODO: remove this code when cross join support is added + Iterable simplifiedJoinConjuncts = transform(extractConjuncts(newJoinPredicate), this::simplifyExpression); + simplifiedJoinConjuncts = filter(simplifiedJoinConjuncts, not(Predicates.equalTo(BooleanLiteral.TRUE_LITERAL))); + if (Iterables.isEmpty(simplifiedJoinConjuncts)) { + simplifiedJoinConjuncts = ImmutableList.of(new ComparisonExpression(ComparisonExpression.Type.EQUAL, new LongLiteral("0"), new LongLiteral("0"))); + } + + // Create new projections for the new join clauses + ImmutableList.Builder builder = ImmutableList.builder(); + for (Expression conjunct : simplifiedJoinConjuncts) { + checkState(joinEqualityExpression(node.getLeft().getOutputSymbols()).apply(conjunct), "Expected join predicate to be a valid join equality"); + + ComparisonExpression equality = (ComparisonExpression) conjunct; + + boolean alignedComparison = Iterables.all(DependencyExtractor.extractUnique(equality.getLeft()), in(node.getLeft().getOutputSymbols())); + Expression leftExpression = (alignedComparison) ? equality.getLeft() : equality.getRight(); + Expression rightExpression = (alignedComparison) ? equality.getRight() : equality.getLeft(); + + Symbol leftSymbol = symbolAllocator.newSymbol(leftExpression, extractType(leftExpression)); + leftProjections.put(leftSymbol, leftExpression); + Symbol rightSymbol = symbolAllocator.newSymbol(rightExpression, extractType(rightExpression)); + rightProjections.put(rightSymbol, rightExpression); + + builder.add(new JoinNode.EquiJoinClause(leftSymbol, rightSymbol)); + } + + leftSource = new ProjectNode(idAllocator.getNextId(), leftSource, leftProjections.build()); + rightSource = new ProjectNode(idAllocator.getNextId(), rightSource, rightProjections.build()); + criteria = builder.build(); + } + output = new JoinNode(node.getId(), node.getType(), leftSource, rightSource, criteria, node.getLeftHashSymbol(), node.getRightHashSymbol()); + } + if (!postJoinPredicate.equals(BooleanLiteral.TRUE_LITERAL)) { + output = new FilterNode(idAllocator.getNextId(), output, postJoinPredicate); + } + return output; + } + + private OuterJoinPushDownResult processLimitedOuterJoin(Expression inheritedPredicate, Expression outerEffectivePredicate, Expression innerEffectivePredicate, Expression joinPredicate, Collection outerSymbols) + { + checkArgument(Iterables.all(DependencyExtractor.extractUnique(outerEffectivePredicate), in(outerSymbols)), "outerEffectivePredicate must only contain symbols from outerSymbols"); + checkArgument(Iterables.all(DependencyExtractor.extractUnique(innerEffectivePredicate), not(in(outerSymbols))), "innerEffectivePredicate must not contain symbols from outerSymbols"); + + ImmutableList.Builder outerPushdownConjuncts = ImmutableList.builder(); + ImmutableList.Builder innerPushdownConjuncts = ImmutableList.builder(); + ImmutableList.Builder postJoinConjuncts = ImmutableList.builder(); + + // Strip out non-deterministic conjuncts + postJoinConjuncts.addAll(filter(extractConjuncts(inheritedPredicate), not(DeterminismEvaluator::isDeterministic))); + inheritedPredicate = stripNonDeterministicConjuncts(inheritedPredicate); + + outerEffectivePredicate = stripNonDeterministicConjuncts(outerEffectivePredicate); + innerEffectivePredicate = stripNonDeterministicConjuncts(innerEffectivePredicate); + joinPredicate = stripNonDeterministicConjuncts(joinPredicate); + + // Generate equality inferences + EqualityInference inheritedInference = createEqualityInference(inheritedPredicate); + EqualityInference outerInference = createEqualityInference(inheritedPredicate, outerEffectivePredicate); + + EqualityInference.EqualityPartition equalityPartition = inheritedInference.generateEqualitiesPartitionedBy(in(outerSymbols)); + Expression outerOnlyInheritedEqualities = combineConjuncts(equalityPartition.getScopeEqualities()); + EqualityInference potentialNullSymbolInference = createEqualityInference(outerOnlyInheritedEqualities, outerEffectivePredicate, innerEffectivePredicate, joinPredicate); + EqualityInference potentialNullSymbolInferenceWithoutInnerInferred = createEqualityInference(outerOnlyInheritedEqualities, outerEffectivePredicate, joinPredicate); + + // Sort through conjuncts in inheritedPredicate that were not used for inference + for (Expression conjunct : EqualityInference.nonInferrableConjuncts(inheritedPredicate)) { + Expression outerRewritten = outerInference.rewriteExpression(conjunct, in(outerSymbols)); + if (outerRewritten != null) { + outerPushdownConjuncts.add(outerRewritten); + + // A conjunct can only be pushed down into an inner side if it can be rewritten in terms of the outer side + Expression innerRewritten = potentialNullSymbolInference.rewriteExpression(outerRewritten, not(in(outerSymbols))); + if (innerRewritten != null) { + innerPushdownConjuncts.add(innerRewritten); + } + } + else { + postJoinConjuncts.add(conjunct); + } + } + + // See if we can push down any outer or join predicates to the inner side + for (Expression conjunct : EqualityInference.nonInferrableConjuncts(and(outerEffectivePredicate, joinPredicate))) { + Expression rewritten = potentialNullSymbolInference.rewriteExpression(conjunct, not(in(outerSymbols))); + if (rewritten != null) { + innerPushdownConjuncts.add(rewritten); + } + } + + // TODO: consider adding join predicate optimizations to outer joins + + // Add the equalities from the inferences back in + outerPushdownConjuncts.addAll(equalityPartition.getScopeEqualities()); + postJoinConjuncts.addAll(equalityPartition.getScopeComplementEqualities()); + postJoinConjuncts.addAll(equalityPartition.getScopeStraddlingEqualities()); + innerPushdownConjuncts.addAll(potentialNullSymbolInferenceWithoutInnerInferred.generateEqualitiesPartitionedBy(not(in(outerSymbols))).getScopeEqualities()); + + return new OuterJoinPushDownResult(combineConjuncts(outerPushdownConjuncts.build()), + combineConjuncts(innerPushdownConjuncts.build()), + combineConjuncts(postJoinConjuncts.build())); + } + + private static class OuterJoinPushDownResult + { + private final Expression outerJoinPredicate; + private final Expression innerJoinPredicate; + private final Expression postJoinPredicate; + + private OuterJoinPushDownResult(Expression outerJoinPredicate, Expression innerJoinPredicate, Expression postJoinPredicate) + { + this.outerJoinPredicate = outerJoinPredicate; + this.innerJoinPredicate = innerJoinPredicate; + this.postJoinPredicate = postJoinPredicate; + } + + private Expression getOuterJoinPredicate() + { + return outerJoinPredicate; + } + + private Expression getInnerJoinPredicate() + { + return innerJoinPredicate; + } + + private Expression getPostJoinPredicate() + { + return postJoinPredicate; + } + } + + private InnerJoinPushDownResult processInnerJoin(Expression inheritedPredicate, Expression leftEffectivePredicate, Expression rightEffectivePredicate, Expression joinPredicate, Collection leftSymbols) + { + checkArgument(Iterables.all(DependencyExtractor.extractUnique(leftEffectivePredicate), in(leftSymbols)), "leftEffectivePredicate must only contain symbols from leftSymbols"); + checkArgument(Iterables.all(DependencyExtractor.extractUnique(rightEffectivePredicate), not(in(leftSymbols))), "rightEffectivePredicate must not contain symbols from leftSymbols"); + + ImmutableList.Builder leftPushDownConjuncts = ImmutableList.builder(); + ImmutableList.Builder rightPushDownConjuncts = ImmutableList.builder(); + ImmutableList.Builder joinConjuncts = ImmutableList.builder(); + + // Strip out non-deterministic conjuncts + joinConjuncts.addAll(filter(extractConjuncts(inheritedPredicate), not(DeterminismEvaluator::isDeterministic))); + inheritedPredicate = stripNonDeterministicConjuncts(inheritedPredicate); + + joinConjuncts.addAll(filter(extractConjuncts(joinPredicate), not(DeterminismEvaluator::isDeterministic))); + joinPredicate = stripNonDeterministicConjuncts(joinPredicate); + + leftEffectivePredicate = stripNonDeterministicConjuncts(leftEffectivePredicate); + rightEffectivePredicate = stripNonDeterministicConjuncts(rightEffectivePredicate); + + // Generate equality inferences + EqualityInference allInference = createEqualityInference(inheritedPredicate, leftEffectivePredicate, rightEffectivePredicate, joinPredicate); + EqualityInference allInferenceWithoutLeftInferred = createEqualityInference(inheritedPredicate, rightEffectivePredicate, joinPredicate); + EqualityInference allInferenceWithoutRightInferred = createEqualityInference(inheritedPredicate, leftEffectivePredicate, joinPredicate); + + // Sort through conjuncts in inheritedPredicate that were not used for inference + for (Expression conjunct : EqualityInference.nonInferrableConjuncts(inheritedPredicate)) { + Expression leftRewrittenConjunct = allInference.rewriteExpression(conjunct, in(leftSymbols)); + if (leftRewrittenConjunct != null) { + leftPushDownConjuncts.add(leftRewrittenConjunct); + } + + Expression rightRewrittenConjunct = allInference.rewriteExpression(conjunct, not(in(leftSymbols))); + if (rightRewrittenConjunct != null) { + rightPushDownConjuncts.add(rightRewrittenConjunct); + } + + // Drop predicate after join only if unable to push down to either side + if (leftRewrittenConjunct == null && rightRewrittenConjunct == null) { + joinConjuncts.add(conjunct); + } + } + + // See if we can push the right effective predicate to the left side + for (Expression conjunct : EqualityInference.nonInferrableConjuncts(rightEffectivePredicate)) { + Expression rewritten = allInference.rewriteExpression(conjunct, in(leftSymbols)); + if (rewritten != null) { + leftPushDownConjuncts.add(rewritten); + } + } + + // See if we can push the left effective predicate to the right side + for (Expression conjunct : EqualityInference.nonInferrableConjuncts(leftEffectivePredicate)) { + Expression rewritten = allInference.rewriteExpression(conjunct, not(in(leftSymbols))); + if (rewritten != null) { + rightPushDownConjuncts.add(rewritten); + } + } + + // See if we can push any parts of the join predicates to either side + for (Expression conjunct : EqualityInference.nonInferrableConjuncts(joinPredicate)) { + Expression leftRewritten = allInference.rewriteExpression(conjunct, in(leftSymbols)); + if (leftRewritten != null) { + leftPushDownConjuncts.add(leftRewritten); + } + + Expression rightRewritten = allInference.rewriteExpression(conjunct, not(in(leftSymbols))); + if (rightRewritten != null) { + rightPushDownConjuncts.add(rightRewritten); + } + + if (leftRewritten == null && rightRewritten == null) { + joinConjuncts.add(conjunct); + } + } + + // Add equalities from the inference back in + leftPushDownConjuncts.addAll(allInferenceWithoutLeftInferred.generateEqualitiesPartitionedBy(in(leftSymbols)).getScopeEqualities()); + rightPushDownConjuncts.addAll(allInferenceWithoutRightInferred.generateEqualitiesPartitionedBy(not(in(leftSymbols))).getScopeEqualities()); + joinConjuncts.addAll(allInference.generateEqualitiesPartitionedBy(in(leftSymbols)).getScopeStraddlingEqualities()); // scope straddling equalities get dropped in as part of the join predicate + + // Since we only currently support equality in join conjuncts, factor out the non-equality conjuncts to a post-join filter + List joinConjunctsList = joinConjuncts.build(); + List postJoinConjuncts = ImmutableList.copyOf(filter(joinConjunctsList, not(joinEqualityExpression(leftSymbols)))); + joinConjunctsList = ImmutableList.copyOf(filter(joinConjunctsList, joinEqualityExpression(leftSymbols))); + + return new InnerJoinPushDownResult(combineConjuncts(leftPushDownConjuncts.build()), combineConjuncts(rightPushDownConjuncts.build()), combineConjuncts(joinConjunctsList), combineConjuncts(postJoinConjuncts)); + } + + private static class InnerJoinPushDownResult + { + private final Expression leftPredicate; + private final Expression rightPredicate; + private final Expression joinPredicate; + private final Expression postJoinPredicate; + + private InnerJoinPushDownResult(Expression leftPredicate, Expression rightPredicate, Expression joinPredicate, Expression postJoinPredicate) + { + this.leftPredicate = leftPredicate; + this.rightPredicate = rightPredicate; + this.joinPredicate = joinPredicate; + this.postJoinPredicate = postJoinPredicate; + } + + private Expression getLeftPredicate() + { + return leftPredicate; + } + + private Expression getRightPredicate() + { + return rightPredicate; + } + + private Expression getJoinPredicate() + { + return joinPredicate; + } + + private Expression getPostJoinPredicate() + { + return postJoinPredicate; + } + } + + private static Expression extractJoinPredicate(JoinNode joinNode) + { + ImmutableList.Builder builder = ImmutableList.builder(); + for (JoinNode.EquiJoinClause equiJoinClause : joinNode.getCriteria()) { + builder.add(equalsExpression(equiJoinClause.getLeft(), equiJoinClause.getRight())); + } + return combineConjuncts(builder.build()); + } + + private static Expression equalsExpression(Symbol symbol1, Symbol symbol2) + { + return new ComparisonExpression(ComparisonExpression.Type.EQUAL, + new QualifiedNameReference(symbol1.toQualifiedName()), + new QualifiedNameReference(symbol2.toQualifiedName())); + } + + private Type extractType(Expression expression) + { + return getExpressionTypes(session, metadata, sqlParser, symbolAllocator.getTypes(), expression).get(expression); + } + + private JoinNode tryNormalizeToInnerJoin(JoinNode node, Expression inheritedPredicate) + { + Preconditions.checkArgument(EnumSet.of(INNER, RIGHT, LEFT, FULL, CROSS).contains(node.getType()), "Unsupported join type: %s", node.getType()); + + if (node.getType() == JoinNode.Type.CROSS) { + return new JoinNode(node.getId(), JoinNode.Type.INNER, node.getLeft(), node.getRight(), node.getCriteria(), node.getLeftHashSymbol(), node.getRightHashSymbol()); + } + + if (node.getType() == JoinNode.Type.FULL) { + boolean canConvertToLeftJoin = canConvertOuterToInner(node.getLeft().getOutputSymbols(), inheritedPredicate); + boolean canConvertToRightJoin = canConvertOuterToInner(node.getRight().getOutputSymbols(), inheritedPredicate); + if (!canConvertToLeftJoin && !canConvertToRightJoin) { + return node; + } + if (canConvertToLeftJoin && canConvertToRightJoin) { + return new JoinNode(node.getId(), INNER, node.getLeft(), node.getRight(), node.getCriteria(), node.getLeftHashSymbol(), node.getRightHashSymbol()); + } + else { + return new JoinNode(node.getId(), canConvertToLeftJoin ? LEFT : RIGHT, + node.getLeft(), node.getRight(), node.getCriteria(), node.getLeftHashSymbol(), node.getRightHashSymbol()); + } + } + + if (node.getType() == JoinNode.Type.INNER || + node.getType() == JoinNode.Type.LEFT && !canConvertOuterToInner(node.getRight().getOutputSymbols(), inheritedPredicate) || + node.getType() == JoinNode.Type.RIGHT && !canConvertOuterToInner(node.getLeft().getOutputSymbols(), inheritedPredicate)) { + return node; + } + return new JoinNode(node.getId(), JoinNode.Type.INNER, node.getLeft(), node.getRight(), node.getCriteria(), node.getLeftHashSymbol(), node.getRightHashSymbol()); + } + + private boolean canConvertOuterToInner(List innerSymbolsForOuterJoin, Expression inheritedPredicate) + { + Set innerSymbols = ImmutableSet.copyOf(innerSymbolsForOuterJoin); + for (Expression conjunct : extractConjuncts(inheritedPredicate)) { + if (DeterminismEvaluator.isDeterministic(conjunct)) { + // Ignore a conjunct for this test if we can not deterministically get responses from it + Object response = nullInputEvaluator(innerSymbols, conjunct); + if (response == null || response instanceof NullLiteral || Boolean.FALSE.equals(response)) { + // If there is a single conjunct that returns FALSE or NULL given all NULL inputs for the inner side symbols of an outer join + // then this conjunct removes all effects of the outer join, and effectively turns this into an equivalent of an inner join. + // So, let's just rewrite this join as an INNER join + return true; + } + } + } + return false; + } + + // Temporary implementation for joins because the SimplifyExpressions optimizers can not run properly on join clauses + private Expression simplifyExpression(Expression expression) + { + IdentityHashMap expressionTypes = getExpressionTypes(session, metadata, sqlParser, symbolAllocator.getTypes(), expression); + ExpressionInterpreter optimizer = ExpressionInterpreter.expressionOptimizer(expression, metadata, session, expressionTypes); + return LiteralInterpreter.toExpression(optimizer.optimize(NoOpSymbolResolver.INSTANCE), expressionTypes.get(expression)); + } + + /** + * Evaluates an expression's response to binding the specified input symbols to NULL + */ + private Object nullInputEvaluator(final Collection nullSymbols, Expression expression) + { + IdentityHashMap expressionTypes = getExpressionTypes(session, metadata, sqlParser, symbolAllocator.getTypes(), expression); + return ExpressionInterpreter.expressionOptimizer(expression, metadata, session, expressionTypes) + .optimize(symbol -> nullSymbols.contains(symbol) ? null : new QualifiedNameReference(symbol.toQualifiedName())); + } + + private static Predicate joinEqualityExpression(final Collection leftSymbols) + { + return expression -> { + // At this point in time, our join predicates need to be deterministic + if (isDeterministic(expression) && expression instanceof ComparisonExpression) { + ComparisonExpression comparison = (ComparisonExpression) expression; + if (comparison.getType() == ComparisonExpression.Type.EQUAL) { + Set symbols1 = DependencyExtractor.extractUnique(comparison.getLeft()); + Set symbols2 = DependencyExtractor.extractUnique(comparison.getRight()); + return (Iterables.all(symbols1, in(leftSymbols)) && Iterables.all(symbols2, not(in(leftSymbols)))) || + (Iterables.all(symbols2, in(leftSymbols)) && Iterables.all(symbols1, not(in(leftSymbols)))); + } + } + return false; + }; + } + + @Override + public PlanNode visitSemiJoin(SemiJoinNode node, RewriteContext context) + { + Expression inheritedPredicate = context.get(); + + Expression sourceEffectivePredicate = EffectivePredicateExtractor.extract(node.getSource(), symbolAllocator.getTypes()); + + List sourceConjuncts = new ArrayList<>(); + List filteringSourceConjuncts = new ArrayList<>(); + List postJoinConjuncts = new ArrayList<>(); + + // TODO: see if there are predicates that can be inferred from the semi join output + + // Push inherited and source predicates to filtering source via a contrived join predicate (but needs to avoid touching NULL values in the filtering source) + Expression joinPredicate = equalsExpression(node.getSourceJoinSymbol(), node.getFilteringSourceJoinSymbol()); + EqualityInference joinInference = createEqualityInference(inheritedPredicate, sourceEffectivePredicate, joinPredicate); + for (Expression conjunct : Iterables.concat(EqualityInference.nonInferrableConjuncts(inheritedPredicate), EqualityInference.nonInferrableConjuncts(sourceEffectivePredicate))) { + Expression rewrittenConjunct = joinInference.rewriteExpression(conjunct, equalTo(node.getFilteringSourceJoinSymbol())); + if (rewrittenConjunct != null && DeterminismEvaluator.isDeterministic(rewrittenConjunct)) { + // Alter conjunct to include an OR filteringSourceJoinSymbol IS NULL disjunct + Expression rewrittenConjunctOrNull = expressionOrNullSymbols(equalTo(node.getFilteringSourceJoinSymbol())).apply(rewrittenConjunct); + filteringSourceConjuncts.add(rewrittenConjunctOrNull); + } + } + EqualityInference.EqualityPartition joinInferenceEqualityPartition = joinInference.generateEqualitiesPartitionedBy(equalTo(node.getFilteringSourceJoinSymbol())); + filteringSourceConjuncts.addAll(ImmutableList.copyOf(transform(joinInferenceEqualityPartition.getScopeEqualities(), + expressionOrNullSymbols(equalTo(node.getFilteringSourceJoinSymbol()))))); + + // Push inheritedPredicates down to the source if they don't involve the semi join output + EqualityInference inheritedInference = createEqualityInference(inheritedPredicate); + for (Expression conjunct : EqualityInference.nonInferrableConjuncts(inheritedPredicate)) { + Expression rewrittenConjunct = inheritedInference.rewriteExpression(conjunct, in(node.getSource().getOutputSymbols())); + // Since each source row is reflected exactly once in the output, ok to push non-deterministic predicates down + if (rewrittenConjunct != null) { + sourceConjuncts.add(rewrittenConjunct); + } + else { + postJoinConjuncts.add(conjunct); + } + } + + // Add the inherited equality predicates back in + EqualityInference.EqualityPartition equalityPartition = inheritedInference.generateEqualitiesPartitionedBy(in(node.getSource().getOutputSymbols())); + sourceConjuncts.addAll(equalityPartition.getScopeEqualities()); + postJoinConjuncts.addAll(equalityPartition.getScopeComplementEqualities()); + postJoinConjuncts.addAll(equalityPartition.getScopeStraddlingEqualities()); + + PlanNode rewrittenSource = context.rewrite(node.getSource(), combineConjuncts(sourceConjuncts)); + PlanNode rewrittenFilteringSource = context.rewrite(node.getFilteringSource(), combineConjuncts(filteringSourceConjuncts)); + + PlanNode output = node; + if (rewrittenSource != node.getSource() || rewrittenFilteringSource != node.getFilteringSource()) { + output = new SemiJoinNode(node.getId(), rewrittenSource, rewrittenFilteringSource, node.getSourceJoinSymbol(), node.getFilteringSourceJoinSymbol(), node.getSemiJoinOutput(), node.getSourceHashSymbol(), node.getFilteringSourceHashSymbol()); + } + if (!postJoinConjuncts.isEmpty()) { + output = new FilterNode(idAllocator.getNextId(), output, combineConjuncts(postJoinConjuncts)); + } + return output; + } + + @Override + public PlanNode visitAggregation(AggregationNode node, RewriteContext context) + { + Expression inheritedPredicate = context.get(); + + EqualityInference equalityInference = createEqualityInference(inheritedPredicate); + + List pushdownConjuncts = new ArrayList<>(); + List postAggregationConjuncts = new ArrayList<>(); + + // Strip out non-deterministic conjuncts + postAggregationConjuncts.addAll(ImmutableList.copyOf(filter(extractConjuncts(inheritedPredicate), not(DeterminismEvaluator::isDeterministic)))); + inheritedPredicate = stripNonDeterministicConjuncts(inheritedPredicate); + + // Sort non-equality predicates by those that can be pushed down and those that cannot + for (Expression conjunct : EqualityInference.nonInferrableConjuncts(inheritedPredicate)) { + Expression rewrittenConjunct = equalityInference.rewriteExpression(conjunct, in(node.getGroupBy())); + if (rewrittenConjunct != null) { + pushdownConjuncts.add(rewrittenConjunct); + } + else { + postAggregationConjuncts.add(conjunct); + } + } + + // Add the equality predicates back in + EqualityInference.EqualityPartition equalityPartition = equalityInference.generateEqualitiesPartitionedBy(in(node.getGroupBy())); + pushdownConjuncts.addAll(equalityPartition.getScopeEqualities()); + postAggregationConjuncts.addAll(equalityPartition.getScopeComplementEqualities()); + postAggregationConjuncts.addAll(equalityPartition.getScopeStraddlingEqualities()); + + PlanNode rewrittenSource = context.rewrite(node.getSource(), combineConjuncts(pushdownConjuncts)); + + PlanNode output = node; + if (rewrittenSource != node.getSource()) { + output = new AggregationNode(node.getId(), + rewrittenSource, + node.getGroupBy(), + node.getAggregations(), + node.getFunctions(), + node.getMasks(), + node.getStep(), + node.getSampleWeight(), + node.getConfidence(), + node.getHashSymbol()); + } + if (!postAggregationConjuncts.isEmpty()) { + output = new FilterNode(idAllocator.getNextId(), output, combineConjuncts(postAggregationConjuncts)); + } + return output; + } + + @Override + public PlanNode visitUnnest(UnnestNode node, RewriteContext context) + { + Expression inheritedPredicate = context.get(); + + EqualityInference equalityInference = createEqualityInference(inheritedPredicate); + + List pushdownConjuncts = new ArrayList<>(); + List postUnnestConjuncts = new ArrayList<>(); + + // Strip out non-deterministic conjuncts + postUnnestConjuncts.addAll(ImmutableList.copyOf(filter(extractConjuncts(inheritedPredicate), not(DeterminismEvaluator::isDeterministic)))); + inheritedPredicate = stripNonDeterministicConjuncts(inheritedPredicate); + + // Sort non-equality predicates by those that can be pushed down and those that cannot + for (Expression conjunct : EqualityInference.nonInferrableConjuncts(inheritedPredicate)) { + Expression rewrittenConjunct = equalityInference.rewriteExpression(conjunct, in(node.getReplicateSymbols())); + if (rewrittenConjunct != null) { + pushdownConjuncts.add(rewrittenConjunct); + } + else { + postUnnestConjuncts.add(conjunct); + } + } + + // Add the equality predicates back in + EqualityInference.EqualityPartition equalityPartition = equalityInference.generateEqualitiesPartitionedBy(in(node.getReplicateSymbols())); + pushdownConjuncts.addAll(equalityPartition.getScopeEqualities()); + postUnnestConjuncts.addAll(equalityPartition.getScopeComplementEqualities()); + postUnnestConjuncts.addAll(equalityPartition.getScopeStraddlingEqualities()); + + PlanNode rewrittenSource = context.rewrite(node.getSource(), combineConjuncts(pushdownConjuncts)); + + PlanNode output = node; + if (rewrittenSource != node.getSource()) { + output = new UnnestNode(node.getId(), rewrittenSource, node.getReplicateSymbols(), node.getUnnestSymbols(), node.getOrdinalitySymbol()); + } + if (!postUnnestConjuncts.isEmpty()) { + output = new FilterNode(idAllocator.getNextId(), output, combineConjuncts(postUnnestConjuncts)); + } + return output; + } + + @Override + public PlanNode visitSample(SampleNode node, RewriteContext context) + { + return context.defaultRewrite(node, context.get()); + } + + @Override + public PlanNode visitTableScan(TableScanNode node, RewriteContext context) + { + Expression predicate = simplifyExpression(context.get()); + + if (BooleanLiteral.FALSE_LITERAL.equals(predicate) || predicate instanceof NullLiteral) { + return new ValuesNode(idAllocator.getNextId(), node.getOutputSymbols(), ImmutableList.of()); + } + else if (!BooleanLiteral.TRUE_LITERAL.equals(predicate)) { + return new FilterNode(idAllocator.getNextId(), node, predicate); + } + + return node; + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/PreferredProperties.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/PreferredProperties.java new file mode 100644 index 00000000..49d64372 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/PreferredProperties.java @@ -0,0 +1,262 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.optimizations; + +import com.facebook.presto.spi.LocalProperty; +import com.facebook.presto.sql.planner.Symbol; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; +import static java.util.Objects.requireNonNull; +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toSet; + +class PreferredProperties +{ + private final Optional partitioningRequirements; + private final List> localProperties; + + public PreferredProperties( + Optional partitioningRequirements, + List> localProperties) + { + requireNonNull(partitioningRequirements, "partitioningRequirements is null"); + requireNonNull(localProperties, "localProperties is null"); + + this.partitioningRequirements = partitioningRequirements; + this.localProperties = ImmutableList.copyOf(localProperties); + } + + public static PreferredProperties any() + { + return new PreferredProperties(Optional.empty(), ImmutableList.of()); + } + + public static PreferredProperties unpartitioned() + { + return new PreferredProperties(Optional.of(PartitioningPreferences.unpartitioned()), ImmutableList.of()); + } + + public static PreferredProperties partitioned(Set columns) + { + return new PreferredProperties(Optional.of(PartitioningPreferences.partitioned(columns)), ImmutableList.of()); + } + + public static PreferredProperties partitioned() + { + return new PreferredProperties(Optional.of(PartitioningPreferences.partitioned()), ImmutableList.of()); + } + + public static PreferredProperties hashPartitioned(List columns) + { + return new PreferredProperties(Optional.of(PartitioningPreferences.hashPartitioned(columns)), ImmutableList.of()); + } + + public static PreferredProperties hashPartitionedWithLocal(List columns, List> localProperties) + { + return new PreferredProperties(Optional.of(PartitioningPreferences.hashPartitioned(columns)), ImmutableList.copyOf(localProperties)); + } + + public static PreferredProperties partitionedWithLocal(Set columns, List> localProperties) + { + return new PreferredProperties(Optional.of(PartitioningPreferences.partitioned(columns)), ImmutableList.copyOf(localProperties)); + } + + public static PreferredProperties unpartitionedWithLocal(List> localProperties) + { + return new PreferredProperties(Optional.of(PartitioningPreferences.unpartitioned()), ImmutableList.copyOf(localProperties)); + } + + public static PreferredProperties local(List> localProperties) + { + return new PreferredProperties(Optional.empty(), ImmutableList.copyOf(localProperties)); + } + + public Optional getPartitioningProperties() + { + return partitioningRequirements; + } + + public List> getLocalProperties() + { + return localProperties; + } + + /** + * @param preferred PreferredProperties in terms of the parent symbols + * @param translations output to input translations for symbols that can be translated + * @return translated PreferredProperties + */ + public static PreferredProperties translate(PreferredProperties preferred, Map translations) + { + List> localProperties = LocalProperties.translate(preferred.getLocalProperties(), column -> Optional.ofNullable(translations.get(column))); + + if (preferred.getPartitioningProperties().isPresent()) { + PartitioningPreferences partitioning = preferred.getPartitioningProperties().get(); + if (partitioning.isHashPartitioned()) { + List hashingSymbols = partitioning.getHashPartitioningColumns().get(); + if (translations.keySet().containsAll(hashingSymbols)) { + List translated = partitioning.getHashPartitioningColumns().get().stream() + .map(translations::get) + .collect(toList()); + return hashPartitionedWithLocal(translated, localProperties); + } + } + if (partitioning.isPartitioned()) { + // check if we can satisfy any partitioning requirements + Set symbols = partitioning.getPartitioningColumns().get(); + Set translated = symbols.stream() + .filter(symbol -> translations.keySet().contains(symbol)) + .map(translations::get) + .collect(toSet()); + + if (!translated.isEmpty()) { + return partitionedWithLocal(translated, localProperties); + } + } + else { + return unpartitionedWithLocal(localProperties); + } + } + return local(localProperties); + } + + public static PreferredProperties derivePreferences( + PreferredProperties parentProperties, + Set partitioningColumns, + List> localProperties) + { + return derivePreferences(parentProperties, partitioningColumns, Optional.empty(), localProperties); + } + + /** + * Derive current node's preferred properties based on parent's preferences + * @param parentProperties Parent's preferences (translated) + * @param partitioningColumns partitioning columns of current node + * @param hashingColumns hashing columns of current node + * @param localProperties local properties of current node + * @return PreferredProperties for current node + */ + public static PreferredProperties derivePreferences( + PreferredProperties parentProperties, + Set partitioningColumns, + Optional> hashingColumns, + List> localProperties) + { + if (hashingColumns.isPresent()) { + checkState(partitioningColumns.equals(ImmutableSet.copyOf(hashingColumns.get())), "hashingColumns and partitioningColumns must be the same"); + } + + List> local = ImmutableList.>builder() + .addAll(localProperties) + .addAll(parentProperties.getLocalProperties()) + .build(); + + // Check we need to be hash partitioned + if (hashingColumns.isPresent()) { + return hashPartitionedWithLocal(hashingColumns.get(), local); + } + + if (parentProperties.getPartitioningProperties().isPresent()) { + PartitioningPreferences parentPartitioning = parentProperties.getPartitioningProperties().get(); + // If parent's hash partitioning satisfies our partitioning, use parent's hash partitioning + if (parentPartitioning.isHashPartitioned() && partitioningColumns.equals(ImmutableSet.copyOf(parentPartitioning.getHashPartitioningColumns().get()))) { + List hashingSymbols = parentPartitioning.getHashPartitioningColumns().get(); + return hashPartitionedWithLocal(hashingSymbols, local); + } + + // if the child plan is partitioned by the common columns between our requirements and our parent's, it can satisfy both in one shot + if (parentPartitioning.isPartitioned()) { + Set parentPartitioningColumns = parentPartitioning.getPartitioningColumns().get(); + Set common = Sets.intersection(partitioningColumns, parentPartitioningColumns); + + // If we find common partitioning columns, use them, else use child's partitioning columns + if (!common.isEmpty()) { + return partitionedWithLocal(common, local); + } + return partitionedWithLocal(partitioningColumns, local); + } + } + return partitionedWithLocal(partitioningColumns, local); + } + + public static class PartitioningPreferences + { + private final boolean partitioned; + private final Optional> partitioningColumns; + private final Optional> hashingColumns; + + private PartitioningPreferences(boolean partitioned, Optional> partitioningColumns, Optional> hashingColumns) + { + requireNonNull(partitioningColumns, "partitioningColumns is null"); + checkArgument(partitioned || (partitioningColumns.isPresent() && partitioningColumns.get().isEmpty()), "unpartitioned implies partitioned on the empty set"); + if (hashingColumns.isPresent()) { + checkArgument(partitioningColumns.isPresent(), "partitioningColumns not present"); + checkArgument(partitioningColumns.get().containsAll(hashingColumns.get()), "partitioningColumns does not include hashingColumns"); + } + + this.partitioned = partitioned; + this.partitioningColumns = partitioningColumns.map(ImmutableSet::copyOf); + this.hashingColumns = hashingColumns.map(ImmutableList::copyOf); + } + + public static PartitioningPreferences unpartitioned() + { + return new PartitioningPreferences(false, Optional.>of(ImmutableSet.of()), Optional.empty()); + } + + public static PartitioningPreferences partitioned(Set columns) + { + return new PartitioningPreferences(true, Optional.of(columns), Optional.empty()); + } + + public static PartitioningPreferences partitioned() + { + return new PartitioningPreferences(true, Optional.empty(), Optional.empty()); + } + + public static PartitioningPreferences hashPartitioned(List columns) + { + return new PartitioningPreferences(true, Optional.of(ImmutableSet.copyOf(columns)), Optional.of(ImmutableList.copyOf(columns))); + } + + public boolean isPartitioned() + { + return partitioned; + } + + public Optional> getPartitioningColumns() + { + return partitioningColumns; + } + + public boolean isHashPartitioned() + { + return hashingColumns.isPresent(); + } + + public Optional> getHashPartitioningColumns() + { + return hashingColumns; + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/PropertyDerivations.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/PropertyDerivations.java new file mode 100644 index 00000000..c31f04a0 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/PropertyDerivations.java @@ -0,0 +1,529 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.optimizations; + +import com.facebook.presto.Session; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.metadata.TableLayout; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ConstantProperty; +import com.facebook.presto.spi.GroupingProperty; +import com.facebook.presto.spi.LocalProperty; +import com.facebook.presto.spi.SortingProperty; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.parser.SqlParser; +import com.facebook.presto.sql.planner.DomainTranslator; +import com.facebook.presto.sql.planner.ExpressionInterpreter; +import com.facebook.presto.sql.planner.NoOpSymbolResolver; +import com.facebook.presto.sql.planner.Symbol; +import com.facebook.presto.sql.planner.plan.AggregationNode; +import com.facebook.presto.sql.planner.plan.DeleteNode; +import com.facebook.presto.sql.planner.plan.DistinctLimitNode; +import com.facebook.presto.sql.planner.plan.ExchangeNode; +import com.facebook.presto.sql.planner.plan.FilterNode; +import com.facebook.presto.sql.planner.plan.IndexJoinNode; +import com.facebook.presto.sql.planner.plan.JoinNode; +import com.facebook.presto.sql.planner.plan.LimitNode; +import com.facebook.presto.sql.planner.plan.MarkDistinctNode; +import com.facebook.presto.sql.planner.plan.OutputNode; +import com.facebook.presto.sql.planner.plan.PlanNode; +import com.facebook.presto.sql.planner.plan.PlanVisitor; +import com.facebook.presto.sql.planner.plan.ProjectNode; +import com.facebook.presto.sql.planner.plan.RowNumberNode; +import com.facebook.presto.sql.planner.plan.SampleNode; +import com.facebook.presto.sql.planner.plan.SemiJoinNode; +import com.facebook.presto.sql.planner.plan.SortNode; +import com.facebook.presto.sql.planner.plan.TableCommitNode; +import com.facebook.presto.sql.planner.plan.TableScanNode; +import com.facebook.presto.sql.planner.plan.TableWriterNode; +import com.facebook.presto.sql.planner.plan.TopNNode; +import com.facebook.presto.sql.planner.plan.TopNRowNumberNode; +import com.facebook.presto.sql.planner.plan.UnnestNode; +import com.facebook.presto.sql.planner.plan.WindowNode; +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.QualifiedNameReference; +import com.google.common.collect.ImmutableBiMap; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.collect.Maps; + +import java.util.Collection; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static com.facebook.presto.sql.analyzer.ExpressionAnalyzer.getExpressionTypes; +import static com.facebook.presto.util.ImmutableCollectors.toImmutableList; +import static com.facebook.presto.util.ImmutableCollectors.toImmutableSet; +import static com.google.common.base.Preconditions.checkArgument; +import static java.util.stream.Collectors.toMap; + +class PropertyDerivations +{ + private PropertyDerivations() {} + + public static ActualProperties deriveProperties(PlanNode node, ActualProperties inputProperties, Metadata metadata, Session session, Map types, SqlParser parser) + { + return deriveProperties(node, ImmutableList.of(inputProperties), metadata, session, types, parser); + } + + public static ActualProperties deriveProperties(PlanNode node, List inputProperties, Metadata metadata, Session session, Map types, SqlParser parser) + { + return node.accept(new Visitor(metadata, session, types, parser), inputProperties); + } + + private static class Visitor + extends PlanVisitor, ActualProperties> + { + private final Metadata metadata; + private final Session session; + private final Map types; + private final SqlParser parser; + + public Visitor(Metadata metadata, Session session, Map types, SqlParser parser) + { + this.metadata = metadata; + this.session = session; + this.types = types; + this.parser = parser; + } + + @Override + protected ActualProperties visitPlan(PlanNode node, List inputProperties) + { + throw new UnsupportedOperationException("not yet implemented: " + node.getClass().getName()); + } + + @Override + public ActualProperties visitOutput(OutputNode node, List inputProperties) + { + return Iterables.getOnlyElement(inputProperties); + } + + @Override + public ActualProperties visitMarkDistinct(MarkDistinctNode node, List inputProperties) + { + return Iterables.getOnlyElement(inputProperties); + } + + @Override + public ActualProperties visitWindow(WindowNode node, List inputProperties) + { + // If the input is completely pre-partitioned and sorted, then the original input properties will be respected + if (ImmutableSet.copyOf(node.getPartitionBy()).equals(node.getPrePartitionedInputs()) && node.getPreSortedOrderPrefix() == node.getOrderBy().size()) { + return Iterables.getOnlyElement(inputProperties); + } + + ImmutableList.Builder> localProperties = ImmutableList.builder(); + if (!node.getPartitionBy().isEmpty()) { + localProperties.add(new GroupingProperty<>(node.getPartitionBy())); + } + for (Symbol column : node.getOrderBy()) { + localProperties.add(new SortingProperty<>(column, node.getOrderings().get(column))); + } + + ActualProperties properties = Iterables.getOnlyElement(inputProperties); + + return ActualProperties.builder() + .partitioned(properties) + .coordinatorOnly(properties) + .local(localProperties.build()) + .constants(properties) + .build(); + } + + @Override + public ActualProperties visitAggregation(AggregationNode node, List inputProperties) + { + ActualProperties properties = Iterables.getOnlyElement(inputProperties); + + if (!properties.isPartitioned()) { + return ActualProperties.builder() + .unpartitioned() + .coordinatorOnly(properties) + .local(LocalProperties.grouped(node.getGroupBy())) + .constants(Maps.filterKeys(properties.getConstants(), ImmutableSet.of(node.getGroupBy())::contains)) + .build(); + } + + return ActualProperties.builder() + .partitioned(properties) + .local(LocalProperties.grouped(node.getGroupBy())) + .constants(Maps.filterKeys(properties.getConstants(), ImmutableSet.of(node.getGroupBy())::contains)) + .build(); + } + + @Override + public ActualProperties visitRowNumber(RowNumberNode node, List inputProperties) + { + ActualProperties properties = Iterables.getOnlyElement(inputProperties); + + return ActualProperties.builder() + .partitioned(properties) + .coordinatorOnly(properties) + .local(LocalProperties.grouped(node.getPartitionBy())) + .constants(properties) + .build(); + } + + @Override + public ActualProperties visitTopNRowNumber(TopNRowNumberNode node, List inputProperties) + { + ActualProperties properties = Iterables.getOnlyElement(inputProperties); + + ImmutableList.Builder> localProperties = ImmutableList.builder(); + localProperties.add(new GroupingProperty<>(node.getPartitionBy())); + for (Symbol column : node.getOrderBy()) { + localProperties.add(new SortingProperty<>(column, node.getOrderings().get(column))); + } + + return ActualProperties.builder() + .partitioned(properties) + .coordinatorOnly(properties) + .local(localProperties.build()) + .constants(properties) + .build(); + } + + @Override + public ActualProperties visitTopN(TopNNode node, List inputProperties) + { + ActualProperties properties = Iterables.getOnlyElement(inputProperties); + + List> localProperties = node.getOrderBy().stream() + .map(column -> new SortingProperty<>(column, node.getOrderings().get(column))) + .collect(toImmutableList()); + + return ActualProperties.builder() + .partitioned(properties) + .coordinatorOnly(properties) + .local(localProperties) + .constants(properties) + .build(); + } + + @Override + public ActualProperties visitSort(SortNode node, List inputProperties) + { + ActualProperties properties = Iterables.getOnlyElement(inputProperties); + + List> localProperties = node.getOrderBy().stream() + .map(column -> new SortingProperty<>(column, node.getOrderings().get(column))) + .collect(toImmutableList()); + + return ActualProperties.builder() + .partitioned(properties) + .coordinatorOnly(properties) + .local(localProperties) + .constants(properties) + .build(); + } + + @Override + public ActualProperties visitLimit(LimitNode node, List inputProperties) + { + return Iterables.getOnlyElement(inputProperties); + } + + @Override + public ActualProperties visitDistinctLimit(DistinctLimitNode node, List inputProperties) + { + ActualProperties properties = Iterables.getOnlyElement(inputProperties); + + return ActualProperties.builder() + .partitioned(properties) + .coordinatorOnly(properties) + .local(LocalProperties.grouped(node.getDistinctSymbols())) + .constants(properties) + .build(); + } + + @Override + public ActualProperties visitTableCommit(TableCommitNode node, List inputProperties) + { + ActualProperties properties = Iterables.getOnlyElement(inputProperties); + + return ActualProperties.builder() + .unpartitioned() + .coordinatorOnly(properties) + .build(); + } + + @Override + public ActualProperties visitDelete(DeleteNode node, List inputProperties) + { + return Iterables.getOnlyElement(inputProperties); + } + + @Override + public ActualProperties visitJoin(JoinNode node, List inputProperties) + { + // TODO: include all equivalent columns in partitioning properties + // TODO: derive constants for right side + return inputProperties.get(0); + } + + @Override + public ActualProperties visitSemiJoin(SemiJoinNode node, List inputProperties) + { + return inputProperties.get(0); + } + + @Override + public ActualProperties visitIndexJoin(IndexJoinNode node, List inputProperties) + { + return inputProperties.get(0); + } + + @Override + public ActualProperties visitExchange(ExchangeNode node, List inputProperties) + { + ActualProperties properties = inputProperties.get(0); + + switch (node.getType()) { + case GATHER: + return ActualProperties.builder() + .unpartitioned() + .constants(properties) + .build(); + case REPARTITION: + return ActualProperties.builder() + .hashPartitioned(node.getPartitionKeys()) + .constants(properties) + .build(); + case REPLICATE: + return ActualProperties.builder() + .partitioned(properties) + .constants(properties) + .build(); + } + + throw new UnsupportedOperationException("not yet implemented"); + } + + @Override + public ActualProperties visitFilter(FilterNode node, List inputProperties) + { + ActualProperties properties = Iterables.getOnlyElement(inputProperties); + + DomainTranslator.ExtractionResult decomposedPredicate = DomainTranslator.fromPredicate( + metadata, + session, + node.getPredicate(), + types); + + Map constants = new HashMap<>(properties.getConstants()); + constants.putAll(decomposedPredicate.getTupleDomain().extractFixedValues()); + + return ActualProperties.builder() + .partitioned(properties) + .coordinatorOnly(properties) + .local(properties) + .constants(constants) + .build(); + } + + @Override + public ActualProperties visitProject(ProjectNode node, List inputProperties) + { + ActualProperties properties = Iterables.getOnlyElement(inputProperties); + + Map identities = computeIdentityTranslations(node.getAssignments()); + + List> localProperties = LocalProperties.translate(properties.getLocalProperties(), column -> Optional.ofNullable(identities.get(column))); + + Map constants = new HashMap<>(); + for (Map.Entry assignment : node.getAssignments().entrySet()) { + Expression expression = assignment.getValue(); + + IdentityHashMap expressionTypes = getExpressionTypes(session, metadata, parser, types, expression); + ExpressionInterpreter optimizer = ExpressionInterpreter.expressionOptimizer(expression, metadata, session, expressionTypes); + // TODO: + // We want to use a symbol resolver that looks up in the constants from the input subplan + // to take advantage of constant-folding for complex expressions + // However, that currently causes errors when those expressions operate on arrays or row types + // ("ROW comparison not supported for fields with null elements", etc) + Object value = optimizer.optimize(NoOpSymbolResolver.INSTANCE); + + if (value instanceof QualifiedNameReference) { + Symbol symbol = Symbol.fromQualifiedName(((QualifiedNameReference) value).getName()); + value = constants.getOrDefault(symbol, value); + } + + // TODO: remove value null check when constants are supported + if (value != null && !(value instanceof Expression)) { + constants.put(assignment.getKey(), value); + } + } + properties.getConstants().entrySet().stream() + .filter(entry -> identities.containsKey(entry.getKey())) + .forEach(entry -> constants.put(identities.get(entry.getKey()), entry.getValue())); + + if (!properties.isPartitioned()) { + return ActualProperties.builder() + .coordinatorOnly(properties) + .unpartitioned() + .local(localProperties) + .constants(constants) + .build(); + } + + if (properties.isHashPartitioned()) { + Optional> translated = translate(properties.getHashPartitioningColumns().get(), identities); + + if (translated.isPresent()) { + return ActualProperties.builder() + .coordinatorOnly(properties) + .hashPartitioned(translated.get()) + .local(localProperties) + .constants(constants) + .build(); + } + } + + if (properties.hasKnownPartitioningScheme()) { + Optional> translated = translate(properties.getPartitioningColumns().get(), identities); + + if (translated.isPresent()) { + return ActualProperties.builder() + .coordinatorOnly(properties) + .partitioned(ImmutableSet.copyOf(translated.get())) + .local(localProperties) + .constants(constants) + .build(); + } + } + + return ActualProperties.builder() + .coordinatorOnly(properties) + .partitioned() + .local(localProperties) + .constants(constants) + .build(); + } + + @Override + public ActualProperties visitTableWriter(TableWriterNode node, List inputProperties) + { + ActualProperties properties = Iterables.getOnlyElement(inputProperties); + + ActualProperties.Builder derived = ActualProperties.builder() + .coordinatorOnly(properties); + + if (properties.isPartitioned()) { + derived.partitioned(); + } + else { + derived.unpartitioned(); + } + + return derived.build(); + } + + @Override + public ActualProperties visitSample(SampleNode node, List inputProperties) + { + return Iterables.getOnlyElement(inputProperties); + } + + @Override + public ActualProperties visitUnnest(UnnestNode node, List inputProperties) + { + return Iterables.getOnlyElement(inputProperties); + } + + @Override + public ActualProperties visitTableScan(TableScanNode node, List inputProperties) + { + checkArgument(node.getLayout().isPresent(), "table layout has not yet been chosen"); + + TableLayout layout = metadata.getLayout(node.getLayout().get()); + Map assignments = ImmutableBiMap.copyOf(node.getAssignments()).inverse(); + + ActualProperties.Builder properties = ActualProperties.builder(); + + // Constant assignments + Map constants = new HashMap<>(); + LocalProperties.extractLeadingConstants(layout.getLocalProperties()).stream() + .forEach(column -> constants.put(column, new Object())); // Use an arbitrary object value for property constants b/c we don't know its actual value + // Do predicate constants after property constants so that we can override with known real predicate values (if they exist) + node.getCurrentConstraint().extractFixedValues().entrySet().stream() + .forEach(entry -> constants.put(entry.getKey(), entry.getValue())); + + Map symbolConstants = constants.entrySet().stream() + .filter(entry -> assignments.containsKey(entry.getKey())) + .collect(toMap(entry -> assignments.get(entry.getKey()), Map.Entry::getValue)); + properties.constants(symbolConstants); + + // Partitioning properties + Optional> partitioningColumns = Optional.empty(); + if (layout.getPartitioningColumns().isPresent()) { + // Strip off the constants from the partitioning columns (since those are not required for translation) + Set constantsStrippedPartitionColumns = layout.getPartitioningColumns().get().stream() + .filter(column -> !constants.containsKey(column)) + .collect(toImmutableSet()); + partitioningColumns = translate(constantsStrippedPartitionColumns, assignments); + } + + if (partitioningColumns.isPresent()) { + properties.partitioned(ImmutableSet.copyOf(partitioningColumns.get())); + } + else { + properties.partitioned(); + } + + // Append the constants onto the local properties to maximize their translation potential + List> constantAppendedLocalProperties = ImmutableList.>builder() + .addAll(constants.keySet().stream().map(column -> new ConstantProperty<>(column)).iterator()) + .addAll(layout.getLocalProperties()) + .build(); + properties.local(LocalProperties.translate(constantAppendedLocalProperties, column -> Optional.ofNullable(assignments.get(column)))); + + return properties.build(); + } + + private static Map computeIdentityTranslations(Map assignments) + { + Map inputToOutput = new HashMap<>(); + for (Map.Entry assignment : assignments.entrySet()) { + if (assignment.getValue() instanceof QualifiedNameReference) { + inputToOutput.put(Symbol.fromQualifiedName(((QualifiedNameReference) assignment.getValue()).getName()), assignment.getKey()); + } + } + return inputToOutput; + } + + /** + * @return Optional.empty() if not all columns could be translated + */ + private static Optional> translate(Collection columns, Map mappings) + { + ImmutableList.Builder builder = ImmutableList.builder(); + + for (T column : columns) { + Symbol translated = mappings.get(column); + if (translated == null) { + return Optional.empty(); + } + builder.add(translated); + } + + return Optional.of(builder.build()); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/PruneRedundantProjections.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/PruneRedundantProjections.java new file mode 100644 index 00000000..e4dde62d --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/PruneRedundantProjections.java @@ -0,0 +1,80 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.optimizations; + +import com.facebook.presto.Session; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.planner.PlanNodeIdAllocator; +import com.facebook.presto.sql.planner.Symbol; +import com.facebook.presto.sql.planner.SymbolAllocator; +import com.facebook.presto.sql.planner.plan.PlanNode; +import com.facebook.presto.sql.planner.plan.PlanRewriter; +import com.facebook.presto.sql.planner.plan.ProjectNode; +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.QualifiedNameReference; +import com.google.common.collect.ImmutableList; + +import java.util.Map; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Removes pure identity projections (e.g., Project $0 := $0, $1 := $1, ...) + */ +public class PruneRedundantProjections + extends PlanOptimizer +{ + @Override + public PlanNode optimize(PlanNode plan, Session session, Map types, SymbolAllocator symbolAllocator, PlanNodeIdAllocator idAllocator) + { + checkNotNull(plan, "plan is null"); + checkNotNull(session, "session is null"); + checkNotNull(types, "types is null"); + checkNotNull(symbolAllocator, "symbolAllocator is null"); + checkNotNull(idAllocator, "idAllocator is null"); + + return PlanRewriter.rewriteWith(new Rewriter(), plan); + } + + private static class Rewriter + extends PlanRewriter + { + @Override + public PlanNode visitProject(ProjectNode node, RewriteContext context) + { + PlanNode source = context.rewrite(node.getSource()); + + if (node.getOutputSymbols().size() != source.getOutputSymbols().size()) { + // Can't get rid of this projection. It constrains the output tuple from the underlying operator + return context.replaceChildren(node, ImmutableList.of(source)); + } + + boolean canElide = true; + for (Map.Entry entry : node.getAssignments().entrySet()) { + Expression expression = entry.getValue(); + Symbol symbol = entry.getKey(); + if (!(expression instanceof QualifiedNameReference && ((QualifiedNameReference) expression).getName().equals(symbol.toQualifiedName()))) { + canElide = false; + break; + } + } + + if (canElide) { + return source; + } + + return context.replaceChildren(node, ImmutableList.of(source)); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/PruneUnreferencedOutputs.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/PruneUnreferencedOutputs.java new file mode 100644 index 00000000..1c7019c8 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/PruneUnreferencedOutputs.java @@ -0,0 +1,586 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.optimizations; + +import com.facebook.presto.Session; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.planner.DependencyExtractor; +import com.facebook.presto.sql.planner.PlanNodeIdAllocator; +import com.facebook.presto.sql.planner.Symbol; +import com.facebook.presto.sql.planner.SymbolAllocator; +import com.facebook.presto.sql.planner.plan.AggregationNode; +import com.facebook.presto.sql.planner.plan.DeleteNode; +import com.facebook.presto.sql.planner.plan.DistinctLimitNode; +import com.facebook.presto.sql.planner.plan.ExchangeNode; +import com.facebook.presto.sql.planner.plan.FilterNode; +import com.facebook.presto.sql.planner.plan.IndexJoinNode; +import com.facebook.presto.sql.planner.plan.IndexSourceNode; +import com.facebook.presto.sql.planner.plan.JoinNode; +import com.facebook.presto.sql.planner.plan.LimitNode; +import com.facebook.presto.sql.planner.plan.MarkDistinctNode; +import com.facebook.presto.sql.planner.plan.OutputNode; +import com.facebook.presto.sql.planner.plan.PlanNode; +import com.facebook.presto.sql.planner.plan.PlanRewriter; +import com.facebook.presto.sql.planner.plan.ProjectNode; +import com.facebook.presto.sql.planner.plan.RowNumberNode; +import com.facebook.presto.sql.planner.plan.SemiJoinNode; +import com.facebook.presto.sql.planner.plan.SortNode; +import com.facebook.presto.sql.planner.plan.TableCommitNode; +import com.facebook.presto.sql.planner.plan.TableScanNode; +import com.facebook.presto.sql.planner.plan.TableWriterNode; +import com.facebook.presto.sql.planner.plan.TopNNode; +import com.facebook.presto.sql.planner.plan.TopNRowNumberNode; +import com.facebook.presto.sql.planner.plan.UnionNode; +import com.facebook.presto.sql.planner.plan.UnnestNode; +import com.facebook.presto.sql.planner.plan.WindowNode; +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.FunctionCall; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.collect.ListMultimap; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Predicates.in; +import static com.google.common.collect.Iterables.concat; + +/** + * Removes all computation that does is not referenced transitively from the root of the plan + *

+ * E.g., + *

+ * {@code Output[$0] -> Project[$0 := $1 + $2, $3 = $4 / $5] -> ...} + *

+ * gets rewritten as + *

+ * {@code Output[$0] -> Project[$0 := $1 + $2] -> ...} + */ +public class PruneUnreferencedOutputs + extends PlanOptimizer +{ + @Override + public PlanNode optimize(PlanNode plan, Session session, Map types, SymbolAllocator symbolAllocator, PlanNodeIdAllocator idAllocator) + { + checkNotNull(plan, "plan is null"); + checkNotNull(session, "session is null"); + checkNotNull(types, "types is null"); + checkNotNull(symbolAllocator, "symbolAllocator is null"); + checkNotNull(idAllocator, "idAllocator is null"); + + return PlanRewriter.rewriteWith(new Rewriter(types), plan, ImmutableSet.of()); + } + + private static class Rewriter + extends PlanRewriter> + { + private final Map types; + + public Rewriter(Map types) + { + this.types = types; + } + + @Override + public PlanNode visitExchange(ExchangeNode node, RewriteContext> context) + { + Set expectedOutputSymbols = Sets.newHashSet(context.get()); + node.getHashSymbol().ifPresent(expectedOutputSymbols::add); + expectedOutputSymbols.addAll(node.getPartitionKeys()); + + List> inputsBySource = new ArrayList<>(node.getInputs().size()); + for (int i = 0; i < node.getInputs().size(); i++) { + inputsBySource.add(new ArrayList<>()); + } + + List newOutputSymbols = new ArrayList<>(node.getOutputSymbols().size()); + for (int i = 0; i < node.getOutputSymbols().size(); i++) { + Symbol outputSymbol = node.getOutputSymbols().get(i); + if (expectedOutputSymbols.contains(outputSymbol)) { + newOutputSymbols.add(outputSymbol); + for (int source = 0; source < node.getInputs().size(); source++) { + inputsBySource.get(source).add(node.getInputs().get(source).get(i)); + } + } + } + + ImmutableList.Builder rewrittenSources = ImmutableList.builder(); + for (int i = 0; i < node.getSources().size(); i++) { + ImmutableSet.Builder expectedInputs = ImmutableSet.builder() + .addAll(inputsBySource.get(i)); + + rewrittenSources.add(context.rewrite( + node.getSources().get(i), + expectedInputs.build())); + } + + return new ExchangeNode( + node.getId(), + node.getType(), + node.getPartitionKeys(), + node.getHashSymbol(), + rewrittenSources.build(), + newOutputSymbols, + inputsBySource); + } + + @Override + public PlanNode visitJoin(JoinNode node, RewriteContext> context) + { + ImmutableSet.Builder leftInputsBuilder = ImmutableSet.builder(); + leftInputsBuilder.addAll(context.get()).addAll(Iterables.transform(node.getCriteria(), JoinNode.EquiJoinClause::getLeft)); + if (node.getLeftHashSymbol().isPresent()) { + leftInputsBuilder.add(node.getLeftHashSymbol().get()); + } + Set leftInputs = leftInputsBuilder.build(); + + ImmutableSet.Builder rightInputsBuilder = ImmutableSet.builder(); + rightInputsBuilder.addAll(context.get()).addAll(Iterables.transform(node.getCriteria(), JoinNode.EquiJoinClause::getRight)); + if (node.getRightHashSymbol().isPresent()) { + rightInputsBuilder.add(node.getRightHashSymbol().get()); + } + + Set rightInputs = rightInputsBuilder.build(); + + PlanNode left = context.rewrite(node.getLeft(), leftInputs); + PlanNode right = context.rewrite(node.getRight(), rightInputs); + + return new JoinNode(node.getId(), node.getType(), left, right, node.getCriteria(), node.getLeftHashSymbol(), node.getRightHashSymbol()); + } + + @Override + public PlanNode visitSemiJoin(SemiJoinNode node, RewriteContext> context) + { + ImmutableSet.Builder sourceInputsBuilder = ImmutableSet.builder(); + sourceInputsBuilder.addAll(context.get()).add(node.getSourceJoinSymbol()); + if (node.getSourceHashSymbol().isPresent()) { + sourceInputsBuilder.add(node.getSourceHashSymbol().get()); + } + Set sourceInputs = sourceInputsBuilder.build(); + + ImmutableSet.Builder filteringSourceInputBuilder = ImmutableSet.builder(); + filteringSourceInputBuilder.add(node.getFilteringSourceJoinSymbol()); + if (node.getFilteringSourceHashSymbol().isPresent()) { + filteringSourceInputBuilder.add(node.getFilteringSourceHashSymbol().get()); + } + Set filteringSourceInputs = filteringSourceInputBuilder.build(); + + PlanNode source = context.rewrite(node.getSource(), sourceInputs); + PlanNode filteringSource = context.rewrite(node.getFilteringSource(), filteringSourceInputs); + + return new SemiJoinNode(node.getId(), + source, + filteringSource, + node.getSourceJoinSymbol(), + node.getFilteringSourceJoinSymbol(), + node.getSemiJoinOutput(), + node.getSourceHashSymbol(), + node.getFilteringSourceHashSymbol()); + } + + @Override + public PlanNode visitIndexJoin(IndexJoinNode node, RewriteContext> context) + { + ImmutableSet.Builder probeInputsBuilder = ImmutableSet.builder(); + probeInputsBuilder.addAll(context.get()) + .addAll(Iterables.transform(node.getCriteria(), IndexJoinNode.EquiJoinClause::getProbe)); + if (node.getProbeHashSymbol().isPresent()) { + probeInputsBuilder.add(node.getProbeHashSymbol().get()); + } + Set probeInputs = probeInputsBuilder.build(); + + ImmutableSet.Builder indexInputBuilder = ImmutableSet.builder(); + indexInputBuilder.addAll(context.get()) + .addAll(Iterables.transform(node.getCriteria(), IndexJoinNode.EquiJoinClause::getIndex)); + if (node.getIndexHashSymbol().isPresent()) { + indexInputBuilder.add(node.getIndexHashSymbol().get()); + } + Set indexInputs = indexInputBuilder.build(); + + PlanNode probeSource = context.rewrite(node.getProbeSource(), probeInputs); + PlanNode indexSource = context.rewrite(node.getIndexSource(), indexInputs); + + return new IndexJoinNode(node.getId(), node.getType(), probeSource, indexSource, node.getCriteria(), node.getProbeHashSymbol(), node.getIndexHashSymbol()); + } + + @Override + public PlanNode visitIndexSource(IndexSourceNode node, RewriteContext> context) + { + List newOutputSymbols = FluentIterable.from(node.getOutputSymbols()) + .filter(in(context.get())) + .toList(); + + Set newLookupSymbols = FluentIterable.from(node.getLookupSymbols()) + .filter(in(context.get())) + .toSet(); + + Set requiredAssignmentSymbols = context.get(); + if (!node.getEffectiveTupleDomain().isNone()) { + Set requiredSymbols = Maps.filterValues(node.getAssignments(), in(node.getEffectiveTupleDomain().getDomains().keySet())).keySet(); + requiredAssignmentSymbols = Sets.union(context.get(), requiredSymbols); + } + Map newAssignments = Maps.filterKeys(node.getAssignments(), in(requiredAssignmentSymbols)); + + return new IndexSourceNode(node.getId(), node.getIndexHandle(), node.getTableHandle(), newLookupSymbols, newOutputSymbols, newAssignments, node.getEffectiveTupleDomain()); + } + + @Override + public PlanNode visitAggregation(AggregationNode node, RewriteContext> context) + { + ImmutableSet.Builder expectedInputs = ImmutableSet.builder() + .addAll(node.getGroupBy()); + if (node.getHashSymbol().isPresent()) { + expectedInputs.add(node.getHashSymbol().get()); + } + + ImmutableMap.Builder functions = ImmutableMap.builder(); + ImmutableMap.Builder functionCalls = ImmutableMap.builder(); + ImmutableMap.Builder masks = ImmutableMap.builder(); + for (Map.Entry entry : node.getAggregations().entrySet()) { + Symbol symbol = entry.getKey(); + + if (context.get().contains(symbol)) { + FunctionCall call = entry.getValue(); + expectedInputs.addAll(DependencyExtractor.extractUnique(call)); + if (node.getMasks().containsKey(symbol)) { + expectedInputs.add(node.getMasks().get(symbol)); + masks.put(symbol, node.getMasks().get(symbol)); + } + + functionCalls.put(symbol, call); + functions.put(symbol, node.getFunctions().get(symbol)); + } + } + if (node.getSampleWeight().isPresent()) { + expectedInputs.add(node.getSampleWeight().get()); + } + + PlanNode source = context.rewrite(node.getSource(), expectedInputs.build()); + + return new AggregationNode(node.getId(), + source, + node.getGroupBy(), + functionCalls.build(), + functions.build(), + masks.build(), + node.getStep(), + node.getSampleWeight(), + node.getConfidence(), + node.getHashSymbol()); + } + + @Override + public PlanNode visitWindow(WindowNode node, RewriteContext> context) + { + ImmutableSet.Builder expectedInputs = ImmutableSet.builder() + .addAll(context.get()) + .addAll(node.getPartitionBy()) + .addAll(node.getOrderBy()); + + if (node.getFrame().getStartValue().isPresent()) { + expectedInputs.add(node.getFrame().getStartValue().get()); + } + if (node.getFrame().getEndValue().isPresent()) { + expectedInputs.add(node.getFrame().getEndValue().get()); + } + + if (node.getHashSymbol().isPresent()) { + expectedInputs.add(node.getHashSymbol().get()); + } + + ImmutableMap.Builder functions = ImmutableMap.builder(); + ImmutableMap.Builder functionCalls = ImmutableMap.builder(); + for (Map.Entry entry : node.getWindowFunctions().entrySet()) { + Symbol symbol = entry.getKey(); + + if (context.get().contains(symbol)) { + FunctionCall call = entry.getValue(); + expectedInputs.addAll(DependencyExtractor.extractUnique(call)); + + functionCalls.put(symbol, call); + functions.put(symbol, node.getSignatures().get(symbol)); + } + } + + PlanNode source = context.rewrite(node.getSource(), expectedInputs.build()); + + return new WindowNode( + node.getId(), + source, + node.getPartitionBy(), + node.getOrderBy(), + node.getOrderings(), + node.getFrame(), + functionCalls.build(), + functions.build(), + node.getHashSymbol(), + node.getPrePartitionedInputs(), + node.getPreSortedOrderPrefix()); + } + + @Override + public PlanNode visitTableScan(TableScanNode node, RewriteContext> context) + { + Set requiredTableScanOutputs = FluentIterable.from(context.get()) + .filter(in(ImmutableSet.copyOf(node.getOutputSymbols()))) + .toSet(); + + List newOutputSymbols = FluentIterable.from(node.getOutputSymbols()) + .filter(in(requiredTableScanOutputs)) + .toList(); + + Map newAssignments = Maps.filterKeys(node.getAssignments(), in(requiredTableScanOutputs)); + + return new TableScanNode( + node.getId(), + node.getTable(), + newOutputSymbols, + newAssignments, + node.getLayout(), + node.getCurrentConstraint(), + node.getOriginalConstraint()); + } + + @Override + public PlanNode visitFilter(FilterNode node, RewriteContext> context) + { + Set expectedInputs = ImmutableSet.builder() + .addAll(DependencyExtractor.extractUnique(node.getPredicate())) + .addAll(context.get()) + .build(); + + PlanNode source = context.rewrite(node.getSource(), expectedInputs); + + return new FilterNode(node.getId(), source, node.getPredicate()); + } + + @Override + public PlanNode visitMarkDistinct(MarkDistinctNode node, RewriteContext> context) + { + if (!context.get().contains(node.getMarkerSymbol())) { + return context.rewrite(node.getSource(), context.get()); + } + + ImmutableSet.Builder expectedInputs = ImmutableSet.builder() + .addAll(node.getDistinctSymbols()) + .addAll(context.get()); + + if (node.getHashSymbol().isPresent()) { + expectedInputs.add(node.getHashSymbol().get()); + } + PlanNode source = context.rewrite(node.getSource(), expectedInputs.build()); + + return new MarkDistinctNode(node.getId(), source, node.getMarkerSymbol(), node.getDistinctSymbols(), node.getHashSymbol()); + } + + @Override + public PlanNode visitUnnest(UnnestNode node, RewriteContext> context) + { + List replicateSymbols = FluentIterable.from(node.getReplicateSymbols()) + .filter(in(context.get())) + .toList(); + Optional ordinalitySymbol = node.getOrdinalitySymbol(); + if (ordinalitySymbol.isPresent() && !context.get().contains(ordinalitySymbol.get())) { + ordinalitySymbol = Optional.empty(); + } + Map> unnestSymbols = node.getUnnestSymbols(); + ImmutableSet.Builder expectedInputs = ImmutableSet.builder() + .addAll(replicateSymbols) + .addAll(unnestSymbols.keySet()); + + PlanNode source = context.rewrite(node.getSource(), expectedInputs.build()); + return new UnnestNode(node.getId(), source, replicateSymbols, unnestSymbols, ordinalitySymbol); + } + + @Override + public PlanNode visitProject(ProjectNode node, RewriteContext> context) + { + ImmutableSet.Builder expectedInputs = ImmutableSet.builder(); + + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (int i = 0; i < node.getOutputSymbols().size(); i++) { + Symbol output = node.getOutputSymbols().get(i); + Expression expression = node.getExpressions().get(i); + + if (context.get().contains(output)) { + expectedInputs.addAll(DependencyExtractor.extractUnique(expression)); + builder.put(output, expression); + } + } + + PlanNode source = context.rewrite(node.getSource(), expectedInputs.build()); + + return new ProjectNode(node.getId(), source, builder.build()); + } + + @Override + public PlanNode visitOutput(OutputNode node, RewriteContext> context) + { + Set expectedInputs = ImmutableSet.copyOf(node.getOutputSymbols()); + PlanNode source = context.rewrite(node.getSource(), expectedInputs); + return new OutputNode(node.getId(), source, node.getColumnNames(), node.getOutputSymbols()); + } + + @Override + public PlanNode visitLimit(LimitNode node, RewriteContext> context) + { + ImmutableSet.Builder expectedInputs = ImmutableSet.builder() + .addAll(context.get()); + PlanNode source = context.rewrite(node.getSource(), expectedInputs.build()); + return new LimitNode(node.getId(), source, node.getCount()); + } + + @Override + public PlanNode visitDistinctLimit(DistinctLimitNode node, RewriteContext> context) + { + Set expectedInputs; + if (node.getHashSymbol().isPresent()) { + expectedInputs = ImmutableSet.copyOf(concat(node.getOutputSymbols(), ImmutableList.of(node.getHashSymbol().get()))); + } + else { + expectedInputs = ImmutableSet.copyOf(node.getOutputSymbols()); + } + PlanNode source = context.rewrite(node.getSource(), expectedInputs); + return new DistinctLimitNode(node.getId(), source, node.getLimit(), node.getHashSymbol()); + } + + @Override + public PlanNode visitTopN(TopNNode node, RewriteContext> context) + { + ImmutableSet.Builder expectedInputs = ImmutableSet.builder() + .addAll(context.get()) + .addAll(node.getOrderBy()); + + PlanNode source = context.rewrite(node.getSource(), expectedInputs.build()); + + return new TopNNode(node.getId(), source, node.getCount(), node.getOrderBy(), node.getOrderings(), node.isPartial()); + } + + @Override + public PlanNode visitRowNumber(RowNumberNode node, RewriteContext> context) + { + ImmutableSet.Builder inputsBuilder = ImmutableSet.builder(); + ImmutableSet.Builder expectedInputs = inputsBuilder + .addAll(context.get()) + .addAll(node.getPartitionBy()); + + if (node.getHashSymbol().isPresent()) { + inputsBuilder.add(node.getHashSymbol().get()); + } + PlanNode source = context.rewrite(node.getSource(), expectedInputs.build()); + + return new RowNumberNode(node.getId(), source, node.getPartitionBy(), node.getRowNumberSymbol(), node.getMaxRowCountPerPartition(), node.getHashSymbol()); + } + + @Override + public PlanNode visitTopNRowNumber(TopNRowNumberNode node, RewriteContext> context) + { + ImmutableSet.Builder expectedInputs = ImmutableSet.builder() + .addAll(context.get()) + .addAll(node.getPartitionBy()) + .addAll(node.getOrderBy()); + + if (node.getHashSymbol().isPresent()) { + expectedInputs.add(node.getHashSymbol().get()); + } + PlanNode source = context.rewrite(node.getSource(), expectedInputs.build()); + + return new TopNRowNumberNode(node.getId(), + source, + node.getPartitionBy(), + node.getOrderBy(), + node.getOrderings(), + node.getRowNumberSymbol(), + node.getMaxRowCountPerPartition(), + node.isPartial(), + node.getHashSymbol()); + } + + @Override + public PlanNode visitSort(SortNode node, RewriteContext> context) + { + Set expectedInputs = ImmutableSet.copyOf(concat(context.get(), node.getOrderBy())); + + PlanNode source = context.rewrite(node.getSource(), expectedInputs); + + return new SortNode(node.getId(), source, node.getOrderBy(), node.getOrderings()); + } + + @Override + public PlanNode visitTableWriter(TableWriterNode node, RewriteContext> context) + { + ImmutableSet.Builder expectedInputs = ImmutableSet.builder() + .addAll(node.getColumns()); + if (node.getSampleWeightSymbol().isPresent()) { + expectedInputs.add(node.getSampleWeightSymbol().get()); + } + PlanNode source = context.rewrite(node.getSource(), expectedInputs.build()); + + return new TableWriterNode(node.getId(), source, node.getTarget(), node.getColumns(), node.getColumnNames(), node.getOutputSymbols(), node.getSampleWeightSymbol()); + } + + @Override + public PlanNode visitTableCommit(TableCommitNode node, RewriteContext> context) + { + // Maintain the existing inputs needed for TableCommitNode + PlanNode source = context.rewrite(node.getSource(), ImmutableSet.copyOf(node.getSource().getOutputSymbols())); + return new TableCommitNode(node.getId(), source, node.getTarget(), node.getOutputSymbols()); + } + + @Override + public PlanNode visitDelete(DeleteNode node, RewriteContext> context) + { + PlanNode source = context.rewrite(node.getSource(), ImmutableSet.of(node.getRowId())); + return new DeleteNode(node.getId(), source, node.getTarget(), node.getRowId(), node.getOutputSymbols()); + } + + @Override + public PlanNode visitUnion(UnionNode node, RewriteContext> context) + { + // Find out which output symbols we need to keep + ImmutableListMultimap.Builder rewrittenSymbolMappingBuilder = ImmutableListMultimap.builder(); + for (Symbol symbol : node.getOutputSymbols()) { + if (context.get().contains(symbol)) { + rewrittenSymbolMappingBuilder.putAll(symbol, node.getSymbolMapping().get(symbol)); + } + } + ListMultimap rewrittenSymbolMapping = rewrittenSymbolMappingBuilder.build(); + + // Find the corresponding input symbol to the remaining output symbols and prune the subplans + ImmutableList.Builder rewrittenSubPlans = ImmutableList.builder(); + for (int i = 0; i < node.getSources().size(); i++) { + ImmutableSet.Builder expectedInputSymbols = ImmutableSet.builder(); + for (Collection symbols : rewrittenSymbolMapping.asMap().values()) { + expectedInputSymbols.add(Iterables.get(symbols, i)); + } + rewrittenSubPlans.add(context.rewrite(node.getSources().get(i), expectedInputSymbols.build())); + } + + return new UnionNode(node.getId(), rewrittenSubPlans.build(), rewrittenSymbolMapping); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/SetFlatteningOptimizer.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/SetFlatteningOptimizer.java new file mode 100644 index 00000000..86b35482 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/SetFlatteningOptimizer.java @@ -0,0 +1,117 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.optimizations; + +import com.facebook.presto.Session; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.planner.PlanNodeIdAllocator; +import com.facebook.presto.sql.planner.Symbol; +import com.facebook.presto.sql.planner.SymbolAllocator; +import com.facebook.presto.sql.planner.plan.AggregationNode; +import com.facebook.presto.sql.planner.plan.PlanNode; +import com.facebook.presto.sql.planner.plan.PlanRewriter; +import com.facebook.presto.sql.planner.plan.UnionNode; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.Iterables; + +import java.util.Collection; +import java.util.Map; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class SetFlatteningOptimizer + extends PlanOptimizer +{ + @Override + public PlanNode optimize(PlanNode plan, Session session, Map types, SymbolAllocator symbolAllocator, PlanNodeIdAllocator idAllocator) + { + checkNotNull(plan, "plan is null"); + checkNotNull(session, "session is null"); + checkNotNull(types, "types is null"); + checkNotNull(symbolAllocator, "symbolAllocator is null"); + checkNotNull(idAllocator, "idAllocator is null"); + + return PlanRewriter.rewriteWith(new Rewriter(), plan, false); + } + + // TODO: remove expectation that UNION DISTINCT => distinct aggregation directly above union node + private static class Rewriter + extends PlanRewriter + { + @Override + public PlanNode visitPlan(PlanNode node, RewriteContext context) + { + return context.defaultRewrite(node, false); + } + + @Override + public PlanNode visitUnion(UnionNode node, RewriteContext context) + { + ImmutableList.Builder flattenedSources = ImmutableList.builder(); + ImmutableListMultimap.Builder flattenedSymbolMap = ImmutableListMultimap.builder(); + for (int i = 0; i < node.getSources().size(); i++) { + PlanNode subplan = node.getSources().get(i); + PlanNode rewrittenSource = context.rewrite(subplan, context.get()); + + if (rewrittenSource instanceof UnionNode) { + // Absorb source's subplans if it is also a UnionNode + UnionNode rewrittenUnion = (UnionNode) rewrittenSource; + flattenedSources.addAll(rewrittenUnion.getSources()); + for (Map.Entry> entry : node.getSymbolMapping().asMap().entrySet()) { + Symbol inputSymbol = Iterables.get(entry.getValue(), i); + flattenedSymbolMap.putAll(entry.getKey(), rewrittenUnion.getSymbolMapping().get(inputSymbol)); + } + } + else { + flattenedSources.add(rewrittenSource); + for (Map.Entry> entry : node.getSymbolMapping().asMap().entrySet()) { + flattenedSymbolMap.put(entry.getKey(), Iterables.get(entry.getValue(), i)); + } + } + } + return new UnionNode(node.getId(), flattenedSources.build(), flattenedSymbolMap.build()); + } + + @Override + public PlanNode visitAggregation(AggregationNode node, RewriteContext context) + { + boolean distinct = isDistinctOperator(node); + + PlanNode rewrittenNode = context.rewrite(node.getSource(), distinct); + + if (context.get() && distinct) { + // Assumes underlying node has same output symbols as this distinct node + return rewrittenNode; + } + + return new AggregationNode( + node.getId(), + rewrittenNode, + node.getGroupBy(), + node.getAggregations(), + node.getFunctions(), + node.getMasks(), + node.getStep(), + node.getSampleWeight(), + node.getConfidence(), + node.getHashSymbol()); + } + + private static boolean isDistinctOperator(AggregationNode node) + { + return node.getAggregations().isEmpty(); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/SimplifyExpressions.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/SimplifyExpressions.java new file mode 100644 index 00000000..2e129450 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/SimplifyExpressions.java @@ -0,0 +1,125 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.optimizations; + +import com.facebook.presto.Session; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.parser.SqlParser; +import com.facebook.presto.sql.planner.ExpressionInterpreter; +import com.facebook.presto.sql.planner.LiteralInterpreter; +import com.facebook.presto.sql.planner.NoOpSymbolResolver; +import com.facebook.presto.sql.planner.PlanNodeIdAllocator; +import com.facebook.presto.sql.planner.Symbol; +import com.facebook.presto.sql.planner.SymbolAllocator; +import com.facebook.presto.sql.planner.plan.FilterNode; +import com.facebook.presto.sql.planner.plan.PlanNode; +import com.facebook.presto.sql.planner.plan.PlanRewriter; +import com.facebook.presto.sql.planner.plan.ProjectNode; +import com.facebook.presto.sql.planner.plan.TableScanNode; +import com.facebook.presto.sql.tree.BooleanLiteral; +import com.facebook.presto.sql.tree.Expression; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; + +import java.util.IdentityHashMap; +import java.util.Map; + +import static com.facebook.presto.sql.analyzer.ExpressionAnalyzer.getExpressionTypes; +import static com.google.common.base.Preconditions.checkNotNull; + +public class SimplifyExpressions + extends PlanOptimizer +{ + private final Metadata metadata; + private final SqlParser sqlParser; + + public SimplifyExpressions(Metadata metadata, SqlParser sqlParser) + { + this.metadata = checkNotNull(metadata, "metadata is null"); + this.sqlParser = checkNotNull(sqlParser, "sqlParser is null"); + } + + @Override + public PlanNode optimize(PlanNode plan, Session session, Map types, SymbolAllocator symbolAllocator, PlanNodeIdAllocator idAllocator) + { + checkNotNull(plan, "plan is null"); + checkNotNull(session, "session is null"); + checkNotNull(types, "types is null"); + checkNotNull(symbolAllocator, "symbolAllocator is null"); + checkNotNull(idAllocator, "idAllocator is null"); + + return PlanRewriter.rewriteWith(new Rewriter(metadata, sqlParser, session, types), plan); + } + + private static class Rewriter + extends PlanRewriter + { + private final Metadata metadata; + private final SqlParser sqlParser; + private final Session session; + private final Map types; + + public Rewriter(Metadata metadata, SqlParser sqlParser, Session session, Map types) + { + this.metadata = metadata; + this.sqlParser = sqlParser; + this.session = session; + this.types = types; + } + + @Override + public PlanNode visitProject(ProjectNode node, RewriteContext context) + { + PlanNode source = context.rewrite(node.getSource()); + Map assignments = ImmutableMap.copyOf(Maps.transformValues(node.getAssignments(), this::simplifyExpression)); + return new ProjectNode(node.getId(), source, assignments); + } + + @Override + public PlanNode visitFilter(FilterNode node, RewriteContext context) + { + PlanNode source = context.rewrite(node.getSource()); + Expression simplified = simplifyExpression(node.getPredicate()); + if (simplified.equals(BooleanLiteral.TRUE_LITERAL)) { + return source; + } + return new FilterNode(node.getId(), source, simplified); + } + + @Override + public PlanNode visitTableScan(TableScanNode node, RewriteContext context) + { + Expression originalConstraint = null; + if (node.getOriginalConstraint() != null) { + originalConstraint = simplifyExpression(node.getOriginalConstraint()); + } + return new TableScanNode( + node.getId(), + node.getTable(), + node.getOutputSymbols(), + node.getAssignments(), + node.getLayout(), + node.getCurrentConstraint(), + originalConstraint); + } + + private Expression simplifyExpression(Expression input) + { + IdentityHashMap expressionTypes = getExpressionTypes(session, metadata, sqlParser, types, input); + ExpressionInterpreter interpreter = ExpressionInterpreter.expressionOptimizer(input, metadata, session, expressionTypes); + return LiteralInterpreter.toExpression(interpreter.optimize(NoOpSymbolResolver.INSTANCE), expressionTypes.get(input)); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/SingleDistinctOptimizer.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/SingleDistinctOptimizer.java new file mode 100644 index 00000000..a7e18c24 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/SingleDistinctOptimizer.java @@ -0,0 +1,133 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.optimizations; + +import com.facebook.presto.Session; +import com.facebook.presto.sql.planner.PlanNodeIdAllocator; +import com.facebook.presto.sql.planner.Symbol; +import com.facebook.presto.sql.planner.SymbolAllocator; +import com.facebook.presto.sql.planner.plan.AggregationNode; +import com.facebook.presto.sql.planner.plan.PlanNode; +import com.facebook.presto.sql.planner.plan.MarkDistinctNode; +import com.facebook.presto.sql.planner.plan.PlanRewriter; +import com.facebook.presto.sql.planner.plan.ProjectNode; +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.FunctionCall; +import com.facebook.presto.sql.tree.NullLiteral; +import com.facebook.presto.sql.tree.QualifiedNameReference; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.collect.Maps; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.Optional; + +import static com.facebook.presto.sql.planner.plan.AggregationNode.Step.SINGLE; +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Converts Single Distinct Aggregation into GroupBy + * + * Rewrite if and only if + * 1 all aggregation functions have a single common distinct mask symbol + * 2 all aggregation functions have mask + * + * Rewrite MarkDistinctNode into AggregationNode(use DistinctSymbols as GroupBy) + * Add ProjectNode on top of the new AggregationNode, which adds null assignment for mask + * All unused mask will be removed by PruneUnreferencedOutputs + * Remove Distincts in the original AggregationNode + */ +public class SingleDistinctOptimizer + extends PlanOptimizer +{ + @Override + public PlanNode optimize(PlanNode plan, Session session, Map types, SymbolAllocator symbolAllocator, PlanNodeIdAllocator idAllocator) + { + return PlanRewriter.rewriteWith(new Optimizer(idAllocator), plan, Optional.empty()); + } + + private static class Optimizer + extends PlanRewriter> + { + private final PlanNodeIdAllocator idAllocator; + + private Optimizer(PlanNodeIdAllocator idAllocator) + { + this.idAllocator = checkNotNull(idAllocator, "idAllocator is null"); + } + + @Override + public PlanNode visitAggregation(AggregationNode node, RewriteContext> context) + { + // optimize if and only if + // all aggregation functions have a single common distinct mask symbol + // AND all aggregation functions have mask + Set masks = ImmutableSet.copyOf(node.getMasks().values()); + if (masks.size() != 1 || node.getMasks().size() != node.getAggregations().size()) { + return context.defaultRewrite(node, Optional.empty()); + } + PlanNode source = context.rewrite(node.getSource(), Optional.of(Iterables.getOnlyElement(masks))); + + Map aggregations = ImmutableMap.copyOf(Maps.transformValues(node.getAggregations(), call -> new FunctionCall(call.getName(), call.getWindow(), false, call.getArguments()))); + + return new AggregationNode(idAllocator.getNextId(), + source, + node.getGroupBy(), + aggregations, + node.getFunctions(), + Collections.emptyMap(), + node.getStep(), + node.getSampleWeight(), + node.getConfidence(), + node.getHashSymbol()); + } + + @Override + public PlanNode visitMarkDistinct(MarkDistinctNode node, RewriteContext> context) + { + Optional mask = context.get(); + if (mask.isPresent() && mask.get().equals(node.getMarkerSymbol())) { + // rewrite Distinct into GroupBy + AggregationNode aggregationNode = new AggregationNode(idAllocator.getNextId(), + context.rewrite(node.getSource(), Optional.empty()), + node.getDistinctSymbols(), + Collections.emptyMap(), + Collections.emptyMap(), + Collections.emptyMap(), + SINGLE, + Optional.empty(), + 1.0, + node.getHashSymbol()); + + ImmutableMap.Builder outputSymbols = ImmutableMap.builder(); + for (Symbol symbol : aggregationNode.getOutputSymbols()) { + Expression expression = new QualifiedNameReference(symbol.toQualifiedName()); + outputSymbols.put(symbol, expression); + } + + // add null assignment for mask + // unused mask will be removed by PruneUnreferencedOutputs + outputSymbols.put(mask.get(), new NullLiteral()); + return new ProjectNode(idAllocator.getNextId(), + aggregationNode, + outputSymbols.build()); + } + return context.defaultRewrite(node, Optional.empty()); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/UnaliasSymbolReferences.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/UnaliasSymbolReferences.java new file mode 100644 index 00000000..320729f6 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/UnaliasSymbolReferences.java @@ -0,0 +1,442 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.optimizations; + +import com.facebook.presto.Session; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.spi.block.SortOrder; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.planner.DeterminismEvaluator; +import com.facebook.presto.sql.planner.PlanNodeIdAllocator; +import com.facebook.presto.sql.planner.Symbol; +import com.facebook.presto.sql.planner.SymbolAllocator; +import com.facebook.presto.sql.planner.plan.AggregationNode; +import com.facebook.presto.sql.planner.plan.FilterNode; +import com.facebook.presto.sql.planner.plan.IndexJoinNode; +import com.facebook.presto.sql.planner.plan.IndexSourceNode; +import com.facebook.presto.sql.planner.plan.JoinNode; +import com.facebook.presto.sql.planner.plan.MarkDistinctNode; +import com.facebook.presto.sql.planner.plan.OutputNode; +import com.facebook.presto.sql.planner.plan.PlanNode; +import com.facebook.presto.sql.planner.plan.PlanRewriter; +import com.facebook.presto.sql.planner.plan.ProjectNode; +import com.facebook.presto.sql.planner.plan.SemiJoinNode; +import com.facebook.presto.sql.planner.plan.SortNode; +import com.facebook.presto.sql.planner.plan.TableScanNode; +import com.facebook.presto.sql.planner.plan.TableWriterNode; +import com.facebook.presto.sql.planner.plan.TopNNode; +import com.facebook.presto.sql.planner.plan.UnionNode; +import com.facebook.presto.sql.planner.plan.UnnestNode; +import com.facebook.presto.sql.planner.plan.WindowNode; +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.ExpressionRewriter; +import com.facebook.presto.sql.tree.ExpressionTreeRewriter; +import com.facebook.presto.sql.tree.FunctionCall; +import com.facebook.presto.sql.tree.NullLiteral; +import com.facebook.presto.sql.tree.QualifiedNameReference; +import com.google.common.base.Preconditions; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.collect.ListMultimap; +import com.google.common.collect.Lists; + +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Re-maps symbol references that are just aliases of each other (e.g., due to projections like {@code $0 := $1}) + *

+ * E.g., + *

+ * {@code Output[$0, $1] -> Project[$0 := $2, $1 := $3 * 100] -> Aggregate[$2, $3 := sum($4)] -> ...} + *

+ * gets rewritten as + *

+ * {@code Output[$2, $1] -> Project[$2, $1 := $3 * 100] -> Aggregate[$2, $3 := sum($4)] -> ...} + */ +public class UnaliasSymbolReferences + extends PlanOptimizer +{ + @Override + public PlanNode optimize(PlanNode plan, Session session, Map types, SymbolAllocator symbolAllocator, PlanNodeIdAllocator idAllocator) + { + checkNotNull(plan, "plan is null"); + checkNotNull(session, "session is null"); + checkNotNull(types, "types is null"); + checkNotNull(symbolAllocator, "symbolAllocator is null"); + checkNotNull(idAllocator, "idAllocator is null"); + + return PlanRewriter.rewriteWith(new Rewriter(new HashMap()), plan); + } + + private static class Rewriter + extends PlanRewriter + { + private final Map mapping; + + public Rewriter(Map mapping) + { + this.mapping = mapping; + } + + @Override + public PlanNode visitAggregation(AggregationNode node, RewriteContext context) + { + PlanNode source = context.rewrite(node.getSource()); + + ImmutableMap.Builder functionInfos = ImmutableMap.builder(); + ImmutableMap.Builder functionCalls = ImmutableMap.builder(); + ImmutableMap.Builder masks = ImmutableMap.builder(); + for (Map.Entry entry : node.getAggregations().entrySet()) { + Symbol symbol = entry.getKey(); + Symbol canonical = canonicalize(symbol); + FunctionCall canonicalCall = (FunctionCall) canonicalize(entry.getValue()); + functionCalls.put(canonical, canonicalCall); + functionInfos.put(canonical, node.getFunctions().get(symbol)); + } + for (Map.Entry entry : node.getMasks().entrySet()) { + masks.put(canonicalize(entry.getKey()), canonicalize(entry.getValue())); + } + + List groupByKeys = ImmutableList.copyOf(ImmutableSet.copyOf(canonicalize(node.getGroupBy()))); + return new AggregationNode( + node.getId(), + source, + groupByKeys, + functionCalls.build(), + functionInfos.build(), + masks.build(), + node.getStep(), + canonicalize(node.getSampleWeight()), + node.getConfidence(), + node.getHashSymbol()); + } + + @Override + public PlanNode visitMarkDistinct(MarkDistinctNode node, RewriteContext context) + { + PlanNode source = context.rewrite(node.getSource()); + List symbols = ImmutableList.copyOf(ImmutableSet.copyOf(canonicalize(node.getDistinctSymbols()))); + return new MarkDistinctNode(node.getId(), source, canonicalize(node.getMarkerSymbol()), symbols, node.getHashSymbol()); + } + + @Override + public PlanNode visitUnnest(UnnestNode node, RewriteContext context) + { + PlanNode source = context.rewrite(node.getSource()); + ImmutableMap.Builder> builder = ImmutableMap.builder(); + for (Map.Entry> entry : node.getUnnestSymbols().entrySet()) { + builder.put(canonicalize(entry.getKey()), canonicalize(entry.getValue())); + } + return new UnnestNode(node.getId(), source, canonicalize(node.getReplicateSymbols()), builder.build(), canonicalize(node.getOrdinalitySymbol())); + } + + @Override + public PlanNode visitWindow(WindowNode node, RewriteContext context) + { + PlanNode source = context.rewrite(node.getSource()); + + ImmutableMap.Builder functionInfos = ImmutableMap.builder(); + ImmutableMap.Builder functionCalls = ImmutableMap.builder(); + for (Map.Entry entry : node.getWindowFunctions().entrySet()) { + Symbol symbol = entry.getKey(); + Symbol canonical = canonicalize(symbol); + functionCalls.put(canonical, (FunctionCall) canonicalize(entry.getValue())); + functionInfos.put(canonical, node.getSignatures().get(symbol)); + } + + ImmutableMap.Builder orderings = ImmutableMap.builder(); + for (Map.Entry entry : node.getOrderings().entrySet()) { + orderings.put(canonicalize(entry.getKey()), entry.getValue()); + } + + WindowNode.Frame frame = node.getFrame(); + frame = new WindowNode.Frame(frame.getType(), + frame.getStartType(), canonicalize(frame.getStartValue()), + frame.getEndType(), canonicalize(frame.getEndValue())); + + return new WindowNode( + node.getId(), + source, + canonicalize(node.getPartitionBy()), + canonicalize(node.getOrderBy()), + orderings.build(), + frame, + functionCalls.build(), + functionInfos.build(), + node.getHashSymbol().map(this::canonicalize), + canonicalize(node.getPrePartitionedInputs()), + node.getPreSortedOrderPrefix()); + } + + @Override + public PlanNode visitTableScan(TableScanNode node, RewriteContext context) + { + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (Map.Entry entry : node.getAssignments().entrySet()) { + builder.put(canonicalize(entry.getKey()), entry.getValue()); + } + + Expression originalConstraint = null; + if (node.getOriginalConstraint() != null) { + originalConstraint = canonicalize(node.getOriginalConstraint()); + } + return new TableScanNode( + node.getId(), + node.getTable(), + canonicalize(node.getOutputSymbols()), + builder.build(), + node.getLayout(), + node.getCurrentConstraint(), + originalConstraint); + } + + @Override + public PlanNode visitFilter(FilterNode node, RewriteContext context) + { + PlanNode source = context.rewrite(node.getSource()); + + return new FilterNode(node.getId(), source, canonicalize(node.getPredicate())); + } + + @Override + public PlanNode visitProject(ProjectNode node, RewriteContext context) + { + PlanNode source = context.rewrite(node.getSource()); + + Map computedExpressions = new HashMap<>(); + + Map assignments = new LinkedHashMap<>(); + for (Map.Entry entry : node.getAssignments().entrySet()) { + Expression expression = canonicalize(entry.getValue()); + + if (entry.getValue() instanceof QualifiedNameReference) { + // Always map a trivial symbol projection + Symbol symbol = Symbol.fromQualifiedName(((QualifiedNameReference) entry.getValue()).getName()); + if (!symbol.equals(entry.getKey())) { + map(entry.getKey(), symbol); + } + } + else if (DeterminismEvaluator.isDeterministic(expression) && !(expression instanceof NullLiteral)) { + // Try to map same deterministic expressions within a projection into the same symbol + // Omit NullLiterals since those have ambiguous types + Symbol computedSymbol = computedExpressions.get(expression); + if (computedSymbol == null) { + // If we haven't seen the expression before in this projection, record it + computedExpressions.put(expression, entry.getKey()); + } + else { + // If we have seen the expression before and if it is deterministic + // then we can rewrite references to the current symbol in terms of the parallel computedSymbol in the projection + map(entry.getKey(), computedSymbol); + } + } + + Symbol canonical = canonicalize(entry.getKey()); + + if (!assignments.containsKey(canonical)) { + assignments.put(canonical, expression); + } + } + + return new ProjectNode(node.getId(), source, assignments); + } + + @Override + public PlanNode visitOutput(OutputNode node, RewriteContext context) + { + PlanNode source = context.rewrite(node.getSource()); + + List canonical = Lists.transform(node.getOutputSymbols(), this::canonicalize); + return new OutputNode(node.getId(), source, node.getColumnNames(), canonical); + } + + @Override + public PlanNode visitTopN(TopNNode node, RewriteContext context) + { + PlanNode source = context.rewrite(node.getSource()); + + ImmutableList.Builder symbols = ImmutableList.builder(); + ImmutableMap.Builder orderings = ImmutableMap.builder(); + for (Symbol symbol : node.getOrderBy()) { + Symbol canonical = canonicalize(symbol); + symbols.add(canonical); + orderings.put(canonical, node.getOrderings().get(symbol)); + } + + return new TopNNode(node.getId(), source, node.getCount(), symbols.build(), orderings.build(), node.isPartial()); + } + + @Override + public PlanNode visitSort(SortNode node, RewriteContext context) + { + PlanNode source = context.rewrite(node.getSource()); + + ImmutableList.Builder symbols = ImmutableList.builder(); + ImmutableMap.Builder orderings = ImmutableMap.builder(); + for (Symbol symbol : node.getOrderBy()) { + Symbol canonical = canonicalize(symbol); + symbols.add(canonical); + orderings.put(canonical, node.getOrderings().get(symbol)); + } + + return new SortNode(node.getId(), source, symbols.build(), orderings.build()); + } + + @Override + public PlanNode visitJoin(JoinNode node, RewriteContext context) + { + PlanNode left = context.rewrite(node.getLeft()); + PlanNode right = context.rewrite(node.getRight()); + + return new JoinNode(node.getId(), node.getType(), left, right, canonicalizeJoinCriteria(node.getCriteria()), node.getLeftHashSymbol(), node.getRightHashSymbol()); + } + + @Override + public PlanNode visitSemiJoin(SemiJoinNode node, RewriteContext context) + { + PlanNode source = context.rewrite(node.getSource()); + PlanNode filteringSource = context.rewrite(node.getFilteringSource()); + + return new SemiJoinNode(node.getId(), source, filteringSource, canonicalize(node.getSourceJoinSymbol()), canonicalize(node.getFilteringSourceJoinSymbol()), canonicalize(node.getSemiJoinOutput()), node.getSourceHashSymbol(), node.getFilteringSourceHashSymbol()); + } + + @Override + public PlanNode visitIndexSource(IndexSourceNode node, RewriteContext context) + { + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (Map.Entry entry : node.getAssignments().entrySet()) { + builder.put(canonicalize(entry.getKey()), entry.getValue()); + } + return new IndexSourceNode(node.getId(), node.getIndexHandle(), node.getTableHandle(), canonicalize(node.getLookupSymbols()), canonicalize(node.getOutputSymbols()), builder.build(), node.getEffectiveTupleDomain()); + } + + @Override + public PlanNode visitIndexJoin(IndexJoinNode node, RewriteContext context) + { + PlanNode probeSource = context.rewrite(node.getProbeSource()); + PlanNode indexSource = context.rewrite(node.getIndexSource()); + + return new IndexJoinNode(node.getId(), node.getType(), probeSource, indexSource, canonicalizeIndexJoinCriteria(node.getCriteria()), node.getProbeHashSymbol(), node.getIndexHashSymbol()); + } + + @Override + public PlanNode visitUnion(UnionNode node, RewriteContext context) + { + ImmutableList.Builder rewrittenSources = ImmutableList.builder(); + for (PlanNode source : node.getSources()) { + rewrittenSources.add(context.rewrite(source)); + } + + return new UnionNode(node.getId(), rewrittenSources.build(), canonicalizeUnionSymbolMap(node.getSymbolMapping())); + } + + @Override + public PlanNode visitTableWriter(TableWriterNode node, RewriteContext context) + { + PlanNode source = context.rewrite(node.getSource()); + + return new TableWriterNode(node.getId(), source, node.getTarget(), canonicalize(node.getColumns()), node.getColumnNames(), canonicalize(node.getOutputSymbols()), canonicalize(node.getSampleWeightSymbol())); + } + + private void map(Symbol symbol, Symbol canonical) + { + Preconditions.checkArgument(!symbol.equals(canonical), "Can't map symbol to itself: %s", symbol); + mapping.put(symbol, canonical); + } + + private Optional canonicalize(Optional symbol) + { + if (symbol.isPresent()) { + return Optional.of(canonicalize(symbol.get())); + } + return Optional.empty(); + } + + private Symbol canonicalize(Symbol symbol) + { + Symbol canonical = symbol; + while (mapping.containsKey(canonical)) { + canonical = mapping.get(canonical); + } + return canonical; + } + + private Expression canonicalize(Expression value) + { + return ExpressionTreeRewriter.rewriteWith(new ExpressionRewriter() + { + @Override + public Expression rewriteQualifiedNameReference(QualifiedNameReference node, Void context, ExpressionTreeRewriter treeRewriter) + { + Symbol canonical = canonicalize(Symbol.fromQualifiedName(node.getName())); + return new QualifiedNameReference(canonical.toQualifiedName()); + } + }, value); + } + + private List canonicalize(List outputs) + { + return Lists.transform(outputs, this::canonicalize); + } + + private Set canonicalize(Set symbols) + { + return FluentIterable.from(symbols) + .transform(this::canonicalize) + .toSet(); + } + + private List canonicalizeJoinCriteria(List criteria) + { + ImmutableList.Builder builder = ImmutableList.builder(); + for (JoinNode.EquiJoinClause clause : criteria) { + builder.add(new JoinNode.EquiJoinClause(canonicalize(clause.getLeft()), canonicalize(clause.getRight()))); + } + + return builder.build(); + } + + private List canonicalizeIndexJoinCriteria(List criteria) + { + ImmutableList.Builder builder = ImmutableList.builder(); + for (IndexJoinNode.EquiJoinClause clause : criteria) { + builder.add(new IndexJoinNode.EquiJoinClause(canonicalize(clause.getProbe()), canonicalize(clause.getIndex()))); + } + + return builder.build(); + } + + private ListMultimap canonicalizeUnionSymbolMap(ListMultimap unionSymbolMap) + { + ImmutableListMultimap.Builder builder = ImmutableListMultimap.builder(); + for (Map.Entry> entry : unionSymbolMap.asMap().entrySet()) { + builder.putAll(canonicalize(entry.getKey()), Iterables.transform(entry.getValue(), this::canonicalize)); + } + return builder.build(); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/WindowFilterPushDown.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/WindowFilterPushDown.java new file mode 100644 index 00000000..7f5a9259 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/optimizations/WindowFilterPushDown.java @@ -0,0 +1,279 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.optimizations; + +import com.facebook.presto.Session; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.sql.planner.DependencyExtractor; +import com.facebook.presto.sql.planner.PlanNodeIdAllocator; +import com.facebook.presto.sql.planner.Symbol; +import com.facebook.presto.sql.planner.SymbolAllocator; +import com.facebook.presto.sql.planner.plan.FilterNode; +import com.facebook.presto.sql.planner.plan.LimitNode; +import com.facebook.presto.sql.planner.plan.PlanNode; +import com.facebook.presto.sql.planner.plan.PlanRewriter; +import com.facebook.presto.sql.planner.plan.RowNumberNode; +import com.facebook.presto.sql.planner.plan.TopNRowNumberNode; +import com.facebook.presto.sql.planner.plan.WindowNode; +import com.facebook.presto.sql.tree.ComparisonExpression; +import com.facebook.presto.sql.tree.DefaultExpressionTraversalVisitor; +import com.facebook.presto.sql.tree.DoubleLiteral; +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.Literal; +import com.facebook.presto.sql.tree.LongLiteral; +import com.facebook.presto.sql.tree.QualifiedNameReference; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.common.collect.Sets; + +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.Iterables.getOnlyElement; + +public class WindowFilterPushDown + extends PlanOptimizer +{ + private static final Signature ROW_NUMBER_SIGNATURE = new Signature("row_number", StandardTypes.BIGINT, ImmutableList.of()); + + @Override + public PlanNode optimize(PlanNode plan, Session session, Map types, SymbolAllocator symbolAllocator, PlanNodeIdAllocator idAllocator) + { + checkNotNull(plan, "plan is null"); + checkNotNull(session, "session is null"); + checkNotNull(types, "types is null"); + checkNotNull(symbolAllocator, "symbolAllocator is null"); + checkNotNull(idAllocator, "idAllocator is null"); + + return PlanRewriter.rewriteWith(new Rewriter(idAllocator), plan, null); + } + + private static class Rewriter + extends PlanRewriter + { + private final PlanNodeIdAllocator idAllocator; + + private Rewriter(PlanNodeIdAllocator idAllocator) + { + this.idAllocator = checkNotNull(idAllocator, "idAllocator is null"); + } + + @Override + public PlanNode visitWindow(WindowNode node, RewriteContext context) + { + if (canOptimizeWindowFunction(node)) { + PlanNode rewrittenSource = context.rewrite(node.getSource(), null); + Optional limit = getLimit(node, context.get()); + if (node.getOrderBy().isEmpty()) { + return new RowNumberNode(idAllocator.getNextId(), + rewrittenSource, + node.getPartitionBy(), + getOnlyElement(node.getWindowFunctions().keySet()), + limit, + Optional.empty()); + } + if (limit.isPresent()) { + return new TopNRowNumberNode(idAllocator.getNextId(), + rewrittenSource, + node.getPartitionBy(), + node.getOrderBy(), + node.getOrderings(), + getOnlyElement(node.getWindowFunctions().keySet()), + limit.get(), + false, + Optional.empty()); + } + } + return context.defaultRewrite(node); + } + + private static Optional getLimit(WindowNode node, Constraint filter) + { + if (filter == null || (!filter.getLimit().isPresent() && !filter.getFilterExpression().isPresent())) { + return Optional.empty(); + } + if (filter.getLimit().isPresent()) { + return filter.getLimit(); + } + if (filterContainsWindowFunctions(node, filter.getFilterExpression().get()) && + filter.getFilterExpression().get() instanceof ComparisonExpression) { + Symbol rowNumberSymbol = Iterables.getOnlyElement(node.getWindowFunctions().entrySet()).getKey(); + return WindowLimitExtractor.extract(filter.getFilterExpression().get(), rowNumberSymbol); + } + return Optional.empty(); + } + + private static boolean canOptimizeWindowFunction(WindowNode node) + { + if (node.getWindowFunctions().size() != 1) { + return false; + } + Symbol rowNumberSymbol = getOnlyElement(node.getWindowFunctions().entrySet()).getKey(); + return isRowNumberSignature(node.getSignatures().get(rowNumberSymbol)); + } + + private static boolean filterContainsWindowFunctions(WindowNode node, Expression filterPredicate) + { + Set windowFunctionSymbols = node.getWindowFunctions().keySet(); + Sets.SetView commonSymbols = Sets.intersection(DependencyExtractor.extractUnique(filterPredicate), windowFunctionSymbols); + return !commonSymbols.isEmpty(); + } + + @Override + public PlanNode visitLimit(LimitNode node, RewriteContext context) + { + // Operators can handle MAX_VALUE rows per page, so do not optimize if count is greater than this value + if (node.getCount() >= Integer.MAX_VALUE) { + return context.defaultRewrite(node); + } + Constraint constraint = new Constraint(Optional.of((int) node.getCount()), Optional.empty()); + PlanNode rewrittenSource = context.rewrite(node.getSource(), constraint); + + if (rewrittenSource != node.getSource()) { + return rewrittenSource; + } + + return context.defaultRewrite(node); + } + + @Override + public PlanNode visitFilter(FilterNode node, RewriteContext context) + { + PlanNode rewrittenSource = context.rewrite(node.getSource(), new Constraint(Optional.empty(), Optional.of(node.getPredicate()))); + if (rewrittenSource != node.getSource()) { + if (rewrittenSource instanceof TopNRowNumberNode) { + return rewrittenSource; + } + return new FilterNode(idAllocator.getNextId(), rewrittenSource, node.getPredicate()); + } + return context.defaultRewrite(node); + } + } + + private static boolean isRowNumberSignature(Signature signature) + { + return signature.equals(ROW_NUMBER_SIGNATURE); + } + + private static class Constraint + { + private final Optional limit; + private final Optional filterExpression; + + private Constraint(Optional limit, Optional filterExpression) + { + this.limit = limit; + this.filterExpression = filterExpression; + } + + public Optional getLimit() + { + return limit; + } + + public Optional getFilterExpression() + { + return filterExpression; + } + } + + private static final class WindowLimitExtractor + { + private WindowLimitExtractor() {} + + public static Optional extract(Expression expression, Symbol rowNumberSymbol) + { + Visitor visitor = new Visitor(); + Long limit = visitor.process(expression, rowNumberSymbol); + if (limit == null || limit >= Integer.MAX_VALUE) { + return Optional.empty(); + } + + return Optional.of(limit.intValue()); + } + + private static class Visitor + extends DefaultExpressionTraversalVisitor + { + @Override + protected Long visitComparisonExpression(ComparisonExpression node, Symbol rowNumberSymbol) + { + Optional reference = extractReference(node); + Optional literal = extractLiteral(node); + if (!reference.isPresent() || !literal.isPresent()) { + return null; + } + if (!Symbol.fromQualifiedName(reference.get().getName()).equals(rowNumberSymbol)) { + return null; + } + + long literalValue = extractValue(literal.get()); + if (node.getLeft() instanceof QualifiedNameReference && node.getRight() instanceof Literal) { + if (node.getType() == ComparisonExpression.Type.LESS_THAN_OR_EQUAL) { + return literalValue; + } + if (node.getType() == ComparisonExpression.Type.LESS_THAN) { + return literalValue - 1; + } + } + else if (node.getLeft() instanceof Literal && node.getRight() instanceof QualifiedNameReference) { + if (node.getType() == ComparisonExpression.Type.GREATER_THAN_OR_EQUAL) { + return literalValue; + } + if (node.getType() == ComparisonExpression.Type.GREATER_THAN) { + return literalValue - 1; + } + } + return null; + } + } + + private static Optional extractReference(ComparisonExpression expression) + { + if (expression.getLeft() instanceof QualifiedNameReference) { + return Optional.of((QualifiedNameReference) expression.getLeft()); + } + if (expression.getRight() instanceof QualifiedNameReference) { + return Optional.of((QualifiedNameReference) expression.getRight()); + } + return Optional.empty(); + } + + private static Optional extractLiteral(ComparisonExpression expression) + { + if (expression.getLeft() instanceof Literal) { + return Optional.of((Literal) expression.getLeft()); + } + if (expression.getRight() instanceof Literal) { + return Optional.of((Literal) expression.getRight()); + } + return Optional.empty(); + } + + private static long extractValue(Literal literal) + { + if (literal instanceof DoubleLiteral) { + return (long) ((DoubleLiteral) literal).getValue(); + } + if (literal instanceof LongLiteral) { + return ((LongLiteral) literal).getValue(); + } + throw new IllegalArgumentException("Row number compared to non numeric literal"); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/AggregationNode.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/AggregationNode.java new file mode 100644 index 00000000..5102e1a8 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/AggregationNode.java @@ -0,0 +1,162 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.plan; + +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.sql.planner.Symbol; +import com.facebook.presto.sql.tree.FunctionCall; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +import javax.annotation.concurrent.Immutable; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Immutable +public class AggregationNode + extends PlanNode +{ + private final PlanNode source; + private final List groupByKeys; + private final Map aggregations; + // Map from function symbol, to the mask symbol + private final Map masks; + private final Map functions; + private final Step step; + private final Optional sampleWeight; + private final double confidence; + private final Optional hashSymbol; + + public enum Step + { + PARTIAL, + FINAL, + SINGLE + } + + @JsonCreator + public AggregationNode(@JsonProperty("id") PlanNodeId id, + @JsonProperty("source") PlanNode source, + @JsonProperty("groupBy") List groupByKeys, + @JsonProperty("aggregations") Map aggregations, + @JsonProperty("functions") Map functions, + @JsonProperty("masks") Map masks, + @JsonProperty("step") Step step, + @JsonProperty("sampleWeight") Optional sampleWeight, + @JsonProperty("confidence") double confidence, + @JsonProperty("hashSymbol") Optional hashSymbol) + { + super(id); + + this.source = source; + this.groupByKeys = ImmutableList.copyOf(checkNotNull(groupByKeys, "groupByKeys is null")); + this.aggregations = ImmutableMap.copyOf(checkNotNull(aggregations, "aggregations is null")); + this.functions = ImmutableMap.copyOf(checkNotNull(functions, "functions is null")); + this.masks = ImmutableMap.copyOf(checkNotNull(masks, "masks is null")); + for (Symbol mask : masks.keySet()) { + checkArgument(aggregations.containsKey(mask), "mask does not match any aggregations"); + } + this.step = step; + this.sampleWeight = checkNotNull(sampleWeight, "sampleWeight is null"); + checkArgument(confidence >= 0 && confidence <= 1, "confidence must be in [0, 1]"); + this.confidence = confidence; + this.hashSymbol = hashSymbol; + } + + @Override + public List getSources() + { + return ImmutableList.of(source); + } + + @Override + public List getOutputSymbols() + { + ImmutableList.Builder symbols = ImmutableList.builder(); + symbols.addAll(groupByKeys); + // output hashSymbol if present + if (hashSymbol.isPresent()) { + symbols.add(hashSymbol.get()); + } + symbols.addAll(aggregations.keySet()); + return symbols.build(); + } + + @JsonProperty("confidence") + public double getConfidence() + { + return confidence; + } + + @JsonProperty("aggregations") + public Map getAggregations() + { + return aggregations; + } + + @JsonProperty("functions") + public Map getFunctions() + { + return functions; + } + + @JsonProperty("masks") + public Map getMasks() + { + return masks; + } + + @JsonProperty("groupBy") + public List getGroupBy() + { + return groupByKeys; + } + + @JsonProperty("source") + public PlanNode getSource() + { + return source; + } + + @JsonProperty("step") + public Step getStep() + { + return step; + } + + @JsonProperty("sampleWeight") + public Optional getSampleWeight() + { + return sampleWeight; + } + + @JsonProperty("hashSymbol") + public Optional getHashSymbol() + { + return hashSymbol; + } + + @Override + public R accept(PlanVisitor visitor, C context) + { + return visitor.visitAggregation(this, context); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/ChildReplacer.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/ChildReplacer.java new file mode 100644 index 00000000..bf8383b1 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/ChildReplacer.java @@ -0,0 +1,231 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.plan; + +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import com.google.common.collect.Iterables; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkArgument; + +public class ChildReplacer + extends PlanVisitor, PlanNode> +{ + private static final ChildReplacer INSTANCE = new ChildReplacer(); + + public static PlanNode replaceChildren(PlanNode node, List children) + { + return node.accept(INSTANCE, children); + } + + @Override + public PlanNode visitPlan(PlanNode node, List newChildren) + { + throw new UnsupportedOperationException("not yet implemented: " + node.getClass().getName()); + } + + @Override + public PlanNode visitLimit(LimitNode node, List newChildren) + { + return new LimitNode(node.getId(), Iterables.getOnlyElement(newChildren), node.getCount()); + } + + @Override + public PlanNode visitDistinctLimit(DistinctLimitNode node, List newChildren) + { + return new DistinctLimitNode(node.getId(), Iterables.getOnlyElement(newChildren), node.getLimit(), node.getHashSymbol()); + } + + @Override + public PlanNode visitRemoteSource(RemoteSourceNode node, List newChildren) + { + checkArgument(newChildren.isEmpty(), "newChildren is not empty"); + return node; + } + + @Override + public PlanNode visitExchange(ExchangeNode node, List newChildren) + { + return new ExchangeNode( + node.getId(), + node.getType(), + node.getPartitionKeys(), + node.getHashSymbol(), + newChildren, + node.getOutputSymbols(), + node.getInputs()); + } + + @Override + public PlanNode visitTopN(TopNNode node, List newChildren) + { + return new TopNNode(node.getId(), Iterables.getOnlyElement(newChildren), node.getCount(), node.getOrderBy(), node.getOrderings(), node.isPartial()); + } + + @Override + public PlanNode visitTableScan(TableScanNode node, List newChildren) + { + checkArgument(newChildren.isEmpty(), "newChildren is not empty"); + return node; + } + + @Override + public PlanNode visitValues(ValuesNode node, List newChildren) + { + checkArgument(newChildren.isEmpty(), "newChildren is not empty"); + return node; + } + + @Override + public PlanNode visitUnnest(UnnestNode node, List newChildren) + { + return new UnnestNode(node.getId(), Iterables.getOnlyElement(newChildren), node.getReplicateSymbols(), node.getUnnestSymbols(), node.getOrdinalitySymbol()); + } + + @Override + public PlanNode visitProject(ProjectNode node, List newChildren) + { + return new ProjectNode(node.getId(), Iterables.getOnlyElement(newChildren), node.getAssignments()); + } + + @Override + public PlanNode visitFilter(FilterNode node, List newChildren) + { + return new FilterNode(node.getId(), Iterables.getOnlyElement(newChildren), node.getPredicate()); + } + + @Override + public PlanNode visitSample(SampleNode node, List newChildren) + { + return new SampleNode(node.getId(), Iterables.getOnlyElement(newChildren), node.getSampleRatio(), node.getSampleType(), node.isRescaled(), node.getSampleWeightSymbol()); + } + + @Override + public PlanNode visitIndexSource(IndexSourceNode node, List newChildren) + { + return node; + } + + @Override + public PlanNode visitJoin(JoinNode node, List newChildren) + { + checkArgument(newChildren.size() == 2, "expected newChildren to contain 2 nodes"); + return new JoinNode(node.getId(), node.getType(), newChildren.get(0), newChildren.get(1), node.getCriteria(), node.getLeftHashSymbol(), node.getRightHashSymbol()); + } + + @Override + public PlanNode visitSemiJoin(SemiJoinNode node, List newChildren) + { + checkArgument(newChildren.size() == 2, "expected newChildren to contain 2 nodes"); + return new SemiJoinNode(node.getId(), newChildren.get(0), newChildren.get(1), node.getSourceJoinSymbol(), node.getFilteringSourceJoinSymbol(), node.getSemiJoinOutput(), node.getSourceHashSymbol(), node.getFilteringSourceHashSymbol()); + } + + @Override + public PlanNode visitIndexJoin(IndexJoinNode node, List newChildren) + { + checkArgument(newChildren.size() == 2, "expected newChildren to contain 2 nodes"); + return new IndexJoinNode(node.getId(), node.getType(), newChildren.get(0), newChildren.get(1), node.getCriteria(), node.getProbeHashSymbol(), node.getIndexHashSymbol()); + } + + @Override + public PlanNode visitAggregation(AggregationNode node, List newChildren) + { + return new AggregationNode(node.getId(), Iterables.getOnlyElement(newChildren), node.getGroupBy(), node.getAggregations(), node.getFunctions(), node.getMasks(), node.getStep(), node.getSampleWeight(), node.getConfidence(), node.getHashSymbol()); + } + + @Override + public PlanNode visitMarkDistinct(MarkDistinctNode node, List newChildren) + { + return new MarkDistinctNode(node.getId(), Iterables.getOnlyElement(newChildren), node.getMarkerSymbol(), node.getDistinctSymbols(), node.getHashSymbol()); + } + + @Override + public PlanNode visitWindow(WindowNode node, List newChildren) + { + return new WindowNode( + node.getId(), + Iterables.getOnlyElement(newChildren), + node.getPartitionBy(), + node.getOrderBy(), + node.getOrderings(), + node.getFrame(), + node.getWindowFunctions(), + node.getSignatures(), + node.getHashSymbol(), + node.getPrePartitionedInputs(), + node.getPreSortedOrderPrefix()); + } + + @Override + public PlanNode visitTopNRowNumber(TopNRowNumberNode node, List newChildren) + { + return new TopNRowNumberNode(node.getId(), Iterables.getOnlyElement(newChildren), node.getPartitionBy(), node.getOrderBy(), node.getOrderings(), node.getRowNumberSymbol(), node.getMaxRowCountPerPartition(), node.isPartial(), node.getHashSymbol()); + } + + @Override + public PlanNode visitRowNumber(RowNumberNode node, List newChildren) + { + return new RowNumberNode(node.getId(), Iterables.getOnlyElement(newChildren), node.getPartitionBy(), node.getRowNumberSymbol(), node.getMaxRowCountPerPartition(), node.getHashSymbol()); + } + + @Override + public PlanNode visitOutput(OutputNode node, List newChildren) + { + return new OutputNode(node.getId(), Iterables.getOnlyElement(newChildren), node.getColumnNames(), node.getOutputSymbols()); + } + + @Override + public PlanNode visitSort(SortNode node, List newChildren) + { + return new SortNode(node.getId(), Iterables.getOnlyElement(newChildren), node.getOrderBy(), node.getOrderings()); + } + + @Override + public PlanNode visitTableWriter(TableWriterNode node, List newChildren) + { + return new TableWriterNode(node.getId(), Iterables.getOnlyElement(newChildren), node.getTarget(), node.getColumns(), node.getColumnNames(), node.getOutputSymbols(), node.getSampleWeightSymbol()); + } + + @Override + public PlanNode visitTableCommit(TableCommitNode node, List newChildren) + { + return new TableCommitNode(node.getId(), Iterables.getOnlyElement(newChildren), node.getTarget(), node.getOutputSymbols()); + } + + @Override + public PlanNode visitUnion(UnionNode node, List newChildren) + { + return new UnionNode(node.getId(), newChildren, node.getSymbolMapping()); + } + + @Override + public PlanNode visitDelete(DeleteNode node, List newChildren) + { + return new DeleteNode(node.getId(), Iterables.getOnlyElement(newChildren), node.getTarget(), node.getRowId(), node.getOutputSymbols()); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/DeleteNode.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/DeleteNode.java new file mode 100644 index 00000000..3aad7362 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/DeleteNode.java @@ -0,0 +1,89 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.plan; + +import com.facebook.presto.sql.planner.Symbol; +import com.facebook.presto.sql.planner.plan.TableWriterNode.DeleteHandle; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; + +import javax.annotation.concurrent.Immutable; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Immutable +public class DeleteNode + extends PlanNode +{ + private final PlanNode source; + private final DeleteHandle target; + private final Symbol rowId; + private final List outputs; + + @JsonCreator + public DeleteNode( + @JsonProperty("id") PlanNodeId id, + @JsonProperty("source") PlanNode source, + @JsonProperty("target") DeleteHandle target, + @JsonProperty("rowId") Symbol rowId, + @JsonProperty("outputs") List outputs) + { + super(id); + + this.source = checkNotNull(source, "source is null"); + this.target = checkNotNull(target, "target is null"); + this.rowId = checkNotNull(rowId, "rowId is null"); + this.outputs = ImmutableList.copyOf(checkNotNull(outputs, "outputs is null")); + } + + @JsonProperty + public PlanNode getSource() + { + return source; + } + + @JsonProperty + public DeleteHandle getTarget() + { + return target; + } + + @JsonProperty + public Symbol getRowId() + { + return rowId; + } + + @JsonProperty("outputs") + @Override + public List getOutputSymbols() + { + return outputs; + } + + @Override + public List getSources() + { + return ImmutableList.of(source); + } + + @Override + public R accept(PlanVisitor visitor, C context) + { + return visitor.visitDelete(this, context); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/DistinctLimitNode.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/DistinctLimitNode.java new file mode 100644 index 00000000..8dd9fcf9 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/DistinctLimitNode.java @@ -0,0 +1,96 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.plan; + +import com.facebook.presto.sql.planner.Symbol; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; + +import javax.annotation.concurrent.Immutable; + +import java.util.List; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Predicates.not; + +@Immutable +public class DistinctLimitNode + extends PlanNode +{ + private final PlanNode source; + private final long limit; + private final Optional hashSymbol; + + @JsonCreator + public DistinctLimitNode( + @JsonProperty("id") PlanNodeId id, + @JsonProperty("source") PlanNode source, + @JsonProperty("limit") long limit, + @JsonProperty("hashSymbol") Optional hashSymbol) + { + super(id); + this.source = checkNotNull(source, "source is null"); + checkArgument(limit >= 0, "limit must be greater than or equal to zero"); + this.limit = limit; + this.hashSymbol = checkNotNull(hashSymbol, "hashSymbol is null"); + } + + @Override + public List getSources() + { + return ImmutableList.of(source); + } + + @JsonProperty("source") + public PlanNode getSource() + { + return source; + } + + @JsonProperty("limit") + public long getLimit() + { + return limit; + } + + @JsonProperty("hashSymbol") + public Optional getHashSymbol() + { + return hashSymbol; + } + + public List getDistinctSymbols() + { + if (hashSymbol.isPresent()) { + return ImmutableList.copyOf(Iterables.filter(getOutputSymbols(), not(hashSymbol.get()::equals))); + } + return getOutputSymbols(); + } + + @Override + public List getOutputSymbols() + { + return source.getOutputSymbols(); + } + + @Override + public R accept(PlanVisitor visitor, C context) + { + return visitor.visitDistinctLimit(this, context); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/ExchangeNode.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/ExchangeNode.java new file mode 100644 index 00000000..caad344b --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/ExchangeNode.java @@ -0,0 +1,150 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.plan; + +import com.facebook.presto.sql.planner.Symbol; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; + +import javax.annotation.concurrent.Immutable; + +import java.util.List; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Immutable +public class ExchangeNode + extends PlanNode +{ + public enum Type + { + GATHER, + REPARTITION, + REPLICATE + } + + private final Type type; + private final List outputs; + + private final List sources; + private final List partitionKeys; + private final Optional hashSymbol; + + // for each source, the list of inputs corresponding to each output + private final List> inputs; + + @JsonCreator + public ExchangeNode( + @JsonProperty("id") PlanNodeId id, + @JsonProperty("type") Type type, + @JsonProperty("partitionKeys") List partitionKeys, + @JsonProperty("hashSymbol") Optional hashSymbol, + @JsonProperty("sources") List sources, + @JsonProperty("outputs") List outputs, + @JsonProperty("inputs") List> inputs) + { + super(id); + + checkNotNull(type, "type is null"); + checkNotNull(sources, "sources is null"); + checkNotNull(partitionKeys, "partitionKeys is null"); + checkNotNull(hashSymbol, "hashSymbol is null"); + checkNotNull(outputs, "outputs is null"); + checkNotNull(inputs, "inputs is null"); + checkArgument(outputs.containsAll(partitionKeys), "outputs must contain all partitionKeys"); + checkArgument(!hashSymbol.isPresent() || outputs.contains(hashSymbol.get()), "outputs must contain hashSymbol"); + checkArgument(inputs.stream().allMatch(inputSymbols -> inputSymbols.size() == outputs.size()), "Input symbols do not match output symbols"); + checkArgument(inputs.size() == sources.size(), "Must have same number of input lists as sources"); + for (int i = 0; i < inputs.size(); i++) { + checkArgument(sources.get(i).getOutputSymbols().containsAll(inputs.get(i)), "Source does not supply all required input symbols"); + } + + this.type = type; + this.sources = sources; + this.partitionKeys = ImmutableList.copyOf(partitionKeys); + this.hashSymbol = hashSymbol; + this.outputs = ImmutableList.copyOf(outputs); + this.inputs = ImmutableList.copyOf(inputs); + } + + public static ExchangeNode partitionedExchange(PlanNodeId id, PlanNode child, List partitionKeys, Optional hashSymbol) + { + return new ExchangeNode( + id, + ExchangeNode.Type.REPARTITION, + partitionKeys, + hashSymbol, + ImmutableList.of(child), + child.getOutputSymbols(), + ImmutableList.of(child.getOutputSymbols())); + } + + public static ExchangeNode gatheringExchange(PlanNodeId id, PlanNode child) + { + return new ExchangeNode( + id, + ExchangeNode.Type.GATHER, + ImmutableList.of(), + Optional.empty(), + ImmutableList.of(child), + child.getOutputSymbols(), + ImmutableList.of(child.getOutputSymbols())); + } + + @JsonProperty + public Type getType() + { + return type; + } + + @Override + public List getSources() + { + return sources; + } + + @Override + @JsonProperty("outputs") + public List getOutputSymbols() + { + return outputs; + } + + @JsonProperty + public List getPartitionKeys() + { + return partitionKeys; + } + + @JsonProperty + public Optional getHashSymbol() + { + return hashSymbol; + } + + @JsonProperty + public List> getInputs() + { + return inputs; + } + + @Override + public R accept(PlanVisitor visitor, C context) + { + return visitor.visitExchange(this, context); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/FilterNode.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/FilterNode.java new file mode 100644 index 00000000..a78416b0 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/FilterNode.java @@ -0,0 +1,73 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.plan; + +import com.facebook.presto.sql.planner.Symbol; +import com.facebook.presto.sql.tree.Expression; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; + +import javax.annotation.concurrent.Immutable; + +import java.util.List; + +@Immutable +public class FilterNode + extends PlanNode +{ + private final PlanNode source; + private final Expression predicate; + + @JsonCreator + public FilterNode(@JsonProperty("id") PlanNodeId id, + @JsonProperty("source") PlanNode source, + @JsonProperty("predicate") Expression predicate) + { + super(id); + + this.source = source; + this.predicate = predicate; + } + + @JsonProperty("predicate") + public Expression getPredicate() + { + return predicate; + } + + @Override + public List getOutputSymbols() + { + return source.getOutputSymbols(); + } + + @Override + public List getSources() + { + return ImmutableList.of(source); + } + + @JsonProperty("source") + public PlanNode getSource() + { + return source; + } + + @Override + public R accept(PlanVisitor visitor, C context) + { + return visitor.visitFilter(this, context); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/IndexJoinNode.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/IndexJoinNode.java new file mode 100644 index 00000000..1e50431e --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/IndexJoinNode.java @@ -0,0 +1,157 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.plan; + +import com.facebook.presto.sql.planner.Symbol; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; + +import javax.annotation.concurrent.Immutable; + +import java.util.List; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Immutable +public class IndexJoinNode + extends PlanNode +{ + private final Type type; + private final PlanNode probeSource; + private final PlanNode indexSource; + private final List criteria; + private final Optional probeHashSymbol; + private final Optional indexHashSymbol; + + @JsonCreator + public IndexJoinNode( + @JsonProperty("id") PlanNodeId id, + @JsonProperty("type") Type type, + @JsonProperty("probeSource") PlanNode probeSource, + @JsonProperty("indexSource") PlanNode indexSource, + @JsonProperty("criteria") List criteria, + @JsonProperty("probeHashSymbol") Optional probeHashSymbol, + @JsonProperty("indexHashSymbol") Optional indexHashSymbol) + { + super(id); + this.type = checkNotNull(type, "type is null"); + this.probeSource = checkNotNull(probeSource, "probeSource is null"); + this.indexSource = checkNotNull(indexSource, "indexSource is null"); + this.criteria = ImmutableList.copyOf(checkNotNull(criteria, "criteria is null")); + this.probeHashSymbol = checkNotNull(probeHashSymbol, "probeHashSymbol is null"); + this.indexHashSymbol = checkNotNull(indexHashSymbol, "indexHashSymbol is null"); + } + + public enum Type + { + INNER("Inner"), + SOURCE_OUTER("SourceOuter"); + + private final String joinLabel; + + private Type(String joinLabel) + { + this.joinLabel = joinLabel; + } + + public String getJoinLabel() + { + return joinLabel; + } + } + + @JsonProperty("type") + public Type getType() + { + return type; + } + + @JsonProperty("probeSource") + public PlanNode getProbeSource() + { + return probeSource; + } + + @JsonProperty("indexSource") + public PlanNode getIndexSource() + { + return indexSource; + } + + @JsonProperty("criteria") + public List getCriteria() + { + return criteria; + } + + @JsonProperty("probeHashSymbol") + public Optional getProbeHashSymbol() + { + return probeHashSymbol; + } + + @JsonProperty("indexHashSymbol") + public Optional getIndexHashSymbol() + { + return indexHashSymbol; + } + + @Override + public List getSources() + { + return ImmutableList.of(probeSource, indexSource); + } + + @Override + public List getOutputSymbols() + { + return ImmutableList.builder() + .addAll(probeSource.getOutputSymbols()) + .addAll(indexSource.getOutputSymbols()) + .build(); + } + + @Override + public R accept(PlanVisitor visitor, C context) + { + return visitor.visitIndexJoin(this, context); + } + + public static class EquiJoinClause + { + private final Symbol probe; + private final Symbol index; + + @JsonCreator + public EquiJoinClause(@JsonProperty("probe") Symbol probe, @JsonProperty("index") Symbol index) + { + this.probe = checkNotNull(probe, "probe is null"); + this.index = checkNotNull(index, "index is null"); + } + + @JsonProperty("probe") + public Symbol getProbe() + { + return probe; + } + + @JsonProperty("index") + public Symbol getIndex() + { + return index; + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/IndexSourceNode.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/IndexSourceNode.java new file mode 100644 index 00000000..bbe36708 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/IndexSourceNode.java @@ -0,0 +1,115 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.plan; + +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.metadata.IndexHandle; +import com.facebook.presto.metadata.TableHandle; +import com.facebook.presto.spi.TupleDomain; +import com.facebook.presto.sql.planner.Symbol; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class IndexSourceNode + extends PlanNode +{ + private final IndexHandle indexHandle; + private final TableHandle tableHandle; + private final Set lookupSymbols; + private final List outputSymbols; + private final Map assignments; // symbol -> column + private final TupleDomain effectiveTupleDomain; // general summary of how the output columns will be constrained + + @JsonCreator + public IndexSourceNode( + @JsonProperty("id") PlanNodeId id, + @JsonProperty("indexHandle") IndexHandle indexHandle, + @JsonProperty("tableHandle") TableHandle tableHandle, + @JsonProperty("lookupSymbols") Set lookupSymbols, + @JsonProperty("outputSymbols") List outputSymbols, + @JsonProperty("assignments") Map assignments, + @JsonProperty("effectiveTupleDomain") TupleDomain effectiveTupleDomain) + { + super(id); + this.indexHandle = checkNotNull(indexHandle, "indexHandle is null"); + this.tableHandle = checkNotNull(tableHandle, "tableHandle is null"); + this.lookupSymbols = ImmutableSet.copyOf(checkNotNull(lookupSymbols, "lookupSymbols is null")); + this.outputSymbols = ImmutableList.copyOf(checkNotNull(outputSymbols, "outputSymbols is null")); + this.assignments = ImmutableMap.copyOf(checkNotNull(assignments, "assignments is null")); + this.effectiveTupleDomain = checkNotNull(effectiveTupleDomain, "effectiveTupleDomain is null"); + checkArgument(!lookupSymbols.isEmpty(), "lookupSymbols is empty"); + checkArgument(!outputSymbols.isEmpty(), "outputSymbols is empty"); + checkArgument(assignments.keySet().containsAll(lookupSymbols), "Assignments do not include all lookup symbols"); + checkArgument(outputSymbols.containsAll(lookupSymbols), "Lookup symbols need to be part of the output symbols"); + } + + @JsonProperty + public IndexHandle getIndexHandle() + { + return indexHandle; + } + + @JsonProperty + public TableHandle getTableHandle() + { + return tableHandle; + } + + @JsonProperty + public Set getLookupSymbols() + { + return lookupSymbols; + } + + @Override + @JsonProperty + public List getOutputSymbols() + { + return outputSymbols; + } + + @JsonProperty + public Map getAssignments() + { + return assignments; + } + + @JsonProperty + public TupleDomain getEffectiveTupleDomain() + { + return effectiveTupleDomain; + } + + @Override + public List getSources() + { + return ImmutableList.of(); + } + + @Override + public R accept(PlanVisitor visitor, C context) + { + return visitor.visitIndexSource(this, context); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/JoinNode.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/JoinNode.java new file mode 100644 index 00000000..5176af7c --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/JoinNode.java @@ -0,0 +1,188 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.plan; + +import com.facebook.presto.sql.planner.Symbol; +import com.facebook.presto.sql.tree.Join; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; + +import javax.annotation.concurrent.Immutable; + +import java.util.List; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Immutable +public class JoinNode + extends PlanNode +{ + private final Type type; + private final PlanNode left; + private final PlanNode right; + private final List criteria; + private final Optional leftHashSymbol; + private final Optional rightHashSymbol; + + @JsonCreator + public JoinNode(@JsonProperty("id") PlanNodeId id, + @JsonProperty("type") Type type, + @JsonProperty("left") PlanNode left, + @JsonProperty("right") PlanNode right, + @JsonProperty("criteria") List criteria, + @JsonProperty("leftHashSymbol") Optional leftHashSymbol, + @JsonProperty("rightHashSymbol") Optional rightHashSymbol) + { + super(id); + checkNotNull(type, "type is null"); + checkNotNull(left, "left is null"); + checkNotNull(right, "right is null"); + checkNotNull(criteria, "criteria is null"); + checkNotNull(leftHashSymbol, "leftHashSymbol is null"); + checkNotNull(rightHashSymbol, "rightHashSymbol is null"); + + this.type = type; + this.left = left; + this.right = right; + this.criteria = ImmutableList.copyOf(criteria); + this.leftHashSymbol = leftHashSymbol; + this.rightHashSymbol = rightHashSymbol; + } + + public enum Type + { + INNER("InnerJoin"), + LEFT("LeftJoin"), + RIGHT("RightJoin"), + CROSS("CrossJoin"), + FULL("FullJoin"); + + private final String joinLabel; + + Type(String joinLabel) + { + this.joinLabel = joinLabel; + } + + public String getJoinLabel() + { + return joinLabel; + } + + public static Type typeConvert(Join.Type joinType) + { + // Omit SEMI join types because they must be inferred by the planner and not part of the SQL parse tree + switch (joinType) { + case INNER: + return Type.INNER; + case LEFT: + return Type.LEFT; + case RIGHT: + return Type.RIGHT; + case FULL: + return Type.FULL; + case CROSS: + case IMPLICIT: + return Type.CROSS; + default: + throw new UnsupportedOperationException("Unsupported join type: " + joinType); + } + } + } + + @JsonProperty("type") + public Type getType() + { + return type; + } + + @JsonProperty("left") + public PlanNode getLeft() + { + return left; + } + + @JsonProperty("right") + public PlanNode getRight() + { + return right; + } + + @JsonProperty("criteria") + public List getCriteria() + { + return criteria; + } + + @JsonProperty("leftHashSymbol") + public Optional getLeftHashSymbol() + { + return leftHashSymbol; + } + + @JsonProperty("rightHashSymbol") + public Optional getRightHashSymbol() + { + return rightHashSymbol; + } + + @Override + public List getSources() + { + return ImmutableList.of(left, right); + } + + @Override + @JsonProperty("outputSymbols") + public List getOutputSymbols() + { + return ImmutableList.builder() + .addAll(left.getOutputSymbols()) + .addAll(right.getOutputSymbols()) + .build(); + } + + @Override + public R accept(PlanVisitor visitor, C context) + { + return visitor.visitJoin(this, context); + } + + public static class EquiJoinClause + { + private final Symbol left; + private final Symbol right; + + @JsonCreator + public EquiJoinClause(@JsonProperty("left") Symbol left, @JsonProperty("right") Symbol right) + { + this.left = checkNotNull(left, "left is null"); + this.right = checkNotNull(right, "right is null"); + } + + @JsonProperty("left") + public Symbol getLeft() + { + return left; + } + + @JsonProperty("right") + public Symbol getRight() + { + return right; + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/LimitNode.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/LimitNode.java new file mode 100644 index 00000000..e92a05df --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/LimitNode.java @@ -0,0 +1,74 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.plan; + +import com.facebook.presto.sql.planner.Symbol; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; + +import javax.annotation.concurrent.Immutable; + +import java.util.List; + +@Immutable +public class LimitNode + extends PlanNode +{ + private final PlanNode source; + private final long count; + + @JsonCreator + public LimitNode(@JsonProperty("id") PlanNodeId id, @JsonProperty("source") PlanNode source, @JsonProperty("count") long count) + { + super(id); + + Preconditions.checkNotNull(source, "source is null"); + Preconditions.checkArgument(count >= 0, "count must be greater than or equal to zero"); + + this.source = source; + this.count = count; + } + + @Override + public List getSources() + { + return ImmutableList.of(source); + } + + @JsonProperty("source") + public PlanNode getSource() + { + return source; + } + + @JsonProperty("count") + public long getCount() + { + return count; + } + + @Override + public List getOutputSymbols() + { + return source.getOutputSymbols(); + } + + @Override + public R accept(PlanVisitor visitor, C context) + { + return visitor.visitLimit(this, context); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/MarkDistinctNode.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/MarkDistinctNode.java new file mode 100644 index 00000000..bd9199e3 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/MarkDistinctNode.java @@ -0,0 +1,96 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.plan; + +import com.facebook.presto.sql.planner.Symbol; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; + +import javax.annotation.concurrent.Immutable; + +import java.util.List; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Immutable +public class MarkDistinctNode + extends PlanNode +{ + private final PlanNode source; + private final Symbol markerSymbol; + + private final Optional hashSymbol; + private final List distinctSymbols; + + @JsonCreator + public MarkDistinctNode(@JsonProperty("id") PlanNodeId id, + @JsonProperty("source") PlanNode source, + @JsonProperty("markerSymbol") Symbol markerSymbol, + @JsonProperty("distinctSymbols") List distinctSymbols, + @JsonProperty("hashSymbol") Optional hashSymbol) + { + super(id); + this.source = source; + this.markerSymbol = markerSymbol; + this.hashSymbol = checkNotNull(hashSymbol, "hashSymbol is null"); + this.distinctSymbols = ImmutableList.copyOf(checkNotNull(distinctSymbols, "distinctSymbols is null")); + } + + @Override + public List getOutputSymbols() + { + return ImmutableList.builder() + .addAll(source.getOutputSymbols()) + .add(markerSymbol) + .build(); + } + + @Override + public List getSources() + { + return ImmutableList.of(source); + } + + @JsonProperty + public PlanNode getSource() + { + return source; + } + + @JsonProperty + public Symbol getMarkerSymbol() + { + return markerSymbol; + } + + @JsonProperty + public List getDistinctSymbols() + { + return distinctSymbols; + } + + @JsonProperty + public Optional getHashSymbol() + { + return hashSymbol; + } + + @Override + public R accept(PlanVisitor visitor, C context) + { + return visitor.visitMarkDistinct(this, context); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/OutputNode.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/OutputNode.java new file mode 100644 index 00000000..c9ebe79b --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/OutputNode.java @@ -0,0 +1,81 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.plan; + +import com.facebook.presto.sql.planner.Symbol; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; + +import javax.annotation.concurrent.Immutable; + +import java.util.List; + +@Immutable +public class OutputNode + extends PlanNode +{ + private final PlanNode source; + private final List columnNames; + private final List outputs; // column name = symbol + + @JsonCreator + public OutputNode(@JsonProperty("id") PlanNodeId id, + @JsonProperty("source") PlanNode source, + @JsonProperty("columns") List columnNames, + @JsonProperty("outputs") List outputs) + { + super(id); + + Preconditions.checkNotNull(source, "source is null"); + Preconditions.checkNotNull(columnNames, "columnNames is null"); + Preconditions.checkArgument(columnNames.size() == outputs.size(), "columnNames and assignments sizes don't match"); + + this.source = source; + this.columnNames = columnNames; + this.outputs = ImmutableList.copyOf(outputs); + } + + @Override + public List getSources() + { + return ImmutableList.of(source); + } + + @Override + @JsonProperty("outputs") + public List getOutputSymbols() + { + return outputs; + } + + @JsonProperty("columns") + public List getColumnNames() + { + return columnNames; + } + + @JsonProperty + public PlanNode getSource() + { + return source; + } + + @Override + public R accept(PlanVisitor visitor, C context) + { + return visitor.visitOutput(this, context); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/PlanFragmentId.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/PlanFragmentId.java new file mode 100644 index 00000000..f7ff1fe9 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/PlanFragmentId.java @@ -0,0 +1,66 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.plan; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +import javax.annotation.concurrent.Immutable; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Immutable +public class PlanFragmentId +{ + private final String id; + + @JsonCreator + public PlanFragmentId(String id) + { + checkNotNull(id, "id is null"); + this.id = id; + } + + @Override + @JsonValue + public String toString() + { + return id; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + PlanFragmentId that = (PlanFragmentId) o; + + if (!id.equals(that.id)) { + return false; + } + + return true; + } + + @Override + public int hashCode() + { + return id.hashCode(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/PlanNode.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/PlanNode.java new file mode 100644 index 00000000..6365efe4 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/PlanNode.java @@ -0,0 +1,81 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.plan; + +import com.facebook.presto.sql.planner.Symbol; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = OutputNode.class, name = "output"), + @JsonSubTypes.Type(value = ProjectNode.class, name = "project"), + @JsonSubTypes.Type(value = TableScanNode.class, name = "tablescan"), + @JsonSubTypes.Type(value = ValuesNode.class, name = "values"), + @JsonSubTypes.Type(value = AggregationNode.class, name = "aggregation"), + @JsonSubTypes.Type(value = MarkDistinctNode.class, name = "markDistinct"), + @JsonSubTypes.Type(value = FilterNode.class, name = "filter"), + @JsonSubTypes.Type(value = WindowNode.class, name = "window"), + @JsonSubTypes.Type(value = RowNumberNode.class, name = "rowNumber"), + @JsonSubTypes.Type(value = TopNRowNumberNode.class, name = "topnRowNumber"), + @JsonSubTypes.Type(value = LimitNode.class, name = "limit"), + @JsonSubTypes.Type(value = DistinctLimitNode.class, name = "distinctlimit"), + @JsonSubTypes.Type(value = TopNNode.class, name = "topn"), + @JsonSubTypes.Type(value = SampleNode.class, name = "sample"), + @JsonSubTypes.Type(value = SortNode.class, name = "sort"), + @JsonSubTypes.Type(value = RemoteSourceNode.class, name = "remoteSource"), + @JsonSubTypes.Type(value = JoinNode.class, name = "join"), + @JsonSubTypes.Type(value = SemiJoinNode.class, name = "semijoin"), + @JsonSubTypes.Type(value = IndexJoinNode.class, name = "indexjoin"), + @JsonSubTypes.Type(value = IndexSourceNode.class, name = "indexsource"), + @JsonSubTypes.Type(value = TableWriterNode.class, name = "tablewriter"), + @JsonSubTypes.Type(value = DeleteNode.class, name = "delete"), + @JsonSubTypes.Type(value = TableCommitNode.class, name = "tablecommit"), + @JsonSubTypes.Type(value = UnnestNode.class, name = "unnest"), + @JsonSubTypes.Type(value = ExchangeNode.class, name = "exchange"), + @JsonSubTypes.Type(value = UnionNode.class, name = "union"), +}) +public abstract class PlanNode +{ + private final PlanNodeId id; + + protected PlanNode(PlanNodeId id) + { + checkNotNull(id, "id is null"); + this.id = id; + } + + @JsonProperty("id") + public PlanNodeId getId() + { + return id; + } + + public abstract List getSources(); + + public abstract List getOutputSymbols(); + + public R accept(PlanVisitor visitor, C context) + { + return visitor.visitPlan(this, context); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/PlanNodeId.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/PlanNodeId.java new file mode 100644 index 00000000..2e8a6b2e --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/PlanNodeId.java @@ -0,0 +1,66 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.plan; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +import javax.annotation.concurrent.Immutable; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Immutable +public class PlanNodeId +{ + private final String id; + + @JsonCreator + public PlanNodeId(String id) + { + checkNotNull(id, "id is null"); + this.id = id; + } + + @Override + @JsonValue + public String toString() + { + return id; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + PlanNodeId that = (PlanNodeId) o; + + if (!id.equals(that.id)) { + return false; + } + + return true; + } + + @Override + public int hashCode() + { + return id.hashCode(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/PlanRewriter.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/PlanRewriter.java new file mode 100644 index 00000000..1e54a4fd --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/PlanRewriter.java @@ -0,0 +1,113 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.plan; + +import com.facebook.presto.util.ImmutableCollectors; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; + +public abstract class PlanRewriter + extends PlanVisitor, PlanNode> +{ + public static PlanNode rewriteWith(PlanRewriter rewriter, PlanNode node) + { + return node.accept(rewriter, new RewriteContext<>(rewriter, null)); + } + + public static PlanNode rewriteWith(PlanRewriter rewriter, PlanNode node, C context) + { + return node.accept(rewriter, new RewriteContext<>(rewriter, context)); + } + + @Override + protected PlanNode visitPlan(PlanNode node, RewriteContext context) + { + return context.defaultRewrite(node, context.get()); + } + + public static class RewriteContext + { + private final C userContext; + private final PlanRewriter nodeRewriter; + + private RewriteContext(PlanRewriter nodeRewriter, C userContext) + { + this.nodeRewriter = nodeRewriter; + this.userContext = userContext; + } + + public C get() + { + return userContext; + } + + /** + * Invoke the rewrite logic recursively on children of the given node and swap it + * out with an identical copy with the rewritten children + */ + public PlanNode defaultRewrite(PlanNode node) + { + return defaultRewrite(node, null); + } + + /** + * Invoke the rewrite logic recursively on children of the given node and swap it + * out with an identical copy with the rewritten children + */ + public PlanNode defaultRewrite(PlanNode node, C context) + { + List children = node.getSources().stream() + .map(child -> rewrite(child, context)) + .collect(ImmutableCollectors.toImmutableList()); + + return replaceChildren(node, children); + } + + /** + * Return an identical copy of the given node with its children replaced + */ + public PlanNode replaceChildren(PlanNode node, List newChildren) + { + for (int i = 0; i < node.getSources().size(); i++) { + if (newChildren.get(i) != node.getSources().get(i)) { + return ChildReplacer.replaceChildren(node, newChildren); + } + } + + // children haven't change, so make this a no-op + return node; + } + + /** + * This method is meant for invoking the rewrite logic on children while processing a node + */ + public PlanNode rewrite(PlanNode node, C userContext) + { + PlanNode result = node.accept(nodeRewriter, new RewriteContext<>(nodeRewriter, userContext)); + checkNotNull(result, "nodeRewriter returned null for %s", node.getClass().getName()); + + return result; + } + + /** + * This method is meant for invoking the rewrite logic on children while processing a node + */ + public PlanNode rewrite(PlanNode node) + { + return rewrite(node, null); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/PlanVisitor.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/PlanVisitor.java new file mode 100644 index 00000000..1974242b --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/PlanVisitor.java @@ -0,0 +1,152 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.plan; + +public class PlanVisitor +{ + protected R visitPlan(PlanNode node, C context) + { + return null; + } + + public R visitRemoteSource(RemoteSourceNode node, C context) + { + return visitPlan(node, context); + } + + public R visitAggregation(AggregationNode node, C context) + { + return visitPlan(node, context); + } + + public R visitFilter(FilterNode node, C context) + { + return visitPlan(node, context); + } + + public R visitProject(ProjectNode node, C context) + { + return visitPlan(node, context); + } + + public R visitTopN(TopNNode node, C context) + { + return visitPlan(node, context); + } + + public R visitOutput(OutputNode node, C context) + { + return visitPlan(node, context); + } + + public R visitLimit(LimitNode node, C context) + { + return visitPlan(node, context); + } + + public R visitDistinctLimit(DistinctLimitNode node, C context) + { + return visitPlan(node, context); + } + + public R visitSample(SampleNode node, C context) + { + return visitPlan(node, context); + } + + public R visitTableScan(TableScanNode node, C context) + { + return visitPlan(node, context); + } + + public R visitValues(ValuesNode node, C context) + { + return visitPlan(node, context); + } + + public R visitIndexSource(IndexSourceNode node, C context) + { + return visitPlan(node, context); + } + + public R visitJoin(JoinNode node, C context) + { + return visitPlan(node, context); + } + + public R visitSemiJoin(SemiJoinNode node, C context) + { + return visitPlan(node, context); + } + + public R visitIndexJoin(IndexJoinNode node, C context) + { + return visitPlan(node, context); + } + + public R visitSort(SortNode node, C context) + { + return visitPlan(node, context); + } + + public R visitWindow(WindowNode node, C context) + { + return visitPlan(node, context); + } + + public R visitTableWriter(TableWriterNode node, C context) + { + return visitPlan(node, context); + } + + public R visitDelete(DeleteNode node, C context) + { + return visitPlan(node, context); + } + + public R visitTableCommit(TableCommitNode node, C context) + { + return visitPlan(node, context); + } + + public R visitUnion(UnionNode node, C context) + { + return visitPlan(node, context); + } + + public R visitUnnest(UnnestNode node, C context) + { + return visitPlan(node, context); + } + + public R visitMarkDistinct(MarkDistinctNode node, C context) + { + return visitPlan(node, context); + } + + public R visitRowNumber(RowNumberNode node, C context) + { + return visitPlan(node, context); + } + + public R visitTopNRowNumber(TopNRowNumberNode node, C context) + { + return visitPlan(node, context); + } + + public R visitExchange(ExchangeNode node, C context) + { + return visitPlan(node, context); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/ProjectNode.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/ProjectNode.java new file mode 100644 index 00000000..995e6920 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/ProjectNode.java @@ -0,0 +1,83 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.plan; + +import com.facebook.presto.sql.planner.Symbol; +import com.facebook.presto.sql.tree.Expression; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +import javax.annotation.concurrent.Immutable; + +import java.util.List; +import java.util.Map; + +@Immutable +public class ProjectNode + extends PlanNode +{ + private final PlanNode source; + private final Map assignments; + private final List outputs; + + // TODO: pass in the "assignments" and the "outputs" separately (i.e., get rid if the symbol := symbol idiom) + @JsonCreator + public ProjectNode(@JsonProperty("id") PlanNodeId id, + @JsonProperty("source") PlanNode source, + @JsonProperty("assignments") Map assignments) + { + super(id); + + this.source = source; + this.assignments = ImmutableMap.copyOf(assignments); + this.outputs = ImmutableList.copyOf(assignments.keySet()); + } + + public List getExpressions() + { + return ImmutableList.copyOf(assignments.values()); + } + + @Override + public List getOutputSymbols() + { + return outputs; + } + + @JsonProperty + public Map getAssignments() + { + return assignments; + } + + @Override + public List getSources() + { + return ImmutableList.of(source); + } + + @JsonProperty + public PlanNode getSource() + { + return source; + } + + @Override + public R accept(PlanVisitor visitor, C context) + { + return visitor.visitProject(this, context); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/RemoteSourceNode.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/RemoteSourceNode.java new file mode 100644 index 00000000..5d8d26dd --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/RemoteSourceNode.java @@ -0,0 +1,76 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.plan; + +import com.facebook.presto.sql.planner.Symbol; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; + +import javax.annotation.concurrent.Immutable; + +import java.util.List; + +@Immutable +public class RemoteSourceNode + extends PlanNode +{ + private final List sourceFragmentIds; + private final List outputs; + + @JsonCreator + public RemoteSourceNode( + @JsonProperty("id") PlanNodeId id, + @JsonProperty("sourceFragmentIds") List sourceFragmentIds, + @JsonProperty("outputs") List outputs) + { + super(id); + + Preconditions.checkNotNull(outputs, "outputs is null"); + + this.sourceFragmentIds = sourceFragmentIds; + this.outputs = ImmutableList.copyOf(outputs); + } + + public RemoteSourceNode(PlanNodeId id, PlanFragmentId sourceFragmentId, List outputs) + { + this(id, ImmutableList.of(sourceFragmentId), outputs); + } + + @Override + public List getSources() + { + return ImmutableList.of(); + } + + @Override + @JsonProperty("outputs") + public List getOutputSymbols() + { + return outputs; + } + + @JsonProperty("sourceFragmentIds") + public List getSourceFragmentIds() + { + return sourceFragmentIds; + } + + @Override + public R accept(PlanVisitor visitor, C context) + { + return visitor.visitRemoteSource(this, context); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/RowNumberNode.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/RowNumberNode.java new file mode 100644 index 00000000..0113174f --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/RowNumberNode.java @@ -0,0 +1,110 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.plan; + +import com.facebook.presto.sql.planner.Symbol; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; + +import javax.annotation.concurrent.Immutable; + +import java.util.List; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.Iterables.concat; + +@Immutable +public final class RowNumberNode + extends PlanNode +{ + private final PlanNode source; + private final List partitionBy; + private final Optional maxRowCountPerPartition; + private final Symbol rowNumberSymbol; + private final Optional hashSymbol; + + @JsonCreator + public RowNumberNode( + @JsonProperty("id") PlanNodeId id, + @JsonProperty("source") PlanNode source, + @JsonProperty("partitionBy") List partitionBy, + @JsonProperty("rowNumberSymbol") Symbol rowNumberSymbol, + @JsonProperty("maxRowCountPerPartition") Optional maxRowCountPerPartition, + @JsonProperty("hashSymbol") Optional hashSymbol) + { + super(id); + + checkNotNull(source, "source is null"); + checkNotNull(partitionBy, "partitionBy is null"); + checkNotNull(rowNumberSymbol, "rowNumberSymbol is null"); + checkNotNull(maxRowCountPerPartition, "maxRowCountPerPartition is null"); + checkNotNull(hashSymbol, "hashSymbol is null"); + + this.source = source; + this.partitionBy = ImmutableList.copyOf(partitionBy); + this.rowNumberSymbol = rowNumberSymbol; + this.maxRowCountPerPartition = maxRowCountPerPartition; + this.hashSymbol = hashSymbol; + } + + @Override + public List getSources() + { + return ImmutableList.of(source); + } + + @Override + public List getOutputSymbols() + { + return ImmutableList.copyOf(concat(source.getOutputSymbols(), ImmutableList.of(rowNumberSymbol))); + } + + @JsonProperty + public PlanNode getSource() + { + return source; + } + + @JsonProperty + public List getPartitionBy() + { + return partitionBy; + } + + @JsonProperty + public Symbol getRowNumberSymbol() + { + return rowNumberSymbol; + } + + @JsonProperty + public Optional getMaxRowCountPerPartition() + { + return maxRowCountPerPartition; + } + + @JsonProperty + public Optional getHashSymbol() + { + return hashSymbol; + } + + @Override + public R accept(PlanVisitor visitor, C context) + { + return visitor.visitRowNumber(this, context); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/SampleNode.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/SampleNode.java new file mode 100644 index 00000000..78ee61f8 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/SampleNode.java @@ -0,0 +1,136 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.plan; + +import com.facebook.presto.sql.planner.Symbol; +import com.facebook.presto.sql.tree.SampledRelation; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; + +import javax.annotation.concurrent.Immutable; + +import java.util.List; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Immutable +public class SampleNode + extends PlanNode +{ + private final PlanNode source; + private final double sampleRatio; + private final Type sampleType; + private final boolean rescaled; + private final Optional sampleWeightSymbol; + + public enum Type + { + BERNOULLI, + POISSONIZED, + SYSTEM; + + public static Type fromType(SampledRelation.Type sampleType) + { + switch (sampleType) { + case BERNOULLI: + return Type.BERNOULLI; + case POISSONIZED: + return Type.POISSONIZED; + case SYSTEM: + return Type.SYSTEM; + default: + throw new UnsupportedOperationException("Unsupported sample type: " + sampleType); + } + } + } + + @JsonCreator + public SampleNode( + @JsonProperty("id") PlanNodeId id, + @JsonProperty("source") PlanNode source, + @JsonProperty("sampleRatio") double sampleRatio, + @JsonProperty("sampleType") Type sampleType, + @JsonProperty("rescaled") boolean rescaled, + @JsonProperty("sampleWeightSymbol") Optional sampleWeightSymbol) + { + super(id); + + checkArgument(sampleRatio >= 0.0, "sample ratio must be greater than or equal to 0"); + checkArgument((sampleRatio <= 1.0) || (sampleType == Type.POISSONIZED), "sample ratio must be less than or equal to 1"); + + this.sampleType = checkNotNull(sampleType, "sample type is null"); + this.source = checkNotNull(source, "source is null"); + this.sampleRatio = sampleRatio; + this.rescaled = rescaled; + checkArgument(!rescaled || sampleType == Type.POISSONIZED); + this.sampleWeightSymbol = checkNotNull(sampleWeightSymbol, "sample weight symbol is null"); + checkArgument(sampleWeightSymbol.isPresent() == (sampleType == Type.POISSONIZED), "sample weight symbol must be used with POISSONIZED sampling"); + } + + @Override + public List getSources() + { + return ImmutableList.of(source); + } + + @JsonProperty + public boolean isRescaled() + { + return rescaled; + } + + @JsonProperty + public Optional getSampleWeightSymbol() + { + return sampleWeightSymbol; + } + + @JsonProperty + public PlanNode getSource() + { + return source; + } + + @JsonProperty + public double getSampleRatio() + { + return sampleRatio; + } + + @JsonProperty + public Type getSampleType() + { + return sampleType; + } + + @Override + public List getOutputSymbols() + { + if (sampleWeightSymbol.isPresent()) { + return ImmutableList.builder().addAll(source.getOutputSymbols()).add(sampleWeightSymbol.get()).build(); + } + else { + return source.getOutputSymbols(); + } + } + + @Override + public R accept(PlanVisitor visitor, C context) + { + return visitor.visitSample(this, context); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/SemiJoinNode.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/SemiJoinNode.java new file mode 100644 index 00000000..8a4e98fe --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/SemiJoinNode.java @@ -0,0 +1,122 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.plan; + +import com.facebook.presto.sql.planner.Symbol; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; + +import javax.annotation.concurrent.Immutable; + +import java.util.List; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Immutable +public class SemiJoinNode + extends PlanNode +{ + private final PlanNode source; + private final PlanNode filteringSource; + private final Symbol sourceJoinSymbol; + private final Symbol filteringSourceJoinSymbol; + private final Symbol semiJoinOutput; + private final Optional sourceHashSymbol; + private final Optional filteringSourceHashSymbol; + + @JsonCreator + public SemiJoinNode(@JsonProperty("id") PlanNodeId id, + @JsonProperty("source") PlanNode source, + @JsonProperty("filteringSource") PlanNode filteringSource, + @JsonProperty("sourceJoinSymbol") Symbol sourceJoinSymbol, + @JsonProperty("filteringSourceJoinSymbol") Symbol filteringSourceJoinSymbol, + @JsonProperty("semiJoinOutput") Symbol semiJoinOutput, + @JsonProperty("sourceHashSymbol") Optional sourceHashSymbol, + @JsonProperty("filteringSourceHashSymbol") Optional filteringSourceHashSymbol) + { + super(id); + this.source = checkNotNull(source, "source is null"); + this.filteringSource = checkNotNull(filteringSource, "filteringSource is null"); + this.sourceJoinSymbol = checkNotNull(sourceJoinSymbol, "sourceJoinSymbol is null"); + this.filteringSourceJoinSymbol = checkNotNull(filteringSourceJoinSymbol, "filteringSourceJoinSymbol is null"); + this.semiJoinOutput = checkNotNull(semiJoinOutput, "semiJoinOutput is null"); + this.sourceHashSymbol = checkNotNull(sourceHashSymbol, "sourceHashSymbol is null"); + this.filteringSourceHashSymbol = checkNotNull(filteringSourceHashSymbol, "filteringSourceHashSymbol is null"); + } + + @JsonProperty("source") + public PlanNode getSource() + { + return source; + } + + @JsonProperty("filteringSource") + public PlanNode getFilteringSource() + { + return filteringSource; + } + + @JsonProperty("sourceJoinSymbol") + public Symbol getSourceJoinSymbol() + { + return sourceJoinSymbol; + } + + @JsonProperty("filteringSourceJoinSymbol") + public Symbol getFilteringSourceJoinSymbol() + { + return filteringSourceJoinSymbol; + } + + @JsonProperty("semiJoinOutput") + public Symbol getSemiJoinOutput() + { + return semiJoinOutput; + } + + @JsonProperty("sourceHashSymbol") + public Optional getSourceHashSymbol() + { + return sourceHashSymbol; + } + + @JsonProperty("filteringSourceHashSymbol") + public Optional getFilteringSourceHashSymbol() + { + return filteringSourceHashSymbol; + } + + @Override + public List getSources() + { + return ImmutableList.of(source, filteringSource); + } + + @Override + public List getOutputSymbols() + { + return ImmutableList.builder() + .addAll(source.getOutputSymbols()) + .add(semiJoinOutput) + .build(); + } + + @Override + public R accept(PlanVisitor visitor, C context) + { + return visitor.visitSemiJoin(this, context); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/SortNode.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/SortNode.java new file mode 100644 index 00000000..9107d6cf --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/SortNode.java @@ -0,0 +1,87 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.plan; + +import com.facebook.presto.spi.block.SortOrder; +import com.facebook.presto.sql.planner.Symbol; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +import java.util.List; +import java.util.Map; + +public class SortNode + extends PlanNode +{ + private final PlanNode source; + private final List orderBy; + private final Map orderings; + + @JsonCreator + public SortNode(@JsonProperty("id") PlanNodeId id, + @JsonProperty("source") PlanNode source, + @JsonProperty("orderBy") List orderBy, + @JsonProperty("orderings") Map orderings) + { + super(id); + + Preconditions.checkNotNull(source, "source is null"); + Preconditions.checkNotNull(orderBy, "orderBy is null"); + Preconditions.checkArgument(!orderBy.isEmpty(), "orderBy is empty"); + Preconditions.checkArgument(orderings.size() == orderBy.size(), "orderBy and orderings sizes don't match"); + + this.source = source; + this.orderBy = ImmutableList.copyOf(orderBy); + this.orderings = ImmutableMap.copyOf(orderings); + } + + @Override + public List getSources() + { + return ImmutableList.of(source); + } + + @JsonProperty("source") + public PlanNode getSource() + { + return source; + } + + @Override + public List getOutputSymbols() + { + return source.getOutputSymbols(); + } + + @JsonProperty("orderBy") + public List getOrderBy() + { + return orderBy; + } + + @JsonProperty("orderings") + public Map getOrderings() + { + return orderings; + } + + @Override + public R accept(PlanVisitor visitor, C context) + { + return visitor.visitSort(this, context); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/TableCommitNode.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/TableCommitNode.java new file mode 100644 index 00000000..f0e8a35b --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/TableCommitNode.java @@ -0,0 +1,82 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.plan; + +import com.facebook.presto.sql.planner.Symbol; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; + +import javax.annotation.concurrent.Immutable; + +import java.util.List; + +import static com.facebook.presto.sql.planner.plan.TableWriterNode.WriterTarget; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Immutable +public class TableCommitNode + extends PlanNode +{ + private final PlanNode source; + private final WriterTarget target; + private final List outputs; + + @JsonCreator + public TableCommitNode( + @JsonProperty("id") PlanNodeId id, + @JsonProperty("source") PlanNode source, + @JsonProperty("target") WriterTarget target, + @JsonProperty("outputs") List outputs) + { + super(id); + + checkArgument(target != null || source instanceof TableWriterNode); + this.source = checkNotNull(source, "source is null"); + this.target = checkNotNull(target, "target is null"); + this.outputs = ImmutableList.copyOf(checkNotNull(outputs, "outputs is null")); + } + + @JsonProperty + public PlanNode getSource() + { + return source; + } + + @JsonProperty + public WriterTarget getTarget() + { + return target; + } + + @JsonProperty("outputs") + @Override + public List getOutputSymbols() + { + return outputs; + } + + @Override + public List getSources() + { + return ImmutableList.of(source); + } + + @Override + public R accept(PlanVisitor visitor, C context) + { + return visitor.visitTableCommit(this, context); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/TableScanNode.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/TableScanNode.java new file mode 100644 index 00000000..bb6d310a --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/TableScanNode.java @@ -0,0 +1,150 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.plan; + +import com.facebook.presto.metadata.TableHandle; +import com.facebook.presto.metadata.TableLayoutHandle; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.TupleDomain; +import com.facebook.presto.sql.planner.Symbol; +import com.facebook.presto.sql.tree.Expression; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Immutable +public class TableScanNode + extends PlanNode +{ + private final TableHandle table; + private final Optional tableLayout; + private final List outputSymbols; + private final Map assignments; // symbol -> column + + // Used during predicate refinement over multiple passes of predicate pushdown + // TODO: think about how to get rid of this in new planner + private final TupleDomain currentConstraint; + + // HACK! + // + // This field exists for the sole purpose of being able to print the original predicates (from the query) in + // a human readable way. Predicates that get converted to and from TupleDomains might get more bulky and thus + // more difficult to read when printed. + // For example: + // (ds > '2013-01-01') in the original query could easily become (ds IN ('2013-01-02', '2013-01-03', ...)) after the partitions are generated. + // To make this work, the originalConstraint should be set exactly once after the first predicate push down and never adjusted after that. + // In this way, we are always guaranteed to have a readable predicate that provides some kind of upper bound on the constraints. + private final Expression originalConstraint; + + @JsonCreator + public TableScanNode( + @JsonProperty("id") PlanNodeId id, + @JsonProperty("table") TableHandle table, + @JsonProperty("outputSymbols") List outputs, + @JsonProperty("assignments") Map assignments, + @JsonProperty("layout") Optional tableLayout, + @JsonProperty("currentConstraint") TupleDomain currentConstraint, + @JsonProperty("originalConstraint") @Nullable Expression originalConstraint) + { + super(id); + checkNotNull(table, "table is null"); + checkNotNull(outputs, "outputs is null"); + checkNotNull(assignments, "assignments is null"); + checkArgument(assignments.keySet().containsAll(outputs), "assignments does not cover all of outputs"); + checkNotNull(tableLayout, "tableLayout is null"); + checkNotNull(currentConstraint, "currentConstraint is null"); + + this.table = table; + this.outputSymbols = ImmutableList.copyOf(outputs); + this.assignments = ImmutableMap.copyOf(assignments); + this.originalConstraint = originalConstraint; + this.tableLayout = tableLayout; + this.currentConstraint = currentConstraint; + } + + @JsonProperty("table") + public TableHandle getTable() + { + return table; + } + + @JsonProperty + public Optional getLayout() + { + return tableLayout; + } + + @Override + @JsonProperty("outputSymbols") + public List getOutputSymbols() + { + return outputSymbols; + } + + @JsonProperty("assignments") + public Map getAssignments() + { + return assignments; + } + + @Nullable + @JsonProperty("originalConstraint") + public Expression getOriginalConstraint() + { + return originalConstraint; + } + + @JsonProperty("currentConstraint") + public TupleDomain getCurrentConstraint() + { + return currentConstraint; + } + + @Override + public List getSources() + { + return ImmutableList.of(); + } + + @Override + public R accept(PlanVisitor visitor, C context) + { + return visitor.visitTableScan(this, context); + } + + @Override + public String toString() + { + return MoreObjects.toStringHelper(this) + .add("table", table) + .add("tableLayout", tableLayout) + .add("outputSymbols", outputSymbols) + .add("assignments", assignments) + .add("currentConstraint", currentConstraint) + .add("originalConstraint", originalConstraint) + .toString(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/TableWriterNode.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/TableWriterNode.java new file mode 100644 index 00000000..ac69615a --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/TableWriterNode.java @@ -0,0 +1,268 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.plan; + +import com.facebook.presto.metadata.InsertTableHandle; +import com.facebook.presto.metadata.OutputTableHandle; +import com.facebook.presto.metadata.TableHandle; +import com.facebook.presto.metadata.TableMetadata; +import com.facebook.presto.spi.InsertOption; +import com.facebook.presto.sql.planner.Symbol; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.google.common.collect.ImmutableList; + +import javax.annotation.concurrent.Immutable; + +import java.util.List; +import java.util.Optional; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Immutable +public class TableWriterNode + extends PlanNode +{ + private final PlanNode source; + private final WriterTarget target; + private final List outputs; + private final List columns; + private final List columnNames; + private final Optional sampleWeightSymbol; + + @JsonCreator + public TableWriterNode( + @JsonProperty("id") PlanNodeId id, + @JsonProperty("source") PlanNode source, + @JsonProperty("target") WriterTarget target, + @JsonProperty("columns") List columns, + @JsonProperty("columnNames") List columnNames, + @JsonProperty("outputs") List outputs, + @JsonProperty("sampleWeightSymbol") Optional sampleWeightSymbol) + { + super(id); + + checkNotNull(columns, "columns is null"); + checkNotNull(columnNames, "columnNames is null"); + checkArgument(columns.size() == columnNames.size(), "columns and columnNames sizes don't match"); + + this.source = checkNotNull(source, "source is null"); + this.target = checkNotNull(target, "target is null"); + this.columns = ImmutableList.copyOf(columns); + this.columnNames = ImmutableList.copyOf(columnNames); + this.outputs = ImmutableList.copyOf(checkNotNull(outputs, "outputs is null")); + this.sampleWeightSymbol = checkNotNull(sampleWeightSymbol, "sampleWeightSymbol is null"); + } + + @JsonProperty + public Optional getSampleWeightSymbol() + { + return sampleWeightSymbol; + } + + @JsonProperty + public PlanNode getSource() + { + return source; + } + + @JsonProperty + public WriterTarget getTarget() + { + return target; + } + + @JsonProperty + public List getColumns() + { + return columns; + } + + @JsonProperty + public List getColumnNames() + { + return columnNames; + } + + @JsonProperty("outputs") + @Override + public List getOutputSymbols() + { + return outputs; + } + + @Override + public List getSources() + { + return ImmutableList.of(source); + } + + @Override + public R accept(PlanVisitor visitor, C context) + { + return visitor.visitTableWriter(this, context); + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") + @JsonSubTypes({ + @JsonSubTypes.Type(value = CreateHandle.class, name = "CreateHandle"), + @JsonSubTypes.Type(value = InsertHandle.class, name = "InsertHandle"), + @JsonSubTypes.Type(value = DeleteHandle.class, name = "DeleteHandle"), + }) + @SuppressWarnings({"EmptyClass", "ClassMayBeInterface"}) + public abstract static class WriterTarget + { + @Override + public abstract String toString(); + } + + // only used during planning -- will not be serialized + public static class CreateName + extends WriterTarget + { + private final String catalog; + private final TableMetadata tableMetadata; + + public CreateName(String catalog, TableMetadata tableMetadata) + { + this.catalog = checkNotNull(catalog, "catalog is null"); + this.tableMetadata = checkNotNull(tableMetadata, "tableMetadata is null"); + } + + public String getCatalog() + { + return catalog; + } + + public TableMetadata getTableMetadata() + { + return tableMetadata; + } + + @Override + public String toString() + { + return catalog + "." + tableMetadata.getTable(); + } + } + + public static class CreateHandle + extends WriterTarget + { + private final OutputTableHandle handle; + + @JsonCreator + public CreateHandle(@JsonProperty("handle") OutputTableHandle handle) + { + this.handle = checkNotNull(handle, "handle is null"); + } + + @JsonProperty + public OutputTableHandle getHandle() + { + return handle; + } + + @Override + public String toString() + { + return handle.toString(); + } + } + + // only used during planning -- will not be serialized + public static class InsertReference + extends WriterTarget + { + private final TableHandle handle; + private final InsertOption insertOption; + + public InsertReference(TableHandle handle, InsertOption insertOption) + { + this.handle = checkNotNull(handle, "handle is null"); + this.insertOption = checkNotNull(insertOption, "insertOption is null"); + } + + public TableHandle getHandle() + { + return handle; + } + + public InsertOption getInsertOption() + { + return insertOption; + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("handle", handle.toString()) + .add("insertOption", insertOption) + .toString(); + } + } + + public static class InsertHandle + extends WriterTarget + { + private final InsertTableHandle handle; + + @JsonCreator + public InsertHandle(@JsonProperty("handle") InsertTableHandle handle) + { + this.handle = checkNotNull(handle, "handle is null"); + } + + @JsonProperty + public InsertTableHandle getHandle() + { + return handle; + } + + @Override + public String toString() + { + return handle.toString(); + } + } + + public static class DeleteHandle + extends WriterTarget + { + private final TableHandle handle; + + @JsonCreator + public DeleteHandle(@JsonProperty("handle") TableHandle handle) + { + this.handle = checkNotNull(handle, "handle is null"); + } + + @JsonProperty + public TableHandle getHandle() + { + return handle; + } + + @Override + public String toString() + { + return handle.toString(); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/TopNNode.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/TopNNode.java new file mode 100644 index 00000000..fc694d84 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/TopNNode.java @@ -0,0 +1,109 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.plan; + +import com.facebook.presto.spi.block.SortOrder; +import com.facebook.presto.sql.planner.Symbol; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +import javax.annotation.concurrent.Immutable; + +import java.util.List; +import java.util.Map; + +@Immutable +public class TopNNode + extends PlanNode +{ + private final PlanNode source; + private final long count; + private final List orderBy; + private final Map orderings; + private final boolean partial; + + @JsonCreator + public TopNNode(@JsonProperty("id") PlanNodeId id, + @JsonProperty("source") PlanNode source, + @JsonProperty("count") long count, + @JsonProperty("orderBy") List orderBy, + @JsonProperty("orderings") Map orderings, + @JsonProperty("partial") boolean partial) + { + super(id); + + Preconditions.checkNotNull(source, "source is null"); + Preconditions.checkArgument(count >= 0, "count must be positive"); + Preconditions.checkNotNull(orderBy, "orderBy is null"); + Preconditions.checkArgument(!orderBy.isEmpty(), "orderBy is empty"); + Preconditions.checkArgument(orderings.size() == orderBy.size(), "orderBy and orderings sizes don't match"); + + this.source = source; + this.count = count; + this.orderBy = ImmutableList.copyOf(orderBy); + this.orderings = ImmutableMap.copyOf(orderings); + this.partial = partial; + } + + @Override + public List getSources() + { + return ImmutableList.of(source); + } + + @JsonProperty("source") + public PlanNode getSource() + { + return source; + } + + @Override + public List getOutputSymbols() + { + return source.getOutputSymbols(); + } + + @JsonProperty("count") + public long getCount() + { + return count; + } + + @JsonProperty("orderBy") + public List getOrderBy() + { + return orderBy; + } + + @JsonProperty("orderings") + public Map getOrderings() + { + return orderings; + } + + @JsonProperty("partial") + public boolean isPartial() + { + return partial; + } + + @Override + public R accept(PlanVisitor visitor, C context) + { + return visitor.visitTopN(this, context); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/TopNRowNumberNode.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/TopNRowNumberNode.java new file mode 100644 index 00000000..4982b6ea --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/TopNRowNumberNode.java @@ -0,0 +1,147 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.plan; + +import com.facebook.presto.spi.block.SortOrder; +import com.facebook.presto.sql.planner.Symbol; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +import javax.annotation.concurrent.Immutable; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.Iterables.concat; + +@Immutable +public final class TopNRowNumberNode + extends PlanNode +{ + private final PlanNode source; + private final List partitionBy; + private final List orderBy; + private final Map orderings; + private final Symbol rowNumberSymbol; + private final int maxRowCountPerPartition; + private final boolean partial; + private final Optional hashSymbol; + + @JsonCreator + public TopNRowNumberNode( + @JsonProperty("id") PlanNodeId id, + @JsonProperty("source") PlanNode source, + @JsonProperty("partitionBy") List partitionBy, + @JsonProperty("orderBy") List orderBy, + @JsonProperty("orderings") Map orderings, + @JsonProperty("rowNumberSymbol") Symbol rowNumberSymbol, + @JsonProperty("maxRowCountPerPartition") int maxRowCountPerPartition, + @JsonProperty("partial") boolean partial, + @JsonProperty("hashSymbol") Optional hashSymbol) + { + super(id); + + checkNotNull(source, "source is null"); + checkNotNull(partitionBy, "partitionBy is null"); + checkNotNull(orderBy, "orderBy is null"); + checkNotNull(orderings, "orderings is null"); + checkArgument(orderings.size() == orderBy.size(), "orderBy and orderings sizes don't match"); + checkNotNull(rowNumberSymbol, "rowNumberSymbol is null"); + checkArgument(maxRowCountPerPartition > 0, "maxRowCountPerPartition must be > 0"); + checkNotNull(hashSymbol, "hashSymbol is null"); + + this.source = source; + this.partitionBy = ImmutableList.copyOf(partitionBy); + this.orderBy = ImmutableList.copyOf(orderBy); + this.orderings = ImmutableMap.copyOf(orderings); + this.rowNumberSymbol = rowNumberSymbol; + this.maxRowCountPerPartition = maxRowCountPerPartition; + this.partial = partial; + this.hashSymbol = hashSymbol; + } + + @Override + public List getSources() + { + return ImmutableList.of(source); + } + + @Override + public List getOutputSymbols() + { + if (!partial) { + return ImmutableList.copyOf(concat(source.getOutputSymbols(), ImmutableList.of(rowNumberSymbol))); + } + return ImmutableList.copyOf(source.getOutputSymbols()); + } + + @JsonProperty + public PlanNode getSource() + { + return source; + } + + @JsonProperty + public List getPartitionBy() + { + return partitionBy; + } + + @JsonProperty + public List getOrderBy() + { + return orderBy; + } + + @JsonProperty + public Map getOrderings() + { + return orderings; + } + + @JsonProperty + public Symbol getRowNumberSymbol() + { + return rowNumberSymbol; + } + + @JsonProperty + public int getMaxRowCountPerPartition() + { + return maxRowCountPerPartition; + } + + @JsonProperty + public boolean isPartial() + { + return partial; + } + + @JsonProperty + public Optional getHashSymbol() + { + return hashSymbol; + } + + @Override + public R accept(PlanVisitor visitor, C context) + { + return visitor.visitTopNRowNumber(this, context); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/UnionNode.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/UnionNode.java new file mode 100644 index 00000000..39c213ed --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/UnionNode.java @@ -0,0 +1,135 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.plan; + +import com.facebook.presto.sql.planner.Symbol; +import com.facebook.presto.sql.tree.QualifiedNameReference; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Function; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; +import com.google.common.collect.ListMultimap; +import com.google.common.collect.Multimap; +import com.google.common.collect.Multimaps; + +import javax.annotation.concurrent.Immutable; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static com.facebook.presto.util.ImmutableCollectors.toImmutableList; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Immutable +public class UnionNode + extends PlanNode +{ + private final List sources; + private final ImmutableListMultimap symbolMapping; // Key is output symbol, value is a List of the input symbols supplying that output + + @JsonCreator + public UnionNode(@JsonProperty("id") PlanNodeId id, + @JsonProperty("sources") List sources, + @JsonProperty("symbolMapping") ListMultimap symbolMapping) + { + super(id); + + checkNotNull(sources, "sources is null"); + checkArgument(!sources.isEmpty(), "Must have at least one source"); + checkNotNull(symbolMapping, "symbolMapping is null"); + + this.sources = ImmutableList.copyOf(sources); + this.symbolMapping = ImmutableListMultimap.copyOf(symbolMapping); + + for (Collection symbols : this.symbolMapping.asMap().values()) { + checkArgument(symbols.size() == this.sources.size(), "Every source needs to map its symbols to an output UNION symbol"); + } + + // Make sure each source positionally corresponds to their Symbol values in the Multimap + for (int i = 0; i < sources.size(); i++) { + for (Collection symbols : this.symbolMapping.asMap().values()) { + checkArgument(sources.get(i).getOutputSymbols().contains(Iterables.get(symbols, i)), "Source does not provide required symbols"); + } + } + } + + @Override + @JsonProperty("sources") + public List getSources() + { + return sources; + } + + @Override + public List getOutputSymbols() + { + return ImmutableList.copyOf(symbolMapping.keySet()); + } + + @JsonProperty("symbolMapping") + public ListMultimap getSymbolMapping() + { + return symbolMapping; + } + + public List sourceOutputLayout(int sourceIndex) + { + // Make sure the sourceOutputLayout symbols are listed in the same order as the corresponding output symbols + return getOutputSymbols().stream() + .map(symbol -> symbolMapping.get(symbol).get(sourceIndex)) + .collect(toImmutableList()); + } + + /** + * Returns the output to input symbol mapping for the given source channel + */ + public Map sourceSymbolMap(int sourceIndex) + { + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (Map.Entry> entry : symbolMapping.asMap().entrySet()) { + builder.put(entry.getKey(), Iterables.get(entry.getValue(), sourceIndex).toQualifiedNameReference()); + } + + return builder.build(); + } + + /** + * Returns the input to output symbol mapping for the given source channel. + * A single input symbol can map to multiple output symbols, thus requiring a Multimap. + */ + public Multimap outputSymbolMap(int sourceIndex) + { + return Multimaps.transformValues(FluentIterable.from(getOutputSymbols()) + .toMap(outputToSourceSymbolFunction(sourceIndex)) + .asMultimap() + .inverse(), Symbol::toQualifiedNameReference); + } + + private Function outputToSourceSymbolFunction(final int sourceIndex) + { + return outputSymbol -> symbolMapping.get(outputSymbol).get(sourceIndex); + } + + @Override + public R accept(PlanVisitor visitor, C context) + { + return visitor.visitUnion(this, context); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/UnnestNode.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/UnnestNode.java new file mode 100644 index 00000000..414dc50a --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/UnnestNode.java @@ -0,0 +1,107 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.plan; + +import com.facebook.presto.sql.planner.Symbol; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; + +import javax.annotation.concurrent.Immutable; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Immutable +public class UnnestNode + extends PlanNode +{ + private final PlanNode source; + private final List replicateSymbols; + private final Map> unnestSymbols; + private final Optional ordinalitySymbol; + + @JsonCreator + public UnnestNode( + @JsonProperty("id") PlanNodeId id, + @JsonProperty("source") PlanNode source, + @JsonProperty("replicateSymbols") List replicateSymbols, + @JsonProperty("unnestSymbols") Map> unnestSymbols, + @JsonProperty("ordinalitySymbol") Optional ordinalitySymbol) + { + super(id); + this.source = checkNotNull(source, "source is null"); + this.replicateSymbols = ImmutableList.copyOf(checkNotNull(replicateSymbols, "replicateSymbols is null")); + checkNotNull(unnestSymbols, "unnestSymbols is null"); + checkArgument(!unnestSymbols.isEmpty(), "unnestSymbols is empty"); + ImmutableMap.Builder> builder = ImmutableMap.builder(); + for (Map.Entry> entry : unnestSymbols.entrySet()) { + builder.put(entry.getKey(), ImmutableList.copyOf(entry.getValue())); + } + this.unnestSymbols = builder.build(); + this.ordinalitySymbol = checkNotNull(ordinalitySymbol, "ordinalitySymbol is null"); + } + + @Override + public List getOutputSymbols() + { + ImmutableList.Builder outputSymbolsBuilder = ImmutableList.builder() + .addAll(replicateSymbols) + .addAll(Iterables.concat(unnestSymbols.values())); + ordinalitySymbol.ifPresent(outputSymbolsBuilder::add); + return outputSymbolsBuilder.build(); + } + + @JsonProperty + public PlanNode getSource() + { + return source; + } + + @JsonProperty + public List getReplicateSymbols() + { + return replicateSymbols; + } + + @JsonProperty + public Map> getUnnestSymbols() + { + return unnestSymbols; + } + + @JsonProperty + public Optional getOrdinalitySymbol() + { + return ordinalitySymbol; + } + + @Override + public List getSources() + { + return ImmutableList.of(source); + } + + @Override + public R accept(PlanVisitor visitor, C context) + { + return visitor.visitUnnest(this, context); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/ValuesNode.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/ValuesNode.java new file mode 100644 index 00000000..c047cd82 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/ValuesNode.java @@ -0,0 +1,74 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.plan; + +import com.facebook.presto.sql.planner.Symbol; +import com.facebook.presto.sql.tree.Expression; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; + +import javax.annotation.concurrent.Immutable; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkArgument; + +@Immutable +public class ValuesNode + extends PlanNode +{ + private final List outputSymbols; + private final List> rows; + + @JsonCreator + public ValuesNode(@JsonProperty("id") PlanNodeId id, + @JsonProperty("outputSymbols") List outputSymbols, + @JsonProperty("rows") List> rows) + { + super(id); + this.outputSymbols = ImmutableList.copyOf(outputSymbols); + this.rows = ImmutableList.copyOf(rows); + + for (List row : rows) { + checkArgument(row.size() == outputSymbols.size() || row.size() == 0, + "Expected row to have %s values, but row has %s values", outputSymbols.size(), row.size()); + } + } + + @Override + @JsonProperty + public List getOutputSymbols() + { + return outputSymbols; + } + + @JsonProperty + public List> getRows() + { + return rows; + } + + @Override + public List getSources() + { + return ImmutableList.of(); + } + + @Override + public R accept(PlanVisitor visitor, C context) + { + return visitor.visitValues(this, context); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/WindowNode.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/WindowNode.java new file mode 100644 index 00000000..d9a6dcdb --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/plan/WindowNode.java @@ -0,0 +1,228 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.planner.plan; + +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.spi.block.SortOrder; +import com.facebook.presto.sql.planner.Symbol; +import com.facebook.presto.sql.tree.FrameBound; +import com.facebook.presto.sql.tree.FunctionCall; +import com.facebook.presto.sql.tree.WindowFrame; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; + +import javax.annotation.concurrent.Immutable; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.Iterables.concat; + +@Immutable +public class WindowNode + extends PlanNode +{ + private final PlanNode source; + private final List partitionBy; + private final Set prePartitionedInputs; + private final List orderBy; + private final Map orderings; + private final int preSortedOrderPrefix; + private final Frame frame; + private final Map windowFunctions; + private final Map functionHandles; + private final Optional hashSymbol; + + @JsonCreator + public WindowNode( + @JsonProperty("id") PlanNodeId id, + @JsonProperty("source") PlanNode source, + @JsonProperty("partitionBy") List partitionBy, + @JsonProperty("orderBy") List orderBy, + @JsonProperty("orderings") Map orderings, + @JsonProperty("frame") Frame frame, + @JsonProperty("windowFunctions") Map windowFunctions, + @JsonProperty("signatures") Map signatures, + @JsonProperty("hashSymbol") Optional hashSymbol, + @JsonProperty("prePartitionedInputs") Set prePartitionedInputs, + @JsonProperty("preSortedOrderPrefix") int preSortedOrderPrefix) + { + super(id); + + checkNotNull(source, "source is null"); + checkNotNull(partitionBy, "partitionBy is null"); + checkNotNull(orderBy, "orderBy is null"); + checkArgument(orderings.size() == orderBy.size(), "orderBy and orderings sizes don't match"); + checkArgument(orderings.keySet().containsAll(orderBy), "Every orderBy symbol must have an ordering direction"); + checkNotNull(frame, "frame is null"); + checkNotNull(windowFunctions, "windowFunctions is null"); + checkNotNull(signatures, "signatures is null"); + checkArgument(windowFunctions.keySet().equals(signatures.keySet()), "windowFunctions does not match signatures"); + checkNotNull(hashSymbol, "hashSymbol is null"); + checkArgument(partitionBy.containsAll(prePartitionedInputs), "prePartitionedInputs must be contained in partitionBy"); + checkArgument(preSortedOrderPrefix <= orderBy.size(), "Cannot have sorted more symbols than those requested"); + checkArgument(preSortedOrderPrefix == 0 || ImmutableSet.copyOf(prePartitionedInputs).equals(ImmutableSet.copyOf(partitionBy)), "preSortedOrderPrefix can only be greater than zero if all partition symbols are pre-partitioned"); + + this.source = source; + this.partitionBy = ImmutableList.copyOf(partitionBy); + this.prePartitionedInputs = ImmutableSet.copyOf(prePartitionedInputs); + this.orderBy = ImmutableList.copyOf(orderBy); + this.orderings = ImmutableMap.copyOf(orderings); + this.frame = frame; + this.windowFunctions = ImmutableMap.copyOf(windowFunctions); + this.functionHandles = ImmutableMap.copyOf(signatures); + this.hashSymbol = hashSymbol; + this.preSortedOrderPrefix = preSortedOrderPrefix; + } + + @Override + public List getSources() + { + return ImmutableList.of(source); + } + + @Override + public List getOutputSymbols() + { + return ImmutableList.copyOf(concat(source.getOutputSymbols(), windowFunctions.keySet())); + } + + @JsonProperty + public PlanNode getSource() + { + return source; + } + + @JsonProperty + public List getPartitionBy() + { + return partitionBy; + } + + @JsonProperty + public List getOrderBy() + { + return orderBy; + } + + @JsonProperty + public Map getOrderings() + { + return orderings; + } + + @JsonProperty + public Frame getFrame() + { + return frame; + } + + @JsonProperty + public Map getWindowFunctions() + { + return windowFunctions; + } + + @JsonProperty + public Map getSignatures() + { + return functionHandles; + } + + @JsonProperty + public Optional getHashSymbol() + { + return hashSymbol; + } + + @JsonProperty + public Set getPrePartitionedInputs() + { + return prePartitionedInputs; + } + + @JsonProperty + public int getPreSortedOrderPrefix() + { + return preSortedOrderPrefix; + } + + @Override + public R accept(PlanVisitor visitor, C context) + { + return visitor.visitWindow(this, context); + } + + @Immutable + public static class Frame + { + private final WindowFrame.Type type; + private final FrameBound.Type startType; + private final Optional startValue; + private final FrameBound.Type endType; + private final Optional endValue; + + @JsonCreator + public Frame( + @JsonProperty("type") WindowFrame.Type type, + @JsonProperty("startType") FrameBound.Type startType, + @JsonProperty("startValue") Optional startValue, + @JsonProperty("endType") FrameBound.Type endType, + @JsonProperty("endValue") Optional endValue) + { + this.startType = checkNotNull(startType, "startType is null"); + this.startValue = checkNotNull(startValue, "startValue is null"); + this.endType = checkNotNull(endType, "endType is null"); + this.endValue = checkNotNull(endValue, "endValue is null"); + this.type = checkNotNull(type, "type is null"); + } + + @JsonProperty + public WindowFrame.Type getType() + { + return type; + } + + @JsonProperty + public FrameBound.Type getStartType() + { + return startType; + } + + @JsonProperty + public Optional getStartValue() + { + return startValue; + } + + @JsonProperty + public FrameBound.Type getEndType() + { + return endType; + } + + @JsonProperty + public Optional getEndValue() + { + return endValue; + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/relational/CallExpression.java b/presto-main/src/main/java/com/facebook/presto/sql/relational/CallExpression.java new file mode 100644 index 00000000..b8d45b31 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/relational/CallExpression.java @@ -0,0 +1,89 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.relational; + +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.spi.type.Type; +import com.google.common.base.Joiner; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; + +import java.util.List; +import java.util.Objects; + +public final class CallExpression + extends RowExpression +{ + private final Signature signature; + private final Type returnType; + private final List arguments; + + public CallExpression(Signature signature, Type returnType, List arguments) + { + Preconditions.checkNotNull(signature, "signature is null"); + Preconditions.checkNotNull(arguments, "arguments is null"); + Preconditions.checkNotNull(returnType, "returnType is null"); + + this.signature = signature; + this.returnType = returnType; + this.arguments = ImmutableList.copyOf(arguments); + } + + public Signature getSignature() + { + return signature; + } + + @Override + public Type getType() + { + return returnType; + } + + public List getArguments() + { + return arguments; + } + + @Override + public String toString() + { + return signature.getName() + "(" + Joiner.on(", ").join(arguments) + ")"; + } + + @Override + public int hashCode() + { + return Objects.hash(signature, arguments); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + CallExpression other = (CallExpression) obj; + return Objects.equals(this.signature, other.signature) && Objects.equals(this.arguments, other.arguments); + } + + @Override + public R accept(RowExpressionVisitor visitor, C context) + { + return visitor.visitCall(this, context); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/relational/ConstantExpression.java b/presto-main/src/main/java/com/facebook/presto/sql/relational/ConstantExpression.java new file mode 100644 index 00000000..a6bb406b --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/relational/ConstantExpression.java @@ -0,0 +1,76 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.relational; + +import com.facebook.presto.spi.type.Type; +import com.google.common.base.Preconditions; + +import java.util.Objects; + +public final class ConstantExpression + extends RowExpression +{ + private final Object value; + private final Type type; + + public ConstantExpression(Object value, Type type) + { + Preconditions.checkNotNull(type, "type is null"); + + this.value = value; + this.type = type; + } + + public Object getValue() + { + return value; + } + + @Override + public Type getType() + { + return type; + } + + @Override + public String toString() + { + return String.valueOf(value); + } + + @Override + public int hashCode() + { + return Objects.hash(value, type); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + ConstantExpression other = (ConstantExpression) obj; + return Objects.equals(this.value, other.value) && Objects.equals(this.type, other.type); + } + + @Override + public R accept(RowExpressionVisitor visitor, C context) + { + return visitor.visitConstant(this, context); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/relational/Expressions.java b/presto-main/src/main/java/com/facebook/presto/sql/relational/Expressions.java new file mode 100644 index 00000000..84ae0736 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/relational/Expressions.java @@ -0,0 +1,89 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.relational; + +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; + +import java.util.Arrays; +import java.util.List; + +public final class Expressions +{ + private Expressions() + { + } + + public static ConstantExpression constant(Object value, Type type) + { + return new ConstantExpression(value, type); + } + + public static ConstantExpression constantNull(Type type) + { + return new ConstantExpression(null, type); + } + + public static CallExpression call(Signature signature, Type returnType, RowExpression... arguments) + { + return new CallExpression(signature, returnType, Arrays.asList(arguments)); + } + + public static CallExpression call(Signature signature, Type returnType, List arguments) + { + return new CallExpression(signature, returnType, arguments); + } + + public static InputReferenceExpression field(int field, Type type) + { + return new InputReferenceExpression(field, type); + } + + public static List subExpressions(Iterable expressions) + { + final ImmutableList.Builder builder = ImmutableList.builder(); + + for (RowExpression expression : expressions) { + expression.accept(new RowExpressionVisitor() + { + @Override + public Void visitCall(CallExpression call, Void context) + { + builder.add(call); + for (RowExpression argument : call.getArguments()) { + argument.accept(this, context); + } + return null; + } + + @Override + public Void visitInputReference(InputReferenceExpression reference, Void context) + { + builder.add(reference); + return null; + } + + @Override + public Void visitConstant(ConstantExpression literal, Void context) + { + builder.add(literal); + return null; + } + }, null); + } + + return builder.build(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/relational/InputReferenceExpression.java b/presto-main/src/main/java/com/facebook/presto/sql/relational/InputReferenceExpression.java new file mode 100644 index 00000000..17db30a7 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/relational/InputReferenceExpression.java @@ -0,0 +1,79 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.relational; + +import com.facebook.presto.spi.type.Type; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Preconditions; + +import java.util.Objects; + +public final class InputReferenceExpression + extends RowExpression +{ + private final int field; + private final Type type; + + public InputReferenceExpression(int field, Type type) + { + Preconditions.checkNotNull(type, "type is null"); + + this.field = field; + this.type = type; + } + + @JsonProperty + public int getField() + { + return field; + } + + @Override + @JsonProperty + public Type getType() + { + return type; + } + + @Override + public int hashCode() + { + return Objects.hash(field, type); + } + + @Override + public String toString() + { + return "#" + field; + } + + @Override + public R accept(RowExpressionVisitor visitor, C context) + { + return visitor.visitInputReference(this, context); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + InputReferenceExpression other = (InputReferenceExpression) obj; + return Objects.equals(this.field, other.field) && Objects.equals(this.type, other.type); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/relational/RowExpression.java b/presto-main/src/main/java/com/facebook/presto/sql/relational/RowExpression.java new file mode 100644 index 00000000..4a03a9eb --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/relational/RowExpression.java @@ -0,0 +1,31 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.relational; + +import com.facebook.presto.spi.type.Type; + +public abstract class RowExpression +{ + public abstract Type getType(); + + @Override + public abstract boolean equals(Object other); + @Override + public abstract int hashCode(); + + @Override + public abstract String toString(); + + public abstract R accept(RowExpressionVisitor visitor, C context); +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/relational/RowExpressionVisitor.java b/presto-main/src/main/java/com/facebook/presto/sql/relational/RowExpressionVisitor.java new file mode 100644 index 00000000..e4fde246 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/relational/RowExpressionVisitor.java @@ -0,0 +1,21 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.relational; + +public interface RowExpressionVisitor +{ + R visitCall(CallExpression call, C context); + R visitInputReference(InputReferenceExpression reference, C context); + R visitConstant(ConstantExpression literal, C context); +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/relational/Signatures.java b/presto-main/src/main/java/com/facebook/presto/sql/relational/Signatures.java new file mode 100644 index 00000000..e1242129 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/relational/Signatures.java @@ -0,0 +1,161 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.relational; + +import com.facebook.presto.metadata.OperatorType; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeSignature; +import com.facebook.presto.sql.tree.ArithmeticBinaryExpression; +import com.facebook.presto.sql.tree.ComparisonExpression; +import com.facebook.presto.sql.tree.LogicalBinaryExpression; +import com.facebook.presto.type.LikePatternType; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; + +import java.util.List; + +import static com.facebook.presto.metadata.FunctionRegistry.mangleOperatorName; +import static com.facebook.presto.metadata.OperatorType.SUBSCRIPT; +import static com.facebook.presto.metadata.Signature.internalFunction; +import static com.facebook.presto.metadata.Signature.internalOperator; +import static com.facebook.presto.spi.type.TypeSignature.parseTypeSignature; +import static com.facebook.presto.sql.tree.ArrayConstructor.ARRAY_CONSTRUCTOR; + +public final class Signatures +{ + public static final String IF = "IF"; + public static final String NULL_IF = "NULL_IF"; + public static final String SWITCH = "SWITCH"; + public static final String CAST = mangleOperatorName("CAST"); + public static final String TRY_CAST = "TRY_CAST"; + public static final String IS_NULL = "IS_NULL"; + public static final String COALESCE = "COALESCE"; + public static final String IN = "IN"; + + private Signatures() + { + } + + // **************** sql operators **************** + public static Signature notSignature() + { + return new Signature("not", StandardTypes.BOOLEAN, ImmutableList.of(StandardTypes.BOOLEAN)); + } + + public static Signature betweenSignature(Type valueType, Type minType, Type maxType) + { + return internalOperator("BETWEEN", parseTypeSignature(StandardTypes.BOOLEAN), valueType.getTypeSignature(), minType.getTypeSignature(), maxType.getTypeSignature()); + } + + public static Signature likeSignature() + { + return internalFunction("LIKE", StandardTypes.BOOLEAN, StandardTypes.VARCHAR, LikePatternType.NAME); + } + + public static Signature likePatternSignature() + { + return internalFunction("LIKE_PATTERN", LikePatternType.NAME, StandardTypes.VARCHAR, StandardTypes.VARCHAR); + } + + public static Signature castSignature(Type returnType, Type valueType) + { + // Name has already been mangled, so don't use internalOperator + return internalFunction(CAST, returnType.getTypeSignature(), valueType.getTypeSignature()); + } + + public static Signature tryCastSignature(Type returnType, Type valueType) + { + return internalFunction(TRY_CAST, returnType.getTypeSignature(), valueType.getTypeSignature()); + } + + public static Signature logicalExpressionSignature(LogicalBinaryExpression.Type expressionType) + { + return internalFunction(expressionType.name(), StandardTypes.BOOLEAN, StandardTypes.BOOLEAN, StandardTypes.BOOLEAN); + } + + public static Signature arithmeticNegationSignature(Type returnType, Type valueType) + { + return internalOperator("NEGATION", returnType.getTypeSignature(), valueType.getTypeSignature()); + } + + public static Signature arithmeticExpressionSignature(ArithmeticBinaryExpression.Type expressionType, Type returnType, Type leftType, Type rightType) + { + return internalOperator(expressionType.name(), returnType.getTypeSignature(), leftType.getTypeSignature(), rightType.getTypeSignature()); + } + + public static Signature subscriptSignature(Type returnType, Type leftType, Type rightType) + { + return internalOperator(SUBSCRIPT.name(), returnType.getTypeSignature(), leftType.getTypeSignature(), rightType.getTypeSignature()); + } + + public static Signature arrayConstructorSignature(Type returnType, List argumentTypes) + { + return internalFunction(ARRAY_CONSTRUCTOR, returnType.getTypeSignature(), Lists.transform(argumentTypes, Type::getTypeSignature)); + } + + public static Signature arrayConstructorSignature(TypeSignature returnType, List argumentTypes) + { + return internalFunction(ARRAY_CONSTRUCTOR, returnType, argumentTypes); + } + + public static Signature comparisonExpressionSignature(ComparisonExpression.Type expressionType, Type leftType, Type rightType) + { + for (OperatorType operatorType : OperatorType.values()) { + if (operatorType.name().equals(expressionType.name())) { + return internalOperator(expressionType.name(), parseTypeSignature(StandardTypes.BOOLEAN), leftType.getTypeSignature(), rightType.getTypeSignature()); + } + } + return internalFunction(expressionType.name(), parseTypeSignature(StandardTypes.BOOLEAN), leftType.getTypeSignature(), rightType.getTypeSignature()); + } + + // **************** special forms (lazy evaluation, etc) **************** + public static Signature ifSignature(Type returnType) + { + return new Signature(IF, returnType.getTypeSignature()); + } + + public static Signature nullIfSignature(Type returnType, Type firstType, Type secondType) + { + return new Signature(NULL_IF, returnType.getTypeSignature(), firstType.getTypeSignature(), secondType.getTypeSignature()); + } + + public static Signature switchSignature(Type returnType) + { + return new Signature(SWITCH, returnType.getTypeSignature()); + } + + public static Signature whenSignature(Type returnType) + { + return new Signature("WHEN", returnType.getTypeSignature()); + } + + // **************** functions that require varargs and/or complex types (e.g., lists) **************** + public static Signature inSignature() + { + return internalFunction(IN, StandardTypes.BOOLEAN); + } + + // **************** functions that need to do special null handling **************** + public static Signature isNullSignature(Type argumentType) + { + return internalFunction(IS_NULL, parseTypeSignature(StandardTypes.BOOLEAN), argumentType.getTypeSignature()); + } + + public static Signature coalesceSignature(Type returnType, List argumentTypes) + { + return internalFunction(COALESCE, returnType.getTypeSignature(), Lists.transform(argumentTypes, Type::getTypeSignature)); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/relational/SqlToRowExpressionTranslator.java b/presto-main/src/main/java/com/facebook/presto/sql/relational/SqlToRowExpressionTranslator.java new file mode 100644 index 00000000..64570b12 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/relational/SqlToRowExpressionTranslator.java @@ -0,0 +1,516 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.relational; + +import com.facebook.presto.Session; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.spi.type.TimeZoneKey; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.spi.type.TypeSignature; +import com.facebook.presto.sql.relational.optimizer.ExpressionOptimizer; +import com.facebook.presto.sql.tree.ArithmeticBinaryExpression; +import com.facebook.presto.sql.tree.ArithmeticUnaryExpression; +import com.facebook.presto.sql.tree.ArrayConstructor; +import com.facebook.presto.sql.tree.AstVisitor; +import com.facebook.presto.sql.tree.BetweenPredicate; +import com.facebook.presto.sql.tree.BooleanLiteral; +import com.facebook.presto.sql.tree.Cast; +import com.facebook.presto.sql.tree.CoalesceExpression; +import com.facebook.presto.sql.tree.ComparisonExpression; +import com.facebook.presto.sql.tree.DoubleLiteral; +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.FunctionCall; +import com.facebook.presto.sql.tree.GenericLiteral; +import com.facebook.presto.sql.tree.IfExpression; +import com.facebook.presto.sql.tree.InListExpression; +import com.facebook.presto.sql.tree.InPredicate; +import com.facebook.presto.sql.tree.InputReference; +import com.facebook.presto.sql.tree.IntervalLiteral; +import com.facebook.presto.sql.tree.IsNotNullPredicate; +import com.facebook.presto.sql.tree.IsNullPredicate; +import com.facebook.presto.sql.tree.LikePredicate; +import com.facebook.presto.sql.tree.LogicalBinaryExpression; +import com.facebook.presto.sql.tree.LongLiteral; +import com.facebook.presto.sql.tree.NotExpression; +import com.facebook.presto.sql.tree.NullIfExpression; +import com.facebook.presto.sql.tree.NullLiteral; +import com.facebook.presto.sql.tree.SearchedCaseExpression; +import com.facebook.presto.sql.tree.SimpleCaseExpression; +import com.facebook.presto.sql.tree.StringLiteral; +import com.facebook.presto.sql.tree.SubscriptExpression; +import com.facebook.presto.sql.tree.TimeLiteral; +import com.facebook.presto.sql.tree.TimestampLiteral; +import com.facebook.presto.sql.tree.WhenClause; +import com.facebook.presto.type.UnknownType; +import com.google.common.base.Preconditions; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import io.airlift.slice.Slices; + +import java.nio.charset.StandardCharsets; +import java.util.IdentityHashMap; +import java.util.List; + +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.BooleanType.BOOLEAN; +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; +import static com.facebook.presto.spi.type.TimeWithTimeZoneType.TIME_WITH_TIME_ZONE; +import static com.facebook.presto.spi.type.TimestampWithTimeZoneType.TIMESTAMP_WITH_TIME_ZONE; +import static com.facebook.presto.spi.type.TypeSignature.parseTypeSignature; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static com.facebook.presto.sql.relational.Expressions.call; +import static com.facebook.presto.sql.relational.Expressions.constant; +import static com.facebook.presto.sql.relational.Expressions.constantNull; +import static com.facebook.presto.sql.relational.Expressions.field; +import static com.facebook.presto.sql.relational.Signatures.arithmeticExpressionSignature; +import static com.facebook.presto.sql.relational.Signatures.arithmeticNegationSignature; +import static com.facebook.presto.sql.relational.Signatures.arrayConstructorSignature; +import static com.facebook.presto.sql.relational.Signatures.betweenSignature; +import static com.facebook.presto.sql.relational.Signatures.castSignature; +import static com.facebook.presto.sql.relational.Signatures.coalesceSignature; +import static com.facebook.presto.sql.relational.Signatures.comparisonExpressionSignature; +import static com.facebook.presto.sql.relational.Signatures.likePatternSignature; +import static com.facebook.presto.sql.relational.Signatures.likeSignature; +import static com.facebook.presto.sql.relational.Signatures.logicalExpressionSignature; +import static com.facebook.presto.sql.relational.Signatures.nullIfSignature; +import static com.facebook.presto.sql.relational.Signatures.subscriptSignature; +import static com.facebook.presto.sql.relational.Signatures.switchSignature; +import static com.facebook.presto.sql.relational.Signatures.tryCastSignature; +import static com.facebook.presto.sql.relational.Signatures.whenSignature; +import static com.facebook.presto.type.LikePatternType.LIKE_PATTERN; +import static com.facebook.presto.util.DateTimeUtils.parseDayTimeInterval; +import static com.facebook.presto.util.DateTimeUtils.parseTimeWithTimeZone; +import static com.facebook.presto.util.DateTimeUtils.parseTimeWithoutTimeZone; +import static com.facebook.presto.util.DateTimeUtils.parseTimestampWithTimeZone; +import static com.facebook.presto.util.DateTimeUtils.parseTimestampWithoutTimeZone; +import static com.facebook.presto.util.DateTimeUtils.parseYearMonthInterval; +import static com.facebook.presto.util.ImmutableCollectors.toImmutableList; + +public final class SqlToRowExpressionTranslator +{ + private SqlToRowExpressionTranslator() {} + + public static RowExpression translate( + Expression expression, + IdentityHashMap types, + FunctionRegistry functionRegistry, + TypeManager typeManager, + Session session, + boolean optimize) + { + RowExpression result = new Visitor(types, typeManager, session.getTimeZoneKey()).process(expression, null); + + Preconditions.checkNotNull(result, "translated expression is null"); + + if (optimize) { + ExpressionOptimizer optimizer = new ExpressionOptimizer(functionRegistry, typeManager, session); + return optimizer.optimize(result); + } + + return result; + } + + private static class Visitor + extends AstVisitor + { + private final IdentityHashMap types; + private final TypeManager typeManager; + private final TimeZoneKey timeZoneKey; + + private Visitor(IdentityHashMap types, TypeManager typeManager, TimeZoneKey timeZoneKey) + { + this.types = types; + this.typeManager = typeManager; + this.timeZoneKey = timeZoneKey; + } + + @Override + protected RowExpression visitExpression(Expression node, Void context) + { + throw new UnsupportedOperationException("not yet implemented: expression translator for " + node.getClass().getName()); + } + + @Override + protected RowExpression visitInputReference(InputReference node, Void context) + { + return field(node.getChannel(), types.get(node)); + } + + @Override + protected RowExpression visitNullLiteral(NullLiteral node, Void context) + { + return constantNull(UnknownType.UNKNOWN); + } + + @Override + protected RowExpression visitBooleanLiteral(BooleanLiteral node, Void context) + { + return constant(node.getValue(), BOOLEAN); + } + + @Override + protected RowExpression visitLongLiteral(LongLiteral node, Void context) + { + return constant(node.getValue(), BIGINT); + } + + @Override + protected RowExpression visitDoubleLiteral(DoubleLiteral node, Void context) + { + return constant(node.getValue(), DOUBLE); + } + + @Override + protected RowExpression visitStringLiteral(StringLiteral node, Void context) + { + return constant(node.getSlice(), VARCHAR); + } + + @Override + protected RowExpression visitGenericLiteral(GenericLiteral node, Void context) + { + Type type = typeManager.getType(parseTypeSignature(node.getType())); + if (type == null) { + throw new IllegalArgumentException("Unsupported type: " + node.getType()); + } + + return call( + castSignature(types.get(node), VARCHAR), + types.get(node), + constant(Slices.copiedBuffer(node.getValue(), StandardCharsets.UTF_8), VARCHAR)); + } + + @Override + protected RowExpression visitTimeLiteral(TimeLiteral node, Void context) + { + long value; + if (types.get(node).equals(TIME_WITH_TIME_ZONE)) { + value = parseTimeWithTimeZone(node.getValue()); + } + else { + // parse in time zone of client + value = parseTimeWithoutTimeZone(timeZoneKey, node.getValue()); + } + return constant(value, types.get(node)); + } + + @Override + protected RowExpression visitTimestampLiteral(TimestampLiteral node, Void context) + { + long value; + if (types.get(node).equals(TIMESTAMP_WITH_TIME_ZONE)) { + value = parseTimestampWithTimeZone(node.getValue()); + } + else { + // parse in time zone of client + value = parseTimestampWithoutTimeZone(timeZoneKey, node.getValue()); + } + return constant(value, types.get(node)); + } + + @Override + protected RowExpression visitIntervalLiteral(IntervalLiteral node, Void context) + { + long value; + if (node.isYearToMonth()) { + value = node.getSign().multiplier() * parseYearMonthInterval(node.getValue(), node.getStartField(), node.getEndField()); + } + else { + value = node.getSign().multiplier() * parseDayTimeInterval(node.getValue(), node.getStartField(), node.getEndField()); + } + return constant(value, types.get(node)); + } + + @Override + protected RowExpression visitComparisonExpression(ComparisonExpression node, Void context) + { + RowExpression left = process(node.getLeft(), context); + RowExpression right = process(node.getRight(), context); + + return call( + comparisonExpressionSignature(node.getType(), left.getType(), right.getType()), + BOOLEAN, + left, + right); + } + + @Override + protected RowExpression visitFunctionCall(FunctionCall node, Void context) + { + List arguments = node.getArguments().stream() + .map(value -> process(value, context)) + .collect(toImmutableList()); + + List argumentTypes = FluentIterable.from(arguments) + .transform(RowExpression::getType) + .transform(Type::getTypeSignature) + .toList(); + Signature signature = new Signature(node.getName().getSuffix(), types.get(node).getTypeSignature(), argumentTypes); + + return call(signature, types.get(node), arguments); + } + + @Override + protected RowExpression visitArithmeticBinary(ArithmeticBinaryExpression node, Void context) + { + RowExpression left = process(node.getLeft(), context); + RowExpression right = process(node.getRight(), context); + + return call( + arithmeticExpressionSignature(node.getType(), types.get(node), left.getType(), right.getType()), + types.get(node), + left, + right); + } + + @Override + protected RowExpression visitArithmeticUnary(ArithmeticUnaryExpression node, Void context) + { + RowExpression expression = process(node.getValue(), context); + + switch (node.getSign()) { + case PLUS: + return expression; + case MINUS: + return call( + arithmeticNegationSignature(types.get(node), expression.getType()), + types.get(node), + expression); + } + + throw new UnsupportedOperationException("Unsupported unary operator: " + node.getSign()); + } + + @Override + protected RowExpression visitLogicalBinaryExpression(LogicalBinaryExpression node, Void context) + { + return call( + logicalExpressionSignature(node.getType()), + BOOLEAN, + process(node.getLeft(), context), + process(node.getRight(), context)); + } + + @Override + protected RowExpression visitCast(Cast node, Void context) + { + RowExpression value = process(node.getExpression(), context); + + if (node.isSafe()) { + return call(tryCastSignature(types.get(node), value.getType()), types.get(node), value); + } + + return call(castSignature(types.get(node), value.getType()), types.get(node), value); + } + + @Override + protected RowExpression visitCoalesceExpression(CoalesceExpression node, Void context) + { + List arguments = node.getOperands().stream() + .map(value -> process(value, context)) + .collect(toImmutableList()); + + List argumentTypes = FluentIterable.from(arguments).transform(RowExpression::getType).toList(); + return call(coalesceSignature(types.get(node), argumentTypes), types.get(node), arguments); + } + + @Override + protected RowExpression visitSimpleCaseExpression(SimpleCaseExpression node, Void context) + { + ImmutableList.Builder arguments = ImmutableList.builder(); + + arguments.add(process(node.getOperand(), context)); + + for (WhenClause clause : node.getWhenClauses()) { + arguments.add(call(whenSignature(types.get(clause)), + types.get(clause), + process(clause.getOperand(), context), + process(clause.getResult(), context))); + } + + Type returnType = types.get(node); + + arguments.add(node.getDefaultValue() + .map((value) -> process(value, context)) + .orElse(constantNull(returnType))); + + return call(switchSignature(returnType), returnType, arguments.build()); + } + + @Override + protected RowExpression visitSearchedCaseExpression(SearchedCaseExpression node, Void context) + { + /* + Translates an expression like: + + case when cond1 then value1 + when cond2 then value2 + when cond3 then value3 + else value4 + end + + To: + + IF(cond1, + value1, + IF(cond2, + value2, + If(cond3, + value3, + value4))) + + */ + RowExpression expression = node.getDefaultValue() + .map((value) -> process(value, context)) + .orElse(constantNull(types.get(node))); + + for (WhenClause clause : Lists.reverse(node.getWhenClauses())) { + expression = call( + Signatures.ifSignature(types.get(node)), + types.get(node), + process(clause.getOperand(), context), + process(clause.getResult(), context), + expression); + } + + return expression; + } + + @Override + protected RowExpression visitIfExpression(IfExpression node, Void context) + { + ImmutableList.Builder arguments = ImmutableList.builder(); + + arguments.add(process(node.getCondition(), context)) + .add(process(node.getTrueValue(), context)); + + if (node.getFalseValue().isPresent()) { + arguments.add(process(node.getFalseValue().get(), context)); + } + else { + arguments.add(constantNull(types.get(node))); + } + + return call(Signatures.ifSignature(types.get(node)), types.get(node), arguments.build()); + } + + @Override + protected RowExpression visitInPredicate(InPredicate node, Void context) + { + ImmutableList.Builder arguments = ImmutableList.builder(); + arguments.add(process(node.getValue(), context)); + InListExpression values = (InListExpression) node.getValueList(); + for (Expression value : values.getValues()) { + arguments.add(process(value, context)); + } + + return call(Signatures.inSignature(), BOOLEAN, arguments.build()); + } + + @Override + protected RowExpression visitIsNotNullPredicate(IsNotNullPredicate node, Void context) + { + RowExpression expression = process(node.getValue(), context); + + return call( + Signatures.notSignature(), + BOOLEAN, + call(Signatures.isNullSignature(expression.getType()), BOOLEAN, ImmutableList.of(expression))); + } + + @Override + protected RowExpression visitIsNullPredicate(IsNullPredicate node, Void context) + { + RowExpression expression = process(node.getValue(), context); + + return call(Signatures.isNullSignature(expression.getType()), BOOLEAN, expression); + } + + @Override + protected RowExpression visitNotExpression(NotExpression node, Void context) + { + return call(Signatures.notSignature(), BOOLEAN, process(node.getValue(), context)); + } + + @Override + protected RowExpression visitNullIfExpression(NullIfExpression node, Void context) + { + RowExpression first = process(node.getFirst(), context); + RowExpression second = process(node.getSecond(), context); + + return call( + nullIfSignature(types.get(node), first.getType(), second.getType()), + types.get(node), + first, + second); + } + + @Override + protected RowExpression visitBetweenPredicate(BetweenPredicate node, Void context) + { + RowExpression value = process(node.getValue(), context); + RowExpression min = process(node.getMin(), context); + RowExpression max = process(node.getMax(), context); + + return call( + betweenSignature(value.getType(), min.getType(), max.getType()), + BOOLEAN, + value, + min, + max); + } + + @Override + protected RowExpression visitLikePredicate(LikePredicate node, Void context) + { + RowExpression value = process(node.getValue(), context); + RowExpression pattern = process(node.getPattern(), context); + + if (node.getEscape() != null) { + RowExpression escape = process(node.getEscape(), context); + return call(likeSignature(), BOOLEAN, value, call(likePatternSignature(), LIKE_PATTERN, pattern, escape)); + } + + return call(likeSignature(), BOOLEAN, value, call(castSignature(LIKE_PATTERN, VARCHAR), LIKE_PATTERN, pattern)); + } + + @Override + protected RowExpression visitSubscriptExpression(SubscriptExpression node, Void context) + { + RowExpression base = process(node.getBase(), context); + RowExpression index = process(node.getIndex(), context); + + return call( + subscriptSignature(types.get(node), base.getType(), index.getType()), + types.get(node), + base, + index); + } + + @Override + protected RowExpression visitArrayConstructor(ArrayConstructor node, Void context) + { + List arguments = node.getValues().stream() + .map(value -> process(value, context)) + .collect(toImmutableList()); + List argumentTypes = FluentIterable.from(arguments) + .transform(RowExpression::getType) + .toList(); + return call(arrayConstructorSignature(types.get(node), argumentTypes), types.get(node), arguments); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/sql/relational/optimizer/ExpressionOptimizer.java b/presto-main/src/main/java/com/facebook/presto/sql/relational/optimizer/ExpressionOptimizer.java new file mode 100644 index 00000000..5ee87661 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/sql/relational/optimizer/ExpressionOptimizer.java @@ -0,0 +1,158 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.sql.relational.optimizer; + +import com.facebook.presto.Session; +import com.facebook.presto.metadata.FunctionInfo; +import com.facebook.presto.metadata.FunctionRegistry; +import com.facebook.presto.metadata.Signature; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.sql.relational.CallExpression; +import com.facebook.presto.sql.relational.ConstantExpression; +import com.facebook.presto.sql.relational.InputReferenceExpression; +import com.facebook.presto.sql.relational.RowExpression; +import com.facebook.presto.sql.relational.RowExpressionVisitor; +import com.facebook.presto.sql.tree.QualifiedName; +import com.facebook.presto.type.UnknownType; +import com.google.common.collect.Iterables; + +import java.lang.invoke.MethodHandle; +import java.util.ArrayList; +import java.util.List; + +import static com.facebook.presto.sql.relational.Expressions.call; +import static com.facebook.presto.sql.relational.Expressions.constant; +import static com.facebook.presto.sql.relational.Expressions.constantNull; +import static com.facebook.presto.sql.relational.Signatures.CAST; +import static com.facebook.presto.sql.relational.Signatures.COALESCE; +import static com.facebook.presto.sql.relational.Signatures.IF; +import static com.facebook.presto.sql.relational.Signatures.IN; +import static com.facebook.presto.sql.relational.Signatures.IS_NULL; +import static com.facebook.presto.sql.relational.Signatures.NULL_IF; +import static com.facebook.presto.sql.relational.Signatures.SWITCH; +import static com.facebook.presto.sql.relational.Signatures.TRY_CAST; +import static com.facebook.presto.util.ImmutableCollectors.toImmutableList; +import static com.google.common.base.Predicates.instanceOf; + +public class ExpressionOptimizer +{ + private final FunctionRegistry registry; + private final TypeManager typeManager; + private final ConnectorSession session; + + public ExpressionOptimizer(FunctionRegistry registry, TypeManager typeManager, Session session) + { + this.registry = registry; + this.typeManager = typeManager; + this.session = session.toConnectorSession(); + } + + public RowExpression optimize(RowExpression expression) + { + return expression.accept(new Visitor(), null); + } + + private class Visitor + implements RowExpressionVisitor + { + @Override + public RowExpression visitInputReference(InputReferenceExpression reference, Void context) + { + return reference; + } + + @Override + public RowExpression visitConstant(ConstantExpression literal, Void context) + { + return literal; + } + + @Override + public RowExpression visitCall(CallExpression call, final Void context) + { + FunctionInfo function; + Signature signature = call.getSignature(); + + if (signature.getName().equals(CAST)) { + if (call.getArguments().get(0).getType().equals(UnknownType.UNKNOWN)) { + return constantNull(call.getType()); + } + function = registry.getCoercion(call.getArguments().get(0).getType(), call.getType()); + } + else { + switch (signature.getName()) { + // TODO: optimize these special forms + case IF: + case NULL_IF: + case SWITCH: + case "WHEN": + case TRY_CAST: + case IS_NULL: + case "IS_DISTINCT_FROM": + case COALESCE: + case "AND": + case "OR": + case IN: + List arguments = call.getArguments().stream() + .map(argument -> argument.accept(this, null)) + .collect(toImmutableList()); + return call(signature, call.getType(), arguments); + default: + function = registry.getExactFunction(signature); + if (function == null) { + // TODO: temporary hack to deal with magic timestamp literal functions which don't have an "exact" form and need to be "resolved" + function = registry.resolveFunction(QualifiedName.of(signature.getName()), signature.getArgumentTypes(), false); + } + } + } + + List arguments = call.getArguments().stream() + .map(argument -> argument.accept(this, context)) + .collect(toImmutableList()); + + if (Iterables.all(arguments, instanceOf(ConstantExpression.class)) && function.isDeterministic()) { + MethodHandle method = function.getMethodHandle(); + + if (method.type().parameterCount() > 0 && method.type().parameterType(0) == ConnectorSession.class) { + method = method.bindTo(session); + } + + int index = 0; + List constantArguments = new ArrayList<>(); + for (RowExpression argument : arguments) { + Object value = ((ConstantExpression) argument).getValue(); + // if any argument is null, return null + if (value == null && !function.getNullableArguments().get(index)) { + return constantNull(call.getType()); + } + constantArguments.add(value); + index++; + } + + try { + return constant(method.invokeWithArguments(constantArguments), call.getType()); + } + catch (Throwable e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + // Do nothing. As a result, this specific tree will be left untouched. But irrelevant expressions will continue to get evaluated and optimized. + } + } + + return call(signature, typeManager.getType(signature.getReturnType()), arguments); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/testing/LocalQueryRunner.java b/presto-main/src/main/java/com/facebook/presto/testing/LocalQueryRunner.java new file mode 100644 index 00000000..9f145fc7 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/testing/LocalQueryRunner.java @@ -0,0 +1,565 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.testing; + +import com.facebook.presto.ScheduledSplit; +import com.facebook.presto.Session; +import com.facebook.presto.SystemSessionProperties; +import com.facebook.presto.TaskSource; +import com.facebook.presto.block.BlockEncodingManager; +import com.facebook.presto.connector.ConnectorManager; +import com.facebook.presto.connector.system.CatalogSystemTable; +import com.facebook.presto.connector.system.NodeSystemTable; +import com.facebook.presto.connector.system.SystemConnector; +import com.facebook.presto.execution.TaskManagerConfig; +import com.facebook.presto.index.IndexManager; +import com.facebook.presto.metadata.HandleResolver; +import com.facebook.presto.metadata.InMemoryNodeManager; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.metadata.MetadataManager; +import com.facebook.presto.metadata.QualifiedTableName; +import com.facebook.presto.metadata.QualifiedTablePrefix; +import com.facebook.presto.metadata.Split; +import com.facebook.presto.metadata.TableHandle; +import com.facebook.presto.metadata.TableLayoutHandle; +import com.facebook.presto.metadata.TableLayoutResult; +import com.facebook.presto.operator.Driver; +import com.facebook.presto.operator.DriverContext; +import com.facebook.presto.operator.DriverFactory; +import com.facebook.presto.operator.FilterAndProjectOperator; +import com.facebook.presto.operator.FilterFunctions; +import com.facebook.presto.operator.GenericPageProcessor; +import com.facebook.presto.operator.Operator; +import com.facebook.presto.operator.OperatorContext; +import com.facebook.presto.operator.OperatorFactory; +import com.facebook.presto.operator.OutputFactory; +import com.facebook.presto.operator.PageSourceOperator; +import com.facebook.presto.operator.ProjectionFunction; +import com.facebook.presto.operator.ProjectionFunctions; +import com.facebook.presto.operator.TaskContext; +import com.facebook.presto.operator.index.IndexJoinLookupStats; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.spi.ColumnMetadata; +import com.facebook.presto.spi.Connector; +import com.facebook.presto.spi.ConnectorFactory; +import com.facebook.presto.spi.ConnectorPageSource; +import com.facebook.presto.spi.Constraint; +import com.facebook.presto.spi.Plugin; +import com.facebook.presto.spi.RecordCursor; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.block.BlockEncodingSerde; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.split.PageSinkManager; +import com.facebook.presto.split.PageSourceManager; +import com.facebook.presto.split.SplitManager; +import com.facebook.presto.split.SplitSource; +import com.facebook.presto.sql.analyzer.Analysis; +import com.facebook.presto.sql.analyzer.Analyzer; +import com.facebook.presto.sql.analyzer.FeaturesConfig; +import com.facebook.presto.sql.analyzer.QueryExplainer; +import com.facebook.presto.sql.gen.ExpressionCompiler; +import com.facebook.presto.sql.parser.SqlParser; +import com.facebook.presto.sql.planner.CompilerConfig; +import com.facebook.presto.sql.planner.LocalExecutionPlanner; +import com.facebook.presto.sql.planner.LocalExecutionPlanner.LocalExecutionPlan; +import com.facebook.presto.sql.planner.LogicalPlanner; +import com.facebook.presto.sql.planner.Plan; +import com.facebook.presto.sql.planner.PlanFragmenter; +import com.facebook.presto.sql.planner.PlanNodeIdAllocator; +import com.facebook.presto.sql.planner.PlanOptimizersFactory; +import com.facebook.presto.sql.planner.PlanPrinter; +import com.facebook.presto.sql.planner.SubPlan; +import com.facebook.presto.sql.planner.plan.PlanNode; +import com.facebook.presto.sql.planner.plan.PlanNodeId; +import com.facebook.presto.sql.planner.plan.TableScanNode; +import com.facebook.presto.sql.tree.Statement; +import com.facebook.presto.type.TypeRegistry; +import com.facebook.presto.type.TypeUtils; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import org.intellij.lang.annotations.Language; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicReference; + +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.sql.testing.TreeAssertions.assertFormattedSql; +import static com.facebook.presto.testing.TestingTaskContext.createTaskContext; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static io.airlift.concurrent.MoreFutures.getFutureValue; +import static io.airlift.concurrent.Threads.daemonThreadsNamed; +import static java.util.concurrent.Executors.newCachedThreadPool; + +public class LocalQueryRunner + implements QueryRunner +{ + private final Session defaultSession; + private final ExecutorService executor; + + private final SqlParser sqlParser; + private final InMemoryNodeManager nodeManager; + private final TypeRegistry typeRegistry; + private final MetadataManager metadata; + private final SplitManager splitManager; + private final BlockEncodingSerde blockEncodingSerde; + private final PageSourceManager pageSourceManager; + private final IndexManager indexManager; + private final PageSinkManager pageSinkManager; + + private final ExpressionCompiler compiler; + private final ConnectorManager connectorManager; + private final boolean hashEnabled; + + private boolean printPlan; + + public LocalQueryRunner(Session defaultSession) + { + this.defaultSession = checkNotNull(defaultSession, "defaultSession is null"); + this.hashEnabled = SystemSessionProperties.isOptimizeHashGenerationEnabled(defaultSession, false); + this.executor = newCachedThreadPool(daemonThreadsNamed("local-query-runner-%s")); + + this.sqlParser = new SqlParser(); + this.nodeManager = new InMemoryNodeManager(); + this.typeRegistry = new TypeRegistry(); + this.indexManager = new IndexManager(); + this.pageSinkManager = new PageSinkManager(); + + this.splitManager = new SplitManager(); + this.blockEncodingSerde = new BlockEncodingManager(typeRegistry); + this.metadata = new MetadataManager(new FeaturesConfig().setExperimentalSyntaxEnabled(true), typeRegistry, splitManager, blockEncodingSerde); + this.pageSourceManager = new PageSourceManager(); + + this.compiler = new ExpressionCompiler(metadata); + + this.connectorManager = new ConnectorManager( + metadata, + splitManager, + pageSourceManager, + indexManager, + pageSinkManager, + new HandleResolver(), + ImmutableMap.of(), + nodeManager + ); + + Connector systemConnector = new SystemConnector(nodeManager, ImmutableSet.of( + new NodeSystemTable(nodeManager), + new CatalogSystemTable(metadata))); + + connectorManager.createConnection(SystemConnector.NAME, systemConnector); + } + + public static LocalQueryRunner createHashEnabledQueryRunner(LocalQueryRunner localQueryRunner) + { + Session session = localQueryRunner.getDefaultSession(); + Session.SessionBuilder builder = Session.builder() + .setUser(session.getUser()) + .setSource(session.getSource()) + .setCatalog(session.getCatalog()) + .setTimeZoneKey(session.getTimeZoneKey()) + .setLocale(session.getLocale()) + .setSystemProperties(ImmutableMap.of("optimizer.optimize_hash_generation", "true")); + return new LocalQueryRunner(builder.build()); + } + + @Override + public void close() + { + executor.shutdownNow(); + connectorManager.stop(); + } + + @Override + public int getNodeCount() + { + return 1; + } + + public InMemoryNodeManager getNodeManager() + { + return nodeManager; + } + + public TypeRegistry getTypeManager() + { + return typeRegistry; + } + + public Metadata getMetadata() + { + return metadata; + } + + public ExecutorService getExecutor() + { + return executor; + } + + @Override + public Session getDefaultSession() + { + return defaultSession; + } + + public void createCatalog(String catalogName, ConnectorFactory connectorFactory, Map properties) + { + nodeManager.addCurrentNodeDatasource(catalogName); + connectorManager.createConnection(catalogName, connectorFactory, properties); + } + + @Override + public void installPlugin(Plugin plugin) + { + throw new UnsupportedOperationException(); + } + + @Override + public void createCatalog(String catalogName, String connectorName, Map properties) + { + throw new UnsupportedOperationException(); + } + + public LocalQueryRunner printPlan() + { + printPlan = true; + return this; + } + + public boolean isHashEnabled() + { + return hashEnabled; + } + + public static class MaterializedOutputFactory + implements OutputFactory + { + private final AtomicReference materializingOperator = new AtomicReference<>(); + + private MaterializingOperator getMaterializingOperator() + { + MaterializingOperator operator = materializingOperator.get(); + checkState(operator != null, "Output not created"); + return operator; + } + + @Override + public OperatorFactory createOutputOperator(int operatorId, List sourceTypes) + { + checkNotNull(sourceTypes, "sourceType is null"); + + return new OperatorFactory() + { + @Override + public List getTypes() + { + return ImmutableList.of(); + } + + @Override + public Operator createOperator(DriverContext driverContext) + { + OperatorContext operatorContext = driverContext.addOperatorContext(operatorId, MaterializingOperator.class.getSimpleName()); + MaterializingOperator operator = new MaterializingOperator(operatorContext, sourceTypes); + + if (!materializingOperator.compareAndSet(null, operator)) { + throw new IllegalArgumentException("Output already created"); + } + return operator; + } + + @Override + public void close() + { + } + }; + } + } + + @Override + public List listTables(Session session, String catalog, String schema) + { + return getMetadata().listTables(session, new QualifiedTablePrefix(catalog, schema)); + } + + @Override + public boolean tableExists(Session session, String table) + { + QualifiedTableName name = new QualifiedTableName(session.getCatalog(), session.getSchema(), table); + return getMetadata().getTableHandle(session, name).isPresent(); + } + + @Override + public MaterializedResult execute(@Language("SQL") String sql) + { + return execute(defaultSession, sql); + } + + @Override + public MaterializedResult execute(Session session, @Language("SQL") String sql) + { + MaterializedOutputFactory outputFactory = new MaterializedOutputFactory(); + + TaskContext taskContext = createTaskContext(executor, session); + List drivers = createDrivers(session, sql, outputFactory, taskContext); + + boolean done = false; + while (!done) { + boolean processed = false; + for (Driver driver : drivers) { + if (!driver.isFinished()) { + driver.process(); + processed = true; + } + } + done = !processed; + } + + return outputFactory.getMaterializingOperator().getMaterializedResult(); + } + + public List createDrivers(@Language("SQL") String sql, OutputFactory outputFactory, TaskContext taskContext) + { + return createDrivers(defaultSession, sql, outputFactory, taskContext); + } + + public List createDrivers(Session session, @Language("SQL") String sql, OutputFactory outputFactory, TaskContext taskContext) + { + Statement statement = sqlParser.createStatement(sql); + + assertFormattedSql(sqlParser, statement); + + PlanNodeIdAllocator idAllocator = new PlanNodeIdAllocator(); + FeaturesConfig featuresConfig = new FeaturesConfig() + .setExperimentalSyntaxEnabled(true) + .setDistributedIndexJoinsEnabled(false) + .setOptimizeHashGeneration(true); + PlanOptimizersFactory planOptimizersFactory = new PlanOptimizersFactory(metadata, sqlParser, indexManager, featuresConfig, true); + + QueryExplainer queryExplainer = new QueryExplainer(session, planOptimizersFactory.get(), metadata, sqlParser, featuresConfig.isExperimentalSyntaxEnabled()); + Analyzer analyzer = new Analyzer(session, metadata, sqlParser, Optional.of(queryExplainer), featuresConfig.isExperimentalSyntaxEnabled()); + + Analysis analysis = analyzer.analyze(statement); + Plan plan = new LogicalPlanner(session, planOptimizersFactory.get(), idAllocator, metadata).plan(analysis); + + if (printPlan) { + System.out.println(PlanPrinter.textLogicalPlan(plan.getRoot(), plan.getTypes(), metadata)); + } + + SubPlan subplan = new PlanFragmenter().createSubPlans(plan); + if (!subplan.getChildren().isEmpty()) { + throw new AssertionError("Expected subplan to have no children"); + } + + LocalExecutionPlanner executionPlanner = new LocalExecutionPlanner( + metadata, + sqlParser, + pageSourceManager, + indexManager, + pageSinkManager, + null, + compiler, + new IndexJoinLookupStats(), + new CompilerConfig().setInterpreterEnabled(false), // make sure tests fail if compiler breaks + new TaskManagerConfig().setTaskDefaultConcurrency(4) + ); + + // plan query + LocalExecutionPlan localExecutionPlan = executionPlanner.plan(session, + subplan.getFragment().getRoot(), + subplan.getFragment().getOutputLayout(), + plan.getTypes(), + subplan.getFragment().getDistribution(), + outputFactory); + + // generate sources + List sources = new ArrayList<>(); + long sequenceId = 0; + for (TableScanNode tableScan : findTableScanNodes(subplan.getFragment().getRoot())) { + TableLayoutHandle layout = tableScan.getLayout().get(); + + SplitSource splitSource = splitManager.getSplits(layout); + + ImmutableSet.Builder scheduledSplits = ImmutableSet.builder(); + while (!splitSource.isFinished()) { + for (Split split : getFutureValue(splitSource.getNextBatch(1000))) { + scheduledSplits.add(new ScheduledSplit(sequenceId++, split)); + } + } + + sources.add(new TaskSource(tableScan.getId(), scheduledSplits.build(), true)); + } + + // create drivers + List drivers = new ArrayList<>(); + Map driversBySource = new HashMap<>(); + for (DriverFactory driverFactory : localExecutionPlan.getDriverFactories()) { + for (int i = 0; i < driverFactory.getDriverInstances(); i++) { + DriverContext driverContext = taskContext.addPipelineContext(driverFactory.isInputDriver(), driverFactory.isOutputDriver()).addDriverContext(); + Driver driver = driverFactory.createDriver(driverContext); + drivers.add(driver); + for (PlanNodeId sourceId : driver.getSourceIds()) { + driversBySource.put(sourceId, driver); + } + } + driverFactory.close(); + } + + // add sources to the drivers + for (TaskSource source : sources) { + for (Driver driver : driversBySource.values()) { + driver.updateSource(source); + } + } + + return ImmutableList.copyOf(drivers); + } + + public OperatorFactory createTableScanOperator(int operatorId, String tableName, String... columnNames) + { + return createTableScanOperator(defaultSession, operatorId, tableName, columnNames); + } + + public OperatorFactory createTableScanOperator( + Session session, + int operatorId, + String tableName, + String... columnNames) + { + // look up the table + TableHandle tableHandle = metadata.getTableHandle(session, new QualifiedTableName(session.getCatalog(), session.getSchema(), tableName)).orElse(null); + checkArgument(tableHandle != null, "Table %s does not exist", tableName); + + // lookup the columns + Map allColumnHandles = metadata.getColumnHandles(tableHandle); + ImmutableList.Builder columnHandlesBuilder = ImmutableList.builder(); + ImmutableList.Builder columnTypesBuilder = ImmutableList.builder(); + for (String columnName : columnNames) { + ColumnHandle columnHandle = allColumnHandles.get(columnName); + checkArgument(columnHandle != null, "Table %s does not have a column %s", tableName, columnName); + columnHandlesBuilder.add(columnHandle); + ColumnMetadata columnMetadata = metadata.getColumnMetadata(tableHandle, columnHandle); + columnTypesBuilder.add(columnMetadata.getType()); + } + List columnHandles = columnHandlesBuilder.build(); + List columnTypes = columnTypesBuilder.build(); + + // get the split for this table + List layouts = metadata.getLayouts(tableHandle, Constraint.alwaysTrue(), Optional.empty()); + Split split = getLocalQuerySplit(layouts.get(0).getLayout().getHandle()); + + return new OperatorFactory() + { + @Override + public List getTypes() + { + return columnTypes; + } + + @Override + public Operator createOperator(DriverContext driverContext) + { + OperatorContext operatorContext = driverContext.addOperatorContext(operatorId, "BenchmarkSource"); + ConnectorPageSource pageSource = pageSourceManager.createPageSource(split, columnHandles); + return new PageSourceOperator(pageSource, columnTypes, operatorContext); + } + + @Override + public void close() + { + } + }; + } + + public OperatorFactory createHashProjectOperator(int operatorId, List columnTypes) + { + ImmutableList.Builder projectionFunctions = ImmutableList.builder(); + for (int i = 0; i < columnTypes.size(); i++) { + projectionFunctions.add(ProjectionFunctions.singleColumn(columnTypes.get(i), i)); + } + projectionFunctions.add(new HashProjectionFunction(columnTypes)); + return new FilterAndProjectOperator.FilterAndProjectOperatorFactory( + operatorId, + new GenericPageProcessor(FilterFunctions.TRUE_FUNCTION, projectionFunctions.build()), + ImmutableList.copyOf(Iterables.concat(columnTypes, ImmutableList.of(BIGINT)))); + } + + private Split getLocalQuerySplit(TableLayoutHandle handle) + { + SplitSource splitSource = splitManager.getSplits(handle); + List splits = new ArrayList<>(); + splits.addAll(getFutureValue(splitSource.getNextBatch(1000))); + while (!splitSource.isFinished()) { + splits.addAll(getFutureValue(splitSource.getNextBatch(1000))); + } + checkArgument(splits.size() == 1, "Expected only one split for a local query, but got %s splits", splits.size()); + return splits.get(0); + } + + private static List findTableScanNodes(PlanNode node) + { + ImmutableList.Builder tableScanNodes = ImmutableList.builder(); + findTableScanNodes(node, tableScanNodes); + return tableScanNodes.build(); + } + + private static void findTableScanNodes(PlanNode node, ImmutableList.Builder builder) + { + for (PlanNode source : node.getSources()) { + findTableScanNodes(source, builder); + } + + if (node instanceof TableScanNode) { + builder.add((TableScanNode) node); + } + } + + private static class HashProjectionFunction + implements ProjectionFunction + { + private final List columnTypes; + + public HashProjectionFunction(List columnTypes) + { + this.columnTypes = columnTypes; + } + + @Override + public Type getType() + { + return BIGINT; + } + + @Override + public void project(int position, Block[] blocks, BlockBuilder output) + { + BIGINT.writeLong(output, TypeUtils.getHashPosition(columnTypes, blocks, position)); + } + + @Override + public void project(RecordCursor cursor, BlockBuilder output) + { + throw new UnsupportedOperationException("Operation not supported"); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/testing/MaterializedResult.java b/presto-main/src/main/java/com/facebook/presto/testing/MaterializedResult.java new file mode 100644 index 00000000..228ec3d9 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/testing/MaterializedResult.java @@ -0,0 +1,301 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.testing; + +import com.facebook.presto.Session; +import com.facebook.presto.spi.ConnectorPageSource; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.type.SqlDate; +import com.facebook.presto.spi.type.SqlTime; +import com.facebook.presto.spi.type.SqlTimeWithTimeZone; +import com.facebook.presto.spi.type.SqlTimestamp; +import com.facebook.presto.spi.type.SqlTimestampWithTimeZone; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import org.joda.time.DateTimeZone; + +import java.sql.Date; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class MaterializedResult + implements Iterable +{ + public static final int DEFAULT_PRECISION = 5; + + private final List rows; + private final List types; + private final Map setSessionProperties; + private final Set resetSessionProperties; + + public MaterializedResult(List rows, List types) + { + this(rows, types, ImmutableMap.of(), ImmutableSet.of()); + } + + public MaterializedResult(List rows, List types, Map setSessionProperties, Set resetSessionProperties) + { + this.rows = ImmutableList.copyOf(checkNotNull(rows, "rows is null")); + this.types = ImmutableList.copyOf(checkNotNull(types, "types is null")); + this.setSessionProperties = ImmutableMap.copyOf(checkNotNull(setSessionProperties, "setSessionProperties is null")); + this.resetSessionProperties = ImmutableSet.copyOf(checkNotNull(resetSessionProperties, "resetSessionProperties is null")); + } + + public int getRowCount() + { + return rows.size(); + } + + @Override + public Iterator iterator() + { + return rows.iterator(); + } + + public List getMaterializedRows() + { + return rows; + } + + public List getTypes() + { + return types; + } + + public Map getSetSessionProperties() + { + return setSessionProperties; + } + + public Set getResetSessionProperties() + { + return resetSessionProperties; + } + + @Override + public boolean equals(Object obj) + { + if (obj == this) { + return true; + } + if ((obj == null) || (getClass() != obj.getClass())) { + return false; + } + MaterializedResult o = (MaterializedResult) obj; + return Objects.equals(types, o.types) && + Objects.equals(rows, o.rows) && + Objects.equals(setSessionProperties, o.setSessionProperties) && + Objects.equals(resetSessionProperties, o.resetSessionProperties); + } + + @Override + public int hashCode() + { + return Objects.hash(rows, types, setSessionProperties, resetSessionProperties); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("rows", rows) + .add("types", types) + .add("setSessionProperties", setSessionProperties) + .add("resetSessionProperties", resetSessionProperties) + .toString(); + } + + public MaterializedResult toJdbcTypes() + { + ImmutableList.Builder jdbcRows = ImmutableList.builder(); + for (MaterializedRow row : rows) { + jdbcRows.add(convertToJdbcTypes(row)); + } + return new MaterializedResult(jdbcRows.build(), types, setSessionProperties, resetSessionProperties); + } + + private static MaterializedRow convertToJdbcTypes(MaterializedRow prestoRow) + { + List jdbcValues = new ArrayList<>(); + for (int field = 0; field < prestoRow.getFieldCount(); field++) { + Object prestoValue = prestoRow.getField(field); + Object jdbcValue; + if (prestoValue instanceof SqlDate) { + int days = ((SqlDate) prestoValue).getDays(); + jdbcValue = new Date(TimeUnit.DAYS.toMillis(days)); + } + else if (prestoValue instanceof SqlTime) { + jdbcValue = new Time(((SqlTime) prestoValue).getMillisUtc()); + } + else if (prestoValue instanceof SqlTimeWithTimeZone) { + jdbcValue = new Time(((SqlTimeWithTimeZone) prestoValue).getMillisUtc()); + } + else if (prestoValue instanceof SqlTimestamp) { + jdbcValue = new Timestamp(((SqlTimestamp) prestoValue).getMillisUtc()); + } + else if (prestoValue instanceof SqlTimestampWithTimeZone) { + jdbcValue = new Timestamp(((SqlTimestampWithTimeZone) prestoValue).getMillisUtc()); + } + else { + jdbcValue = prestoValue; + } + jdbcValues.add(jdbcValue); + } + return new MaterializedRow(prestoRow.getPrecision(), jdbcValues); + } + + public MaterializedResult toTimeZone(DateTimeZone oldTimeZone, DateTimeZone newTimeZone) + { + ImmutableList.Builder jdbcRows = ImmutableList.builder(); + for (MaterializedRow row : rows) { + jdbcRows.add(toTimeZone(row, oldTimeZone, newTimeZone)); + } + return new MaterializedResult(jdbcRows.build(), types); + } + + private static MaterializedRow toTimeZone(MaterializedRow prestoRow, DateTimeZone oldTimeZone, DateTimeZone newTimeZone) + { + List values = new ArrayList<>(); + for (int field = 0; field < prestoRow.getFieldCount(); field++) { + Object value = prestoRow.getField(field); + if (value instanceof Date) { + long oldMillis = ((Date) value).getTime(); + long newMillis = oldTimeZone.getMillisKeepLocal(newTimeZone, oldMillis); + value = new Date(newMillis); + } + values.add(value); + } + return new MaterializedRow(prestoRow.getPrecision(), values); + } + + public static MaterializedResult materializeSourceDataStream(Session session, ConnectorPageSource pageSource, List types) + { + return materializeSourceDataStream(session.toConnectorSession(), pageSource, types); + } + + public static MaterializedResult materializeSourceDataStream(ConnectorSession session, ConnectorPageSource pageSource, List types) + { + MaterializedResult.Builder builder = resultBuilder(session, types); + while (!pageSource.isFinished()) { + Page outputPage = pageSource.getNextPage(); + if (outputPage == null) { + break; + } + builder.page(outputPage); + } + return builder.build(); + } + + public static Builder resultBuilder(Session session, Type... types) + { + return resultBuilder(session.toConnectorSession(), types); + } + + public static Builder resultBuilder(Session session, Iterable types) + { + return resultBuilder(session.toConnectorSession(), types); + } + + public static Builder resultBuilder(ConnectorSession session, Type... types) + { + return resultBuilder(session, ImmutableList.copyOf(types)); + } + + public static Builder resultBuilder(ConnectorSession session, Iterable types) + { + return new Builder(session, ImmutableList.copyOf(types)); + } + + public static class Builder + { + private final ConnectorSession session; + private final List types; + private final ImmutableList.Builder rows = ImmutableList.builder(); + + Builder(ConnectorSession session, List types) + { + this.session = session; + this.types = ImmutableList.copyOf(types); + } + + public Builder rows(List rows) + { + this.rows.addAll(rows); + return this; + } + + public Builder row(Object... values) + { + rows.add(new MaterializedRow(DEFAULT_PRECISION, values)); + return this; + } + + public Builder rows(Object[][] rows) + { + for (Object[] row : rows) { + row(row); + } + return this; + } + + public Builder pages(Iterable pages) + { + for (Page page : pages) { + this.page(page); + } + + return this; + } + + public Builder page(Page page) + { + checkNotNull(page, "page is null"); + checkArgument(page.getChannelCount() == types.size(), "Expected a page with %s columns, but got %s columns", types.size(), page.getChannelCount()); + + for (int position = 0; position < page.getPositionCount(); position++) { + List values = new ArrayList<>(page.getChannelCount()); + for (int channel = 0; channel < page.getChannelCount(); channel++) { + Type type = types.get(channel); + Block block = page.getBlock(channel); + values.add(type.getObjectValue(session, block, position)); + } + values = Collections.unmodifiableList(values); + + rows.add(new MaterializedRow(DEFAULT_PRECISION, values)); + } + return this; + } + + public MaterializedResult build() + { + return new MaterializedResult(rows.build(), types); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/testing/MaterializedRow.java b/presto-main/src/main/java/com/facebook/presto/testing/MaterializedRow.java new file mode 100644 index 00000000..8e5565f8 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/testing/MaterializedRow.java @@ -0,0 +1,149 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.testing; + +import com.google.common.base.Preconditions; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Materialize all values in a row + * Special handling is added for Double types for approximate comparisons + */ +public class MaterializedRow +{ + private final int precision; + private final List values; + + public MaterializedRow(int precision, Object... values) + { + this(precision, Arrays.asList(checkNotNull(values, "values is null"))); + } + + public MaterializedRow(int precision, List values) + { + checkArgument(precision > 0, "Need at least one digit of precision"); + this.precision = precision; + + this.values = new ArrayList<>(values.size()); + for (Object object : values) { + if (object instanceof Double || object instanceof Float) { + this.values.add(new ApproximateDouble(((Number) object).doubleValue(), precision)); + } + else if (object instanceof Number) { + this.values.add(((Number) object).longValue()); + } + else { + this.values.add(object); + } + } + } + + public int getPrecision() + { + return precision; + } + + public int getFieldCount() + { + return values.size(); + } + + public Object getField(int field) + { + Preconditions.checkElementIndex(field, values.size()); + Object o = values.get(field); + return (o instanceof ApproximateDouble) ? ((ApproximateDouble) o).getValue() : o; + } + + @Override + public String toString() + { + return values.toString(); + } + + @Override + public boolean equals(Object obj) + { + if (obj == this) { + return true; + } + if ((obj == null) || (getClass() != obj.getClass())) { + return false; + } + MaterializedRow o = (MaterializedRow) obj; + return Objects.equals(values, o.values); + } + + @Override + public int hashCode() + { + return Objects.hash(values); + } + + private static class ApproximateDouble + { + private final Double value; + private final Double normalizedValue; + + private ApproximateDouble(Double value, int precision) + { + this.value = value; + if (value.isNaN() || value.isInfinite()) { + this.normalizedValue = value; + } + else { + this.normalizedValue = new BigDecimal(value).round(new MathContext(precision)).doubleValue(); + } + } + + public Double getValue() + { + return value; + } + + @Override + public String toString() + { + return value.toString(); + } + + @Override + public boolean equals(Object obj) + { + if (obj == this) { + return true; + } + if ((obj == null) || (getClass() != obj.getClass())) { + return false; + } + ApproximateDouble o = (ApproximateDouble) obj; + return Objects.equals(normalizedValue, o.normalizedValue); + } + + @Override + public int hashCode() + { + return Objects.hash(normalizedValue); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/testing/MaterializingOperator.java b/presto-main/src/main/java/com/facebook/presto/testing/MaterializingOperator.java new file mode 100644 index 00000000..545f06fa --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/testing/MaterializingOperator.java @@ -0,0 +1,138 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.testing; + +import com.facebook.presto.operator.DriverContext; +import com.facebook.presto.operator.Operator; +import com.facebook.presto.operator.OperatorContext; +import com.facebook.presto.operator.OperatorFactory; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +public class MaterializingOperator + implements Operator +{ + public static class MaterializingOperatorFactory + implements OperatorFactory + { + private final int operatorId; + private final List sourceTypes; + private boolean closed; + + public MaterializingOperatorFactory(int operatorId, List sourceTypes) + { + this.operatorId = operatorId; + this.sourceTypes = sourceTypes; + } + + @Override + public List getTypes() + { + return ImmutableList.of(); + } + + @Override + public MaterializingOperator createOperator(DriverContext driverContext) + { + checkState(!closed, "Factory is already closed"); + OperatorContext operatorContext = driverContext.addOperatorContext(operatorId, MaterializingOperator.class.getSimpleName()); + return new MaterializingOperator(operatorContext, sourceTypes); + } + + @Override + public void close() + { + closed = true; + } + } + + private final OperatorContext operatorContext; + private final MaterializedResult.Builder resultBuilder; + private boolean finished; + private boolean closed; + + public MaterializingOperator(OperatorContext operatorContext, List sourceTypes) + { + this.operatorContext = checkNotNull(operatorContext, "operatorContext is null"); + resultBuilder = MaterializedResult.resultBuilder(operatorContext.getSession(), sourceTypes); + } + + public boolean isClosed() + { + return closed; + } + + public MaterializedResult getMaterializedResult() + { + return resultBuilder.build(); + } + + @Override + public OperatorContext getOperatorContext() + { + return operatorContext; + } + + @Override + public List getTypes() + { + return ImmutableList.of(); + } + + @Override + public void finish() + { + finished = true; + } + + @Override + public boolean isFinished() + { + return finished; + } + + @Override + public boolean needsInput() + { + return !finished; + } + + @Override + public void addInput(Page page) + { + checkNotNull(page, "page is null"); + checkState(!finished, "operator finished"); + + resultBuilder.page(page); + operatorContext.recordGeneratedOutput(page.getSizeInBytes(), page.getPositionCount()); + } + + @Override + public Page getOutput() + { + return null; + } + + @Override + public void close() + { + closed = true; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/testing/NullOutputOperator.java b/presto-main/src/main/java/com/facebook/presto/testing/NullOutputOperator.java new file mode 100644 index 00000000..947d38bd --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/testing/NullOutputOperator.java @@ -0,0 +1,124 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.testing; + +import com.facebook.presto.operator.DriverContext; +import com.facebook.presto.operator.Operator; +import com.facebook.presto.operator.OperatorContext; +import com.facebook.presto.operator.OperatorFactory; +import com.facebook.presto.operator.OutputFactory; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class NullOutputOperator + implements Operator +{ + public static class NullOutputFactory + implements OutputFactory + { + @Override + public OperatorFactory createOutputOperator(int operatorId, List sourceTypes) + { + return new NullOutputOperatorFactory(operatorId, sourceTypes); + } + } + + public static class NullOutputOperatorFactory + implements OperatorFactory + { + private final int operatorId; + private final List types; + + public NullOutputOperatorFactory(int operatorId, List types) + { + this.operatorId = operatorId; + this.types = types; + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public Operator createOperator(DriverContext driverContext) + { + OperatorContext operatorContext = driverContext.addOperatorContext(operatorId, NullOutputOperator.class.getSimpleName()); + return new NullOutputOperator(operatorContext, types); + } + + @Override + public void close() + { + } + } + + private final OperatorContext operatorContext; + private final List types; + private boolean finished; + + public NullOutputOperator(OperatorContext operatorContext, List types) + { + this.operatorContext = checkNotNull(operatorContext, "operatorContext is null"); + this.types = ImmutableList.copyOf(checkNotNull(types, "types is null")); + } + + @Override + public OperatorContext getOperatorContext() + { + return operatorContext; + } + + @Override + public List getTypes() + { + return types; + } + + @Override + public void finish() + { + finished = true; + } + + @Override + public boolean isFinished() + { + return finished; + } + + @Override + public boolean needsInput() + { + return true; + } + + @Override + public void addInput(Page page) + { + operatorContext.recordGeneratedOutput(page.getSizeInBytes(), page.getPositionCount()); + } + + @Override + public Page getOutput() + { + return null; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/testing/QueryRunner.java b/presto-main/src/main/java/com/facebook/presto/testing/QueryRunner.java new file mode 100644 index 00000000..086e4fa9 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/testing/QueryRunner.java @@ -0,0 +1,46 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.testing; + +import com.facebook.presto.Session; +import com.facebook.presto.metadata.QualifiedTableName; +import com.facebook.presto.spi.Plugin; +import org.intellij.lang.annotations.Language; + +import java.io.Closeable; +import java.util.List; +import java.util.Map; + +public interface QueryRunner + extends Closeable +{ + @Override + void close(); + + int getNodeCount(); + + Session getDefaultSession(); + + MaterializedResult execute(@Language("SQL") String sql); + + MaterializedResult execute(Session session, @Language("SQL") String sql); + + List listTables(Session session, String catalog, String schema); + + boolean tableExists(Session session, String table); + + void installPlugin(Plugin plugin); + + void createCatalog(String catalogName, String connectorName, Map properties); +} diff --git a/presto-main/src/main/java/com/facebook/presto/testing/RunLengthBlockEncoding.java b/presto-main/src/main/java/com/facebook/presto/testing/RunLengthBlockEncoding.java new file mode 100644 index 00000000..1c7113ea --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/testing/RunLengthBlockEncoding.java @@ -0,0 +1,110 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.testing; + +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockEncoding; +import com.facebook.presto.spi.block.BlockEncodingFactory; +import com.facebook.presto.spi.block.BlockEncodingSerde; +import com.facebook.presto.spi.type.TypeManager; +import io.airlift.slice.SliceInput; +import io.airlift.slice.SliceOutput; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class RunLengthBlockEncoding + implements BlockEncoding +{ + public static final BlockEncodingFactory FACTORY = new RunLengthBlockEncodingFactory(); + private static final String NAME = "RLE"; + + private final BlockEncoding valueBlockEncoding; + + public RunLengthBlockEncoding(BlockEncoding valueBlockEncoding) + { + this.valueBlockEncoding = checkNotNull(valueBlockEncoding, "valueBlockEncoding is null"); + } + + @Override + public String getName() + { + return NAME; + } + + public BlockEncoding getValueBlockEncoding() + { + return valueBlockEncoding; + } + + @Override + public void writeBlock(SliceOutput sliceOutput, Block block) + { + RunLengthEncodedBlock rleBlock = (RunLengthEncodedBlock) block; + + // write the run length + sliceOutput.writeInt(rleBlock.getPositionCount()); + + // write the value + getValueBlockEncoding().writeBlock(sliceOutput, rleBlock.getValue()); + } + + @Override + public int getEstimatedSize(Block block) + { + RunLengthEncodedBlock rleBlock = (RunLengthEncodedBlock) block; + + return 4 + getValueBlockEncoding().getEstimatedSize(rleBlock.getValue()); + } + + @Override + public RunLengthEncodedBlock readBlock(SliceInput sliceInput) + { + // read the run length + int positionCount = sliceInput.readInt(); + + // read the value + Block value = getValueBlockEncoding().readBlock(sliceInput); + + return new RunLengthEncodedBlock(value, positionCount); + } + + @Override + public BlockEncodingFactory getFactory() + { + return FACTORY; + } + + private static class RunLengthBlockEncodingFactory + implements BlockEncodingFactory + { + @Override + public String getName() + { + return NAME; + } + + @Override + public RunLengthBlockEncoding readEncoding(TypeManager manager, BlockEncodingSerde serde, SliceInput input) + { + BlockEncoding valueBlockEncoding = serde.readBlockEncoding(input); + return new RunLengthBlockEncoding(valueBlockEncoding); + } + + @Override + public void writeEncoding(BlockEncodingSerde serde, SliceOutput output, RunLengthBlockEncoding blockEncoding) + { + serde.writeBlockEncoding(output, blockEncoding.getValueBlockEncoding()); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/testing/RunLengthEncodedBlock.java b/presto-main/src/main/java/com/facebook/presto/testing/RunLengthEncodedBlock.java new file mode 100644 index 00000000..8a7335bd --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/testing/RunLengthEncodedBlock.java @@ -0,0 +1,203 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.testing; + +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import io.airlift.slice.Slice; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkPositionIndexes; + +public class RunLengthEncodedBlock + implements Block +{ + private final Block value; + private final int positionCount; + + public RunLengthEncodedBlock(Block value, int positionCount) + { + this.value = checkNotNull(value, "value is null"); + checkArgument(value.getPositionCount() == 1, "Expected value to contain a single position but has %s positions", value.getPositionCount()); + + // value can not be a RunLengthEncodedBlock because this could cause stack overflow in some of the methods + checkArgument(!(value instanceof RunLengthEncodedBlock), "Value can not be an instance of a %s", getClass().getName()); + + checkArgument(positionCount >= 0, "positionCount is negative"); + this.positionCount = positionCount; + } + + public Block getValue() + { + return value; + } + + @Override + public int getPositionCount() + { + return positionCount; + } + + @Override + public int getSizeInBytes() + { + return value.getSizeInBytes(); + } + + @Override + public int getRetainedSizeInBytes() + { + return value.getRetainedSizeInBytes(); + } + + @Override + public RunLengthBlockEncoding getEncoding() + { + return new RunLengthBlockEncoding(value.getEncoding()); + } + + @Override + public Block getRegion(int positionOffset, int length) + { + checkPositionIndexes(positionOffset, positionOffset + length, positionCount); + return new RunLengthEncodedBlock(value, length); + } + + @Override + public Block copyRegion(int positionOffset, int length) + { + checkPositionIndexes(positionOffset, positionOffset + length, positionCount); + return new RunLengthEncodedBlock(value.copyRegion(0, 1), length); + } + + @Override + public int getLength(int position) + { + return value.getLength(0); + } + + @Override + public byte getByte(int position, int offset) + { + return value.getByte(0, offset); + } + + @Override + public short getShort(int position, int offset) + { + return value.getShort(0, offset); + } + + @Override + public int getInt(int position, int offset) + { + return value.getInt(0, offset); + } + + @Override + public long getLong(int position, int offset) + { + return value.getLong(0, offset); + } + + @Override + public float getFloat(int position, int offset) + { + return value.getFloat(0, offset); + } + + @Override + public double getDouble(int position, int offset) + { + return value.getDouble(0, offset); + } + + @Override + public Slice getSlice(int position, int offset, int length) + { + return value.getSlice(0, offset, length); + } + + @Override + public boolean bytesEqual(int position, int offset, Slice otherSlice, int otherOffset, int length) + { + return value.bytesEqual(0, offset, otherSlice, otherOffset, length); + } + + @Override + public int bytesCompare(int position, int offset, int length, Slice otherSlice, int otherOffset, int otherLength) + { + return value.bytesCompare(0, offset, length, otherSlice, otherOffset, otherLength); + } + + @Override + public void writeBytesTo(int position, int offset, int length, BlockBuilder blockBuilder) + { + value.writeBytesTo(0, offset, length, blockBuilder); + } + + @Override + public boolean equals(int position, int offset, Block otherBlock, int otherPosition, int otherOffset, int length) + { + return value.equals(0, offset, otherBlock, otherPosition, otherOffset, length); + } + + @Override + public int hash(int position, int offset, int length) + { + return value.hash(0, offset, length); + } + + @Override + public int compareTo(int leftPosition, int leftOffset, int leftLength, Block rightBlock, int rightPosition, int rightOffset, int rightLength) + { + return value.compareTo(0, leftOffset, leftLength, rightBlock, rightPosition, rightOffset, rightLength); + } + + @Override + public Block getSingleValueBlock(int position) + { + checkReadablePosition(position); + return value; + } + + @Override + public boolean isNull(int position) + { + checkReadablePosition(position); + return value.isNull(0); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("value", value) + .add("positionCount", positionCount) + .toString(); + } + + @Override + public void assureLoaded() + { + value.assureLoaded(); + } + + private void checkReadablePosition(int position) + { + checkArgument(position >= 0 && position < positionCount, "position is not valid"); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/testing/TestingTaskContext.java b/presto-main/src/main/java/com/facebook/presto/testing/TestingTaskContext.java new file mode 100644 index 00000000..51a53c5a --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/testing/TestingTaskContext.java @@ -0,0 +1,61 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.testing; + +import com.facebook.presto.Session; +import com.facebook.presto.execution.TaskId; +import com.facebook.presto.execution.TaskStateMachine; +import com.facebook.presto.memory.MemoryPool; +import com.facebook.presto.memory.MemoryPoolId; +import com.facebook.presto.memory.QueryContext; +import com.facebook.presto.operator.TaskContext; +import io.airlift.units.DataSize; + +import java.util.concurrent.Executor; + +import static com.facebook.presto.util.Threads.checkNotSameThreadExecutor; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.airlift.units.DataSize.Unit.GIGABYTE; +import static io.airlift.units.DataSize.Unit.MEGABYTE; + +public final class TestingTaskContext +{ + private TestingTaskContext() {} + + public static TaskContext createTaskContext(Executor executor, Session session) + { + return createTaskContext( + checkNotSameThreadExecutor(executor, "executor is null"), + session, + new DataSize(256, MEGABYTE)); + } + + public static TaskContext createTaskContext(Executor executor, Session session, DataSize maxMemory) + { + MemoryPool memoryPool = new MemoryPool(new MemoryPoolId("test"), new DataSize(1, GIGABYTE), false); + QueryContext queryContext = new QueryContext(false, new DataSize(10, MEGABYTE), memoryPool, executor); + return createTaskContext(queryContext, executor, session, maxMemory, new DataSize(1, MEGABYTE)); + } + + public static TaskContext createTaskContext(QueryContext queryContext, Executor executor, Session session, DataSize maxMemory, DataSize preallocated) + { + return queryContext.addTaskContext( + new TaskStateMachine(new TaskId("query", "stage", "task"), checkNotSameThreadExecutor(executor, "executor is null")), + session, + checkNotNull(maxMemory, "maxMemory is null"), + preallocated, + true, + true); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/type/ArrayParametricType.java b/presto-main/src/main/java/com/facebook/presto/type/ArrayParametricType.java new file mode 100644 index 00000000..7c9c13ad --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/type/ArrayParametricType.java @@ -0,0 +1,45 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.type; + +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkArgument; + +public final class ArrayParametricType + implements ParametricType +{ + public static final ArrayParametricType ARRAY = new ArrayParametricType(); + + private ArrayParametricType() + { + } + + @Override + public String getName() + { + return StandardTypes.ARRAY; + } + + @Override + public ArrayType createType(List types, List literals) + { + checkArgument(types.size() == 1, "Expected only one type, got %s", types); + checkArgument(literals.isEmpty(), "Unexpected literals: %s", literals); + return new ArrayType(types.get(0)); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/type/ArrayType.java b/presto-main/src/main/java/com/facebook/presto/type/ArrayType.java new file mode 100644 index 00000000..38545ff3 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/type/ArrayType.java @@ -0,0 +1,185 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.type; + +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.block.BlockBuilderStatus; +import com.facebook.presto.spi.block.VariableWidthBlockBuilder; +import com.facebook.presto.spi.type.AbstractVariableWidthType; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import io.airlift.slice.Slice; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import static com.facebook.presto.type.TypeUtils.appendToBlockBuilder; +import static com.facebook.presto.type.TypeUtils.buildStructuralSlice; +import static com.facebook.presto.type.TypeUtils.checkElementNotNull; +import static com.facebook.presto.type.TypeUtils.parameterizedTypeName; +import static com.facebook.presto.type.TypeUtils.readStructuralBlock; +import static com.google.common.base.Preconditions.checkNotNull; + +public class ArrayType + extends AbstractVariableWidthType +{ + private final Type elementType; + public static final String ARRAY_NULL_ELEMENT_MSG = "ARRAY comparison not supported for arrays with null elements"; + + public ArrayType(Type elementType) + { + super(parameterizedTypeName("array", elementType.getTypeSignature()), Slice.class); + this.elementType = checkNotNull(elementType, "elementType is null"); + } + + public Type getElementType() + { + return elementType; + } + + /** + * Takes a list of stack types and converts them to the stack representation of an array + */ + public static Slice toStackRepresentation(List values, Type elementType) + { + BlockBuilder blockBuilder = new VariableWidthBlockBuilder(new BlockBuilderStatus(), 0); + for (Object element : values) { + appendToBlockBuilder(elementType, element, blockBuilder); + } + return buildStructuralSlice(blockBuilder); + } + + @Override + public boolean isComparable() + { + return elementType.isComparable(); + } + + @Override + public boolean isOrderable() + { + return elementType.isOrderable(); + } + + @Override + public boolean equalTo(Block leftBlock, int leftPosition, Block rightBlock, int rightPosition) + { + return compareTo(leftBlock, leftPosition, rightBlock, rightPosition) == 0; + } + + @Override + public int hash(Block block, int position) + { + Slice value = getSlice(block, position); + Block array = readStructuralBlock(value); + List hashArray = new ArrayList<>(array.getPositionCount()); + for (int i = 0; i < array.getPositionCount(); i++) { + checkElementNotNull(array.isNull(i), ARRAY_NULL_ELEMENT_MSG); + hashArray.add(elementType.hash(array, i)); + } + return Objects.hash(hashArray); + } + + @Override + public int compareTo(Block leftBlock, int leftPosition, Block rightBlock, int rightPosition) + { + Slice leftSlice = getSlice(leftBlock, leftPosition); + Slice rightSlice = getSlice(rightBlock, rightPosition); + Block leftArray = readStructuralBlock(leftSlice); + Block rightArray = readStructuralBlock(rightSlice); + + int len = Math.min(leftArray.getPositionCount(), rightArray.getPositionCount()); + int index = 0; + while (index < len) { + checkElementNotNull(leftArray.isNull(index), ARRAY_NULL_ELEMENT_MSG); + checkElementNotNull(rightArray.isNull(index), ARRAY_NULL_ELEMENT_MSG); + int comparison = elementType.compareTo(leftArray, index, rightArray, index); + if (comparison != 0) { + return comparison; + } + index++; + } + + if (index == len) { + return leftArray.getPositionCount() - rightArray.getPositionCount(); + } + + return 0; + } + + @Override + public Object getObjectValue(ConnectorSession session, Block block, int position) + { + if (block.isNull(position)) { + return null; + } + + Slice slice = block.getSlice(position, 0, block.getLength(position)); + Block arrayBlock = readStructuralBlock(slice); + List values = Lists.newArrayListWithCapacity(arrayBlock.getPositionCount()); + + for (int i = 0; i < arrayBlock.getPositionCount(); i++) { + values.add(elementType.getObjectValue(session, arrayBlock, i)); + } + + return Collections.unmodifiableList(values); + } + + @Override + public void appendTo(Block block, int position, BlockBuilder blockBuilder) + { + if (block.isNull(position)) { + blockBuilder.appendNull(); + } + else { + block.writeBytesTo(position, 0, block.getLength(position), blockBuilder); + blockBuilder.closeEntry(); + } + } + + @Override + public Slice getSlice(Block block, int position) + { + return block.getSlice(position, 0, block.getLength(position)); + } + + @Override + public void writeSlice(BlockBuilder blockBuilder, Slice value) + { + writeSlice(blockBuilder, value, 0, value.length()); + } + + @Override + public void writeSlice(BlockBuilder blockBuilder, Slice value, int offset, int length) + { + blockBuilder.writeBytes(value, offset, length).closeEntry(); + } + + @Override + public List getTypeParameters() + { + return ImmutableList.of(getElementType()); + } + + @Override + public String getDisplayName() + { + return "array<" + elementType.getDisplayName() + ">"; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/type/BigintOperators.java b/presto-main/src/main/java/com/facebook/presto/type/BigintOperators.java new file mode 100644 index 00000000..c86303f9 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/type/BigintOperators.java @@ -0,0 +1,175 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.type; + +import com.facebook.presto.operator.scalar.ScalarOperator; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.type.StandardTypes; +import io.airlift.slice.Slice; +import io.airlift.slice.Slices; + +import static com.facebook.presto.metadata.OperatorType.ADD; +import static com.facebook.presto.metadata.OperatorType.BETWEEN; +import static com.facebook.presto.metadata.OperatorType.CAST; +import static com.facebook.presto.metadata.OperatorType.DIVIDE; +import static com.facebook.presto.metadata.OperatorType.EQUAL; +import static com.facebook.presto.metadata.OperatorType.GREATER_THAN; +import static com.facebook.presto.metadata.OperatorType.GREATER_THAN_OR_EQUAL; +import static com.facebook.presto.metadata.OperatorType.HASH_CODE; +import static com.facebook.presto.metadata.OperatorType.LESS_THAN; +import static com.facebook.presto.metadata.OperatorType.LESS_THAN_OR_EQUAL; +import static com.facebook.presto.metadata.OperatorType.MODULUS; +import static com.facebook.presto.metadata.OperatorType.MULTIPLY; +import static com.facebook.presto.metadata.OperatorType.NEGATION; +import static com.facebook.presto.metadata.OperatorType.NOT_EQUAL; +import static com.facebook.presto.metadata.OperatorType.SUBTRACT; +import static com.facebook.presto.spi.StandardErrorCode.DIVISION_BY_ZERO; +import static java.nio.charset.StandardCharsets.UTF_8; + +public final class BigintOperators +{ + private BigintOperators() + { + } + + @ScalarOperator(ADD) + @SqlType(StandardTypes.BIGINT) + public static long add(@SqlType(StandardTypes.BIGINT) long left, @SqlType(StandardTypes.BIGINT) long right) + { + return left + right; + } + + @ScalarOperator(SUBTRACT) + @SqlType(StandardTypes.BIGINT) + public static long subtract(@SqlType(StandardTypes.BIGINT) long left, @SqlType(StandardTypes.BIGINT) long right) + { + return left - right; + } + + @ScalarOperator(MULTIPLY) + @SqlType(StandardTypes.BIGINT) + public static long multiply(@SqlType(StandardTypes.BIGINT) long left, @SqlType(StandardTypes.BIGINT) long right) + { + return left * right; + } + + @ScalarOperator(DIVIDE) + @SqlType(StandardTypes.BIGINT) + public static long divide(@SqlType(StandardTypes.BIGINT) long left, @SqlType(StandardTypes.BIGINT) long right) + { + try { + return left / right; + } + catch (ArithmeticException e) { + throw new PrestoException(DIVISION_BY_ZERO, e); + } + } + + @ScalarOperator(MODULUS) + @SqlType(StandardTypes.BIGINT) + public static long modulus(@SqlType(StandardTypes.BIGINT) long left, @SqlType(StandardTypes.BIGINT) long right) + { + try { + return left % right; + } + catch (ArithmeticException e) { + throw new PrestoException(DIVISION_BY_ZERO, e); + } + } + + @ScalarOperator(NEGATION) + @SqlType(StandardTypes.BIGINT) + public static long negate(@SqlType(StandardTypes.BIGINT) long value) + { + return -value; + } + + @ScalarOperator(EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean equal(@SqlType(StandardTypes.BIGINT) long left, @SqlType(StandardTypes.BIGINT) long right) + { + return left == right; + } + + @ScalarOperator(NOT_EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean notEqual(@SqlType(StandardTypes.BIGINT) long left, @SqlType(StandardTypes.BIGINT) long right) + { + return left != right; + } + + @ScalarOperator(LESS_THAN) + @SqlType(StandardTypes.BOOLEAN) + public static boolean lessThan(@SqlType(StandardTypes.BIGINT) long left, @SqlType(StandardTypes.BIGINT) long right) + { + return left < right; + } + + @ScalarOperator(LESS_THAN_OR_EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean lessThanOrEqual(@SqlType(StandardTypes.BIGINT) long left, @SqlType(StandardTypes.BIGINT) long right) + { + return left <= right; + } + + @ScalarOperator(GREATER_THAN) + @SqlType(StandardTypes.BOOLEAN) + public static boolean greaterThan(@SqlType(StandardTypes.BIGINT) long left, @SqlType(StandardTypes.BIGINT) long right) + { + return left > right; + } + + @ScalarOperator(GREATER_THAN_OR_EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean greaterThanOrEqual(@SqlType(StandardTypes.BIGINT) long left, @SqlType(StandardTypes.BIGINT) long right) + { + return left >= right; + } + + @ScalarOperator(BETWEEN) + @SqlType(StandardTypes.BOOLEAN) + public static boolean between(@SqlType(StandardTypes.BIGINT) long value, @SqlType(StandardTypes.BIGINT) long min, @SqlType(StandardTypes.BIGINT) long max) + { + return min <= value && value <= max; + } + + @ScalarOperator(CAST) + @SqlType(StandardTypes.BOOLEAN) + public static boolean castToBoolean(@SqlType(StandardTypes.BIGINT) long value) + { + return value != 0; + } + + @ScalarOperator(CAST) + @SqlType(StandardTypes.DOUBLE) + public static double castToDouble(@SqlType(StandardTypes.BIGINT) long value) + { + return value; + } + + @ScalarOperator(CAST) + @SqlType(StandardTypes.VARCHAR) + public static Slice castToVarchar(@SqlType(StandardTypes.BIGINT) long value) + { + // todo optimize me + return Slices.copiedBuffer(String.valueOf(value), UTF_8); + } + + @ScalarOperator(HASH_CODE) + @SqlType(StandardTypes.BIGINT) + public static long hashCode(@SqlType(StandardTypes.BIGINT) long value) + { + return (int) (value ^ (value >>> 32)); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/type/BooleanOperators.java b/presto-main/src/main/java/com/facebook/presto/type/BooleanOperators.java new file mode 100644 index 00000000..798ffc6e --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/type/BooleanOperators.java @@ -0,0 +1,125 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.type; + +import com.facebook.presto.operator.scalar.ScalarFunction; +import com.facebook.presto.operator.scalar.ScalarOperator; +import com.facebook.presto.spi.type.StandardTypes; +import io.airlift.slice.Slice; +import io.airlift.slice.Slices; + +import static com.facebook.presto.metadata.OperatorType.BETWEEN; +import static com.facebook.presto.metadata.OperatorType.CAST; +import static com.facebook.presto.metadata.OperatorType.EQUAL; +import static com.facebook.presto.metadata.OperatorType.GREATER_THAN; +import static com.facebook.presto.metadata.OperatorType.GREATER_THAN_OR_EQUAL; +import static com.facebook.presto.metadata.OperatorType.HASH_CODE; +import static com.facebook.presto.metadata.OperatorType.LESS_THAN; +import static com.facebook.presto.metadata.OperatorType.LESS_THAN_OR_EQUAL; +import static com.facebook.presto.metadata.OperatorType.NOT_EQUAL; +import static java.nio.charset.StandardCharsets.US_ASCII; + +public final class BooleanOperators +{ + private static final Slice TRUE = Slices.copiedBuffer("true", US_ASCII); + private static final Slice FALSE = Slices.copiedBuffer("false", US_ASCII); + + private BooleanOperators() + { + } + + @ScalarOperator(EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean equal(@SqlType(StandardTypes.BOOLEAN) boolean left, @SqlType(StandardTypes.BOOLEAN) boolean right) + { + return left == right; + } + + @ScalarOperator(NOT_EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean notEqual(@SqlType(StandardTypes.BOOLEAN) boolean left, @SqlType(StandardTypes.BOOLEAN) boolean right) + { + return left != right; + } + + @ScalarOperator(LESS_THAN) + @SqlType(StandardTypes.BOOLEAN) + public static boolean lessThan(@SqlType(StandardTypes.BOOLEAN) boolean left, @SqlType(StandardTypes.BOOLEAN) boolean right) + { + return !left && right; + } + + @ScalarOperator(LESS_THAN_OR_EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean lessThanOrEqual(@SqlType(StandardTypes.BOOLEAN) boolean left, @SqlType(StandardTypes.BOOLEAN) boolean right) + { + return !left || right; + } + + @ScalarOperator(GREATER_THAN) + @SqlType(StandardTypes.BOOLEAN) + public static boolean greaterThan(@SqlType(StandardTypes.BOOLEAN) boolean left, @SqlType(StandardTypes.BOOLEAN) boolean right) + { + return left && !right; + } + + @ScalarOperator(GREATER_THAN_OR_EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean greaterThanOrEqual(@SqlType(StandardTypes.BOOLEAN) boolean left, @SqlType(StandardTypes.BOOLEAN) boolean right) + { + return left || !right; + } + + @ScalarOperator(BETWEEN) + @SqlType(StandardTypes.BOOLEAN) + public static boolean between(@SqlType(StandardTypes.BOOLEAN) boolean value, @SqlType(StandardTypes.BOOLEAN) boolean min, @SqlType(StandardTypes.BOOLEAN) boolean max) + { + return (value && max) || (!value && !min); + } + + @ScalarOperator(CAST) + @SqlType(StandardTypes.DOUBLE) + public static double castToDouble(@SqlType(StandardTypes.BOOLEAN) boolean value) + { + return value ? 1 : 0; + } + + @ScalarOperator(CAST) + @SqlType(StandardTypes.BIGINT) + public static long castToBigint(@SqlType(StandardTypes.BOOLEAN) boolean value) + { + return value ? 1 : 0; + } + + @ScalarOperator(CAST) + @SqlType(StandardTypes.VARCHAR) + public static Slice castToVarchar(@SqlType(StandardTypes.BOOLEAN) boolean value) + { + return value ? TRUE : FALSE; + } + + @ScalarOperator(HASH_CODE) + @SqlType(StandardTypes.BIGINT) + public static long hashCode(@SqlType(StandardTypes.BOOLEAN) boolean value) + { + return value ? 1231 : 1237; + } + + @SqlType(StandardTypes.BOOLEAN) + @ScalarFunction(hidden = true) // TODO: this should not be callable from SQL + public static boolean not(@SqlType(StandardTypes.BOOLEAN) boolean value) + { + return !value; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/type/ColorType.java b/presto-main/src/main/java/com/facebook/presto/type/ColorType.java new file mode 100644 index 00000000..d85ccb15 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/type/ColorType.java @@ -0,0 +1,96 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.type; + +import com.facebook.presto.operator.scalar.ColorFunctions; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.AbstractFixedWidthType; + +import static com.facebook.presto.type.TypeUtils.parameterizedTypeName; +import static io.airlift.slice.SizeOf.SIZE_OF_INT; + +public class ColorType + extends AbstractFixedWidthType +{ + public static final ColorType COLOR = new ColorType(); + public static final String NAME = "color"; + + private ColorType() + { + super(parameterizedTypeName(NAME), long.class, SIZE_OF_INT); + } + + @Override + public boolean isComparable() + { + return true; + } + + @Override + public Object getObjectValue(ConnectorSession session, Block block, int position) + { + if (block.isNull(position)) { + return null; + } + + int color = block.getInt(position, 0); + if (color < 0) { + return ColorFunctions.SystemColor.valueOf(-(color + 1)).getName(); + } + + return String.format("#%02x%02x%02x", + (color >> 16) & 0xFF, + (color >> 8) & 0xFF, + color & 0xFF); + } + + @Override + public boolean equalTo(Block leftBlock, int leftPosition, Block rightBlock, int rightPosition) + { + int leftValue = leftBlock.getInt(leftPosition, 0); + int rightValue = rightBlock.getInt(rightPosition, 0); + return leftValue == rightValue; + } + + @Override + public int hash(Block block, int position) + { + return block.getInt(position, 0); + } + + @Override + public void appendTo(Block block, int position, BlockBuilder blockBuilder) + { + if (block.isNull(position)) { + blockBuilder.appendNull(); + } + else { + blockBuilder.writeInt(block.getInt(position, 0)).closeEntry(); + } + } + + @Override + public long getLong(Block block, int position) + { + return block.getInt(position, 0); + } + + @Override + public void writeLong(BlockBuilder blockBuilder, long value) + { + blockBuilder.writeInt((int) value).closeEntry(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/type/DateOperators.java b/presto-main/src/main/java/com/facebook/presto/type/DateOperators.java new file mode 100644 index 00000000..89465241 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/type/DateOperators.java @@ -0,0 +1,147 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.type; + +import com.facebook.presto.operator.scalar.ScalarOperator; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.type.StandardTypes; +import io.airlift.slice.Slice; +import io.airlift.slice.Slices; +import org.joda.time.chrono.ISOChronology; + +import java.util.concurrent.TimeUnit; + +import static com.facebook.presto.metadata.OperatorType.BETWEEN; +import static com.facebook.presto.metadata.OperatorType.CAST; +import static com.facebook.presto.metadata.OperatorType.EQUAL; +import static com.facebook.presto.metadata.OperatorType.GREATER_THAN; +import static com.facebook.presto.metadata.OperatorType.GREATER_THAN_OR_EQUAL; +import static com.facebook.presto.metadata.OperatorType.HASH_CODE; +import static com.facebook.presto.metadata.OperatorType.LESS_THAN; +import static com.facebook.presto.metadata.OperatorType.LESS_THAN_OR_EQUAL; +import static com.facebook.presto.metadata.OperatorType.NOT_EQUAL; +import static com.facebook.presto.spi.StandardErrorCode.INVALID_CAST_ARGUMENT; +import static com.facebook.presto.spi.type.DateTimeEncoding.packDateTimeWithZone; +import static com.facebook.presto.util.DateTimeUtils.parseDate; +import static com.facebook.presto.util.DateTimeUtils.printDate; +import static com.facebook.presto.util.DateTimeZoneIndex.getChronology; +import static java.nio.charset.StandardCharsets.UTF_8; + +public final class DateOperators +{ + private DateOperators() + { + } + + @ScalarOperator(EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean equal(@SqlType(StandardTypes.DATE) long left, @SqlType(StandardTypes.DATE) long right) + { + return left == right; + } + + @ScalarOperator(NOT_EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean notEqual(@SqlType(StandardTypes.DATE) long left, @SqlType(StandardTypes.DATE) long right) + { + return left != right; + } + + @ScalarOperator(LESS_THAN) + @SqlType(StandardTypes.BOOLEAN) + public static boolean lessThan(@SqlType(StandardTypes.DATE) long left, @SqlType(StandardTypes.DATE) long right) + { + return left < right; + } + + @ScalarOperator(LESS_THAN_OR_EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean lessThanOrEqual(@SqlType(StandardTypes.DATE) long left, @SqlType(StandardTypes.DATE) long right) + { + return left <= right; + } + + @ScalarOperator(GREATER_THAN) + @SqlType(StandardTypes.BOOLEAN) + public static boolean greaterThan(@SqlType(StandardTypes.DATE) long left, @SqlType(StandardTypes.DATE) long right) + { + return left > right; + } + + @ScalarOperator(GREATER_THAN_OR_EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean greaterThanOrEqual(@SqlType(StandardTypes.DATE) long left, @SqlType(StandardTypes.DATE) long right) + { + return left >= right; + } + + @ScalarOperator(BETWEEN) + @SqlType(StandardTypes.BOOLEAN) + public static boolean between(@SqlType(StandardTypes.DATE) long value, @SqlType(StandardTypes.DATE) long min, @SqlType(StandardTypes.DATE) long max) + { + return min <= value && value <= max; + } + + @ScalarOperator(CAST) + @SqlType(StandardTypes.TIMESTAMP) + public static long castToTimestamp(ConnectorSession session, @SqlType(StandardTypes.DATE) long value) + { + long utcMillis = TimeUnit.DAYS.toMillis(value); + + // date is encoded as milliseconds at midnight in UTC + // convert to midnight if the session timezone + ISOChronology chronology = getChronology(session.getTimeZoneKey()); + return utcMillis - chronology.getZone().getOffset(utcMillis); + } + + @ScalarOperator(CAST) + @SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) + public static long castToTimestampWithTimeZone(ConnectorSession session, @SqlType(StandardTypes.DATE) long value) + { + long utcMillis = TimeUnit.DAYS.toMillis(value); + + // date is encoded as milliseconds at midnight in UTC + // convert to midnight if the session timezone + ISOChronology chronology = getChronology(session.getTimeZoneKey()); + long millis = utcMillis - chronology.getZone().getOffset(utcMillis); + return packDateTimeWithZone(millis, session.getTimeZoneKey()); + } + + @ScalarOperator(CAST) + @SqlType(StandardTypes.VARCHAR) + public static Slice castToSlice(@SqlType(StandardTypes.DATE) long value) + { + return Slices.copiedBuffer(printDate((int) value), UTF_8); + } + + @ScalarOperator(CAST) + @SqlType(StandardTypes.DATE) + public static long castFromSlice(@SqlType(StandardTypes.VARCHAR) Slice value) + { + try { + return parseDate(value.toStringUtf8()); + } + catch (IllegalArgumentException e) { + throw new PrestoException(INVALID_CAST_ARGUMENT, e); + } + } + + @ScalarOperator(HASH_CODE) + @SqlType(StandardTypes.BIGINT) + public static long hashCode(@SqlType(StandardTypes.DATE) long value) + { + return (int) value; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/type/DateTimeOperators.java b/presto-main/src/main/java/com/facebook/presto/type/DateTimeOperators.java new file mode 100644 index 00000000..1fd0fdb7 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/type/DateTimeOperators.java @@ -0,0 +1,267 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.type; + +import com.facebook.presto.operator.scalar.ScalarOperator; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.type.StandardTypes; +import org.joda.time.DateTimeField; +import org.joda.time.chrono.ISOChronology; + +import java.util.concurrent.TimeUnit; + +import static com.facebook.presto.metadata.OperatorType.ADD; +import static com.facebook.presto.metadata.OperatorType.SUBTRACT; +import static com.facebook.presto.spi.type.DateTimeEncoding.unpackMillisUtc; +import static com.facebook.presto.spi.type.DateTimeEncoding.updateMillisUtc; +import static com.facebook.presto.util.DateTimeZoneIndex.getChronology; +import static com.facebook.presto.util.DateTimeZoneIndex.unpackChronology; + +public final class DateTimeOperators +{ + private static final DateTimeField MILLIS_OF_DAY = ISOChronology.getInstanceUTC().millisOfDay(); + private static final DateTimeField MONTH_OF_YEAR_UTC = ISOChronology.getInstanceUTC().monthOfYear(); + + private DateTimeOperators() + { + } + + @ScalarOperator(ADD) + @SqlType(StandardTypes.DATE) + public static long datePlusIntervalDayToSecond(@SqlType(StandardTypes.DATE) long left, @SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long right) + { + if (MILLIS_OF_DAY.get(right) != 0) { + throw new IllegalArgumentException("Can not add hour, minutes or seconds to a Date"); + } + return left + TimeUnit.MILLISECONDS.toDays(right); + } + + @ScalarOperator(ADD) + @SqlType(StandardTypes.DATE) + public static long intervalDayToSecondPlusDate(@SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long left, @SqlType(StandardTypes.DATE) long right) + { + if (MILLIS_OF_DAY.get(left) != 0) { + throw new IllegalArgumentException("Can not add hour, minutes or seconds to a Date"); + } + return TimeUnit.MILLISECONDS.toDays(left) + right; + } + + @ScalarOperator(ADD) + @SqlType(StandardTypes.TIME) + public static long timePlusIntervalDayToSecond(ConnectorSession session, @SqlType(StandardTypes.TIME) long left, @SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long right) + { + return modulo24Hour(getChronology(session.getTimeZoneKey()), left + right); + } + + @ScalarOperator(ADD) + @SqlType(StandardTypes.TIME) + public static long intervalDayToSecondPlusTime(ConnectorSession session, @SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long left, @SqlType(StandardTypes.TIME) long right) + { + return modulo24Hour(getChronology(session.getTimeZoneKey()), left + right); + } + + @ScalarOperator(ADD) + @SqlType(StandardTypes.TIME_WITH_TIME_ZONE) + public static long timeWithTimeZonePlusIntervalDayToSecond(@SqlType(StandardTypes.TIME_WITH_TIME_ZONE) long left, @SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long right) + { + return updateMillisUtc((long) modulo24Hour(unpackChronology(left), unpackMillisUtc(left) + right), left); + } + + @ScalarOperator(ADD) + @SqlType(StandardTypes.TIME_WITH_TIME_ZONE) + public static long intervalDayToSecondPlusTimeWithTimeZone(@SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long left, @SqlType(StandardTypes.TIME_WITH_TIME_ZONE) long right) + { + return updateMillisUtc((long) modulo24Hour(unpackChronology(right), left + unpackMillisUtc(right)), right); + } + + @ScalarOperator(ADD) + @SqlType(StandardTypes.TIMESTAMP) + public static long timestampPlusIntervalDayToSecond(@SqlType(StandardTypes.TIMESTAMP) long left, @SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long right) + { + return left + right; + } + + @ScalarOperator(ADD) + @SqlType(StandardTypes.TIMESTAMP) + public static long intervalDayToSecondPlusTimestamp(@SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long left, @SqlType(StandardTypes.TIMESTAMP) long right) + { + return left + right; + } + + @ScalarOperator(ADD) + @SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) + public static long timestampWithTimeZonePlusIntervalDayToSecond(@SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long left, @SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long right) + { + return updateMillisUtc(unpackMillisUtc(left) + right, left); + } + + @ScalarOperator(ADD) + @SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) + public static long intervalDayToSecondPlusTimestampWithTimeZone(@SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long left, @SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long right) + { + return updateMillisUtc(left + unpackMillisUtc(right), right); + } + + @ScalarOperator(ADD) + @SqlType(StandardTypes.DATE) + public static long datePlusIntervalYearToMonth(@SqlType(StandardTypes.DATE) long left, @SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) long right) + { + long millis = MONTH_OF_YEAR_UTC.add(TimeUnit.DAYS.toMillis(left), right); + return TimeUnit.MILLISECONDS.toDays(millis); + } + + @ScalarOperator(ADD) + @SqlType(StandardTypes.DATE) + public static long intervalYearToMonthPlusDate(@SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) long left, @SqlType(StandardTypes.DATE) long right) + { + long millis = MONTH_OF_YEAR_UTC.add(TimeUnit.DAYS.toMillis(right), left); + return TimeUnit.MILLISECONDS.toDays(millis); + } + + @ScalarOperator(ADD) + @SqlType(StandardTypes.TIME) + public static long timePlusIntervalYearToMonth(@SqlType(StandardTypes.TIME) long left, @SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) long right) + { + return left; + } + + @ScalarOperator(ADD) + @SqlType(StandardTypes.TIME) + public static long intervalYearToMonthPlusTime(@SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) long left, @SqlType(StandardTypes.TIME) long right) + { + return right; + } + + @ScalarOperator(ADD) + @SqlType(StandardTypes.TIME_WITH_TIME_ZONE) + public static long timeWithTimeZonePlusIntervalYearToMonth(@SqlType(StandardTypes.TIME_WITH_TIME_ZONE) long left, @SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) long right) + { + return left; + } + + @ScalarOperator(ADD) + @SqlType(StandardTypes.TIME_WITH_TIME_ZONE) + public static long intervalYearToMonthPlusTimeWithTimeZone(@SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) long left, @SqlType(StandardTypes.TIME_WITH_TIME_ZONE) long right) + { + return right; + } + + @ScalarOperator(ADD) + @SqlType(StandardTypes.TIMESTAMP) + public static long timestampPlusIntervalYearToMonth(ConnectorSession session, @SqlType(StandardTypes.TIMESTAMP) long left, @SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) long right) + { + return getChronology(session.getTimeZoneKey()).monthOfYear().add(left, right); + } + + @ScalarOperator(ADD) + @SqlType(StandardTypes.TIMESTAMP) + public static long intervalYearToMonthPlusTimestamp(ConnectorSession session, @SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) long left, @SqlType(StandardTypes.TIMESTAMP) long right) + { + return getChronology(session.getTimeZoneKey()).monthOfYear().add(right, left); + } + + @ScalarOperator(ADD) + @SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) + public static long timestampWithTimeZonePlusIntervalYearToMonth(@SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long left, @SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) long right) + { + return updateMillisUtc(unpackChronology(left).monthOfYear().add(unpackMillisUtc(left), right), left); + } + + @ScalarOperator(ADD) + @SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) + public static long intervalYearToMonthPlusTimestampWithTimeZone(@SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) long left, @SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long right) + { + return updateMillisUtc(unpackChronology(right).monthOfYear().add(unpackMillisUtc(right), left), right); + } + + @ScalarOperator(SUBTRACT) + @SqlType(StandardTypes.DATE) + public static long dateMinusIntervalDayToSecond(@SqlType(StandardTypes.DATE) long left, @SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long right) + { + if (MILLIS_OF_DAY.get(right) != 0) { + throw new IllegalArgumentException("Can not subtract hour, minutes or seconds from a Date"); + } + return left - TimeUnit.MILLISECONDS.toDays(right); + } + + @ScalarOperator(SUBTRACT) + @SqlType(StandardTypes.TIME) + public static long timeMinusIntervalDayToSecond(ConnectorSession session, @SqlType(StandardTypes.TIME) long left, @SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long right) + { + return modulo24Hour(getChronology(session.getTimeZoneKey()), left - right); + } + + @ScalarOperator(SUBTRACT) + @SqlType(StandardTypes.TIME_WITH_TIME_ZONE) + public static long timeWithTimeZoneMinusIntervalDayToSecond(@SqlType(StandardTypes.TIME_WITH_TIME_ZONE) long left, @SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long right) + { + return updateMillisUtc((long) modulo24Hour(unpackChronology(left), unpackMillisUtc(left) - right), left); + } + + @ScalarOperator(SUBTRACT) + @SqlType(StandardTypes.TIMESTAMP) + public static long timestampMinusIntervalDayToSecond(@SqlType(StandardTypes.TIMESTAMP) long left, @SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long right) + { + return left - right; + } + + @ScalarOperator(SUBTRACT) + @SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) + public static long timestampWithTimeZoneMinusIntervalDayToSecond(@SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long left, @SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long right) + { + return updateMillisUtc(unpackMillisUtc(left) - right, left); + } + + @ScalarOperator(SUBTRACT) + @SqlType(StandardTypes.DATE) + public static long dateMinusIntervalYearToMonth(ConnectorSession session, @SqlType(StandardTypes.DATE) long left, @SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) long right) + { + long millis = MONTH_OF_YEAR_UTC.add(TimeUnit.DAYS.toMillis(left), -right); + return TimeUnit.MILLISECONDS.toDays(millis); + } + + @ScalarOperator(SUBTRACT) + @SqlType(StandardTypes.TIME) + public static long timeMinusIntervalYearToMonth(@SqlType(StandardTypes.TIME) long left, @SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) long right) + { + return left; + } + + @ScalarOperator(SUBTRACT) + @SqlType(StandardTypes.TIME_WITH_TIME_ZONE) + public static long timeWithTimeZoneMinusIntervalYearToMonth(@SqlType(StandardTypes.TIME_WITH_TIME_ZONE) long left, @SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) long right) + { + return left; + } + + @ScalarOperator(SUBTRACT) + @SqlType(StandardTypes.TIMESTAMP) + public static long timestampMinusIntervalYearToMonth(ConnectorSession session, @SqlType(StandardTypes.TIMESTAMP) long left, @SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) long right) + { + return getChronology(session.getTimeZoneKey()).monthOfYear().add(left, -right); + } + + @ScalarOperator(SUBTRACT) + @SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) + public static long timestampWithTimeZoneMinusIntervalYearToMonth(@SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long left, @SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) long right) + { + long dateTimeWithTimeZone = unpackChronology(left).monthOfYear().add(unpackMillisUtc(left), -right); + return updateMillisUtc(dateTimeWithTimeZone, left); + } + + public static int modulo24Hour(ISOChronology chronology, long millis) + { + return chronology.millisOfDay().get(millis) - chronology.getZone().getOffset(millis); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/type/DoubleOperators.java b/presto-main/src/main/java/com/facebook/presto/type/DoubleOperators.java new file mode 100644 index 00000000..e4de0e86 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/type/DoubleOperators.java @@ -0,0 +1,179 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.type; + +import com.facebook.presto.operator.scalar.MathFunctions; +import com.facebook.presto.operator.scalar.ScalarOperator; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.type.StandardTypes; +import io.airlift.slice.Slice; +import io.airlift.slice.Slices; + +import static com.facebook.presto.metadata.OperatorType.ADD; +import static com.facebook.presto.metadata.OperatorType.BETWEEN; +import static com.facebook.presto.metadata.OperatorType.CAST; +import static com.facebook.presto.metadata.OperatorType.DIVIDE; +import static com.facebook.presto.metadata.OperatorType.EQUAL; +import static com.facebook.presto.metadata.OperatorType.GREATER_THAN; +import static com.facebook.presto.metadata.OperatorType.GREATER_THAN_OR_EQUAL; +import static com.facebook.presto.metadata.OperatorType.HASH_CODE; +import static com.facebook.presto.metadata.OperatorType.LESS_THAN; +import static com.facebook.presto.metadata.OperatorType.LESS_THAN_OR_EQUAL; +import static com.facebook.presto.metadata.OperatorType.MODULUS; +import static com.facebook.presto.metadata.OperatorType.MULTIPLY; +import static com.facebook.presto.metadata.OperatorType.NEGATION; +import static com.facebook.presto.metadata.OperatorType.NOT_EQUAL; +import static com.facebook.presto.metadata.OperatorType.SUBTRACT; +import static com.facebook.presto.spi.StandardErrorCode.DIVISION_BY_ZERO; +import static java.lang.Double.doubleToLongBits; +import static java.nio.charset.StandardCharsets.UTF_8; + +public final class DoubleOperators +{ + private DoubleOperators() + { + } + + @ScalarOperator(ADD) + @SqlType(StandardTypes.DOUBLE) + public static double add(@SqlType(StandardTypes.DOUBLE) double left, @SqlType(StandardTypes.DOUBLE) double right) + { + return left + right; + } + + @ScalarOperator(SUBTRACT) + @SqlType(StandardTypes.DOUBLE) + public static double subtract(@SqlType(StandardTypes.DOUBLE) double left, @SqlType(StandardTypes.DOUBLE) double right) + { + return left - right; + } + + @ScalarOperator(MULTIPLY) + @SqlType(StandardTypes.DOUBLE) + public static double multiply(@SqlType(StandardTypes.DOUBLE) double left, @SqlType(StandardTypes.DOUBLE) double right) + { + return left * right; + } + + @ScalarOperator(DIVIDE) + @SqlType(StandardTypes.DOUBLE) + public static double divide(@SqlType(StandardTypes.DOUBLE) double left, @SqlType(StandardTypes.DOUBLE) double right) + { + try { + return left / right; + } + catch (ArithmeticException e) { + throw new PrestoException(DIVISION_BY_ZERO, e); + } + } + + @ScalarOperator(MODULUS) + @SqlType(StandardTypes.DOUBLE) + public static double modulus(@SqlType(StandardTypes.DOUBLE) double left, @SqlType(StandardTypes.DOUBLE) double right) + { + try { + return left % right; + } + catch (ArithmeticException e) { + throw new PrestoException(DIVISION_BY_ZERO, e); + } + } + + @ScalarOperator(NEGATION) + @SqlType(StandardTypes.DOUBLE) + public static double negate(@SqlType(StandardTypes.DOUBLE) double value) + { + return -value; + } + + @ScalarOperator(EQUAL) + @SuppressWarnings("FloatingPointEquality") + @SqlType(StandardTypes.BOOLEAN) + public static boolean equal(@SqlType(StandardTypes.DOUBLE) double left, @SqlType(StandardTypes.DOUBLE) double right) + { + return left == right; + } + + @ScalarOperator(NOT_EQUAL) + @SuppressWarnings("FloatingPointEquality") + @SqlType(StandardTypes.BOOLEAN) + public static boolean notEqual(@SqlType(StandardTypes.DOUBLE) double left, @SqlType(StandardTypes.DOUBLE) double right) + { + return left != right; + } + + @ScalarOperator(LESS_THAN) + @SqlType(StandardTypes.BOOLEAN) + public static boolean lessThan(@SqlType(StandardTypes.DOUBLE) double left, @SqlType(StandardTypes.DOUBLE) double right) + { + return left < right; + } + + @ScalarOperator(LESS_THAN_OR_EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean lessThanOrEqual(@SqlType(StandardTypes.DOUBLE) double left, @SqlType(StandardTypes.DOUBLE) double right) + { + return left <= right; + } + + @ScalarOperator(GREATER_THAN) + @SqlType(StandardTypes.BOOLEAN) + public static boolean greaterThan(@SqlType(StandardTypes.DOUBLE) double left, @SqlType(StandardTypes.DOUBLE) double right) + { + return left > right; + } + + @ScalarOperator(GREATER_THAN_OR_EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean greaterThanOrEqual(@SqlType(StandardTypes.DOUBLE) double left, @SqlType(StandardTypes.DOUBLE) double right) + { + return left >= right; + } + + @ScalarOperator(BETWEEN) + @SqlType(StandardTypes.BOOLEAN) + public static boolean between(@SqlType(StandardTypes.DOUBLE) double value, @SqlType(StandardTypes.DOUBLE) double min, @SqlType(StandardTypes.DOUBLE) double max) + { + return min <= value && value <= max; + } + + @ScalarOperator(CAST) + @SqlType(StandardTypes.BOOLEAN) + public static boolean castToBoolean(@SqlType(StandardTypes.DOUBLE) double value) + { + return value != 0; + } + + @ScalarOperator(CAST) + @SqlType(StandardTypes.BIGINT) + public static long castToLong(@SqlType(StandardTypes.DOUBLE) double value) + { + return (long) MathFunctions.round(value); + } + + @ScalarOperator(CAST) + @SqlType(StandardTypes.VARCHAR) + public static Slice castToVarchar(@SqlType(StandardTypes.DOUBLE) double value) + { + return Slices.copiedBuffer(String.valueOf(value), UTF_8); + } + + @ScalarOperator(HASH_CODE) + @SqlType(StandardTypes.BIGINT) + public static long hashCode(@SqlType(StandardTypes.DOUBLE) double value) + { + long bits = doubleToLongBits(value); + return (int) (bits ^ (bits >>> 32)); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/type/HyperLogLogOperators.java b/presto-main/src/main/java/com/facebook/presto/type/HyperLogLogOperators.java new file mode 100644 index 00000000..746e6673 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/type/HyperLogLogOperators.java @@ -0,0 +1,41 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.type; + +import com.facebook.presto.operator.scalar.ScalarOperator; +import com.facebook.presto.spi.type.StandardTypes; +import io.airlift.slice.Slice; + +import static com.facebook.presto.metadata.OperatorType.CAST; + +public final class HyperLogLogOperators +{ + private HyperLogLogOperators() + { + } + + @ScalarOperator(CAST) + @SqlType(StandardTypes.VARBINARY) + public static Slice castToBinary(@SqlType(StandardTypes.HYPER_LOG_LOG) Slice slice) + { + return slice; + } + + @ScalarOperator(CAST) + @SqlType(StandardTypes.HYPER_LOG_LOG) + public static Slice castFromVarbinary(@SqlType(StandardTypes.VARBINARY) Slice slice) + { + return slice; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/type/IntervalDayTimeOperators.java b/presto-main/src/main/java/com/facebook/presto/type/IntervalDayTimeOperators.java new file mode 100644 index 00000000..9f0358bb --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/type/IntervalDayTimeOperators.java @@ -0,0 +1,165 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.type; + +import com.facebook.presto.operator.scalar.ScalarOperator; +import com.facebook.presto.spi.type.SqlIntervalDayTime; +import com.facebook.presto.spi.type.StandardTypes; +import io.airlift.slice.Slice; +import io.airlift.slice.Slices; + +import static com.facebook.presto.metadata.OperatorType.ADD; +import static com.facebook.presto.metadata.OperatorType.BETWEEN; +import static com.facebook.presto.metadata.OperatorType.CAST; +import static com.facebook.presto.metadata.OperatorType.DIVIDE; +import static com.facebook.presto.metadata.OperatorType.EQUAL; +import static com.facebook.presto.metadata.OperatorType.GREATER_THAN; +import static com.facebook.presto.metadata.OperatorType.GREATER_THAN_OR_EQUAL; +import static com.facebook.presto.metadata.OperatorType.HASH_CODE; +import static com.facebook.presto.metadata.OperatorType.LESS_THAN; +import static com.facebook.presto.metadata.OperatorType.LESS_THAN_OR_EQUAL; +import static com.facebook.presto.metadata.OperatorType.MULTIPLY; +import static com.facebook.presto.metadata.OperatorType.NEGATION; +import static com.facebook.presto.metadata.OperatorType.NOT_EQUAL; +import static com.facebook.presto.metadata.OperatorType.SUBTRACT; +import static java.nio.charset.StandardCharsets.UTF_8; + +public final class IntervalDayTimeOperators +{ + private IntervalDayTimeOperators() + { + } + + @ScalarOperator(ADD) + @SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) + public static long add(@SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long left, @SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long right) + { + return left + right; + } + + @ScalarOperator(SUBTRACT) + @SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) + public static long subtract(@SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long left, @SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long right) + { + return left - right; + } + + @ScalarOperator(MULTIPLY) + @SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) + public static long multiplyByBigint(@SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long left, @SqlType(StandardTypes.BIGINT) long right) + { + return left * right; + } + + @ScalarOperator(MULTIPLY) + @SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) + public static long multiplyByDouble(@SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long left, @SqlType(StandardTypes.DOUBLE) double right) + { + return (long) (left * right); + } + + @ScalarOperator(MULTIPLY) + @SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) + public static long bigintMultiply(@SqlType(StandardTypes.BIGINT) long left, @SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long right) + { + return left * right; + } + + @ScalarOperator(MULTIPLY) + @SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) + public static long doubleMultiply(@SqlType(StandardTypes.DOUBLE) double left, @SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long right) + { + return (long) (left * right); + } + + @ScalarOperator(DIVIDE) + @SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) + public static long divideByDouble(@SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long left, @SqlType(StandardTypes.DOUBLE) double right) + { + return (long) (left / right); + } + + @ScalarOperator(NEGATION) + @SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) + public static long negate(@SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long value) + { + return -value; + } + + @ScalarOperator(EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean equal(@SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long left, @SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long right) + { + return left == right; + } + + @ScalarOperator(NOT_EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean notEqual(@SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long left, @SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long right) + { + return left != right; + } + + @ScalarOperator(LESS_THAN) + @SqlType(StandardTypes.BOOLEAN) + public static boolean lessThan(@SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long left, @SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long right) + { + return left < right; + } + + @ScalarOperator(LESS_THAN_OR_EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean lessThanOrEqual(@SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long left, @SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long right) + { + return left <= right; + } + + @ScalarOperator(GREATER_THAN) + @SqlType(StandardTypes.BOOLEAN) + public static boolean greaterThan(@SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long left, @SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long right) + { + return left > right; + } + + @ScalarOperator(GREATER_THAN_OR_EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean greaterThanOrEqual(@SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long left, @SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long right) + { + return left >= right; + } + + @ScalarOperator(BETWEEN) + @SqlType(StandardTypes.BOOLEAN) + public static boolean between( + @SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long value, + @SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long min, + @SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long max) + { + return min <= value && value <= max; + } + + @ScalarOperator(CAST) + @SqlType(StandardTypes.VARCHAR) + public static Slice castToSlice(@SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long value) + { + return Slices.copiedBuffer(SqlIntervalDayTime.formatMillis(value), UTF_8); + } + + @ScalarOperator(HASH_CODE) + @SqlType(StandardTypes.BIGINT) + public static long hashCode(@SqlType(StandardTypes.INTERVAL_DAY_TO_SECOND) long value) + { + return (int) (value ^ (value >>> 32)); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/type/IntervalYearMonthOperators.java b/presto-main/src/main/java/com/facebook/presto/type/IntervalYearMonthOperators.java new file mode 100644 index 00000000..b209f598 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/type/IntervalYearMonthOperators.java @@ -0,0 +1,164 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.type; + +import com.facebook.presto.operator.scalar.ScalarOperator; +import com.facebook.presto.spi.type.SqlIntervalYearMonth; +import com.facebook.presto.spi.type.StandardTypes; +import io.airlift.slice.Slice; + +import static com.facebook.presto.metadata.OperatorType.ADD; +import static com.facebook.presto.metadata.OperatorType.BETWEEN; +import static com.facebook.presto.metadata.OperatorType.CAST; +import static com.facebook.presto.metadata.OperatorType.DIVIDE; +import static com.facebook.presto.metadata.OperatorType.EQUAL; +import static com.facebook.presto.metadata.OperatorType.GREATER_THAN; +import static com.facebook.presto.metadata.OperatorType.GREATER_THAN_OR_EQUAL; +import static com.facebook.presto.metadata.OperatorType.HASH_CODE; +import static com.facebook.presto.metadata.OperatorType.LESS_THAN; +import static com.facebook.presto.metadata.OperatorType.LESS_THAN_OR_EQUAL; +import static com.facebook.presto.metadata.OperatorType.MULTIPLY; +import static com.facebook.presto.metadata.OperatorType.NEGATION; +import static com.facebook.presto.metadata.OperatorType.NOT_EQUAL; +import static com.facebook.presto.metadata.OperatorType.SUBTRACT; +import static io.airlift.slice.Slices.utf8Slice; + +public final class IntervalYearMonthOperators +{ + private IntervalYearMonthOperators() + { + } + + @ScalarOperator(ADD) + @SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) + public static long add(@SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) long left, @SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) long right) + { + return left + right; + } + + @ScalarOperator(SUBTRACT) + @SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) + public static long subtract(@SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) long left, @SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) long right) + { + return left - right; + } + + @ScalarOperator(MULTIPLY) + @SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) + public static long multiplyByBigint(@SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) long left, @SqlType(StandardTypes.BIGINT) long right) + { + return left * right; + } + + @ScalarOperator(MULTIPLY) + @SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) + public static long multiplyByDouble(@SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) long left, @SqlType(StandardTypes.DOUBLE) double right) + { + return (long) (left * right); + } + + @ScalarOperator(MULTIPLY) + @SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) + public static long bigintMultiply(@SqlType(StandardTypes.BIGINT) long left, @SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) long right) + { + return left * right; + } + + @ScalarOperator(MULTIPLY) + @SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) + public static long doubleMultiply(@SqlType(StandardTypes.DOUBLE) double left, @SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) long right) + { + return (long) (left * right); + } + + @ScalarOperator(DIVIDE) + @SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) + public static long divideByDouble(@SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) long left, @SqlType(StandardTypes.DOUBLE) double right) + { + return (long) (left / right); + } + + @ScalarOperator(NEGATION) + @SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) + public static long negate(@SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) long value) + { + return -value; + } + + @ScalarOperator(EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean equal(@SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) long left, @SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) long right) + { + return left == right; + } + + @ScalarOperator(NOT_EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean notEqual(@SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) long left, @SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) long right) + { + return left != right; + } + + @ScalarOperator(LESS_THAN) + @SqlType(StandardTypes.BOOLEAN) + public static boolean lessThan(@SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) long left, @SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) long right) + { + return left < right; + } + + @ScalarOperator(LESS_THAN_OR_EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean lessThanOrEqual(@SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) long left, @SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) long right) + { + return left <= right; + } + + @ScalarOperator(GREATER_THAN) + @SqlType(StandardTypes.BOOLEAN) + public static boolean greaterThan(@SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) long left, @SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) long right) + { + return left > right; + } + + @ScalarOperator(GREATER_THAN_OR_EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean greaterThanOrEqual(@SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) long left, @SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) long right) + { + return left >= right; + } + + @ScalarOperator(BETWEEN) + @SqlType(StandardTypes.BOOLEAN) + public static boolean between( + @SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) long value, + @SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) long min, + @SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) long max) + { + return min <= value && value <= max; + } + + @ScalarOperator(CAST) + @SqlType(StandardTypes.VARCHAR) + public static Slice castToSlice(@SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) long value) + { + return utf8Slice(SqlIntervalYearMonth.formatMonths(value)); + } + + @ScalarOperator(HASH_CODE) + @SqlType(StandardTypes.BIGINT) + public static long hashCode(@SqlType(StandardTypes.INTERVAL_YEAR_TO_MONTH) long value) + { + return (int) (value ^ (value >>> 32)); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/type/JsonPathType.java b/presto-main/src/main/java/com/facebook/presto/type/JsonPathType.java new file mode 100644 index 00000000..d7bdca1b --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/type/JsonPathType.java @@ -0,0 +1,61 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.type; + +import com.facebook.presto.operator.scalar.JsonPath; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.block.BlockBuilderStatus; +import com.facebook.presto.spi.type.AbstractType; + +import static com.facebook.presto.spi.StandardErrorCode.INTERNAL_ERROR; +import static com.facebook.presto.type.TypeUtils.parameterizedTypeName; + +public class JsonPathType + extends AbstractType +{ + public static final JsonPathType JSON_PATH = new JsonPathType(); + public static final String NAME = "JsonPath"; + + public JsonPathType() + { + super(parameterizedTypeName(NAME), JsonPath.class); + } + + @Override + public Object getObjectValue(ConnectorSession session, Block block, int position) + { + throw new UnsupportedOperationException(); + } + + @Override + public void appendTo(Block block, int position, BlockBuilder blockBuilder) + { + throw new UnsupportedOperationException(); + } + + @Override + public BlockBuilder createBlockBuilder(BlockBuilderStatus blockBuilderStatus, int expectedEntries, int expectedBytesPerEntry) + { + throw new PrestoException(INTERNAL_ERROR, "JsonPath type cannot be serialized"); + } + + @Override + public BlockBuilder createBlockBuilder(BlockBuilderStatus blockBuilderStatus, int expectedEntries) + { + throw new PrestoException(INTERNAL_ERROR, "JsonPath type cannot be serialized"); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/type/JsonType.java b/presto-main/src/main/java/com/facebook/presto/type/JsonType.java new file mode 100644 index 00000000..0ad2d85f --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/type/JsonType.java @@ -0,0 +1,103 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.type; + +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.AbstractVariableWidthType; +import com.facebook.presto.spi.type.StandardTypes; +import io.airlift.slice.Slice; +import io.airlift.slice.Slices; + +import static com.facebook.presto.type.TypeUtils.parameterizedTypeName; + +/** + * The stack representation for JSON objects must have the keys in natural sorted order. + */ +public class JsonType + extends AbstractVariableWidthType +{ + public static final JsonType JSON = new JsonType(); + + public JsonType() + { + super(parameterizedTypeName(StandardTypes.JSON), Slice.class); + } + + @Override + public boolean isComparable() + { + return true; + } + + @Override + public boolean equalTo(Block leftBlock, int leftPosition, Block rightBlock, int rightPosition) + { + Slice leftValue = leftBlock.getSlice(leftPosition, 0, leftBlock.getLength(leftPosition)); + Slice rightValue = rightBlock.getSlice(rightPosition, 0, rightBlock.getLength(rightPosition)); + return leftValue.equals(rightValue); + } + + @Override + public int hash(Block block, int position) + { + return block.hash(position, 0, block.getLength(position)); + } + + @Override + public Object getObjectValue(ConnectorSession session, Block block, int position) + { + if (block.isNull(position)) { + return null; + } + + return block.getSlice(position, 0, block.getLength(position)).toStringUtf8(); + } + + @Override + public void appendTo(Block block, int position, BlockBuilder blockBuilder) + { + if (block.isNull(position)) { + blockBuilder.appendNull(); + } + else { + block.writeBytesTo(position, 0, block.getLength(position), blockBuilder); + blockBuilder.closeEntry(); + } + } + + @Override + public Slice getSlice(Block block, int position) + { + return block.getSlice(position, 0, block.getLength(position)); + } + + public void writeString(BlockBuilder blockBuilder, String value) + { + writeSlice(blockBuilder, Slices.utf8Slice(value)); + } + + @Override + public void writeSlice(BlockBuilder blockBuilder, Slice value) + { + writeSlice(blockBuilder, value, 0, value.length()); + } + + @Override + public void writeSlice(BlockBuilder blockBuilder, Slice value, int offset, int length) + { + blockBuilder.writeBytes(value, offset, length).closeEntry(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/type/LikeFunctions.java b/presto-main/src/main/java/com/facebook/presto/type/LikeFunctions.java new file mode 100644 index 00000000..8b55b9d2 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/type/LikeFunctions.java @@ -0,0 +1,152 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.type; + +import com.facebook.presto.metadata.OperatorType; +import com.facebook.presto.operator.scalar.ScalarFunction; +import com.facebook.presto.operator.scalar.ScalarOperator; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.type.StandardTypes; +import io.airlift.jcodings.specific.UTF8Encoding; +import io.airlift.joni.Option; +import io.airlift.joni.Regex; +import io.airlift.joni.Syntax; +import io.airlift.slice.Slice; + +import static com.facebook.presto.spi.StandardErrorCode.INVALID_FUNCTION_ARGUMENT; +import static io.airlift.joni.constants.MetaChar.INEFFECTIVE_META_CHAR; +import static io.airlift.joni.constants.SyntaxProperties.OP_ASTERISK_ZERO_INF; +import static io.airlift.joni.constants.SyntaxProperties.OP_DOT_ANYCHAR; +import static io.airlift.joni.constants.SyntaxProperties.OP_LINE_ANCHOR; +import static java.nio.charset.StandardCharsets.UTF_8; + +public final class LikeFunctions +{ + private static final Syntax SYNTAX = new Syntax( + OP_DOT_ANYCHAR | OP_ASTERISK_ZERO_INF | OP_LINE_ANCHOR, + 0, + 0, + Option.NONE, + new Syntax.MetaCharTable( + '\\', /* esc */ + INEFFECTIVE_META_CHAR, /* anychar '.' */ + INEFFECTIVE_META_CHAR, /* anytime '*' */ + INEFFECTIVE_META_CHAR, /* zero or one time '?' */ + INEFFECTIVE_META_CHAR, /* one or more time '+' */ + INEFFECTIVE_META_CHAR /* anychar anytime */ + ) + ); + + private LikeFunctions() {} + + // TODO: this should not be callable from SQL + @ScalarFunction(value = "like", hidden = true) + @SqlType(StandardTypes.BOOLEAN) + public static boolean like(@SqlType(StandardTypes.VARCHAR) Slice value, @SqlType(LikePatternType.NAME) Regex pattern) + { + // Joni doesn't handle invalid UTF-8, so replace invalid characters + byte[] bytes = value.getBytes(); + if (isAscii(bytes)) { + return regexMatches(pattern, bytes); + } + // convert to a String and back to "fix" any broken UTF-8 sequences + return regexMatches(pattern, value.toStringUtf8().getBytes(UTF_8)); + } + + @ScalarOperator(OperatorType.CAST) + @SqlType(LikePatternType.NAME) + public static Regex likePattern(@SqlType(StandardTypes.VARCHAR) Slice pattern) + { + return likeToPattern(pattern.toStringUtf8(), '0', false); + } + + @ScalarFunction + @SqlType(LikePatternType.NAME) + public static Regex likePattern(@SqlType(StandardTypes.VARCHAR) Slice pattern, @SqlType(StandardTypes.VARCHAR) Slice escape) + { + return likeToPattern(pattern.toStringUtf8(), getEscapeChar(escape), true); + } + + private static boolean regexMatches(Regex regex, byte[] bytes) + { + return regex.matcher(bytes).match(0, bytes.length, Option.NONE) != -1; + } + + @SuppressWarnings("NestedSwitchStatement") + private static Regex likeToPattern(String patternString, char escapeChar, boolean shouldEscape) + { + StringBuilder regex = new StringBuilder(patternString.length() * 2); + + regex.append('^'); + boolean escaped = false; + for (char currentChar : patternString.toCharArray()) { + if (shouldEscape && !escaped && (currentChar == escapeChar)) { + escaped = true; + } + else { + switch (currentChar) { + case '%': + regex.append(escaped ? "%" : ".*"); + escaped = false; + break; + case '_': + regex.append(escaped ? "_" : "."); + escaped = false; + break; + default: + // escape special regex characters + switch (currentChar) { + case '\\': + case '^': + case '$': + case '.': + case '*': + regex.append('\\'); + } + + regex.append(currentChar); + escaped = false; + } + } + } + regex.append('$'); + + byte[] bytes = regex.toString().getBytes(UTF_8); + return new Regex(bytes, 0, bytes.length, Option.MULTILINE, UTF8Encoding.INSTANCE, SYNTAX); + } + + @SuppressWarnings("NumericCastThatLosesPrecision") + private static char getEscapeChar(Slice escape) + { + String escapeString = escape.toString(UTF_8); + if (escapeString.isEmpty()) { + // escaping disabled + return (char) -1; // invalid character + } + if (escapeString.length() == 1) { + return escapeString.charAt(0); + } + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, "Escape must be empty or a single character"); + } + + private static boolean isAscii(byte[] bytes) + { + for (byte b : bytes) { + if (b < 0) { + return false; + } + } + return true; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/type/LikePatternType.java b/presto-main/src/main/java/com/facebook/presto/type/LikePatternType.java new file mode 100644 index 00000000..051aa5f0 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/type/LikePatternType.java @@ -0,0 +1,61 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.type; + +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.block.BlockBuilderStatus; +import com.facebook.presto.spi.type.AbstractType; +import io.airlift.joni.Regex; + +import static com.facebook.presto.spi.StandardErrorCode.INTERNAL_ERROR; +import static com.facebook.presto.type.TypeUtils.parameterizedTypeName; + +public class LikePatternType + extends AbstractType +{ + public static final LikePatternType LIKE_PATTERN = new LikePatternType(); + public static final String NAME = "LikePattern"; + + public LikePatternType() + { + super(parameterizedTypeName(NAME), Regex.class); + } + + @Override + public Object getObjectValue(ConnectorSession session, Block block, int position) + { + throw new UnsupportedOperationException(); + } + + @Override + public void appendTo(Block block, int position, BlockBuilder blockBuilder) + { + throw new UnsupportedOperationException(); + } + + @Override + public BlockBuilder createBlockBuilder(BlockBuilderStatus blockBuilderStatus, int expectedEntries, int expectedBytesPerEntry) + { + throw new PrestoException(INTERNAL_ERROR, "LikePattern type cannot be serialized"); + } + + @Override + public BlockBuilder createBlockBuilder(BlockBuilderStatus blockBuilderStatus, int expectedEntries) + { + throw new PrestoException(INTERNAL_ERROR, "LikePattern type cannot be serialized"); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/type/MapParametricType.java b/presto-main/src/main/java/com/facebook/presto/type/MapParametricType.java new file mode 100644 index 00000000..1e8e7aa2 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/type/MapParametricType.java @@ -0,0 +1,45 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.type; + +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkArgument; + +public final class MapParametricType + implements ParametricType +{ + public static final MapParametricType MAP = new MapParametricType(); + + private MapParametricType() + { + } + + @Override + public String getName() + { + return StandardTypes.MAP; + } + + @Override + public MapType createType(List types, List literals) + { + checkArgument(types.size() == 2, "Expected two types"); + checkArgument(literals.isEmpty(), "Unexpected literals: %s", literals); + return new MapType(types.get(0), types.get(1)); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/type/MapType.java b/presto-main/src/main/java/com/facebook/presto/type/MapType.java new file mode 100644 index 00000000..7032314b --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/type/MapType.java @@ -0,0 +1,226 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.type; + +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.block.BlockBuilderStatus; +import com.facebook.presto.spi.block.VariableWidthBlockBuilder; +import com.facebook.presto.spi.type.AbstractVariableWidthType; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.facebook.presto.type.TypeUtils.appendToBlockBuilder; +import static com.facebook.presto.type.TypeUtils.buildStructuralSlice; +import static com.facebook.presto.type.TypeUtils.checkElementNotNull; +import static com.facebook.presto.type.TypeUtils.hashPosition; +import static com.facebook.presto.type.TypeUtils.parameterizedTypeName; +import static com.facebook.presto.type.TypeUtils.readStructuralBlock; +import static com.google.common.base.Preconditions.checkArgument; + +public class MapType + extends AbstractVariableWidthType +{ + private final Type keyType; + private final Type valueType; + private static final String MAP_NULL_ELEMENT_MSG = "MAP comparison not supported for null value elements"; + + public MapType(Type keyType, Type valueType) + { + super(parameterizedTypeName("map", keyType.getTypeSignature(), valueType.getTypeSignature()), Slice.class); + checkArgument(keyType.isComparable(), "key type must be comparable"); + this.keyType = keyType; + this.valueType = valueType; + } + + public static Slice toStackRepresentation(Map value, Type keyType, Type valueType) + { + BlockBuilder blockBuilder = new VariableWidthBlockBuilder(new BlockBuilderStatus(), 1024); + for (Map.Entry entry : value.entrySet()) { + appendToBlockBuilder(keyType, entry.getKey(), blockBuilder); + appendToBlockBuilder(valueType, entry.getValue(), blockBuilder); + } + return buildStructuralSlice(blockBuilder); + } + + public Type getKeyType() + { + return keyType; + } + + public Type getValueType() + { + return valueType; + } + + @Override + public boolean isComparable() + { + return valueType.isComparable(); + } + + @Override + public int hash(Block block, int position) + { + Slice mapSlice = getSlice(block, position); + Block mapBlock = readStructuralBlock(mapSlice); + int result = 0; + + for (int i = 0; i < mapBlock.getPositionCount(); i += 2) { + result += hashPosition(keyType, mapBlock, i); + result += hashPosition(valueType, mapBlock, i + 1); + } + return result; + } + + @Override + public boolean equalTo(Block leftBlock, int leftPosition, Block rightBlock, int rightPosition) + { + Slice leftSlice = getSlice(leftBlock, leftPosition); + Slice rightSlice = getSlice(rightBlock, rightPosition); + Block leftMapBlock = readStructuralBlock(leftSlice); + Block rightMapBlock = readStructuralBlock(rightSlice); + + if (leftMapBlock.getPositionCount() != rightMapBlock.getPositionCount()) { + return false; + } + + Map wrappedLeftMap = new HashMap<>(); + for (int position = 0; position < leftMapBlock.getPositionCount(); position += 2) { + wrappedLeftMap.put(new KeyWrapper(keyType, leftMapBlock, position), position + 1); + } + + for (int position = 0; position < rightMapBlock.getPositionCount(); position += 2) { + KeyWrapper key = new KeyWrapper(keyType, rightMapBlock, position); + Integer leftValuePosition = wrappedLeftMap.get(key); + if (leftValuePosition == null) { + return false; + } + int rightValuePosition = position + 1; + checkElementNotNull(leftMapBlock.isNull(leftValuePosition), MAP_NULL_ELEMENT_MSG); + checkElementNotNull(rightMapBlock.isNull(rightValuePosition), MAP_NULL_ELEMENT_MSG); + + if (!valueType.equalTo(leftMapBlock, leftValuePosition, rightMapBlock, rightValuePosition)) { + return false; + } + } + return true; + } + + private static final class KeyWrapper + { + private final Type type; + private final Block block; + private final int position; + + public KeyWrapper(Type type, Block block, int position) + { + this.type = type; + this.block = block; + this.position = position; + } + + public Block getBlock() + { + return this.block; + } + + public int getPosition() + { + return this.position; + } + + @Override + public int hashCode() + { + return type.hash(block, position); + } + + @Override + public boolean equals(Object obj) + { + if (obj == null || !getClass().equals(obj.getClass())) { + return false; + } + KeyWrapper other = (KeyWrapper) obj; + return type.equalTo(this.block, this.position, other.getBlock(), other.getPosition()); + } + } + + @Override + public Object getObjectValue(ConnectorSession session, Block block, int position) + { + if (block.isNull(position)) { + return null; + } + + Slice slice = block.getSlice(position, 0, block.getLength(position)); + Block mapBlock = readStructuralBlock(slice); + Map map = new HashMap<>(); + for (int i = 0; i < mapBlock.getPositionCount(); i += 2) { + map.put(keyType.getObjectValue(session, mapBlock, i), valueType.getObjectValue(session, mapBlock, i + 1)); + } + + return Collections.unmodifiableMap(map); + } + + @Override + public void appendTo(Block block, int position, BlockBuilder blockBuilder) + { + if (block.isNull(position)) { + blockBuilder.appendNull(); + } + else { + block.writeBytesTo(position, 0, block.getLength(position), blockBuilder); + blockBuilder.closeEntry(); + } + } + + @Override + public Slice getSlice(Block block, int position) + { + return block.getSlice(position, 0, block.getLength(position)); + } + + @Override + public void writeSlice(BlockBuilder blockBuilder, Slice value) + { + writeSlice(blockBuilder, value, 0, value.length()); + } + + @Override + public void writeSlice(BlockBuilder blockBuilder, Slice value, int offset, int length) + { + blockBuilder.writeBytes(value, offset, length).closeEntry(); + } + + @Override + public List getTypeParameters() + { + return ImmutableList.of(getKeyType(), getValueType()); + } + + @Override + public String getDisplayName() + { + return "map<" + keyType.getDisplayName() + ", " + valueType.getDisplayName() + ">"; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/type/ParametricType.java b/presto-main/src/main/java/com/facebook/presto/type/ParametricType.java new file mode 100644 index 00000000..fe2c33ac --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/type/ParametricType.java @@ -0,0 +1,25 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.type; + +import com.facebook.presto.spi.type.Type; + +import java.util.List; + +public interface ParametricType +{ + String getName(); + + Type createType(List types, List literals); +} diff --git a/presto-main/src/main/java/com/facebook/presto/type/RawSliceSerializer.java b/presto-main/src/main/java/com/facebook/presto/type/RawSliceSerializer.java new file mode 100644 index 00000000..f256a392 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/type/RawSliceSerializer.java @@ -0,0 +1,32 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.type; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import io.airlift.slice.Slice; + +import java.io.IOException; + +public class RawSliceSerializer + extends JsonSerializer +{ + @Override + public void serialize(Slice slice, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) + throws IOException + { + jsonGenerator.writeRawValue(slice.toStringUtf8()); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/type/RegexpType.java b/presto-main/src/main/java/com/facebook/presto/type/RegexpType.java new file mode 100644 index 00000000..78733123 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/type/RegexpType.java @@ -0,0 +1,61 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.type; + +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.block.BlockBuilderStatus; +import com.facebook.presto.spi.type.AbstractType; +import io.airlift.joni.Regex; + +import static com.facebook.presto.spi.StandardErrorCode.INTERNAL_ERROR; +import static com.facebook.presto.type.TypeUtils.parameterizedTypeName; + +public class RegexpType + extends AbstractType +{ + public static final RegexpType REGEXP = new RegexpType(); + public static final String NAME = "RegExp"; + + public RegexpType() + { + super(parameterizedTypeName(NAME), Regex.class); + } + + @Override + public Object getObjectValue(ConnectorSession session, Block block, int position) + { + throw new UnsupportedOperationException(); + } + + @Override + public void appendTo(Block block, int position, BlockBuilder blockBuilder) + { + throw new UnsupportedOperationException(); + } + + @Override + public BlockBuilder createBlockBuilder(BlockBuilderStatus blockBuilderStatus, int expectedEntries, int expectedBytesPerEntry) + { + throw new PrestoException(INTERNAL_ERROR, "RegExp type cannot be serialized"); + } + + @Override + public BlockBuilder createBlockBuilder(BlockBuilderStatus blockBuilderStatus, int expectedEntries) + { + throw new PrestoException(INTERNAL_ERROR, "RegExp type cannot be serialized"); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/type/RowParametricType.java b/presto-main/src/main/java/com/facebook/presto/type/RowParametricType.java new file mode 100644 index 00000000..5a4804d9 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/type/RowParametricType.java @@ -0,0 +1,72 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.type; + +import com.facebook.presto.metadata.ParametricFunction; +import com.facebook.presto.operator.scalar.RowFieldReference; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.google.common.collect.ImmutableList; + +import java.util.List; +import java.util.Optional; + +import static com.facebook.presto.type.RowType.RowField; +import static com.facebook.presto.util.Types.checkType; +import static com.google.common.base.Preconditions.checkArgument; + +public final class RowParametricType + implements ParametricType +{ + public static final RowParametricType ROW = new RowParametricType(); + + private RowParametricType() + { + } + + @Override + public String getName() + { + return StandardTypes.ROW; + } + + @Override + public RowType createType(List types, List literals) + { + checkArgument(!types.isEmpty(), "types is empty"); + + if (literals.isEmpty()) { + return new RowType(types, Optional.empty()); + } + + checkArgument(types.size() == literals.size(), "types and literals must be matched in size"); + + ImmutableList.Builder builder = ImmutableList.builder(); + for (Object literal : literals) { + builder.add(checkType(literal, String.class, "literal")); + } + return new RowType(types, Optional.of(builder.build())); + } + + public List createFunctions(Type type) + { + RowType rowType = checkType(type, RowType.class, "type"); + ImmutableList.Builder builder = ImmutableList.builder(); + for (RowField field : rowType.getFields()) { + field.getName() + .ifPresent(name -> builder.add(new RowFieldReference(rowType, field.getName().get()))); + } + return builder.build(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/type/RowType.java b/presto-main/src/main/java/com/facebook/presto/type/RowType.java new file mode 100644 index 00000000..62c34b34 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/type/RowType.java @@ -0,0 +1,219 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.type; + +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.StandardErrorCode; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.AbstractVariableWidthType; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeSignature; +import com.google.common.base.Joiner; +import com.google.common.base.Predicate; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import io.airlift.slice.Slice; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static com.facebook.presto.type.TypeUtils.readStructuralBlock; +import static com.facebook.presto.util.ImmutableCollectors.toImmutableList; +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * As defined in ISO/IEC FCD 9075-2 (SQL 2011), section 4.8 + */ +public class RowType + extends AbstractVariableWidthType +{ + private final List fields; + + public RowType(List fieldTypes, Optional> fieldNames) + { + super(new TypeSignature( + "row", + Lists.transform(fieldTypes, Type::getTypeSignature), + fieldNames.orElse(ImmutableList.of()).stream() + .map(Object.class::cast) + .collect(toImmutableList())), + Slice.class); + + ImmutableList.Builder builder = ImmutableList.builder(); + for (int i = 0; i < fieldTypes.size(); i++) { + int index = i; + builder.add(new RowField(fieldTypes.get(i), fieldNames.map((names) -> names.get(index)))); + } + fields = builder.build(); + } + + @Override + public String getDisplayName() + { + // Convert to standard sql name + List fieldDisplayNames = new ArrayList<>(); + for (RowField field : fields) { + String typeDisplayName = field.getType().getDisplayName(); + if (field.getName().isPresent()) { + fieldDisplayNames.add(field.getName().get() + " " + typeDisplayName); + } + else { + fieldDisplayNames.add(typeDisplayName); + } + } + return "row(" + Joiner.on(", ").join(fieldDisplayNames) + ")"; + } + + @Override + public Object getObjectValue(ConnectorSession session, Block block, int position) + { + if (block.isNull(position)) { + return null; + } + + Slice slice = getSlice(block, position); + Block arrayBlock = readStructuralBlock(slice); + List values = Lists.newArrayListWithCapacity(arrayBlock.getPositionCount()); + + for (int i = 0; i < arrayBlock.getPositionCount(); i++) { + values.add(fields.get(i).getType().getObjectValue(session, arrayBlock, i)); + } + + return Collections.unmodifiableList(values); + } + + @Override + public void appendTo(Block block, int position, BlockBuilder blockBuilder) + { + if (block.isNull(position)) { + blockBuilder.appendNull(); + } + else { + block.writeBytesTo(position, 0, block.getLength(position), blockBuilder); + blockBuilder.closeEntry(); + } + } + + @Override + public Slice getSlice(Block block, int position) + { + return block.getSlice(position, 0, block.getLength(position)); + } + + @Override + public void writeSlice(BlockBuilder blockBuilder, Slice value) + { + writeSlice(blockBuilder, value, 0, value.length()); + } + + @Override + public void writeSlice(BlockBuilder blockBuilder, Slice value, int offset, int length) + { + blockBuilder.writeBytes(value, offset, length).closeEntry(); + } + + @Override + public List getTypeParameters() + { + return fields.stream() + .map(RowField::getType) + .collect(toImmutableList()); + } + + public List getFields() + { + return fields; + } + + public static class RowField + { + private final Type type; + private final Optional name; + + public RowField(Type type, Optional name) + { + this.type = checkNotNull(type, "type is null"); + this.name = checkNotNull(name, "name is null"); + } + + public Type getType() + { + return type; + } + + public Optional getName() + { + return name; + } + } + + @Override + public boolean isComparable() + { + return Iterables.all(fields, new Predicate() + { + @Override + public boolean apply(RowField field) + { + return field.getType().isComparable(); + } + }); + } + + @Override + public boolean equalTo(Block leftBlock, int leftPosition, Block rightBlock, int rightPosition) + { + Slice leftSlice = getSlice(leftBlock, leftPosition); + Slice rightSlice = getSlice(rightBlock, rightPosition); + Block leftArray = readStructuralBlock(leftSlice); + Block rightArray = readStructuralBlock(rightSlice); + + for (int i = 0; i < leftArray.getPositionCount(); i++) { + checkElementNotNull(leftArray.isNull(i)); + checkElementNotNull(rightArray.isNull(i)); + Type fieldType = fields.get(i).getType(); + if (!fieldType.equalTo(leftArray, i, rightArray, i)) { + return false; + } + } + + return true; + } + + @Override + public int hash(Block block, int position) + { + Slice value = getSlice(block, position); + Block arrayBlock = readStructuralBlock(value); + int result = 1; + for (int i = 0; i < arrayBlock.getPositionCount(); i++) { + checkElementNotNull(arrayBlock.isNull(i)); + Type elementType = fields.get(i).getType(); + result = 31 * result + elementType.hash(arrayBlock, i); + } + return result; + } + + private static void checkElementNotNull(boolean isNull) + { + if (isNull) { + throw new PrestoException(StandardErrorCode.NOT_SUPPORTED, "ROW comparison not supported for fields with null elements"); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/type/SqlType.java b/presto-main/src/main/java/com/facebook/presto/type/SqlType.java new file mode 100644 index 00000000..3077e7cc --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/type/SqlType.java @@ -0,0 +1,26 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.type; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.PARAMETER}) +public @interface SqlType +{ + String value() default ""; +} diff --git a/presto-main/src/main/java/com/facebook/presto/type/TimeOperators.java b/presto-main/src/main/java/com/facebook/presto/type/TimeOperators.java new file mode 100644 index 00000000..3520d57f --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/type/TimeOperators.java @@ -0,0 +1,139 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.type; + +import com.facebook.presto.operator.scalar.ScalarOperator; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.type.StandardTypes; +import io.airlift.slice.Slice; +import io.airlift.slice.Slices; + +import static com.facebook.presto.metadata.OperatorType.BETWEEN; +import static com.facebook.presto.metadata.OperatorType.CAST; +import static com.facebook.presto.metadata.OperatorType.EQUAL; +import static com.facebook.presto.metadata.OperatorType.GREATER_THAN; +import static com.facebook.presto.metadata.OperatorType.GREATER_THAN_OR_EQUAL; +import static com.facebook.presto.metadata.OperatorType.HASH_CODE; +import static com.facebook.presto.metadata.OperatorType.LESS_THAN; +import static com.facebook.presto.metadata.OperatorType.LESS_THAN_OR_EQUAL; +import static com.facebook.presto.metadata.OperatorType.NOT_EQUAL; +import static com.facebook.presto.spi.StandardErrorCode.INVALID_CAST_ARGUMENT; +import static com.facebook.presto.spi.type.DateTimeEncoding.packDateTimeWithZone; +import static com.facebook.presto.util.DateTimeUtils.parseTimeWithoutTimeZone; +import static com.facebook.presto.util.DateTimeUtils.printTimeWithoutTimeZone; +import static java.nio.charset.StandardCharsets.UTF_8; + +public final class TimeOperators +{ + private TimeOperators() + { + } + + @ScalarOperator(EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean equal(@SqlType(StandardTypes.TIME) long left, @SqlType(StandardTypes.TIME) long right) + { + return left == right; + } + + @ScalarOperator(NOT_EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean notEqual(@SqlType(StandardTypes.TIME) long left, @SqlType(StandardTypes.TIME) long right) + { + return left != right; + } + + @ScalarOperator(LESS_THAN) + @SqlType(StandardTypes.BOOLEAN) + public static boolean lessThan(@SqlType(StandardTypes.TIME) long left, @SqlType(StandardTypes.TIME) long right) + { + return left < right; + } + + @ScalarOperator(LESS_THAN_OR_EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean lessThanOrEqual(@SqlType(StandardTypes.TIME) long left, @SqlType(StandardTypes.TIME) long right) + { + return left <= right; + } + + @ScalarOperator(GREATER_THAN) + @SqlType(StandardTypes.BOOLEAN) + public static boolean greaterThan(@SqlType(StandardTypes.TIME) long left, @SqlType(StandardTypes.TIME) long right) + { + return left > right; + } + + @ScalarOperator(GREATER_THAN_OR_EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean greaterThanOrEqual(@SqlType(StandardTypes.TIME) long left, @SqlType(StandardTypes.TIME) long right) + { + return left >= right; + } + + @ScalarOperator(BETWEEN) + @SqlType(StandardTypes.BOOLEAN) + public static boolean between(@SqlType(StandardTypes.TIME) long value, @SqlType(StandardTypes.TIME) long min, @SqlType(StandardTypes.TIME) long max) + { + return min <= value && value <= max; + } + + @ScalarOperator(CAST) + @SqlType(StandardTypes.TIME_WITH_TIME_ZONE) + public static long castToTimeWithTimeZone(ConnectorSession session, @SqlType(StandardTypes.TIME) long value) + { + return packDateTimeWithZone(value, session.getTimeZoneKey()); + } + + @ScalarOperator(CAST) + @SqlType(StandardTypes.TIMESTAMP) + public static long castToTimestamp(@SqlType(StandardTypes.TIME) long value) + { + return value; + } + + @ScalarOperator(CAST) + @SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) + public static long castToTimestampWithTimeZone(ConnectorSession session, @SqlType(StandardTypes.TIME) long value) + { + return packDateTimeWithZone(value, session.getTimeZoneKey()); + } + + @ScalarOperator(CAST) + @SqlType(StandardTypes.VARCHAR) + public static Slice castToSlice(ConnectorSession session, @SqlType(StandardTypes.TIME) long value) + { + return Slices.copiedBuffer(printTimeWithoutTimeZone(session.getTimeZoneKey(), value), UTF_8); + } + + @ScalarOperator(CAST) + @SqlType(StandardTypes.TIME) + public static long castFromSlice(ConnectorSession session, @SqlType(StandardTypes.VARCHAR) Slice value) + { + try { + return parseTimeWithoutTimeZone(session.getTimeZoneKey(), value.toStringUtf8()); + } + catch (IllegalArgumentException e) { + throw new PrestoException(INVALID_CAST_ARGUMENT, e); + } + } + + @ScalarOperator(HASH_CODE) + @SqlType(StandardTypes.BIGINT) + public static long hashCode(@SqlType(StandardTypes.TIME) long value) + { + return (int) (value ^ (value >>> 32)); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/type/TimeWithTimeZoneOperators.java b/presto-main/src/main/java/com/facebook/presto/type/TimeWithTimeZoneOperators.java new file mode 100644 index 00000000..facb44a1 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/type/TimeWithTimeZoneOperators.java @@ -0,0 +1,125 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.type; + +import com.facebook.presto.operator.scalar.ScalarOperator; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.type.StandardTypes; +import io.airlift.slice.Slice; +import io.airlift.slice.Slices; + +import static com.facebook.presto.metadata.OperatorType.BETWEEN; +import static com.facebook.presto.metadata.OperatorType.CAST; +import static com.facebook.presto.metadata.OperatorType.EQUAL; +import static com.facebook.presto.metadata.OperatorType.GREATER_THAN; +import static com.facebook.presto.metadata.OperatorType.GREATER_THAN_OR_EQUAL; +import static com.facebook.presto.metadata.OperatorType.HASH_CODE; +import static com.facebook.presto.metadata.OperatorType.LESS_THAN; +import static com.facebook.presto.metadata.OperatorType.LESS_THAN_OR_EQUAL; +import static com.facebook.presto.metadata.OperatorType.NOT_EQUAL; +import static com.facebook.presto.spi.type.DateTimeEncoding.unpackMillisUtc; +import static com.facebook.presto.util.DateTimeUtils.printTimeWithTimeZone; +import static java.nio.charset.StandardCharsets.UTF_8; + +public final class TimeWithTimeZoneOperators +{ + private TimeWithTimeZoneOperators() + { + } + + @ScalarOperator(EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean equal(@SqlType(StandardTypes.TIME_WITH_TIME_ZONE) long left, @SqlType(StandardTypes.TIME_WITH_TIME_ZONE) long right) + { + return unpackMillisUtc(left) == unpackMillisUtc(right); + } + + @ScalarOperator(NOT_EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean notEqual(@SqlType(StandardTypes.TIME_WITH_TIME_ZONE) long left, @SqlType(StandardTypes.TIME_WITH_TIME_ZONE) long right) + { + return unpackMillisUtc(left) != unpackMillisUtc(right); + } + + @ScalarOperator(LESS_THAN) + @SqlType(StandardTypes.BOOLEAN) + public static boolean lessThan(@SqlType(StandardTypes.TIME_WITH_TIME_ZONE) long left, @SqlType(StandardTypes.TIME_WITH_TIME_ZONE) long right) + { + return unpackMillisUtc(left) < unpackMillisUtc(right); + } + + @ScalarOperator(LESS_THAN_OR_EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean lessThanOrEqual(@SqlType(StandardTypes.TIME_WITH_TIME_ZONE) long left, @SqlType(StandardTypes.TIME_WITH_TIME_ZONE) long right) + { + return unpackMillisUtc(left) <= unpackMillisUtc(right); + } + + @ScalarOperator(GREATER_THAN) + @SqlType(StandardTypes.BOOLEAN) + public static boolean greaterThan(@SqlType(StandardTypes.TIME_WITH_TIME_ZONE) long left, @SqlType(StandardTypes.TIME_WITH_TIME_ZONE) long right) + { + return unpackMillisUtc(left) > unpackMillisUtc(right); + } + + @ScalarOperator(GREATER_THAN_OR_EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean greaterThanOrEqual(@SqlType(StandardTypes.TIME_WITH_TIME_ZONE) long left, @SqlType(StandardTypes.TIME_WITH_TIME_ZONE) long right) + { + return unpackMillisUtc(left) >= unpackMillisUtc(right); + } + + @ScalarOperator(BETWEEN) + @SqlType(StandardTypes.BOOLEAN) + public static boolean between(@SqlType(StandardTypes.TIME_WITH_TIME_ZONE) long value, @SqlType(StandardTypes.TIME_WITH_TIME_ZONE) long min, @SqlType(StandardTypes.TIME_WITH_TIME_ZONE) long max) + { + return unpackMillisUtc(min) <= unpackMillisUtc(value) && unpackMillisUtc(value) <= unpackMillisUtc(max); + } + + @ScalarOperator(CAST) + @SqlType(StandardTypes.TIME) + public static long castToTime(ConnectorSession session, @SqlType(StandardTypes.TIME_WITH_TIME_ZONE) long value) + { + return unpackMillisUtc(value); + } + + @ScalarOperator(CAST) + @SqlType(StandardTypes.TIMESTAMP) + public static long castToTimestamp(@SqlType(StandardTypes.TIME_WITH_TIME_ZONE) long value) + { + return unpackMillisUtc(value); + } + + @ScalarOperator(CAST) + @SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) + public static long castToTimestampWithTimeZone(@SqlType(StandardTypes.TIME_WITH_TIME_ZONE) long value) + { + return value; + } + + @ScalarOperator(CAST) + @SqlType(StandardTypes.VARCHAR) + public static Slice castToSlice(@SqlType(StandardTypes.TIME_WITH_TIME_ZONE) long value) + { + return Slices.copiedBuffer(printTimeWithTimeZone(value), UTF_8); + } + + @ScalarOperator(HASH_CODE) + @SqlType(StandardTypes.BIGINT) + public static long hashCode(@SqlType(StandardTypes.TIME_WITH_TIME_ZONE) long value) + { + long millis = unpackMillisUtc(value); + return (int) (millis ^ (millis >>> 32)); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/type/TimestampOperators.java b/presto-main/src/main/java/com/facebook/presto/type/TimestampOperators.java new file mode 100644 index 00000000..c36b1a38 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/type/TimestampOperators.java @@ -0,0 +1,158 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.type; + +import com.facebook.presto.operator.scalar.ScalarOperator; +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.type.StandardTypes; +import io.airlift.slice.Slice; +import io.airlift.slice.Slices; +import org.joda.time.chrono.ISOChronology; + +import java.util.concurrent.TimeUnit; + +import static com.facebook.presto.metadata.OperatorType.BETWEEN; +import static com.facebook.presto.metadata.OperatorType.CAST; +import static com.facebook.presto.metadata.OperatorType.EQUAL; +import static com.facebook.presto.metadata.OperatorType.GREATER_THAN; +import static com.facebook.presto.metadata.OperatorType.GREATER_THAN_OR_EQUAL; +import static com.facebook.presto.metadata.OperatorType.HASH_CODE; +import static com.facebook.presto.metadata.OperatorType.LESS_THAN; +import static com.facebook.presto.metadata.OperatorType.LESS_THAN_OR_EQUAL; +import static com.facebook.presto.metadata.OperatorType.NOT_EQUAL; +import static com.facebook.presto.spi.StandardErrorCode.INVALID_CAST_ARGUMENT; +import static com.facebook.presto.spi.type.DateTimeEncoding.packDateTimeWithZone; +import static com.facebook.presto.type.DateTimeOperators.modulo24Hour; +import static com.facebook.presto.util.DateTimeUtils.parseTimestampWithoutTimeZone; +import static com.facebook.presto.util.DateTimeUtils.printTimestampWithoutTimeZone; +import static com.facebook.presto.util.DateTimeZoneIndex.getChronology; +import static java.nio.charset.StandardCharsets.UTF_8; + +public final class TimestampOperators +{ + private TimestampOperators() + { + } + + @ScalarOperator(EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean equal(@SqlType(StandardTypes.TIMESTAMP) long left, @SqlType(StandardTypes.TIMESTAMP) long right) + { + return left == right; + } + + @ScalarOperator(NOT_EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean notEqual(@SqlType(StandardTypes.TIMESTAMP) long left, @SqlType(StandardTypes.TIMESTAMP) long right) + { + return left != right; + } + + @ScalarOperator(LESS_THAN) + @SqlType(StandardTypes.BOOLEAN) + public static boolean lessThan(@SqlType(StandardTypes.TIMESTAMP) long left, @SqlType(StandardTypes.TIMESTAMP) long right) + { + return left < right; + } + + @ScalarOperator(LESS_THAN_OR_EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean lessThanOrEqual(@SqlType(StandardTypes.TIMESTAMP) long left, @SqlType(StandardTypes.TIMESTAMP) long right) + { + return left <= right; + } + + @ScalarOperator(GREATER_THAN) + @SqlType(StandardTypes.BOOLEAN) + public static boolean greaterThan(@SqlType(StandardTypes.TIMESTAMP) long left, @SqlType(StandardTypes.TIMESTAMP) long right) + { + return left > right; + } + + @ScalarOperator(GREATER_THAN_OR_EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean greaterThanOrEqual(@SqlType(StandardTypes.TIMESTAMP) long left, @SqlType(StandardTypes.TIMESTAMP) long right) + { + return left >= right; + } + + @ScalarOperator(BETWEEN) + @SqlType(StandardTypes.BOOLEAN) + public static boolean between(@SqlType(StandardTypes.TIMESTAMP) long value, @SqlType(StandardTypes.TIMESTAMP) long min, @SqlType(StandardTypes.TIMESTAMP) long max) + { + return min <= value && value <= max; + } + + @ScalarOperator(CAST) + @SqlType(StandardTypes.DATE) + public static long castToDate(ConnectorSession session, @SqlType(StandardTypes.TIMESTAMP) long value) + { + // round down the current timestamp to days + ISOChronology chronology = getChronology(session.getTimeZoneKey()); + long date = chronology.dayOfYear().roundFloor(value); + // date is currently midnight in timezone of the session + // convert to UTC + long millis = date + chronology.getZone().getOffset(date); + return TimeUnit.MILLISECONDS.toDays(millis); + } + + @ScalarOperator(CAST) + @SqlType(StandardTypes.TIME) + public static long castToTime(ConnectorSession session, @SqlType(StandardTypes.TIMESTAMP) long value) + { + return modulo24Hour(getChronology(session.getTimeZoneKey()), value); + } + + @ScalarOperator(CAST) + @SqlType(StandardTypes.TIME_WITH_TIME_ZONE) + public static long castToTimeWithTimeZone(ConnectorSession session, @SqlType(StandardTypes.TIMESTAMP) long value) + { + int timeMillis = modulo24Hour(getChronology(session.getTimeZoneKey()), value); + return packDateTimeWithZone(timeMillis, session.getTimeZoneKey()); + } + + @ScalarOperator(CAST) + @SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) + public static long castToTimestampWithTimeZone(ConnectorSession session, @SqlType(StandardTypes.TIMESTAMP) long value) + { + return packDateTimeWithZone(value, session.getTimeZoneKey()); + } + + @ScalarOperator(CAST) + @SqlType(StandardTypes.VARCHAR) + public static Slice castToSlice(ConnectorSession session, @SqlType(StandardTypes.TIMESTAMP) long value) + { + return Slices.copiedBuffer(printTimestampWithoutTimeZone(session.getTimeZoneKey(), value), UTF_8); + } + + @ScalarOperator(CAST) + @SqlType(StandardTypes.TIMESTAMP) + public static long castFromSlice(ConnectorSession session, @SqlType(StandardTypes.VARCHAR) Slice value) + { + try { + return parseTimestampWithoutTimeZone(session.getTimeZoneKey(), value.toStringUtf8()); + } + catch (IllegalArgumentException e) { + throw new PrestoException(INVALID_CAST_ARGUMENT, e); + } + } + + @ScalarOperator(HASH_CODE) + @SqlType(StandardTypes.BIGINT) + public static long hashCode(@SqlType(StandardTypes.TIMESTAMP) long value) + { + return (int) (value ^ (value >>> 32)); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/type/TimestampWithTimeZoneOperators.java b/presto-main/src/main/java/com/facebook/presto/type/TimestampWithTimeZoneOperators.java new file mode 100644 index 00000000..8383d274 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/type/TimestampWithTimeZoneOperators.java @@ -0,0 +1,148 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.type; + +import com.facebook.presto.operator.scalar.ScalarOperator; +import com.facebook.presto.spi.type.StandardTypes; +import io.airlift.slice.Slice; +import io.airlift.slice.Slices; +import org.joda.time.chrono.ISOChronology; + +import java.util.concurrent.TimeUnit; + +import static com.facebook.presto.metadata.OperatorType.BETWEEN; +import static com.facebook.presto.metadata.OperatorType.CAST; +import static com.facebook.presto.metadata.OperatorType.EQUAL; +import static com.facebook.presto.metadata.OperatorType.GREATER_THAN; +import static com.facebook.presto.metadata.OperatorType.GREATER_THAN_OR_EQUAL; +import static com.facebook.presto.metadata.OperatorType.HASH_CODE; +import static com.facebook.presto.metadata.OperatorType.LESS_THAN; +import static com.facebook.presto.metadata.OperatorType.LESS_THAN_OR_EQUAL; +import static com.facebook.presto.metadata.OperatorType.NOT_EQUAL; +import static com.facebook.presto.spi.type.DateTimeEncoding.packDateTimeWithZone; +import static com.facebook.presto.spi.type.DateTimeEncoding.unpackMillisUtc; +import static com.facebook.presto.spi.type.DateTimeEncoding.unpackZoneKey; +import static com.facebook.presto.type.DateTimeOperators.modulo24Hour; +import static com.facebook.presto.util.DateTimeUtils.printTimestampWithTimeZone; +import static com.facebook.presto.util.DateTimeZoneIndex.unpackChronology; +import static java.nio.charset.StandardCharsets.UTF_8; + +public final class TimestampWithTimeZoneOperators +{ + private TimestampWithTimeZoneOperators() + { + } + + @ScalarOperator(EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean equal(@SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long left, @SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long right) + { + return unpackMillisUtc(left) == unpackMillisUtc(right); + } + + @ScalarOperator(NOT_EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean notEqual(@SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long left, @SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long right) + { + return unpackMillisUtc(left) != unpackMillisUtc(right); + } + + @ScalarOperator(LESS_THAN) + @SqlType(StandardTypes.BOOLEAN) + public static boolean lessThan(@SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long left, @SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long right) + { + return unpackMillisUtc(left) < unpackMillisUtc(right); + } + + @ScalarOperator(LESS_THAN_OR_EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean lessThanOrEqual(@SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long left, @SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long right) + { + return unpackMillisUtc(left) <= unpackMillisUtc(right); + } + + @ScalarOperator(GREATER_THAN) + @SqlType(StandardTypes.BOOLEAN) + public static boolean greaterThan(@SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long left, @SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long right) + { + return unpackMillisUtc(left) > unpackMillisUtc(right); + } + + @ScalarOperator(GREATER_THAN_OR_EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean greaterThanOrEqual(@SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long left, @SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long right) + { + return unpackMillisUtc(left) >= unpackMillisUtc(right); + } + + @ScalarOperator(BETWEEN) + @SqlType(StandardTypes.BOOLEAN) + public static boolean between( + @SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long value, + @SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long min, + @SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long max) + { + return unpackMillisUtc(min) <= unpackMillisUtc(value) && unpackMillisUtc(value) <= unpackMillisUtc(max); + } + + @ScalarOperator(CAST) + @SqlType(StandardTypes.DATE) + public static long castToDate(@SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long value) + { + // round down the current timestamp to days + ISOChronology chronology = unpackChronology(value); + long date = chronology.dayOfYear().roundFloor(unpackMillisUtc(value)); + // date is currently midnight in timezone of the original value + // convert to UTC + long millis = date + chronology.getZone().getOffset(date); + return TimeUnit.MILLISECONDS.toDays(millis); + } + + @ScalarOperator(CAST) + @SqlType(StandardTypes.TIME) + public static long castToTime(@SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long value) + { + return modulo24Hour(unpackChronology(value), unpackMillisUtc(value)); + } + + @ScalarOperator(CAST) + @SqlType(StandardTypes.TIME_WITH_TIME_ZONE) + public static long castToTimeWithTimeZone(@SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long value) + { + int millis = modulo24Hour(unpackChronology(value), unpackMillisUtc(value)); + return packDateTimeWithZone(millis, unpackZoneKey(value)); + } + + @ScalarOperator(CAST) + @SqlType(StandardTypes.TIMESTAMP) + public static long castToTimestamp(@SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long value) + { + return unpackMillisUtc(value); + } + + @ScalarOperator(CAST) + @SqlType(StandardTypes.VARCHAR) + public static Slice castToSlice(@SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long value) + { + return Slices.copiedBuffer(printTimestampWithTimeZone(value), UTF_8); + } + + @ScalarOperator(HASH_CODE) + @SqlType(StandardTypes.BIGINT) + public static long hashCode(@SqlType(StandardTypes.TIMESTAMP_WITH_TIME_ZONE) long value) + { + long millis = unpackMillisUtc(value); + return (int) (millis ^ (millis >>> 32)); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/type/TypeDeserializer.java b/presto-main/src/main/java/com/facebook/presto/type/TypeDeserializer.java new file mode 100644 index 00000000..554890e5 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/type/TypeDeserializer.java @@ -0,0 +1,46 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.type; + +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.deser.std.FromStringDeserializer; + +import javax.inject.Inject; + +import static com.facebook.presto.spi.type.TypeSignature.parseTypeSignature; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public final class TypeDeserializer + extends FromStringDeserializer +{ + private final TypeManager typeManager; + + @Inject + public TypeDeserializer(TypeManager typeManager) + { + super(Type.class); + this.typeManager = checkNotNull(typeManager, "typeManager is null"); + } + + @Override + protected Type _deserialize(String value, DeserializationContext context) + { + Type type = typeManager.getType(parseTypeSignature(value)); + checkArgument(type != null, "Unknown type %s", value); + return type; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/type/TypeJsonUtils.java b/presto-main/src/main/java/com/facebook/presto/type/TypeJsonUtils.java new file mode 100644 index 00000000..1f20c0c1 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/type/TypeJsonUtils.java @@ -0,0 +1,190 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.type; + +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.block.BlockBuilderStatus; +import com.facebook.presto.spi.type.FixedWidthType; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.google.common.base.Throwables; +import io.airlift.slice.Slice; +import io.airlift.slice.Slices; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static com.fasterxml.jackson.core.JsonFactory.Feature.CANONICALIZE_FIELD_NAMES; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +public final class TypeJsonUtils +{ + private static final JsonFactory JSON_FACTORY = new JsonFactory().disable(CANONICALIZE_FIELD_NAMES); + + private TypeJsonUtils() {} + + public static Object stackRepresentationToObject(ConnectorSession session, Slice value, Type type) + { + if (value == null) { + return null; + } + + try (JsonParser jsonParser = JSON_FACTORY.createJsonParser(value.getInput())) { + jsonParser.nextToken(); + return stackRepresentationToObjectHelper(session, jsonParser, type); + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } + + private static Object stackRepresentationToObjectHelper(ConnectorSession session, JsonParser parser, Type type) + throws IOException + { + if (parser.getCurrentToken() == JsonToken.VALUE_NULL) { + return null; + } + + if (type instanceof ArrayType) { + List list = new ArrayList<>(); + checkState(parser.getCurrentToken() == JsonToken.START_ARRAY, "Expected a json array"); + while (parser.nextToken() != JsonToken.END_ARRAY) { + list.add(stackRepresentationToObjectHelper(session, parser, ((ArrayType) type).getElementType())); + } + + return Collections.unmodifiableList(list); + } + + if (type instanceof MapType) { + Map map = new LinkedHashMap<>(); + checkState(parser.getCurrentToken() == JsonToken.START_OBJECT, "Expected a json object"); + while (parser.nextValue() != JsonToken.END_OBJECT) { + Object key = mapKeyToObject(session, parser.getCurrentName(), ((MapType) type).getKeyType()); + Object value = stackRepresentationToObjectHelper(session, parser, ((MapType) type).getValueType()); + map.put(key, value); + } + + return Collections.unmodifiableMap(map); + } + + if (type instanceof RowType) { + List list = new ArrayList<>(); + checkState(parser.getCurrentToken() == JsonToken.START_ARRAY, "Expected a json array"); + int field = 0; + RowType rowType = (RowType) type; + while (parser.nextValue() != JsonToken.END_ARRAY) { + checkArgument(field < rowType.getFields().size(), "Unexpected field for type %s", type); + Object value = stackRepresentationToObjectHelper(session, parser, rowType.getFields().get(field).getType()); + list.add(value); + field++; + } + checkArgument(field == rowType.getFields().size(), "Expected %s fields for type %s", rowType.getFields().size(), type); + + return Collections.unmodifiableList(list); + } + + Slice sliceValue = null; + if (type.getJavaType() == Slice.class) { + sliceValue = Slices.utf8Slice(parser.getValueAsString()); + } + + BlockBuilder blockBuilder; + if (type instanceof FixedWidthType) { + blockBuilder = type.createBlockBuilder(new BlockBuilderStatus(), 1); + } + else { + blockBuilder = type.createBlockBuilder(new BlockBuilderStatus(), 1, checkNotNull(sliceValue, "sliceValue is null").length()); + } + + if (type.getJavaType() == boolean.class) { + type.writeBoolean(blockBuilder, parser.getBooleanValue()); + } + else if (type.getJavaType() == long.class) { + type.writeLong(blockBuilder, parser.getLongValue()); + } + else if (type.getJavaType() == double.class) { + type.writeDouble(blockBuilder, getDoubleValue(parser)); + } + else if (type.getJavaType() == Slice.class) { + type.writeSlice(blockBuilder, checkNotNull(sliceValue, "sliceValue is null")); + } + return type.getObjectValue(session, blockBuilder.build(), 0); + } + + private static Object mapKeyToObject(ConnectorSession session, String jsonKey, Type type) + { + BlockBuilder blockBuilder; + if (type instanceof FixedWidthType) { + blockBuilder = type.createBlockBuilder(new BlockBuilderStatus(), 1); + } + else { + blockBuilder = type.createBlockBuilder(new BlockBuilderStatus(), 1, jsonKey.length()); + } + if (type.getJavaType() == boolean.class) { + type.writeBoolean(blockBuilder, Boolean.parseBoolean(jsonKey)); + } + else if (type.getJavaType() == long.class) { + type.writeLong(blockBuilder, Long.parseLong(jsonKey)); + } + else if (type.getJavaType() == double.class) { + type.writeDouble(blockBuilder, Double.parseDouble(jsonKey)); + } + else if (type.getJavaType() == Slice.class) { + type.writeSlice(blockBuilder, Slices.utf8Slice(jsonKey)); + } + return type.getObjectValue(session, blockBuilder.build(), 0); + } + + private static double getDoubleValue(JsonParser parser) throws IOException + { + double value; + try { + value = parser.getDoubleValue(); + } + catch (JsonParseException e) { + //handle non-numeric numbers (inf/nan) + value = Double.parseDouble(parser.getValueAsString()); + } + return value; + } + + public static boolean canCastFromJson(Type type) + { + String baseType = type.getTypeSignature().getBase(); + if (baseType.equals(StandardTypes.BOOLEAN) || + baseType.equals(StandardTypes.BIGINT) || + baseType.equals(StandardTypes.DOUBLE) || + baseType.equals(StandardTypes.VARCHAR)) { + return true; + } + if (type instanceof ArrayType) { + return canCastFromJson(((ArrayType) type).getElementType()); + } + if (type instanceof MapType) { + return canCastFromJson(((MapType) type).getKeyType()) && canCastFromJson(((MapType) type).getValueType()); + } + return false; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/type/TypeRegistry.java b/presto-main/src/main/java/com/facebook/presto/type/TypeRegistry.java new file mode 100644 index 00000000..9bed2550 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/type/TypeRegistry.java @@ -0,0 +1,165 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.type; + +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.spi.type.TypeSignature; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; + +import javax.annotation.concurrent.ThreadSafe; +import javax.inject.Inject; + +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.spi.type.BooleanType.BOOLEAN; +import static com.facebook.presto.spi.type.DateType.DATE; +import static com.facebook.presto.spi.type.DoubleType.DOUBLE; +import static com.facebook.presto.spi.type.HyperLogLogType.HYPER_LOG_LOG; +import static com.facebook.presto.spi.type.IntervalDayTimeType.INTERVAL_DAY_TIME; +import static com.facebook.presto.spi.type.IntervalYearMonthType.INTERVAL_YEAR_MONTH; +import static com.facebook.presto.spi.type.TimeType.TIME; +import static com.facebook.presto.spi.type.TimeWithTimeZoneType.TIME_WITH_TIME_ZONE; +import static com.facebook.presto.spi.type.TimestampType.TIMESTAMP; +import static com.facebook.presto.spi.type.TimestampWithTimeZoneType.TIMESTAMP_WITH_TIME_ZONE; +import static com.facebook.presto.spi.type.VarbinaryType.VARBINARY; +import static com.facebook.presto.spi.type.VarcharType.VARCHAR; +import static com.facebook.presto.type.ArrayParametricType.ARRAY; +import static com.facebook.presto.type.ColorType.COLOR; +import static com.facebook.presto.type.JsonPathType.JSON_PATH; +import static com.facebook.presto.type.JsonType.JSON; +import static com.facebook.presto.type.LikePatternType.LIKE_PATTERN; +import static com.facebook.presto.type.MapParametricType.MAP; +import static com.facebook.presto.type.RegexpType.REGEXP; +import static com.facebook.presto.type.RowParametricType.ROW; +import static com.facebook.presto.type.UnknownType.UNKNOWN; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +@ThreadSafe +public final class TypeRegistry + implements TypeManager +{ + private final ConcurrentMap types = new ConcurrentHashMap<>(); + private final ConcurrentMap parametricTypes = new ConcurrentHashMap<>(); + + public TypeRegistry() + { + this(ImmutableSet.of()); + } + + @Inject + public TypeRegistry(Set types) + { + checkNotNull(types, "types is null"); + + // Manually register UNKNOWN type without a verifyTypeClass call since it is a special type that can not be used by functions + this.types.put(UNKNOWN.getTypeSignature(), UNKNOWN); + + // always add the built-in types; Presto will not function without these + addType(BOOLEAN); + addType(BIGINT); + addType(DOUBLE); + addType(VARCHAR); + addType(VARBINARY); + addType(DATE); + addType(TIME); + addType(TIME_WITH_TIME_ZONE); + addType(TIMESTAMP); + addType(TIMESTAMP_WITH_TIME_ZONE); + addType(INTERVAL_YEAR_MONTH); + addType(INTERVAL_DAY_TIME); + addType(HYPER_LOG_LOG); + addType(REGEXP); + addType(LIKE_PATTERN); + addType(JSON_PATH); + addType(COLOR); + addType(JSON); + addParametricType(ROW); + addParametricType(ARRAY); + addParametricType(MAP); + + for (Type type : types) { + addType(type); + } + } + + @Override + public Type getType(TypeSignature signature) + { + Type type = types.get(signature); + if (type == null) { + return instantiateParametricType(signature); + } + return type; + } + + @Override + public Type getParameterizedType(String baseTypeName, List typeParameters, List literalParameters) + { + return getType(new TypeSignature(baseTypeName, typeParameters, literalParameters)); + } + + private Type instantiateParametricType(TypeSignature signature) + { + ImmutableList.Builder parameterTypes = ImmutableList.builder(); + for (TypeSignature parameter : signature.getParameters()) { + Type parameterType = getType(parameter); + if (parameterType == null) { + return null; + } + parameterTypes.add(parameterType); + } + + ParametricType parametricType = parametricTypes.get(signature.getBase().toLowerCase(Locale.ENGLISH)); + if (parametricType == null) { + return null; + } + Type instantiatedType = parametricType.createType(parameterTypes.build(), signature.getLiteralParameters()); + checkState(instantiatedType.getTypeSignature().equals(signature), "Instantiated parametric type name (%s) does not match expected name (%s)", instantiatedType, signature); + return instantiatedType; + } + + @Override + public List getTypes() + { + return ImmutableList.copyOf(types.values()); + } + + public void addType(Type type) + { + verifyTypeClass(type); + Type existingType = types.putIfAbsent(type.getTypeSignature(), type); + checkState(existingType == null || existingType.equals(type), "Type %s is already registered", type); + } + + public void addParametricType(ParametricType parametricType) + { + String name = parametricType.getName().toLowerCase(Locale.ENGLISH); + checkArgument(!parametricTypes.containsKey(name), "Parametric type already registered: %s", name); + parametricTypes.putIfAbsent(name, parametricType); + } + + public static void verifyTypeClass(Type type) + { + checkNotNull(type, "type is null"); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/type/TypeUtils.java b/presto-main/src/main/java/com/facebook/presto/type/TypeUtils.java new file mode 100644 index 00000000..4c59fd31 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/type/TypeUtils.java @@ -0,0 +1,280 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.type; + +import com.facebook.presto.operator.HashGenerator; +import com.facebook.presto.operator.InterpretedHashGenerator; +import com.facebook.presto.spi.Page; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.block.BlockBuilderStatus; +import com.facebook.presto.spi.block.BlockEncoding; +import com.facebook.presto.spi.block.VariableWidthBlockBuilder; +import com.facebook.presto.spi.block.VariableWidthBlockEncoding; +import com.facebook.presto.spi.type.FixedWidthType; +import com.facebook.presto.spi.type.StandardTypes; +import com.facebook.presto.spi.type.Type; +import com.facebook.presto.spi.type.TypeManager; +import com.facebook.presto.spi.type.TypeSignature; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.DynamicSliceOutput; +import io.airlift.slice.Slice; +import io.airlift.slice.Slices; + +import java.lang.invoke.MethodHandle; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static com.facebook.presto.spi.StandardErrorCode.INVALID_FUNCTION_ARGUMENT; +import static com.facebook.presto.spi.StandardErrorCode.NOT_SUPPORTED; +import static com.facebook.presto.spi.type.BigintType.BIGINT; +import static com.facebook.presto.util.ImmutableCollectors.toImmutableList; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public final class TypeUtils +{ + public static final int EXPECTED_ARRAY_SIZE = 1024; + public static final int NULL_HASH_CODE = 0; + + private TypeUtils() + { + } + + public static int expectedValueSize(Type type, int defaultSize) + { + if (type instanceof FixedWidthType) { + return ((FixedWidthType) type).getFixedSize(); + } + return defaultSize; + } + + public static int hashPosition(Type type, Block block, int position) + { + if (block.isNull(position)) { + return NULL_HASH_CODE; + } + return type.hash(block, position); + } + + public static long hashPosition(MethodHandle methodHandle, Type type, Block block, int position) + { + if (block.isNull(position)) { + return NULL_HASH_CODE; + } + try { + if (type.getJavaType() == boolean.class) { + return (long) methodHandle.invoke(type.getBoolean(block, position)); + } + else if (type.getJavaType() == long.class) { + return (long) methodHandle.invoke(type.getLong(block, position)); + } + else if (type.getJavaType() == double.class) { + return (long) methodHandle.invoke(type.getDouble(block, position)); + } + else if (type.getJavaType() == Slice.class) { + return (long) methodHandle.invoke(type.getSlice(block, position)); + } + else { + throw new UnsupportedOperationException("Unsupported native container type: " + type.getJavaType() + " with type " + type.getTypeSignature()); + } + } + catch (Throwable throwable) { + throw Throwables.propagate(throwable); + } + } + + public static boolean positionEqualsPosition(Type type, Block leftBlock, int leftPosition, Block rightBlock, int rightPosition) + { + boolean leftIsNull = leftBlock.isNull(leftPosition); + boolean rightIsNull = rightBlock.isNull(rightPosition); + if (leftIsNull || rightIsNull) { + return leftIsNull && rightIsNull; + } + return type.equalTo(leftBlock, leftPosition, rightBlock, rightPosition); + } + + public static List resolveTypes(List typeNames, TypeManager typeManager) + { + return typeNames.stream() + .map((TypeSignature type) -> checkNotNull(typeManager.getType(type), "Type '%s' not found", type)) + .collect(toImmutableList()); + } + + public static TypeSignature parameterizedTypeName(String base, TypeSignature... argumentNames) + { + return new TypeSignature(base, ImmutableList.copyOf(argumentNames), ImmutableList.of()); + } + + public static int getHashPosition(List hashTypes, Block[] hashBlocks, int position) + { + int[] hashChannels = new int[hashBlocks.length]; + for (int i = 0; i < hashBlocks.length; i++) { + hashChannels[i] = i; + } + HashGenerator hashGenerator = new InterpretedHashGenerator(ImmutableList.copyOf(hashTypes), hashChannels); + Page page = new Page(hashBlocks); + return hashGenerator.hashPosition(position, page); + } + + public static Block getHashBlock(List hashTypes, Block... hashBlocks) + { + checkArgument(hashTypes.size() == hashBlocks.length); + int[] hashChannels = new int[hashBlocks.length]; + for (int i = 0; i < hashBlocks.length; i++) { + hashChannels[i] = i; + } + HashGenerator hashGenerator = new InterpretedHashGenerator(ImmutableList.copyOf(hashTypes), hashChannels); + int positionCount = hashBlocks[0].getPositionCount(); + BlockBuilder builder = BIGINT.createFixedSizeBlockBuilder(positionCount); + Page page = new Page(hashBlocks); + for (int i = 0; i < positionCount; i++) { + BIGINT.writeLong(builder, hashGenerator.hashPosition(i, page)); + } + return builder.build(); + } + + public static Page getHashPage(Page page, List types, List hashChannels) + { + Block[] blocks = Arrays.copyOf(page.getBlocks(), page.getChannelCount() + 1); + ImmutableList.Builder hashTypes = ImmutableList.builder(); + Block[] hashBlocks = new Block[hashChannels.size()]; + int hashBlockIndex = 0; + + for (int channel : hashChannels) { + hashTypes.add(types.get(channel)); + hashBlocks[hashBlockIndex++] = blocks[channel]; + } + blocks[page.getChannelCount()] = getHashBlock(hashTypes.build(), hashBlocks); + return new Page(blocks); + } + + public static Block createBlock(Type type, Object element) + { + BlockBuilder blockBuilder = type.createBlockBuilder(new BlockBuilderStatus(), 1, EXPECTED_ARRAY_SIZE); + appendToBlockBuilder(type, element, blockBuilder); + return blockBuilder.build(); + } + + public static void appendToBlockBuilder(Type type, Object element, BlockBuilder blockBuilder) + { + Class javaType = type.getJavaType(); + if (element == null) { + blockBuilder.appendNull(); + } + // TODO: This should be removed. Functions that rely on this functionality should + // be doing the conversion themselves. + else if (type.getTypeSignature().getBase().equals(StandardTypes.ARRAY) && element instanceof Iterable) { + BlockBuilder subBlockBuilder = new VariableWidthBlockBuilder(new BlockBuilderStatus(), EXPECTED_ARRAY_SIZE); + for (Object subElement : (Iterable) element) { + appendToBlockBuilder(type.getTypeParameters().get(0), subElement, subBlockBuilder); + } + type.writeSlice(blockBuilder, buildStructuralSlice(subBlockBuilder)); + } + else if (type.getTypeSignature().getBase().equals(StandardTypes.ROW) && element instanceof Iterable) { + BlockBuilder subBlockBuilder = new VariableWidthBlockBuilder(new BlockBuilderStatus(), EXPECTED_ARRAY_SIZE); + int field = 0; + for (Object subElement : (Iterable) element) { + appendToBlockBuilder(type.getTypeParameters().get(field), subElement, subBlockBuilder); + field++; + } + type.writeSlice(blockBuilder, buildStructuralSlice(subBlockBuilder)); + } + else if (type.getTypeSignature().getBase().equals(StandardTypes.MAP) && element instanceof Map) { + BlockBuilder subBlockBuilder = new VariableWidthBlockBuilder(new BlockBuilderStatus(), EXPECTED_ARRAY_SIZE); + for (Map.Entry entry : ((Map) element).entrySet()) { + appendToBlockBuilder(type.getTypeParameters().get(0), entry.getKey(), subBlockBuilder); + appendToBlockBuilder(type.getTypeParameters().get(1), entry.getValue(), subBlockBuilder); + } + type.writeSlice(blockBuilder, buildStructuralSlice(subBlockBuilder)); + } + + else if (javaType == boolean.class) { + type.writeBoolean(blockBuilder, (Boolean) element); + } + else if (javaType == long.class) { + type.writeLong(blockBuilder, ((Number) element).longValue()); + } + else if (javaType == double.class) { + type.writeDouble(blockBuilder, ((Number) element).doubleValue()); + } + else if (javaType == Slice.class) { + if (element instanceof String) { + type.writeSlice(blockBuilder, Slices.utf8Slice(element.toString())); + } + else if (element instanceof byte[]) { + type.writeSlice(blockBuilder, Slices.wrappedBuffer((byte[]) element)); + } + else { + type.writeSlice(blockBuilder, (Slice) element); + } + } + else { + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, String.format("Unexpected type %s", javaType.getName())); + } + } + + public static Object castValue(Type type, Block block, int position) + { + Class javaType = type.getJavaType(); + + if (block.isNull(position)) { + return null; + } + else if (javaType == boolean.class) { + return type.getBoolean(block, position); + } + else if (javaType == long.class) { + return type.getLong(block, position); + } + else if (javaType == double.class) { + return type.getDouble(block, position); + } + else if (type.getJavaType() == Slice.class) { + return type.getSlice(block, position); + } + else { + throw new PrestoException(INVALID_FUNCTION_ARGUMENT, String.format("Unexpected type %s", javaType.getName())); + } + } + + public static Slice buildStructuralSlice(BlockBuilder builder) + { + return buildStructuralSlice(builder.build()); + } + + public static Slice buildStructuralSlice(Block block) + { + BlockEncoding encoding = block.getEncoding(); + DynamicSliceOutput output = new DynamicSliceOutput(encoding.getEstimatedSize(block)); + encoding.writeBlock(output, block); + + return output.slice(); + } + + public static Block readStructuralBlock(Slice slice) + { + return new VariableWidthBlockEncoding().readBlock(slice.getInput()); + } + + public static void checkElementNotNull(boolean isNull, String errorMsg) + { + if (isNull) { + throw new PrestoException(NOT_SUPPORTED, errorMsg); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/type/UnknownType.java b/presto-main/src/main/java/com/facebook/presto/type/UnknownType.java new file mode 100644 index 00000000..9796090a --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/type/UnknownType.java @@ -0,0 +1,86 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.type; + +import com.facebook.presto.spi.ConnectorSession; +import com.facebook.presto.spi.block.Block; +import com.facebook.presto.spi.block.BlockBuilder; +import com.facebook.presto.spi.type.AbstractFixedWidthType; + +import static com.facebook.presto.type.TypeUtils.parameterizedTypeName; +import static com.google.common.base.Preconditions.checkArgument; + +public final class UnknownType + extends AbstractFixedWidthType +{ + public static final UnknownType UNKNOWN = new UnknownType(); + public static final String NAME = "unknown"; + + private UnknownType() + { + super(parameterizedTypeName(NAME), void.class, 0); + } + + @Override + public boolean isComparable() + { + return true; + } + + @Override + public boolean isOrderable() + { + return true; + } + + @Override + public int hash(Block block, int position) + { + // Check that the position is valid + checkArgument(block.isNull(position), "Expected NULL value for UnknownType"); + return 0; + } + + @Override + public boolean equalTo(Block leftBlock, int leftPosition, Block rightBlock, int rightPosition) + { + // Check that the position is valid + checkArgument(leftBlock.isNull(leftPosition), "Expected NULL value for UnknownType"); + checkArgument(rightBlock.isNull(rightPosition), "Expected NULL value for UnknownType"); + return true; + } + + @Override + public int compareTo(Block leftBlock, int leftPosition, Block rightBlock, int rightPosition) + { + // Check that the position is valid + checkArgument(leftBlock.isNull(leftPosition), "Expected NULL value for UnknownType"); + checkArgument(rightBlock.isNull(rightPosition), "Expected NULL value for UnknownType"); + return 0; + } + + @Override + public Object getObjectValue(ConnectorSession session, Block block, int position) + { + // call is null in case position is out of bounds + checkArgument(block.isNull(position), "Expected NULL value for UnknownType"); + return null; + } + + @Override + public void appendTo(Block block, int position, BlockBuilder blockBuilder) + { + blockBuilder.appendNull(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/type/VarbinaryOperators.java b/presto-main/src/main/java/com/facebook/presto/type/VarbinaryOperators.java new file mode 100644 index 00000000..fbe06052 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/type/VarbinaryOperators.java @@ -0,0 +1,90 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.type; + +import com.facebook.presto.operator.scalar.ScalarOperator; +import com.facebook.presto.spi.type.StandardTypes; +import io.airlift.slice.Slice; + +import static com.facebook.presto.metadata.OperatorType.BETWEEN; +import static com.facebook.presto.metadata.OperatorType.EQUAL; +import static com.facebook.presto.metadata.OperatorType.GREATER_THAN; +import static com.facebook.presto.metadata.OperatorType.GREATER_THAN_OR_EQUAL; +import static com.facebook.presto.metadata.OperatorType.HASH_CODE; +import static com.facebook.presto.metadata.OperatorType.LESS_THAN; +import static com.facebook.presto.metadata.OperatorType.LESS_THAN_OR_EQUAL; +import static com.facebook.presto.metadata.OperatorType.NOT_EQUAL; + +public final class VarbinaryOperators +{ + private VarbinaryOperators() + { + } + + @ScalarOperator(EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean equal(@SqlType(StandardTypes.VARBINARY) Slice left, @SqlType(StandardTypes.VARBINARY) Slice right) + { + return left.equals(right); + } + + @ScalarOperator(NOT_EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean notEqual(@SqlType(StandardTypes.VARBINARY) Slice left, @SqlType(StandardTypes.VARBINARY) Slice right) + { + return !left.equals(right); + } + + @ScalarOperator(LESS_THAN) + @SqlType(StandardTypes.BOOLEAN) + public static boolean lessThan(@SqlType(StandardTypes.VARBINARY) Slice left, @SqlType(StandardTypes.VARBINARY) Slice right) + { + return left.compareTo(right) < 0; + } + + @ScalarOperator(LESS_THAN_OR_EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean lessThanOrEqual(@SqlType(StandardTypes.VARBINARY) Slice left, @SqlType(StandardTypes.VARBINARY) Slice right) + { + return left.compareTo(right) <= 0; + } + + @ScalarOperator(GREATER_THAN) + @SqlType(StandardTypes.BOOLEAN) + public static boolean greaterThan(@SqlType(StandardTypes.VARBINARY) Slice left, @SqlType(StandardTypes.VARBINARY) Slice right) + { + return left.compareTo(right) > 0; + } + + @ScalarOperator(GREATER_THAN_OR_EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean greaterThanOrEqual(@SqlType(StandardTypes.VARBINARY) Slice left, @SqlType(StandardTypes.VARBINARY) Slice right) + { + return left.compareTo(right) >= 0; + } + + @ScalarOperator(BETWEEN) + @SqlType(StandardTypes.BOOLEAN) + public static boolean between(@SqlType(StandardTypes.VARBINARY) Slice value, @SqlType(StandardTypes.VARBINARY) Slice min, @SqlType(StandardTypes.VARBINARY) Slice max) + { + return min.compareTo(value) <= 0 && value.compareTo(max) <= 0; + } + + @ScalarOperator(HASH_CODE) + @SqlType(StandardTypes.BIGINT) + public static long hashCode(@SqlType(StandardTypes.VARBINARY) Slice value) + { + return value.hashCode(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/type/VarcharOperators.java b/presto-main/src/main/java/com/facebook/presto/type/VarcharOperators.java new file mode 100644 index 00000000..7d441d91 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/type/VarcharOperators.java @@ -0,0 +1,192 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.type; + +import com.facebook.presto.metadata.OperatorType; +import com.facebook.presto.operator.scalar.ScalarOperator; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.spi.type.StandardTypes; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.airlift.json.ObjectMapperProvider; +import io.airlift.slice.DynamicSliceOutput; +import io.airlift.slice.Slice; +import io.airlift.slice.SliceOutput; + +import java.io.IOException; + +import static com.facebook.presto.metadata.OperatorType.BETWEEN; +import static com.facebook.presto.metadata.OperatorType.CAST; +import static com.facebook.presto.metadata.OperatorType.EQUAL; +import static com.facebook.presto.metadata.OperatorType.GREATER_THAN; +import static com.facebook.presto.metadata.OperatorType.GREATER_THAN_OR_EQUAL; +import static com.facebook.presto.metadata.OperatorType.HASH_CODE; +import static com.facebook.presto.metadata.OperatorType.LESS_THAN; +import static com.facebook.presto.metadata.OperatorType.LESS_THAN_OR_EQUAL; +import static com.facebook.presto.metadata.OperatorType.NOT_EQUAL; +import static com.facebook.presto.spi.StandardErrorCode.INVALID_CAST_ARGUMENT; +import static com.fasterxml.jackson.databind.SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS; +import static java.lang.String.format; +import static java.nio.charset.StandardCharsets.UTF_8; + +public final class VarcharOperators +{ + private static final ObjectMapper SORTED_MAPPER = new ObjectMapperProvider().get().configure(ORDER_MAP_ENTRIES_BY_KEYS, true); + + private VarcharOperators() + { + } + + @ScalarOperator(EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean equal(@SqlType(StandardTypes.VARCHAR) Slice left, @SqlType(StandardTypes.VARCHAR) Slice right) + { + return left.equals(right); + } + + @ScalarOperator(NOT_EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean notEqual(@SqlType(StandardTypes.VARCHAR) Slice left, @SqlType(StandardTypes.VARCHAR) Slice right) + { + return !left.equals(right); + } + + @ScalarOperator(LESS_THAN) + @SqlType(StandardTypes.BOOLEAN) + public static boolean lessThan(@SqlType(StandardTypes.VARCHAR) Slice left, @SqlType(StandardTypes.VARCHAR) Slice right) + { + return left.compareTo(right) < 0; + } + + @ScalarOperator(LESS_THAN_OR_EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean lessThanOrEqual(@SqlType(StandardTypes.VARCHAR) Slice left, @SqlType(StandardTypes.VARCHAR) Slice right) + { + return left.compareTo(right) <= 0; + } + + @ScalarOperator(GREATER_THAN) + @SqlType(StandardTypes.BOOLEAN) + public static boolean greaterThan(@SqlType(StandardTypes.VARCHAR) Slice left, @SqlType(StandardTypes.VARCHAR) Slice right) + { + return left.compareTo(right) > 0; + } + + @ScalarOperator(GREATER_THAN_OR_EQUAL) + @SqlType(StandardTypes.BOOLEAN) + public static boolean greaterThanOrEqual(@SqlType(StandardTypes.VARCHAR) Slice left, @SqlType(StandardTypes.VARCHAR) Slice right) + { + return left.compareTo(right) >= 0; + } + + @ScalarOperator(BETWEEN) + @SqlType(StandardTypes.BOOLEAN) + public static boolean between(@SqlType(StandardTypes.VARCHAR) Slice value, @SqlType(StandardTypes.VARCHAR) Slice min, @SqlType(StandardTypes.VARCHAR) Slice max) + { + return min.compareTo(value) <= 0 && value.compareTo(max) <= 0; + } + + @ScalarOperator(CAST) + @SqlType(StandardTypes.BOOLEAN) + public static boolean castToBoolean(@SqlType(StandardTypes.VARCHAR) Slice value) + { + if (value.length() == 1) { + byte character = toUpperCase(value.getByte(0)); + if (character == 'T' || character == '1') { + return true; + } + if (character == 'F' || character == '0') { + return false; + } + } + if ((value.length() == 4) && + (toUpperCase(value.getByte(0)) == 'T') && + (toUpperCase(value.getByte(1)) == 'R') && + (toUpperCase(value.getByte(2)) == 'U') && + (toUpperCase(value.getByte(3)) == 'E')) { + return true; + } + if ((value.length() == 5) && + (toUpperCase(value.getByte(0)) == 'F') && + (toUpperCase(value.getByte(1)) == 'A') && + (toUpperCase(value.getByte(2)) == 'L') && + (toUpperCase(value.getByte(3)) == 'S') && + (toUpperCase(value.getByte(4)) == 'E')) { + return false; + } + throw new PrestoException(INVALID_CAST_ARGUMENT, format("Cannot cast '%s' to BOOLEAN", value.toString(UTF_8))); + } + + private static byte toUpperCase(byte b) + { + return isLowerCase(b) ? ((byte) (b - 32)) : b; + } + + private static boolean isLowerCase(byte b) + { + return (b >= 'a') && (b <= 'z'); + } + + @ScalarOperator(CAST) + @SqlType(StandardTypes.DOUBLE) + public static double castToDouble(@SqlType(StandardTypes.VARCHAR) Slice slice) + { + try { + return Double.parseDouble(slice.toString(UTF_8)); + } + catch (Exception e) { + throw new PrestoException(INVALID_CAST_ARGUMENT, format("Can not cast '%s' to DOUBLE", slice.toString(UTF_8))); + } + } + + @ScalarOperator(CAST) + @SqlType(StandardTypes.BIGINT) + public static long castToBigint(@SqlType(StandardTypes.VARCHAR) Slice slice) + { + try { + return Long.parseLong(slice.toString(UTF_8)); + } + catch (Exception e) { + throw new PrestoException(INVALID_CAST_ARGUMENT, format("Can not cast '%s' to BIGINT", slice.toString(UTF_8))); + } + } + + @ScalarOperator(CAST) + @SqlType(StandardTypes.VARBINARY) + public static Slice castToBinary(@SqlType(StandardTypes.VARCHAR) Slice slice) + { + return slice; + } + + @ScalarOperator(OperatorType.CAST) + @SqlType(StandardTypes.JSON) + public static Slice castToJson(@SqlType(StandardTypes.VARCHAR) Slice slice) throws IOException + { + try { + byte[] in = slice.getBytes(); + SliceOutput dynamicSliceOutput = new DynamicSliceOutput(in.length); + SORTED_MAPPER.writeValue(dynamicSliceOutput, SORTED_MAPPER.readValue(in, Object.class)); + return dynamicSliceOutput.slice(); + } + catch (Exception e) { + throw new PrestoException(INVALID_CAST_ARGUMENT, format("Cannot cast '%s' to JSON", slice.toStringUtf8())); + } + } + + @ScalarOperator(HASH_CODE) + @SqlType(StandardTypes.BIGINT) + public static long hashCode(@SqlType(StandardTypes.VARCHAR) Slice value) + { + return value.hashCode(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/util/CpuTimer.java b/presto-main/src/main/java/com/facebook/presto/util/CpuTimer.java new file mode 100644 index 00000000..f9f883df --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/util/CpuTimer.java @@ -0,0 +1,165 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.util; + +import io.airlift.units.Duration; + +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadMXBean; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static java.util.concurrent.TimeUnit.NANOSECONDS; + +public class CpuTimer +{ + private static final ThreadMXBean THREAD_MX_BEAN = ManagementFactory.getThreadMXBean(); + + private final long wallStartTime; + private final long cpuStartTime; + private final long userStartTime; + + private long intervalWallStart; + private long intervalCpuStart; + private long intervalUserStart; + + public CpuTimer() + { + wallStartTime = System.nanoTime(); + cpuStartTime = THREAD_MX_BEAN.getCurrentThreadCpuTime(); + userStartTime = THREAD_MX_BEAN.getCurrentThreadUserTime(); + + intervalWallStart = wallStartTime; + intervalCpuStart = cpuStartTime; + intervalUserStart = userStartTime; + } + + public CpuDuration startNewInterval() + { + long currentWallTime = System.nanoTime(); + long currentCpuTime = THREAD_MX_BEAN.getCurrentThreadCpuTime(); + long currentUserTime = THREAD_MX_BEAN.getCurrentThreadUserTime(); + + CpuDuration cpuDuration = new CpuDuration( + nanosBetween(intervalWallStart, currentWallTime), + nanosBetween(intervalCpuStart, currentCpuTime), + nanosBetween(intervalUserStart, currentUserTime)); + + intervalWallStart = currentWallTime; + intervalCpuStart = currentCpuTime; + intervalUserStart = currentUserTime; + + return cpuDuration; + } + + public CpuDuration elapsedIntervalTime() + { + long currentWallTime = System.nanoTime(); + long currentCpuTime = THREAD_MX_BEAN.getCurrentThreadCpuTime(); + long currentUserTime = THREAD_MX_BEAN.getCurrentThreadUserTime(); + + return new CpuDuration( + nanosBetween(intervalWallStart, currentWallTime), + nanosBetween(intervalCpuStart, currentCpuTime), + nanosBetween(intervalUserStart, currentUserTime)); + } + + public CpuDuration elapsedTime() + { + long currentWallTime = System.nanoTime(); + long currentCpuTime = THREAD_MX_BEAN.getCurrentThreadCpuTime(); + long currentUserTime = THREAD_MX_BEAN.getCurrentThreadUserTime(); + + return new CpuDuration( + nanosBetween(wallStartTime, currentWallTime), + nanosBetween(cpuStartTime, currentCpuTime), + nanosBetween(userStartTime, currentUserTime)); + } + + private Duration nanosBetween(long start, long end) + { + return new Duration(Math.abs(end - start), NANOSECONDS); + } + + public static class CpuDuration + { + private final Duration wall; + private final Duration cpu; + private final Duration user; + + public CpuDuration() + { + this.wall = new Duration(0, NANOSECONDS); + this.cpu = new Duration(0, NANOSECONDS); + this.user = new Duration(0, NANOSECONDS); + } + + public CpuDuration(Duration wall, Duration cpu, Duration user) + { + this.wall = wall; + this.cpu = cpu; + this.user = user; + } + + public Duration getWall() + { + return wall; + } + + public Duration getCpu() + { + return cpu; + } + + public Duration getUser() + { + return user; + } + + public CpuDuration add(CpuDuration cpuDuration) + { + return new CpuDuration( + addDurations(wall, cpuDuration.wall), + addDurations(cpu, cpuDuration.cpu), + addDurations(user, cpuDuration.user)); + } + + public CpuDuration subtract(CpuDuration cpuDuration) + { + return new CpuDuration( + subtractDurations(wall, cpuDuration.wall), + subtractDurations(cpu, cpuDuration.cpu), + subtractDurations(user, cpuDuration.user)); + } + + private static Duration addDurations(Duration a, Duration b) + { + return new Duration(a.getValue(NANOSECONDS) + b.getValue(NANOSECONDS), NANOSECONDS); + } + + private static Duration subtractDurations(Duration a, Duration b) + { + return new Duration(Math.max(0, a.getValue(NANOSECONDS) - b.getValue(NANOSECONDS)), NANOSECONDS); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("wall", wall) + .add("cpu", cpu) + .add("user", user) + .toString(); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/util/DateTimeUtils.java b/presto-main/src/main/java/com/facebook/presto/util/DateTimeUtils.java new file mode 100644 index 00000000..39f2d96d --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/util/DateTimeUtils.java @@ -0,0 +1,445 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.util; + +import com.facebook.presto.spi.type.SqlIntervalDayTime; +import com.facebook.presto.spi.type.TimeZoneKey; +import com.facebook.presto.sql.tree.IntervalLiteral.IntervalField; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.joda.time.MutablePeriod; +import org.joda.time.Period; +import org.joda.time.ReadWritablePeriod; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; +import org.joda.time.format.DateTimeFormatterBuilder; +import org.joda.time.format.DateTimeParser; +import org.joda.time.format.DateTimePrinter; +import org.joda.time.format.ISODateTimeFormat; +import org.joda.time.format.PeriodFormatter; +import org.joda.time.format.PeriodFormatterBuilder; +import org.joda.time.format.PeriodParser; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import static com.facebook.presto.spi.type.DateTimeEncoding.unpackMillisUtc; +import static com.facebook.presto.util.DateTimeZoneIndex.getDateTimeZone; +import static com.facebook.presto.util.DateTimeZoneIndex.packDateTimeWithZone; +import static com.facebook.presto.util.DateTimeZoneIndex.unpackDateTimeZone; + +public final class DateTimeUtils +{ + private DateTimeUtils() + { + } + + private static final DateTimeFormatter DATE_FORMATTER = ISODateTimeFormat.date().withZoneUTC(); + + public static int parseDate(String value) + { + return (int) TimeUnit.MILLISECONDS.toDays(DATE_FORMATTER.parseMillis(value)); + } + + public static String printDate(int days) + { + return DATE_FORMATTER.print(TimeUnit.DAYS.toMillis(days)); + } + + private static final DateTimeFormatter TIMESTAMP_FORMATTER; + private static final DateTimeFormatter TIMESTAMP_WITH_TIME_ZONE_FORMATTER; + + static { + DateTimeParser[] timestampWithoutTimeZoneParser = { + DateTimeFormat.forPattern("yyyy-M-d").getParser(), + DateTimeFormat.forPattern("yyyy-M-d H:m").getParser(), + DateTimeFormat.forPattern("yyyy-M-d H:m:s").getParser(), + DateTimeFormat.forPattern("yyyy-M-d H:m:s.SSS").getParser()}; + DateTimePrinter timestampWithoutTimeZonePrinter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss.SSS").getPrinter(); + TIMESTAMP_FORMATTER = new DateTimeFormatterBuilder().append(timestampWithoutTimeZonePrinter, timestampWithoutTimeZoneParser).toFormatter().withZoneUTC(); + + DateTimeParser[] timestampWithTimeZoneParser = { + DateTimeFormat.forPattern("yyyy-M-dZ").getParser(), + DateTimeFormat.forPattern("yyyy-M-d Z").getParser(), + DateTimeFormat.forPattern("yyyy-M-d H:mZ").getParser(), + DateTimeFormat.forPattern("yyyy-M-d H:m Z").getParser(), + DateTimeFormat.forPattern("yyyy-M-d H:m:sZ").getParser(), + DateTimeFormat.forPattern("yyyy-M-d H:m:s Z").getParser(), + DateTimeFormat.forPattern("yyyy-M-d H:m:s.SSSZ").getParser(), + DateTimeFormat.forPattern("yyyy-M-d H:m:s.SSS Z").getParser(), + DateTimeFormat.forPattern("yyyy-M-dZZZ").getParser(), + DateTimeFormat.forPattern("yyyy-M-d ZZZ").getParser(), + DateTimeFormat.forPattern("yyyy-M-d H:mZZZ").getParser(), + DateTimeFormat.forPattern("yyyy-M-d H:m ZZZ").getParser(), + DateTimeFormat.forPattern("yyyy-M-d H:m:sZZZ").getParser(), + DateTimeFormat.forPattern("yyyy-M-d H:m:s ZZZ").getParser(), + DateTimeFormat.forPattern("yyyy-M-d H:m:s.SSSZZZ").getParser(), + DateTimeFormat.forPattern("yyyy-M-d H:m:s.SSS ZZZ").getParser()}; + DateTimePrinter timestampWithTimeZonePrinter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss.SSS ZZZ").getPrinter(); + TIMESTAMP_WITH_TIME_ZONE_FORMATTER = new DateTimeFormatterBuilder().append(timestampWithTimeZonePrinter, timestampWithTimeZoneParser).toFormatter().withOffsetParsed(); + } + + public static long parseTimestamp(TimeZoneKey timeZoneKey, String value) + { + try { + return parseTimestampWithTimeZone(value); + } + catch (Exception e) { + return parseTimestampWithoutTimeZone(timeZoneKey, value); + } + } + + public static long parseTimestampWithTimeZone(String timestampWithTimeZone) + { + DateTime dateTime = TIMESTAMP_WITH_TIME_ZONE_FORMATTER.parseDateTime(timestampWithTimeZone); + return packDateTimeWithZone(dateTime); + } + + public static long parseTimestampWithoutTimeZone(TimeZoneKey timeZoneKey, String value) + { + return TIMESTAMP_FORMATTER.withZone(getDateTimeZone(timeZoneKey)).parseMillis(value); + } + + public static String printTimestampWithTimeZone(long timestampWithTimeZone) + { + DateTimeZone timeZone = unpackDateTimeZone(timestampWithTimeZone); + long millis = unpackMillisUtc(timestampWithTimeZone); + return TIMESTAMP_WITH_TIME_ZONE_FORMATTER.withZone(timeZone).print(millis); + } + + public static String printTimestampWithoutTimeZone(TimeZoneKey timeZoneKey, long timestamp) + { + return TIMESTAMP_FORMATTER.withZone(getDateTimeZone(timeZoneKey)).print(timestamp); + } + + public static boolean timestampHasTimeZone(String value) + { + try { + parseTimestampWithTimeZone(value); + return true; + } + catch (Exception e) { + return false; + } + } + + private static final DateTimeFormatter TIME_FORMATTER; + private static final DateTimeFormatter TIME_WITH_TIME_ZONE_FORMATTER; + + static { + DateTimeParser[] timeWithoutTimeZoneParser = { + DateTimeFormat.forPattern("H:m").getParser(), + DateTimeFormat.forPattern("H:m:s").getParser(), + DateTimeFormat.forPattern("H:m:s.SSS").getParser()}; + DateTimePrinter timeWithoutTimeZonePrinter = DateTimeFormat.forPattern("HH:mm:ss.SSS").getPrinter(); + TIME_FORMATTER = new DateTimeFormatterBuilder().append(timeWithoutTimeZonePrinter, timeWithoutTimeZoneParser).toFormatter().withZoneUTC(); + + DateTimeParser[] timeWithTimeZoneParser = { + DateTimeFormat.forPattern("H:mZ").getParser(), + DateTimeFormat.forPattern("H:m Z").getParser(), + DateTimeFormat.forPattern("H:m:sZ").getParser(), + DateTimeFormat.forPattern("H:m:s Z").getParser(), + DateTimeFormat.forPattern("H:m:s.SSSZ").getParser(), + DateTimeFormat.forPattern("H:m:s.SSS Z").getParser(), + DateTimeFormat.forPattern("H:mZZZ").getParser(), + DateTimeFormat.forPattern("H:m ZZZ").getParser(), + DateTimeFormat.forPattern("H:m:sZZZ").getParser(), + DateTimeFormat.forPattern("H:m:s ZZZ").getParser(), + DateTimeFormat.forPattern("H:m:s.SSSZZZ").getParser(), + DateTimeFormat.forPattern("H:m:s.SSS ZZZ").getParser()}; + DateTimePrinter timeWithTimeZonePrinter = DateTimeFormat.forPattern("HH:mm:ss.SSS ZZZ").getPrinter(); + TIME_WITH_TIME_ZONE_FORMATTER = new DateTimeFormatterBuilder().append(timeWithTimeZonePrinter, timeWithTimeZoneParser).toFormatter().withOffsetParsed(); + } + + public static long parseTime(TimeZoneKey timeZoneKey, String value) + { + try { + return parseTimeWithTimeZone(value); + } + catch (Exception e) { + return parseTimeWithoutTimeZone(timeZoneKey, value); + } + } + + public static long parseTimeWithTimeZone(String timeWithTimeZone) + { + DateTime dateTime = TIME_WITH_TIME_ZONE_FORMATTER.parseDateTime(timeWithTimeZone); + return packDateTimeWithZone(dateTime); + } + + public static long parseTimeWithoutTimeZone(TimeZoneKey timeZoneKey, String value) + { + return TIME_FORMATTER.withZone(getDateTimeZone(timeZoneKey)).parseMillis(value); + } + + public static String printTimeWithTimeZone(long timeWithTimeZone) + { + DateTimeZone timeZone = unpackDateTimeZone(timeWithTimeZone); + long millis = unpackMillisUtc(timeWithTimeZone); + return TIME_WITH_TIME_ZONE_FORMATTER.withZone(timeZone).print(millis); + } + + public static String printTimeWithoutTimeZone(TimeZoneKey timeZoneKey, long value) + { + return TIME_FORMATTER.withZone(getDateTimeZone(timeZoneKey)).print(value); + } + + public static boolean timeHasTimeZone(String value) + { + try { + parseTimeWithTimeZone(value); + return true; + } + catch (Exception e) { + return false; + } + } + + private static final int YEAR_FIELD = 0; + private static final int MONTH_FIELD = 1; + private static final int DAY_FIELD = 3; + private static final int HOUR_FIELD = 4; + private static final int MINUTE_FIELD = 5; + private static final int SECOND_FIELD = 6; + private static final int MILLIS_FIELD = 7; + + private static final PeriodFormatter INTERVAL_DAY_SECOND_FORMATTER = cretePeriodFormatter(IntervalField.DAY, IntervalField.SECOND); + private static final PeriodFormatter INTERVAL_DAY_MINUTE_FORMATTER = cretePeriodFormatter(IntervalField.DAY, IntervalField.MINUTE); + private static final PeriodFormatter INTERVAL_DAY_HOUR_FORMATTER = cretePeriodFormatter(IntervalField.DAY, IntervalField.HOUR); + private static final PeriodFormatter INTERVAL_DAY_FORMATTER = cretePeriodFormatter(IntervalField.DAY, IntervalField.DAY); + + private static final PeriodFormatter INTERVAL_HOUR_SECOND_FORMATTER = cretePeriodFormatter(IntervalField.HOUR, IntervalField.SECOND); + private static final PeriodFormatter INTERVAL_HOUR_MINUTE_FORMATTER = cretePeriodFormatter(IntervalField.HOUR, IntervalField.MINUTE); + private static final PeriodFormatter INTERVAL_HOUR_FORMATTER = cretePeriodFormatter(IntervalField.HOUR, IntervalField.HOUR); + + private static final PeriodFormatter INTERVAL_MINUTE_SECOND_FORMATTER = cretePeriodFormatter(IntervalField.MINUTE, IntervalField.SECOND); + private static final PeriodFormatter INTERVAL_MINUTE_FORMATTER = cretePeriodFormatter(IntervalField.MINUTE, IntervalField.MINUTE); + + private static final PeriodFormatter INTERVAL_SECOND_FORMATTER = cretePeriodFormatter(IntervalField.SECOND, IntervalField.SECOND); + + private static final PeriodFormatter INTERVAL_YEAR_MONTH_FORMATTER = cretePeriodFormatter(IntervalField.YEAR, IntervalField.MONTH); + private static final PeriodFormatter INTERVAL_YEAR_FORMATTER = cretePeriodFormatter(IntervalField.YEAR, IntervalField.YEAR); + + private static final PeriodFormatter INTERVAL_MONTH_FORMATTER = cretePeriodFormatter(IntervalField.MONTH, IntervalField.MONTH); + + public static long parseDayTimeInterval(String value, IntervalField startField, Optional endField) + { + IntervalField end = endField.orElse(startField); + + if (startField == IntervalField.DAY && end == IntervalField.SECOND) { + return parsePeriodMillis(INTERVAL_DAY_SECOND_FORMATTER, value, startField, end); + } + if (startField == IntervalField.DAY && end == IntervalField.MINUTE) { + return parsePeriodMillis(INTERVAL_DAY_MINUTE_FORMATTER, value, startField, end); + } + if (startField == IntervalField.DAY && end == IntervalField.HOUR) { + return parsePeriodMillis(INTERVAL_DAY_HOUR_FORMATTER, value, startField, end); + } + if (startField == IntervalField.DAY && end == IntervalField.DAY) { + return parsePeriodMillis(INTERVAL_DAY_FORMATTER, value, startField, end); + } + + if (startField == IntervalField.HOUR && end == IntervalField.SECOND) { + return parsePeriodMillis(INTERVAL_HOUR_SECOND_FORMATTER, value, startField, end); + } + if (startField == IntervalField.HOUR && end == IntervalField.MINUTE) { + return parsePeriodMillis(INTERVAL_HOUR_MINUTE_FORMATTER, value, startField, end); + } + if (startField == IntervalField.HOUR && end == IntervalField.HOUR) { + return parsePeriodMillis(INTERVAL_HOUR_FORMATTER, value, startField, end); + } + + if (startField == IntervalField.MINUTE && end == IntervalField.SECOND) { + return parsePeriodMillis(INTERVAL_MINUTE_SECOND_FORMATTER, value, startField, end); + } + if (startField == IntervalField.MINUTE && end == IntervalField.MINUTE) { + return parsePeriodMillis(INTERVAL_MINUTE_FORMATTER, value, startField, end); + } + + if (startField == IntervalField.SECOND && end == IntervalField.SECOND) { + return parsePeriodMillis(INTERVAL_SECOND_FORMATTER, value, startField, end); + } + + throw new IllegalArgumentException("Invalid day second interval qualifier: " + startField + " to " + end); + } + + public static long parsePeriodMillis(PeriodFormatter periodFormatter, String value, IntervalField startField, IntervalField endField) + { + Period period = parsePeriod(periodFormatter, value, startField, endField); + return SqlIntervalDayTime.toMillis( + period.getValue(DAY_FIELD), + period.getValue(HOUR_FIELD), + period.getValue(MINUTE_FIELD), + period.getValue(SECOND_FIELD), + period.getValue(MILLIS_FIELD)); + } + + public static String printDayTimeInterval(long millis) + { + return SqlIntervalDayTime.formatMillis(millis); + } + + public static long parseYearMonthInterval(String value, IntervalField startField, Optional endField) + { + IntervalField end = endField.orElse(startField); + + if (startField == IntervalField.YEAR && end == IntervalField.MONTH) { + PeriodFormatter periodFormatter = INTERVAL_YEAR_MONTH_FORMATTER; + return parsePeriodMonths(value, periodFormatter, startField, end); + } + if (startField == IntervalField.YEAR && end == IntervalField.YEAR) { + return parsePeriodMonths(value, INTERVAL_YEAR_FORMATTER, startField, end); + } + + if (startField == IntervalField.MONTH && end == IntervalField.MONTH) { + return parsePeriodMonths(value, INTERVAL_MONTH_FORMATTER, startField, end); + } + + throw new IllegalArgumentException("Invalid year month interval qualifier: " + startField + " to " + end); + } + + private static long parsePeriodMonths(String value, PeriodFormatter periodFormatter, IntervalField startField, IntervalField endField) + { + Period period = parsePeriod(periodFormatter, value, startField, endField); + return period.getValue(YEAR_FIELD) * 12 + + period.getValue(MONTH_FIELD); + } + + public static String printYearMonthInterval(long months) + { + return (months / 12) + "-" + (months % 12); + } + + private static Period parsePeriod(PeriodFormatter periodFormatter, String value, IntervalField startField, IntervalField endField) + { + try { + return periodFormatter.parsePeriod(value); + } + catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Invalid INTERVAL " + startField + " to " + endField + " value: " + value, e); + } + } + + private static PeriodFormatter cretePeriodFormatter(IntervalField startField, IntervalField endField) + { + if (endField == null) { + endField = startField; + } + + List parsers = new ArrayList<>(); + + PeriodFormatterBuilder builder = new PeriodFormatterBuilder(); + //CHECKSTYLE.OFF + switch (startField) { + case YEAR: + builder.appendYears(); + parsers.add(builder.toParser()); + if (endField == IntervalField.YEAR) { + break; + } + builder.appendLiteral("-"); + case MONTH: + builder.appendMonths(); + parsers.add(builder.toParser()); + if (endField != IntervalField.MONTH) { + throw new IllegalArgumentException("Invalid interval qualifier: " + startField + " to " + endField); + } + break; + + case DAY: + builder.appendDays(); + parsers.add(builder.toParser()); + if (endField == IntervalField.DAY) { + break; + } + builder.appendLiteral(" "); + + case HOUR: + builder.appendHours(); + parsers.add(builder.toParser()); + if (endField == IntervalField.HOUR) { + break; + } + builder.appendLiteral(":"); + + case MINUTE: + builder.appendMinutes(); + parsers.add(builder.toParser()); + if (endField == IntervalField.MINUTE) { + break; + } + builder.appendLiteral(":"); + + case SECOND: + builder.appendSecondsWithOptionalMillis(); + parsers.add(builder.toParser()); + } + //CHECKSTYLE.ON + + return new PeriodFormatter(builder.toPrinter(), new OrderedPeriodParser(parsers)); + } + + private static class OrderedPeriodParser + implements PeriodParser + { + private final List parsers; + + private OrderedPeriodParser(List parsers) + { + this.parsers = parsers; + } + + @Override + public int parseInto(ReadWritablePeriod period, String text, int position, Locale locale) + { + int bestValidPos = position; + ReadWritablePeriod bestValidPeriod = null; + + int bestInvalidPos = position; + + for (PeriodParser parser : parsers) { + ReadWritablePeriod parsedPeriod = new MutablePeriod(); + int parsePos = parser.parseInto(parsedPeriod, text, position, locale); + if (parsePos >= position) { + if (parsePos > bestValidPos) { + bestValidPos = parsePos; + bestValidPeriod = parsedPeriod; + if (parsePos >= text.length()) { + break; + } + } + } + else if (parsePos < 0) { + parsePos = ~parsePos; + if (parsePos > bestInvalidPos) { + bestInvalidPos = parsePos; + } + } + } + + if (bestValidPos > position || (bestValidPos == position)) { + // Restore the state to the best valid parse. + if (bestValidPeriod != null) { + period.setPeriod(bestValidPeriod); + } + return bestValidPos; + } + + return ~bestInvalidPos; + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/util/DateTimeZoneIndex.java b/presto-main/src/main/java/com/facebook/presto/util/DateTimeZoneIndex.java new file mode 100644 index 00000000..b742f3ef --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/util/DateTimeZoneIndex.java @@ -0,0 +1,93 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.util; + +import com.facebook.presto.spi.type.DateTimeEncoding; +import com.facebook.presto.spi.type.TimeZoneKey; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.joda.time.chrono.ISOChronology; + +import static com.facebook.presto.spi.type.DateTimeEncoding.unpackMillisUtc; +import static com.facebook.presto.spi.type.DateTimeEncoding.unpackZoneKey; +import static com.facebook.presto.spi.type.TimeZoneKey.MAX_TIME_ZONE_KEY; +import static com.facebook.presto.spi.type.TimeZoneKey.getTimeZoneKeys; + +public final class DateTimeZoneIndex +{ + private DateTimeZoneIndex() + { + } + + private static final DateTimeZone[] DATE_TIME_ZONES; + private static final ISOChronology[] CHRONOLOGIES; + private static final int[] FIXED_ZONE_OFFSET; + + private static final int VARIABLE_ZONE = Integer.MAX_VALUE; + + static { + DATE_TIME_ZONES = new DateTimeZone[MAX_TIME_ZONE_KEY + 1]; + CHRONOLOGIES = new ISOChronology[MAX_TIME_ZONE_KEY + 1]; + FIXED_ZONE_OFFSET = new int[MAX_TIME_ZONE_KEY + 1]; + for (TimeZoneKey timeZoneKey : getTimeZoneKeys()) { + short zoneKey = timeZoneKey.getKey(); + DateTimeZone dateTimeZone = DateTimeZone.forID(timeZoneKey.getId()); + DATE_TIME_ZONES[zoneKey] = dateTimeZone; + CHRONOLOGIES[zoneKey] = ISOChronology.getInstance(dateTimeZone); + if (dateTimeZone.isFixed() && dateTimeZone.getOffset(0) % 60_000 == 0) { + FIXED_ZONE_OFFSET[zoneKey] = dateTimeZone.getOffset(0) / 60_000; + } + else { + FIXED_ZONE_OFFSET[zoneKey] = VARIABLE_ZONE; + } + } + } + + public static ISOChronology getChronology(TimeZoneKey zoneKey) + { + return CHRONOLOGIES[zoneKey.getKey()]; + } + + public static ISOChronology unpackChronology(long timestampWithTimeZone) + { + return getChronology(unpackZoneKey(timestampWithTimeZone)); + } + + public static DateTimeZone getDateTimeZone(TimeZoneKey zoneKey) + { + return DATE_TIME_ZONES[zoneKey.getKey()]; + } + + public static DateTimeZone unpackDateTimeZone(long dateTimeWithTimeZone) + { + return getDateTimeZone(unpackZoneKey(dateTimeWithTimeZone)); + } + + public static long packDateTimeWithZone(DateTime dateTime) + { + return DateTimeEncoding.packDateTimeWithZone(dateTime.getMillis(), dateTime.getZone().getID()); + } + + public static int extractZoneOffsetMinutes(long dateTimeWithTimeZone) + { + short zoneKey = unpackZoneKey(dateTimeWithTimeZone).getKey(); + + if (FIXED_ZONE_OFFSET[zoneKey] == VARIABLE_ZONE) { + return DATE_TIME_ZONES[zoneKey].getOffset(unpackMillisUtc(dateTimeWithTimeZone)) / 60_000; + } + else { + return FIXED_ZONE_OFFSET[zoneKey]; + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/util/Failures.java b/presto-main/src/main/java/com/facebook/presto/util/Failures.java new file mode 100644 index 00000000..234113be --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/util/Failures.java @@ -0,0 +1,118 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.util; + +import com.facebook.presto.client.ErrorLocation; +import com.facebook.presto.execution.ExecutionFailureInfo; +import com.facebook.presto.execution.Failure; +import com.facebook.presto.spi.ErrorCode; +import com.facebook.presto.spi.ErrorCodeSupplier; +import com.facebook.presto.spi.PrestoException; +import com.facebook.presto.sql.analyzer.SemanticException; +import com.facebook.presto.sql.parser.ParsingException; +import com.google.common.collect.Lists; + +import javax.annotation.Nullable; + +import java.util.Collection; +import java.util.List; + +import static com.facebook.presto.spi.StandardErrorCode.INTERNAL_ERROR; +import static com.facebook.presto.spi.StandardErrorCode.SYNTAX_ERROR; +import static com.facebook.presto.util.ImmutableCollectors.toImmutableList; +import static com.google.common.base.Functions.toStringFunction; +import static java.lang.String.format; +import static java.util.Arrays.asList; + +public final class Failures +{ + private static final String NODE_CRASHED_ERROR = "The node may have crashed or be under too much load. " + + "This is probably a transient issue, so please retry your query in a few minutes."; + + public static final String WORKER_NODE_ERROR = "Encountered too many errors talking to a worker node. " + NODE_CRASHED_ERROR; + + public static final String WORKER_RESTARTED_ERROR = "A worker node running your query has restarted. " + NODE_CRASHED_ERROR; + + private Failures() {} + + public static ExecutionFailureInfo toFailure(Throwable failure) + { + if (failure == null) { + return null; + } + // todo prevent looping with suppressed cause loops and such + String type; + if (failure instanceof Failure) { + type = ((Failure) failure).getType(); + } + else { + type = failure.getClass().getCanonicalName(); + } + + return new ExecutionFailureInfo(type, + failure.getMessage(), + toFailure(failure.getCause()), + toFailures(asList(failure.getSuppressed())), + Lists.transform(asList(failure.getStackTrace()), toStringFunction()), + getErrorLocation(failure), + toErrorCode(failure)); + } + + public static void checkCondition(boolean condition, ErrorCodeSupplier errorCode, String formatString, Object... args) + { + if (!condition) { + throw new PrestoException(errorCode, format(formatString, args)); + } + } + + public static List toFailures(Collection failures) + { + return failures.stream() + .map(Failures::toFailure) + .collect(toImmutableList()); + } + + @Nullable + private static ErrorLocation getErrorLocation(Throwable throwable) + { + // TODO: this is a big hack + if (throwable instanceof ParsingException) { + ParsingException e = (ParsingException) throwable; + return new ErrorLocation(e.getLineNumber(), e.getColumnNumber()); + } + return null; + } + + @Nullable + private static ErrorCode toErrorCode(@Nullable Throwable throwable) + { + if (throwable == null) { + return null; + } + + if (throwable instanceof PrestoException) { + return ((PrestoException) throwable).getErrorCode(); + } + if (throwable instanceof Failure && ((Failure) throwable).getErrorCode() != null) { + return ((Failure) throwable).getErrorCode(); + } + if (throwable instanceof ParsingException || throwable instanceof SemanticException) { + return SYNTAX_ERROR.toErrorCode(); + } + if (throwable.getCause() != null) { + return toErrorCode(throwable.getCause()); + } + return INTERNAL_ERROR.toErrorCode(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/util/GenerateTimeZoneIndex.java b/presto-main/src/main/java/com/facebook/presto/util/GenerateTimeZoneIndex.java new file mode 100644 index 00000000..820e433a --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/util/GenerateTimeZoneIndex.java @@ -0,0 +1,127 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.util; + +import com.google.common.base.Predicate; +import com.google.common.collect.Sets; +import org.joda.time.DateTimeZone; + +import java.util.Arrays; +import java.util.TimeZone; +import java.util.TreeSet; + +import static com.facebook.presto.spi.type.TimeZoneKey.isUtcZoneId; +import static com.google.common.base.Predicates.not; +import static com.google.common.collect.Sets.filter; +import static com.google.common.collect.Sets.intersection; +import static java.lang.Math.abs; + +public final class GenerateTimeZoneIndex +{ + private GenerateTimeZoneIndex() + { + } + + public static void main(String[] args) + throws InterruptedException + { + // + // Header + // + System.out.println("#"); + System.out.println("# DO NOT REMOVE OR MODIFY EXISTING ENTRIES"); + System.out.println("#"); + System.out.println("# This file contain the fixed numeric id of every supported time zone id."); + System.out.println("# Every zone id in this file must be supported by java.util.TimeZone and the"); + System.out.println("# Joda time library. This is because Presto uses both java.util.TimeZone and"); + System.out.println("# the Joda time## for during execution."); + System.out.println("#"); + System.out.println("# suppress inspection \"UnusedProperty\" for whole file"); + + // + // We assume 0 is UTC and do not generate it + // + + // + // Negative offset + // + short nextZoneKey = 1; + for (int offset = 14 * 60; offset > 0; offset--) { + String zoneId = String.format("-%02d:%02d", offset / 60, abs(offset % 60)); + + short zoneKey = nextZoneKey++; + + System.out.println(zoneKey + " " + zoneId); + } + + // + // Positive offset + // + for (int offset = 1; offset <= 14 * 60; offset++) { + String zoneId = String.format("+%02d:%02d", offset / 60, abs(offset % 60)); + + short zoneKey = nextZoneKey++; + + System.out.println(zoneKey + " " + zoneId); + } + + // + // IANA regional zones: region/city + // + + TreeSet jodaZones = new TreeSet<>(DateTimeZone.getAvailableIDs()); + TreeSet jdkZones = new TreeSet<>(Arrays.asList(TimeZone.getAvailableIDs())); + + TreeSet zoneIds = new TreeSet<>(filter(intersection(jodaZones, jdkZones), not(ignoredZone()))); + + for (String zoneId : zoneIds) { + if (zoneId.indexOf('/') < 0) { + continue; + } + short zoneKey = nextZoneKey++; + + System.out.println(zoneKey + " " + zoneId); + } + + // + // Other zones + // + for (String zoneId : zoneIds) { + if (zoneId.indexOf('/') >= 0) { + continue; + } + short zoneKey = nextZoneKey++; + + System.out.println(zoneKey + " " + zoneId); + } + + System.out.println(); + System.out.println("# Zones not supported in Java"); + for (String invalidZone : filter(Sets.difference(jodaZones, jdkZones), not(ignoredZone()))) { + System.out.println("# " + invalidZone); + } + + System.out.println(); + System.out.println("# Zones not supported in Joda"); + for (String invalidZone : filter(Sets.difference(jdkZones, jodaZones), not(ignoredZone()))) { + System.out.println("# " + invalidZone); + } + Thread.sleep(1000); + } + + public static Predicate ignoredZone() + { + return zoneId -> isUtcZoneId(zoneId) || zoneId.startsWith("Etc/"); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/util/GraphvizPrinter.java b/presto-main/src/main/java/com/facebook/presto/util/GraphvizPrinter.java new file mode 100644 index 00000000..ad1a0af6 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/util/GraphvizPrinter.java @@ -0,0 +1,594 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.util; + +import com.facebook.presto.sql.planner.PlanFragment; +import com.facebook.presto.sql.planner.SubPlan; +import com.facebook.presto.sql.planner.Symbol; +import com.facebook.presto.sql.planner.plan.AggregationNode; +import com.facebook.presto.sql.planner.plan.DistinctLimitNode; +import com.facebook.presto.sql.planner.plan.ExchangeNode; +import com.facebook.presto.sql.planner.plan.FilterNode; +import com.facebook.presto.sql.planner.plan.IndexJoinNode; +import com.facebook.presto.sql.planner.plan.IndexSourceNode; +import com.facebook.presto.sql.planner.plan.JoinNode; +import com.facebook.presto.sql.planner.plan.LimitNode; +import com.facebook.presto.sql.planner.plan.MarkDistinctNode; +import com.facebook.presto.sql.planner.plan.OutputNode; +import com.facebook.presto.sql.planner.plan.PlanFragmentId; +import com.facebook.presto.sql.planner.plan.PlanNode; +import com.facebook.presto.sql.planner.plan.PlanVisitor; +import com.facebook.presto.sql.planner.plan.ProjectNode; +import com.facebook.presto.sql.planner.plan.RemoteSourceNode; +import com.facebook.presto.sql.planner.plan.RowNumberNode; +import com.facebook.presto.sql.planner.plan.SampleNode; +import com.facebook.presto.sql.planner.plan.SemiJoinNode; +import com.facebook.presto.sql.planner.plan.SortNode; +import com.facebook.presto.sql.planner.plan.TableCommitNode; +import com.facebook.presto.sql.planner.plan.TableScanNode; +import com.facebook.presto.sql.planner.plan.TableWriterNode; +import com.facebook.presto.sql.planner.plan.TopNNode; +import com.facebook.presto.sql.planner.plan.TopNRowNumberNode; +import com.facebook.presto.sql.planner.plan.UnionNode; +import com.facebook.presto.sql.planner.plan.UnnestNode; +import com.facebook.presto.sql.planner.plan.ValuesNode; +import com.facebook.presto.sql.planner.plan.WindowNode; +import com.facebook.presto.sql.tree.ComparisonExpression; +import com.facebook.presto.sql.tree.Expression; +import com.facebook.presto.sql.tree.FunctionCall; +import com.facebook.presto.sql.tree.QualifiedNameReference; +import com.google.common.base.Joiner; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; +import com.google.common.collect.Maps; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import static com.facebook.presto.sql.planner.plan.ExchangeNode.Type.REPARTITION; +import static com.google.common.collect.Maps.immutableEnumMap; +import static java.lang.String.format; + +public final class GraphvizPrinter +{ + private enum NodeType + { + EXCHANGE, + AGGREGATE, + FILTER, + PROJECT, + TOPN, + OUTPUT, + LIMIT, + TABLESCAN, + VALUES, + JOIN, + SINK, + WINDOW, + UNION, + SORT, + SAMPLE, + MARK_DISTINCT, + TABLE_WRITER, + TABLE_COMMIT, + INDEX_SOURCE, + UNNEST + } + + private static final Map NODE_COLORS = immutableEnumMap(ImmutableMap.builder() + .put(NodeType.EXCHANGE, "gold") + .put(NodeType.AGGREGATE, "chartreuse3") + .put(NodeType.FILTER, "yellow") + .put(NodeType.PROJECT, "bisque") + .put(NodeType.TOPN, "darksalmon") + .put(NodeType.OUTPUT, "white") + .put(NodeType.LIMIT, "gray83") + .put(NodeType.TABLESCAN, "deepskyblue") + .put(NodeType.VALUES, "deepskyblue") + .put(NodeType.JOIN, "orange") + .put(NodeType.SORT, "aliceblue") + .put(NodeType.SINK, "indianred1") + .put(NodeType.WINDOW, "darkolivegreen4") + .put(NodeType.UNION, "turquoise4") + .put(NodeType.MARK_DISTINCT, "violet") + .put(NodeType.TABLE_WRITER, "cyan") + .put(NodeType.TABLE_COMMIT, "hotpink") + .put(NodeType.INDEX_SOURCE, "dodgerblue3") + .put(NodeType.UNNEST, "crimson") + .put(NodeType.SAMPLE, "goldenrod4") + .build()); + + static { + Preconditions.checkState(NODE_COLORS.size() == NodeType.values().length); + } + + private GraphvizPrinter() {} + + public static String printLogical(List fragments) + { + Map fragmentsById = Maps.uniqueIndex(fragments, PlanFragment::getId); + PlanNodeIdGenerator idGenerator = new PlanNodeIdGenerator(); + + StringBuilder output = new StringBuilder(); + output.append("digraph logical_plan {\n"); + + for (PlanFragment fragment : fragments) { + printFragmentNodes(output, fragment, idGenerator); + } + + for (PlanFragment fragment : fragments) { + fragment.getRoot().accept(new EdgePrinter(output, fragmentsById, idGenerator), null); + } + + output.append("}\n"); + + return output.toString(); + } + + public static String printDistributed(SubPlan plan) + { + List fragments = plan.getAllFragments(); + Map fragmentsById = Maps.uniqueIndex(fragments, PlanFragment::getId); + PlanNodeIdGenerator idGenerator = new PlanNodeIdGenerator(); + + StringBuilder output = new StringBuilder(); + output.append("digraph distributed_plan {\n"); + + printSubPlan(plan, fragmentsById, idGenerator, output); + + output.append("}\n"); + + return output.toString(); + } + + private static void printSubPlan(SubPlan plan, Map fragmentsById, PlanNodeIdGenerator idGenerator, StringBuilder output) + { + PlanFragment fragment = plan.getFragment(); + printFragmentNodes(output, fragment, idGenerator); + fragment.getRoot().accept(new EdgePrinter(output, fragmentsById, idGenerator), null); + + for (SubPlan child : plan.getChildren()) { + printSubPlan(child, fragmentsById, idGenerator, output); + } + } + + private static void printFragmentNodes(StringBuilder output, PlanFragment fragment, PlanNodeIdGenerator idGenerator) + { + String clusterId = "cluster_" + fragment.getId(); + output.append("subgraph ") + .append(clusterId) + .append(" {") + .append('\n'); + + output.append(format("label = \"%s\"", fragment.getDistribution())) + .append('\n'); + + PlanNode plan = fragment.getRoot(); + plan.accept(new NodePrinter(output, idGenerator), null); + + output.append("}") + .append('\n'); + } + + private static class NodePrinter + extends PlanVisitor + { + private static final int MAX_NAME_WIDTH = 100; + private final StringBuilder output; + private final PlanNodeIdGenerator idGenerator; + + public NodePrinter(StringBuilder output, PlanNodeIdGenerator idGenerator) + { + this.output = output; + this.idGenerator = idGenerator; + } + + @Override + protected Void visitPlan(PlanNode node, Void context) + { + throw new UnsupportedOperationException(format("Node %s does not have a Graphviz visitor", node.getClass().getName())); + } + + @Override + public Void visitTableWriter(TableWriterNode node, Void context) + { + List columns = new ArrayList<>(); + for (int i = 0; i < node.getColumnNames().size(); i++) { + columns.add(node.getColumnNames().get(i) + " := " + node.getColumns().get(i)); + } + printNode(node, format("TableWriter[%s]", Joiner.on(", ").join(columns)), NODE_COLORS.get(NodeType.TABLE_WRITER)); + return node.getSource().accept(this, context); + } + + @Override + public Void visitTableCommit(TableCommitNode node, Void context) + { + printNode(node, format("TableCommit[%s]", Joiner.on(", ").join(node.getOutputSymbols())), NODE_COLORS.get(NodeType.TABLE_COMMIT)); + return node.getSource().accept(this, context); + } + + @Override + public Void visitSample(SampleNode node, Void context) + { + printNode(node, format("Sample[type=%s, ratio=%f, rescaled=%s]", node.getSampleType(), node.getSampleRatio(), node.isRescaled()), NODE_COLORS.get(NodeType.SAMPLE)); + return node.getSource().accept(this, context); + } + + @Override + public Void visitSort(SortNode node, Void context) + { + printNode(node, format("Sort[%s]", Joiner.on(", ").join(node.getOrderBy())), NODE_COLORS.get(NodeType.SORT)); + return node.getSource().accept(this, context); + } + + @Override + public Void visitMarkDistinct(MarkDistinctNode node, Void context) + { + printNode(node, format("MarkDistinct[%s]", node.getMarkerSymbol()), format("%s => %s", node.getDistinctSymbols(), node.getMarkerSymbol()), NODE_COLORS.get(NodeType.MARK_DISTINCT)); + return node.getSource().accept(this, context); + } + + @Override + public Void visitWindow(WindowNode node, Void context) + { + printNode(node, "Window", format("partition by = %s|order by = %s", Joiner.on(", ").join(node.getPartitionBy()), Joiner.on(", ").join(node.getOrderBy())), NODE_COLORS.get(NodeType.WINDOW)); + return node.getSource().accept(this, context); + } + + @Override + public Void visitRowNumber(RowNumberNode node, Void context) + { + printNode(node, + "RowNumber", + format("partition by = %s", Joiner.on(", ").join(node.getPartitionBy())), + NODE_COLORS.get(NodeType.WINDOW)); + return node.getSource().accept(this, context); + } + + @Override + public Void visitTopNRowNumber(TopNRowNumberNode node, Void context) + { + printNode(node, + "TopNRowNumber", + format("partition by = %s|order by = %s|n = %s", Joiner.on(", ").join(node.getPartitionBy()), Joiner.on(", ").join(node.getOrderBy()), node.getMaxRowCountPerPartition()), + NODE_COLORS.get(NodeType.WINDOW)); + return node.getSource().accept(this, context); + } + + @Override + public Void visitUnion(UnionNode node, Void context) + { + printNode(node, "Union", NODE_COLORS.get(NodeType.UNION)); + + for (PlanNode planNode : node.getSources()) { + planNode.accept(this, context); + } + + return null; + } + + @Override + public Void visitRemoteSource(RemoteSourceNode node, Void context) + { + printNode(node, "Exchange 1:N", NODE_COLORS.get(NodeType.EXCHANGE)); + return null; + } + + @Override + public Void visitExchange(ExchangeNode node, Void context) + { + List symbols = node.getOutputSymbols(); + if (node.getType() == REPARTITION) { + symbols = node.getPartitionKeys(); + } + String columns = Joiner.on(", ").join(symbols); + printNode(node, format("ExchangeNode[%s]", node.getType()), columns, NODE_COLORS.get(NodeType.EXCHANGE)); + for (PlanNode planNode : node.getSources()) { + planNode.accept(this, context); + } + return null; + } + + @Override + public Void visitAggregation(AggregationNode node, Void context) + { + StringBuilder builder = new StringBuilder(); + for (Map.Entry entry : node.getAggregations().entrySet()) { + if (node.getMasks().containsKey(entry.getKey())) { + builder.append(format("%s := %s (mask = %s)\\n", entry.getKey(), entry.getValue(), node.getMasks().get(entry.getKey()))); + } + else { + builder.append(format("%s := %s\\n", entry.getKey(), entry.getValue())); + } + } + printNode(node, format("Aggregate[%s]", node.getStep()), builder.toString(), NODE_COLORS.get(NodeType.AGGREGATE)); + return node.getSource().accept(this, context); + } + + @Override + public Void visitFilter(FilterNode node, Void context) + { + String expression = node.getPredicate().toString(); + printNode(node, "Filter", expression, NODE_COLORS.get(NodeType.FILTER)); + return node.getSource().accept(this, context); + } + + @Override + public Void visitProject(ProjectNode node, Void context) + { + StringBuilder builder = new StringBuilder(); + for (Map.Entry entry : node.getAssignments().entrySet()) { + if ((entry.getValue() instanceof QualifiedNameReference) && + ((QualifiedNameReference) entry.getValue()).getName().equals(entry.getKey().toQualifiedName())) { + // skip identity assignments + continue; + } + builder.append(format("%s := %s\\n", entry.getKey(), entry.getValue())); + } + + printNode(node, "Project", builder.toString(), NODE_COLORS.get(NodeType.PROJECT)); + return node.getSource().accept(this, context); + } + + @Override + public Void visitUnnest(UnnestNode node, Void context) + { + if (node.getOrdinalitySymbol() == null) { + printNode(node, format("Unnest[%s]", node.getUnnestSymbols().keySet()), NODE_COLORS.get(NodeType.UNNEST)); + } + else { + printNode(node, format("Unnest[%s (ordinality)]", node.getUnnestSymbols().keySet()), NODE_COLORS.get(NodeType.UNNEST)); + } + return node.getSource().accept(this, context); + } + + @Override + public Void visitTopN(final TopNNode node, Void context) + { + Iterable keys = Iterables.transform(node.getOrderBy(), input -> input + " " + node.getOrderings().get(input)); + printNode(node, format("TopN[%s]", node.getCount()), Joiner.on(", ").join(keys), NODE_COLORS.get(NodeType.TOPN)); + return node.getSource().accept(this, context); + } + + @Override + public Void visitOutput(OutputNode node, Void context) + { + String columns = getColumns(node); + printNode(node, format("Output[%s]", columns), NODE_COLORS.get(NodeType.OUTPUT)); + return node.getSource().accept(this, context); + } + + @Override + public Void visitDistinctLimit(DistinctLimitNode node, Void context) + { + printNode(node, format("DistinctLimit[%s]", node.getLimit()), NODE_COLORS.get(NodeType.LIMIT)); + return node.getSource().accept(this, context); + } + + @Override + public Void visitLimit(LimitNode node, Void context) + { + printNode(node, format("Limit[%s]", node.getCount()), NODE_COLORS.get(NodeType.LIMIT)); + return node.getSource().accept(this, context); + } + + @Override + public Void visitTableScan(TableScanNode node, Void context) + { + printNode(node, format("TableScan[%s]", node.getTable()), format("original constraint=%s", node.getOriginalConstraint()), NODE_COLORS.get(NodeType.TABLESCAN)); + return null; + } + + @Override + public Void visitValues(ValuesNode node, Void context) + { + printNode(node, "Values", NODE_COLORS.get(NodeType.TABLESCAN)); + return null; + } + + @Override + public Void visitJoin(JoinNode node, Void context) + { + List joinExpressions = new ArrayList<>(); + for (JoinNode.EquiJoinClause clause : node.getCriteria()) { + joinExpressions.add(new ComparisonExpression(ComparisonExpression.Type.EQUAL, + new QualifiedNameReference(clause.getLeft().toQualifiedName()), + new QualifiedNameReference(clause.getRight().toQualifiedName()))); + } + + String criteria = Joiner.on(" AND ").join(joinExpressions); + printNode(node, node.getType().getJoinLabel(), criteria, NODE_COLORS.get(NodeType.JOIN)); + + node.getLeft().accept(this, context); + node.getRight().accept(this, context); + + return null; + } + + @Override + public Void visitSemiJoin(SemiJoinNode node, Void context) + { + printNode(node, "SemiJoin", format("%s = %s", node.getSourceJoinSymbol(), node.getFilteringSourceJoinSymbol()), NODE_COLORS.get(NodeType.JOIN)); + + node.getSource().accept(this, context); + node.getFilteringSource().accept(this, context); + + return null; + } + + @Override + public Void visitIndexSource(IndexSourceNode node, Void context) + { + printNode(node, format("IndexSource[%s]", node.getIndexHandle()), NODE_COLORS.get(NodeType.INDEX_SOURCE)); + return null; + } + + @Override + public Void visitIndexJoin(IndexJoinNode node, Void context) + { + List joinExpressions = new ArrayList<>(); + for (IndexJoinNode.EquiJoinClause clause : node.getCriteria()) { + joinExpressions.add(new ComparisonExpression(ComparisonExpression.Type.EQUAL, + new QualifiedNameReference(clause.getProbe().toQualifiedName()), + new QualifiedNameReference(clause.getIndex().toQualifiedName()))); + } + + String criteria = Joiner.on(" AND ").join(joinExpressions); + String joinLabel = format("%sIndexJoin", node.getType().getJoinLabel()); + printNode(node, joinLabel, criteria, NODE_COLORS.get(NodeType.JOIN)); + + node.getProbeSource().accept(this, context); + node.getIndexSource().accept(this, context); + + return null; + } + + private void printNode(PlanNode node, String label, String color) + { + String nodeId = idGenerator.getNodeId(node); + label = escapeSpecialCharacters(label); + output.append(nodeId) + .append(format("[label=\"{%s}\", style=\"rounded, filled\", shape=record, fillcolor=%s]", label, color)) + .append(';') + .append('\n'); + } + + private void printNode(PlanNode node, String label, String details, String color) + { + if (details.isEmpty()) { + printNode(node, label, color); + } + else { + String nodeId = idGenerator.getNodeId(node); + label = escapeSpecialCharacters(label); + details = escapeSpecialCharacters(details); + output.append(nodeId) + .append(format("[label=\"{%s|%s}\", style=\"rounded, filled\", shape=record, fillcolor=%s]", label, details, color)) + .append(';') + .append('\n'); + } + } + + private static String getColumns(OutputNode node) + { + Iterator columnNames = node.getColumnNames().iterator(); + String columns = ""; + int nameWidth = 0; + while (columnNames.hasNext()) { + String columnName = columnNames.next(); + columns += columnName; + nameWidth += columnName.length(); + if (columnNames.hasNext()) { + columns += ", "; + } + if (nameWidth >= MAX_NAME_WIDTH) { + columns += "\\n"; + nameWidth = 0; + } + } + return columns; + } + + /** + * Escape characters that are special to graphviz. + */ + private static String escapeSpecialCharacters(String label) + { + return label + .replace("<", "\\<") + .replace(">", "\\>") + .replace("\"", "\\\""); + } + } + + private static class EdgePrinter + extends PlanVisitor + { + private final StringBuilder output; + private final Map fragmentsById; + private final PlanNodeIdGenerator idGenerator; + + public EdgePrinter(StringBuilder output, Map fragmentsById, PlanNodeIdGenerator idGenerator) + { + this.output = output; + this.fragmentsById = ImmutableMap.copyOf(fragmentsById); + this.idGenerator = idGenerator; + } + + @Override + protected Void visitPlan(PlanNode node, Void context) + { + for (PlanNode child : node.getSources()) { + printEdge(node, child); + + child.accept(this, context); + } + + return null; + } + + @Override + public Void visitRemoteSource(RemoteSourceNode node, Void context) + { + for (PlanFragmentId planFragmentId : node.getSourceFragmentIds()) { + PlanFragment target = fragmentsById.get(planFragmentId); + printEdge(node, target.getRoot()); + } + + return null; + } + + private void printEdge(PlanNode from, PlanNode to) + { + String fromId = idGenerator.getNodeId(from); + String toId = idGenerator.getNodeId(to); + + output.append(fromId) + .append(" -> ") + .append(toId) + .append(';') + .append('\n'); + } + } + + private static class PlanNodeIdGenerator + { + private final Map planNodeIds; + private int idCount; + + public PlanNodeIdGenerator() + { + planNodeIds = new HashMap<>(); + } + + public String getNodeId(PlanNode from) + { + int nodeId; + + if (planNodeIds.containsKey(from)) { + nodeId = planNodeIds.get(from); + } + else { + idCount++; + planNodeIds.put(from, idCount); + nodeId = idCount; + } + return ("plannode_" + nodeId); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/util/ImmutableCollectors.java b/presto-main/src/main/java/com/facebook/presto/util/ImmutableCollectors.java new file mode 100644 index 00000000..66924f7d --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/util/ImmutableCollectors.java @@ -0,0 +1,63 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.util; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMultiset; +import com.google.common.collect.ImmutableSet; + +import java.util.stream.Collector; + +public final class ImmutableCollectors +{ + private ImmutableCollectors() {} + + public static Collector> toImmutableList() + { + return Collector., ImmutableList>of( + ImmutableList.Builder::new, + ImmutableList.Builder::add, + (left, right) -> { + left.addAll(right.build()); + return left; + }, + ImmutableList.Builder::build); + } + + public static Collector> toImmutableSet() + { + return Collector., ImmutableSet>of( + ImmutableSet.Builder::new, + ImmutableSet.Builder::add, + (left, right) -> { + left.addAll(right.build()); + return left; + }, + ImmutableSet.Builder::build, + Collector.Characteristics.UNORDERED); + } + + public static Collector> toImmutableMultiset() + { + return Collector., ImmutableMultiset>of( + ImmutableMultiset.Builder::new, + ImmutableMultiset.Builder::add, + (left, right) -> { + left.addAll(right.build()); + return left; + }, + ImmutableMultiset.Builder::build, + Collector.Characteristics.UNORDERED); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/util/JsonPlanPrinter.java b/presto-main/src/main/java/com/facebook/presto/util/JsonPlanPrinter.java new file mode 100644 index 00000000..430a0dc5 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/util/JsonPlanPrinter.java @@ -0,0 +1,286 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.util; + +import com.facebook.presto.execution.Column; +import com.facebook.presto.execution.Input; +import com.facebook.presto.execution.SimpleDomain; +import com.facebook.presto.spi.ColumnHandle; +import com.facebook.presto.metadata.Metadata; +import com.facebook.presto.metadata.TableMetadata; +import com.facebook.presto.spi.ColumnMetadata; +import com.facebook.presto.spi.Domain; +import com.facebook.presto.spi.TupleDomain; +import com.facebook.presto.sql.planner.Symbol; +import com.facebook.presto.sql.planner.plan.AggregationNode; +import com.facebook.presto.sql.planner.plan.FilterNode; +import com.facebook.presto.sql.planner.plan.IndexJoinNode; +import com.facebook.presto.sql.planner.plan.IndexSourceNode; +import com.facebook.presto.sql.planner.plan.JoinNode; +import com.facebook.presto.sql.planner.plan.LimitNode; +import com.facebook.presto.sql.planner.plan.MarkDistinctNode; +import com.facebook.presto.sql.planner.plan.OutputNode; +import com.facebook.presto.sql.planner.plan.PlanNode; +import com.facebook.presto.sql.planner.plan.PlanVisitor; +import com.facebook.presto.sql.planner.plan.ProjectNode; +import com.facebook.presto.sql.planner.plan.RemoteSourceNode; +import com.facebook.presto.sql.planner.plan.RowNumberNode; +import com.facebook.presto.sql.planner.plan.SemiJoinNode; +import com.facebook.presto.sql.planner.plan.SortNode; +import com.facebook.presto.sql.planner.plan.TableCommitNode; +import com.facebook.presto.sql.planner.plan.TableScanNode; +import com.facebook.presto.sql.planner.plan.TableWriterNode; +import com.facebook.presto.sql.planner.plan.TopNNode; +import com.facebook.presto.sql.planner.plan.TopNRowNumberNode; +import com.facebook.presto.sql.planner.plan.UnionNode; +import com.facebook.presto.sql.planner.plan.WindowNode; +import com.google.common.collect.ImmutableList; +import io.airlift.json.JsonCodec; + +import java.util.Map; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkNotNull; + +public final class JsonPlanPrinter +{ + private static final JsonCodec CODEC = JsonCodec.jsonCodec(QueryExplanation.class); + private final ImmutableList.Builder inputBuilder = ImmutableList.builder(); + + private JsonPlanPrinter(PlanNode plan, Metadata metadata) + { + checkNotNull(plan, "plan is null"); + checkNotNull(metadata, "metadata is null"); + SourceVisitor visitor = new SourceVisitor(metadata); + plan.accept(visitor, null); + } + + public static String getPlan(PlanNode plan, Metadata metadata) + { + return new JsonPlanPrinter(plan, metadata).toString(); + } + + @Override + public String toString() + { + return CODEC.toJson(new QueryExplanation(inputBuilder.build())); + } + + private class SourceVisitor + extends PlanVisitor + { + private final Metadata metadata; + + public SourceVisitor(Metadata metadata) + { + this.metadata = checkNotNull(metadata); + } + + @Override + public Void visitJoin(JoinNode node, Void context) + { + node.getLeft().accept(this, null); + node.getRight().accept(this, null); + + return null; + } + + @Override + public Void visitSemiJoin(SemiJoinNode node, Void context) + { + node.getSource().accept(this, null); + node.getFilteringSource().accept(this, null); + + return null; + } + + @Override + public Void visitIndexJoin(IndexJoinNode node, Void context) + { + node.getProbeSource().accept(this, null); + node.getIndexSource().accept(this, null); + + return null; + } + + @Override + public Void visitLimit(LimitNode node, Void context) + { + return processChildren(node); + } + + @Override + public Void visitAggregation(AggregationNode node, Void context) + { + return processChildren(node); + } + + @Override + public Void visitMarkDistinct(MarkDistinctNode node, Void context) + { + return processChildren(node); + } + + @Override + public Void visitWindow(final WindowNode node, Void context) + { + return processChildren(node); + } + + @Override + public Void visitRowNumber(final RowNumberNode node, Void context) + { + return processChildren(node); + } + + @Override + public Void visitTopNRowNumber(final TopNRowNumberNode node, Void context) + { + return processChildren(node); + } + + @Override + public Void visitTableScan(TableScanNode node, Void context) + { + TableMetadata tableMetadata = metadata.getTableMetadata(node.getTable()); + + ImmutableList.Builder columnBuilder = ImmutableList.builder(); + + for (Map.Entry entry : node.getAssignments().entrySet()) { + ColumnMetadata columnMetadata = metadata.getColumnMetadata(node.getTable(), entry.getValue()); + TupleDomain constraint = node.getCurrentConstraint(); + Domain domain = null; + if (constraint.isNone()) { + domain = Domain.none(columnMetadata.getType().getJavaType()); + } + else if (constraint.getDomains().containsKey(entry.getValue())) { + domain = constraint.getDomains().get(entry.getValue()); + } + Column column = new Column( + columnMetadata.getName(), + columnMetadata.getType().toString(), + Optional.empty()); + Optional.ofNullable(SimpleDomain.fromDomain(domain)); + columnBuilder.add(column); + } + Input input = new Input( + tableMetadata.getConnectorId(), + tableMetadata.getTable().getSchemaName(), + tableMetadata.getTable().getTableName(), + columnBuilder.build()); + inputBuilder.add(input); + return null; + } + + @Override + public Void visitIndexSource(IndexSourceNode node, Void context) + { + TableMetadata tableMetadata = metadata.getTableMetadata(node.getTableHandle()); + + ImmutableList.Builder columnBuilder = ImmutableList.builder(); + + for (Map.Entry entry : node.getAssignments().entrySet()) { + ColumnMetadata columnMetadata = metadata.getColumnMetadata(node.getTableHandle(), entry.getValue()); + Domain domain = null; + if (!node.getEffectiveTupleDomain().isNone() && node.getEffectiveTupleDomain().getDomains().containsKey(entry.getValue())) { + domain = node.getEffectiveTupleDomain().getDomains().get(entry.getValue()); + } + else if (node.getEffectiveTupleDomain().isNone()) { + domain = Domain.none(columnMetadata.getType().getJavaType()); + } + Column column = new Column( + columnMetadata.getName(), + columnMetadata.getType().toString(), + Optional.ofNullable(SimpleDomain.fromDomain(domain))); + columnBuilder.add(column); + } + Input input = new Input( + tableMetadata.getConnectorId(), + tableMetadata.getTable().getSchemaName(), + tableMetadata.getTable().getTableName(), + columnBuilder.build()); + inputBuilder.add(input); + return null; + } + + @Override + public Void visitFilter(FilterNode node, Void context) + { + return processChildren(node); + } + + @Override + public Void visitProject(ProjectNode node, Void context) + { + return processChildren(node); + } + + @Override + public Void visitOutput(OutputNode node, Void context) + { + return processChildren(node); + } + + @Override + public Void visitTopN(final TopNNode node, Void context) + { + return processChildren(node); + } + + @Override + public Void visitSort(final SortNode node, Void context) + { + return processChildren(node); + } + + @Override + public Void visitRemoteSource(RemoteSourceNode node, Void context) + { + return null; + } + + @Override + public Void visitUnion(UnionNode node, Void context) + { + return processChildren(node); + } + + @Override + public Void visitTableWriter(TableWriterNode node, Void context) + { + return processChildren(node); + } + + @Override + public Void visitTableCommit(TableCommitNode node, Void context) + { + return processChildren(node); + } + + @Override + protected Void visitPlan(PlanNode node, Void context) + { + throw new UnsupportedOperationException("not yet implemented: " + node.getClass().getName()); + } + + private Void processChildren(PlanNode node) + { + for (PlanNode child : node.getSources()) { + child.accept(this, null); + } + + return null; + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/util/MoreFutures.java b/presto-main/src/main/java/com/facebook/presto/util/MoreFutures.java new file mode 100644 index 00000000..65c61160 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/util/MoreFutures.java @@ -0,0 +1,144 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.util; + +import com.google.common.base.Throwables; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import io.airlift.units.Duration; + +import javax.annotation.Nullable; + +import java.lang.ref.WeakReference; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static com.google.common.base.Preconditions.checkNotNull; + +public final class MoreFutures +{ + private MoreFutures() + { + } + + public static T tryGetUnchecked(Future future) + { + checkNotNull(future, "future is null"); + if (!future.isDone()) { + return null; + } + + try { + return future.get(0, TimeUnit.MILLISECONDS); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw Throwables.propagate(e); + } + catch (ExecutionException e) { + Throwable cause = e.getCause(); + if (cause == null) { + cause = e; + } + throw Throwables.propagate(cause); + } + catch (TimeoutException e) { + // this mean that isDone() does not agree with get() + // this should not happen for reasonable implementations of Future + throw Throwables.propagate(e); + } + } + + public static ListenableFuture addTimeout(final ListenableFuture future, final Callable timeoutTask, Duration timeout, ScheduledExecutorService executorService) + { + // if the future is already complete, just return it + if (future.isDone()) { + return future; + } + + // wrap the future, so we can set the result directly + final SettableFuture settableFuture = SettableFuture.create(); + + // schedule a task to complete the future when the time expires + final ScheduledFuture timeoutTaskFuture = executorService.schedule(new TimeoutFutureTask<>(settableFuture, timeoutTask, future), timeout.toMillis(), TimeUnit.MILLISECONDS); + + // add a listener to the core future, which simply updates the settable future + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(@Nullable T result) + { + settableFuture.set(result); + timeoutTaskFuture.cancel(true); + } + + @Override + public void onFailure(Throwable t) + { + settableFuture.setException(t); + timeoutTaskFuture.cancel(true); + } + }, executorService); + + return settableFuture; + } + + private static class TimeoutFutureTask + implements Runnable + { + private final SettableFuture settableFuture; + private final Callable timeoutTask; + private final WeakReference> futureReference; + + public TimeoutFutureTask(SettableFuture settableFuture, Callable timeoutTask, ListenableFuture future) + { + this.settableFuture = settableFuture; + this.timeoutTask = timeoutTask; + + // the scheduled executor can hold on to the timeout task for a long time, and + // the future can reference large expensive objects. Since we are only interested + // in canceling this future on a timeout, only hold a weak reference to the future + this.futureReference = new WeakReference>(future); + } + + @Override + public void run() + { + if (settableFuture.isDone()) { + return; + } + + // run the timeout task and set the result into the future + try { + T result = timeoutTask.call(); + settableFuture.set(result); + } + catch (Throwable t) { + settableFuture.setException(t); + } + + // cancel the original future, if it still exists + Future future = futureReference.get(); + if (future != null) { + future.cancel(true); + } + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/util/NodeIdUserAgentRequestFilter.java b/presto-main/src/main/java/com/facebook/presto/util/NodeIdUserAgentRequestFilter.java new file mode 100644 index 00000000..e47d2932 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/util/NodeIdUserAgentRequestFilter.java @@ -0,0 +1,64 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.util; + +import io.airlift.http.client.HttpRequestFilter; +import io.airlift.http.client.Request; +import io.airlift.node.NodeInfo; + +import javax.inject.Inject; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.net.HttpHeaders.USER_AGENT; +import static io.airlift.http.client.Request.Builder.fromRequest; + +public class NodeIdUserAgentRequestFilter + implements HttpRequestFilter +{ + private final String nodeId; + + @Inject + public NodeIdUserAgentRequestFilter(NodeInfo nodeInfo) + { + checkNotNull(nodeInfo, "nodeInfo is null"); + this.nodeId = checkNotNull(nodeInfo.getNodeId(), "nodeId is null"); + } + + @Override + public Request filterRequest(Request request) + { + checkNotNull(request, "request is null"); + return fromRequest(request) + .addHeader(USER_AGENT, nodeId) + .build(); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if ((obj == null) || (getClass() != obj.getClass())) { + return false; + } + return nodeId.equals(((NodeIdUserAgentRequestFilter) obj).nodeId); + } + + @Override + public int hashCode() + { + return nodeId.hashCode(); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/util/QueryExplanation.java b/presto-main/src/main/java/com/facebook/presto/util/QueryExplanation.java new file mode 100644 index 00000000..54045971 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/util/QueryExplanation.java @@ -0,0 +1,41 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.util; + +import com.facebook.presto.execution.Input; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; + +import javax.annotation.concurrent.Immutable; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Immutable +public class QueryExplanation +{ + private final List inputs; + + public QueryExplanation(List inputs) + { + this.inputs = ImmutableList.copyOf(checkNotNull(inputs, "inputs is null")); + } + + @JsonProperty + public List getInputs() + { + return inputs; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/util/Reflection.java b/presto-main/src/main/java/com/facebook/presto/util/Reflection.java new file mode 100644 index 00000000..78a73222 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/util/Reflection.java @@ -0,0 +1,58 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.util; + +import com.facebook.presto.spi.PrestoException; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +import static com.facebook.presto.spi.StandardErrorCode.INTERNAL_ERROR; + +public final class Reflection +{ + private Reflection() {} + + public static Field field(Class clazz, String name) + { + try { + return clazz.getField(name); + } + catch (NoSuchFieldException e) { + throw new PrestoException(INTERNAL_ERROR, e); + } + } + + public static Method method(Class clazz, String name, Class... parameterTypes) + { + try { + return clazz.getMethod(name, parameterTypes); + } + catch (NoSuchMethodException e) { + throw new PrestoException(INTERNAL_ERROR, e); + } + } + + public static MethodHandle methodHandle(Class clazz, String name, Class... parameterTypes) + { + try { + return MethodHandles.lookup().unreflect(clazz.getMethod(name, parameterTypes)); + } + catch (IllegalAccessException | NoSuchMethodException e) { + throw new PrestoException(INTERNAL_ERROR, e); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/util/ThreadLocalCache.java b/presto-main/src/main/java/com/facebook/presto/util/ThreadLocalCache.java new file mode 100644 index 00000000..29e86473 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/util/ThreadLocalCache.java @@ -0,0 +1,74 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.util; + +import javax.annotation.Nonnull; + +import java.util.LinkedHashMap; +import java.util.Map; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Provides a ThreadLocal cache with a maximum cache size per thread. + * Values must not be null. + * + * @param - cache key type + * @param - cache value type + */ +public abstract class ThreadLocalCache +{ + @SuppressWarnings("ThreadLocalNotStaticFinal") + private final ThreadLocal> cache; + + public ThreadLocalCache(final int maxSizePerThread) + { + checkArgument(maxSizePerThread > 0, "max size must be greater than zero"); + cache = new ThreadLocal>() + { + @SuppressWarnings({"CloneableClassWithoutClone", "ClassExtendsConcreteCollection"}) + @Override + protected LinkedHashMap initialValue() + { + return new LinkedHashMap() + { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) + { + return size() > maxSizePerThread; + } + }; + } + }; + } + + @Nonnull + protected abstract V load(K key); + + public final V get(K key) + { + LinkedHashMap map = cache.get(); + + V value = map.get(key); + if (value != null) { + return value; + } + + value = load(key); + checkNotNull(value, "value must not be null"); + map.put(key, value); + return value; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/util/Threads.java b/presto-main/src/main/java/com/facebook/presto/util/Threads.java new file mode 100644 index 00000000..852149d5 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/util/Threads.java @@ -0,0 +1,69 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.util; + +import com.google.common.base.Throwables; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.SettableFuture; + +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public final class Threads +{ + private static final Class GUAVA_SAME_THREAD_EXECUTOR_CLASS = MoreExecutors.sameThreadExecutor().getClass(); + + private Threads() {} + + public static Executor checkNotSameThreadExecutor(Executor executor, String name) + { + checkNotNull(executor, "%s is null", name); + checkArgument(!isSameThreadExecutor(executor), "%s is a same thread executor", name); + return executor; + } + + public static boolean isSameThreadExecutor(Executor executor) + { + checkNotNull(executor, "executor is null"); + if (executor.getClass() == GUAVA_SAME_THREAD_EXECUTOR_CLASS) { + return true; + } + + final Thread thisThread = Thread.currentThread(); + final SettableFuture isSameThreadExecutor = SettableFuture.create(); + executor.execute(new Runnable() + { + @Override + public void run() + { + isSameThreadExecutor.set(thisThread == Thread.currentThread()); + } + }); + try { + return Futures.get(isSameThreadExecutor, 10, TimeUnit.SECONDS, Exception.class); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw Throwables.propagate(e); + } + catch (Exception e) { + throw Throwables.propagate(e); + } + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/util/Types.java b/presto-main/src/main/java/com/facebook/presto/util/Types.java new file mode 100644 index 00000000..1845d233 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/util/Types.java @@ -0,0 +1,33 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.util; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public final class Types +{ + private Types() {} + + public static B checkType(A value, Class target, String name) + { + checkNotNull(value, "%s is null", name); + checkArgument(target.isInstance(value), + "%s must be of type %s, not %s", + name, + target.getName(), + value.getClass().getName()); + return target.cast(value); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/util/array/BigArrays.java b/presto-main/src/main/java/com/facebook/presto/util/array/BigArrays.java new file mode 100644 index 00000000..534fe5e1 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/util/array/BigArrays.java @@ -0,0 +1,65 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.util.array; + +// Note: this code was forked from fastutil (http://fastutil.di.unimi.it/) +// Copyright (C) 2010-2013 Sebastiano Vigna +public final class BigArrays +{ + private BigArrays() + { + } + + /** + * Initial number of segments to support in array. + */ + public static final int INITIAL_SEGMENTS = 1024; + + /** + * The shift used to compute the segment associated with an index (equivalently, the logarithm of the segment size). + */ + public static final int SEGMENT_SHIFT = 10; + + /** + * Size of a single segment of a BigArray + */ + public static final int SEGMENT_SIZE = 1 << SEGMENT_SHIFT; + + /** + * The mask used to compute the offset associated to an index. + */ + public static final int SEGMENT_MASK = SEGMENT_SIZE - 1; + + /** + * Computes the segment associated with a given index. + * + * @param index an index into a big array. + * @return the associated segment. + */ + public static int segment(long index) + { + return (int) (index >>> SEGMENT_SHIFT); + } + + /** + * Computes the offset associated with a given index. + * + * @param index an index into a big array. + * @return the associated offset (in the associated {@linkplain #segment(long) segment}). + */ + public static int offset(long index) + { + return (int) (index & SEGMENT_MASK); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/util/array/BlockBigArray.java b/presto-main/src/main/java/com/facebook/presto/util/array/BlockBigArray.java new file mode 100644 index 00000000..8db07539 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/util/array/BlockBigArray.java @@ -0,0 +1,77 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.util.array; + +import com.facebook.presto.spi.block.Block; + +public final class BlockBigArray +{ + private final ObjectBigArray array; + private long sizeOfBlocks; + + public BlockBigArray() + { + array = new ObjectBigArray<>(); + } + + public BlockBigArray(Block block) + { + array = new ObjectBigArray<>(block); + } + + /** + * Returns the size of this big array in bytes. + */ + public long sizeOf() + { + return array.sizeOf() + sizeOfBlocks; + } + + /** + * Returns the element of this big array at specified index. + * + * @param index a position in this big array. + * @return the element of this big array at the specified position. + */ + public Block get(long index) + { + return array.get(index); + } + + /** + * Sets the element of this big array at specified index. + * + * @param index a position in this big array. + */ + public void set(long index, Block value) + { + Block currentValue = array.get(index); + if (currentValue != null) { + sizeOfBlocks -= currentValue.getRetainedSizeInBytes(); + } + if (value != null) { + sizeOfBlocks += value.getRetainedSizeInBytes(); + } + array.set(index, value); + } + + /** + * Ensures this big array is at least the specified length. If the array is smaller, segments + * are added until the array is larger then the specified length. + */ + public void ensureCapacity(long length) + { + array.ensureCapacity(length); + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/util/array/BooleanBigArray.java b/presto-main/src/main/java/com/facebook/presto/util/array/BooleanBigArray.java new file mode 100644 index 00000000..78f6c48f --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/util/array/BooleanBigArray.java @@ -0,0 +1,121 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.util.array; + +import io.airlift.slice.SizeOf; + +import java.util.Arrays; + +import static com.facebook.presto.util.array.BigArrays.INITIAL_SEGMENTS; +import static com.facebook.presto.util.array.BigArrays.SEGMENT_SIZE; +import static com.facebook.presto.util.array.BigArrays.offset; +import static com.facebook.presto.util.array.BigArrays.segment; +import static io.airlift.slice.SizeOf.sizeOfBooleanArray; + +// Note: this code was forked from fastutil (http://fastutil.di.unimi.it/) +// Copyright (C) 2010-2013 Sebastiano Vigna +public final class BooleanBigArray +{ + private static final long SIZE_OF_SEGMENT = sizeOfBooleanArray(SEGMENT_SIZE); + + private final boolean initialValue; + + private boolean[][] array; + private int capacity; + private int segments; + + /** + * Creates a new big array containing one initial segment + */ + public BooleanBigArray() + { + this(false); + } + + public BooleanBigArray(boolean initialValue) + { + this.initialValue = initialValue; + array = new boolean[INITIAL_SEGMENTS][]; + allocateNewSegment(); + } + + /** + * Returns the size of this big array in bytes. + */ + public long sizeOf() + { + return SizeOf.sizeOf(array) + (segments * SIZE_OF_SEGMENT); + } + + /** + * Returns the element of this big array at specified index. + * + * @param index a position in this big array. + * @return the element of this big array at the specified position. + */ + public boolean get(long index) + { + return array[segment(index)][offset(index)]; + } + + /** + * Sets the element of this big array at specified index. + * + * @param index a position in this big array. + */ + public void set(long index, boolean value) + { + array[segment(index)][offset(index)] = value; + } + + /** + * Ensures this big array is at least the specified length. If the array is smaller, segments + * are added until the array is larger then the specified length. + */ + public void ensureCapacity(long length) + { + if (capacity > length) { + return; + } + + grow(length); + } + + private void grow(long length) + { + // how many segments are required to get to the length? + int requiredSegments = segment(length) + 1; + + // grow base array if necessary + if (array.length < requiredSegments) { + array = Arrays.copyOf(array, requiredSegments); + } + + // add new segments + while (segments < requiredSegments) { + allocateNewSegment(); + } + } + + private void allocateNewSegment() + { + boolean[] newSegment = new boolean[SEGMENT_SIZE]; + if (initialValue) { + Arrays.fill(newSegment, initialValue); + } + array[segments] = newSegment; + capacity += SEGMENT_SIZE; + segments++; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/util/array/ByteBigArray.java b/presto-main/src/main/java/com/facebook/presto/util/array/ByteBigArray.java new file mode 100644 index 00000000..bbd3004d --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/util/array/ByteBigArray.java @@ -0,0 +1,121 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.util.array; + +import io.airlift.slice.SizeOf; + +import java.util.Arrays; + +import static com.facebook.presto.util.array.BigArrays.INITIAL_SEGMENTS; +import static com.facebook.presto.util.array.BigArrays.SEGMENT_SIZE; +import static com.facebook.presto.util.array.BigArrays.offset; +import static com.facebook.presto.util.array.BigArrays.segment; +import static io.airlift.slice.SizeOf.sizeOfByteArray; + +// Note: this code was forked from fastutil (http://fastutil.di.unimi.it/) +// Copyright (C) 2010-2013 Sebastiano Vigna +public final class ByteBigArray +{ + private static final long SIZE_OF_SEGMENT = sizeOfByteArray(SEGMENT_SIZE); + + private final byte initialValue; + + private byte[][] array; + private int capacity; + private int segments; + + /** + * Creates a new big array containing one initial segment + */ + public ByteBigArray() + { + this((byte) 0); + } + + public ByteBigArray(byte initialValue) + { + this.initialValue = initialValue; + array = new byte[INITIAL_SEGMENTS][]; + allocateNewSegment(); + } + + /** + * Returns the size of this big array in bytes. + */ + public long sizeOf() + { + return SizeOf.sizeOf(array) + (segments * SIZE_OF_SEGMENT); + } + + /** + * Returns the element of this big array at specified index. + * + * @param index a position in this big array. + * @return the element of this big array at the specified position. + */ + public byte get(long index) + { + return array[segment(index)][offset(index)]; + } + + /** + * Sets the element of this big array at specified index. + * + * @param index a position in this big array. + */ + public void set(long index, byte value) + { + array[segment(index)][offset(index)] = value; + } + + /** + * Ensures this big array is at least the specified length. If the array is smaller, segments + * are added until the array is larger then the specified length. + */ + public void ensureCapacity(long length) + { + if (capacity > length) { + return; + } + + grow(length); + } + + private void grow(long length) + { + // how many segments are required to get to the length? + int requiredSegments = segment(length) + 1; + + // grow base array if necessary + if (array.length < requiredSegments) { + array = Arrays.copyOf(array, requiredSegments); + } + + // add new segments + while (segments < requiredSegments) { + allocateNewSegment(); + } + } + + private void allocateNewSegment() + { + byte[] newSegment = new byte[SEGMENT_SIZE]; + if (initialValue != 0) { + Arrays.fill(newSegment, initialValue); + } + array[segments] = newSegment; + capacity += SEGMENT_SIZE; + segments++; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/util/array/DoubleBigArray.java b/presto-main/src/main/java/com/facebook/presto/util/array/DoubleBigArray.java new file mode 100644 index 00000000..174e8082 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/util/array/DoubleBigArray.java @@ -0,0 +1,135 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.util.array; + +import io.airlift.slice.SizeOf; + +import java.util.Arrays; + +import static com.facebook.presto.util.array.BigArrays.INITIAL_SEGMENTS; +import static com.facebook.presto.util.array.BigArrays.SEGMENT_SIZE; +import static com.facebook.presto.util.array.BigArrays.offset; +import static com.facebook.presto.util.array.BigArrays.segment; +import static io.airlift.slice.SizeOf.sizeOfDoubleArray; + +// Note: this code was forked from fastutil (http://fastutil.di.unimi.it/) +// Copyright (C) 2010-2013 Sebastiano Vigna +public final class DoubleBigArray +{ + private static final long SIZE_OF_SEGMENT = sizeOfDoubleArray(SEGMENT_SIZE); + + private final double initialValue; + + private double[][] array; + private int capacity; + private int segments; + + /** + * Creates a new big array containing one initial segment + */ + public DoubleBigArray() + { + this(0.0); + } + + /** + * Creates a new big array containing one initial segment filled with the specified default value + */ + public DoubleBigArray(double initialValue) + { + this.initialValue = initialValue; + array = new double[INITIAL_SEGMENTS][]; + allocateNewSegment(); + } + + /** + * Returns the size of this big array in bytes. + */ + public long sizeOf() + { + return SizeOf.sizeOf(array) + (segments * SIZE_OF_SEGMENT); + } + + /** + * Returns the element of this big array at specified index. + * + * @param index a position in this big array. + * @return the element of this big array at the specified position. + */ + public double get(long index) + { + return array[segment(index)][offset(index)]; + } + + /** + * Sets the element of this big array at specified index. + * + * @param index a position in this big array. + */ + public void set(long index, double value) + { + array[segment(index)][offset(index)] = value; + } + + /** + * Adds the specified value to the specified element of this big array. + * + * @param index a position in this big array. + * @param value the value + */ + public void add(long index, double value) + { + array[segment(index)][offset(index)] += value; + } + + /** + * Ensures this big array is at least the specified length. If the array is smaller, segments + * are added until the array is larger then the specified length. + */ + public void ensureCapacity(long length) + { + if (capacity > length) { + return; + } + + grow(length); + } + + private void grow(long length) + { + // how many segments are required to get to the length? + int requiredSegments = segment(length) + 1; + + // grow base array if necessary + if (array.length < requiredSegments) { + array = Arrays.copyOf(array, requiredSegments); + } + + // add new segments + while (segments < requiredSegments) { + allocateNewSegment(); + } + } + + private void allocateNewSegment() + { + double[] newSegment = new double[SEGMENT_SIZE]; + if (initialValue != 0.0) { + Arrays.fill(newSegment, initialValue); + } + array[segments] = newSegment; + capacity += SEGMENT_SIZE; + segments++; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/util/array/IntBigArray.java b/presto-main/src/main/java/com/facebook/presto/util/array/IntBigArray.java new file mode 100644 index 00000000..badf8a6b --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/util/array/IntBigArray.java @@ -0,0 +1,145 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.util.array; + +import io.airlift.slice.SizeOf; + +import java.util.Arrays; + +import static com.facebook.presto.util.array.BigArrays.INITIAL_SEGMENTS; +import static com.facebook.presto.util.array.BigArrays.SEGMENT_SIZE; +import static com.facebook.presto.util.array.BigArrays.offset; +import static com.facebook.presto.util.array.BigArrays.segment; +import static io.airlift.slice.SizeOf.sizeOfLongArray; + +// Note: this code was forked from fastutil (http://fastutil.di.unimi.it/) +// Copyright (C) 2010-2013 Sebastiano Vigna +public final class IntBigArray +{ + private static final long SIZE_OF_SEGMENT = sizeOfLongArray(SEGMENT_SIZE); + + private final int initialValue; + + private int[][] array; + private int capacity; + private int segments; + + /** + * Creates a new big array containing one initial segment + */ + public IntBigArray() + { + this(0); + } + + /** + * Creates a new big array containing one initial segment filled with the specified default value + */ + public IntBigArray(int initialValue) + { + this.initialValue = initialValue; + array = new int[INITIAL_SEGMENTS][]; + allocateNewSegment(); + } + + /** + * Returns the size of this big array in bytes. + */ + public long sizeOf() + { + return SizeOf.sizeOf(array) + (segments * SIZE_OF_SEGMENT); + } + + /** + * Returns the element of this big array at specified index. + * + * @param index a position in this big array. + * @return the element of this big array at the specified position. + */ + public int get(long index) + { + return array[segment(index)][offset(index)]; + } + + /** + * Sets the element of this big array at specified index. + * + * @param index a position in this big array. + */ + public void set(long index, int value) + { + array[segment(index)][offset(index)] = value; + } + + /** + * Increments the element of this big array at specified index. + * + * @param index a position in this big array. + */ + public void increment(long index) + { + array[segment(index)][offset(index)]++; + } + + /** + * Adds the specified value to the specified element of this big array. + * + * @param index a position in this big array. + * @param value the value + */ + public void add(long index, int value) + { + array[segment(index)][offset(index)] += value; + } + + /** + * Ensures this big array is at least the specified length. If the array is smaller, segments + * are added until the array is larger then the specified length. + */ + public void ensureCapacity(long length) + { + if (capacity > length) { + return; + } + + grow(length); + } + + private void grow(long length) + { + // how many segments are required to get to the length? + int requiredSegments = segment(length) + 1; + + // grow base array if necessary + if (array.length < requiredSegments) { + array = Arrays.copyOf(array, requiredSegments); + } + + // add new segments + while (segments < requiredSegments) { + allocateNewSegment(); + } + } + + private void allocateNewSegment() + { + int[] newSegment = new int[SEGMENT_SIZE]; + if (initialValue != 0) { + Arrays.fill(newSegment, initialValue); + } + array[segments] = newSegment; + capacity += SEGMENT_SIZE; + segments++; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/util/array/LongBigArray.java b/presto-main/src/main/java/com/facebook/presto/util/array/LongBigArray.java new file mode 100644 index 00000000..4e96bc63 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/util/array/LongBigArray.java @@ -0,0 +1,145 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.util.array; + +import io.airlift.slice.SizeOf; + +import java.util.Arrays; + +import static com.facebook.presto.util.array.BigArrays.INITIAL_SEGMENTS; +import static com.facebook.presto.util.array.BigArrays.SEGMENT_SIZE; +import static com.facebook.presto.util.array.BigArrays.offset; +import static com.facebook.presto.util.array.BigArrays.segment; +import static io.airlift.slice.SizeOf.sizeOfLongArray; + +// Note: this code was forked from fastutil (http://fastutil.di.unimi.it/) +// Copyright (C) 2010-2013 Sebastiano Vigna +public final class LongBigArray +{ + private static final long SIZE_OF_SEGMENT = sizeOfLongArray(SEGMENT_SIZE); + + private final long initialValue; + + private long[][] array; + private int capacity; + private int segments; + + /** + * Creates a new big array containing one initial segment + */ + public LongBigArray() + { + this(0L); + } + + /** + * Creates a new big array containing one initial segment filled with the specified default value + */ + public LongBigArray(long initialValue) + { + this.initialValue = initialValue; + array = new long[INITIAL_SEGMENTS][]; + allocateNewSegment(); + } + + /** + * Returns the size of this big array in bytes. + */ + public long sizeOf() + { + return SizeOf.sizeOf(array) + (segments * SIZE_OF_SEGMENT); + } + + /** + * Returns the element of this big array at specified index. + * + * @param index a position in this big array. + * @return the element of this big array at the specified position. + */ + public long get(long index) + { + return array[segment(index)][offset(index)]; + } + + /** + * Sets the element of this big array at specified index. + * + * @param index a position in this big array. + */ + public void set(long index, long value) + { + array[segment(index)][offset(index)] = value; + } + + /** + * Increments the element of this big array at specified index. + * + * @param index a position in this big array. + */ + public void increment(long index) + { + array[segment(index)][offset(index)]++; + } + + /** + * Adds the specified value to the specified element of this big array. + * + * @param index a position in this big array. + * @param value the value + */ + public void add(long index, long value) + { + array[segment(index)][offset(index)] += value; + } + + /** + * Ensures this big array is at least the specified length. If the array is smaller, segments + * are added until the array is larger then the specified length. + */ + public void ensureCapacity(long length) + { + if (capacity > length) { + return; + } + + grow(length); + } + + private void grow(long length) + { + // how many segments are required to get to the length? + int requiredSegments = segment(length) + 1; + + // grow base array if necessary + if (array.length < requiredSegments) { + array = Arrays.copyOf(array, requiredSegments); + } + + // add new segments + while (segments < requiredSegments) { + allocateNewSegment(); + } + } + + private void allocateNewSegment() + { + long[] newSegment = new long[SEGMENT_SIZE]; + if (initialValue != 0) { + Arrays.fill(newSegment, initialValue); + } + array[segments] = newSegment; + capacity += SEGMENT_SIZE; + segments++; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/util/array/ObjectBigArray.java b/presto-main/src/main/java/com/facebook/presto/util/array/ObjectBigArray.java new file mode 100644 index 00000000..8104bc92 --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/util/array/ObjectBigArray.java @@ -0,0 +1,121 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.util.array; + +import io.airlift.slice.SizeOf; + +import java.util.Arrays; + +import static com.facebook.presto.util.array.BigArrays.INITIAL_SEGMENTS; +import static com.facebook.presto.util.array.BigArrays.SEGMENT_SIZE; +import static com.facebook.presto.util.array.BigArrays.offset; +import static com.facebook.presto.util.array.BigArrays.segment; +import static io.airlift.slice.SizeOf.sizeOfObjectArray; + +// Note: this code was forked from fastutil (http://fastutil.di.unimi.it/) +// Copyright (C) 2010-2013 Sebastiano Vigna +public final class ObjectBigArray +{ + private static final long SIZE_OF_SEGMENT = sizeOfObjectArray(SEGMENT_SIZE); + + private final Object initialValue; + + private Object[][] array; + private int capacity; + private int segments; + + /** + * Creates a new big array containing one initial segment + */ + public ObjectBigArray() + { + this(null); + } + + public ObjectBigArray(Object initialValue) + { + this.initialValue = initialValue; + array = new Object[INITIAL_SEGMENTS][]; + allocateNewSegment(); + } + + /** + * Returns the size of this big array in bytes. + */ + public long sizeOf() + { + return SizeOf.sizeOf(array) + (segments * SIZE_OF_SEGMENT); + } + + /** + * Returns the element of this big array at specified index. + * + * @param index a position in this big array. + * @return the element of this big array at the specified position. + */ + public T get(long index) + { + return (T) array[segment(index)][offset(index)]; + } + + /** + * Sets the element of this big array at specified index. + * + * @param index a position in this big array. + */ + public void set(long index, T value) + { + array[segment(index)][offset(index)] = value; + } + + /** + * Ensures this big array is at least the specified length. If the array is smaller, segments + * are added until the array is larger then the specified length. + */ + public void ensureCapacity(long length) + { + if (capacity > length) { + return; + } + + grow(length); + } + + private void grow(long length) + { + // how many segments are required to get to the length? + int requiredSegments = segment(length) + 1; + + // grow base array if necessary + if (array.length < requiredSegments) { + array = Arrays.copyOf(array, requiredSegments); + } + + // add new segments + while (segments < requiredSegments) { + allocateNewSegment(); + } + } + + private void allocateNewSegment() + { + Object[] newSegment = new Object[SEGMENT_SIZE]; + if (initialValue != null) { + Arrays.fill(newSegment, initialValue); + } + array[segments] = newSegment; + capacity += SEGMENT_SIZE; + segments++; + } +} diff --git a/presto-main/src/main/java/com/facebook/presto/util/array/SliceBigArray.java b/presto-main/src/main/java/com/facebook/presto/util/array/SliceBigArray.java new file mode 100644 index 00000000..b417c65b --- /dev/null +++ b/presto-main/src/main/java/com/facebook/presto/util/array/SliceBigArray.java @@ -0,0 +1,77 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.facebook.presto.util.array; + +import io.airlift.slice.Slice; + +public final class SliceBigArray +{ + private final ObjectBigArray array; + private long sizeOfSlices; + + public SliceBigArray() + { + array = new ObjectBigArray<>(); + } + + public SliceBigArray(Slice slice) + { + array = new ObjectBigArray<>(slice); + } + + /** + * Returns the size of this big array in bytes. + */ + public long sizeOf() + { + return array.sizeOf() + sizeOfSlices; + } + + /** + * Returns the element of this big array at specified index. + * + * @param index a position in this big array. + * @return the element of this big array at the specified position. + */ + public Slice get(long index) + { + return array.get(index); + } + + /** + * Sets the element of this big array at specified index. + * + * @param index a position in this big array. + */ + public void set(long index, Slice value) + { + Slice currentValue = array.get(index); + if (currentValue != null) { + sizeOfSlices -= currentValue.length(); + } + if (value != null) { + sizeOfSlices += value.length(); + } + array.set(index, value); + } + + /** + * Ensures this big array is at least the specified length. If the array is smaller, segments + * are added until the array is larger then the specified length. + */ + public void ensureCapacity(long length) + { + array.ensureCapacity(length); + } +} diff --git a/presto-main/src/main/resources/com/facebook/presto/server/query-execution.html b/presto-main/src/main/resources/com/facebook/presto/server/query-execution.html new file mode 100644 index 00000000..576c53b6 --- /dev/null +++ b/presto-main/src/main/resources/com/facebook/presto/server/query-execution.html @@ -0,0 +1,204 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/presto-main/src/main/resources/com/facebook/presto/server/thread.html b/presto-main/src/main/resources/com/facebook/presto/server/thread.html new file mode 100644 index 00000000..fa515435 --- /dev/null +++ b/presto-main/src/main/resources/com/facebook/presto/server/thread.html @@ -0,0 +1,188 @@ + + + + + + + + + + + + + + + +
+ + + +
+ + + diff --git a/presto-main/src/main/resources/webapp/favicon.ico b/presto-main/src/main/resources/webapp/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..70ce7dab056e71265e8cff5e6f297afcca703360 GIT binary patch literal 318 zcmZQzU<5(|0RbS%!l1#(z#zuJz@P!d0zj+)#2|4HXaJKC0wf0m|NsAI_%k_*;qPN( jm=M`C69Wh$0T37HCIkR+nGsx&^O%vfki + + + + + + + + + + + + + + + + + + +
+
+
+ + + +
+
+ +
+
+ + + + + diff --git a/presto-main/src/main/resources/webapp/presto.png b/presto-main/src/main/resources/webapp/presto.png new file mode 100644 index 0000000000000000000000000000000000000000..93769ade102cb4bb7b5ccea68cf97c763819bbd9 GIT binary patch literal 19402 zcmV)|KzzT6P)4Tx07wn3mUmQB$rs1(d+Ci9h=7#PJ5r=euK^UK3N}I#NGKtM1QENkxB_cI zMFcClBBJ1009gxS!NMZgurJ7>qOOYt8!EpS_SK0rb zP~{79cmPNwHl57R;bmfc4C_015~&OT7lYZ27swiP_n?yj08&?H$H;V!lWWKsUzNiznprF~&MrXJ%$8cs<7E z$$#^c`ZwlslP7#~xv3L877O+z_TvCijwFM-aP;N>`N+(WWBD3wdf1bmlO7$0jiy-6 z@_5|9=y4VbIEpMJtg-z(i$ulCx?Yta+EcatR z^Mx_-i|? zCSpmE22E%LIp5=z31dRW{m8HbFwQBWAkH`or6Ch`Nyg}z9$kY}9yO5%fjD*|hkS0p zL~bDlgdhho@Ie4lNQNxP02#0$07Q@j66}o&*mlbVdDs~P@XwMgl!{V?GPW;vD+JbT zj+kd_&33S}cLc>9Q)mF*vPSO?!n*R^mfZs2R)p=R;&`(aDgdq#z= z>8VbhT$($k!z%{D=OsLIf5`0?Y#*7EdM?$pTmmE1(dH zVLfby3fKvIU_Ts!BhUyZ;VfK)tI!Fz;6C&~KfHiJcn`w}f{+jf!bG$XeZ&N@LL3lR z#0v>T!jUK>0ZBqq5iuf179*>WVx$zQKz1WF$S=qVY}FT6x0>S@f2^R>r2>pcjL?V$zG$XnY zLx>5)G~yy+32{5Ip4dX{BK8r7NFpvY5P*Tt+@XK25$(eo7vus8CEO?vw}$pR$m$jHhRNbUA%3y_()aze|71 zU@%M>K8!?$jIowc!#Kxy#Q3PhRI*j#DDjn+DOD;pC|y^2rA$#aQTA1yr<|``rhHWS zn(|8(ii)Xc#4Z)H~D%HPkewYQ$;eX;f;Q(&%B~Sf(rv zD}%L;b%b@B^?8!+B;QGbNoyw6O}akmgXUySA5DQ~k!HQ-EzQqbY^^}8bgd0qjam=2 z@!FQ!Gqo3J@6f)W{c^JUWRJR9ST=`7OOqtmW4q^qYJtSiyos(ViN zr5;PqM=wopliq2)0ez;vm%d1Ull~d~=WGqOFFS)>&OXl`G|({!F_0VVHs~-IHZ(Jg zHC$;}Z}`xNYUFB^VzkMq)o9RI-*~$5V&huldnObUR}-Ph7LzuU52j|O@usUy8%>{? zX_|4&^37_^9+)%Cz0I@DtITg&5G`CS(k-@IbXua8PL@K;O3Q0jsFkyo$ZES)mo>rK z&01_-W!-HfwW+n~wPo3c+pe^2w0$+jcuL}w4O7~te6w@1%dp#Pci&#kKGc4t zeY5==2TKQ@L#4w_M}}jN<1)u4$JbM>rV6I+n0m*F=``JGwNtCpS7#SzsdJt4fQzw9 zlFK%iZdY~JNY@hAOKt==KeuIWC*3}|JG;x=kGTKkVdIhRalqr*G_z@fX?v&jc^Z0h zJ$HNdc(J{bymoo@cpG?gy{o(*`xyD~efIf0^|kO#^F8SM%FoVE>UZ4lgTK4~691L} zTtIL@Q9wtaT3~EoMc|_#!=Til+MvN;r{G1wEg^)Eu#gQQ-5gyGk5j`L40Q=z5_&$2 z9u^%|5%xITGF%$oJRO}LI(_5x2Qy4&h-WlLKm;dZW5mNqv&ihoUuP0$M$W99`6S9d zYH?IsG&6dB^nvL2F@7=YV(!P9$I4?{AoB!XKqfD zOdTs`+U4f*ALPBcUb&UI9~Zhwks|x{!+p#x$=|!Pvt+2tW8(4d&wD%S7`&uoo?%5x;|9p*0P0azF1M6zhT6yij!8r#z4+R`*sfEyQY za&BI|HRD#-?bzFQy61HF+~MDOb~oeh>wCHPhVL(bKzLB}P~~CSBb`T8zgzrX-{ahK zsyCpw{c-f;`+dB=7k}jZ@uk1u3FArGQ~jqk&m5ke90(fdd_Mbm{|m{B;g^Mfs{Xm{ zmHDe$eWV4T5qf0O?}ro6gl+hedhb&4@DofKJNeI`l;=6{O6~` z3%-!Plz+AO+Wal-+k=tJkr4@3${no*aM%+~Ndb6M4ZujiYKKk$s?t~uJgPv(>UNA3 z^}C`g+CO-#P9B|c2B`J{h+Pg4-U6@#>n0e}u~AXZ#rOc~=s3|ai=ySEI6901LYHEt z$;XkAx4HnNW`M8Kk&)r;BO_n;Vzv5RfR^mB+FPL}ssNOlvFy;o_HGRS(f$K$CjIT` z5=$fi000SaNLh0L01FcU01FcV0GgZ_002M$NklJJ+mc%#w?J+IERZ|4d>rH!$SpABERZ|4A=jmOgK`Vx7RVi2J_d3NvC={Ijoc`7<${pL9)nB=`xdqA=I9(6ePj#+g>KxaA z11e|qK?Hfo)s9OxU6yce9QT3f0TGcqwgEA7Udcb11#mJr0nRTA`^*G|(_IlqGsC&g z+0J#$p5i)iK&1CYC8tXc@{o%>e=O(5G4Z~rqrYVo@=fslZQ@-1KbZw^YH4ql$V#ut zJQ7lr@J4G@M&$MwHKSt%T#65tl!FraMR#?W;Q1*L?8r za&}i;e!ROMnat>LeznRUDE{2B4V0zxdj1(Kz-$JmGg>%7m?XR*TrAuo^oq=B?fjA( z-O%a2Ct(K)>j`fPH*|EkuRk>2Eiyi()kJb&1$nB{QJ1)3?MvOvhGusZ3v4B|sMzRS z+X}Zr3@6Px!JTKkHB~Tx%xlE7ascg-SCw0!UJHay?T->NL--frFyVUP2mz|=ob!%E zGbx2dP1(+CT-O~fN$blIA@N;0{#-+&yJ(+(xM9rL;3umVI_>nROzS)Dn@z*rQ5|ir zy`$Bw>}YpwZ7b!}ijrM$KRxt3w>J)OFt54QW#n3nmGkBn_$RS|_c3bgmxSGg_86@S zHQ#Zk31b9ibTyxMQ{pnayy!lv+0&HvwZ z0p>Yxp>3g1;Ep1vJu@_%%nD(NP!~NOJ1PlU>IDQ2r5sg6YhY0wb84){_&dFxt++MK z(7KZp(BHD=*65CDcZR`{k~_A+(GR&6YqkY&0d_9^CT2aH*!y?$P>Ec z{3Vjw8J?gtFd{w8`iqaJ)|k)1=VT2`ndAPJyxIX^O}dA z1#kl9v?eOPldxF0SKwv>POT<~BmPv{^#wOst9Zw#!i~l7isF|)uyHh7iyfQkky<`0 z(QYE>&gUoe6!011d#`R*G|6!;k>|Xjt=*k`<(J)jy3=#Vwz_*DuYagn0OtlJ z9ZR1%wJgV#l^&^6{eN>>Q)jx7O(nOpuBAunPU*{+92Z?mTYj2Ef0s0Fxq_q((u(v; zTJG5T#jLr|+ydn_H%!3^Ud>6Ko$Mdw$mHi4 zHQQ1ZR-Wb5aA;FzyJA=dUdq}|hEPs_$8*Ql-xkj0=N9PM0ys3A(cu8O;Xs(_c$DW! z1^sRTJ5CGXaeDYRxz!3sCw^{zQdc*k+>|-4_{tNmFnT*znln|-Fl8H6QU^IjZfy8H ztO%VtRZepDz}`j89b32V%oB1845$U<7t$Sz(ktavrDP%4J38q_w~e+(Z7HE8?NWH+ z-i_P>gd`nPRf1!tu+?{ni_g2saV`-T?JnM^YF;1{n2U?9TNuEf7WNLn#TOhZGdKD-9FmbHNE3)f3tuGdfE3`EVa4T&gFiJcH*R$-FsPot7TI= zvoF@o4!KY>KkV<-?UtTn~ zz)-Y+IV(Ni?>-SUuu)sFxN!iWy@ZKGL^U}!+o_;Jdo+yj<=f>tO)d$Ia+f^hJ5tZ6 z`TeDAFs+tsZ)u^;ZKpam*X1W0=0o{jlxB`4&DQXsycoc3|Ab-#K)GWZ03+vB)sdSOqG$P+Uy5RdoN@~cO$%_U z+tt#!iu0CTW;9H005c%yEa4pi-c+Zd^i-O2dc?n?(_Pc?j{8f)+FpK}q<6{%`Sm>A zP-xU8J7ey^19(uQumz8j(5LCZPnB5Q4gG5=(eRt%}zYcB)MZtvr3+pTVP-< zz|1Y_SXi^-(ykKW^o%kcLM6x5Q{Lpct~h$KE4?z;73NOXXZ}(gk2yCKnf4RC{=C_3 zCg=Hf!!S4R;v@ZwD^s0X+J01?jpN;LY4un`s|xx2UXI)b`50L3V}MD9K2YqT?|N3$ z$9ThYm$H7Ml2%rXdakTgmOEv3sllDL>NqSTKFy-i^C%bcTKu#OS)|JscF~Jt>aem? zeq0(p?d;N8p^PwYe6@sLcvT)SXUZ**TVOC)fLU5NJA0RoUk?;Z<_B5P~@Bz=O|*;Sc)uzfBa|^6?3)m$q3-n(UJ|m10-WIMA zelBP~fp;$6c_>~rnZr_8)Q*KyLvQK*o$*#JJi(}K zSSL%%v-Aq`B6~|A9^?hug7v$OM)!p)rn^_`b#A$1i)@vi*Zu4)a%wngbY*WTIuzy9 z+LeE(W_9}_l^0kN)L^+&v}a82VRX9RY`D z&QQ_HWDoS}I|utVuI(KOP7S%MNXK$+IMi+R%JALdjH>F;Y|ZyWCHX2Hn=vZ!J&7qmF#lzh8te!Dmycqr<(Dot>SdL9pP_7&P_(>Co=o8XMqxdNa3QSTNPMEyO|k^|72i&nEKCz72;}F^O-AS^GTX9g{vLrxJ~)3# zC*=6A!gqz5$u_{>Ki*xhJE6a6TvtD0TFm#(jZO1%a+=9CGn><0(eK~8iF-{Im!Em- zsY140@#w~-UvvxQq%P6RC(&F>dj1AT^pP07aaU>WO)ajuz0KX;w$fdP+Vz~clr3Ae5hXaFM`E zE1wtOt9rI3UOJe!acF0PE3IsI$F;xVE+btV<{OJGuBp)E8nyQJmbU4ibpIyxeF@&) z#r9;A`(E1$cafZ1qrQ>QqV=;TU3xYaoAF%AZQL(TCAN`x5|lr@3QIcMR=I$ zQ6FT_!9_=_{->qqj%}5F@>seB%&~o5*~|)7YR_I1Kb0|YBJD5aj?FC6|6!K%%1WoR z$m0hpBQGB#$|;0y8krq^XzIy6_g|85HTtEIX;) z7P;}t-&4t(ifL&>r+e)Faqf1dMVGljSuP>doMG(1&=+)}BYy67(&HjWtImB?G3&g{ zRPUBX7hm=zf77I_Pwv>dm@Yp^u|V$FQjD^OB!v#^2OPjES2N+FMGw?`R9w>0YLsX71RqL{60~kUO?Y))~Bc@(Jbh z|0jz7dxomtRQmTa@+u}5;`O*mi}a!7X8)-`-FeX7QdU}{Z!9$Gt-VFPz4tLq5kL9R z824(&2sPl6xDnjgAsym!l!q=LTS~CLj#4Z9 zH>b1PLbs!?Gw+tu8Ky6y@>=t78y4q8JFg_~c+1Vw-P9x0S>;RD3gU5Co^R?*H?pPV zZfj_EJ9o6}rIq?tX}5AbU#B~&diL!_W$yl~J@%;mlxcY>L9<~7T1><*w?I@`5tLOPOVM4 zmc0Jm%M^5TqEpVT;KsC-+{afN#Cmnda-iW-qM0xYNztY%K1!0e))z32R+jSeI$ln zx4uj(L3=y>hhvIvdl>NGYYQ!c4LFz>#N2U!XuJ(pjyDi?5Z*0pCGac_a+V7`OY@BI zl<JHehZ99qaFse@sYQsFhLkE+#)5kQ z1M0*kuL;iy=)^;ue;4dICq**}8)NH`a3+ie+M7i@OGW*hNY?H&Uiyd!NY^yZ!K&oj1w5c=+2lxSK2#YctUtqFrCbnO3G=VEWp5})AX$4fDu)lE__OeCXCVa z_2oEii9&M#IJ4=(WWoOmC`=_ydpq?D@t87yFZ@#AB!g(5ouEFU!aw81d2sh;l7Nns z=R~?%_?p0Ag_pr=19(Tp4;1zm82sY}Xqs@jP-%c83(#)tgB;53EN~L_L~n@UVc{y_ zG65T!%xc@NBA!A$CoAP>VRwNa6i;$$7R?HAA`65kg&TxxgoQ##R?jXi0(Vds-VwgBK@xhH#}|bt;-k#zT4PKV0}%fw8!*&=Wl?h6jXM z!v6{MDKe3lrl*pJ1X3@#Lazm|p}>pcPvDx^Ji zKpvd~ppOYWAD4eJoQU>6LV+JWlt+F^*L@L-?Hl z(K%Uldxqej@iGhgkuXib(HfckIX1mnODXq5oQ@rso`cT_%qssS4y_IydK#twJH_#| z!1^zlH<1MOdon%km^!N=`pepQjN;!A$__oqqs^+C?}Kz{JM~P6LA!4h{!_S5fKawl zUwWoB^dIe_{Ve8RDB#$zXS-nb$F|IJp^Kux^O()!r1ZeAf$7DO%Kg3WdY+tl^FRyx z#kE|cZXVq9@Hqd-hj)nSQdgzg8eVI`-P_{aL%Qo(>E?mo$Er)+mm1pL^+=(S_(P~o z1EYh%AwuEN0uJtW;ok+E8u{dLaxyT=BRL+_$isjLF+OHFmkQ?y5P9WtKBeb>%VL%` zg#f~Vj1`cF1Jw_KZ`7w1j?k93PzNP>+9D23|M<`crW56GNOOgK1jyP#-1xf0Wfx0F zHvFd7ZxufiW)XQduzW(sKX~AsAka}q2{<+EPdc*Up-!t;Q5{6ig*3}IeHi2rd0D^C z4Ez-^iHOq<19H05rDG$j!+4V+vT!_%wdbr*h0vTDeSC%R-vUmJb|bq>`{Onz+b%Y5 zL-?V6g}K641qc~g?ePq2_&?eQT`c^}$D%RBxB%wVzNPqealY9qKFkhy8!nG~+&|V| zot#?G=+v7(nq}1vc>v_l=LLU9bcuUh57vG=mX>OB4cjj1QoiKxylQ8G&g3(-DV}$- z4HFY`8E+o`SbI(Nwh%h>j7z`b$CqDFq>&JVsUbKq4%YG20+*dO$QeBFnQ@XK%26R? zd}b^z8_)1Lwf{362|2+b)|!}M93adU{!M_WPhQ5K;iiMn(7*A5mA5nt(Vf9?oN&It zC9pps6^8W_N3O~CaoL4*K5vE`;)K?A29FIc>_c7_kp~Y0j}v;5WTFSU(LdBd-0Jc5 z>NpvaZ&0*MPbv1IVD*$U;xyzyY4W0+?*FUNg|UM@UhZDJOA7VEhf`yw%Q(ayq)`v{ zHX9~&v}5>;;KyFrkUsq{VTJ&i9iyY6^*?OezpH7V7j6^g3(p8Q3e05B5}?p#QA(7C zO>L}OdKAv^_H%M>XX_cVFO^!|qK1)jGQ)*+RdxlLF7N;_%RgXrr zcVsv-tHe7`6+SL(C_vU`3rWMdb7G+j>1kwn$1C$ZfmxdU;J{zjN9iw0)p<&LMW7#W z+M|R`1$5#>PCD`*D(z&!+N@|Yr{uTAFj06zuy!h1ChHT%&~JtP zg|vQ!528Qk3yXv{VJ%@|4C0XWBk`~=zOLgR3u$`5yG9T*9h^c1WzD9>!q~tLFIa-Y zBoA5P_%K#%1ySCU3UX=b>(@tMHr2tN`e~n_l%;yT(O(}J=WZikd|md7+H-IwCk)}$ zF~8pa&AHY-eJ^1bWgIOBw~u(ZqI^@D-`$}tRabn;`v@B1^+^tTlWz@RP#Mos#s4Vq zD|(y2;3S_$Vgx)iv%4d8!hJy}9LMA#3)lcVL-CsgPDawpMpC>MqU=ZF0?0s53~cp~ zrFNEyj(n(mv?^k{ZJ?xcaB9+D&&2qdDe_FluM|I1SRiE4IAxqD;J~bHVR*XIZx&ssKfJjgoFeI{&@WU(fL8&4WC&yVeN3 z=4i3&xab>x{4-1Ab>s1$B&mb15+G&n>O{MBGz@x3TOp`7+8sq{^S$lxyG;&jn9y$plVx?=vB~+Mc#?XUyxWx563DQ!J%yXA%%Q@#xNNfS~TU zeLTG$xAVf%XD2Nx&D-sbQfc?MN+lmIDV26vSXwe^Vd;mH<9 zk9_W^a>+;ip$(W_O&0zfr-%B%$MM&N9|@4vMH`8KOt?v4R)(xBGMf-)>6|PJFKq}3 zow&^Xf^eZ=?NQY0Lz#aOt`(Yvusu5LMrrxTuy%3@dZ5Nm+N}9_<^3X-$jmHF7TMMg z>_dB4D?UK*eX7$^3|z5zzT*_KrfRlVbnucIhTSI?JsqmE8@MSu(H3jsC}QB zA8G^r<+_YL93L9C$!2D3oIh=b?@0N+^4KX?m>k4p(BY)Bjy6TcyI3KOrsEkH3?@5K z;0-}KfcIKCwf|NK!2=-&+FIZQh0=70aw`8vCi1{M;Z$KSfs<^eKpMItpOb*l>Q~g0 z9e++oI5lL#XQ4W=c)E@u+Jro73uz&dZuOClW126R-S`n9A6*U-AZ(RJo5zXpqodWi zQ1ScYJWg8DvtWIJ40vc4x>%bP?MCpyp9G9qx`ui1B+Dj`;ZKt3w6|=;OS( zKW%PtUuzuZeym3Qna$$ls}c3I!^&_8Z%z#x+KVEw7wecTaaW0HgY05p+2=rIUwRgOu{iSQW!*+e%(KnFk=A9UAq{HUGUw{Mo15wt+AH)JM5|u2y+Y48$&ph~iH&HDt*&1AxjlkB} zlyS=%F=Rgs66^Z{do{)B;}uR9AnUv6DEq%Ry#9T*aUHjwwqY@jk=K=&v*|HkwrT02 z_6u9h^LIUK`7pDWhq6gBMQJ2=8zHY$6s_vN4Lls<-EoPej2)D|j$ng0W~(YNSr3Z) z9N`Ut0Sw4PCOR0OqTR^azMXQ=1+unVcwLn2Po_9pRbcJs@{9puCy~kBTxr|Kk}NGg z^g;Iyk8){Or8b2!)|5E5pY;jw6Fx$Z~@WB;@+9cklC;Z3i<&K}BUgIZT1=$!JFW#ME ze&kl7J>twBZ6@LV@_8RQPQ~}oVfBsuTlk#l#E+vj1)=k$E{t3)nW4CII9i}Qx>@*|;?D@Z zp}(jwP8YKF6UT9Jml85Pq9gs60$H=Njg{S8W*iz3Yd=3O?~Z3wHbyLuHB-obxT2%# z{#$=OC-!?OqB^WEO#Z$~UW>fJO4~nC9zPqD=lw}#)fyR&C6#(>v* zMPlZewNYj3S1PO?qB&ZFL#LX_22jlft0-Uw;_?tOgAg*trH$J1g zAZm&(0ra$gXQz8y(W_{{oai;%)wacK!4LVJaUqsak_4ai&n19!S^Q zv}vA^o8Fg5SDrrkN1+EdA5)%On2vF^m4hCYs$9Hct|`I-Kfk(=rA0@pW^6Rl3}$%) z5i+1Bh7|<^o&{^jW}eYeZU1SPcaDL>GzqLPLN-GoZuYrdaefU$qOZu`Q@C09pa4-G zkcY1DVawHFPgPqN;i&Bg#sruxUQz;mVePm4dP0^K_>ncjMs|YP40^EI&EF8X=<{xg zUjK8HZXDbp-RW-+9Y3VoE{E!okJs$(sWq~S5L;tk=zAKg{Al8S2(gcWaYlU)2p0;F z*)KZk;eTPJyHbyG%r6f2ODf3c*_&0+3nO*RH%w9JC$lxS*`*)_Z9Mc)LzHcesfxKy zrX?!JC0UtdI2$}0l@nJ_gw9K@NtkZv4cReat)RT0+6NBPV8Ai($VUz!Z6Nws)zn4Se%5NnCnQ*&U6$K%f~~4UvnxL` zZ^b1n%}&fstCF-DnYb&8)4F)NdvfJU_i=saQ(FLa7qj3F(jfcLWyiWlWTZmI4uqaG z#}3%|E{)Zr6*B%gKNvp-*0fn#IzqsXq?;WT_2o%p+<|hRiK!C@b>BqylJq{lW4U`n z`%0NuM0}BJD{8+|Q*oFsvs>L+_l@;Co$C4$Jc{-gS%a6U0s#3mKX;Cf*Hu8nI3Wj; zFgUtkSl0T|$Q)bRlst_;OtZF=)*5C6aqIk0KIP0FKTv!>fj+WdN=5x(Vi1zPKhO`w zo3beb?tzYuj$0ZU8dytWrfhaaCS^jsbU0{e18Ksn4YHHLLLQsxE5g>u89FQHb{!aYBML!f0lgwEX@~qkaq?68g}&yT%Cf>Ww3> zo_3z4atx;L3-lEyrVTvgnM~5iMwQtiU7e!nZZKC<V7g`f zC|Z6kA#L_c11W`A8UR`ODMk7wsXWt)Ufk$~PMzu+^qoy}7TqdRLrS`hRPMtW-8i}? z_;o7ehjr-EkXck$PJhH$!g&#fYm(MSRwf)r`-u1d2|Ehw3-sHw!taH%g*OGtlV|z<CwZ-gBNcwy9|19wyE$8u{I=)h{yuN6k#Pyn3%PHGPx*|~O z7MwO>w-&X3sE>&DW;sgE1+!rvRoAl^HM+n|>(~oj)AClggVvv#b;ds>r{-_C$prRT zN6*4CIAR$cQ!jL*nmgRKTA1%#(&8Tb!*sU{rjs3v>WJGfPo<)! zf4w1{MLRukMsZ~pX4OqO>ZFhUBTsL`HySrIih;C9v zH%gJ)5C289Wl25j-$_=l0_GT}&2$I1w7ENE)f=@#;l@$zZth{{y8UCPmNgFOgAkz_ z^b2_(7A_EQYFu5I9m!+takcg#;e3HHNxrX74Bm+`YXmy>m#WI|HTC77$x?>KfHX2g zq~#4n8uilwNg@s7qc=9{Gw6i1U$wO)w%9^;LReezT~)`G zs{eyJu6J)K_B=&^upMKYbA@!k*aP(KCk1YHLH+J`2I5lG)-IwZb^X6ohjs#X%^8@Z zcT|l(;hFTrClsAPr#vm(BcOlzq*J`NM0;^+e-Wyci;n2TfFS;G3*IG@fG0_;aBmow}IlTih_A*-GF?7P4^`9j*G$xV3S(pW@tE z<(TxU+aunZs6QQ>Db8I*pm}HXHK9O5?$#v-bdcR|Q8Oqr1qRe8IW^MInE^*9-60$k z$NO7H(?`{15P;YW@*&d)m?h5bWujZ^>24R2|2JGQs_YB(3^2dJ?#vc`Bl zO}43y|Hd8A@MeMMs_@VFmvy+!_0;J2j+8pIgMk_+9&(}EpfugRYO}}+Vv|`Dm0(vO z$-MB`+66y-(BJ2i@5E4n{)n$Fp}=NDL~SrJpe;`NKof7ZeK}F7>9cN}ZpR6>)pm$J zoOtHsI1%lnVE8f<9c}IH(Vb6bZ8cUA-Z8DFXqP32Y6{fL)1L#{1_%#6t`{q4X{PTBaRfbTHLA(z zP1vdvd|@bnS!H}|I-?h9{eiIRdgADYljHZ@^fX6DyFMqFa|?eZ&DMJ;AL`#3N>oRs z|C}K66Aac5=7`LZC)jE_WCqI`6Qn7+H-nu6$@(L%e)z9i{hF2*%v&|8uG72KJgLF$ zQb}(PcJ12x3Eg?+anVPHYjrx@6p*}v{~TS(-Pgngc*&==`+9V!wLC`k9kca`J034V zFQwE%Pl=hwIG{>pZG1f#wHQ*^+Ii=xNk zT9Z2BjK;-zz15M&4{ZHW4K6P~Fb;U62D192qkjIsvgFQNxm?bzNw36ib}d@eZ`Rt^ zu(oAxzHSkotl67)f>Nqxz5L}U`WM`{^)>asAeTCT9A~rYy|uYL`utv!2o((crezKK^79u8^^Y~sN4@y;Gu*PHu$$x`et#Sr6pK%#R>jx9rf=0 z^k1!_j?0dB^YqZnC)-!Jb5zf*s^@0a@txL{?%#g(W%mj^vPEEy9?bx+ofAJ$e4fA$ zDe3&EGR`dIT*vC))&EBY$|c94lKPl*>6QFSm3`~Ir55d!*W+0SGgEgr~hMcnMQpW?{i%U!kF!sPSyBYm^TlgYKZ z{zIwAt#urR?P8qV<)yD`gW2IarJEjEcBr*oRq>0dQhk;3s*2Z;4yH(1gY!9&h z*iL+ATSdFjavhx_n5;f&Qy|aeb_vbvrnyV|l}pR(Db31mF2)bU%$8=q3|yZxl|=`e z-Nmc}5?Gx$@F(LMEzNY~@d?P%qobkopS7*J+Qz`@BeMEge|b2TX=yfQrYN0NC(3 zpx!vos!k0n)Ioos=M%z5g=2-QH8$>0c(W!kKUcW7@I3*-9(`tCFEQg^x62R)Y`o4y zN813)D4v;2V<~%-!0mN20MC0^2i~A`h{moC!ocYAx^%irA?q(j=ns8bV5-eJ9K5}q zLR(v_>0HwR-fH;8){U(HHtd!YCGf+-C2KX*MTdBdkVczK*J|V>d5~eYWesK@L6=6n z$3~}AL^~^(DqJeS%VkbAM0xA8;}o}g2(b+zcVqi>EaH^doUT);!{T$M>Zy7fl|o7^ zv4_(f2<1}Px+KMpPqUOBKbUOgN<7_PZ5{AUnc|AuZ|^5U{*^-+oAd45UZ#pJ1o1iE z5o{W99h<-=s%P<7Dp#AO$R+qBgwZ*!)+8_EI>Bpw@VDt#|y}|>8!V4 zoJQR0>aR=Du0+s zt1hz{`3DO|*1x9vEM>qG_D`0~9@E8;saK`ESce|v!^b2qjXx_rjki)-B{>z^fKJJA zo;&#qZH<6))>kxpHeh&!v0$%u}k|Jy0JN_t^;vsZPB@NBd-Pp`FG`blOPP0c@Ac|gC%DVhcz@YR*qaKeL*EeHTe6Q)9b+VI zv09~dR6Z*p1H^jNQ%6`<-!EmBJVkYk(jD4oWw>9IPSBfrRYd_u3%K-38)%c+JlO~J zsM4!4X@eAjrr1Xnzg76Du$us(JOd*MOW#X**9v>aJm%EkXCN||n6g62lOReRxc_v9 z>Bl9f9iuBY;27dW0o)|#CL%(U4X2w$tV7_aEAa{JENnn|1IxDCF9&eyO_Syl#Xq1rpQm)fi&ds z44IWtG?f?|4-44996E7iVuKq6_8rm&^254_Qx{lYI7PTxSVthwY+!AoPq`b4PUP23 zTpHqV{NBq>ccUJyI6=>jeN>Nc9H@^2&pPZp|MCHhQ1_;GyZk(KraMv}`@CLtOrs8k zhse|XpPb!Sd;_d?HMS;nYT6)wrWTEUqpeu~rZ()WZC}UeXYoeuJN*=GFkLdLK!ZB9 z$dFiLGSf$XX%7;@j`veW{lYUq7*r4o&36m;2@eU+3EU}u(e?Hj3OTrD4Vo_9dD$(zY5HJjj$ng{$0Q+%@-C5Zwa)& zMWFtP0&7y#v55dt*4jdxdQK9~5$F@_nnhNwSq!%c?-#5eC}XnG?PB3^fpI|{I2RoI z=ED1g1;Pme`kGut%VfHIP$_>BaD-u9*6$Sfvv9kBomeYu6-ElvL+T4`Ok9YseB0lR~ zwN!>9VWIzs^W4@_^sZu)TVLC*d>xWs(i=O5X_okoD*Mo7npGKpJ$2|!Ae>XzN_+HQ zp~m_4=|SRQYS+_pcpsegMYp(~I%?6UH@tLkD#eS#bhvc@C+}IwCA)^ve4jDL(YXuxz zNG5w*CH_!AAL=2UxaCk>U*TOho)XhJ(G_@$iVf?Qx;xoe|zZ$a7 z?{ya4o=weeebvGEYcc_HYJ4zxq~5jm38g<|{ME=d9xEf62gr#PI$NEdA6``lU!9jj z-CA|=!*R`A*>y(G75!d+a2j{mc+X#b9PRT(A?%>;*q@%Htg137wM; z%j8c49qlh52YpDREIJ~~`an^i&P)nj=mUQ@QK8jo{a`vH^FHAS0bNY4q7@1Kp_!WD zKdBJ8p>sky$6yXh98RA52!Uk=<)pB}gx- z!GU$lWA$D1K||{C-ut$`WUaG6$!oIe@zbL zIEBjubfdgETI54kui1#u+J*Dq7^W%+d6x(u7r5LsJCYviZS{pAJk&`Vamo^2F6=8j zC6I=0eMWKykE@oY3bh30WupUV*QF?tM{LJ|;-ptiHk(qZr+8C`3KvyZq<<80tTrId$GqYB2 zkLKZSxVA9O(+M@Jd+F*@cn%KEtzL0A=?>+b<`Mc~DaAjO3U%D5OT_CN){6FH*1BmP z*?i!IP&2bj^tGr5B~urqzJHmAvcHtPi&?{?y|u0rB72}H?dAcEV?c1p@eyH>0Bt6G zT9_oLC*-V`8Ydc*mnp8qi@7~svqRAHRJK)*_;MXuO; zBTzmI^GKGt0#pqTi#!y}~?UnNTZH$F{-;gq?*6 z!iEC%V}DK~oC~&Mwn_asuNQ;|1^R*dbzXU@wIAYQ<-p1_`#N0sq<##ylA|51_(*|# z`UzVw9>9wNKXP{nw+Q4}KPp<0a8RZ@GhO;&e_bhO!~3v*i&-WlKfZIrYj0-yKm2$ zR{M)D>(Qk*Whyx~oYRK|e&LdXfy;?a1O^(vObnXk0)u#=@S0#mSx!2zrcroOneI)B z(EjxWoQnN2+ew9Eq5XtO;$${kRS~tT%*A$ea^#?9Ld(vC$8-k9w(#_Tx-L>dW?ra?5db zns;s_9Dc5!2yCVo`M+S)&1SKpNs3T&IhoXm^=eCz&aGr}V|5EgJk}$sx@xF-3 zpgo8D%lkaGHE%i4Ls9?EbNygkW{GxU#xR0wO~?MW8- zCXXkXLy%rg`QGr*-mv~^Wl_$ayrw+I)z~g6zc1>qr{1I<$zx>FzG`Hf{mc$YS=Ang zwy36z*}obctC80uj~EJR8_UzuWV;^arSTw(dXjm4T}PVyw0e_#gW(wCTqO^BkiajA z{Sp&anz513tOByoW&(u0`c?8$2k$@HYe>_Q_8Zdjdz%*4ZMF|(T0abAAB5u}sY`#^ z!t9V`gZ`>FY+q)aS&c3DSNJ$_X8CF|%5~@-{+v14dr_;AmBzz5+LYPeC-nFc=d|*x zGp%07wmNmr>&`8ZTOhYUMGNptn^SIq+yc1;hLiJN zRNMksAa`u}ILIxKTVTjpAa`s-u1oU<^*QI%batq`Z z$Q@fg267AJ78r6Cz_HmrJT?m(XejCSuqVek&A z&?0#}w?J-zHNpZowp-cTr8gn)<_R1bTXEP_JYQgAJCx7dB0J>&a|`4aSOYBZ{{ec~ V(lp^63;zHB002ovPDHLkV1j@GDJlQ} literal 0 HcmV?d00001 diff --git a/presto-main/src/main/resources/webapp/query.html b/presto-main/src/main/resources/webapp/query.html new file mode 100644 index 00000000..e57ae9a9 --- /dev/null +++ b/presto-main/src/main/resources/webapp/query.html @@ -0,0 +1,688 @@ + + + + + + + + + + + + + + + + + +
+ + +

Summary

+ +
+ +
+
Query State
+
+ +
User
+
+ +
Source
+
+ +
Catalog
+
+ +
Schema
+
+ +
Elapsed
+
+ +
Memory Pool
+
+ +
Memory
+
+ +
CPU Time
+
+ +
Rows
+
+ +
Data Size
+
+ +
Raw
+
+ +
Execution
+
+ +
Visualization
+
Timeline
+ +
Kill
+
+ +
Error Type
+
+ +
Error Code
+
+ +
Message
+
+ +
Session Properties
+
+
+ +

Stages

+ +
+ +

Tasks

+ +

+ + + + + + + + + + + + + + + + +
IdHostStateRowsRows/sBytesBytes/sSplits PendingSplits RunningSplits DoneOutput BuffersBuffered Pages
+ + + + + diff --git a/presto-main/src/main/resources/webapp/timeline.html b/presto-main/src/main/resources/webapp/timeline.html new file mode 100644 index 00000000..9f6292eb --- /dev/null +++ b/presto-main/src/main/resources/webapp/timeline.html @@ -0,0 +1,176 @@ + + + + + + + + + + + + + + + + + + +
+ + +

Timeline

+ +
+
+
+
Created
+
+
+
First split started
+
+
+
Last split started
+
+
+
Ended
+
+
+
+
+ + + + + diff --git a/presto-main/src/main/resources/webapp/units.js b/presto-main/src/main/resources/webapp/units.js new file mode 100644 index 00000000..87aa4dcd --- /dev/null +++ b/presto-main/src/main/resources/webapp/units.js @@ -0,0 +1,77 @@ +function formatDuration(millis) { + var unit = "ms"; + if (millis >= 1000) { + millis /= 1000; + unit = "s"; + } + if (millis >= 60) { + millis /= 60; + unit = "m"; + } + if (millis >= 60) { + millis /= 60; + unit = "h"; + } + + return precisionRound(millis) + " " + unit; +} + +function formatDataSize(bytes) { + var unit = "B"; + if (bytes >= 1024) { + bytes /= 1024; + unit = "KB"; + } + if (bytes >= 1024) { + bytes /= 1024; + unit = "MB"; + } + if (bytes >= 1024) { + bytes /= 1024; + unit = "GB"; + } + if (bytes >= 1024) { + bytes /= 1024; + unit = "TB"; + } + if (bytes >= 1024) { + bytes /= 1024; + unit = "PB"; + } + return precisionRound(bytes) + " " + unit; +} + +function formatCount(count) { + var unit = ""; + if (count > 1000) { + count /= 1000; + unit = "K"; + } + if (count > 1000) { + count /= 1000; + unit = "M"; + } + if (count > 1000) { + count /= 1000; + unit = "B"; + } + if (count > 1000) { + count /= 1000; + unit = "T"; + } + if (count > 1000) { + count /= 1000; + unit = "Q"; + } + return precisionRound(count) + unit; +} + +function precisionRound(n) { + if (n < 10) { + return d3.round(n, 2); + } + if (n < 100) { + return d3.round(n, 1); + } + return Math.round(n); +} diff --git a/presto-main/src/main/resources/webapp/vendor/bootstrap/css/bootstrap.css b/presto-main/src/main/resources/webapp/vendor/bootstrap/css/bootstrap.css new file mode 100644 index 00000000..fb15e3d6 --- /dev/null +++ b/presto-main/src/main/resources/webapp/vendor/bootstrap/css/bootstrap.css @@ -0,0 +1,6584 @@ +/*! + * Bootstrap v3.3.4 (http://getbootstrap.com) + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ + +/*! normalize.css v3.0.2 | MIT License | git.io/normalize */ +html { + font-family: sans-serif; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} +body { + margin: 0; +} +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +menu, +nav, +section, +summary { + display: block; +} +audio, +canvas, +progress, +video { + display: inline-block; + vertical-align: baseline; +} +audio:not([controls]) { + display: none; + height: 0; +} +[hidden], +template { + display: none; +} +a { + background-color: transparent; +} +a:active, +a:hover { + outline: 0; +} +abbr[title] { + border-bottom: 1px dotted; +} +b, +strong { + font-weight: bold; +} +dfn { + font-style: italic; +} +h1 { + margin: .67em 0; + font-size: 2em; +} +mark { + color: #000; + background: #ff0; +} +small { + font-size: 80%; +} +sub, +sup { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; +} +sup { + top: -.5em; +} +sub { + bottom: -.25em; +} +img { + border: 0; +} +svg:not(:root) { + overflow: hidden; +} +figure { + margin: 1em 40px; +} +hr { + height: 0; + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; +} +pre { + overflow: auto; +} +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; +} +button, +input, +optgroup, +select, +textarea { + margin: 0; + font: inherit; + color: inherit; +} +button { + overflow: visible; +} +button, +select { + text-transform: none; +} +button, +html input[type="button"], +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; + cursor: pointer; +} +button[disabled], +html input[disabled] { + cursor: default; +} +button::-moz-focus-inner, +input::-moz-focus-inner { + padding: 0; + border: 0; +} +input { + line-height: normal; +} +input[type="checkbox"], +input[type="radio"] { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + padding: 0; +} +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + height: auto; +} +input[type="search"] { + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; + -webkit-appearance: textfield; +} +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} +fieldset { + padding: .35em .625em .75em; + margin: 0 2px; + border: 1px solid #c0c0c0; +} +legend { + padding: 0; + border: 0; +} +textarea { + overflow: auto; +} +optgroup { + font-weight: bold; +} +table { + border-spacing: 0; + border-collapse: collapse; +} +td, +th { + padding: 0; +} +/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */ +@media print { + *, + *:before, + *:after { + color: #000 !important; + text-shadow: none !important; + background: transparent !important; + -webkit-box-shadow: none !important; + box-shadow: none !important; + } + a, + a:visited { + text-decoration: underline; + } + a[href]:after { + content: " (" attr(href) ")"; + } + abbr[title]:after { + content: " (" attr(title) ")"; + } + a[href^="#"]:after, + a[href^="javascript:"]:after { + content: ""; + } + pre, + blockquote { + border: 1px solid #999; + + page-break-inside: avoid; + } + thead { + display: table-header-group; + } + tr, + img { + page-break-inside: avoid; + } + img { + max-width: 100% !important; + } + p, + h2, + h3 { + orphans: 3; + widows: 3; + } + h2, + h3 { + page-break-after: avoid; + } + select { + background: #fff !important; + } + .navbar { + display: none; + } + .btn > .caret, + .dropup > .btn > .caret { + border-top-color: #000 !important; + } + .label { + border: 1px solid #000; + } + .table { + border-collapse: collapse !important; + } + .table td, + .table th { + background-color: #fff !important; + } + .table-bordered th, + .table-bordered td { + border: 1px solid #ddd !important; + } +} +@font-face { + font-family: 'Glyphicons Halflings'; + + src: url('../fonts/glyphicons-halflings-regular.eot'); + src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../fonts/glyphicons-halflings-regular.woff2') format('woff2'), url('../fonts/glyphicons-halflings-regular.woff') format('woff'), url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg'); +} +.glyphicon { + position: relative; + top: 1px; + display: inline-block; + font-family: 'Glyphicons Halflings'; + font-style: normal; + font-weight: normal; + line-height: 1; + + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +.glyphicon-asterisk:before { + content: "\2a"; +} +.glyphicon-plus:before { + content: "\2b"; +} +.glyphicon-euro:before, +.glyphicon-eur:before { + content: "\20ac"; +} +.glyphicon-minus:before { + content: "\2212"; +} +.glyphicon-cloud:before { + content: "\2601"; +} +.glyphicon-envelope:before { + content: "\2709"; +} +.glyphicon-pencil:before { + content: "\270f"; +} +.glyphicon-glass:before { + content: "\e001"; +} +.glyphicon-music:before { + content: "\e002"; +} +.glyphicon-search:before { + content: "\e003"; +} +.glyphicon-heart:before { + content: "\e005"; +} +.glyphicon-star:before { + content: "\e006"; +} +.glyphicon-star-empty:before { + content: "\e007"; +} +.glyphicon-user:before { + content: "\e008"; +} +.glyphicon-film:before { + content: "\e009"; +} +.glyphicon-th-large:before { + content: "\e010"; +} +.glyphicon-th:before { + content: "\e011"; +} +.glyphicon-th-list:before { + content: "\e012"; +} +.glyphicon-ok:before { + content: "\e013"; +} +.glyphicon-remove:before { + content: "\e014"; +} +.glyphicon-zoom-in:before { + content: "\e015"; +} +.glyphicon-zoom-out:before { + content: "\e016"; +} +.glyphicon-off:before { + content: "\e017"; +} +.glyphicon-signal:before { + content: "\e018"; +} +.glyphicon-cog:before { + content: "\e019"; +} +.glyphicon-trash:before { + content: "\e020"; +} +.glyphicon-home:before { + content: "\e021"; +} +.glyphicon-file:before { + content: "\e022"; +} +.glyphicon-time:before { + content: "\e023"; +} +.glyphicon-road:before { + content: "\e024"; +} +.glyphicon-download-alt:before { + content: "\e025"; +} +.glyphicon-download:before { + content: "\e026"; +} +.glyphicon-upload:before { + content: "\e027"; +} +.glyphicon-inbox:before { + content: "\e028"; +} +.glyphicon-play-circle:before { + content: "\e029"; +} +.glyphicon-repeat:before { + content: "\e030"; +} +.glyphicon-refresh:before { + content: "\e031"; +} +.glyphicon-list-alt:before { + content: "\e032"; +} +.glyphicon-lock:before { + content: "\e033"; +} +.glyphicon-flag:before { + content: "\e034"; +} +.glyphicon-headphones:before { + content: "\e035"; +} +.glyphicon-volume-off:before { + content: "\e036"; +} +.glyphicon-volume-down:before { + content: "\e037"; +} +.glyphicon-volume-up:before { + content: "\e038"; +} +.glyphicon-qrcode:before { + content: "\e039"; +} +.glyphicon-barcode:before { + content: "\e040"; +} +.glyphicon-tag:before { + content: "\e041"; +} +.glyphicon-tags:before { + content: "\e042"; +} +.glyphicon-book:before { + content: "\e043"; +} +.glyphicon-bookmark:before { + content: "\e044"; +} +.glyphicon-print:before { + content: "\e045"; +} +.glyphicon-camera:before { + content: "\e046"; +} +.glyphicon-font:before { + content: "\e047"; +} +.glyphicon-bold:before { + content: "\e048"; +} +.glyphicon-italic:before { + content: "\e049"; +} +.glyphicon-text-height:before { + content: "\e050"; +} +.glyphicon-text-width:before { + content: "\e051"; +} +.glyphicon-align-left:before { + content: "\e052"; +} +.glyphicon-align-center:before { + content: "\e053"; +} +.glyphicon-align-right:before { + content: "\e054"; +} +.glyphicon-align-justify:before { + content: "\e055"; +} +.glyphicon-list:before { + content: "\e056"; +} +.glyphicon-indent-left:before { + content: "\e057"; +} +.glyphicon-indent-right:before { + content: "\e058"; +} +.glyphicon-facetime-video:before { + content: "\e059"; +} +.glyphicon-picture:before { + content: "\e060"; +} +.glyphicon-map-marker:before { + content: "\e062"; +} +.glyphicon-adjust:before { + content: "\e063"; +} +.glyphicon-tint:before { + content: "\e064"; +} +.glyphicon-edit:before { + content: "\e065"; +} +.glyphicon-share:before { + content: "\e066"; +} +.glyphicon-check:before { + content: "\e067"; +} +.glyphicon-move:before { + content: "\e068"; +} +.glyphicon-step-backward:before { + content: "\e069"; +} +.glyphicon-fast-backward:before { + content: "\e070"; +} +.glyphicon-backward:before { + content: "\e071"; +} +.glyphicon-play:before { + content: "\e072"; +} +.glyphicon-pause:before { + content: "\e073"; +} +.glyphicon-stop:before { + content: "\e074"; +} +.glyphicon-forward:before { + content: "\e075"; +} +.glyphicon-fast-forward:before { + content: "\e076"; +} +.glyphicon-step-forward:before { + content: "\e077"; +} +.glyphicon-eject:before { + content: "\e078"; +} +.glyphicon-chevron-left:before { + content: "\e079"; +} +.glyphicon-chevron-right:before { + content: "\e080"; +} +.glyphicon-plus-sign:before { + content: "\e081"; +} +.glyphicon-minus-sign:before { + content: "\e082"; +} +.glyphicon-remove-sign:before { + content: "\e083"; +} +.glyphicon-ok-sign:before { + content: "\e084"; +} +.glyphicon-question-sign:before { + content: "\e085"; +} +.glyphicon-info-sign:before { + content: "\e086"; +} +.glyphicon-screenshot:before { + content: "\e087"; +} +.glyphicon-remove-circle:before { + content: "\e088"; +} +.glyphicon-ok-circle:before { + content: "\e089"; +} +.glyphicon-ban-circle:before { + content: "\e090"; +} +.glyphicon-arrow-left:before { + content: "\e091"; +} +.glyphicon-arrow-right:before { + content: "\e092"; +} +.glyphicon-arrow-up:before { + content: "\e093"; +} +.glyphicon-arrow-down:before { + content: "\e094"; +} +.glyphicon-share-alt:before { + content: "\e095"; +} +.glyphicon-resize-full:before { + content: "\e096"; +} +.glyphicon-resize-small:before { + content: "\e097"; +} +.glyphicon-exclamation-sign:before { + content: "\e101"; +} +.glyphicon-gift:before { + content: "\e102"; +} +.glyphicon-leaf:before { + content: "\e103"; +} +.glyphicon-fire:before { + content: "\e104"; +} +.glyphicon-eye-open:before { + content: "\e105"; +} +.glyphicon-eye-close:before { + content: "\e106"; +} +.glyphicon-warning-sign:before { + content: "\e107"; +} +.glyphicon-plane:before { + content: "\e108"; +} +.glyphicon-calendar:before { + content: "\e109"; +} +.glyphicon-random:before { + content: "\e110"; +} +.glyphicon-comment:before { + content: "\e111"; +} +.glyphicon-magnet:before { + content: "\e112"; +} +.glyphicon-chevron-up:before { + content: "\e113"; +} +.glyphicon-chevron-down:before { + content: "\e114"; +} +.glyphicon-retweet:before { + content: "\e115"; +} +.glyphicon-shopping-cart:before { + content: "\e116"; +} +.glyphicon-folder-close:before { + content: "\e117"; +} +.glyphicon-folder-open:before { + content: "\e118"; +} +.glyphicon-resize-vertical:before { + content: "\e119"; +} +.glyphicon-resize-horizontal:before { + content: "\e120"; +} +.glyphicon-hdd:before { + content: "\e121"; +} +.glyphicon-bullhorn:before { + content: "\e122"; +} +.glyphicon-bell:before { + content: "\e123"; +} +.glyphicon-certificate:before { + content: "\e124"; +} +.glyphicon-thumbs-up:before { + content: "\e125"; +} +.glyphicon-thumbs-down:before { + content: "\e126"; +} +.glyphicon-hand-right:before { + content: "\e127"; +} +.glyphicon-hand-left:before { + content: "\e128"; +} +.glyphicon-hand-up:before { + content: "\e129"; +} +.glyphicon-hand-down:before { + content: "\e130"; +} +.glyphicon-circle-arrow-right:before { + content: "\e131"; +} +.glyphicon-circle-arrow-left:before { + content: "\e132"; +} +.glyphicon-circle-arrow-up:before { + content: "\e133"; +} +.glyphicon-circle-arrow-down:before { + content: "\e134"; +} +.glyphicon-globe:before { + content: "\e135"; +} +.glyphicon-wrench:before { + content: "\e136"; +} +.glyphicon-tasks:before { + content: "\e137"; +} +.glyphicon-filter:before { + content: "\e138"; +} +.glyphicon-briefcase:before { + content: "\e139"; +} +.glyphicon-fullscreen:before { + content: "\e140"; +} +.glyphicon-dashboard:before { + content: "\e141"; +} +.glyphicon-paperclip:before { + content: "\e142"; +} +.glyphicon-heart-empty:before { + content: "\e143"; +} +.glyphicon-link:before { + content: "\e144"; +} +.glyphicon-phone:before { + content: "\e145"; +} +.glyphicon-pushpin:before { + content: "\e146"; +} +.glyphicon-usd:before { + content: "\e148"; +} +.glyphicon-gbp:before { + content: "\e149"; +} +.glyphicon-sort:before { + content: "\e150"; +} +.glyphicon-sort-by-alphabet:before { + content: "\e151"; +} +.glyphicon-sort-by-alphabet-alt:before { + content: "\e152"; +} +.glyphicon-sort-by-order:before { + content: "\e153"; +} +.glyphicon-sort-by-order-alt:before { + content: "\e154"; +} +.glyphicon-sort-by-attributes:before { + content: "\e155"; +} +.glyphicon-sort-by-attributes-alt:before { + content: "\e156"; +} +.glyphicon-unchecked:before { + content: "\e157"; +} +.glyphicon-expand:before { + content: "\e158"; +} +.glyphicon-collapse-down:before { + content: "\e159"; +} +.glyphicon-collapse-up:before { + content: "\e160"; +} +.glyphicon-log-in:before { + content: "\e161"; +} +.glyphicon-flash:before { + content: "\e162"; +} +.glyphicon-log-out:before { + content: "\e163"; +} +.glyphicon-new-window:before { + content: "\e164"; +} +.glyphicon-record:before { + content: "\e165"; +} +.glyphicon-save:before { + content: "\e166"; +} +.glyphicon-open:before { + content: "\e167"; +} +.glyphicon-saved:before { + content: "\e168"; +} +.glyphicon-import:before { + content: "\e169"; +} +.glyphicon-export:before { + content: "\e170"; +} +.glyphicon-send:before { + content: "\e171"; +} +.glyphicon-floppy-disk:before { + content: "\e172"; +} +.glyphicon-floppy-saved:before { + content: "\e173"; +} +.glyphicon-floppy-remove:before { + content: "\e174"; +} +.glyphicon-floppy-save:before { + content: "\e175"; +} +.glyphicon-floppy-open:before { + content: "\e176"; +} +.glyphicon-credit-card:before { + content: "\e177"; +} +.glyphicon-transfer:before { + content: "\e178"; +} +.glyphicon-cutlery:before { + content: "\e179"; +} +.glyphicon-header:before { + content: "\e180"; +} +.glyphicon-compressed:before { + content: "\e181"; +} +.glyphicon-earphone:before { + content: "\e182"; +} +.glyphicon-phone-alt:before { + content: "\e183"; +} +.glyphicon-tower:before { + content: "\e184"; +} +.glyphicon-stats:before { + content: "\e185"; +} +.glyphicon-sd-video:before { + content: "\e186"; +} +.glyphicon-hd-video:before { + content: "\e187"; +} +.glyphicon-subtitles:before { + content: "\e188"; +} +.glyphicon-sound-stereo:before { + content: "\e189"; +} +.glyphicon-sound-dolby:before { + content: "\e190"; +} +.glyphicon-sound-5-1:before { + content: "\e191"; +} +.glyphicon-sound-6-1:before { + content: "\e192"; +} +.glyphicon-sound-7-1:before { + content: "\e193"; +} +.glyphicon-copyright-mark:before { + content: "\e194"; +} +.glyphicon-registration-mark:before { + content: "\e195"; +} +.glyphicon-cloud-download:before { + content: "\e197"; +} +.glyphicon-cloud-upload:before { + content: "\e198"; +} +.glyphicon-tree-conifer:before { + content: "\e199"; +} +.glyphicon-tree-deciduous:before { + content: "\e200"; +} +.glyphicon-cd:before { + content: "\e201"; +} +.glyphicon-save-file:before { + content: "\e202"; +} +.glyphicon-open-file:before { + content: "\e203"; +} +.glyphicon-level-up:before { + content: "\e204"; +} +.glyphicon-copy:before { + content: "\e205"; +} +.glyphicon-paste:before { + content: "\e206"; +} +.glyphicon-alert:before { + content: "\e209"; +} +.glyphicon-equalizer:before { + content: "\e210"; +} +.glyphicon-king:before { + content: "\e211"; +} +.glyphicon-queen:before { + content: "\e212"; +} +.glyphicon-pawn:before { + content: "\e213"; +} +.glyphicon-bishop:before { + content: "\e214"; +} +.glyphicon-knight:before { + content: "\e215"; +} +.glyphicon-baby-formula:before { + content: "\e216"; +} +.glyphicon-tent:before { + content: "\26fa"; +} +.glyphicon-blackboard:before { + content: "\e218"; +} +.glyphicon-bed:before { + content: "\e219"; +} +.glyphicon-apple:before { + content: "\f8ff"; +} +.glyphicon-erase:before { + content: "\e221"; +} +.glyphicon-hourglass:before { + content: "\231b"; +} +.glyphicon-lamp:before { + content: "\e223"; +} +.glyphicon-duplicate:before { + content: "\e224"; +} +.glyphicon-piggy-bank:before { + content: "\e225"; +} +.glyphicon-scissors:before { + content: "\e226"; +} +.glyphicon-bitcoin:before { + content: "\e227"; +} +.glyphicon-btc:before { + content: "\e227"; +} +.glyphicon-xbt:before { + content: "\e227"; +} +.glyphicon-yen:before { + content: "\00a5"; +} +.glyphicon-jpy:before { + content: "\00a5"; +} +.glyphicon-ruble:before { + content: "\20bd"; +} +.glyphicon-rub:before { + content: "\20bd"; +} +.glyphicon-scale:before { + content: "\e230"; +} +.glyphicon-ice-lolly:before { + content: "\e231"; +} +.glyphicon-ice-lolly-tasted:before { + content: "\e232"; +} +.glyphicon-education:before { + content: "\e233"; +} +.glyphicon-option-horizontal:before { + content: "\e234"; +} +.glyphicon-option-vertical:before { + content: "\e235"; +} +.glyphicon-menu-hamburger:before { + content: "\e236"; +} +.glyphicon-modal-window:before { + content: "\e237"; +} +.glyphicon-oil:before { + content: "\e238"; +} +.glyphicon-grain:before { + content: "\e239"; +} +.glyphicon-sunglasses:before { + content: "\e240"; +} +.glyphicon-text-size:before { + content: "\e241"; +} +.glyphicon-text-color:before { + content: "\e242"; +} +.glyphicon-text-background:before { + content: "\e243"; +} +.glyphicon-object-align-top:before { + content: "\e244"; +} +.glyphicon-object-align-bottom:before { + content: "\e245"; +} +.glyphicon-object-align-horizontal:before { + content: "\e246"; +} +.glyphicon-object-align-left:before { + content: "\e247"; +} +.glyphicon-object-align-vertical:before { + content: "\e248"; +} +.glyphicon-object-align-right:before { + content: "\e249"; +} +.glyphicon-triangle-right:before { + content: "\e250"; +} +.glyphicon-triangle-left:before { + content: "\e251"; +} +.glyphicon-triangle-bottom:before { + content: "\e252"; +} +.glyphicon-triangle-top:before { + content: "\e253"; +} +.glyphicon-console:before { + content: "\e254"; +} +.glyphicon-superscript:before { + content: "\e255"; +} +.glyphicon-subscript:before { + content: "\e256"; +} +.glyphicon-menu-left:before { + content: "\e257"; +} +.glyphicon-menu-right:before { + content: "\e258"; +} +.glyphicon-menu-down:before { + content: "\e259"; +} +.glyphicon-menu-up:before { + content: "\e260"; +} +* { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +*:before, +*:after { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +html { + font-size: 10px; + + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} +body { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + line-height: 1.42857143; + color: #333; + background-color: #fff; +} +input, +button, +select, +textarea { + font-family: inherit; + font-size: inherit; + line-height: inherit; +} +a { + color: #337ab7; + text-decoration: none; +} +a:hover, +a:focus { + color: #23527c; + text-decoration: underline; +} +a:focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +figure { + margin: 0; +} +img { + vertical-align: middle; +} +.img-responsive, +.thumbnail > img, +.thumbnail a > img, +.carousel-inner > .item > img, +.carousel-inner > .item > a > img { + display: block; + max-width: 100%; + height: auto; +} +.img-rounded { + border-radius: 6px; +} +.img-thumbnail { + display: inline-block; + max-width: 100%; + height: auto; + padding: 4px; + line-height: 1.42857143; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 4px; + -webkit-transition: all .2s ease-in-out; + -o-transition: all .2s ease-in-out; + transition: all .2s ease-in-out; +} +.img-circle { + border-radius: 50%; +} +hr { + margin-top: 20px; + margin-bottom: 20px; + border: 0; + border-top: 1px solid #eee; +} +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} +.sr-only-focusable:active, +.sr-only-focusable:focus { + position: static; + width: auto; + height: auto; + margin: 0; + overflow: visible; + clip: auto; +} +[role="button"] { + cursor: pointer; +} +h1, +h2, +h3, +h4, +h5, +h6, +.h1, +.h2, +.h3, +.h4, +.h5, +.h6 { + font-family: inherit; + font-weight: 500; + line-height: 1.1; + color: inherit; +} +h1 small, +h2 small, +h3 small, +h4 small, +h5 small, +h6 small, +.h1 small, +.h2 small, +.h3 small, +.h4 small, +.h5 small, +.h6 small, +h1 .small, +h2 .small, +h3 .small, +h4 .small, +h5 .small, +h6 .small, +.h1 .small, +.h2 .small, +.h3 .small, +.h4 .small, +.h5 .small, +.h6 .small { + font-weight: normal; + line-height: 1; + color: #777; +} +h1, +.h1, +h2, +.h2, +h3, +.h3 { + margin-top: 20px; + margin-bottom: 10px; +} +h1 small, +.h1 small, +h2 small, +.h2 small, +h3 small, +.h3 small, +h1 .small, +.h1 .small, +h2 .small, +.h2 .small, +h3 .small, +.h3 .small { + font-size: 65%; +} +h4, +.h4, +h5, +.h5, +h6, +.h6 { + margin-top: 10px; + margin-bottom: 10px; +} +h4 small, +.h4 small, +h5 small, +.h5 small, +h6 small, +.h6 small, +h4 .small, +.h4 .small, +h5 .small, +.h5 .small, +h6 .small, +.h6 .small { + font-size: 75%; +} +h1, +.h1 { + font-size: 36px; +} +h2, +.h2 { + font-size: 30px; +} +h3, +.h3 { + font-size: 24px; +} +h4, +.h4 { + font-size: 18px; +} +h5, +.h5 { + font-size: 14px; +} +h6, +.h6 { + font-size: 12px; +} +p { + margin: 0 0 10px; +} +.lead { + margin-bottom: 20px; + font-size: 16px; + font-weight: 300; + line-height: 1.4; +} +@media (min-width: 768px) { + .lead { + font-size: 21px; + } +} +small, +.small { + font-size: 85%; +} +mark, +.mark { + padding: .2em; + background-color: #fcf8e3; +} +.text-left { + text-align: left; +} +.text-right { + text-align: right; +} +.text-center { + text-align: center; +} +.text-justify { + text-align: justify; +} +.text-nowrap { + white-space: nowrap; +} +.text-lowercase { + text-transform: lowercase; +} +.text-uppercase { + text-transform: uppercase; +} +.text-capitalize { + text-transform: capitalize; +} +.text-muted { + color: #777; +} +.text-primary { + color: #337ab7; +} +a.text-primary:hover { + color: #286090; +} +.text-success { + color: #3c763d; +} +a.text-success:hover { + color: #2b542c; +} +.text-info { + color: #31708f; +} +a.text-info:hover { + color: #245269; +} +.text-warning { + color: #8a6d3b; +} +a.text-warning:hover { + color: #66512c; +} +.text-danger { + color: #a94442; +} +a.text-danger:hover { + color: #843534; +} +.bg-primary { + color: #fff; + background-color: #337ab7; +} +a.bg-primary:hover { + background-color: #286090; +} +.bg-success { + background-color: #dff0d8; +} +a.bg-success:hover { + background-color: #c1e2b3; +} +.bg-info { + background-color: #d9edf7; +} +a.bg-info:hover { + background-color: #afd9ee; +} +.bg-warning { + background-color: #fcf8e3; +} +a.bg-warning:hover { + background-color: #f7ecb5; +} +.bg-danger { + background-color: #f2dede; +} +a.bg-danger:hover { + background-color: #e4b9b9; +} +.page-header { + padding-bottom: 9px; + margin: 40px 0 20px; + border-bottom: 1px solid #eee; +} +ul, +ol { + margin-top: 0; + margin-bottom: 10px; +} +ul ul, +ol ul, +ul ol, +ol ol { + margin-bottom: 0; +} +.list-unstyled { + padding-left: 0; + list-style: none; +} +.list-inline { + padding-left: 0; + margin-left: -5px; + list-style: none; +} +.list-inline > li { + display: inline-block; + padding-right: 5px; + padding-left: 5px; +} +dl { + margin-top: 0; + margin-bottom: 20px; +} +dt, +dd { + line-height: 1.42857143; +} +dt { + font-weight: bold; +} +dd { + margin-left: 0; +} +@media (min-width: 768px) { + .dl-horizontal dt { + float: left; + width: 160px; + overflow: hidden; + clear: left; + text-align: right; + text-overflow: ellipsis; + white-space: nowrap; + } + .dl-horizontal dd { + margin-left: 180px; + } +} +abbr[title], +abbr[data-original-title] { + cursor: help; + border-bottom: 1px dotted #777; +} +.initialism { + font-size: 90%; + text-transform: uppercase; +} +blockquote { + padding: 10px 20px; + margin: 0 0 20px; + font-size: 17.5px; + border-left: 5px solid #eee; +} +blockquote p:last-child, +blockquote ul:last-child, +blockquote ol:last-child { + margin-bottom: 0; +} +blockquote footer, +blockquote small, +blockquote .small { + display: block; + font-size: 80%; + line-height: 1.42857143; + color: #777; +} +blockquote footer:before, +blockquote small:before, +blockquote .small:before { + content: '\2014 \00A0'; +} +.blockquote-reverse, +blockquote.pull-right { + padding-right: 15px; + padding-left: 0; + text-align: right; + border-right: 5px solid #eee; + border-left: 0; +} +.blockquote-reverse footer:before, +blockquote.pull-right footer:before, +.blockquote-reverse small:before, +blockquote.pull-right small:before, +.blockquote-reverse .small:before, +blockquote.pull-right .small:before { + content: ''; +} +.blockquote-reverse footer:after, +blockquote.pull-right footer:after, +.blockquote-reverse small:after, +blockquote.pull-right small:after, +.blockquote-reverse .small:after, +blockquote.pull-right .small:after { + content: '\00A0 \2014'; +} +address { + margin-bottom: 20px; + font-style: normal; + line-height: 1.42857143; +} +code, +kbd, +pre, +samp { + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; +} +code { + padding: 2px 4px; + font-size: 90%; + color: #c7254e; + background-color: #f9f2f4; + border-radius: 4px; +} +kbd { + padding: 2px 4px; + font-size: 90%; + color: #fff; + background-color: #333; + border-radius: 3px; + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .25); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .25); +} +kbd kbd { + padding: 0; + font-size: 100%; + font-weight: bold; + -webkit-box-shadow: none; + box-shadow: none; +} +pre { + display: block; + padding: 9.5px; + margin: 0 0 10px; + font-size: 13px; + line-height: 1.42857143; + color: #333; + word-break: break-all; + word-wrap: break-word; + background-color: #f5f5f5; + border: 1px solid #ccc; + border-radius: 4px; +} +pre code { + padding: 0; + font-size: inherit; + color: inherit; + white-space: pre-wrap; + background-color: transparent; + border-radius: 0; +} +.pre-scrollable { + max-height: 340px; + overflow-y: scroll; +} +.container { + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} +@media (min-width: 768px) { + .container { + width: 750px; + } +} +@media (min-width: 992px) { + .container { + width: 970px; + } +} +@media (min-width: 1200px) { + .container { + width: 1170px; + } +} +.container-fluid { + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} +.row { + margin-right: -15px; + margin-left: -15px; +} +.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; +} +.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 { + float: left; +} +.col-xs-12 { + width: 100%; +} +.col-xs-11 { + width: 91.66666667%; +} +.col-xs-10 { + width: 83.33333333%; +} +.col-xs-9 { + width: 75%; +} +.col-xs-8 { + width: 66.66666667%; +} +.col-xs-7 { + width: 58.33333333%; +} +.col-xs-6 { + width: 50%; +} +.col-xs-5 { + width: 41.66666667%; +} +.col-xs-4 { + width: 33.33333333%; +} +.col-xs-3 { + width: 25%; +} +.col-xs-2 { + width: 16.66666667%; +} +.col-xs-1 { + width: 8.33333333%; +} +.col-xs-pull-12 { + right: 100%; +} +.col-xs-pull-11 { + right: 91.66666667%; +} +.col-xs-pull-10 { + right: 83.33333333%; +} +.col-xs-pull-9 { + right: 75%; +} +.col-xs-pull-8 { + right: 66.66666667%; +} +.col-xs-pull-7 { + right: 58.33333333%; +} +.col-xs-pull-6 { + right: 50%; +} +.col-xs-pull-5 { + right: 41.66666667%; +} +.col-xs-pull-4 { + right: 33.33333333%; +} +.col-xs-pull-3 { + right: 25%; +} +.col-xs-pull-2 { + right: 16.66666667%; +} +.col-xs-pull-1 { + right: 8.33333333%; +} +.col-xs-pull-0 { + right: auto; +} +.col-xs-push-12 { + left: 100%; +} +.col-xs-push-11 { + left: 91.66666667%; +} +.col-xs-push-10 { + left: 83.33333333%; +} +.col-xs-push-9 { + left: 75%; +} +.col-xs-push-8 { + left: 66.66666667%; +} +.col-xs-push-7 { + left: 58.33333333%; +} +.col-xs-push-6 { + left: 50%; +} +.col-xs-push-5 { + left: 41.66666667%; +} +.col-xs-push-4 { + left: 33.33333333%; +} +.col-xs-push-3 { + left: 25%; +} +.col-xs-push-2 { + left: 16.66666667%; +} +.col-xs-push-1 { + left: 8.33333333%; +} +.col-xs-push-0 { + left: auto; +} +.col-xs-offset-12 { + margin-left: 100%; +} +.col-xs-offset-11 { + margin-left: 91.66666667%; +} +.col-xs-offset-10 { + margin-left: 83.33333333%; +} +.col-xs-offset-9 { + margin-left: 75%; +} +.col-xs-offset-8 { + margin-left: 66.66666667%; +} +.col-xs-offset-7 { + margin-left: 58.33333333%; +} +.col-xs-offset-6 { + margin-left: 50%; +} +.col-xs-offset-5 { + margin-left: 41.66666667%; +} +.col-xs-offset-4 { + margin-left: 33.33333333%; +} +.col-xs-offset-3 { + margin-left: 25%; +} +.col-xs-offset-2 { + margin-left: 16.66666667%; +} +.col-xs-offset-1 { + margin-left: 8.33333333%; +} +.col-xs-offset-0 { + margin-left: 0; +} +@media (min-width: 768px) { + .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 { + float: left; + } + .col-sm-12 { + width: 100%; + } + .col-sm-11 { + width: 91.66666667%; + } + .col-sm-10 { + width: 83.33333333%; + } + .col-sm-9 { + width: 75%; + } + .col-sm-8 { + width: 66.66666667%; + } + .col-sm-7 { + width: 58.33333333%; + } + .col-sm-6 { + width: 50%; + } + .col-sm-5 { + width: 41.66666667%; + } + .col-sm-4 { + width: 33.33333333%; + } + .col-sm-3 { + width: 25%; + } + .col-sm-2 { + width: 16.66666667%; + } + .col-sm-1 { + width: 8.33333333%; + } + .col-sm-pull-12 { + right: 100%; + } + .col-sm-pull-11 { + right: 91.66666667%; + } + .col-sm-pull-10 { + right: 83.33333333%; + } + .col-sm-pull-9 { + right: 75%; + } + .col-sm-pull-8 { + right: 66.66666667%; + } + .col-sm-pull-7 { + right: 58.33333333%; + } + .col-sm-pull-6 { + right: 50%; + } + .col-sm-pull-5 { + right: 41.66666667%; + } + .col-sm-pull-4 { + right: 33.33333333%; + } + .col-sm-pull-3 { + right: 25%; + } + .col-sm-pull-2 { + right: 16.66666667%; + } + .col-sm-pull-1 { + right: 8.33333333%; + } + .col-sm-pull-0 { + right: auto; + } + .col-sm-push-12 { + left: 100%; + } + .col-sm-push-11 { + left: 91.66666667%; + } + .col-sm-push-10 { + left: 83.33333333%; + } + .col-sm-push-9 { + left: 75%; + } + .col-sm-push-8 { + left: 66.66666667%; + } + .col-sm-push-7 { + left: 58.33333333%; + } + .col-sm-push-6 { + left: 50%; + } + .col-sm-push-5 { + left: 41.66666667%; + } + .col-sm-push-4 { + left: 33.33333333%; + } + .col-sm-push-3 { + left: 25%; + } + .col-sm-push-2 { + left: 16.66666667%; + } + .col-sm-push-1 { + left: 8.33333333%; + } + .col-sm-push-0 { + left: auto; + } + .col-sm-offset-12 { + margin-left: 100%; + } + .col-sm-offset-11 { + margin-left: 91.66666667%; + } + .col-sm-offset-10 { + margin-left: 83.33333333%; + } + .col-sm-offset-9 { + margin-left: 75%; + } + .col-sm-offset-8 { + margin-left: 66.66666667%; + } + .col-sm-offset-7 { + margin-left: 58.33333333%; + } + .col-sm-offset-6 { + margin-left: 50%; + } + .col-sm-offset-5 { + margin-left: 41.66666667%; + } + .col-sm-offset-4 { + margin-left: 33.33333333%; + } + .col-sm-offset-3 { + margin-left: 25%; + } + .col-sm-offset-2 { + margin-left: 16.66666667%; + } + .col-sm-offset-1 { + margin-left: 8.33333333%; + } + .col-sm-offset-0 { + margin-left: 0; + } +} +@media (min-width: 992px) { + .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 { + float: left; + } + .col-md-12 { + width: 100%; + } + .col-md-11 { + width: 91.66666667%; + } + .col-md-10 { + width: 83.33333333%; + } + .col-md-9 { + width: 75%; + } + .col-md-8 { + width: 66.66666667%; + } + .col-md-7 { + width: 58.33333333%; + } + .col-md-6 { + width: 50%; + } + .col-md-5 { + width: 41.66666667%; + } + .col-md-4 { + width: 33.33333333%; + } + .col-md-3 { + width: 25%; + } + .col-md-2 { + width: 16.66666667%; + } + .col-md-1 { + width: 8.33333333%; + } + .col-md-pull-12 { + right: 100%; + } + .col-md-pull-11 { + right: 91.66666667%; + } + .col-md-pull-10 { + right: 83.33333333%; + } + .col-md-pull-9 { + right: 75%; + } + .col-md-pull-8 { + right: 66.66666667%; + } + .col-md-pull-7 { + right: 58.33333333%; + } + .col-md-pull-6 { + right: 50%; + } + .col-md-pull-5 { + right: 41.66666667%; + } + .col-md-pull-4 { + right: 33.33333333%; + } + .col-md-pull-3 { + right: 25%; + } + .col-md-pull-2 { + right: 16.66666667%; + } + .col-md-pull-1 { + right: 8.33333333%; + } + .col-md-pull-0 { + right: auto; + } + .col-md-push-12 { + left: 100%; + } + .col-md-push-11 { + left: 91.66666667%; + } + .col-md-push-10 { + left: 83.33333333%; + } + .col-md-push-9 { + left: 75%; + } + .col-md-push-8 { + left: 66.66666667%; + } + .col-md-push-7 { + left: 58.33333333%; + } + .col-md-push-6 { + left: 50%; + } + .col-md-push-5 { + left: 41.66666667%; + } + .col-md-push-4 { + left: 33.33333333%; + } + .col-md-push-3 { + left: 25%; + } + .col-md-push-2 { + left: 16.66666667%; + } + .col-md-push-1 { + left: 8.33333333%; + } + .col-md-push-0 { + left: auto; + } + .col-md-offset-12 { + margin-left: 100%; + } + .col-md-offset-11 { + margin-left: 91.66666667%; + } + .col-md-offset-10 { + margin-left: 83.33333333%; + } + .col-md-offset-9 { + margin-left: 75%; + } + .col-md-offset-8 { + margin-left: 66.66666667%; + } + .col-md-offset-7 { + margin-left: 58.33333333%; + } + .col-md-offset-6 { + margin-left: 50%; + } + .col-md-offset-5 { + margin-left: 41.66666667%; + } + .col-md-offset-4 { + margin-left: 33.33333333%; + } + .col-md-offset-3 { + margin-left: 25%; + } + .col-md-offset-2 { + margin-left: 16.66666667%; + } + .col-md-offset-1 { + margin-left: 8.33333333%; + } + .col-md-offset-0 { + margin-left: 0; + } +} +@media (min-width: 1200px) { + .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 { + float: left; + } + .col-lg-12 { + width: 100%; + } + .col-lg-11 { + width: 91.66666667%; + } + .col-lg-10 { + width: 83.33333333%; + } + .col-lg-9 { + width: 75%; + } + .col-lg-8 { + width: 66.66666667%; + } + .col-lg-7 { + width: 58.33333333%; + } + .col-lg-6 { + width: 50%; + } + .col-lg-5 { + width: 41.66666667%; + } + .col-lg-4 { + width: 33.33333333%; + } + .col-lg-3 { + width: 25%; + } + .col-lg-2 { + width: 16.66666667%; + } + .col-lg-1 { + width: 8.33333333%; + } + .col-lg-pull-12 { + right: 100%; + } + .col-lg-pull-11 { + right: 91.66666667%; + } + .col-lg-pull-10 { + right: 83.33333333%; + } + .col-lg-pull-9 { + right: 75%; + } + .col-lg-pull-8 { + right: 66.66666667%; + } + .col-lg-pull-7 { + right: 58.33333333%; + } + .col-lg-pull-6 { + right: 50%; + } + .col-lg-pull-5 { + right: 41.66666667%; + } + .col-lg-pull-4 { + right: 33.33333333%; + } + .col-lg-pull-3 { + right: 25%; + } + .col-lg-pull-2 { + right: 16.66666667%; + } + .col-lg-pull-1 { + right: 8.33333333%; + } + .col-lg-pull-0 { + right: auto; + } + .col-lg-push-12 { + left: 100%; + } + .col-lg-push-11 { + left: 91.66666667%; + } + .col-lg-push-10 { + left: 83.33333333%; + } + .col-lg-push-9 { + left: 75%; + } + .col-lg-push-8 { + left: 66.66666667%; + } + .col-lg-push-7 { + left: 58.33333333%; + } + .col-lg-push-6 { + left: 50%; + } + .col-lg-push-5 { + left: 41.66666667%; + } + .col-lg-push-4 { + left: 33.33333333%; + } + .col-lg-push-3 { + left: 25%; + } + .col-lg-push-2 { + left: 16.66666667%; + } + .col-lg-push-1 { + left: 8.33333333%; + } + .col-lg-push-0 { + left: auto; + } + .col-lg-offset-12 { + margin-left: 100%; + } + .col-lg-offset-11 { + margin-left: 91.66666667%; + } + .col-lg-offset-10 { + margin-left: 83.33333333%; + } + .col-lg-offset-9 { + margin-left: 75%; + } + .col-lg-offset-8 { + margin-left: 66.66666667%; + } + .col-lg-offset-7 { + margin-left: 58.33333333%; + } + .col-lg-offset-6 { + margin-left: 50%; + } + .col-lg-offset-5 { + margin-left: 41.66666667%; + } + .col-lg-offset-4 { + margin-left: 33.33333333%; + } + .col-lg-offset-3 { + margin-left: 25%; + } + .col-lg-offset-2 { + margin-left: 16.66666667%; + } + .col-lg-offset-1 { + margin-left: 8.33333333%; + } + .col-lg-offset-0 { + margin-left: 0; + } +} +table { + background-color: transparent; +} +caption { + padding-top: 8px; + padding-bottom: 8px; + color: #777; + text-align: left; +} +th { + text-align: left; +} +.table { + width: 100%; + max-width: 100%; + margin-bottom: 20px; +} +.table > thead > tr > th, +.table > tbody > tr > th, +.table > tfoot > tr > th, +.table > thead > tr > td, +.table > tbody > tr > td, +.table > tfoot > tr > td { + padding: 8px; + line-height: 1.42857143; + vertical-align: top; + border-top: 1px solid #ddd; +} +.table > thead > tr > th { + vertical-align: bottom; + border-bottom: 2px solid #ddd; +} +.table > caption + thead > tr:first-child > th, +.table > colgroup + thead > tr:first-child > th, +.table > thead:first-child > tr:first-child > th, +.table > caption + thead > tr:first-child > td, +.table > colgroup + thead > tr:first-child > td, +.table > thead:first-child > tr:first-child > td { + border-top: 0; +} +.table > tbody + tbody { + border-top: 2px solid #ddd; +} +.table .table { + background-color: #fff; +} +.table-condensed > thead > tr > th, +.table-condensed > tbody > tr > th, +.table-condensed > tfoot > tr > th, +.table-condensed > thead > tr > td, +.table-condensed > tbody > tr > td, +.table-condensed > tfoot > tr > td { + padding: 5px; +} +.table-bordered { + border: 1px solid #ddd; +} +.table-bordered > thead > tr > th, +.table-bordered > tbody > tr > th, +.table-bordered > tfoot > tr > th, +.table-bordered > thead > tr > td, +.table-bordered > tbody > tr > td, +.table-bordered > tfoot > tr > td { + border: 1px solid #ddd; +} +.table-bordered > thead > tr > th, +.table-bordered > thead > tr > td { + border-bottom-width: 2px; +} +.table-striped > tbody > tr:nth-of-type(odd) { + background-color: #f9f9f9; +} +.table-hover > tbody > tr:hover { + background-color: #f5f5f5; +} +table col[class*="col-"] { + position: static; + display: table-column; + float: none; +} +table td[class*="col-"], +table th[class*="col-"] { + position: static; + display: table-cell; + float: none; +} +.table > thead > tr > td.active, +.table > tbody > tr > td.active, +.table > tfoot > tr > td.active, +.table > thead > tr > th.active, +.table > tbody > tr > th.active, +.table > tfoot > tr > th.active, +.table > thead > tr.active > td, +.table > tbody > tr.active > td, +.table > tfoot > tr.active > td, +.table > thead > tr.active > th, +.table > tbody > tr.active > th, +.table > tfoot > tr.active > th { + background-color: #f5f5f5; +} +.table-hover > tbody > tr > td.active:hover, +.table-hover > tbody > tr > th.active:hover, +.table-hover > tbody > tr.active:hover > td, +.table-hover > tbody > tr:hover > .active, +.table-hover > tbody > tr.active:hover > th { + background-color: #e8e8e8; +} +.table > thead > tr > td.success, +.table > tbody > tr > td.success, +.table > tfoot > tr > td.success, +.table > thead > tr > th.success, +.table > tbody > tr > th.success, +.table > tfoot > tr > th.success, +.table > thead > tr.success > td, +.table > tbody > tr.success > td, +.table > tfoot > tr.success > td, +.table > thead > tr.success > th, +.table > tbody > tr.success > th, +.table > tfoot > tr.success > th { + background-color: #dff0d8; +} +.table-hover > tbody > tr > td.success:hover, +.table-hover > tbody > tr > th.success:hover, +.table-hover > tbody > tr.success:hover > td, +.table-hover > tbody > tr:hover > .success, +.table-hover > tbody > tr.success:hover > th { + background-color: #d0e9c6; +} +.table > thead > tr > td.info, +.table > tbody > tr > td.info, +.table > tfoot > tr > td.info, +.table > thead > tr > th.info, +.table > tbody > tr > th.info, +.table > tfoot > tr > th.info, +.table > thead > tr.info > td, +.table > tbody > tr.info > td, +.table > tfoot > tr.info > td, +.table > thead > tr.info > th, +.table > tbody > tr.info > th, +.table > tfoot > tr.info > th { + background-color: #d9edf7; +} +.table-hover > tbody > tr > td.info:hover, +.table-hover > tbody > tr > th.info:hover, +.table-hover > tbody > tr.info:hover > td, +.table-hover > tbody > tr:hover > .info, +.table-hover > tbody > tr.info:hover > th { + background-color: #c4e3f3; +} +.table > thead > tr > td.warning, +.table > tbody > tr > td.warning, +.table > tfoot > tr > td.warning, +.table > thead > tr > th.warning, +.table > tbody > tr > th.warning, +.table > tfoot > tr > th.warning, +.table > thead > tr.warning > td, +.table > tbody > tr.warning > td, +.table > tfoot > tr.warning > td, +.table > thead > tr.warning > th, +.table > tbody > tr.warning > th, +.table > tfoot > tr.warning > th { + background-color: #fcf8e3; +} +.table-hover > tbody > tr > td.warning:hover, +.table-hover > tbody > tr > th.warning:hover, +.table-hover > tbody > tr.warning:hover > td, +.table-hover > tbody > tr:hover > .warning, +.table-hover > tbody > tr.warning:hover > th { + background-color: #faf2cc; +} +.table > thead > tr > td.danger, +.table > tbody > tr > td.danger, +.table > tfoot > tr > td.danger, +.table > thead > tr > th.danger, +.table > tbody > tr > th.danger, +.table > tfoot > tr > th.danger, +.table > thead > tr.danger > td, +.table > tbody > tr.danger > td, +.table > tfoot > tr.danger > td, +.table > thead > tr.danger > th, +.table > tbody > tr.danger > th, +.table > tfoot > tr.danger > th { + background-color: #f2dede; +} +.table-hover > tbody > tr > td.danger:hover, +.table-hover > tbody > tr > th.danger:hover, +.table-hover > tbody > tr.danger:hover > td, +.table-hover > tbody > tr:hover > .danger, +.table-hover > tbody > tr.danger:hover > th { + background-color: #ebcccc; +} +.table-responsive { + min-height: .01%; + overflow-x: auto; +} +@media screen and (max-width: 767px) { + .table-responsive { + width: 100%; + margin-bottom: 15px; + overflow-y: hidden; + -ms-overflow-style: -ms-autohiding-scrollbar; + border: 1px solid #ddd; + } + .table-responsive > .table { + margin-bottom: 0; + } + .table-responsive > .table > thead > tr > th, + .table-responsive > .table > tbody > tr > th, + .table-responsive > .table > tfoot > tr > th, + .table-responsive > .table > thead > tr > td, + .table-responsive > .table > tbody > tr > td, + .table-responsive > .table > tfoot > tr > td { + white-space: nowrap; + } + .table-responsive > .table-bordered { + border: 0; + } + .table-responsive > .table-bordered > thead > tr > th:first-child, + .table-responsive > .table-bordered > tbody > tr > th:first-child, + .table-responsive > .table-bordered > tfoot > tr > th:first-child, + .table-responsive > .table-bordered > thead > tr > td:first-child, + .table-responsive > .table-bordered > tbody > tr > td:first-child, + .table-responsive > .table-bordered > tfoot > tr > td:first-child { + border-left: 0; + } + .table-responsive > .table-bordered > thead > tr > th:last-child, + .table-responsive > .table-bordered > tbody > tr > th:last-child, + .table-responsive > .table-bordered > tfoot > tr > th:last-child, + .table-responsive > .table-bordered > thead > tr > td:last-child, + .table-responsive > .table-bordered > tbody > tr > td:last-child, + .table-responsive > .table-bordered > tfoot > tr > td:last-child { + border-right: 0; + } + .table-responsive > .table-bordered > tbody > tr:last-child > th, + .table-responsive > .table-bordered > tfoot > tr:last-child > th, + .table-responsive > .table-bordered > tbody > tr:last-child > td, + .table-responsive > .table-bordered > tfoot > tr:last-child > td { + border-bottom: 0; + } +} +fieldset { + min-width: 0; + padding: 0; + margin: 0; + border: 0; +} +legend { + display: block; + width: 100%; + padding: 0; + margin-bottom: 20px; + font-size: 21px; + line-height: inherit; + color: #333; + border: 0; + border-bottom: 1px solid #e5e5e5; +} +label { + display: inline-block; + max-width: 100%; + margin-bottom: 5px; + font-weight: bold; +} +input[type="search"] { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +input[type="radio"], +input[type="checkbox"] { + margin: 4px 0 0; + margin-top: 1px \9; + line-height: normal; +} +input[type="file"] { + display: block; +} +input[type="range"] { + display: block; + width: 100%; +} +select[multiple], +select[size] { + height: auto; +} +input[type="file"]:focus, +input[type="radio"]:focus, +input[type="checkbox"]:focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +output { + display: block; + padding-top: 7px; + font-size: 14px; + line-height: 1.42857143; + color: #555; +} +.form-control { + display: block; + width: 100%; + height: 34px; + padding: 6px 12px; + font-size: 14px; + line-height: 1.42857143; + color: #555; + background-color: #fff; + background-image: none; + border: 1px solid #ccc; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + -webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s; + -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; + transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; +} +.form-control:focus { + border-color: #66afe9; + outline: 0; + -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6); + box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6); +} +.form-control::-moz-placeholder { + color: #999; + opacity: 1; +} +.form-control:-ms-input-placeholder { + color: #999; +} +.form-control::-webkit-input-placeholder { + color: #999; +} +.form-control[disabled], +.form-control[readonly], +fieldset[disabled] .form-control { + background-color: #eee; + opacity: 1; +} +.form-control[disabled], +fieldset[disabled] .form-control { + cursor: not-allowed; +} +textarea.form-control { + height: auto; +} +input[type="search"] { + -webkit-appearance: none; +} +@media screen and (-webkit-min-device-pixel-ratio: 0) { + input[type="date"], + input[type="time"], + input[type="datetime-local"], + input[type="month"] { + line-height: 34px; + } + input[type="date"].input-sm, + input[type="time"].input-sm, + input[type="datetime-local"].input-sm, + input[type="month"].input-sm, + .input-group-sm input[type="date"], + .input-group-sm input[type="time"], + .input-group-sm input[type="datetime-local"], + .input-group-sm input[type="month"] { + line-height: 30px; + } + input[type="date"].input-lg, + input[type="time"].input-lg, + input[type="datetime-local"].input-lg, + input[type="month"].input-lg, + .input-group-lg input[type="date"], + .input-group-lg input[type="time"], + .input-group-lg input[type="datetime-local"], + .input-group-lg input[type="month"] { + line-height: 46px; + } +} +.form-group { + margin-bottom: 15px; +} +.radio, +.checkbox { + position: relative; + display: block; + margin-top: 10px; + margin-bottom: 10px; +} +.radio label, +.checkbox label { + min-height: 20px; + padding-left: 20px; + margin-bottom: 0; + font-weight: normal; + cursor: pointer; +} +.radio input[type="radio"], +.radio-inline input[type="radio"], +.checkbox input[type="checkbox"], +.checkbox-inline input[type="checkbox"] { + position: absolute; + margin-top: 4px \9; + margin-left: -20px; +} +.radio + .radio, +.checkbox + .checkbox { + margin-top: -5px; +} +.radio-inline, +.checkbox-inline { + position: relative; + display: inline-block; + padding-left: 20px; + margin-bottom: 0; + font-weight: normal; + vertical-align: middle; + cursor: pointer; +} +.radio-inline + .radio-inline, +.checkbox-inline + .checkbox-inline { + margin-top: 0; + margin-left: 10px; +} +input[type="radio"][disabled], +input[type="checkbox"][disabled], +input[type="radio"].disabled, +input[type="checkbox"].disabled, +fieldset[disabled] input[type="radio"], +fieldset[disabled] input[type="checkbox"] { + cursor: not-allowed; +} +.radio-inline.disabled, +.checkbox-inline.disabled, +fieldset[disabled] .radio-inline, +fieldset[disabled] .checkbox-inline { + cursor: not-allowed; +} +.radio.disabled label, +.checkbox.disabled label, +fieldset[disabled] .radio label, +fieldset[disabled] .checkbox label { + cursor: not-allowed; +} +.form-control-static { + min-height: 34px; + padding-top: 7px; + padding-bottom: 7px; + margin-bottom: 0; +} +.form-control-static.input-lg, +.form-control-static.input-sm { + padding-right: 0; + padding-left: 0; +} +.input-sm { + height: 30px; + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +select.input-sm { + height: 30px; + line-height: 30px; +} +textarea.input-sm, +select[multiple].input-sm { + height: auto; +} +.form-group-sm .form-control { + height: 30px; + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +select.form-group-sm .form-control { + height: 30px; + line-height: 30px; +} +textarea.form-group-sm .form-control, +select[multiple].form-group-sm .form-control { + height: auto; +} +.form-group-sm .form-control-static { + height: 30px; + min-height: 32px; + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; +} +.input-lg { + height: 46px; + padding: 10px 16px; + font-size: 18px; + line-height: 1.3333333; + border-radius: 6px; +} +select.input-lg { + height: 46px; + line-height: 46px; +} +textarea.input-lg, +select[multiple].input-lg { + height: auto; +} +.form-group-lg .form-control { + height: 46px; + padding: 10px 16px; + font-size: 18px; + line-height: 1.3333333; + border-radius: 6px; +} +select.form-group-lg .form-control { + height: 46px; + line-height: 46px; +} +textarea.form-group-lg .form-control, +select[multiple].form-group-lg .form-control { + height: auto; +} +.form-group-lg .form-control-static { + height: 46px; + min-height: 38px; + padding: 10px 16px; + font-size: 18px; + line-height: 1.3333333; +} +.has-feedback { + position: relative; +} +.has-feedback .form-control { + padding-right: 42.5px; +} +.form-control-feedback { + position: absolute; + top: 0; + right: 0; + z-index: 2; + display: block; + width: 34px; + height: 34px; + line-height: 34px; + text-align: center; + pointer-events: none; +} +.input-lg + .form-control-feedback { + width: 46px; + height: 46px; + line-height: 46px; +} +.input-sm + .form-control-feedback { + width: 30px; + height: 30px; + line-height: 30px; +} +.has-success .help-block, +.has-success .control-label, +.has-success .radio, +.has-success .checkbox, +.has-success .radio-inline, +.has-success .checkbox-inline, +.has-success.radio label, +.has-success.checkbox label, +.has-success.radio-inline label, +.has-success.checkbox-inline label { + color: #3c763d; +} +.has-success .form-control { + border-color: #3c763d; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); +} +.has-success .form-control:focus { + border-color: #2b542c; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #67b168; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #67b168; +} +.has-success .input-group-addon { + color: #3c763d; + background-color: #dff0d8; + border-color: #3c763d; +} +.has-success .form-control-feedback { + color: #3c763d; +} +.has-warning .help-block, +.has-warning .control-label, +.has-warning .radio, +.has-warning .checkbox, +.has-warning .radio-inline, +.has-warning .checkbox-inline, +.has-warning.radio label, +.has-warning.checkbox label, +.has-warning.radio-inline label, +.has-warning.checkbox-inline label { + color: #8a6d3b; +} +.has-warning .form-control { + border-color: #8a6d3b; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); +} +.has-warning .form-control:focus { + border-color: #66512c; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #c0a16b; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #c0a16b; +} +.has-warning .input-group-addon { + color: #8a6d3b; + background-color: #fcf8e3; + border-color: #8a6d3b; +} +.has-warning .form-control-feedback { + color: #8a6d3b; +} +.has-error .help-block, +.has-error .control-label, +.has-error .radio, +.has-error .checkbox, +.has-error .radio-inline, +.has-error .checkbox-inline, +.has-error.radio label, +.has-error.checkbox label, +.has-error.radio-inline label, +.has-error.checkbox-inline label { + color: #a94442; +} +.has-error .form-control { + border-color: #a94442; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); +} +.has-error .form-control:focus { + border-color: #843534; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ce8483; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ce8483; +} +.has-error .input-group-addon { + color: #a94442; + background-color: #f2dede; + border-color: #a94442; +} +.has-error .form-control-feedback { + color: #a94442; +} +.has-feedback label ~ .form-control-feedback { + top: 25px; +} +.has-feedback label.sr-only ~ .form-control-feedback { + top: 0; +} +.help-block { + display: block; + margin-top: 5px; + margin-bottom: 10px; + color: #737373; +} +@media (min-width: 768px) { + .form-inline .form-group { + display: inline-block; + margin-bottom: 0; + vertical-align: middle; + } + .form-inline .form-control { + display: inline-block; + width: auto; + vertical-align: middle; + } + .form-inline .form-control-static { + display: inline-block; + } + .form-inline .input-group { + display: inline-table; + vertical-align: middle; + } + .form-inline .input-group .input-group-addon, + .form-inline .input-group .input-group-btn, + .form-inline .input-group .form-control { + width: auto; + } + .form-inline .input-group > .form-control { + width: 100%; + } + .form-inline .control-label { + margin-bottom: 0; + vertical-align: middle; + } + .form-inline .radio, + .form-inline .checkbox { + display: inline-block; + margin-top: 0; + margin-bottom: 0; + vertical-align: middle; + } + .form-inline .radio label, + .form-inline .checkbox label { + padding-left: 0; + } + .form-inline .radio input[type="radio"], + .form-inline .checkbox input[type="checkbox"] { + position: relative; + margin-left: 0; + } + .form-inline .has-feedback .form-control-feedback { + top: 0; + } +} +.form-horizontal .radio, +.form-horizontal .checkbox, +.form-horizontal .radio-inline, +.form-horizontal .checkbox-inline { + padding-top: 7px; + margin-top: 0; + margin-bottom: 0; +} +.form-horizontal .radio, +.form-horizontal .checkbox { + min-height: 27px; +} +.form-horizontal .form-group { + margin-right: -15px; + margin-left: -15px; +} +@media (min-width: 768px) { + .form-horizontal .control-label { + padding-top: 7px; + margin-bottom: 0; + text-align: right; + } +} +.form-horizontal .has-feedback .form-control-feedback { + right: 15px; +} +@media (min-width: 768px) { + .form-horizontal .form-group-lg .control-label { + padding-top: 14.333333px; + } +} +@media (min-width: 768px) { + .form-horizontal .form-group-sm .control-label { + padding-top: 6px; + } +} +.btn { + display: inline-block; + padding: 6px 12px; + margin-bottom: 0; + font-size: 14px; + font-weight: normal; + line-height: 1.42857143; + text-align: center; + white-space: nowrap; + vertical-align: middle; + -ms-touch-action: manipulation; + touch-action: manipulation; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + background-image: none; + border: 1px solid transparent; + border-radius: 4px; +} +.btn:focus, +.btn:active:focus, +.btn.active:focus, +.btn.focus, +.btn:active.focus, +.btn.active.focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +.btn:hover, +.btn:focus, +.btn.focus { + color: #333; + text-decoration: none; +} +.btn:active, +.btn.active { + background-image: none; + outline: 0; + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); +} +.btn.disabled, +.btn[disabled], +fieldset[disabled] .btn { + pointer-events: none; + cursor: not-allowed; + filter: alpha(opacity=65); + -webkit-box-shadow: none; + box-shadow: none; + opacity: .65; +} +.btn-default { + color: #333; + background-color: #fff; + border-color: #ccc; +} +.btn-default:hover, +.btn-default:focus, +.btn-default.focus, +.btn-default:active, +.btn-default.active, +.open > .dropdown-toggle.btn-default { + color: #333; + background-color: #e6e6e6; + border-color: #adadad; +} +.btn-default:active, +.btn-default.active, +.open > .dropdown-toggle.btn-default { + background-image: none; +} +.btn-default.disabled, +.btn-default[disabled], +fieldset[disabled] .btn-default, +.btn-default.disabled:hover, +.btn-default[disabled]:hover, +fieldset[disabled] .btn-default:hover, +.btn-default.disabled:focus, +.btn-default[disabled]:focus, +fieldset[disabled] .btn-default:focus, +.btn-default.disabled.focus, +.btn-default[disabled].focus, +fieldset[disabled] .btn-default.focus, +.btn-default.disabled:active, +.btn-default[disabled]:active, +fieldset[disabled] .btn-default:active, +.btn-default.disabled.active, +.btn-default[disabled].active, +fieldset[disabled] .btn-default.active { + background-color: #fff; + border-color: #ccc; +} +.btn-default .badge { + color: #fff; + background-color: #333; +} +.btn-primary { + color: #fff; + background-color: #337ab7; + border-color: #2e6da4; +} +.btn-primary:hover, +.btn-primary:focus, +.btn-primary.focus, +.btn-primary:active, +.btn-primary.active, +.open > .dropdown-toggle.btn-primary { + color: #fff; + background-color: #286090; + border-color: #204d74; +} +.btn-primary:active, +.btn-primary.active, +.open > .dropdown-toggle.btn-primary { + background-image: none; +} +.btn-primary.disabled, +.btn-primary[disabled], +fieldset[disabled] .btn-primary, +.btn-primary.disabled:hover, +.btn-primary[disabled]:hover, +fieldset[disabled] .btn-primary:hover, +.btn-primary.disabled:focus, +.btn-primary[disabled]:focus, +fieldset[disabled] .btn-primary:focus, +.btn-primary.disabled.focus, +.btn-primary[disabled].focus, +fieldset[disabled] .btn-primary.focus, +.btn-primary.disabled:active, +.btn-primary[disabled]:active, +fieldset[disabled] .btn-primary:active, +.btn-primary.disabled.active, +.btn-primary[disabled].active, +fieldset[disabled] .btn-primary.active { + background-color: #337ab7; + border-color: #2e6da4; +} +.btn-primary .badge { + color: #337ab7; + background-color: #fff; +} +.btn-success { + color: #fff; + background-color: #5cb85c; + border-color: #4cae4c; +} +.btn-success:hover, +.btn-success:focus, +.btn-success.focus, +.btn-success:active, +.btn-success.active, +.open > .dropdown-toggle.btn-success { + color: #fff; + background-color: #449d44; + border-color: #398439; +} +.btn-success:active, +.btn-success.active, +.open > .dropdown-toggle.btn-success { + background-image: none; +} +.btn-success.disabled, +.btn-success[disabled], +fieldset[disabled] .btn-success, +.btn-success.disabled:hover, +.btn-success[disabled]:hover, +fieldset[disabled] .btn-success:hover, +.btn-success.disabled:focus, +.btn-success[disabled]:focus, +fieldset[disabled] .btn-success:focus, +.btn-success.disabled.focus, +.btn-success[disabled].focus, +fieldset[disabled] .btn-success.focus, +.btn-success.disabled:active, +.btn-success[disabled]:active, +fieldset[disabled] .btn-success:active, +.btn-success.disabled.active, +.btn-success[disabled].active, +fieldset[disabled] .btn-success.active { + background-color: #5cb85c; + border-color: #4cae4c; +} +.btn-success .badge { + color: #5cb85c; + background-color: #fff; +} +.btn-info { + color: #fff; + background-color: #5bc0de; + border-color: #46b8da; +} +.btn-info:hover, +.btn-info:focus, +.btn-info.focus, +.btn-info:active, +.btn-info.active, +.open > .dropdown-toggle.btn-info { + color: #fff; + background-color: #31b0d5; + border-color: #269abc; +} +.btn-info:active, +.btn-info.active, +.open > .dropdown-toggle.btn-info { + background-image: none; +} +.btn-info.disabled, +.btn-info[disabled], +fieldset[disabled] .btn-info, +.btn-info.disabled:hover, +.btn-info[disabled]:hover, +fieldset[disabled] .btn-info:hover, +.btn-info.disabled:focus, +.btn-info[disabled]:focus, +fieldset[disabled] .btn-info:focus, +.btn-info.disabled.focus, +.btn-info[disabled].focus, +fieldset[disabled] .btn-info.focus, +.btn-info.disabled:active, +.btn-info[disabled]:active, +fieldset[disabled] .btn-info:active, +.btn-info.disabled.active, +.btn-info[disabled].active, +fieldset[disabled] .btn-info.active { + background-color: #5bc0de; + border-color: #46b8da; +} +.btn-info .badge { + color: #5bc0de; + background-color: #fff; +} +.btn-warning { + color: #fff; + background-color: #f0ad4e; + border-color: #eea236; +} +.btn-warning:hover, +.btn-warning:focus, +.btn-warning.focus, +.btn-warning:active, +.btn-warning.active, +.open > .dropdown-toggle.btn-warning { + color: #fff; + background-color: #ec971f; + border-color: #d58512; +} +.btn-warning:active, +.btn-warning.active, +.open > .dropdown-toggle.btn-warning { + background-image: none; +} +.btn-warning.disabled, +.btn-warning[disabled], +fieldset[disabled] .btn-warning, +.btn-warning.disabled:hover, +.btn-warning[disabled]:hover, +fieldset[disabled] .btn-warning:hover, +.btn-warning.disabled:focus, +.btn-warning[disabled]:focus, +fieldset[disabled] .btn-warning:focus, +.btn-warning.disabled.focus, +.btn-warning[disabled].focus, +fieldset[disabled] .btn-warning.focus, +.btn-warning.disabled:active, +.btn-warning[disabled]:active, +fieldset[disabled] .btn-warning:active, +.btn-warning.disabled.active, +.btn-warning[disabled].active, +fieldset[disabled] .btn-warning.active { + background-color: #f0ad4e; + border-color: #eea236; +} +.btn-warning .badge { + color: #f0ad4e; + background-color: #fff; +} +.btn-danger { + color: #fff; + background-color: #d9534f; + border-color: #d43f3a; +} +.btn-danger:hover, +.btn-danger:focus, +.btn-danger.focus, +.btn-danger:active, +.btn-danger.active, +.open > .dropdown-toggle.btn-danger { + color: #fff; + background-color: #c9302c; + border-color: #ac2925; +} +.btn-danger:active, +.btn-danger.active, +.open > .dropdown-toggle.btn-danger { + background-image: none; +} +.btn-danger.disabled, +.btn-danger[disabled], +fieldset[disabled] .btn-danger, +.btn-danger.disabled:hover, +.btn-danger[disabled]:hover, +fieldset[disabled] .btn-danger:hover, +.btn-danger.disabled:focus, +.btn-danger[disabled]:focus, +fieldset[disabled] .btn-danger:focus, +.btn-danger.disabled.focus, +.btn-danger[disabled].focus, +fieldset[disabled] .btn-danger.focus, +.btn-danger.disabled:active, +.btn-danger[disabled]:active, +fieldset[disabled] .btn-danger:active, +.btn-danger.disabled.active, +.btn-danger[disabled].active, +fieldset[disabled] .btn-danger.active { + background-color: #d9534f; + border-color: #d43f3a; +} +.btn-danger .badge { + color: #d9534f; + background-color: #fff; +} +.btn-link { + font-weight: normal; + color: #337ab7; + border-radius: 0; +} +.btn-link, +.btn-link:active, +.btn-link.active, +.btn-link[disabled], +fieldset[disabled] .btn-link { + background-color: transparent; + -webkit-box-shadow: none; + box-shadow: none; +} +.btn-link, +.btn-link:hover, +.btn-link:focus, +.btn-link:active { + border-color: transparent; +} +.btn-link:hover, +.btn-link:focus { + color: #23527c; + text-decoration: underline; + background-color: transparent; +} +.btn-link[disabled]:hover, +fieldset[disabled] .btn-link:hover, +.btn-link[disabled]:focus, +fieldset[disabled] .btn-link:focus { + color: #777; + text-decoration: none; +} +.btn-lg, +.btn-group-lg > .btn { + padding: 10px 16px; + font-size: 18px; + line-height: 1.3333333; + border-radius: 6px; +} +.btn-sm, +.btn-group-sm > .btn { + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +.btn-xs, +.btn-group-xs > .btn { + padding: 1px 5px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +.btn-block { + display: block; + width: 100%; +} +.btn-block + .btn-block { + margin-top: 5px; +} +input[type="submit"].btn-block, +input[type="reset"].btn-block, +input[type="button"].btn-block { + width: 100%; +} +.fade { + opacity: 0; + -webkit-transition: opacity .15s linear; + -o-transition: opacity .15s linear; + transition: opacity .15s linear; +} +.fade.in { + opacity: 1; +} +.collapse { + display: none; +} +.collapse.in { + display: block; +} +tr.collapse.in { + display: table-row; +} +tbody.collapse.in { + display: table-row-group; +} +.collapsing { + position: relative; + height: 0; + overflow: hidden; + -webkit-transition-timing-function: ease; + -o-transition-timing-function: ease; + transition-timing-function: ease; + -webkit-transition-duration: .35s; + -o-transition-duration: .35s; + transition-duration: .35s; + -webkit-transition-property: height, visibility; + -o-transition-property: height, visibility; + transition-property: height, visibility; +} +.caret { + display: inline-block; + width: 0; + height: 0; + margin-left: 2px; + vertical-align: middle; + border-top: 4px dashed; + border-right: 4px solid transparent; + border-left: 4px solid transparent; +} +.dropup, +.dropdown { + position: relative; +} +.dropdown-toggle:focus { + outline: 0; +} +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + float: left; + min-width: 160px; + padding: 5px 0; + margin: 2px 0 0; + font-size: 14px; + text-align: left; + list-style: none; + background-color: #fff; + -webkit-background-clip: padding-box; + background-clip: padding-box; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, .15); + border-radius: 4px; + -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, .175); + box-shadow: 0 6px 12px rgba(0, 0, 0, .175); +} +.dropdown-menu.pull-right { + right: 0; + left: auto; +} +.dropdown-menu .divider { + height: 1px; + margin: 9px 0; + overflow: hidden; + background-color: #e5e5e5; +} +.dropdown-menu > li > a { + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: 1.42857143; + color: #333; + white-space: nowrap; +} +.dropdown-menu > li > a:hover, +.dropdown-menu > li > a:focus { + color: #262626; + text-decoration: none; + background-color: #f5f5f5; +} +.dropdown-menu > .active > a, +.dropdown-menu > .active > a:hover, +.dropdown-menu > .active > a:focus { + color: #fff; + text-decoration: none; + background-color: #337ab7; + outline: 0; +} +.dropdown-menu > .disabled > a, +.dropdown-menu > .disabled > a:hover, +.dropdown-menu > .disabled > a:focus { + color: #777; +} +.dropdown-menu > .disabled > a:hover, +.dropdown-menu > .disabled > a:focus { + text-decoration: none; + cursor: not-allowed; + background-color: transparent; + background-image: none; + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); +} +.open > .dropdown-menu { + display: block; +} +.open > a { + outline: 0; +} +.dropdown-menu-right { + right: 0; + left: auto; +} +.dropdown-menu-left { + right: auto; + left: 0; +} +.dropdown-header { + display: block; + padding: 3px 20px; + font-size: 12px; + line-height: 1.42857143; + color: #777; + white-space: nowrap; +} +.dropdown-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 990; +} +.pull-right > .dropdown-menu { + right: 0; + left: auto; +} +.dropup .caret, +.navbar-fixed-bottom .dropdown .caret { + content: ""; + border-top: 0; + border-bottom: 4px solid; +} +.dropup .dropdown-menu, +.navbar-fixed-bottom .dropdown .dropdown-menu { + top: auto; + bottom: 100%; + margin-bottom: 2px; +} +@media (min-width: 768px) { + .navbar-right .dropdown-menu { + right: 0; + left: auto; + } + .navbar-right .dropdown-menu-left { + right: auto; + left: 0; + } +} +.btn-group, +.btn-group-vertical { + position: relative; + display: inline-block; + vertical-align: middle; +} +.btn-group > .btn, +.btn-group-vertical > .btn { + position: relative; + float: left; +} +.btn-group > .btn:hover, +.btn-group-vertical > .btn:hover, +.btn-group > .btn:focus, +.btn-group-vertical > .btn:focus, +.btn-group > .btn:active, +.btn-group-vertical > .btn:active, +.btn-group > .btn.active, +.btn-group-vertical > .btn.active { + z-index: 2; +} +.btn-group .btn + .btn, +.btn-group .btn + .btn-group, +.btn-group .btn-group + .btn, +.btn-group .btn-group + .btn-group { + margin-left: -1px; +} +.btn-toolbar { + margin-left: -5px; +} +.btn-toolbar .btn-group, +.btn-toolbar .input-group { + float: left; +} +.btn-toolbar > .btn, +.btn-toolbar > .btn-group, +.btn-toolbar > .input-group { + margin-left: 5px; +} +.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { + border-radius: 0; +} +.btn-group > .btn:first-child { + margin-left: 0; +} +.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.btn-group > .btn:last-child:not(:first-child), +.btn-group > .dropdown-toggle:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +.btn-group > .btn-group { + float: left; +} +.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} +.btn-group > .btn-group:first-child:not(:last-child) > .btn:last-child, +.btn-group > .btn-group:first-child:not(:last-child) > .dropdown-toggle { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +.btn-group .dropdown-toggle:active, +.btn-group.open .dropdown-toggle { + outline: 0; +} +.btn-group > .btn + .dropdown-toggle { + padding-right: 8px; + padding-left: 8px; +} +.btn-group > .btn-lg + .dropdown-toggle { + padding-right: 12px; + padding-left: 12px; +} +.btn-group.open .dropdown-toggle { + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); +} +.btn-group.open .dropdown-toggle.btn-link { + -webkit-box-shadow: none; + box-shadow: none; +} +.btn .caret { + margin-left: 0; +} +.btn-lg .caret { + border-width: 5px 5px 0; + border-bottom-width: 0; +} +.dropup .btn-lg .caret { + border-width: 0 5px 5px; +} +.btn-group-vertical > .btn, +.btn-group-vertical > .btn-group, +.btn-group-vertical > .btn-group > .btn { + display: block; + float: none; + width: 100%; + max-width: 100%; +} +.btn-group-vertical > .btn-group > .btn { + float: none; +} +.btn-group-vertical > .btn + .btn, +.btn-group-vertical > .btn + .btn-group, +.btn-group-vertical > .btn-group + .btn, +.btn-group-vertical > .btn-group + .btn-group { + margin-top: -1px; + margin-left: 0; +} +.btn-group-vertical > .btn:not(:first-child):not(:last-child) { + border-radius: 0; +} +.btn-group-vertical > .btn:first-child:not(:last-child) { + border-top-right-radius: 4px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.btn-group-vertical > .btn:last-child:not(:first-child) { + border-top-left-radius: 0; + border-top-right-radius: 0; + border-bottom-left-radius: 4px; +} +.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} +.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child, +.btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child { + border-top-left-radius: 0; + border-top-right-radius: 0; +} +.btn-group-justified { + display: table; + width: 100%; + table-layout: fixed; + border-collapse: separate; +} +.btn-group-justified > .btn, +.btn-group-justified > .btn-group { + display: table-cell; + float: none; + width: 1%; +} +.btn-group-justified > .btn-group .btn { + width: 100%; +} +.btn-group-justified > .btn-group .dropdown-menu { + left: auto; +} +[data-toggle="buttons"] > .btn input[type="radio"], +[data-toggle="buttons"] > .btn-group > .btn input[type="radio"], +[data-toggle="buttons"] > .btn input[type="checkbox"], +[data-toggle="buttons"] > .btn-group > .btn input[type="checkbox"] { + position: absolute; + clip: rect(0, 0, 0, 0); + pointer-events: none; +} +.input-group { + position: relative; + display: table; + border-collapse: separate; +} +.input-group[class*="col-"] { + float: none; + padding-right: 0; + padding-left: 0; +} +.input-group .form-control { + position: relative; + z-index: 2; + float: left; + width: 100%; + margin-bottom: 0; +} +.input-group-lg > .form-control, +.input-group-lg > .input-group-addon, +.input-group-lg > .input-group-btn > .btn { + height: 46px; + padding: 10px 16px; + font-size: 18px; + line-height: 1.3333333; + border-radius: 6px; +} +select.input-group-lg > .form-control, +select.input-group-lg > .input-group-addon, +select.input-group-lg > .input-group-btn > .btn { + height: 46px; + line-height: 46px; +} +textarea.input-group-lg > .form-control, +textarea.input-group-lg > .input-group-addon, +textarea.input-group-lg > .input-group-btn > .btn, +select[multiple].input-group-lg > .form-control, +select[multiple].input-group-lg > .input-group-addon, +select[multiple].input-group-lg > .input-group-btn > .btn { + height: auto; +} +.input-group-sm > .form-control, +.input-group-sm > .input-group-addon, +.input-group-sm > .input-group-btn > .btn { + height: 30px; + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +select.input-group-sm > .form-control, +select.input-group-sm > .input-group-addon, +select.input-group-sm > .input-group-btn > .btn { + height: 30px; + line-height: 30px; +} +textarea.input-group-sm > .form-control, +textarea.input-group-sm > .input-group-addon, +textarea.input-group-sm > .input-group-btn > .btn, +select[multiple].input-group-sm > .form-control, +select[multiple].input-group-sm > .input-group-addon, +select[multiple].input-group-sm > .input-group-btn > .btn { + height: auto; +} +.input-group-addon, +.input-group-btn, +.input-group .form-control { + display: table-cell; +} +.input-group-addon:not(:first-child):not(:last-child), +.input-group-btn:not(:first-child):not(:last-child), +.input-group .form-control:not(:first-child):not(:last-child) { + border-radius: 0; +} +.input-group-addon, +.input-group-btn { + width: 1%; + white-space: nowrap; + vertical-align: middle; +} +.input-group-addon { + padding: 6px 12px; + font-size: 14px; + font-weight: normal; + line-height: 1; + color: #555; + text-align: center; + background-color: #eee; + border: 1px solid #ccc; + border-radius: 4px; +} +.input-group-addon.input-sm { + padding: 5px 10px; + font-size: 12px; + border-radius: 3px; +} +.input-group-addon.input-lg { + padding: 10px 16px; + font-size: 18px; + border-radius: 6px; +} +.input-group-addon input[type="radio"], +.input-group-addon input[type="checkbox"] { + margin-top: 0; +} +.input-group .form-control:first-child, +.input-group-addon:first-child, +.input-group-btn:first-child > .btn, +.input-group-btn:first-child > .btn-group > .btn, +.input-group-btn:first-child > .dropdown-toggle, +.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle), +.input-group-btn:last-child > .btn-group:not(:last-child) > .btn { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.input-group-addon:first-child { + border-right: 0; +} +.input-group .form-control:last-child, +.input-group-addon:last-child, +.input-group-btn:last-child > .btn, +.input-group-btn:last-child > .btn-group > .btn, +.input-group-btn:last-child > .dropdown-toggle, +.input-group-btn:first-child > .btn:not(:first-child), +.input-group-btn:first-child > .btn-group:not(:first-child) > .btn { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +.input-group-addon:last-child { + border-left: 0; +} +.input-group-btn { + position: relative; + font-size: 0; + white-space: nowrap; +} +.input-group-btn > .btn { + position: relative; +} +.input-group-btn > .btn + .btn { + margin-left: -1px; +} +.input-group-btn > .btn:hover, +.input-group-btn > .btn:focus, +.input-group-btn > .btn:active { + z-index: 2; +} +.input-group-btn:first-child > .btn, +.input-group-btn:first-child > .btn-group { + margin-right: -1px; +} +.input-group-btn:last-child > .btn, +.input-group-btn:last-child > .btn-group { + margin-left: -1px; +} +.nav { + padding-left: 0; + margin-bottom: 0; + list-style: none; +} +.nav > li { + position: relative; + display: block; +} +.nav > li > a { + position: relative; + display: block; + padding: 10px 15px; +} +.nav > li > a:hover, +.nav > li > a:focus { + text-decoration: none; + background-color: #eee; +} +.nav > li.disabled > a { + color: #777; +} +.nav > li.disabled > a:hover, +.nav > li.disabled > a:focus { + color: #777; + text-decoration: none; + cursor: not-allowed; + background-color: transparent; +} +.nav .open > a, +.nav .open > a:hover, +.nav .open > a:focus { + background-color: #eee; + border-color: #337ab7; +} +.nav .nav-divider { + height: 1px; + margin: 9px 0; + overflow: hidden; + background-color: #e5e5e5; +} +.nav > li > a > img { + max-width: none; +} +.nav-tabs { + border-bottom: 1px solid #ddd; +} +.nav-tabs > li { + float: left; + margin-bottom: -1px; +} +.nav-tabs > li > a { + margin-right: 2px; + line-height: 1.42857143; + border: 1px solid transparent; + border-radius: 4px 4px 0 0; +} +.nav-tabs > li > a:hover { + border-color: #eee #eee #ddd; +} +.nav-tabs > li.active > a, +.nav-tabs > li.active > a:hover, +.nav-tabs > li.active > a:focus { + color: #555; + cursor: default; + background-color: #fff; + border: 1px solid #ddd; + border-bottom-color: transparent; +} +.nav-tabs.nav-justified { + width: 100%; + border-bottom: 0; +} +.nav-tabs.nav-justified > li { + float: none; +} +.nav-tabs.nav-justified > li > a { + margin-bottom: 5px; + text-align: center; +} +.nav-tabs.nav-justified > .dropdown .dropdown-menu { + top: auto; + left: auto; +} +@media (min-width: 768px) { + .nav-tabs.nav-justified > li { + display: table-cell; + width: 1%; + } + .nav-tabs.nav-justified > li > a { + margin-bottom: 0; + } +} +.nav-tabs.nav-justified > li > a { + margin-right: 0; + border-radius: 4px; +} +.nav-tabs.nav-justified > .active > a, +.nav-tabs.nav-justified > .active > a:hover, +.nav-tabs.nav-justified > .active > a:focus { + border: 1px solid #ddd; +} +@media (min-width: 768px) { + .nav-tabs.nav-justified > li > a { + border-bottom: 1px solid #ddd; + border-radius: 4px 4px 0 0; + } + .nav-tabs.nav-justified > .active > a, + .nav-tabs.nav-justified > .active > a:hover, + .nav-tabs.nav-justified > .active > a:focus { + border-bottom-color: #fff; + } +} +.nav-pills > li { + float: left; +} +.nav-pills > li > a { + border-radius: 4px; +} +.nav-pills > li + li { + margin-left: 2px; +} +.nav-pills > li.active > a, +.nav-pills > li.active > a:hover, +.nav-pills > li.active > a:focus { + color: #fff; + background-color: #337ab7; +} +.nav-stacked > li { + float: none; +} +.nav-stacked > li + li { + margin-top: 2px; + margin-left: 0; +} +.nav-justified { + width: 100%; +} +.nav-justified > li { + float: none; +} +.nav-justified > li > a { + margin-bottom: 5px; + text-align: center; +} +.nav-justified > .dropdown .dropdown-menu { + top: auto; + left: auto; +} +@media (min-width: 768px) { + .nav-justified > li { + display: table-cell; + width: 1%; + } + .nav-justified > li > a { + margin-bottom: 0; + } +} +.nav-tabs-justified { + border-bottom: 0; +} +.nav-tabs-justified > li > a { + margin-right: 0; + border-radius: 4px; +} +.nav-tabs-justified > .active > a, +.nav-tabs-justified > .active > a:hover, +.nav-tabs-justified > .active > a:focus { + border: 1px solid #ddd; +} +@media (min-width: 768px) { + .nav-tabs-justified > li > a { + border-bottom: 1px solid #ddd; + border-radius: 4px 4px 0 0; + } + .nav-tabs-justified > .active > a, + .nav-tabs-justified > .active > a:hover, + .nav-tabs-justified > .active > a:focus { + border-bottom-color: #fff; + } +} +.tab-content > .tab-pane { + display: none; +} +.tab-content > .active { + display: block; +} +.nav-tabs .dropdown-menu { + margin-top: -1px; + border-top-left-radius: 0; + border-top-right-radius: 0; +} +.navbar { + position: relative; + min-height: 50px; + margin-bottom: 20px; + border: 1px solid transparent; +} +@media (min-width: 768px) { + .navbar { + border-radius: 4px; + } +} +@media (min-width: 768px) { + .navbar-header { + float: left; + } +} +.navbar-collapse { + padding-right: 15px; + padding-left: 15px; + overflow-x: visible; + -webkit-overflow-scrolling: touch; + border-top: 1px solid transparent; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1); +} +.navbar-collapse.in { + overflow-y: auto; +} +@media (min-width: 768px) { + .navbar-collapse { + width: auto; + border-top: 0; + -webkit-box-shadow: none; + box-shadow: none; + } + .navbar-collapse.collapse { + display: block !important; + height: auto !important; + padding-bottom: 0; + overflow: visible !important; + } + .navbar-collapse.in { + overflow-y: visible; + } + .navbar-fixed-top .navbar-collapse, + .navbar-static-top .navbar-collapse, + .navbar-fixed-bottom .navbar-collapse { + padding-right: 0; + padding-left: 0; + } +} +.navbar-fixed-top .navbar-collapse, +.navbar-fixed-bottom .navbar-collapse { + max-height: 340px; +} +@media (max-device-width: 480px) and (orientation: landscape) { + .navbar-fixed-top .navbar-collapse, + .navbar-fixed-bottom .navbar-collapse { + max-height: 200px; + } +} +.container > .navbar-header, +.container-fluid > .navbar-header, +.container > .navbar-collapse, +.container-fluid > .navbar-collapse { + margin-right: -15px; + margin-left: -15px; +} +@media (min-width: 768px) { + .container > .navbar-header, + .container-fluid > .navbar-header, + .container > .navbar-collapse, + .container-fluid > .navbar-collapse { + margin-right: 0; + margin-left: 0; + } +} +.navbar-static-top { + z-index: 1000; + border-width: 0 0 1px; +} +@media (min-width: 768px) { + .navbar-static-top { + border-radius: 0; + } +} +.navbar-fixed-top, +.navbar-fixed-bottom { + position: fixed; + right: 0; + left: 0; + z-index: 1030; +} +@media (min-width: 768px) { + .navbar-fixed-top, + .navbar-fixed-bottom { + border-radius: 0; + } +} +.navbar-fixed-top { + top: 0; + border-width: 0 0 1px; +} +.navbar-fixed-bottom { + bottom: 0; + margin-bottom: 0; + border-width: 1px 0 0; +} +.navbar-brand { + float: left; + height: 50px; + padding: 15px 15px; + font-size: 18px; + line-height: 20px; +} +.navbar-brand:hover, +.navbar-brand:focus { + text-decoration: none; +} +.navbar-brand > img { + display: block; +} +@media (min-width: 768px) { + .navbar > .container .navbar-brand, + .navbar > .container-fluid .navbar-brand { + margin-left: -15px; + } +} +.navbar-toggle { + position: relative; + float: right; + padding: 9px 10px; + margin-top: 8px; + margin-right: 15px; + margin-bottom: 8px; + background-color: transparent; + background-image: none; + border: 1px solid transparent; + border-radius: 4px; +} +.navbar-toggle:focus { + outline: 0; +} +.navbar-toggle .icon-bar { + display: block; + width: 22px; + height: 2px; + border-radius: 1px; +} +.navbar-toggle .icon-bar + .icon-bar { + margin-top: 4px; +} +@media (min-width: 768px) { + .navbar-toggle { + display: none; + } +} +.navbar-nav { + margin: 7.5px -15px; +} +.navbar-nav > li > a { + padding-top: 10px; + padding-bottom: 10px; + line-height: 20px; +} +@media (max-width: 767px) { + .navbar-nav .open .dropdown-menu { + position: static; + float: none; + width: auto; + margin-top: 0; + background-color: transparent; + border: 0; + -webkit-box-shadow: none; + box-shadow: none; + } + .navbar-nav .open .dropdown-menu > li > a, + .navbar-nav .open .dropdown-menu .dropdown-header { + padding: 5px 15px 5px 25px; + } + .navbar-nav .open .dropdown-menu > li > a { + line-height: 20px; + } + .navbar-nav .open .dropdown-menu > li > a:hover, + .navbar-nav .open .dropdown-menu > li > a:focus { + background-image: none; + } +} +@media (min-width: 768px) { + .navbar-nav { + float: left; + margin: 0; + } + .navbar-nav > li { + float: left; + } + .navbar-nav > li > a { + padding-top: 15px; + padding-bottom: 15px; + } +} +.navbar-form { + padding: 10px 15px; + margin-top: 8px; + margin-right: -15px; + margin-bottom: 8px; + margin-left: -15px; + border-top: 1px solid transparent; + border-bottom: 1px solid transparent; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 1px 0 rgba(255, 255, 255, .1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 1px 0 rgba(255, 255, 255, .1); +} +@media (min-width: 768px) { + .navbar-form .form-group { + display: inline-block; + margin-bottom: 0; + vertical-align: middle; + } + .navbar-form .form-control { + display: inline-block; + width: auto; + vertical-align: middle; + } + .navbar-form .form-control-static { + display: inline-block; + } + .navbar-form .input-group { + display: inline-table; + vertical-align: middle; + } + .navbar-form .input-group .input-group-addon, + .navbar-form .input-group .input-group-btn, + .navbar-form .input-group .form-control { + width: auto; + } + .navbar-form .input-group > .form-control { + width: 100%; + } + .navbar-form .control-label { + margin-bottom: 0; + vertical-align: middle; + } + .navbar-form .radio, + .navbar-form .checkbox { + display: inline-block; + margin-top: 0; + margin-bottom: 0; + vertical-align: middle; + } + .navbar-form .radio label, + .navbar-form .checkbox label { + padding-left: 0; + } + .navbar-form .radio input[type="radio"], + .navbar-form .checkbox input[type="checkbox"] { + position: relative; + margin-left: 0; + } + .navbar-form .has-feedback .form-control-feedback { + top: 0; + } +} +@media (max-width: 767px) { + .navbar-form .form-group { + margin-bottom: 5px; + } + .navbar-form .form-group:last-child { + margin-bottom: 0; + } +} +@media (min-width: 768px) { + .navbar-form { + width: auto; + padding-top: 0; + padding-bottom: 0; + margin-right: 0; + margin-left: 0; + border: 0; + -webkit-box-shadow: none; + box-shadow: none; + } +} +.navbar-nav > li > .dropdown-menu { + margin-top: 0; + border-top-left-radius: 0; + border-top-right-radius: 0; +} +.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu { + margin-bottom: 0; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.navbar-btn { + margin-top: 8px; + margin-bottom: 8px; +} +.navbar-btn.btn-sm { + margin-top: 10px; + margin-bottom: 10px; +} +.navbar-btn.btn-xs { + margin-top: 14px; + margin-bottom: 14px; +} +.navbar-text { + margin-top: 15px; + margin-bottom: 15px; +} +@media (min-width: 768px) { + .navbar-text { + float: left; + margin-right: 15px; + margin-left: 15px; + } +} +@media (min-width: 768px) { + .navbar-left { + float: left !important; + } + .navbar-right { + float: right !important; + margin-right: -15px; + } + .navbar-right ~ .navbar-right { + margin-right: 0; + } +} +.navbar-default { + background-color: #f8f8f8; + border-color: #e7e7e7; +} +.navbar-default .navbar-brand { + color: #777; +} +.navbar-default .navbar-brand:hover, +.navbar-default .navbar-brand:focus { + color: #5e5e5e; + background-color: transparent; +} +.navbar-default .navbar-text { + color: #777; +} +.navbar-default .navbar-nav > li > a { + color: #777; +} +.navbar-default .navbar-nav > li > a:hover, +.navbar-default .navbar-nav > li > a:focus { + color: #333; + background-color: transparent; +} +.navbar-default .navbar-nav > .active > a, +.navbar-default .navbar-nav > .active > a:hover, +.navbar-default .navbar-nav > .active > a:focus { + color: #555; + background-color: #e7e7e7; +} +.navbar-default .navbar-nav > .disabled > a, +.navbar-default .navbar-nav > .disabled > a:hover, +.navbar-default .navbar-nav > .disabled > a:focus { + color: #ccc; + background-color: transparent; +} +.navbar-default .navbar-toggle { + border-color: #ddd; +} +.navbar-default .navbar-toggle:hover, +.navbar-default .navbar-toggle:focus { + background-color: #ddd; +} +.navbar-default .navbar-toggle .icon-bar { + background-color: #888; +} +.navbar-default .navbar-collapse, +.navbar-default .navbar-form { + border-color: #e7e7e7; +} +.navbar-default .navbar-nav > .open > a, +.navbar-default .navbar-nav > .open > a:hover, +.navbar-default .navbar-nav > .open > a:focus { + color: #555; + background-color: #e7e7e7; +} +@media (max-width: 767px) { + .navbar-default .navbar-nav .open .dropdown-menu > li > a { + color: #777; + } + .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover, + .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus { + color: #333; + background-color: transparent; + } + .navbar-default .navbar-nav .open .dropdown-menu > .active > a, + .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover, + .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus { + color: #555; + background-color: #e7e7e7; + } + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a, + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover, + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus { + color: #ccc; + background-color: transparent; + } +} +.navbar-default .navbar-link { + color: #777; +} +.navbar-default .navbar-link:hover { + color: #333; +} +.navbar-default .btn-link { + color: #777; +} +.navbar-default .btn-link:hover, +.navbar-default .btn-link:focus { + color: #333; +} +.navbar-default .btn-link[disabled]:hover, +fieldset[disabled] .navbar-default .btn-link:hover, +.navbar-default .btn-link[disabled]:focus, +fieldset[disabled] .navbar-default .btn-link:focus { + color: #ccc; +} +.navbar-inverse { + background-color: #222; + border-color: #080808; +} +.navbar-inverse .navbar-brand { + color: #9d9d9d; +} +.navbar-inverse .navbar-brand:hover, +.navbar-inverse .navbar-brand:focus { + color: #fff; + background-color: transparent; +} +.navbar-inverse .navbar-text { + color: #9d9d9d; +} +.navbar-inverse .navbar-nav > li > a { + color: #9d9d9d; +} +.navbar-inverse .navbar-nav > li > a:hover, +.navbar-inverse .navbar-nav > li > a:focus { + color: #fff; + background-color: transparent; +} +.navbar-inverse .navbar-nav > .active > a, +.navbar-inverse .navbar-nav > .active > a:hover, +.navbar-inverse .navbar-nav > .active > a:focus { + color: #fff; + background-color: #080808; +} +.navbar-inverse .navbar-nav > .disabled > a, +.navbar-inverse .navbar-nav > .disabled > a:hover, +.navbar-inverse .navbar-nav > .disabled > a:focus { + color: #444; + background-color: transparent; +} +.navbar-inverse .navbar-toggle { + border-color: #333; +} +.navbar-inverse .navbar-toggle:hover, +.navbar-inverse .navbar-toggle:focus { + background-color: #333; +} +.navbar-inverse .navbar-toggle .icon-bar { + background-color: #fff; +} +.navbar-inverse .navbar-collapse, +.navbar-inverse .navbar-form { + border-color: #101010; +} +.navbar-inverse .navbar-nav > .open > a, +.navbar-inverse .navbar-nav > .open > a:hover, +.navbar-inverse .navbar-nav > .open > a:focus { + color: #fff; + background-color: #080808; +} +@media (max-width: 767px) { + .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header { + border-color: #080808; + } + .navbar-inverse .navbar-nav .open .dropdown-menu .divider { + background-color: #080808; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a { + color: #9d9d9d; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus { + color: #fff; + background-color: transparent; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a, + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus { + color: #fff; + background-color: #080808; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a, + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus { + color: #444; + background-color: transparent; + } +} +.navbar-inverse .navbar-link { + color: #9d9d9d; +} +.navbar-inverse .navbar-link:hover { + color: #fff; +} +.navbar-inverse .btn-link { + color: #9d9d9d; +} +.navbar-inverse .btn-link:hover, +.navbar-inverse .btn-link:focus { + color: #fff; +} +.navbar-inverse .btn-link[disabled]:hover, +fieldset[disabled] .navbar-inverse .btn-link:hover, +.navbar-inverse .btn-link[disabled]:focus, +fieldset[disabled] .navbar-inverse .btn-link:focus { + color: #444; +} +.breadcrumb { + padding: 8px 15px; + margin-bottom: 20px; + list-style: none; + background-color: #f5f5f5; + border-radius: 4px; +} +.breadcrumb > li { + display: inline-block; +} +.breadcrumb > li + li:before { + padding: 0 5px; + color: #ccc; + content: "/\00a0"; +} +.breadcrumb > .active { + color: #777; +} +.pagination { + display: inline-block; + padding-left: 0; + margin: 20px 0; + border-radius: 4px; +} +.pagination > li { + display: inline; +} +.pagination > li > a, +.pagination > li > span { + position: relative; + float: left; + padding: 6px 12px; + margin-left: -1px; + line-height: 1.42857143; + color: #337ab7; + text-decoration: none; + background-color: #fff; + border: 1px solid #ddd; +} +.pagination > li:first-child > a, +.pagination > li:first-child > span { + margin-left: 0; + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; +} +.pagination > li:last-child > a, +.pagination > li:last-child > span { + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; +} +.pagination > li > a:hover, +.pagination > li > span:hover, +.pagination > li > a:focus, +.pagination > li > span:focus { + color: #23527c; + background-color: #eee; + border-color: #ddd; +} +.pagination > .active > a, +.pagination > .active > span, +.pagination > .active > a:hover, +.pagination > .active > span:hover, +.pagination > .active > a:focus, +.pagination > .active > span:focus { + z-index: 2; + color: #fff; + cursor: default; + background-color: #337ab7; + border-color: #337ab7; +} +.pagination > .disabled > span, +.pagination > .disabled > span:hover, +.pagination > .disabled > span:focus, +.pagination > .disabled > a, +.pagination > .disabled > a:hover, +.pagination > .disabled > a:focus { + color: #777; + cursor: not-allowed; + background-color: #fff; + border-color: #ddd; +} +.pagination-lg > li > a, +.pagination-lg > li > span { + padding: 10px 16px; + font-size: 18px; +} +.pagination-lg > li:first-child > a, +.pagination-lg > li:first-child > span { + border-top-left-radius: 6px; + border-bottom-left-radius: 6px; +} +.pagination-lg > li:last-child > a, +.pagination-lg > li:last-child > span { + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; +} +.pagination-sm > li > a, +.pagination-sm > li > span { + padding: 5px 10px; + font-size: 12px; +} +.pagination-sm > li:first-child > a, +.pagination-sm > li:first-child > span { + border-top-left-radius: 3px; + border-bottom-left-radius: 3px; +} +.pagination-sm > li:last-child > a, +.pagination-sm > li:last-child > span { + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; +} +.pager { + padding-left: 0; + margin: 20px 0; + text-align: center; + list-style: none; +} +.pager li { + display: inline; +} +.pager li > a, +.pager li > span { + display: inline-block; + padding: 5px 14px; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 15px; +} +.pager li > a:hover, +.pager li > a:focus { + text-decoration: none; + background-color: #eee; +} +.pager .next > a, +.pager .next > span { + float: right; +} +.pager .previous > a, +.pager .previous > span { + float: left; +} +.pager .disabled > a, +.pager .disabled > a:hover, +.pager .disabled > a:focus, +.pager .disabled > span { + color: #777; + cursor: not-allowed; + background-color: #fff; +} +.label { + display: inline; + padding: .2em .6em .3em; + font-size: 75%; + font-weight: bold; + line-height: 1; + color: #fff; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: .25em; +} +a.label:hover, +a.label:focus { + color: #fff; + text-decoration: none; + cursor: pointer; +} +.label:empty { + display: none; +} +.btn .label { + position: relative; + top: -1px; +} +.label-default { + background-color: #777; +} +.label-default[href]:hover, +.label-default[href]:focus { + background-color: #5e5e5e; +} +.label-primary { + background-color: #337ab7; +} +.label-primary[href]:hover, +.label-primary[href]:focus { + background-color: #286090; +} +.label-success { + background-color: #5cb85c; +} +.label-success[href]:hover, +.label-success[href]:focus { + background-color: #449d44; +} +.label-info { + background-color: #5bc0de; +} +.label-info[href]:hover, +.label-info[href]:focus { + background-color: #31b0d5; +} +.label-warning { + background-color: #f0ad4e; +} +.label-warning[href]:hover, +.label-warning[href]:focus { + background-color: #ec971f; +} +.label-danger { + background-color: #d9534f; +} +.label-danger[href]:hover, +.label-danger[href]:focus { + background-color: #c9302c; +} +.badge { + display: inline-block; + min-width: 10px; + padding: 3px 7px; + font-size: 12px; + font-weight: bold; + line-height: 1; + color: #fff; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + background-color: #777; + border-radius: 10px; +} +.badge:empty { + display: none; +} +.btn .badge { + position: relative; + top: -1px; +} +.btn-xs .badge, +.btn-group-xs > .btn .badge { + top: 0; + padding: 1px 5px; +} +a.badge:hover, +a.badge:focus { + color: #fff; + text-decoration: none; + cursor: pointer; +} +.list-group-item.active > .badge, +.nav-pills > .active > a > .badge { + color: #337ab7; + background-color: #fff; +} +.list-group-item > .badge { + float: right; +} +.list-group-item > .badge + .badge { + margin-right: 5px; +} +.nav-pills > li > a > .badge { + margin-left: 3px; +} +.jumbotron { + padding: 30px 15px; + margin-bottom: 30px; + color: inherit; + background-color: #eee; +} +.jumbotron h1, +.jumbotron .h1 { + color: inherit; +} +.jumbotron p { + margin-bottom: 15px; + font-size: 21px; + font-weight: 200; +} +.jumbotron > hr { + border-top-color: #d5d5d5; +} +.container .jumbotron, +.container-fluid .jumbotron { + border-radius: 6px; +} +.jumbotron .container { + max-width: 100%; +} +@media screen and (min-width: 768px) { + .jumbotron { + padding: 48px 0; + } + .container .jumbotron, + .container-fluid .jumbotron { + padding-right: 60px; + padding-left: 60px; + } + .jumbotron h1, + .jumbotron .h1 { + font-size: 63px; + } +} +.thumbnail { + display: block; + padding: 4px; + margin-bottom: 20px; + line-height: 1.42857143; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 4px; + -webkit-transition: border .2s ease-in-out; + -o-transition: border .2s ease-in-out; + transition: border .2s ease-in-out; +} +.thumbnail > img, +.thumbnail a > img { + margin-right: auto; + margin-left: auto; +} +a.thumbnail:hover, +a.thumbnail:focus, +a.thumbnail.active { + border-color: #337ab7; +} +.thumbnail .caption { + padding: 9px; + color: #333; +} +.alert { + padding: 15px; + margin-bottom: 20px; + border: 1px solid transparent; + border-radius: 4px; +} +.alert h4 { + margin-top: 0; + color: inherit; +} +.alert .alert-link { + font-weight: bold; +} +.alert > p, +.alert > ul { + margin-bottom: 0; +} +.alert > p + p { + margin-top: 5px; +} +.alert-dismissable, +.alert-dismissible { + padding-right: 35px; +} +.alert-dismissable .close, +.alert-dismissible .close { + position: relative; + top: -2px; + right: -21px; + color: inherit; +} +.alert-success { + color: #3c763d; + background-color: #dff0d8; + border-color: #d6e9c6; +} +.alert-success hr { + border-top-color: #c9e2b3; +} +.alert-success .alert-link { + color: #2b542c; +} +.alert-info { + color: #31708f; + background-color: #d9edf7; + border-color: #bce8f1; +} +.alert-info hr { + border-top-color: #a6e1ec; +} +.alert-info .alert-link { + color: #245269; +} +.alert-warning { + color: #8a6d3b; + background-color: #fcf8e3; + border-color: #faebcc; +} +.alert-warning hr { + border-top-color: #f7e1b5; +} +.alert-warning .alert-link { + color: #66512c; +} +.alert-danger { + color: #a94442; + background-color: #f2dede; + border-color: #ebccd1; +} +.alert-danger hr { + border-top-color: #e4b9c0; +} +.alert-danger .alert-link { + color: #843534; +} +@-webkit-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} +@-o-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} +@keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} +.progress { + height: 20px; + margin-bottom: 20px; + overflow: hidden; + background-color: #f5f5f5; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1); +} +.progress-bar { + float: left; + width: 0; + height: 100%; + font-size: 12px; + line-height: 20px; + color: #fff; + text-align: center; + background-color: #337ab7; + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15); + -webkit-transition: width .6s ease; + -o-transition: width .6s ease; + transition: width .6s ease; +} +.progress-striped .progress-bar, +.progress-bar-striped { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + -webkit-background-size: 40px 40px; + background-size: 40px 40px; +} +.progress.active .progress-bar, +.progress-bar.active { + -webkit-animation: progress-bar-stripes 2s linear infinite; + -o-animation: progress-bar-stripes 2s linear infinite; + animation: progress-bar-stripes 2s linear infinite; +} +.progress-bar-success { + background-color: #5cb85c; +} +.progress-striped .progress-bar-success { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); +} +.progress-bar-info { + background-color: #5bc0de; +} +.progress-striped .progress-bar-info { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); +} +.progress-bar-warning { + background-color: #f0ad4e; +} +.progress-striped .progress-bar-warning { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); +} +.progress-bar-danger { + background-color: #d9534f; +} +.progress-striped .progress-bar-danger { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); +} +.media { + margin-top: 15px; +} +.media:first-child { + margin-top: 0; +} +.media, +.media-body { + overflow: hidden; + zoom: 1; +} +.media-body { + width: 10000px; +} +.media-object { + display: block; +} +.media-right, +.media > .pull-right { + padding-left: 10px; +} +.media-left, +.media > .pull-left { + padding-right: 10px; +} +.media-left, +.media-right, +.media-body { + display: table-cell; + vertical-align: top; +} +.media-middle { + vertical-align: middle; +} +.media-bottom { + vertical-align: bottom; +} +.media-heading { + margin-top: 0; + margin-bottom: 5px; +} +.media-list { + padding-left: 0; + list-style: none; +} +.list-group { + padding-left: 0; + margin-bottom: 20px; +} +.list-group-item { + position: relative; + display: block; + padding: 10px 15px; + margin-bottom: -1px; + background-color: #fff; + border: 1px solid #ddd; +} +.list-group-item:first-child { + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} +.list-group-item:last-child { + margin-bottom: 0; + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; +} +a.list-group-item { + color: #555; +} +a.list-group-item .list-group-item-heading { + color: #333; +} +a.list-group-item:hover, +a.list-group-item:focus { + color: #555; + text-decoration: none; + background-color: #f5f5f5; +} +.list-group-item.disabled, +.list-group-item.disabled:hover, +.list-group-item.disabled:focus { + color: #777; + cursor: not-allowed; + background-color: #eee; +} +.list-group-item.disabled .list-group-item-heading, +.list-group-item.disabled:hover .list-group-item-heading, +.list-group-item.disabled:focus .list-group-item-heading { + color: inherit; +} +.list-group-item.disabled .list-group-item-text, +.list-group-item.disabled:hover .list-group-item-text, +.list-group-item.disabled:focus .list-group-item-text { + color: #777; +} +.list-group-item.active, +.list-group-item.active:hover, +.list-group-item.active:focus { + z-index: 2; + color: #fff; + background-color: #337ab7; + border-color: #337ab7; +} +.list-group-item.active .list-group-item-heading, +.list-group-item.active:hover .list-group-item-heading, +.list-group-item.active:focus .list-group-item-heading, +.list-group-item.active .list-group-item-heading > small, +.list-group-item.active:hover .list-group-item-heading > small, +.list-group-item.active:focus .list-group-item-heading > small, +.list-group-item.active .list-group-item-heading > .small, +.list-group-item.active:hover .list-group-item-heading > .small, +.list-group-item.active:focus .list-group-item-heading > .small { + color: inherit; +} +.list-group-item.active .list-group-item-text, +.list-group-item.active:hover .list-group-item-text, +.list-group-item.active:focus .list-group-item-text { + color: #c7ddef; +} +.list-group-item-success { + color: #3c763d; + background-color: #dff0d8; +} +a.list-group-item-success { + color: #3c763d; +} +a.list-group-item-success .list-group-item-heading { + color: inherit; +} +a.list-group-item-success:hover, +a.list-group-item-success:focus { + color: #3c763d; + background-color: #d0e9c6; +} +a.list-group-item-success.active, +a.list-group-item-success.active:hover, +a.list-group-item-success.active:focus { + color: #fff; + background-color: #3c763d; + border-color: #3c763d; +} +.list-group-item-info { + color: #31708f; + background-color: #d9edf7; +} +a.list-group-item-info { + color: #31708f; +} +a.list-group-item-info .list-group-item-heading { + color: inherit; +} +a.list-group-item-info:hover, +a.list-group-item-info:focus { + color: #31708f; + background-color: #c4e3f3; +} +a.list-group-item-info.active, +a.list-group-item-info.active:hover, +a.list-group-item-info.active:focus { + color: #fff; + background-color: #31708f; + border-color: #31708f; +} +.list-group-item-warning { + color: #8a6d3b; + background-color: #fcf8e3; +} +a.list-group-item-warning { + color: #8a6d3b; +} +a.list-group-item-warning .list-group-item-heading { + color: inherit; +} +a.list-group-item-warning:hover, +a.list-group-item-warning:focus { + color: #8a6d3b; + background-color: #faf2cc; +} +a.list-group-item-warning.active, +a.list-group-item-warning.active:hover, +a.list-group-item-warning.active:focus { + color: #fff; + background-color: #8a6d3b; + border-color: #8a6d3b; +} +.list-group-item-danger { + color: #a94442; + background-color: #f2dede; +} +a.list-group-item-danger { + color: #a94442; +} +a.list-group-item-danger .list-group-item-heading { + color: inherit; +} +a.list-group-item-danger:hover, +a.list-group-item-danger:focus { + color: #a94442; + background-color: #ebcccc; +} +a.list-group-item-danger.active, +a.list-group-item-danger.active:hover, +a.list-group-item-danger.active:focus { + color: #fff; + background-color: #a94442; + border-color: #a94442; +} +.list-group-item-heading { + margin-top: 0; + margin-bottom: 5px; +} +.list-group-item-text { + margin-bottom: 0; + line-height: 1.3; +} +.panel { + margin-bottom: 20px; + background-color: #fff; + border: 1px solid transparent; + border-radius: 4px; + -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, .05); + box-shadow: 0 1px 1px rgba(0, 0, 0, .05); +} +.panel-body { + padding: 15px; +} +.panel-heading { + padding: 10px 15px; + border-bottom: 1px solid transparent; + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} +.panel-heading > .dropdown .dropdown-toggle { + color: inherit; +} +.panel-title { + margin-top: 0; + margin-bottom: 0; + font-size: 16px; + color: inherit; +} +.panel-title > a, +.panel-title > small, +.panel-title > .small, +.panel-title > small > a, +.panel-title > .small > a { + color: inherit; +} +.panel-footer { + padding: 10px 15px; + background-color: #f5f5f5; + border-top: 1px solid #ddd; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} +.panel > .list-group, +.panel > .panel-collapse > .list-group { + margin-bottom: 0; +} +.panel > .list-group .list-group-item, +.panel > .panel-collapse > .list-group .list-group-item { + border-width: 1px 0; + border-radius: 0; +} +.panel > .list-group:first-child .list-group-item:first-child, +.panel > .panel-collapse > .list-group:first-child .list-group-item:first-child { + border-top: 0; + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} +.panel > .list-group:last-child .list-group-item:last-child, +.panel > .panel-collapse > .list-group:last-child .list-group-item:last-child { + border-bottom: 0; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} +.panel-heading + .list-group .list-group-item:first-child { + border-top-width: 0; +} +.list-group + .panel-footer { + border-top-width: 0; +} +.panel > .table, +.panel > .table-responsive > .table, +.panel > .panel-collapse > .table { + margin-bottom: 0; +} +.panel > .table caption, +.panel > .table-responsive > .table caption, +.panel > .panel-collapse > .table caption { + padding-right: 15px; + padding-left: 15px; +} +.panel > .table:first-child, +.panel > .table-responsive:first-child > .table:first-child { + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} +.panel > .table:first-child > thead:first-child > tr:first-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child, +.panel > .table:first-child > tbody:first-child > tr:first-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child { + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} +.panel > .table:first-child > thead:first-child > tr:first-child td:first-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child, +.panel > .table:first-child > tbody:first-child > tr:first-child td:first-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child, +.panel > .table:first-child > thead:first-child > tr:first-child th:first-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child, +.panel > .table:first-child > tbody:first-child > tr:first-child th:first-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child { + border-top-left-radius: 3px; +} +.panel > .table:first-child > thead:first-child > tr:first-child td:last-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child, +.panel > .table:first-child > tbody:first-child > tr:first-child td:last-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child, +.panel > .table:first-child > thead:first-child > tr:first-child th:last-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child, +.panel > .table:first-child > tbody:first-child > tr:first-child th:last-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child { + border-top-right-radius: 3px; +} +.panel > .table:last-child, +.panel > .table-responsive:last-child > .table:last-child { + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} +.panel > .table:last-child > tbody:last-child > tr:last-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child { + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} +.panel > .table:last-child > tbody:last-child > tr:last-child td:first-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child, +.panel > .table:last-child > tbody:last-child > tr:last-child th:first-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child { + border-bottom-left-radius: 3px; +} +.panel > .table:last-child > tbody:last-child > tr:last-child td:last-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child, +.panel > .table:last-child > tbody:last-child > tr:last-child th:last-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child { + border-bottom-right-radius: 3px; +} +.panel > .panel-body + .table, +.panel > .panel-body + .table-responsive, +.panel > .table + .panel-body, +.panel > .table-responsive + .panel-body { + border-top: 1px solid #ddd; +} +.panel > .table > tbody:first-child > tr:first-child th, +.panel > .table > tbody:first-child > tr:first-child td { + border-top: 0; +} +.panel > .table-bordered, +.panel > .table-responsive > .table-bordered { + border: 0; +} +.panel > .table-bordered > thead > tr > th:first-child, +.panel > .table-responsive > .table-bordered > thead > tr > th:first-child, +.panel > .table-bordered > tbody > tr > th:first-child, +.panel > .table-responsive > .table-bordered > tbody > tr > th:first-child, +.panel > .table-bordered > tfoot > tr > th:first-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child, +.panel > .table-bordered > thead > tr > td:first-child, +.panel > .table-responsive > .table-bordered > thead > tr > td:first-child, +.panel > .table-bordered > tbody > tr > td:first-child, +.panel > .table-responsive > .table-bordered > tbody > tr > td:first-child, +.panel > .table-bordered > tfoot > tr > td:first-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child { + border-left: 0; +} +.panel > .table-bordered > thead > tr > th:last-child, +.panel > .table-responsive > .table-bordered > thead > tr > th:last-child, +.panel > .table-bordered > tbody > tr > th:last-child, +.panel > .table-responsive > .table-bordered > tbody > tr > th:last-child, +.panel > .table-bordered > tfoot > tr > th:last-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child, +.panel > .table-bordered > thead > tr > td:last-child, +.panel > .table-responsive > .table-bordered > thead > tr > td:last-child, +.panel > .table-bordered > tbody > tr > td:last-child, +.panel > .table-responsive > .table-bordered > tbody > tr > td:last-child, +.panel > .table-bordered > tfoot > tr > td:last-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child { + border-right: 0; +} +.panel > .table-bordered > thead > tr:first-child > td, +.panel > .table-responsive > .table-bordered > thead > tr:first-child > td, +.panel > .table-bordered > tbody > tr:first-child > td, +.panel > .table-responsive > .table-bordered > tbody > tr:first-child > td, +.panel > .table-bordered > thead > tr:first-child > th, +.panel > .table-responsive > .table-bordered > thead > tr:first-child > th, +.panel > .table-bordered > tbody > tr:first-child > th, +.panel > .table-responsive > .table-bordered > tbody > tr:first-child > th { + border-bottom: 0; +} +.panel > .table-bordered > tbody > tr:last-child > td, +.panel > .table-responsive > .table-bordered > tbody > tr:last-child > td, +.panel > .table-bordered > tfoot > tr:last-child > td, +.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td, +.panel > .table-bordered > tbody > tr:last-child > th, +.panel > .table-responsive > .table-bordered > tbody > tr:last-child > th, +.panel > .table-bordered > tfoot > tr:last-child > th, +.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th { + border-bottom: 0; +} +.panel > .table-responsive { + margin-bottom: 0; + border: 0; +} +.panel-group { + margin-bottom: 20px; +} +.panel-group .panel { + margin-bottom: 0; + border-radius: 4px; +} +.panel-group .panel + .panel { + margin-top: 5px; +} +.panel-group .panel-heading { + border-bottom: 0; +} +.panel-group .panel-heading + .panel-collapse > .panel-body, +.panel-group .panel-heading + .panel-collapse > .list-group { + border-top: 1px solid #ddd; +} +.panel-group .panel-footer { + border-top: 0; +} +.panel-group .panel-footer + .panel-collapse .panel-body { + border-bottom: 1px solid #ddd; +} +.panel-default { + border-color: #ddd; +} +.panel-default > .panel-heading { + color: #333; + background-color: #f5f5f5; + border-color: #ddd; +} +.panel-default > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #ddd; +} +.panel-default > .panel-heading .badge { + color: #f5f5f5; + background-color: #333; +} +.panel-default > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #ddd; +} +.panel-primary { + border-color: #337ab7; +} +.panel-primary > .panel-heading { + color: #fff; + background-color: #337ab7; + border-color: #337ab7; +} +.panel-primary > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #337ab7; +} +.panel-primary > .panel-heading .badge { + color: #337ab7; + background-color: #fff; +} +.panel-primary > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #337ab7; +} +.panel-success { + border-color: #d6e9c6; +} +.panel-success > .panel-heading { + color: #3c763d; + background-color: #dff0d8; + border-color: #d6e9c6; +} +.panel-success > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #d6e9c6; +} +.panel-success > .panel-heading .badge { + color: #dff0d8; + background-color: #3c763d; +} +.panel-success > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #d6e9c6; +} +.panel-info { + border-color: #bce8f1; +} +.panel-info > .panel-heading { + color: #31708f; + background-color: #d9edf7; + border-color: #bce8f1; +} +.panel-info > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #bce8f1; +} +.panel-info > .panel-heading .badge { + color: #d9edf7; + background-color: #31708f; +} +.panel-info > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #bce8f1; +} +.panel-warning { + border-color: #faebcc; +} +.panel-warning > .panel-heading { + color: #8a6d3b; + background-color: #fcf8e3; + border-color: #faebcc; +} +.panel-warning > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #faebcc; +} +.panel-warning > .panel-heading .badge { + color: #fcf8e3; + background-color: #8a6d3b; +} +.panel-warning > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #faebcc; +} +.panel-danger { + border-color: #ebccd1; +} +.panel-danger > .panel-heading { + color: #a94442; + background-color: #f2dede; + border-color: #ebccd1; +} +.panel-danger > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #ebccd1; +} +.panel-danger > .panel-heading .badge { + color: #f2dede; + background-color: #a94442; +} +.panel-danger > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #ebccd1; +} +.embed-responsive { + position: relative; + display: block; + height: 0; + padding: 0; + overflow: hidden; +} +.embed-responsive .embed-responsive-item, +.embed-responsive iframe, +.embed-responsive embed, +.embed-responsive object, +.embed-responsive video { + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 100%; + height: 100%; + border: 0; +} +.embed-responsive-16by9 { + padding-bottom: 56.25%; +} +.embed-responsive-4by3 { + padding-bottom: 75%; +} +.well { + min-height: 20px; + padding: 19px; + margin-bottom: 20px; + background-color: #f5f5f5; + border: 1px solid #e3e3e3; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05); +} +.well blockquote { + border-color: #ddd; + border-color: rgba(0, 0, 0, .15); +} +.well-lg { + padding: 24px; + border-radius: 6px; +} +.well-sm { + padding: 9px; + border-radius: 3px; +} +.close { + float: right; + font-size: 21px; + font-weight: bold; + line-height: 1; + color: #000; + text-shadow: 0 1px 0 #fff; + filter: alpha(opacity=20); + opacity: .2; +} +.close:hover, +.close:focus { + color: #000; + text-decoration: none; + cursor: pointer; + filter: alpha(opacity=50); + opacity: .5; +} +button.close { + -webkit-appearance: none; + padding: 0; + cursor: pointer; + background: transparent; + border: 0; +} +.modal-open { + overflow: hidden; +} +.modal { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1050; + display: none; + overflow: hidden; + -webkit-overflow-scrolling: touch; + outline: 0; +} +.modal.fade .modal-dialog { + -webkit-transition: -webkit-transform .3s ease-out; + -o-transition: -o-transform .3s ease-out; + transition: transform .3s ease-out; + -webkit-transform: translate(0, -25%); + -ms-transform: translate(0, -25%); + -o-transform: translate(0, -25%); + transform: translate(0, -25%); +} +.modal.in .modal-dialog { + -webkit-transform: translate(0, 0); + -ms-transform: translate(0, 0); + -o-transform: translate(0, 0); + transform: translate(0, 0); +} +.modal-open .modal { + overflow-x: hidden; + overflow-y: auto; +} +.modal-dialog { + position: relative; + width: auto; + margin: 10px; +} +.modal-content { + position: relative; + background-color: #fff; + -webkit-background-clip: padding-box; + background-clip: padding-box; + border: 1px solid #999; + border: 1px solid rgba(0, 0, 0, .2); + border-radius: 6px; + outline: 0; + -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, .5); + box-shadow: 0 3px 9px rgba(0, 0, 0, .5); +} +.modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + background-color: #000; +} +.modal-backdrop.fade { + filter: alpha(opacity=0); + opacity: 0; +} +.modal-backdrop.in { + filter: alpha(opacity=50); + opacity: .5; +} +.modal-header { + min-height: 16.42857143px; + padding: 15px; + border-bottom: 1px solid #e5e5e5; +} +.modal-header .close { + margin-top: -2px; +} +.modal-title { + margin: 0; + line-height: 1.42857143; +} +.modal-body { + position: relative; + padding: 15px; +} +.modal-footer { + padding: 15px; + text-align: right; + border-top: 1px solid #e5e5e5; +} +.modal-footer .btn + .btn { + margin-bottom: 0; + margin-left: 5px; +} +.modal-footer .btn-group .btn + .btn { + margin-left: -1px; +} +.modal-footer .btn-block + .btn-block { + margin-left: 0; +} +.modal-scrollbar-measure { + position: absolute; + top: -9999px; + width: 50px; + height: 50px; + overflow: scroll; +} +@media (min-width: 768px) { + .modal-dialog { + width: 600px; + margin: 30px auto; + } + .modal-content { + -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, .5); + box-shadow: 0 5px 15px rgba(0, 0, 0, .5); + } + .modal-sm { + width: 300px; + } +} +@media (min-width: 992px) { + .modal-lg { + width: 900px; + } +} +.tooltip { + position: absolute; + z-index: 1070; + display: block; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 12px; + font-weight: normal; + line-height: 1.4; + filter: alpha(opacity=0); + opacity: 0; +} +.tooltip.in { + filter: alpha(opacity=90); + opacity: .9; +} +.tooltip.top { + padding: 5px 0; + margin-top: -3px; +} +.tooltip.right { + padding: 0 5px; + margin-left: 3px; +} +.tooltip.bottom { + padding: 5px 0; + margin-top: 3px; +} +.tooltip.left { + padding: 0 5px; + margin-left: -3px; +} +.tooltip-inner { + max-width: 200px; + padding: 3px 8px; + color: #fff; + text-align: center; + text-decoration: none; + background-color: #000; + border-radius: 4px; +} +.tooltip-arrow { + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} +.tooltip.top .tooltip-arrow { + bottom: 0; + left: 50%; + margin-left: -5px; + border-width: 5px 5px 0; + border-top-color: #000; +} +.tooltip.top-left .tooltip-arrow { + right: 5px; + bottom: 0; + margin-bottom: -5px; + border-width: 5px 5px 0; + border-top-color: #000; +} +.tooltip.top-right .tooltip-arrow { + bottom: 0; + left: 5px; + margin-bottom: -5px; + border-width: 5px 5px 0; + border-top-color: #000; +} +.tooltip.right .tooltip-arrow { + top: 50%; + left: 0; + margin-top: -5px; + border-width: 5px 5px 5px 0; + border-right-color: #000; +} +.tooltip.left .tooltip-arrow { + top: 50%; + right: 0; + margin-top: -5px; + border-width: 5px 0 5px 5px; + border-left-color: #000; +} +.tooltip.bottom .tooltip-arrow { + top: 0; + left: 50%; + margin-left: -5px; + border-width: 0 5px 5px; + border-bottom-color: #000; +} +.tooltip.bottom-left .tooltip-arrow { + top: 0; + right: 5px; + margin-top: -5px; + border-width: 0 5px 5px; + border-bottom-color: #000; +} +.tooltip.bottom-right .tooltip-arrow { + top: 0; + left: 5px; + margin-top: -5px; + border-width: 0 5px 5px; + border-bottom-color: #000; +} +.popover { + position: absolute; + top: 0; + left: 0; + z-index: 1060; + display: none; + max-width: 276px; + padding: 1px; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + font-weight: normal; + line-height: 1.42857143; + text-align: left; + white-space: normal; + background-color: #fff; + -webkit-background-clip: padding-box; + background-clip: padding-box; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, .2); + border-radius: 6px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, .2); + box-shadow: 0 5px 10px rgba(0, 0, 0, .2); +} +.popover.top { + margin-top: -10px; +} +.popover.right { + margin-left: 10px; +} +.popover.bottom { + margin-top: 10px; +} +.popover.left { + margin-left: -10px; +} +.popover-title { + padding: 8px 14px; + margin: 0; + font-size: 14px; + background-color: #f7f7f7; + border-bottom: 1px solid #ebebeb; + border-radius: 5px 5px 0 0; +} +.popover-content { + padding: 9px 14px; +} +.popover > .arrow, +.popover > .arrow:after { + position: absolute; + display: block; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} +.popover > .arrow { + border-width: 11px; +} +.popover > .arrow:after { + content: ""; + border-width: 10px; +} +.popover.top > .arrow { + bottom: -11px; + left: 50%; + margin-left: -11px; + border-top-color: #999; + border-top-color: rgba(0, 0, 0, .25); + border-bottom-width: 0; +} +.popover.top > .arrow:after { + bottom: 1px; + margin-left: -10px; + content: " "; + border-top-color: #fff; + border-bottom-width: 0; +} +.popover.right > .arrow { + top: 50%; + left: -11px; + margin-top: -11px; + border-right-color: #999; + border-right-color: rgba(0, 0, 0, .25); + border-left-width: 0; +} +.popover.right > .arrow:after { + bottom: -10px; + left: 1px; + content: " "; + border-right-color: #fff; + border-left-width: 0; +} +.popover.bottom > .arrow { + top: -11px; + left: 50%; + margin-left: -11px; + border-top-width: 0; + border-bottom-color: #999; + border-bottom-color: rgba(0, 0, 0, .25); +} +.popover.bottom > .arrow:after { + top: 1px; + margin-left: -10px; + content: " "; + border-top-width: 0; + border-bottom-color: #fff; +} +.popover.left > .arrow { + top: 50%; + right: -11px; + margin-top: -11px; + border-right-width: 0; + border-left-color: #999; + border-left-color: rgba(0, 0, 0, .25); +} +.popover.left > .arrow:after { + right: 1px; + bottom: -10px; + content: " "; + border-right-width: 0; + border-left-color: #fff; +} +.carousel { + position: relative; +} +.carousel-inner { + position: relative; + width: 100%; + overflow: hidden; +} +.carousel-inner > .item { + position: relative; + display: none; + -webkit-transition: .6s ease-in-out left; + -o-transition: .6s ease-in-out left; + transition: .6s ease-in-out left; +} +.carousel-inner > .item > img, +.carousel-inner > .item > a > img { + line-height: 1; +} +@media all and (transform-3d), (-webkit-transform-3d) { + .carousel-inner > .item { + -webkit-transition: -webkit-transform .6s ease-in-out; + -o-transition: -o-transform .6s ease-in-out; + transition: transform .6s ease-in-out; + + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + -webkit-perspective: 1000; + perspective: 1000; + } + .carousel-inner > .item.next, + .carousel-inner > .item.active.right { + left: 0; + -webkit-transform: translate3d(100%, 0, 0); + transform: translate3d(100%, 0, 0); + } + .carousel-inner > .item.prev, + .carousel-inner > .item.active.left { + left: 0; + -webkit-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); + } + .carousel-inner > .item.next.left, + .carousel-inner > .item.prev.right, + .carousel-inner > .item.active { + left: 0; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} +.carousel-inner > .active, +.carousel-inner > .next, +.carousel-inner > .prev { + display: block; +} +.carousel-inner > .active { + left: 0; +} +.carousel-inner > .next, +.carousel-inner > .prev { + position: absolute; + top: 0; + width: 100%; +} +.carousel-inner > .next { + left: 100%; +} +.carousel-inner > .prev { + left: -100%; +} +.carousel-inner > .next.left, +.carousel-inner > .prev.right { + left: 0; +} +.carousel-inner > .active.left { + left: -100%; +} +.carousel-inner > .active.right { + left: 100%; +} +.carousel-control { + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 15%; + font-size: 20px; + color: #fff; + text-align: center; + text-shadow: 0 1px 2px rgba(0, 0, 0, .6); + filter: alpha(opacity=50); + opacity: .5; +} +.carousel-control.left { + background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%); + background-image: -o-linear-gradient(left, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%); + background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, .5)), to(rgba(0, 0, 0, .0001))); + background-image: linear-gradient(to right, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1); + background-repeat: repeat-x; +} +.carousel-control.right { + right: 0; + left: auto; + background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%); + background-image: -o-linear-gradient(left, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%); + background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, .0001)), to(rgba(0, 0, 0, .5))); + background-image: linear-gradient(to right, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1); + background-repeat: repeat-x; +} +.carousel-control:hover, +.carousel-control:focus { + color: #fff; + text-decoration: none; + filter: alpha(opacity=90); + outline: 0; + opacity: .9; +} +.carousel-control .icon-prev, +.carousel-control .icon-next, +.carousel-control .glyphicon-chevron-left, +.carousel-control .glyphicon-chevron-right { + position: absolute; + top: 50%; + z-index: 5; + display: inline-block; +} +.carousel-control .icon-prev, +.carousel-control .glyphicon-chevron-left { + left: 50%; + margin-left: -10px; +} +.carousel-control .icon-next, +.carousel-control .glyphicon-chevron-right { + right: 50%; + margin-right: -10px; +} +.carousel-control .icon-prev, +.carousel-control .icon-next { + width: 20px; + height: 20px; + margin-top: -10px; + font-family: serif; + line-height: 1; +} +.carousel-control .icon-prev:before { + content: '\2039'; +} +.carousel-control .icon-next:before { + content: '\203a'; +} +.carousel-indicators { + position: absolute; + bottom: 10px; + left: 50%; + z-index: 15; + width: 60%; + padding-left: 0; + margin-left: -30%; + text-align: center; + list-style: none; +} +.carousel-indicators li { + display: inline-block; + width: 10px; + height: 10px; + margin: 1px; + text-indent: -999px; + cursor: pointer; + background-color: #000 \9; + background-color: rgba(0, 0, 0, 0); + border: 1px solid #fff; + border-radius: 10px; +} +.carousel-indicators .active { + width: 12px; + height: 12px; + margin: 0; + background-color: #fff; +} +.carousel-caption { + position: absolute; + right: 15%; + bottom: 20px; + left: 15%; + z-index: 10; + padding-top: 20px; + padding-bottom: 20px; + color: #fff; + text-align: center; + text-shadow: 0 1px 2px rgba(0, 0, 0, .6); +} +.carousel-caption .btn { + text-shadow: none; +} +@media screen and (min-width: 768px) { + .carousel-control .glyphicon-chevron-left, + .carousel-control .glyphicon-chevron-right, + .carousel-control .icon-prev, + .carousel-control .icon-next { + width: 30px; + height: 30px; + margin-top: -15px; + font-size: 30px; + } + .carousel-control .glyphicon-chevron-left, + .carousel-control .icon-prev { + margin-left: -15px; + } + .carousel-control .glyphicon-chevron-right, + .carousel-control .icon-next { + margin-right: -15px; + } + .carousel-caption { + right: 20%; + left: 20%; + padding-bottom: 30px; + } + .carousel-indicators { + bottom: 20px; + } +} +.clearfix:before, +.clearfix:after, +.dl-horizontal dd:before, +.dl-horizontal dd:after, +.container:before, +.container:after, +.container-fluid:before, +.container-fluid:after, +.row:before, +.row:after, +.form-horizontal .form-group:before, +.form-horizontal .form-group:after, +.btn-toolbar:before, +.btn-toolbar:after, +.btn-group-vertical > .btn-group:before, +.btn-group-vertical > .btn-group:after, +.nav:before, +.nav:after, +.navbar:before, +.navbar:after, +.navbar-header:before, +.navbar-header:after, +.navbar-collapse:before, +.navbar-collapse:after, +.pager:before, +.pager:after, +.panel-body:before, +.panel-body:after, +.modal-footer:before, +.modal-footer:after { + display: table; + content: " "; +} +.clearfix:after, +.dl-horizontal dd:after, +.container:after, +.container-fluid:after, +.row:after, +.form-horizontal .form-group:after, +.btn-toolbar:after, +.btn-group-vertical > .btn-group:after, +.nav:after, +.navbar:after, +.navbar-header:after, +.navbar-collapse:after, +.pager:after, +.panel-body:after, +.modal-footer:after { + clear: both; +} +.center-block { + display: block; + margin-right: auto; + margin-left: auto; +} +.pull-right { + float: right !important; +} +.pull-left { + float: left !important; +} +.hide { + display: none !important; +} +.show { + display: block !important; +} +.invisible { + visibility: hidden; +} +.text-hide { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} +.hidden { + display: none !important; +} +.affix { + position: fixed; +} +@-ms-viewport { + width: device-width; +} +.visible-xs, +.visible-sm, +.visible-md, +.visible-lg { + display: none !important; +} +.visible-xs-block, +.visible-xs-inline, +.visible-xs-inline-block, +.visible-sm-block, +.visible-sm-inline, +.visible-sm-inline-block, +.visible-md-block, +.visible-md-inline, +.visible-md-inline-block, +.visible-lg-block, +.visible-lg-inline, +.visible-lg-inline-block { + display: none !important; +} +@media (max-width: 767px) { + .visible-xs { + display: block !important; + } + table.visible-xs { + display: table; + } + tr.visible-xs { + display: table-row !important; + } + th.visible-xs, + td.visible-xs { + display: table-cell !important; + } +} +@media (max-width: 767px) { + .visible-xs-block { + display: block !important; + } +} +@media (max-width: 767px) { + .visible-xs-inline { + display: inline !important; + } +} +@media (max-width: 767px) { + .visible-xs-inline-block { + display: inline-block !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm { + display: block !important; + } + table.visible-sm { + display: table; + } + tr.visible-sm { + display: table-row !important; + } + th.visible-sm, + td.visible-sm { + display: table-cell !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm-block { + display: block !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm-inline { + display: inline !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm-inline-block { + display: inline-block !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md { + display: block !important; + } + table.visible-md { + display: table; + } + tr.visible-md { + display: table-row !important; + } + th.visible-md, + td.visible-md { + display: table-cell !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md-block { + display: block !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md-inline { + display: inline !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md-inline-block { + display: inline-block !important; + } +} +@media (min-width: 1200px) { + .visible-lg { + display: block !important; + } + table.visible-lg { + display: table; + } + tr.visible-lg { + display: table-row !important; + } + th.visible-lg, + td.visible-lg { + display: table-cell !important; + } +} +@media (min-width: 1200px) { + .visible-lg-block { + display: block !important; + } +} +@media (min-width: 1200px) { + .visible-lg-inline { + display: inline !important; + } +} +@media (min-width: 1200px) { + .visible-lg-inline-block { + display: inline-block !important; + } +} +@media (max-width: 767px) { + .hidden-xs { + display: none !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .hidden-sm { + display: none !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .hidden-md { + display: none !important; + } +} +@media (min-width: 1200px) { + .hidden-lg { + display: none !important; + } +} +.visible-print { + display: none !important; +} +@media print { + .visible-print { + display: block !important; + } + table.visible-print { + display: table; + } + tr.visible-print { + display: table-row !important; + } + th.visible-print, + td.visible-print { + display: table-cell !important; + } +} +.visible-print-block { + display: none !important; +} +@media print { + .visible-print-block { + display: block !important; + } +} +.visible-print-inline { + display: none !important; +} +@media print { + .visible-print-inline { + display: inline !important; + } +} +.visible-print-inline-block { + display: none !important; +} +@media print { + .visible-print-inline-block { + display: inline-block !important; + } +} +@media print { + .hidden-print { + display: none !important; + } +} +/*# sourceMappingURL=bootstrap.css.map */ diff --git a/presto-main/src/main/resources/webapp/vendor/bootstrap/css/bootstrap.css.map b/presto-main/src/main/resources/webapp/vendor/bootstrap/css/bootstrap.css.map new file mode 100644 index 00000000..2fd84f36 --- /dev/null +++ b/presto-main/src/main/resources/webapp/vendor/bootstrap/css/bootstrap.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["bootstrap.css","less/normalize.less","less/print.less","less/glyphicons.less","less/scaffolding.less","less/mixins/vendor-prefixes.less","less/mixins/tab-focus.less","less/mixins/image.less","less/type.less","less/mixins/text-emphasis.less","less/mixins/background-variant.less","less/mixins/text-overflow.less","less/code.less","less/grid.less","less/mixins/grid.less","less/mixins/grid-framework.less","less/tables.less","less/mixins/table-row.less","less/forms.less","less/mixins/forms.less","less/buttons.less","less/mixins/buttons.less","less/mixins/opacity.less","less/component-animations.less","less/dropdowns.less","less/mixins/nav-divider.less","less/mixins/reset-filter.less","less/button-groups.less","less/mixins/border-radius.less","less/input-groups.less","less/navs.less","less/navbar.less","less/mixins/nav-vertical-align.less","less/utilities.less","less/breadcrumbs.less","less/pagination.less","less/mixins/pagination.less","less/pager.less","less/labels.less","less/mixins/labels.less","less/badges.less","less/jumbotron.less","less/thumbnails.less","less/alerts.less","less/mixins/alerts.less","less/progress-bars.less","less/mixins/gradients.less","less/mixins/progress-bar.less","less/media.less","less/list-group.less","less/mixins/list-group.less","less/panels.less","less/mixins/panels.less","less/responsive-embed.less","less/wells.less","less/close.less","less/modals.less","less/tooltip.less","less/popovers.less","less/carousel.less","less/mixins/clearfix.less","less/mixins/center-block.less","less/mixins/hide-text.less","less/responsive-utilities.less","less/mixins/responsive-visibility.less"],"names":[],"mappings":"AAAA,6DAA4D;ACQ5D;EACE,yBAAA;EACA,4BAAA;EACA,gCAAA;EDND;ACaD;EACE,WAAA;EDXD;ACwBD;;;;;;;;;;;;;EAaE,gBAAA;EDtBD;AC8BD;;;;EAIE,uBAAA;EACA,0BAAA;ED5BD;ACoCD;EACE,eAAA;EACA,WAAA;EDlCD;AC0CD;;EAEE,eAAA;EDxCD;ACkDD;EACE,+BAAA;EDhDD;ACuDD;;EAEE,YAAA;EDrDD;AC+DD;EACE,2BAAA;ED7DD;ACoED;;EAEE,mBAAA;EDlED;ACyED;EACE,oBAAA;EDvED;AC+ED;EACE,gBAAA;EACA,kBAAA;ED7ED;ACoFD;EACE,kBAAA;EACA,aAAA;EDlFD;ACyFD;EACE,gBAAA;EDvFD;AC8FD;;EAEE,gBAAA;EACA,gBAAA;EACA,oBAAA;EACA,0BAAA;ED5FD;AC+FD;EACE,aAAA;ED7FD;ACgGD;EACE,iBAAA;ED9FD;ACwGD;EACE,WAAA;EDtGD;AC6GD;EACE,kBAAA;ED3GD;ACqHD;EACE,kBAAA;EDnHD;AC0HD;EACE,8BAAA;EACA,iCAAA;UAAA,yBAAA;EACA,WAAA;EDxHD;AC+HD;EACE,gBAAA;ED7HD;ACoID;;;;EAIE,mCAAA;EACA,gBAAA;EDlID;ACoJD;;;;;EAKE,gBAAA;EACA,eAAA;EACA,WAAA;EDlJD;ACyJD;EACE,mBAAA;EDvJD;ACiKD;;EAEE,sBAAA;ED/JD;AC0KD;;;;EAIE,4BAAA;EACA,iBAAA;EDxKD;AC+KD;;EAEE,iBAAA;ED7KD;ACoLD;;EAEE,WAAA;EACA,YAAA;EDlLD;AC0LD;EACE,qBAAA;EDxLD;ACmMD;;EAEE,gCAAA;KAAA,6BAAA;UAAA,wBAAA;EACA,YAAA;EDjMD;AC0MD;;EAEE,cAAA;EDxMD;ACiND;EACE,+BAAA;EACA,8BAAA;EACA,iCAAA;EACA,yBAAA;ED/MD;ACwND;;EAEE,0BAAA;EDtND;AC6ND;EACE,2BAAA;EACA,eAAA;EACA,gCAAA;ED3ND;ACmOD;EACE,WAAA;EACA,YAAA;EDjOD;ACwOD;EACE,gBAAA;EDtOD;AC8OD;EACE,mBAAA;ED5OD;ACsPD;EACE,2BAAA;EACA,mBAAA;EDpPD;ACuPD;;EAEE,YAAA;EDrPD;AACD,sFAAqF;AE1ErF;EAnGI;;;IAGI,oCAAA;IACA,wBAAA;IACA,qCAAA;YAAA,6BAAA;IACA,8BAAA;IFgLL;EE7KC;;IAEI,4BAAA;IF+KL;EE5KC;IACI,8BAAA;IF8KL;EE3KC;IACI,+BAAA;IF6KL;EExKC;;IAEI,aAAA;IF0KL;EEvKC;;IAEI,wBAAA;IACA,0BAAA;IFyKL;EEtKC;IACI,6BAAA;IFwKL;EErKC;;IAEI,0BAAA;IFuKL;EEpKC;IACI,4BAAA;IFsKL;EEnKC;;;IAGI,YAAA;IACA,WAAA;IFqKL;EElKC;;IAEI,yBAAA;IFoKL;EE7JC;IACI,6BAAA;IF+JL;EE3JC;IACI,eAAA;IF6JL;EE3JC;;IAGQ,mCAAA;IF4JT;EEzJC;IACI,wBAAA;IF2JL;EExJC;IACI,sCAAA;IF0JL;EE3JC;;IAKQ,mCAAA;IF0JT;EEvJC;;IAGQ,mCAAA;IFwJT;EACF;AGpPD;EACE,qCAAA;EACA,uDAAA;EACA,iYAAA;EHsPD;AG9OD;EACE,oBAAA;EACA,UAAA;EACA,uBAAA;EACA,qCAAA;EACA,oBAAA;EACA,qBAAA;EACA,gBAAA;EACA,qCAAA;EACA,oCAAA;EHgPD;AG5OmC;EAAW,gBAAA;EH+O9C;AG9OmC;EAAW,gBAAA;EHiP9C;AG/OmC;;EAAW,kBAAA;EHmP9C;AGlPmC;EAAW,kBAAA;EHqP9C;AGpPmC;EAAW,kBAAA;EHuP9C;AGtPmC;EAAW,kBAAA;EHyP9C;AGxPmC;EAAW,kBAAA;EH2P9C;AG1PmC;EAAW,kBAAA;EH6P9C;AG5PmC;EAAW,kBAAA;EH+P9C;AG9PmC;EAAW,kBAAA;EHiQ9C;AGhQmC;EAAW,kBAAA;EHmQ9C;AGlQmC;EAAW,kBAAA;EHqQ9C;AGpQmC;EAAW,kBAAA;EHuQ9C;AGtQmC;EAAW,kBAAA;EHyQ9C;AGxQmC;EAAW,kBAAA;EH2Q9C;AG1QmC;EAAW,kBAAA;EH6Q9C;AG5QmC;EAAW,kBAAA;EH+Q9C;AG9QmC;EAAW,kBAAA;EHiR9C;AGhRmC;EAAW,kBAAA;EHmR9C;AGlRmC;EAAW,kBAAA;EHqR9C;AGpRmC;EAAW,kBAAA;EHuR9C;AGtRmC;EAAW,kBAAA;EHyR9C;AGxRmC;EAAW,kBAAA;EH2R9C;AG1RmC;EAAW,kBAAA;EH6R9C;AG5RmC;EAAW,kBAAA;EH+R9C;AG9RmC;EAAW,kBAAA;EHiS9C;AGhSmC;EAAW,kBAAA;EHmS9C;AGlSmC;EAAW,kBAAA;EHqS9C;AGpSmC;EAAW,kBAAA;EHuS9C;AGtSmC;EAAW,kBAAA;EHyS9C;AGxSmC;EAAW,kBAAA;EH2S9C;AG1SmC;EAAW,kBAAA;EH6S9C;AG5SmC;EAAW,kBAAA;EH+S9C;AG9SmC;EAAW,kBAAA;EHiT9C;AGhTmC;EAAW,kBAAA;EHmT9C;AGlTmC;EAAW,kBAAA;EHqT9C;AGpTmC;EAAW,kBAAA;EHuT9C;AGtTmC;EAAW,kBAAA;EHyT9C;AGxTmC;EAAW,kBAAA;EH2T9C;AG1TmC;EAAW,kBAAA;EH6T9C;AG5TmC;EAAW,kBAAA;EH+T9C;AG9TmC;EAAW,kBAAA;EHiU9C;AGhUmC;EAAW,kBAAA;EHmU9C;AGlUmC;EAAW,kBAAA;EHqU9C;AGpUmC;EAAW,kBAAA;EHuU9C;AGtUmC;EAAW,kBAAA;EHyU9C;AGxUmC;EAAW,kBAAA;EH2U9C;AG1UmC;EAAW,kBAAA;EH6U9C;AG5UmC;EAAW,kBAAA;EH+U9C;AG9UmC;EAAW,kBAAA;EHiV9C;AGhVmC;EAAW,kBAAA;EHmV9C;AGlVmC;EAAW,kBAAA;EHqV9C;AGpVmC;EAAW,kBAAA;EHuV9C;AGtVmC;EAAW,kBAAA;EHyV9C;AGxVmC;EAAW,kBAAA;EH2V9C;AG1VmC;EAAW,kBAAA;EH6V9C;AG5VmC;EAAW,kBAAA;EH+V9C;AG9VmC;EAAW,kBAAA;EHiW9C;AGhWmC;EAAW,kBAAA;EHmW9C;AGlWmC;EAAW,kBAAA;EHqW9C;AGpWmC;EAAW,kBAAA;EHuW9C;AGtWmC;EAAW,kBAAA;EHyW9C;AGxWmC;EAAW,kBAAA;EH2W9C;AG1WmC;EAAW,kBAAA;EH6W9C;AG5WmC;EAAW,kBAAA;EH+W9C;AG9WmC;EAAW,kBAAA;EHiX9C;AGhXmC;EAAW,kBAAA;EHmX9C;AGlXmC;EAAW,kBAAA;EHqX9C;AGpXmC;EAAW,kBAAA;EHuX9C;AGtXmC;EAAW,kBAAA;EHyX9C;AGxXmC;EAAW,kBAAA;EH2X9C;AG1XmC;EAAW,kBAAA;EH6X9C;AG5XmC;EAAW,kBAAA;EH+X9C;AG9XmC;EAAW,kBAAA;EHiY9C;AGhYmC;EAAW,kBAAA;EHmY9C;AGlYmC;EAAW,kBAAA;EHqY9C;AGpYmC;EAAW,kBAAA;EHuY9C;AGtYmC;EAAW,kBAAA;EHyY9C;AGxYmC;EAAW,kBAAA;EH2Y9C;AG1YmC;EAAW,kBAAA;EH6Y9C;AG5YmC;EAAW,kBAAA;EH+Y9C;AG9YmC;EAAW,kBAAA;EHiZ9C;AGhZmC;EAAW,kBAAA;EHmZ9C;AGlZmC;EAAW,kBAAA;EHqZ9C;AGpZmC;EAAW,kBAAA;EHuZ9C;AGtZmC;EAAW,kBAAA;EHyZ9C;AGxZmC;EAAW,kBAAA;EH2Z9C;AG1ZmC;EAAW,kBAAA;EH6Z9C;AG5ZmC;EAAW,kBAAA;EH+Z9C;AG9ZmC;EAAW,kBAAA;EHia9C;AGhamC;EAAW,kBAAA;EHma9C;AGlamC;EAAW,kBAAA;EHqa9C;AGpamC;EAAW,kBAAA;EHua9C;AGtamC;EAAW,kBAAA;EHya9C;AGxamC;EAAW,kBAAA;EH2a9C;AG1amC;EAAW,kBAAA;EH6a9C;AG5amC;EAAW,kBAAA;EH+a9C;AG9amC;EAAW,kBAAA;EHib9C;AGhbmC;EAAW,kBAAA;EHmb9C;AGlbmC;EAAW,kBAAA;EHqb9C;AGpbmC;EAAW,kBAAA;EHub9C;AGtbmC;EAAW,kBAAA;EHyb9C;AGxbmC;EAAW,kBAAA;EH2b9C;AG1bmC;EAAW,kBAAA;EH6b9C;AG5bmC;EAAW,kBAAA;EH+b9C;AG9bmC;EAAW,kBAAA;EHic9C;AGhcmC;EAAW,kBAAA;EHmc9C;AGlcmC;EAAW,kBAAA;EHqc9C;AGpcmC;EAAW,kBAAA;EHuc9C;AGtcmC;EAAW,kBAAA;EHyc9C;AGxcmC;EAAW,kBAAA;EH2c9C;AG1cmC;EAAW,kBAAA;EH6c9C;AG5cmC;EAAW,kBAAA;EH+c9C;AG9cmC;EAAW,kBAAA;EHid9C;AGhdmC;EAAW,kBAAA;EHmd9C;AGldmC;EAAW,kBAAA;EHqd9C;AGpdmC;EAAW,kBAAA;EHud9C;AGtdmC;EAAW,kBAAA;EHyd9C;AGxdmC;EAAW,kBAAA;EH2d9C;AG1dmC;EAAW,kBAAA;EH6d9C;AG5dmC;EAAW,kBAAA;EH+d9C;AG9dmC;EAAW,kBAAA;EHie9C;AGhemC;EAAW,kBAAA;EHme9C;AGlemC;EAAW,kBAAA;EHqe9C;AGpemC;EAAW,kBAAA;EHue9C;AGtemC;EAAW,kBAAA;EHye9C;AGxemC;EAAW,kBAAA;EH2e9C;AG1emC;EAAW,kBAAA;EH6e9C;AG5emC;EAAW,kBAAA;EH+e9C;AG9emC;EAAW,kBAAA;EHif9C;AGhfmC;EAAW,kBAAA;EHmf9C;AGlfmC;EAAW,kBAAA;EHqf9C;AGpfmC;EAAW,kBAAA;EHuf9C;AGtfmC;EAAW,kBAAA;EHyf9C;AGxfmC;EAAW,kBAAA;EH2f9C;AG1fmC;EAAW,kBAAA;EH6f9C;AG5fmC;EAAW,kBAAA;EH+f9C;AG9fmC;EAAW,kBAAA;EHigB9C;AGhgBmC;EAAW,kBAAA;EHmgB9C;AGlgBmC;EAAW,kBAAA;EHqgB9C;AGpgBmC;EAAW,kBAAA;EHugB9C;AGtgBmC;EAAW,kBAAA;EHygB9C;AGxgBmC;EAAW,kBAAA;EH2gB9C;AG1gBmC;EAAW,kBAAA;EH6gB9C;AG5gBmC;EAAW,kBAAA;EH+gB9C;AG9gBmC;EAAW,kBAAA;EHihB9C;AGhhBmC;EAAW,kBAAA;EHmhB9C;AGlhBmC;EAAW,kBAAA;EHqhB9C;AGphBmC;EAAW,kBAAA;EHuhB9C;AGthBmC;EAAW,kBAAA;EHyhB9C;AGxhBmC;EAAW,kBAAA;EH2hB9C;AG1hBmC;EAAW,kBAAA;EH6hB9C;AG5hBmC;EAAW,kBAAA;EH+hB9C;AG9hBmC;EAAW,kBAAA;EHiiB9C;AGhiBmC;EAAW,kBAAA;EHmiB9C;AGliBmC;EAAW,kBAAA;EHqiB9C;AGpiBmC;EAAW,kBAAA;EHuiB9C;AGtiBmC;EAAW,kBAAA;EHyiB9C;AGxiBmC;EAAW,kBAAA;EH2iB9C;AG1iBmC;EAAW,kBAAA;EH6iB9C;AG5iBmC;EAAW,kBAAA;EH+iB9C;AG9iBmC;EAAW,kBAAA;EHijB9C;AGhjBmC;EAAW,kBAAA;EHmjB9C;AGljBmC;EAAW,kBAAA;EHqjB9C;AGpjBmC;EAAW,kBAAA;EHujB9C;AGtjBmC;EAAW,kBAAA;EHyjB9C;AGxjBmC;EAAW,kBAAA;EH2jB9C;AG1jBmC;EAAW,kBAAA;EH6jB9C;AG5jBmC;EAAW,kBAAA;EH+jB9C;AG9jBmC;EAAW,kBAAA;EHikB9C;AGhkBmC;EAAW,kBAAA;EHmkB9C;AGlkBmC;EAAW,kBAAA;EHqkB9C;AGpkBmC;EAAW,kBAAA;EHukB9C;AGtkBmC;EAAW,kBAAA;EHykB9C;AGxkBmC;EAAW,kBAAA;EH2kB9C;AG1kBmC;EAAW,kBAAA;EH6kB9C;AG5kBmC;EAAW,kBAAA;EH+kB9C;AG9kBmC;EAAW,kBAAA;EHilB9C;AGhlBmC;EAAW,kBAAA;EHmlB9C;AGllBmC;EAAW,kBAAA;EHqlB9C;AGplBmC;EAAW,kBAAA;EHulB9C;AGtlBmC;EAAW,kBAAA;EHylB9C;AGxlBmC;EAAW,kBAAA;EH2lB9C;AG1lBmC;EAAW,kBAAA;EH6lB9C;AG5lBmC;EAAW,kBAAA;EH+lB9C;AG9lBmC;EAAW,kBAAA;EHimB9C;AGhmBmC;EAAW,kBAAA;EHmmB9C;AGlmBmC;EAAW,kBAAA;EHqmB9C;AGpmBmC;EAAW,kBAAA;EHumB9C;AGtmBmC;EAAW,kBAAA;EHymB9C;AGxmBmC;EAAW,kBAAA;EH2mB9C;AG1mBmC;EAAW,kBAAA;EH6mB9C;AG5mBmC;EAAW,kBAAA;EH+mB9C;AG9mBmC;EAAW,kBAAA;EHinB9C;AGhnBmC;EAAW,kBAAA;EHmnB9C;AGlnBmC;EAAW,kBAAA;EHqnB9C;AGpnBmC;EAAW,kBAAA;EHunB9C;AGtnBmC;EAAW,kBAAA;EHynB9C;AGxnBmC;EAAW,kBAAA;EH2nB9C;AG1nBmC;EAAW,kBAAA;EH6nB9C;AG5nBmC;EAAW,kBAAA;EH+nB9C;AG9nBmC;EAAW,kBAAA;EHioB9C;AGhoBmC;EAAW,kBAAA;EHmoB9C;AGloBmC;EAAW,kBAAA;EHqoB9C;AGpoBmC;EAAW,kBAAA;EHuoB9C;AGtoBmC;EAAW,kBAAA;EHyoB9C;AGhoBmC;EAAW,kBAAA;EHmoB9C;AGloBmC;EAAW,kBAAA;EHqoB9C;AGpoBmC;EAAW,kBAAA;EHuoB9C;AGtoBmC;EAAW,kBAAA;EHyoB9C;AGxoBmC;EAAW,kBAAA;EH2oB9C;AG1oBmC;EAAW,kBAAA;EH6oB9C;AG5oBmC;EAAW,kBAAA;EH+oB9C;AG9oBmC;EAAW,kBAAA;EHipB9C;AGhpBmC;EAAW,kBAAA;EHmpB9C;AGlpBmC;EAAW,kBAAA;EHqpB9C;AGppBmC;EAAW,kBAAA;EHupB9C;AGtpBmC;EAAW,kBAAA;EHypB9C;AGxpBmC;EAAW,kBAAA;EH2pB9C;AG1pBmC;EAAW,kBAAA;EH6pB9C;AG5pBmC;EAAW,kBAAA;EH+pB9C;AG9pBmC;EAAW,kBAAA;EHiqB9C;AGhqBmC;EAAW,kBAAA;EHmqB9C;AGlqBmC;EAAW,kBAAA;EHqqB9C;AGpqBmC;EAAW,kBAAA;EHuqB9C;AGtqBmC;EAAW,kBAAA;EHyqB9C;AGxqBmC;EAAW,kBAAA;EH2qB9C;AG1qBmC;EAAW,kBAAA;EH6qB9C;AG5qBmC;EAAW,kBAAA;EH+qB9C;AG9qBmC;EAAW,kBAAA;EHirB9C;AGhrBmC;EAAW,kBAAA;EHmrB9C;AGlrBmC;EAAW,kBAAA;EHqrB9C;AGprBmC;EAAW,kBAAA;EHurB9C;AGtrBmC;EAAW,kBAAA;EHyrB9C;AGxrBmC;EAAW,kBAAA;EH2rB9C;AG1rBmC;EAAW,kBAAA;EH6rB9C;AG5rBmC;EAAW,kBAAA;EH+rB9C;AG9rBmC;EAAW,kBAAA;EHisB9C;AGhsBmC;EAAW,kBAAA;EHmsB9C;AGlsBmC;EAAW,kBAAA;EHqsB9C;AGpsBmC;EAAW,kBAAA;EHusB9C;AGtsBmC;EAAW,kBAAA;EHysB9C;AGxsBmC;EAAW,kBAAA;EH2sB9C;AG1sBmC;EAAW,kBAAA;EH6sB9C;AG5sBmC;EAAW,kBAAA;EH+sB9C;AG9sBmC;EAAW,kBAAA;EHitB9C;AGhtBmC;EAAW,kBAAA;EHmtB9C;AGltBmC;EAAW,kBAAA;EHqtB9C;AGptBmC;EAAW,kBAAA;EHutB9C;AGttBmC;EAAW,kBAAA;EHytB9C;AGxtBmC;EAAW,kBAAA;EH2tB9C;AG1tBmC;EAAW,kBAAA;EH6tB9C;AG5tBmC;EAAW,kBAAA;EH+tB9C;AG9tBmC;EAAW,kBAAA;EHiuB9C;AGhuBmC;EAAW,kBAAA;EHmuB9C;AGluBmC;EAAW,kBAAA;EHquB9C;AGpuBmC;EAAW,kBAAA;EHuuB9C;AGtuBmC;EAAW,kBAAA;EHyuB9C;AGxuBmC;EAAW,kBAAA;EH2uB9C;AG1uBmC;EAAW,kBAAA;EH6uB9C;AG5uBmC;EAAW,kBAAA;EH+uB9C;AG9uBmC;EAAW,kBAAA;EHivB9C;AIvhCD;ECgEE,gCAAA;EACG,6BAAA;EACK,wBAAA;EL09BT;AIzhCD;;EC6DE,gCAAA;EACG,6BAAA;EACK,wBAAA;ELg+BT;AIvhCD;EACE,iBAAA;EACA,+CAAA;EJyhCD;AIthCD;EACE,6DAAA;EACA,iBAAA;EACA,yBAAA;EACA,gBAAA;EACA,2BAAA;EJwhCD;AIphCD;;;;EAIE,sBAAA;EACA,oBAAA;EACA,sBAAA;EJshCD;AIhhCD;EACE,gBAAA;EACA,uBAAA;EJkhCD;AIhhCC;;EAEE,gBAAA;EACA,4BAAA;EJkhCH;AI/gCC;EErDA,sBAAA;EAEA,4CAAA;EACA,sBAAA;ENskCD;AIzgCD;EACE,WAAA;EJ2gCD;AIrgCD;EACE,wBAAA;EJugCD;AIngCD;;;;;EGvEE,gBAAA;EACA,iBAAA;EACA,cAAA;EPilCD;AIvgCD;EACE,oBAAA;EJygCD;AIngCD;EACE,cAAA;EACA,yBAAA;EACA,2BAAA;EACA,2BAAA;EACA,oBAAA;EC6FA,0CAAA;EACK,qCAAA;EACG,kCAAA;EEvLR,uBAAA;EACA,iBAAA;EACA,cAAA;EPimCD;AIngCD;EACE,oBAAA;EJqgCD;AI//BD;EACE,kBAAA;EACA,qBAAA;EACA,WAAA;EACA,+BAAA;EJigCD;AIz/BD;EACE,oBAAA;EACA,YAAA;EACA,aAAA;EACA,cAAA;EACA,YAAA;EACA,kBAAA;EACA,wBAAA;EACA,WAAA;EJ2/BD;AIn/BC;;EAEE,kBAAA;EACA,aAAA;EACA,cAAA;EACA,WAAA;EACA,mBAAA;EACA,YAAA;EJq/BH;AIz+BD;EACE,iBAAA;EJ2+BD;AQnoCD;;;;;;;;;;;;EAEE,sBAAA;EACA,kBAAA;EACA,kBAAA;EACA,gBAAA;ER+oCD;AQppCD;;;;;;;;;;;;;;;;;;;;;;;;EASI,qBAAA;EACA,gBAAA;EACA,gBAAA;ERqqCH;AQjqCD;;;;;;EAGE,kBAAA;EACA,qBAAA;ERsqCD;AQ1qCD;;;;;;;;;;;;EAQI,gBAAA;ERgrCH;AQ7qCD;;;;;;EAGE,kBAAA;EACA,qBAAA;ERkrCD;AQtrCD;;;;;;;;;;;;EAQI,gBAAA;ER4rCH;AQxrCD;;EAAU,iBAAA;ER4rCT;AQ3rCD;;EAAU,iBAAA;ER+rCT;AQ9rCD;;EAAU,iBAAA;ERksCT;AQjsCD;;EAAU,iBAAA;ERqsCT;AQpsCD;;EAAU,iBAAA;ERwsCT;AQvsCD;;EAAU,iBAAA;ER2sCT;AQrsCD;EACE,kBAAA;ERusCD;AQpsCD;EACE,qBAAA;EACA,iBAAA;EACA,kBAAA;EACA,kBAAA;ERssCD;AQjsCD;EAAA;IAFI,iBAAA;IRusCD;EACF;AQ/rCD;;EAEE,gBAAA;ERisCD;AQ9rCD;;EAEE,2BAAA;EACA,eAAA;ERgsCD;AQ5rCD;EAAuB,kBAAA;ER+rCtB;AQ9rCD;EAAuB,mBAAA;ERisCtB;AQhsCD;EAAuB,oBAAA;ERmsCtB;AQlsCD;EAAuB,qBAAA;ERqsCtB;AQpsCD;EAAuB,qBAAA;ERusCtB;AQpsCD;EAAuB,2BAAA;ERusCtB;AQtsCD;EAAuB,2BAAA;ERysCtB;AQxsCD;EAAuB,4BAAA;ER2sCtB;AQxsCD;EACE,gBAAA;ER0sCD;AQxsCD;ECrGE,gBAAA;ETgzCD;AS/yCC;EACE,gBAAA;ETizCH;AQ3sCD;ECxGE,gBAAA;ETszCD;ASrzCC;EACE,gBAAA;ETuzCH;AQ9sCD;EC3GE,gBAAA;ET4zCD;AS3zCC;EACE,gBAAA;ET6zCH;AQjtCD;EC9GE,gBAAA;ETk0CD;ASj0CC;EACE,gBAAA;ETm0CH;AQptCD;ECjHE,gBAAA;ETw0CD;ASv0CC;EACE,gBAAA;ETy0CH;AQntCD;EAGE,aAAA;EE3HA,2BAAA;EV+0CD;AU90CC;EACE,2BAAA;EVg1CH;AQptCD;EE9HE,2BAAA;EVq1CD;AUp1CC;EACE,2BAAA;EVs1CH;AQvtCD;EEjIE,2BAAA;EV21CD;AU11CC;EACE,2BAAA;EV41CH;AQ1tCD;EEpIE,2BAAA;EVi2CD;AUh2CC;EACE,2BAAA;EVk2CH;AQ7tCD;EEvIE,2BAAA;EVu2CD;AUt2CC;EACE,2BAAA;EVw2CH;AQ3tCD;EACE,qBAAA;EACA,qBAAA;EACA,kCAAA;ER6tCD;AQrtCD;;EAEE,eAAA;EACA,qBAAA;ERutCD;AQ1tCD;;;;EAMI,kBAAA;ER0tCH;AQntCD;EACE,iBAAA;EACA,kBAAA;ERqtCD;AQjtCD;EALE,iBAAA;EACA,kBAAA;EAMA,mBAAA;ERotCD;AQttCD;EAKI,uBAAA;EACA,mBAAA;EACA,oBAAA;ERotCH;AQ/sCD;EACE,eAAA;EACA,qBAAA;ERitCD;AQ/sCD;;EAEE,yBAAA;ERitCD;AQ/sCD;EACE,mBAAA;ERitCD;AQ/sCD;EACE,gBAAA;ERitCD;AQxrCD;EAAA;IAVM,aAAA;IACA,cAAA;IACA,aAAA;IACA,mBAAA;IGtNJ,kBAAA;IACA,yBAAA;IACA,qBAAA;IX65CC;EQlsCH;IAHM,oBAAA;IRwsCH;EACF;AQ/rCD;;EAGE,cAAA;EACA,mCAAA;ERgsCD;AQ9rCD;EACE,gBAAA;EA9IqB,2BAAA;ER+0CtB;AQ5rCD;EACE,oBAAA;EACA,kBAAA;EACA,mBAAA;EACA,gCAAA;ER8rCD;AQzrCG;;;EACE,kBAAA;ER6rCL;AQvsCD;;;EAmBI,gBAAA;EACA,gBAAA;EACA,yBAAA;EACA,gBAAA;ERyrCH;AQvrCG;;;EACE,wBAAA;ER2rCL;AQnrCD;;EAEE,qBAAA;EACA,iBAAA;EACA,iCAAA;EACA,gBAAA;EACA,mBAAA;ERqrCD;AQ/qCG;;;;;;EAAW,aAAA;ERurCd;AQtrCG;;;;;;EACE,wBAAA;ER6rCL;AQvrCD;EACE,qBAAA;EACA,oBAAA;EACA,yBAAA;ERyrCD;AY/9CD;;;;EAIE,gEAAA;EZi+CD;AY79CD;EACE,kBAAA;EACA,gBAAA;EACA,gBAAA;EACA,2BAAA;EACA,oBAAA;EZ+9CD;AY39CD;EACE,kBAAA;EACA,gBAAA;EACA,gBAAA;EACA,2BAAA;EACA,oBAAA;EACA,wDAAA;UAAA,gDAAA;EZ69CD;AYn+CD;EASI,YAAA;EACA,iBAAA;EACA,mBAAA;EACA,0BAAA;UAAA,kBAAA;EZ69CH;AYx9CD;EACE,gBAAA;EACA,gBAAA;EACA,kBAAA;EACA,iBAAA;EACA,yBAAA;EACA,uBAAA;EACA,uBAAA;EACA,gBAAA;EACA,2BAAA;EACA,2BAAA;EACA,oBAAA;EZ09CD;AYr+CD;EAeI,YAAA;EACA,oBAAA;EACA,gBAAA;EACA,uBAAA;EACA,+BAAA;EACA,kBAAA;EZy9CH;AYp9CD;EACE,mBAAA;EACA,oBAAA;EZs9CD;AahhDD;ECHE,oBAAA;EACA,mBAAA;EACA,oBAAA;EACA,qBAAA;EdshDD;AahhDC;EAAA;IAFE,cAAA;IbshDD;EACF;AalhDC;EAAA;IAFE,cAAA;IbwhDD;EACF;AaphDD;EAAA;IAFI,eAAA;Ib0hDD;EACF;AajhDD;ECvBE,oBAAA;EACA,mBAAA;EACA,oBAAA;EACA,qBAAA;Ed2iDD;Aa9gDD;ECvBE,oBAAA;EACA,qBAAA;EdwiDD;AexiDG;EACE,oBAAA;EAEA,iBAAA;EAEA,oBAAA;EACA,qBAAA;EfwiDL;AexhDG;EACE,aAAA;Ef0hDL;AenhDC;EACE,aAAA;EfqhDH;AethDC;EACE,qBAAA;EfwhDH;AezhDC;EACE,qBAAA;Ef2hDH;Ae5hDC;EACE,YAAA;Ef8hDH;Ae/hDC;EACE,qBAAA;EfiiDH;AeliDC;EACE,qBAAA;EfoiDH;AeriDC;EACE,YAAA;EfuiDH;AexiDC;EACE,qBAAA;Ef0iDH;Ae3iDC;EACE,qBAAA;Ef6iDH;Ae9iDC;EACE,YAAA;EfgjDH;AejjDC;EACE,qBAAA;EfmjDH;AepjDC;EACE,oBAAA;EfsjDH;AexiDC;EACE,aAAA;Ef0iDH;Ae3iDC;EACE,qBAAA;Ef6iDH;Ae9iDC;EACE,qBAAA;EfgjDH;AejjDC;EACE,YAAA;EfmjDH;AepjDC;EACE,qBAAA;EfsjDH;AevjDC;EACE,qBAAA;EfyjDH;Ae1jDC;EACE,YAAA;Ef4jDH;Ae7jDC;EACE,qBAAA;Ef+jDH;AehkDC;EACE,qBAAA;EfkkDH;AenkDC;EACE,YAAA;EfqkDH;AetkDC;EACE,qBAAA;EfwkDH;AezkDC;EACE,oBAAA;Ef2kDH;AevkDC;EACE,aAAA;EfykDH;AezlDC;EACE,YAAA;Ef2lDH;Ae5lDC;EACE,oBAAA;Ef8lDH;Ae/lDC;EACE,oBAAA;EfimDH;AelmDC;EACE,WAAA;EfomDH;AermDC;EACE,oBAAA;EfumDH;AexmDC;EACE,oBAAA;Ef0mDH;Ae3mDC;EACE,WAAA;Ef6mDH;Ae9mDC;EACE,oBAAA;EfgnDH;AejnDC;EACE,oBAAA;EfmnDH;AepnDC;EACE,WAAA;EfsnDH;AevnDC;EACE,oBAAA;EfynDH;Ae1nDC;EACE,mBAAA;Ef4nDH;AexnDC;EACE,YAAA;Ef0nDH;Ae5mDC;EACE,mBAAA;Ef8mDH;Ae/mDC;EACE,2BAAA;EfinDH;AelnDC;EACE,2BAAA;EfonDH;AernDC;EACE,kBAAA;EfunDH;AexnDC;EACE,2BAAA;Ef0nDH;Ae3nDC;EACE,2BAAA;Ef6nDH;Ae9nDC;EACE,kBAAA;EfgoDH;AejoDC;EACE,2BAAA;EfmoDH;AepoDC;EACE,2BAAA;EfsoDH;AevoDC;EACE,kBAAA;EfyoDH;Ae1oDC;EACE,2BAAA;Ef4oDH;Ae7oDC;EACE,0BAAA;Ef+oDH;AehpDC;EACE,iBAAA;EfkpDH;AalpDD;EElCI;IACE,aAAA;IfurDH;EehrDD;IACE,aAAA;IfkrDD;EenrDD;IACE,qBAAA;IfqrDD;EetrDD;IACE,qBAAA;IfwrDD;EezrDD;IACE,YAAA;If2rDD;Ee5rDD;IACE,qBAAA;If8rDD;Ee/rDD;IACE,qBAAA;IfisDD;EelsDD;IACE,YAAA;IfosDD;EersDD;IACE,qBAAA;IfusDD;EexsDD;IACE,qBAAA;If0sDD;Ee3sDD;IACE,YAAA;If6sDD;Ee9sDD;IACE,qBAAA;IfgtDD;EejtDD;IACE,oBAAA;IfmtDD;EersDD;IACE,aAAA;IfusDD;EexsDD;IACE,qBAAA;If0sDD;Ee3sDD;IACE,qBAAA;If6sDD;Ee9sDD;IACE,YAAA;IfgtDD;EejtDD;IACE,qBAAA;IfmtDD;EeptDD;IACE,qBAAA;IfstDD;EevtDD;IACE,YAAA;IfytDD;Ee1tDD;IACE,qBAAA;If4tDD;Ee7tDD;IACE,qBAAA;If+tDD;EehuDD;IACE,YAAA;IfkuDD;EenuDD;IACE,qBAAA;IfquDD;EetuDD;IACE,oBAAA;IfwuDD;EepuDD;IACE,aAAA;IfsuDD;EetvDD;IACE,YAAA;IfwvDD;EezvDD;IACE,oBAAA;If2vDD;Ee5vDD;IACE,oBAAA;If8vDD;Ee/vDD;IACE,WAAA;IfiwDD;EelwDD;IACE,oBAAA;IfowDD;EerwDD;IACE,oBAAA;IfuwDD;EexwDD;IACE,WAAA;If0wDD;Ee3wDD;IACE,oBAAA;If6wDD;Ee9wDD;IACE,oBAAA;IfgxDD;EejxDD;IACE,WAAA;IfmxDD;EepxDD;IACE,oBAAA;IfsxDD;EevxDD;IACE,mBAAA;IfyxDD;EerxDD;IACE,YAAA;IfuxDD;EezwDD;IACE,mBAAA;If2wDD;Ee5wDD;IACE,2BAAA;If8wDD;Ee/wDD;IACE,2BAAA;IfixDD;EelxDD;IACE,kBAAA;IfoxDD;EerxDD;IACE,2BAAA;IfuxDD;EexxDD;IACE,2BAAA;If0xDD;Ee3xDD;IACE,kBAAA;If6xDD;Ee9xDD;IACE,2BAAA;IfgyDD;EejyDD;IACE,2BAAA;IfmyDD;EepyDD;IACE,kBAAA;IfsyDD;EevyDD;IACE,2BAAA;IfyyDD;Ee1yDD;IACE,0BAAA;If4yDD;Ee7yDD;IACE,iBAAA;If+yDD;EACF;AavyDD;EE3CI;IACE,aAAA;Ifq1DH;Ee90DD;IACE,aAAA;Ifg1DD;Eej1DD;IACE,qBAAA;Ifm1DD;Eep1DD;IACE,qBAAA;Ifs1DD;Eev1DD;IACE,YAAA;Ify1DD;Ee11DD;IACE,qBAAA;If41DD;Ee71DD;IACE,qBAAA;If+1DD;Eeh2DD;IACE,YAAA;Ifk2DD;Een2DD;IACE,qBAAA;Ifq2DD;Eet2DD;IACE,qBAAA;Ifw2DD;Eez2DD;IACE,YAAA;If22DD;Ee52DD;IACE,qBAAA;If82DD;Ee/2DD;IACE,oBAAA;Ifi3DD;Een2DD;IACE,aAAA;Ifq2DD;Eet2DD;IACE,qBAAA;Ifw2DD;Eez2DD;IACE,qBAAA;If22DD;Ee52DD;IACE,YAAA;If82DD;Ee/2DD;IACE,qBAAA;Ifi3DD;Eel3DD;IACE,qBAAA;Ifo3DD;Eer3DD;IACE,YAAA;Ifu3DD;Eex3DD;IACE,qBAAA;If03DD;Ee33DD;IACE,qBAAA;If63DD;Ee93DD;IACE,YAAA;Ifg4DD;Eej4DD;IACE,qBAAA;Ifm4DD;Eep4DD;IACE,oBAAA;Ifs4DD;Eel4DD;IACE,aAAA;Ifo4DD;Eep5DD;IACE,YAAA;Ifs5DD;Eev5DD;IACE,oBAAA;Ify5DD;Ee15DD;IACE,oBAAA;If45DD;Ee75DD;IACE,WAAA;If+5DD;Eeh6DD;IACE,oBAAA;Ifk6DD;Een6DD;IACE,oBAAA;Ifq6DD;Eet6DD;IACE,WAAA;Ifw6DD;Eez6DD;IACE,oBAAA;If26DD;Ee56DD;IACE,oBAAA;If86DD;Ee/6DD;IACE,WAAA;Ifi7DD;Eel7DD;IACE,oBAAA;Ifo7DD;Eer7DD;IACE,mBAAA;Ifu7DD;Een7DD;IACE,YAAA;Ifq7DD;Eev6DD;IACE,mBAAA;Ify6DD;Ee16DD;IACE,2BAAA;If46DD;Ee76DD;IACE,2BAAA;If+6DD;Eeh7DD;IACE,kBAAA;Ifk7DD;Een7DD;IACE,2BAAA;Ifq7DD;Eet7DD;IACE,2BAAA;Ifw7DD;Eez7DD;IACE,kBAAA;If27DD;Ee57DD;IACE,2BAAA;If87DD;Ee/7DD;IACE,2BAAA;Ifi8DD;Eel8DD;IACE,kBAAA;Ifo8DD;Eer8DD;IACE,2BAAA;Ifu8DD;Eex8DD;IACE,0BAAA;If08DD;Ee38DD;IACE,iBAAA;If68DD;EACF;Aal8DD;EE9CI;IACE,aAAA;Ifm/DH;Ee5+DD;IACE,aAAA;If8+DD;Ee/+DD;IACE,qBAAA;Ifi/DD;Eel/DD;IACE,qBAAA;Ifo/DD;Eer/DD;IACE,YAAA;Ifu/DD;Eex/DD;IACE,qBAAA;If0/DD;Ee3/DD;IACE,qBAAA;If6/DD;Ee9/DD;IACE,YAAA;IfggED;EejgED;IACE,qBAAA;IfmgED;EepgED;IACE,qBAAA;IfsgED;EevgED;IACE,YAAA;IfygED;Ee1gED;IACE,qBAAA;If4gED;Ee7gED;IACE,oBAAA;If+gED;EejgED;IACE,aAAA;IfmgED;EepgED;IACE,qBAAA;IfsgED;EevgED;IACE,qBAAA;IfygED;Ee1gED;IACE,YAAA;If4gED;Ee7gED;IACE,qBAAA;If+gED;EehhED;IACE,qBAAA;IfkhED;EenhED;IACE,YAAA;IfqhED;EethED;IACE,qBAAA;IfwhED;EezhED;IACE,qBAAA;If2hED;Ee5hED;IACE,YAAA;If8hED;Ee/hED;IACE,qBAAA;IfiiED;EeliED;IACE,oBAAA;IfoiED;EehiED;IACE,aAAA;IfkiED;EeljED;IACE,YAAA;IfojED;EerjED;IACE,oBAAA;IfujED;EexjED;IACE,oBAAA;If0jED;Ee3jED;IACE,WAAA;If6jED;Ee9jED;IACE,oBAAA;IfgkED;EejkED;IACE,oBAAA;IfmkED;EepkED;IACE,WAAA;IfskED;EevkED;IACE,oBAAA;IfykED;Ee1kED;IACE,oBAAA;If4kED;Ee7kED;IACE,WAAA;If+kED;EehlED;IACE,oBAAA;IfklED;EenlED;IACE,mBAAA;IfqlED;EejlED;IACE,YAAA;IfmlED;EerkED;IACE,mBAAA;IfukED;EexkED;IACE,2BAAA;If0kED;Ee3kED;IACE,2BAAA;If6kED;Ee9kED;IACE,kBAAA;IfglED;EejlED;IACE,2BAAA;IfmlED;EeplED;IACE,2BAAA;IfslED;EevlED;IACE,kBAAA;IfylED;Ee1lED;IACE,2BAAA;If4lED;Ee7lED;IACE,2BAAA;If+lED;EehmED;IACE,kBAAA;IfkmED;EenmED;IACE,2BAAA;IfqmED;EetmED;IACE,0BAAA;IfwmED;EezmED;IACE,iBAAA;If2mED;EACF;AgB/qED;EACE,+BAAA;EhBirED;AgB/qED;EACE,kBAAA;EACA,qBAAA;EACA,gBAAA;EACA,kBAAA;EhBirED;AgB/qED;EACE,kBAAA;EhBirED;AgB3qED;EACE,aAAA;EACA,iBAAA;EACA,qBAAA;EhB6qED;AgBhrED;;;;;;EAWQ,cAAA;EACA,yBAAA;EACA,qBAAA;EACA,+BAAA;EhB6qEP;AgB3rED;EAoBI,wBAAA;EACA,kCAAA;EhB0qEH;AgB/rED;;;;;;EA8BQ,eAAA;EhByqEP;AgBvsED;EAoCI,+BAAA;EhBsqEH;AgB1sED;EAyCI,2BAAA;EhBoqEH;AgB7pED;;;;;;EAOQ,cAAA;EhB8pEP;AgBnpED;EACE,2BAAA;EhBqpED;AgBtpED;;;;;;EAQQ,2BAAA;EhBspEP;AgB9pED;;EAeM,0BAAA;EhBmpEL;AgBzoED;EAEI,2BAAA;EhB0oEH;AgBjoED;EAEI,2BAAA;EhBkoEH;AgBznED;EACE,kBAAA;EACA,aAAA;EACA,uBAAA;EhB2nED;AgBtnEG;;EACE,kBAAA;EACA,aAAA;EACA,qBAAA;EhBynEL;AiBrwEC;;;;;;;;;;;;EAOI,2BAAA;EjB4wEL;AiBtwEC;;;;;EAMI,2BAAA;EjBuwEL;AiB1xEC;;;;;;;;;;;;EAOI,2BAAA;EjBiyEL;AiB3xEC;;;;;EAMI,2BAAA;EjB4xEL;AiB/yEC;;;;;;;;;;;;EAOI,2BAAA;EjBszEL;AiBhzEC;;;;;EAMI,2BAAA;EjBizEL;AiBp0EC;;;;;;;;;;;;EAOI,2BAAA;EjB20EL;AiBr0EC;;;;;EAMI,2BAAA;EjBs0EL;AiBz1EC;;;;;;;;;;;;EAOI,2BAAA;EjBg2EL;AiB11EC;;;;;EAMI,2BAAA;EjB21EL;AgBzsED;EACE,kBAAA;EACA,mBAAA;EhB2sED;AgB9oED;EAAA;IA1DI,aAAA;IACA,qBAAA;IACA,oBAAA;IACA,8CAAA;IACA,2BAAA;IhB4sED;EgBtpEH;IAlDM,kBAAA;IhB2sEH;EgBzpEH;;;;;;IAzCY,qBAAA;IhB0sET;EgBjqEH;IAjCM,WAAA;IhBqsEH;EgBpqEH;;;;;;IAxBY,gBAAA;IhBosET;EgB5qEH;;;;;;IApBY,iBAAA;IhBwsET;EgBprEH;;;;IAPY,kBAAA;IhBisET;EACF;AkB35ED;EACE,YAAA;EACA,WAAA;EACA,WAAA;EAIA,cAAA;ElB05ED;AkBv5ED;EACE,gBAAA;EACA,aAAA;EACA,YAAA;EACA,qBAAA;EACA,iBAAA;EACA,sBAAA;EACA,gBAAA;EACA,WAAA;EACA,kCAAA;ElBy5ED;AkBt5ED;EACE,uBAAA;EACA,iBAAA;EACA,oBAAA;EACA,mBAAA;ElBw5ED;AkB74ED;Eb4BE,gCAAA;EACG,6BAAA;EACK,wBAAA;ELo3ET;AkB74ED;;EAEE,iBAAA;EACA,oBAAA;EACA,qBAAA;ElB+4ED;AkB34ED;EACE,gBAAA;ElB64ED;AkBz4ED;EACE,gBAAA;EACA,aAAA;ElB24ED;AkBv4ED;;EAEE,cAAA;ElBy4ED;AkBr4ED;;;EZxEE,sBAAA;EAEA,4CAAA;EACA,sBAAA;ENi9ED;AkBr4ED;EACE,gBAAA;EACA,kBAAA;EACA,iBAAA;EACA,yBAAA;EACA,gBAAA;ElBu4ED;AkB72ED;EACE,gBAAA;EACA,aAAA;EACA,cAAA;EACA,mBAAA;EACA,iBAAA;EACA,yBAAA;EACA,gBAAA;EACA,2BAAA;EACA,wBAAA;EACA,2BAAA;EACA,oBAAA;EbzDA,0DAAA;EACQ,kDAAA;EAyHR,wFAAA;EACK,2EAAA;EACG,wEAAA;ELizET;AmBz7EC;EACE,uBAAA;EACA,YAAA;EdUF,wFAAA;EACQ,gFAAA;ELk7ET;AKj5EC;EACE,gBAAA;EACA,YAAA;ELm5EH;AKj5EC;EAA0B,gBAAA;ELo5E3B;AKn5EC;EAAgC,gBAAA;ELs5EjC;AkBr3EC;;;EAGE,2BAAA;EACA,YAAA;ElBu3EH;AkBp3EC;;EAEE,qBAAA;ElBs3EH;AkBl3EC;EACE,cAAA;ElBo3EH;AkBx2ED;EACE,0BAAA;ElB02ED;AkBt0ED;EAxBE;;;;IAIE,mBAAA;IlBi2ED;EkB/1EC;;;;;;;;IAEE,mBAAA;IlBu2EH;EkBp2EC;;;;;;;;IAEE,mBAAA;IlB42EH;EACF;AkBl2ED;EACE,qBAAA;ElBo2ED;AkB51ED;;EAEE,oBAAA;EACA,gBAAA;EACA,kBAAA;EACA,qBAAA;ElB81ED;AkBn2ED;;EAQI,kBAAA;EACA,oBAAA;EACA,kBAAA;EACA,qBAAA;EACA,iBAAA;ElB+1EH;AkB51ED;;;;EAIE,oBAAA;EACA,oBAAA;EACA,oBAAA;ElB81ED;AkB31ED;;EAEE,kBAAA;ElB61ED;AkBz1ED;;EAEE,oBAAA;EACA,uBAAA;EACA,oBAAA;EACA,kBAAA;EACA,wBAAA;EACA,qBAAA;EACA,iBAAA;ElB21ED;AkBz1ED;;EAEE,eAAA;EACA,mBAAA;ElB21ED;AkBl1EC;;;;;;EAGE,qBAAA;ElBu1EH;AkBj1EC;;;;EAEE,qBAAA;ElBq1EH;AkB/0EC;;;;EAGI,qBAAA;ElBk1EL;AkBv0ED;EAEE,kBAAA;EACA,qBAAA;EAEA,kBAAA;EACA,kBAAA;ElBu0ED;AkBr0EC;;EAEE,iBAAA;EACA,kBAAA;ElBu0EH;AkB1zED;EC1PE,cAAA;EACA,mBAAA;EACA,iBAAA;EACA,kBAAA;EACA,oBAAA;EnBujFD;AmBrjFC;EACE,cAAA;EACA,mBAAA;EnBujFH;AmBpjFC;;EAEE,cAAA;EnBsjFH;AkBt0ED;EC7PE,cAAA;EACA,mBAAA;EACA,iBAAA;EACA,kBAAA;EACA,oBAAA;EnBskFD;AmBpkFC;EACE,cAAA;EACA,mBAAA;EnBskFH;AmBnkFC;;EAEE,cAAA;EnBqkFH;AkBr1ED;EAKI,cAAA;EACA,mBAAA;EACA,iBAAA;EACA,kBAAA;EACA,kBAAA;ElBm1EH;AkB/0ED;EC1QE,cAAA;EACA,oBAAA;EACA,iBAAA;EACA,wBAAA;EACA,oBAAA;EnB4lFD;AmB1lFC;EACE,cAAA;EACA,mBAAA;EnB4lFH;AmBzlFC;;EAEE,cAAA;EnB2lFH;AkB31ED;EC7QE,cAAA;EACA,oBAAA;EACA,iBAAA;EACA,wBAAA;EACA,oBAAA;EnB2mFD;AmBzmFC;EACE,cAAA;EACA,mBAAA;EnB2mFH;AmBxmFC;;EAEE,cAAA;EnB0mFH;AkB12ED;EAKI,cAAA;EACA,oBAAA;EACA,iBAAA;EACA,wBAAA;EACA,kBAAA;ElBw2EH;AkB/1ED;EAEE,oBAAA;ElBg2ED;AkBl2ED;EAMI,uBAAA;ElB+1EH;AkB31ED;EACE,oBAAA;EACA,QAAA;EACA,UAAA;EACA,YAAA;EACA,gBAAA;EACA,aAAA;EACA,cAAA;EACA,mBAAA;EACA,oBAAA;EACA,sBAAA;ElB61ED;AkB31ED;EACE,aAAA;EACA,cAAA;EACA,mBAAA;ElB61ED;AkB31ED;EACE,aAAA;EACA,cAAA;EACA,mBAAA;ElB61ED;AkBz1ED;;;;;;;;;;ECrXI,gBAAA;EnB0tFH;AkBr2ED;ECjXI,uBAAA;Ed+CF,0DAAA;EACQ,kDAAA;EL2qFT;AmBztFG;EACE,uBAAA;Ed4CJ,2EAAA;EACQ,mEAAA;ELgrFT;AkB/2ED;ECvWI,gBAAA;EACA,uBAAA;EACA,2BAAA;EnBytFH;AkBp3ED;ECjWI,gBAAA;EnBwtFH;AkBp3ED;;;;;;;;;;ECxXI,gBAAA;EnBwvFH;AkBh4ED;ECpXI,uBAAA;Ed+CF,0DAAA;EACQ,kDAAA;ELysFT;AmBvvFG;EACE,uBAAA;Ed4CJ,2EAAA;EACQ,mEAAA;EL8sFT;AkB14ED;EC1WI,gBAAA;EACA,uBAAA;EACA,2BAAA;EnBuvFH;AkB/4ED;ECpWI,gBAAA;EnBsvFH;AkB/4ED;;;;;;;;;;EC3XI,gBAAA;EnBsxFH;AkB35ED;ECvXI,uBAAA;Ed+CF,0DAAA;EACQ,kDAAA;ELuuFT;AmBrxFG;EACE,uBAAA;Ed4CJ,2EAAA;EACQ,mEAAA;EL4uFT;AkBr6ED;EC7WI,gBAAA;EACA,uBAAA;EACA,2BAAA;EnBqxFH;AkB16ED;ECvWI,gBAAA;EnBoxFH;AkBt6EC;EACG,WAAA;ElBw6EJ;AkBt6EC;EACG,QAAA;ElBw6EJ;AkB95ED;EACE,gBAAA;EACA,iBAAA;EACA,qBAAA;EACA,gBAAA;ElBg6ED;AkB70ED;EAAA;IA9DM,uBAAA;IACA,kBAAA;IACA,wBAAA;IlB+4EH;EkBn1EH;IAvDM,uBAAA;IACA,aAAA;IACA,wBAAA;IlB64EH;EkBx1EH;IAhDM,uBAAA;IlB24EH;EkB31EH;IA5CM,uBAAA;IACA,wBAAA;IlB04EH;EkB/1EH;;;IAtCQ,aAAA;IlB04EL;EkBp2EH;IAhCM,aAAA;IlBu4EH;EkBv2EH;IA5BM,kBAAA;IACA,wBAAA;IlBs4EH;EkB32EH;;IApBM,uBAAA;IACA,eAAA;IACA,kBAAA;IACA,wBAAA;IlBm4EH;EkBl3EH;;IAdQ,iBAAA;IlBo4EL;EkBt3EH;;IATM,oBAAA;IACA,gBAAA;IlBm4EH;EkB33EH;IAHM,QAAA;IlBi4EH;EACF;AkBv3ED;;;;EASI,eAAA;EACA,kBAAA;EACA,kBAAA;ElBo3EH;AkB/3ED;;EAiBI,kBAAA;ElBk3EH;AkBn4ED;EJjfE,oBAAA;EACA,qBAAA;Edu3FD;AkBh2EC;EAAA;IAVI,mBAAA;IACA,kBAAA;IACA,kBAAA;IlB82EH;EACF;AkB94ED;EAwCI,aAAA;ElBy2EH;AkB51EC;EAAA;IAHM,0BAAA;IlBm2EL;EACF;AkB11EC;EAAA;IAHM,kBAAA;IlBi2EL;EACF;AoBn5FD;EACE,uBAAA;EACA,kBAAA;EACA,qBAAA;EACA,oBAAA;EACA,wBAAA;EACA,gCAAA;MAAA,4BAAA;EACA,iBAAA;EACA,wBAAA;EACA,+BAAA;EACA,qBAAA;EC6BA,mBAAA;EACA,iBAAA;EACA,yBAAA;EACA,oBAAA;EhB4KA,2BAAA;EACG,wBAAA;EACC,uBAAA;EACI,mBAAA;EL8sFT;AoBt5FG;;;;;;EdrBF,sBAAA;EAEA,4CAAA;EACA,sBAAA;ENk7FD;AoB15FC;;;EAGE,gBAAA;EACA,uBAAA;EpB45FH;AoBz5FC;;EAEE,YAAA;EACA,wBAAA;Ef2BF,0DAAA;EACQ,kDAAA;ELi4FT;AoBz5FC;;;EAGE,qBAAA;EACA,sBAAA;EE9CF,eAAA;EAGA,2BAAA;EjB8DA,0BAAA;EACQ,kBAAA;EL24FT;AoBr5FD;ECrDE,gBAAA;EACA,2BAAA;EACA,uBAAA;ErB68FD;AqB38FC;;;;;;EAME,gBAAA;EACA,2BAAA;EACI,uBAAA;ErB68FP;AqB38FC;;;EAGE,wBAAA;ErB68FH;AqBx8FG;;;;;;;;;;;;;;;;;;EAME,2BAAA;EACI,uBAAA;ErBs9FT;AoB97FD;ECnBI,gBAAA;EACA,2BAAA;ErBo9FH;AoB/7FD;ECxDE,gBAAA;EACA,2BAAA;EACA,uBAAA;ErB0/FD;AqBx/FC;;;;;;EAME,gBAAA;EACA,2BAAA;EACI,uBAAA;ErB0/FP;AqBx/FC;;;EAGE,wBAAA;ErB0/FH;AqBr/FG;;;;;;;;;;;;;;;;;;EAME,2BAAA;EACI,uBAAA;ErBmgGT;AoBx+FD;ECtBI,gBAAA;EACA,2BAAA;ErBigGH;AoBx+FD;EC5DE,gBAAA;EACA,2BAAA;EACA,uBAAA;ErBuiGD;AqBriGC;;;;;;EAME,gBAAA;EACA,2BAAA;EACI,uBAAA;ErBuiGP;AqBriGC;;;EAGE,wBAAA;ErBuiGH;AqBliGG;;;;;;;;;;;;;;;;;;EAME,2BAAA;EACI,uBAAA;ErBgjGT;AoBjhGD;EC1BI,gBAAA;EACA,2BAAA;ErB8iGH;AoBjhGD;EChEE,gBAAA;EACA,2BAAA;EACA,uBAAA;ErBolGD;AqBllGC;;;;;;EAME,gBAAA;EACA,2BAAA;EACI,uBAAA;ErBolGP;AqBllGC;;;EAGE,wBAAA;ErBolGH;AqB/kGG;;;;;;;;;;;;;;;;;;EAME,2BAAA;EACI,uBAAA;ErB6lGT;AoB1jGD;EC9BI,gBAAA;EACA,2BAAA;ErB2lGH;AoB1jGD;ECpEE,gBAAA;EACA,2BAAA;EACA,uBAAA;ErBioGD;AqB/nGC;;;;;;EAME,gBAAA;EACA,2BAAA;EACI,uBAAA;ErBioGP;AqB/nGC;;;EAGE,wBAAA;ErBioGH;AqB5nGG;;;;;;;;;;;;;;;;;;EAME,2BAAA;EACI,uBAAA;ErB0oGT;AoBnmGD;EClCI,gBAAA;EACA,2BAAA;ErBwoGH;AoBnmGD;ECxEE,gBAAA;EACA,2BAAA;EACA,uBAAA;ErB8qGD;AqB5qGC;;;;;;EAME,gBAAA;EACA,2BAAA;EACI,uBAAA;ErB8qGP;AqB5qGC;;;EAGE,wBAAA;ErB8qGH;AqBzqGG;;;;;;;;;;;;;;;;;;EAME,2BAAA;EACI,uBAAA;ErBurGT;AoB5oGD;ECtCI,gBAAA;EACA,2BAAA;ErBqrGH;AoBvoGD;EACE,gBAAA;EACA,qBAAA;EACA,kBAAA;EpByoGD;AoBvoGC;;;;;EAKE,+BAAA;Ef7BF,0BAAA;EACQ,kBAAA;ELuqGT;AoBxoGC;;;;EAIE,2BAAA;EpB0oGH;AoBxoGC;;EAEE,gBAAA;EACA,4BAAA;EACA,+BAAA;EpB0oGH;AoBtoGG;;;;EAEE,gBAAA;EACA,uBAAA;EpB0oGL;AoBjoGD;;EC/EE,oBAAA;EACA,iBAAA;EACA,wBAAA;EACA,oBAAA;ErBotGD;AoBpoGD;;ECnFE,mBAAA;EACA,iBAAA;EACA,kBAAA;EACA,oBAAA;ErB2tGD;AoBvoGD;;ECvFE,kBAAA;EACA,iBAAA;EACA,kBAAA;EACA,oBAAA;ErBkuGD;AoBtoGD;EACE,gBAAA;EACA,aAAA;EpBwoGD;AoBpoGD;EACE,iBAAA;EpBsoGD;AoB/nGC;;;EACE,aAAA;EpBmoGH;AuBvxGD;EACE,YAAA;ElBoLA,0CAAA;EACK,qCAAA;EACG,kCAAA;ELsmGT;AuB1xGC;EACE,YAAA;EvB4xGH;AuBxxGD;EACE,eAAA;EvB0xGD;AuBxxGC;EAAY,gBAAA;EvB2xGb;AuB1xGC;EAAY,oBAAA;EvB6xGb;AuB5xGC;EAAY,0BAAA;EvB+xGb;AuB5xGD;EACE,oBAAA;EACA,WAAA;EACA,kBAAA;ElBuKA,iDAAA;EACQ,4CAAA;KAAA,yCAAA;EAOR,oCAAA;EACQ,+BAAA;KAAA,4BAAA;EAGR,0CAAA;EACQ,qCAAA;KAAA,kCAAA;ELgnGT;AwB1zGD;EACE,uBAAA;EACA,UAAA;EACA,WAAA;EACA,kBAAA;EACA,wBAAA;EACA,wBAAA;EACA,qCAAA;EACA,oCAAA;ExB4zGD;AwBxzGD;;EAEE,oBAAA;ExB0zGD;AwBtzGD;EACE,YAAA;ExBwzGD;AwBpzGD;EACE,oBAAA;EACA,WAAA;EACA,SAAA;EACA,eAAA;EACA,eAAA;EACA,aAAA;EACA,kBAAA;EACA,gBAAA;EACA,iBAAA;EACA,kBAAA;EACA,iBAAA;EACA,kBAAA;EACA,2BAAA;EACA,2BAAA;EACA,uCAAA;EACA,oBAAA;EnBuBA,qDAAA;EACQ,6CAAA;EmBtBR,sCAAA;UAAA,8BAAA;ExBuzGD;AwBlzGC;EACE,UAAA;EACA,YAAA;ExBozGH;AwB70GD;ECxBE,aAAA;EACA,eAAA;EACA,kBAAA;EACA,2BAAA;EzBw2GD;AwBn1GD;EAmCI,gBAAA;EACA,mBAAA;EACA,aAAA;EACA,qBAAA;EACA,yBAAA;EACA,gBAAA;EACA,qBAAA;ExBmzGH;AwB7yGC;;EAEE,uBAAA;EACA,gBAAA;EACA,2BAAA;ExB+yGH;AwBzyGC;;;EAGE,gBAAA;EACA,uBAAA;EACA,YAAA;EACA,2BAAA;ExB2yGH;AwBlyGC;;;EAGE,gBAAA;ExBoyGH;AwBhyGC;;EAEE,uBAAA;EACA,+BAAA;EACA,wBAAA;EE1GF,qEAAA;EF4GE,qBAAA;ExBkyGH;AwB7xGD;EAGI,gBAAA;ExB6xGH;AwBhyGD;EAQI,YAAA;ExB2xGH;AwBnxGD;EACE,YAAA;EACA,UAAA;ExBqxGD;AwB7wGD;EACE,SAAA;EACA,aAAA;ExB+wGD;AwB3wGD;EACE,gBAAA;EACA,mBAAA;EACA,iBAAA;EACA,yBAAA;EACA,gBAAA;EACA,qBAAA;ExB6wGD;AwBzwGD;EACE,iBAAA;EACA,SAAA;EACA,UAAA;EACA,WAAA;EACA,QAAA;EACA,cAAA;ExB2wGD;AwBvwGD;EACE,UAAA;EACA,YAAA;ExBywGD;AwBjwGD;;EAII,eAAA;EACA,0BAAA;EACA,aAAA;ExBiwGH;AwBvwGD;;EAUI,WAAA;EACA,cAAA;EACA,oBAAA;ExBiwGH;AwB5uGD;EAXE;IAnEA,YAAA;IACA,UAAA;IxB8zGC;EwB5vGD;IAzDA,SAAA;IACA,aAAA;IxBwzGC;EACF;A2Bv8GD;;EAEE,oBAAA;EACA,uBAAA;EACA,wBAAA;E3By8GD;A2B78GD;;EAMI,oBAAA;EACA,aAAA;E3B28GH;A2Bz8GG;;;;;;;;EAIE,YAAA;E3B+8GL;A2Bz8GD;;;;EAKI,mBAAA;E3B08GH;A2Br8GD;EACE,mBAAA;E3Bu8GD;A2Bx8GD;;EAMI,aAAA;E3Bs8GH;A2B58GD;;;EAWI,kBAAA;E3Bs8GH;A2Bl8GD;EACE,kBAAA;E3Bo8GD;A2Bh8GD;EACE,gBAAA;E3Bk8GD;A2Bj8GC;ECjDA,+BAAA;EACG,4BAAA;E5Bq/GJ;A2Bh8GD;;EC9CE,8BAAA;EACG,2BAAA;E5Bk/GJ;A2B/7GD;EACE,aAAA;E3Bi8GD;A2B/7GD;EACE,kBAAA;E3Bi8GD;A2B/7GD;;EClEE,+BAAA;EACG,4BAAA;E5BqgHJ;A2B97GD;EChEE,8BAAA;EACG,2BAAA;E5BigHJ;A2B77GD;;EAEE,YAAA;E3B+7GD;A2B96GD;EACE,mBAAA;EACA,oBAAA;E3Bg7GD;A2B96GD;EACE,oBAAA;EACA,qBAAA;E3Bg7GD;A2B36GD;EtB9CE,0DAAA;EACQ,kDAAA;EL49GT;A2B36GC;EtBlDA,0BAAA;EACQ,kBAAA;ELg+GT;A2Bx6GD;EACE,gBAAA;E3B06GD;A2Bv6GD;EACE,yBAAA;EACA,wBAAA;E3By6GD;A2Bt6GD;EACE,yBAAA;E3Bw6GD;A2Bj6GD;;;EAII,gBAAA;EACA,aAAA;EACA,aAAA;EACA,iBAAA;E3Bk6GH;A2Bz6GD;EAcM,aAAA;E3B85GL;A2B56GD;;;;EAsBI,kBAAA;EACA,gBAAA;E3B45GH;A2Bv5GC;EACE,kBAAA;E3By5GH;A2Bv5GC;EACE,8BAAA;ECnKF,+BAAA;EACC,8BAAA;E5B6jHF;A2Bx5GC;EACE,gCAAA;EC/KF,4BAAA;EACC,2BAAA;E5B0kHF;A2Bx5GD;EACE,kBAAA;E3B05GD;A2Bx5GD;;EC9KE,+BAAA;EACC,8BAAA;E5B0kHF;A2Bv5GD;EC5LE,4BAAA;EACC,2BAAA;E5BslHF;A2Bn5GD;EACE,gBAAA;EACA,aAAA;EACA,qBAAA;EACA,2BAAA;E3Bq5GD;A2Bz5GD;;EAOI,aAAA;EACA,qBAAA;EACA,WAAA;E3Bs5GH;A2B/5GD;EAYI,aAAA;E3Bs5GH;A2Bl6GD;EAgBI,YAAA;E3Bq5GH;A2Bp4GD;;;;EAKM,oBAAA;EACA,wBAAA;EACA,sBAAA;E3Bq4GL;A6B9mHD;EACE,oBAAA;EACA,gBAAA;EACA,2BAAA;E7BgnHD;A6B7mHC;EACE,aAAA;EACA,iBAAA;EACA,kBAAA;E7B+mHH;A6BxnHD;EAeI,oBAAA;EACA,YAAA;EAKA,aAAA;EAEA,aAAA;EACA,kBAAA;E7BumHH;A6B9lHD;;;EV8BE,cAAA;EACA,oBAAA;EACA,iBAAA;EACA,wBAAA;EACA,oBAAA;EnBqkHD;AmBnkHC;;;EACE,cAAA;EACA,mBAAA;EnBukHH;AmBpkHC;;;;;;EAEE,cAAA;EnB0kHH;A6BhnHD;;;EVyBE,cAAA;EACA,mBAAA;EACA,iBAAA;EACA,kBAAA;EACA,oBAAA;EnB4lHD;AmB1lHC;;;EACE,cAAA;EACA,mBAAA;EnB8lHH;AmB3lHC;;;;;;EAEE,cAAA;EnBimHH;A6B9nHD;;;EAGE,qBAAA;E7BgoHD;A6B9nHC;;;EACE,kBAAA;E7BkoHH;A6B9nHD;;EAEE,WAAA;EACA,qBAAA;EACA,wBAAA;E7BgoHD;A6B3nHD;EACE,mBAAA;EACA,iBAAA;EACA,qBAAA;EACA,gBAAA;EACA,gBAAA;EACA,oBAAA;EACA,2BAAA;EACA,2BAAA;EACA,oBAAA;E7B6nHD;A6B1nHC;EACE,mBAAA;EACA,iBAAA;EACA,oBAAA;E7B4nHH;A6B1nHC;EACE,oBAAA;EACA,iBAAA;EACA,oBAAA;E7B4nHH;A6BhpHD;;EA0BI,eAAA;E7B0nHH;A6BrnHD;;;;;;;EDhGE,+BAAA;EACG,4BAAA;E5B8tHJ;A6BtnHD;EACE,iBAAA;E7BwnHD;A6BtnHD;;;;;;;EDpGE,8BAAA;EACG,2BAAA;E5BmuHJ;A6BvnHD;EACE,gBAAA;E7BynHD;A6BpnHD;EACE,oBAAA;EAGA,cAAA;EACA,qBAAA;E7BonHD;A6BznHD;EAUI,oBAAA;E7BknHH;A6B5nHD;EAYM,mBAAA;E7BmnHL;A6BhnHG;;;EAGE,YAAA;E7BknHL;A6B7mHC;;EAGI,oBAAA;E7B8mHL;A6B3mHC;;EAGI,mBAAA;E7B4mHL;A8BtwHD;EACE,kBAAA;EACA,iBAAA;EACA,kBAAA;E9BwwHD;A8B3wHD;EAOI,oBAAA;EACA,gBAAA;E9BuwHH;A8B/wHD;EAWM,oBAAA;EACA,gBAAA;EACA,oBAAA;E9BuwHL;A8BtwHK;;EAEE,uBAAA;EACA,2BAAA;E9BwwHP;A8BnwHG;EACE,gBAAA;E9BqwHL;A8BnwHK;;EAEE,gBAAA;EACA,uBAAA;EACA,+BAAA;EACA,qBAAA;E9BqwHP;A8B9vHG;;;EAGE,2BAAA;EACA,uBAAA;E9BgwHL;A8BzyHD;ELHE,aAAA;EACA,eAAA;EACA,kBAAA;EACA,2BAAA;EzB+yHD;A8B/yHD;EA0DI,iBAAA;E9BwvHH;A8B/uHD;EACE,kCAAA;E9BivHD;A8BlvHD;EAGI,aAAA;EAEA,qBAAA;E9BivHH;A8BtvHD;EASM,mBAAA;EACA,yBAAA;EACA,+BAAA;EACA,4BAAA;E9BgvHL;A8B/uHK;EACE,uCAAA;E9BivHP;A8B3uHK;;;EAGE,gBAAA;EACA,2BAAA;EACA,2BAAA;EACA,kCAAA;EACA,iBAAA;E9B6uHP;A8BxuHC;EAqDA,aAAA;EA8BA,kBAAA;E9BypHD;A8B5uHC;EAwDE,aAAA;E9BurHH;A8B/uHC;EA0DI,oBAAA;EACA,oBAAA;E9BwrHL;A8BnvHC;EAgEE,WAAA;EACA,YAAA;E9BsrHH;A8B1qHD;EAAA;IAPM,qBAAA;IACA,WAAA;I9BqrHH;E8B/qHH;IAJQ,kBAAA;I9BsrHL;EACF;A8BhwHC;EAuFE,iBAAA;EACA,oBAAA;E9B4qHH;A8BpwHC;;;EA8FE,2BAAA;E9B2qHH;A8B7pHD;EAAA;IATM,kCAAA;IACA,4BAAA;I9B0qHH;E8BlqHH;;;IAHM,8BAAA;I9B0qHH;EACF;A8B3wHD;EAEI,aAAA;E9B4wHH;A8B9wHD;EAMM,oBAAA;E9B2wHL;A8BjxHD;EASM,kBAAA;E9B2wHL;A8BtwHK;;;EAGE,gBAAA;EACA,2BAAA;E9BwwHP;A8BhwHD;EAEI,aAAA;E9BiwHH;A8BnwHD;EAIM,iBAAA;EACA,gBAAA;E9BkwHL;A8BtvHD;EACE,aAAA;E9BwvHD;A8BzvHD;EAII,aAAA;E9BwvHH;A8B5vHD;EAMM,oBAAA;EACA,oBAAA;E9ByvHL;A8BhwHD;EAYI,WAAA;EACA,YAAA;E9BuvHH;A8B3uHD;EAAA;IAPM,qBAAA;IACA,WAAA;I9BsvHH;E8BhvHH;IAJQ,kBAAA;I9BuvHL;EACF;A8B/uHD;EACE,kBAAA;E9BivHD;A8BlvHD;EAKI,iBAAA;EACA,oBAAA;E9BgvHH;A8BtvHD;;;EAYI,2BAAA;E9B+uHH;A8BjuHD;EAAA;IATM,kCAAA;IACA,4BAAA;I9B8uHH;E8BtuHH;;;IAHM,8BAAA;I9B8uHH;EACF;A8BruHD;EAEI,eAAA;E9BsuHH;A8BxuHD;EAKI,gBAAA;E9BsuHH;A8B7tHD;EAEE,kBAAA;EF3OA,4BAAA;EACC,2BAAA;E5B08HF;A+Bp8HD;EACE,oBAAA;EACA,kBAAA;EACA,qBAAA;EACA,+BAAA;E/Bs8HD;A+B97HD;EAAA;IAFI,oBAAA;I/Bo8HD;EACF;A+Br7HD;EAAA;IAFI,aAAA;I/B27HD;EACF;A+B76HD;EACE,qBAAA;EACA,qBAAA;EACA,oBAAA;EACA,mCAAA;EACA,4DAAA;UAAA,oDAAA;EAEA,mCAAA;E/B86HD;A+B56HC;EACE,kBAAA;E/B86HH;A+Bl5HD;EAAA;IAxBI,aAAA;IACA,eAAA;IACA,0BAAA;YAAA,kBAAA;I/B86HD;E+B56HC;IACE,2BAAA;IACA,yBAAA;IACA,mBAAA;IACA,8BAAA;I/B86HH;E+B36HC;IACE,qBAAA;I/B66HH;E+Bx6HC;;;IAGE,iBAAA;IACA,kBAAA;I/B06HH;EACF;A+Bt6HD;;EAGI,mBAAA;E/Bu6HH;A+Bl6HC;EAAA;;IAFI,mBAAA;I/By6HH;EACF;A+Bh6HD;;;;EAII,qBAAA;EACA,oBAAA;E/Bk6HH;A+B55HC;EAAA;;;;IAHI,iBAAA;IACA,gBAAA;I/Bs6HH;EACF;A+B15HD;EACE,eAAA;EACA,uBAAA;E/B45HD;A+Bv5HD;EAAA;IAFI,kBAAA;I/B65HD;EACF;A+Bz5HD;;EAEE,iBAAA;EACA,UAAA;EACA,SAAA;EACA,eAAA;E/B25HD;A+Br5HD;EAAA;;IAFI,kBAAA;I/B45HD;EACF;A+B15HD;EACE,QAAA;EACA,uBAAA;E/B45HD;A+B15HD;EACE,WAAA;EACA,kBAAA;EACA,uBAAA;E/B45HD;A+Bt5HD;EACE,aAAA;EACA,oBAAA;EACA,iBAAA;EACA,mBAAA;EACA,cAAA;E/Bw5HD;A+Bt5HC;;EAEE,uBAAA;E/Bw5HH;A+Bj6HD;EAaI,gBAAA;E/Bu5HH;A+B94HD;EALI;;IAEE,oBAAA;I/Bs5HH;EACF;A+B54HD;EACE,oBAAA;EACA,cAAA;EACA,oBAAA;EACA,mBAAA;EC9LA,iBAAA;EACA,oBAAA;ED+LA,+BAAA;EACA,wBAAA;EACA,+BAAA;EACA,oBAAA;E/B+4HD;A+B34HC;EACE,YAAA;E/B64HH;A+B35HD;EAmBI,gBAAA;EACA,aAAA;EACA,aAAA;EACA,oBAAA;E/B24HH;A+Bj6HD;EAyBI,iBAAA;E/B24HH;A+Br4HD;EAAA;IAFI,eAAA;I/B24HD;EACF;A+Bl4HD;EACE,qBAAA;E/Bo4HD;A+Br4HD;EAII,mBAAA;EACA,sBAAA;EACA,mBAAA;E/Bo4HH;A+Bx2HC;EAAA;IAtBI,kBAAA;IACA,aAAA;IACA,aAAA;IACA,eAAA;IACA,+BAAA;IACA,WAAA;IACA,0BAAA;YAAA,kBAAA;I/Bk4HH;E+Bl3HD;;IAbM,4BAAA;I/Bm4HL;E+Bt3HD;IAVM,mBAAA;I/Bm4HL;E+Bl4HK;;IAEE,wBAAA;I/Bo4HP;EACF;A+Bl3HD;EAAA;IAXI,aAAA;IACA,WAAA;I/Bi4HD;E+Bv3HH;IAPM,aAAA;I/Bi4HH;E+B13HH;IALQ,mBAAA;IACA,sBAAA;I/Bk4HL;EACF;A+Bv3HD;EACE,oBAAA;EACA,qBAAA;EACA,oBAAA;EACA,mCAAA;EACA,sCAAA;E1B9NA,8FAAA;EACQ,sFAAA;E2B/DR,iBAAA;EACA,oBAAA;EhCwpID;AkBvqHD;EAAA;IA9DM,uBAAA;IACA,kBAAA;IACA,wBAAA;IlByuHH;EkB7qHH;IAvDM,uBAAA;IACA,aAAA;IACA,wBAAA;IlBuuHH;EkBlrHH;IAhDM,uBAAA;IlBquHH;EkBrrHH;IA5CM,uBAAA;IACA,wBAAA;IlBouHH;EkBzrHH;;;IAtCQ,aAAA;IlBouHL;EkB9rHH;IAhCM,aAAA;IlBiuHH;EkBjsHH;IA5BM,kBAAA;IACA,wBAAA;IlBguHH;EkBrsHH;;IApBM,uBAAA;IACA,eAAA;IACA,kBAAA;IACA,wBAAA;IlB6tHH;EkB5sHH;;IAdQ,iBAAA;IlB8tHL;EkBhtHH;;IATM,oBAAA;IACA,gBAAA;IlB6tHH;EkBrtHH;IAHM,QAAA;IlB2tHH;EACF;A+Bh6HC;EAAA;IANI,oBAAA;I/B06HH;E+Bx6HG;IACE,kBAAA;I/B06HL;EACF;A+Bz5HD;EAAA;IARI,aAAA;IACA,WAAA;IACA,gBAAA;IACA,iBAAA;IACA,gBAAA;IACA,mBAAA;I1BzPF,0BAAA;IACQ,kBAAA;IL+pIP;EACF;A+B/5HD;EACE,eAAA;EHpUA,4BAAA;EACC,2BAAA;E5BsuIF;A+B/5HD;EACE,kBAAA;EHzUA,8BAAA;EACC,6BAAA;EAOD,+BAAA;EACC,8BAAA;E5BquIF;A+B35HD;EChVE,iBAAA;EACA,oBAAA;EhC8uID;A+B55HC;ECnVA,kBAAA;EACA,qBAAA;EhCkvID;A+B75HC;ECtVA,kBAAA;EACA,qBAAA;EhCsvID;A+Bv5HD;EChWE,kBAAA;EACA,qBAAA;EhC0vID;A+Bn5HD;EAAA;IAJI,aAAA;IACA,mBAAA;IACA,oBAAA;I/B25HD;EACF;A+B93HD;EAhBE;IExWA,wBAAA;IjC0vIC;E+Bj5HD;IE5WA,yBAAA;IF8WE,qBAAA;I/Bm5HD;E+Br5HD;IAKI,iBAAA;I/Bm5HH;EACF;A+B14HD;EACE,2BAAA;EACA,uBAAA;E/B44HD;A+B94HD;EAKI,gBAAA;E/B44HH;A+B34HG;;EAEE,gBAAA;EACA,+BAAA;E/B64HL;A+Bt5HD;EAcI,gBAAA;E/B24HH;A+Bz5HD;EAmBM,gBAAA;E/By4HL;A+Bv4HK;;EAEE,gBAAA;EACA,+BAAA;E/By4HP;A+Br4HK;;;EAGE,gBAAA;EACA,2BAAA;E/Bu4HP;A+Bn4HK;;;EAGE,gBAAA;EACA,+BAAA;E/Bq4HP;A+B76HD;EA8CI,uBAAA;E/Bk4HH;A+Bj4HG;;EAEE,2BAAA;E/Bm4HL;A+Bp7HD;EAoDM,2BAAA;E/Bm4HL;A+Bv7HD;;EA0DI,uBAAA;E/Bi4HH;A+B13HK;;;EAGE,2BAAA;EACA,gBAAA;E/B43HP;A+B31HC;EAAA;IAzBQ,gBAAA;I/Bw3HP;E+Bv3HO;;IAEE,gBAAA;IACA,+BAAA;I/By3HT;E+Br3HO;;;IAGE,gBAAA;IACA,2BAAA;I/Bu3HT;E+Bn3HO;;;IAGE,gBAAA;IACA,+BAAA;I/Bq3HT;EACF;A+Bv9HD;EA8GI,gBAAA;E/B42HH;A+B32HG;EACE,gBAAA;E/B62HL;A+B79HD;EAqHI,gBAAA;E/B22HH;A+B12HG;;EAEE,gBAAA;E/B42HL;A+Bx2HK;;;;EAEE,gBAAA;E/B42HP;A+Bp2HD;EACE,2BAAA;EACA,uBAAA;E/Bs2HD;A+Bx2HD;EAKI,gBAAA;E/Bs2HH;A+Br2HG;;EAEE,gBAAA;EACA,+BAAA;E/Bu2HL;A+Bh3HD;EAcI,gBAAA;E/Bq2HH;A+Bn3HD;EAmBM,gBAAA;E/Bm2HL;A+Bj2HK;;EAEE,gBAAA;EACA,+BAAA;E/Bm2HP;A+B/1HK;;;EAGE,gBAAA;EACA,2BAAA;E/Bi2HP;A+B71HK;;;EAGE,gBAAA;EACA,+BAAA;E/B+1HP;A+Bv4HD;EA+CI,uBAAA;E/B21HH;A+B11HG;;EAEE,2BAAA;E/B41HL;A+B94HD;EAqDM,2BAAA;E/B41HL;A+Bj5HD;;EA2DI,uBAAA;E/B01HH;A+Bp1HK;;;EAGE,2BAAA;EACA,gBAAA;E/Bs1HP;A+B/yHC;EAAA;IA/BQ,uBAAA;I/Bk1HP;E+BnzHD;IA5BQ,2BAAA;I/Bk1HP;E+BtzHD;IAzBQ,gBAAA;I/Bk1HP;E+Bj1HO;;IAEE,gBAAA;IACA,+BAAA;I/Bm1HT;E+B/0HO;;;IAGE,gBAAA;IACA,2BAAA;I/Bi1HT;E+B70HO;;;IAGE,gBAAA;IACA,+BAAA;I/B+0HT;EACF;A+Bv7HD;EA+GI,gBAAA;E/B20HH;A+B10HG;EACE,gBAAA;E/B40HL;A+B77HD;EAsHI,gBAAA;E/B00HH;A+Bz0HG;;EAEE,gBAAA;E/B20HL;A+Bv0HK;;;;EAEE,gBAAA;E/B20HP;AkCr9ID;EACE,mBAAA;EACA,qBAAA;EACA,kBAAA;EACA,2BAAA;EACA,oBAAA;ElCu9ID;AkC59ID;EAQI,uBAAA;ElCu9IH;AkC/9ID;EAWM,mBAAA;EACA,gBAAA;EACA,gBAAA;ElCu9IL;AkCp+ID;EAkBI,gBAAA;ElCq9IH;AmCz+ID;EACE,uBAAA;EACA,iBAAA;EACA,gBAAA;EACA,oBAAA;EnC2+ID;AmC/+ID;EAOI,iBAAA;EnC2+IH;AmCl/ID;;EAUM,oBAAA;EACA,aAAA;EACA,mBAAA;EACA,yBAAA;EACA,uBAAA;EACA,gBAAA;EACA,2BAAA;EACA,2BAAA;EACA,mBAAA;EnC4+IL;AmC1+IG;;EAGI,gBAAA;EPXN,gCAAA;EACG,6BAAA;E5Bu/IJ;AmCz+IG;;EPvBF,iCAAA;EACG,8BAAA;E5BogJJ;AmCp+IG;;;;EAEE,gBAAA;EACA,2BAAA;EACA,uBAAA;EnCw+IL;AmCl+IG;;;;;;EAGE,YAAA;EACA,gBAAA;EACA,2BAAA;EACA,uBAAA;EACA,iBAAA;EnCu+IL;AmC7hJD;;;;;;EAiEM,gBAAA;EACA,2BAAA;EACA,uBAAA;EACA,qBAAA;EnCo+IL;AmC39ID;;EC1EM,oBAAA;EACA,iBAAA;EpCyiJL;AoCviJG;;ERMF,gCAAA;EACG,6BAAA;E5BqiJJ;AoCtiJG;;ERRF,iCAAA;EACG,8BAAA;E5BkjJJ;AmCr+ID;;EC/EM,mBAAA;EACA,iBAAA;EpCwjJL;AoCtjJG;;ERMF,gCAAA;EACG,6BAAA;E5BojJJ;AoCrjJG;;ERRF,iCAAA;EACG,8BAAA;E5BikJJ;AqCpkJD;EACE,iBAAA;EACA,gBAAA;EACA,kBAAA;EACA,oBAAA;ErCskJD;AqC1kJD;EAOI,iBAAA;ErCskJH;AqC7kJD;;EAUM,uBAAA;EACA,mBAAA;EACA,2BAAA;EACA,2BAAA;EACA,qBAAA;ErCukJL;AqCrlJD;;EAmBM,uBAAA;EACA,2BAAA;ErCskJL;AqC1lJD;;EA2BM,cAAA;ErCmkJL;AqC9lJD;;EAkCM,aAAA;ErCgkJL;AqClmJD;;;;EA2CM,gBAAA;EACA,2BAAA;EACA,qBAAA;ErC6jJL;AsC3mJD;EACE,iBAAA;EACA,yBAAA;EACA,gBAAA;EACA,mBAAA;EACA,gBAAA;EACA,gBAAA;EACA,oBAAA;EACA,qBAAA;EACA,0BAAA;EACA,sBAAA;EtC6mJD;AsCzmJG;;EAEE,gBAAA;EACA,uBAAA;EACA,iBAAA;EtC2mJL;AsCtmJC;EACE,eAAA;EtCwmJH;AsCpmJC;EACE,oBAAA;EACA,WAAA;EtCsmJH;AsC/lJD;ECtCE,2BAAA;EvCwoJD;AuCroJG;;EAEE,2BAAA;EvCuoJL;AsClmJD;EC1CE,2BAAA;EvC+oJD;AuC5oJG;;EAEE,2BAAA;EvC8oJL;AsCrmJD;EC9CE,2BAAA;EvCspJD;AuCnpJG;;EAEE,2BAAA;EvCqpJL;AsCxmJD;EClDE,2BAAA;EvC6pJD;AuC1pJG;;EAEE,2BAAA;EvC4pJL;AsC3mJD;ECtDE,2BAAA;EvCoqJD;AuCjqJG;;EAEE,2BAAA;EvCmqJL;AsC9mJD;EC1DE,2BAAA;EvC2qJD;AuCxqJG;;EAEE,2BAAA;EvC0qJL;AwC5qJD;EACE,uBAAA;EACA,iBAAA;EACA,kBAAA;EACA,iBAAA;EACA,mBAAA;EACA,gBAAA;EACA,gBAAA;EACA,0BAAA;EACA,qBAAA;EACA,oBAAA;EACA,2BAAA;EACA,qBAAA;ExC8qJD;AwC3qJC;EACE,eAAA;ExC6qJH;AwCzqJC;EACE,oBAAA;EACA,WAAA;ExC2qJH;AwCxqJC;;EAEE,QAAA;EACA,kBAAA;ExC0qJH;AwCrqJG;;EAEE,gBAAA;EACA,uBAAA;EACA,iBAAA;ExCuqJL;AwClqJC;;EAEE,gBAAA;EACA,2BAAA;ExCoqJH;AwCjqJC;EACE,cAAA;ExCmqJH;AwChqJC;EACE,mBAAA;ExCkqJH;AwC/pJC;EACE,kBAAA;ExCiqJH;AyC3tJD;EACE,oBAAA;EACA,qBAAA;EACA,gBAAA;EACA,2BAAA;EzC6tJD;AyCjuJD;;EAQI,gBAAA;EzC6tJH;AyCruJD;EAYI,qBAAA;EACA,iBAAA;EACA,kBAAA;EzC4tJH;AyC1uJD;EAkBI,2BAAA;EzC2tJH;AyCxtJC;;EAEE,oBAAA;EzC0tJH;AyCjvJD;EA2BI,iBAAA;EzCytJH;AyCxsJD;EAAA;IAbI,iBAAA;IzCytJD;EyCvtJC;;IAEE,oBAAA;IACA,qBAAA;IzCytJH;EyCjtJH;;IAHM,iBAAA;IzCwtJH;EACF;A0CjwJD;EACE,gBAAA;EACA,cAAA;EACA,qBAAA;EACA,yBAAA;EACA,2BAAA;EACA,2BAAA;EACA,oBAAA;ErCiLA,6CAAA;EACK,wCAAA;EACG,qCAAA;ELmlJT;A0C7wJD;;EAaI,mBAAA;EACA,oBAAA;E1CowJH;A0ChwJC;;;EAGE,uBAAA;E1CkwJH;A0CvxJD;EA0BI,cAAA;EACA,gBAAA;E1CgwJH;A2CzxJD;EACE,eAAA;EACA,qBAAA;EACA,+BAAA;EACA,oBAAA;E3C2xJD;A2C/xJD;EAQI,eAAA;EAEA,gBAAA;E3CyxJH;A2CnyJD;EAeI,mBAAA;E3CuxJH;A2CtyJD;;EAqBI,kBAAA;E3CqxJH;A2C1yJD;EAyBI,iBAAA;E3CoxJH;A2C5wJD;;EAEE,qBAAA;E3C8wJD;A2ChxJD;;EAMI,oBAAA;EACA,WAAA;EACA,cAAA;EACA,gBAAA;E3C8wJH;A2CtwJD;ECvDE,2BAAA;EACA,uBAAA;EACA,gBAAA;E5Cg0JD;A2C3wJD;EClDI,2BAAA;E5Cg0JH;A2C9wJD;EC/CI,gBAAA;E5Cg0JH;A2C7wJD;EC3DE,2BAAA;EACA,uBAAA;EACA,gBAAA;E5C20JD;A2ClxJD;ECtDI,2BAAA;E5C20JH;A2CrxJD;ECnDI,gBAAA;E5C20JH;A2CpxJD;EC/DE,2BAAA;EACA,uBAAA;EACA,gBAAA;E5Cs1JD;A2CzxJD;EC1DI,2BAAA;E5Cs1JH;A2C5xJD;ECvDI,gBAAA;E5Cs1JH;A2C3xJD;ECnEE,2BAAA;EACA,uBAAA;EACA,gBAAA;E5Ci2JD;A2ChyJD;EC9DI,2BAAA;E5Ci2JH;A2CnyJD;EC3DI,gBAAA;E5Ci2JH;A6Cn2JD;EACE;IAAQ,6BAAA;I7Cs2JP;E6Cr2JD;IAAQ,0BAAA;I7Cw2JP;EACF;A6Cr2JD;EACE;IAAQ,6BAAA;I7Cw2JP;E6Cv2JD;IAAQ,0BAAA;I7C02JP;EACF;A6C72JD;EACE;IAAQ,6BAAA;I7Cw2JP;E6Cv2JD;IAAQ,0BAAA;I7C02JP;EACF;A6Cn2JD;EACE,kBAAA;EACA,cAAA;EACA,qBAAA;EACA,2BAAA;EACA,oBAAA;ExCsCA,wDAAA;EACQ,gDAAA;ELg0JT;A6Cl2JD;EACE,aAAA;EACA,WAAA;EACA,cAAA;EACA,iBAAA;EACA,mBAAA;EACA,gBAAA;EACA,oBAAA;EACA,2BAAA;ExCyBA,wDAAA;EACQ,gDAAA;EAyHR,qCAAA;EACK,gCAAA;EACG,6BAAA;ELotJT;A6C/1JD;;ECCI,+MAAA;EACA,0MAAA;EACA,uMAAA;EDAF,oCAAA;UAAA,4BAAA;E7Cm2JD;A6C51JD;;ExC5CE,4DAAA;EACK,uDAAA;EACG,oDAAA;EL44JT;A6Cz1JD;EErEE,2BAAA;E/Ci6JD;A+C95JC;EDgDE,+MAAA;EACA,0MAAA;EACA,uMAAA;E9Ci3JH;A6C71JD;EEzEE,2BAAA;E/Cy6JD;A+Ct6JC;EDgDE,+MAAA;EACA,0MAAA;EACA,uMAAA;E9Cy3JH;A6Cj2JD;EE7EE,2BAAA;E/Ci7JD;A+C96JC;EDgDE,+MAAA;EACA,0MAAA;EACA,uMAAA;E9Ci4JH;A6Cr2JD;EEjFE,2BAAA;E/Cy7JD;A+Ct7JC;EDgDE,+MAAA;EACA,0MAAA;EACA,uMAAA;E9Cy4JH;AgDj8JD;EAEE,kBAAA;EhDk8JD;AgDh8JC;EACE,eAAA;EhDk8JH;AgD97JD;;EAEE,SAAA;EACA,kBAAA;EhDg8JD;AgD77JD;EACE,gBAAA;EhD+7JD;AgD57JD;EACE,gBAAA;EhD87JD;AgD37JD;;EAEE,oBAAA;EhD67JD;AgD17JD;;EAEE,qBAAA;EhD47JD;AgDz7JD;;;EAGE,qBAAA;EACA,qBAAA;EhD27JD;AgDx7JD;EACE,wBAAA;EhD07JD;AgDv7JD;EACE,wBAAA;EhDy7JD;AgDr7JD;EACE,eAAA;EACA,oBAAA;EhDu7JD;AgDj7JD;EACE,iBAAA;EACA,kBAAA;EhDm7JD;AiDr+JD;EAEE,qBAAA;EACA,iBAAA;EjDs+JD;AiD99JD;EACE,oBAAA;EACA,gBAAA;EACA,oBAAA;EAEA,qBAAA;EACA,2BAAA;EACA,2BAAA;EjD+9JD;AiD59JC;ErB3BA,8BAAA;EACC,6BAAA;E5B0/JF;AiD79JC;EACE,kBAAA;ErBvBF,iCAAA;EACC,gCAAA;E5Bu/JF;AiDt9JD;EACE,gBAAA;EjDw9JD;AiDz9JD;EAII,gBAAA;EjDw9JH;AiDp9JC;;EAEE,uBAAA;EACA,gBAAA;EACA,2BAAA;EjDs9JH;AiDh9JC;;;EAGE,2BAAA;EACA,gBAAA;EACA,qBAAA;EjDk9JH;AiDv9JC;;;EASI,gBAAA;EjDm9JL;AiD59JC;;;EAYI,gBAAA;EjDq9JL;AiDh9JC;;;EAGE,YAAA;EACA,gBAAA;EACA,2BAAA;EACA,uBAAA;EjDk9JH;AiDx9JC;;;;;;;;;EAYI,gBAAA;EjDu9JL;AiDn+JC;;;EAeI,gBAAA;EjDy9JL;AkDrjKC;EACE,gBAAA;EACA,2BAAA;ElDujKH;AkDrjKG;EACE,gBAAA;ElDujKL;AkDxjKG;EAII,gBAAA;ElDujKP;AkDpjKK;;EAEE,gBAAA;EACA,2BAAA;ElDsjKP;AkDpjKK;;;EAGE,aAAA;EACA,2BAAA;EACA,uBAAA;ElDsjKP;AkD3kKC;EACE,gBAAA;EACA,2BAAA;ElD6kKH;AkD3kKG;EACE,gBAAA;ElD6kKL;AkD9kKG;EAII,gBAAA;ElD6kKP;AkD1kKK;;EAEE,gBAAA;EACA,2BAAA;ElD4kKP;AkD1kKK;;;EAGE,aAAA;EACA,2BAAA;EACA,uBAAA;ElD4kKP;AkDjmKC;EACE,gBAAA;EACA,2BAAA;ElDmmKH;AkDjmKG;EACE,gBAAA;ElDmmKL;AkDpmKG;EAII,gBAAA;ElDmmKP;AkDhmKK;;EAEE,gBAAA;EACA,2BAAA;ElDkmKP;AkDhmKK;;;EAGE,aAAA;EACA,2BAAA;EACA,uBAAA;ElDkmKP;AkDvnKC;EACE,gBAAA;EACA,2BAAA;ElDynKH;AkDvnKG;EACE,gBAAA;ElDynKL;AkD1nKG;EAII,gBAAA;ElDynKP;AkDtnKK;;EAEE,gBAAA;EACA,2BAAA;ElDwnKP;AkDtnKK;;;EAGE,aAAA;EACA,2BAAA;EACA,uBAAA;ElDwnKP;AiD5hKD;EACE,eAAA;EACA,oBAAA;EjD8hKD;AiD5hKD;EACE,kBAAA;EACA,kBAAA;EjD8hKD;AmDlpKD;EACE,qBAAA;EACA,2BAAA;EACA,+BAAA;EACA,oBAAA;E9C0DA,mDAAA;EACQ,2CAAA;EL2lKT;AmDjpKD;EACE,eAAA;EnDmpKD;AmD9oKD;EACE,oBAAA;EACA,sCAAA;EvBpBA,8BAAA;EACC,6BAAA;E5BqqKF;AmDppKD;EAMI,gBAAA;EnDipKH;AmD5oKD;EACE,eAAA;EACA,kBAAA;EACA,iBAAA;EACA,gBAAA;EnD8oKD;AmDlpKD;;;;;EAWI,gBAAA;EnD8oKH;AmDzoKD;EACE,oBAAA;EACA,2BAAA;EACA,+BAAA;EvBxCA,iCAAA;EACC,gCAAA;E5BorKF;AmDnoKD;;EAGI,kBAAA;EnDooKH;AmDvoKD;;EAMM,qBAAA;EACA,kBAAA;EnDqoKL;AmDjoKG;;EAEI,eAAA;EvBvEN,8BAAA;EACC,6BAAA;E5B2sKF;AmDhoKG;;EAEI,kBAAA;EvBtEN,iCAAA;EACC,gCAAA;E5BysKF;AmD7nKD;EAEI,qBAAA;EnD8nKH;AmD3nKD;EACE,qBAAA;EnD6nKD;AmDrnKD;;;EAII,kBAAA;EnDsnKH;AmD1nKD;;;EAOM,oBAAA;EACA,qBAAA;EnDwnKL;AmDhoKD;;EvBnGE,8BAAA;EACC,6BAAA;E5BuuKF;AmDroKD;;;;EAmBQ,6BAAA;EACA,8BAAA;EnDwnKP;AmD5oKD;;;;;;;;EAwBU,6BAAA;EnD8nKT;AmDtpKD;;;;;;;;EA4BU,8BAAA;EnDooKT;AmDhqKD;;EvB3FE,iCAAA;EACC,gCAAA;E5B+vKF;AmDrqKD;;;;EAyCQ,gCAAA;EACA,iCAAA;EnDkoKP;AmD5qKD;;;;;;;;EA8CU,gCAAA;EnDwoKT;AmDtrKD;;;;;;;;EAkDU,iCAAA;EnD8oKT;AmDhsKD;;;;EA2DI,+BAAA;EnD2oKH;AmDtsKD;;EA+DI,eAAA;EnD2oKH;AmD1sKD;;EAmEI,WAAA;EnD2oKH;AmD9sKD;;;;;;;;;;;;EA0EU,gBAAA;EnDkpKT;AmD5tKD;;;;;;;;;;;;EA8EU,iBAAA;EnD4pKT;AmD1uKD;;;;;;;;EAuFU,kBAAA;EnD6pKT;AmDpvKD;;;;;;;;EAgGU,kBAAA;EnD8pKT;AmD9vKD;EAsGI,WAAA;EACA,kBAAA;EnD2pKH;AmDjpKD;EACE,qBAAA;EnDmpKD;AmDppKD;EAKI,kBAAA;EACA,oBAAA;EnDkpKH;AmDxpKD;EASM,iBAAA;EnDkpKL;AmD3pKD;EAcI,kBAAA;EnDgpKH;AmD9pKD;;EAkBM,+BAAA;EnDgpKL;AmDlqKD;EAuBI,eAAA;EnD8oKH;AmDrqKD;EAyBM,kCAAA;EnD+oKL;AmDxoKD;ECpPE,uBAAA;EpD+3KD;AoD73KC;EACE,gBAAA;EACA,2BAAA;EACA,uBAAA;EpD+3KH;AoDl4KC;EAMI,2BAAA;EpD+3KL;AoDr4KC;EASI,gBAAA;EACA,2BAAA;EpD+3KL;AoD53KC;EAEI,8BAAA;EpD63KL;AmDvpKD;ECvPE,uBAAA;EpDi5KD;AoD/4KC;EACE,gBAAA;EACA,2BAAA;EACA,uBAAA;EpDi5KH;AoDp5KC;EAMI,2BAAA;EpDi5KL;AoDv5KC;EASI,gBAAA;EACA,2BAAA;EpDi5KL;AoD94KC;EAEI,8BAAA;EpD+4KL;AmDtqKD;EC1PE,uBAAA;EpDm6KD;AoDj6KC;EACE,gBAAA;EACA,2BAAA;EACA,uBAAA;EpDm6KH;AoDt6KC;EAMI,2BAAA;EpDm6KL;AoDz6KC;EASI,gBAAA;EACA,2BAAA;EpDm6KL;AoDh6KC;EAEI,8BAAA;EpDi6KL;AmDrrKD;EC7PE,uBAAA;EpDq7KD;AoDn7KC;EACE,gBAAA;EACA,2BAAA;EACA,uBAAA;EpDq7KH;AoDx7KC;EAMI,2BAAA;EpDq7KL;AoD37KC;EASI,gBAAA;EACA,2BAAA;EpDq7KL;AoDl7KC;EAEI,8BAAA;EpDm7KL;AmDpsKD;EChQE,uBAAA;EpDu8KD;AoDr8KC;EACE,gBAAA;EACA,2BAAA;EACA,uBAAA;EpDu8KH;AoD18KC;EAMI,2BAAA;EpDu8KL;AoD78KC;EASI,gBAAA;EACA,2BAAA;EpDu8KL;AoDp8KC;EAEI,8BAAA;EpDq8KL;AmDntKD;ECnQE,uBAAA;EpDy9KD;AoDv9KC;EACE,gBAAA;EACA,2BAAA;EACA,uBAAA;EpDy9KH;AoD59KC;EAMI,2BAAA;EpDy9KL;AoD/9KC;EASI,gBAAA;EACA,2BAAA;EpDy9KL;AoDt9KC;EAEI,8BAAA;EpDu9KL;AqDv+KD;EACE,oBAAA;EACA,gBAAA;EACA,WAAA;EACA,YAAA;EACA,kBAAA;ErDy+KD;AqD9+KD;;;;;EAYI,oBAAA;EACA,QAAA;EACA,SAAA;EACA,WAAA;EACA,cAAA;EACA,aAAA;EACA,WAAA;ErDy+KH;AqDp+KD;EACE,wBAAA;ErDs+KD;AqDl+KD;EACE,qBAAA;ErDo+KD;AsD//KD;EACE,kBAAA;EACA,eAAA;EACA,qBAAA;EACA,2BAAA;EACA,2BAAA;EACA,oBAAA;EjDwDA,yDAAA;EACQ,iDAAA;EL08KT;AsDzgLD;EASI,oBAAA;EACA,mCAAA;EtDmgLH;AsD9/KD;EACE,eAAA;EACA,oBAAA;EtDggLD;AsD9/KD;EACE,cAAA;EACA,oBAAA;EtDggLD;AuDthLD;EACE,cAAA;EACA,iBAAA;EACA,mBAAA;EACA,gBAAA;EACA,gBAAA;EACA,8BAAA;EjCRA,cAAA;EAGA,2BAAA;EtB+hLD;AuDvhLC;;EAEE,gBAAA;EACA,uBAAA;EACA,iBAAA;EjCfF,cAAA;EAGA,2BAAA;EtBuiLD;AuDnhLC;EACE,YAAA;EACA,iBAAA;EACA,yBAAA;EACA,WAAA;EACA,0BAAA;EvDqhLH;AwD1iLD;EACE,kBAAA;ExD4iLD;AwDxiLD;EACE,eAAA;EACA,kBAAA;EACA,iBAAA;EACA,QAAA;EACA,UAAA;EACA,WAAA;EACA,SAAA;EACA,eAAA;EACA,mCAAA;EAIA,YAAA;ExDuiLD;AwDpiLC;EnD+GA,uCAAA;EACI,mCAAA;EACC,kCAAA;EACG,+BAAA;EAkER,qDAAA;EAEK,2CAAA;EACG,qCAAA;ELu3KT;AwD1iLC;EnD2GA,oCAAA;EACI,gCAAA;EACC,+BAAA;EACG,4BAAA;ELk8KT;AwD9iLD;EACE,oBAAA;EACA,kBAAA;ExDgjLD;AwD5iLD;EACE,oBAAA;EACA,aAAA;EACA,cAAA;ExD8iLD;AwD1iLD;EACE,oBAAA;EACA,2BAAA;EACA,2BAAA;EACA,sCAAA;EACA,oBAAA;EnDaA,kDAAA;EACQ,0CAAA;EmDZR,sCAAA;UAAA,8BAAA;EAEA,YAAA;ExD4iLD;AwDxiLD;EACE,iBAAA;EACA,QAAA;EACA,UAAA;EACA,WAAA;EACA,SAAA;EACA,eAAA;EACA,2BAAA;ExD0iLD;AwDxiLC;ElCrEA,YAAA;EAGA,0BAAA;EtB8mLD;AwD3iLC;ElCtEA,cAAA;EAGA,2BAAA;EtBknLD;AwD1iLD;EACE,eAAA;EACA,kCAAA;EACA,2BAAA;ExD4iLD;AwDziLD;EACE,kBAAA;ExD2iLD;AwDviLD;EACE,WAAA;EACA,yBAAA;ExDyiLD;AwDpiLD;EACE,oBAAA;EACA,eAAA;ExDsiLD;AwDliLD;EACE,eAAA;EACA,mBAAA;EACA,+BAAA;ExDoiLD;AwDviLD;EAQI,kBAAA;EACA,kBAAA;ExDkiLH;AwD3iLD;EAaI,mBAAA;ExDiiLH;AwD9iLD;EAiBI,gBAAA;ExDgiLH;AwD3hLD;EACE,oBAAA;EACA,cAAA;EACA,aAAA;EACA,cAAA;EACA,kBAAA;ExD6hLD;AwD3gLD;EAZE;IACE,cAAA;IACA,mBAAA;IxD0hLD;EwDxhLD;InDvEA,mDAAA;IACQ,2CAAA;ILkmLP;EwDvhLD;IAAY,cAAA;IxD0hLX;EACF;AwDrhLD;EAFE;IAAY,cAAA;IxD2hLX;EACF;AyD1qLD;EACE,oBAAA;EACA,eAAA;EACA,gBAAA;EAEA,6DAAA;EACA,iBAAA;EACA,qBAAA;EACA,kBAAA;EnCXA,YAAA;EAGA,0BAAA;EtBqrLD;AyD1qLC;EnCdA,cAAA;EAGA,2BAAA;EtByrLD;AyD7qLC;EAAW,kBAAA;EAAmB,gBAAA;EzDirL/B;AyDhrLC;EAAW,kBAAA;EAAmB,gBAAA;EzDorL/B;AyDnrLC;EAAW,iBAAA;EAAmB,gBAAA;EzDurL/B;AyDtrLC;EAAW,mBAAA;EAAmB,gBAAA;EzD0rL/B;AyDtrLD;EACE,kBAAA;EACA,kBAAA;EACA,gBAAA;EACA,oBAAA;EACA,uBAAA;EACA,2BAAA;EACA,oBAAA;EzDwrLD;AyDprLD;EACE,oBAAA;EACA,UAAA;EACA,WAAA;EACA,2BAAA;EACA,qBAAA;EzDsrLD;AyDlrLC;EACE,WAAA;EACA,WAAA;EACA,mBAAA;EACA,yBAAA;EACA,2BAAA;EzDorLH;AyDlrLC;EACE,WAAA;EACA,YAAA;EACA,qBAAA;EACA,yBAAA;EACA,2BAAA;EzDorLH;AyDlrLC;EACE,WAAA;EACA,WAAA;EACA,qBAAA;EACA,yBAAA;EACA,2BAAA;EzDorLH;AyDlrLC;EACE,UAAA;EACA,SAAA;EACA,kBAAA;EACA,6BAAA;EACA,6BAAA;EzDorLH;AyDlrLC;EACE,UAAA;EACA,UAAA;EACA,kBAAA;EACA,6BAAA;EACA,4BAAA;EzDorLH;AyDlrLC;EACE,QAAA;EACA,WAAA;EACA,mBAAA;EACA,yBAAA;EACA,8BAAA;EzDorLH;AyDlrLC;EACE,QAAA;EACA,YAAA;EACA,kBAAA;EACA,yBAAA;EACA,8BAAA;EzDorLH;AyDlrLC;EACE,QAAA;EACA,WAAA;EACA,kBAAA;EACA,yBAAA;EACA,8BAAA;EzDorLH;A0DlxLD;EACE,oBAAA;EACA,QAAA;EACA,SAAA;EACA,eAAA;EACA,eAAA;EACA,kBAAA;EACA,cAAA;EAEA,6DAAA;EACA,iBAAA;EACA,qBAAA;EACA,yBAAA;EACA,kBAAA;EACA,2BAAA;EACA,sCAAA;UAAA,8BAAA;EACA,2BAAA;EACA,sCAAA;EACA,oBAAA;ErD6CA,mDAAA;EACQ,2CAAA;EqD1CR,qBAAA;E1DkxLD;A0D/wLC;EAAY,mBAAA;E1DkxLb;A0DjxLC;EAAY,mBAAA;E1DoxLb;A0DnxLC;EAAY,kBAAA;E1DsxLb;A0DrxLC;EAAY,oBAAA;E1DwxLb;A0DrxLD;EACE,WAAA;EACA,mBAAA;EACA,iBAAA;EACA,2BAAA;EACA,kCAAA;EACA,4BAAA;E1DuxLD;A0DpxLD;EACE,mBAAA;E1DsxLD;A0D9wLC;;EAEE,oBAAA;EACA,gBAAA;EACA,UAAA;EACA,WAAA;EACA,2BAAA;EACA,qBAAA;E1DgxLH;A0D7wLD;EACE,oBAAA;E1D+wLD;A0D7wLD;EACE,oBAAA;EACA,aAAA;E1D+wLD;A0D3wLC;EACE,WAAA;EACA,oBAAA;EACA,wBAAA;EACA,2BAAA;EACA,uCAAA;EACA,eAAA;E1D6wLH;A0D5wLG;EACE,cAAA;EACA,aAAA;EACA,oBAAA;EACA,wBAAA;EACA,2BAAA;E1D8wLL;A0D3wLC;EACE,UAAA;EACA,aAAA;EACA,mBAAA;EACA,sBAAA;EACA,6BAAA;EACA,yCAAA;E1D6wLH;A0D5wLG;EACE,cAAA;EACA,WAAA;EACA,eAAA;EACA,sBAAA;EACA,6BAAA;E1D8wLL;A0D3wLC;EACE,WAAA;EACA,oBAAA;EACA,qBAAA;EACA,8BAAA;EACA,0CAAA;EACA,YAAA;E1D6wLH;A0D5wLG;EACE,cAAA;EACA,UAAA;EACA,oBAAA;EACA,qBAAA;EACA,8BAAA;E1D8wLL;A0D1wLC;EACE,UAAA;EACA,cAAA;EACA,mBAAA;EACA,uBAAA;EACA,4BAAA;EACA,wCAAA;E1D4wLH;A0D3wLG;EACE,cAAA;EACA,YAAA;EACA,uBAAA;EACA,4BAAA;EACA,eAAA;E1D6wLL;A2D14LD;EACE,oBAAA;E3D44LD;A2Dz4LD;EACE,oBAAA;EACA,kBAAA;EACA,aAAA;E3D24LD;A2D94LD;EAMI,eAAA;EACA,oBAAA;EtD6KF,2CAAA;EACK,sCAAA;EACG,mCAAA;EL+tLT;A2Dr5LD;;EAcM,gBAAA;E3D24LL;A2Dj3LC;EAAA;ItDiKA,wDAAA;IAEK,8CAAA;IACG,wCAAA;IA7JR,qCAAA;IAEQ,6BAAA;IA+GR,2BAAA;IAEQ,mBAAA;ILowLP;E2D/4LG;;ItDmHJ,4CAAA;IACQ,oCAAA;IsDjHF,SAAA;I3Dk5LL;E2Dh5LG;;ItD8GJ,6CAAA;IACQ,qCAAA;IsD5GF,SAAA;I3Dm5LL;E2Dj5LG;;;ItDyGJ,yCAAA;IACQ,iCAAA;IsDtGF,SAAA;I3Do5LL;EACF;A2D17LD;;;EA6CI,gBAAA;E3Dk5LH;A2D/7LD;EAiDI,SAAA;E3Di5LH;A2Dl8LD;;EAsDI,oBAAA;EACA,QAAA;EACA,aAAA;E3Dg5LH;A2Dx8LD;EA4DI,YAAA;E3D+4LH;A2D38LD;EA+DI,aAAA;E3D+4LH;A2D98LD;;EAmEI,SAAA;E3D+4LH;A2Dl9LD;EAuEI,aAAA;E3D84LH;A2Dr9LD;EA0EI,YAAA;E3D84LH;A2Dt4LD;EACE,oBAAA;EACA,QAAA;EACA,SAAA;EACA,WAAA;EACA,YAAA;ErC9FA,cAAA;EAGA,2BAAA;EqC6FA,iBAAA;EACA,gBAAA;EACA,oBAAA;EACA,2CAAA;E3Dy4LD;A2Dp4LC;EblGE,oGAAA;EACA,+FAAA;EACA,sHAAA;EAAA,gGAAA;EACA,6BAAA;EACA,wHAAA;E9Cy+LH;A2Dx4LC;EACE,YAAA;EACA,UAAA;EbvGA,oGAAA;EACA,+FAAA;EACA,sHAAA;EAAA,gGAAA;EACA,6BAAA;EACA,wHAAA;E9Ck/LH;A2D14LC;;EAEE,YAAA;EACA,gBAAA;EACA,uBAAA;ErCtHF,cAAA;EAGA,2BAAA;EtBigMD;A2D36LD;;;;EAsCI,oBAAA;EACA,UAAA;EACA,YAAA;EACA,uBAAA;E3D24LH;A2Dp7LD;;EA6CI,WAAA;EACA,oBAAA;E3D24LH;A2Dz7LD;;EAkDI,YAAA;EACA,qBAAA;E3D24LH;A2D97LD;;EAuDI,aAAA;EACA,cAAA;EACA,mBAAA;EACA,gBAAA;EACA,oBAAA;E3D24LH;A2Dt4LG;EACE,kBAAA;E3Dw4LL;A2Dp4LG;EACE,kBAAA;E3Ds4LL;A2D53LD;EACE,oBAAA;EACA,cAAA;EACA,WAAA;EACA,aAAA;EACA,YAAA;EACA,mBAAA;EACA,iBAAA;EACA,kBAAA;EACA,oBAAA;E3D83LD;A2Dv4LD;EAYI,uBAAA;EACA,aAAA;EACA,cAAA;EACA,aAAA;EACA,qBAAA;EACA,2BAAA;EACA,qBAAA;EACA,iBAAA;EAWA,2BAAA;EACA,oCAAA;E3Do3LH;A2Dn5LD;EAkCI,WAAA;EACA,aAAA;EACA,cAAA;EACA,2BAAA;E3Do3LH;A2D72LD;EACE,oBAAA;EACA,WAAA;EACA,YAAA;EACA,cAAA;EACA,aAAA;EACA,mBAAA;EACA,sBAAA;EACA,gBAAA;EACA,oBAAA;EACA,2CAAA;E3D+2LD;A2D92LC;EACE,mBAAA;E3Dg3LH;A2Dv0LD;EAhCE;;;;IAKI,aAAA;IACA,cAAA;IACA,mBAAA;IACA,iBAAA;I3Dy2LH;E2Dj3LD;;IAYI,oBAAA;I3Dy2LH;E2Dr3LD;;IAgBI,qBAAA;I3Dy2LH;E2Dp2LD;IACE,WAAA;IACA,YAAA;IACA,sBAAA;I3Ds2LD;E2Dl2LD;IACE,cAAA;I3Do2LD;EACF;A4DlmMC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAEE,cAAA;EACA,gBAAA;E5DgoMH;A4D9nMC;;;;;;;;;;;;;;;EACE,aAAA;E5D8oMH;AiCtpMD;E4BRE,gBAAA;EACA,mBAAA;EACA,oBAAA;E7DiqMD;AiCxpMD;EACE,yBAAA;EjC0pMD;AiCxpMD;EACE,wBAAA;EjC0pMD;AiClpMD;EACE,0BAAA;EjCopMD;AiClpMD;EACE,2BAAA;EjCopMD;AiClpMD;EACE,oBAAA;EjCopMD;AiClpMD;E6BzBE,aAAA;EACA,oBAAA;EACA,mBAAA;EACA,+BAAA;EACA,WAAA;E9D8qMD;AiChpMD;EACE,0BAAA;EjCkpMD;AiC3oMD;EACE,iBAAA;EjC6oMD;A+D9qMD;EACE,qBAAA;E/DgrMD;A+D1qMD;;;;ECdE,0BAAA;EhE8rMD;A+DzqMD;;;;;;;;;;;;EAYE,0BAAA;E/D2qMD;A+DpqMD;EAAA;IChDE,2BAAA;IhEwtMC;EgEvtMD;IAAU,gBAAA;IhE0tMT;EgEztMD;IAAU,+BAAA;IhE4tMT;EgE3tMD;;IACU,gCAAA;IhE8tMT;EACF;A+D9qMD;EAAA;IAFI,2BAAA;I/DorMD;EACF;A+D9qMD;EAAA;IAFI,4BAAA;I/DorMD;EACF;A+D9qMD;EAAA;IAFI,kCAAA;I/DorMD;EACF;A+D7qMD;EAAA;ICrEE,2BAAA;IhEsvMC;EgErvMD;IAAU,gBAAA;IhEwvMT;EgEvvMD;IAAU,+BAAA;IhE0vMT;EgEzvMD;;IACU,gCAAA;IhE4vMT;EACF;A+DvrMD;EAAA;IAFI,2BAAA;I/D6rMD;EACF;A+DvrMD;EAAA;IAFI,4BAAA;I/D6rMD;EACF;A+DvrMD;EAAA;IAFI,kCAAA;I/D6rMD;EACF;A+DtrMD;EAAA;IC1FE,2BAAA;IhEoxMC;EgEnxMD;IAAU,gBAAA;IhEsxMT;EgErxMD;IAAU,+BAAA;IhEwxMT;EgEvxMD;;IACU,gCAAA;IhE0xMT;EACF;A+DhsMD;EAAA;IAFI,2BAAA;I/DssMD;EACF;A+DhsMD;EAAA;IAFI,4BAAA;I/DssMD;EACF;A+DhsMD;EAAA;IAFI,kCAAA;I/DssMD;EACF;A+D/rMD;EAAA;IC/GE,2BAAA;IhEkzMC;EgEjzMD;IAAU,gBAAA;IhEozMT;EgEnzMD;IAAU,+BAAA;IhEszMT;EgErzMD;;IACU,gCAAA;IhEwzMT;EACF;A+DzsMD;EAAA;IAFI,2BAAA;I/D+sMD;EACF;A+DzsMD;EAAA;IAFI,4BAAA;I/D+sMD;EACF;A+DzsMD;EAAA;IAFI,kCAAA;I/D+sMD;EACF;A+DxsMD;EAAA;IC5HE,0BAAA;IhEw0MC;EACF;A+DxsMD;EAAA;ICjIE,0BAAA;IhE60MC;EACF;A+DxsMD;EAAA;ICtIE,0BAAA;IhEk1MC;EACF;A+DxsMD;EAAA;IC3IE,0BAAA;IhEu1MC;EACF;A+DrsMD;ECnJE,0BAAA;EhE21MD;A+DlsMD;EAAA;ICjKE,2BAAA;IhEu2MC;EgEt2MD;IAAU,gBAAA;IhEy2MT;EgEx2MD;IAAU,+BAAA;IhE22MT;EgE12MD;;IACU,gCAAA;IhE62MT;EACF;A+DhtMD;EACE,0BAAA;E/DktMD;A+D7sMD;EAAA;IAFI,2BAAA;I/DmtMD;EACF;A+DjtMD;EACE,0BAAA;E/DmtMD;A+D9sMD;EAAA;IAFI,4BAAA;I/DotMD;EACF;A+DltMD;EACE,0BAAA;E/DotMD;A+D/sMD;EAAA;IAFI,kCAAA;I/DqtMD;EACF;A+D9sMD;EAAA;ICpLE,0BAAA;IhEs4MC;EACF","file":"bootstrap.css","sourcesContent":["/*! normalize.css v3.0.2 | MIT License | git.io/normalize */\nhtml {\n font-family: sans-serif;\n -ms-text-size-adjust: 100%;\n -webkit-text-size-adjust: 100%;\n}\nbody {\n margin: 0;\n}\narticle,\naside,\ndetails,\nfigcaption,\nfigure,\nfooter,\nheader,\nhgroup,\nmain,\nmenu,\nnav,\nsection,\nsummary {\n display: block;\n}\naudio,\ncanvas,\nprogress,\nvideo {\n display: inline-block;\n vertical-align: baseline;\n}\naudio:not([controls]) {\n display: none;\n height: 0;\n}\n[hidden],\ntemplate {\n display: none;\n}\na {\n background-color: transparent;\n}\na:active,\na:hover {\n outline: 0;\n}\nabbr[title] {\n border-bottom: 1px dotted;\n}\nb,\nstrong {\n font-weight: bold;\n}\ndfn {\n font-style: italic;\n}\nh1 {\n font-size: 2em;\n margin: 0.67em 0;\n}\nmark {\n background: #ff0;\n color: #000;\n}\nsmall {\n font-size: 80%;\n}\nsub,\nsup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n}\nsup {\n top: -0.5em;\n}\nsub {\n bottom: -0.25em;\n}\nimg {\n border: 0;\n}\nsvg:not(:root) {\n overflow: hidden;\n}\nfigure {\n margin: 1em 40px;\n}\nhr {\n -moz-box-sizing: content-box;\n box-sizing: content-box;\n height: 0;\n}\npre {\n overflow: auto;\n}\ncode,\nkbd,\npre,\nsamp {\n font-family: monospace, monospace;\n font-size: 1em;\n}\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n color: inherit;\n font: inherit;\n margin: 0;\n}\nbutton {\n overflow: visible;\n}\nbutton,\nselect {\n text-transform: none;\n}\nbutton,\nhtml input[type=\"button\"],\ninput[type=\"reset\"],\ninput[type=\"submit\"] {\n -webkit-appearance: button;\n cursor: pointer;\n}\nbutton[disabled],\nhtml input[disabled] {\n cursor: default;\n}\nbutton::-moz-focus-inner,\ninput::-moz-focus-inner {\n border: 0;\n padding: 0;\n}\ninput {\n line-height: normal;\n}\ninput[type=\"checkbox\"],\ninput[type=\"radio\"] {\n box-sizing: border-box;\n padding: 0;\n}\ninput[type=\"number\"]::-webkit-inner-spin-button,\ninput[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\ninput[type=\"search\"] {\n -webkit-appearance: textfield;\n -moz-box-sizing: content-box;\n -webkit-box-sizing: content-box;\n box-sizing: content-box;\n}\ninput[type=\"search\"]::-webkit-search-cancel-button,\ninput[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\nfieldset {\n border: 1px solid #c0c0c0;\n margin: 0 2px;\n padding: 0.35em 0.625em 0.75em;\n}\nlegend {\n border: 0;\n padding: 0;\n}\ntextarea {\n overflow: auto;\n}\noptgroup {\n font-weight: bold;\n}\ntable {\n border-collapse: collapse;\n border-spacing: 0;\n}\ntd,\nth {\n padding: 0;\n}\n/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */\n@media print {\n *,\n *:before,\n *:after {\n background: transparent !important;\n color: #000 !important;\n box-shadow: none !important;\n text-shadow: none !important;\n }\n a,\n a:visited {\n text-decoration: underline;\n }\n a[href]:after {\n content: \" (\" attr(href) \")\";\n }\n abbr[title]:after {\n content: \" (\" attr(title) \")\";\n }\n a[href^=\"#\"]:after,\n a[href^=\"javascript:\"]:after {\n content: \"\";\n }\n pre,\n blockquote {\n border: 1px solid #999;\n page-break-inside: avoid;\n }\n thead {\n display: table-header-group;\n }\n tr,\n img {\n page-break-inside: avoid;\n }\n img {\n max-width: 100% !important;\n }\n p,\n h2,\n h3 {\n orphans: 3;\n widows: 3;\n }\n h2,\n h3 {\n page-break-after: avoid;\n }\n select {\n background: #fff !important;\n }\n .navbar {\n display: none;\n }\n .btn > .caret,\n .dropup > .btn > .caret {\n border-top-color: #000 !important;\n }\n .label {\n border: 1px solid #000;\n }\n .table {\n border-collapse: collapse !important;\n }\n .table td,\n .table th {\n background-color: #fff !important;\n }\n .table-bordered th,\n .table-bordered td {\n border: 1px solid #ddd !important;\n }\n}\n@font-face {\n font-family: 'Glyphicons Halflings';\n src: url('../fonts/glyphicons-halflings-regular.eot');\n src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../fonts/glyphicons-halflings-regular.woff2') format('woff2'), url('../fonts/glyphicons-halflings-regular.woff') format('woff'), url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg');\n}\n.glyphicon {\n position: relative;\n top: 1px;\n display: inline-block;\n font-family: 'Glyphicons Halflings';\n font-style: normal;\n font-weight: normal;\n line-height: 1;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n.glyphicon-asterisk:before {\n content: \"\\2a\";\n}\n.glyphicon-plus:before {\n content: \"\\2b\";\n}\n.glyphicon-euro:before,\n.glyphicon-eur:before {\n content: \"\\20ac\";\n}\n.glyphicon-minus:before {\n content: \"\\2212\";\n}\n.glyphicon-cloud:before {\n content: \"\\2601\";\n}\n.glyphicon-envelope:before {\n content: \"\\2709\";\n}\n.glyphicon-pencil:before {\n content: \"\\270f\";\n}\n.glyphicon-glass:before {\n content: \"\\e001\";\n}\n.glyphicon-music:before {\n content: \"\\e002\";\n}\n.glyphicon-search:before {\n content: \"\\e003\";\n}\n.glyphicon-heart:before {\n content: \"\\e005\";\n}\n.glyphicon-star:before {\n content: \"\\e006\";\n}\n.glyphicon-star-empty:before {\n content: \"\\e007\";\n}\n.glyphicon-user:before {\n content: \"\\e008\";\n}\n.glyphicon-film:before {\n content: \"\\e009\";\n}\n.glyphicon-th-large:before {\n content: \"\\e010\";\n}\n.glyphicon-th:before {\n content: \"\\e011\";\n}\n.glyphicon-th-list:before {\n content: \"\\e012\";\n}\n.glyphicon-ok:before {\n content: \"\\e013\";\n}\n.glyphicon-remove:before {\n content: \"\\e014\";\n}\n.glyphicon-zoom-in:before {\n content: \"\\e015\";\n}\n.glyphicon-zoom-out:before {\n content: \"\\e016\";\n}\n.glyphicon-off:before {\n content: \"\\e017\";\n}\n.glyphicon-signal:before {\n content: \"\\e018\";\n}\n.glyphicon-cog:before {\n content: \"\\e019\";\n}\n.glyphicon-trash:before {\n content: \"\\e020\";\n}\n.glyphicon-home:before {\n content: \"\\e021\";\n}\n.glyphicon-file:before {\n content: \"\\e022\";\n}\n.glyphicon-time:before {\n content: \"\\e023\";\n}\n.glyphicon-road:before {\n content: \"\\e024\";\n}\n.glyphicon-download-alt:before {\n content: \"\\e025\";\n}\n.glyphicon-download:before {\n content: \"\\e026\";\n}\n.glyphicon-upload:before {\n content: \"\\e027\";\n}\n.glyphicon-inbox:before {\n content: \"\\e028\";\n}\n.glyphicon-play-circle:before {\n content: \"\\e029\";\n}\n.glyphicon-repeat:before {\n content: \"\\e030\";\n}\n.glyphicon-refresh:before {\n content: \"\\e031\";\n}\n.glyphicon-list-alt:before {\n content: \"\\e032\";\n}\n.glyphicon-lock:before {\n content: \"\\e033\";\n}\n.glyphicon-flag:before {\n content: \"\\e034\";\n}\n.glyphicon-headphones:before {\n content: \"\\e035\";\n}\n.glyphicon-volume-off:before {\n content: \"\\e036\";\n}\n.glyphicon-volume-down:before {\n content: \"\\e037\";\n}\n.glyphicon-volume-up:before {\n content: \"\\e038\";\n}\n.glyphicon-qrcode:before {\n content: \"\\e039\";\n}\n.glyphicon-barcode:before {\n content: \"\\e040\";\n}\n.glyphicon-tag:before {\n content: \"\\e041\";\n}\n.glyphicon-tags:before {\n content: \"\\e042\";\n}\n.glyphicon-book:before {\n content: \"\\e043\";\n}\n.glyphicon-bookmark:before {\n content: \"\\e044\";\n}\n.glyphicon-print:before {\n content: \"\\e045\";\n}\n.glyphicon-camera:before {\n content: \"\\e046\";\n}\n.glyphicon-font:before {\n content: \"\\e047\";\n}\n.glyphicon-bold:before {\n content: \"\\e048\";\n}\n.glyphicon-italic:before {\n content: \"\\e049\";\n}\n.glyphicon-text-height:before {\n content: \"\\e050\";\n}\n.glyphicon-text-width:before {\n content: \"\\e051\";\n}\n.glyphicon-align-left:before {\n content: \"\\e052\";\n}\n.glyphicon-align-center:before {\n content: \"\\e053\";\n}\n.glyphicon-align-right:before {\n content: \"\\e054\";\n}\n.glyphicon-align-justify:before {\n content: \"\\e055\";\n}\n.glyphicon-list:before {\n content: \"\\e056\";\n}\n.glyphicon-indent-left:before {\n content: \"\\e057\";\n}\n.glyphicon-indent-right:before {\n content: \"\\e058\";\n}\n.glyphicon-facetime-video:before {\n content: \"\\e059\";\n}\n.glyphicon-picture:before {\n content: \"\\e060\";\n}\n.glyphicon-map-marker:before {\n content: \"\\e062\";\n}\n.glyphicon-adjust:before {\n content: \"\\e063\";\n}\n.glyphicon-tint:before {\n content: \"\\e064\";\n}\n.glyphicon-edit:before {\n content: \"\\e065\";\n}\n.glyphicon-share:before {\n content: \"\\e066\";\n}\n.glyphicon-check:before {\n content: \"\\e067\";\n}\n.glyphicon-move:before {\n content: \"\\e068\";\n}\n.glyphicon-step-backward:before {\n content: \"\\e069\";\n}\n.glyphicon-fast-backward:before {\n content: \"\\e070\";\n}\n.glyphicon-backward:before {\n content: \"\\e071\";\n}\n.glyphicon-play:before {\n content: \"\\e072\";\n}\n.glyphicon-pause:before {\n content: \"\\e073\";\n}\n.glyphicon-stop:before {\n content: \"\\e074\";\n}\n.glyphicon-forward:before {\n content: \"\\e075\";\n}\n.glyphicon-fast-forward:before {\n content: \"\\e076\";\n}\n.glyphicon-step-forward:before {\n content: \"\\e077\";\n}\n.glyphicon-eject:before {\n content: \"\\e078\";\n}\n.glyphicon-chevron-left:before {\n content: \"\\e079\";\n}\n.glyphicon-chevron-right:before {\n content: \"\\e080\";\n}\n.glyphicon-plus-sign:before {\n content: \"\\e081\";\n}\n.glyphicon-minus-sign:before {\n content: \"\\e082\";\n}\n.glyphicon-remove-sign:before {\n content: \"\\e083\";\n}\n.glyphicon-ok-sign:before {\n content: \"\\e084\";\n}\n.glyphicon-question-sign:before {\n content: \"\\e085\";\n}\n.glyphicon-info-sign:before {\n content: \"\\e086\";\n}\n.glyphicon-screenshot:before {\n content: \"\\e087\";\n}\n.glyphicon-remove-circle:before {\n content: \"\\e088\";\n}\n.glyphicon-ok-circle:before {\n content: \"\\e089\";\n}\n.glyphicon-ban-circle:before {\n content: \"\\e090\";\n}\n.glyphicon-arrow-left:before {\n content: \"\\e091\";\n}\n.glyphicon-arrow-right:before {\n content: \"\\e092\";\n}\n.glyphicon-arrow-up:before {\n content: \"\\e093\";\n}\n.glyphicon-arrow-down:before {\n content: \"\\e094\";\n}\n.glyphicon-share-alt:before {\n content: \"\\e095\";\n}\n.glyphicon-resize-full:before {\n content: \"\\e096\";\n}\n.glyphicon-resize-small:before {\n content: \"\\e097\";\n}\n.glyphicon-exclamation-sign:before {\n content: \"\\e101\";\n}\n.glyphicon-gift:before {\n content: \"\\e102\";\n}\n.glyphicon-leaf:before {\n content: \"\\e103\";\n}\n.glyphicon-fire:before {\n content: \"\\e104\";\n}\n.glyphicon-eye-open:before {\n content: \"\\e105\";\n}\n.glyphicon-eye-close:before {\n content: \"\\e106\";\n}\n.glyphicon-warning-sign:before {\n content: \"\\e107\";\n}\n.glyphicon-plane:before {\n content: \"\\e108\";\n}\n.glyphicon-calendar:before {\n content: \"\\e109\";\n}\n.glyphicon-random:before {\n content: \"\\e110\";\n}\n.glyphicon-comment:before {\n content: \"\\e111\";\n}\n.glyphicon-magnet:before {\n content: \"\\e112\";\n}\n.glyphicon-chevron-up:before {\n content: \"\\e113\";\n}\n.glyphicon-chevron-down:before {\n content: \"\\e114\";\n}\n.glyphicon-retweet:before {\n content: \"\\e115\";\n}\n.glyphicon-shopping-cart:before {\n content: \"\\e116\";\n}\n.glyphicon-folder-close:before {\n content: \"\\e117\";\n}\n.glyphicon-folder-open:before {\n content: \"\\e118\";\n}\n.glyphicon-resize-vertical:before {\n content: \"\\e119\";\n}\n.glyphicon-resize-horizontal:before {\n content: \"\\e120\";\n}\n.glyphicon-hdd:before {\n content: \"\\e121\";\n}\n.glyphicon-bullhorn:before {\n content: \"\\e122\";\n}\n.glyphicon-bell:before {\n content: \"\\e123\";\n}\n.glyphicon-certificate:before {\n content: \"\\e124\";\n}\n.glyphicon-thumbs-up:before {\n content: \"\\e125\";\n}\n.glyphicon-thumbs-down:before {\n content: \"\\e126\";\n}\n.glyphicon-hand-right:before {\n content: \"\\e127\";\n}\n.glyphicon-hand-left:before {\n content: \"\\e128\";\n}\n.glyphicon-hand-up:before {\n content: \"\\e129\";\n}\n.glyphicon-hand-down:before {\n content: \"\\e130\";\n}\n.glyphicon-circle-arrow-right:before {\n content: \"\\e131\";\n}\n.glyphicon-circle-arrow-left:before {\n content: \"\\e132\";\n}\n.glyphicon-circle-arrow-up:before {\n content: \"\\e133\";\n}\n.glyphicon-circle-arrow-down:before {\n content: \"\\e134\";\n}\n.glyphicon-globe:before {\n content: \"\\e135\";\n}\n.glyphicon-wrench:before {\n content: \"\\e136\";\n}\n.glyphicon-tasks:before {\n content: \"\\e137\";\n}\n.glyphicon-filter:before {\n content: \"\\e138\";\n}\n.glyphicon-briefcase:before {\n content: \"\\e139\";\n}\n.glyphicon-fullscreen:before {\n content: \"\\e140\";\n}\n.glyphicon-dashboard:before {\n content: \"\\e141\";\n}\n.glyphicon-paperclip:before {\n content: \"\\e142\";\n}\n.glyphicon-heart-empty:before {\n content: \"\\e143\";\n}\n.glyphicon-link:before {\n content: \"\\e144\";\n}\n.glyphicon-phone:before {\n content: \"\\e145\";\n}\n.glyphicon-pushpin:before {\n content: \"\\e146\";\n}\n.glyphicon-usd:before {\n content: \"\\e148\";\n}\n.glyphicon-gbp:before {\n content: \"\\e149\";\n}\n.glyphicon-sort:before {\n content: \"\\e150\";\n}\n.glyphicon-sort-by-alphabet:before {\n content: \"\\e151\";\n}\n.glyphicon-sort-by-alphabet-alt:before {\n content: \"\\e152\";\n}\n.glyphicon-sort-by-order:before {\n content: \"\\e153\";\n}\n.glyphicon-sort-by-order-alt:before {\n content: \"\\e154\";\n}\n.glyphicon-sort-by-attributes:before {\n content: \"\\e155\";\n}\n.glyphicon-sort-by-attributes-alt:before {\n content: \"\\e156\";\n}\n.glyphicon-unchecked:before {\n content: \"\\e157\";\n}\n.glyphicon-expand:before {\n content: \"\\e158\";\n}\n.glyphicon-collapse-down:before {\n content: \"\\e159\";\n}\n.glyphicon-collapse-up:before {\n content: \"\\e160\";\n}\n.glyphicon-log-in:before {\n content: \"\\e161\";\n}\n.glyphicon-flash:before {\n content: \"\\e162\";\n}\n.glyphicon-log-out:before {\n content: \"\\e163\";\n}\n.glyphicon-new-window:before {\n content: \"\\e164\";\n}\n.glyphicon-record:before {\n content: \"\\e165\";\n}\n.glyphicon-save:before {\n content: \"\\e166\";\n}\n.glyphicon-open:before {\n content: \"\\e167\";\n}\n.glyphicon-saved:before {\n content: \"\\e168\";\n}\n.glyphicon-import:before {\n content: \"\\e169\";\n}\n.glyphicon-export:before {\n content: \"\\e170\";\n}\n.glyphicon-send:before {\n content: \"\\e171\";\n}\n.glyphicon-floppy-disk:before {\n content: \"\\e172\";\n}\n.glyphicon-floppy-saved:before {\n content: \"\\e173\";\n}\n.glyphicon-floppy-remove:before {\n content: \"\\e174\";\n}\n.glyphicon-floppy-save:before {\n content: \"\\e175\";\n}\n.glyphicon-floppy-open:before {\n content: \"\\e176\";\n}\n.glyphicon-credit-card:before {\n content: \"\\e177\";\n}\n.glyphicon-transfer:before {\n content: \"\\e178\";\n}\n.glyphicon-cutlery:before {\n content: \"\\e179\";\n}\n.glyphicon-header:before {\n content: \"\\e180\";\n}\n.glyphicon-compressed:before {\n content: \"\\e181\";\n}\n.glyphicon-earphone:before {\n content: \"\\e182\";\n}\n.glyphicon-phone-alt:before {\n content: \"\\e183\";\n}\n.glyphicon-tower:before {\n content: \"\\e184\";\n}\n.glyphicon-stats:before {\n content: \"\\e185\";\n}\n.glyphicon-sd-video:before {\n content: \"\\e186\";\n}\n.glyphicon-hd-video:before {\n content: \"\\e187\";\n}\n.glyphicon-subtitles:before {\n content: \"\\e188\";\n}\n.glyphicon-sound-stereo:before {\n content: \"\\e189\";\n}\n.glyphicon-sound-dolby:before {\n content: \"\\e190\";\n}\n.glyphicon-sound-5-1:before {\n content: \"\\e191\";\n}\n.glyphicon-sound-6-1:before {\n content: \"\\e192\";\n}\n.glyphicon-sound-7-1:before {\n content: \"\\e193\";\n}\n.glyphicon-copyright-mark:before {\n content: \"\\e194\";\n}\n.glyphicon-registration-mark:before {\n content: \"\\e195\";\n}\n.glyphicon-cloud-download:before {\n content: \"\\e197\";\n}\n.glyphicon-cloud-upload:before {\n content: \"\\e198\";\n}\n.glyphicon-tree-conifer:before {\n content: \"\\e199\";\n}\n.glyphicon-tree-deciduous:before {\n content: \"\\e200\";\n}\n.glyphicon-cd:before {\n content: \"\\e201\";\n}\n.glyphicon-save-file:before {\n content: \"\\e202\";\n}\n.glyphicon-open-file:before {\n content: \"\\e203\";\n}\n.glyphicon-level-up:before {\n content: \"\\e204\";\n}\n.glyphicon-copy:before {\n content: \"\\e205\";\n}\n.glyphicon-paste:before {\n content: \"\\e206\";\n}\n.glyphicon-alert:before {\n content: \"\\e209\";\n}\n.glyphicon-equalizer:before {\n content: \"\\e210\";\n}\n.glyphicon-king:before {\n content: \"\\e211\";\n}\n.glyphicon-queen:before {\n content: \"\\e212\";\n}\n.glyphicon-pawn:before {\n content: \"\\e213\";\n}\n.glyphicon-bishop:before {\n content: \"\\e214\";\n}\n.glyphicon-knight:before {\n content: \"\\e215\";\n}\n.glyphicon-baby-formula:before {\n content: \"\\e216\";\n}\n.glyphicon-tent:before {\n content: \"\\26fa\";\n}\n.glyphicon-blackboard:before {\n content: \"\\e218\";\n}\n.glyphicon-bed:before {\n content: \"\\e219\";\n}\n.glyphicon-apple:before {\n content: \"\\f8ff\";\n}\n.glyphicon-erase:before {\n content: \"\\e221\";\n}\n.glyphicon-hourglass:before {\n content: \"\\231b\";\n}\n.glyphicon-lamp:before {\n content: \"\\e223\";\n}\n.glyphicon-duplicate:before {\n content: \"\\e224\";\n}\n.glyphicon-piggy-bank:before {\n content: \"\\e225\";\n}\n.glyphicon-scissors:before {\n content: \"\\e226\";\n}\n.glyphicon-bitcoin:before {\n content: \"\\e227\";\n}\n.glyphicon-btc:before {\n content: \"\\e227\";\n}\n.glyphicon-xbt:before {\n content: \"\\e227\";\n}\n.glyphicon-yen:before {\n content: \"\\00a5\";\n}\n.glyphicon-jpy:before {\n content: \"\\00a5\";\n}\n.glyphicon-ruble:before {\n content: \"\\20bd\";\n}\n.glyphicon-rub:before {\n content: \"\\20bd\";\n}\n.glyphicon-scale:before {\n content: \"\\e230\";\n}\n.glyphicon-ice-lolly:before {\n content: \"\\e231\";\n}\n.glyphicon-ice-lolly-tasted:before {\n content: \"\\e232\";\n}\n.glyphicon-education:before {\n content: \"\\e233\";\n}\n.glyphicon-option-horizontal:before {\n content: \"\\e234\";\n}\n.glyphicon-option-vertical:before {\n content: \"\\e235\";\n}\n.glyphicon-menu-hamburger:before {\n content: \"\\e236\";\n}\n.glyphicon-modal-window:before {\n content: \"\\e237\";\n}\n.glyphicon-oil:before {\n content: \"\\e238\";\n}\n.glyphicon-grain:before {\n content: \"\\e239\";\n}\n.glyphicon-sunglasses:before {\n content: \"\\e240\";\n}\n.glyphicon-text-size:before {\n content: \"\\e241\";\n}\n.glyphicon-text-color:before {\n content: \"\\e242\";\n}\n.glyphicon-text-background:before {\n content: \"\\e243\";\n}\n.glyphicon-object-align-top:before {\n content: \"\\e244\";\n}\n.glyphicon-object-align-bottom:before {\n content: \"\\e245\";\n}\n.glyphicon-object-align-horizontal:before {\n content: \"\\e246\";\n}\n.glyphicon-object-align-left:before {\n content: \"\\e247\";\n}\n.glyphicon-object-align-vertical:before {\n content: \"\\e248\";\n}\n.glyphicon-object-align-right:before {\n content: \"\\e249\";\n}\n.glyphicon-triangle-right:before {\n content: \"\\e250\";\n}\n.glyphicon-triangle-left:before {\n content: \"\\e251\";\n}\n.glyphicon-triangle-bottom:before {\n content: \"\\e252\";\n}\n.glyphicon-triangle-top:before {\n content: \"\\e253\";\n}\n.glyphicon-console:before {\n content: \"\\e254\";\n}\n.glyphicon-superscript:before {\n content: \"\\e255\";\n}\n.glyphicon-subscript:before {\n content: \"\\e256\";\n}\n.glyphicon-menu-left:before {\n content: \"\\e257\";\n}\n.glyphicon-menu-right:before {\n content: \"\\e258\";\n}\n.glyphicon-menu-down:before {\n content: \"\\e259\";\n}\n.glyphicon-menu-up:before {\n content: \"\\e260\";\n}\n* {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\n*:before,\n*:after {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\nhtml {\n font-size: 10px;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\nbody {\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n font-size: 14px;\n line-height: 1.42857143;\n color: #333333;\n background-color: #ffffff;\n}\ninput,\nbutton,\nselect,\ntextarea {\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\na {\n color: #337ab7;\n text-decoration: none;\n}\na:hover,\na:focus {\n color: #23527c;\n text-decoration: underline;\n}\na:focus {\n outline: thin dotted;\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\nfigure {\n margin: 0;\n}\nimg {\n vertical-align: middle;\n}\n.img-responsive,\n.thumbnail > img,\n.thumbnail a > img,\n.carousel-inner > .item > img,\n.carousel-inner > .item > a > img {\n display: block;\n max-width: 100%;\n height: auto;\n}\n.img-rounded {\n border-radius: 6px;\n}\n.img-thumbnail {\n padding: 4px;\n line-height: 1.42857143;\n background-color: #ffffff;\n border: 1px solid #dddddd;\n border-radius: 4px;\n -webkit-transition: all 0.2s ease-in-out;\n -o-transition: all 0.2s ease-in-out;\n transition: all 0.2s ease-in-out;\n display: inline-block;\n max-width: 100%;\n height: auto;\n}\n.img-circle {\n border-radius: 50%;\n}\nhr {\n margin-top: 20px;\n margin-bottom: 20px;\n border: 0;\n border-top: 1px solid #eeeeee;\n}\n.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n border: 0;\n}\n.sr-only-focusable:active,\n.sr-only-focusable:focus {\n position: static;\n width: auto;\n height: auto;\n margin: 0;\n overflow: visible;\n clip: auto;\n}\n[role=\"button\"] {\n cursor: pointer;\n}\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\n.h1,\n.h2,\n.h3,\n.h4,\n.h5,\n.h6 {\n font-family: inherit;\n font-weight: 500;\n line-height: 1.1;\n color: inherit;\n}\nh1 small,\nh2 small,\nh3 small,\nh4 small,\nh5 small,\nh6 small,\n.h1 small,\n.h2 small,\n.h3 small,\n.h4 small,\n.h5 small,\n.h6 small,\nh1 .small,\nh2 .small,\nh3 .small,\nh4 .small,\nh5 .small,\nh6 .small,\n.h1 .small,\n.h2 .small,\n.h3 .small,\n.h4 .small,\n.h5 .small,\n.h6 .small {\n font-weight: normal;\n line-height: 1;\n color: #777777;\n}\nh1,\n.h1,\nh2,\n.h2,\nh3,\n.h3 {\n margin-top: 20px;\n margin-bottom: 10px;\n}\nh1 small,\n.h1 small,\nh2 small,\n.h2 small,\nh3 small,\n.h3 small,\nh1 .small,\n.h1 .small,\nh2 .small,\n.h2 .small,\nh3 .small,\n.h3 .small {\n font-size: 65%;\n}\nh4,\n.h4,\nh5,\n.h5,\nh6,\n.h6 {\n margin-top: 10px;\n margin-bottom: 10px;\n}\nh4 small,\n.h4 small,\nh5 small,\n.h5 small,\nh6 small,\n.h6 small,\nh4 .small,\n.h4 .small,\nh5 .small,\n.h5 .small,\nh6 .small,\n.h6 .small {\n font-size: 75%;\n}\nh1,\n.h1 {\n font-size: 36px;\n}\nh2,\n.h2 {\n font-size: 30px;\n}\nh3,\n.h3 {\n font-size: 24px;\n}\nh4,\n.h4 {\n font-size: 18px;\n}\nh5,\n.h5 {\n font-size: 14px;\n}\nh6,\n.h6 {\n font-size: 12px;\n}\np {\n margin: 0 0 10px;\n}\n.lead {\n margin-bottom: 20px;\n font-size: 16px;\n font-weight: 300;\n line-height: 1.4;\n}\n@media (min-width: 768px) {\n .lead {\n font-size: 21px;\n }\n}\nsmall,\n.small {\n font-size: 85%;\n}\nmark,\n.mark {\n background-color: #fcf8e3;\n padding: .2em;\n}\n.text-left {\n text-align: left;\n}\n.text-right {\n text-align: right;\n}\n.text-center {\n text-align: center;\n}\n.text-justify {\n text-align: justify;\n}\n.text-nowrap {\n white-space: nowrap;\n}\n.text-lowercase {\n text-transform: lowercase;\n}\n.text-uppercase {\n text-transform: uppercase;\n}\n.text-capitalize {\n text-transform: capitalize;\n}\n.text-muted {\n color: #777777;\n}\n.text-primary {\n color: #337ab7;\n}\na.text-primary:hover {\n color: #286090;\n}\n.text-success {\n color: #3c763d;\n}\na.text-success:hover {\n color: #2b542c;\n}\n.text-info {\n color: #31708f;\n}\na.text-info:hover {\n color: #245269;\n}\n.text-warning {\n color: #8a6d3b;\n}\na.text-warning:hover {\n color: #66512c;\n}\n.text-danger {\n color: #a94442;\n}\na.text-danger:hover {\n color: #843534;\n}\n.bg-primary {\n color: #fff;\n background-color: #337ab7;\n}\na.bg-primary:hover {\n background-color: #286090;\n}\n.bg-success {\n background-color: #dff0d8;\n}\na.bg-success:hover {\n background-color: #c1e2b3;\n}\n.bg-info {\n background-color: #d9edf7;\n}\na.bg-info:hover {\n background-color: #afd9ee;\n}\n.bg-warning {\n background-color: #fcf8e3;\n}\na.bg-warning:hover {\n background-color: #f7ecb5;\n}\n.bg-danger {\n background-color: #f2dede;\n}\na.bg-danger:hover {\n background-color: #e4b9b9;\n}\n.page-header {\n padding-bottom: 9px;\n margin: 40px 0 20px;\n border-bottom: 1px solid #eeeeee;\n}\nul,\nol {\n margin-top: 0;\n margin-bottom: 10px;\n}\nul ul,\nol ul,\nul ol,\nol ol {\n margin-bottom: 0;\n}\n.list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n.list-inline {\n padding-left: 0;\n list-style: none;\n margin-left: -5px;\n}\n.list-inline > li {\n display: inline-block;\n padding-left: 5px;\n padding-right: 5px;\n}\ndl {\n margin-top: 0;\n margin-bottom: 20px;\n}\ndt,\ndd {\n line-height: 1.42857143;\n}\ndt {\n font-weight: bold;\n}\ndd {\n margin-left: 0;\n}\n@media (min-width: 768px) {\n .dl-horizontal dt {\n float: left;\n width: 160px;\n clear: left;\n text-align: right;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n .dl-horizontal dd {\n margin-left: 180px;\n }\n}\nabbr[title],\nabbr[data-original-title] {\n cursor: help;\n border-bottom: 1px dotted #777777;\n}\n.initialism {\n font-size: 90%;\n text-transform: uppercase;\n}\nblockquote {\n padding: 10px 20px;\n margin: 0 0 20px;\n font-size: 17.5px;\n border-left: 5px solid #eeeeee;\n}\nblockquote p:last-child,\nblockquote ul:last-child,\nblockquote ol:last-child {\n margin-bottom: 0;\n}\nblockquote footer,\nblockquote small,\nblockquote .small {\n display: block;\n font-size: 80%;\n line-height: 1.42857143;\n color: #777777;\n}\nblockquote footer:before,\nblockquote small:before,\nblockquote .small:before {\n content: '\\2014 \\00A0';\n}\n.blockquote-reverse,\nblockquote.pull-right {\n padding-right: 15px;\n padding-left: 0;\n border-right: 5px solid #eeeeee;\n border-left: 0;\n text-align: right;\n}\n.blockquote-reverse footer:before,\nblockquote.pull-right footer:before,\n.blockquote-reverse small:before,\nblockquote.pull-right small:before,\n.blockquote-reverse .small:before,\nblockquote.pull-right .small:before {\n content: '';\n}\n.blockquote-reverse footer:after,\nblockquote.pull-right footer:after,\n.blockquote-reverse small:after,\nblockquote.pull-right small:after,\n.blockquote-reverse .small:after,\nblockquote.pull-right .small:after {\n content: '\\00A0 \\2014';\n}\naddress {\n margin-bottom: 20px;\n font-style: normal;\n line-height: 1.42857143;\n}\ncode,\nkbd,\npre,\nsamp {\n font-family: Menlo, Monaco, Consolas, \"Courier New\", monospace;\n}\ncode {\n padding: 2px 4px;\n font-size: 90%;\n color: #c7254e;\n background-color: #f9f2f4;\n border-radius: 4px;\n}\nkbd {\n padding: 2px 4px;\n font-size: 90%;\n color: #ffffff;\n background-color: #333333;\n border-radius: 3px;\n box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25);\n}\nkbd kbd {\n padding: 0;\n font-size: 100%;\n font-weight: bold;\n box-shadow: none;\n}\npre {\n display: block;\n padding: 9.5px;\n margin: 0 0 10px;\n font-size: 13px;\n line-height: 1.42857143;\n word-break: break-all;\n word-wrap: break-word;\n color: #333333;\n background-color: #f5f5f5;\n border: 1px solid #cccccc;\n border-radius: 4px;\n}\npre code {\n padding: 0;\n font-size: inherit;\n color: inherit;\n white-space: pre-wrap;\n background-color: transparent;\n border-radius: 0;\n}\n.pre-scrollable {\n max-height: 340px;\n overflow-y: scroll;\n}\n.container {\n margin-right: auto;\n margin-left: auto;\n padding-left: 15px;\n padding-right: 15px;\n}\n@media (min-width: 768px) {\n .container {\n width: 750px;\n }\n}\n@media (min-width: 992px) {\n .container {\n width: 970px;\n }\n}\n@media (min-width: 1200px) {\n .container {\n width: 1170px;\n }\n}\n.container-fluid {\n margin-right: auto;\n margin-left: auto;\n padding-left: 15px;\n padding-right: 15px;\n}\n.row {\n margin-left: -15px;\n margin-right: -15px;\n}\n.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 {\n position: relative;\n min-height: 1px;\n padding-left: 15px;\n padding-right: 15px;\n}\n.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 {\n float: left;\n}\n.col-xs-12 {\n width: 100%;\n}\n.col-xs-11 {\n width: 91.66666667%;\n}\n.col-xs-10 {\n width: 83.33333333%;\n}\n.col-xs-9 {\n width: 75%;\n}\n.col-xs-8 {\n width: 66.66666667%;\n}\n.col-xs-7 {\n width: 58.33333333%;\n}\n.col-xs-6 {\n width: 50%;\n}\n.col-xs-5 {\n width: 41.66666667%;\n}\n.col-xs-4 {\n width: 33.33333333%;\n}\n.col-xs-3 {\n width: 25%;\n}\n.col-xs-2 {\n width: 16.66666667%;\n}\n.col-xs-1 {\n width: 8.33333333%;\n}\n.col-xs-pull-12 {\n right: 100%;\n}\n.col-xs-pull-11 {\n right: 91.66666667%;\n}\n.col-xs-pull-10 {\n right: 83.33333333%;\n}\n.col-xs-pull-9 {\n right: 75%;\n}\n.col-xs-pull-8 {\n right: 66.66666667%;\n}\n.col-xs-pull-7 {\n right: 58.33333333%;\n}\n.col-xs-pull-6 {\n right: 50%;\n}\n.col-xs-pull-5 {\n right: 41.66666667%;\n}\n.col-xs-pull-4 {\n right: 33.33333333%;\n}\n.col-xs-pull-3 {\n right: 25%;\n}\n.col-xs-pull-2 {\n right: 16.66666667%;\n}\n.col-xs-pull-1 {\n right: 8.33333333%;\n}\n.col-xs-pull-0 {\n right: auto;\n}\n.col-xs-push-12 {\n left: 100%;\n}\n.col-xs-push-11 {\n left: 91.66666667%;\n}\n.col-xs-push-10 {\n left: 83.33333333%;\n}\n.col-xs-push-9 {\n left: 75%;\n}\n.col-xs-push-8 {\n left: 66.66666667%;\n}\n.col-xs-push-7 {\n left: 58.33333333%;\n}\n.col-xs-push-6 {\n left: 50%;\n}\n.col-xs-push-5 {\n left: 41.66666667%;\n}\n.col-xs-push-4 {\n left: 33.33333333%;\n}\n.col-xs-push-3 {\n left: 25%;\n}\n.col-xs-push-2 {\n left: 16.66666667%;\n}\n.col-xs-push-1 {\n left: 8.33333333%;\n}\n.col-xs-push-0 {\n left: auto;\n}\n.col-xs-offset-12 {\n margin-left: 100%;\n}\n.col-xs-offset-11 {\n margin-left: 91.66666667%;\n}\n.col-xs-offset-10 {\n margin-left: 83.33333333%;\n}\n.col-xs-offset-9 {\n margin-left: 75%;\n}\n.col-xs-offset-8 {\n margin-left: 66.66666667%;\n}\n.col-xs-offset-7 {\n margin-left: 58.33333333%;\n}\n.col-xs-offset-6 {\n margin-left: 50%;\n}\n.col-xs-offset-5 {\n margin-left: 41.66666667%;\n}\n.col-xs-offset-4 {\n margin-left: 33.33333333%;\n}\n.col-xs-offset-3 {\n margin-left: 25%;\n}\n.col-xs-offset-2 {\n margin-left: 16.66666667%;\n}\n.col-xs-offset-1 {\n margin-left: 8.33333333%;\n}\n.col-xs-offset-0 {\n margin-left: 0%;\n}\n@media (min-width: 768px) {\n .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 {\n float: left;\n }\n .col-sm-12 {\n width: 100%;\n }\n .col-sm-11 {\n width: 91.66666667%;\n }\n .col-sm-10 {\n width: 83.33333333%;\n }\n .col-sm-9 {\n width: 75%;\n }\n .col-sm-8 {\n width: 66.66666667%;\n }\n .col-sm-7 {\n width: 58.33333333%;\n }\n .col-sm-6 {\n width: 50%;\n }\n .col-sm-5 {\n width: 41.66666667%;\n }\n .col-sm-4 {\n width: 33.33333333%;\n }\n .col-sm-3 {\n width: 25%;\n }\n .col-sm-2 {\n width: 16.66666667%;\n }\n .col-sm-1 {\n width: 8.33333333%;\n }\n .col-sm-pull-12 {\n right: 100%;\n }\n .col-sm-pull-11 {\n right: 91.66666667%;\n }\n .col-sm-pull-10 {\n right: 83.33333333%;\n }\n .col-sm-pull-9 {\n right: 75%;\n }\n .col-sm-pull-8 {\n right: 66.66666667%;\n }\n .col-sm-pull-7 {\n right: 58.33333333%;\n }\n .col-sm-pull-6 {\n right: 50%;\n }\n .col-sm-pull-5 {\n right: 41.66666667%;\n }\n .col-sm-pull-4 {\n right: 33.33333333%;\n }\n .col-sm-pull-3 {\n right: 25%;\n }\n .col-sm-pull-2 {\n right: 16.66666667%;\n }\n .col-sm-pull-1 {\n right: 8.33333333%;\n }\n .col-sm-pull-0 {\n right: auto;\n }\n .col-sm-push-12 {\n left: 100%;\n }\n .col-sm-push-11 {\n left: 91.66666667%;\n }\n .col-sm-push-10 {\n left: 83.33333333%;\n }\n .col-sm-push-9 {\n left: 75%;\n }\n .col-sm-push-8 {\n left: 66.66666667%;\n }\n .col-sm-push-7 {\n left: 58.33333333%;\n }\n .col-sm-push-6 {\n left: 50%;\n }\n .col-sm-push-5 {\n left: 41.66666667%;\n }\n .col-sm-push-4 {\n left: 33.33333333%;\n }\n .col-sm-push-3 {\n left: 25%;\n }\n .col-sm-push-2 {\n left: 16.66666667%;\n }\n .col-sm-push-1 {\n left: 8.33333333%;\n }\n .col-sm-push-0 {\n left: auto;\n }\n .col-sm-offset-12 {\n margin-left: 100%;\n }\n .col-sm-offset-11 {\n margin-left: 91.66666667%;\n }\n .col-sm-offset-10 {\n margin-left: 83.33333333%;\n }\n .col-sm-offset-9 {\n margin-left: 75%;\n }\n .col-sm-offset-8 {\n margin-left: 66.66666667%;\n }\n .col-sm-offset-7 {\n margin-left: 58.33333333%;\n }\n .col-sm-offset-6 {\n margin-left: 50%;\n }\n .col-sm-offset-5 {\n margin-left: 41.66666667%;\n }\n .col-sm-offset-4 {\n margin-left: 33.33333333%;\n }\n .col-sm-offset-3 {\n margin-left: 25%;\n }\n .col-sm-offset-2 {\n margin-left: 16.66666667%;\n }\n .col-sm-offset-1 {\n margin-left: 8.33333333%;\n }\n .col-sm-offset-0 {\n margin-left: 0%;\n }\n}\n@media (min-width: 992px) {\n .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 {\n float: left;\n }\n .col-md-12 {\n width: 100%;\n }\n .col-md-11 {\n width: 91.66666667%;\n }\n .col-md-10 {\n width: 83.33333333%;\n }\n .col-md-9 {\n width: 75%;\n }\n .col-md-8 {\n width: 66.66666667%;\n }\n .col-md-7 {\n width: 58.33333333%;\n }\n .col-md-6 {\n width: 50%;\n }\n .col-md-5 {\n width: 41.66666667%;\n }\n .col-md-4 {\n width: 33.33333333%;\n }\n .col-md-3 {\n width: 25%;\n }\n .col-md-2 {\n width: 16.66666667%;\n }\n .col-md-1 {\n width: 8.33333333%;\n }\n .col-md-pull-12 {\n right: 100%;\n }\n .col-md-pull-11 {\n right: 91.66666667%;\n }\n .col-md-pull-10 {\n right: 83.33333333%;\n }\n .col-md-pull-9 {\n right: 75%;\n }\n .col-md-pull-8 {\n right: 66.66666667%;\n }\n .col-md-pull-7 {\n right: 58.33333333%;\n }\n .col-md-pull-6 {\n right: 50%;\n }\n .col-md-pull-5 {\n right: 41.66666667%;\n }\n .col-md-pull-4 {\n right: 33.33333333%;\n }\n .col-md-pull-3 {\n right: 25%;\n }\n .col-md-pull-2 {\n right: 16.66666667%;\n }\n .col-md-pull-1 {\n right: 8.33333333%;\n }\n .col-md-pull-0 {\n right: auto;\n }\n .col-md-push-12 {\n left: 100%;\n }\n .col-md-push-11 {\n left: 91.66666667%;\n }\n .col-md-push-10 {\n left: 83.33333333%;\n }\n .col-md-push-9 {\n left: 75%;\n }\n .col-md-push-8 {\n left: 66.66666667%;\n }\n .col-md-push-7 {\n left: 58.33333333%;\n }\n .col-md-push-6 {\n left: 50%;\n }\n .col-md-push-5 {\n left: 41.66666667%;\n }\n .col-md-push-4 {\n left: 33.33333333%;\n }\n .col-md-push-3 {\n left: 25%;\n }\n .col-md-push-2 {\n left: 16.66666667%;\n }\n .col-md-push-1 {\n left: 8.33333333%;\n }\n .col-md-push-0 {\n left: auto;\n }\n .col-md-offset-12 {\n margin-left: 100%;\n }\n .col-md-offset-11 {\n margin-left: 91.66666667%;\n }\n .col-md-offset-10 {\n margin-left: 83.33333333%;\n }\n .col-md-offset-9 {\n margin-left: 75%;\n }\n .col-md-offset-8 {\n margin-left: 66.66666667%;\n }\n .col-md-offset-7 {\n margin-left: 58.33333333%;\n }\n .col-md-offset-6 {\n margin-left: 50%;\n }\n .col-md-offset-5 {\n margin-left: 41.66666667%;\n }\n .col-md-offset-4 {\n margin-left: 33.33333333%;\n }\n .col-md-offset-3 {\n margin-left: 25%;\n }\n .col-md-offset-2 {\n margin-left: 16.66666667%;\n }\n .col-md-offset-1 {\n margin-left: 8.33333333%;\n }\n .col-md-offset-0 {\n margin-left: 0%;\n }\n}\n@media (min-width: 1200px) {\n .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 {\n float: left;\n }\n .col-lg-12 {\n width: 100%;\n }\n .col-lg-11 {\n width: 91.66666667%;\n }\n .col-lg-10 {\n width: 83.33333333%;\n }\n .col-lg-9 {\n width: 75%;\n }\n .col-lg-8 {\n width: 66.66666667%;\n }\n .col-lg-7 {\n width: 58.33333333%;\n }\n .col-lg-6 {\n width: 50%;\n }\n .col-lg-5 {\n width: 41.66666667%;\n }\n .col-lg-4 {\n width: 33.33333333%;\n }\n .col-lg-3 {\n width: 25%;\n }\n .col-lg-2 {\n width: 16.66666667%;\n }\n .col-lg-1 {\n width: 8.33333333%;\n }\n .col-lg-pull-12 {\n right: 100%;\n }\n .col-lg-pull-11 {\n right: 91.66666667%;\n }\n .col-lg-pull-10 {\n right: 83.33333333%;\n }\n .col-lg-pull-9 {\n right: 75%;\n }\n .col-lg-pull-8 {\n right: 66.66666667%;\n }\n .col-lg-pull-7 {\n right: 58.33333333%;\n }\n .col-lg-pull-6 {\n right: 50%;\n }\n .col-lg-pull-5 {\n right: 41.66666667%;\n }\n .col-lg-pull-4 {\n right: 33.33333333%;\n }\n .col-lg-pull-3 {\n right: 25%;\n }\n .col-lg-pull-2 {\n right: 16.66666667%;\n }\n .col-lg-pull-1 {\n right: 8.33333333%;\n }\n .col-lg-pull-0 {\n right: auto;\n }\n .col-lg-push-12 {\n left: 100%;\n }\n .col-lg-push-11 {\n left: 91.66666667%;\n }\n .col-lg-push-10 {\n left: 83.33333333%;\n }\n .col-lg-push-9 {\n left: 75%;\n }\n .col-lg-push-8 {\n left: 66.66666667%;\n }\n .col-lg-push-7 {\n left: 58.33333333%;\n }\n .col-lg-push-6 {\n left: 50%;\n }\n .col-lg-push-5 {\n left: 41.66666667%;\n }\n .col-lg-push-4 {\n left: 33.33333333%;\n }\n .col-lg-push-3 {\n left: 25%;\n }\n .col-lg-push-2 {\n left: 16.66666667%;\n }\n .col-lg-push-1 {\n left: 8.33333333%;\n }\n .col-lg-push-0 {\n left: auto;\n }\n .col-lg-offset-12 {\n margin-left: 100%;\n }\n .col-lg-offset-11 {\n margin-left: 91.66666667%;\n }\n .col-lg-offset-10 {\n margin-left: 83.33333333%;\n }\n .col-lg-offset-9 {\n margin-left: 75%;\n }\n .col-lg-offset-8 {\n margin-left: 66.66666667%;\n }\n .col-lg-offset-7 {\n margin-left: 58.33333333%;\n }\n .col-lg-offset-6 {\n margin-left: 50%;\n }\n .col-lg-offset-5 {\n margin-left: 41.66666667%;\n }\n .col-lg-offset-4 {\n margin-left: 33.33333333%;\n }\n .col-lg-offset-3 {\n margin-left: 25%;\n }\n .col-lg-offset-2 {\n margin-left: 16.66666667%;\n }\n .col-lg-offset-1 {\n margin-left: 8.33333333%;\n }\n .col-lg-offset-0 {\n margin-left: 0%;\n }\n}\ntable {\n background-color: transparent;\n}\ncaption {\n padding-top: 8px;\n padding-bottom: 8px;\n color: #777777;\n text-align: left;\n}\nth {\n text-align: left;\n}\n.table {\n width: 100%;\n max-width: 100%;\n margin-bottom: 20px;\n}\n.table > thead > tr > th,\n.table > tbody > tr > th,\n.table > tfoot > tr > th,\n.table > thead > tr > td,\n.table > tbody > tr > td,\n.table > tfoot > tr > td {\n padding: 8px;\n line-height: 1.42857143;\n vertical-align: top;\n border-top: 1px solid #dddddd;\n}\n.table > thead > tr > th {\n vertical-align: bottom;\n border-bottom: 2px solid #dddddd;\n}\n.table > caption + thead > tr:first-child > th,\n.table > colgroup + thead > tr:first-child > th,\n.table > thead:first-child > tr:first-child > th,\n.table > caption + thead > tr:first-child > td,\n.table > colgroup + thead > tr:first-child > td,\n.table > thead:first-child > tr:first-child > td {\n border-top: 0;\n}\n.table > tbody + tbody {\n border-top: 2px solid #dddddd;\n}\n.table .table {\n background-color: #ffffff;\n}\n.table-condensed > thead > tr > th,\n.table-condensed > tbody > tr > th,\n.table-condensed > tfoot > tr > th,\n.table-condensed > thead > tr > td,\n.table-condensed > tbody > tr > td,\n.table-condensed > tfoot > tr > td {\n padding: 5px;\n}\n.table-bordered {\n border: 1px solid #dddddd;\n}\n.table-bordered > thead > tr > th,\n.table-bordered > tbody > tr > th,\n.table-bordered > tfoot > tr > th,\n.table-bordered > thead > tr > td,\n.table-bordered > tbody > tr > td,\n.table-bordered > tfoot > tr > td {\n border: 1px solid #dddddd;\n}\n.table-bordered > thead > tr > th,\n.table-bordered > thead > tr > td {\n border-bottom-width: 2px;\n}\n.table-striped > tbody > tr:nth-of-type(odd) {\n background-color: #f9f9f9;\n}\n.table-hover > tbody > tr:hover {\n background-color: #f5f5f5;\n}\ntable col[class*=\"col-\"] {\n position: static;\n float: none;\n display: table-column;\n}\ntable td[class*=\"col-\"],\ntable th[class*=\"col-\"] {\n position: static;\n float: none;\n display: table-cell;\n}\n.table > thead > tr > td.active,\n.table > tbody > tr > td.active,\n.table > tfoot > tr > td.active,\n.table > thead > tr > th.active,\n.table > tbody > tr > th.active,\n.table > tfoot > tr > th.active,\n.table > thead > tr.active > td,\n.table > tbody > tr.active > td,\n.table > tfoot > tr.active > td,\n.table > thead > tr.active > th,\n.table > tbody > tr.active > th,\n.table > tfoot > tr.active > th {\n background-color: #f5f5f5;\n}\n.table-hover > tbody > tr > td.active:hover,\n.table-hover > tbody > tr > th.active:hover,\n.table-hover > tbody > tr.active:hover > td,\n.table-hover > tbody > tr:hover > .active,\n.table-hover > tbody > tr.active:hover > th {\n background-color: #e8e8e8;\n}\n.table > thead > tr > td.success,\n.table > tbody > tr > td.success,\n.table > tfoot > tr > td.success,\n.table > thead > tr > th.success,\n.table > tbody > tr > th.success,\n.table > tfoot > tr > th.success,\n.table > thead > tr.success > td,\n.table > tbody > tr.success > td,\n.table > tfoot > tr.success > td,\n.table > thead > tr.success > th,\n.table > tbody > tr.success > th,\n.table > tfoot > tr.success > th {\n background-color: #dff0d8;\n}\n.table-hover > tbody > tr > td.success:hover,\n.table-hover > tbody > tr > th.success:hover,\n.table-hover > tbody > tr.success:hover > td,\n.table-hover > tbody > tr:hover > .success,\n.table-hover > tbody > tr.success:hover > th {\n background-color: #d0e9c6;\n}\n.table > thead > tr > td.info,\n.table > tbody > tr > td.info,\n.table > tfoot > tr > td.info,\n.table > thead > tr > th.info,\n.table > tbody > tr > th.info,\n.table > tfoot > tr > th.info,\n.table > thead > tr.info > td,\n.table > tbody > tr.info > td,\n.table > tfoot > tr.info > td,\n.table > thead > tr.info > th,\n.table > tbody > tr.info > th,\n.table > tfoot > tr.info > th {\n background-color: #d9edf7;\n}\n.table-hover > tbody > tr > td.info:hover,\n.table-hover > tbody > tr > th.info:hover,\n.table-hover > tbody > tr.info:hover > td,\n.table-hover > tbody > tr:hover > .info,\n.table-hover > tbody > tr.info:hover > th {\n background-color: #c4e3f3;\n}\n.table > thead > tr > td.warning,\n.table > tbody > tr > td.warning,\n.table > tfoot > tr > td.warning,\n.table > thead > tr > th.warning,\n.table > tbody > tr > th.warning,\n.table > tfoot > tr > th.warning,\n.table > thead > tr.warning > td,\n.table > tbody > tr.warning > td,\n.table > tfoot > tr.warning > td,\n.table > thead > tr.warning > th,\n.table > tbody > tr.warning > th,\n.table > tfoot > tr.warning > th {\n background-color: #fcf8e3;\n}\n.table-hover > tbody > tr > td.warning:hover,\n.table-hover > tbody > tr > th.warning:hover,\n.table-hover > tbody > tr.warning:hover > td,\n.table-hover > tbody > tr:hover > .warning,\n.table-hover > tbody > tr.warning:hover > th {\n background-color: #faf2cc;\n}\n.table > thead > tr > td.danger,\n.table > tbody > tr > td.danger,\n.table > tfoot > tr > td.danger,\n.table > thead > tr > th.danger,\n.table > tbody > tr > th.danger,\n.table > tfoot > tr > th.danger,\n.table > thead > tr.danger > td,\n.table > tbody > tr.danger > td,\n.table > tfoot > tr.danger > td,\n.table > thead > tr.danger > th,\n.table > tbody > tr.danger > th,\n.table > tfoot > tr.danger > th {\n background-color: #f2dede;\n}\n.table-hover > tbody > tr > td.danger:hover,\n.table-hover > tbody > tr > th.danger:hover,\n.table-hover > tbody > tr.danger:hover > td,\n.table-hover > tbody > tr:hover > .danger,\n.table-hover > tbody > tr.danger:hover > th {\n background-color: #ebcccc;\n}\n.table-responsive {\n overflow-x: auto;\n min-height: 0.01%;\n}\n@media screen and (max-width: 767px) {\n .table-responsive {\n width: 100%;\n margin-bottom: 15px;\n overflow-y: hidden;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n border: 1px solid #dddddd;\n }\n .table-responsive > .table {\n margin-bottom: 0;\n }\n .table-responsive > .table > thead > tr > th,\n .table-responsive > .table > tbody > tr > th,\n .table-responsive > .table > tfoot > tr > th,\n .table-responsive > .table > thead > tr > td,\n .table-responsive > .table > tbody > tr > td,\n .table-responsive > .table > tfoot > tr > td {\n white-space: nowrap;\n }\n .table-responsive > .table-bordered {\n border: 0;\n }\n .table-responsive > .table-bordered > thead > tr > th:first-child,\n .table-responsive > .table-bordered > tbody > tr > th:first-child,\n .table-responsive > .table-bordered > tfoot > tr > th:first-child,\n .table-responsive > .table-bordered > thead > tr > td:first-child,\n .table-responsive > .table-bordered > tbody > tr > td:first-child,\n .table-responsive > .table-bordered > tfoot > tr > td:first-child {\n border-left: 0;\n }\n .table-responsive > .table-bordered > thead > tr > th:last-child,\n .table-responsive > .table-bordered > tbody > tr > th:last-child,\n .table-responsive > .table-bordered > tfoot > tr > th:last-child,\n .table-responsive > .table-bordered > thead > tr > td:last-child,\n .table-responsive > .table-bordered > tbody > tr > td:last-child,\n .table-responsive > .table-bordered > tfoot > tr > td:last-child {\n border-right: 0;\n }\n .table-responsive > .table-bordered > tbody > tr:last-child > th,\n .table-responsive > .table-bordered > tfoot > tr:last-child > th,\n .table-responsive > .table-bordered > tbody > tr:last-child > td,\n .table-responsive > .table-bordered > tfoot > tr:last-child > td {\n border-bottom: 0;\n }\n}\nfieldset {\n padding: 0;\n margin: 0;\n border: 0;\n min-width: 0;\n}\nlegend {\n display: block;\n width: 100%;\n padding: 0;\n margin-bottom: 20px;\n font-size: 21px;\n line-height: inherit;\n color: #333333;\n border: 0;\n border-bottom: 1px solid #e5e5e5;\n}\nlabel {\n display: inline-block;\n max-width: 100%;\n margin-bottom: 5px;\n font-weight: bold;\n}\ninput[type=\"search\"] {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n margin: 4px 0 0;\n margin-top: 1px \\9;\n line-height: normal;\n}\ninput[type=\"file\"] {\n display: block;\n}\ninput[type=\"range\"] {\n display: block;\n width: 100%;\n}\nselect[multiple],\nselect[size] {\n height: auto;\n}\ninput[type=\"file\"]:focus,\ninput[type=\"radio\"]:focus,\ninput[type=\"checkbox\"]:focus {\n outline: thin dotted;\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\noutput {\n display: block;\n padding-top: 7px;\n font-size: 14px;\n line-height: 1.42857143;\n color: #555555;\n}\n.form-control {\n display: block;\n width: 100%;\n height: 34px;\n padding: 6px 12px;\n font-size: 14px;\n line-height: 1.42857143;\n color: #555555;\n background-color: #ffffff;\n background-image: none;\n border: 1px solid #cccccc;\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n -webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n}\n.form-control:focus {\n border-color: #66afe9;\n outline: 0;\n -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);\n box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);\n}\n.form-control::-moz-placeholder {\n color: #999999;\n opacity: 1;\n}\n.form-control:-ms-input-placeholder {\n color: #999999;\n}\n.form-control::-webkit-input-placeholder {\n color: #999999;\n}\n.form-control[disabled],\n.form-control[readonly],\nfieldset[disabled] .form-control {\n background-color: #eeeeee;\n opacity: 1;\n}\n.form-control[disabled],\nfieldset[disabled] .form-control {\n cursor: not-allowed;\n}\ntextarea.form-control {\n height: auto;\n}\ninput[type=\"search\"] {\n -webkit-appearance: none;\n}\n@media screen and (-webkit-min-device-pixel-ratio: 0) {\n input[type=\"date\"],\n input[type=\"time\"],\n input[type=\"datetime-local\"],\n input[type=\"month\"] {\n line-height: 34px;\n }\n input[type=\"date\"].input-sm,\n input[type=\"time\"].input-sm,\n input[type=\"datetime-local\"].input-sm,\n input[type=\"month\"].input-sm,\n .input-group-sm input[type=\"date\"],\n .input-group-sm input[type=\"time\"],\n .input-group-sm input[type=\"datetime-local\"],\n .input-group-sm input[type=\"month\"] {\n line-height: 30px;\n }\n input[type=\"date\"].input-lg,\n input[type=\"time\"].input-lg,\n input[type=\"datetime-local\"].input-lg,\n input[type=\"month\"].input-lg,\n .input-group-lg input[type=\"date\"],\n .input-group-lg input[type=\"time\"],\n .input-group-lg input[type=\"datetime-local\"],\n .input-group-lg input[type=\"month\"] {\n line-height: 46px;\n }\n}\n.form-group {\n margin-bottom: 15px;\n}\n.radio,\n.checkbox {\n position: relative;\n display: block;\n margin-top: 10px;\n margin-bottom: 10px;\n}\n.radio label,\n.checkbox label {\n min-height: 20px;\n padding-left: 20px;\n margin-bottom: 0;\n font-weight: normal;\n cursor: pointer;\n}\n.radio input[type=\"radio\"],\n.radio-inline input[type=\"radio\"],\n.checkbox input[type=\"checkbox\"],\n.checkbox-inline input[type=\"checkbox\"] {\n position: absolute;\n margin-left: -20px;\n margin-top: 4px \\9;\n}\n.radio + .radio,\n.checkbox + .checkbox {\n margin-top: -5px;\n}\n.radio-inline,\n.checkbox-inline {\n position: relative;\n display: inline-block;\n padding-left: 20px;\n margin-bottom: 0;\n vertical-align: middle;\n font-weight: normal;\n cursor: pointer;\n}\n.radio-inline + .radio-inline,\n.checkbox-inline + .checkbox-inline {\n margin-top: 0;\n margin-left: 10px;\n}\ninput[type=\"radio\"][disabled],\ninput[type=\"checkbox\"][disabled],\ninput[type=\"radio\"].disabled,\ninput[type=\"checkbox\"].disabled,\nfieldset[disabled] input[type=\"radio\"],\nfieldset[disabled] input[type=\"checkbox\"] {\n cursor: not-allowed;\n}\n.radio-inline.disabled,\n.checkbox-inline.disabled,\nfieldset[disabled] .radio-inline,\nfieldset[disabled] .checkbox-inline {\n cursor: not-allowed;\n}\n.radio.disabled label,\n.checkbox.disabled label,\nfieldset[disabled] .radio label,\nfieldset[disabled] .checkbox label {\n cursor: not-allowed;\n}\n.form-control-static {\n padding-top: 7px;\n padding-bottom: 7px;\n margin-bottom: 0;\n min-height: 34px;\n}\n.form-control-static.input-lg,\n.form-control-static.input-sm {\n padding-left: 0;\n padding-right: 0;\n}\n.input-sm {\n height: 30px;\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\nselect.input-sm {\n height: 30px;\n line-height: 30px;\n}\ntextarea.input-sm,\nselect[multiple].input-sm {\n height: auto;\n}\n.form-group-sm .form-control {\n height: 30px;\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\nselect.form-group-sm .form-control {\n height: 30px;\n line-height: 30px;\n}\ntextarea.form-group-sm .form-control,\nselect[multiple].form-group-sm .form-control {\n height: auto;\n}\n.form-group-sm .form-control-static {\n height: 30px;\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n min-height: 32px;\n}\n.input-lg {\n height: 46px;\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\nselect.input-lg {\n height: 46px;\n line-height: 46px;\n}\ntextarea.input-lg,\nselect[multiple].input-lg {\n height: auto;\n}\n.form-group-lg .form-control {\n height: 46px;\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\nselect.form-group-lg .form-control {\n height: 46px;\n line-height: 46px;\n}\ntextarea.form-group-lg .form-control,\nselect[multiple].form-group-lg .form-control {\n height: auto;\n}\n.form-group-lg .form-control-static {\n height: 46px;\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n min-height: 38px;\n}\n.has-feedback {\n position: relative;\n}\n.has-feedback .form-control {\n padding-right: 42.5px;\n}\n.form-control-feedback {\n position: absolute;\n top: 0;\n right: 0;\n z-index: 2;\n display: block;\n width: 34px;\n height: 34px;\n line-height: 34px;\n text-align: center;\n pointer-events: none;\n}\n.input-lg + .form-control-feedback {\n width: 46px;\n height: 46px;\n line-height: 46px;\n}\n.input-sm + .form-control-feedback {\n width: 30px;\n height: 30px;\n line-height: 30px;\n}\n.has-success .help-block,\n.has-success .control-label,\n.has-success .radio,\n.has-success .checkbox,\n.has-success .radio-inline,\n.has-success .checkbox-inline,\n.has-success.radio label,\n.has-success.checkbox label,\n.has-success.radio-inline label,\n.has-success.checkbox-inline label {\n color: #3c763d;\n}\n.has-success .form-control {\n border-color: #3c763d;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-success .form-control:focus {\n border-color: #2b542c;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;\n}\n.has-success .input-group-addon {\n color: #3c763d;\n border-color: #3c763d;\n background-color: #dff0d8;\n}\n.has-success .form-control-feedback {\n color: #3c763d;\n}\n.has-warning .help-block,\n.has-warning .control-label,\n.has-warning .radio,\n.has-warning .checkbox,\n.has-warning .radio-inline,\n.has-warning .checkbox-inline,\n.has-warning.radio label,\n.has-warning.checkbox label,\n.has-warning.radio-inline label,\n.has-warning.checkbox-inline label {\n color: #8a6d3b;\n}\n.has-warning .form-control {\n border-color: #8a6d3b;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-warning .form-control:focus {\n border-color: #66512c;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;\n}\n.has-warning .input-group-addon {\n color: #8a6d3b;\n border-color: #8a6d3b;\n background-color: #fcf8e3;\n}\n.has-warning .form-control-feedback {\n color: #8a6d3b;\n}\n.has-error .help-block,\n.has-error .control-label,\n.has-error .radio,\n.has-error .checkbox,\n.has-error .radio-inline,\n.has-error .checkbox-inline,\n.has-error.radio label,\n.has-error.checkbox label,\n.has-error.radio-inline label,\n.has-error.checkbox-inline label {\n color: #a94442;\n}\n.has-error .form-control {\n border-color: #a94442;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-error .form-control:focus {\n border-color: #843534;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;\n}\n.has-error .input-group-addon {\n color: #a94442;\n border-color: #a94442;\n background-color: #f2dede;\n}\n.has-error .form-control-feedback {\n color: #a94442;\n}\n.has-feedback label ~ .form-control-feedback {\n top: 25px;\n}\n.has-feedback label.sr-only ~ .form-control-feedback {\n top: 0;\n}\n.help-block {\n display: block;\n margin-top: 5px;\n margin-bottom: 10px;\n color: #737373;\n}\n@media (min-width: 768px) {\n .form-inline .form-group {\n display: inline-block;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .form-inline .form-control {\n display: inline-block;\n width: auto;\n vertical-align: middle;\n }\n .form-inline .form-control-static {\n display: inline-block;\n }\n .form-inline .input-group {\n display: inline-table;\n vertical-align: middle;\n }\n .form-inline .input-group .input-group-addon,\n .form-inline .input-group .input-group-btn,\n .form-inline .input-group .form-control {\n width: auto;\n }\n .form-inline .input-group > .form-control {\n width: 100%;\n }\n .form-inline .control-label {\n margin-bottom: 0;\n vertical-align: middle;\n }\n .form-inline .radio,\n .form-inline .checkbox {\n display: inline-block;\n margin-top: 0;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .form-inline .radio label,\n .form-inline .checkbox label {\n padding-left: 0;\n }\n .form-inline .radio input[type=\"radio\"],\n .form-inline .checkbox input[type=\"checkbox\"] {\n position: relative;\n margin-left: 0;\n }\n .form-inline .has-feedback .form-control-feedback {\n top: 0;\n }\n}\n.form-horizontal .radio,\n.form-horizontal .checkbox,\n.form-horizontal .radio-inline,\n.form-horizontal .checkbox-inline {\n margin-top: 0;\n margin-bottom: 0;\n padding-top: 7px;\n}\n.form-horizontal .radio,\n.form-horizontal .checkbox {\n min-height: 27px;\n}\n.form-horizontal .form-group {\n margin-left: -15px;\n margin-right: -15px;\n}\n@media (min-width: 768px) {\n .form-horizontal .control-label {\n text-align: right;\n margin-bottom: 0;\n padding-top: 7px;\n }\n}\n.form-horizontal .has-feedback .form-control-feedback {\n right: 15px;\n}\n@media (min-width: 768px) {\n .form-horizontal .form-group-lg .control-label {\n padding-top: 14.333333px;\n }\n}\n@media (min-width: 768px) {\n .form-horizontal .form-group-sm .control-label {\n padding-top: 6px;\n }\n}\n.btn {\n display: inline-block;\n margin-bottom: 0;\n font-weight: normal;\n text-align: center;\n vertical-align: middle;\n touch-action: manipulation;\n cursor: pointer;\n background-image: none;\n border: 1px solid transparent;\n white-space: nowrap;\n padding: 6px 12px;\n font-size: 14px;\n line-height: 1.42857143;\n border-radius: 4px;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n}\n.btn:focus,\n.btn:active:focus,\n.btn.active:focus,\n.btn.focus,\n.btn:active.focus,\n.btn.active.focus {\n outline: thin dotted;\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\n.btn:hover,\n.btn:focus,\n.btn.focus {\n color: #333333;\n text-decoration: none;\n}\n.btn:active,\n.btn.active {\n outline: 0;\n background-image: none;\n -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n}\n.btn.disabled,\n.btn[disabled],\nfieldset[disabled] .btn {\n cursor: not-allowed;\n pointer-events: none;\n opacity: 0.65;\n filter: alpha(opacity=65);\n -webkit-box-shadow: none;\n box-shadow: none;\n}\n.btn-default {\n color: #333333;\n background-color: #ffffff;\n border-color: #cccccc;\n}\n.btn-default:hover,\n.btn-default:focus,\n.btn-default.focus,\n.btn-default:active,\n.btn-default.active,\n.open > .dropdown-toggle.btn-default {\n color: #333333;\n background-color: #e6e6e6;\n border-color: #adadad;\n}\n.btn-default:active,\n.btn-default.active,\n.open > .dropdown-toggle.btn-default {\n background-image: none;\n}\n.btn-default.disabled,\n.btn-default[disabled],\nfieldset[disabled] .btn-default,\n.btn-default.disabled:hover,\n.btn-default[disabled]:hover,\nfieldset[disabled] .btn-default:hover,\n.btn-default.disabled:focus,\n.btn-default[disabled]:focus,\nfieldset[disabled] .btn-default:focus,\n.btn-default.disabled.focus,\n.btn-default[disabled].focus,\nfieldset[disabled] .btn-default.focus,\n.btn-default.disabled:active,\n.btn-default[disabled]:active,\nfieldset[disabled] .btn-default:active,\n.btn-default.disabled.active,\n.btn-default[disabled].active,\nfieldset[disabled] .btn-default.active {\n background-color: #ffffff;\n border-color: #cccccc;\n}\n.btn-default .badge {\n color: #ffffff;\n background-color: #333333;\n}\n.btn-primary {\n color: #ffffff;\n background-color: #337ab7;\n border-color: #2e6da4;\n}\n.btn-primary:hover,\n.btn-primary:focus,\n.btn-primary.focus,\n.btn-primary:active,\n.btn-primary.active,\n.open > .dropdown-toggle.btn-primary {\n color: #ffffff;\n background-color: #286090;\n border-color: #204d74;\n}\n.btn-primary:active,\n.btn-primary.active,\n.open > .dropdown-toggle.btn-primary {\n background-image: none;\n}\n.btn-primary.disabled,\n.btn-primary[disabled],\nfieldset[disabled] .btn-primary,\n.btn-primary.disabled:hover,\n.btn-primary[disabled]:hover,\nfieldset[disabled] .btn-primary:hover,\n.btn-primary.disabled:focus,\n.btn-primary[disabled]:focus,\nfieldset[disabled] .btn-primary:focus,\n.btn-primary.disabled.focus,\n.btn-primary[disabled].focus,\nfieldset[disabled] .btn-primary.focus,\n.btn-primary.disabled:active,\n.btn-primary[disabled]:active,\nfieldset[disabled] .btn-primary:active,\n.btn-primary.disabled.active,\n.btn-primary[disabled].active,\nfieldset[disabled] .btn-primary.active {\n background-color: #337ab7;\n border-color: #2e6da4;\n}\n.btn-primary .badge {\n color: #337ab7;\n background-color: #ffffff;\n}\n.btn-success {\n color: #ffffff;\n background-color: #5cb85c;\n border-color: #4cae4c;\n}\n.btn-success:hover,\n.btn-success:focus,\n.btn-success.focus,\n.btn-success:active,\n.btn-success.active,\n.open > .dropdown-toggle.btn-success {\n color: #ffffff;\n background-color: #449d44;\n border-color: #398439;\n}\n.btn-success:active,\n.btn-success.active,\n.open > .dropdown-toggle.btn-success {\n background-image: none;\n}\n.btn-success.disabled,\n.btn-success[disabled],\nfieldset[disabled] .btn-success,\n.btn-success.disabled:hover,\n.btn-success[disabled]:hover,\nfieldset[disabled] .btn-success:hover,\n.btn-success.disabled:focus,\n.btn-success[disabled]:focus,\nfieldset[disabled] .btn-success:focus,\n.btn-success.disabled.focus,\n.btn-success[disabled].focus,\nfieldset[disabled] .btn-success.focus,\n.btn-success.disabled:active,\n.btn-success[disabled]:active,\nfieldset[disabled] .btn-success:active,\n.btn-success.disabled.active,\n.btn-success[disabled].active,\nfieldset[disabled] .btn-success.active {\n background-color: #5cb85c;\n border-color: #4cae4c;\n}\n.btn-success .badge {\n color: #5cb85c;\n background-color: #ffffff;\n}\n.btn-info {\n color: #ffffff;\n background-color: #5bc0de;\n border-color: #46b8da;\n}\n.btn-info:hover,\n.btn-info:focus,\n.btn-info.focus,\n.btn-info:active,\n.btn-info.active,\n.open > .dropdown-toggle.btn-info {\n color: #ffffff;\n background-color: #31b0d5;\n border-color: #269abc;\n}\n.btn-info:active,\n.btn-info.active,\n.open > .dropdown-toggle.btn-info {\n background-image: none;\n}\n.btn-info.disabled,\n.btn-info[disabled],\nfieldset[disabled] .btn-info,\n.btn-info.disabled:hover,\n.btn-info[disabled]:hover,\nfieldset[disabled] .btn-info:hover,\n.btn-info.disabled:focus,\n.btn-info[disabled]:focus,\nfieldset[disabled] .btn-info:focus,\n.btn-info.disabled.focus,\n.btn-info[disabled].focus,\nfieldset[disabled] .btn-info.focus,\n.btn-info.disabled:active,\n.btn-info[disabled]:active,\nfieldset[disabled] .btn-info:active,\n.btn-info.disabled.active,\n.btn-info[disabled].active,\nfieldset[disabled] .btn-info.active {\n background-color: #5bc0de;\n border-color: #46b8da;\n}\n.btn-info .badge {\n color: #5bc0de;\n background-color: #ffffff;\n}\n.btn-warning {\n color: #ffffff;\n background-color: #f0ad4e;\n border-color: #eea236;\n}\n.btn-warning:hover,\n.btn-warning:focus,\n.btn-warning.focus,\n.btn-warning:active,\n.btn-warning.active,\n.open > .dropdown-toggle.btn-warning {\n color: #ffffff;\n background-color: #ec971f;\n border-color: #d58512;\n}\n.btn-warning:active,\n.btn-warning.active,\n.open > .dropdown-toggle.btn-warning {\n background-image: none;\n}\n.btn-warning.disabled,\n.btn-warning[disabled],\nfieldset[disabled] .btn-warning,\n.btn-warning.disabled:hover,\n.btn-warning[disabled]:hover,\nfieldset[disabled] .btn-warning:hover,\n.btn-warning.disabled:focus,\n.btn-warning[disabled]:focus,\nfieldset[disabled] .btn-warning:focus,\n.btn-warning.disabled.focus,\n.btn-warning[disabled].focus,\nfieldset[disabled] .btn-warning.focus,\n.btn-warning.disabled:active,\n.btn-warning[disabled]:active,\nfieldset[disabled] .btn-warning:active,\n.btn-warning.disabled.active,\n.btn-warning[disabled].active,\nfieldset[disabled] .btn-warning.active {\n background-color: #f0ad4e;\n border-color: #eea236;\n}\n.btn-warning .badge {\n color: #f0ad4e;\n background-color: #ffffff;\n}\n.btn-danger {\n color: #ffffff;\n background-color: #d9534f;\n border-color: #d43f3a;\n}\n.btn-danger:hover,\n.btn-danger:focus,\n.btn-danger.focus,\n.btn-danger:active,\n.btn-danger.active,\n.open > .dropdown-toggle.btn-danger {\n color: #ffffff;\n background-color: #c9302c;\n border-color: #ac2925;\n}\n.btn-danger:active,\n.btn-danger.active,\n.open > .dropdown-toggle.btn-danger {\n background-image: none;\n}\n.btn-danger.disabled,\n.btn-danger[disabled],\nfieldset[disabled] .btn-danger,\n.btn-danger.disabled:hover,\n.btn-danger[disabled]:hover,\nfieldset[disabled] .btn-danger:hover,\n.btn-danger.disabled:focus,\n.btn-danger[disabled]:focus,\nfieldset[disabled] .btn-danger:focus,\n.btn-danger.disabled.focus,\n.btn-danger[disabled].focus,\nfieldset[disabled] .btn-danger.focus,\n.btn-danger.disabled:active,\n.btn-danger[disabled]:active,\nfieldset[disabled] .btn-danger:active,\n.btn-danger.disabled.active,\n.btn-danger[disabled].active,\nfieldset[disabled] .btn-danger.active {\n background-color: #d9534f;\n border-color: #d43f3a;\n}\n.btn-danger .badge {\n color: #d9534f;\n background-color: #ffffff;\n}\n.btn-link {\n color: #337ab7;\n font-weight: normal;\n border-radius: 0;\n}\n.btn-link,\n.btn-link:active,\n.btn-link.active,\n.btn-link[disabled],\nfieldset[disabled] .btn-link {\n background-color: transparent;\n -webkit-box-shadow: none;\n box-shadow: none;\n}\n.btn-link,\n.btn-link:hover,\n.btn-link:focus,\n.btn-link:active {\n border-color: transparent;\n}\n.btn-link:hover,\n.btn-link:focus {\n color: #23527c;\n text-decoration: underline;\n background-color: transparent;\n}\n.btn-link[disabled]:hover,\nfieldset[disabled] .btn-link:hover,\n.btn-link[disabled]:focus,\nfieldset[disabled] .btn-link:focus {\n color: #777777;\n text-decoration: none;\n}\n.btn-lg,\n.btn-group-lg > .btn {\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\n.btn-sm,\n.btn-group-sm > .btn {\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\n.btn-xs,\n.btn-group-xs > .btn {\n padding: 1px 5px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\n.btn-block {\n display: block;\n width: 100%;\n}\n.btn-block + .btn-block {\n margin-top: 5px;\n}\ninput[type=\"submit\"].btn-block,\ninput[type=\"reset\"].btn-block,\ninput[type=\"button\"].btn-block {\n width: 100%;\n}\n.fade {\n opacity: 0;\n -webkit-transition: opacity 0.15s linear;\n -o-transition: opacity 0.15s linear;\n transition: opacity 0.15s linear;\n}\n.fade.in {\n opacity: 1;\n}\n.collapse {\n display: none;\n}\n.collapse.in {\n display: block;\n}\ntr.collapse.in {\n display: table-row;\n}\ntbody.collapse.in {\n display: table-row-group;\n}\n.collapsing {\n position: relative;\n height: 0;\n overflow: hidden;\n -webkit-transition-property: height, visibility;\n transition-property: height, visibility;\n -webkit-transition-duration: 0.35s;\n transition-duration: 0.35s;\n -webkit-transition-timing-function: ease;\n transition-timing-function: ease;\n}\n.caret {\n display: inline-block;\n width: 0;\n height: 0;\n margin-left: 2px;\n vertical-align: middle;\n border-top: 4px dashed;\n border-right: 4px solid transparent;\n border-left: 4px solid transparent;\n}\n.dropup,\n.dropdown {\n position: relative;\n}\n.dropdown-toggle:focus {\n outline: 0;\n}\n.dropdown-menu {\n position: absolute;\n top: 100%;\n left: 0;\n z-index: 1000;\n display: none;\n float: left;\n min-width: 160px;\n padding: 5px 0;\n margin: 2px 0 0;\n list-style: none;\n font-size: 14px;\n text-align: left;\n background-color: #ffffff;\n border: 1px solid #cccccc;\n border: 1px solid rgba(0, 0, 0, 0.15);\n border-radius: 4px;\n -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);\n box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);\n background-clip: padding-box;\n}\n.dropdown-menu.pull-right {\n right: 0;\n left: auto;\n}\n.dropdown-menu .divider {\n height: 1px;\n margin: 9px 0;\n overflow: hidden;\n background-color: #e5e5e5;\n}\n.dropdown-menu > li > a {\n display: block;\n padding: 3px 20px;\n clear: both;\n font-weight: normal;\n line-height: 1.42857143;\n color: #333333;\n white-space: nowrap;\n}\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n text-decoration: none;\n color: #262626;\n background-color: #f5f5f5;\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n color: #ffffff;\n text-decoration: none;\n outline: 0;\n background-color: #337ab7;\n}\n.dropdown-menu > .disabled > a,\n.dropdown-menu > .disabled > a:hover,\n.dropdown-menu > .disabled > a:focus {\n color: #777777;\n}\n.dropdown-menu > .disabled > a:hover,\n.dropdown-menu > .disabled > a:focus {\n text-decoration: none;\n background-color: transparent;\n background-image: none;\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n cursor: not-allowed;\n}\n.open > .dropdown-menu {\n display: block;\n}\n.open > a {\n outline: 0;\n}\n.dropdown-menu-right {\n left: auto;\n right: 0;\n}\n.dropdown-menu-left {\n left: 0;\n right: auto;\n}\n.dropdown-header {\n display: block;\n padding: 3px 20px;\n font-size: 12px;\n line-height: 1.42857143;\n color: #777777;\n white-space: nowrap;\n}\n.dropdown-backdrop {\n position: fixed;\n left: 0;\n right: 0;\n bottom: 0;\n top: 0;\n z-index: 990;\n}\n.pull-right > .dropdown-menu {\n right: 0;\n left: auto;\n}\n.dropup .caret,\n.navbar-fixed-bottom .dropdown .caret {\n border-top: 0;\n border-bottom: 4px solid;\n content: \"\";\n}\n.dropup .dropdown-menu,\n.navbar-fixed-bottom .dropdown .dropdown-menu {\n top: auto;\n bottom: 100%;\n margin-bottom: 2px;\n}\n@media (min-width: 768px) {\n .navbar-right .dropdown-menu {\n left: auto;\n right: 0;\n }\n .navbar-right .dropdown-menu-left {\n left: 0;\n right: auto;\n }\n}\n.btn-group,\n.btn-group-vertical {\n position: relative;\n display: inline-block;\n vertical-align: middle;\n}\n.btn-group > .btn,\n.btn-group-vertical > .btn {\n position: relative;\n float: left;\n}\n.btn-group > .btn:hover,\n.btn-group-vertical > .btn:hover,\n.btn-group > .btn:focus,\n.btn-group-vertical > .btn:focus,\n.btn-group > .btn:active,\n.btn-group-vertical > .btn:active,\n.btn-group > .btn.active,\n.btn-group-vertical > .btn.active {\n z-index: 2;\n}\n.btn-group .btn + .btn,\n.btn-group .btn + .btn-group,\n.btn-group .btn-group + .btn,\n.btn-group .btn-group + .btn-group {\n margin-left: -1px;\n}\n.btn-toolbar {\n margin-left: -5px;\n}\n.btn-toolbar .btn-group,\n.btn-toolbar .input-group {\n float: left;\n}\n.btn-toolbar > .btn,\n.btn-toolbar > .btn-group,\n.btn-toolbar > .input-group {\n margin-left: 5px;\n}\n.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) {\n border-radius: 0;\n}\n.btn-group > .btn:first-child {\n margin-left: 0;\n}\n.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) {\n border-bottom-right-radius: 0;\n border-top-right-radius: 0;\n}\n.btn-group > .btn:last-child:not(:first-child),\n.btn-group > .dropdown-toggle:not(:first-child) {\n border-bottom-left-radius: 0;\n border-top-left-radius: 0;\n}\n.btn-group > .btn-group {\n float: left;\n}\n.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn {\n border-radius: 0;\n}\n.btn-group > .btn-group:first-child:not(:last-child) > .btn:last-child,\n.btn-group > .btn-group:first-child:not(:last-child) > .dropdown-toggle {\n border-bottom-right-radius: 0;\n border-top-right-radius: 0;\n}\n.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child {\n border-bottom-left-radius: 0;\n border-top-left-radius: 0;\n}\n.btn-group .dropdown-toggle:active,\n.btn-group.open .dropdown-toggle {\n outline: 0;\n}\n.btn-group > .btn + .dropdown-toggle {\n padding-left: 8px;\n padding-right: 8px;\n}\n.btn-group > .btn-lg + .dropdown-toggle {\n padding-left: 12px;\n padding-right: 12px;\n}\n.btn-group.open .dropdown-toggle {\n -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n}\n.btn-group.open .dropdown-toggle.btn-link {\n -webkit-box-shadow: none;\n box-shadow: none;\n}\n.btn .caret {\n margin-left: 0;\n}\n.btn-lg .caret {\n border-width: 5px 5px 0;\n border-bottom-width: 0;\n}\n.dropup .btn-lg .caret {\n border-width: 0 5px 5px;\n}\n.btn-group-vertical > .btn,\n.btn-group-vertical > .btn-group,\n.btn-group-vertical > .btn-group > .btn {\n display: block;\n float: none;\n width: 100%;\n max-width: 100%;\n}\n.btn-group-vertical > .btn-group > .btn {\n float: none;\n}\n.btn-group-vertical > .btn + .btn,\n.btn-group-vertical > .btn + .btn-group,\n.btn-group-vertical > .btn-group + .btn,\n.btn-group-vertical > .btn-group + .btn-group {\n margin-top: -1px;\n margin-left: 0;\n}\n.btn-group-vertical > .btn:not(:first-child):not(:last-child) {\n border-radius: 0;\n}\n.btn-group-vertical > .btn:first-child:not(:last-child) {\n border-top-right-radius: 4px;\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group-vertical > .btn:last-child:not(:first-child) {\n border-bottom-left-radius: 4px;\n border-top-right-radius: 0;\n border-top-left-radius: 0;\n}\n.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn {\n border-radius: 0;\n}\n.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child,\n.btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle {\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child {\n border-top-right-radius: 0;\n border-top-left-radius: 0;\n}\n.btn-group-justified {\n display: table;\n width: 100%;\n table-layout: fixed;\n border-collapse: separate;\n}\n.btn-group-justified > .btn,\n.btn-group-justified > .btn-group {\n float: none;\n display: table-cell;\n width: 1%;\n}\n.btn-group-justified > .btn-group .btn {\n width: 100%;\n}\n.btn-group-justified > .btn-group .dropdown-menu {\n left: auto;\n}\n[data-toggle=\"buttons\"] > .btn input[type=\"radio\"],\n[data-toggle=\"buttons\"] > .btn-group > .btn input[type=\"radio\"],\n[data-toggle=\"buttons\"] > .btn input[type=\"checkbox\"],\n[data-toggle=\"buttons\"] > .btn-group > .btn input[type=\"checkbox\"] {\n position: absolute;\n clip: rect(0, 0, 0, 0);\n pointer-events: none;\n}\n.input-group {\n position: relative;\n display: table;\n border-collapse: separate;\n}\n.input-group[class*=\"col-\"] {\n float: none;\n padding-left: 0;\n padding-right: 0;\n}\n.input-group .form-control {\n position: relative;\n z-index: 2;\n float: left;\n width: 100%;\n margin-bottom: 0;\n}\n.input-group-lg > .form-control,\n.input-group-lg > .input-group-addon,\n.input-group-lg > .input-group-btn > .btn {\n height: 46px;\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\nselect.input-group-lg > .form-control,\nselect.input-group-lg > .input-group-addon,\nselect.input-group-lg > .input-group-btn > .btn {\n height: 46px;\n line-height: 46px;\n}\ntextarea.input-group-lg > .form-control,\ntextarea.input-group-lg > .input-group-addon,\ntextarea.input-group-lg > .input-group-btn > .btn,\nselect[multiple].input-group-lg > .form-control,\nselect[multiple].input-group-lg > .input-group-addon,\nselect[multiple].input-group-lg > .input-group-btn > .btn {\n height: auto;\n}\n.input-group-sm > .form-control,\n.input-group-sm > .input-group-addon,\n.input-group-sm > .input-group-btn > .btn {\n height: 30px;\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\nselect.input-group-sm > .form-control,\nselect.input-group-sm > .input-group-addon,\nselect.input-group-sm > .input-group-btn > .btn {\n height: 30px;\n line-height: 30px;\n}\ntextarea.input-group-sm > .form-control,\ntextarea.input-group-sm > .input-group-addon,\ntextarea.input-group-sm > .input-group-btn > .btn,\nselect[multiple].input-group-sm > .form-control,\nselect[multiple].input-group-sm > .input-group-addon,\nselect[multiple].input-group-sm > .input-group-btn > .btn {\n height: auto;\n}\n.input-group-addon,\n.input-group-btn,\n.input-group .form-control {\n display: table-cell;\n}\n.input-group-addon:not(:first-child):not(:last-child),\n.input-group-btn:not(:first-child):not(:last-child),\n.input-group .form-control:not(:first-child):not(:last-child) {\n border-radius: 0;\n}\n.input-group-addon,\n.input-group-btn {\n width: 1%;\n white-space: nowrap;\n vertical-align: middle;\n}\n.input-group-addon {\n padding: 6px 12px;\n font-size: 14px;\n font-weight: normal;\n line-height: 1;\n color: #555555;\n text-align: center;\n background-color: #eeeeee;\n border: 1px solid #cccccc;\n border-radius: 4px;\n}\n.input-group-addon.input-sm {\n padding: 5px 10px;\n font-size: 12px;\n border-radius: 3px;\n}\n.input-group-addon.input-lg {\n padding: 10px 16px;\n font-size: 18px;\n border-radius: 6px;\n}\n.input-group-addon input[type=\"radio\"],\n.input-group-addon input[type=\"checkbox\"] {\n margin-top: 0;\n}\n.input-group .form-control:first-child,\n.input-group-addon:first-child,\n.input-group-btn:first-child > .btn,\n.input-group-btn:first-child > .btn-group > .btn,\n.input-group-btn:first-child > .dropdown-toggle,\n.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle),\n.input-group-btn:last-child > .btn-group:not(:last-child) > .btn {\n border-bottom-right-radius: 0;\n border-top-right-radius: 0;\n}\n.input-group-addon:first-child {\n border-right: 0;\n}\n.input-group .form-control:last-child,\n.input-group-addon:last-child,\n.input-group-btn:last-child > .btn,\n.input-group-btn:last-child > .btn-group > .btn,\n.input-group-btn:last-child > .dropdown-toggle,\n.input-group-btn:first-child > .btn:not(:first-child),\n.input-group-btn:first-child > .btn-group:not(:first-child) > .btn {\n border-bottom-left-radius: 0;\n border-top-left-radius: 0;\n}\n.input-group-addon:last-child {\n border-left: 0;\n}\n.input-group-btn {\n position: relative;\n font-size: 0;\n white-space: nowrap;\n}\n.input-group-btn > .btn {\n position: relative;\n}\n.input-group-btn > .btn + .btn {\n margin-left: -1px;\n}\n.input-group-btn > .btn:hover,\n.input-group-btn > .btn:focus,\n.input-group-btn > .btn:active {\n z-index: 2;\n}\n.input-group-btn:first-child > .btn,\n.input-group-btn:first-child > .btn-group {\n margin-right: -1px;\n}\n.input-group-btn:last-child > .btn,\n.input-group-btn:last-child > .btn-group {\n margin-left: -1px;\n}\n.nav {\n margin-bottom: 0;\n padding-left: 0;\n list-style: none;\n}\n.nav > li {\n position: relative;\n display: block;\n}\n.nav > li > a {\n position: relative;\n display: block;\n padding: 10px 15px;\n}\n.nav > li > a:hover,\n.nav > li > a:focus {\n text-decoration: none;\n background-color: #eeeeee;\n}\n.nav > li.disabled > a {\n color: #777777;\n}\n.nav > li.disabled > a:hover,\n.nav > li.disabled > a:focus {\n color: #777777;\n text-decoration: none;\n background-color: transparent;\n cursor: not-allowed;\n}\n.nav .open > a,\n.nav .open > a:hover,\n.nav .open > a:focus {\n background-color: #eeeeee;\n border-color: #337ab7;\n}\n.nav .nav-divider {\n height: 1px;\n margin: 9px 0;\n overflow: hidden;\n background-color: #e5e5e5;\n}\n.nav > li > a > img {\n max-width: none;\n}\n.nav-tabs {\n border-bottom: 1px solid #dddddd;\n}\n.nav-tabs > li {\n float: left;\n margin-bottom: -1px;\n}\n.nav-tabs > li > a {\n margin-right: 2px;\n line-height: 1.42857143;\n border: 1px solid transparent;\n border-radius: 4px 4px 0 0;\n}\n.nav-tabs > li > a:hover {\n border-color: #eeeeee #eeeeee #dddddd;\n}\n.nav-tabs > li.active > a,\n.nav-tabs > li.active > a:hover,\n.nav-tabs > li.active > a:focus {\n color: #555555;\n background-color: #ffffff;\n border: 1px solid #dddddd;\n border-bottom-color: transparent;\n cursor: default;\n}\n.nav-tabs.nav-justified {\n width: 100%;\n border-bottom: 0;\n}\n.nav-tabs.nav-justified > li {\n float: none;\n}\n.nav-tabs.nav-justified > li > a {\n text-align: center;\n margin-bottom: 5px;\n}\n.nav-tabs.nav-justified > .dropdown .dropdown-menu {\n top: auto;\n left: auto;\n}\n@media (min-width: 768px) {\n .nav-tabs.nav-justified > li {\n display: table-cell;\n width: 1%;\n }\n .nav-tabs.nav-justified > li > a {\n margin-bottom: 0;\n }\n}\n.nav-tabs.nav-justified > li > a {\n margin-right: 0;\n border-radius: 4px;\n}\n.nav-tabs.nav-justified > .active > a,\n.nav-tabs.nav-justified > .active > a:hover,\n.nav-tabs.nav-justified > .active > a:focus {\n border: 1px solid #dddddd;\n}\n@media (min-width: 768px) {\n .nav-tabs.nav-justified > li > a {\n border-bottom: 1px solid #dddddd;\n border-radius: 4px 4px 0 0;\n }\n .nav-tabs.nav-justified > .active > a,\n .nav-tabs.nav-justified > .active > a:hover,\n .nav-tabs.nav-justified > .active > a:focus {\n border-bottom-color: #ffffff;\n }\n}\n.nav-pills > li {\n float: left;\n}\n.nav-pills > li > a {\n border-radius: 4px;\n}\n.nav-pills > li + li {\n margin-left: 2px;\n}\n.nav-pills > li.active > a,\n.nav-pills > li.active > a:hover,\n.nav-pills > li.active > a:focus {\n color: #ffffff;\n background-color: #337ab7;\n}\n.nav-stacked > li {\n float: none;\n}\n.nav-stacked > li + li {\n margin-top: 2px;\n margin-left: 0;\n}\n.nav-justified {\n width: 100%;\n}\n.nav-justified > li {\n float: none;\n}\n.nav-justified > li > a {\n text-align: center;\n margin-bottom: 5px;\n}\n.nav-justified > .dropdown .dropdown-menu {\n top: auto;\n left: auto;\n}\n@media (min-width: 768px) {\n .nav-justified > li {\n display: table-cell;\n width: 1%;\n }\n .nav-justified > li > a {\n margin-bottom: 0;\n }\n}\n.nav-tabs-justified {\n border-bottom: 0;\n}\n.nav-tabs-justified > li > a {\n margin-right: 0;\n border-radius: 4px;\n}\n.nav-tabs-justified > .active > a,\n.nav-tabs-justified > .active > a:hover,\n.nav-tabs-justified > .active > a:focus {\n border: 1px solid #dddddd;\n}\n@media (min-width: 768px) {\n .nav-tabs-justified > li > a {\n border-bottom: 1px solid #dddddd;\n border-radius: 4px 4px 0 0;\n }\n .nav-tabs-justified > .active > a,\n .nav-tabs-justified > .active > a:hover,\n .nav-tabs-justified > .active > a:focus {\n border-bottom-color: #ffffff;\n }\n}\n.tab-content > .tab-pane {\n display: none;\n}\n.tab-content > .active {\n display: block;\n}\n.nav-tabs .dropdown-menu {\n margin-top: -1px;\n border-top-right-radius: 0;\n border-top-left-radius: 0;\n}\n.navbar {\n position: relative;\n min-height: 50px;\n margin-bottom: 20px;\n border: 1px solid transparent;\n}\n@media (min-width: 768px) {\n .navbar {\n border-radius: 4px;\n }\n}\n@media (min-width: 768px) {\n .navbar-header {\n float: left;\n }\n}\n.navbar-collapse {\n overflow-x: visible;\n padding-right: 15px;\n padding-left: 15px;\n border-top: 1px solid transparent;\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);\n -webkit-overflow-scrolling: touch;\n}\n.navbar-collapse.in {\n overflow-y: auto;\n}\n@media (min-width: 768px) {\n .navbar-collapse {\n width: auto;\n border-top: 0;\n box-shadow: none;\n }\n .navbar-collapse.collapse {\n display: block !important;\n height: auto !important;\n padding-bottom: 0;\n overflow: visible !important;\n }\n .navbar-collapse.in {\n overflow-y: visible;\n }\n .navbar-fixed-top .navbar-collapse,\n .navbar-static-top .navbar-collapse,\n .navbar-fixed-bottom .navbar-collapse {\n padding-left: 0;\n padding-right: 0;\n }\n}\n.navbar-fixed-top .navbar-collapse,\n.navbar-fixed-bottom .navbar-collapse {\n max-height: 340px;\n}\n@media (max-device-width: 480px) and (orientation: landscape) {\n .navbar-fixed-top .navbar-collapse,\n .navbar-fixed-bottom .navbar-collapse {\n max-height: 200px;\n }\n}\n.container > .navbar-header,\n.container-fluid > .navbar-header,\n.container > .navbar-collapse,\n.container-fluid > .navbar-collapse {\n margin-right: -15px;\n margin-left: -15px;\n}\n@media (min-width: 768px) {\n .container > .navbar-header,\n .container-fluid > .navbar-header,\n .container > .navbar-collapse,\n .container-fluid > .navbar-collapse {\n margin-right: 0;\n margin-left: 0;\n }\n}\n.navbar-static-top {\n z-index: 1000;\n border-width: 0 0 1px;\n}\n@media (min-width: 768px) {\n .navbar-static-top {\n border-radius: 0;\n }\n}\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n position: fixed;\n right: 0;\n left: 0;\n z-index: 1030;\n}\n@media (min-width: 768px) {\n .navbar-fixed-top,\n .navbar-fixed-bottom {\n border-radius: 0;\n }\n}\n.navbar-fixed-top {\n top: 0;\n border-width: 0 0 1px;\n}\n.navbar-fixed-bottom {\n bottom: 0;\n margin-bottom: 0;\n border-width: 1px 0 0;\n}\n.navbar-brand {\n float: left;\n padding: 15px 15px;\n font-size: 18px;\n line-height: 20px;\n height: 50px;\n}\n.navbar-brand:hover,\n.navbar-brand:focus {\n text-decoration: none;\n}\n.navbar-brand > img {\n display: block;\n}\n@media (min-width: 768px) {\n .navbar > .container .navbar-brand,\n .navbar > .container-fluid .navbar-brand {\n margin-left: -15px;\n }\n}\n.navbar-toggle {\n position: relative;\n float: right;\n margin-right: 15px;\n padding: 9px 10px;\n margin-top: 8px;\n margin-bottom: 8px;\n background-color: transparent;\n background-image: none;\n border: 1px solid transparent;\n border-radius: 4px;\n}\n.navbar-toggle:focus {\n outline: 0;\n}\n.navbar-toggle .icon-bar {\n display: block;\n width: 22px;\n height: 2px;\n border-radius: 1px;\n}\n.navbar-toggle .icon-bar + .icon-bar {\n margin-top: 4px;\n}\n@media (min-width: 768px) {\n .navbar-toggle {\n display: none;\n }\n}\n.navbar-nav {\n margin: 7.5px -15px;\n}\n.navbar-nav > li > a {\n padding-top: 10px;\n padding-bottom: 10px;\n line-height: 20px;\n}\n@media (max-width: 767px) {\n .navbar-nav .open .dropdown-menu {\n position: static;\n float: none;\n width: auto;\n margin-top: 0;\n background-color: transparent;\n border: 0;\n box-shadow: none;\n }\n .navbar-nav .open .dropdown-menu > li > a,\n .navbar-nav .open .dropdown-menu .dropdown-header {\n padding: 5px 15px 5px 25px;\n }\n .navbar-nav .open .dropdown-menu > li > a {\n line-height: 20px;\n }\n .navbar-nav .open .dropdown-menu > li > a:hover,\n .navbar-nav .open .dropdown-menu > li > a:focus {\n background-image: none;\n }\n}\n@media (min-width: 768px) {\n .navbar-nav {\n float: left;\n margin: 0;\n }\n .navbar-nav > li {\n float: left;\n }\n .navbar-nav > li > a {\n padding-top: 15px;\n padding-bottom: 15px;\n }\n}\n.navbar-form {\n margin-left: -15px;\n margin-right: -15px;\n padding: 10px 15px;\n border-top: 1px solid transparent;\n border-bottom: 1px solid transparent;\n -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);\n margin-top: 8px;\n margin-bottom: 8px;\n}\n@media (min-width: 768px) {\n .navbar-form .form-group {\n display: inline-block;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .navbar-form .form-control {\n display: inline-block;\n width: auto;\n vertical-align: middle;\n }\n .navbar-form .form-control-static {\n display: inline-block;\n }\n .navbar-form .input-group {\n display: inline-table;\n vertical-align: middle;\n }\n .navbar-form .input-group .input-group-addon,\n .navbar-form .input-group .input-group-btn,\n .navbar-form .input-group .form-control {\n width: auto;\n }\n .navbar-form .input-group > .form-control {\n width: 100%;\n }\n .navbar-form .control-label {\n margin-bottom: 0;\n vertical-align: middle;\n }\n .navbar-form .radio,\n .navbar-form .checkbox {\n display: inline-block;\n margin-top: 0;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .navbar-form .radio label,\n .navbar-form .checkbox label {\n padding-left: 0;\n }\n .navbar-form .radio input[type=\"radio\"],\n .navbar-form .checkbox input[type=\"checkbox\"] {\n position: relative;\n margin-left: 0;\n }\n .navbar-form .has-feedback .form-control-feedback {\n top: 0;\n }\n}\n@media (max-width: 767px) {\n .navbar-form .form-group {\n margin-bottom: 5px;\n }\n .navbar-form .form-group:last-child {\n margin-bottom: 0;\n }\n}\n@media (min-width: 768px) {\n .navbar-form {\n width: auto;\n border: 0;\n margin-left: 0;\n margin-right: 0;\n padding-top: 0;\n padding-bottom: 0;\n -webkit-box-shadow: none;\n box-shadow: none;\n }\n}\n.navbar-nav > li > .dropdown-menu {\n margin-top: 0;\n border-top-right-radius: 0;\n border-top-left-radius: 0;\n}\n.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu {\n margin-bottom: 0;\n border-top-right-radius: 4px;\n border-top-left-radius: 4px;\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.navbar-btn {\n margin-top: 8px;\n margin-bottom: 8px;\n}\n.navbar-btn.btn-sm {\n margin-top: 10px;\n margin-bottom: 10px;\n}\n.navbar-btn.btn-xs {\n margin-top: 14px;\n margin-bottom: 14px;\n}\n.navbar-text {\n margin-top: 15px;\n margin-bottom: 15px;\n}\n@media (min-width: 768px) {\n .navbar-text {\n float: left;\n margin-left: 15px;\n margin-right: 15px;\n }\n}\n@media (min-width: 768px) {\n .navbar-left {\n float: left !important;\n }\n .navbar-right {\n float: right !important;\n margin-right: -15px;\n }\n .navbar-right ~ .navbar-right {\n margin-right: 0;\n }\n}\n.navbar-default {\n background-color: #f8f8f8;\n border-color: #e7e7e7;\n}\n.navbar-default .navbar-brand {\n color: #777777;\n}\n.navbar-default .navbar-brand:hover,\n.navbar-default .navbar-brand:focus {\n color: #5e5e5e;\n background-color: transparent;\n}\n.navbar-default .navbar-text {\n color: #777777;\n}\n.navbar-default .navbar-nav > li > a {\n color: #777777;\n}\n.navbar-default .navbar-nav > li > a:hover,\n.navbar-default .navbar-nav > li > a:focus {\n color: #333333;\n background-color: transparent;\n}\n.navbar-default .navbar-nav > .active > a,\n.navbar-default .navbar-nav > .active > a:hover,\n.navbar-default .navbar-nav > .active > a:focus {\n color: #555555;\n background-color: #e7e7e7;\n}\n.navbar-default .navbar-nav > .disabled > a,\n.navbar-default .navbar-nav > .disabled > a:hover,\n.navbar-default .navbar-nav > .disabled > a:focus {\n color: #cccccc;\n background-color: transparent;\n}\n.navbar-default .navbar-toggle {\n border-color: #dddddd;\n}\n.navbar-default .navbar-toggle:hover,\n.navbar-default .navbar-toggle:focus {\n background-color: #dddddd;\n}\n.navbar-default .navbar-toggle .icon-bar {\n background-color: #888888;\n}\n.navbar-default .navbar-collapse,\n.navbar-default .navbar-form {\n border-color: #e7e7e7;\n}\n.navbar-default .navbar-nav > .open > a,\n.navbar-default .navbar-nav > .open > a:hover,\n.navbar-default .navbar-nav > .open > a:focus {\n background-color: #e7e7e7;\n color: #555555;\n}\n@media (max-width: 767px) {\n .navbar-default .navbar-nav .open .dropdown-menu > li > a {\n color: #777777;\n }\n .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover,\n .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus {\n color: #333333;\n background-color: transparent;\n }\n .navbar-default .navbar-nav .open .dropdown-menu > .active > a,\n .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover,\n .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus {\n color: #555555;\n background-color: #e7e7e7;\n }\n .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a,\n .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover,\n .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus {\n color: #cccccc;\n background-color: transparent;\n }\n}\n.navbar-default .navbar-link {\n color: #777777;\n}\n.navbar-default .navbar-link:hover {\n color: #333333;\n}\n.navbar-default .btn-link {\n color: #777777;\n}\n.navbar-default .btn-link:hover,\n.navbar-default .btn-link:focus {\n color: #333333;\n}\n.navbar-default .btn-link[disabled]:hover,\nfieldset[disabled] .navbar-default .btn-link:hover,\n.navbar-default .btn-link[disabled]:focus,\nfieldset[disabled] .navbar-default .btn-link:focus {\n color: #cccccc;\n}\n.navbar-inverse {\n background-color: #222222;\n border-color: #080808;\n}\n.navbar-inverse .navbar-brand {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-brand:hover,\n.navbar-inverse .navbar-brand:focus {\n color: #ffffff;\n background-color: transparent;\n}\n.navbar-inverse .navbar-text {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-nav > li > a {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-nav > li > a:hover,\n.navbar-inverse .navbar-nav > li > a:focus {\n color: #ffffff;\n background-color: transparent;\n}\n.navbar-inverse .navbar-nav > .active > a,\n.navbar-inverse .navbar-nav > .active > a:hover,\n.navbar-inverse .navbar-nav > .active > a:focus {\n color: #ffffff;\n background-color: #080808;\n}\n.navbar-inverse .navbar-nav > .disabled > a,\n.navbar-inverse .navbar-nav > .disabled > a:hover,\n.navbar-inverse .navbar-nav > .disabled > a:focus {\n color: #444444;\n background-color: transparent;\n}\n.navbar-inverse .navbar-toggle {\n border-color: #333333;\n}\n.navbar-inverse .navbar-toggle:hover,\n.navbar-inverse .navbar-toggle:focus {\n background-color: #333333;\n}\n.navbar-inverse .navbar-toggle .icon-bar {\n background-color: #ffffff;\n}\n.navbar-inverse .navbar-collapse,\n.navbar-inverse .navbar-form {\n border-color: #101010;\n}\n.navbar-inverse .navbar-nav > .open > a,\n.navbar-inverse .navbar-nav > .open > a:hover,\n.navbar-inverse .navbar-nav > .open > a:focus {\n background-color: #080808;\n color: #ffffff;\n}\n@media (max-width: 767px) {\n .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header {\n border-color: #080808;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu .divider {\n background-color: #080808;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > li > a {\n color: #9d9d9d;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover,\n .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus {\n color: #ffffff;\n background-color: transparent;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus {\n color: #ffffff;\n background-color: #080808;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus {\n color: #444444;\n background-color: transparent;\n }\n}\n.navbar-inverse .navbar-link {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-link:hover {\n color: #ffffff;\n}\n.navbar-inverse .btn-link {\n color: #9d9d9d;\n}\n.navbar-inverse .btn-link:hover,\n.navbar-inverse .btn-link:focus {\n color: #ffffff;\n}\n.navbar-inverse .btn-link[disabled]:hover,\nfieldset[disabled] .navbar-inverse .btn-link:hover,\n.navbar-inverse .btn-link[disabled]:focus,\nfieldset[disabled] .navbar-inverse .btn-link:focus {\n color: #444444;\n}\n.breadcrumb {\n padding: 8px 15px;\n margin-bottom: 20px;\n list-style: none;\n background-color: #f5f5f5;\n border-radius: 4px;\n}\n.breadcrumb > li {\n display: inline-block;\n}\n.breadcrumb > li + li:before {\n content: \"/\\00a0\";\n padding: 0 5px;\n color: #cccccc;\n}\n.breadcrumb > .active {\n color: #777777;\n}\n.pagination {\n display: inline-block;\n padding-left: 0;\n margin: 20px 0;\n border-radius: 4px;\n}\n.pagination > li {\n display: inline;\n}\n.pagination > li > a,\n.pagination > li > span {\n position: relative;\n float: left;\n padding: 6px 12px;\n line-height: 1.42857143;\n text-decoration: none;\n color: #337ab7;\n background-color: #ffffff;\n border: 1px solid #dddddd;\n margin-left: -1px;\n}\n.pagination > li:first-child > a,\n.pagination > li:first-child > span {\n margin-left: 0;\n border-bottom-left-radius: 4px;\n border-top-left-radius: 4px;\n}\n.pagination > li:last-child > a,\n.pagination > li:last-child > span {\n border-bottom-right-radius: 4px;\n border-top-right-radius: 4px;\n}\n.pagination > li > a:hover,\n.pagination > li > span:hover,\n.pagination > li > a:focus,\n.pagination > li > span:focus {\n color: #23527c;\n background-color: #eeeeee;\n border-color: #dddddd;\n}\n.pagination > .active > a,\n.pagination > .active > span,\n.pagination > .active > a:hover,\n.pagination > .active > span:hover,\n.pagination > .active > a:focus,\n.pagination > .active > span:focus {\n z-index: 2;\n color: #ffffff;\n background-color: #337ab7;\n border-color: #337ab7;\n cursor: default;\n}\n.pagination > .disabled > span,\n.pagination > .disabled > span:hover,\n.pagination > .disabled > span:focus,\n.pagination > .disabled > a,\n.pagination > .disabled > a:hover,\n.pagination > .disabled > a:focus {\n color: #777777;\n background-color: #ffffff;\n border-color: #dddddd;\n cursor: not-allowed;\n}\n.pagination-lg > li > a,\n.pagination-lg > li > span {\n padding: 10px 16px;\n font-size: 18px;\n}\n.pagination-lg > li:first-child > a,\n.pagination-lg > li:first-child > span {\n border-bottom-left-radius: 6px;\n border-top-left-radius: 6px;\n}\n.pagination-lg > li:last-child > a,\n.pagination-lg > li:last-child > span {\n border-bottom-right-radius: 6px;\n border-top-right-radius: 6px;\n}\n.pagination-sm > li > a,\n.pagination-sm > li > span {\n padding: 5px 10px;\n font-size: 12px;\n}\n.pagination-sm > li:first-child > a,\n.pagination-sm > li:first-child > span {\n border-bottom-left-radius: 3px;\n border-top-left-radius: 3px;\n}\n.pagination-sm > li:last-child > a,\n.pagination-sm > li:last-child > span {\n border-bottom-right-radius: 3px;\n border-top-right-radius: 3px;\n}\n.pager {\n padding-left: 0;\n margin: 20px 0;\n list-style: none;\n text-align: center;\n}\n.pager li {\n display: inline;\n}\n.pager li > a,\n.pager li > span {\n display: inline-block;\n padding: 5px 14px;\n background-color: #ffffff;\n border: 1px solid #dddddd;\n border-radius: 15px;\n}\n.pager li > a:hover,\n.pager li > a:focus {\n text-decoration: none;\n background-color: #eeeeee;\n}\n.pager .next > a,\n.pager .next > span {\n float: right;\n}\n.pager .previous > a,\n.pager .previous > span {\n float: left;\n}\n.pager .disabled > a,\n.pager .disabled > a:hover,\n.pager .disabled > a:focus,\n.pager .disabled > span {\n color: #777777;\n background-color: #ffffff;\n cursor: not-allowed;\n}\n.label {\n display: inline;\n padding: .2em .6em .3em;\n font-size: 75%;\n font-weight: bold;\n line-height: 1;\n color: #ffffff;\n text-align: center;\n white-space: nowrap;\n vertical-align: baseline;\n border-radius: .25em;\n}\na.label:hover,\na.label:focus {\n color: #ffffff;\n text-decoration: none;\n cursor: pointer;\n}\n.label:empty {\n display: none;\n}\n.btn .label {\n position: relative;\n top: -1px;\n}\n.label-default {\n background-color: #777777;\n}\n.label-default[href]:hover,\n.label-default[href]:focus {\n background-color: #5e5e5e;\n}\n.label-primary {\n background-color: #337ab7;\n}\n.label-primary[href]:hover,\n.label-primary[href]:focus {\n background-color: #286090;\n}\n.label-success {\n background-color: #5cb85c;\n}\n.label-success[href]:hover,\n.label-success[href]:focus {\n background-color: #449d44;\n}\n.label-info {\n background-color: #5bc0de;\n}\n.label-info[href]:hover,\n.label-info[href]:focus {\n background-color: #31b0d5;\n}\n.label-warning {\n background-color: #f0ad4e;\n}\n.label-warning[href]:hover,\n.label-warning[href]:focus {\n background-color: #ec971f;\n}\n.label-danger {\n background-color: #d9534f;\n}\n.label-danger[href]:hover,\n.label-danger[href]:focus {\n background-color: #c9302c;\n}\n.badge {\n display: inline-block;\n min-width: 10px;\n padding: 3px 7px;\n font-size: 12px;\n font-weight: bold;\n color: #ffffff;\n line-height: 1;\n vertical-align: baseline;\n white-space: nowrap;\n text-align: center;\n background-color: #777777;\n border-radius: 10px;\n}\n.badge:empty {\n display: none;\n}\n.btn .badge {\n position: relative;\n top: -1px;\n}\n.btn-xs .badge,\n.btn-group-xs > .btn .badge {\n top: 0;\n padding: 1px 5px;\n}\na.badge:hover,\na.badge:focus {\n color: #ffffff;\n text-decoration: none;\n cursor: pointer;\n}\n.list-group-item.active > .badge,\n.nav-pills > .active > a > .badge {\n color: #337ab7;\n background-color: #ffffff;\n}\n.list-group-item > .badge {\n float: right;\n}\n.list-group-item > .badge + .badge {\n margin-right: 5px;\n}\n.nav-pills > li > a > .badge {\n margin-left: 3px;\n}\n.jumbotron {\n padding: 30px 15px;\n margin-bottom: 30px;\n color: inherit;\n background-color: #eeeeee;\n}\n.jumbotron h1,\n.jumbotron .h1 {\n color: inherit;\n}\n.jumbotron p {\n margin-bottom: 15px;\n font-size: 21px;\n font-weight: 200;\n}\n.jumbotron > hr {\n border-top-color: #d5d5d5;\n}\n.container .jumbotron,\n.container-fluid .jumbotron {\n border-radius: 6px;\n}\n.jumbotron .container {\n max-width: 100%;\n}\n@media screen and (min-width: 768px) {\n .jumbotron {\n padding: 48px 0;\n }\n .container .jumbotron,\n .container-fluid .jumbotron {\n padding-left: 60px;\n padding-right: 60px;\n }\n .jumbotron h1,\n .jumbotron .h1 {\n font-size: 63px;\n }\n}\n.thumbnail {\n display: block;\n padding: 4px;\n margin-bottom: 20px;\n line-height: 1.42857143;\n background-color: #ffffff;\n border: 1px solid #dddddd;\n border-radius: 4px;\n -webkit-transition: border 0.2s ease-in-out;\n -o-transition: border 0.2s ease-in-out;\n transition: border 0.2s ease-in-out;\n}\n.thumbnail > img,\n.thumbnail a > img {\n margin-left: auto;\n margin-right: auto;\n}\na.thumbnail:hover,\na.thumbnail:focus,\na.thumbnail.active {\n border-color: #337ab7;\n}\n.thumbnail .caption {\n padding: 9px;\n color: #333333;\n}\n.alert {\n padding: 15px;\n margin-bottom: 20px;\n border: 1px solid transparent;\n border-radius: 4px;\n}\n.alert h4 {\n margin-top: 0;\n color: inherit;\n}\n.alert .alert-link {\n font-weight: bold;\n}\n.alert > p,\n.alert > ul {\n margin-bottom: 0;\n}\n.alert > p + p {\n margin-top: 5px;\n}\n.alert-dismissable,\n.alert-dismissible {\n padding-right: 35px;\n}\n.alert-dismissable .close,\n.alert-dismissible .close {\n position: relative;\n top: -2px;\n right: -21px;\n color: inherit;\n}\n.alert-success {\n background-color: #dff0d8;\n border-color: #d6e9c6;\n color: #3c763d;\n}\n.alert-success hr {\n border-top-color: #c9e2b3;\n}\n.alert-success .alert-link {\n color: #2b542c;\n}\n.alert-info {\n background-color: #d9edf7;\n border-color: #bce8f1;\n color: #31708f;\n}\n.alert-info hr {\n border-top-color: #a6e1ec;\n}\n.alert-info .alert-link {\n color: #245269;\n}\n.alert-warning {\n background-color: #fcf8e3;\n border-color: #faebcc;\n color: #8a6d3b;\n}\n.alert-warning hr {\n border-top-color: #f7e1b5;\n}\n.alert-warning .alert-link {\n color: #66512c;\n}\n.alert-danger {\n background-color: #f2dede;\n border-color: #ebccd1;\n color: #a94442;\n}\n.alert-danger hr {\n border-top-color: #e4b9c0;\n}\n.alert-danger .alert-link {\n color: #843534;\n}\n@-webkit-keyframes progress-bar-stripes {\n from {\n background-position: 40px 0;\n }\n to {\n background-position: 0 0;\n }\n}\n@keyframes progress-bar-stripes {\n from {\n background-position: 40px 0;\n }\n to {\n background-position: 0 0;\n }\n}\n.progress {\n overflow: hidden;\n height: 20px;\n margin-bottom: 20px;\n background-color: #f5f5f5;\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);\n box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);\n}\n.progress-bar {\n float: left;\n width: 0%;\n height: 100%;\n font-size: 12px;\n line-height: 20px;\n color: #ffffff;\n text-align: center;\n background-color: #337ab7;\n -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);\n box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);\n -webkit-transition: width 0.6s ease;\n -o-transition: width 0.6s ease;\n transition: width 0.6s ease;\n}\n.progress-striped .progress-bar,\n.progress-bar-striped {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-size: 40px 40px;\n}\n.progress.active .progress-bar,\n.progress-bar.active {\n -webkit-animation: progress-bar-stripes 2s linear infinite;\n -o-animation: progress-bar-stripes 2s linear infinite;\n animation: progress-bar-stripes 2s linear infinite;\n}\n.progress-bar-success {\n background-color: #5cb85c;\n}\n.progress-striped .progress-bar-success {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-info {\n background-color: #5bc0de;\n}\n.progress-striped .progress-bar-info {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-warning {\n background-color: #f0ad4e;\n}\n.progress-striped .progress-bar-warning {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-danger {\n background-color: #d9534f;\n}\n.progress-striped .progress-bar-danger {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.media {\n margin-top: 15px;\n}\n.media:first-child {\n margin-top: 0;\n}\n.media,\n.media-body {\n zoom: 1;\n overflow: hidden;\n}\n.media-body {\n width: 10000px;\n}\n.media-object {\n display: block;\n}\n.media-right,\n.media > .pull-right {\n padding-left: 10px;\n}\n.media-left,\n.media > .pull-left {\n padding-right: 10px;\n}\n.media-left,\n.media-right,\n.media-body {\n display: table-cell;\n vertical-align: top;\n}\n.media-middle {\n vertical-align: middle;\n}\n.media-bottom {\n vertical-align: bottom;\n}\n.media-heading {\n margin-top: 0;\n margin-bottom: 5px;\n}\n.media-list {\n padding-left: 0;\n list-style: none;\n}\n.list-group {\n margin-bottom: 20px;\n padding-left: 0;\n}\n.list-group-item {\n position: relative;\n display: block;\n padding: 10px 15px;\n margin-bottom: -1px;\n background-color: #ffffff;\n border: 1px solid #dddddd;\n}\n.list-group-item:first-child {\n border-top-right-radius: 4px;\n border-top-left-radius: 4px;\n}\n.list-group-item:last-child {\n margin-bottom: 0;\n border-bottom-right-radius: 4px;\n border-bottom-left-radius: 4px;\n}\na.list-group-item {\n color: #555555;\n}\na.list-group-item .list-group-item-heading {\n color: #333333;\n}\na.list-group-item:hover,\na.list-group-item:focus {\n text-decoration: none;\n color: #555555;\n background-color: #f5f5f5;\n}\n.list-group-item.disabled,\n.list-group-item.disabled:hover,\n.list-group-item.disabled:focus {\n background-color: #eeeeee;\n color: #777777;\n cursor: not-allowed;\n}\n.list-group-item.disabled .list-group-item-heading,\n.list-group-item.disabled:hover .list-group-item-heading,\n.list-group-item.disabled:focus .list-group-item-heading {\n color: inherit;\n}\n.list-group-item.disabled .list-group-item-text,\n.list-group-item.disabled:hover .list-group-item-text,\n.list-group-item.disabled:focus .list-group-item-text {\n color: #777777;\n}\n.list-group-item.active,\n.list-group-item.active:hover,\n.list-group-item.active:focus {\n z-index: 2;\n color: #ffffff;\n background-color: #337ab7;\n border-color: #337ab7;\n}\n.list-group-item.active .list-group-item-heading,\n.list-group-item.active:hover .list-group-item-heading,\n.list-group-item.active:focus .list-group-item-heading,\n.list-group-item.active .list-group-item-heading > small,\n.list-group-item.active:hover .list-group-item-heading > small,\n.list-group-item.active:focus .list-group-item-heading > small,\n.list-group-item.active .list-group-item-heading > .small,\n.list-group-item.active:hover .list-group-item-heading > .small,\n.list-group-item.active:focus .list-group-item-heading > .small {\n color: inherit;\n}\n.list-group-item.active .list-group-item-text,\n.list-group-item.active:hover .list-group-item-text,\n.list-group-item.active:focus .list-group-item-text {\n color: #c7ddef;\n}\n.list-group-item-success {\n color: #3c763d;\n background-color: #dff0d8;\n}\na.list-group-item-success {\n color: #3c763d;\n}\na.list-group-item-success .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-success:hover,\na.list-group-item-success:focus {\n color: #3c763d;\n background-color: #d0e9c6;\n}\na.list-group-item-success.active,\na.list-group-item-success.active:hover,\na.list-group-item-success.active:focus {\n color: #fff;\n background-color: #3c763d;\n border-color: #3c763d;\n}\n.list-group-item-info {\n color: #31708f;\n background-color: #d9edf7;\n}\na.list-group-item-info {\n color: #31708f;\n}\na.list-group-item-info .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-info:hover,\na.list-group-item-info:focus {\n color: #31708f;\n background-color: #c4e3f3;\n}\na.list-group-item-info.active,\na.list-group-item-info.active:hover,\na.list-group-item-info.active:focus {\n color: #fff;\n background-color: #31708f;\n border-color: #31708f;\n}\n.list-group-item-warning {\n color: #8a6d3b;\n background-color: #fcf8e3;\n}\na.list-group-item-warning {\n color: #8a6d3b;\n}\na.list-group-item-warning .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-warning:hover,\na.list-group-item-warning:focus {\n color: #8a6d3b;\n background-color: #faf2cc;\n}\na.list-group-item-warning.active,\na.list-group-item-warning.active:hover,\na.list-group-item-warning.active:focus {\n color: #fff;\n background-color: #8a6d3b;\n border-color: #8a6d3b;\n}\n.list-group-item-danger {\n color: #a94442;\n background-color: #f2dede;\n}\na.list-group-item-danger {\n color: #a94442;\n}\na.list-group-item-danger .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-danger:hover,\na.list-group-item-danger:focus {\n color: #a94442;\n background-color: #ebcccc;\n}\na.list-group-item-danger.active,\na.list-group-item-danger.active:hover,\na.list-group-item-danger.active:focus {\n color: #fff;\n background-color: #a94442;\n border-color: #a94442;\n}\n.list-group-item-heading {\n margin-top: 0;\n margin-bottom: 5px;\n}\n.list-group-item-text {\n margin-bottom: 0;\n line-height: 1.3;\n}\n.panel {\n margin-bottom: 20px;\n background-color: #ffffff;\n border: 1px solid transparent;\n border-radius: 4px;\n -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);\n box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);\n}\n.panel-body {\n padding: 15px;\n}\n.panel-heading {\n padding: 10px 15px;\n border-bottom: 1px solid transparent;\n border-top-right-radius: 3px;\n border-top-left-radius: 3px;\n}\n.panel-heading > .dropdown .dropdown-toggle {\n color: inherit;\n}\n.panel-title {\n margin-top: 0;\n margin-bottom: 0;\n font-size: 16px;\n color: inherit;\n}\n.panel-title > a,\n.panel-title > small,\n.panel-title > .small,\n.panel-title > small > a,\n.panel-title > .small > a {\n color: inherit;\n}\n.panel-footer {\n padding: 10px 15px;\n background-color: #f5f5f5;\n border-top: 1px solid #dddddd;\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .list-group,\n.panel > .panel-collapse > .list-group {\n margin-bottom: 0;\n}\n.panel > .list-group .list-group-item,\n.panel > .panel-collapse > .list-group .list-group-item {\n border-width: 1px 0;\n border-radius: 0;\n}\n.panel > .list-group:first-child .list-group-item:first-child,\n.panel > .panel-collapse > .list-group:first-child .list-group-item:first-child {\n border-top: 0;\n border-top-right-radius: 3px;\n border-top-left-radius: 3px;\n}\n.panel > .list-group:last-child .list-group-item:last-child,\n.panel > .panel-collapse > .list-group:last-child .list-group-item:last-child {\n border-bottom: 0;\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel-heading + .list-group .list-group-item:first-child {\n border-top-width: 0;\n}\n.list-group + .panel-footer {\n border-top-width: 0;\n}\n.panel > .table,\n.panel > .table-responsive > .table,\n.panel > .panel-collapse > .table {\n margin-bottom: 0;\n}\n.panel > .table caption,\n.panel > .table-responsive > .table caption,\n.panel > .panel-collapse > .table caption {\n padding-left: 15px;\n padding-right: 15px;\n}\n.panel > .table:first-child,\n.panel > .table-responsive:first-child > .table:first-child {\n border-top-right-radius: 3px;\n border-top-left-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child {\n border-top-left-radius: 3px;\n border-top-right-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child td:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child td:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child,\n.panel > .table:first-child > thead:first-child > tr:first-child th:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child th:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child {\n border-top-left-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child td:last-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child td:last-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child,\n.panel > .table:first-child > thead:first-child > tr:first-child th:last-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child th:last-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child {\n border-top-right-radius: 3px;\n}\n.panel > .table:last-child,\n.panel > .table-responsive:last-child > .table:last-child {\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child {\n border-bottom-left-radius: 3px;\n border-bottom-right-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child td:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child,\n.panel > .table:last-child > tbody:last-child > tr:last-child th:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child {\n border-bottom-left-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child td:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child,\n.panel > .table:last-child > tbody:last-child > tr:last-child th:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child {\n border-bottom-right-radius: 3px;\n}\n.panel > .panel-body + .table,\n.panel > .panel-body + .table-responsive,\n.panel > .table + .panel-body,\n.panel > .table-responsive + .panel-body {\n border-top: 1px solid #dddddd;\n}\n.panel > .table > tbody:first-child > tr:first-child th,\n.panel > .table > tbody:first-child > tr:first-child td {\n border-top: 0;\n}\n.panel > .table-bordered,\n.panel > .table-responsive > .table-bordered {\n border: 0;\n}\n.panel > .table-bordered > thead > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > thead > tr > th:first-child,\n.panel > .table-bordered > tbody > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > th:first-child,\n.panel > .table-bordered > tfoot > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child,\n.panel > .table-bordered > thead > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > thead > tr > td:first-child,\n.panel > .table-bordered > tbody > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > td:first-child,\n.panel > .table-bordered > tfoot > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child {\n border-left: 0;\n}\n.panel > .table-bordered > thead > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > thead > tr > th:last-child,\n.panel > .table-bordered > tbody > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > th:last-child,\n.panel > .table-bordered > tfoot > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child,\n.panel > .table-bordered > thead > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > thead > tr > td:last-child,\n.panel > .table-bordered > tbody > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > td:last-child,\n.panel > .table-bordered > tfoot > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child {\n border-right: 0;\n}\n.panel > .table-bordered > thead > tr:first-child > td,\n.panel > .table-responsive > .table-bordered > thead > tr:first-child > td,\n.panel > .table-bordered > tbody > tr:first-child > td,\n.panel > .table-responsive > .table-bordered > tbody > tr:first-child > td,\n.panel > .table-bordered > thead > tr:first-child > th,\n.panel > .table-responsive > .table-bordered > thead > tr:first-child > th,\n.panel > .table-bordered > tbody > tr:first-child > th,\n.panel > .table-responsive > .table-bordered > tbody > tr:first-child > th {\n border-bottom: 0;\n}\n.panel > .table-bordered > tbody > tr:last-child > td,\n.panel > .table-responsive > .table-bordered > tbody > tr:last-child > td,\n.panel > .table-bordered > tfoot > tr:last-child > td,\n.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td,\n.panel > .table-bordered > tbody > tr:last-child > th,\n.panel > .table-responsive > .table-bordered > tbody > tr:last-child > th,\n.panel > .table-bordered > tfoot > tr:last-child > th,\n.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th {\n border-bottom: 0;\n}\n.panel > .table-responsive {\n border: 0;\n margin-bottom: 0;\n}\n.panel-group {\n margin-bottom: 20px;\n}\n.panel-group .panel {\n margin-bottom: 0;\n border-radius: 4px;\n}\n.panel-group .panel + .panel {\n margin-top: 5px;\n}\n.panel-group .panel-heading {\n border-bottom: 0;\n}\n.panel-group .panel-heading + .panel-collapse > .panel-body,\n.panel-group .panel-heading + .panel-collapse > .list-group {\n border-top: 1px solid #dddddd;\n}\n.panel-group .panel-footer {\n border-top: 0;\n}\n.panel-group .panel-footer + .panel-collapse .panel-body {\n border-bottom: 1px solid #dddddd;\n}\n.panel-default {\n border-color: #dddddd;\n}\n.panel-default > .panel-heading {\n color: #333333;\n background-color: #f5f5f5;\n border-color: #dddddd;\n}\n.panel-default > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #dddddd;\n}\n.panel-default > .panel-heading .badge {\n color: #f5f5f5;\n background-color: #333333;\n}\n.panel-default > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #dddddd;\n}\n.panel-primary {\n border-color: #337ab7;\n}\n.panel-primary > .panel-heading {\n color: #ffffff;\n background-color: #337ab7;\n border-color: #337ab7;\n}\n.panel-primary > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #337ab7;\n}\n.panel-primary > .panel-heading .badge {\n color: #337ab7;\n background-color: #ffffff;\n}\n.panel-primary > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #337ab7;\n}\n.panel-success {\n border-color: #d6e9c6;\n}\n.panel-success > .panel-heading {\n color: #3c763d;\n background-color: #dff0d8;\n border-color: #d6e9c6;\n}\n.panel-success > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #d6e9c6;\n}\n.panel-success > .panel-heading .badge {\n color: #dff0d8;\n background-color: #3c763d;\n}\n.panel-success > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #d6e9c6;\n}\n.panel-info {\n border-color: #bce8f1;\n}\n.panel-info > .panel-heading {\n color: #31708f;\n background-color: #d9edf7;\n border-color: #bce8f1;\n}\n.panel-info > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #bce8f1;\n}\n.panel-info > .panel-heading .badge {\n color: #d9edf7;\n background-color: #31708f;\n}\n.panel-info > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #bce8f1;\n}\n.panel-warning {\n border-color: #faebcc;\n}\n.panel-warning > .panel-heading {\n color: #8a6d3b;\n background-color: #fcf8e3;\n border-color: #faebcc;\n}\n.panel-warning > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #faebcc;\n}\n.panel-warning > .panel-heading .badge {\n color: #fcf8e3;\n background-color: #8a6d3b;\n}\n.panel-warning > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #faebcc;\n}\n.panel-danger {\n border-color: #ebccd1;\n}\n.panel-danger > .panel-heading {\n color: #a94442;\n background-color: #f2dede;\n border-color: #ebccd1;\n}\n.panel-danger > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #ebccd1;\n}\n.panel-danger > .panel-heading .badge {\n color: #f2dede;\n background-color: #a94442;\n}\n.panel-danger > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #ebccd1;\n}\n.embed-responsive {\n position: relative;\n display: block;\n height: 0;\n padding: 0;\n overflow: hidden;\n}\n.embed-responsive .embed-responsive-item,\n.embed-responsive iframe,\n.embed-responsive embed,\n.embed-responsive object,\n.embed-responsive video {\n position: absolute;\n top: 0;\n left: 0;\n bottom: 0;\n height: 100%;\n width: 100%;\n border: 0;\n}\n.embed-responsive-16by9 {\n padding-bottom: 56.25%;\n}\n.embed-responsive-4by3 {\n padding-bottom: 75%;\n}\n.well {\n min-height: 20px;\n padding: 19px;\n margin-bottom: 20px;\n background-color: #f5f5f5;\n border: 1px solid #e3e3e3;\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);\n}\n.well blockquote {\n border-color: #ddd;\n border-color: rgba(0, 0, 0, 0.15);\n}\n.well-lg {\n padding: 24px;\n border-radius: 6px;\n}\n.well-sm {\n padding: 9px;\n border-radius: 3px;\n}\n.close {\n float: right;\n font-size: 21px;\n font-weight: bold;\n line-height: 1;\n color: #000000;\n text-shadow: 0 1px 0 #ffffff;\n opacity: 0.2;\n filter: alpha(opacity=20);\n}\n.close:hover,\n.close:focus {\n color: #000000;\n text-decoration: none;\n cursor: pointer;\n opacity: 0.5;\n filter: alpha(opacity=50);\n}\nbutton.close {\n padding: 0;\n cursor: pointer;\n background: transparent;\n border: 0;\n -webkit-appearance: none;\n}\n.modal-open {\n overflow: hidden;\n}\n.modal {\n display: none;\n overflow: hidden;\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1050;\n -webkit-overflow-scrolling: touch;\n outline: 0;\n}\n.modal.fade .modal-dialog {\n -webkit-transform: translate(0, -25%);\n -ms-transform: translate(0, -25%);\n -o-transform: translate(0, -25%);\n transform: translate(0, -25%);\n -webkit-transition: -webkit-transform 0.3s ease-out;\n -moz-transition: -moz-transform 0.3s ease-out;\n -o-transition: -o-transform 0.3s ease-out;\n transition: transform 0.3s ease-out;\n}\n.modal.in .modal-dialog {\n -webkit-transform: translate(0, 0);\n -ms-transform: translate(0, 0);\n -o-transform: translate(0, 0);\n transform: translate(0, 0);\n}\n.modal-open .modal {\n overflow-x: hidden;\n overflow-y: auto;\n}\n.modal-dialog {\n position: relative;\n width: auto;\n margin: 10px;\n}\n.modal-content {\n position: relative;\n background-color: #ffffff;\n border: 1px solid #999999;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 6px;\n -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);\n box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);\n background-clip: padding-box;\n outline: 0;\n}\n.modal-backdrop {\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1040;\n background-color: #000000;\n}\n.modal-backdrop.fade {\n opacity: 0;\n filter: alpha(opacity=0);\n}\n.modal-backdrop.in {\n opacity: 0.5;\n filter: alpha(opacity=50);\n}\n.modal-header {\n padding: 15px;\n border-bottom: 1px solid #e5e5e5;\n min-height: 16.42857143px;\n}\n.modal-header .close {\n margin-top: -2px;\n}\n.modal-title {\n margin: 0;\n line-height: 1.42857143;\n}\n.modal-body {\n position: relative;\n padding: 15px;\n}\n.modal-footer {\n padding: 15px;\n text-align: right;\n border-top: 1px solid #e5e5e5;\n}\n.modal-footer .btn + .btn {\n margin-left: 5px;\n margin-bottom: 0;\n}\n.modal-footer .btn-group .btn + .btn {\n margin-left: -1px;\n}\n.modal-footer .btn-block + .btn-block {\n margin-left: 0;\n}\n.modal-scrollbar-measure {\n position: absolute;\n top: -9999px;\n width: 50px;\n height: 50px;\n overflow: scroll;\n}\n@media (min-width: 768px) {\n .modal-dialog {\n width: 600px;\n margin: 30px auto;\n }\n .modal-content {\n -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);\n box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);\n }\n .modal-sm {\n width: 300px;\n }\n}\n@media (min-width: 992px) {\n .modal-lg {\n width: 900px;\n }\n}\n.tooltip {\n position: absolute;\n z-index: 1070;\n display: block;\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n font-size: 12px;\n font-weight: normal;\n line-height: 1.4;\n opacity: 0;\n filter: alpha(opacity=0);\n}\n.tooltip.in {\n opacity: 0.9;\n filter: alpha(opacity=90);\n}\n.tooltip.top {\n margin-top: -3px;\n padding: 5px 0;\n}\n.tooltip.right {\n margin-left: 3px;\n padding: 0 5px;\n}\n.tooltip.bottom {\n margin-top: 3px;\n padding: 5px 0;\n}\n.tooltip.left {\n margin-left: -3px;\n padding: 0 5px;\n}\n.tooltip-inner {\n max-width: 200px;\n padding: 3px 8px;\n color: #ffffff;\n text-align: center;\n text-decoration: none;\n background-color: #000000;\n border-radius: 4px;\n}\n.tooltip-arrow {\n position: absolute;\n width: 0;\n height: 0;\n border-color: transparent;\n border-style: solid;\n}\n.tooltip.top .tooltip-arrow {\n bottom: 0;\n left: 50%;\n margin-left: -5px;\n border-width: 5px 5px 0;\n border-top-color: #000000;\n}\n.tooltip.top-left .tooltip-arrow {\n bottom: 0;\n right: 5px;\n margin-bottom: -5px;\n border-width: 5px 5px 0;\n border-top-color: #000000;\n}\n.tooltip.top-right .tooltip-arrow {\n bottom: 0;\n left: 5px;\n margin-bottom: -5px;\n border-width: 5px 5px 0;\n border-top-color: #000000;\n}\n.tooltip.right .tooltip-arrow {\n top: 50%;\n left: 0;\n margin-top: -5px;\n border-width: 5px 5px 5px 0;\n border-right-color: #000000;\n}\n.tooltip.left .tooltip-arrow {\n top: 50%;\n right: 0;\n margin-top: -5px;\n border-width: 5px 0 5px 5px;\n border-left-color: #000000;\n}\n.tooltip.bottom .tooltip-arrow {\n top: 0;\n left: 50%;\n margin-left: -5px;\n border-width: 0 5px 5px;\n border-bottom-color: #000000;\n}\n.tooltip.bottom-left .tooltip-arrow {\n top: 0;\n right: 5px;\n margin-top: -5px;\n border-width: 0 5px 5px;\n border-bottom-color: #000000;\n}\n.tooltip.bottom-right .tooltip-arrow {\n top: 0;\n left: 5px;\n margin-top: -5px;\n border-width: 0 5px 5px;\n border-bottom-color: #000000;\n}\n.popover {\n position: absolute;\n top: 0;\n left: 0;\n z-index: 1060;\n display: none;\n max-width: 276px;\n padding: 1px;\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n font-size: 14px;\n font-weight: normal;\n line-height: 1.42857143;\n text-align: left;\n background-color: #ffffff;\n background-clip: padding-box;\n border: 1px solid #cccccc;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 6px;\n -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);\n box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);\n white-space: normal;\n}\n.popover.top {\n margin-top: -10px;\n}\n.popover.right {\n margin-left: 10px;\n}\n.popover.bottom {\n margin-top: 10px;\n}\n.popover.left {\n margin-left: -10px;\n}\n.popover-title {\n margin: 0;\n padding: 8px 14px;\n font-size: 14px;\n background-color: #f7f7f7;\n border-bottom: 1px solid #ebebeb;\n border-radius: 5px 5px 0 0;\n}\n.popover-content {\n padding: 9px 14px;\n}\n.popover > .arrow,\n.popover > .arrow:after {\n position: absolute;\n display: block;\n width: 0;\n height: 0;\n border-color: transparent;\n border-style: solid;\n}\n.popover > .arrow {\n border-width: 11px;\n}\n.popover > .arrow:after {\n border-width: 10px;\n content: \"\";\n}\n.popover.top > .arrow {\n left: 50%;\n margin-left: -11px;\n border-bottom-width: 0;\n border-top-color: #999999;\n border-top-color: rgba(0, 0, 0, 0.25);\n bottom: -11px;\n}\n.popover.top > .arrow:after {\n content: \" \";\n bottom: 1px;\n margin-left: -10px;\n border-bottom-width: 0;\n border-top-color: #ffffff;\n}\n.popover.right > .arrow {\n top: 50%;\n left: -11px;\n margin-top: -11px;\n border-left-width: 0;\n border-right-color: #999999;\n border-right-color: rgba(0, 0, 0, 0.25);\n}\n.popover.right > .arrow:after {\n content: \" \";\n left: 1px;\n bottom: -10px;\n border-left-width: 0;\n border-right-color: #ffffff;\n}\n.popover.bottom > .arrow {\n left: 50%;\n margin-left: -11px;\n border-top-width: 0;\n border-bottom-color: #999999;\n border-bottom-color: rgba(0, 0, 0, 0.25);\n top: -11px;\n}\n.popover.bottom > .arrow:after {\n content: \" \";\n top: 1px;\n margin-left: -10px;\n border-top-width: 0;\n border-bottom-color: #ffffff;\n}\n.popover.left > .arrow {\n top: 50%;\n right: -11px;\n margin-top: -11px;\n border-right-width: 0;\n border-left-color: #999999;\n border-left-color: rgba(0, 0, 0, 0.25);\n}\n.popover.left > .arrow:after {\n content: \" \";\n right: 1px;\n border-right-width: 0;\n border-left-color: #ffffff;\n bottom: -10px;\n}\n.carousel {\n position: relative;\n}\n.carousel-inner {\n position: relative;\n overflow: hidden;\n width: 100%;\n}\n.carousel-inner > .item {\n display: none;\n position: relative;\n -webkit-transition: 0.6s ease-in-out left;\n -o-transition: 0.6s ease-in-out left;\n transition: 0.6s ease-in-out left;\n}\n.carousel-inner > .item > img,\n.carousel-inner > .item > a > img {\n line-height: 1;\n}\n@media all and (transform-3d), (-webkit-transform-3d) {\n .carousel-inner > .item {\n -webkit-transition: -webkit-transform 0.6s ease-in-out;\n -moz-transition: -moz-transform 0.6s ease-in-out;\n -o-transition: -o-transform 0.6s ease-in-out;\n transition: transform 0.6s ease-in-out;\n -webkit-backface-visibility: hidden;\n -moz-backface-visibility: hidden;\n backface-visibility: hidden;\n -webkit-perspective: 1000;\n -moz-perspective: 1000;\n perspective: 1000;\n }\n .carousel-inner > .item.next,\n .carousel-inner > .item.active.right {\n -webkit-transform: translate3d(100%, 0, 0);\n transform: translate3d(100%, 0, 0);\n left: 0;\n }\n .carousel-inner > .item.prev,\n .carousel-inner > .item.active.left {\n -webkit-transform: translate3d(-100%, 0, 0);\n transform: translate3d(-100%, 0, 0);\n left: 0;\n }\n .carousel-inner > .item.next.left,\n .carousel-inner > .item.prev.right,\n .carousel-inner > .item.active {\n -webkit-transform: translate3d(0, 0, 0);\n transform: translate3d(0, 0, 0);\n left: 0;\n }\n}\n.carousel-inner > .active,\n.carousel-inner > .next,\n.carousel-inner > .prev {\n display: block;\n}\n.carousel-inner > .active {\n left: 0;\n}\n.carousel-inner > .next,\n.carousel-inner > .prev {\n position: absolute;\n top: 0;\n width: 100%;\n}\n.carousel-inner > .next {\n left: 100%;\n}\n.carousel-inner > .prev {\n left: -100%;\n}\n.carousel-inner > .next.left,\n.carousel-inner > .prev.right {\n left: 0;\n}\n.carousel-inner > .active.left {\n left: -100%;\n}\n.carousel-inner > .active.right {\n left: 100%;\n}\n.carousel-control {\n position: absolute;\n top: 0;\n left: 0;\n bottom: 0;\n width: 15%;\n opacity: 0.5;\n filter: alpha(opacity=50);\n font-size: 20px;\n color: #ffffff;\n text-align: center;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);\n}\n.carousel-control.left {\n background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n background-image: linear-gradient(to right, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);\n}\n.carousel-control.right {\n left: auto;\n right: 0;\n background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n background-image: linear-gradient(to right, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);\n}\n.carousel-control:hover,\n.carousel-control:focus {\n outline: 0;\n color: #ffffff;\n text-decoration: none;\n opacity: 0.9;\n filter: alpha(opacity=90);\n}\n.carousel-control .icon-prev,\n.carousel-control .icon-next,\n.carousel-control .glyphicon-chevron-left,\n.carousel-control .glyphicon-chevron-right {\n position: absolute;\n top: 50%;\n z-index: 5;\n display: inline-block;\n}\n.carousel-control .icon-prev,\n.carousel-control .glyphicon-chevron-left {\n left: 50%;\n margin-left: -10px;\n}\n.carousel-control .icon-next,\n.carousel-control .glyphicon-chevron-right {\n right: 50%;\n margin-right: -10px;\n}\n.carousel-control .icon-prev,\n.carousel-control .icon-next {\n width: 20px;\n height: 20px;\n margin-top: -10px;\n line-height: 1;\n font-family: serif;\n}\n.carousel-control .icon-prev:before {\n content: '\\2039';\n}\n.carousel-control .icon-next:before {\n content: '\\203a';\n}\n.carousel-indicators {\n position: absolute;\n bottom: 10px;\n left: 50%;\n z-index: 15;\n width: 60%;\n margin-left: -30%;\n padding-left: 0;\n list-style: none;\n text-align: center;\n}\n.carousel-indicators li {\n display: inline-block;\n width: 10px;\n height: 10px;\n margin: 1px;\n text-indent: -999px;\n border: 1px solid #ffffff;\n border-radius: 10px;\n cursor: pointer;\n background-color: #000 \\9;\n background-color: rgba(0, 0, 0, 0);\n}\n.carousel-indicators .active {\n margin: 0;\n width: 12px;\n height: 12px;\n background-color: #ffffff;\n}\n.carousel-caption {\n position: absolute;\n left: 15%;\n right: 15%;\n bottom: 20px;\n z-index: 10;\n padding-top: 20px;\n padding-bottom: 20px;\n color: #ffffff;\n text-align: center;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);\n}\n.carousel-caption .btn {\n text-shadow: none;\n}\n@media screen and (min-width: 768px) {\n .carousel-control .glyphicon-chevron-left,\n .carousel-control .glyphicon-chevron-right,\n .carousel-control .icon-prev,\n .carousel-control .icon-next {\n width: 30px;\n height: 30px;\n margin-top: -15px;\n font-size: 30px;\n }\n .carousel-control .glyphicon-chevron-left,\n .carousel-control .icon-prev {\n margin-left: -15px;\n }\n .carousel-control .glyphicon-chevron-right,\n .carousel-control .icon-next {\n margin-right: -15px;\n }\n .carousel-caption {\n left: 20%;\n right: 20%;\n padding-bottom: 30px;\n }\n .carousel-indicators {\n bottom: 20px;\n }\n}\n.clearfix:before,\n.clearfix:after,\n.dl-horizontal dd:before,\n.dl-horizontal dd:after,\n.container:before,\n.container:after,\n.container-fluid:before,\n.container-fluid:after,\n.row:before,\n.row:after,\n.form-horizontal .form-group:before,\n.form-horizontal .form-group:after,\n.btn-toolbar:before,\n.btn-toolbar:after,\n.btn-group-vertical > .btn-group:before,\n.btn-group-vertical > .btn-group:after,\n.nav:before,\n.nav:after,\n.navbar:before,\n.navbar:after,\n.navbar-header:before,\n.navbar-header:after,\n.navbar-collapse:before,\n.navbar-collapse:after,\n.pager:before,\n.pager:after,\n.panel-body:before,\n.panel-body:after,\n.modal-footer:before,\n.modal-footer:after {\n content: \" \";\n display: table;\n}\n.clearfix:after,\n.dl-horizontal dd:after,\n.container:after,\n.container-fluid:after,\n.row:after,\n.form-horizontal .form-group:after,\n.btn-toolbar:after,\n.btn-group-vertical > .btn-group:after,\n.nav:after,\n.navbar:after,\n.navbar-header:after,\n.navbar-collapse:after,\n.pager:after,\n.panel-body:after,\n.modal-footer:after {\n clear: both;\n}\n.center-block {\n display: block;\n margin-left: auto;\n margin-right: auto;\n}\n.pull-right {\n float: right !important;\n}\n.pull-left {\n float: left !important;\n}\n.hide {\n display: none !important;\n}\n.show {\n display: block !important;\n}\n.invisible {\n visibility: hidden;\n}\n.text-hide {\n font: 0/0 a;\n color: transparent;\n text-shadow: none;\n background-color: transparent;\n border: 0;\n}\n.hidden {\n display: none !important;\n}\n.affix {\n position: fixed;\n}\n@-ms-viewport {\n width: device-width;\n}\n.visible-xs,\n.visible-sm,\n.visible-md,\n.visible-lg {\n display: none !important;\n}\n.visible-xs-block,\n.visible-xs-inline,\n.visible-xs-inline-block,\n.visible-sm-block,\n.visible-sm-inline,\n.visible-sm-inline-block,\n.visible-md-block,\n.visible-md-inline,\n.visible-md-inline-block,\n.visible-lg-block,\n.visible-lg-inline,\n.visible-lg-inline-block {\n display: none !important;\n}\n@media (max-width: 767px) {\n .visible-xs {\n display: block !important;\n }\n table.visible-xs {\n display: table;\n }\n tr.visible-xs {\n display: table-row !important;\n }\n th.visible-xs,\n td.visible-xs {\n display: table-cell !important;\n }\n}\n@media (max-width: 767px) {\n .visible-xs-block {\n display: block !important;\n }\n}\n@media (max-width: 767px) {\n .visible-xs-inline {\n display: inline !important;\n }\n}\n@media (max-width: 767px) {\n .visible-xs-inline-block {\n display: inline-block !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm {\n display: block !important;\n }\n table.visible-sm {\n display: table;\n }\n tr.visible-sm {\n display: table-row !important;\n }\n th.visible-sm,\n td.visible-sm {\n display: table-cell !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm-block {\n display: block !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm-inline {\n display: inline !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm-inline-block {\n display: inline-block !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md {\n display: block !important;\n }\n table.visible-md {\n display: table;\n }\n tr.visible-md {\n display: table-row !important;\n }\n th.visible-md,\n td.visible-md {\n display: table-cell !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md-block {\n display: block !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md-inline {\n display: inline !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md-inline-block {\n display: inline-block !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg {\n display: block !important;\n }\n table.visible-lg {\n display: table;\n }\n tr.visible-lg {\n display: table-row !important;\n }\n th.visible-lg,\n td.visible-lg {\n display: table-cell !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg-block {\n display: block !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg-inline {\n display: inline !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg-inline-block {\n display: inline-block !important;\n }\n}\n@media (max-width: 767px) {\n .hidden-xs {\n display: none !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .hidden-sm {\n display: none !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .hidden-md {\n display: none !important;\n }\n}\n@media (min-width: 1200px) {\n .hidden-lg {\n display: none !important;\n }\n}\n.visible-print {\n display: none !important;\n}\n@media print {\n .visible-print {\n display: block !important;\n }\n table.visible-print {\n display: table;\n }\n tr.visible-print {\n display: table-row !important;\n }\n th.visible-print,\n td.visible-print {\n display: table-cell !important;\n }\n}\n.visible-print-block {\n display: none !important;\n}\n@media print {\n .visible-print-block {\n display: block !important;\n }\n}\n.visible-print-inline {\n display: none !important;\n}\n@media print {\n .visible-print-inline {\n display: inline !important;\n }\n}\n.visible-print-inline-block {\n display: none !important;\n}\n@media print {\n .visible-print-inline-block {\n display: inline-block !important;\n }\n}\n@media print {\n .hidden-print {\n display: none !important;\n }\n}\n/*# sourceMappingURL=bootstrap.css.map */","/*! normalize.css v3.0.2 | MIT License | git.io/normalize */\n\n//\n// 1. Set default font family to sans-serif.\n// 2. Prevent iOS text size adjust after orientation change, without disabling\n// user zoom.\n//\n\nhtml {\n font-family: sans-serif; // 1\n -ms-text-size-adjust: 100%; // 2\n -webkit-text-size-adjust: 100%; // 2\n}\n\n//\n// Remove default margin.\n//\n\nbody {\n margin: 0;\n}\n\n// HTML5 display definitions\n// ==========================================================================\n\n//\n// Correct `block` display not defined for any HTML5 element in IE 8/9.\n// Correct `block` display not defined for `details` or `summary` in IE 10/11\n// and Firefox.\n// Correct `block` display not defined for `main` in IE 11.\n//\n\narticle,\naside,\ndetails,\nfigcaption,\nfigure,\nfooter,\nheader,\nhgroup,\nmain,\nmenu,\nnav,\nsection,\nsummary {\n display: block;\n}\n\n//\n// 1. Correct `inline-block` display not defined in IE 8/9.\n// 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.\n//\n\naudio,\ncanvas,\nprogress,\nvideo {\n display: inline-block; // 1\n vertical-align: baseline; // 2\n}\n\n//\n// Prevent modern browsers from displaying `audio` without controls.\n// Remove excess height in iOS 5 devices.\n//\n\naudio:not([controls]) {\n display: none;\n height: 0;\n}\n\n//\n// Address `[hidden]` styling not present in IE 8/9/10.\n// Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.\n//\n\n[hidden],\ntemplate {\n display: none;\n}\n\n// Links\n// ==========================================================================\n\n//\n// Remove the gray background color from active links in IE 10.\n//\n\na {\n background-color: transparent;\n}\n\n//\n// Improve readability when focused and also mouse hovered in all browsers.\n//\n\na:active,\na:hover {\n outline: 0;\n}\n\n// Text-level semantics\n// ==========================================================================\n\n//\n// Address styling not present in IE 8/9/10/11, Safari, and Chrome.\n//\n\nabbr[title] {\n border-bottom: 1px dotted;\n}\n\n//\n// Address style set to `bolder` in Firefox 4+, Safari, and Chrome.\n//\n\nb,\nstrong {\n font-weight: bold;\n}\n\n//\n// Address styling not present in Safari and Chrome.\n//\n\ndfn {\n font-style: italic;\n}\n\n//\n// Address variable `h1` font-size and margin within `section` and `article`\n// contexts in Firefox 4+, Safari, and Chrome.\n//\n\nh1 {\n font-size: 2em;\n margin: 0.67em 0;\n}\n\n//\n// Address styling not present in IE 8/9.\n//\n\nmark {\n background: #ff0;\n color: #000;\n}\n\n//\n// Address inconsistent and variable font size in all browsers.\n//\n\nsmall {\n font-size: 80%;\n}\n\n//\n// Prevent `sub` and `sup` affecting `line-height` in all browsers.\n//\n\nsub,\nsup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n}\n\nsup {\n top: -0.5em;\n}\n\nsub {\n bottom: -0.25em;\n}\n\n// Embedded content\n// ==========================================================================\n\n//\n// Remove border when inside `a` element in IE 8/9/10.\n//\n\nimg {\n border: 0;\n}\n\n//\n// Correct overflow not hidden in IE 9/10/11.\n//\n\nsvg:not(:root) {\n overflow: hidden;\n}\n\n// Grouping content\n// ==========================================================================\n\n//\n// Address margin not present in IE 8/9 and Safari.\n//\n\nfigure {\n margin: 1em 40px;\n}\n\n//\n// Address differences between Firefox and other browsers.\n//\n\nhr {\n -moz-box-sizing: content-box;\n box-sizing: content-box;\n height: 0;\n}\n\n//\n// Contain overflow in all browsers.\n//\n\npre {\n overflow: auto;\n}\n\n//\n// Address odd `em`-unit font size rendering in all browsers.\n//\n\ncode,\nkbd,\npre,\nsamp {\n font-family: monospace, monospace;\n font-size: 1em;\n}\n\n// Forms\n// ==========================================================================\n\n//\n// Known limitation: by default, Chrome and Safari on OS X allow very limited\n// styling of `select`, unless a `border` property is set.\n//\n\n//\n// 1. Correct color not being inherited.\n// Known issue: affects color of disabled elements.\n// 2. Correct font properties not being inherited.\n// 3. Address margins set differently in Firefox 4+, Safari, and Chrome.\n//\n\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n color: inherit; // 1\n font: inherit; // 2\n margin: 0; // 3\n}\n\n//\n// Address `overflow` set to `hidden` in IE 8/9/10/11.\n//\n\nbutton {\n overflow: visible;\n}\n\n//\n// Address inconsistent `text-transform` inheritance for `button` and `select`.\n// All other form control elements do not inherit `text-transform` values.\n// Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.\n// Correct `select` style inheritance in Firefox.\n//\n\nbutton,\nselect {\n text-transform: none;\n}\n\n//\n// 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`\n// and `video` controls.\n// 2. Correct inability to style clickable `input` types in iOS.\n// 3. Improve usability and consistency of cursor style between image-type\n// `input` and others.\n//\n\nbutton,\nhtml input[type=\"button\"], // 1\ninput[type=\"reset\"],\ninput[type=\"submit\"] {\n -webkit-appearance: button; // 2\n cursor: pointer; // 3\n}\n\n//\n// Re-set default cursor for disabled elements.\n//\n\nbutton[disabled],\nhtml input[disabled] {\n cursor: default;\n}\n\n//\n// Remove inner padding and border in Firefox 4+.\n//\n\nbutton::-moz-focus-inner,\ninput::-moz-focus-inner {\n border: 0;\n padding: 0;\n}\n\n//\n// Address Firefox 4+ setting `line-height` on `input` using `!important` in\n// the UA stylesheet.\n//\n\ninput {\n line-height: normal;\n}\n\n//\n// It's recommended that you don't attempt to style these elements.\n// Firefox's implementation doesn't respect box-sizing, padding, or width.\n//\n// 1. Address box sizing set to `content-box` in IE 8/9/10.\n// 2. Remove excess padding in IE 8/9/10.\n//\n\ninput[type=\"checkbox\"],\ninput[type=\"radio\"] {\n box-sizing: border-box; // 1\n padding: 0; // 2\n}\n\n//\n// Fix the cursor style for Chrome's increment/decrement buttons. For certain\n// `font-size` values of the `input`, it causes the cursor style of the\n// decrement button to change from `default` to `text`.\n//\n\ninput[type=\"number\"]::-webkit-inner-spin-button,\ninput[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n//\n// 1. Address `appearance` set to `searchfield` in Safari and Chrome.\n// 2. Address `box-sizing` set to `border-box` in Safari and Chrome\n// (include `-moz` to future-proof).\n//\n\ninput[type=\"search\"] {\n -webkit-appearance: textfield; // 1\n -moz-box-sizing: content-box;\n -webkit-box-sizing: content-box; // 2\n box-sizing: content-box;\n}\n\n//\n// Remove inner padding and search cancel button in Safari and Chrome on OS X.\n// Safari (but not Chrome) clips the cancel button when the search input has\n// padding (and `textfield` appearance).\n//\n\ninput[type=\"search\"]::-webkit-search-cancel-button,\ninput[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n//\n// Define consistent border, margin, and padding.\n//\n\nfieldset {\n border: 1px solid #c0c0c0;\n margin: 0 2px;\n padding: 0.35em 0.625em 0.75em;\n}\n\n//\n// 1. Correct `color` not being inherited in IE 8/9/10/11.\n// 2. Remove padding so people aren't caught out if they zero out fieldsets.\n//\n\nlegend {\n border: 0; // 1\n padding: 0; // 2\n}\n\n//\n// Remove default vertical scrollbar in IE 8/9/10/11.\n//\n\ntextarea {\n overflow: auto;\n}\n\n//\n// Don't inherit the `font-weight` (applied by a rule above).\n// NOTE: the default cannot safely be changed in Chrome and Safari on OS X.\n//\n\noptgroup {\n font-weight: bold;\n}\n\n// Tables\n// ==========================================================================\n\n//\n// Remove most spacing between table cells.\n//\n\ntable {\n border-collapse: collapse;\n border-spacing: 0;\n}\n\ntd,\nth {\n padding: 0;\n}\n","/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */\n\n// ==========================================================================\n// Print styles.\n// Inlined to avoid the additional HTTP request: h5bp.com/r\n// ==========================================================================\n\n@media print {\n *,\n *:before,\n *:after {\n background: transparent !important;\n color: #000 !important; // Black prints faster: h5bp.com/s\n box-shadow: none !important;\n text-shadow: none !important;\n }\n\n a,\n a:visited {\n text-decoration: underline;\n }\n\n a[href]:after {\n content: \" (\" attr(href) \")\";\n }\n\n abbr[title]:after {\n content: \" (\" attr(title) \")\";\n }\n\n // Don't show links that are fragment identifiers,\n // or use the `javascript:` pseudo protocol\n a[href^=\"#\"]:after,\n a[href^=\"javascript:\"]:after {\n content: \"\";\n }\n\n pre,\n blockquote {\n border: 1px solid #999;\n page-break-inside: avoid;\n }\n\n thead {\n display: table-header-group; // h5bp.com/t\n }\n\n tr,\n img {\n page-break-inside: avoid;\n }\n\n img {\n max-width: 100% !important;\n }\n\n p,\n h2,\n h3 {\n orphans: 3;\n widows: 3;\n }\n\n h2,\n h3 {\n page-break-after: avoid;\n }\n\n // Bootstrap specific changes start\n //\n // Chrome (OSX) fix for https://github.com/twbs/bootstrap/issues/11245\n // Once fixed, we can just straight up remove this.\n select {\n background: #fff !important;\n }\n\n // Bootstrap components\n .navbar {\n display: none;\n }\n .btn,\n .dropup > .btn {\n > .caret {\n border-top-color: #000 !important;\n }\n }\n .label {\n border: 1px solid #000;\n }\n\n .table {\n border-collapse: collapse !important;\n\n td,\n th {\n background-color: #fff !important;\n }\n }\n .table-bordered {\n th,\n td {\n border: 1px solid #ddd !important;\n }\n }\n\n // Bootstrap specific changes end\n}\n","//\n// Glyphicons for Bootstrap\n//\n// Since icons are fonts, they can be placed anywhere text is placed and are\n// thus automatically sized to match the surrounding child. To use, create an\n// inline element with the appropriate classes, like so:\n//\n// Star\n\n// Import the fonts\n@font-face {\n font-family: 'Glyphicons Halflings';\n src: url('@{icon-font-path}@{icon-font-name}.eot');\n src: url('@{icon-font-path}@{icon-font-name}.eot?#iefix') format('embedded-opentype'),\n url('@{icon-font-path}@{icon-font-name}.woff2') format('woff2'),\n url('@{icon-font-path}@{icon-font-name}.woff') format('woff'),\n url('@{icon-font-path}@{icon-font-name}.ttf') format('truetype'),\n url('@{icon-font-path}@{icon-font-name}.svg#@{icon-font-svg-id}') format('svg');\n}\n\n// Catchall baseclass\n.glyphicon {\n position: relative;\n top: 1px;\n display: inline-block;\n font-family: 'Glyphicons Halflings';\n font-style: normal;\n font-weight: normal;\n line-height: 1;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\n// Individual icons\n.glyphicon-asterisk { &:before { content: \"\\2a\"; } }\n.glyphicon-plus { &:before { content: \"\\2b\"; } }\n.glyphicon-euro,\n.glyphicon-eur { &:before { content: \"\\20ac\"; } }\n.glyphicon-minus { &:before { content: \"\\2212\"; } }\n.glyphicon-cloud { &:before { content: \"\\2601\"; } }\n.glyphicon-envelope { &:before { content: \"\\2709\"; } }\n.glyphicon-pencil { &:before { content: \"\\270f\"; } }\n.glyphicon-glass { &:before { content: \"\\e001\"; } }\n.glyphicon-music { &:before { content: \"\\e002\"; } }\n.glyphicon-search { &:before { content: \"\\e003\"; } }\n.glyphicon-heart { &:before { content: \"\\e005\"; } }\n.glyphicon-star { &:before { content: \"\\e006\"; } }\n.glyphicon-star-empty { &:before { content: \"\\e007\"; } }\n.glyphicon-user { &:before { content: \"\\e008\"; } }\n.glyphicon-film { &:before { content: \"\\e009\"; } }\n.glyphicon-th-large { &:before { content: \"\\e010\"; } }\n.glyphicon-th { &:before { content: \"\\e011\"; } }\n.glyphicon-th-list { &:before { content: \"\\e012\"; } }\n.glyphicon-ok { &:before { content: \"\\e013\"; } }\n.glyphicon-remove { &:before { content: \"\\e014\"; } }\n.glyphicon-zoom-in { &:before { content: \"\\e015\"; } }\n.glyphicon-zoom-out { &:before { content: \"\\e016\"; } }\n.glyphicon-off { &:before { content: \"\\e017\"; } }\n.glyphicon-signal { &:before { content: \"\\e018\"; } }\n.glyphicon-cog { &:before { content: \"\\e019\"; } }\n.glyphicon-trash { &:before { content: \"\\e020\"; } }\n.glyphicon-home { &:before { content: \"\\e021\"; } }\n.glyphicon-file { &:before { content: \"\\e022\"; } }\n.glyphicon-time { &:before { content: \"\\e023\"; } }\n.glyphicon-road { &:before { content: \"\\e024\"; } }\n.glyphicon-download-alt { &:before { content: \"\\e025\"; } }\n.glyphicon-download { &:before { content: \"\\e026\"; } }\n.glyphicon-upload { &:before { content: \"\\e027\"; } }\n.glyphicon-inbox { &:before { content: \"\\e028\"; } }\n.glyphicon-play-circle { &:before { content: \"\\e029\"; } }\n.glyphicon-repeat { &:before { content: \"\\e030\"; } }\n.glyphicon-refresh { &:before { content: \"\\e031\"; } }\n.glyphicon-list-alt { &:before { content: \"\\e032\"; } }\n.glyphicon-lock { &:before { content: \"\\e033\"; } }\n.glyphicon-flag { &:before { content: \"\\e034\"; } }\n.glyphicon-headphones { &:before { content: \"\\e035\"; } }\n.glyphicon-volume-off { &:before { content: \"\\e036\"; } }\n.glyphicon-volume-down { &:before { content: \"\\e037\"; } }\n.glyphicon-volume-up { &:before { content: \"\\e038\"; } }\n.glyphicon-qrcode { &:before { content: \"\\e039\"; } }\n.glyphicon-barcode { &:before { content: \"\\e040\"; } }\n.glyphicon-tag { &:before { content: \"\\e041\"; } }\n.glyphicon-tags { &:before { content: \"\\e042\"; } }\n.glyphicon-book { &:before { content: \"\\e043\"; } }\n.glyphicon-bookmark { &:before { content: \"\\e044\"; } }\n.glyphicon-print { &:before { content: \"\\e045\"; } }\n.glyphicon-camera { &:before { content: \"\\e046\"; } }\n.glyphicon-font { &:before { content: \"\\e047\"; } }\n.glyphicon-bold { &:before { content: \"\\e048\"; } }\n.glyphicon-italic { &:before { content: \"\\e049\"; } }\n.glyphicon-text-height { &:before { content: \"\\e050\"; } }\n.glyphicon-text-width { &:before { content: \"\\e051\"; } }\n.glyphicon-align-left { &:before { content: \"\\e052\"; } }\n.glyphicon-align-center { &:before { content: \"\\e053\"; } }\n.glyphicon-align-right { &:before { content: \"\\e054\"; } }\n.glyphicon-align-justify { &:before { content: \"\\e055\"; } }\n.glyphicon-list { &:before { content: \"\\e056\"; } }\n.glyphicon-indent-left { &:before { content: \"\\e057\"; } }\n.glyphicon-indent-right { &:before { content: \"\\e058\"; } }\n.glyphicon-facetime-video { &:before { content: \"\\e059\"; } }\n.glyphicon-picture { &:before { content: \"\\e060\"; } }\n.glyphicon-map-marker { &:before { content: \"\\e062\"; } }\n.glyphicon-adjust { &:before { content: \"\\e063\"; } }\n.glyphicon-tint { &:before { content: \"\\e064\"; } }\n.glyphicon-edit { &:before { content: \"\\e065\"; } }\n.glyphicon-share { &:before { content: \"\\e066\"; } }\n.glyphicon-check { &:before { content: \"\\e067\"; } }\n.glyphicon-move { &:before { content: \"\\e068\"; } }\n.glyphicon-step-backward { &:before { content: \"\\e069\"; } }\n.glyphicon-fast-backward { &:before { content: \"\\e070\"; } }\n.glyphicon-backward { &:before { content: \"\\e071\"; } }\n.glyphicon-play { &:before { content: \"\\e072\"; } }\n.glyphicon-pause { &:before { content: \"\\e073\"; } }\n.glyphicon-stop { &:before { content: \"\\e074\"; } }\n.glyphicon-forward { &:before { content: \"\\e075\"; } }\n.glyphicon-fast-forward { &:before { content: \"\\e076\"; } }\n.glyphicon-step-forward { &:before { content: \"\\e077\"; } }\n.glyphicon-eject { &:before { content: \"\\e078\"; } }\n.glyphicon-chevron-left { &:before { content: \"\\e079\"; } }\n.glyphicon-chevron-right { &:before { content: \"\\e080\"; } }\n.glyphicon-plus-sign { &:before { content: \"\\e081\"; } }\n.glyphicon-minus-sign { &:before { content: \"\\e082\"; } }\n.glyphicon-remove-sign { &:before { content: \"\\e083\"; } }\n.glyphicon-ok-sign { &:before { content: \"\\e084\"; } }\n.glyphicon-question-sign { &:before { content: \"\\e085\"; } }\n.glyphicon-info-sign { &:before { content: \"\\e086\"; } }\n.glyphicon-screenshot { &:before { content: \"\\e087\"; } }\n.glyphicon-remove-circle { &:before { content: \"\\e088\"; } }\n.glyphicon-ok-circle { &:before { content: \"\\e089\"; } }\n.glyphicon-ban-circle { &:before { content: \"\\e090\"; } }\n.glyphicon-arrow-left { &:before { content: \"\\e091\"; } }\n.glyphicon-arrow-right { &:before { content: \"\\e092\"; } }\n.glyphicon-arrow-up { &:before { content: \"\\e093\"; } }\n.glyphicon-arrow-down { &:before { content: \"\\e094\"; } }\n.glyphicon-share-alt { &:before { content: \"\\e095\"; } }\n.glyphicon-resize-full { &:before { content: \"\\e096\"; } }\n.glyphicon-resize-small { &:before { content: \"\\e097\"; } }\n.glyphicon-exclamation-sign { &:before { content: \"\\e101\"; } }\n.glyphicon-gift { &:before { content: \"\\e102\"; } }\n.glyphicon-leaf { &:before { content: \"\\e103\"; } }\n.glyphicon-fire { &:before { content: \"\\e104\"; } }\n.glyphicon-eye-open { &:before { content: \"\\e105\"; } }\n.glyphicon-eye-close { &:before { content: \"\\e106\"; } }\n.glyphicon-warning-sign { &:before { content: \"\\e107\"; } }\n.glyphicon-plane { &:before { content: \"\\e108\"; } }\n.glyphicon-calendar { &:before { content: \"\\e109\"; } }\n.glyphicon-random { &:before { content: \"\\e110\"; } }\n.glyphicon-comment { &:before { content: \"\\e111\"; } }\n.glyphicon-magnet { &:before { content: \"\\e112\"; } }\n.glyphicon-chevron-up { &:before { content: \"\\e113\"; } }\n.glyphicon-chevron-down { &:before { content: \"\\e114\"; } }\n.glyphicon-retweet { &:before { content: \"\\e115\"; } }\n.glyphicon-shopping-cart { &:before { content: \"\\e116\"; } }\n.glyphicon-folder-close { &:before { content: \"\\e117\"; } }\n.glyphicon-folder-open { &:before { content: \"\\e118\"; } }\n.glyphicon-resize-vertical { &:before { content: \"\\e119\"; } }\n.glyphicon-resize-horizontal { &:before { content: \"\\e120\"; } }\n.glyphicon-hdd { &:before { content: \"\\e121\"; } }\n.glyphicon-bullhorn { &:before { content: \"\\e122\"; } }\n.glyphicon-bell { &:before { content: \"\\e123\"; } }\n.glyphicon-certificate { &:before { content: \"\\e124\"; } }\n.glyphicon-thumbs-up { &:before { content: \"\\e125\"; } }\n.glyphicon-thumbs-down { &:before { content: \"\\e126\"; } }\n.glyphicon-hand-right { &:before { content: \"\\e127\"; } }\n.glyphicon-hand-left { &:before { content: \"\\e128\"; } }\n.glyphicon-hand-up { &:before { content: \"\\e129\"; } }\n.glyphicon-hand-down { &:before { content: \"\\e130\"; } }\n.glyphicon-circle-arrow-right { &:before { content: \"\\e131\"; } }\n.glyphicon-circle-arrow-left { &:before { content: \"\\e132\"; } }\n.glyphicon-circle-arrow-up { &:before { content: \"\\e133\"; } }\n.glyphicon-circle-arrow-down { &:before { content: \"\\e134\"; } }\n.glyphicon-globe { &:before { content: \"\\e135\"; } }\n.glyphicon-wrench { &:before { content: \"\\e136\"; } }\n.glyphicon-tasks { &:before { content: \"\\e137\"; } }\n.glyphicon-filter { &:before { content: \"\\e138\"; } }\n.glyphicon-briefcase { &:before { content: \"\\e139\"; } }\n.glyphicon-fullscreen { &:before { content: \"\\e140\"; } }\n.glyphicon-dashboard { &:before { content: \"\\e141\"; } }\n.glyphicon-paperclip { &:before { content: \"\\e142\"; } }\n.glyphicon-heart-empty { &:before { content: \"\\e143\"; } }\n.glyphicon-link { &:before { content: \"\\e144\"; } }\n.glyphicon-phone { &:before { content: \"\\e145\"; } }\n.glyphicon-pushpin { &:before { content: \"\\e146\"; } }\n.glyphicon-usd { &:before { content: \"\\e148\"; } }\n.glyphicon-gbp { &:before { content: \"\\e149\"; } }\n.glyphicon-sort { &:before { content: \"\\e150\"; } }\n.glyphicon-sort-by-alphabet { &:before { content: \"\\e151\"; } }\n.glyphicon-sort-by-alphabet-alt { &:before { content: \"\\e152\"; } }\n.glyphicon-sort-by-order { &:before { content: \"\\e153\"; } }\n.glyphicon-sort-by-order-alt { &:before { content: \"\\e154\"; } }\n.glyphicon-sort-by-attributes { &:before { content: \"\\e155\"; } }\n.glyphicon-sort-by-attributes-alt { &:before { content: \"\\e156\"; } }\n.glyphicon-unchecked { &:before { content: \"\\e157\"; } }\n.glyphicon-expand { &:before { content: \"\\e158\"; } }\n.glyphicon-collapse-down { &:before { content: \"\\e159\"; } }\n.glyphicon-collapse-up { &:before { content: \"\\e160\"; } }\n.glyphicon-log-in { &:before { content: \"\\e161\"; } }\n.glyphicon-flash { &:before { content: \"\\e162\"; } }\n.glyphicon-log-out { &:before { content: \"\\e163\"; } }\n.glyphicon-new-window { &:before { content: \"\\e164\"; } }\n.glyphicon-record { &:before { content: \"\\e165\"; } }\n.glyphicon-save { &:before { content: \"\\e166\"; } }\n.glyphicon-open { &:before { content: \"\\e167\"; } }\n.glyphicon-saved { &:before { content: \"\\e168\"; } }\n.glyphicon-import { &:before { content: \"\\e169\"; } }\n.glyphicon-export { &:before { content: \"\\e170\"; } }\n.glyphicon-send { &:before { content: \"\\e171\"; } }\n.glyphicon-floppy-disk { &:before { content: \"\\e172\"; } }\n.glyphicon-floppy-saved { &:before { content: \"\\e173\"; } }\n.glyphicon-floppy-remove { &:before { content: \"\\e174\"; } }\n.glyphicon-floppy-save { &:before { content: \"\\e175\"; } }\n.glyphicon-floppy-open { &:before { content: \"\\e176\"; } }\n.glyphicon-credit-card { &:before { content: \"\\e177\"; } }\n.glyphicon-transfer { &:before { content: \"\\e178\"; } }\n.glyphicon-cutlery { &:before { content: \"\\e179\"; } }\n.glyphicon-header { &:before { content: \"\\e180\"; } }\n.glyphicon-compressed { &:before { content: \"\\e181\"; } }\n.glyphicon-earphone { &:before { content: \"\\e182\"; } }\n.glyphicon-phone-alt { &:before { content: \"\\e183\"; } }\n.glyphicon-tower { &:before { content: \"\\e184\"; } }\n.glyphicon-stats { &:before { content: \"\\e185\"; } }\n.glyphicon-sd-video { &:before { content: \"\\e186\"; } }\n.glyphicon-hd-video { &:before { content: \"\\e187\"; } }\n.glyphicon-subtitles { &:before { content: \"\\e188\"; } }\n.glyphicon-sound-stereo { &:before { content: \"\\e189\"; } }\n.glyphicon-sound-dolby { &:before { content: \"\\e190\"; } }\n.glyphicon-sound-5-1 { &:before { content: \"\\e191\"; } }\n.glyphicon-sound-6-1 { &:before { content: \"\\e192\"; } }\n.glyphicon-sound-7-1 { &:before { content: \"\\e193\"; } }\n.glyphicon-copyright-mark { &:before { content: \"\\e194\"; } }\n.glyphicon-registration-mark { &:before { content: \"\\e195\"; } }\n.glyphicon-cloud-download { &:before { content: \"\\e197\"; } }\n.glyphicon-cloud-upload { &:before { content: \"\\e198\"; } }\n.glyphicon-tree-conifer { &:before { content: \"\\e199\"; } }\n.glyphicon-tree-deciduous { &:before { content: \"\\e200\"; } }\n.glyphicon-cd { &:before { content: \"\\e201\"; } }\n.glyphicon-save-file { &:before { content: \"\\e202\"; } }\n.glyphicon-open-file { &:before { content: \"\\e203\"; } }\n.glyphicon-level-up { &:before { content: \"\\e204\"; } }\n.glyphicon-copy { &:before { content: \"\\e205\"; } }\n.glyphicon-paste { &:before { content: \"\\e206\"; } }\n// The following 2 Glyphicons are omitted for the time being because\n// they currently use Unicode codepoints that are outside the\n// Basic Multilingual Plane (BMP). Older buggy versions of WebKit can't handle\n// non-BMP codepoints in CSS string escapes, and thus can't display these two icons.\n// Notably, the bug affects some older versions of the Android Browser.\n// More info: https://github.com/twbs/bootstrap/issues/10106\n// .glyphicon-door { &:before { content: \"\\1f6aa\"; } }\n// .glyphicon-key { &:before { content: \"\\1f511\"; } }\n.glyphicon-alert { &:before { content: \"\\e209\"; } }\n.glyphicon-equalizer { &:before { content: \"\\e210\"; } }\n.glyphicon-king { &:before { content: \"\\e211\"; } }\n.glyphicon-queen { &:before { content: \"\\e212\"; } }\n.glyphicon-pawn { &:before { content: \"\\e213\"; } }\n.glyphicon-bishop { &:before { content: \"\\e214\"; } }\n.glyphicon-knight { &:before { content: \"\\e215\"; } }\n.glyphicon-baby-formula { &:before { content: \"\\e216\"; } }\n.glyphicon-tent { &:before { content: \"\\26fa\"; } }\n.glyphicon-blackboard { &:before { content: \"\\e218\"; } }\n.glyphicon-bed { &:before { content: \"\\e219\"; } }\n.glyphicon-apple { &:before { content: \"\\f8ff\"; } }\n.glyphicon-erase { &:before { content: \"\\e221\"; } }\n.glyphicon-hourglass { &:before { content: \"\\231b\"; } }\n.glyphicon-lamp { &:before { content: \"\\e223\"; } }\n.glyphicon-duplicate { &:before { content: \"\\e224\"; } }\n.glyphicon-piggy-bank { &:before { content: \"\\e225\"; } }\n.glyphicon-scissors { &:before { content: \"\\e226\"; } }\n.glyphicon-bitcoin { &:before { content: \"\\e227\"; } }\n.glyphicon-btc { &:before { content: \"\\e227\"; } }\n.glyphicon-xbt { &:before { content: \"\\e227\"; } }\n.glyphicon-yen { &:before { content: \"\\00a5\"; } }\n.glyphicon-jpy { &:before { content: \"\\00a5\"; } }\n.glyphicon-ruble { &:before { content: \"\\20bd\"; } }\n.glyphicon-rub { &:before { content: \"\\20bd\"; } }\n.glyphicon-scale { &:before { content: \"\\e230\"; } }\n.glyphicon-ice-lolly { &:before { content: \"\\e231\"; } }\n.glyphicon-ice-lolly-tasted { &:before { content: \"\\e232\"; } }\n.glyphicon-education { &:before { content: \"\\e233\"; } }\n.glyphicon-option-horizontal { &:before { content: \"\\e234\"; } }\n.glyphicon-option-vertical { &:before { content: \"\\e235\"; } }\n.glyphicon-menu-hamburger { &:before { content: \"\\e236\"; } }\n.glyphicon-modal-window { &:before { content: \"\\e237\"; } }\n.glyphicon-oil { &:before { content: \"\\e238\"; } }\n.glyphicon-grain { &:before { content: \"\\e239\"; } }\n.glyphicon-sunglasses { &:before { content: \"\\e240\"; } }\n.glyphicon-text-size { &:before { content: \"\\e241\"; } }\n.glyphicon-text-color { &:before { content: \"\\e242\"; } }\n.glyphicon-text-background { &:before { content: \"\\e243\"; } }\n.glyphicon-object-align-top { &:before { content: \"\\e244\"; } }\n.glyphicon-object-align-bottom { &:before { content: \"\\e245\"; } }\n.glyphicon-object-align-horizontal{ &:before { content: \"\\e246\"; } }\n.glyphicon-object-align-left { &:before { content: \"\\e247\"; } }\n.glyphicon-object-align-vertical { &:before { content: \"\\e248\"; } }\n.glyphicon-object-align-right { &:before { content: \"\\e249\"; } }\n.glyphicon-triangle-right { &:before { content: \"\\e250\"; } }\n.glyphicon-triangle-left { &:before { content: \"\\e251\"; } }\n.glyphicon-triangle-bottom { &:before { content: \"\\e252\"; } }\n.glyphicon-triangle-top { &:before { content: \"\\e253\"; } }\n.glyphicon-console { &:before { content: \"\\e254\"; } }\n.glyphicon-superscript { &:before { content: \"\\e255\"; } }\n.glyphicon-subscript { &:before { content: \"\\e256\"; } }\n.glyphicon-menu-left { &:before { content: \"\\e257\"; } }\n.glyphicon-menu-right { &:before { content: \"\\e258\"; } }\n.glyphicon-menu-down { &:before { content: \"\\e259\"; } }\n.glyphicon-menu-up { &:before { content: \"\\e260\"; } }\n","//\n// Scaffolding\n// --------------------------------------------------\n\n\n// Reset the box-sizing\n//\n// Heads up! This reset may cause conflicts with some third-party widgets.\n// For recommendations on resolving such conflicts, see\n// http://getbootstrap.com/getting-started/#third-box-sizing\n* {\n .box-sizing(border-box);\n}\n*:before,\n*:after {\n .box-sizing(border-box);\n}\n\n\n// Body reset\n\nhtml {\n font-size: 10px;\n -webkit-tap-highlight-color: rgba(0,0,0,0);\n}\n\nbody {\n font-family: @font-family-base;\n font-size: @font-size-base;\n line-height: @line-height-base;\n color: @text-color;\n background-color: @body-bg;\n}\n\n// Reset fonts for relevant elements\ninput,\nbutton,\nselect,\ntextarea {\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\n\n// Links\n\na {\n color: @link-color;\n text-decoration: none;\n\n &:hover,\n &:focus {\n color: @link-hover-color;\n text-decoration: @link-hover-decoration;\n }\n\n &:focus {\n .tab-focus();\n }\n}\n\n\n// Figures\n//\n// We reset this here because previously Normalize had no `figure` margins. This\n// ensures we don't break anyone's use of the element.\n\nfigure {\n margin: 0;\n}\n\n\n// Images\n\nimg {\n vertical-align: middle;\n}\n\n// Responsive images (ensure images don't scale beyond their parents)\n.img-responsive {\n .img-responsive();\n}\n\n// Rounded corners\n.img-rounded {\n border-radius: @border-radius-large;\n}\n\n// Image thumbnails\n//\n// Heads up! This is mixin-ed into thumbnails.less for `.thumbnail`.\n.img-thumbnail {\n padding: @thumbnail-padding;\n line-height: @line-height-base;\n background-color: @thumbnail-bg;\n border: 1px solid @thumbnail-border;\n border-radius: @thumbnail-border-radius;\n .transition(all .2s ease-in-out);\n\n // Keep them at most 100% wide\n .img-responsive(inline-block);\n}\n\n// Perfect circle\n.img-circle {\n border-radius: 50%; // set radius in percents\n}\n\n\n// Horizontal rules\n\nhr {\n margin-top: @line-height-computed;\n margin-bottom: @line-height-computed;\n border: 0;\n border-top: 1px solid @hr-border;\n}\n\n\n// Only display content to screen readers\n//\n// See: http://a11yproject.com/posts/how-to-hide-content/\n\n.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n overflow: hidden;\n clip: rect(0,0,0,0);\n border: 0;\n}\n\n// Use in conjunction with .sr-only to only display content when it's focused.\n// Useful for \"Skip to main content\" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1\n// Credit: HTML5 Boilerplate\n\n.sr-only-focusable {\n &:active,\n &:focus {\n position: static;\n width: auto;\n height: auto;\n margin: 0;\n overflow: visible;\n clip: auto;\n }\n}\n\n\n// iOS \"clickable elements\" fix for role=\"button\"\n//\n// Fixes \"clickability\" issue (and more generally, the firing of events such as focus as well)\n// for traditionally non-focusable elements with role=\"button\"\n// see https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile\n// Upstream patch for normalize.css submitted: https://github.com/necolas/normalize.css/pull/379 - remove this fix once that is merged\n\n[role=\"button\"] {\n cursor: pointer;\n}","// Vendor Prefixes\n//\n// All vendor mixins are deprecated as of v3.2.0 due to the introduction of\n// Autoprefixer in our Gruntfile. They will be removed in v4.\n\n// - Animations\n// - Backface visibility\n// - Box shadow\n// - Box sizing\n// - Content columns\n// - Hyphens\n// - Placeholder text\n// - Transformations\n// - Transitions\n// - User Select\n\n\n// Animations\n.animation(@animation) {\n -webkit-animation: @animation;\n -o-animation: @animation;\n animation: @animation;\n}\n.animation-name(@name) {\n -webkit-animation-name: @name;\n animation-name: @name;\n}\n.animation-duration(@duration) {\n -webkit-animation-duration: @duration;\n animation-duration: @duration;\n}\n.animation-timing-function(@timing-function) {\n -webkit-animation-timing-function: @timing-function;\n animation-timing-function: @timing-function;\n}\n.animation-delay(@delay) {\n -webkit-animation-delay: @delay;\n animation-delay: @delay;\n}\n.animation-iteration-count(@iteration-count) {\n -webkit-animation-iteration-count: @iteration-count;\n animation-iteration-count: @iteration-count;\n}\n.animation-direction(@direction) {\n -webkit-animation-direction: @direction;\n animation-direction: @direction;\n}\n.animation-fill-mode(@fill-mode) {\n -webkit-animation-fill-mode: @fill-mode;\n animation-fill-mode: @fill-mode;\n}\n\n// Backface visibility\n// Prevent browsers from flickering when using CSS 3D transforms.\n// Default value is `visible`, but can be changed to `hidden`\n\n.backface-visibility(@visibility){\n -webkit-backface-visibility: @visibility;\n -moz-backface-visibility: @visibility;\n backface-visibility: @visibility;\n}\n\n// Drop shadows\n//\n// Note: Deprecated `.box-shadow()` as of v3.1.0 since all of Bootstrap's\n// supported browsers that have box shadow capabilities now support it.\n\n.box-shadow(@shadow) {\n -webkit-box-shadow: @shadow; // iOS <4.3 & Android <4.1\n box-shadow: @shadow;\n}\n\n// Box sizing\n.box-sizing(@boxmodel) {\n -webkit-box-sizing: @boxmodel;\n -moz-box-sizing: @boxmodel;\n box-sizing: @boxmodel;\n}\n\n// CSS3 Content Columns\n.content-columns(@column-count; @column-gap: @grid-gutter-width) {\n -webkit-column-count: @column-count;\n -moz-column-count: @column-count;\n column-count: @column-count;\n -webkit-column-gap: @column-gap;\n -moz-column-gap: @column-gap;\n column-gap: @column-gap;\n}\n\n// Optional hyphenation\n.hyphens(@mode: auto) {\n word-wrap: break-word;\n -webkit-hyphens: @mode;\n -moz-hyphens: @mode;\n -ms-hyphens: @mode; // IE10+\n -o-hyphens: @mode;\n hyphens: @mode;\n}\n\n// Placeholder text\n.placeholder(@color: @input-color-placeholder) {\n // Firefox\n &::-moz-placeholder {\n color: @color;\n opacity: 1; // Override Firefox's unusual default opacity; see https://github.com/twbs/bootstrap/pull/11526\n }\n &:-ms-input-placeholder { color: @color; } // Internet Explorer 10+\n &::-webkit-input-placeholder { color: @color; } // Safari and Chrome\n}\n\n// Transformations\n.scale(@ratio) {\n -webkit-transform: scale(@ratio);\n -ms-transform: scale(@ratio); // IE9 only\n -o-transform: scale(@ratio);\n transform: scale(@ratio);\n}\n.scale(@ratioX; @ratioY) {\n -webkit-transform: scale(@ratioX, @ratioY);\n -ms-transform: scale(@ratioX, @ratioY); // IE9 only\n -o-transform: scale(@ratioX, @ratioY);\n transform: scale(@ratioX, @ratioY);\n}\n.scaleX(@ratio) {\n -webkit-transform: scaleX(@ratio);\n -ms-transform: scaleX(@ratio); // IE9 only\n -o-transform: scaleX(@ratio);\n transform: scaleX(@ratio);\n}\n.scaleY(@ratio) {\n -webkit-transform: scaleY(@ratio);\n -ms-transform: scaleY(@ratio); // IE9 only\n -o-transform: scaleY(@ratio);\n transform: scaleY(@ratio);\n}\n.skew(@x; @y) {\n -webkit-transform: skewX(@x) skewY(@y);\n -ms-transform: skewX(@x) skewY(@y); // See https://github.com/twbs/bootstrap/issues/4885; IE9+\n -o-transform: skewX(@x) skewY(@y);\n transform: skewX(@x) skewY(@y);\n}\n.translate(@x; @y) {\n -webkit-transform: translate(@x, @y);\n -ms-transform: translate(@x, @y); // IE9 only\n -o-transform: translate(@x, @y);\n transform: translate(@x, @y);\n}\n.translate3d(@x; @y; @z) {\n -webkit-transform: translate3d(@x, @y, @z);\n transform: translate3d(@x, @y, @z);\n}\n.rotate(@degrees) {\n -webkit-transform: rotate(@degrees);\n -ms-transform: rotate(@degrees); // IE9 only\n -o-transform: rotate(@degrees);\n transform: rotate(@degrees);\n}\n.rotateX(@degrees) {\n -webkit-transform: rotateX(@degrees);\n -ms-transform: rotateX(@degrees); // IE9 only\n -o-transform: rotateX(@degrees);\n transform: rotateX(@degrees);\n}\n.rotateY(@degrees) {\n -webkit-transform: rotateY(@degrees);\n -ms-transform: rotateY(@degrees); // IE9 only\n -o-transform: rotateY(@degrees);\n transform: rotateY(@degrees);\n}\n.perspective(@perspective) {\n -webkit-perspective: @perspective;\n -moz-perspective: @perspective;\n perspective: @perspective;\n}\n.perspective-origin(@perspective) {\n -webkit-perspective-origin: @perspective;\n -moz-perspective-origin: @perspective;\n perspective-origin: @perspective;\n}\n.transform-origin(@origin) {\n -webkit-transform-origin: @origin;\n -moz-transform-origin: @origin;\n -ms-transform-origin: @origin; // IE9 only\n transform-origin: @origin;\n}\n\n\n// Transitions\n\n.transition(@transition) {\n -webkit-transition: @transition;\n -o-transition: @transition;\n transition: @transition;\n}\n.transition-property(@transition-property) {\n -webkit-transition-property: @transition-property;\n transition-property: @transition-property;\n}\n.transition-delay(@transition-delay) {\n -webkit-transition-delay: @transition-delay;\n transition-delay: @transition-delay;\n}\n.transition-duration(@transition-duration) {\n -webkit-transition-duration: @transition-duration;\n transition-duration: @transition-duration;\n}\n.transition-timing-function(@timing-function) {\n -webkit-transition-timing-function: @timing-function;\n transition-timing-function: @timing-function;\n}\n.transition-transform(@transition) {\n -webkit-transition: -webkit-transform @transition;\n -moz-transition: -moz-transform @transition;\n -o-transition: -o-transform @transition;\n transition: transform @transition;\n}\n\n\n// User select\n// For selecting text on the page\n\n.user-select(@select) {\n -webkit-user-select: @select;\n -moz-user-select: @select;\n -ms-user-select: @select; // IE10+\n user-select: @select;\n}\n","// WebKit-style focus\n\n.tab-focus() {\n // Default\n outline: thin dotted;\n // WebKit\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\n","// Image Mixins\n// - Responsive image\n// - Retina image\n\n\n// Responsive image\n//\n// Keep images from scaling beyond the width of their parents.\n.img-responsive(@display: block) {\n display: @display;\n max-width: 100%; // Part 1: Set a maximum relative to the parent\n height: auto; // Part 2: Scale the height according to the width, otherwise you get stretching\n}\n\n\n// Retina image\n//\n// Short retina mixin for setting background-image and -size. Note that the\n// spelling of `min--moz-device-pixel-ratio` is intentional.\n.img-retina(@file-1x; @file-2x; @width-1x; @height-1x) {\n background-image: url(\"@{file-1x}\");\n\n @media\n only screen and (-webkit-min-device-pixel-ratio: 2),\n only screen and ( min--moz-device-pixel-ratio: 2),\n only screen and ( -o-min-device-pixel-ratio: 2/1),\n only screen and ( min-device-pixel-ratio: 2),\n only screen and ( min-resolution: 192dpi),\n only screen and ( min-resolution: 2dppx) {\n background-image: url(\"@{file-2x}\");\n background-size: @width-1x @height-1x;\n }\n}\n","//\n// Typography\n// --------------------------------------------------\n\n\n// Headings\n// -------------------------\n\nh1, h2, h3, h4, h5, h6,\n.h1, .h2, .h3, .h4, .h5, .h6 {\n font-family: @headings-font-family;\n font-weight: @headings-font-weight;\n line-height: @headings-line-height;\n color: @headings-color;\n\n small,\n .small {\n font-weight: normal;\n line-height: 1;\n color: @headings-small-color;\n }\n}\n\nh1, .h1,\nh2, .h2,\nh3, .h3 {\n margin-top: @line-height-computed;\n margin-bottom: (@line-height-computed / 2);\n\n small,\n .small {\n font-size: 65%;\n }\n}\nh4, .h4,\nh5, .h5,\nh6, .h6 {\n margin-top: (@line-height-computed / 2);\n margin-bottom: (@line-height-computed / 2);\n\n small,\n .small {\n font-size: 75%;\n }\n}\n\nh1, .h1 { font-size: @font-size-h1; }\nh2, .h2 { font-size: @font-size-h2; }\nh3, .h3 { font-size: @font-size-h3; }\nh4, .h4 { font-size: @font-size-h4; }\nh5, .h5 { font-size: @font-size-h5; }\nh6, .h6 { font-size: @font-size-h6; }\n\n\n// Body text\n// -------------------------\n\np {\n margin: 0 0 (@line-height-computed / 2);\n}\n\n.lead {\n margin-bottom: @line-height-computed;\n font-size: floor((@font-size-base * 1.15));\n font-weight: 300;\n line-height: 1.4;\n\n @media (min-width: @screen-sm-min) {\n font-size: (@font-size-base * 1.5);\n }\n}\n\n\n// Emphasis & misc\n// -------------------------\n\n// Ex: (12px small font / 14px base font) * 100% = about 85%\nsmall,\n.small {\n font-size: floor((100% * @font-size-small / @font-size-base));\n}\n\nmark,\n.mark {\n background-color: @state-warning-bg;\n padding: .2em;\n}\n\n// Alignment\n.text-left { text-align: left; }\n.text-right { text-align: right; }\n.text-center { text-align: center; }\n.text-justify { text-align: justify; }\n.text-nowrap { white-space: nowrap; }\n\n// Transformation\n.text-lowercase { text-transform: lowercase; }\n.text-uppercase { text-transform: uppercase; }\n.text-capitalize { text-transform: capitalize; }\n\n// Contextual colors\n.text-muted {\n color: @text-muted;\n}\n.text-primary {\n .text-emphasis-variant(@brand-primary);\n}\n.text-success {\n .text-emphasis-variant(@state-success-text);\n}\n.text-info {\n .text-emphasis-variant(@state-info-text);\n}\n.text-warning {\n .text-emphasis-variant(@state-warning-text);\n}\n.text-danger {\n .text-emphasis-variant(@state-danger-text);\n}\n\n// Contextual backgrounds\n// For now we'll leave these alongside the text classes until v4 when we can\n// safely shift things around (per SemVer rules).\n.bg-primary {\n // Given the contrast here, this is the only class to have its color inverted\n // automatically.\n color: #fff;\n .bg-variant(@brand-primary);\n}\n.bg-success {\n .bg-variant(@state-success-bg);\n}\n.bg-info {\n .bg-variant(@state-info-bg);\n}\n.bg-warning {\n .bg-variant(@state-warning-bg);\n}\n.bg-danger {\n .bg-variant(@state-danger-bg);\n}\n\n\n// Page header\n// -------------------------\n\n.page-header {\n padding-bottom: ((@line-height-computed / 2) - 1);\n margin: (@line-height-computed * 2) 0 @line-height-computed;\n border-bottom: 1px solid @page-header-border-color;\n}\n\n\n// Lists\n// -------------------------\n\n// Unordered and Ordered lists\nul,\nol {\n margin-top: 0;\n margin-bottom: (@line-height-computed / 2);\n ul,\n ol {\n margin-bottom: 0;\n }\n}\n\n// List options\n\n// Unstyled keeps list items block level, just removes default browser padding and list-style\n.list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n\n// Inline turns list items into inline-block\n.list-inline {\n .list-unstyled();\n margin-left: -5px;\n\n > li {\n display: inline-block;\n padding-left: 5px;\n padding-right: 5px;\n }\n}\n\n// Description Lists\ndl {\n margin-top: 0; // Remove browser default\n margin-bottom: @line-height-computed;\n}\ndt,\ndd {\n line-height: @line-height-base;\n}\ndt {\n font-weight: bold;\n}\ndd {\n margin-left: 0; // Undo browser default\n}\n\n// Horizontal description lists\n//\n// Defaults to being stacked without any of the below styles applied, until the\n// grid breakpoint is reached (default of ~768px).\n\n.dl-horizontal {\n dd {\n &:extend(.clearfix all); // Clear the floated `dt` if an empty `dd` is present\n }\n\n @media (min-width: @grid-float-breakpoint) {\n dt {\n float: left;\n width: (@dl-horizontal-offset - 20);\n clear: left;\n text-align: right;\n .text-overflow();\n }\n dd {\n margin-left: @dl-horizontal-offset;\n }\n }\n}\n\n\n// Misc\n// -------------------------\n\n// Abbreviations and acronyms\nabbr[title],\n// Add data-* attribute to help out our tooltip plugin, per https://github.com/twbs/bootstrap/issues/5257\nabbr[data-original-title] {\n cursor: help;\n border-bottom: 1px dotted @abbr-border-color;\n}\n.initialism {\n font-size: 90%;\n .text-uppercase();\n}\n\n// Blockquotes\nblockquote {\n padding: (@line-height-computed / 2) @line-height-computed;\n margin: 0 0 @line-height-computed;\n font-size: @blockquote-font-size;\n border-left: 5px solid @blockquote-border-color;\n\n p,\n ul,\n ol {\n &:last-child {\n margin-bottom: 0;\n }\n }\n\n // Note: Deprecated small and .small as of v3.1.0\n // Context: https://github.com/twbs/bootstrap/issues/11660\n footer,\n small,\n .small {\n display: block;\n font-size: 80%; // back to default font-size\n line-height: @line-height-base;\n color: @blockquote-small-color;\n\n &:before {\n content: '\\2014 \\00A0'; // em dash, nbsp\n }\n }\n}\n\n// Opposite alignment of blockquote\n//\n// Heads up: `blockquote.pull-right` has been deprecated as of v3.1.0.\n.blockquote-reverse,\nblockquote.pull-right {\n padding-right: 15px;\n padding-left: 0;\n border-right: 5px solid @blockquote-border-color;\n border-left: 0;\n text-align: right;\n\n // Account for citation\n footer,\n small,\n .small {\n &:before { content: ''; }\n &:after {\n content: '\\00A0 \\2014'; // nbsp, em dash\n }\n }\n}\n\n// Addresses\naddress {\n margin-bottom: @line-height-computed;\n font-style: normal;\n line-height: @line-height-base;\n}\n","// Typography\n\n.text-emphasis-variant(@color) {\n color: @color;\n a&:hover {\n color: darken(@color, 10%);\n }\n}\n","// Contextual backgrounds\n\n.bg-variant(@color) {\n background-color: @color;\n a&:hover {\n background-color: darken(@color, 10%);\n }\n}\n","// Text overflow\n// Requires inline-block or block for proper styling\n\n.text-overflow() {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n","//\n// Code (inline and block)\n// --------------------------------------------------\n\n\n// Inline and block code styles\ncode,\nkbd,\npre,\nsamp {\n font-family: @font-family-monospace;\n}\n\n// Inline code\ncode {\n padding: 2px 4px;\n font-size: 90%;\n color: @code-color;\n background-color: @code-bg;\n border-radius: @border-radius-base;\n}\n\n// User input typically entered via keyboard\nkbd {\n padding: 2px 4px;\n font-size: 90%;\n color: @kbd-color;\n background-color: @kbd-bg;\n border-radius: @border-radius-small;\n box-shadow: inset 0 -1px 0 rgba(0,0,0,.25);\n\n kbd {\n padding: 0;\n font-size: 100%;\n font-weight: bold;\n box-shadow: none;\n }\n}\n\n// Blocks of code\npre {\n display: block;\n padding: ((@line-height-computed - 1) / 2);\n margin: 0 0 (@line-height-computed / 2);\n font-size: (@font-size-base - 1); // 14px to 13px\n line-height: @line-height-base;\n word-break: break-all;\n word-wrap: break-word;\n color: @pre-color;\n background-color: @pre-bg;\n border: 1px solid @pre-border-color;\n border-radius: @border-radius-base;\n\n // Account for some code outputs that place code tags in pre tags\n code {\n padding: 0;\n font-size: inherit;\n color: inherit;\n white-space: pre-wrap;\n background-color: transparent;\n border-radius: 0;\n }\n}\n\n// Enable scrollable blocks of code\n.pre-scrollable {\n max-height: @pre-scrollable-max-height;\n overflow-y: scroll;\n}\n","//\n// Grid system\n// --------------------------------------------------\n\n\n// Container widths\n//\n// Set the container width, and override it for fixed navbars in media queries.\n\n.container {\n .container-fixed();\n\n @media (min-width: @screen-sm-min) {\n width: @container-sm;\n }\n @media (min-width: @screen-md-min) {\n width: @container-md;\n }\n @media (min-width: @screen-lg-min) {\n width: @container-lg;\n }\n}\n\n\n// Fluid container\n//\n// Utilizes the mixin meant for fixed width containers, but without any defined\n// width for fluid, full width layouts.\n\n.container-fluid {\n .container-fixed();\n}\n\n\n// Row\n//\n// Rows contain and clear the floats of your columns.\n\n.row {\n .make-row();\n}\n\n\n// Columns\n//\n// Common styles for small and large grid columns\n\n.make-grid-columns();\n\n\n// Extra small grid\n//\n// Columns, offsets, pushes, and pulls for extra small devices like\n// smartphones.\n\n.make-grid(xs);\n\n\n// Small grid\n//\n// Columns, offsets, pushes, and pulls for the small device range, from phones\n// to tablets.\n\n@media (min-width: @screen-sm-min) {\n .make-grid(sm);\n}\n\n\n// Medium grid\n//\n// Columns, offsets, pushes, and pulls for the desktop device range.\n\n@media (min-width: @screen-md-min) {\n .make-grid(md);\n}\n\n\n// Large grid\n//\n// Columns, offsets, pushes, and pulls for the large desktop device range.\n\n@media (min-width: @screen-lg-min) {\n .make-grid(lg);\n}\n","// Grid system\n//\n// Generate semantic grid columns with these mixins.\n\n// Centered container element\n.container-fixed(@gutter: @grid-gutter-width) {\n margin-right: auto;\n margin-left: auto;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n &:extend(.clearfix all);\n}\n\n// Creates a wrapper for a series of columns\n.make-row(@gutter: @grid-gutter-width) {\n margin-left: (@gutter / -2);\n margin-right: (@gutter / -2);\n &:extend(.clearfix all);\n}\n\n// Generate the extra small columns\n.make-xs-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n float: left;\n width: percentage((@columns / @grid-columns));\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n}\n.make-xs-column-offset(@columns) {\n margin-left: percentage((@columns / @grid-columns));\n}\n.make-xs-column-push(@columns) {\n left: percentage((@columns / @grid-columns));\n}\n.make-xs-column-pull(@columns) {\n right: percentage((@columns / @grid-columns));\n}\n\n// Generate the small columns\n.make-sm-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n\n @media (min-width: @screen-sm-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-offset(@columns) {\n @media (min-width: @screen-sm-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-push(@columns) {\n @media (min-width: @screen-sm-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-pull(@columns) {\n @media (min-width: @screen-sm-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n\n// Generate the medium columns\n.make-md-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n\n @media (min-width: @screen-md-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-offset(@columns) {\n @media (min-width: @screen-md-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-push(@columns) {\n @media (min-width: @screen-md-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-pull(@columns) {\n @media (min-width: @screen-md-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n\n// Generate the large columns\n.make-lg-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n\n @media (min-width: @screen-lg-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-offset(@columns) {\n @media (min-width: @screen-lg-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-push(@columns) {\n @media (min-width: @screen-lg-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-pull(@columns) {\n @media (min-width: @screen-lg-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n","// Framework grid generation\n//\n// Used only by Bootstrap to generate the correct number of grid classes given\n// any value of `@grid-columns`.\n\n.make-grid-columns() {\n // Common styles for all sizes of grid columns, widths 1-12\n .col(@index) { // initial\n @item: ~\".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}\";\n .col((@index + 1), @item);\n }\n .col(@index, @list) when (@index =< @grid-columns) { // general; \"=<\" isn't a typo\n @item: ~\".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}\";\n .col((@index + 1), ~\"@{list}, @{item}\");\n }\n .col(@index, @list) when (@index > @grid-columns) { // terminal\n @{list} {\n position: relative;\n // Prevent columns from collapsing when empty\n min-height: 1px;\n // Inner gutter via padding\n padding-left: (@grid-gutter-width / 2);\n padding-right: (@grid-gutter-width / 2);\n }\n }\n .col(1); // kickstart it\n}\n\n.float-grid-columns(@class) {\n .col(@index) { // initial\n @item: ~\".col-@{class}-@{index}\";\n .col((@index + 1), @item);\n }\n .col(@index, @list) when (@index =< @grid-columns) { // general\n @item: ~\".col-@{class}-@{index}\";\n .col((@index + 1), ~\"@{list}, @{item}\");\n }\n .col(@index, @list) when (@index > @grid-columns) { // terminal\n @{list} {\n float: left;\n }\n }\n .col(1); // kickstart it\n}\n\n.calc-grid-column(@index, @class, @type) when (@type = width) and (@index > 0) {\n .col-@{class}-@{index} {\n width: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = push) and (@index > 0) {\n .col-@{class}-push-@{index} {\n left: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = push) and (@index = 0) {\n .col-@{class}-push-0 {\n left: auto;\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = pull) and (@index > 0) {\n .col-@{class}-pull-@{index} {\n right: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = pull) and (@index = 0) {\n .col-@{class}-pull-0 {\n right: auto;\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = offset) {\n .col-@{class}-offset-@{index} {\n margin-left: percentage((@index / @grid-columns));\n }\n}\n\n// Basic looping in LESS\n.loop-grid-columns(@index, @class, @type) when (@index >= 0) {\n .calc-grid-column(@index, @class, @type);\n // next iteration\n .loop-grid-columns((@index - 1), @class, @type);\n}\n\n// Create grid for specific class\n.make-grid(@class) {\n .float-grid-columns(@class);\n .loop-grid-columns(@grid-columns, @class, width);\n .loop-grid-columns(@grid-columns, @class, pull);\n .loop-grid-columns(@grid-columns, @class, push);\n .loop-grid-columns(@grid-columns, @class, offset);\n}\n","//\n// Tables\n// --------------------------------------------------\n\n\ntable {\n background-color: @table-bg;\n}\ncaption {\n padding-top: @table-cell-padding;\n padding-bottom: @table-cell-padding;\n color: @text-muted;\n text-align: left;\n}\nth {\n text-align: left;\n}\n\n\n// Baseline styles\n\n.table {\n width: 100%;\n max-width: 100%;\n margin-bottom: @line-height-computed;\n // Cells\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n padding: @table-cell-padding;\n line-height: @line-height-base;\n vertical-align: top;\n border-top: 1px solid @table-border-color;\n }\n }\n }\n // Bottom align for column headings\n > thead > tr > th {\n vertical-align: bottom;\n border-bottom: 2px solid @table-border-color;\n }\n // Remove top border from thead by default\n > caption + thead,\n > colgroup + thead,\n > thead:first-child {\n > tr:first-child {\n > th,\n > td {\n border-top: 0;\n }\n }\n }\n // Account for multiple tbody instances\n > tbody + tbody {\n border-top: 2px solid @table-border-color;\n }\n\n // Nesting\n .table {\n background-color: @body-bg;\n }\n}\n\n\n// Condensed table w/ half padding\n\n.table-condensed {\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n padding: @table-condensed-cell-padding;\n }\n }\n }\n}\n\n\n// Bordered version\n//\n// Add borders all around the table and between all the columns.\n\n.table-bordered {\n border: 1px solid @table-border-color;\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n border: 1px solid @table-border-color;\n }\n }\n }\n > thead > tr {\n > th,\n > td {\n border-bottom-width: 2px;\n }\n }\n}\n\n\n// Zebra-striping\n//\n// Default zebra-stripe styles (alternating gray and transparent backgrounds)\n\n.table-striped {\n > tbody > tr:nth-of-type(odd) {\n background-color: @table-bg-accent;\n }\n}\n\n\n// Hover effect\n//\n// Placed here since it has to come after the potential zebra striping\n\n.table-hover {\n > tbody > tr:hover {\n background-color: @table-bg-hover;\n }\n}\n\n\n// Table cell sizing\n//\n// Reset default table behavior\n\ntable col[class*=\"col-\"] {\n position: static; // Prevent border hiding in Firefox and IE9-11 (see https://github.com/twbs/bootstrap/issues/11623)\n float: none;\n display: table-column;\n}\ntable {\n td,\n th {\n &[class*=\"col-\"] {\n position: static; // Prevent border hiding in Firefox and IE9-11 (see https://github.com/twbs/bootstrap/issues/11623)\n float: none;\n display: table-cell;\n }\n }\n}\n\n\n// Table backgrounds\n//\n// Exact selectors below required to override `.table-striped` and prevent\n// inheritance to nested tables.\n\n// Generate the contextual variants\n.table-row-variant(active; @table-bg-active);\n.table-row-variant(success; @state-success-bg);\n.table-row-variant(info; @state-info-bg);\n.table-row-variant(warning; @state-warning-bg);\n.table-row-variant(danger; @state-danger-bg);\n\n\n// Responsive tables\n//\n// Wrap your tables in `.table-responsive` and we'll make them mobile friendly\n// by enabling horizontal scrolling. Only applies <768px. Everything above that\n// will display normally.\n\n.table-responsive {\n overflow-x: auto;\n min-height: 0.01%; // Workaround for IE9 bug (see https://github.com/twbs/bootstrap/issues/14837)\n\n @media screen and (max-width: @screen-xs-max) {\n width: 100%;\n margin-bottom: (@line-height-computed * 0.75);\n overflow-y: hidden;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n border: 1px solid @table-border-color;\n\n // Tighten up spacing\n > .table {\n margin-bottom: 0;\n\n // Ensure the content doesn't wrap\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n white-space: nowrap;\n }\n }\n }\n }\n\n // Special overrides for the bordered tables\n > .table-bordered {\n border: 0;\n\n // Nuke the appropriate borders so that the parent can handle them\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th:first-child,\n > td:first-child {\n border-left: 0;\n }\n > th:last-child,\n > td:last-child {\n border-right: 0;\n }\n }\n }\n\n // Only nuke the last row's bottom-border in `tbody` and `tfoot` since\n // chances are there will be only one `tr` in a `thead` and that would\n // remove the border altogether.\n > tbody,\n > tfoot {\n > tr:last-child {\n > th,\n > td {\n border-bottom: 0;\n }\n }\n }\n\n }\n }\n}\n","// Tables\n\n.table-row-variant(@state; @background) {\n // Exact selectors below required to override `.table-striped` and prevent\n // inheritance to nested tables.\n .table > thead > tr,\n .table > tbody > tr,\n .table > tfoot > tr {\n > td.@{state},\n > th.@{state},\n &.@{state} > td,\n &.@{state} > th {\n background-color: @background;\n }\n }\n\n // Hover states for `.table-hover`\n // Note: this is not available for cells or rows within `thead` or `tfoot`.\n .table-hover > tbody > tr {\n > td.@{state}:hover,\n > th.@{state}:hover,\n &.@{state}:hover > td,\n &:hover > .@{state},\n &.@{state}:hover > th {\n background-color: darken(@background, 5%);\n }\n }\n}\n","//\n// Forms\n// --------------------------------------------------\n\n\n// Normalize non-controls\n//\n// Restyle and baseline non-control form elements.\n\nfieldset {\n padding: 0;\n margin: 0;\n border: 0;\n // Chrome and Firefox set a `min-width: min-content;` on fieldsets,\n // so we reset that to ensure it behaves more like a standard block element.\n // See https://github.com/twbs/bootstrap/issues/12359.\n min-width: 0;\n}\n\nlegend {\n display: block;\n width: 100%;\n padding: 0;\n margin-bottom: @line-height-computed;\n font-size: (@font-size-base * 1.5);\n line-height: inherit;\n color: @legend-color;\n border: 0;\n border-bottom: 1px solid @legend-border-color;\n}\n\nlabel {\n display: inline-block;\n max-width: 100%; // Force IE8 to wrap long content (see https://github.com/twbs/bootstrap/issues/13141)\n margin-bottom: 5px;\n font-weight: bold;\n}\n\n\n// Normalize form controls\n//\n// While most of our form styles require extra classes, some basic normalization\n// is required to ensure optimum display with or without those classes to better\n// address browser inconsistencies.\n\n// Override content-box in Normalize (* isn't specific enough)\ninput[type=\"search\"] {\n .box-sizing(border-box);\n}\n\n// Position radios and checkboxes better\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n margin: 4px 0 0;\n margin-top: 1px \\9; // IE8-9\n line-height: normal;\n}\n\n// Set the height of file controls to match text inputs\ninput[type=\"file\"] {\n display: block;\n}\n\n// Make range inputs behave like textual form controls\ninput[type=\"range\"] {\n display: block;\n width: 100%;\n}\n\n// Make multiple select elements height not fixed\nselect[multiple],\nselect[size] {\n height: auto;\n}\n\n// Focus for file, radio, and checkbox\ninput[type=\"file\"]:focus,\ninput[type=\"radio\"]:focus,\ninput[type=\"checkbox\"]:focus {\n .tab-focus();\n}\n\n// Adjust output element\noutput {\n display: block;\n padding-top: (@padding-base-vertical + 1);\n font-size: @font-size-base;\n line-height: @line-height-base;\n color: @input-color;\n}\n\n\n// Common form controls\n//\n// Shared size and type resets for form controls. Apply `.form-control` to any\n// of the following form controls:\n//\n// select\n// textarea\n// input[type=\"text\"]\n// input[type=\"password\"]\n// input[type=\"datetime\"]\n// input[type=\"datetime-local\"]\n// input[type=\"date\"]\n// input[type=\"month\"]\n// input[type=\"time\"]\n// input[type=\"week\"]\n// input[type=\"number\"]\n// input[type=\"email\"]\n// input[type=\"url\"]\n// input[type=\"search\"]\n// input[type=\"tel\"]\n// input[type=\"color\"]\n\n.form-control {\n display: block;\n width: 100%;\n height: @input-height-base; // Make inputs at least the height of their button counterpart (base line-height + padding + border)\n padding: @padding-base-vertical @padding-base-horizontal;\n font-size: @font-size-base;\n line-height: @line-height-base;\n color: @input-color;\n background-color: @input-bg;\n background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214\n border: 1px solid @input-border;\n border-radius: @input-border-radius; // Note: This has no effect on s in CSS.\n .box-shadow(inset 0 1px 1px rgba(0,0,0,.075));\n .transition(~\"border-color ease-in-out .15s, box-shadow ease-in-out .15s\");\n\n // Customize the `:focus` state to imitate native WebKit styles.\n .form-control-focus();\n\n // Placeholder\n .placeholder();\n\n // Disabled and read-only inputs\n //\n // HTML5 says that controls under a fieldset > legend:first-child won't be\n // disabled if the fieldset is disabled. Due to implementation difficulty, we\n // don't honor that edge case; we style them as disabled anyway.\n &[disabled],\n &[readonly],\n fieldset[disabled] & {\n background-color: @input-bg-disabled;\n opacity: 1; // iOS fix for unreadable disabled content; see https://github.com/twbs/bootstrap/issues/11655\n }\n\n &[disabled],\n fieldset[disabled] & {\n cursor: @cursor-disabled;\n }\n\n // Reset height for `textarea`s\n textarea& {\n height: auto;\n }\n}\n\n\n// Search inputs in iOS\n//\n// This overrides the extra rounded corners on search inputs in iOS so that our\n// `.form-control` class can properly style them. Note that this cannot simply\n// be added to `.form-control` as it's not specific enough. For details, see\n// https://github.com/twbs/bootstrap/issues/11586.\n\ninput[type=\"search\"] {\n -webkit-appearance: none;\n}\n\n\n// Special styles for iOS temporal inputs\n//\n// In Mobile Safari, setting `display: block` on temporal inputs causes the\n// text within the input to become vertically misaligned. As a workaround, we\n// set a pixel line-height that matches the given height of the input, but only\n// for Safari. See https://bugs.webkit.org/show_bug.cgi?id=139848\n\n@media screen and (-webkit-min-device-pixel-ratio: 0) {\n input[type=\"date\"],\n input[type=\"time\"],\n input[type=\"datetime-local\"],\n input[type=\"month\"] {\n line-height: @input-height-base;\n\n &.input-sm,\n .input-group-sm & {\n line-height: @input-height-small;\n }\n\n &.input-lg,\n .input-group-lg & {\n line-height: @input-height-large;\n }\n }\n}\n\n\n// Form groups\n//\n// Designed to help with the organization and spacing of vertical forms. For\n// horizontal forms, use the predefined grid classes.\n\n.form-group {\n margin-bottom: @form-group-margin-bottom;\n}\n\n\n// Checkboxes and radios\n//\n// Indent the labels to position radios/checkboxes as hanging controls.\n\n.radio,\n.checkbox {\n position: relative;\n display: block;\n margin-top: 10px;\n margin-bottom: 10px;\n\n label {\n min-height: @line-height-computed; // Ensure the input doesn't jump when there is no text\n padding-left: 20px;\n margin-bottom: 0;\n font-weight: normal;\n cursor: pointer;\n }\n}\n.radio input[type=\"radio\"],\n.radio-inline input[type=\"radio\"],\n.checkbox input[type=\"checkbox\"],\n.checkbox-inline input[type=\"checkbox\"] {\n position: absolute;\n margin-left: -20px;\n margin-top: 4px \\9;\n}\n\n.radio + .radio,\n.checkbox + .checkbox {\n margin-top: -5px; // Move up sibling radios or checkboxes for tighter spacing\n}\n\n// Radios and checkboxes on same line\n.radio-inline,\n.checkbox-inline {\n position: relative;\n display: inline-block;\n padding-left: 20px;\n margin-bottom: 0;\n vertical-align: middle;\n font-weight: normal;\n cursor: pointer;\n}\n.radio-inline + .radio-inline,\n.checkbox-inline + .checkbox-inline {\n margin-top: 0;\n margin-left: 10px; // space out consecutive inline controls\n}\n\n// Apply same disabled cursor tweak as for inputs\n// Some special care is needed because